이미지 로딩 중...

데이터 클리닝 완벽 가이드 - 슬라이드 1/11
A

AI Generated

2025. 11. 17. · 7 Views

데이터 클리닝 완벽 가이드

데이터 분석의 80%를 차지하는 데이터 클리닝! 실무에서 자주 마주치는 결측치, 중복 데이터, 이상치를 Polars로 효율적으로 처리하는 방법을 배워보세요. 초보자도 바로 따라할 수 있는 실전 예제와 함께 데이터 정제의 모든 것을 알려드립니다.


목차

  1. 결측치 확인하기 - 데이터의 빈 공간 찾아내기
  2. 결측치 제거하기 - 불완전한 데이터 걸러내기
  3. 결측치 채우기 - 빈 공간을 의미있는 값으로 대체하기
  4. 중복 데이터 확인하기 - 같은 데이터가 여러 번 들어있는지 찾기
  5. 중복 데이터 제거하기 - 하나만 남기고 나머지 삭제하기
  6. 이상치 탐지하기 - 비정상적으로 튀는 값 찾아내기
  7. 데이터 타입 변환하기 - 올바른 형식으로 바꾸기
  8. 문자열 정제하기 - 공백과 특수문자 제거하기
  9. 데이터 표준화하기 - 범위와 단위 통일하기
  10. 데이터 검증하기 - 규칙 위반 찾아내기

1. 결측치 확인하기 - 데이터의 빈 공간 찾아내기

시작하며

여러분이 고객 데이터를 분석하려고 엑셀 파일을 열었는데, 이메일 주소나 전화번호가 비어있는 행들이 여기저기 섞여있는 걸 발견한 적 있나요? 혹은 매출 데이터를 정리하려는데 일부 날짜나 금액이 누락되어 있어서 당황한 경험이 있으실 겁니다.

이런 문제는 실제 개발 현장에서 매우 자주 발생합니다. 데이터를 수집하는 과정에서 입력 오류가 생기거나, 시스템 오류로 일부 값이 저장되지 않거나, 사용자가 선택적으로 정보를 입력하지 않는 경우들이 흔하기 때문입니다.

이런 결측치를 그대로 두고 분석하면 잘못된 결과가 나오거나 프로그램이 오류를 일으킬 수 있습니다. 바로 이럴 때 필요한 것이 결측치 확인입니다.

Polars를 사용하면 데이터의 어느 부분에 빈 값이 있는지 빠르게 찾아낼 수 있어서, 다음 단계의 처리 방법을 결정할 수 있습니다.

개요

간단히 말해서, 결측치 확인은 데이터에서 비어있는 값(NULL, None, NaN)이 어디에 얼마나 있는지 찾아내는 작업입니다. 왜 이 과정이 필요할까요?

데이터 분석을 시작하기 전에 전체 데이터의 상태를 파악해야 하기 때문입니다. 예를 들어, 1만 개의 고객 데이터 중 이메일이 5천 개나 비어있다면 이메일 마케팅 전략을 다시 세워야 할 것이고, 매출 금액이 10개만 누락되었다면 해당 데이터만 수정하거나 제거하면 됩니다.

기존에는 pandas의 isna() 함수를 사용했다면, Polars는 null_count() 메서드로 더 빠르게 결측치를 집계할 수 있습니다. 특히 대용량 데이터를 다룰 때 Polars의 성능이 훨씬 뛰어납니다.

결측치 확인의 핵심 특징은 첫째, 각 컬럼별로 결측치 개수를 파악할 수 있고, 둘째, 전체 데이터 대비 결측치 비율을 계산할 수 있으며, 셋째, 결측치가 있는 행만 필터링해서 볼 수 있다는 점입니다. 이러한 특징들은 데이터 정제 전략을 수립하는 데 매우 중요한 정보를 제공합니다.

코드 예제

import polars as pl

# 샘플 데이터 생성 (실제로는 CSV나 DB에서 불러옵니다)
df = pl.DataFrame({
    "이름": ["김철수", "이영희", None, "박민수", "최지원"],
    "나이": [25, 30, 28, None, 35],
    "이메일": ["kim@email.com", None, "lee@email.com", None, "choi@email.com"],
    "점수": [85, 90, None, 88, 92]
})

# 각 컬럼별 결측치 개수 확인
null_counts = df.null_count()
print("컬럼별 결측치 개수:")
print(null_counts)

설명

이것이 하는 일: 위 코드는 데이터프레임의 각 컬럼에서 비어있는 값(None, NULL)이 몇 개인지 세어줍니다. 마치 학교에서 출석부를 확인할 때 각 날짜별로 결석한 학생 수를 세는 것과 같습니다.

첫 번째로, pl.DataFrame()으로 예제 데이터를 만듭니다. 실제 업무에서는 pl.read_csv() 또는 pl.read_parquet()로 파일을 불러오겠지만, 여기서는 학습을 위해 직접 만들었습니다.

데이터를 보면 "이름"에 1개, "나이"에 1개, "이메일"에 2개의 결측치가 있는 것을 알 수 있습니다. 그 다음으로, df.null_count() 메서드가 실행되면서 각 컬럼을 순회하며 None 값을 카운팅합니다.

내부적으로 Polars는 Rust로 작성된 고성능 엔진을 사용하기 때문에 수백만 행의 데이터도 몇 초 안에 처리할 수 있습니다. 마지막으로, 결과를 출력하면 각 컬럼별로 결측치 개수가 담긴 새로운 데이터프레임을 얻을 수 있습니다.

최종적으로 "이메일 컬럼에 결측치가 2개나 있으니 이메일 기반 분석은 조심해야겠다"는 판단을 내릴 수 있습니다. 여러분이 이 코드를 사용하면 데이터 품질을 즉시 파악할 수 있고, 어느 컬럼을 중점적으로 정제해야 할지 우선순위를 정할 수 있으며, 데이터 수집 프로세스의 문제점도 발견할 수 있습니다.

실전 팁

💡 전체 결측치 비율을 계산하려면 (df.null_count().sum() / (df.height * df.width) * 100)을 사용하세요. 전체 데이터 중 몇 %가 비어있는지 한눈에 파악할 수 있습니다.

💡 결측치가 있는 행만 보고 싶다면 df.filter(pl.any_horizontal(pl.all().is_null()))을 사용하세요. 문제가 있는 데이터만 골라서 확인할 수 있어 효율적입니다.

💡 특정 컬럼의 결측치 비율만 확인하려면 df.select(pl.col("컬럼명").is_null().mean() * 100)을 사용하세요. 해당 컬럼의 결측치 퍼센트가 출력됩니다.

💡 결측치 패턴을 시각화하려면 결과를 pandas로 변환 후 seaborn의 heatmap을 사용하세요. 어느 컬럼들에 동시에 결측치가 많은지 패턴을 발견할 수 있습니다.

💡 대용량 파일은 처음부터 다 읽지 말고 pl.scan_csv()로 lazy evaluation을 활용하세요. 메모리를 절약하면서도 결측치 통계를 빠르게 얻을 수 있습니다.


2. 결측치 제거하기 - 불완전한 데이터 걸러내기

시작하며

여러분이 설문조사 데이터 1,000건을 받았는데, 그중 50건은 필수 질문에 답변하지 않은 불완전한 데이터라면 어떻게 하시겠어요? 이런 데이터를 그대로 두고 평균을 계산하거나 그래프를 그리면 왜곡된 결과가 나올 수밖에 없습니다.

이런 문제는 데이터 분석에서 가장 먼저 해결해야 할 과제입니다. 불완전한 데이터를 포함하면 분석 결과의 신뢰도가 떨어지고, 머신러닝 모델을 학습시킬 때도 성능이 저하됩니다.

특히 결측치 비율이 5% 미만으로 적을 때는 해당 행을 삭제하는 것이 가장 간단하고 효과적인 방법입니다. 바로 이럴 때 필요한 것이 결측치 제거입니다.

Polars의 drop_nulls() 메서드를 사용하면 결측치가 있는 행을 한 줄의 코드로 깔끔하게 제거할 수 있습니다.

개요

간단히 말해서, 결측치 제거는 비어있는 값이 포함된 행이나 컬럼을 데이터에서 삭제하는 작업입니다. 왜 이 작업이 필요한지 실무 관점에서 설명하자면, 대부분의 통계 함수나 머신러닝 알고리즘은 결측치를 처리하지 못하거나 예상치 못한 결과를 만들어내기 때문입니다.

