Google Play データ活用

特定ジャンルに強いゲーム会社はどこ?Google Playデータで可視化する【Python】

2025年10月11日

はじめに

「どの運営会社(デベロッパー)が、どのジャンルに強いのか?」を、Google Playの検索から取得したデータでざっくり検証します。
検索でヒットしたタイトルをユニーク化し、ジャンル×運営会社でのタイトル数・インストール規模(区分)・平均スコアなどを可視化します。

注意(サンプル性):本記事は Google Play の search() 結果を用いるため、検索にヒットした範囲の傾向であり、各社の全タイトルを網羅しません。検索語・上限件数・地域設定に依存します。


1. セットアップ

pip install google-play-scraper pandas plotly tqdm
from google_play_scraper import search, app
import pandas as pd
import numpy as np
import time
from tqdm import tqdm
import plotly.express as px
import plotly.graph_objects as go

# ========== 設定 ==========
LANG = "ja"
COUNTRY = "jp"

# 検索クエリ(主要ジャンルの代表語を複数)
QUERIES = [
    "RPG", "ロールプレイング",
    "シミュレーション", "経営 シミュレーション",
    "パズル",
    "アクション",
    "アドベンチャー",
    "カジュアル",
    "シューティング",
    "カードゲーム",
    "音楽 ゲーム"
]

# 各クエリの取得上限(searchは通常~250-500件/クエリが上限目安。ここでは控えめに)
N_HITS_PER_QUERY = 200

# app() 詳細取得のリクエスト間隔(優しめ)
PAUSE_SEC = 0.6

2. 検索 → 詳細取得 → ユニーク化

def parse_installs_to_band(s: str) -> str:
    """
    "10,000,000+" のような文字列をインストール区分に正規化。
    例: 0-10K, 10K-100K, 100K-1M, 1M-10M, 10M+ など
    """
    if not isinstance(s, str) or s.strip()=="":
        return "unknown"
    ss = s.replace(",", "").replace("+", "")
    try:
        n = int(ss)
    except:
        return "unknown"
    if n < 10_000:
        return "0-10K"
    elif n < 100_000:
        return "10K-100K"
    elif n < 1_000_000:
        return "100K-1M"
    elif n < 10_000_000:
        return "1M-10M"
    else:
        return "10M+"

def fetch_search_results(queries, lang=LANG, country=COUNTRY, n_hits=N_HITS_PER_QUERY):
    items = []
    for q in tqdm(queries, desc="Searching"):
        try:
            res = search(q, lang=lang, country=country, n_hits=n_hits)
            for r in res:
                items.append({"query": q, **r})
        except Exception as e:
            print("検索失敗:", q, e)
    return pd.DataFrame(items)

def fetch_app_details(app_ids, lang=LANG, country=COUNTRY, pause=PAUSE_SEC):
    rows = []
    for aid in tqdm(app_ids, desc="Fetching details"):
        try:
            info = app(aid, lang=lang, country=country)
            rows.append(info)
            time.sleep(pause)
        except Exception as e:
            print("詳細取得失敗:", aid, e)
    return pd.DataFrame(rows)

# 1) 検索の統合結果
df_search = fetch_search_results(QUERIES)

# 2) appId をユニーク化(重複アプリは1件に)
unique_app_ids = sorted(set(df_search["appId"])) if len(df_search) else []
print("ユニークapp数:", len(unique_app_ids))

# 3) 詳細取得
df_app = fetch_app_details(unique_app_ids)

# 4) 必要列の抽出と正規化
cols = ["appId","title","score","ratings","installs","genre","developer","developerId","free","currency","price"]
df_app = df_app.reindex(columns=cols)
df_app["install_band"] = df_app["installs"].map(parse_installs_to_band)

# 5) 検索由来のクエリを代表1つ紐付け(最初の一致を採用)
first_query = (df_search.groupby("appId")["query"].first().rename("search_query"))
df_app = df_app.merge(first_query, how="left", left_on="appId", right_index=True)

# 保存(任意)
df_search.to_csv("gp_search_raw.csv", index=False, encoding="utf-8-sig")
df_app.to_csv("gp_app_details.csv", index=False, encoding="utf-8-sig")

3. 基本集計(ジャンル×運営会社)

# タイトル数(会社別)
dev_counts = (df_app.groupby("developer")
                    .agg(titles=("appId","nunique"),
                         avg_score=("score","mean"),
                         med_score=("score","median"),
                         total_ratings=("ratings","sum"))
                    .reset_index()
                    .sort_values("titles", ascending=False))

# インストール区分の比率(会社別)
pivot_inst = (df_app.pivot_table(index="developer", columns="install_band",
                                 values="appId", aggfunc="nunique", fill_value=0)
                     .reset_index())
# 合計と構成比
band_cols = [c for c in pivot_inst.columns if c not in ["developer"]]
pivot_inst["total_titles"] = pivot_inst[band_cols].sum(axis=1)
for c in band_cols:
    pivot_inst[c+"_ratio"] = (pivot_inst[c] / pivot_inst["total_titles"]).replace(np.nan, 0)

