残差診断#

in English or the language of your choice.

import lmdiag
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import statsmodels.api as sm

from statsmodels.formula.api import ols
from scipy.stats import norm, uniform

# 警告メッセージを非表示
import warnings
warnings.filterwarnings("ignore")

説明#

<目的>

実際に回帰分析をする際,少なくとも2つの問題が存在する。

  • 母集団回帰式自体が正しいかどうか(モデルの特定化の問題)。

  • GM仮定1,4,5は観測できない母集団回帰式の誤差項に関するものだが,これらの仮定が成立するかどうか。

これらは重要な問題であり,非常に難しい問題でもある。1つの対処方法として,誤差項の推定値である残差の値の分布など図示して大まかな判断をする方法がある。ここではこの手法について考えることにする。

<内容>

  1. 残差と\(R^2\)

    • 残差と決定係数の関係をシミュレーションで確認する。

  2. 「理想的な」残差の分布

    • 標本の大きさによって残差の散布図がどう変化するかを確かめる。

  3. 回帰診断

    • 回帰結果の「健康状態」を示すパッケージlmdiagを紹介する。

  4. 問題がある残差の例

    • lmdiagを使って確認する。

残差と\(R^2\)#

説明#

回帰分析のシミュレーションをおこない残差の分布と\(R^2\)の関係を図を使っておさらいする。


単回帰のシミュレーション用の関数ols_simを定義する。

  • 引き数

    1. 標本の大きさn

    2. 誤差項の標準偏差u_sd

  • 返り値

    1. 説明変数 \(x\)

    2. 被説明変数 \(y\)

    3. 予測値 \(\hat{y}\)

    4. 残差 \(u\)

    5. 標準化残差

      • 残差\(u\)は母集団の誤差項の推定値であるが,\(u\)を使い誤差項の性質について調べるには限度があることが知られている。その代わりになるのが「標準化残差」である。\(u\)をある統計量で除して標準化した残差であり,観測できない誤差項についてより正確な情報が得られる。

      • OLSの結果のメソッドget_influence()を使い,その属性.resid_studentized_internalで取得する

      • 英語ではInternally Studentized ResidualsもしくはStandardized Residualsと呼ばれる。

    6. \(R^2\)

def ols_sim(n, u_sd):  # n=標本の大きさ, u_sd=誤差項の標準偏差
    
    x = uniform.rvs(1, 10, size=n)  # 説明変数
    u = norm.rvs(scale=u_sd, size=n)  # 誤差項
    y = 1.0 + 0.5*x + u               # 被説明変数
    
    df = pd.DataFrame({'Y':y, 'X':x})  # DataFrame
    
    res = ols(formula='Y ~ X', data=df).fit()  # OLSの計算
    u_standardized = res.get_influence().resid_studentized_internal  # 標準化残差
    
    return x, y, res.fittedvalues, res.resid, u_standardized, res.rsquared  # 返り値の設定

この関数を使い\(R^2\)が高いケースと低いケースを別々にシミュレーションを行う。返り値の順番に合わせて代入する変数を並べる。(添え字のhighlowは「高いケース」と「低いケース」を表す。)

# R^2が高いケース
x_high, y_high, y_fit_high, resid_high, resid_st_high, r2_high = ols_sim(50, 0.5)

# R^2が低いケース
x_low, y_low, y_fit_low, resid_low, resid_st_low, r2_low = ols_sim(50, 1.5)

ここでは以下のように変数を定義している(highlowの添字は省いている):

  • x: 説明変数

  • y: 被説明変数

  • u: 誤差項

  • f_fit: 予測値

  • resid: 残差

  • resid_st: 標準化残差

  • r2: 決定係数

まず決定係数の値を確認する。(注意)以前説明したf-stringを使っている。

print(f'決定係数が高いケース: {r2_high:.3f}\n決定係数が高いケース: {r2_low:.3f}')
決定係数が高いケース: 0.916
決定係数が高いケース: 0.573

上のコードで:.3fが付け加えられているが,表示する小数点を第三位までと指定するオプション。

散布図と回帰直線#

図を並べて描くために,matplotlibsubplots()を使う。

plt.figure(figsize=(8,3))  # figsizeは左右2つの図を合わせた大きさ

