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 章(単回帰)でテキストとして出力した内容の「グラフ版」に相当します。
コードの読みどころ
- 冒頭の
matplotlib.use("Agg")は、画面のないサーバ環境でも画像ファイルだけを生成できるバックエンド指定です。pyplotを import する前に呼ぶ必要があります。 fig, axes = plt.subplots(2, 2, figsize=(13, 9))がオブジェクト指向 API の入口です。plt.plot(...)に頼らず、ax = axes[0, 0]のように各パネルを取り出してax.plot()ax.set_title()を呼ぶ書き方は、複数パネルの図で混乱しないための定石です。- (1) では
np.convolve(sales, np.ones(12) / 12, mode="valid")で 12 か月移動平均を計算し、ax.fill_between()で面を淡く塗っています。 - (2) では
np.polyfit(ad, sales, 1)で回帰係数を求め、回帰直線の平均予測の標準誤差seを式どおりに計算してfill_betweenで 95% 信頼帯を描いています。信頼帯が平均から離れるほど広がる形状に注目してください。 - (4) の
ax.annotate("Area of inequality", xy=..., xytext=..., arrowprops=...)は、図中の注目箇所へ矢印付きの注釈を入れる方法です。ジニ係数はnp.trapezoidの台形積分で求めています。 - 仕上げは
fig.tight_layout(rect=(0, 0, 1, 0.96))でタイトルと重ならないよう余白を調整し、fig.savefig(png, dpi=140)で PNG に保存、plt.close(fig)でメモリを解放します。 - matplotlib の既定フォントは日本語を含まないため、日本語ラベルは豆腐(□)になります。スクリプト末尾に
japanize-matplotlibの導入かplt.rcParams['font.family']の指定という対処法をまとめており、本編はあえて英語ラベルで環境非依存にしています。
実行結果の見方
コンソール出力の回帰式は 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' 等のフォント指定
本スクリプトは環境非依存のため英語ラベルにしてある。
