이미지 로딩 중...

탐색적 데이터 분석(EDA) 완벽 가이드 - 슬라이드 1/9
A

AI Generated

2025. 11. 15. · 3 Views

탐색적 데이터 분석(EDA) 완벽 가이드

데이터의 숨겨진 인사이트를 발견하는 탐색적 데이터 분석(EDA)의 핵심 개념과 실전 기법을 배워봅니다. Python과 Polars를 활용하여 데이터의 구조를 파악하고, 패턴을 발견하며, 이상치를 탐지하는 방법을 단계별로 익힐 수 있습니다.


목차

  1. 데이터_기본_구조_파악
  2. 기술_통계량_분석
  3. 결측치_탐지_및_처리
  4. 데이터_타입_변환
  5. 이상치_탐지
  6. 범주형_데이터_분석
  7. 상관관계_분석
  8. 데이터_그룹별_집계

1. 데이터_기본_구조_파악

시작하며

여러분이 새로운 데이터셋을 받았을 때 어디서부터 시작해야 할지 막막했던 경험 있나요? 수백 개의 컬럼과 수천 개의 행이 담긴 CSV 파일을 열었을 때, 무엇부터 확인해야 할지 모르겠더라구요.

이런 문제는 실제 데이터 분석 프로젝트에서 가장 먼저 마주하는 난관입니다. 데이터의 전체적인 구조를 파악하지 못하면, 잘못된 분석 방향을 설정하거나 중요한 정보를 놓칠 수 있습니다.

바로 이럴 때 필요한 것이 데이터 기본 구조 파악입니다. Polars를 사용하면 빠르고 효율적으로 데이터의 형태, 크기, 컬럼 타입 등을 한눈에 확인할 수 있습니다.

개요

간단히 말해서, 데이터 기본 구조 파악은 데이터셋의 '첫인상'을 확인하는 과정입니다. 마치 새 책을 받았을 때 목차와 페이지 수를 먼저 확인하는 것과 같습니다.

데이터 분석의 첫 단계에서 이 과정이 필수적인 이유는, 전체 데이터의 규모와 구조를 이해해야 적절한 분석 전략을 세울 수 있기 때문입니다. 예를 들어, 100만 행의 고객 거래 데이터를 분석할 때, 어떤 컬럼이 있고 각 컬럼의 데이터 타입이 무엇인지 알아야 메모리 관리와 처리 방법을 결정할 수 있습니다.

기존 Pandas에서는 .info(), .head() 메서드를 사용했다면, Polars에서는 더 빠른 성능으로 .schema, .head(), .describe() 등을 활용할 수 있습니다. Polars의 핵심 특징은 Lazy Evaluation(지연 평가), 병렬 처리, 메모리 효율성입니다.

이러한 특징들이 대용량 데이터 처리 시 Pandas보다 10배 이상 빠른 성능을 제공하여, 실무에서 시간을 크게 절약할 수 있습니다.

코드 예제

import polars as pl

# CSV 파일 읽기
df = pl.read_csv("sales_data.csv")

# 데이터프레임의 기본 정보 확인
print(f"행 개수: {df.height}, 열 개수: {df.width}")

# 컬럼 이름과 데이터 타입 확인
print(df.schema)

# 첫 5개 행 미리보기
print(df.head())

# 전체 데이터 요약 정보
print(df.describe())

설명

이것이 하는 일: 이 코드는 CSV 파일을 읽어와서 데이터의 전체적인 모습을 다양한 각도에서 확인합니다. 마치 건물의 설계도를 보듯이, 데이터의 구조를 빠르게 파악할 수 있습니다.

첫 번째로, pl.read_csv()는 CSV 파일을 Polars DataFrame으로 불러옵니다. 이 과정에서 Polars는 자동으로 데이터 타입을 추론하며, Pandas보다 훨씬 빠른 속도로 대용량 파일도 처리할 수 있습니다.

왜 빠를까요? Polars는 Rust로 작성되어 메모리를 효율적으로 사용하고, 멀티스레드를 기본으로 지원하기 때문입니다.

두 번째로, df.heightdf.width를 통해 데이터의 행과 열 개수를 확인합니다. 이는 데이터의 규모를 파악하는 가장 기본적인 단계입니다.

또한 df.schema는 각 컬럼의 이름과 데이터 타입을 딕셔너리 형태로 반환하여, 어떤 컬럼이 숫자형인지 문자형인지 즉시 알 수 있습니다. 세 번째로, df.head()는 처음 5개 행을 출력하여 실제 데이터가 어떻게 생겼는지 확인할 수 있게 해줍니다.

df.describe()는 각 숫자형 컬럼의 평균, 표준편차, 최솟값, 최댓값 등 기술 통계량을 한 번에 보여주어, 데이터의 분포를 빠르게 이해할 수 있습니다. 여러분이 이 코드를 사용하면 몇 초 만에 데이터의 전체 그림을 그릴 수 있습니다.

수백만 행의 데이터라도 Polars는 빠르게 처리하여, 컬럼별 데이터 타입 확인, 크기 파악, 샘플 데이터 미리보기를 즉시 제공합니다. 이를 통해 다음 분석 단계를 효율적으로 계획할 수 있습니다.

실전 팁

💡 대용량 CSV 파일(1GB 이상)을 다룰 때는 pl.scan_csv()로 Lazy 모드를 사용하세요. 메모리에 전체를 로드하지 않고 필요한 부분만 처리하여 메모리를 절약할 수 있습니다.

💡 df.sample(n=100)을 사용하면 랜덤으로 100개 행을 추출하여 데이터의 다양한 부분을 확인할 수 있습니다. head()만 보면 초반부 패턴만 보이는 한계를 극복할 수 있습니다.

💡 df.null_count()로 각 컬럼의 결측치 개수를 빠르게 확인하세요. 데이터 품질을 초기에 파악하여 전처리 계획을 세울 수 있습니다.

