Python大数据实战(十一):电影推荐系统入门——MovieLens百万级评分数据全维度分析

Python大数据实战(十一):电影推荐系统入门------MovieLens百万级评分数据全维度分析


文章目录


前言

"为什么有些电影评分高却无人问津?为什么年长观众打分比年轻人更慷慨?"

电影推荐系统是数据科学领域的经典课题,而 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 默认字体不支持中文字符

解决方案:

  1. 设置 plt.rcParams['font.family'] = 'Kaiti''SimHei'
  2. 确保系统已安装对应中文字体:fc-list :lang=zh
  3. 同时设置 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.useru.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 ← 真实口碑

解决方案:

  1. 设置评分人数阈值(如 ≥ 100)过滤小众电影
  2. sort_values() 中同时按 ['mean', 'count'] 降序排列
  3. 使用贝叶斯平均等算法平衡评分人数和分值

七、核心分析:评分与年龄的关系

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 极少 最高 样本量小,仅供参考

核心洞察:年龄越大的用户群体,对电影的平均打分越高。 这可能是因为:

  1. 年长用户更倾向于选择自己确定喜欢的电影观看
  2. 年长用户评分习惯更宽容
  3. 年长用户样本量较小,可能存在幸存者偏差

八、高级分析:不同年龄段对热门电影的评分交叉分析

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 数据集出发,完成了从数据加载 → 数据清洗 → 探索性分析 → 交叉分析的完整流程。核心收获如下:

  1. 多文件合并技巧 :使用 pd.merge() 将用户、评分、电影三个数据源串联,构建统一分析视图
  2. 冷启动问题处理:在计算平均评分时设置评分人数阈值,避免小众高分电影的虚假排名
  3. 年龄离散化 :通过 pd.cut() + np.arange() 将连续年龄分段,便于分组统计
  4. 交叉透视分析groupby().mean().unstack().fillna() 四连招实现多维交叉表
  5. 中文显示配置:Matplotlib 中文字体设置是每个数据分析师的必修课

核心发现: 年龄越大的用户群体对电影的平均打分越高;科幻经典和动画短片分别在"评分次数"和"平均评分"两个维度上占据优势。


下一篇预告

下一篇文章将深入 航班准点数据分析------我们将面对百万级的航班起降记录,使用 Pandas 高级技巧分析延误规律、航空公司准点率排行、以及天气因素对航班的影响。敬请期待!


参考链接

  1. MovieLens 官方数据集 - GroupLens Research, University of Minnesota
  2. Pandas 官方文档 - merge
  3. Pandas 官方文档 - cut
  4. Matplotlib 中文显示解决方案
  5. MovieLens 100K Dataset README

本文收录于「16个Python大数据项目实战」系列,作者持续更新中。如有疑问欢迎在评论区交流讨论!