결측치(Missing Values)와 이상치(Outlier Values) 탐지 및 처리
결측치: 데이터에 값이 없는 경우
이상치: 데이터의 일반적인 패턴에서 벗어난 값, 문제의 정의에 따라 값을 새롭게 정의할 수 있습니다.
※ 적절한 처리 방법은 데이터의 특성과 도메인 지식, 분석 목적에 따라 달라지므로 경우에 맞게 적절한 처리방법을 선택해야 합니다.
일관된 결측치 처리와 목적을 고려하지 않은 이상치 판단은 잘못된 데이터 분석을 만들며 성능을 낮출 수 있습니다.
NaN은 수치적인 데이터 측면으로, 정의되지 않은 값이고 None은 값이 없는 /할당되지 않은 값
파이썬에서는 None도 결측치로 처리하기 때문에 상관 없지만 둘은 개념적으로는 다르다
결측치 탐지
- isna(), isnull(): 결측치 탐지
import pandas as pd
#데이터프레임 생성
dogs = {
"name": ["mung", "bella", "teddy", "lucy"],
"age": [3, 2, 1, None],
"gender": ["male", None, "female", "female"],
"breed": ["maltese", "beagle", "poodle", "poodle"]
}
df = pd.DataFrame(dogs)
print(df)
#age는 수치 데이터이므로 NaN, gender는 None으로 결과 도출
#--------아래는 결과----------
name age gender breed
0 mung 3.0 male maltese
1 bella 2.0 None beagle
2 teddy 1.0 female poodle
3 lucy NaN female poodle
print(df.isna()) #결측치가 있는 곳은 True, 값이 있는 곳은 False
#----------아래는 결과------------
name age gender breed
0 False False False False
1 False False True False
2 False False False False
3 False True False False
- isna().sum(), isnull().sum(): 결측치 개수 확인
print(df.isna().sum())
#----------아래는 결과------------
name 0
age 1
gender 1
breed 0
결측치 처리
- dropna(): 결측치 제거
df_dropped_rows = df.dropna() #행 제거
print("결측치가 있는 행 제거\n", df_dropped_rows)
df_dropped_columns = df.dropna(axis=1) #열 제거
print("\n결측치가 있는 열 제거\n", df_dropped_columns)
#행은 bella와 lucy가 , 열은 age, gender가 제거됨
#----------아래는 결과------------
결측치가 있는 행 제거
name age gender breed
0 mung 3.0 male maltese
2 teddy 1.0 female poodle
결측치가 있는 열 제거
name breed
0 mung maltese
1 bella beagle
2 teddy poodle
3 lucy poodle
- fillna(): 결측치 채우기
- 평균: mean(), 중앙값: mediana(), 최빈값: mode().iloc[0] 등으로 채울 수 있음
#결측치를 특정 값으로 대체
df_filled = df.fillna('--')
print(df_filled)
#결측치 자리가 '-'로 채워짐
#----------아래는 결과------------
name age gender breed
0 mung 3.0 male maltese
1 bella 2.0 -- beagle
2 teddy 1.0 female poodle
3 lucy -- female poodle
#age의 결측치를 평균값으로 대체
df['age'] = df['age'].fillna(df['age'].mean())
print(df)
#lucy의 나이가 평균값 2로 채워짐
#----------아래는 결과------------
name age gender breed
0 mung 3.0 male maltese
1 bella 2.0 None beagle
2 teddy 1.0 female poodle
3 lucy 2.0 female poodle
- interpolate(): 주변 값들을 기반으로 결측치 보간
√ 행의 순서에 따라 값이 정렬되어 있어 결측치를 추출할 수 있을 때 사용 가능, 예) 사람 이름을 기준으로 나이를 보간할 수 없음
더보기
보간( Interpolation): 인접한 주위 점들을 평균화 해 새로운 점을 만듦
선형 보간법: 끝점의 값으로 그 사이 값을 추정하기 위해 직선 거리에 따라 선형적으로 계산하는 방법
import pandas as pd
#데이터프레임 생성
data = {
"day": [1, 2, 3, 4, 5, 6, 7],
"temperature": [22.5, None, 24.3, 25.1, None, 23.8, 24.5],
"humidity": [60, 62, None, 65, 64, None, 63]
}
df = pd.DataFrame(data)
print(df)
#----------아래는 결과------------
day temperature humidity
0 1 22.5 60.0
1 2 NaN 62.0
2 3 24.3 NaN
3 4 25.1 65.0
4 5 NaN 64.0
5 6 23.8 NaN
6 7 24.5 63.0
df['temperature'] = df['temperature'].interpolate()
print(df)
#온도 열의 결측치가 선형 보간법을 통해 채워짐
#----------아래는 결과------------
day temperature humidity
0 1 22.50 60.0
1 2 23.40 62.0
2 3 24.30 NaN
3 4 25.10 65.0
4 5 24.45 64.0
5 6 23.80 NaN
6 7 24.50 63.0
- apply(): 사용자 정의 함수를 적용해 결측치 처리
import pandas as pd
#데이터프레임 생성
data = {
"name": ["alice", "bob"],
"age": [None, 25],
"occupation": ["student", None]
}
df = pd.DataFrame(data)
print(df)
#----------아래는 결과------------
name age occupation
0 alice NaN student
1 bob 25.0 None
#'occupation'이 결측치일 경우, 기본값으로 'employee'로 채우는 함수
def fill_missing_age(x):
if pd.isna(x):
return 'employee'
return x
df['occupation'] = df['occupation'].apply(fill_missing_age)
print(df)
#bob의 직업이 employee로 채워짐
#----------아래는 결과------------
name age occupation
0 alice NaN student
1 bob 25.0 employee
- 특정 조건 기반 결측치 처리 (경우에 따른 결측치 처리가 가능함)
#다른 열의 값을 기준으로 결측치를 채우는 방법
#'직업'이 '학생'인 경우, '나이'를 18로 채우기
df.loc[(df['occupation'] == 'student') & (df['age'].isna()), 'age'] = 18
print(df)
#alice의 나이가 18로 채워짐
#----------아래는 결과------------
name age occupation
0 alice 18.0 student
1 bob 25.0 employee
- 예측 모델을 이용해 결측값을 예측하여 대체
#예측 모델로 결측값 대체
from sklearn.linear_model import LinearRegression
# 결측값이 있는 열과 없는 열 분리
df_with_na = df[df['column_with_na'].isnull()]
df_without_na = df[df['column_with_na'].notnull()]
# 회귀 모델 학습
model = LinearRegression()
model.fit(df_without_na[['feature1', 'feature2']], df_without_na['column_with_na'])
# 결측값 예측
predicted_values = model.predict(df_with_na[['feature1', 'feature2']])
# 예측된 값으로 결측값 대체
df.loc[df['column_with_na'].isnull(), 'column_with_na'] = predicted_values
이상치 탐지
- 기술 통계 기반 이상치 탐지: describe()를 통해 주요 통계 정보 확인
평균과 표준편차가 큰 차이를 보이는 경우 또는 최대값이 비정상적으로 높은 경우 등은 이상치로 의심할 수 있습니다.
import pandas as pd
#데이터프레임 생성
data = {
'name': ['bob', 'alice', 'mung', 'lucy'],
'age': [25, 30, 22, 120], #나이가 120은 이상치로 의심
'score': [90, 85, 95, 80]
}
df = pd.DataFrame(data)
# 기술 통계량 확인
print(df['age'].describe())
#----------아래는 결과------------
count 4.000000
mean 49.250000
std 47.281956
min 22.000000
25% 24.250000
50% 27.500000
75% 52.500000
max 120.000000
- 시각화를 통한 이상치 탐지: boxplot()과 hist()를 사용해 데이터의 분포를 시각적으로 확인 가능 = 이상치 탐지 용이
박스플롯의 이상치는 통상적으로 박스(사분위수 범위)의 위아래에 위치한 점으로 표시됩니다.
#!pip install matplotlib #matplotlib 설치
import matplotlib.pyplot as plt
# 박스플롯으로 이상치 시각화
plt.boxplot(df['age'])
plt.title('age')
plt.show()
#박스에서 확연히 벗어난 값을 확인할 수 있음
- IQR (Inter Quartile Range): 1사분위수(Q1)와 3사분위수(Q3)의 차이로, 이 범위를 벗어나는 데이터를 이상치로 간주
√ 대표적인 방법이지만 데이터의 특성을 고려하지 않고 규칙을 고정적으로 사용하기 때문에 사용할 때 주의가 필요.
데이터 분석이 가능하지 않아 이상치 처리가 어려울 때에만 사용하는 것을 추천
#IQR 계산
Q1 = df['age'].quantile(0.25)
Q3 = df['age'].quantile(0.75)
IQR = Q3 - Q1
#IQR을 이용한 이상치 탐지
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
#범위를 벗어나는 데이터 확인
outliers = df[(df['age'] < lower_bound) | (df['age'] > upper_bound)]
print(outliers)
#----------아래는 결과------------
name age score
3 lucy 120 80
이상치 처리
- 처리하지 않고 유지: 이 상치가 분석 포인트가 될 수 있다면 별도의 처리를 하지 않고 넘어감
- 이상치 제거: 이상치가 있는 행 제거 또는 IQR로 확인한 범위에 속하지 않는 데이터 제거
#IQR 범위를 벗어나는 데이터를 제거 해 새로운 데이터프레임 생성
df_without_outliers = df[(df['age'] >= lower_bound) & (df['age'] <= upper_bound)]
print(df_without_outliers)
#lucy행이 제거됨
#----------아래는 결과------------
name age score
0 bob 25 90
1 alice 30 85
2 mung 22 95
- 특정 값으로 대체: 평균/중앙값/최빈값 등으로 채울 수 있음
median_age = df['age'].median() #중앙값
#나이 열의 값이 IQR 범위를 벗어나면(이상치라고 판단되면) 중앙값으로 대체
df['age'] = df['age'].apply(lambda x: median_age if x > upper_bound or x < lower_bound else x)
print(df)
#lucy의 나이가 120에서 27.5로 변경됨
#----------아래는 결과------------
name age score
0 bob 25.0 90
1 alice 30.0 85
2 mung 22.0 95
3 lucy 27.5 80