Pandas

[Do it! Pandas] 6. 누락값 처리하기

PKkomi 2023. 2. 12. 15:00

누락값은 말 그대로 데이터에서 값을 가지지 않는 것으로 데이터가 없음을 의미한다.

데이터 분석에서 누락값은 생각 외로 많이 존재하기 때문에 이를 다루는 것이 매우 중요하다.

깨끗한 데이터를 만들기 위해 누락값의 처리 방법을 알아보자.

 

 

먼저 누락값을 눈에 보이게 확인해보자.

누락값은 비교할 값 자체가 존재하지 않기 때문에 비교 구문을 작성하면 다음과 같은 결과를 얻는다.

from numpy import NaN, NAN, nan
print(NaN==True)
print(NaN==False)
print(NaN==0)
print(NaN=='')
print(NaN==NaN)
False
False
False
False
False

 

 

어떤 값이 누락값인지 아닌지 판별해주는 isnull 메서드와 notnull 메서드가 존재한다. 

사용방법은 다음과 같다.

import pandas as pd

print(pd.isnull(NaN)) # isnull 메서드를 이용해 누락값을 판별함.
print(pd.isnull(NAN))
print(pd.isnull(nan))
print(pd.notnull(42)) # notnull 메서드도 있음.
print(pd.notnull(NAN))
True
True
True
True
False

 

 

먼저 누락값이 언제 생기는지 확인해보자.

 

누락값이 있는 데이터를 연결하면 누락값이 발생하기도 한다.

앞장에서 사용한 visited와 survey 데이터에는 누락값(각각 1개, 2개)이 존재한다.

아래는 각각 visited와 survey이다.

   ident   site       dated
0    619   DR-1  1927-02-08
1    622   DR-1  1927-02-10
2    734   DR-3  1939-01-07
3    735   DR-3  1930-01-12
4    751   DR-3  1930-02-26
5    752   DR-3         NaN
6    837  MSK-4  1932-01-14
7    844   DR-1  1932-03-22 


    taken person quant  reading
0     619   dyer   rad     9.82
1     619   dyer   sal     0.13
2     622   dyer   rad     7.80
3     622   dyer   sal     0.09
4     734     pb   rad     8.41
5     734   lake   sal     0.05
6     734     pb  temp   -21.50
7     735     pb   rad     7.22
8     735    NaN   sal     0.06
9     735    NaN  temp   -26.00
10    751     pb   rad     4.35
11    751     pb  temp   -18.50
12    751   lake   sal     0.10
13    752   lake   rad     2.19
14    752   lake   sal     0.09
15    752   lake  temp   -16.00
16    752    roe   sal    41.60
17    837   lake   rad     1.46
18    837   lake   sal     0.21
19    837    roe   sal    22.50
20    844    roe   rad    11.25

 

두 데이터를 연결할 때 concat에서 join='inner'로 지정하거나 merge를 사용하여 누락값을 처리하였다.

visited = pd.read_csv('../data/survey_visited.csv') # 누락값 1개.
survey = pd.read_csv('../data/survey_survey.csv') # 누락값 2개.
vs = visited.merge(survey, left_on='ident', right_on='taken') # 누락값 4개
print(vs)
    ident   site       dated  taken person quant  reading
0     619   DR-1  1927-02-08    619   dyer   rad     9.82
1     619   DR-1  1927-02-08    619   dyer   sal     0.13
2     622   DR-1  1927-02-10    622   dyer   rad     7.80
3     622   DR-1  1927-02-10    622   dyer   sal     0.09
4     734   DR-3  1939-01-07    734     pb   rad     8.41
5     734   DR-3  1939-01-07    734   lake   sal     0.05
6     734   DR-3  1939-01-07    734     pb  temp   -21.50
7     735   DR-3  1930-01-12    735     pb   rad     7.22
8     735   DR-3  1930-01-12    735    NaN   sal     0.06
9     735   DR-3  1930-01-12    735    NaN  temp   -26.00
10    751   DR-3  1930-02-26    751     pb   rad     4.35
11    751   DR-3  1930-02-26    751     pb  temp   -18.50
12    751   DR-3  1930-02-26    751   lake   sal     0.10
13    752   DR-3         NaN    752   lake   rad     2.19
14    752   DR-3         NaN    752   lake   sal     0.09
15    752   DR-3         NaN    752   lake  temp   -16.00
16    752   DR-3         NaN    752    roe   sal    41.60
17    837  MSK-4  1932-01-14    837   lake   rad     1.46
18    837  MSK-4  1932-01-14    837   lake   sal     0.21
19    837  MSK-4  1932-01-14    837    roe   sal    22.50
20    844   DR-1  1932-03-22    844    roe   rad    11.25

