Pandas сортировать по совокупности групп и столбцам

Учитывая следующий фрейм данных

In [31]: rand = np.random.RandomState(1)
         df = pd.DataFrame({'A': ['foo', 'bar', 'baz'] * 2,
                            'B': rand.randn(6),
                            'C': rand.rand(6) > .5})

In [32]: df
Out[32]:      A         B      C
         0  foo  1.624345  False
         1  bar -0.611756   True
         2  baz -0.528172  False
         3  foo -1.072969   True
         4  bar  0.865408  False
         5  baz -2.301539   True 

Я хотел бы отсортировать его по группам (A) по агрегированной сумме B, а затем по значению в C (не агрегируется). Таким образом, в основном получаем порядок групп A с

In [28]: df.groupby('A').sum().sort('B')
Out[28]:             B  C
         A               
         baz -2.829710  1
         bar  0.253651  1
         foo  0.551377  1

И затем True/False, чтобы он в конечном итоге выглядел следующим образом:

In [30]: df.ix[[5, 2, 1, 4, 3, 0]]
Out[30]: A         B      C
    5  baz -2.301539   True
    2  baz -0.528172  False
    1  bar -0.611756   True
    4  bar  0.865408  False
    3  foo -1.072969   True
    0  foo  1.624345  False

Как это можно сделать?

Ответ 1

Groupby A:

In [0]: grp = df.groupby('A')

В каждой группе суммируются по B и транслируются значения с помощью преобразования. Затем сортируйте по B:

In [1]: grp[['B']].transform(sum).sort('B')
Out[1]:
          B
2 -2.829710
5 -2.829710
1  0.253651
4  0.253651
0  0.551377
3  0.551377

Индексируйте исходный df, передавая индекс сверху. Это приведет к переопределению значений A по суммарной сумме значений B:

In [2]: sort1 = df.ix[grp[['B']].transform(sum).sort('B').index]

In [3]: sort1
Out[3]:
     A         B      C
2  baz -0.528172  False
5  baz -2.301539   True
1  bar -0.611756   True
4  bar  0.865408  False
0  foo  1.624345  False
3  foo -1.072969   True

Наконец, отсортируйте значения "C" внутри групп "A", используя опцию sort=False, чтобы сохранить порядок сортировки A с шага 1:

In [4]: f = lambda x: x.sort('C', ascending=False)

In [5]: sort2 = sort1.groupby('A', sort=False).apply(f)

In [6]: sort2
Out[6]:
         A         B      C
A
baz 5  baz -2.301539   True
    2  baz -0.528172  False
bar 1  bar -0.611756   True
    4  bar  0.865408  False
foo 3  foo -1.072969   True
    0  foo  1.624345  False

Очистите индекс df с помощью reset_index с помощью drop=True:

In [7]: sort2.reset_index(0, drop=True)
Out[7]:
     A         B      C
5  baz -2.301539   True
2  baz -0.528172  False
1  bar -0.611756   True
4  bar  0.865408  False
3  foo -1.072969   True
0  foo  1.624345  False

Ответ 2

Здесь более краткий подход...

df['a_bsum'] = df.groupby('A')['B'].transform(sum)
df.sort(['a_bsum','C'], ascending=[True, False]).drop('a_bsum', axis=1)

Первая строка добавляет столбец в фрейм данных с групповой суммой. Вторая строка выполняет сортировку, а затем удаляет дополнительный столбец.

Результат:

    A       B           C
5   baz     -2.301539   True
2   baz     -0.528172   False
1   bar     -0.611756   True
4   bar      0.865408   False
3   foo     -1.072969   True
0   foo      1.624345   False

ПРИМЕЧАНИЕ: sort устарел, используйте sort_values вместо

Ответ 3

Один из способов сделать это - вставить фиктивный столбец с суммами для сортировки:

In [10]: sum_B_over_A = df.groupby('A').sum().B

In [11]: sum_B_over_A
Out[11]: 
A
bar    0.253652
baz   -2.829711
foo    0.551376
Name: B

in [12]: df['sum_B_over_A'] = df.A.apply(sum_B_over_A.get_value)

In [13]: df
Out[13]: 
     A         B      C  sum_B_over_A
0  foo  1.624345  False      0.551376
1  bar -0.611756   True      0.253652
2  baz -0.528172  False     -2.829711
3  foo -1.072969   True      0.551376
4  bar  0.865408  False      0.253652
5  baz -2.301539   True     -2.829711

In [14]: df.sort(['sum_B_over_A', 'A', 'B'])
Out[14]: 
     A         B      C   sum_B_over_A
5  baz -2.301539   True      -2.829711
2  baz -0.528172  False      -2.829711
1  bar -0.611756   True       0.253652
4  bar  0.865408  False       0.253652
3  foo -1.072969   True       0.551376
0  foo  1.624345  False       0.551376

и, возможно, вы сбросите фиктивную строку:

In [15]: df.sort(['sum_B_over_A', 'A', 'B']).drop('sum_B_over_A', axis=1)
Out[15]: 
     A         B      C
5  baz -2.301539   True
2  baz -0.528172  False
1  bar -0.611756   True
4  bar  0.865408  False
3  foo -1.072969   True
0  foo  1.624345  False