データ活用 テキスト分析

Google Playレビューのトピック構造を可視化する:LDA × BERTopic比較と解釈【Python】

2025年10月12日

本記事は「トピック抽出編(LDA/BERTopic)」の発展パートとして、両モデルの結果を比較・可視化し、トピック構造の違いをわかりやすく整理します。 LDAとBERTopicの両方を同一データで適用し、どのトピックが対応しているか・どの部分が異なるかを可視化・定量化します。

対象:レビューや口コミのテーマを分析し、「話題のまとまり方」を俯瞰したい方。


1. 比較の目的と概要

LDAとBERTopicは、どちらもテキスト群から「話題のまとまり」を抽出する手法ですが、 トピックの形成ロジックが大きく異なります。

手法特徴トピック数
LDA(Latent Dirichlet Allocation)単語の共起確率をもとに話題を均等に分割するベイズモデル。固定(K指定)
BERTopic文書埋め込み+HDBSCANで密度ベースの自然クラスタを抽出。自動(データ構造から判定)

両者の結果はトピック数・粒度・話題のまとまり方が異なり、 どちらを採用するかは目的によって最適解が変わります。 その違いを定量的に理解するため、本記事では以下の2軸で可視化します。

  • 対応表: 各BERTopicトピックがLDAのどのトピックに対応しているかをヒートマップで表示
  • 散布図: BERTopicトピックを2次元空間に配置し、色でLDA対応を可視化

2. 分析環境と前提

前提として、既存記事で作成したデータを利用します。

  • df_lda:LDAで抽出した各文書の主トピック(列 topic_id
  • docinfo:BERTopicの出力(列 Topic
  • topic_model:BERTopicモデルオブジェクト

いずれも同じレビュー群をもとにしており、文書インデックスが一致している必要があります。 もし異なる場合は reset_index(drop=True) で整列してください。


3. トピック対応表(ヒートマップ+テーブル)

LDAの主トピックとBERTopicトピックの対応関係をクロス集計します。 各BERTopicトピックがどのLDAトピックに多く含まれるかを行正規化し、ヒートマップで比率を可視化します。

import pandas as pd
import numpy as np
import plotly.express as px

# 前提:df_lda["topic_id"], docinfo["Topic"] が存在する
cmp = pd.DataFrame({
    "lda_topic": df_lda["topic_id"].values,
    "bt_topic": docinfo["Topic"].values
})
cmp_no_out = cmp[cmp["bt_topic"] != -1].copy()

# クロス集計
cross = pd.crosstab(cmp_no_out["bt_topic"], cmp_no_out["lda_topic"])
row_sum = cross.sum(axis=1).replace(0, np.nan)
heat = cross.div(row_sum, axis=0).fillna(0)

# Plotlyヒートマップ
fig_heat = px.imshow(
    heat,
    aspect="auto",
    origin="lower",
    color_continuous_scale="Blues",
    labels=dict(x="LDA Topic", y="BERTopic", color="比率"),
    title="BERTopic → LDA 対応ヒートマップ(行正規化)"
)
fig_heat.update_layout(template="plotly_white", height=700)
fig_heat.write_html("bt_lda_heatmap.html", include_plotlyjs="cdn")

# トップ対応テーブル
top_map = heat.idxmax(axis=1).to_frame(name="best_lda_topic")
top_map["purity"] = heat.max(axis=1)
top_map["count"] = cross.sum(axis=1).astype(int)
top_map = top_map.reset_index().rename(columns={"bt_topic":"BERTopic"})
top_map = top_map.sort_values(["purity","count"], ascending=[False, False])
top_map.head(10)

解釈: 各BERTopicトピックが、LDAのどのトピックに最も対応しているか(best_lda_topic)と、その割合(purity=行内最大比率)を確認できます。純度が高いほど両者の一致度が高いです。

BERTopic → LDA の対応関係(行正規化)。各BERTopicトピックが、どのLDAトピックに最も対応するかを比率で可視化。

4. トピック散布図(色=LDA対応、サイズ=件数)

BERTopicトピックを埋め込みベクトル空間上で2次元化(PCA)し、LDA対応トピックを色で表現します。

from sklearn.decomposition import PCA
import plotly.express as px

info = topic_model.get_topic_info()
info_nz = info[info["Topic"] != -1].copy()

topic_id_to_row = {tid:i for i, tid in enumerate(info["Topic"].values)}
rows = [topic_id_to_row[tid] for tid in info_nz["Topic"].values]
emb = topic_model.topic_embeddings_[rows]

pca = PCA(n_components=2, random_state=42)
xy = pca.fit_transform(emb)

plot_df = info_nz.copy()
plot_df["x"] = xy[:,0]
plot_df["y"] = xy[:,1]
plot_df = plot_df.merge(top_map.rename(columns={"BERTopic":"Topic"}), on="Topic", how="left")

fig_scatter = px.scatter(
    plot_df,
    x="x", y="y",
    color="best_lda_topic",
    size="Count",
    hover_name="Name",
    hover_data={"Topic":True, "purity":":.2f", "Count":True, "x":False, "y":False},
    title="BERTopicトピック散布図(色=LDA対応、サイズ=件数)"
)
fig_scatter.update_layout(template="plotly_white", height=700)
fig_scatter.write_html("bt_scatter_with_lda_color.html", include_plotlyjs="cdn")

空間的に近いBERTopicトピックは内容が類似しており、同色(同じLDAテーマ)にまとまる場合は両モデルの一致が強いことを示します。

BERTopicのトピックを2次元空間に射影。色は対応するLDAトピック、円の大きさは文書数を示します。

5. 自動コメントによる一次解釈

トピック純度(LDAとの一致率)と文書数を基準に、自動で一次解釈コメントを出力します。

k = 10
sample = plot_df.sort_values(["purity","Count"], ascending=[False, False]).head(k)

for _, r in sample.iterrows():
    tid = r["Topic"]
    name = r["Name"]
    best = int(r["best_lda_topic"]) if pd.notna(r["best_lda_topic"]) else None
    pur = r["purity"]
    cnt = int(r["Count"])
    print(f"[BT {tid}] {name} | 文書数={cnt} | LDA対応={best} | 純度={pur:.2f}")
    if pur >= 0.65:
        print(" - 解釈: LDAでもほぼ同一テーマとして出現。両手法で安定した話題。")
    elif pur >= 0.45:
        print(" - 解釈: 主題は共通するが、副次的トピックが混在。粒度差あり。")
    else:
        print(" - 解釈: LDAとは異なる話題構成。min_topic_sizeの再調整も検討。")
    print()

6. 結果の読み方と考察

  • 一致領域: LDAとBERTopicのトピックが同じ色でまとまる領域は「明確な話題のコア」。この領域は分析上も安定して再現性が高い。
  • ズレ領域: LDAが1テーマとした範囲を、BERTopicが細分化しているケースは、語彙的には同じでも文脈的に異なる話題を分離している可能性がある。
  • ノイズ・外れ値: Topic = -1 は外れ値(HDBSCANのノイズ扱い)。多い場合はそれ単独で再学習すると有意な話題群になることがある。

7. まとめ

  • LDAは「語彙ベースの確率モデル」ゆえに、均等なトピック分割を得意とします。
  • BERTopicは「文脈ベースのクラスタリング」ゆえに、自然な粒度のトピックを発見します。
  • 両者を比較することで、「どの話題が安定して検出されるか」「どの部分に粒度差があるか」を俯瞰できます。

-データ活用, テキスト分析