Python 分析香港 AQHI 归档 1644 天:同一天的空气,早上 8 点报 3,下午 5 点报 10

文章目录
1. 先说结论:同一天,同一批监测站,3 和 10
2026 年 8 月 8 日,香港 18 个空气质素监测站。当天全港最高的那个 AQHI 读数是这样走的:
| 时刻 | 全港最高 AQHI | 18 站均值 | 分级 |
|---|---|---|---|
| 08:15 | 3 | 2.67 | 低 |
| 13:15 | 6 | 5.50 | 中 |
| 16:15 | 10 | 8.39 | 甚高 |
| 17:15 | 10 | 8.67 | 甚高 |
| 23:15 | 6 | 4.44 | 中 |
同一天、同一批监测站:早上 8 点全港 18 个站没有一处超过「低」,下午 5 点大部分站已踩进「高」到「甚高」。
这不是个例——归档里 8 个「当日峰值 ≥7」的采样日,峰值全部落在 15–19 时,谷值集中在清晨 5–8 时;极端的一天(2026-07-25)全港均值从凌晨 2.00 走到下午 8.22,一天内摆动 4.11 倍。
这篇要讲的不是「香港空气好不好」,而是读这个指数有几种读法、每种给出什么不同结论:日内、快照、排行、跨期、编码。
2. 先把口径钉死:AQHI 不是平均值,也不是最大值
动手前先确认它在算什么。环保署 FAQ 的定义有三个要点:
- 四项风险相加,不是取平均也不是取最大:
%AR = %AR(NO₂) + %AR(SO₂) + %AR(O₃) + %AR(PM),其中%AR = [exp(β × C) − 1] × 100%,C 是三小时移动平均浓度;%AR(PM)取 PM₁₀ 与 PM₂.₅ 中风险更高的一项。 - 分级 11 档:1–3 低 / 4–6 中 / 7 高 / 8–10 甚高 / 10+ 严重。7 是易受影响人群的预防分界,8 是一般市民的。
- 监测站两类:15 个一般站 + 3 个路边站(铜锣湾、中环、旺角)。官方写明「路边监测站录得的指数一般会较一般监测站为高,主要是受地面汽车废气影响」,并提示除非整日在繁忙街道逗留数小时,否则应参考一般站。
C 用三小时移动平均,所以 AQHI 天然比瞬时读数平滑。
数据源(两个端点,都零鉴权):实时快照 https://dashboard.data.gov.hk/api/aqhi-individual?format=json,返回 18 个站的 {station, aqhi, health_risk, publish_date};逐小时归档 https://api.data.gov.hk/v1/historical-archive/ 把每个版本都存了一份。
实测归档规模:39,453 个版本、65.3 MB、1,644 天可查(2022-01-01 至 2026-09-11),累计 18 个站、29,514 个有效读数。
3. 采集代码:三个必须先摸清的接口行为
import json
import subprocess
import urllib.parse
LIVE = "https://dashboard.data.gov.hk/api/aqhi-individual?format=json"
ARCHIVE = "https://api.data.gov.hk/v1/historical-archive"
def curl(url, timeout=60):
"""走 curl 而非 urllib:这条链路对请求特征的限流更宽容。"""
return subprocess.run(["curl", "-sL", "-m", str(timeout), url],
capture_output=True, text=True).stdout
def list_versions(start, end):
"""列归档版本清单。
坑 1:单次最多只返回 10000 个时间戳(version-count 才是真实总数),
跨年必须按年分页,否则被静默截断成开头那一段。
"""
q = urllib.parse.quote(LIVE, safe="")
stamps = []
for year in range(int(start[:4]), int(end[:4]) + 1):
a = max(f"{year}0101", start)
b = min(f"{year}1231", end)
m = json.loads(curl(f"{ARCHIVE}/list-file-versions"
f"?url={q}&start={a}&end={b}", 90))
stamps += m["timestamps"]
return sorted(set(stamps))
def get_version(stamp):
"""取一个归档版本。
坑 2:end 参数不能晚于昨天,传今天直接 400。
坑 3:不存在的 time 不报错——HTTP 200 + 31 字节 {"message":"NOT FOUND"},
只判断状态码会把垃圾写进缓存,用长度哨兵拦掉。
注意时间戳的分钟并不规整,必须取自清单,不能自己拼。
"""
q = urllib.parse.quote(LIVE, safe="")
raw = curl(f"{ARCHIVE}/get-file?url={q}&time={stamp}")
if len(raw) < 200 and "NOT FOUND" in raw:
return None
try:
return json.loads(raw)
except ValueError:
return None
三个坑里第一个最隐蔽:它不报错、不告警,只是安静地把 4.7 年的清单截成前面一段——我第一次就是拿这个截断清单去建索引的,直到发现天数对不上才回头查。
这段逻辑可以直接接 data.gov.hk 的任何一个归档端点,只需换掉 LIVE 这一个常量,建议收藏备用。
4. 读法一:把一天摊开看

