Google Playデータ取得 データ取得

LDAとBERTopicの結果を比較する方法|Google Playレビューのトピック構造を読む

2025年10月12日

LDAとBERTopicの保存済み割当を、モデルの再学習なしで比較します。対応比率は「割当の重なり」を示すもので、話題の正しさや意味の一致を採点する指標ではありません。

最短手順:ファイルを用意し、実行して出力を開く

同じ実行で作成したLDA・BERTopicの割当表を読み、どの組合せが何件あるかをCSVにします。この比較ではAPI通信・モデル学習は行いません。

掲載Python10ファイルのZIPをダウンロードし、「すべて展開」で任意の作業フォルダーへ展開します。ZIP内にサブフォルダーはありません。必要ファイルは gp_common.py、compare_gp_topics.py です。使わないファイルは実行しません。以下の長いコードを手で保存する場合も、同じ名前・同じフォルダーへ置きます。

Python導入済みのWindows PowerShellで実行します。最初の入力欄へ、展開後の gp_common.py が見えるフォルダーの絶対パスを貼り付けてEnterを押してください。引用符は入力不要です。以降もその作業フォルダーから実行します。インストールには通信が必要ですが、その後の保存CSV分析とは別です。

$work = Read-Host "ZIPを展開したフォルダーの絶対パス"
Set-Location -LiteralPath $work
Get-ChildItem *.py
python -m pip install pandas

事前に トピック抽出のBERTopic付き手順で両手法を同じ実行に保存しておきます。次の2つの入力欄に、そのtopicsフォルダーと学習時の元CSVを指定します。6行の練習データでは両手法が省略されるため、この比較まで成功する教材ではありません。

$topics = Read-Host "両手法が完了したtopicsフォルダーのパス"
$csv = Read-Host "その実行で使った元CSVのパス"
python compare_gp_topics.py "$topics" --input "$csv"

グループごとの件数・比率に続き、最後にtopic-comparisonフォルダーが表示されます。そこにあるcorrespondence.csvを最初に開きます。両手法が完了していない/入力が違うと停止します。過去の成功ファイルを混ぜて進めないでください。

まず開くファイル:知りたいことから選ぶ

組合せごとの対応件数
correspondence.csv:bertopic・lda・count・denominator・ratio。

各グループの最大割当比率
group_largest_share.csv:largest_lda・count・denominator・purity。数値だけで品質は判定しません。

対応するレビューを確認
joined_assignments.csvのrow_idを、元のtopicsフォルダーの*_assignments.csvに照合しcontent_rawを読む。外れ値数はmetadata.jsonのoutlier_rows。

CSVは表計算ソフトでも開けますが、保存し直すと型やファイルの確認値(hash)が変わる場合があります。元ファイルは上書きせず閲覧してください。図がない場合は、同じ実行フォルダーのmetadata.jsonにある skipped や手法別状態を確認します。

コードの前に:結果から何が分かるか

同じレビュー集合を比べなければ、手法の違いと入力の違いが混ざります。対応表は横長の行列ではなく、bertopic・ldaの組合せが1行の表です。行列として読むなら行をBERTopic、列をLDAに対応させます。ratioの分母はそのBERTopicグループの件数です。既存の小例「3件中2件がLDA 0」は2/3で、サイト全体や全レビューが分母ではありません。最大のratioをpurityと呼びますが、意味の一致や再学習時の安定性を表す値ではありません。−1の外れ値は対応件数から除外し、別に数を残します。

掲載コードと詳しい条件(ZIPと同じ内容)

トピック抽出記事で両手法を同時に実行したフォルダーを指定します。gp_common.pyと次のファイルが必要です。metadataとCSVのhash、run_id、元入力hash、row_id・本文・評価・日時を照合します。片方を省略したときは停止し、別フォルダーの結果で埋めません。

同じ実行の割当を比較する:compare_gp_topics.py

掲載コードを保存:compare_gp_topics.py

import argparse
import pandas as pd
from gp_common import Run, checked_run, digest

