【问题标题】:Python vectorized operation involving data from a previous row涉及前一行数据的 Python 矢量化操作
【发布时间】:2019-01-06 18:13:18
【问题描述】:

我非常了解如何利用 pandas 和 numpy 对整列数据进行矢量化操作。但是,我遇到了一种我似乎无法矢量化的情况。当计算涉及利用前一行的值来计算当前行时,我必须退回到 for 循环。

这种东西可以矢量化吗?这是我的意思的一个简单示例:

# Test set of 20 random integers
df = pd.DataFrame({'base': [15, 16, 2, 16, 14,
                            1, 18, 18, 4, 7,
                            4, 18, 19, 13, 16,
                            11, 1, 8, 1, 9]})


# Empty array to hold calculated values
calc_data = np.empty((20, 1))

period = 14

for idx, value in enumerate(df.base):

    # Seeding the first element of the calculated array
    if idx == 0:
        calc_data[idx] = 5

    else:
        calc_data[idx] = (calc_data[idx - 1] * (period - 1) + df.base.iloc[idx]) / period

# Adding the column to the dataframe
df['calculated'] = calc_data

print(df)

输出:

    base  calculated
0     15    5.000000
1     16    5.785714
2      2    5.515306
3     16    6.264213
4     14    6.816769
5      1    6.401286
6     18    7.229765
7     18    7.999068
8      4    7.713420
9      7    7.662461
10     4    7.400857
11    18    8.157939
12    19    8.932372
13    13    9.222916
14    16    9.706994
15    11    9.799351
16     1    9.170826
17     8    9.087196
18     1    8.509539
19     9    8.544572

【问题讨论】:

  • 我不认为你可以矢量化它
  • 我认为这是不可能的。矢量化的整个想法是一次对整个系列/数据帧应用操作。如果一行的值取决于前一行的计算值,则不可能同时执行 2+ 个。
  • 谢谢佩德罗,我担心可能是这种情况,这也是我对矢量化的理解。我希望我遗漏了一些明显的东西。
  • 您唯一的机会是将ufuncaccumulate 方法一起使用,例如cumsum

标签: python pandas numpy vectorization


【解决方案1】:

一种矢量化方式(将“矢量化”视为“避免 Python 级循环”)是将其视为linear signal filter

import numpy as np
import pandas as pd
import scipy.signal

def via_lfilter(arr):
    period = 14
    y0 = 5.0  # initial value

    # calc_data[idx] = (calc_data[idx - 1] * (period - 1) + df.base.iloc[idx]) / period
    b = [1.0/period]  # coefficients of 'original' terms
    a = [1.0, -(period-1)/period]  # coefficients of 'computed' terms

    zi = scipy.signal.lfiltic(b, a, [y0], x=arr[1::-1])

    y = np.zeros_like(arr)
    y[0] = y0
    result = scipy.signal.lfilter(b, a, arr[1:], axis=0, zi=zi)
    y[1:] = result[0]

    return y

但在现实世界中,我只会使用 numba,它旨在为我们提供矢量化的性能优势而不会让人头疼:

import numba

@numba.jit(nopython=True)
def via_numba(arr):
    calc_data = np.zeros_like(arr)
    period = 14
    calc_data[0] = 5.0  # initial value
    for idx in range(1, len(arr)):
        calc_data[idx] = (calc_data[idx - 1] * (period - 1) + arr[idx]) / period
    return calc_data

这些给我:

In [238]: df["vect"] = via_lfilter(df.base.values.astype(float))
     ...: df["via_numba"] = via_numba(df.base.values.astype(float))
     ...: 
     ...: 

In [239]: df
Out[239]: 
    base  calculated      vect  via_numba
