【问题标题】:How do you apply a function on a dataframe column using data from previous rows?如何使用前几行的数据在数据框列上应用函数?
【发布时间】:2020-09-21 23:49:23
【问题描述】:

我有一个包含三列的数据框:nums 有一些值可以使用,b 始终是 10result 列,除了在第一行(因为我们必须有一个初始值才能使用)。 数据框如下所示:

   nums   b    result
0  20.0  1    20.0
1  22.0  0    0
2  30.0  1    0
3  29.1  1    0
4  20.0  0    0
...

问题

我想从第二行开始遍历数据框中的每一行,进行一些计算并将结果存储在result 列中。由于我正在处理大文件,因此我需要一种方法来快速执行此操作,这就是为什么我想要apply 之类的东西。

我想做的计算是从 previous 行中获取 numsresult 中的值,如果在 current 行中,则 @ 987654332@ col 是0 然后我想(例如)从前一行添加numresult。例如,如果该行中的b1,我想减去它们。

我尝试了什么?

我尝试使用apply,但我无法访问上一行,可悲的是,如果我设法访问上一行,数据框直到最后才会更新结果列。

我也尝试过使用这样的循环,但对于我正在使用的大型文件来说它太慢了:

       for i in range(1, len(df.index)):
            row = df.index[i]
            new_row = df.index[i - 1]  # get index of previous row for "nums" and "result"
            df.loc[row, 'result'] = some_calc_func(prev_result=df.loc[new_row, 'result'], prev_num=df.loc[new_row, 'nums'], \
                                     current_b=df.loc[row, 'b'])

some_calc_func 看起来像这样(只是一个一般示例):

def some_calc_func(prev_result, prev_num, current_b):
    if current_b == 1:
        return prev_result * prev_num / 2
    else:
        return prev_num + 17

请回复some_calc_func

【问题讨论】:

  • “我需要一种方法来加快这个操作,所以这就是为什么我想要 apply 之类的东西” 注意:When should I ever want to use pandas apply() in my code
  • 不一定要应用,我只想要快速完成所描述操作的东西
  • 明白,只是想让您知道apply 不是您想要速度时应该首先寻找的东西。

标签: python pandas dataframe vectorization apply


【解决方案1】:

IIUC:

>>> df['result'] = (df[df.result.eq(0)].b.replace({0: 1, 1: -1}) * df.nums
                    ).fillna(df.result).cumsum()

>>> df
   nums  b  result
0  20.0  1    20.0
1  22.0  0    42.0
2  30.0  1    12.0
3  29.1  1   -17.1
4  20.0  0     2.9

解释:

# replace 0 with 1 and 1 with -1 in column `b` for rows where result==0
>>> df[df.result.eq(0)].b.replace({0: 1, 1: -1})
1    1
2   -1
3   -1
4    1
Name: b, dtype: int64

# multiply with nums
>>> (df[df.result.eq(0)].b.replace({0: 1, 1: -1}) * df.nums)
0     NaN
1    22.0
2   -30.0
3   -29.1
4    20.0
dtype: float64

# fill the 'NaN' with the corresponding value from df.result (which is 20 here)
>>> (df[df.result.eq(0)].b.replace({0: 1, 1: -1}) * df.nums).fillna(df.result)
0    20.0
1    22.0
2   -30.0
3   -29.1
4    20.0
dtype: float64

# take the cumulative sum (cumsum)
>>> (df[df.result.eq(0)].b.replace({0: 1, 1: -1}) * df.nums).fillna(df.result).cumsum()
0    20.0
1    42.0
2    12.0
3   -17.1
4     2.9
dtype: float64

根据你在cmets中的要求,我想不出没有循环的办法:

c1, c2 = 2, 1
l = [df.loc[0, 'result']]            # store the first result in a list

# then loop over the series (df.b * df.nums)

for i, val in (df.b * df.nums).iteritems():
    if i:                            # except for 0th index
        if val == 0:                 # (df.b * df.nums) == 0 if df.b == 0
            l.append(l[-1])          # append the last result
        else:                        # otherwise apply the rule
            t = l[-1] *c2 + val * c1
            l.append(t)

