Python (pandas): использование декораторов с использованием pandas API

Я новичок в декораторах и классах в Python в целом, но у меня есть вопрос, есть ли лучший способ украсить объекты панд. В качестве примера я написал следующее для создания двух методов - lisa и wil:

import numpy as np
import pandas as pd

test = np.array([['john', 'meg', 2.23, 6.49],
       ['lisa', 'wil', 9.67, 8.87],
       ['lisa', 'fay', 3.41, 5.04],
       ['lisa', 'wil', 0.58, 6.12],
       ['john', 'wil', 7.31, 1.74]],
)
test = pd.DataFrame(test)
test.columns = ['name1','name2','scoreA','scoreB']

@pd.api.extensions.register_dataframe_accessor('abc')
class ABCDataFrame:

    def __init__(self, pandas_obj):
        self._obj = pandas_obj

    @property
    def lisa(self):
        return self._obj.loc[self._obj['name1'] == 'lisa']
    @property
    def wil(self):
        return self._obj.loc[self._obj['name2'] == 'wil']

Пример вывода выглядит следующим образом:

test.abc.lisa.abc.wil
  name1 name2 scoreA scoreB
1  lisa   wil   9.67   8.87
3  lisa   wil   0.58   6.12

У меня два вопроса.

Во-первых, на практике я создаю намного больше, чем два метода, и мне нужно вызывать многие из них в одной строке. Есть ли способ получитьtest.lisa.wil чтобы вернуть тот же результат, что и выше, где я написал test.abc.lisa.abc.wil, поскольку первое избавит меня от необходимости вводить abc каждый раз?

Во-вторых, если есть какие-либо другие предложения / ресурсы по украшению pandas DataFrames, пожалуйста, дайте мне знать.

3 ответа

Решение

Вы можете сделать это с помощью библиотеки pandas-flavour, которая позволяет расширитьDataFrame класс с дополнительными методами.

import pandas as pd
import pandas_flavor as pf

# Create test DataFrame as before.
test = pd.DataFrame([
    ['john', 'meg', 2.23, 6.49],
    ['lisa', 'wil', 9.67, 8.87],
    ['lisa', 'fay', 3.41, 5.04],
    ['lisa', 'wil', 0.58, 6.12],
    ['john', 'wil', 7.31, 1.74]
], columns=['name1', 'name2', 'scoreA', 'scoreB'])

# Register new methods.
@pf.register_dataframe_method
def lisa(df):
    return df.loc[df['name1'] == 'lisa']

@pf.register_dataframe_method
def wil(df):
    return df.loc[df['name2'] == 'wil']

Теперь их можно рассматривать как методы без промежуточных .abc аксессуар.

test.lisa()                                                                                                                                                                                                                         
#   name1 name2  scoreA  scoreB
# 1  lisa   wil    9.67    8.87
# 2  lisa   fay    3.41    5.04
# 3  lisa   wil    0.58    6.12

test.lisa().wil()                                                                                                                                                                                                                   
#   name1 name2  scoreA  scoreB
# 1  lisa   wil    9.67    8.87
# 3  lisa   wil    0.58    6.12

Обновить

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

def add_method(key, val, fn_name=None):  
    def fn(df):
        return df.loc[df[key] == val]

    if fn_name is None:
        fn_name = f'{key}_{val}'

    fn.__name__ = fn_name
    fn = pf.register_dataframe_method(fn)
    return fn

for name1 in ['john', 'lisa']:
    add_method('name1', name1)

for name2 in ['fay', 'meg', 'wil']:
    add_method('name2', name2)

Затем они становятся доступными как методы, как если бы вы определили методы напрямую. Обратите внимание, что я поставил перед именем столбца (name1 или name2), чтобы быть предельно ясным. Это необязательно.

test.name1_john()                                                                                                                                                                                                             
#   name1 name2  scoreA  scoreB
# 0  john   meg    2.23    6.49
# 4  john   wil    7.31    1.74

test.name1_lisa()                                                                                                                                                                                                                   
#   name1 name2  scoreA  scoreB
# 1  lisa   wil    9.67    8.87
# 2  lisa   fay    3.41    5.04
# 3  lisa   wil    0.58    6.12

test.name2_fay()                                                                                                                                                                                                                    
#   name1 name2  scoreA  scoreB
# 2  lisa   fay    3.41    5.04

Обновление 2

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

@pf.register_dataframe_method
def name1(df, val):
    return df.loc[df['name1'] == val]

@pf.register_dataframe_method
def name2(df, val):
    return df.loc[df['name2'] == val]

test.name1('lisa')
#   name1 name2  scoreA  scoreB
# 1  lisa   wil    9.67    8.87
# 2  lisa   fay    3.41    5.04
# 3  lisa   wil    0.58    6.12

test.name1('lisa').name2('wil')
#   name1 name2  scoreA  scoreB
# 1  lisa   wil    9.67    8.87
# 3  lisa   wil    0.58    6.12

Вы можете использовать classчтобы помочь вам. (хотя это не имеет ничего общего с настоящей функцией украшения).

см. следующее:

class DecoratorDF:
    def __init__(self, df: pd.DataFrame, n_layer: int = 0):
        self.df = df
        self.layer = n_layer

    def __repr__(self):
        return str(self.df)

    def __getattr__(self, item):
        layer = self.df.columns[self.layer]
        return DecoratorDF(self.df.loc[self.df[layer] == item], self.layer + 1)


my_df = DecoratorDF(
    pd.DataFrame([['A', 'B', 'C'],
                  ['A', 'B', 'D'],
                  ['E', 'F', 'G'],
                  ], columns=['name1', 'name2', 'name3'])
)

print(my_df.A.B)
print(my_df.A.B.C)
  name1 name2 name3
0     A     B     C
1     A     B     D

  name1 name2 name3
0     A     B     C

Полный пример

import numpy as np
import pandas as pd


class DecoratorDF:
    def __init__(self, df: pd.DataFrame, n_layer: int = 0):
        self.df = df
        self.layer = n_layer

    def __repr__(self):
        return str(self.df)

    def __getattr__(self, item):
        layer = self.df.columns[self.layer]
        return DecoratorDF(self.df.loc[self.df[layer] == item], self.layer + 1)


test_data = np.array([['john', 'meg', 2.23, 6.49],
                      ['lisa', 'wil', 9.67, 8.87],
                      ['lisa', 'fay', 3.41, 5.04],
                      ['lisa', 'wil', 0.58, 6.12],
                      ['john', 'wil', 7.31, 1.74]],
                     )
test_df = pd.DataFrame(test_data, columns=['name1', 'name2', 'scoreA', 'scoreB'])
test_df = DecoratorDF(test_df)
df_lisa_and_wil = test_df.lisa.wil
print(df_lisa_and_wil)

df_lisa_and_wil = df_lisa_and_wil.df
print(df_lisa_and_wil.loc[df_lisa_and_wil['scoreA'] == '9.67'])

  name1 name2 scoreA scoreB
1  lisa   wil   9.67   8.87
3  lisa   wil   0.58   6.12

  name1 name2 scoreA scoreB
1  lisa   wil   9.67   8.87

Если вы хотите получать данные с test.lisa.wil, Я думаю, что использование класса-оболочки более уместно, чем декоратор. Также я лично предпочитаю что-то вродеtest.access(name1='lisa', name2='wil') для доступа к данным.

Вот пример того, как это сделать:

import numpy as np
import pandas as pd

test = np.array([['john', 'meg', 2.23, 6.49],
       ['lisa', 'wil', 9.67, 8.87],
       ['lisa', 'fay', 3.41, 5.04],
       ['lisa', 'wil', 0.58, 6.12],
       ['john', 'wil', 7.31, 1.74]],
)
test = pd.DataFrame(test)
test.columns = ['name1','name2','scoreA','scoreB']

class WrapDataFrame(pd.DataFrame):
    def access(self, **kwargs):
        result = self
        for key, val in kwargs.items():
            result = result.loc[result[key] == val]
        return WrapDataFrame(result)
    @property
    def lisa(self):
        return WrapDataFrame(self.loc[self['name1'] == 'lisa'])
    @property
    def wil(self):
        return WrapDataFrame(self.loc[self['name2'] == 'wil'])

wdf = WrapDataFrame(test)

# First way to access
print(wdf.lisa.wil)

# Second way to access (recommended)
print(wdf.access(name1='lisa', name2='wil'))

# Third way to access (easiest to do programaticaly)
data_filter = {'name1': 'lisa', 'name2': 'wil'}
print(wdf.access(**data_filter))

Обратите внимание, что класс WrapDataFrame наследовать pd.DataFrame, поэтому все операции для фрейма данных pandas должны быть совместимы.

Другие вопросы по тегам