iBetter Books
수정

반응형 시각화 대시보드 구성

Ch 02에서 만든 필터가 filtered_df를 갱신합니다. 이제 그 데이터프레임으로 차트를 그립니다. 차트도 filtered_df에 의존하므로, 위젯을 건드리면 SQL 재실행과 차트 갱신이 연달아 일어납니다. 버튼 없이, 새로고침 없이입니다.

이 챕터에서는 filtered_df를 기반으로 세 가지 차트를 만들고, mo.hstack()mo.vstack()으로 레이아웃을 구성합니다.

셀 12: 카테고리별 주문 금액 막대 차트

필터 결과를 카테고리별로 집계해 막대 차트로 표시합니다.

cat_agg = mo.sql(f"""    SELECT        category,        COUNT(*) AS 주문수,        ROUND(SUM(amount), 0) AS 총_금액    FROM filtered_df    GROUP BY category    ORDER BY 총_금액 DESC""")fig_bar = px.bar(    cat_agg,    x="category",    y="총_금액",    color="category",    title="카테고리별 총 주문 금액",    labels={"category": "카테고리", "총_금액": "총 금액 (원)"},)fig_bar

px는 셀 1에서 임포트된 변수입니다. marimo DAG는 이 셀이 셀 1에 의존한다는 사실을 알고 있으므로 import plotly.express as px를 여기서 다시 쓰지 않습니다. 같은 변수를 두 셀에서 정의하면 MB002(중복 정의) 오류가 발생합니다.

filtered_df를 다시 SQL로 집계합니다. filtered_df가 바뀔 때마다 이 셀도 재실행됩니다. mo.sql() 안에서 Python 변수를 테이블로 쓰는 패턴은 Ch 01에서 df를 참조한 것과 동일합니다.

plotly의 fig 객체를 셀 마지막에 두면 marimo가 차트를 렌더링합니다. plt.show()display() 없이 동작합니다.

셀 13: 지역별 평균 금액 가로 막대 차트

지역별 평균 주문 금액을 가로 막대 차트로 봅니다.

region_agg = mo.sql(f"""    SELECT        region,        COUNT(*) AS 주문수,        ROUND(AVG(amount), 0) AS 평균_금액    FROM filtered_df    GROUP BY region    ORDER BY 평균_금액 DESC""")fig_region = px.bar(    region_agg,    x="평균_금액",    y="region",    orientation="h",    title="지역별 평균 주문 금액",    labels={"region": "지역", "평균_금액": "평균 금액 (원)"},    color="평균_금액",    color_continuous_scale="Blues",)fig_region

가로 막대 차트(orientation="h")는 지역 이름이 겹치지 않아 읽기 편합니다. color_continuous_scale로 금액이 높을수록 진한 파란색으로 표현했습니다.

셀 14: 요일별 주문 분포 차트

요일별 주문 건수 분포를 확인합니다. 요일 순서를 정렬해서 읽기 쉽게 만듭니다.

weekday_order = ["월", "화", "수", "목", "금", "토", "일"]weekday_agg = mo.sql(f"""    SELECT        weekday,        COUNT(*) AS 주문수    FROM filtered_df    GROUP BY weekday""")# Pandas를 통해 요일 순서 지정weekday_pd = weekday_agg.to_pandas() if hasattr(weekday_agg, "to_pandas") else weekday_aggweekday_pd["weekday"] = pd.Categorical(    weekday_pd["weekday"],    categories=weekday_order,    ordered=True,)weekday_pd = weekday_pd.sort_values("weekday")fig_weekday = px.line(    weekday_pd,    x="weekday",    y="주문수",    markers=True,    title="요일별 주문 건수",    labels={"weekday": "요일", "주문수": "주문 건수"},)fig_weekday

Ch 01에서 default_sql_output = "pandas" 설정으로 mo.sql()이 항상 Pandas DataFrame을 반환하도록 고정했습니다. 따라서 weekday_agg는 Pandas DataFrame입니다. hasattr(weekday_agg, "to_pandas") 조건은 방어적으로 남겨뒀지만, 이 설정에서는 항상 else 분기를 탑니다. 팀 프로젝트에서 설정 파일 없이 노트북만 공유할 경우를 대비해 방어 코드를 유지했습니다.

셀 15: 차트를 대시보드 레이아웃으로 배치

세 차트를 mo.hstack()mo.vstack()으로 배치합니다.

top_row = mo.hstack([fig_bar, fig_region], widths="equal")dashboard = mo.vstack([top_row, fig_weekday])dashboard

