Как избежать чрезмерных лямбда-функций в pandas DataFrame назначать и применять цепочки методов - PullRequest
4 голосов
/ 16 апреля 2020

Я пытаюсь перевести поток манипуляций на кадре данных в R в его Python эквивалент. Базовый c пример конвейера выглядит следующим образом: несколько вызовов mutate и filter:

library(tidyverse)

calc_circle_area <- function(diam) pi / 4 * diam^2
calc_cylinder_vol <- function(area, length) area * length

raw_data <- tibble(cylinder_name=c('a', 'b', 'c'), length=c(3, 5, 9), diam=c(1, 2, 4))

new_table <- raw_data %>% 
  mutate(area = calc_circle_area(diam)) %>% 
  mutate(vol = calc_cylinder_vol(area, length)) %>% 
  mutate(is_small_vol = vol < 100) %>% 
  filter(is_small_vol)

Я могу повторить это в pandas без особых проблем, но обнаружу, что это включает в себя несколько вложенных lambda вызовов при использовании assign для выполнения apply (сначала, когда вызывающий объект dataframe является аргументом, а затем со строками dataframe в качестве аргумента). Это имеет тенденцию затенять смысл вызова присваивания, где я хотел бы указать что-то большее в точку (например, версию R), если это вообще возможно.

import pandas as pd
import math

calc_circle_area = lambda diam: math.pi / 4 * diam**2
calc_cylinder_vol = lambda area, length: area * length

raw_data = pd.DataFrame({'cylinder_name': ['a', 'b', 'c'], 'length': [3, 5, 9], 'diam': [1, 2, 4]})

new_table = (
    raw_data
        .assign(area=lambda df: df.diam.apply(lambda r: calc_circle_area(r.diam), axis=1))
        .assign(vol=lambda df: df.apply(lambda r: calc_cylinder_vol(r.area, r.length), axis=1))
        .assign(is_small_vol=lambda df: df.vol < 100)
        .loc[lambda df: df.is_small_vol]
)

Я знаю, что .assign(area=lambda df: df.diam.apply(calc_circle_area)) можно записать как .assign(area=raw_data.diam.apply(calc_circle_area)), но только потому, что столбец diam уже существует в исходном кадре данных, что может не всегда иметь место.

Я также понимаю, что функции calc_... здесь векторизуемы, что означает Я также мог бы делать такие вещи, как

.assign(area=lambda df: calc_circle_area(df.diam))
.assign(vol=lambda df: calc_cylinder_vol(df.area, df.length))

, но опять же, поскольку большинство функций не векторизовано, в большинстве случаев это не сработало бы.

TL; DR Мне интересно, есть ли более чистый способ «мутировать» столбцы в кадре данных, который не включает в себя двойные вложенные операторы lambda, например, что-то вроде:

.assign(vol=lambda df: df.apply(lambda r: calc_cylinder_vol(r.area, r.length), axis=1))

Существуют ли передовые практики для приложений такого типа или это лучшее, что можно сделать в контексте цепочки методов?

Ответы [ 2 ]

6 голосов
/ 16 апреля 2020

Лучшая практика - это векторизация операций.

Причина этого - производительность, потому что apply очень медленный. Вы уже пользуетесь векторизацией в коде R, и вы должны продолжать делать это в Python. Из этого соображения производительности вы обнаружите, что большинство функций, которые вам нужны на самом деле , векторизуемы.

Это избавит вас от ваших внутренних лямбд. Что касается внешних лямбд над df, я думаю, что у вас есть самый чистый образец. Альтернативой является многократное переназначение переменной raw_data или некоторых других промежуточных переменных, но это не соответствует стилю цепочки методов, о котором вы просите.

Также есть Python пакеты типа dfply , которые нацелены на мимику c ощущения dplyr в Python. Они не получают такой же уровень поддержки, как ядро ​​pandas, поэтому имейте это в виду, если вы хотите go этот маршрут.


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

def df_apply(col_fn, *col_names):
    def inner_fn(df):
        cols = [df[col] for col in col_names]
        return col_fn(*cols)
    return inner_fn

Тогда использование в конечном итоге выглядит примерно так:

new_table = (
    raw_data
        .assign(area=df_apply(calc_circle_area, 'diam'))
        .assign(vol=df_apply(calc_cylinder_vol, 'area', 'length'))
        .assign(is_small_vol=lambda df: df.vol < 100)
        .loc[lambda df: df.is_small_vol]
)

Можно также написать это, не используя векторизацию, на случай, если это произойдет.

def df_apply_unvec(fn, *col_names):
    def inner_fn(df):
        def row_fn(row):
            vals = [row[col] for col in col_names]
            return fn(*vals)
        return df.apply(row_fn, axis=1)
    return inner_fn

Я использовал именованные функции для большей ясности. Но это может быть сжато с лямбдами в нечто похожее на ваш оригинальный формат, просто generi c.

2 голосов
/ 16 апреля 2020

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

import math

def area(df):
    df['area'] = math.pi/4*df['diam']**2
    return df

def vol(df):
    df['vol'] = df['area'] * df['length']
    return df

new_table = (raw_data
             .pipe(area)
             .pipe(vol)
             .assign(is_small_vol = lambda df: df.vol < 100)
             .loc[lambda df: df.is_small_vol]
             )

new_table

    cylinder_name   length  diam    area     vol    is_small_vol
0       a             3      1    0.785398  2.356194    True
1       b             5      2    3.141593  15.707963   True
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...