10가지 필수 데이터 정제 기법 12분 안에 설명하기

10 Essential Data Cleaning Techniques Explained in 12 Minutes

데이터 정제는 가장 즐거운 작업은 아닐 수 있습니다! 하지만 경험 많은 데이터 전문가들에게 가장 가치 있는 기술이 무엇인지 물어보면, 많은 이들이 복잡한 데이터를 유용한 정보로 가공하는 능력이라고 답할 것입니다.

이 글에서는 모든 성공적인 데이터 프로젝트의 핵심이 되는 필수 정제 기법들을 다룹니다. 실용적인 코드 예제와 샘플 데이터셋을 포함하여 직접 따라하고 자신의 작업에 적용할 수 있도록 했습니다.

시작해 봅시다.

1. 결측값 처리하기

실제 데이터에서 가장 흔한 문제는 아마도 결측값일 것입니다. 결측값은 분석을 왜곡하고 결과의 신뢰성을 떨어뜨릴 수 있습니다.

따라서 결측값을 식별하고 처리하는 것이 데이터 정제의 첫 번째 단계입니다.

식별

먼저 결측값 패턴을 정량화하고 이해합니다:

python

import pandas as pd

import numpy as np

샘플 데이터셋 로드

df = pd.readcsv('messydata.csv')

전체 결측값 수와 비율 구하기

missing_count = df.isnull().sum()

missing_percentage = (df.isnull().sum() / len(df)) * 100

결측값이 있는 열 표시

missingdata = pd.concat([missingcount, missing_percentage], axis=1,

keys=['Count', 'Percentage'])

print(missingdata[missingdata['Count'] > 0].sort_values('Count', ascending=False))

이 코드는 데이터셋을 로드하고, 각 열의 결측값 수를 세고, 그 비율을 계산한 다음, 결측값이 있는 열만 표시하되 결측 항목 수에 따라 정렬합니다.

삭제 전략

때로는 결측값이 있는 행이나 열을 제거해야 할 수도 있습니다(최선의 방법일 때).

완전히 비어 있는 행을 제거합니다.

python

df_clean = df.dropna(how='all')

중요한 열(이름, 이메일, 나이)이 없는 행을 제거합니다.

python

df_clean = df.dropna(subset=['name', 'email', 'age'])

결측값이 50% 이상인 열 전체를 제거합니다.

python

threshold = len(df) * 0.5

df_clean = df.dropna(axis=1, thresh=threshold)

대체 방법

대체는 결측값을 의미 있는 추정치로 바꾸는 작업입니다.

우리가 다루는 데이터에 따라 다른 대체 전략을 사용해야 합니다. 나이와 같은 수치형 데이터의 경우 중앙값(이상치에 덜 영향받음)을 사용합니다. 소득의 경우 평균을 사용합니다.

python

df['age'] = df['age'].fillna(df['age'].median())

df['income'] = df['income'].fillna(df['income'].mean())

df['customerrating'] = df['customerrating'].fillna(df['customer_rating'].mode()[0])

범주형 데이터의 경우 최빈값(가장 흔한 값)이나 의미 있는 기본값을 사용합니다.

python

df['gender'] = df['gender'].fillna(df['gender'].mode()[0])

df['comments'] = df['comments'].fillna('No comment provided')

KNN 대체기는 더 정교한 방법으로, 유사한 레코드를 살펴보고 결측값에 대해 더 현명한 추정을 합니다.

python

더 고급: 여러 열에 KNN 대체 사용

from sklearn.impute import KNNImputer

numericcols = ['age', 'income', 'customerrating', 'purchase_amount']

imputer = KNNImputer(n_neighbors=3)

df[numeric_cols] = pd.DataFrame(

imputer.fittransform(df[numericcols]),

columns=numeric_cols,

index=df.index

)

2. 중복 제거하기

중복 레코드는 통계를 왜곡하고 잘못된 결론으로 이어질 수 있습니다.

먼저 모든 열에 걸쳐 정확한 중복을 확인합니다.

python

모든 열에 걸쳐 정확한 중복 찾기

exact_duplicates = df.duplicated().sum()

print(f"Number of exact duplicate rows: {exact_duplicates}")

정확한 중복 제거

dfunique = df.dropduplicates()

그런 다음 이름과 이메일과 같은 주요 식별자를 기반으로 "기능적" 중복을 찾습니다.

keep=False 플래그는 후속 중복만이 아니라 모든 중복을 표시합니다. 마지막으로, 중복이 발견될 때 가장 완전한 레코드(결측값이 가장 적은 행)를 유지하는 스마트 중복 제거 전략을 구현합니다.

