1학년 / 2학기 / AI-R-PY / 7강

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)
원래 값정리 결과이유
18681868그대로
1839, 38-541839, 앞까지
1865-18661865- 앞까지
[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. AWalter 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"])))
원래 값결과
LondonLondon
London; Virtue & YorstonLondon
OxfordOxford
Newcastle upon TyneNewcastle-upon-Tyne
EdinburghEdinburgh (그대로)

PART 2 · 결측치 처리

결측치는 여러 모습으로 숨어 있다

9p, 12p

property_data.csv

ST_NUMST_NAMENUM_BEDROOMSOWN_OCCUPIED
104PUTNAM3Y
197LEXINGTON3N
(빈칸)LEXINGTONn/aN
201BERKELEY112
203BERKELEY3Y
207BERKELEYn/aY
NAWASHINGTON2(빈칸)
213TREMONT--Y
215TREMONTnaY

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()

0부터 10까지 사인 곡선은 실선, 코사인 곡선은 파선으로 겹쳐 그린 선 그래프

색 지정하기

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()

위상이 1씩 다른 사인 곡선 여섯 개를 파랑, 초록, 회색, 노랑, 빨강, 연두로 그린 그래프

선 모양 지정하기

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축 범위

x축을 -1에서 11, y축을 -1.5에서 1.5로 넓혀 여백을 둔 사인 곡선

plt.plot(x, np.sin(x))
plt.title("A Sine Curve")
plt.xlabel("x")
plt.ylabel("sin(x)")
plt.show()

A Sine Curve라는 제목과 x, sin(x) 축 라벨이 붙은 사인 곡선

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()

네 측정값의 상자 그림을 2×2로 배치한 그림. Sepal width에만 위아래로 이상치 점이 있다

히스토그램

26p

dataset.hist()
plt.show()

네 측정값의 히스토그램. Petal length와 Petal width는 왼쪽 작은 값 무리와 오른쪽 큰 값 무리로 나뉜다

산점도 (scatter plot)

27p

dataset.plot(kind="scatter", x="Sepal length", y="Sepal width")
plt.show()

꽃받침 길이를 x축, 꽃받침 너비를 y축으로 150송이를 점으로 찍은 산점도

그림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%는 다.
  • 상자 그림에서 수염 밖의 점은 후보다.
오늘도 한 걸음 앞으로.

내용을 이해했다면 학습 완료를 체크해 주세요.

학습 기록은 현재 브라우저에 저장됩니다.
HJS / STUDY한지석