예를 들어, 고객의 구매 금액 평균을 계산할 때 금액이 비어있는 행이 포함되면 계산이 불가능하거나 잘못된 평균이 나옵니다. 기존 pandas에서는 dropna() 메서드를 사용했다면, Polars는 drop_nulls() 메서드로 더욱 직관적이고 빠르게 결측치를 제거할 수 있습니다.

게다가 특정 컬럼만 지정해서 제거할 수도 있어 유연합니다. 이 방법의 핵심 특징은 첫째, 전체 행 중 하나라도 결측치가 있으면 삭제할 수 있고, 둘째, 특정 컬럼의 결측치만 기준으로 삭제할 수 있으며, 셋째, 원본 데이터는 그대로 두고 새로운 데이터프레임을 반환한다는 점입니다.

이러한 특징들은 데이터의 무결성을 유지하면서도 안전하게 정제 작업을 수행할 수 있게 해줍니다.

코드 예제

import polars as pl

# 샘플 데이터
df = pl.DataFrame({
    "고객ID": [1, 2, 3, 4, 5],
    "이름": ["김철수", "이영희", None, "박민수", "최지원"],
    "구매금액": [50000, None, 30000, 45000, None],
    "등급": ["Gold", "Silver", "Bronze", None, "Gold"]
})

# 방법 1: 모든 컬럼에서 결측치가 하나라도 있는 행 제거
df_cleaned_all = df.drop_nulls()
print("모든 결측치 제거:", df_cleaned_all.height, "행 남음")

# 방법 2: 특정 컬럼(구매금액)의 결측치만 기준으로 행 제거
df_cleaned_subset = df.drop_nulls(subset=["구매금액"])
print("구매금액 결측치만 제거:", df_cleaned_subset.height, "행 남음")

설명

이것이 하는 일: 위 코드는 데이터프레임에서 비어있는 값이 포함된 행들을 찾아서 삭제하는 작업을 수행합니다. 마치 시험지 중에서 이름을 안 쓴 답안지나 답을 전혀 쓰지 않은 답안지를 골라내는 것과 비슷합니다.

첫 번째로, 예제 데이터를 보면 총 5개 행이 있고, "이름", "구매금액", "등급" 컬럼에 각각 결측치가 섞여 있습니다. df.drop_nulls()를 실행하면 어느 컬럼이든 하나라도 결측치가 있는 행은 모두 제거되어, 결국 "고객ID 1번"과 "고객ID 4번" 2개 행만 남게 됩니다.

그 다음으로, df.drop_nulls(subset=["구매금액"]) 코드는 "구매금액" 컬럼만 확인해서 해당 컬럼에 결측치가 있는 행만 제거합니다. 다른 컬럼에 결측치가 있어도 무시하므로, "고객ID 1, 3, 4번" 3개 행이 남습니다.

이 방식은 분석에 꼭 필요한 컬럼만 깨끗하게 유지하고 싶을 때 유용합니다. 마지막으로, 두 메서드 모두 원본 데이터프레임 df는 변경하지 않고 새로운 데이터프레임을 반환합니다.

최종적으로 여러분은 원본을 보관한 채로 정제된 버전을 별도로 가질 수 있어, 나중에 비교하거나 되돌릴 수 있습니다. 여러분이 이 코드를 사용하면 불완전한 데이터로 인한 분석 오류를 예방할 수 있고, 필수 정보가 있는 데이터만 골라서 고품질 데이터셋을 만들 수 있으며, 통계 계산이나 모델 학습 시 에러를 방지할 수 있습니다.

실전 팁

💡 결측치 제거 전에 항상 df.null_count()로 먼저 확인하세요. 너무 많은 데이터가 삭제되면 분석 자체가 불가능할 수 있으니, 결측치가 5% 미만일 때만 제거하는 것이 안전합니다.

💡 중요한 컬럼만 subset으로 지정하세요. 예를 들어 매출 분석이라면 "구매금액"은 필수지만 "메모" 같은 부가 정보는 비어있어도 괜찮습니다.

💡 제거한 행의 개수를 기록하세요. removed_count = df.height - df_cleaned.height로 몇 건이 삭제되었는지 추적하면 데이터 손실을 모니터링할 수 있습니다.

💡 시계열 데이터는 결측치 제거 대신 보간(interpolation)을 고려하세요. 연속된 날짜 데이터에서 중간 값을 삭제하면 시간 흐름이 끊겨 분석이 어려워집니다.

💡 결측치 제거 후 인덱스를 재설정하지 않아도 됩니다. Polars는 pandas와 달리 자동으로 인덱스를 관리하므로 별도 작업이 필요 없습니다.


3. 결측치 채우기 - 빈 공간을 의미있는 값으로 대체하기

시작하며

여러분이 온도 센서 데이터를 분석하는데, 센서가 일시적으로 끊겨서 10시 37분의 온도 값이 누락되었다면 어떻게 할까요? 10시 36분은 25.3도, 10시 38분은 25.5도인데 37분만 비어있다면, 이 행을 삭제하는 것보다 주변 값을 참고해서 25.4도로 채우는 게 더 합리적이지 않을까요?

이런 상황은 IoT 센서 데이터, 주가 데이터, 웹사이트 트래픽 데이터 등에서 매우 흔합니다. 결측치 비율이 높거나, 시계열 데이터처럼 연속성이 중요하거나, 특정 값으로 채우는 것이 업무 규칙인 경우에는 제거보다 채우기가 더 적절합니다.

예를 들어 설문조사에서 "응답 안 함"을 "기타"로 분류하는 것도 결측치 채우기의 일종입니다. 바로 이럴 때 필요한 것이 결측치 채우기입니다.

Polars의 fill_null(), fill_nan(), forward_fill(), backward_fill() 메서드들을 활용하면 다양한 전략으로 빈 값을 채울 수 있습니다.

개요

간단히 말해서, 결측치 채우기는 비어있는 값을 특정 값(평균, 중앙값, 0 등), 앞뒤 값, 또는 계산된 값으로 대체하는 작업입니다. 왜 이 작업이 중요할까요?

데이터를 삭제하면 정보 손실이 크지만, 합리적으로 채우면 데이터를 최대한 활용하면서도 분석의 정확도를 유지할 수 있기 때문입니다. 예를 들어, 1만 건의 고객 데이터 중 나이가 500건 누락되었다면 전부 삭제하는 것보다 평균 나이로 채우거나 다른 정보를 기반으로 추정하는 것이 현명합니다.

기존 pandas에서는 fillna() 메서드를 사용했다면, Polars는 fill_null() 메서드와 forward/backward fill 같은 다양한 전략을 제공하여 더 유연하게 결측치를 처리할 수 있습니다. 특히 lazy evaluation을 지원해서 대용량 데이터도 메모리 효율적으로 처리합니다.

결측치 채우기의 핵심 특징은 첫째, 평균, 중앙값, 최빈값 등 통계값으로 채울 수 있고, 둘째, 앞뒤 행의 값을 이용한 forward/backward fill이 가능하며, 셋째, 특정 상수값(0, "없음", "기타" 등)으로 일괄 대체할 수 있다는 점입니다. 이러한 특징들은 데이터의 특성과 분석 목적에 맞게 최적의 전략을 선택할 수 있게 해줍니다.

코드 예제

import polars as pl

# 샘플 데이터
df = pl.DataFrame({
    "날짜": ["2024-01-01", "2024-01-02", "2024-01-03", "2024-01-04", "2024-01-05"],
    "온도": [25.3, None, 25.5, None, 26.1],
    "습도": [60, 65, None, 70, None],
    "상태": ["정상", "정상", None, "정상", "정상"]
})

# 방법 1: 특정 값으로 채우기 (상태를 "측정안함"으로)
df_filled1 = df.with_columns(pl.col("상태").fill_null("측정안함"))

# 방법 2: 앞 값으로 채우기 (온도)
df_filled2 = df.with_columns(pl.col("온도").forward_fill())

# 방법 3: 평균값으로 채우기 (습도)
df_filled3 = df.with_columns(pl.col("습도").fill_null(pl.col("습도").mean()))

print("채워진 데이터:")
print(df_filled3)

설명

이것이 하는 일: 위 코드는 데이터의 빈 공간을 여러 가지 전략으로 채워서 완전한 데이터셋을 만듭니다. 마치 퍼즐에서 빠진 조각을 주변 조각들을 참고해서 그려 넣는 것과 같습니다.

첫 번째로, fill_null("측정안함") 부분은 "상태" 컬럼의 모든 None 값을 "측정안함"이라는 문자열로 바꿉니다. 왜 이렇게 하는지 설명하자면, 나중에 데이터를 그룹화하거나 필터링할 때 None은 처리가 까다로운데, 명확한 문자열로 바꾸면 "정상"과 "측정안함" 두 카테고리로 깔끔하게 분류할 수 있기 때문입니다.

