Python - Векторизованная разница дат в таблице строк на миллион - PullRequest
0 голосов
/ 09 июня 2018

У меня есть следующий файл данных pandas:

Date                    
2018-04-10 21:05:00        
2018-04-10 21:05:00        
2018-04-10 21:10:00        
2018-04-10 21:15:00     
2018-04-10 21:35:00     

Моя цель - вычислить количество строк, которые составляют 20 минут до и 20 минут после каждого времени (включая строки с одинаковым временем как до, так и после).Примерно так:

Date                   nr_20_min_bef    nr_20_min_after   
2018-04-10 21:05:00          2                 4                                 
2018-04-10 21:05:00          2                 4  
2018-04-10 21:10:00          3                 2
2018-04-10 21:15:00          4                 2
2018-04-10 21:35:00          2                 1

Я попытался выполнить цикл for для итерации по всем строкам, проблема в том, что во всей серии более миллиона строк, поэтому я искал более эффективное решение,Мой текущий подход заключается в использовании функций pandas:

import datetime
import pandas

df = pd.DataFrame(pd.to_datetime(['2018-04-10 21:05:00',        
'2018-04-10 21:05:00',        
'2018-04-10 21:10:00',        
'2018-04-10 21:15:00',     
'2018-04-10 21:35:00']),columns = ['Date'])

nr_20_min_bef = []
nr_20_min_after = []

for i in range(0, len(df)):
    nr_20_min_bef.append(df.Date.between(df.Date[i] - 
pd.offsets.DateOffset(minutes=20), df.Date[i], inclusive = True).sum())
    nr_20_min_after.append(df.Date.between(df.Date[i], df.Date[i] + 
pd.offsets.DateOffset(minutes=20), inclusive = True).sum())

Вероятно, векторное решение было бы идеальным для этого случая, однако я не очень знаю, как это сделать.

Заранее спасибо.

Ответы [ 2 ]

0 голосов
/ 09 июня 2018

Хорошей новостью является то, что это можно векторизовать.Плохая новость ... это не совсем просто.

Вот эталонный тест perfplot код:

import numpy as np
import pandas as pd
import perfplot

def orig(df):
    nr_20_min_bef = []
    nr_20_min_after = []

    for i in range(0, len(df)):
        nr_20_min_bef.append(df.Date.between(
            df.Date[i] - pd.offsets.DateOffset(minutes=20), df.Date[i], inclusive = True).sum())
        nr_20_min_after.append(df.Date.between(
            df.Date[i], df.Date[i] + pd.offsets.DateOffset(minutes=20), inclusive = True).sum())
    df['nr_20_min_bef'] = nr_20_min_bef
    df['nr_20_min_after'] = nr_20_min_after
    return df

def alt(df):
    df = df.copy()
    df['Date'] = pd.to_datetime(df['Date'])
    df['num'] = 1
    df = df.set_index('Date')

    dup_count = df.groupby(level=0)['num'].count()
    result = dup_count.rolling('20T', closed='both').sum()
    df['nr_20_min_bef'] = result.astype(int)

    max_date = df.index.max()
    min_date = df.index.min()
    dup_count_reversed = df.groupby((max_date - df.index)[::-1] + min_date)['num'].count()
    result = dup_count_reversed.rolling('20T', closed='both').sum()
    result = pd.Series(result.values[::-1], dup_count.index)
    df['nr_20_min_after'] = result.astype(int)
    df = df.drop('num', axis=1)
    df = df.reset_index()
    return df

def make_df(N):
    dates = (np.array(['2018-04-10'], dtype='M8[m]') 
             + (np.random.randint(10, size=N).cumsum()).astype('<i8').astype('<m8[m]'))
    df = pd.DataFrame({'Date': dates})
    return df

def check(df1, df2):
    return df1.equals(df2)

perfplot.show(
    setup=make_df,
    kernels=[orig, alt],
    n_range=[2**k for k in range(4,10)],
    logx=True,
    logy=True,
    xlabel='N',
    equality_check=check)

, который показывает, что alt значительно быстрее, чем orig: enter image description here

В дополнение к бенчмаркингу orig и alt, perfplot.show также проверяет, что фреймы данных, возвращаемые orig и alt, равны.Учитывая сложность alt, это, по крайней мере, дает нам некоторую уверенность в том, что он ведет себя так же, как и orig.

Немного сложно создать перфлоплот для большого N, поскольку orig начинает принимать довольнодолгое время и каждый тест повторяется сотни раз.Итак, вот несколько сравнений %timeit для больших N:

| N     | orig (ms) | alt (ms) |
|-------+-----------+----------|
| 2**10 |      3040 |     9.32 |
| 2**12 |     12600 |     10.8 |
| 2**20 |         ? |      909 |

In [300]: df = make_df(2**10)
In [301]: %timeit orig(df)
1 loop, best of 3: 3.04 s per loop
In [302]: %timeit alt(df)
100 loops, best of 3: 9.32 ms per loop
In [303]: df = make_df(2**12)
In [304]: %timeit orig(df)
1 loop, best of 3: 12.6 s per loop
In [305]: %timeit alt(df)
100 loops, best of 3: 10.8 ms per loop
In [306]: df = make_df(2**20)
In [307]: %timeit alt(df)
1 loop, best of 3: 909 ms per loop

