首頁/模組/模組四
04 死亡螺旋 · Death Spiral

北極海冰
學生親手畫的崩潰

整個課程的情意高潮。從模組一的浪漫入口(櫻花)走到模組四的殘酷現實(北極崩潰)。 學生不只看圖,要親手把氣候變遷畫出來——當一個人類文科生第一次看到自己敲的程式吐出那個螺旋時,那份震撼是任何教師演示都無法取代的。

課程時長
100–120 分鐘
技術重點
極坐標 · LineCollection · OLS 外推
資料源
NSIDC Sea Ice Index v4(月更新)
配套程式
module4_death_spiral.py

真實資料:自 1979 起每月由衛星量到的死亡螺旋

NSIDC Sea Ice Index v4.0 · 即時下載計算
北極海冰死亡螺旋圖:自 1979 年至今的月均海冰範圍極坐標視覺化
圖四|死亡螺旋
極坐標視覺化:每一圈代表一年,半徑為當月海冰範圍(百萬 km²)。 從深紫(1979)到亮黃(最新)的色階呈現時間遞進——螺旋整體向圓心退縮,即為「死亡螺旋」。
2066
以九月海冰最小值的線性外推年份。 1979 年的 7.05 M km² → 2025 年的 4.75 M km²,已下降 32.6%。 誠實提醒:此線性外推太樂觀,IPCC AR6 推估首次「無冰九月」可能在 2050 年前出現。
觀測年份
1979–2025
47 個九月觀測值
起始 vs 最新
7.05 → 4.75M km²
減少約 230 萬 km²(約 6 個台灣)
每十年下降
−0.76M km²
線性趨勢;R² = 0.802(強)
總下降率
−32.6%
1979 至 2025 年九月最小值

一、本模組的教學定位

面向本模組的獨特性
資料識讀能力域使用資料(Using data)——前三模組多為「理解資料」,本模組強調動手操作
核心方法極坐標視覺化 + OLS 外推
與前幾模組的差異學生不只看圖,要敲程式碼、改參數、除錯,親手「製造」那張死亡螺旋
情意設計整個課程的 climax——先讓學生沉浸於視覺衝擊,再以量化迴歸戳破「還很久才會無冰」的安慰
教學核心理念:如果前三模組是讓學生「看見」氣候變遷,第四模組要讓他們親手把氣候變遷「畫出來」。 當一個人類文科生第一次看到自己敲的程式吐出那個螺旋時,那份震撼是任何教師演示都無法取代的。 這正呼應 Ridsdale 等人(2015)所強調的「數據操作已成為各行各業的日常流程」。

二、課堂前置準備

2.1 環境

與前三模組相同。Colab 首次執行:

!apt-get install -y fonts-noto-cjk -q

2.2 資料特性說明

  • 資料源:NSIDC Sea Ice Index v4(北半球月均海冰範圍)
  • 時間起點:1979 年 1 月(1978 年 11 月起有資料,但 1979 年起才是完整年份)
  • 更新頻率:月更新。學生每次執行程式都會看到最新資料
  • 下載量:12 個小 CSV 檔,每檔約 2 KB,合計不到 30 KB。速度很快

2.3 背景小知識(教師備課必讀)

  • 海冰範圍(extent)vs. 面積(area):前者包含冰濃度 ≥15% 的所有格點;後者只計入實際冰覆蓋。本模組用 extent,因為歷史序列較一致(area 因衛星盲區變動而有不連續性)。
  • 為什麼九月最重要? 九月是夏末,海冰年度最小值;最能代表「夏季極端狀態」。科學家討論「無冰北極」時,幾乎都指「九月極小值 < 1 百萬 km²」。
  • 1979 年不是「氣候史的起點」,而是衛星可靠觀測的起點。之前的海冰資料來自船舶紀錄、冰河地質、文獻等,較不均質。

三、課堂節奏建議(100 分鐘版)

時段內容教學重點對應程式
0–10 分情境破冰播放 NSIDC 網站的動態地圖,讓學生看今日的即時海冰狀態
10–20 分複習前模組連回模組一(櫻花)、模組二(葡萄):「我們看過生物尺度與文化尺度的氣候信號,今天要看地球物理尺度的」
20–40 分動手時間 A學生親自執行 load_nsidc_monthly()。觀察「下載 01 02 03...」進度。這個小儀式能讓學生感覺自己是「從衛星下載資料的人」第 1 節
40–55 分從直角到極坐標先看 fig4a(直角版):問「這張圖在告訴你什麼?」再看 fig4b(螺旋版):問「同一份資料,換個坐標系,故事怎麼變了?」第 3、5 節
55–75 分動手時間 B讓學生改變參數:改 cmap_name"plasma""magma""RdYlBu_r",看圖的感覺有什麼差別。「資料敘事的情感引擎」最佳教學時刻第 5 節
75–90 分量化證據時刻解讀 fig4c(九月趨勢圖)與「線性外推無冰北極年份」。誠實說明:這個數字太樂觀了,IPCC AR6 的推估更早第 6 節
90–100 分收束與情意處理讓學生寫下一句話:「我今天畫的這張螺旋圖代表什麼?」——情意評量的關鍵時刻

