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

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

这事是怎么来的

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/aP(V ≥ a) ≤ EV/a P(V≥a)≤EV/a
    代入 51:上界 = 35.0/51 = 68.7%
  • 切比雪夫不等式(要知道均值和方差):

P(∣V−μ∣≥kσ)≤1/k2P(|V − μ| ≥ kσ) ≤ 1/k² P(∣V−μ∣≥kσ)≤1/k2

这里 k=(51−35.0)/18.0=0.89,1/k2=126 k = (51 − 35.0)/18.0 = 0.89,1/k² = 126% 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 m/s μ+σ=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(A | B) = P(B | A) × P(A) / P(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、看监控曲线、或者又被"百年一遇"刷屏,其实都在用同一套工具。数学没离开过你,只是换了个名字。

三个值得记住的点

  1. "百年一遇"是单点单指标的口径:中国沿海 20+ 段"独立区域"任一发生 = 30 年几乎必然 99.7%。年年都有"百年一遇"在新闻里,是观察窗口放大了,不是概率失效。
  2. 只知道均值和方差时,不等式给的是兜底,不是答案 :本数据里切比雪夫在 a=51 m/s 处上界超过 100%(k=0.89,失效边界);马尔可夫的 68.7% 也比真实 18.2% 宽得多。数据够、分布能拟合时,别拿不等式当默认工具
  3. 证据的含金量差很多 :知道"超强台风"只把登陆概率从 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()

最后想问你

你最近一次被台风打乱安排是哪一次?当时看新闻里说"百年一遇",你心里信了几分?评论区聊聊,看看你卡在了哪条概率链上。

相关推荐
Zguigo16 分钟前
【DL】RNN|序列数据|Hidden State
人工智能·rnn·深度学习
苏其鸿21 分钟前
Python开发入门:从环境搭建到第一个实用小项目
python
练习两年半的攻城狮24 分钟前
【RAG实战】知识库 BGE-M3 稀疏向量混合检索方案
python·fastapi·llamaindex
weixin_5316708925 分钟前
Interlude起来:动作动画和文字通知哪个更适合休息提醒?Mac上有没有用桌宠做动作引导的软件?
人工智能·macos·mac·swift
A hao26 分钟前
户外LED广告牌需要什么防护等级IP级别?
大数据·网络·图像处理·人工智能·网络协议·tcp/ip·广告
l12586530 分钟前
# LangGraph 核心架构入门:State、Node、Edge 与 Reducer 的工程理解
大数据·人工智能·python·架构·langchain·edge
redfred30 分钟前
Upscayl 使用教程:开源 AI 图片放大器 模糊图片变清晰 2x/4x 放大 本地处理 免费
人工智能·开源
一切皆是因缘际会32 分钟前
人工智能底层架构缺陷
人工智能·架构
木圭的AI时代指南33 分钟前
我的8G显存电脑实测本地部署qwen3.8-27B记录
人工智能·ai·语言模型·电脑