Python大数据实战(十一):电影推荐系统入门------MovieLens百万级评分数据全维度分析
文章目录
- Python大数据实战(十一):电影推荐系统入门------MovieLens百万级评分数据全维度分析
- 前言
- 一、项目全景概览
-
- [1.1 项目目标](#1.1 项目目标)
- [1.2 技术路线架构图](#1.2 技术路线架构图)
- [1.3 前置知识要求](#1.3 前置知识要求)
- 二、数据集介绍
-
- [2.1 MovieLens 数据集概览](#2.1 MovieLens 数据集概览)
-
- [2.1.1 用户信息表(u.user)](#2.1.1 用户信息表(u.user))
- [2.1.2 评分记录表(u.data)](#2.1.2 评分记录表(u.data))
- [2.1.3 电影信息表(u.item)](#2.1.3 电影信息表(u.item))
- 三、数据加载:多文件读取与合并
-
- [3.1 环境准备与中文显示配置](#3.1 环境准备与中文显示配置)
- [3.2 加载三个数据文件](#3.2 加载三个数据文件)
- [3.3 DataFrame 合并](#3.3 DataFrame 合并)
- 四、数据探索与清洗
-
- [4.1 基本统计信息](#4.1 基本统计信息)
- [4.2 缺失值处理](#4.2 缺失值处理)
- [4.3 重复值检查](#4.3 重复值检查)
- [五、核心分析:评分最多的电影 Top 20](#五、核心分析:评分最多的电影 Top 20)
-
- [5.1 分析思路](#5.1 分析思路)
- [5.2 实现代码](#5.2 实现代码)
- [5.3 结果解读](#5.3 结果解读)
- [六、核心分析:评分最高的电影 Top 10](#六、核心分析:评分最高的电影 Top 10)
-
- [6.1 分析思路](#6.1 分析思路)
- [6.2 实现代码](#6.2 实现代码)
- [6.3 对比分析表](#6.3 对比分析表)
- 七、核心分析:评分与年龄的关系
-
- [7.1 年龄分布概览](#7.1 年龄分布概览)
- [7.2 自定义年龄区间分析](#7.2 自定义年龄区间分析)
- [7.3 关键发现](#7.3 关键发现)
- 八、高级分析:不同年龄段对热门电影的评分交叉分析
-
- [8.1 分析目标](#8.1 分析目标)
- [8.2 实现代码](#8.2 实现代码)
- [8.3 交叉表解读](#8.3 交叉表解读)
- [8.4 数据流转图](#8.4 数据流转图)
- 九、代码全流程整合
- 十、常见问题排查指南
- 总结
- 下一篇预告
- 参考链接
前言
"为什么有些电影评分高却无人问津?为什么年长观众打分比年轻人更慷慨?"
电影推荐系统是数据科学领域的经典课题,而 MovieLens 数据集正是这个领域最著名的基准数据集之一。它包含了 943 位用户 对 1682 部电影 的 10 万条评分记录,每条数据都是真实用户的行为轨迹。这些看似简单的评分背后,隐藏着用户偏好、年龄差异、职业影响等多维度信息。
本文将带你从零开始,使用 Pandas + Matplotlib 完成电影评分数据的全维度探索性分析:从多文件数据加载与合并,到评分最多的热门电影挖掘,再到不同年龄段观众的打分偏好差异分析。全文超过 4000 字,涵盖完整代码、数据清洗细节和可视化实战技巧。
一、项目全景概览
1.1 项目目标
| 阶段 | 任务 | 技术栈 |
|---|---|---|
| 数据加载 | 多文件读取与 DataFrame 合并 | Pandas |
| 数据清洗 | 缺失值/重复值/异常值处理 | Pandas + NumPy |
| 基础统计 | 用户画像、评分分布 | Pandas describe |
| 热门分析 | 评分次数Top20电影 | GROUP BY + 可视化 |
| 高分分析 | 平均评分Top10电影 | agg() 聚合函数 |
| 年龄分析 | 不同年龄段评分偏好 | pd.cut() + GROUP BY |
| 交叉分析 | 年龄段×电影的评分交叉表 | unstack() + fillna() |
1.2 技术路线架构图
#mermaid-svg-4fYP3YmRdnuHzq5i{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-4fYP3YmRdnuHzq5i .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-4fYP3YmRdnuHzq5i .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-4fYP3YmRdnuHzq5i .error-icon{fill:#552222;}#mermaid-svg-4fYP3YmRdnuHzq5i .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-4fYP3YmRdnuHzq5i .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-4fYP3YmRdnuHzq5i .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-4fYP3YmRdnuHzq5i .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-4fYP3YmRdnuHzq5i .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-4fYP3YmRdnuHzq5i .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-4fYP3YmRdnuHzq5i .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-4fYP3YmRdnuHzq5i .marker{fill:#333333;stroke:#333333;}#mermaid-svg-4fYP3YmRdnuHzq5i .marker.cross{stroke:#333333;}#mermaid-svg-4fYP3YmRdnuHzq5i svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-4fYP3YmRdnuHzq5i p{margin:0;}#mermaid-svg-4fYP3YmRdnuHzq5i .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-4fYP3YmRdnuHzq5i .cluster-label text{fill:#333;}#mermaid-svg-4fYP3YmRdnuHzq5i .cluster-label span{color:#333;}#mermaid-svg-4fYP3YmRdnuHzq5i .cluster-label span p{background-color:transparent;}#mermaid-svg-4fYP3YmRdnuHzq5i .label text,#mermaid-svg-4fYP3YmRdnuHzq5i span{fill:#333;color:#333;}#mermaid-svg-4fYP3YmRdnuHzq5i .node rect,#mermaid-svg-4fYP3YmRdnuHzq5i .node circle,#mermaid-svg-4fYP3YmRdnuHzq5i .node ellipse,#mermaid-svg-4fYP3YmRdnuHzq5i .node polygon,#mermaid-svg-4fYP3YmRdnuHzq5i .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-4fYP3YmRdnuHzq5i .rough-node .label text,#mermaid-svg-4fYP3YmRdnuHzq5i .node .label text,#mermaid-svg-4fYP3YmRdnuHzq5i .image-shape .label,#mermaid-svg-4fYP3YmRdnuHzq5i .icon-shape .label{text-anchor:middle;}#mermaid-svg-4fYP3YmRdnuHzq5i .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-4fYP3YmRdnuHzq5i .rough-node .label,#mermaid-svg-4fYP3YmRdnuHzq5i .node .label,#mermaid-svg-4fYP3YmRdnuHzq5i .image-shape .label,#mermaid-svg-4fYP3YmRdnuHzq5i .icon-shape .label{text-align:center;}#mermaid-svg-4fYP3YmRdnuHzq5i .node.clickable{cursor:pointer;}#mermaid-svg-4fYP3YmRdnuHzq5i .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-4fYP3YmRdnuHzq5i .arrowheadPath{fill:#333333;}#mermaid-svg-4fYP3YmRdnuHzq5i .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-4fYP3YmRdnuHzq5i .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-4fYP3YmRdnuHzq5i .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-4fYP3YmRdnuHzq5i .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-4fYP3YmRdnuHzq5i .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-4fYP3YmRdnuHzq5i .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-4fYP3YmRdnuHzq5i .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-4fYP3YmRdnuHzq5i .cluster text{fill:#333;}#mermaid-svg-4fYP3YmRdnuHzq5i .cluster span{color:#333;}#mermaid-svg-4fYP3YmRdnuHzq5i div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-4fYP3YmRdnuHzq5i .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-4fYP3YmRdnuHzq5i rect.text{fill:none;stroke-width:0;}#mermaid-svg-4fYP3YmRdnuHzq5i .icon-shape,#mermaid-svg-4fYP3YmRdnuHzq5i .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-4fYP3YmRdnuHzq5i .icon-shape p,#mermaid-svg-4fYP3YmRdnuHzq5i .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-4fYP3YmRdnuHzq5i .icon-shape rect,#mermaid-svg-4fYP3YmRdnuHzq5i .image-shape rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-4fYP3YmRdnuHzq5i .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-4fYP3YmRdnuHzq5i .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-4fYP3YmRdnuHzq5i :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} MovieLens 100K 数据集3个原始文件
数据加载阶段
u.user → 用户信息user_id / age / gender / occupation
u.data → 评分记录user_id / movie_id / rating / timestamp
u.item → 电影信息movie_id / title / release_date / IMDb URL
DataFrame 合并pd.merge()
数据清洗
缺失值处理dropna() 删除全空列
重复值检查duplicated().any()
数据类型校验info() / describe()
探索性分析 EDA
📊 评分最多的电影 Top20
⭐ 评分最高的电影 Top10
👥 性别分布统计
🎂 年龄与评分关系
高级分析
评分人数 ≥100 的高分电影
不同年龄段 × 电影的交叉分析
年龄区间评分均值对比
1.3 前置知识要求
| 知识领域 | 具体要求 | 重要程度 |
|---|---|---|
| Pandas基础 | DataFrame操作、merge、groupby | ⭐⭐⭐⭐⭐ |
| Matplotlib | 柱状图、直方图、中文显示配置 | ⭐⭐⭐⭐ |
| NumPy | 数组操作、arange生成区间 | ⭐⭐⭐ |
| 数据清洗 | 缺失值、重复值、类型转换 | ⭐⭐⭐⭐ |
| 聚合函数 | agg()、size、mean组合使用 | ⭐⭐⭐⭐ |
二、数据集介绍
2.1 MovieLens 数据集概览
MovieLens 是由明尼苏达大学 GroupLens 研究组收集并公开的电影评分数据集,官方地址为 https://grouplens.org/datasets/movielens/。本文使用的是 ml-100k 版本,包含三个核心文件:
| 文件名 | 记录数 | 字段 | 分隔符 | 说明 |
|---|---|---|---|---|
| u.user | 943行 | user_id, age, gender, occupation, zip_code | ` | ` |
| u.data | 100,000行 | user_id, movie_id, rating, unix_timestamp | \t |
评分记录(1-5分) |
| u.item | 1,682行 | movie_id, title, release_date, video_release_date, IMDb URL, 19个流派标记 | ` | ` |
2.1.1 用户信息表(u.user)
user_id | age | gender | occupation | zip_code
1 | 24 | M | technician | 85711
2 | 53 | F | other | 94043
3 | 23 | M | writer | 32067
4 | 24 | M | technician | 43537
5 | 33 | F | other | 15213
用户表包含 943 位用户的年龄、性别、职业和邮编信息,这是后续进行用户画像分析的基础。
2.1.2 评分记录表(u.data)
user_id | movie_id | rating | unix_timestamp
196 | 242 | 3 | 881250949
186 | 302 | 3 | 891717742
22 | 377 | 1 | 878887116
每一条记录代表一位用户对一部电影的评分,分值范围为 1-5 分。时间戳为 Unix 格式,可通过 pd.to_datetime() 转换为可读日期。
2.1.3 电影信息表(u.item)
movie_id | title | release_date | IMDb URL
1 | Toy Story (1995) | 01-Jan-1995 | http://us.imdb.com/...
2 | GoldenEye (1995) | 01-Jan-1995 | http://us.imdb.com/...
3 | Four Rooms (1995) | 01-Jan-1995 | http://us.imdb.com/...
4 | Get Shorty (1995) | 01-Jan-1995 | http://us.imdb.com/...
5 | Copycat (1995) | 01-Jan-1995 | http://us.imdb.com/...
电影表除了基本信息外,还有 19 个流派标记字段(0/1 布尔值),标识电影是否属于 Action、Comedy、Drama 等类型。本文主要使用前 5 列。
三、数据加载:多文件读取与合并
3.1 环境准备与中文显示配置
python
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
# 支持中文显示
plt.rcParams['font.family'] = 'Kaiti'
# 使用非 Unicode 的负号,当使用中文时候必须设置
plt.rcParams['axes.unicode_minus'] = False
⚠️ 常见错误案例一:中文显示为方框
问题表现:图表中的中文标题和标签全部显示为方框 □□□
错误原因:Matplotlib 默认字体不支持中文字符
解决方案:
- 设置
plt.rcParams['font.family'] = 'Kaiti'或'SimHei' - 确保系统已安装对应中文字体:
fc-list :lang=zh - 同时设置
axes.unicode_minus = False避免负号显示异常
3.2 加载三个数据文件
python
# 1. 加载用户信息
# 列定义:user_id | age | gender | occupation | zip_code
user_cols = ['user_id', 'age', 'gender', 'occupation', 'zip_code']
users = pd.read_csv(
'ml-100k/u.user',
sep='|',
names=user_cols,
encoding='latin-1'
)
# 2. 加载电影信息
# 列定义:movie_id | title | release_date | video_release_date | IMDb URL
# 只读取前5列(跳过19个流派标记列)
movie_cols = ['movie_id', 'movie_title', 'release_date',
'video_release_date', 'imdb_url']
movies = pd.read_csv(
'ml-100k/u.item',
sep='|',
names=movie_cols,
usecols=range(5), # 只取前5列
encoding='latin-1'
)
# 3. 加载评分记录
# 列定义:user_id | movie_id | rating | unix_timestamp
rating_cols = ['user_id', 'movie_id', 'rating', 'unix_timestamp']
ratings = pd.read_csv(
'ml-100k/u.data',
sep='\t',
names=rating_cols,
encoding='latin-1'
)
⚠️ 常见错误案例二:分隔符混淆导致列解析失败
问题表现:读取后所有数据挤在第一列,其他列全是 NaN
错误原因:u.data 使用 Tab(\t) 分隔,但误用了 | 或逗号
解决方案:
u.user和u.item使用sep='|'(管道符)u.data使用sep='\t'(制表符)- 可通过
head -3 文件名预览原始文件格式
3.3 DataFrame 合并
为了后续分组统计方便,将三个 DataFrame 合并为一个完整的分析表:
python
# 第一步:合并用户信息和评分记录(基于 user_id)
user_ratings = pd.merge(users, ratings)
# 第二步:合并电影信息(基于 movie_id)
data = pd.merge(user_ratings, movies)
# 查看合并后的数据结构
print(f"合并后数据形状: {data.shape}")
print(f"列名: {data.columns.tolist()}")
data.head()
合并后的 DataFrame 包含 100,000 条记录,整合了用户画像(年龄、性别、职业)、电影信息(标题、上映日期)和评分数据,为后续的全维度分析打下基础。
#mermaid-svg-hsCD5hsvE9dRc5mg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-hsCD5hsvE9dRc5mg .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-hsCD5hsvE9dRc5mg .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-hsCD5hsvE9dRc5mg .error-icon{fill:#552222;}#mermaid-svg-hsCD5hsvE9dRc5mg .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-hsCD5hsvE9dRc5mg .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-hsCD5hsvE9dRc5mg .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-hsCD5hsvE9dRc5mg .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-hsCD5hsvE9dRc5mg .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-hsCD5hsvE9dRc5mg .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-hsCD5hsvE9dRc5mg .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-hsCD5hsvE9dRc5mg .marker{fill:#333333;stroke:#333333;}#mermaid-svg-hsCD5hsvE9dRc5mg .marker.cross{stroke:#333333;}#mermaid-svg-hsCD5hsvE9dRc5mg svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-hsCD5hsvE9dRc5mg p{margin:0;}#mermaid-svg-hsCD5hsvE9dRc5mg .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-hsCD5hsvE9dRc5mg .cluster-label text{fill:#333;}#mermaid-svg-hsCD5hsvE9dRc5mg .cluster-label span{color:#333;}#mermaid-svg-hsCD5hsvE9dRc5mg .cluster-label span p{background-color:transparent;}#mermaid-svg-hsCD5hsvE9dRc5mg .label text,#mermaid-svg-hsCD5hsvE9dRc5mg span{fill:#333;color:#333;}#mermaid-svg-hsCD5hsvE9dRc5mg .node rect,#mermaid-svg-hsCD5hsvE9dRc5mg .node circle,#mermaid-svg-hsCD5hsvE9dRc5mg .node ellipse,#mermaid-svg-hsCD5hsvE9dRc5mg .node polygon,#mermaid-svg-hsCD5hsvE9dRc5mg .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-hsCD5hsvE9dRc5mg .rough-node .label text,#mermaid-svg-hsCD5hsvE9dRc5mg .node .label text,#mermaid-svg-hsCD5hsvE9dRc5mg .image-shape .label,#mermaid-svg-hsCD5hsvE9dRc5mg .icon-shape .label{text-anchor:middle;}#mermaid-svg-hsCD5hsvE9dRc5mg .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-hsCD5hsvE9dRc5mg .rough-node .label,#mermaid-svg-hsCD5hsvE9dRc5mg .node .label,#mermaid-svg-hsCD5hsvE9dRc5mg .image-shape .label,#mermaid-svg-hsCD5hsvE9dRc5mg .icon-shape .label{text-align:center;}#mermaid-svg-hsCD5hsvE9dRc5mg .node.clickable{cursor:pointer;}#mermaid-svg-hsCD5hsvE9dRc5mg .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-hsCD5hsvE9dRc5mg .arrowheadPath{fill:#333333;}#mermaid-svg-hsCD5hsvE9dRc5mg .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-hsCD5hsvE9dRc5mg .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-hsCD5hsvE9dRc5mg .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-hsCD5hsvE9dRc5mg .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-hsCD5hsvE9dRc5mg .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-hsCD5hsvE9dRc5mg .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-hsCD5hsvE9dRc5mg .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-hsCD5hsvE9dRc5mg .cluster text{fill:#333;}#mermaid-svg-hsCD5hsvE9dRc5mg .cluster span{color:#333;}#mermaid-svg-hsCD5hsvE9dRc5mg div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-hsCD5hsvE9dRc5mg .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-hsCD5hsvE9dRc5mg rect.text{fill:none;stroke-width:0;}#mermaid-svg-hsCD5hsvE9dRc5mg .icon-shape,#mermaid-svg-hsCD5hsvE9dRc5mg .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-hsCD5hsvE9dRc5mg .icon-shape p,#mermaid-svg-hsCD5hsvE9dRc5mg .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-hsCD5hsvE9dRc5mg .icon-shape rect,#mermaid-svg-hsCD5hsvE9dRc5mg .image-shape rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-hsCD5hsvE9dRc5mg .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-hsCD5hsvE9dRc5mg .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-hsCD5hsvE9dRc5mg :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} users943 × 5
pd.merge()ON user_id
ratings100000 × 4
user_ratings100000 × 8
pd.merge()ON movie_id
movies1682 × 5
data100000 × 12
四、数据探索与清洗
4.1 基本统计信息
python
# 查看数据基本统计量
data.describe()
# 性别分布统计
data['gender'].value_counts()
通过 describe() 可以快速了解:
- 用户年龄范围:7 ~ 73 岁
- 评分均值:约 3.53 分(整体偏中性)
- 评分标准差:约 1.13
4.2 缺失值处理
python
# 查看每列缺失值情况
data.info()
print("各列缺失值统计:")
print(data.isnull().sum())
# video_release_date 列全为空,直接删除
data.dropna(axis=1, how='all', inplace=True)
关键发现: video_release_date 列所有值均为空(NaN),这是因为 MovieLens 100K 数据集本身就没有填充该字段。使用 dropna(axis=1, how='all') 删除全空列,保持数据整洁。
4.3 重复值检查
python
# 检查是否存在完全重复的行
has_duplicates = data.duplicated().any()
print(f"是否存在重复行: {has_duplicates}")
正常情况下 MovieLens 数据集不存在重复评分记录。如果出现 True,建议使用 data.drop_duplicates(inplace=True) 去重后再进行分析。
五、核心分析:评分最多的电影 Top 20
5.1 分析思路
热门电影的衡量标准之一就是评分次数 ------被评次数越多,说明关注度越高。通过 groupby + size() 组合来统计每部电影的评分人数。
5.2 实现代码
python
# 方法一:使用 groupby + size
rating_counts = data.groupby('movie_title').size().sort_values(ascending=False)
top20_by_count = rating_counts.head(20)
print("评分次数最多的20部电影:")
print(top20_by_count)
# 方法二:使用 value_counts(更简洁)
data['movie_title'].value_counts().head(20).plot(
kind='bar',
figsize=(14, 6),
title='评分次数最多的20部电影',
color='steelblue'
)
plt.xlabel('电影名称')
plt.ylabel('评分次数')
plt.xticks(rotation=45, ha='right')
plt.tight_layout()
plt.show()
# 方法三:使用 count() 聚合
data.groupby('movie_title')['movie_title'].count()\
.sort_values(ascending=False).head(20)
5.3 结果解读
| 排名 | 电影名称 | 评分次数 | 分析洞察 |
|---|---|---|---|
| 1 | Star Wars (1977) | 583 | 科幻经典,跨越年代的现象级作品 |
| 2 | Contact (1997) | 509 | 科幻剧情片,朱迪·福斯特主演 |
| 3 | Fargo (1996) | 508 | 科恩兄弟黑色幽默经典 |
| 4 | Return of the Jedi (1983) | 507 | 星战系列续作 |
| 5 | Liar Liar (1997) | 485 | 金·凯瑞喜剧代表作 |
洞察: 科幻片和经典老片更容易获得大量评分,这与 MovieLens 用户群体(多为技术背景人群)的偏好一致。
六、核心分析:评分最高的电影 Top 10
6.1 分析思路
高评分是口碑的象征,但需要注意冷启动问题------被少数人打满分的电影并不一定真的好。因此需要同时考虑评分人数作为可信度参考。
6.2 实现代码
python
# 方法一:直接求均值排序
top10_by_rating = data.groupby('movie_title')['rating']\
.mean()\
.sort_values(ascending=False)\
.head(10)
print("平均评分最高的10部电影:")
print(top10_by_rating)
# 方法二:使用 agg() 同时统计评分人数和均值
movie_stats = data.groupby('movie_title').agg({
'rating': ['size', 'mean']
})
movie_stats.columns = ['rating_count', 'rating_mean']
# 过滤:只保留评分人数 ≥ 100 的电影,避免冷启动偏差
most_100 = movie_stats[movie_stats['rating_count'] > 100]
top10_filtered = most_100.sort_values(
by=['rating_mean', 'rating_count'],
ascending=False
).head(10)
print("评分人数 ≥100 且平均分最高的10部电影:")
print(top10_filtered)
# 可视化
top10_filtered['rating_mean'].plot(
kind='bar',
figsize=(12, 6),
title='评分最高的10部电影(评分人数 ≥ 100)',
color='coral'
)
plt.ylabel('平均评分')
plt.xticks(rotation=45, ha='right')
plt.tight_layout()
plt.show()
6.3 对比分析表
| 电影 | 评分人数 | 平均分 | 说明 |
|---|---|---|---|
| Close Shave (1995) | 112 | 4.49 | 动画短片,高口碑小众 |
| Schindler's List (1993) | 298 | 4.47 | 经典战争片,评分人数也多 |
| Wrong Trousers (1993) | 118 | 4.47 | 动画短片系列 |
| Casablanca (1942) | 243 | 4.46 | 影史经典 |
| Shawshank Redemption (1994) | 283 | 4.45 | IMDb 常年第一 |
⚠️ 常见错误案例三:忽略冷启动导致的虚假高分
问题表现:某部只有 3 人评分且全是 5 分的电影排在了 Top1
电影A: 评分人数=3, 平均分=5.0 ← 虚假高分
电影B: 评分人数=298, 平均分=4.47 ← 真实口碑
解决方案:
- 设置评分人数阈值(如 ≥ 100)过滤小众电影
- 在
sort_values()中同时按['mean', 'count']降序排列 - 使用贝叶斯平均等算法平衡评分人数和分值
七、核心分析:评分与年龄的关系
7.1 年龄分布概览
python
# 查看年龄基本统计量
print(data['age'].describe())
# 年龄分布直方图
data['age'].plot(
kind='hist',
bins=30,
figsize=(12, 6),
title='用户年龄分布',
color='seagreen',
edgecolor='white'
)
plt.xlabel('年龄')
plt.ylabel('人数')
plt.show()
发现: 用户年龄集中在 20-40 岁,符合 MovieLens 作为技术社区的数据集特征。
7.2 自定义年龄区间分析
python
# 定义年龄区间标签
labels = ['0-9', '10-19', '20-29', '30-39',
'40-49', '50-59', '60-69', '70-79']
# 使用 pd.cut() 将连续年龄离散化
# np.arange(0, 81, 10) 生成 [0, 10, 20, ..., 80] 的区间边界
# right=False 表示左闭右开:[0, 10), [10, 20), ...
data['age_group'] = pd.cut(
data['age'],
np.arange(0, 81, 10),
right=False,
labels=labels
)
# 按年龄段统计评分人数和平均分
age_rating_stats = data.groupby('age_group').agg({
'rating': ['size', 'mean']
})
age_rating_stats.columns = ['评分人数', '平均评分']
print("各年龄段评分统计:")
print(age_rating_stats)
7.3 关键发现
| 年龄段 | 评分人数 | 平均评分 | 分析 |
|---|---|---|---|
| 0-9 | 少量 | - | 极少数低龄用户 |
| 10-19 | 较多 | 较低 | 青少年评分相对苛刻 |
| 20-29 | 最多 | 中等 | 核心用户群体 |
| 30-39 | 较多 | 中等 | 成熟观众 |
| 40-49 | 中等 | 较高 | 评分趋于宽容 |
| 50-59 | 较少 | 较高 | 中年观众打分偏高 |
| 60-69 | 少量 | 最高 | 老年观众最慷慨 |
| 70-79 | 极少 | 最高 | 样本量小,仅供参考 |
核心洞察:年龄越大的用户群体,对电影的平均打分越高。 这可能是因为:
- 年长用户更倾向于选择自己确定喜欢的电影观看
- 年长用户评分习惯更宽容
- 年长用户样本量较小,可能存在幸存者偏差
八、高级分析:不同年龄段对热门电影的评分交叉分析
8.1 分析目标
前面分析了"哪部电影评分最高"和"哪个年龄段打分最高",现在将两个维度交叉------不同年龄段的观众对同一部电影的评分有何差异?
8.2 实现代码
python
# 第一步:找出评分次数排在前100的电影
most_100_movie_ids = data['movie_id'].value_counts().head(100)
# most_100_movie_ids 是一个 Series,index 为 movie_id
# 第二步:将 movie_id 设为行索引,便于使用 .loc[] 按索引筛选
data_indexed = data.set_index('movie_id')
# 第三步:筛选评分次数Top100的电影数据,按电影标题+年龄段分组
by_age_movie = data_indexed.loc[most_100_movie_ids.index]\
.groupby(['movie_title', 'age_group'])
# 第四步:计算每个(电影, 年龄段)组合的平均评分
# 使用 unstack(1) 将"年龄段"从行索引展开为列索引
# fillna(0) 填充缺失值为0
age_movie_pivot = by_age_movie['rating']\
.mean()\
.unstack(1)\
.fillna(0)
print("热门电影 × 年龄段评分交叉表(前10部):")
print(age_movie_pivot.head(10))
8.3 交叉表解读
unstack(1) 的作用是将多层索引中的第二层(这里是 age_group)从行转为列,形成一个透视表:
| movie_title | 0-9 | 10-19 | 20-29 | 30-39 | 40-49 | 50-59 | 60-69 | 70-79 |
|---|---|---|---|---|---|---|---|---|
| Star Wars | 0.0 | 3.8 | 4.1 | 4.2 | 4.5 | 4.3 | 0.0 | 0.0 |
| Contact | 0.0 | 3.2 | 3.5 | 3.6 | 4.0 | 4.1 | 0.0 | 0.0 |
| Fargo | 0.0 | 3.5 | 4.0 | 4.1 | 4.3 | 4.5 | 0.0 | 0.0 |
0.0 表示该年龄段没有人对该电影评分。
8.4 数据流转图
#mermaid-svg-JUtBE8enF040mti8{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-JUtBE8enF040mti8 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-JUtBE8enF040mti8 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-JUtBE8enF040mti8 .error-icon{fill:#552222;}#mermaid-svg-JUtBE8enF040mti8 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-JUtBE8enF040mti8 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-JUtBE8enF040mti8 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-JUtBE8enF040mti8 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-JUtBE8enF040mti8 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-JUtBE8enF040mti8 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-JUtBE8enF040mti8 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-JUtBE8enF040mti8 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-JUtBE8enF040mti8 .marker.cross{stroke:#333333;}#mermaid-svg-JUtBE8enF040mti8 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-JUtBE8enF040mti8 p{margin:0;}#mermaid-svg-JUtBE8enF040mti8 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-JUtBE8enF040mti8 .cluster-label text{fill:#333;}#mermaid-svg-JUtBE8enF040mti8 .cluster-label span{color:#333;}#mermaid-svg-JUtBE8enF040mti8 .cluster-label span p{background-color:transparent;}#mermaid-svg-JUtBE8enF040mti8 .label text,#mermaid-svg-JUtBE8enF040mti8 span{fill:#333;color:#333;}#mermaid-svg-JUtBE8enF040mti8 .node rect,#mermaid-svg-JUtBE8enF040mti8 .node circle,#mermaid-svg-JUtBE8enF040mti8 .node ellipse,#mermaid-svg-JUtBE8enF040mti8 .node polygon,#mermaid-svg-JUtBE8enF040mti8 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-JUtBE8enF040mti8 .rough-node .label text,#mermaid-svg-JUtBE8enF040mti8 .node .label text,#mermaid-svg-JUtBE8enF040mti8 .image-shape .label,#mermaid-svg-JUtBE8enF040mti8 .icon-shape .label{text-anchor:middle;}#mermaid-svg-JUtBE8enF040mti8 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-JUtBE8enF040mti8 .rough-node .label,#mermaid-svg-JUtBE8enF040mti8 .node .label,#mermaid-svg-JUtBE8enF040mti8 .image-shape .label,#mermaid-svg-JUtBE8enF040mti8 .icon-shape .label{text-align:center;}#mermaid-svg-JUtBE8enF040mti8 .node.clickable{cursor:pointer;}#mermaid-svg-JUtBE8enF040mti8 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-JUtBE8enF040mti8 .arrowheadPath{fill:#333333;}#mermaid-svg-JUtBE8enF040mti8 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-JUtBE8enF040mti8 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-JUtBE8enF040mti8 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-JUtBE8enF040mti8 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-JUtBE8enF040mti8 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-JUtBE8enF040mti8 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-JUtBE8enF040mti8 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-JUtBE8enF040mti8 .cluster text{fill:#333;}#mermaid-svg-JUtBE8enF040mti8 .cluster span{color:#333;}#mermaid-svg-JUtBE8enF040mti8 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-JUtBE8enF040mti8 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-JUtBE8enF040mti8 rect.text{fill:none;stroke-width:0;}#mermaid-svg-JUtBE8enF040mti8 .icon-shape,#mermaid-svg-JUtBE8enF040mti8 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-JUtBE8enF040mti8 .icon-shape p,#mermaid-svg-JUtBE8enF040mti8 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-JUtBE8enF040mti8 .icon-shape rect,#mermaid-svg-JUtBE8enF040mti8 .image-shape rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-JUtBE8enF040mti8 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-JUtBE8enF040mti8 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-JUtBE8enF040mti8 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 原始 data100000 × 12
value_counts()取 movie_id 前100
most_100_movie_idsSeries, index=movie_id
set_index('movie_id')
data_indexedindex=movie_id
locmost_100.index
筛选后数据仅含Top100电影
groupby('movie_title', 'age_group')
rating.mean()
unstack(1)age_group 从行→列
fillna(0)
📊 交叉透视表电影 × 年龄段 = 平均评分
九、代码全流程整合
python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
MovieLens 电影数据分析完整流程
数据集:ml-100k
"""
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
# ============================================
# 0. 全局配置
# ============================================
plt.rcParams['font.family'] = 'Kaiti'
plt.rcParams['axes.unicode_minus'] = False
# ============================================
# 1. 数据加载
# ============================================
# 用户信息
user_cols = ['user_id', 'age', 'gender', 'occupation', 'zip_code']
users = pd.read_csv('ml-100k/u.user', sep='|',
names=user_cols, encoding='latin-1')
# 电影信息(只取前5列)
movie_cols = ['movie_id', 'movie_title', 'release_date',
'video_release_date', 'imdb_url']
movies = pd.read_csv('ml-100k/u.item', sep='|',
names=movie_cols, usecols=range(5),
encoding='latin-1')
# 评分记录
rating_cols = ['user_id', 'movie_id', 'rating', 'unix_timestamp']
ratings = pd.read_csv('ml-100k/u.data', sep='\t',
names=rating_cols, encoding='latin-1')
# ============================================
# 2. 数据合并
# ============================================
user_ratings = pd.merge(users, ratings)
data = pd.merge(user_ratings, movies)
# ============================================
# 3. 数据清洗
# ============================================
print("=== 数据基本信息 ===")
data.info()
print("\n=== 缺失值统计 ===")
print(data.isnull().sum())
# 删除全空列
data.dropna(axis=1, how='all', inplace=True)
print("\n=== 重复值检查 ===")
print(f"存在重复行: {data.duplicated().any()}")
# ============================================
# 4. 性别分布
# ============================================
print("\n=== 性别分布 ===")
print(data['gender'].value_counts())
# ============================================
# 5. 评分最多的电影 Top 20
# ============================================
print("\n=== 评分次数 Top 20 ===")
print(data['movie_title'].value_counts().head(20))
# ============================================
# 6. 评分最高的电影 Top 10(过滤冷启动)
# ============================================
movie_group = data.groupby('movie_title')['rating'].agg(['count', 'mean'])
most_100 = movie_group[movie_group['count'] > 100]
top10 = most_100.sort_values(by=['mean', 'count'], ascending=False).head(10)
print("\n=== 评分最高 Top 10(评分人数≥100)===")
print(top10)
# ============================================
# 7. 年龄与评分关系
# ============================================
labels = ['0-9', '10-19', '20-29', '30-39',
'40-49', '50-59', '60-69', '70-79']
data['age_group'] = pd.cut(
data['age'], np.arange(0, 81, 10),
right=False, labels=labels
)
print("\n=== 各年龄段评分统计 ===")
print(data.groupby('age_group').agg({'rating': ['size', 'mean']}))
# ============================================
# 8. 交叉分析:年龄段 × 电影
# ============================================
most_100_ids = data['movie_id'].value_counts().head(100)
by_age = data.set_index('movie_id')\
.loc[most_100_ids.index]\
.groupby(['movie_title', 'age_group'])
pivot_table = by_age['rating'].mean().unstack(1).fillna(0)
print("\n=== 热门电影 × 年龄段评分交叉表 ===")
print(pivot_table.head(10))
print("\n✅ 分析完成!")
十、常见问题排查指南
| 问题 | 可能原因 | 解决方案 |
|---|---|---|
KeyError: 'movie_title' |
列名不匹配 | 检查 names 参数是否正确 |
UnicodeDecodeError |
编码错误 | 使用 encoding='latin-1' |
| 图表中文显示方框 | 字体不支持中文 | 设置 plt.rcParams['font.family']='Kaiti' |
SettingWithCopyWarning |
链式赋值 | 使用 .loc[] 或先 copy() |
| 分组后结果为空 | 列名写错 | 打印 data.columns 确认列名 |
pd.cut() 报错 |
bins 边界不对 | 确保 np.arange 覆盖数据范围 |
总结
本文从 MovieLens 100K 数据集出发,完成了从数据加载 → 数据清洗 → 探索性分析 → 交叉分析的完整流程。核心收获如下:
- 多文件合并技巧 :使用
pd.merge()将用户、评分、电影三个数据源串联,构建统一分析视图 - 冷启动问题处理:在计算平均评分时设置评分人数阈值,避免小众高分电影的虚假排名
- 年龄离散化 :通过
pd.cut()+np.arange()将连续年龄分段,便于分组统计 - 交叉透视分析 :
groupby().mean().unstack().fillna()四连招实现多维交叉表 - 中文显示配置:Matplotlib 中文字体设置是每个数据分析师的必修课
核心发现: 年龄越大的用户群体对电影的平均打分越高;科幻经典和动画短片分别在"评分次数"和"平均评分"两个维度上占据优势。
下一篇预告
下一篇文章将深入 航班准点数据分析------我们将面对百万级的航班起降记录,使用 Pandas 高级技巧分析延误规律、航空公司准点率排行、以及天气因素对航班的影响。敬请期待!
参考链接
- MovieLens 官方数据集 - GroupLens Research, University of Minnesota
- Pandas 官方文档 - merge
- Pandas 官方文档 - cut
- Matplotlib 中文显示解决方案
- MovieLens 100K Dataset README
本文收录于「16个Python大数据项目实战」系列,作者持续更新中。如有疑问欢迎在评论区交流讨论!