Сортировка значений с помощью объекта multi-index/groupby «по группе» без нарушения уровня индекса

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

#DataFrame
ff = pd.DataFrame([('P1', 17, 'male'),
                   ('P2', 10, 'female'),
                   ('P3', 10, 'male'),
                   ('P4', 19, 'female'),
                   ('P5', 10, 'male'),
                   ('P6', 12, 'male'),
                   ('P7', 12, 'male'),
                   ('P8', 15, 'female'),
                   ('P9', 15, 'female'),
                   ('P10', 10, 'male')],
                  columns=['Name', 'Age', 'Sex'])

# Attempts
(
    ff
    .groupby(['Age', 'Sex'])
    .agg(**{
        'Count': pd.NamedAgg(column = "Name", aggfunc='count'),
        'Who': pd.NamedAgg(column = "Name", aggfunc=lambda x: ', '.join([i for i in x]))})
#     .sort_values('Count')           <- this breaks the index level
#     .sort_values(['Count', 'Age'])  <- this too breaks the index level
)

Исходные данные:

СчитатьКто
ВозрастСекс
10женский1р2
мужчина3п3,п5,п10
12мужчина2р6, р7
15женщина2п8, п9
17мужчина1р1
19женщина1п4

Желаемый результат: (отсортируйте значения по сумме группы «Возраст», но сохраните сгруппированный индекс)

СчитатьКто
ВозрастСекс
17мужчина1р1
19женщина1п4
12мужчина2р6, р7
15женщина2п8, п9
10женский1р2
мужчина3п3,п5,п10

Обновлено: вот как я наконец решаю проблему, любые дополнительные советы приветствуются.

# DataFrame -- I update a bit for testcases.
ff = pd.DataFrame([('P1', 19, 'male'),
                   ('P2', 10, 'female'),
                   ('P3', 10, 'male'),
                   ('P4', 19, 'female'),
                   ('P5', 10, 'male'),
                   ('P6', 12, 'male'),
                   ('P7', 12, 'male'),
                   ('P7', 12, 'male'),
                   ('P7', 12, 'male'),
                   ('P7', 12, 'male'),
                   ('P8', 15, 'female'),
                   ('P9', 15, 'female'),
                   ('P10', 10, 'male')],
                  columns=['Name', 'Age', 'Sex'])

# It works !
(
    ff.groupby(['Age', 'Sex']).agg(**{
    'Count': pd.NamedAgg(column = "Name", aggfunc='count'),
    'Who': pd.NamedAgg(column = "Name", aggfunc=lambda x: ', '.join([i for i in x]))})
    # Sort by 'Count' and keep the group adding 'tmp'
    .assign(
        tmp=lambda x: x.reset_index().groupby('Age')['Count'].transform('sum').to_numpy())
    .sort_values(['tmp','Age'])
     # drop tmp
    .drop('tmp', axis=1)
)

Непонятно, по чему вы хотите сортировать?

LarryBird 06.05.2022 06:14

что вы подразумеваете под без нарушения уровня индекса?

user7864386 06.05.2022 06:17

Привет, закомментированные строки показывают, что происходит, в основном это означает, что он будет сортироваться по «количеству» и разбивать, например, группу 10-летнего возраста на 2 непоследовательные строки.

Cookie 06.05.2022 08:37
2
3
36
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ну вот.

Давайте сохраним переменную temp в data.

data = ff.groupby(['Age', 'Sex']).agg(**{
    'Count': pd.NamedAgg(column = "Name", aggfunc='count'),
    'Who': pd.NamedAgg(column = "Name", aggfunc=lambda x: ', '.join([i for i in x]))})

Вы можете написать пользовательскую функцию, чтобы делать то, что вы хотите делать в каждой группе, с помощью функции apply.

Например.

data.groupby("Age", group_keys=False).apply(lambda x: x.sort_values("Count", ascending=False))
            Count          Who
Age Sex                       
10  male        3  P3, P5, P10
    female      1           P2
12  male        2       P6, P7
15  female      2       P8, P9
17  male        1           P1
19  female      1           P4

Или измените на ascending order

data.groupby("Age", group_keys=False).apply(lambda x: x.sort_values("Count", ascending=False))
            Count          Who
