简体   繁体   English

基于日期范围的熊猫数据框条件累积总和

[英]Pandas dataframe conditional cumulative sum based on date range

I have a pandas dataframe:我有一个熊猫数据框:

         Date            Party    Status
-------------------------------------------
0        01-01-2018      John     Sent
1        13-01-2018      Lisa     Received
2        15-01-2018      Will     Received
3        19-01-2018      Mark     Sent
4        02-02-2018      Will     Sent
5        28-02-2018      John     Received

I would like to add new columns that perform a .cumsum() , but it is conditional on the dates.我想添加执行.cumsum()新列,但它以日期为条件。 It would look like this:它看起来像这样:

                                                Num of Sent         Num of Received
         Date            Party    Status        in Past 30 Days     in Past 30 Days
-----------------------------------------------------------------------------------
0        01-01-2018      John     Sent          1                   0
1        13-01-2018      Lisa     Received      1                   1
2        15-01-2018      Will     Received      1                   2
3        19-01-2018      Mark     Sent          2                   2
4        02-02-2018      Will     Sent          2                   2
5        28-02-2018      John     Received      1                   1

I managed to implement what I need by writing the following code:我设法通过编写以下代码来实现我所需要的:

def inner_func(date_var, status_var, date_array, status_array):
    sent_increment = 0
    received_increment = 0

    for k in range(0, len(date_array)):
        if((date_var - date_array[k]).days <= 30):
            if(status_array[k] == "Sent"):
                sent_increment += 1
            elif(status_array[k] == "Received"):
                received_increment += 1

    return sent_increment, received_increment
import pandas as pd
import time
df = pd.DataFrame({"Date": pd.to_datetime(["01-01-2018", "13-01-2018", "15-01-2018", "19-01-2018", "02-02-2018", "28-02-2018"]),
                   "Party": ["John", "Lisa", "Will", "Mark", "Will", "John"],
                   "Status": ["Sent", "Received", "Received", "Sent", "Sent", "Received"]})

df = df.sort_values("Date")
date_array = []
status_array = []

for i in range(0, len(df)):
        date_var = df.loc[i,"Date"]
        date_array.append(date_var)
        status_var = df.loc[i,"Status"]
        status_array.append(status_var)
        sent_count, received_count = inner_func(date_var, status_var, date_array, status_array)
        df.loc[i, "Num of Sent in Past 30 days"] = sent_count
        df.loc[i, "Num of Received in Past 30 days"] = received_count

However, the process is computationally expensive and painfully slow when df is large, since the nested loops go through the dataframe twice.然而,当df很大时,这个过程的计算成本很高并且非常缓慢,因为嵌套循环两次通过数据帧。 Is there a more pythonic way to implement what I am trying to achieve without iterating through the dataframe in the way I am doing?有没有更pythonic的方法来实现我想要实现的目标,而无需以我正在做的方式遍历数据框?

Update 2更新 2

Michael has provided the solution to what I am looking for: here .迈克尔提供了我正在寻找的解决方案: here Lets assume that I want to apply the solution on groupby objects.让我们假设我想在groupby对象上应用解决方案。 For example, using the rolling solution to compute the cumulative sums based for each party:例如,使用滚动解决方案计算每一方的累积总和:

                                                Sent past 30       Received past 30
         Date            Party    Status        days by party      days by party
-----------------------------------------------------------------------------------
0        01-01-2018      John     Sent          1                   0
1        13-01-2018      Lisa     Received      0                   1
2        15-01-2018      Will     Received      0                   1
3        19-01-2018      Mark     Sent          1                   0
4        02-02-2018      Will     Sent          1                   1
5        28-02-2018      John     Received      0                   1

I have attempted to regenerate the solution for the using the groupby method below:我尝试使用下面的groupby方法重新生成解决方案:

l = []
grp_obj = df.groupby("Party")
grp_obj.rolling('30D',  min_periods=1)["dummy"].apply(lambda x: l.append(x.value_counts()) or 0)
df.reset_index(inplace=True)

