用集合思维理解Pandas DataFrame操作原理

📅 2026/7/12 3:10:24 👁️ 阅读次数 📝 编程学习
用集合思维理解Pandas DataFrame操作原理

1. 为什么用集合思维重构 DataFrame 操作,比死记merge参数更可靠?

在数据科学一线干活的这几年里,我带过不少刚转行的朋友,也帮团队新人 debug 过上百个“明明 merge 写对了,结果却漏数据”的案例。最常听到的一句抱怨是:“how='inner'how='left'到底哪个是交集?drop_duplicates()放前面还是后面才不丢行?”——这说明问题不在语法,而在底层逻辑没打通。

其实,Pandas 的mergeconcatisin这些操作,根本不是凭空设计的魔法命令,它们就是集合论在二维表格上的工程实现。你把df1.merge(df2, how='inner')理解成“取两个表的公共行”,和把set(df1.values.tolist()) & set(df2.values.tolist())理解成“取两个集合的交集”,本质完全一致。区别只在于:前者是带索引、列名、类型校验的工业级封装,后者是数学原语。一旦你脑子里有这张“集合映射图”,所有看似零散的操作就自动连成一张网。

比如,当业务方突然说“我要所有在 A 表但不在 B 表的订单,且必须保留 A 表的原始时间戳和备注字段”,新手会翻文档查anti-join,老手直接想:“这是 A−B 差集,但要求保留 A 的全部列”。于是立刻排除merge + indicator(它默认只保留交集列),转向concat + isin组合——因为差集的本质是“筛选”,不是“连接”。

再比如,遇到含 NaN 的表做去重合并,pd.concat([df1, df2]).drop_duplicates()常常失效。为什么?因为 NaN != NaN,集合里nan被视为不同元素。这时候你若只背命令,就会卡住;但若理解集合论中“相等性需明确定义”,马上意识到要先用df.fillna()统一占位符,或改用mergeindicator=True方案——它内部用 hash 比较,能正确处理 NaN。

我见过太多人把 Pandas 当成 Excel 函数来学:VLOOKUP对应mergeFILTER对应query。但真实项目里,数据源来自 API、数据库、Excel、JSON,字段名不统一、空值逻辑不一致、时间格式混杂。这时候靠“函数对应表”只会越走越偏。而集合思维是通用语言:无论数据长什么样,你都能先问三个问题——哪些行属于集合 A?哪些属于集合 B?我要的是 A∪B、A∩B、A−B 还是 B−A?答案出来,代码自然浮现。

所以这篇不是讲“怎么写 merge”,而是带你重建一套可迁移、可推演、可 debug 的思维框架。接下来每一节,我都会从数学定义出发,落到 Pandas 实现,再补上生产环境踩过的坑。你不需要记住所有代码,只要记住那张 Venn 图——灰色重叠区是交集,橙色独占区是差集,整个圆圈是并集。剩下的,都是填空题。

2. 集合论基础与 DataFrame 的映射关系:为什么“行”才是集合的基本元素?

2.1 集合论的四个核心约定,如何严格对应到 DataFrame 行操作?

很多人误以为“DataFrame 的列是集合”,这是最大的认知陷阱。我们先看数学定义:集合是互异元素的无序整体,元素本身不可拆分。对应到 DataFrame,只有整行数据(即Series)才能作为集合的原子单位。原因有四,缺一不可:

第一,互异性(Uniqueness)。数学中{1,1,2}等价于{1,2},重复元素自动去重。DataFrame 的drop_duplicates()正是此原则的实现——它默认按所有列比较整行是否完全相同。如果你只传subset=['id'],相当于定义了一个新集合,其元素是(id,)元组,而非整行。这解释了为什么df.drop_duplicates(subset=['price'])可能保留多行“同价不同量”的记录:在你定义的集合里,“价格”才是元素,其他字段只是附属信息。

第二,无序性(Unorderedness)。集合{a,b,c}{c,b,a}完全等价。DataFrame 的concat([df1,df2])默认不保证顺序,merge结果的行序也取决于索引和算法。若业务强依赖顺序(如时序分析),你必须显式用sort_values()reset_index(drop=True)重排——这不是 bug,而是集合论与现实需求的必然妥协。

