Python 資料全景課 / 第四部分 應用 · M13

M13 綜合專案:天文台天氣報告

最後一個單元,把整門課串起來:從香港天文台的開放資料取得真實的天氣 JSON,寫一份設定檔,整理成表格,統計、畫圖,對照官方數字,最後寫成一份任何人都能重新執行的 notebook 報告。
約 90 分鐘專案 notebook 1 個先修:M12
學完本單元,你能夠:
  1. 從開放資料 API 取得 JSON,網絡不通時改用離線副本;
  2. 用 YAML 設定檔控制報告,並用 JSON Schema 檢查設定;
  3. 把天文台的原始資料整理成 DataFrame,計算酷熱天氣日數和熱夜數目,並與天文台公布的數字對照;
  4. 寫出報告(JSON、CSV、圖),確認「重新啟動並全部執行」之後結果不變。

1專案:一份天氣報告 The project

M0 的第一幅圖說過資料的旅程:檔案 → Python 物件 → 在 JupyterLab 裏整理 → 寫出新的檔案。這個專案把整段旅程走一次,用的是真實的資料:香港天文台 2025 年每日的最高和最低氣溫,以及未來九天的天氣預報。

flowchart TD
    C["config.yaml<br/>設定(M9、M10)"] --> F["取得資料<br/>API 或離線副本(M8)"]
    F --> R["原始 JSON<br/>存檔(M7)"]
    R --> T["整理成表格<br/>pandas(M11)"]
    T --> A["統計、畫圖<br/>(M5、M11)"]
    A --> O["報告<br/>JSON、CSV、圖(M8、M10)"]
    O --> V["重新啟動並全部執行<br/>(M2)"]
    classDef s fill:#ffffff,stroke:#d3d1cb,color:#37352f
    classDef e fill:#f1f1ef,stroke:#d3d1cb,color:#37352f
    class C,O e
    class F,R,T,A,V s
圖 1 專案的流程。每一個方框,都是前面某個單元教過的事。

完成的報告要回答幾個問題:2025 年每月的平均最高、最低氣溫是多少?有多少個酷熱天氣日和熱夜?最熱和最冷是哪一天?未來九天的預報如何?

2開放資料 API Open data APIs

API(Application Programming Interface)在這裏的意思是:一個專門給程式讀的網址。瀏覽器打開網頁,得到給人看的 HTML;程式打開 API 的網址,得到 JSON。天文台的開放資料 API 不用登記、不用密鑰:

? 之後的 名稱=值,用 & 分隔,就是告訴 API 你要甚麼:station=HKO 是天文台總部,year=2025 是年份。

用標準函式庫的 urllib.request 就能取得,不用另外安裝套件:

import json
import urllib.request

with urllib.request.urlopen(url, timeout=10) as resp:
    text = resp.read().decode("utf-8")
data = json.loads(text)

timeout=10:等十秒還沒有回應就放棄,不要讓 notebook 一直卡住。

參數錯了,回來的不是 JSON

把 station 寫錯,天文台回來的是一句英文說明,不是 JSON,json.loads 會報 Expecting value: line 1 column 1 (char 0)(M8)。看到這個錯誤,先把 text 印出來看看收到了甚麼,而不是懷疑自己的 JSON 程式。

資料的使用條款

天文台的開放資料經 DATA.GOV.HK 提供,可以免費使用和轉載,包括商業用途,但必須清楚註明資料來源。本課的報告和離線副本都寫明:資料來源:香港天文台(經 DATA.GOV.HK)。你的報告也要寫。

網絡不通怎麼辦? 課堂上的網絡不一定可靠,API 也可能改版。所以專案先試 API,失敗就改用課程提供的離線副本(2026 年 10 月 4 日下載的同一批資料)。兩條路得到的資料格式完全相同,後面的程式不用改。離線副本只有 2025 年;年份改了又連不上網,程式會清楚地說「離線副本只有 2025 年的資料」,而不是報一個看不懂的錯誤。

3先寫設定 Configuration first

報告用哪個氣象站、哪一年、甚麼溫度算「酷熱」,不要寫死在程式裏,而是放在一份設定檔:

# config.yaml:天氣報告的設定
station: "HKO"            # 天文台總部。加引號:氣象站代碼不是用來計算的(M9)
year: 2025
very_hot_day: 33.0        # 日最高氣溫 ≥ 33.0 °C:酷熱天氣日(天文台的定義)
hot_night: 28.0           # 日最低氣溫 ≥ 28.0 °C:熱夜
output_dir: "report"
offline: false            # true:不連網,直接用離線副本(課堂網絡不穩時用)

讀入之後,先用 JSON Schema(M10)檢查一次,再開始做事。改設定檔是最常出錯的地方(M12),一開始就擋下,比跑到一半才出錯好。

4讀進來、整理 Loading and cleaning

天文台的每日氣溫是這樣的 JSON:

{
  "type": ["\ufeff日最高氣溫(攝氏度) - 天文台", "Daily Maximum Temperature (°C) at the Hong Kong Observatory"],
  "fields": ["年/Year", "月/Month", "日/Day", "數值/Value", "數據完整性/data Completeness"],
  "data": [["2025", "1", "1", "19.1", "C"], ["2025", "1", "2", "22.0", "C"], …]
}

這段真實的資料,用得上好幾個單元的知識:

df = pd.DataFrame(raw["data"], columns=["year", "month", "day", "max_temp", "complete"])
df = df[df["complete"] == "C"].copy()     # .copy():明確要一張獨立的新表(M4)
df["max_temp"] = df["max_temp"].astype(float)
df["date"] = pd.to_datetime(df[["year", "month", "day"]].astype(int))