四、關鍵技術概念之淺白解釋

4.1 什麼是「極坐標」?

  • 直覺:想像一個圓形時鐘。時針指的「方向」=極坐標中的角度(theta);時針的「長度」=極坐標中的半徑(r)。
  • 與直角坐標的差異:直角坐標是「平面格子」(左右 × 上下),極坐標是「從原點出發的角度 × 距離」。
  • 為什麼適合季節性資料:因為一年本身就是「循環」——一月接著十二月再回到一月。圓形(極坐標)比直線(直角)更忠實地呈現這個循環結構。

4.2 什麼是 LineCollection?為什麼不用 plot()

  • 單一 plot() 只能用一種顏色畫一條線。
  • LineCollection 可以讓每一段線段有自己的顏色。
  • 死亡螺旋需要「每段線依年份上色」,所以必須用 LineCollection。這是 matplotlib 較進階的用法,教師可以藉機介紹「工具的極限會推動寫更好的程式」。

4.3 set_theta_zero_location("N")set_theta_direction(-1) 是什麼?

  • 預設:matplotlib polar 圖,0° 在右方(3 點鐘),逆時針方向為正。
  • "N":把 0° 移到上方(12 點鐘),符合我們對日曆的直覺——一月在上方。
  • -1:改為順時針,也符合日曆走向(一月 → 二月 → 三月...)。

4.4 為什麼要用 viridis 色盤?

  • 色盲友善:viridis 在色盲模擬器中仍可清楚辨識變化。
  • 亮度遞增單調:深紫 → 亮黃是單向的感知梯度,適合表示「時間進展」。
  • 常見替代:magma、plasma、cividis 皆同家族。教學上可請學生試試 "jet"(傳統彩虹色),再解釋為什麼這個色盤現在被認為「誤導性」。

五、常見學生提問與建議回應

老師,我跑程式時跳出紅字,是不是做錯了?
這是本模組最常遇到的教學時刻。99% 的「紅字」來自兩個原因:
  1. 沒安裝中文字型(圖上中文變方框)→ 跑 !apt-get install -y fonts-noto-cjk -q,然後 Runtime → Restart
  2. NSIDC 網路暫時中斷 → 等一分鐘再試
教師應該講的話:「紅字不是失敗,紅字是對話。真正做資料工作的人,每天都在看紅字——然後修正。你看到的每一個 Traceback,都是 Python 在幫你定位問題。」
為什麼這張螺旋圖讓我感覺想哭?
這是非常好的情意反應,不要壓抑。你感受到的是 Cunsolo & Ellis(2018)所稱的「生態哀愁」(ecological grief)——當一個超越個人經驗的集體喪失被壓縮到一張圖時,人類的同理心會被觸動。這種情緒是合理的、是可以分享的。Ojala(2012)的賦能式教育建議:從哀愁走向行動的第一步,是承認哀愁本身的合法性。
為什麼不是「已經無冰」了?新聞都在說北極很危險。
新聞討論的是「第一次九月無冰事件」何時出現,這是「一個月、一次」的極端事件,不等於「永久無冰」。在海冰的動力學中,「第一次出現」會比「長期平均 < 1 M km²」早很多年。IPCC AR6 的中位數推估是「2050 年前會出現至少一次」。
我把 cmap 改成 "plasma" 之後,螺旋看起來更「燒焦」了。這樣會不會過度渲染?
這是 Van Audenhove(2021)所稱「批判資料素養」的核心問題之一。每一次我們做視覺化選擇,都是在做「如何說這個故事」的決定。忠實不等於冷淡——但過度戲劇化也會損害可信度。請思考:如果你是氣候學家要給政策官員看,你會用哪個色盤?如果是給小學生?如果是給否認者?
台灣有沒有類似的長期地球物理資料?
台灣的幾個相關長期序列:
  • 玉山高山站氣溫紀錄(自 1943 年)
  • 台灣周邊海溫(中研院海洋研究所)
  • 台灣周邊海平面高度(基隆、高雄潮位站超過 80 年)
  • 高山積雪日數(玉山、合歡山,需自行從觀測報告彙整)

六、評量建議

6.1 形成性評量(課中)

請每位學生在執行完程式後,拍下自己螢幕上的螺旋圖並交給教師。這個動作有強烈的儀式感:「這張圖,是我畫的。」

6.2 總結性評量(課後)