第三,元素不可变性(Immutability of Elements)。Python 的set要求元素可哈希(hashable),所以列表、字典不能直接放入set。DataFrame 行转为元组时,若含 list/dict,tuple(row)会报错。这就是为什么df.apply(tuple, axis=1)在含嵌套结构的表上失败。解决方案不是硬扛,而是先标准化:用json.dumps()序列化复杂字段,或用df.astype(str)统一转字符串——本质是重新定义“什么算作一个可比较的元素”。

第四,空集(Empty Set)的明确性。数学中 ∅ 是合法集合,Pandas 里pd.DataFrame(columns=df1.columns)就是空集的完美对应。当你做df1[~df1.isin(df2).all(axis=1)]这类操作,结果为空时返回Empty DataFrame,而非报错。这点至关重要:ETL 流程中上游数据缺失是常态,用集合思维设计的逻辑天然具备空集鲁棒性。

提示:检验你是否真正理解“行即元素”,请回答这个问题——df1.equals(df2)set(df1.values.tolist()) == set(df2.values.tolist())在什么情况下结果不同?答案是:当两表行数相同、内容相同但顺序不同时,equals()返回False(因它校验索引和顺序),而集合比较返回True(因集合无视顺序)。这恰恰证明equals()不是集合操作,而是结构校验。

2.2 Venn 图的三重解读:从数学符号到 Pandas 代码的逐层翻译

Venn 图不是装饰画,它是集合操作的执行蓝图。我们以原文的df1df2为例,深挖每个区域的代码实现逻辑:

灰色重叠区(A∩B):数学定义是“同时属于 A 和 B 的元素”。Pandas 中最直接的实现是df1.merge(df2, how='inner')。但注意,merge默认以所有同名列为键。若df1df2都有pricequantitymargin%三列,则等价于on=['price','quantity','margin%']。这正是集合交集的要求:整行完全匹配才算重叠。如果业务只需“价格相同”,就必须显式指定on='price',此时集合元素变为(price,),而非整行。

橙色独占区(A−B):数学定义是“A 中有、B 中没有的元素”。这里最容易出错。新手常用df1[~df1.isin(df2).all(axis=1)],但这是严重错误!isin()按列广播比较,它检查df1的每个单元格是否在df2对应列中出现,而非整行匹配。例如df1.iloc[0] = [10,100,1]df2[10,120,7]isin()会认为price=10存在,quantity=100不存在,最终all(axis=1)返回False,导致本该保留的行被过滤。正确做法永远是:将行转为可哈希对象再比较df1.apply(tuple, axis=1)是最直观的,但要注意 NaN 处理。

蓝色独占区(B−A):同理,是df2[~df2.apply(tuple, axis=1).isin(df1.apply(tuple, axis=1))]。但生产环境我更推荐merge方案:df2.merge(df1, indicator=True, how='left').query('_merge == "left_only"')。为什么?因为merge内部用 hash 表查找,时间复杂度 O(n+m),而apply(tuple)是 Python 循环,大数据量时慢 5-10 倍。indicator=True还能让你看到_merge列的'both''left_only''right_only',调试时一目了然。

整个圆圈(A∪B):数学上是“A 或 B 中的元素”。pd.concat([df1,df2]).drop_duplicates()看似合理,但存在隐患:若df1df2列顺序不同(如df1列为['price','qty']df2['qty','price']),concat会按位置拼接,导致列错位。安全做法是先对齐列:df2 = df2[df1.columns],再concat。或者直接用mergeouter模式:df1.merge(df2, how='outer', suffixes=('_1','_2')),它自动处理列名冲突,且支持validate='one_to_one'校验一对一关系。

注意:Venn 图的“区域面积”在 DataFrame 中不表示行数比例。df1有 6 行,df2有 6 行,但交集可能只有 1 行(如原文的article 3)。不要用图形大小推断数据量,那是数学教学图的误导。

3. 四大核心操作的工业级实现:从原理到避坑的完整链路

3.1 并集(Union):如何确保合并后既不丢行也不错列?

并集的目标是生成一个新 DataFrame,包含df1df2的所有行,且无重复。表面看pd.concat([df1, df2]).drop_duplicates()最简单,但实际项目中,我至少遇到过五种让它失效的场景:

场景一:列名大小写/空格不一致
df1.columns = ['Price', 'Quantity', 'Margin%']df2.columns = ['price', ' quantity', 'margin %']concat会创建 6 列,而非 3 列。解决方案是标准化列名:

def standardize_cols(df): return df.rename(columns=lambda x: x.strip().lower().replace(' ', '_').replace('%', 'pct')) df1_std = standardize_cols(df1) df2_std = standardize_cols(df2) union_df = pd.concat([df1_std, df2_std], ignore_index=True).drop_duplicates()

