Statsmodelsのサマリレポートの日本語化

Statsmodelsは、2009年にGoogle Summer of Code の採択プロジェクトとして誕生した。2010年代前半にパッケージ名をstatsmodelsへ移行した。機械学習、AI全盛の中でニッチなユーザー層をもち、学術研究や経済分析でデファクト・スタンダードと見なされている。しかし、summary reportなどでの日本語化が遅れているため、日本語化してみた。

```Python
import re
import numpy as np
import pandas as pd
import statsmodels.api as sm
from wcwidth import wcswidth

# ─────────────────────────
# 1. 文字幅ユーティリティ
# ─────────────────────────
def ljust_jp(s, w):  # 左寄せ
    return s + ' ' * max(0, w - wcswidth(s))

def rjust_jp(s, w):  # 右寄せ（数値）
    return ' ' * max(0, w - wcswidth(s)) + s

_num = re.compile(r'^[+\-]?\d+(?:\.\d+)?(?:[eE][+\-]?\d+)?$')
isnum = lambda s: bool(_num.fullmatch(s.strip()))

# ─────────────────────────
# 2. 日本語ラベル
# ─────────────────────────
JP = {
    'Dep. Variable:':  '目的変数:',
    'Model:':          'モデル:',
    'Method:':         '推定方法:',
    'Date:':           '推定日時:',
    'Time:':           '',
    'No. Observations:':'サンプル数:',
    'Df Residuals:':   '残差の自由度:',
    'Df Model:':       'モデルの自由度:',
    'Covariance Type:':'共分散のタイプ:',
    'R-squared:':      '決定係数 (R^2):',
    'Adj. R-squared:': '自由度調整済R^2:',
    'F-statistic:':    'F統計量:',
    'Prob (F-statistic):':'F統計量のp値:',
    'Log-Likelihood:': '対数尤度:',
    'AIC:':            'AIC:',
    'BIC:':            'BIC:',
    'coef':            '係数',
    'std err':         '標準誤差',
    't':               't値',
    'P>|t|':           'p値',
    '[0.025':          '[0.025',
    '0.975]':          '0.975]',
}

# ─────────────────────────
# 3. 整形関数
# ─────────────────────────
def summary_jp(results, trans=JP, sep='  '):
    """OLS Summary を日本語表示＆罫線・Notes付きで返す"""
    summ = results.summary()

    # --- 1) テーブル部分を 2D → 整形行リストへ ---
    tables_fmt = []
    for tbl in summ.tables:
        rows = [[str(c.data).strip() for c in r] for r in tbl]
        if not rows:
            continue
        ncol = max(len(r) for r in rows)
        for r in rows:           # 列数合わせ
            r += [''] * (ncol - len(r))

        # 列幅（全角考慮）
        widths = [max(wcswidth(trans.get(r[c], r[c])) for r in rows)
                  for c in range(ncol)]

        # パディング
        fmt = []
        for r in rows:
            pcells = []
            for i, c in enumerate(r):
                c = trans.get(c, c)
                pcells.append(rjust_jp(c, widths[i]) if isnum(c)
                               else ljust_jp(c, widths[i]))
            fmt.append(sep.join(pcells).rstrip())
        tables_fmt.append(fmt)

    # --- 2) 罫線幅は最大行幅に合わせる ---
    width = max(max(wcswidth(l) for t in tables_fmt for l in t),
                wcswidth('OLS 回帰結果（日本語）'))
    eq_line  = '=' * width
    dash_line = '-' * width

    # --- 3) 組み立て ---
    out = []
    out.append('OLS 回帰結果（日本語）'.center(width))
    out.append(eq_line)
    out.extend(tables_fmt[0])          # ヘッダ
    out.append(eq_line)
    out.append(tables_fmt[1][0])       # 係数ヘッダ
    out.append(dash_line)
    out.extend(tables_fmt[1][1:])      # 係数本体
    out.append(eq_line)
    if len(tables_fmt) > 2:            # 診断統計
        out.extend(tables_fmt[2])
        out.append(eq_line)

    # --- 4) Notes を末尾に ---
    if summ.extra_txt:
        out.append(summ.extra_txt)

    return '\n'.join(out)

# ─────────────────────────
# 4. デモ
# ─────────────────────────
if __name__ == '__main__':
    np.random.seed(0)
    df = pd.DataFrame({
        '目的変数Y':  np.random.rand(100),
        '説明変数X1': np.random.rand(100),
        '定数項':     1
    })
    y, X = df['目的変数Y'], df[['説明変数X1', '定数項']]
    res = sm.OLS(y, X).fit()           # cov_type='HC3' などでも可

    print(summary_jp(res))
```
```
                      OLS 回帰結果（日本語）                        
============================================================
目的変数:        目的変数Y         決定係数 (R^2):     0.004
モデル:          OLS               自由度調整済R^2:   -0.006
推定方法:        Least Squares     F統計量:           0.4302
推定日時:        Wed, 18 Jun 2025  F統計量のp値:       0.513
                 00:16:28          対数尤度:         -17.300
サンプル数:                   100  AIC:                38.60
残差の自由度:                  98  BIC:                43.81
モデルの自由度:                 1
共分散のタイプ:  nonrobust
============================================================
            係数     標準誤差  t値     p値    [0.025  0.975]
------------------------------------------------------------
説明変数X1  -0.0689     0.105  -0.656  0.513  -0.277   0.140
定数項       0.5092     0.063   8.132  0.000   0.385   0.633
============================================================
Omnibus:        30.275  Durbin-Watson:      2.100
Prob(Omnibus):   0.000  Jarque-Bera (JB):   5.797
Skew:            0.037  Prob(JB):          0.0551
Kurtosis:        1.823  Cond. No.            4.68
============================================================
Notes:
[1] Standard Errors assume that the covariance matrix of the errors is correctly specified.
```
また、より簡単な方法として、

