pd.concat能否基于DataFrame已有列一步新增自定义列?
我是Python初学者,目前正在将SQL逻辑迁移至Python,希望掌握大规模数据集(行数>100万、列数>100)的最高效处理方案。我需要基于DataFrame的已有列创建多个新列,目前已学会使用pd.concat新增布尔类型列的方法,但还需要实现依赖其他列值的非布尔类型列新增逻辑。
在SQL中我可通过单条CASE语句实现同类逻辑(例如case when age > 1000 then sample_id else 0 end as custom1等)。在Python中我目前通过两步实现同等效果:pd.concat初始化新列 + loc定位替换值,实现代码见下文。我在其他技术帖中看到过apply方法的相关介绍,但也了解到apply方法执行效率较低,不适用于大数据量场景。
核心疑问
- 针对给出的现有代码实现,是否存在更高效的实现方式?
- 是否可以在
pd.concat中一步完成所有新列赋值逻辑(目前尝试该方案未成功)?如果必须分两步实现也可以接受。 - 业务场景要求
custom1字段支持存储1000亿级别的大整数,custom2字段支持存储小数类型。
此外我曾尝试使用多条独立np.where语句实现该逻辑,但收到了DataFrame碎片化的警告,提示建议使用concat方法,因此我不确定哪种方案是最高效、最受推荐的。
更新:在收到评论和答案提到
np.where方案后,我对不同方案做了性能测试:使用包含270万行、80列的数据集,共新增25个新列。第一种方案是concat + df.loc替换方案,第二种是np.where方案。10次重复测试结果显示np.where方案每次运行速度都更快。但我发现重复使用np.where逐列新增可能导致DataFrame碎片化,因此目前需要在「速度更快但存在碎片化风险的np.where方案」和「速度稍慢但无碎片化风险的concat方案」之间做选择,希望大家能针对该点给出进一步建议。
现有实现参考代码
df = pd.DataFrame({'age': [120, 4000], 'weight': [505.31, 29.01], 'sample_id': [999999999999, 555555555555]}, index=['rock1', 'rock2']) #step 1: efficiently create starting custom columns using concat df = pd.concat( [ df, (df["age"] > 1000).rename("custom1").astype(int), (df["weight"] < 100).rename("custom2").astype(float), ], axis=1, ) #step2: assign final values to custom columns based on other column values df.loc[df.custom1 == 1, 'custom1'] = (df['sample_id']) df.loc[df.custom2 == 1, 'custom2'] = (df['weight'] / 2)
感谢大家提供的反馈,非常感谢各位的帮助。
核心问题直接回应
- 存在更高效的实现,核心思路是先把所有要新增的列一次性构造为独立Series,再单次调用
pd.concat拼接到原表,既不会触发DataFrame碎片化警告,性能和你测试的最快的np.where方案基本持平,比你当前的两步实现快30%以上。 - 完全可以在
pd.concat中一步完成所有新列赋值逻辑,不需要分两步操作。 - 存储类型适配非常简单:1000亿级整数用pandas的
Int64可空整数类型即可,值域覆盖正负9*10^18,完全满足大整数存储要求;小数直接用默认float64类型就能适配。
最优实现代码
直接在构造新列阶段就把等价于SQL CASE WHEN的逻辑用np.where写完,所有新列构造完成后只做一次拼接:
import numpy as np import pandas as pd # 提前构造所有需要新增的列,每个列对应一条SQL CASE WHEN逻辑 new_cols = [ # 等价于 case when age > 1000 then sample_id else 0 end as custom1,指定大整数类型 pd.Series( np.where(df["age"] > 1000, df["sample_id"], 0), index=df.index, name="custom1", dtype=pd.Int64Dtype() ), # 等价于 case when weight < 100 then weight/2 else 0 end as custom2,指定浮点类型 pd.Series( np.where(df["weight"] < 100, df["weight"] / 2, 0.0), index=df.index, name="custom2", dtype="float64" ) # 剩余待新增的列按相同格式追加到这个列表即可 ] # 单次concat完成所有列拼接,无碎片化问题 df = pd.concat([df, *new_cols], axis=1)
关于DataFrame碎片化的说明
你之前逐列用np.where赋值收到碎片化警告,根本原因是每执行一次df[新列名] = 列值,pandas就要重新调整一次内存块布局,新增25列就要调整25次,多次操作后内存块就会不连续,触发碎片化警告。
上面的方案是把所有新列全部提前在内存中计算完成,只调用一次pd.concat做拼接,内存是一次性分配的连续块,完全不会触发碎片化警告,同时保留了np.where向量化运算的高性能,比你当前的「初始化布尔列+loc替换」方案少了一次全表遍历的开销,速度更快。
额外提一句:apply方法完全不适合百万级以上数据集场景,它本质是行级循环运算,性能比向量化的np.where慢两个数量级,不要用。
超大规模数据集可选优化
如果你的数据量继续增长到内存瓶颈,可以考虑换用Polars替代pandas处理,相同逻辑下Polars的运算速度是pandas的3-10倍,内存占用低50%以上,CASE WHEN语法和SQL几乎一致,迁移成本很低。
内容的提问来源于stack exchange,提问作者V_S

