[Pandas 기초] 결측치(NaN), 중복 데이터 처리

업데이트:

개요

jpg

이번 포스팅에서는 데이터의 결측치(누락 데이터)중복 데이터를 처리하는 방법에 대해 알아보자. 결측값 대체에 대한 다양한 방법론과 이론들이 존재하지만 여기서는 테크닉 적으로 어떻게 대체하는지에 대한 방법만을 얘기하도록 한다.

예제 데이터로는 타이타닉 데이터를 활용.

import seaborn as sns
import pandas as pd
import numpy as np

df = sns.load_dataset('titanic')
df.head()
survived pclass sex age sibsp parch fare embarked class who adult_male deck embark_town alive alone
0 0 3 male 22.0 1 0 7.2500 S Third man True NaN Southampton no False
1 1 1 female 38.0 1 0 71.2833 C First woman False C Cherbourg yes False
2 1 3 female 26.0 0 0 7.9250 S Third woman False NaN Southampton yes True
3 1 1 female 35.0 1 0 53.1000 S First woman False C Southampton yes False
4 0 3 male 35.0 0 0 8.0500 S Third man True NaN Southampton no True


1. 결측치(누락 데이터) 처리

1-1. 데이터 탐색

앞의 데이터 탐색 포스팅에서 봤던 함수들을 이용해 타이타닉 데이터를 간단하게 살펴보자.

df.info()
[Output]
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 891 entries, 0 to 890
Data columns (total 15 columns):
survived       891 non-null int64
pclass         891 non-null int64
sex            891 non-null object
age            714 non-null float64
sibsp          891 non-null int64
parch          891 non-null int64
fare           891 non-null float64
embarked       889 non-null object
class          891 non-null category
who            891 non-null object
adult_male     891 non-null bool
deck           203 non-null category
embark_town    889 non-null object
alive          891 non-null object
alone          891 non-null bool
dtypes: bool(2), category(2), float64(2), int64(4), object(5)
memory usage: 80.6+ KB

총 15개의 변수(컬럼), 891개의 데이터(row)를 가지고 있고 age, deck, embarked ,embark_town 열에 결측치가 존재함을 알 수 있다.

deck열은 범주형(category) 변수이므로 value_counts함수를 이용해 고유값의 개수를 확인하자.
여기서 dropna=False 옵션(default는 True)을 주면 누락데이터(NaN)의 개수도 함께 카운트해 준다.

df['deck'].value_counts(dropna = False)  

[Output]
NaN    688
C       59
B       47
D       33
E       32
A       15
F       13
G        4
Name: deck, dtype: int64

1-2. 결측치 탐색 : isnull(), notnull()

isnull()함수는 판다스 데이터프레임 및 시리즈의 결측치(NaN)를 탐색해 결측치에 대해 True를 반환해주고 notnull()은 그 반대이므로 둘중 하나의 함수만 이용해도 충분하다고 생각한다.

df.head().isnull()
survived pclass sex age sibsp parch fare embarked class who adult_male deck embark_town alive alone
0 False False False False False False False False False False False True False False False
1 False False False False False False False False False False False False False False False
2 False False False False False False False False False False False True False False False
3 False False False False False False False False False False False False False False False
4 False False False False False False False False False False False True False False False
df.head().notnull()
survived pclass sex age sibsp parch fare embarked class who adult_male deck embark_town alive alone
0 True True True True True True True True True True True False True True True
1 True True True True True True True True True True True True True True True
2 True True True True True True True True True True True False True True True
3 True True True True True True True True True True True True True True True
4 True True True True True True True True True True True False True True True

여기서 True에 대해 연산(sum)하면 참은 1로 처리를 해주기 때문에, sum()함수를 데이터프레임에 적용해 컬럼별 결측치의 수를 계산해보자.

df.isnull().sum()
[Output]
survived         0
pclass           0
sex              0
age            177
sibsp            0
parch            0
fare             0
embarked         2
class            0
who              0
adult_male       0
deck           688
embark_town      2
alive            0
alone            0
dtype: int64

