Как суммировать по Pandas кадру данных условно - PullRequest
2 голосов
/ 23 апреля 2020

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

Фрейм данных:

ClientID   Date           Orders
123        2020-03-01     23
123        2020-03-05     10
123        2020-03-10     7
456        2020-02-22     3
456        2020-02-25     15
456        2020-02-28     5
...

Я хочу добавить столбец "orders_last_week", содержащий общее количество заказов для указанного клиента c за 7 дней до указанной даты. Эквивалент Excel будет выглядеть примерно так:

SUMIFS([orders],[ClientID],ClientID,[Date]>=Date-7,[Date]<Date)

Так что это будет результат:

ClientID   Date           Orders  Orders_Last_Week
123        2020-03-01     23      0
123        2020-03-05     10      23
123        2020-03-10     7       10
456        2020-02-22     3       0
456        2020-02-25     15      3
456        2020-02-28     5       18
...

Я могу решить это с помощью al oop, но так как мой фрейм данных содержит> 20M записи, это нереальное решение. Может кто-нибудь, пожалуйста, помогите мне? Очень ценится!

1 Ответ

1 голос
/ 23 апреля 2020

Я предполагаю, что ваш фрейм данных называется df. Я также предполагаю, что даты не повторяются для данного ClientID и располагаются в порядке возрастания (если это не так, выполните суммирование по группам и отсортируйте результат так, чтобы он был).

Суть моего решения заключается в том, что для данного ClientID и даты.

  1. Используйте groupby.transform, чтобы разделить эту проблему по ClientID.
  2. Используйте rolling, чтобы проверить следующие 7 строк для дат, которые находятся в пределах 1 недели.
  3. В этих 7 строках даты в промежутке времени помечены как True (= 1). Даты, которые не помечены как Ложные (= 0).
  4. В этих 7 строках умножьте столбец «Заказы» на маркировку дат «Истина / Ложь».
  5. Суммируйте результат.

На самом деле, мы используем 8 строк, потому что, например, SuMoTuWeThFrSaSu имеет 8 дней.

Что затрудняет то, что скользящий агрегирует столбцы по одному за раз и поэтому явно не позволяет работать с несколькими столбцами при агрегировании. Если бы это было так, вы могли бы сделать фильтр, используя столбец даты, и использовать его для суммирования заказов.

Однако есть лазейка: вы можете использовать несколько столбцов, если вы счастливы их переправить через индекс!

Я использую некоторые вспомогательные функции. Примечание. Под a понимается серия pandas с 8 строками и значениями «Заказы» с указанием «Дата» в индексе.

Любопытно узнать, какова производительность ваших реальных данных.

import pandas as pd

data =  {
    'ClientID': {0: 123, 1: 123, 2: 123, 3: 456, 4: 456, 5: 456},
    'Date': {0: '2020-03-01', 1: '2020-03-05', 2: '2020-03-10',
             3: '2020-02-22', 4: '2020-02-25', 5: '2020-02-28'},
 'Orders': {0: 23, 1: 10, 2: 7, 3: 3, 4: 15, 5: 5}
}

df = pd.DataFrame(data)

# Make sure the dates are datetimes
df['Date'] = pd.to_datetime(df['Date'])

# Put into index so we can smuggle them through "rolling"
df = df.set_index(['ClientID', 'Date'])


def date(a):
    # get the "Date" index-column from the dataframe 
    return a.index.get_level_values('Date')

def previous_week(a):
    # get a column of 0s and 1s identifying the previous week, 
    # (compared to the date in the last row in a).
    return (date(a) >= date(a)[-1] - pd.DateOffset(days=7)) * (date(a) < date(a)[-1]) 

def previous_week_order_total(a):
    #compute the order total for the previous week
    return sum(previous_week(a) * a)

def total_last_week(group):
    # for a "ClientID" compute all the "previous week order totals"
    return group.rolling(8, min_periods=1).apply(previous_week_order_total, raw=False)

# Ok, actually compute this
df['Orders_Last_Week'] = df.groupby(['ClientID']).transform(total_last_week)

# Reset the index back so you can have the ClientID and Date columns back
df = df.reset_index()

Приведенный выше код основан на том факте, что прошедшая неделя охватывает не более 7 строк данных, т. Е. 7 дней в неделе (хотя в вашем примере это на самом деле меньше 7)

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

Например, если ваш временные метки даты расположены не ближе 1 секунды, и вас интересует временное окно в 1 минуту (например, "Orders_last_minute"), замените pd.DateOffset(days=7) на pd.DateOffset(seconds=60) и group.rolling(8,... на group.rolling(61,....)

Очевидно, этот код немного пессимистичен c: в этом случае для каждой строки он всегда просматривает 61 строку. К сожалению, rolling не предлагает подходящую функцию переменного размера окна. Я подозреваю, что в некоторых случаях python l oop, который использует тот факт, что датафрейм отсортирован по дате, может работать быстрее, чем это частично векторизованное решение.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...