python

주요 필드를 기반으로 잠재적 기능적 중복 찾고 검사

potential_duplicates = df.duplicated(subset=['name', 'email'], keep=False)

print(f"Number of potential functional duplicates: {potential_duplicates.sum()}")

print("Potential duplicate records:")

print(df[potentialduplicates].sortvalues('name'))

중복을 제거하되 가장 완전한 레코드 유지

먼저 null이 아닌 값의 수로 정렬한 다음 중복 제거

df['completeness'] = df.notna().sum(axis=1)

dfsorted = df.sortvalues('completeness', ascending=False)

dfclean = dfsorted.drop_duplicates(subset=['name', 'email'])

dfclean = dfclean.drop(columns=['completeness'])

3. 텍스트 데이터 표준화하기

텍스트 불일치는 분석을 복잡하게 만드는 불필요한 다양성을 만듭니다. 텍스트 필드를 표준화하면 일관성을 보장하는 데 도움이 됩니다.

먼저 텍스트 필드의 대소문자를 표준화합니다: 이름에는 제목 대소문자, 국가에는 대문자, 직책에는 소문자를 사용합니다.

python

범주형 필드의 일관성을 위해 대소문자 변환

df['name'] = df['name'].str.title()

df['country'] = df['country'].str.upper()

df['jobtitle'] = df['jobtitle'].str.lower()

다음으로, 매핑 사전을 사용하여 국가 이름과 성별 값을 표준화합니다.

python

매핑을 사용하여 국가 이름 표준화

country_mapping = {

'US': 'USA',

'U.S.A.': 'USA',

'United States': 'USA',

'united states': 'USA',

'United states': 'USA'

}

df['country'] = df['country'].replace(country_mapping)

성별 값 표준화

gender_mapping = {

'M': 'Male',

'm': 'Male',

'Male': 'Male',

'male': 'Male',

'F': 'Female',

'f': 'Female',

'Female': 'Female',

'female': 'Female'

}

df['gender'] = df['gender'].replace(gender_mapping)

마지막으로, 키워드를 기반으로 다양한 형식과 철자를 처리하여 교육 수준을 표준화하는 사용자 정의 함수를 만듭니다.

python

교육 수준 표준화

def standardizeeducation(edustr):

if pd.isna(edu_str):

return np.nan

edustr = str(edustr).lower().strip()

if 'bachelor' in edu_str:

return "Bachelor's Degree"

elif 'master' in edustr or 'mba' in edustr or 'msc' in edu_str:

return "Master's Degree"

elif 'phd' in edustr or 'doctor' in edustr:

return "Doctorate"

else:

return "Other"

df['education'] = df['education'].apply(standardize_education)

4. 이상치 관리하기

이상치는 통계와 모델 성능을 크게 왜곡할 수 있습니다. 이상치의 적절한 식별과 처리가 필요합니다.

먼저 수치형 열이 실제로 숫자로 저장되어 있는지 확인합니다.

python

df['income'] = pd.to_numeric(df['income'], errors='coerce')

df['age'] = pd.to_numeric(df['age'], errors='coerce')

그런 다음 두 가지 다른 이상치 탐지 방법을 적용합니다: Z-점수(정규 분포를 가정)와 IQR 방법(비정규 데이터에 더 강력).

python

Z-점수 방법을 사용한 이상치 탐지

from scipy import stats

z_scores = stats.zscore(df['income'].dropna())

outliersz = (abs(zscores) > 3)

print(f"Z-score method identified {outliers_z.sum()} outliers in income")

IQR

Q1 = df['income'].quantile(0.25)

Q3 = df['income'].quantile(0.75)

IQR = Q3 - Q1

lower_bound = Q1 - 1.5 * IQR

upper_bound = Q3 + 1.5 * IQR

outliersiqr = ((df['income'] < lowerbound) | (df['income'] > upper_bound))

print(f"IQR method identified {outliers_iqr.sum()} outliers in income")

이상치 시각화

import matplotlib.pyplot as plt

plt.figure(figsize=(10, 6))

plt.boxplot(df['income'].dropna())

plt.title('Income Distribution with Outliers')

plt.ylabel('Income')

plt.show()

마지막으로, 명백히 잘못된 값(음수 소득)을 수정하고 윈저화(윈조라이제이션)를 보여줍니다—데이터를 제거하지 않고 극단적인 값의 영향을 줄이기 위해 백분위수 임계값에서 값을 제한합니다.

python

음수가 되어서는 안 되는 음수 값 수정

df.loc[df['income'] < 0, 'income'] = np.nan # 나중에 처리하기 위해 NaN으로 대체

