简介面向Python数据分析与可视化学习者及影视行业数据从业者这份资源围绕影视数据全链路处理展开从数据清洗、特征工程到深度学习建模再到漏斗图、饼图、柱状图等可视化呈现并利用FCN提取特征、LTSM完成情感分类与票房预测是一套可运行、结构完整的项目方案。压缩包共273个文件涵盖63个py脚本、38个html页面、36个pyc编译文件、33张jpg图片及js/css前端资源另有csv/xls数据表、h5/pb模型权重、fasttext模型和bin标准化文件等整体约120.38MB目录按功能模块划分便于定位源码、模型、数据与网页展示内容。目前已有569人学习下载适合需要参考完整影视数据分析源码、理解深度学习模型应用或想直接复用可视化模板的读者。资源中除核心代码外还附带了训练好的模型文件、词向量与归一化参数可帮助快速复现预测流程并减少重复调参成本。1. Python影视数据分析可视化难的不是画图是让数据先“站得住”“Python影视数据分析可视化”这句话听上去像一个画图项目真正动手之后才会发现至少一半时间花在数据清洗和口径确认上画图反而是最不起眼的那一步。很多新人拿着某评分网站的公开榜单 CSV第一步就跑sns.histplot结果要么中文变方块要么年份列里混着“1994 再版”这种脏值要么评分同时存在 8.6/10 和 72/100 两套体系每种图都要先回答同一个问题——这一列的数值到底能不能直接用于计算。这篇文章就围绕一套最常见的影视分析链路展开拿到 CSV 后怎么体检、怎么清洗拆解、怎么定分析口径再到三张核心图的必调参数最后给几条高频避坑记录和验证方法。适合正在做数据分析练习、作品集或者想快速完成影视榜单复盘的读者也适合已经跑通过一次流程、但被边界情况卡住的人。2. 数据准备与字段体检拿到影视数据先别急着画图2.1 字段清单一份可用的影视数据至少需要哪几列先定义“可用”。做影视数据可视化常见做法是用一份含多部影片信息的表格而不是单部电影的详情页。至少要包含这几类字段标题、上映年份、评分、评分人数、类型、地区、导演/主演、片长。标题用于去重和人工核对年份和评分是大多数图表的基础轴评分人数是权重依据——一部只有 300 人评分的 9.2 分电影和一部有 50 万人评分的 8.8 分电影不能直接用同一个口径比高下。类型和地区是多值字段后面需要拆开才够分析导演/主演在分析“是谁在稳定产出高分内容”时会用到平时可以留着不做拆解。我一般拿到数据后第一时间先看列名清单把不需要的列直接放一边不要盲目 drop因为后面核对时你很可能还要翻回来看原始值。实际项目里看到最多的版本是title, alias, year, rating, votes, duration, genres, regions, director, actors。alias 往往是英文名或别名用来辅助去重duration 有时是字符串“105 分钟”要清洗regions 可能是“中国大陆 / 美国”这种多地区组合。字段没有绝对标准但只要是“一行为一部影片”的宽表后续所有分析和可视化都能用同一套 Pandas 流程这是最省事的数据结构。如果你拿到的是一份长表一行为一个类型标签后面分析时反而要先把宽表构造出来多一道手续。所以第一步不是写分析代码而是先跟数据源确认行语义看是“一影一行”还是“一关一列”。字段角色常见脏数据建议处理方式title去重、核对中英文混排、别名多保留原值另建 title_cleanyear时间轴“1994 (再版)” “1994 (中国大陆)”正则提取四位数年份rating核心指标“8.6/10” “86/100” “8.6 分”抽取数字后统一分制votes权重字符串带逗号去逗号后转 floatgenres多维下钻“剧情, 爱情, 动画”split exploderegions多维下钻“中国大陆 / 美国”split explodeduration辅助分析“105 分钟”正则提取数字表里这些字段不一定能一次拿齐。缺 votes 时年份趋势图就少了一个加权维度结论的可靠度要打折扣缺 genres 时类型偏好分析只能直接砍掉。所以拿到数据先做的不是补全而是评估“缺了哪块、哪些结论不能做”避免拍脑袋画图。2.2 用 read_csv 完成导入与第一轮字段体检拿到 CSV 后我先不指定任何全量转换先把所有字段读成字符串让清洗逻辑完全可控。这里给一套常用导入参数它能避免不少隐蔽报错。import pandas as pd df pd.read_csv( movies.csv, encodingutf-8, dtypestr, # 先全量按字符串读入 keep_default_naFalse, # 不要把“暂无”“-”自动替你换成 NaN ) print(df.shape) print(df.info())dtypestr 的作用是推迟类型判断因为原始数据里年份可能是“(1994)”评分可能是“8.6/10”直接让 Pandas 推断只会得到 object 或 float64 的混合值等到你 groupby 时才会暴露问题。keep_default_naFalse 配合后面的 replace 清洗可以保住“暂无评分”这类文本否则 Pandas 会默认把空字符串、NA、null 等统一视作缺失最后你根本分不清“缺失”是因为数据源没写还是因为本来就没有。跑完df.info()后我还会做三件事df.isna().sum()看缺失分布df.nunique()看每个字段的取值种类df.sample(5).T随机抽 5 行转置后肉眼扫一遍。这一步是给整份数据做“安检”省得写到一半才发现某列 70% 都是空值前面的聚合全部白算。字段体检不看均值不看最大值重点看“空值占比”和“取值种类”。如果 title 的 nunique 和行数相近说明几乎没有重名如果 year 的 nunique 只有 12 个说明数据源本身就是按季度或年代整理的时间粒度不够不适合做年份趋势。2.3 统一年份口径上映年、首播年、还是再版年影视数据最容易出问题的字段就是年份。常见来源里年份会出现三种形态纯数字 1994标题括号里的“(1994)”“1994 (中国大陆)”、“1994 再版”这类带后缀的脏文本。另外评分网站为了收录重映、DVD 发行年份列有时会被覆盖成最新发行年而与首映年不一致。这个属于数据口径问题代码层面没有万能解只能靠外部知识判断。我自己的处理方法是把“原始年份文本”先存一列留底再用正则抽出一个标准字段df[year_raw] df[year] df[year] df[year].astype(str).str.extract(r(19\d{2}|20\d{2})) df[year] pd.to_numeric(df[year], errorscoerce) print(df[year_raw].tail(5)) print(df[year].tail(5))这里str.extract返回第一个匹配到的四位数年份好处是完全不依赖第几组括号也不会被“1994 再版”这种后缀干扰pd.to_numeric(errorscoerce)会把没匹配到的值转成 float 类型的 NaN而不是直接抛异常。留year_raw的原因非常实际当你发现 1990 年前后的影片数量异常多想排查是不是数据源把某个地区的发行年混进来了year_raw能让你一眼看到原因不用重新读一遍文件。年份标准化之后还需要看一眼df[year].describe()如果最小年份早于 1895 或出现 2099 这种异常值基本可以判定正则或数据本身有问题先处理异常再进入下一章。3. 清洗与维度拆解把类型、地区、评分改造成能下钻的字段3.1 基础清洗三步去重、空值、脏文本在开始任何聚合前我一般先做三轮清洗。第一轮去重drop_duplicates(subset[title, year])因为同一部影片如果有不同引进版本标题相同但年份不同不能直接按 title 去重。第二轮处理空值前面用keep_default_naFalse读进来的空值都变成了空字符串需要先统一替换成 pd.NA再针对关键字段做dropna。第三轮清洗脏文本主要是把“8.6分”“105分钟”这类带单位的字段里的数字抽出来。用代码表示df df.replace(, pd.NA) df df.drop_duplicates(subset[title, year], keepfirst) df df.dropna(subset[title, rating, year]) df[duration_min] ( df[duration] .astype(str) .str.extract(r(\d)) .astype(float) )drop_duplicates的 keepfirst 会保留第一次出现的行前提是数据源本身按热度或评分排过序如果源文件顺序不固定建议先按评分人数排序再去重保留“看起来更重要”的那一版记录。duration_min同理用正则提取数字这里不直接处理单位是因为后续分析里我们只关心数值。清洗到这一步最直观的验收指标是df.info()里各行非空数量是否一致如果 title、year、rating 三列仍有明显缺口先回头查数据源而不是写代码硬补。3.2 多值字段拆解类型和地区不拆会骗你影视数据的类型列长这样“剧情, 爱情, 动画”。如果直接对它value_counts()你会得到成百上千个“组合标签”每个组合只出现少量次数根本看不出“剧情类整体评分高还是低”。常见做法是把多值字段拆成一行一个值用 Pandas 的 explode 机制。这一步会让 DataFrame 行数变多但每个分析维度都变得可下钻。拆解代码如下df[genres] df[genres].str.split(,) df_genre df.explode(genres) df_region df.explode(regions) df_region[genres] df_region[genres].str.strip() df_region[regions] df_region[regions].str.strip() print(df_region.shape) print(df_genre[genres].value_counts().head(10))这里要特别强调str.strip()的重要性。“剧情, 爱情”在 split(,) 之后会得到带前导空格的“ 爱情”如果不去空格后面 groupby(genres) 会把“爱情”和“ 爱情”当成两个不同类别图上就会出现两条几乎重合的柱子这是非常常见的翻车点。explode 之后的行数会膨胀到原来的一倍多属于正常现象如果你只想保留“主类型”更稳妥的办法不是取第一个值而是先拆开、跑完频次统计后把每部影片拆分后计数最高的类型作为主类型。这样更贴近真实分类逻辑尤其是“剧情/喜剧/动画”这种类型并列的片子。3.3 评分标准化8.6/10 和 86/100 混在同一列怎么处理评分列是影视数据里第二容易出问题的字段。常见形态有四种纯小数 8.6带分母的“8.6/10”百分制的“86/100”带标签的“8.6 分”。如果不清洗前两种能被 astype(float) 处理第三种会被读成字符串第四种会在聚合时报错。统一做法是先抽数字再判定分制最后折叠到 10 分制。我惯用的方案是把这个逻辑单独封装成一个函数避免每次读不同 CSV 都重复踩同一个坑。def standardize_rating(series: pd.Series) - pd.Series: s series.astype(str) nums s.str.extract(r(\d{1,3}(?:\.\d)?))[0].astype(float) denom s.str.extract(r/(\d{1,3}))[0].astype(float) # 如果分母大部分是100说明数据源按百分制记录统一除以10 if (denom 100).mean() 0.3: nums nums / 10 return nums.round(1) df[rating_num] standardize_rating(df[rating]) print(df[rating_num].describe())函数里先看分母列如果超过三成的记录分母是 100就认为整列是百分制按百分制折叠否则默认已经是 10 分制。参数说明里有个细节值得注意mean() 0.3这个阈值不能拍脑袋我一般会先打印denom.value_counts()看看真实分布再定。有的数据源只有部分条目带分母其余是纯数字这时阈值判断会比无脑判断更稳。判断完分制后还应该再看一眼rating_num.describe()如果最小值小于 1 或最大值超过 10基本可以判定是抽取正则写错了先修正则再继续不要带着脏值往下做。4. 指标设计与分析计算评分分布、年份趋势与类型偏好的正确口径4.1 评分分布偏态才是正常甩尾你才要怀疑评分分布是影视分析里出现率最高的一张图。很多教材第一步就是画直方图但很少解释“分布长什么样才可信”。以 10 分制为例主流评分网站的用户打分习惯是 6 分以下极少7.59.2 之间密集堆积所以直方图会呈现明显的偏态。看到这种偏态不要恐慌它不是数据错误反而说明数据源是有区分度的。真正要怀疑的是两类情况一是在 8.6、9.0 这类整值处出现断层高峰可能是评分被四舍五入了二是 46 分区间占比极高说明数据源里混入了打分系统不同的条目比如某些地区用户的打分习惯以 5 分为中位数。分析之前过一遍分位数比过一遍均值更重要。我一般会算三个数rate_col df[rating_num] print(rate_col.quantile([0.1, 0.25, 0.5, 0.75, 0.9])) print(rate_col.mean())如果 10 分位到 90 分位只差不到 1.5 分说明这列高分内容非常拥挤后面做“高分片单筛选”的阈值要更谨慎不能靠均值切一刀要结合 90 分位。如果差到 3 分以上反而要去核对是不是把不同分制混在一起了。分位数表还有一个用途帮你给count 20这类样本量阈值找参照。当你看到 90 分位以上片子占比明显稀疏说明头部片单确实稀缺图表里那条均值线的解释力也有限要明确标注出来。4.2 年份趋势不能只画按年份平均分还要看样本量年份趋势图最常见的陷阱是只按年份算平均分然后用一条折线穿起来。问题是早期年份的影片数可能只有几条一条冷门高分片就能把那年刷到 9.3看起来像“黄金年代”其实只是样本太少。我处理这个问题的标准姿势是同时聚合均值与计数然后过滤样本量不足的年份再画图yearly df.groupby(year).agg( avg_rating(rating_num, mean), count(rating_num, size), ) yearly yearly[yearly[count] 20] yearly[avg_5y] yearly[avg_rating].rolling(5, min_periods1).mean() print(yearly.head())这里rolling(5, min_periods1)做了一个简单平滑让趋势线不会被单年波动带得上下乱跳。参数 min_periods1 表示窗口内只有一年数据也参与计算目的是让最早期年份不至于出现空值如果要更严格可把 min_periods 设为 3窗口不足三年的年份就不画值这样趋势图开头会缺一段但更符合统计直觉。过滤条件count 20本身没有绝对标准整个数据源只有一万部条片时设 20 是合理的如果数据源有 30 万条我一般会提到 50。思路是先看 count 的分位再定阈值不要照抄数字。4.3 类型偏好组合最多的类型不等于口碑最好的类型做类型分析时会碰到的追问是“到底拍哪种类型更卖座 / 更容易拿高分”这里有两个维度一个是各类型的平均评分一个是各类型的出现频次。只画频次会发现剧情、喜剧永远霸榜因为这类标签最常用信息量不大。只画平均分又会看到“纪录片、音乐、传记”这类相对冷门的类型冲到前五因为选这几类标签的片子本来就少样本量太小。我建议把两个维度放同一张表里然后给平均分设定最小样本量下限genre_stats ( df_genre.groupby(genres)[rating_num] .agg([mean, count, std]) .query(count 30) .sort_values(mean, ascendingFalse) ) print(genre_stats.head(10).to_string())query(count 30)就是那个“及格线”它的位置放在聚合之后、排序之前顺序很重要。如果你先 sort 再看 head会把只有 5 部 9.4 分的“冷门类型”错当头部加了样本量下限头部才真正代表“大批量稳定高分”。std列也有用处标准差越大说明该类型内部口碑分化越严重即使均值高也不代表随便挑一部该类型片都好看。类型标签本身是并集关系一部动画片也可能同时标成剧情片所以这种聚合结果更适合说明“类型标签口碑”而不是“影片类型归属”措辞上要小心。如果想看“哪个年代 哪个类型组合的评价高”用交叉表更合适。交叉表既不要求把多值拆干净又可以同时下钻两个维度注意先把年份离散成年代区间避免透视表列数过多df[decade] (df[year] // 10) * 10 df[genres_top] ( df_genre[df_genre[genres].notna()] .groupby(title)[genres] .agg(lambda s: s.value_counts().idxmax()) ) result pd.crosstab( df[decade], df[genres_top], valuesdf[rating_num], aggfuncmean, marginsTrue, ).round(2) print(result.head())这段代码里genres_top用value_counts().idxmax()提取每部影片拆分后出现次数最多的类型作为主类型比直接取第一个字段更能反映标签权重。透视表的marginsTrue会多出“全部”行列方便对比每个年代的整体平均值。crosstab 中如果某个年代-类型组合没有电影默认值是 NaN出图时 seaborn 的 heatmap 会留白这比填 0 更诚实因为缺失不代表评分低而是没有样本。5. 可视化必调参数与高频避坑三种核心图不改参数必翻车5.1 三个必调参数中文字体、画布尺寸、坐标轴刻度先说不改必翻车的三个参数。第一个是中文字体。Matplotlib 默认字体里一般没有中文字形直接plt.title(评分分布)画出来的是一堆方框控制台还会刷Glyph missing警告。我在脚本开头统一设置一次而不是每次画图前重复设置import matplotlib.pyplot as plt import seaborn as sns plt.rcParams[font.sans-serif] [Microsoft YaHei, SimHei] plt.rcParams[axes.unicode_minus] False sns.set_theme(stylewhitegrid, fontMicrosoft YaHei, font_scale1.1)axes.unicode_minusFalse是第二个必调参数它解决的是坐标轴负号显示成方框的问题记住它和中文字体要成对出现。sns.set_theme的font参数缺一不可但它依赖系统已安装字体的名字Linux 服务器上如果没有对应中文字体这行会静默失效画完才发现。第三种最容易踩的坑是年代轴刻度年份跨度长时默认刻度可能只显示 1960、1980、2000间距不规整。这时候重新设置刻度和旋转角度即可fig, ax plt.subplots(figsize(10, 5)) ax.set_xticks(range(1960, 2025, 5)) plt.xticks(rotation45)figsize是画布尺寸单位是英寸。横向趋势图我会给 10x5 到 12x6直方图保持接近正方形更舒服。rotation45不应盲目使用扁平的二维热力图不需要旋转而带长年份轴的折线图几乎都需要它否则年份标签互相重叠成一片墨水。还有一点常被忽略fig, ax plt.subplots()创建的 axes 对象如果后续用sns.histplot(axax)往里画不要再用plt.xxx设置坐标轴两种接口混用会导致某些参数不生效。5.2 分布图与趋势图同一条数据不同的参数心态分布图我惯用sns.histplot它的默认分箱数在很多场景下偏保守长尾会被拉扁需要手动指定 bins 或 binwidth。核密度曲线kdeTrue在样本量不足 30 时会画出离谱的长尾小数据源里建议关掉只保留直方柱大样本几百部以上开 kde 反而更能看出偏态。给分布图加均值线是低成本高收益的操作sns.histplot( datadf, xrating_num, bins20, kdeTrue, color#c0392b, ) plt.axvline(df[rating_num].mean(), ls--, lw1.2, color#2c3e50)ls--是虚线的常用写法lw 控制线宽。均值线不一定要放在平均值上也可以换成中位数。当平均分和中位数差距超过 0.2 分时分布其实左右不对称直接说“均分 7.9”会误导人看图上两条线的相对位置更直观。趋势图则相反重点不在线宽而在于“有没有把有缺失的年份空出来”。sns.lineplot遇到缺失年份会自动连线跨越视觉上好像那几年一直有数据这时候查一下trend[count] 0的年份或者按年份做普通点线图缺失处自动断开。这两个细节分别决定分布图的可读性和趋势图的诚实度比配色更重要。5.3 高频避坑四条现象、原因、解决第一条汉字变方块。现象是图能出来标题和坐标轴标签全变成空心小方块。原因是rcParams里指定的字体在系统里不存在或者设置发生在pyplot第一次绘图之后没有生效太多。解决方法是在 import 之后、第一次画图之前设置Linux 下先执行fc-list :langzh查可用字体名把中文字体名完整复制进rcParams。代码排错时经常看到“设置了 SimHei 还乱码”的帖子绝大多数是系统里根本没有这个字体文件。第二条pd.to_numeric后年份出现大片 NaN。现象是年份轴缺了一截或透视表行名出现“1994 (中国大陆)”。原因是原始年份列里混入括号和后缀直接astype(int)失败。解决方法用str.extract(r(19\d{2}|20\d{2}))抽出首现四位数年份再转数字。如果抽完仍然很多 NaN再把year_raw打印出来检查是否存在非公元纪年数据源不要急着把缺失填成 0。第三条sns.set_theme设置了 whitegrid但折线图背景还是白板。现象是代码不报错网格就是不出现。原因是新版 seaborn 的某些坐标图主题需要显式调用ax.grid(True)才会显示次要网格或者你的图表是subplots创建的对象主题对 Axes 级别的实例约束较弱。解决方法是在绘图函数后补一行ax.grid(True, alpha0.3)不要依赖全局主题。第四条保存的 PNG 里标题和图例被裁掉。现象是屏幕显示正常保存下来的图片四边残缺。原因是plt.tight_layout()只对当前 figure 的布局做优化保存时若没有把图例和标题的边界算进去边缘会被截掉。解决方法是保存时加bbox_inchestight和dpi200plt.savefig(rating_dist.png, dpi200, bbox_inchestight)bbox_inchestight 会把画布收缩到恰好容纳所有可见元素适合出报告用dpi 决定清晰度打印用 300屏幕上展示 150 足够不要为了“高清”盲目上 600文件体积会成倍增长加载也慢。6. 进阶验证与一个偷懒技巧证明你的结论不是画出来的6.1 三类验证方法第一类是人肉抽样比对用df.sample(5, random_state42)抽出记录回到原始榜单页核对标题、年份、评分。重点是年份因为提取正则很难发现“数据源把发行年当成了上映年”如果你抽出的 5 条里有 2 条年份对不上说明清洗阶段的口径判断错了要回炉而不是继续出图。第二类是窗宽变化测试把第 4 章的count 20改成 20、50、100 三档各跑一次年份均值趋势如果折线形态变化小说明结论稳定变化大则说明结论被少数样本支配。第三类是分组复核把数据按年代切两段分别重算类型偏好 Top5只有一个时代才会出现的冠军类型基本属于小样本噪声。这三步做完分析结论才算“能拿出来见人”。6.2 一个偷懒技巧与我的习惯偷懒技巧是先用 pandas 内置绘图完成快速预览再决定要不要走 seaborn 精细路线。比如想看评分和年份的散点关系一行df.plot.scatter(xyear, yrating_num, alpha0.3)就够一个成熟方案值得把这一行换成sns.scatterplot并补上均值线和回归线。这个习惯能让你在探索阶段不浪费时间调样式把精力留给真正需要精细呈现的最终图。我的长期习惯是保留所有_raw列随时能顺着rating_raw追查一条异常数据每次拿到新数据源先跑一版集成的describe()再谈可视化。之前在一个模拟项目X里我整整花了一晚才发现那年趋势图走低是因为一批条目把发行年份混进了 year 字段从那以后“年份到底是上映年还是发行年”成了我拿到影视数据首先要确认的问题。希望你也能把这些检查项放进自己的流水线省掉重复踩坑的时间希望帮到你。本文还有配套的精品资源点击获取