Более эффективный / чистый способ агрегирования данных

#python #pandas #group-by #pivot-table

#питон #панды #группировка по #сводная таблица

Вопрос:

python 3.7.10 панды 1.1.5

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

 import pandas as pd

df = pd.DataFrame({
    'fruit': ['orange', 'orange', 'orange', 'banana', 'banana', 'banana'],
    'origin': ['USA', 'Canada', 'USA', 'Canada', 'USA', 'Canada'],
    'weight': [1, 2, 3, 4, 5, 6]
})
df
 
фрукты происхождение вес
0 Оранжевый США 1
1 Оранжевый Канада 2
2 Оранжевый США 3
3 банан Канада 4
4 банан США 5
5 банан Канада 6
 (df
 .groupby('fruit')
 .apply(lambda x: (x
                   .groupby('origin')
                   .agg({'weight': sum})
                   .assign(share=lambda x: x.weight / x.weight.sum()))
 )
)
 
фрукты происхождение вес Поделиться
банан Канада 10 0.666667
США 5 0.333333
Оранжевый Канада 2 0.333333
США 4 0.666667

Есть ли более питонический / пандский / более чистый способ добиться того же результата? Например, я не могу переименовать вес на лету, если это не сумма, а скорее количество, и я хочу, чтобы имя столбца отражало это.

В R это выглядит для меня намного чище.

 library(dplyr)

df <- tibble(
  fruit = c('orange', 'orange', 'orange', 'banana', 'banana', 'banana'),
  origin = c('USA', 'Canada', 'USA', 'Canada', 'USA', 'Canada'),
  weight = c(1, 2, 3, 4, 5, 6)
)

df %>%
  group_by(fruit, origin) %>%
  summarise(total = sum(weight)) %>%
  mutate(share = total / sum(total))
 

Я считаю, что есть какой-то более чистый способ сделать это на python.

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

1. является ли pandas.pivot_table способом для этого? посмотрите это -> pandas.pydata.org/pandas-docs/stable/reference/api /…

2. @WillianVieira подумал об этом. Не удалось найти точное решение. Можете ли вы предоставить его?

Ответ №1:

У вас может быть два отдельных groupby оператора, чтобы сделать его чище:

 In [101]: x = df.groupby(['fruit', 'origin']).sum().reset_index()
In [104]: x['share'] = x.groupby('fruit')['weight'].apply(lambda i: i/i.sum())

In [105]: x
Out[105]: 
    fruit  origin  weight     share
0  banana  Canada      10  0.666667
1  banana     USA       5  0.333333
2  orange  Canada       2  0.333333
3  orange     USA       4  0.666667
 

ИЛИ, согласно комментарию @Manakin, избегать применения:

 In [101]: x = df.groupby(['fruit', 'origin']).sum().reset_index()
In [109]: x['share'] = x['weight'].div(x.groupby('fruit')['weight'].transform('sum'))

In [110]: x
Out[110]: 
    fruit  origin  weight     share
0  banana  Canada      10  0.666667
1  banana     USA       5  0.333333
2  orange  Canada       2  0.333333
3  orange     USA       4  0.666667
 

Ответ №2:

Для прямого перевода из вашего r кода потребуется другой groupby :

 >>> ( df.groupby(['fruit', 'origin'])
        .sum().assign(
            share=lambda x: x.groupby('fruit').transform(lambda x: x / x.sum())
         )
     )
               weight     share
fruit  origin                  
banana Canada      10  0.666667
       USA          5  0.333333
orange Canada       2  0.333333
       USA          4  0.666667
 

Или,

 >>> ( df.groupby(['fruit', 'origin'])
        .sum().assign(share=lambda x: x / x.groupby('fruit').transform(sum))
    )
 
               weight     share
fruit  origin                  
banana Canada      10  0.666667
       USA          5  0.333333
orange Canada       2  0.333333
       USA          4  0.666667
 

Или, вероятно, самый читаемый:

 >>> ( df.groupby(['fruit', 'origin']).sum()
        .assign(share=lambda x: x.div(df.groupby('fruit').sum()))
    )

               weight     share
fruit  origin                  
banana Canada      10  0.666667
       USA          5  0.333333
orange Canada       2  0.333333
       USA          4  0.666667
 

Еще лучше с rdiv , и, наконец, действительно однострочный 🙂 :

 >>> df.groupby(['fruit', 'origin']).sum().assign(share=df.groupby('fruit').sum().rdiv)
               weight     share
fruit  origin                  
banana Canada      10  0.666667
       USA          5  0.333333
orange Canada       2  0.333333
       USA          4  0.666667
 

Что-то без groupby, используя pd.melt и pd.crosstab :

 >>> df2 = df.melt(['fruit', 'origin'], var_name='stats')
>>> pd.crosstab(
        index=[df2['fruit'], df2['origin']], 
        columns=df2['stats'], 
        values=df2['value'], 
        aggfunc=sum
    ).assign(share=lambda x:x/x.sum(level=0))

stats          weight     share
fruit  origin                  
banana Canada      10  0.666667
       USA          5  0.333333
orange Canada       2  0.333333
       USA          4  0.666667
 

Ответ №3:

Это не так «чисто», как R, но это можно сделать в однострочном формате.:

 df.groupby(['fruit', 'origin'])['weight'].sum().reset_index()
  .pipe(lambda x: x.assign(share=x['weight'] / 
                                 x.groupby('fruit')['weight'].transform('sum')))
 

Выходной сигнал:

     fruit  origin  weight     share
0  banana  Canada      10  0.666667
1  banana     USA       5  0.333333
2  orange  Canada       2  0.333333
3  orange     USA       4  0.666667
 

Ответ №4:

Вы можете использовать .set_index , а затем использовать .div здесь.

 Out = df.groupby(["fruit", "origin"]).sum()
Out = Out.assign(share=Out.div(df.set_index(["fruit", "origin"]).sum(level=0)))

               weight     share
fruit  origin                  
banana Canada      10  0.666667
       USA          5  0.333333
orange Canada       2  0.333333
       USA          4  0.666667
 

Ответ №5:

В вашем R-коде вы преобразовали sum(weight) , чтобы переименовать его, "total" что вы можете сделать, передав аргументы ключевого слова в a groupby(...).agg(new_name=("column_name", aggfunc) . Вы также можете добиться некоторой чистоты, написав вспомогательную функцию для выполнения нормализации.

 def normalize(x):
    return x / x.sum()

out = (df.groupby(["fruit", "origin"])
         .agg(total=("weight", "sum"))
         .assign(
             share=lambda df: df.groupby("fruit").transform(normalize)
         ))

print(out)
               total     share
fruit  origin
banana Canada     10  0.666667
       USA         5  0.333333
orange Canada      2  0.333333
       USA         4  0.666667