파이썬 종합 정리/2강

2강 <전처리> 내용 중 알아야 할 코드

jjaio8986 2025. 5. 27. 01:49

1. 데이터 중 수치형, 범주형 컬럼의 확인 및 결측치의 확인

# 0. 데이터 불러오기
df = pd.read_csv('어떤 무슨 데이터.csv')
 
# 1. 데이터 확인
"""수치형 컬럼 확인"""
numeric_cols = df.select_dtypes(include=['number']).columns.tolist()   
 
"""범주형 컬럼 확인"""
categorical_cols = df.select_dtypes(exclude=['number']).columns.to_list()
print("수치형 :", numeric_cols)
print("범주형 :", categorical_cols)
 
# 설명) df 내의 데이터 타입을 선택하는 select_dtypes() 을 사용하여 데이터 타입이 number 인 것을 선택
여기에 해당하는 columnstolist()를 통해 리스트 형태로 담아뒀다.
 
# 2. 각 열의 결측치 개수 확인
missing_cnt = df.isnull().sum()
missing_cnt = missing_cnt[missing_cnt > 0]
missing_cnt = missing_cnt.sort_values(ascending=False)
print(f"결측치가 있는 열 개수: {len(missing_cnt)}개")
display(missing_cnt)
 
# 설명) df 내의 결측치의 개수를 합산하는 .isnull().sum()을 사용하여 1차적으로 missing_cnt 에 담았다.
 그 후 missing_cnt 에 다시 컬럼 중에서 결측치 개수가 0개 초과인 컬럼만을 다시 missing_cnt 에 담는다.
최종적으로 missing_cntsort_values(ascending=False) 를 통해 결측치가 많은 순서대로 내림차순 정렬한다. 
 

 

1.1. 결측치 제거 및 대체

※ 수치형 컬럼은 그 결측치가 원본 데이터의 행 개수의 90%이상인 경우(금액과 관련 컬럼 제외) 컬럼을 제거하는 것이 좋다.

 또한, 나머지 중에서 결측치가 많은 컬럼(20~80%)은 컬럼의 의미를 확인하여 "평균값", "중앙값"으로 채운다.[통계적 큰 왜곡 방지]

 덧붙여 1~2개 정도의 결측치는 "최빈값"으로 채우는 것이 무난하다.

※ 범주형 컬럼은 해당 컬럼의 의미상 그러한 "특징"이 없다! 라는 의미! 따라서 "NaN"을 "None"으로 채우는 것이 적절하다!

 단, 무조건 "None"으로 체우지 말고 범주형 컬럼의 의미를 확인하여 "NaN"의 의미를 확인하는 것이 좋다.

 

# 수치형 컬럼 중 결측치가 많은 열 삭제
cols_to_drop = ['PoolQC', 'MiscFeature', 'Alley']
df = df.drop(columns=cols_to_drop)
print(f"삭제한 열: {cols_to_drop}")
print(f"남은 열 개수: {df.shape[1]}")
# 범주형 결측 대체: NaN을 str(None)으로 채운다.
cols_fill_none = ['FireplaceQu', 'Fence', 'GarageType', 'GarageFinish',
                  'GarageQual', 'GarageCond', 'BsmtQual', 'BsmtCond',
                  'BsmtExposure', 'BsmtFinType1', 'BsmtFinType2', 'MasVnrType']
for col in cols_fill_none:
    if col in df.columns:
        df[col].fillna('None', inplace=True)
 
# 설명)
컬럼을 지울때는 drop() 메서드를 사용한다.
컬럼을 채울때는 fillna('채워넣을 값', inplace= True) 를 사용하여 inplace= True 를 통해 실제  df[col]에 적용한다.

2. 데이터 타입 변경

 - 데이터 타입에 따라 사용 가능한 함수, 연산, 메모리 효율 및 성능이 달라지기에 올바른 데이터 타입으로 전환!

# 데이터 타입 변경
print(df.dtypes[['MSSubClass', 'MSZoning', 'LotFrontage', 'YrSold', 'MoSold']])
 
