Панды: применить функцию с массивом 2 в качестве ввода и вернуть единственное значение

У меня есть DataFrame панды с иерархическими именами столбцов, как это

import pandas as pd
import numpy as np

np.random.seed(1542)

dates = pd.date_range('29/01/17', periods = 6)

pd.DataFrame(np.random.randn(6,6), index = dates,\
             columns = [['g1', 'g1', 'g1', 'g2', 'g2', 'g2'],\
                        ['c1', 'c2', 'c3', 'c1', 'c2', 'c3']])

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

Примером функции (которая в реальном случае является более сложной) может быть

def function(first_column, second_column):
    return(max(first_column) - max(second_column))

Когда я применяю его к моему DataFrame, я хочу получить обратно DataFrame, который сообщает мне вывод 'function' для каждой группы, поэтому, в данном случае, только 2 числа для 'g1' и 'g2'.

Обратите внимание, что я хочу, чтобы он работал также в случае gorupby(), чтобы в этом случае я получил результат функции для каждой группы ('g1' и 'g2') и для каждого подгруппы groupby.

Для случая выше, если я хочу агрегировать по месяцам, результат должен быть:

         g1      g2                    
1  0.909464     1.638375
2  0.698515     0.33819

2 ответа

Решение

Спасибо Jezrael за ваш полезный вклад. Основываясь на этом, я написал решение проблемы: примените сложную функцию, которая принимает два или более массивов в качестве входных данных, и возвращает одно значение, и примените его к кадру данных с иерархическими именами столбцов вместе с повторной выборкой на основе индексации по времени и дате.

Во-первых, вот таблица, которую я буду использовать для примера

mat = np.random.randint(0, 101, size = (10, 6))

index = pd.date_range(start = '25 Jan 2018', periods = 10)

first_column_name = ['Group1']*3 + ['Group2']*3
second_column_name = ['Col1', 'Col2', 'Col3']*2

df = pd.DataFrame(mat, index = index, columns = [first_column_name,\
                                                 second_column_name])

           Group1           Group2          
             Col1 Col2 Col3   Col1 Col2 Col3
2018-01-25     11   36   80     88   31   33
2018-01-26     30   32   61     53   55   43
2018-01-27     64   26   21     63   33   93
2018-01-28     52   59   23     54   91   60
2018-01-29     93   88   27     16   88    7
2018-01-30     28   76   48      5   38    1
2018-01-31      7   29   45     86   53   96
2018-02-01     18   89   69      3   34   34
2018-02-02      0    7   94     99    5   68
2018-02-03     29   13   98     25   51   44

Теперь я хочу применить функцию:

def my_fun(arr1, arr2):

    arr1 = np.array(arr1)
    arr2 = np.array(arr2)
    tmp = np.abs(arr1 - arr2)
    return(np.sum(tmp))

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

Когда я применяю функцию к 'Col1' 'Col 3', желаемым результатом будет следующее:

            Group1  Group2
2018-01-31     296     124
2018-02-28     214      81

Для этого я применил немного объектно-ориентированного программирования, чтобы объединить повторную выборку с groupby.

Итак, я создал этот класс

class ApplyFunction():

    def __init__(self, column_names, fun, resample = None):
        self.cn = column_names
        self.fun  = fun
        self.resample = resample

        # Initialize the stored values
        self.stored_values = dict()
        for name in self.cn:
            self.stored_values[name] = []

    def __store(self, x):
        self.stored_values[self.to_store].append(x.values.copy())

    def wrapper_with_resample(self, x):

        if self.resample is None:
            print('Can not use this function with resample = None')
            return np.nan

        # Get the names of the group
        group_name = x.columns.levels[0][x.columns.labels[0][0]]

        # Get the time-steps output of resample (doing a dumm operation)
        self.timesteps = x.resample(self.resample).apply(lambda x : len(x)).index

        # Store the resampled variables
        for name in self.cn:
            self.to_store = name
            x[(group_name, name)].resample(self.resample).apply(self.__store)

        # Create a new DataFrame for the output
        out = []
        for i in range(len(self.timesteps)):
            out.append(self.fun(*[self.stored_values[name][i] for name in self.cn]))
        out = pd.Series(out, index = self.timesteps)

        # Reset self.stored_values
        for name in self.cn:
            self.stored_values[name] = []        
        return out

И тогда я использую это следующим образом:

f = ApplyFunction(column_names = ['Col1', 'Col3'], fun = my_fun, resample = 'M')

output = df.groupby(level = 0, axis = 1).apply(f.wrapper_with_resample)

Это решение было сделано, потому что здесь мы хотим применить групповую обработку и повторную выборку, и я не нашел подходящего решения в пандах.

Я надеюсь, что это решение кому-то пригодится; Конечно, есть возможности для улучшения, поэтому не стесняйтесь размещать альтернативные и более эффективные решения!

Благодарю. Marco

Я думаю тебе нужно groupby по первому уровню MultiIndex с пользовательской функцией с xs для выбора второго уровня MultiIndex:

np.random.seed(1542)

df = pd.DataFrame(np.random.randn(6,6), index = range(6),\
             columns = [['g1', 'g1', 'g1', 'g2', 'g2', 'g2'],\
                        ['c1', 'c2', 'c3', 'c1', 'c2', 'c3']])
print (df)
         g1                            g2                    
         c1        c2        c3        c1        c2        c3
0 -0.556376 -0.295627  0.618673 -0.409434  0.107020 -1.143460
1 -0.145909  0.017417  0.117667 -0.301128  0.880918 -1.027282
2  2.287448  1.528137 -1.528636  0.052728 -1.842634 -0.757457
3 -0.651587 -1.075176  1.128277  0.632036 -0.240965  0.421812
4 -1.620718  0.146108  0.030500 -0.446294 -0.206774  0.819859
5 -0.757296  1.826793 -0.352837 -2.048026  1.362865  1.024671

def f(x):
    a = x.xs('c2', axis=1, level=1)[x.name].max()
    b = x.xs('c3', axis=1, level=1)[x.name].max()
    #print (a)
    return a - b

s = df.groupby(level=0, axis=1).apply(f)
print (s)
g1    0.698516
g2    0.338194
dtype: float64

Аналогичное решение:

def f(x):
    a = x.xs('c2', axis=1, level=1).squeeze()
    b = x.xs('c3', axis=1, level=1).squeeze()
    return a.max() - b.max()

a = df.groupby(level=0, axis=1).apply(f)
print (a)
g1    0.698516
g2    0.338194
dtype: float64

РЕДАКТИРОВАТЬ:

def f(x):
    a = x.xs('c2', axis=1, level=1)[x.name]
    b = x.xs('c3', axis=1, level=1)[x.name]
    #print (a)
    return a - b

s = df.resample('M').max().groupby(level=0, axis=1).apply(f)
print (s)
                  g1        g2
2017-01-31  0.909464  1.638375
2017-02-28  0.698516  0.338194

print (df.resample('M').max())
                  g1                            g2                    
                  c1        c2        c3        c1        c2        c3
2017-01-31  2.287448  1.528137  0.618673  0.052728  0.880918 -0.757457
2017-02-28 -0.651587  1.826793  1.128277  0.632036  1.362865  1.024671

EDIT1:

Решение должно быть упрощено больше:

a = df.resample('M').max()
b = a.xs('c2', axis=1, level=1)
c = a.xs('c3', axis=1, level=1)
d = b - c
print (d)
                  g1        g2
2017-01-31  0.909464  1.638375
2017-02-28  0.698516  0.338194
Другие вопросы по тегам