Обновление фрейма данных pandas на основе следующего значения

#python #pandas #dataframe

#python #pandas #фрейм данных

Вопрос:

Я использую фрейм данных pandas и применяю тренд к самой ранней точке данных, чтобы попытаться как можно лучше заполнить недостающие исторические данные. Я знаю, что перебор фрейма данных pandas неверен, но я не нашел альтернативного способа сделать это, поскольку новое значение зависит от следующего значения. Если кто-нибудь знает лучший способ добиться этого, это было бы здорово!

Пример df:

    Week no  Data  Trend
0        1   0.0    1.5
1        2   0.0    1.5
2        3   0.0    1.0
3        4   0.0    0.5
4        5  10.0    0.6
  

Код, который я сейчас использую:

 for wk in range(len(df)-1, 0, -1):
       if (df.loc[wk, 'Data'] != 0 and df.loc[wk-1, 'Data'] == 0
               and not math.isnan(df.loc[wk, 'Trend'])):
           df.loc[wk-1, 'Data'] = (df.loc[wk, 'Data']
                                          *df.loc[wk, 'Trend'])
  

Результат:

   Week no  Data  Trend
0        1   4.5    1.5
1        2   3.0    1.5
2        3   3.0    1.0
3        4   6.0    0.5
4        5  10.0    0.6
  

Ответ №1:

Рекурсивные вычисления не векторизуются, для повышения производительности используется numba:

 from numba import jit

@jit(nopython=True)
def f(a, b):
    for i in range(a.shape[0]-1, 0, -1):
        if (a[i] != 0) and (a[i-1] == 0) and not np.isnan(b[i]):
            a[i-1] = a[i] * b[i]
    return a

df['Data'] = f(df['Data'].to_numpy(), df['Trend'].to_numpy())
print (df)

   Week no  Data  Trend
0        1   4.5    1.5
1        2   3.0    1.5
2        3   3.0    1.0
3        4   6.0    0.5
4        5  10.0    0.6
  

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

 df = pd.concat([df] * 40, ignore_index=True)
print (df)
     Week  no  Data  Trend
0       0   1   4.5    1.5
1       1   2   3.0    1.5
2       2   3   3.0    1.0
3       3   4   6.0    0.5
4       4   5  10.0    0.6
..    ...  ..   ...    ...
195     0   1   4.5    1.5
196     1   2   3.0    1.5
197     2   3   3.0    1.0
198     3   4   6.0    0.5
199     4   5  10.0    0.6

[200 rows x 4 columns]
  

 In [114]: %%timeit
     ...: df['Data'] = f(df['Data'].to_numpy(), df['Trend'].to_numpy())
     ...: 
     ...: 
121 µs ± 2.08 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
  

 df = pd.concat([df] * 40, ignore_index=True)

print (df.shape)
(200, 4)


In [115]: %%timeit
     ...: for wk in range(len(df)-1, 0, -1):
     ...:         if (df.loc[wk, 'Data'] != 0 and df.loc[wk-1, 'Data'] == 0
     ...:                 and not math.isnan(df.loc[wk, 'Trend'])):
     ...:             df.loc[wk-1, 'Data'] = (df.loc[wk, 'Data']
     ...:                                           *df.loc[wk, 'Trend'])
     ...:                                           
3.3 ms ± 414 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
  

Я тестирую с 2 * 40 пропущенными значениями, и производительность аналогична:

 print (df)
   Week  no  Data  Trend
0     0   1   0.0    NaN
1     1   2   0.0    NaN
2     2   3   0.0    1.0
3     3   4   0.0    0.5
4     4   5  10.0    0.6


df = pd.concat([df] * 40, ignore_index=True)

print (df.shape)
(200, 4)

   
In [117]: %%timeit
     ...: df['Data'] = f(df['Data'].to_numpy(), df['Trend'].to_numpy())
     ...: 
119 µs ± 480 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)
  

 df = pd.concat([df] * 40, ignore_index=True)

print (df.shape)
(200, 4)


In [121]: %%timeit
     ...: for wk in range(len(df)-1, 0, -1):
     ...:         if (df.loc[wk, 'Data'] != 0 and df.loc[wk-1, 'Data'] == 0
     ...:                 and not math.isnan(df.loc[wk, 'Trend'])):
     ...:             df.loc[wk-1, 'Data'] = (df.loc[wk, 'Data']
     ...:                                           *df.loc[wk, 'Trend'])
     ...:                                           
3.12 ms ± 10.9 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
  

Комментарии:

1. Существует ли определенный размер фрейма данных, чтобы этот метод был быстрее? Я только что попробовал это против своего метода в примере фрейма данных, выполняя каждые 100 раз, и ваш метод занял ~ 15 секунд, в то время как мой занял ~ 0,2 (базовая синхронизация с использованием модуля time, так что ничего особенного!) Мне интересно, работает ли ваш метод быстрее только на больших фреймах данных? Для моих целей мне нужно будет сделать это максимум для 200 строк.

2. @EmiOB — хм, не уверен, в чем проблема, но для меня это работает хорошо — numba быстрее, чем ваше решение, ответ был отредактирован.

3. Мой плохой! Я определял функцию внутри цикла, теперь исправил, и это быстрее. Спасибо за вашу помощь!