Google Play データ活用

Google Playレビューのトピック構造を比較する方法|LDAとBERTopicの対応関係を可視化

2025年10月12日

この記事では、Google Playレビュー本文に対して LDABERTopic を適用した結果を比較し、両者のトピック構造の違いを可視化する方法を解説します。

LDAとBERTopicは、どちらもレビュー本文から話題のまとまりを抽出する手法ですが、トピックの作られ方が異なります。LDAは単語の出現パターンをもとにトピックを分け、BERTopicは文章の意味的な近さをもとにレビューをクラスタリングします。

本記事では、前回の記事で作成したLDAとBERTopicの出力を使い、トピック対応表、ヒートマップ、散布図を作成します。どの話題が両手法で安定して出ているのか、どの話題が片方の手法だけで細かく分かれているのかを確認します。

この記事でできること

  • LDAとBERTopicのトピック構造の違いを整理する
  • LDAトピックとBERTopicトピックの対応関係をクロス集計する
  • LDAトピック比率を確認する
  • BERTopicから見たLDA対応をヒートマップで可視化する
  • BERTopicトピックを2次元散布図で表示する
  • トピックの一致度を purity として確認する
  • 両手法の結果をどう解釈すればよいかを理解する

想定読者

  • LDAとBERTopicの違いを実データで比較したい方
  • Google Playレビューの話題構造をより深く見たい方
  • トピック抽出結果をそのまま使うのではなく、解釈の妥当性を確認したい方
  • レビュー分析記事で、話題分類の根拠を整理したい方
  • 教師なし学習の結果を可視化して比較したい方

事前準備

この記事は、以下の記事でLDAとBERTopicを実行済みであることを前提にしています。

Google Playレビューのトピック抽出入門|LDAとBERTopicで話題を可視化する方法

この記事で使う主なデータは以下です。

変数内容
df_ldaLDAで各レビューに割り当てた主トピックを含むDataFrame
df_bertopicBERTopicで各レビューに割り当てたトピックを含むDataFrame
topic_model学習済みのBERTopicモデル

LDAとBERTopicは、同じレビュー群に対して実行している必要があります。レビューの順番がずれていると対応関係を正しく比較できないため、必要に応じて reset_index(drop=True) でインデックスを揃えてください。

LDAとBERTopicの比較観点

まず、LDAとBERTopicの違いを整理します。

項目LDABERTopic
考え方単語の出現パターンからトピックを推定文章の意味ベクトルから近いレビューをクラスタリング
トピック数事前に指定するデータ構造に応じて自動で決まりやすい
得意なこと全体を一定粒度で分ける文脈や言い換えを含めて近い文をまとめる
注意点トピック数の設定で結果が変わる外れ値トピックや細かすぎるクラスタが出る場合がある

この比較では、どちらが絶対に正しいかを判定するのではなく、「どの話題が両方の手法で安定して出ているか」「どの話題はBERTopicで細分化されているか」を確認します。

使用ライブラリ

集計にはpandas、可視化にはPlotly、BERTopicトピックの2次元化にはPCAを使います。

pip install -U pandas numpy plotly scikit-learn

LDAとBERTopicの結果を結合する

まず、LDAの主トピックとBERTopicのトピックを1つのDataFrameにまとめます。

import pandas as pd
import numpy as np

# 例:
# df_lda["topic_id"] にLDAの主トピック
# df_bertopic["Topic"] にBERTopicの割り当てトピックが入っている前提

lda_topics = df_lda["topic_id"].reset_index(drop=True)
bt_topics = df_bertopic["Topic"].reset_index(drop=True)

compare_df = pd.DataFrame({
    "lda_topic": lda_topics,
    "bt_topic": bt_topics,
})

print(compare_df.head())
print(compare_df.shape)

BERTopicでは、Topic = -1 が外れ値トピックとして扱われます。対応関係を見るときは、必要に応じて -1 を除外します。

LDAトピック比率を確認する

比較に入る前に、LDAで抽出された各トピックの比率を確認します。どのトピックが全体の中で大きいかを見ておくと、後続のBERTopicとの対応関係も読み取りやすくなります。

LDAで抽出したトピック比率。レビュー全体でどの話題が多いかを確認します。

