WhatsApp聊天记录分析:Python轻量级社交图谱构建指南 1. 项目概述从 WhatsApp 聊天记录里挖出真实社交图谱你有没有试过翻看自己三年前的某个旅行群聊里面全是“谁带充电宝”“酒店退房几点”“最后一张自拍发群里了”还有凌晨两点突然冒出来的“快看海豚”——这些碎片化、非结构化、带着强烈时间戳和情绪色彩的文字其实是一份极其珍贵的微型社会行为日志。它不经过滤、不加修饰比任何问卷或访谈都更接近人的真实互动逻辑。我第一次系统性地处理这类数据是在帮朋友分析一个持续了18个月的考研互助群。当时他们想搞清楚为什么前期活跃度很高但到冲刺阶段反而沉默了是信息过载还是角色分工失衡抑或是情绪传染导致的集体倦怠靠肉眼翻聊天记录根本无解但用 Python 搭建一套轻量级分析流程后答案就浮在了图表上群内消息峰值与模考时间高度重合而“求助类”消息占比在最后两个月下降了63%取而代之的是大量未附带文字的图片和表情包——这说明大家已从“知识协作”转向“情绪共鸣”只是没人意识到这种转变。这个项目标题里的“…And More!”不是营销话术而是实打实的技术延展路径。核心确实是用Pandas 清洗原始文本、用 Plotly 做交互式可视化但真正让分析立住脚的是背后一整套针对中文聊天场景的定制化处理逻辑。比如 WhatsApp 导出的 .txt 文件每条消息固定以[日期, 时间] 发送者: 内容开头但中文环境下存在大量变体有人用空格分隔时间与姓名有人用破折号还有人导出时因编码问题出现乱码再比如“哈哈哈”和“hhhhh”在语义上完全等价但默认分词会把它们当成两个词又比如“所有人”这种系统提示在统计个人发言频次时必须过滤否则会严重扭曲真实参与度。这些细节官方文档不会写教程视频也极少展开但恰恰是决定分析结果是否可信的关键。我后来把这套流程固化成一个可复用的脚本模板只要替换输入文件路径5分钟内就能生成包含消息热力图、成员活跃度雷达图、关键词共现网络、话题时间线演变在内的四类核心图表连刚学完 Pandas 基础语法的新手都能跑通。它不追求学术级的 NLP 深度而是聚焦于“用最低技术成本回答最实际的社交问题”——比如“谁是群里的信息枢纽”、“哪段时间讨论效率最高”、“哪些话题最容易引发长讨论”。如果你也常被微信群、钉钉群、飞书群的海量信息淹没或者需要为团队协作优化提供数据依据这套方法就是为你准备的。2. 核心思路拆解为什么不用现成工具而要自己写代码很多人看到“WhatsApp 聊天分析”第一反应是网上不是有现成的微信聊天分析工具吗直接拖文件进去不就完了这个问题我被问过至少二十次每次我都先反问一句“你导出的聊天记录里有没有出现过‘[2023-05-12 14:23:07] 王磊: 今天食堂的红烧肉绝了’这样的格式”如果对方点头那基本可以确定——市面上90%的所谓“聊天分析工具”对 WhatsApp 的支持是残缺的。原因很简单微信和 WhatsApp 的导出机制完全不同。微信导出的是加密数据库或 HTML 页面结构相对规整而 WhatsApp 导出的纯文本.txt是面向人类阅读设计的不是为机器解析准备的。它的格式自由度极高日期可能用斜杠/、短横-或点.分隔时间可能带秒也可能不带发送者名字前后可能有空格、括号甚至emoji消息内容里还可能嵌套换行符、链接、语音转文字的错别字……这些在正则表达式里都是“坑”一个没填平整条数据就废了。所以我的方案选择非常明确放弃所有黑盒工具用 Python 从零构建可控的解析流水线。这不是为了炫技而是基于三个硬性需求第一可审计性。当业务方质疑“为什么张三的发言量比李四少30%”时我能直接打开代码指着第87行的正则匹配逻辑说“因为张三有7条消息是以‘[转发]’开头的我们按规则排除了转发消息只统计原创内容。”第二可扩展性。某次给一个电商运营团队做分析他们需要额外统计“带商品链接的消息占比”和“客服响应时效”我只在原有框架里加了两个函数15分钟就交付了新报表。第三中文友好性。所有现成工具的分词、停用词、情感词典都是为英文设计的直接套用中文结果荒谬得令人发指。比如“绝了”在英文词典里是“absolutely”情感值0.2但在中文语境里是0.9的强正向“摸鱼”被识别为名词“fish”情感值0实际是职场黑话里的中性偏负。所以必须用 jieba 分词 自建中文停用词表 针对聊天场景优化的情感词典比如把“哈哈”“嗯嗯”“收到”单独归类为“应答词”不参与情感计算。这个思路带来的直接好处是整个分析过程像搭乐高一样模块化。清洗模块只负责把杂乱文本变成标准 DataFrame统计模块只负责按需聚合数据可视化模块只负责把数字变成图表。任何一个环节出问题都能快速定位、单独调试。我见过太多人用 Excel 处理聊天记录手动删掉几百条系统通知再用查找替换清理乱码最后用数据透视表勉强做出个柱状图——表面看完成了任务但一旦原始数据格式微调比如导出时多了一个空格整个流程就得重来。而代码化的方案一次调试永久受益。更重要的是它培养了一种“数据思维”当你开始思考“这条消息为什么无法被正确解析”你就已经在理解数据的生成逻辑当你调整正则表达式去适配新的格式变体你就已经掌握了数据治理的核心能力。这远比学会画一个漂亮的饼图重要得多。3. 核心细节解析从原始文本到结构化数据的七道关卡把 WhatsApp 导出的 .txt 文件变成可分析的 DataFrame绝不是pd.read_csv()一行代码能解决的。我把它拆解成七个必须严控的环节每个环节都有其不可替代的作用和极易踩中的陷阱。下面以一段真实的导出文本为例逐层演示[2023/05/12, 14:23:07] 王磊: 今天食堂的红烧肉绝了 [2023/05/12, 14:24:15] 李婷: 王磊 你拍照片了吗我也想去 [2023/05/12, 14:25:33] 张伟: [转发] 王磊: 今天食堂的红烧肉绝了 [2023/05/12, 14:26:01] 王磊: 在拍马上发群里 [2023/05/12, 14:27:44] 系统: 王磊 加入了群组3.1 第一道关编码识别与预清洗WhatsApp 导出的文件编码极不稳定Windows 用户常见gbkMac 用户多为utf-8而某些安卓版本导出的文件甚至混用utf-8-sig。直接open(file, r)极易报错。我的做法是先用chardet库自动探测编码再用codecs模块强制按探测结果读取。但探测本身也有误差所以必须加一层兜底逻辑——如果探测置信度低于0.7就尝试utf-8和gbk双路读取取能成功读取且字符数最多的那个。预清洗则专治三类顽疾一是删除所有\x00空字符常见于导出异常二是将连续多个空格/制表符统一替换为单个空格三是移除行首尾不可见控制字符如\u200b零宽空格。这一步看似琐碎但能避免后续正则匹配时因隐藏字符导致的“明明写了规则却匹配不上”的诡异问题。3.2 第二道关消息块切分关键在于识别“一条完整消息”的边界。很多人误以为按换行符\n切分就行但聊天记录里大量存在“消息内容跨行”的情况比如长段落、代码块、截图文字识别结果。真正的切分锚点是时间戳模式。我定义的时间戳正则为\[\d{4}[-/\.]\d{1,2}[-/\.]\d{1,2},\s*\d{1,2}:\d{2}:\d{2}\]。注意这里用了\s*匹配任意空白包括换行并用re.split()的maxsplit1参数确保只在第一个匹配处切分从而把“时间戳发送者内容”作为一个整体提取出来。切分后得到的列表每一项都是形如[2023/05/12, 14:23:07] 王磊: 今天食堂的红烧肉绝了的字符串这才是后续解析的可靠输入。3.3 第三道关结构化解析这是最核心也最脆弱的一环。目标是把上面的字符串拆成timestamp,sender,content三列。难点在于发送者名字的提取它总在时间戳之后、冒号之前但中间可能有任意空白。我的正则是\[(.*?)\]\s*(.*?):\s*(.*)。这里.*?是非贪婪匹配确保捕获最短的可能时间戳\s*吃掉所有空白:后的\s*吃掉冒号后的空格。但问题来了如果某人名字里就带冒号比如“张伟:运营总监”这个正则就崩了。所以必须加校验——解析后检查sender字段是否包含:如果包含就回退到用str.split(:, 1)手动分割并取分割后长度大于2的部分作为content因为正常消息里发送者名字不会超过20个字符而内容通常很长。这个兜底逻辑救了我三次其中一次是处理一个跨国律师群成员名全是“John Smith:Partner”。3.4 第四道关时间标准化原始时间戳格式五花八门[2023/05/12, 14:23:07]、[12/05/2023, 2:23:07 PM]、[2023.05.12, 14:23:07]。Pandas 的pd.to_datetime()虽然强大但面对混合格式会报错。我的方案是先用dateutil.parser.parse()这个“万能解析器”它能自动识别绝大多数常见格式再用dt.tz_localize(Asia/Shanghai)统一时区避免夏令时混乱最后用dt.floor(S)将毫秒级精度归零保证时间计算的稳定性。特别提醒一定要用errorscoerce参数让无法解析的时间返回NaTNot a Time而不是中断整个流程。后续再用df.dropna(subset[timestamp])干净地剔除无效行。3.5 第五道关内容净化这步决定了后续所有分析的质量。我定义了五级净化系统消息过滤用content.str.contains(r^(加入|退出|修改了群名称|邀请了|移除了), naFalse)精准识别转发消息标记用content.str.startswith([转发])单独标记不删除而是新增is_forwarded列方便后续交叉分析比如“转发消息的平均响应时长”链接与邮箱剥离用re.sub(rhttps?://\S|[\w.-][\w.-]\.\w, [URL], content)替换为占位符既保留消息长度信息又避免 URL 干扰分词emoji 与特殊符号处理用emoji.demojize()将 emoji 转为:smiling_face_with_smiling_eyes:这样的文本便于统计同时用re.sub(r[^\w\s\u4e00-\u9fff], , content)移除所有非中文、非英文、非数字、非空格的字符如★、→、【】这些符号在聊天中高频出现但对语义分析毫无价值重复标点压缩re.sub(r!{2,}, !, re.sub(r\?{2,}, ?, content))把“太好了”变成“太好了”因为多个感叹号在情感强度上并无线性增长反而会干扰词频统计。3.6 第六道关中文分词与停用词jieba是基础但必须深度定制。首先禁用jieba的搜索引擎模式cut_for_search因为它会把“红烧肉”强行拆成“红烧/肉”破坏语义完整性坚持用cut模式。其次自建停用词表是成败关键。我维护的停用词表包含三类一是通用停用词“的”、“了”、“在”二是聊天特有停用词“嗯嗯”、“哦哦”、“好的”、“收到”、“哈哈”、“嘿嘿”三是群内高频无意义词比如那个考研群的停用词表里就有“背了”、“记住了”、“懂了”因为它们出现频率太高但几乎不携带有效信息。更进一步我用jieba.suggest_freq((红烧肉, 食堂), True)强制提升特定词组的切分优先级确保“食堂红烧肉”不会被切成“食堂/红/烧/肉”。3.7 第七道关数据验证与修复在 DataFrame 构建完成后必须运行一套验证脚本检查timestamp是否全部为datetime64类型统计sender列的唯一值数量是否与预期群成员数一致若不一致说明有昵称变更未处理计算content列的平均长度若低于10字符大概率是解析错误正常消息很少这么短最后随机抽样100条原始文本和解析后的content人工比对。这一步耗时但能发现90%的隐性错误。我曾在一个项目中发现由于导出时手机系统语言切换部分消息的时间戳变成了[12.05.2023, 14:23:07]点号分隔而我的正则只覆盖了斜杠和短横导致这批数据全被解析为NaT。正是验证环节的timestamp.isna().sum()报警才让我及时补上了点号分隔的支持。4. 实操过程用 Plotly 构建四类核心交互图表当清洗完成的 DataFrame我习惯命名为df_chat准备好后真正的分析才开始。Plotly 的优势在于它生成的图表是可交互、可导出、可嵌入网页的远超 Matplotlib 的静态能力。下面详细拆解四类最实用图表的实现逻辑每一步都附带可直接运行的代码片段和参数设计原理。4.1 消息热力图定位群聊的“黄金时间”热力图的目标是回答“这个群在一天中的哪个时段最活跃一周中的哪天讨论最密集”传统做法是用seaborn.heatmap()但它只能生成静态图无法点击缩放。Plotly 的px.density_heatmap()完美解决此问题。import plotly.express as px # 提取小时和星期几 df_chat[hour] df_chat[timestamp].dt.hour df_chat[day_of_week] df_chat[timestamp].dt.day_name() # 按小时和星期几分组计数 heatmap_data df_chat.groupby([day_of_week, hour]).size().reset_index(namecount) # 定义星期顺序确保周一在最左 day_order [Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday] heatmap_data[day_of_week] pd.Categorical(heatmap_data[day_of_week], categoriesday_order, orderedTrue) heatmap_data heatmap_data.sort_values([day_of_week, hour]) # 绘制热力图 fig px.density_heatmap( heatmap_data, xhour, yday_of_week, zcount, title群聊活跃度热力图, labels{hour: 小时 (24小时制), day_of_week: 星期, count: 消息数}, color_continuous_scaleViridis, # 使用深紫到亮黄的渐变视觉冲击力强 range_color[0, heatmap_data[count].quantile(0.95)] # 截断顶部5%异常值避免颜色失真 ) fig.update_layout( height500, xaxisdict(tickmodelinear, dtick2), # 每2小时一个刻度 yaxisdict(categoryorderarray, categoryarrayday_order) # 强制星期顺序 ) fig.show()参数设计原理range_color设置为quantile(0.95)是关键技巧。群聊中常有“突发性刷屏”比如节日祝福、紧急通知会导致某个小时的计数远超日常水平如果按全量范围着色日常的温和活跃就会被压缩成一片浅色失去区分度。截断顶部5%后颜色梯度能更精细地反映日常波动。另外color_continuous_scaleViridis比默认的Plasma更适合屏幕显示色盲用户也能清晰分辨。4.2 成员活跃度雷达图识别群内的“能量中心”雷达图能直观对比多个成员在不同维度上的表现。我定义了五个核心维度total_messages总消息数、avg_message_length平均消息长度、response_rate回复率发出消息后2小时内被他人回复的比例、weekend_ratio周末消息占比、unique_words独立词汇数。这五个维度共同刻画了一个成员的“社交画像”。import plotly.graph_objects as go # 计算各维度指标 member_stats df_chat.groupby(sender).agg( total_messages(content, count), avg_message_length(content, lambda x: x.str.len().mean()), unique_words(content, lambda x: len(set( .join(x).split()))), weekend_ratio(timestamp, lambda x: (x.dt.dayofweek 5).mean()) ).reset_index() # 计算回复率需先排序再用shift()找上一条消息的发送者 df_sorted df_chat.sort_values([timestamp]).copy() df_sorted[prev_sender] df_sorted[sender].shift(1) df_sorted[time_diff] (df_sorted[timestamp] - df_sorted[timestamp].shift(1)).dt.total_seconds() / 3600 # 标记“被回复”当前消息发送者与上一条消息发送者不同且时间差2小时 df_sorted[is_replied] ((df_sorted[sender] ! df_sorted[prev_sender]) (df_sorted[time_diff] 2)) # 按发送者统计被回复次数 reply_counts df_sorted.groupby(sender)[is_replied].sum() member_stats[response_rate] member_stats[sender].map(reply_counts) / member_stats[total_messages] # 归一化到0-1区间便于雷达图绘制 for col in [total_messages, avg_message_length, unique_words, weekend_ratio, response_rate]: min_val, max_val member_stats[col].min(), member_stats[col].max() if max_val ! min_val: member_stats[f{col}_norm] (member_stats[col] - min_val) / (max_val - min_val) else: member_stats[f{col}_norm] 0.5 # 全部相等时设为中值 # 绘制雷达图 fig go.Figure() for idx, row in member_stats.iterrows(): fig.add_trace(go.Scatterpolar( r[row[total_messages_norm], row[avg_message_length_norm], row[unique_words_norm], row[weekend_ratio_norm], row[response_rate_norm]], theta[总消息数, 平均长度, 词汇丰富度, 周末活跃度, 回复率], filltoself, namerow[sender], hovertemplateb%{text}/bbr%{theta}: %{r:.2f}extra/extra, text[row[sender]] * 5 )) fig.update_layout( polardict( radialaxisdict( visibleTrue, range[0, 1] ) ), showlegendTrue, title成员活跃度雷达图归一化 ) fig.show()实操心得雷达图最大的陷阱是“维度间不可比”。比如total_messages可能是1000而response_rate是0.3直接画图会完全失真。所以强制归一化是必须步骤。另一个关键是hovertemplate的设置——鼠标悬停时显示原始数值%{r:.2f}而非归一化值这样用户既能看相对位置又能知绝对大小。我曾用这个图帮一个创业团队发现CTO 发言总数排第三但“词汇丰富度”和“回复率”双料第一说明他是群内真正的技术决策枢纽而不仅仅是参与者。4.3 关键词共现网络揭示群聊的“隐性话题结构”词云只能告诉你“什么词出现最多”但无法揭示“哪些词总是一起出现”。共现网络图则能可视化词语间的关联强度。我的做法是先用 TF-IDF 提取群内最重要的100个关键词再计算它们两两之间的共现频次同一消息中同时出现即计1次最后用plotly.graph_objects.Scatter绘制节点和连线。from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.metrics.pairwise import cosine_similarity import numpy as np # 提取TF-IDF特征只取top 100词 vectorizer TfidfVectorizer(max_features100, stop_wordscustom_stopwords, ngram_range(1,2)) tfidf_matrix vectorizer.fit_transform(df_chat[content].dropna()) feature_names vectorizer.get_feature_names_out() # 计算余弦相似度矩阵词与词之间的相似度 similarity_matrix cosine_similarity(tfidf_matrix.T) # 取上三角矩阵避免重复 co_occurrence np.triu(similarity_matrix, k1) # 获取top 50个最强关联对 np.fill_diagonal(co_occurrence, 0) # 清除对角线 flat_cooc co_occurrence.flatten() top_indices np.argsort(flat_cooc)[-50:][::-1] # 解析索引为词对 edges [] for idx in top_indices: i, j np.unravel_index(idx, co_occurrence.shape) if co_occurrence[i, j] 0.1: # 设定最小相似度阈值过滤弱关联 edges.append((feature_names[i], feature_names[j], co_occurrence[i, j])) # 用networkx布局但用plotly绘制避免依赖graphviz import networkx as nx G nx.Graph() for word1, word2, weight in edges: G.add_edge(word1, word2, weightweight) # 使用spring_layout生成节点坐标 pos nx.spring_layout(G, seed42, k3/np.sqrt(len(G.nodes()))) # k控制节点间距 # 准备绘图数据 edge_x, edge_y [], [] for edge in G.edges(): x0, y0 pos[edge[0]] x1, y1 pos[edge[1]] edge_x.extend([x0, x1, None]) edge_y.extend([y0, y1, None]) node_x, node_y, node_text [], [], [] for node in G.nodes(): x, y pos[node] node_x.append(x) node_y.append(y) node_text.append(f{node}br度中心性: {G.degree(node)}) # 显示节点连接数 # 绘制 fig go.Figure() fig.add_trace(go.Scatter( xedge_x, yedge_y, modelines, linedict(width1, colorlightgray), hoverinfonone )) fig.add_trace(go.Scatter( xnode_x, ynode_y, modemarkerstext, markerdict(size15, colorsteelblue, line_width2), text[n.split()[0] for n in node_text], # 只显示词的前半部分避免重叠 textpositiontop center, hovertextnode_text, hoverinfotext )) fig.update_layout( title关键词共现网络图TF-IDF 余弦相似度, showlegendFalse, xaxisdict(showgridFalse, zerolineFalse, showticklabelsFalse), yaxisdict(showgridFalse, zerolineFalse, showticklabelsFalse), height600 ) fig.show()避坑指南共现网络极易变成一团乱麻。我的经验是第一严格限制节点数量不超过50个否则图会不可读第二用度中心性degree centrality代替词频作为节点大小因为一个词如果和很多其他词都有关联如“项目”、“需求”、“测试”都连向“上线”说明它是核心概念比单纯出现次数多更有价值第三连线粗细必须与权重正相关但要用line_width而非width参数后者在 Plotly 中不生效。这个图曾帮一个产品团队发现“UI”和“UX”在他们的需求讨论群中几乎从不共现而是分别与“前端”和“用户调研”强关联这暴露了设计与研发之间存在认知断层。4.4 话题时间线演变追踪群聊的“兴趣迁移轨迹”这是最体现分析深度的图表。它不满足于“现在在聊什么”而是要回答“这个话题是怎么兴起、发展、衰落的”。我的方法是用KeyBERT库比 LDA 更适合短文本为每条消息提取3个关键词然后按周聚合统计每个关键词的周频次最后用px.line()绘制多条时间线。from keybert import KeyBERT # 初始化KeyBERT模型使用小型中文模型速度快 kw_model KeyBERT(modelparaphrase-multilingual-MiniLM-L12-v2) # 为每条消息提取关键词跳过短消息避免噪声 def extract_keywords(text): if len(text) 10: return [] try: # 提取3个关键词返回词和得分 keywords kw_model.extract_keywords( text, keyphrase_ngram_range(1, 2), stop_wordscustom_stopwords, top_k3, use_maxsumTrue, nr_candidates20 ) return [kw[0] for kw in keywords] # 只取词不要得分 except: return [] # 批量处理避免逐条调用太慢 df_chat[keywords] df_chat[content].apply(extract_keywords) # 展开关键词列表生成长格式数据 keywords_long df_chat.explode(keywords).dropna(subset[keywords]) keywords_long[week] keywords_long[timestamp].dt.to_period(W).dt.start_time # 按周和关键词聚合 topic_trend keywords_long.groupby([week, keywords]).size().reset_index(namecount) # 取出现频次最高的10个关键词作为主话题 top_topics topic_trend.groupby(keywords)[count].sum().nlargest(10).index topic_trend topic_trend[topic_trend[keywords].isin(top_topics)] # 绘制时间线 fig px.line( topic_trend, xweek, ycount, colorkeywords, titleTop 10 话题周频次演变, labels{week: 时间, count: 频次, keywords: 话题}, markersTrue ) fig.update_layout( height500, xaxisdict(tickformat%Y-%m-%d), legenddict(orientationh, yanchorbottom, y1.02, xanchorright, x1) ) fig.show()关键参数说明use_maxsumTrue是 KeyBERT 的核心技巧它确保提取的关键词不仅自身重要而且彼此之间差异性大避免“项目”、“项目管理”、“项目计划”这种同质化词从而让时间线更清晰。nr_candidates20控制候选词数量太少会漏掉关键概念太多则增加计算负担。这个图表曾精准捕捉到一个学习群的转折点在“线性代数”话题曲线触顶回落的同时“机器学习”曲线开始陡峭上升两条线的交叉点恰好是群公告发布“下阶段重点转向ML”的日期——数据与事实严丝合缝说服力拉满。5. 常见问题与排查技巧实录那些只有亲手做过才会懂的坑在几十个真实项目中我总结出一套“WhatsApp 聊天分析排障速查表”。这些问题99% 的教程不会提但每一个都足以让你卡住一整天。下面按发生频率排序附带我的独家解决方案。问题现象根本原因快速排查命令终极解决方案我的血泪教训pd.read_csv()直接报UnicodeDecodeError文件编码被误判常见于 Mac 导出的utf-8-sig带BOM头或 Windows 的gbkfile -i your_chat.txtLinux/Mac或chardet your_chat.txt需安装chardet用codecs.open(file, r, encodingutf-8-sig)强制指定或用with open(file, rb) as f: raw f.read(10000); print(raw[:100])查看文件头字节第一次遇到时我花了3小时重装Python环境以为是库冲突。后来发现utf-8-sig的BOM头是EF BB BF直接用vim编辑器:set nobomb就能清除。正则匹配sender总是失败content列全是NaN发送者名字里含有正则元字符如.、*、、?未做转义print(repr(first_line))查看原始字符串确认是否有.等字符在构建正则前对sender名字用re.escape()处理或改用str.partition(:)这种更鲁棒的字符串方法一个客户群的名字是AI.*Team点号被当作“任意字符”匹配导致整条正则失效。从此我所有涉及用户输入的正则第一行必加re.escape()。热力图颜色一片死灰看不出任何变化count列存在极端异常值如某天刷屏10000条导致range_color默认范围过大df_chat.groupby(df_chat[timestamp].dt.date).size().describe()查看消息数的分布用range_color[0, df[count].quantile(0.95)]截断或改用log_zTrue对Z轴取对数我曾为一个电商大促群做分析活动当天消息量是平日的200倍热力图完全失效。后来发现log_zTrue能完美解决视觉层次立刻清晰。jieba分词把“微信”切成“微/信”把“Python”切成“Py/thon”jieba默认词典不包含技术名词且启用了搜索引擎模式cut_for_searchjieba.lcut(微信Python)测试默认输出[微, 信, Py, thon]禁用搜索引擎模式用jieba.cut()并用jieba.suggest_freq((微信, Python), True)强制提升词频最后加载自定义词典jieba.load_userdict(tech_dict.txt)这个坑让我重跑了三次NLP分析。现在我的标准流程是先用jieba.lcut()测试10个典型词确认无误后再批量处理。雷达图所有线条都挤在中心看不出区别多个维度的量纲差异巨大如total_messages是千级response_rate是小数未归一化member_stats.describe()查看各列统计值必须归一化df[col] (df[col] - df[col].min()) / (df[col].max() - df[col].min())若某列全为0则设为0.5有一次我忘了归一化雷达图看起来像一朵枯萎的花。客户问“这是表示