>>> l
[20.0, 20.0, 80.0, 138.2, 138.2]

>>> df['result'] = l

   nums  b  result
0  20.0  1    20.0
1  22.0  0    20.0
2  30.0  1    80.0   # [ 20 * 1 +   30 * 2]
3  29.1  1   138.2   # [ 80 * 1 + 29.1 * 2]
4  20.0  0   138.2

似乎足够快,没有测试过大样本。

【讨论】:

  • 好的,请稍等
  • 非常感谢!我将使用我的函数版本而不是加法/减法来测试它:)
  • 为迟到的答案道歉。我对此进行了多次测试,如果我还想将行中的值乘以某个常数值,则无法重新创建您所做的事情。让c1, c2 成为两个常数。如果我想在b==0 的情况下将result 中的新行设置为上一行的result,并且在b==1 的情况下设置@987654329 中的新行,我应该如何编写代码@ 是:(c1nums 来自同一行)+(c2result 来自上一行)?
  • 想不出没有循环的办法。
【解决方案2】:

重新使用循环和 some_calc_func

我正在使用您的循环并将其减少到最低限度,如下所示

   for i in range(1, len(df)):
      df.loc[i, 'result'] = some_calc_func(df.loc[i, 'b'], df.loc[i - 1, 'result'], df.loc[i, 'nums'])

some_calc_func 实现如下

def some_calc_func(bval, prev_result, curr_num):
    if bval == 0:
        return prev_result + curr_num
    else:
        return prev_result - curr_num

结果如下

   nums  b  result
0  20.0  1    20.0
1  22.0  0    42.0
2  30.0  1    12.0
3  29.1  1   -17.1
4  20.0  0     2.9

【讨论】:

  • 你好。首先,我的意思是some_calc_func 将与我写的一样(尽管在您的解决方案中它没有任何区别,所以我只是指出)。其次,这与我的方法相似,只是我使用的是日期,所以我不能简单地使用 i-1
【解决方案3】:

如果您想保留函数 some_calc_func 并且不使用另一个库,则不应尝试在每次迭代时访问每个元素,您可以在 nums 和 b 列上使用 zip 并在两者之间进行转换尝试从前一行访问 nums 并在每次迭代时将 prev_res 保存在内存中。此外,append 分配给列表而不是数据框,并在循环之后将列表分配给列。

prev_res = df.loc[0, 'result'] #get first result
l_res = [prev_res] #initialize the list of results
# loop with zip to get both values at same time, 
# use loc to start b at second row but not num
for prev_num, curren_b in zip(df['nums'], df.loc[1:, 'b']):
    # use your function to calculate the new prev_res
    prev_res = some_calc_func (prev_res, prev_num, curren_b)
    # add to the list of results
    l_res.append(prev_res)
# assign to the column
df['result'] = l_res
print (df) #same result than with your method
   nums  b  result
0  20.0  1    20.0
1  22.0  0    37.0
2  30.0  1   407.0
3  29.1  1  6105.0
4  20.0  0    46.1

现在有了 5000 行的数据框 df,我得到了:

%%timeit
prev_res = df.loc[0, 'result']
l_res = [prev_res]
for prev_num, curren_b in zip(df['nums'], df.loc[1:, 'b']):
    prev_res = some_calc_func (prev_res, prev_num, curren_b)
    l_res.append(prev_res)
df['result'] = l_res
# 4.42 ms ± 695 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

使用您原来的解决方案,速度慢了 ~750 倍

%%timeit 
for i in range(1, len(df.index)):
    row = df.index[i]
    new_row = df.index[i - 1]  # get index of previous row for "nums" and "result"
    df.loc[row, 'result'] = some_calc_func(prev_result=df.loc[new_row, 'result'], prev_num=df.loc[new_row, 'nums'], \
                             current_b=df.loc[row, 'b'])
#3.25 s ± 392 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

使用另一个名为 numba 的库进行编辑,如果函数 some_calc_func 可以轻松地与 Numba 装饰器一起使用。

from numba import jit
# decorate your function
@jit
def some_calc_func(prev_result, prev_num, current_b):
    if current_b == 1:
        return prev_result * prev_num / 2
    else:
        return prev_num + 17

# create a function to do your job
# numba likes numpy arrays
@jit
def with_numba(prev_res, arr_nums, arr_b):
    # array for results and initialize
    arr_res = np.zeros_like(arr_nums)
    arr_res[0] = prev_res
    # loop on the length of arr_b
    for i in range(len(arr_b)):
        #do the calculation and set the value in result array
        prev_res = some_calc_func (prev_res, arr_nums[i], arr_b[i])
        arr_res[i+1] = prev_res
    return arr_res

最后,这样称呼它

df['result'] = with_numba(df.loc[0, 'result'], 
                          df['nums'].to_numpy(),  
                          df.loc[1:, 'b'].to_numpy())

通过 timeit,我比使用 zip 的方法快了约 9 倍,并且速度会随着大小而增加

%timeit df['result'] = with_numba(df.loc[0, 'result'], 
                                  df['nums'].to_numpy(),  
                                  df.loc[1:, 'b'].to_numpy()) 
# 526 µs ± 45.5 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

注意使用 Numba 可能会出现问题,具体取决于您的实际 some_calc_func

【讨论】:

  • 您好!不知何故对我来说,你的方法慢了 1-3 秒(总时间是 600~ 秒,所以它并没有太大的区别,但仍然如此)。您还有其他建议吗?
  • @snatchysquid 你的意思是 zip 的方法比你原来的方法慢?关于真正的some_calc_func 的真正问题?也许分享更多关于你的真正问题,比如你所说的大是什么意思,你的实际功能是什么,因为你可以看到你的数据乘以 1000,zip 更快
  • 在超过 200,000 行的文件上运行的函数是相同的(唯一的区别是常量)。我会继续调查,因为它看起来确实很奇怪。
  • 这里,甚至itertuples 都不比zip 方式快。
  • @Ben.T 仅循环就快了 3 倍...在循环内部进行操作时,它可能会随着内部代码增长以支配循环开销而下降。
【解决方案4】:

你有一个 f(...) 可以申请,但不能因为你需要保留一个内存(前一个)行。您可以使用闭包或类来执行此操作。下面是一个类的实现:

import pandas as pd

class Func():

    def __init__(self, value):
        self._prev = value
        self._init = True

    def __call__(self, x):
        if self._init:
            res = self._prev
            self._init = False
        elif x.b == 0:
            res = x.nums - self._prev
        else:
            res = x.nums + self._prev

        self._prev = res
        return res

#df = pd.read_clipboard()
f = Func(20)
df['result'] = df.apply(f, axis=1)

您可以将__call__ 替换为some_calc_func 正文中的任何内容。

【讨论】:

    【解决方案5】:

    我意识到这就是@Prodipta 的答案,但这种方法使用global 关键字代替apply 的每次迭代来记住先前的结果:

    prev_result = 20
    
    def my_calc(row):
        global prev_result
        i = int(row.name)   #the index of the current row
        if i==0:
            return prev_result   
        elif row['b'] == 1:
            out = prev_result * df.loc[i-1,'nums']/2   #loc to get prev_num
        else:
            out = df.loc[i-1,'nums'] + 17
        prev_result = out
        return out
    
    df['result'] = df.apply(my_calc, axis=1)
    

    示例数据的结果:

       nums  b  result
    0  20.0  1    20.0
    1  22.0  0    37.0
    2  30.0  1   407.0
    3  29.1  1  6105.0
    4  20.0  0    46.1
    

    这是@Ben T 回答的速度测试 - 不是最好的,但也不是最差的?

    In[0]
    df = pd.DataFrame({'nums':np.random.randint(0,100,5000),'b':np.random.choice([0,1],5000)})
    
    prev_result = 20
    
    %%timeit
    df['result'] = df.apply(my_calc, axis=1)
    
    Out[0]
    117 ms ± 5.67 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2020-04-21
      • 1970-01-01
      • 2013-01-01
      • 2022-11-13
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多