1 · The words 2 · Watch 3 · See it run 4 · Write the code 5 · Go live

🐍 Disaster Lab — learn Python by building an alert system for Chiang Mai

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 ให้คุณ คุณเรียนด้วยการสร้างโปรแกรมจริงหนึ่งโปรแกรม: ระบบเตือนภัยของเชียงใหม่ โปรแกรมเก็บข้อมูลจากห้าแหล่ง: เครื่องวัดฝน เครื่องวัดระดับน้ำ ดาวเทียม เครื่องวัดแผ่นดินไหว และจดหมายเหตุของภัยพิบัติในอดีต มันเปลี่ยนข้อมูลแต่ละอย่างเป็นตัวเลข รวมตัวเลขเป็นโอกาสหนึ่งค่า แล้วตัดสินใจว่าจะเตือนผู้คนหรือไม่

What you will do

  • Start with a file of ten lines: one list, one loop, one if. 从一个十行的文件开始:一个列表、一个循环、一个 if。 เริ่มจากไฟล์สิบบรรทัด: ลิสต์หนึ่งอัน ลูปหนึ่งอัน และ if หนึ่งอัน
  • Add one Python idea in each step: functions, import, lists inside lists, dictionaries, sets, a class. 每一步加一个 Python 的想法:函数、import、列表里的列表、字典、集合、类。 แต่ละขั้นเพิ่มแนวคิด Python หนึ่งอย่าง: ฟังก์ชัน import ลิสต์ซ้อนลิสต์ ดิกชันนารี เซต และคลาส
  • Finish with one complete program. It reads five signals, prints the chance of a flood, and tests itself on thirty days from the 2024 flood. 最后得到一个完整的程序。它读五个信号,打印洪水的几率,并用 2024 年洪水的三十天数据测试自己。 จบด้วยโปรแกรมที่สมบูรณ์หนึ่งโปรแกรม มันอ่านห้าสัญญาณ พิมพ์โอกาสน้ำท่วม และทดสอบตัวเองกับข้อมูลสามสิบวันจากน้ำท่วมปี 2024
  • Then collect live data: rain, river flow, earthquakes and disaster reports, from four free sources on the internet. 然后收集实时数据:雨量、河流流量、地震和灾害报告,来自互联网上四个免费的来源。 แล้วเก็บข้อมูลสด: ฝน อัตราการไหลของแม่น้ำ แผ่นดินไหว และรายงานภัยพิบัติ จากสี่แหล่งฟรีบนอินเทอร์เน็ต

What you need

  • No Python before this. The code starts from the first line and explains every new word. 不需要学过 Python。代码从第一行开始,每个新词都有解释。 ไม่ต้องรู้ Python มาก่อน โค้ดเริ่มจากบรรทัดแรกและอธิบายคำใหม่ทุกคำ
  • A computer with Python 3 for part 4. Parts 1 to 3 work on a phone. 第 4 部分需要一台装有 Python 3 的电脑。第 1 到第 3 部分在手机上就能用。 ส่วนที่ 4 ต้องใช้คอมพิวเตอร์ที่มี Python 3 ส่วนที่ 1 ถึง 3 ใช้บนโทรศัพท์ได้
  • Simple English. Press 中文 or ไทย at the top to see a translation under each paragraph. 简单的英语。按上面的“中文”或“ไทย”,每段下面会出现翻译。 ภาษาอังกฤษง่าย ๆ กด 中文 หรือ ไทย ด้านบนเพื่อดูคำแปลใต้แต่ละย่อหน้า
The page has five parts. Work through them in order. 本页有五个部分。请按顺序做。 หน้านี้มีห้าส่วน ทำตามลำดับ
  1. The words. Twenty-four words to read and say: twelve from Python, twelve about the data. 词汇。二十四个要读、要说的词:十二个来自 Python,十二个关于数据。 คำศัพท์ ยี่สิบสี่คำให้อ่านและพูด: สิบสองคำจาก Python และสิบสองคำเกี่ยวกับข้อมูล
  2. Watch. The finished system in five minutes. 观看。五分钟看完做好的系统。 ดู ระบบที่เสร็จแล้วในห้านาที
  3. See it run. Seven machines. Each one is a piece of the program, running in this page. 看它运行。七台机器。每一台都是程序的一个部件,就在本页里运行。 ดูมันทำงาน เครื่องเจ็ดเครื่อง แต่ละเครื่องคือชิ้นส่วนหนึ่งของโปรแกรม ทำงานอยู่ในหน้านี้
  4. Write the code. Eight steps, eight files. Every one runs. 写代码。八步,八个文件。每一个都能运行。 เขียนโค้ด แปดขั้น แปดไฟล์ ทุกไฟล์รันได้
  5. Go live. A notebook, and four sources of live data. 接上实时数据。一个笔记本,以及四个实时数据来源。 ใช้ข้อมูลสด โน้ตบุ๊กหนึ่งเล่ม และแหล่งข้อมูลสดสี่แหล่ง
The Ping River in Chiang Mai, seen from Nawarat Bridge: brown water between tree-lined banks under a bright sky
The river your program watches. The Ping at Nawarat Bridge in Chiang Mai. The river gauge called P.1 is here. Its readings from September and October 2024 are in your sample data. On the night of 5 to 6 October 2024 it read 5.30 metres, the highest in about fifty years. 你的程序要盯着的这条河。清迈纳瓦拉桥边的平河。叫做 P.1 的水位站就在这里。 它在 2024 年 9 月和 10 月的读数就在你的样例数据里。2024 年 10 月 5 日到 6 日的夜里,它读到 5.30 米,约五十年来最高。 แม่น้ำที่โปรแกรมของคุณเฝ้าดู แม่น้ำปิงที่สะพานนวรัฐ เชียงใหม่ สถานีวัดน้ำชื่อ P.1 อยู่ตรงนี้ ค่าที่มันวัดได้ในเดือนกันยายนและตุลาคม 2024 อยู่ในข้อมูลตัวอย่างของคุณ คืนวันที่ 5 ถึง 6 ตุลาคม 2024 มันวัดได้ 5.30 เมตร สูงที่สุดในรอบประมาณห้าสิบปี Christophe95, CC BY-SA 4.0, via Wikimedia Commons.

🔤 Part 1 · Twenty-four words

Summary

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 เพื่อฟังคำอธิบายสั้น ๆ คำแปลบนการ์ดเปลี่ยนตามปุ่ม 中文 และ ไทย

🎬 Part 2 · The finished system in five minutes — subtitles in English, 中文 and ไทย

Summary

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 แล้วคุณจะจำทุกชิ้นส่วนได้

About five minutes. The voice is a computer, not a person. The photographs are real and the diagrams are drawn by this page. Choose subtitles in the player's own menu. The video is a tour of the program, not a source of facts: the sources are at the foot of the page. 约五分钟。配音是计算机,不是真人。照片是真实的,图表由本页绘制。字幕在播放器自己的菜单里选。 视频是程序的导览,不是事实的来源:来源在页面底部。 ประมาณห้านาที เสียงเป็นคอมพิวเตอร์ ไม่ใช่คน ภาพถ่ายเป็นของจริง และแผนภาพวาดโดยหน้านี้ เลือกคำบรรยายในเมนูของเครื่องเล่น วิดีโอเป็นการพาชมโปรแกรม ไม่ใช่แหล่งข้อเท็จจริง: แหล่งข้อมูลอยู่ท้ายหน้า

🔬 Part 3 · See it run

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 ตัวเลขทั้งสองที่เหมือนกัน

🗺️ The whole program on one screen

Summary

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. 点击任意方框。卡片会告诉你它做什么,以及代码的哪一步把它做出来。 คลิกกล่องใดก็ได้ การ์ดจะบอกว่ามันทำอะไร และขั้นไหนของโค้ดที่สร้างมัน

🌧️ rain gauge 🌊 river gauge 🛰️ satellite 🌍 seismometer 📚 archive rain_score() river_score()forecast_score() satellite_score() intensity() archive_score() fuse()× × × × decide() quake_report() data → score functions → fuse() → decide() · the earthquake reports alone
Try this:
  • Click the five boxes on the left. Each is one kind of data.
  • Click fuse(). What does a score of 0.5 do?
  • One box is not joined to fuse(). Which one, and why?
