格 11 實作選型・臺北市資料大平臺

用開放資料做一個 Vibe Coding 專案

一題講師示範、五題學員自選。所有資料源都在 2026/8/21 實際打過 API 驗證, 包含它們各自會坑你的地方。

示範題 50 分鐘 學員實作 60–90 分鐘 選題 5 題・難度分級 資料源實測 9 支
路線

先決定在哪裡寫程式

這件事必須先講清楚,因為它不是偏好問題,是限制問題。臺北市資料大平臺的 API 不允許瀏覽器直接讀取——這是實測結果,不是猜測。

實測:誰能在瀏覽器裡直接抓

在 https://example.com 的頁面上執行 fetch(),結果如下。

資料源瀏覽器直接 fetch實測回應意思是
YouBike 2.0 即時
tcgbusfs.blob…
可以 200,1,794 筆,0.99 秒 單檔 HTML 就能做完整個專案
data.taipei 全部 API
AED/事故/人口/垃圾
被擋 TypeError: Failed to fetch 抓取這段必須在 Colab 做
怎麼確定是 CORS 而不是網路不通 同一支網址改用 fetch(url, {mode:'no-cors'}) 會拿到 {type:"opaque", status:0}——連得上、有回應,只是瀏覽器不讓 JavaScript 讀。 伺服器沒有回 Access-Control-Allow-Origin,就是這個結果。 這個區別值得在課堂上示範一次,因為學員(和 AI)都會把它誤判成「網址壞了」。

兩條路線,切點在中間

路線 A ─ 單檔 HTML 全包

只有 YouBike 走得通 → 拿來當示範題
  • 零安裝,瀏覽器打開就有畫面
  • 抓取、處理、分析、視覺化都在同一個檔案
  • 做出來的東西看起來像「一個網站」,成就感最高

路線 B ─ Colab 抓+處理,HTML 做視覺化

其餘四題都得這樣走 → 學員自選題
  • Colab 段的交付物:一份乾淨的 CSV
  • HTML 段讀那份 CSV,不再碰 API
  • 時間不夠時第二段可整段砍掉,第一段仍是完整成品
為什麼中間要切一刀 兩段各自可驗收、可回頭。Colab 段做完就有一份能拿去用的資料檔,就算後面全砍掉也不算白做—— 這正是找出流程中不可逆的那一刀在小專案上的樣子。
示範

YouBike 借還失衡・50 分鐘逐步

五個階段,每一階段都有一個給 AI 的指令、一個驗收動作、一個預期會踩到的坑。 最後一個階段是刻意讓它失敗的——那是整場的樞紐。

示範前先看終點 先打開做好的成品(示範題_YouBike儀表板.html)讓學員看 30 秒,再關掉。 知道終點長什麼樣,中間的每一步才有意義。
00–08

階段 1・抓取

目標不是拿到資料,是逼 AI 先去看資料長什麼樣,而不是憑記憶猜欄位。

貼給 AI
我要做一個網頁,抓臺北市 YouBike 2.0 的即時站點資料,網址是 https://tcgbusfs.blob.core.windows.net/dotapp/youbike/v2/youbike_immediate.json 第一步先不要做任何分析。請寫一個單檔 HTML,用瀏覽器打開後會抓這支 API,並在畫面上顯示: 1. 總共幾筆資料 2. 第一筆資料的完整內容 3. 所有欄位名稱 不要假設欄位長什麼樣子,實際印出來給我看。
驗收・黃金案例 畫面上應該出現 1,700 筆以上,欄位裡要看得到 sna sarea act Quantity available_rent_bikes available_return_bikes latitude longitude。

如果 AI 直接交給你一個「已經做好分析和圖表」的版本,代表它跳過了這一步。 退回重做——這個退回動作本身就是示範。

常見卡點
  • AI 寫成 Node.js 腳本(用了 fs、require)。指令要明講「單檔 HTML,用瀏覽器打開」。
  • AI 憑記憶寫出 sbi / tot 這些欄位——那是 YouBike 1.0 的舊欄位,2.0 已經改名。這是最好的「AI 記憶不等於現況」實例。
