当前位置: 首页 > news >正文

Pandas GroupBy深度解析:从语法到数据建模的范式跃迁

1. 项目概述:这不是简单的“分组求和”,而是数据思维的临界点

“Introduction to GroupBy Methods”——光看标题,很多人会下意识划走:又一个Pandas基础教程?讲完df.groupby('col').sum()就收工?我带过三十多期数据分析实战训练营,每年都有至少三分之一的学员卡在这个看似最“入门”的环节上。他们能写出语法正确的代码,却在真实业务中反复栽跟头:销售报表里区域汇总总对不上财务系统;用户行为分析中,新老客留存率算出来是负数;AB测试结果一跑分组统计就报KeyErrorNaN爆炸。问题从来不在groupby函数本身,而在于我们根本没把它当做一个数据操作范式来理解——它不是语法糖,而是把原始数据从“平铺直叙”切换到“维度切片”的开关。

核心关键词“GroupBy”背后,实际承载着三个不可分割的层次:结构层(数据如何被物理重排)、语义层(分组键如何定义业务逻辑边界)、计算层(聚合函数如何与分组上下文耦合)。这三者一旦错位,结果必然失真。比如你用user_id做分组键去算“每个用户的平均订单金额”,表面没问题,但如果数据里存在同一用户在不同时间注册了多个账号(小号),这个“用户”维度就彻底崩了。再比如用pd.cut()生成的区间分组,若未显式设置include_lowest=True,最左端点会被无情丢弃,导致高价值客户群统计缺失。这些坑,官方文档不会写,Stack Overflow的答案往往只解决报错,不解释根源。本文要做的,就是把groupby从“函数调用”还原成“数据建模动作”——它本质上是在用代码定义你的业务世界坐标系:横轴是分组键(谁/什么/何时),纵轴是聚合指标(多少/频次/分布),原点则是原始数据集的每一行记录。适合谁读?刚学完Pandas基础想进阶的新人、常被业务方质疑“数据不准”的分析师、需要把SQL思维迁移到Python的数据工程师,以及所有曾对着groupby().apply()返回的MultiIndex发呆超过五分钟的人。

2. 核心设计思路拆解:为什么必须放弃“先写代码再想逻辑”的惯性?

2.1 分组不是排序,更不是过滤:三者本质差异的物理级解读

很多初学者把groupby等同于“按某列排序后合并同类项”,这是最危险的认知偏差。我们用一个真实电商日志数据集(10万行)做对比实验:

# 场景:统计每个商品类目的平均点击深度(用户从首页到该商品页的跳转次数) # 错误示范:先sort再手动遍历(伪代码) df_sorted = df.sort_values(['category', 'session_id']) # 然后用for循环找连续相同category的块...(此处省略20行易出错代码) # 正确的groupby本质 result = df.groupby('category')['click_depth'].mean()

表面看结果一样,但底层机制天壤之别。sort_values全量内存重排:它把10万行数据按category字母序打乱重组,产生新DataFrame,内存占用瞬时翻倍。而groupby惰性索引映射:它只构建一个哈希表(Hash Table),键是category的唯一值(如"手机"、"电脑"、"配件"),值是对应行在原始DataFrame中的行号索引列表。计算mean()时,Pandas直接根据索引列表从原始内存地址抓取click_depth列的值,全程不复制数据。实测10万行数据,sort_values耗时842ms,groupby仅需63ms——快13倍,且内存峰值低67%。这解释了为什么groupby在大数据场景不可替代:它不是操作数据,而是操作数据的“指针地图”。

提示:当你发现groupby执行缓慢,第一反应不该是换工具,而是检查分组键是否含大量唯一值(如order_id)。哈希表键越多,内存开销越大。此时应先用nunique()探查基数:“df['order_id'].nunique()返回9.8万?别groupby,改用value_counts()agg()更高效。”

2.2 分组键的设计哲学:业务语义 > 技术实现

分组键(grouping key)是groupby的灵魂,但90%的教程只教你怎么传字符串,不教你怎么“想”键。我们拆解三个典型业务场景:

场景1:用户生命周期分组
需求:计算“新客(注册30天内下单)vs 老客(注册超30天)”的复购率。
错误键:df['user_id']→ 这只是用户ID,无法区分新老。
正确键:df['register_date'].apply(lambda x: 'new' if (pd.Timestamp.now() - x).days <= 30 else 'old')
但更优解是用pd.cut()构造时间区间:

bins = [pd.Timestamp('2020-01-01'), pd.Timestamp('2023-01-01'), pd.Timestamp('2024-01-01')] labels = ['old', 'new'] df['customer_type'] = pd.cut(df['register_date'], bins=bins, labels=labels, include_lowest=True)

这里include_lowest=True是生死线——没有它,2020-01-01注册的用户会被归为NaN,直接消失。

场景2:地理层级动态聚合
需求:按“国家→省份→城市”三级钻取销售额,但数据源只有city列(如"北京市朝阳区")。
错误键:df['city']→ 无法向上聚合。
正确键:用正则提取层级:

df['country'] = 'China' df['province'] = df['city'].str.extract(r'(北京市|上海市|广东省|浙江省)') df['city_level'] = df['city'].str.replace(r'(北京市|上海市|广东省|浙江省)', '', regex=True) # 然后用tuple分组:df.groupby(['country', 'province'])['sales'].sum()

场景3:事件序列分组
需求:识别“下单→支付→发货”完整链路的订单占比。
错误键:df['order_id']→ 但单个order_id在日志表中有多行(不同事件状态)。
正确键:df.groupby('order_id')['event_type'].apply(lambda x: set(x) == {'order', 'pay', 'ship'})
这里groupby的键是order_id,但聚合逻辑在apply中定义业务规则,而非简单数值计算。

注意:分组键必须是可哈希(hashable)对象。listdictnumpy.ndarray会报TypeError: unhashable type。解决方案:转为tupledf.groupby(df['tags'].apply(tuple)))或用pd.util.hash_pandas_object()生成哈希码。

2.3 聚合函数的陷阱:.agg()为何比.sum()/.mean()更值得信赖?

初学者常用df.groupby('A')['B'].sum(),但当需求变复杂(如“每个类目下,价格前10%商品的平均销量”),立刻陷入困境。此时.agg()是唯一出路,但它有三大隐藏机制:

机制1:多函数聚合的执行顺序

# 错误认知:先算max,再算min,最后mean df.groupby('category')['price'].agg(['max', 'min', 'mean']) # 实际执行:对每个分组,**并行**计算三个函数,非串行! # 所以结果是三个独立标量,不是基于同一子集的衍生计算。

机制2:自定义函数的上下文隔离

def top10_avg(x): threshold = x.quantile(0.9) # 取本分组内90%分位数 return x[x >= threshold].mean() # 仅对本分组的高价商品求均值 df.groupby('category')['price'].agg(top10_avg) # ✅ 安全

关键点:x参数是当前分组的Series副本,修改x不影响其他分组。但若函数内引用全局变量(如global_df),则可能引发并发错误。

机制3:命名聚合(Named Aggregation)的革命性价值
Pandas 0.25+引入的语法:

df.groupby('category').agg( avg_price=('price', 'mean'), high_price_count=('price', lambda x: (x > 1000).sum()), price_range=('price', lambda x: x.max() - x.min()) )

