Pandas高级合并:基于列表(对象列)子集关系的DataFrame连接

23次阅读

Pandas 高级合并:基于列表(对象列)子集关系的 DataFrame 连接

本文探讨了如何在 pandas 中实现基于 对象 列(包含列表或 numpy 数组)的复杂 dataframe 合并操作。当合并条件涉及一个 dataframe 的列表型列中的所有元素必须是另一个 dataframe 的列表型列的子集时,传统的 `merge` 方法不再适用。文章提供了一种迭代遍历、应用集合子集判断并拼接结果的解决方案,并详细展示了其实现代码和注意事项,尤其强调了在 大数据 集下的性能考量。

Pandas DataFrame 对象列的复杂合并策略

在数据处理中,我们经常需要合并(merge)两个或多个 Pandas DataFrame。通常情况下,合并操作基于共享的列值,例如使用 pd.merge()函数。然而,当合并条件变得复杂,特别是涉及到列中存储的是列表(或 NumPy 数组)这类“对象类型”数据,并且合并的逻辑是基于一个列表是否为另一个列表的“子集”关系时,标准的合并方法就无法直接应用。

本教程将详细介绍如何处理这类特定场景:给定两个 DataFrame,df1 包含详细的日期时间信息和一组描述符列表(specifiers),df2 包含更通用的描述符列表和对应的值。我们的目标是将 df2 的每一行合并到 df1 中,条件是 df2 行中的所有描述符必须作为子集存在于 df1 行的描述符列表中。

问题场景描述

假设我们有两个 DataFrame,结构如下:

df1 (详细数据): 包含 datetime、value 和 specifiers 列。specifiers 列是对象类型,每行是一个列表,例如 [‘P1’, ‘WEEKDAY’, ‘TUESDAY’],表示日期时间的特定属性。

datetime  value              specifiers 0   2021-06-01 00:00:00  11.30  [P1, WEEKDAY, TUESDAY] 1   2021-06-01 00:30:00   9.00  [P2, WEEKDAY, TUESDAY] ……

df2 (合并源数据): 包含 specifiers 和 value 列。specifiers 列同样是对象类型,每行也是一个列表,但可能包含更少或更通用的描述符,例如 [‘P1’] 或 [‘P4’, ‘WEEKDAY’]。

specifiers    value 0                [P1]     0.43 1                [P2]     0.41 …… 95995  [WEEKEND, P46]     1.67

我们的目标是:对于 df2 中的每一行,找到 df1 中所有其 specifiers 列包含 df2 行中所有 specifiers 的行,并将它们合并起来。例如,df2 中 specifiers 为 [‘P1’] 的行应该与 df1 中所有包含 ‘P1’ 的 specifiers 列表的行合并。

解决方案:迭代、筛选与拼接

由于 Pandas 的内置 merge 函数不支持这种基于列表子集关系的复杂条件,我们需要采用一种迭代式的方法。核心思路是:

  1. 遍历 df2 的每一行。
  2. 对于 df2 的当前行,提取其 specifiers 列表。
  3. 使用这个 specifiers 列表作为条件,筛选 df1 中所有满足子集关系的行。
  4. 将 df2 的当前行(重复多次)与筛选出的 df1 行进行横向拼接。
  5. 将所有这些小块的拼接结果累积起来,形成最终的合并 DataFrame。

详细实现步骤与代码示例

以下是实现上述逻辑的 python 代码:

import pandas as pd import numpy as np  # 1. 准备示例数据 # df1:模拟包含详细描述符的 DataFrame df1_data = {'datetime': pd.to_datetime(['2021-06-01 00:00:00', '2021-06-01 00:30:00',                                 '2021-06-01 01:00:00', '2021-06-01 01:30:00',                                 '2021-06-01 02:00:00', '2021-06-01 02:30:00']),     'value': [11.30, 9.00, 10.40, 8.50, 9.70, 12.00],     'specifiers': [['P1', 'WEEKDAY', 'TUESDAY'],                    ['P2', 'WEEKDAY', 'TUESDAY'],                    ['P3', 'WEEKDAY', 'TUESDAY'],                    ['P4', 'WEEKDAY', 'TUESDAY'],                    ['P5', 'WEEKDAY', 'TUESDAY'],                    ['P6', 'WEEKEND', 'SATURDAY']] # 增加一个周末的示例 } df1 = pd.DataFrame(df1_data)  # df2:模拟包含合并条件的 DataFrame df2_data = {'specifiers': [['P1'], ['P2'], ['P3'], ['P4', 'WEEKDAY'], ['P5', 'TUESDAY'], ['WEEKEND', 'P6']],     'values_from_df2': [0.43, 0.51, 0.62, 0.73, 0.84, 0.99] } df2 = pd.DataFrame(df2_data)  print("--- df1 原始数据 ---") print(df1) print("n--- df2 原始数据 ---") print(df2)  # 2. 执行合并操作 merged_df = pd.DataFrame() # 初始化一个空的 DataFrame 来存储结果  # 遍历 df2 的每一行。itertuples()比 iterrows()更高效,因为它返回命名元组。for row_df2 in df2.itertuples(index=False):     # row_df2.specifiers 是 df2 当前行的 specifiers 列表     # 将其转换为集合以便进行高效的子集判断     df2_specifiers_set = set(row_df2.specifiers)      # 筛选 df1 中满足条件的行:# df1['specifiers'].apply(……) 对 df1 的 specifiers 列的每个元素应用一个函数     # Lambda x: df2_specifiers_set.issubset(set(x)) 检查 df2 的 specifiers 集合是否是 df1 当前行 specifiers 集合的子集     matching_rows_df1 = df1[df1['specifiers'].apply(lambda x: df2_specifiers_set.issubset(set(x))     )]      # 如果找到了匹配的行     if not matching_rows_df1.empty:         # 创建一个与 matching_rows_df1 行数相同的 DataFrame,每行都是 df2 的当前行数据         # 这样做是为了在横向拼接时,df2 的数据能与 df1 的匹配数据一一对应         df2_row_repeated = pd.DataFrame([row_df2] * len(matching_rows_df1))          # 横向拼接 df2 的当前行数据和 df1 的匹配行数据         # reset_index(drop=True) 确保索引重置,避免拼接时因索引不匹配导致的问题         combined_row_data = pd.concat([df2_row_repeated, matching_rows_df1.reset_index(drop=True)], axis=1)          # 将当前拼接结果添加到最终的 merged_df 中         merged_df = pd.concat([merged_df, combined_row_data], ignore_index=True)  print("n--- 合并后的 DataFrame ---") print(merged_df)

代码输出示例

--- df1 原始数据 ---              datetime  value             specifiers 0 2021-06-01 00:00:00  11.30  [P1, WEEKDAY, TUESDAY] 1 2021-06-01 00:30:00   9.00  [P2, WEEKDAY, TUESDAY] 2 2021-06-01 01:00:00  10.40  [P3, WEEKDAY, TUESDAY] 3 2021-06-01 01:30:00   8.50  [P4, WEEKDAY, TUESDAY] 4 2021-06-01 02:00:00   9.70  [P5, WEEKDAY, TUESDAY] 5 2021-06-01 02:30:00  12.00  [P6, WEEKEND, SATURDAY]  --- df2 原始数据 ---         specifiers  values_from_df2 0             [P1]             0.43 1             [P2]             0.51 2             [P3]             0.62 3    [P4, WEEKDAY]             0.73 4    [P5, TUESDAY]             0.84 5  [WEEKEND, P6]             0.99  --- 合并后的 DataFrame ---         specifiers  values_from_df2            datetime  value             specifiers 0             [P1]             0.43 2021-06-01 00:00:00  11.30  [P1, WEEKDAY, TUESDAY] 1             [P2]             0.51 2021-06-01 00:30:00   9.00  [P2, WEEKDAY, TUESDAY] 2             [P3]             0.62 2021-06-01 01:00:00  10.40  [P3, WEEKDAY, TUESDAY] 3    [P4, WEEKDAY]             0.73 2021-06-01 01:30:00   8.50  [P4, WEEKDAY, TUESDAY] 4    [P5, TUESDAY]             0.84 2021-06-01 02:00:00   9.70  [P5, WEEKDAY, TUESDAY] 5  [WEEKEND, P6]             0.99 2021-06-01 02:30:00  12.00  [P6, WEEKEND, SATURDAY]