現場話術 注意我沒有給它任何欄位說明,我要它自己去看。因為 API 文件常常跟實際回傳不一樣——待會你們就會親眼看到一個。
08–20

階段 2・處理

★ 最關鍵的一段

這裡有一個 AI 幾乎必踩的坑。不要提醒它,讓它踩下去,再帶學員抓出來。

貼給 AI
很好。現在做資料清理,加上這三件事: 1. act 欄位代表站點是否營運,我只要營運中的。 但在寫條件之前,先印出 act 這個欄位出現過的所有值、以及它們的型別。 2. sna 站名前面都有 "YouBike2.0_" 前綴,請拿掉。 3. 印出 sarea 欄位所有不重複的值。 清理完之後告訴我:原本幾筆、清理後幾筆、被排除掉的是哪些。
埋好的坑 act 是字串 "0" / "1",不是數字。 而 JavaScript 裡字串 "0" 是 truthy—— AI 若寫成 filter(s => s.act) 或 Boolean(s.act), 一站都濾不掉,而且不會噴任何錯誤。 指令第 1 條刻意要求「先印出值跟型別」,就是為了讓它先看見。
驗收・邊界輸入 正確答案是 1,794 → 1,765,排除 29 站。

最重要的一分鐘:當場把 s.act === "1" 改成 s.act, 讓大家看數字跳回 1,794。程式一樣跑得動、一樣不噴錯,只是答案錯了。

第二個發現・留給學員決定 sarea 印出來有 13 個值,其中一個是 「臺大公館校區」——它不是行政區。 問學員:該不該把它當成一區?

沒有標準答案。重點是這個決定必須由人來做,而 AI 不會主動問你。

現場話術 AI 沒有寫錯。它寫的程式跑得動、不噴錯、看起來完全正常。它只是做了一件跟你想的不一樣的事。 這就是為什麼「它跑起來了」不等於「它做對了」。
20–28

階段 3・分析

示範一件 AI 不能替你做的事:決定定義。

貼給 AI
現在要算各行政區的「可借率」。 但先回答我一個問題,再寫程式: 分母應該用 Quantity(總車柱數),還是用 available_rent_bikes + available_return_bikes? 請先算出「這兩個數字不相等」的站有幾個,再給我你的建議跟理由。
驗收・黃金案例 實測答案是 645 站不相等(約三分之一)。差額是故障或維修中的車柱。

如果 AI 沒回答問題就直接開始寫程式,退回。這也是示範的一部分。

現場話術 這題沒有標準答案,但有標準流程:先知道差多少,再決定用哪個。 AI 可以幫你算差多少,不能幫你決定要用哪個——因為那取決於你想回答什麼問題。
28–40

階段 4・視覺化

出畫面的一段,也是示範「回歸」的一段——加功能最容易弄壞既有的東西。

貼給 AI
加上地圖。用 Leaflet(CDN 引入),每個站畫一個圓點: - 顏色代表借還失衡:車被借光偏一色、車柱塞滿偏另一色、平衡是灰色 - 圓點大小代表站點規模(車柱數) - 點下去顯示站名、行政區、可借數、可還數 停止營運的 29 站也要畫出來,用灰色半透明,但不要算進上面的統計數字。
驗收・回歸 改完之後,前面幾個統計數字必須跟改之前完全一樣。 AI 很常在加新功能時順手改動舊邏輯——例如把「營運中站數」改成算了全部 1,794 站。

這就是三種免工具驗證裡的「回歸」:每次改完,回頭看前一步的答案有沒有變。

常見卡點
  • 地圖容器沒設高度 → 整張地圖看不見。#map{height:460px}
  • 地圖藏在 display:none 的區塊裡 → 顯示後要呼叫 map.invalidateSize(),否則只渲染一角。
  • 失衡色階若做成紅→黃→綠,會讀成「好壞」;這裡兩端沒有好壞之分,該用兩個對稱的顏色配中間灰。
40–50

階段 5・故意撞牆

