pandas 如何使用方法链接转换列?

rm5edbpk  于 2023-01-15  发布在  其他
关注(0)|答案(2)|浏览(134)

在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_numericMap某些列,同时保持其他列不变,在方法链样式中最简单的方法是什么?
我已经可以使用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
    )

我可以使用transform,但是transform会删除(!)未提及的列。Transform对其他列使用passthrough比较理想:

# 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...
    )

有没有一种“最佳”的方法可以流畅地对某些列应用转换,并传递其他列?
编辑:在这一行下面,是阅读劳伦特的想法后的建议。
添加一个helper函数,该函数允许仅对一列应用Map:

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
r1zk6ea1

r1zk6ea11#

以下是我对你这个有趣问题的看法。
在Pandas中,我不知道还有比组合pipeassigntransform更惯用的方法来进行方法链接,但我知道 *“对其他列使用带passthrough的transform将是理想的”。
因此,我建议将它与一个高阶函数一起使用来处理其他列,通过利用Python标准库functools模块来进行更类似函数的编码。
例如,使用以下玩具 Dataframe :

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

可定义以下部分对象:

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)

然后使用transform执行各种链赋值,例如:

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
iklwldmw

iklwldmw2#

如果我理解正确的话,在assign中使用**可能会有帮助,例如,如果你只想使用pd.to_numeric转换数字数据类型,下面的代码应该可以工作。

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

通过解压缩df,你实际上是给assign赋值每列所需的值,这相当于为每列写sepal_length = pd.to_numeric(df['sepal_length'],errors='coerce'), sepal_width = ...

相关问题