TMDB Top300 电影榜单可视化报表设计思路------一块画布上的四宫格
写在前面
学完折线图、柱状图、饼图,到了第十章实战:基于 movies.csv 里的 TMDB Top300 数据,在一张画布上搞个 2×2 四宫格,给业务部门做份数据报表。
这篇记录一下我是怎么从零把这张报表"想"出来又"敲"出来的。
1. 图表选型与画布骨架
动手前先定图型,四个维度性质不同,配图得对症下药:
-
时间趋势(每年上映数量) → 折线图
-
分类对比(语言、类型) → 柱状图
-
占比构成(评分分布) → 饼图
骨架部分,直接用 plt.subplots() 切出四个独立的坐标系,后续画图互不干扰:
import pandas as pd
import matplotlib.pyplot as plt
配置中文显示,解决负号乱码
plt.rcParams'font.sans-serif' = 'SimHei'
plt.rcParams'axes.unicode_minus' = False
创建 2×2 子图画布,解开成四个对象方便单点操作
fig, axes = plt.subplots(nrows=2, ncols=2, figsize=(14, 10), dpi=100)
ax1, ax2 = axes0 # 第一行左右
ax3, ax4 = axes1 # 第二行左右
movies = pd.read_csv("data/movies.csv")
2. 数据清洗:能用数据救的,绝不轻易 dropna
"年份"列有缺失,最省事的是直接 dropna(),但这会让那一年的统计数据失真。既然有"上映时间",年份完全是可以截取出来的。我选择补全:
截取"上映时间"前4位作为年份,格式不对的给 NaN(不报错)
year_from_date = pd.to_numeric(movies"上映时间".astype(str).str:4, errors="coerce")
只填补缺失值,原有的正确年份不动
movies"年份" = movies"年份".fillna(year_from_date)
两步校验:先查异常(<0或>3000),再查缺失是否清零
print("异常年份数:", len(movies(movies\["年份" < 0) | (movies"年份" > 3000)]))
print("填充后缺失数:", movies"年份".isnull().sum())
验证通过才转 int,否则 NaN 转 int 会报错
movies"年份" = movies"年份".astype(int)
3. 折线图:连续年份 + 补零,专治"断线"
groupby 统计有一个坑:某一年没电影上榜,那年就不会出现在索引里。直接画图,折线会"跳接"。
解决办法是强行把 X 轴和 Y 轴拉齐:X 轴用 range 生成完整年份序列,Y 轴去查字典,查不到直接补 0。
year_counts = movies.groupby("年份")"电影名".count()
min_year, max_year = year_counts.index.min(), year_counts.index.max()
x_years = list(range(min_year, max_year + 1)) # 连续X轴
y_counts = year_counts.get(year, 0) for year in x_years # 没数据补0
ax1.plot(x_years, y_counts, color="green")
ax1.set_title("每一年上映电影数量变化", fontsize=14)
ax1.set_xticks(range(min_year, max_year + 1, 8)) # 每隔8年标一个,防拥挤
ax1.set_ylim(0, 30)
ax1.grid(True, linestyle="--", alpha=0.5)
4. 柱状图:排序与"拆行"魔法
语言图 :核心是让业务一眼看明白谁排第一,所以加上 sort_values 降序,并用 bar_label 把数字标在柱顶。网格只开横向(axis="y"),免得竖线穿过柱子辣眼睛。
lang_counts = movies.groupby("语言")"电影名".count().sort_values(ascending=False)
ax2.bar(lang_counts.index, lang_counts.values, color="skyblue", width=0.6)
ax2.bar_label(ax2.containers0)
ax2.grid(True, axis="y", linestyle="--", alpha=0.5)
类型图 :一部电影可能有多个类型(如"剧情,犯罪")。如果直接 groupby,它会变成一个独立类型,统计就废了。
用 str.split(",").explode() 把一行炸成多行,一部电影有几个类型就计入几次,最后 value_counts 统计。
genre_series = movies"类型".str.split(",").explode().str.strip()
genre_counts = genre_series.value_counts()
ax3.bar(genre_counts.index, genre_counts.values, color="orange", width=0.6)
ax3.bar_label(ax3.containers0)
ax3.tick_params(axis="x", rotation=45) # 标签倾斜防重叠
ax3.grid(True, axis="y", linestyle="--", alpha=0.5)
5. 饼图:统计与展示分离
"评分"列存的是 87、86,实际代表 8.7、8.6。如果直接用原始值当标签会误导读者。
所以分组用原始整数做键(干净),生成标签时再除以 10 还原,顺手用 int(score) 消除类型检查器的警告。
rating_counts = movies.groupby("评分")"电影名".count()
rating_labels = \[\]
for score in rating_counts.index:
display_score = int(score) / 10
rating_labels.append(f"{display_score:.1f}")
ax4.pie(rating_counts.values, labels=rating_labels, autopct="%1.1f%%", startangle=90)
ax4.set_title("不同电影评分占比统计", fontsize=14)
6. 收尾与布局防重叠
加总标题时最怕跟子图标题打架,tight_layout 的 rect 参数派上用场,把图区压缩到画布下方 94%,给上方留出物理空间。
fig.suptitle("TMDB Top300 电影榜单数据统计", fontsize=20, fontweight="bold", y=0.98)
rect 给总标题预留空间,且必须传元组,传列表会报警告
plt.tight_layout(rect=(0, 0, 1, 0.94))
plt.show()
7. 回头看:沉淀的通用流水线
这个四宫格写完,其实摸出了一套通用的数据分析流水线:
读数据 → 查缺失 → 补全验证 → 按口径统计(多值先拆) → 补齐坐标轴 → 选图绘制 → 局部美化 → 布局收尾。
以后换个数据集(比如销售、天气),变的只是列名和具体业务口径,这套流水线直接套用就行。画图本身是小事,怎么把脏数据洗干净、怎么把统计口径搞对 ,才是报表能不能用的关键。