优势:

  • 输出列名即业务含义,无需后期rename()
  • 每个元组(column, function)明确绑定,避免.agg([np.mean, np.std])产生的模糊列名price_mean,price_std
  • 支持混合列操作(如同时聚合pricequantity

实操心得:永远优先用命名聚合。我曾重构一个金融风控报表,将23个agg()调用统一改为命名聚合,代码可读性提升40%,后续新增指标时,开发时间从2小时缩短到15分钟。

3. 核心细节与实操要点:从语法到生产环境的12个生死细节

3.1 分组键的预处理:为什么fillna()必须放在groupby之前?

常见错误:

# 危险!NaN会被分到独立分组,且无法通过.dropna()删除 result = df.groupby('category')['sales'].sum().dropna() # ❌ 仍保留NaN分组

正确流程:

# 第一步:明确NaN的业务含义 # 若'category'为空代表“未知类目”,应归入'Other' df['category'] = df['category'].fillna('Other') # 第二步:若NaN代表“数据缺失”,需评估是否影响业务逻辑 # 如电商中'category'为空的商品,可能需单独分析原因,而非简单填充 # 此时应先统计缺失比例:df['category'].isnull().mean() > 0.05? 需预警! # 第三步:执行groupby result = df.groupby('category')['sales'].sum()

原理深挖groupbyNaN的处理遵循IEEE 754标准——NaN != NaN,所以每个NaN都会被当作独立键创建分组。dropna()只能删掉结果中的NaN值,但分组过程已消耗资源。实测100万行数据,fillna()前置使groupby耗时降低37%,因哈希表键数量从10001(含1个NaN键)减至10000。

3.2 多级分组(MultiIndex)的驾驭:从“看不懂的输出”到“精准切片”

当用多个键分组:df.groupby(['category', 'region']),返回的是MultiIndex对象。新手看到result.loc[('手机', '华东')]就懵了。其实它就是一张二维表的行列索引:

# 创建示例 result = df.groupby(['category', 'region'])['sales'].sum().unstack(fill_value=0) # 输出:columns=['华东','华北','华南'],index=['手机','电脑','配件'] # 此时result['华东']直接获取华东各品类销售额

unstack()有局限:若某组合不存在(如"配件"+"西北"无数据),会生成NaN。更鲁棒的方式是用pivot_table

pd.pivot_table( df, values='sales', index='category', columns='region', aggfunc='sum', fill_value=0 # 显式填0,避免NaN )

关键技巧:用xs()进行跨级切片。例如获取所有“华东”地区的数据,不管品类:

result.xs('华东', level='region') # 返回Series,index为'category'

注意:MultiIndexlevel参数必须与groupby时键的顺序一致。若groupby(['region', 'category']),则level='region'对应第0级,level='category'对应第1级。顺序错则报KeyError

3.3.apply()的性能雷区:何时该用,何时必须禁用?

apply()groupby的瑞士军刀,但也是性能黑洞。我们对比三种实现“每个用户首单金额”的方案:

方案代码10万行耗时原理
.apply()df.groupby('user_id').apply(lambda x: x.loc[x['order_time'].idxmin(), 'amount'])2.1秒对每个分组创建子DataFrame,调用idxmin()
.idxmin()+.locdf.loc[df.groupby('user_id')['order_time'].idxmin(), 'amount']89ms向量化操作,只计算索引,不复制数据
sort_values+drop_duplicatesdf.sort_values('order_time').drop_duplicates('user_id', keep='first')['amount']156ms利用排序稳定性,一次去重

结论:只要能用向量化方法(.idxmin(),.first(),.nth(0))替代apply(),就必须替代apply()的适用场景仅限:

  • 逻辑极度复杂,无法拆解为原子操作(如计算用户LTV需调用外部API)
  • 需要访问分组内多列进行条件判断(如“首单金额>1000且下单时间在工作日”)

此时务必加.reset_index()避免MultiIndex混乱:

def first_order_logic(group): first_row = group.loc[group['order_time'].idxmin()] return pd.Series({ 'first_amount': first_row['amount'], 'is_weekday': first_row['order_time'].weekday() < 5 }) result = df.groupby('user_id').apply(first_order_logic).reset_index()

3.4 空分组的处理:为什么.size().count()更可靠?

需求:统计每个类目下的商品数量。
错误:df.groupby('category')['product_id'].count()
问题:若某类目下product_id全为NaNcount()返回0,但实际该类目存在(只是ID缺失)。

正确:df.groupby('category').size()
原理:.size()统计分组内行数,与列值无关;.count()统计非空值数量

验证:

# 构造测试数据:'配件'类目下3行,但'product_id'全为NaN test_df = pd.DataFrame({ 'category': ['手机', '手机', '配件', '配件', '配件'], 'product_id': ['p1', 'p2', np.nan, np.nan, np.nan] }) print(test_df.groupby('category')['product_id'].count()) # 手机:2, 配件:0 print(test_df.groupby('category').size()) # 手机:2, 配件:3 ✅

实操心得:在数据质量监控脚本中,永远用.size()做分组计数基准。我曾发现某物流系统“异常单量”统计长期偏低,根源就是用了.count()统计tracking_number列,而异常单的运单号确实为空——这恰恰是业务特征,却被当成了数据缺失。

3.5 分组聚合的内存优化:.agg()的chunking策略

当分组数极大(如百万级用户ID),.agg()可能OOM。解决方案:

# 分块处理:每次处理10万个分组 def chunked_agg(df, group_col, agg_dict, chunk_size=100000): unique_groups = df[group_col].unique() results = [] for i in range(0, len(unique_groups), chunk_size): chunk_groups = unique_groups[i:i+chunk_size] chunk_df = df[df[group_col].isin(chunk_groups)] results.append(chunk_df.groupby(group_col).agg(agg_dict)) return pd.concat(results) # 使用 result = chunked_agg(df, 'user_id', {'amount': 'sum', 'order_time': 'max'})

原理:避免一次性加载全部分组哈希表。实测处理500万行用户行为日志,内存峰值从12GB降至3.2GB,耗时仅增加18%(可接受)。

3.6 时间窗口分组:.resample().groupby(pd.Grouper())的本质区别

需求:按“每小时”统计订单量。
错误:df.groupby(df['order_time'].dt.floor('H'))['order_id'].count()
问题:floor('H')会将2023-01-01 10:59归为2023-01-01 10:00,但业务要求是“10:00-10:59”为一小时,应向下取整到10:00——这没错,但若数据跨时区,dt.floor不处理时区转换。

正确:

# 方案1:resample(仅适用于DatetimeIndex) df.set_index('order_time').resample('H').count()['order_id'] # 方案2:pd.Grouper(推荐,支持任意列,自动处理时区) df.groupby(pd.Grouper(key='order_time', freq='H'))['order_id'].count()

关键差异

  • resample要求索引是DatetimeIndex,且freq参数严格按日历规则(如'MS'为每月1日)
  • pd.Grouper可作用于任意列(key='order_time'),且freq='H'表示“每小时”,起始点默认为数据最小时间戳向下取整,更符合业务直觉。

注意:pd.Grouperclosedlabel参数决定边界。closed='left'(默认)表示[10:00, 11:00)label='left'则用左端点命名分组(10:00)。若需[10:00, 11:00],设closed='right'

4. 实操全流程:从原始日志到业务看板的7步落地

4.1 数据准备:模拟真实电商日志(10万行)

我们构建一个贴近真实的订单日志数据集,包含典型脏数据:

import pandas as pd import numpy as np from datetime import datetime, timedelta np.random.seed(42) n_rows = 100000 # 生成基础数据 dates = pd.date_range('2023-01-01', '2023-03-31', freq='T') order_times = np.random.choice(dates, n_rows) categories = np.random.choice(['手机', '电脑', '配件', '家电', '图书'], n_rows, p=[0.3, 0.25, 0.2, 0.15, 0.1]) regions = np.random.choice(['华东', '华北', '华南', '西南'], n_rows, p=[0.4, 0.25, 0.2, 0.15]) # 添加脏数据:5%的category为空,3%的region为空,2%的amount为负(退款) df = pd.DataFrame({ 'order_id': [f'ORD{100000+i}' for i in range(n_rows)], 'order_time': order_times, 'category': categories, 'region': regions, 'amount': np.random.lognormal(8, 0.5, n_rows) # 价格服从对数正态分布 }) # 注入脏数据 null_mask = np.random.random(n_rows) < 0.05 df.loc[null_mask, 'category'] = np.nan df.loc[np.random.random(n_rows) < 0.03, 'region'] = np.nan df.loc[np.random.random(n_rows) < 0.02, 'amount'] = -df.loc[np.random.random(n_rows) < 0.02, 'amount'] print(f"原始数据形状: {df.shape}") print(f"category缺失率: {df['category'].isnull().mean():.2%}") print(f"region缺失率: {df['region'].isnull().mean():.2%}")

4.2 步骤1:缺失值治理与业务定义

# Step 1.1: category缺失 —— 业务定义为"未知类目" df['category'] = df['category'].fillna('Unknown') # Step 1.2: region缺失 —— 需分析原因,先标记 df['region_missing_flag'] = df['region'].isnull() # 统计缺失是否集中于特定时段(如新接入渠道) missing_by_hour = df[df['region_missing_flag']].groupby(df['order_time'].dt.hour).size() print("region缺失按小时分布:") print(missing_by_hour.sort_index()) # Step 1.3: amount为负 —— 业务上是退款,需单独分析 df['is_refund'] = df['amount'] < 0 df['abs_amount'] = df['amount'].abs() # 后续聚合用绝对值

4.3 步骤2:构建多维分组键

# 构建时间维度键:小时、日期、星期几、是否周末 df['hour'] = df['order_time'].dt.hour df['date'] = df['order_time'].dt.date df['weekday'] = df['order_time'].dt.weekday df['is_weekend'] = df['weekday'].isin([5, 6]) # 构建用户价值分层键(RFM简化版) # R: 最近下单距今几天;F: 下单频次;M: 总金额 user_stats = df.groupby('order_id').agg( last_order_time=('order_time', 'max'), total_amount=('abs_amount', 'sum') ).reset_index() # 计算R(假设今天是2023-04-01) today = pd.Timestamp('2023-04-01') user_stats['recency_days'] = (today - user_stats['last_order_time']).dt.days user_stats['frequency'] = 1 # 每个order_id算1次(真实场景需关联user_id) # 分层:R<7为高活跃,7-30为中,>30为低 user_stats['r_segment'] = pd.cut( user_stats['recency_days'], bins=[-1, 7, 30, 1000], labels=['high', 'mid', 'low'], include_lowest=True ) # 合并回原表(按order_id) df = df.merge(user_stats[['order_id', 'r_segment']], on='order_id', how='left')

4.4 步骤3:核心业务指标聚合(命名聚合实战)

# 需求:按【类目+地区+时间】三维,统计: # - 订单数、总金额、平均客单价、退款率、高活跃用户订单占比 result = df.groupby(['category', 'region', 'date']).agg( order_count=('order_id', 'count'), total_sales=('abs_amount', 'sum'), avg_order_value=('abs_amount', 'mean'), refund_rate=('is_refund', lambda x: x.mean()), # True/False均值即比例 high_r_orders=('r_segment', lambda x: (x == 'high').mean()) ).round(4) # 保留4位小数 print("聚合结果示例:") print(result.head())

4.5 步骤4:结果透视与业务解读

# 将结果转为宽表,便于BI工具接入 pivot_result = result.reset_index().pivot_table( index=['category', 'date'], columns='region', values=['order_count', 'total_sales'], aggfunc='sum', fill_value=0 ) # 展平列名 pivot_result.columns = ['_'.join(col).strip() for col in pivot_result.columns.values] pivot_result = pivot_result.reset_index() # 业务解读:找出华东区增长最快的类目(环比) latest_date = pivot_result['date'].max() prev_date = latest_date - pd.Timedelta(days=1) latest = pivot_result[pivot_result['date'] == latest_date].set_index('category') prev = pivot_result[pivot_result['date'] == prev_date].set_index('category') growth = (latest['order_count_华东'] - prev['order_count_华东']) / prev['order_count_华东'] print("华东区订单量环比增长TOP5:") print(growth.nlargest(5))

4.6 步骤5:异常检测与根因定位

# 检测异常:某类目某日订单量突增>200% daily_cat = df.groupby(['category', 'date']).size().unstack(fill_value=0) # 计算7日滚动均值和标准差 rolling_mean = daily_cat.rolling(window=7, axis=1).mean() rolling_std = daily_cat.rolling(window=7, axis=1).std() # 标记异常点 anomalies = (daily_cat > rolling_mean + 2 * rolling_std) print("异常点(类目,日期):") print(anomalies.stack()[anomalies.stack()].index.tolist())

4.7 步骤6:导出与自动化

# 导出为Parquet(列式存储,比CSV快5倍,小50%) result.to_parquet('daily_category_region_report.parquet', index=True) # 生成SQL兼容的CREATE TABLE语句(供数仓同步) def generate_create_sql(df, table_name): dtype_map = {'int64': 'BIGINT', 'float64': 'DECIMAL(18,4)', 'object': 'VARCHAR(100)'} columns = [] for col in df.columns: dtype = str(df[col].dtype) sql_type = dtype_map.get(dtype, 'VARCHAR(100)') columns.append(f"`{col}` {sql_type}") return f"CREATE TABLE {table_name} (\n" + ",\n".join(columns) + "\n);" print(generate_create_sql(result.reset_index(), 'report_category_region'))

5. 常见问题与排查技巧实录:来自200+次线上故障的总结

5.1 问题速查表:症状、原因、解决方案

症状可能原因解决方案实测耗时
KeyError: 'col_name'分组键列名拼写错误,或列在groupby前被drop()df.columns.tolist()确认列名;检查groupby前是否有df = df.drop(...)<1分钟
结果中出现NaN分组分组键含NaN且未fillna()df['key'].fillna('Unknown')df.dropna(subset=['key'])2分钟
MemoryError分组键基数过高(如100万唯一user_id改用value_counts();或分块聚合(见3.5节)15分钟
聚合结果为0NaN聚合列全为NaN,或agg()函数返回None.size()验证分组是否存在;检查自定义函数是否漏return5分钟
MultiIndex无法用loc索引loc语法错误,如result.loc['手机', '华东']缺括号正确:result.loc[('手机', '华东')]result.xs('华东', level='region')<1分钟
时间分组结果不连续(如缺2月29日)resample()Grouperfreq参数未覆盖全周期pd.date_range()生成完整时间索引,reindex()填充8分钟
.apply()返回Series但列名丢失未用pd.Series({...})reset_index()apply函数内返回pd.Series,或apply(...).reset_index()3分钟

5.2 独家避坑技巧:那些文档不会写的真相

技巧1:用ngroup()快速标记分组ID
当需要在原表添加“分组序号”列(如“第1组:手机,第2组:电脑”),不用factorize()

df['category_group_id'] = df.groupby('category').ngroup() + 1 # 从1开始编号 # 比df['category'].factorize()[0] + 1 更直观,且保证顺序与groupby一致

技巧2:.filter()[condition]更安全的分组筛选
需求:只保留“订单数>100的类目”。错误:result[result['order_count'] > 100](若resultSeries会报错)。正确:

# 对groupby结果直接filter filtered_result = df.groupby('category').filter(lambda x: len(x) > 100) # 这会返回原始DataFrame中满足条件的行,而非聚合结果

技巧3:as_index=False的隐藏价值
df.groupby('A', as_index=False)['B'].sum()返回DataFrame,as_index=True(默认)返回Series。但as_index=False还有个妙用:

# 当需对分组结果再次groupby时,避免MultiIndex嵌套 step1 = df.groupby(['A', 'B']).size().reset_index(name='count') step2 = step1.groupby('A')['count'].sum() # 直接操作,无索引烦恼

技巧4:调试apply函数的终极方法
在自定义函数内加print会因并行执行而乱序。正确做法:

def debug_func(group): # 用logging记录分组信息 import logging logging.info(f"Processing group: {group.name}, size: {len(group)}") return group['amount'].sum() # 先测试单一分组 sample_group = next(iter(df.groupby('category'))) print("Sample group name:", sample_group[0]) print("Sample group data:", sample_group[1].head())

5.3 性能对比实测:不同场景下的最优解

我们在100万行数据上实测以下操作(i7-11800H, 32GB RAM):

操作代码耗时内存峰值适用场景
单列分组求和df.groupby('category')['amount'].sum()124ms1.2GB通用首选
多列分组命名聚合df.groupby(['cat','reg']).agg({'amt':'sum','cnt':'count'})287ms1.8GB多指标必选
apply+idxmindf.groupby('user_id').apply(lambda x: x.loc[x['time'].idxmin(), 'amt'])3.2s3.5GB逻辑复杂时妥协
sort+drop_duplicatesdf.sort_values('time').drop_duplicates('user_id', keep='first')['amt']412ms2.1GB首单/末单场景
pd.crosstabpd.crosstab(df['category'], df['region'], df['amount'], aggfunc='sum')890ms2.4GB二维频次表专用

结论:命名聚合是平衡性最优解,速度损失在可接受范围(比单列慢2.3倍),但代码可维护性提升10倍。apply应作为最后手段。

6. 进阶思考:当groupby遇上现代数据栈

6.1 与Dask的协同:突破单机内存限制

当数据超10GB,Pandas力不从心。Dask提供无缝迁移:

import dask.dataframe as dd # 读取大文件(自动分块) ddf = dd.read_csv('big_orders.csv', blocksize='64MB') # 语法几乎一致 result = ddf.groupby('category')['amount'].sum().compute() # compute()触发执行

注意:Dask的groupby不支持所有Pandas函数,apply需用map_partitions替代。但核心聚合(sum,mean,size)完全兼容。

6.2 与Polars的对比:为什么说groupby是Polars的“心脏”?

Polars的groupby设计更激进:

import polars as pl # Polars中,groupby后必须跟聚合,无“惰性分组”概念 result = ( pl.read_csv('orders.csv') .groupby(['category', 'region']) .agg([ pl.col('amount').sum().alias('total_sales'), pl.col('amount').mean().alias('avg_order'), pl.col('order_id').count().alias('order_count') ]) )

优势:

  • 编译执行,比Pandas快3-5倍
  • 内存使用低40%(零拷贝)
  • 自动并行化,无需dask

但代价:学习曲线陡峭,生态不如Pandas成熟。

6.3 云原生实践:在Spark SQL中复现Pandas逻辑

当数据达TB级,Spark是唯一选择。关键映射:
| Pandas | Spark SQL |

http://www.zskr.cn/news/1474885.html

相关文章:

  • 2026 济南黄金回收旧金稳妥变现分步教程光谱测金杜绝缺秤陷阱 - 奢侈品回收评测
  • 别再死记硬背了!用示波器抓一次波形,彻底搞懂MIPI D-PHY的LP/HS模式切换
  • 华为/华三交换机配置入门:从VLAN划分到三层互通的完整实验指南(含PVID避坑点)
  • 东莞卖金避坑行业盘点:S 级认证禹竞,持证仪器鉴金规避扣重、虚报价各类套路 - 奢侈品交易观察员
  • 软考 系统架构设计师历年真题集萃(275)
  • 别再只用CrossEntropyLoss了!PyTorch实战:用Label Smoothing提升你的分类模型泛化能力(附完整代码)
  • VirtualBox Host-Only Network #2导致eNSP AR2220报错40?别慌,试试这个网络重置大法
  • Agent-S3:首个超越人类性能的智能体框架终极指南
  • 跨平台解决方案:在Windows电脑上获取官方macOS安装文件的完整指南
  • 从0.35到0.7:示波器带宽与采样率选型实战指南
  • Cadence 16.0安装实战:从破解原理到Win10/11兼容性全解析
  • 保姆级教程:用STM32CubeMX和FreeMODBUS V1.6,在STM32F405上快速实现Modbus RTU从站
  • CMOS、GaAs与SiGe半导体工艺选型指南:射频与模拟电路设计实战解析
  • 【广州楼市研判系列70】2026置换终极选择:核心区小户型VS外围大户型 - 速递信息
  • 肿泡眼用什么眼油?专治顽固泡泡眼的3款眼油,植萃眼油消肿紧致 - 全网最美
  • VSCode设置文件setting.json老弹警告?关掉这个选项,5秒搞定‘Unable to load schema’报错
  • 消费电子设计实战:破解多快少困局,平衡功能、性能与成本
  • 技术思维与商业思维的鸿沟:工程师如何跨越“亲妈滤镜”成为优秀CEO
  • 告别软件盗版烦恼:用YT88加密狗5分钟搞定C#/Java/Python源代码加密(附完整开发包)
  • 液态金属变形技术:从电场控制原理到嵌入式系统实现
  • ZYNQ7000硬件设计避坑指南:MIO引脚分配与EMIO扩展的实战经验分享
  • 51单片机音乐喷泉项目全套开发资料:原理图+PCB+Keil工程+实拍效果
  • 开源国标视频监控平台架构方案:构建企业级GB28181协议栈的微服务实现
  • 紧急预警!CSDN将于2024年11月起关闭旧版定时发布入口——现在掌握新V3.2自动化方案的最后机会
  • 告别重复插拔U盘!手把手教你将Clonezilla备份和飞腾麒麟系统打包成单一ISO,实现批量刷机
  • Python Matter Server:构建本地智能家居控制中枢的技术实现
  • 黄金变现谨防虚报高价套路!哈尔滨优质奢品机构全流程拆解测评 - 奢侈品交易观察员
  • STM32H743 + W25Q64JV SPI Flash DMA读写工程(含MDK/IAR双平台、SDRAM支持)
  • CCS7.3烧写DSP FLASH避坑指南:如何精准擦除指定扇区,保留Bootloader不误删
  • 别再手动调Excel了!用Easypoi 4.1.3实现一对多数据导出,自动合并单元格+智能行高