场景二:数据类型隐式转换
df1['price']int64df2['price']float64drop_duplicates()可能将1010.0视为不同元素。必须显式统一类型:

# 先对齐类型,再去重 for col in df1_std.columns: if df1_std[col].dtype != df2_std[col].dtype: common_type = np.find_common_type([df1_std[col].dtype, df2_std[col].dtype], []) df1_std[col] = df1_std[col].astype(common_type) df2_std[col] = df2_std[col].astype(common_type) union_df = pd.concat([df1_std, df2_std], ignore_index=True).drop_duplicates()

场景三:时序数据的时间精度差异
df1['timestamp']datetime64[ns]df2['timestamp']datetime64[s],微秒级差异导致去重失败。解决方案是截断精度:

df1_std['timestamp'] = df1_std['timestamp'].dt.floor('S') # 截断到秒 df2_std['timestamp'] = df2_std['timestamp'].dt.floor('S')

场景四:含 NaN 的行去重
如前所述,NaN != NaN。drop_duplicates()默认将含 NaN 的行视为不同。安全方案是用merge

# 创建唯一标识列,NaN 用特殊字符串代替 def create_hash_key(df): df_copy = df.copy() for col in df_copy.columns: df_copy[col] = df_copy[col].fillna('NULL_PLACEHOLDER') return df_copy.apply(lambda row: hash(tuple(row)), axis=1) df1_std['_key'] = create_hash_key(df1_std) df2_std['_key'] = create_hash_key(df2_std) union_df = pd.concat([df1_std, df2_std], ignore_index=True).drop_duplicates(subset='_key').drop('_key', axis=1)

场景五:内存溢出的大表合并
df1df2各有千万行时,concat会创建临时 2000 万行的 DataFrame,极易 OOM。此时必须流式处理:

# 分块读取,用集合缓存已见的行哈希 seen_hashes = set() union_rows = [] for chunk in pd.read_csv('df1.csv', chunksize=10000): for _, row in chunk.iterrows(): row_hash = hash(tuple(row.fillna('NULL'))) if row_hash not in seen_hashes: seen_hashes.add(row_hash) union_rows.append(row) # 对 df2 同理,最后构造 DataFrame union_df = pd.DataFrame(union_rows)

实测心得:在日均处理 500GB 数据的广告归因系统中,我们最终采用dask.dataframeconcat+drop_duplicates,配合npartitions=100分区,比 Pandas 快 3.2 倍,内存占用降为 1/4。但小项目没必要上 Dask,掌握上述五种场景的应对策略足矣。

3.2 交集(Intersection):为什么 inner join 不是万能钥匙?

df1.merge(df2, how='inner')是交集的标准解,但它的“标准”建立在两个脆弱假设上:列名完全一致所有列都参与匹配。现实数据往往打破这两个假设。

假设一被打破:同义列名不同
销售表df1product_id,库存表df2item_codemerge必须指定left_on='product_id', right_on='item_code'。但注意,left_onright_on的列数必须相等,且类型兼容。若df1['product_id']是字符串,df2['item_code']是整数,需先转换:

df2['item_code'] = df2['item_code'].astype(str) intersection = df1.merge(df2, left_on='product_id', right_on='item_code', how='inner')

假设二被打破:部分列参与匹配
业务要求“价格和数量都相同才算重叠”,但df1df2还有seller_iddate等无关列。若直接merge,会因seller_id不同而漏掉本该匹配的行。正确做法是只选关键列:

key_cols = ['price', 'quantity'] intersection = df1[key_cols].merge(df2[key_cols], how='inner').drop_duplicates() # 若还需原表其他字段,再用 intersection 结果反查 result = df1.merge(intersection, on=key_cols).merge( df2.merge(intersection, on=key_cols), on=key_cols, suffixes=('_df1', '_df2') )

更隐蔽的陷阱:浮点数精度误差
df1['price'] = [10.0000001, 20.0000002]df2['price'] = [10.0, 20.0]merge会认为无交集。解决方案是量化:

# 将价格四舍五入到分 df1['price_rounded'] = (df1['price'] * 100).round().astype(int) / 100 df2['price_rounded'] = (df2['price'] * 100).round().astype(int) / 100 intersection = df1.merge(df2, on=['price_rounded', 'quantity'], how='inner')

