一朵玫瑰,装下一年的风(《风不是一个数》系列第 5 篇

作者:YaoJaro · 本文由 AI 辅助完成

《风不是一个数》系列第 5 篇。前三篇的图,最长的横轴也只有 48 小时;「这个地方一年到头怎么吹」是 8,760 个时次的问题,折线画不下。统计图的折法是把时间叠起来------**玫瑰把一年叠成一朵:风从哪来、多大、多常来,一瓣一瓣全在里面。**装配、取舍、陷阱,最后一年叠成十二朵。

一、8,760 放不进任何一张

先接上一篇结尾那句话:「把这四张图放到 8,760 个时次上。」放上去才发现------放不下。

折线最先投降:8,760 个点挤进一根横轴,是一坨。时序带只有地面一层,一年画进去,色格细成一条线。剖面只有一天:8,760 个时次是 365 个剖面,并排放不下。前三篇的图种,到这一题全部到顶。

问题出在图种,不在数据。这个问题问的不是先后,是分布------把时间这一维叠起来:8,760 个时次不分先后,按来向 × 风速档归格计数,叠成一张联合频率表;表画成花瓣,就是风玫瑰。第 1 篇预览过这朵花,现在整朵给你。

先把账摆出来。数据是 IEM 的 ASOS 逐时归档(第 1 篇同源同站同窗口):从 2024-01-01 00:00Z 起取 365 个整日(末点不计------12 月 31 日不在内),共 8,760 行例行报文。风向缺测或报「不定」(VRB)的 232 行整行剔出,剩 8,528 行有向有速;其中静风(风速小于 1 kt)26 行、占 0.3%,单独剔出------入扇区 8,502 行。这四笔账后面每张图都要用。

这 8,502 行里藏着一根刺。全年最大风速 48.6 kt (报文按 25 m/s 编报,1 m/s ≈ 1.94 kt),出现在 2024-09-16 02:00Z,北京时上午十点。藏在哪一天?9 月 16 日------台风登陆上海的那一天。在哪一瓣里?几乎不在------那一个小时占全年 0.01%,躺在东南扇区最外档,同格还有 169 个小时,肉眼无从分辨。次高不在别处:09-16 00:00Z,46.65 kt。全年 ≥32 kt 的时次只有 9 个,8 个连成一串:15 日 20:00Z 到 16 日 03:00Z------整整一夜,一小时不缺;把 9 月拿掉,全年最大的风只有 31.1 kt,与这一夜隔着 17.5 kt。**孤岛不是那一个小时,是台风过境的那一夜。**看统计图,先记住这一句:极端 ≠ 常态,极端在统计图里没有体积------玫瑰答「通常怎么吹」,答不了「最猛那天怎么吹」。那一天怎么画,收尾会讲到;先把常态学会。

二、玫瑰装配:一瓣是一格账

玫瑰怎么读,第 1 篇第六节定过两步:先看形状(哪个扇区凸出=主导风向),再看长度(色块长短=各档占比),两步不能颠倒,这里不重讲。现在看装配:一朵玫瑰怎么从 8,502 行报文里长出来。

装配只有三步。第一步,每行报文出一个样本对:来向、风速。第二步,来向归扇区:22.5° 一格,全部先加 11.25° 再取整------让每格的中心对准 0°、22.5°、45°...,348.75°--11.25° 都算北,边界值归最近扇区。第三步,风速归档计数,得一张 16 × 6 的表------每格是「这个方向 × 这个档 × 多少小时」。联合频率是玫瑰的数据骨架:画图只是把表摆开------扇区摆方向、瓣内按档自内向外堆叠,色阶沿用平面场图那套风色标。

风速档的边界不是天然存在的,是参数。同一份 8,502 行,按整 2 kt 档 2,4,6,8,10,∞ 切,≥10 kt 占 35.6%;换成蒲福档 4,7,11,17,22,∞------蒲福风级(按风速分级的常用风级表)的 kt 界,≥22 kt 只占 0.8%。数据一格没动,参数动了一档,结论差出四十多倍。第 1 篇的玫瑰档勘误(标注正改回这套蒲福界)就是现成例子------看玫瑰先看图例的档:瓣按哪条线切的,图例不说清,扇区长短就没法比。

剔除账再报一遍:缺向 232、缺速 0、静风 26、入扇区 8,502------下面代码逐项打了出来。这份账不是脚注,为什么,第四节讲。

取数一条命令,第 1 篇同源同窗口(sknt 列已是 kt;取值是整数 m/s × 1.94384 的痕迹------ZSPD 按米每秒编报,第 4 篇讲过这套单位):

bash 复制代码
curl -o iem-zspd-2024.csv "https://mesonet.agron.iastate.edu/cgi-bin/request/asos.py?station=ZSPD&data=all&year1=2024&month1=1&day1=1&year2=2024&month2=12&day2=31&tz=UTC&format=comma&latlon=yes&elev=yes&missing=M&trace=T&direct=no&report_type=3"
js 复制代码
// rose.mjs ------ 档边界是参数:同一份年报文,换一组档,玫瑰换一副面孔

import { readFileSync } from "node:fs";

  


const lines = readFileSync("./iem-zspd-2024.csv", "utf8").split("\n")

.filter((l) => l && !l.startsWith("#"));

const head = lines[0].split(",");

const iV = head.indexOf("valid"), iD = head.indexOf("drct"), iS = head.indexOf("sknt"); // iV 第三、五节用

const rows = lines.slice(1).map((l) => l.split(","));

const SECTORS = ["N","NNE","NE","ENE","E","ESE","SE","SSE","S","SSW","SW","WSW","W","WNW","NW","NNW"];

const KT = [2, 4, 6, 8, 10, Infinity]; // 本篇主口径:整 2 kt 档

  


// 扇区中心对齐:22.5° 一格,+11.25° 再取整,边界值归最近扇区(348.75°--11.25° 都算北)

const sectorOf = (d) => Math.floor((((d + 11.25) % 360) + 360) % 360 / 22.5) % 16;

const bandOf = (s, E) => { let i = 0; while (i < E.length - 1 && s >= E[i]) i++; return i; };

  


// 装配:逐时样本 → 16 扇区 × n 档联合计数;缺向整行剔、静风(<1 kt)单独剔、不进扇区

function rose(rows, E) {

const m = Array.from({ length: 16 }, () => E.map(() => 0));

let missDir = 0, missSpd = 0, calm = 0;

for (const r of rows) {

const d = r[iD], s = r[iS];

if (d === "M" || d === "") { missDir++; continue; } // 缺向(含 VRB 报文)

if (s === "M" || s === "") { missSpd++; continue; }

if (+s < 1) { calm++; continue; } // 静风:有向有速,但不属于任何扇区

m[sectorOf(+d)][bandOf(+s, E)]++;

}

return { m, missDir, missSpd, calm };

}

  


const label = (E, i) =>

i === 0 ? `<${E[0]}` : i === E.length - 1 ? `≥${E[i - 1]}` : `${E[i - 1]}--${E[i]}`;

function show(name, E) {

const { m, missDir, missSpd, calm } = rose(rows, E);

const tot = m.flat().reduce((a, b) => a + b, 0);

console.log(`${name}:缺向 ${missDir}、缺速 ${missSpd}、静风 ${calm},入扇区 ${tot}`);

console.log(" " + E.map((_, i) =>

`${label(E, i)} kt ${(m.reduce((a, r) => a + r[i], 0) / tot * 100).toFixed(1)}%`).join(" | "));

}

  


show("整 2 kt 档 [2,4,6,8,10,∞]", KT);

show("蒲福档 [4,7,11,17,22,∞]", [4, 7, 11, 17, 22, Infinity]);

(照例交代:2024 全年已在归档里冻结,你跑出的数应与正文逐格一致;剔除账与扇区占比与第 1 篇对得上,档占比对不上------档是参数,第 1 篇切的是另一组线。这就是统计管线比实时取数踏实的地方。)

扇区数、档数、档边界,以后组件化时都是参数(sectors、bins、binEdges),缺省给行业惯例值;静风画中心圆还是披露账行,到时候也留开关。组件还没到,先用三段能跑的代码把这套规矩立住。

三、简版:把「多大」整维丢掉

玫瑰全息,但重:16 扇区 × 6 档 96 格,读一个「风主要从哪来」要跨六个色块。很多时候只要方向偏好------把速度档整维加掉,每个扇区剩一个数,这就是边际频率:第 1 篇那张矩阵按行求和就是它。全年 8,502 行收拢:N 11.07%、S 10.74%、NNW 9.27%、SE 8.70%------季风双峰还在,而且扇区占比与档无关:加掉的是档这一维,怎么切都动不了它。

画出来就是频率条,单环或条形两种形态。什么时候够用?只问「从哪来」的时候:快讯卡片、小屏、一眼扫完。代价同样清楚:逐档结构全丢,「北风多在大风档」这类读法,频率条上不存在。

第 2 篇装配场图时定过分工:色斑吃标量、方向归杆------「色斑告诉你多大,告诉不了你去哪」。频率条是同一件事的镜像:把速度这一维丢掉。任何图都是维度的取舍,统计图不豁免。

频率条与玫瑰,以后做成组件会吃同一套输入、走同一套剔除------并排摆的时候,两图的样本量披露行必须全等,不然读者对不上账。顺带把下一节要用的三档样本量一起算出来:

js 复制代码
// marginal.mjs ------ 边际频率(频率条的原料)+样本量三档(直接接在第二节代码后面:同一文件,rose/rows/SECTORS/KT/sectorOf/bandOf/iV 沿用上文)

const { m } = rose(rows, KT);

const rowT = m.map((rw) => rw.reduce((a, b) => a + b, 0));

const tot = rowT.reduce((a, b) => a + b, 0);

rowT.map((c, i) => ({ s: SECTORS[i], c }))

.sort((a, b) => b.c - a.c).slice(0, 4)

.forEach((x) => console.log(`${x.s} ${(x.c / tot * 100).toFixed(2)}%`));

// → N 11.07%、S 10.74%、NNW 9.27%、SE 8.70%(季风双峰;扇区占比与档口径无关)

  


// 三档样本量:同一份代码,只换样本窗口------单日(抽签档)

const byDay = new Map();

for (const r of rows) {

const k = r[iV].slice(0, 10);

if (!byDay.has(k)) byDay.set(k, []);

byDay.get(k).push(r);

}

for (const k of ["2024-01-15", "2024-04-15", "2024-07-15", "2024-09-16", "2024-10-15"]) {

const rs = byDay.get(k);

const { m: mm } = rose(rs, KT);

const rw = mm.map((c) => c.reduce((a, b) => a + b, 0));

const n = rw.reduce((a, b) => a + b, 0);

const dom = rw.indexOf(Math.max(...rw));

console.log(`${k}(${rs.length} 行):主导 ${SECTORS[dom]} ${(rw[dom] / n * 100).toFixed(1)}%`);

}

四、玫瑰什么时候会说谎

第一种场合:样本太少 。同一站同一年,窗口切三档:全年 8,502 行,N/S 双峰、形态稳定------这是常态(图 01 左边那朵就是它);单月 700 行上下,主导扇区读得出、次级结构开始晃------勉强够形状;单日 24 行------抽签。第三节代码那五个日子,五张玫瑰五副面孔:1 月 15 日像北风站(北扇区 37.5%),7 月 15 日像南风站(南扇区 66.7%),同一块场地隔半年,看着像两个气候区。9 月 16 日更妙:24 个时次全落在东到南的半圈、20 个 ≥10 kt,在自己的小玫瑰里是绝对主角;回到全年玫瑰,这 24 个时次只占 0.28%,最猛的那一个小时更是只有 0.01%。小样本不只画得歪,还会把异常画得特别像常态。

单月不全是反面:第五节的月度阵每张 700 行上下------在「够形状」的下限上,读形状合法,读细节越界。样本量不是道德题,是分辨率题------你要答什么,决定窗口切多小。

第二种场合:把剔除读成「没有」。26 个静风小时不进任何扇区,但它们真实发生过------无风的日子不属于方向统计,不是不存在。给它们留位置,两条路:要么中心圆(有静风才画,占比进图例),要么账目披露行(n、静风、缺向、缺速四笔并排印)。第 1 篇第三节的三态在这里换个身份:玫瑰图外的账,就是那些格子的延伸。

玫瑰不撒谎,前提是你把账摊开。

五、月度玫瑰阵:一年叠成十二朵

全年一张总玫瑰答「一年到头怎么吹」,答不了「怎么变的」------年度那张图是 12 个月的平均,此消彼长全被叠平。最小的年分辨率:按月切片,12 × 16 × 6,画成 12 宫格。

主导扇区逐月换手,一条线读下来:冬(12--3 月)NNW/N → 4 月 ESE → 5--6 月 SE → 盛夏 7 月 S → 8 月回 SE → 9 月 E------全年的最大值也落在这个月 → 秋(10--11 月)N,回冬位。第 1 篇年度玫瑰上的季风双峰(N、S 对称凸出),在这里展开成一条换手时间线:北峰是冬天供的(2 月北扇区 21.0%),南峰是盛夏供的(7 月南扇区 48.2%)。9 月那张一眼异样:东风一根孤峰戳在最外档,东风也是这个月的主导扇区(当月入扇区最多的方向);第一节那 48.6 kt 却不在东瓣,在东南瓣的最外档。月主导答「当月风多从哪来」,最大值时次的扇区答「那一小时从哪来」,两回事。为什么九月翻出这样一片东到东南的大风,也留到收尾。

十二宫格有一条统计图特有的对齐纪律:共用色阶与半径比例 ------十二张各自拉满,1 月和 9 月会看起来一样重;不锁标尺,就是视觉欺骗。共尺是算出来的,基数是堆叠外沿:十二个月里最长的扇区在 7 月(南风累计 48.2%),满圈统一锁 50%。要是按最长单格(7 月南风最外档 23.1%)去锁 25%,七月那瓣会被整段裁掉------堆叠玫瑰的外沿是扇区累计,不是最长的那一格。以后组件化,这个标尺就是一个参数(speedMax)------这张阵用的就是它。

js 复制代码
// monthly.mjs ------ 月度切片:12 × 16 × 6,月度玫瑰阵的原料(直接接在第二节代码后面:同一文件,SECTORS/KT/sectorOf/bandOf/rows/iD/iS/iV 沿用上文)

const byMonth = Array.from({ length: 12 }, () => []);

for (const r of rows) {

if (r[iD] === "M" || r[iD] === "" || r[iS] === "M" || r[iS] === "") continue;

if (+r[iS] < 1) continue; // 静风不入扇区

byMonth[Number(r[iV].slice(5, 7)) - 1].push(r);

}

let secMax = 0;

for (let i = 0; i < 12; i++) {

const m = Array.from({ length: 16 }, () => KT.map(() => 0));

for (const r of byMonth[i]) m[sectorOf(+r[iD])][bandOf(+r[iS], KT)]++;

const rowT = m.map((rw) => rw.reduce((a, b) => a + b, 0));

const n = rowT.reduce((a, b) => a + b, 0);

const dom = rowT.indexOf(Math.max(...rowT));

secMax = Math.max(secMax, Math.max(...rowT) / n);

console.log(`${i + 1} 月 主导 ${SECTORS[dom]} ${(rowT[dom] / n * 100).toFixed(1)}%(入扇区 ${n})`);

}

// → 1 月 NNW 14.3% | 2 月 N 21.0% | 3 月 N 11.1% | 4 月 ESE 14.5% | 5 月 SE 16.5%

// | 6 月 SE 17.4% | 7 月 S 48.2% | 8 月 SE 21.8% | 9 月 E 18.2% | 10 月 N 25.0%

// | 11 月 N 21.8% | 12 月 NNW 21.3%------换手线一眼读完

const SCALE = Math.ceil(secMax * 100 / 5) * 5; // 共尺:基数是堆叠外沿(扇区累计),向上取到 5 的倍数

console.log(`共尺:满圈统一锁 ${SCALE}%,十二张谁也不许各自拉满`);

// → 满圈统一锁 50%(圈线 25% / 50%)

六、选型与收尾:一年看完了

读完这一篇,你能用一年报文做出本地风玫瑰------第二节给的是「做」,这一节补上「选」。三种统计画法一张表收全:

图 回答什么 丢什么 输入 典型场合
风玫瑰 一年到头:从哪来 × 多大 × 多常来(联合分布) 时间------哪天哪时都不在;极端没有体积 逐时样本(来向+风速),剔静风缺向 站点风况速览、年报
频率条 只答从哪来(边际) 整个速度维度 同一套样本、同一套剔除 快讯卡片、小屏
月度玫瑰阵 一年怎么换手(年内节奏) 年际趋势(第 6 篇的事) 同上,按月切片,十二张共尺 季风换手、逐月对照

三条常见错法,每条一句拆解:

  1. 拿小样本当常态------24 时次是抽签,700 上下勉强够形状,8,502 才谈常态(第四节那五副面孔就是证据)。

  2. 把剔除说成没有------静风、缺向剔出扇区 ≠ 不存在;样本量披露行(n、静风、缺向、缺速)是玫瑰的一部分,不是附录。

  3. 拿玫瑰答极端------极端时次在 8,502 行里没有体积(第一节的 0.01%);答「哪天最猛」,换事件画法:打标记、不铺带(第 1 篇定的规矩)。

复现路径:取数(一条 curl)→ 剔除账+玫瑰统计(第二节代码,档边界随参数)→ 边际频率+三档对照(第三节代码)→ 月度切片(第五节代码)------三段 JS 按序贴进同一个文件跑,把表摆成瓣=极坐标堆叠条形,第 1 篇图 05 就是成品。零依赖,Node 20+ 直接跑,连装包一步都省了------统计图没有逐帧、没有粒子,是全系列最轻的性能账(第 3 篇那套用不上)。

边界最后说一次:本篇讲的是把一年的风叠进统计图的画法,是数据可视化,不是天气研判------不做任何气候结论(风变没变是第 6 篇的活,得走再分析数据);跑道与侧风分量不碰;剔除账随图披露;极端 ≠ 常态。

一年看完了,三十年呢------把四种画法套上去,会看见一个不太舒服的趋势。第 6 篇《风的三十年》。玫瑰看不见的那一天,另有一篇把它摊开看------台风的风圈怎么画才不错。这两篇都不承诺日期;这个系列从来不承诺。

一年叠成一朵,风的性格就在瓣里。


署名:YaoJaro

数据来源:地面逐时观测(IEM ASOS 归档·上海浦东 ZSPD:公有领域,无准确性担保;窗口 2024-01-01T00:00Z → 2024-12-31T00:00Z(末点开,365 个整日),例行报文 8,760 行,全篇单管道、无模式数据)|统计管线零第三方依赖(Node 20+),文中数字均可由随文代码复算


下一篇《风的三十年》------把四种画法套到三十年上,看一个不太舒服的趋势。如果这一篇让你想把一年的风叠成一朵,欢迎在掘金关注,系列接下来一篇一个台阶。

相关推荐
UNIHHIOT4 小时前
开源一套工业物联网平台:驱动崩了不影响别的协议,64MB 堆跑边缘网关
前端·后端
deli0075 小时前
秒杀超卖实测:1000 个并发请求下 SQL 加锁前后差多少
前端
徐小夕5 小时前
JitKnow知识库4.8发布:本体+知识图谱检索引擎,让AI回答不再“一本正经地胡说八道”
前端·vue.js·github
智迁5 小时前
MySQL 到 Kingbase 实时同步:从 Docker 部署到增删改验证
前端·后端
tryCbest5 小时前
Vue3——第 11 篇:组合式 API(上)——setup 与响应式 API 深度解析
前端·javascript·vue.js
tryCbest6 小时前
Vue3——第 13 篇:计算属性与侦听器——computed 与 watch 的进化
前端·javascript·vue.js
怒放的生命19916 小时前
本地用 pnpm 省磁盘,团队继续用 npm:极简实践
前端·npm·node.js
believed_breadfruit6 小时前
我开源了一个HTML 分页导出工具,解决了复杂页面的内容截断、跨页错乱和长文档白屏问题
前端·javascript·github
福狼7 小时前
从“网墙”到柔性曲面:一次 WebGPU 波浪的逆向分析与重建
前端·逆向