欢迎光临
我们一直在努力

数据预处理入门学习教程,从入门到精通,数据集成、变换与规约 — 完整知识点与案例代码(6)

数据集成、变换与规约 — 完整知识点与案例代码


一、数据集成

1.1 数据集成概述

数据集成是将多个数据源中的数据合并到一个统一的数据存储中的过程。主要涉及三个关键问题:

问题说明
实体识别 不同数据源中同一实体的名称可能不同(如"学号"vs"ID")
冗余处理 同一属性在多个表中出现,需检测并消除
数据值冲突 同一实体在不同数据源中的属性值不同

1.2 合并数据

1.2.1 merge() 函数 — 基于键的合并

import pandas as pd
import numpy as np

# ============================================================
# 案例1:基本 merge 合并(内连接)
# ============================================================

# 创建学生成绩表
df_score = pd.DataFrame({
'学号': ['S001', 'S002', 'S003', 'S004'], # 学号列,作为关联键
'数学': [90, 85, 78, 92], # 数学成绩
'英语': [88, 92, 75, 85] # 英语成绩
})

# 创建学生信息表
df_info = pd.DataFrame({
'学号': ['S001', 'S002', 'S003', 'S005'], # 学号列,注意S004缺失,S005为新增
'姓名': ['张三', '李四', '王五', '赵六'], # 姓名列
'性别': ['男', '女', '男', '男'] # 性别列
})

# 使用 merge 进行内连接(默认 how='inner')
# 只保留两个表中都存在的学号(S001, S002, S003)
df_inner = pd.merge(df_score, df_info, on='学号', how='inner')
print("内连接结果:")
print(df_inner)
# 学号 数学 英语 姓名 性别
# 0 S001 90 88 张三 男
# 1 S002 85 92 李四 女
# 2 S003 78 75 王五 男

# ============================================================
# 案例2:四种连接方式对比
# ============================================================

# 左连接:以左表为基准,右表中没有匹配的用 NaN 填充
df_left = pd.merge(df_score, df_info, on='学号', how='left')
print("\\n左连接结果:")
print(df_left)
# S004在右表中没有,所以姓名和性别为NaN

# 右连接:以右表为基准,左表中没有匹配的用 NaN 填充
df_right = pd.merge(df_score, df_info, on='学号', how='right')
print("\\n右连接结果:")
print(df_right)
# S005在左表中没有,所以数学和英语为NaN

# 外连接:保留两个表中所有的键
df_outer = pd.merge(df_score, df_info, on='学号', how='outer')
print("\\n外连接结果:")
print(df_outer)
# S004和S005都保留,缺失的用NaN填充

# ============================================================
# 案例3:基于多个键合并
# ============================================================

# 订单详情表
df_orders = pd.DataFrame({
'订单编号': ['A001', 'A001', 'A002', 'A003'], # 订单编号
'商品编号': ['P01', 'P02', 'P01', 'P03'], # 商品编号
'数量': [2, 1, 3, 5] # 购买数量
})

# 商品信息表
df_products = pd.DataFrame({
'商品编号': ['P01', 'P02', 'P03', 'P04'], # 商品编号
'商品名称': ['篮球', '足球', '排球', '网球'], # 商品名称
'单价': [120, 89, 75, 45] # 单价
})

# 基于商品编号进行合并
df_merged = pd.merge(df_orders, df_products, on='商品编号')
# 计算总金额
df_merged['总金额'] = df_merged['数量'] * df_merged['单价'] # 每行的总金额
print("多键合并并计算总金额:")
print(df_merged)

# ============================================================
# 案例4:左右键名不同时使用 left_on 和 right_on
# ============================================================

# 左表使用 '员工ID'
df_emp = pd.DataFrame({
'员工ID': [101, 102, 103], # 员工ID列名
'姓名': ['Alice', 'Bob', 'Charlie']
})

# 右表使用 'emp_id'(列名不同)
df_sal = pd.DataFrame({
'emp_id': [101, 102, 104], # 员工ID列名不同
'薪资': [8000, 12000, 9500]
})

# 使用 left_on 和 right_on 指定不同的键列名
df_result = pd.merge(df_emp, df_sal, left_on='员工ID', right_on='emp_id', how='outer')
print("不同键名合并:")
print(df_result)
# 可以用 drop 删除多余的键列
df_result = df_result.drop('emp_id', axis=1) # 删除冗余的 emp_id 列

# ============================================================
# 案例5:基于索引合并
# ============================================================

# 左表以学号为索引
df_left_idx = pd.DataFrame({
'姓名': ['张三', '李四', '王五'],
'年龄': [20, 21, 19]
}, index=['S001', 'S002', 'S003']) # 将学号设为行索引

# 右表以学号为索引
df_right_idx = pd.DataFrame({
'班级': ['计算机1班', '数学2班', '英语1班'],
'绩点': [3.8, 3.5, 3.9]
}, index=['S001', 'S002', 'S004']) # 索引中S003缺失,S004为新增

# 基于索引进行合并
df_index_merge = pd.merge(df_left_idx, df_right_idx,
left_index=True, # 左表使用索引作为键
right_index=True, # 右表使用索引作为键
how='inner') # 内连接
print("基于索引合并:")
print(df_index_merge)

1.2.2 concat() 函数 — 轴向堆叠

# ============================================================
# 案例6:concat 纵向拼接(axis=0)
# ============================================================

# 第一学期成绩
df_sem1 = pd.DataFrame({
'学号': ['S001', 'S002', 'S003'],
'课程': ['数学', '数学', '数学'],
'成绩': [90, 85, 78]
})

# 第二学期成绩
df_sem2 = pd.DataFrame({
'学号': ['S001', 'S002', 'S003'],
'课程': ['英语', '英语', '英语'],
'成绩': [88, 92, 75]
})

# 纵向拼接(上下合并),axis=0 表示沿行方向拼接
df_vertical = pd.concat([df_sem1, df_sem2], axis=0, ignore_index=True)
# ignore_index=True 重置索引,避免索引重复
print("纵向拼接:")
print(df_vertical)

# ============================================================
# 案例7:concat 横向拼接(axis=1)
# ============================================================

# 基本信息
df_basic = pd.DataFrame({
'姓名': ['张三', '李四', '王五'],
'年龄': [20, 21, 19]
})

# 成绩信息
df_grade = pd.DataFrame({
'数学': [90, 85, 78],
'英语': [88, 92, 75]
})

# 横向拼接(左右合并),axis=1 表示沿列方向拼接
df_horizontal = pd.concat([df_basic, df_grade], axis=1)
print("横向拼接:")
print(df_horizontal)

# ============================================================
# 案例8:concat 处理列不一致的情况
# ============================================================

df_a = pd.DataFrame({'A': [1, 2], 'B': [3, 4]}) # 有A、B列
df_b = pd.DataFrame({'B': [5, 6], 'C': [7, 8]}) # 有B、C列

# 默认 join='outer',取列的并集,缺失位置填 NaN
df_concat_outer = pd.concat([df_a, df_b], axis=0, join='outer')
print("外连接拼接(列并集):")
print(df_concat_outer)

# join='inner',取列的交集
df_concat_inner = pd.concat([df_a, df_b], axis=0, join='inner')
print("\\n内连接拼接(列交集):")
print(df_concat_inner)

1.2.3 join() 方法 — 基于索引合并

# ============================================================
# 案例9:join 方法
# ============================================================

# 左表(索引为日期)
df_left = pd.DataFrame({
'开盘价': [10.5, 11.0, 10.8],
'收盘价': [10.8, 11.2, 10.5]
}, index=pd.to_datetime(['2024-01-01', '2024-01-02', '2024-01-03']))

# 右表(索引为日期)
df_right = pd.DataFrame({
'成交量': [100000, 120000, 95000],
'换手率': [1.2, 1.5, 1.1]
}, index=pd.to_datetime(['2024-01-01', '2024-01-02', '2024-01-04']))

# 使用 join 方法,默认为左连接
df_joined = df_left.join(df_right, how='left')
print("join方法合并:")
print(df_joined)
# 2024-01-03 在右表中没有,成交量和换手率为 NaN

1.2.4 append() 方法(已过时,用 concat 替代)

# ============================================================
# 案例10:使用 concat 替代 append
# ============================================================

df_main = pd.DataFrame({'A': [1, 2, 3], 'B': [4, 5, 6]})
df_new = pd.DataFrame({'A': [7, 8], 'B': [9, 10]})

# 旧写法(已弃用):df_main.append(df_new)
# 新写法:使用 concat
df_appended = pd.concat([df_main, df_new], ignore_index=True)
print("concat替代append:")
print(df_appended)


二、数据变换

2.1 数据变换概述

数据变换是将数据从一种形式转换为另一种形式的过程,主要包括:

变换类型说明
轴向旋转 改变数据的行列结构
分组与聚合 按条件分组并计算统计量
哑变量处理 将分类变量转为数值型虚拟变量
面元划分 将连续变量离散化为区间

2.2 轴向旋转

2.2.1 pivot() — 长格式转宽格式

# ============================================================
# 案例11:pivot 透视表(长格式转宽格式)
# ============================================================

# 原始数据(长格式):每行是一条记录
df_long = pd.DataFrame({
'日期': ['2024-01', '2024-01', '2024-02', '2024-02', '2024-03', '2024-03'],
'城市': ['北京', '上海', '北京', '上海', '北京', '上海'],
'销售额': [1000, 1500, 1200, 1800, 1100, 1600] # 销售额
})

print("原始长格式数据:")
print(df_long)

# 使用 pivot 将长格式转为宽格式
# index: 用作新表行索引的列
# columns: 用作新表列名的列
# values: 填充到表格中的值
df_wide = df_long.pivot(index='日期', columns='城市', values='销售额')
print("\\n透视后的宽格式数据:")
print(df_wide)
# 北京 上海
# 2024-01 1000 1500
# 2024-02 1200 1800
# 2024-03 1100 1600

# ============================================================
# 案例12:pivot_table() — 带聚合功能的透视表
# ============================================================

# 当存在重复组合时,pivot 会报错,需要用 pivot_table
df_sales = pd.DataFrame({
'月份': ['1月', '1月', '1月', '2月', '2月', '2月', '1月', '2月'],
'产品': ['A', 'A', 'B', 'A', 'B', 'B', 'B', 'A'],
'销售额': [100, 150, 200, 130, 180, 220, 170, 160] # 注意:1月-A出现两次
})

# pivot_table 可以自动对重复值进行聚合
# aggfunc 默认为 'mean'(均值),也可以用 'sum', 'count', 'max' 等
df_pivot = df_sales.pivot_table(
index='月份', # 行索引
columns='产品', # 列名
values='销售额', # 聚合值
aggfunc='sum', # 聚合函数:求和
fill_value=0, # 缺失值填充为0
margins=True, # 添加行列汇总(边际值)
margins_name='合计' # 汇总行列的名称
)
print("带聚合的透视表:")
print(df_pivot)

2.2.2 stack() 和 unstack() — 堆叠与反堆叠

# ============================================================
# 案例13:stack 与 unstack
# ============================================================

# 创建一个多列DataFrame
df = pd.DataFrame({
'姓名': ['张三', '李四'],
'数学': [90, 85],
'英语': [88, 92],
'物理': [78, 95]
}).set_index('姓名') # 将姓名设为索引

print("原始数据:")
print(df)

# stack():将列"压缩"到行索引,列变成多级索引的第二级
# 返回一个 Series,索引变为 (姓名, 科目) 的多级索引
df_stacked = df.stack()
print("\\nstack后(列转行):")
print(df_stacked)
# 张三 数学 90
# 英语 88
# 物理 78
# 李四 数学 85
# 英语 92
# 物理 95

# unstack():将行索引的某一级"展开"到列方向
# 将第二级索引(科目)展开为列
df_unstacked = df_stacked.unstack()
print("\\nunstack后(行转列):")
print(df_unstacked)
# 恢复为原始的 DataFrame 格式

# ============================================================
# 案例14:melt() — 宽格式转长格式(逆透视)
# ============================================================

# 宽格式数据
df_wide = pd.DataFrame({
'姓名': ['张三', '李四', '王五'],
'数学': [90, 85, 78],
'英语': [88, 92, 75],
'物理': [78, 95, 82]
})