이 결과는 앞의 df.info()를 이용해 확인해서 총 row수에 대해 not null인 row 수를 고려해서 구할 수도 있다. 이외에도 결측치를 탐색하는 방법은 다양하게 존재한다.

1.3 결측치(누락데이터) 제거 : dropna()

이번에는 dropna()함수를 이용해 결측치를 제거해보자.

df.dropna().info()
[Output]
<class 'pandas.core.frame.DataFrame'>
Int64Index: 182 entries, 1 to 889
Data columns (total 15 columns):
survived       182 non-null int64
pclass         182 non-null int64
sex            182 non-null object
age            182 non-null float64
sibsp          182 non-null int64
parch          182 non-null int64
fare           182 non-null float64
embarked       182 non-null object
class          182 non-null category
who            182 non-null object
adult_male     182 non-null bool
deck           182 non-null category
embark_town    182 non-null object
alive          182 non-null object
alone          182 non-null bool
dtypes: bool(2), category(2), float64(2), int64(4), object(5)
memory usage: 18.2+ KB

아무옵션 없이 dropna()함수를 적용한 후에 요약정보를 확인해보면, 하나라도 결측치(NaN)가 존재하면 그 row는 삭제해버리는 것을 알 수 있다.

axis=1옵션

df.dropna(axis=1).info()
[Output]
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 891 entries, 0 to 890
Data columns (total 11 columns):
survived      891 non-null int64
pclass        891 non-null int64
sex           891 non-null object
sibsp         891 non-null int64
parch         891 non-null int64
fare          891 non-null float64
class         891 non-null category
who           891 non-null object
adult_male    891 non-null bool
alive         891 non-null object
alone         891 non-null bool
dtypes: bool(2), category(1), float64(1), int64(4), object(3)
memory usage: 58.5+ KB

axis=1은 열에 대해 적용하라는 뜻이므로, 결측치(NaN)가 존재하는 열은 삭제해버려서, 컬럼수가 15개에서 11개로 줄어든 것을 볼 수 있다.

thresh=500옵션

이는 결측치(NaN)가 500개 이상인 열을 모두 삭제해버리라는 옵션이다.
또한 열에 대해 적용해야 하므로 axis=1옵션을 추가해 주어야한다. 생략하거나 axis=0이라면 한 row에 대해 결측치가 500개 이상이라면 그 row를 드랍한다.(변수가 500개일 일은 드물지만..)

df_thresh = df.dropna(axis = 1, thresh = 500)
df_thresh.info()
[Output]
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 891 entries, 0 to 890
Data columns (total 14 columns):
survived       891 non-null int64
pclass         891 non-null int64
sex            891 non-null object
age            714 non-null float64
sibsp          891 non-null int64
parch          891 non-null int64
fare           891 non-null float64
embarked       889 non-null object
class          891 non-null category
who            891 non-null object
adult_male     891 non-null bool
embark_town    889 non-null object
alive          891 non-null object
alone          891 non-null bool
dtypes: bool(2), category(1), float64(2), int64(4), object(5)
memory usage: 79.4+ KB

결측치(NaN)가 500개 미만인 다른 열들은 남아있다.

subset = ['age'] 옵션

subset = ['age']옵션은 데이터프레임의 age열에 결측값이 1개라도 있으면 그 행을 drop하라는 옵션이다.
how = 'any'axis=0은 default값으로 생략이 가능하다. 특히 how='any'all로 줄수도 있는데 결측값이 모든 열또는 행에 존재해야 drop하라는 의미이다.

df_age = df.dropna(subset = ['age'], how = 'any', axis=0)
print(len(df_age), '/n')
print(df_age['age'].isnull().sum())
[Output]
714

0

실제로 age열에 결측값이 있는 row에 대해 제거해주었으므로 row는 714(age열의 온전한 row 수)이고, 해당 열의 결측치는 없는걸 확인했다.

1-4. 누락 데이터 치환 : fillna()

이번에는 결측값을 제거하지 않고 다른 어떤 값으로 대체해주는 방법을 알아보자.
다시 타이타닉 데이터를 로드한다.

import seaborn as sns
df = sns.load_dataset('titanic')