두 데이터에는 원래 총누락값이 3개였는데 연결한 후에는 4개로 오히려 개수가 늘어난 것을 확인할 수 있다.

이는 원래 NaN을 가지고 있던 ident열의 값이 752인 행의 값들이 survey의 taken 열의 값이 752인 4개의 행들과 연결되면서 생긴 누락값이다.

 

 

데이터프레임에 없는 열과 행 데이터를 입력하면 누락값이 발생하기도 한다.

num_legs = pd.Series({'goat': 4, 'amoeba': nan})
print(num_legs)
print(type(num_legs))
goat      4.0
amoeba    NaN
dtype: float64
<class 'pandas.core.series.Series'>

 

scientists = pd.DataFrame({
    'Name': ['Rosaline Franklin', 'William Gosset'],
    'Occupation': ['Chemist', 'Statistician'],
    'Born': ['1920-07-25', '1876-06-13'],
    'Died': ['1958-04-16', '1937-10-16'],
    'missing': [NaN, nan]
})

print(scientists)
print(type(scientists))
                Name    Occupation        Born        Died  missing
0  Rosaline Franklin       Chemist  1920-07-25  1958-04-16      NaN
1     William Gosset  Statistician  1876-06-13  1937-10-16      NaN
<class 'pandas.core.frame.DataFrame'>

 

 

책에는 인덱스를 다시 만들때 누락값이 발생하기도 한다고 한다.

gapminder.csv 파일을 가져와 확인해보자.

gapminder = pd.read_csv('../data/gapminder.tsv', sep='\t')
life_exp = gapminder.groupby(['year'])['lifeExp'].mean()
print(life_exp)
year
1952    49.057620
1957    51.507401
1962    53.609249
1967    55.678290
1972    57.647386
1977    59.570157
1982    61.533197
1987    63.212613
1992    64.160338
1997    65.014676
2002    65.694923
2007    67.007423
Name: lifeExp, dtype: float64
 

 

print(life_exp.loc[range(2000,2010)]) # 책에는 누락값으로 표현된다고 하지만 에러가 발생한다.
KeyError: '[2000, 2001, 2003, 2004, 2005, 2006, 2008, 2009] not in index'

 

기대했던 것은 2000년부터 2009년까지는 NaN을, 나머지 2002와 2007은 값을 가진채로 출력되는 것이지만 현재는 위와 같은 에러가 발생한다. 아마 버전이 이전과 달라지면서 생긴 변화가 아닐까 생각된다.

 

아시는 분이 있다면 알려주세요!

 

2000년대 이후의 결과를 보고 싶다면 다음과 같이 코드를 작성하면 된다.

y2000 = life_exp[life_exp.index>2000]
print(y2000)
year
2002    65.694923
2007    67.007423
Name: lifeExp, dtype: float64

 

 

이제 데이터에 포함된 누락값의 개수를 구해보자.

데이터의 개수를 센 후에 총 행의 개수에서 빼면 누락값의 개수를 구할 수 있다.

 

많은 누락값을 가진 country_timeseries.csv를 가져와 사용한다.

행의 개수에서 데이터 수를 담은 시리즈를 빼면 브로드캐스팅되어 한 번에 계산할 수 있다.

ebola = pd.read_csv('../data/country_timeseries.csv')

print(ebola.shape) # (행, 열) == (122, 18)
num_rows = ebola.shape[0]
print(num_rows, type(ebola.count()), '\n')
num_missing = num_rows - ebola.count() # 정수 - series = 값들이 행마다 각각 계산됨.
print(num_missing) # 누락값의 개수
(122, 18)
122 <class 'pandas.core.series.Series'> 

Date                     0
Day                      0
Cases_Guinea            29
Cases_Liberia           39
Cases_SierraLeone       35
Cases_Nigeria           84
Cases_Senegal           97
Cases_UnitedStates     104
Cases_Spain            106
Cases_Mali             110
Deaths_Guinea           30
Deaths_Liberia          41
Deaths_SierraLeone      35
Deaths_Nigeria          84
Deaths_Senegal         100
Deaths_UnitedStates    104
Deaths_Spain           106
Deaths_Mali            110
dtype: int64

 

