Pandas, применять несколько функций из нескольких столбцов для группировки объекта
Я хочу применить несколько функций из нескольких столбцов к объекту groupby, что приведет к созданию нового pandas.DataFrame
.
Я знаю, как это сделать на отдельных шагах:
by_user = lasts.groupby('user')
elapsed_days = by_user.apply(lambda x: (x.elapsed_time * x.num_cores).sum() / 86400)
running_days = by_user.apply(lambda x: (x.running_time * x.num_cores).sum() / 86400)
user_df = elapsed_days.to_frame('elapsed_days').join(running_days.to_frame('running_days'))
В результате получается user_df
:
![user_df]()
Однако я подозреваю, что есть лучший способ, например:
by_user.agg({'elapsed_days': lambda x: (x.elapsed_time * x.num_cores).sum() / 86400,
'running_days': lambda x: (x.running_time * x.num_cores).sum() / 86400})
Однако это не работает, потому что AFAIK agg()
работает на pandas.Series
.
Я нашел этот вопрос и ответ, но решения выглядят довольно уродливо для меня, и учитывая, что ответ почти четыре года, может быть, лучше путь к настоящему времени.
Ответы
Ответ 1
Я думаю, вы можете избежать agg
или apply
и, скорее, сначала несколько mul
, затем div
и последнее использование groupby
на index
с aggregating
sum
:
lasts = pd.DataFrame({'user':['a','s','d','d'],
'elapsed_time':[40000,50000,60000,90000],
'running_time':[30000,20000,30000,15000],
'num_cores':[7,8,9,4]})
print (lasts)
elapsed_time num_cores running_time user
0 40000 7 30000 a
1 50000 8 20000 s
2 60000 9 30000 d
3 90000 4 15000 d
by_user = lasts.groupby('user')
elapsed_days = by_user.apply(lambda x: (x.elapsed_time * x.num_cores).sum() / 86400)
print (elapsed_days)
running_days = by_user.apply(lambda x: (x.running_time * x.num_cores).sum() / 86400)
user_df = elapsed_days.to_frame('elapsed_days').join(running_days.to_frame('running_days'))
print (user_df)
elapsed_days running_days
user
a 3.240741 2.430556
d 10.416667 3.819444
s 4.629630 1.851852
lasts = lasts.set_index('user')
print (lasts[['elapsed_time','running_time']].mul(lasts['num_cores'], axis=0)
.div(86400)
.groupby(level=0)
.sum())
elapsed_time running_time
user
a 3.240741 2.430556
d 10.416667 3.819444
s 4.629630 1.851852
Ответ 2
Еще одно твердое решение - сделать то, что @MaxU сделал с этим решением в аналогичный вопрос и оберните отдельные функции в серию Pandas, поэтому требуется только reset_index()
вернуть фрейм данных.
Сначала определим функции для преобразований:
def ed(group):
return group.elapsed_time * group.num_cores).sum() / 86400
def rd(group):
return group.running_time * group.num_cores).sum() / 86400
Оберните их в серии с помощью get_stats
:
def get_stats(group):
return pd.Series({'elapsed_days': ed(group),
'running_days':rd(group)})
Наконец:
lasts.groupby('user').apply(get_stats).reset_index()
Ответ 3
В ответ на щедрость мы можем сделать его более общим с помощью частичного приложения из стандартной библиотеки functools.partial
.
import functools
import pandas as pd
#same data as other answer:
lasts = pd.DataFrame({'user':['a','s','d','d'],
'elapsed_time':[40000,50000,60000,90000],
'running_time':[30000,20000,30000,15000],
'num_cores':[7,8,9,4]})
#define the desired lambda as a function:
def myfunc(column, df, cores):
return (column * df.ix[column.index][cores]).sum()/86400
#use the partial to define the function with a given column and df:
mynewfunc = functools.partial(myfunc, df = lasts, cores = 'num_cores')
#agg by the partial function
lasts.groupby('user').agg({'elapsed_time':mynewfunc, 'running_time':mynewfunc})
Что дает нам:
running_time elapsed_time
user
a 2.430556 3.240741
d 3.819444 10.416667
s 1.851852 4.629630
Это не очень полезно для приведенного примера, но может быть более полезным в качестве общего примера.
Ответ 4
Чтобы использовать метод agg
для объекта groupby
, используя данные из других столбцов одного и того же блока данных, вы можете сделать следующее:
-
Определите свои функции (lambda
функции или нет), которые принимают как вход a Series
, и получайте данные из других столбцов (ов), используя синтаксис df.loc[series.index, col]
. В этом примере:
ed = lambda x: (x * lasts.loc[x.index, "num_cores"]).sum() / 86400.
rd = lambda x: (x * lasts.loc[x.index, "num_cores"]).sum() / 86400.
где lasts
является основным DataFrame, и мы получаем доступ к данным в столбце num_cores
с помощью метода .loc
.
-
Создайте словарь с этими функциями и имя для вновь созданных столбцов. Ключи - это имя столбцов, в которых должна применяться каждая функция, а значение - это другой словарь, где ключ - это имя функции, а значение - это функция.
my_func = {"elapsed_time" : {"elapsed_day" : ed},
"running_time" : {"running_days" : rd}}
-
Группировка и агрегат:
user_df = lasts.groupby("user").agg(my_func)
user_df
elapsed_time running_time
elapsed_day running_days
user
a 3.240741 2.430556
d 10.416667 3.819444
s 4.629630 1.851852
-
Если вы хотите удалить старые имена столбцов:
user_df.columns = user_df.columns.droplevel(0)
user_df
elapsed_day running_days
user
a 3.240741 2.430556
d 10.416667 3.819444
s 4.629630 1.851852
НТН
Ответ 5
Вот решение, которое очень похоже на оригинальную идею, выраженную в "Я подозреваю, что есть лучший способ".
Я буду использовать те же данные тестирования, что и другие ответы:
lasts = pd.DataFrame({'user':['a','s','d','d'],
'elapsed_time':[40000,50000,60000,90000],
'running_time':[30000,20000,30000,15000],
'num_cores':[7,8,9,4]})
groupby.apply
может принимать функцию, которая возвращает фреймворк данных, и затем автоматически сшивает возвращенные данные. В приведенной ниже формулировке есть два небольших улова. Первое замечает, что значения, переданные в DataFrame
, на самом деле представляют собой одноэлементные списки, а не просто числа.
def aggfunc(group):
""" This function mirrors the OP idea. Note the values below are lists """
return pd.DataFrame({'elapsed_days': [(group.elapsed_time * group.num_cores).sum() / 86400],
'running_days': [(group.running_time * group.num_cores).sum() / 86400]})
user_df = lasts.groupby('user').apply(aggfunc)
Результат:
elapsed_days running_days
user
a 0 3.240741 2.430556
d 0 10.416667 3.819444
s 0 4.629630 1.851852
Во-вторых, возвращаемый фреймворк имеет иерархический индекс (этот столбец нулей), который может быть сплющен, как показано ниже:
user_df.index = user_df.index.levels[0]
Результат:
elapsed_days running_days
user
a 3.240741 2.430556
d 10.416667 3.819444
s 4.629630 1.851852
Ответ 6
Эта функция agg может быть тем, что вы ищете.
Я добавил примерный набор данных и применил операцию к копии lasts
, которую я назвал lasts_
.
import pandas as pd
lasts = pd.DataFrame({'user' :['james','james','james','john','john'],
'elapsed_time':[ 200000, 400000, 300000,800000,900000],
'running_time':[ 100000, 100000, 200000,600000,700000],
'num_cores' :[ 4, 4, 4, 8, 8] })
# create temporary df to add columns to, without modifying original dataframe
lasts_ = pd.Series.to_frame(lasts.loc[:,'user']) # using 'user' column to initialize copy of new dataframe. to_frame gives dataframe instead of series so more columns can be added below
lasts_['elapsed_days'] = lasts.loc[:,'elapsed_time'] * lasts.loc[:,'num_cores'] / 86400
lasts_['running_days'] = lasts.loc[:,'running_time'] * lasts.loc[:,'num_cores'] / 86400
# aggregate
by_user = lasts_.groupby('user').agg({'elapsed_days': 'sum',
'running_days': 'sum' })
# by_user:
# user elapsed_days running_days
# james 41.66666666666667 18.51851851851852
# john 157.4074074074074 120.37037037037037
Если вы хотите сохранить "пользователь" как обычный столбец вместо столбца индекса, используйте:
by_user = lasts_.groupby('user', as_index=False).agg({'elapsed_days': 'sum',
'running_days': 'sum'})