print("宽格式数据:")
print(df_wide)

# melt 将宽格式转换为长格式
df_long = pd.melt(
df_wide,
id_vars=['姓名'], # 保持不变的标识列
value_vars=['数学', '英语', '物理'], # 需要"融化"的列
var_name='科目', # 融化后变量名所在列的列名
value_name='成绩' # 融化后值所在列的列名
)
print("\\n长格式数据(melt后):")
print(df_long)
# 姓名 科目 成绩
# 0 张三 数学 90
# 1 李四 数学 85
# 2 王五 数学 78
# 3 张三 英语 88
# …

2.3 分组与聚合

2.3.1 groupby() 基本用法

# ============================================================
# 案例15:groupby 单列分组
# ============================================================

df = pd.DataFrame({
'部门': ['技术', '销售', '技术', '销售', '技术', '人事'],
'姓名': ['A', 'B', 'C', 'D', 'E', 'F'],
'薪资': [15000, 10000, 18000, 12000, 16000, 13000],
'工龄': [3, 2, 5, 4, 6, 3]
})

# 按部门分组,计算每个部门的平均薪资
# groupby 返回一个 GroupBy 对象,需要配合聚合函数使用
grouped = df.groupby('部门')

# 聚合:计算均值
print("各部门平均薪资和工龄:")
print(grouped.mean(numeric_only=True)) # 只对数值列计算均值

# ============================================================
# 案例16:groupby 多列分组
# ============================================================

df_sales = pd.DataFrame({
'年份': [2023, 2023, 2024, 2024, 2023, 2024],
'季度': ['Q1', 'Q2', 'Q1', 'Q2', 'Q1', 'Q1'],
'区域': ['华东', '华东', '华东', '华东', '华南', '华南'],
'销售额': [100, 150, 120, 180, 200, 210]
})

# 按年份和季度分组,计算各组的销售总额
grouped_multi = df_sales.groupby(['年份', '季度'])
print("按年份和季度分组的销售总额:")
print(grouped_multi['销售额'].sum()) # 选取销售额列并求和

# ============================================================
# 案例17:groupby 配合多种聚合函数 — agg()
# ============================================================

df = pd.DataFrame({
'班级': ['一班', '一班', '一班', '二班', '二班', '二班'],
'姓名': ['A', 'B', 'C', 'D', 'E', 'F'],
'成绩': [90, 85, 78, 92, 88, 75]
})

# 使用 agg 同时应用多个聚合函数
result = df.groupby('班级')['成绩'].agg([
('平均分', 'mean'), # 计算均值,结果列命名为"平均分"
('最高分', 'max'), # 计算最大值
('最低分', 'min'), # 计算最小值
('总分', 'sum'), # 计算总和
('人数', 'count') # 计数
])
print("多聚合函数结果:")
print(result)

# ============================================================
# 案例18:groupby 配合自定义聚合函数
# ============================================================

df = pd.DataFrame({
'商品': ['苹果', '苹果', '香蕉', '香蕉', '苹果', '香蕉'],
'价格': [5.0, 6.5, 3.0, 4.5, 5.5, 3.5]
})

# 自定义函数:计算价格的极差(最大值 – 最小值)
def price_range(x):
return x.max() x.min() # 极差 = 最大值 – 最小值

# 自定义函数:计算变异系数(标准差/均值)
def cv(x):
return x.std() / x.mean() # 变异系数 = 标准差 / 均值

result = df.groupby('商品')['价格'].agg([
('均值', 'mean'),
('极差', price_range), # 应用自定义函数
('变异系数', cv) # 应用自定义函数
])
print("自定义聚合结果:")
print(result)

# ============================================================
# 案例19:transform() — 组内变换(保持原数据形状)
# ============================================================

df = pd.DataFrame({
'班级': ['一班', '一班', '一班', '二班', '二班', '二班'],
'姓名': ['A', 'B', 'C', 'D', 'E', 'F'],
'成绩': [90, 85, 78, 92, 88, 75]
})

# transform 对每个组应用函数,结果与原 DataFrame 形状相同
# 计算每个学生的成绩与班级平均分的差值
df['班级均分'] = df.groupby('班级')['成绩'].transform('mean') # 组内均值广播
df['与均分差'] = df['成绩'] df['班级均分'] # 与均分的差值
df['标准化成绩'] = df.groupby('班级')['成绩'].transform(
lambda x: (x x.mean()) / x.std() # 组内标准化:(x – 均值) / 标准差
)
print("transform结果:")
print(df)

# ============================================================
# 案例20:filter() — 组过滤
# ============================================================

df = pd.DataFrame({
'班级': ['一班', '一班', '一班', '二班', '二班'],
'姓名': ['A', 'B', 'C', 'D', 'E'],
'成绩': [90, 85, 78, 92, 88]
})

# filter:只保留满足条件的组
# 保留平均成绩 >= 85 的班级
df_filtered = df.groupby('班级').filter(lambda x: x['成绩'].mean() >= 85)
print("过滤后(均分>=85的班级):")
print(df_filtered)

2.4 哑变量处理

2.4.1 get_dummies() — 独热编码

# ============================================================
# 案例21:基本哑变量处理
# ============================================================

# 创建包含分类变量的数据
df = pd.DataFrame({
'姓名': ['张三', '李四', '王五', '赵六'],
'颜色偏好': ['红色', '蓝色', '红色', '绿色'], # 分类变量
'评分': [4, 5, 3, 4]
})

print("原始数据:")
print(df)

# 使用 get_dummies 进行独热编码
# 将分类变量'颜色偏好'转换为多个0/1列
df_dummies = pd.get_dummies(df, columns=['颜色偏好'], prefix='颜色')
# columns: 指定需要哑变量处理的列
# prefix: 新生成列的前缀名
print("\\n哑变量处理后:")
print(df_dummies)
# 姓名 评分 颜色_蓝色 颜色_红色 颜色_绿色
# 0 张三 4 0 1 0
# 1 李四 5 1 0 0
# 2 王五 3 0 1 0
# 3 赵六 4 0 0 1

# ============================================================
# 案例22:drop_first 参数 — 避免多重共线性
# ============================================================

df = pd.DataFrame({
'城市': ['北京', '上海', '广州', '北京', '上海'],
'收入': [10000, 12000, 8000, 11000, 13000]
})