Age Sex                       
10  female      1           P2
    male        3  P3, P5, P10
12  male        2       P6, P7
15  female      2       P8, P9
17  male        1           P1
19  female      1           P4

Или, если вы хотите отсортировать по каждому уровню мультииндекса. вы можете сделать так.

Вы можете отсортировать индекс, добавив level аргументы в функцию sort_index.

Например:

  1. data.sort_index(level=0, ascending=True)

Отсортируйте первый индекс по возрастанию.

            Count          Who
Age Sex                       
19  female      1           P4
17  male        1           P1
15  female      2       P8, P9
12  male        2       P6, P7
10  male        3  P3, P5, P10
    female      1           P2
  1. data.sort_index(level=[0,1], ascending=[False, True])

Отсортируйте первый индекс в порядке возрастания, а второй индекс в порядке убывания.

            Count          Who
Age Sex                       
19  female      1           P4
17  male        1           P1
15  female      2       P8, P9
12  male        2       P6, P7
10  female      1           P2
    male        3  P3, P5, P10

Кстати.

breaking index level не является особым результатом. Это просто оптимизация отображения

Например.

Вы можете создать его самостоятельно, например:

pd.DataFrame({"a":[1,2,3,4,5]}, index=pd.MultiIndex.from_arrays([[10,10,20,10,10],['F','M','F','M','F']],names=['A','B']))
      a
A  B   
10 F  1
   M  2
20 F  3
10 M  4
   F  5

Я боюсь, что это только сортирует индекс по «Возрасту» вместо значения «Количество». Я попытался продублировать некоторые столбцы (в моем случае я скопировал прошлый P7, чтобы он имел наибольшее значение счетчика, но все приведенные выше сортировки вернут отсортированные df по возрасту.

Cookie 06.05.2022 09:05
Ответ принят как подходящий

Вы можете изменить форму на DataFrame.unstack и отсортировать индекс по сумме обоих значений Sex, если они существуют, а затем изменить форму на DataFrame.stack:

df1 = df.unstack()
df1 = df1.sort_index(key=df1.sum(axis=1, numeric_only=True).get).stack().astype(df.dtypes)
print (df1)
            Count          Who
Age Sex                       
17  male        1           P1
19  female      1           P4
12  male        2       P6, P7
15  female      2       P8, P9
10  female      1           P2
    male        3  P3, P5, P10

Другая идея — сортировка по сумме обоих значений с помощью GroupBy.transform:

df['tmp'] = df.groupby('Age')['Count'].transform('sum')

df1 = df.sort_values(['tmp','Age']).drop('tmp', axis=1)
print (df1)
             Count          Who
Age Sex                       
17  male        1           P1
19  female      1           P4
12  male        2       P6, P7
15  female      2       P8, P9
10  female      1           P2
    male        3  P3, P5, P10
    

Обновлено: однострочное решение:

df = (
    ff
    .groupby(['Age', 'Sex'])
    .agg(**{
        'Count': pd.NamedAgg(column = "Name", aggfunc='count'),
        'Who': pd.NamedAgg(column = "Name", aggfunc=', '.join)})
    
    .assign(tmp = lambda x: x.groupby('Age')['Count'].transform('sum'))
    .sort_values(['tmp','Age'])
    .drop('tmp', axis=1))
print (df)
            Count          Who
Age Sex                       
17  male        1           P1
19  female      1           P4
12  male        2       P6, P7
15  female      2       P8, P9
10  female      1           P2
    male        3  P3, P5, P10

Второй работает, и если мне нужно сделать его однострочным (для лучшей читаемости, используя .function в каждой строке), я обновил вопрос, чтобы показать, как я это сделал. Есть ли избыточный код или что-то, что можно было бы улучшить?

Cookie 06.05.2022 09:14

@Cookie - ответ был отредактирован.

jezrael 06.05.2022 09:19

Вау, это именно то, что я делал, здорово знать! Обычно ли создают столбец tmp и удаляют их?

Cookie 06.05.2022 09:21

@Cookie - нет, но здесь сортировка по 2 столбцам, поэтому использовал это решение.

jezrael 06.05.2022 09:22

Другие вопросы по теме