Py) 기초 - Pandas(결측치)

Py) 기초 - Pandas(결측치)

파이썬 기반 데이터분석을 위하여 Pandas 라이브러리 객체의 결측치를 다루는 방법에 대해 알아보고자 한다.


※ 본 내용에서 사용하는 missing_sample.csv 파일은 별도로 다운로드 받아야 한다.
missing_sample.csv 다운받기 [클릭]

개요

결측치(missing values)는 일반적인 데이터 집합에서 벗어난다는 뜻을 가진 이상치(outlier)의 하위 개념이며 값이 존재하지 않을 때 사용하는 값이기도 하다. 예를들어 엑셀로 따지면 비어있는 셀이라고 할 수 있겠다.
결측치 예시

결측치의 판별과 처리는 데이터 분석에서 매우 중요한 전처리 과정에 속하는 절차이며 해당 절차가 제대로 수행되지 않는다면 각종 산술연산에서 에러가 발생한다던가 원하지 않은 값이 산출되는 등 양질의 분석 결과를 기대하기 어렵다. 그래서 어떠한 산술연산 수행 이전에 데이터의 결측치를 확인하여 이를 제거하거나 어떤 값으로 대치하는 작업을 실시하며 이를 Pandas 기반의 객체를 다루면서 어떻게 실시하는지 알아보고자 한다.
※ 결측치 유형에 따른 구분 등 이론적인 내용은 별도의 게시글에서 다룬다.

결측치는 기본적으로 NumPyNaN, PandasNA가 있으며 이와 유사한 것으로 None이 있다. NaN이나 NA의 경우 사용자가 직접 만들기도 하지만 None의 경우 보통 파이썬을 다루다가 간혹 마주치는 원소이며 Pandas 문자 데이터 핸들링 게시물에도 등장하니 참고하자.

생성

결측치의 생성은 Pandas 라이브러리의 경우 다음과 같이 생성할 수 있다.

1
2
3
4
5
import numpy as np
import pandas as pd

np.nan, pd.NA
## (nan, <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()
## 0 1
## 1 3
## 2 <NA>
## 3 5
## dtype: object

ser2 = pd.Series([1, 3, np.nan, 5])
ser2.interpolate()
## 0 1.0
## 1 3.0
## 2 4.0
## 3 5.0
## dtype: float64

식별

객체에 결측치가 있는지 확인하는 것은 결측치 처리과정에서 가장 먼저 실시해야할 일이다. 결측치는 특이하게 문자열 처리와 관련된 메서드를 사용하는 것이 아니라 그 특수성 때문에 되도록이면 결측치 관련 메서드를 사용해야 한다. 다음의 코드를 보면 결측치가 일반적인 원소가 아니라는 것을 알 수 있다.

1
2
3
4
5
6
7
8
9
10
11
np.nan == np.nan
## False

np.nan != np.nan
## True

pd.NA == pd.NA
## <NA>

pd.NA != pd.NA
## <NA>

시리즈(Series)

이제 관련 예제를 알아보기 위해 시리즈 객체를 준비하도록 하자.

1
2
3
4
5
6
7
ser1 = pd.Series([1, 3, np.nan, 5])
ser1
## 0 1.0
## 1 3.0
## 2 NaN
## 3 5.0
## dtype: float64

Pandas 객체의 .isna() 메서드는 원소가 결측일 경우 True를 반환하고 그렇지 않을 경우 False를 반환한다. 그리고 .isnull() 또한 똑같은 기능을 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
ser1.isna()
## 0 False
## 1 False
## 2 True
## 3 False
## dtype: bool

ser1.isnull()
## 0 False
## 1 False
## 2 True
## 3 False
## dtype: bool

True의 개수를 세면 결측치의 개수를 세는 것과 같기 때문에 각 원소의 합계를 구하면(True가 1, False가 0으로 계산됨) 결측치의 개수를 확인할 수 있다.

1
2
ser1.isna().sum()
## 1

.isna()의 반대 기능을 하는 것이 .notna().notnull()이 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
ser1.notna()
## 0 True
## 1 True
## 2 False
## 3 True
## dtype: bool

ser1.notnull()
## 0 True
## 1 True
## 2 False
## 3 True
## dtype: bool

