Skip to the content.

← 目次← 前: 29

30. データ可視化

テーマ: 経済時系列の可視化(推移・散布図と回帰直線・分布・ローレンツ曲線)

学習点: matplotlib のオブジェクト指向API, subplots, 軸の設定, 注釈, PNGファイルへの保存, 日本語フォントが無い環境での対処

依存: matplotlib, NumPy / 難易度: 上級

実行方法

uv run 30_visualization.py

スクリプト冒頭の PEP 723 メタデータ(# /// script)により、必要なライブラリは uv が自動的に仮想環境へ導入します。事前の pip install は不要です。

解説

何をするプログラムか

分析結果は、表の数字のままでは意思決定者に伝わりません。本スクリプトはシリーズの締めくくりとして、月次売上 10 年分(トレンド + 季節変動 + ノイズ)の模擬データを生成し、経営・経済分析で最も出番の多い 4 種類のグラフを 1 枚のダッシュボード(2×2 の PNG 画像)にまとめます。(1) 時系列と 12 か月移動平均、(2) 広告費と売上の散布図 + 回帰直線 + 95% 信頼帯、(3) 対数収益率のヒストグラムと正規分布の重ね描き、(4) 所得分布のローレンツ曲線とジニ係数です。それぞれ 13 章(ローレンツ曲線)や 9 章(単回帰)でテキストとして出力した内容の「グラフ版」に相当します。

コードの読みどころ

実行結果の見方

コンソール出力の回帰式は sales = 84.16 + 1.259·ad、R² = 0.1688 です。広告費だけでは売上変動の約 17% しか説明できないことを意味し、決定係数が低い回帰では直線だけでなく信頼帯や散布の広がりを見せることが重要だと分かります。対数収益率の平均 0.00382 はデータ生成時に仕込んだ月率 0.4% の成長とほぼ一致します。ジニ係数 0.4315 はパネル (4) の曲線の弛みに対応します。生成された out_30/econ_dashboard.png(下に掲載)で 4 パネルの仕上がりを確認してください。

ソースコード

# /// script
# requires-python = ">=3.11"
# dependencies = [
#     "matplotlib",
#     "numpy",
# ]
# ///
"""30: データ可視化 -------------------------------------------------------
テーマ: 経済時系列の可視化(推移・散布図と回帰直線・分布・ローレンツ曲線)
学習点: matplotlib のオブジェクト指向API, subplots, 軸の設定, 注釈,
        PNGファイルへの保存, 日本語フォントが無い環境での対処
注意: 画面のない環境で実行するため Agg バックエンドを使用する。
"""
import matplotlib
matplotlib.use("Agg")            # 画面表示なしで画像だけ生成する

import matplotlib.pyplot as plt
import numpy as np
from pathlib import Path

rng = np.random.default_rng(7)
OUT = Path("out_30")
OUT.mkdir(exist_ok=True)


def make_data():
    n = 120                                        # 10年分の月次
    t = np.arange(n)
    trend = 100 * 1.004 ** t                       # 月率0.4%の成長
    season = 8 * np.sin(2 * np.pi * t / 12)        # 季節変動
    noise = rng.normal(0, 4, n)
    sales = trend + season + noise
    ad = 20 + 0.12 * sales + rng.normal(0, 6, n)   # 売上と相関する広告費
    return t, sales, ad


def main() -> None:
    t, sales, ad = make_data()

    fig, axes = plt.subplots(2, 2, figsize=(13, 9))
    fig.suptitle("Economic Data Visualization Examples", fontsize=15,
                 fontweight="bold")

    # --- (1) 時系列と移動平均 ---
    ax = axes[0, 0]
    ma12 = np.convolve(sales, np.ones(12) / 12, mode="valid")
    ax.plot(t, sales, lw=1.0, alpha=0.7, label="Monthly sales")
    ax.plot(np.arange(11, len(sales)), ma12, lw=2.2, color="crimson",
            label="12-month moving average")
    ax.fill_between(t, sales, alpha=0.12)
    ax.set_title("(1) Time series and trend")
    ax.set_xlabel("Month"); ax.set_ylabel("Sales (million JPY)")
    ax.legend(); ax.grid(alpha=0.3)

    # --- (2) 散布図と回帰直線(信頼帯つき) ---
    ax = axes[0, 1]
    b, a = np.polyfit(ad, sales, 1)
    xs = np.linspace(ad.min(), ad.max(), 100)
    resid = sales - (a + b * ad)
    s = resid.std(ddof=2)
    se = s * np.sqrt(1 / len(ad) + (xs - ad.mean()) ** 2 /
                     ((ad - ad.mean()) ** 2).sum())
    r = np.corrcoef(ad, sales)[0, 1]
    ax.scatter(ad, sales, s=22, alpha=0.6, edgecolor="none")
    ax.plot(xs, a + b * xs, color="crimson", lw=2,
            label=f"y = {a:.1f} + {b:.2f}x  (R²={r**2:.3f})")
    ax.fill_between(xs, a + b * xs - 1.96 * se, a + b * xs + 1.96 * se,
                    color="crimson", alpha=0.15, label="95% CI of mean")
    ax.set_title("(2) Scatter plot with OLS fit")
    ax.set_xlabel("Advertising"); ax.set_ylabel("Sales")
    ax.legend(fontsize=8); ax.grid(alpha=0.3)

    # --- (3) ヒストグラムと理論分布 ---
    ax = axes[1, 0]
    logret = np.diff(np.log(sales))
    ax.hist(logret, bins=30, density=True, alpha=0.65,
            edgecolor="white", label="Log returns")
    xs = np.linspace(logret.min(), logret.max(), 200)
    pdf = (np.exp(-0.5 * ((xs - logret.mean()) / logret.std()) ** 2)
           / (logret.std() * np.sqrt(2 * np.pi)))
    ax.plot(xs, pdf, color="darkorange", lw=2, label="Normal density")
    ax.axvline(logret.mean(), color="k", ls="--", lw=1,
               label=f"mean={logret.mean():.4f}")
    ax.set_title("(3) Distribution vs normal benchmark")
    ax.set_xlabel("Log return"); ax.set_ylabel("Density")
    ax.legend(fontsize=8); ax.grid(alpha=0.3)

    # --- (4) ローレンツ曲線 ---
    ax = axes[1, 1]
    income = np.sort(rng.lognormal(6.0, 0.85, 500))
    cum = np.concatenate([[0], np.cumsum(income) / income.sum()])
    p = np.linspace(0, 1, len(cum))
    gini = 1 - 2 * np.trapezoid(cum, p)
    ax.plot([0, 1], [0, 1], "k--", lw=1, label="Perfect equality")
    ax.plot(p, cum, lw=2.2, color="teal", label=f"Lorenz (Gini={gini:.3f})")
    ax.fill_between(p, cum, p, alpha=0.2, color="teal")
    ax.annotate("Area of inequality", xy=(0.62, 0.36), xytext=(0.28, 0.72),
                arrowprops=dict(arrowstyle="->", lw=1.2), fontsize=9)
    ax.set_title("(4) Lorenz curve")
    ax.set_xlabel("Cumulative population share")
    ax.set_ylabel("Cumulative income share")
    ax.set_xlim(0, 1); ax.set_ylim(0, 1)
    ax.legend(fontsize=8); ax.grid(alpha=0.3)

    fig.tight_layout(rect=(0, 0, 1, 0.96))
    png = OUT / "econ_dashboard.png"
    fig.savefig(png, dpi=140)
    plt.close(fig)

    print(f"保存しました: {png} ({png.stat().st_size/1024:.0f} KB)")
    print(f"  回帰式: sales = {a:.2f} + {b:.3f}·ad,  R² = {r**2:.4f}")
    print(f"  対数収益率: 平均 {logret.mean():.5f} / 標準偏差 "
          f"{logret.std(ddof=1):.5f}")
    print(f"  ジニ係数: {gini:.4f}")
    print("\n[日本語表示について]")
    print("  matplotlib の既定フォントは日本語を含まないため、ラベルに日本語を")
    print("  使うと豆腐(□)になる。日本語を使う場合は次のいずれかを行う:")
    print("    pip install japanize-matplotlib   (import するだけで有効)")
    print("    plt.rcParams['font.family'] = 'IPAexGothic' 等のフォント指定")
    print("  本スクリプトは環境非依存のため英語ラベルにしてある。")


if __name__ == "__main__":
    main()

実行結果

保存しました: out_30/econ_dashboard.png (246 KB)
  回帰式: sales = 84.16 + 1.259·ad,  R² = 0.1688
  対数収益率: 平均 0.00382 / 標準偏差 0.04579
  ジニ係数: 0.4315

[日本語表示について]
  matplotlib の既定フォントは日本語を含まないため、ラベルに日本語を
  使うと豆腐(□)になる。日本語を使う場合は次のいずれかを行う:
    pip install japanize-matplotlib   (import するだけで有効)
    plt.rcParams['font.family'] = 'IPAexGothic' 等のフォント指定
  本スクリプトは環境非依存のため英語ラベルにしてある。

生成されたダッシュボード


← 目次← 前: 29