백분위수 값에서 이상치 제한(윈저화)

def capoutliers(series, lowerpercentile=0.05, upper_percentile=0.95):

lowerlimit = series.quantile(lowerpercentile)

upperlimit = series.quantile(upperpercentile)

return series.clip(lower=lowerlimit, upper=upperlimit)

df['incomecapped'] = capoutliers(df['income'], 0.01, 0.99)

5. 데이터 유형 변환

올바른 데이터 유형을 보장하면 성능이 향상되고 각 열에 적절한 연산을 수행할 수 있습니다.

먼저 현재 데이터 유형을 확인합니다. 그런 다음 수치형 열을 문자열에서 실제 수치 유형으로 변환하고, 오류는 NaN으로 처리합니다.

python

현재 데이터 유형 표시

print(df.dtypes)

적절한 열에 대해 문자열을 수치로 변환

df['age'] = pd.to_numeric(df['age'], errors='coerce')

df['income'] = pd.to_numeric(df['income'], errors='coerce')

df['customerrating'] = pd.tonumeric(df['customer_rating'], errors='coerce')

df['purchaseamount'] = pd.tonumeric(df['purchase_amount'], errors='coerce')

날짜의 경우, 데이터셋에 일관되지 않은 날짜 형식이 있기 때문에 여러 날짜 형식을 시도하는 함수를 만듭니다. 날짜가 제대로 형식화되면 연도와 월과 같은 유용한 구성 요소를 추출하고 근속 기간을 계산합니다.

python

오류 처리와 함께 날짜 변환

def parsedate(datestr):

if pd.isna(date_str):

return np.nan

for fmt in ['%Y-%m-%d', '%Y/%m/%d', '%m/%d/%Y']:

try:

return pd.todatetime(datestr, format=fmt)

except:

continue

return pd.NaT

df['startdate'] = df['startdate'].apply(parse_date)

날짜에서 유용한 구성 요소 추출

df['startyear'] = df['startdate'].dt.year

df['startmonth'] = df['startdate'].dt.month

df['tenuredays'] = (pd.Timestamp('now') - df['startdate']).dt.days

마지막으로, 전화번호를 일관된 형식으로 표준화합니다. 먼저 숫자가 아닌 문자를 제거한 다음 형식을 지정합니다.

python

전화번호 형식 표준화

def standardize_phone(phone):

if pd.isna(phone):

return np.nan

# 숫자가 아닌 모든 문자 제거

digits_only = ''.join(c for c in str(phone) if c.isdigit())

# 10자리가 있으면 XXX-XXX-XXXX 형식으로 지정

if len(digits_only) == 10:

return f"{digitsonly[:3]}-{digitsonly[3:6]}-{digits_only[6:]}"

else:

return digits_only # 10자리가 아니면 그대로 반환

df['phonenumber'] = df['phonenumber'].apply(standardize_phone)

6. 일관되지 않은 범주 처리하기

범주형 변수는 종종 일관성 부족, 철자 오류, 너무 많은 고유 값으로 인해 어려움을 겪습니다.

먼저 현재 직책 분포를 검사합니다. 그런 다음 유사한 직책을 공통 형식에 매핑하여 직책을 표준화합니다(예: "Sr. Developer"와 "Senior Developer"는 모두 "senior developer"가 됨).

python

직책의 현재 고유 값 검사

print(f"Original job title count: {df['job_title'].nunique()}")

print(df['jobtitle'].valuecounts())

직책 표준화

job_mapping = {

'sr. developer': 'senior developer',

'senior developer': 'senior developer',

'ux designer': 'designer',

'regional manager': 'manager',

'project manager': 'manager',

'product manager': 'manager',

'lead engineer': 'senior developer',

'bi analyst': 'data analyst',

'data scientist': 'data analyst',

'hr specialist': 'specialist',

'marketing specialist': 'specialist'

}

df['standardizedjob'] = df['jobtitle'].str.lower().replace(job_mapping)

다음으로, 분석을 단순화하기 위해 부서를 더 넓은 범주로 그룹화합니다.

python

부서를 더 넓은 범주로 그룹화

dept_categories = {

'IT': 'Technology',

'Engineering': 'Technology',

'Analytics': 'Technology',

'Design': 'Creative',

'Marketing': 'Business',

'Product': 'Business',

'Executive': 'Management',

'Human Resources': 'Operations',

'Management': 'Management'

}

df['deptcategory'] = df['department'].replace(deptcategories)

