碳排放交易明细数据:从xlsx到dta的清洗与分析 做双碳研究、写ESG分析报告或者单纯想研究碳市场价格的朋友一定都有过这种经历翻遍各个交易所官网下载公告、拼接Excel、手工清理折腾大半天才勉强凑出一张能用的表。2013年到2025年9月这份地区碳排放权交易明细数据以xlsxdta双格式打包把多年来各试点地区的日度成交记录整理成一张可直接使用的明细表对我这种常年和数据打交道的人来说确实省下了大量时间。这篇文章不打算只介绍数据集本身我更多会聊聊拿到这种xlsx格式交易明细之后怎么判断字段口径、怎么高效读取、怎么转成dta以及能拿它做哪些实际分析。适合的数据使用者包括环境经济方向的研究生、ESG数据工程师、做碳市场量化的分析师以及尝试用Excel做筛选和透视的初级学习者。1. 这套数据到底装了什么时段、范围与字段结构先说整体印象。标题里的“2013年-2025年9月”这个时间跨度刚好覆盖了国内碳排放权交易试点市场从起步到逐步活跃的完整阶段。早期试点从2013年前后开始运行多个地区陆续上线交易之后几年交易品种、交易规则、参与主体都有明显变化。这样一条长序列数据对研究市场演进、价格形成、流动性变化来说非常珍贵。从文件结构上看xlsx版本适合用Excel、WPS这类工具直接打开做筛选、透视表、基础图表都很顺手dta版本则是给Stata用户准备的导进去就是Stata能识别的变量类型省掉了不少转格式的麻烦。两种格式共存的情况在数据发布方那里并不少见本质上就是照顾不同使用习惯的人Excel派直接看图筛数Stata派拿来就是建模。1.1 明细表的字段结构虽然我没有拿到原始字段清单但根据国内碳市场公开交易数据的一贯发布口径以及同类日度交易明细的常见结构可以合理推断这张表会包含以下基础字段字段名可能含义需要留意的点交易日期每笔/每个交易日的日期碳市场交易日与股票日历不完全一致部分试点早期每周只有两个交易日地区试点市场名称或代码不同发布文件的叫法可能不同有的叫“地区”有的叫“市场”交易品种配额、核证自愿减排量等各试点品种代码体系不统一合并时要注意成交数量当日/当笔成交量单位可能是吨、手或万吨口径差异极大成交金额对应成交额有的文件按元记录有的按万元记录最高价当日最高成交价缺失常见不能直接填0最低价当日最低成交价同理收盘价当日收盘价格有的市场叫“加权均价”口径不同均价加权平均价或算术平均价决定后续价格序列的可靠性如果你拿到的文件里有额外的“挂牌量”“申报量”“实际成交笔数”这些字段那更好意味着可以做流动性层面的分析。这里先提醒一句任何从公开渠道获取的数据集动手之前都要看一遍描述文件或README确认字段口径没有说明文件时拿前几十行数据做抽查推断比直接全量使用稳妥很多。1.2 为什么xlsx和dta两个版本都要给这个问题值得展开。dta是Stata的原生数据格式但Stata用户在国内学术圈的基数很大做面板、做回归都得靠它。如果数据发布方只给xlsxStata用户还得用import excel去读读到之后变量名、日期格式、缺失值都要重新处理如果只给dtaExcel用户就没法直接打开看。所以双格式分发本质上是降低使用门槛的常见策略不代表数据本身有差异。从处理链路来看xlsx相当于“源文件”任何编程语言都能读尤其是JavaScript生态和Python生态都有非常成熟的解析方案dta更多是“分析就绪”版本列名通常已经被改成英文或拼音缺失值也按Stata规则编码。但这里有个传统误区很多人默认dta就是干净版本直接拿去跑模型。实际上发布方做格式转换时用的工具、参数、编码策略都不一样dta里也可能藏着浮点精度问题、字符串缺失值、日期类型不一致等隐患。所以无论用哪个版本清洗验证都不能跳过。2. 数据到手先别急着跑回归三件必须做的检查拿到这张表我最想强调的事不是“怎么分析”而是“先怎么检查”。碳市场数据和其他金融数据一个很大的不同点在于它的交易频率低、参与者有限很多地区早期甚至出现连续数周无交易的状况。如果你按股票数据的思维去处理一上来就做滚动均线、波动率建模很容易被数据里的断层给骗了。2.1 交易日缺口与频度扫描我拿到任何此类数据的第一步永远是统计每个地区、每年的交易日数量。这一步能快速暴露三类问题一是数据缺失某个地区某个月记录数偏少甚至为零二是日期标签错乱可能是Excel里日期被读成了序列号导致年份变成了1900年或1970年附近三是不同地区交易日历不统一后续做面板合并时会很麻烦。用Python做这个检查非常方便import pandas as pd df pd.read_excel(地区碳交易明细.xlsx, parse_dates[交易日期]) df[年份] df[交易日期].dt.year trade_days df.groupby([地区, 年份])[交易日期].nunique().unstack() print(trade_days)注意这里用的是nunique()而不是count()。因为同一地区同一天可能有多个品种成交产生多行记录nunique()统计的是“日期去重后的交易日数量”更能反映市场活跃度。如果你发现某个地区一年只有二三十个交易日那就不要用日度频率去做时间序列模型后面分析时得先重采样到周度或月度。2.2 单位、缺失值与疑似异常价格单位问题是我踩过最多次的坑。有的市场成交数量按“吨”报有的按“手”报且“手”对应的吨数各不相同成交金额有的按“元”报有的按“万元”报。你如果不看表头备注直接拿数值去算结果会差好几个数量级。我的习惯是先在Excel里打开原始文件随机挑几个交易日的成交量、成交额、均价做交叉验证比如用成交量乘以收盘价看能否对得上成交金额。对不上就说明某个字段单位或口径有问题。价格异常值检查也很重要。碳价波动虽然比股票温和但早期部分市场出现过高价、低价并存的情况有些是数据录入错误有些是真实的极端成交。一个简单的扫描方式# 检查价格是否存在负值、零值或异常倍数 price_cols [最高价, 最低价, 收盘价, 均价] for col in price_cols: print(col, df[col].describe()) print(负值数量:, (df[col] 0).sum())如果出现负价格多半是数据录入错误或单位换算残留出现零价格则要看是否当日只有挂牌没有成交配套的“成交数量”字段会告诉你答案。2.3 Excel空值处理的隐藏分歧Excel文件里的“空单元格”在导入不同工具时表现完全不一样。pandas的read_excel会把空单元格解析成NaN这是理想状态但很多原始数据表里空单元格其实被填成了字符串“-”、半角空格或者大写的“NA”。如果不先统一清理整列会被识别成object类型后续做数学运算就会报错或者强制转换。我通常会用这样一段代码做兜底清理df.replace([, -, #N/A, NA, ], pd.NA, inplaceTrue) for col in [成交数量, 成交金额, 最高价, 最低价, 收盘价, 均价]: df[col] pd.to_numeric(df[col], errorscoerce)这段代码会把各种非标准缺失值统一转成pd.NA再把数值列强制转成数字类型无法转换的会被自动变成缺失值。做完这一步你再去做统计描述结果才是可解释的。3. xlsx不是麻烦而是起点用JS工具库操作Excel的实用方法这类数据发布成xlsx很多人第一反应是用Python去读这没问题。但如果你本身是做前端、Node后端或者系统集成的反而会觉得“读取Excel”是一个不大不小的工作。好在现在JavaScript生态里有一个非常成熟的开源库npm包名就叫xlsx它几乎是当前处理Excel表格的默认选择很多导出报表、批量填表工具在底层都依赖它。3.1 安装与基本读取在Node.js项目里安装只需一行命令npm install xlsx然后按下面这种方式读取文件const XLSX require(xlsx); const workbook XLSX.readFile(地区碳交易明细.xlsx, { cellDates: true }); const sheet workbook.Sheets[workbook.SheetNames[0]]; const rows XLSX.utils.sheet_to_json(sheet, { defval: null }); console.log(总行数:, rows.length); console.log(字段列表:, Object.keys(rows[0]));这里有一个关键参数值得多说两句cellDates: true。Excel内部存储日期的逻辑和普通表不一样它把日期存成了序列号数字比如2025年1月1日对应的序列号是45658。如果读取时不加cellDates你拿到的“交易日期”字段就是一列数字排序和筛选都会出错。加了cellDates: true之后库会尝试把连续数字还原成Date对象这样后续按日期过滤、分组、排序就顺理成章了。3.2 按地区筛选并写回新Excel在实际使用中我经常需要从全国明细里拆出单个市场的数据给不同小组使用。用这个库实现非常简单const target rows.filter(r r[地区] 北京 r[成交数量] ! null); const newSheet XLSX.utils.json_to_sheet(target); const newBook XLSX.utils.book_new(); XLSX.utils.book_append_sheet(newBook, newSheet, 北京市场); XLSX.writeFile(newBook, 北京碳交易明细.xlsx);这套流程对中文表头也是天然支持的json_to_sheet会自动把对象里的key作为表头key是中文导出的Excel表头就是中文。所以从JSON、CSV、数据库查询结果转Excel的时候用这个库几乎是零成本。3.3 大文件场景要换策略库很好用但它毕竟是纯JS解析走了很多兼容层文件一旦超过几十MB读取速度和内存占用都会明显上升。如果你的原始xlsx有几十万行我建议不要硬扛。两个替代方案先用Excel或LibreOffice把大文件另存为CSV格式然后用流式方式按行读取速度提升非常明显Node端升级到SheetJS的付费版xlsx并配合流式解析或者改用Python Pandas它底层是C实现的大文件处理快很多。一句话总结xlsx库适合中小型表格、自动化流程、在线导出场景遇到真正的大数据量回到Python或流式方案更划算。4. 从xlsx到dta跨格式转换中最容易翻车的六个细节很多用户直接用dta版本这没毛病。但如果你拿到的只有xlsx或者你想自己控制字段清洗逻辑那就绕不开“xlsx转dta”这一步。这个过程看着简单实际上坑非常多。4.1 Stata变量名规则与中文列名Stata对变量名的要求很严格长度有限制不能用中文不能包含空格和特殊符号。所以直接用import excel读入原始中文表头Stata大概率会把变量名改成V1、V2之类的占位符或者在保存dta时直接报错。正确处理方式是转dta之前先把列名改成英文。建议的命名方案df.columns [ date, region, product, volume, amount, high, low, close, avg_price ]这套命名语义清晰Stata和Python里调用都没障碍。如果担心忘掉对应关系可以在转格式前单独输出一份字段对照说明.csv给自己或同事留个备忘。4.2 缺失值编码的差异这是最容易出问题的地方。Excel里的空值本质上就是“没有内容”但dta里区分数值型缺失值和字符串型缺失值数值型缺失在Stata中显示为点号.字符串缺失显示为空字符串。Pandas会把NaN自动映射成Stata数值型缺失值但前提是你先做转换而不是留着-、NA这类文本。所以转dta之前一定要先做一次空值替换和类型转换df.replace([, -, #N/A, NA, ], pd.NA, inplaceTrue) for col in [volume, amount, high, low, close, avg_price]: df[col] pd.to_numeric(df[col], errorscoerce)4.3 浮点精度问题Excel用IEEE754浮点数存储数值而对万元级别的成交额来说这通常不会造成太大误差但确实可能出现类似12345678.999999这样的情况。如果后续你要用金额做精确匹配或者加总对账浮点误差就会变得碍眼。我的习惯是在转换前统一做精度控制df[amount] pd.to_numeric(df[amount], errorscoerce).round(2) df[volume] pd.to_numeric(df[volume], errorscoerce).round(4)金额保留两位小数成交量保留四位小数基本能满足绝大多数分析需求。如果以后要做对账也建议以“元”为单位统一转换为整数分存储彻底规避浮点问题。4.4 日期字段的类型保持pandas读Excel时如果指定了parse_dates[交易日期]转换出来的日期列会被自动映射成Stata的日期类型。但如果你不小心让日期变成了字符串Stata里做时间序列操作时排序就会乱掉比如“2025-10-8”会排在“2025-9-1”前面因为字符串排序优先比较第一个字符。所以写转换脚本时日期列务必确认是datetime类型df[date] pd.to_datetime(df[date], errorscoerce)4.5 Stata版本兼容性Pandas的to_stata允许指定输出格式版本。默认情况下新版Pandas会输出较新的dta格式但老版本Stata可能读不了。为了兼容多数用户我建议显式指定version114即Stata 14格式df.to_stata(地区碳交易明细.dta, write_indexFalse, version114)这样无论是Stata 15、16、17还是更早版本读起来都没有障碍。4.6 转换后的交叉验证转完不代表万事大吉。我会在转换后马上做一组简单校验分别读回xlsx和dta版本对比行数、每个字段的非缺失数量、关键字段求和。写个五行的核对脚本比返工代价小得多。5. 拿到这套交易明细后我能实际做哪些分析数据清洗完毕格式转换妥当接下来才是真正有意思的部分。基于多年使用这类数据的经验给你几个可落地的分析方向。5.1 地区碳价走势与市场分化最经典的应用计算各试点市场的月度加权均价观察不同市场碳价的绝对水平和波动趋势。具体做法是先按“地区月份”做分组聚合用成交量作权重计算加权平均价df[月份] df[date].dt.to_period(M) monthly df.groupby([地区, 月份]).apply( lambda x: (x[amount].sum() / x[volume].sum()) ).reset_index(name月度均价)注意个别地区在早期月份几乎没有成交强行计算出来的均价容易失真。建议在聚合结果中加上“当月成交量”列做图时把成交量过低的月份剔除或打上标记。5.2 成交量的年度集中度观察成交量结构能直接反映市场活跃度。你可以快速算出每年不同地区的成交量占比观察哪些市场主导了整体交易哪些市场长期处于低活跃状态。这类图表对写政策研究背景、市场机制对比类文章很有帮助。5.3 履约期前后的成交放量效应碳市场与股票市场最不一样的地方在于它有明显的履约驱动周期。控排企业需要在履约截止日前完成配额清缴所以临近履约期时市场成交通常会显著放大价格也可能出现阶段性波动。这个效应用事件研究法很容易验证把每个地区一年中的履约期前后各30个自然日定义为事件窗口比较窗口内外成交量的均值。5.4 面板数据的准备思路如果你想做严格的计量回归比如碳价与宏观经济变量、能源价格之间的关系那么需要把日度明细整理成地区-时间面板。难点在于不同市场交易日不同步不能直接把日度数据堆积比较。推荐方案先构造一个统一的交易日历取所有市场交易日的并集再对每个市场价格列做前向填充last observation carried forward把缺失的交易日价格用最近一个交易日的价格补上。在Stata里可以用tsset配合tsfill实现在Python里用reindex加ffill更直接。最后补一个我自己的操作习惯拿到这种xlsxdta双格式的数据文件我不会默认两个版本完全一致而是先用脚本跑一遍记录数和关键字段合计两边对上了再进分析。这个动作花不了五分钟却能在后续省下大量返工时间。如果你也希望这套数据真正变成可复现、可发表的研究资产建议从一开始就写上清洗日志把每次字段口径调整和缺失值处理都记录下来这对以后回溯分析会很省力。