三擇一繳交 800–1,000 字短文:

  1. 技術反思題:描述一次你在執行本模組程式時遇到的錯誤,以及你如何解決的過程。不必是大錯——可能是「中文變方框」、「資料下載到一半中斷」、「色盤改錯名字」。重點是你從錯誤中學到的工作習慣。
  2. 視覺化批判題:用至少三種不同的 cmap_name 重跑本模組程式,選出你認為「最誠實」的一個,並說明為什麼。接著選出你認為「最能打動非科學背景讀者」的一個,說明為什麼兩者可能不同。
  3. 跨模組整合題:把模組一(櫻花剪刀)、模組二(葡萄斷層)、模組四(北極螺旋)三張圖並列比較。它們呈現的統計結構不同(漸進 / 斷層 / 螺旋退縮),但所說的故事相同。請寫一篇 10 歲小朋友看得懂的短文,用這三張圖解釋氣候變遷。

七、進階挑戰(供有餘裕的學生)

  1. 動畫版:修改程式碼,讓死亡螺旋一年一年「長出來」,做成 GIF 動畫。提示:使用 matplotlib.animation.FuncAnimation
  2. 南半球對比:把資料源改為 south/monthly/data/S_{mm}_extent_v4.0.csv,畫南極海冰的螺旋圖。它的故事與北極完全不同——南極在 2014 年以前甚至有「增加」的趨勢,2016 年後才急轉直下。這是氣候系統複雜性的極佳教材。
  3. 疊加氣溫:把全球氣溫異常值(模組二已使用的 GISTEMP)疊加在螺旋的背景上,做成「氣溫 × 海冰」的雙變量極坐標圖。

八、延伸閱讀

  • Hawkins, E. (2016). Spiralling global temperatures. Climate Lab Book.
  • Hawkins, E., with K. Pluck (2017, Feb 11). Animating global sea ice changes [guest contribution]. Climate Lab Book. https://www.climate-lab-book.ac.uk/2017/animating-global-sea-ice-changes/
  • Stroeve, J., & Notz, D. (2018). Changing state of Arctic sea ice across all seasons. Environmental Research Letters, 13(10), 103001.
  • IPCC AR6 WG1 (2021). Chapter 9: Ocean, Cryosphere and Sea Level Change.
  • Notz, D., & SIMIP Community (2020). Arctic sea ice in CMIP6. Geophysical Research Letters, 47(10), e2019GL086749.
教學者自我提醒:這個模組的終極目的,不是教會學生寫極坐標程式, 而是讓他們第一次擁有「自己把氣候變遷畫出來」的身體經驗。 當這個經驗建立後,他們一生中再看到任何氣候圖,都會下意識想到: 「這是有人做出來的,那我也可以做。」這就是資料民主化的最小單位。

配套程式碼

CC BY 4.0 · Python 3.10+ · matplotlib polar projection
關鍵片段:極坐標死亡螺旋的核心繪圖邏輯 module4_death_spiral.py · 第 171–220 行
def plot_death_spiral(df: pd.DataFrame,
                      savepath: str = "fig4b_death_spiral.png",
                      cmap_name: str = "viridis") -> None:
    """
    核心視覺化:
      - 極坐標(polar)
      - 1 月朝上;順時針(模擬日曆走法)
      - 線段顏色依「所屬年份」漸變(viridis:深藍 → 亮黃)
      - 徑向軸刻度表示海冰面積
    """
    df = prepare_polar_coordinates(df).sort_values(["year", "mo"])

    # 色彩映射:1979(深藍)→ 最新年(亮黃)
    year_min, year_max = df["year"].min(), df["year"].max()
    norm = Normalize(vmin=year_min, vmax=year_max)
    cmap = mpl.colormaps[cmap_name]

    # 建立 polar axes
    fig = plt.figure(figsize=(10, 10))
    ax = fig.add_subplot(111, projection="polar")
    ax.set_theta_zero_location("N")  # 0° 朝上(1 月)
    ax.set_theta_direction(-1)        # 順時針

    # 每段線使用起點年份的色(LineCollection 才能讓每段不同色)
    segments = []
    colors = []
    arr = df[["theta", "r", "year", "mo"]].values
    for i in range(len(arr) - 1):
        t0, r0, y0, m0 = arr[i]
        t1, r1, y1, m1 = arr[i + 1]
        segments.append([(t0, r0), (t1, r1)])
        colors.append(cmap(norm(y0 + (m0 - 1) / 12)))

    lc = LineCollection(segments, colors=colors, linewidths=1.3, alpha=0.85)
    ax.add_collection(lc)

    r_min, r_max = df["r"].min() * 0.9, df["r"].max() * 1.05
    ax.set_rmin(r_min); ax.set_rmax(r_max)
    ax.set_rticks([4, 8, 12, 16])