★ 全場樞紐

讓學員在你手上先撞一次,他們待會自己做的時候才不會卡死在這裡。

貼給 AI
現在改抓另一支 API:臺北市 AED 設置地點,一樣畫成地圖。 https://data.taipei/api/v1/dataset/438c61ad-24f6-4e54-a1cc-e2cfe0e7051e?scope=resourceAquire&limit=1000&offset=0
會發生什麼 畫面空白,Console 出現 Failed to fetch 或紅色的 CORS 錯誤訊息。

AI 寫的程式碼完全正確。同一段邏輯搬到 Python 裡就跑得動。 這不是 AI 的錯——它看不到你的執行環境。

現場話術 這就是你們等一下自己選題會遇到的第一道牆。data.taipei 上的 API 全部都這樣。 所以你們的題目,「抓資料」那一段要在 Colab 做。我現在示範怎麼三行解決。

接續示範:Colab 三行版

import pandas as pd, requests

url = "https://data.taipei/api/v1/dataset/438c61ad-24f6-4e54-a1cc-e2cfe0e7051e?scope=resourceAquire&limit=1000&offset=0"
df = pd.DataFrame(requests.get(url).json()["result"]["results"])
df.head()

然後指出:這樣只拿到 1000 筆,全部有 2,729 筆。 分頁的通用寫法(五題都會用到,直接給學員):

import requests, pandas as pd, time

def fetch_all(rid, page=1000):
    """data.taipei 通用分頁抓取。limit 上限實測為 1000。"""
    rows, offset = [], 0
    while True:
        url = (f"https://data.taipei/api/v1/dataset/{rid}"
               f"?scope=resourceAquire&limit={page}&offset={offset}")
        r = requests.get(url, timeout=30).json()["result"]
        rows += r["results"]
        print(f"已抓 {len(rows)} / {r['count']}")
        if len(r["results"]) < page or len(rows) >= r["count"]:
            break
        offset += page
        time.sleep(0.2)
    return pd.DataFrame(rows)

df = fetch_all("438c61ad-24f6-4e54-a1cc-e2cfe0e7051e")
df.to_csv("aed.csv", index=False, encoding="utf-8-sig")   # sig 才不會在 Excel 開出亂碼
驗收 印出來的最後一行應該是 已抓 2729 / 2729。 實測抓 22,762 筆的事故資料需要 23 頁、約 6.5 秒。
選題

五題・給學員自己挑

難度從一顆星到四顆星。每一題的資料都實際抓過,筆數和欄位名是實測值不是文件值。 第 1 題是保底題——最沒把握的組給這個,一定收得掉。

1搬家潮真的存在嗎★☆☆☆

哪個月丟掉最多傢具和彈簧床?這個規律十年來有變過嗎?

https://data.taipei/api/v1/dataset/55a77e88-1949-4766-b451-3bf60d97b5fb?scope=resourceAquire&limit=1000&offset=0
資料集筆數期間欄位
臺北市巨大垃圾統計120逐月,到民國 115 年 7 月 年度 月份 總計(噸) 傢具(噸) 彈簧床(噸) 腳踏車(噸) 樹枝(噸)
抓取
一次抓完,不用分頁
處理
民國年轉西元、數值是字串要轉數字
分析
各月份十年平均、年增率
視覺化
月份長條圖+逐年折線
已知陷阱
  • 欄位名用全形括號 (噸),不是半形。AI 寫 df['總計(噸)'] 會 KeyError。
  • 所有數值都是字串("1002")。不轉型別就畫圖,長條會照字串排序。
  • 年度 是民國年(115),畫時間軸前要決定怎麼處理。
收工標準 一張圖能回答「哪個月最多」,而且看得出這個規律在十年間穩不穩定。
2哪一區老得最快★★☆☆

四年來人口往哪一區移動?哪一區的高齡比例上升最快?

