Pandas groupby преобразует кумулятивно с условиями

#python #pandas #pandas-groupby

#python #pandas #pandas-groupby

Вопрос:

У меня есть большая таблица со многими идентификаторами продуктов и iso_codes: всего 2 миллиона строк. Таким образом, ответ должен (если возможно) также учитывать проблемы с памятью, у меня 16 ГБ памяти.

Я хотел бы видеть для каждой комбинации (id, iso_code), какое количество возвращаемых элементов находится перед buy_date в строке (таким образом, кумулятивно), но есть одна загвоздка:
я хочу подсчитывать только возвраты, полученные от предыдущих продаж, где return_date находится перед buy_date, на который я смотрю.

Я добавил столбец items_returned в качестве примера: это столбец, который должен быть вычислен.

Идея заключается в следующем:
На момент продажи я могу рассчитывать только те возвраты, которые уже произошли, а не те, которые произойдут в будущем.

Я попробовал комбинацию df.groupby(['id', 'iso_code']).transform(np.cumsum) и .transform(lambda x: only count returns that happened before my buy_date) , но не смог понять, как сделать a .groupby.transform(np.cumsum) с применением этих особых условий.

Аналогичный вопрос для купленных товаров, где я считаю кумулятивные товары только за дни, меньшие, чем мой buy_date.

Надеюсь, вы сможете мне помочь.

Пример результирующей таблицы:

  ------- ------ ------------ ---------- ------------ --------------- ---------------- ------------------ 
|   row |   id | iso_code   |   return | buy_date   | return_date   |   items_bought |   items_returned |
|------- ------ ------------ ---------- ------------ --------------- ---------------- ------------------|
|     0 |  177 | DE         |        1 | 2019-05-16 | 2019-05-24    |              0 |                0 |
|     1 |  177 | DE         |        1 | 2019-05-29 | 2019-06-03    |              1 |                1 |
|     2 |  177 | DE         |        1 | 2019-10-27 | 2019-11-06    |              2 |                2 |
|     3 |  177 | DE         |        0 | 2019-11-06 | None          |              3 |                2 |
|     4 |  177 | DE         |        1 | 2019-11-18 | 2019-11-28    |              4 |                3 |
|     5 |  177 | DE         |        1 | 2019-11-21 | 2019-12-11    |              5 |                3 |
|     6 |  177 | DE         |        1 | 2019-11-25 | 2019-12-06    |              6 |                3 |
|     7 |  177 | DE         |        0 | 2019-11-30 | None          |              7 |                4 |
|     8 |  177 | DE         |        1 | 2020-04-30 | 2020-05-27    |              8 |                6 |
|     9 |  177 | DE         |        1 | 2020-04-30 | 2020-09-18    |              8 |                6 |
 ------- ------ ------------ ---------- ------------ --------------- ---------------- ------------------ 
  

Пример кода:

 import pandas as pd
from io import StringIO

df_text = """
row id  iso_code    return  buy_date    return_date
0   177 DE  1   2019-05-16  2019-05-24
1   177 DE  1   2019-05-29  2019-06-03
2   177 DE  1   2019-10-27  2019-11-06
3   177 DE  0   2019-11-06  None
4   177 DE  1   2019-11-18  2019-11-28
5   177 DE  1   2019-11-21  2019-12-11
6   177 DE  1   2019-11-25  2019-12-06
7   177 DE  0   2019-11-30  None
8   177 DE  1   2020-04-30  2020-05-27
9   177 DE  1   2020-04-30  2020-09-18
"""

df = pd.read_csv(StringIO(df_text), sep='t', index_col=0)

df['items_bought'] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 8]
df['items_returned'] = [0, 1, 2, 2, 3, 3, 3, 4, 6, 6]
  

Ответ №1:

Похоже, для этого требуется перекрестное слияние:

 (df[['id','iso_code', 'buy_date']].reset_index()
   .merge(df[['id','iso_code', 'return','return_date','buy_date']], on=['id','iso_code'])
   .assign(items_returned=lambda x: x['return_date'].lt(x['buy_date_x'])*x['return'],
           items_bought=lambda x: x['buy_date_y'].lt(x['buy_date_x']))
   .groupby('row')[['items_bought','items_returned']].sum()
)
  

Выходной сигнал:

      items_bought  items_returned
row                              
0               0               0
1               1               1
2               2               2
3               3               2
4               4               3
5               5               3
6               6               3
7               7               4
8               8               6
9               8               6
  

Обновление для больших данных перекрестное слияние не идеально из-за требований к памяти. Затем мы можем сделать groupby() так, чтобы мы объединялись только для небольших групп:

 def myfunc(df):
    return (df[['id','iso_code', 'buy_date']].reset_index()
   .merge(df[['id','iso_code', 'return','return_date','buy_date']], on=['id','iso_code'])
   .assign(items_returned=lambda x: x['return_date'].lt(x['buy_date_x'])*x['return'],
           items_bought=lambda x: x['buy_date_y'].lt(x['buy_date_x']))
   .groupby('row')[['items_bought','items_returned']].sum()
)

df.groupby(['id','iso_code']).apply(myfunc).reset_index(level=[0,1], drop=True)
  

И вы получите тот же результат:

      items_bought  items_returned
row                              
0               0               0
1               1               1
2               2               2
3               3               2
4               4               3
5               5               3
6               6               3
7               7               4
8               8               6
9               8               6
  

Комментарии:

1. Привет, Куанг, спасибо за ваше элегантное решение. Это работает для меньших данных, но у меня возникают проблемы с памятью при применении этого к моему фрейму данных с 2 миллионами строк: невозможно выделить 3,69 гигабайт для массива с формой (3, 165038559) и типом данных object.

2. Привет, Куанг, спасибо за обновление! Я нашел решение .apply() слишком медленным, для завершения потребовалась вечность. Итак, теперь я использую ваше первое решение: поскольку я работаю в облаке, я просто переключил свою виртуальную машину на одну с большим объемом памяти (32 ГБ), и тогда она работает. Мне нравится, что вы использовали цепочку методов в своем ответе 🙂