Do it! 쉽게 배우는 파이썬 데이터 분석

07 데이터 정제
- 빠진 데이터, 이상한 데이터 제거하기




목차

07-1 빠진 데이터를 찾아라! - 결측치 정제하기(link)

07-2 이상한 데이터를 찾아라! - 이상치 정제하기(link)

07-1 빠진 데이터를 찾아라!
- 결측치 정제하기

결측치(missing value)

  • 누락된 값, 비어 있는 값
  • 데이터 수집 과정에서 발생한 오류로 포함될 가능성
  • 함수가 적용되지 않거나 분석 결과가 왜곡되는 문제 발생
  • 실제 데이터 분석시 결측치 확인, 제거 후 분석해야 함

결측치 찾기

결측치 만들기
  • NumPy 패키지의 np.nan 입력
  • 파이썬에서 결측치는 NaN으로 표시
  • 불러온 데이터 파일에 결측치가 있으면 자동으로 NaN이 됨
import pandas as pd
import numpy as np
df = pd.DataFrame({'sex'   : ['M', 'F', np.nan, 'M', 'F'],
                   'score' : [5, 4, 3, 4, np.nan]})
df
   sex  score
0    M    5.0
1    F    4.0
2  NaN    3.0
3    M    4.0
4    F    NaN
NaN 있는 상태로 연산하면 출력 결과도 NaN
df['score'] + 1
0    6.0
1    5.0
2    4.0
3    5.0
4    NaN
Name: score, dtype: float64

결측치 확인하기

pd.isna(df)  # 결측치 확인
     sex  score
0  False  False
1  False  False
2   True  False
3  False  False
4  False   True
결측치 빈도 확인
pd.isna(df).sum()  # 결측치 빈도 확인
sex      1
score    1
dtype: int64

결측치 제거하기

결측치 있는 행 제거하기

df.dropna(subset = 'score')  # score 결측치 제거
   sex  score
0    M    5.0
1    F    4.0
2  NaN    3.0
3    M    4.0
df_nomiss = df.dropna(subset = 'score')  # score 결측치 제거된 데이터 만들기
df_nomiss['score'] + 1                   # score로 연산
0    6.0
1    5.0
2    4.0
3    5.0
Name: score, dtype: float64

여러 변수에 결측치 없는 데이터 추출하기

df_nomiss = df.dropna(subset = ['score', 'sex'])  # score, sex 결측치 제거
df_nomiss
  sex  score
0   M    5.0
1   F    4.0
3   M    4.0

결측치가 하나라도 있으면 제거하기

  • df.dropna()에 아무 변수도 지정하지 않음
  • 모든 변수에 결측치가 없는 행만 남김
df_nomiss2 = df.dropna()  # 모든 변수에 결측치 없는 데이터 추출
df_nomiss2
  sex  score
0   M    5.0
1   F    4.0
3   M    4.0
  • 간편하지만 분석에 사용할 수 있는 데이터까지 제거될 수 있음
    • ex) 성별, 소득, 지역 변수가 있을 때, 성별에 따른 소득 차이 분석 목적
    • 지역이 결측치면 분석하는데 문제 없으나 제거됨
  • 분석에 사용할 변수 직접 지정하여 결측치 제거하는 방법 권장

결측치 제거하지 않고 분석하기

  • pd.mean(), pd.sum()은 결측치 있으면 자동으로 제거하고 연산함
  • groupby(), agg()도 결측치 제거하고 연산함
df['score'].mean()
4.0
df['score'].sum()
16.0

df.groupby('sex').agg(mean_score = ('score', 'mean'),
                      sum_score  = ('score', 'sum'))
     mean_score  sum_score
sex                       
F           4.0        4.0
M           4.5        9.0

  • 자동 결측치 제거 기능은 편리하나, 결측치가 있는지 모른채 데이터 다루게 되는 위험 있음
  • 직접 결측치 확인 후 df.dropna()로 명시적으로 제거 권장