두 번째로, forward_fill() 메서드는 온도 데이터의 결측치를 바로 위(앞) 행의 값으로 채웁니다. 예를 들어 1월 2일의 온도가 None이면 1월 1일의 25.3도를 복사해서 넣습니다.

시계열 데이터에서 값이 급격히 변하지 않는다는 가정 하에 이전 값을 사용하는 것이 합리적입니다. 반대로 backward_fill()을 쓰면 다음 행의 값을 가져옵니다.

세 번째로, fill_null(pl.col("습도").mean()) 코드는 좀 더 복잡합니다. 먼저 습도 컬럼의 평균을 계산한 후(60, 65, 70의 평균 = 65), 그 평균값으로 모든 결측치를 채웁니다.

내부적으로 Polars는 먼저 평균을 구하고 나서 그 값을 결측치 위치에 broadcasting하는 방식으로 동작합니다. 마지막으로, with_columns() 메서드는 원본 컬럼을 새로운 컬럼으로 교체하면서 나머지 컬럼은 그대로 유지합니다.

최종적으로 모든 결측치가 의미있는 값으로 채워진 깨끗한 데이터프레임을 얻을 수 있습니다. 여러분이 이 코드를 사용하면 데이터 손실 없이 전체 데이터를 활용할 수 있고, 시계열 데이터의 연속성을 유지할 수 있으며, 통계 분석이나 시각화를 할 때 결측치로 인한 오류를 방지할 수 있습니다.

실전 팁

💡 평균으로 채우기 전에 이상치를 먼저 제거하세요. 극단적인 값이 평균을 왜곡시켜 오히려 이상한 값으로 채워질 수 있습니다. 중앙값(median)을 사용하는 것도 좋은 대안입니다.

💡 범주형 데이터는 최빈값(mode)으로 채우세요. 예를 들어 "지역" 컬럼의 결측치는 가장 많이 등장하는 지역으로 채우는 것이 통계적으로 합리적입니다.

💡 forward_fill과 backward_fill을 연달아 사용하면 양방향 채우기가 가능합니다. df.forward_fill().backward_fill()처럼 체이닝하면 앞뒤 모두 참고해서 빈틈없이 채울 수 있습니다.

💡 날짜나 시간 데이터는 보간(interpolation)을 고려하세요. Polars는 아직 직접 지원하지 않지만, pandas로 변환 후 interpolate() 사용하거나 직접 선형 보간 로직을 구현할 수 있습니다.

💡 여러 컬럼을 동시에 채우려면 with_columns([pl.col("A").fill_null(0), pl.col("B").fill_null(0)])처럼 리스트로 전달하세요. 한 번에 여러 컬럼을 처리해 코드가 간결해집니다.


4. 중복 데이터 확인하기 - 같은 데이터가 여러 번 들어있는지 찾기

시작하며

여러분이 회원 가입 데이터베이스를 확인하는데, 같은 이메일 주소로 회원 가입이 3번이나 되어 있다면 뭔가 잘못되었다는 걸 알 수 있겠죠? 혹은 쇼핑몰 주문 데이터에서 같은 주문번호가 2개 존재한다면 시스템 오류이거나 데이터 통합 과정에서 실수가 있었을 가능성이 큽니다.

이런 중복 데이터는 여러 이유로 발생합니다. 사용자가 실수로 폼을 여러 번 제출하거나, 여러 데이터 소스를 통합할 때 중복 제거를 하지 않았거나, ETL 파이프라인에서 같은 데이터를 여러 번 적재하는 버그가 있을 수 있습니다.

중복 데이터를 그대로 두면 집계가 잘못되거나(매출 2배로 계산), 통계가 왜곡되거나(같은 사람이 설문에 3번 응답한 것처럼 보임), 저장 공간이 낭비됩니다. 바로 이럴 때 필요한 것이 중복 데이터 확인입니다.

Polars의 is_duplicated() 메서드를 사용하면 어떤 행이 중복인지 빠르게 찾아낼 수 있습니다.

개요

간단히 말해서, 중복 데이터 확인은 데이터프레임에서 완전히 같은 행이나 특정 컬럼 값이 같은 행이 여러 개 있는지 찾아내는 작업입니다. 실무에서 왜 필요한지 구체적으로 설명하자면, 데이터 품질 검증의 핵심 단계이기 때문입니다.

예를 들어, 온라인 광고 클릭 데이터에서 같은 클릭이 중복 집계되면 광고 비용을 잘못 청구할 수 있고, 재고 관리 시스템에서 같은 입고 기록이 2번 들어가면 실제보다 재고가 많은 것처럼 보입니다. 기존에는 pandas의 duplicated() 메서드를 사용했다면, Polars는 is_duplicated() 메서드로 동일한 기능을 더 빠르게 수행할 수 있습니다.

특히 수천만 행의 대용량 데이터에서 중복을 찾을 때 Polars의 병렬 처리 능력이 큰 차이를 만듭니다. 중복 확인의 핵심 특징은 첫째, 전체 행을 비교해서 완전히 동일한 행을 찾을 수 있고, 둘째, 특정 컬럼만 지정해서 해당 컬럼 값이 같은 행을 찾을 수 있으며, 셋째, 중복 여부를 True/False로 표시해서 필터링이나 분석에 바로 활용할 수 있다는 점입니다.

이러한 특징들은 데이터 무결성을 검증하고 문제를 해결하는 데 필수적입니다.

코드 예제

import polars as pl

# 샘플 데이터 (중복이 있는 경우)
df = pl.DataFrame({
    "고객ID": [1, 2, 3, 2, 4, 3],
    "이름": ["김철수", "이영희", "박민수", "이영희", "최지원", "박민수"],
    "이메일": ["kim@email.com", "lee@email.com", "park@email.com",
              "lee@email.com", "choi@email.com", "park@email.com"]
})

# 방법 1: 전체 행이 중복인지 확인
df_with_dup_flag = df.with_columns(
    pl.struct(pl.all()).is_duplicated().alias("중복여부")
)

# 방법 2: 특정 컬럼(이메일)만 기준으로 중복 확인
email_duplicates = df.filter(pl.col("이메일").is_duplicated())

print("중복된 이메일 가진 행들:")
print(email_duplicates)

설명

이것이 하는 일: 위 코드는 데이터프레임에서 완전히 같은 행이나 특정 컬럼 값이 같은 행들을 찾아내서 표시합니다. 마치 명단에서 같은 이름이 여러 번 나오는 사람을 형광펜으로 표시하는 것과 같습니다.

첫 번째로, 예제 데이터를 보면 "고객ID 2번"과 "고객ID 3번"이 각각 2번씩 나타나는 것을 알 수 있습니다. pl.struct(pl.all())은 모든 컬럼을 하나의 구조체로 묶어서 행 전체를 비교할 수 있게 만들고, is_duplicated()는 각 행이 다른 곳에도 존재하는지 확인합니다.

중복이면 True, 아니면 False를 반환하므로 새로운 "중복여부" 컬럼이 생성됩니다. 그 다음으로, pl.col("이메일").is_duplicated() 코드는 "이메일" 컬럼만 확인합니다.

"lee@email.com"과 "park@email.com"이 각각 2번 나타나므로, 이 이메일을 가진 총 4개 행이 True로 표시됩니다. 내부적으로 Polars는 해시 테이블을 사용해 빠르게 중복을 찾아냅니다.

세 번째로, filter() 메서드로 중복된 행만 골라냅니다. 이렇게 하면 문제가 있는 데이터만 따로 추출해서 확인할 수 있고, 데이터 입력 담당자에게 "이 이메일들이 중복되었으니 확인해주세요"라고 리포트할 수 있습니다.

마지막으로, 이 결과를 기반으로 중복 제거 작업을 진행하거나, 중복 발생 원인을 분석하거나, 시스템 개선 방향을 결정할 수 있습니다. 최종적으로 데이터 품질 보고서에 "총 6건 중 4건이 중복, 중복률 66%"라는 정보를 포함시킬 수 있습니다.

여러분이 이 코드를 사용하면 데이터 무결성 문제를 조기에 발견할 수 있고, 잘못된 집계나 분석을 방지할 수 있으며, 데이터 수집 프로세스의 버그를 찾아낼 수 있습니다.

실전 팁

