파이썬 데이터 분석 #6 시각화: matplotlib 기본기와 차트 고르기
groupby로 집계한 표를 한참 들여다봐도 안 보이던 것이, 선 그래프 한 장을 그리는 순간 바로 드러나는 경우가 많습니다. 매출이 언제부터 꺾였는지, 어느 한 건이 평균을 통째로 끌어올렸는지 같은 질문은 숫자보다 그림이 훨씬 빠릅니다. 이번 글에서는 matplotlib의 최소 구조를 잡고, 목적에 맞는 차트를 고르는 기준과 한국 환경 단골 문제인 한글 폰트 깨짐까지 정리하겠습니다.
- #1 시작
- #2 불러오기와 탐색
- #3 선택과 필터
- #4 변형과 결측치
- #5 그룹과 집계·결합
- #6 시각화 ← 이번 글
- #7 polars 맛보기와 마무리
표보다 그림이 빠른 순간 #
요약 통계는 추세와 이상치에 약합니다. describe()가 알려주는 평균과 표준편차가 같아도, 시간에 따라 꾸준히 오르는 데이터와 들쭉날쭉한 데이터는 전혀 다른 데이터입니다. 추세는 선을 그려야 보이고, 이상치는 점을 찍어야 보입니다. 그래서 탐색 단계에서 그림은 선택이 아니라 기본 도구입니다.
먼저 이번 글 전체에서 쓸 예제 데이터를 만들겠습니다. #5에서 다룬 주문 데이터와 같은 모양입니다.
import pandas as pd
import numpy as np
rng = np.random.default_rng(42)
dates = pd.date_range("2026-01-01", periods=180, freq="D")
df = pd.DataFrame({
"주문일": rng.choice(dates, size=600),
"카테고리": rng.choice(["의류", "식품", "가전", "도서"], size=600),
"금액": rng.integers(5_000, 120_000, size=600),
"수량": rng.integers(1, 6, size=600),
})matplotlib의 최소 모델: Figure와 Axes #
matplotlib은 배울 게 많아 보이지만, 실무에서 필요한 구조는 두 개뿐입니다.
- Figure: 도화지 전체입니다. 크기와 저장 단위가 여기에 붙습니다.
- Axes: 도화지 위의 좌표평면 하나입니다. 실제 그리기는 전부 Axes가 담당합니다.
이 둘을 한 번에 만드는 plt.subplots() 패턴 하나만 제대로 익히면 됩니다.
import matplotlib.pyplot as plt
fig, ax = plt.subplots(figsize=(8, 4))
ax.plot([1, 2, 3, 4], [10, 30, 25, 40])
ax.set_title("기본 패턴")
plt.show()검색하다 보면 plt.plot(...)처럼 plt에 직접 그리는 코드도 많이 보입니다. 짧은 실험에는 문제없지만, 그래프가 두 개 이상 되면 “지금 어디에 그리는 중인지"가 모호해집니다. 처음부터 fig, ax = plt.subplots()로 시작해 ax에 그리는 습관을 들이면 혼란이 없습니다.
DataFrame.plot으로 빠르게 #
pandas의 DataFrame.plot은 내부적으로 matplotlib을 호출하는 단축 인터페이스입니다. 탐색 단계에서는 이쪽이 훨씬 빠릅니다.
monthly = df.groupby(df["주문일"].dt.to_period("M"))["금액"].sum()
monthly.plot(figsize=(8, 4), marker="o")
plt.show()plot()이 반환하는 값은 matplotlib의 Axes입니다. 그래서 pandas로 빠르게 그린 다음, 제목이나 라벨 같은 세부 조정은 matplotlib 메서드로 이어서 할 수 있습니다. 두 도구가 별개가 아니라 같은 Figure/Axes 객체를 공유합니다.
ax = monthly.plot(figsize=(8, 4), marker="o")
ax.set_title("월별 매출")
ax.set_ylabel("금액(원)")차트 고르기: 네 가지면 충분합니다 #
차트 종류는 수십 가지지만, 분석 단계에서 실제로 쓰는 것은 네 가지입니다. “무엇을 보여주고 싶은가"에서 출발하면 고민할 일이 거의 없습니다.
| 보여주고 싶은 것 | 차트 | 호출 |
|---|---|---|
| 시간에 따른 추세 | 선 그래프 | monthly.plot() |
| 항목 간 비교 | 막대 그래프 | by_cat.plot.bar() |
| 값의 분포 | 히스토그램 | df["금액"].plot.hist(bins=30) |
| 두 변수의 관계 | 산점도 | df.plot.scatter(x="금액", y="수량") |
네 가지를 실제로 그려보면 다음과 같습니다.
by_cat = df.groupby("카테고리")["금액"].sum().sort_values(ascending=False)
monthly.plot(marker="o") # 추세: 꺾이는 시점이 보입니다
by_cat.plot.bar() # 비교: 항목 간 크기 차이
df["금액"].plot.hist(bins=30) # 분포: 쏠림과 꼬리
df.plot.scatter(x="금액", y="수량") # 관계: 상관과 이상치거꾸로 말하면, 파이 차트나 3D 차트가 필요한 순간은 분석 단계에서 거의 없습니다. 비교라면 막대가 항상 더 정확하게 읽힙니다.
꾸미기는 절제: 제목, 축 라벨, 범례까지만 #
탐색용 그림에 색상 팔레트와 스타일을 고르느라 시간을 쓰는 것은 낭비입니다. 남에게 보여줄 그림이라도 다음 세 가지면 충분합니다.
fig, ax = plt.subplots(figsize=(8, 4))
monthly.plot(ax=ax, marker="o", label="매출")
ax.set_title("월별 매출 추세")
ax.set_xlabel("월")
ax.set_ylabel("금액(원)")
ax.legend()피해야 할 것도 분명합니다. 3D 차트는 깊이 방향의 왜곡 때문에 값을 정확히 읽을 수 없습니다. 이중축(twinx)은 두 축의 스케일을 어떻게 잡느냐에 따라 같은 데이터가 전혀 다른 인상을 주기 때문에, 보는 사람을 오도하기 쉽습니다. 두 지표를 비교하고 싶다면 그래프를 위아래로 두 개 그리는 쪽이 안전합니다.
한글 폰트 깨짐: 한국 환경 단골 문제 #
위 코드를 그대로 실행하면 제목과 라벨의 한글이 전부 네모(□)로 출력되는 환경이 많습니다. matplotlib의 기본 폰트에 한글 글리프가 없기 때문입니다. OS별 기본 한글 폰트를 지정하면 해결됩니다.
import platform
import matplotlib.pyplot as plt
if platform.system() == "Windows":
plt.rcParams["font.family"] = "Malgun Gothic" # 맑은 고딕
elif platform.system() == "Darwin":
plt.rcParams["font.family"] = "AppleGothic" # macOS
else:
plt.rcParams["font.family"] = "NanumGothic" # 리눅스(나눔고딕 설치 필요)
plt.rcParams["axes.unicode_minus"] = False마지막 줄이 빠지기 쉬운 함정입니다. 한글 폰트로 바꾸면 이번에는 음수 축 라벨의 마이너스 기호가 네모로 깨집니다. matplotlib이 기본으로 쓰는 유니코드 마이너스(U+2212)가 한글 폰트에 없는 경우가 많기 때문입니다. axes.unicode_minus를 끄면 일반 하이픈으로 대체되어 정상 표시됩니다. 한글 폰트 설정과 이 옵션은 항상 한 세트로 기억하면 됩니다. 노트북이라면 이 셀을 맨 위에 두고 시작하는 것이 편합니다.
저장과 노트북 표시 #
Jupyter 노트북에서는 셀을 실행하면 그림이 바로 아래에 표시됩니다. 스크립트로 실행할 때는 plt.show()를 호출해야 창이 뜹니다. 파일로 저장할 때는 savefig를 씁니다.
fig.savefig("report.png", dpi=150, bbox_inches="tight")dpi: 해상도입니다. 화면 확인용은 100, 문서 삽입용은 150〜300이면 충분합니다.bbox_inches="tight": 라벨이 그림 밖으로 잘리는 문제를 막아줍니다.
주의할 점이 하나 있습니다. plt.show()를 호출한 뒤에 savefig를 부르면 빈 그림이 저장되는 환경이 있습니다. 저장이 목적이라면 savefig를 먼저 호출하는 순서를 지키면 됩니다.
종합: groupby 결과를 한 장의 보고 그림으로 #
#5에서 만든 두 가지 집계(월별 추세, 카테고리별 비교)를 그림 하나에 나란히 담아보겠습니다. plt.subplots(1, 2)로 Axes를 두 개 만들고, 각 집계 결과를 ax= 인자로 꽂아주는 구조입니다.
monthly = df.groupby(df["주문일"].dt.to_period("M"))["금액"].sum()
by_cat = df.groupby("카테고리")["금액"].sum().sort_values(ascending=False)
fig, axes = plt.subplots(1, 2, figsize=(12, 4))
monthly.plot(ax=axes[0], marker="o")
axes[0].set_title("월별 매출 추세")
axes[0].set_ylabel("금액(원)")
by_cat.plot.bar(ax=axes[1], rot=0)
axes[1].set_title("카테고리별 매출")
fig.tight_layout()
fig.savefig("monthly_report.png", dpi=150, bbox_inches="tight")fig.tight_layout()은 두 그래프의 라벨이 서로 겹치지 않게 간격을 자동 조정합니다. 집계는 pandas가, 배치와 저장은 matplotlib이 맡는 이 분업이 시각화 코드의 기본 골격입니다. 그래프가 네 개로 늘어도 plt.subplots(2, 2)와 axes[행, 열] 인덱싱으로 같은 패턴이 그대로 확장됩니다.
정리 #
이번 글에서 다룬 내용입니다.
- matplotlib의 구조는 Figure(도화지)와 Axes(좌표평면) 둘로 충분하고,
plt.subplots()패턴 하나로 시작합니다 DataFrame.plot은 matplotlib의 단축 인터페이스이며, 반환된 Axes를 이어서 다듬을 수 있습니다- 차트는 목적으로 고릅니다. 추세는 선, 비교는 막대, 분포는 히스토그램, 관계는 산점도입니다
- 꾸미기는 제목, 축 라벨, 범례까지만 하고, 3D와 이중축은 피합니다
- 한글 폰트 지정과
axes.unicode_minus = False는 항상 한 세트입니다 - 저장은
savefig에dpi와bbox_inches="tight"를 함께 지정합니다
다음 글(#7 polars 맛보기와 마무리)에서는 pandas보다 훨씬 빠른 차세대 라이브러리 polars를 맛보고, 일곱 편 전체의 흐름을 한 장으로 정리하며 시리즈를 마치겠습니다.