如何在 Pandas 中 add/subtract 一组不同的月份时间增量?
How to add/subtract an array of varying Month timedeltas in Pandas?
我想从日期时间列中减去一些月数。每行都有不同的月数要减去。例如,
df = pd.DataFrame({
'timestamp': pd.date_range('2017-10-01', '2018-10-01', freq='m'),
'delta_in_months': [1, 4, 2, 5, 1, 3, 1, 5, 2, 4, 1, 3]
})
结果应该是这样的(日期四舍五入无关,可以是 01 或 28/29/30/31,输入 01 更容易),
timestamp delta_in_months new_timestamp
0 2017-10-31 1 2017-09-01
1 2017-11-30 4 2017-07-01
2 2017-12-31 2 2017-10-01
3 2018-01-31 5 2017-08-01
4 2018-02-28 1 2018-01-01
5 2018-03-31 3 2017-12-01
6 2018-04-30 1 2018-03-01
7 2018-05-31 5 2017-12-01
8 2018-06-30 2 2018-04-01
9 2018-07-31 4 2018-03-01
10 2018-08-31 1 2018-07-01
11 2018-09-30 3 2018-06-01
请记住,这将适用于更大的数据框。
我试过了,
months_delta = df.delta_in_months.apply(pd.tseries.offsets.MonthOffset)
df['new_timestamp'] = df.timestamp - months_delta
但这给出了非常意想不到的结果,每行条目都是一个 DatetimeIndex。
你可以试试
df['new_timestamp'] = df.timestamp - pd.to_timedelta(df.delta_in_months,unit='M')
df['new_timestamp'] = df['new_timestamp'].dt.date
一件重要的事情是要记住 1 'M' = 30 'D'.
如果您来到这里是因为您正在寻找一个 向量化、快速和正确的 解决方案来解决将可变月数添加到 Series
的问题Timestamps
,然后继续阅读。
在某些问题中,我们确实想要添加实际月份(pd.offsets.DateOffset(months=x)
的工作方式),即:2021-01-31 + 1 month --> 2021-02-28
,而不仅仅是“30 天”。但是尝试直接使用 pd.offsets.DateOffset
会引发警告 (PerformanceWarning: Adding/subtracting object-dtype array to DatetimeArray not vectorized
)。例如:
dates + df['months'].apply(lambda m: pd.offsets.DateOffset(months=m))
dates + months * pd.offsets.DateOffset(months=1)
,这在某些情况下也是错误的(例如 2015-07-29 + 59 months
应该是 2020-06-29
,而不是 2020-06-28
)。
相反,我们可以自己做一些算术运算并获得向量化的解决方案:
# note: not timezone-aware
def vadd_months(dates, months):
ddt = dates.dt
m = ddt.month - 1 + months
mb = pd.to_datetime(pd.DataFrame({
'year': ddt.year + m // 12,
'month': (m % 12) + 1,
'day': 1})) + (dates - dates.dt.normalize())
me = mb + pd.offsets.MonthEnd()
r = mb + (ddt.day - 1) * pd.Timedelta(days=1)
r = np.minimum(r, me)
return r
OP 示例的用法
df['new_timestamp'] = vadd_months(df['timestamp'], df['delta_in_months'])
速度
n = int(100_000)
df = pd.DataFrame({
'timestamp': pd.Series(pd.to_datetime(np.random.randint(
pd.Timestamp('2000').value,
pd.Timestamp('2020').value,
n
))).dt.floor('1s'),
'months': np.random.randint(0, 120, n),
})
%%time
newts = vadd_months(df['timestamp'], df['months'])
# CPU times: user 52.3 ms, sys: 4.01 ms, total: 56.3 ms
验证
检查(非向量化)直接使用 pd.offsets.DateOffset
:
import warnings
%%time
with warnings.catch_warnings():
warnings.simplefilter(action='ignore', category=pd.errors.PerformanceWarning)
check = df['timestamp'] + df['months'].apply(lambda m: pd.offsets.DateOffset(months=m))
# CPU times: user 2.41 s, sys: 43.9 ms, total: 2.45 s
>>> newts.equals(check)
True
请注意 vadd_months
比非矢量化版本快 40 倍,并且没有要捕获的警告。
我想从日期时间列中减去一些月数。每行都有不同的月数要减去。例如,
df = pd.DataFrame({
'timestamp': pd.date_range('2017-10-01', '2018-10-01', freq='m'),
'delta_in_months': [1, 4, 2, 5, 1, 3, 1, 5, 2, 4, 1, 3]
})
结果应该是这样的(日期四舍五入无关,可以是 01 或 28/29/30/31,输入 01 更容易),
timestamp delta_in_months new_timestamp
0 2017-10-31 1 2017-09-01
1 2017-11-30 4 2017-07-01
2 2017-12-31 2 2017-10-01
3 2018-01-31 5 2017-08-01
4 2018-02-28 1 2018-01-01
5 2018-03-31 3 2017-12-01
6 2018-04-30 1 2018-03-01
7 2018-05-31 5 2017-12-01
8 2018-06-30 2 2018-04-01
9 2018-07-31 4 2018-03-01
10 2018-08-31 1 2018-07-01
11 2018-09-30 3 2018-06-01
请记住,这将适用于更大的数据框。
我试过了,
months_delta = df.delta_in_months.apply(pd.tseries.offsets.MonthOffset)
df['new_timestamp'] = df.timestamp - months_delta
但这给出了非常意想不到的结果,每行条目都是一个 DatetimeIndex。
你可以试试
df['new_timestamp'] = df.timestamp - pd.to_timedelta(df.delta_in_months,unit='M')
df['new_timestamp'] = df['new_timestamp'].dt.date
一件重要的事情是要记住 1 'M' = 30 'D'.
如果您来到这里是因为您正在寻找一个 向量化、快速和正确的 解决方案来解决将可变月数添加到 Series
的问题Timestamps
,然后继续阅读。
在某些问题中,我们确实想要添加实际月份(pd.offsets.DateOffset(months=x)
的工作方式),即:2021-01-31 + 1 month --> 2021-02-28
,而不仅仅是“30 天”。但是尝试直接使用 pd.offsets.DateOffset
会引发警告 (PerformanceWarning: Adding/subtracting object-dtype array to DatetimeArray not vectorized
)。例如:
dates + df['months'].apply(lambda m: pd.offsets.DateOffset(months=m))
dates + months * pd.offsets.DateOffset(months=1)
,这在某些情况下也是错误的(例如2015-07-29 + 59 months
应该是2020-06-29
,而不是2020-06-28
)。
相反,我们可以自己做一些算术运算并获得向量化的解决方案:
# note: not timezone-aware
def vadd_months(dates, months):
ddt = dates.dt
m = ddt.month - 1 + months
mb = pd.to_datetime(pd.DataFrame({
'year': ddt.year + m // 12,
'month': (m % 12) + 1,
'day': 1})) + (dates - dates.dt.normalize())
me = mb + pd.offsets.MonthEnd()
r = mb + (ddt.day - 1) * pd.Timedelta(days=1)
r = np.minimum(r, me)
return r
OP 示例的用法
df['new_timestamp'] = vadd_months(df['timestamp'], df['delta_in_months'])
速度
n = int(100_000)
df = pd.DataFrame({
'timestamp': pd.Series(pd.to_datetime(np.random.randint(
pd.Timestamp('2000').value,
pd.Timestamp('2020').value,
n
))).dt.floor('1s'),
'months': np.random.randint(0, 120, n),
})
%%time
newts = vadd_months(df['timestamp'], df['months'])
# CPU times: user 52.3 ms, sys: 4.01 ms, total: 56.3 ms
验证
检查(非向量化)直接使用 pd.offsets.DateOffset
:
import warnings
%%time
with warnings.catch_warnings():
warnings.simplefilter(action='ignore', category=pd.errors.PerformanceWarning)
check = df['timestamp'] + df['months'].apply(lambda m: pd.offsets.DateOffset(months=m))
# CPU times: user 2.41 s, sys: 43.9 ms, total: 2.45 s
>>> newts.equals(check)
True
请注意 vadd_months
比非矢量化版本快 40 倍,并且没有要捕获的警告。