# 左の図
plt.subplot(121)
plt.scatter(x_high,y_high)  # 散布図
plt.plot(x_high, y_fit_high,color='red')  # 回帰線
plt.ylim(-1,10)  # 縦軸の表示幅を設定
plt.xlabel('x')  # 横軸のラベル
plt.ylabel('y')  # 縦軸のラベル
plt.title('High R^2')  # タイトル

# 右の図
plt.subplot(122)
plt.scatter(x_low,y_low)
plt.plot(x_low, y_fit_low, color='red')
plt.ylim(-1,10)
plt.xlabel('x')
plt.ylabel('y')
plt.title('Low R^2')

plt.tight_layout();  # 左右の間に余裕も持たせる
_images/5d9a61329744e67a0ec7500fd0f08f59110a3a6392efe2f83d65819620f764fa.png

回帰直線のフィット感ははっきりと決定係数の値に現れている。

残差#

次に予測値\(\hat{y}\)と残差\(\hat{u}\)の散布図を描いてみる。

plt.figure(figsize=(8,3))

# 左の図
plt.subplot(121)
plt.scatter(y_fit_high,resid_high)
plt.axhline(y=0, color='red')  # 縦軸の値が0での平行線
plt.ylim(-5,5)  # 縦軸の表示幅を指定
plt.xlabel('Fitted Values')
plt.ylabel('Residuals')

# 右の図
plt.subplot(122)
plt.scatter(y_fit_low,resid_low)
plt.axhline(y=0, color='red')
plt.ylim(-5,5)
plt.xlabel('Fitted Values')
plt.ylabel('Residuals')

plt.tight_layout();
_images/6839c6145d81304003749e98c5a4c88127bdbd6331010a127694dbfc4f1115f5.png

ここでも決定係数の値との関係がハッキリとわかる。

標準化残差#

予測値と標準化残差の散布図を図示する。

plt.figure(figsize=(8,3))

# 左の図
plt.subplot(121)
plt.scatter(y_fit_high,resid_st_high)
plt.axhline(y=0, color='red')  # 縦軸の値が0での平行線
plt.ylim(-5,5)  # 縦軸の表示幅を指定
plt.xlabel('Fitted Values')
plt.ylabel('Standardized Residuals')

# 右の図
plt.subplot(122)
plt.scatter(y_fit_low,resid_st_low)
plt.axhline(y=0, color='red')
plt.ylim(-5,5)
plt.xlabel('Fitted Values')
plt.ylabel('Standardized Residuals')

plt.tight_layout();
_images/04b0535cf48506430ba126aaae3183340ee9e7d088df9a14d4f44fc8cf41215e.png

左右ともに散らばりの差はあまりない。もちろん,理由は平均0,分散1になるように「標準化」されているためである。

「理想的な」残差の分布#

GM仮定1,4,5が成立する場合,残差は平均0で分散が一定なランダムな分布になる。ここでは,そのような「理想的な残差」はどのようなものかを図示し,標本の大きさによってどのように見えるのかを確認する。正規分布の確率変数を「理想的な残差」として考える。

まず図示するための関数を定義する。

  • 引き数

    • 標本の大きさ:n

    • (残差の)標準偏差:u_sd

  • 返り値

    • 散布図

def resid_ideal(n, u_sd):
    
    xx = list(range(n))  # 0から始まるn個の整数のリスト
    u = norm.rvs(scale=u_sd, size=n)   # 正規分布に従うn個の「残差」
    
    plt.scatter(xx,u)
    plt.axhline(y=0,color='red')
    plt.xlabel('n')
    plt.ylabel('Residuals')
    plt.title('Ideal Residuals')

この関数の返り値はmatplotlibのコードである。返り値として図が表示されることになる。


標準偏差1\(n=2000\)の残差

resid_ideal(2000, 1);
_images/3ec3f03eca313b45e1e4ac28b0f06419a82fad66feb23f2de471a08738b6939e.png

0の周りに均等に分散しているのが確認できる。


標準偏差1\(n=500\)の残差

resid_ideal(500, 1);
_images/94e5342a691a533d60d93e248fe3bc73b011a2bb514fd59ecc4890d3ce72e06f.png

