Plotly 완벽 가이드 — 인터랙티브 데이터 시각화의 모든 것




“matplotlib으로 만든 차트는 정적이다. Plotly로 만든 차트는 살아있다.”

데이터 시각화는 숫자를 이야기로 바꾸는 작업이다. 그런데 차트가 정적인 이미지에 머문다면 청중은 데이터를 스스로 탐색할 수 없다. Plotly는 이 문제를 해결하는 Python 인터랙티브 시각화 라이브러리다. 마우스를 올리면 값이 나타나고, 드래그로 확대하고, 클릭으로 데이터를 필터링한다. 이 글에서는 Plotly의 두 API인 plotly.expressplotly.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의 한계

  • 렌더링 속도 — 수십만 행 이상의 대용량 데이터에서는 느려질 수 있다. 이 경우 datashaderWebGL 기반 트레이스(Scattergl)를 사용한다.
  • 학습 곡선graph_objects의 딕셔너리 기반 설정은 처음엔 복잡하게 느껴질 수 있다.
  • 파일 크기 — HTML로 내보낼 경우 Plotly.js 번들이 포함돼 파일이 수 MB에 달할 수 있다.

마무리

Plotly는 “차트를 보여주는” 것을 넘어 “데이터를 탐색하게” 만든다. plotly.express의 간결함과 graph_objects의 강력한 제어력을 적절히 조합하면, 정적 이미지로는 불가능했던 이야기를 데이터로 전달할 수 있다. Streamlit과 함께 사용하면 대시보드 개발 속도는 더욱 빨라진다.




댓글 남기기