결측치 대체하기

  • 결측치가 적고 데이터가 크면 결측치 제거 후 분석 가능
  • 데이터가 작고 결측치 많으면 데이터 손실로 인해 분석 결과 왜곡 발생
  • 결측치 대체법을 이용하면 보완 가능

  • 결측치 대체법(imputation): 결측치를 제거하는 대신 다른 값을 채워 넣는 방법
    • 대표값(평균값, 최빈값 등)을 구해 일괄 대체
    • 통계 분석 기법으로 결측치의 예측값 추정 후 대체

평균값으로 결측치 대체하기

exam = pd.read_csv('exam.csv')           # 데이터 불러오기
exam.loc[[2, 7, 14], ['math']] = np.nan  # 2, 7, 14행의 math에 NaN 할당
exam

    id  nclass  math  english  science
0    1       1  50.0       98       50
1    2       1  60.0       97       60
2    3       1   NaN       86       78
3    4       1  30.0       98       58
4    5       2  25.0       80       65
5    6       2  50.0       89       98
6    7       2  80.0       90       45
7    8       2   NaN       78       25
8    9       3  20.0       98       15
9   10       3  50.0       98       45
10  11       3  65.0       65       65
11  12       3  45.0       85       32
12  13       4  46.0       98       65
13  14       4  48.0       87       12
14  15       4   NaN       56       78
15  16       4  58.0       98       65
16  17       5  65.0       68       98
17  18       5  80.0       78       90
18  19       5  89.0       68       87
19  20       5  78.0       83       58
평균값 구하기
exam['math'].mean()
55.23529411764706
df.fillna()로 결측치를 평균값으로 대체하기
exam['math'] = exam['math'].fillna(55)  # math가 NaN이면 55로 대체
exam                                    # 출력
    id  nclass  math  english  science
0    1       1  50.0       98       50
1    2       1  60.0       97       60
2    3       1  55.0       86       78
3    4       1  30.0       98       58
4    5       2  25.0       80       65
5    6       2  50.0       89       98
6    7       2  80.0       90       45
7    8       2  55.0       78       25
8    9       3  20.0       98       15
9   10       3  50.0       98       45
10  11       3  65.0       65       65
11  12       3  45.0       85       32
12  13       4  46.0       98       65
13  14       4  48.0       87       12
14  15       4  55.0       56       78
15  16       4  58.0       98       65
16  17       5  65.0       68       98
17  18       5  80.0       78       90
18  19       5  89.0       68       87
19  20       5  78.0       83       58
df.fillna()로 결측치를 평균값으로 대체하기
exam['math'].isna().sum()  # 결측치 빈도 확인
0

혼자서 해보기

결측치가 들어 있는 mpg 데이터를 이용해 분석 문제를 해결해 보세요.

mpg 데이터 원본에는 결측치가 없습니다. 우선 mpg 데이터를 불러와 일부러 몇 개의 값을 결측치로
만들겠습니다. 다음 코드를 실행하면 다섯 행의 hwy 변수에 NaN을 할당합니다.

mpg = pd.read_csv('mpg.csv')                       # mpg 데이터 불러오기
mpg.loc[[64, 123, 130, 152, 211], 'hwy'] = np.nan  # NaN 할당하기

Q1. drv(구동 방식)별로 hwy(고속도로 연비) 평균이 어떻게 다른지 알아보려고 합니다. 분석을 하기
       전에 우선 두 변수에 결측치가 있는지 확인해야 합니다. drv 변수와 hwy 변수에 결측치가 몇 개
       있는지 알아보세요.

Q2. df.dropna()를 이용해 hwy 변수의 결측치를 제거하고, 어떤 구동 방식의 hwy 평균이 높은지
       알아보세요. 하나의 pandas 구문으로 만들어야 합니다.

Q1. drv(구동 방식)별로 hwy(고속도로 연비) 평균이 어떻게 다른지 알아보려고 합니다. 분석을 하기
       전에 우선 두 변수에 결측치가 있는지 확인해야 합니다. drv 변수와 hwy 변수에 결측치가 몇 개
       있는지 알아보세요.

# 결측치 빈도 확인
mpg[['drv', 'hwy']].isna().sum()
drv    0
hwy    5
dtype: int64

