PHPで相関係数と回帰直線を表示

(1/1)
PHPで人口を推計する」では、日本の将来推計人口をシミュレーションした。現実には、合計特殊出生率が、なかなか上昇せず、人口減少が加速している。
なぜ出生率は上昇しないのか――婚姻率の低下と離婚率の上昇が関係しているのではないかと考え、それを確かめるため、高校数学で習った相関係数を求めてみることにする。
あわせて、データ集合の散布図回帰曲線を描いてみる。

(2025年1月26日)統計表を更新,PHP8.4対応

目次

サンプル・プログラムの実行例

PHPで相関係数と回帰直線を表示
当初の予想通り、婚姻率と出生率の間には強い正の相関関係が、離婚率と出生率の間には負の相関関係があることが検証できた。

ここで注意してほしいことがある――婚姻率や離婚率と出生率の間に相関関係があるからといっても、それは、婚姻は離婚が原因で出生率が低下しているという結果(因果関係)になっているわけではない。つまり、相関関係と因果関係は別物であるということだ。

2つのデータ集合X, Yがあり、Xが変化すればYも変化することを相関関係という。
これに対し、X(原因)→Y(結果)という一方通行の関係が因果関係である。相関関係は因果関係の必要条件になり得るが、十分条件ではないのである。

サンプル・プログラム

圧縮ファイルの内容
correlation.phpサンプル・プログラム本体
birthrate.csv統計表
pahooStat.php統計に関わるクラス pahooStat。
使い方は「PHPで相関係数と回帰直線を表示」などを参照。include_path が通ったディレクトリに配置すること。

解説:準備

correlation.php

  35: // jqPlotのあるフォルダ
  36: define('JQPLOT', '../../../../common/jqplot/');
  37: 
  38: // 統計表
  39: define('FILE_CSV', 'birthrate.csv');
  40: 
  41: // CSVデリミタ
  42: define('CSV_DELIMITER', ',');
  43: 
  44: // グラフの幅、高さ
  45: define('GRAPH_WIDTH',  600);
  46: define('GRAPH_HEIGHT', 350);
  47: 
  48: // 婚姻率、離婚率、合計特殊出生率 推移グラフの名前
  49: define('GRAPH_PROGRESS', 'jqPlot_progress');
  50: 
  51: // 合計特殊出生率=婚姻率 相関グラフの名前
  52: define('GRAPH_MARRIAGE', 'jqPlot_marriage');
  53: 
  54: // 合計特殊出生率=離婚率 相関グラフの名前
  55: define('GRAPH_DIVORCE', 'jqPlot_divorce');
  56: 
  57: // 合計特殊出生率=初婚年齢 相関グラフの名前
  58: define('GRAPH_1STMARRIAGE', 'jqPlot_1stmarriage');
  59: 
  60: // 統計に関わるクラス:include_pathが通ったディレクトリに配置
  61: require_once('pahooStat.php');

年次推移や散布図を描くのに、jQueryプラグイン「jqPlot」を利用する。
jqPlot の入手方法、インストール方法については、「PHPでNHK政治意識月例調査をグラフ表示」を参照してほしい。

相関係数PECL の統計ライブラリを導入すれば計算できるが、今回は、自前のクラスを用意することにした。
クラスについては「PHPでクラスを使ってテキストの読みやすさを調べる」で解説している。
クラスファイル "pahooStat.php" は、組み込み関数  require_once  を使って読めるディレクトリに配置する。ディレクトリは、設定ファイル php.ini に記述されているオプション設定 include_path に設定しておく。
pahooStat クラスの内容については後述する。

毎年の婚姻率、離婚率、合計特殊出生率、平均初婚年齢(妻)は、厚生労働省「令和5年(2023)人口動態統計月報年計(概数)の概況」をExcelに入力し、統計表ファイル FILE_CSV(CSV形式ファイル)として保存してある。
これらのデータを参照し、各種グラフを描いてゆく。

解説:データの検査

