予測区間(不確実性) を自然に出せる。🎨 直感で掴む 📐 ピンボール損失 🧮 実値で計算 🐍 statsmodels 🐍 GBR (sklearn) 🐍 損失検証 🐍 予測区間 ⚠️ 落とし穴 🌐 関連手法 📚 グループ教材
L1 損失と等価で、 LAD(Least Absolute Deviations)と同じ。身長と体重の散布図を想像してほしい。 OLS は「身長 x cm の人の体重の平均」を予測する直線を引く。 分位点回帰 (τ=0.9) は「身長 x cm の人の体重の上位 10% ライン」を引く。 同じデータでも見える線が違う。 SSDSE-B-2026 の「人口 → 出生数」でも同様で、 平均で見ると傾き 0.0061 だが、 上位 25%(τ=0.75)の県では 0.0063 と若干急、 下位 25%(τ=0.25)では 0.0059 と緩い。 大県ほど「出生数のばらつきが平均からポジティブ側に広い」傾向が読み取れる。
| 手法 | 捉えるもの | 外れ値耐性 | 出力 |
|---|---|---|---|
| OLS (普通の回帰) | 条件付き平均 $E[Y \mid X]$ | 弱い(外れ値 1 個で歪む) | 1 本の点予測 |
| 中央値回帰 (τ=0.5) | 条件付き中央値 $\text{Med}[Y \mid X]$ | 強い (LAD と等価) | 1 本の点予測 (頑健) |
| 分位点回帰 (τ=0.05, 0.95) | 下側 5%・上側 5% 分位点 | 構造的に頑健 | 90% 予測区間 |
| 分位点回帰 (全 τ) | 条件付き分布全体 | 強い | 分布関数の推定 |
分位点回帰の損失関数 $\rho_\tau(u)$ は「ピンボール(pinball)損失」と呼ばれる非対称な絶対値損失:
$$ \rho_\tau(u) = u \cdot (\tau - \mathbb{1}_{u < 0}) = \begin{cases} \tau \cdot u & \text{if } u \ge 0 \\ (\tau - 1) \cdot u & \text{if } u < 0 \end{cases} $$
$$ \hat{\beta}(\tau) = \arg\min_{\beta} \sum_{i=1}^{n} \rho_\tau(y_i - x_i^\top \beta) $$
SSDSE-B-2026 (2023 年, 47 県) で、 説明変数 = 人口 (A1101)、 被説明変数 = 出生数 (A4101) として、 OLS と 3 種類の分位回帰を比較する。
| 手法 | 傾き (slope) | 切片 (intercept) | 解釈 |
|---|---|---|---|
| OLS (平均) | 0.006104 | −676.9 | 人口 100 万増 → 出生 6100 増(平均) |
| τ=0.25 | 0.005885 | −1037.3 | 下位 25% ライン: やや傾き低い |
| τ=0.50 (中央値) | 0.005748 | −287.5 | 中央値: OLS より頑健、 傾き微減 |
| τ=0.75 | 0.006310 | 0.00 | 上位 25%: 傾き急、 大県ほど出生数増 |
🎯 このコードでやること: SSDSE-B-2026 の (人口, 出生数) で τ ∈ {0.25, 0.5, 0.75} の分位回帰を statsmodels.formula.api.quantreg で実行し、 OLS と並べて比較。
📥 入力データ (SSDSE-B-2026 2023 年 抜粋):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | import pandas as pd import statsmodels.formula.api as smf df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) d = df[df['SSDSE-B-2026']==2023][['A1101','A4101']] d.columns = ['pop','birth'] # OLS と分位回帰を 3 つ ols = smf.ols('birth ~ pop', d).fit() print(f'OLS: slope={ols.params["pop"]:.6f}, intercept={ols.params["Intercept"]:.2f}') for tau in [0.25, 0.5, 0.75]: qr = smf.quantreg('birth ~ pop', d).fit(q=tau) print(f'τ={tau}: slope={qr.params["pop"]:.6f}, ' f'intercept={qr.params["Intercept"]:.2f}') |
📤 実行すると次の出力が得られる:
💬 結果の読み方: τ が増えるほど傾きがおおむね急になる(0.0059 → 0.0063)。 これは「人口の大きい県では出生数の 上限が傾向以上に伸びる」ことを意味する(東京・神奈川の集中効果)。 OLS の傾き 0.0061 はちょうど中央値と平均値の間に来ている。
🎯 このコードでやること: GBR の loss='quantile' オプションで τ=0.1, 0.5, 0.9 の非線形分位回帰を実行。 線形より柔軟に「分布の端」を捉えられる。
📥 入力データ: SSDSE-B-2026 (47 県 × 2023), X=人口、 y=出生数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | import pandas as pd import numpy as np from sklearn.ensemble import GradientBoostingRegressor df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) d = df[df['SSDSE-B-2026']==2023] X = d[['A1101']].values y = d['A4101'].values # 3 つの τ で別々にモデルを学習 models = {} for tau in [0.1, 0.5, 0.9]: gbr = GradientBoostingRegressor( loss='quantile', alpha=tau, n_estimators=100, max_depth=3, random_state=42 ) gbr.fit(X, y) models[tau] = gbr # 東京 (人口 14086000) の予測区間 x_tokyo = np.array([[14086000]]) for tau, m in models.items(): print(f'東京 (人口 14086000) τ={tau}: 予測出生数 = {m.predict(x_tokyo)[0]:,.0f}') # 平均的県 (人口 200 万) の予測区間 x_avg = np.array([[2000000]]) for tau, m in models.items(): print(f'仮想県 (人口 2000000) τ={tau}: 予測出生数 = {m.predict(x_avg)[0]:,.0f}') |
📤 実行すると次の出力が得られる:
💬 結果の読み方: 東京は人口が突出しており、 訓練データ内で「東京サイズの県は 1 件のみ」のため、 τ=0.5 と τ=0.9 がほぼ同じ値(86,239 と 86,347)になる(過学習)。 仮想県 (人口 200 万) では「下側 10K 〜 上側 12.5K」の予測区間 (約 2300 幅) が得られ、 OLS の単一点予測より「不確実性」を表現できている。 なお τ=0.1 は木モデルの外挿制約で東京・仮想県とも同じ低値 (10,212) を返す点に注意。
🎯 このコードでやること: 「定数予測 c」を様々な値で試し、 ピンボール損失を計算する。 τ=0.5 では中央値、 τ=0.25 では下位 25% 分位点、 τ=0.75 では上位 25% 分位点で損失が最小になることを実値で確認。
📥 入力データ: SSDSE-B-2026 47 県の出生数 A4101 (平均 15,474、 中央値 9,524、 25%=5,472、 75%=14,390)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | import pandas as pd import numpy as np df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) y = df[df['SSDSE-B-2026']==2023]['A4101'].values # 出生数 def pinball(y, c, tau): u = y - c return np.where(u >= 0, tau * u, (tau - 1) * u).mean() # 候補値: 25%, 中央値, 75%, 平均 candidates = { 'q25 (5472)': 5472, 'median (9524)': 9524, 'q75 (14390)': 14390, 'mean (15474)': 15474, } print('--- τ=0.25 (q25 が最小であるはず) ---') for name, c in candidates.items(): print(f' c={name}: ρ_0.25 = {pinball(y, c, 0.25):.2f}') print('--- τ=0.50 (中央値が最小であるはず) ---') for name, c in candidates.items(): print(f' c={name}: ρ_0.50 = {pinball(y, c, 0.50):.2f}') print('--- τ=0.75 (q75 が最小であるはず) ---') for name, c in candidates.items(): print(f' c={name}: ρ_0.75 = {pinball(y, c, 0.75):.2f}') |
📤 実行すると次の出力が得られる:
💬 結果の読み方: τ=0.25 のピンボール損失は q25=5472 で最小 (2773)、 τ=0.5 では median=9524 で最小 (4857)、 τ=0.75 では q75=14390 で最小 (5913)。 これが「ピンボール損失最小化 = 分位点推定」の証明的確認。 SSDSE は右裾が長いので、 平均 15474 は中央値 9524 から大きく乖離している。
🎯 このコードでやること: τ=0.05 と τ=0.95 の分位回帰を訓練し、 「90% 予測区間 (PI)」を構築。 OLS の正規分布仮定に依存しない、 ノンパラメトリックな PI が得られる。
📥 入力データ: SSDSE-B-2026 47 県、 X=人口 A1101、 y=出生数 A4101。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | import pandas as pd import numpy as np from sklearn.ensemble import GradientBoostingRegressor df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) d = df[df['SSDSE-B-2026']==2023] X = d[['A1101']].values y = d['A4101'].values def fit_qr(tau): m = GradientBoostingRegressor( loss='quantile', alpha=tau, n_estimators=200, max_depth=2, random_state=0 ) m.fit(X, y) return m m_lo = fit_qr(0.05) m_md = fit_qr(0.50) m_hi = fit_qr(0.95) # 仮想県 (人口 100 万, 300 万, 1000 万) で予測区間 for pop in [1_000_000, 3_000_000, 10_000_000]: xq = np.array([[pop]]) lo, md, hi = m_lo.predict(xq)[0], m_md.predict(xq)[0], m_hi.predict(xq)[0] print(f'人口 {pop:>10,}: 90% PI = [{lo:,.0f}, {hi:,.0f}], 中央値={md:,.0f}') # カバレッジ確認 (訓練データの何割が 90% PI に入るか) lo_pred = m_lo.predict(X) hi_pred = m_hi.predict(X) covered = ((y >= lo_pred) & (y <= hi_pred)).mean() print(f'\n訓練データのカバレッジ: {covered*100:.1f}% (目標 90%)') |
📤 実行すると次の出力が得られる:
💬 結果の読み方: 訓練データの 83.0% が 90% 予測区間に入る → 目標をやや下回るが N=47 の小サンプルでは妥当な範囲。 人口が増えるほど PI の絶対幅は広がる(約 1,800 → 6,800 → 62,000)。 なお τ=0.05 の下限は木モデルの外挿制約で人口 300 万以上では 9,950 に張り付く点に注意。 OLS で同じことをやろうとすると「残差の正規性」を仮定する必要があり、 出生数のような右裾分布では PI が不正確になりがち。 分位点回帰は仮定なしで PI を直接学ぶ。
cqr ライブラリや QuantileForest を使う。| 領域 | 手法 | 関係 |
|---|---|---|
| 並列概念 | OLS / ロバスト回帰 | 条件付き分布の別側面 |
| 特殊例 | 中央値回帰 (τ=0.5) = LAD | L1 損失最小化と等価 |
| 機械学習版 | Quantile Random Forest / LightGBM-quantile / NGBoost | 非線形・高次元対応 |
| 応用 | 予測区間 / VaR / Conformal Prediction | 不確実性定量化の標準 |
前提: 分位点 / OLS / 線形計画 / 損失関数
並列: ロバスト回帰 / GAM / 中央値回帰
発展: 分位点 RF / LightGBM / NGBoost / Conformal Prediction
分位点回帰の最適化は線形計画(LP)として定式化できる。 補助変数 $u_i^+, u_i^- \ge 0$ を導入:
$$ \min_{\beta, u^+, u^-} \sum_{i=1}^{n} \left[\tau u_i^+ + (1-\tau) u_i^-\right] $$
$$ \text{s.t.}\quad y_i - x_i^\top \beta = u_i^+ - u_i^-,\ \ u_i^+, u_i^- \ge 0 $$
シンプレックス法や内点法で解ける。 statsmodels の quantreg は内部で内点法(IRLS の派生)を呼んでいる。 n=10,000、 p=50 程度なら 1 秒以内、 n=100,000 でも数秒で解ける。
🎯 このコードでやること: 上記の LP 定式化を scipy.optimize.linprog で自前で解き、 statsmodels の結果と一致するか確認。
📥 入力データ: SSDSE-B-2026 47 県、 X=人口、 y=出生数 (上記と同じ)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | import pandas as pd import numpy as np from scipy.optimize import linprog df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) d = df[df['SSDSE-B-2026']==2023] y = d['A4101'].values.astype(float) X = np.column_stack([np.ones(len(d)), d['A1101'].values.astype(float)/1e4]) # [1, pop万人](well-conditioned) n, p = X.shape tau = 0.5 # 中央値回帰 # 変数: [beta (p個), u+ (n個), u- (n個)] # 目的: minimize sum(tau * u+ + (1-tau) * u-) c = np.concatenate([np.zeros(p), tau * np.ones(n), (1-tau) * np.ones(n)]) # 制約: X @ beta + u+ - u- = y → [X, I, -I] @ [beta; u+; u-] = y A_eq = np.hstack([X, np.eye(n), -np.eye(n)]) b_eq = y # beta は無制約、 u+, u- >= 0 bounds = [(None, None)] * p + [(0, None)] * (2 * n) res = linprog(c, A_eq=A_eq, b_eq=b_eq, bounds=bounds, method='highs') beta = res.x[:p] print(f'LP解 (τ=0.5): intercept={beta[0]:.2f}, slope={beta[1]:.6f}') print(f'statsmodels比: intercept=-287.53, slope=57.484981 (IRLS 未収束のため LP 解とずれる)') |
📤 実行すると次の出力が得られる:
💬 結果の読み方: 自前 LP(HiGHS)は厳密解を返す。 一方 statsmodels の QuantReg は IRLS 反復が上限(max_iter=1000)に達して未収束のまま止まるため、 この n=47 データでは両者が僅かにずれる(ピンボール損失合計: LP 26,872.5 < statsmodels 28,551.0 で LP 解の方が厳密に良い)。 分位点回帰の「本体」は線形計画 1 個に集約されることが確認できる。 OLS が正規方程式 $(X^\top X)^{-1} X^\top y$ の閉形式解を持つのに対し、 分位点回帰は LP の反復解法が必要で計算量は増えるが、 現代の HIGHS / Mosek なら大規模問題でも実用速度。
分位点回帰の条件付き分位点関数 $Q_Y(\tau \mid X = x) = x^\top \beta(\tau)$ の意味を、 もう一段噛み砕く:
| 業界 | τ の選び方 | 用途 |
|---|---|---|
| 金融 (VaR) | τ=0.01, 0.05 | 最大損失(テイルリスク)の推定 |
| 医療 (BMI 成長曲線) | τ=0.03, 0.5, 0.97 | 小児の成長カーブ標準 |
| 公衆衛生 (出生体重) | τ=0.1, 0.5, 0.9 | 低出生体重リスク予測 |
| 電力 (負荷予測) | τ=0.95 | ピーク需要への備え |
| 物流 (配送時間) | τ=0.9 | 「90% の確率で X 分以内」保証 |
| 教育 (試験成績) | τ=0.1, 0.5, 0.9 | 下位層・中位層・上位層の傾向 |
| 不動産 (家賃推定) | τ=0.25, 0.75 | 「相場上下幅」を示す |
scipy.linprog での解法を確認した両者とも「予測区間」を作る手法だが、 アプローチが全く異なる:
| 観点 | 分位点回帰 | Conformal Prediction |
|---|---|---|
| 対象 | 条件付き分位点 | 周辺カバレッジ |
| 仮定 | 線形・GBR 等のモデル | 交換可能性のみ |
| 保証 | 無限サンプル極限 | 有限サンプルで厳密 |
| 適応性 | X に応じて区間幅が変わる | 基本版は固定幅 |
| ハイブリッド | CQR (Conformalized Quantile Regression) | 両者の長所を統合 |
実務では「分位点回帰で適応的な区間を作り、 Conformal で有限サンプル保証を付与する」CQR が主流になりつつある (Romano et al., 2019)。
SSDSE-B-2026 を 2023〜2012 年(12 年分)に拡張し、 各年で 47 県の (人口, 出生数) に τ=0.1, 0.25, 0.5, 0.75, 0.9 の分位回帰を当てる。 傾き $\beta_1(\tau)$ の年次推移を表で示す。
| 年 | τ=0.1 | τ=0.25 | τ=0.5 | τ=0.75 | τ=0.9 | OLS |
|---|---|---|---|---|---|---|
| 2023 | 0.00569 | 0.00589 | 0.00575 | 0.00631 | 0.00647 | 0.00610 |
| 2022 | 0.00574 | 0.00595 | 0.00616 | 0.00653 | 0.00682 | 0.00639 |
| 2021 | 0.00579 | 0.00623 | 0.00625 | 0.00681 | 0.00717 | 0.00668 |
| 2020 | 0.00619 | 0.00644 | 0.00665 | 0.00709 | 0.00737 | 0.00694 |
| 2015 | 0.00756 | 0.00772 | 0.00799 | 0.00838 | 0.00877 | 0.00821 |
| 2012 | 0.00800 | 0.00788 | 0.00815 | 0.00832 | 0.00900 | 0.00826 |
観察 1: 年が進むにつれて傾き全体が低下 (0.0083 → 0.0061) → 少子化進行で「人口あたり出生数」が全国的に下がっている。
観察 2: 各年内で τ=0.9 / τ=0.1 比 = 約 1.13 倍 → 高分位(高人口県)の方が傾きが急という構造は不変。
観察 3: OLS の傾きはおおむね τ=0.5〜0.75 の間 → 出生数分布の右裾の影響を平均が引っ張られている。
複数の説明変数を含めることも可能。 SSDSE-B-2026 で X = {人口 A1101, 年平均気温 B4101, 婚姻件数 A9101} を使い y = 出生数を推定する例:
$$ Q_Y(\tau \mid X) = \beta_0(\tau) + \beta_1(\tau) \cdot \text{pop} + \beta_2(\tau) \cdot \text{temp} + \beta_3(\tau) \cdot \text{marr} $$
係数 $\beta_j(\tau)$ は τ ごとに変わるので、 「婚姻件数が多い県では中央値 (τ=0.5) の出生数は増えるが、 上位 (τ=0.9) では影響が変わる」といった分布依存の関係を抽出できる。 OLS では平均 1 本しか得られないので、 こうした構造は見落とされる。
🎯 このコードでやること: SSDSE-B-2026 で τ=0.1, 0.5, 0.9 を別々に推定し、 「クロスしている点」を検出。 単調性を保つ後処理(sorting)を適用。
📥 入力データ: SSDSE-B-2026 47 県、 X=人口、 y=出生数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | import pandas as pd import numpy as np import statsmodels.formula.api as smf df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) d = df[df['SSDSE-B-2026']==2023][['A1101','A4101']].copy() d.columns = ['pop','birth'] # 3 つの τ で線形分位回帰 preds = {} for tau in [0.1, 0.5, 0.9]: qr = smf.quantreg('birth ~ pop', d).fit(q=tau) preds[tau] = qr.predict(d) pred_df = pd.DataFrame(preds) print('--- 推定結果 (最初の 5 行) ---') print(pred_df.head()) # クロス検出: τ=0.1 > τ=0.5 や τ=0.5 > τ=0.9 となる行 cross = ((pred_df[0.1] > pred_df[0.5]) | (pred_df[0.5] > pred_df[0.9])).sum() print(f'\nクロスしている件数: {cross} / {len(d)}') # 修正: 各行で値をソート fixed = pred_df.apply(lambda row: pd.Series(sorted(row.values), index=[0.1, 0.5, 0.9]), axis=1) print('\n--- ソート後 (クロス解消) ---') print(fixed.head()) |
📤 実行すると次の出力が得られる:
💬 結果の読み方: SSDSE のような直線関係が強いデータではクロスはほぼ起きない(0 件)。 一方、 非線形なデータ(医療コスト等)ではクロスが頻発し、 sorting 修正が必須。 より洗練された手法は Chernozhukov et al. (2010) の「rearrangement」で、 連続関数として単調化する。
Koenker (2005) によれば、 分位点回帰推定量 $\hat{\beta}(\tau)$ は次の漸近分布を持つ:
$$ \sqrt{n}(\hat{\beta}(\tau) - \beta(\tau)) \xrightarrow{d} N\left(0,\ \tau(1-\tau) \cdot D^{-1} \cdot J \cdot D^{-1}\right) $$
ここで $x_i$ は説明変数ベクトル(計画行列 $X$ の第 $i$ 行)で、 $D = E\left[f_{Y \mid X}\bigl(Q_\tau(Y \mid X) \mid X\bigr) \, x_i x_i^\top\right]$、 $J = E[x_i x_i^\top]$。 $D$ の推定には条件付き密度 $f_{Y \mid X}$ が必要で、 これがブートストラップが好まれる理由でもある。
🎯 このコードでやること: τ=0.5 の傾き係数の 95% 信頼区間を、 1000 回のブートストラップで推定。 statsmodels が返すデフォルトの「IID 漸近 SE」と比較。
📥 入力データ: SSDSE-B-2026 47 県、 (人口, 出生数)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | import pandas as pd import numpy as np import statsmodels.formula.api as smf df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) d = df[df['SSDSE-B-2026']==2023][['A1101','A4101']].copy() d.columns = ['pop','birth'] # ブートストラップ 1000 回 B = 1000 slopes = [] for b in range(B): sample = d.sample(n=len(d), replace=True, random_state=b) qr = smf.quantreg('birth ~ pop', sample).fit(q=0.5) slopes.append(qr.params['pop']) slopes = np.array(slopes) # 95% 信頼区間 ci_lo, ci_hi = np.percentile(slopes, [2.5, 97.5]) print(f'ブートストラップ平均: {slopes.mean():.6f}') print(f'ブートストラップ SE: {slopes.std():.6f}') print(f'95% CI (bootstrap): [{ci_lo:.6f}, {ci_hi:.6f}]') # statsmodels の漸近 SE と比較 qr_full = smf.quantreg('birth ~ pop', d).fit(q=0.5) print(f'\nstatsmodels: slope={qr_full.params["pop"]:.6f}, ' f'SE={qr_full.bse["pop"]:.6f}') print(f'statsmodels 95% CI: [{qr_full.conf_int().loc["pop", 0]:.6f}, ' f'{qr_full.conf_int().loc["pop", 1]:.6f}]') |
📤 実行すると次の出力が得られる:
💬 結果の読み方: ブートストラップ CI [0.00547, 0.00628] は statsmodels の漸近 CI [0.00558, 0.00592] より明らかに広い。 これは「右裾の長い分布」「N=47 の小サンプル」で漸近近似が楽観的になっているため。 一般に分位点回帰では ブートストラップ CI を信用すべき。
| 損失 | 対応する推定量 | 対応する分布の点 |
|---|---|---|
| $L_2$ (二乗) | OLS | 平均 |
| $L_1$ (絶対値) | LAD = 中央値回帰 | 中央値 |
| Huber | Huber 回帰 | 頑健な平均 |
| ピンボール $\rho_\tau$ | 分位点回帰 | τ 分位点 |
| $\epsilon$-insensitive | SVR | マージン中央 |
| CRPS | 確率予測 | 分布全体 |
objective='quantile' を実装、 GBDT 主流に
🎯 このコードでやること: LightGBM の objective='quantile' で SSDSE-B-2026 の多変量分位回帰。 説明変数 3 つ(人口・気温・婚姻件数)で出生数の τ=0.1, 0.5, 0.9 を推定。
📥 入力データ: SSDSE-B-2026 47 県、 X=[A1101, B4101, A9101]、 y=A4101。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | import pandas as pd import numpy as np import lightgbm as lgb df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) d = df[df['SSDSE-B-2026']==2023].dropna(subset=['A1101','B4101','A9101','A4101']) X = d[['A1101', 'B4101', 'A9101']].values y = d['A4101'].values # 3 つの τ で別々に学習 preds = {} for tau in [0.1, 0.5, 0.9]: model = lgb.LGBMRegressor( objective='quantile', alpha=tau, n_estimators=100, max_depth=3, learning_rate=0.05, random_state=42, verbose=-1 ) model.fit(X, y) preds[tau] = model.predict(X) # 訓練データ上のカバレッジ (90% PI = τ=0.1〜0.9) covered = ((y >= preds[0.1]) & (y <= preds[0.9])).mean() print(f'90% PI カバレッジ (訓練): {covered*100:.1f}%') # 仮想入力(人口 500 万、 気温 15度、 婚姻件数 2 万件)の予測区間 x_new = np.array([[5_000_000, 15.0, 20000]]) for tau in [0.1, 0.5, 0.9]: model = lgb.LGBMRegressor( objective='quantile', alpha=tau, n_estimators=100, max_depth=3, learning_rate=0.05, random_state=42, verbose=-1 ) model.fit(X, y) print(f'τ={tau}: 予測 = {model.predict(x_new)[0]:,.0f}') |
📤 実行すると次の出力が得られる:
💬 結果の読み方: LightGBM-quantile は非線形・多変量に対応し、 訓練データではカバレッジ 80.9% と名目 80%(τ=0.1〜0.9)にほぼ一致。 実務では検証データでチューニング (alpha, n_estimators) が必要。 仮想入力の PI 幅 [10K, 52K] は OLS では得られない不確実性表現で、 「人口だけ」より精度が上がる。
Expectile(期待分位点)は 1987 年に Newey & Powell が提案した別の非対称損失。 ピンボール損失の代わりに「非対称二乗損失」を使う:
$$ \rho^E_\tau(u) = u^2 \cdot |\tau - \mathbb{1}_{u<0}| = \begin{cases} \tau u^2 & \text{if } u \ge 0 \\ (1-\tau) u^2 & \text{if } u < 0 \end{cases} $$
| 観点 | 分位点 (Quantile) | 期待分位点 (Expectile) |
|---|---|---|
| 損失 | ピンボール (L1) | 非対称 L2 |
| 頑健性 | 高い | 低い (L2 由来) |
| 解 | LP 必要 | IRLS で解析的 |
| 直感的解釈 | τ% 以下に何点 | 非対称 MSE 最小化点 |
| 金融応用 | VaR | Expected Shortfall (ES) |
Basel III では 2017 年から VaR から ES への移行が進んでおり、 Expectile 回帰の重要性が増している。
分位点回帰の予測精度は「平均二乗誤差 (MSE)」では測れない(OLS 用)。 適切な指標は:
| パラメータ | 推奨範囲 | 効果 |
|---|---|---|
| τ (分位点) | 0.01〜0.99 | 業務目的で決定(VaR=0.01, SLA=0.95 等) |
| n_estimators (GBR) | 100〜500 | 多すぎると過学習、 early stopping 推奨 |
| max_depth (GBR) | 2〜5 | 深いほど非線形対応、 過学習リスク |
| learning_rate | 0.01〜0.1 | 小さいほど安定、 n_estimators 増要 |
| min_samples_leaf | 5〜20 | 極端 τ では大きめに(裾の安定化) |
| bootstrap 回数 | 500〜2000 | CI 推定用、 n 小さい時は多めに |
ピンボール損失の期待値 $E[\rho_\tau(Y - c)]$ を $c$ で微分してゼロと置くと:
$$ \frac{\partial}{\partial c} E[\rho_\tau(Y - c)] = -\tau \cdot P(Y \ge c) + (1-\tau) \cdot P(Y < c) = 0 $$
$$ \implies P(Y < c) = \tau \implies c = Q_\tau(Y) $$
つまり「ピンボール損失の期待値を最小化する $c$ は、 分布 $Y$ の τ 分位点に他ならない」が証明される。 これが分位点回帰の数学的核心。 同じロジックで:
「損失関数を決めれば、 分布のどの点を予測するかが決まる」。 これは予測モデリングの最も深い原理の 1 つ。
SSDSE-B-2026 47 県の出生数を「定数 c で予測する」場合、 c の値を変えながら異なる損失を比較:
| c | L2 (MSE) | L1 (MAE) | ρ_0.25 | ρ_0.5 | ρ_0.75 | ρ_0.9 |
|---|---|---|---|---|---|---|
| 3,000 | 444M | 12,474 | 3,118 | 6,237 | 9,355 | 11,226 |
| 5,472 (q25) | 388M | 10,546 | 2,773 ★ | 5,273 | 7,774 | 9,274 |
| 9,524 (med) | 323M | 9,714 ★ | 3,369 | 4,857 ★ | 6,344 | 7,237 |
| 14,390 (q75) | 289M | 11,284 | 5,371 | 5,642 | 5,913 ★ | 6,076 |
| 15,474 (mean) | 288M ★ | 11,839 | 5,920 | 5,920 | 5,920 | 5,920 |
| 40,000 (≈q90) | 890M | 28,192 | 20,227 | 14,096 | 7,964 | 4,285 ★ |
★ は各列で最小値。 L2 最小 = 平均、 L1 最小 = 中央値、 ρ_0.25 最小 = q25、 ρ_0.75 最小 = q75、 ρ_0.9 最小 = q90 付近 (c=40,000。 実測 q90=38,238)。 「損失と推定対象の対応」が一目で分かる。 SSDSE は右裾が長いので、 平均 (15K) と中央値 (9.5K) が大きく乖離している点に注意。
🎯 このコードでやること: τ ∈ {0.1, 0.2, ..., 0.9} の 9 つの分位回帰を学習し、 検証データ上で「予測されたカバレッジ」vs「目標カバレッジ」をテーブル化。 ideally 一致するはず。
📥 入力データ: SSDSE-B-2026 全 12 年 × 47 県 = 564 件、 X=人口、 y=出生数。 ホールドアウト 80/20。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | import pandas as pd import numpy as np import statsmodels.formula.api as smf from sklearn.model_selection import train_test_split df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) d = df[['A1101','A4101']].dropna() # 全 12 年分をプールしテスト標本数を確保 d.columns = ['pop','birth'] train, test = train_test_split(d, test_size=0.2, random_state=42) results = [] for tau in np.arange(0.1, 1.0, 0.1): qr = smf.quantreg('birth ~ pop', train).fit(q=tau) pred = qr.predict(test) coverage = (test['birth'] <= pred).mean() results.append({'target_τ': round(tau, 1), 'actual_coverage': round(coverage, 3), 'gap': round(coverage - tau, 3)}) print(pd.DataFrame(results).to_string(index=False)) |
📤 実行すると次の出力が得られる:
💬 結果の読み方: 目標 τ と実測カバレッジが多くの点で ±0.02 前後(最大でも gap +0.087 @τ=0.4)に収まり、 分位回帰は おおむねキャリブレーション済み。 OLS + 正規分布仮定の PI は右裾の長いデータでカバレッジが落ちることがあるが、 分位回帰は分布仮定なしで近い水準を保つ。 これが「OLS の代替」として分位回帰が推される最大の理由。
| 問い | Yes なら | No なら |
|---|---|---|
| ①「平均」を知りたいか? | OLS | ②へ |
| ② 外れ値が多いか? | 中央値回帰 (τ=0.5) | ③へ |
| ③ 予測区間が必要か? | QR (τ=0.05, 0.95) | ④へ |
| ④ 分布全体の構造を知りたいか? | 複数 τ で QR | ⑤へ |
| ⑤ 極端事象(VaR、 ピーク)が対象か? | QR (τ=0.01 or 0.99) | OLS で OK |
高次元(説明変数が多い)QR では正則化が必要。 Belloni & Chernozhukov (2011) の L1-penalized QR:
$$ \hat{\beta}(\tau) = \arg\min_{\beta} \sum_{i=1}^{n} \rho_\tau(y_i - x_i^\top \beta) + \lambda \|\beta\|_1 $$
Lasso と同じく L1 罰則で sparse な係数を得る。 SCAD、 adaptive lasso も適用可能。 Python では statsmodels 単体では未サポートだが、 sklearn-quantile や pyqreg で対応。
OLS は「的の中心 (黒丸) を狙う矢」と同じ。 ピンボール損失 (τ=0.5) は「的の中央線 (黒丸を通る左右対称線) を狙う」。 τ=0.9 は「的の上から 1 割の位置 (高得点ゾーン) を狙う」。 矢の散らばり方が同じでも、 狙う点が違えば「外れ方の罰則」が違う。 これがピンボール損失の非対称性の本質。
SSDSE で言えば、 「出生数の平均」は的の中央 (15K) だが、 中央値 (9.5K) は的の左寄り、 q75 (14K) は的の右寄り、 q90 (38K) は的の外周。 同じ 47 県データから 5 つの違う「狙い」を導ける。
分位点回帰の真価は、 OLS が描く 1 本の回帰線では捉えられない「条件付き分布の形状変化」を可視化できる点にあります。 SSDSE-B-2026 の 人口総数 を説明変数、 出生数 を目的変数として、 τ ∈ {0.10, 0.25, 0.50, 0.75, 0.90} の 5 本の回帰直線を引くと、 大都市圏 (人口総数が右側) で 傾きが拡大することが見えます。 つまり「人口あたり出生数」のばらつきは大都市ほど大きいという事実が、 1 本の OLS では完全に潰れて見えなくなります。
このコードでやること: SSDSE-B-2026 の data/raw/SSDSE-B-2026.csv から人口と出生数を抽出し、 statsmodels.regression.quantile_regression.QuantReg で 5 本の τ-回帰直線を一気に推定する。 同じ条件で OLS も並走させ、 傾き係数の差を比較する。
📥 入力データ (SSDSE-B-2026 の人口と出生数、 47 行):
1 2 3 4 5 6 7 8 9 | 🐍 quantreg_5taus.py<button onclick="copyTryitCode(this)">📋 コピー</button></div><div class="pygblock">import pandas as pd import statsmodels.formula.api as smf df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) df = df[df['SSDSE-B-2026'] == 2023].rename(columns={'A1101': 'pop', 'A4101': 'births'}) ols = smf.ols('births ~ pop', data=df).fit() print(f'OLS slope = {ols.params[1]:.5f}') for q in [0.10, 0.25, 0.50, 0.75, 0.90]: qr = smf.quantreg('births ~ pop', data=df).fit(q=q) print(f'tau={q:.2f} slope = {qr.params[1]:.5f}')</div> |
📤 実行すると次の出力が得られる:
💬 τ が大きくなるほど傾きがおおむね増加(0.005695 → 0.006473、 約 1.14 倍)しています。 これは「人口の増加に伴い、 上位 10% の県では出生数の伸びが下位 10% の県よりやや急峻」であることを意味します。 OLS は中央付近 (τ=0.50 付近) の値しか返さず、 この 分布の歪みは永久に見えません。 政策決定では「τ=0.10 の県 (下位群)」をターゲットにすべきか「τ=0.90 の県 (上位群)」をターゲットにすべきかで対策が分かれるため、 分位点回帰が必須となります。
🍰 まずはやさしく
データの端っこまで予測する手法です。
平均だけでは見えないばらつきを知るために使います。
テストの点数が高い人と低い人で傾向が違うか調べます。
この章では分位点回帰の結論を短くまとめます。
statsmodels.regression.quantile_regression.QuantReg または LightGBM の objective='quantile' を使う。🍰 まずはやさしく
分析のレポートでよく使われる手法です。
グループごとの動きの違いを比べるために使います。
人口が多い県と少ない県で宿泊客の増え方を比べます。
ここでは実際の分析でどう使われるかを見ます。
計量経済学や予測モデリングの論文で、 こんな表記を見たことがあるはずです:
この 分位点回帰 (Quantile Regression) は、 「人口が増えると平均的に延べ宿泊者数はどう動くか」ではなく、 「下位 10% の県(人口あたり宿泊者数が小さい県)」 と 「上位 10% の県(人口あたり宿泊者数が大きい県)」 で動き方がどう違うか を直接比較できる手法です。 1978 年に Koenker & Bassett が定式化して以来、 経済学・公衆衛生・気象予測・金融リスク管理など 「平均だけでは見えない構造」 を扱う分野で広く使われています。
🍰 まずはやさしく
データの「幅」を捉える虫眼鏡のようなものです。
最悪の場合や最高の場合を予測するために使います。
駅までの時間が一番かかったときは何分か考えます。
ここでは直感的に仕組みを理解するための例を読みます。
あなたは「家から駅まで何分かかるか」を予測したいとします。 普通の回帰なら「平均 12 分」と返してきます。 でも、 本当に知りたいのは:
これらはすべて「同じデータから抽出できる別々の予測値」です。 分位点回帰は 3 本まとめて出してくれる 強力なツール。
「所得が高いほど消費のばらつきも大きい」「人口の多い県ほど延べ宿泊者数のばらつきも大きい」 のような 不等分散 データでは、 通常の OLS が引いた 1 本の直線は 「真ん中だけ」 しか見せてくれません。
| 項目 | 通常の回帰 (OLS) | 分位点回帰 |
|---|---|---|
| 予測対象 | 条件付き平均 $E[Y \mid X]$ | 条件付き分位点 $Q_\tau(Y \mid X)$ |
| 損失関数 | 二乗誤差(外れ値に弱い) | pinball loss(外れ値に強い) |
| 仮定 | 誤差の正規性・等分散性 | 誤差分布の仮定不要 |
| 出力 | 1 本の直線 | τ ごとに 1 本(複数引ける) |
| 解釈 | 「平均的にこう」 | 「上位/中位/下位ではこう」 |
| 不等分散 | 係数は変わらない | 係数が τ により変化(情報量大) |
分位点回帰の中核を 3 枚の図で再整理する。 平均回帰との違い、 ピンボール損失の非対称性、 τ ごとの回帰直線の束という 3 つの視点を並べる。
最小二乗回帰は 条件付き平均 を引く線、 分位点回帰は 条件付き τ 分位点 を引く線である。 外れ値や非対称分布があると、 両者は系統的にずれる。
💬 読み方:赤の OLS 直線は上方の外れ値(赤丸)に引っ張られて中央値直線(青破線, τ=0.5)から離れる。 オレンジ(τ=0.9)と緑(τ=0.1)の 2 本は 予測区間の上下端 を直接与える。
分位点回帰は ρ_τ(u) = u·(τ − 𝟙(u<0)) という非対称損失(ピンボール損失)を最小化する。 τ=0.5 のときは対称の絶対値損失、 τ≠0.5 のときは過小予測と過大予測の罰が異なる。
💬 読み方:青の τ=0.5 は左右対称の V 字(絶対値)。 オレンジ τ=0.9 は 右側(過小予測)の傾きが急 で、 つまり「予測 ŷ が真値 y より小さい状態」を強く罰する → 上方に寄った推定線になる。 緑 τ=0.1 はその逆。
τ を複数(例: 0.1, 0.5, 0.9)で並行推定すると、 条件付き予測区間が x の関数として自然に得られる。 平均回帰+等分散仮定の信頼区間と違い、 ヘテロスケダスティック(x で分散が変わる)な状況にも対応できる。
💬 読み方:水色の帯は τ=0.1〜0.9 の 80% 予測区間。 x が大きくなるにつれ帯が広がる場合、 条件付き分散の不均一性 を分位点回帰がそのまま捉えていることが分かる。 OLS の信頼区間(等幅)では再現できない情報である。
以上、 分位点回帰の本質(中央値推定・ピンボール非対称損失・予測区間)を 3 枚の図で整理した。 詳細は本文の各セクションを参照。
下の散布図は 教材用の擬似データ(x が大きいほど y のばらつきが増える 不均一分散 データ)。 スライダーで分位点 τ を動かすと、 その分位点を通る回帰直線が リアルタイム に引き直される。 散布図を左右にドラッグ(スマホは左右スワイプ)しても τ が変わる。 計算は「最適な分位点回帰直線は少なくとも 2 点を通る」という性質を使い、 全点対を厳密に探索して大域最適解を求めている(近似ではない)。
🍰 まずはやさしく
計算のルールを決めた数式のことです。
正確に予測値を出すための基準を作るために使います。
スマホの利用時間の上位と下位を数式で分けます。
ここでは分位点回帰の数学的な定義について読みます。
SSDSE-B 2026 の 2023 年データを用い、 「延べ宿泊者数 $y$ ~ 人口総数 $x$」 の分位点回帰を τ = 0.10, 0.50, 0.90 の 3 本で推定します。
普通の回帰なら 1 本の直線で「人口が 1 万人増えると延べ宿泊者数は何万人泊増えるか(平均)」を見ます。 しかし都道府県では 大都市・観光地と地方で 1 人あたり宿泊者数が大きく違う ので、 「平均だけ」では情報を失います。
説明変数 $x$ = 人口総数(万人)、 被説明変数 $y$ = 延べ宿泊者数(万人泊)。 5 県を抜粋:
| 都道府県 | $x$(万人) | $y$(万人泊) | $y/x$(1人あたり 人泊) |
|---|---|---|---|
| 東京 | 1409 | 8027 | 5.70 |
| 大阪 | 876 | 4401 | 5.02 |
| 愛知 | 748 | 1721 | 2.30 |
| 北海道 | 509 | 3278 | 6.44 |
| 沖縄 | 147 | 2004 | 13.65 |
分位点回帰が最小化する目的関数は ピンボール損失 (チェック損失とも呼ぶ) です。 τ ∈ (0, 1) のとき、 残差 u = y − ŷ に対し:
$$ \rho_\tau(u) = \begin{cases} \tau \cdot u & (u \geq 0) \\ -(1-\tau) \cdot u & (u < 0) \end{cases} $$
これは「予測が下振れ (u>0) したときのペナルティ τ」と「上振れ (u<0) したときのペナルティ 1-τ」を非対称に課す関数で、 τ=0.5 のとき左右対称となり中央値回帰 (LAD 回帰) と一致します。 τ=0.9 なら下振れ予測を 9 倍重く罰し、 結果として「90% 分位」の上を通る線が得られます。
このコードでやること: sklearn.linear_model.QuantileRegressor を用いて、 ピンボール損失の数値最小化を直接行う。 statsmodels の内部実装 (LP based) と異なり scikit-learn は L1 正則化と組み合わせて使うことで スパース分位点回帰も実現できる。
📥 入力データ (47 県の人口 → 延べ宿泊者数):
1 2 3 4 5 6 7 8 9 10 11 | 🐍 sklearn_quantreg_pinball.py<button onclick="copyTryitCode(this)">📋 コピー</button></div><div class="pygblock">import pandas as pd from sklearn.linear_model import QuantileRegressor from sklearn.metrics import mean_pinball_loss df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) df = df[df['SSDSE-B-2026'] == 2023] X = (df[['A1101']] / 1e4).values y = (df['G7101'] / 1e4).values for q in [0.10, 0.50, 0.90]: m = QuantileRegressor(quantile=q, alpha=0.0, solver='highs').fit(X, y) loss = mean_pinball_loss(y, m.predict(X), alpha=q) print(f'tau={q:.2f} pinball_loss={loss:7.2f} coef={m.coef_.round(4)}')</div> |
📤 実行すると次の出力が得られる:
💬 ピンボール損失は τ=0.5 で最大、 両端 (τ=0.1, 0.9) で小さくなります。 これは「中央値推定は全データの絶対値偏差和を最小化するので大きく、 両端は片側 10% だけ責任を持つので小さい」という幾何学的な帰結です。 業務応用では、 例えば 需要予測の安全在庫 (τ=0.95) や VaR 推定 (τ=0.01) など、 τ を業務 KPI に合わせて設計します。
合成データで τ=0.5 (中央値回帰) の損失を計算する。
| e | ρ_0.5 | ρ_0.9 (高位) |
|---|---|---|
| +2 | 1.0 | 1.8 |
| +1 | 0.5 | 0.9 |
| 0 | 0 | 0 |
| -1 | 0.5 | 0.1 |
| -2 | 1.0 | 0.2 |
1 2 3 4 5 | import numpy as np e = np.array([2, 1, 0, -1, -2]) def rho(u, tau): return u * (tau - (u < 0).astype(float)) print(f"τ=0.5: {rho(e, 0.5).sum()}") print(f"τ=0.9: {rho(e, 0.9).sum()}") |
💬 手計算 (Step 3) と Python 出力が完全一致。
🎯 このコードでやること:statsmodels の quantreg で SSDSE-B 2026 都道府県人口→延べ宿泊者数の関係を τ=0.10/0.50/0.90 の 3 分位点で線形回帰。 中央値 (50%) と下位 10%・上位 90% の回帰係数の違いから「散らばりが人口で広がるか」を読む。
📥 入力:SSDSE-B-2026.csv の 2023 年データ (47 都道府県)。 列 A1101=人口 → 万人単位 / G7101=延べ宿泊者数 → 万人泊単位に換算。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | import pandas as pd import numpy as np import statsmodels.formula.api as smf import matplotlib.pyplot as plt # SSDSE-B 2026 を読み込み、2023 年に絞る df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]).rename(columns={'SSDSE-B-2026': '年度'}) df = df[df['年度'] == 2023].copy() df = df.rename(columns={'A1101': '人口', 'G7101': '延べ宿泊者数'}) # 単位を万人・万人泊に揃える(数値の桁を整える) df['人口万人'] = df['人口'] / 1e4 df['宿泊万人泊'] = df['延べ宿泊者数'] / 1e4 # 3 つの分位点で回帰 taus = [0.10, 0.50, 0.90] models = {} for tau in taus: mod = smf.quantreg('宿泊万人泊 ~ 人口万人', data=df) res = mod.fit(q=tau) models[tau] = res print(f'τ={tau}: intercept={res.params[0]:.2f}, slope={res.params[1]:.3f}') |
📤 実行例:
💬 結果の読み方:slope (人口万人 → 延べ宿泊者数 万人泊) が τ=0.10 で 2.13、 τ=0.90 で 5.57 と増加 → 人口が同じでも上位 (90 パーセンタイル) ほど人口あたりの宿泊者数が多い。 OLS なら 1 本の回帰直線だが、 分位点回帰は「散らばりが人口に依存して扇形に広がる」構造を可視化できる。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | fig, ax = plt.subplots(figsize=(9, 6)) # 散布図 ax.scatter(df['人口万人'], df['宿泊万人泊'], s=70, alpha=0.6, edgecolor='white', linewidth=1.2, color='#1565C0', label='47 都道府県 (2023)') # 3 本の分位点直線 x_grid = np.linspace(df['人口万人'].min(), df['人口万人'].max(), 100) colors = {0.10: '#D32F2F', 0.50: '#388E3C', 0.90: '#D32F2F'} styles = {0.10: '--', 0.50: '-', 0.90: '--'} for tau, res in models.items(): a, b = res.params ax.plot(x_grid, a + b * x_grid, color=colors[tau], linestyle=styles[tau], linewidth=2.2, label=f'τ = {tau:.2f}') # 比較のため OLS も import statsmodels.formula.api as smf ols = smf.ols('宿泊万人泊 ~ 人口万人', data=df).fit() ax.plot(x_grid, ols.params[0] + ols.params[1] * x_grid, color='#1976D2', linewidth=2.2, linestyle=':', label='OLS (平均)') ax.set_xlabel('人口総数(万人)') ax.set_ylabel('延べ宿泊者数(万人泊)') ax.set_title('分位点回帰:延べ宿泊者数 ~ 人口(10/50/90 パーセンタイル)') ax.legend(loc='upper left') plt.tight_layout() plt.show() |
大きなデータや非線形関係には、 LightGBM の objective='quantile' が高速で便利です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | from lightgbm import LGBMRegressor # 同じデータで 3 本の分位点モデルを学習 X = df[['人口万人']].values y = df['宿泊万人泊'].values quantile_models = {} for tau in [0.10, 0.50, 0.90]: m = LGBMRegressor(objective='quantile', alpha=tau, n_estimators=300, learning_rate=0.05, num_leaves=15, random_state=42, verbose=-1) m.fit(X, y) quantile_models[tau] = m # 各 τ で予測(同じ x_grid 上) X_grid = x_grid.reshape(-1, 1) preds = {tau: m.predict(X_grid) for tau, m in quantile_models.items()} # 予測区間として fill_between fig, ax = plt.subplots(figsize=(9, 6)) ax.fill_between(x_grid, preds[0.10], preds[0.90], alpha=0.18, color='#D32F2F', label='80% 予測区間') ax.plot(x_grid, preds[0.50], color='#388E3C', linewidth=2.3, label='中央値') ax.scatter(df['人口万人'], df['宿泊万人泊'], s=60, alpha=0.6, color='#1565C0') ax.set_xlabel('人口(万人)'); ax.set_ylabel('延べ宿泊者数(万人泊)') ax.legend(); plt.tight_layout(); plt.show() |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | # ── この抜粋で使うモデルを用意します ── import pandas as pd from sklearn.ensemble import GradientBoostingRegressor from sklearn.metrics import mean_pinball_loss df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) df = df[df['SSDSE-B-2026'] == 2023] # 2023 年の 47 都道府県 X = df[['A1101', 'A1303']].astype(float).values y = df['A4101'].astype(float).values # 出生数 quantile_models = {q: GradientBoostingRegressor(loss='quantile', alpha=q, random_state=0).fit(X, y) for q in (0.10, 0.90)} # τ=0.9 モデルの平均 pinball loss y_pred_90 = quantile_models[0.90].predict(X) loss_90 = mean_pinball_loss(y, y_pred_90, alpha=0.90) print(f'τ=0.90 の pinball loss: {loss_90:.2f}') # カバレッジ(予測区間に入った観測値の割合) y_lo = quantile_models[0.10].predict(X) y_hi = quantile_models[0.90].predict(X) coverage = ((y >= y_lo) & (y <= y_hi)).mean() print(f'80%予測区間のカバレッジ: {coverage*100:.1f}%') # 理論値 80% |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | # ── この抜粋で使うデータを用意します ── import pandas as pd import statsmodels.formula.api as smf df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) df = df[df['SSDSE-B-2026'] == 2023] # 2023 年の 47 都道府県 df = df.copy() df['宿泊万人泊'] = df['G7101'].astype(float) / 1e4 # 延べ宿泊者数 df['人口万人'] = df['A1101'].astype(float) / 1e4 # 人口 + 気温 + 消費支出(万円)で 3 つの分位点を推定 df['気温'] = df['B4101'] df['消費支出万円'] = df['L3221'] / 1e4 for tau in [0.10, 0.50, 0.90]: res = smf.quantreg('宿泊万人泊 ~ 人口万人 + 気温 + 消費支出万円', data=df).fit(q=tau) print(f'τ={tau}:') print(res.params.round(4)) print() |
res.summary() の SE は参考程度に、 ② ブートストラップ法(n_boot=1000)で再サンプリングして信頼区間を計算するのが正攻法。
objective='quantile' は τ ごとに別モデルNGBoost や conformal prediction を検討、 ② quantile-forest パッケージは 1 モデルで全 τ を出せる、 ③ 結果を rearrange して単調化。
| 階層 | 概念 | 関係 |
|---|---|---|
| 最上位 | 回帰分析 | 分位点回帰はその 1 流派 |
| 同列 | OLS, ロバスト回帰, GAM | いずれも条件付き分布の何かを推定 |
| 特殊例 | 中央値回帰(τ=0.5)= LAD 回帰 | L1 損失最小化と等価 |
| 前提 | 分位点、 pinball loss、 線形計画 | 統計と最適化の交点 |
| 応用 | 予測区間、 リスク分析(VaR)、 不平等指標 | 経済・金融・公衆衛生で頻出 |
| 機械学習版 | 分位点 RF, LightGBM-quantile, NGBoost | 非線形化と高次元対応 |
「分位点回帰」は単独で完結する手法ではなく、 隣接領域と連携することで真価を発揮する。
SSDSE-B-2026 の延べ宿泊者数を分位点回帰で予測すると、 中央値 (τ=0.5) と上位 (τ=0.9) で人口の係数が異なり、 「人口が増えると上位県ほど宿泊者数が急増」の不均一性が定量化できる。
「分位点回帰」を実際の課題に当てはめるとき、 状況別に何を選ぶかを 3 段階で判定する。
SSDSE-B-2026 の延べ宿泊者数を人口で回帰する際、 OLS は東京の影響で傾きが過大評価される。 0.5 分位回帰なら中央値ベースの「典型的な県の人口-宿泊者数 関係」が得られる。
このページの本文・ウィジェットは ピンボール損失で条件付き分位点を引くという
分位点回帰の理論を丁寧に扱っている。ここでは重複を避け、
「たった 1 つの極端な観測(東京都)を抜くと、OLS と中央値回帰でどれだけ結果がブレるか」
という 実データによるストレステスト の角度から深掘りする。数値は
data/raw/SSDSE-B-2026.csv の 2023 年・47 都道府県、
A1101 総人口 を説明変数、G7101 延べ宿泊者数 を目的変数として
実際に statsmodels で推定した実測値のみを載せる。
東京都は総人口 14.086 百万人・延べ宿泊者数 80.27 百万人泊 と、 他県から大きく離れた 高レバレッジ点(1 点の巨人) だ。 平均を最小化する OLS はこの 1 点に敏感で、まるで綱引きのように傾きが引っぱられる。 一方、中央値回帰(τ=0.5)は「順位」だけを見るので、 極端な 1 点が上に飛ぼうが下に飛ぼうが、それが多数派の並びを崩さない限り線はほとんど動かない。 下の表は、その「踏ん張り」を実測で示したものだ。
| 推定(延べ宿泊者数 ~ 総人口, 百万単位) | 全 47 県の傾き | 東京を除く 46 県の傾き | 変化率 |
|---|---|---|---|
| OLS(条件付き平均) | 4.106 | 2.935 | −28.5% |
| 中央値回帰(τ=0.5) | 4.076 | 3.503 | −14.1% |
同じ「東京を 1 県抜く」という操作で、OLS の傾きは 28.5% も動くのに対し、 中央値回帰は 14.1%——およそ半分の揺れで済む。 これが「中央値回帰は外れ値・高レバレッジ点にロバスト」という主張の、実データでの姿だ。 なお全 47 県では両者の傾きが 4.106 と 4.076 とほぼ一致している点も面白い: 東京がたまたま回帰線の近くに乗っているうちは差が出ず、差は「抜いたとき」に初めて露見する。
τ=0.1 / 0.5 / 0.9 を独立に推定すると、3 本の直線の傾き・切片は次のようになる(2023 年・実測)。
観測されている総人口の範囲(最小は 鳥取県 0.537 百万人〜最大は東京 14.086 百万人)の 内側では、q10 ≤ q50 ≤ q90 の順序は一度も崩れない (鳥取・中央値県・東京のいずれの人口でも予測が正しく並ぶことを確認済み)。 ところが τ=0.1 と τ=0.5 の 2 直線を延長すると、 総人口 0.417 百万人のあたりで交差する。 これは観測最小の 0.537 百万人より 外側(データの台の外) だ。
📝 教訓:分位点クロッシング(下位分位の予測が上位分位を上回る破綻)は、 多くの場合 データが薄い/存在しない領域への外挿で顔を出す。 「クロスした=手法が壊れた」ではなく、「そこは推定を信じてはいけない領域」だと教えてくれる警報と読むのが正しい。 実務では観測範囲内の予測に限定するか、単調性制約付き推定(本文の Crossing 修正パート参照)で対処する。
上の 3 本から、任意の人口 x に対する 80% 予測区間を
[q10(x), q90(x)] として組める。幅は
(1.791−0.454) + (5.572−2.132)·x = 1.337 + 3.440·x となり、
人口が大きい県ほど区間幅が線形に広がる。
これは OLS+等分散仮定では決して得られない情報だ——
OLS の予測区間は全 x で(ほぼ)一定幅を仮定するのに対し、
分位点回帰は 「ばらつきの大きさ自体が x で変わる」不等分散を、モデルの出力として直接返す。
延べ宿泊者数のように「大都市ほど年ごと・施設ごとの振れ幅が大きい」データでは、この性質が予測の実用価値を決める。
さらに一歩進めるなら、τ を 0.05〜0.95 まで連続的に動かして係数の軌跡を描く τ-プロセス(quantile process)が、 「人口の効き方が分位によってどう変わるか」を 1 枚で見せてくれる (本文の τ-process 相当パートを参照)。ここでも傾きは τ とともに 2.132 → 4.076 → 5.572 と 単調に増加しており、「上位県ほど人口 1 単位あたりの宿泊者数の伸びが大きい」という不均一性が読み取れる。
※ 本節の数値はすべて SSDSE-B-2026(2023 年・47 都道府県、列 A1101/G7101)を
statsmodels で実際に推定した実測値。合成・架空データは使用していない。