使用日期执行此操作的更快(矢量化)方法

2024-06-26 00:23:53 发布

您现在位置:Python中文网/ 问答频道 /正文

我正在构建一个时间序列,试图找到一种更有效的方法来实现这一点——理想的矢量化。 熊猫应用列表理解步骤非常慢(在大数据集上)

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给出了一个警告:

PerformanceWarning: Adding/subtracting array of DateOffsets to DatetimeArray not vectorized

我试着按照一个答案来构建一些东西,但正如前面提到的,模块化算术需要做很多调整来处理边缘情况,而且还没有弄清楚(calendar monthrange的表现并不好)。 此函数不运行:

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)

Tags: dfdatetimedaterangecolyearpdinterval
2条回答

半矢量化方式

正如我在下面所说的,我不认为有一种纯粹的矢量化方法可以将变量和一般的DateOffset添加到TimestampSeries中@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']

这里有一个选择。您正在添加月份,因此我们实际上可以通过以矢量化方式处理整数来计算新年/月/日,然后根据这些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

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

以下是我的计时(10公里加速约23倍):

%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秒

相关问题 更多 >