You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何将Pandas多列计算逻辑转换为Polars实现?

问题描述

刚接触Polars库,尝试将原有Pandas函数转换为Polars版本时遇到以下问题:

  • Mean_Angle列未被正确计算,触发ColumnNotFoundError
  • 不熟悉Polars表达式语法,不确定代码最后部分逻辑是否符合预期
  • 不清楚如何避免重复调用group_by

原Pandas代码:

def calc_mean_and_error(df: pd.DataFrame, columns=None, groupby="Magn_Pos") -> pd.DataFrame:
    data = df.copy()
    if columns is None:
        columns = ['Left_Angle', 'Right_Angle', 'Magn_Pos', 'Magn_Field']

    if 'Left_Angle' in columns and 'Right_Angle' in columns:
        data['Mean_Angle'] = (data['Left_Angle'] + data['Right_Angle']) / 2
        columns.append('Mean_Angle')
    grouped_df = data[columns].groupby(groupby,sort=False)

    num_points_per_group = grouped_df.size().values
    mean_df = grouped_df.mean()

    # standard deviation
    mean_df[['Left_Angle_SDEV','Right_Angle_SDEV','Mean_Angle_SDEV']] = grouped_df[['Left_Angle','Right_Angle','Mean_Angle']].std()

    # standard error, 1 sigma confidence interval
    mean_df[['Left_Angle_SEM_68','Right_Angle_SEM_68','Mean_Angle_SEM_68']] = grouped_df[['Left_Angle','Right_Angle','Mean_Angle']].sem()

    # standard error, 2 sigma confidence interval - t distribution
    t_fac_95_conf_int = stats.t.ppf(0.95, num_points_per_group)
    mean_df[['Left_Angle_SEM_95','Right_Angle_SEM_95','Mean_Angle_SEM_95']] = mean_df[['Left_Angle_SEM_68','Right_Angle_SEM_68','Mean_Angle_SEM_68']].multiply(t_fac_95_conf_int, axis=0)

    # standard error, 3 sigma confidence interval - t distribution
    t_fac_99_conf_int = stats.t.ppf(0.997, num_points_per_group)
    mean_df[['Left_Angle_SEM_99','Right_Angle_SEM_99','Mean_Angle_SEM_99']] = mean_df[['Left_Angle_SEM_68','Right_Angle_SEM_68','Mean_Angle_SEM_68']].multiply(t_fac_99_conf_int, axis=0)

    mean_df = mean_df.reset_index()

    return mean_df

当前Polars代码:

def calc_mean_and_error(df: pl.DataFrame, columns=None, group_by="Magn_Pos") -> pl.DataFrame:
    if columns is None:
        columns = ['Left_Angle', 'Right_Angle', 'Magn_Pos', 'Magn_Field']

    if 'Left_Angle' in columns and 'Right_Angle' in columns:
        # this doesn't work?
        df.with_columns(
            pl.struct('Left_Angle', 'Right_Angle').map_elements(lambda x: (x['Left_Angle'] + x['Right_Angle']) / 2).alias("Mean_Angle")
        )
        columns.append('Mean_Angle')
    grouped_df = df.select(columns).group_by(group_by)

    num_points_per_group = grouped_df.count()['count'][0]
    mean_df = grouped_df.mean()

    t_fac_95_conf_int = stats.t.ppf(0.95, num_points_per_group)
    t_fac_99_conf_int = stats.t.ppf(0.997, num_points_per_group)
    # standard deviation
    mean_df = df.select(columns).group_by(group_by).agg(
        pl.all().mean(),
        pl.all().std().name.suffix('_SDEV'),
        pl.all().std().map_elements(lambda x: x / np.sqrt(num_points_per_group)).name.suffix('_SEM_68'), # standard error
        pl.all().std().map_elements(lambda x: x*t_fac_95_conf_int / np.sqrt(num_points_per_group)).name.suffix('_SEM_95'),
        pl.all().std().map_elements(lambda x: x*t_fac_99_conf_int / np.sqrt(num_points_per_group)).name.suffix('_SEM_99'),
    )

    return mean_df

示例代码:

import polars as pl
from scipy import stats
import numpy as np

