简介这是一套面向课程设计与Python后端学习者的B站用户行为分析系统完整项目源码适合数据科学、机器学习方向的学生与开发者作为实战案例参考。项目围绕观看历史、弹幕、评论等行为数据展开涵盖爬虫采集、Pandas数据处理、Matplotlib可视化并尝试用聚类与关联规则挖掘用户偏好最终输出用户画像与内容推荐列表配套界面可查看分析报告。资源包共427个文件约72.41MB以png图表、js脚本、html页面、py源码及pyc缓存为主另含css样式、svg图标、sql建表脚本、pkl模型文件与doc说明文档前后端与数据模块划分清晰。已有252人学习下载可帮助读者快速理解从数据抓取到分析展示的完整链路并借鉴其目录组织与排错思路。1. 从一份 B 站用户行为数据说起这套分析系统到底在算什么很多人第一次接触「B站用户行为分析系统」这个词脑子里浮现的是爬虫抓弹幕、画几张词云图就完事。真做过一轮的人会告诉你难点根本不在爬而在「行为」两个字怎么定义。一个用户点开视频、拖进度条、发弹幕、投币、收藏、充电这些动作在时间轴上的分布才是行为分析真正要处理的对象。这套基于 Python 的系统核心目标是把散落在播放页、评论区、动态流里的原始事件整理成能回答「谁在看、看到哪、为什么走」的结构化指标。它适合三类人做内容运营想搞清楚完播率为什么掉的人、学 Python 数据分析想找一个真实数据集练手的人、以及需要给推荐或风控模块提供特征输入的工程师。标题里带「.zip」说明交付形态是一份可解压运行的工程而不是一篇论文。所以这篇笔记不聊虚的直接按「数据从哪来 → 怎么存 → 怎么算 → 怎么避坑 → 怎么验证」的顺序把一套能跑起来的最小闭环讲清楚。你不需要先成为爬虫高手但得愿意动手改参数、看日志、对数据。2. 行为数据从哪来接口、字段与采集边界2.1 先想清楚要采哪几类行为事件B 站用户行为分析系统里最容易被低估的一步是事件建模。新手常犯的错是「先把能抓的都抓下来」结果字段几百个真正能进分析管道的不到十个。我一般会把行为事件收敛成四类曝光类视频出现在推荐流/搜索页、播放类开始播放、暂停、拖拽、完播、互动类点赞、投币、收藏、弹幕、评论、转化类关注、充电、分享。这四类对应分析里的漏斗层级缺一层后面的留存和归因就接不上。字段设计上每条事件至少要有用户标识uid 或脱敏后的 hash、视频标识bvid、事件类型、事件时间戳毫秒级、以及一个上下文对象来源页面、设备类型、是否登录。时间戳精度很关键秒级精度做不了拖拽行为分析因为一次拖拽可能在 800 毫秒内完成。常见做法是用time.time() * 1000取毫秒落库时统一存整数避免浮点误差在聚合时放大。提示uid 属于个人信息落库前做一次不可逆哈希分析用哈希值展示层不还原。这不是可选项是底线。2.2 用 Python 写一个最小采集与清洗脚本采集环节我不建议一上来就上分布式先用单机把一条链路跑通。下面这段代码演示的是「读取一批原始 JSON 事件 → 清洗 → 输出成分析用的 DataFrame」原始数据可以来自你已有的日志文件或公开数据集重点是清洗逻辑本身。import json import hashlib import pandas as pd from datetime import datetime def hash_uid(uid: str) - str: # 对 uid 做加盐哈希盐值从环境变量读取不写死在代码里 salt bili_behavior_salt return hashlib.sha256((salt str(uid)).encode()).hexdigest()[:16] def clean_event(raw: dict) - dict | None: # 丢弃缺少关键字段的脏数据而不是用默认值填充 required [uid, bvid, event_type, ts] if any(k not in raw or raw[k] in (None, ) for k in required): return None ts int(raw[ts]) # 时间戳合理性校验早于 2015 年或晚于当前时间 1 天的直接丢 if ts 1420041600000 or ts int(datetime.now().timestamp() * 1000) 86400000: return None return { uid_hash: hash_uid(raw[uid]), bvid: raw[bvid], event_type: raw[event_type], event_ts: ts, source: raw.get(source, unknown), device: raw.get(device, unknown), } def load_events(path: str) - pd.DataFrame: rows [] with open(path, r, encodingutf-8) as f: for line in f: line line.strip() if not line: continue try: raw json.loads(line) except json.JSONDecodeError: continue # 单行解析失败不影响整批 cleaned clean_event(raw) if cleaned: rows.append(cleaned) df pd.DataFrame(rows) df[event_ts] pd.to_datetime(df[event_ts], unitms) return df if __name__ __main__: df load_events(raw_events.jsonl) print(df.shape) print(df[event_type].value_counts())逻辑说明hash_uid用加盐 SHA256 截前 16 位既保证同一用户可关联又不可逆推。clean_event采用「缺字段即丢弃」策略而不是填默认值因为行为分析里一条时间戳错误的记录比少一条记录危害大得多。load_events逐行解析单行 JSON 出错就跳过保证批量任务不会因为一行脏数据整体失败。参数说明盐值bili_behavior_salt实际部署时应从环境变量注入时间戳下限1420041600000对应 2015-01-01可按你的数据范围调整event_type的取值建议提前定义枚举避免同一行为出现「click」「Click」「CLICK」三种写法。2.3 采集频率与限速的现实取舍做 B 站用户行为分析绕不开请求频率问题。我的血泪经验是不要试图在短时间内拉全量而是按「热点视频优先、长尾视频抽样」的策略分层采集。热点视频比如榜单前 500可以高频更新长尾视频按天抽样即可。单机采集时请求间隔设 1 到 2 秒配合指数退避重试比硬扛限速要稳得多。如果你只是做分析练手直接用官方公开的脱敏数据集或自己构造的模拟日志能省掉大量和接口斗智斗勇的时间。3. 把行为存成能算的结构表设计与聚合管道3.1 事件表、会话表、指标表的三层拆分数据存不好后面算什么都别扭。我一般拆三张表事件表明细一行一个动作、会话表把同一用户 30 分钟内的连续行为切成一个 session、指标表按视频/按天聚合的宽表。事件表用列式存储Parquet比 CSV 快一个量级尤其是做时间范围扫描的时候。会话表的关键是「30 分钟」这个阈值它来自通用的会话切分惯例但 B 站场景下可以调到 20 分钟因为短视频消费的间隔更短。指标表要提前想好维度视频维度bvid、播放量、完播率、互动率、用户维度uid_hash、活跃天数、行为分布、时间维度小时、天、周。三张表之间用 bvid 和 uid_hash 关联不要用自增 ID否则跨表 join 时容易错位。表名粒度主要字段存储格式event_detail单条行为uid_hash, bvid, event_type, event_tsParquetuser_session单次会话session_id, uid_hash, start_ts, end_ts, event_countParquetvideo_metrics视频×天bvid, dt, play_cnt, finish_rate, interact_rate数据库表3.2 用 Pandas 做会话切分与完播率计算会话切分是行为分析里最容易写错的一段。核心逻辑是按用户分组按时间排序当前事件与上一条事件间隔超过阈值就开新会话。下面这段代码可以直接抄。import pandas as pd SESSION_GAP_MS 20 * 60 * 1000 # 20 分钟无行为则切分会话 def build_sessions(df: pd.DataFrame) - pd.DataFrame: df df.sort_values([uid_hash, event_ts]).copy() # 计算同一用户相邻事件的时间差 df[gap] df.groupby(uid_hash)[event_ts].diff().dt.total_seconds() * 1000 # gap 为空每个用户第一条或超过阈值标记为新会话 df[is_new_session] (df[gap].isna()) | (df[gap] SESSION_GAP_MS) df[session_seq] df.groupby(uid_hash)[is_new_session].cumsum() df[session_id] df[uid_hash] _ df[session_seq].astype(str) sessions df.groupby(session_id).agg( uid_hash(uid_hash, first), start_ts(event_ts, min), end_ts(event_ts, max), event_count(event_type, count), ).reset_index() return sessions def calc_finish_rate(df: pd.DataFrame) - pd.DataFrame: # 完播定义同一用户同一视频出现 finish 事件或播放时长 视频时长的 90% play df[df[event_type].isin([play, finish])] agg play.groupby(bvid)[event_type].apply( lambda s: (s finish).sum() / max((s play).sum(), 1) ).reset_index(namefinish_rate) return agg逻辑说明build_sessions先用diff算相邻事件毫秒差再用cumsum给每个新会话打递增序号拼出唯一 session_id。calc_finish_rate用 finish 事件数除以 play 事件数分母用max(..., 1)防止除零。参数说明SESSION_GAP_MS是唯一需要按业务调的参数短视频场景可降到 15 分钟长视频可升到 30 分钟完播率的分母如果换成「去重用户数」而不是「播放次数」得到的是另一套指标两者不要混用。3.3 聚合管道的调度与增量更新全量重算在数据量上来之后会变得不可接受。常见做法是按天分区每天只重算最近 7 天的会话和指标因为会话切分只依赖相邻事件历史数据不会因为新数据而改变。用 Airflow 或简单的 cron Python 脚本都能做关键是给每张表加dt分区字段重跑时先删对应分区再写入避免重复累加。这一步不做后面所有指标都会偏大而且很难排查。4. 可视化与特征输出让分析结果能被用起来4.1 用 Matplotlib 画留存曲线和漏斗分析结果如果只停在 DataFrame 里运营和产品是用不起来的。最实用的两张图是留存曲线和行为漏斗。留存曲线按「首次行为日期」分组看后续每天的活跃比例漏斗按「曝光 → 播放 → 互动 → 转化」四层算转化率。下面这段代码画漏斗留存曲线同理只是分组维度换成日期差。import matplotlib.pyplot as plt def plot_funnel(df: pd.DataFrame, bvid: str): stages [exposure, play, interact, convert] sub df[df[bvid] bvid] counts [sub[sub[event_type] s][uid_hash].nunique() for s in stages] fig, ax plt.subplots(figsize(8, 4)) ax.barh(stages, counts, color#4C72B0) for i, c in enumerate(counts): ax.text(c, i, f {c}, vacenter) ax.set_xlabel(unique users) ax.set_title(ffunnel for {bvid}) plt.tight_layout() plt.savefig(ffunnel_{bvid}.png, dpi150)逻辑说明用nunique而不是count因为漏斗每一层要的是去重用户数同一用户多次播放只算一次。barh横向条形图比纵向更适合展示阶段名称较长的漏斗。参数说明dpi150是发布到文档的清晰度下限屏幕展示 100 即可如果阶段名称要中文化记得先设置plt.rcParams[font.sans-serif]否则中文会显示成方块这是新手最常见的翻车点之一。4.2 把指标导出成推荐/风控可用的特征如果这套系统的下游是推荐或风控指标表要转成「用户×视频」的特征向量。常见特征包括该用户对该视频的播放次数、平均播放进度、互动次数、距上次互动天数。导出时用 Parquet 按天分区特征名统一小写下划线风格缺失值用 -1 而不是 0因为 0 在树模型里是有意义的取值用 0 填缺失会引入偏差。这一步的细节决定了特征能不能直接用而不是还要下游再洗一遍。5. 避坑与排查这套系统最容易翻车的五个地方5.1 时间戳单位混用导致会话全乱现象会话数量比预期多出几倍每个会话只有一两条事件。原因一部分数据用秒级时间戳一部分用毫秒级diff算出来的间隔忽大忽小。解决在清洗阶段统一转成毫秒整数并加一条断言assert df[event_ts].max() 1e12秒级时间戳不会超过这个量级能在入口就拦住。5.2 完播率分母用了播放次数而非去重用户现象完播率超过 100%。原因同一用户重复播放被多次计入分母而 finish 事件只记一次。解决分母改用nunique去重用户数或者明确把指标命名为「完播次数/播放次数」并在文档里写清口径两者不能混叫「完播率」。5.3 中文图表乱码让整张图报废现象图表里所有中文变成方框。原因Matplotlib 默认字体不含中文字形。解决在绘图前设置plt.rcParams[font.sans-serif] [SimHei]和plt.rcParams[axes.unicode_minus] FalseLinux 环境下如果没有 SimHei换成Noto Sans CJK SC并确认字体已安装。5.4 增量重算时忘记删分区导致指标翻倍现象某天指标突然变成前一天的近两倍。原因重跑任务时直接 append没有先清空对应dt分区。解决写入前执行DELETE FROM table WHERE dt 目标日期或者用modeoverwrite配合分区列写入让存储层自己处理覆盖。5.5 uid 未脱敏直接落库现象代码 review 时被安全同学拦下。原因原始 uid 属于个人信息直接存明文违反最小必要原则。解决入口处统一哈希盐值走环境变量分析全程只用哈希值。这件事没有后悔药必须在第一版就做对后期补做要重刷全量历史数据。6. 验证分析结果是否可信三个可复现的校验技巧系统跑通不等于结果可信。我一般用三个校验动作来确认数据没骗我。第一个是「总量对齐」把事件表的记录数按天汇总和采集端日志的行数对比差异超过 1% 就要查是哪段清洗逻辑丢多了。第二个是「抽样回放」随机抽 20 个 session把原始事件按时间打印出来人工看会话切分是否符合直觉这一步能抓出阈值设错的问题。第三个是「指标交叉验证」用两套独立实现算同一个完播率比如一套用 Pandas一套用 SQL结果对不上就说明有一边的口径理解错了。def validate_totals(raw_path: str, df: pd.DataFrame): raw_lines sum(1 for _ in open(raw_path, encodingutf-8)) cleaned len(df) drop_rate 1 - cleaned / raw_lines print(fraw{raw_lines}, cleaned{cleaned}, drop_rate{drop_rate:.2%}) # 丢弃率超过 5% 需要人工确认清洗规则是否过严 assert drop_rate 0.05, drop rate too high, check clean_event rules这段校验代码放在每次批量任务末尾丢弃率超过 5% 直接让任务失败而不是静默通过。参数上5% 这个阈值对小数据集可以放宽到 10%但生产环境建议收紧到 2%。最后一个习惯任何指标上线前我都会先在一个已知答案的小数据集上跑一遍比如手工构造 100 条事件、预期完播率 40%跑出来对不上就先修代码再谈业务。这个习惯帮我省掉了无数次「指标看着合理但其实是错的」的尴尬。希望帮到你。本文还有配套的精品资源点击获取