観測値の間隔は少し広がっているが,シミュレーションによっては\(n=2000\)と比べて大きな変化は見られない場合もあれば,「外れ値」が気になる場合もあるかも知れない。


標準偏差1\(n=100\)の残差

resid_ideal(100, 1);
_images/750744c3aff1e1e9c9c2986c7cf519418d4e053501e0bd49cfaab77834752fc4.png

さらに隙間が広くなっている。シミュレーションによっては,少し偏りがあるように見える場合も発生するため,上の例と比べると,GM仮定1,4,5に関して自信を持って判断しづらくなる。


標準偏差1\(n=30\)の残差

resid_ideal(30, 1);
_images/ef988a027c4686c408f09780dbc78f9f538afcb4660dc7f9987e0307f1a9af93.png

正規分布と分かって眺めるとそう見えるかも知れないが,実際の実証分析を行う上でこの図だけでGM仮定1,4,5が満たされているかどうかを判断するのは難しい。


標準偏差1\(n=20\)の残差

resid_ideal(20, 1);
_images/a566b98b5773d36633eddb0c70a2fe1fb5d0c58dd75631d13c635b74fecb268b.png

標本の大きさが小さい場合,観測数が10少なくなると図が大きく違うように見える。

回帰診断:lmdiag#

説明#

上で見たように,残差を単に図示しただけでは理想的な残差からどれだけ乖離しているかを判断するのは難しいところがある。標本の大きさが小さい場合は特にそうである。ではどのように対処すれば良いのか。1つの方法が,計量経済学のソフトウェアRに実装されている残差を図示するコマンドである。これにより回帰式の定式化に「失敗」がないかをある程度確認することができる。ここではその内の4つの図を自動で描くことができるパッケージlmdiagを紹介する。正常なパターンから著しく異なる場合,回帰式の修正の必要性を知らせる早期警戒システムと思えば良いだろう。

まずGM仮定1~6の全ての仮定が満たされる場合を考え,lmdiagのコマンドを説明する。その後,問題が疑われるケースではどのような図になるかを示すことにする。

次のシミュレーションを考えよう。

n = 50  # 標本の大きさ

x = norm.rvs(loc=4, scale=1, size=n)  # 説明変数
u = norm.rvs(size=n)  # 誤差項(標準正規分布)
y = 1 + 0.5*x + u  # 説明変数

df_diag = pd.DataFrame({'Y':y, 'X':x})  # DataFrameの作成

res_diag = ols(formula='Y ~ X', data=df_diag).fit()  # OLS推定

resid_fit()関数#

  • 横軸:\(y\)の予測値 \(\hat{y}\)

  • 縦軸:残差 \(\hat{u}\)

(目的)

残差に非線形のパターンがないかを確認する。縦軸の0を中心に上下にランダムに散らばっている場合,回帰式は線形と判断できるが,上向きやU字型や何らかのパターンがあると非線形の可能性が示唆される。

  • 赤の線は残差の散らばりに最もフィットする曲線

    • 縦軸0の平行線(点線)が理想的

  • 絶対値が最も大きい3つの残差にインデックスの数字が示されている。

lmdiag.resid_fit(res_diag)
pass
_images/fbdb1c89a04138e1f8363ff7c2efbe8171193fe8cfdf8643caf87a27432c2cf9.png

回帰分析結果の「健康状態」を診断する上でこの図は最もよく使われる。この図について以下の点を覚えておこう。

  • 残差\(\hat{u}\)は観測できない母集団回帰式の誤差項\(u\)の推定である。

  • \(y\)の予測値は\(\hat{y}=\hat{\beta}_0+\hat{\beta}_1x\)である。従って,もし残差\(\hat{u}\)\(x\)に何らかの関係があれば,残差\(\hat{u}\)と予測値\(\hat{y}\)の関係に現れる。

  • GM仮定4を思い出そう。誤差項の条件つき期待値が0,または\(E(u|X)=0\)という仮定である。

    • この仮定が正しければ,残差\(\hat{u}\)の平均は0あり,図の赤い線は0で横軸と並行になることを意味している。

    • この仮定が満たされず,説明変数\(x\)が何らかの形で\(\hat{u}\)に影響を与えているのであれば,その影響は上の図に現れてくる。

