第2回:実在のニュースで景気変動を再現する
はじめに — ゲームのリアリティはデータで決まる
前回は、投資教育すごろくゲーム「MarketQuest」の構想と、4つのHTML5プロトタイプを経て「本格的なゲームエンジンが必要」という結論に至るまでの過程を振り返りました。
ゲームエンジンの選定と並行して、もう一つの重要な課題に取り組んでいました。それはゲームで使うデータの準備です。
MarketQuestの核心は、実在のニュースで景気変動を再現することにあります。プレイヤーが止まったマスで「バブル崩壊」「リーマンショック」といった実在の経済イベントが表示され、それに連動して株価が変動する。この仕組みがなければ、ただのサイコロゲームに過ぎません。
しかし、1980年から2020年までの40年分のニュースを手作業で収集し、それぞれの経済指標への影響を設定するのは膨大な作業です。
そこで、Pythonによるデータパイプラインを構築しました。
データパイプラインの全体像
最終的に構築したパイプラインは、以下の4つのJupyter Notebookで構成されています。
┌──────────────────────┐
│ correctNews.ipynb │ ← Wikipediaからニュースイベントを収集
└──────────┬───────────┘
▼
┌──────────────────────┐
│ CreateMacro.ipynb │ ← マクロ経済指標の時系列データを作成
└──────────┬───────────┘
▼
┌──────────────────────┐
│ SimulateStock.ipynb │ ← ニュースとマクロ指標から株価をシミュレーション
└──────────┬───────────┘
▼
┌──────────────────────┐
│ Normaralize.ipynb │ ← マクロデータの正規化
└──────────┬───────────┘
▼
news_YYYY_YYYY.json
nikkei_YYYY_YYYY.json
macro_YYYY_YYYY.json
各ノートブックの役割を順に見ていきましょう。
ニュース収集:correctNews.ipynb
最初のステップは、歴史的なニュースイベントの収集です。手作業ではなく、Wikipediaのスクレイピングで自動化しました。
英語版Wikipediaからの収集
まず、英語版Wikipediaの日付別ページ(例:January_1)から、1901年以降のイベントを取得します。
import requests
from bs4 import BeautifulSoup
from datetime import datetime
def fetch_events(month, day):
url = f"https://en.wikipedia.org/wiki/{month}_{day}"
response = requests.get(url)
soup = BeautifulSoup(response.text, 'html.parser')
# '1901-present' セクションを探す
headline = soup.find('span', id='1901–present')
if headline:
events_list = headline.find_next('ul')
else:
return []
event_list = []
if events_list:
for li in events_list.find_all('li'):
text = li.text
year_text = text.split(' – ')[0]
try:
year = int(year_text.strip())
event_text = ' – '.join(text.split(' – ')[1:])
event_list.append({
'date': datetime(year=year, month=month_to_int[month], day=int(day)),
'event': event_text
})
except ValueError:
continue
return event_list
1月1日から12月31日まで、365日分のページをクロールし、年でソートしてCSVに出力します。
日本語版Wikipediaからの収集
さらに、日本に特化したニュースを得るために、日本語版Wikipediaの年別ページ(例:1955年)からもスクレイピングしています。
for year in range(1954, 2024):
url = f"https://ja.wikipedia.org/wiki/{year}年"
response = requests.get(url)
soup = BeautifulSoup(response.text, 'html.parser')
events_section = soup.find('span', {'id': 'できごと'}).parent
months = events_section.find_all_next(['h3', 'ul'])
for element in months:
if element.name == 'h3':
current_month = element.text.strip()
if '月' not in current_month:
break
elif element.name == 'ul':
for item in element.find_all('li'):
text = item.get_text().strip()
if ' - ' in text:
date, event = text.split(' - ', 1)
print(f"{year}年{date} {event}")
「できごと」セクションの h3(月)と ul(イベントリスト)を交互に走査し、月ごとのイベントを抽出しています。
マクロ経済指標の構築:CreateMacro.ipynb
ニュースイベントだけでは、株価の動きを再現できません。マクロ経済指標の時系列データが必要です。
このノートブックでは、以下の指標を年次・四半期データから月次データに展開しています。
| 指標 | 出典 | 内容 |
|---|---|---|
| Population_index | 国勢調査 | 人口推移(百万人) |
| GDP_Growth | 内閣府 | GDP成長率(%) |
| Salary_production | 厚生労働省 | 給与・生産性指数 |
| Interest_Rate | 日本銀行 | 政策金利 |
| Inflation_Rate | 総務省 | 消費者物価指数 |
| Employment | 総務省 | 雇用者数 |
| Unemployment_Rate | 総務省 | 失業率 |
| WTI_energy | EIA | 原油価格 |
年次データの月次展開
年次でしか取得できないデータ(例:人口)は、同じ値を12ヶ月にコピーして展開します。
data_str = """
1961/1/1 100.90006
1962/1/1 101.84196
1963/1/1 102.87933
...
2022/1/1 134.23124
"""
data = {}
for line in data_str.strip().split("\n"):
date, value = line.split()
year = date.split('/')[0]
data[year] = float(value)
# 各年の各月に展開
for year in range(1961, 2023):
year_str = str(year)
if year_str in data:
for month in range(1, 13):
print(f'{year}/{month}/1\t{data[year_str]}')
四半期データの月次展開
四半期(3ヶ月ごと)のデータは、pandasの DateOffset を使って各月に展開します。
import pandas as pd
# 四半期データをDataFrameに変換
df = pd.read_csv(data, sep="\t", names=["Date", "Value"], parse_dates=["Date"])
expanded_df = pd.DataFrame()
for i in range(len(df)):
start_date = df.iloc[i]['Date']
for j in range(3):
new_date = start_date + pd.DateOffset(months=j)
new_row = pd.DataFrame({"Date": [new_date], "Value": [df.iloc[i]['Value']]})
expanded_df = pd.concat([expanded_df, new_row], ignore_index=True)
最終的に、1953年から2022年までの約840ヶ月分のマクロ経済指標データが完成しました。
株価シミュレーション:SimulateStock.ipynb
ここが最も面白い部分です。ニュースイベントとマクロ経済指標を組み合わせて、架空の企業の株価をシミュレーションします。
企業ごとの影響度設定
各企業には、マクロ指標への感応度(impact_factors)を個別に設定します。例えば、製造業の企業はGDP成長率に強く反応し、不動産企業は金利に敏感、といった具合です。
def get_impact_settings(ticker):
if ticker == 'NEWTON':
# 大手総合電機メーカー:安定だが成長率は低い
impact_factors = {
'volatility': 1.2,
'macro_factors': {
'Inflation_Rate': 0.0001,
'Interest_Rate': -0.0001,
'GDP_Growth': 0.0002,
'Money_Supply': 0.0001,
'Employment': 0.0002,
'Unemployment_Rate': -0.0002,
'Population_index': 0.0002,
'Birth_rate': 0.0001,
'Salary_production': 0.0002,
'WTI_energy': 0.0003
}
}
elif ticker == 'AERO':
# 新興航空機メーカー:高リスク・高リターン
impact_factors = {
'volatility': 1.5,
'macro_factors': {
'GDP_Growth': 0.0003,
'Interest_Rate': -0.0002,
'Employment': 0.0002,
'Inflation_Rate': -0.0001,
...
}
}
ボラティリティ値が高い企業ほど、同じマクロ指標の変動に対して大きく反応します。NEWTON(安定した大手)のボラティリティが1.2なのに対し、AERO(新興企業)は1.5。この差が、リスクとリターンの違いとしてゲーム内に反映されます。
ニュースインパクトの設計
収集したニュースイベントに対して、各企業への影響度をマッピングします。
news_impacts = {
pd.Timestamp('1953-02-01'): 0.03, # 好材料ニュース
pd.Timestamp('1953-03-01'): -0.05, # 悪材料ニュース
pd.Timestamp('1953-07-01'): 0.04,
pd.Timestamp('1953-08-01'): 0.05,
pd.Timestamp('1954-10-01'): -0.04,
pd.Timestamp('1955-07-01'): -0.03,
pd.Timestamp('1957-10-01'): 0.06,
pd.Timestamp('1958-10-01'): -0.02,
pd.Timestamp('1959-03-01'): 0.05
}
正の値は株価上昇、負の値は下落を意味します。同じニュースでも、業種によって影響の大きさと方向が異なるように設計されています。
セクターごとの特性
SimulateStockのノートブックでは、以下のセクター分類でシミュレーションを行いました。
| セクター | 特性 | GDP成長率への感応度 |
|---|---|---|
| テクノロジー | 革新的。技術ニュースに敏感 | 高(+0.0003) |
| 製造業 | 安定した大手。市場全体に連動 | 中(+0.0002) |
| 不動産 | 金利に敏感。好景気で上昇、不景気で下落 | 中(+0.0002) |
| エネルギー | 原油価格に強く連動 | 低(+0.0001) |
| 金融 | 金利に連動。政策変更の影響大 | 中(+0.0002) |
この分類は、最終的なゲームの銘柄設計(モグモグ・フーズ、ビリビリ・エナジーなど)の土台になっています。
データの正規化:Normaralize.ipynb
最後のノートブックでは、マクロ経済指標を機械学習に投入できる形に正規化します。
import pandas as pd
import numpy as np
from sklearn.preprocessing import StandardScaler
file_path = 'macro_1953_1959.json'
df_macro = pd.read_json(file_path)
# 日付列を除外してスケーリング
features_to_scale = df_macro.drop(columns=['Date'])
scaler = StandardScaler()
scaled_features = scaler.fit_transform(features_to_scale)
StandardScaler(標準化)を使うことで、GDP成長率(数%)と人口(億人単位)のようにスケールが大きく異なる指標を、同じ尺度で比較可能にしています。
ゲームデータへの変換
パイプラインで生成したデータは、最終的にゲームの events.ts に組み込まれます。例えば、1980年から2020年までの各年3件ずつ、計120件以上のニュースイベントが定義されています。
export const NEWS_EVENT_DATA: NewsEventData[] = [
// 1985年 - プラザ合意
{
id: 'news_1985_plaza',
year: 1985,
phase: 'NORMAL',
stockImpact: 0.05,
goldImpact: 0.02,
isHistorical: true,
},
// 1990年 - バブル崩壊
{
id: 'news_1990_bubble_burst',
year: 1990,
phase: 'BAD',
stockImpact: -0.30,
goldImpact: 0.15,
isHistorical: true,
},
// 2008年 - リーマンショック
{
id: 'news_2008_lehman',
year: 2008,
phase: 'BAD',
stockImpact: -0.35,
goldImpact: 0.20,
isHistorical: true,
},
// ...120件以上
];
stockImpact と goldImpact の値は、パイプラインで算出した影響度をゲームバランスに合わせて調整したものです。バブル崩壊(-30%)やリーマンショック(-35%)のような大きなイベントは、実際の歴史に近い変動幅を設定しつつ、子供が理解できるレベルに抑えています。
景気フェーズとの連動
ニュースイベントには必ず phase(景気フェーズ)が紐づいています。ゲーム内では3段階の景気フェーズを使用しています。
| フェーズ | 意味 | チャンスマスでの確率 |
|---|---|---|
| GOOD(好景気) | 株価が上がりやすい | BOOM 70% / CRASH 30% |
| NORMAL(普通) | 五分五分 | BOOM 50% / CRASH 50% |
| BAD(不景気) | 株価が下がりやすい | BOOM 30% / CRASH 70% |
export const CHANCE_PROBABILITIES: Record<EconomyPhase, { boom: number; crash: number }> = {
GOOD: { boom: 0.70, crash: 0.30 },
NORMAL: { boom: 0.50, crash: 0.50 },
BAD: { boom: 0.30, crash: 0.70 },
};
ニュースマスに止まると景気フェーズが更新され、それが後続のチャンスマスの確率に影響する。つまり、ニュースを読んで景気を予測し、投資判断に活かすというゲームの核心部分が、このデータ構造によって支えられています。
子供向け説明文の生成
もう一つ重要なデータ処理があります。ニュースイベントの子供向け説明文の生成です。
「プラザ合意」「量的緩和」「サブプライムローン」 — こうした経済用語を7歳の子供に説明するのは簡単ではありません。
そこで、各ニュースイベントに対して、小学生でも理解できる説明文を別途作成しました。GameEventの summary フィールドがそれに当たります。例えば、リーマンショックの説明は次のようになります。
アメリカのとても大きな銀行がつぶれちゃったんだ。世界中の人がびっくりして、お金を使うのをやめたから、いろんな会社の株が安くなったよ。
専門用語を避け、因果関係をシンプルに表現する。「なぜ株価が下がるのか」を子供なりに理解できることが、このゲームの教育的価値の核心です。
まとめ
この記事で解説したデータパイプラインの全体像を改めて整理します。
- correctNews.ipynb — Wikipediaから1900年代の歴史的ニュースを自動収集
- CreateMacro.ipynb — GDP、金利、人口など10種類のマクロ経済指標を月次データに展開
- SimulateStock.ipynb — ニュースとマクロ指標から、セクターごとの株価変動をシミュレーション
- Normaralize.ipynb — StandardScalerでマクロデータを正規化
このパイプラインで生成されたデータが、ゲームの120件以上のニュースイベント、景気フェーズの遷移、そして株価変動のパラメータとして、TypeScriptの events.ts に組み込まれています。
次回は、いよいよゲーム本体の開発に入ります。フレームワーク非依存の game-core アーキテクチャの設計思想から、Phaser 3によるボード描画、Next.jsとの統合、認証・ランキング・PWA対応まで、技術的な詳細を解説します。