Python Pandas Basic2 (DataFrame)
Series가 1차원이라면 DataFrame은 2차원으로 확대된 것이라고 생각하면 쉽다.
DataFrame은 2차원이기 때문에 인덱스가 row, column로 구성되어 있다. (row는 개별 데이터, column은 개별 속성)
DataFrame은 데이터를 읽어와서 가공하는 과정까지로 설명하는게 쉽다.
1. pd.read_csv(filepath)
Kaggle에서 연습용 데이터로 가장 많이 사용하는 Titanic 데이터를 예시로 설명하겠다.
import pandas as pd
filepath = r'C:\User\data\titanic.csv'
titanic_df = pd.read_csv(filepath)
이런 형식으로 파일을 읽어올 수 있다.
여기서 옵션을 몇개 줄 수 있다
df = pd.read_csv(filepath, index_col='PassengerId', usecols=['PassengerId','Survived', 'Pclass','Name'])
이렇게 사용할 column과 index로 사용할 column을 지정할 수도 있다.
2. DataFrame의 구성요소들
index 와 column 이 있다. df.index를 호출하면 index가, df.column을 호출하면 속성들이 list 형태로 나온다.
column 같은 경우는 이름으로 보통 만드는데 그럴 경우에는 df.column.values 로 값을 호출할 수 있다.
index 변경하는 것은 Series와 비슷하다. df.index = ['index1', 'index2', 'index3'] 이런 식으로 변경하면 된다. column 도 마찬가지로 df.column = ['value1', 'value2', 'value3'] 이렇게 변경하면 된다.
rename 함수를 통해서 바꾸는 방법도 있다. 원래는 원본 변화되지 않지만 inplace=True를 두어서 원본이 변화하게 옵션을 줄 수 있다.
df.rename(columns={'index1':'value1', 'index2':'value2', 'index3':'value3'}, inplace=True) 처럼 column명과 data를,
df.rename(index = {'index1':'index3'}, inplace=True) 로 index명을 바꿀 수 있다.
2-1. DataFrame 직접 생성
Series와 마찬가지로 df = pd.DataFrame([1, 2, 3]) 이런 식으로 만들면, 0-base index가 자동으로 생성된다.
2차원일 경우에 df = pd.DataFrame([ [1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12] ]) 이렇게 만들면 열과 행 번호가 0-base로 자동 생성된다.
dictionary로 생성할 때를 잘 생각해야 한다. dictionary의 key는 column의 자리로 간다. 몇 번째 row에 value들이 들어가는 게 아니다.
data = {'a' : [100, 200, 300], 'b': ['new1','new2','new3'], 'c': [40,50,60]}
pd.DataFrame(data, index=['x', 'y', 'z'])
당연하게도 데이터들은 iterable 하기 때문에 np.array([]) 형태로 만들어도 되고, np.Series([]) 로 만들어도 된다. tuple은 말할 것도 없이 가능하다.
data = {
'a': np.array([100, 200, 300])
, 'b': pd.Series(['new1', 'new2', 'new3'], [1, 2, 3])
, 'c': (40, 50, 60)
}
pd.DataFrame(data, index=[1, 2, 3])
Series로부터 만들 경우도 거의 동일하다. Series의 index가 column 자리로 간다. 마찬가지로 row의 index가 아니라는 것이다. 다만 dictionary는 원래부터가 key : data 의 어떻게 보면 2차원 배열이라고도 이라고 할 수 있는데, Series는 index를 가진 1차원 데이터라고 할 수 있기 때문에, 이렇게 다 따로 data를 만들고, 종합해서 DataFrame을 만들어 줘야 한다.
data1 = pd.Series([100, 200, 300], ['a', 'b', 'c'])
data2 = pd.Series([101, 202, 303], ['a', 'b', 'c'])
data3 = pd.Series([110, 220, 330], ['a', 'b', 'c'])
pd.DataFrame([data1, data2, data3])
당연히 열이 안 겹쳐서 행, 열에 맞지 않는 데이터가 들어가게 되면 NaN으로 표시된다.
data4 = pd.Series([400, 404, 440],['a','b','d'])
pd.DataFrame([data1, data2, data3, data4])
3. set_index, reset_index
일단, reset_index는 기존의 행, 예를 들면 아까 만들었던
이 데이터에서 x, y, z가 index라는 이름으로 column 으로 올라가고 새로운 0-base 인덱스가 생긴다.
역시 원본 변화는 없다.
이전 상태로 되돌리려면 df.set_index('column명') 해주면 된다. 위 경우에는 df.set_index('index')로 하면 되겠다.
multilevel column 으로 column도 tuple 형태로 다차원 구성이 가능하지만, 활용을 많이 하진 않는 것 같다.
pd.DataFrame({
('new1', 'new11') : [10, 20, 30],
('new1', 'new12') : [10, 20, 30],
('new2', 'new21') : [10, 20, 30],
('new2', 'new22') : [10, 20, 30]
})
index도 비슷한 방식으로 multilevel index를 줄 수 있다. index = [ [ ] , [ ] , [ ] , ... ] 로 주면 된다. index = [[], [], ... ], columns = [[], [], [], ... ] 해서 만들 수도 있지만 여기서는 생략하겠다.
pd.DataFrame({
('new1', 'new11') : [10, 20, 30, 40],
('new1', 'new12') : [10, 20, 30, 40],
('new2', 'new21') : [10, 20, 30, 40],
('new2', 'new22') : [10, 20, 30, 40]
}, index = [['index1', 'index1', 'index2', 'index2'],['index11', 'index12', 'index21', 'index22']])
4. DataFrame 함수들
df.shape : row x column 정보
df.head() : 앞 4개의 데이터
df.tail() : 마지막 5개 데이터
df.describe() : 숫자형 데이터의 통계치를 계산 (descriptive statistics : 기술통계량)
df.info() : 데이터 타입, 각 아이템의 개수 등을 출력, NaN이 있는지 파악하기 위해선 필수적으로 보아야 한다.
df.size : 데이터(하나하나)의 전체 개수
len(df) : 데이터 행의 개수 (한 줄 row로 이루어진 한 묶음의 데이터가 몇 개인지)
df.ndim : dataframe의 차원
df[ 'column' ].unique() : 이 column이 가진 값에는 무엇이 있는지 요소만 확인
5. DataFrame Row, Column 골라내기
Slicing은 당연히 있으니까 가볍게 넘어가고 .loc 과 iloc 으로 골라내는 방법으 위주로 작성하겠다.
loc은 location의 약어로, DataFrame의 행 또는 칼럼의 label이나 boolean array로 인덱싱하는 방법이다.
사람이 읽을 수 있는 label 값으로 값들을 골라오는 방법이라고 생각하면 된다.
사용법은 df.loc[ row indexing 값 , column indexing 값 ] 이다. 만일 loc에 하나의 값만 입력한다면, 해당하는 하나의 행 또는 열만 뽑아 온다고 생각하면 된다.
당연히 '행' 에는 조건을 넣을 수도 있다. 가령 df.loc[df ['column명']==값] 이런 식으로 loc안에 조건을 넣어서 만족하는 행만 뽑아내게 만드는 것이다. 이걸 Boolean Indexing이라고도 하는데
age_mask = (titanic_df['Age']>30) & (titanic_df['Age']<40)
pclass_mask = titanic_df['Pclass']==1
titanic_df[pclass_mask & age_mask].loc[:5,['Age', 'PassengerId', 'Pclass', 'Sex', 'Name','Ticket', 'Fare', 'Embarked', 'Ticket', 'Parch']]
이런 식으로 조건에 맞는 행을 골라낼 수 있다.
6. DataFrame Row, Column 추가, 삭제하기
df[ 'column명' ] = 원래 column값 가공한 값 하면 된다. 당연히 column 원래 있었으면 변경되고, 없었으면 새로 만들어진다.
특이한 점은 df.loc['새 index명'] = pd.Series([100, 200, 300, 400], index=['a', 'b', 'c', 'd']) , index 생략하고 df.loc['새 index명'] = [101, 202, 303, 404]이런 식으로 넣을 수도 있다는 것이다
삭제하는 방법은 .drop( ['index1', 'index2', 'index3', ... ]) 이 있다.
7. DataFrame Column 끼리의 상관성 보기
df.corr() 을 통해서 확인할 수 있다. ( 절댓값 1에 가까울수록 상관관계가 높다는 것)
plt.matshow(df.corr()) 처럼 그래프 통해서 확인할 수도 있다.
8. DataFrame 결측치 확인
info() 함수를 통하여 NaN 개수를 확인하고, isna() 함수를 통해 boolean 타입으로 확인 (shape, info, describe는 기본으로 진행한 다음의 이야기)
isna() 함수를 통해 boolean 타입으로 확인한다는 것은 df [ df ['column명'].isna() ] 하면 True인 값들만 걸러져서 DataFrame에 담겨지는데, 이걸 활용하는 경우가 많다.
결측치를 확인했으면 .fillna( 대체할 값 ) 과 .dropna() 를 활용해서 NaN을 처리해주는데, 보통 fillna() 를 사용한다.
.dropna() 를 할 경우에 데이터 손실이 생기기 때문이다. (특히 결측치 많을 경우)
df.dropna(subset=['column1','column2']) 처럼 column을 골라서 dropna()를 할 수도 있다. df.dropna(axis=1) 를 할 경우 NaN이 있는 column은 다 삭제된다.
중요한 건 .fillna()인데, 사용법은 간단하고, 아래의 예시를 보면 쉽게 이해가 간다.
원본이 변화가 안되서 직접 해당 column 을 뽑아서 넣어줬다.
mean1 = titanic_df[titanic_df['Survived']==1]['Age'].mean()
mean2 = titanic_df[titanic_df['Survived']==0]['Age'].mean()
titanic_df.loc[titanic_df['Survived']==1, 'Age'] = titanic_df[titanic_df['Survived']==1]['Age'].fillna(mean1)
titanic_df.loc[titanic_df['Survived']==0, 'Age'] = titanic_df[titanic_df['Survived']==0]['Age'].fillna(mean2)
그럼 이런 식으로 데이터를 변경할 수 있다.
9. 데이터 전처리
NaN 값을 처리했다면, 범주형 데이터는 분석단계에서 계산이 어렵기 때문에 숫자형으로 변경이 필요하다.
apply() 함수를 사용하는 방법이 있다. 범주형을 수치형으로 바꾸는 예는 아니고, apply()를 어떻게 활용하는지에 대한 설명이다.
import math
def age_categorize(age):
if math.isnan(age):
# age 없으면 -1로 리턴
return -1
return int(age / 10) * 10
titanic_df['Age_Categorize'] = titanic_df['Age'].apply(age_categorize)
이런 식으로 나이를 apply() 함수를 통해 나이대를 표시한 column을 새로 만들 수도 있다.
apply()를 활용할 수도 있지만, 활용도가 높은 편은 아니다.
One-Hot Encoding을 활용하는 경우가 있다. 범주형 데이터의 각 범주(category)를 column레벨로 변경하여 해당 범주에 해당하면 1, 아니면 0으로 채우는 인코딩 기법이다.
pandas.get_dummies 함수를 사용하고 drop_first 를 사용해 첫번째 카테고리 값은 사용하지 않는다. (첫번째 카테고리 값이 없어도 충분히 표현 가능하기 때문이다.
pd.get_dummies(titanic_df, columns=['Pclass', 'Sex', 'Embarked'], drop_first=True).head()
이렇게 범주형 데이터를 전처리할 수 있다.
그리고 한가지 더, replace 가 있다. Titanic은 적당한 예제를 찾기가 애매해서 다른 dataset을 가져왔다. 원래 M, L, H 었던 값을 0, -1, 1 로 바꾼 것이다
df['Class2'] = df['Class'].replace('M', 0)
df['Class2'] = df['Class2'].replace('L', -1)
df['Class2'] = df['Class2'].replace('H', 1)
df[['Class2']]
10. GroupBy
groupby는 활용도가 높다. SQL의 GROUPBY를 생각하면 쉽다.
grouped_pclass = titanic_df.groupby('Pclass')
이런식으로 group를 'Pclass'로 나누면 groupby 라는 객체로 저장이 된다.
groups : dict 형태로 각 그룹에 해당하는 index들이 나열된다.
key : column 각 값
value : index 값들의 list
size() : 몇 개의 데이터들이 각각 있는지 확인한다. df.size 와 전혀 다르다.
이외에도 count(), sum(), mean(), std(), var(), min(), max() 등이 있다.
groupby() 안에 들어갈 수 있는 옵션은 groupby(컬럼 혹은 컬럼 리스트), groupby(level=?), groupby(함수)가 있다. level 순서대로 그룹화 할 수 있다는 것이다.
titanic_df.set_index(['Pclass','Sex']).groupby(level=[0, 1]).aggregate([np.mean, np.sum, np.max])
이렇게 만들 수 있다는 것이다.
DataFrame이 데이터 분석의 기초이기 때문에 범위가 넓고 길다. 다음으로는 본격적인 시각화에 대해서 작성하겠다.