steveyehtw commited on
Commit
5fbb440
·
1 Parent(s): 584a199

Publish four-week data analysis course and revised student slides

Browse files
Files changed (26) hide show
  1. .gitattributes +1 -0
  2. README.md +2 -0
  3. classroom_data/course.json +5 -1
  4. classroom_data/curriculum/data_analysis_special_course.md +153 -0
  5. classroom_data/curriculum/slides/Lesson19_資料偵探提出好問題.pptx +3 -0
  6. classroom_data/curriculum/slides/Lesson20_把資料整理成證據.pptx +3 -0
  7. classroom_data/curriculum/slides/Lesson21_找出差異圖表教學簡報.pptx +3 -0
  8. classroom_data/curriculum/slides/Lesson22_資料故事發表會.pptx +3 -0
  9. classroom_data/curriculum/slides/assets/README.md +6 -0
  10. classroom_data/curriculum/slides/assets/data-download-page.png +3 -0
  11. classroom_data/curriculum/slides/assets/telearn-background.png +3 -0
  12. classroom_data/curriculum/slides/data.zip +3 -0
  13. classroom_data/curriculum/slides/data/Lesson19_資料偵探提出好問題_學生版修訂.pptx +3 -0
  14. classroom_data/curriculum/slides/data/Lesson20_先檢查再相信數字_學生版修訂.pptx +3 -0
  15. classroom_data/curriculum/slides/data/Lesson21_找差異讓圖表說清楚_學生版修訂.pptx +3 -0
  16. classroom_data/curriculum/slides/data/Lesson22_分享你的資料發現_學生版修訂.pptx +3 -0
  17. classroom_data/curriculum/slides/previews/Lesson19_資料下載預覽.png +3 -0
  18. classroom_data/curriculum/tools/build_slides.mjs +131 -0
  19. classroom_data/curriculum/tools/lesson_content.mjs +128 -0
  20. classroom_data/templates/week19/DATA_SOURCE.md +18 -0
  21. classroom_data/templates/week19/REPORT.md +38 -0
  22. classroom_data/templates/week19/data/students_by_county.csv +243 -0
  23. classroom_data/templates/week19/main.py +87 -0
  24. classroom_static/classroom.css +2 -0
  25. classroom_static/student.html +2 -2
  26. classroom_static/student.js +21 -1
.gitattributes CHANGED
@@ -43,3 +43,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
43
  *.avif filter=lfs diff=lfs merge=lfs -text
44
  *.apk filter=lfs diff=lfs merge=lfs -text
45
  *.JPEG filter=lfs diff=lfs merge=lfs -text
 
 
43
  *.avif filter=lfs diff=lfs merge=lfs -text
44
  *.apk filter=lfs diff=lfs merge=lfs -text
45
  *.JPEG filter=lfs diff=lfs merge=lfs -text
46
+ *.pptx filter=lfs diff=lfs merge=lfs -text
README.md CHANGED
@@ -18,6 +18,8 @@ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-
18
 
19
  內建 2026 Fall Lesson 路線圖。Lesson 2 只匯入一次專案起始套版,Lesson 3~4 共用同一份案件進度;支援多個 Python 檔、ZIP 匯入匯出、即時監看、專案最後正式版與整班發布。
20
 
 
 
21
  課程與名單管理:
22
 
23
  - 教師先在控制台建立班級與學生名單,可用逗號一次加入多人。
 
18
 
19
  內建 2026 Fall Lesson 路線圖。Lesson 2 只匯入一次專案起始套版,Lesson 3~4 共用同一份案件進度;支援多個 Python 檔、ZIP 匯入匯出、即時監看、專案最後正式版與整班發布。
20
 
21
+ Lesson 19~22 延伸為四週數據分析特別課程,使用公開 CSV、問題選單、資料清理、可重現計算和 Console 文字圖表;帶課流程見 `classroom_data/curriculum/data_analysis_special_course.md`。
22
+
23
  課程與名單管理:
24
 
25
  - 教師先在控制台建立班級與學生名單,可用逗號一次加入多人。
classroom_data/course.json CHANGED
@@ -16,5 +16,9 @@
16
  {"id":"week15","lesson":15,"title":"班級數位藝廊","project":"生成藝術玩具箱","output":"系列作品與線上展示","template":false},
17
  {"id":"week16","lesson":16,"title":"Director's Cut 重製計畫","project":"精選作品重製","output":"選題與改進承諾","template":false},
18
  {"id":"week17","lesson":17,"title":"Release Candidate","project":"精選作品重製","output":"通過驗收的上架版","template":false},
19
- {"id":"week18","lesson":18,"title":"Demo Day","project":"精選作品重製","output":"線上作品、展示稿與回顧","template":false}
 
 
 
 
20
  ]}
 
16
  {"id":"week15","lesson":15,"title":"班級數位藝廊","project":"生成藝術玩具箱","output":"系列作品與線上展示","template":false},
17
  {"id":"week16","lesson":16,"title":"Director's Cut 重製計畫","project":"精選作品重製","output":"選題與改進承諾","template":false},
18
  {"id":"week17","lesson":17,"title":"Release Candidate","project":"精選作品重製","output":"通過驗收的上架版","template":false},
19
+ {"id":"week18","lesson":18,"title":"Demo Day","project":"精選作品重製","output":"線上作品、展示稿與回顧","template":false},
20
+ {"id":"week19","lesson":19,"title":"資料分析 I:公開資料與問題","project":"公開資料分析","output":"從公開資料選定自己的問題,能執行查詢並指出答案在哪個欄位。","workspace_id":"week19","template":true,"guide":{"goal":"從公開資料選定自己的問題,能執行查詢並指出答案在哪個欄位。","source_title":"教育部統計處|各級學校縣市別學生人數","source_url":"https://data.gov.tw/dataset/40121","source_note":"四週使用同一份課堂 CSV:104–114 學年度、22 個縣市、242 筆資料、15 個欄位。檔案已附在 week19 專案。","flow":[{"time":"0–20 分","title":"公開資料","body":"看三筆實際學生人數紀錄,確認年份、縣市與人數欄位。"},{"time":"20–40 分","title":"下載與 CSV","body":"到官方頁面看 CSV 下載位置,確認課堂檔案在 data/students_by_county.csv,讀懂標題列。"},{"time":"40–50 分","title":"選一個問題","body":"各自選定縣市、學年度與教育階段,寫下要比較什麼。"},{"time":"50–60 分","title":"休息","body":"保存目前內容,休息 10 分鐘。"},{"time":"60–80 分","title":"讀取並執行","body":"看 load_rows() 的用途,執行 main.py,核對讀入 242 筆資料與範例人數。"},{"time":"80–95 分","title":"修改條件","body":"改查 114 學年度國中人數,核對前 3 名,再換成自己的條件。"},{"time":"95–105 分","title":"AI 與欄位","body":"請 AI 指出題目所需欄位,到 CSV 檢查它是否存在。"},{"time":"105–120 分","title":"保存個人進度","body":"把題目、來源與一個核對過的數字寫進 REPORT.md,存檔並提交。"}],"checklist":["問題寫明年份、縣市或教育階段","資料名稱與官方網址已記錄","main.py 可以執行","至少在 CSV 找到一筆輸出的人數"],"ai_checkpoint":"把自己的題目和 CSV 欄位名稱交給 AI,請它指出所需欄位,再到原始表格確認。","submission":"問題寫明年份、縣市或教育階段、資料名稱與官方網址已記錄、main.py 可以執行、至少在 CSV 找到一筆輸出的人數","ai_note":"每人各自完成。允許使用外部 AI,保留實際使用與核對紀錄;沒有使用就如實註明。","question_bank":[{"group":"起步題","text":"114 學年度,哪些縣市的國小學生人數最多?"},{"group":"起步題","text":"選一個縣市,104 到 114 學年度的國小學生人數是增加還是減少?"},{"group":"起步題","text":"104 到 114 學年度,哪個縣市的國小學生人數變化最多?"},{"group":"起步題","text":"114 學年度,選一個縣市,國小和國中學生人數差多少?"},{"group":"挑戰題","text":"選一個縣市,哪兩個學年度的國中學生人數最多和最少?"},{"group":"挑戰題","text":"114 學年度,哪個縣市的高中普通科學生人數最多?"},{"group":"挑戰題","text":"104 到 114 學年度,哪個縣市的大專校院校本部學生人數變化比例最大?"},{"group":"挑戰題","text":"114 學年度,哪個縣市的幼兒園與國小學生人數差距最大?"}],"question_rules":["先挑一個數值欄,再加上一個學年度、縣市或教育階段作為比較條件。","問「多少、哪個較多、差多少、如何變化」;不要讓這張表替你猜原因或預測未來。","自訂資料使用公開、彙總的 CSV,留下來源、時間範圍、欄位和單位,不匯入個人資料。"]}},
21
+ {"id":"week20","lesson":20,"title":"資料分析 II:資料整理","project":"公開資料分析","output":"讀懂數字轉換與未知值的處理,實際核對三筆資料。","workspace_id":"week19","template":false,"guide":{"goal":"讀懂數字轉換與未知值的處理,實際核對三筆資料。","source_title":"教育部統計處|各級學校縣市別學生人數","source_url":"https://data.gov.tw/dataset/40121","source_note":"四週使用同一份課堂 CSV:104–114 學年度、22 個縣市、242 筆資料、15 個欄位。檔案已附在 week19 專案。","flow":[{"time":"0–25 分","title":"文字與數字","body":"重跑上週程式,練習文字相加、int()、strip() 與 replace()。"},{"time":"25–45 分","title":"0 與未知","body":"比較 0、空字串與破折號,讀懂 parse_count() 的判斷。"},{"time":"45–50 分","title":"��試輸入","body":"每人測試五種輸入,寫下回傳結果。"},{"time":"50–60 分","title":"休息","body":"保存目前內容,休息 10 分鐘。"},{"time":"60–75 分","title":"筆數與重複","body":"核對 242 筆、15 個欄位,分清不同學年度與真正重複的紀錄。"},{"time":"75–95 分","title":"核對三筆","body":"各自到 CSV 找到三筆查詢結果,記下年份、縣市、欄位和人數。"},{"time":"95–105 分","title":"解釋錯誤","body":"請 AI 解釋 int() 的轉換錯誤,用自己的程式測試一個例子。"},{"time":"105–120 分","title":"整理紀錄","body":"完成資料處理與核對紀錄,重新執行並提交進度;負值檢查為選做題。"}],"checklist":["測試五種字串輸入","沒有把未知值直接改成 0","核對三筆結果並記錄條件","保留原始 CSV,程式可重新執行"],"ai_checkpoint":"請 AI 解釋 int() 遇到空字串的錯誤,自己測試一個例子並記下回傳結果。","submission":"測試五種字串輸入、沒有把未知值直接改成 0、核對三筆結果並記錄條件、保留原始 CSV,程式可重新執行","ai_note":"每人各自完成。允許使用外部 AI,保留實際使用與核對紀錄;沒有使用就如實註明。"}},
22
+ {"id":"week21","lesson":21,"title":"資料分析 III:比較與圖表","project":"公開資料分析","output":"用排名、差值或圖表回答自己的問題,說明計算方式。","workspace_id":"week19","template":false,"guide":{"goal":"用排名、差值或圖表回答自己的問題,說明計算方式。","source_title":"教育部統計處|各級學校縣市別學生人數","source_url":"https://data.gov.tw/dataset/40121","source_note":"四週使用同一份課堂 CSV:104–114 學年度、22 個縣市、242 筆資料、15 個欄位。檔案已附在 week19 專案。","flow":[{"time":"0–20 分","title":"人數增減","body":"用兩個原始數字相減,完成新北市示範與臺中市練習。"},{"time":"20–35 分","title":"百分比與排名","body":"說明變化百分比的分母,檢查人數排名;百分比計算可選做。"},{"time":"35–50 分","title":"長條圖","body":"比較表格、圖表和 Console 文字圖,說明標題、單位、來源與從 0 開始的尺度。"},{"time":"50–60 分","title":"休息","body":"保存目前內容,休息 10 分鐘。"},{"time":"60–75 分","title":"程式計算","body":"用 get_count() 取出兩個學年度的人數,再計算差值。"},{"time":"75–90 分","title":"修改自己的條件","body":"把例題的縣市或欄位改成自己的專題條件,核對結果。"},{"time":"90–100 分","title":"解釋數字","body":"分清總數、比例和原因,改寫 AI 加入未證實原因的句子。"},{"time":"100–120 分","title":"個人圖表","body":"完成一張圖或一組數字比較,寫下方法與說明,重新執行並提交。"}],"checklist":["圖表或比較結果回答自己的問題","有原始數字和計算方式","圖表寫明學年度、對象、單位和來源","沒有把人數變化直接寫成原因"],"ai_checkpoint":"檢查 AI 是否把人數變化寫成原因,找出能從 CSV 核對的部分,改掉缺少證據的說法。","submission":"圖表或比較結果回答自己的問題、有原始數字和計算方式、圖表寫明學年度、對象、單位和來源、沒有把人數變化直接寫成原因","ai_note":"每人各自完成。允許使用外部 AI,保留實際使用與核對紀錄;沒有使用就如實註明。"}},
23
+ {"id":"week22","lesson":22,"title":"資料分析 IV:分析報告","project":"公開資料分析","output":"完成個人報告,重新執行專案,親自說明一組數字與資料範圍。","workspace_id":"week19","template":false,"guide":{"goal":"完成個人報告,重新執行專案,親自說明一組數字與資料範圍。","source_title":"教育部統計處|各級學校縣市別學生人數","source_url":"https://data.gov.tw/dataset/40121","source_note":"四週使用同一份課堂 CSV:104–114 學年度、22 個縣市、242 筆資料、15 個欄位。檔案已附在 week19 專案。","flow":[{"time":"0–15 分","title":"完整報告範例","body":"看一份完整短報告,對照自己的 REPORT.md。"},{"time":"15–30 分","title":"報告各段","body":"寫清問題、來源、方法、數字與解釋,練習修正文句。"},{"time":"30–50 分","title":"重新執行","body":"每人重新打開專案,處理路徑、欄位或條件問題,核對報告數字。"},{"time":"50–60 分","title":"休息","body":"保存目前內容,休息 10 分鐘。"},{"time":"60–65 分","title":"AI 使用紀錄","body":"記錄一件實際使用與核對的事;未使用者如實填寫。"},{"time":"65–85 分","title":"個人報告","body":"各自補完來源、方法、結果和資料還不能回答的事。"},{"time":"85–90 分","title":"準備分享","body":"挑一張圖或一組數字,準備個人說明。"},{"time":"90–115 分","title":"個人分享","body":"每人介紹問題、方法與答案,回答一個問題;依人數調整分享長度。"},{"time":"115–120 分","title":"提交專題","body":"存檔,確認報告和程式一致,提交目前版本。"}],"checklist":["REPORT.md 有問題、來源、方法與結果","程式可重跑,數字與報告一致","資料檔案與 DATA_SOURCE.md 都在","能親自說明數字與一段相關程式","AI 紀錄如實填寫;未使用也可以"],"ai_checkpoint":"檢查 AI 潤飾是否加入原文沒有的原因,保留確定的年份、對象和數字。","submission":"REPORT.md 有問題、來源、方法與結果、程式可重跑,數字與報告一致、資料檔案與 DATA_SOURCE.md 都在、能親自說明數字與一段相關程式、AI 紀錄如實填寫;未使用也可以","ai_note":"每人各自完成。允許使用外部 AI,保留實際使用與核對紀錄;沒有使用就如實註明。"}}
24
  ]}
