この記事では、Google Playレビューの本文を対象に、ワードクラウド、共起ネットワーク、TF-IDFを使って、レビュー内で多く語られている特徴を可視化する方法を解説します。
星評価やレビュー件数の推移だけでは、「ユーザーが何に満足しているのか」「どの点に不満を持っているのか」までは分かりません。レビュー本文をテキスト分析することで、評価の背景にある具体的な話題を把握しやすくなります。
本記事では、取得済みのGoogle PlayレビューCSVを使い、前処理、形態素解析、ストップワード除去、ワードクラウド、共起ネットワーク、TF-IDFによる特徴語抽出までをPythonで実装します。
この記事でできること
- Google Playレビュー本文をPythonで前処理する
- 日本語レビューを形態素解析して単語に分割する
- 表記ゆれや不要語を調整する
- ワードクラウドで頻出語を可視化する
- 共起ネットワークで一緒に語られやすい単語を可視化する
- TF-IDFで星評価ごとの特徴語を比較する
- レビュー本文を分析するときの注意点を理解する
想定読者
- Google Playレビューの本文を分析したい方
- レビュー内で多い不満点や満足点を可視化したい方
- ワードクラウドやTF-IDFを実データで試したい方
- 日本語テキスト分析の前処理例を知りたい方
- レビュー分析記事やアプリ比較の材料を作りたい方
事前準備
この記事では、Google Playレビューを取得済みで、CSVファイルとして保存されている前提で進めます。レビュー取得がまだの場合は、先に以下の記事を確認してください。
Google Playのデータ取得方法まとめ|google-play-scraperでアプリ情報・レビューを取得する
今回使う主な列は以下です。
| 列名 | 内容 |
|---|---|
content | レビュー本文 |
score | 星評価 |
at | レビュー投稿日 |
reviewCreatedVersion | レビュー投稿時のアプリバージョン |
本記事では、例として reviews_genshin_paged.csv というCSVファイルを読み込みます。ファイル名は手元のデータに合わせて変更してください。
3つの分析手法の使い分け
レビュー本文の分析では、目的に応じて手法を使い分けると解釈しやすくなります。
| 手法 | 見たいこと | 向いている用途 |
|---|---|---|
| ワードクラウド | よく出てくる単語 | レビュー全体の雰囲気をざっくり見る |
| 共起ネットワーク | 一緒に語られやすい単語の関係 | 話題のまとまりや論点のつながりを見る |
| TF-IDF | 特定グループで特徴的な単語 | 星1と星5など、評価別の違いを見る |
ワードクラウドは直感的に見やすい一方、頻出語に偏りやすいです。TF-IDFはグループごとの差を見やすく、共起ネットワークは単語同士の関係を確認しやすいという特徴があります。
使用ライブラリ
日本語レビューの形態素解析には Janome、可視化には wordcloud、networkx、plotly、TF-IDFには scikit-learn を使います。
pip install -U pandas janome scikit-learn wordcloud networkx plotlyワードクラウドで日本語を表示するには、日本語フォントのパス指定が必要です。Windows環境では、たとえば C:/Windows/Fonts/meiryo.ttc を指定できます。
CSVを読み込む
まず、取得済みのレビューCSVを読み込みます。レビュー本文とスコアがない行は除外します。
import pandas as pd
csv_path = "reviews_genshin_paged.csv"
df = pd.read_csv(csv_path)
df = df.dropna(subset=["content", "score"]).copy()
df["score"] = df["score"].astype(int)
print(df[["content", "score"]].head())
print("rows:", len(df))星評価別にレビュー本文を比較するため、score は整数に変換しておきます。
日本語レビューの前処理を行う
日本語レビューをそのまま分析すると、表記ゆれ、数字、記号、URL、絵文字、一般的すぎる単語などがノイズになります。まずは、レビュー本文を分析しやすい単語リストに変換します。
import re
import unicodedata
from collections import Counter
from janome.tokenizer import Tokenizer
tokenizer = Tokenizer(wakati=False)
NORMALIZE_MAP = {
"出来る": "できる",
"出来": "できる",
"ゲー": "ゲーム",
"スマ": "スマホ",
"おもろい": "面白い",
"言う": "いう",
"カク": "カクつく",
}
def normalize_surface(text: str) -> str:
text = unicodedata.normalize("NFKC", str(text))
text = NORMALIZE_MAP.get(text, text)
text = re.sub(r"ー{2,}", "ー", text)
return text
def tokenize_ja(text: str) -> list[str]:
text = str(text)
text = re.sub(r"http\S+|www\.\S+", " ", text)
text = re.sub(r"[0-90-9]+", " ", text)
text = re.sub(r"[^\wぁ-んァ-ン一-龥ー]", " ", text)
tokens = []
for token in tokenizer.tokenize(text):
base = token.base_form if token.base_form != "*" else token.surface
base = normalize_surface(base)
pos = token.part_of_speech.split(",")[0]
if pos in {"名詞", "形容詞", "動詞"} and base and len(base) > 1:
tokens.append(base)
return tokens
df["tokens_raw"] = df["content"].map(tokenize_ja)
print(df[["content", "tokens_raw"]].head())ここでは、名詞・形容詞・動詞を中心に抽出しています。分析対象によっては、固有名詞や名詞だけに絞るなど調整してもよいです。
ストップワードを設定する
ストップワードとは、分析上あまり意味を持たないため除外したい単語です。レビュー分析では、「する」「ある」「いい」「ゲーム」など、頻出するものの解釈に使いづらい語が多く出ることがあります。
本記事では、手動で指定するストップワードと、文書全体で出すぎている単語を自動的に除外する方法を組み合わせます。
base_stopwords = {
"する", "なる", "いる", "ある", "できる", "てる", "やる",
"れる", "いう", "思う", "多い", "すぎる",
}
domain_stopwords = {
"ゲーム", "ゲー", "スマホ", "面白い", "楽しい",
"最高", "いい", "良い", "悪い", "最悪", "普通", "神",
}
keepwords = {
"ガチャ", "課金", "キャラ", "ストーリー",
"バグ", "音楽", "UI", "重い", "容量",
}
def learn_corpus_stopwords(tokens_series, df_threshold: float = 0.3, keepwords=None) -> set[str]:
keepwords = keepwords or set()
doc_count = len(tokens_series)
df_counts = Counter()
for tokens in tokens_series:
df_counts.update(set(tokens))
return {
word
for word, count in df_counts.items()
if count / doc_count >= df_threshold and word not in keepwords
}
auto_stopwords = learn_corpus_stopwords(
df["tokens_raw"],
df_threshold=0.3,
keepwords=keepwords,
)
STOPWORDS = base_stopwords | domain_stopwords | auto_stopwords
def remove_stopwords(tokens: list[str]) -> list[str]:
return [word for word in tokens if word not in STOPWORDS]
df["tokens"] = df["tokens_raw"].map(remove_stopwords)
print("auto_stopwords:", sorted(auto_stopwords)[:50])
print(df[["tokens_raw", "tokens"]].head())ストップワードは一度で完成するものではありません。ワードクラウドやTF-IDFの結果を確認しながら、不要な語を少しずつ追加していくと精度が上がります。
スコア別にデータを分ける
星1〜星5で語られる内容を比較するため、スコア別にDataFrameを分けておきます。
dfs = {
score: score_df.copy()
for score, score_df in df.groupby("score")
}
for score in range(1, 6):
print(score, len(dfs.get(score, [])))星1・星2は不満点、星4・星5は満足点、星3は中立的な意見として見やすいですが、実際にはレビュー本文を確認しながら解釈する必要があります。
ワードクラウドで頻出語を可視化する
ワードクラウドは、レビュー本文に多く出てくる単語を大きく表示する可視化方法です。まず、星評価ごとに頻出語を確認します。
from pathlib import Path
from collections import Counter
import itertools
from wordcloud import WordCloud
FONT_PATH = "C:/Windows/Fonts/meiryo.ttc"
output_dir = Path("wc_images")
output_dir.mkdir(parents=True, exist_ok=True)
def wordcloud_from_tokens(
tokens: list[str],
output_path: Path,
max_words: int = 200,
) -> None:
freq = Counter(tokens)
wc = WordCloud(
width=1200,
height=800,
background_color="white",
font_path=FONT_PATH,
collocations=False,
max_words=max_words,
)
wc.generate_from_frequencies(freq)
wc.to_file(str(output_path))
for score in range(1, 6):
score_df = dfs.get(score)
if score_df is None or score_df.empty:
continue
tokens = list(itertools.chain.from_iterable(score_df["tokens"]))
if not tokens:
continue
output_path = output_dir / f"wordcloud_score_{score}.png"
wordcloud_from_tokens(tokens, output_path)
print(f"score={score}: saved {output_path}")ワードクラウドは全体傾向を直感的に見るのに向いています。ただし、頻出語が必ずしも重要語とは限らないため、後述するTF-IDFやレビュー本文の確認と組み合わせて使うのがおすすめです。
共起ネットワークで単語のつながりを見る
共起ネットワークでは、同じレビュー内や近い位置に出てきやすい単語同士をつないで可視化します。単語単体ではなく、「どの言葉が一緒に語られているか」を見るのに向いています。
たとえば、「容量」と「重い」、「ガチャ」と「課金」、「ストーリー」と「キャラ」のような関係が見えると、レビュー内の論点を把握しやすくなります。
import math
from collections import defaultdict
import networkx as nx
import plotly.graph_objects as go
def build_cooccurrence_network(
tokens_list: list[list[str]],
window: int = 8,
min_edge: int = 8,
min_node_freq: int = 10,
top_k_nodes: int = 80,
) -> nx.Graph:
pair_counts = defaultdict(int)
node_counts = Counter()
for tokens in tokens_list:
node_counts.update(tokens)
for i in range(len(tokens)):
for j in range(i + 1, min(i + window, len(tokens))):
word_a, word_b = sorted([tokens[i], tokens[j]])
if word_a != word_b:
pair_counts[(word_a, word_b)] += 1
valid_nodes = {
word
for word, count in node_counts.items()
if count >= min_node_freq
}
top_nodes = {
word
for word, _ in node_counts.most_common(top_k_nodes)
}
valid_nodes = valid_nodes & top_nodes
graph = nx.Graph()
for word in valid_nodes:
graph.add_node(word, size=node_counts[word])
for (word_a, word_b), count in pair_counts.items():
if (
count >= min_edge
and word_a in valid_nodes
and word_b in valid_nodes
):
graph.add_edge(word_a, word_b, weight=count)
return graph
def plot_network_plotly(
graph: nx.Graph,
title: str = "共起ネットワーク",
height: int = 900,
):
if len(graph.nodes) == 0:
print("表示できるノードがありません。")
return None
k = 1.2 / (math.sqrt(len(graph.nodes)) + 1e-6)
pos = nx.spring_layout(graph, k=k, seed=42, iterations=100)
edge_x = []
edge_y = []
for node_a, node_b in graph.edges():
x0, y0 = pos[node_a]
x1, y1 = pos[node_b]
edge_x += [x0, x1, None]
edge_y += [y0, y1, None]
edge_trace = go.Scatter(
x=edge_x,
y=edge_y,
mode="lines",
line=dict(width=0.6, color="#bbb"),
hoverinfo="none",
)
node_x = []
node_y = []
hover_text = []
sizes = []
for node, data in graph.nodes(data=True):
node_x.append(pos[node][0])
node_y.append(pos[node][1])
hover_text.append(f"{node}(freq={data['size']})")
sizes.append(6 + 2 * math.log1p(data["size"]))
node_trace = go.Scatter(
x=node_x,
y=node_y,
mode="markers+text",
text=list(graph.nodes()),
textposition="top center",
hovertext=hover_text,
hoverinfo="text",
marker=dict(
size=sizes,
line=dict(width=0.6, color="#333"),
color="#4c78a8",
),
)
fig = go.Figure([edge_trace, node_trace])
fig.update_layout(
title=title,
template="plotly_white",
showlegend=False,
height=height,
xaxis=dict(visible=False),
yaxis=dict(visible=False),
margin=dict(l=10, r=10, t=60, b=10),
)
fig.show()
return fig以下は、星評価ごとに共起ネットワークを作成し、HTMLとして保存する例です。
from pathlib import Path
output_dir = Path("co_graphs")
output_dir.mkdir(parents=True, exist_ok=True)
for score in range(1, 6):
score_df = dfs.get(score)
if score_df is None or score_df.empty:
continue
tokens_list = score_df["tokens"].tolist()
graph = build_cooccurrence_network(
tokens_list,
window=8,
min_edge=8,
min_node_freq=10,
top_k_nodes=80,
)
fig = plot_network_plotly(
graph,
title=f"共起ネットワーク(score={score})",
)
if fig is not None:
output_path = output_dir / f"co_network_score_{score}.html"
fig.write_html(str(output_path), include_plotlyjs="cdn")
print(f"saved: {output_path}")共起ネットワークは、ノード数やエッジ数が多すぎると見づらくなります。min_edge、min_node_freq、top_k_nodes を調整し、読み取れる範囲に絞ることが重要です。
TF-IDFで星評価ごとの特徴語を比較する
TF-IDFは、あるグループで相対的に特徴的な単語を抽出する方法です。ここでは、星1〜星5のレビュー本文をそれぞれ分析し、評価ごとに特徴的な単語を比較します。
頻出語だけを見ると、どの評価にも共通して出る単語が目立ちやすくなります。TF-IDFを使うと、特定の評価グループで目立つ語を見つけやすくなります。
import numpy as np
from sklearn.feature_extraction.text import TfidfVectorizer
def tokenizer_for_vectorizer(text: str) -> list[str]:
tokens = tokenize_ja(text)
return [token for token in tokens if token not in STOPWORDS]
vectorizer = TfidfVectorizer(
tokenizer=tokenizer_for_vectorizer,
token_pattern=None,
max_features=20000,
min_df=5,
)
top_n = 20
tops = {}
for score in range(1, 6):
texts = df[df["score"] == score]["content"].fillna("")
if len(texts) == 0:
tops[score] = []
continue
X = vectorizer.fit_transform(texts)
vocab = np.array(vectorizer.get_feature_names_out())
mean_scores = X.mean(axis=0).A1
top_idx = np.argsort(mean_scores)[::-1][:top_n]
tops[score] = list(vocab[top_idx])
rows = []
for i in range(top_n):
row = {"rank": i + 1}
for score in range(1, 6):
values = tops.get(score, [])
row[f"score_{score}"] = values[i] if i < len(values) else ""
rows.append(row)
tfidf_table = pd.DataFrame(rows)
tfidf_table.to_csv(
"tfidf_top20_by_score.csv",
index=False,
encoding="utf-8-sig",
)
print(tfidf_table.head(20))この表を見ることで、低評価レビューで特徴的な不満語、高評価レビューで特徴的な満足語を比較しやすくなります。
複合語も見たい場合は、TfidfVectorizer の ngram_range を使う方法もあります。ただし、日本語では分かち書きや前処理の影響を受けるため、まずは1語単位で確認するのがおすすめです。
ストップワード調整の考え方
レビュー分析では、ストップワード調整が結果の見やすさに大きく影響します。初回実行で完璧な単語リストを作るのは難しいため、出力結果を見ながら何度か調整する前提で考えるのが実用的です。
たとえば、TF-IDF上位やワードクラウドに毎回出てくるが、解釈に使いづらい語があれば、ストップワードに追加します。
STOPWORDS |= {
"やる",
"すぎる",
"感じる",
"思う",
}一方で、「課金」「容量」「重い」「ガチャ」のような単語は、一見よく出る語でもレビュー分析では重要な論点になることがあります。除外する前に、分析目的に照らして残すべきか確認してください。
テキスト分析結果を見るときの注意点
レビュー本文のテキスト分析では、以下の点に注意が必要です。
- 取得したレビューは、取得時点のスナップショットです。
- 同じユーザーがレビューを更新した場合、過去の本文は上書きされる可能性があります。
- レビュー本文には、誤字、表記ゆれ、絵文字、スラング、不適切表現が含まれる場合があります。
- ワードクラウドは見やすい一方、頻出語の印象に引っ張られやすいです。
- TF-IDFで上位に出た語が、必ずしも重要な不満点・満足点とは限りません。
- 共起ネットワークはパラメータによって見え方が大きく変わります。
- 最終的な解釈では、実際のレビュー本文を確認することが重要です。
テキスト分析は、レビュー本文を読むための補助として使うのが基本です。単語の出現だけで断定せず、レビュー本文の文脈や時期、星評価、バージョン情報と合わせて解釈します。
取得した特徴語の活用例
ワードクラウド、共起ネットワーク、TF-IDFで得られた特徴語は、以下のような用途に使えます。
- 高評価レビューで多い魅力要素を整理する
- 低評価レビューで多い不満点を抽出する
- レビュー記事の「魅力」「改善点」の材料にする
- 感情分析やトピック抽出の前処理として確認する
- アプリごとの評価傾向を比較する
たとえば、低評価レビューで「重い」「落ちる」「容量」「バグ」が多い場合、パフォーマンスや安定性に関する不満が多い可能性があります。一方で、高評価レビューで「キャラ」「音楽」「ストーリー」が多い場合、世界観や演出面が魅力として語られている可能性があります。
次に読む記事
- Google Playレビューの感情分析入門|ポジ・ネガ傾向をPythonで月次可視化する方法
- Google Playレビューのトピック抽出入門|LDAとBERTopicで話題を可視化する方法
- Google Playレビューのトピック構造を比較する方法|LDAとBERTopicの対応関係を可視化
- Google Playレビュー可視化入門|日次・月次の評価推移をPlotlyで分析する方法
- Google Playのデータ取得方法まとめ|google-play-scraperでアプリ情報・レビューを取得する
- Google Playレビュー分析まとめ
まとめ
この記事では、Google Playレビュー本文を対象に、ワードクラウド、共起ネットワーク、TF-IDFを使って特徴語を可視化・比較する方法を紹介しました。
ワードクラウドは頻出語の俯瞰、共起ネットワークは単語同士の関係、TF-IDFは星評価ごとの特徴語比較に向いています。これらを組み合わせることで、レビュー本文からユーザーが語っている魅力や不満点を整理しやすくなります。
ただし、テキスト分析結果はあくまでレビューを読むための補助です。最終的には実際のレビュー本文、星評価、時期、アプリバージョンなどと合わせて解釈することが重要です。