试试这些:点左边的五个方框,每一个是一种数据。点 fuse(),0.5 的分数会做什么?有一个方框没有连到 fuse(),是哪一个,为什么? ลองดู: คลิกกล่องห้ากล่องทางซ้าย แต่ละกล่องคือข้อมูลหนึ่งชนิด คลิก fuse() คะแนน 0.5 ทำอะไร? มีกล่องหนึ่งที่ไม่ได้ต่อกับ fuse() กล่องไหน และทำไม?

Why the program reads five kinds of data

One source can be wrong. Leaves can block a rain gauge. Cloud can hide a flood from a camera. A river gauge can break in the flood it is measuring. So the program never trusts one number. It reads five kinds of data and asks whether they agree. That is also why you learn five kinds of Python here: numbers, lists, grids, text, and your own class. 一个来源可能出错。树叶会堵住雨量计。云会把洪水挡在相机之外。河流水位计可能在它正测量的洪水里坏掉。 所以程序从不只相信一个数。它读五种数据,看它们是否一致。这也是你在这里要学五种 Python 的原因:数字、列表、网格、文本,以及你自己的类。 แหล่งข้อมูลเดียวอาจผิดได้ ใบไม้อุดเครื่องวัดฝนได้ เมฆบังน้ำท่วมจากกล้องได้ เครื่องวัดระดับน้ำอาจพังในน้ำท่วมที่มันกำลังวัด โปรแกรมจึงไม่เชื่อตัวเลขเดียว มันอ่านข้อมูลห้าชนิดแล้วถามว่าตรงกันไหม นั่นคือเหตุผลที่คุณเรียน Python ห้าแบบที่นี่ด้วย: ตัวเลข ลิสต์ ตาราง ข้อความ และคลาสของคุณเอง

🐍 Part 4 · Write the code: eight steps, eight files

Summary

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 แล้วดูมันทำงาน

1 A list, a loop and an if

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
# 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.")
python step1_rain.py
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 แล้วรันไฟล์อีกครั้ง ตอนนี้เตือนสามวัน ไม่ใช่สองวัน

2 Functions, and a file with a shape

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
# 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()
python step2_river.py
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.00

Why 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() ส่วนบนของไฟล์แค่ตั้งชื่อให้สิ่งต่าง ๆ สองบรรทัดสุดท้ายเป็นตัวเริ่มโปรแกรม ภายหลังไฟล์อื่นจึงนำเข้าฟังก์ชันของคุณได้โดยไม่เริ่มโปรแกรม

3 A function that looks back in a list

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
# 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()
python step3_forecast.py
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.67

Change it. Make forecast() add only half of the climb. Run it. One day gets a different score. Which one? 改一改。让 forecast() 只加上涨幅的一半,再运行。有一天的分数变了,是哪一天? ลองเปลี่ยน ให้ forecast() บวกแค่ครึ่งหนึ่งของการขึ้น แล้วรัน มีหนึ่งวันที่คะแนนเปลี่ยน วันไหน?

4 import, and maths

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
# 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()
python step4_shake.py
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 away

An 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 เพราะมันไม่รู้อะไรเกี่ยวกับพื้นดินอ่อน

5 Lists inside lists

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
# 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()
python step5_satellite.py
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

6 Strings, sets and dictionaries

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
# 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()
python step6_archive.py
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.00

Change 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” ผลลัพธ์แรกเป็นน้ำท่วม ไม่ใช่แผ่นดินไหว การนับคำไม่ใช่ความเข้าใจ คุณจะเพิ่มคำไหนเพื่อแก้?

7 A class

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
# 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()
python step7_fuse.py
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 closely

One 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 เป็นลิสต์ธรรมดา คุณรู้วิธีวนลูปผ่านลิสต์อยู่แล้ว

8 The whole program, and a test

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
# 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()
python disaster_agent.py
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 damage

Read 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 จะให้ฝนรายวันจริงจากบันทึกสภาพอากาศ

🌐 Part 5 · Go live: collect data from four sources

Summary

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 weather

Daily rain for any place on Earth: the past week, and a forecast for the next seven days. 地球上任何地方的日雨量:过去一周,以及未来七天的预报。 ฝนรายวันของที่ใดก็ได้บนโลก: สัปดาห์ที่ผ่านมา และพยากรณ์เจ็ดวันข้างหน้า

river_days() · Open-Meteo flood API

