引言
在数据清洗与预处理的坚实基础上,我们迎来了数据分析流程中最富创造力、最具洞察力的阶段——数据转换。如果说数据清洗是“整理原料”,那么数据转换就是“精心烹饪”,它将分散、原始的数据转化为结构化、有意义的洞察。在Pandas的武器库中,groupby、聚合、合并与重塑是数据转换的“四大神器”,掌握它们意味着你能够:
从细节中发现模式:通过分组与聚合,从海量数据中提取关键统计信息
连接分散的信息孤岛:通过合并操作,整合多个数据源形成完整视图
自由变换数据视角:通过重塑操作,为不同分析需求准备合适的数据结构
想象一下这样的场景:你手中有客户的交易数据、产品信息和用户 demographics 数据,你需要回答“哪些产品类别在不同年龄段的客户中最受欢迎?”或“各地区的销售趋势如何?”。没有数据转换技能,这些问题将遥不可及;掌握本章技能,你将能轻松应对。
让我们开始探索Pandas数据转换的艺术,这些技能将直接决定你从“数据操作员”成长为“数据分析师”的关键一步。
第一章:分组艺术:深入理解groupby机制
1.1 groupby的本质:拆分-应用-组合模式
Pandas的groupby操作遵循着经典的“拆分-应用-组合”模式,理解这一模式是掌握分组操作的关键:
importpandasaspdimportnumpyasnpimportmatplotlib.pyplotasplt# 创建示例数据集:电商销售数据np.random.seed(42)n_records=1000sales_data={'订单ID':[f'ORD{10000+i}'foriinrange(n_records)],'日期':pd.date_range('2023-01-01',periods=n_records,freq='D'),'产品类别':np.random.choice(['电子产品','家居用品','服装','食品','书籍'],n_records),'子类别':np.random.choice(['手机','电脑','厨具','家具','男装','女装','零食','饮料','小说','专业书籍'],n_records),'地区':np.random.choice(['华东','华北','华南','华中','西南','西北','东北'],n_records),'销售员':np.random.choice(['张三','李四','王五','赵六','钱七'],n_records),'销售额':np.random.uniform(100,5000,n_records).round(2),'利润':np.random.uniform(10,1000,n_records).round(2),'数量':np.random.randint(1,10,n_records)}sales_df=pd.DataFrame(sales_data)sales_df['月份']=sales_df['日期'].dt.month sales_df['季度']=sales_df['日期'].dt.quarter sales_df['星期']=sales_df['日期'].dt.day_name()print("电商销售数据集(前10行):")print(sales_df.head(10))print(f"\n数据集形状:{sales_df.shape}")print(f"时间范围:{sales_df['日期'].min()}到{sales_df['日期'].max()}")图1:groupby的拆分-应用-组合模式示意图
原始DataFrame ↓ 按指定键"拆分" ↓ ┌─────────┬─────────┬─────────┐ │ 分组1 │ 分组2 │ 分组3 │ │ (键值A) │ (键值B) │ (键值C) │ └─────────┴─────────┴─────────┘ ↓ 对每个分组"应用"函数 (聚合、转换、过滤等) ↓ "组合"结果到新数据结构 ↓ 输出结果1.2 单键与多键分组
分组操作可以从简单的单键分组到复杂的多键层级分组:
# 1. 单键分组:按产品类别分组category_group=sales_df.groupby('产品类别')print(f"按产品类别分组:共{len(category_group)}个组")print("分组键值:",list(category_group.groups.keys()))# 查看一个分组的内容print("\n'电子产品'分组的前5行:")print(category_group.get_group('电子产品').head())# 2. 多键分组:按产品和地区分组multi_group=sales_df.groupby(['产品类别','地区'])print(f"\n按产品和地区分组:共{len(multi_group)}个组")# 查看分组详情print("\n分组大小(前10个):")fori,(name,group)inenumerate(multi_group):ifi<10:print(f" 分组{name}:{len(group)}条记录")else:break# 3. 查看分组统计group_sizes=sales_df.groupby(['产品类别','地区']).size()print("\n各产品-地区组合的记录数量:")print(group_sizes.head(15))1.3 groupby对象的内部结构与迭代
了解groupby对象的内部结构有助于更高效地使用它:
# 深入探索groupby对象print("groupby对象类型:",type(category_group))print("\ngroupby对象属性:")# 查看分组信息print("1. 分组键:",category_group.keys)print("2. 分组数:",category_group.ngroups)print("3. 分组大小:")print(category_group.size())# 迭代groupby对象print("\n迭代groupby对象(前3个组):")fori,(group_name,group_data)inenumerate(category_group):ifi<3:print(f"\n组名:{group_name}")print(f" 记录数:{len(group_data)}")print(f" 销售额均值:{group_data['销售额'].mean():.2f}")print(f" 数据形状:{group_data.shape}")# 访问特定分组print("\n访问特定分组:")electronics_group=category_group.get_group('电子产品')print(f"'电子产品'组:{len(electronics_group)}条记录")print(f"平均销售额:{electronics_group['销售额'].mean():.2f}")1.4 分组键的多种形式
分组键不仅可以是列名,还可以是更复杂的表达式:
# 1. 列名作为分组键(最常用)by_column=sales_df.groupby('产品类别')# 2. Series作为分组键by_series=sales_df.groupby(sales_df['产品类别'])# 3. 函数作为分组键# 按销售额是否超过平均值分组defhigh_sales_flag(row):return'高销售额'ifrow['销售额']>sales_df['销售额'].mean()else'低销售额'# 注意:使用apply或transform时,函数应用到每行sales_df['销售额等级']=sales_df.apply(high_sales_flag,axis=1)by_function=sales_df.groupby('销售额等级')print("\n按销售额等级分组统计:")print(by_function['销售额'].describe())# 4. 字典或Series映射# 创建产品类别到部门的映射category_to_dept={'电子产品':'数码部','家居用品':'家居部','服装':'服装部','食品':'食品部','书籍':'图书部'}sales_df['部门']=sales_df['产品类别'].map(category_to_dept)by_mapping=sales_df.groupby('部门')print("\n按部门分组统计:")print(by_mapping.agg({'销售额':'sum','利润':'sum','数量':'sum'}))# 5. 多个不同形式的键组合complex_group=sales_df.groupby(['部门',sales_df['日期'].dt.month,'销售额等级'])print(f"\n复杂分组键的分组数:{complex_group.ngroups}")第二章:聚合操作:从分组到洞察
2.1 基本聚合方法
聚合是groupby最常用的操作,Pandas提供了多种聚合方式:
# 1. 单聚合函数print("按产品类别的销售额统计:")category_stats=sales_df.groupby('产品类别')['销售额'].agg('sum')print(category_stats)# 2. 多聚合函数print("\n按产品类别的多指标统计:")multi_agg=sales_df.groupby('产品类别').agg({'销售额':['sum','mean','std','count'],'利润':['sum','mean'],'数量':'sum'})print(multi_agg)# 3. 命名聚合结果(Pandas 0.25+)named_agg=sales_df.groupby('产品类别').agg(总销售额=('销售额','sum'),平均销售额=('销售额','mean'),销售次数=('销售额','count'),总利润=('利润','sum'),利润率=('利润',lambdax:x.sum()/sales_df.loc[x.index,'销售额'].sum()))print("\n命名聚合结果:")print(named_agg)2.2 自定义聚合函数
当内置聚合函数不满足需求时,可以定义自己的聚合函数:
# 1. 简单的自定义聚合函数defsales_range(series):"""计算销售范围(最大值-最小值)"""returnseries.max()-series.min()defprofit_margin(group):"""计算利润率:总利润/总销售额"""total_profit=group['利润'].sum()total_sales=group['销售额'].sum()returntotal_profit/total_salesiftotal_sales>0else0# 应用自定义聚合函数custom_agg=sales_df.groupby('产品类别').agg({'销售额':['sum','mean',sales_range],'利润':['sum',profit_margin]})print("自定义聚合函数结果:")print(custom_agg)# 2. 返回多个值的聚合函数defdescribe_custom(series):"""类似describe但返回自定义统计量"""returnpd.Series({'总和':series.sum(),'均值':series.mean(),'中位数':series.median(),'标准差':series.std(),'变异系数':series.std()/series.mean()ifseries.mean()>0else0,'最小值':series.min(),'最大值':series.max(),'范围':series.max()-series.min()})# 应用返回多个值的聚合函数detailed_stats=sales_df.groupby('产品类别')['销售额'].apply(describe_custom)print("\n详细统计信息:")print(detailed_stats)2.3 条件聚合与分组筛选
有时我们需要基于条件进行聚合,或在聚合前进行筛选:
# 1. 分组后条件聚合# 计算每个产品类别中高销售额(>2000)的比例defhigh_sales_proportion(group):high_sales=group[group['销售额']>2000]returnlen(high_sales)/len(group)iflen(group)>0else0conditional_agg=sales_df.groupby('产品类别').apply(lambdag:pd.Series({'总记录数':len(g),'高销售额记录数':len(g[g['销售额']>2000]),'高销售额比例':high_sales_proportion(g),'高销售额总额':g[g['销售额']>2000]['销售额'].sum()}))print("条件聚合结果:")print(conditional_agg)# 2. 分组前筛选# 只考虑销售额大于500的记录filtered_agg=sales_df[sales_df['销售额']>500].groupby('产品类别').agg({'销售额':['sum','mean','count']})print("\n筛选后的聚合结果:")print(filtered_agg)# 3. 分组后筛选(使用filter方法)# 筛选出记录数大于100的分组large_groups=sales_df.groupby('产品类别').filter(lambdax:len(x)>100)print(f"\n筛选后数据形状:{large_groups.shape}")print("剩余分组:",large_groups['产品类别'].unique())图2:聚合操作流程图
原始分组 ↓ 选择聚合方式 ├── 内置聚合函数 (sum, mean, count...) ├── 自定义聚合函数 └── 多函数聚合 ↓ 应用聚合操作 ↓ 输出聚合结果 ├── Series (单列聚合) ├── DataFrame (多列聚合) └── 多级索引DataFrame (多键分组)2.4 时间序列分组聚合
对于时间序列数据,时间维度分组尤为重要:
# 时间序列分组聚合示例print("时间序列分组聚合:")# 1. 按月份聚合monthly_sales=sales_df.groupby(sales_df['日期'].dt.to_period('M')).agg({'销售额':'sum','利润':'sum','数量':'sum','订单ID':'count'}).rename(columns={'订单ID':'订单数'})print("月度销售统计:")print(monthly_sales)# 2. 按季度和产品类别聚合quarterly_by_category=sales_df.groupby([sales_df['日期'].dt.to_period('Q'),'产品类别']).agg({'销售额':'sum'}).unstack()print("\n分季度、产品类别的销售额:")print(quarterly_by_category)# 3. 按周几聚合weekday_sales=sales_df.groupby('星期').agg({'销售额':['sum','mean','count']}).reindex(['Monday','Tuesday','Wednesday','Thursday','Friday','Saturday','Sunday'])print("\n星期销售统计:")print(weekday_sales)# 4. 滚动时间窗口聚合# 创建时间索引sales_time=sales_df.set_index('日期').sort_index()# 计算7天滚动平均销售额rolling_7d=sales_time['销售额'].rolling('7D').mean()print("\n滚动窗口聚合示例:")print("原始数据形状:",sales_time.shape)print("7天滚动平均形状:",rolling_7d.shape)print("\n滚动平均(前10天):")print(rolling_7d.head(10))第三章:数据合并:连接多源信息
3.1 合并操作基础:merge与join
Pandas提供了多种合并数据的方法,最常用的是merge()和join():
# 创建多个相关数据集# 1. 订单详情表orders=sales_df[['订单ID','日期','产品类别','销售额','利润','数量']].copy()# 2. 产品信息表products=pd.DataFrame({'产品类别':['电子产品','家居用品','服装','食品','书籍'],'部门':['数码部','家居部','服装部','食品部','图书部'],'负责人':['张三','李四','王五','赵六','钱七'],'毛利率目标':[0.25,0.30,0.40,0.20,0.35]# 目标毛利率})# 3. 地区信息表regions=pd.DataFrame({'地区':['华东','华北','华南','华中','西南','西北','东北'],'区域经理':['经理A','经理B','经理C','经理D','经理E','经理F','经理G'],'地区级别':['一线','一线','一线','二线','二线','三线','二线']})# 4. 销售员信息表salespersons=pd.DataFrame({'销售员':['张三','李四','王五','赵六','钱七'],'入职日期':['2020-01-15','2019-03-20','2021-06-10','2018-11-05','2022-02-28'],'级别':['高级','中级','初级','高级','初级'],'所属地区':['华东','华北','华南','华中','西南']})# 转换日期格式salespersons['入职日期']=pd.to_datetime(salespersons['入职日期'])print("各数据表信息:")print(f"订单表:{orders.shape}")print(f"产品表:{products.shape}")print(f"地区表:{regions.shape}")print(f"销售员表:{salespersons.shape}")3.2 合并类型详解
SQL风格的合并操作在Pandas中通过how参数实现:
# 1. 内连接(inner join) - 默认inner_merged=pd.merge(orders,products,on='产品类别',how='inner')print("内连接结果形状:",inner_merged.shape)print("内连接示例(前5行):")print(inner_merged.head())# 2. 左连接(left join)left_merged=pd.merge(orders,products,on='产品类别',how='left')print(f"\n左连接结果形状:{left_merged.shape}")print("左连接保留所有订单记录:",left_merged.shape[0]==orders.shape[0])# 3. 右连接(right join)right_merged=pd.merge(orders,products,on='产品类别',how='right')print(f"\n右连接结果形状:{right_merged.shape}")print("右连接保留所有产品记录:",right_merged.shape[0]>=products.shape[0])# 4. 外连接(outer join)outer_merged=pd.merge(orders,products,on='产品类别',how='outer')print(f"\n外连接结果形状:{outer_merged.shape}")print("外连接保留所有记录:",outer_merged.shape[0]>=max(orders.shape[0],products.shape[0]))# 5. 多键合并# 先给orders添加一个虚拟的地区列用于演示orders_sample=orders.head(100).copy()orders_sample['地区']=np.random.choice(regions['地区'],100)multi_key_merged=pd.merge(orders_sample,salespersons,left_on=['产品类别','地区'],right_on=['所属地区','销售员'],how='inner')print(f"\n多键合并结果形状:{multi_key_merged.shape}")图3:合并类型可视化示意图
内连接(inner) 左连接(left) 右连接(right) 外连接(outer) A B A B A B A B ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ │1│ │1│ │1│←→│1│ │1│←→│1│ │1│←→│1│ │2│←→│2│ │2│ │2│ │2│←→│2│ │2│←→│2│ │3│ │4│ │3│ └─┘ └─┘ │3│ │3│ │3│ └─┘ └─┘ └─┘ │4│ └─┘ │4│ 交集部分←→ A全保留←→ B全保留←→ 全部保留←→3.3 高级合并技巧
# 1. 处理重复列名(suffixes参数)# 创建有重叠列名的DataFramesales_summary=orders.groupby('产品类别').agg({'销售额':'sum','利润':'sum'}).reset_index()profit_summary=orders.groupby('产品类别').agg({'利润':'mean',# 平均利润'数量':'sum'}).reset_index()# 合并时自动添加后缀merged_with_suffix=pd.merge(sales_summary,profit_summary,on='产品类别',suffixes=('_总计','_平均'))print("添加后缀的合并结果:")print(merged_with_suffix)# 2. 根据索引合并# 设置索引后合并products_indexed=products.set_index('产品类别')orders_with_index=orders.set_index('产品类别')merged_by_index=pd.merge(orders_with_index,products_indexed,left_index=True,right_index=True,how='left')print(f"\n按索引合并结果形状:{merged_by_index.shape}")# 3. 使用join方法(基于索引的合并)joined=orders_with_index.join(products_indexed,how='left')print(f"\n使用join方法结果形状:{joined.shape}")# 4. 合并多个DataFramefromfunctoolsimportreducedataframes=[orders,products,regions]# 注意:这里需要实际可合并的列,仅为示例print("\n合并多个DataFrame示例:")# 实际应用中需要确保有共同的键3.4 性能优化与大型数据集合并
# 大型数据集合并优化技巧importtime# 创建大型数据集np.random.seed(42)n_large=100000large_orders=pd.DataFrame({'订单ID':range(n_large),'产品ID':np.random.randint(1,1000,n_large),'销售额':np.random.uniform(10,1000,n_large)})large_products=pd.DataFrame({'产品ID':range(1,1001),'产品名称':[f'产品_{i}'foriinrange(1,1001)],'类别':np.random.choice(['A','B','C','D'],1000)})print("大型数据集信息:")print(f"订单表:{large_orders.shape}")print(f"产品表:{large_products.shape}")# 1. 基准合并start=time.time()basic_merge=pd.merge(large_orders,large_products,on='产品ID',how='left')basic_time=time.time()-startprint(f"\n基准合并时间:{basic_time:.4f}秒")# 2. 优化1:设置适当的数据类型large_orders_opt=large_orders.copy()large_products_opt=large_products.copy()# 将ID列转换为更小的数据类型large_orders_opt['产品ID']=large_orders_opt['产品ID'].astype('int32')large_products_opt['产品ID']=large_products_opt['产品ID'].astype('int32')start=time.time()opt1_merge=pd.merge(large_orders_opt,large_products_opt,on='产品ID',how='left')opt1_time=time.time()-startprint(f"优化1(数据类型)合并时间:{opt1_time:.4f}秒,提升:{basic_time/opt1_time:.1f}倍")# 3. 优化2:先筛选再合并# 如果只需要某些类别的产品start=time.time()filtered_products=large_products[large_products['类别'].isin(['A','B'])]opt2_merge=pd.merge(large_orders,filtered_products,on='产品ID',how='inner')opt2_time=time.time()-startprint(f"优化2(先筛选)合并时间:{opt2_time:.4f}秒,结果形状:{opt2_merge.shape}")第四章:数据重塑:变换数据视角
4.1 宽表与长表转换:melt与pivot
数据重塑的核心是改变数据的形状而不改变其内容:
# 创建宽格式数据wide_data=sales_df.pivot_table(index='日期',columns='产品类别',values='销售额',aggfunc='sum').fillna(0)print("宽格式数据(日期×产品类别):")print(wide_data.head())print(f"形状:{wide_data.shape}")# 1. melt:宽表转长表long_data=wide_data.reset_index().melt(id_vars='日期',value_vars=['电子产品','家居用品','服装','食品','书籍'],var_name='产品类别',value_name='销售额')print("\n宽表转长表结果:")print(long_data.head())print(f"形状:{long_data.shape}")# 2. pivot:长表转宽表# 注意:pivot不能处理重复值,需要先聚合pivot_data=sales_df.pivot_table(index='日期',columns='产品类别',values=['销售额','利润'],# 多值列aggfunc='sum').fillna(0)print("\n长表转宽表结果(多值列):")print(pivot_data.head())print(f"形状:{pivot_data.shape}")print("列结构:",pivot_data.columns.tolist()[:10])4.2 多层索引操作:stack与unstack
多层索引是处理高维数据的强大工具:
# 创建多层索引DataFramemulti_index_df=sales_df.groupby(['日期','产品类别','地区']).agg({'销售额':'sum','利润':'sum'}).unstack(['产品类别','地区'])print("多层索引DataFrame:")print(multi_index_df.head())print(f"形状:{multi_index_df.shape}")print("列索引级别:",multi_index_df.columns.nlevels)print("行索引级别:",multi_index_df.index.nlevels)# 1. stack:将列索引的层级旋转为行索引stacked=multi_index_df.stack(['产品类别','地区'])print("\nstack操作后:")print(stacked.head())print(f"形状:{stacked.shape}")# 2. unstack:将行索引的层级旋转为列索引unstacked=stacked.unstack(['产品类别','地区'])print("\nunstack操作后(恢复原状):")print(unstacked.head())print(f"形状:{unstacked.shape}")# 3. 选择性stack/unstackpartial_unstack=stacked.unstack('产品类别')print("\n只unstack产品类别:")print(partial_unstack.head())print(f"形状:{partial_unstack.shape}")4.3 数据透视表:pivot_table高级功能
pivot_table是比pivot更强大的工具,支持数据聚合:
# 高级pivot_table应用print("高级数据透视表示例:")# 1. 多级行列、多值透视advanced_pivot=sales_df.pivot_table(index=['季度','地区'],columns=['产品类别','销售额等级'],values=['销售额','利润'],aggfunc={'销售额':['sum','mean','count'],'利润':'sum'},fill_value=0,margins=True,# 添加总计margins_name='总计')print("多级透视表结构:")print(f"行索引:{advanced_pivot.index.names}")print(f"列索引:{advanced_pivot.columns.names}")print(f"形状:{advanced_pivot.shape}")# 2. 使用不同的聚合函数custom_pivot=sales_df.pivot_table(index='产品类别',columns='销售额等级',values=['销售额','利润'],aggfunc={'销售额':['sum','mean',lambdax:x.std()/x.mean()],# 变异系数'利润':['sum','mean','count']},fill_value=0)print("\n自定义聚合函数透视表:")print(custom_pivot)# 3. 处理缺失值pivot_with_na=sales_df.pivot_table(index='地区',columns='产品类别',values='销售额',aggfunc='mean',fill_value=None,# 保留NaNdropna=False# 不删除全为NaN的列)print("\n保留NaN的透视表:")print(pivot_with_na)图4:数据重塑操作转换示意图
原始长格式数据 日期 类别 销售额 2023-01-01 A 100 2023-01-01 B 200 2023-01-02 A 150 pivot/unstack ↓ 宽格式数据 日期 A B 2023-01-01 100 200 2023-01-02 150 NaN melt/stack ↓ 长格式数据 日期 类别 销售额 2023-01-01 A 100 2023-01-01 B 200 2023-01-02 A 150 2023-01-02 B NaN4.4 交叉表:crosstab快速频率分析
crosstab是专门用于计算频率交叉表的工具:
# crosstab应用print("交叉表示例:")# 1. 基本交叉表basic_crosstab=pd.crosstab(index=sales_df['产品类别'],columns=sales_df['地区'],margins=True,# 添加总计margins_name='合计')print("产品类别×地区交叉表:")print(basic_crosstab)# 2. 多级交叉表multi_crosstab=pd.crosstab(index=[sales_df['产品类别'],sales_df['销售额等级']],columns=sales_df['地区'],margins=True)print("\n多级交叉表:")print(multi_crosstab.head(10))# 3. 带值的交叉表(类似透视表)crosstab_with_values=pd.crosstab(index=sales_df['产品类别'],columns=sales_df['地区'],values=sales_df['销售额'],aggfunc='mean',normalize=False# 不标准化).round(2)print("\n带平均销售额的交叉表:")print(crosstab_with_values)# 4. 标准化交叉表normalized_crosstab=pd.crosstab(index=sales_df['产品类别'],columns=sales_df['地区'],normalize='index'# 按行标准化).round(3)print("\n按行标准化的交叉表:")print(normalized_crosstab)第五章:综合实战:电商数据分析全流程
5.1 业务场景与数据准备
让我们通过一个完整的电商数据分析案例,综合应用本章所学技能:
# 综合实战:电商数据分析print("="*60)print("综合实战:电商数据分析全流程")print("="*60)# 准备完整数据集# 1. 销售事实表fact_sales=sales_df.copy()# 2. 产品维度表dim_products=pd.DataFrame({'产品类别':['电子产品','家居用品','服装','食品','书籍'],'部门':['数码部','家居部','服装部','食品部','图书部'],'负责人':['张三','李四','王五','赵六','钱七'],'采购成本率':[0.60,0.50,0.40,0.70,0.45],# 成本占售价比例'库存周转目标':[4.0,3.0,6.0,12.0,2.0]# 目标年周转次数})# 3. 时间维度表dates=pd.DataFrame({'日期':pd.date_range('2023-01-01','2023-12-31'),'年份':lambdadf:df['日期'].dt.year,'季度':lambdadf:df['日期'].dt.quarter,'月份':lambdadf:df['日期'].dt.month,'星期':lambdadf:df['日期'].dt.day_name(),'是否周末':lambdadf:df['日期'].dt.dayofweek>=5,'是否节假日':lambdadf:df['日期'].isin(['2023-01-01','2023-01-22','2023-01-23','2023-01-24',# 元旦、春节'2023-04-05','2023-05-01','2023-06-22','2023-09-29',# 清明、劳动、端午、中秋'2023-10-01','2023-10-02','2023-10-03'# 国庆])})# 4. 地区维度表dim_regions=pd.DataFrame({'地区':['华东','华北','华南','华中','西南','西北','东北'],'区域经理':['经理A','经理B','经理C','经理D','经理E','经理F','经理G'],'地区级别':['一线','一线','一线','二线','二线','三线','二线'],'GDP等级':['高','高','高','中','中','低','中']})print("数据表准备完成:")print(f"销售事实表:{fact_sales.shape}")print(f"产品维度表:{dim_products.shape}")print(f"时间维度表:{dates.shape}")print(f"地区维度表:{dim_regions.shape}")5.2 数据整合与多维分析
# 数据整合:创建星型模型# 1. 连接事实表与维度表enriched_sales=pd.merge(fact_sales,dim_products,on='产品类别',how='left')enriched_sales=pd.merge(enriched_sales,dim_regions,on='地区',how='left')# 添加时间维度属性enriched_sales=enriched_sales.merge(dates,left_on='日期',right_on='日期',how='left')print("整合后的数据:")print(f"形状:{enriched_sales.shape}")print("列:",enriched_sales.columns.tolist())# 2. 计算衍生指标enriched_sales['毛利率']=enriched_sales['利润']/enriched_sales['销售额']enriched_sales['客单价']=enriched_sales['销售额']/enriched_sales['数量']enriched_sales['是否达标']=enriched_sales['毛利率']>enriched_sales['采购成本率']print("\n添加衍生指标后的数据(前5行):")print(enriched_sales[['订单ID','产品类别','销售额','利润','毛利率','客单价','是否达标']].head())5.3 多维度业务分析
# 业务分析1:各产品类别的销售表现print("\n1. 各产品类别销售表现分析")category_performance=enriched_sales.groupby(['部门','产品类别']).agg({'销售额':['sum','mean','count'],'利润':['sum','mean'],'毛利率':'mean','客单价':'mean'}).round(2)category_performance.columns=['_'.join(col).strip()forcolincategory_performance.columns.values]category_performance=category_performance.rename(columns={'销售额_sum':'总销售额','销售额_mean':'平均订单额','销售额_count':'订单数','利润_sum':'总利润','利润_mean':'平均利润','毛利率_mean':'平均毛利率','客单价_mean':'平均客单价'})print("产品类别表现:")print(category_performance)# 业务分析2:时间趋势分析print("\n2. 销售时间趋势分析")# 月度趋势monthly_trend=enriched_sales.groupby(['年份','月份']).agg({'销售额':'sum','利润':'sum','订单ID':'count'}).rename(columns={'订单ID':'订单数'})print("月度销售趋势:")print(monthly_trend)# 周内模式weekday_pattern=enriched_sales.groupby('星期').agg({'销售额':['sum','mean'],'订单ID':'count'}).rename(columns={'订单ID':'订单数'})print("\n星期销售模式:")print(weekday_pattern)# 业务分析3:地区分析print("\n3. 地区表现分析")region_performance=enriched_sales.groupby(['地区级别','地区']).agg({'销售额':'sum','利润':'sum','毛利率':'mean','订单ID':'count'}).round(2)region_performance=region_performance.sort_values(['地区级别','销售额'],ascending=[True,False])print("地区表现排名:")print(region_performance)5.4 高级分析:透视与钻取
# 创建高级数据立方体print("\n4. 高级数据立方体分析")# 创建数据透视立方体sales_cube=pd.pivot_table(enriched_sales,index=['部门','产品类别'],columns=['季度','地区级别'],values=['销售额','利润'],aggfunc={'销售额':'sum','利润':'sum'},fill_value=0,margins=True,margins_name='总计')print("销售数据立方体(部分):")# 只显示部分数据以避免输出过长print(sales_cube.iloc[:5,:10])# 向下钻取:查看特定维度的详细数据print("\n5. 向下钻取分析:电子产品部门详情")electronics_detail=enriched_sales[enriched_sales['部门']=='数码部'].groupby(['产品类别','地区']).agg({'销售额':'sum','利润':'sum','毛利率':'mean'}).round(2)print("电子产品部门详情:")print(electronics_detail)# 向上卷起:汇总到更高层级print("\n6. 向上卷起:部门级别汇总")department_summary=enriched_sales.groupby('部门').agg({'销售额':'sum','利润':'sum','毛利率':lambdax:x.mean(),'订单ID':'count'}).round(2)department_summary=department_summary.rename(columns={'订单ID':'订单数'})department_summary['销售额占比']=(department_summary['销售额']/department_summary['销售额'].sum()*100).round(1)department_summary=department_summary.sort_values('销售额',ascending=False)print("部门级别汇总:")print(department_summary)5.5 可视化呈现分析结果
# 可视化分析结果importmatplotlib.pyplotasplt fig,axes=plt.subplots(2,2,figsize=(15,12))# 1. 产品类别销售额占比(饼图)sales_by_category=enriched_sales.groupby('产品类别')['销售额'].sum()axes[0,0].pie(sales_by_category.values,labels=sales_by_category.index,autopct='%1.1f%%')axes[0,0].set_title('各产品类别销售额占比')# 2. 月度销售趋势(折线图)monthly_sales_trend=enriched_sales.groupby('月份')['销售额'].sum()axes[0,1].plot(monthly_sales_trend.index,monthly_sales_trend.values,marker='o',linewidth=2)axes[0,1].set_title('月度销售趋势')axes[0,1].set_xlabel('月份')axes[0,1].set_ylabel('销售额')axes[0,1].grid(True,alpha=0.3)# 3. 地区销售额(柱状图)sales_by_region=enriched_sales.groupby('地区')['销售额'].sum().sort_values(ascending=False)axes[1,0].bar(range(len(sales_by_region)),sales_by_region.values)axes[1,0].set_title('各地区销售额')axes[1,0].set_xlabel('地区')axes[1,0].set_ylabel('销售额')axes[1,0].set_xticks(range(len(sales_by_region)))axes[1,0].set_xticklabels(sales_by_region.index,rotation=45)# 4. 产品类别利润率(水平柱状图)margin_by_category=enriched_sales.groupby('产品类别')['毛利率'].mean().sort_values()axes[1,1].barh(range(len(margin_by_category)),margin_by_category.values)axes[1,1].set_title('各产品类别平均毛利率')axes[1,1].set_xlabel('毛利率')axes[1,1].set_yticks(range(len(margin_by_category)))axes[1,1].set_yticklabels(margin_by_category.index)plt.tight_layout()plt.show()# 保存分析结果analysis_results={'category_performance':category_performance,'monthly_trend':monthly_trend,'region_performance':region_performance,'department_summary':department_summary}# 保存到Excel的不同sheetwithpd.ExcelWriter('sales_analysis_report.xlsx')aswriter:forsheet_name,dfinanalysis_results.items():df.to_excel(writer,sheet_name=sheet_name[:31])# Excel sheet名最多31字符print("\n分析报告已保存到 'sales_analysis_report.xlsx'")图5:数据转换在分析工作流中的位置
第六章:性能优化与最佳实践
6.1 groupby性能优化
# groupby性能优化技巧print("groupby性能优化技巧:")# 1. 使用合适的分组键顺序# 当分组键的基数(唯一值数量)差异大时,将基数小的键放在前面print("1. 分组键顺序优化:")key_cardinality={'产品类别':enriched_sales['产品类别'].nunique(),'地区':enriched_sales['地区'].nunique(),'季度':enriched_sales['季度'].nunique()}print(" 各键基数:",key_cardinality)print(" 建议顺序:",sorted(key_cardinality,key=key_cardinality.get))# 2. 仅选择需要的列print("\n2. 列选择优化:")# 不好的做法:选择所有列start=time.time()slow_groupby=enriched_sales.groupby('产品类别').mean()slow_time=time.time()-start# 好的做法:只选择需要的列start=time.time()fast_groupby=enriched_sales[['产品类别','销售额','利润']].groupby('产品类别').mean()fast_time=time.time()-startprint(f" 全列分组时间:{slow_time:.4f}秒")print(f" 选择列分组时间:{fast_time:.4f}秒")print(f" 性能提升:{slow_time/fast_time:.1f}倍")# 3. 使用as_index参数print("\n3. as_index参数优化:")# 当需要将分组键作为列而不是索引时grouped_as_column=enriched_sales.groupby('产品类别',as_index=False).agg({'销售额':'sum'})print(" 分组键作为列:",'产品类别'ingrouped_as_column.columns)6.2 合并操作优化
# 合并操作优化print("\n合并操作优化:")# 创建测试数据df1=pd.DataFrame({'key':range(10000),'value1':np.random.randn(10000)})df2=pd.DataFrame({'key':range(5000,15000),# 部分重叠'value2':np.random.randn(10000)})# 1. 使用合适的合并类型print("1. 合并类型选择:")# 内连接通常最快inner_time=time.time()result_inner=pd.merge(df1,df2,on='key',how='inner')inner_time=time.time()-inner_time# 外连接较慢outer_time=time.time()result_outer=pd.merge(df1,df2,on='key',how='outer')outer_time=time.time()-outer_timeprint(f" 内连接时间:{inner_time:.4f}秒,结果形状:{result_inner.shape}")print(f" 外连接时间:{outer_time:.4f}秒,结果形状:{result_outer.shape}")# 2. 设置索引加速合并print("\n2. 索引优化:")# 无索引合并df1_no_index=df1.copy()df2_no_index=df2.copy()start=time.time()merge_no_index=pd.merge(df1_no_index,df2_no_index,on='key')time_no_index=time.time()-start# 设置索引后合并df1_indexed=df1.set_index('key')df2_indexed=df2.set_index('key')start=time.time()merge_indexed=pd.merge(df1_indexed,df2_indexed,left_index=True,right_index=True)time_indexed=time.time()-startprint(f" 无索引合并时间:{time_no_index:.4f}秒")print(f" 有索引合并时间:{time_indexed:.4f}秒")6.3 内存使用优化
# 内存使用优化print("\n内存使用优化:")# 1. 使用category类型减少内存print("1. 分类数据类型优化:")# 转换前的内存使用original_memory=enriched_sales.memory_usage(deep=True).sum()/1024**2# 转换分类列categorical_cols=['产品类别','部门','地区','地区级别','GDP等级','星期']enriched_sales_opt=enriched_sales.copy()forcolincategorical_cols:ifcolinenriched_sales_opt.columns:enriched_sales_opt[col]=enriched_sales_opt[col].astype('category')optimized_memory=enriched_sales_opt.memory_usage(deep=True).sum()/1024**2print(f" 原始内存使用:{original_memory:.2f}MB")print(f" 优化后内存使用:{optimized_memory:.2f}MB")print(f" 内存减少:{(1-optimized_memory/original_memory)*100:.1f}%")# 2. 使用合适的数据类型print("\n2. 数值数据类型优化:")# 检查数值列的数据类型numeric_cols=enriched_sales.select_dtypes(include=[np.number]).columnsprint(" 数值列数据类型:")forcolinnumeric_cols[:5]:# 只显示前5个dtype=enriched_sales[col].dtypeprint(f"{col}:{dtype}")结语
核心能力总结
通过本章的学习,你已经掌握了Pandas数据转换的四大核心能力:
分组艺术(groupby):深刻理解了"拆分-应用-组合"模式,能够灵活运用单键、多键及复杂条件分组,将海量数据按需分割为有意义的子集。
聚合智慧:掌握了从简单统计到复杂自定义聚合的全套方法,能够将分组数据转化为具有业务洞察力的汇总信息,发现数据中的模式和趋势。
合并技巧:熟练运用多种合并策略连接分散的数据源,理解不同连接类型(内连、左连、右连、外连)的适用场景,能够构建完整的数据视图。
重塑能力:掌握了数据结构的自由变换技巧,能够在宽表与长表之间灵活转换,利用多层索引处理高维数据,满足不同分析需求的数据格式要求。
最佳实践建议
理解业务需求再选择技术:数据转换不是目的,而是手段。始终从业务问题出发,选择最适合的转换方法。
分层构建复杂转换:对于复杂的转换需求,分步骤、分层级地构建,每步都验证结果,确保转换的正确性。
性能与可读性平衡:在追求性能优化的同时,保持代码的可读性和可维护性,复杂操作添加必要注释。
充分利用向量化操作:避免在groupby等操作中使用Python循环,充分利用Pandas的向量化能力提升性能。
内存敏感处理大数据:处理大型数据集时,注意数据类型优化和内存使用,适时使用分块处理或抽样分析。
保存中间结果:对于耗时的复杂转换,保存关键中间结果,便于调试和复现分析过程。
持续学习路径
深入高级分组技术:学习使用transform和apply进行更复杂的分组操作,掌握分组后的数据标准化、排名等高级技巧。
掌握时间序列分组:深入学习基于时间窗口的分组操作,如滚动窗口、扩展窗口和指数加权移动平均等时间序列特定技术。
学习Dask等分布式计算:当数据量超出单机内存时,学习使用Dask等分布式计算框架处理Pandas-like操作。
数据库集成技能:掌握将Pandas操作转换为SQL语句的能力,学习直接与数据库交互进行大数据处理。
可视化集成:学习将数据转换结果无缝集成到可视化流程中,掌握使用Plotly、Seaborn等库进行高级可视化。
实战项目积累:在Kaggle、天池等平台参与真实项目,将数据转换技能应用于更复杂、更真实的业务场景中。
数据转换是数据分析师从"数据工人"成长为"数据艺术家"的关键一步。它不仅仅是技术的堆砌,更是对业务理解、逻辑思维和创造力的综合考验。每一行转换代码背后,都是对业务问题的深刻思考;每一个转换结果,都可能是新洞察的起点。继续精进这些技能,你将在数据世界中游刃有余,从数据中发现的不仅是信息,更是价值。