q_q()関数#

qqプロットとも呼ばれる。qquantile(分位数)のこと。

  • 横軸:横軸に標準正規分布の理論値

  • 縦軸:標準化残差(standardized residuals)

    • 標準化残差とは平均0,分散1に変換した残差

(目的)

残差が正規分布に従っているかを確認する。データが正規分布に従っている場合,データは45度線(赤の点線)に沿って分布することになる。残差が概ね45度線近傍にあれば問題ない。しかし多くの残差が45度線から外れている場合は,正規分布ではない可能性が大きくなる。

lmdiag.q_q(res_diag)
pass
_images/2cca4410f188f96c06cd74f0f32082d7c3030a63b39e51860c2ce66101374142.png

scale_loc()関数#

  • 横軸:\(y\)の予測値\(\hat{y}\)

  • 縦軸:標準化残差(standardized residuals)の絶対値のルート

    • 標準化残差とは平均0,分散1に変換した残差

(目的)

残差が均一分散かどうかを確認する。縦方向(上下方向)の散らばりが概ね均等であれば均一分散と解釈できるが,何らかのパターンがあれば不均一分散の可能性がある。

  • 赤の線は残差の散らばりに最もフィットする曲線

    • 横軸に平行となるのが理想的

  • 絶対値が最も大きい3つの残差にインデックスの数字が示されている。

lmdiag.scale_loc(res_diag)
pass
_images/12483f85c0b7bce6d0d6590993ab6ec7aca11af6dcfac4bc206aa8f1c201543d.png

resid_lev()関数#

  • 縦軸:標準化残差(standardized residuals)

    • 残差を標準偏差で除して平均0,分散1に変換した残差

    • 99.7%の確率で(-3,3)の間に入る

    • 観測値\(y_i\)が予測値\(\hat{y}\)からどれだけ離れているかを示す(外れ値=outlier)。縦軸の絶対値が大きい(例えば、絶対値3以上)と外れ値の可能性が高まる。

    • この距離が長くなると(絶対値が大きくなると),推定値\(\hat{\beta}_1\)に大きな影響力をもつ可能性が高くなる。

  • 横軸:レバレッジ(leverage)

    • 説明変数がどれだけ「極端」な値を取るかを示す。

    • 単回帰の場合

      • 説明変数\(x_i\)と平均の\(x\)の間の距離を示す。

      • この距離が長くなると,推定値\(\hat{\beta}_1\)に影響力をもつ可能性が高くなり,高いレバレッジの値として反映される。

    • 重回帰の場合

      • 説明変数\(x_{ij},\;j=1,2,..,k\)のうち1つ以上の説明変数の値が極端に大きい場合や小さい場合にレバレッジは高くなる。また説明変数の値の組み合わせが「通常」ではない場合にもレバレッジは高くなる。

(影響力がある観測値)

  • レバレッジと標準化残差の絶対値の両方が大きい観測値

(目的)

OLS推定値を計算する上で影響力が大きい観測値を特定し,推定値が小数の観測値に大きく左右されていないかを確認する。そのような影響力がある観測値は図の右上と右下に現れ,特に点線(Cook’s Distanceと呼ばれる)の外(右上と右下)にある場合は要注意(右の真ん中のエリアは重要ではない)。観測値の散らばりやそのパターンは関係ない。

  • 赤い点線:Cook’s Distance (CD)

    • ある観測値を外して再計算するとどれだけ予測値\(\hat{y}\)が変化するかを数値化したもの。

    • 内側の点線:CD = 0.5

    • 外側の点線:CD = 1

  • 赤い線は観測値にフィットする曲線

  • 最も影響力が大きい観測値にインデックスの数字が示されている。


下の図ではCDの赤い点線は出ていない。

lmdiag.resid_lev(res_diag)
pass
_images/d97782afce474f0956daf61ad604fc7fcd1180e95ca2d2646299ae141757a776.png

plot():4つの図を同時に表示#

plt.figure(figsize=(8,7))
lmdiag.plot(res_diag)
pass
_images/41003f3f915aedda0037ce74b9c9239e8cf4d2452998a9a5189cd2d25006151c.png