💡 df.estimated_size('mb')를 사용하면 DataFrame이 차지하는 메모리 크기를 확인할 수 있어, 대용량 데이터 처리 시 메모리 관리에 유용합니다.

💡 컬럼 이름이 너무 많아 보기 힘들 때는 df.columns로 리스트 형태로 확인하거나, df.select(pl.col("^.*price.*$"))처럼 정규식으로 특정 패턴의 컬럼만 필터링하여 볼 수 있습니다.


2. 기술_통계량_분석

시작하며

여러분이 수천 개의 제품 가격 데이터를 분석해야 할 때, 어떻게 전체적인 가격 수준을 파악하시나요? 하나하나 눈으로 확인할 수는 없고, 막연히 평균만 보기엔 뭔가 부족하다는 느낌이 들 때가 있습니다.

이런 문제는 데이터의 중심 경향과 분산을 제대로 이해하지 못해서 발생합니다. 평균만 보면 극단값에 영향을 받아 왜곡된 정보를 얻을 수 있고, 데이터가 얼마나 흩어져 있는지도 알 수 없습니다.

바로 이럴 때 필요한 것이 기술 통계량 분석입니다. 평균, 중앙값, 표준편차, 사분위수 등을 통해 데이터의 전체적인 분포와 특성을 숫자로 요약하여 한눈에 파악할 수 있습니다.

개요

간단히 말해서, 기술 통계량은 데이터를 대표하는 핵심 숫자들의 집합입니다. 마치 사람의 키, 몸무게, 혈압으로 건강 상태를 파악하듯이, 데이터의 '건강 상태'를 진단하는 지표입니다.

데이터 분석에서 기술 통계량이 필수적인 이유는, 수천~수백만 개의 데이터 포인트를 몇 개의 의미 있는 숫자로 압축하여 전체적인 경향을 빠르게 이해할 수 있기 때문입니다. 예를 들어, 월별 매출 데이터를 분석할 때 평균 매출, 중앙값, 표준편차를 보면 매출의 안정성과 변동성을 즉시 파악할 수 있습니다.

기존에는 Excel이나 Pandas의 .describe()로 기본 통계량을 확인했다면, Polars에서는 더 빠른 속도로 .mean(), .median(), .std() 등 개별 통계량을 계산하거나, 한 번에 여러 통계량을 집계할 수 있습니다. Polars의 핵심 특징은 표현식(Expression) 기반 문법으로, 여러 통계량을 체이닝하여 한 번에 계산할 수 있다는 점입니다.

이를 통해 코드가 간결해지고, 내부적으로 병렬 처리되어 대용량 데이터에서도 빠른 성능을 발휘합니다.

코드 예제

import polars as pl

# 샘플 데이터 로드
df = pl.read_csv("product_sales.csv")

# 여러 기술 통계량을 한 번에 계산
stats = df.select([
    pl.col("price").mean().alias("평균_가격"),
    pl.col("price").median().alias("중앙값_가격"),
    pl.col("price").std().alias("표준편차"),
    pl.col("price").min().alias("최소값"),
    pl.col("price").max().alias("최대값"),
    pl.col("price").quantile(0.25).alias("1사분위수"),
    pl.col("price").quantile(0.75).alias("3사분위수")
])

print(stats)

설명

이것이 하는 일: 이 코드는 가격 데이터의 다양한 통계적 특성을 한 번에 계산하여, 데이터의 중심 위치, 분산 정도, 범위를 종합적으로 보여줍니다. 첫 번째로, pl.col("price").mean()은 가격의 평균을 계산합니다.

하지만 평균만으로는 부족합니다. 예를 들어, 대부분 제품이 1만원대인데 몇 개가 100만원이면 평균이 크게 왜곡되기 때문입니다.

그래서 median()으로 중앙값도 함께 확인하여, 극단값의 영향을 받지 않는 중심 경향을 파악합니다. 두 번째로, std()는 표준편차를 계산하여 데이터가 평균으로부터 얼마나 흩어져 있는지 측정합니다.

표준편차가 크면 데이터가 널리 퍼져 있고, 작으면 평균 근처에 모여 있다는 의미입니다. 이는 데이터의 변동성과 예측 가능성을 판단하는 핵심 지표입니다.

세 번째로, min()max()로 데이터의 범위를 확인하고, quantile(0.25)quantile(0.75)로 1사분위수와 3사분위수를 계산합니다. 사분위수는 데이터를 정렬했을 때 25%, 75% 지점의 값으로, 이상치를 제외한 데이터의 실제 분포를 이해하는 데 매우 유용합니다.

IQR(3사분위수 - 1사분위수)을 계산하면 이상치 탐지에도 활용할 수 있습니다. 마지막으로, .alias()를 사용하여 각 통계량에 한글 이름을 부여하여 결과를 더 읽기 쉽게 만듭니다.

Polars는 이 모든 계산을 병렬로 수행하여, 수백만 행의 데이터도 몇 초 만에 처리합니다. 여러분이 이 코드를 사용하면 단 몇 줄로 데이터의 핵심 특성을 한눈에 파악할 수 있습니다.

평균과 중앙값의 차이로 데이터의 왜곡도를 확인하고, 표준편차로 변동성을 파악하며, 사분위수로 실제 데이터 분포를 이해할 수 있습니다.

실전 팁

💡 평균과 중앙값의 차이가 크다면 데이터에 극단값이나 왜곡이 있다는 신호입니다. 이럴 때는 중앙값을 더 신뢰하세요.

💡 표준편차가 평균의 50%를 넘으면 데이터 변동성이 매우 크다는 의미입니다. 이런 경우 로그 변환이나 정규화를 고려하세요.

💡 여러 컬럼의 통계량을 한 번에 계산하려면 df.select(pl.all().mean())처럼 pl.all()을 사용하여 모든 숫자형 컬럼에 적용할 수 있습니다.

💡 df.select(pl.col("price").describe())를 사용하면 한 컬럼에 대한 모든 기본 통계량을 한 번에 볼 수 있어 편리합니다.

