소개: 왜 데이터 시각화가 중요한가?
데이터 분석을 하다 보면 결국 한 가지 벽에 부딪히게 됩니다. 분석 결과를 다른 사람에게 어떻게 전달할 것인가, 하는 문제죠. 아무리 멋진 모델을 만들고 의미 있는 패턴을 찾아냈다 해도, 그걸 눈에 보이게 보여주지 못하면 절반은 허사입니다.
솔직히 말해서, 저도 처음에는 시각화를 대충 넘기곤 했습니다. 그런데 실무에서 보고서를 만들다 보니 차트 하나가 설명 열 줄을 대신하더라고요.
Python 생태계에는 데이터 시각화 라이브러리가 정말 다양합니다. 그중에서도 Matplotlib, Seaborn, Plotly 이 세 가지가 사실상 핵심이라고 할 수 있죠. 이 가이드에서는 각 라이브러리의 최신 기능(Matplotlib 3.10, Seaborn 0.13, Plotly 6.x 기준)을 실전 코드와 함께 하나하나 살펴보겠습니다.
환경 설정 및 라이브러리 설치
먼저 필요한 라이브러리들부터 설치합시다. 터미널에서 아래 명령어를 실행하면 됩니다.
pip install matplotlib seaborn plotly pandas numpy kaleido
설치가 끝났으면 임포트하고, 한글 폰트도 세팅해줘야 합니다. (이거 빼먹으면 나중에 차트에 한글이 네모로 나와서 당황하게 됩니다.)
import matplotlib.pyplot as plt
import seaborn as sns
import plotly.express as px
import plotly.graph_objects as go
import pandas as pd
import numpy as np
# 한글 폰트 설정 (맥OS/Windows/Linux 대응)
import platform
if platform.system() == "Darwin":
plt.rcParams["font.family"] = "AppleGothic"
elif platform.system() == "Windows":
plt.rcParams["font.family"] = "Malgun Gothic"
else:
plt.rcParams["font.family"] = "NanumGothic"
plt.rcParams["axes.unicode_minus"] = False # 마이너스 기호 깨짐 방지
한글 폰트 설정은 Matplotlib과 Seaborn에서 한국어 텍스트를 올바르게 표시하려면 반드시 해줘야 합니다. 반면 Plotly는 웹 기반 렌더링이라 별도 폰트 설정 없이도 한글이 잘 나옵니다.
Part 1: Matplotlib — 데이터 시각화의 기반
1.1 Matplotlib의 구조 이해하기
Matplotlib은 2003년부터 함께해 온 Python 시각화의 원조입니다. 좀 오래됐다고 느낄 수 있지만, 그만큼 안정적이고 거의 모든 것을 세밀하게 제어할 수 있는 저수준 API를 제공합니다. Seaborn 같은 라이브러리도 내부적으로 Matplotlib을 기반으로 동작하고요.
Matplotlib을 쓸 때 꼭 알아야 할 개념이 두 가지 있습니다. Figure는 전체 그림 캔버스이고, Axes는 그 안의 개별 차트 영역입니다. 이 두 가지만 확실히 이해하면 복잡한 멀티 차트도 어렵지 않습니다.
# Figure와 Axes의 기본 구조
fig, axes = plt.subplots(1, 2, figsize=(12, 5))
# 왼쪽 차트: 선 그래프
x = np.linspace(0, 10, 100)
axes[0].plot(x, np.sin(x), label="sin(x)", color="#2196F3", linewidth=2)
axes[0].plot(x, np.cos(x), label="cos(x)", color="#FF5722", linewidth=2)
axes[0].set_title("삼각함수 그래프")
axes[0].set_xlabel("x")
axes[0].set_ylabel("y")
axes[0].legend()
axes[0].grid(True, alpha=0.3)
# 오른쪽 차트: 산점도
np.random.seed(42)
data_x = np.random.randn(200)
data_y = 2 * data_x + np.random.randn(200) * 0.8
axes[1].scatter(data_x, data_y, alpha=0.6, c=data_y, cmap="viridis", s=40)
axes[1].set_title("상관관계 산점도")
axes[1].set_xlabel("변수 X")
axes[1].set_ylabel("변수 Y")
plt.tight_layout()
plt.savefig("basic_matplotlib.png", dpi=150, bbox_inches="tight")
plt.show()
1.2 Matplotlib 3.10 최신 기능 활용하기
2024년 12월에 출시된 Matplotlib 3.10에는 꽤 흥미로운 신기능들이 들어왔습니다. 개인적으로 가장 마음에 드는 업데이트 몇 가지를 소개하겠습니다.
접근성 향상 컬러 사이클: petroff10
새로 추가된 petroff10 컬러 사이클은 색각 이상(이른바 색맹) 사용자도 차트의 색상을 구분할 수 있도록 과학적으로 설계된 팔레트입니다. 크라우드소싱 기반의 미적 선호도 조사와 접근성 제약 조건을 동시에 충족하도록 만들어졌는데, 솔직히 색상 조합도 꽤 예쁩니다.
# petroff10 접근성 컬러 사이클 사용
plt.style.use("petroff10")
fig, ax = plt.subplots(figsize=(10, 6))
categories = ["제품A", "제품B", "제품C", "제품D", "제품E"]
for i, cat in enumerate(categories):
values = np.random.randn(50).cumsum() + i * 5
ax.plot(values, label=cat, linewidth=2)
ax.set_title("petroff10 컬러 사이클 - 색각 이상 친화적 디자인")
ax.set_xlabel("시간")
ax.set_ylabel("누적 값")
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
다크 모드 다이버징 컬러맵
Matplotlib 3.10에는 berlin, managua, vanimo라는 세 가지 다크 모드 전용 다이버징 컬러맵도 추가됐습니다. 중앙이 어둡고 양 끝으로 갈수록 밝아지는 구조라서, 어두운 배경의 대시보드에서 쓰면 정말 깔끔하게 보입니다.
# 다크 모드 다이버징 컬러맵 비교
fig, axes = plt.subplots(1, 3, figsize=(15, 4))
fig.patch.set_facecolor("#1a1a2e")
data = np.random.randn(20, 20)
cmaps = ["berlin", "managua", "vanimo"]
for ax, cmap in zip(axes, cmaps):
im = ax.imshow(data, cmap=cmap, aspect="auto")
ax.set_title(cmap, color="white", fontsize=14)
ax.set_facecolor("#1a1a2e")
ax.tick_params(colors="white")
plt.colorbar(im, ax=ax)
plt.tight_layout()
plt.show()
3D 축 제한 클리핑 (axlim_clip)
3D 플롯을 만들다 보면 축 범위 밖으로 삐져나가는 데이터 때문에 차트가 지저분해질 때가 있잖아요? axlim_clip 파라미터가 이 문제를 깔끔하게 해결해 줍니다. 패닝이나 줌 중에도 실시간으로 클리핑이 적용됩니다.
# 3D 산점도에서 axlim_clip 활용
fig = plt.figure(figsize=(10, 7))
ax = fig.add_subplot(111, projection="3d")
n = 500
x = np.random.randn(n)
y = np.random.randn(n)
z = np.sin(x * y)
ax.scatter(x, y, z, c=z, cmap="viridis", alpha=0.6, axlim_clip=True)
ax.set_xlim(-2, 2)
ax.set_ylim(-2, 2)
ax.set_zlim(-0.8, 0.8)
ax.set_title("3D 산점도 - axlim_clip으로 데이터 클리핑")
plt.show()
1.3 고급 서브플롯 레이아웃
실무에서는 차트 하나만 딱 그리는 일은 별로 없습니다. 보통 여러 차트를 한 화면에 배치해서 비교해야 하죠. 이럴 때 GridSpec을 쓰면 자유도 높은 레이아웃을 만들 수 있습니다.
from matplotlib.gridspec import GridSpec
# 샘플 데이터 생성
np.random.seed(42)
df = pd.DataFrame({
"매출": np.random.randint(100, 1000, 12),
"비용": np.random.randint(50, 500, 12),
"월": [f"{i}월" for i in range(1, 13)]
})
df["이익"] = df["매출"] - df["비용"]
fig = plt.figure(figsize=(14, 8))
gs = GridSpec(2, 3, figure=fig, hspace=0.35, wspace=0.3)
# 상단 전체: 매출 vs 비용 추이
ax1 = fig.add_subplot(gs[0, :])
ax1.plot(df["월"], df["매출"], "o-", label="매출", linewidth=2)
ax1.plot(df["월"], df["비용"], "s--", label="비용", linewidth=2)
ax1.fill_between(range(12), df["매출"], df["비용"], alpha=0.1)
ax1.set_title("월별 매출 vs 비용 추이", fontsize=14)
ax1.legend()
ax1.grid(True, alpha=0.3)
# 하단 왼쪽: 이익 막대 그래프
ax2 = fig.add_subplot(gs[1, 0])
colors = ["#4CAF50" if v > 0 else "#F44336" for v in df["이익"]]
ax2.bar(range(12), df["이익"], color=colors, alpha=0.8)
ax2.set_title("월별 이익")
ax2.set_xticks(range(12))
ax2.set_xticklabels(df["월"], rotation=45)
# 하단 중앙: 파이 차트
ax3 = fig.add_subplot(gs[1, 1])
top5 = df.nlargest(5, "매출")
ax3.pie(top5["매출"], labels=top5["월"], autopct="%1.1f%%")
ax3.set_title("매출 상위 5개월")
# 하단 오른쪽: 박스 플롯
ax4 = fig.add_subplot(gs[1, 2])
ax4.boxplot([df["매출"], df["비용"], df["이익"]],
labels=["매출", "비용", "이익"])
ax4.set_title("분포 비교")
plt.suptitle("월별 비즈니스 대시보드", fontsize=16, fontweight="bold", y=1.02)
plt.savefig("dashboard_matplotlib.png", dpi=150, bbox_inches="tight")
plt.show()
Part 2: Seaborn — 통계적 시각화의 전문가
2.1 Seaborn의 강점: 통계적 시각화
자, 이제 Seaborn 차례입니다. Seaborn은 Matplotlib 위에 얹어진 고수준 시각화 라이브러리인데, 한마디로 "통계 차트를 쉽고 예쁘게 그리는 도구"라고 보면 됩니다. pandas DataFrame을 바로 넣을 수 있고, 기본 스타일도 깔끔해서 EDA(탐색적 데이터 분석) 할 때 정말 편합니다.
# Seaborn으로 탐색적 데이터 분석(EDA)
tips = sns.load_dataset("tips")
fig, axes = plt.subplots(2, 2, figsize=(14, 10))
# 1) 바이올린 플롯: 분포 비교
sns.violinplot(data=tips, x="day", y="total_bill", hue="sex",
split=True, inner="quart", ax=axes[0, 0])
axes[0, 0].set_title("요일별/성별 총 결제 금액 분포")
# 2) 회귀 플롯: 상관관계 분석
sns.regplot(data=tips, x="total_bill", y="tip",
scatter_kws={"alpha": 0.5}, line_kws={"color": "red"},
ax=axes[0, 1])
axes[0, 1].set_title("총 결제 금액 vs 팁 (회귀선 포함)")
# 3) 히트맵: 상관 행렬
numeric_cols = tips.select_dtypes(include=[np.number])
corr = numeric_cols.corr()
sns.heatmap(corr, annot=True, cmap="coolwarm", center=0,
square=True, ax=axes[1, 0], fmt=".2f")
axes[1, 0].set_title("상관 행렬 히트맵")
# 4) 스웜 플롯: 개별 데이터 포인트
sns.swarmplot(data=tips, x="day", y="tip", hue="time",
dodge=True, size=4, ax=axes[1, 1])
axes[1, 1].set_title("요일별/시간대별 팁 분포 (스웜 플롯)")
plt.tight_layout()
plt.show()
2.2 Seaborn 0.13의 새로운 기능
Seaborn 0.13은 꽤 큰 업데이트였습니다. 카테고리 플롯 함수가 완전히 재작성됐고, Polars 같은 대체 DataFrame 라이브러리도 지원하기 시작했죠. Objects 인터페이스도 한층 강화됐습니다.
native_scale 파라미터
예전에는 카테고리 함수에 숫자 데이터를 넣으면 무조건 카테고리로 취급해서 균등 간격으로 배치했는데, 이제 native_scale=True를 설정하면 원래 수치 스케일을 유지해 줍니다. 작은 변화처럼 보이지만, 실제로 써보면 차이가 큽니다.
# native_scale 활용 예시
np.random.seed(42)
data = pd.DataFrame({
"온도": np.random.choice([20, 25, 30, 35, 40], 200),
"판매량": np.random.randint(50, 300, 200) + np.random.choice([20, 25, 30, 35, 40], 200) * 3
})
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
# native_scale=False (기본값): 균등 간격
sns.boxplot(data=data, x="온도", y="판매량", ax=axes[0])
axes[0].set_title("native_scale=False (기본값)")
# native_scale=True: 실제 수치 간격 반영
sns.boxplot(data=data, x="온도", y="판매량", native_scale=True, ax=axes[1])
axes[1].set_title("native_scale=True (실제 스케일)")
plt.tight_layout()
plt.show()
Seaborn Objects 인터페이스
Seaborn 0.12에서 처음 등장하고 0.13에서 더 다듬어진 Objects 인터페이스는, ggplot2에서 영감을 받은 선언적 API입니다. 레이어를 하나씩 쌓아 올리듯 차트를 구성할 수 있어서, 복잡한 시각화도 코드가 읽기 쉬워집니다.
import seaborn.objects as so
# Objects 인터페이스로 다층 시각화 구성
tips = sns.load_dataset("tips")
(
so.Plot(tips, x="total_bill", y="tip", color="smoker")
.add(so.Dot(alpha=0.5)) # 산점도 레이어
.add(so.Line(), so.PolyFit(3)) # 다항 회귀선 레이어
.facet("time") # 시간대별 패싯
.label(
x="총 결제 금액 ($)",
y="팁 ($)",
color="흡연 여부",
title="결제 금액과 팁의 관계 분석"
)
.layout(size=(12, 5))
.show()
)
2.3 FacetGrid를 활용한 다차원 분석
FacetGrid는 데이터의 여러 하위 그룹에 동일한 차트를 반복 적용해서 패턴을 한눈에 비교할 수 있게 해주는 도구입니다. 변수가 많은 데이터를 탐색할 때 특히 유용하죠.
# FacetGrid로 다차원 탐색
tips = sns.load_dataset("tips")
g = sns.FacetGrid(tips, col="time", row="smoker",
hue="sex", margin_titles=True,
height=4, aspect=1.2)
g.map_dataframe(sns.scatterplot, x="total_bill", y="tip", alpha=0.7)
g.add_legend()
g.set_axis_labels("총 결제 금액 ($)", "팁 ($)")
g.figure.suptitle("시간대/흡연 여부별 결제 금액과 팁", y=1.02, fontsize=14)
plt.show()
2.4 pairplot으로 변수 간 관계 한눈에 파악하기
고차원 데이터셋에서 변수들 사이의 관계를 빠르게 훑어보고 싶다면 pairplot만 한 게 없습니다. 코드 한 줄이면 모든 변수 조합의 산점도와 분포를 동시에 볼 수 있거든요.
# pairplot으로 다변수 관계 시각화
iris = sns.load_dataset("iris")
g = sns.pairplot(iris, hue="species", diag_kind="kde",
plot_kws={"alpha": 0.6, "s": 40},
diag_kws={"fill": True, "alpha": 0.5})
g.figure.suptitle("붓꽃 데이터 - 종별 변수 관계", y=1.02, fontsize=14)
plt.show()
Part 3: Plotly — 인터랙티브 시각화의 정석
3.1 Plotly Express: 빠른 인터랙티브 차트
Plotly는 웹 기반 인터랙티브 시각화 라이브러리입니다. 줌, 팬, 호버 툴팁, 애니메이션... 정적 차트에서는 불가능한 것들을 자연스럽게 제공하죠. 특히 Plotly Express라는 고수준 API를 사용하면, 놀라울 정도로 적은 코드로 인터랙티브 차트를 만들 수 있습니다.
# Plotly Express로 인터랙티브 산점도 매트릭스
iris = px.data.iris()
fig = px.scatter_matrix(
iris,
dimensions=["sepal_width", "sepal_length", "petal_width", "petal_length"],
color="species",
title="붓꽃 데이터 인터랙티브 산점도 매트릭스",
opacity=0.7
)
fig.update_traces(diagonal_visible=False)
fig.update_layout(height=700, width=900)
fig.show()
3.2 다양한 차트 유형 실습
인터랙티브 히스토그램과 박스 플롯
# 서브플롯으로 여러 인터랙티브 차트 결합
from plotly.subplots import make_subplots
tips = px.data.tips()
fig = make_subplots(
rows=1, cols=2,
subplot_titles=("결제 금액 분포", "요일별 결제 금액 비교")
)
# 히스토그램
for sex in tips["sex"].unique():
subset = tips[tips["sex"] == sex]
fig.add_trace(
go.Histogram(x=subset["total_bill"], name=sex,
opacity=0.7, nbinsx=20),
row=1, col=1
)
# 박스 플롯
for sex in tips["sex"].unique():
subset = tips[tips["sex"] == sex]
fig.add_trace(
go.Box(x=subset["day"], y=subset["total_bill"],
name=sex),
row=1, col=2
)
fig.update_layout(
height=450, width=1000,
title_text="팁 데이터 분석 대시보드",
barmode="overlay"
)
fig.show()
애니메이션 버블 차트
Plotly의 가장 임팩트 있는 기능을 하나만 꼽으라면 단연 애니메이션입니다. 시간에 따른 데이터 변화를 재생 버튼 하나로 동적으로 보여줄 수 있는데, 프레젠테이션에서 쓰면 반응이 확실히 다릅니다.
# Gapminder 데이터로 애니메이션 버블 차트 생성
gapminder = px.data.gapminder()
fig = px.scatter(
gapminder,
x="gdpPercap",
y="lifeExp",
animation_frame="year",
animation_group="country",
size="pop",
color="continent",
hover_name="country",
log_x=True,
size_max=55,
range_x=[100, 100000],
range_y=[25, 90],
title="국가별 GDP와 기대수명 변화 (1952-2007)"
)
fig.update_layout(
xaxis_title="1인당 GDP (로그 스케일)",
yaxis_title="기대수명 (년)",
height=600
)
fig.show()
3.3 Plotly Graph Objects: 세밀한 제어
Plotly Express로 안 되는, 좀 더 정교한 커스터마이징이 필요할 때는 plotly.graph_objects를 직접 사용합니다. 보조 y축이나 복합 차트를 만들 때 특히 유용합니다.
# Graph Objects로 복합 차트 생성
np.random.seed(42)
months = ["1월", "2월", "3월", "4월", "5월", "6월",
"7월", "8월", "9월", "10월", "11월", "12월"]
sales = [120, 135, 148, 162, 175, 190, 205, 198, 210, 225, 240, 260]
costs = [80, 85, 90, 95, 100, 110, 120, 115, 125, 130, 140, 150]
margins = [(s - c) / s * 100 for s, c in zip(sales, costs)]
fig = go.Figure()
# 막대 차트: 매출
fig.add_trace(go.Bar(
x=months, y=sales, name="매출",
marker_color="#2196F3", opacity=0.8
))
# 막대 차트: 비용
fig.add_trace(go.Bar(
x=months, y=costs, name="비용",
marker_color="#FF9800", opacity=0.8
))
# 선 차트: 이익률 (보조 y축)
fig.add_trace(go.Scatter(
x=months, y=margins, name="이익률 (%)",
yaxis="y2", mode="lines+markers",
line=dict(color="#4CAF50", width=3),
marker=dict(size=8)
))
fig.update_layout(
title="월별 매출/비용 분석 대시보드",
xaxis_title="월",
yaxis_title="금액 (만 원)",
yaxis2=dict(
title="이익률 (%)",
overlaying="y",
side="right",
range=[0, 100]
),
barmode="group",
height=500,
legend=dict(x=0.01, y=0.99),
template="plotly_white"
)
fig.show()
3.4 지리 데이터 시각화
Plotly는 지도 기반 시각화도 상당히 잘 지원합니다. 코로플레스 맵이나 산점도 맵 같은 지리 시각화를 몇 줄의 코드로 만들 수 있는데, 다른 라이브러리에서는 이게 꽤 번거롭거든요.
# 코로플레스 맵: 국가별 기대수명
gapminder = px.data.gapminder()
gapminder_2007 = gapminder[gapminder["year"] == 2007]
fig = px.choropleth(
gapminder_2007,
locations="iso_alpha",
color="lifeExp",
hover_name="country",
color_continuous_scale="Viridis",
title="2007년 국가별 기대수명",
labels={"lifeExp": "기대수명 (년)"}
)
fig.update_layout(
height=500,
geo=dict(showframe=False, showcoastlines=True)
)
fig.show()
Part 4: 라이브러리 비교 및 선택 가이드
4.1 성능 비교
세 라이브러리는 각각 다른 렌더링 방식을 사용하기 때문에 데이터 크기에 따라 성능 차이가 납니다. 궁금해서 직접 벤치마크를 돌려봤습니다.
import time
# 대용량 데이터 렌더링 성능 비교
sizes = [1000, 10000, 100000]
for n in sizes:
x = np.random.randn(n)
y = np.random.randn(n)
# Matplotlib 성능 측정
start = time.time()
fig, ax = plt.subplots()
ax.scatter(x, y, alpha=0.1, s=1)
plt.savefig(f"mpl_{n}.png", dpi=72)
plt.close()
mpl_time = time.time() - start
# Plotly 성능 측정
start = time.time()
fig = px.scatter(x=x, y=y, opacity=0.1)
fig.write_image(f"plotly_{n}.png")
plotly_time = time.time() - start
print(f"n={n:>7,}: Matplotlib={mpl_time:.3f}s, Plotly={plotly_time:.3f}s")
결과를 간단히 정리하면 이렇습니다. Matplotlib은 대용량 정적 차트에서 압도적으로 빠릅니다. Plotly는 인터랙티브 기능이 필요한 중소규모 데이터에서 최적이고요. Seaborn은 Matplotlib 기반이라 약간의 오버헤드가 있지만, 통계 차트를 한두 줄로 뽑아낼 수 있는 편의성이 그 차이를 충분히 만회합니다.
4.2 사용 사례별 권장 라이브러리
- 학술 논문 및 보고서: Matplotlib — 출판 품질의 정적 차트를 만들 수 있고, 세밀한 제어가 가능합니다
- 탐색적 데이터 분석(EDA): Seaborn — 통계 차트를 간결한 코드로 빠르게 뽑아낼 수 있습니다
- 웹 대시보드 및 프레젠테이션: Plotly — 줌, 필터링, 애니메이션 등 인터랙티브 기능이 기본 내장
- Jupyter 노트북: 셋 다 잘 작동하지만, Plotly의 인터랙티브 기능이 노트북 환경에서 특히 빛납니다
- 대용량 데이터(100만+ 행): Matplotlib이나 Plotly의 WebGL 렌더러를 사용하세요
- 지리 데이터: Plotly가 코로플레스와 Mapbox 지원으로 가장 편리합니다 (Folium도 대안)
4.3 기능 비교표
세 라이브러리의 핵심 특성을 한눈에 비교할 수 있도록 표로 정리했습니다.
| 기능 | Matplotlib | Seaborn | Plotly |
|---|---|---|---|
| 학습 곡선 | 가파름 | 보통 | 보통 |
| 인터랙티브 | 제한적 | 제한적 | 기본 지원 |
| 정적 차트 품질 | 최고 | 우수 | 우수 |
| 통계 차트 | 수동 구현 | 기본 내장 | 일부 지원 |
| 3D 시각화 | 기본 지원 | 미지원 | 우수 |
| 애니메이션 | 가능 | 미지원 | 기본 내장 |
| 웹 내보내기 | 이미지만 | 이미지만 | HTML 인터랙티브 |
| 커스터마이징 | 무제한 | Matplotlib 수준 | 높음 |
| 대용량 데이터 | 우수 | 보통 | WebGL로 우수 |
Part 5: 실전 프로젝트 — 종합 데이터 분석 리포트
5.1 데이터 준비
이론은 충분히 다뤘으니, 이제 세 라이브러리를 한꺼번에 활용하는 실전 프로젝트를 해보겠습니다. 가상의 전자상거래 데이터를 만들어서 종합 분석 리포트를 구성해 볼 건데요, 실무에서 가장 흔히 마주치는 형태의 분석이라 참고하시면 좋을 겁니다.
# 가상 전자상거래 데이터 생성
np.random.seed(42)
n_orders = 5000
dates = pd.date_range("2024-01-01", periods=365, freq="D")
order_dates = np.random.choice(dates, n_orders)
categories_list = ["전자제품", "의류", "식품", "가구", "도서"]
regions = ["서울", "경기", "부산", "대구", "인천"]
ecommerce_df = pd.DataFrame({
"주문일자": order_dates,
"카테고리": np.random.choice(categories_list, n_orders),
"지역": np.random.choice(regions, n_orders, p=[0.35, 0.25, 0.15, 0.13, 0.12]),
"주문금액": np.random.lognormal(mean=10, sigma=1, size=n_orders).astype(int),
"할인율": np.random.choice([0, 5, 10, 15, 20, 30], n_orders),
"고객연령": np.random.normal(35, 10, n_orders).astype(int).clip(18, 70)
})
ecommerce_df["실결제금액"] = (
ecommerce_df["주문금액"] * (1 - ecommerce_df["할인율"] / 100)
).astype(int)
ecommerce_df["월"] = ecommerce_df["주문일자"].dt.month
print(ecommerce_df.head())
print(f"\n데이터 크기: {ecommerce_df.shape}")
print(f"총 매출: {ecommerce_df['실결제금액'].sum():,}원")
5.2 Matplotlib으로 정적 분석 차트
# 월별 매출 추이와 카테고리별 구성 비율
monthly_sales = ecommerce_df.groupby("월")["실결제금액"].sum()
cat_sales = ecommerce_df.groupby("카테고리")["실결제금액"].sum()
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
# 월별 매출 추이
axes[0].fill_between(monthly_sales.index, monthly_sales.values,
alpha=0.3, color="#2196F3")
axes[0].plot(monthly_sales.index, monthly_sales.values,
"o-", color="#2196F3", linewidth=2, markersize=8)
axes[0].set_title("월별 총 매출 추이", fontsize=13)
axes[0].set_xlabel("월")
axes[0].set_ylabel("매출 (원)")
axes[0].grid(True, alpha=0.3)
axes[0].yaxis.set_major_formatter(
plt.FuncFormatter(lambda x, p: f"{x/1e6:.0f}M"))
# 카테고리별 매출 비율
explode = [0.05] * len(cat_sales)
axes[1].pie(cat_sales, labels=cat_sales.index, autopct="%1.1f%%",
explode=explode, startangle=90)
axes[1].set_title("카테고리별 매출 비율", fontsize=13)
plt.tight_layout()
plt.savefig("ecommerce_static.png", dpi=150, bbox_inches="tight")
plt.show()
5.3 Seaborn으로 통계적 패턴 발견
# 카테고리별/지역별 결제 금액 통계 분석
fig, axes = plt.subplots(1, 2, figsize=(14, 6))
# 카테고리별 결제 금액 분포
sns.boxenplot(data=ecommerce_df, x="카테고리", y="실결제금액",
ax=axes[0], palette="Set2")
axes[0].set_title("카테고리별 결제 금액 분포 (Boxen Plot)")
axes[0].set_ylabel("결제 금액 (원)")
axes[0].yaxis.set_major_formatter(
plt.FuncFormatter(lambda x, p: f"{x/1e3:.0f}K"))
# 연령 vs 결제 금액 (카테고리별)
sns.kdeplot(data=ecommerce_df, x="고객연령", y="실결제금액",
hue="카테고리", fill=True, alpha=0.3,
ax=axes[1], common_norm=False)
axes[1].set_title("연령-결제 금액 밀도 (카테고리별)")
axes[1].set_xlim(15, 65)
axes[1].set_ylim(0, 100000)
axes[1].set_ylabel("결제 금액 (원)")
plt.tight_layout()
plt.show()
5.4 Plotly로 인터랙티브 대시보드 구성
# 인터랙티브 선버스트 차트: 지역 > 카테고리 계층 구조
region_cat = (ecommerce_df.groupby(["지역", "카테고리"])["실결제금액"]
.sum().reset_index())
fig = px.sunburst(
region_cat,
path=["지역", "카테고리"],
values="실결제금액",
color="실결제금액",
color_continuous_scale="Blues",
title="지역별/카테고리별 매출 구조 (선버스트 차트)"
)
fig.update_layout(height=600, width=700)
fig.show()
# 인터랙티브 트리맵: 할인율별 매출 구성
discount_cat = (ecommerce_df.groupby(["할인율", "카테고리"])
.agg(매출합계=("실결제금액", "sum"),
주문수=("실결제금액", "count"))
.reset_index())
fig = px.treemap(
discount_cat,
path=["할인율", "카테고리"],
values="매출합계",
color="주문수",
color_continuous_scale="Reds",
title="할인율/카테고리별 매출 트리맵"
)
fig.update_layout(height=550)
fig.show()
Part 6: 시각화 모범 사례와 팁
6.1 차트 디자인 원칙
마지막으로, 효과적인 데이터 시각화를 위해 꼭 기억해두면 좋은 원칙들을 정리해 봤습니다. 사실 기술적인 것보다 이런 디자인 감각이 차트의 품질을 좌우하는 경우가 많습니다.
- 데이터-잉크 비율 최대화: Edward Tufte의 유명한 원칙인데요, 핵심은 간단합니다. 불필요한 장식은 빼고 데이터 자체에 집중하세요. 3D 효과, 과도한 그리드, 무지개 색상 같은 건 대부분 방해만 됩니다.
- 적절한 차트 유형 선택: 비교에는 막대/선 차트, 분포에는 히스토그램/바이올린 플롯, 관계에는 산점도, 구성 비율에는 파이/트리맵. 이건 거의 공식처럼 외워두시면 편합니다.
- 색상을 전략적으로 사용: 범주형 데이터엔 구분이 뚜렷한 팔레트를, 연속형 데이터엔 순차적 컬러맵을 쓰세요. 그리고 색각 이상 사용자를 위해
petroff10이나viridis컬러맵을 쓰는 걸 습관으로 만들면 좋습니다. - 명확한 레이블과 제목: 축 레이블, 범례, 제목은 반드시 포함하되 간결하게. 그리고 단위 표시, 이건 절대 빼먹지 마세요.
6.2 재사용 가능한 스타일 설정
프로젝트마다 매번 스타일을 처음부터 설정하면 시간이 아깝습니다. 아래처럼 표준 스타일 함수를 하나 만들어두면 일관성도 유지되고 훨씬 효율적입니다.
# 프로젝트 전체에서 일관된 스타일 적용
def setup_plot_style():
# 프로젝트 표준 시각화 스타일 설정
plt.rcParams.update({
"figure.figsize": (10, 6),
"figure.dpi": 100,
"axes.spines.top": False,
"axes.spines.right": False,
"axes.grid": True,
"grid.alpha": 0.3,
"font.size": 11,
"axes.titlesize": 14,
"axes.labelsize": 12,
})
sns.set_palette("husl")
# Plotly 표준 템플릿
plotly_template = dict(
layout=dict(
font=dict(family="Noto Sans KR, sans-serif", size=13),
title=dict(font=dict(size=18)),
plot_bgcolor="white",
xaxis=dict(showgrid=True, gridcolor="#E0E0E0"),
yaxis=dict(showgrid=True, gridcolor="#E0E0E0"),
)
)
setup_plot_style()
6.3 차트 저장 및 내보내기
차트를 만들었으면 적절한 형식으로 저장하는 것도 중요합니다. 용도에 따라 포맷을 달리해야 하는데, 간단히 정리하면 웹용은 PNG, 논문용은 SVG나 PDF, 인터랙티브 공유는 HTML입니다.
# Matplotlib/Seaborn 차트 저장
fig, ax = plt.subplots(figsize=(10, 6))
ax.plot([1, 2, 3, 4], [10, 20, 25, 30])
# PNG (웹/프레젠테이션용)
fig.savefig("chart.png", dpi=300, bbox_inches="tight",
facecolor="white", transparent=False)
# SVG (벡터, 논문/보고서용)
fig.savefig("chart.svg", format="svg", bbox_inches="tight")
# PDF (인쇄용)
fig.savefig("chart.pdf", format="pdf", bbox_inches="tight")
plt.close()
# Plotly 차트 저장
fig_plotly = px.scatter(x=[1, 2, 3], y=[4, 5, 6])
# 인터랙티브 HTML
fig_plotly.write_html("chart_interactive.html", include_plotlyjs="cdn")
# 정적 이미지 (kaleido 패키지 필요)
fig_plotly.write_image("chart_plotly.png", width=1200, height=600, scale=2)
결론: 어떤 라이브러리를 선택할 것인가?
Python 데이터 시각화에서 "이게 최고다"라고 딱 잘라 말할 수 있는 라이브러리는 없습니다. 각각 잘하는 영역이 다르고, 실무에서는 상황에 맞게 세 가지를 섞어 쓰는 게 가장 현실적인 접근법입니다.
Matplotlib은 완전한 제어가 필요한 출판 품질의 정적 차트에 최적입니다. 3.10에서 추가된 접근성 컬러 사이클과 다크 모드 컬러맵은 "오래됐지만 계속 진화하고 있다"는 걸 보여주죠.
Seaborn은 탐색적 데이터 분석에서 진가를 발휘합니다. 0.13의 Objects 인터페이스와 native_scale 파라미터 덕분에 더 유연하고 직관적인 시각화가 가능해졌고요.
Plotly는 인터랙티브 시각화의 사실상 표준입니다. 줌, 호버, 애니메이션 기능은 데이터 스토리텔링에 완전히 새로운 차원을 열어줍니다.
제가 추천하는 워크플로우는 이렇습니다: Seaborn으로 EDA를 빠르게 돌리고, Matplotlib으로 발표/보고서용 차트를 정교하게 다듬고, Plotly로 인터랙티브 대시보드를 만드는 것. 이 세 가지를 자유자재로 쓸 수 있다면, 어떤 데이터가 와도 효과적으로 시각화하고 전달할 수 있을 겁니다.