“matplotlib으로 만든 차트는 정적이다. Plotly로 만든 차트는 살아있다.”
데이터 시각화는 숫자를 이야기로 바꾸는 작업이다. 그런데 차트가 정적인 이미지에 머문다면 청중은 데이터를 스스로 탐색할 수 없다. Plotly는 이 문제를 해결하는 Python 인터랙티브 시각화 라이브러리다. 마우스를 올리면 값이 나타나고, 드래그로 확대하고, 클릭으로 데이터를 필터링한다. 이 글에서는 Plotly의 두 API인 plotly.express와 plotly.graph_objects를 중심으로 실전 활용법을 깊게 파고든다.
Plotly란?
Plotly는 2012년 캐나다 몬트리올에서 창립된 데이터 시각화 회사이자, 같은 이름의 오픈소스 라이브러리다. Python 외에도 R, JavaScript(Plotly.js)를 지원한다.
핵심 특징을 정리하면 다음과 같다.
- 인터랙티브 기본 제공 — zoom, pan, hover tooltip, legend 클릭 필터링이 추가 코드 없이 동작한다.
- 두 가지 API 레벨 — 빠른 작업을 위한
plotly.express와 세밀한 제어를 위한plotly.graph_objects가 공존한다. - 광범위한 차트 타입 — 라인, 바, 산점도, 히트맵, 박스플롯, 바이올린, 3D 서피스, 지도, 트리맵, 선버스트 등 50가지 이상 지원한다.
- 다양한 환경 지원 — Jupyter Notebook, Streamlit, Dash, HTML 파일, VS Code 모두에서 동작한다.
설치
pip install plotly
# 데이터 처리를 위해 pandas도 함께 설치
pip install pandas
plotly.express: 빠르고 간결하게
plotly.express(줄여서 px)는 한두 줄로 차트를 완성하는 고수준 API다. 내부적으로 graph_objects를 사용하지만 대부분의 옵션을 자동으로 처리해준다.
기본 차트들
import plotly.express as px
import pandas as pd
# 라인 차트
df = px.data.gapminder().query("country == 'South Korea'")
fig = px.line(df, x="year", y="gdpPercap", title="한국 1인당 GDP 추이")
fig.show()
# 바 차트
df_bar = px.data.gapminder().query("year == 2007 and continent == 'Asia'")
fig = px.bar(df_bar, x="country", y="gdpPercap",
color="gdpPercap", title="2007년 아시아 국가별 GDP")
fig.show()
# 산점도
df_iris = px.data.iris()
fig = px.scatter(df_iris, x="sepal_width", y="sepal_length",
color="species", size="petal_length",
hover_data=["petal_width"],
title="붓꽃 데이터 산점도")
fig.show()
# 히스토그램
fig = px.histogram(df_iris, x="sepal_length", color="species",
nbins=30, barmode="overlay", opacity=0.7)
fig.show()
# 박스 플롯
fig = px.box(df_iris, x="species", y="sepal_length",
color="species", points="all")
fig.show()
지도 시각화
df_geo = px.data.gapminder().query("year == 2007")
# 코로플레스 지도 (국가별 색상)
fig = px.choropleth(df_geo, locations="iso_alpha",
color="lifeExp",
hover_name="country",
color_continuous_scale="Viridis",
title="2007년 국가별 기대수명")
fig.show()
# 버블 지도
fig = px.scatter_geo(df_geo, locations="iso_alpha",
color="continent", size="pop",
hover_name="country")
fig.show()
트리맵과 선버스트
fig = px.treemap(df_geo, path=["continent", "country"],
values="pop", color="lifeExp",
color_continuous_scale="RdYlGn",
title="대륙→국가 인구 트리맵")
fig.show()
fig = px.sunburst(df_geo, path=["continent", "country"],
values="pop", color="lifeExp")
fig.show()
plotly.graph_objects: 세밀한 제어
graph_objects(줄여서 go)는 차트의 모든 요소를 직접 제어할 수 있는 저수준 API다. 여러 트레이스를 하나의 그림에 조합하거나 커스텀 애니메이션, 복잡한 레이아웃이 필요할 때 사용한다.
import plotly.graph_objects as go
import numpy as np
x = np.linspace(0, 4 * np.pi, 200)
fig = go.Figure()
fig.add_trace(go.Scatter(
x=x, y=np.sin(x),
mode="lines",
name="sin(x)",
line=dict(color="royalblue", width=2)
))
fig.add_trace(go.Scatter(
x=x, y=np.cos(x),
mode="lines+markers",
name="cos(x)",
line=dict(color="firebrick", width=2, dash="dash"),
marker=dict(size=4)
))
fig.update_layout(
title="삼각함수 비교",
xaxis_title="x",
yaxis_title="y",
hovermode="x unified", # 같은 x 값의 모든 트레이스를 함께 표시
template="plotly_white"
)
fig.show()
서브플롯 (여러 차트 한 화면에)
from plotly.subplots import make_subplots
import plotly.graph_objects as go
fig = make_subplots(
rows=2, cols=2,
subplot_titles=("라인 차트", "바 차트", "산점도", "파이 차트"),
specs=[[{"type": "scatter"}, {"type": "bar"}],
[{"type": "scatter"}, {"type": "pie"}]]
)
categories = ["A", "B", "C", "D"]
values = [4, 7, 2, 9]
fig.add_trace(go.Scatter(y=[1, 3, 2, 5], mode="lines+markers"), row=1, col=1)
fig.add_trace(go.Bar(x=categories, y=values, marker_color="steelblue"), row=1, col=2)
fig.add_trace(go.Scatter(x=[1,2,3,4], y=[4,7,2,9], mode="markers",
marker=dict(size=12, color="orange")), row=2, col=1)
fig.add_trace(go.Pie(labels=categories, values=values, hole=0.3), row=2, col=2)
fig.update_layout(height=700, title_text="2×2 서브플롯 예시", showlegend=False)
fig.show()
레이아웃 커스터마이징
fig.update_layout(
# 제목
title=dict(text="차트 제목", font=dict(size=22), x=0.5),
# 축
xaxis=dict(title="X축", showgrid=True, gridcolor="lightgray"),
yaxis=dict(title="Y축", range=[0, 100]),
# 범례
legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1),
# 배경 & 마진
plot_bgcolor="white",
paper_bgcolor="white",
margin=dict(l=60, r=40, t=80, b=60),
# 글꼴
font=dict(family="Noto Sans KR, sans-serif", size=13),
# 템플릿 (plotly, plotly_white, plotly_dark, ggplot2, seaborn 등)
template="plotly_white"
)
애니메이션
Plotly의 숨은 강점 중 하나가 애니메이션이다.
df = px.data.gapminder()
fig = px.scatter(
df,
x="gdpPercap", y="lifeExp",
size="pop", color="continent",
hover_name="country",
log_x=True,
animation_frame="year", # 연도별 프레임 생성
animation_group="country", # 동일 국가를 연결
range_x=[100, 100000],
range_y=[25, 90],
title="연도별 GDP vs 기대수명 애니메이션"
)
fig.show()
express vs graph_objects 언제 쓸까?
| 상황 | 추천 API |
|---|---|
| 빠른 EDA, 간단한 시각화 | plotly.express |
| 여러 트레이스 조합 | graph_objects |
| 서브플롯 | graph_objects + make_subplots |
| 커스텀 애니메이션 | graph_objects |
| express 차트를 세밀하게 수정 | express로 생성 후 update_traces(), update_layout() 활용 |
사실 두 API는 함께 쓸 수 있다. px로 만든 Figure도 결국 go.Figure 객체이므로 .update_layout(), .update_traces()로 이후 수정이 가능하다.
Streamlit과 연동
Streamlit과의 통합은 단 한 줄이다.
import streamlit as st
import plotly.express as px
df = px.data.tips()
fig = px.scatter(df, x="total_bill", y="tip",
color="day", size="size",
title="요일별 팁 분석")
st.plotly_chart(fig, use_container_width=True)
차트를 파일로 저장
# HTML (인터랙티브 유지)
fig.write_html("chart.html")
# 정적 이미지 (kaleido 패키지 필요: pip install kaleido)
fig.write_image("chart.png", width=1200, height=800, scale=2)
fig.write_image("chart.svg")
fig.write_image("chart.pdf")
자주 쓰는 팁 모음
호버 텍스트 커스터마이징
fig = px.scatter(df, x="x", y="y",
hover_data={"extra_col": True, "x": False},
custom_data=["id"])
fig.update_traces(
hovertemplate="<b>%{customdata[0]}</b><br>값: %{y:.2f}<extra></extra>"
)
색상 팔레트
# 연속형
px.scatter(df, color="value", color_continuous_scale="Viridis")
# 범주형
px.bar(df, color="category",
color_discrete_sequence=px.colors.qualitative.Set2)
이중 y축
fig = make_subplots(specs=[[{"secondary_y": True}]])
fig.add_trace(go.Bar(x=x, y=y1, name="매출"), secondary_y=False)
fig.add_trace(go.Scatter(x=x, y=y2, name="성장률"), secondary_y=True)
fig.update_yaxes(title_text="매출 (억원)", secondary_y=False)
fig.update_yaxes(title_text="성장률 (%)", secondary_y=True)
Plotly의 한계
- 렌더링 속도 — 수십만 행 이상의 대용량 데이터에서는 느려질 수 있다. 이 경우
datashader나WebGL기반 트레이스(Scattergl)를 사용한다. - 학습 곡선 —
graph_objects의 딕셔너리 기반 설정은 처음엔 복잡하게 느껴질 수 있다. - 파일 크기 — HTML로 내보낼 경우 Plotly.js 번들이 포함돼 파일이 수 MB에 달할 수 있다.
마무리
Plotly는 “차트를 보여주는” 것을 넘어 “데이터를 탐색하게” 만든다. plotly.express의 간결함과 graph_objects의 강력한 제어력을 적절히 조합하면, 정적 이미지로는 불가능했던 이야기를 데이터로 전달할 수 있다. Streamlit과 함께 사용하면 대시보드 개발 속도는 더욱 빨라진다.