# 범주형 변환
df['MSSubClass']=df['MSSubClass'].astype('category')
print(df['MSSubClass'].dtype)
print(df['MSSubClass'].cat.categories[:5])
 
# 설명
# astype('category') : 해당 객체를 카테고리 타입의 데이터로 전환!
# cat.categories: cat은 카테고리 전용 기능에 접근!, categories 는 해당 열의 모든 고유 범주값을 반환!
# df.memory_usage().sum() :  df 의 메모리 사용량을 확인해 볼 수 있다.

3. 이상치 탐지 및 처리

 3.1. 이상치의 정의

  - 데이터 분포에서 다른 관측치들과 크게 동떨어진 값들

  - 측정 오류 혹은 특수한 경우일 수 있음

  - 통계 분석과 모델 학습 시 극단값이 평균 등을 왜곡 혹은 모델 과적합 문제가 발생할 수 있음.

  - 따라서 전처리 단계에서 이상치를 탐지, 필요시 처리(제거 또는 변환)하는 것이 중요!

 

 3.2. 이상치 탐지 방법

  A. 통계적 방법 : 사분위 범위(IQR)사용

   "단변량 통계치" : ["(Q1 - 1.5IQR)"이하 혹은 "(Q3+1.5IQR)"이상을 이상치로 간주] (적은 데이터 풀에서 효과적)

                              cf) IQR 사용시 1.5 대신 3으로 두어 더 보수적으로 이상치를 잡아낼 수 있다. [이상치 경계 조정]

   ["Z-score"(평균 대비 표준편차 몇 배) 기준으로 |Z| > 3이상일 시 이상치] (이게 가장 일반적)

  B.시각적 방법 : Boxplot, Histogram, Scatter plot(산점도)를 통해 분포를 그려보면 극단치가 눈에 띈다.

  C. 도메인 기반 : 데이터 배경 지식으로 현실적으로 말이 안 되는 값들을 찾는다. ex. 사람키 = 0cm 혹은 300cm 같은 값

 

 3.3. 이상치 처리 방법

  A. 삭제 : 값들이 명백히 잘못되었거나 분석 목적에 방해시 해당 행을 제거!

  B. 변환 : 삭제 대신 다른 값으로 조정

   ex. 상한 및 하한을 두어 그 이상은 모두 그 경계값으로 윈저라이징(winsorizing)하거나, 로그 변환을 통해 분포의 치우침(skewness)을 완화!

  C. 모델에 따라 이상치를 그대로 두어도 괜찮을 수 있다.

   ex. 트리 기반 모델(이상치 영향이 적음) but 선형 회귀, 평균에 민감한 알고리즘, 거리 기반 모델(k-NN 등)은 영향이 크기에 처리 필요!

 

# 캐글 데이터 설명에선 거주 면적 4000sqft를 초과하는 주택 중
가격이 상대적으로 낮은 두 개의 데이터가 이상치로 간주됨.
# GrLivArea > 4000인 집들의 SalePrice 확인
large_area_houses = df[df['GrLivArea'] > 4000]
print(large_area_houses[['GrLivArea', 'SalePrice']])
print(f"이상치로 의심되는 행 개수: {large_area_houses.shape[0]}")
 
# 해당 이상치로 의심도는 행 삭제
df = df[~((df['GrLivArea'] > 4000) & (df['SalePrice'] < 200000))]
print(f"제거 후 남은 데이터 개수: {df.shape[0]}")

 

# IQR을 통한 이상치 검출 'LotArea'
Q1 = df['LotArea'].quantile(0.25)
Q3 = df['LotArea'].quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR

outliers = df[(df['LotArea']<lower_bound) | (df['LotArea']>upper_bound)]
print(f"LotArea IQR 기반 이상치 개수: {outliers.shape[0]}")
print(outliers[['LotArea', 'SalePrice']].head())

 

# SalePrice 정규 분포에 가깝게 만들어, 이상치 완화
# a. 로그 변환 전 분포 및 왜곡도 확인
print(f"변환 전 SalePrice 왜곡도: {df['SalePrice'].skew():.3f}")