Что теперь делает alt?Возможно, проще всего взглянуть на небольшой пример, используя опубликованную вами df:

df = pd.DataFrame(pd.to_datetime(['2018-04-10 21:05:00',        
                                  '2018-04-10 21:05:00',        
                                  '2018-04-10 21:10:00',        
                                  '2018-04-10 21:15:00',     
                                  '2018-04-10 21:35:00']),columns = ['Date'])

Основная идея - использовать Series.rolling для выполнения скользящей суммы.Когда у Series есть DatetimeIndex, Series.rolling может принять частоту времени для размера окна.Таким образом, мы можем вычислить скользящие суммы с переменными окнами фиксированного промежутка времени.Поэтому сначала необходимо сделать даты DatetimeIndex:

df['Date'] = pd.to_datetime(df['Date'])
df['num'] = 1
df = df.set_index('Date')

Поскольку df имеет повторяющиеся даты, сгруппируйте их по значениям DatetimeIndex и посчитайте количество дубликатов:

dup_count = df.groupby(level=0)['num'].count()
# Date
# 2018-04-10 21:05:00    2
# 2018-04-10 21:10:00    1
# 2018-04-10 21:15:00    1
# 2018-04-10 21:35:00    1
# Name: num, dtype: int64

Теперь вычисляем скользящую сумму на dup_count:

result = dup_count.rolling('20T', closed='both').sum()
# Date
# 2018-04-10 21:05:00    2.0
# 2018-04-10 21:10:00    3.0
# 2018-04-10 21:15:00    4.0
# 2018-04-10 21:35:00    2.0
# Name: num, dtype: float64

Виола, это nr_20_min_bef.20T определяет размер окна длиной 20 минут.closed='both' указывает, что каждое окно включает в себя как левую, так и правую конечные точки.

Теперь, если бы только вычисления nr_20_min_after были такими же простыми.Теоретически все, что нам нужно сделать, - это изменить порядок строк в dup_count и вычислить другую скользящую сумму.К сожалению, Series.rolling требует, чтобы DatetimeIndex монотонно увеличивался :

In [275]: dup_count[::-1].rolling('20T', closed='both').sum()
ValueError: index must be monotonic

Поскольку очевидный путь заблокирован, мы берем объезд:

max_date = df.index.max()
min_date = df.index.min()
dup_count_reversed = df.groupby((max_date - df.index)[::-1] + min_date)['num'].count()
# Date
# 2018-04-10 21:05:00    1
# 2018-04-10 21:25:00    1
# 2018-04-10 21:30:00    1
# 2018-04-10 21:35:00    2
# Name: num, dtype: int64

Thisгенерирует новый псевдо datetime DatetimeIndex для группировки по:

In [288]: (max_date - df.index)[::-1] + min_date
Out[288]: 
DatetimeIndex(['2018-04-10 21:05:00', '2018-04-10 21:25:00',
               '2018-04-10 21:30:00', '2018-04-10 21:35:00',
               '2018-04-10 21:35:00'],
              dtype='datetime64[ns]', name='Date', freq=None)

Эти значения могут быть не в df.index - но это нормально.Единственное, что нам нужно, это то, что значения монотонно растут и что разница между датами и временем соответствует разнице в df.index при обращении.

Теперь, используя это обращенное dup_count, мы можемнаслаждайтесь большим выигрышем (в исполнении), взяв скользящую сумму:

result = dup_count_reversed.rolling('20T', closed='both').sum()
# Date
# 2018-04-10 21:05:00    1.0
# 2018-04-10 21:25:00    2.0
# 2018-04-10 21:30:00    2.0
# 2018-04-10 21:35:00    4.0
# Name: num, dtype: float64

result имеет значения, которые мы хотим получить nr_20_min_after, но в обратном порядке и с неверным индексом.Вот как мы можем исправить это:

result = pd.Series(result.values[::-1], dup_count.index)
# Date
# 2018-04-10 21:05:00    4.0
# 2018-04-10 21:10:00    2.0
# 2018-04-10 21:15:00    2.0
# 2018-04-10 21:35:00    1.0
# dtype: float64

И это в основном все, что нужно alt.

0 голосов
/ 09 июня 2018

Я думаю, что вы можете использовать apply, даже если это не векторизованный способ, он должен быть быстрее, чем с циклом for, таким как:

#first create the timedelta of 20 minutes
dt_20 = pd.Timedelta(minutes=20)
# then apply on the first column
df['nr_20_min_bef'] = df['Date'].apply(lambda x: df['Date'][((x - dt_20) <= df['Date'] ) 
                                                            & (x >=df['Date'])].count())

df['nr_20_min_after'] = df['Date'].apply(lambda x: df['Date'][(x <= df['Date'] )& 
                                                              ((x + dt_20) >= df['Date'])].count())

После выполнения %timeit, этоКажется, что использование between метода немного быстрее, чем с mask, поэтому вы можете сделать

df['nr_20_min_bef'] = df['Date'].apply(lambda x: df.Date.between(x - dt_20, 
                                                                 x, inclusive = True).sum())

и idem для after.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...