Более чистый способ выбора подмножества минимальных значений для каждой группы?

#python #pandas #dataframe

#python #pandas #фрейм данных

Вопрос:

Учитывая фрейм данных, подобный приведенному ниже, вот что я хочу: только в строках, содержащих самую раннюю дату для каждого серийного номера, найдите строки, где Location равно null, и обновите их указанным значением по умолчанию.

 df = pd.DataFrame([['123456',pd.to_datetime('1/1/2019'),'Location A'],
                   ['123456',pd.to_datetime('1/2/2019'),np.nan],
                   ['123456',pd.to_datetime('1/3/2019'),np.nan],
                   ['123456',pd.to_datetime('5/1/2019'),np.nan],
                   ['654321',pd.to_datetime('2/1/2019'),'Location B'],
                   ['654321',pd.to_datetime('2/2/2019'),'Location B'],
                   ['654321',pd.to_datetime('2/3/2019'),'Location C'],
                   ['112233',pd.to_datetime('3/1/2019'),np.nan],
                   ['112233',pd.to_datetime('3/2/2019'),'Location D'],
                   ['112233',pd.to_datetime('3/3/2019'),np.nan],
                   ['445566',pd.to_datetime('4/1/2019'),'Location E'],
                   ['445566',pd.to_datetime('4/2/2019'),'Location E'],
                   ['445566',pd.to_datetime('4/3/2019'),'Location E'],
                   ['778899',pd.to_datetime('5/1/2019'),np.nan],
                   ['778899',pd.to_datetime('5/2/2019'),np.nan],
                   ['778899',pd.to_datetime('5/3/2019'),np.nan],
                   ['332211',pd.to_datetime('6/1/2019'),np.nan],
                   ['332211',pd.to_datetime('6/2/2019'),'Location F'],
                   ['332211',pd.to_datetime('6/3/2019'),'Location F'],
                   ['665544',pd.to_datetime('7/1/2019'),'Location G'],
                   ['665544',pd.to_datetime('7/2/2019'),'Location G'],
                   ['665544',pd.to_datetime('7/3/2019'),'Location G'],
                   ['998877',pd.to_datetime('8/1/2019'),'Location H'],
                   ['998877',pd.to_datetime('8/2/2019'),'Location I'],
                   ['998877',pd.to_datetime('8/2/2019'),'Location I'],
                   ['147258',pd.to_datetime('9/1/2019'),np.nan],
                   ['147258',pd.to_datetime('9/2/2019'),np.nan],
                   ['147258',pd.to_datetime('9/3/2019'),'Location J']],
                   columns=['Serial','Date','Location'])

df
Out[498]: 
    Serial       Date    Location
0   123456 2019-01-01  Location A
1   123456 2019-01-02         NaN
2   123456 2019-01-03         NaN
3   123456 2019-05-01         NaN
4   654321 2019-02-01  Location B
5   654321 2019-02-02  Location B
6   654321 2019-02-03  Location C
7   112233 2019-03-01         NaN
8   112233 2019-03-02  Location D
9   112233 2019-03-03         NaN
10  445566 2019-04-01  Location E
11  445566 2019-04-02  Location E
12  445566 2019-04-03  Location E
13  778899 2019-05-01         NaN
14  778899 2019-05-02         NaN
15  778899 2019-05-03         NaN
16  332211 2019-06-01         NaN
17  332211 2019-06-02  Location F
18  332211 2019-06-03  Location F
19  665544 2019-07-01  Location G
20  665544 2019-07-02  Location G
21  665544 2019-07-03  Location G
22  998877 2019-08-01  Location H
23  998877 2019-08-02  Location I
24  998877 2019-08-02  Location I
25  147258 2019-09-01         NaN
26  147258 2019-09-02         NaN
27  147258 2019-09-03  Location J
  

Итак, в приведенном выше примере должны быть выбраны только строки 6, 12, 15 и 24. У меня это работает со строкой ниже, которая:

  • Использует groupby для получения списка индексов минимальных дат
  • Сравнивает это с индексом df, возвращает логический ряд
  • Проверяет наличие любых нулей в столбце Location, возвращает другой логический ряд
  • Сравнивает два логических ряда, возвращает окончательный логический ряд
  • Наконец, выбирает записи местоположения на основе этого окончательного логического ряда

Несмотря на функциональность, это кажется неуклюжим и обходным. Есть ли способ лучше?

 df.loc[pd.Series(df.index).isin(df.groupby('Serial')['Date'].idxmin().tolist()) amp; df['Location'].isnull(), 'Location'] = 'XXXX'

df
Out[502]: 
    Serial       Date    Location
0   123456 2019-01-01  Location A
1   123456 2019-01-02         NaN
2   123456 2019-01-03         NaN
3   123456 2019-05-01         NaN
4   654321 2019-02-01  Location B
5   654321 2019-02-02  Location B
6   654321 2019-02-03  Location C
7   112233 2019-03-01        XXXX
8   112233 2019-03-02  Location D
9   112233 2019-03-03         NaN
10  445566 2019-04-01  Location E
11  445566 2019-04-02  Location E
12  445566 2019-04-03  Location E
13  778899 2019-05-01        XXXX
14  778899 2019-05-02         NaN
15  778899 2019-05-03         NaN
16  332211 2019-06-01        XXXX
17  332211 2019-06-02  Location F
18  332211 2019-06-03  Location F
19  665544 2019-07-01  Location G
20  665544 2019-07-02  Location G
21  665544 2019-07-03  Location G
22  998877 2019-08-01  Location H
23  998877 2019-08-02  Location I
24  998877 2019-08-02  Location I
25  147258 2019-09-01        XXXX
26  147258 2019-09-02         NaN
27  147258 2019-09-03  Location J
  

