在 Pandas 中,当前行的值需要使用先前计算的值时,是否有一种方法可以在 dataframe.apply 中实现?

158

我有以下数据框:

Index_Date    A   B     C    D
================================
2015-01-31    10   10   Nan   10
2015-02-01     2    3   Nan   22 
2015-02-02    10   60   Nan  280
2015-02-03    10  100   Nan  250

要求:

Index_Date    A   B    C     D
================================
2015-01-31    10   10    10   10
2015-02-01     2    3    23   22
2015-02-02    10   60   290  280
2015-02-03    10  100  3000  250

2015-01-31C列是通过获取D列推导出来的。

接下来,我需要使用2015-01-31C列值乘以2015-02-01A列值并加上B列值

我已经尝试运用applyshift函数,并在此基础上使用if else语句,但是这样会导致键错误。


18
这是一个好问题。我有类似需要使用向量化解决方案。如果pandas提供了一种版本的apply()函数,使得用户的函数能够在计算过程中访问前一行的一个或多个值,或者至少返回一个值,然后在下一次迭代时将该值传递“给自己”,那就太好了。相比于for循环,这难道不会带来一些效率上的收益吗? - Bill
7个回答

110

首先创建派生值:

df.loc[0, 'C'] = df.loc[0, 'D']
然后迭代遍历剩余的行,并填充计算出的值:
for i in range(1, len(df)):
    df.loc[i, 'C'] = df.loc[i-1, 'C'] * df.loc[i, 'A'] + df.loc[i, 'B']


  Index_Date   A   B    C    D
0 2015-01-31  10  10   10   10
1 2015-02-01   2   3   23   22
2 2015-02-02  10  60  290  280

82
Pandas中是否有一种函数可以不用循环来完成这个操作? - ctrl-alt-delete
6
计算的迭代性质导致输入依赖于先前步骤的结果,这使得向量化变得复杂。您可以尝试使用apply函数并编写一个与循环执行相同计算的函数,但在幕后,这也将是一个循环。请参考:http://pandas.pydata.org/pandas-docs/version/0.17.1/generated/pandas.DataFrame.apply.html - Stefan
如果我使用这个循环并在合并的数据框上进行计算,它会工作,但只能到具有Nan的行。没有抛出错误。如果我尝试使用fillNa,我会得到AttributeError: 'numpy.float64'对象没有属性'fillna'。有没有办法跳过具有Nan的行或将值设置为零? - ctrl-alt-delete
你的意思是除了“C”列以外,其他列中有缺失值吗? - Stefan
1
是的,您的解决方案很好。我只是确保在循环之前填充数据帧中的NaN值。 - ctrl-alt-delete
显示剩余4条评论

96
给定一列数字:
lst = []
cols = ['A']
for a in range(100, 105):
    lst.append([a])
df = pd.DataFrame(lst, columns=cols, index=range(5))
df

    A
0   100
1   101
2   102
3   103
4   104

你可以用shift引用上一行:
df['Change'] = df.A - df.A.shift(1)
df

    A   Change
0   100 NaN
1   101 1.0
2   102 1.0
3   103 1.0
4   104 1.0

您可以使用fill_value参数填充缺失值
df['Change'] = df.A - df.A.shift(1, fill_value=df.A[0]) # fills in the missing value e.g. 100<br>
df

    A   Change
0   100 0.0
1   101 1.0
2   102 1.0
3   103 1.0
4   104 1.0

20
由于上一行的值在开始时不可知,因此这不会在这种情况下有所帮助。它必须在每次迭代中计算,然后在下一次迭代中使用。 - Bill
19
我很感激这个答案,因为我在寻找一个我确实知道前一行数值的情况下偶然发现了它。所以感谢 @kztd。 - Kevin Pauli
3
正是我在寻找的内容。这也更快,因为它具有数组操作,而不像其他答案建议的那样进行循环。 - Dimanjan
2
“shift” 绝对是最好的选择。使用 fill_value 参数为第一行提供默认值。 - maccaroo

32

numba

对于无法矢量化的递归计算,numba 使用 JIT 编译和较低级别的对象,通常能够大幅提高性能。您只需定义一个常规的 for 循环,并使用装饰器 @njit 或(对于旧版本)@jit(nopython=True)

对于一个大小合理的数据框而言,与常规的 for 循环相比,这将带来约 30 倍的性能提升:

from numba import jit

@jit(nopython=True)
def calculator_nb(a, b, d):
    res = np.empty(d.shape)
    res[0] = d[0]
    for i in range(1, res.shape[0]):
        res[i] = res[i-1] * a[i] + b[i]
    return res

df['C'] = calculator_nb(*df[list('ABD')].values.T)

n = 10**5
df = pd.concat([df]*n, ignore_index=True)

# benchmarking on Python 3.6.0, Pandas 0.19.2, NumPy 1.11.3, Numba 0.30.1
# calculator() is same as calculator_nb() but without @jit decorator
%timeit calculator_nb(*df[list('ABD')].values.T)  # 14.1 ms per loop
%timeit calculator(*df[list('ABD')].values.T)     # 444 ms per loop

