12. 결측치 처리와 기능 엔지니어링

🎯 이 장의 목표
  • 결측치(NaN, 빈 값)가 무엇인지 알고 찾아낸다
  • 결측치를 제거하거나 채우는 방법을 안다
  • 기능 엔지니어링으로 의미 있는 새 열을 만든다
  • 데이터 타입을 변환한다
💡 팁
현실의 데이터는 항상 지저분합니다. 빈칸, 잘못된 타입, 이상한 값이 섞여 있죠. 이 장은 그런 데이터를 분석 가능한 상태로 다듬는 법을 다룹니다. 분석 시간의 절반 이상이 이 작업이라는 말이 있을 만큼 중요합니다.

먼저: 결측치(NaN)가 뭔가요?

결측치(missing value)비어 있는 값입니다. 설문에서 답을 안 한 칸, 측정이 안 된 날의 데이터 등이 빈칸으로 남습니다. Pandas는 이 빈칸을 NaN으로 표시합니다.

🔑 새 용어 — NaN
NaN은 Not a Number(숫자가 아님)의 약자로, 값이 비어 있음을 뜻하는 특수한 표시입니다. 화면에는 NaN으로 보입니다. 숫자 열의 빈칸은 NaN으로 채워집니다.

실습용 데이터를 만듭니다. None을 넣으면 NaN이 됩니다.

PYTHON
import pandas as pd
import numpy as np

df = pd.DataFrame({
    "이름": ["철수", "영희", "민수", "지수"],
    "나이": [20, None, 25, 19],
    "점수": [85, 92, None, 95],
    "도시": ["서울", "부산", "서울", None]
})
print(df)

실행 결과:

CODE
   이름    나이    점수  도시
0  철수  20.0  85.0  서울
1  영희   NaN  92.0  부산
2  민수  25.0   NaN  서울
3  지수  19.0  95.0  None

(나이에 NaN이 생기면서 정수가 소수 20.0으로 바뀐 점도 눈여겨보세요. NaN이 섞이면 그 열은 보통 float가 됩니다.)

결측치 찾기

PYTHON
print(df.isnull())          # 각 칸이 비었는지 (참/거짓)
print("---")
print(df.isnull().sum())    # 열별 결측치 개수 (가장 유용!)

실행 결과:

CODE
      이름     나이     점수     도시
0  False  False  False  False
1  False   True  False  False
2  False  False   True  False
3  False  False  False   True
---
이름    0
나이    1
점수    1
도시    1
dtype: int64
💡 팁
df.isnull().sum()은 필수 습관: 새 데이터를 받으면 이걸로 어느 열에 빈칸이 몇 개인지 한눈에 파악하세요. isnull() 대신 isna()를 써도 똑같습니다.

결측치 처리 (1) — 제거하기

dropna()는 결측치가 있는 행(또는 열)을 버립니다.

PYTHON
print(df.dropna())             # NaN이 하나라도 있는 '행' 제거

실행 결과:

CODE
   이름    나이    점수  도시
0  철수  20.0  85.0  서울

(NaN이 없는 행은 0번뿐이라 한 줄만 남았습니다.)

⚠️ 흔한 실수
제거는 신중하게
dropna()는 간단하지만, 데이터를 통째로 버리므로 멀쩡한 다른 값까지 사라집니다. 위처럼 4행이 1행으로 줄 수도 있죠. 데이터가 아까울 땐 아래 '채우기'를 고려하세요.

특정 열 기준으로만 제거할 수도 있습니다.

PYTHON
print(df.dropna(subset=["점수"]))   # '점수'가 빈 행만 제거

결측치 처리 (2) — 채우기

fillna()는 빈칸을 특정 값으로 채웁니다.

PYTHON
print(df.fillna(0))                    # 모든 NaN을 0으로
print("---")
print(df["나이"].fillna(df["나이"].mean()))  # 나이는 '평균'으로 채우기

실행 결과:

CODE
   이름    나이    점수  도시
0  철수  20.0  85.0  서울
1  영희   0.0  92.0  부산
2  민수  25.0   0.0  서울
3  지수  19.0  95.0   0
---
0    20.000000
1    21.333333
2    25.000000
3    19.000000
Name: 나이, dtype: float64
방법언제 쓰나
dropna()빈칸이 적고, 행을 버려도 괜찮을 때
fillna(0)"없음 = 0"이 자연스러울 때 (예: 판매량)
fillna(평균/중앙값)숫자 열에서 대표값으로 메울 때
fillna(method=...)시계열에서 앞/뒤 값으로 메울 때
💡 팁
정답은 없습니다. 결측치를 어떻게 다룰지는 데이터의 의미에 달려 있습니다. "재고 빈칸 = 0"은 말이 되지만 "나이 빈칸 = 0"은 이상하죠. 맥락을 보고 판단하세요.

기능 엔지니어링: 의미 있는 새 열 만들기

🔑 새 용어 — 기능 엔지니어링(feature engineering)
기존 데이터를 조합·변형해 분석에 도움이 되는 새 열(특성)을 만드는 일입니다. 여기서 "기능(feature, 특성)"은 데이터의 한 속성, 즉 한 열을 뜻합니다. 좋은 새 열 하나가 분석의 질을 크게 바꾸기도 합니다.

11장에서 맛본 새 열 만들기를 더 본격적으로 봅시다.

PYTHON
import pandas as pd