마지막으로, 특정 횟수 미만으로 나타나는 직책을 "Other" 범주로 그룹화하여 희귀 범주를 처리하는 방법을 보여줍니다.

python

희귀 범주를 그룹화하여 처리

valuecounts = df['standardizedjob'].value_counts()

threshold = 2 # 별도 범주로 유지하기 위한 최소 개수

frequentjobs = valuecounts[value_counts >= threshold].index

df['jobgrouped'] = df['standardizedjob'].apply(

lambda x: x if x in frequent_jobs else 'Other'

)

7. 스케일링 및 정규화

다른 특성들이 모두 같은 스케일에 있지 않으면 많은 머신러닝 알고리즘에 상당히 불편할 수 있습니다.

먼저 수치형 열을 선택하고 결측값을 처리합니다.

python

from sklearn.preprocessing import StandardScaler, MinMaxScaler, RobustScaler

스케일링을 위한 수치형 열 선택

numericcols = ['age', 'income', 'customerrating', 'purchase_amount']

numericdf = df[numericcols].copy()

스케일링 전 결측값 처리

numericdf = numericdf.fillna(numeric_df.median())

그런 다음 세 가지 다른 스케일링 기법을 적용합니다.

StandardScaler: 데이터를 평균 0, 표준편차 1로 변환합니다.

python

표준화(Z-점수 정규화)

scaler = StandardScaler()

scaleddata = scaler.fittransform(numeric_df)

dfscaled = pd.DataFrame(scaleddata,

columns=[f"{col}scaled" for col in numericcols],

index=df.index)

MinMaxScaler: 데이터를 특정 범위(기본 [0,1])로 스케일링합니다.

python

최소-최대 스케일링을 [0,1] 범위로

min_max = MinMaxScaler()

minmaxdata = minmax.fittransform(numericdf)

dfminmax = pd.DataFrame(minmaxdata,

columns=[f"{col}minmax" for col in numericcols],

index=df.index)

RobustScaler: 중앙값과 사분위수를 사용하여 이상치에 덜 민감합니다.

python

로버스트 스케일링(이상치에 덜 영향받음)

robust = RobustScaler()

robustdata = robust.fittransform(numeric_df)

dfrobust = pd.DataFrame(robustdata,

columns=[f"{col}robust" for col in numericcols],

index=df.index)

원본 데이터와 스케일링된 버전 결합

dfwithscaled = pd.concat([df, dfscaled, dfminmax, df_robust], axis=1)

처음 몇 행 비교

print(dfwithscaled[['income', 'incomescaled', 'incomeminmax', 'income_robust']].head())

각 스케일링 방법에 대해 새 열을 만들고 나란히 비교합니다. 이는 소득(수천)과 고객 평점(1-5)과 같이 크게 다른 스케일을 가진 특성을 비교할 때 특히 중요합니다.

8. 문자열 정제 및 정규 표현식

정규 표현식은 텍스트 데이터를 정제하고 정보를 추출하는 데 매우 유용합니다. 샘플 데이터셋(및 일반적인 모든 데이터셋)에 대해 여러 문자열 정제 기법을 구현할 수 있습니다.

대소문자 표준화 및 이름을 첫 이름/성으로 분할:

python

import re

이름 정제: 여분의 공백 제거, 대소문자 표준화

df['name_clean'] = df['name'].str.strip().str.title()

이름과 성 추출

def extractnames(fullname):

if pd.isna(full_name):

return pd.Series([np.nan, np.nan])

parts = full_name.strip().split()

if len(parts) >= 2:

return pd.Series([parts[0], parts[-1]])

else:

return pd.Series([parts[0] if parts else np.nan, np.nan])

nameparts = df['nameclean'].apply(extract_names)

df['firstname'] = nameparts[0]

df['lastname'] = nameparts[1]

이메일 유효성 검사를 위해 정규 표현식 패턴에 대해 확인하고 일반적인 문제 해결 시도:

python

이메일 주소 검증 및 정제

def clean_email(email):

if pd.isna(email):

return np.nan

# 유효한 이메일의 기본 패턴

pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'

if re.match(pattern, email):

return email.lower()

else:

# 일반적인 문제 해결 시도

if '@' not in email:

return np.nan # @ 누락된 이메일은 수정 불가

# 도메인 확장자가 없으면 추가

if not re.search(r'\.[a-zA-Z]{2,}$', email):

return email.lower() + '.com'

return email.lower()

df['emailclean'] = df['email'].apply(cleanemail)

이메일 도메인 정보 추출:

python

이메일에서 도메인 추출

df['emaildomain'] = df['emailclean'].str.extract(r'@([^@]+)$')

