본문 바로가기

Data Scientist

판다스 한번에 정리하기

반응형

 

 

♧ Pandas란?

  • 데이터를 효과적으로 처리하고, 보여줄 수 있도록 도와주는 라이브러리

  • Numpy와 함께 사용되어 다양한 연계적인 기능을 제공

  • 인덱스에 따라 데이터를 나열하므로 사전(Dictionary) 자료형에 가까움

  • 시리즈(Series)를 기본적인 자료형으로 사용

# Series: 시리즈는 인덱스와 값으로 구성됨 (칼럼이 하나)

# DataFrame: 다수의 Series를 모아 처리하기 위한 목적으로 사용 (칼럼이 여러개)

 표 형태로 데이터를 손쉽게 출력하고자 할 때 사용 가능

 

 

♧ 파일 읽어오기

다양한 포맷으로 된 파일을 DataFrame으로 로딩할 수 있다.

  read_csv( )  /  read_table( )  /  read_fwf( )  

read_csv와 read_table의 차이는 필드 구분 문자(delimiter)가 콤마( , )인지, 탭( \t ) 인지 차이다. (큰차이x)

read_csv는 csv뿐만 아니라 다른 구분 문자 기반의 파일 포맷도 DataFrame으로 변환 가능하다. 

read_csv의 인자인 sep에 해당 구분 문자를 입력하면 된다. 

ex) 탭으로 구분: read_csv('파일경로, 파일명', sep='\t')

* encoding = ('utf-8' , 'euc-kr', 'cp949') 가끔 불러올 때 써줘야하는 경우가 있다.
  쓸 경우 read_csv('c:/ex.csv', encoding='utf-8') 이런 식으로 뒤에 붙여 써주면 된다. 

sep인자 생략시 자동으로 콤마로 할당된다. 가끔 콤마로 구분이 아닌 ';' 세미콜론 등으로 구분된 파일 들이 있다. 