РЕДАКТИРОВАТЬ: Добавлена новая строка 3 в образец df, чтобы уточнить, что даты уникальны в группах серийных номеров, но могут не быть уникальными в сериалах. Строка с индексом 3 в этом примере имеет ту же дату, что и минимальная дата другого серийного номера, но ее не следует выбирать. Я справился с этим, сопоставив индексы вместо самих дат, но способ, которым я это сделал, кажется беспорядочным.

Ответ №1:

Я думаю, что ваше решение «нормально», но вы могли бы сделать его немного более жестким и ускорить его с помощью numpy .

Вы можете использовать df.groupby.Series.min() для этого и df.Series.isnull() .

После этого вы условно заполняете свой Location столбец XXXX с np.where :

 min_date = df.groupby('Serial')['Date'].min()
cond = df['Location'].isnull()

df['Location'] = np.where((df['Date'].isin(min_date)) amp; (cond) , 'XXXX', df['Location'])

print(df)
    Serial       Date    Location
0   123456 2019-01-01  Location A
1   123456 2019-01-02         NaN
2   123456 2019-01-03         NaN
3   654321 2019-02-01  Location B
4   654321 2019-02-02  Location B
5   654321 2019-02-03  Location C
6   112233 2019-03-01        XXXX
7   112233 2019-03-02  Location D
8   112233 2019-03-03         NaN
9   445566 2019-04-01  Location E
10  445566 2019-04-02  Location E
11  445566 2019-04-03  Location E
12  778899 2019-05-01        XXXX
13  778899 2019-05-02         NaN
14  778899 2019-05-03         NaN
15  332211 2019-06-01        XXXX
16  332211 2019-06-02  Location F
17  332211 2019-06-03  Location F
18  665544 2019-07-01  Location G
19  665544 2019-07-02  Location G
20  665544 2019-07-03  Location G
21  998877 2019-08-01  Location H
22  998877 2019-08-02  Location I
23  998877 2019-08-02  Location I
24  147258 2019-09-01        XXXX
25  147258 2019-09-02         NaN
26  147258 2019-09-03  Location J
  

Редактировать после комментария OP о повторяющихся датах:

Мы можем объединить min_dates фрейм данных и использовать indicator=True при объединении

 min_date = df.groupby('Serial')['Date'].min().reset_index()
cond = df['Location'].isnull()

df = df.merge(min_date, on=['Serial', 'Date'], how='left', indicator=True)

df['Location'] = np.where((df['_merge'] == 'both') amp; (cond) , 'XXXX', df['Location'])
df = df.drop('_merge', axis=1)
print(df)

    Serial       Date    Location
0   123456 2019-01-01  Location A
1   123456 2019-01-02         NaN
2   123456 2019-01-03         NaN
3   123456 2019-05-01         NaN
4   654321 2019-02-01  Location B
5   654321 2019-02-02  Location B
6   654321 2019-02-03  Location C
7   112233 2019-03-01        XXXX
8   112233 2019-03-02  Location D
9   112233 2019-03-03         NaN
10  445566 2019-04-01  Location E
11  445566 2019-04-02  Location E
12  445566 2019-04-03  Location E
13  778899 2019-05-01        XXXX
14  778899 2019-05-02         NaN
15  778899 2019-05-03         NaN
16  332211 2019-06-01        XXXX
17  332211 2019-06-02  Location F
18  332211 2019-06-03  Location F
19  665544 2019-07-01  Location G
20  665544 2019-07-02  Location G
21  665544 2019-07-03  Location G
22  998877 2019-08-01  Location H
23  998877 2019-08-02  Location I
24  998877 2019-08-02  Location I
25  147258 2019-09-01        XXXX
26  147258 2019-09-02         NaN
27  147258 2019-09-03  Location J
  

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

1. Извините, чтобы немного прояснить: даты уникальны в пределах каждого серийного номера, но не обязательно могут быть уникальными для серийных номеров. Итак, если бы я добавил следующую запись в этот пример df: ['123456',pd.to_datetime('5/1/2019'),np.nan] Ваш код выбрал бы эту новую строку из-за строки 12 в примере df, которая мне не нужна. Следовательно, почему я использовал idxmin вместо просто min.

2. Хотя мне нравится, как вы используете np.where.

3. Не уверен, что это не неуклюжий или обходной путь, но это другой способ, которым вы можете сделать это для вашего удобства @LuminosityXVII

4. Хм. Это полезно, но я не уверен, следует ли мне принимать это как ответ, поскольку это скорее альтернатива, чем определенное улучшение, и я более склонен использовать свою версию в данном случае. Я протестирую np.where и воспользуюсь им, если это будет быстрее.

5. …Хорошо, в конце концов, я принимаю это, в основном для np.where, но также и для понимания слияния. Спасибо, вы были полезны.