はじめに
「どの運営会社(デベロッパー)が、どのジャンルに強いのか?」を、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")
5. 使い方のコツと注意点
- 検索語は複数指定して網羅性を高める(例:「RPG」「ロールプレイング」「放置 RPG」など)。
- search()は軽量検索のため、詳細(ratings, installs など)は
app()
で補完。 - 結果は検索ヒットの範囲に限定されるため、傾向把握の参考値として扱う。
- 会社の粒度(グループ会社/配信子会社など)はPlay表記依存。必要なら正規化辞書で統合。
- リクエスト間隔(
PAUSE_SEC
)は0.5〜1.0秒程度を推奨。過度な並列取得は避ける。
まとめ
- Google Playの検索→詳細取得→ユニーク化で、ジャンル×運営会社の強みを可視化できる。
- タイトル数やインストール帯の構成比、10M+の比率などで“強い会社”の輪郭が見える。
- 検索由来のサンプリングである点に留意しつつ、クエリ設計と可視化で実務的な示唆を引き出す。