关键概念解析

  1. df2.itertuples(index=False):

    Pandas 高级合并:基于列表(对象列)子集关系的 DataFrame 连接

    序列猴子开放平台

    具有长序列、多模态、单模型、大数据 等特点的超大规模语言模型

    Pandas 高级合并:基于列表(对象列)子集关系的 DataFrame 连接 0

    查看详情 Pandas 高级合并:基于列表(对象列)子集关系的 DataFrame 连接

    • itertuples()是一种高效遍历 DataFrame 行的方法,它将每行转换为一个命名元组(namedtuple)。相比 iterrows(),它通常具有更好的性能,尤其是在处理大量数据时。
    • index=False 参数表示在生成的元组中不包含行索引,使 数据访问 更简洁。
  2. set(list_a).issubset(set(list_b)):

    • 这是实现子集判断的核心。将列表转换为集合(set)是进行集合操作(如子集、交集、并集等)的常用且高效的方法。
    • issubset()方法用于检查一个集合是否是另一个集合的子集。在这里,我们检查 df2 当前行的 specifiers 集合是否是 df1 某行 specifiers 集合的子集。
  3. df1[‘specifiers’].apply(lambda x: …):

    • apply()方法用于对 DataFrame 或 Series 的每个元素或每行 / 列应用一个函数。
    • lambda x: … 是一个匿名函数,x 代表 df1[‘specifiers’]列中的每一个列表元素。这个函数将每个列表转换为集合,然后执行子集判断。
    • apply()返回一个 布尔型Series,用于筛选 df1 中满足条件的行。
  4. pd.concat([…], axis=1):

    • pd.concat()用于沿着特定轴(行或列)连接 Pandas 对象。
    • axis= 1 表示按列(横向)拼接。
    • 我们首先通过 pd.DataFrame([row_df2] * len(matching_rows_df1))将 df2 的当前行数据重复 matching_rows_df1 的行数次,这样可以确保在横向拼接时,df2 的单行数据能与 df1 的多个匹配行一一对应。
    • matching_rows_df1.reset_index(drop=True):在拼接前重置 df1 匹配行的索引,避免因索引不一致导致的问题。drop=True 表示不将旧索引作为新列。
    • 最终,通过反复调用 merged_df = pd.concat([merged_df, combined_row_data], ignore_index=True)将每次迭代的结果累积到 merged_df 中。ignore_index=True 确保最终 DataFrame 的索引是连续的。

性能考量

虽然上述迭代方法能够准确解决问题,但其性能在大规模数据集上可能会成为瓶颈。具体来说:

  • df2.itertuples(): 遍历 DataFrame 行本身就比 Pandas 的矢量化操作慢。
  • df1[‘specifiers’].apply(lambda x: …): apply 方法虽然比纯 Python循环 快,但对于每一行都要执行集合转换和子集判断,这仍然是一个行级操作,效率低于完全矢量化的 Pandas 操作。
  • 重复 pd.concat(): 在循环内部反复将结果拼接到一个不断增长的 DataFrame 上,会导致频繁的数据复制和内存重新分配,效率较低。更优的做法是先将所有中间结果存储在一个列表中,最后一次性 concat。

对于原始问题中 df1 有 623 行,df2 有 95999 行的数据规模,这种方法在合理的时间内完成计算是可接受的。然而,如果数据集规模更大(例如,df1 和 df2 都有数百万行),则需要考虑更高级的优化技术,例如:

  • 将列表列转换为可哈希的 字符串 表示:如果列表元素顺序不重要且是固定的,可以将其排序后转换为字符串,然后使用字符串匹配或哈希表进行更快的查找。
  • 利用稀疏矩阵或倒排索引:如果 specifiers 中的元素种类很多,可以构建一个倒排索引,将每个 specifier 映射到包含它的 df1 行索引,从而加速查找过程。
  • 使用 Cython 或 Numba:对于性能要求极高的场景,可以将核心的循环和判断逻辑用 Cython 或 Numba 进行 JIT 编译,以接近 c 语言 的性能。

总结

当 Pandas 的传统 merge 功能无法满足基于列表(对象列)的复杂合并条件时,通过迭代、结合集合操作(issubset)和 apply()进行筛选,再利用 pd.concat()拼接结果,是一种灵活有效的解决方案。虽然这种方法在性能上可能不如完全矢量化的操作,但对于中等规模的数据集而言,其可读性和实现难度相对较低,能够满足业务需求。在处理大规模数据时,应额外关注 性能瓶颈 并考虑进一步的优化策略。

站长
版权声明:本站原创文章,由 站长 2025-11-05发表,共计5663字。
转载说明:除特殊说明外本站文章皆由CC-4.0协议发布,转载请注明出处。
1a44ec70fbfb7ca70432d56d3e5ef742
text=ZqhQzanResources