pahooStat.php

  61: /**
  62:  * 配列かどうか、要素が1つ以上あるかどうか検査する
  63:  * @param   array $x データ配列
  64:  * @return  bool TRUE:配列である/FALSE:配列でない
  65: */
  66: function is_data($x) {
  67:     $res = TRUE;
  68:     if (! is_array($x)) {
  69:         $this->error = TRUE;
  70:         $this->errmsg = '配列でない';
  71:         $res = FALSE;
  72:     } else if (count($x< 1) {
  73:         $this->error = TRUE;
  74:         $this->errmsg = '配列要素がない';
  75:         $res = FALSE;
  76:     }
  77:     return $res;
  78: }

クラス pahooStat では、配列を計算することが多い。
そこで、引数として渡された変数が配列であり、かつ要素が1つ以上あることを検査するメソッド is_data を用意した。

解説:相加平均

pahooStat.php

  80: /**
  81:  * 相加平均
  82:  * @param   array $x データ配列
  83:  * @return  double 平均/NULL
  84: */
  85: function arithmetic_mean($x) {
  86:     if (! $this->is_data($x))       return NULL;
  87: 
  88:     return array_sum($x) / count($x);
  89: }

日常、よく使う平均は「相加平均(算術平均)」と呼ばれ、次の計算式によって導かれる。
Excelの AVERAGE関数に相当する。
\[ \displaystyle \overline{x}\ =\ \frac{1}{n}\ \sum_{i=1}^{n}\ x_i \]

解説:分散

pahooStat.php

  91: /**
  92:  * 分散
  93:  * @param   array $x データ配列
  94:  * @return  double 分散
  95: */
  96: function variance($x) {
  97:     if (! $this->is_data($x))       return NULL;
  98: 
  99:     $xm = $this->arithmetic_mean($x);
 100:     $xs = 0;
 101:     $n = count($x);
 102:     $xs = 0;
 103:     for ($i = 0$i < $n$i++) {
 104:         $xs += ($x[$i- $xm* ($x[$i- $xm);
 105:     }
 106: 
 107:     return $xs / $n;
 108: }

分散は、データの散らばり度合いを表す値で、偏差(各々の値と相加平均の差)を二乗し、その相加平均を求める。
Excelの VARP関数に相当する。
\[ \displaystyle s^2\ =\ \frac{1}{n}\ \sum_{i=1}^{n}\ (x_i\ -\ \overline{x})^2 \]

解説:標準偏差

pahooStat.php

 130: /**
 131:  * 標準偏差
 132:  * @param   array $x データ配列
 133:  * @return  double 標準偏差
 134: */
 135: function standard_deviation($x) {
 136:     if (! $this->is_data($x))       return NULL;
 137: 
 138:     return sqrt($this->variance($x));
 139: }

標準偏差は、データの散らばり度合いを表す値で、分散の平方根を求める。
Excelの STDEV.P関数に相当する。
\[ \displaystyle s\ =\ \sqrt{s^2} \]

解説:共分散

pahooStat.php

 110: /**
 111:  * 共分散
 112:  * @param   array $x, $y データ配列
 113:  * @return  double 共分散
 114: */
 115: function covariance($x, $y) {
 116:     if (! $this->is_data($x))       return NULL;
 117:     if (! $this->is_data($y))       return NULL;
 118: 
 119:     $xm = $this->arithmetic_mean($x);
 120:     $ym = $this->arithmetic_mean($y);
 121:     $n = min(count($x), count($y));
 122:     $xs = 0;
 123:     for ($i = 0$i < $n$i++) {
 124:         $xs += ($x[$i- $xm* ($y[$i- $ym);
 125:     }
 126: 
 127:     return $xs / $n;
 128: }

共分散は、2種類のデータ集合の関係を示す指標で、2つのデータ集合の偏差の積の平均を求める。
Excelの COVAR関数に相当する。
\[ \displaystyle s_{xy}\ =\ \frac{1}{n}\ \sum_{i=1}^{n}\ (x_i\ -\ \overline{x})(y_i\ -\ \overline{y}) \]

解説:相関係数

pahooStat.php

 141: /**
 142:  * 相関係数
 143:  * @param   array $x, $y データ配列
 144:  * @return  double 相関係数
 145: */
 146: function correl($x, $y) {
 147:     if (! $this->is_data($x))       return NULL;
 148:     if (! $this->is_data($y))       return NULL;
 149: 
 150:     return $this->covariance($x, $y) / ($this->standard_deviation($x* $this->standard_deviation($y));
 151: }

相関係数は、2種類のデータ集合の関係を示す指標で、単位の影響を受けずに関係を示すことができる。
Excelの CORREL関数に相当する。
\[ \displaystyle r\ =\ \frac{s_{xy}}{s_x\ s_y}\ =\ \frac{\frac{1}{n}\ \sum_{i=1}^{n}\ (x_i\ -\ \overline{x})(y_i\ -\ \overline{y})}{\sqrt{\frac{1}{n}\ \sum_{i=1}^{n}\ (x_i\ -\ \overline{x})^2}\ \sqrt{\frac{1}{n}\ \sum_{i=1}^{n}\ (y_i\ -\ \overline{y})^2}} \]

解説:相関の強さ

pahooStat.php

 153: /**
 154:  * 相関の強さ
 155:  * @param   double $r 相関係数
 156:  * @return  string 相関の強さ(テキスト)
 157: */
 158: function correl_strength($r) {
 159:     $table = array(
 160: array(-0.7, '強い負の相関'),
 161: array(-0.4, '負の相関'),
 162: array(-0.2, '弱い負の相関'),
 163: array(+0.2, 'ほとんど相関がない'),
 164: array(+0.4, '弱い正の相関'),
 165: array(+0.7, '正の相関'),
 166: array(+1.1, '強い正の相関')
 167: );
 168: 
 169:     foreach ($table as $item) {
 170:         if ($r < $item[0])       return $item[1];
 171:     }
 172:     return FALSE;
 173: }

相関係数の値と、相関の強さの基準は下表の通りである。
相関係数 r 相関の強さ
-1.0 ≦ r < -0.7強い負の相関
-0.7 ≦ r < -0.4負の相関
-0.4 ≦ r < -0.2弱い負の相関
-0.2 ≦ r < +0.2ほとんど相関がない
+0.2 ≦ r < +0.4弱い正の相関
+0.4 ≦ r < +0.7正の相関
+0.7 ≦ r ≦ +1.0強い正の相関

解説:回帰直線

pahooStat.php

 175: /**
 176:  * 回帰係数、切片
 177:  * @param   array $x, $y データ配列
 178:  * @return  array(回帰係数,切片)
 179: */
 180: function slope($x, $y) {
 181:     if (! $this->is_data($x))       return NULL;
 182:     if (! $this->is_data($y))       return NULL;
 183: 
 184:     $a = $this->covariance($x, $y) / $this->variance($x);
 185:     $b = $this->arithmetic_mean($y- $a * $this->arithmetic_mean($x);
 186: 
 187:     return array($a, $b);
 188: }

2種類のデータ集合を散布図にプロットしたとき、相関があれば、直線上にデータが並ぶ。この直線のことを回帰直線と呼び、最小自乗法(最小二乗法)によって求めることができる。
Excelの SLOPE関数INTERCEPT関数に相当する。

回帰直線を \( y\ =\ ax\ +\ b \) とするとき、a, b は次の式によって求める。
\[ \displaystyle a\ =\ \frac{s_{xy}}{{s_x}^2}\ =\ \frac{\sum_{i=1}^{n}\ (x_i\ -\ \overline{x})(y_i\ -\ \overline{y})}{\sum_{i=1}^{n}\ (x_i\ -\ \overline{x})^2}
b\ =\ \overline{y}\ -\ a\overline{x} \]

解説:散布図

correlation.php

 298: /**
 299:  * jqPlot用のスクリプト:散布図
 300:  * @param   array $x, $y データ配列
 301:  * @param   string $label_x, $label_y 軸ラベル
 302:  * @param   string $name オブジェクト名
 303:  * @return  string スクリプト/FALSE
 304: */
 305: function plotScatter($x, $y, $label_x, $label_y, $name) {
 306:     // 相関係数、回帰直線
 307:     $pst = new pahooStat();
 308:     $r = $pst->correl($x, $y);              // 相関係数
 309:     if ($pst->iserror())    return FALSE;
 310:     $st = $pst->correl_strength($r);
 311:     $r = sprintf('%4.3f (%s)', $r, $st);
 312:     list($a, $b) = $pst->slope($x, $y);     // 回帰係数、切片
 313:     if ($pst->iserror())    return FALSE;
 314:     $x0 = min($x);
 315:     $y0 = $a * $x0 + $b;
 316:     $x1 = max($x);
 317:     $y1 = $a * $x1 + $b;
 318:     $pst = NULL;
 319: 
 320:     // 系列の生成
 321:     $series = '';
 322:     $n = min(count($x), count($y));
 323:     for ($i = 0$i < $n$i++) {
 324:         $series .sprintf('[%f, %f], ', $x[$i], $y[$i]);
 325:     }
 326: 
 327:     $width  = GRAPH_WIDTH;
 328:     $height = GRAPH_HEIGHT;
 329:     $js =<<< EOT
 330: <script>
 331: jQuery(function() {
 332:     jQuery.jqplot('{$name}',
 333:     [
 334:         [ {$series} ]
 335:     ],
 336:     {
 337:         // タイトル
 338:         title: {
 339:             text: '{$label_x}={$label_y}<br /><span style="font-size:small; color:blue;">相関係数 = {$r}</span>',
 340:             show: true,
 341:             fontSize: '20px',
 342:             textAlign: 'center',
 343:             textColor: 'black'
 344:         },
 345:         // 散布図
 346:         seriesDefaults: {
 347:             showLine: false,
 348:             color: 'green',
 349:             markerOptions: { size: 4 }
 350:         },
 351:         // 回帰直線
 352:         canvasOverlay: {
 353:             show: true,
 354:             objects: [
 355:             {
 356:                 line: {
 357:                     lineWidth: 3,
 358:                     color: 'blue',
 359:                     shadow: false,
 360:                     lineCap: 'round',
 361:                     start: [ {$x0}, {$y0} ],
 362:                     stop:  [ {$x1}, {$y1} ]
 363:                 }
 364:             }]
 365:         },
 366:         // 軸
 367:         axes: {
 368:             xaxis: {
 369:                 label: '{$label_x}',
 370:                 tickOptions: { formatString: '%.1f' }
 371:             },
 372:             yaxis: {
 373:                 label: '{$label_y}',
 374:                 tickOptions: { formatString: '%.1f' }
 375:             }
 376:         }
 377:     }
 378:     );
 379: });
 380: </script>
 381: <div id="{$name}" style="width:{$width}px; height:{$height}px;"></div>
 382: 
 383: EOT;
 384: 
 385:     return $js;
 386: }

配列に格納したデータを、jqPlotスクリプトへ展開し散布図とする処理は、ユーザー関数 plotScatter で行う。

回帰直線は、X軸の最小値を始点とし、同じく最大値を終点とする直線で、jqPlotプラグインの「jqplot.dateAxisRenderer.js」(グラフに描き加える線)で行う。

解説:年次推移グラフ

correlation.php

 197: /**
 198:  * jqPlot用のスクリプト:年次推移グラフ
 199:  * @param   array  $items データ配列
 200:  * @param   array  $labels ラベル配列
 201:  * @param   string $name オブジェクト名
 202:  * @param   string $title グラフ・タイトル
 203:  * @return  string スクリプト
 204: */
 205: function plotProgress($items, $labels, $name, $title) {
 206:     // 系列の生成
 207:     $series = array();
 208:     $n = count($labels);
 209:     for ($i = 1$i < $n$i++) {
 210:         $series[$i] = '';
 211:         foreach ($items[$ias $key=>$val) {
 212:             $series[$i.sprintf('[%d, %f], ', $items[0][$key], $val);
 213:         }
 214:     }
 215:     $s1 = '';
 216:     $s2 = '';
 217:     foreach ($series as $key=>$val) {
 218:         $s1 ."[ {$val} ], ";
 219:         if (max($items[$key]) > 20) {
 220:             $s2 .=<<< EOT
 221:                 {
 222:                 label: '{$labels[$key]}',
 223:                 yaxis: 'y2axis'
 224:                 },
 225: 
 226: EOT;
 227:         } else {
 228:             $s2 .=<<< EOT
 229:                 {
 230:                 label: '{$labels[$key]}'
 231:                 },
 232: 
 233: EOT;
 234:         }
 235:     }
 236: 
 237:     $width  = GRAPH_WIDTH;
 238:     $height = GRAPH_HEIGHT;
 239:     $js =<<< EOT
 240: <script>
 241: jQuery(function() {
 242:     jQuery.jqplot('{$name}',
 243:     [
 244:         {$s1}
 245:     ],
 246:     {
 247:         // タイトル
 248:         title: {
 249:             text: '{$title}',
 250:             show: true,
 251:             fontSize: '20px',
 252:             textAlign: 'center',
 253:             textColor: 'black'
 254:         },
 255:         // ラベル
 256:         series: [
 257:             {$s2}
 258:         ],
 259:         legend: {
 260:             show: true,
 261:             placement: 'inside',
 262:             location: 'ne',
 263:             renderer: $.jqplot.EnhancedLegendRenderer,
 264:             rendererOptions: { numberRows: 1 }
 265:         },
 266:         // グラフ
 267:         seriesDefaults: {
 268:             showLine: true,
 269:             rendererOptions: { smooth: false },
 270:             markerOptions: { size: 0 },
 271:         },
 272:         // 軸
 273:         axes: {
 274:             xaxis: {
 275:                 label: '{$labels[0]}',
 276:                 tickOptions: { formatString: '%d' }
 277:             },
 278:             yaxis: {
 279:                 label: '率',
 280:                 tickOptions: { formatString: '%.1f' }
 281:             },
 282:             y2axis: {
 283:                 label: '年齢',
 284:                 tickOptions: { formatString: '%d' }
 285:             }
 286:         }
 287:     }
 288:     );
 289: });
 290: </script>
 291: <div id="{$name}" style="width:{$width}px; height:{$height}px;"></div>
 292: 
 293: EOT;
 294: 
 295:     return $js;
 296: }

配列に格納したデータから、合計特殊出生率、婚姻率、離婚率、平均初婚年齢(妻)の年次推移をjqPlotスクリプトへ展開し、折れ線グラフとする処理は、ユーザー関数 plotProgress で行う。

参考サイト

(この項おわり)
header