💡 중복 확인 전에 데이터를 정렬하세요. df.sort(["고객ID", "등록일시"])처럼 정렬하면 중복된 행들이 모여 있어 육안으로도 확인하기 쉽습니다.

💡 첫 번째 발생은 유지하고 싶다면 is_duplicated() 대신 is_first_distinct()의 반대를 사용하세요. ~pl.col("이메일").is_first_distinct()는 첫 번째는 False, 나머지는 True로 표시합니다.

💡 여러 컬럼 조합으로 중복 확인하려면 pl.struct(["컬럼1", "컬럼2"]).is_duplicated()를 사용하세요. 예를 들어 이름과 생년월일 조합으로 동명이인을 구분할 수 있습니다.

💡 중복 개수를 세려면 df.group_by("이메일").count().filter(pl.col("count") > 1)을 사용하세요. 각 이메일이 몇 번 중복되었는지 통계를 얻을 수 있습니다.

💡 대용량 데이터는 pl.scan_csv()로 읽고 lazy evaluation으로 처리하세요. 메모리에 다 올리지 않고도 중복 확인이 가능합니다.


5. 중복 데이터 제거하기 - 하나만 남기고 나머지 삭제하기

시작하며

여러분이 고객 데이터베이스에서 같은 이메일로 3개의 계정을 발견했다면, 분석할 때는 이 고객을 1명으로 세어야 할까요, 3명으로 세어야 할까요? 당연히 1명이겠죠.

중복을 그대로 두고 "전체 고객 수"를 계산하면 실제보다 부풀려진 숫자가 나와서 마케팅 전략을 잘못 세울 수 있습니다. 중복 제거는 데이터 클리닝에서 가장 중요한 작업 중 하나입니다.

중복된 주문 데이터는 매출을 과대평가하게 만들고, 중복된 로그 데이터는 웹사이트 트래픽을 잘못 분석하게 만들며, 중복된 실험 데이터는 연구 결과를 왜곡시킵니다. 특히 여러 시스템의 데이터를 통합할 때 중복 제거는 필수적입니다.

바로 이럴 때 필요한 것이 중복 데이터 제거입니다. Polars의 unique() 메서드를 사용하면 중복된 행들 중 하나만 남기고 나머지를 삭제할 수 있습니다.

개요

간단히 말해서, 중복 데이터 제거는 완전히 같은 행이나 특정 컬럼 값이 같은 행들 중 하나만 남기고 나머지를 삭제하는 작업입니다. 왜 이 작업이 데이터 분석의 기본인지 설명하자면, 정확한 집계와 통계를 위해서는 각 엔티티(고객, 주문, 이벤트 등)가 정확히 한 번씩만 카운트되어야 하기 때문입니다.

예를 들어, 설문조사에서 같은 사람이 3번 응답했다면 그 사람의 의견이 3배로 반영되는 셈이 되어 공정하지 않습니다. 기존 pandas에서는 drop_duplicates() 메서드를 사용했다면, Polars는 unique() 메서드로 더 직관적이고 빠르게 중복을 제거할 수 있습니다.

게다가 어떤 중복을 남길지(첫 번째, 마지막, 또는 랜덤) 선택할 수도 있습니다. 중복 제거의 핵심 특징은 첫째, 전체 행이 동일한 경우를 제거할 수 있고, 둘째, 특정 컬럼만 기준으로 중복을 정의할 수 있으며, 셋째, 첫 번째 또는 마지막 발생을 유지하는 전략을 선택할 수 있다는 점입니다.

이러한 특징들은 비즈니스 규칙에 맞게 유연하게 중복을 처리할 수 있게 해줍니다.

코드 예제

import polars as pl

# 샘플 데이터 (중복 포함)
df = pl.DataFrame({
    "고객ID": [1, 2, 3, 2, 4, 3, 1],
    "이름": ["김철수", "이영희", "박민수", "이영희", "최지원", "박민수", "김철수"],
    "가입일": ["2024-01-01", "2024-01-02", "2024-01-03",
              "2024-01-02", "2024-01-05", "2024-01-03", "2024-01-01"]
})

# 방법 1: 모든 컬럼을 기준으로 완전히 동일한 행 제거 (첫 번째 유지)
df_unique_all = df.unique()

# 방법 2: 특정 컬럼(고객ID)만 기준으로 중복 제거 (첫 번째 유지)
df_unique_subset = df.unique(subset=["고객ID"], keep="first")

# 방법 3: 마지막 발생을 유지하고 싶을 때
df_unique_last = df.unique(subset=["고객ID"], keep="last")

print("고객ID 기준 중복 제거 (첫 번째 유지):")
print(df_unique_subset)

설명

이것이 하는 일: 위 코드는 데이터프레임에서 중복된 행들을 찾아서 대표값 하나만 남기고 나머지를 삭제합니다. 마치 반 명단에서 같은 이름이 3번 나오면 1명만 남기고 지우는 것과 같습니다.

첫 번째로, df.unique() 메서드는 모든 컬럼의 값이 완전히 일치하는 행들을 찾습니다. 예제에서는 7개 행 중 "고객ID 1번"과 "고객ID 2번", "고객ID 3번"이 각각 중복되어 있으므로, 각 그룹의 첫 번째 행만 남기고 나머지를 제거해 최종적으로 4개 행이 남습니다.

그 다음으로, unique(subset=["고객ID"]) 코드는 "고객ID" 컬럼만 확인합니다. 다른 컬럼 값은 달라도 고객ID만 같으면 중복으로 간주합니다.

왜 이렇게 하는지 설명하자면, 실무에서는 고유 식별자(ID, 이메일, 주민번호 등)를 기준으로 중복을 판단하는 경우가 많기 때문입니다. 내부적으로 Polars는 해시 기반 알고리즘으로 중복을 빠르게 찾아냅니다.

세 번째로, keep="first" 파라미터는 중복된 행들 중 첫 번째로 나타난 것을 유지하고 나머지를 삭제합니다. 반대로 keep="last"를 사용하면 마지막 발생을 유지하는데, 이는 최신 정보를 유지하고 싶을 때 유용합니다.

예를 들어 같은 고객이 여러 번 가입했다면 가장 최근 가입 정보를 남기는 것이 합리적일 수 있습니다. 마지막으로, 원본 데이터는 변경되지 않고 중복이 제거된 새로운 데이터프레임이 반환됩니다.

최종적으로 "고객은 4명입니다"라는 정확한 집계를 할 수 있고, 각 고객당 하나의 레코드만 유지되어 1:1 분석이 가능해집니다. 여러분이 이 코드를 사용하면 정확한 통계와 집계를 얻을 수 있고, 중복으로 인한 왜곡을 제거할 수 있으며, 데이터 저장 공간을 절약할 수 있고, 데이터 품질을 크게 향상시킬 수 있습니다.

실전 팁

💡 중복 제거 전에 데이터를 정렬하세요. df.sort("가입일").unique(subset=["고객ID"], keep="first")처럼 하면 가장 오래된(또는 최신) 레코드를 유지할 수 있습니다.

💡 제거된 행의 개수를 기록하세요. removed = df.height - df_unique.height로 몇 건이 중복이었는지 추적하면 데이터 품질 리포트에 포함시킬 수 있습니다.

💡 여러 컬럼 조합으로 중복 판단하려면 unique(subset=["이름", "생년월일"])처럼 리스트로 전달하세요. 동명이인을 구분하는 데 유용합니다.

💡 중복을 제거하지 말고 표시만 하고 싶다면 df.with_row_count().filter(~pl.col("이메일").is_duplicated())를 사용하세요. 중복을 수동으로 검토할 수 있습니다.

💡 대용량 데이터는 pl.scan_csv().unique().collect()처럼 lazy evaluation을 활용하세요. 메모리 사용량을 최소화하면서 중복을 제거할 수 있습니다.


6. 이상치 탐지하기 - 비정상적으로 튀는 값 찾아내기

시작하며

여러분이 온라인 쇼핑몰의 상품 가격 데이터를 분석하는데, 대부분 가격이 1만원에서 10만원 사이인데 갑자기 999만원짜리 상품이 하나 있다면 이상하다고 느끼지 않나요? 이건 입력 실수일 수도 있고(원래 9만 9천원인데 0을 2개 더 누름), 아니면 실제로 특별한 명품 상품일 수도 있습니다.

이런 이상치는 데이터 분석에 큰 영향을 미칩니다. 평균을 계산하면 극단값에 의해 왜곡되고, 그래프를 그리면 대부분의 데이터는 한쪽에 몰려서 잘 안 보이고, 머신러닝 모델은 이상치에 과적합될 수 있습니다.