# drop_first=True:删除第一个类别,避免多重共线性
# 如果三个城市列全为0,则代表第一个被删除的城市
df_dummies = pd.get_dummies(df, columns=['城市'], prefix='城市', drop_first=True)
print("去除第一个类别后的哑变量:")
print(df_dummies)
# 只保留城市_上海 和 城市_广州,北京被作为基准(两个都为0时即为北京)

# ============================================================
# 案例23:多列哑变量处理
# ============================================================

df = pd.DataFrame({
'性别': ['男', '女', '男', '女'],
'学历': ['本科', '硕士', '博士', '本科'],
'年龄': [25, 30, 35, 28]
})

# 对多个分类列同时进行哑变量处理
df_dummies = pd.get_dummies(df, columns=['性别', '学历'], prefix=['性别', '学历'])
print("多列哑变量处理:")
print(df_dummies)

# ============================================================
# 案例24:手动实现哑变量编码(理解原理)
# ============================================================

df = pd.DataFrame({
'等级': ['A', 'B', 'C', 'A', 'B']
})

# 获取唯一类别
categories = df['等级'].unique() # ['A', 'B', 'C']

# 手动创建哑变量列
for cat in categories:
df[f'等级_{cat}'] = (df['等级'] == cat).astype(int)
# 将等级列与当前类别比较,相等为True(1),不等为False(0)

print("手动哑变量编码:")
print(df)

2.5 面元划分

2.5.1 cut() — 等宽分箱

# ============================================================
# 案例25:cut 基本用法
# ============================================================

# 30名学生的考试成绩
np.random.seed(42) # 设置随机种子保证结果可复现
scores = np.random.randint(50, 100, 30) # 生成30个50-100的随机整数

print("原始成绩:", scores)

# 使用 cut 将成绩划分为不同的等级区间
# 指定分箱的边界点
bins = [0, 60, 70, 80, 90, 100] # 分箱边界:0-60, 60-70, 70-80, 80-90, 90-100
labels = ['不及格', '及格', '中等', '良好', '优秀'] # 每个区间的标签

# cut 函数:将连续数据分割到离散的区间中
score_levels = pd.cut(
scores,
bins=bins, # 分箱的边界列表
labels=labels, # 每个箱的标签
include_lowest=True # 包含最左边的边界(即包含0)
)

# 将结果放入 DataFrame
df_scores = pd.DataFrame({
'成绩': scores,
'等级': score_levels
})
print("\\n面元划分结果:")
print(df_scores)

# 统计每个等级的人数
print("\\n各等级人数统计:")
print(df_scores['等级'].value_counts().sort_index())

# ============================================================
# 案例26:cut 等宽分箱(指定箱数)
# ============================================================

ages = [18, 22, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70]

# 指定分为4个等宽的箱,不指定具体边界
age_groups = pd.cut(ages, bins=4, precision=0)
# bins=4: 分为4个等宽的区间
# precision=0: 区间边界保留0位小数

df_ages = pd.DataFrame({
'年龄': ages,
'年龄段': age_groups
})
print("等宽分箱结果:")
print(df_ages)

2.5.2 qcut() — 等频分箱(分位数分箱)

# ============================================================
# 案例27:qcut 等频分箱
# ============================================================

np.random.seed(42)
data = np.random.exponential(scale=50, size=100) # 生成指数分布数据

# qcut:按分位数分箱,每个箱中的数据个数大致相同
quantile_groups = pd.qcut(
data,
q=4, # 分为4组(四分位数)
labels=['Q1(最低)', 'Q2', 'Q3', 'Q4(最高)'] # 组标签
)

df_quantile = pd.DataFrame({
'原始值': data,
'分位数组': quantile_groups
})

print("等频分箱(分位数)结果:")
print(df_quantile['分位数组'].value_counts().sort_index())
# 每组大约25个数据

# 对比:等宽分箱和等频分箱的区别
print("\\n等宽分箱(cut)分布:")
print(pd.cut(data, bins=4).value_counts().sort_index())
print("\\n等频分箱(qcut)分布:")
print(pd.qcut(data, q=4).value_counts().sort_index())


三、数据规约

3.1 数据规约概述

数据规约是在保持数据完整性的前提下,精简数据规模的过程。主要方法:

方法说明
重塑分层索引 通过多级索引重新组织数据结构
降采样 降低时间序列数据的频率

3.2 重塑分层索引

# ============================================================
# 案例28:创建多层索引(MultiIndex)
# ============================================================

# 方法1:使用 pd.MultiIndex.from_tuples 从元组创建
index = pd.MultiIndex.from_tuples([
('一班', '张三'), # 第一级"一班",第二级"张三"
('一班', '李四'),
('二班', '王五'),
('二班', '赵六')
], names=['班级', '姓名']) # 每级索引的名称

df = pd.DataFrame({
'数学': [90, 85, 78, 92],
'英语': [88, 92, 75, 85]
}, index=index)

print("多层索引 DataFrame:")
print(df)

# ============================================================
# 案例29:多层索引的切片与选取
# ============================================================

# 选取第一级索引为"一班"的所有数据
print("一班的所有数据:")
print(df.loc['一班']) # 使用第一级索引标签选取

# 选取具体某个人的数据
print("\\n二班王五的数据:")
print(df.loc['二班', '王五']) # 两级索引同时指定

# 使用 slice 进行切片
print("\\n切片选取:")
print(df.loc[('一班', '张三'):('二班', '王五')]) # 切片选取

# ============================================================
# 案例30:swaplevel() — 交换索引层级
# ============================================================

# 交换第一级和第二级索引
df_swapped = df.swaplevel('班级', '姓名')
print("交换索引层级后:")
print(df_swapped)
# 现在姓名在第一级,班级在第二级

# ============================================================
# 案例31:sort_index() — 多层索引排序
# ============================================================

# 对多层索引进行排序
df_sorted = df.sort_index(level=0, ascending=True)
# level=0: 按第一级索引排序
# ascending=True: 升序
print("排序后的多层索引:")
print(df_sorted)

# ============================================================
# 案例32:set_index / reset_index — 设置与重置索引
# ============================================================