Q2. df.dropna()를 이용해 hwy 변수의 결측치를 제거하고, 어떤 구동 방식의 hwy 평균이 높은지
       알아보세요. 하나의 pandas 구문으로 만들어야 합니다.

mpg.dropna(subset = ['hwy']) \
   .groupby('drv') \
   .agg(mean_hwy = ('hwy', 'mean'))
      mean_hwy
drv           
4    19.242424
f    28.200000
r    21.000000
# hwy 결측치 제거
# drv별 분리
# hwy 평균 구하기

07-2 이상한 데이터를 찾아라!
- 이상치 정제하기

이상치(anomaly): 정상 범위에서 크게 벗어난 값
  • 실제 데이터에 대부분 이상치 들어있음
  • 제거하지 않으면 분석 결과 왜곡되므로 분석 전에 제거 작업 필요

이상치 제거하기 - 존재할 수 없는 값

  • 논리적으로 존재할 수 없는 값이 있을 경우 결측치로 변환 후 제거
    • ex) 성별 변수에 1(남성), 2(여성) 외 3이 있다면 3을 NaN으로 변환


이상치가 들어있는 데이터 만들기
df = pd.DataFrame({'sex'   : [1, 2, 1, 3, 2, 1],
                   'score' : [5, 4, 3, 4, 2, 6]})
df
   sex  score
0    1      5
1    2      4
2    1      3
3    3      4
4    2      2
5    1      6

이상치 확인하기

빈도표를 만들어 존재할 수 없는 값이 있는지 확인
df['sex'].value_counts(sort = False).sort_index()
sex
1    3
2    2
3    1
Name: count, dtype: int64
df['score'].value_counts(sort = False).sort_index()
score
2    1
3    1
4    2
5    1
6    1
Name: count, dtype: int64

결측 처리하기

이상치일 경우 NaN 부여
# sex가 3이면 NaN 부여
df['sex'] = np.where(df['sex'] == 3, np.nan, df['sex'])
df
   sex  score
0  1.0      5
1  2.0      4
2  1.0      3
3  NaN      4
4  2.0      2
5  1.0      6

결측 처리하기

이상치일 경우 NaN 부여
# score가 5보다 크면 NaN 부여
df['score'] = np.where(df['score'] > 5, np.nan, df['score'])
df
   sex  score
0  1.0    5.0
1  2.0    4.0
2  1.0    3.0
3  NaN    4.0
4  2.0    2.0
5  1.0    NaN
결측치 제거 후 분석
df.dropna(subset = ['sex', 'score']) \
       .groupby('sex') \
       .agg(mean_score = ('score', 'mean'))
     mean_score
sex            
1.0         4.0
2.0         3.0
# sex, score 결측치 제거
# sex별 분리
# score 평균 구하기

np.where()는 문자와 NaN을 함께 반환할 수 없습니다.

  • np.where() 사용할 때 반환 값 중 문자가 있으면 np.nan 지정해도 문자 'nan' 반환
  • 결측치 확인하면 모든 값이 False로 나타남
df = pd.DataFrame({'x1' : [1, 1, 2, 2]})
df['x2'] = np.where(df['x1'] == 1, 'a', np.nan)  # 조건에 맞으면 문자 부여

df
   x1   x2
0   1    a
1   1    a
2   2  nan
3   2  nan
df.isna()
      x1     x2
0  False  False
1  False  False
2  False  False
3  False  False


변수를 문자와 NaN으로 함께 구성하는 방법

  1. 결측치로 만들 값에 문자 부여

# 결측치로 만들 값에 문자 부여
df['x2'] = np.where(df['x1'] == 1, 'a', 'etc')
  1. df.replace()를 이용해 결측치로 만들 문자를 np.nan으로 바꾸기

# 'etc'를 NaN으로 바꾸기
df['x2'] = df['x2'].replace('etc', np.nan)

df
   x1   x2
0   1    a
1   1    a
2   2  NaN
3   2  NaN
df.isna()
      x1     x2
0  False  False
1  False  False
2  False   True
3  False   True

이상치 제거하기 - 극단적인 값

극단치(outlier): 논리적으로 존재할 수 있지만 극단적으로 크거나 작은 값
  • 극단치 있으면 분석 결과 왜곡될 수 있으므로 제거 후 분석
  • 기준 정하기
    • 논리적 판단(ex: 성인 몸무게 40~150kg 벗어나면 매우 드물므로 극단치로 간주)
    • 통계적 기준(ex: 상하위 0.3% 또는 ±3 표준편차 벗어나면 극단치로 간주)
    • 상자 그림(box plot)을 이용해 중심에서 크게 벗어난 값을 극단치로 간주

상자 그림으로 극단치 기준 정하기

상자 그림(box plot): 데이터의 분포를 상자 모양으로 표현한 그래프
  • 중심에서 멀리 떨어진 값을 점으로 표현
  • 상자 그림을 이용해 극단치 기준 구할 수 있음

1. 상자 그림 살펴보기

mpg = pd.read_csv('mpg.csv')

import seaborn as sns
sns.boxplot(data = mpg, y = 'hwy')

• IQR(사분위 범위): 1사분위수와 3사분위수의 거리 • 1.5 IQR: IQR의 1.5배

2. 극단치 기준값 구하기

(1) 1사분위수, 3사분위수 구하기

pct25 = mpg['hwy'].quantile(.25)
pct25
18.0
pct75 = mpg['hwy'].quantile(.75)
pct75
27.0

(2) IQR 구하기

iqr = pct75 - pct25
iqr
9.0

2. 극단치 기준값 구하기

(3) 하한, 상한 구하기

pct25 - 1.5 * iqr  # 하한
4.5
pct75 + 1.5 * iqr  # 상한
40.5

3. 극단치를 결측 처리하기

# 4.5 ~ 40.5 벗어나면 NaN 부여
mpg['hwy'] = np.where((mpg['hwy'] < 4.5) | (mpg['hwy'] > 40.5), np.nan, mpg['hwy'])

# 결측치 빈도 확인
mpg['hwy'].isna().sum()
3

4. 결측치 제거하고 분석하기

mpg.dropna(subset = ['hwy']) \
   .groupby('drv') \
   .agg(mean_hwy = ('hwy', 'mean'))
      mean_hwy
drv           
4    19.174757
f    27.728155
r    21.000000
# hwy 결측치 제거
# drv별 분리
# hwy 평균 구하기

혼자서 해보기

이상치가 들어 있는 mpg 데이터를 이용해 분석 문제를 해결해 보세요.

우선 mpg 데이터를 불러와 일부러 이상치를 만들겠습니다. drv(구동 방식) 변수의 값은 4(사륜구동), f(전륜구동), r(후륜구동) 세 종류입니다. 몇 개의 행에 존재할 수 없는 값 k를 할당하겠습니다. cty(도시 연비) 변수도 몇 개의 행에 극단적으로 크거나 작은 값을 할당하겠습니다.

mpg = pd.read_csv('mpg.csv')                         # mpg 데이터 불러오기
mpg.loc[[9, 13, 57, 92], 'drv'] = 'k'                # drv 이상치 할당
mpg.loc[[28, 42, 128, 202], 'cty'] = [3, 4, 39, 42]  # cty 이상치 할당

혼자서 해보기

구동 방식별로 도시 연비가 어떻게 다른지 알아보려고 합니다. 분석을 하기 전에 우선 두 변수에
이상치가 있는지 확인하려고 합니다.

Q1. drv에 이상치가 있는지 확인하세요. 이상치를 결측 처리한 다음 이상치가 사라졌는지
       확인하세요. 결측 처리를 할 때는 df.isin()을 활용하세요.

Q2. 상자 그림을 이용해 cty에 이상치가 있는지 확인하세요. 상자 그림 기준으로 정상 범위를 벗어난
       값을 결측 처리한 다음 다시 상자 그림을 만들어 이상치가 사라졌는지 확인하세요