qqプロット(again)#

qqプロットを表示する代替方法として,statsmodelsqqplotモジュールがある。ここではその使い方を簡単に説明する。

statsmodels自体をsmとして読み込んでいる。

<の使い方>

    sm.qqplot(引数,fit=True, line='45)
  • 引数:回帰結果の残差を指定する。上の例ではres_diag.resid

  • オプションfit:残差の平均・標準偏差を自動的に計算することを指定する。

  • オプションline:は45度線を表示する。

sm.qqplot(res_diag.resid, fit=True, line='45')
pass
_images/fe3e5e90eaab242090ce3907ed8009675290f14afdad761a2889472ac6a5e784.png

問題がある残差の例#

ケース1:被説明変数の変換が必要な回帰式#

この例では以下を想定する。

  • 母集団回帰式

    \[\ln y = \beta_0 + \beta_1 x + u \]
  • 標本回帰式

    \[y_i = \beta_0 + \beta_1 x_i + u_i\]

(解決方法)

\(y\)を対数変換する。

n = 100  # 標本の大きさ

x = uniform.rvs(1, 10, size=n)  # 説明変数
u = norm.rvs(scale=1, size=n)  # 誤差項
y = np.exp(100.0 + .1*x + u)   # 被説明変数

df = pd.DataFrame({'Y':y, 'X':x})  # DataFrame

res = ols(formula='Y ~ X', data=df).fit()  # OLSの計算
resid_std = res.get_influence().resid_studentized_internal  # 標準化残差

plt.scatter(res.fittedvalues,resid_std)  # 散布図
plt.axhline(y=0, color='red')          # 赤い平行線
plt.xlabel('Fitted Values')
plt.ylabel('Standardized Residuals')
pass
_images/d7d2aea8d367e961e527a654d8378b6fcc395917eac7aaf419af03cc48d46714.png

lmdiagパッケージを使う。

plt.figure(figsize=(8,7))
lmdiag.plot(res)
pass
_images/ec1a6ddeca5c531bae2ea15a471bae7664c9b717c577b133269ba28fea3dd4bc.png

ケース2:説明変数の2乗項の欠落#

この例では以下を想定する。

  • 母集団回帰式

    \[y = \beta_0 + \beta_1 x + \beta_2 x^2+ u \]
  • 標本回帰式

    \[y_i = \beta_0 + \beta_1 x_i + u_i\]

(解決方法)

標本回帰式に\(x^2\)を加える。

n = 100
x = np.linspace(0,16,n)
x2 = x**2
u = norm.rvs(scale=1, size=n)
y = 1.0 + 0.1*x +0.1*x2+ u
df = pd.DataFrame({'Y':y, 'X':x})

res = ols(formula='Y ~ X', data=df).fit()
resid_std = res.get_influence().resid_studentized_internal

plt.scatter(res.fittedvalues,resid_std)
plt.axhline(y=0, color='red')
plt.xlabel('Fitted Values')
plt.ylabel('Standardized Residuals')
pass
_images/4a5574bad646440c723988338f6efd6af00f20414c210c5e9cfd1d0975066673.png

lmdiagパッケージを使う。

plt.figure(figsize=(8,7))
lmdiag.plot(res)
pass
_images/ec367371d39f4c322bf447389c1b36e2b2ee64bc9f82bacc40c8e065245f1f1f.png

ケース3:定数項がある条件により変化する場合#

この例では以下を想定する。(以下にある「条件1」と「条件2」が何らかの理由で成立すると仮定する。)

  • 母集団回帰式

    \[\begin{split} y = \left\{ \begin{split} &\delta_0 + \beta_1 x + u\quad\text{条件1の場合} \\ &\gamma_0 + \beta_1 x + u\quad\text{条件2の場合} \end{split} \right. \end{split}\]
  • 標本回帰式

    \[y_i = \beta_0 + \beta_1 x_i + u_i\]

(解決方法)

ダミー変数を使う。

n = 100

b0 = np.random.choice([1,7], n, p=[0.5,0.5])
x = uniform.rvs(0,10,size=n)
u = norm.rvs(scale=1, size=n)
y = b0 + 0.1*x + u

df = pd.DataFrame({'Y':y, 'X':x})

res = ols(formula='Y ~ X', data=df).fit()
resid_std = res.get_influence().resid_studentized_internal

plt.scatter(res.fittedvalues,resid_std)
plt.axhline(y=0, color='red')
plt.xlabel('Fitted Values')
plt.ylabel('Standardized Residuals')
pass
_images/14ea2eb74f63a44ad2f79a118c41850389651c44d11c7e9fb381da083e4bbd60.png

lmdiagパッケージを使う。

plt.figure(figsize=(8,7))
lmdiag.plot(res)
pass
_images/d509a8a7a726490cc121541a20363c58e55f3b67cae1e4daa0fc792f8d99a92a.png

ケース4:不均一分散#

この例では以下を想定する。

  • 母集団回帰式

    \[y = \beta_0 + \beta_1 x + u(x) \]
    • 誤差項が説明変数に依存し,不均一分散となる。

    • (理由)欠落変数など

  • 標本回帰式

    \[y_i = \beta_0 + \beta_1 x_i + u_i\]

(解決方法)

  • 説明変数を追加し欠落変数をなくす。

  • 変数の変換(対数化など)

  • 可能な推定方法を試す

    • 加重最小二乗法(Weighted Least Squares)

    • 操作変数法

    • パネル推定法

n = 100
x = uniform.rvs(0,10,size=n)
u = norm.rvs(scale=1, size=n)
y = 1 + 0.1*x + x**0.6*u
df = pd.DataFrame({'Y':y, 'X':x})

res = ols(formula='Y ~ X', data=df).fit()
resid_std = res.get_influence().resid_studentized_internal

plt.scatter(res.fittedvalues,resid_std)
plt.axhline(y=0, color='red')
plt.xlabel('Fitted Values')
plt.ylabel('Standardized Residuals')
pass
_images/827c9ee16b4db0124279f3b37da638ce6acb5d339400380c4e280551ebab3e21.png

lmdiagパッケージを使う。

plt.figure(figsize=(8,7))
lmdiag.plot(res)
pass
_images/13e8353644c12228d3871d1037263e24697feebae130290214c79574f8def095.png

ケース5:小数の観測値に影響される場合:高いレバレッジ#

ここでは推定値が1つの観測値に大きく左右される場合を考える。

np.random.seed(123)

n = 20
x = norm.rvs(loc=20, scale=3, size=n)
u = norm.rvs(size=n)
y = 5 + 0.5*x + u

# 外れ値を追加する
x = np.append(x, 30)
y = np.append(y, 10)
df_cd = pd.DataFrame({'Y':y, 'X':x})

# 外れ値がない場合のOLS
res_no = ols(formula='Y ~ X', data=df_cd.loc[0:19,:]).fit()

# 外れ値がある場合のOLS
res_cd = ols(formula='Y ~ X', data=df_cd).fit()

回帰曲線の図示

plt.scatter(x,y)
plt.scatter(30,10, color='red')  # 外れ値
plt.plot(x[:20],res_no.fittedvalues, label='No Outlier')
plt.plot(x,res_cd.fittedvalues, label='With Outlier')
plt.legend()
pass
_images/c3ddc494196891689e3ba7c596c1037f403eb2f0b07f7fa4c9db2feb72668b2b.png

外れ値(赤の観測値)がない場合の回帰診断

plt.figure(figsize=(8,7))
lmdiag.plot(res_no)
pass
_images/70492b8d25a70e6779dd24b012f4e8d7d325d84e20ceaa03c0ddab1d2e9cbe78.png

外れ値(赤の観測値)がある場合の回帰診断

plt.figure(figsize=(8,7))
lmdiag.plot(res_cd)
pass
_images/0fd24f0dc55f14b3c7f442809a271dcd90596873d911ff05a6773e94b758179b.png

観測値20が非常に影響力が高いことが分かる。その観測値を取り出すには以下のようにする。

  • 右下の図でCDの赤い点線の外(右下)にある観測値のインデックスは20であり、他の図でも20は他の観測値と比べると「外れ値」の疑いが高いことがわかる。インデックス20を使い観測値を抽出する。

df_cd.iloc[20,:]
Y    10.0
X    30.0
Name: 20, dtype: float64