df = pd.DataFrame({
'年份': [2023, 2023, 2024, 2024],
'季度': ['Q1', 'Q2', 'Q1', 'Q2'],
'收入': [100, 150, 120, 180],
'支出': [80, 100, 90, 120]
})

# 设置多层索引
df_indexed = df.set_index(['年份', '季度'])
print("设置多层索引:")
print(df_indexed)

# 重置索引,将索引变回普通列
df_reset = df_indexed.reset_index()
print("\\n重置索引:")
print(df_reset)

# ============================================================
# 案例33:stack/unstack 与分层索引配合
# ============================================================

df = pd.DataFrame({
'班级': ['一班', '一班', '二班', '二班'],
'学期': ['上学期', '下学期', '上学期', '下学期'],
'平均分': [85, 88, 78, 82],
'及格率': [0.95, 0.98, 0.88, 0.92]
})

# 设置多层索引
df = df.set_index(['班级', '学期'])
print("原始多层索引:")
print(df)

# unstack 将内层索引展开为列
df_unstacked = df.unstack('学期')
print("\\n将学期展开为列:")
print(df_unstacked)
# 这样每个班级只有一行,上学期和下学期的数据变为列

# 再 stack 回来
df_stacked = df_unstacked.stack()
print("\\nstack回来:")
print(df_stacked)

3.3 降采样

# ============================================================
# 案例34:创建时间序列数据
# ============================================================

# 生成每分钟一条的高频数据(模拟一天的数据)
np.random.seed(42)
dates = pd.date_range('2024-01-01 09:30:00', periods=240, freq='1min')
# 生成240分钟(4小时)的数据,从9:30到13:30

df = pd.DataFrame({
'价格': 100 + np.cumsum(np.random.randn(240) * 0.1), # 模拟价格(随机游走)
'成交量': np.random.randint(100, 1000, 240) # 模拟成交量
}, index=dates)

print("原始高频数据(前10行):")
print(df.head(10))
print(f"\\n数据形状: {df.shape}") # (240, 2)

# ============================================================
# 案例35:resample 降采样 — 分钟级到小时级
# ============================================================

# 将1分钟数据降采样为1小时数据
# resample 类似于 groupby,但是基于时间的分组
df_hourly = df.resample('1h').agg({
'价格': 'last', # 每小时取最后一条价格(收盘价)
'成交量': 'sum' # 每小时成交量求和
})
print("降采样为1小时:")
print(df_hourly)

# ============================================================
# 案例36:resample 降采样 — 分钟级到5分钟级
# ============================================================

# 5分钟K线数据
df_5min = df.resample('5min').agg({
'价格': ['first', 'max', 'min', 'last'], # 开盘价、最高、最低、收盘
'成交量': 'sum' # 5分钟总成交量
})
# 重命名列
df_5min.columns = ['开盘价', '最高价', '最低价', '收盘价', '成交量']
print("5分钟K线数据(前10行):")
print(df_5min.head(10))

# ============================================================
# 案例37:resample 降采样 — 天级到月级
# ============================================================

# 生成365天的日数据
np.random.seed(42)
daily_dates = pd.date_range('2024-01-01', periods=365, freq='D')

df_daily = pd.DataFrame({
'销售额': np.random.randint(1000, 5000, 365), # 日销售额
'访客数': np.random.randint(100, 500, 365) # 日访客数
}, index=daily_dates)

print("日数据(前5行):")
print(df_daily.head())

# 降采样为月度数据
df_monthly = df_daily.resample('ME').agg({
'销售额': 'sum', # 月销售总额
'访客数': 'mean' # 月平均日访客数
})
print("\\n月度数据:")
print(df_monthly)

# ============================================================
# 案例38:resample 升采样 + 填充
# ============================================================

# 周度数据
df_weekly = pd.DataFrame({
'库存': [100, 85, 90, 75]
}, index=pd.date_range('2024-01-01', periods=4, freq='W'))

print("周度数据:")
print(df_weekly)

# 升采样为日数据(会有缺失值)
df_daily = df_weekly.resample('D').asfreq() # 升采样,缺失值为NaN
print("\\n升采样为日数据(有缺失):")
print(df_daily.head(10))

# 使用前向填充(ffill)填充缺失值
df_daily_filled = df_weekly.resample('D').ffill()
print("\\n前向填充后:")
print(df_daily_filled.head(10))

# 使用后向填充(bfill)
df_daily_bfill = df_weekly.resample('D').bfill()
print("\\n后向填充后:")
print(df_daily_bfill.head(10))

# ============================================================
# 案例39:OHLC 降采样
# ============================================================

# 生成模拟的股价数据
np.random.seed(42)
price_dates = pd.date_range('2024-01-01 09:30', periods=480, freq='1min')
prices = 100 + np.cumsum(np.random.randn(480) * 0.05)

df_stock = pd.DataFrame({'价格': prices}, index=price_dates)

# 使用 ohlc 方法直接生成 OHLC(开盘、最高、最低、收盘)数据
df_ohlc = df_stock['价格'].resample('30min').ohlc()
print("30分钟OHLC数据:")
print(df_ohlc.head())


四、综合案例 —— 中国篮球运动员的基本信息分析

4.1 分析目标

