小さな辞書による感情分析を、判定語・重み・一致回数まで確認できる形で実行します。文脈を理解するモデルではなく、辞書にない語は未判定として残します。
最短手順:ファイルを用意し、実行して出力を開く
レビューCSVの語を小さな辞書と照合し、判定に使った語と月別件数を保存します。取得済みCSVの分析で、Google Playへの通信はありません。
掲載Python10ファイルのZIPをダウンロードし、「すべて展開」で任意の作業フォルダーへ展開します。ZIP内にサブフォルダーはありません。必要ファイルは gp_common.py、gp_text_analysis.py、gp_sentiment.py、plot_gp_saved.py(架空データならmake_gp_sample.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 Janome scikit-learn plotly入力は次のどちらか一方です。自分のCSVなら 取得・保存記事で確認したCSV のパスを指定します(必要列はcontent・score・at)。練習なら次だけ実行し、最後に表示された outputs/gp_runs/fictional-sample-… をコピーします。
python make_gp_sample.py次の入力欄にはCSVファイルのパスを貼り付けます。架空データの場合は、今コピーしたフォルダー名の末尾へ /google_play_reviews.csv を付けてください。… は実際の表示ではなく省略記号で、そのまま入力しません。自分のCSVなら例として固定した実行名を使わず、実在するファイルを指定します。
$csv = Read-Host "分析するCSVファイルのパス"
Test-Path -LiteralPath $csvTrue を確認してから進みます。False ならフォルダーではなくCSVまで指定したか、作業フォルダーが正しいかを確認してください。別のPowerShellを開いた場合は作業フォルダーと $csv を設定し直します。
python gp_sentiment.py "$csv"処理後、最後の行に今回の実行フォルダー(review-aggregates/text/sentimentから始まる名前)が表示されます。その行だけをコピーして、次の入力欄へ貼り付けます。これはCSVファイルではなくmetadata.jsonを含むフォルダーです。
$run = Read-Host "直前に表示された実行フォルダーのパス"
python plot_gp_saved.py "$run"さらに新しいsaved-plotsフォルダーが表示されます。元の集計CSVは先ほどのフォルダー、HTMLはこの新しいフォルダーです。HTMLをダブルクリックするとブラウザで開きます。0件・語彙なし等で図が省略された場合はmetadata.jsonのskippedを確認し、全図が揃うと決めつけないでください。
まず開くファイル:知りたいことから選ぶ
なぜこの判定になったか
review_sentiment.csv:content_raw・matched_terms・matched_count・sentiment_score・label。
月別に未判定がどれだけあるか
sentiment_monthly_summary.csv:review_count・unmatched_count・unmatched_ratio。mean_matched_scoreは一致語のある行だけ。
判定ごとの月別件数を見る
sentiment_monthly.csv:label・count・denominator・ratio。図はHTMLフォルダーのsentiment_monthly.html。
CSVは表計算ソフトでも開けますが、保存し直すと型やファイルの確認値(hash)が変わる場合があります。元ファイルは上書きせず閲覧してください。図がない場合は、同じ実行フォルダーのmetadata.jsonにある skipped や手法別状態を確認します。
コードの前に:結果から何が分かるか
辞書に一致した基本形の語について「重み×出現回数」を足します。架空例の「良い 悪い」は+1と−1が相殺してbalanced(0)ですが、辞書語がない「未知語だけ」はunmatched(スコア欠損)です。0と未判定は別物です。「楽しくない」から「楽しい」が一致するとpositiveになり得るため、否定や文脈を理解した判定ではありません。実アプリの精度を検証した例ではありません。
掲載コードと詳しい条件(ZIPと同じ内容)
入力の基本は outputs/google_play_reviews.csv(取得・保存手順)。content・score・atが必要です。reviewId・バージョンは文字列、row_idは入力CSVの0始まり行番号として保持します。元本文・元日時・元評価と、分析用の値を分けます。日時にオフセットがある行はUTCで集計し、旧CSVの時刻帯不明な行は壁時計の年月を別集団として扱います。lang・countryから時刻帯を推定しません。
gp_common.pyとgp_text_analysis.pyのtokenize関数を使います。これらを同じフォルダーへ保存してください。SENTIMENTを変更したら以下のファイル全体を再実行します。辞書のupdateだけでは既存CSVやグラフは変わりません。
辞書の判定根拠を保存する:gp_sentiment.py
import argparse
import json
from collections import Counter
import pandas as pd
from janome.tokenizer import Tokenizer
from gp_common import Run, load_reviews
from gp_text_analysis import tokenize
# 教材用の小さい辞書。課金・広告などを一律に負の語へ追加しない。
SENTIMENT = {"楽しい": 1, "良い": 1, "好き": 1, "重い": -1, "悪い": -1}
def analyze(path="outputs/google_play_reviews.csv", root="outputs/gp_runs", dictionary=SENTIMENT):
df = load_reviews(path); tokenizer = Tokenizer()
run = Run("sentiment", path, {"dictionary": dictionary, "method": "base-form exact token count; negation not resolved"}, root)
results, terms = [], Counter()
for _, row in df.iterrows():
counts = Counter(tokenize(row["content_raw"], tokenizer))
matched = [{"term": w, "weight": dictionary[w], "count": n} for w, n in counts.items() if w in dictionary]
score = sum(m["weight"] * m["count"] for m in matched)
label = "unmatched" if not matched else "positive" if score > 0 else "negative" if score < 0 else "balanced"
terms.update({m["term"]: m["count"] for m in matched})
results.append((json.dumps(matched, ensure_ascii=False), sum(m["count"] for m in matched), score if matched else None, label))
for i, col in enumerate(["matched_terms", "matched_count", "sentiment_score", "label"]):
df[col] = [r[i] for r in results]
run.csv("review_sentiment.csv", df)
valid = df[df["month"].ne("")]
monthly = valid.groupby(["date_basis", "month", "label"]).size().reset_index(name="count")
monthly["denominator"] = monthly.groupby(["date_basis", "month"])["count"].transform("sum")
monthly["ratio"] = monthly["count"] / monthly["denominator"]
run.csv("sentiment_monthly.csv", monthly)
summary = valid.groupby(["date_basis", "month"]).agg(
review_count=("row_id", "size"), scored_count=("sentiment_score", "count"),
mean_matched_score=("sentiment_score", "mean"), unmatched_count=("label", lambda s: int(s.eq("unmatched").sum()))).reset_index()
summary["unmatched_ratio"] = summary["unmatched_count"] / summary["review_count"]
run.csv("sentiment_monthly_summary.csv", summary)
run.csv("term_frequency.csv", pd.DataFrame(terms.most_common(), columns=["term", "occurrences"]))
return run.finish(input_rows=len(df), date_excluded=len(df)-len(valid),
limitation="単語辞書の例。否定や文脈は解決せず、星評価も正解ラベルとして使用しない。")
if __name__ == "__main__":
p = argparse.ArgumentParser(); p.add_argument("input", nargs="?", default="outputs/google_play_reviews.csv")
analyze(p.parse_args().input)未判定と相殺、割合と平均を区別する
review_sentiment.csvのmatched_termsに一致語・重み・回数を保存します。該当語0件はunmatchedでスコア欠損、正負が相殺した場合はbalancedでスコア0です。月別件数の分母には日付有効な未判定行も含みます。未判定率はunmatched_count / review_count、mean_matched_scoreは一致語がある行だけの平均スコアで、割合とは別の尺度です。日付欠損は月別だけから除外し、レビュー別表には残します。
架空例の「楽しくない」は基本形「楽しい」が一致してpositiveとなり得ます。「良い 悪い」はbalanced、「未知語だけ」はunmatchedです。星1でもpositiveになることがあり、星評価を正解ラベルとして判定を補正していません。否定や文脈を扱えない限界を確認する例で、実レビューの精度検証ではありません。課金・広告・イベント・アップデートへの重み追加は調整方法の例にすぎず、全アプリ共通の正解辞書ではありません。
従来の分析グラフ(保存済みの例)
以下は以前の取得条件・コードで作成した保存済みの実分析例です。今回の改訂コードで再取得・再集計した結果ではありません。読者のローカルCSVがこの見本へ送信・反映される仕組みではありません。旧見本には外部Plotly CDNへ接続するものがあり、オフライン用の新しい出力HTMLとは異なります。値を比較する際は元の取得範囲を確認してください。