#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