对中国篮球运动员的基本信息(身高、体重、位置、年龄等)进行数据集成、变换和规约,完成以下任务:

  • 合并多张数据表
  • 进行数据变换(面元划分、哑变量、透视等)
  • 进行分组聚合分析
  • 数据规约与展示
  • 4.2 数据获取与准备

    # ============================================================
    # 案例40:模拟中国篮球运动员数据集
    # ============================================================

    import pandas as pd
    import numpy as np

    np.random.seed(42)

    # —- 表1:运动员基本信息表 —-
    n = 50 # 50名运动员
    df_basic = pd.DataFrame({
    '球员编号': [f'P{str(i).zfill(3)}' for i in range(1, n+1)],
    # 生成编号:P001, P002, …, P050,zfill(3) 表示补零到3位
    '姓名': [f'球员{i}' for i in range(1, n+1)],
    # 生成球员姓名
    '年龄': np.random.randint(18, 35, n),
    # 随机生成18-34岁的年龄
    '身高(cm)': np.random.randint(180, 220, n),
    # 随机生成180-219cm的身高
    '体重(kg)': np.random.randint(70, 120, n),
    # 随机生成70-119kg的体重
    '位置': np.random.choice(['中锋', '大前锋', '小前锋', '得分后卫', '控球后卫'], n),
    # 随机分配5个位置
    '球队': np.random.choice(['广东', '辽宁', '浙江', '北京', '新疆', '山东'], n),
    # 随机分配6支球队
    '球龄(年)': np.random.randint(1, 15, n)
    # 随机生成1-14年的球龄
    })

    # —- 表2:赛季技术统计表 —-
    df_stats = pd.DataFrame({
    '球员编号': df_basic['球员编号'].tolist(),
    # 与基本信息表相同的球员编号
    '场均得分': np.round(np.random.uniform(2, 30, n), 1),
    # 随机生成2-30的场均得分,保留1位小数
    '场均篮板': np.round(np.random.uniform(1, 15, n), 1),
    # 随机生成场均篮板
    '场均助攻': np.round(np.random.uniform(0.5, 10, n), 1),
    # 随机生成场均助攻
    '场均抢断': np.round(np.random.uniform(0.2, 3, n), 1),
    # 随机生成场均抢断
    '投篮命中率(%)': np.round(np.random.uniform(30, 65, n), 1),
    # 随机生成投篮命中率
    '赛季': ['2023-2024'] * n
    # 所有数据都是2023-2024赛季
    })

    print("=== 基本信息表 ===")
    print(df_basic.head(10))
    print(f"\\n数据形状: {df_basic.shape}")

    print("\\n=== 技术统计表 ===")
    print(df_stats.head(10))
    print(f"\\n数据形状: {df_stats.shape}")

    4.3 数据清理

    # ============================================================
    # 案例41:数据清理
    # ============================================================

    # 检查缺失值
    print("=== 基本信息表缺失值检查 ===")
    print(df_basic.isnull().sum()) # 统计每列缺失值数量

    print("\\n=== 技术统计表缺失值检查 ===")
    print(df_stats.isnull().sum())

    # 检查重复值
    print(f"\\n基本信息表重复行数: {df_basic.duplicated().sum()}")
    # duplicated() 返回布尔Series,sum()统计True的数量

    # 检查数据类型
    print("\\n=== 数据类型 ===")
    print(df_basic.dtypes)

    # 描述性统计
    print("\\n=== 数值列的描述性统计 ===")
    print(df_basic.describe())
    # describe() 自动计算 count, mean, std, min, 25%, 50%, 75%, max

    4.4 实现步骤

    步骤一:数据集成

    # ============================================================
    # 案例42:合并运动员信息和技术统计
    # ============================================================

    # 使用 merge 基于球员编号合并两张表
    df_merged = pd.merge(
    df_basic, # 左表:基本信息
    df_stats, # 右表:技术统计
    on='球员编号', # 合并键
    how='inner' # 内连接
    )

    print("=== 合并后的完整数据 ===")
    print(df_merged.head())
    print(f"\\n合并后数据形状: {df_merged.shape}")
    print(f"\\n合并后列名:{df_merged.columns.tolist()}")

    步骤二:数据变换 — 面元划分

    # ============================================================
    # 案例43:对身高进行面元划分
    # ============================================================

    # 将身高划分为不同的等级
    height_bins = [179, 190, 200, 210, 220] # 分箱边界
    height_labels = ['矮个(180-190)', '中等(190-200)', '高个(200-210)', '超高(210+)']
    # 每个区间的标签

    df_merged['身高等级'] = pd.cut(
    df_merged['身高(cm)'],
    bins=height_bins,
    labels=height_labels,
    include_lowest=True # 包含最左边界(179)
    )

    print("=== 身高面元划分结果 ===")
    print(df_merged[['姓名', '身高(cm)', '身高等级']].head(10))

    # 统计各身高等级的人数
    print("\\n各身高等级人数:")
    print(df_merged['身高等级'].value_counts().sort_index())

    # ============================================================
    # 案例44:对年龄进行等频分箱
    # ============================================================

    # 使用 qcut 将年龄按分位数分为3组
    df_merged['年龄段'] = pd.qcut(
    df_merged['年龄'],
    q=3, # 分为3组
    labels=['年轻', '中生代', '老将']
    )

    print("=== 年龄等频分箱 ===")
    print(df_merged[['姓名', '年龄', '年龄段']].head(10))

    print("\\n各年龄段人数分布:")
    print(df_merged['年龄段'].value_counts().sort_index())

    # ============================================================
    # 案例45:计算BMI并进行面元划分
    # ============================================================

    # 计算BMI = 体重(kg) / (身高(m))^2
    df_merged['BMI'] = df_merged['体重(kg)'] / (df_merged['身高(cm)'] / 100) ** 2
    # 身高除以100将厘米转为米,再平方

    # 对BMI进行面元划分
    bmi_bins = [0, 20, 23, 25, 30, 100] # BMI分箱边界
    bmi_labels = ['偏瘦', '正常', '偏胖', '肥胖', '重度肥胖']

    df_merged['BMI等级'] = pd.cut(df_merged['BMI'], bins=bmi_bins, labels=bmi_labels)

    print("=== BMI分析 ===")
    print(df_merged[['姓名', '身高(cm)', '体重(kg)', 'BMI', 'BMI等级']].head(10))
    print("\\nBMI等级分布:")
    print(df_merged['BMI等级'].value_counts().sort_index())

    步骤三:数据变换 — 哑变量处理

    # ============================================================
    # 案例46:对位置进行哑变量编码
    # ============================================================

    # 对"位置"列进行独热编码
    df_with_dummies = pd.get_dummies(
    df_merged,
    columns=['位置'], # 对"位置"列进行编码
    prefix='位置', # 新列前缀
    dtype=int # 数据类型设为整数(0和1)
    )

    print("=== 哑变量处理结果 ===")
    # 查看新增的哑变量列
    dummy_cols = [col for col in df_with_dummies.columns if col.startswith('位置_')]
    # 列表推导式:筛选所有以"位置_"开头的列
    print(df_with_dummies[['姓名', '位置'] + dummy_cols].head(10) if '位置' in df_with_dummies.columns
    else df_with_dummies[['姓名'] + dummy_cols].head(10))

    # ============================================================
    # 案例47:对球队进行哑变量编码(drop_first 避免共线性)
    # ============================================================

    df_with_team_dummies = pd.get_dummies(
    df_merged,
    columns=['球队'], # 对球队列编码
    prefix='球队',
    drop_first=True, # 删除第一个类别,避免多重共线性
    dtype=int
    )

    # 查看新增的球队哑变量列
    team_dummy_cols = [col for col in df_with_team_dummies.columns if col.startswith('球队_')]
    print("=== 球队哑变量(去除基准类别)===")
    print(df_with_team_dummies[['姓名'] + team_dummy_cols].head(8))

    步骤四:数据变换 — 轴向旋转(透视)

    # ============================================================
    # 案例48:按位置和球队透视分析平均得分
    # ============================================================

    # 创建透视表:行为球队,列为位置,值为平均得分
    pivot_score = df_merged.pivot_table(
    index='球队', # 行索引:球队
    columns='位置', # 列名:位置
    values='场均得分', # 值:场均得分
    aggfunc='mean', # 聚合函数:均值
    fill_value=0 # 缺失值填充为0
    )

    # 保留一位小数
    pivot_score = pivot_score.round(1)

    print("=== 各球队各位置平均得分透视表 ===")
    print(pivot_score)

    # ============================================================
    # 案例49:带边际汇总的透视表
    # ============================================================

    pivot_full = df_merged.pivot_table(
    index='球队',
    columns='位置',
    values='场均得分',
    aggfunc='mean',
    fill_value=0,
    margins=True, # 添加边际汇总行和列
    margins_name='均值' # 汇总行列的名称
    ).round(1)

    print("=== 带汇总的透视表 ===")
    print(pivot_full)

    # ============================================================
    # 案例50:melt 逆透视 — 将宽表转为长表
    # ============================================================

    # 假设我们有一个各队平均数据的宽表
    team_avg = df_merged.groupby('球队')[['场均得分', '场均篮板', '场均助攻']].mean().round(1)
    print("球队平均数据(宽格式):")
    print(team_avg)

    # 使用 melt 将宽表转为长表
    team_avg_reset = team_avg.reset_index() # 先将索引变为列
    df_long = pd.melt(
    team_avg_reset,
    id_vars=['球队'], # 保持不变的标识列
    value_vars=['场均得分', '场均篮板', '场均助攻'], # 要融化的列
    var_name='统计项', # 新的变量名列
    value_name='平均值' # 新的值列名
    )

    print("\\n球队平均数据(长格式):")
    print(df_long.sort_values(['球队', '统计项']))

    步骤五:分组与聚合分析

    # ============================================================
    # 案例51:按位置分组统计
    # ============================================================

    # 按位置分组,计算多项统计指标
    position_stats = df_merged.groupby('位置').agg({
    '身高(cm)': ['mean', 'min', 'max'], # 身高的均值、最小值、最大值
    '体重(kg)': ['mean', 'std'], # 体重的均值和标准差
    '场均得分': 'mean', # 场均得分均值
    '场均篮板': 'mean', # 场均篮板均值
    '场均助攻': 'mean', # 场均助攻均值
    '球员编号': 'count' # 人数计数
    }).round(1)

    # 扁平化多级列名
    position_stats.columns = ['_'.join(col).strip('_') for col in position_stats.columns]
    # join 将元组中的列名用下划线连接

    print("=== 按位置分组统计 ===")
    print(position_stats)

    # ============================================================
    # 案例52:按球队分组的综合分析
    # ============================================================

    # 按球队分组
    team_group = df_merged.groupby('球队')

    # 使用 agg 进行多维度聚合
    team_analysis = team_group.agg({
    '球员编号': 'count', # 球队人数
    '年龄': 'mean', # 平均年龄
    '身高(cm)': 'mean', # 平均身高
    '场均得分': 'sum', # 球队总得分(所有球员之和)
    '场均篮板': 'mean', # 平均篮板
    '投篮命中率(%)': 'mean' # 平均命中率
    }).round(1)

    # 重命名列
    team_analysis.columns = ['人数', '平均年龄', '平均身高(cm)',
    '总得分', '平均篮板', '平均命中率(%)']

    # 计算场均得分效率 = 总得分 / 人数
    team_analysis['人均得分'] = (team_analysis['总得分'] / team_analysis['人数']).round(1)

    print("=== 球队综合分析 ===")
    print(team_analysis.sort_values('总得分', ascending=False))
    # 按总得分降序排列

    # ============================================================
    # 案例53:使用 transform 计算组内排名
    # ============================================================

    # 按球队分组,计算每个球员的得分在球队内的排名
    df_merged['队内得分排名'] = df_merged.groupby('球队')['场均得分'].rank(
    ascending=False, # 降序排名(得分最高排名1)
    method='dense' # 相同值取相同名次,下一个名次连续(1,1,3而非1,1,4)
    ).astype(int)

    # 按球队分组,计算得分与球队均值的差
    df_merged['与队均得分差'] = df_merged.groupby('球队')['场均得分'].transform(
    lambda x: x x.mean() # 每个球员得分减去其所在球队的平均得分
    ).round(2)

    print("=== 队内得分排名 ===")
    print(df_merged[['姓名', '球队', '场均得分', '队内得分排名', '与队均得分差']]
    .sort_values(['球队', '队内得分排名'])
    .head(15))

    # ============================================================
    # 案例54:filter 组过滤 — 筛选高水平球队
    # ============================================================

    # 只保留平均得分 > 球队平均得分中位数的球队
    # 先计算每个球队的平均得分
    team_mean_score = df_merged.groupby('球队')['场均得分'].mean()
    print("各球队平均得分:")
    print(team_mean_score.round(1))

    # 中位数
    median_score = team_mean_score.median()
    print(f"\\n球队平均得分中位数: {median_score:.1f}")

    # 使用 filter 保留平均得分高于中位数的球队
    high_level_teams = df_merged.groupby('球队').filter(
    lambda x: x['场均得分'].mean() > median_score
    )
    # lambda x 中的 x 是每个组的 DataFrame

    print(f"\\n高水平球队的球员数量: {len(high_level_teams)}")
    print("高水平球队:")
    print(high_level_teams['球队'].unique())

    步骤六:数据规约

    # ============================================================
    # 案例55:多层索引规约
    # ============================================================

    # 设置多层索引:球队 -> 位置
    df_multi = df_merged.set_index(['球队', '位置'])

    # 选取关键列
    df_multi = df_multi[['姓名', '场均得分', '场均篮板', '场均助攻']]

    print("=== 多层索引数据 ===")
    print(df_multi.head(15))

    # 通过多层索引快速查询某队某位置的所有球员
    print("\\n=== 广东队中锋 ===")
    try:
    print(df_multi.loc[('广东', '中锋')])
    except KeyError:
    print("没有找到广东队中锋的数据")

    # ============================================================
    # 案例56:unstack 规约展示
    # ============================================================

    # 计算每个球队每个位置的平均得分
    avg_score = df_merged.groupby(['球队', '位置'])['场均得分'].mean().round(1)

    # unstack:将位置从行索引展开为列,形成更紧凑的表格
    avg_score_wide = avg_score.unstack('位置')
    print("=== 各队各位置平均得分(规约展示)===")
    print(avg_score_wide)

    # 填充 NaN 为 '-'
    avg_score_display = avg_score_wide.fillna('-')
    print("\\n=== 格式化展示 ===")
    print(avg_score_display)

    # ============================================================
    # 案例57:降采样示例 — 模拟赛季月度数据
    # ============================================================

    # 模拟一个赛季的逐场比赛数据
    np.random.seed(42)
    n_games = 82 # NBA一个赛季82场比赛
    game_dates = pd.date_range('2023-10-01', periods=n_games, freq='4D')
    # 每4天一场比赛

    df_games = pd.DataFrame({
    '日期': game_dates,
    '得分': np.random.randint(80, 130, n_games), # 每场比赛得分
    '篮板': np.random.randint(30, 60, n_games), # 每场比赛篮板
    '助攻': np.random.randint(15, 35, n_games) # 每场比赛助攻
    })
    df_games.set_index('日期', inplace=True) # 将日期设为索引

    print("=== 比赛日数据(前10场)===")
    print(df_games.head(10))

    # 降采样为月度数据
    df_monthly_games = df_games.resample('ME').agg({
    '得分': ['mean', 'sum'], # 月均得分和总得分
    '篮板': 'mean', # 月均篮板
    '助攻': 'mean' # 月均助攻
    }).round(1)

    # 扁平化列名
    df_monthly_games.columns = ['_'.join(col).strip('_') for col in df_monthly_games.columns]

    print("\\n=== 月度统计数据 ===")
    print(df_monthly_games)

    # ============================================================
    # 案例58:最终汇总报表
    # ============================================================

    # 综合分析报表
    print("=" * 60)
    print(" 中国篮球运动员综合分析报表")
    print("=" * 60)

    # 1. 总体概况
    print(f"\\n【总体概况】")
    print(f" 运动员总数: {len(df_merged)}")
    print(f" 球队数量: {df_merged['球队'].nunique()}") # nunique() 唯一值计数
    print(f" 位置数量: {df_merged['位置'].nunique()}")
    print(f" 平均年龄: {df_merged['年龄'].mean():.1f} 岁")
    print(f" 平均身高: {df_merged['身高(cm)'].mean():.1f} cm")
    print(f" 平均体重: {df_merged['体重(kg)'].mean():.1f} kg")

    # 2. 各位置实力分析
    print(f"\\n【各位置实力排名】")
    pos_rank = df_merged.groupby('位置')['场均得分'].mean().sort_values(ascending=False)
    for pos, score in pos_rank.items():
    # items() 返回 (索引, 值) 的迭代器
    print(f" {pos}: 场均 {score:.1f} 分")

    # 3. 各球队综合实力
    print(f"\\n【球队综合实力TOP3】")
    df_merged['效率值'] = (
    df_merged['场均得分'] * 1.0 +
    df_merged['场均篮板'] * 0.7 +
    df_merged['场均助攻'] * 0.5
    ).round(1)
    # 简单的效率值计算公式

    team_eff = df_merged.groupby('球队')['效率值'].mean().sort_values(ascending=False)
    for i, (team, eff) in enumerate(team_eff.head(3).items(), 1):
    # enumerate 从1开始计数
    print(f" 第{i}名: {team}队 (平均效率值: {eff:.1f})")

    # 4. MVP候选人(效率值最高的前5名球员)
    print(f"\\n【MVP候选人TOP5】")
    mvp_candidates = df_merged.nlargest(5, '效率值')[['姓名', '球队', '位置', '场均得分', '场均篮板', '场均助攻', '效率值']]
    # nlargest(n, col) 返回指定列最大的前n行
    print(mvp_candidates.to_string(index=False))
    # to_string(index=False) 不显示索引

    print("\\n" + "=" * 60)


    五、关键函数速查表

    函数用途关键参数
    pd.merge() 基于键合并 on, how, left_on, right_on, left_index, right_index
    pd.concat() 轴向堆叠 axis, join, ignore_index, keys
    .join() 基于索引合并 how, on, lsuffix, rsuffix
    .pivot() 长转宽 index, columns, values
    .pivot_table() 透视表(带聚合) index, columns, values, aggfunc, margins
    .melt() 宽转长 id_vars, value_vars, var_name, value_name
    .stack() 列转行(堆叠) level
    .unstack() 行转列(展开) level
    .groupby() 分组 by, as_index
    .agg() 聚合 多种函数/字典映射
    .transform() 组内变换 函数/lambda
    .filter() 组过滤 函数/lambda
    pd.get_dummies() 哑变量编码 columns, prefix, drop_first, dtype
    pd.cut() 等宽分箱 bins, labels, include_lowest
    pd.qcut() 等频分箱 q, labels
    .resample() 降/升采样 rule, on
    .set_index() 设置索引 keys, drop
    .reset_index() 重置索引 level, drop
    .swaplevel() 交换索引层级 i, j
    .sort_index() 索引排序 level, ascending
    赞(0)
    未经允许不得转载:171主机测评 » 数据预处理入门学习教程,从入门到精通,数据集成、变换与规约 — 完整知识点与案例代码(6)
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址