통계학에서는 일반적으로 데이터의 1.5×IQR(사분위수 범위)를 벗어나는 값을 이상치로 정의합니다. 바로 이럴 때 필요한 것이 이상치 탐지입니다.

Polars의 통계 함수들을 조합하면 IQR 기반 또는 표준편차 기반으로 이상치를 찾아낼 수 있습니다.

개요

간단히 말해서, 이상치 탐지는 데이터 분포에서 비정상적으로 크거나 작은 값, 즉 다른 값들과 동떨어진 값들을 찾아내는 작업입니다. 실무에서 왜 중요한지 구체적으로 설명하자면, 이상치는 데이터 입력 오류일 수도 있고, 사기 거래의 신호일 수도 있고, 시스템 오작동의 증거일 수도 있기 때문입니다.

예를 들어, 신용카드 거래 데이터에서 평소 5만원 이하 결제만 하던 카드가 갑자기 500만원을 결제했다면 도난 카드일 가능성을 의심해야 합니다. 기존에는 pandas와 scipy를 함께 사용해서 복잡하게 계산했다면, Polars는 내장된 통계 함수들(quantile, std, mean 등)을 조합해서 더 빠르고 간결하게 이상치를 탐지할 수 있습니다.

특히 대용량 데이터에서 Polars의 병렬 처리가 큰 성능 차이를 만듭니다. 이상치 탐지의 핵심 특징은 첫째, IQR(Interquartile Range) 방식으로 상하위 이상치를 찾을 수 있고, 둘째, 표준편차 방식으로 평균에서 멀리 떨어진 값을 찾을 수 있으며, 셋째, 탐지된 이상치를 제거하거나 대체하거나 별도로 분석할 수 있다는 점입니다.

이러한 특징들은 데이터의 분포 특성에 맞게 적절한 방법을 선택할 수 있게 해줍니다.

코드 예제

import polars as pl

# 샘플 데이터 (이상치 포함)
df = pl.DataFrame({
    "상품명": ["상품A", "상품B", "상품C", "상품D", "상품E", "상품F", "상품G"],
    "가격": [25000, 30000, 28000, 35000, 9999999, 27000, 32000]
})

# IQR 방식으로 이상치 탐지
q1 = df.select(pl.col("가격").quantile(0.25)).item()  # 1사분위수
q3 = df.select(pl.col("가격").quantile(0.75)).item()  # 3사분위수
iqr = q3 - q1  # 사분위수 범위
lower_bound = q1 - 1.5 * iqr  # 하한
upper_bound = q3 + 1.5 * iqr  # 상한

# 이상치 표시 및 필터링
df_with_outlier = df.with_columns(
    ((pl.col("가격") < lower_bound) | (pl.col("가격") > upper_bound)).alias("이상치여부")
)

# 이상치만 추출
outliers = df_with_outlier.filter(pl.col("이상치여부"))
print("탐지된 이상치:")
print(outliers)

설명

이것이 하는 일: 위 코드는 데이터의 통계적 분포를 분석해서 비정상적으로 튀는 값을 자동으로 찾아냅니다. 마치 반 학생들의 키를 재서 평균에서 너무 벗어난 사람을 찾는 것과 비슷합니다.

첫 번째로, quantile(0.25)quantile(0.75)로 데이터의 25% 지점(Q1)과 75% 지점(Q3)의 값을 구합니다. 예제에서는 가격들을 정렬하면 [25000, 27000, 28000, 30000, 32000, 35000, 9999999]이 되고, Q1은 약 27000원, Q3는 약 32000원이 됩니다.

왜 이 값들을 구하는지 설명하자면, 데이터의 중간 50%가 어느 범위에 있는지 파악하기 위함입니다. 그 다음으로, IQR(Interquartile Range)을 계산합니다.

iqr = q3 - q1이므로 약 5000원이 됩니다. 통계학에서는 "Q1 - 1.5×IQR"보다 작거나 "Q3 + 1.5×IQR"보다 큰 값을 이상치로 정의합니다.

이 공식을 적용하면 하한은 약 19500원, 상한은 약 39500원이 되므로, 9999999원은 명백히 상한을 초과하는 이상치입니다. 세 번째로, with_columns()로 각 행이 이상치인지 아닌지 True/False로 표시하는 새로운 컬럼을 추가합니다.

내부적으로 (조건1) | (조건2)는 OR 연산으로, 하한보다 작거나 상한보다 크면 True가 됩니다. 마지막으로, filter(pl.col("이상치여부"))로 이상치만 추출해서 별도로 확인할 수 있습니다.

최종적으로 "상품E의 9999999원은 이상치입니다"라는 결과를 얻고, 이 값을 수정하거나 제거하거나 별도로 검토할 수 있습니다. 여러분이 이 코드를 사용하면 입력 오류를 자동으로 발견할 수 있고, 사기 거래나 이상 징후를 조기에 탐지할 수 있으며, 통계 분석 시 왜곡을 방지할 수 있고, 데이터 품질을 객관적으로 평가할 수 있습니다.

실전 팁

💡 IQR 방식은 정규분포가 아닌 데이터에도 잘 작동합니다. 반면 표준편차 방식(평균 ± 3×표준편차)은 정규분포를 가정하므로 데이터 분포를 먼저 확인하세요.

💡 이상치를 무조건 제거하지 마세요. 실제로 의미 있는 극단값(명품, VIP 고객, 대량 주문 등)일 수 있으니 도메인 전문가와 상의하세요.

💡 여러 컬럼의 이상치를 한 번에 탐지하려면 함수로 만드세요. def detect_outliers(col)처럼 재사용 가능한 함수를 작성하면 코드가 깔끔해집니다.

💡 이상치 대체 전략으로는 중앙값으로 바꾸기, 상하한값으로 자르기(winsorization), 로그 변환 등이 있습니다. 분석 목적에 맞게 선택하세요.

💡 시계열 데이터는 이동 평균과 비교하는 방법도 있습니다. 최근 7일 평균에서 크게 벗어난 값을 이상치로 판단하는 방식입니다.


7. 데이터 타입 변환하기 - 올바른 형식으로 바꾸기

시작하며

여러분이 CSV 파일에서 날짜를 불러왔는데 "2024-01-15"가 문자열로 인식되어서 날짜 계산이 안 되는 경험을 해보셨나요? 혹은 숫자로 저장되어야 할 우편번호가 "06234"처럼 문자열로 저장되어 있어서 평균을 계산하려니 에러가 나는 상황도 흔합니다.

이런 데이터 타입 문제는 외부 시스템에서 데이터를 가져올 때 매우 자주 발생합니다. 엑셀에서는 자동으로 날짜를 인식하지만 CSV로 저장하면 문자열이 되고, 데이터베이스에서는 숫자로 저장했지만 API로 받으면 문자열이 되는 경우들이 많습니다.

타입이 맞지 않으면 연산, 정렬, 필터링 모두 제대로 작동하지 않습니다. 바로 이럴 때 필요한 것이 데이터 타입 변환입니다.

Polars의 cast() 메서드를 사용하면 문자열을 숫자로, 숫자를 문자열로, 문자열을 날짜로 자유롭게 변환할 수 있습니다.

개요

간단히 말해서, 데이터 타입 변환은 컬럼의 데이터 타입을 다른 타입으로 바꾸는 작업입니다. 예를 들어 문자열(String)을 정수(Int64), 실수(Float64), 날짜(Date), 불린(Boolean) 등으로 변환하는 것입니다.

왜 이 작업이 필수적인지 설명하자면, 데이터 타입이 맞지 않으면 모든 후속 작업이 불가능하기 때문입니다. 예를 들어, "나이" 컬럼이 문자열 "25"로 저장되어 있으면 평균 나이를 계산할 수 없고, "주문일자"가 문자열 "2024-01-15"로 되어 있으면 "최근 30일 주문"을 필터링할 수 없습니다.

기존 pandas에서는 astype() 메서드를 사용했다면, Polars는 cast() 메서드로 더 명확하게 타입 변환을 수행할 수 있습니다. 특히 Polars는 타입 시스템이 엄격해서 타입 불일치로 인한 버그를 조기에 발견할 수 있습니다.

타입 변환의 핵심 특징은 첫째, 다양한 타입 간 변환을 지원하고(문자열↔숫자↔날짜↔불린), 둘째, 변환 실패 시 에러 처리 전략을 선택할 수 있으며(strict 모드), 셋째, 성능이 빠르고 메모리 효율적이라는 점입니다. 이러한 특징들은 안전하고 효율적인 데이터 전처리를 가능하게 합니다.

