资讯详情

资讯详情

Python实现播放量月报自动化:准神话曲统计系统实战

相信很多长期关注“术力口”文化圈的朋友每个月都有类似的习惯等着看一批播放量数据接近“神话级”的曲目更新。与其说这是单纯的音乐盘点不如说它是一套典型的“数据指标监控”需求——把作品的播放量按固定周期拉成快照再计算增量、距离阈值缺口和排名变化。手动整理一次两次还能接受但如果做成月刊每个月都靠人工复制粘贴既容易漏歌也容易在“十万”、“百万”、“千万”这些数量级上写错。所以就有了本篇的设想用 Python 实现一个“虚拟的术力口准神话曲月刊系统”只要给定本期与上期的播放量快照数据就能自动生成准神话候选、增量榜和可视化图片。本文不讨论任何平台数据的非法抓取方案只围绕本地快照做分级统计与月报生成帮你把这类“周期性榜单监控”的思路沉淀成可复用代码。1. 背景与核心概念1.1 什么是“术力口”和“准神话曲”“术力口”是中文 VOCALOID 文化社区里使用频率很高的代称通常泛指使用歌声合成软件创作歌曲的作者与听众圈子。歌曲被投递到弹幕视频平台后平台会记录累计播放量或再生数社区玩家为了方便交流习惯把这些播放量划出几个等级。比较常见的说法是达到十万播放称为“殿堂曲”达到一百万播放称为“传说曲”达到一千万播放称为“神话曲”。那“准神话曲”是什么从字面理解就是播放量已经非常高、但还没有正式跨过“神话曲”门槛的作品。它并不是一个官方产品字段而是社区在月度榜单或专题盘点时设计出来的“业务概念”。在一个数据分析系统里这类概念必须被转成可执行的规则比如“当前播放量在一百万到一千万之间并且距离一千万阈值的缺口小于某个百分比”或者“本月增量在所有传说曲中排名靠前”。本文采用“已完成神话曲阈值的 95% 以上且尚未达到一千万”的判断口径并在后续代码中开放成配置项方便自行修改。1.2 为什么要用“月报系统”代替人工整理当我们只是临时想知道“哪些歌有望冲击下一首神话曲”时人工在页面上排序当然可行。但月刊的难点在于周期性、可比较、可追溯。每个月如果只看绝对播放量很难发现歌曲是否增长放缓只有把本期快照与上期快照做差才能知道一首歌在过去一个月实际涨了多少。再者统计口径不能每次都不一样例如“上月纳入的歌这个月如果被删除或下架算不算入总榜”“本月新增歌曲没有上月值时怎么办”都必须由代码统一处理。因此月报系统的核心价值并不在于“生成一张好看的表格”而在于把统计规则稳定下来。哪怕未来数据量从几十首扩展到几千首也不需要重新研发算法只要保证输入文件格式一致脚本就能继续输出工作报告。这正是很多数据处理任务适合自动化的原因不是计算有多难而是重复劳动会引入人为误差。1.3 数据边界与合规说明在做这类榜单统计时最容易踩到的坑不是代码逻辑而是数据来源。如果某个平台的用户协议或服务条款没有开放批量数据接口个人去写爬虫抓取大量页面并不合适既可能违反规则也可能给平台带来压力。本文之所以强调“本地快照 CSV”就是为了把代码逻辑和来源合规性分开。你可以通过官方导出、人工记录或企业内部授权的数据仓库来生成快照文件拿到文件后使用下面的统计脚本做分析就是完全可控的。 这样操作既保证项目可以演示也避免把技术教程写成爬虫教程。2. 环境准备与项目结构2.1 Python 运行环境本项目的代码主要依赖 Python 的数据处理生态。建议使用 Python 3.10 及以上版本因为脚本中会用到较新的类型提示写法。需要安装的第三方库包括pandas、openpyxl、matplotlib。pandas负责读取 CSV 和完成表合并、分组、排序openpyxl用于把数据写进带多个 Sheet 的 Excel 文件matplotlib负责绘制月度增量图和距离神话曲缺口图。如果你的电脑还没有安装这些库可以在终端中执行pip install pandas openpyxl matplotlib版本不需要完全固定建议使用当前 PyPI 上的稳定版本。需要注意pandas和openpyxl都经常更新某些老的 API 可能有废弃警告但本文使用的核心方法非常稳定不会依赖实验性功能。2.2 项目文件规划为了让读者可以清楚地看到每个模块的职责建议采用下面的工程结构voca_monthly/ ├── config.py # 阈值、目录、期数等配置 ├── make_demo_data.py # 生成本地演示快照 ├── snapshot_loader.py # 读取并清洗快照 CSV ├── report_builder.py # 计算月度指标与准神话候选 ├── excel_exporter.py # 输出 Excel 月报 ├── chart_exporter.py # 输出可视化图片 ├── main.py # 统一入口 ├── requirements.txt # 依赖清单 ├── data/ # 输入快照 └── output/ # 输出结果这里每一个 Python 文件职责单一config.py只放全局参数make_demo_data.py生产演示数据让读者在没有真实数据时也能跑通流程snapshot_loader.py只负责数据读取report_builder.py只负责指标计算。后期如果数据结构变化优先调整配置文件或加载函数而不会改动主流程。2.3 依赖清单示例requirements.txt可以写成如下内容具体版本号可以结合自己环境锁定pandas2.0.0 openpyxl3.1.0 matplotlib3.8.0当项目需要部署到服务器或交给其他同事运行时建议使用pip freeze requirements-lock.txt生成带精确版本的环境锁文件。这样能最大程度避免“本地能跑服务器报错”的问题。3. 统计口径与算法拆解3.1 播放量等级划分把播放量映射为等级时最简单的方法是使用多个“阈值判断”。先确认上限再依次向下判断避免一首一千万以上的歌曲被误判成“传说曲”。下面这段伪代码描述了划分逻辑def tier_of(count: float) - str: if count 10_000_000: return 神话曲 if count 1_000_000: return 传说曲 if count 100_000: return 殿堂曲 return 普通区在实际业务中“殿堂曲”这些称呼可能有平台定制含义因此我推荐把阈值全部放到配置文件中而不是散落在函数里。例如本文整理的“准神话曲”候选只关心传说曲和神话曲之间的区间但月报概览页仍会展示各等级歌曲的数量以便知道整体数据分布。3.2 准神话曲候选的判断方法“准神话曲”并没有唯一标准项目中通常采用“百分比缺口”与“绝对量缺口”两种方式。百分比缺口适合不同量级的对比例如“已到达神话曲阈值的 95%”绝对量缺口则直接看还差多少播放量例如“距离一千万还差不到五十万”。两种方式都能用但需要特别注意当歌曲已经超过一千万时它已经正式成为神话曲不应继续出现在准神话候选里。因此更稳妥的业务口径是先计算percent_to_myth current_count / myth_threshold然后同时判断“当前播放量大于等于传说曲阈值”和“百分比小于 1”以及“百分比大于等于 0.95”。如果某首歌本月刚刚突破一千万它应该出现在“本月新晋神话曲”榜中而不是继续留在“准神话曲”栏目里。把这种细节规则写清楚能避免不同月份之间前后口径不一致。3.3 月度快照模型月度快照可以理解成“每个月月底或月初导出的全量歌曲表”。每首歌在快照里通常有唯一 ID、标题、作者名称和截止到快照日期的累计播放量。我们需要比较的是“本月底累计播放量”和“上月底累计播放量”两值相减得到当月净增。因为累计播放量永远不会倒减少所以正常情况下delta应大于等于零如果出现负数基本可以判断是快照错误、口径不同或歌曲曾下架又重新上传。采用快照模型最大的好处是数据可追溯。假设八月份的月报出现了明显异常我们可以直接查看数据目录里的snap_202508.csv与上一期快照做对比。每次运行都保留原始输入文件远比只保留一份 Excel 结果更适合排查问题。3.4 合并时必须考虑的新增与下架两个月份快照的歌单并不是一定完全一致。本月可能有新歌第一次进入统计范围也可能有旧歌因为作者删除、视频失效等原因不再出现在本期快照里。最常用的合并方式是“外连接”把左边上月快照和右边本月快照都保留下来本月不存在的新歌上月播放量填充为0本月已消失的旧歌本期播放量填充为0但在后续统计中不应把它当作正常歌曲看待。实际操作中建议在输出明细表时增加一列is_quasi_myth和一列is_in_current之类标记方便读者快速筛选。若歌曲本月已经下架即使上月播放量接近一千万也不能把它标记成当前有效的准神话曲候选否则会误导阅读者。4. 完整实战案例下面进入核心流程。为便于复现本文使用脚本生成一批演示数据并演示从读取、计算到导出 Excel、绘图的全过程。4.1 生成演示数据创建make_demo_data.py写入如下代码。它会模拟 12 首歌曲在 2025 年 7 月和 8 月两个快照中的累计播放量并把文件输出到data目录。这些歌曲名称均为“演示用”不代表任何真实作品。# -*- coding: utf-8 -*- 生成两份演示快照数据用于月报统计。 运行后会在 data 目录下生成 - snap_202507.csv - snap_202508.csv import random from pathlib import Path import pandas as pd random.seed(202508) BASE_DIR Path(__file__).parent DATA_DIR BASE_DIR / data DATA_DIR.mkdir(exist_okTrue) SONGS [ (v0001, 示例术曲·月夜观测站, 青月P), (v0002, 示例术曲·星轨回信, 青月P), (v0003, 示例术曲·黑箱里的人偶, 白泽P), (v0004, 示例术曲·一百秒后告别, MochaP), (v0005, 示例术曲·极北列车, 灰岛P), (v0006, 示例术曲·电子海豚假日, 凉宫P), (v0007, 示例术曲·重力落体, 北白川P), (v0008, 示例术曲·雨音博物馆, 音律研究社), (v0009, 示例术曲·透明伞问卷, 无糖P), (v0010, 示例术曲·旧时钟塔, 八度音P), (v0011, 示例术曲·星期天的云, 青月P), (v0012, 示例术曲·地下铁诗人, 灰岛P), ] BASE_PLAY { v0001: 210_000, v0002: 4_800_000, v0003: 9_710_000, v0004: 8_200_000, v0005: 9_950_000, v0006: 3_000_000, v0007: 650_000, v0008: 9_890_000, v0009: 5_500_000, v0010: 11_200_000, v0011: 50_000, v0012: 720_000, } def make_snapshot(month: str, previous_df): snap_date f{month[:4]}-{month[4:]}-01 00:00:00 rows [] for song_id, title, author in SONGS: if previous_df is None: base_count BASE_PLAY[song_id] else: last_row previous_df.loc[previous_df[song_id] song_id] base_count int(last_row[play_count].iloc[0]) growth random.randint(5_000, 180_000) play base_count growth rows.append({ song_id: song_id, title: title, author: author, snap_date: snap_date, play_count: play, }) return pd.DataFrame(rows) if __name__ __main__: snap_july make_snapshot(202507, None) snap_july.to_csv(DATA_DIR / snap_202507.csv, indexFalse, encodingutf-8-sig) snap_aug make_snapshot(202508, snap_july) snap_aug.to_csv(DATA_DIR / snap_202508.csv, indexFalse, encodingutf-8-sig) print(演示数据已生成到 data 目录。)运行命令python make_demo_data.py生成后的snap_202508.csv文件内容类似song_id,title,author,snap_date,play_count v0001,示例术曲·月夜观测站,青月P,2025-08-01 00:00:00,310000 v0002,示例术曲·星轨回信,青月P,2025-08-01 00:00:00,4850000 ...要注意这里把快照时间写为次月月初是模拟“月末结算后导出”的场景业务上完全没问题只要两期快照时间保持一致即可。4.2 配置文件管理阈值创建config.py集中放置目录、期数和阈值。# -*- coding: utf-8 -*- 全局配置。 from pathlib import Path BASE_DIR Path(__file__).parent DATA_DIR BASE_DIR / data OUTPUT_DIR BASE_DIR / output OUTPUT_DIR.mkdir(exist_okTrue) # 默认统计周期与期数 DEFAULT_LAST_MONTH 202507 DEFAULT_MONTH 202508 DEFAULT_ISSUE 10 # 平台分级阈值 HALL_THRESHOLD 100_000 # 殿堂曲 LEGEND_THRESHOLD 1_000_000 # 传说曲 MYTH_THRESHOLD 10_000_000 # 神话曲 # 准神话曲候选播放量达到神话阈值的比例 QUASI_MYTH_RATIO 0.95 # 增量榜展示数量 TOP_N 15把所有阈值放在同一个文件里后期做“第十一期”“第十二期”时不需要读懂每个函数只要修改月份、期数以及可能变化的业务阈值即可。这也是工程项目中很常见的“约定优于配置”思路。4.3 读取与清洗快照创建snapshot_loader.py统一完成 CSV 读取。虽然简单但这一步能避免重复代码。# -*- coding: utf-8 -*- 加载 CSV 快照。 import pandas as pd def load_snapshot(path: str) - pd.DataFrame: df pd.read_csv(path, dtype{song_id: str}) df[play_count] pd.to_numeric(df[play_count], errorscoerce) df[snap_date] pd.to_datetime(df[snap_date], errorscoerce) df df.sort_values(snap_date) df df.drop_duplicates(subset[song_id], keeplast) df df.dropna(subset[play_count, snap_date]) return df.reset_index(dropTrue)这段代码包含两个关键动作第一按snap_date排序后对song_id去重保证同一首歌在快照中只有一行第二把播放量和日期列做类型转换避免后续计算因字符串类型报错。如果真实数据带有脏字符可以继续在该函数里增加清洗逻辑例如去除播放量中的逗号、处理标题空格等。4.4 核心指标计算创建report_builder.py。这里实现“上月快照 本月快照 - 完整指标表”的核心逻辑。# -*- coding: utf-8 -*- 月度播放量指标计算。 import pandas as pd from config import LEGEND_THRESHOLD, MYTH_THRESHOLD, QUASI_MYTH_RATIO def _merge_snapshots(last_df: pd.DataFrame, current_df: pd.DataFrame) - pd.DataFrame: last_cols [song_id, title, author, play_count] current_cols [song_id, title, author, play_count] merged pd.merge( last_df[last_cols], current_df[current_cols], onsong_id, howouter, suffixes(_last, _current), ) merged[play_count_last] merged[play_count_last].fillna(0).astype(float) merged[play_count_current] merged[play_count_current].fillna(0).astype(float) merged[title] merged[title_current].fillna(merged[title_last]) merged[author] merged[author_current].fillna(merged[author_last]) return merged def _calc_indicators(merged: pd.DataFrame) - pd.DataFrame: merged[delta] merged[play_count_current] - merged[play_count_last] merged[delta_rate] merged[delta] / merged[play_count_last].replace(0, pd.NA) merged[remain_to_myth] (MYTH_THRESHOLD - merged[play_count_current]).clip(lower0) merged[percent_to_myth] (merged[play_count_current] / MYTH_THRESHOLD).round(6) return merged def _mark_tier(df: pd.DataFrame) - pd.DataFrame: def tier_of(count: float) - str: if count MYTH_THRESHOLD: return 神话曲 if count LEGEND_THRESHOLD: return 传说曲 return 普通区 df[tier] df[play_count_current].apply(tier_of) return df def build_monthly_report(last_df: pd.DataFrame, current_df: pd.DataFrame) - pd.DataFrame: merged _merge_snapshots(last_df, current_df) merged _calc_indicators(merged) merged _mark_tier(merged) merged[is_in_current] merged[play_count_current] 0 merged[is_quasi_myth] ( merged[is_in_current] (merged[play_count_current] LEGEND_THRESHOLD) (merged[play_count_current] MYTH_THRESHOLD) (merged[percent_to_myth] QUASI_MYTH_RATIO) ) merged merged.sort_values( [is_quasi_myth, delta], ascending[False, False] ).reset_index(dropTrue) return merged这段代码的思路是先做外连接再计算增量、增速和距离神话曲的缺口最后根据规则打标签。is_in_current的作用是标记本月是否仍然存在缺失的本月歌曲即使上月播放量很高也不能成为准神话候选。借助布尔类型配合后续 Excel 输出可以非常方便地筛选不同子集。4.5 导出 Excel 月报创建excel_exporter.py把结果输出到带多个 Sheet 的 Excel 文件。# -*- coding: utf-8 -*- 导出 Excel 月度报告。 from pathlib import Path import pandas as pd from config import OUTPUT_DIR, TOP_N def export_excel( report_df: pd.DataFrame, month: str, issue: int, ) - Path: output_path OUTPUT_DIR / fvocaloid_monthly_{month}_issue{issue}.xlsx summary_df pd.DataFrame({ 统计月份: [month], 期数: [f第{issue}期], 歌曲总数: [len(report_df)], 本月仍在榜歌曲数: [int(report_df[is_in_current].sum())], 神话曲数量: [int((report_df[tier] 神话曲).sum())], 准神话曲候选数量: [int(report_df[is_quasi_myth].sum())], }) quasi_df report_df[report_df[is_quasi_myth]].sort_values( percent_to_myth, ascendingFalse ) growth_df report_df[report_df[is_in_current]].sort_values( delta, ascendingFalse ).head(TOP_N) with pd.ExcelWriter(output_path, engineopenpyxl) as writer: summary_df.to_excel(writer, sheet_name概览, indexFalse) quasi_df.to_excel(writer, sheet_name准神话曲候选, indexFalse) growth_df.to_excel(writer, sheet_name本月增量Top, indexFalse) report_df.to_excel(writer, sheet_name全部明细, indexFalse) return output_pathExcel 导出后概览 Sheet 会直接显示本期有多少首神话曲、多少首准神话曲候选准神话曲候选 Sheet 则按完成度从高到低排列。由于全部明细 Sheet 包含所有歌曲分析人员也能自行使用 Excel 透视表做更深一步分析减少重复沟通成本。4.6 输出可视化图创建chart_exporter.py生成“月度增量 Top 15”和“准神话候选缺口”两张图。# -*- coding: utf-8 -*- 输出图表。 import matplotlib matplotlib.use(Agg) import matplotlib.pyplot as plt import pandas as pd from config import MYTH_THRESHOLD, TOP_N plt.rcParams[font.sans-serif] [ Microsoft YaHei, PingFang SC, Noto Sans CJK SC, SimHei, sans-serif, ] plt.rcParams[axes.unicode_minus] False def export_charts(report_df: pd.DataFrame, output_dir, month: str) - list: charts [] growth_df report_df[report_df[is_in_current]].sort_values( delta, ascendingTrue ).tail(TOP_N) fig1, ax1 plt.subplots(figsize(10, 7)) ax1.barh(growth_df[title], growth_df[delta], color#4C72B0) ax1.set_title(f{month} 播放量增量 Top {TOP_N}) ax1.set_xlabel(播放量增量) ax1.set_ylabel(歌曲) path1 output_dir / fgrowth_top_{month}.png fig1.savefig(path1, dpi150, bbox_inches
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →