图片来源
2130 字
11 分钟
聊天记录可视化
有了之前两篇的数据,接下来就可以生成一些好玩的报告了😋
可以用之前学过的 ipywidgets 做交互式的界面,大部分交给 ai,细节部分部分自己调就好了。
废话 & 叠甲 & 提醒
- 感谢某位好友几个月内的 22329 条聊天数据支持。
- 下面词云图片的 args 是
month='全部', user='用户1',都是我发出去的消息,我侵我自己的权,情感分析里面的 content 也都是我发的,如果仍然介意我就换别人的图。 - jieba 分词时用的
stopwords.txt参考了 SmartPorridge/Wechat_QQ_wordcloud 并额外屏蔽了几个人名。 - QQ 那篇里面声明过代码运行的环境是 macOS 15.5,其他版本或者操作系统可能需要微调,比如字体。
- 下面的代码依赖: pandas, numpy, matplotlib, seaborn, scipy, ipywidgets, wordcloud, jieba, Pillow,snownlp。如果已经安装了 Anaconda,在 base 环境的基础上只需要
conda install ipywidgets snownlp,pip install wordcloud jieba. 🐦的训练赛题解明天考完科二就补……(已经是明天了)
预处理 & 初始化设置
import pandas as pdfrom datetime import datetimeimport numpy as npimport seaborn as snsimport matplotlib.pyplot as pltfrom ipywidgets import interact, widgets
df_qq = pd.read_csv('qq.csv')df_wx = pd.read_csv('wx.csv')
df = pd.concat([df_qq, df_wx], ignore_index=True) # 合并 csv
df['time'] = pd.to_datetime(df['time']) # 转成 datetime 格式
plt.rcParams['font.family'] = 'Heiti TC' # 黑体plt.rcParams['axes.unicode_minus'] = False # 正确显示负号# 添加辅助列df["month"] = df["time"].dt.to_period("M").astype(str)df["date"] = df["time"].dt.datedf["weekday"] = df["time"].dt.day_name()
# 用户分类(假设 True 和 False 分别代表两个用户)user_labels = {True: "用户1", False: "用户2"}数据可视化
可以做很多有意思的分析,比如
每日聊天记录条数柱状图
按月分
from scipy.interpolate import make_interp_spline
def plot_daily_counts_by_month(month): month_df = df[df["month"] == month].copy() month_df["date"] = month_df["time"].dt.date
# 每日用户记录数 daily_user_counts = month_df.groupby(["date", "user"]).size().unstack(fill_value=0) daily_user_counts["总计"] = daily_user_counts.sum(axis=1)
dates = daily_user_counts.index x = np.arange(len(dates)) y = daily_user_counts["总计"].values
# 配色 color_user1 = "#8ecae6" color_user2 = "#ffb3b3" color_line = "#023047"
# 画图 plt.figure(figsize=(10, 6))
# 堆叠柱状图 plt.bar(x, daily_user_counts[True], label="用户1", color=color_user1) plt.bar(x, daily_user_counts[False], bottom=daily_user_counts[True], label="用户2", color=color_user2)
# 平滑折线(用样条插值) if len(x) >= 4: x_smooth = np.linspace(x.min(), x.max(), 300) spline = make_interp_spline(x, y, k=3) y_smooth = spline(x_smooth) plt.plot(x_smooth, y_smooth, color=color_line, linewidth=2, label="总聊天数") else: plt.plot(x, y, color=color_line, linewidth=2, label="总聊天数")
# x 轴标签用真实日期 plt.xticks(ticks=x[::max(1, len(x)//15)], labels=[f'{d.day : 02d}' for d in dates[::max(1, len(x)//15)]])
plt.title(f"{month} 每日聊天记录数量") plt.xlabel("日期") plt.ylabel("聊天数") plt.legend() plt.tight_layout() plt.show()
interact(plot_daily_counts_by_month, month=widgets.Dropdown(options=sorted(df["month"].unique())))
全部的
# 按天、用户分组统计daily_user_counts = df.groupby(["date", "user"]).size().unstack(fill_value=0)
# 总记录数daily_user_counts["总计"] = daily_user_counts.sum(axis=1)
# 画图plt.figure(figsize=(12, 6))
# 堆叠柱状图plt.bar(daily_user_counts.index, daily_user_counts[True], label="用户1", color="#90caf9")plt.bar(daily_user_counts.index, daily_user_counts[False], bottom=daily_user_counts[True], label="用户2", color="#f48fb1")
# 总记录数折线plt.plot(daily_user_counts.index, daily_user_counts["总计"], label="总聊天数", color="black", linewidth=1)
plt.title("每日聊天记录数量")plt.xlabel("日期")plt.ylabel("聊天数")plt.legend()plt.xticks(rotation=45)plt.tight_layout()plt.show()
去年 12 月 1 号发生什么了🤯
各月中不同星期的聊天记录热力图
weekday_counts = df.groupby(["month", "weekday"]).size().unstack().fillna(0)
weekday_order = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]weekday_counts = weekday_counts[weekday_order]
plt.figure(figsize=(12, 6))sns.heatmap(weekday_counts.T, cmap="YlGnBu", annot=True, fmt=".0f")plt.title("各月中不同星期的聊天记录热力图")plt.xlabel("月份")plt.ylabel("星期")plt.tight_layout()plt.show()
显然,经常周天晚上聊到周一凌晨……
各月聊天时间分布图
# 确保字段准备好df["hour"] = df["time"].dt.hour
# 分2小时一段bins = list(range(0, 25, 2)) # 0-2, 2-4, ..., 22-24labels = [f"{h:02d}-{h+2:02d}" for h in bins[:-1]]df["time_bin"] = pd.cut(df["hour"], bins=bins, labels=labels, right=False)
# 统计每月每时间段的聊天数pivot = df.pivot_table(index="month", columns="time_bin", aggfunc="size", fill_value=0, observed=False)
# 计算百分比percent = pivot.div(pivot.sum(axis=1), axis=0)
# 按时间段顺序排列列percent = percent[labels] # 保证顺序一致
sorted_index = pivot.sum(axis=1).sort_values().indexpercent = percent.loc[sorted_index]
# 绘图fig, ax = plt.subplots(figsize=(12, len(percent)*0.6))
left = [0] * len(percent) # 初始位置colors = sns.color_palette("Spectral", len(labels)) # 可自定义色板
for i, col in enumerate(percent.columns): ax.barh(percent.index, percent[col], left=left, label=col, color=colors[i]) left = [l + p for l, p in zip(left, percent[col])]
# 标注总聊天数(右侧)totals = pivot.sum(axis=1).loc[sorted_index]for i, (y, total) in enumerate(zip(percent.index, totals)): ax.text(1.01, i, f"{total}条", va="center", fontsize=10)
ax.set_title("各月聊天时间段分布(百分比)")ax.set_xlabel("比例")ax.set_xlim(0, 1)ax.set_ylabel("月份")ax.legend( title="时间段", loc="upper center", bbox_to_anchor=(0.5, -0.12), ncol=6, frameon=False)plt.tight_layout()plt.show()
验证了上述猜想,确实是经常从晚上聊到凌晨……
聊天关键词——词云图
词云这里面可以自定义很多参数,比如
import jiebafrom wordcloud import WordCloud, ImageColorGeneratorfrom PIL import Imageimport numpy as npimport matplotlib.pyplot as pltfrom ipywidgets import interact, widgets
# 读取停用词with open('stopwords.txt', 'r', encoding='utf-8') as f: stopwords = set(line.strip() for line in f if line.strip())
# 词云生成函数def generate_wordcloud(month, user): # 筛选数据 if month == "全部" and user == "全部": sub_df = df elif month == "全部": user_bool = (user == "用户1") sub_df = df[df["user"] == user_bool] elif user == "全部": sub_df = df[df["month"] == month] else: user_bool = (user == "用户1") sub_df = df[(df["month"] == month) & (df["user"] == user_bool)] # 拼接所有内容 text = "\n".join(str(x) for x in sub_df["content"] if pd.notnull(x) and str(x).strip()) # 分词并去除停用词 segs = [seg.strip() for seg in jieba.cut(text) if seg.strip() and seg not in stopwords] if not segs: print("无内容可生成词云") return # 词云参数 coloring = np.array(Image.open("mask.jpeg")) wc = WordCloud( font_path='fonts/NotoSansSC-Regular.ttf', background_color="white", max_words=400, mask=coloring, stopwords=stopwords, max_font_size=400, random_state=42 ) wc.generate(' '.join(segs)) image_colors = ImageColorGenerator(coloring) wc.recolor(color_func=image_colors) # 展示 plt.figure(figsize=(10, 10)) plt.imshow(wc, interpolation='bilinear') plt.axis("off") plt.title(f"{month} - {user} 词云") plt.show()
# 交互式界面month_options = ["全部"] + sorted(df["month"].unique())user_options = ["全部", "用户1", "用户2"]interact( generate_wordcloud, month=widgets.Dropdown(options=month_options, description="月份"), user=widgets.Dropdown(options=user_options, description="用户"))
聊天时长直方图
没有上下文直接这样判断很容易把能续上的聊天阶段,后面大部分就看个乐子了。
df = df.sort_values("time")df = df.reset_index(drop=True)
# 时间差(单位:秒)df["time_diff"] = df["time"].diff().dt.total_seconds().fillna(0)
# 若时间间隔超过 N 秒,则视为新会话session_threshold = 600 # 单位:秒df["session_id"] = (df["time_diff"] > session_threshold).cumsum()
# 每个 session 的起止时间与持续时长sessions = df.groupby("session_id").agg( start_time=("time", "first"), end_time=("time", "last"), message_count=("time", "count"))
sessions["duration_sec"] = (sessions["end_time"] - sessions["start_time"]).dt.total_seconds()sessions["duration_min"] = sessions["duration_sec"] / 60 # 转成分钟
# 过滤掉只发一条消息的聊天sessions = sessions[sessions["message_count"] > 1]
# 对数直方图:log1p(duration)log_values = np.log1p(sessions["duration_min"])
plt.figure(figsize=(10, 6))plt.hist(log_values, bins=40, color="#f48fb1", edgecolor="black")
# 设置横轴刻度:原始时间(分钟)ticks_raw = [1, 5, 10, 30, 60, 120, 300] # 你可以按需要改ticks_log = np.log1p(ticks_raw)tick_labels = [str(t) for t in ticks_raw]
plt.xticks(ticks=ticks_log, labels=tick_labels)
plt.xlabel("每次聊天持续时间(分钟)")plt.ylabel("聊天轮次数")plt.title("聊天持续时间分布(对数压缩,横轴显示原始分钟)")plt.tight_layout()plt.show()
每轮聊天双方发言文本长度柱状图
超过 30 min 的对话中双方的文本长度,一定程度上反应了聊天(最好不是吵架)的主动权。
# 标记每个 session 的持续时间(单位:分钟)session_duration = df.groupby("session_id")["time"].agg(["first", "last"])session_duration["duration_min"] = (session_duration["last"] - session_duration["first"]).dt.total_seconds() / 60long_sessions = session_duration[session_duration["duration_min"] > 30].index
# 仅保留长 session 的数据df_long = df[df["session_id"].isin(long_sessions) & df["content"].notna()].copy()df_long["text_length"] = df_long["content"].astype(str).str.len()
# 分组统计每个 session 每个用户的文本总长度length_by_user = df_long.groupby(["session_id", "user"])["text_length"].sum().unstack(fill_value=0)
# 重命名列,避免布尔列名引起混淆length_by_user = length_by_user.rename(columns={True: "用户1", False: "用户2"})
# 增加总量和主导者列length_by_user["total"] = length_by_user["用户1"] + length_by_user["用户2"]length_by_user["dominant_user"] = length_by_user[["用户1", "用户2"]].idxmax(axis=1)
length_by_user[["用户1", "用户2"]].plot(kind="bar", stacked=True, figsize=(14, 6), color=["#8ecae6", "#ffb3b3"])plt.title("每轮聊天中双方发言的文本长度(大于30分钟)")plt.xticks([])plt.xlabel("session")plt.ylabel("文本长度")plt.legend()plt.tight_layout()plt.show()
可以看出来,我话比较多……但这并不一定(一定不)是件好事。
聊天内容情感分析
SnowNLP 的训练数据主要是买卖东西时的评价,所以不是很准,列举几个比较离谱的
| content | sentiment |
|---|---|
| 睡醒了? | 0.07963618947920525 |
| 那个号只有个位数的英雄 | 0.9351870482742778 |
| 昂 | 0.8 |
| 我本来就有三张改名卡 | 0.02719995770409467 |
| [图片] | 0.5826086956521737 |
| … | … |
另外我让他分析 “我要上吊” 竟然有 0.5455370938053228 的高分,总之他特抽象,大部分也是看个乐子。
conda install snownlpfrom snownlp import SnowNLP
def get_sentiment(text): try: s = SnowNLP(text) return s.sentiments # 0 ~ 1 越大越积极 except: return None
df["sentiment"] = df["content"].astype(str).apply(get_sentiment)谁的情绪更稳定——箱线图
sns.boxplot(data=df, x="user", y="sentiment")plt.title("不同用户的情绪分布")plt.xticks([0, 1], ["用户2", "用户1"])plt.tight_layout()plt.show()
显然,我情绪不稳定,而且还消极……
每日平均情绪变化——折线图
# 按日期和用户分组,计算平均情绪daily_sentiment = df.groupby(["date", "user"])["sentiment"].mean().unstack()
# 画图plt.figure(figsize=(12, 4))plt.plot(daily_sentiment.index, daily_sentiment[True], label="用户1", color="#8ecae6")plt.plot(daily_sentiment.index, daily_sentiment[False], label="用户2", color="#ffb3b3")plt.axhline(0.5, color="gray", linestyle="--", label="中性线")
plt.title("每日平均情绪变化")plt.ylabel("情绪值(0=负面,1=正面)")plt.xlabel("日期")plt.legend()plt.tight_layout()plt.show()
后面数据比较少了,可能受极端值影响太大了,波动非常剧烈。前面的数据相对还正常一点,一眼就可以看出来,红线在大部分时间都比蓝线积极,而且好像还负相关。
总结 & 建议
学都学会了,快去祸害自己的好友吧~~~
另外如果有其他有意思的建议可以在下面的 comments 里提。
部分信息可能已经过时