코드 예제

import polars as pl

# 샘플 데이터 (잘못된 타입)
df = pl.DataFrame({
    "고객ID": ["1", "2", "3", "4", "5"],  # 문자열이지만 숫자여야 함
    "나이": ["25", "30", "28", "35", "42"],  # 문자열이지만 숫자여야 함
    "가입일": ["2024-01-01", "2024-01-15", "2024-02-01", "2024-02-10", "2024-03-01"],  # 문자열이지만 날짜여야 함
    "활성여부": ["True", "False", "True", "True", "False"]  # 문자열이지만 불린이어야 함
})

# 타입 변환
df_converted = df.with_columns([
    pl.col("고객ID").cast(pl.Int64),  # 문자열 -> 정수
    pl.col("나이").cast(pl.Int32),  # 문자열 -> 정수
    pl.col("가입일").str.to_date("%Y-%m-%d"),  # 문자열 -> 날짜
    pl.col("활성여부").cast(pl.Boolean)  # 문자열 -> 불린
])

print("타입 변환 후:")
print(df_converted)
print("\n변환 후 데이터 타입:")
print(df_converted.dtypes)

설명

이것이 하는 일: 위 코드는 각 컬럼의 데이터 타입을 분석 목적에 맞는 올바른 타입으로 변환합니다. 마치 영어로 쓰인 숫자 "twenty-five"를 숫자 25로 바꾸는 번역 작업과 같습니다.

첫 번째로, cast(pl.Int64)는 "고객ID" 컬럼의 문자열 "1", "2", "3"을 정수 1, 2, 3으로 변환합니다. 왜 이렇게 하는지 설명하자면, 고객ID는 숫자 연산을 할 일은 없지만 정수로 저장하면 메모리를 절약하고 정렬이나 조인 성능이 향상되기 때문입니다.

내부적으로 Polars는 문자열을 파싱해서 64비트 정수로 변환합니다. 그 다음으로, "나이"도 비슷하게 cast(pl.Int32)로 변환합니다.

Int32는 -21억부터 +21억까지 저장할 수 있어 나이에는 충분하고, Int64보다 메모리를 절반만 사용합니다. 변환 후에는 df.select(pl.col("나이").mean())처럼 평균 계산이 가능해집니다.

세 번째로, "가입일"은 특별히 str.to_date("%Y-%m-%d")를 사용합니다. cast(pl.Date)도 작동하지만 to_date()는 날짜 형식을 명시적으로 지정할 수 있어 더 안전합니다.

변환 후에는 pl.col("가입일") > pl.date(2024, 2, 1)처럼 날짜 비교와 필터링이 가능해집니다. 마지막으로, "활성여부"는 cast(pl.Boolean)으로 불린 타입으로 변환됩니다.

문자열 "True"는 True로, "False"는 False로 자동 변환됩니다. 최종적으로 모든 컬럼이 올바른 타입을 가지게 되어, 통계 계산, 날짜 연산, 조건 필터링 등 모든 작업이 정상적으로 수행됩니다.

여러분이 이 코드를 사용하면 타입 불일치 오류를 제거할 수 있고, 연산과 분석이 정상적으로 작동하며, 메모리 사용량을 최적화할 수 있고, 코드의 명확성과 유지보수성이 향상됩니다.

실전 팁

💡 변환 실패를 무시하려면 cast(pl.Int64, strict=False)를 사용하세요. 변환할 수 없는 값은 null이 되어 프로그램이 중단되지 않습니다.

💡 CSV 읽을 때부터 타입을 지정하세요. pl.read_csv("file.csv", dtypes={"나이": pl.Int32})처럼 하면 읽으면서 동시에 변환되어 효율적입니다.

💡 날짜 형식이 여러 가지라면 coalesce를 사용하세요. pl.coalesce([pl.col("날짜").str.to_date("%Y-%m-%d"), pl.col("날짜").str.to_date("%d/%m/%Y")])처럼 여러 형식을 시도할 수 있습니다.

💡 숫자인 척 하는 문자열을 확인하려면 먼저 str.contains(r"^\d+$")로 검증하세요. 변환 전에 잘못된 데이터를 걸러낼 수 있습니다.

💡 대용량 데이터는 lazy evaluation으로 타입 변환하세요. pl.scan_csv().with_columns([...]).collect()하면 메모리에 다 올리지 않고 변환할 수 있습니다.


8. 문자열 정제하기 - 공백과 특수문자 제거하기

시작하며

여러분이 사용자 입력 데이터를 분석하는데, 이름이 " 김철수 " (앞뒤 공백), "이영희 " (뒤에만 공백 2개), "박민수" (공백 없음)처럼 들쑥날쑥하다면 같은 사람을 찾는 것도 어렵겠죠? 혹은 전화번호가 "010-1234-5678", "01012345678", "010 1234 5678" 같이 형식이 제각각이라면 통일된 분석이 불가능합니다.

이런 문자열 불일치는 사용자 입력 폼, 외부 API, 다양한 시스템의 데이터 통합 과정에서 매우 흔하게 발생합니다. 공백, 대소문자 혼용, 특수문자, 줄바꿈 등이 섞여 있으면 같은 데이터를 다르게 인식하여 중복 제거가 안 되고, 검색이 안 되고, 그룹화가 잘못됩니다.

바로 이럴 때 필요한 것이 문자열 정제입니다. Polars의 .str 네임스페이스 메서드들을 사용하면 공백 제거, 대소문자 변환, 특수문자 제거, 문자열 분리 등 다양한 정제 작업을 쉽게 수행할 수 있습니다.

개요

간단히 말해서, 문자열 정제는 텍스트 데이터에서 불필요한 공백, 특수문자, 대소문자 불일치 등을 제거하거나 통일하여 일관된 형식으로 만드는 작업입니다. 실무에서 왜 이 작업이 중요한지 구체적으로 설명하자면, 데이터 매칭과 중복 제거의 정확도를 높이기 위해서입니다.

예를 들어, 고객 이름을 기준으로 주문 내역과 회원 정보를 매칭할 때 공백 하나 차이로 매칭이 실패하면 완전히 다른 사람으로 인식됩니다. 이메일 주소에서 대소문자를 구분하면 "user@email.com"과 "User@Email.Com"을 다른 사람으로 봅니다.

기존 pandas에서는 .str.strip(), .str.lower() 같은 메서드들을 개별적으로 사용했다면, Polars도 동일한 .str 네임스페이스를 제공하지만 더 빠른 성능과 메서드 체이닝이 가능합니다. 특히 정규표현식 기반 치환도 지원해서 복잡한 패턴도 처리할 수 있습니다.

문자열 정제의 핵심 특징은 첫째, 앞뒤 공백을 제거하는 strip 기능, 둘째, 대소문자를 통일하는 upper/lower 기능, 셋째, 정규표현식으로 패턴 기반 치환이 가능하다는 점입니다. 이러한 특징들은 텍스트 데이터의 일관성을 확보하고 데이터 품질을 크게 향상시킵니다.

코드 예제

import polars as pl

# 샘플 데이터 (불일치하는 문자열)
df = pl.DataFrame({
    "이름": [" 김철수 ", "이영희  ", "  박민수", "최지원", " 정다은"],
    "이메일": ["Kim@Email.Com", "LEE@email.com", "park@EMAIL.com", "choi@email.com", "jung@Email.com"],
    "전화번호": ["010-1234-5678", "01023456789", "010 3456 7890", "010.4567.8901", "010-5678-9012"]
})

# 문자열 정제
df_cleaned = df.with_columns([
    pl.col("이름").str.strip_chars(),  # 앞뒤 공백 제거
    pl.col("이메일").str.to_lowercase(),  # 소문자로 통일
    pl.col("전화번호").str.replace_all(r"[-.\s]", "")  # 하이픈, 점, 공백 제거
])

print("정제된 데이터:")
print(df_cleaned)

설명

이것이 하는 일: 위 코드는 텍스트 데이터에서 불필요한 공백과 특수문자를 제거하고 대소문자를 통일하여 일관된 형식을 만듭니다. 마치 편지를 정리할 때 여백을 다듬고 글씨체를 통일하는 것과 같습니다.

첫 번째로, str.strip_chars()는 "이름" 컬럼의 각 값에서 앞뒤 공백을 제거합니다. " 김철수 "는 "김철수"로, " 박민수"는 "박민수"로 깔끔해집니다.

왜 이게 중요한지 설명하자면, 나중에 df.filter(pl.col("이름") == "김철수")로 검색할 때 공백 때문에 못 찾는 문제를 방지하기 때문입니다. 내부적으로 Polars는 유니코드 공백 문자들을 모두 인식해서 제거합니다.

