AICE (에이스) 시험 노트개념 정리14 MIN
결측치를 지울지 채울지 정하기
AICE Associate 전처리 과목의 첫 갈림길입니다. 결측 비율로 지울지 채울지 정하는 기준, dropna의 how·thresh·subset, fillna에 평균·중앙값·최빈값을 넣는 자리, ffill과 bfill, SimpleImputer를 연습 문제 7개로 짚습니다.
분석 과목에서 빈 칸이 어디에 몇 개인지까지 세어 두었습니다. 이제 그것을 실제로 손보는데, 손보는 방법이 둘뿐입니다 — 지우거나 채우거나입니다. AICE Associate는 「결측치를 적절히 처리하시오」처럼 방법을 지정하지 않고 묻는 문항을 내므로, 어느 쪽을 왜 골랐는지가 답이 됩니다.
셋을 먼저 갈라 둡니다. 결측치는 값이 비어 있는 칸이고, 대치는 그 칸에 다른 값을 넣어 메우는 일이며, 제거는 그 칸이 든 행이나 열을 통째로 빼는 일입니다. 지우는 쪽이 간단해 보이지만 빈 칸 하나 때문에 그 행의 멀쩡한 값 스무 개까지 함께 버리게 되므로, 둘 중 어느 쪽이 덜 잃는지를 매번 따져야 합니다.
지울까 채울까
결측 비율
기준의 뼈대는 비율입니다. 열의 값이 거의 다 비어 있으면 그 열은 채워도 근거 없는 값으로만 가득 차므로 열째 버리는 편이 낫고, 몇 개만 비어 있으면 그 행 몇 개를 지우는 비용이 작습니다.
(df.isnull().sum() / len(df) * 100).round(1)
대략의 눈금은 이렇습니다. 5퍼센트 안쪽이면 그 행을 지워도 표가 거의 안 줄어들고, 절반을 넘으면 그 열에 남은 정보가 적어 열을 지웁니다. 그 사이가 채우는 자리입니다. 다만 이 수는 관습이지 규칙이 아니고, 행 수가 200뿐인 표에서는 5퍼센트도 열 행이라 아깝습니다.
지울 때 잃는 것
비율만으로 정하면 안 되는 자리가 둘 있습니다.
하나는 타깃입니다. 예측하려는 열이 비어 있는 행은 정답이 없는 문제라 학습에 쓸 수 없고, 그 값을 지어내 채우면 모델이 지어낸 값을 배웁니다. 타깃의 결측은 비율과 무관하게 행을 지웁니다.
다른 하나는 빈 것 자체가 뜻을 가질 때입니다. 「최근 구매일」이 비어 있는 것은 값을 못 받은 것이 아니라 아직 한 번도 안 샀다는 뜻일 수 있습니다. 이런 열은 평균으로 채우면 사실이 뒤집히므로 0이나 별도의 갈래 값으로 채웁니다.
dropna
행과 열
dropna()는 빈 칸이 든 행을 지웁니다. axis=1을 주면 행 대신 열을 지웁니다.
df.dropna() # 빈 칸이 하나라도 있는 행을 전부 제거
df.dropna(axis=1) # 빈 칸이 있는 열을 제거
df = df.dropna() # 결과를 다시 대입해야 원본이 바뀐다
아무 인자 없이 쓰면 표가 예상보다 훨씬 줄어드는 일이 흔합니다. 열이 스물이면 행마다 빈 칸을 만날 기회가 스무 번이라, 열마다 결측이 1퍼센트씩만 있어도 행의 상당수가 걸립니다. 지우기 전후로 df.shape을 찍어 몇 행이 사라졌는지 확인하는 습관이 필요합니다.
subset과 how
지우는 범위를 좁힐 때 쓰는 인자가 셋입니다.
df.dropna(subset=['Churn']) # 이 열이 빈 행만 제거
df.dropna(how='all') # 모든 값이 빈 행만 제거
df.dropna(thresh=15) # 값이 15개 미만인 행 제거
how의 기본값은 'any'라 하나라도 비면 지웁니다. 'all'은 통째로 빈 행만 지우므로 훨씬 덜 지웁니다. thresh는 남길 조건을 개수로 적는 것이라, thresh=15는 결측이 아닌 값이 15개 이상인 행만 남깁니다.
타깃의 결측을 처리하는 줄이 dropna(subset=['타깃'])입니다. 이 한 줄만큼은 거의 모든 문제에서 같은 모양으로 나옵니다.
fillna
대푯값
fillna()는 빈 칸을 지정한 값으로 채웁니다. 무엇으로 채울지가 이 절의 전부입니다.
df['Age'] = df['Age'].fillna(df['Age'].mean())
df['Income'] = df['Income'].fillna(df['Income'].median())
df['City'] = df['City'].fillna(df['City'].mode()[0])
수치형에는 평균이나 중앙값을, 갈래형에는 최빈값, 곧 가장 많이 나온 값을 씁니다. 갈래형에 평균을 쓸 수 없는 것은 Seoul과 Busan의 평균이 없기 때문입니다.
평균과 중앙값 사이의 선택이 자주 나오는 문항입니다. 분포가 한쪽으로 치우쳐 있으면 중앙값입니다. 소득처럼 위쪽에 아주 큰 값이 몇 개 있는 열은 평균이 그 값들에 끌려 올라가서, 평균으로 채운 칸이 대다수 사람보다 훨씬 높은 값이 됩니다. 중앙값은 순서상 가운데 값이라 끝값에 안 흔들립니다.
mode() 뒤에 [0]을 붙이는 이유는 최빈값이 여럿일 수 있어 결과가 Series로 나오기 때문입니다. 첫 번째 값을 꺼내야 하나의 값이 됩니다.
ffill과 bfill
시간 순으로 쌓인 표에서는 대푯값보다 바로 앞의 값이 더 그럴듯합니다. 어제 기온이 비었으면 그저께 기온이 전체 평균보다 가깝기 때문입니다.
df['Temp'] = df['Temp'].ffill() # 앞 값으로 채우기
df['Temp'] = df['Temp'].bfill() # 뒤 값으로 채우기
ffill은 위쪽 값을 아래로 끌어내리고 bfill은 아래쪽 값을 위로 끌어올립니다. 둘 다 행 순서에 뜻이 있을 때만 쓸 수 있습니다. 순서가 무의미한 고객 명부에서 쓰면 남의 값을 가져다 쓰는 셈입니다.
첫 행이 비어 있으면 ffill은 끌어올 앞 값이 없어 그 칸이 그대로 남습니다. 끝 행에서 bfill도 마찬가지입니다. 채우고 나서 isnull().sum()을 다시 찍어 0인지 확인합니다.
SimpleImputer
strategy
SimpleImputer는 사이킷런이 제공하는 대치 도구로, fillna가 하는 일을 학습·예측 흐름에 맞춰 감싼 것입니다. 무엇으로 채울지는 strategy가 정합니다.
from sklearn.impute import SimpleImputer
imputer = SimpleImputer(strategy='median')
df[['Age', 'Income']] = imputer.fit_transform(df[['Age', 'Income']])
strategy에 올 수 있는 값은 넷입니다 — 'mean'·'median'·'most_frequent'·'constant'입니다. 앞의 셋이 평균·중앙값·최빈값이고, 'constant'는 fill_value에 적은 값을 그대로 넣습니다. 'most_frequent'만 문자 열에도 쓸 수 있습니다.
fit과 transform
fillna가 아니라 이쪽을 쓰는 이유가 여기 있습니다. fit은 채울 값을 계산해 기억하고 transform은 기억한 값을 실제로 넣습니다. 둘이 갈려 있어야 학습 데이터에서 잰 값을 평가 데이터에도 그대로 쓸 수 있습니다.
imputer.fit(X_train[['Age']])
X_train[['Age']] = imputer.transform(X_train[['Age']])
X_test[['Age']] = imputer.transform(X_test[['Age']])
평가 데이터로 fit을 다시 부르면 평가 데이터의 평균이 학습 과정에 새어 들어옵니다. 이것을 데이터 누수라고 하고, 그러면 실제보다 좋은 점수가 나와 모델을 잘못 고르게 됩니다. fit_transform은 학습 데이터에만 쓰고 평가 데이터에는 transform만 씁니다.
fit_transform이 돌려주는 것은 DataFrame이 아니라 넘파이 배열이라 열 이름이 사라집니다. 위 예처럼 원래 열 자리에 다시 대입하면 이름이 유지됩니다.
이 영역의 출제 형태는 대개 셋입니다. 결측 비율을 주고 지울지 채울지 고르게 하거나, 치우친 분포를 주고 평균과 중앙값 중 하나를 고르게 하거나, fillna 한 줄의 빈 자리를 채우게 합니다.
연습 문제
1,000행 표에서
Note열의 결측이 850개다. 이 열을 어떻게 처리하는 것이 적절한가?결측 비율이 퍼센트라 남은 값이 150개뿐입니다. 채워도 대부분이 지어낸 값이 되므로df.drop('Note', axis=1)로 열을 지웁니다.Income값이10, 20, 30, 40, 1000이고 여섯 번째 행이 비어 있다. 평균으로 채울 때와 중앙값으로 채울 때 들어가는 값을 각각 구하고, 어느 쪽이 적절한지 쓰시오.평균은 이고 중앙값은 순서상 가운데인 30입니다. 1000 하나가 평균을 220까지 끌어올려 다섯 값 중 넷보다 큰 값이 되므로, 위쪽으로 치우친 이 분포에서는 중앙값이 적절합니다.갈래형 열
City의 결측을 최빈값으로 채우려 한다. 알맞은 것은?
①df['City'].fillna(df['City'].mean())
②df['City'].fillna(df['City'].mode())
③df['City'].fillna(df['City'].mode()[0])
④df['City'].fillna(df['City'].median())③.mode()는 최빈값이 여럿일 수 있어 Series를 돌려주므로[0]으로 첫 값을 꺼내야 합니다. ①과 ④는 문자 값에 평균이나 중앙값을 낼 수 없습니다. ②는 Series를 채울 값으로 넘기는 것이라 의도대로 채워지지 않습니다.df.dropna()와df.dropna(how='all')은 각각 어떤 행을 지우는가?기본값은how='any'라 빈 칸이 하나라도 있는 행을 지웁니다.how='all'은 그 행의 모든 값이 비어 있을 때만 지우므로 훨씬 적게 지워집니다.학습 데이터와 평가 데이터에
SimpleImputer를 적용하는 다음 코드에서 잘못된 줄을 찾고 이유를 쓰시오.imputer = SimpleImputer(strategy='mean') X_train = imputer.fit_transform(X_train) X_test = imputer.fit_transform(X_test)셋째 줄입니다. 평가 데이터로fit을 다시 불러 평가 데이터의 평균을 계산해 버리면 데이터 누수가 생겨 실제보다 좋은 점수가 나옵니다.X_test = imputer.transform(X_test)로 고쳐 학습 데이터에서 잰 값을 그대로 씁니다.일자별로 쌓인 기온 표에서 중간 며칠이 비어 있다.
ffill과 전체 평균 중 어느 쪽이 적절하며,ffill을 쓸 때 주의할 점은 무엇인가?행 순서에 뜻이 있고 이웃한 날의 기온이 전체 평균보다 가까우므로ffill이 적절합니다. 다만 첫 행이 비어 있으면 끌어올 앞 값이 없어 그대로 남으므로, 채운 뒤isnull().sum()으로 0인지 확인해야 합니다.타깃 열
Churn에 결측이 12개 있다. 어떻게 처리하며 그 이유는 무엇인가?df = df.dropna(subset=['Churn'])으로 그 행들을 지웁니다. 타깃이 빈 행은 정답이 없어 학습에 쓸 수 없고, 값을 지어내 채우면 모델이 지어낸 정답을 배우게 되기 때문입니다. 비율이 작든 크든 같습니다.
여기서 손이 굳어야 하는 것은 세 줄입니다 — df.dropna(subset=['타깃']), df['열'].fillna(df['열'].median()), 그리고 학습에만 fit_transform, 평가에는 transform을 쓰는 짝입니다. 빈 칸을 메웠으면 다음은 앞서 IQR로 찾아 둔 튀는 값을 실제로 손보고, 쓸모없는 열을 걷어내는 자리입니다.

