파이썬 기반 데이터분석을 위하여 Pandas 라이브러리 객체의 결측치를 다루는 방법에 대해 알아보고자 한다.
※ 본 내용에서 사용하는 missing_sample.csv 파일은 별도로 다운로드 받아야 한다. ※ missing_sample.csv 다운받기 [클릭]
개요 결측치(missing values)는 일반적인 데이터 집합에서 벗어난다는 뜻을 가진 이상치(outlier)의 하위 개념이며 값이 존재하지 않을 때 사용하는 값이기도 하다. 예를들어 엑셀로 따지면 비어있는 셀이라고 할 수 있겠다.
결측치의 판별과 처리는 데이터 분석에서 매우 중요한 전처리 과정에 속하는 절차이며 해당 절차가 제대로 수행되지 않는다면 각종 산술연산에서 에러가 발생한다던가 원하지 않은 값이 산출되는 등 양질의 분석 결과를 기대하기 어렵다. 그래서 어떠한 산술연산 수행 이전에 데이터의 결측치를 확인하여 이를 제거하거나 어떤 값으로 대치하는 작업을 실시하며 이를 Pandas 기반의 객체를 다루면서 어떻게 실시하는지 알아보고자 한다. ※ 결측치 유형에 따른 구분 등 이론적인 내용은 별도의 게시글에서 다룬다.
결측치는 기본적으로 NumPy 의 NaN
, Pandas 의 NA
가 있으며 이와 유사한 것으로 None
이 있다. NaN
이나 NA
의 경우 사용자가 직접 만들기도 하지만 None
의 경우 보통 파이썬을 다루다가 간혹 마주치는 원소이며 Pandas 문자 데이터 핸들링 게시물 에도 등장하니 참고하자.
생성 결측치의 생성은 Pandas 라이브러리의 경우 다음과 같이 생성할 수 있다.
1 2 3 4 5 import numpy as npimport pandas as pdnp.nan, pd.NA
pd.NA
를 1.0.0 버전부터 지원하고 있으나 2.0.0 버전에서도 아직 선형보간을 지원하는 .interpolate()
메서드 운용에서 버그가 남아있기에 아직은 np.nan
사용을 권장한다. 그리고 해당 버그를 재현하는 코드는 다음과 같다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 ser1 = pd.Series([1 , 3 , pd.NA, 5 ]) ser1.interpolate() ser2 = pd.Series([1 , 3 , np.nan, 5 ]) ser2.interpolate()
식별 객체에 결측치가 있는지 확인하는 것은 결측치 처리과정에서 가장 먼저 실시해야할 일이다. 결측치는 특이하게 문자열 처리와 관련된 메서드를 사용하는 것이 아니라 그 특수성 때문에 되도록이면 결측치 관련 메서드를 사용해야 한다. 다음의 코드를 보면 결측치가 일반적인 원소가 아니라는 것을 알 수 있다.
1 2 3 4 5 6 7 8 9 10 11 np.nan == np.nan np.nan != np.nan pd.NA == pd.NA pd.NA != pd.NA
시리즈(Series) 이제 관련 예제를 알아보기 위해 시리즈 객체를 준비하도록 하자.
1 2 3 4 5 6 7 ser1 = pd.Series([1 , 3 , np.nan, 5 ]) ser1
Pandas 객체의 .isna()
메서드는 원소가 결측일 경우 True
를 반환하고 그렇지 않을 경우 False
를 반환한다. 그리고 .isnull()
또한 똑같은 기능을 한다.
1 2 3 4 5 6 7 8 9 10 11 12 13 ser1.isna() ser1.isnull()
True
의 개수를 세면 결측치의 개수를 세는 것과 같기 때문에 각 원소의 합계를 구하면(True가 1, False가 0으로 계산됨) 결측치의 개수를 확인할 수 있다.
.isna()
의 반대 기능을 하는 것이 .notna()
와 .notnull()
이 있다.
1 2 3 4 5 6 7 8 9 10 11 12 13 ser1.notna() ser1.notnull()
여기서 각 원소의 합을 구하면 관측치의 개수가 되겠다. 그리고 관측치의 개수를 확인하고자 한다면 .count()
를 쓸 수 있다. 그런데 보통 결측치의 개수를 세기 때문에 사용 빈도는 낮은 편이다.
데이터프레임(DataFrame) 데이터프레임 객체를 대상으로 결측치를 확인해보자.
1 2 df = pd.read_csv("missing_sample.csv" ) df
id
group
t1
t2
t3
t4
t5
0
0
A
95000.0
100000.0
95000.0
75000.0
70000.0
1
1
A
NaN
NaN
95000.0
55000.0
25000.0
2
2
A
NaN
NaN
80000.0
35000.0
30000.0
3
3
NaN
95000.0
NaN
30000.0
10000.0
4
4
A
100000.0
100000.0
70000.0
45000.0
15000.0
5
5
B
NaN
NaN
80000.0
75000.0
65000.0
6
6
B
NaN
NaN
NaN
NaN
NaN
7
7
B
85000.0
95000.0
85000.0
45000.0
40000.0
8
8
B
NaN
NaN
70000.0
NaN
NaN
9
9
B
100000.0
100000.0
70000.0
70000.0
45000.0
특정 변수에 대해 결측치를 확인하는 코드는 시리즈 객체를 다루는 것과 같다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 df["t1" ].isna() df["t1" ].isna().sum()
데이터프레임 전체 변수의 결측치를 한 번에 확인해보자. 데이터프레임 객체에서 .isna()
메서드를 사용하면 특이하게 모든 변수의 원소에 대해 결측여부를 검사한 결과가 나온다.
id
group
t1
t2
t3
t4
t5
0
False
False
False
False
False
False
False
1
False
False
True
True
False
False
False
2
False
False
True
True
False
False
False
3
False
False
True
False
True
False
False
4
False
False
False
False
False
False
False
5
False
False
True
True
False
False
False
6
False
False
True
True
True
True
True
7
False
False
False
False
False
False
False
8
False
False
True
True
False
True
True
9
False
False
False
False
False
False
False
여기서 각 변수별로 합계를 구하면 각 변수별 결측치 개수를 쉽게 산출할 수 있다.
이 시점에서 눈썰미가 좋은 사람은 “group” 변수에 빈칸이 있다는 것을 알 수 있는데 여기서 .unique()
메서드로 중복제거를 해보면 한 칸 띄어쓰기가 되어있는 원소가 있는 것을 알 수 있다.
이 경우는 다음과 같은 코드로 확인할 수 있다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 df["group" ] == " " sum(df["group" ] == " " )
이렇게 결측치가 NA
또는 nan
같이 일반적인 형태가 아니고 빈칸, 한 칸 띄어쓰기, 문자 “x” 등 다양한 형식으로 있을 수 있는데 이런 경우는 일반적인 문자열을 다루는 코드를 사용해야 한다.
필터링 결측치의 필터링은 크게 결측치가 있는 행만 뽑아내거나 결측치를 제외하는 상황이 있을 수 있다.
시리즈(Series) 앞에서 만들었던 “ser1” 객체를 다시 보자.
1 2 3 4 5 6 7 ser1 = pd.Series([1 , 3 , np.nan, 5 ]) ser1
.isna()
와 .notna()
를 사용하면 다음과 같은 결과를 확인할 수 있다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 ser1[ser1.isna()] ser1[ser1.isna() == False ] ser1[~ser1.isna()] ser1[ser1.notna()]
그리고 .notna()
를 사용한 결과와 유사한 것이 .dropna()
메서드를 사용한 결과이다.
데이터프레임(DataFrame) 앞에서 불러왔던 “missing_sample.csv”를 사용해서 알아보자.
1 2 df = pd.read_csv("missing_sample.csv" ) df
id
group
t1
t2
t3
t4
t5
0
0
A
95000.0
100000.0
95000.0
75000.0
70000.0
1
1
A
NaN
NaN
95000.0
55000.0
25000.0
2
2
A
NaN
NaN
80000.0
35000.0
30000.0
3
3
NaN
95000.0
NaN
30000.0
10000.0
4
4
A
100000.0
100000.0
70000.0
45000.0
15000.0
5
5
B
NaN
NaN
80000.0
75000.0
65000.0
6
6
B
NaN
NaN
NaN
NaN
NaN
7
7
B
85000.0
95000.0
85000.0
45000.0
40000.0
8
8
B
NaN
NaN
70000.0
NaN
NaN
9
9
B
100000.0
100000.0
70000.0
70000.0
45000.0
1 df.loc[df["t2" ].isna(), ]
id
group
t1
t2
t3
t4
t5
1
1
A
NaN
NaN
95000.0
55000.0
25000.0
2
2
A
NaN
NaN
80000.0
35000.0
30000.0
5
5
B
NaN
NaN
80000.0
75000.0
65000.0
6
6
B
NaN
NaN
NaN
NaN
NaN
8
8
B
NaN
NaN
70000.0
NaN
NaN
1 df.loc[df["t2" ].notna(), ]
id
group
t1
t2
t3
t4
t5
0
0
A
95000.0
100000.0
95000.0
75000.0
70000.0
3
3
NaN
95000.0
NaN
30000.0
10000.0
4
4
A
100000.0
100000.0
70000.0
45000.0
15000.0
7
7
B
85000.0
95000.0
85000.0
45000.0
40000.0
9
9
B
100000.0
100000.0
70000.0
70000.0
45000.0
그리고 데이터프레임 객체에 .dropna()
를 사용하면 결측값이 있는 행을 한 번에 제거할 수 있어 꽤 유용하다.
id
group
t1
t2
t3
t4
t5
0
0
A
95000.0
100000.0
95000.0
75000.0
70000.0
4
4
A
100000.0
100000.0
70000.0
45000.0
15000.0
7
7
B
85000.0
95000.0
85000.0
45000.0
40000.0
9
9
B
100000.0
100000.0
70000.0
70000.0
45000.0
.dropna()
메서드의 인자 중 “how”에 “all”을 할당하면 원소가 모두 결측치인 행만 제거된다.
1 df.loc[:, "t1" :"t5" ].dropna(how = "all" )
t1
t2
t3
t4
t5
0
95000.0
100000.0
95000.0
75000.0
70000.0
1
NaN
NaN
95000.0
55000.0
25000.0
2
NaN
NaN
80000.0
35000.0
30000.0
3
NaN
95000.0
NaN
30000.0
10000.0
4
100000.0
100000.0
70000.0
45000.0
15000.0
5
NaN
NaN
80000.0
75000.0
65000.0
7
85000.0
95000.0
85000.0
45000.0
40000.0
8
NaN
NaN
70000.0
NaN
NaN
9
100000.0
100000.0
70000.0
70000.0
45000.0
대치 이제 결측치에 별도의 값을 할당하는 방법을 알아보자.
시리즈(Series) 앞에서 사용했던 “ser1” 객체를 다시 확인해보자.
.fillna()
메서드가 유용하게 사용되며 별도의 값을 지정할 수 있고 평균값을 집어넣고자 한다면 두 번째 코드와 같이 작성하면 된다. 이 때 평균값의 계산은 결측값을 제외한 나머지 원소로 계산되는 것이니 참고하도록 한다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 ser1.fillna(-1 ) ser1.fillna(ser1.mean()) ser1[ser1.isna()] = -100 ser1
데이터프레임(DataFrame) .fillna()
를 데이터프레임 객체에 사용할 경우 기본적으로 모든 변수의 결측치를 특정 값으로 바꿀 수 있다.
id
group
t1
t2
t3
t4
t5
0
0
A
95000.0
100000.0
95000.0
75000.0
70000.0
1
1
A
-1.0
-1.0
95000.0
55000.0
25000.0
2
2
A
-1.0
-1.0
80000.0
35000.0
30000.0
3
3
-1.0
95000.0
-1.0
30000.0
10000.0
4
4
A
100000.0
100000.0
70000.0
45000.0
15000.0
5
5
B
-1.0
-1.0
80000.0
75000.0
65000.0
6
6
B
-1.0
-1.0
-1.0
-1.0
-1.0
7
7
B
85000.0
95000.0
85000.0
45000.0
40000.0
8
8
B
-1.0
-1.0
70000.0
-1.0
-1.0
9
9
B
100000.0
100000.0
70000.0
70000.0
45000.0
.fillna()
메서드에 딕셔너리를 입력할 경우 각 변수별 결측치 대치값을 지정할 수 있다.
1 df.fillna({"t1" : -1 , "t2" : -999 })
id
group
t1
t2
t3
t4
t5
0
0
A
95000.0
100000.0
95000.0
75000.0
70000.0
1
1
A
-1.0
-999.0
95000.0
55000.0
25000.0
2
2
A
-1.0
-999.0
80000.0
35000.0
30000.0
3
3
-1.0
95000.0
NaN
30000.0
10000.0
4
4
A
100000.0
100000.0
70000.0
45000.0
15000.0
5
5
B
-1.0
-999.0
80000.0
75000.0
65000.0
6
6
B
-1.0
-999.0
NaN
NaN
NaN
7
7
B
85000.0
95000.0
85000.0
45000.0
40000.0
8
8
B
-1.0
-999.0
70000.0
NaN
NaN
9
9
B
100000.0
100000.0
70000.0
70000.0
45000.0
마치며 상기 내용 외에도 결측치의 대치는 다양한 결측치 대치법이 있으며 그 중에서도 특히 머신러닝을 활용한 결측치 대치법이 있으니 이와 관련해서는 이후에 별도의 게시글에서 다룰 예정이다. 머신러닝 이외에 추가적인 내용은 다음을 참고하도록 하자.
Py) 전처리 - 결측치 처리-01 Py) 전처리 - 결측치 처리-02