그 다음으로, str.to_lowercase()는 "이메일" 컬럼을 모두 소문자로 바꿉니다. "Kim@Email.Com"은 "kim@email.com"이 되고, "LEE@email.com"은 "lee@email.com"이 됩니다.

이메일은 기술적으로 대소문자를 구분하지 않지만 데이터베이스나 프로그램에서는 구분할 수 있으므로, 소문자로 통일하면 중복 제거와 매칭이 정확해집니다. 세 번째로, str.replace_all(r"[-.\s]", "")는 정규표현식을 사용해서 전화번호의 하이픈(-), 점(.), 공백(\s)을 모두 빈 문자열로 교체합니다.

"010-1234-5678"은 "01012345678"이 되어, 모든 전화번호가 숫자만 남은 통일된 형식이 됩니다. 정규표현식 [-.\s]는 "이 중 하나"를 의미하므로 세 가지 문자를 한 번에 제거할 수 있습니다.

마지막으로, 이렇게 정제된 데이터는 중복 확인, 데이터 매칭, 검색, 그룹화 모든 작업에서 정확도가 크게 향상됩니다. 최종적으로 "같은 사람이지만 형식 차이로 다르게 인식되는" 문제를 해결할 수 있습니다.

여러분이 이 코드를 사용하면 데이터 매칭 정확도가 향상되고, 중복 제거가 제대로 작동하며, 검색과 필터링이 일관되게 동작하고, 사용자 입력 오류로 인한 데이터 품질 저하를 방지할 수 있습니다.

실전 팁

💡 전화번호는 정제 후 길이를 확인하세요. pl.col("전화번호").str.len_chars() == 11처럼 검증하면 잘못된 데이터를 걸러낼 수 있습니다.

💡 이메일 형식 검증은 정규표현식으로 가능합니다. str.contains(r"^[a-z0-9]+@[a-z0-9]+\.[a-z]+$")로 기본 형식을 체크하세요.

💡 여러 정제 작업은 메서드 체이닝하세요. pl.col("이름").str.strip_chars().str.to_uppercase()처럼 연결하면 깔끔합니다.

💡 한글 이름의 공백 제거는 주의하세요. "김 철수"는 성과 이름 사이 공백일 수 있으므로 strip_chars()만 사용하고 중간 공백은 유지하는 게 좋습니다.

💡 대량 텍스트 정제는 정규표현식 컴파일을 재사용하세요. 하지만 Polars는 내부적으로 최적화되어 있어 일반적으로는 신경 쓰지 않아도 됩니다.


9. 데이터 표준화하기 - 범위와 단위 통일하기

시작하며

여러분이 여러 센서에서 온도 데이터를 수집했는데, 센서A는 섭씨로, 센서B는 화씨로 기록되어 있다면 어떻게 비교하시겠어요? 혹은 학생들의 국어, 영어, 수학 점수를 합산해서 총점을 계산하려는데 국어는 100점 만점, 영어는 200점 만점이라면 공정한 비교가 불가능합니다.

이런 척도 불일치 문제는 데이터 통합이나 머신러닝에서 매우 중요합니다. 서로 다른 단위나 범위를 가진 변수들을 그대로 사용하면 값이 큰 변수가 분석이나 모델에 과도한 영향을 미치고, 작은 변수는 무시됩니다.

예를 들어 연봉(단위: 백만원, 범위 30200)과 나이(단위: 세, 범위 2060)를 함께 분석하면 연봉의 영향력이 압도적입니다. 바로 이럴 때 필요한 것이 데이터 표준화입니다.

Min-Max 정규화나 Z-score 표준화를 사용하면 모든 변수를 같은 척도로 변환할 수 있습니다.

개요

간단히 말해서, 데이터 표준화는 서로 다른 범위와 단위를 가진 변수들을 같은 척도로 변환하는 작업입니다. 대표적으로 0~1 사이로 만드는 Min-Max 정규화와 평균 0, 표준편차 1로 만드는 Z-score 표준화가 있습니다.

왜 이 작업이 필수적인지 설명하자면, 공정한 비교와 분석을 위해서입니다. 예를 들어, 머신러닝 모델은 값의 크기에 민감하므로 표준화하지 않으면 큰 값을 가진 특성에만 의존하게 됩니다.

K-means 클러스터링 같은 거리 기반 알고리즘은 반드시 표준화가 필요합니다. 기존에는 scikit-learn의 StandardScaler나 MinMaxScaler를 사용했다면, Polars는 수식을 직접 계산해서 더 명확하고 유연하게 표준화할 수 있습니다.

특히 lazy evaluation을 활용하면 대용량 데이터도 메모리 효율적으로 처리할 수 있습니다. 표준화의 핵심 특징은 첫째, Min-Max 정규화로 0~1(또는 지정 범위)로 변환할 수 있고, 둘째, Z-score 표준화로 평균 중심의 정규분포로 만들 수 있으며, 셋째, 이상치의 영향을 줄이거나(Robust Scaling) 로그 변환으로 왜도를 줄일 수 있다는 점입니다.

이러한 특징들은 데이터의 분포 특성에 맞게 최적의 방법을 선택할 수 있게 해줍니다.

코드 예제

import polars as pl

# 샘플 데이터 (범위가 다른 변수들)
df = pl.DataFrame({
    "학생": ["A", "B", "C", "D", "E"],
    "국어": [85, 90, 78, 92, 88],  # 범위: 78~92
    "수학": [150, 180, 160, 190, 170],  # 범위: 150~190 (200점 만점)
    "영어": [70, 85, 65, 90, 80]  # 범위: 65~90
})

# 방법 1: Min-Max 정규화 (0~1 범위로)
df_minmax = df.with_columns([
    ((pl.col("국어") - pl.col("국어").min()) /
     (pl.col("국어").max() - pl.col("국어").min())).alias("국어_정규화"),
    ((pl.col("수학") - pl.col("수학").min()) /
     (pl.col("수학").max() - pl.col("수학").min())).alias("수학_정규화"),
    ((pl.col("영어") - pl.col("영어").min()) /
     (pl.col("영어").max() - pl.col("영어").min())).alias("영어_정규화")
])

# 방법 2: Z-score 표준화 (평균 0, 표준편차 1)
df_zscore = df.with_columns([
    ((pl.col("국어") - pl.col("국어").mean()) / pl.col("국어").std()).alias("국어_표준화"),
    ((pl.col("수학") - pl.col("수학").mean()) / pl.col("수학").std()).alias("수학_표준화")
])

print("Min-Max 정규화 결과:")
print(df_minmax)

설명

이것이 하는 일: 위 코드는 서로 다른 범위를 가진 점수들을 같은 척도로 변환해서 공정하게 비교할 수 있게 만듭니다. 마치 서로 다른 화폐(원, 달러, 엔)를 하나의 기준 화폐로 환전하는 것과 같습니다.

첫 번째로, Min-Max 정규화 공식 (값 - 최소) / (최대 - 최소)를 사용합니다. 국어 점수를 예로 들면, 최소값 78, 최대값 92이므로 범위는 14입니다.

학생A의 85점은 (85-78)/(92-78) = 7/14 = 0.5가 되어 01 사이의 값으로 변환됩니다. 왜 이렇게 하는지 설명하자면, 모든 과목을 01 범위로 통일하면 과목 간 비교와 합산이 공정해지기 때문입니다.

그 다음으로, 수학과 영어도 동일한 공식을 적용합니다. 수학은 원래 150190 범위이지만 정규화 후에는 01 사이가 되고, 영어도 6590에서 01로 변환됩니다.

내부적으로 Polars는 먼저 각 컬럼의 최소/최대값을 계산하고, 그 값을 모든 행에 broadcasting해서 빠르게 연산합니다. 세 번째로, Z-score 표준화 공식 (값 - 평균) / 표준편차를 사용합니다.

이 방법은 평균을 0으로, 표준편차를 1로 만들어서 "평균에서 몇 표준편차 떨어져 있는가"를 나타냅니다. 예를 들어 Z-score가 1.5라면 "평균보다 1.5 표준편차 높다"는 의미입니다.

이 방법은 정규분포를 가정하므로 이상치가 적을 때 적합합니다. 마지막으로, 표준화된 값들은 이제 같은 척도를 가지므로 가중 평균을 계산하거나, 거리를 측정하거나, 머신러닝 모델에 입력하기에 적합해집니다.

