资讯详情 豆瓣电影10万数据集:从清洗到推荐系统的完整实战
📅 2026/10/17 19:50:57
简介十万条规模的豆瓣电影数据包面向数据分析爱好者、推荐系统开发者及影视市场研究者可支撑用户行为分析、个性化推荐、情感挖掘等多种场景。资源共4个文件含两个CSV表格、一个SQLite数据库和一个爬虫脚本压缩后约40MB表格分别记录用户与电影基础数据涵盖编号、片名、导演、类型、评分等关键字段数据库则包含观影记录、影评和互动信息便于深度查询脚本演示了抓取清洗数据的完整思路。目前已有3442人学习。借助这套数据既可构建基于用户评分与电影属性的推荐系统也能分析不同影片类型的受欢迎程度与口碑走向还能通过爬虫代码理解从采集、解析到存储的工程化方法。数据真实来源于豆瓣对学术研究与商业决策均有参考价值。1. 这个压缩包里装了什么十万部电影信息和它的真实用途拿到“豆瓣电影数据集10万.zip”这个文件名第一反应很容易是“不就是10万行电影列表”实际上它对做中文推荐系统的人来说是相当好的一手素材包含电影基础信息、导演主演、类型标签、评分及评分人数还有年份和地区既能训练内容推荐模型也能做统计分析。自己从零去抓豆瓣公开页面要面对登录验证、反爬限制、IP封禁这一串麻烦公开数据集的价值就是先把采集环节省掉把时间留给清洗、特征工程和算法实验。这份数据适合两类人练手推荐系统的开发者以及准备做电影分析类产品、需要快速拿到结构化样本的从业者。当然它也保留了豆瓣数据的固有脾气——字段缺失、版本混杂、编码陷阱后面几章专门讲怎么处理。2. 拆开压缩包先看骨头文件结构与核心字段取舍先纠正一个很多人会踩的误判拿到“豆瓣电影数据集10万.zip”后直接解压看CSV不先看字段说明。这个数据集在社区里有多个流传版本常见结构是一张主表加可选的评论表主表每行是一部电影字段命名每个版本不一样比如类型字段有叫“genres”也有叫“type”主演字段有叫“casts”也有叫“actors”。先把字段对应关系捋清后面所有清洗代码才不会反复改。2.1 主表字段速查先分清展示类与特征类解开压缩包后核心文件通常是一张逗号分隔的电影主表有时还带一个字段说明文档。我的习惯是先把字段分成“展示类”和“特征类”展示类如片名、详情页URL主要用于前台页面展示特征类如类型、主演、导演用于算法和筛选。下面这张速查表覆盖了最常见主表结构你拿到的版本可能缺一两列或多一两列但核心字段基本在其中。字段常见命名类型缺失风险在项目里的用途电影名name / titlestr低展示、检索、去重评分rating / scorefloat中排序、推荐加权评分人数votes / rating_peopleint中热度加权、过滤噪声导演directorstr低内容特征、作者偏好主演casts / actorsstr中内容特征年份year / pubdateint / str中时间衰减、趋势分析类型genres / typestr低推荐核心特征地区regions / countrystr中地区过滤、用户画像语言languagestr高辅助过滤可缺失详情页URLurl / altstr低跳转展示或补充采集这里要重点记住一行主表是“一部电影一行”不是“一条评分一行”。也就是说如果没有带用户ID的评分子表基于用户的协同过滤在主表上做不了你能用的只是电影侧的结构化信息。另一个容易看错的列是“评分人数”它不只是统计字段而是评分的可信度标尺。一部只有两个人打分的小众片拿了9.8分和一部两万人评的8.5分电影相比后者的口碑参考价值明显更高所以后面所有榜单类逻辑都得把评分人数作为权重带进去。2.2 三类字段决定项目下限评分、类型、年份第一类是评分和评分人数。这两个字段最直接的用法是排“评分热榜”但直接按评分排序会翻车因为冷门片样本小、方差大几个5星就能把均值顶上去。我一般会做热度加权用贝叶斯平均的思路给评分打折扣# 用评分人数做平滑系数避免小样本高分霸榜 C 100.0 # 平滑常数决定“多少票才算可信” mean_rating df[rating].mean() df[weighted_rating] ( (df[votes] / (df[votes] C)) * df[rating] (C / (df[votes] C)) * mean_rating )逻辑说明这个公式把每部电影的评分往全局均值方向拉。评分人数很少时votes / (votes C)接近0加权分逼近全局均值评分人数很大时权重接近1加权分接近原始评分。C值取多少要看数据分布10万行规模取50到200之间都合理C越大对冷门片越严厉。第二类是类型和主演。类型字段通常是用分隔符把多个值拼在一格里存放一部电影同时属于“剧情”“爱情”“历史”三个标签很正常。这种结构不能直接进分类模型必须先拆。主演字段更麻烦部分版本会把角色名用括号写在人名后面比如“某演员饰某角色”正则清洗要放在拆分之前顺序反了后面的One-Hot编码会多出一堆无意义特征。实测中还发现类型字段有时存在空字符串而不是NaN空字符串在 if 判断里是 True容易混过缺失值检查需要先替换成NaN。第三类是年份和地区。年份适合做时间维度分析比如看哪些年份高分电影密度最高也可以给推荐结果加时间衰减。但年份字段本身可能混入“2001-01-01”这种完整日期格式直接pd.to_numeric会报错或读成NaN地区字段则是多个地区挤在一格里要像类型一样拆分。这两个字段单独作为特征的效果一般但和类型组合后对相似度计算的区分度有明显提升。最后说一句字段优先级。如果目标是推荐系统优先级是“类型 评分人数 主演 导演 年份 地区 语言”如果目标是统计可视化优先级变成“年份 评分 评分人数 类型 地区”。目标不同前面清洗脚本里保留字段的策略也会不同所以拿到数据先别急着动手先明确项目输出是什么。3. 读进内存只是开始编码、脏数据和列表字段的清洗读取一个10万行的CSV在技术上没有难度真正的难点是编码和字段污染。数据集是从豆瓣不同来源导出的工具链不同文件编码可能是UTF-8也可能是GBK。不要赌编码先用探测库确认再把结果交给pandas。3.1 最小读取方案一次跑通先做编码探测再读取主表。这段代码直接抄了能跑。import pandas as pd from charset_normalizer import from_path # 第一步探测文件真实编码避免手动试错 result from_path(douban_movies.csv).best() print(encoding:, result.encoding) # 第二步按探测结果读取主表 df pd.read_csv( douban_movies.csv, encodingresult.encoding, # 常见结果是 utf-8 或 gb18030 sep,, # 如果解压出来是tsv格式改成 sep\t dtype{year: str}, # 年份先按字符串读防止被读成float low_memoryFalse, # 10万行级别没有性能压力关掉分块避免类型误判 ) print(df.shape) print(df.columns.tolist())逻辑说明charset_normalizer是比 chardet 更新一点的编码探测库from_path会采样文件头部给出候选编码best()取置信度最高的结果。把探测结果直接传给encoding在绝大多数情况下能得到 utf-8 或 gb18030。dtype{year: str}这行很多人会忽略但如果年份列里混入完整日期字符串pandas 可能自动推断成 datetime后面做年份过滤反而多一层转换。low_memoryFalse在10万行规模不会拖慢速度但能避免 pandas 因为分块读取而把某列类型推断成 object这是一个很隐蔽的坑。读取之后不要急着清洗先做一轮体检输出缺失占比、重复行数和几个真实样例# 体检输出缺失占比、整行重复数、类型和主演字段的真实形态 print(缺失占比: ) print(df.isnull().mean().round(3)) print(整行重复: , df.duplicated().sum()) print(类型样例: , df[genres].dropna().head(3).to_list()) print(主演样例: , df[casts].dropna().head(3).to_list())参数说明isnull().mean()是计算缺失率的最短写法布尔值True被当1、False被当0均值就是缺失比例越接近1说明字段越可有可无。duplicated()默认要求整行所有字段完全相等才算重复这一步只能查出“完全一样”的垃圾行片名相同但导演不同的“变体重复”必须用 subset 参数单独判断。to_list()把Series转成列表打印是故意为之直接跑head()在终端里看到的是带索引的列不如列表直观。3.2 三类脏数据的处理原则看完体检输出后把脏数据分三类处理。第一类是整字段缺失。语言字段缺失率最高地区次之。缺失不等于错误如果字段不影响推荐逻辑保留NaN或填“未知”即可但类型这种核心特征一旦缺失这一行基本不能再参与特征计算应在构造特征矩阵前整行过滤否则 One-Hot 出来的就是全零向量。第二类是格式混用。最典型的是类型和主演字段的分隔符不统一一部分行用管道符一部分用斜杠还有一部分用中文逗号。这是多来源数据合并的后遗症。处理顺序必须是“先统一后拆分”不能边拆边替换否则会漏掉一批行。还有一个容易被忽略的细节是演员名里可能带空格比如“张 某”拆分后要记得 strip 首尾空白。第三类是假正常数据。比如某行评分、评分人数都有但类型字段是空字符串而不是NaN再比如年份写成“未知”。空字符串和NaN在pandas里的处理路径完全不同统一用replace把空字符串转成NaN再做缺失统计否则缺失值报表会漏掉一批坏样本。3.3 列表型字段拆分统一分隔符与去角色名这里给出通用的字段规整函数兼容多种分隔符和带角色名的数据。import re import pandas as pd def normalize_genres(value): 类型字段统一分隔符后拆分去掉空白项 if pd.isna(value): return [] text str(value) text re.sub(r[|/,], |, text) # 管道符、斜杠、中英文逗号全换成 | items [x.strip() for x in text.split(|) if x.strip()] return items def normalize_cast(value): 主演字段先去角色名括号再统一分隔符拆分 if pd.isna(value): return [] text str(value) text re.sub(r.*?|\(.*?\), , text) # 去掉饰某角色 text re.sub(r[|/,], |, text) items [x.strip() for x in text.split(|) if x.strip()] return items df[genre_list] df[genres].apply(normalize_genres) df[cast_list] df[casts].apply(normalize_cast) # 检查拆分效果 print(df[[name, genres, genre_list]].head(3).to_dict(records))逻辑说明正则里的[|/,]是字符集匹配四种分隔符中的任意一种统一替换成管道符之后split就只需要处理一种情况。主演里去括号的正则写得比较保守中英文括号都覆盖因为原始数据里常见“演员名饰角色”这种写法。apply而不是for循环10万行用apply大约一两秒用循环写可能要几十秒差别不大但apply返回的列表列可以直接喂给后面的 MultiLabelBinarizer。注意函数最前面的pd.isna(value)判断必须保留它同时拦截NaN和None防止后面str(value)把NaN变成字符串“nan”混进列表。做完这步数据里就多了genre_list和cast_list两个列表列它们直接对接后面的特征矩阵构建。到这里清洗工作完成了一半剩下的就是按需求做特征工程。4. 十万条数据怎么产出推荐内容召回与相似度两个方案清洗的最终目的不是得到一个干净表格而是让它参与推荐计算。这里介绍两个不依赖用户ID的方案分别对应冷启动场景和相似物品召回场景。4.1 冷启动内容召回从类型标签生成候选集新用户没有历史行为协同过滤直接失效此时用类型标签做召回是最简单实际的做法。用户勾选几个感兴趣的标签系统返回命中数量最多的电影再按热度加权评分排序。from sklearn.preprocessing import MultiLabelBinarizer # 用清洗好的 genre_list 构建稀疏矩阵 mlb MultiLabelBinarizer() genre_matrix mlb.fit_transform(df[genre_list]) print(genre matrix shape:, genre_matrix.shape) print(genre classes:, mlb.classes_) # 模拟用户选择“剧情”和“悬疑”两个标签 user_tags [剧情, 悬疑] user_vector [1 if g in user_tags else 0 for g in mlb.classes_] # 矩阵乘法算出每部电影命中用户标签的数量 hit_count genre_matrix user_vector df[tag_hit] hit_count # 候选集命中0 且热度足够再按评分排序 min_votes df[votes].median() # 热度门槛用中位数 candidates df[(df[tag_hit] 0) (df[votes] min_votes)] result candidates.sort_values([tag_hit, rating], ascending[False, False]) print(result.head(10)[[name, rating, votes, genres, tag_hit]])逻辑说明MultiLabelBinarizer接收的是“列表的列表”输出二维0/1矩阵行对应每部电影列对应所有出现过的类型。user_vector是手工构造的0/1向量维度必须和genre_matrix列数一致所以用mlb.classes_的顺序去生成不会错位。矩阵乘法的结果是每个电影命中标签的数量整个计算没有循环10万行一瞬间完成。真正值得调的是min_votes这个门槛它防止低热度电影靠一两票高分排在前面中位数只是一个起点按业务场景改成 0.75 分位数或者固定数字都行。4.2 物品相似度没有评分矩阵也能做推荐很多版本的豆瓣数据集只有电影主表没有“用户-电影-评分”这种三列结构矩阵分解类的协同过滤做不了。但基于内容的“物品相似度”依然可以用核心思路是把每部电影转化为特征向量然后计算向量之间的余弦相似度。import numpy as np from sklearn.metrics.pairwise import cosine_similarity # 类型和主演都做One-Hot年份做归一化 cast_mlb MultiLabelBinarizer() cast_matrix cast_mlb.fit_transform(df[cast_list]) years pd.to_numeric(df[year], errorscoerce).fillna(0) year_norm (years - years.min()) / (years.max() - years.min() 1e-6) # 横向拼接成完整特征矩阵 features np.hstack([ genre_matrix, cast_matrix, year_norm.values.reshape(-1, 1), ]) print(feature matrix shape:, features.shape) # 取索引0作为锚点找Top10相似电影 anchor_vec features[0].reshape(1, -1) sims cosine_similarity(anchor_vec, features).flatten() top_idx sims.argsort()[::-1][:11] for i in top_idx: print(df.iloc[i][name], round(sims[i], 4))逻辑说明把类型矩阵和主演矩阵拼接后特征维度会明显变大通常在几千维但绝大多数位置是0cosine_similarity对稀疏矩阵做了优化内存不会爆。拼接顺序可以自己定但每次跑不同的锚点电影相似度结果受拼接顺序的影响很小。坑主要在主演列如果第3章的角色名括号没清干净同一演员可能被拆成两个特征相似度会被稀释导致找出来的“相似电影”明显跑偏。4.3 推荐算法选型的边界条件这里需要有个清醒判断内容推荐什么时候够用什么时候必须上协同过滤。如果目标是做一个可展示的Demo内容推荐基本够用类型命中加评分排序结果直观而且解释成本低。如果目标是做带个性化的产品级推荐必须有用户行为数据光靠电影主表做不了。数据集如果附带评论文本子表可以从评论里抽词向量做基于文本的相似推荐这条路比评分矩阵更稳因为文本不需要用户ID对齐。另一个常见误区是直接用评分列做“物品相似度”比如统计两部电影所有评分的皮尔逊相关。前提是你得先有一张“用户-电影评分”宽表而且评分极其稀疏时相关系数会失真。常见做法是先过滤只对“共同评分人数超过50”的电影对计算相似度否则结果基本是噪声。5. 避坑手册把真实踩过的坑按现象整理给你下面是处理这个数据集时最容易翻车的四个问题每个都按“现象、原因、解决”记录方便直接对号入座。5.1 现象CSV读进来全是乱码现象文件读进来电影名和类型全是乱码或者读取直接报UnicodeDecodeError。有的版本前100行正常后半文件突然出现乱码这种更隐蔽。原因文件实际编码不是UTF-8而是GBK或GB18030。数据集多半是用国内Windows环境下的工具导出的默认编码就是GBK。直接指定encodingutf-8时解码器遇到不兼容的字节就会报错或者解出错误字符前半段正常是因为文件开头碰巧是纯ASCII字段名后半段中文一多就露馅。解决不要赌编码先探测再读取代码见第3章的charset_normalizer用法。或者直接试gb18030它向下兼容GBK和GB2312。还有一个经验用记事本打开正常不代表pandas该用UTF-8记事本有自动编码检测pandas没有。5.2 现象同一部电影出现多行现象按片名去重时发现同一部电影出现两行甚至三行年份相同但评分人数不同有的版本连导演都一样。原因不同时间点导出的数据被合并过同一个片名在数据库里有多个条目部分版本还会把“中国大陆版”和“中国香港版”拆成两行。此时按主键ID去重不可靠因为不同导出版本里ID可能不一样。解决用“片名导演年份”三字段组合去重保留评分人数最多的那一行。如果在意的是一版和二版的差别就先加“地区”字段参与判断两种版本地区不同不应该合并。去重时用subset参数指定列# 组合去重片名导演年份都相同才算重复保留评分人数最多的行 df df.sort_values(votes, ascendingFalse) df df.drop_duplicates(subset[name, director, year], keepfirst)参数说明先按votes降序排序再用drop_duplicates的keepfirst天然留下票数最多的行。如果不排序保留哪一行是不确定的结果不可复现。5.3 现象评分最高分不等于最好片现象按评分排Top100结果前20全是没有听过的小众独立片评分9.5以上但评分人数只有几十人。原因样本量太小评分均值方差巨大。几个5星直接把分数拉到9.8这不代表大众口碑。解决加热度门槛或贝叶斯加权。最简单的做法是第2章的weighted_rating或者直接先过滤再排序# 过滤评分人数低于中位数的电影再按评分排 threshold df[votes].median() top_movies df[df[votes] threshold].sort_values(rating, ascendingFalse) print(top_movies.head(20)[[name, rating, votes]])门槛用中位数而不是固定数字是为了避免换了一个导出版本后阈值失效。如果你想在“过滤”和“加权”之间找一个折中加权分更平滑不会在门槛处出现断层。5.4 现象类型字段汇总统计翻车现象用value_counts统计类型分布发现“剧情爱情”和“剧情/爱情”被统计成两个类型实际是一种电影被拆到了两个格子。原因类型字段分隔符不统一有的行用逗号有的行用斜杠有的用管道符。不统一分隔符就拆分等价于把多标签当成一整串字符串去统计结果类型数量明显膨胀。解决用第3章的normalize_genres先归一化再拆拆完用explode展开后统计# 用 explode 把列表字段展开成一行一值再统计频率 df_exploded df.explode(genre_list) genre_stats df_exploded[genre_list].value_counts() print(genre_stats.head(15))逻辑说明explode是pandas处理列表型字段的标准操作会把一个“类型列表”纵向展开成多行每行保留原电影的其他字段。这样value_counts统计的就是真正的类型频率而不是拼接字符串的频率。展开后的表行数会明显增加比如10万部电影平均每个3个类型展开后就是30万行这个尺寸在内存里依然很轻但如果后续还要关联计算记得及时释放中间变量。6. 让数据集真正跑起来一个能验证的离线Demo数据清洗和推荐方案都验证过后剩下两件事把方案变成一个能对外演示的接口以及验证推荐效果不是随机的。我的建议是一开始别上重型框架用一个小函数加一个简单的接口就能把链路跑通。6.1 最小接口雏形两步调用把前面两套推荐逻辑封装成一个函数输入用户标签输出推荐列表这样后续接Flask或FastAPI都只需要加一层路由不需要改动算法核心。def recommend_by_tags(user_tags, top_n20): 输入标签列表返回推荐电影DataFrame user_vector [1 if g in user_tags else 0 for g in mlb.classes_] hit genre_matrix user_vector temp df.copy() temp[hit] hit temp temp[(temp[hit] 0) (temp[votes] df[votes].median())] temp temp.sort_values([hit, rating], ascending[False, False]) return temp.head(top_n) print(recommend_by_tags([剧情, 悬疑], top_n10)[[name, rating, votes, genres]])参数说明user_tags里的标签必须能在mlb.classes_里找到否则用户向量全为0结果空列表。接口层应当先做一次“标签是否存在”的校验返回可读的提示而不是一个空的DataFrame。6.2 离线稳定性验证改标签看结果变化上线前最值得做的一个验证是“输入扰动测试”。随机改用户标签观察推荐结果变化是否合理。如果换了一个标签后推荐列表几乎不变说明特征区分度不足大概率是类型清洗不彻底或者热度门槛把候选集压得太窄。import random # 随机抽取标签做稳定性检查 sample_tags random.sample(mlb.classes_.tolist(), 5) for tag in sample_tags: rec recommend_by_tags([tag], top_n5) print(tag, -, rec[name].tolist())这个测试看起来简单实际很有用如果某个标签下推荐列表为空那就是该类型标签在清洗后的数据里缺失严重需要回第3章检查genre_list里是不是混入大量无关字符串。如果每个标签都返回相同的前几名那就是热度过滤设置过严或者类型字段取数本身有问题。我的习惯是把这套测试写成脚本每次清洗脚本改动后跑一遍确认改动没有破坏推荐链路。这个数据集第一次跑的时候我被编码和重复数据卡了两天后来发现这些恰恰是中文数据集里最值得学的东西。希望帮到你。本文还有配套的精品资源点击获取