Q3. 두 변수의 이상치를 결측 처리했으니 이제 분석할 차례입니다. 이상치를 제거한 다음 drv별로
        cty 평균이 어떻게 다른지 알아보세요. 하나의 pandas 구문으로 만들어야 합니다.

Q1. drv에 이상치가 있는지 확인하세요. 이상치를 결측 처리한 다음 이상치가 사라졌는지
       확인하세요. 결측 처리를 할 때는 df.isin()을 활용하세요.

# 이상치 확인
mpg['drv'].value_counts().sort_index()
drv
4    100
f    106
k      4
r     24
Name: count, dtype: int64
# drv가 4, f, r이면 기존 값 유지, 그 외 NaN 부여
mpg['drv'] = np.where(mpg['drv'].isin(['4', 'f', 'r']), mpg['drv'], np.nan)

# 이상치 확인
mpg['drv'].value_counts().sort_index()
drv
4    100
f    106
r     24
Name: count, dtype: int64

Q2. 상자 그림을 이용해 cty에 이상치가 있는지 확인하세요. 상자 그림 기준으로 정상 범위를 벗어난
       값을 결측 처리한 다음 다시 상자 그림을 만들어 이상치가 사라졌는지 확인하세요

# 상자 그림 만들기
sns.boxplot(data = mpg, y = 'cty')

Q2. 상자 그림을 이용해 cty에 이상치가 있는지 확인하세요. 상자 그림 기준으로 정상 범위를 벗어난
       값을 결측 처리한 다음 다시 상자 그림을 만들어 이상치가 사라졌는지 확인하세요

pct25 = mpg['cty'].quantile(.25)  # 1사분위수
pct75 = mpg['cty'].quantile(.75)  # 3사분위수
iqr = pct75 - pct25               # IQR
pct25 - 1.5 * iqr  # 하한
6.5
pct75 + 1.5 * iqr  # 상한
26.5

Q2. 상자 그림을 이용해 cty에 이상치가 있는지 확인하세요. 상자 그림 기준으로 정상 범위를 벗어난
       값을 결측 처리한 다음 다시 상자 그림을 만들어 이상치가 사라졌는지 확인하세요

# 6.5 ~ 26.5 벗어나면 NaN 부여
mpg['cty'] = np.where((mpg['cty'] < 6.5) | (mpg['cty'] > 26.5), np.nan, mpg['cty'])

# 상자 그림 만들기
sns.boxplot(data = mpg, y = 'cty')

Q3. 두 변수의 이상치를 결측 처리했으니 이제 분석할 차례입니다. 이상치를 제거한 다음 drv별로
        cty 평균이 어떻게 다른지 알아보세요. 하나의 pandas 구문으로 만들어야 합니다.

mpg.dropna(subset = ['drv', 'cty']) \
   .groupby('drv') \
   .agg(mean_cty = ('cty', 'mean'))
      mean_cty
drv           
4    14.247423
f    19.470000
r    13.958333
# drv, cty 결측치 제거
# drv별 분리
# cty 평균 구하기

정리하기

## 1. 결측치 정제하기
pd.isna(df).sum()                                 # 결측치 확인
df_nomiss = df.dropna(subset = 'score' )          # 결측치 제거
df_nomiss = df.dropna(subset = ['score', 'sex'])  # 여러 변수 동시에 결측치 제거

정리하기

## 2. 이상치 정제하기

# 이상치 확인
df['sex'].value_counts(sort = False)

# 이상치 결측 처리
df['sex'] = np.where(df['sex'] == 3, np.nan, df['sex'])

# 상자 그림으로 극단치 기준값 찾기
pct25 = mpg['hwy'].quantile(.25)  # 1사분위수
pct75 = mpg['hwy'].quantile(.75)  # 3사분위수
iqr = pct75 - pct25               # IQR
pct25 - 1.5 * iqr                 # 하한
pct75 + 1.5 * iqr                 # 상한

# 극단치 결측 처리
mpg['hwy'] = np.where((mpg['hwy'] < 4.5) | (mpg['hwy'] > 40.5), np.nan, mpg['hwy'])