데이터 분석을 시작하면 숫자부터 잔뜩 보게 된다. 평균, 최댓값, 표준편차 같은 통계량도 중요하지만, 숫자만으로는 데이터가 어떤 방향으로 움직이는지 한눈에 파악하기 어렵다.
그래서 데이터 분석에서는 보통 수치화와 시각화를 함께 사용한다.
Python에서 가장 기본이 되는 시각화 도구는 Matplotlib이고, 이를 기반으로 더 간결한 통계 그래프 API를 제공하는 라이브러리가 Seaborn이다.
두 라이브러리는 경쟁 관계라기보다 함께 사용하는 도구에 가깝다.
| 구분 | Matplotlib | Seaborn |
|---|---|---|
| 특징 | 세부 요소를 직접 제어하기 좋음 | 적은 코드로 통계 그래프를 만들기 쉬움 |
| 장점 | 축, 선, 범례, 글자 등을 세밀하게 조절 가능 | 기본 디자인이 보기 좋고 hue 등 데이터 기반 구분이 편리 |
| 주 사용처 | 범용 그래프, 세밀한 커스터마이징 | 분포, 범주 비교, 통계 시각화 |
| 자주 쓰는 별칭 | plt | sns |
실제로는 Seaborn으로 그래프를 그리고 제목이나 크기 같은 부분을 Matplotlib으로 마무리하는 경우가 많다.
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
Jupyter Notebook이나 Google Colab에서 한글이 깨진다면 koreanize-matplotlib 같은 방법을 사용할 수 있다.
# 필요한 환경에서 한 번 설치
# !pip install koreanize-matplotlib -q
import koreanize_matplotlib
경고 메시지를 무조건 숨기는
warnings.filterwarnings("ignore")는 학습 중에는 권장하지 않는다. deprecated API 같은 중요한 경고까지 놓칠 수 있기 때문이다.
간단한 웹 서비스 지표를 만들어 보자.
metrics = pd.DataFrame({
"day": ["01", "02", "03", "04", "05", "06", "07"],
"active_users": [1200, 1310, 1280, 1420, 1510, 1470, 1600],
"orders": [92, 101, 96, 116, 130, 124, 143]
})
events = pd.DataFrame({
"latency_ms": [110, 130, 125, 180, 160, 145, 210, 190, 135, 155, 170, 240],
"status": [
"success", "success", "success", "fail",
"success", "success", "fail", "success",
"success", "success", "fail", "success"
],
"endpoint": [
"/login", "/login", "/search", "/search",
"/order", "/order", "/order", "/login",
"/search", "/search", "/login", "/order"
],
"retry_count": [0, 0, 0, 1, 0, 0, 2, 1, 0, 0, 1, 3]
})
plt.plot()선 그래프는 시간에 따라 값이 어떻게 변하는지 볼 때 특히 유용하다.
plt.plot(metrics["day"], metrics["active_users"])
plt.xlabel("날짜")
plt.ylabel("활성 사용자 수")
plt.title("일별 활성 사용자")
plt.show()
plot()에 하나의 값만 전달하면 그 값이 y축이 되고, 데이터의 인덱스가 x축으로 사용된다.
plt.plot(metrics["active_users"])
plt.show()
따라서 x축에 의미 있는 날짜나 순서가 필요하다면 직접 지정하는 편이 안전하다.
plt.plot(metrics["day"], metrics["orders"])
plt.title("일별 주문 수", fontsize=15, pad=10)
plt.xlabel("날짜")
plt.ylabel("주문 수")
plt.show()
그래프를 만들 때 제목과 축 레이블은 장식이 아니라 해석을 위한 정보다.
Matplotlib은 선 색상, 마커, 선 스타일을 조합할 수 있다.
plt.plot(
metrics["day"],
metrics["orders"],
marker="o",
linestyle="--",
linewidth=1.5
)
plt.show()
간단한 축약 표현도 가능하다.
plt.plot(metrics["day"], metrics["orders"], "go--")
plt.show()
다만 실무 코드에서는 color=, marker=, linestyle=처럼 이름을 명시하는 편이 읽기 쉽다.
plt.show()를 호출하기 전에 plot()을 여러 번 호출하면 같은 영역에 그래프가 겹쳐진다.
plt.plot(
metrics["day"],
metrics["active_users"],
marker="o",
label="활성 사용자"
)
plt.plot(
metrics["day"],
metrics["orders"],
marker="s",
label="주문"
)
plt.legend()
plt.grid(axis="y", alpha=0.3)
plt.show()
단, 두 값의 단위나 크기가 지나치게 다르면 같은 y축에서 비교하는 것이 오히려 오해를 만들 수 있다. 그런 경우에는 그래프를 분리하는 편이 낫다.
plt.plot(metrics["day"], metrics["orders"], marker="o")
plt.ylim(80, 150)
plt.show()
xlim()과 ylim()을 사용하면 화면에 보여줄 범위를 조절할 수 있다.
plt.figure(figsize=(8, 4))
plt.plot(metrics["day"], metrics["orders"], marker="o")
plt.show()
Matplotlib의 기본 크기는 설정값인 rcParams를 따른다.
print(plt.rcParams["figure.figsize"])
현재 Matplotlib 기본 설정은 일반적으로 [6.4, 4.8]인 만큼, 환경에 따라 직접 확인하는 습관이 안전하다.
특정 목표값이나 평균을 그래프에 표시할 때 axhline()과 axvline()이 유용하다.
mean_orders = metrics["orders"].mean()
plt.plot(metrics["day"], metrics["orders"], marker="o")
plt.axhline(
mean_orders,
linestyle="--",
linewidth=1,
label=f"평균 {mean_orders:.1f}"
)
plt.legend()
plt.show()
평균선처럼 의미 있는 기준이 있을 때만 추가해야 한다. 선이 너무 많아지면 오히려 데이터가 보이지 않는다.
subplot()으로 여러 그래프 나누기서로 다른 지표를 억지로 한 그래프에 겹치기보다 각각의 영역에 나누어 표현할 수 있다.
plt.figure(figsize=(8, 6))
plt.subplot(2, 1, 1)
plt.plot(metrics["day"], metrics["active_users"], marker="o")
plt.title("활성 사용자")
plt.subplot(2, 1, 2)
plt.plot(metrics["day"], metrics["orders"], marker="o")
plt.title("주문")
plt.tight_layout()
plt.show()
subplot(행, 열, 위치)라고 기억하면 된다.
새 코드를 작성할 때는 객체 지향 방식의 plt.subplots()도 많이 사용한다.
fig, axes = plt.subplots(2, 1, figsize=(8, 6))
axes[0].plot(metrics["day"], metrics["active_users"], marker="o")
axes[0].set_title("활성 사용자")
axes[1].plot(metrics["day"], metrics["orders"], marker="o")
axes[1].set_title("주문")
plt.tight_layout()
plt.show()
Seaborn의 큰 장점 중 하나는 데이터프레임의 열 이름을 직접 지정할 수 있다는 점이다.
sns.histplot(
data=events,
x="latency_ms",
bins=6,
edgecolor="black"
)
plt.show()
히스토그램은 수치형 데이터가 어떤 구간에 많이 몰려 있는지 보여준다.
다만 bins를 어떻게 설정하느냐에 따라 모양이 달라질 수 있으므로 하나의 설정만 보고 분포를 단정하면 안 된다.
hue로 그룹을 나눠 보기Seaborn에서는 hue에 범주형 변수를 지정하면 색상으로 그룹을 나눌 수 있다.
sns.histplot(
data=events,
x="latency_ms",
hue="status",
bins=6
)
plt.show()
hue는 Seaborn을 사용할 때 매우 자주 접하게 되는 매개변수다.
sns.kdeplot(
data=events,
x="latency_ms",
fill=True
)
plt.show()
KDE(Kernel Density Estimation)는 히스토그램보다 부드러운 곡선으로 분포를 추정한다.
중요한 점은 곡선의 높이 자체가 해당 값이 나올 확률을 뜻하는 것은 아니라는 것이다. 연속형 확률 밀도에서는 특정 구간의 확률을 곡선 아래 면적으로 해석한다.
그룹별 KDE도 가능하다.
sns.kdeplot(
data=events,
x="latency_ms",
hue="status",
common_norm=False
)
plt.show()
common_norm=False를 사용하면 각 그룹의 밀도 곡선을 독립적으로 정규화해 형태를 비교하기 편하다.
예전에는 sns.distplot()을 사용한 코드가 많이 보였지만, 이 함수는 Seaborn 공식 문서에서 deprecated 상태다.
현재는 다음처럼 작성하는 편이 좋다.
sns.histplot(
data=events,
x="latency_ms",
kde=True
)
plt.show()
개별 데이터 위치까지 보고 싶다면 rugplot()을 추가할 수 있다.
sns.histplot(data=events, x="latency_ms", kde=True)
sns.rugplot(data=events, x="latency_ms")
plt.show()
sns.boxplot(
data=events,
x="latency_ms"
)
plt.show()
박스 플롯은 작은 공간 안에 분포 정보를 많이 담고 있다.
Q1 - 1.5 × IQR보다 작거나 Q3 + 1.5 × IQR보다 큰 값여기서 박스 안의 선은 평균이 아니라 중앙값이라는 점이 중요하다.
sns.countplot(
data=events,
x="endpoint"
)
plt.show()
countplot()은 범주별 개수를 직접 집계해서 막대 그래프로 보여준다.
따라서 다음 Pandas 코드와 비슷한 질문에 답한다.
events["endpoint"].value_counts()
countplot()이 개수를 비교한다면 barplot()은 범주별 수치형 값의 통계량을 비교할 때 사용한다.
sns.barplot(
data=events,
x="status",
y="latency_ms",
estimator="mean",
errorbar=("ci", 95)
)
plt.show()
기본적으로 평균을 비교하며, 현재 Seaborn에서는 errorbar 매개변수로 신뢰구간 등의 표시 방법을 제어할 수 있다.
오차 막대가 필요 없다면 다음처럼 끌 수 있다.
sns.barplot(
data=events,
x="status",
y="latency_ms",
errorbar=None
)
plt.show()
먼저 집계된 표를 만든 뒤 Heatmap으로 표현할 수 있다.
summary = (
events
.groupby(["endpoint", "status"], as_index=False)["latency_ms"]
.mean()
.pivot(
index="endpoint",
columns="status",
values="latency_ms"
)
)
sns.heatmap(
summary,
annot=True,
fmt=".1f",
cmap="Blues"
)
plt.show()
Heatmap에서 진한 색이 항상 큰 값이라는 법칙은 없다.
어떤 색상표를 사용했는지에 따라 방향이 달라질 수 있으므로 반드시 옆의 colorbar와 실제 숫자를 함께 봐야 한다.
두 함수는 여러 정보를 한 화면에서 빠르게 탐색하고 싶을 때 유용하다.
sns.jointplot(
data=events,
x="latency_ms",
y="retry_count"
)
jointplot()은 두 변수의 관계와 주변 분포를 함께 볼 수 있다.
pairplot()은 여러 수치형 변수의 조합을 한꺼번에 그린다.
sns.pairplot(data=some_dataframe)
하지만 열이 많아질수록 그래프 수가 급격히 증가하고 실행 시간도 길어진다. 실무에서는 무엇을 확인하려는지 정한 뒤 필요한 변수만 선택해서 그리는 편이 효율적이다.
| 알고 싶은 것 | 대표적인 그래프 |
|---|---|
| 시간에 따른 변화 | Line Plot |
| 수치형 변수의 분포 | Histogram, KDE |
| 중앙값·사분위수·이상치 후보 | Box Plot |
| 두 수치형 변수 관계 | Scatter Plot |
| 범주별 개수 | Count Plot |
| 범주별 평균 등 통계량 | Bar Plot |
| 행렬 형태의 값 비교 | Heatmap |
| 여러 변수 관계 빠르게 탐색 | Pair Plot |
중요한 것은 예쁜 그래프를 많이 만드는 것이 아니다.
그래프를 왜 그렸는지, 그리고 그 그래프에서 어떤 사실을 읽을 수 있는지 설명할 수 있어야 한다.
plt.show()는 마지막에그래프 제목, 축 이름, 범례, 기준선 등은 일반적으로 show() 전에 설정한다.
plt.plot(metrics["day"], metrics["orders"])
plt.title("일별 주문")
plt.xlabel("날짜")
plt.ylabel("주문 수")
plt.show()
특히 선 그래프에서 인덱스의 순서가 이상하면 그래프도 예상하지 못한 형태가 될 수 있다.
색, 마커, 그리드, 텍스트를 모두 넣는다고 좋은 시각화가 되는 것은 아니다. 데이터 해석에 필요한 요소만 남기는 것이 더 중요하다.
plt.plot()은 선 그래프의 기본이며 제목, 레이블, 범례, 축 범위 등을 조합해 사용한다.subplot() 또는 subplots()로 분리할 수도 있다.hue는 범주형 기준으로 데이터를 나눠 비교할 때 유용하다.sns.distplot()은 deprecated이므로 새 코드에서는 histplot(), displot(), kdeplot() 등을 사용한다.