1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Steamにおける日本の美少女ゲームについて市場分析してみた

Posted at

はじめに

近年、PCゲームプラットフォーム「Steam」において、「ビジュアルノベル(VN)」は一大ジャンルとして確固たる地位を築いています。今年には当該ジャンルにフォーカスしたセールも開催されています。

今回焦点を当てるのはヴィジュアルノベルの中でも、日本産の美少女ゲームを中心とした作品群です。

大体2014年頃から日本の美少女ゲームがパブリッシャーにローカライズされた形で市場に増え始めました。
主要パブリッシャーの1つである「Sekai Project」が、Kickstarterにて『CLANNAD』の英語版の資金を集め始めると、目標額の140,000ドルを大きく超え、最終的には500,000ドル以上の資金を集めたことが話題になりました。


そこで、本記事では多くの美少女ゲームのローカライズを手掛ける下記のパブリッシャー3社に着目し、Steam市場でどのような販売戦略がとられているのかを簡易的に分析しました。

「HIKARI FIELD」

「Sekai Project」

「Sekai Project」

目次

  1. 使用したライブラリなど
  2. 使用したデータ
  3. 価格やレビューに関して
  4. 対応言語
  5. ざっくりまとめ
  6. あとがき

使用したライブラリなど

Windows11
JupyterNotebook7.2.2

import pandas as pd
import requests
import time
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import platform
import os
import re  

使用したデータ

SteamDBからパブリッシャーごとにリリースタイトルを絞り込み、[Games only][Visual Novel]でフィルタリングしました。
そこからRating順にAppIDをひたすらコピペしてリストに格納しました。ゲームタイトルも途中までコピペしてましたが、結局タイトルもAppIDから引っ張ってくるので絶対にやらなくていいです。

# 分析対象のシードデータを作成
# https://steamdb.info/publisher/HIKARI+FIELD/?displayOnly=Game&tagid=3799
# https://steamdb.info/publisher/Sekai+Project/?displayOnly=Game&tagid=3799
# 「Games only」「Visual Novel」でフィルター
game_list = [
    # --- HIKARI FIELD ---
    {'publisher': 'HIKARI FIELD', 'game_name': 'Senren*Banka', 'appid': 1144400},
    {'publisher': 'HIKARI FIELD', 'game_name': '魔女的夜宴', 'appid': 2458530}, 
    # -----中略-----              
    {'publisher': 'HIKARI FIELD', 'game_name': 'Select Oblige', 'appid': 2947250}, 

    # --- Sekai Project ---
    {'publisher': 'Sekai Project', 'game_name': 'CLANNAD', 'appid': 324160},
    {'publisher': 'Sekai Project', 'game_name': 'NEKOPARA Vol. 2', 'appid': 420110},
    # -----中略----- 
    {'publisher': 'Sekai Project', 'game_name': 'A More Beautiful World - A Visual Novel', 'appid': 436680}
    
     # --- MangaGamer ---
    {'publisher': 'MangaGamer', 'game_name': '-', 'appid': 445670},
    {'publisher': 'MangaGamer', 'game_name': '-', 'appid': 1070240},
    # -----中略----- 
    {'publisher': 'MangaGamer', 'game_name': 'A More Beautiful World - A Visual Novel', 'appid': 436680}
]
]

# pandasのDataFrameに変換
df_seed = pd.DataFrame(game_list)

# AppIDの重複がないか確認(両社が同じゲームを扱う場合もあるため)
df_seed = df_seed.drop_duplicates(subset=['appid'], keep='first')

print(f"合計 {len(df_seed)} 件のゲームリストを作成")
df_seed.head()

image.png

続いてSteamAPIを叩いて(get_steam_app_details)各ゲームの情報を取得しました。

# Steam APIから詳細情報を取得
def get_steam_app_details(appid):
    
    url = f"https://store.steampowered.com/api/appdetails?appids={appid}&l=japanese"
    
    try:
        response = requests.get(url)
        response.raise_for_status() # エラーチェック
        
        data = response.json()
        
        if data and str(appid) in data and data[str(appid)]['success']:
            return data[str(appid)]['data'] 
        else:
            print(f"AppID {appid}: データの取得に失敗しました (success: false)")
            return None
            
    except requests.exceptions.RequestException as e:
        print(f"AppID {appid}: リクエストエラー: {e}")
        return None
    except Exception as e:
        print(f"AppID {appid}: 不明なエラー: {e}")
        return None

