이미지 로딩 중...
AI Generated
2025. 11. 25. · 5 Views
결측치 처리 전략 완벽 가이드
데이터 분석에서 가장 먼저 만나는 문제, 결측치! 삭제부터 고급 대체 기법까지, 실무에서 바로 쓸 수 있는 결측치 처리 전략을 초급자도 이해하기 쉽게 알려드립니다. 데이터의 품질을 높이고 정확한 분석 결과를 얻어보세요.
목차
- 결측치란 무엇인가 - 데이터의 빈칸을 이해하기
- 행 삭제 전략 - 결측치가 있는 데이터 줄 자체를 제거하기
- 열 삭제 전략 - 결측치가 많은 컬럼 자체를 제거하기
- 평균값 대체 - 숫자 데이터의 가장 기본적인 채우기
- 중앙값과 최빈값 대체 - 더 견고한 채우기 방법
- 전방채우기와 후방채우기 - 시계열 데이터의 특별한 처리
- KNN 대체 - 유사한 데이터로 똑똑하게 채우기
- 다중 대체 - 여러 가능성을 고려하는 고급 기법
- 결측치 패턴 분석 - 왜 빠졌는지 이해하기
- 결측치 처리 전략 선택 가이드 - 상황별 최적 방법
1. 결측치란 무엇인가 - 데이터의 빈칸을 이해하기
시작하며
여러분이 설문조사 데이터를 받았는데, 어떤 사람은 나이를 적지 않고, 어떤 사람은 이메일을 적지 않았다면 어떻게 하시겠어요? 이런 "빈칸"이 바로 결측치입니다.
실제 데이터 분석 현장에서는 완벽한 데이터를 받는 경우가 거의 없습니다. 센서 오류로 값이 기록되지 않거나, 사용자가 입력을 건너뛰거나, 데이터 전송 중 손실이 발생하는 등 다양한 이유로 결측치가 생깁니다.
이런 결측치를 제대로 처리하지 않으면 분석 결과가 왜곡되거나 모델이 제대로 학습되지 않습니다. 결측치를 어떻게 다루느냐에 따라 데이터 분석의 품질이 완전히 달라집니다.
개요
간단히 말해서, 결측치(Missing Value)는 데이터셋에서 값이 없는 상태를 의미합니다. Python에서는 주로 NaN(Not a Number), None, 또는 빈 문자열로 표현됩니다.
결측치가 생기는 이유는 정말 다양합니다. 설문조사에서 응답자가 대답하기 싫어서 건너뛴 경우(MCAR: Missing Completely At Random), 소득이 높은 사람이 연봉을 밝히기 꺼려해서 생긴 경우(MAR: Missing At Random), 또는 측정 장비가 특정 범위 밖의 값은 기록하지 못해서 생긴 경우(MNAR: Missing Not At Random) 등이 있습니다.
기존에는 결측치가 있는 데이터를 그냥 버리는 경우가 많았습니다. 하지만 이제는 pandas와 같은 강력한 도구를 사용해서 결측치를 확인하고, 패턴을 분석하고, 적절히 처리할 수 있습니다.
결측치 처리의 핵심은 "왜 값이 없는지"를 이해하는 것입니다. 이유에 따라 삭제할지, 평균값으로 채울지, 예측 모델로 추정할지가 달라집니다.
무작정 삭제하면 중요한 정보를 잃을 수 있고, 잘못 채우면 데이터가 왜곡됩니다.
코드 예제
import pandas as pd
import numpy as np
# 샘플 데이터 생성
data = {
'이름': ['김철수', '이영희', '박민수', '정지원', '최동욱'],
'나이': [25, np.nan, 30, 28, np.nan],
'연봉': [3000, 3500, np.nan, 4000, 3800],
'부서': ['개발', '마케팅', '개발', np.nan, '인사']
}
df = pd.DataFrame(data)
# 결측치 확인하기
print("결측치 개수:")
print(df.isnull().sum()) # 각 컬럼별 결측치 개수
print("\n결측치 비율:")
print(df.isnull().mean() * 100) # 결측치 비율(%)
# 결측치 시각화
print("\n결측치 위치 확인:")
print(df.isnull()) # True가 결측치 위치
설명
이것이 하는 일: 위 코드는 데이터프레임에서 결측치가 어디에 얼마나 있는지 확인하는 기본적인 방법을 보여줍니다. 첫 번째로, numpy의 np.nan을 사용해서 의도적으로 결측치가 있는 샘플 데이터를 만들었습니다.
실무에서는 CSV 파일을 읽어올 때 자동으로 빈 값이 NaN으로 처리됩니다. NaN은 "Not a Number"의 약자로, 숫자가 아니라는 뜻이 아니라 "값이 없다"는 의미입니다.
그 다음으로, isnull() 메서드를 사용하면 각 셀이 결측치인지 아닌지를 True/False로 알려줍니다. 여기에 sum()을 붙이면 각 컬럼별로 결측치가 몇 개인지 세어줍니다.
예를 들어 위 데이터에서 '나이' 컬럼은 2개, '연봉' 컬럼은 1개, '부서' 컬럼은 1개의 결측치가 있습니다. mean()을 사용하면 결측치의 비율을 알 수 있습니다.
True는 1로, False는 0으로 계산되기 때문에 평균을 내면 전체 중 결측치가 차지하는 비율이 나옵니다. 100을 곱하면 퍼센트로 표시되죠.
여러분이 이 코드를 사용하면 데이터 분석을 시작하기 전에 결측치 상황을 한눈에 파악할 수 있습니다. 결측치가 10% 미만이면 삭제를 고려하고, 30% 이상이면 대체 방법을 신중하게 선택해야 하며, 50% 이상이면 해당 컬럼 자체를 제거하는 것도 고려해야 합니다.
실전 팁
💡 결측치 확인은 항상 데이터 분석의 첫 단계로 해야 합니다. df.info()를 사용하면 각 컬럼의 non-null 개수를 한번에 볼 수 있어 더 빠릅니다.
💡 missingno 라이브러리를 사용하면 결측치를 시각적으로 예쁘게 볼 수 있습니다. "pip install missingno" 후 "msno.matrix(df)"를 실행해보세요.
💡 결측치가 특정 패턴을 가지고 있는지 확인하세요. 예를 들어 '연봉'이 높은 행에서만 결측치가 많다면, 이는 무작위가 아닌 의도적인 누락일 수 있습니다.
💡 df.isnull().sum().sum()을 사용하면 전체 데이터셋의 총 결측치 개수를 한 번에 확인할 수 있습니다.
2. 행 삭제 전략 - 결측치가 있는 데이터 줄 자체를 제거하기
시작하며
여러분이 100명의 설문조사 데이터를 가지고 있는데, 5명만 나이를 적지 않았다면 어떻게 하시겠어요? 이 5명의 데이터를 아예 제거하는 것이 가장 간단한 방법입니다.
이 방법은 "완전 케이스 분석(Complete Case Analysis)"이라고도 불리며, 통계학에서 가장 오래되고 가장 많이 사용되는 방법입니다. 결측치가 있는 행을 통째로 삭제하면 남은 데이터는 완전한 데이터만 남게 되어 분석이 쉬워집니다.
하지만 주의할 점이 있습니다. 결측치 비율이 높거나, 결측치가 무작위로 발생하지 않았다면 행을 삭제하는 것이 오히려 분석 결과를 왜곡할 수 있습니다.
언제 삭제하고 언제 보존해야 하는지 판단하는 것이 중요합니다.
개요
간단히 말해서, 행 삭제는 결측치가 하나라도 있는 행을 데이터셋에서 완전히 제거하는 방법입니다. pandas의 dropna() 메서드로 한 줄로 처리할 수 있습니다.
왜 이 방법이 필요할까요? 많은 머신러닝 알고리즘은 결측치를 받아들이지 않습니다.
sklearn의 대부분의 모델은 NaN 값이 있으면 에러를 발생시킵니다. 또한 통계 분석에서도 결측치가 있으면 계산이 복잡해지거나 불가능해집니다.
예를 들어, 상관관계 분석이나 회귀분석은 모든 변수에 값이 있어야 제대로 작동합니다. 기존에는 엑셀에서 일일이 필터를 걸어서 빈 칸이 있는 행을 찾아 삭제했다면, 이제는 dropna() 한 줄로 몇 초 만에 처리할 수 있습니다.
행 삭제의 핵심 장점은 간단하고 빠르며, 결과 데이터가 "깨끗하다"는 것입니다. 하지만 단점도 분명합니다.
데이터 손실이 발생하고, 특히 결측치가 많은 데이터셋에서는 대부분의 데이터를 잃을 수도 있습니다. 또한 결측치가 특정 패턴을 가지고 있다면(예: 고소득자만 연봉을 밝히지 않음) 편향된 데이터만 남게 됩니다.
코드 예제
import pandas as pd
import numpy as np
# 샘플 데이터
data = {
'이름': ['김철수', '이영희', '박민수', '정지원', '최동욱'],
'나이': [25, np.nan, 30, 28, 33],
'연봉': [3000, 3500, np.nan, 4000, 3800]
}
df = pd.DataFrame(data)
print("원본 데이터:")
print(df)
print(f"\n원본 행 개수: {len(df)}")
# 결측치가 있는 모든 행 삭제
df_dropped = df.dropna()
print("\n결측치 행 삭제 후:")
print(df_dropped)
print(f"남은 행 개수: {len(df_dropped)}")
# 특정 컬럼의 결측치만 고려해서 삭제
df_dropped_subset = df.dropna(subset=['나이'])
print("\n'나이' 컬럼 결측치만 삭제:")
print(df_dropped_subset)
설명
이것이 하는 일: 위 코드는 결측치가 있는 행을 여러 방식으로 삭제하는 방법을 보여줍니다. 첫 번째로, df.dropna()를 기본 설정으로 실행하면 하나의 컬럼이라도 결측치가 있는 행은 모두 제거됩니다.
위 예시에서 이영희(나이 결측)와 박민수(연봉 결측)의 행이 삭제되어 5개 행 중 3개만 남습니다. 이것을 "행 단위 완전 삭제(listwise deletion)"라고 합니다.
그 다음으로, subset 매개변수를 사용하면 특정 컬럼의 결측치만 고려할 수 있습니다. subset=['나이']로 설정하면 '나이' 컬럼에 결측치가 있는 행만 삭제하고, '연봉' 컬럼의 결측치는 무시합니다.
이 경우 이영희의 행만 삭제되고 박민수의 행은 유지됩니다. 이 방법은 분석에 꼭 필요한 컬럼만 완전한 데이터로 확보하고 싶을 때 유용합니다.
inplace=True 옵션을 사용하면 원본 데이터프레임을 직접 수정합니다. 기본적으로 dropna()는 새로운 데이터프레임을 반환하고 원본은 그대로 유지하지만, inplace=True를 쓰면 원본이 변경됩니다.
실무에서는 원본을 보존하는 것이 안전하므로 inplace를 사용하지 않는 것을 추천합니다. thresh 매개변수도 알아두면 좋습니다.
thresh=2로 설정하면 "최소 2개 이상의 non-null 값이 있는 행만 유지"하라는 뜻입니다. 예를 들어 10개 컬럼 중 8개가 결측치인 행은 제거하고 싶을 때 유용합니다.
여러분이 이 방법을 사용하면 빠르게 깨끗한 데이터셋을 만들 수 있습니다. 하지만 항상 삭제 전후의 데이터 개수를 확인하고, 너무 많은 데이터가 손실되지 않는지 체크해야 합니다.
일반적으로 전체 데이터의 5% 미만이 삭제될 때 이 방법이 안전합니다.
실전 팁
💡 삭제하기 전에 반드시 df.isnull().sum()으로 결측치 분포를 확인하세요. 한 컬럼에 결측치가 몰려있다면 행 삭제 대신 그 컬럼을 삭제하는 것이 나을 수 있습니다.
💡 원본 데이터는 항상 보존하세요. df_clean = df.dropna()처럼 새 변수에 저장하고, 나중에 비교할 수 있도록 하세요.
💡 시계열 데이터에서는 행 삭제를 조심하세요. 중간에 데이터가 빠지면 시간 순서가 깨질 수 있습니다.
💡 how='all' 옵션을 사용하면 모든 값이 결측치인 행만 삭제합니다. 완전히 빈 행만 제거하고 싶을 때 유용합니다.
💡 삭제된 데이터의 특성을 분석하세요. 삭제된 행들이 특정 패턴을 가진다면(예: 모두 고령자) 이는 샘플링 편향을 일으킬 수 있습니다.
3. 열 삭제 전략 - 결측치가 많은 컬럼 자체를 제거하기
시작하며
여러분이 100개의 컬럼이 있는 데이터를 받았는데, 그 중 한 컬럼이 80%가 비어있다면 어떻게 하시겠어요? 이 컬럼을 채우려고 애쓰기보다는 아예 제거하는 것이 현명할 수 있습니다.
실제 실무에서는 데이터 수집 과정에서 문제가 생겨 특정 컬럼에만 결측치가 집중되는 경우가 많습니다. 예를 들어 센서가 고장 나서 특정 측정값만 기록이 안 되거나, 설문지에서 특정 질문을 대부분이 건너뛴 경우입니다.
이럴 때는 해당 컬럼 전체를 삭제하는 것이 가장 실용적입니다. 80%가 비어있는 컬럼을 억지로 채워봤자 대부분 추정값이 되어 분석의 신뢰도가 떨어집니다.
개요
간단히 말해서, 열 삭제는 결측치 비율이 높은 컬럼을 데이터셋에서 완전히 제거하는 방법입니다. 보통 결측치가 50% 이상인 컬럼은 삭제를 고려합니다.
왜 이 방법이 필요할까요? 결측치가 많은 컬럼은 정보의 가치가 낮습니다.
10명 중 9명의 데이터가 없다면 그 1명의 데이터로는 유의미한 패턴을 찾기 어렵습니다. 또한 이런 컬럼을 억지로 채우면 실제 데이터보다 추정값이 더 많아져서 "가짜 정보"로 분석하는 셈이 됩니다.
실무에서는 불완전한 특성보다는 완전한 특성 몇 개로 분석하는 것이 훨씬 정확합니다. 기존에는 각 컬럼의 결측치 비율을 수작업으로 계산해서 판단했다면, 이제는 pandas로 자동으로 결측치 비율을 계산하고 임계값 이상인 컬럼을 한 번에 제거할 수 있습니다.
열 삭제의 핵심은 "정보 손실 vs 데이터 품질"의 트레이드오프입니다. 컬럼을 삭제하면 해당 특성의 정보는 완전히 잃지만, 남은 데이터의 품질은 높아집니다.
일반적으로 결측치 비율이 50-70% 이상이면 삭제를, 30-50%면 대체 방법을, 30% 미만이면 보존을 고려합니다.
코드 예제
import pandas as pd
import numpy as np
# 샘플 데이터 (여러 컬럼에 다양한 결측치 비율)
data = {
'이름': ['김철수', '이영희', '박민수', '정지원', '최동욱'],
'나이': [25, 30, 30, 28, 33], # 결측치 0%
'연봉': [3000, 3500, np.nan, 4000, 3800], # 결측치 20%
'취미': [np.nan, np.nan, np.nan, np.nan, '독서'], # 결측치 80%
'차량': [np.nan, np.nan, np.nan, np.nan, np.nan] # 결측치 100%
}
df = pd.DataFrame(data)
# 각 컬럼의 결측치 비율 계산
missing_ratio = df.isnull().mean()
print("컬럼별 결측치 비율:")
print(missing_ratio)
# 결측치가 50% 이상인 컬럼 찾기
threshold = 0.5
cols_to_drop = missing_ratio[missing_ratio > threshold].index
print(f"\n삭제할 컬럼 (결측치 {threshold*100}% 이상): {list(cols_to_drop)}")
# 해당 컬럼들 삭제
df_cleaned = df.drop(columns=cols_to_drop)
print("\n컬럼 삭제 후:")
print(df_cleaned)
설명
이것이 하는 일: 위 코드는 각 컬럼의 결측치 비율을 자동으로 계산하고, 임계값 이상인 컬럼을 찾아서 삭제하는 과정을 보여줍니다. 첫 번째로, df.isnull().mean()을 사용해서 각 컬럼의 결측치 비율을 계산합니다.
isnull()은 True/False를 반환하고, mean()은 True를 1로 계산하기 때문에 결과적으로 0과 1 사이의 비율이 나옵니다. 예를 들어 0.8이면 80%가 결측치라는 뜻입니다.
그 다음으로, 임계값(threshold)을 설정합니다. 위 코드에서는 0.5(50%)로 설정했는데, 이는 절반 이상이 비어있으면 삭제하겠다는 의미입니다.
이 값은 여러분의 상황에 따라 조정할 수 있습니다. 보수적으로 접근하려면 0.7이나 0.8로 설정하고, 공격적으로 정리하려면 0.3이나 0.4로 낮출 수 있습니다.
missing_ratio[missing_ratio > threshold]는 불린 인덱싱을 사용해서 임계값을 초과하는 컬럼만 선택합니다. .index를 붙이면 컬럼 이름들을 리스트로 얻을 수 있습니다.
위 예시에서는 '취미'(80%)와 '차량'(100%)이 선택됩니다. 마지막으로, df.drop(columns=cols_to_drop)을 사용해서 선택된 컬럼들을 한 번에 삭제합니다.
drop()의 columns 매개변수에 리스트를 전달하면 여러 컬럼을 동시에 삭제할 수 있습니다. 결과적으로 '이름', '나이', '연봉' 컬럼만 남게 됩니다.
여러분이 이 코드를 사용하면 수십, 수백 개의 컬럼이 있는 대규모 데이터셋에서도 자동으로 불필요한 컬럼을 정리할 수 있습니다. 특히 공공 데이터나 설문조사 데이터처럼 많은 변수가 있지만 대부분 비어있는 경우에 매우 유용합니다.
실무에서는 이 과정을 탐색적 데이터 분석(EDA)의 첫 단계로 수행하는 것이 일반적입니다.
실전 팁
💡 임계값을 정하기 전에 도메인 지식을 활용하세요. 결측치가 많아도 비즈니스적으로 중요한 컬럼(예: 구매 금액)은 보존하는 것이 좋습니다.
💡 삭제하기 전에 왜 해당 컬럼에 결측치가 많은지 조사하세요. 데이터 수집 프로세스의 문제일 수 있고, 이를 개선하면 향후 더 나은 데이터를 얻을 수 있습니다.
💡 컬럼 삭제 후에는 반드시 df.shape로 데이터프레임의 크기를 확인하세요. 너무 많은 컬럼이 삭제되었다면 임계값을 조정해야 합니다.
💡 삭제된 컬럼 목록을 기록해두세요. 나중에 보고서를 작성할 때 "어떤 변수를 제외했는지"를 명시해야 합니다.
💡 여러 임계값을 실험해보세요. threshold=[0.3, 0.5, 0.7]로 반복문을 돌려서 각 경우에 얼마나 많은 컬럼이 삭제되는지 비교해보면 적절한 값을 찾을 수 있습니다.
4. 평균값 대체 - 숫자 데이터의 가장 기본적인 채우기
시작하며
여러분이 학생 10명의 키를 측정했는데 2명의 키가 빠져있다면, 나머지 8명의 평균 키로 채우면 어떨까요? 이것이 바로 평균값 대체 방법입니다.
이 방법은 가장 직관적이고 이해하기 쉬운 결측치 처리 방법입니다. "평균적인 값"으로 채우기 때문에 전체 데이터의 평균이 변하지 않고, 구현도 매우 간단합니다.
통계학에서 오랫동안 사용되어온 검증된 방법입니다. 하지만 조심해야 할 점이 있습니다.
평균값으로 채우면 데이터의 분산이 줄어들고, 변수 간의 상관관계가 왜곡될 수 있습니다. 예를 들어 키가 작은 사람과 큰 사람의 차이가 줄어들어 데이터가 평균 쪽으로 몰리게 됩니다.
개요
간단히 말해서, 평균값 대체(Mean Imputation)는 결측치를 해당 컬럼의 평균값으로 채우는 방법입니다. pandas의 fillna(df['컬럼'].mean())으로 쉽게 구현할 수 있습니다.
왜 이 방법이 필요할까요? 많은 경우 평균값은 "가장 무난한 추측"입니다.
어떤 사람의 키를 모를 때 극단적으로 크거나 작다고 가정하는 것보다는 평균 키라고 가정하는 것이 더 합리적입니다. 또한 회귀분석이나 머신러닝 모델은 완전한 데이터를 요구하는데, 평균값 대체를 사용하면 빠르게 완전한 데이터셋을 만들 수 있습니다.
특히 결측치가 무작위로 발생한 경우(MCAR)에 효과적입니다. 기존에는 엑셀에서 AVERAGE 함수로 평균을 계산한 다음 빈 셀에 직접 입력했다면, 이제는 pandas로 한 줄에 모든 결측치를 자동으로 채울 수 있습니다.
평균값 대체의 장점은 구현이 간단하고, 데이터의 평균이 보존되며, 빠르게 처리할 수 있다는 것입니다. 단점은 분산이 줄어들어 데이터가 "너무 평균적"이 되고, 변수 간의 관계가 약화되며, 이상치의 영향을 크게 받는다는 것입니다.
예를 들어 연봉 데이터에 억만장자 한 명이 있으면 평균이 크게 올라갑니다.
코드 예제
import pandas as pd
import numpy as np
# 샘플 데이터
data = {
'이름': ['김철수', '이영희', '박민수', '정지원', '최동욱', '한민재'],
'나이': [25, np.nan, 30, 28, np.nan, 35],
'연봉': [3000, 3500, np.nan, 4000, 3800, np.nan]
}
df = pd.DataFrame(data)
print("원본 데이터:")
print(df)
# 평균값으로 결측치 채우기
df['나이_평균대체'] = df['나이'].fillna(df['나이'].mean())
df['연봉_평균대체'] = df['연봉'].fillna(df['연봉'].mean())
print("\n평균값 대체 후:")
print(df)
# 평균값 확인
print(f"\n나이 평균: {df['나이'].mean():.2f}")
print(f"연봉 평균: {df['연봉'].mean():.2f}")
# 원본과 대체 후 통계 비교
print("\n통계 비교:")
print(df[['나이', '나이_평균대체']].describe())
설명
이것이 하는 일: 위 코드는 숫자형 컬럼의 결측치를 해당 컬럼의 평균값으로 대체하는 과정을 보여줍니다. 첫 번째로, df['나이'].mean()으로 '나이' 컬럼의 평균을 계산합니다.
이 계산에서는 NaN 값이 자동으로 제외됩니다. 위 예시에서 나이가 있는 사람은 25, 30, 28, 35로 4명이고, 평균은 (25+30+28+35)/4 = 29.5세입니다.
엑셀처럼 결측치를 신경 쓸 필요 없이 pandas가 알아서 처리해줍니다. 그 다음으로, fillna() 메서드에 계산된 평균값을 전달합니다.
fillna()는 NaN인 위치를 찾아서 전달된 값으로 채워줍니다. 위 코드에서는 원본 컬럼을 보존하고 새로운 컬럼을 만들었지만, df['나이'] = df['나이'].fillna(...)처럼 원본을 직접 수정할 수도 있습니다.
describe() 메서드를 사용하면 대체 전후의 통계를 비교할 수 있습니다. 주목할 점은 평균은 거의 같지만(소수점 오차만 있음), 표준편차(std)가 줄어든다는 것입니다.
이는 평균값으로 채운 데이터가 극단값이 없어서 변동성이 감소했기 때문입니다. round() 함수를 사용하면 소수점을 정리할 수 있습니다.
나이는 보통 정수로 표현하므로 df['나이'].fillna(df['나이'].mean()).round()처럼 반올림할 수 있습니다. 하지만 연봉처럼 소수점이 의미 있는 경우는 그대로 두는 것이 좋습니다.
여러분이 이 방법을 사용하면 몇 초 만에 완전한 데이터셋을 만들 수 있습니다. 하지만 결측치 비율이 30%를 넘으면 데이터의 원래 분포가 크게 왜곡될 수 있으므로 주의해야 합니다.
또한 나이와 연봉처럼 관련이 있는 변수들을 따로 평균으로 채우면 둘 사이의 상관관계가 약해질 수 있습니다(실제로는 나이가 많을수록 연봉이 높은 경향이 있는데, 평균값으로 채우면 이 패턴이 희석됨).
실전 팁
💡 이상치가 있다면 평균 대신 중앙값(median)을 사용하세요. 연봉처럼 극단값에 민감한 데이터는 median()이 더 안정적입니다.
💡 그룹별 평균을 사용하면 더 정확합니다. df.groupby('부서')['연봉'].transform('mean')을 사용하면 같은 부서 내 평균으로 채울 수 있습니다.
💡 대체 전후의 히스토그램을 그려서 분포 변화를 시각적으로 확인하세요. import matplotlib.pyplot as plt로 간단히 확인할 수 있습니다.
💡 sklearn의 SimpleImputer를 사용하면 여러 컬럼을 한 번에 처리할 수 있고, 학습 데이터의 평균을 테스트 데이터에도 일관되게 적용할 수 있습니다.
💡 평균값으로 채운 행에 플래그를 추가하세요. df['나이_결측여부'] = df['나이'].isnull()처럼 원래 결측치였던 행을 표시하면 나중에 모델링 시 이 정보를 활용할 수 있습니다.
5. 중앙값과 최빈값 대체 - 더 견고한 채우기 방법
시작하며
여러분이 직원들의 연봉 데이터를 분석하는데, CEO 한 명의 연봉이 10억이고 나머지는 3천만원대라면 평균은 얼마나 될까요? 아마 5천만원쯤 나올 텐데, 이건 대부분 직원의 실제 연봉과는 거리가 멉니다.
이럴 때 평균 대신 중앙값(중간값)을 사용하면 더 현실적인 값을 얻을 수 있습니다. 중앙값은 데이터를 크기순으로 정렬했을 때 정중앙에 있는 값으로, 극단값의 영향을 받지 않습니다.
위 예시에서는 3천만원대가 중앙값이 됩니다. 또한 성별이나 직급처럼 범주형(카테고리) 데이터에는 최빈값을 사용합니다.
최빈값은 가장 자주 나타나는 값으로, "평균적인 카테고리"를 의미합니다.
개요
간단히 말해서, 중앙값 대체(Median Imputation)는 결측치를 중앙값으로, 최빈값 대체(Mode Imputation)는 가장 흔한 값으로 채우는 방법입니다. pandas에서는 median()과 mode()로 쉽게 구현할 수 있습니다.
왜 이 방법들이 필요할까요? 중앙값은 이상치(outlier)에 강합니다.
연봉, 집값, 대출금액처럼 소수의 극단값이 있는 데이터에서는 평균보다 중앙값이 더 대표성이 있습니다. 최빈값은 범주형 데이터의 "가장 일반적인" 값을 나타냅니다.
성별 데이터에서 결측치가 있다면 평균을 계산할 수 없지만, 최빈값(예: 남성이 더 많다면 '남성')으로 채울 수 있습니다. 기존에는 데이터를 정렬해서 중간값을 직접 찾거나, 각 카테고리를 세어서 가장 많은 것을 수작업으로 찾았다면, 이제는 median()과 mode() 한 줄로 처리할 수 있습니다.
중앙값의 장점은 이상치에 강하고, 왜곡된 분포(skewed distribution)에서도 잘 작동한다는 것입니다. 최빈값의 장점은 범주형 데이터를 처리할 수 있고, "가장 흔한" 값으로 채우므로 직관적이라는 것입니다.
둘 다 평균보다 극단값의 영향을 덜 받습니다.
코드 예제
import pandas as pd
import numpy as np
# 샘플 데이터 (이상치 포함)
data = {
'이름': ['김철수', '이영희', '박민수', '정지원', '최동욱', 'CEO'],
'연봉': [3000, 3500, np.nan, 3200, np.nan, 100000], # CEO는 이상치
'직급': ['사원', '사원', '대리', np.nan, '사원', '임원'],
'성별': ['남', '여', '남', np.nan, np.nan, '남']
}
df = pd.DataFrame(data)
print("원본 데이터:")
print(df)
# 연봉: 평균 vs 중앙값 비교
mean_salary = df['연봉'].mean()
median_salary = df['연봉'].median()
print(f"\n연봉 평균: {mean_salary:.0f}만원")
print(f"연봉 중앙값: {median_salary:.0f}만원")
# 중앙값으로 대체
df['연봉_중앙값대체'] = df['연봉'].fillna(median_salary)
# 최빈값으로 대체 (범주형 데이터)
mode_position = df['직급'].mode()[0] # mode()는 시리즈를 반환하므로 [0]으로 첫 값 선택
mode_gender = df['성별'].mode()[0]
df['직급_최빈값대체'] = df['직급'].fillna(mode_position)
df['성별_최빈값대체'] = df['성별'].fillna(mode_gender)
print("\n대체 후 데이터:")
print(df)
print(f"\n직급 최빈값: {mode_position}")
print(f"성별 최빈값: {mode_gender}")
설명
이것이 하는 일: 위 코드는 숫자형 데이터에는 중앙값을, 범주형 데이터에는 최빈값을 사용해서 결측치를 대체하는 방법을 보여줍니다. 첫 번째로, 연봉 데이터에서 평균과 중앙값을 비교합니다.
CEO의 연봉 10만(만원 단위)이 극단값이므로 평균은 약 22,340만원으로 크게 왜곡되지만, 중앙값은 3,350만원으로 일반 직원들의 실제 연봉 범위에 가깝습니다. median() 함수는 데이터를 자동으로 정렬한 후 중간값을 찾아주므로 수작업으로 정렬할 필요가 없습니다.
그 다음으로, fillna(median_salary)를 사용해서 결측치를 중앙값으로 채웁니다. 위 예시에서 박민수와 최동욱의 연봉은 3,350만원으로 채워집니다.
이 값은 CEO의 극단적인 연봉에 영향받지 않은 "일반적인" 연봉을 나타냅니다. 범주형 데이터인 '직급'과 '성별'에는 mode()를 사용합니다.
mode()는 가장 많이 나타나는 값을 찾아줍니다. 주의할 점은 mode()가 Series를 반환하기 때문에 [0]으로 첫 번째 값을 선택해야 한다는 것입니다.
만약 최빈값이 여러 개라면(예: 사원과 대리가 동일하게 2명) 모두 반환되는데, 보통 첫 번째 값을 사용합니다. 범주형 데이터에 평균이나 중앙값을 사용할 수 없는 이유는 숫자 연산이 불가능하기 때문입니다.
'남'과 '여'의 평균을 계산할 수는 없죠. 하지만 최빈값은 "가장 흔한 카테고리"라는 의미에서 합리적입니다.
위 예시에서 '성별' 결측치는 '남'(3명)으로 채워집니다. 여러분이 이 방법을 사용하면 데이터의 특성에 맞게 더 적절한 대체값을 선택할 수 있습니다.
특히 실무에서 연봉, 가격, 거래량처럼 왜곡된 분포를 가진 데이터는 매우 흔하므로 중앙값을 기본으로 고려하는 것이 좋습니다. 범주형 데이터는 무조건 최빈값이나 특정 카테고리로만 채울 수 있습니다.
실전 팁
💡 df.skew()로 데이터의 왜도를 확인하세요. 절댓값이 0.5 이상이면 평균 대신 중앙값을 사용하는 것이 좋습니다.
💡 최빈값이 여러 개일 때를 대비해 mode()[0] 대신 mode().iloc[0]을 사용하면 더 안전합니다.
💡 연봉처럼 로그 정규분포를 따르는 데이터는 로그 변환 후 평균을 사용하는 것도 좋은 방법입니다: np.log(df['연봉']).mean()
💡 범주형 데이터에서 결측치가 의미를 가질 수 있습니다. 예를 들어 '결혼여부' 결측치는 "답변 거부"일 수 있으므로 새로운 카테고리 '미응답'으로 처리하는 것이 더 나을 수 있습니다.
💡 value_counts()로 각 카테고리의 빈도를 확인한 후 최빈값 대체를 사용하세요. 만약 카테고리들이 비슷한 빈도라면 최빈값 대체가 별 의미가 없을 수 있습니다.
6. 전방채우기와 후방채우기 - 시계열 데이터의 특별한 처리
시작하며
여러분이 매일 주식 가격을 기록하는데, 월요일 가격이 빠졌다면 어떻게 채우시겠어요? 금요일 가격으로 채우는 것이 억만원의 평균보다 훨씬 합리적입니다!
시계열 데이터는 시간 순서가 중요합니다. 이런 데이터에서는 바로 이전 값이나 바로 다음 값으로 채우는 것이 가장 자연스럽습니다.
주식 가격, 온도, 센서 측정값처럼 연속적으로 변하는 데이터는 갑자기 튀지 않기 때문입니다. 전방채우기(Forward Fill)는 앞의 값으로, 후방채우기(Backward Fill)는 뒤의 값으로 채우는 방법입니다.
마치 "오늘 데이터가 없으면 어제 데이터를 그대로 사용하기"처럼 작동합니다.
개요
간단히 말해서, 전방채우기(ffill)는 결측치를 바로 이전 값으로, 후방채우기(bfill)는 바로 다음 값으로 채우는 방법입니다. pandas의 fillna(method='ffill')이나 fillna(method='bfill')로 구현할 수 있습니다.
왜 이 방법이 필요할까요? 시계열 데이터는 시간적 연속성을 가집니다.
오늘의 기온이 어제와 크게 다르지 않고, 오늘의 주식 가격이 어제와 비슷한 경향이 있습니다. 센서 데이터에서 측정값이 일시적으로 누락되었다면, 그 순간의 값은 직전 값과 거의 같다고 가정하는 것이 합리적입니다.
이는 전체 데이터의 평균이나 중앙값보다 훨씬 정확한 추정입니다. 기존에는 엑셀에서 빈 셀을 찾아 위 셀의 값을 복사-붙여넣기 했다면, 이제는 ffill() 한 줄로 모든 결측치가 자동으로 앞 값으로 채워집니다.
전방채우기의 장점은 시간적 연속성을 유지하고, 급격한 변화를 방지하며, 구현이 간단하다는 것입니다. 후방채우기는 미래 값을 알 때 사용하며, 특히 데이터의 끝부분 결측치를 처리할 때 유용합니다.
단점은 결측치가 연속으로 많으면 같은 값이 계속 반복되고, 첫 번째나 마지막 값이 결측이면 채울 수 없다는 것입니다.
코드 예제
import pandas as pd
import numpy as np
# 시계열 샘플 데이터 (주식 가격)
data = {
'날짜': pd.date_range('2024-01-01', periods=10, freq='D'),
'종가': [100, 102, np.nan, np.nan, 105, 107, np.nan, 110, 112, np.nan]
}
df = pd.DataFrame(data)
print("원본 데이터:")
print(df)
# 전방채우기 (앞의 값으로 채우기)
df['종가_전방채우기'] = df['종가'].ffill()
# 후방채우기 (뒤의 값으로 채우기)
df['종가_후방채우기'] = df['종가'].bfill()
# 선형 보간 (앞뒤 값의 평균으로 채우기)
df['종가_보간'] = df['종가'].interpolate()
print("\n채우기 방법별 비교:")
print(df)
# 시각화용 통계
print("\n평균값 비교:")
print(f"원본 평균: {df['종가'].mean():.2f}")
print(f"전방채우기 평균: {df['종가_전방채우기'].mean():.2f}")
print(f"후방채우기 평균: {df['종가_후방채우기'].mean():.2f}")
print(f"보간 평균: {df['종가_보간'].mean():.2f}")
설명
이것이 하는 일: 위 코드는 시계열 데이터에서 결측치를 시간 순서를 고려해서 채우는 여러 방법을 비교합니다. 첫 번째로, ffill()(forward fill의 약자)을 사용하면 각 결측치가 바로 앞의 값으로 채워집니다.
위 예시에서 1월 3일(인덱스 2)의 결측치는 1월 2일의 102로, 1월 4일(인덱스 3)의 결측치도 102로 채워집니다. 연속된 결측치가 있으면 모두 같은 값(가장 최근의 유효한 값)으로 채워지는 것이죠.
이는 "마지막으로 알려진 값이 계속 유지된다"는 가정을 반영합니다. 그 다음으로, bfill()(backward fill의 약자)을 사용하면 반대로 뒤의 값으로 채워집니다.
1월 3일과 4일의 결측치는 1월 5일의 105로 채워집니다. 마지막 날(1월 10일)의 결측치는 뒤에 값이 없으므로 그대로 NaN으로 남습니다.
후방채우기는 "미래 값을 이미 알고 있을 때" 사용하며, 데이터 보정이나 사후 분석에서 유용합니다. interpolate()는 더 정교한 방법으로, 앞뒤 값을 선형적으로 보간합니다.
예를 들어 102와 105 사이의 결측치 2개는 103과 104로 채워져 부드러운 전환을 만듭니다. 이는 값이 선형적으로 변한다고 가정하며, 주가, 온도, 센서 데이터처럼 급격한 변화가 없는 경우에 가장 현실적입니다.
limit 매개변수를 사용하면 채울 결측치 개수를 제한할 수 있습니다. df['종가'].ffill(limit=1)을 사용하면 연속된 결측치 중 첫 1개만 채우고 나머지는 그대로 둡니다.
이는 결측치가 너무 길게 이어질 때 같은 값이 계속 반복되는 것을 방지합니다. 여러분이 이 방법을 사용하면 시계열 데이터의 흐름을 자연스럽게 유지하면서 결측치를 처리할 수 있습니다.
주식 거래 시스템, IoT 센서 모니터링, 기상 데이터 분석 등 실시간 또는 연속 측정 데이터에서 매우 유용합니다. 평균값 대체를 사용하면 갑자기 평균값으로 튀는 것처럼 보이지만, ffill이나 interpolate를 사용하면 자연스러운 변화를 유지할 수 있습니다.
실전 팁
💡 시계열 데이터는 먼저 날짜/시간 순으로 정렬하세요: df.sort_values('날짜', inplace=True). 정렬되지 않으면 ffill이 의도대로 작동하지 않습니다.
💡 금융 데이터에서는 주말/공휴일을 고려하세요. 금요일 종가를 월요일로 ffill하는 것은 정상이지만, 1주일 이상 같은 값이 반복되면 문제가 있는 것입니다.
💡 interpolate(method='time')을 사용하면 시간 간격을 고려한 보간이 가능합니다. 예를 들어 1시간 뒤 데이터와 10시간 뒤 데이터를 다르게 가중치를 줍니다.
💡 센서 데이터에서는 ffill보다 interpolate를 선호하세요. 온도, 습도, 압력 등은 급격히 변하지 않으므로 선형 보간이 더 정확합니다.
💡 결측치가 연속으로 5개 이상이면 ffill/bfill 대신 삭제를 고려하세요. 너무 오래된 값으로 채우면 현재 상황을 제대로 반영하지 못합니다.
7. KNN 대체 - 유사한 데이터로 똑똑하게 채우기
시작하며
여러분이 30세 남성 개발자의 연봉이 빠졌다면, 비슷한 나이와 직업을 가진 다른 사람들의 평균 연봉으로 채우는 것이 전체 평균보다 정확하지 않을까요? 이것이 바로 KNN(K-Nearest Neighbors) 대체의 아이디어입니다.
결측치가 있는 데이터와 비슷한 다른 데이터를 찾아서, 그들의 값으로 채우는 방법입니다. 마치 "너와 비슷한 친구들은 이렇던데, 너도 이럴 거야"라고 추측하는 것과 같습니다.
이 방법은 단순히 전체 평균을 사용하는 것보다 훨씬 정교하고, 변수 간의 관계를 고려합니다. 예를 들어 나이와 연봉의 상관관계, 직급과 연봉의 관계를 자동으로 반영합니다.
개요
간단히 말해서, KNN 대체는 결측치가 있는 행과 가장 유사한 K개의 행을 찾아, 그들의 평균값으로 채우는 방법입니다. sklearn의 KNNImputer로 쉽게 구현할 수 있습니다.
왜 이 방법이 필요할까요? 실제 데이터에서는 변수들이 서로 관련되어 있습니다.
나이가 많으면 연봉이 높고, 경력이 많으면 직급이 높은 식입니다. 전체 평균이나 중앙값으로 채우면 이런 관계가 무시되지만, KNN은 비슷한 특성을 가진 데이터를 참고하므로 더 정확합니다.
예를 들어 40세 임원의 연봉 결측치를 채울 때, 20대 신입사원까지 포함한 전체 평균보다는 비슷한 나이와 직급의 사람들 평균이 훨씬 현실적입니다. 기존에는 이런 유사도 기반 대체를 수작업으로 하려면 복잡한 그룹화와 계산이 필요했지만, 이제는 KNNImputer가 자동으로 거리를 계산하고 가장 가까운 이웃을 찾아서 채워줍니다.
KNN 대체의 핵심은 "거리(distance)" 개념입니다. 나이 30세, 경력 5년인 사람과 나이 32세, 경력 6년인 사람은 "가깝고", 나이 50세, 경력 25년인 사람과는 "멀다"고 판단합니다.
일반적으로 유클리드 거리를 사용하며, K는 보통 3~7 사이의 값을 선택합니다. K가 너무 작으면 이상치의 영향을 받고, 너무 크면 평균값 대체와 비슷해집니다.
코드 예제
import pandas as pd
import numpy as np
from sklearn.impute import KNNImputer
# 샘플 데이터 (나이와 연봉이 상관관계를 가짐)
data = {
'이름': ['김철수', '이영희', '박민수', '정지원', '최동욱', '한민재', '윤서연'],
'나이': [25, 28, 30, np.nan, 35, 40, np.nan],
'경력년수': [2, 3, 5, 6, np.nan, 15, 8],
'연봉': [3000, 3500, 4000, np.nan, 5000, np.nan, 4500]
}
df = pd.DataFrame(data)
print("원본 데이터:")
print(df)
# KNN Imputer 설정 (K=3: 가장 가까운 3개 이웃 참고)
imputer = KNNImputer(n_neighbors=3)
# 숫자형 컬럼만 선택
numeric_cols = ['나이', '경력년수', '연봉']
df_numeric = df[numeric_cols]
# KNN 대체 수행
df_imputed = pd.DataFrame(
imputer.fit_transform(df_numeric),
columns=numeric_cols
)
# 이름 컬럼 다시 추가
df_imputed.insert(0, '이름', df['이름'])
print("\nKNN 대체 후:")
print(df_imputed.round(0)) # 소수점 제거
# 비교: 평균 대체 vs KNN 대체
print("\n정지원의 연봉 비교:")
print(f"전체 평균: {df['연봉'].mean():.0f}만원")
print(f"KNN 대체: {df_imputed.loc[3, '연봉']:.0f}만원")
설명
이것이 하는 일: 위 코드는 KNN 알고리즘을 사용해서 결측치를 유사한 데이터 기반으로 채우는 과정을 보여줍니다. 첫 번째로, KNNImputer(n_neighbors=3)을 생성합니다.
n_neighbors는 몇 개의 유사한 데이터를 참고할지 결정하는 K값입니다. 3으로 설정하면 가장 가까운 3개 행의 평균을 사용합니다.
예를 들어 정지원(인덱스 3)의 나이가 결측이면, 경력년수 6년과 가장 유사한 3명을 찾습니다. 경력년수가 5년인 박민수, 8년인 윤서연, 3년인 이영희가 선택될 수 있고, 이들의 나이 평균을 사용합니다.
그 다음으로, 숫자형 컬럼만 선택합니다. KNNImputer는 거리 계산을 위해 숫자가 필요하므로 '이름'같은 문자열 컬럼은 제외합니다.
만약 '성별', '부서' 같은 범주형 변수가 있다면 먼저 원-핫 인코딩이나 라벨 인코딩으로 숫자로 변환해야 합니다. fit_transform()을 호출하면 실제 대체가 수행됩니다.
"fit"은 데이터의 구조를 학습하고, "transform"은 결측치를 채운 새로운 데이터를 반환합니다. 내부적으로는 각 행 간의 유클리드 거리를 계산합니다: distance = sqrt((나이1-나이2)² + (경력1-경력2)² + (연봉1-연봉2)²).
결측치가 있는 부분은 거리 계산에서 제외됩니다. 결과를 보면 정지원의 연봉은 전체 평균(4000만원)이 아니라 비슷한 경력년수를 가진 사람들의 평균으로 채워집니다.
경력 6년은 박민수(5년, 4000만원), 윤서연(8년, 4500만원)과 가까우므로 이들의 평균인 약 4250만원으로 채워질 것입니다. 이는 신입사원까지 포함한 전체 평균보다 훨씬 정확합니다.
여러분이 이 방법을 사용하면 다변량 관계를 고려한 정교한 대체가 가능합니다. 특히 여러 변수가 서로 연관된 복잡한 데이터셋(고객 프로필, 의료 데이터, 금융 데이터 등)에서 매우 유용합니다.
단점은 계산량이 많아 대규모 데이터에서는 느리고, 모든 컬럼이 숫자여야 한다는 제약이 있습니다.
실전 팁
💡 스케일링을 먼저 하세요! 나이(20-60)와 연봉(3000-10000)의 스케일이 다르면 거리 계산이 왜곡됩니다. StandardScaler로 표준화한 후 KNN을 적용하세요.
💡 K값을 실험해보세요. K=3, 5, 7로 시도한 후 교차검증으로 최적값을 찾으세요. 일반적으로 sqrt(n) 정도가 좋은 출발점입니다.
💡 범주형 변수는 pd.get_dummies()로 원-핫 인코딩하세요. '부서'가 있다면 '부서_개발', '부서_마케팅' 같은 더미 변수로 변환 후 KNN을 적용합니다.
💡 IterativeImputer도 고려하세요. sklearn의 또 다른 고급 방법으로, 회귀 모델을 사용해 결측치를 예측합니다. KNN보다 더 정교하지만 느립니다.
💡 결측치가 30% 이상인 컬럼에서는 KNN도 불안정합니다. 먼저 열 삭제로 고결측 컬럼을 제거한 후 남은 컬럼에 KNN을 적용하세요.
8. 다중 대체 - 여러 가능성을 고려하는 고급 기법
시작하며
여러분이 누군가의 키를 추측해야 하는데, 정확한 값을 알 수 없다면 어떻게 하시겠어요? "정확히 170cm다"라고 하나만 찍는 것보다, "165cm일 수도, 170cm일 수도, 175cm일 수도 있다"고 여러 가능성을 고려하는 것이 더 안전합니다.
이것이 바로 다중 대체(Multiple Imputation)의 핵심 아이디어입니다. 결측치를 한 가지 값으로 채우는 것이 아니라, 여러 번 다르게 채운 데이터셋을 여러 개 만들고, 각각 분석한 후 결과를 통합합니다.
이 방법은 통계학에서 가장 정교한 결측치 처리 방법으로 인정받으며, 특히 결측치로 인한 불확실성을 정량화할 수 있다는 장점이 있습니다. 단순 대체보다 표준오차가 더 정확하게 계산됩니다.
개요
간단히 말해서, 다중 대체는 결측치를 여러 번(보통 5-10번) 다르게 채워서 여러 개의 완전한 데이터셋을 만들고, 각각 분석한 후 결과를 평균내는 방법입니다. sklearn의 IterativeImputer로 구현할 수 있습니다.
왜 이 방법이 필요할까요? 단순 대체(평균, 중앙값, KNN 등)는 결측치를 하나의 확정된 값으로 채우기 때문에 불확실성을 과소평가합니다.
실제로는 결측치가 여러 값일 가능성이 있는데, 하나로 고정하면 데이터가 "너무 확실해" 보입니다. 이는 통계적 검정에서 p-value를 낮게 만들고, 신뢰구간을 좁게 만들어 잘못된 결론으로 이어질 수 있습니다.
다중 대체는 여러 시나리오를 고려함으로써 이런 문제를 해결합니다. 기존 방법들은 "결측치 = 하나의 추정값"이었다면, 다중 대체는 "결측치 = 여러 가능한 값의 분포"로 접근합니다.
예를 들어 누군가의 연봉이 결측이면, 3500만원, 4000만원, 4200만원 등 여러 값으로 채운 데이터셋을 각각 만들고, 각각에서 회귀분석을 수행한 후 계수와 p-value를 통합합니다. 다중 대체의 핵심은 "베이지안 추론"과 "변동성 보존"입니다.
단순히 평균값으로 채우면 데이터의 분산이 줄어들지만, 다중 대체는 예측값에 랜덤 노이즈를 추가해 원래 데이터의 변동성을 유지합니다. 또한 MICE(Multivariate Imputation by Chained Equations) 알고리즘을 사용해 변수 간 관계를 고려합니다.
코드 예제
import pandas as pd
import numpy as np
from sklearn.experimental import enable_iterative_imputer
from sklearn.impute import IterativeImputer
# 샘플 데이터
data = {
'이름': ['김철수', '이영희', '박민수', '정지원', '최동욱', '한민재'],
'나이': [25, 28, np.nan, 32, 35, np.nan],
'경력년수': [2, 3, 5, np.nan, 10, 15],
'연봉': [3000, 3500, np.nan, 4200, np.nan, 6000]
}
df = pd.DataFrame(data)
print("원본 데이터:")
print(df)
# IterativeImputer 설정 (다중 대체의 한 버전)
# random_state를 다르게 하면 매번 다른 대체값 생성
imputer = IterativeImputer(max_iter=10, random_state=0)
# 숫자형 컬럼만 선택
numeric_cols = ['나이', '경력년수', '연봉']
df_numeric = df[numeric_cols]
# 다중 대체 수행 (5번 반복)
n_imputations = 5
imputed_datasets = []
for i in range(n_imputations):
imputer_i = IterativeImputer(max_iter=10, random_state=i)
df_imputed_i = pd.DataFrame(
imputer_i.fit_transform(df_numeric),
columns=numeric_cols
)
imputed_datasets.append(df_imputed_i)
print(f"\n대체 #{i+1}:")
print(df_imputed_i.round(0))
# 5개 데이터셋의 평균 계산
df_final = pd.concat(imputed_datasets).groupby(level=0).mean()
df_final.insert(0, '이름', df['이름'])
print("\n최종 통합 결과 (5개 평균):")
print(df_final.round(0))
설명
이것이 하는 일: 위 코드는 다중 대체를 사용해서 결측치를 여러 방식으로 채우고, 결과를 통합하는 과정을 보여줍니다. 첫 번째로, IterativeImputer를 생성합니다.
이것은 MICE 알고리즘을 구현한 것으로, 각 변수를 다른 변수들의 함수로 예측하는 회귀 모델을 만듭니다. 예를 들어 '나이'를 예측할 때는 '경력년수'와 '연봉'을 사용하고, '연봉'을 예측할 때는 '나이'와 '경력년수'를 사용합니다.
max_iter=10은 이 과정을 10번 반복해서 수렴시킨다는 의미입니다. 그 다음으로, random_state를 다르게 설정해서 5번 대체를 수행합니다.
random_state=0, 1, 2, 3, 4로 각각 실행하면 매번 약간씩 다른 예측값이 나옵니다. 예를 들어 박민수의 나이가 첫 번째 대체에서는 29세, 두 번째에서는 31세, 세 번째에서는 30세로 채워질 수 있습니다.
이는 모델의 불확실성을 반영한 것입니다. 각 대체마다 완전한 데이터셋이 생성되고, imputed_datasets 리스트에 저장됩니다.
실제 통계 분석에서는 이 5개 데이터셋 각각에서 회귀분석이나 t-test를 수행하고, 5개의 결과(회귀계수, p-value, 신뢰구간 등)를 루빈의 규칙(Rubin's Rules)에 따라 통합합니다. 위 예시에서는 간단히 평균만 계산했습니다.
최종 통합 결과를 보면, 5번의 다른 대체 결과의 평균이 사용되었습니다. 이는 "단일 대체"보다 더 안정적이고, 극단값의 영향을 줄입니다.
예를 들어 한 번의 대체에서 우연히 이상치가 나왔더라도, 5번 평균을 내면 상쇄됩니다. IterativeImputer의 내부 작동을 이해하는 것이 중요합니다.
첫 단계에서 모든 결측치를 중앙값으로 채우고, 두 번째 단계에서 첫 번째 변수(나이)를 예측하는 회귀 모델을 만들어 나이 결측치를 다시 채웁니다. 세 번째 단계에서 두 번째 변수(경력년수)를 예측하고...
이렇게 모든 변수를 한 바퀴 돌면 1 iteration이 완료됩니다. 이를 max_iter번 반복하면 값들이 수렴합니다.
여러분이 이 방법을 사용하면 통계적으로 가장 엄밀한 결측치 처리가 가능합니다. 특히 학술 연구, 임상시험, 정책 분석처럼 결과의 신뢰성이 매우 중요한 분야에서 권장됩니다.
단점은 계산량이 많고, 구현과 해석이 복잡하며, 결과를 설명하기 어렵다는 것입니다.
실전 팁
💡 n_imputations는 보통 5-10이면 충분합니다. 연구에 따르면 5번이면 대부분의 경우 안정적인 결과를 얻을 수 있습니다.
💡 estimator 매개변수로 예측 모델을 바꿀 수 있습니다. 기본은 BayesianRidge이지만, RandomForestRegressor 등을 사용할 수도 있습니다.
💡 수렴을 확인하세요! 여러 iteration을 거친 후에도 값이 크게 변한다면 max_iter를 늘리거나 데이터에 문제가 있는 것입니다.
💡 Python의 miceforest 라이브러리는 랜덤 포레스트 기반 MICE를 제공하며, 더 강력하지만 느립니다. "pip install miceforest"로 설치 가능합니다.
💡 결과 통합 시 단순 평균이 아닌 루빈의 규칙을 사용하는 것이 정석입니다. scipy.stats를 사용해 분산과 신뢰구간도 제대로 통합하세요.
9. 결측치 패턴 분석 - 왜 빠졌는지 이해하기
시작하며
여러분이 설문조사에서 "연봉" 질문의 응답률이 낮다는 것을 발견했다면, 이게 우연일까요 아니면 이유가 있을까요? 고소득자들이 의도적으로 건너뛴 거라면 이는 중요한 정보입니다!
결측치를 무작정 채우기 전에, 왜 결측치가 발생했는지 이해하는 것이 매우 중요합니다. 결측치가 완전히 무작위인지(MCAR), 다른 변수와 관련이 있는지(MAR), 아니면 그 값 자체 때문인지(MNAR)에 따라 처리 방법이 달라져야 합니다.
결측치 패턴 분석은 데이터 품질 진단의 첫 단계이며, 이를 통해 데이터 수집 프로세스의 문제를 발견하거나, 비즈니스 인사이트를 얻을 수도 있습니다.
개요
간단히 말해서, 결측치 패턴 분석은 어떤 변수에 얼마나 많은 결측치가 있는지, 결측치들이 서로 관련되어 있는지, 특정 그룹에 집중되어 있는지를 탐색하는 과정입니다. pandas와 missingno 라이브러리로 시각화할 수 있습니다.
왜 이 분석이 필요할까요? 결측치의 메커니즘에 따라 처리 전략이 완전히 달라지기 때문입니다.
MCAR(Missing Completely At Random)이면 행 삭제나 단순 대체가 안전하지만, MNAR(Missing Not At Random)이면 결측치 자체가 정보를 담고 있으므로 별도 카테고리로 처리하거나 고급 모델링이 필요합니다. 예를 들어 고소득자가 연봉을 밝히지 않는 패턴이 있다면, 결측치를 평균으로 채우면 오히려 편향이 생깁니다.
기존에는 이런 패턴을 눈으로 일일이 확인했다면, 이제는 상관관계 행렬이나 히트맵으로 시각적으로 한눈에 파악할 수 있습니다. 어떤 변수들의 결측치가 함께 나타나는지, 특정 조건에서 결측이 많은지를 빠르게 발견할 수 있습니다.
결측치 패턴 분석의 핵심 질문들: (1) 어떤 변수에 결측이 많은가? → 컬럼별 결측 비율 확인 (2) 결측치들이 함께 나타나는가?
→ 결측 상관관계 분석 (3) 특정 그룹에 결측이 집중되었는가? → 그룹별 결측 비율 비교 (4) 결측이 다른 변수 값과 관련있는가?
→ 결측 여부에 따른 평균 비교
코드 예제
import pandas as pd
import numpy as np
# 샘플 데이터 (의도적인 패턴 포함)
np.random.seed(42)
data = {
'이름': [f'사람{i}' for i in range(1, 21)],
'나이': np.random.randint(25, 50, 20),
'연봉': np.random.randint(3000, 6000, 20),
'자산': np.random.randint(1000, 10000, 20)
}
df = pd.DataFrame(data)
# 고소득자는 자산 정보를 밝히지 않는 패턴 만들기
df.loc[df['연봉'] > 4500, '자산'] = np.nan
# 나이가 40 이상인 사람 중 일부는 연봉도 밝히지 않음
df.loc[(df['나이'] > 40) & (df.index % 2 == 0), '연봉'] = np.nan
print("데이터 샘플:")
print(df.head(10))
# 1. 기본 결측 통계
print("\n=== 결측치 기본 통계 ===")
print("결측 개수:")
print(df.isnull().sum())
print("\n결측 비율(%):")
print(df.isnull().mean() * 100)
# 2. 결측 패턴 행렬 (어떤 조합으로 결측이 나타나는지)
print("\n=== 결측 패턴 ===")
missing_pattern = df.isnull().astype(int)
pattern_counts = missing_pattern.groupby(missing_pattern.columns.tolist()).size()
print(pattern_counts)
# 3. 변수 간 결측 상관관계
print("\n=== 결측치 간 상관관계 ===")
missing_corr = df.isnull().corr()
print(missing_corr)
# 4. 결측 여부에 따른 다른 변수 평균 비교
print("\n=== 자산 결측 여부에 따른 연봉 평균 ===")
print(f"자산 있음: {df[df['자산'].notna()]['연봉'].mean():.0f}만원")
print(f"자산 결측: {df[df['자산'].isna()]['연봉'].mean():.0f}만원")
설명
이것이 하는 일: 위 코드는 결측치가 무작위로 발생하지 않고 특정 패턴을 가질 때, 이를 탐지하고 분석하는 여러 방법을 보여줍니다. 첫 번째로, 의도적으로 패턴이 있는 결측치를 만들었습니다.
연봉이 4500만원 이상인 고소득자는 자산을 밝히지 않도록 설정했습니다. 이는 현실에서 흔한 MNAR(Missing Not At Random) 상황을 시뮬레이션한 것입니다.
실제로 고소득자나 고자산가일수록 개인정보를 덜 공개하는 경향이 있습니다. 그 다음으로, 기본 결측 통계를 확인합니다.
isnull().sum()은 각 컬럼의 결측 개수를, isnull().mean()은 비율을 알려줍니다. 만약 '자산' 컬럼의 결측 비율이 30%라면, 이 자체로는 우연인지 패턴인지 알 수 없습니다.
추가 분석이 필요합니다. 결측 패턴 행렬을 만들어 groupby로 카운트하면 어떤 조합의 결측이 많은지 알 수 있습니다.
예를 들어 "자산만 결측" 5명, "연봉과 자산 둘 다 결측" 3명처럼 패턴을 발견할 수 있습니다. 만약 특정 조합이 지나치게 많다면 이는 데이터 수집 과정의 문제나 응답자의 의도적 회피를 의미할 수 있습니다.
결측치 간 상관관계를 계산하면 더 명확합니다. df.isnull()은 결측 여부를 True/False로 반환하는데, 이를 1/0으로 변환해서 상관계수를 구하면 "연봉이 결측인 사람은 자산도 결측일 가능성이 높은가?"를 확인할 수 있습니다.
상관계수가 0.5 이상이면 강한 관계가 있는 것입니다. 가장 중요한 것은 결측 여부에 따른 조건부 분석입니다.
"자산이 결측인 사람"의 평균 연봉과 "자산이 있는 사람"의 평균 연봉을 비교했을 때, 만약 큰 차이가 있다면 결측이 무작위가 아니라는 강력한 증거입니다. 위 코드에서는 자산 결측 그룹의 연봉이 더 높게 나올 것입니다(고소득자가 자산을 안 밝혔으므로).
여러분이 이런 패턴을 발견하면 처리 전략을 바꿔야 합니다. 예를 들어 자산 결측을 전체 평균으로 채우면 고소득자의 자산을 과소평가하게 됩니다.
차라리 결측을 별도 카테고리("미공개")로 두거나, 연봉 기반으로 예측하는 모델을 만드는 것이 낫습니다. missingno.matrix(df)를 사용하면 결측 패턴을 예쁜 히트맵으로 시각화할 수 있습니다.
실전 팁
💡 missingno 라이브러리를 꼭 사용해보세요. "pip install missingno" 후 msno.matrix(df), msno.heatmap(df)로 직관적인 시각화가 가능합니다.
💡 Little's MCAR test를 사용하면 통계적으로 MCAR 가정을 검증할 수 있습니다. "pip install pymice"로 설치 가능합니다.
💡 결측 여부를 새로운 이진 변수로 만들어보세요. df['자산_결측여부'] = df['자산'].isnull().astype(int)로 만든 후 이를 예측 변수로 사용하면 의외의 인사이트를 얻을 수 있습니다.
💡 시간 흐름에 따른 결측 패턴도 확인하세요. df.groupby('연도')['컬럼'].isnull().mean()으로 연도별 결측 비율을 보면 데이터 품질 변화를 알 수 있습니다.
💡 도메인 전문가와 결측 패턴을 논의하세요. 통계적으로는 보이지 않는 비즈니스 이유가 있을 수 있습니다(예: 특정 설문 항목이 앱 버전에 따라 안 보임).
10. 결측치 처리 전략 선택 가이드 - 상황별 최적 방법
시작하며
여러분이 여기까지 읽으셨다면 이런 고민이 생기실 겁니다. "이렇게 많은 방법 중에 내 데이터에는 뭘 써야 하지?" 정답은 데이터의 특성과 분석 목적에 따라 다릅니다.
시계열 데이터에는 전방채우기가, 범주형 데이터에는 최빈값이, 다변량 관계가 중요한 데이터에는 KNN이나 다중 대체가 적합합니다. 하나의 "만능 방법"은 없습니다.
이 섹션에서는 실무에서 자주 만나는 상황별로 어떤 방법을 선택해야 하는지, 의사결정 트리처럼 체계적으로 정리해드리겠습니다. 여러분의 데이터가 어디에 해당하는지 찾아보세요!
개요
간단히 말해서, 결측치 처리 전략 선택은 (1) 데이터 타입 (2) 결측 비율 (3) 결측 메커니즘 (4) 분석 목적 (5) 계산 자원을 종합적으로 고려하는 의사결정 과정입니다. 왜 전략적 선택이 필요할까요?
잘못된 방법을 사용하면 오히려 데이터를 망칠 수 있기 때문입니다. 시계열 데이터에 전체 평균을 사용하면 시간적 연속성이 깨지고, 범주형 데이터에 평균을 사용하면 에러가 발생합니다.
또한 결측이 5%인 경우와 50%인 경우는 완전히 다른 접근이 필요합니다. 5%는 삭제해도 되지만, 50%는 삭제하면 데이터가 거의 남지 않습니다.
경험 법칙들: 결측 5% 미만이면 삭제 고려, 5-20%면 단순 대체(평균/중앙값/최빈값), 20-40%면 고급 대체(KNN/다중 대체), 40% 이상이면 컬럼 삭제 또는 모델 기반 대체를 신중히 고려합니다. 하지만 이는 절대적 기준이 아니라 시작점일 뿐입니다.
결측 메커니즘도 중요합니다. MCAR(완전 무작위)이면 거의 모든 방법이 안전하고, MAR(다른 변수에 의존)이면 다변량 방법(KNN, 다중 대체)이 좋으며, MNAR(값 자체에 의존)이면 별도 카테고리나 고급 모델링이 필요합니다.
분석 목적도 중요한데, 탐색적 분석이면 빠른 방법(삭제, 평균)이, 학술 연구면 정교한 방법(다중 대체)이 적합합니다.
코드 예제
import pandas as pd
import numpy as np
def recommend_imputation_strategy(df, column):
"""결측치 처리 전략을 추천하는 함수"""
# 기본 정보 수집
total_count = len(df)
missing_count = df[column].isnull().sum()
missing_ratio = missing_count / total_count
dtype = df[column].dtype
print(f"\n=== '{column}' 컬럼 분석 ===")
print(f"전체 행 수: {total_count}")
print(f"결측 수: {missing_count} ({missing_ratio*100:.1f}%)")
print(f"데이터 타입: {dtype}")
# 전략 추천
recommendations = []
# 1. 결측 비율에 따른 1차 판단
if missing_ratio > 0.5:
recommendations.append("⚠️ 결측 50% 초과: 컬럼 삭제 고려")
return recommendations
if missing_ratio < 0.05:
recommendations.append("✅ 결측 5% 미만: 행 삭제 안전")
# 2. 데이터 타입에 따른 방법 추천
if pd.api.types.is_numeric_dtype(df[column]):
# 숫자형
values = df[column].dropna()
skewness = values.skew()
if abs(skewness) > 1:
recommendations.append("📊 왜곡된 분포: 중앙값 대체 추천")
else:
recommendations.append("📊 정규 분포: 평균값 대체 가능")
if missing_ratio > 0.2:
recommendations.append("🤖 결측 20% 초과: KNN 또는 다중 대체 고려")
else:
# 범주형
recommendations.append("📝 범주형 데이터: 최빈값 대체")
unique_ratio = df[column].nunique() / total_count
if unique_ratio > 0.5:
recommendations.append("⚠️ 고유값 많음: 새 카테고리 '미상' 추가 고려")
# 3. 시계열 여부 확인 (간단한 휴리스틱)
if 'date' in column.lower() or 'time' in column.lower():
recommendations.append("⏰ 시계열 가능성: ffill/bfill 또는 보간 고려")
return recommendations
# 테스트 데이터
data = {
'날짜': pd.date_range('2024-01-01', periods=100),
'매출': np.random.randint(100, 1000, 100),
'방문자수': np.random.randint(50, 500, 100),
'카테고리': np.random.choice(['A', 'B', 'C'], 100)
}
df = pd.DataFrame(data)
# 의도적으로 결측치 추가
df.loc[5:10, '매출'] = np.nan # 6% 결측
df.loc[20:50, '방문자수'] = np.nan # 31% 결측
df.loc[70:75, '카테고리'] = np.nan # 6% 결측
# 각 컬럼에 대한 추천
for col in ['매출', '방문자수', '카테고리']:
recs = recommend_imputation_strategy(df, col)
for rec in recs:
print(rec)
설명
이것이 하는 일: 위 코드는 데이터의 특성을 자동으로 분석해서 적절한 결측치 처리 전략을 추천하는 의사결정 시스템을 보여줍니다. 첫 번째로, 결측 비율을 계산합니다.
이것이 가장 중요한 첫 번째 기준입니다. 50% 이상이면 해당 컬럼을 사용하는 것 자체를 재고해야 하므로 즉시 경고를 반환합니다.
5% 미만이면 데이터 손실이 크지 않으므로 행 삭제가 가장 간단하고 안전한 방법입니다. 그 다음으로, 데이터 타입을 확인합니다.
pd.api.types.is_numeric_dtype()로 숫자인지 범주형인지 판단합니다. 숫자형이라면 분포의 왜도(skewness)를 계산합니다.
skew()가 1 이상이면 오른쪽으로 치우친 분포(연봉, 가격 등)이므로 평균보다 중앙값이 적합합니다. -1 이하면 왼쪽 치우침, -1~1 사이면 대칭 분포로 평균값 사용 가능합니다.
결측 비율이 20%를 넘으면 단순 대체의 한계가 나타나므로 KNN이나 다중 대체 같은 고급 방법을 추천합니다. 이 임계값은 경험적으로 정해진 것으로, 여러분의 도메인에 따라 15%나 25%로 조정할 수 있습니다.
범주형 데이터는 최빈값이 기본이지만, nunique()/len(df)로 계산한 고유값 비율이 0.5 이상이면(즉, 값들이 너무 다양하면) 최빈값도 대표성이 떨어집니다. 예를 들어 '주소' 컬럼에서 100명 중 80개의 다른 주소가 있다면 최빈값으로 채우는 것이 이상합니다.
이 경우 '미상'이나 '기타' 같은 새 카테고리를 만드는 것이 낫습니다. 컬럼 이름에 'date'나 'time'이 포함되면 시계열 데이터일 가능성을 체크합니다.
물론 이것만으로는 확실하지 않지만(실제로는 데이터가 시간순으로 정렬되어있는지도 확인해야 함), 간단한 휴리스틱으로 유용합니다. 실무에서는 df.index가 DatetimeIndex인지도 확인해야 합니다.
여러분이 이런 자동화된 추천 시스템을 사용하면 실수를 줄이고 일관된 기준을 적용할 수 있습니다. 하지만 이것은 출발점일 뿐, 최종 결정은 도메인 지식과 분석 목적을 고려해야 합니다.
예를 들어 고객 이탈 예측 모델이라면 결측 자체가 중요한 신호일 수 있으므로(서비스 미사용 = 이탈 가능성), 별도 플래그로 남겨야 합니다.
실전 팁
💡 여러 방법을 시도하고 비교하세요. 평균, 중앙값, KNN으로 각각 채운 후 모델 성능을 비교하면 어떤 방법이 최선인지 알 수 있습니다.
💡 결측 처리 전후의 통계를 비교하는 리포트를 만드세요. 평균, 분산, 상관계수가 어떻게 변했는지 기록하면 나중에 감사(audit)할 때 유용합니다.
💡 프로덕션 환경에서는 결측 처리 로직을 재사용 가능하게 만드세요. sklearn의 Pipeline에 포함시키면 학습과 예측에 동일한 로직이 적용됩니다.
💡 결측치 처리는 가설 검증의 일부입니다. 여러 방법으로 처리한 후 결론이 달라진다면, 결과가 robust하지 않다는 신호이므로 더 신중한 분석이 필요합니다.
💡 자동화를 맹신하지 마세요. 이 가이드는 80%의 일반적 상황을 다루지만, 여러분의 데이터는 특별할 수 있습니다. 항상 결과를 육안으로 확인하세요!
댓글 (0)
함께 보면 좋은 카드 뉴스
범주형 변수 시각화 완벽 가이드 Bar Chart와 Count Plot
데이터 분석에서 가장 기본이 되는 범주형 변수 시각화 방법을 알아봅니다. Matplotlib의 Bar Chart부터 Seaborn의 Count Plot까지, 실무에서 바로 활용할 수 있는 시각화 기법을 배워봅니다.
이변량 분석 완벽 가이드: 변수 간 관계 탐색
두 변수 사이의 관계를 분석하는 이변량 분석의 핵심 개념과 기법을 배웁니다. 상관관계, 산점도, 교차분석 등 데이터 분석의 필수 도구들을 실습과 함께 익혀봅시다.
단변량 분석 분포 시각화 완벽 가이드
데이터 분석의 첫걸음인 단변량 분석과 분포 시각화를 배웁니다. 히스토그램, 박스플롯, 밀도 그래프 등 다양한 시각화 방법을 초보자도 쉽게 이해할 수 있도록 설명합니다.
데이터 타입 변환 및 정규화 완벽 가이드
데이터 분석과 머신러닝에서 가장 기초가 되는 데이터 타입 변환과 정규화 기법을 배워봅니다. 실무에서 자주 마주치는 데이터 전처리 문제를 Python으로 쉽게 해결하는 방법을 알려드립니다.
이상치 탐지 및 처리 완벽 가이드
데이터 속에 숨어있는 이상한 값들을 찾아내고 처리하는 방법을 배워봅니다. 실무에서 자주 마주치는 이상치 문제를 Python으로 해결하는 다양한 기법을 소개합니다.