mo.hstack([fig_bar, fig_region], widths="equal")은 두 차트를 동일한 너비로 나란히 놓습니다. mo.vstack([top_row, fig_weekday])는 위 행과 아래 차트를 세로로 쌓습니다. 레이아웃 코드가 두 줄입니다. 복잡한 CSS나 그리드 설정 없이 구성됩니다.

dashboard를 셀 마지막에 두면 세 차트가 한 셀 출력 안에 표시됩니다.

셀 16: 필터 위젯과 대시보드를 한 화면에

필터 위젯과 차트를 한 화면에 함께 보고 싶다면 위젯들도 레이아웃에 넣을 수 있습니다.

filters_panel = mo.vstack([    mo.md("### 필터"),    category_filter,    amount_min_filter,    region_filter,])full_layout = mo.hstack([filters_panel, dashboard], widths=[1, 3])full_layout

widths=[1, 3]은 왼쪽 필터 패널과 오른쪽 차트 영역의 너비 비율을 1:3으로 지정합니다. 전형적인 사이드바-메인 레이아웃입니다.

이 셀 하나로 필터와 차트가 나란히 보이는 대시보드가 완성됩니다. 왼쪽 드롭다운이나 슬라이더를 건드리면 오른쪽 차트가 즉시 갱신됩니다.

matplotlib을 쓴다면

plotly 대신 matplotlib을 쓰는 환경이라면 같은 흐름을 이렇게 작성합니다. matplotlib 경로를 선택했다면 import matplotlib.pyplot as plt를 셀 1로 옮겨 임포트를 한 곳에 모아둡니다. 아래 예제에서는 독립 섹션임을 명확히 하기 위해 임포트를 함께 표기했습니다.

import matplotlib.pyplot as pltplt.close("all")fig_mpl, axes = plt.subplots(1, 2, figsize=(12, 4))cat_pd = cat_agg.to_pandas() if hasattr(cat_agg, "to_pandas") else cat_aggaxes[0].bar(cat_pd["category"], cat_pd["총_금액"])axes[0].set_title("카테고리별 총 금액")axes[0].set_xlabel("카테고리")axes[0].set_ylabel("총 금액 (원)")axes[0].tick_params(axis="x", rotation=30)region_pd = region_agg.to_pandas() if hasattr(region_agg, "to_pandas") else region_aggaxes[1].barh(region_pd["region"], region_pd["평균_금액"])axes[1].set_title("지역별 평균 금액")axes[1].set_xlabel("평균 금액 (원)")plt.tight_layout()fig_mpl

matplotlib는 전역 Figure 상태를 유지하기 때문에 셀 재실행 시 누적이 일어납니다. plt.close("all")을 셀 첫 줄에 두는 습관을 지킵니다. PART 04 Ch 04에서 다룬 내용입니다.

matplotlib Figure 객체는 mo.hstack()에 그대로 넣을 수 있습니다.

mo.hstack([fig_mpl])

여기까지 노트북의 상태

셀 1:  import (mo, np, pd, px)셀 2:  df 생성 (고정 시드)셀 3:  overview SQL셀 4:  by_category SQL셀 5:  by_region_weekday SQL셀 6:  mo.md() 요약셀 7:  category_filter 위젯셀 8:  amount_min_filter 위젯셀 9:  region_filter 위젯셀 10: filtered_df SQL셀 11: 필터 결과 요약셀 12: cat_agg SQL + fig_bar셀 13: region_agg SQL + fig_region셀 14: weekday_agg SQL + fig_weekday셀 15: dashboard 레이아웃셀 16: full_layout (필터 + 차트)

위젯 하나를 건드리면 셀 10, 11, 12, 13, 14, 15, 16이 순서대로 재실행됩니다. DAG가 의존 관계를 알고 있기 때문에, 불필요한 셀(셀 2, 3, 4, 5, 6)은 다시 실행하지 않습니다.

정리

  • filtered_df를 다시 mo.sql()에서 테이블로 참조해 차트별 집계를 작성합니다. filtered_df가 바뀌면 차트 셀도 연쇄 재실행됩니다.
  • plotly Figure 객체는 셀 마지막 표현식으로 두면 렌더링됩니다.
  • mo.hstack([fig1, fig2], widths="equal")로 차트를 나란히, mo.vstack()으로 위아래로 배치합니다.
  • widths=[1, 3]으로 필터 패널과 차트 영역의 비율을 지정합니다.
  • matplotlib를 쓴다면 셀 첫 줄에 plt.close("all")을 두어 Figure 누적을 방지합니다.
  • Ch 01에서 default_sql_output = "pandas"로 설정하면 SQL 출력이 Pandas로 고정됩니다. 방어적 .to_pandas() 패턴은 설정 파일 없이 노트북만 배포하는 경우를 위해 남겨둡니다.