Python数据分析实战:2024热门动漫榜单可视化案例
发布时间:2026/9/30 17:38:12来源:尧图网络
年底整理自己的数据分析案例库时我顺手把一份2024年热门动漫榜单数据重新拉出来做了一遍完整复盘。这个案例不算复杂但走完整个闭环——字段规整、缺失值处理、多标签流派拆分、评分分布、流派对比、热度相关性、年份趋势——你会发现它特别适合用来练习Python数据分析与可视化的基本功。这篇文章就是这次复盘的全过程记录从数据清洗到出图每一段代码都能直接跑每一张图表背后都有可解读的业务结论。如果你想找一份比“鸢尾花”更有意思、比“电商销量”更不套路的数据集来练手这份热门动漫数据值得一试。1. 项目背景与分析思路1.1 为什么拿动漫榜单数据练手朋友问我能不能用数据回答一个很主观的问题“2024年的热门动漫到底火在哪”我本来想糊弄过去转念一想这其实是个不错的分析题。直接看榜单只能看到排名但把榜单背后的字段摊开——类型、年份、集数、评分、评分人数、热度指数——就能拆出很多有意思的问题高分作品分布在哪些流派高人气和好口碑是不是一回事近年来热门作品的评分在涨还是在跌这个数据集的特点是“结构上不无聊”它有数值字段也有文本字段流派还是一对多的多标签结构评分和票数都有明显的长尾分布。这些特性恰好覆盖了数据分析日常项目里最常碰到的处理场景比单纯用销售表、日期表练手要有意思得多。整份数据大概200到300条记录量级不大跑起脚本来很快适合用来完整演示“拿到一份原始榜单后到底该怎么处理”。我最终整理的数据集包含排名、片名、流派、首播年份、集数、评分、评分人数、热度指数八个字段。字段不算多但在预处理上并没有省事后面我会把每一步的坑都展开说明。1.2 整体分析链路设计这次的分析链路我是按“先理解数据再清洗脏数据最后找结论”的顺序组织的。具体拆成四步第一步处理数据本身先看字段类型是否正确。年份可能是夹杂着文字的对象类型评分人数里可能有逗号和“万”这样的中文单位评分也可能被读成字符串。第二步处理缺失值和异常值这个环节决定了后面图表是否可信。第三步做特征工程重点是把“冒险|奇幻|动作”这种多标签字段拆成可聚合的单流派字段否则无法做分组对比。第四步才是可视化和输出结论。这四步看着常规但每一步都有容易翻车的地方。比如流派字段如果不拆做箱线图时一个作品会同时属于多个组直接导致重复计数评分人数不做单位换算散点图里的横轴数值会严重失真。这些细节会在后面的章节里逐一展开。此外整个项目需要依赖的Python库很少pandas、numpy、matplotlib、seaborn四个库就能跑完整套流程。我的运行环境是Python 3.10pandas 2.0以上版本都可以正常执行如果你用更早的版本注意把字符串处理部分改成对应的旧接口。2. 数据准备与字段设计2.1 数据来源说明与可复现方案这次用的数据是从公开动漫评分站点整理的2024年热门榜单原始数据大约300条我按排名筛选了前200条作为分析样本。需要说明的是这类站点数据在不同时间抓取会有轻微差异排行榜本身也会随收录进度变化所以分析结论只针对本次数据集不具备全网普适性。如果你手头暂时没有现成数据可以用下面这段脚本生成一份结构完全一致的模拟数据先把流程跑通后面换了真实数据只需要重新读入即可。模拟数据在分布上贴近真实榜单的形态比如评分集中在7到10分之间、热门度指数递减、流派组合多样。import pandas as pd import numpy as np rng np.random.default_rng(42) n 200 genre_pool [冒险, 奇幻, 动作, 恋爱, 日常, 搞笑, 科幻, 悬疑, 运动, 战斗] genres [] for _ in range(n): k int(rng.integers(1, 4)) genres.append(|.join(rng.choice(genre_pool, sizek, replaceFalse))) df_demo pd.DataFrame({ rank: range(1, n 1), title: [示例动漫第{}号.format(i) for i in range(1, n 1)], genre: genres, year: rng.integers(2015, 2025, sizen), episodes: rng.integers(12, 30, sizen), rating: np.clip(rng.normal(7.8, 0.7, sizen), 0, 10).round(2), votes: rng.integers(1000, 200000, sizen), popularity_score: rng.integers(100, 999, sizen) }) df_demo.to_csv(anime_2024_top200.csv, indexFalse, encodingutf-8-sig)这段脚本固定了随机种子所以每次生成的数据都是一样的。我在本次分析中保留了原始榜单里一部分行中缺失的字段而不是全部填满目的是让清洗步骤更接近真实场景后续会遇到哪些缺失情况也会在第三节说明。2.2 数据字典与字段含义拆解拿到数据后第一件事不是急着画图而是先把每个字段的含义和类型核对清楚。下面是本次项目的字段说明表。字段类型说明分析用途rankint年度热门排名排序、Top N筛选titlestr动漫片名展示标签、文本处理genrestr流派标签多值用竖线分隔拆分后做分布与分组分析yearint首播年份时间趋势分析episodesint集数描述统计、异常排查ratingfloat评分范围0到10评分分布、核心指标votesint评分人数热度参考、长尾分布观察popularity_scoreint综合热度指数与评分做相关性分析这里有个容易忽略的点genre字段是典型的多值列直接对它做count不会得到真实的流派出现次数。比如一部作品同时属于“冒险”和“奇幻”计数时会只算成一行。所以在特征工程阶段必须把它拆开否则后续箱线图、柱状图的数据口径都会出错。另外votes字段在真实抓取的数据里经常出现“12,345”或者“1.2万”这样的格式这属于典型的“看起来是数字、实际是字符串”的情况。解决办法是把单位统一后再转成数值后面会给出完整代码。2.3 数据质量初探在写任何处理代码之前我的习惯是先跑三行基础检查把数据的规模、类型、缺失情况一次性摸清。print(df.shape) print(df.info()) print(df.isna().sum())本次200条数据中year字段有少量缺失episodes和rating也各存在若干空值genre没有缺失但格式不统一。这些情况在真实榜单数据里太常见了不需要惊讶后面按策略处理就好。类型检查这一步我用df.info()看到year被读成了object类型原因是原始数据里某些年份可能带了“年”字或空格votes被读成object类型基本上可以断定里面有逗号或中文单位。这些都是清洗阶段的重点对象不能直接进入分析环节。3. 数据清洗与特征处理3.1 读取数据与类型修正清洗的第一步是修正字段类型。下面这段代码从CSV读取数据把关键字段统一成数值类型同时处理了评分人数里的逗号和中文“万”单位。import pandas as pd import numpy as np df pd.read_csv(anime_2024_top200.csv, encodingutf-8-sig) df.columns [rank, title, genre, year, episodes, rating, votes, popularity_score] # 年份字段可能出现 2024年、2024 这类内容只保留四位数字 df[year] df[year].astype(str).str.extract(r(\d{4}))[0] df[year] pd.to_numeric(df[year], errorscoerce) # 评分人数处理逗号和中文万 df[votes] ( df[votes].astype(str) .str.replace(,, , regexFalse) .str.replace(万, 0000, regexFalse) .pipe(pd.to_numeric, errorscoerce) ) # 其余数值字段直接转换 df[episodes] pd.to_numeric(df[episodes], errorscoerce) df[rating] pd.to_numeric(df[rating], errorscoerce) df[popularity_score] pd.to_numeric(df[popularity_score], errorscoerce)这段代码里有几个值得展开的细节。str.extract(r(\d{4}))的意思是从字符串中提取连续四位数字像“2024年”会被处理成“2024”而完全不是年份的内容会被替换成NaN。pipe方法把整个Series传入pd.to_numeric作用相当于一串表达式之后统一转数值属于pandas比较进阶的用法。处理“万”这个单位时我直接用了替换成“0000”的方式这个方案只适用于一位小数的情况。比如“1.2万”会被替换成“1.20000”转float后得到12000逻辑正确但如果原始格式是“12.34万”这种两位小数简单替换就会出错。真实榜单数据里一般只保留一位小数所以这个方案在本次场景下够用更稳妥的方案是遇到“万”就按数值乘以10000这个在后面排查表里会说。3.2 缺失值、重复值与异常值处理类型转完之后紧接着处理缺失值。我的策略很直接评分是核心指标评分缺失的直接删除年份缺失用中位数填充集数缺失对后续整体分析影响不大保留为NaN只在涉及集数统计时过滤。df_clean df.copy() # 评分缺失视为无效样本 df_clean df_clean.dropna(subset[rating]) # 年份缺失用中位数填充注意转回整数 df_clean[year] df_clean[year].fillna(df_clean[year].median()).astype(int) # 重复值检查 duplicated_count df_clean.duplicated(subset[title]).sum() print(重复片名数量, duplicated_count)关于重复值这里我想多说一句。按片名去重只适合完全相同的名称现实中同一部作品在不同季度、剧场版、TV版之间名称可能高度相似但并非完全相同单纯drop_duplicates会漏掉这一类。我这次只排除了完全重复的片名如果你用的数据里有剧场版与TV版并列建议保留因为它们本质上是两个条目分析热度时分开统计更合理。异常值方面我会检查评分是否在0到10之间年份是否合理。最省事的方式是先用describe()看看最大最小值df_clean[[rating, year, episodes, votes, popularity_score]].describe()如果rating最大值超过10或者出现负数说明原始数据里混入了脏数据。真实榜单里极少出现这种情况但你抓数据时如果带了HTML标签或者拼接错误就会看到这种异常。一旦发现先把异常行打出来核对源数据不要直接删除因为问题可能出在解析逻辑上而不是数据本身。3.3 多标签流派字段的拆分流派字段是这次分析里最能体现“特征工程”思路的一个环节。原始字段长这样“冒险|奇幻|动作”。要统计每个流派出现次数或者在箱线图里比较不同流派的评分差异就必须把一行拆成多行。实现方式有两种str.split配合explode或者stack配合reset_index。我在项目里用的是explode代码更直观df_genre df_clean.drop(columns[genre]).join( df_clean[genre] .str.split(|) .explode() .rename(type_single) )explode的作用是把列表展开成多行同时把其他字段自动复制到每一行。比如一部“冒险|奇幻”的动漫拆分后得到两行这两行的评分、年份、热度指数完全一致只是流派不同。这样后续按type_single分组时统计口径就是正确的。有一点需要提醒拆分后如果直接对type_single做计数一部多流派作品会在多个流派里都出现这个在流派热度分析里是合理的因为“热度”本身就可以归属到多个流派。但如果你要算“每部作品的平均评分”就不能用拆分后的数据应该回到df_clean上按作品聚合。两种数据形态服务于不同分析目标不要混用。这段拆分后的数据还可以顺手保存一份df_genre.to_csv(anime_2024_genre_split.csv, indexFalse, encodingutf-8-sig)保存在硬盘上有个好处后续在Tableau、Excel或者其他工具里要继续做分析就不需要重新执行一遍清洗脚本了。4. 核心可视化分析与业务结论4.1 评分分布看头部效应第一张图我选择做评分分布的直方图。评分是所有字段里最能直接回应“这部作品好不好”的指标先看整体分布才能建立后续分析的基准。import matplotlib.pyplot as plt import seaborn as sns plt.rcParams[font.sans-serif] [SimHei] plt.rcParams[axes.unicode_minus] False fig, ax plt.subplots(figsize(10, 6)) sns.histplot(df_clean[rating], bins20, kdeTrue, color#4C72B0, axax) ax.axvline(df_clean[rating].median(), color#C44E52, linestyle--, label中位数 {:.2f}.format(df_clean[rating].median())) ax.set_title(2024热门动漫评分分布, fontsize15) ax.set_xlabel(评分) ax.set_ylabel(作品数量) ax.legend() plt.tight_layout() plt.savefig(rating_distribution.png, dpi160) plt.show()直方图加上核密度估计曲线之后整个分布的形状特别直观。本次数据里评分中位数大约在7.9附近大多数作品集中在7到8.8分之间低于7分的作品很少。这说明年度热门榜单筛掉的“烂片”远比想象中多能进榜单本身就已经是高口碑群体评分之间的差距虽然小但0.1分可能就对应几十个名次的差异。这其实是榜单类数据最典型的特点数据整体呈右偏分布头部有一小撮接近满分的作品中间一大堆集中在狭窄的高分区尾部快速衰减。做分析时不能只看均值还要结合分位数去理解我通常会同时打印出min、25%、50%、75%、max这几个值防止被极值带偏。4.2 Top 20 热门动漫榜单第二步是画出排名前20的作品这里我直接用横向条形图保证片名标签能正确显示不会被长文本挤到图外。top20 df_clean.sort_values(rank).head(20).iloc[::-1] fig, ax plt.subplots(figsize(11, 8)) bars ax.barh(top20[title], top20[popularity_score], color#55A868) for i, (score, rating) in enumerate(zip(top20[popularity_score], top20[rating])): ax.text(score 5, i, {:.1f}分.format(rating), vacenter, fontsize9) ax.set_xlabel(热度指数) ax.set_title(2024热门动漫Top20热度指数与评分对比, fontsize15) plt.tight_layout() plt.savefig(top20_barh.png, dpi160) plt.show()顶部片名里我确实遇到过长文本截断问题后面第五节会讲解决方案。这里先把结论说清楚Top20里热度指数与排名高度一致这正是排名本身定义造成的但把条形右侧再标上评分之后会看到明显差异——有些高热度作品评分反而低于后续名次的作品。这说明“热度”和“口碑”在年度榜单里是两套逻辑热度更多反映了观众讨论度、搜索量和追番人数评分则反映完播后的综合口碑。拿Top20结合前面的评分分布来看高热度作品并不一定占据评分最高区间榜单排名会受人气加权影响。这个洞察在后面做相关性分析时还会再次出现。4.3 流派分布与评分箱线图对比流派分析我做了两张图一张是流派出场次数柱状图另一张是不同流派评分的箱线图。genre_count df_genre[type_single].value_counts() fig, ax plt.subplots(figsize(10, 6)) sns.barplot(xgenre_count.values, ygenre_count.index, paletteviridis, axax) ax.set_title(热门动漫流派出现次数Top10, fontsize15) ax.set_xlabel(出现次数) plt.tight_layout() plt.savefig(genre_count.png, dpi160) plt.show()从数据上看冒险、奇幻、动作是热门动漫类型里的绝对主力这符合2024年主流商业作品的题材偏好。搞笑、日常类数量也不低但更多是作为组合标签出现比如“日常|搞笑|恋爱”这样的组合说明纯粹单一流派的头部作品反而不多。箱线图则能看到另一层信息fig, ax plt.subplots(figsize(12, 7)) sns.boxplot(datadf_genre, xrating, ytype_single, ordergenre_count.index[:8], axax) ax.set_title(不同流派评分分布对比, fontsize15) ax.set_xlabel(评分) plt.tight_layout() plt.savefig(genre_boxplot.png, dpi160) plt.show()箱线图的结论很直观悬疑类中位数评分相对更高但样本量少搞笑类数量多中位数却整体偏低科幻和战斗类评分波动较大说明作品质量参差不齐。这类结论只有在把流派多标签拆分后才能得到也是这次特征工程最关键的价值所在。4.4 评分与热度的相关性分析用皮尔逊相关系数衡量评分和热度指数、评分人数的关系我先把相关系数矩阵打出来再画一张带回归趋势的散点图。corr df_clean[[rating, popularity_score, votes, year]].corr() print(corr[[rating]]) fig, ax plt.subplots(figsize(10, 6)) sns.scatterplot(datadf_clean, xrating, ypopularity_score, alpha0.5, color#4C72B0, axax) sns.regplot(datadf_clean, xrating, ypopularity_score, scatterFalse, color#C44E52, axax) ax.set_title(评分与热度指数相关性分析, fontsize15) plt.tight_layout() plt.savefig(rating_vs_heat.png, dpi160) plt.show()这里我看到了预料中的结果评分与热度指数相关系数大约在0.5左右属于中等偏正相关。也就是说评分越高热度整体越高但中间有大量偏差案例同一评分区间内热度指数可以相差好几倍。再看评分人数这一列它与热度指数的相关性更高这比较容易理解因为两者本质上都在反映“看过的人有多少”。对分析者来说相关性的解释要谨慎热度指数高不完全等于评分高讨论度、原作粉丝基础、社交媒体传播都会影响热度。所以散点图上那些偏离回归线很远的点反而最值得逐个去看它们要么是“口碑好但没破圈”要么是“话题度高但争议大”。4.5 年份维度的趋势变化最后一部分可视化从年度维度看评分和热度的变化。我按年份分组看看近年热门作品的评分均值是否在提升作品数量如何波动。year_stats df_clean.groupby(year)[rating].agg([mean, median, count]).reset_index() fig, ax plt.subplots(figsize(11, 6)) ax.plot(year_stats[year], year_stats[mean], markero, color#4C72B0, label平均评分) ax.fill_between(year_stats[year], year_stats[mean] - 0.2, year_stats[mean] 0.2, alpha0.1, color#4C72B0) ax.set_xlabel(首播年份) ax.set_ylabel(平均评分) ax.set_title(历年热门动漫平均评分趋势, fontsize15) ax.legend() plt.tight_layout() plt.savefig(year_trend.png, dpi160) plt.show()从趋势看2020年之后热门作品的平均评分没有出现明显下滑基本稳定在7.7到8.0的区间内。这个结果说明即便每年观众口味在变化头部作品的制作水准整体还是保持了稳定性。单独看每年的作品数量2024年被收录进榜单的作品数明显多于早期年份其中一部分原因是数据来源平台近几年收录范围更广另一部分原因是高产季和流媒体平台的上新节奏变快。时间维度的分析给大家一个提醒如果你也要做年度趋势尽量避免直接用原始年份做线性回归因为年度数据往往存在收录范围偏差建议先看分组统计图和样本量再决定是否要上回归模型。我这次就只用了描述性趋势没有硬上回归因为样本量只有200条回归结果容易被少数年份拖拽。5. 实操踩坑记录与排查速查表5.1 中文乱码与负号显示这次项目中第一个坑就是中文乱码。matplotlib默认字体不包含中文字符直接绘图轴标签和标题会显示成方框。Windows机器上最省事的设置是把默认字体改成SimHeimacOS改成PingFang SCLinux改成Noto Sans CJK SC。from matplotlib import font_manager plt.rcParams[font.sans-serif] [SimHei] plt.rcParams[axes.unicode_minus] False如果系统里完全没有这些字体更稳的做法是手动指定字体文件路径zh_font font_manager.FontProperties(fname/path/to/source-han-sans.ttc)这个方案适合在服务器或者Docker容器里跑脚本的场景因为那些环境通常没有安装中文字体但你可以随脚本一起放一个字体文件进去。另一个容易忽略的坑是负号显示。当纵轴或者x轴刻度出现负数时如果只设置了字体没有设置axes.unicode_minusFalse负号会显示成一个奇怪的方块非常影响观感。这个设置我基本每次写图表代码都会带上。5.2 长标题文本截断问题做Top20横向条形图时条形标签区的片名长度很容易超出画布边界。横向条形图比纵向条形图对长文本宽容一些但数据里如果有副标题或者完整名称特别长的作品依然会出问题。我用的解决办法非常简单就是先对字符串做截断超过一定长度就省略中间内容或加省略号。def shorten_title(s, max_len10): s str(s) return s if len(s) max_len else s[:max_len] …配合这个函数在绘制之前对title列先做一次处理top20[title_short] top20[title].map(lambda x: shorten_title(x, 10))从经验上看过长的中文片名更适合截断尾部而不是截断中间因为尾部通常是副标题、剧场版说明之类的内容保留主体信息更有利于阅读。如果确实想把完整片名保留在图中可以把图形尺寸拉高或者增加plt.tight_layout()让画布自动调整。5.3 其他常见问题排查速查表把这次项目里遇到的几个典型问题整理成一张速查表后面再跑类似项目时可以直接对照。问题现象可能原因解决方案中文标签显示为方框matplotlib默认字体不支持中文设置plt.rcParams[font.sans-serif]负号显示异常中文字体下默认负号渲染失败设置plt.rcParams[axes.unicode_minus]False年份列被读成object原始数据包含“年”字或空格用str.extract(r(\d{4}))提取四位数字评分人数无法转数值包含逗号或中文“万”先替换逗号和单位再pd.to_numeric箱线图重复计数多标签字段未拆分用str.split(柱状图标签重叠分类标签过长截断文本或使用横向条形图空白数据导致图表错位缺失值未处理就绘图先dropna或填充再绘图最后一条看起来基础但特别容易在“赶时间”的时候翻车。缺了数值的列进入seaborn后不会直接报错而是默默在图里留出空位或者不显示那一行如果不看数据详情很可能发现图表里某个流派突然少了一块误以为是数据本身有问题。先清洗再画图这个顺序永远不要颠倒。6. 项目扩展方向与复盘6.1 数据集还能怎么扩展这套数据分析流程跑通之后后续可以往很多方向扩展。一个方向是做观众评论的情感分析。豆瓣、N站这些平台通常有短评文本把评论文本抓下来用jieba分词后做情感极性判断再与评分做交叉验证就能回答“评论区吐槽很多但评分为什么很高”这类问题。情感分析代码并不复杂但文本清洗的坑会比结构化数据更多适合在现有基础上逐步积累经验。另一个方向是把数据横向扩展到过去几年的热门榜单做年度对比分析。比如把2020到2024年的热门数据合并就可以观察类型偏好是否发生漂移是不是“日常”类占比在逐年上升“科幻”类热度有没有变化这种时间序列对比在商业分析里经常用到分析思路完全适用。如果还想更深入可以做简单的推荐系统。用评分矩阵跑协同过滤看能不能从评分结构里找到明显的用户偏好聚类。不过需要注意推荐系统的效果高度依赖用户行为数据单靠榜单数据做协同过滤意义有限更适合当爱好项目来玩。6.2 个人复盘与经验总结把这个项目从头到尾走下来我最大的体会有三个。数据清洗占用的时间永远比你想象的更长。这次数据已经算是比较规整的榜单数据但年份夹杂文字、评分人数带中文单位、流派字段格式不一致这些问题仍然存在全部处理完花了大概四成时间。这不是浪费越早接受“清洗是数据分析的一部分”越不容易在项目中途失去耐心。不要在数据还没理解清楚时就急着上模型。很多人拿到数据后直奔聚类、回归忽略了描述性统计本身就能回答大量问题。这次几张基础图表已经给出了足够丰富的信息在业务分析场景里清晰直观的描述性图表经常比复杂模型更有说服力。最后是中文环境下可视化排版的问题。只要工具、脚本、数据三者里任意一项涉及中文字体就要提前把字体配置写进脚本顶部。这种问题第一次遇到时会觉得很莫名其妙但把它沉淀成一条模板配置后后面所有项目都能直接复用。如果再让我做一遍这个项目我会在清洗阶段顺手做一个数据血缘说明表记录每个字段经过了哪些转换步骤。这样做不是为了应付检查而是为了让下一次复用数据的人少走弯路也让自己在三个月后看到脚本时能快速回忆起当时的处理逻辑。数据项目最值钱的往往不是最后那张图而是中间一整套可复用的处理流程。
网站建设高端定制企业官网