Mam ramkę danych pandy:

         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

Chciałbym dodać nowe kolumny, które wykonują .cumsum(), ale jest warunkowy na daty. Wyglądałoby tak:

                                                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

Udało mi się wdrożyć to, czego potrzebuję, pisząc następujący kod:

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

Jednak proces jest obliczalnie drogi i boleśnie powolny, gdy jest duży, ponieważ zagnieżdżone pętle przechodzą dwukrotnie przez Dataframe. Czy istnieje bardziej piythonic sposób na wdrożenie tego, co próbuję osiągnąć bez irytacji przez dataframe w drodze robię?

Aktualizacja 2

Michael podał rozwiązanie tego, czego szukam: Tutaj. Załóżmy, że chcę zastosować rozwiązanie na obiektach groupby. Na przykład, korzystając z roztworu toczenia, aby obliczyć kumulacyjne kwoty oparte na każdej ze stron:

                                                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

Próbowałem zregenerować rozwiązanie dla metody przy użyciu {X0}} poniżej:

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)

Ale skończyłem na nieprawidłowe wartości. Wiem, że dzieje się, ponieważ metoda concat łączy Dataframes bez kabla ich indeksów, ponieważ groupby one inaczej. Czy istnieje sposób, w jaki mogę zmodyfikować listę dołączącą do włączenia oryginalnego indeksu, takiego, że mogę łączyć / łączyć Dataframe Dataframe do oryginału?

2
Pleastry 10 październik 2020, 14:09

1 odpowiedź

Najlepsza odpowiedź

Jeśli ustawisz Date jako indeks i konwertować Status tymczasowy do kategorycznego, możesz użyć pd.rolling z małą sztuczką

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)

Na zewnątrz:

        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

Utrzymanie oryginalnego indeksu, aby umożliwić kolejne połączenie

Lekko dostosuj dane, aby mieć różne sekwencje w {x0}} i index

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

Na zewnątrz:

        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

Przechowuj oryginalny indeks po sortowaniu przez Date i reindex po operacji na Dataframe posortowany przez 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')

Na zewnątrz:

            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

Wartości liczni w grupach

Sortuj według Party i Date Najpierw, aby uzyskać odpowiednią kolejność, aby dołączyć zgrupowane liczenia

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'])

Po tym renix przed concat, aby dołączyć do właściwych wierszy

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')

Na zewnątrz:

        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

Ponieważ to rozwiązanie jest również iterujące nad zestawem danych, porównałem czasy bieżące obu podejść. Zastosowano tylko bardzo małe zestawy danych, ponieważ czas pracy oryginalnego rozwiązania wzrastał szybko.

Wyniki

benchmark results

Kod, aby odtworzyć benchmarku

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)'
)
2
Michael Szczesny 12 październik 2020, 17:57