目录一、七份文件最大的比最小的大 25 倍二、文件名里的年份不是数据里的年份三、字段会改名Court → Court/Estate还多了个 Wing四、楼层是字符串单位号里还有字母五、把七份合在一起算均价会算出一个不存在的数六、可以直接抄的做法参考链接中国香港房委会把资助出售房屋的成交记录按「每期计划」发成一份文件零鉴权JSON 格式中英文各一版。我把能抓到的七份全下载下来共约 9.7MB、18,822 条成交只想回答一个问题这批数据的年份怎么对。结果是没有一份文件的「文件名年份」等于它里面的成交年份——七份里有5 份完全不重合代号 g24 的那份文件名写着 2024里面的成交全部发生在 2025 和 2026字段在改名2018 年那份用Court之后六份全改成Court/Estate还多出一个Wing字段。11 个字段里只有 8 个是七份共有的同一条记录里楼层是字符串、面积是浮点、价格是整数——楼层排序会得到1, 10, 11, 12, 13这种顺序而单位号里还混着04A。一、七份文件最大的比最小的大 25 倍先看清这批文件的规模代号计划成交条数018居屋 20184,431g20绿置居 20203,294g22绿置居 20224,586g23绿置居 20233,002g24绿置居 20243,069e222022另一批256e232023另一批184最后两份只有 184 和 256 条跟前面的几千条不在一个量级最大与最小相差 24.9 倍。这两份对应的是规模小得多的批次把它们和主期计划当成同一个均匀的时间序列做年度趋势等于让 184 条和 4,586 条拥有同样的话语权——趋势线的形状主要由样本量决定而不是由市场决定。所以按「期」分析时要么显式加权要么把小批次单独标注出来不要和主期混在一条线上。二、文件名里的年份不是数据里的年份这是今天最值得记住的一条。每份文件里都有一个签约日期字段我按它统计了实际年份# 文件名年份 vs 实际签约年份importjson,pathlib,collections RAWpathlib.Path(原始返回/ssfs)SERIES{018:2018,g20:2020,g22:2022,g23:2023,g24:2024,e22:2022,e23:2023}DATEDate of Agreement for Sale and Purchase (ASP)forcode,file_yearinSERIES.items():recsjson.loads((RAW/f{code}_en_r0.json).read_bytes().decode(utf-8-sig))[records]yearscollections.Counter(r[DATE].split(/)[-1]forrinrecs)hitstr(file_year)inyearsprint(code,file_year,dict(years.most_common()),重合:,hit)# 018 2018 {2019: 4431} 重合: False# g24 2024 {2025: 2783, 2026: 286} 重合: False# e22 2022 {2022: 151, 2023: 105} 重合: True七份里五份完全不重合2018 年那份的成交全部发生在20194,431 条2024 年那份全部发生在20252,783和 20262862023 年那份全部发生在 2024。只有 e22、e23 两份有部分重合。原因不复杂文件名里的年份是「第几期计划」成交签约发生在计划之后——先抽签选楼、再签买卖协议跨年是常态。但如果你按文件名给数据打年份标签这是最顺手的做法整条时间轴会平移一到两年跟任何其他年度数据楼价指数、人口、利率对不上而且不会有任何报错数字照样算得出来图表照样画得出来只是整体错了一格。正确做法是只用日期字段派生年份文件名只当版本号。三、字段会改名Court → Court/Estate还多了个 Wing把七份文件的字段取并集和交集会看到漂移keysets{}forcodeinSERIES:recsjson.loads((RAW/f{code}_en_r0.json).read_bytes().decode(utf-8-sig))[records]keysets[code]set().union(*[set(r)forrinrecs])unionset().union(*keysets.values())interset.intersection(*keysets.values())print(len(union),len(inter),sorted(union-inter))forkinsorted(union-inter):print(k,有于,[cforcinSERIESifkinkeysets[c]],缺于,[cforcinSERIESifknotinkeysets[c]])# 11 8 [Court, Court/Estate, Wing]# Court 有于 [018] 缺于 [g20, g22, g23, g24, e22, e23]# Court/Estate 有于 [g20, ...] 缺于 [018]# Wing 有于 [g20, ...] 缺于 [018]2018 年那份里屋苑字段叫Court从 2020 年起改叫Court/Estate同时新增了Wing翼字段。所以按Court/Estate取数2018 年那份会取到 4,431 个空值——不报错只是那一整年的屋苑全是空按Court取数另外六份全空。这类漂移没有任何提示。处理办法是建一张别名表Court→Court/Estate并在读取时断言关键字段的空值率——空值率突然 100% 就是改名了。四、楼层是字符串单位号里还有字母同一条记录里字段类型并不统一recsjson.loads((RAW/g24_en_r0.json).read_bytes().decode(utf-8-sig))[records]types{k:type(v).__name__fork,vinrecs[0].items()}print(types)# Scheme Type: str Court/Estate: str Block: str Wing: str# Floor: str Unit: str# Saleable Area of Flats (sq. m.): float Transaction Price: intfloorssorted({r[Floor]forrinrecs})print(floors[:5])# [1, 10, 11, 12, 13] ← 字典序不是楼层顺序units[r[Unit]forrinrecs]print(sum(1foruinunitsifu.isdigit()),/,len(units))# 3065 / 3069 ← 有 4 条不是纯数字比如 04A两个后果楼层是字符串直接排序得到1, 10, 11, 12, 13…——想找「最高几层」或者按楼层分组必须转成整数再排单位号里混着04A这种值3,069 条里 4 条不是纯数字。所以「单位号是数字」这个假设不成立int()会在这 4 条上抛异常——正好少到不容易被发现。价格和面积反而是干净的面积是浮点35.4价格是整数715500。所以不要因为「看起来都是数字」就统一处理。五、把七份合在一起算均价会算出一个不存在的数最后看一眼口径018居屋 2018均价267.6 万绿置居各期在119.2 万到 224.2 万之间七份合并后是210.7 万。这个 210.7 万没有意义——它是两种不同性质的计划居屋和绿置居的定价机制、单位面积都不同加权出来的。均面积也一样从 20.3 平方米到 38.6 平方米。合并统计之前先确认「合并」这个动作在业务上是否成立。好消息是中文版和英文版完全对齐抽查两份条数 4,431/4,431、3,069/3,069逐条价格全部一致。所以跨语言对齐这一关这份数据是过了的。六、可以直接抄的做法importjsonimportpathlib ALIASES{Court:Court/Estate}# 旧名 → 新名INT_FIELDS(Floor,)# 需要转整数的数字型字符串DATE_FIELDDate of Agreement for Sale and Purchase (ASP)defload_series(raw:pathlib.Path,code:str)-list:读一期刊的成交别名归一 年份派生 类型修正。recsjson.loads((raw/f{code}_en_r0.json).read_bytes().decode(utf-8-sig))[records]out[]forrinrecs:rec{ALIASES.get(k,k):vfork,vinr.items()}d,m,yrec[DATE_FIELD].split(/)# 明确按 D/M/YYYY 解析rec[date]f{y}-{m}-{d}rec[year]int(y)# 年份只从日期派生forfinINT_FIELDS:sstr(rec.get(f,)).strip()rec[f{f}_int]int(s)ifs.isdigit()elseNoneout.append(rec)# 关键字段全空 疑似改名别让它静默通过missingsum(1forxinoutifnotx.get(Court/Estate))assertmissinglen(out),f{code}: 屋苑字段全空检查是否改名returnout四个要点收尾年份只从日期字段派生文件名当版本号别让「文件名看起来像年份」骗过去字段改名要靠空值率发现建一张别名表兜底并对关键字段断言「不能全空」类型不要靠猜数字型字符串显式转换遇到非数字值留None而不是硬转合并之前先问一句「合并在业务上成立吗」不同定价机制的计划不能加权平均样本量差 25 倍的批次也不能等权处理。今天 11 次请求七份英文 两份中文 两份第二轮快照全部真实抓取原始文件已落盘脚本可以整篇复跑。参考链接https://data.housingauthority.gov.hk/dataset/ssfs/ssfs-sale-transactions-g24_en.jsonhttps://data.gov.hk/en-data/api/3/action/package_show?idhk-housing-gsh2024-ssfs-sale-transactions-g24https://data.housingauthority.gov.hk/原创声明本文所有数据于 2026 年 9 月 27 日实测抓取自中国香港房屋委员会公开数据集与 data.gov.hk抓取与校验脚本随文附上欢迎复现。文中「文件名年份 ≠ 成交年份」是这一系列文件的实际形态描述不构成对发布方的评价做年度对比时请以记录内的签约日期为准。如果这篇帮你避开了一次「整条时间轴平移一年」的返工点个收藏下一篇继续拆系列型数据的坑。