목차
- 열 변경 : rename(), drop(), map(), replace()
- 결측치 처리 : isna(), dropna(), fillna(), ffill(), bfill(), interpolate()
- 범주값 처리 : cut(), qcut(), one-hot encoding
연휴가 끝나고 돌아온 TIL.
오늘은 데이터프레임의 열 이름 변경, 생성, 삭제와 범주값 변경, 생성 그리고 결측치 및 가변수의 처리를 배웠다.
나날이 배우는 것도 많아진다만, 다음 주에 있을 AICE에 응시하는 객기를 부린 고로 오늘도 힘 내 보자.
열 변경
데이터프레임을 다루다 보면 열을 변경할 일이 많다.
AICE에서 가장 먼저 요구하는 사항 역시 이 부분이니만큼 데이터 전처리의 첫단계라 할 수 있겠다.
- rename()
# 라이브러리 불러오기
import numpy as np
import python as plt
# 데이터 읽어오기
tip = pd.read_csv(path) # path 주소는 에이블스쿨 강사님께서 주셨다.
# 확인
tip.head()
# rename() 함수로 열 이름 변경 ## columns = {} 형태 주의!
tip.rename(columns={
'total_bill_amount' : 'total_bill',
'male_female' : 'sex',
'smoke_yes_no' : 'smoker',
'week_name' : 'day',
'dinner_lunch' : 'time'
}, inplace=True)
이런 식으로 열의 이름을 한번에 변경할 수 있다.
파라미터로 columns = {}가 쓰인다는 점이 낯설다.
그런데 추후에 볼 map()이나 replace()도 중괄호 {}를 많이 활용하는 걸 보면,
pandas는 'a를 b로 매핑한다.' 라는 의미로 딕셔너리 {a:b}를 즐겨 쓰나 보다.
# 모든 열 이름 변경:
tip.columns = ['total_bill', 'tip', 'sex', 'smoker', 'day', 'time', 'size']
당연히 이렇게 columns 속성으로 모든 컬럼명을 호출한 후에 재할당해도 된다.
- drop()
데이터프레임의 줄(row, column)을 탈락시키는데 쓴다.
다만 우리는 row를 drop()으로 탈락시킬 일이 거의 없기에, axis=1 파라미터를 통해 열을 지정하는 습관을 들이자.
# 열 하나 삭제
drop_cols = ['final_amt']
tip.drop(drop_cols, axis = 1, inplace=True)
# 열 두 개 삭제
drop_cols = ['div_tb', 'day'] # 소스코드는 놔두고 리스트만 바꾸면 된다.
tip.drop(drop_cols, axis=1, inplace=True)
이렇게 재사용성과 의지을 위해 하나의 열이라도 리스트로 묶어 변수화해두면 좋다.
※ 참고 : 열 추가
지난 번에 배운 대로 없는 열은 선언하기만 하면 추가된다.
이 과정에서 다양한 방법이 쓰인다.
# final_amt 열 추가: final_amt = total_bill + tip
tip['final_amt'] = tip['total_bill'] + tip['tip']
이렇게 열 간의 연산을 통해 새로운 열을 만들 수도 있고,
# day = 'Sat' 또는 'Sun'인 행만 1이고 나머지는 0인 새 열 'holyday' 만들기
tip['holiday'] = 0
tip.loc[tip['day'].isin(['Sat', 'Sun']), 'holiday'] = 1
# size = 1~3인 행만 뽑은 새 데이터프레임 tmp 만들기
tmp = tip.loc[tip['size'].between(1, 3)
이렇게 isin()이나 between을 활용할 수도 있다.
- map(), replace()
map()과 replace()는 rename()의 밸류 버전이라고 할 수 있다.
매개변수로 딕셔너리를 받아, {A → B}로 바꾸어 준다.
다만 열 이름 바꾸기(rename())는 데이터프레임 전체를 건드리는 대공사지만,
값 바꾸기(map(), replace())는 열 내에서 일어나는 작은 공사이기에 inplace()가 따로 없다.
그래서 기본적으로는 df['열'] = df['열'] .map({A:B}) 형태.
둘의 차이는 map()이 매핑되지 못한 값을 모두 null로 반환한다면,
replace()는 매핑되지 못한 값이 모두 유지된다는 것이다.
# Male -> 1, Female -> 0
tip['sex'] = tip['sex'].map({'Male' : 1, 'Female' : 0})
# 1 --> Male, 0 --> Female
tip['sex'] = tip['sex'].replace({1 : 'Male', 0 : 'Female'})
실행 결과는 같다만, 'sex'열에 'Male', 'Female' 이외의 값이 있었다면 null이 되었을 것이다.
배운 것들을 활용한 타이타닉 데이터프레임 문제.
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
titanic = pd.read_csv('https://raw.githubusercontent.com/Jangrae/csv/master/titanic.csv')
- 다음 요구사항에 맞게 순서대로 구문을 작성하고 확인하세요.
- 4-1) PassengerId, Name, Ticket, Cabin 열을 한 번에 삭제하세요.
- 4-2) Sex 열 이름을 Male로 변경하세요.
- 4-3) Male 열 값을 'male'은 1, 'female'은 0으로 변경하세요.
- 4-4) SibSp 열과 Parch 열의 값을 더한 결과를 갖는 Family 열을 추가하세요.
- 4-5) SibSp, Parch 두 열을 삭제하세요.
- 4-6) 결과를 확인하세요.
# 요구사항대로 삭제할 열 목록 작성
drop_cols = ['PassengerId', 'Name', 'Ticket', 'Cabin']
# 열 삭제
titanic.drop(drop_cols, axis=1, inplace=True)
# 열 이름 변경
titanic.rename(columns={'Sex' : 'Male'}, inplace=True)
# 열 값 매핑
titanic['Male'] = titanic['Male'].map({'male' : 1, 'female' : 0})
# 새로운 열 생성
titanic['Family'] = titanic['SibSp'] + titanic['Parch']
# 열 삭제
titanic.drop(['SibSp', 'Parch'], axis=1, inplace=True)
titanic.head()
결과.
결측치 처리
정보 수집 단계든, 처리 단계든 대부분의 경우에서 의도치 않게 누락된 값, 결측치.
(강사님 曰 소비자 설문조사 등 결측치 자체가 의미를 갖기도 한다. 실제로 최신 학습 모델은 결측치의 의미를 고려한다고.)
- isna(), notna()
na 대신 null을 쓰기도 한다.
isna()는 결측치를, notna()는 정상치를 True로 반환한다.
air.isna()
Ozone Solar.R Wind Temp Month Day
False False False False False False 111
True False False False False False 35
False True False False False False 5
True True False False False False 2
Name: count, dtype: int64
보다시피, 항마다 bool을 뱉기 때문에 단독으로 쓰기는 힘들다.
그래서 쓰이는 합동 코드.
air.isna().sum()
Ozone 37
Solar.R 7
Wind 0
Temp 0
Month 0
Day 0
dtype: int64
보다시피 각 열마다 몇 개의 결측치가 있는지 알 수 있다.
air.isna().sum() / len(air) * 100
Ozone 24.183007
Solar.R 4.575163
Wind 0.000000
Temp 0.000000
Month 0.000000
Day 0.000000
dtype: float64
이렇게 결측치의 비율도 알 수 있다.
실무에서 drop()할지 fill()할지 결정할 근거가 될 수 있다.
- dropna()
결측치가 있는 행을 제거한다.(axis=1을 통해 제거할 수도 있지만, 그럴 일은 없다.)
해당 행의 나머지 열 데이터도 다 날아가기에 신중해야 한다.
# 결측치가 하나라도 있는 행 제거
air_test.dropna(axis=0, inplace=True)
subset 옵션을 통해 특정 열에 결측치가 있는 행만 제거할 수도 있다.
# Ozone 열이 결측치인 행 제거
air_test.dropna(subset=['Ozone'], axis=0, inplace=True)
- fillna(), ffill(), bfill()
결측치를 입력한 값(fillna()), 앞 row값(ffill()), 뒤 row값(bfill())등으로 채운다.
# Ozone 평균 구하기
mean_Ozone = air_test['Ozone'].mean()
# 결측치를 평균값으로 채우기
air_test['Ozone'].fillna(mean_Ozone, inplace=True)
# Solar.R 열의 누락된 값을 0으로 채우기
air_test['Solar.R'].fillna(0, inplace=True)
fillna()를 쓸 때는 이렇게 매개변수로 수 자체나 시리즈 등 채우고 싶은 값을 넣으면 된다.
# Ozone 열의 누락된 값을 바로 앞의 값으로 채우기
air_test['Ozone'] = air_test['Ozone'].ffill()
# Solar.R 열의 누락된 값을 바로 뒤의 값으로 채우기
air_test['Solar.R'] = air_test['Solar.R'].bfill()
ffill()이나 bfill()은 심플하게 뒤에 붙이면 된다.
추세를 유지하게 되므로,
주로 시계열 데이터처럼 흐름에 따른 값이 나타나는 자료에 유용하다.
하지만 추세가 정말 일관적이라면, 선형보간법으로 결측치를 채울 수도 있다.
선형 보간법(線型補間法, linear interpolation)은 끝점의 값이 주어졌을 때 그 사이에 위치한 값을 추정하기 위하여 직선 거리에 따라 선형적으로 계산하는 방법이다. (위키백과)
한마디로 선형 회귀를 바탕으로 빈 부분을 채우는 방법이다.
비슷한 걸 예전에 머신러닝 부분에서 봤었는데, 독학이라 기억은 잘 안난다.
강사님께서도 선형 회귀 기반의 머신러닝 모델인 리니어 모델에 대해 간단히 말씀하셨다.
# 선형보간법으로 채우기
air_test.interpolate(method='linear', inplace=True)
method= 'linear'가 기본으로, 직선의 추세를 기반으로 보간한다.
이외에도 time 등 추가적인 요소들을 고려하는 매개변수가 있는 듯.
범주값 처리
지난 시간에 배운 범주형 데이터와 연속형 데이터는 결국 데이터 분석 막바지에 하나로 수렴된다.
하지만 전처리 과정에서 의미를 도출하기 위해 연속형 데이터를 범주화하거나,
모델 학습을 위해 범주형 데이터를 True/False 또는 1/0으로 인코딩해야 할 일이 있다.
이 두 경우에 대해 알아보자.
- 연속형 데이터의 범주화 : cut(), qcut()
cut()은 내가 주관적으로 크기(bins)를 나누어 데이터를 쪼개는 것,
qcut()은 개수를 기준으로 균일하게 데이터를 쪼개는 것.
# cut()의 사용
bin = [-np.inf, 2.0, 2.9, 3.5625, 10.0]
label = ['a', 'b', 'c', 'd']
tip['tip_grp'] = pd.cut(tip['tip'], bins=bin, labels=label)
cut()과 qcut()은 기본적으로 pd.cut()을 새로운 열에 전달해 새 열을 만드는 방식으로 쓴다.
cut()의 경우 내가 구간(bins)을 지정해 줘야 하는데,
강사님께서 추천해 주신 방법은 4분위수를 쓰는 것.
4분위수는 .describe()에 의해 시리즈로 반환되기 때문에
.describe('n%') 형태로 쓸 수 있다.
이때 최솟값과 최댓값은 numpy의 무한대(inf)를 활용한다.
# 사분위수
q1 = tip['total_bill'].describe()['25%']
q2 = tip['total_bill'].describe()['50%']
q3 = tip['total_bill'].describe()['75%']
# 4분위수 기반 구간 생성
bin = [-np.inf, q1, q2, q3, np.inf]
# 각 구간의 이름 생성
label = ['a', 'b', 'c', 'd']
# 자르기
tip['bill_grp'] = pd.cut(tip['total_bill'], bins=bin, labels=label)
이런 식이다.
다른 에이블러 분의 채팅에 의하면, describe()를 쓰지 않고도 4분위수를 구할 수 있다나 보다.
# quantile()을 통해 4분위수 구하기
q1 = tip['total_bill'].quantile(0.25)
q2 = tip['total_bill'].quantile(0.5)
q3 = tip['total_bill'].quantile(0.75)
한편 qcut()은 bins를 설정할 필요가 없다.
구간의 수만 입력하면 끝.
나머지는 cut()과 같다.
# 같은 개수의 total_bill을 갖는 4개 구간으로 나누기
tip['bill_grp2'] = pd.qcut(tip['total_bill'], 4, labels=list('abcd'))
- 범주형 데이터 인코딩 : pd.get_dummies()
범주형 데이터는 보통 문자고, 숫자여도 그들끼리 산술적 관계를 갖지는 않는다.
강사님 말마따나 5반이 1반보다 다섯 배 잘하는 게 아니란 말이지.
따라서 이런 값들이 컴퓨터가 이해하기 편하게 이진화해 주는 게 가변수 만들기, one-hot encoding이다.
예를 들어 남/여를 1/0으로, 빨간불/노란불/파란불을 00/01/10 100/010/001로 만들어 주는 것.적어 놓고 보니 이진화랑은 미묘하게 다르다.
여기서 한가지, 다중공선성을 고려해야 한다.
다중공선성이란 건 독립변수들 간의 상관관계가 너무 높은건데...
쉽게 말해, a 변수를 몰라도 b c d를 보면 a를 알 수 있는 상황이다.
당장 위의 그림에서 class_1은 없어도 나머지 2~4로 유추가 가능하다.
저 상황에서 class_1이 멀쩡히 있다면, 컴퓨터는 이 데이터를 더 중요하게 취급하겠지.
따라서 우리는 class_1을 굳이 지워 줘야 한다.
이 과정은 혼자 하려면 상당히 손가락이 아프단 게 단점인데...
이 고충을 헤아리사 pandas에서 딸깍으로 원 핫 인코딩이 되는 함수를 주셨다.
# 가변수화: sex
dumm_cols = ['sex']
tip = pd.get_dummies(tip, columns=dumm_cols, drop_first=True, dtype=int)
# 여러 범주형 변수를 가변수화: smoker, day, time
dumm_cols = ['smoker', 'day', 'time']
tip = pd.get_dummies(tip, columns=dumm_cols, drop_first=True, dtype=int)
그것이 바로 get_dummies().
get_dummies의 매개변수 구성은 다음과 같다.
(데이터프레임, columns=적용할 컬럼 목록, drop_first=bool, dtype=type)
columns는 위 소스코드처럼 따로 묶어 두는게 예쁘고 좋다.
drop_first가 아까 말한 다중공선성 해결을 위해 True로 줘야 하는 녀석이다.
dtype은 기본 bool로 되어 있는데, int를 쓰는게 좀 더 직관적이다.