行政區×月 https://data.taipei/api/v1/dataset/9681db4c-fb1b-4a23-9013-e74483b6b046?scope=resourceAquire&limit=1000&offset=0
各里×逐齡 https://data.taipei/api/v1/dataset/c8f5b53d-ef3d-4321-ae8e-58cd2a5ee73c?scope=resourceAquire&limit=1000&offset=0
資料集筆數期間關鍵欄位
各行政區人口數及戶數715111/1 – 115/7 年份 月份 行政區 戶數 人口數_合計數量 人口數_男數量 人口數_女數量
各里人口數按年齡分60,501112 年起 區域別 性別 總計 0歲數量 … 100歲以上數量
抓取
第二支要分頁(61 頁)
處理
strip 空白、濾掉總計列、逐齡欄 wide→long
分析
老化指數自算、四年變化量
視覺化
折線+人口金字塔
這一題最好的教材 行政區 欄位的值有三種空白變體:" 總計 "、"總計"、" 總計"。 不做 .str.strip() 直接 group by,13 個區會變成 39 組。

而且圖上不會報錯,只會多出一堆看起來很像的區名。AI 幾乎不會主動處理這件事。

其他陷阱
  • 資料裡混了「總計」列,不濾掉的話它會是最高的那根長條。
  • 各里資料是每個年齡一欄(101 欄),要轉成長格式才畫得出金字塔。
收工標準 group by 之後剛好 12 個行政區(不含總計)。這個數字對了,清理就對了。
3你家附近有 AED 嗎★★☆☆

哪一區的 AED 密度最低?分佈跟人口對得上嗎?

https://data.taipei/api/v1/dataset/438c61ad-24f6-4e54-a1cc-e2cfe0e7051e?scope=resourceAquire&limit=1000&offset=0
資料集筆數座標欄位
臺北市 AED 設置地點2,729WGS84,實測全部有效 場所名稱 場所地址 區域代碼 緯度 經度 場所分類 場所類型 aed放置地點
抓取
3 頁分頁
處理
區域代碼對照區名、9 筆場所名稱有空白
分析
併人口算每萬人 AED 數
視覺化
點位地圖+分區密度排行
已知陷阱
  • 沒有「行政區」這一欄,只有 區域代碼(如 63000030)。 實測共 12 種,要自己從 場所地址 或另建對照表還原區名——這一題的主要工作量在這裡。
  • metadata 寫的欄位名是 AED放置地點,API 實際回的是小寫 aed放置地點。 永遠以實際回傳的 key 為準。
  • 要併人口資料,就會連帶踩到第 2 題的空白陷阱。
收工標準 地圖上 2,729 個點全部落在臺北市範圍內(實測沒有離群點);密度排行是「每萬人幾台」而不是「幾台」。
4哪裡借不到車★★★☆

全市即時的空站與滿站在哪裡?一整天下來,車子是怎麼在城市裡流動的?

https://tcgbusfs.blob.core.windows.net/dotapp/youbike/v2/youbike_immediate.json ← 唯一能在瀏覽器直接抓的
資料集筆數更新關鍵欄位
YouBike2.0 即時資訊1,794每分鐘 sna sarea act Quantity available_rent_bikes available_return_bikes latitude longitude mday
抓取
一支網址、846 KB、1 秒
處理
act 字串、站名前綴、非行政區值
分析
可借率、空站滿站排行
視覺化
即時地圖+分區長條
進階做法(給有餘力的組) 這支 API 只有當下,沒有歷史。想看「一天怎麼流動」,就得自己每 10 分鐘抓一次存起來。 資料不會自己長出歷史——這件事很多人第一次做才會意識到。
收工標準 清理後站數 1,765(排除 29 站);分區統計裡「臺大公館校區」有被標註或另外處理。

這題是示範題。留給學員時要求他們做出示範沒做過的東西,例如加時間累積、或改成回答「該從哪一站調度到哪一站」。

5幾點最容易出事★★★★

哪個路口最危險?死亡事故有沒有集中在特定時段、天候或路型?

