Pandas гибкое определение метрик - PullRequest
3 голосов
/ 07 марта 2020

Представьте, что у нас есть различные структуры фреймов данных в Pandas

# creating the first dataframe 
df1 = pd.DataFrame({
  "width": [1, 5], 
  "height": [5, 8]})

# creating second dataframe
df2 = pd.DataFrame({
  "a": [7, 8], 
  "b": [11, 23],
  "c": [1, 3]})

# creating second dataframe
df3 = pd.DataFrame({
  "radius": [7, 8], 
  "height": [11, 23]})

В общем, может быть более 2 фреймов данных. Теперь я хочу создать логи c, которые сопоставляют имена столбцов с указанными c функциями для создания нового столбца «metri c» (представьте, что это область для двух столбцов и том для 3 столбцов). Я хочу указать ансамбли имен столбцов

column_name_ensembles = {
    "1": {
       "ensemble": ['height', 'width'],
       "method": area},
    "2": {
       "ensemble": ['a', 'b', 'c'],
       "method": volume_cube},
    "3": {
       "ensemble": ['radius', 'height'],
       "method": volume_cylinder}}

def area(width, height):
    return width * height

def volume_cube(a, b, c):
    return a * b * c

def volume_cylinder(radius, height):
    return (3.14159 * radius ** 2) * height

Теперь функция области создаст новый столбец для фрейма данных df1['metric'] = df1['height'] * df2['widht'], а функция объем создаст новый столбец для фрейма данных df2['metic'] = df2['a'] * df2['b'] * df2['c']. Обратите внимание, что функции могут иметь произвольную форму, но она принимает ансамбль в качестве параметров. Желаемая функция metric(df, column_name_ensembles) должна принять произвольный фрейм данных в качестве входных данных и решить путем проверки имен столбцов, какую функцию следует применять.

Пример поведения ввода-вывода

df1_with_metric = metric(df1, column_name_ensembles)
print(df1_with_metric)
# output
#    width height metric
#  0 1     5      5 
#  1 5     8      40
df2_with_metric = metric(df2, column_name_ensembles)
print(df2_with_metric)
# output
#    a  b  c  metric
#  0 7  11 1  77
#  1 8  23 3  552
df3_with_metric = metric(df3, column_name_ensembles)
print(df3_with_metric)
# output
#    radius  height  metric
#  0 7       11      1693.31701
#  1 8       23      4624.42048

Идеальным решением будет функция, которая принимает фрейм данных и column_name_ensembles в качестве параметров и возвращает фрейм данных с соответствующим 'metri c', добавленным к нему.

Я знаю, что это может быть достигнуто несколькими операторами if и else, но это не кажется самым разумным решением. Возможно, существует шаблон проектирования, который может решить эту проблему, но я не эксперт в шаблонах проектирования.

Спасибо, что прочитали мой вопрос! Я с нетерпением жду ваших замечательных ответов.

Ответы [ 5 ]

2 голосов
/ 16 марта 2020

Вы можете использовать модуль inspect для автоматического извлечения имен параметров, а затем сопоставить frozenset имен параметров непосредственно с метри c функциями:

import inspect

metrics = {
    frozenset(inspect.signature(f).parameters): f
    for f in (area, volume_cube, volume_cylinder)
}

Затем для для данного фрейма данных, если все столбцы гарантированно являются аргументами для соответствующего метри c, вы можете просто запросить этот словарь:

def apply_metric(df, metrics):
    metric = metrics[frozenset(df.columns)]
    args = tuple(df[p] for p in inspect.signature(metric).parameters)
    df['metric'] = metric(*args)
    return df

В случае, если во фрейме входных данных больше столбцов, чем требуется для функция metri c, с помощью которой вы можете установить пересечение для поиска соответствующего метри c:

def apply_metric(df, metrics):
    for parameters, metric in metrics.items():
        if parameters & set(df.columns) == parameters:
            args = tuple(df[p] for p in inspect.signature(metric).parameters)
            df['metric'] = metric(*args)
            break
    else:
        raise ValueError(f'No metric found for columns {df.columns}')
    return df
0 голосов
/ 16 марта 2020

Вот интересный способ сделать это, используя pandas методы ( Подробности ниже )

def metric(dataframe,column_name_ensembles):
    func_df = pd.DataFrame(column_name_ensembles).T
    func_to_apply = func_df.loc[func_df['ensemble'].map(dataframe.columns.difference)
                        .str.len().eq(0),'method'].iat[0]
    return dataframe.assign(metric=dataframe.apply(lambda x: func_to_apply(**x),axis=1))

print(metric(df1,column_name_ensembles),'\n')
print(metric(df2,column_name_ensembles),'\n')
print(metric(df3,column_name_ensembles))

   width  height  metric
0      1       5       5
1      5       8      40 

   a   b  c  metric
0  7  11  1      77
1  8  23  3     552 

   radius  height      metric
0       7      11  1693.31701
1       8      23  4624.42048

Подробнее:

func_df = pd.DataFrame(column_name_ensembles).T

Это создает фрейм данных имен столбцов и связанных с ними методов, как показано ниже:

          ensemble                                            method
1   [height, width]             <function area at 0x000002809540F9D8>
2         [a, b, c]      <function volume_cube at 0x000002809540F950>
3  [radius, height]  <function volume_cylinder at 0x000002809540FF28>

Используя этот фрейм данных, мы находим строку, в которой разница названий столбцов переданного фрейма данных и список столбцов в ансамбле равен 0 с использованием pd.Index.difference, series.map, series.str.len и series.eq()

func_df['ensemble'].map(df1.columns.difference)

1                     Index([], dtype='object') <- Row matches the df columns completely
2    Index(['height', 'width'], dtype='object')
3              Index(['width'], dtype='object')
Name: ensemble, dtype: object

func_df['ensemble'].map(df1.columns.difference).str.len().eq(0)
1     True
2    False
3    False

Далее, где True, мы выбираем функцию в столбце method

func_df.loc[func_df['ensemble'].map(df1.columns.difference)
                            .str.len().eq(0),'method'].iat[0]
#<function __main__.area(width, height)>

, используя apply и df.assign мы создаем новую строку с возвращенной копией переданного фрейма данных.

0 голосов
/ 09 марта 2020

Функция, которая запускает модель, должна быть достаточно гибкой. Предполагая, что вычисления всегда будут ограничены данными в одной строке, это, вероятно, сработает.

Сначала я изменил функции, чтобы использовать общий ввод. Я добавил треугольную область cal c, чтобы убедиться, что она была расширяемой.

#def area(width, height):
#    return width * height

def area(row):
    return row['width'] * row['height']

#def volume_cube(a, b, c):
#    return a * b * c

def volume_cube(row):
    return row['a'] * row['b'] * row['c']

#def volume_cylinder(radius, height):
#    return (3.14159 * radius ** 2) * height

def volume_cylinder(row):
    return (3.14159 * row['radius'] ** 2) * row['height']

def area_triangle(row):
    return 0.5 * row['width'] * row['height']

Это позволяет нам использовать одно и то же приложение для всех функций. Поскольку я немного прав, я изменил имена ключей в справочном словаре.

column_name_ensembles = {
    "area": {
       "ensemble": ['width', 'height'],
       "method": area},
    "volume_cube": {
       "ensemble": ['a', 'b', 'c'],
       "method": volume_cube},
    "volume_cylinder": {
       "ensemble": ['radius', 'height'],
       "method": volume_cylinder},
    "area_triangle": {
       "ensemble": ['width', 'height'],
       "method": area_triangle},
    }

Функция metri c тогда применима к df. Вы должны указать функцию, на которую вы нацеливаетесь в этой версии, но вы можете определить метод ансамбля на основе столбцов. Эта версия обеспечивает доступность необходимых столбцов.

def metric(df,method_id):
    source_columns = list(df.columns)
    calc_columns = column_name_ensembles[method_id]['ensemble']
    if all(factor in source_columns for factor in calc_columns):
        df['metric'] = df.apply(lambda row: column_name_ensembles[method_id]['method'](row),axis=1)
        return df
    else:
        print('Column Mismatch')

Затем можно указать фрейм данных и метод ансамбля.

df1_with_metric = metric(df1,'area')
df2_with_metric = metric(df2,'volume_cube')
df3_with_metric = metric(df3,'volume_cylinder')
df1_with_triangle_metric = metric(df1,'area_triangle')
0 голосов
/ 13 марта 2020

Решение

Идея состоит в том, чтобы сделать функцию максимально обобщенной c . Чтобы сделать это, вы должны положиться на df.apply, используя axis=1, чтобы применить строку функции.

Функция будет:

def method(df, ensembles):

    # To avoid modifying the original dataframe
    df = df_in.copy()

    for data in ensembles.values():
        if set(df.columns) == set(data["ensemble"]):
            df["method"] = df.apply(lambda row: data["method"](**row), axis=1)
            return df

Почему это всегда работает?

Это можно применить даже к функциям, которые не будут работать со всем столбцом.

Например:

df = pd.DataFrame({
    "a": [1, 2], 
    "b": [[1, 2], [3, 4]],
})

def a_in_b(a, b):
    return a in b

# This will work
df.apply(lambda row: a_in_b(**row), axis=1)

# This won't
a_in_b(df["a"], df["b"])
0 голосов
/ 09 марта 2020
def metric(df, column_name_ensembles):

    df_cols_set = set(df.columns)
    # if there is a need to overwrite the previously calculated 'metric' column
    df_cols_set.discard('metric')

    for column_name_ensemble in column_name_ensembles.items():

        # pick up the first `column_name_ensemble` dictionary 
        # with 'ensemble' matching the df columns 
        # (excluding 'metric' column, if present)
        # comparing `set` if order of column names 
        # in ensemble does not matter (as per your df1 example), 
        # else can compare `list`
        if df_cols_set == set(column_name_ensemble[1]['ensemble']):
            df['metric'] = column_name_ensemble[1]['method'](**{col: df[col] for col in df_cols_set})
            break

    # if there is a match, return df with 'metric' calculated
    # else, return original df untouched
    return df
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...