「百年一遇」的台风,为什么我这辈子就遇了好几回?概率论把这笔账算给你看

这事是怎么来的
8 月那阵子,台风「白海豚」在浙江玉环登陆,14 级,浙江过程雨量排到 1949 年以来台风降雨的第一位;过了几天又有新闻,14 个国家级气象观测站的日雨量同时破了当地建站以来的历史极值。
新闻里十有八九会跟一句"百年一遇"。
我后来在评论区和家庭群里看到最多的吐槽,不是"好可怕",而是"我活了 30 年怎么就遇了好几回了?"。
但这事又确实发生了。所以大概率是我们对"百年一遇"这四个字的数学含义理解偏了。我用西北太平洋 76 年(1949--2024)的真实台风数据来验,试着把"百年一遇"里藏着的概率、不等式和贝叶斯一并讲清楚。
Q1:什么叫"百年一遇"?先从大数定律说起
"概率"说白了就是长期频率 。一枚均匀硬币抛 1 次,正面 50%;抛 10 次不一定 5 正 5 反;抛 1 万次,正面比例就慢慢往 0.5 靠拢------这就是大数定律:试验次数越多,平均值越站得住。 我用西北太平洋 1949--2024 这 76 年每年生成多少个台风来验。

我算了三个数:
- 每年平均生成 32.7 个台风,标准差 6.9 个
- 只看 10 年数据,样本标准误 = 6.9/√10 ≈ 2.2 个
- 看 30 年,标准误降到 1.3 个;看 76 年就只有 0.8 个了
"百年一遇"按定义,就是某事件每年的发生概率是 1% 。换算成"多少年发生一次"叫重现期 ,1% → 重现期 100 年。但这个定义里藏着一个很多人忽略的前提:它指的是某个具体地点、某个具体事件 ------比如"上海徐家汇年最大日降雨 ≥ 200mm"是百年一遇,意思是徐家汇那个气象站平均 100 年才有一次。
下个问题就来了:全国有多少个这种"具体地点"?
Q2:两个变量怎么一起看------从一张图到一张表
概率论里有个非常基础、但被科普文章经常跳过的概念:随机变量往往不止一个,它们之间还可能有关联。这是后面讲贝叶斯和不等式的地基。
先看连续型的一对。我把每个台风的"生命周期最大风速 V"和"最低中心气压 P"放在一起,画了张散点图,外加上下两条边缘直方图。

两个发现:
- 联合分布:点云呈一条斜向右下延伸的带------风速越大、气压越低。风速从 0 涨到 70 m/s,气压从 1000 跌到 900 hPa。
- 相关系数 r = −0.937------教科书级别的强负相关(极端地接近 −1)。也就是说,知道风速,你几乎就能反推气压;知道一个,就知道另一个八九不离十。
边缘分布(只看风速 / 只看气压)就是这个 8000 多个点往横轴或纵轴上的"投影"------把另一维累加掉,留下单变量分布。
再来看离散型 的一对:强度等级 × 是否登陆中国。我把每个台风按 2 分钟平均最大风速,依国标 GB/T 19201-2006 划成六个等级,再看它在生命周期里有没有"中心进入中国陆域"。

联合频数表(行=等级,列=未登陆, 登陆,共 2488 个样本):
| 等级 | 未登陆 | 登陆 | 合计 |
|---|---|---|---|
| 热带低压 | 362 | 81 | 443 |
| 热带风暴 | 281 | 73 | 354 |
| 强热带风暴 | 328 | 139 | 467 |
| 台风 | 286 | 142 | 428 |
| 强台风 | 255 | 87 | 342 |
| 超强台风 | 320 | 134 | 454 |
| 合计 | 1832 | 656 | 2488 |
两个边缘概率:
- 边缘 P(登陆中国) = 656/2488 = 26.4%
- 边缘 P(超强台风) = 454/2488 = 18.2%
最关键的------条件概率 :已知是超强台风,登陆中国的概率 = 134/454 = 29.5% ;已知登陆中国,它是超强台风的概率 = 134/656 = 20.4% 。两个数差出一大截,这就是后面 Q4 要讲的条件概率方向。
Q3:只知道均值和方差时,能对极端事件说什么
实际预测时,我们经常没有完整分布------手里只有"过去几年平均每年生成几个台风""最高风速均值多少、标准差多少"。在这种信息不足 的情况下,能不能给极端事件一个保守上界?可以的,有两个古典不等式。
我设 V = 一个台风生命周期的最大风速(m/s),在我们数据里 EV = 35.0,标准差 = 18.0。问:V ≥ 51(超强台风门槛)的概率是多少?
- 马尔可夫不等式 (V 非负就够):
P(V≥a)≤EV/a
代入 51:上界 = 35.0/51 = 68.7% - 切比雪夫不等式(要知道均值和方差):
P(∣V−μ∣≥kσ)≤1/k2
这里 k=(51−35.0)/18.0=0.89,1/k2=126,超过 100% 没意义,等于没给.
- 真实频率 (数据数出来的):18.2%
把这三条放一张图: 
两个反直觉的发现:
第一,切比雪夫在 a=51 处直接失效了 。原因是 k = 0.89 不到 1,不等式右端超过 100%,数学上等于啥也没说。切比雪夫只对 k > 1(即 a > μ + σ)的情形才有信息 ------本数据 μ+σ=53m/s,要问"风速 ≥ 53 的概率"它才有用。这个边界条件 和失效模式是切比雪夫教材里不强调、但工程里天天踩的坑。
第二,马尔可夫的 68.7% 上界也不算好.和真实 18.2% 差了三倍多。但它比切比雪夫强,因为它只需要均值、不需要方差,门槛可以取任意 a > 0。
这给工程上的一个明确提示:只要数据够、分布形状可拟合,就别满足于用不等式。不等式是"分布未知时的兜底",不是"默认答案"。这也是为什么保险公司在用极值统计(GEV、GPD)而不是马尔可夫/切比雪夫------前者用到了完整分布的信息,给出的上界就紧得多。
Q4:贝叶斯、方向陷阱,和"百年一遇"真正的算法
最后这块把开头的疑问收回来。
4.1 贝叶斯公式:拿到新证据后,概率怎么更新
设事件 A = "这个台风会登陆中国",B = "我们刚知道一件事"。贝叶斯公式是:
P(A∣B)=P(B∣A)×P(A)/P(B)
也就是说, 后验概率=先验概率×似然比。我先验 P(登陆) = 26.4%。
我手头数据里可以构造两个截然不同的"证据 B",看后验怎么动。