トピック比率は、後からトピック名を付けるための補助です。トピックIDだけでは意味が分からないため、上位語や代表レビュー本文と組み合わせて解釈します。

トピック対応表を作成する

LDAトピックとBERTopicトピックの対応関係をクロス集計します。ここでは、BERTopicトピックごとに、どのLDAトピックに多く対応しているかを確認します。

compare_no_outlier = compare_df[
    compare_df["bt_topic"] != -1
].copy()

cross = pd.crosstab(
    compare_no_outlier["bt_topic"],
    compare_no_outlier["lda_topic"],
)

row_sum = cross.sum(axis=1).replace(0, np.nan)

heatmap_ratio = cross.div(row_sum, axis=0).fillna(0)

print(heatmap_ratio.head())

この表は、行がBERTopicトピック、列がLDAトピックです。各行の値は、そのBERTopicトピックに含まれるレビューが、LDAではどのトピックに割り当てられているかの比率を表します。

対応関係をヒートマップで可視化する

クロス集計表をヒートマップにすると、対応関係を直感的に確認できます。

import plotly.express as px

fig_heatmap = px.imshow(
    heatmap_ratio,
    aspect="auto",
    origin="lower",
    labels={
        "x": "LDA Topic",
        "y": "BERTopic Topic",
        "color": "比率",
    },
    title="BERTopic → LDA 対応ヒートマップ",
)

fig_heatmap.update_layout(
    template="plotly_white",
    height=700,
)

fig_heatmap.show()

fig_heatmap.write_html(
    "bt_lda_heatmap.html",
    include_plotlyjs="cdn",
    full_html=True,
)
BERTopic → LDA 対応ヒートマップ。各BERTopicトピックが、LDAのどのトピックに対応しやすいかを可視化しています。

行の中で特定の列に色が集中している場合、そのBERTopicトピックはLDAでも同じような話題として検出されている可能性があります。逆に複数列に分散している場合は、LDAでは複数話題が混ざっている、またはBERTopicが別の粒度で分けている可能性があります。

purityで対応の強さを見る

各BERTopicトピックが、最も強く対応するLDAトピックを確認します。ここでは、行内の最大比率を purity として扱います。

topic_mapping = heatmap_ratio.idxmax(axis=1).to_frame(
    name="best_lda_topic"
)

topic_mapping["purity"] = heatmap_ratio.max(axis=1)
topic_mapping["count"] = cross.sum(axis=1).astype(int)

topic_mapping = (
    topic_mapping
    .reset_index()
    .rename(columns={"bt_topic": "bt_topic"})
    .sort_values(["purity", "count"], ascending=[False, False])
)

topic_mapping.to_csv(
    "bertopic_lda_topic_mapping.csv",
    index=False,
    encoding="utf-8-sig",
)

print(topic_mapping.head(20))

purity が高いほど、そのBERTopicトピックは特定のLDAトピックに強く対応しています。これは、両手法で安定して検出された話題の候補と考えられます。

BERTopicトピックを2次元散布図で見る

次に、BERTopicトピックを2次元空間に配置し、対応するLDAトピックで色分けします。これにより、意味的に近いBERTopicトピックが、LDAではどのトピックに対応しているかを確認できます。

BERTopicモデルに topic_embeddings_ がある場合、これをPCAで2次元化して可視化できます。

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

topic_info = topic_model.get_topic_info()
topic_info_no_outlier = topic_info[
    topic_info["Topic"] != -1
].copy()

topic_id_to_row = {
    topic_id: index
    for index, topic_id in enumerate(topic_info["Topic"].values)
}

rows = [
    topic_id_to_row[topic_id]
    for topic_id in topic_info_no_outlier["Topic"].values
]

topic_embeddings = topic_model.topic_embeddings_[rows]

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

plot_df = topic_info_no_outlier.copy()
plot_df["x"] = xy[:, 0]
plot_df["y"] = xy[:, 1]

