主题演化关系挖掘:桑基图 + 相似度矩阵,2 张图讲透主题怎么”流”

上篇博客讲了主题强度演化(某个主题在每个时间窗有多火)。但老板们更关心的是主题之间的关系——T1(聊”创新驱动”)是怎么”流”到 T2(聊”数字经济”)的?T3 是从 T1 演化来的还是独立新生?

这就需要主题演化关系挖掘——算主题间相似度,画桑基图展示主题从一个时间窗”流向”另一个时间窗。

我们的 LDA 主题演化分析软件 专门做这件事:相似度矩阵 + 桑基图 + 三元组 三件套输出。

一、核心数据:主题相似度矩阵

主题 A 在 T1 时间窗,主题 B 在 T2 时间窗,它俩有多像?用 JS 散度(Jensen-Shannon Divergence)余弦相似度 算:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import numpy as np
from gensim.matutils import hellinger
from scipy.spatial.distance import cosine

def topic_similarity(model_a, topic_a_id, model_b, topic_b_id, topn=20):
"""两个主题的词分布相似度(余弦),值越大越相似"""
vec_a = np.array([prob for _, prob in model_a.show_topic(topic_a_id, topn=topn)])
vec_b = np.array([prob for _, prob in model_b.show_topic(topic_b_id, topn=topn)])
# 因为不同主题 topn 顺序可能不同,按词重新对齐
words_a = {w: p for w, p in model_a.show_topic(topic_a_id, topn=topn)}
words_b = {w: p for w, p in model_b.show_topic(topic_b_id, topn=topn)}
all_words = set(words_a.keys()) | set(words_b.keys())
va = np.array([words_a.get(w, 0) for w in all_words])
vb = np.array([words_b.get(w, 0) for w in all_words])
return 1 - cosine(va, vb) # 余弦相似度

# 假设有 3 个时间窗,每个 10 个主题
K = 10
time_models = {
'2022': lda_2022,
'2023': lda_2023,
'2024': lda_2024
}

# 算相邻时间窗的主题-主题相似度
similarity = {}
for t1, t2 in [('2022', '2023'), ('2023', '2024')]:
sim_matrix = np.zeros((K, K))
for i in range(K):
for j in range(K):
sim_matrix[i][j] = topic_similarity(
time_models[t1], i, time_models[t2], j
)
similarity[(t1, t2)] = sim_matrix

二、相似度矩阵热力图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import matplotlib.pyplot as plt
import seaborn as sns

fig, axes = plt.subplots(1, 2, figsize=(16, 6))
for idx, ((t1, t2), mat) in enumerate(similarity.items()):
sns.heatmap(
mat, annot=True, fmt='.2f',
cmap='YlGnBu', vmin=0, vmax=1,
xticklabels=[f'T{j}' for j in range(K)],
yticklabels=[f'T{i}' for i in range(K)],
ax=axes[idx]
)
axes[idx].set_title(f'{t1}{t2} 主题相似度矩阵', fontsize=13)
axes[idx].set_xlabel(f'{t2} 主题')
axes[idx].set_ylabel(f'{t1} 主题')
plt.tight_layout()
plt.savefig('topic_similarity_matrix.png', dpi=150)

怎么看

三、桑基图:主题怎么”流”

桑基图(Sankey diagram)最适合展示流向——左边一列节点是 t1 的主题,右边一列是 t2 的主题,连线的粗细 = 主题相似度(继承强度)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import plotly.graph_objects as go

def build_sankey_data(similarity_matrix, threshold=0.5, K=10):
"""把相似度矩阵转成桑基图数据"""
sources, targets, values, labels = [], [], [], []

# 左列(0~K-1): t1 的主题
# 右列(K~2K-1): t2 的主题
for i in range(K):
labels.append(f'{t1} 主题{i}')
for j in range(K):
labels.append(f'{t2} 主题{j}')

for i in range(K):
for j in range(K):
sim = similarity_matrix[i][j]
if sim >= threshold: # 只画高相似度的连线
sources.append(i)
targets.append(K + j)
values.append(sim)

return sources, targets, values, labels

