카테고리 없음

데이터 전처리_결측치, 이상치

멍주다배 2024. 11. 28.

결측치(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