# 1件だけ動かす
if not df_seed.empty:
    test_appid = df_seed.iloc[0]['appid']
    game_name = df_seed.iloc[0]['game_name']
    
    print(f"--- テスト取得: {game_name} (AppID: {test_appid}) ---")
    test_data = get_steam_app_details(test_appid)
    
    if test_data:
        print(f"名前: {test_data.get('name')}")
        print(f"リリース日: {test_data.get('release_date', {}).get('date')}")
        print(f"ジャンル: {[genre.get('description') for genre in test_data.get('genres', [])]}")
        print(f"価格: {test_data.get('price_overview', {}).get('final_formatted', '価格情報なし')}")
        print(f"レビュー総数: {test_data.get('recommendations', {}).get('total', 0)}")
    else:
        print("テスト取得に失敗")

image.png

# 全ゲームの情報を取得して、1つのDataFrameに
all_game_data = []

# df_seedの1行(1ゲーム)ずつループ処理
for idx, row in df_seed.iterrows():
    appid = row['appid']
    publisher = row['publisher']
    game_name_seed = row['game_name'] 
    
    print(f"処理中 ({idx + 1}/{len(df_seed)}): {game_name_seed} (AppID: {appid})")
    
    data = get_steam_app_details(appid)
    
    if data:
        # 必要な情報だけを抜き出して、辞書にまとめる
        extracted_data = {
            'publisher': publisher,
            'appid': appid,
            'name': data.get('name'),
            'release_date': data.get('release_date', {}).get('date', ''),
            'is_free': data.get('is_free', False),
            'price_jpy': data.get('price_overview', {}).get('final', 0) / 100, # 価格は「銭」で来るから100で割るの
            'review_total': data.get('recommendations', {}).get('total', 0),
            'supported_languages': data.get('supported_languages'),           
            
            'genres': [genre.get('description') for genre in data.get('genres', [])],
            'tags': [tag.get('description') for tag in data.get('tags', [])], 
            'adult_only': data.get('adult_only', False) 
        }
        all_game_data.append(extracted_data)
    else:
        print(f" {game_name_seed} のデータ取得に失敗")

    time.sleep(1.5)

# リストをまとめてDataFrameに変換
df_main = pd.DataFrame(all_game_data)
df_main.head()

配信停止されているタイトルは取得に失敗します。

image.png

plt.rcParams['font.family'] = 'Yu Gothic' 

plt.figure(figsize=(8, 5))
sns.countplot(data=df_main, x='publisher', hue='publisher', palette='coolwarm', legend=False)
plt.title('パブリッシャー別 ゲーム本数', fontsize=16)
plt.xlabel('パブリッシャー')
plt.ylabel('ゲーム本数')

plt.show()

download.png

ここから無料・レビュー0件のデータを除きます。

plt.rcParams['font.family'] = 'Yu Gothic' 

output_dir = 'outputs'

# release_dateをdatetimeに変換
df_main['release_datetime'] = pd.to_datetime(df_main['release_date'], format='%d %b, %Y', errors='coerce')
df_main['release_year'] = df_main['release_datetime'].dt.year

# 分析用のデータをフィルタリング
df_analysis = df_main[
    (df_main['is_free'] == False) & 
    (df_main['review_total'] > 0)
].copy()

print(f"元データ: {len(df_main)}")
print(f"分析対象(無料・レビュー0件を除く): {len(df_analysis)}")

image.png

価格やレビューに関して

ざっくりとデータを見ていきます。

plt.figure(figsize=(10, 6))
sns.histplot(data=df_analysis, x='price_jpy', hue='publisher', 
             bins=30, multiple='stack', palette='coolwarm') # 積み上げグラフ
plt.title('パブリッシャー別:価格帯の分布 (有料ゲーム)', fontsize=16)
plt.xlabel('価格 (円)')
plt.ylabel('ゲーム本数')

plt.show()

plt.figure(figsize=(8, 6))
sns.boxplot(data=df_analysis, x='publisher', y='price_jpy', hue='publisher', palette='coolwarm', legend=False)
plt.xlabel('パブリッシャー')
plt.ylabel('価格 (円)')