def compare(folder, input_path="outputs/google_play_reviews.csv", root="outputs/gp_runs"):
    folder, meta = checked_run(folder)
    if digest(input_path) != meta["input_sha256"]:
        raise ValueError("入力CSVが学習時と異なります。元ファイルを確認してください。")
    required = ["lda_assignments.csv", "bertopic_assignments.csv"]
    if meta.get("statuses", {}).get("lda") != "OK" or meta.get("statuses", {}).get("bertopic") not in {"OK", "ALL_OUTLIERS"}:
        raise ValueError("同じ実行で両手法が完了していません。手法別statusを確認してください。")
    if not all(name in meta["files"] for name in required):
        raise ValueError("片方のモデルが省略・失敗しています。別実行の成功ファイルを混ぜないでください。")
    tables = []
    for name in required:
        d = pd.read_csv(folder / name, dtype=str, keep_default_na=False)
        if d["row_id"].duplicated().any() or d["row_id"].eq("").any():
            raise ValueError("row_id重複・欠損")
        if not d["run_id"].eq(meta["run_id"]).all() or not d["input_sha256"].eq(meta["input_sha256"]).all():
            raise ValueError("別実行の結果が混ざっています。")
        tables.append(d.set_index("row_id").sort_index())
    a, b = tables
    columns = ["content_raw", "score_raw", "at_raw", "reviewId"]
    if not a[columns].equals(b[columns]) or set(a.index) != {str(i) for i in meta["selected_row_ids"]}:
        raise ValueError("ID・本文・評価・日付または分析対象が一致しません。")
    joined = pd.DataFrame({"lda": a["topic_id"], "bertopic": b["topic_id"]})
    outliers = int(joined["bertopic"].eq("-1").sum())
    valid = joined[joined["bertopic"].ne("-1")]
    count = valid.groupby(["bertopic", "lda"]).size().reset_index(name="count")
    count["denominator"] = count.groupby("bertopic")["count"].transform("sum")
    count["ratio"] = count["count"] / count["denominator"]
    summary = count.sort_values("count", ascending=False).drop_duplicates("bertopic").rename(columns={"lda": "largest_lda", "ratio": "purity"})
    run = Run("topic-comparison", folder / "metadata.json", {"source_run": meta["run_id"], "source_input_sha256": meta["input_sha256"]}, root)
    run.csv("correspondence.csv", count)
    run.csv("group_largest_share.csv", summary)
    run.csv("joined_assignments.csv", joined.reset_index())
    for _, row in summary.iterrows():
        print(f"BERTopic {row.bertopic}: {int(row.denominator)}件中{int(row['count'])}件がLDA {row.largest_lda} ({row.purity:.1%})。意味の一致は上位語と原文で確認。")
    return run.finish(paired_rows=len(joined), comparison_rows=len(valid), outlier_rows=outliers,
                      note="最大割当比率。意味の一致・正しさ・再学習時の安定性ではない。")

if __name__ == "__main__":
    p = argparse.ArgumentParser(); p.add_argument("run_folder"); p.add_argument("--input", default="outputs/google_play_reviews.csv")
    a = p.parse_args(); compare(a.run_folder, a.input)

purityから言えること・言えないこと

この計算のpurityは、各BERTopicグループ内で最も多いLDA割当の件数を、そのグループの比較対象件数で割った値です。BERTopicの-1は比較表から除外し、除外件数は別記します。例えば3件中2件がLDA 0なら2/3です。1件だけのグループでも1/1になり、LDAがほぼ全件を同じトピックへ割り当てた場合も高くなります。

高い値だけで意味の一致・話題の正しさ・再学習時の安定性は判断できません。0.65/0.45を品質基準とする自動コメントは使いません。correspondence.csvに件数・分母・比率、group_largest_share.csvに最大比率、metadataに比較対象件数・外れ値数を保存します。意味を確認するには同じ実行の*_terms.csvと元レビューを読みます。