numpy와 isnull을 이용하여 총 누락값과 데이터의 개수를 쉽게 구할 수 있다.

import numpy as np

print(np.count_nonzero(ebola.isnull())) # 전체 누락값의 개수
print(122*18) # 누락값이 없을 때 데이터의 개수
print(np.count_nonzero(ebola['Cases_Guinea'].isnull())) # 누락값의 개수
1214
2196
29

 

시리즈에 포함된 value_counts 메서드를 이용하여 시리즈의 누락값의 개수를 구할 수도 있다.

print(ebola.Cases_Guinea.value_counts(dropna=False).head()) 
# 시리즈에 포함된 메서드이므로 ebola(데이터프레임)의 한 열(Cases_Guinea; 시리즈)을 대상으로 사용.
NaN      29
86.0      3
495.0     2
112.0     2
390.0     2
Name: Cases_Guinea, dtype: int64

 

 

 

마침내 누락값을 처리해보려 한다.

누락값의 처리 방법은 크게는 두 가지로, 다른 값으로 변경하거나 삭제하는 것이다.

 

다른 값으로 변경할때는 주로 fillna 메서드를 사용한다.

fillna는 데이터프레임에 포함된 메서드로 처리해야 하는 데이터프레임의 크기가 매우 크고 메모리를 효율적으로 사용해야 할 때 주로 사용된다.

fillna메서드에 입력하는 값으로 누락값을 모두 변경할 수 있다.

fillna메서드에 method='ffill'을 사용하면 누락값의 바로 전 값으로 누락값을 변경한다. 바로 전 행의 값을 사용하기 때문에 처음 0행에 있는 누락값은 처리가 불가능하다.

fillna메서드에 method='bfill'을 사용하면 누락값의 바로 다음 값을 사용하여 누락값을 처리한다. 따라서 마지막행에 있는 누락값은 처리가 불가능하다.

 

다음의 예를 살펴보자.

print(ebola.fillna(0).iloc[0:10, 0:5])
         Date  Day  Cases_Guinea  Cases_Liberia  Cases_SierraLeone
0    1/5/2015  289        2776.0            0.0            10030.0
1    1/4/2015  288        2775.0            0.0             9780.0
2    1/3/2015  287        2769.0         8166.0             9722.0
3    1/2/2015  286           0.0         8157.0                0.0
4  12/31/2014  284        2730.0         8115.0             9633.0
5  12/28/2014  281        2706.0         8018.0             9446.0
6  12/27/2014  280        2695.0            0.0             9409.0
7  12/24/2014  277        2630.0         7977.0             9203.0
8  12/21/2014  273        2597.0            0.0             9004.0
9  12/20/2014  272        2571.0         7862.0             8939.0

누락값이 모두 0으로 변경되었다.

 

print(ebola.fillna(method='ffill').iloc[0:10, 0:5])
         Date  Day  Cases_Guinea  Cases_Liberia  Cases_SierraLeone
0    1/5/2015  289        2776.0            NaN            10030.0
1    1/4/2015  288        2775.0            NaN             9780.0
2    1/3/2015  287        2769.0         8166.0             9722.0
3    1/2/2015  286        2769.0         8157.0             9722.0
4  12/31/2014  284        2730.0         8115.0             9633.0
5  12/28/2014  281        2706.0         8018.0             9446.0
6  12/27/2014  280        2695.0         8018.0             9409.0
7  12/24/2014  277        2630.0         7977.0             9203.0
8  12/21/2014  273        2597.0         7977.0             9004.0
9  12/20/2014  272        2571.0         7862.0             8939.0

처음 행의 누락값을 제외하고 모두 전 행의 값들로 누락값이 변경되었다.

 

print(ebola.fillna(method='bfill').iloc[0:10, 0:5])
         Date  Day  Cases_Guinea  Cases_Liberia  Cases_SierraLeone
0    1/5/2015  289        2776.0         8166.0            10030.0
1    1/4/2015  288        2775.0         8166.0             9780.0
2    1/3/2015  287        2769.0         8166.0             9722.0
3    1/2/2015  286        2730.0         8157.0             9633.0
4  12/31/2014  284        2730.0         8115.0             9633.0
5  12/28/2014  281        2706.0         8018.0             9446.0
6  12/27/2014  280        2695.0         7977.0             9409.0
7  12/24/2014  277        2630.0         7977.0             9203.0
8  12/21/2014  273        2597.0         7862.0             9004.0
9  12/20/2014  272        2571.0         7862.0             8939.0