斑點圖(輕量) https://data.taipei/api/v1/dataset/d4aaaaa6-d03e-4539-945b-cdbd9387007d?scope=resourceAquire&limit=1000&offset=0
明細(46 欄) https://data.taipei/api/v1/dataset/83d6d29c-6801-41a2-95c6-47d551646db3?scope=resourceAquire&limit=1000&offset=0
資料集筆數年度關鍵欄位
道路交通事故斑點圖22,762114 年(108–114 各一支) 發生時間 處理別 肇事地點 座標-x 座標-y
死傷交通事故資料22,762114 年(101–114 各一支) 發生時-hours 天候 光線 速限-速度限制 道路型態 年齡 飲酒情形 …
抓取
23 頁分頁,約 6.5 秒
處理
全形欄位名、時間解析、濾掉零座標
分析
小時分佈、A1/A2 比較、天候交叉
視覺化
熱點地圖+24 小時折線
兩個一定會踩的陷阱
  • 欄位名的破折號是全形 -(U+FF0D),不是半形 -;而且英文被轉成小寫(metadata 寫 座標-X,API 實回 座標-x)。手打幾乎一定打錯,要複製貼上。
  • 22,762 筆裡有 2 筆座標是 0。不濾掉,地圖會在西非外海出現兩個點,整張圖的縮放全毀。 兩筆、萬分之一,但足以毀掉整張圖——這就是為什麼「跑得出來」不等於「對」。
驗收・可以拿來對答案的實測值
  • 總筆數 22,762;處理別 1(A1 死亡)57 筆、2(A2)22,705 筆
  • 事故最多的小時是 08 時(2,117 件),其次 17 時、18 時 — 通勤時段
  • 最少的是凌晨 03 時(105 件)
  • 行政區排行:中山區 3,010 > 大安區 2,491 > 中正區 2,172

學員做出來的數字對不上這些,就是清理出了問題。這是全部五題裡唯一有標準答案可以對的。

附錄

平台的三個陷阱與驗證紀錄

這幾件事本身就是很好的教材:不是每個掛在開放資料平台上的資料集都有 API 可以抓。

陷阱一・API 回空陣列,不是回錯誤

?scope=resourceAquire 只對平台真的入庫的表格型資源有效。若資源只是一個外部連結(CSV、SHP、Excel), API 會回空陣列 [] 而不是報錯——AI 拿到空陣列常會自己編一組假資料填進去。 實測 9 個 rid 有 6 個回空。先用這支看清楚再決定:

https://data.taipei/api/frontstage/tpeod/dataset.view?id={資料集頁的 id}
→ 看每個 resource 的 has_datastore 是不是 true

陷阱二・交通類資料常只回「型錄」

公車、捷運 OD 這類資料集,API 回的是一筆記錄,裡面的「介接網址」才是真資料。 捷運分時 OD 的 API 回的是 115 筆每月 CSV 的清單,單月檔超過 30 MB。這也是這兩題沒被排進五題的原因。

陷阱三・欄位名以實際回傳為準

全形破折號、全形括號、英文自動轉小寫、值的前後空白——這四種在五題裡各出現過。 metadata 和實際回傳不一致是常態,而 AI 傾向相信 metadata。

查證為不可用(省下重複踩雷的時間)

想做的題目結果原因
空氣品質實際測值不可用 data.taipei 三個候選資料集都是 has_datastore:false。要做請改用環境部 data.moenv.gov.tw。平台上只有 17 個測站的點位,沒有測值。
垃圾清運量/資源回收率不可用 「一般廢棄物處理工作」34 個欄位完全符合需求,但只有 Excel 沒有 API。故改用巨大垃圾統計。
停車場即時剩餘車位未採用 網址存活但格式是 octet-stream,且座標是 TWD97 二度分帶要轉換。要用請上課前先自行確認編碼。
公車站牌/動態未採用 實際資料全是 .gz 壓縮檔,多一層解壓對初學者是不必要的門檻。

實測環境

2026 年 8 月 21 日,透過瀏覽器實際發出請求驗證。 筆數與欄位名為當日實際回傳值。limit 上限實測為 1000(填 2000 仍只回 1000)。 上課前建議重跑一次分頁函式確認資料仍在。