코드 보기
import numpy as np
import pandas as pd
df = pd.DataFrame([5, 27.3, np.nan, -16], columns=["numbers"])
df| numbers | |
|---|---|
| 0 | 5.0 |
| 1 | 27.3 |
| 2 | NaN |
| 3 | -16.0 |
이 장에서는 누락된 값을 다루기 위한 도구와 요령을 살펴봅니다. 먼저 NA로 기록된 누락된 값을 처리하는 일반적인 도구들에 대해 논의하겠습니다. 그런 다음 데이터에서 단순히 존재하지 않는 값인 암시적 누락값(implicitly missing values)의 개념을 탐구하고, 이를 명시적으로 만드는 도구들을 보여드리겠습니다. 마지막으로 데이터에 나타나지 않는 카테고리로 인해 발생하는 비어 있는 그룹(empty groups)에 대한 논의로 마무리하겠습니다.
이 장에서는 pandas 데이터 분석 패키지를 사용합니다.
먼저 명시적 누락값, 즉 NA나 nan이 표시된 셀을 생성하거나 제거하는 유용한 도구들을 살펴보겠습니다.
pandas의 누락된 값들이 모두 똑같지는 않다는 점에 유의해야 합니다!
예를 들어, pandas의 실수(예: float64 dtype)는 ‘nan’(Not a Number의 약자)을 사용합니다:
import numpy as np
import pandas as pd
df = pd.DataFrame([5, 27.3, np.nan, -16], columns=["numbers"])
df| numbers | |
|---|---|
| 0 | 5.0 |
| 1 | 27.3 |
| 2 | NaN |
| 3 | -16.0 |
하지만 이것이 유일한 방법은 아닙니다! 파이썬 내장 None 값(여기서는 유효한 값들이 모두 부동 소수점이므로 NaN으로 변환됨)과 pandas의 pd.NA를 사용할 수도 있습니다:
numbers = pd.DataFrame([pd.NA, 27.3, np.nan, -16, None], columns=["numbers"])
numbers| numbers | |
|---|---|
| 0 | <NA> |
| 1 | 27.3 |
| 2 | NaN |
| 3 | -16 |
| 4 | None |
하지만 객체(object) 데이터 타입(문자열의 기본값)의 경우 이러한 타입들이 공존할 수 있습니다:
fruits = pd.DataFrame(
["orange", np.nan, "apple", None, "banana", pd.NA], columns=["fruit"]
)
fruits| fruit | |
|---|---|
| 0 | orange |
| 1 | NaN |
| 2 | apple |
| 3 | None |
| 4 | banana |
| 5 | <NA> |
이러한 모든 유형의 누락된 값은 pandas의 .isna() 함수를 사용하여 찾을 수 있습니다. 이 함수는 값이 누락된 경우 True인 불리언 열을 반환합니다.
fruits.isna()| fruit | |
|---|---|
| 0 | False |
| 1 | True |
| 2 | False |
| 3 | True |
| 4 | False |
| 5 | True |
편의상 notna() 함수도 제공됩니다.
누락된 값을 처리하는 데는 여러 옵션이 있습니다. fillna() 함수가 이 역할을 합니다. 테스트 데이터를 사용하여 살펴보겠습니다:
nan_df = pd.DataFrame(
[
[np.nan, 2, None, 0],
[3, 4, np.nan, 1],
[5, np.nan, np.nan, pd.NA],
[np.nan, 3, np.nan, 4],
],
columns=list("ABCD"),
)
nan_df| A | B | C | D | |
|---|---|---|---|---|
| 0 | NaN | 2.0 | NaN | 0 |
| 1 | 3.0 | 4.0 | NaN | 1 |
| 2 | 5.0 | NaN | NaN | <NA> |
| 3 | NaN | 3.0 | NaN | 4 |
먼저, 모든 누락된 값을 단일 고정값으로 채울 수 있습니다:
nan_df.fillna(0)/tmp/ipykernel_5940/4054961691.py:1: FutureWarning: Downcasting object dtype arrays on .fillna, .ffill, .bfill is deprecated and will change in a future version. Call result.infer_objects(copy=False) instead. To opt-in to the future behavior, set `pd.set_option('future.no_silent_downcasting', True)`
nan_df.fillna(0)
| A | B | C | D | |
|---|---|---|---|---|
| 0 | 0.0 | 2.0 | 0.0 | 0 |
| 1 | 3.0 | 4.0 | 0.0 | 1 |
| 2 | 5.0 | 0.0 | 0.0 | 0 |
| 3 | 0.0 | 3.0 | 0.0 | 4 |
열 단위로 수행할 수도 있습니다. 여기서는 ‘A’, ‘B’, ‘C’, ‘D’ 열의 모든 NaN 요소를 각각 0, 1, 2, 3으로 바꿉니다.
nan_df.fillna(value={"A": 0, "B": 1, "C": 2, "D": 3})/tmp/ipykernel_5940/2397886090.py:1: FutureWarning: Downcasting object dtype arrays on .fillna, .ffill, .bfill is deprecated and will change in a future version. Call result.infer_objects(copy=False) instead. To opt-in to the future behavior, set `pd.set_option('future.no_silent_downcasting', True)`
nan_df.fillna(value={"A": 0, "B": 1, "C": 2, "D": 3})
| A | B | C | D | |
|---|---|---|---|---|
| 0 | 0.0 | 2.0 | 2.0 | 0 |
| 1 | 3.0 | 4.0 | 2.0 | 1 |
| 2 | 5.0 | 1.0 | 2.0 | 3 |
| 3 | 0.0 | 3.0 | 2.0 | 4 |
또한 null이 아닌 값을 (인덱스 기준으로) 앞으로 또는 뒤로 전파할 수 있습니다.
nan_df.fillna(method="ffill")/tmp/ipykernel_5940/1353804149.py:1: FutureWarning: DataFrame.fillna with 'method' is deprecated and will raise in a future version. Use obj.ffill() or obj.bfill() instead.
nan_df.fillna(method="ffill")
/tmp/ipykernel_5940/1353804149.py:1: FutureWarning: Downcasting object dtype arrays on .fillna, .ffill, .bfill is deprecated and will change in a future version. Call result.infer_objects(copy=False) instead. To opt-in to the future behavior, set `pd.set_option('future.no_silent_downcasting', True)`
nan_df.fillna(method="ffill")
| A | B | C | D | |
|---|---|---|---|---|
| 0 | NaN | 2.0 | NaN | 0 |
| 1 | 3.0 | 4.0 | NaN | 1 |
| 2 | 5.0 | 4.0 | NaN | 1 |
| 3 | 5.0 | 3.0 | NaN | 4 |
nan_df.fillna(method="bfill")/tmp/ipykernel_5940/2505504399.py:1: FutureWarning: DataFrame.fillna with 'method' is deprecated and will raise in a future version. Use obj.ffill() or obj.bfill() instead.
nan_df.fillna(method="bfill")
/tmp/ipykernel_5940/2505504399.py:1: FutureWarning: Downcasting object dtype arrays on .fillna, .ffill, .bfill is deprecated and will change in a future version. Call result.infer_objects(copy=False) instead. To opt-in to the future behavior, set `pd.set_option('future.no_silent_downcasting', True)`
nan_df.fillna(method="bfill")
| A | B | C | D | |
|---|---|---|---|---|
| 0 | 3.0 | 2.0 | NaN | 0 |
| 1 | 3.0 | 4.0 | NaN | 1 |
| 2 | 5.0 | 3.0 | NaN | 4 |
| 3 | NaN | 3.0 | NaN | 4 |
전방 채우기(forward fill)와 후방 채우기(backward fill) 옵션은 시계열 데이터에서 특히 유용합니다 - 단, 예측 모델링을 수행 중이라면 주의해서 사용해야 합니다!
이러한 모든 함수의 또 다른 특징은 limit= 키워드 인수를 사용하여 대체되는 NaN의 개수를 제한할 수 있다는 점입니다.
nan_df.fillna(value={"A": 0, "B": 1, "C": 2, "D": 3}, limit=1)/tmp/ipykernel_5940/1730877720.py:1: FutureWarning: Downcasting object dtype arrays on .fillna, .ffill, .bfill is deprecated and will change in a future version. Call result.infer_objects(copy=False) instead. To opt-in to the future behavior, set `pd.set_option('future.no_silent_downcasting', True)`
nan_df.fillna(value={"A": 0, "B": 1, "C": 2, "D": 3}, limit=1)
| A | B | C | D | |
|---|---|---|---|---|
| 0 | 0.0 | 2.0 | 2.0 | 0 |
| 1 | 3.0 | 4.0 | NaN | 1 |
| 2 | 5.0 | 1.0 | NaN | 3 |
| 3 | NaN | 3.0 | NaN | 4 |
물론 다른 옵션은 누락된 값을 완전히 필터링하는 것입니다. 전체 행(axis=0)을 제거할지 아니면 열(axis=1)을 제거할지에 따라 몇 가지 방법이 있습니다. (단, 이 경우 각 열에 최소 하나의 NaN이 있으므로 열 기준 제거 시 데이터가 남지 않을 것입니다!)
nan_df["A"].dropna(axis=0) # 단일 열에서 수행1 3.0
2 5.0
Name: A, dtype: float64
nan_df.dropna(axis=1)| 0 |
|---|
| 1 |
| 2 |
| 3 |
dropna()도 몇 가지 키워드 인수를 받습니다. 예를 들어 how="all"은 행이나 열의 모든 값이 NA인 경우에만 삭제합니다.
nan_df.dropna(how="all")| A | B | C | D | |
|---|---|---|---|---|
| 0 | NaN | 2.0 | NaN | 0 |
| 1 | 3.0 | 4.0 | NaN | 1 |
| 2 | 5.0 | NaN | NaN | <NA> |
| 3 | NaN | 3.0 | NaN | 4 |
thresh(임계값) 키워드도 있습니다 - 이를 사용하면 최소 몇 개 이상의 누락되지 않은 관측치가 있는 행이나 열만 유지할 수 있습니다.
NaN을 필터링하는 또 다른 방법은 불리언 열을 사용하는 일반적인 필터링 방법을 .notna() 함수와 조합하여 사용하는 것입니다. 아래 예제에서 ’A’가 NA가 아닌 행들에 대해 모든 열을 확인합니다.
nan_df[nan_df["A"].notna()]| A | B | C | D | |
|---|---|---|---|---|
| 1 | 3.0 | 4.0 | NaN | 1 |
| 2 | 5.0 | NaN | NaN | <NA> |
가끔은 반대로 어떤 구체적인 값이 실제로는 누락된 값을 나타내는 문제를 겪을 수도 있습니다. 이는 대개 누락된 값을 표현하는 적절한 방법이 없는 오래된 소프트웨어에서 생성된 데이터에서 발생하며, 99나 -999와 같은 특별한 값을 대신 사용합니다.
가능하다면 데이터를 읽어올 때 이를 처리하세요. 예를 들어 pd.read_csv()를 호출할 때 na_values= 키워드 인수를 사용하는 것입니다. 나중에 문제를 발견했거나 데이터 소스에서 읽기 시 처리를 제공하지 않는다면, 제공된 데이터를 교체하기 위해 다양한 옵션을 사용할 수 있습니다:
stata_df = pd.DataFrame([[3, 4, 5], [-7, 4, -99], [-99, 6, 5]], columns=list("ABC"))
stata_df| A | B | C | |
|---|---|---|---|
| 0 | 3 | 4 | 5 |
| 1 | -7 | 4 | -99 |
| 2 | -99 | 6 | 5 |
가장 쉬운 옵션은 아마도 .replace()일 것입니다:
stata_df.replace({-99: pd.NA})| A | B | C | |
|---|---|---|---|
| 0 | 3 | 4 | 5 |
| 1 | -7 | 4 | <NA> |
| 2 | <NA> | 6 | 5 |
.replace()는 딕셔너리를 받기 때문에 여러 값을 한 번에 교체할 수 있습니다:
stata_df.replace({-99: pd.NA, -7: pd.NA})| A | B | C | |
|---|---|---|---|
| 0 | 3 | 4 | 5 |
| 1 | <NA> | 4 | <NA> |
| 2 | <NA> | 6 | 5 |
이는 데이터 프레임의 모든 열에 적용된다는 점에 유의하세요. 단 하나에만 적용하려면 특정 열을 먼저 선택하세요.
지금까지는 데이터에서 확인할 수 있는 NA와 같은 명시적 누락값에 대해 이야기했습니다. 하지만 누락된 값은 전체 데이터 행 자체가 데이터에서 단순히 누락된 경우인 암시적 누락값이 될 수도 있습니다. 매 분기 주식 가격을 기록하는 간단한 데이터셋으로 차이를 설명해 보겠습니다:
stocks = pd.DataFrame(
{
"year": [2020, 2020, 2020, 2020, 2021, 2021, 2021],
"qtr": [1, 2, 3, 4, 2, 3, 4],
"price": [1.88, 0.59, 0.35, np.nan, 0.92, 0.17, 2.66],
}
)
stocks| year | qtr | price | |
|---|---|---|---|
| 0 | 2020 | 1 | 1.88 |
| 1 | 2020 | 2 | 0.59 |
| 2 | 2020 | 3 | 0.35 |
| 3 | 2020 | 4 | NaN |
| 4 | 2021 | 2 | 0.92 |
| 5 | 2021 | 3 | 0.17 |
| 6 | 2021 | 4 | 2.66 |
이 데이터셋에는 두 개의 누락된 관측치가 있습니다:
2020년 4분기 가격은 값이 NA이므로 명시적으로 누락되었습니다.
2021년 1분기 가격은 데이터셋에 아예 나타나지 않으므로 암시적으로 누락되었습니다.
차이를 생각하는 한 가지 방법은 다음과 같은 화두(koan)를 통하는 것입니다:
명시적 누락값은 부재(absence)의 존재(presence)이다.
암시적 누락값은 존재(presence)의 부재(absence)이다.
가끔은 물리적으로 다룰 수 있도록 암시적 누락값을 명시적으로 만들고 싶을 때가 있습니다. 다른 경우에는 데이터 구조에 의해 강제된 명시적 누락값을 제거하고 싶을 수도 있습니다. 다음 섹션들에서는 암시적 누락과 명시적 누락 사이를 이동하는 몇 가지 도구들에 대해 논의합니다.
암시적 누락값을 명시적으로 만들 수 있는 도구 하나를 이미 보았습니다: 피벗(pivoting)입니다. 데이터를 넓게 만들면 모든 행과 새로운 열의 조합에 어떤 값이 있어야 하므로 암시적 누락값이 명시적이 될 수 있습니다. 예를 들어 stocks를 피벗하여 quarter를 열로 두면(그리고 year를 인덱스로 만들면), 두 누락된 값 모두 명시적이 됩니다:
stocks.pivot(columns="qtr", values="price", index="year")| qtr | 1 | 2 | 3 | 4 |
|---|---|---|---|---|
| year | ||||
| 2020 | 1.88 | 0.59 | 0.35 | NaN |
| 2021 | NaN | 0.92 | 0.17 | 2.66 |
기본적으로 데이터를 길게 만들면 명시적 누락값은 보존됩니다.
누락된 값의 마지막 유형은 비어 있는 그룹(empty group)으로, 범주형 데이터를 다룰 때 발생할 수 있는 데이터에 나타나지 않는 그룹입니다.
예를 들어 사람들에 대한 건강 정보가 담긴 데이터셋이 있다고 가정해 봅시다:
health = pd.DataFrame(
{
"name": ["Ikaia", "Oletta", "Leriah", "Dashay", "Tresaun"],
"smoker": ["no", "no", "previously", "no", "yes"],
"age": [34, 88, 75, 47, 56],
}
)
health["smoker"] = health["smoker"].astype("category")이제 마지막 행의 데이터를 삭제합니다:
health_cut = health.iloc[:-1, :]
health_cut| name | smoker | age | |
|---|---|---|---|
| 0 | Ikaia | no | 34 |
| 1 | Oletta | no | 88 |
| 2 | Leriah | previously | 75 |
| 3 | Dashay | no | 47 |
이제 smoker의 ‘yes’ 값은 우리 데이터 프레임의 어디에도 나타나지 않는 것처럼 보입니다. 하지만 카테고리별 빈도수를 얻기 위해 value_counts()를 실행하면, 데이터 프레임은 현재 존재하지 않는 ‘yes’ 카테고리가 있다는 것을 ’기억’하고 있음을 볼 수 있습니다:
health_cut["smoker"].value_counts()smoker
no 3
previously 1
yes 0
Name: count, dtype: int64
groupby() 연산에서도 같은 현상이 일어나는 것을 볼 수 있습니다:
health_cut.groupby("smoker")["age"].mean()/tmp/ipykernel_5940/3998383890.py:1: FutureWarning: The default of observed=False is deprecated and will be changed to True in a future version of pandas. Pass observed=False to retain current behavior or observed=True to adopt the future default and silence this warning.
health_cut.groupby("smoker")["age"].mean()
smoker
no 56.333333
previously 75.000000
yes NaN
Name: age, dtype: float64
존재하지 않는 숫자의 평균을 구했기 때문에 yes 행에 실제 값 대신 NaN을 얻게 된 것을 볼 수 있습니다(하지만 ‘yes’ 행 자체는 존재합니다).