更快(矢量化)的方式来使用日期做这个熊猫公式



我正在构建一个时间序列,试图获得一种更有效的方法 - 理想情况下是矢量化。 熊猫应用列表理解步骤非常慢(在大数据集上)。

import datetime
import pandas as pd
# Dummy data:
todays_date = datetime.datetime.now().date()
xdates = pd.date_range(todays_date-datetime.timedelta(10), periods=4, freq='D')
categories = list(2*'A') + list(2*'B')
d = {'xdate': xdates, 'periods': [8]*2 + [2]*2, 'interval': [3]*2 + [12]*2}
df = pd.DataFrame(d,index=categories)
# This step is slow:
df['sdates'] = df.apply(lambda x: [x.xdate + pd.DateOffset(months=k*x.interval) for k in range(x.periods)], axis=1)
# This step is quite quick, but shown here for completeness
df = df.explode('sdates')

也许是这样的:

df['sdates'] = [df.xdate + df.periods * [df.interval.astype('timedelta64[M]')]]

但是语法不太正确。 此代码

df = pd.DataFrame(d,index=categories)
df['m_offsets'] = df.interval.apply(lambda x: list(range(0, 72, x)))
df = df.explode('m_offsets')
df['sdate'] = df.xdate + df.m_offsets * pd.DateOffset(months=1)

我认为类似于其中一个答案,但最后一步,pd。DateOffset 给出警告:

性能警告:将日期偏移量数组添加/减去到日期时间数组未矢量化

我尝试按照一个答案构建一些东西,但如前所述,模块化算术需要大量调整以处理边缘情况,并且还没有弄清楚(日历月范围玩得不好)。 此函数不会运行:

from calendar import monthrange
def add_months(df, date_col, n_col):
""" Adds ncol months do date_col """
z = df.copy()
# calculate new year/month/day and convert to datetime
z['year'] = (z[date_col].dt.year * 12 + (z[date_col].dt.month-1) + z[n_col]) // 12
z['month'] = ((z[date_col].dt.month + z[n_col] - 1) % 12) + 1
x,x = monthrange(z.year, z.month)
z['days_in_month'] = monthrange(z.year, z.month)
z['target_day'] = z[date_col].dt.day
# z['day'] = min(z.target_day, z.days_in_month)
z['day'] = z.days_in_month
z['sdates'] = pd.to_datetime(z[['year', 'month', 'day']])
return z['sdates']

目前,这有效,但日期偏移量是一个非常沉重的步骤。

df = pd.DataFrame(d,index=categories)
df['m_offsets'] = df.interval.apply(lambda x: list(range(0, 72, x)))
df = df.explode('m_offsets')
df['sdates'] = df.apply(lambda x: x.xdate + pd.DateOffset(months=x.m_offsets), axis=1)

这里有一个选项。您正在添加月份,因此我们实际上可以通过仅以矢量化方式处理整数来计算新的年/月/日,然后从这些 y/m/d 组合中创建日期时间:

def f_proposed(df):
z = df.copy()
z = z.reset_index()
# repeat xdate as many times as the number of periods
z = z.loc[np.repeat(z.index, z['periods'])]

# calculate k number of months to add
z['k'] = z.groupby(level=0).cumcount() * z['interval']

# calculate new year/month/day and convert to datetime
z['year'] = (z['xdate'].dt.year * 12 + z['xdate'].dt.month - 1 + z['k']) // 12
z['month'] = (z['xdate'].dt.month - 1 + z['k']) % 12 + 1

# clip day to days_in_month
z['days_in_month'] = pd.to_datetime(
z['year'].astype(str)+'-'+z['month'].astype(str)+'-01').dt.days_in_month
z['day'] = np.clip(z['xdate'].dt.day, 0, z['days_in_month'])

z['sdates'] = pd.to_datetime(z[['year', 'month', 'day']])

# drop temporary columns
z = z.set_index('index').drop(columns=['k', 'year', 'month', 'day', 'days_in_month'])
return z

为了将性能与原始数据进行比较,我生成了一个包含 10,000 行的测试数据集。

这是我的时间(23K 的 ~10 倍加速):

%timeit f_proposed(z)
82.7 ms ± 222 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
%timeit f_original(z)
1.92 s ± 2.75 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

附言对于 170K,我的机器上的f_proposed大约需要 1.39 秒,f_original大约需要 33.6 秒

半矢量化方式

正如我在下面所说,我认为没有一种纯粹的矢量化方法可以将变量和一般DateOffset添加到Timestamps 的Series中。 @perl解决方案适用于DateOffset是 1 个月的精确倍数的情况下。

现在,添加单个常量DateOffset矢量化的,因此我们可以使用以下方法。它利用了这样一个事实,即日期偏移量有一组有限的不同值。它也相对较快,并且对于任何DateOffset和日期都是正确的:

n = df['periods'].values
period_no = np.repeat(n - n.cumsum(), n) + np.arange(n.sum())
z = pd.DataFrame(
np.repeat(df.reset_index().values, repeats=n, axis=0),
columns=df.reset_index().columns,
).set_index('index')
z = z.assign(madd=period_no * z['interval'])
z['sdates'] = z['xdate']
for madd in set(z['madd'].unique()):
z.loc[z['madd'] == madd, 'sdates'] += pd.DateOffset(months=madd)

定时:

# modified large dummy data:
N = 170_000
todays_date = datetime.datetime.now().date()
xdates = pd.date_range(todays_date-datetime.timedelta(10), periods=N, freq='H')
categories = np.random.choice(list('ABCDE'), N)
d = {'xdate': xdates, 'periods': np.random.randint(1,10,N), 'interval': np.random.randint(1,12,N)}
df = pd.DataFrame(d,index=categories)
%%time (the above)
CPU times: user 3.49 s, sys: 13.5 ms, total: 3.51 s
Wall time: 3.51 s

(注意:对于使用上述生成的 10K 行,我看到的时间为 ~240ms,但当然这取决于数据中有多少不同的月份偏移量)。

示例结果(如上所述,对于一次 170K 行的绘制):

>>> z.tail()
xdate periods interval madd              sdates
index                                                              
B     2040-08-25 06:00:00       8        8   48 2044-08-25 06:00:00
B     2040-08-25 06:00:00       8        8   56 2045-04-25 06:00:00
D     2040-08-25 07:00:00       3        2    0 2040-08-25 07:00:00
D     2040-08-25 07:00:00       3        2    2 2040-10-25 07:00:00
D     2040-08-25 07:00:00       3        2    4 2040-12-25 07:00:00

对初始答案的更正

我站正了:我原来的答案也没有矢量化。第一部分是分解数据帧并构建要添加的月数,这是矢量化的,并且非常快。但第二部分,增加可变月数的DateOffset,则不是。

我希望我错了,但我认为目前没有办法以矢量化的方式完成第二部分。

直接日期部分操作(例如month = (month - 1 + n_months) % 12 + 1等)在极端情况下必然会失败(例如'2021-02-31')。 除了复制DateOffset中使用的逻辑,这在某些情况下是行不通的。

初步答案

这是一种矢量化方法:

n = df.periods.values
period_no = np.repeat(n - n.cumsum(), n) + np.arange(n.sum())
z = pd.DataFrame(
np.repeat(df.reset_index().values, repeats=n, axis=0),
columns=df.reset_index().columns,
).set_index('index').assign(period_no=period_no)
z['sdates'] = z['period_no']  * z['interval'] * pd.DateOffset(months=1) + z['xdate']

最新更新