```python
import pandas as pd
from datetime import datetime
import statsmodels.api as sm
import statsmodels.formula.api as smf
import numpy as np
from wcwidth import wcswidth

def summary_jp2(results):

    def pad(text, width, align='left'):
        padding = ' ' * (width - wcswidth(text))
        if align == 'right':
            return padding + text
        return text + padding

    # モデル概要テーブル
    left_keys = ["従属変数:", "モデル:", "推定方法:", "日付:", "時刻:", "観測数:", "残差の自由度:", "モデルの自由度:", "共分散タイプ:"]
    left_values = [
        results.model.endog_names,
        results.model.__class__.__name__,
        "最小二乗法",
        datetime.now().strftime("%Y年%m月%d日"),
        datetime.now().strftime("%H:%M:%S"),
        f"{int(results.nobs)}",
        f"{int(results.df_resid)}",
        f"{int(results.df_model)}",
        results.cov_type
    ]

    right_keys = ["決定係数 (R-squared):", "自由度調整済み決定係数:", "F値:", "F値の確率 (Prob (F-statistic)):", "対数尤度 (Log-Likelihood):", "AIC:", "BIC:"]
    right_values = [
        f"{results.rsquared:.3f}",
        f"{results.rsquared_adj:.3f}",
        f"{results.fvalue:.2f}",
        f"{results.f_pvalue:.2e}",
        f"{results.llf:.2f}",
        f"{results.aic:.1f}",
        f"{results.bic:.1f}"
    ]

    print(" " * 29 + "OLS回帰分析の結果")
    print("=" * 87)

    lk_width = 17
    lv_width = 21
    rk_width = 38
    rv_width = 8

    for i in range(max(len(left_keys), len(right_keys))):
        lk = left_keys[i] if i < len(left_keys) else ""
        lv = left_values[i] if i < len(left_values) else ""
        rk = right_keys[i] if i < len(right_keys) else ""
        rv = right_values[i] if i < len(right_values) else ""

        lk_padded = pad(lk, lk_width)
        lv_padded = pad(lv, lv_width, align='right')
        rk_padded = pad(rk, rk_width)
        rv_padded = pad(rv, rv_width, align='right')


        print(f"{lk_padded}{lv_padded}   {rk_padded}{rv_padded}")

    # 係数テーブル
    coef_df = pd.DataFrame({
        "係数": results.params,
        "標準誤差": results.bse,
        "t値": results.tvalues,
        "p値": results.pvalues,
        "95%信頼区間(下限)": results.conf_int()[0],
        "95%信頼区間(上限)": results.conf_int()[1],
    })

    header = "                     係数      標準誤差         t値         p値    [0.025      0.975]"

    print("=" * 87)
    print(header)
    print("-" * 87)

    for index, row in coef_df.iterrows():
        index_str = f"{index:<17}"
        row_str = f"{row['係数']:>9.4f}     {row['標準誤差']:>9.3f}   {row['t値']:>9.3f}   {row['p値']:>9.3f}   {row['95%信頼区間(下限)']:>9.3f}   {row['95%信頼区間(上限)']:>9.3f}"
        print(f"{index_str} {row_str}")
    print("=" * 87)

    # 統計量テーブル
    jb_stat, jb_p, skew, kurtosis = sm.stats.stattools.jarque_bera(results.resid)
    omnibus_stat, omnibus_p = sm.stats.stattools.omni_normtest(results.resid)

    diag_left_keys = ["オムニバス検定 (Omnibus):", "確率(Omnibus):", "歪度 (Skew):", "尖度 (Kurtosis):"]
    diag_left_values = [ f"{omnibus_stat:.3f}", f"{omnibus_p:.3f}", f"{skew:.3f}", f"{kurtosis:.3f}" ]
    diag_right_keys = ["ダービン・ワトソン比:", "ジャック・ベラ検定 (JB):", "確率(JB):", "条件数 (Cond. No.):"]
    diag_right_values = [ f"{sm.stats.stattools.durbin_watson(results.resid):.3f}", f"{jb_stat:.3f}", f"{jb_p:.3f}", f"{results.condition_number:.0f}" ]

    for i in range(len(diag_left_keys)):
        lk = diag_left_keys[i]
        lv = diag_left_values[i]
        rk = diag_right_keys[i]
        rv = diag_right_values[i]

        lk_padded = pad(lk, 32)
        lv_padded = pad(lv, 10, align='right')
        rk_padded = pad(rk, 28)
        rv_padded = pad(rv, 8, align='right')

        print(f"{lk_padded}{lv_padded}   {rk_padded}{rv_padded}")
    print("=" * 78)

dat = sm.datasets.get_rdataset("Guerry", "HistData").data
results = smf.ols('Lottery ~ Literacy + np.log(Pop1831)', data=dat).fit()

summary_jp2(results)
```
```
                             OLS回帰分析の結果
=======================================================================================
従属変数:                      Lottery   決定係数 (R-squared):                    0.348
モデル:                            OLS   自由度調整済み決定係数:                  0.333
推定方法:                   最小二乗法   F値:                                     22.20
日付:                   2025年06月18日   F値の確率 (Prob (F-statistic)):       1.90e-08
時刻:                         00:30:35   対数尤度 (Log-Likelihood):             -379.82
観測数:                             86   AIC:                                     765.6
残差の自由度:                       83   BIC:                                     773.0
モデルの自由度:                      2                                                 
共分散タイプ:                nonrobust                                                 
=======================================================================================
                     係数      標準誤差         t値         p値    [0.025      0.975]
---------------------------------------------------------------------------------------
Intercept          246.4341        35.233       6.995       0.000     176.358     316.510
Literacy            -0.4889         0.128      -3.832       0.000      -0.743      -0.235
np.log(Pop1831)    -31.3114         5.977      -5.239       0.000     -43.199     -19.424
=======================================================================================
オムニバス検定 (Omnibus):            3.713   ダービン・ワトソン比:          2.019
確率(Omnibus):                       0.156   ジャック・ベラ検定 (JB):       3.394
歪度 (Skew):                        -0.487   確率(JB):                      0.183
尖度 (Kurtosis):                     3.003   条件数 (Cond. No.):              702
==============================================================================
```