使用apply()和DateOffset给Pandas日期列加月份的性能警告解决
我有一个Pandas DataFrame,其中包含存储datetime类型值的"date"列,以及存储整数类型值的"months"列,该列代表需要为每行日期添加的月份数。我需要计算每行的新日期,即原"date"加上对应"months"列指定的月份数。
我使用了以下代码:
df['date'] + df["months"].apply(lambda y: pd.DateOffset(months=y))
这段代码能得到正确结果,但触发了一条PerformanceWarning,内容如下:
PerformanceWarning: Adding/subtracting object-dtype array to DatetimeArray not vectorized warnings.warn
我的数据集需要处理多组日期和月份列,当前代码执行耗时较长。
输入样例
| date | months | |
|---|---|---|
| 0 | 2020-01-01 | 3 |
| 1 | 2021-06-15 | 6 |
| 2 | 2022-02-28 | 1 |
输出样例
| date | months | new_date | |
|---|---|---|---|
| 0 | 2020-01-01 | 3 | 2020-04-01 |
| 1 | 2021-06-15 | 6 | 2021-12-15 |
| 2 | 2022-02-28 | 1 | 2022-03-28 |
请问该警告的含义是什么?如何优化代码以避免该警告并提升执行效率?
一、警告含义解释
这个警告的核心是:你当前的操作是把object类型的数组(apply返回的是包含pd.DateOffset对象的Series,属于object dtype)和DatetimeArray类型的日期列做加法,Pandas无法对这种操作进行向量化优化,只能逐行处理,这就导致了性能低下,尤其是在处理大数据集时会明显变慢。
apply(lambda y: pd.DateOffset(months=y))会生成一个每个元素都是独立DateOffset对象的Series,本质是Python对象的集合,不是Pandas能高效处理的数值型数组,所以加法操作无法利用向量化的优势,只能循环执行。
二、优化方案
方案1:使用dateutil.relativedelta(边界处理精准)
直接对datetime对象添加可变月份,避免生成DateOffset对象数组:
from dateutil.relativedelta import relativedelta df['new_date'] = df.apply(lambda row: row['date'] + relativedelta(months=row['months']), axis=1)
这种方法逻辑直观,能准确处理月末、跨年等边界日期,性能比原方法有所提升。
方案2:向量化日期拆分计算(性能最优)
通过拆分日期的年、月字段,用数值运算直接计算新日期,完全利用Pandas向量化能力,无Python级循环:
import numpy as np # 拆分原日期的年、月、日 years = df['date'].dt.year months = df['date'].dt.month days = df['date'].dt.day # 计算新的年和月 new_total_months = years * 12 + months + df['months'] new_years = new_total_months // 12 new_months = new_total_months % 12 # 处理月份为0的特殊情况(如1月减1个月变为去年12月) new_years = np.where(new_months == 0, new_years - 1, new_years) new_months = np.where(new_months == 0, 12, new_months) # 重新组合日期,自动处理月末边界 df['new_date'] = pd.to_datetime({ 'year': new_years, 'month': new_months, 'day': days }, errors='coerce')
这种方法是性能最高的方案,不会触发性能警告,适合处理超大数据集。
方案3:周期转换法(代码简洁)
先将日期转换为月度周期,完成月份加减后再转回日期:
# 转换为月度周期,添加月份后转回日期 df['new_date'] = df['date'].dt.to_period('M').add(df['months']).dt.to_timestamp() # 修正月末日期(如2022-02-28加1个月默认变为2022-03-01,需调整回原日) df['new_date'] = df.apply( lambda row: row['new_date'].replace(day=row['date'].day) if row['date'].day == row['date'].dt.days_in_month.iloc[0] else row['new_date'], axis=1 )
代码简洁易读,适合普通场景,仅需额外处理月末日期的特殊情况。
内容的提问来源于stack exchange,提问作者Cristian Jara

