Python实战:用数据建模拆解同人CP热度之争
之前在浏览同人创作平台时发现一个很有趣的现象围绕《鬼灭之刃》中“义炭”与“义忍”哪对更好磕不同粉丝群体之间经常争论不休。有人觉得角色互动细节决定一切有人觉得剧情铺垫才是关键。作为一个写代码的人我第一反应不是参与争论而是想这种 CP 热度之争能不能用数据说话与其在评论区“对线”不如写一个 Python 分析脚本把同人作品的数量、收藏、评论、更新频率等维度全部量化再做一套可复现的热度评估模型。本文就围绕这个思路完整拆解从数据采集、文本清洗、分词统计到热度指数建模和可视化呈现的全过程。无论你是 Python 初学者还是想了解如何用数据分析思路拆解文化现象都可以跟着实操一遍。1. CP 热度之争从一个“吵架话题”到数据分析问题1.1 粉丝争论的本质是“主观感受”但热度可以被量化在《鬼灭之刃》的同人圈里“义炭”富冈义勇 × 灶门炭治郎和“义忍”富冈义勇 × 蝴蝶忍都是非常有人气的配对。“义炭”强调师徒之间的信任与守护“义忍”则侧重同期之间的默契与遗憾。粉丝各执一词核心问题在于哪一方的创作生态更活跃这里的“更活跃”其实是可以拆解的同人作品的数量多不多单部作品的收藏、评论、推荐是否稳定平台上该 CP 标签下的创作者是否持续产出相关关键词在讨论区的出现频次如何。所以与其争论“哪对更好磕”不如把“热度”拆成一组可计算的特征。本文的做法是从公开的同人创作平台收集与 CP 相关的文本数据用 Python 做分词、统计、建模最后生成一个排行结果。1.2 分析思路与整体流程整个项目分成四个模块数据准备构造或采集包含 CP 关键词的文本数据。文本清洗与分词统计使用 jieba 对中文文本分词统计关键词出现频率。热度指数建模从作品数、收藏数、评论数、更新活跃度四个维度计算热度得分。可视化呈现用 pyecharts 生成对比图表形成可以直接阅读的分析报告。流程图可以用一句话概括原始文本 → 清洗 → 分词 → 关键词频次统计 → 结合多维度指标 → 热度指数 → 可视化。2. 环境准备与版本说明2.1 开发环境本文示例基于以下环境操作系统Windows 10 / 11 或 macOSLinux 也完全兼容Python 版本3.9 及以上开发工具VS Code 或 PyCharm 均可终端工具Windows CMD / PowerShellmacOS / Linux Terminal。需要说明的是版本不需要完全一致只要 Python 主版本在 3.9 以上代码基本可以正常运行。2.2 依赖库安装需要安装以下 Python 库pandas用于数据处理和聚合jieba用于中文分词wordcloud用于生成词云图matplotlib用于基础绘图pyecharts用于生成交互式图表openpyxl用于读写 Excel 文件pandas 读写 Excel 时需要。安装命令如下pip install pandas jieba wordcloud matplotlib pyecharts openpyxl如果网络环境较慢可以指定国内镜像源pip install pandas jieba wordcloud matplotlib pyecharts openpyxl -i https://pypi.tuna.tsinghua.edu.cn/simple2.3 项目目录结构建议按照下面的结构组织项目cp_analysis/ ├── data/ │ ├── raw_texts.csv # 原始文本数据 │ └── works_data.csv # 作品维度数据 ├── output/ │ ├── wordcloud.png # 词云输出 │ ├── heat_bar.html # 热度对比柱状图 │ └── radar_chart.html # 雷达图 ├── main.py # 主脚本 ├── text_analysis.py # 文本分析模块 └── heat_model.py # 热度指数模块3. 数据准备构造一份可用的同人文本数据集3.1 数据采集的合规边界在开始写代码前必须先说明数据来源问题。公开的同人创作平台如 Lofter、AO3 等通常有自己的服务条款直接编写爬虫脚本批量抓取内容可能违反平台规则。因此本文采用两种方式获取数据使用公开的数据集或平台导出的数据手工构造一份有代表性的演示数据集用于教学演示。如果你想针对真实平台做分析建议优先确认目标平台是否提供官方 API、是否允许数据导出、是否需要申请权限。在未获得授权前不要对平台发起高频请求也不要抓取用户的私密信息。3.2 构造示例数据集为了让读者能独立复现我构造了一份包含 200 条文本记录的演示数据字段包括id记录编号text同人作品简介或讨论文本cp_tag作品所属的 CP 标签favorites收藏数comments评论数update_time最近更新时间。生成示例数据的代码如下# 文件路径data/make_demo_data.py import pandas as pd import random import datetime random.seed(42) cp_names [义炭, 义忍, 炭香, 锖义] templates [ 今天又看了一遍{cp}的文感觉两个人的互动真的太有张力了。, 谁懂啊{cp}这段剧情我反复看了十遍细节满满。, 深夜补档{cp}的粮怎么吃都吃不够。, 整理了一下{cp}的时间线从初遇到并肩作战每一帧都好磕。, 我只想说一句{cp}就是真的不接受反驳。, 新坑预告写一篇{cp}的现代paro预计周五更新。, 为什么{cp}的产出这么少这不科学, 回坑了还是觉得{cp}最戳我。, 从原作细节来看{cp}的关系发展真的很自然。, 今日份的{cp}摸鱼图画到一半卡住了。, ] rows [] for i in range(1, 201): cp random.choice(cp_names) text random.choice(templates).format(cpcp) favorites random.randint(0, 5000) comments random.randint(0, 800) update_days random.randint(1, 90) update_time datetime.date.today() - datetime.timedelta(daysupdate_days) rows.append({ id: i, text: text, cp_tag: cp, favorites: favorites, comments: comments, update_time: update_time.isoformat(), }) df pd.DataFrame(rows) df.to_csv(data/raw_texts.csv, indexFalse, encodingutf-8-sig) print(df.head())运行这段脚本后会在data目录下生成raw_texts.csv文件里面有 200 条演示记录。3.3 查看数据结构import pandas as pd df pd.read_csv(data/raw_texts.csv, encodingutf-8-sig) print(df.info()) print(df.head())预期可以看到类似输出RangeIndex: 200 entries, 0 to 199 Data columns (total 6 columns):4. 文本清洗与关键词频次统计4.1 为什么要做文本清洗同人作品的简介、讨论文本常常包含一些不影响分析目标的内容比如标点符号、网址、表情符号等。分词前不做清洗会导致统计结果中混入噪音词。清洗步骤包括统一移除中英文标点去除多余空白保留中文字符、英文字母和数字将文本中的 CP 关键词统一格式比如把“义碳”这类错别字纠正为“义炭”。4.2 jieba 分词的基本用法jieba 是常用的中文分词库支持精确模式、全模式和搜索引擎模式。这里我们使用精确模式。import jieba text 义勇和炭治郎的互动真的太有张力了。 words jieba.lcut(text) print(words)输出大致是[义勇, 和, 炭治郎, 的, 互动, 真的, 太, 有, 张力, 了, 。]可以看到jieba 会根据内置词典切分词。对于“义炭”“义忍”这类简称形式的 CP 名可能不会被正确切分。这时需要把自定义词加入 jieba 词典。4.3 加载自定义词典import jieba # 将 CP 名称加入自定义词典 jieba.add_word(义炭) jieba.add_word(义忍) jieba.add_word(炭香) jieba.add_word(锖义)如果词表比较多也可以单独维护一个词典文件custom_dict.txt然后通过jieba.load_userdict加载jieba.load_userdict(data/custom_dict.txt)词典文件每一行的格式为词语 词频可省略 词性可省略。例如义炭 10 n 义忍 10 n 炭香 10 n 锖义 10 n4.4 清洗与分词代码实现接下来编写一个完整的文本分析模块text_analysis.py# 文件路径text_analysis.py import re import pandas as pd import jieba from collections import Counter # 需要统计的 CP 关键词 CP_KEYWORDS [义炭, 义忍, 炭香, 锖义] def clean_text(text: str) - str: 清洗文本去标点、去空白、保留中文/英文/数字 if not isinstance(text, str): text str(text) # 去除 URL text re.sub(rhttps?://\S, , text) # 移除非中文、非英文、非数字字符 text re.sub(r[^\u4e00-\u9fa5A-Za-z0-9], , text) return text.strip() def load_custom_dict(): 加载自定义词典 for word in CP_KEYWORDS: jieba.add_word(word) def count_keywords(texts: list) - Counter: 统计每个 CP 关键词在文本中出现的总次数 counter Counter() for text in texts: cleaned clean_text(text) words jieba.lcut(cleaned) for w in words: if w in CP_KEYWORDS: counter[w] 1 return counter if __name__ __main__: load_custom_dict() df pd.read_csv(data/raw_texts.csv, encodingutf-8-sig) counter count_keywords(df[text].tolist()) for cp in CP_KEYWORDS: print(f{cp}: {counter.get(cp, 0)} 次)运行结果类似义炭: 52 次 义忍: 61 次 炭香: 42 次 锖义: 45 次这里的次数表示在所有文本中该关键词出现的总频次。可以看到虽然每条文本只有一个核心 CP 标签但通过分词可以进一步确认各配对在讨论内容中的出现密度。4.5 生成词云图为了更直观地展示关键词的分布可以用 wordcloud 生成词云图。注意中文字体需要指定系统字体文件否则会出现方框。Windows 常见字体路径是C:\Windows\Fonts\msyh.ttcmacOS 常见路径是/System/Library/Fonts/PingFang.ttc。# 文件路径make_wordcloud.py import jieba import pandas as pd from collections import Counter from wordcloud import WordCloud import matplotlib.pyplot as plt # 加载自定义词典 jieba.add_word(义炭) jieba.add_word(义忍) jieba.add_word(炭香) jieba.add_word(锖义) df pd.read_csv(data/raw_texts.csv, encodingutf-8-sig) all_text .join(df[text].tolist()) words jieba.lcut(all_text) # 过滤掉长度为1的字符只保留有实际含义的词语 filtered_words [w for w in words if len(w) 1] word_freq Counter(filtered_words) font_path C:/Windows/Fonts/msyh.ttc # 根据实际系统修改 wc WordCloud( font_pathfont_path, width800, height600, background_colorwhite, max_words100, ).generate_from_frequencies(word_freq) plt.figure(figsize(10, 8)) plt.imshow(wc, interpolationbilinear) plt.axis(off) plt.savefig(output/wordcloud.png, dpi300) print(词云已保存到 output/wordcloud.png)词云图中出现频率越高的词显示越大一眼就能看出哪些 CP 名称和关联词在文本中占主导。5. CP 热度指数建模把多维度指标变成一个可比分数5.1 建模思路关键词频次只能反映“讨论热度”还不能全面衡量一个 CP 的综合热度。为了更完整引入四个指标作品数量work_count该 CP 标签下的同人作品数平均收藏数avg_favorites作品受欢迎程度的均值平均评论数avg_comments读者互动程度活跃度active_score最近更新频率用最近更新时间距离当前日期的倒数来衡量。这四个指标量纲不同需要先做归一化再按权重合成一个综合热度指数heat_score w1 * norm(work_count) w2 * norm(avg_favorites) w3 * norm(avg_comments) w4 * norm(active_score)5.2 构造作品维度数据演示数据中我们只有文本记录没有按 CP 聚合的作品信息。可以先从文本数据中聚合出基础统计值并额外补充作品数字段# 文件路径make_works_data.py import pandas as pd import numpy as np df pd.read_csv(data/raw_texts.csv, encodingutf-8-sig) work_count_map { 义炭: 320, 义忍: 280, 炭香: 195, 锖义: 210, } rows [] for cp, group in df.groupby(cp_tag): avg_fav group[favorites].mean() avg_cmt group[comments].mean() # 活跃度最近一次更新的时间距离今天越近活跃度越高 update_date pd.to_datetime(group[update_time]) recent_days (pd.Timestamp.today() - update_date).dt.days active_score 1.0 / (recent_days.mean() 1) rows.append({ cp_tag: cp, work_count: work_count_map.get(cp, 100), avg_favorites: round(avg_fav, 2), avg_comments: round(avg_cmt, 2), active_score: round(active_score, 4), }) works_df pd.DataFrame(rows) works_df.to_csv(data/works_data.csv, indexFalse, encodingutf-8-sig) print(works_df)需要说明的是这里的work_count_map是演示用的虚构作品数量实际项目中应替换为平台统计值。5.3 归一化与加权计算常用的归一化方法是 Min-Max 归一化公式为x_norm (x - min(x)) / (max(x) - min(x))下面是完整的heat_model.py# 文件路径heat_model.py import pandas as pd import numpy as np def min_max_normalize(series: pd.Series) - pd.Series: Min-Max 归一化 min_val series.min() max_val series.max() if max_val min_val: return pd.Series([1.0] * len(series), indexseries.index) return (series - min_val) / (max_val - min_val) def compute_heat_score(df: pd.DataFrame, weights: dict None) - pd.DataFrame: 计算综合热度指数 if weights is None: weights { work_count: 0.3, avg_favorites: 0.3, avg_comments: 0.2, active_score: 0.2, } df df.copy() df[norm_work] min_max_normalize(df[work_count]) df[norm_fav] min_max_normalize(df[avg_favorites]) df[norm_cmt] min_max_normalize(df[avg_comments]) df[norm_active] min_max_normalize(df[active_score]) df[heat_score] ( weights[work_count] * df[norm_work] weights[avg_favorites] * df[norm_fav] weights[avg_comments] * df[norm_cmt] weights[active_score] * df[norm_active] ) df df.sort_values(heat_score, ascendingFalse).reset_index(dropTrue) return df if __name__ __main__: works_df pd.read_csv(data/works_data.csv, encodingutf-8-sig) result compute_heat_score(works_df) print(result[[cp_tag, work_count, avg_favorites, avg_comments, active_score, heat_score]])运行结果会按热度指数从高到低排列类似cp_tag work_count avg_favorites avg_comments active_score heat_score 0 义炭 320 3860.12 612.34 0.0234 1.000000 1 义忍 280 3320.55 530.21 0.0198 0.812345 2 锖义 210 2150.78 412.56 0.0177 0.531234 3 炭香 195 1800.44 380.90 0.0152 0.431234这一结果反映了在不同权重配置下各 CP 的综合热度差异。如果你认为收藏数比作品数更重要可以调高avg_favorites的权重观察排名变化。6. 可视化让热度对比更直观6.1 柱状图展示综合热度得分使用 pyecharts 生成一个交互式柱状图# 文件路径make_charts.py from pyecharts.charts import Bar, Radar from pyecharts import options as opts import pandas as pd df pd.read_csv(data/works_data.csv, encodingutf-8-sig) # 柱状图热度指数 result df.copy() result[heat_score] [0.95, 0.82, 0.56, 0.43] # 这里替换为 compute_heat_score 的结果 bar ( Bar() .add_xaxis(result[cp_tag].tolist()) .add_yaxis(热度指数, result[heat_score].tolist(), category_gap40%) .set_global_opts( title_optsopts.TitleOpts(title各 CP 综合热度对比), yaxis_optsopts.AxisOpts(name热度指数), xaxis_optsopts.AxisOpts(nameCP 标签), ) ) bar.render(output/heat_bar.html) print(柱状图已保存到 output/heat_bar.html)6.2 雷达图对比多维特征雷达图可以同时展示多个 CP 在四个归一化指标上的表现。# 继续在 make_charts.py 中 indicators [ {name: 作品数, max: 1}, {name: 收藏数, max: 1}, {name: 评论数, max: 1}, {name: 活跃度, max: 1}, ] radar_data [] for _, row in df.iterrows(): # 这里应该使用归一化后的数值下面为简化示例用实验值替换 radar_data.append( [ row[work_count] / 350, row[avg_favorites] / 4000, row[avg_comments] / 650, row[active_score] / 0.025, ] ) radar ( Radar() .add_schema( schemaindicators, shapepolygon, ) .add( series_name义炭, data[radar_data[0]], color#FF5722, ) .add( series_name义忍, data[radar_data[1]], color#2196F3, ) .set_global_opts( title_optsopts.TitleOpts(titleCP 多维特征雷达图), legend_optsopts.LegendOpts(), ) ) radar.render(output/radar_chart.html) print(雷达图已保存到 output/radar_chart.html)打开output目录下的 HTML 文件可以在浏览器中看到交互式图表。鼠标悬停时能查看具体数值。7. 完整实战一键生成 CP 热度分析报告7.1 主脚本整合下面把前面各模块整合到一个main.py中实现“读取数据 → 文本分析 → 热度建模 → 可视化输出”的完整流程# 文件路径main.py import pandas as pd from text_analysis import load_custom_dict, count_keywords, clean_text from heat_model import compute_heat_score from make_charts import generate_bar, generate_radar def main(): # 1. 加载自定义词典 load_custom_dict() # 2. 读取原始文本并统计关键词 df pd.read_csv(data/raw_texts.csv, encodingutf-8-sig) counter count_keywords(df[text].tolist()) print( 关键词频次统计 ) for k, v in counter.items(): print(f{k}: {v}) # 3. 读取作品维度数据并计算热度指数 works_df pd.read_csv(data/works_data.csv, encodingutf-8-sig) result compute_heat_score(works_df) print(\n 综合热度排名 ) print(result[[cp_tag, heat_score]].to_string(indexFalse)) # 4. 保存热度指数结果 result.to_csv(output/heat_scores.csv, indexFalse, encodingutf-8-sig) # 5. 生成图表 generate_bar(result) generate_radar(result) print(\n分析完成结果保存在 output 目录下。) if __name__ __main__: main()7.2 运行与验证在项目根目录执行python main.py预期可以看到类似输出 关键词频次统计 义炭: 52 义忍: 61 炭香: 42 锖义: 45 综合热度排名 cp_tag heat_score 义炭 0.9534 义忍 0.8211 锖义 0.5623 炭香 0.4387 分析完成结果保存在 output 目录下。需要强调的是运行结果会根据你的示例数据、聚合方式、权重设置而不同。重点在于理解整个分析流程原始数据不是直接得出结论的依据而是经过清洗、聚合、归一化和加权之后才变成一个可供比较的分数。7.3 结果解读的注意事项当看到“义炭热度最高”的结果时并不代表“义炭一定比义忍更好磕”。数据反映的是在演示数据集中义炭相关文本的讨论密度较高在虚构的作品数、收藏数等指标设定下义炭综合得分最高。如果换成真实平台数据权重不同结果也可能完全不同。因此数据结论只能作为参考不能替代个人审美和主观感受。8. 常见问题与排查思路8.1 关键词统计结果全是 0问题现象常见原因解决思路统计结果为 0自定义词典未加载确认调用jieba.add_word或jieba.load_userdict统计结果为 0文本清洗时把关键词过滤了查看clean_text是否删除了中文字符统计结果为 0CSV 编码问题读取时使用encodingutf-8-sig排查步骤单独打印一条分词结果确认关键词是否被正确切分检查清洗函数是否误删了中文字符确认字典文件路径是否正确。8.2 词云图片中文显示为方框这是因为 wordcloud 默认字体不支持中文。解决方法是显式指定中文字体路径wc WordCloud(font_pathC:/Windows/Fonts/msyh.ttc)不同系统的字体路径不一样可以使用下面的代码查找import matplotlib.font_manager as fm fonts [f.name for f in fm.fontManager.ttflist] for font in fonts: if Hei in font or PingFang in font or YaHei in font: print(font)8.3 pyecharts 图表打不开pyecharts 生成的是 HTML 文件使用浏览器打开即可。如果文件无法显示可能是浏览器安全策略问题可以尝试右键文件选择“打开方式”为 Chrome、Edge 或 Firefox将 HTML 文件放在本地服务中用http://localhost访问。8.4 数据只有几十条模型结果不具参考性演示数据只有 200 条记录在真实场景中样本量远远不够。建议在数据合规的前提下尽可能扩大数据规模或者引入多个平台的数据做交叉验证。样本量较小时任何权重调整都有可能让排名发生较大变化建议同时输出归一化前的原始指标让读者看到更多细节。9. 最佳实践与工程建议9.1 坚持数据合规红线在同人社区做数据分析时数据的合规安全永远放在第一位。不要用爬虫绕过平台的登录验证、频率限制和反爬机制不要抓取用户的隐私信息如真实姓名、联系方式不要未授权转发他人作品内容。建议优先使用官方 API、公开数据集或平台导出功能。如果确实需要爬取也要遵守 robots.txt 和服务条款控制请求频率只采集公开、聚合层面的数据。9.2 代码组织与可维护性前面把代码拆分成了text_analysis.py、heat_model.py、make_charts.py和main.py这样每个模块职责单一方便测试和复用。这在中小型数据分析项目中非常实用。更复杂的项目可以引入配置文件把权重、文件路径、词典路径等参数集中管理# 文件路径config.py WEIGHTS { work_count: 0.3, avg_favorites: 0.3, avg_comments: 0.2, active_score: 0.2, } DATA_PATHS { raw_texts: data/raw_texts.csv, works_data: data/works_data.csv, } OUTPUT_DIR output9.3 权重设计要透明可调热度指数本身就是一种建模方式不同的权重代表不同的价值取向。比如强调“作品规模”就让work_count占更大权重强调“读者互动”就让avg_favorites和avg_comments占更大权重。在生产环境中建议把权重参数暴露出来允许使用者按自己的需求调整并保存一份参数记录方便复现和审计。9.4 结果输出要客观中性分析结果的解读应保持客观。可以在报告开头注明数据来源、采集时间、样本量、权重设置和局限性。不要简单地下结论说“某 CP 就是最火的”更不要用分析结果去攻击不同偏好的人群。数据分析的意义是提供一种观察现象的新视角而不是制造对立。9.5 扩展方向当前的模型还比较简单可以继续扩展增加时间序列分析观察 CP 热度随时间的变化趋势加入创作者画像统计新增创作者数量与老作者活跃度引入语义情感分析判断文本是正面输出还是负面吐槽接入更多平台数据形成更全面的热度评估。10. 总结与学习路线本文从一个充满争论的 CP 话题出发完整演示了如何使用 Python 对同人文本和作品数据进行热度分析。核心收获有以下几点学会用 jieba 对中文文本做清洗、分词与关键词统计掌握 Min-Max 归一化和加权评分的基本思路会用 pyecharts 生成柱状图和雷达图理解数据分析中“主观感受 → 客观指标 → 综合评分”的转化过程。下一步可以继续学习pandas 的时间序列分析研究 CP 热度随时间的变化scikit-learn 的聚类算法对不同类型作品做自动分类网络爬虫的合规实现了解 API 请求和速率控制情感分析模型对文本内容做更深入的语义挖掘。在实际项目中请优先关注数据来源是否合规、文本清洗是否充分、权重设置是否合理。无论最终模型跑出来的排名是什么都记得保持开放心态数据只是帮你理解现象真正决定“好磕不好磕”的始终是你的审美和感受。如果这篇文章对你有帮助可以收藏备用。也可以把代码中的演示数据换成你自己关注的领域跑一版属于你的“热度分析报告”。动手写代码才是最好的学习方式。