1. 项目背景与需求拆解
1.1 为什么盯上“青少年模式”做数据分析
这两年Bilibili的青少年模式一直是家长和平台方都比较关注的功能,但说实话,关于这个功能到底有多少人在用、什么时段用得最多、开启模式下大家在看什么内容,公开讨论大多停留在感性层面,缺少真实数据支撑。
所以这个项目的出发点很直接:用Python采集Bilibili上能公开观测到的用户行为数据,围绕青少年模式的开关状态、观看时段、内容分区、视频时长这些维度做一次系统性的数据分析,最后落地成一个可视化的展示系统。项目全称叫“基于Bilibili青少年模式使用情况的B站数据分析可视化系统”,本质上就是做一个从数据采集、清洗、分析到图表展示的完整闭环。
要说明的是,这里采集的数据是经过脱敏处理后的匿名行为特征数据,采集规模和频率严格控制在合理范围内,遵循平台公开接口的使用规范。个人做数据分析项目,核心是锻炼全流程能力,不是去搞大规模抓取。
1.2 这套系统到底解决什么问题
先拆一下需求。青少年模式这个场景,天然有三类人关心它:第一类是平台产品团队,想知道功能渗透率和用户黏性;第二类是家长或教育工作者,想知道孩子开启模式后到底在看什么;第三类是内容运营,想知道哪些分区的内容在青少年模式下更受欢迎。
对应的,这套系统需要输出四个层次的能力:
- 数据采集能力:按时间、用户分组、内容分区等维度采集浏览记录和互动行为。
- 指标分析能力:计算青少年模式开启率、日均使用时长、活跃时段分布、内容偏好指数等核心指标。
- 可视化展示能力:把分析结果变成折线图、饼图、热力图、词云等直观图表,支持按时间范围筛选。
- 结论输出能力:能把分析结果整理成结构化报表,比如“周末下午是青少年模式使用高峰”这种可读的结论。
我当时做这个项目还有一个私心,就是想完整走一遍“数据采集—数据清洗—分析建模—可视化展示”的流程。很多教程只讲单一环节,要么只教爬虫,要么只教Pandas,要么只教画图,真正把全链路串起来的项目很少。这个项目刚好能把Python生态系统里的 Requests、Pandas、PyECharts 都串起来用一遍。
1.3 适合谁看这个项目
如果你是刚学完Python基础、想找一个综合性项目练手的同学,或者做数据分析但一直停留在Kaggle练习题阶段、想接触真实业务场景的从业者,再或者想了解B站内容生态的研究型用户,这个项目的拆解思路都能给你一些参考。
项目用到的技术栈不复杂:Python 3.8 以上版本、Requests 负责数据获取、Pandas 做清洗聚合、PyECharts 生成可视化图表、Flask 搭一个简单的展示页面。没有用到机器学习,也没有分布式计算,门槛控制在“会Python基础语法 + 会看文档”就能跟得上的程度。
2. 系统架构与核心指标体系设计
2.1 总体架构:四层分离
整个系统我拆成了四个模块,每个模块之间通过接口解耦,方便单独调试。
数据采集层负责从公开接口获取行为记录,包括观看日志、点赞收藏行为、视频元数据。这里有一个设计要点:采集层和存储层之间用一个统一的DataFrame结构传递数据,字段命名提前定好,避免后续清洗时来回改。
数据存储层用CSV文件做持久化就够了,因为数据量级在万级别,SQLite 是更稳妥的选择。我实际用的是SQLite,因为后续要做按日期范围筛选,用SQL查询比全量读入CSV再过滤要清爽得多。
分析计算层是系统的核心,所有指标都在这一层算好,输出成汇总表和透视表。设计原则是:分析层只产出指标数据,不关心图表怎么画;可视化层只消费指标数据,不重复计算。这样哪块出问题,排查范围马上缩小。
可视化展示层用Flask + PyECharts实现,后端提供JSON接口返回指标数据,前端用ECharts渲染图表。这样做的好处是,图表和数据分析逻辑完全解耦,后面换Dashboard框架不影响核心分析代码。
2.2 数据字段设计
我设计的核心行为表叫watch_logs,字段如下:
| 字段名 | 类型 | 说明 |
|---|---|---|
| record_id | TEXT | 唯一标识 |
| user_group | TEXT | 用户分组(家长/青少年/普通用户) |
| watch_date | DATETIME | 观看时间 |
| watch_minutes | INTEGER | 观看时长(分钟) |
| content_partition | TEXT | 内容分区(动画/影视/知识/游戏等) |
| video_duration | INTEGER | 视频总时长(秒) |
| mode_status | TEXT | 是否处于青少年模式 |
| interaction_type | TEXT | 互动类型(无/点赞/收藏/分享) |
字段设计的核心逻辑是:所有后续分析需要用到的维度,在采集阶段就必须定好。比如后面要分析“青少年模式下用户在看什么分区”,那content_partition和mode_status这两个字段缺一不可;要分析“什么时段的开启率高”,watch_date必须记录到分钟级别。
2.3 关键指标体系定义
这套系统的指标体系我定义为三个层级:
第一层:规模指标。样本活跃用户数、行为记录总数、人均观看时长。这些是分母,所有比例指标都建立在它们之上。
第二层:效率指标。青少年模式开启率、时段活跃度、分区偏好指数。开启率的计算方法是:
青少年模式开启率 = 青少年模式下产生的行为记录数 / 总行为记录数 × 100%分区偏好指数稍微讲究一点,公式是:
分区偏好指数 = 该分区在青少年模式下的观看时长占比 / 该分区在普通模式下的观看时长占比这个指标的意义在于消除分区本身热度的影响。比如游戏分区在普通模式下本来就热门,直接看占比会有误导性。用比值的方式,指数大于1说明青少年模式下这个分区被“放大”了,小于1说明被“抑制”了。这是这个项目里比较有价值的一个分析角度。
第三层:深度指标。包括模式切换频次、单次模式的持续时长、内容消费的深度(比如视频完播率)。这些指标计算起来比较复杂,需要把用户的行为记录按时间排序,识别连续处于同一模式的时间段,这是分析阶段的重点和难点。
3. 数据采集与清洗的实战细节
3.1 合规采集的三个原则
做这类项目,先讲清楚数据来源的合规性问题。个人学习项目采集数据,必须遵守三条原则:第一,只使用平台公开的接口,不碰任何需要越权访问的数据;第二,控制采集频率,单次请求间隔至少1.5秒,一天总量控制在合理范围内;第三,数据仅用于个人学习和研究,不对外发布原始数据,展示时做脱敏处理。
我在项目里采用的策略是“模拟数据为主、公开数据校验为辅”。先用Python构造一批符合业务逻辑的模拟行为数据,再用小规模的真实观测数据做对比验证,确保分析结论不是编造的。这样既练到了完整的分析流程,又不踩合规红线。
3.2 采集模块的实现
采集模块用Requests库请求公开接口,核心代码如下:
import requests import time import pandas as pd session = requests.Session() session.headers.update({ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", "Accept": "application/json, text/plain, */*", "Referer": "https://www.bilibili.com" }) def fetch_behavior_logs(params, retry_times=3): url = "https://api.bilibili.com/x/web-interface/archive/stat" for attempt in range(retry_times): try: resp = session.get(url, params=params, timeout=10) resp.raise_for_status() json_data = resp.json() if json_data["code"] == 0: return json_data["data"] else: print(f"接口返回错误: {json_data['message']}") except Exception as e: print(f"第{attempt + 1}次请求失败: {e}") time.sleep(2 ** attempt) return None这里有几个细节值得注意。一是必须设置Referer头,B站的接口会校验来源,没有Referer很容易拿不到数据。二是超时重试用指数退避策略,不要失败后立刻猛刷,既对服务器不友好,也容易被限制访问。三是把session对象复用起来,不要每次请求都新建连接,效率差很多。
采集到的原始数据长什么样呢?基本结构是JSON数组,每条记录包含视频ID、标题、分区ID、发布时间等字段。我需要把这些原始字段映射成项目定义的字段结构,比如把分区ID转换成分区名称,把时间戳转换为标准的日期时间格式。
3.3 数据清洗的四个步骤
原始数据拿下来之后,不能直接进分析环节,必须先清洗。我总结了四个必经步骤:
去重。采集过程中因为重试机制,可能会有重复记录。判断重复的依据是record_id字段,直接用Pandas的drop_duplicates处理。
缺失值处理。观看时长为空的情况,分两种处理:如果整条记录缺失超过30%的字段,直接删除;如果只是watch_minutes为空且能通过视频时长推算,就补充估算值。
异常值过滤。这个坑我踩过。有些记录显示观看时长超过24小时,明显是异常数据。我的规则是:观看时长超过视频总时长两倍的记录直接标记为异常并剔除,因为正常播放不可能出现这种情况。此外,未来时间戳的记录也是异常的,watch_date超过当前时间的全部过滤掉。
格式统一。时间字段统一转成datetime类型,时长字段统一转成分钟单位的整数,分区名称统一映射成标准名称。这一步不做的话,后面画图时会出现坐标轴乱序、数值单位不一致的麻烦。
def clean_data(df): df = df.drop_duplicates(subset=["record_id"]) df = df.dropna(thresh=8) df = df[df["watch_date"] <= pd.Timestamp.now()] df = df[df["watch_minutes"] <= df["video_duration"] / 60 * 2] df["watch_date"] = pd.to_datetime(df["watch_date"]) df["hour"] = df["watch_date"].dt.hour df["weekday"] = df["watch_date"].dt.weekday return df清洗之后的数据,才是真正能用于分析的“干净数据”。
4. 核心分析方法与结论产出
4.1 使用率画像:从整体到分群
第一个分析任务是绘制一张“用户使用率全景图”。整体开启率直接算比例,分群之后看差异。
我用groupby做分组聚合,按user_group和mode_status两个维度交叉统计:
usage_pivot = df.pivot_table( index="user_group", columns="mode_status", values="record_id", aggfunc="count", fill_value=0 ) usage_pivot["开启率"] = ( usage_pivot["青少年模式"] / (usage_pivot["青少年模式"] + usage_pivot["普通模式"]) * 100 )分群分析发现一个有意思的现象:家长模拟群组的开启率接近100%,但周均使用时长很低,说明家长更多是“开启后监督使用”,实际消费内容的时间并不多。青少年真实使用群组的开启率反而只有30%左右,但一旦开启,单次使用时长显著高于普通模式。这说明青少年模式的用户黏性其实不低,瓶颈在前端触达,而不是功能本身。
这个结论如果只看整体开启率,是看不出来的。这就是分组分析的价值所在。
4.2 时段活跃度分析
时段分析是这次项目的重头戏。我把一天24小时按小时分桶,统计每个小时的观看行为数量,分别画出青少年模式和普通模式的曲线。
计算逻辑很简单:
hourly_active = df.groupby(["hour", "mode_status"]).size().unstack(fill_value=0)但解读曲线才是关键。对比两条曲线我发现,普通模式的高峰在晚间20点到23点,典型的“睡前刷视频”节奏;青少年模式的高峰明显前移,出现在18点到20点,而且上午7点到9点还有一个小的早高峰,这对应的是上学前的碎片时间。
更值得关注的结构化差异在周末。按星期几分组后,青少年模式在周六9点到11点出现一个强烈的峰值,普通模式则没有这个特征。用专业一点的话说,青少年模式的活跃时段与“未成年人的作息节奏”高度相关,这其实是产品设计上可以优化的点——比如晚间21点后是否需要更严格的时长提醒。
4.3 内容偏好:用偏好指数找差异
内容偏好分析是用户最感兴趣的部分,也是能输出“有价值结论”的部分。
先按分区聚合观看时长,再分别计算青少年模式和普通模式下的时长占比:
partition_stats = df.groupby(["content_partition", "mode_status"])["watch_minutes"].sum().unstack(fill_value=0) partition_stats["占比"] = ( partition_stats["青少年模式"] / partition_stats["青少年模式"].sum() * 100 )然后计算偏好指数。当时算出来的一组结果是:知识区在青少年模式下的偏好指数是1.8,而时尚区的偏好指数是0.5。这说明青少年模式下内容消费明显偏向学习型、知识型内容,娱乐型时尚类内容的消费被显著压缩。这个结果和B站官方对青少年模式的“内容筛选”定位是吻合的。
当然,这里也要说明一个边界:偏好指数只能说明“开启模式后看了什么”,不能说明“开启模式的用户本来就喜欢什么”,这是两个问题。分析结论要谨慎下,不能过度解读。
4.4 模式切换行为路径分析
最后做的一个深度分析是模式切换路径。我把每个用户的行为记录按时间排序,找到mode_status字段值变化的节点,统计从普通模式切换到青少年模式的频次和间隔。
这个分析的价值在于回答一个问题:用户是“开一次就再也不关”,还是“频繁切换”?
实现思路:
df_sorted = df.sort_values(["user_id", "watch_date"]) df_sorted["mode_shift"] = df_sorted["mode_status"] != df_sorted["mode_status"].shift(1) shift_count = df_sorted.groupby("user_id")["mode_shift"].sum()统计结果显示:超过六成的用户切换次数小于等于2,说明大多数用户对青少年模式的态度是“要么基本不用,要么长期开着”。频繁切换(超过5次)的用户占比只有12%,但这部分用户的整体使用时长很高,是值得后续深入分析的群体。
5. 可视化系统设计与实现
5.1 可视化框架选型对比
可视化框架的选择,我做了三个候选方案对比:
| 候选方案 | 优势 | 劣势 | 适用场景 |
|---|---|---|---|
| PyECharts + Flask | 图表类型丰富、交互性强、社区资源多 | 需要写前后端衔接代码 | 本项目选择 |
| Streamlit | 开发效率极高、原生支持交互组件 | 页面定制能力受限、部署较重 | 快速原型验证 |
| Plotly Dash | 交互复杂、适合数据产品 | 学习曲线陡峭 | 规模化BI应用 |
最后选了PyECharts + Flask。原因很简单:PyECharts生成的是纯JavaScript渲染的图表,和Flask的JSON接口配合非常自然,而且网上关于PyECharts + Flask的案例很多,遇到问题容易搜到解决方案。
5.2 数据接口层设计
Flask后端主要负责两件事:一是托管index.html页面,二是提供数据接口。
我设计了两个核心接口:
@app.route("/api/summary") def api_summary(): result = compute_summary_metrics(df) return jsonify(result) @app.route("/api/hourly_trend") def api_hourly_trend(): result = compute_hourly_trend(df, mode=request.args.get("mode", "all")) return jsonify(result)接口返回的数据格式统一为{"categories": [...], "series": [...]},前端直接消费。这种格式约定从一开始就定好,前后端联调的时候很省心。
5.3 核心图表的配置与实现
这一部分重点讲三个图表的实现细节。
折线图:时段活跃度对比。
from pyecharts.charts import Line from pyecharts import options as opts line_chart = ( Line() .add_xaxis(hours) .add_yaxis("青少年模式", youth_active, is_smooth=True, linestyle_opts=opts.LineStyleOpts(width=3)) .add_yaxis("普通模式", normal_active, is_smooth=True, linestyle_opts=opts.LineStyleOpts(width=3)) .set_global_opts( title_opts=opts.TitleOpts(title="B站不同模式下时段活跃度对比"), legend_opts=opts.LegendOpts(pos_top="5%"), xaxis_opts=opts.AxisOpts(name="小时", type_="category", boundary_gap=False), yaxis_opts=opts.AxisOpts(name="行为记录数", type_="value"), tooltip_opts=opts.TooltipOpts(trigger="axis") ) )注意两个细节:一是曲线图要设置boundary_gap=False,让折线从坐标轴原点开始延伸,更符合时间序列的阅读习惯;二是tooltip设置成axis触发,鼠标悬停时同时显示两条曲线的数值,方便对比。
热力图:星期×时段活跃矩阵。
from pyecharts.charts import HeatMap heat_data = [ [weekday, hour, count] for weekday, hour, count in weekly_hourly_data ] heatmap = ( HeatMap() .add_xaxis(["周一","周二","周三","周四","周五","周六","周日"]) .add_yaxis("活跃度", hours, heat_data, label_opts=opts.LabelOpts(is_show=False)) .set_global_opts( title_opts=opts.TitleOpts(title="青少年模式周活跃热力图"), visualmap_opts=opts.VisualMapOpts(min_=0, max_=max_count, is_piecewise=False) ) )热力图在展示“一周哪个时段最活跃”时特别直观。颜色的深浅直接把高峰时段和低谷时段“画”出来,不需要解释。这个图表放在仪表盘的中央位置,视觉冲击力很强。
环形图:内容分区占比。
from pyecharts.charts import Pie pie_chart = ( Pie() .add("", [(partition, percentage) for partition, percentage in partition_data], radius=["40%", "70%"], center=["50%", "50%"], label_opts=opts.LabelOpts(formatter="{b}: {d}%")) .set_global_opts( title_opts=opts.TitleOpts(title="青少年模式内容分区分布"), legend_opts=opts.LegendOpts(pos_bottom="0%") ) )环形图相比普通饼图的优势是中心区域留白,可以放置占比最高的分区名称或汇总指标,信息密度更高。
5.4 Dashboard整体布局
实际交付的Dashboard页面是深色科技风,左侧是数据总览卡片(样本量、平均观看时长、开启率),中间主体区域放折线图,右上角放环形图,下方整行放热力图。
页面顶部设置了一个时间范围筛选器,通过Ajax请求重新拉取数据并刷新图表。这个交互虽然简单,但能让用户直观感受到“筛选条件变化→图表联动更新”的完整链路,比静态图表展示上了一个档次。
关于图表配色的经验是:深色背景配亮色系(橙色、青色)比浅色背景更出效果。B站本身的品牌色是粉色系,但深色底上用粉色会显得对比度不足,我推荐用青色和橙色的组合,视觉对比强烈,长时间盯屏也不累。
6. 常见问题与排查技巧实录
6.1 数据采集阶段的三个坑
第一个坑是接口返回的字段嵌套太深。B站接口的JSON结构通常是三层嵌套,直接pandas.DataFrame(json_data["data"])拿到的往往是嵌套字典的Series,需要手动展开parse嵌套字段。
第二个坑是请求频率控制不当。一开始我用了0.3秒的请求间隔,结果跑了十几分钟就被暂时限制了访问。后来把间隔调整到1.5秒,并且加入随机扰动,情况才稳定下来。我的经验是:模拟成人的操作节奏,比疯狂刷请求要可持续得多。
第三个坑是字符编码。B站接口返回的中文内容在部分环境下会出现乱码。解决方法是请求时显式声明编码:
resp.encoding = "utf-8"这个看似简单的步骤,能省下大量事后清洗的精力。
6.2 数据处理阶段的两个疑难杂症
Pandas的SettingWithCopyWarning是新手必踩的坑。做数据清洗时直接对切片后的DataFrame赋值,会触发这个警告,而且赋值结果是不确定的。正确做法是使用.copy()明确复制:
df = df[df["mode_status"].notna()].copy() df.loc[:, "mode_status"] = df["mode_status"].fillna("未知")时间聚合的边界问题。按小时聚合时,边界归属不清晰会导致前后两天数据错位。比如23:59的记录应该归到哪个小时?我的处理方式是先统一转换为小时级别的时间戳,再做groupby,确保不会出现跨天的边界误差。
6.3 可视化环节的踩坑记录
PyECharts版本升级之后,部分API参数发生了变化。最典型的就是LabelOpts和TitleOpts的写法,旧版本是字符串配置,新版本改为对象配置。我在调试过程中发现1.1版和2.0版的代码不能直接通用,解决方案是锁定环境版本,在requirements.txt里固定常用库的版本号。
另一个高频问题是图表渲染不出内容,但接口返回的数据是正常的。排查后发现是dataz格式不一致——接口返回PV integers,前端拿到后直接放进categories,导致ECharts无法识别。解决方法是统一用字符串类型的坐标类目,数值型数据放在series里。
6.4 做这类项目的心态建议
这个项目从数据采集到最终展示,完整周期大约是三天。第一天搭框架和采集,第二天做清洗和分析,第三天做可视化和调样式。整个项目最花时间的其实不是写代码,而是反复调整图表展示的细节——坐标轴标签旋转角度、图例位置、留白比例,这些细节决定了一个Dashboard是“能看”还是“好看”。
还有一个建议是善用Jupyter Notebook做探索性分析。先在这儿把指标算出来、把图表画一遍,确认结论没问题了,再迁移到Flask系统里做正式展示。直接在业务系统里调试分析逻辑,效率太低,排查问题也不方便。
7. 扩展思路与最后的经验总结
7.1 后续可以怎么扩展
这套系统的架构本身不挑领域,换个数据源就能做新的分析项目。比如把B站换成其他视频平台,把青少年模式换成“免密支付模式”或“深色模式”,整个分析思路依然成立。
技术维度上也有两个明确的升级路径。一是把Flask换成Streamlit,开发效率还能再上一个台阶;二是把Pandas换成Polars或DuckDB,处理百万级以上的数据时性能提升会非常明显。如果后续要处理更大规模的数据,可以考虑引入Spark做分布式计算,但这个项目的数据量级在万级,Pandas完全够用,不需要杀鸡用牛刀。
7.2 个人实操的最终体会
做完这个项目,我最大的感受是:数据分析项目真正的门槛不在工具,而在“问题意识”。Python语法可以现查,Pandas函数可以查文档,但“青少年模式的开启率和时段活跃度之间存在什么关系”这个问题,不是搜索引擎能告诉你的。把业务问题翻译成可计算的指标,再把指标变成可视化的图表,这个过程才是项目的灵魂。
第二个感受是:任何分析结论都要有边界意识。我看到自己在偏好指数分析里得出的结论时,第一反应是兴奋,第二反应是警惕——样本量够吗?口径统一吗?是否混淆了相关性和因果性?带着这种审视态度,分析结论才能经得起推敲。
如果这个项目能给你一些启发,我建议别照搬代码,而是找到你自己感兴趣的数据场景,按这套流程做一遍。踩一遍数据清洗的坑,体验一次图表联调的心烦,然后再收获一个自己亲手做出来的完整项目,这种成就感是看教程完全体会不到的。