信息速览
NDBC 是海洋机器学习里最容易被当成「配角」的一份数据:它不生产图,只生产点。卫星产品给你整片海的温度,浮标只告诉你它脚下那一小块水面此刻几度、风多大、浪多高。
也正因为如此,它几乎是所有格点产品的校验源。想判断一颗卫星的反演有没有偏、想给再分析场找一个不参与同化的独立参照、想确认某个海湾的养殖水温到底是多少,最后都要落到这些点上。
这份条目只讲 标准气象数据(stdmet)这一路接口:站表、实时目录、归档目录。不展开波浪谱密度、连续风、声学多普勒流剖面这几条平行通道,也不涉及 NetCDF/THREDDS 镜像。
| 项目 | 数值 | 来源 |
|---|---|---|
| 官方全名 | NOAA National Data Buoy Center observations(Standard Meteorological Data) | 官方度量说明页 |
| 运营方 | NOAA 国家气象局 / 国家数据浮标中心 | 站页页脚 |
| 活动站数 | 1,352(2026-10-29T10:45:03UTC) | 站表 count 属性 |
| 站型构成 | 固定 710 / 浮标 438 / DART 76 / 钻油 54 / TAO 48 / 其它 24 / USV 2 | 站表 type 属性统计 |
| 气象在测站 | 863(met="y") |
站表属性统计 |
| 站表体积 | 272,488 字节 | HTTP Content-Length |
| 归档站点 | 1,350 站、17,049 个年文件 | 归档目录索引计数 |
| 归档年份 | 1970–2025 | 文件名年份解析 |
| 主记录列数 | 18 列定宽文本 | 文件表头 |
| 时间步长 | 整点逐小时;部分站 10 分钟 | 文件间隔 |
| 实时窗口 | 约 45 天 | 官方度量说明页 |
| 缺测写法 | 实时 MM;归档 99.00/999 |
官方度量说明页 |
| 访问门槛 | 无注册,无令牌 | 目录可直接列出 |
| 许可 | 公有领域(联邦作品),无 SPDX | 无独立许可页 |
§1 速览:它是点,不是面
§1.1 一句话定义
NDBC 是一张由一千多个海上观测点组成的网:每个点固定在一个有名字的坐标附近,按小时(少数按十分钟)报一次风、气压、气温、水温、浪高,从 1970 年代起陆续开张,一直报到今天。
它没有网格,没有插值,没有「每一格都有值」这件事。它给你的是一行一行的时间戳,每一行背后是一次真实发生的测量,或者一次明确的失败。
§1.2 为什么它不是「又一份海洋数据」
第一层原因,它不可被插值出来。格点产品的价值在于覆盖,浮标的价值在于它不是算出来的。当你想知道某个模型在开阔洋面的偏差有多大,能拿来当尺子的只有不参与建模的独立观测——而开放海域的独立观测,基本上就是这些浮标。
第二层原因,它的缺失是一种信息。卫星产品缺值意味着「今天云太厚」,浮标缺值意味着「这个传感器坏了」或「这个站根本没装这个传感器」或「这个站上月被回收了」。三种情况在文件里长得一模一样,要区分只能去站页看载荷与维护记录。这一点是格点产品里没有的维度。
第三层原因,它的历史不是一条连续的线,而是一串彼此不重叠的线。每个站有自己的起止,站与站之间不复用坐标,退役的站还在归档里,新开的站不在旧文件里。想拼一条四十年的序列,得先在四百多个已退役的站里找到那个一直活着的。
§1.3 三十秒决策卡:你的处境,它能帮你什么
| 你的处境 | 它能帮你什么 |
|---|---|
| 要验证海温格点产品准不准 | 逐站的现场水温,独立于卫星反演与再分析同化 |
| 要训练一个海浪预报或补缺模型 | 有效波高、主波周期、平均周期三联,逐小时,含大量失败样本 |
| 要做飓风、风暴潮的路经与强度复盘 | 风暴中心擦过的浮标给出不中断的高频气压与风速序列 |
| 要一份「某片海到底多冷」的判决依据 | 可以,但要先确认那个站今天还在,且当年装了水温传感器 |
| 要一张全球覆盖的海温图 | 给不了,这是点集,太平洋腹地、南大洋、印度洋近乎空白 |
| 要等间隔、无洞、可直接喂进时序模型的整齐表 | 给不了,每个站的列构成与时间步长都不同 |
§1.4 不适合谁
如果需求是空间连续场,这份数据会让人做无用功。全部站里,七百多个固定在北美沿岸、五大湖与内河河口,只有半数浮标在开阔洋面。把它们的经纬度画在地图上,看到的是一条沿美国海岸与北大西洋航线的带状分布,中间大片留白。用它做「全球平均海表温度」,得到的是「北美沿海浮标平均水温」。
如果需求是开箱即用的规整表,同样不合适。官方给的是定宽文本,不是关系表。同一列在不同年代的列名不同(WD 后来叫 WDIR,BAR 后来叫 PRES,H0 后来叫 WVHT),同一年份不同站的时间步长不同(有的整点,有的每十分钟),同一张表头下各站有效列不同。把它读进来这一步,本身就是大部分工作量。
如果需求是短期业务预报,实时流能用但不该盲信。实时流只经过 NWSTG 侧的自动量程与连续性检查,归档流还要过 NDBC 侧更严的一套限值检查和人工复核。同一天的同一时刻,两条流给的值可能不一样。
如果需求是已退役站的历史,必须绕开站表。站表只列当前活动站;2026 年 10 月,一份曾经长期在报的东海岸浮标已被回收,但它仍留在站表里,页面上写着回收日期,实时文件返回 404。这类边界情况在站表里看不出来。
1.5 贯穿本文的案例:一枚已经回收、却还在站表里的浮标
后面几节会反复回到同一个站:41001,代号「EAST HATTERAS」,在北纬 34.738、西经 72.503,距哈特拉斯角以东约 150 海里,脚下水深 4,462 米。
它的站页写着三件互相矛盾的事。第一,它在今天的活动站表里,type="buoy",坐标齐全。第二,它的实时数据接口返回 404,最近二十四小时、五天、四十五天全部写着「无数据」。第三,页面正文里有一句「Buoy recovered 9/9/26」——2026 年 9 月 9 日,浮标被回收了。
这三件事同时为真,正好构成理解这份数据的入口。站表是「名录」,不是「心跳」;实时流看淡的是「还在不在」,归档流看的是「曾经报过什么」;而一个站的生命周期,只有把站表、实时目录、归档目录、站页四份材料叠起来才看得全。
§3.3 会用它讲站号与坐标,§4.2 会用它讲回收与缺测的关系,§5.4 会用它讲取数时的探测手法,§7.3 会用它讲「看得见但取不到」这一类边界,§8.4 会用它讲为什么这份数据必须靠「站」而不是「数据集」来维护。
§2 数据构成:十八列,两套哨兵,三种目录
§2.1 规模:哪些是官方口径,哪些是数出来的
官方从不公布总站数、总体积这类数字——它每天在变,写死就会过期。站表自己在 count 属性里给了这一刻的数。
| 维度 | 数值 | 怎么得到的 | 性质 |
|---|---|---|---|
| 活动站 | 1,352 | 站表 count 属性与 station 元素计数一致 |
官方 |
| 其中固定站 | 710 | type="fixed" |
属性统计 |
| 其中浮标 | 438 | type="buoy" |
属性统计 |
| 其中海啸 DART | 76 | type="dart" |
属性统计 |
| 其中钻油平台 | 54 | type="oilrig" |
属性统计 |
| 其中 TAO 锚系 | 48 | type="tao" |
属性统计 |
| 其中其它与 USV | 26 | type="other" 24 个、type="usv" 2 个 |
属性统计 |
| 气象在测 | 863 | met="y" |
属性统计 |
| 海流在测 | 少数 | currents="y" 另计,与 stdmet 无关 |
属性统计 |
| 归档站点 | 1,350 | 归档目录文件名去重 | 目录 |
| 归档年文件 | 17,049 | 归档目录计数 | 目录 |
| 归档中最老的年份 | 1970 | 文件名年份解析,全目录仅 1 个该年文件 | 目录 |
| 归档中 2024 年文件 | 784 | 按年份分桶 | 目录 |
三个数字值得单独说。
一个是站型与「是不是浮标」不是一回事。438 个 buoy 只是系泊浮标;710 个固定站里九百公里外的灯塔、河口验潮站、大湖边的码头上都算,它们测的是同一个 18 列气象量,只是仪器高度与代表性完全不同。一个把「浮标」当筛选条件的脚本,会漏掉 863 个在测站里的 554 个。
另一个是归档站数略少于活动站数。1,350 对 1,352,看着像是几乎相同,实际上交集只有 908 个:有 440 个站在归档里有记录、在今天的站表里没有;反过来,有四百多个站在站表里有、在归档目录里一个文件也没有。两组数字接近纯属巧合,不能拿总数去想象「这批站就是那批站」。
第三个是年文件不是全覆盖。这些文件分给全部一千三百多个归档站,平均 12.6 个,中位数只有 4 个——大量站只报过不到五年就被拆了。活过二十年的站大约一百个。这与「NDBC 有五十多年历史」这句宣传不矛盾:历史属于机构,不属于任何一个站。
§2.2 一个文件里到底有什么
stdmet 文件是纯文本,两行以 # 开头的表头(第一行是列名,第二行是单位),下面是数据行。当前格式 18 列。
| 列 | 单位 | 含义 | 常见状态 |
|---|---|---|---|
| YY | 年 | 四位(早年两位) | 总有 |
| MM | 月 | 1–12 | 总有 |
| DD | 日 | 1–31 | 总有 |
| hh | 时 | UTC | 总有 |
| mm | 分 | 当年格式有,早年无 | 总有 |
| WDIR | 度 | 风来向,真北 0 顺时针 | 多数有 |
| WSPD | 米每秒 | 8 分钟平均(岸站 2 分钟) | 多数有 |
| GST | 米每秒 | 阵风,5 或 8 秒峰值 | 多数有 |
| WVHT | 米 | 有效波高 | 只有带波浪计的站有 |
| DPD | 秒 | 主波周期 | 随 WVHT |
| APD | 秒 | 平均波周期 | 随 WVHT |
| MWD | 度 | 主波向 | 随 WVHT |
| PRES | 百帕 | 海面气压 | 多数有,早年少 |
| ATMP | 摄氏度 | 气温 | 多数有 |
| WTMP | 摄氏度 | 海表温度 | 多数有 |
| DEWP | 摄氏度 | 露点 | 多数有 |
| VIS | 海里 | 能见度 | 浮标站限 0–1.6,常年缺 |
| PTDY | 百帕 | 三小时气压倾向 | 归档文件中常缺 |
| TIDE | 英尺 | 潮位,相对 MLLW | 极少数站有 |
表里最后一列「常见状态」是你最该先看的一列。十八列摆在那里,会让人以为每个站都测十八样东西,实际上一个典型的系泊浮标只报风和气压、气温水温,波浪要看有没有装波浪计,能见度与潮位基本是别的设备的事。
§2.3 缺测不是空,是 MM,或者一串 9
这是全篇最需要记住的一条,官方文档把它写在度量说明页的第二段:
Missing data in the Realtime files are denoted by “MM” while a variable number of 9’s are used to denote missing data in the Historical files, depending on the data type (for example: 999.0 99.0).
两个字母 MM,两句之隔就是月份列 MM。同一个文件里,第 2 列叫 MM(月份),数据区里却也出现 MM(缺测),位置不同、含义无关。用空格切分后按列号取值不会出错,因为列号是固定的;但用正则去替换「所有 MM」就会连月份一起换掉。
年份分用 9 也不统一。2026 年 9 月的一份归档文件里,同一行会同时出现 99.00(波高、波周期缺)与 999(波向缺)与 99.0(能见度缺)与 999.0(露点缺)与 99.00(潮位缺)。五种写法,五个不同的小数位数,取决于这一列的物理量纲与历史上被写死进格式的位数。
这就是为什么直接用 float() 读归档文件会出一场静默的灾难:99.00 能顺利解析成 99.0,程序不报错,于是「浪高 99 米」与「气温 99 摄氏度」顺利进入统计。实时文件里的 MM 反而安全,因为 float("MM") 会当场抛异常。
| 位置 | 缺测写法 | 直接 float 的后果 |
|---|---|---|
| 实时文件 | MM |
抛 ValueError,当场暴露 |
| 归档文件(当年月包) | 99.00 / 999 / 99.0 / 999.0 |
解析成功,静默变成极大值 |
| 归档文件(早年年度文件) | 99.00 / 999 / 99.0 / 999.0 |
同上 |
| 表头内 | MM(月份列名) |
若整行参与解析会把表头当数据 |
有一个更隐蔽的坑:归档文件的缺测不是「这一列没测」,而是「这一列的哨兵值」。同一列在不同站上哨兵长度不同——波高写 99.00,但早年有的站写 99.0;气压缺测写 999.0,而真实气压 999.0 百帕是合法值(约等于 1999 年那次东太平洋飓风时的中心气压量级)。所以判缺测不能只比较数值大小,得按列宽取、按列定义判。
§2.4 一条观测记录的逐项解剖
取 2026 年 10 月 29 日 10:00 UTC、浮标 42001(墨西哥湾中部)的一行实时记录:
| 层面 | 内容 | 要注意什么 |
|---|---|---|
| 原始行 | 2026 10 29 10 00 40 8.0 10.0 MM MM MM MM 1016.0 25.8 27.0 22.0 MM -1.0 MM |
空格分隔,非严格定宽 |
| 站 | 42001,MID GULF,北纬 25.922 西经 89.638 | 坐标来自站表,不是这一行 |
| 时刻 | 2026-10-29T10:00Z | UTC,无时区后缀 |
| 风 | 40 度、8.0 米每秒、阵风 10.0 | 来向 40 度即东北风 |
| 浪 | 全部 MM | 这一刻没有波高,不等于这个站没有波浪计 |
| 气压 | 1016.0 百帕 | 合法 |
| 气温 | 25.8 摄氏度 | 合法 |
| 水温 | 27.0 摄氏度 | 比气温高 1.2 度,正常 |
| 露点 | 22.0 摄氏度 | 与气温差 3.8 度,相对湿度约 79% |
| 能见度 | MM | 浮标站几乎永远这样 |
| 气压倾向 | -1.0 百帕 | 三小时下降 |
| 潮位 | MM | 墨西哥湾浮标没有潮位测量 |
这一行里最容易被当成「数据坏了」的是连续四个 MM(波浪四项)。把它理解成「这个站坏了」会误导。同一份文件往前翻十分钟,WVHT 是 2.0;再往前十分钟,DPD 又是 MM。波浪观测的有效率天然低于风与气压——这一点在 §4.4 会展开。
§2.5 空间与时间的边界
时间只有 UTC,没有本地时。 官方度量说明页写明:站页默认显示站点本地时,实时文件与归档文件一律只给 UTC。跨越日界线的站在本地日历上会「提前一天」或「落后一天」,做日聚合时必须自己决定按哪个日历。
早年没有分钟列。 1985 年的归档文件表头是 YY MM DD hh WD WSPD GST WVHT DPD APD MWD BAR ATMP WTMP DEWP VIS,只有四列时间,步长本身就是一小时。1990 年代初开始出现分钟列,但早期仍按整点报。把两个年代的记录拼成时序,时间索引的分辨率会跳变。
坐标是名义坐标,不是逐条坐标。 站表给一个 lat 与 lon,通常三位小数,约合百米级;站页另给 watch circle radius,41001 是 4,638 码(约 4.2 公里)、42001 是 3,032 码(约 2.8 公里)。这个半径表示系泊浮标在锚点周围的正常摆动范围。想要逐条位置,只能看漂流平台格式——那种格式把 LAT 与 LON 写成两列,逐条给出。
站号不是绝对主键。 官方说明写明:五位 WMO 站号自 1976 年起使用,「ID’s can be reassigned to future deployments within the same 1 degree square」——同一个一度方格内,站号可以被后续布放的设备复用。所以「站 41012 的四十年序列」这个说法只在物理意义上勉强成立,它至少包含两次不同的浮标。
经度范围从 -179.797 到 180。 站表里既有西经,也有正值 180.0。这不是数据错,是东太平洋与太平洋岛屿站分别按各自习惯录入。做空间连接时必须先归一。
§3 定位层:站、站号与「这批站是哪批站」
§3.1 一个机构,七种站型
站表 type 字段只有七种取值,但它们背后的实体差别极大。
| 站型 | 数量 | 物理形态 | 测什么 | 经度分布 |
|---|---|---|---|---|
| fixed | 710 | 岸边码头、灯塔、河口验潮亭、大湖栈桥 | 风、气压、气温,少数加水温 | 北美沿岸与大湖高度密集 |
| buoy | 438 | 3 米或 10 米直径系泊浮标 | 全套气象加波浪 | 开阔洋面与五大湖 |
| dart | 76 | 海啸浮标,底压计加水面单元 | 主要报水位,气象多为空 | 环太平洋与印度洋 |
| oilrig | 54 | 墨西哥湾钻油平台 | 风、气压、气温 | 墨西哥湾 |
| tao | 48 | 赤道锚系 TAO/TRITON 阵列 | 风、气温、水温为主 | 赤道太平洋与印度洋 |
| other | 24 | 公私合作站点、Sofar Spotter 波浪浮子、Backyard Buoys 社区浮子 | 视合作方而定 | 分散 |
| usv | 2 | 无人水面艇 | 视载荷 | 旧金山外海 |
这张表说明一件常被忽略的事:NDBC 不是一个「浮标保有单位」,而是一个「观测汇集与分发单位」。站表里 234 个标 owner="NDBC",其余一千一百多个属于 IOOS 区域协会、州政府、大学、国际气象机构,甚至私人石油公司。NDBC 做的是把它们的数据统一成同一套 18 列格式。
§3.2 站号里的地理密码
五位站号的首位数字大致对应区块,虽然官方从未把它写成正式规则。
| 首位 | 大致区域 | 站表内计数(首位匹配) |
|---|---|---|
| 4 | 西大西洋、墨西哥湾、加勒比、北美五大湖 | 约 400 |
| 5 | 东太平洋、夏威夷、太平洋岛屿 | 约 90 |
| 6 | 欧洲与大西洋东部(英法爱国际伙伴站) | 约 40 |
| 1 / 2 / 3 | 南大洋、印度洋、国际伙伴 TAO 与 Moored Array 站 | 约 80 |
这套编号只是「大致」,反例很多:42001 在墨西哥湾、51001 在夏威夷西北、64045 在苏格兰以西的 K5 锚系。真正可靠的产地信息在站表的 owner 与 pgm 两个字段里。
§3.3 站号、坐标与显示名三者的关系
41001 这一枚浮标的三个身份标识,分别对应三种不同的用途。
| 标识 | 值 | 谁在用 | 稳定性 |
|---|---|---|---|
| 站号 | 41001 | 文件名、URL、所有下游数据库的外键 | 在同一个一度方格内可被复用 |
| 坐标 | 34.738 N / 72.503 W | 空间连接、地图符号 | 名义值,实际摆幅 ±4.2 公里 |
| 显示名 | EAST HATTERAS - 150 NM East of Cape Hatteras | 人读、搜索 | 会随布放位置调整而改写 |
三者的不一致是常态。站表里 41001 的 name 是「EAST HATTERAS - 150 NM East of Cape Hatteras」,站页标题写「LLNR 635」,owner 是 NDBC,pgm 是「NDBC Meteorological/Ocean」。LLNR 是本地编号,不进文件名;档号在这里只出现在站页正文,不进主键。
所以按站号查、按坐标查、按名字查,会得到三套不同的结果集。做数据引用时唯一该写进元数据的是站号加取数日期——名字会被改,坐标会被微调,站号会被复用,三者都有风险,但站号至少是可检索的。
§3.4 名字、文件名与唯一指认
归档文件的命名极不规则,这是很多人第一次接触时会踩的坑。
| 目录 | 文件命名 | 例子 |
|---|---|---|
| 历史年归档 | <站号>h<年>.txt.gz |
42001h2024.txt.gz |
| 历史连续风 | <站号>c<年>.txt.gz |
42a01c2003.txt.gz |
| 历史波浪谱密度 | <站号>w<年>.txt.gz |
45T01w2018.txt.gz |
| 历史海流 | <站号>o<年>.txt.gz |
32st1o2009.txt.gz |
| 历史流剖面 | <站号>a<年>.txt.gz |
41008a2005.txt.gz |
| 当年已封闭月 | <站号><月号><年>.txt.gz |
4200152026.txt.gz(5 月) |
| 当年当月 | <站号>.txt |
42001.txt,位于 /data/stdmet/Sep/ |
七种命名,五种字母后缀,两种大小写(45T01 里的大写 T 真实存在)。写通配符时用 * 比写 h 安全,但要注意 h 与 c、w、o、a 不会同时出现在同一个站号下——这四个是不同数据类型,彼此不覆盖。
§3.5 站表与归档目录是两份清单,不是一份
这是全篇的第二条边界,也是长期序列最容易出错的地方。
| 口径 | 站数 | 说明 |
|---|---|---|
| 活动站表(2026-10-29) | 1,352 | 此刻在册的站,含已回收但未摘牌的 |
归档目录 historical/stdmet |
1,350 | 有至少一个年文件的站 |
| 两者交集 | 908 | 既在册、又报过历史 |
| 只在归档 | 440 | 已退役,历史还在 |
| 只在站表 | 444 | 在册,但当前目录里没有年文件 |
「只在站表」的四百多个站,多数是当年新开或近两年才接入的:它们的记录还在实时流与当年月目录里,还没被折叠成年文件。也就是说,同一个站的数据可能同时存在于三个位置:实时文件(最近 45 天)、当年月目录(本年度已封闭的月份)、历史年归档(上一年及更早)。三处不重叠,拼一份完整序列必须三处都取。
「只在归档」的四百多个站,是这份数据真正的历史价值所在:它们大多布放在 1970 年代到 1990 年代,物理上早已沉没或被拖回,但记录还在。任何以站表为起点的脚本,都会在第一步就把这批站漏掉。
§4 质量层:它最容易被误用的四个地方
§4.1 两套缺测写法会同时出现在一个目录里
先说结论:同一份数据里,MM 与 99.00 不是先后关系,是目录关系。实时目录用前者,归档目录用后者,两者不会交叉,但使用者往往会在同一个脚本里同时处理这两类文件——因为完整的一条序列必然要横跨两头。
这种安排带来一个反直觉的后果:没有值比有值更容易被发现。实时文件里的 MM 会抛异常,归档文件里的 99.00 不会。于是最常出错的反而是「历史上那段看起来最完整的数据」。
判别办法只有一条:按列宽读,不按空格切。官方声明的格式是定宽文本,虽然实际上不同年代、不同年份的行宽会有 ±1 字符的出入(2025 年的 42001 年文件里同时存在 88 与 89 字符两种行宽),但列位置是稳定的。按固定字符位置切片,99.00 与 99.0 会各自落回它该去的列,也不会把 999 误读成 99 与 9。
#-- 按官方声明的列位切归档文件,而不是 split();缺测按列定义判
COLS = [(0, 4), (5, 7), (8, 10), (11, 13), (14, 16),
(18, 21), (23, 27), (29, 33), (35, 40), (42, 47), (49, 54), (56, 59),
(61, 67), (69, 74), (76, 81), (83, 88), (90, 93), (96, 102)]
NAMES = ["YY", "MM", "DD", "hh", "mm", "WDIR", "WSPD", "GST", "WVHT", "DPD",
"APD", "MWD", "PRES", "ATMP", "WTMP", "DEWP", "VIS", "TIDE"]
line = "2025 01 01 00 00 310 4.4 5.4 99.00 99.00 99.00 999 1012.7 25.7 26.1 999.0 99.0 99.00 "
rec = {}
for (a, b), name in zip(COLS, NAMES):
tok = line[a:b].strip()
# 归档文件的缺测是一串 9:整列全 9 即缺测,没有其它合法值能长得像它
rec[name] = None if tok and set(tok) <= {"9", ".", "+", "-"} else tok
print(rec["WVHT"], rec["WTMP"], rec["PRES"])
#-- 期望:None, '26.1', '1012.7'
这段代码的两个要点:set(tok) <= {"9", ".", "+", "-"} 判的是「整串只由 9 和小数点构成」,99.00、999、999.0 全部命中;1012.7 里有两个 1 一个 2,不命中。第二个要点是列位表——它按 §2.2 的表逐列对齐,不是猜的。换年份时要先看一眼表头,早年文件的列位完全不同。
§4.2 §1.5 的案例:回收日期比站表更可靠
回到 41001。今天它仍在站表里,met="n",坐标齐全,但实时接口返回 404。要判断它是否还在服役,靠的不是站表,是站页正文里的那一句「Buoy recovered 9/9/26」。
这个案例揭示了一个更大的问题:站表记录的是「在册」,不是「在测」。官方在首页图例里区分了「最近 8 小时有数据的站」与「只有历史数据的站」,但站表 XML 里没有这个状态字段。只有 met、currents、waterquality、dart 四个「这一类传感器有没有配」的标记,没有「今天还在不在报」。
于是判断一个站是否活着,得靠三种间接证据。
| 证据 | 怎么查 | 判读 |
|---|---|---|
| 实时文件是否 404 | 直接请求 realtime2/<id>.txt |
404 表示没有任何最近数据 |
| 实时文件最后一行时间 | 读文件末行 | 距今天超过数小时即停报 |
| 站页维护提示 | 抓站页正文找「recovered」「operational」等字样 | 有明确回收日期即可判定 |
| 当年月目录是否有该站文件 | 请求 /data/stdmet/<月>/<id>.txt |
有文件说明本月至少报过 |
| 归档最新年份 | 查归档目录里该站的最大年份 | 决定历史序列的右端 |
五种证据各有盲区:404 可能只是当日文件还没生成;末行时间可能只是当日尚未到整点;站页提示是散文,需要解析;月目录文件存在不代表内容非空;归档右端滞后于实时。没有一种能单独定论。
§1.5 的案例还说明另一件事:41001 在 2026 年 9 月 9 日被回收,但它 2026 年 5 月到 7 月的归档数据仍然可以通过站页列出的链接取到(月包形式),实时接口却已全部 404。「看得见、取不到」与「取得到、看不见」在这份数据里同时存在,判断口径必须分开。
§4.3 §1.5 的案例之后:哪些记录其实不属于这个站
41001 站页还有一行常被忽略:2026 年 5 月起,它的数据源换成了另一种载荷,页面写明「3-meter foam buoy SCOOP payload」。同一站号下,前后两段数据的仪器不同、采样方式不同、传感器高度不同。
这种「同名不同器」在这份数据里是常态,且有三种不同程度的表现。
| 程度 | 表现 | 影响 |
|---|---|---|
| 同一站号,换载荷 | 41001 换 SCOOP;46012 从 Saildrone 换成 Orchard USV | 仪器高度、采样率、精度全变 |
| 同一站号,换物理实体 | 站号在同一度方格内被重新布放 | 四十年序列至少含两次不同浮标 |
| 同一坐标,换站号 | 旧站退役、新站顶替(名字里常写「Site of 46012」) | 按坐标查会命中两个站 |
第二种最隐蔽。官方明说站号可复用,但没有公布一张「某站号在哪些年份对应哪个物理实体」的映射表。唯一能查的是站页下方的历史记录,而那是逐站手工维护的散文,进不了自动化管线。
所以「站 42001 四十年的海温序列」这句话,在物理意义上应该读作「站号 42001 所在位置附近,四十年来由若干台不同设备测得的表层温度」。做趋势分析时,这个差别至少要在方法一节里写清楚。
§4.4 列头固定,但常年全空的列不一样
十八列的表头在所有站上完全相同,这是这份数据最友好的地方,也是最容易误判的地方。
取四个站在 2026 年 10 月 29 日实时文件里的有效情况:
| 站 | 行数 | WVHT 缺测率 | VIS | TIDE | 另有常年全空 |
|---|---|---|---|---|---|
| 42001(墨西哥湾浮标) | 6,476 | 50.6% | 100% | 100% | PTDY 83.2% |
| 51001(夏威夷西北浮标) | 6,457 | 波浪时有时无 | 100% | 100% | 视时段 |
| 44013(波士顿外海浮标) | 6,474 | 约一半 | 100% | 100% | 视时段 |
| 64045(苏格兰以西 K5) | 25 | 100% | 100% | 100% | 全部气象也多缺 |
同一列 42001 的波高缺一半、64045 的波高全缺。如果按「某一列缺测率超过九成就删掉这一列」的规则清洗,前三个站保住波高,64045 会连风一起被删——而 64045 这一刻本来就没有风。
更麻烦的是列名的历史漂移。官方在度量说明页末尾专门列了一张「已废弃缩写」表:WD 与 DIR 都改叫 WDIR,SPD 改叫 WSPD,GSP 改叫 GST,GMN 改叫 GTIME,BARO 改叫 PRES,H0 改叫 WVHT,DOMPD 改叫 DPD,AVP 改叫 APD,SRAD 与 SRAD2 改叫 SWRAD,LRAD 与 LRAD1 改叫 LWRAD。
一张 1985 年的文件表头写 WD 与 BAR,一张 2026 年的写 WDIR 与 PRES。按列名解析的代码,跨 1990 年代就会断。
§4.5 浮标会漂,坐标有三层含义
51001 的站表坐标是北纬 24.475、西经 162.03。站页给的观察圈半径没有直接写在这份站表里,但同类站可以参照:41001 的 4,638 码约 4.2 公里,42001 的 3,032 码约 2.8 公里。
也就是说,一个名义坐标为 24.475 N 的站,实际观测点可能落在半径四公里内的任何地方。对海温这种空间尺度较缓的量,四公里几乎无影响;但对海面风与浪,四公里的位移足以改变有效吹程与水深。
三层坐标的含义要分清。
| 层 | 出处 | 含义 | 精度 |
|---|---|---|---|
| 名义坐标 | 站表 lat/lon |
锚点位置 | 三位小数,约 100 米 |
| 观察圈 | 站页 watch circle radius |
允许摆动的半径 | 通常 1–5 公里 |
| 实际坐标 | 漂流格式的 LAT/LON 列 |
逐条记录的位置 | 两位小数,约 1 公里 |
只有漂流平台与无人水面艇的 stdmet 里有第三层。系泊浮标没有——想要实际位置,只能从观察圈推一个不确定区间。这对把浮标当「点」用于卫星像元匹配的人是个实质限制:你只能证明「观测发生在锚点 4 公里内」,不能证明「发生在锚点正上方」。
§4.6 实时流与归档流是两条时间线
官方对这两条流的定性写得很清楚:实时文件是「went through automated quality checks and were distributed as soon as they were received」的数据,归档文件是「gone through post-processing analysis and represent the data sent to the archive centers」的数据。
两句话点出三个差别。
质控的深度不同。 实时流只过 NWSTG 侧的自动检查:传输奇偶校验、量程限值、时间连续性、阵风与风速比这类关系检查、气压传感器电池电压检查。归档流在 NDBC 侧还要过更严的量程与时间连续性限值、重复传感器一致性比对,以及人工机助复核(把风速与波浪谱能量画在一起看、看时序列图与天气图)。
时效不同。 实时流覆盖最近约 45 天,随收随发;归档流按年、按月生成,当月的数据要等月结束才封闭,上一年的年度文件要等下一年的处理周期。
缺测写法不同。 这一点在 §2.3 已经展开,此处只补一句因果:实时流的 MM 是「此刻没收到或没过自动检查」,归档流的 99.00 是「后处理时判定这一格不可用」。两者可能对同一格给出不同结论。
同一个时刻,两条流给的值也可能不一致——这不是错误,是两条不同处理链的正常产物。做近实时应用的人用的是前者,做完备分析的人用的是后者,把两者拼成一条序列而不做标记,等于在一条曲线上混入两种误差结构。
§5 工具层:怎么取、怎么读、怎么验
§5.1 四条获取路径
| 路径 | 形态 | 适合 | 注意 |
|---|---|---|---|
| 站表 XML | 单文件,272,488 字节 | 拿当前站名录与坐标 | 每几分钟重生成,须记 created 时间 |
| 实时目录 | 每站一个 .txt,最近约 45 天 |
近实时监控、最近窗口回溯 | MM 缺测,未过人工质控 |
| 当年月目录 | /data/stdmet/<月英文>/ |
取本年度已封闭的月份 | 已封闭月份是 .gz 月包,当月才是单站 .txt |
| 历史年归档 | /data/historical/stdmet/ |
取上一年及更早的整年 | 一站一年一文件,1990 年代前格式不同 |
四条路给的字段同一套,格式同一套,只有缺测写法与目录结构不同。写死路径的脚本换条路就要改。
§5.2 先量站表,再谈取数
站表只有一份,两百多 KB,但它决定了后面所有循环的规模。
#-- 取站表并统计站型;count 属性与元素数应一致
curl -sS https://www.ndbc.noaa.gov/activestations.xml -o activestations.xml
grep -o '<station ' activestations.xml | wc -l
grep -o 'count="[0-9]*"' activestations.xml | head -1
#-- 期望:两者都是 1352(2026-10-29 抓取时)
#-- 按 type 与 met 两个属性交叉统计,决定后面要循环哪些站
import xml.etree.ElementTree as ET
from collections import Counter
root = ET.parse("activestations.xml").getroot()
st = root.findall("station")
print("created", root.get("created"), "count", root.get("count"))
print(Counter((s.get("type"), s.get("met")) for s in st).most_common(8))
#-- 期望:('fixed','y') 与 ('buoy','y') 占大头,('fixed','n') 也很多
这两段一起做的意义在于:你会发现 met="n" 的站有 441 个,它们即使有文件,多半也只有浪或只有水位。把它们全部纳入 stdmet 循环,会拉进来大量空文件。
§5.3 拉一个站,先看表头再决定解析器
不同年代的文件表头不同,解析器不能写死。
#-- 只读前两行,判断这是哪种年代的格式
import urllib.request
def head(url, n=3):
req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
with urllib.request.urlopen(req, timeout=60) as r:
return [next(r).decode("ascii", "replace").rstrip("\n") for _ in range(n)]
for u in [
"https://www.ndbc.noaa.gov/data/realtime2/42001.txt",
"https://www.ndbc.noaa.gov/data/stdmet/Sep/42001.txt",
"https://www.ndbc.noaa.gov/view_text_file.php?filename=42001h2025.txt.gz&dir=data/historical/stdmet/",
]:
print(u.split("/")[-1][:40], head(u))
#-- 期望:实时与当年月文件有 18 列、含 mm;年归档文件缺 PTDY 列(17 列)
第三段的结果会揭示一件官方文档没有明说的事:当年月文件的表头有 PTDY,年度归档文件的表头没有。度量说明页写「PTDY … (not in Historical files)」,但当年月文件严格说不算「historical files」——它们还没被折叠进年归档。于是同一个月的数据,在当月目录里带气压倾向、在下一年的年文件里就没了。这是一处真实的、会让「回补历史」与「滚动取数」得到不同列集的边界。
§5.4 §1.5 的案例在取数这一层的落地
给 41001 写一个「它还在吗」的探测函数,会把 §4.2 那张证据表变成具体代码。
#-- 三级探测:实时文件、当年当月文件、归档目录里最大年份
import re
import urllib.error
import urllib.request
def probe(sid, months=("Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec")):
base = "https://www.ndbc.noaa.gov/data/realtime2/"
try:
req = urllib.request.Request(base + sid + ".txt", headers={"User-Agent": "Mozilla/5.0"})
with urllib.request.urlopen(req, timeout=30) as r:
last = r.read().decode("ascii", "replace").splitlines()[-1]
return "realtime", last[:16]
except urllib.error.HTTPError as e:
if e.code != 404:
raise
# 实时没了,看本年度已封闭月份是否还有归档
for m in months:
u = f"https://www.ndbc.noaa.gov/data/stdmet/{m}/{sid}.txt"
try:
urllib.request.urlopen(urllib.request.Request(u, headers={"User-Agent": "Mozilla/5.0"}), timeout=30)
return "monthly", m
except urllib.error.HTTPError as e:
if e.code != 404:
raise
return "archived-only", None
print(probe("41001"))
#-- 期望:('archived-only', None)——9 月被回收后,当月目录里的文件也已清空
这个函数的返回值只有三种,但对应五种现实情况:实时有数据(在测)、当年月份有数据(本月停报)、只有归档(已回收)、也许还有第四种「实时有但当年月份没有」(新开站,本月还没归档),以及第五种「三处都有」(正常在测的老站)。判断一个站的状态,需要多次请求,没有单一字段能回答。
§5.5 批量取归档:先列目录,再定并发
归档目录是一个 Apache 索引页,1.5 MB 上下的 HTML,包含全部年文件名。拿它一次就能建好完整的下载清单。
#-- 列目录、抽文件名、按站聚合;不下载数据
curl -sS https://www.ndbc.noaa.gov/data/historical/stdmet/ \
| grep -o 'href="[^"]*\.txt\.gz"' \
| sed 's/href="//; s/"//' > files.txt
wc -l files.txt
#-- 期望:17049
cut -d'h' -f1 files.txt | sort -u | wc -l
#-- 期望:1350(含少数站号里本身带字母、被 cut 切错的情况,需人工核对几行)
cut -d'h' 这一行是故意留的坑:站号里可能含字母 h(站表里确有 0y2w3、54nr7 这类编号),用字母切分会把它们切开。更稳的写法是用正则剥离末尾的 h<年>.txt.gz。
#-- 用正则剥离尾部年份,站号里的字母不会被误伤
sed -E 's/h[0-9]{4}\.txt\.gz$//' files.txt | sort | uniq -c | sort -rn | head -5
#-- 期望:前几行是报了几十年的老站,计数在 40 以上
§5.6 读一份年文件:形状、年份、缺测比例
拉回来之后先跑三项检查,比什么都重要。
#-- 形状、年份跨度、每列缺测比例;只依赖标准库
import gzip
import io
import urllib.request
url = ("https://www.ndbc.noaa.gov/view_text_file.php"
"?filename=42001h2025.txt.gz&dir=data/historical/stdmet/")
req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
raw = gzip.decompress(urllib.request.urlopen(req, timeout=90).read()).decode("ascii", "replace")
lines = raw.splitlines()
head, names = lines[0].split(), lines[1].split()
rows = [l for l in lines[2:] if l.strip()]
print(len(names), "columns;", len(rows), "rows;", rows[0][:10], "to", rows[-1][:10])
first = [r.split()[0] for r in rows]
assert len(set(first)) == 1, "一份年文件里出现多个年份"
print("rows/year =", len(rows))
#-- 期望:17 columns; 45335 rows; 2025 01 01 to 2025 12 31
#-- 期望:rows/year = 45335,约为 365 天 × 1440 分钟 / 10 分钟 = 52560 的 86%
最后一行那个 86% 是这份数据最重要的一个「规模」数字:一份看似完整的年文件,实际只覆盖理论可能时刻的八成半。缺的那一成半,是停报、传感器故障、与后处理剔除。
§5.7 一个不报错的错:把当年月文件当年文件用
当年目录的结构随时间变化,这是最容易写错的一处。
| 月份状态 | 位置 | 文件名 | 格式 |
|---|---|---|---|
| 本月(尚未封闭) | /data/stdmet/<月>/ |
<站号>.txt |
纯文本 |
| 本年度已封闭月 | /data/stdmet/<月>/ |
<站号><月号><年>.txt.gz |
gzip |
| 上一年及更早 | /data/historical/stdmet/ |
<站号>h<年>.txt.gz |
gzip |
2026 年 10 月 29 日的实际情况是:9 月目录里是 769 个 .txt(当月即将封闭),8 月、7 月、5 月目录里各七百多个 .txt.gz(已封闭)。同一个月目录下的文件扩展名会切换,<站号>.txt 在月份滚动后就变成 <站号><月号><年>.txt.gz。
写一个「每天取一遍当月文件」的脚本,如果写死 <id>.txt,跨月那天会静默开始 404;如果写死 .gz,当月取不到。安全的写法是两种都试,且把「本月」与「上月」都纳入。
§5.8 拼一条长序列:三个来源的交界处
拼一条从 1975 年到今天的序列,会在三个地方遇到边界。
| 交界 | 哪两边 | 遇到什么 |
|---|---|---|
| 归档 / 当年 | historical/stdmet 与 data/stdmet |
列数不同(年文件无 PTDY) |
| 上年 / 本年 | 上年 12 月与本年 1 月 | 无重叠,靠日历对齐 |
| 当年 / 实时 | 上月月包与实时 45 天窗口 | 有重叠,须去重且注明来源 |
第二处看着最无害,其实最容易错位:归档按年切分,跨年那一条记录必然落在两个文件里各一次(12 月 31 日 23:50 与 1 月 1 日 00:00),按年拼接时如果不做时间索引去重,会在跨年处出现重复行。
第三处是真正的陷阱:实时窗口覆盖最近 45 天,其中会与「上一个已封闭月份的最后几天」重叠。同一时刻在两处都有,值可能不同(实时未过人工质控)。一条干净的序列必须在重叠区里只取一处,并在元数据里写明取的是哪一处。
§5.9 批量下载的纪律
一万七千余个文件、单文件从几百字节到几 MB,全部拉下来是几十 GB 的量级。真正会出问题的不是带宽,是「拉了一半、不知道缺哪一半」。
第一条纪律是从目录索引生成清单,不要凭年份区间推。归档里有的站只报过两个月,有的站中间断了几年;用「站号 × 年份」笛卡尔积去猜文件是否存在,会得到大量 404,且无法判断 404 是「本来就没有」还是「刚才网络抖了」。
第二条是先记清单,再下载,最后对数。files.txt 的行数与下载目录里的文件数必须相等。少了就是少了。
#-- 限四路并发、断点续传、失败重试两次;先建目录再落盘
base="https://www.ndbc.noaa.gov/data/historical/stdmet"
xargs -P 4 -I{} curl -sS --retry 2 -C - --create-dirs -o "data/{}" "$base/{}" < files.txt
find data -name "*.gz" | wc -l
#-- 期望:等于 files.txt 的行数
并发开到四而不是十六,是因为这条链路对单连接有节流,且失败重试会被放大。断点续传对 .gz 尤其重要:zip 与 gzip 都不能用「大小相等」判断完整性,中途断开的文件看起来大小对了,解压时会报错。
§6 层与层之间:它在本库中的位置
§6.1 库内的海洋与气候家底
这套库以医学数据为主,地球观测是近几年补进来的一块。能与 NDBC 形成上下游关系的,目前有三类:格点海温产品(OISST)、再分析场(ERA5)、陆地站点观测(GHCNd),另有一个在方法上同构的空气质量站网(AQS)。
四者的关系不是「同类替代」,而是「同一套物理量的不同采样几何」:两个是格点,两个是点集;两个在海上,两个在陆上。
§6.2 与 OISST 的分工:点与面
这是最该写清楚的一对关系。
OISST 是每天一张 0.25° 的全球海温图,720 乘 1440 个格点,处处有值——值不是测出来的,是插值出来的。NDBC 是几百个点,每个点只报自己脚下那一小块水面,绝大多数时刻没有值,因为那一格根本没人去过。
| 维度 | NDBC | OISST v2.1 |
|---|---|---|
| 几何 | 点集 | 规则格点 |
| 空间覆盖 | 北美沿岸与北大西洋为主 | 全球海洋 |
| 时间覆盖 | 1970 起,逐站不同 | 1981-09 起,全局统一 |
| 值从哪来 | 传感器测量 | 卫星反演加现场订正再插值 |
| 缺值含义 | 传感器坏了 / 没装 / 站没了 | 云遮蔽或被掩膜 |
| 适合做 | 真值校验、点尺度分析 | 场尺度建模、全球统计 |
实践中的分工是一句话:要面 → OISST;要点的真值 → NDBC;拿 NDBC 校 OISST、拿 OISST 补 NDBC 的空,是常见但不该混为一谈的两件事。
「拿 OISST 补 NDBC 的空」这件事尤其要小心:OISST 的插值本身就吸收了一部分浮标观测,用它来填浮标的缺口,等于用被浮标影响过的场来补浮标自己。填补之前至少要确认那批浮标是否参与了订正——官方文档没有公布逐站的参与清单。
§6.3 与 ERA5 的分工:观测与同化
ERA5 单层再分析给的是同一套气象量:风、气压、气温。它也在海上,也按小时给。
差别在于 ERA5 是一个模式的产物,浮标进了它的同化系统。于是「用 ERA5 验证 NDBC」这件事在逻辑上是倒的:同化的输入不能当同化的裁判。正确的做法是把 NDBC 当代替品——用 ERA5 看大尺度形势(哪几天有锋面过境、哪段时间气压在掉),用 NDBC 看那一格到底多高。
§6.4 与 GHCNd 的分工:海站与陆站
GHCNd 是全球陆地的逐日气温与降水站网,十三万个站。它和 NDBC 在方法上是同构的:都是点集、都是定宽或半结构文本、都有大量退役站与列不全的问题。
区别在采样高度与代表性。GHCNd 的站多在有人烟的地方,NDBC 的站多在人迹罕至的洋面。把两者配对,能得到一条横切海岸线的温度剖面——这正是海岸带城市热岛研究里最缺的一段。
§6.5 与 AQS 的分工:一个方法上的对照
AQS 是美国的环境空气质量地面站网,与 NDBC 没有物理关系,但两者的工程问题几乎一一对应。
| 问题 | AQS 的说法 | NDBC 的说法 |
|---|---|---|
| 站表是不是全部 | 站点清单含已关闭站 | 站表不含退役站,归档目录才有 |
| 缺测怎么标 | 有独立的 null 标记与符号 | 实时 MM、归档 99.00 |
| 两种数据流 | 原始数据与质控数据 | 实时流与归档流 |
| 站点位置 | 逐站固定坐标 | 锚点坐标加观察圈 |
这类问题的迁移性很强:在一个站点型数据集上踩过的坑,换一个领域往往还踩得到。
§6.6 检索路径建议
按需求找:要海表温度的格点场 → OISST;要海上的原位真值 → 本篇;要多要素大气背景 → ERA5;要陆地气温与降水 → GHCNd;要地面空气质量 → AQS。做「海温趋势加风暴事件」的组合研究,本篇提供事件时刻的高频观测,OISST 提供事件前后的场,两者在同一张时间轴上对齐的关键是时区——NDBC 全 UTC,OISST 的日值取 UTC 正午,对齐时要决定按哪个小时切片。
§1.5 的案例在这一层的意义是:当你要把本篇与格点产品拼起来做长期趋势时,已经退役的站会以两种方式同时消失——实时流 404,站表却还在。如果按站表遍历,那个站会一直返回空,而空值在下游的均值计算里被静默跳过,趋势线的端点就悄悄断了。
§7 质量评估与局限:它不该按什么标准评价
§7.0 阅读本节前需要知道的一件事
用「数据集」这个词去评价它,会得出错的结论。
它不是一个有版本号、有 DOI、有发布说明的产品。它是一个运营着的观测网:站会开、会关、会换载荷、会漂。今天抓下来的站表,明天就变了;今天写下的站数,下个月可能就少了十几个。
所以评价它该用的尺子是:作为一个持续运营五十多年的观测体系,它的口径是否可追溯、它的边界是否被写清楚、它的缺口是否被标注。
而不是:它的覆盖率有多高、它的缺失率有多低、它有没有现成的训练测试划分。
§7.1 三种常见的评价框架错位
| 常见错位 | 表现 | 应该怎么想 |
|---|---|---|
| 拿格点产品的标准套它 | 抱怨「到处都是洞」「没法直接算平均」 | 洞是本质,点集天生稀疏;要面就去用格点产品 |
| 拿成品数据集的标准套它 | 抱怨「没有版本号」「没有引用格式」 | 它是运营中的服务,不是出版物;引用靠站号加取数日期 |
| 拿自己脚本的标准套它 | 抱怨「昨天还能跑,今天 404」 | 站被回收了;这恰恰是它真实的样子 |
三种错位里,第一种最常见也最伤。一个团队拿 NDBC 去算「墨西哥湾 2020 年逐日平均水温」,把 42001 与 42002 两个站的观测求日平均,得到一条曲线。这条曲线实际上是两个点的平均,代表的是两片直径几公里的水面,不是「墨西哥湾」。
§7.2 DAIMS 二十四项逐条对照
DAIMS 原本面向社会科学调查数据,这里借用它的检查点,把这份观测站网的成熟度摊开。
| # | 检查项 | 状态 | 说明 |
|---|---|---|---|
| 1 | 宽格式 | ⚠️ | 定宽文本勉强算宽表,但列位随年代变,需要转换器 |
| 2 | 唯一标识 | ⚠️ | 站号加时间戳,但站号在同一度方格内可复用 |
| 3 | 特殊字符 | ✅ | 纯 ASCII,无 BOM,无空格 |
| 4 | 重复行 | ⚠️ | 跨年处与实时/归档重叠区会出现同一时刻两次 |
| 5 | 缺失编码 | ✅ | 两套哨兵值,官方文档明确声明 |
| 6 | 标签标识 | ⚠️ | 列名有历史漂移(WD 到 WDIR 等),文档给了对照表 |
| 7 | 罕见类分组 | ✅ | 无类别概念;站型与产物类型在站表里有字段 |
| 8 | 偏倚评估 | ❌ | 传感器更替与站位移无逐站量化 |
| 9 | 数据字典 | ✅ | 度量说明页逐列解释,含公式与采样方式 |
| 10 | 信息性缺失解释 | ⚠️ | MM 不区分「没装」「坏了」「站没了」三种成因 |
| 11 | 设备记录 | ⚠️ | 站页有人读的载荷说明,不进数据文件 |
| 12 | 共线性 | ⚠️ | WVHT、DPD、APD、MWD 同源同仪器,同作特征等于重复计权 |
| 13 | 编码映射 | ✅ | 站型、owner、pgm 三个字段在站表里 |
| 14 | 时间戳处理 | ✅ | 一律 UTC,无时区歧义 |
| 15 | 划分建议 | ❌ | 无训练测试划分建议;站间不独立,时序强相关 |
| 16 | 泄漏讨论 | ❌ | 官方未讨论站间共址与时序邻近导致的样本相关问题 |
| 17 | 标签分布 | ⚠️ | 无标签;值域由量程限值约束,但未写进文件 |
| 18 | 测量偏倚 | ❌ | 仪器高度、壳水线深度、载荷更替都影响量值 |
| 19 | 外部验证建议 | ⚠️ | 官方无统一交叉验证协议 |
| 20 | 版本记录 | ❌ | 无版本号,无 DOI,站表只有 created 时间戳 |
| 21 | 预处理脚本 | ❌ | 不发布官方读取脚本 |
| 22 | 合规要求 | ✅ | 公有领域,无使用限制 |
| 23 | 多模态对齐 | ❌ | 波浪谱、连续风、海流是平行通道,无统一键对齐说明 |
| 24 | 去标识化 | ✅ | 无个人数据;船位与站址均为公共设施 |
DAIMS 评分:10.5 / 24。按满项 1 分、半分项 0.5 分、零分 0 分计,七个满项、七个半分项、十个零分项。
评分解读:分数偏低,但低在「它不是为机器学习准备的数据集」这件事上,而不是低在观测质量上。满项集中在编码、时间戳、合规、字典这些「标准化」维度;零分集中在版本、划分、泄漏、脚本这些「产品化」维度。NDBC 是一套运营了半个世纪的观测服务,没有人需要给它发版本号。
对你意味着什么:不要指望官方替你解决「哪些站可以合并成一条序列」「哪些站之间不独立」这类问题。这些判断只能自己做,且必须写进方法的 Limitations。
§7.3 逐条局限
| 局限 | 具体表现 | 后果 |
|---|---|---|
| 空间极不均匀 | 710 个固定站集中在北美沿岸与五大湖 | 「区域平均」实质是沿海平均 |
| 站号可复用 | 同一度方格内站号可重新分配 | 长序列包含不同物理实体 |
| 载荷会换 | 41001 换 SCOOP,46012 从 USV 换 | 仪器高度与精度不连续 |
| 缺测不分因 | MM 不区分没装、坏了、站没了 | 无法区分「结构性空」与「故障性空」 |
| 列名漂移 | WD 到 WDIR,BAR 到 PRES | 跨年代解析器要分叉 |
| 列数漂移 | 年归档无 PTDY,当年月文件有 | 回补与滚动取数得到不同列集 |
| 目录结构漂移 | 已封闭月是月包,当月是单站 | 跨月取值会静默断 |
| 两条流不等价 | 实时未过人工质控 | 重叠区同为两值时不知该信谁 |
| 无版本号 | 站表只有 created 时间戳 | 结果不可精确复现,只能记取数时刻 |
| 010 与 111 无交换 | 无 | — |
八行之外还有一条最实在的:没有一个文件记录「这个站哪年到哪年报了哪几类量」。想知道 41001 从哪年开始有波浪观测,只能逐年打开文件数非空列,或者去读站页的散文。这份元信息在这份数据里是缺失的,而它恰恰是做长期序列时最先要回答的问题。
§7.4 §1.5 的案例在这一层的意义
41001 这个例子在质量评估这一节给出的教训,不是「站会关」,而是**「关」这件事本身没有被结构化记录**。
站表没有状态字段,归档目录没有关闭日期,实时文件只有 404。一个站在 2026 年 9 月 9 日被回收,这件事只写在那一天的站页 HTML 里。下个月站页可能被改写成另一个措辞,明年可能连这句话都消失,只剩下「无数据」六个字。
于是「某站的水温序列到哪一年结束」这个问题,没有一个字段能回答。你能做的是:查归档目录里该站的最大年份,查当年月目录里该站最晚的月份,查站页有没有维护提示,三者取最晚的那个,并把它记在自己的元数据里。
这就是为什么 §5.4 那个三级探测函数是必要的——它不优雅,但它是唯一能逼近真值的手法。
§7.5 外部验证:怎么用别的数据检验它
| 参照 | 能验证什么 | 注意 |
|---|---|---|
| OISST 格点 | 浮标与所在格点的偏差,检验格点产品的插值误差 | 取格点时要先确认浮标没参与该产品的订正 |
| 卫星 L2/L3 海温 | 单星反演的偏差 | 需按像元足迹匹配,浮标是点、像元是公里级 |
| 船报观测 | 同一海区的第二种独立观测 | 船报时空稀疏,且高度不一致 |
| 高频雷达海流 | 表层流速交叉验证 | 只覆盖近岸,与浮标的位置不重合 |
| 再分析场 | 大尺度形势的一致性 | 同化了浮标,不能当独立参照 |
这些组合都没有官方协议,所以「偏差多少度」这类说法必须附上时段、区域、匹配半径与是否剔除高缺测站,否则不可复现。
§8 生态与版本:一个机构怎么让五十年的点连起来
§8.1 没有版本号的时间线
它没有 release,只有事件。下面这些时间点是能查到的、会改变数据形态的节点的集合。
| 时间 | 事件 | 证据 |
|---|---|---|
| 1970 | 归档目录里最早的年份,只有 1 个年文件 | 目录文件名 |
| 1971–1973 | 每年 1 至 10 个年文件,早期站零星上线 | 目录文件名 |
| 1976 | 五位 WMO 站号开始使用 | 度量说明页 |
| 1980 年代 | 归档文件表头为两位年份,无分钟列,WD/BAR 旧列名 |
41002h1985 表头 |
| 1990 年代 | 出现分钟列,气压列逐步由 BAR 改称 PRES |
41001h1990 表头 |
| 2026-09-09 | 41001 被回收,仍在站表 | 站页正文 |
| 2026-10-29 | 站表 1,352 站,归档 1,350 站 | 站表与目录 |
这条时间线里最值得注意的一格是 1980 年代。那个时期的归档文件是两位数年份、没有分钟列、采样每小时一次、列名完全是另一套。它不是「同一种数据的早期版本」,而是「另一种格式的数据」——任何跨 1990 年代的序列拼接,都要在这里写一段专门的转换。
§8.2 它在生态中的位置
NDBC 处在一个独特的位置:它既是业务(国家级海洋气象观测网,直接服务于天气预报与航行警告),又是研究原料(几乎所有海洋模型与卫星产品的校验源)。
这种双重身份有一个副作用:它的口径首先是为业务定的,不是为科研定的。十八列里为什么有能见度与潮位?因为海岸警卫队与引航需要。为什么气压对岸站要折算到海平面?因为预报要用。研究者想要的次表层温度、盐度、叶绿素,绝大部分不在这一套接口里。
§8.3 谁在依赖它
- 业务层:国家气象局的海洋预报、飓风中心的强度分析、海岸警卫队的航行警告
- 科研层:卫星海温产品的现场校验、海浪模型的初始场、海气通量估算
- 工程层:海洋牧场与海上风电的环境基准、港口运营的风浪阈值
- 教学层:定宽文本解析的经典练习——格式简单,坑足够多
§8.4 它为什么能持续
一套起步于 1970 年代、输出格式是定宽文本、缺测标记只有两个字母的观测体系,为什么没有被 JSON API 与云原生格式取代?答案有三层。
第一层是接口的稳定性本身就是资产。四十年前写的解析脚本今天还能跑(除了两处列名与列数的漂移)。对业务系统而言,不坏比先进重要。
第二层是它是唯一的上游。区域观测系统(IOOS)的站、州政府的站、大学的站,最终都要汇进这同一张站表与同一种格式。下游可以有很多,上游只有一个。
第三层是维护是按站做的,不是按数据集做的。每一次布放、回收、换载荷都是一次独立的维护工单,站页上写着回收日期、载荷型号、传感器高度、水深、观察圈半径。这种「逐个照看」的模式没法批量化,但也没法被整体替换。
三层叠加的结果是:它不是一份数据,是一个还在运转的设施。设施的价值不在于它此刻有多完整,而在于它每一天都还在产出新的一行。
§8.5 许可与引用
许可这件事,官方的表述比多数人想的更简单,也更含糊。
没有独立的许可页。 站内页脚只有「Disclaimer」「Information Quality」「FOIA」「Privacy Policy」四个链接,没有一个叫「License」或「Terms of Use」。
性质上属于美国联邦政府作品。 NDBC 由 NOAA 国家气象局运营,站页页脚的机构全称是「US Department of Commerce / National Oceanic and Atmospheric Administration / National Weather Service / National Data Buoy Center」。联邦政府的作品在美国版权法下不享有版权。
没有 SPDX 标识。 没有 CC0,没有 CC BY,也没有任何机器可读的许可字段。文件里没有,站表里没有,页脚里没有。
所以能写出的最准确表述是:公有领域性质,无使用限制,官方未给 SPDX 标识,也无单独使用协议。 写成 CC0 或 CC BY 都是替官方做主;写成「保留所有权利」则是反向的错。
引用方面,官方不给引用格式。实践中的做法是写站号、取数时刻与三个入口 URL 之一。站号比站名稳定,取数时刻比「最新数据」可复现。
§9 使用指引:三条路径,一份最小实现
§9.1 三种角色,三条路
| 你的角色 | 建议路径 | 关键动作 |
|---|---|---|
| 想快速验证一个格点海温产品 | 先取站表,再按区域筛出带波浪与水温的浮标 | 只取有 WTMP 的站,逐个拉年文件 |
| 要做海浪或风的短临预测 | 实时目录加当年月目录 | 先确认目标站本月还在报,再谈模型 |
| 要拼四十年的长序列 | 从归档目录反查站,不要从站表正查 | 先把 440 个只在归档里的站找出来 |
三条路的第一动作都不是写模型,是搞清楚「哪个站还在、测什么、从哪年到哪年」。
§9.2 在 AI 流水线中的位置
浮标传感器 → 自动量程检查(NWSTG)→ 实时文件(MM,45 天)
↓
人工机助复核(NDBC)
↓
当年月文件 / 历史年归档(99.00,定版)
↓
格点产品订正(OISST、卫星 L2) / 模型训练目标 / 业务阈值
这条链说明一个常见误用:把浮标当「训练目标」,拿 OISST 当「输入特征」,然后宣称模型能从卫星预测浮标水温。如果 OISST 的插值里已经吸收了这批浮标,模型学到的是「复现浮标对 OISST 的贡献」,不是「从卫星推断现场温度」。
§9.3 一份最小可跑的读取实现
下面这段只依赖标准库,把「读文件—判缺测—拼成时间索引」这条链完全摊开。
#-- 依赖:仅标准库;输入为一个年归档文件的解压文本
import datetime as dt
COLS = ["YY", "MM", "DD", "hh", "mm", "WDIR", "WSPD", "GST", "WVHT", "DPD",
"APD", "MWD", "PRES", "ATMP", "WTMP", "DEWP", "VIS", "TIDE"]
def is_missing_hist(tok):
# 归档文件的缺测是一串 9;空字段也算缺
return tok == "" or set(tok) <= {"9", ".", "+", "-"}
def parse_year(text):
rows = [l for l in text.splitlines() if not l.startswith("#") and l.strip()]
out = []
for line in rows:
cols = line.split()
if len(cols) < 16:
continue
rec = dict(zip(COLS, cols))
ts = dt.datetime(int(rec["YY"]), int(rec["MM"]), int(rec["DD"]),
int(rec["hh"]), int(rec["mm"]))
out.append((ts, {k: None if is_missing_hist(v) else float(v)
for k, v in rec.items() if k not in COLS[:5]}))
out.sort()
return out
rows = parse_year(open("42001h2025.txt", encoding="ascii", errors="replace").read())
print(len(rows), rows[0][0], rows[-1][0])
print(sum(r[1]["WTMP"] is None for r in rows), "rows without sea temp")
#-- 期望:首尾跨度恰好一年,缺水温的行数在多数站上小于一成
两个要点:is_missing_hist 用集合判断,99.00、999、999.0 全部命中而 1012.7 不命中;out.sort() 是必要的,个别年文件里月份不是严格升序,跨月拼接时依赖排序。
§9.4 评估时最少要报的三件事
| 要报的 | 为什么 | 怎么报 |
|---|---|---|
| 站的选择规则 | 站与站不独立,邻近站高度相关 | 写明按区域、按站型、还是按数据完整度筛 |
| 时间划分方式 | 逐小时强烈自相关,随机划分高估精度 | 写明按年、按月块还是按事件切 |
| 缺测的判定口径 | MM 与 99.00 两套,混用会静默污染 | 写明每列用哪条规则、剔除了多少行 |
§9.5 三种流水线的形态,与它们各自的错法
同一份数据,三种任务,三种切法。切错了,模型照样收敛,指标照样好看。
| 任务 | 数据形态 | 常见错法 | 该怎么做 |
|---|---|---|---|
| 单站时序预测 | 取一个站,几十年,一小时一条 | 随机抽时刻做训练验证 | 按年切,留出完整年份 |
| 多站联合建模 | 取一片海的十几个站,拼成长表 | 把邻近站当独立样本 | 按地理块分组,块内不跨训练测试 |
| 场重建 | 用浮标点重建格点场 | 用 OISST 当标签又当输入 | 标签与输入必须来自不同来源 |
第三种错法最隐蔽。用浮标训练一个「从卫星反演到现场温度」的模型,验证时却拿 OISST 当答案,而 OISST 里吸收了这批浮标——三边互相污染,指标会很漂亮,上线后不成立。
§1.5 的案例在这一层给出的提醒是:即使切分正确,样本量也会在你没注意的时候缩水。41001 在 2026 年 9 月后没有新数据,用它做的「近期验证集」会不声不响地变成空集。一个只按文件名拉取的流程不会报错,它只会给出一条停了三个月的曲线。
§10 AI 使用声明
| 栏目 | 内容 |
|---|---|
| 性质 | 海上原位观测站网,点集,非格点,非插值 |
| 用途 | 格点产品校验、海洋事件复盘、区域环境基准、时序建模 |
| 不建议用途 | 全球空间统计、等间隔规整表训练、把邻近站当独立样本 |
| 伦理 | 无个人数据;下游若用于海上作业决策,须披露观测中断与站位移 |
| 引用 | 站号 + 取数时刻 + 入口 URL,缺一不可复现 |
| 许可 | 公有领域性质,无使用限制;无 SPDX 标识,无独立协议页 |
| 版本 | 无版本号;站表有 created 时间戳;归档无版本概念 |
| 状态 | 在线运营,整点更新;实时流 45 天,归档流按年/月封闭 |
§11 常见误读与避坑清单
| 常见说法 | 实际情况 | 证据 |
|---|---|---|
| 「NDBC 有上千个浮标」 | 站表里浮标只有 438 个,固定站占一半以上 | 站表 count 与 type 属性 |
| 「缺测写 NaN」 | 实时写 MM,归档写 99.00/999,两套都不叫 NaN |
度量说明页第二段 |
| 「归档文件是空格对齐的,直接 split 就行」 | 早期格式是定宽文本,split() 会错位;列位随年代变 |
1985 与 2026 年文件表头对照 |
| 「站表就是全部站」 | 站表只有活动站,归档里另有 440 个已退役站 | 站表与归档目录比对 |
| 「站表里的站都在报数据」 | 41001 已被回收,仍在站表里但实时接口 404 | 站页「Buoy recovered 9/9/26」 |
| 「同一个站号就是同一台设备」 | 站号在同一度方格内可被重新分配 | 度量说明页 Station ID 一节 |
| 「站表坐标就是浮标实际位置」 | 名义坐标,实际在观察圈半径内摆动,41001 约 4.2 公里 | 站页 watch circle radius |
| 「十八列每列都有值」 | 各站常年全空的列不同,能见度与潮位绝大多数站全空 | 实时文件逐列缺测率 |
| 「实时流和归档流是一回事」 | 实时只过自动检查,归档过人工复核;缺测写法也不同 | 度量说明页与质控页 |
| 「年归档文件包含 PTDY」 | 年归档没有该列,当年月文件有 | 两处表头对照 |
| 「上个月的文件就在 historical 目录里」 | 当年数据按月放,已封闭月是 .txt.gz 包,当月才是 <id>.txt |
/data/stdmet/<月>/ 目录列表 |
| 「波浪谱、连续风、海流在同一份文件里」 | 各自独立的字母后缀(w、c、o、a),互不覆盖 | 归档目录文件名后缀 |
| 「NDBC 的站和 GHCNd 的站可以随便合并」 | 站号体系不同,NDBC 是五位 WMO 号,GHCNd 是本国编号 | 两个站表对照 |
| 「数据从 1970 年连续到今年」 | 1970 年只有 1 个年文件;站点数从个位数涨到一千多 | 归档目录按年计数 |
附录 A 速查卡
| 键 | 值 |
|---|---|
| 站表 | https://www.ndbc.noaa.gov/activestations.xml |
| 实时文件 | https://www.ndbc.noaa.gov/data/realtime2/<站号>.txt |
| 当年当月 | https://www.ndbc.noaa.gov/data/stdmet/<月英文>/<站号>.txt |
| 历史年归档 | https://www.ndbc.noaa.gov/data/historical/stdmet/<站号>h<年>.txt.gz |
| 缺测(实时) | MM |
| 缺测(归档) | 99.00 / 999 / 99.0 / 999.0 |
| 列数 | 当年格式 18 列;年归档 17 列(无 PTDY) |
| 时间 | 一律 UTC;早年文件无分钟列 |
| 站数(2026-10-29) | 见 §2.1;活动与归档交集 908 |
| 许可 | 公有领域性质,无 SPDX,无独立协议页 |
附录 B 站型与产物对照
| 站型 | 站表数量 | met="y" |
有无 stdmet 文件 | 主要产物 |
|---|---|---|---|---|
| fixed | 710 | 554 | 多数有 | 风、气压、气温 |
| buoy | 438 | 272 | 多数有 | 全套气象加波浪 |
| dart | 76 | 0 | 少 | 水位,海啸事件 |
| oilrig | 54 | 23 | 部分有 | 风、气压、气温 |
| tao | 48 | 0 | 部分有 | 赤道锚系气象 |
| other | 24 | 13 | 部分有 | 视合作方 |
| usv | 2 | 1 | 有 | 视载荷 |
附录 C 术语表
| 术语 | 含义 |
|---|---|
| NDBC | 国家数据浮标中心,NOAA 国家气象局下属 |
| stdmet | 标准气象数据,本文的主接口 |
| C-MAN | 海岸海洋自动网,岸基固定站 |
| LLNR | 本地站号,出现在站页标题,不进文件名 |
| 观察圈半径 | 系泊浮标在锚点周围允许摆动的距离 |
| 有效波高 | 20 分钟波浪序列中最高三分之一的平均,不是最大波 |
| 主波周期 | 能量最大处的周期,与波高同源 |
| 哨兵值 | 用一个特定数值表示缺测,NDBC 归档用一串 9 |
| 定宽文本 | 按字符位置而非分隔符对齐的文本格式 |
| 当年月文件 | 本年度已封闭月份的数据,按月打包 |
| 年归档 | 上一年及更早的整年数据,一站一年一文件 |
| WMO 站号 | 世界气象组织五位编号,1976 年起使用,可在同格内复用 |
| NWSTG | 国家气象电信枢纽,实时流的自动质控在此进行 |
| TAO/TRITON | 赤道太平洋与印度洋的锚系观测阵列 |
| DART | 深海海啸评估与报告系统 |
| USV | 无人水面艇 |
附录 D 常见问题
Q1:要注册才能下吗?
不用。站表、实时目录、归档目录都允许匿名访问,没有令牌,没有点击同意。也没有速率限制声明,但实际并发太高会被节流。
Q2:为什么同一个站昨天能下今天 404?
两种可能。一是站被回收了(41001 在 2026-09-09 被回收),二是月份滚动导致扩展名从 .txt 变成 .txt.gz。前者要查站页,后者要查目录。
Q3:为什么归档里的浪高是 99?
不是 99 米,是缺测。归档文件用一串 9 表示缺测,99.00 是该列的固定写法。按 float 读会得到 99.0,不会报错。
Q4:能算某个海区的平均水温吗?
能,但要点名是哪几个站、每个站代表多大范围。在没有站的区域,平均是不可得的。算出来的数字最好写成「N 个站的平均」而不是「某海区的平均」。
Q5:站表里的站数为什么每次抓都不一样?
站表每几分钟重生成一次,created 属性带时间戳。站会开、会关,数字会变。引用时带上时间戳。
Q6:波浪、风、海流为什么不在一个文件里?
官网把不同物理量分成不同产物,各有独立的字母后缀(w 波浪谱、c 连续风、o 海流、a 流剖面)。同一站同一年可能有多份文件,按产物分开。
Q7:41001 现在还有数据吗?
实时没有(404),2026 年 5 至 7 月的归档还能从站页链接取到,2026 年 9 月被回收。它的最后一个月数据在 /data/stdmet/ 里随月份滚动逐步转为包文件。
Q8:这份数据能不能当卫星海温的标签?
能,而且是常用的做法。要注意三点:浮标与卫星像元的空间匹配、浮标观测深度与卫星表层深度的差异、以及浮标本身是否参与了卫星产品的订正。
Q9:这算不算「AI-ready」?
按本文的口径算一半。它格式简单、文档齐全、匿名可取、无版权限制,这四条是 ready 的;它缺版本号、缺划分建议、缺逐站元数据表,这三条要使用者自己补。
编辑说明
核验时间 2026-10-29。方法:直接下载站表 activestations.xml(Content-Length 272,488 字节),解析 count 属性与全部 station 元素,按 type、met、owner、pgm 四个属性分桶计数,并计算经纬度极值;抓取归档目录 /data/historical/stdmet/ 索引(17,049 个 .txt.gz),按正则剥离年份得到 1,350 个站号,与站表做双向差集;抓取当年目录 /data/stdmet/ 下九个月份目录,确认已封闭月份为 .txt.gz、当月为 <站号>.txt;下载 42001 的实时文件与 2025 年年归档、2026 年 9 月月文件,逐列统计缺测率与行宽;下载 41002(1985)、46001(1975)、41001(1990)三个早年归档文件,取表头确认两位年份、WD/BAR 旧列名、无分钟列;读取站页 station_page.php 的正文,取 41001、42001、46012 的 watch circle radius、传感器高度、载荷型号与维护提示;读取度量说明页、归档问答页、质控页、小时文件页与五天文件页,取缺测写法、列定义、列名废弃对照表与两段质控流程的原文。未做任何分布统计,因此全文没有水温均值、浪高极值频率或区域偏差数字。当年九个月目录的文件计数(740–769)只用于说明「当月与已封闭月」的目录切换,未据此推算全年总量。实时文件的行数(6,457–6,478)为抓取当日窗口的实际长度,约 45 天、10 分钟一格,未作为官方口径。文中「约合公里」的观察圈半径换算按 1 码等于 0.9144 米计算,非官方表述。
相关数据集导航
以下为站内 AI-Ready 数据集百科中与本词条共享多个主题标签的相关数据集,按相关度降序排列:
- noaa-cors-gnss — 共享标签:公共卫生与流行病学 / 时序数据 / 疾病监测
- era5-single-levels — 共享标签:公共卫生与流行病学 / 时序数据 / 疾病监测
- oisst-v2-1 — 共享标签:公共卫生与流行病学 / 时序数据 / 疾病监测
- ibtracs — 共享标签:公共卫生与流行病学 / 时序数据 / 疾病监测
- argo-gdac-floats — 共享标签:公共卫生与流行病学 / 时序数据 / 疾病监测
- jhu-csse-covid-19 — 共享标签:公共卫生与流行病学 / 时序数据 / 疾病监测
- aqs — 共享标签:公共卫生与流行病学 / 疾病监测
- our-world-in-data — 共享标签:公共卫生与流行病学 / 时序数据
- mics — 共享标签:公共卫生与流行病学 / 疾病监测
- bts-on-time-performance — 共享标签:公共卫生与流行病学 / 时序数据
导航说明:本章节由全站统一标签体系自动计算生成(标签重合度算法),双向可达;点击链接可跳转至对应数据集词条。

