简介:IMDb完整数据集是一份面向自然语言处理与深度学习入门者的经典语料资源,主要解决情感分析、文本分类与词向量训练等场景中缺少标准基准数据的问题,适合高校学生、算法工程师以及准备NLP相关竞赛的开发者使用。数据源自互联网电影资料库的海量影评,整理为两个高度规整的文件:NPZ格式数据文件以NumPy二进制形式完整存储已分好训练集与测试集的文本序列及其标签,无需额外解析即可直接加载进主流深度学习框架进行模型训练与评估;JSON格式索引文件提供单词到索引的完整映射,帮助开发者免去原始英文分词、清洗与编码的繁琐步骤,快速将任意影评文本转换为模型可接收的数字张量。整个压缩包仅17.26MB,结构清晰、开箱即用,既适合本地离线练习,也便于在资源受限的教学或实验环境中演示。目前已有4717人学习下载,尤其适合课程设计、论文复现或竞赛练手时快速搭建基线模型。
1. 这份IMDb完整数据集,npz与json两种形态解决什么
一开始拿到IMDb原始数据的人,十有八九会被它的文件结构劝退:title.basics.tsv.gz、title.ratings.tsv.gz、name.basics.tsv.gz散落一堆,每个文件几十GB的文本要自己清洗、对齐、过滤。某开发者整理这份“imdb完整数据集”时,把它压缩成了npz加json两个文件,npz管数值矩阵、json管嵌套元数据,一次性解决“加载慢、字段太散、清洗重复劳动”这三个痛点。适合做电影推荐、影视数据分析、NLP剧情摘要实验的从业者,也适合需要快速跑通Demo、不想在数据准备上耗一周的人。下面直接拆文件结构和用法。
2. 数据内容与格式选型:为什么是npz和json,而不是原始TSV
2.1 这份资源里有什么:文件清单与字段对照
完整数据集按内容分成两大部分。npz文件保存的是可计算的数值矩阵,json文件保存的是带嵌套结构的元数据。拆开来看,文件组织大致是这个逻辑:
| 文件形态 | 主要内容 | 典型字段 | 适合做的操作 |
|---|---|---|---|
| npz | 数值矩阵 | average_rating、num_votes、start_year、runtime_minutes、genre_onehot | 相似度计算、分布统计、矩阵运算 |
| json | 元数据与嵌套结构 | tconst、primary_title、genres、plot、known_for_titles | 查询详情、文本生成、前端展示 |
| npz | 关系矩阵 | cast_indices、director_indices | 图分析、二分网络建模 |
很多拿到手的人第一反应是“为什么不是csv”。答案在于数据规模。原始IMDb数据集的tsv.gz版本,解压后是几十个GB的纯文本,每一行有一个tconst作为主键,不同文件靠主键join。csv格式看起来方便,当你真的去load一个几GB的文件,内存占用会直接爆炸。npz是numpy原生的压缩存储格式,numpy一行代码读回内存,不用自己写解析器。json则保留那种“一部电影对应多个别名、多个genres”的嵌套结构,这在csv里必须拆成多张表才能表达。
2.2 npz和json的分工:矩阵进numpy,嵌套进字典
npz和json的分工,核心一条原则:矩阵能做的事,不要塞进json;嵌套结构和文本,不要硬摊平进表。
json适合存这些内容。一部电影的plot是一段英文文本,可能还带标点和换行;一个tconst可能对应多个akas(多个国家/地区的发行别名);一部电影经keyed关系的演员数量不定,存成list比强行拆列要理解得多。json慢,但胜在读取整段语义结构。npz则适合存那些可以行列对位的数值:每部电影在genre上的one-hot编码,每部电影的年份、时长、评分均值和票数,这些体育干净利落切成二维数组形态。矩阵的好处是numpy底层是连续内存块,切片运算快;遇到需要算cosine similarity、Jaccard相似度、做分布直方图的场景,直接拿数组切行列就完事了。
我一般拿到数据先跑一个print(data.files)看npz里有哪些键,再看json的顶层key。这样做的好处是立刻知道这个json组织中心到底是“按电影id”还是“按演员id”,后面所有查询都依赖于这个锚点。这套数据里的json是主键锚定在tconst上的——也就是每部电影一个条目,演员信息在内部用list引用。后续写代码时,不要把一个电影的元数据拆到多个json顶层key上,否则后面数据对齐会让你哭。
2.3 直接读原始tsv和读这份处理好的数据,差距在哪
并不是说原始数据不好,但你不是每次都缺一个“从零处理”的机会。原始tsv每次都需要几个典型操作:
# 解压原始数据集(常见做法) gunzip -k title.basics.tsv.gz wc -l title.basics.tsv解压后你面对的是制表符分隔的中文到英文混杂文本,缺失值以\N形式出现,年份可能为空,runtime可能为空,adult字段是“0/1”字符串。要把这些变成可供numpy计算的数字,等于自己写一遍ETL。这份资源已经把\N统一替换成npz里的-1,把start_year从字符串转成了int16数组,把gens one-hot编码成了float32矩阵。也就是说,你拿到的数据已经过了“数据对齐”这一关。
提示:npz里出现的
-1全部是原始tsv中缺失值的映射。在过滤数据时,注意不要让-1混进你的计算里。
原始tsv的坑还在于文件体积和编码,尤其name.basics.tsv.gz里有人名、职业、known_for_titles三列,known_for_titles里是逗号分隔的多个tconst。如果要从里面抽“演员A和演员B合作过哪些电影”,需要自己拆分-join-聚合三个步骤。在npz版里,cast关系矩阵已经建好,你需要的就是两个索引:一个把tconst映射成整数行号,一个从矩阵取关系。这才是这份资源真正的价值:把时间花在分析上,而不是花在数据准备上。
3. 环境配置与读取:把数据装进内存
3.1 最小依赖:numpy、json和标准库就够了
这套数据的运行环境比你想象的要朴素。不需要Spark,不需要数据库,甚至不需要pandas也能跑起来。纯Python环境,装一个numpy就能完成数据读取。json是Python标准库的一部分,直接import就行。强烈建议使用Python 3.8以上的版本,因为下面的读取方式会用到一些类型提示和numpy的新特性,旧版本不是不行,但没必要在环境上踩坑。
pip install numpy装完numpy,先把两个文件放到同一目录。然后写一个读取脚本,把两个文件同时load进来。这个操作在后面的所有章节都会反复用到,建议直接复制保存成一个load_imdb.py,后面每次跑实验前import load_imdb即可。
3.2 读取NPZ:认识懒加载特性
import numpy as np # 加载npz文件 npz = np.load('imdb_movie.npz', allow_pickle=True) # 查看npz内部包含哪些键 print(npz.files) # 取出评分数组 ratings = npz['average_rating'] # 取出票数数组 votes = npz['num_votes'] # 取出年份数组 years = npz['start_year'] print(ratings.shape, ratings.dtype) print(ratings[:5])这段代码有几个关键点需要说明。np.load默认读取的是压缩后的NPZ格式,它内部是一个包含多个键的映射,跟Python dict一样用键来获取数组。注意npz是懒加载的,也就是说npz = np.load(...)这句执行时,它只是打开文件建立索引,并没有把所有数组都读进内存。真正读数据是当你在npz['average_rating']这一行执行的时候才发生的。
这就是一个隐藏的坑:如果你在一个函数里load了npz,然后return了那个数组对象,过一会儿再用,有可能会出现文件句柄已关闭的报错。解决方法是获取数组后立刻调用.copy(),让数据脱离npz文件的控制。这个细节我后面避坑章还会重点讲。
参数上,allow_pickle=True非常重要。如果npz里有object类型数组(通常出现在元数据编码里),不带这个参数会直接报错。如果文件里存的都是纯数值数组,去掉也没事,但保险起见建议每次都带着。
3.3 读取JSON:把嵌套结构摊平成可用形态
import json # 加载json文件 with open('imdb_meta.json', 'r', encoding='utf-8') as f: meta = json.load(f) # meta是一个dict,键是tconst print(type(meta)) print(len(meta)) # 取出一个样例看看结构 one_movie_id = list(meta.keys())[0] sample = meta[one_movie_id] print(sample.keys()) print(sample['primary_title'], sample['start_year']) # 查看plot的文本长度分布 plot_lengths = [len(v.get('plot', '')) for v in meta.values()] print(min(plot_lengths), max(plot_lengths))json文件的读取原理很简单,json.load把整个文件解析成Python字典。这里有一个体积问题:这个json文件如果很大,一次性load进内存会占用可观的空间。所以读取时用with open(...)确保文件句柄及时关闭。
注意代码里的v.get('plot', '')——有些电影没有对应的剧情摘要,如果直接v['plot']会抛KeyError,用get返回空字符串更安全。这之后你会发现很多电影的plot长度是0,这些就是要在后续词云、文本实验里过滤掉的。
如果你觉得字典嵌套难操作,可以用pandas把它摊平成DataFrame:
import pandas as pd df = pd.DataFrame.from_dict(meta, orient='index') print(df.columns) print(df[['primary_title', 'start_year', 'runtime_minutes']].head())orient='index'的意思是:meta这个dict的每个key(也就是tconst)变成DataFrame的索引,value里的每个字段变成列。这一步会直接展开一层嵌套,但注意genres这种list类型的字段展开后还是list对象,不会自动拆成多列,后续做one-hot还要自己处理。
提示:json和npz加载完成后,建议做一个对齐验证:
print(npz['ids'][:5]),再交叉验证它和list(meta.keys())[:5]顺序是否一致。如果id列表顺序和矩阵行顺序错位,后面一切分析都是错的。
4. 应用实战:推荐系统与文本热度场景怎么用
4.1 从npz切出电影相似度:类型和评分一起算
拿到这份资源的第一个典型诉求是做“找相似电影”功能。常见做法是:先从npz里取出genre_onehot矩阵,用Jaccard相似度算类型层面的重叠,再叠加评分的距离作为加权项。code如下:
import numpy as np from numpy.linalg import norm npz = np.load('imdb_movie.npz', allow_pickle=True) genres = npz['genre_onehot'] # shape: (n_movies, n_genres) ratings = npz['average_rating'] # shape: (n_movies,) # 内部索引到tconst的映射 id_list = npz['ids'] def jaccard_similarity(a, b): inter = np.sum((a > 0) & (b > 0)) union = np.sum((a > 0) | (b > 0)) return inter / union if union > 0 else 0.0 # 找一个电影的top10相似 target_idx = 0 sim_scores = [] for i in range(len(genres)): if i == target_idx: continue sim = jaccard_similarity(genres[target_idx], genres[i]) # 评分差越小则加权越高 rating_close = 1.0 / (1.0 + abs(ratings[target_idx] - ratings[i])) combined = 0.7 * sim + 0.3 * rating_close sim_scores.append((i, combined)) sim_scores.sort(key=lambda x: x[1], reverse=True) top10 = sim_scores[:10] print([id_list[i] for i, _ in top10])逻辑说明:genre_onehot是二维数组,每一行代表一部电影在哪些类型上有值。Jaccard相似度计算两个集合交并比例——交集大说明类型重叠多,数值落在0到1之间。rating_close用倒数函数把评分差映射到接近1的值,用来惩罚“类型相同但口碑差距过大”的组合。最后用0.7和0.3做加权,前一个权重是类型相似度,后一个是评分接近度。
调参建议:如果你只关心类型匹配,可以把评分权重降到0.1甚至去掉。如果你发现推荐结果同质化严重,则可以加大评分权重。target_idx换成任意一部电影的内部行号即可,这就是npz比json好用的地方——矩阵直接索引,不用通过字典查key再取字段。
4.2 用json里的剧情摘要做词云图
另一个高频场景是从Json的plot字段挖热点词。英文剧情摘要里大量出现“他们”、“那个”之类的停用词,直接丢进词云会产生一堆废词。所以要先做正则清洗和停用词过滤。
import json import re from collections import Counter with open('imdb_meta.json', 'r', encoding='utf-8') as f: meta = json.load(f) stopwords = {'the', 'a', 'an', 'and', 'or', 'but', 'of', 'to', 'is', 'it', 'in', 'with', 'as', 'for', 'his', 'her', 'their', 'they'} counter = Counter() for movie_id, info in meta.items(): plot = info.get('plot', '') # 去掉标点,统一小写 words = re.findall(r"[a-zA-Z']+", plot.lower()) for w in words: if w not in stopwords and len(w) > 3: counter[w] += 1 print(counter.most_common(20))逻辑说明:re.findall(r"[a-zA-Z']+", plot.lower())会把文本切割成单词列表,同时去掉标点。stopwords集合过滤掉高频虚词。len(w) > 3去掉缩写和短词,经过这个过滤后统计出来的词大概率是“love、family、death、war、friendship”这类有实际含义的词。
这个场景下json的价值体现出来了:npz里没有文本字段,json里才有完整摘要。如果你直接拿原始tsv里的剧情摘要,需要处理换行和制表符转义,而这份json已经清洗过了。做词云时如果想保持genre区分,可以先按类型过滤,再对单一类型的电影摘要做词频统计。
4.3 数据下采样:保证分布不漂移
当你要从几十万部电影里抽1万部做可视化,直接np.random.choice等于给自己埋雷。样本里冷门电影、老电影的比例会跟全集差很多。正确做法是分层抽样。
import numpy as np npz = np.load('imdb_movie.npz', allow_pickle=True) years = npz['start_year'] valid_mask = years > 0 # 按年代分箱 bins = np.array([1920, 1940, 1960, 1980, 2000, 2020, 2030]) year_bucket = np.digitize(years, bins) # 只抽有效样本 valid_indices = np.where(valid_mask)[0] bucket = year_bucket[valid_indices] rng = np.random.default_rng(42) sample_ratio = 0.05 sampled_idx = [] for b in np.unique(bucket): idx_in_bucket = valid_indices[bucket == b] n = max(1, int(len(idx_in_bucket) * sample_ratio)) sampled_idx.extend(rng.choice(idx_in_bucket, n, replace=False)) sampled_idx = np.array(sampled_idx) print(sampled_idx.shape)逻辑说明:np.digitize把年份划分到对应的年代桶里,比如1945年落在“1940-1960”这个桶。然后每个年代分别按5%的比例抽样,保证1920年代的电影和2000年代的电影在样本里的相对比例与全集一致。rng = np.random.default_rng(42)设固定种子,保证实验结果可复现——这是做对比实验的基本要求。
坑点说明:只看years > 0还不够,有些电影年份字段是0(原始tsv里以\N呈现),np.digitize会把0归到第一个桶[-inf, 1920),导致1920年代的比例被虚高的“无效年份”污染。所以必须先做valid_mask过滤。如果你后续要用下采样数据做训练集和测试集,建议把这句话的种子换成你实验记录里的值,否则团队其他人复现不了你的结果。
5. 避坑指南:清洗阶段最常见的五个翻车点
5.1 年份字段全是-1:缺失值被统一替换带来的误判
现象:你分析各年代电影数量,画出来的直方图在“-1”这个位置突然冒出一个高得不正常的柱子。
原因:原始tsv里,startYear的缺失值写作\N。这份npz把\N统一替换成了-1。但问题在于,不是所有电影都有“缺失”这种状态,其中大量视频类条目(短剧、视频片段)根本没有正式上映年份。如果不过滤,这堆-1会占到全数据集很大的比例,直接影响计算中位数、平均数等统计结果。
解决:任何涉及年份的操作,先做years = npz['start_year']; valid = years > 0。同理,runtime_minutes字段的有效范围应该大于10小于400,超出这个区间的直接剔除或单独标记。
5.2 评分0.0是真实值吗?先看票数再下结论
现象:你按评分排序找“最好看的电影”,结果排在最前面的几部全是评分9.9以上但票数只有个位数的冷门条目。
原因:IMDb的平均评分是加权来的,但这份npz保留了原始average_rating字段,票数只有1票也可能得出高分。在数据分析场景,这不是真实可信的“高口碑”,而是噪声。更隐蔽的是,某些条目在原始数据里评分字段也是\N,转出来变成了0.0,跟真实评分最低1.0形成断层。
解决:每次按评分做排序或筛选,必须加一个票数下限。常见做法是过滤num_votes < 1000的条目,再按评分排。如果你做推荐系统,直接用评分不筛票数,冷门条目会被大量推到用户面前,直观感受就是“推荐的什么怪东西”。
5.3 npz加载后数组丢了:懒加载的坑
现象:你在一个函数里写了data = np.load('imdb_movie.npz', allow_pickle=True),然后把这个函数返回的数组存下来,等程序跑一会儿再拿这个数组做计算,报错提示“file is not open for reading”或直接段错误。
原因:npz是懒加载的,np.load返回的是一个NpzFile对象,它内部维护了一个文件句柄。你获取的数组只是这个文件对象的“视图”,不是独立的内存副本。一旦NpzFile被垃圾回收或文件句柄被关闭(比如函数返回后局部变量销毁),视图就失效了。
解决:任何要从npz里取出并长期使用的数组,加一步.copy()。
data = np.load('imdb_movie.npz', allow_pickle=True) ratings = data['average_rating'].copy() votes = data['num_votes'].copy()这个坑非常隐蔽,因为它不是必现——在短小的脚本里,函数执行完数组马上被使用,文件句柄还没关闭,一切正常。一旦放到长时间运行的Flask服务里,错误才出现。所以我把这条排在前三,它是我个人遇到过的“坑王”。
5.4 读取json出现乱码:永远用utf-8-sig
现象:用json.load(open('imdb_meta.json'))读取后,发现每一个key前面多了一个奇怪的\ufeff字符,导致所有按tconst查数据的代码全部返回空。
原因:某些工具导出json时会在文件开头写入UTF-8 BOM头(字节顺序标记)。Python的open()函数默认用utf-8编码读取,它不认识BOM,于是把它当作一个普通字符带进了第一行的开头。这个字符肉眼难察觉,但用==比对时永远不相等。
解决:读取时显式指定encoding='utf-8-sig'。这个编码会告诉Python自动去掉开头的BOM标记。注意,这个坑只影响文件开头的第一个key,后面的key都是好的,但恰好你第一个查的key就是最常用的那个tconst,于是整个逻辑崩盘。
import json with open('imdb_meta.json', 'r', encoding='utf-8-sig') as f: meta = json.load(f)5.5 下采样后冷门年代全消失了:随机抽样与分层抽样
现象:我用np.random.choice(ids, size=10000)抽了1万部电影做可视化,结果发现1920年代的电影一部都没被抽到,1930年代也只有两条。整个图上出现一段空白,看起来像是数据处理有bug。
原因:全集里1920年代的电影数量本来就很少,假设总共有800部,1万/总样本数这个比例算下来,1920年代一个都抽不到是概率上完全正常的。单纯随机抽样不保证每个子集都有代表。
解决:参考4.3的分层抽样代码,按年代分桶后再在每个桶里抽样。做推荐系统时,还要额外注意按票数分桶——把高票热门片和低票冷门片分开抽,这样训练集的覆盖度才够,否则模型只见过热门片,冷门电影的向量一直训不好。
6. 进阶技巧:给数据做一个O(1)的“ID→元信息”查询接口
json是一个字典,按tconst查元信息本身是O(1)。但当你频繁按某个字段反查,比如“找出所有类型是Drama且年份在2010年之后的电影”,每次都遍历整个dict,1万次查询就是1万次全量遍历,性能非常差。这时我会用这套数据的id体系做一个内存侧索引,让常用查询变成哈希查找。
import numpy as np npz = np.load('imdb_movie.npz', allow_pickle=True) ids = npz['ids'] # 内部行号到tconst的映射 # 建立 tconst -> 内部行号 的反向索引 id_to_index = {tconst: i for i, tconst in enumerate(ids)} # 建立一个按genre筛选的倒排索引 meta = {} # 实际使用时加载json genre_index = {} for idx, tconst in enumerate(ids): for g in meta.get(tconst, {}).get('genres', []): genre_index.setdefault(g, []).append(idx) # 查询Drama电影的idx列表,O(1) drama_indices = genre_index.get('Drama', []) print(len(drama_indices))逻辑说明:id_to_index和genre_index是预先构建的两个索引。前者把字符串tconst映射成整数行号,方便去npz里取向量;后者把genre(类型)映射成idx列表,查某类型下的所有电影不用再全量for循环。这就是用空间换时间。设计索引时要考虑查询模式:如果90%的需求是按类型查,只建genre索引就好;如果还需要按年份查,就再加一个年份桶。
这个技巧适合数据量到达百万级之后复用。我从那以后每次拿到npz+json组合的数据,都会先花半小时想清楚业务侧到底要按哪些键做高频查询,然后把索引一次性建好,而不是在业务代码里反复全表扫描。数据准备阶段多做的这几分钟,能省下后面跑实验的无数个小时。希望帮到你。
本文还有配套的精品资源,点击获取