旧形式の結果を使う場合:確認範囲を分ける

上の新形式とは代替手順です。従来のlda_document_topics.csv/bertopic_document_topics.csvしかない場合は、次の照合を維持します。入力全体の当時のhash・実行設定までは確認できません。時刻帯不明な日時にUTCを補わず、同じ壁時計値として照合します。結果を新しい実行の確認済み出力とは扱いません。

from pathlib import Path
import hashlib
import json
import pandas as pd
import numpy as np

def checked_results(lda, bt, source):
    meta = ["row_id", "content", "score", "at"]
    for name, frame, topic in [("LDA", lda, "topic_id"), ("BERTopic", bt, "Topic")]:
        missing = set(meta + [topic]) - set(frame.columns)
        if missing:
            raise ValueError(f"{name}: 必要列不足 {missing}")
        if frame["row_id"].isna().any() or frame["row_id"].duplicated().any():
            raise ValueError(f"{name}: 行IDの欠損・重複があります")
        numeric = pd.to_numeric(frame["row_id"], errors="coerce")
        if numeric.isna().any() or (numeric % 1 != 0).any():
            raise ValueError(f"{name}: 行IDは取得CSV内の整数行番号です")
        if frame[topic].isna().any():
            raise ValueError(f"{name}: トピック割り当て欠損")
    if set(lda["row_id"]) != set(bt["row_id"]):
        raise ValueError("LDA / BERTopicの行ID集合が異なります。欠落を確認してください")
    if not {"content", "score", "at"}.issubset(source.columns):
        raise ValueError("取得元CSVには content / score / at が必要です")
    raw = source.copy()
    raw.insert(0, "row_id", range(len(raw)))
    def canonical(frame):
        f = frame[meta].copy()
        f["row_id"] = pd.to_numeric(f["row_id"]).astype("int64")
        f["content"] = f["content"].fillna("").astype(str)
        f["score"] = pd.to_numeric(f["score"], errors="coerce")
        f["at"] = f["at"].fillna("").astype(str).map(lambda v: str(pd.Timestamp(v)) if v.strip() and not pd.isna(pd.to_datetime(v, errors="coerce")) else "")
        return f.set_index("row_id").sort_index()
    left, right, original = canonical(lda), canonical(bt), canonical(raw)
    if not left.index.isin(original.index).all():
        raise ValueError("取得元CSVに存在しない行IDがあります")
    for field in ["content", "score", "at"]:
        for other in [right, original.reindex(left.index)]:
            a, b = left[field], other[field]
            if not ((a == b) | (a.isna() & b.isna())).all():
                raise ValueError(f"入力不一致: {field}。並び替えず元CSVと結果を確認してください")
    return lda[["row_id", "topic_id"]].merge(
        bt[["row_id", "Topic"]], on="row_id", how="inner", validate="one_to_one"
    ).rename(columns={"topic_id": "lda_topic", "Topic": "bt_topic"})

source_path = Path("outputs/google_play_reviews.csv")
lda = pd.read_csv("lda_document_topics.csv", dtype=str)
bt = pd.read_csv("bertopic_document_topics.csv", dtype=str)
source = pd.read_csv(source_path, dtype=str)
result = checked_results(lda, bt, source)
print(result.head(), "照合件数:", len(result))

従来の分析グラフ(保存済みの例)

以下は以前の取得条件・コードで作成した保存済みの実分析例です。今回の改訂コードで再取得・再集計した結果ではありません。読者のローカルCSVがこの見本へ送信・反映される仕組みではありません。旧見本には外部Plotly CDNへ接続するものがあり、オフライン用の新しい出力HTMLとは異なります。値を比較する際は元の取得範囲を確認してください。

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

BERTopic → LDA 対応ヒートマップ。各BERTopicトピックが、LDAのどのトピックに対応しやすいかを可視化しています。

BERTopicトピックを2次元空間に配置し、対応するLDAトピックで色分けしています。

-Google Playデータ取得, データ取得