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'})