Mobile wallpaper 1Mobile wallpaper 2Mobile wallpaper 3Mobile wallpaper 4Mobile wallpaper 5
2130 字
11 分钟
聊天记录可视化
2025-07-07
统计加载中...

有了之前两篇的数据,接下来就可以生成一些好玩的报告了😋

可以用之前学过的 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 pd
from datetime import datetime
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt
from 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.date
df["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-24
labels = [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().index
percent = 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 jieba
from wordcloud import WordCloud, ImageColorGenerator
from PIL import Image
import numpy as np
import matplotlib.pyplot as plt
from 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() / 60
long_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 的训练数据主要是买卖东西时的评价,所以不是很准,列举几个比较离谱的

contentsentiment
睡醒了?0.07963618947920525
那个号只有个位数的英雄0.9351870482742778
0.8
我本来就有三张改名卡0.02719995770409467
[图片]0.5826086956521737

另外我让他分析 “我要上吊” 竟然有 0.5455370938053228 的高分,总之他特抽象,大部分也是看个乐子。

conda install snownlp
from 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 里提。

聊天记录可视化
https://starlab.top/posts/chatlog-vis/
作者
Star
发布于
2025-07-07
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时