0     15    5.000000  5.000000   5.000000
1     16    5.785714  5.785714   5.785714
2      2    5.515306  5.515306   5.515306
3     16    6.264213  6.264213   6.264213
4     14    6.816769  6.816769   6.816769
5      1    6.401286  6.401286   6.401286
6     18    7.229765  7.229765   7.229765
7     18    7.999068  7.999068   7.999068
8      4    7.713420  7.713420   7.713420
9      7    7.662461  7.662461   7.662461
10     4    7.400857  7.400857   7.400857
11    18    8.157939  8.157939   8.157939
12    19    8.932372  8.932372   8.932372
13    13    9.222916  9.222916   9.222916
14    16    9.706994  9.706994   9.706994
15    11    9.799351  9.799351   9.799351
16     1    9.170826  9.170826   9.170826
17     8    9.087196  9.087196   9.087196
18     1    8.509539  8.509539   8.509539
19     9    8.544572  8.544572   8.544572

两者在较大的帧中表现合理:

In [240]: df = pd.DataFrame({"base": np.random.uniform(1, 100, 10**6)})

In [241]: %timeit via_lfilter(df.base.values.astype(float))
11.4 ms ± 49.5 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

In [242]: %timeit via_numba(df.base.values.astype(float))
11 ms ± 342 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

【讨论】:

    【解决方案2】:

    tldr:

    以下是矢量化的,因为所有使用的操作都是 pandas & numpy 层的数组操作。

    X = ((period-1)/period) ** np.arange(len(df)) / period
    a = df.base.copy()
    a.loc[0] = 5*period
    df['calculated'] = a.expanding().apply(lambda x: np.sum(x * X[:len(x)][::-1]), raw=True)
    

    解释:

    可以通过提取递归的顺序性质来构建快速解决方案。

    即请注意,结果的每个元素都遵循一定的模式:

    0: 5
    1: 5 (13/14) + 16 (1/14)
    2: 5 (13 / 14)^2 + 16 (13 / 14^2) + 2 (1/14)
    ...
    

    如果第一个元素乘以14,那么我们可以将上面的表示为

    0: sum{(1/14)*[70]}
    1: sum{(1/14)*[70(13/14), 16]}
    2: sum{(1/14)*[70(13/14)^2, 16(13/14), 2]}
    ...
    

    如果我们从df.base 中删除元素,我们会得到可以相加的系列:

    0: (1/14) * [1]
    1: (1/14) * [(13/14), 1]
    2: (1/14) * [(13/14)^2, (13/14), 1]
    ...
    

    上面的这个系列序列可以得到以下的反转切片:

    X = ((period-1)/period) ** np.arange(len(df)) / period
    

    还要注意df.base 的第一个值没有用于calculated 的构造。取而代之的是(5*period = 70)

    所以,第 n 个结果是修改后的 df.base 的扩展系列乘以 X 的适当切片的总和

    a = df.base.copy()
    a.loc[0] = 5*period
    df['calculated'] = a.expanding().apply(lambda x: np.sum(x * X[:len(x)][::-1]), raw=True)
    # df outputs:
        base  calculated
    0     15    5.000000
    1     16    5.785714
    2      2    5.515306
    3     16    6.264213
    4     14    6.816769
    5      1    6.401286
    6     18    7.229765
    7     18    7.999068
    8      4    7.713420
    9      7    7.662461
    10     4    7.400857
    11    18    8.157939
    12    19    8.932372
    13    13    9.222916
    14    16    9.706994
    15    11    9.799351
    16     1    9.170826
    17     8    9.087196
    18     1    8.509539
    19     9    8.544572
    

    【讨论】:

      【解决方案3】:

      您可以使用 shift() 方法访问 n 个位置的移位值,

      这应该会让你的任务更容易

      df.value.shift(1) + df.value
      

      【讨论】:

      • 感谢您的快速回复。如果“计算”中的值列已经存在,我已经考虑过 shift 可以很好地工作。但是,在这种情况下,它会即时计算“计算”的每一行,然后在下一行的操作中使用前一行的值。除非我遗漏了一些东西,否则我看不到 shift 会如何缓解这个问题。
      猜你喜欢
      • 1970-01-01
      • 2012-10-11
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-08-03
      相关资源
      最近更新 更多