终极方案:用 indicator 精确控制
当逻辑复杂时,mergeindicator=True是我的首选:

merged = df1.merge(df2, how='outer', indicator=True, suffixes=('_1', '_2')) intersection = merged[merged['_merge'] == 'both'].drop('_merge', axis=1) # 此时 intersection 包含 df1 和 df2 的所有列,可自由选择保留哪些 final_result = intersection[['price', 'quantity', 'margin%_1', 'margin%_2']]

这个方案的优势在于:它不预设匹配逻辑,而是先生成全量关联结果,再用布尔索引筛选。调试时打印merged['_merge'].value_counts(),立刻知道bothleft_onlyright_only各有多少行,定位数据质量问题。

3.3 差集(Difference):A−B 的七种写法,哪种在生产环境最稳?

差集是四大操作中最易出错的。df1[~df1.isin(df2).all(axis=1)]这种写法,我在代码审查中已标记为“禁止使用”。下面列出七种实现,并标注适用场景:

方法代码示例时间复杂度NaN 安全大数据友好推荐指数说明
1. merge + indicatordf1.merge(df2, indicator=True, how='left').query('_merge=="left_only"')O(n log n)⭐⭐⭐⭐⭐内置 hash,自动处理 NaN,支持validate校验
2. isin + tupledf1[~df1.apply(tuple,1).isin(df2.apply(tuple,1))]O(n×m)⭐⭐小数据快,但apply是 Python 循环,大数据极慢
3. isin + strdf1[~df1.astype(str).apply(tuple,1).isin(df2.astype(str).apply(tuple,1))]O(n×m)⭐⭐⭐解决 NaN,但字符串转换耗内存
4. concat + duplicatedpd.concat([df1,df2,df2]).drop_duplicates(keep=False)O((2n+m) log(2n+m))⭐⭐⭐巧妙利用keep=False,但需确保 df2 无重复行
5. numpy.isinmask = ~np.isin(df1.values.view([('', df1.values.dtype)] * df1.shape[1]), df2.values.view([('', df2.values.dtype)] * df2.shape[1]))O(n log n)⭐⭐⭐⭐NumPy 底层优化,但 dtype 处理复杂
6. query + mergedf1.query('index not in @df2.merge(df1, left_index=True, right_index=True, how="inner").index')O(n log n)⭐⭐⭐利用索引,但需索引对齐
7. dask.dataframedask_df1[~dask_df1.map_partitions(lambda x: x.apply(tuple,1)).isin(dask_df2.map_partitions(lambda x: x.apply(tuple,1)))]O(n/p)✅✅⭐⭐⭐⭐⭐分布式,但需集群

生产环境首选方案详解
我在线上风控系统中,对 800 万行用户行为日志做A−B(A 是当日活跃用户,B 是已封禁用户),采用方法 1(merge + indicator):

# 关键优化:提前设置索引加速 merge df1_indexed = df1.set_index('user_id') df2_indexed = df2.set_index('user_id') result = df1_indexed.merge(df2_indexed, left_index=True, right_index=True, indicator=True, how='left').query('_merge=="left_only"').drop('_merge', axis=1)

加索引后,merge从 12 秒降至 1.8 秒。query比布尔索引['_merge']=='left_only'快 30%,因它编译为 numexpr 表达式。

实操心得:永远用df1.merge(df2, indicator=True, how='left')代替df1[~df1.isin(df2)]。前者是数据库级的关联算法,后者是 Python 级的暴力循环。当数据量超过 10 万行,性能差距就是分钟级 vs 秒级。

3.4 对称差集(Symmetric Difference):被忽略但高频的第五种操作

集合论中,对称差集 A⊕B = (A−B) ∪ (B−A),即“只在 A 或只在 B 中的元素”。Pandas 没有直接方法,但它在数据质量监控中极其高频:

  • 新旧版本配置表对比,找出所有变更项(新增+删除)
  • AB 测试分流日志,验证分流逻辑是否准确(只在 A 组或只在 B 组的用户)
  • 主备数据库同步校验,定位不一致的记录

实现方式有两种:

方案一:两次 merge + concat(推荐)