最高氣溫和最低氣溫是兩份 JSON,各整理成一張表後,按日期 merge(M11)成一張。

5分析 Analysis

very_hot = (df["max_temp"] >= cfg["very_hot_day"]).sum()     # 酷熱天氣日數
hot_nights = (df["min_temp"] >= cfg["hot_night"]).sum()      # 熱夜數目
hottest = df.loc[df["max_temp"].idxmax()]                     # 最熱的一天

idxmax() 找出最大值所在的那一行的索引,.loc 再把整行取出。按月統計則是 M11 的 groupby:

圖 2 2025 年天文台總部每月平均最高和最低氣溫,以及每月的酷熱天氣日數。資料來源:香港天文台(經 DATA.GOV.HK)。
圖 2 2025 年天文台總部每月平均最高和最低氣溫,以及每月的酷熱天氣日數。資料來源:香港天文台(經 DATA.GOV.HK)。
對照官方數字

自己算出來的結果,最好找一個可靠的來源對照。天文台的《2025年天氣概況》公布:2025 年酷熱天氣日數 53 天、熱夜 54 天,全年最低氣溫是 1 月 27 日的 10.6 度。用上面的程式算出來,三個數字完全相同。對得上,才可以放心寫進報告;對不上,就要回頭找原因(定義不同?漏了資料?)。

6九天預報 The nine-day forecast

預報的 JSON 是巢狀的:每一天是一個 dict,氣溫又是一個 {"value": 31, "unit": "C"} 的 dict。M11 的 json_normalize 一行攤平:

fc = pd.json_normalize(forecast["weatherForecast"])
fc["date"] = pd.to_datetime(fc["forecastDate"], format="%Y%m%d")   # "20261004" → 日期
fc[["date", "week", "forecastMintemp.value", "forecastMaxtemp.value", "forecastWeather"]]

巢狀的 forecastMaxtemp 變成了 forecastMaxtemp.value 和 forecastMaxtemp.unit 兩欄。日期是 "20261004" 這種沒有分隔符的字串,用 format="%Y%m%d" 告訴 pandas 怎樣讀。

7輸出與重現 Output and reproducibility

報告寫到 report 資料夾(M7):

檔案 內容 格式要點
summary.json 全年摘要:酷熱天氣日數、熱夜、最熱最冷的一天 ensure_ascii=False、indent=2(M8),寫出前用 schema 檢查(M10)
monthly.csv 每月平均氣溫和酷熱日數 utf-8-sig,Excel 開得到(M10)。CSV 沒有地方寫註解,交給別人時在附帶的說明裏註明資料來源
monthly.png、forecast.png 兩張圖 中文字型(M11)
config-used.yaml 這次報告用的設定 safe_dump(allow_unicode=True)(M9)

最後一步最重要:選 Kernel → Restart Kernel and Run All Cells(M2),確認由頭到尾一次跑通、結果不變。這份報告連同 pyproject.toml、uv.lock(M1)交給任何人,他們 uv sync 之後重新執行,就得到同樣的結果。再把它放進 Git(git-course),就有了完整的歷史。

8回顧:每個單元在哪裏 Recap

圖 3 M0 那段資料的旅程,現在每一段都有了名字。每個標籤是教那一段的單元。
圖 3 M0 那段資料的旅程,現在每一段都有了名字。每個標籤是教那一段的單元。
單元 在這個專案裏
M0 資料的一生 整個專案就是那幅旅程圖
M1 環境 uv 專案、pyproject.toml 和 uv.lock,讓別人能重現
M2 JupyterLab 報告是一個 notebook;最後重新啟動並全部執行
M3 值與型別 氣溫是字串,要轉成 float
M4 容器 API 回來的 dict 和 list,沿着路徑取值
M5 流程控制 if not offline:設定要離線,就不試 API
M6 函式與模組 load_daily()、fetch_json() 等函式
M7 檔案與路徑 report 資料夾、離線副本、encoding="utf-8"
M8 JSON API 的回應、summary.json;try/except:試 API,失敗就用離線副本
M9 YAML config.yaml,氣象站代碼加引號
M10 選擇格式 設定檔的 schema、monthly.csv 用 utf-8-sig
M11 表格資料 json_normalize、merge、groupby、畫圖
M12 設定檔的世界 先檢查設定,再開始做事

9動手:你的天氣報告 Hands-on: your weather report

  1. 下載 m13-weather-report.ipynb、config.yaml 和 m13-hko-snapshot.zip(離線副本,不用解壓),放進 python-course 資料夾;
  2. 確認已經 uv add pandas matplotlib jsonschema;
  3. 執行 uv run jupyter lab,打開 notebook,逐格執行,最後重新啟動並全部執行一次。

完成基本報告之後,試試改 config.yaml:換一個年份(2024?),或者把酷熱的門檻改成 35 °C(天文台稱為「極端酷熱」),重新執行,看報告怎樣變。改年份會令程式去 API 拿新的資料;離線副本只有 2025 年。

工程線(選做)

m13-script-eng.ipynb 和 report.py:把 notebook 的流程變成一個可以在終端機執行的腳本(uv run python report.py)。它用 Path(__file__).parent(M7)找到自己的設定檔和離線副本,可以每天由排程自動執行。它的圖存成 monthly-script.png,不會蓋掉 notebook 畫的 monthly.png。

完成

這是本課最後一個單元。你已經走完了資料的整段旅程:從一個 JSON 檔開始,到一份可以重現、可以交給別人的報告。之後遇到新的格式、新的工具,問自己三個問題:它在檔案裏長甚麼樣?讀進 Python 變成甚麼?要寫出去給誰?