【问题标题】:How to transform columns with method chaining?如何使用方法链接转换列?
【发布时间】:2022-06-21 13:36:43
【问题描述】:

在 Pandas 中转换列最流畅(或易于阅读)method chaining 的解决方案是什么?

(“方法链接”或“流利的”是coding style made popular by Tom Augspurger 等等。)

为了示例,我们设置一些示例数据:

import pandas as pd
import seaborn as sns

df = sns.load_dataset("iris").astype(str)  # Just for this example
df.loc[1, :] = "NA"

df.head()
# 
#   sepal_length sepal_width petal_length petal_width species
# 0          5.1         3.5          1.4         0.2  setosa
# 1           NA          NA           NA          NA      NA
# 2          4.7         3.2          1.3         0.2  setosa
# 3          4.6         3.1          1.5         0.2  setosa
# 4          5.0         3.6          1.4         0.2  setosa

仅针对此示例:我想通过函数映射某些列 - sepal_length 使用 pd.to_numeric - 同时保持其他列不变。在方法链样式中最简单的方法是什么?

我已经可以使用assign,但我在这里重复列名,这是我不想要的。

new_result = (
        df.assign(sepal_length = lambda df_: pd.to_numeric(df_.sepal_length, errors="coerce"))
          .head()  # Further chaining methods, what it may be
    )

我可以使用变换,但变换会丢弃(!)未提及的列。对其他列进行直通转换是理想的:

# Columns not mentioned in transform are lost
new_result = (
        df.transform({'sepal_length': lambda series: pd.to_numeric(series, errors="coerce")})
          .head()  # Further chaining methods...
    )

是否有一种“最佳”方式以流畅的风格将转换应用于某些列,并传递其他列?


编辑:在此行下方,阅读 Laurent 的想法后提出的建议。

添加一个帮助函数,允许仅将映射应用于一列:

import functools

coerce_numeric = functools.partial(pd.to_numeric, errors='coerce')

def on_column(column, mapping):
    """
    Adaptor that takes a column transformation and returns a "whole dataframe" function suitable for .pipe()
    
    Notice that columns take the name of the returned series, if applicable
    Columns mapped to None are removed from the result.
    """
    def on_column_(df):
        df = df.copy(deep=False)
        res = mapping(df[column])
        # drop column if mapped to None
        if res is None:
            df.pop(column)
            return df
        df[column] = res
        # update column name if mapper changes its name
        if hasattr(res, 'name') and res.name != col:
            df = df.rename(columns={column: res.name})
        return df
    return on_column_

这现在允许在前面的示例中进行以下整洁的链接:

new_result = (
        df.pipe(on_column('sepal_length', coerce_numeric))
          .head()  # Further chaining methods...
    )

但是,我仍然愿意在没有胶水代码的情况下仅在原生 pandas 中执行此操作。


编辑 2 以进一步适应 Laurent 的想法,作为替代方案。独立示例:

import pandas as pd

df = pd.DataFrame(
    {"col1": ["4", "1", "3", "2"], "col2": [9, 7, 6, 5], "col3": ["w", "z", "x", "y"]}
)

def map_columns(mapping=None, /, **kwargs):
    """
    Transform the specified columns and let the rest pass through.
    
    Examples:
    
        df.pipe(map_columns(a=lambda x: x + 1, b=str.upper))
        
        # dict for non-string column names
        df.pipe({(0, 0): np.sqrt, (0, 1): np.log10})
    """
    if mapping is not None and kwargs:
        raise ValueError("Only one of a dict and kwargs can be used at the same time")
    mapping = mapping or kwargs
    
    def map_columns_(df: pd.DataFrame) -> pd.DataFrame:
        mapping_funcs = {**{k: lambda x: x for k in df.columns}, **mapping}
        # preserve original order of columns
        return df.transform({key: mapping_funcs[key] for key in df.columns})
    return map_columns_


df2 = (
    df
    .pipe(map_columns(col2=pd.to_numeric))
    .sort_values(by="col1")
    .pipe(map_columns(col1=lambda x: x.astype(str) + "0"))
    .pipe(map_columns({'col2': lambda x: -x, 'col3': str.upper}))
    .reset_index(drop=True)
)