여기서 각 원소의 합을 구하면 관측치의 개수가 되겠다. 그리고 관측치의 개수를 확인하고자 한다면 .count()를 쓸 수 있다. 그런데 보통 결측치의 개수를 세기 때문에 사용 빈도는 낮은 편이다.

1
2
ser1.count()
## 3

데이터프레임(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()
## 0 False
## 1 True
## 2 True
## 3 True
## 4 False
## 5 True
## 6 True
## 7 False
## 8 True
## 9 False
## Name: t1, dtype: bool

df["t1"].isna().sum()
## 6

데이터프레임 전체 변수의 결측치를 한 번에 확인해보자. 데이터프레임 객체에서 .isna() 메서드를 사용하면 특이하게 모든 변수의 원소에 대해 결측여부를 검사한 결과가 나온다.

1
df.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

여기서 각 변수별로 합계를 구하면 각 변수별 결측치 개수를 쉽게 산출할 수 있다.

1
2
3
4
5
6
7
8
9
df.isna().sum()
## id 0
## group 0
## t1 6
## t2 5
## t3 2
## t4 2
## t5 2
## dtype: int64

이 시점에서 눈썰미가 좋은 사람은 “group” 변수에 빈칸이 있다는 것을 알 수 있는데 여기서 .unique() 메서드로 중복제거를 해보면 한 칸 띄어쓰기가 되어있는 원소가 있는 것을 알 수 있다.

1
2
df["group"].unique()
## array(['A', ' ', 'B'], dtype=object)

이 경우는 다음과 같은 코드로 확인할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
df["group"] == " "
## 0 False
## 1 False
## 2 False
## 3 True
## 4 False
## 5 False
## 6 False
## 7 False
## 8 False
## 9 False
## Name: group, dtype: bool

sum(df["group"] == " ")
## 1

이렇게 결측치가 NA 또는 nan 같이 일반적인 형태가 아니고 빈칸, 한 칸 띄어쓰기, 문자 “x” 등 다양한 형식으로 있을 수 있는데 이런 경우는 일반적인 문자열을 다루는 코드를 사용해야 한다.

필터링

결측치의 필터링은 크게 결측치가 있는 행만 뽑아내거나 결측치를 제외하는 상황이 있을 수 있다.

시리즈(Series)

앞에서 만들었던 “ser1” 객체를 다시 보자.

1
2
3
4
5
6
7
ser1 = pd.Series([1, 3, np.nan, 5])
ser1
## 0 1.0
## 1 3.0
## 2 NaN
## 3 5.0
## dtype: float64

.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()]
## 2 NaN
## dtype: float64

ser1[ser1.isna() == False]
## 0 1.0
## 1 3.0
## 3 5.0
## dtype: float64

ser1[~ser1.isna()]
## 0 1.0
## 1 3.0
## 3 5.0
## dtype: float64

ser1[ser1.notna()]
## 0 1.0
## 1 3.0
## 3 5.0
## dtype: float64

그리고 .notna()를 사용한 결과와 유사한 것이 .dropna() 메서드를 사용한 결과이다.

1
2
3
4
5
ser1.dropna()
## 0 1.0
## 1 3.0
## 3 5.0
## dtype: float64

데이터프레임(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()를 사용하면 결측값이 있는 행을 한 번에 제거할 수 있어 꽤 유용하다.

1
df.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” 객체를 다시 확인해보자.

1
2
3
4
5
6
ser1
## 0 1.0
## 1 3.0
## 2 NaN
## 3 5.0
## dtype: float64

.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)
## 0 1.0
## 1 3.0
## 2 -1.0
## 3 5.0
## dtype: float64

ser1.fillna(ser1.mean())
## 0 1.0
## 1 3.0
## 2 3.0
## 3 5.0
## dtype: float64

ser1[ser1.isna()] = -100
ser1
## 0 1.0
## 1 3.0
## 2 -100.0
## 3 5.0
## dtype: float64

데이터프레임(DataFrame)

.fillna()를 데이터프레임 객체에 사용할 경우 기본적으로 모든 변수의 결측치를 특정 값으로 바꿀 수 있다.

1
df.fillna(-1)
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

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×