一. 前言:Notebook 很方便,但也很容易變成考古現場 #
Notebook 是資料分析和原型開發的好朋友。 你可以一格一格跑、邊看結果邊改、把圖表和解釋放在同一個地方。 但拍拍君也要老實說:Notebook 很容易變成一種微妙的黑魔法。 常見狀況大概長這樣:
- 第 3 格其實依賴第 17 格先跑過。
- 變數在記憶體裡,但檔案裡看不到它從哪裡來。
- Git diff 看到一大坨 JSON,review 起來眼神死。
- 想把 notebook 變成小工具,最後又重寫成 Streamlit。 今天要介紹的 marimo,想解決的就是這些問題。 它的核心概念很直接:
- Cell 之間有明確依賴關係。
- 變數改變後,相關 cell 自動更新。
- 檔案本質上是乾淨的 Python script。
- 可以同時當 notebook、互動式 app、命令列腳本使用。 如果你之前看過拍拍君寫的 Streamlit 或 DuckDB,可以這樣分工:
- Streamlit:適合做給別人用的資料 Web App。
- DuckDB:適合用 SQL 快速分析檔案資料。
- marimo:適合把探索過程做成可重現、可互動、好 review 的 notebook。 今天我們會做一個小型銷售資料探索工具,從資料產生、互動篩選、圖表到匯出一次走完。
二. 安裝:先建立一個乾淨專案 #
先建立專案:
mkdir marimo-demo
cd marimo-demo
uv init
uv add marimo pandas altair duckdb
如果你不用 uv,也可以用一般虛擬環境:
python -m venv .venv
source .venv/bin/activate
pip install marimo pandas altair duckdb
確認 marimo 可以啟動:
marimo --version
建立第一個 notebook:
marimo edit sales_report.py
注意檔名是 sales_report.py,不是 .ipynb。
這是 marimo 很重要的設計:notebook 直接存成 Python 檔。
對 Git、code review、formatter、搜尋工具都比較友善。
三. 第一個 marimo Notebook:每個 cell 都是 Python #
開啟編輯器後,你可以建立第一個 cell:
import marimo as mo
import pandas as pd
再建立第二個 cell:
mo.md(
"""
# Sales Report
這是一份用 marimo 做的小型互動式資料報告。
"""
)
mo.md() 會把 Markdown 內容渲染到 notebook 裡。
這點跟 Jupyter 類似,但 marimo 的 cell 仍然是普通 Python。
你可以把 Markdown、UI 元件、資料處理都放在 Python script 裡。
如果你打開 sales_report.py,會看到它不是一坨難懂 JSON,而是類似這樣的程式:
import marimo
__generated_with = "0.12.0"
app = marimo.App()
@app.cell
def _():
import marimo as mo
import pandas as pd
return mo, pd
@app.cell
def _(mo):
mo.md("# Sales Report")
return
if __name__ == "__main__":
app.run()
這就是它適合放進專案的原因。 你可以 git diff,可以 review,也可以搜尋某個函式或變數。
四. Reactive Cell:變數不是隨便亂飄的 #
marimo 跟傳統 notebook 最大的差異,是它會追蹤 cell 之間的依賴關係。 假設你有一個 cell:
tax_rate = 0.05
另一個 cell:
subtotal = 1200
total = subtotal * (1 + tax_rate)
total
當你改 tax_rate,使用到 tax_rate 的 cell 會自動重新執行。
這聽起來很小,但它解決了 notebook 最煩的問題:執行順序不可信。
在傳統 notebook 裡,你可能先跑下面的格子,再回去改上面的格子,最後畫面看起來對,實際狀態卻不一定對。
marimo 比較像 spreadsheet:
- 上游變數改了,下游結果就更新。
- 沒有被依賴的 cell 不會亂跑。
- 同一個變數不能在多個 cell 重複定義。 這個限制一開始會讓人覺得「怎麼這麼嚴格」。 但寫久會發現,這正是它讓 notebook 可重現的關鍵。
五. 準備範例資料:做一份小型訂單表 #
我們先建立一個 cell 產生資料。
from random import Random
rng = Random(42)
products = [
("keyboard", "hardware", 1200),
("mouse", "hardware", 600),
("monitor", "hardware", 4200),
("notebook", "stationery", 80),
("sticker", "stationery", 30),
("coffee", "food", 120),
]
cities = ["Taipei", "Tainan", "Taichung", "Kaohsiung"]
orders = []
for order_id in range(1, 401):
product, category, base_price = rng.choice(products)
month = rng.randint(1, 4)
day = rng.randint(1, 28)
quantity = rng.randint(1, 5)
discount = rng.choice([0, 0, 0, 0.05, 0.1])
orders.append(
{
"order_id": order_id,
"date": f"2026-{month:02d}-{day:02d}",
"city": rng.choice(cities),
"product": product,
"category": category,
"price": base_price,
"quantity": quantity,
"discount": discount,
}
)
df = pd.DataFrame(orders)
df["date"] = pd.to_datetime(df["date"])
df["revenue"] = df["price"] * df["quantity"] * (1 - df["discount"])
df.head()
這裡沒有讀外部檔案,方便你直接跟著跑。
真實專案裡,df 可以來自 CSV、Parquet、資料庫,或前一篇介紹過的 DuckDB 查詢結果。
接著放一個快速摘要:
summary = {
"orders": len(df),
"revenue": int(df["revenue"].sum()),
"cities": df["city"].nunique(),
"products": df["product"].nunique(),
}
summary
你可以先確認資料長得合理,再開始加互動。
六. 加上 UI 元件:讓資料探索不只靠改程式碼 #
marimo 內建一組 UI 元件。 先做城市篩選:
city_picker = mo.ui.multiselect(
options=sorted(df["city"].unique()),
value=sorted(df["city"].unique()),
label="城市",
)
city_picker
再做類別篩選:
category_picker = mo.ui.dropdown(
options=["all"] + sorted(df["category"].unique()),
value="all",
label="商品類別",
)
category_picker
UI 元件本身是 Python 物件。
你可以用 .value 取得目前選擇:
filtered = df[df["city"].isin(city_picker.value)]
if category_picker.value != "all":
filtered = filtered[filtered["category"] == category_picker.value]
filtered.head()
這就是 reactive notebook 的爽點。
使用者改了 dropdown 或 multiselect,filtered 會更新,依賴 filtered 的圖表也會跟著更新。
你不用自己寫 callback,也不用手動管理狀態。
七. 做一張圖:Altair 很適合搭配 marimo #
接著用 Altair 畫每個城市的營收。
import altair as alt
revenue_by_city = (
filtered.groupby("city", as_index=False)["revenue"]
.sum()
.sort_values("revenue", ascending=False)
)
chart = (
alt.Chart(revenue_by_city)
.mark_bar()
.encode(
x=alt.X("city:N", title="City"),
y=alt.Y("revenue:Q", title="Revenue"),
tooltip=["city", "revenue"],
)
.properties(width=520, height=300)
)
chart
如果你不想加 Altair,也可以先用 pandas 的簡單輸出。 但 marimo 很適合這種「資料表 + 圖表 + 文字解釋」的工作流。 再加一個月份趨勢:
monthly = filtered.assign(month=filtered["date"].dt.to_period("M").astype(str))
monthly = monthly.groupby("month", as_index=False)["revenue"].sum()
line = (
alt.Chart(monthly)
.mark_line(point=True)
.encode(
x=alt.X("month:N", title="Month"),
y=alt.Y("revenue:Q", title="Revenue"),
tooltip=["month", "revenue"],
)
.properties(width=520, height=260)
)
line
到這裡,這份 notebook 已經不只是靜態筆記。 它變成一個小型互動式資料報告。
八. 用 DuckDB 查 DataFrame:SQL 派也可以很舒服 #
如果你喜歡 SQL,可以把 DuckDB 接進來。
import duckdb
top_products = duckdb.sql(
"""
SELECT
product,
category,
COUNT(*) AS orders,
ROUND(SUM(revenue), 2) AS revenue
FROM filtered
GROUP BY product, category
ORDER BY revenue DESC
LIMIT 5
"""
).df()
top_products
DuckDB 可以直接查目前 Python 作用域裡的 DataFrame。 也就是說,你可以用 pandas 做前處理,用 UI 做篩選,再用 SQL 做彙總。 這種混搭在資料探索時很實用。 但拍拍君建議你保持一個原則:每個 cell 只做一件清楚的事。 例如:
- 一格建立 UI。
- 一格套用篩選。
- 一格做彙總。
- 一格畫圖。
- 一格寫解釋。 這樣依賴關係清楚,未來回頭看才不會想把昨天的自己抓去 code review。
九. Markdown 報告:把結果寫成人看得懂的故事 #
數字本身不會自動變成洞見。
你可以用 mo.md() 把摘要整理成文字:
best_city = revenue_by_city.iloc[0]["city"]
best_revenue = int(revenue_by_city.iloc[0]["revenue"])
mo.md(
f"""
## 本次篩選結果
目前篩選後共有 **{len(filtered)}** 筆訂單。
營收最高的城市是 **{best_city}**,營收為 **{best_revenue:,}**。
"""
)
這段文字會隨著 UI 選擇一起更新。 所以報告不是「某一次執行後留下的截圖」,而是跟資料狀態綁在一起的說明。 這對內部分析很有用。 你可以把同一份 notebook 交給同事,讓他們切換城市、類別,看到對應的數字和文字。
十. 執行模式:edit、run、export 各有用途 #
開發時使用:
marimo edit sales_report.py
如果你只是想把它當成 app 跑:
marimo run sales_report.py
這時使用者會看到互動介面,但不一定要進入編輯模式。 如果想輸出成 HTML:
marimo export html sales_report.py -o sales_report.html
也可以匯出成 notebook:
marimo export ipynb sales_report.py -o sales_report.ipynb
拍拍君會建議:原始碼保留 .py,必要時才 export。
因為 .py 比較適合版本控制,也比較容易被一般開發工具理解。
十一. 跟 Jupyter、Streamlit 怎麼選? #
這三者不是誰取代誰,而是使用情境不同。
| 工具 | 最適合 | 優點 | 要注意 |
|---|---|---|---|
| Jupyter | 教學、快速探索、龐大生態 | 大家熟、套件多、文件多 | 執行順序容易失真 |
| marimo | 可重現分析、互動 notebook、Git-friendly 報告 | reactive、純 Python、好 review | 生態還在成長 |
| Streamlit | 給使用者操作的資料 app | 部署和 UI 很直覺 | 分析過程通常要另外整理 |
| 拍拍君自己的選法通常是: |
- 想快速試一段模型或資料處理:Jupyter 可以。
- 想讓 notebook 長期留在 repo 裡:marimo 很適合。
- 想做成穩定給別人用的 web tool:Streamlit 比較順。 如果你的團隊常常把 notebook 放進 Git,但 review 很痛苦,marimo 值得試。
十二. 專案化建議:不要把 notebook 寫成一坨 #
marimo 雖然比傳統 notebook 更有結構,但還是可能被寫亂。 拍拍君建議這樣整理:
marimo-demo/
├── pyproject.toml
├── sales_report.py
├── src/
│ └── sales_demo/
│ ├── __init__.py
│ ├── data.py
│ └── metrics.py
└── tests/
└── test_metrics.py
把可測試的邏輯放進 src:
# src/sales_demo/metrics.py
import pandas as pd
def revenue_by_city(df: pd.DataFrame) -> pd.DataFrame:
return (
df.groupby("city", as_index=False)["revenue"]
.sum()
.sort_values("revenue", ascending=False)
)
notebook 裡只負責組裝:
from sales_demo.metrics import revenue_by_city
city_revenue = revenue_by_city(filtered)
city_revenue
這樣有幾個好處:
- 核心邏輯可以寫測試。
- notebook 不會越長越難讀。
- 專案日後可以改成 CLI、API 或 Streamlit app。
- marimo 保留互動探索的角色,不需要承擔所有架構責任。 Notebook 不是不能工程化。 只是你要很早就把「可重用邏輯」和「互動報告」分開。
十三. 常見踩坑:拍拍君先幫你踩一輪 #
第一個坑:同一個變數不能在多個 cell 定義。
這是 marimo 故意的。
如果你在兩個 cell 都寫 df = …,它會提醒你。
解法是把流程拆清楚,例如:
raw_df = load_orders()
filtered_df = apply_filters(raw_df, city_picker.value)
summary_df = summarize(filtered_df)
第二個坑:不要在 cell 裡做太重的副作用。
例如每次 UI 一改,就重新呼叫昂貴 API 或寫入資料庫。
比較好的方式是把昂貴步驟獨立出來,必要時加上快取或明確按鈕。
第三個坑:UI 元件的值要從 .value 讀。
city_picker.value
不要把 UI 物件本身拿去做 pandas 篩選。 第四個坑:不要把密碼和 token 寫進 notebook。 這點跟所有開發工具一樣。 用環境變數、.env、secret manager,或至少把本機設定排除在 Git 外。
結語 #
marimo 很適合那些「一開始只是探索,後來變成大家都要用」的分析工作。 它比傳統 notebook 更在乎可重現性,也比完整 Web App 更輕。 拍拍君會把它放在這個位置:
- 比 Jupyter 更適合進 repo。
- 比 Streamlit 更適合保留分析過程。
- 比純 Python script 更適合互動探索。 如果你的 notebook 常常變成神祕狀態機,試試 marimo。 它會逼你把資料依賴整理乾淨。 一開始有點嚴格,但這種嚴格很值得。