💡 사분위수 범위(IQR)를 활용한 이상치 탐지: Q1 - 1.5*IQR 미만 또는 Q3 + 1.5*IQR 초과 값을 이상치로 판단하는 것이 통계적 표준 방법입니다.


3. 결측치_탐지_및_처리

시작하며

여러분이 고객 설문조사 데이터를 분석하려는데, 일부 응답자가 특정 질문을 건너뛴 경우를 발견한 적 있나요? 이런 빈 값들을 그대로 두고 분석하면 통계량이 왜곡되거나 모델 학습이 실패할 수 있습니다.

이런 문제는 실제 데이터에서 매우 흔하게 발생합니다. 센서 오류, 입력 누락, 시스템 에러 등 다양한 이유로 결측치가 생기며, 이를 제대로 처리하지 않으면 분석 결과의 신뢰성이 떨어집니다.

바로 이럴 때 필요한 것이 결측치 탐지 및 처리입니다. Polars를 사용하면 어떤 컬럼에 얼마나 많은 결측치가 있는지 빠르게 확인하고, 적절한 방법으로 처리할 수 있습니다.

개요

간단히 말해서, 결측치 처리는 데이터의 빈 공간을 찾아내고 적절한 값으로 채우거나 제거하는 과정입니다. 마치 퍼즐에서 빠진 조각을 찾아 맞추는 것과 비슷합니다.

데이터 분석에서 결측치 처리가 중요한 이유는, 대부분의 통계 기법과 머신러닝 알고리즘이 결측치를 허용하지 않기 때문입니다. 예를 들어, 고객의 나이 정보가 30% 누락된 상태에서 나이별 구매 패턴을 분석하면, 편향된 결과를 얻을 수 있습니다.

기존 Pandas에서는 .isnull(), .fillna(), .dropna()를 사용했다면, Polars에서는 .null_count(), .fill_null(), .drop_nulls() 등으로 더 빠르고 메모리 효율적으로 처리할 수 있습니다. Polars의 핵심 특징은 결측치를 null로 명확히 표현하고, 다양한 전략(평균, 중앙값, 전방/후방 채우기 등)으로 유연하게 처리할 수 있다는 점입니다.

또한 체이닝 문법으로 여러 컬럼을 한 번에 처리하여 코드를 간결하게 만들 수 있습니다.

코드 예제

import polars as pl

# 데이터 로드
df = pl.read_csv("customer_survey.csv")

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

# 결측치 비율 계산
null_ratio = (df.null_count() / df.height * 100).select([
    pl.all().suffix("_결측률(%)")
])
print(null_ratio)

# 숫자형 컬럼은 평균으로, 문자형 컬럼은 'Unknown'으로 채우기
df_filled = df.with_columns([
    pl.col("age").fill_null(pl.col("age").mean()),
    pl.col("income").fill_null(pl.col("income").median()),
    pl.col("region").fill_null("Unknown")
])

# 결측치가 있는 행 제거 (특정 컬럼 기준)
df_dropped = df.drop_nulls(subset=["customer_id", "purchase_date"])

설명

이것이 하는 일: 이 코드는 데이터셋에서 결측치를 찾아내고, 컬럼의 특성에 맞는 적절한 방법으로 처리하여 완전한 데이터셋을 만듭니다. 첫 번째로, df.null_count()는 각 컬럼별로 결측치(null 또는 None)의 개수를 세어 줍니다.

이를 통해 어떤 컬럼에 문제가 많은지 즉시 파악할 수 있습니다. 추가로 결측치 비율을 계산하면, 예를 들어 80% 이상 결측치인 컬럼은 아예 분석에서 제외하는 결정을 내릴 수 있습니다.

두 번째로, .fill_null()을 사용하여 결측치를 채웁니다. 여기서 중요한 것은 컬럼의 특성에 맞는 전략을 선택하는 것입니다.

age 컬럼은 평균으로 채우고, income은 극단값의 영향을 피하기 위해 중앙값으로 채웁니다. 범주형 데이터인 region은 'Unknown'이라는 명시적인 값으로 채워 결측치였음을 표시합니다.

세 번째로, .drop_nulls()로 결측치가 있는 행을 제거합니다. subset 매개변수를 사용하면 특정 컬럼만 기준으로 삼을 수 있습니다.

예를 들어, customer_idpurchase_date처럼 핵심 식별 정보가 없는 행은 분석 가치가 없으므로 제거하는 것이 합리적입니다. Polars는 이러한 결측치 처리를 메모리 효율적으로 수행합니다.

원본 데이터를 복사하지 않고 필요한 부분만 수정하여, 대용량 데이터에서도 빠르게 처리할 수 있습니다. 여러분이 이 코드를 사용하면 결측치 문제를 체계적으로 해결할 수 있습니다.

결측치의 위치와 정도를 파악하고, 데이터의 특성과 분석 목적에 맞는 처리 방법을 선택하여, 신뢰할 수 있는 분석 결과를 얻을 수 있습니다.

실전 팁

💡 결측치가 20% 이상인 컬럼은 채우는 것보다 제거하는 것을 고려하세요. 너무 많이 채우면 원본 데이터의 의미가 왜곡될 수 있습니다.

💡 시계열 데이터의 결측치는 .fill_null(strategy="forward")로 이전 값으로 채우거나, .interpolate()로 선형 보간하는 것이 효과적입니다.

💡 결측치 처리 전후로 통계량을 비교하여 데이터 분포가 크게 바뀌지 않았는지 확인하세요. 평균이나 표준편차가 크게 변했다면 다른 전략을 시도해야 합니다.

💡 머신러닝 모델링 시 결측치 여부 자체가 중요한 정보일 수 있습니다. 결측치를 채우기 전에 df.with_columns(pl.col("age").is_null().alias("age_was_missing"))로 결측 여부를 별도 컬럼으로 저장하세요.

💡 범주형 데이터의 결측치를 최빈값으로 채우려면 pl.col("category").fill_null(pl.col("category").mode().first())를 사용할 수 있습니다.