sources, targets, values, labels = build_sankey_data(
similarity[('2022', '2023')], threshold=0.4
)

fig = go.Figure(data=[go.Sankey(
node=dict(
pad=15, thickness=20,
line=dict(color='black', width=0.5),
label=labels,
color='#6e5cff'
),
link=dict(
source=sources, target=targets, value=values,
color='rgba(110, 92, 255, 0.4)'
)
)])

fig.update_layout(
title_text='LDA 主题演化桑基图(2022 → 2023)',
font_size=12,
height=600
)
fig.write_html('topic_sankey.html')
fig.show()

关键参数threshold=0.4 是相似度过滤阈值,低于这个值的连线不画(避免画面太乱)。建议从 0.3 试起,根据画面清晰度调到 0.5-0.7。

四、三元组(source, target, value)数据结构

桑基图底层其实就是三元组列表——老板可能想把这份数据喂给 BI 工具画自己风格的图:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def to_triples(similarity_matrix, t1, t2, threshold=0.4, K=10):
triples = []
for i in range(K):
for j in range(K):
sim = similarity_matrix[i][j]
if sim >= threshold:
triples.append({
'source': f'{t1}_T{i}',
'target': f'{t2}_T{j}',
'value': round(float(sim), 4)
})
return triples

triples = to_triples(similarity[('2022', '2023')], '2022', '2023', threshold=0.4)
pd.DataFrame(triples).to_csv('topic_evolution_triples.csv', index=False, encoding='utf-8-sig')

输出格式

source target value
2022_T0 2023_T1 0.7321
2022_T0 2023_T5 0.4521

喂给 ECharts 桑基图TableauPower BI 都能直接画。

五、批量处理:3 年 30 个主题全自动演化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 假设有 3 个时间窗 × 10 主题 = 30 个节点,2 个相邻相似度矩阵
all_triples = []
for (t1, t2), mat in similarity.items():
triples = to_triples(mat, t1, t2, threshold=0.4, K=K)
all_triples.extend(triples)

# 跨 3 年甚至 5 年的全演化
# 把所有时间窗的桑基图拼成一张大图
all_labels = []
for t in time_models.keys():
for i in range(K):
all_labels.append(f'{t}_T{i}')

# 构建 sources/targets/values (合并所有相邻时间窗的边)
sources_all, targets_all, values_all = [], [], []
offset = 0
for (t1, t2), mat in similarity.items():
K_curr = K
for i in range(K_curr):
for j in range(K_curr):
sim = mat[i][j]
if sim >= 0.4:
sources_all.append(offset + i)
targets_all.append(offset + K_curr + j)
values_all.append(sim)
offset += K_curr

:拼图时 offset 要对——offset 是当前时间窗在全局节点列表里的起始位置。

六、典型应用案例

案例 1:政策演化
2022-2024 三年的”科技创新”政策文件,桑基图清晰展示:

案例 2:竞品口碑演化
2022-2024 三个双 11 的电商评论,桑基图显示:

七、踩坑大全

1. 阈值选取:threshold 太高(>0.7)= 几乎画不出连线(主题间相似度很少到 0.7);太低(<0.3)= 画面太乱。建议从 0.4 起步。

2. 主题编号不一致:不同时间窗 LDA 给的主题编号是随机的,必须用相似度矩阵做对齐(前面代码里的贪心匹配)。否则会画出”假演化”——以为 T1 → T2,实际是 T5 → T7。

3. 桑基图性能:节点数 > 50 时 plotly 会卡。建议分时间窗画,不要拼 1 张大图。

4. 三元组的 value 含义:value = 相似度(0~1),不是流量。通量图才用通量值。老板常分不清,记得在报告里标注。

5. 节点标签:一定要带”时间戳前缀”(如 2022_T0),否则读图时不知道节点属于哪一年。


我们的 LDA 主题演化分析软件 一键出三大件:相似度矩阵(Excel + 热力图)+ 桑基图(HTML 交互版)+ 三元组(CSV)。老板拿到三元组后想用 ECharts、Tableau、Power BI 二次加工都行。