classroom_data/curriculum/data_analysis_special_course.md ADDED
@@ -0,0 +1,153 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 數據分析特別課程|四週帶課說明
2
+
3
+ 這四週接在 Lesson 18 後面,使用 Lesson 19–22。學生每人完成自己的題目、程式與報告,四週都使用同一個 week19 工作區。四週結束後可以回到原來的專題,繼續開發原有功能。
4
+
5
+ ## 學生投影片
6
+
7
+ 每週一份可編輯 PowerPoint。說明、例題、練習與解答分頁,投影時依順序帶課;遇到練習先停下來,學生作答後才翻解答。講者備註包含教學提醒、時間安排與來源。
8
+
9
+ | 週次 | 課題 | 頁數 | 投影片 |
10
+ | --- | --- | ---: | --- |
11
+ | Lesson 19 | 資料分析 I:公開資料與問題 | 24 | [第一週](slides/data/Lesson19_資料偵探提出好問題_學生版修訂.pptx) |
12
+ | Lesson 20 | 資料分析 II:資料整理 | 23 | [第二週](slides/data/Lesson20_先檢查再相信數字_學生版修訂.pptx) |
13
+ | Lesson 21 | 資料分析 III:比較與圖表 | 24 | [第三週](slides/data/Lesson21_找差異讓圖表說清楚_學生版修訂.pptx) |
14
+ | Lesson 22 | 資料分析 IV:分析報告 | 21 | [第四週](slides/data/Lesson22_分享你的資料發現_學生版修訂.pptx) |
15
+
16
+ 教師提供的參考稿用於課程版面及教學節奏,沒有沿用電腦科學基礎課的內容。學生頁不放整堂課的流程表,時間安排在本文件與講者備註。
17
+
18
+ ## 課前準備
19
+
20
+ - 開啟課堂系統的 week19 專案,確認 main.py、REPORT.md、DATA_SOURCE.md 與 data/students_by_county.csv 都在。
21
+ - 專案只使用 Python 內建模組;課堂圖表先使用 Console 文字長條圖。
22
+ - 示範用 CSV 已附在專案,不需要每位學生現場連網下載。
23
+ - 第一週第 5–6 頁會示範官方來源、CSV 下載按鈕、原始檔名與專案位置。
24
+ - 若學生已用舊版起始程式建立工作區,先備份學生的修改,再補上新版 main.py 裡的 get_count()。不要直接覆蓋學生程式。第三週的跨年比較會用到此函式。
25
+
26
+ ## 資料與範例答案
27
+
28
+ 資料提供者為教育部統計處,資料集名稱為「各級學校縣市別學生人數」。
29
+
30
+ - [資料集頁面](https://data.gov.tw/dataset/40121)
31
+ - [原始 CSV](https://stats.moe.gov.tw/files/opendata/edu_B_1_4.csv)
32
+ - 課堂檔案:classroom_data/templates/week19/data/students_by_county.csv
33
+ - 本課版本有 242 筆資料、15 個欄位,涵蓋 104–114 學年度與 22 個縣市。
34
+ - 一列代表一個學年度、一個縣市;同列有不同教育階段的人數。
35
+ - 這份附檔沒有空白格,也沒有重複的「學年度+縣市別」組合。第二週的空白、破折號及重複列都是另做的練習例子,不能說成原始資料的錯誤。
36
+ - 資料平台會更新,課堂範例以附檔版本為準。不要把更新後下載的檔案和舊範例答案混用。
37
+ - 使用資料時保留提供機關與網址,依政府資料開放授權條款第 1 版註明來源。
38
+
39
+ | 範例 | 答案 |
40
+ | --- | --- |
41
+ | 114 學年度國小人數前 3 名 | 新北市 194,670;臺中市 161,332;桃園市 132,964 |
42
+ | 114 學年度國中人數前 3 名 | 新北市 96,668;臺中市 82,767;臺北市 68,231 |
43
+ | 新北市國小人數,114 減 104 | 194,670 − 201,970 = −7,300 人 |
44
+ | 新北市國小人數變化率 | −7,300 ÷ 201,970 × 100% ≈ −3.6% |
45
+ | 臺中市國小人數,114 減 104 | 161,332 − 157,272 = 4,060 人 |
46
+
47
+ ## 題目選擇
48
+
49
+ 全班共用一份示範資料,每位學生自己選問題。基本題完成後可以換縣市、學年度或教育階段;不必每人找不同的資料集。
50
+
51
+ 基本題:
52
+
53
+ 1. 114 學年度,哪個縣市的國小學生最多?
54
+ 2. 選一個縣市,104 和 114 學年度的國小學生相差幾人?
55
+ 3. 114 學年度,選一個縣市,國小和國中學生相差幾人?
56
+ 4. 換一個教育階段,比較同一學年度的人數排名。
57
+
58
+ 選做題:
59
+
60
+ 5. 104 到 114 學年度,哪個縣市的國小學生增加最多?
61
+ 6. 選一個縣市,比較不同學年度的國中人數,找出最多和最少的一年。
62
+ 7. 選一個縣市,計算國小學生人數的變化百分比。
63
+ 8. 取同一縣市的多個學年度,檢查期間的變化,避免只憑端點說每年都增減。
64
+
65
+ 自訂題目要能指出欄位與比較條件。如果題目問原因,可把它保留為後續問題,先完成這份資料能回答的人數比較。學生要換其他資料集時,確認資料是公開 CSV、有來源、欄位與單位清楚,且能在現有專案讀取;教師同意題目後再換,避免四週都花在找檔案。
66
+
67
+ ## 第一週:公開資料與問題(120 分鐘)
68
+
69
+ | 時間 | 投影片 | 帶課安排 |
70
+ | --- | --- | --- |
71
+ | 0–10 分 | 1–2 | 問「哪個縣市的國小學生最多」,請學生說出答案可能從哪裡來。 |
72
+ | 10–20 分 | 3–4 | 用三筆實際紀錄說明學年度、縣市、人數與公開資料。 |
73
+ | 20–40 分 | 5–8 | 示範下載頁、CSV 按鈕、檔名與資料夾,再比較原始文字和表格。 |
74
+ | 40–50 分 | 9–14 | 完成找欄位的例題,各自選一個基本問題。 |
75
+ | 50–60 分 | — | 休息。 |
76
+ | 60–80 分 | 15–19 | 解釋資料能回答的範圍,讀取 CSV,執行起始程式並核對人數。 |
77
+ | 80–95 分 | 20–22 | 修改學年度與教育階段,完成國中人數排名練習。 |
78
+ | 95–105 分 | 23 | 請 AI 指出題目需要的欄位,學生到 CSV 確認。 |
79
+ | 105–120 分 | 24 | 寫下個人題目與來源,保存一個已核對的數字,提交進度。 |
80
+
81
+ 來賓分析師可示範自己遇到陌生表格時怎麼確認欄位,以及怎麼把大問題改成能用手上資料計算的問題。不要一開始就要求學生完整理解整份函式。
82
+
83
+ 較慢的學生完成一次排名查詢即可。較快的學生可換條件或做同年度的教育階段比較。
84
+
85
+ ## 第二週:資料整理(120 分鐘)
86
+
87
+ | 時間 | 投影片 | 帶課安排 |
88
+ | --- | --- | --- |
89
+ | 0–10 分 | 1–2 | 各自重新執行上週程式,說明目前條件。 |
90
+ | 10–25 分 | 3–7 | 比較文字相加與整數相加,示範 int()、strip()、replace() 及轉換錯誤。 |
91
+ | 25–45 分 | 8–11 | 區分 0 與未知,讀懂 parse_count() 的兩部分。 |
92
+ | 45–50 分 | 12 | 學生測試五種輸入。 |
93
+ | 50–60 分 | — | 休息。 |
94
+ | 60–75 分 | 13–18 | 對答案,確認筆數與欄位數,練習辨認重複資料,說明查詢條件。 |
95
+ | 75–95 分 | 19–20 | 老師核對一筆,學生自行核對三筆並記錄。 |
96
+ | 95–105 分 | 21 | 請 AI 解釋一個錯誤,用程式測試它的說法。 |
97
+ | 105–115 分 | 22–23 | 完成整理紀錄;熟練者挑戰負值檢查。 |
98
+ | 115–120 分 | 23 | 重新執行、存檔並提交進度。 |
99
+
100
+ 來賓分析師可用自己的資料工作經驗說明:能轉成數字,不一定代表數值合理;資料沒有某個問題,也不需要為了練習而修改原始檔。
101
+
102
+ 基本目標是測試函式及核對三筆,負值檢查不列入最低標準。
103
+
104
+ ## 第三週:比較與圖表(120 分鐘)
105
+
106
+ | 時間 | 投影片 | 帶課安排 |
107
+ | --- | --- | --- |
108
+ | 0–10 分 | 1–2 | 取回學生各自的問題與已核對數字。 |
109
+ | 10–20 分 | 3–6 | 完成新北市示範與臺中市增減人數練習。 |
110
+ | 20–35 分 | 7–10 | 解釋百分比的分母與人數排名;百分比可只看示範。 |
111
+ | 35–50 分 | 11–14 | 比較表格、長條圖與 Console 文字圖,說明標題、單位與從 0 開始。 |
112
+ | 50–60 分 | — | 休息。 |
113
+ | 60–75 分 | 15–18 | 計算長條長度,示範 get_count() 與跨年相減。 |
114
+ | 75–90 分 | 19 | 用程式重算臺中市的例題,再改成自己的條件。 |
115
+ | 90–100 分 | 20–21 | 區分總數、比例與原因,修改 AI 過度推論的句子。 |
116
+ | 100–115 分 | 22–23 | 各自完成一張圖或一組計算;熟練者查看多個學年度。 |
117
+ | 115–120 分 | 24 | 寫下原始數字、計算方式與說明,提交進度。 |
118
+
119
+ 基本學生沿用現成的排名文字圖也能完成。能做跨年差值的學生使用 get_count();百分比與多年度走勢是選做內容。不要要求每個人同時完成排名、差值、比例和趨勢。
120
+
121
+ ## 第四週:分析報告(120 分鐘)
122
+
123
+ | 時間 | 投影片 | 帶課安排 |
124
+ | --- | --- | --- |
125
+ | 0–15 分 | 1–3 | 打開 REPORT.md,看一份完整短報告的例子。 |
126
+ | 15–30 分 | 4–9 | 拆開問題、來源、方法、結果與解釋,改寫只看端點卻說「一直增加」的句子。 |
127
+ | 30–50 分 | 10–14 | 每人重新執行,處理路徑、欄位或條件問題,核對一個報告數字。 |
128
+ | 50–60 分 | — | 休息。 |
129
+ | 60–65 分 | 15–16 | 檢查 AI 修改是否加入沒有證據的原因,寫實際使用紀錄。 |
130
+ | 65–85 分 | 17 | 各自完成個人報告。 |
131
+ | 85–90 分 | 18 | 準備展示一張圖或一組數字。 |
132
+ | 90–115 分 | 18–19 | 個人分享與提問。 |
133
+ | 115–120 分 | 20 | 確認檔案與結果,正式提交。第 21 頁為後續延伸。 |
134
+
135
+ 8 人以內可每人 2 分鐘分享、1 分鐘提問。超過 8 人,改每人 1 分鐘分享、30 秒提問;若人數超過這段時間能容納的數量,另排展示時段,課內仍逐一抽問數字與程式,不用改成小組報告。
136
+
137
+ ## AI 使用與評量
138
+
139
+ 允許使用外部 AI。課堂要求學生能指出自己的資料、說明一段相關程式,並實際核對數字。AI 使用紀錄寫一件具體發生的事即可,不要求抄整段對話;未使用者如實填寫。
140
+
141
+ 最低完成標準:
142
+
143
+ - 問題能由資料中的欄位回答。
144
+ - main.py 可以執行,結果能重新出現。
145
+ - REPORT.md 有來源、方法、原始數字與答案。
146
+ - 至少核對一組結果;相減時核對兩個原始數字。
147
+ - 學生能親自解釋結果,說出資料尚不能回答的一件事。
148
+
149
+ 評量比重可維持問題與範圍 20%、資料整理 20%、計算與核對 25%、圖表與說明 20%、個人分享與 AI 使用紀錄 15%。沒有使用 AI 不扣分,程式碼長度與圖表華麗程度不作為主要評分。
150
+
151
+ ## 修改教材
152
+
153
+ 學生文字與例題在 tools/lesson_content.mjs。tools/build_slides.mjs 產生可編輯的正文、表格與圖表,背景及下載頁截圖放在 slides/assets。修改後重新輸出,再逐頁檢查;不要只修改 PowerPoint 卻忘記同步範例程式與教師流程。
classroom_data/curriculum/slides/Lesson19_資料偵探提出好問題.pptx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:5c5d8875e55eb4ba22397e6a2c90ad506f7c1e43734e812b2386688fe6752acc
3
+ size 56045
classroom_data/curriculum/slides/Lesson20_把資料整理成證據.pptx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ecf08226553e256c91f6bba7a99e67c255fae57a546e57c75431a964dae03cc2
3
+ size 55876
classroom_data/curriculum/slides/Lesson21_找出差異圖表教學簡報.pptx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:4228baa8c7614666bdfa89ff28354bcd685ce9376742540e61812ad013572216
3
+ size 58187
classroom_data/curriculum/slides/Lesson22_資料故事發表會.pptx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ad01fd550d4a3e209649a3512229a26cb33232d1227669942438cf19ff015749
3
+ size 56564
classroom_data/curriculum/slides/assets/README.md ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ # 投影片素材
2
+
3
+ - `telearn-background.png`:從教師提供的 `Level3_0_1電腦科學基礎I.pdf` 擷取共同背景,保留原本得能標誌、漸層與頁尾圖樣。原頁的教學文字與插圖不列入新課程。
4
+ - `data-download-page.png`:2026-10-17 擷取政府資料開放平臺 <https://data.gov.tw/dataset/40121> 的「資料資源下載網址」區域,供課堂說明 CSV 下載位置。
5
+
6
+ 投影片正文、程式碼、表格與圖表均以可編輯物件重建;參考 PDF 的教學內容沒有沿用。
classroom_data/curriculum/slides/assets/data-download-page.png ADDED

Git LFS Details

  • SHA256: 9bde2159315461ccf1b541b5fb4da3c17beb2ed48a258cb2051e502deb3147a1
  • Pointer size: 131 Bytes
  • Size of remote file: 168 kB
classroom_data/curriculum/slides/assets/telearn-background.png ADDED

Git LFS Details

  • SHA256: c725bbafd859bf2fd18ad94060ec062566fff2a53d05e26b8ef399b705910ce1
  • Pointer size: 130 Bytes
  • Size of remote file: 87.3 kB
classroom_data/curriculum/slides/data.zip ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:4374fef0f6fe55fddfbb07ce9d05f7ebfce8a273bef5351cf8e2a103c21ce3db
3
+ size 7741502
classroom_data/curriculum/slides/data/Lesson19_資料偵探提出好問題_學生版修訂.pptx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:5c33861a50694104dd6cb8152095d7993640178c107c928c1be0d78a97631b32
3
+ size 2158186
classroom_data/curriculum/slides/data/Lesson20_先檢查再相信數字_學生版修訂.pptx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:1dfabee63aa5860723efba25b6177b5b42f8a6be0a03e2f070358eb4b8662b3b
3
+ size 1910079
classroom_data/curriculum/slides/data/Lesson21_找差異讓圖表說清楚_學生版修訂.pptx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:f718dd322d2df1b0824d367968c475b766cea0c90e12b7f485a41e11e54885b6
3
+ size 1986245
classroom_data/curriculum/slides/data/Lesson22_分享你的資料發現_學生版修訂.pptx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:8e10d4345b901d3e95d9de9766caa7139ac5e4b283fd4689bf8c9fe3ba4a5801
3
+ size 1744900
classroom_data/curriculum/slides/previews/Lesson19_資料下載預覽.png ADDED

Git LFS Details

  • SHA256: 7284e9a87c3814f9cd8b355f3ced6de4f3ac9d6811fa2fa21683f12478615a9c
  • Pointer size: 131 Bytes
  • Size of remote file: 317 kB
classroom_data/curriculum/tools/build_slides.mjs ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { fileURLToPath, pathToFileURL } from 'node:url';
4
+ import { decks } from './lesson_content.mjs';
5
+ import { importRuntimeModule } from '/Users/steveyeh/.codex/plugins/cache/openai-primary-runtime/presentations/26.1013.11914/skills/presentations/container_tools/runtime_helpers.mjs';
6
+
7
+ const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../..');
8
+ const build = path.join(root, '.presentation-build');
9
+ const assets = path.join(root, 'classroom_data/curriculum/slides/assets');
10
+ const skill = '/Users/steveyeh/.codex/plugins/cache/openai-primary-runtime/presentations/26.1013.11914/skills/presentations';
11
+ const { Presentation, PresentationFile } = await importRuntimeModule('@oai/artifact-tool');
12
+ const { finalizePresentation, applyPresentationChartFont } = await import(pathToFileURL(path.join(skill, 'container_tools/artifact_tool_utils.mjs')));
13
+ const PYTHON = '/Users/steveyeh/.cache/codex-runtimes/codex-primary-runtime/dependencies/python/bin/python3';
14
+ const FONT = 'PingFang TC';
15
+ const PURPLE = '#351C75';
16
+ const RED = '#E00000';
17
+ const SOURCE = '教育部統計處「各級學校縣市別學生人數」。資料集:https://data.gov.tw/dataset/40121 ;原始 CSV:https://stats.moe.gov.tw/files/opendata/edu_B_1_4.csv 。數字以課堂附檔 data/students_by_county.csv 的 104–114 學年度快照為準。';
18
+ const background = await fs.readFile(path.join(assets, 'telearn-background.png'));
19
+ const downloadImage = await fs.readFile(path.join(assets, 'data-download-page.png'));
20
+ const asArrayBuffer = b => b.buffer.slice(b.byteOffset, b.byteOffset + b.byteLength);
21
+ await fs.mkdir(path.join(build, 'final'), { recursive: true });
22
+
23
+ function text(slide, value, x, y, w, h, size = 32, opts = {}) {
24
+ const shape = slide.shapes.add({geometry:'textbox', position:{left:x,top:y,width:w,height:h}, fill:'none', line:{fill:'none',width:0}});
25
+ shape.text = value.replace(/\{\{|\}\}/g,'');
26
+ shape.text.style = {typeface:opts.mono?'Menlo':FONT, fontSize:size, color:opts.color||'#000000', bold:opts.bold||false, alignment:opts.align||'left', verticalAlignment:'top', insets:0, autoFit:'none'};
27
+ for(const m of value.matchAll(/\{\{(.*?)\}\}/g)) shape.text.get(m[1]).fill = RED;
28
+ return shape;
29
+ }
30
+ function length(value) {return [...value.replace(/\{\{|\}\}/g,'')].reduce((n,c)=>n+(/[\x00-\x7f]/.test(c)?0.54:1),0);}
31
+ function paraHeight(value, width, size) {
32
+ const lines=value.split('\n').reduce((n,l)=>n+Math.max(1,Math.ceil(length(l)/(width/size))),0);
33
+ return Math.ceil(lines*size*1.36+5);
34
+ }
35
+ function paragraphs(slide, values, start=230, size=32, width=1110, gap=24) {
36
+ let y=start;
37
+ for(const value of values) {
38
+ const h=paraHeight(value,width,size);
39
+ text(slide,value,80,y,width,h,size);
40
+ y+=h+gap;
41
+ }
42
+ if(y-gap>635) throw new Error(`Body too long: ${y-gap}px; ${values.join(' / ')}`);
43
+ }
44
+ function addTable(slide, spec) {
45
+ text(slide,spec.intro,80,219,1110,68,27);
46
+ const cols=spec.values[0].length;
47
+ const h=Math.min(304,spec.values.length*56);
48
+ const table=slide.tables.add({rows:spec.values.length,columns:cols,left:100,top:292,width:1060,height:h,values:spec.values,...(spec.widths?{columnWidths:spec.widths}:{columnTracks:Array.from({length:cols},()=>({mode:'fr',value:1}))})});
49
+ table.borders.assign({fill:'#777777',width:1,style:'solid'});
50
+ table.cells.block({row:0,column:0,rowCount:spec.values.length,columnCount:cols}).assign({fill:'#FFFFFF',textStyle:{typeface:FONT,fontSize:27,color:'#000000'},margins:{left:12,right:10,top:7,bottom:6},anchor:'center'});
51
+ for(let r=0;r<spec.values.length;r++) for(let c=0;c<cols;c++) {
52
+ const cell=table.getCell(r,c);
53
+ cell.text.style={typeface:FONT,fontSize:27,color:'#000000',bold:r===0,verticalAlignment:'middle',alignment:'left'};
54
+ if(r===0)cell.fill='#E5E5E5';
55
+ }
56
+ if(spec.after)text(slide,spec.after,80,Math.max(568,292+h+20),1110,66,25);
57
+ }
58
+ function addCode(slide,spec) {
59
+ const introH=paraHeight(spec.intro,1110,28);
60
+ text(slide,spec.intro,80,220,1110,introH,28);
61
+ const top=220+introH+20;
62
+ const lines=spec.code.split('\n').length;
63
+ const size=lines>7?25:28;
64
+ const h=lines*size*1.32+8;
65
+ text(slide,spec.code,100,top,1090,h,size,{mono:true});
66
+ if(spec.after) {
67
+ const afterTop=top+h+20;
68
+ const afterH=paraHeight(spec.after,1110,27);
69
+ if(afterTop+afterH>636)throw new Error(`Code too tall: ${spec.title} ${afterTop+afterH}`);
70
+ text(slide,spec.after,80,afterTop,1110,afterH,27);
71
+ }
72
+ }
73
+ function addDownload(slide) {
74
+ const url=text(slide,'https://data.gov.tw/dataset/40121',80,221,1100,45,31,{color:'#184E98'});
75
+ url.text.get('https://data.gov.tw/dataset/40121').link={uri:'https://data.gov.tw/dataset/40121',isExternal:true};
76
+ slide.images.add({blob:asArrayBuffer(downloadImage),contentType:'image/png',alt:'官方資料頁的資料資源下載區,左側 CSV 按鈕用於下載檔案',position:{left:70,top:290,width:1140,height:225},fit:'contain'});
77
+ text(slide,'找到「資料資源下載網址」,按左側的 CSV ��鈕。\n下載後會得到 edu_B_1_4.csv。旁邊的「檢視資料」是檔案說明。',80,548,1110,85,28);
78
+ }
79
+ function addChart(slide) {
80
+ const chart=slide.charts.add('bar',{
81
+ position:{left:90,top:230,width:1080,height:350},
82
+ title:'114 學年度各縣市國小學生人數(前 5 名,單位:人)',titleTextStyle:{fontSize:27,fill:'#000000'},
83
+ categories:['臺北市','高雄市','桃園市','臺中市','新北市'],
84
+ series:[{name:'學生人數(人)',values:[117650,127327,132964,161332,194670],fill:PURPLE}],
85
+ barOptions:{direction:'bar',grouping:'clustered',gapWidth:70},hasLegend:false,
86
+ xAxis:{textStyle:{fontSize:24,fill:'#000000'}},
87
+ yAxis:{min:0,max:220000,majorUnit:50000,numberFormatCode:'#,##0',textStyle:{fontSize:21,fill:'#000000'},majorGridlines:{fill:'#CCCCCC',width:1}},
88
+ dataLabels:{showValue:true,position:'outEnd',numberFormatCode:'#,##0',textStyle:{fontSize:22,fill:'#000000'}},
89
+ chartFill:'#FFFFFF',plotAreaFill:'#FFFFFF',
90
+ });
91
+ applyPresentationChartFont(chart,{fontFamily:FONT});
92
+ text(slide,'資料來源:教育部統計處「各級學校縣市別學生人數」',80,602,1110,38,23);
93
+ }
94
+
95
+ for(const deck of decks.filter(d => !process.argv[2] || d.lesson === Number(process.argv[2]))) {
96
+ const p=Presentation.create({slideSize:{width:1280,height:720}});
97
+ const tables=[],charts=[];
98
+ for(const [index,spec] of deck.slides.entries()) {
99
+ const slide=p.slides.add();
100
+ slide.images.add({blob:asArrayBuffer(background),contentType:'image/png',alt:'得能資訊教育課程版型',position:{left:0,top:0,width:1280,height:720},fit:'fill'});
101
+ text(slide,'得能資訊教育 - 專題課程',32,665,800,30,21,{bold:true,color:PURPLE});
102
+ text(slide,String(index+1),1168,643,48,28,21,{bold:true,color:PURPLE,align:'center'});
103
+ if(spec.kind==='cover') {
104
+ text(slide,spec.title,100,300,1080,100,66,{bold:true,color:PURPLE,align:'center'});
105
+ text(slide,spec.subtitle,100,418,1080,80,40,{align:'center'});
106
+ } else {
107
+ text(slide,spec.title,50,77,990,72,48,{bold:true,color:PURPLE});
108
+ if(spec.kind==='text')paragraphs(slide,spec.paras);
109
+ if(spec.kind==='exercise') {
110
+ text(slide,spec.label,80,218,1110,45,31);
111
+ paragraphs(slide,spec.paras,278,31,1110,23);
112
+ }
113
+ if(spec.kind==='code')addCode(slide,spec);
114
+ if(spec.kind==='table'){addTable(slide,spec);tables.push(index+1);}
115
+ if(spec.kind==='download')addDownload(slide);
116
+ if(spec.kind==='chart'){addChart(slide);charts.push(index+1);}
117
+ }
118
+ slide.speakerNotes.text=[spec.note||'依投影片說明與例子帶課。練習題先讓學生獨立作答,再顯示解答。',SOURCE].join('\n\n');
119
+ }
120
+ const candidatePath=path.join(build,`candidate-${deck.file}`);
121
+ await(await PresentationFile.exportPptx(p)).save(candidatePath);
122
+ const result=await finalizePresentation({workspaceDir:root,candidatePath,finalPath:path.join(build,'final',deck.file),pythonExecutable:PYTHON,
123
+ integrityValidatorPath:path.join(skill,'container_tools/inspect_presentation_package_integrity.py'),
124
+ layoutValidatorPath:path.join(skill,'container_tools/inspect_presentation_layout_geometry.py'),
125
+ layoutArgs:['--expected-slide-size-emu','12192000,6858000','--validate-bullet-geometry','--validate-heading-fit',...tables.flatMap(n=>['--require-native-table-slide',String(n)])],
126
+ explicitTotalSlideCount:deck.slides.length,requiredNativeTableOwnerSlides:tables,requiredNativeChartOwnerSlides:charts,
127
+ ...(charts.length?{materializeLiteralChartWorkbooks:true}:{}),fontPolicy:{basis:'design',families:[FONT,'Menlo']},verifyArtifactToolImport:true,
128
+ receiptPath:path.join(build,`Lesson${deck.lesson}.validation.json`),
129
+ });
130
+ console.log(JSON.stringify({lesson:deck.lesson,slides:deck.slides.length,file:deck.file,package:result.packageIntegrity.status,layout:result.presentationLayout.finding_count}));
131
+ }
classroom_data/curriculum/tools/lesson_content.mjs ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const text = (title, paras, note = '') => ({ kind: 'text', title, paras, note });
2
+ const code = (title, intro, code, after = '', note = '') => ({ kind: 'code', title, intro, code, after, note });
3
+ const table = (title, intro, values, after = '', note = '', widths) => ({ kind: 'table', title, intro, values, after, note, widths });
4
+ const exercise = (title, label, paras, note = '') => ({ kind: 'exercise', title, label, paras, note });
5
+ const cover = (title, subtitle) => ({ kind: 'cover', title, subtitle });
6
+
7
+ export const decks = [
8
+ {
9
+ lesson: 19,
10
+ title: '資料分析 I|公開資料與問題',
11
+ file: 'Lesson19_資料偵探提出好問題_學生版修訂.pptx',
12
+ slides: [
13
+ cover('資料分析 I', '公開資料與問題'),
14
+ exercise('學生人數', '[問題]', ['哪個縣市的國小學生最多?', '你會去哪裡找答案?'], '0–10 分。讓學生各自回答。暫時不公布排名,請他們說出答案的來源。'),
15
+ table('學生人數', '這是同一份資料裡的三筆紀錄。', [['學年度','縣市別','國小[人]'],['104','新北市','201970'],['114','新北市','194670'],['114','臺中市','161332']], '同一個縣市,可以比較不同學年度;同一個學年度,可以比較不同縣市。', '用這張表說明後面要做的兩種比較。原始 CSV 的數字沒有千分位逗號。'),
16
+ text('公開資料', ['政府機關把整理好的資料公開,提供大家下載使用,稱為{{公開資料}}。', '這次使用教育部統計處的「各級學校縣市別學生人數」。\n裡面有 104 到 114 學年度、22 個縣市的學生人數。'], '10–20 分。說明提供資料的機關與資料平台不同:教育部統計處整理資料,政府資料開放平臺提供查找入口。'),
17
+ { kind:'download', title:'資料下載', note:'20–30 分。開啟網址,現場指出提供機關、CSV 按鈕和檢視資料的差別。CSV 按鈕下載檔案;檢視資料開啟資源說明視窗,不是試算表預覽。課堂專案已附同一份資料,下載只需示範一次。' },
18
+ code('資料下載', '下載的檔名是 edu_B_1_4.csv。課堂專案把它改名為 students_by_county.csv,放在 data 資料夾。', 'week19/\n├── main.py\n├── REPORT.md\n├── DATA_SOURCE.md\n└── data/\n └── students_by_county.csv', '專案裡已經有這些檔案,可以直接使用。', '說明檔案位置。這裡的 week19 是四週共用的工作區;資料夾名稱以課堂系統顯示為準。'),
19
+ text('CSV', ['CSV 是儲存表格資料的一種文字格式。', '{{每一行}}是一筆紀錄,{{逗號}}用來分開不同欄位。\n用試算表開啟時,就會排成表格。', '第一行通常是欄位名稱。這份檔案共有 15 個欄位,\n例如「學年度」、「縣市別」和「國小[人]」。'], '30–40 分。可開啟原始 CSV,再切到試算表或系統文字檢視,讓學生比較同一檔案的兩種樣子。'),
20
+ code('CSV', '下面只取原始檔的三個欄位。', '學年度,縣市別,國小[人]\n104,新北市,201970\n114,新北市,194670\n114,臺中市,161332', '在這個例子裡,每一列代表「某個學年度、某個縣市」的國小學生人數。'),
21
+ exercise('資料欄位', '[例1]', ['114 學年度,新北市有多少位國小學生?', '請指出要用的欄位,以及符合條件的那一列。'], '給 2 分鐘。讓學生先口頭或寫下答案,再切到下一頁。'),
22
+ table('資料欄位', '[例1 解答]', [['學年度','縣市別','國小[人]'],['114','新北市','194670']], '學年度是 114、縣市別是新北市。讀取「國小[人]」這一欄,答案是 194,670 人。', '這裡強調找列與取欄是不同動作。'),
23
+ text('學年度', ['這份資料的學年度寫成 104、105,一直到 114。', '程式要查 114 學年度,就填{{114}}。如果填 2025,會找不到符合條件的資料。', '「國小[人]」的 [人] 是單位,不是另一個欄位。程式裡的欄位名稱要和 CSV 完全一樣。']),
24
+ text('分析問題', ['「學生是不是越來越少?」還不夠清楚:哪個地方?哪一種學校?比較哪幾年?', '把問題寫成:{{新北市的國小學生人數,104 到 114 學年度增加還是減少?}}', '這樣就知道要選哪兩筆資料,也知道要做什麼計算。'], '40–50 分。從原問題逐項補出地區、教育階段和年份,不把這些詞當成需要背的術語。'),
25
+ exercise('分析問題', '[練習1]', ['從下面兩題選一題,換成你想看的縣市或教育階段。', '1. 114 學年度,哪個縣市的國小學生最多?', '2. 新北市的國小學生,104 和 114 學年度相差幾人?'], '每個學生各自選題。這兩題是基本題,先讓所有人有可執行的起點。'),
26
+ text('分析問題', ['也可以比較同一個學年度的不同教育階段。', '[例] 114 學年度,新北市的國小和國中學生相差幾人?', '想進一步比較各縣市的變化,可以挑戰:104 到 114 學年度,哪個縣市的國小學生增加最多?'], '最後一題需要對多個縣市重複計算,是選做題。50–60 分休息。'),
27
+ text('資料能回答的範圍', ['這份資料有年份、縣市和學生人數,可以回答人數多少、差多少、增加或減少。', '「學生人數為什麼減少?」需要其他資料。這張表沒有出生人數、人口遷移或每間學校的資料。'], '60–65 分。這是分析範圍的解釋,不要求學生為每一題寫一大段限制。'),
28
+ code('Python 讀取 CSV', 'main.py 裡的 load_rows() 負責讀取檔案。', 'def load_rows(path=DATA_FILE):\n with open(path, encoding="utf-8-sig", newline="") as file:\n return list(csv.DictReader(file))', 'DictReader 會把欄位名稱當作 key,每一列變成一個 dictionary。', '65–75 分。只講這三行的用途。utf-8-sig 用於讀取附帶 BOM 的 UTF-8 檔案;不必在這週展開編碼課程。'),
29
+ code('Python 讀取 CSV', '例如 rows[0] 是讀進來的第一筆資料。以下只顯示其中三個欄位。', '{\n "學年度": "104",\n "縣市別": "新北市",\n "國小[人]": "201970"\n}', '用 rows[0]["縣市別"] 可以取得「新北市」。數字目前也先以文字讀入,下一週會處理。'),
30
+ code('執行範例', '打開 main.py。檔案上方這兩個設定,決定要查哪個學年度、哪種學生人數。', 'SAMPLE_YEAR = "114"\nSAMPLE_FIELD = "國小[人]"', '按「執行」,到 Console 查看結果。', '75–80 分。先確認每位學生能執行。如果找不到檔案,核對 data/students_by_county.csv 的位置。'),
31
+ table('執行範例', 'Console 會顯示「讀入 242 列資料」,並列出各縣市的排名。前五名如下。', [['縣市','114 學年度國小學生人數(人)'],['新北市','194,670'],['臺中市','161,332'],['桃園市','132,964'],['高雄市','127,327'],['臺北市','117,650']], '回到 CSV 找新北市 114 學年度的那一列,確認人數相同。', '若網站未來更新資料,課堂仍以附檔快照為準;不要用即時下載的新版本和舊範例混算。', [360,700]),
32
+ code('修改查詢條件', '把 SAMPLE_YEAR 改成 "104",其他設定維持原樣。', 'SAMPLE_YEAR = "104"\nSAMPLE_FIELD = "國小[人]"', '執行後,新北市是 201,970 人。這次查到的是 104 學年度,不是程式算錯。'),
33
+ exercise('修改查詢條件', '[練習2]', ['改查 114 學年度的國中學生人數,寫下前 3 名。', '你需要修改哪兩個設定?', '到 CSV 核對其中一個縣市的人數。'], '80–90 分。基本題做完才換成其他教育階段。下一頁是答案。'),
34
+ table('修改查詢條件', '[練習2 解答] SAMPLE_YEAR = "114",SAMPLE_FIELD = "國中[人]"', [['排名','縣市','國中學生人數(人)'],['1','新北市','96,668'],['2','臺中市','82,767'],['3','臺北市','68,231']], '國小和國中的前 3 名並不完全相同。問題換了,要使用的欄位也會改變。'),
35
+ exercise('AI 與分析問題', '[練習3]', ['把你寫的問題和這份 CSV 的欄位名稱交給 AI,請它指出需要哪幾欄。', '如果它提到資料裡沒有的欄位,回去修改問題,或請它說明還缺哪份資料。', '在 REPORT.md 寫下最後選定的問題。'], '90–100 分。不要只問 AI「幫我做專題」。教師抽問學生需要哪個欄位,確認不是照抄不理解的題目。'),
36
+ text('個人專題', ['把你的問題、資料名稱、來源網址和比較的年份寫進 REPORT.md。', '執行 main.py,記下一個和問題有關的數字,並在 CSV 裡找到它。', '完成後存檔,提交這週的進度。下週沿用同一份專案。'], '100–120 分。教師逐一看問題、執行結果與來源。進度較快者改比較另一個縣市或教育階段;不用新增另一個專題。'),
37
+ ],
38
+ },
39
+ {
40
+ lesson:20,
41
+ title:'資料分析 II|資料整理',
42
+ file:'Lesson20_先檢查再相信數字_學生版修訂.pptx',
43
+ slides:[
44
+ cover('資料分析 II','資料整理'),
45
+ exercise('上週的程式','[複習]',['上週選了什麼問題?程式目前查的是哪個學年度、哪個欄位?','打開同一份專案,執行 main.py。'], '0–10 分。教師從學生自己的設定開始,不換新的資料集。'),
46
+ code('文字與數字','CSV 讀進來的內容通常是文字。例如:','value = "194670"\nprint(type(value))\n\n# <class \'str\'>','畫面上看起來像數字,程式仍然把它當成文字。'),
47
+ exercise('文字與數字','[例1]',['下面兩行的結果一樣嗎?','print("12" + "3")','print(12 + 3)'], '10–15 分。讓學生先寫結果,再執行或看解答。'),
48
+ code('文字與數字','[例1 解答]','print("12" + "3") # 123\nprint(12 + 3) # 15\nprint(int("12") + int("3")) # 15','文字相加會接在一起;int() 可以把數字文字轉成整數。'),
49
+ code('字串整理','下面是一個帶空格和千分位逗號的練習字串。','value = " 201,970 "\ncleaned = value.strip().replace(",", "")\nprint(cleaned)\nprint(int(cleaned))','strip() 去掉前後空白,replace(",", "") 把逗號換成空字串。', '15–25 分。附檔原始數字沒有逗號;這裡是自訂練習,不暗示原始檔有格式錯誤。'),
50
+ code('字串整理','如果欄位裡沒有填數字,int() 會發生什麼事?','int("")\n\n# ValueError: invalid literal for int() ...','空字串不能直接轉成整數。程式要先決定這種情況怎麼處理。'),
51
+ table('0 與缺少資料','下面是練習資料,不代表原始檔裡一定有這些問題。',[['欄位內容','可能的意思','本次程式的處理'],['0','人數確實為 0','保留數字 0'],['空白','沒有填寫數字','以 None 表示未知'],['-','需要查看原資料的註記','暫時記為 None']], '未知和 0 的意思不同。不能因為程式比較好寫,就把未知都改成 0。','25–35 分。真實附檔中宗教研修學院欄位有 0,這些 0 不需要刪除。',[170,430,460]),
52
+ text('None',['Python 用 {{None}} 表示沒有取得數值。', '例如一個縣市的資料沒有填,不能說這個縣市「沒有學生」。這時保留 None,後面的計算可以略過它,並記下缺少哪一筆。', '這份課堂 CSV 有 242 列、15 個欄位,沒有空白格。下面的空白和「-」是用來測試程式的例子。']),
53
+ code('parse_count()','main.py 裡已附這個函式。前半段整理字串,並檢查空白或「-」。','def parse_count(value):\n cleaned = (value or "").strip().replace(",", "")\n if not cleaned or cleaned == "-":\n return None','如果是空白或「-」,先回傳 None,不繼續轉成整數。', '35–45 分。逐行對照上一頁測試值,不要求學生默寫。'),
54
+ code('parse_count()','後半段嘗試轉成整數。無法轉換時,回傳 None。',' try:\n return int(cleaned)\n except ValueError:\n return None','try / except 讓程式能處理預期中的轉換錯誤。原始值仍留在 CSV 裡。'),
55
+ exercise('parse_count()','[練習1]',['用 parse_count() 測試下面五個值。執行前,寫下你預期的結果。','"0"  " 123 "  "1,234"  ""  "-"','在 main() 裡、rows = load_rows() 下方加入測試,例如:\n print(parse_count("1,234"))'],'45–50 分測試,50–60 分休息。保留 main() 原本內容,只加入測試輸出。'),
56
+ table('parse_count()','[練習1 解答]',[['輸入的值','回傳結果'],['"0"','0'],['" 123 "','123'],['"1,234"','1234'],['""','None'],['"-"','None']], '檢查結果時,也要看資料型態:0 是數字;None 代表沒有數值。'),
57
+ code('資料筆數與欄位','在 main() 裡,rows = load_rows() 下方加入這兩行。',' print(len(rows))\n print(len(rows[0]))\n\n# 242\n# 15','第一行算有幾筆資料;第二行算第一筆資料有幾個欄位。','60–70 分。再次說明標題列不算一筆資料,15 個欄位不等於 15 個縣市。'),
58
+ text('資料筆數與欄位',['資料包含 11 個學年度,每個學年度都有 22 個縣市。', '{{11 × 22 = 242}},和程式讀到的筆數相同。','如果下載到另一個版本,筆數可能不同。這時要回到資料頁查看年份,不能直接刪掉多出來的資料。']),
59
+ table('重複資料','假設遇到這三筆資料,哪些可能重複?',[['學年度','縣市別','國小[人]'],['114','新北市','194670'],['114','臺中市','161332'],['114','新北市','194670']], '在這份資料裡,「學年度+縣市別」應該對應一筆紀錄。上表是刻意製作的練習例子。','70–75 分。原始附檔沒有重複的(學年度、縣市別)組合。'),
60
+ text('重複資料',['同一個縣市在不同學年度出現,是正常的,不算重複。','同一個學年度、同一個縣市出現兩次,才需要回去查明來源。也可能是一筆被重複貼上,或混用了兩個版本。','不要把這兩筆直接加起來,否則人數可能變成兩倍。']),
61
+ code('查詢條件','這個函式先挑出指定學年度,再依人數排序。','results = count_by_county(rows, "114", "國小[人]")\nprint(results[0])\n\n# ("新北市", 194670)','這次固定 114 學年度,只比較各縣市的國小學生人數。'),
62
+ table('數字核對','把程式的結果和原始 CSV 放在一起看。',[['要核對的項目','CSV 裡的內容'],['學年度','114'],['縣市別','新北市'],['欄位','國小[人]'],['人數','194670']], '程式也應該得到 194,670。四個條件都相同,才是在核對同一筆資料。','75–85 分。教師示範一筆,學生再自行核對三筆。',[330,730]),
63
+ exercise('數字核對','[練習2]',['從自己的結果選 3 個縣市,在 CSV 找到對應的人數。','在 REPORT.md 記下學年度、縣市、欄位名稱和人數。','如果數字不同,從年份和欄位名稱開始查。'], '85–95 分。這個活動每人獨立進行;教師看學生找到的原始列。'),
64
+ exercise('AI 與錯誤訊息','[練習3]',['把 ValueError: invalid literal for int() 這段錯誤交給 AI,請它解釋哪種輸入會造成錯誤。','用自己的程式實際測試其中一個例子。','記下輸入值、原本的錯誤,以及 parse_count() 的回傳結果。'],'95–105 分。不需要上傳整份檔案或帳號資訊;錯誤訊息和幾行相關程式即可。'),
65
+ exercise('資料檢查','[挑戰]',['parse_count("-12") 會得到什麼?','能轉成整數,就一定是合理的學生人數嗎?','如果要排除負的人數,條件應該加在哪裡?'],'105–110 分選做。答案為 -12;型態正確不代表符合資料意義。不要強迫所有人完成新功能。'),
66
+ text('整理紀錄',['在 REPORT.md 記下哪些值能使用、哪些值無法計算,\n並說明你的處理方式。','加入你核對過的 3 筆資料。原始 CSV 保持原樣,整理方式寫在程式裡。','存檔後再執行一次,確認你的問題仍然查得到結果,提交本週進度。'],'110–120 分。核對基本測試、三筆紀錄與可執行程式;快速學生可完成負值檢查。'),
67
+ ],
68
+ },
69
+ {
70
+ lesson:21,
71
+ title:'資料分析 III|比較與圖表',
72
+ file:'Lesson21_找差異讓圖表說清楚_學生版修訂.pptx',
73
+ slides:[
74
+ cover('資料分析 III','比較與圖表'),
75
+ exercise('比較資料','[複習]',['你的問題要找人數最多的地方,還是比較某個地方的增減?','找出上週核對過的數字。'],'0–10 分。按學生自己的題目分別說明排名和跨年比較。'),
76
+ table('人數變化','新北市的國小學生人數。',[['學年度','學生人數(人)'],['104','201,970'],['114','194,670']], '人數減少了多少?','10–15 分。讓學生口算或用計算機算差值。'),
77
+ text('人數變化',['以後來的人數減去原來的人數:','194,670 − 201,970 = {{−7,300}}','負數表示減少,因此可以寫「新北市的國小學生人數,在 104 到 114 學年度之間減少 7,300 人」。']),
78
+ exercise('人數變化','[例1]',['臺中市的國小學生人數:\n104 學年度是 157,272 人,114 學年度是 161,332 人。','這段期間增加或減少了多少人?'],'15–20 分。每人各自計算,下一頁公布答案。'),
79
+ text('人數變化',['[例1 解答]','161,332 − 157,272 = {{4,060}}','臺中市的國小學生人數,在 104 到 114 學年度之間增加 4,060 人。']),
80
+ text('百分比',['少了 100 人,原來有 1,000 人和原來有 10,000 人,影響一樣大嗎?','要比較變化的程度,可以把增減人數除以{{原來的人數}},再乘以 100%。','原來有 1,000 人,少了 100 人是少 10%;原來有 10,000 人,少了 100 人是少 1%。'],'20–30 分。百分比列為選做計算,基本任務仍可用差幾人完成。'),
81
+ text('百分比',['新北市減少 7,300 人,原來有 201,970 人。','−7,300 ÷ 201,970 × 100% ≈ {{−3.6%}}','可以寫「新北市的國小學生人數約減少 3.6%」。這裡的分母是 104 學年度的人數。']),
82
+ exercise('百分比','[挑戰]',['如果原來的人數是 0,還能用剛才的公式算百分比嗎?','程式遇到這種情況,應該顯示什麼?'],'答案:不能除以 0,可以顯示無法計算變化率,並保留原來與後來的數字。不要用 1 代替分母。'),
83
+ table('排名','114 學年度,國小學生人數前 5 名。',[['縣市','人數(人)'],['新北市','194,670'],['臺中市','161,332'],['桃園市','132,964'],['高雄市','127,327'],['臺北市','117,650']], '表格能列出確切數字;圖表則方便比較人數的差距。','30–35 分。資料取自原始 CSV,不是隨意挑選的五個縣市。'),
84
+ { kind:'chart', title:'長條圖', note:'35–40 分。先讓學生指出最高、第三和最低的長條,再回表格核對數字。前 5 名只是全臺 22 縣市的一部分。' },
85
+ text('圖表標題',['如果標題只寫「學生人數」,讀者不知道是哪一年,也不知道是哪個教育階段。','這張圖的標題是:{{114 學年度各縣市國小學生人數(前 5 名)}}。','人數的單位是「人」。圖表下方也要註明教育部統計處的資料來源。']),
86
+ text('長條圖',['長條的長度代表人數,所以人數軸應該{{從 0 開始}}。','如果把前面一大段截掉,相近的數字就可能看起來差很多。','這張圖只列前 5 名。如果想知道第 6 名,\n要回到完整的排序結果查看。'],'40–45 分。說明尺度與範圍,不延伸到所有圖都必須從零開始。折線圖的尺度另有選擇,本課重點是長條。'),
87
+ code('文字長條圖','課堂系統可以在 Console 用文字畫出長條。','新北市 194,670 | ████████████████████████\n臺中市 161,332 | ████████████████████\n桃園市 132,964 | ████████████████','每一格代表相同人數;旁邊仍保留實際數字。','45–50 分。文字圖不需要安裝額外套件。50–60 分休息。'),
88
+ text('文字長條圖',['把最多人的縣市畫成 24 格,其他縣市照比例縮短。','臺中市的長度:161,332 ÷ 194,670 × 24 ≈ {{20 格}}','因此兩個縣市用的是同一個比例,才能比較。'],'60–65 分。用計算機核對長度。'),
89
+ code('文字長條圖','main.py 裡的 print_text_chart() 使用下面的計算方式。','bar_length = round(value / largest * 24) if largest else 0\nprint(f"{county} {value:,} | {\'█\' * bar_length}")','largest 是這次結果中最多的人數。人數很少時可能四捨五入成 0 格,因此旁邊仍保留實際數字。','所有結果都是 0 時,長條也畫 0 格,避免除以 0。'),
90
+ code('取得一筆人數','main.py 已附 get_count()。下面這段加在 main() 裡、rows = load_rows() 之後。','before = get_count(rows, "104", "新北市", "國小[人]")\nafter = get_count(rows, "114", "新北市", "國小[人]")\nprint(before)\nprint(after)','參數依序是資料、學年度、縣市和欄位。\n找不到資料時回傳 None。這裡會得到 201970 和 194670。','65–75 分。這段放在 main() 的 rows = load_rows() 之後,縮排和其他主程式相同。'),
91
+ code('程式計算人數變化','接在剛才的 before 和 after 下面。','if before is None or after is None:\n print("找不到資料,請檢查年份、縣市和欄位")\nelse:\n change = after - before\n print(change)','查到兩筆資料後再相減,結果是 -7300。','教師示範條件判斷,學生依自己的縣市修改。百分比可在 else 區塊內加上 change / before * 100,並另處理 before 為 0 的情況。'),
92
+ exercise('程式計算人數變化','[練習2]',['把縣市改成臺中市,計算 104 到 114 學年度的國小學生人數變化。','程式的結果是否和例1的答案相同?','再換成你自己的專題條件。'],'75–90 分。基本目標是成功得到 4060,再改自己的條件。'),
93
+ text('總數與比例',['新北市有 194,670 位國小學生,這是{{學生總數}}。','如果想問「居民裡有多少比例是國小學生」,還需要新北市的居民總數,這份 CSV 沒有這個欄位。','總數最多,不等於占居民的比例最高。'],'90–95 分。這裡只區分總數與占比,不額外要求學生取得人口資料。'),
94
+ exercise('AI 與資料解釋','[練習3]',['假設 AI 寫出:「因為少子化,新北市國小學生在 104 到 114 學年度之間減少 7,300 人。」','哪一部分可以從這份 CSV 查到?哪一部分還需要其他資料?','把這句話改成這份資料確實能支持的說法。'],'95–100 分。可查到減少 7300;不能只靠這份表證明原因。不要否認少子化可能有影響,只區分證據是否足夠。'),
95
+ exercise('個人專題','[實作]',['依自己的問題完成一張圖,或一個有原始數字的比較結果。','在 REPORT.md 寫出計算方式、結果,以及資料不能回答的一件事。','圖表要有學年度、教育階段、單位和來源。'],'100–115 分。基礎學生可使用既有文字長條圖與排名;熟練者做差值或百分比。'),
96
+ table('個人專題','[延伸] 這三筆都來自新北市的國小學生人數。',[['學年度','國小學生人數(人)'],['104','201,970'],['109','199,521'],['114','194,670']], '只比較起點和終點,看不到中間的變化。可以再取其他學年度,檢查這段期間的走勢。','選做,不要求所有人畫折線圖。題目若談整段趨勢,就不能只憑兩個端點說每年都減少。'),
97
+ text('本週成果',['執行你的專案,確認圖表或計算結果能重新出現。','在 REPORT.md 保留一組原始數字、計算方式和你的說明。','存檔並提交進度。下一週會把這些內容整理成報告。'],'115–120 分。教師看條件、數字和說明是否一致,不以圖表華麗程度評分。'),
98
+ ],
99
+ },
100
+ {
101
+ lesson:22,
102
+ title:'資料分析 IV|分析報告',
103
+ file:'Lesson22_分享你的資料發現_學生版修訂.pptx',
104
+ slides:[
105
+ cover('資料分析 IV','分析報告'),
106
+ text('分析報告',['報告要讓別人知道:你問了什麼、用了哪些資料,以及答案怎麼算出來。','報告裡選用和問題有關的數字與圖表,\n並說明計算方式,以及這些結果代表什麼。'],'0–10 分。打開學生自己的 REPORT.md,不重新選題或開新專案。'),
107
+ text('分析報告',['[範例]','我比較了新北市 104 和 114 學年度的國小學生人數。104 學年度有 201,970 人,114 學年度有 194,670 人,相減後減少 7,300 人。','資料來自教育部統計處。這份表可以說明人數變化,但沒有提供變化原因。'],'10–15 分。完整範例先讓學生知道最後要寫成什麼樣子,後面逐段拆開。'),
108
+ text('研究問題',['報告標題可以直接寫:','{{新北市的國小學生人數變化(104–114 學年度)}}','讀者看到標題,就知道比較的地區、教育階段和年份。自己���題目不同,就換成自己的條件。']),
109
+ table('資料來源','在 REPORT.md 的「資料來源與範圍」寫清楚這些內容。',[['項目','本次範例'],['資料名稱','各級學校縣市別學生人數'],['提供機關','教育部統計處'],['資料頁面','https://data.gov.tw/dataset/40121'],['使用範圍','新北市,104 與 114 學年度,國小[人]']], '使用其他公開資料,也要保留原本的資料名稱和網址。','15–20 分。資料已附在專案;來源網址不能只寫 Google 或 AI。',[260,800]),
110
+ text('計算方法',['可以這樣寫:','從資料中找出新北市 104 和 114 學年度的紀錄,讀取「國小[人]」欄位,將 114 學年度的人數減去 104 學年度的人數。','讓讀者照著這段話,也能從 CSV 找到相同的兩個數字。']),
111
+ table('計算結果','原始數字和計算結果都要保留。',[['項目','數值'],['104 學年度','201,970 人'],['114 學年度','194,670 人'],['114 減 104','−7,300 人']], '「減少 7,300 人」是這次比較的答案。若有百分比,另外寫明分母與計算方式。'),
112
+ text('結果解釋',['「新北市國小學生減少 7,300 人」是這份資料可以支持的說法。','「新北市每一間國小都少了學生」則不能從縣市總數判斷。有些學校可能增加,有些可能減少。','分析的範圍是縣市總數,解釋時也要維持這個範圍。'],'20–25 分。用同一個例子說明總數不等於每個個體都如此。'),
113
+ exercise('報告文字','[練習1]',['「臺中市國小學生從 157,272 人變成 161,332 人,所以學生一直增加。」','如果只用了 104 和 114 學年度兩筆資料,這句話哪裡需要修改?'],'25–30 分。可改成「104 到 114 學年度之間增加 4060 人」。不能只憑端點斷言中間每年都增加。'),
114
+ code('重新執行專案','報告完成後,關掉檔案,再從專案重新打開。','main.py\nREPORT.md\nDATA_SOURCE.md\ndata/\n students_by_county.csv','執行 main.py,確認報告裡的數字仍然能算出來。','30–40 分。所有學生各自重跑,教師確認是學生本人操作。'),
115
+ code('找不到檔案','如果出現 FileNotFoundError,檢查 main.py 上方的設定。','DATA_FILE = Path("data/students_by_county.csv")','資料夾是 data,檔名是 students_by_county.csv。\n檢查這兩個名稱是否和專案中的檔案相同。'),
116
+ code('找不到欄位','欄位名稱要依照原始 CSV。','row["國小學生人數"] # 這份 CSV 沒有這個 key\nrow["國小[人]"] # 原始欄位名稱','如果發生 KeyError,回去看 CSV 第一行,不要憑印象重打欄位名稱。'),
117
+ code('找不到資料','如果結果顯示「沒有符合條件的資料」,檢查查詢條件。','SAMPLE_YEAR = "2025" # CSV 沒有這個學年度值\nSAMPLE_YEAR = "114" # CSV 裡的寫法','同樣也要檢查縣市名稱,例如資料裡用「臺中市」,不是「台中市」。'),
118
+ exercise('重新執行專案','[練習2]',['核對自己的報告和程式:年份、縣市、欄位名稱及結果是否相同?','從報告裡選一個數字,在 CSV 裡找到它,並記下它在哪一列。','如果報告用到相減,就把兩個原始數字都找出來。'],'40–50 分。教師巡看學生找到的原始資料。50–60 分休息。'),
119
+ exercise('AI 修改報告','[例]',['你寫:「新北市的國小學生在 104 到 114 學年度減少 7,300 人。」','AI 改成:「新北市因人口外流,國小學生銳減 7,300 人。」','這個修改能直接採用嗎?'],'60–65 分。人口外流不是這份資料提供的原因;「銳減」也帶入強烈評價。讓學生保留確定的地區、年份、數字與變化方向。'),
120
+ text('AI 使用紀錄',['紀錄可以寫成具體的一件事:','「我請 AI 解釋空字串為什麼不能轉成整數。\n它建議先檢查空白。我用空字串和 0 測試,\n結果分別是 None 和 0。」','如果沒有使用 AI,就寫「本次未使用」,不需要補一段沒有發生過的對話。']),
121
+ exercise('個人報告','[實作]',['完成 REPORT.md 裡的問題、資料來源、計算方法、結果和解釋。','把程式輸出或文字圖表放在結果旁邊,並寫出這份資料還不能回答什麼。','使用 AI 的部分,記下自己接受了什麼建議、實際檢查了什麼。'],'65–85 分。學生各自整理。教師對尚未完成者優先協助題目、可執行程式與一組可信數字,先不要增加功能。'),
122
+ text('個人分享',['用約 2 分鐘介紹自己的題目和結果。','展示一張圖或一組數字,說明這些數字從哪來、怎麼算。最後說明這份資料還缺什麼。','分享後,回答一個問題。'],'85–90 分準備,90–115 分分享。最多 8 人時每人 2 分鐘+1 分鐘提問;超過 8 人改每人 1 分鐘+30 秒提問,或分時段分享,確保人人可展示。'),
123
+ exercise('分享提問','[例]',['「如果把 114 學年度換成 104 學年���,排名可能改變嗎?」','「這個負數是什麼意思?」','「你可以指出程式用的是哪個欄位嗎?」'],'作為教師提問頁,可依學生題目選一個問題。不要把問答變成背整段程式。'),
124
+ text('提交專題',['提交前,確認 main.py 能執行,REPORT.md 的結果和程式相同。','資料來源留在 DATA_SOURCE.md,CSV 放在 data 資料夾。','把最後修改的內容存檔,再用課堂系統提交目前版本。'],'115–120 分。評量重點是可回答的題目、來源、數字核對與個人說明。若程式仍有錯,學生先保留錯誤和目前能確認的結果,教師再處理。'),
125
+ exercise('後續專題','[延伸]',['把程式改成可以輸入學年度或縣市,不用每次修改程式碼。','也可以在原來的專題功能裡加入這次整理好的統計結果。'],'選做延伸。四週後回到原專題課程,這頁不列入本次最低完成標準,也不要求本週開發 GUI。'),
126
+ ].filter(s => typeof s === 'object'),
127
+ },
128
+ ];
classroom_data/templates/week19/DATA_SOURCE.md ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 範例資料來源
2
+
3
+ - 資料集:教育部統計處「各級學校縣市別學生人數」
4
+ - 資料集頁面:https://data.gov.tw/dataset/40121
5
+ - 原始 CSV:`data/students_by_county.csv`
6
+ - 內容:104–114 學年度各縣市各教育階段的彙總學生人數。
7
+ - 檔案共有 242 筆資料、15 個欄位,涵蓋 22 個縣市。
8
+ - 授權:政府資料開放授權條款第 1 版;使用或改作時保留資料來源註記。
9
+
10
+ ## 下載同一份資料
11
+
12
+ 1. 開啟上方資料集頁面。
13
+ 2. 找到「資料資源下載網址」,按左側的 **CSV** 按鈕。旁邊的「檢視資料」是檔案說明。
14
+ 3. 下載的檔名是 `edu_B_1_4.csv`。本課把它改名為 `students_by_county.csv`,放在專案的 `data` 資料夾。
15
+
16
+ 課堂專案已附檔,不需要重新下載。網站之後可能更新年份;上課範例以本課提供的 104–114 學年度版本為準。
17
+
18
+ 這是彙總統計資料。它可以回答「多少人」或「不同地區、學年度的數值如何變化」,不能單獨說明變化的原因。若換成其他公開資料,請把來源網址、下載日期、欄位和單位補進本檔。
classroom_data/templates/week19/REPORT.md ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 我的資料分析
2
+
3
+ ## 我想回答的問題
4
+
5
+ <!-- 寫清楚對象、指標和比較條件。 -->
6
+
7
+ ## 資料來源與範圍
8
+
9
+ - 資料名稱:
10
+ - 提供機關:
11
+ - 來源網址:
12
+ - 專案裡的檔案位置:
13
+ - 時間範圍:
14
+ - 一列代表什麼:
15
+ - 數字單位:
16
+
17
+ ## 我怎麼整理和計算
18
+
19
+ <!-- 寫出使用的學年度、縣市、欄位名稱,以及怎麼計算。資料有空白時,也寫下你的處理方法。 -->
20
+
21
+ ## 我找到的證據
22
+
23
+ <!-- 貼上 main.py 的輸出或文字圖表,並寫下你在 CSV 裡核對過的數字。相減時保留兩個原始數字。 -->
24
+
25
+ ## 我的結論
26
+
27
+ <!-- 結論必須能由上面的數字支持。 -->
28
+
29
+ ## 這份資料還不能回答什麼
30
+
31
+ <!-- 例如:人數有變化,但資料沒有告訴我們原因。 -->
32
+
33
+ ## AI 使用紀錄
34
+
35
+ <!-- 寫一件實際發生的事;本次沒有用 AI,就寫「本次未使用」。 -->
36
+ - 我問了什麼:
37
+ - 我採納或修改了什麼:
38
+ - 我如何用資料或程式核對:
classroom_data/templates/week19/data/students_by_county.csv ADDED
@@ -0,0 +1,243 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 學年度,縣市別,幼兒園[人],國小[人],國中[人],高級中等學校-普通科[人],高級中等學校-專業群科[人],高級中等學校-綜合高中[人],高級中等學校-實用技能學程[人],高級中等學校-進修部[人],大專校院(全部計入校本部)[人],大專校院(跨縣市教學計入所在地縣市)[人],宗教研修學院[人],國中小進修部(補校)及大專進修學校及空大[人],特殊教育學校[人]
2
+ 104,新北市,73526,201970,116039,40566,41358,5773,1434,6816,161853,154484,138,17150,400
3
+ 104,臺北市,47269,118471,75015,60532,39235,4892,530,7152,267363,244091,42,9174,828
4
+ 104,桃園市,44367,128444,76299,29529,36060,4300,4963,7623,109869,120426,0,2532,438
5
+ 104,臺中市,65946,157272,97447,37771,54951,3027,3423,7885,192593,191962,0,10182,953
6
+ 104,臺南市,40731,92570,58160,25795,24022,6146,4557,2813,132736,135744,0,1601,533
7
+ 104,高雄市,49321,134949,83539,35600,39574,5499,6502,4915,149988,156110,12,9450,678
8
+ 104,宜蘭縣,9791,23227,16014,6409,4899,1471,805,732,14725,17952,0,811,129
9
+ 104,新竹縣,13997,36265,19150,4230,5487,2763,858,1436,17525,23665,0,653,230
10
+ 104,苗栗縣,10492,29940,18268,5922,8086,2136,1285,1247,21352,21352,0,903,136
11
+ 104,彰化縣,24073,68920,42767,11939,19919,4790,2755,3975,38990,38990,0,1309,670
12
+ 104,南投縣,9328,25439,17256,4504,8411,870,1236,960,12458,12458,0,555,126
13
+ 104,雲林縣,12818,35903,24178,8009,9430,1971,1563,698,26340,27035,0,1145,211
14
+ 104,嘉義縣,8175,22434,14440,2708,5244,515,1697,460,25071,30424,0,914,0
15
+ 104,屏東縣,13986,39629,26193,8977,10010,1381,855,1299,42083,42203,0,1406,185
16
+ 104,臺東縣,4319,11156,7227,513,2349,3224,188,329,5963,6008,0,54,101
17
+ 104,花蓮縣,6561,16538,10861,4168,4184,2682,93,673,19610,19562,0,284,170
18
+ 104,澎湖縣,1635,3972,2670,1018,1651,0,60,0,2984,2984,0,33,0
19
+ 104,基隆市,6091,17341,11496,5081,6555,416,365,656,16077,16077,0,2018,157
20
+ 104,新竹市,11697,29694,16662,8793,5764,3630,621,1785,51748,51050,0,249,0
21
+ 104,嘉義市,5918,16104,11640,6358,9204,1660,1846,842,18728,15324,0,268,316
22
+ 104,金門縣,1851,3679,2144,988,961,0,60,53,4389,4523,0,0,0
23
+ 104,連江縣,223,443,259,0,0,335,0,0,0,21,0,0,0
24
+ 105,新北市,77229,195706,106199,41342,39875,5791,1205,6119,157674,151012,136,16599,397
25
+ 105,臺北市,50079,116496,69178,59008,37749,4659,472,6294,267043,244062,53,8685,815
26
+ 105,桃園市,47794,124604,70471,29658,36600,3393,4944,7041,106343,116787,0,2675,448
27
+ 105,臺中市,72269,153062,89408,37764,54626,2042,3634,7160,191224,190633,0,10001,940
28
+ 105,臺南市,43250,89475,53089,26098,23331,5324,4362,2700,128882,131468,0,1429,508
29
+ 105,高雄市,54095,129914,76167,35688,39310,4683,5988,4136,148244,154088,27,8797,667
30
+ 105,宜蘭縣,10247,22349,14783,6383,4743,1399,769,607,14458,17717,0,872,115
31
+ 105,新竹縣,14995,35692,18044,5089,5565,1803,829,1392,16395,22667,0,607,230
32
+ 105,苗栗縣,11018,28579,16836,6157,8037,1852,1216,1124,20832,20794,0,707,129
33
+ 105,彰化縣,25336,65423,39679,12079,19434,4550,2799,3684,37422,37450,0,1160,623
34
+ 105,南投縣,9873,24180,15716,4461,8426,739,1226,791,12191,12191,13,536,149
35
+ 105,雲林縣,13080,34012,22308,8080,9369,1810,1527,658,25729,26405,0,1070,208
36
+ 105,嘉義縣,8490,20810,13153,2729,4953,519,1778,483,24448,29757,0,915,0
37
+ 105,屏東縣,14616,37482,24014,9246,9664,1091,811,1206,41174,41150,0,1487,189
38
+ 105,臺東縣,4510,10702,6682,537,2519,2847,182,303,5910,6091,0,37,111
39
+ 105,花蓮縣,6772,15914,10165,4144,4074,2404,64,598,19426,19510,0,257,171
40
+ 105,澎湖縣,1685,3756,2452,1000,1668,0,65,0,3018,3018,0,29,0
41
+ 105,基隆市,6451,16654,10279,5082,6395,383,373,535,15741,15741,0,2118,159
42
+ 105,新竹市,12544,29534,15683,9016,5783,3649,607,1677,50315,49211,0,282,0
43
+ 105,嘉義市,6220,15468,10634,6507,9047,1475,1892,777,18457,15053,0,251,329
44
+ 105,金門縣,1936,3618,2023,1009,1016,0,51,36,4515,4618,0,0,0
45
+ 105,連江縣,292,455,249,0,0,324,0,0,0,18,0,0,0
46
+ 106,新北市,81348,192321,100608,40999,37328,5581,917,5336,151475,145054,116,16615,385
47
+ 106,臺北市,52253,115679,66746,58742,35058,4072,377,5448,263668,241829,81,8120,779
48
+ 106,桃園市,51322,122590,67375,29822,35420,3032,4770,5965,102983,114101,0,2633,450
49
+ 106,臺中市,77619,150412,85244,37962,50784,2020,3836,6211,188641,188114,0,9450,897
50
+ 106,臺南市,46663,87329,50556,25817,22075,4418,4007,2540,124447,126610,0,1397,482
51
+ 106,高雄市,56968,125900,72395,35657,38273,3347,5579,3557,144540,149969,72,8142,673
52
+ 106,宜蘭縣,10639,21563,14108,6290,4513,1306,673,529,13546,16707,0,806,116
53
+ 106,新竹縣,16035,35578,17398,5351,5436,1431,869,1241,15259,20942,0,561,235
54
+ 106,苗栗縣,11727,27602,15835,6216,7835,1552,1221,934,19806,19674,0,425,135
55
+ 106,彰化縣,26724,62830,37275,12119,18398,4270,2726,3397,35163,35166,0,1099,565
56
+ 106,南投縣,10345,23221,14436,4525,8302,623,1114,679,11578,11578,28,530,145
57
+ 106,雲林縣,13617,32410,21017,7949,8666,1577,1451,667,25198,25932,0,1066,197
58
+ 106,嘉義縣,8649,19569,12207,2732,4964,495,1583,502,24088,29101,0,886,0
59
+ 106,屏東縣,15592,35645,22574,9068,9114,964,698,1105,39787,39639,0,1494,189
60
+ 106,臺東縣,4643,10385,6303,563,2543,2635,188,257,5860,5997,0,28,114
61
+ 106,花蓮縣,7017,15363,9614,4047,3875,2185,28,538,18762,18819,0,256,158
62
+ 106,澎湖縣,1766,3524,2362,970,1588,0,64,0,3008,3008,0,18,0
63
+ 106,基隆市,6909,15944,9635,4696,5852,349,412,437,15211,15211,0,2208,131
64
+ 106,新竹市,13284,29635,15414,9300,5680,3397,559,1518,48359,47203,0,280,0
65
+ 106,嘉義市,6466,15068,9996,6407,8940,1371,1922,693,18014,14659,0,230,302
66
+ 106,金門縣,2026,3619,1948,1007,1005,0,47,52,4501,4562,0,0,0
67
+ 106,連江縣,292,474,227,0,0,304,0,0,0,19,0,0,0
68
+ 107,新北市,83960,195862,95923,39153,34275,5128,700,4693,147293,141046,89,17056,318
69
+ 107,臺北市,54229,118097,64651,57381,31239,3316,302,4712,259340,239263,124,7409,739
70
+ 107,桃園市,54358,124639,64734,29262,33257,2544,4422,5138,101858,113066,0,2628,419
71
+ 107,臺中市,79891,153260,81865,37422,45640,1970,4119,5479,186363,185886,0,9543,807
72
+ 107,臺南市,47210,88745,48570,24629,19920,3480,3594,2101,120215,121598,47,1342,446
73
+ 107,高雄市,57683,126527,69362,34231,36231,2134,5094,3021,141008,146140,128,7742,625
74
+ 107,宜蘭縣,10876,21515,13233,5981,4325,1233,610,555,12864,15850,0,816,103
75
+ 107,新竹縣,16412,36512,16990,5204,5121,1174,850,949,14411,19541,0,545,241
76
+ 107,苗栗縣,12027,27778,14967,6182,7363,1152,1206,867,18887,18704,0,219,118
77
+ 107,彰化縣,29513,62367,35150,11949,17139,3951,2689,2712,33376,33328,0,870,511
78
+ 107,南投縣,10617,22763,13531,4269,7575,637,970,588,10926,10926,50,470,137
79
+ 107,雲林縣,14170,31645,19840,7607,7833,1273,1403,642,24836,25598,0,1038,202
80
+ 107,嘉義縣,8865,18736,11450,2700,4597,426,1336,399,27312,31702,0,979,0
81
+ 107,屏東縣,16004,35146,21110,8665,8086,828,640,1095,38794,38541,0,1532,173
82
+ 107,臺東縣,4733,10189,6052,562,2622,2349,185,275,5757,5814,0,37,108
83
+ 107,花蓮縣,7160,15235,9102,3975,3725,1780,72,491,18269,18301,0,269,143
84
+ 107,澎湖縣,1803,3472,2200,897,1542,0,44,0,3025,3025,0,18,0
85
+ 107,基隆市,7169,15896,9031,4093,5088,338,411,413,14802,14802,0,2162,114
86
+ 107,新竹市,13773,30866,15214,9256,5875,2906,533,1358,47161,46003,0,246,0
87
+ 107,嘉義市,6628,15090,9351,6298,8298,1214,1865,589,14022,11234,0,208,288
88
+ 107,金門縣,2037,3729,1863,976,1018,0,34,47,4303,4374,0,0,0
89
+ 107,連江縣,286,482,220,0,0,285,0,0,0,80,0,0,0
90
+ 108,新北市,87311,198588,93426,37425,30732,4471,600,4028,141670,135795,81,17747,271
91
+ 108,臺北市,56635,120623,64558,56217,26944,2641,323,4024,256209,237838,148,7326,653
92
+ 108,桃園市,58589,126902,63378,29030,30448,1781,3993,4183,99870,110856,0,2639,409
93
+ 108,臺中市,81908,155376,80549,36466,39657,1806,4435,4760,184268,183875,0,9967,702
94
+ 108,臺南市,47967,90188,47473,23228,17808,2843,3121,1912,115057,115794,67,1578,408
95
+ 108,高雄市,60483,127513,67579,32753,33865,1212,4279,2681,138258,143051,171,7619,566
96
+ 108,宜蘭縣,11389,21698,12534,5663,4093,1103,601,581,12197,14880,0,689,86
97
+ 108,新竹縣,17325,37203,16909,5029,4977,918,824,760,13314,17919,0,584,213
98
+ 108,苗栗縣,13390,27856,14515,5933,6656,971,1083,769,18246,18018,0,228,114
99
+ 108,彰化縣,31301,62052,33185,11609,15924,3582,2643,2113,31147,31147,0,817,493
100
+ 108,南投縣,11295,22689,12793,3907,6876,778,796,561,10214,10214,58,494,123
101
+ 108,雲林縣,14988,31117,18968,7225,6736,1182,1285,530,24603,25358,0,817,193
102
+ 108,嘉義縣,9097,18355,10668,2636,4058,352,1080,333,26309,30480,0,1095,0
103
+ 108,屏東縣,17157,34688,20054,8123,7020,700,562,866,36734,36549,0,1310,168
104
+ 108,臺東縣,4853,9975,5805,541,2533,2105,187,237,5842,5863,0,33,114
105
+ 108,花蓮縣,7531,15217,8567,3810,3504,1485,20,434,18027,18040,0,289,127
106
+ 108,澎湖縣,1866,3472,2062,833,1368,0,52,0,3051,3051,0,36,0
107
+ 108,基隆市,7506,15862,8704,3502,4334,270,323,370,14561,14561,0,2061,114
108
+ 108,新竹市,14559,31907,15323,9025,6102,2073,494,1158,45986,44777,0,179,0
109
+ 108,嘉義市,6996,15107,8843,6076,7488,991,1746,423,13222,10593,0,184,264
110
+ 108,金門縣,2099,3756,1846,948,931,0,4,63,4387,4451,0,0,0
111
+ 108,連江縣,300,470,241,0,0,257,0,0,0,62,0,0,0
112
+ 109,新北市,89211,199521,92271,36692,28494,4259,516,3514,139130,133358,74,17165,260
113
+ 109,臺北市,58312,122605,65349,55247,24765,2264,328,3646,256492,239317,154,4010,633
114
+ 109,桃園市,61674,128236,62781,28861,28965,1269,3538,3496,100402,111374,0,1409,384
115
+ 109,臺中市,84021,156584,79598,36000,35898,1680,5002,4219,186976,186543,0,6087,655
116
+ 109,臺南市,49113,90522,46886,22560,16556,2453,2897,1689,111807,112370,75,819,391
117
+ 109,高雄市,63771,127298,66052,31908,32132,1033,3562,2256,137812,142353,178,5159,535
118
+ 109,宜蘭縣,11549,21737,11869,5520,4016,988,566,481,11674,14166,0,427,91
119
+ 109,新竹縣,18473,37672,17130,4971,4884,851,780,729,12657,16589,0,187,192
120
+ 109,苗栗縣,14077,27698,14088,5921,6148,720,946,655,18036,17809,0,221,105
121
+ 109,彰化縣,32060,61453,31796,10988,15106,3324,2598,1737,28036,28036,0,455,498
122
+ 109,南投縣,11741,22273,12183,3646,6155,913,636,512,9771,9771,69,220,114
123
+ 109,雲林縣,15183,30472,18282,7091,6310,1116,1228,469,24519,25319,0,435,187
124
+ 109,嘉義縣,9397,17770,9990,2682,3212,197,850,218,25456,29682,0,118,0
125
+ 109,屏東縣,17983,34316,18930,7907,6302,649,483,722,35511,35454,0,683,163
126
+ 109,臺東縣,4923,9859,5592,592,2457,1867,167,199,5980,5953,0,28,104
127
+ 109,花蓮縣,7601,15194,8248,3696,3356,1212,14,419,17821,17714,0,192,110
128
+ 109,澎湖縣,1848,3446,1902,818,1283,0,47,0,3047,3047,0,33,0
129
+ 109,基隆市,8009,15691,8368,3338,3956,240,271,332,15735,15735,0,261,119
130
+ 109,新竹市,14728,32372,15651,8807,6680,1373,492,1106,45641,44392,0,154,0
131
+ 109,嘉義市,7237,14846,8710,6177,6906,796,1706,373,12826,10221,0,95,218
132
+ 109,金門縣,2202,3808,1854,941,911,0,0,50,4131,4213,0,0,0
133
+ 109,連江縣,293,493,257,0,0,237,0,0,0,44,0,0,0
134
+ 110,新北市,88778,202529,91044,36286,27493,4080,403,3135,135995,130782,65,16922,262
135
+ 110,臺北市,57492,125093,65268,54449,22915,1942,360,3373,250939,239082,165,3422,587
136
+ 110,桃園市,62155,131258,62184,28842,27813,922,3225,3082,99535,110565,18,1203,353
137
+ 110,臺中市,82579,160242,78320,35832,33498,1509,5328,3795,187115,186634,0,4737,635
138
+ 110,臺南市,49071,91967,46589,22150,15877,2340,2637,1595,108005,108508,73,728,387
139
+ 110,高雄市,65003,128846,64368,31497,30510,1052,3279,2098,136309,140652,150,5263,499
140
+ 110,宜蘭縣,11578,21720,11620,5346,3938,933,521,459,11076,13022,0,312,91
141
+ 110,新竹縣,18949,38429,17235,4993,4709,832,829,690,11956,15458,0,148,195
142
+ 110,苗栗縣,14072,28275,13787,5967,5755,498,802,602,17825,17633,0,175,105
143
+ 110,彰化縣,31533,61364,30638,10593,14115,3062,2428,1482,25049,25049,0,381,496
144
+ 110,南投縣,11684,22481,11549,3413,5517,1043,574,486,9477,9477,89,205,111
145
+ 110,雲林縣,15011,30256,17466,6900,5978,978,1153,399,24124,24936,0,269,180
146
+ 110,嘉義縣,9198,17571,9219,2652,2692,74,657,138,24808,28824,0,92,0
147
+ 110,屏東縣,18038,34546,17910,7705,5825,642,398,621,34038,33925,0,378,156
148
+ 110,臺東縣,4937,9768,5322,577,2341,1786,151,160,6095,6105,0,19,99
149
+ 110,花蓮縣,7687,15242,8045,3608,3276,987,16,374,17714,17588,0,171,103
150
+ 110,澎湖縣,1853,3485,1810,776,1189,0,41,0,3063,3063,0,30,0
151
+ 110,基隆市,7961,15795,8125,3255,3598,195,156,274,15000,15000,0,231,107
152
+ 110,新竹市,14554,33197,15841,8654,6983,972,452,937,50745,44811,0,122,0
153
+ 110,嘉義市,7302,14883,8456,6038,6502,600,1594,375,12747,10384,0,78,207
154
+ 110,金門縣,2194,3878,1871,940,828,0,0,46,4215,4284,0,0,0
155
+ 110,連江縣,292,492,247,0,0,232,0,0,0,48,0,0,0
156
+ 111,新北市,88525,207675,87567,36341,27082,3983,282,2660,129608,125103,69,16834,252
157
+ 111,臺北市,55777,127285,63593,53909,22367,1690,376,3139,244638,234009,175,3305,583
158
+ 111,桃園市,62603,135930,59783,28878,26777,752,2852,2508,96681,107419,43,1092,321
159
+ 111,臺中市,80497,165662,75249,36005,32114,1480,5374,3438,181341,180820,0,4170,640
160
+ 111,臺南市,48170,95050,44319,21835,15310,2307,2461,1416,101751,102225,68,639,393
161
+ 111,高雄市,63808,132841,61145,31388,28889,987,3032,1767,130544,134435,118,5064,483
162
+ 111,宜蘭縣,11243,22238,11253,5297,3685,916,473,385,10345,11767,0,249,81
163
+ 111,新竹縣,19065,39663,16837,4983,4707,873,862,605,11119,14069,0,119,204
164
+ 111,苗栗縣,13742,29098,12944,6063,5488,262,775,384,17239,17071,0,155,113
165
+ 111,彰化縣,30784,62313,29093,10370,13124,2801,2293,1249,21299,21299,0,260,480
166
+ 111,南投縣,11322,22789,10915,3344,4990,991,436,420,9165,9165,86,162,105
167
+ 111,雲林縣,14710,30506,16394,6807,5802,878,1068,340,23820,24677,0,218,163
168
+ 111,嘉義縣,8811,17658,8625,2613,2277,0,462,105,23901,27830,0,75,0
169
+ 111,屏東縣,17798,35286,16731,7658,5470,644,328,518,32324,32114,0,298,141
170
+ 111,臺東縣,4907,9976,4962,546,2243,1742,140,149,5854,5958,0,12,99
171
+ 111,花蓮縣,7407,15530,7741,3538,3112,913,7,359,17143,17038,0,139,101
172
+ 111,澎湖縣,1788,3548,1697,733,1082,0,31,0,2927,2927,0,29,0
173
+ 111,基隆市,7733,16146,7644,3289,3321,170,121,224,14190,14190,0,162,92
174
+ 111,新竹市,14456,33869,15530,8716,7225,704,426,835,50067,44047,0,104,0
175
+ 111,嘉義市,7143,15123,8126,5925,6159,523,1363,310,11958,9656,0,50,203
176
+ 111,金門縣,2151,3909,1823,939,807,0,0,44,4175,4243,0,0,0
177
+ 111,連江縣,290,480,245,0,0,265,0,0,0,27,0,0,0
178
+ 112,新北市,88341,208980,85762,36496,26180,3948,254,2371,121961,117829,46,16626,243
179
+ 112,臺北市,53918,127398,62422,53653,21986,1648,456,2982,237221,228032,159,3289,572
180
+ 112,桃園市,63995,138435,58718,29096,26703,740,2731,2016,94438,104594,56,1092,310
181
+ 112,臺中市,79789,168752,73448,36378,31315,1380,5568,3166,177725,177187,0,4035,640
182
+ 112,臺南市,47927,96383,42710,21844,14793,2258,2295,1231,94711,95217,59,605,387
183
+ 112,高雄市,63390,134605,58888,31351,27449,1027,3129,1629,124633,127985,77,4930,471
184
+ 112,宜蘭縣,11251,22523,11013,5164,3434,880,422,308,9876,11221,0,247,65
185
+ 112,新竹縣,18821,40410,16537,5062,4741,1006,907,533,10596,13208,0,137,210
186
+ 112,苗栗縣,13893,29208,12557,6025,5249,195,714,325,16591,16425,0,164,106
187
+ 112,彰化縣,30602,62675,27965,10231,12599,2596,2035,1123,17722,17722,0,249,446
188
+ 112,南投縣,11253,22883,10602,3288,4731,922,320,359,9122,9122,114,156,105
189
+ 112,雲林縣,14799,30604,15454,6719,5383,769,1006,305,23049,23832,0,206,161
190
+ 112,嘉義縣,8757,17664,8216,2535,2276,0,326,98,23233,27497,0,67,0
191
+ 112,屏東縣,17854,35817,15960,7301,5100,612,279,450,30157,29958,0,331,134
192
+ 112,臺東縣,4916,9949,4752,506,2058,1650,121,129,5807,5907,0,8,90
193
+ 112,花蓮縣,7313,15595,7484,3460,2800,972,5,322,16622,16532,0,137,96
194
+ 112,澎湖縣,1822,3632,1625,693,971,0,26,0,2702,2702,0,29,0
195
+ 112,基隆市,7703,16303,7270,3292,3083,136,59,222,13813,13813,0,182,80
196
+ 112,新竹市,14584,34035,15109,8810,7432,552,378,731,49073,42827,0,111,0
197
+ 112,嘉義市,7288,15035,7760,5899,5861,470,1126,298,11727,9060,0,42,218
198
+ 112,金門縣,2087,3988,1795,939,778,0,0,41,4050,4105,0,0,0
199
+ 112,連江縣,278,496,252,0,0,269,0,0,0,54,0,0,0
200
+ 113,新北市,85532,202483,90131,36744,25707,3992,242,2340,119190,116885,28,16888,222
201
+ 113,臺北市,50990,122987,64808,53247,21907,1665,413,2968,234053,223992,172,3113,590
202
+ 113,桃園市,63421,135942,61743,29186,26245,770,2661,1844,94684,104871,54,1106,293
203
+ 113,臺中市,77511,165484,77345,36635,30749,1371,5705,3055,175722,175075,0,3970,624
204
+ 113,臺南市,46610,93966,44331,21940,14353,2291,2245,1106,91347,91947,53,564,377
205
+ 113,高雄市,61751,131156,61242,31247,26786,914,3412,1444,121965,124137,43,4698,468
206
+ 113,宜蘭縣,10954,22157,11263,5227,3292,860,405,304,9388,10659,0,237,69
207
+ 113,新竹縣,18790,39379,17585,5054,4947,1130,999,523,10980,12951,0,161,201
208
+ 113,苗栗縣,13459,28315,13092,6104,5088,106,700,310,16438,16365,0,175,108
209
+ 113,彰化縣,29864,60625,28516,10028,12375,2407,1922,928,15787,15854,0,270,435
210
+ 113,南投縣,10944,22256,10999,3191,4494,856,206,335,9073,9073,147,170,109
211
+ 113,雲林縣,14534,29618,15584,6655,5026,694,949,252,22445,23301,0,236,157
212
+ 113,嘉義縣,8639,17154,8240,2490,2221,0,224,98,23133,27250,0,59,0
213
+ 113,屏東縣,17527,34969,16427,7017,4806,565,182,390,28810,28750,0,329,142
214
+ 113,臺東縣,4739,9790,4834,519,1913,1634,111,140,5645,5833,0,11,85
215
+ 113,花蓮縣,6943,15179,7507,3455,2530,1102,16,299,15861,15839,0,120,106
216
+ 113,澎湖縣,1814,3534,1666,682,854,0,22,0,2388,2651,0,12,0
217
+ 113,基隆市,7355,15951,7408,3263,2905,127,45,185,13383,13429,0,189,90
218
+ 113,新竹市,14063,32858,15928,9046,7575,421,369,606,48901,42763,0,108,0
219
+ 113,嘉義市,6997,14593,8017,5819,5922,447,939,300,11349,8734,0,57,227
220
+ 113,金門縣,2011,3928,1877,954,803,0,0,39,3823,3866,0,0,0
221
+ 113,連江縣,268,487,257,0,0,257,0,0,0,140,0,0,0
222
+ 114,新北市,81149,194670,96668,36359,23972,4106,221,2306,117768,117768,42,16892,243
223
+ 114,臺北市,47886,117650,68231,53020,21069,1626,415,2829,230483,230483,162,3140,564
224
+ 114,桃園市,61764,132964,66607,28850,25498,760,2636,1742,93462,93462,57,1154,285
225
+ 114,臺中市,74369,161332,82767,36484,29330,1354,5575,2772,174618,174618,0,3975,562
226
+ 114,臺南市,44318,91142,47883,21507,13301,2070,2128,1012,88863,88863,56,404,375
227
+ 114,高雄市,59174,127327,65631,30498,25072,820,3435,1329,118531,118531,0,5465,466
228
+ 114,宜蘭縣,10651,21605,11843,5046,3099,865,372,291,9143,9143,0,219,74
229
+ 114,新竹縣,18409,38373,18746,5267,4847,1160,1077,489,11340,11340,0,129,194
230
+ 114,苗栗縣,12852,27368,14041,6011,4587,44,641,285,15950,15950,0,159,100
231
+ 114,彰化縣,28486,58539,29788,9769,11956,2287,1656,758,15361,15361,0,279,429
232
+ 114,南投縣,10499,21608,11428,3121,4139,803,184,318,8947,8947,160,192,111
233
+ 114,雲林縣,13953,28798,16118,6500,4573,638,776,254,22453,22453,30,250,152
234
+ 114,嘉義縣,8270,16524,8509,2467,2208,0,117,93,22510,22510,0,81,0
235
+ 114,屏東縣,16988,34245,17225,6668,4547,504,162,371,27749,27749,0,298,151
236
+ 114,臺東縣,4553,9507,5035,502,1671,1580,95,139,5391,5391,0,10,85
237
+ 114,花蓮縣,6670,14809,7708,3328,2390,1115,7,301,15029,15029,0,156,115
238
+ 114,澎湖縣,1809,3434,1757,658,799,0,26,0,2202,2202,0,22,0
239
+ 114,基隆市,7020,15403,7827,3066,2530,127,26,168,13242,13242,0,154,83
240
+ 114,新竹市,13500,31460,16964,9222,7124,416,313,573,49039,49039,0,96,0
241
+ 114,嘉義市,6655,14126,8377,5680,5782,384,879,323,11103,11103,0,64,210
242
+ 114,金門縣,1867,3886,1948,950,752,0,0,32,3660,3660,0,0,0
243
+ 114,連江縣,235,488,247,0,0,252,0,0,0,0,0,0,0
classroom_data/templates/week19/main.py ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """資料分析:讀取公開資料,查詢各縣市學生人數。"""
2
+
3
+ import csv
4
+ from pathlib import Path
5
+
6
+
7
+ DATA_FILE = Path("data/students_by_county.csv")
8
+ SAMPLE_YEAR = "114"
9
+ SAMPLE_FIELD = "國小[人]"
10
+
11
+
12
+ def load_rows(path=DATA_FILE):
13
+ """讀取 CSV;utf-8-sig 也能正確處理政府資料常見的 BOM。"""
14
+ with open(path, encoding="utf-8-sig", newline="") as file:
15
+ return list(csv.DictReader(file))
16
+
17
+
18
+ def parse_count(value):
19
+ """將有人數的欄位轉成整數;空白或非數字代表未知,不當成 0。"""
20
+ cleaned = (value or "").strip().replace(",", "")
21
+ if not cleaned or cleaned == "-":
22
+ return None
23
+ try:
24
+ return int(cleaned)
25
+ except ValueError:
26
+ return None
27
+
28
+
29
+ def count_by_county(rows, school_year, field):
30
+ """依學年度篩選,再回傳各縣市的人數排序。"""
31
+ totals = {}
32
+ for row in rows:
33
+ if row.get("學年度", "").strip() != str(school_year):
34
+ continue
35
+
36
+ count = parse_count(row.get(field))
37
+ county = row.get("縣市別", "").strip()
38
+ if county and count is not None:
39
+ totals[county] = count
40
+
41
+ return sorted(totals.items(), key=lambda item: item[1], reverse=True)
42
+
43
+
44
+ def get_count(rows, school_year, county, field):
45
+ """取得指定學年度、縣市及欄位的人數;找不到時回傳 None。"""
46
+ for row in rows:
47
+ if row.get("學年度", "").strip() != str(school_year):
48
+ continue
49
+ if row.get("縣市別", "").strip() == county:
50
+ return parse_count(row.get(field))
51
+ return None
52
+
53
+
54
+ def print_text_chart(results, limit=8):
55
+ """在 Console 印出簡單長條圖,讓每筆數字都能被核對。"""
56
+ if not results:
57
+ print("沒有符合條件的資料,請檢查學年度和欄位名稱。")
58
+ return
59
+
60
+ visible = results[:limit]
61
+ largest = max(value for _, value in visible)
62
+ print("縣市 人數 | 比例長條")
63
+ for county, value in visible:
64
+ bar_length = round(value / largest * 24) if largest else 0
65
+ print(f"{county:<6} {value:>8,} | {'█' * bar_length}")
66
+
67
+
68
+ def main():
69
+ rows = load_rows()
70
+ fields = list(rows[0]) if rows else []
71
+ print("=== 資料分析:縣市學生人數 ===")
72
+ print(f"讀入 {len(rows)} 列資料")
73
+ print(f"欄位:{', '.join(fields)}")
74
+ print()
75
+
76
+ # 範例問題:114 學年度,哪些縣市的國小學生人數最多?
77
+ question = f"{SAMPLE_YEAR} 學年度,哪些縣市的 {SAMPLE_FIELD} 最多?"
78
+ results = count_by_county(rows, SAMPLE_YEAR, SAMPLE_FIELD)
79
+ print(f"範例問題:{question}")
80
+ print_text_chart(results)
81
+
82
+ # 你的下一步:換一個學年度、教育階段或可比較的欄位,
83
+ # 先寫下預測,再執行並核對其中一筆原始資料。
84
+
85
+
86
+ if __name__ == "__main__":
87
+ main()
classroom_static/classroom.css CHANGED
@@ -199,6 +199,8 @@ body.panel-resizing { cursor:col-resize;user-select:none; }
199
  .lesson-timeline { gap:.65rem!important; }
200
  .lesson-timeline strong { color:var(--navy); }
201
  .lesson-guide-block li::marker,.lesson-details li::marker { color:var(--action);font-weight:800; }
 
 
202
  .lesson-target { padding:var(--space-md);border:0;border-radius:var(--radius-md);background:var(--action-soft); }
203
  .lesson-ai-note { padding:var(--space-md);border:0;border-radius:var(--radius-md);background:var(--help-soft); }
204
  .lesson-details { margin:0 0 var(--space-lg);border-bottom:1px solid var(--line); }
 
199
  .lesson-timeline { gap:.65rem!important; }
200
  .lesson-timeline strong { color:var(--navy); }
201
  .lesson-guide-block li::marker,.lesson-details li::marker { color:var(--action);font-weight:800; }
202
+ .lesson-source-link { display:inline-flex;min-height:44px;align-items:center;color:var(--action);font-size:.84rem;font-weight:800;text-decoration-thickness:1px;text-underline-offset:3px; }
203
+ .lesson-source-link:focus-visible { outline:2px solid var(--action);outline-offset:3px;border-radius:2px; }
204
  .lesson-target { padding:var(--space-md);border:0;border-radius:var(--radius-md);background:var(--action-soft); }
205
  .lesson-ai-note { padding:var(--space-md);border:0;border-radius:var(--radius-md);background:var(--help-soft); }
206
  .lesson-details { margin:0 0 var(--space-lg);border-bottom:1px solid var(--line); }
classroom_static/student.html CHANGED
@@ -7,7 +7,7 @@
7
  <title>Python 專案教室|學生工作區</title>
8
  <link rel="stylesheet" href="/classroom-assets/vendor/codemirror/codemirror.css">
9
  <link rel="stylesheet" href="/classroom-assets/vendor/codemirror/material-darker.css">
10
- <link rel="stylesheet" href="/classroom-assets/classroom.css?v=2026fall-20">
11
  </head>
12
  <body>
13
  <header class="app-header">
@@ -143,6 +143,6 @@
143
  <script src="/classroom-assets/vendor/codemirror/python.js"></script>
144
  <script src="/classroom-assets/vendor/jszip/jszip.min.js"></script>
145
  <script src="/classroom-assets/pygame-support.js?v=2026fall-1"></script>
146
- <script src="/classroom-assets/student.js?v=2026fall-18"></script>
147
  </body>
148
  </html>
 
7
  <title>Python 專案教室|學生工作區</title>
8
  <link rel="stylesheet" href="/classroom-assets/vendor/codemirror/codemirror.css">
9
  <link rel="stylesheet" href="/classroom-assets/vendor/codemirror/material-darker.css">
10
+ <link rel="stylesheet" href="/classroom-assets/classroom.css?v=2026fall-21">
11
  </head>
12
  <body>
13
  <header class="app-header">
 
143
  <script src="/classroom-assets/vendor/codemirror/python.js"></script>
144
  <script src="/classroom-assets/vendor/jszip/jszip.min.js"></script>
145
  <script src="/classroom-assets/pygame-support.js?v=2026fall-1"></script>
146
+ <script src="/classroom-assets/student.js?v=2026fall-19"></script>
147
  </body>
148
  </html>
classroom_static/student.js CHANGED
@@ -83,12 +83,32 @@ function initResizablePanels(){
83
  setupResizer("reference");setupResizer("console");new ResizeObserver(refreshEditorLayout).observe(E.editorPanel);window.addEventListener("resize",()=>syncPanelWidths(false));
84
  }
85
  function setLayout(referenceHidden,consoleHidden){E.studentLayout.classList.toggle("reference-hidden",referenceHidden);E.studentLayout.classList.toggle("console-hidden",consoleHidden);E.toggleReferenceButton.setAttribute("aria-pressed",String(referenceHidden));E.toggleConsoleButton.setAttribute("aria-pressed",String(consoleHidden));E.toggleReferenceButton.textContent=referenceHidden?"顯示側欄":"隱藏側欄";E.toggleConsoleButton.textContent=consoleHidden?"顯示 Console":"隱藏 Console";localStorage.setItem("classroom_hide_reference",String(referenceHidden));localStorage.setItem("classroom_hide_console",String(consoleHidden));requestAnimationFrame(()=>{syncPanelWidths(false);refreshEditorLayout()})}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86
  function renderLesson(){
87
  const item=lessonInfo(),shared=item?.workspace_id&&item.workspace_id!==item.id,isClosure=item?.id==="week05";
88
  E.templateButton.disabled=!item?.template;
89
  E.templateButton.textContent=item?.template?"匯入專案起始套版":shared?"沿用目前專案":"尚未提供套版";
90
  E.lessonHeading.textContent=item?`Lesson ${item.lesson??item.week}|${item.title}`:"本堂任務";
91
  E.lessonLabel.textContent=item?.project||"";
 
92
  E.lessonGuideContent.innerHTML=isClosure?`
93
  <section class="lesson-guide-block lesson-target"><h2>今天完成一條可玩的案件路線</h2><p>新玩家可以自己開始調查、收集線索、完成推理並看到結局。</p></section>
94
  <section class="lesson-guide-block"><h2>收尾檢查</h2><ul><li>至少三個地點、三項不同線索</li><li>能查看證物,重複調查不會重複加入</li><li>必要線索能解鎖推理和合理結局</li><li>空白或錯誤輸入不會讓程式中止</li></ul></section>
@@ -102,7 +122,7 @@ function renderLesson(){
102
  <section class="lesson-help-note"><h2>需要老師幫忙?</h2><p>按一次就會通知老師。成功呼叫後冷卻 ${helpCooldownLabel()};取消或老師回應都不會重置。</p></section>`;
103
  if(isClosure)setSideView("lessonView");
104
  }
105
- function setSideView(name){document.querySelectorAll(".side-tab").forEach(button=>button.classList.toggle("active",button.dataset.side===name));document.querySelectorAll(".side-view").forEach(view=>view.hidden=view.id!==name);if(lessonInfo()?.id==="week05"){document.querySelectorAll(".mobile-tab").forEach(button=>button.setAttribute("aria-selected",String(button.dataset.panel==="referencePanel")));document.querySelectorAll(".student-layout>.panel").forEach(panel=>panel.classList.toggle("mobile-active",panel.id==="referencePanel"))}}
106
  function setConsoleView(view){E.consoleStream.dataset.view=view;E.consoleOutputTab.classList.toggle("active",view==="console");E.pygameOutputTab.classList.toggle("active",view==="pygame");E.consoleOutputTab.setAttribute("aria-selected",String(view==="console"));E.pygameOutputTab.setAttribute("aria-selected",String(view==="pygame"))}
107
  ClassroomPygame.attach(E.pygameFrame,message=>{if(message.type==="output"&&message.text)append(message.text);if(message.type==="status"&&message.label)E.runStatusText.textContent=message.label;if(message.type==="done"){pygameRunning=false;runState("success","遊戲已結束")}if(message.type==="error"||message.type==="load_error"){pygameRunning=false;if(message.text)append(`${message.text}\n`);runState("error","Pygame 執行錯誤");setConsoleView("console")}});
108
  function initWorker(){worker?.terminate();workerReady=false;running=false;E.consoleInputForm.hidden=true;E.runButton.disabled=true;E.runButton.textContent="載入 Python…";if(!crossOriginIsolated||typeof SharedArrayBuffer==="undefined"){append("瀏覽器未啟用互動式 Python。請重新整理,並使用最新版 Chrome、Edge 或 Safari。\n");runState("error","環境不支援");return}inputBuffer=new SharedArrayBuffer(16392);inputControl=new Int32Array(inputBuffer,0,2);inputBytes=new Uint8Array(inputBuffer,8);worker=new Worker("/classroom-assets/py-worker.js?v=2026fall-4");worker.onmessage=e=>{const m=e.data;if(m.type==="ready"){workerReady=true;E.runButton.disabled=false;E.runButton.textContent="執行";runState("online","準備完成")}else if(m.type==="stream"){const decoder=m.channel==="stderr"?stderrDecoder:stdoutDecoder;append(decoder.decode(m.bytes,{stream:true}))}else if(m.type==="input_request"){E.consoleInputForm.hidden=false;E.consoleInput.value="";E.runStatusText.textContent="等待輸入";E.consoleInput.focus()}else if(m.type==="done"){E.consoleInputForm.hidden=true;runState("success","執行成功")}else if(m.type==="python_error"){E.consoleInputForm.hidden=true;if(m.text)append(`${m.text}\n`);runState("error","程式錯誤")}else if(m.type==="load_error"){append(`Python 載入失敗:${m.text}\n`);runState("error","載入失敗")}};worker.onerror=e=>{append(`Python 執行器錯誤:${e.message}\n`);runState("error","執行器錯誤")}}
 
83
  setupResizer("reference");setupResizer("console");new ResizeObserver(refreshEditorLayout).observe(E.editorPanel);window.addEventListener("resize",()=>syncPanelWidths(false));
84
  }
85
  function setLayout(referenceHidden,consoleHidden){E.studentLayout.classList.toggle("reference-hidden",referenceHidden);E.studentLayout.classList.toggle("console-hidden",consoleHidden);E.toggleReferenceButton.setAttribute("aria-pressed",String(referenceHidden));E.toggleConsoleButton.setAttribute("aria-pressed",String(consoleHidden));E.toggleReferenceButton.textContent=referenceHidden?"顯示側欄":"隱藏側欄";E.toggleConsoleButton.textContent=consoleHidden?"顯示 Console":"隱藏 Console";localStorage.setItem("classroom_hide_reference",String(referenceHidden));localStorage.setItem("classroom_hide_console",String(consoleHidden));requestAnimationFrame(()=>{syncPanelWidths(false);refreshEditorLayout()})}
86
+ function renderStructuredGuide(guide){
87
+ const bank=Array.isArray(guide.question_bank)?guide.question_bank:[];
88
+ const flow=Array.isArray(guide.flow)?guide.flow:[];
89
+ const checklist=Array.isArray(guide.checklist)?guide.checklist:[];
90
+ const rules=Array.isArray(guide.question_rules)?guide.question_rules:[];
91
+ const sourceUrl=String(guide.source_url||"");
92
+ const sourceLink=/^https:\/\//i.test(sourceUrl)?`<a class="lesson-source-link" href="${esc(sourceUrl)}" target="_blank" rel="noopener noreferrer">查看官方資料集</a>`:"";
93
+ return `
94
+ <section class="lesson-guide-block lesson-target"><h2>本週目標</h2><p>${esc(guide.goal||"")}</p></section>
95
+ ${bank.length?`<section class="lesson-guide-block"><h2>可選研究問題</h2><p>先選一題,改成你真正好奇的比較。</p><ul>${bank.map(item=>`<li><strong>${esc(item.group||"選題")}</strong> ${esc(item.text||"")}</li>`).join("")}</ul></section>`:""}
96
+ ${guide.source_title?`<section class="lesson-guide-block"><h2>共用範例資料</h2><p><strong>${esc(guide.source_title)}</strong><br>${esc(guide.source_note||"")}</p>${sourceLink}</section>`:""}
97
+ ${rules.length?`<details class="lesson-details"><summary>自訂題目檢查</summary><ul>${rules.map(item=>`<li>${esc(item)}</li>`).join("")}</ul></details>`:""}
98
+ ${flow.length?`<section class="lesson-guide-block"><h2>課堂流程|120 分鐘</h2><ol class="lesson-timeline">${flow.map(step=>`<li><strong>${esc(step.time||"")}|${esc(step.title||"")}:</strong>${esc(step.body||"")}</li>`).join("")}</ol></section>`:""}
99
+ ${checklist.length?`<section class="lesson-guide-block"><h2>完成檢查</h2><ul>${checklist.map(item=>`<li>${esc(item)}</li>`).join("")}</ul></section>`:""}
100
+ <section class="lesson-guide-block lesson-ai-note"><h2>AI 自主驗證</h2><p>${esc(guide.ai_checkpoint||"")}</p>${guide.ai_note?`<p>${esc(guide.ai_note)}</p>`:""}</section>
101
+ <section class="lesson-help-note"><h2>本週要留下的成果</h2><p>${esc(guide.submission||"")}</p></section>
102
+ <details class="lesson-details"><summary>暫存和正式提交的差別</summary><ul><li><strong>自動同步:</strong>老師可查看目前草稿,不會建立歷史版本。</li><li><strong>暫存版本:</strong>可載回的工作進度,不會交給老師或公開。</li><li><strong>正式提交:</strong>凍結一份交給老師檢查;老師另行發布後才會公開。</li></ul></details>
103
+ <section class="lesson-help-note"><h2>需要老師幫忙?</h2><p>按一次就會通知老師。成功呼叫後冷卻 ${helpCooldownLabel()};取消或老師回應都不會重置。</p></section>`;
104
+ }
105
  function renderLesson(){
106
  const item=lessonInfo(),shared=item?.workspace_id&&item.workspace_id!==item.id,isClosure=item?.id==="week05";
107
  E.templateButton.disabled=!item?.template;
108
  E.templateButton.textContent=item?.template?"匯入專案起始套版":shared?"沿用目前專案":"尚未提供套版";
109
  E.lessonHeading.textContent=item?`Lesson ${item.lesson??item.week}|${item.title}`:"本堂任務";
110
  E.lessonLabel.textContent=item?.project||"";
111
+ if(item?.guide){E.lessonGuideContent.innerHTML=renderStructuredGuide(item.guide);setSideView("lessonView");return}
112
  E.lessonGuideContent.innerHTML=isClosure?`
113
  <section class="lesson-guide-block lesson-target"><h2>今天完成一條可玩的案件路線</h2><p>新玩家可以自己開始調查、收集線索、完成推理並看到結局。</p></section>
114
  <section class="lesson-guide-block"><h2>收尾檢查</h2><ul><li>至少三個地點、三項不同線索</li><li>能查看證物,重複調查不會重複加入</li><li>必要線索能解鎖推理和合理結局</li><li>空白或錯誤輸入不會讓程式中止</li></ul></section>
 
122
  <section class="lesson-help-note"><h2>需要老師幫忙?</h2><p>按一次就會通知老師。成功呼叫後冷卻 ${helpCooldownLabel()};取消或老師回應都不會重置。</p></section>`;
123
  if(isClosure)setSideView("lessonView");
124
  }
125
+ function setSideView(name){document.querySelectorAll(".side-tab").forEach(button=>button.classList.toggle("active",button.dataset.side===name));document.querySelectorAll(".side-view").forEach(view=>view.hidden=view.id!==name);if(lessonInfo()?.id==="week05"||lessonInfo()?.guide){document.querySelectorAll(".mobile-tab").forEach(button=>button.setAttribute("aria-selected",String(button.dataset.panel==="referencePanel")));document.querySelectorAll(".student-layout>.panel").forEach(panel=>panel.classList.toggle("mobile-active",panel.id==="referencePanel"))}}
126
  function setConsoleView(view){E.consoleStream.dataset.view=view;E.consoleOutputTab.classList.toggle("active",view==="console");E.pygameOutputTab.classList.toggle("active",view==="pygame");E.consoleOutputTab.setAttribute("aria-selected",String(view==="console"));E.pygameOutputTab.setAttribute("aria-selected",String(view==="pygame"))}
127
  ClassroomPygame.attach(E.pygameFrame,message=>{if(message.type==="output"&&message.text)append(message.text);if(message.type==="status"&&message.label)E.runStatusText.textContent=message.label;if(message.type==="done"){pygameRunning=false;runState("success","遊戲已結束")}if(message.type==="error"||message.type==="load_error"){pygameRunning=false;if(message.text)append(`${message.text}\n`);runState("error","Pygame 執行錯誤");setConsoleView("console")}});
128
  function initWorker(){worker?.terminate();workerReady=false;running=false;E.consoleInputForm.hidden=true;E.runButton.disabled=true;E.runButton.textContent="載入 Python…";if(!crossOriginIsolated||typeof SharedArrayBuffer==="undefined"){append("瀏覽器未啟用互動式 Python。請重新整理,並使用最新版 Chrome、Edge 或 Safari。\n");runState("error","環境不支援");return}inputBuffer=new SharedArrayBuffer(16392);inputControl=new Int32Array(inputBuffer,0,2);inputBytes=new Uint8Array(inputBuffer,8);worker=new Worker("/classroom-assets/py-worker.js?v=2026fall-4");worker.onmessage=e=>{const m=e.data;if(m.type==="ready"){workerReady=true;E.runButton.disabled=false;E.runButton.textContent="執行";runState("online","準備完成")}else if(m.type==="stream"){const decoder=m.channel==="stderr"?stderrDecoder:stdoutDecoder;append(decoder.decode(m.bytes,{stream:true}))}else if(m.type==="input_request"){E.consoleInputForm.hidden=false;E.consoleInput.value="";E.runStatusText.textContent="等待輸入";E.consoleInput.focus()}else if(m.type==="done"){E.consoleInputForm.hidden=true;runState("success","執行成功")}else if(m.type==="python_error"){E.consoleInputForm.hidden=true;if(m.text)append(`${m.text}\n`);runState("error","程式錯誤")}else if(m.type==="load_error"){append(`Python 載入失敗:${m.text}\n`);runState("error","載入失敗")}};worker.onerror=e=>{append(`Python 執行器錯誤:${e.message}\n`);runState("error","執行器錯誤")}}