4. 데이터_타입_변환

시작하며

여러분이 날짜 컬럼으로 월별 집계를 하려는데, 날짜가 문자열로 저장되어 있어서 제대로 정렬이나 계산이 안 된 경험 있나요? "2024-01-15"가 날짜가 아니라 그냥 텍스트로 인식되면 분석이 불가능합니다.

이런 문제는 데이터를 잘못된 타입으로 저장하거나 읽어올 때 자주 발생합니다. 숫자가 문자열로 저장되면 수학 연산이 불가능하고, 날짜가 텍스트면 시간 순서를 제대로 파악할 수 없습니다.

바로 이럴 때 필요한 것이 데이터 타입 변환입니다. Polars에서 컬럼의 데이터 타입을 올바르게 변환하면, 의도한 분석과 계산을 정확하게 수행할 수 있습니다.

개요

간단히 말해서, 데이터 타입 변환은 컬럼의 데이터를 올바른 형식으로 바꾸는 과정입니다. 마치 영어로 쓰인 숫자를 실제 숫자로 번역하는 것과 같습니다.

데이터 타입이 중요한 이유는, 같은 값이라도 타입에 따라 사용할 수 있는 연산과 함수가 완전히 달라지기 때문입니다. 예를 들어, "100"이라는 문자열에는 수학 연산을 할 수 없지만, 100이라는 정수로 변환하면 덧셈, 곱셈, 평균 계산 등이 가능합니다.

날짜 타입으로 변환하면 시간 차이 계산, 월별 그룹화 등 시계열 분석이 가능해집니다. 기존 Pandas에서는 .astype(), pd.to_datetime() 등을 사용했다면, Polars에서는 .cast() 메서드로 통일되어 있으며, 더 빠르고 안전하게 타입을 변환할 수 있습니다.

Polars의 핵심 특징은 엄격한 타입 시스템과 명시적인 변환입니다. 암묵적 변환으로 인한 예기치 않은 오류를 방지하고, 타입 변환 실패 시 에러를 명확하게 알려주어 데이터 품질을 유지할 수 있습니다.

코드 예제

import polars as pl
from datetime import datetime

# 샘플 데이터 로드
df = pl.read_csv("transactions.csv")

# 데이터 타입 확인
print(df.schema)

# 다양한 타입 변환
df_converted = df.with_columns([
    # 문자열을 정수로 변환
    pl.col("quantity").cast(pl.Int64),

    # 문자열을 실수로 변환
    pl.col("price").cast(pl.Float64),

    # 문자열을 날짜로 변환
    pl.col("purchase_date").str.strptime(pl.Date, "%Y-%m-%d"),

    # 문자열을 날짜시간으로 변환
    pl.col("timestamp").str.strptime(pl.Datetime, "%Y-%m-%d %H:%M:%S"),

    # 범주형으로 변환 (메모리 절약)
    pl.col("category").cast(pl.Categorical)
])

print(df_converted.schema)

설명

이것이 하는 일: 이 코드는 CSV 파일에서 읽어온 데이터의 타입을 각 컬럼의 의미에 맞게 적절히 변환하여, 후속 분석에서 올바른 연산을 할 수 있도록 준비합니다. 첫 번째로, .cast()는 기본적인 타입 변환에 사용됩니다.

pl.Int64는 64비트 정수, pl.Float64는 64비트 부동소수점 숫자를 의미합니다. CSV에서 읽어온 숫자는 기본적으로 문자열일 수 있으므로, 명시적으로 숫자 타입으로 변환해야 수학 연산, 집계, 정렬이 제대로 작동합니다.

두 번째로, .str.strptime()은 문자열 형태의 날짜를 실제 날짜/날짜시간 타입으로 파싱합니다. "%Y-%m-%d" 같은 포맷 문자열을 지정하여 날짜 형식을 정확히 알려줘야 합니다.

날짜 타입으로 변환하면 .dt.year(), .dt.month() 같은 날짜 전용 메서드를 사용할 수 있고, 날짜 간 차이 계산도 가능해집니다. 세 번째로, pl.Categorical 타입은 반복되는 문자열 값(예: 카테고리, 지역명 등)을 효율적으로 저장합니다.

내부적으로 정수 코드로 변환하여 메모리를 크게 절약하고, 그룹화 연산도 빨라집니다. 예를 들어, "Electronics"라는 문자열이 100만 번 반복되면 엄청난 메모리를 차지하지만, Categorical로 변환하면 한 번만 저장하고 인덱스로 참조합니다.

Polars의 타입 시스템은 매우 엄격하여, 변환할 수 없는 값이 있으면 에러를 발생시킵니다. 이는 잘못된 데이터를 조기에 발견하여 데이터 품질을 보장하는 장점이 있습니다.

여러분이 이 코드를 사용하면 각 컬럼을 올바른 타입으로 변환하여, 의미 있는 분석을 수행할 수 있습니다. 숫자 타입으로 평균과 합계를 계산하고, 날짜 타입으로 시계열 분석을 하며, 범주형으로 메모리를 절약할 수 있습니다.

실전 팁

💡 CSV 읽을 때 미리 타입을 지정하려면 pl.read_csv("file.csv", dtypes={"price": pl.Float64, "date": pl.Date})처럼 dtypes 매개변수를 사용하세요. 나중에 변환하는 것보다 빠릅니다.

💡 타입 변환 실패를 무시하고 null로 만들려면 pl.col("price").cast(pl.Float64, strict=False)처럼 strict=False를 사용하세요. 하지만 데이터 품질 확인을 위해 기본값(strict=True)을 권장합니다.

💡 날짜 파싱 시 여러 포맷이 섞여 있다면, pl.coalesce()로 여러 파싱 시도를 결합하거나, 정규식으로 먼저 정리한 후 변환하세요.

💡 반복되는 값이 많은 문자열 컬럼(카테고리, 코드 등)은 항상 Categorical 타입으로 변환하여 메모리를 50% 이상 절약할 수 있습니다.