df = pd.DataFrame({
    "이름": ["철수", "영희", "민수"],
    "키": [170, 165, 180],
    "몸무게": [70, 55, 75]
})

# BMI = 몸무게 / (키(m))^2
df["BMI"] = df["몸무게"] / (df["키"] / 100) ** 2
df["BMI"] = df["BMI"].round(1)   # 소수 1자리로 반올림
print(df)

실행 결과:

CODE
   이름   키  몸무게   BMI
0  철수  170   70  24.2
1  영희  165   55  20.2
2  민수  180   75  23.1

구간으로 나누기 — 범주 만들기

연속된 숫자를 구간(범주)으로 묶으면 분석이 쉬워집니다. apply로 직접 만든 함수를 적용할 수 있습니다.

PYTHON
def bmi_group(bmi):
    if bmi < 18.5:
        return "저체중"
    elif bmi < 23:
        return "정상"
    else:
        return "과체중"

df["판정"] = df["BMI"].apply(bmi_group)
print(df[["이름", "BMI", "판정"]])

실행 결과:

CODE
   이름   BMI  판정
0  철수  24.2  과체중
1  영희  20.2  정상
2  민수  23.1  과체중
🔑 새 용어 — apply
apply(함수)열의 각 값에 함수를 하나씩 적용합니다. 0부의 map과 비슷한 발상입니다. 람다와 함께 자주 쓰입니다: df["BMI"].apply(lambda x: "정상" if x < 23 else "주의").

데이터 타입 변환하기

열의 타입이 잘못돼 있으면(예: 숫자가 글자로 저장됨) 계산이 안 됩니다. astype으로 바꿉니다.

PYTHON
import pandas as pd

df = pd.DataFrame({
    "상품": ["A", "B", "C"],
    "가격": ["1000", "2000", "3000"]   # 숫자인데 글자(문자열)로 저장됨!
})
print(df.dtypes)        # 타입 확인 → object(글자)

df["가격"] = df["가격"].astype(int)   # 정수로 변환
print(df.dtypes)
print(df["가격"].sum())  # 이제 계산 가능

실행 결과:

CODE
상품    object
가격    object
dtype: object
상품    object
가격     int64
dtype: object
6000
🔑 새 용어 — dtype과 object
앞서 본 dtype은 열의 데이터 타입입니다. object는 보통 문자열(글자)을 뜻합니다. "숫자처럼 보이는데 object"라면 글자로 저장된 것이니, astype(int)astype(float)로 바꿔야 계산됩니다.

자주 쓰는 변환:

코드변환
astype(int)정수로
astype(float)소수로
astype(str)글자로
pd.to_datetime(열)날짜/시간으로 (17장)
pd.to_numeric(열, errors="coerce")숫자로 (변환 실패 시 NaN)
💡 팁
pd.to_numeric(..., errors="coerce")는 "1,000"이나 "abc"처럼 변환 안 되는 값을 만나면 에러 대신 NaN으로 처리해 줘서, 지저분한 실데이터에 유용합니다.

🛠 미니 챌린지

PYTHON
import pandas as pd
df = pd.DataFrame({
    "학생": ["가", "나", "다", "라"],
    "중간": [80, None, 90, 70],
    "기말": [85, 88, None, 75]
})
  1. 각 열의 결측치 개수를 출력하세요.
  2. 중간의 빈칸을 중간 열의 평균으로 채우세요.
  3. 중간기말의 평균을 담은 총점평균이라는 새 열을 만드세요. (빈칸은 2번에서 채운 뒤, 기말의 빈칸도 평균으로 채우고 계산)

✅ 미니 챌린지 해설

PYTHON
# 1. 결측치 개수
print(df.isnull().sum())

실행 결과:

CODE
학생    0
중간    1
기말    1
dtype: int64
PYTHON
# 2. 중간 빈칸을 평균으로
df["중간"] = df["중간"].fillna(df["중간"].mean())
# 3. 기말도 채우고 평균 열 만들기
df["기말"] = df["기말"].fillna(df["기말"].mean())
df["총점평균"] = (df["중간"] + df["기말"]) / 2
print(df)

실행 결과:

CODE
  학생    중간         기말     총점평균
0  가  80.0  85.000000  82.500000
1  나  80.0  88.000000  84.000000
2  다  90.0  82.666667  86.333333
3  라  70.0  75.000000  72.500000

(중간 평균은 (80+90+70)/3=80, 기말 평균은 (85+88+75)/3≈82.67)

이 장에서 배운 것

  • 결측치는 빈 값(NaN)이며, df.isnull().sum()으로 열별 개수를 파악한다.
  • dropna()로 제거하거나 fillna()로 채우며, 선택은 데이터의 의미에 달렸다.
  • 기능 엔지니어링으로 의미 있는 새 열을 만들고, apply로 함수를 적용한다.
  • astype·to_numeric·to_datetime으로 타입을 변환한다. object는 보통 글자다.

✍️ 확인 문제

  1. NaN은 무엇의 약자이며 무엇을 뜻하나요?
  2. dropna()를 쓸 때 주의할 점은 무엇인가요?
  3. 어떤 열의 dtype이 object인데 숫자가 들어 있습니다. 합계를 구하려면 먼저 무엇을 해야 하나요?
데이터를 깨끗이 다듬었습니다. 다음 부에서는 여러 표를 합치고(concat·merge) 그룹별로 묶어 요약하는(group-by) 법을 배웁니다.
👉 13. 연결·병합·결합