결측값을 대체해주는 함수는 fillna()함수이다. fillna(결측값을 대체할 값)의 형태로 간단하게 사용한다.

평균값(mean)으로 대체

print("**변경 전**")
print(df['age'].head(10))

mean_age = df['age'].mean()
df['age'].fillna(mean_age, inplace = True)

print('\n')
print("**변경 후**")
print(df['age'].head(10))
[Output]
**변경 전**
0    22.0
1    38.0
2    26.0
3    35.0
4    35.0
5     NaN
6    54.0
7     2.0
8    27.0
9    14.0
Name: age, dtype: float64


**변경 후**
0    22.000000
1    38.000000
2    26.000000
3    35.000000
4    35.000000
5    29.699118
6    54.000000
7     2.000000
8    27.000000
9    14.000000
Name: age, dtype: float64

결측값이었던 index 5가 평균값(29.699118)으로 치환된 것 확인했다.

최빈값(top_freq)으로 대체

embark_town열 3개의 범주를 가진 범주형 변수로 2개의 결측치가 있는 열이었다.

df['embark_town'].value_counts()
[Output]
Southampton    644
Cherbourg      168
Queenstown      77
Name: embark_town, dtype: int64

해당 결측치 2개가 데이터프레임 내 어느 index(row번호)에 해당하는지 찾는다.

df.index[df['embark_town'].isnull()] 
[Output]
Int64Index([61, 829], dtype='int64')

직접 확인해보자.

print(df.loc[61])
print('\n')
print(df.loc[829])
[Output]
survived            1
pclass              1
sex            female
age                38
sibsp               0
parch               0
fare               80
embarked          NaN
class           First
who             woman
adult_male      False
deck                B
embark_town       NaN
alive             yes
alone            True
Name: 61, dtype: object


survived            1
pclass              1
sex            female
age                62
sibsp               0
parch               0
fare               80
embarked          NaN
class           First
who             woman
adult_male      False
deck                B
embark_town       NaN
alive             yes
alone            True
Name: 829, dtype: object

이제 이 2개의 결측치를 세가지 범주(category)중 최빈값으로 대체하는 두가지 방법을 소개한다.

print(df['embark_town'].describe()['top'])
print(df['embark_town'].value_counts(dropna=True).idxmax())
[Output]
'Southampton'
'Southampton'
  1. decribe()데이터 탐색 포스팅에서도 설명한바 있지만, 아쉽게도 이 함수는 데이터프레임 전체에 대해 적용(df.describe())하면 문자열과 숫자형의 개별 통계를 제공하지 않는다. 무튼 범주형열에 대해 적용해서 얻게되는 top(최빈값)을 사용할 수 있다.

  2. idxmax()함수는 데이터 프레임의 특정열(시리즈)에 대하여 가장 값이 큰 row의 index를 반환한다. 따라서 value_countS()함수로 고유값의 개수를 구하고, 최대값을 가지는 인덱스(범주명)를 불러와 준다.

그럼 2번째 방법으로 fillna()함수에 적용해 치환해보자.

most_freq = df['embark_town'].value_counts(dropna=True).idxmax()
df['embark_town'].fillna(most_freq, inplace = True)

print(df.loc[61])
print('\n')
print(df.loc[829])
[Output]
survived                 1
pclass                   1
sex                 female
age                     38
sibsp                    0
parch                    0
fare                    80
embarked               NaN
class                First
who                  woman
adult_male           False
deck                     B
embark_town    Southampton
alive                  yes
alone                 True
Name: 61, dtype: object


survived                 1
pclass                   1
sex                 female
age                     62
sibsp                    0
parch                    0
fare                    80
embarked               NaN
class                First
who                  woman
adult_male           False
deck                     B
embark_town    Southampton
alive                  yes
alone                 True
Name: 829, dtype: object

원래 결측값(NaN)이 었던 61, 829행은 Southampton으로 치환되었음을 확인했다.

이웃하고 있는 값으로 대체

데이터를 다시 로드해서 embark_town열을 최빈값(top)이 아닌 이웃값으로 대체해보자.