💡 타입 변환 후 df.schema로 변환이 제대로 되었는지 확인하는 습관을 들이세요. 예상과 다른 타입이면 후속 분석에서 오류가 발생할 수 있습니다.


5. 이상치_탐지

시작하며

여러분이 매출 데이터를 분석하는데, 평소 100만원대 매출이 나오다가 갑자기 1억원이 찍힌 날이 있다면 어떻게 하시겠어요? 이게 진짜 대박 매출인지, 아니면 데이터 입력 오류인지 확인해야 합니다.

이런 문제는 데이터 수집 과정에서 센서 오류, 입력 실수, 시스템 버그 등으로 인해 발생합니다. 이상치를 제대로 처리하지 않으면 평균이 왜곡되고, 머신러닝 모델의 성능이 크게 떨어질 수 있습니다.

바로 이럴 때 필요한 것이 이상치 탐지입니다. 통계적 방법을 사용하여 정상 범위를 벗어난 값들을 찾아내고, 제거하거나 수정하여 데이터 품질을 높일 수 있습니다.

개요

간단히 말해서, 이상치(Outlier)는 다른 데이터들과 현저하게 다른 값을 의미합니다. 마치 키가 2.5m인 사람처럼, 정상 범위에서 크게 벗어난 관측값입니다.

이상치 탐지가 중요한 이유는, 소수의 극단값이 전체 분석 결과를 크게 왜곡시킬 수 있기 때문입니다. 예를 들어, 직원 10명의 연봉을 분석할 때 9명은 3000-5000만원이고 CEO 1명이 10억이라면, 평균이 1억 5000만원으로 계산되어 실제 직원 연봉 수준을 전혀 반영하지 못합니다.

기존에는 수동으로 범위를 정하거나 그래프로 육안 확인했다면, 통계적 방법인 IQR(Interquartile Range)이나 Z-Score를 사용하여 객관적이고 자동화된 이상치 탐지를 할 수 있습니다. Polars의 핵심 특징은 벡터화된 연산으로 수백만 행의 데이터에서도 이상치를 빠르게 탐지할 수 있다는 점입니다.

표현식을 사용하여 복잡한 조건도 간결하게 작성할 수 있습니다.

코드 예제

import polars as pl

# 데이터 로드
df = pl.read_csv("sales_data.csv")

# IQR 방법으로 이상치 탐지
Q1 = df.select(pl.col("sales").quantile(0.25)).item()
Q3 = df.select(pl.col("sales").quantile(0.75)).item()
IQR = Q3 - Q1

# 이상치 경계 계산
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR

# 이상치 표시 컬럼 추가
df_with_outliers = df.with_columns([
    ((pl.col("sales") < lower_bound) | (pl.col("sales") > upper_bound))
    .alias("is_outlier")
])

# 이상치만 필터링
outliers = df_with_outliers.filter(pl.col("is_outlier"))
print(f"이상치 개수: {outliers.height}")

# 이상치 제거
df_clean = df_with_outliers.filter(~pl.col("is_outlier"))

설명

이것이 하는 일: 이 코드는 사분위수 범위(IQR) 방법을 사용하여 통계적으로 이상한 값들을 자동으로 찾아내고 표시합니다. 첫 번째로, quantile(0.25)quantile(0.75)로 1사분위수(Q1)와 3사분위수(Q3)를 계산합니다.

Q1은 데이터의 하위 25% 지점, Q3는 상위 25% 지점의 값입니다. 이 두 값의 차이가 IQR(사분위수 범위)로, 데이터의 중간 50%가 퍼져 있는 범위를 나타냅니다.

두 번째로, 통계학에서 널리 인정받는 "1.5 × IQR" 규칙을 적용합니다. Q1에서 1.5×IQR을 뺀 값보다 작거나, Q3에 1.5×IQR을 더한 값보다 크면 이상치로 판단합니다.

이 기준은 정규분포에서 약 99.3%의 데이터를 정상으로 간주하는 합리적인 수준입니다. 세 번째로, with_columns()를 사용하여 각 행이 이상치인지 여부를 나타내는 is_outlier 컬럼을 추가합니다.

|는 OR 연산자로, 하한선보다 작거나 상한선보다 크면 True를 반환합니다. 이렇게 하면 원본 데이터를 유지하면서 이상치 정보를 추가할 수 있습니다.

마지막으로, filter() 함수로 이상치만 추출하여 확인하거나, ~ 연산자(NOT)를 사용하여 이상치를 제외한 깨끗한 데이터셋을 만듭니다. 이상치를 무조건 제거하기보다는, 먼저 확인하여 진짜 오류인지 중요한 이벤트인지 판단하는 것이 중요합니다.

여러분이 이 코드를 사용하면 객관적인 통계 기준으로 이상치를 자동 탐지할 수 있습니다. 수동으로 하나하나 확인하는 것보다 빠르고 정확하며, 수백만 행의 데이터에서도 몇 초 만에 처리됩니다.

실전 팁

