[Python] Pandas

5 minute read

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

개요

이번 포스팅에서는 데이터의 결측치(NaN)와 중복 데이터를 어떻게 처리하는지에 대해서 알아보겠습니다.

예제 데이터로는 데이터 분석 프로젝트에서 사용한 게임 데이터를 사용하겠습니다.

df = pd.read_csv('/content/drive/MyDrive/vgames2.csv')
df.head(5)

1. 결측치 처리

1-1. 데이터 탐색(info(), value_counts())
df.info()

#output
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 16598 entries, 0 to 16597
Data columns (total 9 columns):
 #   Column       Non-Null Count  Dtype  
---  ------       --------------  -----  
 0   Name         16598 non-null  object 
 1   Platform     16598 non-null  object 
 2   Year         16327 non-null  float64
 3   Genre        16548 non-null  object 
 4   Publisher    16540 non-null  object 
 5   NA_Sales     16598 non-null  object 
 6   EU_Sales     16598 non-null  object 
 7   JP_Sales     16598 non-null  object 
 8   Other_Sales  16598 non-null  object 
dtypes: float64(1), object(8)
memory usage: 1.1+ MB

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

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

#output
Action          3305
Sports          2341
Misc            1734
Role-Playing    1483
Shooter         1308
Adventure       1280
Racing          1243
Platform         884
Simulation       865
Fighting         847
Strategy         680
Puzzle           578
NaN               50
Name: Genre, dtype: int64

Genre 컬럼에 결측값이 50개가 있는 것을 확인할 수 있다.

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

isnull()은 판다스의 시리즈 및 데이터프레임 내의 결측치를 탐색해 결측치에 대해 True를 반환해주고 notnull()은 그 반대이다.

df.head(5).isnull()

df.head(5).notnull()

여기서 컬럼별 결측치의 수를 확인하고 싶으면 파이썬 내장 함수인 sum을 다음과 같이 사용하면 된다. (df.isnull이 결측치를 true로 반환하기 때문에 sum이 true에 해당하는 데이터를 카운트해서 그 결과를 반환하게 된다.)

df.isnull().sum()
#output - 데이터프레임의 컬럼별로 결측값의 수를 반환
Name             0
Platform         0
Year           271
Genre           50
Publisher       58
NA_Sales         0
EU_Sales         0
JP_Sales         0
Other_Sales      0
dtype: int64
1-3. 결측치 제거(dropna())

아무 옵션 없이 dropna()를 사용하게 되면 결측치가 존재하는 모든 행을 삭제하게 된다.

df = df.dropna()

df.isnull.sum()
#output - 결측치가 모두 사라진 것을 확인할 수 있다. 
Name           0
Platform       0
Year           0
Genre          0
Publisher      0
NA_Sales       0
EU_Sales       0
JP_Sales       0
Other_Sales    0
dtype: int64

or 

df.info()
#output
<class 'pandas.core.frame.DataFrame'>
Int64Index: 16241 entries, 0 to 16597
Data columns (total 9 columns):
 \#   Column       Non-Null Count  Dtype  
---  ------       --------------  -----  
 0   Name         16241 non-null  object 
 1   Platform     16241 non-null  object 
 2   Year         16241 non-null  float64
 3   Genre        16241 non-null  object 
 4   Publisher    16241 non-null  object 
 5   NA_Sales     16241 non-null  object 
 6   EU_Sales     16241 non-null  object 
 7   JP_Sales     16241 non-null  object 
 8   Other_Sales  16241 non-null  object 
dtypes: float64(1), object(8)
memory usage: 1.2+ MB

axis = 1 옵션 사용

df.dropna(axis=1)
#output
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 16598 entries, 0 to 16597
Data columns (total 6 columns):

 \#   Column       Non-Null Count  Dtype 
---  ------       --------------  ----- 
 0   Name         16598 non-null  object
 1   Platform     16598 non-null  object
 2   NA_Sales     16598 non-null  object
 3   EU_Sales     16598 non-null  object
 4   JP_Sales     16598 non-null  object
 5   Other_Sales  16598 non-null  object
dtypes: object(6)
memory usage: 778.2+ KB

axis=1 옵션을 사용하게 되면 결측값이 하나라도 들어 있는 컬럼을 그냥 삭제하게 된다. 따라서 컬럼의 개수가 6개로 줄어든 것을 확인할 수 있다.

thresh=500 옵션 사용

thresh옵션을 사용하면, 예를 들어 thresh = 3이면 NaN이 아닌 값이 최소 3개 이상은 나와야 한다. 그거보다 적게 나오면 row를 제거해버린다.즉, 한 행에 10개의 컬럼 값이 있다면 그 중 3개 이상이 NaN이 아닌 값이 나와야 한다. 그렇지 않으면 그 행은 삭제된다. 만약 열을 기준으로 사용하고 싶다면 axis=1을 함께 사용하면 된다.

print(df.shape) #output: (16598, 10)
df = df.dropna(thresh = 10) 
# row에 10개 이상이 NaN이 아닌 값이여야 한다.
print(df.shape) #output: (16241, 10)

해당 데이터프레임중 357개의 행이 NaN의 값을 하나라도 포함하고 있다고   있다.

subset = [‘컬럼이름’]

데이터프레임의 ‘컬럼이름’ 열에 결측값이 1개라도 있으면 그 행을 drop한다. 동시에 주는 옵션은 ‘how’가 있는데 how = ‘any’는 default값이고 how = ‘all’로 주면 결측값이 모든 열 또는 행에 존재하면 drop하라는 의미이다.