def symmetric_diff(df1, df2, on=None): if on is None: on = df1.columns.intersection(df2.columns).tolist() left_only = df1.merge(df2, on=on, indicator=True, how='left').query('_merge=="left_only"').drop('_merge', axis=1) right_only = df2.merge(df1, on=on, indicator=True, how='left').query('_merge=="left_only"').drop('_merge', axis=1) # 添加来源标识便于调试 left_only['_source'] = 'df1_only' right_only['_source'] = 'df2_only' return pd.concat([left_only, right_only], ignore_index=True) # 使用 diff_report = symmetric_diff(df1, df2) print(f"Total differences: {len(diff_report)}") print(diff_report['_source'].value_counts())

方案二:基于哈希的集合运算(超大数据)

def hash_symmetric_diff(df1, df2): def get_row_hashes(df): # 用 pickle 序列化避免类型问题 import pickle return set(pickle.dumps(row.to_dict()) for _, row in df.iterrows()) set1 = get_row_hashes(df1) set2 = get_row_hashes(df2) diff_hashes = set1.symmetric_difference(set2) # 重建 DataFrame(需保存原始 df 引用) all_rows = list(df1.iterrows()) + list(df2.iterrows()) result_rows = [row for _, row in all_rows if pickle.dumps(row.to_dict()) in diff_hashes] return pd.DataFrame(result_rows)

方案一更易读、易 debug,方案二在单机内存不足时可用。我建议始终用方案一,除非 profiling 显示它成为瓶颈。

4. 生产环境避坑指南:那些文档不会写的 12 个血泪教训

4.1 字段类型不一致引发的“幽灵差异”

去年双十一大促期间,我们的订单对账系统突然报警:df_ordersdf_payments的交集行数每天少 3-5 行。排查三天,发现df_orders['order_id']int64df_payments['order_id']object(因某些 ID 含字母前缀)。merge时,int6412345str'12345'被视为不同元素。

根治方案

# 在所有 merge/concat 前,强制类型对齐 def align_dtypes(df1, df2, key_cols): for col in key_cols: if col in df1.columns and col in df2.columns: # 优先转为字符串(最安全) if df1[col].dtype != df2[col].dtype: df1[col] = df1[col].astype(str) df2[col] = df2[col].astype(str) return df1, df2 df1_aligned, df2_aligned = align_dtypes(df1, df2, ['order_id'])

教训:永远不要假设上游数据类型一致。在 ETL 流程入口处,用df.dtypes打印类型报告,将其纳入 CI/CD 检查。

4.2 索引陷阱:merge 后索引爆炸的隐形杀手

df1有默认RangeIndexdf2DatetimeIndexdf1.merge(df2, on='date')后,结果索引是df1的索引,但df2的日期索引信息丢失。更糟的是,若df2索引有重复值,merge会生成笛卡尔积。

安全实践

# merge 前重置索引,避免意外 df1_reset = df1.reset_index(drop=True) df2_reset = df2.reset_index(drop=True) result = df1_reset.merge(df2_reset, on='date') # 若需保留索引,显式指定 df1_with_idx = df1.reset_index().rename(columns={'index': 'orig_idx1'}) df2_with_idx = df2.reset_index().rename(columns={'index': 'orig_idx2'}) result = df1_with_idx.merge(df2_with_idx, on='date')

4.3 空值(NaN)的七种处理哲学

NaN 是集合操作的头号敌人。以下是针对不同场景的处理策略:

场景NaN 含义处理方式代码示例
主键缺失记录不完整,应剔除dropna(subset=['key_col'])df1_clean = df1.dropna(subset=['product_id'])
数值缺省0 值更合理fillna(0)df1['price'] = df1['price'].fillna(0)
分类缺省单独类别 "Unknown"fillna('Unknown')df1['category'] = df1['category'].fillna('Unknown')
时间缺省用业务基准时间fillna(pd.Timestamp('1970-01-01'))df1['created_at'] = df1['created_at'].fillna(pd.Timestamp('1970-01-01'))
字符串缺省用空字符串fillna('')df1['desc'] = df1['desc'].fillna('')
需要保留 NaNNaN 本身是有效状态indicator=Truedf1.merge(df2, indicator=True, how='left')
跨表 NaN 比较统一占位符replace({np.nan: 'NULL'})df1_rep = df1.replace({np.nan: 'NULL'})

关键原则:NaN 处理必须在merge/concat之前完成。在drop_duplicates()后处理 NaN 是无效的,因为重复行可能已被错误合并。

4.4 性能调优的五个黄金参数

