)
上个月一个做内容运营的朋友发来几千行抖音视频数据表格让我帮她做一份可视化分析。表格里的字段很全播放量、点赞、评论、分享、收藏全都有但问题恰恰出在“太全”上——几千行原始数据堆在一起根本看不出哪个视频值得继续投入。我顺手把处理过程整理成了一个通用系统基于 Python 的抖音视频数据分析与可视化系统。这套系统不搞花哨算法核心就三件事清洗原始数据、计算关键指标、输出可交互图表。如果你正在找 Python 数据分析项目的参考或者想给自己负责的账号做一次数据复盘这篇内容应该能帮你省掉不少弯路。标题里说的“附源码”我会在文中把核心代码拆开讲不是只给一个空壳。系统定位其实很明确它是一个面向短视频运营复盘场景的分析工具同时也是一个很典型的 Python 数据分析练手项目。后端用 pandas 做清洗和聚合前端用 pyecharts 和 wordcloud 出图再用 Flask 拼成一个本地看板。全程不依赖重型框架一台普通电脑就能跑起来。1. 项目设想从一份乱糟糟的表格说起1.1 为什么表格越大信息量反而越少一个很反直觉的现象是运营后台导出的数据越详细运营者越不知道从哪看起。几十列字段、上千行记录横着看是人名竖着看是数字唯一能做的操作就是按播放量降序排一下然后就没有然后了。问题不是数据不够多而是缺少两步处理第一把原始字段转成有业务含义的指标第二把指标用图表呈现成可对比的结构。比如播放量 80 万和播放量 50 万单独看没有意义。但如果你算一下互动率发现 80 万播放的视频互动率反而只有 0.3%而 50 万播放的视频互动率有 3%那结论就完全不一样了。前者大概率是推荐流量灌进来的泛受众后者才是真正对标的垂类人群。这就是指标换算的价值。1.2 系统整体拆成三个模块我把整个项目拆成三块分别处理数据、分析和展示数据模块负责读取原始 CSV、构造模拟数据、做字段校验。这块用 pandas 完成输出一份统一口径的干净 DataFrame。分析模块负责计算互动率、完播率、发布时段、标签词频等指标。这块用 pandas 的 groupby 加 jieba 分词配合完成。展示模块负责把分析结果做成 HTML 图表再用 Flask 拼成看板。这块用 pyecharts 生成图表文件wordcloud 生成词云图。三个模块互相独立随便替换其中一部分都不会影响另外两部分。比如你已经有一份整理好的 Excel那直接把数据模块换成pd.read_excel就行分析和展示不用动。1.3 数据从哪里来一个必须提前说清楚的边界很多人看到“抖音数据分析”第一反应是“写个爬虫去抓”。这里必须把边界讲清楚这套系统本身不包含任何爬虫逻辑也不调用平台私有接口。我测试时用的数据有两类。一类是运营者自己在后台导出的作品数据这是合法且最贴近真实场景的数据来源另一类是按真实业务规律构造的模拟数据用于在没有任何私有数据的情况下演示整条分析链路。这篇文里的所有演示都会基于模拟数据你真实使用时只要把自己的表格换成同样结构的 CSV 就行。这样设计的原因很实际爬虫接口不稳定、有合规风险而且会让教学项目偏离数据分析主线。数据分析和可视化系统的价值在于“怎么把已经拿到的数据用起来”而不是“怎么绕过限制去拿数据”。2. 数据字典和指标体系先想清楚要算什么2.1 字段设计是后面所有计算的起点动手写代码之前我先把表格字段固定下来。这个步骤看起来不起眼但决定了后面所有代码能不能复用。本项目使用的核心字段如下字段名含义示例值说明video_id视频唯一编号v_1001去重用title视频标题周末露营装备清单用于分词tags话题标签露营|装备|周末用竖线分隔publish_time发布时间2024-06-01 18:30用于时段分析play_count播放量125000整数like_count点赞数8200整数comment_count评论数680整数share_count分享数510整数favorite_count收藏数2300整数duration视频时长秒数45整数用于时长分桶其中tags字段用竖线分隔是为了方便后面拆分成多个标签。如果你的后台导出的是 JSON 数组或者逗号分隔读进来之后做一次替换即可统一。2.2 三个关键指标的计算口径系统里最核心的指标是下面三个按这三个口径去聚合数据基本能覆盖 90% 的运营复盘需求。互动率点赞 评论 分享 收藏/ 播放量。这个指标衡量的是内容对受众的激发程度。一般短视频的互动率在 1% 到 5% 之间低于 1% 说明内容偏“一次性消费”高于 5% 通常意味着选题切中了用户的表达欲。完播率 完整看完的人数 / 播放量。真实后台会有这个字段模拟数据里我用了 watch_finish_rate 表示。完播率直接反映内容节奏和时长是否匹配30 秒以内的视频完播率明显高于 60 秒以上。发布时段 publish_time 的小时数。这个字段用来观察用户的活跃周期比如晚间 18 点到 21 点发布的视频是否普遍播放量更高。需要注意我不建议对播放量、点赞数这类天生就有量纲差异的字段做简单加权求和。量纲不同相加没有意义准确做法是先归一到同一尺度再比较或者干脆分开统计。这套系统里总量类指标用分组求和效率类指标用比值计算。2.3 用 Python 生成一套可复现的模拟数据没有真实数据时模拟数据是调试管道最好的工具。我用 numpy 生成 1000 条记录分布上做了一点倾向性设计播放量近似长尾分布少数视频播放量很高多数视频集中在均值附近发布时间集中在白天和晚间标签从一组预设词里随机组合。import pandas as pd import numpy as np np.random.seed(42) n 1000 video_ids [fv_{i:04d} for i in range(n)] tag_pool [美食, 旅行, 数码, 健身, 穿搭, 育儿, 职场, 手工, 宠物, 家居] title_templates [分享一个{}的小技巧, 实测{}避坑指南, 关于{}你不知道的3件事, {}入门到放弃] titles [] tags [] publish_hours np.random.choice([8, 9, 12, 13, 18, 19, 20, 21], sizen, p[0.1, 0.1, 0.12, 0.08, 0.2, 0.2, 0.12, 0.08]) for i in range(n): tag np.random.choice(tag_pool, size2, replaceFalse) tags.append(|.join(tag)) titles.append(np.random.choice(title_templates).format(tag[0])) df pd.DataFrame({ video_id: video_ids, title: titles, tags: tags, publish_time: pd.to_datetime(2024-06-01) pd.to_timedelta(np.random.randint(0, 30 * 24, sizen), unith) pd.to_timedelta(publish_hours, unith), play_count: np.random.lognormal(mean10, sigma1.2, sizen).astype(int), like_count: np.random.lognormal(mean7, sigma1.0, sizen).astype(int), comment_count: np.random.poisson(lam20, sizen), share_count: np.random.poisson(lam8, sizen), favorite_count: np.random.poisson(lam30, sizen), duration: np.random.randint(15, 120, sizen), watch_finish_rate: np.random.uniform(0.1, 0.8, sizen).round(3) }) df.to_csv(douyin_demo.csv, indexFalse, encodingutf-8-sig)这里用np.random.lognormal生成播放量是为了模拟真实短视频平台“少数头部视频拿走大部分播放量”的分布特征。如果全程用正态分布那么后面画出来的散点图会是一团均匀的点看不出规律演示效果也不好。3. 清洗与聚合代码pandas 的一次完整实战3.1 第一步先把脏数据挡在外面模拟数据看着很干净但真实后台导出的 CSV 基本都会遇到几个问题数字被读成字符串、时间列格式不统一、有整行重复记录、个别字段为空。我的习惯是读取之后立刻做四步清洗df pd.read_csv(douyin_demo.csv, encodingutf-8-sig) # 1. 去掉重复视频 df df.drop_duplicates(subsetvideo_id, keepfirst) # 2. 数值列统一转成 int空值填 0 numeric_cols [play_count, like_count, comment_count, share_count, favorite_count, duration] for col in numeric_cols: df[col] pd.to_numeric(df[col], errorscoerce).fillna(0).astype(int) # 3. 播放量、点赞数不能为负数 df df[df[numeric_cols].ge(0).all(axis1)] # 4. 时间列统一格式 df[publish_time] pd.to_datetime(df[publish_time], errorscoerce) df df.dropna(subset[publish_time])errorscoerce是处理脏数据时非常顺手的一个参数。它会把无法解析的内容变成NaN之后再统一填 0 或删除而不是让整个程序因为一条坏数据崩掉。3.2 时间字段拆出日、小时才能做趋势和时段分析日期时间这种字段最大的问题是信息一直被包在一个对象里不拆出来就没法聚合。我从publish_time里拆出日期和小时df[publish_date] df[publish_time].dt.date df[publish_hour] df[publish_time].dt.hour df[weekday] df[publish_time].dt.weekday # 0周一 daily df.groupby(publish_date).agg( video_count(video_id, count), total_play(play_count, sum), avg_play(play_count, mean) ).reset_index() hourly df.groupby(publish_hour).agg( avg_play(play_count, mean), avg_like(like_count, mean) ).reset_index()这一步之后daily可以用来画播放量走势hourly可以用来观察哪个时段发视频的平均表现更好。注意这里的avg_play是均值会受到极端头部视频影响所以实际看板里我通常把它的排序结果和median_play一起展示中位数更稳健。3.3 标签拆分和词频统计jieba 的两种用法标签分析和标题词频分析是两条不同的路径。标签本身就是结构化数据直接用竖线拆分即可from collections import Counter tag_series df[tags].str.split(|, expandFalse) tag_counter Counter() for tags in tag_series: for tag in tags: tag_counter[tag.strip()] 1 tag_df pd.DataFrame(tag_counter.most_common(20), columns[tag, count])标题词频则更适合用 jieba 做中文分词。这里不要对标题逐条分词再累加数据库效率太低正确做法是先把所有标题拼接成一个长文本再一次性分词import jieba from collections import Counter all_text .join(df[title].tolist()) words jieba.lcut(all_text) stopwords {分享, 一个, 的, 技巧, 关于, 3, 件事, 入门, 放弃, 避坑, 指南, 实测, 知道, 你, 有} word_counter Counter() for w in words: w w.strip() if len(w) 2: continue if w in stopwords: continue if any(ch.isdigit() for ch in w): continue word_counter[w] 1 word_df pd.DataFrame(word_counter.most_common(30), columns[word, freq])分词里的停用词表非常关键。不人工去掉“技巧”“指南”“分享”这类高频但无意义的模板词词云和词频表就会被这些词霸占真正的内容关键词反而不突出。到这里数据分析需要的结构化结果就全部准备好了下一步是把它们画出来。4. 可视化实现让数据分析结果“看得见”4.1 四种图表对应四种业务问题我从来不认为图表越多越好。一张图对应一个要回答的问题否则就是好看不中用。这套系统里只保留了四类图播放量 Top10 柱状图回答“哪些视频是存量爆款”。发布时段均值折线图回答“几点发视频数据更好”。时长与互动率散点图回答“视频做多长更容易获得互动”。标签/标题词云图回答“用户到底在为什么内容买单”。用 pyecharts 画柱状图和散点图代码量很小但它导出的渲染方式适合嵌入 HTML 页面。from pyecharts.charts import Bar, Scatter, Line import pyecharts.options as opts df[interaction_rate] ( (df[like_count] df[comment_count] df[share_count] df[favorite_count]) / df[play_count] ).round(4) top10 df.sort_values(play_count, ascendingFalse).head(10) bar ( Bar() .add_xaxis(top10[video_id].tolist()) .add_yaxis(播放量, top10[play_count].tolist(), label_optsopts.LabelOpts(is_showFalse)) .set_global_opts( title_optsopts.TitleOpts(title播放量 Top10 视频), yaxis_optsopts.AxisOpts(name播放量) ) ) scatter ( Scatter() .add_xaxis(df[duration].tolist()) .add_yaxis(互动率, (df[interaction_rate] * 100).round(2).tolist()) .set_global_opts( title_optsopts.TitleOpts(title视频时长与互动率关系), xaxis_optsopts.AxisOpts(name时长秒数), yaxis_optsopts.AxisOpts(name互动率 %) ) )散点图里有一个容易被忽略的小问题当 1000 个点叠在一起时大部分点会重合看不出密度差异。建议在画图前对样本做一次随机抽样只画 200 个点或者让点的透明度降到 0.4效果会好很多。4.2 中文词云的字体问题是躲不开的wordcloud 默认字体对中文不支持直接生成会得到一堆方块。解决办法是显式指定一个支持中文的字体文件。Windows 上可以用系统自带的msyh.ttc微软雅黑macOS 上可以用PingFang.ttc。from wordcloud import WordCloud import matplotlib.pyplot as plt freq_dict dict(zip(word_df[word], word_df[freq])) wc WordCloud( font_pathmsyh.ttc, width800, height400, background_colorwhite, max_words80 ).generate_from_frequencies(freq_dict) plt.figure(figsize(10, 5)) plt.imshow(wc, interpolationbilinear) plt.axis(off) plt.savefig(wordcloud.png, dpi150, bbox_inchestight)注意generate_from_frequencies接收的是“词到频次”的字典而不是原始文本。用频率字典的好处是可控性更强你不希望出现在词云里的词在上一步的词频统计阶段就能过滤掉。4.3 用 Flask 把图表拼成看板pyecharts 生成的图表对象可以直接调用render(templates/bar.html)生成一个独立 HTML 文件。最后用 Flask 写一个非常简单的入口把这些页面拼到同一个看板里from flask import Flask, render_template app Flask(__name__) app.route(/) def index(): return render_template(index.html) if __name__ __main__: app.run(debugTrue)index.html里用 iframe 把四个图表文件引进来。这样做比在 Flask 里直接传图表对象简单得多也更容易替换!DOCTYPE html html head meta charsetutf-8 title短视频数据分析看板/title style .chart-frame { width: 48%; height: 450px; margin: 1%; border: 1px solid #eee; float: left; } /style /head body iframe classchart-frame src/static/bar.html/iframe iframe classchart-frame src/static/line.html/iframe iframe classchart-frame src/static/scatter.html/iframe iframe classchart-frame src/static/wordcloud.html/iframe /body /html如果想让项目更像一个完整产品你还可以把 /static 目录里由 pyecharts 生成出来的 HTML 放在 templates 外面由静态路由直接访问。整个看板的开发量非常小但演示效果比一串控制台打印出来的数字强得多。5. 我自己踩过的三个坑5.1 词云中文字体显示成方块这个坑几乎必踩。第一次跑出词云图满屏方块我当时还以为是数据编码出了问题。后来排查发现是 wordcloud 默认的 DroidSansMono 字体不支持中文。验证方法很简单print(matplotlib.font_manager.findfont(msyh.ttc))如果找不到字体文件路径就说明字体没被识别到。解决方案有两个一是写死font_path二是先把字体文件复制到项目目录下再指定相对路径。我推荐第二种因为换电脑跑项目时不会因为系统不同而找不到字体。5.2 pyecharts 版本差异导致的接口报错pyecharts 从 0.x 升级到 1.x 之后API 风格变化很大。网上很多旧教程写的是pyecharts.Bar但在新版本里必须写成from pyecharts.charts import Bar。我用的是 2.x 系列版本还遇到过LabelOpts参数名调整的问题。如果你照着别人的源码跑不起来第一件事不是改业务逻辑而是先核对 pyecharts 版本。项目里配套一个固定版本号的 requirements.txt 是很有必要的。5.3 数据量一大图表直接卡死模拟数据只有 1000 条跑起来毫无压力。但我朋友后来拿到的真实表格有 7 万行直接把 7 万点全放到散点图里页面加载了十几秒。处理方案是聚合之后再画散点图抽样、折线图按时段聚合均值、柱状图只保留 Top20。可视化的本质是“降低人的认知负担”不是把原始数据原封不动挪到前端所以数据量越大越要狠心做降维。另一个和性能相关的细节是publish_time的时区问题。如果原始表格里时间带时区后缀直接dt.hour可能和你本地时间差 8 个小时。稳妥的做法是读取时统一指定utcTrue再转换到目标时区确保“下午 6 点发布”真的是当地时间下午 6 点。6. 从“能跑”到“好用”的几个后续思路这套系统到现在已经能满足单次数据复盘的基本要求。但如果你准备把它往更完整的方向扩展我建议优先加三件事第一把 CSV 读取换成数据库读取。数据量到几十万行之后每次都全量读 CSV 会越来越慢换成 SQLite 或者 MySQL 能明显改善。第二增加筛选条件。看板左侧加一个日期范围选择器和一个标签下拉框只分析某段时间、某个垂类的视频。第三加一个简单报告导出功能。用 Jinja2 模板生成一份包括关键结论的 HTML 报告运营同事直接分享链接就能看到结论不用把整套系统装在自己电脑上跑。我给朋友做的那版仪表盘后来就加了“只看近 7 天”“只看旅行标签”两个筛选运营从拿到数据到看出问题的时间从原来的半小时缩短到了几分钟。这是数据分析系统最有成就感的部分不是图表多漂亮而是它真的帮人节省了做决策的时间。