df2

#   col1    col2    col3
# 0     10  -7  Z
# 1     20  -5  Y
# 2     30  -6  X
# 3     40  -9  W

【问题讨论】:

  • this 问题和几个答案有帮助吗?
  • 谢谢,但它并没有真正解决一般的流畅列转换
  • 我以为这就是您要的。将函数(例如 pd.to_numeric)应用于多个列(特定列,但不是全部)。在我发送的链接中,他们确实做到了这一点,要么列出要选择的列,然后使用带有axis = 1的df[cols].applycol in cols,或者他们将相同类型的列组合在一起。在其他列维护的所有解决方案中。也许我没有得到“流利的列转换”这个词,那么我很抱歉:P
  • 我为方法链添加了一个链接。以这种方式找到解决方案是问题的主要焦点。我知道如何解决一般问题,您链接中的解决方案肯定有帮助,但不是我所追求的。

标签: pandas method-chaining


【解决方案1】:

这是我对你有趣问题的看法。

我不知道在 Pandas 中进行方法链接的方式比组合 pipeassigntransform 更惯用。但我知道“对其他列进行直通转换是理想的”。

因此,我建议将它与高阶函数一起使用来处理其他列,通过利用 Python 标准库 functools 模块进行更多类似函数的编码。

例如,使用以下玩具数据框:

df = pd.DataFrame(
    {"col1": ["4", "1", "3", "2"], "col2": [9, 7, 6, 5], "col3": ["w", "z", "x", "y"]}
)

您可以定义以下partial object

from functools import partial
from typing import Any, Callable
import pandas as pd

def helper(df: pd.DataFrame, col: str, method: Callable[..., Any]) -> pd.DataFrame:
    funcs = {col: method} | {k: lambda x: x for k in df.columns if k != col}
    # preserve original order of columns
    return {key: funcs[key] for key in df.columns}

on = partial(helper, df)

然后做各种链式赋值,例如:

df = (
    df
    .transform(on("col1", pd.to_numeric))
    .sort_values(by="col1")
    .transform(on("col2", lambda x: x.astype(str) + "0"))
    .transform(on("col3", str.upper))
    .reset_index(drop=True)
)

print(df)
# Ouput
   col1 col2 col3
0     1   70    Z
1     2   50    Y
2     3   60    X
3     4   90    W

【讨论】:

  • 很好的答案! @Laurent 我真的很喜欢部分方法。
  • 有趣的想法!你介意我把它改成我想用的东西吗?我想看看我是否可以避免部分(数据框应该是在链中的点,而不是在开始时冻结) - 并且最好始终保持列的顺序 - 以 == 顺序排列外出对我来说很重要。
  • 谢谢@ShubhamSharma。 @creanion我已经编辑了我的答案以调整辅助函数,以便保留列的顺序。你可以不用partial,主要是为了避免重复 df,在你寻求的 DRY 精神中。而且由于 df 是一个可变对象,因此在初始部分分配之后它并没有真正“冻结”。但这取决于你。干杯。
  • df 不会反映方法链的结果,在链中的那个点,所以它不适用于所有流畅的代码。没什么私人的,只是看看这里的最终目标。在您的启发下,我在问题中发布了另一个帮助函数。
  • 对。这越来越有趣了,干杯!
【解决方案2】:

如果我正确理解了这个问题,也许在 assign 中使用 ** 会有所帮助。例如,如果您只想使用 pd.to_numeric 转换数字数据类型,则应该可以使用以下方法。

df.assign(**df.select_dtypes(include=np.number).apply(pd.to_numeric,errors='coerce'))

通过解压缩 df,您实际上是在为分配每列分配所需的内容。这相当于为每一列写sepal_length = pd.to_numeric(df['sepal_length'],errors='coerce'), sepal_width = ...

【讨论】:

    猜你喜欢
    • 2021-01-30
    • 2013-07-20
    • 1970-01-01
    • 2022-06-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-02-27
    • 1970-01-01
    相关资源
    最近更新 更多