plt.figure(figsize=(10,4))
plt.subplot(1,2,1)
plt.hist(df['SalePrice'], bins=40, edgecolor='k')
plt.title("SalePrice (Raw)")
plt.xlabel("price")
plt.ylabel("count")

# b. 로그 변환 (log1p: log(1+x) -> 0    값도 안전)
df['SalePrice_log'] = np.log1p(df['SalePrice'])
print(f"변환 후 SalePrice 왜곡도: {df['SalePrice_log'].skew():.3f}")

plt.subplot(1,2,2)
plt.hist(df['SalePrice_log'], bins=40, edgecolor='k', color='salmon')
plt.title("SalePrice (log1p transformed)")
plt.xlabel("log(price)")
plt.ylabel("count")
plt.tight_layout
plt.show()

4. 범주형 인코딩

 4.1. 정의

  - 머신러닝 모델은 대개 숫자형 입력만 처리할 수 있기에, 범주형 데이터를 적절한 숫자로 변환할 필요가 있다.

 

 4.2. 인코딩 방법

  A. 레이블 인코딩(Lable Encoding) : 각 범주 값을 정수로 매핑

   ex. ['Red', 'Green', 'Blue']    =>    [0, 1, 2]

   - 장점 : 간단하며 범주 개수가 많아도 하나의 열에 담을 수 있다.

   - 단점 : 범주 간 순서나 크기관계가 없으니 알고리즘은 크기 순서로 인식할 우려가 있다.

# 레이블 인코딩
# 사실상 astype('category')사용시 내부적 코드가 생성되지만 여기서는 명시적으로 코드를 확인할 수 있게 pd.factorize를 사용!
# factorize : 범주형 값을 코드(정수) 배열과 범주 목록을 반환한다.
# MSZoning 컬럼에 레이블 인코딩 적용(새로운 열 추가)
codes, uniques = pd.factorize(df['MSZoning'])
df['MSZoning_Code'] = codes
print(uniques)
print(df[['MSZoning', 'MSZoning_Code']].head(5))

  B. 원-핫 인코딩(One-Hot Encoding) : 범주의 고유값에 이진(dummy) 컬럼을 만든다.

   ex) Color 컬럼에 3가지 값이 있다면 3개의 새로운 컬럼 Color_Red, Color_Green, Color_Blue를 생성

         해당 범주에만 1, 나미저는 0으로 표시! 

   - 장점 : 범주 간 거리/순서 정보가 없기에 순서 왜곡 문제 없음

   - 단점 : 범주 종류가 많으면 컬럼 수가 매우 증가 차원의 저주 문제가 될 수 있음.

cf) 차원의 저주 : 데이터 학습을 위해 차원이 증가하면서 학습 데이터 수가 차원의 수보다 적어져 성능이 저하되는 현상

One-Hot Encoding 팁: 만약 서로 배타적인 범주라면 위 방식대로 해도 되지만, 하나의 관측치에 여러 범주가 해당될 수 있는 경우 (예: 태그가 여러 개 붙을 수 있는 데이터)에는 다르게 처리해야 합니다. 또한, dummy 변수 중 하나는 상수관계(다 합치면 1)이므로 회귀 모형 등에 넣을 때는 하나를 드롭(drop-first) 해서 다중공선성을 피하기도 합니다. Pandas에서는 pd.get_dummies(drop_first=True) 옵션으로 첫 범주 열을 제거할 수 있습니다.

 

참고: pd.get_dummies(df, columns=[...])를 쓰면 원본의 해당 컬럼들은 삭제되고 더미 컬럼이 추가됩니다.