다양한 전화번호 형식을 일관된 패턴으로 변환:

python

전화번호 표준화/추출

def extract_phone(phone):

if pd.isna(phone):

return np.nan

# 숫자만 추출

digits = re.sub(r'\D', '', str(phone))

# 10자리 숫자인지 확인

if len(digits) == 10:

return f"({digits[:3]}) {digits[3:6]}-{digits[6:]}"

elif len(digits) > 10: # 국가 코드가 포함될 수 있음

return f"({digits[-10:-7]}) {digits[-7:-4]}-{digits[-4:]}"

else:

return np.nan # 충분하지 않은 숫자

df['phonestandardized'] = df['phonenumber'].apply(extract_phone)

이러한 기법은 텍스트 데이터를 더 일관되게 만들고 비구조화 또는 반구조화된 필드에서 구조화된 정보를 추출할 수 있습니다.

9. 불완전한 데이터에서 특성 공학하기

때때로 지저분한 데이터의 패턴이 유익할 수 있으며 필요에 따라 새로운 대표 특성을 만들 수 있습니다.

결측값 지표: 어떤 필드가 누락되었는지 보여주는 더미 변수 생성

python

주요 필드의 결측값에 대한 지표 생성

missingindicators = ['age', 'income', 'email', 'phonenumber', 'comments']

for col in missing_indicators:

df[f'{col}ismissing'] = df[col].isnull().astype(int)

데이터 품질 점수: 전체 레코드 완전성을 계산하고 범주화

python

데이터 품질 점수 생성

df['quality_score'] = df.notna().sum(axis=1) / len(df.columns) * 10

df['quality_category'] = pd.cut(

df['quality_score'],

bins=[0, 6, 8, 10],

labels=['Poor', 'Average', 'Good']

)

완벽하게 반올림된 소득, 합리적인 범위를 벗어난 나이 등의 의심스러운 값 플래그:

python

잠재적 데이터 입력 오류 감지

df['incomesuspiciouslyround'] = (df['income'] % 10000 == 0).astype(int)

df['ageoutof_range'] = ((df['age'] < 18) | (df['age'] > 80)).astype(int)

df['ratingoutofrange'] = ((df['customerrating'] < 1) | (df['customer_rating'] > 5)).astype(int)

불완전한 데이터를 단순히 정제하는 대신, 그로부터 유용한 정보를 추출합니다.

10. 형식 문제 처리하기

일관되지 않은 형식은 데이터를 분석하거나 모델을 구축할 때 문제를 일으킬 수 있습니다.

여기서는 일반적인 형식 문제를 다룹니다.

다양한 날짜 형식을 일관된 형식으로 변환:

python

날짜 형식 표준화

df['startdateclean'] = pd.todatetime(df['startdate'], errors='coerce')

표시를 위해 날짜를 일관되게 형식화

df['startdateformatted'] = df['startdateclean'].dt.strftime('%Y-%m-%d')

수치 변환 전 통화 기호 및 구분자 제거:

python

통화 형식 문제 수정

def clean_currency(amount):

if pd.isna(amount):

return np.nan

if isinstance(amount, (int, float)):

return amount

# 통화 기호 및 쉼표 제거

amount_str = str(amount)

amountstr = re.sub(r'[$,]', '', amountstr)

# 실수로 변환

try:

return float(amount_str)

except:

return np.nan

purchase_amount에 형식 문제가 있을 수 있다면

df['purchaseamountclean'] = df['purchaseamount'].apply(cleancurrency)

보고를 위해 일관되게 형식화된 값 생성:

python

일관성을 위한 숫자 형식화

df['income_formatted'] = df['income'].apply(

lambda x: f"${x:,.2f}" if not pd.isna(x) else ""

)

일관된 형식은 최종 사용자에게 제시되거나 보고서에 사용될 데이터에 특히 중요합니다.

여러 정제 기법을 적용한 후, 분석을 위한 최종 정제 데이터셋을 만듭니다.

마무리

실제 데이터셋에서 가장 흔히 접하게 될 일반적인 데이터 품질 문제를 해결하는 실용적인 기법들을 다루었습니다. 이러한 방법은 문제가 있는 데이터를 신뢰할 수 있는 분석 자산으로 변환하는 데 도움이 됩니다.

데이터 정제는 반복적인 과정입니다—각 단계는 종종 데이터의 구조와 품질에 대한 새로운 통찰력을 제공합니다. 여기서 공유된 패턴은 특정 요구 사항에 맞게 조정하고 자동화된 워크플로우에 통합할 수 있습니다.

즐거운 데이터 정제가 되길 바랍니다!