由于借助 AI 工具学习编程已经变得非常容易了,因此之后的课程就不再默认进行视频讲解了,如果特别需要视频讲解也可以联系李老师预约讲解~讲义材料学习过程中遇到的问题也可以及时与李老师联系。
购买 RStata 会员(任意会员均可)即可参加该课程啦(之前的和未来的都可以参加)!
购买会员可以从这里下单:https://rstata.duanshu.com/#/card/list/
* 如果发票可添加小编微信 r_stata2 (RStata 李老师)开具。如需数据资料,购买后可添加小编微信免费领取数据折扣卡。
更多关于 RStata 会员的更多信息可添加微信号 r_stata2 咨询:

课程主页(点击文末的阅读原文即可跳转):https://rstata.duanshu.com/#/brief/course/aae08b44662b44b284369804399ae4e9
一、为什么要统一海关税号编码
海关数据中的商品编码(税号编码),也就是我们常说的 HS 编码(Harmonized System,商品名称及编码协调制度),是国际贸易中对商品进行分类的通用语言。它有两个特点需要特别注意:
- 前 6 位是国际通用码,全球统一;后 2 位是国内子目,由各国海关自行扩展。我国海关数据中的商品编码通常为 8 位。
- HS 编码每隔约 5 年修订一次。世界海关组织(WCO)会新增、删除、拆分或合并部分税目,因此不同年份的数据使用的是不同版本的 HS 编码。
在我们的海关数据(2000–2016 年)中,年份与 HS 版本的对应关系如下:
hs_version = c(rep("HS1996", 2), rep("HS2002", 5), rep("HS2007", 5), rep("HS2012", 5)) summarise(年份区间 = paste0(min(year), "–", max(year)), .groups= "drop") %>% rename(HS版本 = hs_version) %>% knitr::kable(align = "c")如果直接把跨年份的数据放在一起做面板分析,同一个商品在不同年份可能对应不同的编码,会导致匹配错误。因此在做任何跨期分析之前,必须先把所有年份的编码统一到同一个版本。
本讲义演示如何用 Python(借助 R 的 reticulate 包创建独立虚拟环境来运行)把海关数据统一到 HS1996 / HS2002 / HS2007 / HS2012 / HS2017 五个版本,并保留原始编码。为节省篇幅,正文以 2014–2016 年(均为 HS2012 版本)的数据为例;处理全部年份只需把年份列表改为 range(2000, 2017) 即可。
二、数据准备
原始海关数据体量庞大,我们先用 Stata 把每年的数据抽取出 newhgid(观测 ID)、年份、商品编码 三个变量,另存为分年 .dta 文件,减小后续读取的压力。对应的 Stata 代码如下(其 Python 等价写法见下方程序块,本项目已附带抽取好的 海关数据商品编码分年/ 数据):
use".../海关数据分年/`y'.dta", clearsave"海关数据商品编码分年/`y'", replace# Python 等价写法(需原始海关分年 .dta,本项目已提前抽取好,此处仅供示意)# for y in range(2000, 2017):# df = pd.read_stata(f".../海关数据分年/{y}.dta")# df = df[["newhgid", "年份", "商品编码"]]# df.to_stata(f"海关数据商品编码分年/{y}.dta", write_index=False)三、读取 HS 转换表(reticulate 虚拟环境)
本文档借助 reticulate 在 R 中调用 Python,并为项目创建专属虚拟环境 .venv,所需依赖(pandas / numpy / openpyxl / xlrd / matplotlib)全部隔离其中,避免与系统 Python(如 Anaconda)冲突。
重要说明:reticulate 在 R 会话中只能绑定一次 Python——一旦某个 {python} 代码块运行,Python 解释器就被锁定。因此虚拟环境的激活已在本文档开头的 reticulate-setup chunk 中、通过 Sys.setenv(RETICULATE_PYTHON = ...) 提前完成。
安装 Python 包(仅首次 knit 时自动执行)
py_pkgs <- c("pandas", "numpy", "openpyxl", "xlrd", "matplotlib")installed <- py_list_packages(".venv")$packageneed_install <- setdiff(py_pkgs, installed)if (length(need_install) > 0) { virtualenv_install(".venv", packages = need_install) message("已安装缺失的包:", paste(need_install, collapse = ", ")) message("所有 Python 包已就绪,无需安装")验证激活状态
版本之间的转换依赖联合国统计司提供的官方对照表。每个 Excel 文件里通常有两张表:
- Conversion Table:只保留在新版本中仍有对应关系的旧码,覆盖不全;
- Correlation Table:记录 1:1 / 1:n / n:1 / n:n 全部对应关系,覆盖度更高。
转换表可以从这里下载:https://unstats.un.org/unsd/classifications/Econ
因此我们统一使用 Correlation Table。所有转换表方向都是「新版本 → 旧版本」,其中第 1 列是新版本码、第 3 列是旧版本码。我们分别按正向(旧→新)和反向(新→旧)读取,一对多时取第一个匹配:
# 路径设置(knit 时已锁定工作目录为本文档所在目录)data_dir = os.path.join(base_dir, "海关数据商品编码分年")conv_dir = os.path.join(base_dir, "HS代码转换对照表")output_dir = os.path.join(base_dir, "统一HS编码")os.makedirs(output_dir, exist_ok=True)years = [2014, 2015, 2016]"""安全地转换为字符串,去掉被识别为浮点而产生的 '.0'。""" s = series.dropna().astype(str)return s.str.replace(r"\.0$", "", regex=True)file_02to96 = os.path.join(conv_dir, "HS 2002 to HS 1996 - Correlation and conversion tables.xls")file_07to02 = os.path.join(conv_dir, "HS 2007 to HS 2002 Correlation and conversion tables.xls")file_12to07 = os.path.join(conv_dir, "HS 2012 to HS 2007 Correlation and conversion tables.xls")file_17to12 = os.path.join(conv_dir, "HS 2017 to HS 2012 Conversion And Correlation Tables.xlsx")print(f"正向表行数: HS96->02={len(fwd_96to02)}, HS02->07={len(fwd_02to07)}, "f"HS07->12={len(fwd_07to12)}, HS12->17={len(fwd_12to17)}")四、构建完整查找表
有了相邻版本的对照关系后,就可以把它们链式拼接起来,为每个源版本构建一张包含全部 5 个目标版本的查找表。核心技巧是级联填充:若某一步转换缺失(NaN),则沿用上一版本的码,保证链条不中断。
以 HS2012 源(对应 2014–2016 年数据)为例——它向前只需一步到 HS2017,向后需要依次退回到 HS2007、HS2002、HS1996:
def_fill_chain(lk, order):"""按 order 顺序级联填充:缺失则沿用上一个版本的码。"""for i inrange(1, len(order)): prev, cur = order[i - 1], order[i] lk[cur] = lk[cur].fillna(lk[prev])print(f"HS2012 源查找表行数: {len(lk_12)}")其余三个源版本(HS1996 / HS2002 / HS2007)的查找表构建方式完全类似,只是正向、反向链条的长度不同,完整代码见配套脚本 使用 Python 统一海关税号编码.py。
五、执行编码转换
商品编码为 8 位:前 6 位参与版本转换,后 2 位(国内子目)保留不变。转换时用字典做「查表 → 缺失填原码 → 拼回后 2 位」,对上千万行数据也非常高效:
# 用 2014 年数据演示(源版本 HS2012)可以看到,源版本变量(这里是 HS2012)与原始 商品编码 完全一致(恒等转换),而其他版本会随着税目的历史变动而不同。最终把 HS1996 ~ HS2017 五个新变量与原始变量一起写回 .dta,即完成统一:
df = pd.read_stata(os.path.join(data_dir, f"{yr}.dta")) code = df["商品编码"].astype(str) mapping = dict(zip(lk_12["source_code"], lk_12[col])) conv = hs6.map(mapping).fillna(hs6) return (conv.astype(str) + suffix).values df["HS1996"] = map_ver("hs96") df["HS2002"] = map_ver("hs02") df["HS2007"] = map_ver("hs07") df["HS2012"] = map_ver("hs12") df["HS2017"] = map_ver("hs17") df.to_stata(os.path.join(output_dir, f"{yr}.dta"), write_index=False)print(f"{yr} 年已写出:统一HS编码/{yr}.dta")六、统一质量:无法统一的比例
并非所有编码都能成功统一。若某个 6 位码不是目标版本中的合法 HS 编码(例如中国海关特有的 98/99 章特殊码、或数据录入错误),它就无法被统一,我们保留其原码。下面按观测数加权,统计 2014–2016 年各版本无法统一的比例:
"hs96": pd.unique(pd.concat([bwd_02to96["to_old"], fwd_96to02["from_old"]])),"hs02": pd.unique(pd.concat([fwd_96to02["to_new"], fwd_02to07["from_old"], bwd_02to96["from_new"], bwd_07to02["to_old"]])),"hs07": pd.unique(pd.concat([fwd_02to07["to_new"], fwd_07to12["from_old"], bwd_07to02["from_new"], bwd_12to07["to_old"]])),"hs12": pd.unique(pd.concat([fwd_07to12["to_new"], fwd_12to17["from_old"], bwd_12to07["from_new"], bwd_17to12["to_old"]])),"hs17": pd.unique(pd.concat([fwd_12to17["to_new"], bwd_17to12["from_new"]])),ver_cols = {"hs96": "HS1996", "hs02": "HS2002", "hs07": "HS2007","hs12": "HS2012", "hs17": "HS2017"}src_of = {2014: "HS2012", 2015: "HS2012", 2016: "HS2012"}d = pd.read_stata(os.path.join(data_dir, f"{yr}.dta"), columns=["商品编码"]) hs6 = d["商品编码"].astype(str).str[:6]tab = hs6.value_counts().rename_axis("hs6").rename("n").reset_index()total = int(tab["n"].sum())for col, labelin ver_cols.items(): valid = set(valid_codes[col]) unmatched = int(tab.loc[~tab["hs6"].isin(valid), "n"].sum()) rows.append({"year": yr, "source_version": src_of[yr],"target_version": label, "total_obs": total,"unmatched_obs": unmatched,"unmatched_pct": unmatched / total * 100})miss_stats = pd.DataFrame(rows)print(miss_stats.to_string(index=False))
再用 matplotlib 画成热力图,直观展示各年各版本的无法统一比例:
matplotlib.use("Agg") # 无显示后端,仅供保存from 绘制缺失比例图 import plot_missing_heatmapplot_missing_heatmap(miss_stats,"各版本缺失比例热力图_2014-2016.png")从图中可以看出:
- 2015 年约 0.04%,主要是少量海关特殊码;
- 2016 年明显偏高(约 1.33%),原因是当年
980400 系列跨境电商监管码大量出现——它一个码就占了约 23 万条观测,而这类码根本不属于国际 HS 体系,因此在任何版本下都无法统一。
七、小结
统一海关税号编码的完整流程可以概括为四步:
- 抽取变量:用 Stata / Python 把大数据瘦身为
newhgid + 年份 + 商品编码; - 读取 Correlation Table:正、反两个方向分别构建相邻版本的对照关系;
- 链式拼接 + 级联填充:为每个源版本构建到 5 个目标版本的查找表;
- 向量化转换:前 6 位查表转换、后 2 位保留,缺失则保留原码。
除个别中国海关特殊码与数据录入错误外,绝大多数年份的统一成功率都在 99.9% 以上,可以放心用于跨期面板分析。完整可运行代码见配套脚本 使用 Python 统一海关税号编码.py 与 绘制缺失比例图.py。
如何参加课程?
购买 RStata 会员(任意会员均可)即可参加该课程啦(之前的和未来的都可以参加)!
购买会员可以从这里下单:https://rstata.duanshu.com/#/card/list/
* 如果发票可添加小编微信 r_stata2 (RStata 李老师)开具。如需数据资料,购买后可添加小编微信免费领取数据折扣卡。
更多关于 RStata 会员的更多信息可添加微信号 r_stata2 咨询:

课程主页(点击文末的阅读原文即可跳转):https://rstata.duanshu.com/#/brief/course/aae08b44662b44b284369804399ae4e9






