Панды заполняют недостающие даты и значения одновременно для каждой группы

#python #pandas

Вопрос:

У меня есть фрейм данных (mydf) с датами для каждой группы с ежемесячной периодичностью, как показано ниже:

 Dt          Id  Sales
2021-03-01  B   2
2021-04-01  B   42
2021-05-01  B   20
2021-06-01  B   4
2020-10-01  A   47
2020-11-01  A   67
2020-12-01  A   46
 

Я хочу заполнить dt для каждой группы до максимальной даты в столбце даты, начиная с даты идентификатора, одновременно заполняя 0 для столбца продаж. Таким образом, каждая группа начинается со своей собственной даты начала, но заканчивается в ту же дату окончания.

Так, например, ID=A начнется с 2020-10-01 и дойдет до 2021-06-03, а значение для заполненных дат будет равно 0.

Таким образом, результат будет

 Dt          Id  Sales
2021-03-01  B   2
2021-04-01  B   42
2021-05-01  B   20
2021-06-01  B   4
2020-10-01  A   46
2020-11-01  A   47
2020-12-01  A   67
2021-01-01  A   0
2021-02-01  A   0
2021-03-01  A   0
2021-04-01  A   0
2021-05-01  A   0
2021-06-01  A   0
 

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

Мой код таков :

 f = lambda x: x.reindex(pd.date_range('2020-10-01', '2021-06-01', freq='MS', name='Dt'))
mydf = mydf.set_index('Dt').groupby('Id').apply(f).drop('Id', axis=1).fillna(0)
mydf = mydf.reset_index()

 

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

1. Ваш пример не очень понятен. Вы продублировали «2021-06-01» для B, и пример не соответствует текстовому описанию (для A повторная выборка отсутствует). Можете ли вы улучшить вопрос?

2. @mozway Идентификаторы в выходных данных были продублированы по ошибке. Теперь все ясно? Спасибо

Ответ №1:

Альтернативное использование pd.MultiIndex с пониманием списка:

 s = (pd.MultiIndex.from_tuples([[x, d]
      for x, y in df.groupby("Id")["Dt"]
      for d in pd.date_range(min(y), max(df["Dt"]), freq="MS")], names=["Id", "Dt"]))

print (df.set_index(["Id", "Dt"]).reindex(s, fill_value=0).reset_index())
 

Ответ №2:

Давайте попробуем:

  1. Получение минимального значения для каждой группы с помощью groupby.min
  2. Добавьте новый столбец в агрегированные минуты, которые называются max , в котором хранятся максимальные значения из кадра, используя Series.max на Dt
  3. Создайте человека date_range для каждой группы на основе значений min и max
  4. Series.explode в строки, чтобы иметь фрейм данных, представляющий новый индекс.
  5. MultiIndex.from_frame Создайте в кадре данных с помощью. reindex
  6. reindex с midx помощью и установите fillvalue=0
 # Get Min Per Group
dates = mydf.groupby('Id')['Dt'].min().to_frame(name='min')
# Get max from Frame
dates['max'] = mydf['Dt'].max()

# Create MultiIndex with separate Date ranges per Group
midx = pd.MultiIndex.from_frame(
    dates.apply(
        lambda x: pd.date_range(x['min'], x['max'], freq='MS'), axis=1
    ).explode().reset_index(name='Dt')[['Dt', 'Id']]
)

# Reindex
mydf = (
    mydf.set_index(['Dt', 'Id'])
        .reindex(midx, fill_value=0)
        .reset_index()
)
 

mydf :

            Dt Id  Sales
0  2020-10-01  A     47
1  2020-11-01  A     67
2  2020-12-01  A     46
3  2021-01-01  A      0
4  2021-02-01  A      0
5  2021-03-01  A      0
6  2021-04-01  A      0
7  2021-05-01  A      0
8  2021-06-01  A      0
9  2021-03-01  B      2
10 2021-04-01  B     42
11 2021-05-01  B     20
12 2021-06-01  B      4
 

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

 import pandas as pd