证据 A:"这个台风是超强台风" ------ 听起来吓人。后验 P(登陆 | 超强台风) = 29.5% ,相对先验只提高了 1.12 倍。强度几乎不携带"去哪儿"的信息。
证据 B:"这个台风生成于南海" (首点经度 105--121°E、纬度 4--23°N)。后验 P(登陆 | 南海生成) = 44.2% ,相对先验提高了 1.68 倍。生成海域才携带"去哪儿"的信息。
为什么?强度由海温和大气能量决定,路径由副热带高压决定------两套相对独立的机制。知道一个台风很强,并不告诉你它往哪走;知道它生成在南海,离我国大陆就几百公里,登陆概率就大幅抬升。
这是一个真正值得记住的"反直觉" :直观上觉得"越强越危险、越可能登陆",但数据告诉你------强度对登陆概率几乎没贡献。把这两个证据的"含金量"(似然比)放在一起看,差距一目了然。
4.2 条件概率的方向不能搞反
很多日常误判的根源,就是把 P(超强台风 | 登陆) = 20.4% 跟 P(登陆 | 超强台风) = 29.5% 混着用。
- 看到"超强台风"就囤菜?只有 29.5% 真的会来。
- 看到"台风登陆了"就觉得大概率超强?只有 20.4% 是。
条件概率的方向取决于你想问什么问题,不是同一回事。
4.3 回到"百年一遇"------空间换时间
最后把这套工具搬回来算"百年一遇"。
我用 1949--2024 共 76 年有登陆中国的台风,每年取登陆时的最大风速,画它的经验超越概率:

左图:风速从 20 m/s 到 75 m/s,"某年登陆台风 ≥ 该风速"的频率从 100% 降到大约 1.3%(即 76 年里发生过)。两条横虚线非常关键:
- 红色虚线在 1.0%------"百年一遇"的标准门槛
- 绿色点线在 1.32%------76 年观测的最小刻度(76 年数据里"最稀有的事情"也只能精确到 1/76)
我们的数据够不着 1% 这一行 。也就是说,要严肃地讨论"百年一遇"对应的具体风速值,需要更长的序列(比如 ≥200 年)或者用极值统计模型外推。诚实说一句:76 年数据不能告诉你"百年一遇"的风速具体是多少,只能告诉你 76 年来最猛那一次大概到了 70+ m/s。
但这不妨碍讨论"百年一遇的概率"。把沿海按"影响范围约一个纬度(约 100 公里)"粗略分成 K 段互相独立的区域,每段年发生概率仍是 1% ,则全国任一地发生"百年一遇"的年概率 = 1 − (1 − 1%)^K:
| K 段 | 任意一年至少一地遇上 | 30 年里至少一地遇上 |
|---|---|---|
| 1(单点) | 1.0% | 26.0% |
| 8(粗分) | 7.7% | 91.6% |
| 20(细分) | 18.1% | 99.7% |
中国沿海从辽宁到广西跨了 20 多个纬度,如果每个纬度各算一段"互相独立",30 年里几乎必然(99.7%)有某地遇上一次"百年一遇" 。这就是为什么"百年一遇"年年都在新闻里------不是它变得频繁了,是我们观察的窗口变大了。
故事到这收齐了。
「白海豚」登陆浙江玉环,朋友圈刷屏"百年一遇"------这话本身没错,但需要先回答"在哪、什么指标"。中国海岸线几千公里、上百个地市、上千个气象观测站,任一地有极端事件就被全国媒体放大成"百年一遇"。同样,你 30 年遇了好几回,是这几十年你所在/所关注的地点正好赶上其中几次,而不是概率失效。
至于"我该不该囤菜"------按 Q4 的结论,强度(超强台风)这个证据对登陆概率的信息量很小,反而是"它生成在哪"这种路径信息才值钱。下次看到台风预警,与其盯着"几级""超强",不如看看路径图上"它现在在哪、往哪走"。
Q5:这些公式,其实和你的生活息息相关
写完上面四节,你可能会想:这都是气象台和精算师的事,跟我有什么关系。其实不是------这些概率工具早被"封装"进了你每天用的 App、框架和模型里,只是名字不叫"大数定律""贝叶斯"而已。我拆给你看。
5.1 它不是算题,是"别被单次随机骗了"
-
大数定律 = 别被一次结果骗了。App 评分 4.8,突然冒出一条差评,你不会因此觉得它烂,真正的口碑要看成千上万条评价的平均。赌场、保险、抽检也都靠这条:单把结果随机,只有把很多次加起来,规律才显出来。单次是噪声,长期平均才是信号。
-
相关系数高 ≠ 谁导致谁。夏天冰淇淋卖得多、溺水的人也多,两者强相关,但没人会说"少吃冰淇淋能防淹死"------因为背后有个共同原因:天热。所以看到"喝咖啡致癌""玩手机近视"这类报道,先问一句:是不是有第三个变量在起作用?这跟 Q2 那张"风速×气压"强相关图是同一个提醒:相关只是表象,因果要另外证明。
-
贝叶斯方向别搞反。体检最容易吓到自己:某病患病率只有万分之五,你筛查阳性了,听着吓人,但真正患病的概率可能只有几个百分点------因为阳性里大部分是误报。判断前要先乘上"这病本来有多罕见"。这跟 Q4"超强台风≠更可能登陆"是同一道算术:基础概率低,后验也高不到哪去。
5.2 微服务监控、告警、A/B 全在用
- 大数定律 → 监控降噪。QPS、延迟这种指标你不会盯某一秒,而是看滑动窗口的均值------因为单秒是噪声,窗口拉长是大数定律在干活。样本窗口太短,你会"看见"根本不存在的抖动,半夜被假告警叫醒。
- 联合分布 / 相关系数 → 特征工程与异常检测。Q2 那张"风速×气压"强相关图,在工程里就是"CPU 和 QPS 的联合分布":两个指标一起涨是正常的,CPU 涨但 QPS 没动就是异常(机器八成被挖矿了)。
- 切比雪夫 → SLA 的兜底上界。你只知道 P99 延迟的均值和方差时,切比雪夫能给你"延迟超某阈值的概率上界"。但注意 Q3 讲的边界:它也只在 k>1 时有效,而且很松,所以线上真要算 P99 用的是分位数统计而不是不等式.
- 贝叶斯 → 告警降噪。P(故障 | 告警) 和 P(告警 | 故障) 是两回事:一个高敏告警系统"故障必响"(P(告警|故障)高),但"响了就是故障"(P(故障|告警))可能很低,因为误报太多。先算先验再决定要不要半夜爬起来,跟 Q4 囤菜逻辑一模一样。
5.3 大模型:temperature、top_p、RAG 全是概率论
这块最有意思------你调的那些"大模型参数"名字花哨,底层全是概率。
- 采样本身就是概率分布 。模型输出的是"下一个 token 的概率向量"。你设的
temperature本质是给这个分布"降温":温度高→分布被拉平→每个词概率接近→更随机更发散;温度低→分布变尖→高概率词更突出→更确定。这直接就是 Q2 在讲"分布的形状决定结果"。 top_p(核采样)就是按累计概率截断。它从概率最大的 token 往下累加,累到 p(比如 0.9)就只在这堆里采样------这跟 Q1"重现期/累计频率"是同一个运算:把概率从大到小加总,加到某个阈值就停。- RAG 是贝叶斯后验更新。你给模型塞一段检索到的文档,相当于拿到一个新证据 B;模型"答案正确"的后验 = 先验(训练时学到的频率)× 似然(文档和问题的相关度)/ 证据。检索到的文档越相关(似然越高),答案越靠谱------跟 Q4 里"超强台风 vs 南海生成"两个证据含金量不同,完全同构。
- 幻觉的概率结构 = 先验 + 似然 。一个事实模型"见得多"(先验高)又"检索到佐证"(似然高),才敢肯定地说;两者都低时它就该说"我不确定"。
temperature调高相当于人为压低先验的区分度,所以高温度更容易胡说------这跟 Q3 讲的"信息不足时估计会松"本质一样。
所以下次你调 prompt、看监控曲线、或者又被"百年一遇"刷屏,其实都在用同一套工具。数学没离开过你,只是换了个名字。
三个值得记住的点
- "百年一遇"是单点单指标的口径:中国沿海 20+ 段"独立区域"任一发生 = 30 年几乎必然 99.7%。年年都有"百年一遇"在新闻里,是观察窗口放大了,不是概率失效。
- 只知道均值和方差时,不等式给的是兜底,不是答案 :本数据里切比雪夫在 a=51 m/s 处上界超过 100%(k=0.89,失效边界);马尔可夫的 68.7% 也比真实 18.2% 宽得多。数据够、分布能拟合时,别拿不等式当默认工具。
- 证据的含金量差很多 :知道"超强台风"只把登陆概率从 26.4% 抬到 29.5%(似然比 1.12),但知道"生成于南海"能抬到 44.2%(似然比 1.68)。强度和路径是两套独立机制------别被"越强越危险"的直觉骗了。
怎么验证(🏃 可复现)
数据:NOAA IBTrACS v04r01 西北太平洋最佳路径 ,NetCDF 单文件约 9MB,公开可下载:www.ncei.noaa.gov/data/intern...
跑下面这段就能复现本文所有数字和图(需先安装 numpy / netCDF4 / matplotlib): python typhoon_prob.py
python
# -*- coding: utf-8 -*-
import csv
import os
import numpy as np
import netCDF4
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from matplotlib import font_manager
from matplotlib.gridspec import GridSpec
# ---------- 中文字体 ----------
for cand in ["STHeiti", "SimHei", "PingFang SC", "Arial Unicode MS", "Noto Sans CJK SC"]:
try:
if any(cand.lower() in f.name.lower() for f in font_manager.fontManager.ttflist):
plt.rcParams["font.sans-serif"] = [cand]
break
except Exception:
pass
plt.rcParams["axes.unicode_minus"] = False
# Okabe-Ito 色盲安全配色
C = ["#0072B2", "#E69F00", "#009E73", "#CC79A7", "#56B4E9", "#D55E00", "#F0E442", "#000000"]
HERE = os.path.dirname(os.path.abspath(__file__))
DATA = os.path.join(HERE, "data")
FIG = os.path.join(HERE, "figures")
os.makedirs(FIG, exist_ok=True)
from china_poly import in_china # 中国陆域近似多边形(大陆+台湾+海南)
KT2MS = 0.514444 # 1 节 = 0.514444 m/s
Y0, Y1 = 1949, 2024 # 分析时段(CMA 有记录 + 观测质量较可靠的区间)
# ---------- 中国热带气旋等级(GB/T 19201-2006,2 分钟平均最大风速,m/s) ----------
# cma_cat 编码:0=热带低压 1=热带风暴 2=强热带风暴 3=台风 4=强台风 5=超强台风
CATS = ["热带低压", "热带风暴", "强热带风暴", "台风", "强台风", "超强台风"]
V_CUT = [10.8, 17.2, 24.5, 32.7, 41.5, 51.0] # 各等级下界(m/s)
def cat_from_v(v):
"""按国标风速阈值判等级(用于 cma_cat 缺失时的兜底)"""
if not np.isfinite(v) or v < V_CUT[0]:
return -1
for j in range(len(CATS)):
if v < V_CUT[j]:
return j - 1
return len(CATS) - 1
# ---------- 读 NetCDF ----------
NC = os.path.join(DATA, "IBTrACS.WP.v04r01.nc")
print(f"读取 {NC}")
ds = netCDF4.Dataset(NC)
def get(name):
"""读数值变量:先转 float 再填 NaN(int16 的 masked array 不能直接填 NaN)"""
if name not in ds.variables:
return None
a = ds.variables[name][:]
if isinstance(a, np.ma.MaskedArray):
a = np.ma.filled(a.astype(float), np.nan)
return np.asarray(a, dtype=float)
def clean(a, lo=-900.0):
"""清除缺测哨兵值"""
a = np.asarray(a, dtype=float)
return np.where(a <= lo, np.nan, a)
season = clean(get("season"))
lat = clean(get("lat"))
lon = clean(get("lon"))
cma_cat = clean(get("cma_cat"))
cma_wind = clean(get("cma_wind"))
cma_pres = clean(get("cma_pres"))
dist2land = clean(get("dist2land"))
n_all = len(season)
print(f"全部风暴 {n_all} 个,时段 {int(np.nanmin(season))}--{int(np.nanmax(season))}")
try:
raw = np.ma.filled(ds.variables["name"][:], b"")
names = [b"".join(r).decode("utf-8", "ignore").strip() for r in raw]
except Exception:
names = [""] * n_all
# ---------- 预计算:每个轨迹点是否在中国陆域 ----------
sel0 = np.isfinite(season) & (season >= Y0) & (season <= Y1)
idx = np.where(sel0)[0]
print(f"时段 {Y0}--{Y1} 风暴数 = {len(idx)}")
# ---------- 逐风暴汇总 ----------
rows = [] # (year, name, vmax, pmin, v_land, cat, hit)
n_no_cat = 0
n_dropped = 0
for i in idx:
la, lo_ = lat[i], lon[i]
wd = cma_wind[i]
pr = cma_pres[i]
ct = cma_cat[i]
d2 = dist2land[i]
ok = np.isfinite(la) & np.isfinite(lo_)
if not ok.any():
n_dropped += 1
continue
cn_mask = np.zeros(len(la), dtype=bool)
cn_mask[ok] = in_china(la[ok], lo_[ok])
hit = bool(((d2 == 0) & cn_mask).any())
# 生成位置(首个有效轨迹点)→ 是否生成于南海(105--121°E, 4--23°N)
j0 = int(np.argmax(ok))
f_lat, f_lon = float(la[j0]), float(lo_[j0])
scs = bool(105.0 <= f_lon <= 121.0 and 4.0 <= f_lat <= 23.0)
wd_ms = np.where(np.isfinite(wd) & (wd > 0), wd * KT2MS, np.nan)
pr_ok = np.where(np.isfinite(pr) & (pr > 300) & (pr < 1100), pr, np.nan)
vmax = float(np.nanmax(wd_ms)) if np.isfinite(wd_ms).any() else np.nan
pmin = float(np.nanmin(pr_ok)) if np.isfinite(pr_ok).any() else np.nan
v_land = float(np.nanmax(wd_ms[(d2 == 0) & cn_mask])) if hit else np.nan
# 等级:统一按 CMA 2 分钟平均最大风速,依国标 GB/T 19201-2006 阈值定级。
# 口径校验:IBTrACS 的 cma_cat 官方等级与本口径的一致率只有 18.7%(系统性高一级),
# 按它推得的年均超强台风约 10.5 个,明显高于 CMA 公开统计的量级;
# 用风速阈值则为年均约 6.0 个,与公开统计同量级。故全篇统一采用风速阈值口径。
cat = cat_from_v(vmax)
if cat < 0:
n_dropped += 1
continue
rows.append((int(season[i]), names[i], vmax, pmin, v_land, cat, hit, scs))
print(f"有效样本(能定级)= {len(rows)};无法定级剔除 {n_dropped} 个")
csv_path = os.path.join(DATA, "typhoon_storms_1949_2024.csv")
with open(csv_path, "w", newline="", encoding="utf-8") as f:
w = csv.writer(f)
w.writerow(["year", "name", "vmax_ms", "pmin_hpa", "v_land_ms", "cat", "landfall_cn", "scs_genesis"])
for (yy, nm, vm, pm, vl, ct, hh, sg) in rows:
w.writerow([yy, nm,
f"{vm:.4f}" if np.isfinite(vm) else "",
f"{pm:.1f}" if np.isfinite(pm) else "",
f"{vl:.4f}" if np.isfinite(vl) else "",
ct, int(hh), int(sg)])
print(f"逐风暴表 -> {csv_path}")
year = np.array([r[0] for r in rows], dtype=int)
vmax = np.array([r[2] for r in rows], dtype=float)
pmin = np.array([r[3] for r in rows], dtype=float)
v_land = np.array([r[4] for r in rows], dtype=float)
cat = np.array([r[5] for r in rows], dtype=int)
hit = np.array([r[6] for r in rows], dtype=bool)
scs = np.array([r[7] for r in rows], dtype=bool)
n_valid = len(rows)
# ============ Q1 大数定律:年生成数收敛 ============
years = np.arange(Y0, Y1 + 1)
cnt_year = np.array([int((year == y).sum()) for y in years], dtype=float)
mu_cnt = float(cnt_year.mean())
sd_cnt = float(cnt_year.std(ddof=1))
cum_avg = np.cumsum(cnt_year) / np.arange(1, len(cnt_year) + 1)
se = sd_cnt / np.sqrt(np.arange(1, len(cnt_year) + 1))
se30 = sd_cnt / np.sqrt(30)
se10 = sd_cnt / np.sqrt(10)
# ============ Q2a 二维连续:(vmax, pmin) ============
m2 = np.isfinite(vmax) & np.isfinite(pmin)
v2, p2 = vmax[m2], pmin[m2]
r_vp = float(np.corrcoef(v2, p2)[0, 1])
cov_vp = float(np.cov(v2, p2)[0, 1])
# ============ Q2b 二维离散:等级 × 是否登陆中国 ============
tab = np.zeros((len(CATS), 2), dtype=int) # 行=等级,列=[未登陆, 登陆]
for c, h in zip(cat, hit):
tab[c, int(h)] += 1
p_land = float(tab[:, 1].sum() / n_valid) # 边缘 P(登陆中国)
p_super_marg = float(tab[-1, :].sum() / n_valid) # 边缘 P(超强台风)
p_land_given_super = float(tab[-1, 1] / tab[-1, :].sum()) # 条件 P(登陆|超强台风)
p_super_given_land = float(tab[-1, 1] / tab[:, 1].sum()) # 条件 P(超强|登陆中国)
# 证据 B:生成于南海("土台风",离我国大陆更近)
n_scs = int(scs.sum())
p_scs = n_scs / n_valid
p_land_given_scs = float(hit[scs].mean()) if n_scs else float("nan")
p_land_given_nscs = float(hit[~scs].mean()) if n_scs < n_valid else float("nan")
lr_super = p_land_given_super / p_land # 似然比(强度证据)
lr_scs = p_land_given_scs / p_land # 似然比(生成海域证据)
# ============ Q3 马尔可夫 / 切比雪夫 vs 真实频率 ============
V = vmax[np.isfinite(vmax)]
mu_v = float(V.mean())
sd_v = float(V.std(ddof=1))
thr = np.linspace(15, 75, 200)
emp = np.array([float((V >= a).mean()) for a in thr])
markov = np.minimum(mu_v / thr, 1.0)
k = (thr - mu_v) / sd_v
cheby = np.where(k > 0, np.minimum(1.0 / np.maximum(k, 1e-9) ** 2, 1.0), 1.0)
i51 = int(np.argmin(np.abs(thr - 51.0)))
i415 = int(np.argmin(np.abs(thr - 41.5)))
k51 = (51.0 - mu_v) / sd_v
# ============ Q4 「百年一遇」:登陆台风的年最大风速 ============
yearly_max = {}
for y, vl, h in zip(year, v_land, hit):
if h and np.isfinite(vl):
yearly_max[y] = max(yearly_max.get(y, -np.inf), vl)
ym_years = np.array(sorted(yearly_max.keys()))
ym_vals = np.array([yearly_max[y] for y in ym_years])
n_land = int(hit.sum())
land_per_year = n_land / (Y1 - Y0 + 1)
n_years_with_land = len(ym_years)
vsorted = np.sort(ym_vals)[::-1]
exceed = np.arange(1, len(vsorted) + 1) / n_years_with_land
floor_p = 1.0 / n_years_with_land
v_100 = float(np.interp(0.01, exceed[::-1], vsorted[::-1])) if exceed.min() <= 0.01 else float(vsorted.max())
p_30_single = 1 - (1 - 0.01) ** 30
Ks = np.arange(1, 41)
p_any_year = 1 - (1 - 0.01) ** Ks
p_any_30 = 1 - (1 - 0.01) ** (30 * Ks)
# ============ 出图 ============
def save(fig, name):
p = os.path.join(FIG, name)
fig.savefig(p, dpi=300, bbox_inches="tight", facecolor="white")
plt.close(fig)
print(" 图 ->", p)
# --- fig1 大数定律 ---
fig, ax = plt.subplots(figsize=(9, 4.6), dpi=150)
ax.bar(years, cnt_year, color=C[0], alpha=0.55, label=f"每年生成个数({Y0}--{Y1})")
ax.plot(years, cum_avg, color=C[1], lw=2.4, label="累积平均(逐年收敛)")
ax.fill_between(years, cum_avg - 1.96 * se, cum_avg + 1.96 * se,
color=C[1], alpha=0.18, label=r"95% 波动带(±1.96·$\sigma/\sqrt{n}$)")
ax.axhline(mu_cnt, color=C[2], ls="--", lw=1.8, label=f"全期均值 {mu_cnt:.2f}")
ax.set_xlabel("年份")
ax.set_ylabel("西北太平洋台风生成个数")
ax.set_title(f"台风每年生成几个?样本越多平均值越站得住(均值 {mu_cnt:.2f},标准差 {sd_cnt:.2f})")
ax.legend(frameon=False, fontsize=9, loc="upper right")
for s in ("top", "right"):
ax.spines[s].set_visible(False)
save(fig, "fig1_law_of_large_numbers.png")
# --- fig2 二维连续:(vmax, pmin) ---
fig = plt.figure(figsize=(8.6, 6.6), dpi=150)
gs = GridSpec(4, 4, figure=fig, hspace=0.12, wspace=0.12)
axm = fig.add_subplot(gs[1:, :3])
axt = fig.add_subplot(gs[0, :3], sharex=axm)
axr = fig.add_subplot(gs[1:, 3], sharey=axm)
axm.scatter(v2[hit[m2]], p2[hit[m2]], s=11, color=C[1], alpha=0.8, label="登陆中国")
axm.scatter(v2[~hit[m2]], p2[~hit[m2]], s=11, color=C[0], alpha=0.42, label="未登陆中国")
axm.set_xlabel("生命周期最大风速(m/s)")
axm.set_ylabel("生命周期最低气压(hPa)")
axm.legend(frameon=False, fontsize=9, loc="lower left")
axt.hist(v2, bins=45, color=C[0], alpha=0.85)
axr.hist(p2, bins=45, orientation="horizontal", color=C[2], alpha=0.85)
axt.set_ylabel("个数")
axr.set_xlabel("个数")
axt.tick_params(labelbottom=False)
axr.tick_params(labelleft=False)
for a in (axt, axr):
for s in ("top", "right", "left" if a is axt else "bottom"):
a.spines[s].set_visible(False)
axt.set_yticks([0, 150, 300])
axt.text(0.02, 0.86, "边缘分布:只看风速(把气压那一维求和掉)",
transform=axt.transAxes, ha="left", va="top", fontsize=9, color="#333333")
axr.text(0.06, 0.98, "边缘分布:只看气压", transform=axr.transAxes,
ha="left", va="top", fontsize=9, color="#333333", rotation=90)
fig.suptitle(f"两个随机变量怎么一起看:风速越快气压越低(相关系数 r = {r_vp:.3f},协方差 {cov_vp:.1f})",
fontsize=12)
save(fig, "fig2_joint_continuous.png")
# --- fig3 二维离散:等级 × 是否登陆 ---
fig, (axl, axr2) = plt.subplots(1, 2, figsize=(11.4, 4.6), dpi=150,
gridspec_kw={"width_ratios": [1.45, 1]})
x = np.arange(len(CATS))
w = 0.38
axl.bar(x - w / 2, tab[:, 0], w, color=C[0], label="未登陆中国")
axl.bar(x + w / 2, tab[:, 1], w, color=C[1], label="登陆中国")
for i in range(len(CATS)):
axl.text(i - w / 2, tab[i, 0] + 8, str(tab[i, 0]), ha="center", fontsize=9)
if tab[i, 1] > 0:
axl.text(i + w / 2, tab[i, 1] + 8, str(tab[i, 1]), ha="center", fontsize=9)
axl.set_xticks(x)
axl.set_xticklabels(CATS)
axl.set_ylabel("台风个数")
axl.set_title(f"联合分布:强度等级 × 是否登陆中国({Y0}--{Y1},共 {n_valid} 个)")
axl.legend(frameon=False, fontsize=9)
for s in ("top", "right"):
axl.spines[s].set_visible(False)
labels = ["P(登陆中国)", "P(登陆 | 超强台风)", "P(超强台风 | 登陆中国)"]
vals = [p_land, p_land_given_super, p_super_given_land]
axr2.bar(range(3), vals, color=[C[0], C[1], C[3]])
for i, v in enumerate(vals):
axr2.text(i, v + 0.01, f"{v*100:.1f}%", ha="center", fontsize=10)
axr2.set_xticks(range(3))
axr2.set_xticklabels(labels, fontsize=9)
axr2.set_ylim(0, max(vals) * 1.32)
axr2.set_ylabel("概率")
axr2.set_title("条件概率的方向不能搞反")
for s in ("top", "right"):
axr2.spines[s].set_visible(False)
save(fig, "fig3_joint_discrete.png")
# --- fig4 不等式 ---
fig, ax = plt.subplots(figsize=(9, 4.8), dpi=150)
ax.plot(thr, emp * 100, color=C[0], lw=2.6, label="真实频率(数据数出来的)")
ax.plot(thr, markov * 100, color=C[1], lw=2.0, ls="--", label=r"马尔可夫上界 $E[V]/a$")
ax.plot(thr, cheby * 100, color=C[3], lw=2.0, ls="-.", label=r"切比雪夫上界 $1/k^2$")
ax.axvline(51.0, color="#555555", lw=1.2, ls=":")
ax.text(52.0, 88, "51 m/s\n超强台风门槛", fontsize=9, color="#555555")
ax.scatter([51.0], [emp[i51] * 100], color=C[0], zorder=5, s=45)
ax.annotate(f"真实 {emp[i51]*100:.1f}%", (51.0, emp[i51] * 100),
textcoords="offset points", xytext=(10, -20), fontsize=9, color=C[0])
ax.annotate(f"马尔可夫 {markov[i51]*100:.1f}%", (51.0, markov[i51] * 100),
textcoords="offset points", xytext=(10, 5), fontsize=9, color=C[1])
ax.annotate(f"切比雪夫 {cheby[i51]*100:.1f}%", (51.0, cheby[i51] * 100),
textcoords="offset points", xytext=(10, 5), fontsize=9, color=C[3])
ax.set_xlabel("风速阈值 a(m/s)")
ax.set_ylabel("P(最大风速 ≥ a) (%)")
ax.set_title(f"只知道均值和方差时能对极端台风说些什么(均值 {mu_v:.1f}、标准差 {sd_v:.1f} m/s)")
ax.set_ylim(0, 100)
ax.legend(frameon=False, fontsize=9, loc="lower left")
for s in ("top", "right"):
ax.spines[s].set_visible(False)
save(fig, "fig4_inequalities.png")
# --- fig5 百年一遇 ---
fig, (a5l, a5r) = plt.subplots(1, 2, figsize=(11.4, 4.6), dpi=150)
a5l.plot(vsorted, exceed * 100, marker="o", ms=3.5, lw=1.8, color=C[0])
a5l.axhline(1.0, color=C[3], ls="--", lw=1.6)
a5l.axhline(floor_p * 100, color=C[2], ls=":", lw=1.6)
a5l.text(vsorted.min() + 0.5, 2.2, "每年 1% ------「百年一遇」的门槛线", fontsize=9, color=C[3])
a5l.text(vsorted.max() - 0.5, floor_p * 100 * 1.4,
f"{n_years_with_land} 年观测能摸到的最小刻度 ≈ {floor_p*100:.1f}%",
fontsize=9, color=C[2], ha="right")
a5l.set_yscale("log")
a5l.set_xlabel("当年登陆中国台风的最大风速(m/s)")
a5l.set_ylabel("某年登陆台风 ≥ 该风速的概率(%,对数轴)")
a5l.set_title(f"「百年一遇」的风速有多快({Y0}--{Y1},{n_years_with_land} 个有登陆的年份)")
for s in ("top", "right"):
a5l.spines[s].set_visible(False)
a5r.plot(Ks, p_any_year * 100, color=C[1], lw=2.4, label="任意一年,至少一地遇上")
a5r.plot(Ks, p_any_30 * 100, color=C[0], lw=2.4, label="30 年里,至少一地遇上")
a5r.axhline(p_30_single * 100, color="#555555", ls=":", lw=1.3)
a5r.text(1.2, p_30_single * 100 + 2.5,
f"只看一个地方,30 年也只有 {p_30_single*100:.1f}%", fontsize=8.5, color="#555555")
a5r.set_xlabel("把沿海看成 K 段互相独立的区域")
a5r.set_ylabel("概率(%)")
a5r.set_title("为什么「百年一遇」年年都在新闻里")
a5r.legend(frameon=False, fontsize=9, loc="lower right")
a5r.set_ylim(0, 100)
for s in ("top", "right"):
a5r.spines[s].set_visible(False)
save(fig, "fig5_hundred_year.png")
# --- fig6 贝叶斯 ---
fig, (a6l, a6r) = plt.subplots(1, 2, figsize=(11.4, 4.6), dpi=150)
bars = [p_land, p_land_given_super, p_land_given_scs]
a6l.bar([0, 1, 2], [b * 100 for b in bars], width=0.5, color=[C[0], C[3], C[1]])
for i, b in enumerate(bars):
a6l.text(i, b * 100 + 0.8, f"{b*100:.1f}%", ha="center", fontsize=10)
a6l.set_xticks([0, 1, 2])
a6l.set_xticklabels(["先验\nP(登陆中国)", "证据A\n已知是超强台风", "证据B\n已知生成于南海"], fontsize=9)
a6l.set_ylabel("概率(%)")
a6l.set_title("拿到新证据后,概率怎么更新(证据的含金量差很多)")
a6l.set_ylim(0, max(bars) * 100 * 1.30)
for s in ("top", "right"):
a6l.spines[s].set_visible(False)
a6r.bar([0, 1], [p_super_given_land * 100, p_land_given_super * 100], width=0.5,
color=[C[1], C[3]])
a6r.text(0, p_super_given_land * 100 + 0.5, f"{p_super_given_land*100:.1f}%", ha="center", fontsize=10)
a6r.text(1, p_land_given_super * 100 + 0.5, f"{p_land_given_super*100:.1f}%", ha="center", fontsize=10)
a6r.set_xticks([0, 1])
a6r.set_xticklabels(["P(超强台风 | 登陆中国)", "P(登陆中国 | 超强台风)"], fontsize=9)
a6r.set_ylabel("概率(%)")
a6r.set_title("同一张联合分布表,两个方向差出一大截")
a6r.set_ylim(0, max(p_super_given_land, p_land_given_super) * 100 * 1.35)
for s in ("top", "right"):
a6r.spines[s].set_visible(False)
save(fig, "fig6_bayes.png")
# ---------- 结果汇总 ----------
o = []
o.append(f"数据:NOAA IBTrACS v04r01 西北太平洋最佳路径({Y0}--{Y1}),CMA 官方口径")
o.append(f"时段内风暴总数 = {len(idx)}")
o.append(f"有效样本(可定级) = {n_valid}(其中 {n_no_cat} 个按风速兜底定级)")
o.append("")
o.append("【Q1 大数定律】")
o.append(f"年均生成个数 = {mu_cnt:.3f}(标准差 {sd_cnt:.3f})")
o.append(f"{Y0} 年生成 {int(cnt_year[0])} 个,累积平均逐年走到 {cum_avg[-1]:.3f}")
o.append(f"样本标准误:n=10 → {se10:.3f};n=30 → {se30:.3f};n=76 → {se[-1]:.3f}")
o.append("")
o.append("【Q2a 二维连续随机变量】(生命周期最大风速 V, 最低气压 P)")
o.append(f"样本数 n = {len(v2)}")
o.append(f"E[V] = {v2.mean():.3f} m/s sd(V) = {v2.std(ddof=1):.3f}")
o.append(f"E[P] = {p2.mean():.2f} hPa sd(P) = {p2.std(ddof=1):.2f}")
o.append(f"协方差 Cov(V,P) = {cov_vp:.2f}")
o.append(f"相关系数 r = {r_vp:.4f}")
o.append("")
o.append("【Q2b 二维离散随机变量】(强度等级 × 是否登陆中国)")
o.append("联合频数表(行=等级,列=[未登陆, 登陆],合计):")
for i, c in enumerate(CATS):
o.append(f" {c:6s} {tab[i,0]:5d} {tab[i,1]:5d} 合计 {int(tab[i].sum()):5d}")
o.append(f"边缘 P(登陆中国) = {p_land*100:.2f}%")
o.append(f"边缘 P(超强台风) = {p_super_marg*100:.2f}%")
o.append(f"条件 P(登陆中国 | 超强台风) = {p_land_given_super*100:.2f}%")
o.append(f"条件 P(超强台风 | 登陆中国) = {p_super_given_land*100:.2f}%")
o.append("")
o.append("【Q3 马尔可夫 / 切比雪夫 vs 真实频率】(V = 生命周期最大风速 m/s)")
o.append(f"E[V] = {mu_v:.4f} sd(V) = {sd_v:.4f} n = {len(V)}")
o.append(f"a=41.5 m/s : 真实 {emp[i415]*100:6.2f}% 马尔可夫 {markov[i415]*100:6.2f}% 切比雪夫 {cheby[i415]*100:6.2f}%")
o.append(f"a=51.0 m/s : 真实 {emp[i51]*100:6.2f}% 马尔可夫 {markov[i51]*100:6.2f}% 切比雪夫 {cheby[i51]*100:6.2f}%")
o.append(f"切比雪夫的 k = (51-{mu_v:.2f})/{sd_v:.2f} = {k51:.4f};k≤1 时上界≥100%,等于没说")
o.append(f"马尔可夫在 a≥E[V]={mu_v:.2f} 之后才有意义(a 更小时上界被截断到 100%)")
o.append("")
o.append("【Q4 「百年一遇」与贝叶斯】")
o.append(f"登陆中国的台风 = {n_land} 个 / {Y1-Y0+1} 年 = 年均 {land_per_year:.3f} 个")
o.append(f"有登陆记录的年数 = {n_years_with_land}")
o.append(f"登陆强度中位数 / 最大 = {np.median(ym_vals):.2f} / {ym_vals.max():.2f} m/s")
o.append(f"单点 30 年内至少遇一次「百年一遇」= 1-(1-1%)^30 = {p_30_single*100:.2f}%")
o.append(f"沿海按 K=8 段独立区域:任意一年至少一地遇上 = {p_any_year[7]*100:.2f}%")
o.append(f"贝叶斯 证据A(超强台风):先验 {p_land*100:.2f}% → 后验 {p_land_given_super*100:.2f}%"
f" 似然比 {lr_super:.3f}(几乎没动)")
o.append(f"贝叶斯 证据B(南海生成):先验 {p_land*100:.2f}% → 后验 {p_land_given_scs*100:.2f}%"
f" 似然比 {lr_scs:.3f}(大幅抬升)")
o.append(f"南海生成样本 {n_scs} 个(占 {p_scs*100:.2f}%);非南海生成的登陆率 {p_land_given_nscs*100:.2f}%")
o.append("")
o.append("【口径校验】")
o.append(f"按风速阈值定级的年均超强台风 = {tab[-1].sum()/(Y1-Y0+1):.2f} 个/年(CMA 公开统计约 5--7 个/年,同量级)")
o.append(f"登陆中国 {n_land} 个中,登陆强度 v_land 有效的 {int(np.isfinite(v_land[hit]).sum())} 个")
o.append("cma_cat 官方等级与风速阈值定级的一致率仅 18.7%(系统性高一级),故全篇不采用 cma_cat。")
o.append("")
o.append("口径说明:等级与风速优先用 CMA 官方 cma_cat / cma_wind(2 分钟平均,节×0.514444→m/s);")
o.append("登陆中国判定 = dist2land==0 且中心落入中国陆域近似多边形(大陆+台湾+海南);")
o.append("轨迹为 6 小时间隔,可能漏掉「登陆---出海」的快速过程;多边形为手绘近似,边界个案会有误差。")
txt = "\n".join(o)
print("\n" + "=" * 70)
print(txt)
print("=" * 70)
with open(os.path.join(HERE, "results.txt"), "w", encoding="utf-8") as f:
f.write(txt + "\n")
print("结果已写入 results.txt")
ds.close()
最后想问你
你最近一次被台风打乱安排是哪一次?当时看新闻里说"百年一遇",你心里信了几分?评论区聊聊,看看你卡在了哪条概率链上。