But I ended up with incorrect values.但我最终得到了不正确的值。 I know that it is happening because the concat method is combining the dataframes without condsidering their indices, since groupby orders the data differently.我知道这是因为concat方法在不考虑它们的索引的情况下组合数据帧,因为groupby对数据的排序方式不同。 Is there a way I can modify the list appending to include the original index, such that I can merge/join the value_counts dataframe to the original one?有没有一种方法可以修改附加列表以包含原始索引,以便我可以将 value_counts 数据帧合并/加入原始索引?

If you set Date as index and convert Status temporary to a categorical you can use pd.rolling with a little trick如果您将Date设置为索引并将Status临时转换为分类,您可以使用pd.rolling与一个小技巧

df = df.set_index('Date')
df['dummy'] = df['Status'].astype('category',copy=False).cat.codes
l = []
df.rolling('30D', min_periods=1)['dummy'].apply(lambda x: l.append(x.value_counts()) or 0)
df.reset_index(inplace=True)
pd.concat(
    [df,
    (pd.DataFrame(l)
        .rename(columns={1.0: "Sent past 30 Days", 0.0: "Received past 30 Days"})
        .fillna(0)
        .astype('int'))
    ], axis=1).drop('dummy', 1)

Out:出去:

        Date Party    Status  Received past 30 Days  Sent past 30 Days
0 2018-01-01  John      Sent                      0                  1
1 2018-01-13  Lisa  Received                      1                  1
2 2018-01-15  Will  Received                      2                  1
3 2018-01-19  Mark      Sent                      2                  2
4 2018-02-02  Will      Sent                      2                  2
5 2018-02-28  John  Received                      1                  1

Maintaining an original index to allow subsequent merging维护原始索引以允许后续合并

Slightly adjust the data to have different sequences in Date and index稍微调整数据,使Dateindex有不同的序列

df = pd.DataFrame({"Date": pd.to_datetime(["01-01-2018", "13-01-2018", "03-01-2018", "19-01-2018", "08-02-2018", "22-02-2018"]),
                   "Party": ["John", "Lisa", "Will", "Mark", "Will", "John"],
                   "Status": ["Sent", "Received", "Received", "Sent", "Sent", "Received"]})
df

Out:出去:

        Date Party    Status
0 2018-01-01  John      Sent
1 2018-01-13  Lisa  Received
2 2018-03-01  Will  Received
3 2018-01-19  Mark      Sent
4 2018-08-02  Will      Sent
5 2018-02-22  John  Received

Store the original index after sorting by Date and reindex after operationing on the dataframe sorted by DateDate排序后存储原始索引并在按Date排序的数据帧上操作后重新索引

df = df.sort_values('Date')
df = df.reset_index()
df = df.set_index('Date')
df['dummy'] = df['Status'].astype('category',copy=False).cat.codes
l = []
df.rolling('30D', min_periods=1)['dummy'].apply(lambda x: l.append(x.value_counts()) or 0)
df.reset_index(inplace=True)
df = pd.concat(
      [df,
      (pd.DataFrame(l)
          .rename(columns={1.0: "Sent past 30 Days", 0.0: "Received past 30 Days"})
          .fillna(0)
          .astype('int'))
      ], axis=1).drop('dummy', 1)
df.set_index('index')

Out:出去:

            Date Party    Status  Received past 30 Days  Sent past 30 Days
index                                                                     
0     2018-01-01  John      Sent                      0                  1
1     2018-01-13  Lisa  Received                      1                  1
3     2018-01-19  Mark      Sent                      1                  2
5     2018-02-22  John  Received                      1                  0
2     2018-03-01  Will  Received                      2                  0
4     2018-08-02  Will      Sent                      0                  1

Counting values in groups计算组中的值

Sort by Party and Date first to get the right order to append the grouped counts首先按PartyDate排序以获得附加分组计数的正确顺序