1
太棒了!我加速了我的函数,它可以计算前一个数值的值。谢谢! - Artem Malikov
我该如何在jupyter-notebook中使用@jit(nopython = True) - sergzemsk
1
@sergzemsk,就像你写的那样(以及我的回答),它被称为装饰器。请注意,numba的后续版本支持快捷方式@njit - jpp
@jpp,我有一个if条件,所以这个改进失败了。我收到了一个错误信息:“TypingError:在nopython模式管道中失败(步骤:nopython前端)”。 - sergzemsk
@sergzemsk,我建议您提出一个新问题,因为我不清楚if语句的位置,也不知道为什么它没有被numba向量化。 - jpp

27

在numpy数组上应用递归函数将比当前答案更快。

df = pd.DataFrame(np.repeat(np.arange(2, 6),3).reshape(4,3), columns=['A', 'B', 'D'])
new = [df.D.values[0]]
for i in range(1, len(df.index)):
    new.append(new[i-1]*df.A.values[i]+df.B.values[i])
df['C'] = new

输出

      A  B  D    C
   0  1  1  1    1
   1  2  2  2    4
   2  3  3  3   15
   3  4  4  4   64
   4  5  5  5  325

3
这个答案对我非常适用,使用类似的计算方法。我曾尝试使用cumsum和shift的组合,但这个解决方案效果更好。谢谢。 - Simon

17

虽然这个问题已经问了一段时间,但我会发表我的答案,希望能帮助到某些人。

免责声明:我知道这个解决方案不是标准的,但我认为它运作良好。

import pandas as pd
import numpy as np

data = np.array([[10, 2, 10, 10],
                 [10, 3, 60, 100],
                 [np.nan] * 4,
                 [10, 22, 280, 250]]).T
idx = pd.date_range('20150131', end='20150203')
df = pd.DataFrame(data=data, columns=list('ABCD'), index=idx)
df
               A    B     C    D
 =================================
 2015-01-31    10   10    NaN  10
 2015-02-01    2    3     NaN  22 
 2015-02-02    10   60    NaN  280
 2015-02-03    10   100   NaN  250

def calculate(mul, add):
    global value
    value = value * mul + add
    return value

value = df.loc['2015-01-31', 'D']
df.loc['2015-01-31', 'C'] = value
df.loc['2015-02-01':, 'C'] = df.loc['2015-02-01':].apply(lambda row: calculate(*row[['A', 'B']]), axis=1)
df
               A    B     C     D
 =================================
 2015-01-31    10   10    10    10
 2015-02-01    2    3     23    22 
 2015-02-02    10   60    290   280
 2015-02-03    10   100   3000  250

基本上,我们使用 pandas 中的 apply 和一个全局变量来跟踪先前计算出的值。

使用for循环进行时间比较:

data = np.random.random(size=(1000, 4))
idx = pd.date_range('20150131', end='20171026')
df = pd.DataFrame(data=data, columns=list('ABCD'), index=idx)
df.C = np.nan

df.loc['2015-01-31', 'C'] = df.loc['2015-01-31', 'D']

%%timeit
for i in df.loc['2015-02-01':].index.date:
    df.loc[i, 'C'] = df.loc[(i - pd.DateOffset(days=1)).date(), 'C'] * df.loc[i, 'A'] + df.loc[i, 'B']

每次循环需要3.2秒,标准差为114毫秒(7次运行的平均值和标准偏差,每次运行1个循环)

data = np.random.random(size=(1000, 4))
idx = pd.date_range('20150131', end='20171026')
df = pd.DataFrame(data=data, columns=list('ABCD'), index=idx)
df.C = np.nan

def calculate(mul, add):
    global value
    value = value * mul + add
    return value

value = df.loc['2015-01-31', 'D']
df.loc['2015-01-31', 'C'] = value

%%timeit
df.loc['2015-02-01':, 'C'] = df.loc['2015-02-01':].apply(lambda row: calculate(*row[['A', 'B']]), axis=1)

每次循环平均1.82秒±64.4毫秒(7次运行,每次1个循环的标准差)

因此平均快0.57倍。


2

这是一个老问题,但以下解决方案(不使用for循环)可能会有所帮助:

def new_fun(df):
    prev_value = df.iloc[0]["C"]
    def func2(row):
        # non local variable ==> will use pre_value from the new_fun function
        nonlocal prev_value
        new_value =  prev_value * row['A'] + row['B']
        prev_value = row['C']
        return new_value
    # This line might throw a SettingWithCopyWarning warning
    df.iloc[1:]["C"] = df.iloc[1:].apply(func2, axis=1)
    return df

df = new_fun(df)

这对.apply做了一些假设,这些假设可能不成立:如果.apply被并行化或以你所期望的顺序之外的任何方式调用,则结果将不如预期。 - feetwet
我同意你的担忧。这个答案中的假设是基于这个主题的问题。此外,默认情况下,apply不是并行化的... - Wazaa

1
通常避免显式循环的关键是将数据帧的两个实例按行索引-1==行索引连接(合并)。

然后,您将拥有一个包含r和r-1行的大型数据帧,从中可以执行df.apply()函数。

然而,创建大型数据集的开销可能抵消了并行处理的好处...


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接