1. 为什么专门拆一个"青少年模式"的分析项目
1.1 青少年模式不是单纯的开关,而是一套内容过滤机制
我是在做B站内容数据分析时,顺手点开青少年模式体验了一下,发现它和很多人理解的"只能看部分动漫"完全不同:内容池被重新组织,搜索、推荐、弹幕入口都有独立的策略,还会触发时长提醒。换句话说,它是一套独立的内容过滤机制,而不是一个简单的布尔开关。
既然是一套机制,就一定会产生数据。内容侧有"哪些内容能被放进来、哪些被挡在外面"的供给结构;用户侧有"谁开启、什么时候用、活跃多深、在哪里流失"的行为痕迹。这些数据放在一起,就意味着我可以不做主观推测,而是直接量化回答:B站青少年模式的真实覆盖和使用情况到底是怎么样的。
这正是我用Python做这套系统的原因——一个基于Bilibili青少年模式使用情况的数据分析可视化系统,本质上是把内容供给和用户行为两个观察视角接进同一张大屏,让趋势、结构、偏好都能被直观呈现。
1.2 这套系统到底想回答哪些问题
项目启动之前,我先把业务问题收敛成三句话,这几句话也是后续所有指标的源头:
- 青少年模式处于什么健康状态?——看启用率、活跃用户数、日均使用时长在时间维度上的变化。
- 内容池的结构是否合理?——看不同分区下可用内容的占比、高播放内容的分布,判断供给端有没有明显偏科。
- 用户偏好集中在哪些时段和内容类型?——看小时维度活跃度、热门内容分区,定位真实使用节奏。
这三问分别对应趋势分析、内容生态分析和用户行为画像分析。项目中所有代码、图表、页面布局都围绕这三句话展开,没有一句空转。
1.3 什么样的读者适合照搬这套做法
这个项目很适合三类人去参考。第一类是做Python数据分析方向课程设计或毕业设计的学生,整套链路从采集、清洗、聚合到可视化都齐全,且主题有现实意义。第二类是想建立内容平台监控看板的从业者,可以把里面的指标体系和Streamlit框架直接迁移到别的平台。第三类是对青少年内容生态本身感兴趣的研究者,可以重点看指标口径和解读方法,代码反而是次要的。
需要先说清楚的是,用户侧的青少年模式开启率、活跃时长这类数据,平台并不会公开提供。所以我在系统里做了双通道设计:平台内容侧用公开接口采集,用户行为侧用标明假设条件的仿真数据补全。这种混源方式在真实项目中很常见,关键是把仿真部分标清楚,避免把伪造数据当成真数据来解读。
2. 数据通道设计:真实公开接口加脱敏仿真数据
2.1 平台侧数据:B站公开API能拿到什么
平台侧数据负责回答"内容池长什么样"。我选择的是B站公开排行榜接口,不需要登录、不需要额外签名,按分区参数就能拿到榜单视频元数据,包括标题、分区、播放量、弹幕数等字段。
有人可能会想直接用搜索接口做"青少年模式可见性"的对比。但据我实测,搜索接口目前在未登录状态下的可用性很差,非登录态做大规模抓取统计既不现实也不稳定,我不建议把时间花在高耦合的逆向参数上。我的替代方案是"分区结构分析":把主要分区的榜单位数据抓下来,按分区聚合播放量、弹幕量、视频数量,观察内容供给结构在分区维度的分布情况。
这里要特别强调一个边界:排行榜反映的是全站的公开展示内容,不能直接等同青少年模式下的内容池。但把它作为供给结构的近似样本是合理的,用于判断"哪些分区内容更丰富、哪些分区头部效应更强"这类结构性问题,参考价值依然很高。
2.2 用户侧行为数据:为什么必须用仿真方案
用户侧数据在数据伦理上是敏感的。真实的开启率、个人观看时长、每个用户的分区偏好,都属于个人行为数据。我作为一个独立开发者,没有平台授权,也没有合理途径拿到这些明细,最稳妥的做法就是构造一份仿真数据集,并且把"仿真"两个字写清楚。
仿真不是拍脑袋,我结合公开信息中平台用户规模的量级、常规内容产品的活跃时段规律、以及节假日效应来设定参数区间。例如,启用率落在30%到45%之间,一天内晚间20点到22点活跃度自然抬升,周末比工作日高几个百分点。用这些约束生成的数据,用于演示分析链路和看板效果是够用的,但它不代表真实运营数据,这一点要在所有输出页面上标注。
2.3 数据字典与最小可复现采集代码
在设计阶段,我先定义了统一数据字典,防止后面每张图各算各的、字段对不上。
| 字段名 | 类型 | 含义 | 数据来源 |
|---|---|---|---|
| bvid | string | 视频唯一标识 | B站排行榜接口 |
| title | string | 视频标题 | B站排行榜接口 |
| partition | string | 一级分区名 | B站排行榜接口 |
| view_count | int | 播放量 | B站排行榜接口 |
| danmaku | int | 弹幕数 | B站排行榜接口 |
| date | datetime | 统计日期 | 本地生成 |
| total_active | int | 当日活跃用户数 | 仿真数据 |
| enable_rate | float | 青少年模式启用率 | 仿真数据 |
| hourly_active | dict | 24小时活跃分布 | 仿真数据 |
采集部分直接按这个字典落字段。排行榜接口的调用代码很简洁:
import requests import pandas as pd import time HEADERS = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" } def fetch_ranking(rid: int = 0, page_size: int = 50) -> pd.DataFrame: """抓取B站公开排行榜数据,rid=0表示全站""" url = "https://api.bilibili.com/x/web-interface/ranking/v2" params = {"rid": rid, "type": "all", "page_size": page_size} resp = requests.get(url, headers=HEADERS, params=params, timeout=10) data = resp.json() if data.get("code") != 0: raise ValueError(f"接口返回异常: {data.get('message')}") items = data.get("data", {}).get("list", []) rows = [{ "bvid": it.get("bvid"), "title": it.get("title"), "partition": it.get("tname"), "view_count": it.get("stat", {}).get("view", 0), "danmaku": it.get("stat", {}).get("danmaku", 0), } for it in items] return pd.DataFrame(rows)实际跑的时候,我会把rid从0换到动画、音乐、知识、影视、生活等主要分区,循环请求,每次间隔0.8秒左右。一次能拿到几百条覆盖多分区的公开视频样本,足够做结构分析。
2.4 合规与请求频率:别把接口打挂了
我在第一个版本里犯过请求过快的错误,结果很快触发接口限流。后来调整成三条纪律:第一,每个请求之间sleep 0.5秒以上;第二,定期更换User-Agent字符串;第三,只保存自己要用的字段,不长时间重复抓同一批数据。采集代码跑完,我直接把原始DataFrame落成CSV,后续分析只读本地文件,避免反复请求线上接口。
这个习惯值得坚持。数据合规不是一句口号,落到工程上就是"能本地存就本地存,能少量请求就少量请求",既保护自己,也保护平台服务的稳定性。
3. 指标体系:把"使用情况"拆成四类可计算的量
3.1 第一层:启用与活跃类指标
这是整个大屏的北极星部分。最核心的是两个:
- 青少年模式启用率 = 启用青少年模式的用户数 / 适龄用户总数
- 日活跃用户数 = 当天至少有1次页面访问行为的用户数
这两个指标分别回答"渗透"和"黏性"。启用率反映有多少适龄用户认可这个功能,日活跃反映启用之后是否真的在使用。如果启用率高但活跃低,问题大概率出在提醒机制或内容吸引力上;如果启用率低但活跃高,说明存量用户忠诚度不错,接下来要考虑的是扩大触达。
为了让趋势更立体,我在仿真数据里给每日启用率加了一个随机波动区间,并叠加周末抬升效应,日活跃用户数按量级线性相关。这部分逻辑全部集中在生成脚本里,页面层只看最终表,不关心随机过程。
3.2 第二层:时段与时长类指标
时段信息用来回答"什么时间在用"。这里设计两个指标:
- 时段活跃度:把一天切成24小时,统计每个小时窗口内的活跃用户占当日活跃用户的比例。
- 日均使用时长 = 当日累计使用时长 / 日活跃用户数。
时段活跃度用热力图呈现,X轴放一周七天,Y轴放0点到23点,颜色深浅代表活跃比例。这样一眼能看到周末的下午和晚上是不是比工作日更活跃、晚间有没有明显的高峰窗口。
日均使用时长单独做成柱状图或小折线,放在启用率趋势图旁边,用来观察"用得更久"和"用得更广"是否同步变化。实际分析中,这两者经常错位:启用率平稳但时长上升,说明留存用户在加深使用;启用率上升但时长下降,则可能是新用户拉低均值,需要进一步看分用户层的数据。
3.3 第三层:内容生态结构类指标
内容侧的核心指标围绕"供给结构"展开。我在真实排行榜数据上计算:
- 分区覆盖率 = 该分区上榜视频数 / 榜单视频总数
- 高播放内容占比 = 播放量超过100万的视频数 / 该分区上榜视频数
- 分区平均播放量 = 该分区上榜视频播放量的均值
这三个指标合起来能说明很多问题:哪些分区给内容池贡献了更多存量,哪些分区虽然上榜多但头部效应严重,平均播放量被少数爆款拉高。内容运营看到这类数据,可以针对偏低的分区做供给补充。
分区结构的聚合代码很清晰:
def aggregate_partition(df: pd.DataFrame) -> pd.DataFrame: """按分区聚合视频结构指标""" result = ( df.groupby("partition") .agg( video_count=("bvid", "count"), avg_view=("view_count", "mean"), high_view_ratio=("view_count", lambda s: (s >= 1_000_000).mean()), total_danmaku=("danmaku", "sum"), ) .reset_index() .sort_values("video_count", ascending=False) ) return result3.4 指标计算脚本封装
因为大屏上要反复挂载这些指标,我在core/metrics.py里把它们封装成一个入口,避免在页面文件里堆公式:
import pandas as pd def compute_key_metrics(daily: pd.DataFrame, hourly: pd.DataFrame) -> dict: return { "avg_enable_rate": daily["enable_rate"].mean(), "latest_active": int(daily["total_active"].iloc[-1]), "avg_duration": daily["total_duration"].mean() / daily["total_active"].mean(), "peak_hour": int(hourly.mean().idxmax()), "enable_rate_change": daily["enable_rate"].iloc[-1] - daily["enable_rate"].iloc[0], }这样一个函数返回所有北极星指标,页面层只负责展示,指标口径的变更只发生在这一处。后面任何人接手,只需要看数据字典和这几个函数,不用翻完整个前端页面。
4. 可视化大屏:Streamlit加Plotly的工程落地
4.1 技术选型对比:为什么要走Streamlit路线
可视化系统有很多搭法,我在动手前先做了个简单的横向对比:
| 方案 | 优点 | 缺点 | 适合场景 |
|---|---|---|---|
| Streamlit + Plotly | 纯Python、开发速度极快、图表交互开箱即用 | 大屏精细定制能力弱 | 内部看板、分析汇报、课程设计 |
| Flask + ECharts | 前端定制自由度高、视觉效果大气 | 需要前后端同时写、联调成本高 | 正式对外产品大屏 |
| Jupyter + Matplotlib | 探索分析顺手、代码量少 | 静态图多、不适合持续观察 | 一次性分析报告 |
考虑到项目核心是把指标逻辑和可视化流程讲清楚,而不是做一个对外运营级别的炫酷大屏,我选了Streamlit。它的优势是写纯Python就能产出交互页面,表格、下拉框、日期选择器都是现成组件,不需要额外学前端。Plotly那一层负责产出交互式图表,我目前用下来非常稳,改指标后刷新页面即可,不用重启服务。
4.2 工程目录与模块边界
整个项目的目录结构长这样:
bilibili_mode_analysis/ ├── app.py ├── data/ │ ├── raw_ranking.csv │ └── demo_usage.csv ├── core/ │ ├── cleaner.py │ ├── metrics.py │ └── aggregator.py └── views/ ├── overview.py ├── trend.py └── content.py数据文件放在data下,core里只做清洗、聚合、指标计算,views只负责把数据渲染成图表。app.py是唯一的入口,负责初始化数据和路由到各个Tab。这个边界划分花了我一点时间,但它直接决定了后面加需求时要不要动老代码。我的体验是,如果指标计算和页面渲染混在同一个文件里,加第三张图的时候一定会后悔。
4.3 Dashboard布局与核心页面代码
主入口按Tab组织页面结构:
import streamlit as st import pandas as pd from views import overview, trend, content st.set_page_config(page_title="B站青少年模式数据分析可视化系统", layout="wide") st.title("B站青少年模式使用情况数据分析系统") @st.cache_data(ttl=3600) def load_all(): daily = pd.read_csv("data/demo_usage.csv", parse_dates=["date"]) hourly = pd.read_csv("data/hourly_active.csv") ranking = pd.read_csv("data/raw_ranking.csv") return daily, hourly, ranking daily, hourly, ranking = load_all() tab1, tab2, tab3 = st.tabs(["总览", "趋势分析", "内容生态"]) with tab1: overview.render(daily, hourly) with tab2: trend.render(daily, hourly) with tab3: content.render(ranking)总览页首屏是4个KPI卡片,然后接一张趋势折线图,往下是热力图和分区玫瑰图。核心图表用Plotly生成,代码很直接:
import plotly.express as px def render(daily: pd.DataFrame, hourly: pd.DataFrame): c1, c2, c3, c4 = st.columns(4) metrics = compute_key_metrics(daily, hourly) c1.metric("平均启用率", f"{metrics['avg_enable_rate']:.2%}") c2.metric("最新活跃用户", f"{metrics['latest_active']:,}") c3.metric("峰值活跃时段", f"{metrics['peak_hour']}:00") c4.metric("日均使用时长", f"{metrics['avg_duration']:.2f} 分钟") fig = px.line( daily, x="date", y="enable_rate", title="青少年模式启用率趋势", labels={"enable_rate": "启用率", "date": "日期"} ) st.plotly_chart(fig, use_container_width=True)4.4 一条完整的"数据流"演练
用总览页的启用率趋势图来走一遍完整链路:CSV文件先是日期和启用率两列原始数据,这是仿真环节生成的;进入core/cleaner.py后,做缺失值检查和日期类型转换;再进入core/metrics.py计算均值;最后在views/overview.py中渲染成Plotly折线图。一个指标对应一整个管道,误差只可能出在三处:数据生成、指标口径、展示字段名。
这套结构的好处是问题定位快。有一次折线图的日期显示成数字时间戳,我没有去动页面代码,直接在cleaner里检查parse_dates配置,一次就修好了。把变更收敛在必要层,是我维护这个项目最大的体会。
5. 跑起来之后:六张关键图表及其业务解读
5.1 总览页的KPI卡片
大屏跑起来后,首屏四张卡片直接给结论。我这组仿真数据的平均启用率在37%左右,最新活跃用户量级在数百万上下,峰值活跃时段落在晚上21点,日均使用时长大约40多分钟。这四个数字放在一起,第一眼就能判断当前处在什么状态:启用率接近四成,在内容产品里属于有明显用户基础但尚未主流化的阶段;晚间高峰说明青少年模式的使用节奏和全站用户习惯基本一致。
5.2 启用率趋势折线图
折线图比卡片更能看出变化。我在仿真数据里特意叠加了周末和假期脉冲,所以趋势线呈现出周期性锯齿——工作日平缓,周末抬升,遇到长假会出现一个明显尖峰。这个规律在业务上说得通:使用节奏和在校时间强相关,休息日集中使用。如果你换一套真实数据,可以继续对比学期中与寒暑假的斜率差异,这是一个相当有洞察力的分析角度。
5.3 时段活跃热力图
热力图是全场信息量最大的一张图。仿真数据里晚间20点到22点颜色最深,周末下午14点到18点也有一片明显的暖色。这说明内容供给策略可以适当向晚间时段倾斜,比如晚间多推知识类和优质动画,而不是全天平均用力。
这里还有一个细节值得注意:如果凌晨1点到3点出现意外的高活跃区域,那大概率不是真实的小用户群体,而需要考虑设备共享、家长代看等异常因素。分析时段异常,往往比分析时段峰值更能发现隐藏问题。
5.4 内容生态玫瑰图与分区结构
内容生态页用玫瑰图展示分区覆盖率,数据来自真实公开接口。我实测跑到的结果是,知识区、动画区、生活区上榜数量排在前列,音乐区和影视区相对靠后。但高播放内容占比的排序完全不同——有些分区上榜数量不多,单条爆款播放量极高,说明该分区内容供给总体稀缺、头部效应严重。所以这三张图必须放到一起看,单看任何一张都会得到偏颇的结论。
5.5 偏好路径漏斗
漏斗图用来模拟从进入青少年模式到长期留存的转化路径。我的口径是四级:开启设置、当日观看、连续使用七天、主动调节偏好选项,逐级收敛。仿真结果显示:开启设置后当日观看比例约八成,连续使用七天降到五成,主动调节偏好选项只剩三成。这里能清楚看到产品还能优化的一环:多数用户停留在被动接受默认过滤内容的状态,真正主动定制内容偏好的比例很低。
5.6 图表组合说明问题时的可信度边界
我必须再次强调可信度边界:平台内容结构指标来自公开接口,具备参考意义;用户行为类指标全部是仿真数据。所以这套大屏更适合用来演示分析方法论、验证工程链路和训练图表解读能力,不能直接拿去做真实运营决策。我在页脚加一行标注就是这个原因——先标明数据性质,再谈业务解读,这是数据工作者的基本素养。
6. 开发过程中踩到的坑和收尾建议
6.1 B站接口返回code非零与字段兼容问题
排行榜接口最容易踩的坑是请求频率一高,接口开始返回code为负值的异常JSON。处理办法是采集层统一做code校验,只要code不是0就抛错并停止本轮抓取,避免把异常数据写进CSV。另外,不同接口返回的播放量字段位置不一致,有的在顶层,有的藏在stat子对象里,我建议固定用一个字段提取函数,不要在每个地方各自解析。
还遇到过一个隐蔽问题:排行榜接口偶尔对某个分区返回空列表,这不是报错,而是正常返回空数据。如果不处理,聚合时该分区会直接消失,玫瑰图缺一块扇区。我在cleaner里对这种情况做了兜底,空分区保留占位并填0值,图表展示才不会缺失。
6.2 热力图坐标密度与中文乱码
热力图第一版做出来,横坐标塞了连续日期,图表密到根本看不清。后来我把X轴改成"周一、周日",Y轴保持"0点到23点",聚合逻辑用一周窗口做透视,画面立刻清爽了。中文乱码是Plotly内置字体在部分Linux服务器上的老问题,解决办法是显式指定系统已有的中文字体,比如"Microsoft YaHei"或"Noto Sans CJK SC"。开发机上显示正常不代表部署服务器也正常,字体问题一定要在目标环境提前测。
import plotly.graph_objects as go fig_heat = go.Figure(data=go.Heatmap( z=hourly_pivot.values, x=hourly_pivot.columns, y=hourly_pivot.index, colorscale="YlOrRd", texttemplate="%{z:.1%}", textfont={"size": 10} )) fig_heat.update_layout(font={"family": "Noto Sans CJK SC, Microsoft YaHei"})6.3 Streamlit缓存导致数据不刷新
这个坑特别容易被忽略。我给数据读取函数加@st.cache_data后,新增的CSV文件在页面上迟迟不出现,原因就是缓存命中了旧文件。排查到后面,我把cache_data的ttl参数设成3600秒,并在侧边栏放一个刷新按钮,调用st.cache_data.clear()强制清理。实际经验是:缓存能大幅提升页面重刷时的响应速度,但一定要留手动清理入口,否则开发调试时会怀疑人生。
6.4 留给后续的扩展空间
这个项目目前的完成度,我认为足够作为独立的数据分析案例使用。如果后续要继续扩展,有两个方向性价比最高:一是把CSV换成MySQL,采集脚本做成定时任务,让大屏变成持续更新的自动化看板;二是引入弹幕文本分词和情感判断,把内容生态分析从结构层面延伸到语义层面,探究不同分区内容的情感分布,这会是一个完全不同的研究课题。
我自己做完这个项目的感受是:青少年模式这个主题非常容易在讨论中滑向情绪化评价,但数据给了一个特别有用的锚点——你可以不用情绪立场,只用指标去观察变化。这种"把模糊概念拆成可计算指标,再用可视化大屏把结论讲清楚"的流程,放到任何平台分析项目上都成立。希望这套代码和思路,能帮你把自己的数据想法真正落到画面上。