plt.show()
#後略

download.png
download.png
download.png

  • どのタイトルも好評率75%以上と高め、良くも悪くも好みのユーザーしか手に取らないジャンルともいえる
  • HIKARI FIELDの方が高価格・高品質のタイトルが多い?→ローカライズするタイトルをより厳選している
  • Sekai Projectはインディー作品を多数取り扱い
  • MangaGamerは人気タイトルをチャプター別(〇〇編)にばら売りしているため、2000円未満に集中

先程定義したget_steam_app_detailsではレビューの好評率は取得できなかったため、別途取得してdf_analysis_with_reviewsへ更新しています(コード全文は最後に)。
次に時系列で見てみます。

# 時系列分析
# release_year が NaN(欠損)のデータは除外
df_time_analysis = df_analysis_with_reviews.dropna(subset=['release_year'])

 # 年とパブリッシャーでグループ化
df_grouped_year = df_time_analysis.groupby(['release_year', 'publisher']).agg(
        game_count=('appid', 'count'),
        avg_price=('price_jpy', 'mean'),
        avg_review=('review_total_new', 'mean'),
        avg_quality=('review_positive_percent_new', 'mean')
    ).reset_index()

# リリース本数の推移
plt.figure(figsize=(12, 6))
sns.lineplot(data=df_grouped_year, x='release_year', y='game_count', hue='publisher', 
                 marker='o', linewidth=2.5, palette='coolwarm', 
                 hue_order=["HIKARI FIELD", "Sekai Project", "MangaGamer"])
plt.title('パブリッシャー別:年間リリース本数の推移', fontsize=16)
plt.xlabel('リリース年')
plt.ylabel('ゲーム本数')
plt.legend(title='Publisher')
plt.grid(True, which='both', linestyle='--', linewidth=0.5)
plt.xlim(int(df_grouped_year['release_year'].min()) - 1, int(df_grouped_year['release_year'].max()) + 1)

plt.show()

# 平均価格の推移
plt.figure(figsize=(12, 6))
sns.lineplot(data=df_grouped_year, x='release_year', y='avg_price', hue='publisher', 
             marker='o', linewidth=2.5, palette='coolwarm', 
             hue_order=["HIKARI FIELD", "Sekai Project", "MangaGamer"])
plt.title('パブリッシャー別:平均価格の推移', fontsize=16)
plt.xlabel('リリース年')
plt.ylabel('平均価格 (円)')
plt.legend(title='Publisher')
plt.grid(True, which='both', linestyle='--', linewidth=0.5)
plt.xlim(int(df_grouped_year['release_year'].min()) - 1, int(df_grouped_year['release_year'].max()) + 1)

plt.show()

download.png
download.png

対応言語

このセクションでは無料とレビュー数0のゲームも含んで集計しました。なお、元データのsupported_languagesにはHTMLタグが含まれる等扱いづらいので整えて使いました。

def clean_languages(text):
    """ HTMLタグとアスタリスクを除去し、言語リストを返す """
    if not isinstance(text, str):
        return []
    
    # HTMLタグを除去 (例: <br>, <strong>)
    text = re.sub(r'<[^>]+>', '', text)
    # アスタリスクを除去
    text = text.replace('*', '')
    # 言語をコンマで分割
    languages = text.split(',')
    
    # 各言語の前後の空白を除去し、空文字列や不要な文字列を除外
    cleaned_languages = [
        lang.strip() for lang in languages 
        if lang.strip() and 'フル音声対応言語' not in lang and lang.strip() != ' '
    ]
    return cleaned_languages

# 'supported_languages' 列にクレンジング関数を適用
df_cleaned = df_main[['publisher', 'supported_languages']].copy()
df_cleaned['languages_list'] = df_cleaned['supported_languages'].apply(clean_languages)

# データを言語リストで展開 (explode)
df_exploded = df_cleaned.explode('languages_list')

# 'languages_list' 列の名前を 'language' に変更
df_exploded.rename(columns={'languages_list': 'language'}, inplace=True)
df_exploded.dropna(subset=['language'], inplace=True)


# 集計
# publisher と language でグループ化し、ゲーム数をカウント
df_agg = df_exploded.groupby(['publisher', 'language']).size().reset_index(name='game_count')

# グラフ
plt.figure(figsize=(15, 7)) 

sns.barplot(
    data=df_agg, 
    x='language',     
    y='game_count',   
    hue='publisher',  
    palette='coolwarm',
    hue_order=["HIKARI FIELD", "Sekai Project", "MangaGamer"]
)

plt.title('パブリッシャー別・対応言語別 ゲーム数', fontsize=16)
plt.xlabel('対応言語')
plt.ylabel('ゲーム本数')

plt.xticks(rotation=45, ha='right')
plt.legend(title='パブリッシャー', bbox_to_anchor=(1.02, 1), loc='upper left')
plt.tight_layout() 
# 後略

download.png
download.png

  • HIKARI FIELD: 中国語の中でも簡体字が使われるのは中国本土、シンガポール、マレーシアらしいので、その辺りが主要なターゲット
  • Sekai Project・MangaGamer: 英語圏がターゲット

ざっくりまとめ

HIKARI FIELD

  • ローカライズするタイトルをより厳選している印象、日本国内でも年間セールスランキング常連の『ゆずソフト』に代表される有名ソフトハウスのタイトルをリリース
  • ジャンルとしては後発ながら、市場規模最大の中国語圏のSteamユーザーにリーチ

Sekai Project

  • 英語圏のユーザーを相手に、Steamにおける日本産美少女ゲームの市場を開拓
  • リリースタイトルの多さで、ジャンルの立ち位置の維持に貢献

MangaGamer

  • Sekai Projectとターゲットは被るが、『ひぐらしのなく頃に』をはじめとした、ホラー・ミステリ作品が強み

これからSteamでビジュアルノベル作品を売りたいって方は英語と簡体字に対応するといいのかもしれません。市場規模で言えば簡体字ですね。

また、パブリッシャーのメインターゲットとなる言語圏が見えてきたところで、その地域での販売価格が鍵となってきます。

しかし、日本国内でSteam APIを叩く以上日本円しか返してくれないので、やるならAppIDコピペした時のように全タイトルの米ドルや元での販売価格(現地価格)をSteamDBからコピペするしかなさそうです。スクレイピングはやりたくないし、対策もされています。←Seleniumならできるかも?

とはいえ気になりはするので、HIKARI FIELDから配信されているいくつかのタイトルの、中国市場での販売価格を見てみました。

image.png
image.png
image.png
※2025年10月24日時点の情報です。
物価とかレートとか色々抜きにしても、中国ユーザーに対して特別安価な価格設定がされていそうです。日本国内で一定の販売価格が保たれている(=高品質な)タイトルを、厳選して、中国向けに販売といった具合でしょうか。
Sekai Projectも英語圏に参入した際には安くしていたんですかね。

ヴィジュアルノベルは平均でも文庫本7冊分のテキスト量とのことで、ローカライズのコストを考えるとHIKARI FIELDの戦略は至極合理的だと考えられます。

あとがき

ここからは分析とは関係ありません。

Sekai Projectがインディーゲームをばら撒いているだけって訳ではなくて、『CLANNAD』しかり『G線上の魔王』『BALDR SKY』等名作も沢山配信しています。
というよりソフトハウス毎にパブリッシャーが異なるって具合です。
自分の知る限りのところですと...
HIKARI FIELD

  • ゆずソフト
  • SAGA PLANETS
  • sprite(販売停止中)
  • Lump of Sugar(販売停止、2025年からkawaiiniumというパブリッシャーが取り扱っている)
  • あっぷりけ
  • Navel

Sekai Project

  • Visual Arts/key
  • あかべぇそふとつぅ
  • ぱれっと
  • まどそふと
  • FAVORITE
  • Whirlpool
  • NEKO WORKs

MangaGamer

  • BaseSon
  • Waffle
  • CIRCUS
  • 07th Expansion(『ひぐらし』『うみねこ』シリーズ等)

加えて、Sekai Projectは戯画やfengといった無くなってしまった有名ブランドのゲームも取り扱っています。特にfengは会社自体が倒産してしまったので、Sekai Projectへ権利が譲渡されたなんて背景があります。

てな感じで、コンテンツを広めるだけではなく大事にしてくれているんだなあと感じられたところで今回は終わります。


ここまでお読みいただき有難うございました。
使用した全コードは以下リンクにございます。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?