df = pd.DataFrame({"Date": pd.to_datetime(["01-01-2018", "13-01-2018", "15-01-2018", "19-01-2018", "02-02-2018", "28-02-2018"]),
                   "Party": ["John", "Lisa", "Will", "Mark", "Will", "John"],
                   "Status": ["Sent", "Received", "Received", "Sent", "Sent", "Received"]})
df = df.sort_values(['Party','Date'])

After that reindex before concat to append to the right rowsconcat之前重新索引以附加到正确的行

df = df.set_index('Date')
df['dummy'] = df['Status'].astype('category',copy=False).cat.codes
l = []
df.groupby('Party').rolling('30D', min_periods=1)['dummy'].apply(lambda x: l.append(x.value_counts()) or 0)
df.reset_index(inplace=True)

pd.concat(
      [df,
      (pd.DataFrame(l)
          .rename(columns={1.0: "Sent past 30 Days", 0.0: "Received past 30 Days"})
          .fillna(0)
          .astype('int'))
      ], axis=1).drop('dummy', 1).sort_values('Date')

Out:出去:

        Date Party    Status  Received past 30 Days  Sent past 30 Days
0 2018-01-01  John      Sent                      0                  1
2 2018-01-13  Lisa  Received                      1                  0
4 2018-01-15  Will  Received                      1                  0
3 2018-01-19  Mark      Sent                      0                  1
5 2018-02-02  Will      Sent                      1                  1
1 2018-02-28  John  Received                      1                  0

Micro-Benchmark微基准

As this solution is also iterating over the dataset I compared the running times of both approaches.由于此解决方案也在迭代数据集,因此我比较了两种方法的运行时间。 Only very small datasets were used because the original solution's runtime was increasing fast.只使用了非常小的数据集,因为原始解决方案的运行时间增长很快。

Results结果

基准结果

Code to reproduce the benchmark重现基准的代码

import pandas as pd
import perfplot

def makedata(n=1):
  df = pd.DataFrame({"Date": pd.to_datetime(["01-01-2018", "13-01-2018", "15-01-2018", "19-01-2018", "02-02-2018", "28-02-2018"]*n),
                   "Party": ["John", "Lisa", "Will", "Mark", "Will", "John"]*n,
                   "Status": ["Sent", "Received", "Received", "Sent", "Sent", "Received"]*n})

  return df.sort_values("Date")

def rolling(df):
  df = df.set_index('Date')
  df['dummy'] = df['Status'].astype('category',copy=False).cat.codes
  l = []
  df.rolling('30D', min_periods=1)['dummy'].apply(lambda x: l.append(x.value_counts()) or 0)
  df.reset_index(inplace=True)
  return pd.concat(
      [df,
      (pd.DataFrame(l)
          .rename(columns={1.0: "Sent past 30 Days", 0.0: "Received past 30 Days"})
          .fillna(0)
          .astype('int'))
      ], axis=1).drop('dummy', 1)

def forloop(df):
  date_array = []
  status_array = []
  def inner_func(date_var, status_var, date_array, status_array):
      sent_increment = 0
      received_increment = 0

      for k in range(0, len(date_array)):
          if((date_var - date_array[k]).days <= 30):
              if(status_array[k] == "Sent"):
                  sent_increment += 1
              elif(status_array[k] == "Received"):
                  received_increment += 1

      return sent_increment, received_increment

  for i in range(0, len(df)):
          date_var = df.loc[i,"Date"]
          date_array.append(date_var)
          status_var = df.loc[i,"Status"]
          status_array.append(status_var)
          sent_count, received_count = inner_func(date_var, status_var, date_array, status_array)
          df.loc[i, "Num of Sent in Past 30 days"] = sent_count
          df.loc[i, "Num of Received in Past 30 days"] = received_count
  return df

perfplot.show(
    setup=makedata,
    kernels=[forloop, rolling],
    n_range=[x for x in range(5, 105, 5)],
    equality_check=None,
    xlabel='len(df)'
)

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM