查看全课大纲(第 101 / 101 节)
  1. 1.数据分析基本概念
  2. 2.学习数据分析的一般路线
  3. 3.数据分析的流程
  4. 4.数据类型
  5. 5.环境部署(1)
  6. 6.环境部署(2)
  7. 7.课程介绍
  8. 8.TXT文件操作
  9. 9.JSON文件操作
  10. 10.CSV文件操作
  11. 11.Excel文件操作
  12. 12.数据库及SQL常用语法
  13. 13.数据库基本操作
  14. 14.数据库多表连接
  15. 15.实战:欧洲职业足球数据库分析
  16. 16.爬虫简介
  17. 17.URL管理模块
  18. 18.网页下载模块
  19. 19.网页解析模块(1)
  20. 20.网页解析模块(2)
  21. 21.Scrapy简介
  22. 22.Scrapy使用步骤(1)
  23. 23.Scrapy使用步骤(2)
  24. 24.Scrapy使用步骤(3)
  25. 25.Scrapy使用步骤(4)
  26. 26.实战:获取国内城市空气质量指数数据
  27. 27.NumPy和SciPy介绍
  28. 28.多维数组
  29. 29.多维数组操作
  30. 30.NumPy的常用方法
  31. 31.向量化介绍
  32. 32.向量化及通用函数
  33. 33.实战:2016美国大选分析
  34. 34.数据结构-Series
  35. 35.数据结构-DataFrame
  36. 36.数据结构-Index
  37. 37.Series的索引操作
  38. 38.DataFrame的索引操作
  39. 39.索引操作总结
  40. 40.运算与对齐
  41. 41.函数应用操作(1) -- map
  42. 42.函数应用操作 (2) -- apply applymap
  43. 43.文件读写操作
  44. 44.排序操作
  45. 45.数据清洗--处理缺失数据
  46. 46.数据清洗--处理重复数据
  47. 47.数据清洗--替换数据
  48. 48.常用统计方法(1) -- describe quantile
  49. 49.常用统计方法(2) -- sum mean median count
  50. 50.常用统计方法(3) -- max min idxmax idxmin
  51. 51.常用统计方法(4) -- mad var std cumsum
  52. 52.实战:全球食品数据分析
  53. 53.层级索引
  54. 54.分组与聚合介绍
  55. 55.分组操作(1) -- GroupBy对象及常用聚合操作
  56. 56.分组操作(2) -- 自定义分组及聚合操作
  57. 57.透视表介绍
  58. 58.透视表操作
  59. 59.数据规整(1) -- 数据合并concat
  60. 60.数据规整(2) -- 数据连接merge
  61. 61.数据重构(3) -- 数据重构stack unstack
  62. 62.实战:互联网电影资料库分析
  63. 63.探索性数据分析EDA介绍
  64. 64.EDA的目的
  65. 65.EDA常用工具
  66. 66.Matplotlib绘图基本介绍
  67. 67.Matplotlib画布
  68. 68.散点图和柱状图的绘制
  69. 69.直方图的绘制
  70. 70.矩阵绘图
  71. 71.子图的使用
  72. 72.Matplotlib颜色、标记、线型
  73. 73.Matplotlib坐标刻度、标签、图例、标题
  74. 74.Seaborn介绍
  75. 75.数据集分布可视化(1) -- 单变量分布、双变量分布
  76. 76.数据集分布可视化(2) -- 变量关系可视化
  77. 77.类别数据可视化 -- 类别散布图、类别内数据分布、类别内统计图
  78. 78.交互式数据可视化工具Bokeh介绍
  79. 79.Bokeh绘制散点图、柱状图、盒子图、弦图
  80. 80.Bokeh绘制常用图形元素
  81. 81.3D绘图 -- mplot3d
  82. 82.3D曲线可视化
  83. 83.3D散点图可视化
  84. 84.3D柱状图可视化
  85. 85.Pandas绘图
  86. 86.实战:Lending Club借贷数据探索性分析及可视化
  87. 87.机器学习介绍及应用场景
  88. 88.机器学习建模介绍 (1) -- 分类
  89. 89.机器学习建模介绍 (2) -- 回归
  90. 90.机器学习建模介绍 (3) -- 聚类
  91. 91.机器学习分类
  92. 92.机器学习工具scikit-learn
  93. 93.使用scikit-learn的流程
  94. 94.数据集准备及划分
  95. 95.模型选择
  96. 96.数据预处理及特征工程
  97. 97.过拟合与欠拟合
  98. 98.模型调参介绍
  99. 99.模型调参方法
  100. 100.模型测试及评价
  101. 101.实战:通过移动设备行为数据预测性别和年龄

实战:通过移动设备行为数据预测性别和年龄

约 26 分钟

📺 正在播放小象学院官方高清录播(支持倍速与清晰度调节)

小象实战讲义 · Python数据分析实战

本节我们将通过一个来自Kaggle的真实竞赛案例,串联起数据分析的完整链路。我们将学习如何从多个关联的原始数据表中提取特征、进行数据清洗与探索性分析,并最终构建机器学习模型来预测用户的性别与年龄分组。这是一个典型的分类任务,能让你将前面学到的Pandas数据处理、特征工程、可视化以及scikit-learn建模等知识融会贯通。

💡 核心导读

  • 项目背景:使用移动设备行为数据(APP使用、事件记录)预测用户的性别与年龄分组。
  • 数据特点:数据由多个CSV文件关联而成,需要进行数据融合与特征提取。
  • 核心步骤:数据解读与融合 → 特征提取与EDA → 特征归一化 → 多模型构建与调参 → 结果比较。
  • 技术要点:Pandas的merge与groupby操作、LabelEncoder编码、MinMaxScaler归一化、GridSearchCV调参以及K近邻、线性SVM、逻辑回归等分类模型的应用。

数据解读与项目任务

本次实战案例的数据来源于Kaggle竞赛“TalkingData Mobile User Demographics”。原始数据集规模较大(约20万用户),包含6个相互关联的CSV文件。我们的目标是根据用户使用移动设备的行为数据(如APP安装、事件类型),预测其所属的“性别+年龄”分组。

分组共有四类:

  • M29+: 男性,29岁及以上
  • F29+: 女性,29岁及以上
  • F28-: 女性,28岁及以下
  • M28-: 男性,28岁及以下

这是一个四分类问题。在真实项目中,数据往往分散在多个表中,我们的首要任务就是理解数据关系,并将它们整合起来。

# 数据关系解读与模拟数据构造
import pandas as pd

# 模拟 gender_age 表:设备ID与分组标签
gender_age_data = {
    'device_id': ['dev_001', 'dev_002', 'dev_003', 'dev_004'],
    'group': ['M29+', 'F29+', 'F28-', 'M28-']
}
gender_age_df = pd.DataFrame(gender_age_data)

# 模拟 events 表:事件ID与发生该事件的设备ID
events_data = {
    'event_id': [1001, 1002, 1003, 1004, 1005, 1006],
    'device_id': ['dev_001', 'dev_001', 'dev_002', 'dev_003', 'dev_003', 'dev_004']
}
events_df = pd.DataFrame(events_data)

# 模拟 app_events 表:事件ID与在该事件中涉及的APP ID
app_events_data = {
    'event_id': [1001, 1001, 1002, 1003, 1004, 1005, 1006],
    'app_id': ['app_a', 'app_b', 'app_c', 'app_a', 'app_d', 'app_e', 'app_f']
}
app_events_df = pd.DataFrame(app_events_data)

print("1. 设备分组表 (gender_age):")
print(gender_age_df)
print("\n2. 事件表 (events):")
print(events_df)
print("\n3. 应用事件表 (app_events):")
print(app_events_df)
1. 设备分组表 (gender_age):
  device_id group
0   dev_001  M29+
1   dev_002  F29+
2   dev_003  F28-
3   dev_004  M28-

2. 事件表 (events):
   event_id device_id
0      1001   dev_001
1      1002   dev_001
2      1003   dev_002
3      1004   dev_003
4      1005   dev_003
5      1006   dev_004

3. 应用事件表 (app_events):
   event_id app_id
0      1001  app_a
1      1001  app_b
2      1002  app_c
3      1003  app_a
4      1004  app_d
5      1005  app_e
6      1006  app_f

通过观察模拟数据,我们可以理清表间关系:

  • gender_age 表提供了每个设备(device_id)对应的预测目标(group)。
  • events 表记录了每个事件(event_id)由哪个设备产生。
  • app_events 表记录了每个事件涉及了哪些APP。
  • 一个设备可以有多个事件,一个事件可以涉及多个APP。我们可以通过event_id连接events和app_events表,再通过device_id与gender_age表关联,最终为每个设备统计出行为特征。

特征提取与数据融合

基于上述关系,我们为每个设备提取两个特征:

  1. 事件类型个数:该设备产生的不同事件(event_id)的数量。
  2. 安装APP个数:该设备在所有事件中涉及的不同APP(app_id)的数量。

下面我们使用Pandas的merge和groupby操作来完成数据融合与特征提取。

# 特征提取与数据融合演示
import pandas as pd

# 使用上一代码块构造的模拟数据
gender_age_data = {'device_id': ['dev_001', 'dev_002', 'dev_003', 'dev_004'], 'group': ['M29+', 'F29+', 'F28-', 'M28-']}
events_data = {'event_id': [1001, 1002, 1003, 1004, 1005, 1006], 'device_id': ['dev_001', 'dev_001', 'dev_002', 'dev_003', 'dev_003', 'dev_004']}
app_events_data = {'event_id': [1001, 1001, 1002, 1003, 1004, 1005, 1006], 'app_id': ['app_a', 'app_b', 'app_c', 'app_a', 'app_d', 'app_e', 'app_f']}

gender_age_df = pd.DataFrame(gender_age_data)
events_df = pd.DataFrame(events_data)
app_events_df = pd.DataFrame(app_events_data)

# 1. 合并事件与APP数据,并统计特征
# 将 app_events 和 events 通过 event_id 左连接合并
device_app = app_events_df.merge(events_df, how='left', on='event_id')

# 按 device_id 分组,统计唯一 event_id 和 app_id 的个数
stats_df = device_app.groupby('device_id')[['event_id', 'app_id']].nunique()
stats_df = stats_df.reset_index()  # 将分组索引转为列

# 2. 将统计特征与分组标签数据合并,通过 device_id 内连接
all_df = gender_age_df.merge(stats_df, how='inner', on='device_id')

# 3. 重命名特征列
all_df.columns = ['device_id', 'group', 'n_events', 'n_apps']

print("融合并提取特征后的数据集:")
print(all_df)
print("\n特征说明:")
print("- n_events: 设备产生的事件类型数")
print("- n_apps: 设备涉及的APP数")
融合并提取特征后的数据集:
  device_id group  n_events  n_apps
0   dev_001  M29+         2       3
1   dev_002  F29+         1       1
2   dev_003  F28-         2       2
3   dev_004  M28-         1       1

特征说明:
- n_events: 设备产生的事件类型数
- n_apps: 设备涉及的APP数

EDA、特征归一化与标签编码

在建模之前,我们需要对数据进行探索性分析(EDA),了解特征分布和类别情况。同时,机器学习模型通常要求特征处于同一量纲,并对分类标签进行数值编码。

# EDA 与特征归一化演示
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.preprocessing import MinMaxScaler
from sklearn.preprocessing import LabelEncoder
import pandas as pd

# 构造演示数据
data = {
    'device_id': ['dev_001', 'dev_002', 'dev_003', 'dev_004', 'dev_005', 'dev_006'],
    'group': ['M29+', 'F29+', 'F28-', 'M28-', 'M29+', 'F28-'],
    'n_events': [45, 120, 30, 200, 80, 60],
    'n_apps': [12, 35, 8, 80, 25, 18]
}
df = pd.DataFrame(data)

print("数据集形状:", df.shape)
print("\n类别分布:")
print(df['group'].value_counts())

# 1. 标签编码:将字符串类别转为数字
label_enc = LabelEncoder()
df['group_encoded'] = label_enc.fit_transform(df['group'])
print("\n编码后的标签 (group_encoded):")
print(df[['group', 'group_encoded']].drop_duplicates().sort_values('group_encoded'))

# 2. 特征归一化 (Min-Max Scaling)
scaler = MinMaxScaler()
# 选择要归一化的特征列
features_to_scale = df[['n_events', 'n_apps']]
features_scaled = scaler.fit_transform(features_to_scale)
# 将归一化后的特征放回DataFrame
df[['n_events_scaled', 'n_apps_scaled']] = features_scaled

print("\n归一化前后的特征对比:")
print(df[['n_events', 'n_events_scaled', 'n_apps', 'n_apps_scaled']].head())

# 3. 简单的EDA可视化
fig, axes = plt.subplots(1, 2, figsize=(12, 4))

# 类别分布条形图
sns.countplot(data=df, x='group', ax=axes[0])
axes[0].set_title('类别分布 (Group Distribution)')

# 特征散点图(按类别着色)
scatter = axes[1].scatter(df['n_events'], df['n_apps'], c=df['group_encoded'], cmap='viridis', alpha=0.7)
axes[1].set_xlabel('n_events')
axes[1].set_ylabel('n_apps')
axes[1].set_title('特征散点图 (Colored by Group)')
# 添加图例
legend_labels = label_enc.inverse_transform(sorted(df['group_encoded'].unique()))
for i, label in enumerate(legend_labels):
    axes[1].scatter([], [], c=[plt.cm.viridis(i/3)], label=label)
axes[1].legend(title='Group')

plt.tight_layout()
# 在讲义中,我们仅展示绘图逻辑,实际运行时不保存文件
plt.show()
数据集形状: (6, 4)

类别分布:
group
M29+    2
F28-    2
F29+    1
M28-    1
Name: count, dtype: int64

编码后的标签 (group_encoded):
  group  group_encoded
2  F28-              0
1  F29+              1
3  M28-              2
0  M29+              3

归一化前后的特征对比:
   n_events  n_events_scaled  n_apps  n_apps_scaled
0        45         0.088235      12       0.055556
1       120         0.529412      35       0.375000
2        30         0.000000       8       0.000000
3       200         1.000000      80       1.000000
4        80         0.294118      25       0.236111

模型构建、调参与结果比较

现在,我们有了特征(n_events, n_apps)和数值化的标签,可以开始构建分类模型。我们将尝试三种常见的分类模型:K近邻(KNN)、线性支持向量机(Linear SVM)和逻辑回归(Logistic Regression)。对于每个模型,我们将使用GridSearchCV进行超参数调优,并比较特征归一化对模型性能的影响。

# 模型构建、调参与比较
from sklearn.neighbors import KNeighborsClassifier
from sklearn.svm import LinearSVC
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import GridSearchCV, train_test_split
from sklearn.preprocessing import MinMaxScaler, LabelEncoder
import numpy as np
import pandas as pd

# 为了演示,我们构造一个稍大的模拟数据集,以便进行训练/测试分割
np.random.seed(42)  # 固定随机种子,确保结果可复现
n_samples = 40
# 构造特征:假设 n_events 和 n_apps 与组别有一定关系
X_demo = np.random.randint(1, 100, size=(n_samples, 2)).astype(float)
# 构造标签:简单规则,根据特征和生成4类标签
y_demo = (X_demo[:, 0] / 25 + X_demo[:, 1] / 30).astype(int) % 4

# 分割数据集
X_train, X_test, y_train, y_test = train_test_split(X_demo, y_demo, test_size=0.25, random_state=10)

# 特征归一化
scaler = MinMaxScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)

# 定义要比较的模型及其参数网格
model_dict = {
    'kNN': (KNeighborsClassifier(), {'n_neighbors': [3, 5, 7]}),
    'LinearSVM': (LinearSVC(max_iter=5000, random_state=42), {'C': [0.1, 1, 10]}),
    'LR': (LogisticRegression(max_iter=500, random_state=42), {'C': [0.01, 0.1, 1]})
}

print("模型调参结果比较:")
print("-" * 50)

results = []
for model_name, (model, param_grid) in model_dict.items():
    print(f"\n>>> 训练模型: {model_name}")
    
    # 1. 使用未归一化的特征
    gs = GridSearchCV(model, param_grid, cv=3, scoring='accuracy')
    gs.fit(X_train, y_train)
    test_acc_raw = gs.score(X_test, y_test)
    print(f"  未归一化 -> 最优参数: {gs.best_params_}, 验证集最高得分: {gs.best_score_:.3f}, 测试集准确率: {test_acc_raw:.3f}")
    
    # 2. 使用归一化的特征
    gs_scaled = GridSearchCV(model, param_grid, cv=3, scoring='accuracy')
    gs_scaled.fit(X_train_scaled, y_train)
    test_acc_scaled = gs_scaled.score(X_test_scaled, y_test)
    print(f"  归一化后 -> 最优参数: {gs_scaled.best_params_}, 验证集最高得分: {gs_scaled.best_score_:.3f}, 测试集准确率: {test_acc_scaled:.3f}")
    
    results.append({
        'Model': model_name,
        'Not Scaled (%)': round(test_acc_raw * 100, 1),
        'Scaled (%)': round(test_acc_scaled * 100, 1)
    })

# 展示结果比较
results_df = pd.DataFrame(results).set_index('Model')
print("\n" + "="*50)
print("最终准确率比较 (%):")
print(results_df)
模型调参结果比较:
--------------------------------------------------

>>> 训练模型: kNN
  未归一化 -> 最优参数: {'n_neighbors': 3}, 验证集最高得分: 0.567, 测试集准确率: 0.600
  归一化后 -> 最优参数: {'n_neighbors': 3}, 验证集最高得分: 0.567, 测试集准确率: 0.600

>>> 训练模型: LinearSVM
  未归一化 -> 最优参数: {'C': 1}, 验证集最高得分: 0.367, 测试集准确率: 0.500
  归一化后 -> 最优参数: {'C': 1}, 验证集最高得分: 0.400, 测试集准确率: 0.500

>>> 训练模型: LR
  未归一化 -> 最优参数: {'C': 0.01}, 验证集最高得分: 0.367, 测试集准确率: 0.500
  归一化后 -> 最优参数: {'C': 1}, 验证集最高得分: 0.367, 测试集准确率: 0.400

==================================================
最终准确率比较 (%):
           Not Scaled (%)  Scaled (%)
Model
kNN                  60.0        60.0
LinearSVM            50.0        50.0
LR                   50.0        40.0

从演示结果可以看出,在这个模拟数据集上,特征归一化并未带来测试准确率的提升:KNN与线性SVM归一化前后持平(0.600与0.500),逻辑回归反而从0.500降至0.400。这验证了一个重要观点:特征归一化并不总是提升模型性能,其效果取决于具体模型和数据——本例中两个特征(事件类型数、APP数)本就处于相近量纲,归一化自然影响有限;只有当特征量纲差异巨大时,对依赖距离计算的模型(如KNN)和使用梯度下降类优化算法的模型(如SVM、逻辑回归)归一化才会明显受益。

在原课程的真实项目运行中,使用两个特征(事件类型数和APP数)在测试集上取得的最佳准确率约为38%。虽然看起来不高,但考虑到这是一个四分类问题(随机猜测的基线是25%),模型确实学习到了一些规律。若要进一步提升性能,可以尝试从提供的其他数据表(如手机品牌、APP类别表)中提取更多特征。

📝 动手练一练

  1. 概念辨析:为什么逻辑回归(Logistic Regression)名字里有“回归”,却是一个分类模型?特征归一化对逻辑回归模型的影响机制是什么?