최종적으로 "국어 0.5, 수학 0.75, 영어 0.8"처럼 모든 과목이 0~1 사이 값으로 표현되어 공정한 비교가 가능합니다. 여러분이 이 코드를 사용하면 서로 다른 단위의 변수를 공정하게 비교할 수 있고, 머신러닝 모델 성능을 향상시킬 수 있으며, 거리 기반 알고리즘(K-means, KNN 등)을 정확하게 사용할 수 있고, 변수 간 중요도를 왜곡 없이 평가할 수 있습니다.

실전 팁

💡 Min-Max는 이상치에 민감합니다. 극단값 하나가 전체 범위를 넓혀서 대부분 값이 0 근처로 몰릴 수 있으니, 이상치 제거를 먼저 하세요.

💡 Z-score는 정규분포를 가정합니다. 데이터가 심하게 치우쳐 있다면(skewed) 로그 변환을 먼저 적용한 후 표준화하세요.

💡 테스트 데이터는 훈련 데이터의 통계값으로 변환해야 합니다. 훈련 데이터의 평균/표준편차를 저장해두고 테스트 데이터에 적용하세요.

💡 범주형 변수는 표준화하지 마세요. 숫자처럼 보여도 "지역코드 1, 2, 3"은 크기 순서가 없으므로 표준화가 의미 없습니다.

💡 여러 컬럼을 한 번에 표준화하려면 함수로 만드세요. def normalize(col)을 정의하고 반복문으로 적용하면 코드가 간결해집니다.


10. 데이터 검증하기 - 규칙 위반 찾아내기

시작하며

여러분이 온라인 쇼핑몰 주문 데이터를 분석하는데, 나이가 -5세인 고객이나 주문 금액이 0원인 주문이 있다면 뭔가 잘못되었다는 걸 알 수 있겠죠? 혹은 이메일 주소에 @가 없거나, 전화번호가 8자리인 데이터가 있다면 입력 오류이거나 시스템 버그일 가능성이 큽니다.

이런 비즈니스 규칙 위반은 데이터 품질을 크게 저하시킵니다. 나이는 0120 사이여야 하고, 주문 금액은 양수여야 하고, 이메일은 특정 형식을 따라야 하고, 전화번호는 1011자리여야 한다는 규칙들이 있지만, 시스템 오류나 사용자 실수로 이런 규칙이 깨질 수 있습니다.

규칙을 위반한 데이터를 그대로 두면 분석 결과가 왜곡되고 비즈니스 의사결정에 악영향을 미칩니다. 바로 이럴 때 필요한 것이 데이터 검증입니다.

Polars의 필터링과 조건 표현식을 조합하면 비즈니스 규칙을 체계적으로 검증하고 위반 사항을 찾아낼 수 있습니다.

개요

간단히 말해서, 데이터 검증은 비즈니스 규칙이나 논리적 제약 조건을 정의하고, 데이터가 이 규칙을 만족하는지 확인하는 작업입니다. 실무에서 왜 중요한지 구체적으로 설명하자면, 데이터 품질 보증과 문제 조기 발견을 위해서입니다.

예를 들어, 금융 시스템에서 거래 금액이 음수라면 심각한 버그이고, 의료 시스템에서 환자 나이가 200세라면 입력 오류입니다. 이런 문제를 조기에 발견하지 못하면 잘못된 분석 결과나 시스템 장애로 이어질 수 있습니다.

기존에는 수동으로 조건을 하나씩 체크했다면, Polars는 표현식과 필터를 조합해서 여러 규칙을 한 번에 검증하고 위반 사항을 자동으로 리포트할 수 있습니다. 특히 복잡한 조건도 when-then-otherwise 구문으로 명확하게 표현할 수 있습니다.

데이터 검증의 핵심 특징은 첫째, 범위 검증(최소/최대값 체크), 둘째, 형식 검증(정규표현식 패턴 매칭), 셋째, 논리 검증(여러 컬럼 간 관계 확인)이 가능하다는 점입니다. 이러한 특징들은 데이터 품질을 자동으로 모니터링하고 문제를 신속하게 해결할 수 있게 해줍니다.

코드 예제

import polars as pl

# 샘플 데이터 (규칙 위반 포함)
df = pl.DataFrame({
    "고객ID": [1, 2, 3, 4, 5],
    "이름": ["김철수", "", "박민수", "최지원", "정다은"],
    "나이": [25, 150, -5, 35, 42],
    "이메일": ["kim@email.com", "invalidemail", "park@email.com", "choi@", "jung@email.com"],
    "주문금액": [50000, 30000, -10000, 0, 45000]
})

# 검증 규칙 정의 및 위반 사항 확인
df_validated = df.with_columns([
    (pl.col("나이").is_between(0, 120)).alias("나이_정상"),
    (pl.col("이메일").str.contains(r"^[\w\.-]+@[\w\.-]+\.\w+$")).alias("이메일_정상"),
    (pl.col("주문금액") > 0).alias("금액_정상"),
    (pl.col("이름").str.len_chars() > 0).alias("이름_정상")
])

# 전체 검증 통과 여부
df_validated = df_validated.with_columns(
    (pl.col("나이_정상") & pl.col("이메일_정상") &
     pl.col("금액_정상") & pl.col("이름_정상")).alias("전체_정상")
)

# 규칙 위반 데이터만 추출
invalid_data = df_validated.filter(~pl.col("전체_정상"))
print("규칙 위반 데이터:")
print(invalid_data)

설명

이것이 하는 일: 위 코드는 각 데이터가 정의된 비즈니스 규칙을 만족하는지 자동으로 체크하고 위반 사항을 찾아냅니다. 마치 공항 보안 검색대에서 금지 물품을 찾아내는 것과 같습니다.

첫 번째로, is_between(0, 120) 메서드는 나이가 0세 이상 120세 이하인지 확인합니다. 고객ID 2번(150세)과 3번(-5세)은 False가 되어 규칙 위반으로 표시됩니다.

왜 이 범위를 사용하는지 설명하자면, 인간의 수명을 고려할 때 합리적인 범위이고, 음수는 논리적으로 불가능하기 때문입니다. 그 다음으로, str.contains(r"^[\w\.-]+@[\w\.-]+\.\w+$") 정규표현식은 이메일 형식을 검증합니다.

이 패턴은 "문자+숫자+특수문자 @ 도메인 . 최상위도메인" 형식을 요구합니다.

"invalidemail"(@ 없음)과 "choi@"(도메인 없음)은 False가 되어 형식 오류로 표시됩니다. 내부적으로 Polars는 정규표현식 엔진을 사용해 각 값을 매칭합니다.

세 번째로, pl.col("주문금액") > 0 조건은 주문 금액이 양수인지 확인합니다. -10000원과 0원은 규칙 위반입니다.

0원 주문은 비즈니스 로직에 따라 허용할 수도 있지만, 여기서는 프로모션이나 무료 샘플 제외하고 모든 주문은 양수여야 한다고 가정합니다. 네 번째로, 모든 검증 결과를 AND 연산(&)으로 결합하여 "전체_정상" 컬럼을 만듭니다.

모든 규칙을 통과해야 True가 되므로, 하나라도 위반하면 False입니다. 마지막으로 filter(~pl.col("전체_정상"))로 위반 데이터만 추출해서 데이터 품질 리포트로 만들 수 있습니다.

여러분이 이 코드를 사용하면 데이터 입력 오류를 자동으로 발견할 수 있고, 시스템 버그를 조기에 감지할 수 있으며, 데이터 품질 리포트를 자동 생성할 수 있고, 분석 전에 불량 데이터를 걸러낼 수 있습니다.

실전 팁

💡 검증 규칙을 문서화하세요. 각 규칙의 의미와 근거를 주석이나 별도 문서로 남기면 유지보수가 쉽습니다.

💡 규칙 위반 통계를 대시보드로 만드세요. df_validated.select([pl.col("나이_정상").mean(), ...])로 각 규칙의 통과율을 계산할 수 있습니다.

💡 심각도를 구분하세요. 필수 규칙(나이 음수)과 권장 규칙(이메일 형식)을 분리해서 처리 방법을 다르게 할 수 있습니다.

💡 검증을 데이터 파이프라인에 통합하세요. ETL 과정에서 자동으로 검증하고 위반 데이터는 별도 테이블로 분리하면 운영이 편리합니다.

💡 복잡한 규칙은 함수로 분리하세요. def validate_customer(df)처럼 재사용 가능한 함수를 만들면 코드가 깔끔해집니다.


#Python#Polars#DataCleaning#DataAnalysis#DataProcessing#데이터분석,Python,Polars

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.