第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_energyEIA原油価格

年次データの月次展開

年次でしか取得できないデータ(例:人口)は、同じ値を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成長率(数%)と人口(億人単位)のようにスケールが大きく異なる指標を、同じ尺度で比較可能にしています。


ゲーム内のニュースカットシーン — 2011年 東日本大震災

ゲームデータへの変換

パイプラインで生成したデータは、最終的にゲームの 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件以上
];

stockImpactgoldImpact の値は、パイプラインで算出した影響度をゲームバランスに合わせて調整したものです。バブル崩壊(-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 フィールドがそれに当たります。例えば、リーマンショックの説明は次のようになります。

アメリカのとても大きな銀行がつぶれちゃったんだ。世界中の人がびっくりして、お金を使うのをやめたから、いろんな会社の株が安くなったよ。

専門用語を避け、因果関係をシンプルに表現する。「なぜ株価が下がるのか」を子供なりに理解できることが、このゲームの教育的価値の核心です。


まとめ

この記事で解説したデータパイプラインの全体像を改めて整理します。

  1. correctNews.ipynb — Wikipediaから1900年代の歴史的ニュースを自動収集
  2. CreateMacro.ipynb — GDP、金利、人口など10種類のマクロ経済指標を月次データに展開
  3. SimulateStock.ipynb — ニュースとマクロ指標から、セクターごとの株価変動をシミュレーション
  4. Normaralize.ipynb — StandardScalerでマクロデータを正規化

このパイプラインで生成されたデータが、ゲームの120件以上のニュースイベント、景気フェーズの遷移、そして株価変動のパラメータとして、TypeScriptの events.ts に組み込まれています。

次回は、いよいよゲーム本体の開発に入ります。フレームワーク非依存の game-core アーキテクチャの設計思想から、Phaser 3によるボード描画、Next.jsとの統合、認証・ランキング・PWA対応まで、技術的な詳細を解説します。


前回: 第1回「子供に投資をどう教えるか — ゲームという選択肢」

次回: 第3回「Next.js × Phaser 3 で本格ブラウザゲームを作る」

この記事をシェア

関連記事