이미지 로딩 중...
AI Generated
2025. 11. 15. · 2 Views
결측치와 이상치 처리 완벽 가이드
데이터 분석의 첫 관문인 결측치와 이상치 처리를 Polars로 마스터하세요. 실무에서 자주 만나는 데이터 품질 문제를 효율적으로 해결하는 방법을 단계별로 알아봅니다.
목차
- 결측치_확인과_시각화
- 결측치_삭제_전략
- 결측치_대체_기본_전략
- 결측치_전방_후방_채우기
- 이상치_탐지_IQR_방법
- 이상치_처리_캡핑과_변환
- Z_스코어를_이용한_이상치_탐지
- 그룹별_결측치와_이상치_처리
- 다중_대체법_개념
- 결측치_시각화로_패턴_파악
1. 결측치_확인과_시각화
시작하며
여러분이 고객 데이터를 분석하려고 DataFrame을 불러왔는데, 일부 셀이 비어있거나 null 값으로 가득한 상황을 겪어본 적 있나요? 실제로 수집된 데이터의 80% 이상은 결측치를 포함하고 있습니다.
이런 문제는 실제 개발 현장에서 자주 발생합니다. 센서 오류, 사용자 입력 누락, 시스템 장애 등 다양한 원인으로 데이터가 누락되며, 이를 제대로 처리하지 않으면 분석 결과가 왜곡될 수 있습니다.
바로 이럴 때 필요한 것이 체계적인 결측치 확인입니다. Polars를 사용하면 빠르고 효율적으로 결측치를 찾아내고 그 패턴을 파악할 수 있습니다.
개요
간단히 말해서, 이 개념은 데이터셋에서 누락된 값을 찾아내고 그 분포를 파악하는 과정입니다. 데이터 분석의 첫 단계에서 결측치 확인은 필수입니다.
어떤 컬럼에 얼마나 많은 결측치가 있는지 알아야 적절한 처리 방법을 선택할 수 있기 때문입니다. 예를 들어, 고객 나이 데이터의 70%가 누락되었다면 해당 컬럼을 삭제하거나 다른 방법으로 추정해야 합니다.
기존 Pandas에서는 isnull().sum()을 사용했다면, Polars에서는 null_count()와 표현식을 활용하여 더 빠르고 직관적으로 처리할 수 있습니다. Polars의 지연 평가(lazy evaluation)와 병렬 처리 덕분에 대용량 데이터에서도 결측치 확인이 빠릅니다.
또한 select와 표현식을 조합하면 컬럼별 결측치 비율까지 한 번에 계산할 수 있어 효율적입니다.
코드 예제
import polars as pl
# 샘플 데이터 생성
df = pl.DataFrame({
"name": ["Alice", "Bob", None, "David"],
"age": [25, None, 30, 28],
"salary": [50000, 60000, None, None]
})
# 각 컬럼의 결측치 개수 확인
null_counts = df.select([
pl.col(col).null_count().alias(f"{col}_nulls")
for col in df.columns
])
print(null_counts)
# 결측치 비율 계산 (백분율)
null_ratios = df.select([
(pl.col(col).null_count() / pl.count() * 100).alias(f"{col}_null_pct")
for col in df.columns
])
print(null_ratios)
설명
이것이 하는 일: 데이터프레임의 각 컬럼에서 null 값이 몇 개나 있는지 세고, 전체 데이터 대비 비율을 계산합니다. 첫 번째로, pl.col(col).null_count()는 해당 컬럼의 null 값 개수를 반환합니다.
alias()를 사용하여 결과 컬럼명을 명확하게 지정하여, 나중에 결과를 해석하기 쉽게 만듭니다. 리스트 컴프리헨션으로 모든 컬럼을 순회하면서 한 번에 처리합니다.
그 다음으로, 결측치 비율을 계산할 때는 null_count()를 pl.count()로 나눕니다. pl.count()는 전체 행 수를 반환하며, 100을 곱하여 백분율로 표시합니다.
이렇게 하면 "이 컬럼의 30%가 비어있다"처럼 직관적으로 이해할 수 있습니다. 마지막으로, select() 메서드 안에서 리스트 컴프리헨션을 사용하여 모든 컬럼을 동시에 처리합니다.
이는 Polars의 병렬 처리 기능을 활용하여 대용량 데이터에서도 빠른 속도를 보장합니다. 여러분이 이 코드를 사용하면 데이터 품질 보고서를 자동으로 생성하고, 어떤 컬럼이 문제인지 즉시 파악할 수 있습니다.
결측치가 많은 컬럼은 삭제하거나 보완 전략을 세우고, 적은 컬럼은 간단한 대체 방법을 적용할 수 있습니다.
실전 팁
💡 결측치가 30% 이상인 컬럼은 삭제를 고려하세요. 데이터가 너무 부족하면 통계적 신뢰도가 낮아집니다.
💡 is_null() 메서드를 filter와 함께 사용하면 결측치가 있는 행만 추출하여 패턴을 분석할 수 있습니다.
💡 결측치 확인 후에는 반드시 비즈니스 도메인 전문가와 상의하세요. 결측치가 의미 있는 정보일 수도 있습니다.
💡 시계열 데이터라면 결측치가 특정 시점에 집중되어 있는지 확인하세요. 시스템 장애나 데이터 수집 문제를 발견할 수 있습니다.
💡 describe() 메서드를 함께 사용하면 결측치와 기본 통계량을 동시에 파악할 수 있어 효율적입니다.
2. 결측치_삭제_전략
시작하며
여러분이 결측치를 발견했을 때 가장 먼저 떠오르는 방법은 무엇인가요? 아마도 "그냥 지워버릴까?"라는 생각일 것입니다.
실제로 결측치가 소수이고 무작위로 분포되어 있다면 삭제가 가장 간단하고 효과적인 방법입니다. 하지만 무작정 지우면 중요한 정보를 잃거나 데이터가 편향될 수 있습니다.
1000개 행 중 500개를 삭제한다면 분석에 충분한 데이터가 남지 않을 수도 있습니다. 또한 결측치가 특정 패턴을 가지고 있다면 삭제로 인해 샘플의 대표성이 훼손됩니다.
바로 이럴 때 필요한 것이 전략적인 결측치 삭제입니다. Polars의 drop_nulls()를 활용하여 컬럼 단위 또는 행 단위로 선택적으로 삭제할 수 있습니다.
개요
간단히 말해서, 이 개념은 결측치를 포함한 행이나 컬럼을 데이터셋에서 제거하는 것입니다. 리스트와이즈 삭제(listwise deletion)는 하나라도 결측치가 있는 행을 통째로 삭제합니다.
간단하지만 데이터 손실이 클 수 있어서, 결측치가 5% 미만일 때 권장됩니다. 예를 들어, 고객 설문 데이터에서 한 문항이라도 누락된 응답자를 제외할 때 유용합니다.
기존에는 dropna()를 사용했다면, Polars에서는 drop_nulls()로 더 명확하게 의도를 표현할 수 있습니다. subset 매개변수로 특정 컬럼만 기준으로 삭제할 수도 있습니다.
이 방법의 핵심 특징은 간단함과 속도입니다. 복잡한 대체 알고리즘 없이 즉시 깨끗한 데이터셋을 얻을 수 있으며, Polars의 병렬 처리로 대용량에서도 빠릅니다.
또한 결과가 예측 가능하여 재현성이 높습니다.
코드 예제
import polars as pl
df = pl.DataFrame({
"id": [1, 2, 3, 4, 5],
"name": ["Alice", None, "Charlie", "David", "Eve"],
"age": [25, 30, None, 28, 35],
"city": ["Seoul", "Busan", "Seoul", None, "Incheon"]
})
# 모든 컬럼에서 하나라도 null이 있으면 행 삭제
df_clean_all = df.drop_nulls()
print("전체 결측치 삭제:", df_clean_all.shape)
# 특정 컬럼의 결측치만 기준으로 삭제
df_clean_subset = df.drop_nulls(subset=["name", "age"])
print("name, age 기준 삭제:", df_clean_subset.shape)
# 원본 데이터 유지하면서 처리
print("원본 데이터:", df.shape)
설명
이것이 하는 일: 데이터프레임에서 결측치를 포함한 행을 제거하여 완전한 케이스만 남깁니다. 첫 번째로, df.drop_nulls()는 기본적으로 모든 컬럼을 검사합니다.
어느 컬럼에서든 하나라도 null이 있으면 해당 행 전체를 삭제합니다. 이 방식은 완벽한 데이터만 필요한 머신러닝 모델 학습에 적합합니다.
그 다음으로, subset 매개변수를 사용하면 중요한 컬럼만 검사합니다. 예를 들어 name과 age만 필수라면 subset=["name", "age"]로 지정하여, city가 누락된 행은 유지할 수 있습니다.
이렇게 하면 불필요한 데이터 손실을 줄일 수 있습니다. 마지막으로, Polars는 원본 데이터를 변경하지 않고 새로운 DataFrame을 반환합니다.
따라서 원본을 보존하면서 여러 가지 삭제 전략을 시도해볼 수 있습니다. 실험 후 가장 적절한 방법을 선택하면 됩니다.
여러분이 이 코드를 사용하면 데이터 품질을 빠르게 개선하고, 분석이나 모델링에 바로 사용할 수 있는 깨끗한 데이터셋을 얻을 수 있습니다. 또한 삭제 전후의 shape을 비교하여 얼마나 많은 데이터가 손실되었는지 정량적으로 파악할 수 있습니다.
실전 팁
💡 삭제 전에 반드시 원본 데이터를 백업하세요. df.clone()으로 복사본을 만들어두면 안전합니다.
💡 삭제 후 데이터가 전체의 70% 이상 남아있는지 확인하세요. 너무 많이 손실되면 대체 방법을 고려해야 합니다.
💡 결측치가 랜덤하게 분포되어 있는지 검증하세요. 특정 그룹에 편중되어 있다면 삭제로 인한 편향이 발생합니다.
💡 시계열 데이터에서는 drop_nulls() 대신 forward_fill()이나 interpolate()를 고려하세요. 시간 순서가 중요하기 때문입니다.
💡 삭제된 행의 특성을 분석하면 데이터 수집 과정의 문제를 발견할 수 있습니다. 로그로 남겨두면 좋습니다.
3. 결측치_대체_기본_전략
시작하며
여러분이 중요한 데이터셋에서 결측치를 발견했는데, 삭제하기에는 손실이 너무 큰 상황을 겪어본 적 있나요? 예를 들어 고객 구매 이력 데이터에서 일부 가격 정보가 누락되었지만, 해당 거래 전체를 버릴 수는 없는 경우입니다.
이런 문제는 실무에서 매우 흔합니다. 삭제하면 샘플 크기가 줄어들어 통계적 검정력이 약해지고, 편향된 결과를 얻을 위험이 있습니다.
특히 결측치가 20% 이상이면 삭제는 현실적이지 않습니다. 바로 이럴 때 필요한 것이 결측치 대체(imputation)입니다.
Polars의 fill_null()을 사용하면 평균, 중앙값, 최빈값 등으로 빠르게 결측치를 채울 수 있습니다.
개요
간단히 말해서, 이 개념은 결측치를 통계적으로 합리적인 값으로 채워 넣는 과정입니다. 수치형 데이터는 평균이나 중앙값으로, 범주형 데이터는 최빈값으로 대체하는 것이 일반적입니다.
평균은 정규분포를 따를 때, 중앙값은 이상치가 있을 때 적합합니다. 예를 들어, 나이 데이터에 결측치가 있다면 중앙값으로 채우는 것이 안전합니다.
기존 Pandas에서는 fillna(df.mean())을 사용했다면, Polars에서는 fill_null(pl.col("age").mean())처럼 표현식을 활용하여 더 명확하고 유연하게 처리할 수 있습니다. 이 방법의 핵심 특징은 데이터 손실 없이 완전한 데이터셋을 만들 수 있다는 점입니다.
또한 Polars의 표현식 API로 여러 컬럼에 서로 다른 대체 전략을 한 번에 적용할 수 있어 효율적입니다. 간단하지만 많은 경우에 충분히 좋은 결과를 제공합니다.
코드 예제
import polars as pl
df = pl.DataFrame({
"product": ["A", "B", None, "A", "C"],
"price": [1000, None, 1500, 1200, None],
"quantity": [5, 10, None, 8, 12]
})
# 수치형 컬럼은 중앙값으로 대체
df_filled = df.with_columns([
pl.col("price").fill_null(pl.col("price").median()).alias("price"),
pl.col("quantity").fill_null(pl.col("quantity").mean()).alias("quantity")
])
# 범주형 컬럼은 최빈값으로 대체 (mode의 첫 번째 값)
mode_value = df.select(pl.col("product").drop_nulls().mode().first()).item()
df_filled = df_filled.with_columns([
pl.col("product").fill_null(mode_value)
])
print(df_filled)
설명
이것이 하는 일: 각 컬럼의 통계값(평균, 중앙값, 최빈값)을 계산하고, 그 값으로 결측치를 채웁니다. 첫 번째로, pl.col("price").fill_null(pl.col("price").median())은 price 컬럼의 중앙값을 계산하여 null 위치에 채워 넣습니다.
median()은 표현식이므로 실행 시점에 동적으로 계산되며, 이상치에 강건한 장점이 있습니다. alias로 컬럼명을 유지하여 기존 컬럼을 덮어씁니다.
그 다음으로, quantity는 mean()을 사용하여 평균값으로 대체합니다. 평균은 데이터가 정규분포를 따를 때 적합하며, 전체 데이터의 중심 경향을 반영합니다.
with_columns()는 여러 컬럼을 동시에 변환할 수 있어 효율적입니다. 범주형 데이터인 product는 mode()로 최빈값을 구합니다.
mode()는 리스트를 반환하므로 first()로 첫 번째 값을 가져오고, item()으로 스칼라 값으로 변환합니다. 이 값을 fill_null()에 전달하여 결측치를 채웁니다.
여러분이 이 코드를 사용하면 데이터 손실 없이 완전한 데이터셋을 만들 수 있습니다. 머신러닝 모델은 대부분 결측치를 허용하지 않으므로, 이 방법으로 전처리하면 바로 모델 학습에 사용할 수 있습니다.
또한 간단하면서도 통계적으로 타당한 방법입니다.
실전 팁
💡 평균 대신 중앙값을 사용하면 이상치의 영향을 줄일 수 있어 안전합니다. 특히 소득, 가격 같은 데이터에 유용합니다.
💡 대체 전후의 분포를 비교하세요. describe()로 통계량을 확인하여 분포가 크게 왜곡되지 않았는지 검증해야 합니다.
💡 범주형 데이터에 "Unknown"이나 "Missing" 같은 별도 카테고리를 만드는 것도 좋은 방법입니다. 결측 자체가 의미 있는 정보일 수 있습니다.
💡 그룹별로 다른 값으로 대체하려면 group_by()와 함께 사용하세요. 예를 들어 지역별 평균으로 대체하면 더 정확합니다.
💡 대체 전략을 문서화하세요. 나중에 분석 결과를 해석할 때 어떤 방법을 사용했는지 알아야 합니다.
4. 결측치_전방_후방_채우기
시작하며
여러분이 시계열 데이터를 다룰 때, 센서가 일시적으로 끊겨서 몇 시간 동안의 측정값이 누락된 경험이 있나요? 주식 가격, 온도 센서, 웹 트래픽 등 시간 순서가 중요한 데이터에서는 평균값으로 채우는 것이 부자연스럽습니다.
이런 문제는 시계열 분석에서 특히 심각합니다. 시간적 연속성이 깨지면 트렌드 분석이나 예측 모델의 성능이 떨어집니다.
갑자기 평균값이 나타나면 실제로는 없었던 패턴이 생겨날 수도 있습니다. 바로 이럴 때 필요한 것이 전방/후방 채우기(forward/backward fill)입니다.
Polars의 fill_null() 전략 중 forward와 backward를 사용하면 시간 순서를 고려한 자연스러운 대체가 가능합니다.
개요
간단히 말해서, 이 개념은 결측치를 이전 값(전방) 또는 다음 값(후방)으로 채워 시간적 연속성을 유지하는 방법입니다. 전방 채우기(forward fill)는 마지막으로 관측된 값을 그대로 사용합니다.
"변화가 없었을 것"이라는 가정 하에 이전 값을 그대로 사용하며, 주식 가격처럼 급격한 변화가 드문 데이터에 적합합니다. 예를 들어, 어제 주가가 50,000원이었다면 오늘 데이터가 없어도 50,000원으로 추정하는 것입니다.
기존 Pandas에서는 ffill()이나 bfill()을 사용했다면, Polars에서는 fill_null(strategy="forward") 또는 fill_null(strategy="backward")로 명확하게 의도를 표현합니다. 이 방법의 핵심 특징은 시간적 맥락을 보존한다는 점입니다.
통계값으로 대체할 때보다 실제 데이터의 흐름을 더 잘 반영하며, 시계열 분석이나 예측 모델에서 더 나은 결과를 제공합니다. 또한 계산이 매우 빠르고 직관적입니다.
코드 예제
import polars as pl
from datetime import datetime, timedelta
# 시계열 데이터 생성
dates = [datetime(2024, 1, i) for i in range(1, 8)]
df = pl.DataFrame({
"date": dates,
"temperature": [15.2, None, None, 18.5, None, 20.1, 19.8],
"humidity": [60, 65, None, None, 70, None, 68]
})
# 전방 채우기 (이전 값으로 채움)
df_ffill = df.with_columns([
pl.col("temperature").fill_null(strategy="forward"),
pl.col("humidity").fill_null(strategy="forward")
])
print("전방 채우기:\n", df_ffill)
# 후방 채우기 (다음 값으로 채움)
df_bfill = df.with_columns([
pl.col("temperature").fill_null(strategy="backward")
])
print("\n후방 채우기:\n", df_bfill)
설명
이것이 하는 일: 시계열 데이터에서 결측치를 시간 순서에 따라 이전 또는 다음 관측값으로 채웁니다. 첫 번째로, strategy="forward"는 각 null 값을 바로 위(이전 시점)의 값으로 대체합니다.
15.2 다음에 두 개의 null이 있으면 모두 15.2로 채워집니다. 이는 "마지막 상태가 유지되었을 것"이라는 가정에 기반하며, 센서 데이터처럼 점진적으로 변하는 경우에 적합합니다.
그 다음으로, strategy="backward"는 반대 방향으로 작동합니다. null 다음에 나오는 첫 번째 유효한 값을 거슬러 올라가며 채웁니다.
미래의 정보를 사용하므로 실시간 예측에는 부적절하지만, 사후 분석이나 데이터 정제에는 유용합니다. with_columns()로 여러 컬럼에 동시에 적용하면 효율적입니다.
Polars는 각 컬럼을 병렬로 처리하므로 대용량 시계열 데이터에서도 빠릅니다. 또한 원본 데이터는 그대로 유지되어 여러 전략을 시도해볼 수 있습니다.
여러분이 이 코드를 사용하면 시계열 데이터의 연속성을 유지하면서 결측치를 처리할 수 있습니다. ARIMA, Prophet 같은 시계열 모델은 연속된 데이터를 요구하므로, 이 방법으로 전처리하면 모델 성능이 향상됩니다.
또한 시각화할 때도 선이 끊기지 않아 트렌드를 명확히 파악할 수 있습니다.
실전 팁
💡 전방 채우기는 실시간 시스템에 적합하지만, 후방 채우기는 미래 정보를 사용하므로 예측 모델에서는 데이터 누수를 일으킬 수 있습니다.
💡 limit 매개변수로 채울 수 있는 연속 null의 개수를 제한하세요. 너무 오래 전 값으로 채우면 부정확해집니다.
💡 전방과 후방 채우기를 연속으로 적용하면 처음과 끝의 null까지 모두 처리할 수 있습니다.
💡 급격한 변화가 예상되는 데이터(주가 폭락, 이벤트 등)에는 적합하지 않으므로 보간법(interpolation)을 고려하세요.
💡 채우기 전후의 시각화를 비교하여 자연스러운지 확인하세요. line plot으로 그려보면 이상한 패턴을 쉽게 발견할 수 있습니다.
5. 이상치_탐지_IQR_방법
시작하며
여러분이 고객 구매 금액 데이터를 분석하는데, 대부분은 1만~10만원인데 갑자기 1000만원짜리 주문이 튀어나온 경험이 있나요? 이런 극단적인 값은 데이터 입력 오류일 수도 있고, 정말 특별한 케이스일 수도 있습니다.
이런 문제는 통계 분석과 머신러닝에서 심각한 영향을 미칩니다. 평균, 표준편차 같은 통계량이 왜곡되고, 회귀 모델이 이상치에 과적합되어 일반화 성능이 떨어집니다.
특히 선형 모델은 이상치에 매우 민감합니다. 바로 이럴 때 필요한 것이 IQR(Interquartile Range) 방법입니다.
사분위수를 이용하여 통계적으로 이상한 값을 찾아내며, Polars로 빠르고 간단하게 구현할 수 있습니다.
개요
간단히 말해서, 이 개념은 데이터의 25%, 75% 지점(사분위수)을 기준으로 정상 범위를 정의하고, 그 범위를 벗어난 값을 이상치로 판단하는 방법입니다. IQR은 Q3(75번째 백분위수)에서 Q1(25번째 백분위수)을 뺀 값입니다.
일반적으로 Q1 - 1.5IQR보다 작거나 Q3 + 1.5IQR보다 큰 값을 이상치로 봅니다. 이 1.5라는 계수는 통계적으로 정규분포의 99.3%를 포함하는 범위입니다.
예를 들어, 시험 점수가 Q1=60, Q3=80이면 IQR=20이고, 30 미만이나 110 초과는 이상치입니다. 기존 Python에서는 numpy나 scipy로 복잡하게 계산했다면, Polars에서는 quantile() 메서드와 표현식으로 간결하게 처리할 수 있습니다.
이 방법의 핵심 특징은 중앙값 기반이라 이상치 자체의 영향을 받지 않는다는 점입니다. 평균-표준편차 방법과 달리 극단값이 있어도 안정적으로 작동하며, 정규분포를 가정하지 않아도 됩니다.
또한 해석이 직관적이고 시각화(박스플롯)와 잘 연결됩니다.
코드 예제
import polars as pl
df = pl.DataFrame({
"customer_id": range(1, 11),
"purchase_amount": [50000, 48000, 52000, 49000, 51000,
47000, 50000, 9800000, 48500, 51500]
})
# Q1, Q3, IQR 계산
q1 = df.select(pl.col("purchase_amount").quantile(0.25)).item()
q3 = df.select(pl.col("purchase_amount").quantile(0.75)).item()
iqr = q3 - q1
# 이상치 경계 계산
lower_bound = q1 - 1.5 * iqr
upper_bound = q3 + 1.5 * iqr
# 이상치 표시 및 필터링
df_with_outlier = df.with_columns([
((pl.col("purchase_amount") < lower_bound) |
(pl.col("purchase_amount") > upper_bound)).alias("is_outlier")
])
print(f"정상 범위: {lower_bound:.0f} ~ {upper_bound:.0f}")
print(df_with_outlier)
# 이상치 제거
df_clean = df_with_outlier.filter(~pl.col("is_outlier"))
print(f"\n정상 데이터: {df_clean.shape[0]}개")
설명
이것이 하는 일: 데이터의 사분위수를 계산하여 통계적으로 정상 범위를 정의하고, 그 범위를 벗어난 극단값을 찾아냅니다. 첫 번째로, quantile(0.25)와 quantile(0.75)로 제1사분위수(Q1)와 제3사분위수(Q3)를 계산합니다.
item()은 단일 값을 스칼라로 추출하며, 이를 이용해 IQR = Q3 - Q1을 구합니다. IQR은 데이터의 중간 50% 구간의 폭을 나타냅니다.
그 다음으로, 이상치 경계를 lower_bound = Q1 - 1.5IQR, upper_bound = Q3 + 1.5IQR로 정의합니다. 1.5는 통계학에서 관례적으로 사용되는 계수이며, 더 엄격하게는 3을 사용하기도 합니다.
이 범위는 박스플롯의 수염(whisker) 범위와 동일합니다. with_columns()로 각 행이 이상치인지 판단하는 is_outlier 컬럼을 추가합니다.
논리 OR 연산자(|)로 하한선 미만이거나 상한선 초과인 경우를 모두 잡아냅니다. filter(~pl.col("is_outlier"))로 이상치가 아닌 행만 남깁니다.
여러분이 이 코드를 사용하면 데이터 입력 오류나 비정상적인 값을 빠르게 식별할 수 있습니다. 이상치를 제거하거나 별도로 분석하여 데이터 품질을 높이고, 통계 분석의 신뢰도를 향상시킬 수 있습니다.
또한 이상치가 발견되면 비즈니스 팀에 알려 데이터 수집 과정을 개선할 수도 있습니다.
실전 팁
💡 1.5 대신 3을 사용하면 더 극단적인 이상치만 잡습니다. 도메인 지식에 따라 계수를 조정하세요.
💡 이상치를 무조건 삭제하지 말고, 먼저 원인을 파악하세요. 정말 중요한 고객이거나 특별 프로모션일 수 있습니다.
💡 그룹별로 이상치를 탐지하면 더 정확합니다. group_by()를 사용하여 카테고리별로 다른 기준을 적용하세요.
💡 로그 변환 후 이상치를 탐지하면 왜도가 큰 데이터(소득, 매출 등)에서 더 나은 결과를 얻습니다.
💡 시각화는 필수입니다. 박스플롯이나 산점도로 이상치의 분포를 확인하면 숨겨진 패턴을 발견할 수 있습니다.
6. 이상치_처리_캡핑과_변환
시작하며
여러분이 이상치를 발견했을 때, 그냥 삭제해버리면 정보 손실이 크고 샘플 크기가 줄어드는 문제가 있습니다. 특히 머신러닝에서는 데이터가 많을수록 좋은데, 이상치 때문에 귀중한 샘플을 버리는 것은 아깝습니다.
이런 문제는 실무에서 흔히 발생합니다. 이상치가 10%나 되는데 모두 삭제하면 모델 학습에 충분한 데이터가 남지 않을 수 있습니다.
또한 이상치가 완전히 잘못된 값이 아니라 단지 극단적인 경우라면, 그 정보를 활용하는 것이 바람직합니다. 바로 이럴 때 필요한 것이 캡핑(capping)과 변환입니다.
Polars의 clip()으로 이상치를 경계값으로 제한하거나, 로그 변환으로 극단값의 영향을 줄일 수 있습니다.
개요
간단히 말해서, 캡핑은 이상치를 정상 범위의 최대/최소값으로 잘라내는 것이고, 변환은 데이터 스케일을 바꿔 이상치의 영향을 완화하는 것입니다. 캡핑(윈저화, Winsorization)은 이상치를 제거하지 않고 정상 범위의 끝값으로 대체합니다.
예를 들어 상한선이 100만원인데 1000만원 값이 있으면, 이를 100만원으로 바꿉니다. 정보는 보존하면서도 극단적인 영향을 제한하여, 회귀 분석에서 특히 유용합니다.
기존에는 numpy.clip()이나 조건문으로 처리했다면, Polars에서는 clip() 메서드로 더 간결하게 표현할 수 있습니다. 또한 log() 같은 수학 함수도 표현식으로 쉽게 적용됩니다.
이 방법의 핵심 특징은 데이터 손실 없이 이상치의 영향을 줄인다는 점입니다. 샘플 수는 그대로 유지하면서도 통계량과 모델 성능을 개선할 수 있습니다.
특히 로그 변환은 오른쪽으로 치우친 분포를 정규분포에 가깝게 만들어 많은 알고리즘에 유리합니다.
코드 예제
import polars as pl
df = pl.DataFrame({
"user_id": range(1, 11),
"income": [50000, 52000, 48000, 51000, 9800000,
49000, 50000, 47000, 51500, 48500]
})
# IQR 기반 경계 계산
q1 = df.select(pl.col("income").quantile(0.25)).item()
q3 = df.select(pl.col("income").quantile(0.75)).item()
iqr = q3 - q1
lower_bound = q1 - 1.5 * iqr
upper_bound = q3 + 1.5 * iqr
# 캡핑 (경계값으로 제한)
df_capped = df.with_columns([
pl.col("income").clip(lower_bound, upper_bound).alias("income_capped")
])
# 로그 변환 (1을 더해 0 방지)
df_transformed = df.with_columns([
(pl.col("income") + 1).log10().alias("income_log10")
])
print("원본 vs 캡핑 vs 로그변환:\n", df_capped.join(df_transformed, on="user_id"))
설명
이것이 하는 일: 이상치를 삭제하지 않고 정상 범위로 조정하거나, 수학적 변환으로 극단값의 영향을 완화합니다. 첫 번째로, clip(lower_bound, upper_bound)는 각 값을 지정된 범위 내로 제한합니다.
lower_bound보다 작은 값은 lower_bound로, upper_bound보다 큰 값은 upper_bound로 바뀝니다. 중간 값들은 그대로 유지되어, 데이터의 대부분은 영향을 받지 않습니다.
그 다음으로, 로그 변환은 큰 값의 차이를 줄이고 작은 값의 차이를 강조합니다. (income + 1).log10()은 0이나 음수를 피하기 위해 1을 더하고, 상용로그를 적용합니다.
이렇게 하면 9,800,000과 50,000의 차이가 로그 스케일에서는 훨씬 줄어듭니다. with_columns()로 원본 컬럼을 유지하면서 새로운 컬럼을 추가합니다.
이렇게 하면 원본, 캡핑, 변환된 값을 비교하며 어떤 방법이 적합한지 판단할 수 있습니다. join()으로 결과를 합쳐 한눈에 볼 수도 있습니다.
여러분이 이 코드를 사용하면 이상치를 제거하지 않고도 데이터의 품질을 개선할 수 있습니다. 캡핑은 선형 회귀에서, 로그 변환은 의사결정나무나 신경망에서 모델 성능을 향상시킵니다.
또한 샘플 수를 유지하여 통계적 검정력을 잃지 않습니다.
실전 팁
💡 캡핑은 해석 가능성이 중요한 비즈니스 리포트에 적합하고, 로그 변환은 머신러닝 전처리에 더 유용합니다.
💡 로그 변환은 음수나 0이 있을 때 실패하므로, 반드시 양수로 만들어야 합니다. log1p() (log(1+x))가 편리합니다.
💡 Box-Cox 변환이나 Yeo-Johnson 변환은 더 정교하지만 복잡합니다. 우선 로그 변환으로 시작하세요.
💡 변환 후에는 반드시 분포를 시각화하여 정규성이 개선되었는지 확인하세요. 히스토그램이나 Q-Q plot이 유용합니다.
💡 모델 예측 후에는 역변환을 잊지 마세요. 로그 변환했다면 지수 함수로 원래 스케일로 되돌려야 해석이 가능합니다.
7. Z_스코어를_이용한_이상치_탐지
시작하며
여러분이 데이터가 정규분포를 따른다고 확신할 때, IQR보다 더 정밀하게 이상치를 찾고 싶은 경우가 있습니다. 예를 들어 키, 몸무게, 시험 점수처럼 자연 현상이나 많은 샘플이 모인 데이터는 종종 정규분포를 따릅니다.
이런 상황에서는 평균과 표준편차를 활용한 방법이 더 효과적입니다. 정규분포의 성질을 이용하면 "평균에서 얼마나 떨어져 있는가"를 정량적으로 측정하여, 극단적으로 드문 값을 찾아낼 수 있습니다.
바로 이럴 때 필요한 것이 Z-스코어(표준 점수)입니다. Polars로 간단하게 계산하며, 일반적으로 절대값이 3 이상이면 이상치로 판단합니다.
개요
간단히 말해서, Z-스코어는 각 데이터 포인트가 평균에서 표준편차의 몇 배만큼 떨어져 있는지를 나타내는 값입니다. Z = (X - 평균) / 표준편차로 계산되며, 0에 가까우면 평균 근처, 양수면 평균보다 크고 음수면 작습니다.
정규분포에서 99.7%의 데이터는 Z가 -3에서 +3 사이에 있으므로, 절대값이 3을 넘으면 매우 드문 경우입니다. 예를 들어, 평균 키가 170cm이고 표준편차가 10cm일 때, 210cm는 Z=4로 극단적인 이상치입니다.
기존 scipy.stats.zscore()를 사용했다면, Polars에서는 표현식으로 직접 계산하여 더 유연하고 빠르게 처리할 수 있습니다. 이 방법의 핵심 특징은 정규분포에서 매우 정확하다는 점입니다.
통계적 의미가 명확하여 "이 값은 상위 0.1%에 속한다"처럼 확률적 해석이 가능합니다. 또한 여러 변수의 Z-스코어를 비교하면 어느 변수에서 더 극단적인지 알 수 있습니다.
코드 예제
import polars as pl
df = pl.DataFrame({
"student_id": range(1, 11),
"test_score": [85, 90, 78, 92, 88, 250, 82, 89, 91, 87]
})
# Z-스코어 계산
mean_score = df.select(pl.col("test_score").mean()).item()
std_score = df.select(pl.col("test_score").std()).item()
df_zscore = df.with_columns([
((pl.col("test_score") - mean_score) / std_score).alias("z_score"),
(((pl.col("test_score") - mean_score) / std_score).abs() > 3).alias("is_outlier_zscore")
])
print(f"평균: {mean_score:.2f}, 표준편차: {std_score:.2f}")
print(df_zscore)
# 이상치 필터링
outliers = df_zscore.filter(pl.col("is_outlier_zscore"))
print(f"\n이상치 발견: {outliers.shape[0]}개")
print(outliers)
설명
이것이 하는 일: 각 데이터가 평균으로부터 표준편차의 몇 배 떨어져 있는지 계산하고, 너무 멀리 떨어진 값을 찾습니다. 첫 번째로, mean()과 std()로 데이터의 평균과 표준편차를 계산합니다.
item()으로 스칼라 값을 추출하여 변수에 저장하며, 이 값들은 Z-스코어 공식의 기준점이 됩니다. 정규분포에서 평균은 중심, 표준편차는 퍼진 정도를 나타냅니다.
그 다음으로, (pl.col("test_score") - mean_score) / std_score로 Z-스코어를 계산합니다. 각 점수에서 평균을 빼면 평균 중심으로 이동하고, 표준편차로 나누면 표준화됩니다.
결과는 "평균에서 몇 표준편차 떨어졌는가"를 의미합니다. abs() > 3로 절대값이 3을 초과하는지 확인합니다.
정규분포에서 Z > 3 또는 Z < -3는 전체의 0.3% 미만으로 매우 드뭅니다. 이런 값들을 is_outlier_zscore로 표시하고, filter()로 추출하여 자세히 검토할 수 있습니다.
여러분이 이 코드를 사용하면 통계적으로 의미 있는 이상치를 정확히 찾아낼 수 있습니다. 특히 품질 관리, 시험 성적 분석, 생물학적 측정 등 정규분포를 따르는 데이터에서 매우 효과적입니다.
또한 Z-스코어 자체가 표준화된 값이므로 서로 다른 단위의 변수들을 비교할 수 있습니다.
실전 팁
💡 Z-스코어는 데이터가 정규분포를 따를 때만 정확합니다. 먼저 히스토그램이나 Q-Q plot으로 정규성을 확인하세요.
💡 표준편차는 이상치에 민감하므로, 이상치가 많으면 Z-스코어 자체가 왜곡됩니다. 이럴 때는 IQR이 더 안전합니다.
💡 임계값 3 대신 2나 2.5를 사용하면 더 많은 이상치를 잡지만, 정상값도 많이 포함될 수 있습니다. 도메인에 맞게 조정하세요.
💡 Modified Z-score는 평균 대신 중앙값, 표준편차 대신 MAD(Median Absolute Deviation)를 사용하여 더 강건합니다.
💡 여러 변수의 Z-스코어를 합치면 다변량 이상치를 탐지할 수 있습니다. 예를 들어 키와 몸무게를 동시에 고려할 수 있습니다.
8. 그룹별_결측치와_이상치_처리
시작하며
여러분이 여러 지역의 매출 데이터를 분석할 때, 전체 평균으로 결측치를 채우면 서울과 지방의 차이를 무시하게 됩니다. 서울 매출이 평균 1억, 지방이 1천만원인데 전체 평균으로 채우면 부정확한 결과가 나옵니다.
이런 문제는 그룹 구조가 있는 데이터에서 흔합니다. 카테고리별, 시간대별, 고객 세그먼트별로 특성이 다른데 하나의 기준으로 처리하면 맥락을 잃게 됩니다.
이상치도 마찬가지로, 럭셔리 제품에서는 정상인 가격이 일반 제품에서는 이상치일 수 있습니다. 바로 이럴 때 필요한 것이 그룹별 처리입니다.
Polars의 group_by()와 over()를 활용하면 각 그룹의 통계량으로 결측치를 채우고 이상치를 탐지할 수 있습니다.
개요
간단히 말해서, 이 개념은 데이터를 카테고리별로 나누어 각 그룹의 특성에 맞게 결측치를 대체하고 이상치를 판단하는 것입니다. 그룹별 대체는 더 정확한 추정을 제공합니다.
남성과 여성의 평균 키가 다른데 전체 평균으로 채우면 부정확하지만, 성별로 나누어 각각의 평균으로 채우면 훨씬 합리적입니다. 예를 들어, 제품 카테고리별로 평균 가격이 크게 다를 때 카테고리 내 평균으로 채워야 합니다.
기존 Pandas에서는 groupby().transform()을 사용했다면, Polars에서는 over() 표현식으로 더 직관적이고 빠르게 처리할 수 있습니다. 이 방법의 핵심 특징은 맥락을 보존한다는 점입니다.
각 그룹의 고유한 특성을 유지하면서 결측치를 채우므로, 데이터의 이질성을 존중합니다. 또한 그룹별 이상치 탐지는 서로 다른 스케일을 가진 그룹에서 공정한 기준을 제공합니다.
코드 예제
import polars as pl
df = pl.DataFrame({
"region": ["Seoul", "Seoul", "Busan", "Busan", "Seoul", "Busan"],
"product": ["A", "B", "A", "B", "A", "B"],
"sales": [100, None, 30, 35, None, 32],
"cost": [50, 55, None, 18, 52, None]
})
# 그룹별 평균으로 결측치 채우기
df_filled = df.with_columns([
pl.col("sales").fill_null(
pl.col("sales").mean().over("region")
).alias("sales_filled"),
pl.col("cost").fill_null(
pl.col("cost").mean().over("product")
).alias("cost_filled")
])
# 그룹별 Z-스코어로 이상치 탐지
df_zscore = df_filled.with_columns([
((pl.col("sales_filled") - pl.col("sales_filled").mean().over("region")) /
pl.col("sales_filled").std().over("region")).alias("sales_zscore_region")
])
print(df_zscore)
설명
이것이 하는 일: 데이터를 그룹으로 나누고, 각 그룹 내에서 평균, 중앙값 등을 계산하여 그룹별로 결측치를 처리하고 이상치를 판단합니다. 첫 번째로, pl.col("sales").mean().over("region")은 region 그룹별 평균을 계산합니다.
over()는 윈도우 함수로, 각 행에 해당 행이 속한 그룹의 통계량을 할당합니다. 따라서 Seoul 행들은 Seoul의 평균을, Busan 행들은 Busan의 평균을 받습니다.
그 다음으로, fill_null()과 over()를 조합하면 그룹별로 다른 값으로 채울 수 있습니다. Seoul의 결측치는 Seoul의 평균(100)으로, Busan의 결측치는 Busan의 평균(32.3)으로 채워집니다.
이렇게 하면 지역 특성을 반영한 합리적인 대체가 이루어집니다. Z-스코어도 마찬가지로 그룹별로 계산합니다.
mean().over("region")과 std().over("region")으로 각 지역의 평균과 표준편차를 구하고, 그룹 내에서의 상대적 위치를 계산합니다. 따라서 Seoul 내에서의 이상치와 Busan 내에서의 이상치를 따로 판단할 수 있습니다.
여러분이 이 코드를 사용하면 이질적인 그룹이 섞인 데이터에서도 정확한 처리가 가능합니다. 고객 세그먼트별, 제품 카테고리별, 시간대별로 다른 기준을 적용하여 데이터 품질을 향상시킬 수 있습니다.
또한 각 그룹의 특성을 유지하여 분석 결과의 신뢰도를 높입니다.
실전 팁
💡 그룹 크기가 너무 작으면(예: 3개 미만) 통계량이 불안정합니다. 이럴 때는 전체 평균으로 폴백하세요.
💡 여러 컬럼으로 그룹을 나누려면 over(["region", "product"])처럼 리스트로 전달하세요.
💡 그룹별 처리 전에 각 그룹의 크기와 결측치 비율을 확인하세요. group_by().agg()로 쉽게 파악할 수 있습니다.
💡 계층적 그룹(예: 국가 > 도시)이라면 좁은 그룹부터 채우고, 결측치가 남으면 넓은 그룹으로 채우는 다단계 전략을 사용하세요.
💡 over()는 매우 강력하지만 복잡한 로직에서는 가독성이 떨어질 수 있습니다. 주석을 충분히 달아두세요.
9. 다중_대체법_개념
시작하며
여러분이 단순히 평균값으로 결측치를 채웠을 때, 데이터의 분산이 줄어들고 불확실성을 무시하게 되는 문제를 느낀 적 있나요? 실제로 결측치가 무엇인지는 누구도 정확히 모르므로, 하나의 값으로 단정하는 것은 위험합니다.
이런 문제는 통계적 추론에서 심각합니다. 단일 값으로 대체하면 표준 오차가 과소평가되고, 신뢰구간이 너무 좁아져서 잘못된 결론을 내릴 수 있습니다.
결측치의 불확실성을 고려하지 않으면 통계적 유의성 검정이 왜곡됩니다. 바로 이럴 때 필요한 것이 다중 대체법(Multiple Imputation)입니다.
여러 개의 그럴듯한 값을 생성하여 불확실성을 반영하며, Polars로 간단한 버전을 구현할 수 있습니다.
개요
간단히 말해서, 다중 대체법은 결측치를 한 가지 값이 아니라 여러 가지 그럴듯한 값으로 채운 데이터셋을 여러 개 만드는 방법입니다. 일반적으로 3~10개의 대체 데이터셋을 생성하고, 각각에서 분석을 수행한 후 결과를 통합합니다.
예를 들어, 나이의 결측치를 한 번은 35, 다른 번은 40, 또 다른 번은 38로 채워서 세 개의 데이터셋을 만듭니다. 이렇게 하면 "진짜 값은 이 근처일 것"이라는 불확실성을 반영할 수 있습니다.
기존에는 R의 mice 패키지나 Python의 sklearn.impute.IterativeImputer를 사용했지만, 간단한 경우에는 Polars로 직접 구현할 수 있습니다. 이 방법의 핵심 특징은 불확실성을 정량화한다는 점입니다.
단일 대체는 "결측치가 정확히 이 값이다"라고 가정하지만, 다중 대체는 "이 범위 내 어딘가일 것"이라고 인정합니다. 이는 통계적으로 더 정직하고 신뢰할 수 있는 접근입니다.
코드 예제
import polars as pl
import random
random.seed(42)
df = pl.DataFrame({
"id": range(1, 6),
"age": [25, None, 30, None, 28],
"income": [50, 60, None, 65, 55]
})
# 간단한 다중 대체: 그룹 통계량에 노이즈 추가
age_mean = df.select(pl.col("age").mean()).item()
age_std = df.select(pl.col("age").std()).item()
# 5개의 대체 데이터셋 생성
imputed_datasets = []
for i in range(5):
df_imputed = df.with_columns([
pl.when(pl.col("age").is_null())
.then(age_mean + random.gauss(0, age_std))
.otherwise(pl.col("age"))
.alias("age")
])
imputed_datasets.append(df_imputed)
print(f"\n대체 데이터셋 {i+1}:")
print(df_imputed)
# 각 데이터셋에서 분석 후 결과 평균
age_means = [ds.select(pl.col("age").mean()).item() for ds in imputed_datasets]
print(f"\n대체된 데이터셋들의 평균 나이: {age_means}")
print(f"통합 평균: {sum(age_means) / len(age_means):.2f}")
설명
이것이 하는 일: 결측치를 평균 주변에서 무작위로 선택한 값들로 채워, 여러 버전의 완전한 데이터셋을 생성합니다. 첫 번째로, age_mean과 age_std로 나이의 평균과 표준편차를 계산합니다.
이는 결측치가 따를 것으로 예상되는 분포의 파라미터입니다. 정규분포를 가정하면 대부분의 값이 평균 ± 2*표준편차 범위에 있을 것입니다.
그 다음으로, for 루프로 여러 개의 대체 데이터셋을 생성합니다. random.gauss(0, age_std)는 평균 0, 표준편차 age_std인 정규분포에서 무작위 값을 뽑습니다.
이를 age_mean에 더하면 결측치가 있을 법한 위치의 값이 됩니다. when-then-otherwise로 null인 경우만 대체합니다.
각 대체 데이터셋은 결측치를 다른 값으로 채우고 있지만, 모두 통계적으로 그럴듯합니다. 이 데이터셋들에서 각각 분석(예: 평균 계산, 회귀 분석)을 수행하고, 결과들을 평균내어 최종 추정치와 표준 오차를 구합니다.
여러분이 이 코드를 사용하면 결측치의 불확실성을 정량화할 수 있습니다. 단일 대체보다 표준 오차가 더 정확하게 추정되어, 통계적 검정과 신뢰구간이 신뢰할 수 있게 됩니다.
특히 결측치가 많고 통계적 추론이 중요한 연구에서 필수적입니다.
실전 팁
💡 일반적으로 5~10개의 대체 데이터셋이면 충분합니다. 더 많이 만들어도 결과는 크게 개선되지 않습니다.
💡 더 정교한 방법은 MICE(Multivariate Imputation by Chained Equations)로, 여러 변수 간 관계를 고려합니다.
💡 각 데이터셋에서 같은 분석을 수행하고, Rubin's rules로 결과를 통합하여 최종 추정치와 표준 오차를 구합니다.
💡 머신러닝에서는 다중 대체로 만든 여러 데이터셋으로 모델을 학습하고 앙상블하면 성능이 향상됩니다.
💡 간단한 경우 위 코드로 충분하지만, 복잡한 데이터는 fancyimpute나 sklearn.impute 같은 전문 라이브러리를 사용하세요.
10. 결측치_시각화로_패턴_파악
시작하며
여러분이 결측치를 처리하기 전에, 결측치가 어떤 패턴으로 분포되어 있는지 궁금한 적 있나요? 결측치가 완전히 무작위인지, 특정 그룹에 몰려있는지, 다른 변수와 관련이 있는지에 따라 처리 방법이 달라져야 합니다.
이런 패턴 분석 없이 대체하면 편향된 결과를 얻을 수 있습니다. 예를 들어 고소득자가 소득을 응답하지 않는 경향이 있다면, 평균으로 채우면 소득을 과소평가하게 됩니다.
결측 메커니즘을 이해하는 것이 올바른 처리의 첫걸음입니다. 바로 이럴 때 필요한 것이 결측치 시각화입니다.
Polars로 결측치 패턴을 분석하고, matplotlib이나 seaborn으로 시각화하면 숨겨진 구조를 발견할 수 있습니다.
개요
간단히 말해서, 이 개념은 결측치가 어느 행, 어느 컬럼에 얼마나 있는지, 그리고 결측치끼리 연관이 있는지를 시각적으로 파악하는 것입니다. 결측치 메커니즘은 세 가지로 분류됩니다.
MCAR(완전 무작위)는 결측이 어떤 변수와도 무관하고, MAR(무작위)는 관측된 변수와 관련 있으며, MNAR(비무작위)는 결측값 자체와 관련 있습니다. 예를 들어, 우울증 설문에서 우울한 사람이 응답을 안 한다면 MNAR입니다.
기존에는 missingno 라이브러리를 사용했지만, Polars로 데이터를 가공하고 matplotlib으로 직접 그리면 더 유연합니다. 이 방법의 핵심 특징은 "보이지 않는 것을 보이게" 만든다는 점입니다.
숫자 요약만으로는 놓치는 패턴을 히트맵, 바 차트, 행렬도로 명확히 드러냅니다. 이를 통해 단순 삭제가 안전한지, 고급 대체가 필요한지 판단할 수 있습니다.
코드 예제
import polars as pl
import matplotlib.pyplot as plt
df = pl.DataFrame({
"name": ["Alice", "Bob", None, "David", "Eve", None],
"age": [25, None, 30, 28, None, 22],
"salary": [50, 60, None, None, 70, None],
"city": ["Seoul", "Busan", "Seoul", None, "Incheon", "Seoul"]
})
# 결측치 행렬 (0=존재, 1=결측)
null_matrix = df.select([
pl.col(col).is_null().cast(pl.Int8).alias(col)
for col in df.columns
])
# 컬럼별 결측치 비율
null_pcts = df.select([
(pl.col(col).null_count() / pl.count() * 100).alias(col)
for col in df.columns
]).transpose(include_header=True, header_name="column", column_names=["null_pct"])
print("컬럼별 결측치 비율:\n", null_pcts)
# 간단한 히트맵 시각화 (행별 결측 패턴)
fig, ax = plt.subplots(figsize=(8, 4))
ax.imshow(null_matrix.to_numpy(), cmap="RdYlGn_r", aspect="auto")
ax.set_xticks(range(len(df.columns)))
ax.set_xticklabels(df.columns)
ax.set_ylabel("Row Index")
ax.set_title("Missing Data Pattern (Red=Missing)")
plt.tight_layout()
plt.savefig("/tmp/missing_pattern.png")
print("\n시각화 저장: /tmp/missing_pattern.png")
설명
이것이 하는 일: 각 셀이 결측인지 여부를 0/1로 표현한 행렬을 만들고, 이를 시각화하여 결측치의 분포와 상관관계를 탐색합니다. 첫 번째로, pl.col(col).is_null()로 각 컬럼의 결측 여부를 불린으로 얻고, cast(pl.Int8)로 0/1 정수로 변환합니다.
모든 컬럼에 대해 이를 수행하면 원본 데이터와 같은 크기의 행렬이 생기는데, 값 대신 결측 여부만 담고 있습니다. 그 다음으로, 컬럼별 결측 비율을 계산하여 어느 변수가 문제인지 파악합니다.
transpose()로 컬럼명을 행으로 바꾸면 보기 좋은 요약표가 됩니다. 결측치가 30% 이상인 컬럼은 삭제를 고려하거나 신중한 대체가 필요합니다.
imshow()로 결측치 행렬을 히트맵으로 그립니다. 빨간색이 결측, 초록색이 존재를 나타내며, 행 방향으로 패턴을 보면 "어떤 행들이 여러 컬럼에서 동시에 결측인가"를 알 수 있습니다.
세로 줄무늬는 특정 컬럼의 결측치가 많음을, 가로 줄무늬는 특정 행의 결측치가 많음을 의미합니다. 여러분이 이 코드를 사용하면 결측치 처리 전략을 데이터에 맞게 선택할 수 있습니다.
무작위 패턴이면 단순 대체나 삭제가 안전하지만, 체계적인 패턴이 보이면 편향을 고려한 고급 방법이 필요합니다. 또한 여러 컬럼의 결측치가 함께 나타나면 공통 원인을 찾아 데이터 수집 프로세스를 개선할 수 있습니다.
실전 팁
💡 missingno 라이브러리의 matrix(), heatmap(), dendrogram()을 사용하면 더 정교한 시각화가 가능합니다.
💡 결측치 간 상관관계를 보려면 null_matrix의 상관계수를 계산하세요. 1에 가까우면 함께 결측되는 패턴입니다.
💡 시간대별, 그룹별로 결측 비율이 다르다면 데이터 수집 과정에 문제가 있을 수 있습니다. 담당자에게 피드백하세요.
💡 결측치가 너무 많은 행(예: 50% 이상)은 삭제를 고려하세요. 대체해도 신뢰도가 낮습니다.
💡 시각화 결과를 보고서에 포함하면 데이터 품질을 투명하게 전달하고, 분석 결과의 한계를 명확히 할 수 있습니다.