上图是四个采样日的逐小时曲线:实线是 18 站均值,色带是当天最低站到最高站的区间。四条线的形状差异比数值差异更值得看:
- 2022-09-16(红):全期最极端的一天,24 小时都有站点在 ≥7,均值从早到晚没低于 4。
- 2026-08-08(深蓝):典型「陡升陡降」——早上 8 点前全港最高只有 3,14 时后一路爬升,17 时见顶 10,21 时掉回 6。
- 2026-01-13(绿):冬季日的特征不是峰值更高,而是地板更高——全天均值没低过 4.06,清晨 7 点的谷值仍是 4 分(中)。
- 2026-06-15(青):清洁日形状几乎平的,全天 2.0–2.9。
两条直接读出来的结论:冬季的「高」和夏季的「高」不是一回事——前者靠地板抬升,后者靠午后陡冲。峰值高度集中在 15–19 时,8 个高值日的峰值时刻分别是 15、16、17、17、17、18、18、19 时。至于是哪一项污染物主导,只有 AQHI 综合值无法反推。
5. 读法二:16:00 快照会骗人,而且是双向的
要拿到 4.7 年长期序列,只能「每天取最接近 16:00 的版本」抽样。该做法必须检验,所以我另对 21 天做了全天 24 小时全量采集:
21 天里 8 天(38%)的 16:00 快照漏掉当日峰值(都差 1 级);而它的日均值比真实全日均值平均高 1.215 级(中位 +0.533,最大 +4.400)。漏峰最典型的是 2026-01-13——16:00 读到 9,真实峰值 10 出现在 18 时;高估均值最夸张的是 2026-07-25——那一刻 8.22,全天真实均值只有 3.82,差了 4.4 级。
用「一天一个数」判断某天空气质量,既可能低估极端值、又可能高估均值。判断「今天要不要减少户外活动」看当日峰值,判断「这个月趋势」看全天均值——单点快照两个都不满足。
6. 读法三:同一批监测站,两张不重合的榜

左图按「归档期日均 AQHI」排序,右图按「达到 ≥7 的天数」排序。柱子顺序相同,右边越不整齐说明两张榜分得越开(下表为位移最大的站)。
| 监测站 | 类型 | 日均(排名) | ≥7 天数(排名) | 位移 |
|---|---|---|---|---|
| 屯门 | 一般 | 4.06(1) | 115(1) | 0 |
| 铜锣湾 | 路边 | 4.03(2) | 42(14) | −12 |
| 东区 | 一般 | 3.95(4) | 40(15) | −11 |
| 中环 | 路边 | 3.83(7) | 34(17) | −10 |
| 元朗 | 一般 | 3.82(9) | 95(3) | +6 |
| 东涌 | 一般 | 3.65(17) | 97(2) | +15 |
| 荃湾 | 一般 | 3.61(18) | 50(7) | +11 |
读法:三个路边站全部挤进日均榜前 7,但在爆表榜上集体掉到第 11 名之后;反过来,日均榜垫底的荃湾(18)与东涌(17),在爆表榜上是第 7 和第 2,日均第 9 的元朗排到爆表第 3。
两张榜都不算错,回答的是两个问题:「哪里的人长期吸得多」和「哪里最常出现要减少户外活动的日子」。3 个路边站日均合计 3.943、15 个一般站 3.810,前者更高(与官方说明一致),但一般站撑起了绝大多数高值日。把「同一批对象 × 两种口径」并排画出来,这个做法可以套到任何指标评审场景,值得收藏。
7. 读法四:季节能看,跨期要小心

