7주차: 데이터 클리닝, 시각화, 기술통계
이번 강의에서 배울 것
- drop과 set_index로 분석에 필요한 열과 인덱스만 남길 수 있다.
- apply와 사용자 함수, np.where로 지저분한 문자열 열을 정리할 수 있다.
- na_values와 try/except로 여러 형태의 결측치를 찾아 NaN으로 통일할 수 있다.
- 결측치를 상수·중앙값으로 채우고 그 선택의 근거를 설명할 수 있다.
- matplotlib로 선 그래프의 색·선 모양·축 범위·제목·범례를 지정할 수 있다.
- describe 결과와 상자 그림·히스토그램·산점도를 읽고 해석할 수 있다.
학습 경로
이전: 6주차 · 정규표현식, XML, JSON · 과목 처음: 2주차
범위: 7주차 슬라이드 3–27 · Drill 701–708 · 실습 데이터 BL-Flickr-Images-Book.csv, property_data.csv, iris
단원 도입
| 핵심 포인트 | 중요도 | 이렇게 확인한다 |
|---|---|---|
na_values로 "n/a", "na", "--"를 결측치로 읽기 | ★★★ | 읽기 전후 isnull() 결과 비교하기 |
apply(함수)를 Series와 DataFrame(axis=1)에 쓰는 차이 | ★★★ | Drill 702와 703의 함수 인자 비교하기 |
| 결측치 채우기: 상수 vs 중앙값 | ★★ | 중앙값을 쓰는 이유 설명하기 |
np.where(조건, 참일 때, 거짓일 때) 중첩 | ★★ | 결과 값 예측하기 |
describe()의 count·mean·std·사분위수 | ★★ | 표에서 값 읽기 |
| 상자 그림의 이상치, 히스토그램, 산점도 해석 | ★★ | 그림을 보고 특징 말하기 |
plt.plot의 color·linestyle·label 옵션 | ★ | 서식 문자열 '-g', ':b' 읽기 |
형광펜 읽는 법: 노랑 핵심 정의·결론 · 빨강 헷갈리기 쉬운 구분·함정 · 파랑 외워야 할 순서·목록
코드 읽는 법: 코드 옆 주석의 # → 뒤가 실제 실행 결과다. 슬라이드 코드 중 최신 Pandas·NumPy에서 경고나 오류가 나는 부분은 최신 버전에서 바꿔 쓸 점 박스에 정리했다.
PART 1 · 문자열 클리닝
데이터 불러오기
3p
영국 도서관(British Library)의 도서 목록 데이터다. 출판일·출판지·제목 열에 괄호, 쉼표, 저자 이름이 섞여 있어 그대로는 분석하기 어렵다.
import pandas as pd
import numpy as np
df = pd.read_csv("Datasets/BL-Flickr-Images-Book.csv")
df.head()Drill 701 · 필요 없는 열 제거와 인덱스 설정
4p
to_drop = ["Edition Statement", "Corporate Author", "Corporate Contributors",
"Former owner", "Engraver", "Contributors", "Issuance type", "Shelfmarks"]
df.drop(to_drop, inplace=True, axis=1) # 열 8개 삭제
df.set_index("Identifier", inplace=True) # Identifier 열을 행 인덱스로
df.head()| 코드 | 뜻 |
|---|---|
axis=1 | 열 방향으로 삭제 (axis=0이면 행) |
inplace=True | 새 DataFrame을 돌려주지 않고 원본을 직접 수정 |
set_index("Identifier") | 그 열을 행 이름으로 바꿈 → df.loc[206]처럼 도서 번호로 조회 가능 |
Drill 702 · 출판일 정리: DataFrame.apply(axis=1)
5p
Date of Publication 열에는 1879 [1878], 1839, 38-54, [1897?]처럼 연도 외의 문자가 섞여 있다. 목표는 첫 번째 연도만 남기기다.
unwanted_characters = ["[", ",", "-"]
def clean_dates(item): # item = 행 하나 (Series)
dop = str(item.loc["Date of Publication"])
if dop == "nan" or dop[0] == "[": # 비어 있거나 [로 시작하면 결측
return np.nan
for character in unwanted_characters:
if character in dop:
character_index = dop.find(character)
dop = dop[:character_index] # 그 문자 앞까지만 남김
return dop
df["Date of Publication"] = df.apply(clean_dates, axis=1)| 원래 값 | 정리 결과 | 이유 |
|---|---|---|
1868 | 1868 | 그대로 |
1839, 38-54 | 1839 | , 앞까지 |
1865-1866 | 1865 | - 앞까지 |
[1897?] | NaN | [로 시작 |
1879 [1878] | 1879 | [ 앞까지 (끝에 공백이 남음) |
Drill 703 · 같은 정리를 Series.apply로
6p
def clean_dates(dop): # dop = 값 하나
dop = str(dop)
if dop.startswith("[") or dop == "nan":
return "NaN"
for character in unwanted_characters:
if character in dop:
character_index = dop.find(character)
dop = dop[:character_index]
return dop
df["Date of Publication"] = df["Date of Publication"].apply(clean_dates)Drill 704 · 제목 정리
7p
def clean_title(title):
if title == "nan":
return "NaN"
if title[0] == "[":
title = title[1: title.find("]")] # [ ] 안쪽만
if "by" in title:
title = title[:title.find("by")] # by 앞까지 (저자 부분 제거)
elif "By" in title:
title = title[:title.find("By")]
if "[" in title:
title = title[:title.find("[")] # [ 앞까지
title = title[:-2] # 끝 두 글자 제거 (". " 같은 꼬리)
title = list(map(str.capitalize, title.split()))
return " ".join(title) # 단어마다 첫 글자 대문자
df["Title"] = df["Title"].apply(clean_title)| 원래 제목 | 정리 결과 |
|---|---|
Walter Forbes. [A novel.] By A. A | Walter Forbes |
All for Greed. [A novel. The dedication signed: A. A. D. M.] | All For Greed |
Love the Avenger. By the author of “All for Greed.” [A novel.] | Love The Avenger |
Drill 705 · np.where로 출판지 통일
8p
pub = df["Place of Publication"]
df["Place of Publication"] = np.where(
pub.str.contains("London"), "London",
np.where(pub.str.contains("Oxford"), "Oxford",
np.where(pub.eq("Newcastle upon Tyne"), "Newcastle-upon-Tyne",
df["Place of Publication"])))| 원래 값 | 결과 |
|---|---|
London | London |
London; Virtue & Yorston | London |
Oxford | Oxford |
Newcastle upon Tyne | Newcastle-upon-Tyne |
Edinburgh | Edinburgh (그대로) |
PART 2 · 결측치 처리
결측치는 여러 모습으로 숨어 있다
9p, 12p
property_data.csv
| ST_NUM | ST_NAME | NUM_BEDROOMS | OWN_OCCUPIED |
|---|---|---|---|
| 104 | PUTNAM | 3 | Y |
| 197 | LEXINGTON | 3 | N |
| (빈칸) | LEXINGTON | n/a | N |
| 201 | BERKELEY | 1 | 12 |
| 203 | BERKELEY | 3 | Y |
| 207 | BERKELEY | n/a | Y |
| NA | WASHINGTON | 2 | (빈칸) |
| 213 | TREMONT | -- | Y |
| 215 | TREMONT | na | Y |
Drill 706 · na_values로 결측치 코드 통일
10p~11p
df = pd.read_csv("property_data.csv")
df["ST_NUM"].isnull()
# → False, False, True, False, False, False, True, False, False ← 빈칸과 NA는 자동 인식
df["NUM_BEDROOMS"]
# → '3', '3', NaN, '1', '3', NaN, '2', '--', 'na' ← --, na는 문자열로 남음
missing_values = ["n/a", "na", "--"]
df = pd.read_csv("property_data.csv", na_values=missing_values)
df["NUM_BEDROOMS"]
# → 3.0, 3.0, NaN, 1.0, 3.0, NaN, 2.0, NaN, NaN ← 모두 NaN, 열도 숫자형이 됨Drill 707 · try/except로 숫자 이상값 찾기
13p
cnt = 0
for row in df["OWN_OCCUPIED"]:
try:
int(row) # 숫자로 바뀌면 → Y/N이 아닌 이상값
df.loc[cnt, "OWN_OCCUPIED"] = np.nan
except ValueError: # "Y", "N"은 숫자로 못 바꿔 여기로
pass
cnt += 1
df["OWN_OCCUPIED"]
# → 'Y', 'N', 'N', NaN, 'Y', 'Y', NaN, 'Y', 'Y' ← 12가 NaN이 됨Drill 708 · 결측치 세기와 채우기
14p
df.isnull().sum()
# → ST_NUM 2, ST_NAME 0, NUM_BEDROOMS 4, OWN_OCCUPIED 2
df["ST_NUM"] = df["ST_NUM"].fillna(125) # 상수로 채우기
median = df["NUM_BEDROOMS"].median() # → 3.0
df["NUM_BEDROOMS"] = df["NUM_BEDROOMS"].fillna(median) # 중앙값으로 채우기PART 3 · matplotlib 시각화
첫 번째 그래프
15p
import matplotlib.pyplot as plt
import numpy as np
x = np.linspace(0, 10, 100) # 0~10을 100개 점으로 (5주차 linspace)
fig = plt.figure()
plt.plot(x, np.sin(x), "-") # 실선
plt.plot(x, np.cos(x), "--") # 파선
plt.show()
색 지정하기
16p~17p
plt.plot(x, np.sin(x - 0), color="blue") # 색 이름
plt.plot(x, np.sin(x - 1), color="g") # 한 글자 색 코드 (green)
plt.plot(x, np.sin(x - 2), color="0.75") # 0~1 사이 숫자 = 회색조
plt.plot(x, np.sin(x - 3), color="#FFDD44") # 16진수 코드
plt.plot(x, np.sin(x - 4), color=(1.0, 0.2, 0.3)) # RGB 튜플 (0~1)
plt.plot(x, np.sin(x - 5), color="chartreuse") # HTML 색 이름
plt.show()
선 모양 지정하기
18p~19p
plt.plot(x, x + 0, linestyle="solid") # 실선 = "-"
plt.plot(x, x + 1, linestyle="dashed") # 파선 = "--"
plt.plot(x, x + 2, linestyle="dashdot") # 쇄선 = "-."
plt.plot(x, x + 3, linestyle="dotted") # 점선 = ":"
plt.show()
| 이름 | 줄임 기호 | 모양 |
|---|---|---|
solid | - | ────── |
dashed | -- | ─ ─ ─ |
dashdot | -. | ─ · ─ · |
dotted | : | · · · · |
축 범위, 제목, 라벨, 범례
20p~22p
plt.plot(x, np.sin(x))
plt.xlim(-1, 11) # x축 범위
plt.ylim(-1.5, 1.5) # y축 범위
plt.plot(x, np.sin(x))
plt.title("A Sine Curve")
plt.xlabel("x")
plt.ylabel("sin(x)")
plt.show()
plt.plot(x, np.sin(x), "-g", label="sin(x)") # 초록 실선
plt.plot(x, np.cos(x), ":b", label="cos(x)") # 파랑 점선
plt.axis("equal") # x, y 눈금 간격을 같게
plt.legend() # label을 모아 범례 표시
plt.show()
PART 4 · 기술통계와 시각화
iris 데이터 준비
23p
붓꽃(iris) 150송이의 꽃받침(sepal)·꽃잎(petal) 길이와 너비, 품종(0, 1, 2)을 담은 대표적인 연습용 데이터다.
from sklearn import datasets
import pandas as pd
import matplotlib.pyplot as plt
iris = datasets.load_iris()
labels = pd.DataFrame(iris.target)
labels.columns = ["labels"]
dataset = pd.DataFrame(iris.data)
dataset.columns = ["Sepal length", "Sepal width", "Petal length", "Petal width"]
data = pd.concat([dataset, labels], axis=1) # 좌우로 붙이기 → 열 5개describe: 요약 통계
24p
print(dataset.describe())
# Sepal length Sepal width Petal length Petal width
# count 150.000000 150.000000 150.000000 150.000000
# mean 5.843333 3.057333 3.758000 1.199333
# std 0.828066 0.435866 1.765298 0.762238
# min 4.300000 2.000000 1.000000 0.100000
# 25% 5.100000 2.800000 1.600000 0.300000
# 50% 5.800000 3.000000 4.350000 1.300000
# 75% 6.400000 3.300000 5.100000 1.800000
# max 7.900000 4.400000 6.900000 2.500000
print(dataset.isnull().sum()) # → 네 열 모두 0| 행 | 뜻 |
|---|---|
count | 결측이 아닌 값의 개수 |
mean | 평균 |
std | 표준편차 (Pandas는 n−1로 나눔) |
min / max | 최솟값 / 최댓값 |
25% | 제1사분위수 Q1: 아래에서 25% 지점 |
50% | 중앙값: 제2사분위수 |
75% | 제3사분위수 Q3: 아래에서 75% 지점 |
상자 그림 (box plot)
25p
dataset.plot(kind="box", subplots=True, layout=(2, 2), sharex=False, sharey=False)
plt.show()
히스토그램
26p
dataset.hist()
plt.show()
산점도 (scatter plot)
27p
dataset.plot(kind="scatter", x="Sepal length", y="Sepal width")
plt.show()
| 그림 | kind= | 보는 것 |
|---|---|---|
| 선 그래프 | "line" (기본) | 순서·시간에 따른 변화 |
| 상자 그림 | "box" | 사분위수, 퍼짐, 이상치 |
| 히스토그램 | "hist" 또는 .hist() | 한 변수의 분포 모양 |
| 산점도 | "scatter" | 두 변수의 관계 |
| 막대 그래프 | "bar" | 범주별 크기 비교 |
정답과 해설
1. df.apply(f, axis=1)의 f는 행 하나(Series)를, df["col"].apply(f)의 f는 값 하나를 받는다.
2. 문자열 "NaN"은 실제 결측값이 아니라서 isnull(), dropna(), fillna()가 결측으로 처리하지 않는다. 결측 개수가 틀리고 숫자 변환도 실패한다. np.nan을 돌려줘야 한다.
3. ['양수', '영', '음수']. 3은 첫 조건이 참, 0은 첫 조건이 거짓이고 두 번째 조건이 참, −2는 둘 다 거짓이다.
4. 빨간 점선이 data라는 이름으로 범례에 표시된다. :는 점선, r은 빨강이다.
5. 중앙값(제2사분위수). 값을 크기순으로 늘어놓았을 때 가운데 값이다.
6. 상자에서 1.5×IQR보다 멀리 떨어진 이상치 후보다. 측정 오류인지, 실제로 드문 값인지 따로 확인해야 한다.
플래시카드
7주차 — pd.read_csv(..., na_values=["n/a", "na", "--"])는 무엇을 하는가?
목록의 문자열을 결측치(NaN)로 읽는다. 빈칸과 NA는 원래 자동 인식되지만 --, 소문자 na 같은 비표준 표시는 이렇게 알려 줘야 한다.
7주차 — df.apply(f, axis=1)과 df["col"].apply(f)의 차이는?
앞은 함수가 행 하나(Series)를 받아 item.loc["열"]로 값을 꺼내고, 뒤는 함수가 값 하나를 바로 받는다. 한 열만 고칠 때는 뒤쪽이 단순하다.
7주차 — np.where(조건, A, B)의 뜻은?
조건이 True인 자리는 A, False인 자리는 B를 고른다. B 자리에 np.where를 다시 넣으면 if-elif-else처럼 차례로 검사한다.
7주차 — Drill 707에서 try/except로 이상값을 찾는 원리는?
int("Y")는 ValueError가 나서 그대로 두고, int("12")는 성공하므로 숫자인 칸을 NaN으로 바꾼다. 오류 발생 여부로 값의 종류를 판별한다.
7주차 — 결측치를 평균보다 중앙값으로 채우는 이유는?
중앙값은 이상값의 영향을 덜 받는다. 어떤 값으로 채우든 없던 정보를 넣는 것이므로 결측 비율이 크면 분포가 왜곡될 수 있다.
7주차 — 최신 Pandas에서 df["c"].fillna(0, inplace=True) 대신 써야 하는 코드는?
df["c"] = df["c"].fillna(0) 또는 df.fillna({"c": 0}, inplace=True). 열을 꺼낸 뒤 inplace로 바꾸는 방식은 Pandas 3.0부터 원본에 반영되지 않는다.
7주차 — matplotlib 서식 문자열 '-g', ':b', '--r'의 뜻은?
초록 실선, 파랑 점선, 빨강 파선. 선 모양 기호와 색 글자를 합친 것이다.
7주차 — 상자 그림의 상자 아래·가운데·위 선과 점은?
Q1(25%), 중앙값(50%), Q3(75%). 수염 밖의 점은 1.5×IQR 밖에 있는 이상치 후보다.
7주차 — 히스토그램과 산점도는 각각 무엇을 보는가?
히스토그램은 한 변수의 분포 모양(구간별 개수), 산점도는 두 변수의 관계(관측치 하나 = 점 하나)를 본다.
핵심 점검
- 열을 삭제할 때
drop에 또는columns=를 지정한다. - 비표준 결측 표시를 NaN으로 읽으려면
read_csv에 를 준다. - 열별 결측치 개수는
df.isnull().으로 센다. - 이상값의 영향을 덜 받는 대표값은 이다.
- NumPy 2.0에서는
np.NaN대신 을 쓴다. plt.legend()는 을 지정한 선만 범례에 넣는다.describe()의25%,50%,75%는 다.- 상자 그림에서 수염 밖의 점은 후보다.
내용을 이해했다면 학습 완료를 체크해 주세요.