여기선 타이타닉 데이터셋을 불러올 것이다. 책에서 그랬다.. (타이타닉 데이터는 kaggle에서 다운받을 수 있다, 가입 필요 https://www.kaggle.com/c/titanic/data)

Data Dictionary

VariableDefinitionKey

survival Survival 0 = No, 1 = Yes
pclass Ticket class 1 = 1st, 2 = 2nd, 3 = 3rd
sex Sex
Age Age in years
sibsp # of siblings / spouses aboard the Titanic
parch # of parents / children aboard the Titanic
ticket Ticket number
fare Passenger fare
cabin Cabin number
embarked Port of Embarkation C = Cherbourg, Q = Queenstown, S = Southampton

Variable Notes

pclass: A proxy for socio-economic status (SES)
1st = Upper
2nd = Middle
3rd = Lower

age: Age is fractional if less than 1. If the age is estimated, is it in the form of xx.5

sibsp: The dataset defines family relations in this way...
Sibling = brother, sister, stepbrother, stepsister
Spouse = husband, wife (mistresses and fiancés were ignored)

parch: The dataset defines family relations in this way...
Parent = mother, father
Child = daughter, son, stepdaughter, stepson
Some children travelled only with a nanny, therefore parch=0 for them.

 

분석하기에 앞서 컬럼이 무엇인지 간략한 내용들이다. 설명을 안해줘서 sibsp같은건 이건 뭐야 십습..?십습키?;;

아무튼 간단하게 분석하기에 앞서 데이터의 열들이 무엇을 의미하는지 그런 것들은 확인하는 것이 좋다. 

그래야 변수끼리 비교하거나 분석할 때 유의미한 결과를 도출할 수 있다. 

df = pd.read_csv('./train.csv')
print(df.shape)
df.head()

(891, 12)

 

파일을 불러와서 df라는 이름으로 할당해준다. 

그리고 파일의 형태(shape)를 출력하면 891행 12열인 것을 확인할 수 있다. 

그리고 상위 5개만을 출력하기 위해 head를 사용한다. 괄호 안에 숫자를 넣어주면 그 숫자만큼 출력된다. 

df.info() #총 데이터 건수와 타입, Null 건수

<class 'pandas.core.frame.DataFrame'>
RangeIndex: 891 entries, 0 to 890
Data columns (total 12 columns):
 #   Column       Non-Null Count  Dtype  
---  ------       --------------  -----  
 0   PassengerId  891 non-null    int64  
 1   Survived     891 non-null    int64  
 2   Pclass       891 non-null    int64  
 3   Name         891 non-null    object 
 4   Sex          891 non-null    object 
 5   Age          714 non-null    float64
 6   SibSp        891 non-null    int64  
 7   Parch        891 non-null    int64  
 8   Ticket       891 non-null    object 
 9   Fare         891 non-null    float64
 10  Cabin        204 non-null    object 
 11  Embarked     889 non-null    object 
dtypes: float64(2), int64(5), object(5)
memory usage: 83.7+ KB

info를 통해 정보를 요약해서 볼 수 있다. 데이터 타입과 결측값들이 얼마나 존재하는지 한 눈에 파악할 수 있다. 

describe은 통계적인 수치들을 계산해서 출력해준다. 갯수, 평균, 표준편차, 최소값, 분위값이 출력된다. 

df.describe() #오직 숫자형 칼럼의 분포도만 조사, n-percentile분포도, 평균, 최대값, 최소값 
#count는 not-null의 데이터 건수
#mean은 전체 데이터 평균값 

df['Pclass'].value_counts()
# 데이터의 분포도를 확인하는데 매우 유용한 함수

3    491
1    216
2    184
Name: Pclass, dtype: int64

--------------------------------------------------

df_pclass = df['Pclass']
print(type(df_pclass))
print(df_pclass.head())

<class 'pandas.core.series.Series'>
0    3
1    1
2    3
3    1
4    3
Name: Pclass, dtype: int64

value.counts는 df 중에서 'Pclass'컬럼의 값들의 갯수를 출력하는 함수다. 

Pclass의 경우 3등급이 491, 1등급이 216, 2등급이 184인 것을 확인할 수 있다. 

아래 쪽은 Pclass컬럼만을 df_pclass로 다시 정해주고 출력한 모습인데, 왼쪽 열은 인덱스번호이고 오른쪽은 값들이다.

 

♧ DataFrame과 리스트, 딕셔너리, 넘파이 ndarray 상호변환

기본적으로 DataFrame은 파이썬의 리스트, 딕셔너리 그리고 넘파이 ndarray 등 다양한 데이터로부터 생성될 수 있다.

DataFrame은 리스트와 넘파이 ndarray와 다르게 칼럼명을 가지고 있어서 상대적으로 핸들링이 편하다고 한다.

(내겐 해당사항이 없었다..어렵다....)

일반적으로 DataFrame으로 변환 시 이 칼럼명을 지정해 주고 지정안하면 자동으로 할당한다. 

 

리스트와 ndarray를 DataFrame으로 변환하기

import numpy as np

col_name1 = ['col1']
list1 = [1, 2, 3]
array1 = np.array(list1)
print('array1 shape: ', array1.shape)

# 리스트를 이용해 DataFrame 생성
df_list = pd.DataFrame(list1, columns=col_name1)
print('1차원 리스트로 만든 DataFrame: \n', df_list)

# 넘파이 ndarray를 이용해 DataFrame 생성
df_array1 = pd.DataFrame(array1, columns = col_name1)
print('1차원 ndarray로 만든 DataFrame\n', df_array1)

========================================================
array1 shape:  (3,)
1차원 리스트로 만든 DataFrame: 
    col1
0     1
1     2
2     3

1차원 ndarray로 만든 DataFrame
    col1
0     1
1     2
2     3

DataFrame은 컬럼명을 가지고 있고 행과 열을 가지는 2차원 데이터이다. 

따라서 2차원 이하의 데이터들만 DataFrame으로 변환 가능하다. 

위에 컬럼명을 한개만 준 이유는 1차원 데이터이기 때문이다. [열의 갯수에 맞게 준다] (shape을 보면 3, 로 되어있다)

다음은 2x3형태의 데이터프레임을 만드는 것인데 3열이기 때문에 컬럼명이 3개 필요하다.

# 2행 3열 형태의 리스트와 ndarray를 기반으로 DataFrame 생성 -> 컬럼명 3개 필요
col_name2 = ['col1', 'col2', 'col3']

# 2x3형태의 리스트와 ndarray 생성 후 DataFrame으로 변환
list2 = [[1,2,3],[4,5,6]]  # list생성
array2 = np.array(list2)   # ndarray 생성
print('array2 shape:', array2.shape)
print()
df_list2 = pd.DataFrame(list2,columns=col_name2) # 리스트로 DF생성
print('2차원 리스트로 만든 DataFrame:\n', df_list2)

df_array2 = pd.DataFrame(array2, columns=col_name2) # 배열로 DF생성
print('2차원 ndarray로 만든 DataFrame:\n', df_array2)

================================================================================

array2 shape: (2, 3)

2차원 리스트로 만든 DataFrame:
    col1  col2  col3
0     1     2     3
1     4     5     6
2차원 ndarray로 만든 DataFrame:
    col1  col2  col3
0     1     2     3
1     4     5     6

 

딕셔너리를 DataFrame으로 변환하기

일반적으로 딕셔너리를 DataFrame으로 변환 시에는 딕셔너리의 키(key)는 컬럼명으로, 딕셔너리의 값(Value)는 해당하는 컬럼 데이터로 변환된다. 따라서 키의 경우는 문자열, 값의 경우는 리스트(또는 ndarray) 형태로 딕셔너리를 구성한다.

# key는 문자열 컬럼명으로 매핑, Value는 리스트형(또는 ndarray) 컬럼 데이터로 매핑
dict = {'col1':[1, 11], 'col2':[22,22],'col3':[3,33]} # key, value 형태의 딕셔너리
df_dict = pd.DataFrame(dict)
print('딕셔너리로 만든 DataFrame:\n',df_dict)

==================================================================================

딕셔너리로 만든 DataFrame:
    col1  col2  col3
0     1    22     3
1    11    22    33

ndarray, Dictionary -> DataFrame으로 변환하는 것을 해봤으니 반대로도 해보자.

DataFrame -> ndarray

# DataFrame을 ndarray로 변환해보자
array3 = df_dict.values
print('df_dict.values 타입:', type(array3), 'df_dict.values shape:',array3.shape)
print(array3)

array4 = df_array2.values
print('df_array2.values 타입:', type(array4), 'df_array2.values shape:',array4.shape)
print(array4)

=====================================================================================

df_dict.values 타입: <class 'numpy.ndarray'> df_dict.values shape: (2, 3)
[[ 1 22  3]
 [11 22 33]]

df_array2.values 타입: <class 'numpy.ndarray'> df_array2.values shape: (2, 3)
[[1 2 3]
 [4 5 6]]

DataFrame -> list

# DataFrame을 list로 변환해보자
# values로 얻은 ndarray에 tolist()를 호출하면 된다. 
list3 = df_dict.values.tolist()
print('df_dict.values.tolist() 타입:', type(list3))
print(list3)

# 앞서 만든 것들에도 적용해보자
list4 = df_list2.values.tolist()
print('df_list2.values.tolist() 타입:', type(list4))
print(list4)

========================================================

df_dict.values.tolist() 타입: <class 'list'>
[[1, 22, 3], [11, 22, 33]]

df_list2.values.tolist() 타입: <class 'list'>
[[1, 2, 3], [4, 5, 6]]

DataFrame ->Dictionary

# DataFrame을 Dictionary로 변환해보자
dict3 = df_dict.to_dict('list')
print('\n df_dict() 타입:', type(dict3))
print(dict3)
dict4 = df_dict.to_dict()
print(dict4)

======================================================

 df_dict() 타입: <class 'dict'>
{'col1': [1, 11], 'col2': [22, 22], 'col3': [3, 33]}
{'col1': {0: 1, 1: 11}, 'col2': {0: 22, 1: 22}, 'col3': {0: 3, 1: 33}}

to_dict('list')를 해주면 list 형태로 데이터가 출력된다. 빈상태로 놔두면 dict4처럼 key:value값으로 출력된다. 

 

데이터프레임 컬럼 추가하기 & 컬럼끼리 계산 해서 새로운 컬럼 만들기 

# 다시 등장한 타이타닉

df['Age_0']=0 #상수값을 할당하면 모든 데이터셋에 일괄적으로 적용된다. 
df.head(3)

단순히 [ ] 안에 새로운 컬럼명을 써주고 값을 0으로 할당하는 것으로 새로운 컬럼이 생겼다. 

그런데 단일 값으로만 할당된 것을 알 수 있다. 

df['Age_by_10'] = df['Age_0']*10
df['Family_No'] = df['SibSp'] + df['Parch'] +1
df.head(3)

이와같이 새로운 컬럼을 만들면서 컬럼끼리의 값들을 합하거나 곱하는 것 또한 가능하다. 

 

데이터 프레임 삭제하기

삭제하는 것은 drop( ) 메소드를 사용한다. 

DataFrame.drop(labes=None, axis=0, index=None, columns=None, levle=None, inplcae=False, errors='raise')

원본형태는 이와 같고 세가지의 파라미터가 중요하다. 

df_drop = df.drop('Age_0',axis=1)

inplace=False

inplace는 원본데이터까지 영향을 미칠지를 선택할 수가 있다. 

기본값이 inplace=False기 때문에 원본 데이터를 호출하면 Age_0는 그대로 남아있는 것을 확인할 수 있다. 

여기서 df_drop에 drop( )메소드를 사용하였지만 만약 inplace=True를 주었다면 df를 호출하였을 때 Age_0가 사라지는

것을 확인할 수 있다. 

inplcae=True

Index 객체

print(df.index)

# Index 객체를 실제 값 array로 변환
print('Index 객체 array값: \n', df.index.values)
print('type df.values:',type(df.values))
print('index.values.shape:',df.index.values.shape)
print('df.index[:5].values:',df.index[:5].values)
print('df.index.values[:5]:',df.index.values[:5])
print('df.index[6]:',df.index[6])

======================================================

RangeIndex(start=0, stop=891, step=1)  #1
Index 객체 array값:  #4 df.index.values
 [  0   1   2   3   4   5   6   7   8 . . . 891]
 
type df.values: <class 'numpy.ndarray'> 
index.values.shape: (891,)
df.index[:5].values: [0 1 2 3 4]
df.index.values[:5]: [0 1 2 3 4]
df.index[6]: 6

index 객체는 식별성 데이터를 1차원 array로 가지고 있다. 또한 ndarray와 유사하게 단일 값 반환 및 슬라이싱도 가능하다. 하지만 한 번 만들어진 DataaFrame 및 Series의 Index 객체는 함부로 변경할 수 없다.

df.index[0] = 5
====================================================
TypeError: Index does not support mutable operations

Series 객체는 Index 객체를 포함하지만 Series 객체에 연산 함수를 적용할 때 Index는 연산에서 제외된다. 

Index는 오직 식별용으로만 사용된다. > value값들만 계산되고 인데스값들은 고정

array1 = pd.Series([1,2,3], index = ['A', 'B', 'C'])
array2 = pd.Series([4,5,6], index = ['B', 'C', 'D'])
array1 = array1.add(array2) # fill_value 안넣어주면 NaN
print(array1)

A    NaN
B    6.0
C    8.0
D    NaN
dtype: float64
------------------------------------------------------------

array1 = array1.add(array2, fill_value = 0)
print(array1)

A     NaN
B    10.0
C    13.0
D     6.0
dtype: float64

series_fair = df['Fare']
print('Fair Series max 값:', series_fair.max())
print()
print('Fair Series sum 값:', series_fair.sum())
print()
print('sum() Fair Series:', sum(series_fair))
print()
print('Fair Series: \n', series_fair.head(3))
print()
print('Fair Series + 3 :\n', (series_fair + 3).head(3))

==========================================================

Fair Series max 값: 512.3292

Fair Series sum 값: 28693.9493

sum() Fair Series: 28693.949299999967

Fair Series: 
 0     7.2500
1    71.2833
2     7.9250
Name: Fare, dtype: float64

Fair Series + 3 :
 0    10.2500
1    74.2833
2    10.9250
Name: Fare, dtype: float64

DataFrame 연산

array1 = pd.DataFrame([[1,2],[3,4]], index=['A','B'])
array2 = pd.DataFrame([[1,2,3],[4,5,6],[7,8,9]],index=['B','C','D'])
array3 = array1.add(array2, fill_value=0)

array1 + array2 = array3

 

DataFrame 및 Series에 reset_index( ) 메소드를 수행하면 새롭게 인덱스를 연속 숫자 형으로 할당하며 기존 인덱스는

'index'라는 새로운 컬럼 명으로 추가한다. 

초록색 박스 안의 인덱스가 기존 인덱스

print('### before reset_index ###')
value_counts = df['Pclass'].value_counts()
print(value_counts)
print('value_counts 객체 변수 타입:', type(value_counts))
new_value_counts = value_counts.reset_index(inplace=False)
print('### After reset_index ###')
print(new_value_counts)
print('new_value_counts 객체 변수 타입:', type(new_value_counts))

=================================================================

### before reset_index ###
3    491
1    216
2    184
Name: Pclass, dtype: int64
value_counts 객체 변수 타입: <class 'pandas.core.series.Series'>
### After reset_index ###
   index  Pclass
0      3     491
1      1     216
2      2     184
new_value_counts 객체 변수 타입: <class 'pandas.core.frame.DataFrame'>

reset_index( )는 인덱스가 연속된 int 숫자형 데이터가 아닐 경우에 다시 이를 연속 int 숫자형 데이터로

만들 때 주로 사용한다.

 

 

 

예를 들어 Series와 value_counts( )를 소개하는 예제에서 'Pclass' 컬럼 Series의 value_counts( )를 수행하면 'Pclass' 고유 값이 식별자 인덱스 역할을 했다. 

이보다는 연속 숫자형 인덱스가 고유 식별자로 더 적합해

보인다. 

reset_index( )을 이용해 인덱스를 다시 만든 결과과 위와 같다. Series에 reset_index( )를 적용하면 Series가 아닌 DataFrame이 반환된다. 기존 인덱스가 컬럼으로 추가되기 때문에 컬럼이 2개가 되면서 DataFrame이 되는 것이다. 

 

 

 

 

데이터 셀렉션 및 필터링

    - DataFrame의 [ ] 연산자

DataFrame의 바로 뒤에 있는 '[ ]' 안에 들어갈 수 있는 것은 컬럼 명 문자(또는 컬럼 명의 리스트 객체), 

또는 인덱스로 변환 가능한 표현식이다(어려운 개념이라 나중에 설명해준다고 한다). 현재수준(?)에서는 DataFrame

뒤에 있는 [ ]는 컬럼만 지정할 수 있는 '컬럼 지정 연산자'로 이해하는 게 혼돈을 막는 가장 좋은 방법이다...

print('단일 컬럼 데이터 추출:\n', df['Pclass'].head(3))
print('\n 여러 컬럼의 데이터 추출: \n', df[['Survived', 'Pclass']].head(3))
print('[ ] 안에 숫자 index는 key Error 오류 발생:\n', df[0])

==============================================================================

단일 컬럼 데이터 추출:
 0    3
1    1
2    3
Name: Pclass, dtype: int64

 여러 컬럼의 데이터 추출: 
    Survived  Pclass
0         0       3
1         1       1
2         1       3

KeyError: 0

그런데 판다스의 인덱스 형태로 변환 가능한 표현식은 [ ] 내에 입력할 수 있다고 한다. 처음 두개의 데이터를 추출하기 위해 다음과 같이 슬라이싱을 이용할 수 있다. 0부터 2로 했으니 0,1 값이 반환된다. 

df[0:2]

불린 인덱싱 표현도 가능하다. [ ] 내의 불린 인덱싱 기능은 원하는 데이터를 편리하게 추출해주므로 매우 자주 사용된다.

내가 느끼기에도 진짜 많이 사용되고 유용한 것 같다. 

이렇게 해당 컬럼의 값이 3일 경우만 뽑아내서 출력할 수 있다. 'Pclass'가 3에 해당하는 데이터들은 491개로 구성된다. 

  • DataFrame 바로 뒤의 [ ] 연산자는 넘파이의 [ ] 나 Series의 [ ]와는 다르다.
  • DataFrame 바로 뒤의 [ ] 내 입력 값은 컬럼명(또는 컬럼의 리스트)을 지정해 컬럼 지정 연산에 사용하거나 불린 인덱스 용도로만 사용해야 한다.
  • DataFrame[0:2]와 같은 슬라이싱 연산으로 데이터를 추출하는 방법은 사용하지 않는 것이 좋다. 

이어서 ix[ ] 연산자가 있는데 곧 사라지게 될 예정이라 사용하면 warning이 출력된다. 그래서 생략할 것이다.

그런데 왜 책에서 길게 다루었는지 좀 이해가 안간다. 

대신해서 사용하기 위한 연산자로 iloc[ ]loc [ ]가 있다. 

  • 명칭(Label) 기반 인덱싱은 컬럼의 명칭을 기반으로 위치를 지정하는 방식이다.
  • 위치(Position) 기반 인덱싱은 0을 출발점으로 하는 가로축, 세로축 좌표 기반의 행과 열 위치를 기반으로 데이터를 지정한다. 따라서 행, 열 값으로 정수가 입력된다. 

    - iloc[ ] 연산자

iloc[ ]는 위치 기반 인덱싱만 허용(불린인덱스 불가)하기 때문에 행과 열 값으로 integer 또는 integer형의 슬라이싱, 팬시 리스트 값을 입력해줘야 한다(불린 인덱싱은 조건을 기술하므로 이에 제약받지 않는다.) 다음 예시는 첫번째 행, 첫번째 열의 데이터를 추출한 것이다. 

data = {'Name': ['hun', 'won','man','end'],
       'Year':[1993, 1995, 1953, 2002],
       'Gender':['Male','Female','Male','None']}
data_df = pd.DataFrame(data, index=['one', 'two','three', 'four'])
data_df    
# 결과는 오른쪽 DataFrame

data_df.iloc[0, 0] # 행, 렬
'hun'

    - loc[ ] 연산자

loc[ ]는 명칭 기반으로 데이터를 추출한다. 따라서 행 위치에는 DataFrame index 값을, 그리고 열 위치에는 컬럼 명을 입력해준다. 인덱스 값이 'one'인 행의 컬럼 명이 'name'인 데이터를 추출해보자

data_df.loc['one','Name'] #행, 열

'hun'

data_df.loc['one':'four', 'Name']

one      hun
two      won
three    man
four     end
Name: Name, dtype: object

인덱스값이 'one', 'two', 'three', 'four'이기 때문에  행인 인덱스에 'one', 열이름인 'Name'을 입력하면 첫번째 첫열이 

결과값으로 출력된다. 보통은 숫자로 되어있다. loc 연산자도 슬라이싱이 가능해서 one~four까지 범위를 주고 컬럼명을 지정해주면 해당 컬럼의 값들이 출력된다. 

 

    - 복합조건 결합

1. and 조건일 때는 &

2. or 조건일 때는 < shift+ \

3. Not 조건일 때는 ~

나이가 60세 이상이고, 선실 등급이 1등급이며, 성별이 여성인 승객을 추출해보자. 

개별 조건은 ( )로 묶고, 복합 조건 연산자를 사용하면 된다. 

df[(df['Age']>60) & (df['Pclass']==1)&(df['Sex']=='female')]

정렬, Aggregation(집계) 함수, GroupBy 적용

DataFrame, Series의 정렬 - sort_values()

 

DataFrame과 Series의 정렬을 위해서는 sort_values( ) 메소드를 이용한다. sort_values( )는 

RDBMS SQL의 ordey by 키워드와 매우 유사하다고 한다. (Relational DataBase Management System)  

 

sort_values( ) 의 주요 입력 파라미터는 by, ascending, inplace다. 

  • by로 특정 컬럼을 입력하면 해당 컬럼으로 정렬을 수행한다.
  • ascending = True로 설정하면 오름차순으로 정렬하며, False로 설정하면 내림차순이다.
  • inplace = False로 설정하면 sort_values( )를 호출한 DataFrame은 그대로 유지하며 정렬된 DataFrame을 결과로 반환한다.(기본값)
  • inplace = True로 설정하면 호출한 DataaFrame의 정렬 결과를 그대로 적용한다. 
df_sorted = df.sort_values(by=['Name'], inplace = False, ascending = True)
df_sorted.head(3)

이름기준 ascending = True, 오름차순
ascending = False, 내림차순

df.count()

PassengerId    891
Survived       891
Pclass         891
Name           891
Sex            891
Age            714
SibSp          891
Parch          891
Ticket         891
Fare           891
Cabin          204
Embarked       889
dtype: int64

DataFrame에서 바로 aggregation을 호출할 경우 모든 컬럼에 해당 aggregation을 적용한다.

여기선 count( )를 적용해보았는데 모든 컬럼에 count( )결과가 반환된 것을 알 수 있다.

df[['Age','Fare']].mean()

Age     29.699118
Fare    32.204208
dtype: float64

특정 컬럼에 aggregation 함수를 적용하기 위해서는 DataFrame에 대상 컬럼들만 추출해 

aggregation을 적용하면 된다. 

 

    - groupby( ) 적용

DataFrame의 groupby( ) 사용 시 입력 파라미터 by에 컬럼을 입력하면 대상 컬럼으로 groupby 된다.

DataFrame에 groupby( )를 호출하면 DataFrameGroupBy라는 또 다른 형태의 DataFrame을 반환한다. 

df_groupby = df.groupby(by = 'Pclass')
print(type(df))
print(type(df_groupby))

<class 'pandas.core.frame.DataFrame'>
<class 'pandas.core.groupby.generic.DataFrameGroupBy'>

SQL의 group by와 다르게, DataFrame에 groupby( )를 호출해 반환된 결과에 aggregation 함수를 

호출하면 groupby( ) 대상 컬럼을 제외한 모든 컬럼에 해당 aggregation 함수를 적용한다.

df_groupby = df.groupby('Pclass').count()
df_groupby

'Pclass' 컬럼을 제외한 모든 컬럼에 적용된 모습

SQL의 경우 group by 적용 시 여러 개의 컬럼에 aggregation 함수를 호출하려면 대상 컬럼을 모두 select 절에 

나열해야 하는 것이 다르다.

즉 Select count(PassengerId), count(Survived), .... from titanic_table group by Pclass 와 같이 되야 한다.

DataFrame의 groupby( )에 특정 컬럼만 aggregation 함수를 적용하려면 groupby( )로 반환된 DataFrameGroupBy 객체에 해당 컬럼을 필터링한 뒤 aggregation 함수를 적용한다. 

df_groupby = df.groupby('Pclass')[['PassengerId', 'Survived']].count()
df_groupby

 

위의 코드와 오른쪽의 결과값은

df.groupby('Pclass')로 반환된 DataFrameGroupBy 객체에

[['PassengerId', 'Survived']]로 필터링해서 이 두 열에만

count( )함수를 시행한 결과다.

 

DataFrame groupby( )와 SQL의 group by 키워드의 또 다른 차이는 SQL의 경우
서로 다른 aggregation 함수를 적용할 경우에는 Select 절에 나열하기만 하면 되지만
DataFrame groupby( )의 경우 적용하려는
여러 개의 aggregation 함수명을 DataFrameGroupBy 객체의 agg( ) 내에 인자로 입력해서 사용한다는 점이다.

예를 들어 Select max(Age), min(Age) from titanic_table groupby Pclass와 같은 SQL은 다음과 같이 groupby( )로 반환된 DataFrameGroupBy 객체에 agg( )를 적용해 동일하게 구현할 수 있다. 
df.groupby('Pclass')['Age'].agg([max, min])

그런데 이렇게 DataFrame의 groupby( )를 이용해 *API 기반으로 처리하다보니 SQL보다 유연성이 떨어진다. 

예를 들어 여러 개의 컬럼이 서로 다른 aggregation 함수를 groupby에서 호출하려면

SQL은 select max(Age), sum(SibSp), avg(Fare) from titanic_table group by Pclass 이지만

DataFrame groupby( )는 좀 더 복잡한 처리가 필요하다. groupby( )는 agg( )를 이용해 이 같은

처리가 가능한데, agg( ) 내에 입력 값으로 딕셔너리 형태로 aggregation이 적용될 컬럼들과

aggregation 함수를 입력한다. 

 

* API(Application Programming Interface, 응용 프로그램 프로그래밍 인터페이스)는 응용 프로그램에서 사용할 수 있도록, 운영 체제나 프로그래밍 언어가 제공하는 기능을 제어할 수 있게 만든 인터페이스를 뜻한다.
agg_format = {'Age':'max', 'SibSp': 'sum', 'Fare': 'mean'}
df.groupby('Pclass').agg(agg_format)

 

이와 같이 agg( )를 이용해서 여러개의 aggregation 함수의 구현이 가능하다. 

여기선 Age는 최대값, Sibsp는 합계, Fare는 평균으로 미리 포맷을 만들어주고 

Pclass 기준으로 정렬 후 포맷에 따라 집계한 결과값이다. 

 

 

 

다음은 Groupby와 함께 사용할 수 있는 함수들의 예시이다.

# 데이터 프레임의 그룹화 1 (groupby + 요약[sum])
df = pd.DataFrame([
    ['Apple', 7, 'Fruit'],
    ['Banana', 3, 'Fruit'],
    ['Beef', 5, 'Meal'],
    ['Kimch', 4, 'Meal']
], columns = ['Name', 'Frequency', 'Type']) 
print(df)

     Name  Frequency   Type
0   Apple          7  Fruit
1  Banana          3  Fruit
2    Beef          5   Meal
3   Kimch          4   Meal
====================================================================================
print(df.groupby(['Type']).sum()) #type을 기준으로 그룹화하고 합계(숫자만) 출력

		Frequency
Type            
Fruit         10
Meal           9
# 데이터 프레임의 그룹화 2 (groupby + aggregate)
df = pd.DataFrame([
    ['Apple', 7, 5, 'Fruit'],
    ['Banana', 3, 6, 'Fruit'],
    ['Beef', 5, 2, 'Meal'],
    ['Kimch', 4, 8, 'Meal']],
columns = ['Name', 'Frequency', 'Importance', 'Type'])
print(df)

     Name  Frequency  Importance   Type
0   Apple          7           5  Fruit
1  Banana          3           6  Fruit
2    Beef          5           2   Meal
3   Kimch          4           8   Meal

==============================================================

print(df.groupby(['Type']).aggregate([min, max, np.average]))

 Frequency             Importance            
            min max average        min max average
Type                                              
Fruit         3   7     5.0          5   6     5.5
Meal          4   5     4.5          2   8     5.0
# 데이터 프레임의 그룹화 3 (groupby + filter)
df = pd.DataFrame([
    ['Apple', 7, 5, 'Fruit'],
    ['Banana', 3, 6, 'Fruit'],
    ['Beef', 5, 2, 'Meal'],
    ['Kimchi', 4, 8, 'Meal']],
    columns = ['Name','Frequency','Importance','Type'])
def my_filter(data):
    return data['Frequency'].mean() >= 5
df = df.groupby('Type').filter(my_filter)
print(df)

     Name  Frequency  Importance   Type
0   Apple          7           5  Fruit
1  Banana          3           6  Fruit

# 데이터 프레임의 그룹화 4 (groupby + get_group)
df = pd.DataFrame([
    ['Apple', 7, 5, 'Fruit'],
    ['Banana', 3, 6, 'Fruit'],
    ['Beef', 5, 2, 'Meal'],
    ['Kimchi', 4, 8, 'Meal']],
columns = ['Name', 'Frequency', 'Importance','Type'])
df= df.groupby('Type').get_group('Fruit') # Type 중 Fruit만 추출
print(df)

     Name  Frequency  Importance   Type
0   Apple          7           5  Fruit
1  Banana          3           6  Fruit
#데이터 프레임의 그룹화 5 (groupby + apply(lambda))
df = pd.DataFrame([
    ['Apple', 7, 5, 'Fruit'],
    ['Banana', 3, 6, 'Fruit'],
    ['Beef', 5, 2, 'Meal'],
    ['Kimchi', 4, 8, 'Meal']],
columns = ['Name', 'Frequency', 'Importance', 'Type'])
df['Gap'] = df.groupby('Type')['Frequency'].apply(lambda x: x-x.mean()) #x값에서 x평균값 차감
print(df)

     Name  Frequency  Importance   Type  Gap
0   Apple          7           5  Fruit  2.0
1  Banana          3           6  Fruit -2.0
2    Beef          5           2   Meal  0.5
3  Kimchi          4           8   Meal -0.5
# 데이터 프레임의 다중화
df = pd.DataFrame(
np.random.randint(1, 10, (4,4)),   # 4x4크기의 1~9사이의 숫자로 생성 
index = [['1차','1차','2차','2차'],['공격','수비','공격','수비']],
columns = ['1회','2회','3회','4회'])
df

df[['1회','2회']].loc['2차'] # 1,2회 컬럼에 대해 2차만 추출

df와 2차만 추출할 때

# 피벗테이블의 기초
df = pd.DataFrame([
    ['Apple', 7, 5, 'Fruit'],
    ['Banana', 3, 6, 'Fruit'],
    ['Coconut', 2, 6, 'Fruit'],
    ['Rice', 8, 2, 'Meal'],
    ['Beef', 5, 2, 'Meal'],
    ['Kimchi',  4, 8, 'Meal']],
    columns = ['Name', 'Frequency','Importance', 'Type'])
print(df)

      Name  Frequency  Importance   Type
0    Apple          7           5  Fruit
1   Banana          3           6  Fruit
2  Coconut          2           6  Fruit
3     Rice          8           2   Meal
4     Beef          5           2   Meal
5   Kimchi          4           8   Meal

df = df.pivot_table(
    index = 'Importance', columns = 'Type', values = 'Frequency',
    aggfunc = np.max)       # 두 숫자중 더 큰 것을 출력해줘 
print(df)

Type        Fruit  Meal
Importance             
2             NaN   8.0
5             7.0   NaN
6             3.0   NaN
8             NaN   4.0

    - 결손 데이터 처리하기

판다스는 결손 데이터(Missing Data)를 처리하는 편리한 API를 제공한다. 결손 데이터는 컬럼에 값이 없는, 즉 NULL인 경우를 의미하며, 이를 넘파이의 NaN으로 표시한다. 기본저긍로 머신러닝 알고리즘은 이 NaN 값을 처리하지 않으므로 이 값을 다른 값으로 대체해야 한다. 또한 NaN 값은 평균, 총합 등의 함수 연산 시 제외가 된다. 특정 컬럼의 100개 데이터 중 10개가 NaN 값일 경우, 이 컬럼의 평균 값은 90개 데이터에 대한 평균값이다. NaN 여부를 확인하는 API는 isna( )이며, NaN 값을 다른 값으로 대체하는 API는 fillna( )이다. 

 

    - isna( )로 결손 데이터 여부 확인

isna( )는 데이터가 NaN인지 아닌지를 알려준다. DataFrame에 isna( )를 수행하면 모든 컬럼의 값이 NaN인지 아닌지를 True나 False로 알려준다. 

 

 

결손 데이터의 개수는 isna( ) 결과에 sum( ) 함수를 추가해 구할 수 있다. sum( )을 호출 시 True는 내부적 숫자 1로, False는 숫자 0으로 변환되므로 결손 데이터의 개수를 구할 수 있다. 

오른쪽과 같이 출력된 결과를 살펴보면 Age는 177개, Cabin은 687개, Embarked는 2개다. 

 

 

 

 

 

 

    - fillna()로 결손 데이터 대체하기

fillna( )를 이용하면 결손 데이터를 편리하게 다른 값으로 대체할 수 있다. 여기서는 Cabin 컬럼의 NaN값을 'C000'으로 대체했다. 

df['Cabin'] = df['Cabin'].fillna('C000')
df.head(3)

주의해야 할 점은 fillna( )를 이용해 반환 값을 다시 받거나 inplace=True 파라미터를 fillna( )에 추가해야 실제 데이터 세트 값이 변경된다는 점이다. 

1 . df['Cabin'] = df['Cabin].fillna('C000')

2 . df['Cabin'].fillna('C000', inplcae=True)

이렇게 두가지 방법으로 데이터세트 값을 변경할 수 있다. 

df['Age'] = df['Age'].fillna(df['Age'].mean())
df['Embarked'] = df['Embarked'].fillna('S')
df.isna().sum()

PassengerId    0
Survived       0
Pclass         0
Name           0
Sex            0
Age            0
SibSp          0
Parch          0
Ticket         0
Fare           0
Cabin          0
Embarked       0
dtype: int64

결측치가 있었던 나머지 컬럼도 fillna( )를 이용해 제거해주고나면 결측치가 모두 0이 된 것을 확인할 수 있다. 

 

    - apply lambda 식으로 데이터 가공

판다스는 apply 함수에 lambda 식을 결합해 DataFrame이나 Series의 레코드별로 데이터를 가공하는 기능을 제공한다. 

판다스의 경우 컬럼에 일괄적으로 데이터 가공을 하는 것이 속도 면에서 더 빠르나 복잡한 데이터 가공이 필요할 경우 어쩔 수 없이 apply lambda를 이용한다. 잠깐 lambda를 살펴보자

def get_square(a):
  return a**2
print('3의 제곱은:', get_square(3))
3의 제곱은: 9
*****************************************
lambda_square = lambda x:x**2
print('3의 제곱은', lambda_square(3))
3의 제곱은 9

위의 식은 일반적으로 함수를 사용해서 제곱값을 구하는 식이다. 함수명과 입력 인자를 먼저 선언하고 이후 함수 내에서 입력 인자를 가공한 뒤 결과값을 return과 같은 문법으로 반환해야 한다.

lambda는 이러한 함수의 선언과 함수 내의 처리를 한 줄의 식으로 쉽게 변환하는 식이다. 

lambda x:x**2에서 ':'로 입력 인자와 반환될 입력 인자의 계산식을 분리한다. ':'의 왼쪽에 있는 x는 입력 인자를 가리키며, 오른쪽은 입력 인자의 계산식이다. 오른쪽의 계산식은 반환 값을 의미한다.

 

lambda 식을 이용할 때 여러 개의 값을 입력 인자로 사용해야 할 경우에는 보통 map( )함수를 결합해서 사용한다.

 

a = [1,2,3]
squares = map(lambda x:x**2,a)
list(squares)

[1 4 9]

다음은 'Name' 컬럼의 문자열 개수를 별도의 컬럼인 'Name_len'에 생성한다. 

df['Name_len'] = df['Name'].apply(lambda x: len(x))
df[['Name', 'Name_len']].head(3)

다음으로는 조금 더 복잡한 가공을 하겠다고 한다. Lambda 식에서 if else 절을 사용한다. 

나이가 15세 미만이면 'Child', 그렇지 않으면 'Adult'로 구분하는 새로운 컬럼 'Child_Adult'를 apply lambda를 

이용해 만든다. 

df['Child_Adult'] = df['Age'].apply(lambda x: 'Child' if x<=15 else 'Adult')
df[['Age','Child_Adult']].head(10)

 

 

lambda 식은 if else를 지원하는데, 주의할 점이 있다.

if 절의 경우 if 식보다 반환 값을 먼저 기술해야 한다.

이는 lambda식 ':' 기호의 오른편에 반환 값이 있어야 하기 때문이다.

따라서

(x)   lambda x: if x<=15 'Child' else 'Adult'

(o)   lambda x: 'Child' if x <=15 else 'Adult'

else의 경우는 else 식이 먼저 나오고 반환 값이 나중에 오면 된다.

 

또 한가지는 if, else만 지원하고 if, else if, else와 같이 else if는 지원하지 않는다. 

else if를 사용하기 위해서는 else 절을 ( )로 내포해 ( ) 내에서 다시 if else 적용해 사용한다. 다음은 나이가 15세이하면 Child, 15~60세는 Adult, 61세 이상은 Elderly로 분류한다.

 

df['Age_cat'] = df['Age'].apply(lambda x: 'Child' if x<=15 else ('Adult' if x<=60 else 'Elderly'))
df['Age_cat'].value_counts()

==================================================================================================

Adult      786
Child       83
Elderly     22
Name: Age_cat, dtype: int64

첫번째 else절에서 ( )로 내포했는데, ( ) 안에서도 반환 값이 먼저 나왔다. 그런데 else if가 많이 나와야 하는 경우나 

switch case 문의 경우는 이렇게 else를 계속 내포해서 쓰기에는 부담스럽다.  이 경우에는 별도의 함수를 만드는게 더

나을 수 있다. 

마지막으로 5살 이하는 Baby, 12살이하는 Child, 18세 이하는 Teenage, 25살 이하는 Student, 35살 이하는 Young Adult, 60세 이하는 Adult, 그리고 그 이상은 Elderly로 분류해보자. 

# 나이에 따라 세분화된 분류를 수행하는 함수 생성.
def get_category(age):
  cat = ''
  if age <= 5: cat = 'Baby'
  elif age <= 12: cat = 'Child'
  elif age <= 18: cat = 'Teenager'
  elif age <= 25: cat = 'Student'
  elif age <= 35: cat = 'Young Adult'
  elif age <= 60: cat = 'Adult'
  else: cat = 'Elderly'

  return cat

# lambda 식에 위에서 생성한 get_category( ) 함수를 반환값으로 지정
# get_category(X)는 입력값으로 'Age' 컬럼 값을 받아서 해당하는 cat 반환
df['Age_cat'] = df['Age'].apply(lambda x: get_category(x))
df[['Age', 'Age_cat']].head()

 

 

이와 같이 범위에 해당하는 값이 잘 할당되서 분류되었음을 확인할 수 있다. 

lambda 함수는 식을 간결하게 만들어줄 수 있어서 잘쓰시는 분들은 코드를 

짧게 만들기 위해 능숙하게 사용하신다.

 

함수들이 많기도 많고 어려워서 많은 공부가 필요할 것 같다. 

 

공부하다보니 분석하기 쉽게 준비된 데이터셋들이 많다. 

하지만 실제는 데이터가 없는 경우가 많다. 

사실 이러한 함수들을 사용해서 분석하기 쉽게 테이블로 반환한다거나 

유의미한 결과를 나타내는 것이 기본이겠지만, 이것들을 익히는 것도 어렵다.

실제로 해보면 끝없는 에러의 향연에 정신이 혼미하고 눈이 빠질 것 같다..

 

넘파이와 판다스 라이브러리는 기본 중의 기본이라고 할 수 있으니 잘 복습해야겠다. 

 

😁

 

 

 

 

 

 

 

 

출처: 파이썬 머신러닝 완벽가이드, 나동빈님 유튜브, rfriend, 블로그들

https://www.kaggle.com/c/titanic/data

 

Titanic: Machine Learning from Disaster

Start here! Predict survival on the Titanic and get familiar with ML basics

www.kaggle.com

 

https://www.youtube.com/watch?v=9PF4BAFh-J8

 

http://blog.daum.net/_blog/BlogTypeView.do?blogid=0sjKF&articleno=41&categoryId=8®dt=20160419215145

 

Lambda, filter, reduce and map

Lambda, filter, reduce and map lambda don\'t need def, return

blog.daum.net

https://rfriend.tistory.com/366

 

[Python] 익명 함수 lambda

이번 포스팅에서는 lambda 를 사용한 이름이 없는 익명 함수(the anonymous functions), 한 줄로 간단하게 다음 함수 안에 넣어서 사용할 수 있는 인 라인 함수 (inline functions)에 대해서 알아보겠습니다. R 사..

rfriend.tistory.com

 

반응형

'Data Scientist' 카테고리의 다른 글

군집화(K-means clustering)  (0) 2020.02.16
빅데이터를 지배하는 통계의 힘  (0) 2020.02.11
넘파이 한번에 정리하기  (0) 2020.02.09
R기초  (0) 2020.02.07
기초통계 - 척도  (0) 2020.02.07