# ジャンル別タイトル数(会社×ジャンル)
dev_genre = (df_app.pivot_table(index="developer", columns="genre",
                                values="appId", aggfunc="nunique", fill_value=0)
                    .reset_index())

# 10M+ のタイトル割合(会社別)
high_band = (df_app.assign(is_10m=lambda d: d["install_band"].eq("10M+").astype(int))
                    .groupby("developer")["is_10m"].mean()
                    .rename("ratio_10m"))
summary = (dev_counts.merge(pivot_inst[["developer","total_titles","10M+_ratio"]], on="developer", how="left")
                    .merge(high_band, on="developer", how="left"))
summary = summary.fillna({"10M+_ratio":0, "ratio_10m":0})
summary.head()

4. 可視化(Plotly)

# A) タイトル数 上位20社(棒グラフ)
top20 = dev_counts.head(20).sort_values("titles")
fig1 = px.bar(top20, x="titles", y="developer", orientation="h",
              title="運営会社別 タイトル数(上位20)", text="titles")
fig1.update_traces(textposition="outside")
fig1.update_layout(template="plotly_white", height=900)
fig1.show()
# fig1.write_html("dev_top_titles.html", include_plotlyjs="cdn")

# B) 10M+ 比率 上位20社(最低タイトル数閾値あり)
min_titles = 3  # 例:タイトル3本以上の会社に限定
tmp = summary[summary["titles"] >= min_titles].copy()
tmp = tmp.sort_values("10M+_ratio", ascending=False).head(20).sort_values("10M+_ratio")
fig2 = px.bar(tmp, x="10M+_ratio", y="developer", orientation="h",
              title=f"運営会社別 10M+タイトル比率(上位20, タイトル数≥{min_titles})",
              text=tmp["10M+_ratio"].map(lambda v: f"{v:.0%}"))
fig2.update_traces(textposition="outside")
fig2.update_layout(template="plotly_white", height=900, xaxis_tickformat=".0%")
fig2.show()
# fig2.write_html("dev_ratio_10m.html", include_plotlyjs="cdn")

# C) インストール区分の構成比(スタック・上位10社)
stack_cols = [c for c in pivot_inst.columns if c.endswith("_ratio")]
top10_devs = dev_counts.head(10)["developer"]
stack_df = pivot_inst[pivot_inst["developer"].isin(top10_devs)].copy()
stack_df = stack_df.sort_values("total_titles", ascending=False)

disp_cols = ["0-10K_ratio","10K-100K_ratio","100K-1M_ratio","1M-10M_ratio","10M+_ratio"]
rename_map = {c:c.replace("_ratio","") for c in disp_cols}
fig3 = px.bar(stack_df, x="developer", y=disp_cols, barmode="stack",
              title="運営会社別 インストール構成比(上位10社)",
              labels={"value":"構成比","variable":"インストール帯"})
fig3.for_each_trace(lambda tr: tr.update(name=rename_map.get(tr.name, tr.name)))
fig3.update_layout(template="plotly_white", height=900, yaxis_tickformat=".0%")
fig3.show()
# fig3.write_html("dev_install_mix.html", include_plotlyjs="cdn")

# D) ジャンル×会社のヒートマップ(上位会社のみ)
genre_cols = [c for c in dev_genre.columns if c != "developer"]
heat_top = dev_genre[dev_genre["developer"].isin(top10_devs)].set_index("developer")[genre_cols]
fig4 = px.imshow(heat_top, aspect="auto", color_continuous_scale="Blues",
                 title="ジャンル×運営会社:タイトル数(上位会社)")
fig4.update_layout(template="plotly_white", height=900)
fig4.show()
# fig4.write_html("dev_genre_heatmap.html", include_plotlyjs="cdn")
検索ヒットをユニーク化した上での上位20社のタイトル数ランキング。
10M+インストール帯のタイトル割合(対象は一定以上のタイトル数を持つ会社に限定)。
0-10K/10K-100K/100K-1M/1M-10M/10M+ の構成比。会社ごとの分布の違いを比較。
上位会社におけるジャンル別のタイトル数。得意ジャンルの傾向を俯瞰。

5. 使い方のコツと注意点

  • 検索語は複数指定して網羅性を高める(例:「RPG」「ロールプレイング」「放置 RPG」など)。
  • search()は軽量検索のため、詳細(ratings, installs など)はapp()で補完。
  • 結果は検索ヒットの範囲に限定されるため、傾向把握の参考値として扱う。
  • 会社の粒度(グループ会社/配信子会社など)はPlay表記依存。必要なら正規化辞書で統合。
  • リクエスト間隔PAUSE_SEC)は0.5〜1.0秒程度を推奨。過度な並列取得は避ける。

まとめ

  • Google Playの検索→詳細取得→ユニーク化で、ジャンル×運営会社の強みを可視化できる。
  • タイトル数やインストール帯の構成比、10M+の比率などで“強い会社”の輪郭が見える。
  • 検索由来のサンプリングである点に留意しつつ、クエリ設計と可視化で実務的な示唆を引き出す。

-Google Play, データ活用