mydf = pd.DataFrame({
    'Dt': ['2021-03-01', '2021-04-01', '2021-05-01', '2021-06-01', '2020-10-01',
           '2020-11-01', '2020-12-01'],
    'Id': ['B', 'B', 'B', 'B', 'A', 'A', 'A'],
    'Sales': [2, 42, 20, 4, 47, 67, 46]
})
mydf['Dt'] = pd.to_datetime(mydf['Dt'])
 

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

1. Спасибо вам за предоставленное решение. Однако я не хочу начинать даты для Id=B с самой ранней даты кадра данных. Независимо от идентификатора, я хочу сохранить дату начала прежней и продолжить заполнение до конца максимальной даты. Извините, если я не был ясен раньше.

2. Каждая группа стартует в свою собственную дату начала. Но все группы заканчиваются в одну и ту же дату окончания?

Ответ №3:

Вот другой подход:

 from itertools import product

# compute the min-max date range
date_range = pd.date_range(*mydf['Dt'].agg(['min', 'max']), freq='MS', name='Dt')

# make MultiIndex per group, keep only values above min date per group
idx = pd.MultiIndex.from_tuples([e for Id,Dt_min in mydf.groupby('Id')['Dt'].min().items()
                                   for e in list(product(date_range[date_range>Dt_min],
                                                         [Id]))
                                ])

# concatenate the original dataframe and the missing indexes
mydf = mydf.set_index(['Dt', 'Id'])
mydf = pd.concat([mydf,
                  mydf.reindex(idx.difference(mydf.index)).fillna(0)]
                ).sort_index(level=1).reset_index()

mydf
 

выход:

            Dt Id  Sales
0  2020-10-01  A   47.0
1  2020-11-01  A   67.0
2  2020-12-01  A   46.0
3  2021-01-01  A    0.0
4  2021-02-01  A    0.0
5  2021-03-01  A    0.0
6  2021-04-01  A    0.0
7  2021-05-01  A    0.0
8  2021-06-01  A    0.0
9  2021-03-01  B    2.0
10 2021-04-01  B   42.0
11 2021-05-01  B   20.0
12 2021-06-01  B    4.0
 

Ответ №4:

Мы можем использовать complete функцию from pyjanitor для отображения недостающих значений:

Преобразование Dt в дату и время:

  df['Dt'] = pd.to_datetime(df['Dt'])
 

Создайте сопоставление Dt с новыми значениями через pd.date_range и установите частоту начала месяца ( MS ):

  max_time = df.Dt.max()

 new_values = {"Dt": lambda df:pd.date_range(df.min(), max_time, freq='1MS')}

# pip install pyjanitor
import janitor
import pandas as pd
df.complete([new_values], by='Id').fillna(0)


   Id         Dt  Sales
0   A 2020-10-01   47.0
1   A 2020-11-01   67.0
2   A 2020-12-01   46.0
3   A 2021-01-01    0.0
4   A 2021-02-01    0.0
5   A 2021-03-01    0.0
6   A 2021-04-01    0.0
7   A 2021-05-01    0.0
8   A 2021-06-01    0.0
9   B 2021-03-01    2.0
10  B 2021-04-01   42.0
11  B 2021-05-01   20.0
12  B 2021-06-01    4.0
 

Придерживаясь только Панд, мы можем комбинировать apply , с groupby и reindex ; к счастью, Dt является уникальным, поэтому мы можем безопасно переиндексировать:

 (df
 .set_index('Dt')
 .groupby('Id')
 .apply(lambda df: df.reindex(pd.date_range(df.index.min(), 
                                            max_time, 
                                            freq='1MS'), 
                              fill_value = 0)
                              )
 .drop(columns='Id')
 .rename_axis(['Id', 'Dt'])
 .reset_index())
 
   Id         Dt  Sales
0   A 2020-10-01     47
1   A 2020-11-01     67
2   A 2020-12-01     46
3   A 2021-01-01      0
4   A 2021-02-01      0
5   A 2021-03-01      0
6   A 2021-04-01      0
7   A 2021-05-01      0
8   A 2021-06-01      0
9   B 2021-03-01      2
10  B 2021-04-01     42
11  B 2021-05-01     20
12  B 2021-06-01      4