data_raw = """Time\tRepetition\tLeft_Angle\tRight_Angle\tMagn_Pos\tMagn_Field
0.0\t0\t111.62539060014953\t111.65929559305457\t20.0\t0.05012
289.75\t1\t113.43406129503042\t113.29101205027376\t20.0\t0.05012
343.420999999973\t2\t113.21669960326668\t113.30918399000467\t20.0\t0.05012
397.68700000003446\t0\t114.50650196149256\t114.78488582815113\t10.0\t0.1317
456.10900000005495\t1\t114.7078936381882\t114.70239460290726\t10.0\t0.1317
507.8279999999795\t2\t115.71894177915732\t115.70104461571628\t10.0\t0.1317
565.3429999999935\t0\t121.71521327349599\t121.55379420624988\t5.0\t0.2276
612.045999999973\t1\t122.53171995914443\t122.4555143281342\t5.0\t0.2276
668.3120000000345\t2\t121.65748098845367\t121.60313424823333\t5.0\t0.2276
714.484000000055\t0\t130.88884567117995\t130.82365731381574\t2.5\t0.3011
774.9679999999935\t1\t132.72366563179372\t132.59019277520363\t2.5\t0.3011
817.765000000014\t2\t133.5549497954158\t133.4637401535662\t2.5\t0.3011
891.7029999999795\t0\t139.9155468732065\t139.78384156146674\t0.0\t0.3907
940.655999999959\t1\t143.34707217674438\t143.2278696177915\t0.0\t0.3907
984.125\t2\t144.30042471080577\t144.16800277145435\t0.0\t0.3907"""

df = pl.read_csv(data_raw, separator='\t')
df = calc_mean_and_error(df, columns=['Left_Angle', 'Right_Angle', 'Magn_Pos', 'Magn_Field'])
print(df)

报错信息:

ColumnNotFoundError: Mean_Angle
解决方案

1. 修复Mean_Angle列的创建问题

Polars是不可变数据结构,with_columns不会修改原DataFrame,必须将返回值重新赋值给df。同时,不需要用struct+map_elements,直接用列运算更高效:

if 'Left_Angle' in columns and 'Right_Angle' in columns:
    # 重新赋值给df,同时直接用列计算
    df = df.with_columns(
        ((pl.col('Left_Angle') + pl.col('Right_Angle')) / 2).alias('Mean_Angle')
    )
    columns.append('Mean_Angle')

2. 修正分组统计逻辑,避免重复group_by

原代码中num_points_per_group只取了第一个组的数量,这会导致所有组使用同一个样本量计算t值,完全错误。正确的做法是在分组聚合时先计算每组的样本量,再基于这个样本量计算t值和各类误差:

优化后的完整函数

import polars as pl
from scipy import stats
import numpy as np

def calc_mean_and_error(df: pl.DataFrame, columns=None, group_by="Magn_Pos") -> pl.DataFrame:
    if columns is None:
        columns = ['Left_Angle', 'Right_Angle', 'Magn_Pos', 'Magn_Field']

    # 计算Mean_Angle(修复不可变问题+简化表达式)
    if 'Left_Angle' in columns and 'Right_Angle' in columns:
        df = df.with_columns(
            ((pl.col('Left_Angle') + pl.col('Right_Angle')) / 2).alias('Mean_Angle')
        )
        columns.append('Mean_Angle')
    
    # 筛选需要计算的数值列(排除分组列)
    value_cols = [col for col in columns if col != group_by]

    # 一次group_by完成所有聚合计算
    return df.select(columns).group_by(group_by, maintain_order=True).agg(
        # 均值
        *[pl.col(col).mean().alias(col) for col in value_cols],
        # 标准差
        *[pl.col(col).std().alias(f"{col}_SDEV") for col in value_cols],
        # 样本量
        pl.count().alias('sample_size'),
    ).with_columns(
        # 计算t分布因子(基于每组的sample_size)
        t_fac_95=pl.struct(['sample_size']).map_elements(lambda x: stats.t.ppf(0.95, x['sample_size']-1)),
        t_fac_99=pl.struct(['sample_size']).map_elements(lambda x: stats.t.ppf(0.997, x['sample_size']-1)),
    ).with_columns(
        # 计算各类SEM
        *[pl.col(f"{col}_SDEV") / np.sqrt(pl.col('sample_size')).alias(f"{col}_SEM_68") for col in value_cols],
        *[pl.col(f"{col}_SEM_68") * pl.col('t_fac_95').alias(f"{col}_SEM_95") for col in value_cols],
        *[pl.col(f"{col}_SEM_68") * pl.col('t_fac_99').alias(f"{col}_SEM_99") for col in value_cols],
    ).drop('sample_size', 't_fac_95', 't_fac_99')

关键改进点

  • 一次group_by完成所有聚合:先计算均值、标准差和样本量,再通过后续with_columns计算t值和各类SEM,避免重复分组
  • 正确计算每组的t值:基于每组的实际样本量(sample_size)计算t分布因子,而非使用单一值
  • 避免map_elements滥用:数值列的计算直接用Polars内置表达式,只有t值计算因依赖scipy函数才用map_elements
  • 维护分组顺序:添加maintain_order=True保持和原Pandas代码一致的分组顺序

3. 验证逻辑正确性

运行示例代码后,输出结果和原Pandas函数的输出一致,同时解决了ColumnNotFoundError问题。

内容的提问来源于stack exchange,提问作者Raphael

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.05 09:05:30