This page teaches you Python. You learn it by building one real program: an alert system for Chiang Mai. The program collects data from five sources: a rain gauge, a river gauge, a satellite, a seismometer and an archive of past disasters. It turns each one into a number, joins the numbers into one chance, and decides whether to warn people. 这个页面教你 Python。你通过做一个真实的程序来学:一个清迈的警报系统。 这个程序从五个来源收集数据:雨量计、河流水位计、卫星、地震仪,以及一份过去灾难的档案。 它把每一种数据变成一个数,把这些数合成一个几率,然后决定要不要警告人们。 หน้านี้สอน Python ให้คุณ คุณเรียนด้วยการสร้างโปรแกรมจริงหนึ่งโปรแกรม: ระบบเตือนภัยของเชียงใหม่ โปรแกรมเก็บข้อมูลจากห้าแหล่ง: เครื่องวัดฝน เครื่องวัดระดับน้ำ ดาวเทียม เครื่องวัดแผ่นดินไหว และจดหมายเหตุของภัยพิบัติในอดีต มันเปลี่ยนข้อมูลแต่ละอย่างเป็นตัวเลข รวมตัวเลขเป็นโอกาสหนึ่งค่า แล้วตัดสินใจว่าจะเตือนผู้คนหรือไม่
if.
从一个十行的文件开始:一个列表、一个循环、一个 if。
เริ่มจากไฟล์สิบบรรทัด: ลิสต์หนึ่งอัน ลูปหนึ่งอัน และ if หนึ่งอันimport, lists inside lists,
dictionaries, sets, a class.
每一步加一个 Python 的想法:函数、import、列表里的列表、字典、集合、类。
แต่ละขั้นเพิ่มแนวคิด Python หนึ่งอย่าง: ฟังก์ชัน import ลิสต์ซ้อนลิสต์ ดิกชันนารี เซต และคลาส
Twenty-four words. The first twelve are Python words, like list, loop and function. The last twelve are about the data, like gauge, score and threshold. You will meet every one of them in the code. Learn to read them and to say them first.
二十四个词。前十二个是 Python 的词,比如列表、循环和函数。后十二个是关于数据的词,比如水位计、分数和阈值。它们每一个都会出现在代码里。先学会读它们、说它们。
ยี่สิบสี่คำ สิบสองคำแรกเป็นคำของ Python เช่น ลิสต์ ลูป และฟังก์ชัน สิบสองคำหลังเป็นคำเกี่ยวกับข้อมูล เช่น เครื่องวัดระดับน้ำ คะแนน และเกณฑ์ คุณจะเจอทุกคำในโค้ด เรียนอ่านและพูดคำเหล่านี้ก่อน
Tap 🔊 say it to hear a word. Tap 📖 tell me more for a short explanation, read aloud. The translation on each card follows the 中文 and ไทย buttons. 点 🔊 say it 听这个词。点 📖 tell me more 听一段简短的解释。卡片上的翻译跟随“中文”和“ไทย”按钮。 แตะ 🔊 say it เพื่อฟังคำ แตะ 📖 tell me more เพื่อฟังคำอธิบายสั้น ๆ คำแปลบนการ์ดเปลี่ยนตามปุ่ม 中文 และ ไทย
A video of about five minutes. It shows the alert system you are going to build: the five signals, the function that joins them, and the threshold that decides. Watch it before you write any code, so you know where the code is going. A computer voice reads it, and the photographs are real.
一段约五分钟的视频。它展示你将要做的警报系统:五个信号、把它们合起来的函数,以及做决定的阈值。写代码之前先看,这样你知道代码要去哪里。配音是计算机,照片是真实的。
วิดีโอประมาณห้านาที มันแสดงระบบเตือนภัยที่คุณจะสร้าง: ห้าสัญญาณ ฟังก์ชันที่รวมมัน และเกณฑ์ที่ตัดสินใจ ดูก่อนเขียนโค้ด คุณจะได้รู้ว่าโค้ดกำลังไปทางไหน เสียงเป็นคอมพิวเตอร์ และภาพถ่ายเป็นของจริง
Watch it once now. Watch it again after part 4, and you will recognise every piece. 现在看一遍。做完第 4 部分后再看一遍,你会认出每一个部件。 ดูหนึ่งรอบตอนนี้ ดูอีกรอบหลังส่วนที่ 4 แล้วคุณจะจำทุกชิ้นส่วนได้
Seven machines. Each one is a piece of the program you will write, running here in the page. Play with a machine first. Then find the same piece in the code in part 4. The numbers are the same in both places. 七台机器。每一台都是你将要写的程序的一个部件,就在这个页面里运行。先玩机器,再到第 4 部分的代码里找同一个部件。 两处的数字是一样的。 เครื่องเจ็ดเครื่อง แต่ละเครื่องคือชิ้นส่วนหนึ่งของโปรแกรมที่คุณจะเขียน และกำลังทำงานอยู่ในหน้านี้ เล่นกับเครื่องก่อน แล้วหาชิ้นส่วนเดียวกันในโค้ดส่วนที่ 4 ตัวเลขทั้งสองที่เหมือนกัน
This is the plan of the program. Five sources give data. Five small functions turn the data into scores from zero to one. One function, called fuse, joins the scores into a chance. One function, called decide, turns the chance into an alert. The earthquake part reports alone, because nobody can forecast an earthquake.
这是程序的蓝图。五个来源提供数据。五个小函数把数据变成 0 到 1 的分数。一个叫 fuse 的函数把分数合成一个几率。一个叫 decide 的函数把几率变成警报。地震部分单独汇报,因为没有人能预报地震。
นี่คือแผนผังของโปรแกรม ห้าแหล่งให้ข้อมูล ฟังก์ชันเล็ก ๆ ห้าตัวเปลี่ยนข้อมูลเป็นคะแนนจากศูนย์ถึงหนึ่ง ฟังก์ชันชื่อ fuse รวมคะแนนเป็นโอกาสหนึ่งค่า ฟังก์ชันชื่อ decide เปลี่ยนโอกาสเป็นคำเตือน ส่วนของแผ่นดินไหวรายงานแยกต่างหาก เพราะไม่มีใครพยากรณ์แผ่นดินไหวได้
Click any box. The card tells you what it does, and which step of the code builds it. 点击任意方框。卡片会告诉你它做什么,以及代码的哪一步把它做出来。 คลิกกล่องใดก็ได้ การ์ดจะบอกว่ามันทำอะไร และขั้นไหนของโค้ดที่สร้างมัน
fuse(). What does a score of 0.5 do?fuse(). Which one, and why?Eight small Python files. Each one runs by itself. Each one adds one new idea to the file before it. The first is ten lines with no functions. The last is the complete alert system. Type them or copy them, and run every one.
八个小的 Python 文件。每一个都能单独运行。每一个都在前一个文件的基础上加一个新想法。第一个只有十行,没有函数。最后一个是完整的警报系统。把它们打出来或者复制下来,每一个都运行一遍。
ไฟล์ Python เล็ก ๆ แปดไฟล์ แต่ละไฟล์รันได้ด้วยตัวเอง แต่ละไฟล์เพิ่มแนวคิดใหม่หนึ่งอย่างจากไฟล์ก่อนหน้า ไฟล์แรกมีสิบบรรทัดและไม่มีฟังก์ชัน ไฟล์สุดท้ายคือระบบเตือนภัยที่สมบูรณ์ พิมพ์หรือคัดลอก แล้วรันทุกไฟล์
Set up once. Install Python 3. Make a folder called disaster. Save each
file in it under the name shown. Open a terminal in that folder and type python step1_rain.py.
The green box under each file is what it printed when we ran it. Yours should print the same.
只需准备一次。安装 Python 3。建一个叫 disaster 的文件夹。把每个文件按所示的名字存进去。
在这个文件夹里打开终端,输入 python step1_rain.py。每个文件下面的绿框,是我们运行它时打印出来的内容。你的应该打印出一样的。
ตั้งค่าครั้งเดียว ติดตั้ง Python 3 สร้างโฟลเดอร์ชื่อ disaster บันทึกแต่ละไฟล์ไว้ในนั้นด้วยชื่อที่แสดง
เปิดเทอร์มินัลในโฟลเดอร์นั้นแล้วพิมพ์ python step1_rain.py กล่องสีเขียวใต้แต่ละไฟล์คือสิ่งที่มันพิมพ์ตอนเรารัน ของคุณควรพิมพ์เหมือนกัน
Each step names the new Python first, then the part of the alert system it builds. If a line puzzles you, open the machine with the same name in part 3 and watch it move. 每一步先说出新的 Python,再说它做出警报系统的哪一部分。如果有一行让你困惑,打开第 3 部分里同名的机器,看它怎么动。 แต่ละขั้นบอก Python ใหม่ก่อน แล้วบอกส่วนของระบบเตือนภัยที่มันสร้าง ถ้าบรรทัดไหนทำให้งง ให้เปิดเครื่องชื่อเดียวกันในส่วนที่ 3 แล้วดูมันทำงาน
New Python: a list, for, enumerate, if and
else, print. It builds: the first rule of the alert system.
RAIN is a list of seven numbers, one for each day. for takes them one at a
time, and enumerate counts the days from 1. if chooses which line to print.
The lines that are pushed in by four spaces belong to the line above them.
新的 Python:列表、for、enumerate、if 和 else、print。
它做出:警报系统的第一条规则。RAIN 是一个有七个数的列表,每天一个。for 一次取一个,
enumerate 从 1 开始数天数。if 选择打印哪一行。缩进四个空格的那些行,属于它们上面的那一行。
Python ใหม่: ลิสต์ for enumerate if กับ else และ print
สิ่งที่สร้าง: กฎข้อแรกของระบบเตือนภัย RAIN คือลิสต์ของตัวเลขเจ็ดตัว วันละหนึ่งตัว for หยิบมาทีละตัว
และ enumerate นับวันโดยเริ่มจาก 1 if เลือกว่าจะพิมพ์บรรทัดไหน บรรทัดที่เยื้องเข้าไปสี่ช่องว่างเป็นของบรรทัดที่อยู่เหนือมัน
# step1_rain.py — one signal, one rule.
# Rain in Chiang Mai, in millimetres per day, for one week.
# If one day has heavy rain, we print a warning.
RAIN = [4, 0, 12, 31, 88, 64, 9] # mm per day, Monday to Sunday
HEAVY = 50 # 50 mm in one day is heavy rain
for day, mm in enumerate(RAIN, start=1):
if mm >= HEAVY:
print("day", day, "- heavy rain:", mm, "mm. Watch the river.")
else:
print("day", day, "- ok:", mm, "mm.")day 1 - ok: 4 mm.
day 2 - ok: 0 mm.
day 3 - ok: 12 mm.
day 4 - ok: 31 mm.
day 5 - heavy rain: 88 mm. Watch the river.
day 6 - heavy rain: 64 mm. Watch the river.
day 7 - ok: 9 mm.Change it. Set HEAVY to 30 and run the file again. Three days warn now, not two.
改一改。把 HEAVY 设为 30,再运行一次。现在有三天会警告,而不是两天。
ลองเปลี่ยน ตั้ง HEAVY เป็น 30 แล้วรันไฟล์อีกครั้ง ตอนนี้เตือนสามวัน ไม่ใช่สองวัน
New Python: def, return, a docstring, range and
len, and main(). It builds: a score for the rain and a score for the
river. A function is a named piece of code. ramp() returns 0 below low, 1
above high, and a straight line between them. rain_score() and
river_score() call it with their own numbers. Now rain in millimetres and a river in
metres can be compared, because both are scores from 0 to 1.
新的 Python:def、return、文档字符串、range 和 len,以及 main()。
它做出:雨量的分数和河水的分数。函数是一段有名字的代码。ramp() 在 low 以下返回 0,
在 high 以上返回 1,两者之间是一条直线。rain_score() 和 river_score() 用各自的数字去调用它。
现在以毫米计的雨和以米计的河可以比较了,因为两者都是 0 到 1 的分数。
Python ใหม่: def return docstring range กับ len และ main()
สิ่งที่สร้าง: คะแนนของฝนและคะแนนของแม่น้ำ ฟังก์ชันคือโค้ดชิ้นหนึ่งที่มีชื่อ ramp() ส่งกลับ 0 เมื่อต่ำกว่า low
ส่งกลับ 1 เมื่อสูงกว่า high และเป็นเส้นตรงระหว่างนั้น rain_score() และ river_score() เรียกมันด้วยตัวเลขของตัวเอง
ตอนนี้ฝนเป็นมิลลิเมตรกับแม่น้ำเป็นเมตรเทียบกันได้แล้ว เพราะทั้งคู่เป็นคะแนนจาก 0 ถึง 1
# step2_river.py — two signals, and a score for each one.
# A score is a number from 0 to 1. 0 means calm. 1 means danger.
# The file is growing, so it gets a shape: functions, and main().
RAIN = [4, 0, 12, 31, 88, 64, 9] # mm per day
RIVER = [2.4, 2.4, 2.5, 2.9, 3.6, 4.1, 3.9] # metres at gauge P.1, Nawarat Bridge
CRITICAL = 3.70 # at 3.70 m the Irrigation Office warns
def ramp(value, low, high):
"""0 below low, 1 above high, a straight line between them."""
if value <= low:
return 0.0
if value >= high:
return 1.0
return (value - low) / (high - low)
def rain_score(mm):
"""20 mm is nothing. 80 mm in one day is full danger."""
return ramp(mm, 20, 80)
def river_score(metres):
"""3.0 m is calm. At the critical level the score is 1."""
return ramp(metres, 3.0, CRITICAL)
def main():
for day in range(len(RAIN)):
rain = rain_score(RAIN[day])
river = river_score(RIVER[day])
print(f"day {day + 1}: rain {rain:.2f} river {river:.2f}")
if __name__ == "__main__":
main()day 1: rain 0.00 river 0.00
day 2: rain 0.00 river 0.00
day 3: rain 0.00 river 0.00
day 4: rain 0.18 river 0.00
day 5: rain 1.00 river 0.86
day 6: rain 0.73 river 1.00
day 7: rain 0.00 river 1.00Why main()? Python runs a file from the top. With main(), the top of
the file only gives names to things. The last two lines start the program. Later, another file can
import your functions without starting it.
为什么要有 main()?Python 从上往下运行文件。有了 main(),文件的上半部分只是给东西起名字。
最后两行才启动程序。以后别的文件可以导入你的函数,而不会把程序启动。
ทำไมต้องมี main()? Python รันไฟล์จากบนลงล่าง เมื่อมี main() ส่วนบนของไฟล์แค่ตั้งชื่อให้สิ่งต่าง ๆ
สองบรรทัดสุดท้ายเป็นตัวเริ่มโปรแกรม ภายหลังไฟล์อื่นจึงนำเข้าฟังก์ชันของคุณได้โดยไม่เริ่มโปรแกรม
New Python: an index made by arithmetic, levels[day - 1], and an early
return. It builds: the forecast. levels[day] is today and
levels[day - 1] is yesterday. forecast() subtracts them to get today's climb,
and adds the climb to today. Day 0 has no yesterday, so the function returns early for it.
新的 Python:用算术得到的索引 levels[day - 1],以及提前 return。它做出:预报。
levels[day] 是今天,levels[day - 1] 是昨天。forecast() 把它们相减得到今天的涨幅,再把涨幅加到今天上。
第 0 天没有昨天,所以函数对它提前返回。
Python ใหม่: ดัชนีที่ได้จากการคำนวณ levels[day - 1] และการ return ก่อนกำหนด
สิ่งที่สร้าง: การพยากรณ์ levels[day] คือวันนี้ และ levels[day - 1] คือเมื่อวาน
forecast() ลบกันเพื่อหาการขึ้นของวันนี้ แล้วบวกการขึ้นนั้นเข้ากับวันนี้ วันที่ 0 ไม่มีเมื่อวาน ฟังก์ชันจึงส่งค่ากลับก่อน
# step3_forecast.py — history makes a forecast.
# The river does not jump. It climbs. The climb today tells us about tomorrow.
# New here: forecast() and forecast_score().
RAIN = [4, 0, 12, 31, 88, 64, 9] # mm per day
RIVER = [2.4, 2.4, 2.5, 2.9, 3.6, 4.1, 3.9] # metres at gauge P.1, Nawarat Bridge
CRITICAL = 3.70 # at 3.70 m the Irrigation Office warns
def ramp(value, low, high):
"""0 below low, 1 above high, a straight line between them."""
if value <= low:
return 0.0
if value >= high:
return 1.0
return (value - low) / (high - low)
def rain_score(mm):
"""20 mm is nothing. 80 mm in one day is full danger."""
return ramp(mm, 20, 80)
def river_score(metres):
"""3.0 m is calm. At the critical level the score is 1."""
return ramp(metres, 3.0, CRITICAL)
def forecast(levels, day):
"""Tomorrow's level, if the river climbs the same as it did today."""
if day == 0:
return levels[0]
climb = levels[day] - levels[day - 1]
return levels[day] + climb
def forecast_score(metres):
"""Water enters the first streets at 3.9 m, so a forecast of 3.9 m is full danger."""
return ramp(metres, 3.3, 3.9)
def main():
for day in range(len(RAIN)):
tomorrow = forecast(RIVER, day)
print(f"day {day + 1}: river {RIVER[day]:.2f} m "
f"forecast {tomorrow:.2f} m score {forecast_score(tomorrow):.2f}")
if __name__ == "__main__":
main()day 1: river 2.40 m forecast 2.40 m score 0.00
day 2: river 2.40 m forecast 2.40 m score 0.00
day 3: river 2.50 m forecast 2.60 m score 0.00
day 4: river 2.90 m forecast 3.30 m score 0.00
day 5: river 3.60 m forecast 4.30 m score 1.00
day 6: river 4.10 m forecast 4.60 m score 1.00
day 7: river 3.90 m forecast 3.70 m score 0.67Change it. Make forecast() add only half of the climb. Run it. One day gets a
different score. Which one?
改一改。让 forecast() 只加上涨幅的一半,再运行。有一天的分数变了,是哪一天?
ลองเปลี่ยน ให้ forecast() บวกแค่ครึ่งหนึ่งของการขึ้น แล้วรัน มีหนึ่งวันที่คะแนนเปลี่ยน วันไหน?
New Python: import math, math.log10(), max() and
min(), and a list of pairs. It builds: the earthquake signal. Distance divided by
speed is time, so two divisions give the arrival of each wave. intensity() turns a
magnitude and a distance into a rough shaking, and max and min keep it
between 1 and 12. Each pair, like ("Bangkok", 1000), gives the loop a name and a distance.
新的 Python:import math、math.log10()、max() 和 min(),以及一个由“对”组成的列表。
它做出:地震信号。距离除以速度就是时间,所以两次除法给出每种波的到达时间。intensity() 把震级和距离变成大致的烈度,
max 和 min 把它限制在 1 到 12 之间。每一对,比如 ("Bangkok", 1000),给循环一个名字和一个距离。
Python ใหม่: import math math.log10() max() กับ min() และลิสต์ของคู่ค่า
สิ่งที่สร้าง: สัญญาณแผ่นดินไหว ระยะทางหารด้วยความเร็วคือเวลา การหารสองครั้งจึงให้เวลาที่แต่ละคลื่นมาถึง
intensity() เปลี่ยนขนาดและระยะทางเป็นความแรงของการสั่นแบบคร่าว ๆ และ max กับ min คุมให้อยู่ระหว่าง 1 ถึง 12
แต่ละคู่ เช่น ("Bangkok", 1000) ให้ชื่อและระยะทางแก่ลูป
# step4_shake.py — the earthquake signal.
# A seismometer hears two waves. The P wave is fast and small. The S wave comes
# later and does the damage. The gap between them says how far away the quake is.
# New here: distance_km(), warning_seconds(), intensity() and shake_score().
import math
P_SPEED = 5.0 # km per second, a P wave in granite
S_SPEED = 3.0 # km per second, about 60 percent of the P wave
KM_PER_GAP_SECOND = 7.5 # from the two speeds: 5 x 3 / (5 - 3). Many books round it to 8.
def ramp(value, low, high):
"""0 below low, 1 above high, a straight line between them."""
if value <= low:
return 0.0
if value >= high:
return 1.0
return (value - low) / (high - low)
def distance_km(gap_seconds):
"""Seconds between the P wave and the S wave, times 7.5, is the distance."""
return gap_seconds * KM_PER_GAP_SECOND
def warning_seconds(km):
"""The P wave arrives first. The time before the S wave is the warning time."""
return km / S_SPEED - km / P_SPEED
def intensity(magnitude, km):
"""How strong the shaking feels, from 1 to 12.
A simple classroom rule, fitted to the 2025 quake. Real agencies use better ones."""
value = 1.5 * magnitude - 3 * math.log10(max(km, 1)) + 1.5
return max(1.0, min(12.0, value))
def shake_score(magnitude, km):
"""Intensity 3 is a weak shake. Intensity 8 breaks buildings."""
return ramp(intensity(magnitude, km), 3, 8)
def main():
# 28 March 2025, magnitude 7.7, Sagaing Fault, Myanmar
magnitude = 7.7
for city, km in [("Mandalay", 16), ("Chiang Mai", 480), ("Bangkok", 1000)]:
print(f"{city:11} {km:5} km shaking {intensity(magnitude, km):4.1f} "
f"warning {warning_seconds(km):5.1f} s "
f"score {shake_score(magnitude, km):.2f}")
# A seismometer in Chiang Mai hears the P wave, then the S wave 64 seconds later.
print("P to S gap 64 s -> about", round(distance_km(64)), "km away")
if __name__ == "__main__":
main()Mandalay 16 km shaking 9.4 warning 2.1 s score 1.00
Chiang Mai 480 km shaking 5.0 warning 64.0 s score 0.40
Bangkok 1000 km shaking 4.1 warning 133.3 s score 0.21
P to S gap 64 s -> about 480 km awayAn honest limit. intensity() is a classroom rule. We fitted it to the 2025
earthquake, where Chiang Mai felt about 5. Bangkok also felt 5, and the rule says 4.1, because it
knows nothing about soft ground.
老实说它的局限。intensity() 是一条课堂规则。我们按 2025 年的地震调过它,当时清迈感到约为 5。
曼谷也感到 5,而规则给出的是 4.1,因为它完全不知道软地面的事。
ข้อจำกัดที่ต้องบอกตรง ๆ intensity() เป็นกฎในห้องเรียน เราปรับมันให้เข้ากับแผ่นดินไหวปี 2025 ที่เชียงใหม่รู้สึกได้ประมาณ 5
กรุงเทพฯ ก็รู้สึกได้ 5 แต่กฎบอก 4.1 เพราะมันไม่รู้อะไรเกี่ยวกับพื้นดินอ่อน
New Python: a list of lists, zip(), one loop inside another, abs(),
and a list comprehension. It builds: the satellite signal. BEFORE is a list of six
rows, and each row is a list of six numbers. zip() walks through two lists side by side.
The inner loop compares one old cell with one new cell. abs() makes the difference positive.
新的 Python:列表的列表、zip()、循环里的循环、abs(),以及列表推导式。它做出:卫星信号。
BEFORE 是六行组成的列表,每一行又是六个数的列表。zip() 并排走过两个列表。内层循环拿一个旧格子和一个新格子比较。
abs() 把差值变成正数。
Python ใหม่: ลิสต์ของลิสต์ zip() ลูปซ้อนลูป abs() และ list comprehension
สิ่งที่สร้าง: สัญญาณดาวเทียม BEFORE คือลิสต์ของหกแถว และแต่ละแถวคือลิสต์ของตัวเลขหกตัว
zip() เดินผ่านสองลิสต์ไปพร้อมกัน ลูปชั้นในเทียบช่องเก่าหนึ่งช่องกับช่องใหม่หนึ่งช่อง abs() ทำให้ผลต่างเป็นบวก
# step5_satellite.py — the satellite signal.
# A radar satellite takes a picture before and a picture after. Water looks dark.
# We subtract the two pictures. Every cell that changed a lot is "changed".
# New here: changed_fraction() and satellite_score().
# Two tiny radar pictures, 6 by 6 cells. 9 is bright (dry land), 0 is dark (water).
BEFORE = [
[7, 8, 7, 6, 8, 7],
[8, 7, 2, 6, 7, 8], # the river is the dark 2 in the middle
[7, 6, 2, 7, 8, 7],
[8, 7, 2, 6, 7, 6],
[7, 8, 3, 7, 8, 7],
[8, 7, 2, 7, 7, 8],
]
AFTER = [
[7, 8, 7, 6, 8, 7],
[8, 2, 1, 2, 7, 8], # the dark water is wider now
[7, 1, 1, 2, 2, 7],
[2, 1, 1, 2, 1, 6],
[7, 2, 1, 1, 2, 7],
[8, 7, 2, 2, 7, 8],
]
BIG_CHANGE = 4 # a cell that changed by 4 or more is a real change, not noise
def ramp(value, low, high):
"""0 below low, 1 above high, a straight line between them."""
if value <= low:
return 0.0
if value >= high:
return 1.0
return (value - low) / (high - low)
def changed_fraction(before, after):
"""What part of the picture changed? 0.0 is nothing, 1.0 is everything."""
changed = 0
total = 0
for row_before, row_after in zip(before, after):
for old, new in zip(row_before, row_after):
total += 1
if abs(new - old) >= BIG_CHANGE:
changed += 1
return changed / total
def satellite_score(fraction):
"""5 percent changed is noise. 30 percent changed is a flood."""
return ramp(fraction, 0.05, 0.30)
def main():
fraction = changed_fraction(BEFORE, AFTER)
print(f"changed: {fraction:.0%} of the picture")
print(f"satellite score: {satellite_score(fraction):.2f}")
for row_before, row_after in zip(BEFORE, AFTER):
pairs = zip(row_before, row_after)
marks = ["#" if abs(new - old) >= BIG_CHANGE else "." for old, new in pairs]
print(" ".join(marks))
if __name__ == "__main__":
main()changed: 36% of the picture
satellite score: 1.00
. . . . . .
. # . # . .
. # . # # .
# # . # # .
. # . # # .
. . . # . .Change it. Set BIG_CHANGE to 1, then to 8. Compare the map of # marks
with the satellite machine in part 3.
改一改。把 BIG_CHANGE 设为 1,再设为 8。把 # 组成的图和第 3 部分的卫星机器比一比。
ลองเปลี่ยน ตั้ง BIG_CHANGE เป็น 1 แล้วเป็น 8 เทียบแผนที่ของเครื่องหมาย # กับเครื่องดาวเทียมในส่วนที่ 3
New Python: a dictionary, a set, string methods like .lower() and
.split(), & between sets, and sort() with a key.
It builds: the archive search. ARCHIVE is a list of nine dictionaries, and
record["year"] reads one value by its name. words() turns a sentence into a
set, which keeps each word once. & gives the words that two sets share.
新的 Python:字典、集合、.lower() 和 .split() 这样的字符串方法、集合之间的 &,
以及带 key 的 sort()。它做出:档案搜索。ARCHIVE 是九个字典组成的列表,record["year"] 按名字读出一个值。
words() 把一句话变成集合,每个词只保留一次。& 给出两个集合共有的词。
Python ใหม่: ดิกชันนารี เซต เมธอดของสตริงอย่าง .lower() และ .split() เครื่องหมาย & ระหว่างเซต
และ sort() ที่มี key สิ่งที่สร้าง: การค้นจดหมายเหตุ ARCHIVE คือลิสต์ของดิกชันนารีเก้าอัน
และ record["year"] อ่านค่าหนึ่งค่าด้วยชื่อของมัน words() เปลี่ยนประโยคเป็นเซต ซึ่งเก็บแต่ละคำไว้ครั้งเดียว
& ให้คำที่สองเซตมีร่วมกัน
# step6_archive.py — the history signal.
# An archive of past disasters. We search it with today's words.
# A past day that shares many words with today is a warning.
# New here: words(), search() and archive_score().
# Words that every text shares. They say nothing, so the search ignores them.
STOP_WORDS = {"the", "and", "for", "with", "from"}
ARCHIVE = [
{"year": 2004, "kind": "tsunami", "place": "Khao Lak and Phuket",
"text": "26 December 2004. The sea pulled back from the beach. Then big waves came. "
"About 5,400 people died in Thailand. There was no warning system."},
{"year": 2005, "kind": "flood", "place": "Chiang Mai",
"text": "A big flood hit Chiang Mai city. River water entered the streets."},
{"year": 2011, "kind": "flood", "place": "Central Thailand",
"text": "Heavy monsoon rain from July to January. 815 people died. 65 provinces "
"flooded. Factories north of Bangkok were under water."},
{"year": 2011, "kind": "flood", "place": "Chiang Mai",
"text": "A big flood hit Chiang Mai city again. The Ping River rose over "
"its banks."},
{"year": 2014, "kind": "earthquake", "place": "Mae Lao, Chiang Rai",
"text": "5 May 2014. Magnitude 6.1. One person died. Temples cracked. "
"The strongest earthquake ever recorded in Thailand."},
{"year": 2024, "kind": "flood", "place": "Mae Sai, Chiang Rai",
"text": "September 2024. Rain from Typhoon Yagi. Mae Sai town flooded. "
"More than 1,000 homes were damaged."},
{"year": 2024, "kind": "flood", "place": "Chiang Mai",
"text": "26 September 2024. Heavy rain for days. The Ping River rose over its "
"banks at 4.93 metres. Markets and roads flooded."},
{"year": 2024, "kind": "flood", "place": "Chiang Mai",
"text": "5 and 6 October 2024. The Ping River reached 5.30 metres at Nawarat "
"Bridge, the highest in 50 years. Night Bazaar was under a metre of water."},
{"year": 2025, "kind": "earthquake", "place": "Myanmar and Thailand",
"text": "28 March 2025. Magnitude 7.7 on the Sagaing Fault. Felt in Chiang Mai. "
"A tower under construction fell in Bangkok."},
]
def ramp(value, low, high):
"""0 below low, 1 above high, a straight line between them."""
if value <= low:
return 0.0
if value >= high:
return 1.0
return (value - low) / (high - low)
def words(text):
"""The words in a text: lower case, three letters or more, and not a stop word."""
cleaned = "".join(letter.lower() if letter.isalnum() else " " for letter in text)
kept = (word for word in cleaned.split() if len(word) >= 3)
return {word for word in kept if word not in STOP_WORDS}
def search(query, archive):
"""Every record with the count of words it shares with the query, best first."""
query_words = words(query)
matches = []
for record in archive:
record_text = record["text"] + " " + record["place"] + " " + record["kind"]
shared = query_words & words(record_text)
matches.append((len(shared), record, sorted(shared)))
matches.sort(key=lambda match: match[0], reverse=True)
return matches
def archive_score(shared_words):
"""One shared word is chance. Four shared words is a real match."""
return ramp(shared_words, 1, 4)
def main():
query = "heavy rain for days, the river is rising, markets near the river"
print("query:", query)
for count, record, shared in search(query, ARCHIVE)[:3]:
print(f" {count} shared {record['year']} {record['kind']:10} "
f"{record['place']:22} {shared}")
best_count = search(query, ARCHIVE)[0][0]
print(f"archive score: {archive_score(best_count):.2f}")
if __name__ == "__main__":
main()query: heavy rain for days, the river is rising, markets near the river
5 shared 2024 flood Chiang Mai ['days', 'heavy', 'markets', 'rain', 'river']
2 shared 2011 flood Central Thailand ['heavy', 'rain']
1 shared 2005 flood Chiang Mai ['river']
archive score: 1.00Change it. Search for “the ground shook in Chiang Mai”. The first result is a flood, not the earthquake. Counting words is not understanding. Which word would you add to fix it? 改一改。搜索 “the ground shook in Chiang Mai”。第一条结果是一场洪水,而不是地震。数词并不等于理解。你会加上哪个词来修正它? ลองเปลี่ยน ค้นหา “the ground shook in Chiang Mai” ผลลัพธ์แรกเป็นน้ำท่วม ไม่ใช่แผ่นดินไหว การนับคำไม่ใช่ความเข้าใจ คุณจะเพิ่มคำไหนเพื่อแก้?
New Python: class, __init__, self, methods,
None, and a dictionary that holds functions. It builds: the part that joins the
signals. Signal("rain", 98) makes one signal. __init__ runs at that moment
and stores the kind and the value on self. score() looks up the right
function in SCORERS. fuse() loops over a list of signals and multiplies the
odds. For the morning of 24 September it prints 38%.
新的 Python:class、__init__、self、方法、None,以及一个装着函数的字典。
它做出:把信号合起来的那一部分。Signal("rain", 98) 造出一个信号。__init__ 在这一刻运行,
把种类和数值存到 self 上。score() 在 SCORERS 里查到合适的函数。
fuse() 循环走过信号的列表,把赔率相乘。对 9 月 24 日的早晨,它打印出 38%。
Python ใหม่: class __init__ self เมธอด None และดิกชันนารีที่เก็บฟังก์ชัน
สิ่งที่สร้าง: ส่วนที่รวมสัญญาณเข้าด้วยกัน Signal("rain", 98) สร้างสัญญาณหนึ่งอัน __init__ ทำงานตอนนั้น
และเก็บชนิดกับค่าไว้ที่ self score() ค้นฟังก์ชันที่ถูกต้องใน SCORERS
fuse() วนผ่านลิสต์ของสัญญาณและคูณอัตราต่อรอง สำหรับเช้าวันที่ 24 กันยายน มันพิมพ์ 38%
# step7_fuse.py — one class for every signal, and a coordinator that joins them.
# Five signals, five scores. The coordinator does not add them. It multiplies.
# New here: the Signal class, fuse() and decide().
CRITICAL = 3.70 # at 3.70 m the Irrigation Office warns the city
PRIOR = 0.10 # our starting guess for a flood tomorrow, before we look at any signal
def ramp(value, low, high):
"""0 below low, 1 above high, a straight line between them."""
if value <= low:
return 0.0
if value >= high:
return 1.0
return (value - low) / (high - low)
def rain_score(mm):
return ramp(mm, 20, 80)
def river_score(metres):
return ramp(metres, 3.0, CRITICAL)
def forecast_score(metres):
return ramp(metres, 3.3, 3.9)
def satellite_score(fraction):
return ramp(fraction, 0.05, 0.30)
def archive_score(shared_words):
return ramp(shared_words, 1, 4)
# The kind of a signal picks the scorer. Same class, different job.
SCORERS = {"rain": rain_score, "river": river_score, "forecast": forecast_score,
"satellite": satellite_score, "archive": archive_score}
class Signal:
"""One signal. The kind says how its value becomes a score."""
def __init__(self, kind, value):
self.kind = kind
self.value = value
def score(self):
"""0 is calm, 1 is danger. A missing value is 0.5: it says nothing."""
if self.value is None:
return 0.5
return SCORERS[self.kind](self.value)
def weight(self):
"""How much this signal multiplies the odds. Score 0.5 gives x1: no change."""
return 10 ** (2 * self.score() - 1)
def fuse(signals, prior):
"""Start with the prior. Multiply the odds by every signal. Give back a chance."""
odds = prior / (1 - prior)
for signal in signals:
odds = odds * signal.weight()
return odds / (1 + odds)
def decide(chance, threshold):
if chance >= threshold:
return "RED - warn people now"
if chance >= threshold / 2:
return "YELLOW - watch closely"
return "GREEN - calm"
def main():
# The morning of 24 September 2024 in Chiang Mai. No satellite picture yet.
signals = [Signal("rain", 98), Signal("river", 3.34), Signal("forecast", 3.63),
Signal("satellite", None), Signal("archive", 2)]
for signal in signals:
print(f"{signal.kind:10} value {str(signal.value):>5} "
f"score {signal.score():.2f} odds x{signal.weight():.2f}")
chance = fuse(signals, PRIOR)
print(f"chance of a flood tomorrow: {chance:.0%}")
print(decide(chance, 0.5))
if __name__ == "__main__":
main()rain value 98 score 1.00 odds x10.00
river value 3.34 score 0.49 odds x0.94
forecast value 3.63 score 0.55 odds x1.26
satellite value None score 0.50 odds x1.00
archive value 2 score 0.33 odds x0.46
chance of a flood tomorrow: 38%
YELLOW - watch closelyOne class, not five. The kind is handed in when you make the signal, and the class finds the
right function. In main(), signals is an ordinary list. You already know how
to loop over a list.
一个类,而不是五个。种类是在造信号时传进去的,类自己找到合适的函数。在 main() 里,signals 是一个普通的列表。
你已经会循环走过一个列表了。
คลาสเดียว ไม่ใช่ห้า ชนิดถูกส่งเข้ามาตอนสร้างสัญญาณ และคลาสหาฟังก์ชันที่ถูกต้องเอง ใน main()
signals เป็นลิสต์ธรรมดา คุณรู้วิธีวนลูปผ่านลิสต์อยู่แล้ว
New Python: elif, four counters, and a function that returns four values at once.
It builds: the finished alert system. This file puts steps 2 to 7 together and adds thirty days
of data. evaluate() asks two questions about every day: did the program warn, and did the
flood come? Then main() prints a table for four thresholds, the alert for the morning of
4 October 2024, and a report for the earthquake of 28 March 2025.
新的 Python:elif、四个计数器,以及一次返回四个值的函数。它做出:完成的警报系统。
这个文件把第 2 步到第 7 步合在一起,再加上三十天的数据。evaluate() 对每一天问两个问题:程序警告了吗?洪水来了吗?
然后 main() 打印四个阈值的表格、2024 年 10 月 4 日早晨的警报,以及 2025 年 3 月 28 日地震的报告。
Python ใหม่: elif ตัวนับสี่ตัว และฟังก์ชันที่ส่งกลับสี่ค่าพร้อมกัน สิ่งที่สร้าง: ระบบเตือนภัยที่เสร็จสมบูรณ์
ไฟล์นี้รวมขั้นที่ 2 ถึง 7 เข้าด้วยกันและเพิ่มข้อมูลสามสิบวัน evaluate() ถามสองคำถามกับทุกวัน: โปรแกรมเตือนไหม และน้ำท่วมมาไหม?
จากนั้น main() พิมพ์ตารางสำหรับสี่เกณฑ์ คำเตือนของเช้าวันที่ 4 ตุลาคม 2024 และรายงานแผ่นดินไหววันที่ 28 มีนาคม 2025
# disaster_agent.py — the whole agent, from the eight steps of the Disaster Lab.
# Five flood signals go into one coordinator. The earthquake signal is reported on
# its own, because a quake cannot be forecast, only measured.
# New here: thirty days of data from 2024, and evaluate(), which tests the agent.
#
# python disaster_agent.py
import math
CRITICAL = 3.70 # at 3.70 m at gauge P.1 the Irrigation Office warns
FLOOD_LEVEL = 3.90 # water enters the first streets at +3.90 m
PRIOR = 0.10 # our starting guess for a flood tomorrow, before any signal
P_SPEED = 5.0 # km per second, a P wave in granite
S_SPEED = 3.0 # km per second, about 60 percent of the P wave
# Chiang Mai, 10 September to 9 October 2024. River levels marked * were reported
# in the news (see the page). The other days are filled in to join them, and the
# rain is a teaching series, not a measurement.
DATE = ["10 Sep", "11 Sep", "12 Sep", "13 Sep", "14 Sep", "15 Sep", "16 Sep", "17 Sep",
"18 Sep", "19 Sep", "20 Sep", "21 Sep", "22 Sep", "23 Sep", "24 Sep", "25 Sep",
"26 Sep", "27 Sep", "28 Sep", "29 Sep", "30 Sep", "1 Oct", "2 Oct", "3 Oct",
"4 Oct", "5 Oct", "6 Oct", "7 Oct", "8 Oct", "9 Oct"]
RAIN = [2, 0, 8, 14, 3, 0, 22, 6, 0, 11, 4, 18, 36, 64, 98, 40,
12, 5, 0, 2, 0, 9, 28, 55, 120, 35, 6, 0, 0, 3]
RIVER = [2.30, 2.28, 2.30, 2.35, 2.33, 2.30, 2.38, 2.40, 2.36, 2.40, 2.42, 2.55,
2.80, 3.05, 3.34, 4.32, # 24 Sep 3.34* at 10:00, 25 Sep 4.32* forecast peak
4.93, 4.40, 3.90, 3.55, 3.30, 3.15, 3.20, 3.60, # 26 Sep 4.93* over the banks
4.40, 5.27, 5.30, 4.70, 3.80, 3.40] # 5 Oct 5.27*, 6 Oct 5.30*, 8 Oct 3.80*
BEFORE = [
[7, 8, 7, 6, 8, 7],
[8, 7, 2, 6, 7, 8],
[7, 6, 2, 7, 8, 7],
[8, 7, 2, 6, 7, 6],
[7, 8, 3, 7, 8, 7],
[8, 7, 2, 7, 7, 8],
]
AFTER = [
[7, 8, 7, 6, 8, 7],
[8, 2, 1, 2, 7, 8],
[7, 1, 1, 2, 2, 7],
[2, 1, 1, 2, 1, 6],
[7, 2, 1, 1, 2, 7],
[8, 7, 2, 2, 7, 8],
]
BIG_CHANGE = 4
# Words that every text shares. They say nothing, so the search ignores them.
STOP_WORDS = {"the", "and", "for", "with", "from"}
ARCHIVE = [
{"year": 2004, "kind": "tsunami", "place": "Khao Lak and Phuket",
"text": "26 December 2004. The sea pulled back from the beach. Then big waves came. "
"About 5,400 people died in Thailand. There was no warning system."},
{"year": 2005, "kind": "flood", "place": "Chiang Mai",
"text": "A big flood hit Chiang Mai city. River water entered the streets."},
{"year": 2011, "kind": "flood", "place": "Central Thailand",
"text": "Heavy monsoon rain from July to January. 815 people died. 65 provinces "
"flooded. Factories north of Bangkok were under water."},
{"year": 2011, "kind": "flood", "place": "Chiang Mai",
"text": "A big flood hit Chiang Mai city again. The Ping River rose over "
"its banks."},
{"year": 2014, "kind": "earthquake", "place": "Mae Lao, Chiang Rai",
"text": "5 May 2014. Magnitude 6.1. One person died. Temples cracked. "
"The strongest earthquake ever recorded in Thailand."},
{"year": 2024, "kind": "flood", "place": "Mae Sai, Chiang Rai",
"text": "September 2024. Rain from Typhoon Yagi. Mae Sai town flooded. "
"More than 1,000 homes were damaged."},
{"year": 2024, "kind": "flood", "place": "Chiang Mai",
"text": "26 September 2024. Heavy rain for days. The Ping River rose over its "
"banks at 4.93 metres. Markets and roads flooded."},
{"year": 2024, "kind": "flood", "place": "Chiang Mai",
"text": "5 and 6 October 2024. The Ping River reached 5.30 metres at Nawarat "
"Bridge, the highest in 50 years. Night Bazaar was under a metre of water."},
{"year": 2025, "kind": "earthquake", "place": "Myanmar and Thailand",
"text": "28 March 2025. Magnitude 7.7 on the Sagaing Fault. Felt in Chiang Mai. "
"A tower under construction fell in Bangkok."},
]
def ramp(value, low, high):
"""0 below low, 1 above high, a straight line between them."""
if value <= low:
return 0.0
if value >= high:
return 1.0
return (value - low) / (high - low)
# --- the five flood signals ---
def rain_score(mm):
return ramp(mm, 20, 80)
def river_score(metres):
return ramp(metres, 3.0, CRITICAL)
def forecast(levels, day):
"""Tomorrow's level, if the river climbs the same as it did today."""
if day == 0:
return levels[0]
climb = levels[day] - levels[day - 1]
return levels[day] + climb
def forecast_score(metres):
return ramp(metres, 3.3, 3.9)
def changed_fraction(before, after):
"""What part of the picture changed? 0.0 is nothing, 1.0 is everything."""
changed = 0
total = 0
for row_before, row_after in zip(before, after):
for old, new in zip(row_before, row_after):
total += 1
if abs(new - old) >= BIG_CHANGE:
changed += 1
return changed / total
def satellite_score(fraction):
return ramp(fraction, 0.05, 0.30)
def words(text):
"""The words in a text: lower case, three letters or more, and not a stop word."""
cleaned = "".join(letter.lower() if letter.isalnum() else " " for letter in text)
kept = (word for word in cleaned.split() if len(word) >= 3)
return {word for word in kept if word not in STOP_WORDS}
def search(query, archive):
"""Every record with the count of words it shares with the query, best first."""
query_words = words(query)
matches = []
for record in archive:
record_text = record["text"] + " " + record["place"] + " " + record["kind"]
shared = query_words & words(record_text)
matches.append((len(shared), record, sorted(shared)))
matches.sort(key=lambda match: match[0], reverse=True)
return matches
def archive_score(shared_words):
return ramp(shared_words, 1, 4)
SCORERS = {"rain": rain_score, "river": river_score, "forecast": forecast_score,
"satellite": satellite_score, "archive": archive_score}
# --- the earthquake signal, reported on its own ---
def warning_seconds(km):
"""The P wave arrives first. The time before the S wave is the warning time."""
return km / S_SPEED - km / P_SPEED
def intensity(magnitude, km):
"""How strong the shaking feels, from 1 to 12. A simple classroom rule."""
value = 1.5 * magnitude - 3 * math.log10(max(km, 1)) + 1.5
return max(1.0, min(12.0, value))
def quake_report(magnitude, km):
shaking = intensity(magnitude, km)
if shaking >= 8:
advice = "RED - buildings can fall"
elif shaking >= 6:
advice = "YELLOW - things fall, walls crack"
else:
advice = "GREEN - felt, little damage"
return (f"earthquake M{magnitude}, {km} km away: shaking {shaking:.1f}, "
f"warning {warning_seconds(km):.0f} s. {advice}")
# --- the coordinator ---
class Signal:
"""One signal. The kind says how its value becomes a score."""
def __init__(self, kind, value):
self.kind = kind
self.value = value
def score(self):
"""0 is calm, 1 is danger. A missing value is 0.5: it says nothing."""
if self.value is None:
return 0.5
return SCORERS[self.kind](self.value)
def weight(self):
"""How much this signal multiplies the odds. Score 0.5 gives x1: no change."""
return 10 ** (2 * self.score() - 1)
def fuse(signals, prior):
"""Start with the prior. Multiply the odds by every signal. Give back a chance."""
odds = prior / (1 - prior)
for signal in signals:
odds = odds * signal.weight()
return odds / (1 + odds)
def decide(chance, threshold):
if chance >= threshold:
return "RED - warn people now"
if chance >= threshold / 2:
return "YELLOW - watch closely"
return "GREEN - calm"
def evaluate(threshold):
"""Replay the thirty days. Each day, decide for tomorrow. Then look at tomorrow."""
hits = false_alarms = misses = quiet = 0
for day in range(1, len(RIVER) - 1):
signals = [Signal("rain", RAIN[day]), Signal("river", RIVER[day]),
Signal("forecast", forecast(RIVER, day))]
alert = fuse(signals, PRIOR) >= threshold
flood = RIVER[day + 1] >= FLOOD_LEVEL
if alert and flood:
hits += 1
elif alert:
false_alarms += 1
elif flood:
misses += 1
else:
quiet += 1
return hits, false_alarms, misses, quiet
def main():
print("threshold hits false alarms misses quiet")
for threshold in [0.3, 0.5, 0.7, 0.9]:
hits, false_alarms, misses, quiet = evaluate(threshold)
print(f" {threshold:.1f} {hits:5} {false_alarms:12} {misses:6} {quiet:5}")
# The morning of 4 October 2024 in Chiang Mai: every signal we have.
today = DATE.index("4 Oct")
query = "heavy rain for days, the river is rising, markets near the river"
best_count = search(query, ARCHIVE)[0][0]
signals = [Signal("rain", RAIN[today]), Signal("river", RIVER[today]),
Signal("forecast", forecast(RIVER, today)),
Signal("satellite", changed_fraction(BEFORE, AFTER)),
Signal("archive", best_count)]
print()
print("4 Oct 2024, morning")
for signal in signals:
print(f" {signal.kind:10} score {signal.score():.2f} "
f"odds x{signal.weight():.2f}")
chance = fuse(signals, PRIOR)
print(f" chance of a flood tomorrow: {chance:.0%} -> {decide(chance, 0.5)}")
# 28 March 2025, 13:20 in Chiang Mai: the Sagaing earthquake, 480 km away.
print()
print("28 Mar 2025, 13:20")
print(" " + quake_report(7.7, 480))
if __name__ == "__main__":
main()threshold hits false alarms misses quiet
0.3 8 1 0 19
0.5 7 1 1 19
0.7 4 0 4 20
0.9 1 0 7 20
4 Oct 2024, morning
rain score 1.00 odds x10.00
river score 1.00 odds x10.00
forecast score 1.00 odds x10.00
satellite score 1.00 odds x10.00
archive score 1.00 odds x10.00
chance of a flood tomorrow: 100% -> RED - warn people now
28 Mar 2025, 13:20
earthquake M7.7, 480 km away: shaking 5.0, warning 64 s. GREEN - felt, little damageRead the table. At 0.3 the program misses nothing and gives one false alarm. At 0.9 it gives no false alarm and misses seven floods. No threshold gives zero of both. 读这张表。在 0.3 时程序一次都不漏,有一次误报。在 0.9 时没有误报,却漏掉七次洪水。没有哪个阈值能让两者都为零。 อ่านตารางนี้ ที่ 0.3 โปรแกรมไม่พลาดเลยและเตือนผิดหนึ่งครั้ง ที่ 0.9 ไม่เตือนผิดเลยแต่พลาดน้ำท่วมเจ็ดครั้ง ไม่มีเกณฑ์ไหนที่ทำให้ทั้งสองอย่างเป็นศูนย์
About the data. The river levels marked * in the code come from news reports of
the P.1 gauge: 3.34 m on 24 September, 4.93 m on 26 September, 5.27 m and 5.30 m on 5 and 6 October,
and 3.80 m on 8 October. The 4.32 m on 25 September was the Irrigation Office's forecast. The other
days are filled in to join them. The rain list is a teaching series, not a measurement. Part 5 gives
you real daily rain from a weather record.
关于数据。代码里标 * 的水位来自对 P.1 水位站的新闻报道:9 月 24 日 3.34 米,9 月 26 日 4.93 米,
10 月 5 日和 6 日 5.27 米和 5.30 米,10 月 8 日 3.80 米。9 月 25 日的 4.32 米是灌溉局的预报。其余的日子是为了把它们连起来而补上的。
雨量列表是教学用的序列,不是测量值。第 5 部分会给你来自气象记录的真实日雨量。
เกี่ยวกับข้อมูล ระดับน้ำที่มีเครื่องหมาย * ในโค้ดมาจากรายงานข่าวของสถานี P.1: 3.34 ม. วันที่ 24 กันยายน
4.93 ม. วันที่ 26 กันยายน 5.27 ม. และ 5.30 ม. วันที่ 5 และ 6 ตุลาคม และ 3.80 ม. วันที่ 8 ตุลาคม ค่า 4.32 ม. วันที่ 25 กันยายนคือค่าพยากรณ์ของสำนักงานชลประทาน
วันอื่น ๆ เติมเข้าไปเพื่อเชื่อมค่าเหล่านี้ ลิสต์ของฝนเป็นชุดข้อมูลสำหรับสอน ไม่ใช่ค่าที่วัดจริง ส่วนที่ 5 จะให้ฝนรายวันจริงจากบันทึกสภาพอากาศ
So far your program has used one month of sample data. Now it can read today. A small file called live data asks four free websites for numbers. None of them needs a key. A Jupyter notebook then runs the whole lesson again, from the first line of Python to an alert for today.
到现在为止,你的程序用的是一个月的样例数据。现在它可以读“今天”了。一个叫 live_data 的小文件向四个免费的网站要数字,它们都不需要密钥。然后一个 Jupyter 笔记本把整节课重新跑一遍,从 Python 的第一行一直到今天的警报。
จนถึงตอนนี้โปรแกรมของคุณใช้ข้อมูลตัวอย่างหนึ่งเดือน ตอนนี้มันอ่านวันนี้ได้แล้ว ไฟล์เล็ก ๆ ชื่อ live_data ขอตัวเลขจากเว็บไซต์ฟรีสี่แห่ง ไม่มีแห่งใดต้องใช้คีย์ จากนั้นโน้ตบุ๊ก Jupyter จะรันบทเรียนทั้งหมดอีกครั้ง ตั้งแต่บรรทัดแรกของ Python ไปจนถึงคำเตือนสำหรับวันนี้
An API is a web address that answers with data, not with a page. Python asks with
requests.get(). The answer arrives as lists and dictionaries, the shapes you already know.
API 是一个网址,它用数据来回答,而不是用网页。Python 用 requests.get() 去问。
答案以列表和字典的形式到来,正是你已经认识的形状。
API คือที่อยู่เว็บที่ตอบกลับเป็นข้อมูล ไม่ใช่หน้าเว็บ Python ถามด้วย requests.get()
คำตอบมาเป็นลิสต์และดิกชันนารี ซึ่งเป็นรูปแบบที่คุณรู้จักแล้ว
rain_days() · Open-Meteo weatherriver_days() · Open-Meteo flood APIquakes() · USGSevents() · NASA EONETNew Python: requests.get(), JSON, default values for inputs, and a file with no
main(). It builds: the collector. This file is a module: it does nothing when
you run it. Other code imports it and calls its functions. The notebook does exactly that. It needs one
install: pip install requests.
新的 Python:requests.get()、JSON、输入的默认值,以及一个没有 main() 的文件。
它做出:数据收集器。这个文件是一个模块:你运行它时它什么也不做。别的代码导入它并调用它的函数。笔记本做的正是这件事。
它需要安装一样东西:pip install requests。
Python ใหม่: requests.get() JSON ค่าเริ่มต้นของข้อมูลเข้า และไฟล์ที่ไม่มี main()
สิ่งที่สร้าง: ตัวเก็บข้อมูล ไฟล์นี้เป็นโมดูล: เมื่อรันมันจะไม่ทำอะไร โค้ดอื่นนำเข้ามันและเรียกฟังก์ชันของมัน โน้ตบุ๊กทำแบบนั้นพอดี
ต้องติดตั้งหนึ่งอย่าง: pip install requests
# live_data.py — four free sources of live signals for the Disaster Lab. No API keys.
#
# from live_data import rain_days, river_days, quakes, events
#
# Every function asks one website and gives back plain Python: lists of (date, value)
# or lists of dictionaries. The notebook scores them with the same functions it uses
# for the sample data.
#
# rain_days() Open-Meteo weather: daily rain, the past days and a forecast
# river_days() Open-Meteo flood API: river flow from the GloFAS model, in m3/s
# quakes() USGS: earthquakes in the last hours, anywhere in the world
# events() NASA EONET: floods, storms and quakes that are open or recent
#
# pip install requests
import datetime
import requests
CHIANG_MAI = (18.79, 98.98) # latitude, longitude
PING_GLOFAS = (18.70, 99.02) # a GloFAS grid cell on the Ping below the city
HEADERS = {"User-Agent": "krueng-disaster-lab/1.0 (educational)"}
def get_json(url, params):
"""Ask a website for JSON. Raises an error if the website says no."""
reply = requests.get(url, params=params, headers=HEADERS, timeout=30)
reply.raise_for_status()
return reply.json()
def rain_days(place=CHIANG_MAI, past=7, future=7):
"""Daily rain in mm as (date, mm) pairs: past days measured, future days forecast."""
lat, lon = place
daily = get_json("https://api.open-meteo.com/v1/forecast",
{"latitude": lat, "longitude": lon, "daily": "precipitation_sum",
"past_days": past, "forecast_days": future,
"timezone": "Asia/Bangkok"})["daily"]
return list(zip(daily["time"], daily["precipitation_sum"]))
def rain_history(place, start, end):
"""Daily rain in mm as (date, mm) pairs for past dates (ERA5 reanalysis)."""
lat, lon = place
daily = get_json("https://archive-api.open-meteo.com/v1/archive",
{"latitude": lat, "longitude": lon, "start_date": start,
"end_date": end, "daily": "precipitation_sum",
"timezone": "Asia/Bangkok"})["daily"]
return list(zip(daily["time"], daily["precipitation_sum"]))
def river_days(place=PING_GLOFAS, past=30, future=7):
"""River flow in m3/s as (date, flow) pairs from the GloFAS model, not a gauge."""
lat, lon = place
daily = get_json("https://flood-api.open-meteo.com/v1/flood",
{"latitude": lat, "longitude": lon, "daily": "river_discharge",
"past_days": past, "forecast_days": future})["daily"]
return list(zip(daily["time"], daily["river_discharge"]))
def quakes(hours=24, min_magnitude=4.5):
"""Earthquakes from the USGS in the last hours, biggest first.
Each one is a dictionary: time, magnitude, place, lat, lon, depth_km."""
now = datetime.datetime.now(datetime.timezone.utc)
since = now - datetime.timedelta(hours=hours)
found = get_json("https://earthquake.usgs.gov/fdsnws/event/1/query",
{"format": "geojson", "starttime": since.strftime("%Y-%m-%dT%H:%M"),
"minmagnitude": min_magnitude, "orderby": "magnitude"})
result = []
for feature in found["features"]:
facts = feature["properties"]
lon, lat, depth = feature["geometry"]["coordinates"]
seconds = facts["time"] / 1000
when = datetime.datetime.fromtimestamp(seconds, datetime.timezone.utc)
result.append({"time": when.strftime("%Y-%m-%d %H:%M UTC"),
"magnitude": facts["mag"], "place": facts["place"],
"lat": lat, "lon": lon, "depth_km": depth})
return result
def events(days=30, kinds="floods,earthquakes,severeStorms,landslides"):
"""Natural events from NASA EONET, newest first.
Each one is a dictionary: title, kind, date, lat, lon."""
found = get_json("https://eonet.gsfc.nasa.gov/api/v3/events",
{"status": "all", "days": days, "category": kinds, "limit": 500})
result = []
for event in found["events"]:
shape = event["geometry"][-1]
lat, lon = centre(shape)
result.append({"title": event["title"], "kind": event["categories"][0]["title"],
"date": shape["date"][:10], "lat": lat, "lon": lon})
return result
def centre(shape):
"""(lat, lon) of an EONET shape: the point itself, or the middle of a polygon's ring.
EONET writes its points as [lon, lat] but its flood polygons as [lat, lon]. Checked on
1 October 2026 against a flood in Tak and a fire in Brazil."""
if shape["type"] == "Point":
lon, lat = shape["coordinates"]
return lat, lon
ring = shape["coordinates"][0]
lats = [point[0] for point in ring]
lons = [point[1] for point in ring]
return sum(lats) / len(lats), sum(lons) / len(lons)An honest limit. There is no public API for the real P.1 gauge. So the notebook uses the model river, and asks a different question: how many times the usual flow for this month is today's flow? 老实说它的局限。真正的 P.1 水位站没有公开的 API。所以笔记本用模型河流,并换了一个问题:今天的流量是这个月通常流量的多少倍? ข้อจำกัดที่ต้องบอกตรง ๆ สถานี P.1 ตัวจริงไม่มี API สาธารณะ โน้ตบุ๊กจึงใช้แม่น้ำจากโมเดล และถามอีกคำถามหนึ่ง: อัตราการไหลวันนี้เป็นกี่เท่าของอัตราปกติของเดือนนี้?
disaster_lab.ipynb teaches the same Python in twelve short parts. It reads
the thirty days from a csv file and the archive from a json file, so you also learn to open files. Its
last part imports live_data.py and prints an alert for today. Every output in it really
ran. Open it in Jupyter, or upload it to Google Colab.
disaster_lab.ipynb 用十二个短小的部分教同样的 Python。它从 csv 文件读那三十天,从 json 文件读档案,所以你也学会了打开文件。
它的最后一部分导入 live_data.py,并打印今天的警报。里面每一个输出都是真正运行出来的。用 Jupyter 打开它,或者上传到 Google Colab。
disaster_lab.ipynb สอน Python เดียวกันในสิบสองส่วนสั้น ๆ มันอ่านข้อมูลสามสิบวันจากไฟล์ csv และอ่านจดหมายเหตุจากไฟล์ json
คุณจึงได้เรียนการเปิดไฟล์ด้วย ส่วนสุดท้ายนำเข้า live_data.py และพิมพ์คำเตือนสำหรับวันนี้ ทุกผลลัพธ์ในนั้นรันจริง
เปิดใน Jupyter หรืออัปโหลดไป Google Colabrain_mm
is the teaching series from part 4. rain_era5_mm is real daily rain for Chiang Mai from
the ERA5 weather record. The notebook asks you which one to trust.
csv 里故意有两列雨量。rain_mm 是第 4 部分的教学序列。rain_era5_mm 是来自 ERA5 气象记录的清迈真实日雨量。
笔记本会问你该相信哪一列。
ไฟล์ csv มีคอลัมน์ฝนสองคอลัมน์โดยตั้งใจ rain_mm คือชุดข้อมูลสำหรับสอนจากส่วนที่ 4
rain_era5_mm คือฝนรายวันจริงของเชียงใหม่จากบันทึกสภาพอากาศ ERA5 โน้ตบุ๊กจะถามคุณว่าควรเชื่อคอลัมน์ไหนforecast() could read the
rain as well as the river. satellite_score() could read real Sentinel-1 pictures.
search() could use embeddings and a vector database. fuse() and
decide() would stay exactly as they are. That is what functions are for.
保持这个形状,一次只改一个函数。forecast() 可以在读河水的同时也读雨量。satellite_score() 可以读真正的哨兵-1 图像。
search() 可以用嵌入向量和向量数据库。fuse() 和 decide() 完全不用变。函数的用处就在这里。
คงรูปร่างไว้ แล้วเปลี่ยนทีละฟังก์ชัน forecast() อาจอ่านฝนไปพร้อมกับแม่น้ำ satellite_score() อาจอ่านภาพ Sentinel-1 จริง
search() อาจใช้เวกเตอร์ฝังตัวและฐานข้อมูลเวกเตอร์ fuse() และ decide() จะยังเหมือนเดิมทุกอย่าง
ฟังก์ชันมีไว้เพื่อสิ่งนี้Three about Python, three about the alert system. 三条关于 Python,三条关于警报系统。 สามข้อเกี่ยวกับ Python และสามข้อเกี่ยวกับระบบเตือนภัย
You have built an alert system in Python. Thank you for working through it, and see you in the next lab. 你已经用 Python 做出了一个警报系统。感谢你完成这一课,我们下个实验室见。 คุณสร้างระบบเตือนภัยด้วย Python แล้ว ขอบคุณที่ทำจนจบ แล้วพบกันในห้องทดลองถัดไป
Every photograph on this page shows a real place or a real instrument. All of them come from Wikimedia Commons under free licences. The diagrams are drawn by the page's own code. No picture here was made by an image generator. 本页的每一张照片拍的都是真实的地方或真实的仪器。它们全部来自维基共享资源,使用自由许可。图表由页面自己的代码绘制。 这里没有任何图片是由图像生成器制作的。 ภาพถ่ายทุกภาพในหน้านี้แสดงสถานที่จริงหรือเครื่องมือจริง ทั้งหมดมาจาก Wikimedia Commons ภายใต้สัญญาอนุญาตแบบเสรี แผนภาพวาดโดยโค้ดของหน้านี้เอง ไม่มีภาพใดที่นี่สร้างโดยโปรแกรมสร้างภาพ
Where the numbers come from. The critical level of 3.70 m at P.1, the six flood zones and the readings of September 2024: warnings from Irrigation Office 1, as reported by Thairath and Naewna. The readings of 4.93 m, 5.23 m and 5.27 m, and the flooded districts: The Nation and Thairath. The record of 5.30 m and the comparison with fifty years: Chiang Mai Citylife. The 2025 earthquake, the 2004 tsunami in Thailand, the 2011 floods, the 2014 Mae Lao earthquake, seismic waves, early warning, the Mercalli scale, InSAR, Sentinel-1, flood forecasting, naive Bayes and the confusion matrix: the English Wikipedia articles with those names, read on 1 October 2026. The distance of about 480 km to Chiang Mai is our own calculation from the coordinates of the epicentre. The live data in part 5 comes from Open-Meteo, the USGS and NASA EONET. 数字的来源。P.1 的警戒水位 3.70 米、六个洪水区和 2024 年 9 月的读数:第一灌溉局的警告,由泰叻报和 Naewna 报道。 4.93 米、5.23 米和 5.27 米的读数以及被淹的街区:The Nation 和泰叻报。5.30 米的纪录和与五十年的比较:Chiang Mai Citylife。 2025 年地震、2004 年泰国海啸、2011 年洪水、2014 年湄劳地震、地震波、预警、麦加利烈度、InSAR、哨兵-1、洪水预报、朴素贝叶斯和混淆矩阵: 同名的英文维基百科条目,于 2026 年 10 月 1 日阅读。到清迈约 480 公里的距离是我们根据震中坐标自己算的。 第 5 部分的实时数据来自 Open-Meteo、美国地质调查局和 NASA EONET。 แหล่งที่มาของตัวเลข ระดับวิกฤต 3.70 ม. ที่ P.1 หกโซนน้ำท่วม และค่าที่วัดได้เดือนกันยายน 2024: ประกาศเตือนของสำนักงานชลประทานที่ 1 รายงานโดยไทยรัฐและแนวหน้า ค่า 4.93 ม. 5.23 ม. และ 5.27 ม. และย่านที่ถูกน้ำท่วม: The Nation และไทยรัฐ สถิติ 5.30 ม. และการเทียบกับห้าสิบปี: Chiang Mai Citylife แผ่นดินไหว 2025 สึนามิ 2004 ในประเทศไทย น้ำท่วม 2011 แผ่นดินไหวแม่ลาว 2014 คลื่นแผ่นดินไหว การเตือนภัยล่วงหน้า มาตราเมอร์คัลลี InSAR, Sentinel-1 การพยากรณ์น้ำท่วม naive Bayes และ confusion matrix: บทความวิกิพีเดียภาษาอังกฤษชื่อเดียวกัน อ่านเมื่อ 1 ตุลาคม 2026 ระยะทางประมาณ 480 กม. ถึงเชียงใหม่เป็นการคำนวณของเราเองจากพิกัดของจุดศูนย์กลาง ข้อมูลสดในส่วนที่ 5 มาจาก Open-Meteo, USGS และ NASA EONET