当集合操作变慢,别急着换工具,先检查这五个参数:

  1. sort=Falseconcat([df1,df2], sort=False)省去列名排序,提速 15-20%
  2. ignore_index=True:避免索引对齐开销,尤其当索引无业务意义时
  3. copy=Falsedf1.copy(deep=False)减少内存复制(需确认不修改原 df)
  4. validate参数merge(..., validate='one_to_one')在运行时校验关系,避免静默错误
  5. suffixes预设merge(..., suffixes=('_left', '_right'))避免运行时动态生成列名
# 优化后的高性能 merge result = df1.merge( df2, on=['price', 'quantity'], how='inner', validate='m:1', # df1 多对 df2 一 suffixes=('_src1', '_src2'), indicator=False )

4.5 调试差集的三步诊断法

A−B结果为空或过多,按此流程排查:

第一步:检查 B 是否为空

print(f"df2 shape: {df2.shape}") print(f"df2 is empty: {df2.empty}") if df2.empty: print("Warning: df2 is empty, A-B equals A")

第二步:抽样验证一行

# 取 df1 第一行,手动检查是否在 df2 中 sample_row = df1.iloc[0] print("Sample row from df1:") print(sample_row) # 转为元组并检查 sample_tuple = tuple(sample_row) print(f"In df2? {sample_tuple in set(df2.apply(tuple, axis=1))}")

第三步:用 indicator 查看全貌

debug_merge = df1.merge(df2, indicator=True, how='left') print(debug_merge['_merge'].value_counts()) # 若 'both' 为 0,说明无交集,A-B 应等于 A # 若 'left_only' 为 0,说明 A 全在 B 中

这套方法让我在 2 分钟内定位了 90% 的差集问题。

4.6 单元测试模板:为集合操作编写可信赖的测试

集合操作必须有测试,否则线上事故概率极高。以下是我团队的最小可行测试模板:

import pandas as pd import numpy as np def test_union_operation(): # 构造确定性测试数据 df1 = pd.DataFrame({'a': [1, 2], 'b': [3, 4]}) df2 = pd.DataFrame({'a': [2, 3], 'b': [4, 5]}) # 预期结果:{1,3}, {2,4}, {3,5} -> 3 行 expected_rows = 3 # 执行操作 result = pd.concat([df1, df2], ignore_index=True).drop_duplicates() # 断言 assert len(result) == expected_rows, f"Union failed: got {len(result)}, expected {expected_rows}" assert set(result['a']) == {1, 2, 3}, "Column 'a' values incorrect" assert result.dtypes['a'] == np.int64, "Data type not preserved" # 运行测试 test_union_operation() print("✅ Union test passed")

测试要点

  • 用小数据、确定性数据,避免随机性
  • 断言行数、关键列值、数据类型三要素
  • 将测试加入 pytest,每次 PR 自动运行

5. 从集合思维到数据工程:如何构建可复用的 DataFrame 操作库

5.1 封装核心操作为高阶函数

把集合操作封装成函数,是提升团队效率的关键。以下是我在多个项目中验证过的dataframe_set_ops.py核心:

import pandas as pd import numpy as np def union(df1, df2, on=None, keep='first'): """ DataFrame 并集:A ∪ B :param df1, df2: 输入 DataFrame :param on: 匹配列名列表,None 则用所有同名列 :param keep: 'first'/'last',决定重复行保留策略 :return: 并集 DataFrame """ if on is None: on = df1.columns.intersection(df2.columns).tolist() # 标准化列 df1_std = df1[on].copy() df2_std = df2[on].copy() # 处理 NaN for col in on: if df1_std[col].isna().any() or df2_std[col].isna().any(): placeholder = f"NULL_{col}" df1_std[col] = df1_std[col].fillna(placeholder) df2_std[col] = df2_std[col].fillna(placeholder) # 合并并去重 combined = pd.concat([df1_std, df2_std], ignore_index=True) if keep == 'first': return combined.drop_duplicates(subset=on, keep='first') else: return combined.drop_duplicates(subset=on, keep='last') def difference(df1, df2, on=None): """ DataFrame 差集:A - B :param df1, df2: 输入 DataFrame :param on: 匹配列名列表 :return: A 中有但 B 中没有的行 """ if on is None: on = df1.columns.intersection(df2.columns).tolist() # 使用 merge indicator,最稳定 merged = df1.merge(df2[on], on=on, indicator=True, how='left') return merged[merged['_merge'] == 'left_only'].drop('_merge', axis=1) # 使用示例 # result = union(df_orders_q1, df_orders_q2) # anomalies = difference(df_logs, df_processed)

5.2 构建数据质量看