按月把 4.7 年摊平,季节规律稳定:6–7 月年年是全期低点(月均 2.4–4.0),月度高点都在年末年初——全期最高的 6 个月里 5 个落在 10 月至次年 1 月。年度顶点在深秋入冬,不在盛夏。
显眼的异常是 2022 年 9 月:月均 6.18,全期最高。9 月 12 日至 16 日连续五天全港日均都在 8.4 以上,16 日全天均值 9.83。环保署当日新闻公报写明「下午二时部分监测站的空气质素健康指数录得 10+,健康风险级别达到严重」,成因是入秋后夏季偏南气流缓和、东北季候风仍弱,形成静稳大气,充沛阳光助长光化烟雾——官方说明与数据完全对得上。
图上还有个灰格:2023 年 8 月整月无数据——归档有缺口,全期缺 71 天,其中 2023-07-20 至 09-13 连续 56 天没有任何版本。
一条边界:环保署 2025-03-22 更新过 AQHI 计算,只改了臭氧风险系数 β(O₃),从 0.0005116328 降为 0.0004888034,其余四项不变——所以跨这条线的 AQHI 不可直接比大小。实测变更前 1,117 天日均 3.901、变更后 527 天 3.688,差 −0.213,但分月看正负都有,拆不开系数与真实变化的贡献。本文的季节结论在前后两段都成立,所以可用;跨这条线谈涨跌,都该先说明这个前提。
8. 读法五:编码层的三个坑
这一节与空气质量无关,只看「编码层丢了多少信息」。归档里 aqhi 字段全是 int、取值 1–10,从不出现 "10+",但官网分级里有「10+(严重)」。交叉统计 aqhi 与 health_risk:
aqhi | 出现的标签 | 记录数 |
|---|---|---|
| 7 | High | 547 |
| 8 与 9 | Very high / Very High | 174 / 238 |
| 10 | Serious | 15 |
| 10 | Very high / Very High | 9 / 24 |
① 10+ 被压成了 10,数值字段表达不了 10+,高位信息在编码时丢失;② 但文字标签把它救回来了——Serious 只出现在 aqhi == 10 上,8 和 9 一次都没有,也就是 aqhi: 10 + "Serious" 才是真 10+,aqhi: 10 + "Very High" 是真 10,想还原最高一档不能看数字、要看标签;③ 同一枚举两种大小写——Very high 183 条、Very High 262 条一路并存,直接 groupby("health_risk") 会把这个等级劈成两行,而且总量对得上、不会报错。
第 1 点还有外部证据:2022-09-16 18:30 那版归档里,中西区与元朗的 aqhi 都是 10、标签都是 Serious,而当日新闻公报写的是这两个站 10+——接口确实把 10+ 记成了 10,只是标签留下了痕迹。
9. 踩坑清单
| 坑 | 现象 | 处理 |
|---|---|---|
| 清单被静默截断 | 跨 4.7 年只返回前 418 天 | 按年分页取 list-file-versions |
无效 time 返回 200 | 31 字节 NOT FOUND 被当数据写入 | 加长度哨兵;end 上界取昨天 |
| 单点快照双向有偏 | 既漏峰(38%)又高估均值(+1.215) | 判峰值用全天数据,判趋势用全天均值 |
| 编码层丢精度 | 10+ 压成 10;Very high/Very High 并存 | 用 Serious 标签还原 10+;入库前统一大小写 |
| 归档有洞 + 跨期不可比 | 缺 71 天(最长连续 56 天);β(O₃) 变更 | 建索引前做覆盖检查;跨 2025-03-22 只做形状对比 |
原创声明:本文为原创技术实践,数据来自 data.gov.hk 与环保署公开端点,采集于 2026-09-12,归档区间 2022-01-01 至 2026-09-11。AQHI 为整数分级、分辨率有限,日内 1 级以内的差异不宜过度解读;结论属数据口径层面的观察,不构成健康建议。接口行为与归档覆盖范围可能随官方调整。
11. 参考链接
- https://data.gov.hk/en-data/dataset/hk-dpo-datagovhk2-city-dashboard-aqhi
- https://www.aqhi.gov.hk/tc/what-is-aqhi/faqs.html
- https://www.aqhi.gov.hk/tc/monitoring-network/air-quality-monitoring-stations.html
香港开放数据的逐日实测会持续更新,关注不迷路。

更多推荐




所有评论(0)