River flow in cubic metres per second from the GloFAS model. It is a model, not the P.1 gauge, so its shape is right and its scale is not. 来自 GloFAS 模型的河流流量,单位是立方米每秒。它是模型,不是 P.1 水位站,所以形状是对的,数值的尺度不是。 อัตราการไหลของแม่น้ำเป็นลูกบาศก์เมตรต่อวินาทีจากโมเดล GloFAS มันเป็นโมเดล ไม่ใช่สถานี P.1 รูปร่างจึงถูกต้อง แต่ขนาดของตัวเลขไม่ใช่

quakes() · USGS

Earthquakes from the last hours, anywhere in the world, with magnitude, place and depth. 过去几小时里世界任何地方的地震,带有震级、地点和深度。 แผ่นดินไหวในชั่วโมงที่ผ่านมา ที่ใดก็ได้ในโลก พร้อมขนาด สถานที่ และความลึก

events() · NASA EONET

Floods, storms and other natural events reported in the last days. 最近几天报告的洪水、风暴和其他自然事件。 น้ำท่วม พายุ และเหตุการณ์ทางธรรมชาติอื่น ๆ ที่รายงานในช่วงวันที่ผ่านมา

9 A module that asks the internet

New 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
# 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 สาธารณะ โน้ตบุ๊กจึงใช้แม่น้ำจากโมเดล และถามอีกคำถามหนึ่ง: อัตราการไหลวันนี้เป็นกี่เท่าของอัตราปกติของเดือนนี้?

🧪 The notebook: the same Python, with data from files and from the internet

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 Colab
The csv has two rain columns on purpose. rain_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 โน้ตบุ๊กจะถามคุณว่าควรเชื่อคอลัมน์ไหน

What to build next

Keep the shape and change one function at a time. 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() จะยังเหมือนเดิมทุกอย่าง ฟังก์ชันมีไว้เพื่อสิ่งนี้

📌 Six things to keep

Three about Python, three about the alert system. 三条关于 Python,三条关于警报系统。 สามข้อเกี่ยวกับ Python และสามข้อเกี่ยวกับระบบเตือนภัย

  • Data goes in a list, and a loop visits it. Thirty days or thirty thousand, the code is the same. 数据放进列表,循环来访问它。三十天还是三万天,代码都一样。 ข้อมูลอยู่ในลิสต์ และลูปไปเยี่ยมมัน สามสิบวันหรือสามหมื่นวัน โค้ดก็เหมือนเดิม
  • A function takes inputs and returns an answer. Small functions are easy to test and easy to replace. 函数接收输入,返回答案。小函数容易测试,也容易替换。 ฟังก์ชันรับข้อมูลเข้าและส่งคำตอบกลับ ฟังก์ชันเล็ก ๆ ทดสอบง่ายและเปลี่ยนง่าย
  • A dictionary finds a value by name, a set keeps each thing once, and a class makes your own kind of value. 字典按名字找值,集合让每样东西只出现一次,类造出你自己的那种值。 ดิกชันนารีหาค่าด้วยชื่อ เซตเก็บแต่ละสิ่งไว้ครั้งเดียว และคลาสสร้างค่าชนิดของคุณเอง
  • Turn every signal into a score from 0 to 1. Then different kinds of data can be compared. 把每个信号变成 0 到 1 的分数。这样不同种类的数据才能比较。 เปลี่ยนทุกสัญญาณเป็นคะแนนจาก 0 ถึง 1 แล้วข้อมูลต่างชนิดจึงเทียบกันได้
  • Join scores by multiplying the odds. Strong is ×10, calm is ÷10, missing is ×1. 用赔率相乘来合并分数。强是 ×10,平静是 ÷10,缺失是 ×1。 รวมคะแนนด้วยการคูณอัตราต่อรอง แรงคือ ×10 สงบคือ ÷10 หายไปคือ ×1
  • A program can count its mistakes. A person must choose the threshold. 程序能数出自己的错误。阈值必须由人来选。 โปรแกรมนับความผิดพลาดของตัวเองได้ แต่คนต้องเป็นผู้เลือกเกณฑ์

You have built an alert system in Python. Thank you for working through it, and see you in the next lab. 你已经用 Python 做出了一个警报系统。感谢你完成这一课,我们下个实验室见。 คุณสร้างระบบเตือนภัยด้วย Python แล้ว ขอบคุณที่ทำจนจบ แล้วพบกันในห้องทดลองถัดไป

🖼️ Photographs, data and sources

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