print(df.shape) #output: (16598, 10)
df = df.dropna(subset=['Genre'])
print(df.shape) #output: (16548, 10)

해당 데이터프레임의 Genre열에 결측값이 50개가 있다는 것을 위의 결과로 확인할  있다. 
1-4. 결측치 데이터 치환(fillna())

결측값(NaN)을 제거하지 않고 어떤 다른 값으로 대체해주는 방법이다.

fillna()의 사용을 확인해보기 위해 타이타닉 데이터를 사용하겠다.

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

titanic 데이터 중 embark_town 컬럼의 데이터 분석

#embark_town 결측치 2개 
df['embark_town'].isnull().sum() #output: 2

#3개의 범주형 변수로 이루어진 데이터 
df['embark_town'].value_counts()
#output:
Southampton    644
Cherbourg      168
Queenstown      77
Name: embark_town, dtype: int64

평균값으로 대체

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

최빈값(주어진 자료 중 가장 많은 빈도로 나타나는 변량 또는 자료)으로 대체

2개의 결측치가 데이터프레임 내 어느 index에 있는지 확인

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

2개의 결측값을 해당 컬럼의 3가지 범주중 최빈값으로 대체하는 2가지 방법

1. df['embark_town'].describe()['top']
#output: Southampton

2. df['embark_town'].value_counts(dropna=True).idxmax()
#ouput: Southampton
  1. describe()[‘top’]를 사용하면 전체 데이터프레임에 대해서는 적용되지 않지만 특정 컬럼에 대해서 최빈값의 인덱스를 추출해준다.

  2. idxmax()는 데이터프레임의 특정열(시리즈)에 대해서 가장 값이 큰 row의 index를 반환한다. 그래서 value_counts로 series의 형태로 반환해주고 해당 시리즈의 가장 값이 큰 row의 index를 반환된 것을 볼 수 있다.

ps)
df['embark_town'].value_counts(dropna=False)
#output: 결측값까지도 개수를 세어 보여준다. dropna=True가 default
Southampton    644
Cherbourg      168
Queenstown      77
NaN              2
Name: embark_town, dtype: int64

그럼 이제 fillna()함수를 이용해서 결측값을 최빈값으로 바꿔보자.

most_freq = df['embark_town'].describe()['top'] 
#most_freq = df['embark_town'].value_counts().idxmax()
index = df.index[df['embark_town'].isnull()]

#최빈값으로 대체
df['embark_town'].fillna(most_freq,inplace = True)

print(df.loc[index,'embark_town'])
#output:
61     Southampton
829    Southampton
Name: embark_town, dtype: object

이웃하고 있는 값으로 대체

method = ‘ffill’은 결측값의 바로 앞의 값으로, method = ‘bfill’은 결측값의 바로 뒤의 값으로 바꿔주는 옵션이다.

print('***치환 전***')
print(df.iloc[59:63,'embark_town'])

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

print('***치환 후***')
print(df.iloc[59:63,'embark_town'])

#output
***치환 ***
59    Southampton
60      Cherbourg
61            NaN
62    Southampton
63    Southampton
Name: embark_town, dtype: object
***치환 ***
59    Southampton
60      Cherbourg
61      Cherbourg
62    Southampton
63    Southampton
Name: embark_town, dtype: object

print('***치환 전***')
print(df.iloc[59:63,'embark_town'])

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

print('***치환 후***')
print(df.iloc[59:63,'embark_town'])

#output
***치환 ***
59    Southampton
60      Cherbourg
61            NaN
62    Southampton
63    Southampton
Name: embark_town, dtype: object
***치환 ***
59    Southampton
60      Cherbourg
61    Southampton
62    Southampton
63    Southampton
Name: embark_town, dtype: object

2. 중복 데이터 탐색

중복 데이터 탐색을 위해 사용한 데이터프레임

import pandas as pd

df = pd.DataFrame({'Name':['정인','준모','우진','정인'],'Math':[100,70,90,100],'Eng':[100,70,90,100]})

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

duplicated()함수는 row마다 중복되는 데이터를 탐색해준다. 아무 옵션을 적용하지 않으면 row마다 중복되어 있으면 True를 그렇지 않으면 False를 반환한다.

df.duplicated()
#output
0    False
1    False
2    False
3     True
dtype: bool

바로 전행과만 비교 하는 것이 아닌 처음 나오는 행인지, 나왔던 행인지를 비교한다. 결과적으로 False의 개수가 데이터프레임 내 유니크한 값의 개수이다.

당연하게도 하나의 컬럼에만 적용도 가능하다.

df['Name'].duplicated()
#output
0    False
1    False
2    False
3     True
Name: Name, dtype: bool
2-2. 중복 데이터 제거(drop_duplicated())

데이터프레임 내의 중복된 row를 제거해주는 함수이다.

df.drop_duplicated(inplace=True) 
#inplace = True를 지정해주면 처리한 데이터를 바로 데이터프레임에 적용할 수 있다.
df.duplicated()
#output:
0    False
1    False
2    False
dtype: bool

subset = [‘컬럼명’,’컬러명’,….] 옵션 사용

df.drop_duplicated(subset=['Math'],inplace=True)
#Math 컬럼을 기준으로 중복되는 데이터를 갖고 있는 row를 삭제한다.
#output: 
0    False
1    False
2    False
dtype: bool