method = 'ffill'은 결측값(NaN)의 바로 앞의 값으로, method = 'bfill'은 결측값(NaN)의 바로 뒤의 값으로 바꿔주는 옵션이다.

import seaborn as sns
df = sns.load_dataset('titanic')

print("** 치환 전**")
print(df['embark_town'][825:830])
print('\n')

df['embark_town'].fillna(method = 'ffill', inplace = True)

print("** 치환 후**")
print(df['embark_town'][825:830])

[Output]
** 치환 전**
825     Queenstown
826    Southampton
827      Cherbourg
828     Queenstown
829            NaN
Name: embark_town, dtype: object


** 치환 후**
825     Queenstown
826    Southampton
827      Cherbourg
828     Queenstown
829     Queenstown
Name: embark_town, dtype: object


2. 중복 데이터

2-1. 중복 데이터 탐색 : duplicated()

import pandas as pd 
df = pd.DataFrame({'c1' : ['a','a', 'b', 'a', 'b'],
                  'c2' : [1, 1, 1, 2, 2],
                   'c3' : [1, 1, 2, 2, 2]})
df
c1 c2 c3
0 a 1 1
1 a 1 1
2 b 1 2
3 a 2 2
4 b 2 2

duplicated()함수는 row마다 중복값을 검사해주는 함수이다.
아무 옵션을 적용하지 않으면 모든 컬럼(c1,c2,c3)에 대해 한 row마다 중복된 row인지 아닌지를 판별해준다. 첫번째 row가 나오고 모두 같은 row가 한번 더 나올때, 그 row에 True를 반환한다.

df.duplicated()
[Output]
0    False
1     True
2    False
3    False
4    False
dtype: bool

헷갈릴 수 있는 duplicated()개념

  1. 전에 나온 행들과 비교하여 중복되는 행이면 True, 아니면 False
  2. 첫 번째(0번째) 값은 항상 False
  3. 바로 전 행과만 비교하는게 아니라 처음 나오는 행인지, 나왔던 행인지를 비교
  4. 즉, False의 개수가 유니크한 값의 개수

이 함수는 시리즈(데이터 프레임의 열)에 대해서도 적용이 가능하다. 다음 예를 보자

test = pd.Series([3,1,2,1,2,2,3,])
test.duplicated()
[Output]
0    False
1    False
2    False
3     True
4     True
5     True
6     True
dtype: bool

그러므로 당연히 데이터프레임의 하나의 열에도 적용이 가능하다.

col_dup = df['c2'].duplicated()
col_dup
[Output]
0    False
1     True
2     True
3    False
4     True
Name: c2, dtype: bool

2-2. 중복데이터 제거 : drop_duplicates()

이제 탐색한 중복데이터를 처리하는 방법을 알아보자

import pandas as pd 

df = pd.DataFrame({'c1' : ['a','a', 'b', 'a', 'b'],
                  'c2' : [1, 1, 1, 2, 2],
                   'c3' : [1, 1, 2, 2, 2]})
df
c1 c2 c3
0 a 1 1
1 a 1 1
2 b 1 2
3 a 2 2
4 b 2 2

drop_duplicates()함수는 방금 duplicated()함수에서 True를 반환했던 row를 제거해주는 역할을 한다.
즉, 중복행을 제거해주는 함수이다.

df2 = df.drop_duplicates()
df2
c1 c2 c3
0 a 1 1
2 b 1 2
3 a 2 2
4 b 2 2

subset = ['c2','c3'] 옵션

이 옵션은 특정 열을 기준으로 중복행 제거하라는 의미이다. c2,c3열만 봤을 때는 1,4행이 중복되서 나왔으므로 이 row들이 제거된다.

df3 = df.drop_duplicates(subset = ['c2','c3'])
df3
c1 c2 c3
0 a 1 1
2 b 1 2
3 a 2 2

추가로 inplace=True옵션을 적용해 처리한 데이터를 바로 데이터프레임에 적용할 수 있다.


Reference

도서 [파이썬 머신러닝 판다스 데이터 분석]을 공부하며 작성하였습니다.

댓글남기기