# [데이터 전처리] 결측치

## 결측치

---

- 유효한 데이터 값이 존재하지 않는 누락 데이터를 NaN(Not a Number)으로 표시한다.

- 머신 러닝 분석 모형에 데이터를 입력하기 전 누락 데이터를 제거하거나 다른 값으로 대체하는 과정이 필요

- 누락 데이터가 많아지면 데이터의 품질이 떨어지고

- 머신러닝 알고리즘을 왜곡하는 현상 발생

**원인**

1. 데이터를 파일로 입력할 때 빠트리거나

2. 파일 형식을 변환하는 과정에서 데이터가 소실

## 결측치의 3종류

---

![Image](https://upload.cafenono.com/image/slashpagePost/20250201/173204_VxAoi93ubHnrDT9XYp?q=80&s=1280x180&t=outside&f=webp)

### **Missing Completely At Random (MCAR)**

- 완전 무작위 결측

- 결측값의 발생이 다른 변수와 상관이 없는 경우

- 전산 오류, 통신 문제 등으로 데이터가 누락된 경우

### **Missing At Random (MAR)**

- 무작위 결측

- 결측값의 발생이 특정 변수와 관련이 있으나 얻고자 하는 결과와는 상관이 없는 경우

- 어떤 상황으로 인해서 발생하는 경우, 기업에서 제조과정에서 A제조공정 B제조공정 중 A가 갑자기 문제가 발생해서 결측치가 발생 A공정에만 문제가 발생한 결측치

### **No Missing At Random (NMAR)**

- 비무작위 결측

- 결측값 발생이 다른 변수와 상관이 있는 경우

## 결측치 확인

---

### 결측치 만들기

```javascript
import pandas as pd
import numpy as np
from sklearn.datasets import fetch_california_housing

# 데이터셋 불러오기
california = fetch_california_housing()
X, y = california.data, california.target

# 결측치를 추가할 특성 선택
np.random.seed(42)
missing_percentage = 0.1  # 결측치 비율
n_missing_samples = int(missing_percentage * X.shape[0])
missing_samples = np.random.choice(X.shape[0], n_missing_samples, replace=False)
missing_features = np.random.choice(X.shape[1], n_missing_samples, replace=True)

# 결측치 추가
X_with_missing = X.copy()
X_with_missing[missing_samples, missing_features] = np.nan

# 데이터프레임 만들기
df = pd.DataFrame(X_with_missing, columns=california.feature_names)
df['target'] = y
df.head()
```

### 결측치 확인 - **[is.na()](https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.isna.html)**

```javascript
DataFrame.isna()       # 결측치 True/False 반환
DataFrame.isna().sum() # 열별 결측치 개수
```

- 데이터 프레임 내에 결측 값을 확인하여 그 결과를 True / False로 반환

- isna()를 따로 사용하기보다 sum()을 같이 사용하여 주로 결측치의 개수를 파악

### 결측치 시각화 - [missingno](https://www.rdocumentation.org/packages/poppr/versions/2.9.5/topics/missingno) 라이브러리**

**count**

```javascript
import missingno as msno
import matplotlib.pyplot as plt
%matplotlib inline
msno.bar(df)
```

![Image](https://upload.cafenono.com/image/slashpagePost/20250201/173258_6JNSaidpA6lWRHWHFe?q=80&s=1280x180&t=outside&f=webp)

**matrix**

```javascript
# matrix
import missingno as msno
import matplotlib.pyplot as plt
%matplotlib inline
msno.matrix(df)
```

![Image](https://upload.cafenono.com/image/slashpagePost/20250201/173310_xru8A51uUeEJ2QvcXk?q=80&s=1280x180&t=outside&f=webp)

**heatmap**

```javascript
# heatmap
import missingno as msno
import matplotlib.pyplot as plt
%matplotlib inline
msno.heatmap(df)
```

![Image](https://upload.cafenono.com/image/slashpagePost/20250201/173322_galY4AHeyk1jVOmBvx?q=80&s=1280x180&t=outside&f=webp)

**dendrogram**

```javascript
# dendrogram
import missingno as msno
import matplotlib.pyplot as plt
%matplotlib inline
msno.dendrogram(df)
```

![Image](https://upload.cafenono.com/image/slashpagePost/20250201/173333_S7TPdi8hKFctCCYPWc?q=80&s=1280x180&t=outside&f=webp)

### 결측치 시각화 - seaborn

**seaborn heatmap**

```javascript
import seaborn as sns

sns.heatmap(df.isnull(), cbar=False)
```

![Image](https://upload.cafenono.com/image/slashpagePost/20250201/173408_khL3olaFny1xujqOGn?q=80&s=1280x180&t=outside&f=webp)

## 결측치 처리 방법

---

- 결측치 처리 방법: 아무것도 하지 않기, 데이터 제거, 데이터 대치

### 아무것도 하지 않기

- 일부 알고리즘은 결측치를 고려하여 학습한다. (XGBoost)

- 결측치를 무시하거나 대체하는 파라미터를 가지고 있는 모델도 있다.

### 데이터 제거 delete

- 행이나 열 전체를 제거하는 방법

- 결측치의 삭제에는 많은 리스크가 따른다.

- 데이터의 삭제는 데이터의 누락으로 이어질 수 있기 때문에 꼭 신중하게 진행해야 한다.

- 제거 기준 사람마다 다르기 때문에 도메인 지식을 바탕으로 적절히 평가해야한다.

    - 가이드라인:

    - 10% 미만: 삭제 or 대치

    - 10% ~ 50%: regression or model based imputation

    - 50% 이상: 해당 변수 자체 제거

**[pandas.DataFrame.dropna](https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.dropna.html)**

```javascript
DataFrame.dropna(*, axis=0, how=_NoDefault.no_default, 
    thresh=_NoDefault.no_default, subset=None, inplace=False, ignore_index=False)
```

```javascript
# 결측치가 존재하는 행을 모두 제거
df.dropna(axis=0, how='any') # index 20640 rows -> 18576 rows

# 결측치가 존재하는 열을 모두 제거
df.dropna(axis=1, how='any') # column은 target만 남음
```

- 결측치를 삭제 할 때 사용한다.

- axis: 0(index), 1(columns), default 0

- how: any, all, default 'any'

    - any: NaN이 있을 때

    - all: 모두 NaN일 때

- inplace: False, True

### 데이터 대치 imputation

- 데이터의 결측치를 다양한 값으로 대치할 수 있다.

    - 평균 mean

    - 중간값 median

    - 최빈값 mode

    - 특정 값

    - MICE

- 결측치를 확인하는 과정에서 데이터의 도메인 지식이 필요하다.

- 결측치가 유의미한 데이터일 수도 있기 때문에 도메인 지식을 잘 파악하는 것이 중요

**평균 mean, 중앙값 median**으로 대치

- 다른 feature 간의 상관관계 고려 X

- 절대 범주형 데이터엔 사용 X

- 이상치의 영향을 받는다.

**최빈값 mode, 0, 상수값**으로 대체

- 범주형 데이터에도 사용 가능

- 다른 feature 간의 상과관계 고려 X

- 상수값에 따라 이상치가 될 수 있다.

**보간법**

- 누락값 사이의 값을 평균으로 대체해서 만든다.

- 중간에 결측치가 있으면 보간법을 이용하여 결측치를 대체한다.

**선형 보간법**

- 두 점사의 거리 구하는 공식

- 1차원 두 점사이의 거리로 해당 누락값을 대체한다.

**spline 보간법**

- 기존에는 1차원으로 생각해서 보간

- spline 통해 order 차수 높이면 2차원 이상으로

**KNN 대체**

- K-Nearest Neighbor 알고리즘을 사용해 가장 근접한 데이터를 k개 찾는 방식

- 평균, 중앙값보다 정확한 경우가 있다.

- 이상치에 민감하다.

**MICE**

![Image](https://upload.cafenono.com/image/slashpagePost/20250201/173522_1d5q1sm5ysKhiaSUAN?q=80&s=1280x180&t=outside&f=webp)

- 누락된 데이터를 여러번 채우는 방식으로 여러 결측치 대치 세트를 만들어 with 함수로 특정 통계모델링을 수행

- pool 함수로 생성한 m개의 대치 세트를 평균하여 결과를 도출

- 연속형, 범위형 패턴도 처리 가능

- imputation: distribution을 토대로 m개의 데이터셋을 imputation

- analysis: m개의 완성된 데이터셋을 분석

- pooling: 평균, 분산, 신뢰 구간을 계산하여 결과를 종합

**딥러닝 이용**

- 범주형이나 숫자가 아닌 자료형에 효과적

- DNN을 이용해 누락된 값을 유추

## 결측치 대치의 다양한 방법들

---

### **[pandas.DataFrame.fillna](https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.fillna.html#pandas.DataFrame.fillna)**

```javascript
DataFrame.fillna(value=None, *, method=None, axis=None, 
                                    inplace=False, limit=None, downcast=_NoDefault.no_default)
```

- method: backfill, bfill, ffill, None, dafult None

    - backfill: backfill은 bfill과 완전히 동일한 기능을 수

    - bfill: 누락값이 나타나나 이후의 첫 번째 값으로 앞쪽의 누락값으로 변경

    - ffill: 누락값이 나타나기 전의 값으로 누락값을 변경

- axis: 0(index), 1(columns)

- inplace: False, True

**원본 데이터**

```javascript
df['Latitude'].plot()
```

![Image](https://upload.cafenono.com/image/slashpagePost/20250201/173544_nA4CSqhWWVdxYRToCE?q=80&s=1280x180&t=outside&f=webp)

**median**

```javascript
df['Latitude'].fillna(df['Latitude'].median()).plot()
```

![Image](https://upload.cafenono.com/image/slashpagePost/20250201/173811_ARxcnxyn3JbCMwPOFD?q=80&s=1280x180&t=outside&f=webp)

**mean**

```javascript
df['Latitude'].fillna(df['Latitude'].mean()).plot()
```

![Image](https://upload.cafenono.com/image/slashpagePost/20250201/173817_Td9pvFPaGFPcWAKk70?q=80&s=1280x180&t=outside&f=webp)

**ffill**

```javascript
df['Latitude'].fillna(method='ffill').plot()
```

![Image](https://upload.cafenono.com/image/slashpagePost/20250201/173826_wesrLSBD5TPQLErlbr?q=80&s=1280x180&t=outside&f=webp)

**bfill**

```javascript
df['Latitude'].fillna(method='bfill').plot()
```

![Image](https://upload.cafenono.com/image/slashpagePost/20250201/173832_rDL1jHXXzxiAmHrAKS?q=80&s=1280x180&t=outside&f=webp)

### **[pandas.DataFrame.interpolate](https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.interpolate.html#pandas.DataFrame.interpolate)**

```javascript
DataFrame.interpolate(method='linear', , axis=0, limit=None, 
                inplace=False, limit_direction=None, limit_area=None, 
                downcast=_NoDefault.no_default, *kwargs)
```

- method: default linear

-   ['linear', 'time', 'index', 'values', 'nearest', 'zero', 'slinear', 'quadratic', 'cubic', 'barycentric', 'krogh', 'spline', 'polynomial', 'from_derivatives', 'piecewise_polynomial', 'pchip', 'akima', 'cubicspline']

    - linear: 값들을 동일한 간격으로 취급

    - time: 일일 및 더 높은 해상도 데이터에서 길이의 간격 보간

    - index: 인덱스의 실제 숫자 값 사용

    - values: 인덱스의 실제 숫자 값 사용

    - pad: NaN을 기존 값으로 채운다.

    - nearest, zero, slinear, quadratic, cubic, barcentric, polynomial -> scipy.interpolate.interp1d에 전달되는 메서드 (spline은 scipy.interpolate.UnivariateSpline에 저장

    - krogh, piecewise_polynomial, spline, pchip, akima, cubicspline: scipy 보간 방법의 wrapper

- axis: 0(index), 1(columns)

- limit

- inplace: False, True

- limit_direction: forward, backward, both (optional)

- limi_area: None, inside, outisde

**linear**

```javascript
df['Latitude'].interpolate(method='linear').plot()
```

![Image](https://upload.cafenono.com/image/slashpagePost/20250201/173929_txSwW2EJ3ctikPivOa?q=80&s=1280x180&t=outside&f=webp)

**slinear**

```javascript
df['Latitude'].interpolate(method='slinear').plot()
```

![Image](https://upload.cafenono.com/image/slashpagePost/20250201/173943_SRW9hlWpk7GEhsSBIG?q=80&s=1280x180&t=outside&f=webp)

**cubic**

```javascript
df['Latitude'].interpolate(method='cubic').plot()
```

![Image](https://upload.cafenono.com/image/slashpagePost/20250201/173954_fAlF8UY7iXIGf7o11G?q=80&s=1280x180&t=outside&f=webp)

**spline**

```javascript
df['Latitude'].interpolate(method='spline', order=2).plot()
```

![Image](https://upload.cafenono.com/image/slashpagePost/20250201/174004_1cvkCGiKPNBur44wvZ?q=80&s=1280x180&t=outside&f=webp)

### [sklearn.impute.SimpleImputer](https://scikit-learn.org/stable/modules/generated/sklearn.impute.SimpleImputer.html)

```javascript
sklearn.impute.SimpleImputer(*, missing_values=nan, strategy='mean', 
        fill_value=None, copy=True, add_indicator=False, keep_empty_features=False)
```

**parameters**

| **missing_values** | int, float, str,  **np.nan(default)**,  None or pandas.NA | 결측값을 나타내는 데이터 타입 또는 값 |
| --- | --- | --- |
| **strategy** | str, default=’mean’ ’mean’: 평균 ‘median’: 중앙값 ‘most_frequent’: 최빈값 ‘constant’: 고정값 | 결측값 대체 방법 선택 |
| **fill_value** | str or numerical value, default = None | strategy가 constant일 때 채울 값 |
| **copy** | bool, default=True | 입력 데이터(X)의 복사본 생성 여부 |
| **add_indicator** | bool, default=False | 대체된 결측값을 나타내는 지표 추가 여부 |
| **keep_empty_features** | bool, default=False | 입력 데이터에서 결측값이 없는 열 유지 여부 |

**attributes**

| **indicator** | MissingIndicator | 결측값의 위치를 나타내는 지표에 대한 정보 |
| --- | --- | --- |
| **n_features_in** | int | 입력된 특성(열)의 개수 |
| **feature_names_in_** | ndarray of shape (n_features_in_,) | 입력된 특성(열)의 이름 배열 |

**mean**

```javascript
imputer_mean = SimpleImputer(strategy='mean')
df_imputed = imputer_mean.fit_transform(df)
df_imputed = pd.DataFrame(df_imputed, columns=df.columns)

df_imputed['Latitude'].plot()
```

![Image](https://upload.cafenono.com/image/slashpagePost/20250201/174427_YF3GMTjkA0fDP5aegd?q=80&s=1280x180&t=outside&f=webp)

**median**

```javascript
imputer_mean = SimpleImputer(strategy='median')
df_imputed = imputer_mean.fit_transform(df)
df_imputed = pd.DataFrame(df_imputed, columns=df.columns)

df_imputed['Latitude'].plot()
```

![Image](https://upload.cafenono.com/image/slashpagePost/20250201/174433_UcicRbVOwmqdXWE82D?q=80&s=1280x180&t=outside&f=webp)

**most_frequent**

```javascript
imputer_mean = SimpleImputer(strategy='most_frequent')
df_imputed = imputer_mean.fit_transform(df)
df_imputed = pd.DataFrame(df_imputed, columns=df.columns)

df_imputed['Latitude'].plot()
```

![Image](https://upload.cafenono.com/image/slashpagePost/20250201/174441_fpuu3vdmlBCOYo4MNq?q=80&s=1280x180&t=outside&f=webp)

### [sklearn.imputer.IterativeImputer](https://scikit-learn.org/stable/modules/generated/sklearn.impute.IterativeImputer.html)

```javascript
sklearn.impute.IterativeImputer(estimator=None, *,
missing_values=nan, sample_posterior=False, max_iter=10, tol=0.001,
n_nearest_features=None, initial_strategy='mean', fill_value=None,
imputation_order='ascending', skip_complete=False, min_value=-inf, max_value=inf,
verbose=0, random_state=None, add_indicator=False, keep_empty_features=False)
```

### [MICE](https://www.rdocumentation.org/packages/mice/versions/3.1.0/topics/mice)

```javascript
mice(data, m = 5, method = NULL, predictorMatrix, where = NULL, blocks,
  visitSequence = NULL, formulas, blots = NULL, post = NULL,
  defaultMethod = c("pmm", "logreg", "polyreg", "polr"), maxit = 5,
  printFlag = TRUE, seed = NA, data.init = NULL, ...)
```

```javascript
from sklearn.experimental import enable_iterative_imputer
from sklearn.impute import IterativeImputer

imputer_mice = IterativeImputer(random_state=111)
df_imputed = imputer_mice.fit_transform(df)
df_imputed = pd.DataFrame(df_imputed, columns=df.columns)

df_imputed['Latitude'].plot()
```

![Image](https://upload.cafenono.com/image/slashpagePost/20250201/174458_gsG9fWs61NfVcl4NBm?q=80&s=1280x180&t=outside&f=webp)

```javascript
from impyute.imputation.cs import mice

np_imputed=mice(df.values)
df_imputed = pd.DataFrame(np_imputed, columns=df.columns)
df_imputed['Latitude'].plot()
```

![Image](https://upload.cafenono.com/image/slashpagePost/20250201/174509_c9hvntmlZiZn5Xulhl?q=80&s=1280x180&t=outside&f=webp)

### [KNNImputer](https://scikit-learn.org/stable/modules/generated/sklearn.impute.KNNImputer.html)

```javascript
impute.KNNImputer(*, missing_values=nan, n_neighbors=5, weights='uniform', 
                metric='nan_euclidean', copy=True, add_indicator=False, 
                keep_empty_features=False)[source]
```

### 결측치 보간 주의점

- 도메인 지식 없이 수치로만 다루면 한 쪽으로 치우쳐지는 경향이 있다

- 기존의 데이터의 분포와 데이터의 관계를 잘 알고 해당 특성을 반영하여 보간해야한다

For the site tree, see the [root Markdown](https://slashpage.com/yerim-devnote.md).