모든 누락값이 다음 행의 값들로 변경되었다.

 

fillna대신에 interpolate 메서드를 사용하기도 한다. 한국말로 내삽이라고 하며, 누락값 앞뒤의 두 값의 중간값을 사용하여 데이터가 이어지듯이 연결할 수 있다. ffill이나 bfill과 마찬가지로 양 끝에 있는 값은 처리가 불가능하다

print(ebola.interpolate().iloc[0:10, 0:5])
         Date  Day  Cases_Guinea  Cases_Liberia  Cases_SierraLeone
0    1/5/2015  289        2776.0            NaN            10030.0
1    1/4/2015  288        2775.0            NaN             9780.0
2    1/3/2015  287        2769.0         8166.0             9722.0
3    1/2/2015  286        2749.5         8157.0             9677.5
4  12/31/2014  284        2730.0         8115.0             9633.0
5  12/28/2014  281        2706.0         8018.0             9446.0
6  12/27/2014  280        2695.0         7997.5             9409.0
7  12/24/2014  277        2630.0         7977.0             9203.0
8  12/21/2014  273        2597.0         7919.5             9004.0
9  12/20/2014  272        2571.0         7862.0             8939.0

 

 

위와 같은 방법들로 누락값을 변경하기도 하지만, 누락값을 삭제하는 것으로 처리하기도 한다.

 

누락값의 삭제는 dropna 메서드를 이용하여 간단하게 처리할 수 있다.

이때, 너무 많은 데이터가 한 번에 사라질 수 있으므로 분석하는 사람의 판단이 중요하다.

아래의 결과를 살펴보면 데이터가 대폭 줄어든 것을 알 수 있다.

print(ebola.shape)

ebola_dropna = ebola.dropna() # 누락값이 포함된 시리즈를 삭제하는 메서드.
print(ebola_dropna.shape, '\n')
print(ebola_dropna)
(122, 18)
(1, 18) 

          Date  Day  Cases_Guinea  Cases_Liberia  Cases_SierraLeone  \
19  11/18/2014  241        2047.0         7082.0             6190.0   

    Cases_Nigeria  Cases_Senegal  Cases_UnitedStates  Cases_Spain  Cases_Mali  \
19           20.0            1.0                 4.0          1.0         6.0   

    Deaths_Guinea  Deaths_Liberia  Deaths_SierraLeone  Deaths_Nigeria  \
19         1214.0          2963.0              1267.0             8.0   

    Deaths_Senegal  Deaths_UnitedStates  Deaths_Spain  Deaths_Mali  
19             0.0                  1.0           0.0          6.0  

 

 

만약 누락값을 처리하지 않고 데이터를 계산하면 어떻게 될까?

ebola 데이터의 누락값이 있는 3개 열을 이용하여 이를 확인해보자.

ebola['Cases_multiple'] = ebola['Cases_Guinea'] + ebola['Cases_Liberia'] + ebola['Cases_SierraLeone']
# 누락값이 있는 3개의 열을 더한 새로운 열을 만든다.
ebola_subset = ebola.loc[:, ['Cases_Guinea', 'Cases_Liberia', 'Cases_SierraLeone', 'Cases_multiple']]
print(ebola_subset.head(n=10))
   Cases_Guinea  Cases_Liberia  Cases_SierraLeone  Cases_multiple
0        2776.0            NaN            10030.0             NaN
1        2775.0            NaN             9780.0             NaN
2        2769.0         8166.0             9722.0         20657.0
3           NaN         8157.0                NaN             NaN
4        2730.0         8115.0             9633.0         20478.0
5        2706.0         8018.0             9446.0         20170.0
6        2695.0            NaN             9409.0             NaN
7        2630.0         7977.0             9203.0         19810.0
8        2597.0            NaN             9004.0             NaN
9        2571.0         7862.0             8939.0         19372.0

 

누락값이 하나라도 있으면 더한 값이 모두 누락값으로 처리된 것을 알 수 있다.

 

 

이를 방지하기 위해 sum 메서드에 skipna=True를 사용하는 방법도 있다.

print(ebola.Cases_Guinea.sum(skipna=True)) # 누락값은 무시하고 계산.
print(ebola.Cases_Guinea.sum(skipna=False))
84729.0
nan