💡 Z-Score 방법도 유용합니다: (pl.col("sales") - pl.col("sales").mean()) / pl.col("sales").std().abs() > 3`로 평균에서 3 표준편차 이상 벗어난 값을 이상치로 탐지할 수 있습니다.

💡 이상치를 제거하기 전에 반드시 확인하세요. 진짜 중요한 이벤트(블랙프라이데이 매출 급증 등)를 잃을 수 있습니다.

💡 도메인 지식을 활용한 이상치 정의: 나이가 음수이거나 150세 이상, 가격이 음수인 경우는 통계와 무관하게 명백한 오류입니다.

💡 이상치를 제거하는 대신 cap/floor 처리도 고려하세요: pl.col("sales").clip(lower_bound, upper_bound)로 극단값을 경계값으로 제한하여 정보 손실을 줄일 수 있습니다.

💡 여러 컬럼에 이상치 탐지를 적용할 때는 pl.all().exclude(["id", "date"])로 숫자형 컬럼만 선택하여 일괄 처리할 수 있습니다.


6. 범주형_데이터_분석

시작하며

여러분이 전국 매장의 판매 데이터를 분석할 때, 어느 지역이 가장 많이 팔리는지 알고 싶다면 어떻게 하시겠어요? 서울, 부산, 대구 같은 지역명은 숫자가 아니라 범주(카테고리)이기 때문에 평균이나 합계를 계산할 수 없습니다.

이런 문제는 범주형 데이터(Categorical Data)를 다룰 때 자주 발생합니다. 성별, 직업, 제품 카테고리, 등급 같은 데이터는 각 범주별로 빈도를 세거나 비율을 계산하는 방식으로 분석해야 합니다.

바로 이럴 때 필요한 것이 범주형 데이터 분석입니다. Polars의 그룹화와 집계 기능을 사용하면 각 범주별 분포와 패턴을 빠르게 파악할 수 있습니다.

개요

간단히 말해서, 범주형 데이터 분석은 질적인(숫자가 아닌) 데이터의 분포와 빈도를 파악하는 과정입니다. 마치 설문조사에서 각 선택지를 선택한 사람이 몇 명인지 세는 것과 같습니다.

범주형 데이터 분석이 중요한 이유는, 실무 데이터의 상당 부분이 범주형이기 때문입니다. 예를 들어, 고객 세그먼트별 구매 패턴, 제품 카테고리별 매출, 지역별 선호도 등을 분석할 때 각 범주의 빈도와 비율을 이해해야 비즈니스 인사이트를 얻을 수 있습니다.

기존 Pandas에서는 .value_counts(), .groupby().size()를 사용했다면, Polars에서는 .group_by().count(), .value_counts() 등으로 더 빠르고 메모리 효율적으로 처리할 수 있습니다. Polars의 핵심 특징은 group_by 연산이 매우 빠르고, 여러 집계를 동시에 수행할 수 있다는 점입니다.

또한 Categorical 타입을 사용하면 메모리를 크게 절약하면서도 빠른 그룹화가 가능합니다.

코드 예제

import polars as pl

# 데이터 로드
df = pl.read_csv("product_sales.csv")

# 범주별 빈도수 계산 (간단한 방법)
category_counts = df.group_by("category").count().sort("count", descending=True)
print(category_counts)

# 범주별 비율 계산
category_ratio = df.group_by("category").agg([
    pl.count().alias("count"),
    (pl.count() / df.height * 100).alias("percentage")
]).sort("percentage", descending=True)
print(category_ratio)

# 범주별 다양한 통계량 계산
category_stats = df.group_by("category").agg([
    pl.count().alias("판매_건수"),
    pl.col("sales").sum().alias("총_매출"),
    pl.col("sales").mean().alias("평균_매출"),
    pl.col("customer_id").n_unique().alias("고유_고객수")
])
print(category_stats)

설명

이것이 하는 일: 이 코드는 범주형 컬럼(예: 제품 카테고리)을 기준으로 데이터를 그룹화하고, 각 그룹의 빈도와 다양한 통계량을 계산합니다. 첫 번째로, .group_by("category").count()는 각 카테고리별로 몇 개의 행이 있는지 세어줍니다.

이는 가장 기본적인 범주형 데이터 분석으로, 어떤 카테고리가 가장 많이 나타나는지 파악할 수 있습니다. .sort("count", descending=True)로 내림차순 정렬하면 상위 범주를 쉽게 확인할 수 있습니다.

두 번째로, 비율을 계산하여 전체 데이터에서 각 범주가 차지하는 비중을 파악합니다. pl.count() / df.height * 100으로 백분율을 계산하면, 예를 들어 "Electronics" 카테고리가 전체의 35%를 차지한다는 식으로 해석할 수 있습니다.

이는 보고서나 시각화에서 매우 유용한 정보입니다. 세 번째로, .agg() 함수를 사용하여 여러 집계를 동시에 수행합니다.

단순 개수뿐만 아니라 각 카테고리의 총 매출, 평균 매출, 고유 고객 수 등을 한 번에 계산할 수 있습니다. 이를 통해 "어떤 카테고리가 가장 많이 팔렸나"와 "어떤 카테고리가 가장 수익성이 높나"를 동시에 파악할 수 있습니다.

Polars는 이러한 그룹화 연산을 병렬로 처리하여, 수백만 행의 데이터도 몇 초 만에 집계합니다. 또한 메모리를 효율적으로 사용하여 대용량 데이터에서도 안정적으로 작동합니다.

여러분이 이 코드를 사용하면 범주형 데이터의 분포와 특성을 종합적으로 이해할 수 있습니다. 단순 빈도뿐만 아니라 각 범주의 비즈니스 임팩트(매출, 고객 수 등)를 정량적으로 파악하여 의사결정에 활용할 수 있습니다.

실전 팁

💡 상위 N개 범주만 보려면 .head(10)을 사용하세요. 수백 개의 범주가 있을 때 상위 10개만 보면 전체의 80%를 차지하는 경우가 많습니다(파레토 법칙).

💡 여러 범주를 동시에 그룹화하려면 df.group_by(["category", "region"])처럼 리스트로 전달하세요. 카테고리와 지역의 조합별 분석이 가능합니다.

💡 범주가 너무 많아 보기 힘들 때는, 빈도가 낮은 범주들을 "기타"로 묶으세요: pl.when(pl.col("count") < 100).then(pl.lit("기타")).otherwise(pl.col("category"))

💡 .value_counts().group_by().count()의 간단한 버전으로, 한 컬럼의 빈도만 빠르게 확인할 때 편리합니다.

💡 범주의 순서가 중요하다면(예: 소/중/대), Categorical 타입에 순서를 지정하거나, 커스텀 정렬을 사용하세요: .sort("category", descending=False, nulls_last=True)


7. 상관관계_분석

시작하며

여러분이 광고비와 매출 데이터를 보면서 "광고비를 늘리면 정말 매출이 오를까?"라는 의문을 가진 적 있나요? 두 변수 간의 관계를 감으로만 판단하면 잘못된 투자 결정을 내릴 수 있습니다.

이런 문제는 변수 간의 관계를 정량적으로 측정하지 않아서 발생합니다. 우연히 같이 움직이는 것처럼 보일 수도 있고, 실제로는 강한 관계가 있는데 놓칠 수도 있습니다.

바로 이럴 때 필요한 것이 상관관계 분석입니다. 상관계수를 계산하여 두 변수가 얼마나 함께 움직이는지 수치로 확인할 수 있습니다.

개요

간단히 말해서, 상관관계 분석은 두 변수 간의 선형적 관계 강도를 측정하는 방법입니다. 마치 두 친구가 얼마나 비슷하게 행동하는지 측정하는 것과 같습니다.

상관관계 분석이 중요한 이유는, 변수 간의 관계를 이해해야 예측 모델을 만들거나 비즈니스 의사결정을 할 수 있기 때문입니다. 예를 들어, 기온과 아이스크림 판매량의 상관관계가 0.9라면, 날씨 예보를 보고 재고를 미리 준비할 수 있습니다.

반대로 상관관계가 없다면 다른 요인을 찾아야 합니다. 기존에는 Excel이나 Pandas의 .corr() 메서드를 사용했다면, Polars에서는 더 빠른 속도로 개별 상관계수를 계산하거나, 여러 변수의 상관행렬을 만들 수 있습니다.

Polars의 핵심 특징은 벡터화된 연산으로 대용량 데이터에서도 상관계수를 빠르게 계산할 수 있다는 점입니다. 또한 표현식 기반으로 복잡한 조건부 상관관계도 쉽게 계산할 수 있습니다.

코드 예제

import polars as pl

# 데이터 로드
df = pl.read_csv("marketing_sales.csv")

# 두 변수 간 상관계수 계산 (피어슨 상관계수)
correlation = df.select(
    pl.corr("advertising_cost", "sales").alias("상관계수")
)
print(correlation)

# 여러 변수 간 상관행렬 계산
numeric_cols = ["advertising_cost", "sales", "website_visits", "email_opens"]

# 각 변수 쌍의 상관계수 계산
for i, col1 in enumerate(numeric_cols):
    for col2 in numeric_cols[i:]:
        corr_value = df.select(pl.corr(col1, col2)).item()
        print(f"{col1} vs {col2}: {corr_value:.3f}")

# 상관계수와 함께 p-value도 확인하고 싶다면
# scipy나 다른 통계 라이브러리 활용 필요

설명

이것이 하는 일: 이 코드는 두 개 이상의 숫자형 변수 간의 선형 상관관계를 피어슨 상관계수로 측정합니다. 첫 번째로, pl.corr("advertising_cost", "sales")는 광고비와 매출 간의 피어슨 상관계수를 계산합니다.

상관계수는 -1에서 +1 사이의 값을 가지며, +1은 완벽한 양의 상관(한 변수가 증가하면 다른 변수도 증가), -1은 완벽한 음의 상관(한 변수가 증가하면 다른 변수는 감소), 0은 선형 관계 없음을 의미합니다. 두 번째로, 여러 변수 간의 상관관계를 파악하기 위해 중첩 반복문으로 모든 변수 쌍의 상관계수를 계산합니다.

예를 들어, 광고비-매출, 광고비-웹사이트 방문, 웹사이트 방문-이메일 오픈 등 모든 조합을 확인하여, 어떤 변수들이 서로 영향을 주는지 종합적으로 파악할 수 있습니다. 세 번째로, 상관계수의 해석이 중요합니다.

일반적으로 0.7 이상이면 강한 상관, 0.4-0.7은 중간 정도 상관, 0.4 미만은 약한 상관으로 해석합니다. 하지만 주의할 점은 "상관관계는 인과관계가 아니다"는 것입니다.

아이스크림 판매와 수영장 사고가 상관관계가 있다고 해서 아이스크림이 사고를 일으키는 것은 아니며, 둘 다 기온이라는 제3의 변수에 영향을 받는 것입니다. Polars는 대용량 데이터에서도 상관계수를 빠르게 계산합니다.

수백만 행의 데이터라도 몇 초 만에 결과를 얻을 수 있어, 탐색적 분석에서 여러 변수 조합을 빠르게 시도할 수 있습니다. 여러분이 이 코드를 사용하면 변수 간의 관계를 객관적인 수치로 파악할 수 있습니다.

직관이나 추측이 아닌 데이터 기반으로 어떤 변수가 중요한지, 어떤 변수들이 서로 연관되어 있는지 확인하여 분석 방향을 설정할 수 있습니다.

실전 팁

💡 상관계수가 높다고 인과관계를 의미하는 것은 아닙니다. 항상 도메인 지식과 함께 해석하세요.

💡 비선형 관계는 피어슨 상관계수로 잡히지 않습니다. 산점도를 함께 확인하여 U자형, 지수형 관계 등을 놓치지 마세요.

💡 이상치가 상관계수를 크게 왜곡시킬 수 있습니다. 상관분석 전에 이상치를 제거하거나, 스피어만 상관계수(순위 기반) 사용을 고려하세요.

💡 다중공선성 확인: 독립변수 간 상관계수가 0.8 이상이면 회귀분석에서 문제가 될 수 있으므로, 변수 선택 시 주의하세요.

💡 시계열 데이터의 상관관계는 lag를 고려하세요. 광고 효과가 2주 뒤에 나타난다면, 현재 광고비와 2주 후 매출의 상관을 봐야 합니다: pl.col("sales").shift(-14)


8. 데이터_그룹별_집계

시작하며

여러분이 전국 매장 데이터를 보면서 "지역별로 평균 매출이 얼마나 다를까?", "월별 판매 추이는 어떻게 되지?"라는 질문을 던질 때가 있습니다. 전체 데이터를 보면 숲만 보이고, 의미 있는 단위로 쪼개서 봐야 나무가 보입니다.

이런 문제는 데이터를 적절한 그룹으로 나누지 않아서 발생합니다. 전체 평균만 보면 지역 간 차이, 시간별 변화, 제품별 특성 등 중요한 패턴을 놓칠 수 있습니다.

바로 이럴 때 필요한 것이 데이터 그룹별 집계입니다. Polars의 강력한 group_by 기능을 사용하면 원하는 기준으로 데이터를 묶고, 각 그룹의 통계량을 빠르게 계산할 수 있습니다.

개요

간단히 말해서, 그룹별 집계는 데이터를 의미 있는 그룹으로 나누고 각 그룹의 요약 통계량을 계산하는 과정입니다. 마치 학급별로 평균 점수를 구하는 것처럼, 특정 기준으로 묶어서 분석합니다.

그룹별 집계가 중요한 이유는, 전체 평균이나 합계만으로는 세부적인 패턴과 차이를 발견할 수 없기 때문입니다. 예를 들어, 전체 매출 평균이 1000만원이라도, 서울은 2000만원이고 지방은 500만원일 수 있습니다.

이러한 차이를 알아야 지역별 마케팅 전략을 다르게 세울 수 있습니다. 기존 Pandas에서는 .groupby().agg()를 사용했다면, Polars에서는 더 직관적이고 빠른 .group_by().agg() 문법으로 여러 집계를 동시에 수행할 수 있습니다.

Polars의 핵심 특징은 표현식 기반의 강력한 집계 기능과 병렬 처리입니다. 복잡한 조건부 집계, 윈도우 함수, 다중 그룹화를 간결한 코드로 처리하며, 대용량 데이터에서도 빠른 성능을 발휘합니다.

코드 예제

import polars as pl

# 데이터 로드
df = pl.read_csv("sales_transactions.csv")

# 단일 그룹 집계 (지역별)
region_stats = df.group_by("region").agg([
    pl.count().alias("거래_건수"),
    pl.col("sales").sum().alias("총_매출"),
    pl.col("sales").mean().alias("평균_매출"),
    pl.col("sales").median().alias("중앙값_매출"),
    pl.col("customer_id").n_unique().alias("고유_고객수")
]).sort("총_매출", descending=True)

print(region_stats)

# 다중 그룹 집계 (지역 + 제품 카테고리별)
multi_group = df.group_by(["region", "category"]).agg([
    pl.col("sales").sum().alias("총_매출"),
    pl.col("sales").mean().alias("평균_매출")
]).sort(["region", "총_매출"], descending=[False, True])

print(multi_group)

# 시계열 그룹 집계 (월별)
monthly = df.group_by(pl.col("date").dt.month().alias("month")).agg([
    pl.col("sales").sum().alias("월별_매출")
]).sort("month")

print(monthly)

설명

이것이 하는 일: 이 코드는 데이터를 하나 이상의 기준으로 그룹화하고, 각 그룹에 대해 다양한 통계량을 계산하여 세부적인 인사이트를 도출합니다. 첫 번째로, .group_by("region")은 데이터를 지역별로 묶습니다.

그 다음 .agg()에 여러 집계 표현식을 리스트로 전달하여 각 지역의 거래 건수, 총 매출, 평균 매출, 중앙값 매출, 고유 고객 수를 한 번에 계산합니다. 이를 통해 "서울은 거래가 많지만 건당 매출은 낮고, 제주는 거래는 적지만 건당 매출이 높다" 같은 패턴을 발견할 수 있습니다.

두 번째로, 다중 그룹화를 사용하여 더 세밀한 분석이 가능합니다. .group_by(["region", "category"])는 지역과 제품 카테고리의 조합별로 집계합니다.

예를 들어, "서울-전자제품", "서울-의류", "부산-전자제품" 등 각 조합의 매출을 볼 수 있어, 지역별로 어떤 제품이 잘 팔리는지 파악할 수 있습니다. 세 번째로, 시계열 데이터의 그룹 집계도 간단합니다.

.dt.month()로 날짜에서 월을 추출하여 그룹화 기준으로 사용하면, 월별 매출 추이를 확인할 수 있습니다. 이는 계절성 패턴이나 성장 추세를 파악하는 데 매우 유용합니다.

Polars는 group_by 연산을 병렬로 처리하여 엄청나게 빠릅니다. Pandas보다 10배 이상 빠른 경우가 많으며, 수백만 행의 데이터를 수십 개 그룹으로 나누고 집계하는 작업도 몇 초 만에 완료됩니다.

여러분이 이 코드를 사용하면 데이터를 다양한 각도에서 슬라이스하여 볼 수 있습니다. 전체 통계만으로는 보이지 않던 그룹 간 차이, 시간별 변화, 조합별 특성을 발견하여 더 정교한 전략을 수립할 수 있습니다.

실전 팁

💡 그룹이 너무 많으면 (수백 개 이상) 해석이 어렵습니다. 상위 N개 그룹만 보거나, 소수 그룹을 "기타"로 묶는 것을 고려하세요.

💡 조건부 집계가 필요하면 pl.when().then().otherwise()를 사용하세요: pl.when(pl.col("sales") > 1000000).then(pl.col("sales")).otherwise(None).sum().alias("대형거래_합계")

💡 그룹별 순위를 매기려면 .over() 함수를 사용하세요: pl.col("sales").rank().over("region")로 각 지역 내에서의 순위를 계산할 수 있습니다.

💡 그룹별 비율을 계산하려면: pl.col("sales").sum() / pl.col("sales").sum().sum() * 100로 각 그룹이 전체에서 차지하는 비율을 구할 수 있습니다.

💡 날짜 그룹화는 다양한 단위로 가능합니다: .dt.year(), .dt.quarter(), .dt.week(), .dt.day_of_week() 등을 활용하여 연도별, 분기별, 요일별 분석도 해보세요.


#Python#EDA#Polars#데이터분석#데이터전처리#데이터분석,Python,Polars

댓글 (0)

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