plot_df = plot_df.merge(
    topic_mapping.rename(columns={"bt_topic": "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.show()

fig_scatter.write_html(
    "bt_scatter_with_lda_color.html",
    include_plotlyjs="cdn",
    full_html=True,
)
BERTopicトピックを2次元空間に配置し、対応するLDAトピックで色分けしています。

近い位置にあるBERTopicトピックは、意味的に近い話題である可能性があります。同じ色でまとまっている場合、LDAでも近い話題として扱われていると考えられます。

対応関係を自動コメントする

対応表をもとに、purityと件数から一次解釈コメントを出力します。これは最終判断ではなく、確認すべきトピックを探すための補助です。

top_n = 10

sample_topics = plot_df.sort_values(
    ["purity", "Count"],
    ascending=[False, False],
).head(top_n)

for _, row in sample_topics.iterrows():
    bt_topic = row["Topic"]
    topic_name = row["Name"]
    best_lda_topic = row["best_lda_topic"]
    purity = row["purity"]
    count = int(row["Count"])

    print(
        f"[BERTopic {bt_topic}] {topic_name} | "
        f"文書数={count} | LDA対応={best_lda_topic} | purity={purity:.2f}"
    )

    if purity >= 0.65:
        print(" - 解釈: LDAでも近いテーマとして検出されている可能性が高いです。")
    elif purity >= 0.45:
        print(" - 解釈: 主題は近いものの、副次的な話題が混ざっている可能性があります。")
    else:
        print(" - 解釈: LDAとは異なる粒度で分かれている可能性があります。")

    print()

このコメントは、トピック比較の入口として使えます。実際の判断では、各トピックの上位語と代表レビュー本文を確認してください。

結果の読み方

LDAとBERTopicの比較結果は、以下のように読み取ると分かりやすいです。

状態解釈例
purityが高いLDAとBERTopicの両方で似た話題として検出されている
purityが中程度主題は似ているが、片方の手法で複数の話題が混ざっている
purityが低い両手法でトピックの切り方が異なる
BERTopicが細かく分かれるLDAでは1つの話題に見えるものが、文脈上は複数に分かれている可能性がある
BERTopicの-1が多い外れ値として扱われたレビューが多く、前処理やパラメータ調整が必要な可能性がある

たとえば、LDAでは「操作性・端末負荷」としてまとまっている話題が、BERTopicでは「Androidのコントローラー対応」「PC版の重さ」「初回ダウンロード容量」に分かれることがあります。この場合、BERTopicの方が細かい文脈を拾っていると考えられます。

比較結果を見るときの注意点

LDAとBERTopicの比較では、以下の点に注意してください。

  • purityは対応関係を見るための参考指標であり、トピックの正しさを保証するものではありません。
  • LDAのトピック数を変えると、対応関係も変わります。
  • BERTopicのパラメータや外れ値の扱いによって、トピック数や粒度が変わります。
  • 前処理、ストップワード、表記ゆれ補正の影響を強く受けます。
  • 散布図の2次元配置は、情報を圧縮した表示であり、距離を厳密に解釈しすぎない方がよいです。
  • 最終的な解釈では、上位語と代表レビュー本文を必ず確認してください。

比較可視化は、トピック抽出結果を点検するための補助です。数値やグラフだけで判断せず、実際のレビュー本文を読みながらトピック名を付けることが重要です。

この比較の活用例

LDAとBERTopicの比較は、以下のような場面で役立ちます。

  • 安定して検出される主要トピックを見つける
  • BERTopicで細分化された話題を確認する
  • 低評価レビューの不満点を複数の観点に分ける
  • レビュー分析記事で、話題分類の根拠を説明する
  • モデルやパラメータを調整する必要がある箇所を見つける

たとえば、LDAとBERTopicの両方で「容量・重い・ダウンロード」に関する話題が出ている場合、そのテーマはレビュー内で安定して語られている可能性が高いです。一方で、BERTopicだけが細かく分けている話題は、より具体的な改善点やユーザー文脈を確認する入口になります。

次に読む記事

まとめ

この記事では、Google Playレビューに対してLDAとBERTopicを適用した結果を比較し、トピック構造の違いを可視化する方法を紹介しました。

LDAは語彙ベースで全体を一定粒度に分けやすく、BERTopicは文章の意味的な近さから自然なクラスタを作りやすいという特徴があります。両者を比較することで、安定して検出される話題と、手法によって分かれ方が変わる話題を見つけやすくなります。

ただし、purityや散布図はあくまで確認用の指標です。最終的には、上位語、代表レビュー、星評価、時期、アプリバージョンなどを合わせて確認し、人間がトピックの意味を解釈することが重要です。

-Google Play, データ活用