# 원 핫 인코딩
# 판다스에서 pd.get_dummies() 함수를 통해 매우 쉽게 원 핫 인코딩 가능
# MSZoning을 원-핫 시도
dummies = pd.get_dummies(df['MSZoning'], prefix='MSZ')
print(dummies.head(5))
print(f"생성된 더미(dummy)컬럼 수: {dummies.shape[1]}")
#설명
prefix='MSZ' : 원 핫 인코딩은 해당 컬럼의 고유값을 인코딩하여 고유값의 개수 만큼 컬럼을 만들어 낸다.
                       해당 파라미터는 만들어지는 컬럼의 접두사를 'MSZ'로 시작하게 하여 나머지 고유값의 이름을 컬럼명으로 삼는다.
ex. 'MSZ_FV'

 

  C. 기타 인코딩 기법 : 이 외에도 명목 vs 순서형에 따라 순서형은 그 자체로 순위 숫자를 사용, 빈도 인코딩, 타깃 인코딩(평균값 인코딩)등 다양한 기법이 있다.

# 순서형 변수 인코딩 (기타 인코딩 중 하나인 평균값으로 타겟 인코딩!)
# 1) OverallQual별 SalePrice 평균 구하기
qual_price_mean = (
    df.groupby('OverallQual')['SalePrice']
      .mean()
      .sort_index()
)
print("== OverallQual vs. SalePrice 평균 ==")
print(qual_price_mean)

# 2) Target Encoding: 평균값으로 매핑해 새 컬럼 생성
df['OverallQual_TE'] = df['OverallQual'].map(qual_price_mean)

# 3) 결과 확인 (원본·인코딩 값 나란히)
print("\n== 변환 확인 (앞 5행) ==")
print(df[['OverallQual', 'OverallQual_TE']].head())

5. 특성 스케일링(Feature Scaling)

 5.1. 스케일링 : 데이터의 특성 값들을 일정한 스케일(범위)로 변환하는 작업

   A. 정규화 : 최소값 0, 최대값 1로 맞추는 변환 (Min-Max 스케일링)

   B. 표준화 : 평균 0, 표준편차 1로 분포를 맞추는 변환 (Z-스코어 스케일링)

 5.2. 스케일링 필요성

   - 특성마다 단위와 범위가 다를 시 일부 알고리즘에서 큰 스케일의 변수가 지배적 영향력을 행사!

    [특히 K-평균 군집, K-NN은 거리 계산에 민감하기에 반드시 필요!]

    [경사하강법 모델(신경망, SVM, 선형모델 등)은 스케일이 맞지 않으면 최적화 시간이 오래 걸린다.]

    [트리 기반 모델은 스케일 영향을 거의 받지 않지만 전처리 파이프라인을 일관성 있게 만드는 차원에서 변환하기도 한다.]

# 특성 스케일링 (정규화와 표준화)
# Min Max 정규화 (각 컬럼의 값에서 최소값을 뺀 분자와 컬럼 최대값에서 최소값을 뺀 [일종의 값의 범위]분모를 사용!)
# Min-Max Scaling: 수식으로 직접 구현
df['GrLivArea_norm'] = (df['GrLivArea'] - df['GrLivArea'].min()) / (df['GrLivArea'].max() - df['GrLivArea'].min())
df['LotArea_norm'] = (df['LotArea'] - df['LotArea'].min()) / (df['LotArea'].max() - df['LotArea'].min())

print(df[['GrLivArea','GrLivArea_norm','LotArea','LotArea_norm']].head(3))
print(df[['GrLivArea_norm','LotArea_norm']].describe().loc[['min','max']])
# 정규화 - 사이킷 런 MinMaxScaler
from sklearn.preprocessing import MinMaxScaler

# ── 1) 스케일러 생성 & 학습 ──────────────────────────────────────────────
scaler = MinMaxScaler()                        # 기본 feature_range=(0, 1)
cols_to_scale = ['GrLivArea', 'LotArea']       # 정규화할 컬럼 목록
df_scaled = df.copy()                          # 원본 보존용 복사본

# fit_transform은 넘파이 배열을 반환 → DataFrame에 다시 넣기
df_scaled[cols_to_scale] = scaler.fit_transform(df[cols_to_scale])

# ── 2) 결과 확인 ───────────────────────────────────────────────────────
print(df_scaled[['GrLivArea', 'LotArea']].head(3))
print(df_scaled[cols_to_scale].describe().loc[['min', 'max']])
# 표준화 : 수식 구현
# Standardization (Z-score scaling)
saleprice_mean = df['SalePrice'].mean()
saleprice_std = df['SalePrice'].std()
df['SalePrice_std'] = (df['SalePrice'] - saleprice_mean) / saleprice_std

print(f"SalePrice 원본 평균: {saleprice_mean:.2f}, 표준편차: {saleprice_std:.2f}")
print(df[['SalePrice','SalePrice_std']].head(3))
print(df['SalePrice_std'].describe().loc[['mean','std']].round(10))
# 표준화 - 사이킷 런의 standardScaler
from sklearn.preprocessing import StandardScaler
import pandas as pd

scaler = StandardScaler()

# DataFrame에 바로 적용하려면 2-D 형태가 필요합니다.
df['SalePrice_std'] = scaler.fit_transform(df[['SalePrice']])

print(df[['SalePrice', 'SalePrice_std']].head(3))
print(df['SalePrice_std'].describe().loc[['mean', 'std']])
# 표준화 - Scipy의 Starts.ascore
from scipy.stats import zscore

df['SalePrice_std'] = zscore(df['SalePrice'])

6. 중복 데이터 제거

 6.1. 중복 데이터 : 동일한 관측치가 데이터에 두 번 이상 포함된 경우

        특히 크롤링이나 데이터 통합 과정 상에서 같은데이터가 여러 번 들어오는 경우가 흔히 발생!

 6.2. 중복 확인방법

  - Pandas의 df.duplicated()메서드로 중복 여부를 bool 시리즈 형태로 얻을 수 있고 sum()을 덧붙여 총 중복 행 수를 알 수 있다.

  - add) 기본적으로 첫 번째 등장하는 행은 중복으로 치지 않고, 그 이후 같은 내용이 나오는 행들을 True로 표시합니다. (keep='first' 기본값)

 

 6.3. 중복 제거

   - df.drop_duplicates()로 제거한다. 이때도 keep파라미터로 어느 것을 남길 지 정할 수 있다.

   - add) first, last, False와 같은 서브셋 파라미터로 특정 열들만 비교해 중복판정이 가능하다.

※ 중복 데이터 제거시 중복 발생 원인 파악이 중요! 간혹 완전 동일 행은 아니나 특정 키값 기준에선 중복인 경우가 있음.

예를 들어 고객 데이터에서 같은 사람(ID)은 하나여야 하는데, 시스템 오류로 두 번 들어갔다면 ID 기준 중복 제거를 해야 합니다. Pandas drop_duplicates(subset=['ID'], keep='first') 같은 방법으로 처리

#간단한 예시(현 데이터 셋에선 중복이 없음.)
import pandas as pd
 
# 1) 예시 데이터 생성
data = {
    'Id':   [101, 102, 103, 104, 105],
    'Name': ['Alice', 'Bob', 'Charlie', 'David', 'Eve'],
    'City': ['Seoul', 'Seoul', 'Busan', 'Seoul', 'Busan'],
    'Score': [85, 91, 78, 88, 95]
}
df = pd.DataFrame(data)
print("원본 DataFrame")
print(df)

# 2) 일부 행을 복제해 중복 데이터프레임 만들기
#    (여기선 1·3번째 행을 다시 붙여 넣음)
duplicates = df.iloc[[0, 2]]          # 복사할 행
df_dup = pd.concat([df, duplicates], ignore_index=True)
print("\n중복 추가 DataFrame")
print(df_dup)

# 3) 중복 여부 확인
dup_mask = df_dup.duplicated()
print("\n중복 판정(전체 컬럼 기준):")
print(dup_mask)
print(f"중복 행 개수: {dup_mask.sum()}")

# 4) 중복 제거
#    keep='first'  : 첫 번째만 남기고 이후 중복 삭제(기본값)
#    keep=False    : 중복된 모든 행 제거
df_unique = df_dup.drop_duplicates(keep='first')
print("\n중복 제거 결과 (keep='first')")
print(df_unique)

