데이터를 분석할 때 결측치 처리는 필수 과정이다. 결측치가 존재하는 상태로는 각 컬럼별로 잡히는 통계가 달라져 분석할 때 혼란을 야기할 수 있고, 추후에 데이터 시각화를 진행했을 때 신뢰도 있는 그래프를 표출하기 어렵다.
df = pd.read_csv('data/02_seoul_accident_clean.csv')
df.info()
사용한 자료는 공공데이터 포털에서 교통사고 관련 데이터를 취합한 자료를 사용하였으며, 이윤 추구의 목적이 있는 데이터가 아님을 밝힌다.
<class 'pandas.core.frame.DataFrame'>
Index: 74485 entries, A2019010100100001 to A2020123100100593
Data columns (total 28 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 사고년도 74485 non-null int64
1 사고월 74485 non-null int64
2 사고일 74485 non-null int64
3 사고시각 74485 non-null int64
4 사고요일 74485 non-null int64
5 시군구_대범주 74485 non-null object
6 시군구_소범주 74485 non-null object
7 사고내용 74485 non-null object
8 사망자수 74485 non-null int64
9 중상자수 74485 non-null int64
10 경상자수 74485 non-null int64
11 부상신고자수 74485 non-null int64
12 사고유형_대범주 74485 non-null object
13 사고유형_소범주 74485 non-null object
14 법규위반 74485 non-null object
15 노면상태_대범주 74485 non-null object
16 노면상태_소범주 74485 non-null object
17 기상상태 73575 non-null object
18 도로형태_대범주 74485 non-null object
19 도로형태_소범주 74485 non-null object
...
26 피해운전자연령 72049 non-null float64
27 피해운전자상해정도 72225 non-null object
dtypes: float64(2), int64(9), object(17)
memory usage: 16.5+ MB
Output is truncated. View as a scrollable element or open in a text editor. Adjust cell output settings...
각 컬럼 별로 Non-Null Count 수를 비교 해보면, 결측치가 있는 컬럼들이 있다는 사실을 알 수 있다.
df['기상상태'].isnull() # df.기상상태.isnull()
특정 컬럼을 선택하고 isnull() 함수를 호출하면, 해당 컬럼이 null인 데이터는 True, non-null인 데이터는 False로 반환한다.
사고번호
A2019010100100001 False
A2019010100100002 False
A2019010100100003 False
A2019010100100019 False
A2019010100100020 False
...
A2020123100100571 False
A2020123100100572 False
A2020123100100591 False
A2020123100100592 False
A2020123100100593 False
Name: 기상상태, Length: 74485, dtype: bool
여기서 sum() 함수를 사용하면 False=0, True=1로 반환하여 계산해, 총 결측치의 수를 알 수 있다.
df['기상상태'].isnull().sum() # True=1, False=0
910
이런 성질을 이용해 아래와 같이 전체적인 컬럼의 결측치 수를 카운트 할 수 있다.
df.isnull().sum()
사고년도 0
사고월 0
사고일 0
사고시각 0
사고요일 0
시군구_대범주 0
시군구_소범주 0
사고내용 0
사망자수 0
중상자수 0
경상자수 0
부상신고자수 0
사고유형_대범주 0
사고유형_소범주 0
법규위반 0
노면상태_대범주 0
노면상태_소범주 0
기상상태 910
도로형태_대범주 0
도로형태_소범주 0
가해운전자차종 1751
가해운전자성별 1747
가해운전자연령 1758
가해운전자상해정도 4313
피해운전자차종 2260
피해운전자성별 2260
피해운전자연령 2436
피해운전자상해정도 2260
dtype: int64
또한 sum() 함수 뿐만아니라, mean() 함수를 활용하면 결측치의 비율을 알 수 있다.
df.isnull().mean()
사고년도 0.000000
사고월 0.000000
사고일 0.000000
사고시각 0.000000
사고요일 0.000000
시군구_대범주 0.000000
시군구_소범주 0.000000
사고내용 0.000000
사망자수 0.000000
중상자수 0.000000
경상자수 0.000000
부상신고자수 0.000000
사고유형_대범주 0.000000
사고유형_소범주 0.000000
법규위반 0.000000
노면상태_대범주 0.000000
노면상태_소범주 0.000000
기상상태 0.012217
도로형태_대범주 0.000000
도로형태_소범주 0.000000
가해운전자차종 0.023508
가해운전자성별 0.023454
가해운전자연령 0.023602
가해운전자상해정도 0.057904
피해운전자차종 0.030342
피해운전자성별 0.030342
피해운전자연령 0.032705
피해운전자상해정도 0.030342
dtype: float64
이러한 결측치는 데이터를 분석하는 과정에서 어려움을 줄 뿐 아니라, 처리를 하지 않고 분석할 시 신뢰도 있는 결과를 도출하는데 어려움이 있기 때문에 꼭 결측치를 처리하는 과정이 필요하다.
대표적인 결측치의 처리 방법에는 결측치 제거와 결측치 대체가 있다.
결측치 제거
결측치를 제거하는 방법은 간단하다. 바로 dropna() 함수를 이용하는 것이다.
df.dropna()
dropna()함수는 데이터 내에 null값이 하나라도 포함된 모든 row를 지워버린다. 얼핏보면 간단하게 결측치를 처리하는 방법처럼 보이지만, 충분히 대체 가능한 결측치가 null일 경우, 결측치 대체 과정을 통해 데이터를 살릴 수 있지만, 이러한 과정 없이 dropna()함수를 사용하면 단순히 null값을 포함한다는 이유만으로 유의미한 데이터가 사라진다는 치명적인 단점이 존재한다.
df.shape
(74485, 28)
shape함수는 해당 데이터의 행,열 수를 반환하는 함수이다. shape함수를 통해 총 74485row, 28column의 데이터가 있는 것을 알 수 있다.
여기서 dropna()를 한 결과를 보면,
df.dropna().shape
(66940, 28)
이렇듯, 결측치가 존재하는 모든 row를 제거해 7,545건의 데이터가 손실되었다.
그래서 dropna()함수는 결측치 대체 처리가 마무리 된 후 불필요한 데이터를 제거하기 위한 용도로 사용한다고 보면 된다.
또한, dropna()함수는 데이터 원형을 훼손하지는 않기 때문에 따로 변수에 저장하거나 update 해주어야한다.
df = df.dropna()
결측치 대체 : 새로운 범주 생성
결측치 대체 처리를 할 때 주의해야할 점은, 결측치가 존재하는 컬럼들을 하나하나 조회하며 대체 가능한 컬럼인지 판단하는 것이다. 결측치를 처리하는 방법에도 여러가지가 있는데, 그 중 새로운 범주를 생성해 결측치를 처리하는 방법을 알아보자.
df['기상상태'].value_counts(normalize=True, dropna=False)
맑음 0.884366
비 0.062912
흐림 0.038853
NaN 0.012217
눈 0.001598
안개 0.000054
Name: 기상상태, dtype: float64
기상상태 컬럼의 결측치인 NaN값을 보면 낮은 수치라 무시해도 될 것 같지만, 취합된 눈과 안개보다 높은 수치라 결코 무시할 수 없는 양의 데이터라고 판단할 수 있다. 하지만 NaN 값으로 입력된 이유가 있을 것이다. 날씨가 눈,비 어느쪽으로 집계하기 힘든 진눈개비가 오고 있다거나, 마른 하늘에 벼락만 치고 있거나 등등. 그렇기 때문에 한 카테고리로 합치는 것은 불가능한 것 처럼 보인다. 이럴 때는 '기타(etc)'라는 항목으로 명칭을 부여해주면서 데이터를 살릴 수 있다.
df['기상상태'] = df['기상상태'].fillna('기타')
df['기상상태'].value_counts(normalize=True)
맑음 0.884366
비 0.062912
흐림 0.038853
기타 0.012217
눈 0.001598
안개 0.000054
Name: 기상상태, dtype: float64
fillna(param) 함수는 해당 데이터의 NaN값을 param에 입력한 인자로 대체한다. 기상상태 컬럼의 NaN값이 '기타'로 변한 것을 확인할 수 있다.
결측치 대체 : 최빈값 대체
최빈값을 대체해 결측치를 대체하는 방법은 주로 값의 수(count)가 많지 않고 통계적, 상식적으로 선택지가 정해져 있는 경우에 주로 사용한다. 대표적인 예시로 '가해운전자성별'을 통해 알아보자.
df['가해운전자성별'].value_counts(normalize=True, dropna=False)
남 0.799356
여 0.177190
NaN 0.023454
Name: 가해운전자성별, dtype: float64
성별은 남/여 밖에 없는데 NaN값이 있는게 상식적으로 말이 안되며, 해당 데이터처럼 한 쪽이 압도적으로 높은 수치를 기록하고 있는 경우에는 최빈값으로 대체할 명분이 충분하다.
df['가해운전자성별'] = df['가해운전자성별'].fillna('남')
df['가해운전자성별'].value_counts(normalize=True)
남 0.82281
여 0.17719
Name: 가해운전자성별, dtype: float64
결측치 대체 : 중앙값 대체
결측치 대체의 마지막 방법은 중앙값 대체이다. 중앙값은 숫자 형식의 데이터 집합이면서 평균값과 중앙값의 차이가 크지 않아 결측치를 중앙값으로 대체했을 때 전체적인 데이터 통계에 큰 영향을 끼치지 않을 만한 데이터 집합을 처리할 때 사용할 수 있다. 대표적인 예시로 '가해운전자연령'을 통해 알아보자.
df['가해운전자연령'].describe()
count 72727.000000
mean 48.063498
std 15.705427
min 5.000000
25% 35.000000
50% 50.000000
75% 60.000000
max 98.000000
Name: 가해운전자연령, dtype: float64
가해운전자 연령의 통계를 보면, 약 1,700건 가량의 결측치가 존재하며 평균이 약 48세이고 중앙값이 50세이다. 그렇기에 해당 결측치들은 충분히 중앙값으로 대체할 수 있다. 평균으로도 대체할 수 있겠지만 나이는 기본적으로 정수 데이터가 담긴 컬럼이므로 정수값인 중앙값으로 진행했다.
df['가해운전자연령'] = df['가해운전자연령'].fillna(df['가해운전자연령'].median())
df['가해운전자연령'] = df['가해운전자연령'].astype('int')
df['가해운전자연령'].isnull().sum()
0
데이터 클렌징은 정답이 없는 것 같다. "통계적으로", "상식적으로", "일반적으로"와 같은 개념은 개발자적인 사고방식과 완전히 상반된다고 생각한다. 항상 정답을 맞추려 노력하다가, 정답이 없는 문제를 가장 "상식적으로" 풀어낸다는게 가장 힘든 것 같다. 아직 시작한지 얼마 안되서 익숙치 않고 노련하지 않은 것이 큰 문제겠지만, 이런 문제는 더 많은 데이터를 클렌징하고 더 많은 경험을 한다면 충분히 극복할 수 있는 문제라고 생각한다.
'빅데이터분석👨💻' 카테고리의 다른 글
[Python] 빅데이터 분석 기초 - 객체 병합 함수 (concat, join, merge) (0) | 2023.12.18 |
---|---|
[Python] 빅데이터 분석 기초 - 집계 (Aggregation) (0) | 2023.11.27 |
[Python] 빅데이터 분석 기초 - 필터링 (Filtering) (1) | 2023.11.27 |
[Python] 빅데이터 분석 기초 - 인덱싱 (Indexing) (0) | 2023.11.27 |
[Python] 빅데이터 분석 기초 - 이상치 처리 (Check the Outlier) (0) | 2023.11.24 |