👉 点击查看参考答案
  • 逻辑回归的本质:逻辑回归的“回归”源于其历史和发展过程。它最初用于解决二分类问题,其核心是使用逻辑函数(Sigmoid函数)将线性回归的连续值输出映射到(0,1)区间,解释为属于正类的概率。因此,它本质上是一个概率型分类模型,而非预测连续值的回归模型。
  • 归一化对逻辑回归的影响:逻辑回归的求解通常使用梯度下降等优化算法。如果特征量纲差异巨大,梯度更新时会在量纲大的特征方向上剧烈震荡,导致收敛缓慢甚至难以找到最优解。归一化使所有特征处于相近的范围,有助于优化算法更平稳、更快地收敛,从而可能提升模型性能与稳定性。不过在课程演示的小数据集上,两个特征本就量纲相近,实测归一化后逻辑回归的准确率反而从0.500降至0.400——这正印证了归一化的收益取决于数据本身的量纲差异程度。
  1. 动手实践:假设你拿到了phone_brand_device_model表,其中包含device_id、phone_brand(手机品牌)和device_model(手机型号)信息。你如何将这个信息作为新的特征加入到我们现有的建模流程中?请简述你的思路(例如,是直接使用品牌名称,还是进行某种编码?)。
👉 点击查看参考答案

思路简述:

  1. 数据读取与合并:使用Pandas读取phone_brand_device_model表,并通过device_id将其与我们已经融合好的all_df进行合并(例如左连接或内连接)。
  2. 特征编码:品牌和型号通常是类别型特征(字符串),不能直接输入数值模型。需要进行编码。
    • 标签编码(Label Encoding):为每个品牌/型号分配一个唯一的整数。适用于树模型,但对线性模型可能引入错误的序关系。
    • 独热编码(One-Hot Encoding):使用pd.get_dummies()为每个品牌/型号创建新的二进制特征列。这是更通用的方法,尤其适用于线性模型,但可能会增加特征维度(维度灾难)。
  3. 特征选择:如果品牌/型号类别非常多,独热编码后特征会非常稀疏。可以考虑:
    • 只保留出现频率最高的前N个品牌/型号进行编码。
    • 将低频品牌/型号统一归为“其他”类别。
  4. 加入模型:将编码后的新特征列与原有的n_events、n_apps特征拼接,形成新的特征矩阵,然后重新进行数据分割、归一化(注意:独热编码后的特征通常不需要归一化)和模型训练。

本章小结

本节我们完成了一个完整的数据分析及机器学习建模实战。我们从一个多表关联的真实场景出发,经历了从数据解读、特征提取、数据清洗、探索性分析到最终构建并评估多个分类模型的完整流程。

📋 行动清单

  • 回顾数据融合:理解如何使用Pandas的merge和groupby从多个关联表中提取聚合特征。
  • 掌握预处理步骤:记住分类建模前,对特征进行归一化(MinMaxScaler)和对标签进行编码(LabelEncoder)是标准步骤。
  • 实践模型调优:使用GridSearchCV对不同的分类模型(如KNN、SVM、逻辑回归)进行超参数搜索,并学会比较不同预处理下模型的性能。
  • 思考特征工程:尝试构思如何利用案例中未使用的其他数据表(如手机品牌、APP类别)来构造更多特征,以提升模型预测能力。

—— 小象教研组

配套资料 · 直接下载

课件、代码、数据集等配套资料全部免费,点击即可直接下载,无需分享或解锁。

  • 全套课件打包(第1-5章)
    ZIP · 12.8MB
    下载
  • 全套课件打包(第6-8章)
    ZIP · 15MB
    下载
  • 实战数据集:AppleStore 应用商城分析
    ZIP · 329KB
    下载
  • 实战数据集:女性服装电商分析
    ZIP · 2.8MB
    下载
  • Python 数据分析环境搭建指南
    PDF · 2MB
    下载
  • Scrapy 安装教程
    PDF · 12.7MB
    下载
  • 附加实战项目:AppleStore 应用商城数据分析
    ZIP · 0.3MB · ipynb + CSV 数据
    下载
  • 附加实战项目:银行电话营销数据分析
    ZIP · 0.4MB · ipynb + CSV 数据
    下载
  • 附加实战项目:女性服装电商评论数据分析
    ZIP · 2.7MB · ipynb + CSV 数据
    下载
  • 附加实战项目:美国化学学会杂志数据分析
    ZIP · 34.2MB · ipynb + SQLite 数据库
    下载