# 예) ID만 달라도 나머지가 모두 같으면 중복으로 본다
df_unique_subset = df_dup.drop_duplicates(subset=['Name','City','Score'], keep='first')
print("\nID를 무시하고 Name·City·Score만 비교해 중복 제거")
print(df_unique_subset)

7. 전처리 파이프라인 구조화

 7.1. 파이프라인 : 여러 단계의 전처리 과정을 연속적으로 처리하기 위한 일련의 구성!

                            한 단계의 출력이 다음 단계의 입력으로 들어가는 흐름 = "파이프라인"

                            데이터 처리에서는 이 개념을 활용해 코드를 모듈화하고 재사용 가능!

 

※ 실제 프로젝트에서는 [결측치 처리 → 타입 변환 → 이상치 제거 → 인코딩 → 스케일링 → 중복 제거]의 전처리 과정을 정해진 순서대로 반복함! 파이프라인 구성 시 일일이 단계 실행 필요 없이 한 번에 처리 가능!

 

 7.2. 파이프라인 구성 방법

  A. Pandas method chining: 판다스에는 데이터 프레임을 반환하는 메서드들이 많기에 

<df.some_operation().another_operation().yet_another()>처럼 체인으로 연결해 한 줄로 여러 처리 가능

<df.pipe(finc)>를 사용해 커스텀 함수들을 연달아 적용 가능

  B. Scikit-Learn Pipeline : 사이킷 런의 Pipeline 객체를 사용시, 연속된 변환기(Transformer)와 마지막 Estimator를 묶어 하나처럼 처리 가능!

   - 전처리 전용으로 Pipeline을 쓸 수 있고, ColumnTransformer를 활용할 시 컬럼별 다른 처리를 한 번에 적용 가능!

     [이 경우 모델 학습을 염두에 두고 훈련용/테스트용 데이터에 동일한 변환 적용을 목적으로 사용]

# 전처리 파이프라인 - 사이킷 런을 통한 지금까지의 모든 과정을 파이프라인화! 단, 컬럼별 특징을 반영
# 현재 특정 컬럼만 적용한 상황
from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer
from sklearn.impute import SimpleImputer
from sklearn.preprocessing import StandardScaler, OneHotEncoder
import pandas as pd

df = pd.read_csv('train.csv')

# 사용 변수 지정
numeric_features = ['LotFrontage','LotArea']
categorical_features = ['MSZoning']

# 수치 파이프라인: 중앙값으로 결측치 채우기 -> 표준화
numeric_pipeline = Pipeline([
    ('imputer', SimpleImputer(strategy='median')),
    ('scaler', StandardScaler())
])

# 범주 파이프라인: 'None'으로 결측치 채우기 -> 원핫인코딩
categorical_pipeline = Pipeline([
    ('imputer', SimpleImputer(strategy='constant', fill_value='None')),
    ('onehot', OneHotEncoder(handle_unknown='ignore'))
])

# 컬럼별 파이프라인 결합
preprocessor = ColumnTransformer([
    ('num', numeric_pipeline, numeric_features),
    ('cat', categorical_pipeline, categorical_features)
])

# 파이프라인 전체를 데이터에 적용 (특성 행렬 X 생성)
X = df[numeric_features + categorical_features]
X_processed = preprocessor.fit_transform(X)
print("전처리 후 데이터 형태:", X_processed.shape)

8. 전처리 완료 데이터 저장

 8.1. 개요

  - 전처리는 시간이 꽤 걸리기도 하고, 동일 작업 재현을 하는 경우가 많음! 따라서 중간 결과물을 파일로 저장하면 편리하다.

  - 특히 모델링 단계에서 깨끗히 정리된 데이터를 바로 불러다 쓸 수 있으니 효율적

# 전처리 완료 데이터 저장
df.to_csv('ames_processed.csv', index=False)
 
# index=False는 행 번호를 저장하지 않는다는 의미!
차후 데이터를 불러올 때 새로 저장된 인덱스 번호를 지울 필요가 없어진다!