Zephyr中国跨国并购数据清洗实战:Python处理字段、币种与去重
发布时间:2026/10/11 13:36:05 锦皓数字建站

简介这份资源为Zephyr数据库导出的中国跨国并购交易数据合集时间跨度覆盖2000年至2024年面向从事国际商务、产业经济、金融研究的高校师生与分析师可用于跨国并购趋势分析、案例筛选与实证研究。压缩包共2个文件以xls表格为主体数据文件另附html格式的数据来源说明整体约9.09MB体量适中便于本地整理与二次加工。表格字段通常涵盖并购时间、交易双方、行业分类、交易金额等维度适合按年份、行业或国别做分组统计与可视化。目前已有123人学习下载可作为论文选题、课题申报或行业报告的基础数据支撑帮助读者省去逐条搜集与清洗的重复工作快速进入建模与解读环节。1. 跨国并购数据拿到手第一件事为什么不是打开 ExcelZephyr 中国跨国并购数据2000-2024年这个压缩包名字已经把范围说得很清楚标的涉及中国、时间跨度二十五年、交易类型是并购。但真正做过跨国并购研究的人都知道这类数据最怕的不是缺年份而是口径不统一——同一笔交易在不同年份可能被记成不同状态金额单位可能混着人民币和美元标的行业分类可能中途换过标准。所以拿到压缩包之后第一件事不是双击打开 Excel 看有多少行而是先搞清楚它的字段结构、编码方式和缺失模式。这份数据适合三类人做学术实证的研究者、做行业复盘的投资分析师、以及需要快速摸清某条赛道跨境交易脉络的从业者。它解决的核心问题是把分散在公告、监管披露和交易数据库里的中国相关并购事件整理成一张可以按时间、行业、金额、交易状态筛选的结构化表。但前提是你得知道怎么把它从“能看”变成“能用”。2. 先拆字段再谈分析Zephyr 并购数据的结构长什么样2.1 交易主表、标的表和买方表的三角关系Zephyr 这类并购数据库通常不是一张大宽表而是拆成几张有关联的表。常见做法是一张交易主表记录交易编号、公告日期、完成日期、交易金额、交易状态一张标的表记录被收购方的名称、国家、行业代码一张买方表记录收购方名称、国家、最终母公司。三张表通过交易编号关联。如果你拿到的压缩包里只有一个 CSV那大概率是已经做过扁平化处理的宽表。这时候要重点看列名里有没有target_、acquiror_、deal_这样的前缀它们能帮你快速判断字段归属。我一般会先用 Python 把列名全部打印出来按前缀分组再决定后续怎么拆。import pandas as pd # 先只读列名不加载全部数据避免大文件卡死 cols pd.read_csv(zephyr_china_ma_2000_2024.csv, nrows0).columns.tolist() # 按前缀分组快速看清表结构 groups {} for c in cols: prefix c.split(_)[0] if _ in c else other groups.setdefault(prefix, []).append(c) for k, v in groups.items(): print(k, -, v)这段代码只读表头不占内存。nrows0是关键参数它让 pandas 只解析列名就返回。分组逻辑用第一个下划线前的词作为前缀如果列名本身没有下划线就归到other。跑完之后你会得到类似deal - [deal_id, deal_date, deal_value...]的输出这时候再决定哪些列需要类型转换、哪些列需要拆分。2.2 金额字段的币种和单位陷阱跨国并购数据里最容易被低估的坑就是金额字段。Zephyr 通常会给一个原始金额列和一个美元换算列但不同年份的换算汇率来源可能不同。更麻烦的是有些交易只披露了估值范围字段里会写成undisclosed或NA直接做sum()会静默跳过导致你算出来的总金额偏低。我一般会先做三件事第一统计金额列的缺失率第二看非缺失值里有没有非数字字符第三确认美元换算列是否已经覆盖了所有非缺失的原始金额。df pd.read_csv(zephyr_china_ma_2000_2024.csv, low_memoryFalse) # 缺失率 print(df[deal_value_usd].isna().mean()) # 看非数字样本 sample df[deal_value_usd].dropna().astype(str) non_numeric sample[~sample.str.match(r^[\d\.]$)] print(non_numeric.head(20)) # 原始金额非空但美元金额为空的行数 mask df[deal_value_original].notna() df[deal_value_usd].isna() print(mask.sum())low_memoryFalse是为了让 pandas 一次性推断类型避免分块读取时同一列在不同块里被推断成不同类型。str.match的正则只允许数字和小数点任何带逗号、带货币符号、带文字说明的值都会被筛出来。最后那个mask统计的是“有原始金额但没换算”的行数如果这个数很大说明你不能直接用美元列做全量分析。2.3 时间字段的格式统一与财年对齐2000 到 2024 年跨度很大日期格式可能混着YYYY-MM-DD、DD/MM/YYYY甚至YYYYMM。更隐蔽的问题是财年有些交易记录的是财年结束日有些是公告日有些是完成日。如果你要做年度趋势分析必须明确用哪个日期字段并且统一到日历年。常见做法是优先用deal_completed_date缺失时回退到deal_announced_date再缺失才用deal_fiscal_year_end。回退逻辑要显式写出来不能默认fillna一个了事。df[date_final] pd.to_datetime(df[deal_completed_date], errorscoerce) df[date_final] df[date_final].fillna( pd.to_datetime(df[deal_announced_date], errorscoerce) ) df[date_final] df[date_final].fillna( pd.to_datetime(df[deal_fiscal_year_end], errorscoerce) ) df[year] df[date_final].dt.year print(df[year].value_counts().sort_index())errorscoerce让无法解析的值变成NaT而不是报错这样你可以先跑通再回头处理异常。三次fillna的顺序就是优先级顺序。最后按年统计如果某几年数量异常少大概率是那几年的日期字段格式有问题需要单独回去看原始值。3. 从原始表到可分析样本清洗与筛选的五个关键动作3.1 交易状态筛选别把传闻和意向书算进成交Zephyr 的交易状态字段通常有Completed、Announced、Pending、Rumour、Withdrawn等值。做实证研究时如果你要分析“实际发生的并购”就必须只保留Completed。但很多人会忽略Announced里有一部分后来变成了Completed而Completed里也可能包含后来又被撤销的。我一般会先看状态字段的取值分布再决定筛选策略。如果要做事件研究用公告日如果要做财务绩效分析用完成日且状态为Completed。print(df[deal_status].value_counts(dropnaFalse)) # 只保留已完成交易 df_completed df[df[deal_status].str.lower() completed].copy() # 检查是否有完成日期缺失的已完成交易 print(df_completed[deal_completed_date].isna().sum())str.lower()是为了防止大小写不一致。dropnaFalse让缺失值也出现在统计里避免你误以为没有缺失。如果已完成交易里还有大量完成日期缺失说明状态字段和日期字段的维护质量不一致需要人工抽查。3.2 标的国别与中国相关的判定口径标题写的是“中国跨国并购”但“中国相关”可以有三种口径标的在中国、买方在中国、或者最终母公司在中国。Zephyr 通常会给target_country、acquiror_country和acquiror_ultimate_parent_country三个字段。如果你只按标的国别筛会漏掉中国公司收购海外资产的情况如果只按买方国别筛会漏掉外资收购中国资产的情况。常见做法是先明确你的研究问题。如果是“中国企业出海并购”用买方国别如果是“外资入境并购”用标的国别如果是“所有涉及中国的跨境交易”三个字段取并集。china_mask ( (df[target_country] China) | (df[acquiror_country] China) | (df[acquiror_ultimate_parent_country] China) ) df_china df[china_mask].copy() print(df_china.shape)三个条件用|连接只要任一满足就保留。注意这里没有用str.contains因为国别字段通常是标准化的国家名精确匹配更安全。跑完之后对比一下只按单一字段筛的结果差异行数就是你需要人工判断的部分。3.3 行业分类的版本对齐2000 到 2024 年行业分类标准可能从 GICS 换到 NAICS 再换到 Zephyr 自己的分类。如果你直接按target_industry分组做趋势会发现同一行业在不同年份叫法不同。我一般会先看行业字段的唯一值数量如果超过 200 个说明粒度太细需要映射到一级或二级分类。print(df_china[target_industry].nunique()) print(df_china[target_industry].value_counts().head(30))如果唯一值太多常见做法是保留前 20 个高频行业其余归为Other。或者找一份行业映射表把细分类别归并到大类。映射表可以手工建也可以用关键词匹配但手工建更可控。3.4 去重同一交易的多条记录怎么合并跨国并购数据里同一笔交易可能因为多次公告、多次修订而出现多条记录。Zephyr 通常会给一个deal_id但有时候同一deal_id下会有不同版本的记录。我一般会按deal_id分组保留完成日期最新或状态为Completed的那条。df_china df_china.sort_values(date_final, ascendingFalse) df_dedup df_china.drop_duplicates(subset[deal_id], keepfirst) print(df_china.shape, -, df_dedup.shape)sort_values把最新日期排前面drop_duplicates的keepfirst就保留了最新那条。如果deal_id有缺失需要先用其他字段组合生成一个临时唯一键比如acquiror_name target_name year。3.5 缺失值处理什么时候该删什么时候该填金额和行业是并购分析里最常缺失的两个字段。我的原则是金额缺失超过 40% 的年份不做金额相关的趋势分析行业缺失超过 20% 的样本单独归为Unknown而不是直接删。直接删会引入选择偏差因为缺失往往不是随机的——小交易更可能不披露金额。# 按年统计金额缺失率 missing_by_year df_dedup.groupby(year)[deal_value_usd].apply(lambda x: x.isna().mean()) print(missing_by_year) # 行业缺失归为 Unknown df_dedup[target_industry] df_dedup[target_industry].fillna(Unknown)groupby加apply可以按年看缺失率的变化趋势。如果某几年缺失率突然飙升说明那几年的数据采集口径可能变了需要在论文或报告里注明。4. 避坑与排查Zephyr 中国并购数据最常见的五个翻车点4.1 现象按年统计交易数量某几年突然断崖式下跌原因日期字段在那几年用了不同的格式pd.to_datetime解析失败后变成NaT被value_counts静默排除。解决先看date_final的缺失率按年分布再回去检查原始日期列的格式。如果原始列是YYYYMM这种没有日的格式需要手动补一个01再解析。4.2 现象总交易金额算出来比预期低很多原因金额字段里混着undisclosed、NA、-等占位符astype(float)会报错但如果你用pd.to_numeric(errorscoerce)这些值会变成NaN被跳过。解决先统计非数字样本确认占位符种类再决定是删还是填。如果要做总金额必须明确说明“基于已披露金额的交易”。4.3 现象同一笔交易在数据里出现两次金额还不一样原因一次是公告时的预估金额一次是完成时的最终金额。Zephyr 可能保留了两条记录。解决按deal_id去重保留完成日期最新的那条。如果deal_id也重复用acquiror_name target_name year组合去重。4.4 现象行业趋势图里某个行业在中间几年消失原因行业分类标准换了旧名称不再使用。解决建一张映射表把旧名称映射到新名称或者统一归并到一级分类。映射表要保留原始字段方便回溯。4.5 现象中国买方收购海外资产但标的国别字段显示的是海外原因这是正常现象。如果你只按target_country China筛选就会漏掉出海并购。解决明确你的筛选口径三个国别字段取并集并在方法部分写清楚。5. 用 Python 做一份可复现的年度趋势与行业分布5.1 年度趋势交易数量与金额的双轴图清洗完之后最直接的验证方式就是画年度趋势。交易数量用柱状图金额用折线图双轴展示。这样能一眼看出“数量多但金额小”和“数量少但金额大”的年份差异。import matplotlib.pyplot as plt yearly df_dedup.groupby(year).agg( deal_count(deal_id, count), total_value(deal_value_usd, sum) ).reset_index() fig, ax1 plt.subplots(figsize(12, 5)) ax1.bar(yearly[year], yearly[deal_count], color#4C72B0, alpha0.7, labelDeal Count) ax1.set_ylabel(Deal Count) ax1.set_xlabel(Year) ax2 ax1.twinx() ax2.plot(yearly[year], yearly[total_value], color#DD8452, markero, labelTotal Value (USD)) ax2.set_ylabel(Total Value (USD)) plt.title(China Cross-border MA: 2000-2024) fig.legend(locupper left, bbox_to_anchor(0.1, 0.9)) plt.tight_layout() plt.savefig(yearly_trend.png, dpi150)groupby(year)后agg同时算数量和金额。twinx()创建共享 x 轴的双 y 轴。bbox_to_anchor调整图例位置避免遮挡数据。保存时dpi150保证清晰度。跑完这张图你就能快速判断数据在时间维度上是否合理。5.2 行业分布Top 15 行业的交易金额占比行业分布用横向条形图更易读尤其是行业名称较长的时候。先按行业汇总金额取前 15 个再算占比。industry df_dedup.groupby(target_industry)[deal_value_usd].sum() industry industry.sort_values(ascendingFalse).head(15) industry_pct industry / df_dedup[deal_value_usd].sum() * 100 fig, ax plt.subplots(figsize(10, 6)) industry_pct.sort_values().plot(kindbarh, axax, color#55A868) ax.set_xlabel(Share of Total Deal Value (%)) ax.set_title(Top 15 Target Industries by Deal Value) plt.tight_layout() plt.savefig(industry_dist.png, dpi150)sort_values(ascendingFalse).head(15)取金额最大的 15 个行业。industry_pct算的是占总金额的比例不是占交易数量的比例。barh是横向条形图sort_values()默认升序这样最大的行业会显示在最上面。如果某个行业占比异常高需要回去检查是不是有几笔超大交易集中在这个行业。5.3 用交叉表验证“出海”与“入境”的年份差异最后一步验证按年份和交易方向做交叉表。交易方向可以用标的国别和买方国别组合判断标的在中国、买方在海外是入境标的在海外、买方在中国是出海。def deal_direction(row): if row[target_country] China and row[acquiror_country] ! China: return Inbound elif row[target_country] ! China and row[acquiror_country] China: return Outbound else: return Other df_dedup[direction] df_dedup.apply(deal_direction, axis1) cross pd.crosstab(df_dedup[year], df_dedup[direction]) print(cross.tail(10))apply(axis1)逐行判断方向。pd.crosstab生成年份和方向的交叉表。tail(10)看最近十年。如果Other占比很高说明你的国别字段可能有缺失或口径不一致需要回去检查。5.4 一个我踩过的坑不要用sum()直接算缺失金额早期我做年度金额趋势时直接df.groupby(year)[deal_value_usd].sum()结果某几年金额特别低。后来才发现那几年缺失率超过 50%sum()默认跳过NaN导致总金额被低估。正确做法是同时输出缺失率或者在图表里标注“基于已披露金额”。这个习惯我保留到现在任何金额聚合旁边必须有一列缺失率。希望帮到你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。