生产级多维聚合:pandas高效计算27个指标的工程实践

发布时间:2026/7/13 4:19:26
生产级多维聚合:pandas高效计算27个指标的工程实践 1. 项目概述为什么多维聚合不是“加个groupby”就能搞定的事我在银行风控部门做过三年数据管道开发后来跳槽到一家头部支付机构做BI平台架构。这期间最常被业务方拍着桌子问的一句话是“上个月华东区餐饮类商户的交易金额中位数、手续费波动范围、近7天滚动均值还有和去年同期比的增长率能不能现在就给我”——注意这不是三个问题而是一个问题的四个维度。它背后藏着一个现实真实业务场景里的数据聚合从来不是对单列求个sum或mean那么简单。它是一场多线程的精密协作既要横向切分按区域、按行业、按客户等级又要纵向延展滚动窗口、累计值、同比环比还得把结果揉成业务能一眼看懂的表格形态。你用pandas写df.groupby(region)[amount].sum()当然能跑通但当你面对的是日均3亿笔交易、需要同时输出27个指标、下游要直连Power BI做自动刷新时这种写法会立刻暴露出三重硬伤第一计算效率断崖式下跌——每个指标单独跑一遍groupby等于把整个数据集扫27遍第二逻辑耦合度高——某个指标的计算逻辑改了其他26个都得跟着动第三结果结构难维护——默认输出的MultiIndex Series像一堵砖墙财务总监打开Excel看到(amount, mean)这种列名只会皱眉。这篇文章讲的就是怎么用一套统一、可复用、生产级的聚合框架一次性把这27个指标全算出来且结果能直接喂给报表系统。核心关键词是多维聚合、生产级聚合策略、滚动窗口计算、多级分组展开、自定义业务聚合函数。它不教你怎么写第一个pandas程序而是告诉你当你的代码要扛住每天TB级数据、支撑50业务方实时取数、经得起审计抽查时聚合这件事到底该怎么设计。适合两类人一是已经会基础groupby但一写复杂分析就陷入“for循环嵌套手动拼表”泥潭的中级分析师二是正在搭建企业级数据服务API需要把聚合逻辑封装成稳定服务的数据工程师。我试过用纯SQL写这类需求光是窗口函数嵌套就写了400行上线后查一次要8秒换成本文这套pandas组合拳同样逻辑压缩到60行代码响应时间压到300毫秒以内。这不是炫技是活命的刚需。2. 多维聚合的核心设计思想从“单点计算”到“矩阵式计算”2.1 为什么必须放弃“一个指标一个groupby”的惯性思维刚入行时我也这么干。比如要算“各地区各产品线的销售额、毛利率、订单量”我会写三段代码第一段groupby([region,product])[revenue].sum()第二段groupby([region,product])[profit].sum()/...第三段groupby([region,product])[order_id].count()最后用pd.concat()拼起来。实测下来当数据量超过500万行这种写法会触发pandas的底层性能陷阱——每次groupby都要重建索引、重新哈希分组键、再扫描全量数据。更致命的是它完全无视了现代CPU的缓存局部性原理三次扫描意味着三次把整个数据块从内存拖进CPU缓存而实际计算只用了其中极小部分。我拿真实信用卡交易数据做过对比测试1000万行数据单指标groupby平均耗时1.8秒三个指标串行就是5.4秒而用agg()字典一次调用耗时仅2.1秒。省下的3.3秒在高频调度的ETL任务里意味着每天多跑120次任务或者把SLA从15分钟压到5分钟。这背后是pandas的底层优化agg()在第一次分组时就把所有分组键的哈希值、数据块指针全部缓存后续对不同列应用不同函数时直接复用这些元信息避免重复计算。所以第一步认知升级就是聚合的本质不是“计算”而是“分组状态的复用”。你不是在算多个指标而是在一个分组状态下对不同字段施加不同变换。2.2 “矩阵式计算”的三层架构设计我把生产环境的聚合逻辑拆成三个不可分割的层次就像盖楼的地基、承重墙、装修第一层分组骨架Grouping Skeleton这是整个计算的锚点决定了数据如何切割。它必须严格遵循业务语义不能为了技术方便而妥协。比如银行风控要求“按客户风险等级商户行业分类交易时间粒度日/周/月”三维分组那groupby([risk_level,merchant_category,date_trunc])就是铁律。这里有个血泪教训曾有同事为图省事把日期截断成字符串再分组结果跨年时2023-12和2024-01被当成两个孤立维度同比计算全错。正确做法是用pd.Grouper(keydate, freqM)让pandas原生处理时间序列连续性。第二层指标矩阵Metric Matrix在固定分组骨架上我们定义一个二维映射行是数据字段如transaction_amount、fee列是聚合函数如sum、median、std。这个矩阵用字典实现{transaction_amount: [sum,median,count], fee: [min,max,mean]}。关键在于同一字段可以挂多个函数不同字段可以挂不同函数且函数间完全解耦。这意味着transaction_amount的中位数计算不会影响fee的最大值计算它们共享分组状态但各自走独立计算路径。这种设计天然支持指标增减——要加个“95分位数”只需在transaction_amount列表里加quantile(0.95)无需动其他代码。第三层结果塑形Result Shapingagg()输出的默认结构是MultiIndex DataFrame列名是二元组(transaction_amount,sum)。这对程序友好但对人不友好。业务方要的是revenue_sum、fee_max这种扁平化列名。这里必须做两件事一是用columns.map(_.join)把二元组压成字符串二是用rename(columns{...})做语义化重命名比如把transaction_amount_median改成txn_amt_median。很多团队卡在这一步导致下游ETL脚本里全是df[(transaction_amount,median)]这种反人类写法。我的经验是在聚合层就完成列名标准化让下游消费方零学习成本。我们团队的规范是所有聚合结果列名必须符合{业务域}_{字段缩写}_{聚合函数缩写}比如crm_customer_cnt、fraud_txn_amt_std。2.3 生产环境必须考虑的四个隐藏约束除了功能真正的生产级聚合还要扛住四重压力这些在教程里永远不提但在上线第一天就会打脸内存爆炸防护当分组键组合过多比如groupby([user_id,session_id,page_url])pandas会为每个唯一组合分配内存。1000万用户×1000次会话×1000个页面10^9个分组内存直接爆掉。解决方案是预过滤用df.query(user_id in top_10k_users)先筛出核心用户或用nunique()估算分组数超阈值则降级为采样聚合。空值污染阻断agg()遇到全NaN列会返回NaN但业务上“无数据”和“数据为0”意义完全不同。比如手续费fee列全为空min返回NaN但风控规则要求此时视为0。必须在agg前用df[fee] df[fee].fillna(0)显式处理或在自定义函数里加if pd.isna(x).all(): return 0兜底。类型安全校验sum作用于字符串列会静默失败返回空字符串mean作用于布尔列会变成0/1均值。我们在聚合前强制执行类型检查assert np.issubdtype(df[amount].dtype, np.number), amount must be numeric并在CI流程里加入类型断言测试。计算精度锁定金融场景要求浮点数精确到分。np.float64在累加时会有微小误差比如0.10.2 ! 0.3。我们的方案是金额类字段统一转pd.Int64Dtype()用整数存储分聚合后再除100或用decimal.Decimal包装虽然慢30%但审计零争议。3. 核心聚合模式详解从基础到高阶的七种实战写法3.1 多字段多函数聚合告别“for循环拼表”这是最常用也最容易写错的模式。原始示例里用agg({transaction_amount: [mean,median], processing_fee: [min,max]})是对的但生产环境要解决三个细节问题细节1函数别名控制默认输出列名是(transaction_amount,mean)但业务方要avg_txn_amt。pandas 1.3支持字典键为元组agg({(transaction_amount,avg): mean, (transaction_amount,med): median})这样输出列名直接是(transaction_amount,avg)再用columns.map(_.join)就得到transaction_amount_avg。比事后rename更干净。细节2混合聚合函数的坑想同时算sum和count没问题但若混入first取首行值要注意first返回的是原始数据类型而sum返回数值。如果first列是字符串整个结果DataFrame的dtype会退化为object后续数值计算全报错。解决方案统一用lambda x: x.iloc[0] if len(x) else np.nan并显式.astype(str)。细节3空分组处理当某分组键在数据中不存在比如新上线的cryptocurrency行业agg()默认不输出该行。但报表系统要求所有维度必须存在缺失值填0。要用groupby(..., dropnaFalse)reindex()补全# 先获取所有可能的分组组合 all_combos pd.MultiIndex.from_product( [regions, products], names[region,product] ) result df.groupby([region,product]).agg({...}).reindex(all_combos, fill_value0)我在线上系统里封装了一个robust_agg()函数自动处理这三类问题调用时只需df.robust_agg(group_keys, metric_dict)内部已集成空值填充、类型校验、列名标准化。代码虽多20行但省下的是每周3小时的debug时间。3.2 自定义聚合函数把业务规则刻进代码里标准函数解决不了的问题才是业务价值所在。比如风控场景的“交易离散度”不是简单max-min而是(max-min)/median因为绝对差值在大额交易和小额交易间不可比。原始示例的lambda写法lambda x: x.max()-x.min()太脆弱——没处理空值、没校验长度、没加文档。生产级写法必须是命名函数def txn_dispersion(series): 计算交易离散度(最大值-最小值)/中位数 业务意义衡量商户交易金额波动剧烈程度值0.5需人工核查 防御性编程空序列返回np.nan单值序列返回0 if len(series) 0: return np.nan if len(series) 1: return 0.0 median_val series.median() if median_val 0: # 避免除零 return np.nan return (series.max() - series.min()) / median_val # 调用时明确标注业务含义 result df.groupby(merchant_category).agg({ transaction_amount: txn_dispersion, fee: lambda x: x.sum() / x.count() if len(x) 0 else 0 # 手续费均值 })这里的关键是函数即文档。当半年后新人接手看到txn_dispersion就知道这是风控指标看到docstring就明白阈值逻辑不用翻需求文档。我们团队规定所有自定义聚合函数必须包含三要素——业务意义注释、防御性边界处理、输入输出类型说明。另一个经典案例是“加权移动平均”原始示例用np.linspace生成权重但实际业务中权重要按交易时间衰减最近3天权重1.04-7天权重0.88-30天权重0.5。这就必须写成带参数的闭包def time_decay_weighted_avg(decay_config{3d:1.0, 7d:0.8, 30d:0.5}): def _func(series): # series.index 是datetime计算每行距最新日期的天数 days_ago (series.index.max() - series.index).days weights np.ones(len(series)) for days, weight in decay_config.items(): threshold int(days[:-1]) # 3d - 3 weights[days_ago threshold] weight return np.average(series, weightsweights) return _func # 使用 result df.groupby(customer_id).agg({ amount: time_decay_weighted_avg({3d:1.2, 7d:0.9}) })这种写法让业务规则可配置、可测试、可审计而不是硬编码在lambda里。3.3 滚动窗口聚合时间序列分析的基石滚动窗口的核心是理解window参数的物理意义。原始示例用rolling(window3)但3天窗口在业务中可能对应三种完全不同的需求日历日窗口不管有没有交易固定向前推3个自然日freq3D交易日窗口只算有交易的日期取最近3笔交易min_periods3业务周期窗口按周/月对齐比如“最近3个完整周”ondate, freqW-MON我踩过的最大坑是混淆了min_periods和center。原始示例输出前两行是NaN因为min_periods3默认值要求至少3个点才计算。但业务上首日数据也要显示哪怕只有1个点。这时必须设min_periods1否则日报系统第一天就没数据。另一个坑是centerTrue它让窗口中心对齐当前行但会导致首尾数据外推——比如window3, centerTrue时第1行会用第1、2、3行数据但第2行用第1、2、3行和第1行一样这违反了滚动计算的时序一致性。生产环境一律用centerFalse。更关键的是窗口大小的业务决策逻辑。为什么是7天因为小于7天受周末效应干扰大周五交易量通常是周一的2倍大于7天无法及时捕捉突发变化如营销活动后3天内效果就显现等于7天完美覆盖一个自然周消除周期性噪声所以我们在代码里写死window7但加注释说明“此值经AB测试验证7日滚动均值对欺诈率突变的检测灵敏度最高误报率低于2%”。这比写# magic number专业一万倍。3.4 扩展窗口聚合累计值的正确打开方式expanding()看似简单但有两个致命误区误区1cumsum()不等于expanding().sum()df[col].cumsum()是向量化操作快且省内存df.groupby(key)[col].expanding().sum()是分组后逐组计算慢且吃内存。除非需要分组累计否则直接用cumsum()。我们线上系统里90%的累计需求用df.sort_values(date).groupby(customer_id)[amount].cumsum()实现比expanding()快5倍。误区2累计值必须带时间戳对齐原始示例expanding().sum()输出的索引是MultiIndex(category,date)但下游系统要的是单层date索引。必须用reset_index(level0, dropTrue)剥离分组键否则df.to_sql()会报错。更稳妥的做法是# 先确保数据按时间排序 df_sorted df.sort_values([customer_id,date]) # 计算累计值并重置索引 df_sorted[cumulative_spend] df_sorted.groupby(customer_id)[amount].cumsum() # 保留原始索引用于关联 result df_sorted.set_index([customer_id,date])[[cumulative_spend]]扩展窗口真正的高阶用法是累计统计量的动态校准。比如风控系统要实时更新“历史交易金额的标准差”但expanding().std()在数据少时不稳定前3笔交易std0。我们的方案是用expanding(min_periods30)前29个点用滑动窗口rolling(30).std()填充30点后切到扩展窗口。代码封装成adaptive_expanding_std()内部自动切换对外接口统一。3.5 多级分组与unstack让老板一眼看懂的数据透视unstack()是业务沟通的翻译器。原始示例groupby([region,product])[revenue].mean().unstack()输出矩阵但生产环境要解决三个落地问题问题1缺失值填充策略unstack()遇到某区域没有某产品时填NaN。但财务报表要求填0表示“无销售”不是“数据缺失”。必须用unstack(fill_value0)且fill_value必须是数字不能是字符串N/A否则后续数值计算报错。问题2行列顺序的业务语义unstack()默认把最内层分组键转列但业务上可能要求“产品作行、区域作列”。要用swaplevel()调整result df.groupby([product,region])[revenue].mean().unstack(region) # 如果想region作行product作列先swap再unstack result df.groupby([product,region])[revenue].mean().swaplevel().unstack(product)问题3多指标unstack的列名爆炸当agg()输出多指标时unstack()会让列名变成三层(revenue,sum,North)。这时要用unstack([1,2])指定展开多层再用columns.map(_.join)压平。我们团队的规范是unstack()后立即执行flatten_columns()工具函数保证列名是revenue_sum_North这种可读格式。真正体现功力的是动态维度切换。业务方今天要“区域×产品”明天要“客户等级×渠道”如果每个需求都重写groupby维护成本爆炸。我们的方案是把分组键定义成配置项GROUPING_DIMENSIONS [region,product]然后用df.groupby(GROUPING_DIMENSIONS)unstack()时用unstack(GROUPING_DIMENSIONS[-1])自动取最后一个维度。这样改需求只需改配置不动代码。3.6 综合实战信用卡客户分析流水线把前面所有模式串起来就是一个完整的生产流水线。原始示例的end-to-end代码是教学版我把它重构为工业级版本重点展示工程化细节class CreditCardAnalyzer: 信用卡客户分析器 - 生产环境封装 def __init__(self, data: pd.DataFrame): self.df data.copy() # 强制类型转换防意外 self.df[amount] pd.to_numeric(self.df[amount], errorscoerce) self.df[date] pd.to_datetime(self.df[date]) self.df[fee] pd.to_numeric(self.df[fee], errorscoerce) def run_full_analysis(self) - Dict[str, pd.DataFrame]: 执行全部7项分析返回命名结果字典 results {} # Analysis 1: 多指标聚合带空值防护 results[multi_agg] self._multi_metric_agg() # Analysis 2: 自定义离散度带业务阈值 results[dispersion] self._calculate_dispersion() # Analysis 3: 滚动窗口带min_periods1 results[rolling_7day] self._rolling_window_agg() # Analysis 4: 累计值用cumsum替代expanding results[cumulative_spend] self._cumulative_spend() # Analysis 5: 多维透视带fill_value0 results[crosstab] self._crosstab_analysis() # Analysis 6: 管理层摘要带业务公式 results[exec_summary] self._executive_summary() # Analysis 7: 风险分层带多条件聚合 results[risk_segmentation] self._risk_segmentation() return results def _multi_metric_agg(self) - pd.DataFrame: Analysis 1: 客户×品类多指标聚合 # 防御性过滤异常金额 valid_df self.df[self.df[amount].between(1, 10000)] # 多函数聚合 result valid_df.groupby([customer_id,category]).agg({ amount: [mean,median,count], fee: [min,max,sum] }) # 列名扁平化 result.columns [_.join(col).strip() for col in result.columns] return result.round(2) def _calculate_dispersion(self) - pd.DataFrame: Analysis 2: 交易离散度 - (max-min)/median def dispersion(series): if len(series) 2: return np.nan med series.median() if med 0: return np.nan return (series.max() - series.min()) / med return self.df.groupby(category)[amount].apply(dispersion).to_frame(dispersion_ratio) def _rolling_window_agg(self) - pd.DataFrame: Analysis 3: 7日滚动均值 - 按客户分组 # 确保时间排序 sorted_df self.df.sort_values([customer_id,date]) # 计算滚动均值min_periods1保证首日有值 sorted_df[rolling_7day_avg] ( sorted_df.groupby(customer_id)[amount] .rolling(window7, min_periods1) .mean() .reset_index(level0, dropTrue) ) return sorted_df[[customer_id,date,amount,rolling_7day_avg]] def _cumulative_spend(self) - pd.DataFrame: Analysis 4: 累计消费 - 用cumsum提升性能 sorted_df self.df.sort_values([customer_id,date]) sorted_df[cumulative_spend] ( sorted_df.groupby(customer_id)[amount].cumsum() ) return sorted_df[[customer_id,date,amount,cumulative_spend]] def _crosstab_analysis(self) - pd.DataFrame: Analysis 5: 客户×品类平均交易额透视表 result self.df.groupby([customer_id,category])[amount].mean().unstack(fill_value0) return result.round(2) def _executive_summary(self) - pd.DataFrame: Analysis 6: 管理层摘要 - 总额、均值、手续费率 summary self.df.groupby(customer_id).agg({ amount: [sum,mean,count], fee: sum }) summary.columns [total_spend,avg_transaction,txn_count,total_fee] # 业务公式手续费率 总手续费 / 总交易额 summary[fee_rate_pct] (summary[total_fee] / summary[total_spend] * 100).round(2) return summary.round(2) def _risk_segmentation(self) - pd.DataFrame: Analysis 7: 风险分层 - 高价值交易识别 def risk_metrics(series): high_val series 300 return pd.Series({ high_value_count: high_val.sum(), high_value_pct: (high_val.sum() / len(series) * 100).round(1), regular_avg: series[~high_val].mean() if (~high_val).any() else np.nan }) return self.df.groupby(customer_id)[amount].apply(risk_metrics) # 使用示例 analyzer CreditCardAnalyzer(df_transactions) all_results analyzer.run_full_analysis() # 直接取某个结果 print(all_results[exec_summary])这个类封装了所有工程实践类型校验、空值防护、性能优化cumsum替代expanding、业务公式内嵌、结果命名化。上线后新增一个分析项只需在run_full_analysis()里加一行调用不用动底层逻辑。3.7 高阶技巧聚合结果的下游集成聚合不是终点而是数据服务的起点。生产环境必须考虑如何把结果喂给下游对接BI工具Power BI/Tableau要求列名是合法标识符不能有空格、括号、点号。agg()输出的(amount,mean)必须转成amount_mean。我们用正则清洗re.sub(r[^a-zA-Z0-9_], _, str(col))。写入数据库to_sql()对MultiIndex支持差。必须先reset_index()再to_sql()。且要指定if_existsreplace或append并加indexFalse避免写入索引列。API服务化用FastAPI封装输入是JSON配置{groupby: [region,product], metrics: {amount: [sum,avg]}}输出是JSON。关键是要做参数校验检查groupby字段是否存在、metrics函数是否被允许黑名单eval、exec等危险函数。监控告警聚合结果要加质量水印。比如在exec_summary里加data_quality_score列计算逻辑是100 - (null_count / total_count) * 100。当分数95时自动发钉钉告警。4. 实操避坑指南那些文档里不会写的血泪教训4.1 内存泄漏的隐形杀手未释放的groupby对象这是最隐蔽的性能杀手。pandas的groupby对象在计算后不会自动释放内存尤其当分组键基数高时。我曾在线上环境遇到一个groupby([user_id,date])操作后即使del result内存也不释放。根本原因是groupby对象内部缓存了分组索引映射直到Python垃圾回收。解决方案有三显式删除gb df.groupby(...),result gb.agg(...),del gb不是del result上下文管理器封装成with GroupByContext(df, keys) as gb:退出时自动清理禁用缓存df.groupby(keys, sortFalse, observedTrue)observedTrue只对观测到的分组键建索引减少内存占用我们团队的强制规范所有groupby操作必须用with语句包裹CI检查会扫描groupby(关键字未配with则失败。4.2 时间窗口的时区陷阱UTC vs 本地时间原始示例用pd.date_range(2024-01-01)但生产环境必须明确时区。银行系统用UTC但报表要北京时间UTC8。如果date列是naive datetime无时区rolling(window7D)会按UTC计算导致北京用户看到的“7日滚动”其实是UTC时间的7天和本地时间错位8小时。正确做法# 加载数据时就绑定时区 df[date] pd.to_datetime(df[date]).dt.tz_localize(UTC).dt.tz_convert(Asia/Shanghai) # 或者用时区感知的rolling df.set_index(date).groupby(customer_id)[amount].rolling(7D, closedboth).mean()closedboth表示窗口包含首尾两天这是业务常识“过去7天”包含今天和7天前。4.3 自定义函数的序列化难题Dask/Spark集群失效当把pandas聚合迁移到Dask或Spark时自定义函数必须可序列化。原始示例的lambda x: x.max()-x.min()在Dask里会报TypeError: cant pickle _thread.RLock objects。因为lambda闭包捕获了全局状态。解决方案用普通函数替代lambda命名函数默认可序列化避免闭包引用time_decay_weighted_avg()返回的函数不能引用外部变量要把decay_config作为参数传入函数内部用functools.partialfrom functools import partial; partial(txn_dispersion, threshold300)我们线上用Dask时所有自定义函数都放在独立模块aggs.py里且通过dask.delayed装饰确保可跨节点传输。4.4 结果一致性的终极保障单元测试模板没有测试的聚合代码就是定时炸弹。我们为每个聚合函数写三类测试边界测试空DataFrame、单行数据、全NaN列精度测试用np.testing.assert_allclose()验证浮点数结果容忍1e-10误差业务逻辑测试给定已知输入断言输出符合业务规则例如txn_dispersion的测试def test_txn_dispersion(): # 构造已知数据[100,200,300] - (300-100)/200 1.0 series pd.Series([100,200,300]) assert txn_dispersion(series) 1.0 # 边界单值 assert txn_dispersion(pd.Series([100])) 0.0 # 边界空值 assert np.isnan(txn_dispersion(pd.Series([])))CI流程里聚合函数的测试覆盖率必须≥95%否则禁止合并。4.5 性能调优的黄金法则从profile开始不要猜要测。用line_profiler定位瓶颈pip install line_profiler kernprof -l -v your_script.py常见瓶颈点agg()里用median比mean慢10倍因需排序→ 业务允许时用quantile(0.5)或用mean替代unstack()在分组键多时慢→ 改用pivot_table(index..., columns..., values...)后者针对稀疏矩阵优化rolling().mean()在大数据集慢→ 改用numba.jit加速或用scipy.ndimage.uniform_filter1d我们线上系统的聚合耗时监控看板会实时显示每个分析项的P95耗时超阈值自动告警。5. 常见问题速查表从报错到优化的实战应答问题现象根本原因解决方案我的实测效果KeyError: column_name分组键或聚合字段名拼写错误或字段不存在用df.columns.tolist()打印所有列名确认大小写和空格用df.info()检查字段是否存在100%解决避免低级错误ValueError: No numeric types to aggregate对非数值列如字符串应用了sum/mean在agg()前用select_dtypes(include[np.number])筛选数值列或在自定义函数里加if not np.issubdtype(series.dtype, np.number): return np.nan防止整个job崩溃返回NaN可追溯MemoryError分组键组合爆炸如1000万用户×1000商户启用df.groupby(..., observedTrue)或用sample(frac0.1)降采样或改用dask.dataframe内存从OOM降到2GB耗时增加15%NaN在滚动窗口首部min_periods默认为window值要求满窗才计算显式设置min_periods1或用fillna(methodffill)前向填充首日数据可用业务方满意度40%unstack()后列名混乱MultiIndex列未扁平化下游系统无法解析用result.columns [_.join(map(str, col)) for col in result.columns]Power