インフルエンザ患者数のグラフを作る

(1/1)
PHPとJpGraphで人口ピラミッドを表示する」では棒グラフを扱ったが、同様にPHPと JpGraph を用い、インフルエンザなどの感染症患者数を折れ線グラフであらわすプログラムを作ってみる。
インフルエンザ患者数は日々変化しているので、国立感染症研究所が発表している患者数データを取り込むようにした。

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

インフルエンザ患者数のグラフを作る
サンプル・プログラムを実行すると、インフルエンザの定点あたり報告数(速報値)を10年分グラフ表示する。出力はPNG形式の画像データだ。

グラフ描画する感染症は、インフルエンザを含めて19種類設定できる。コマンドライン・オプション d に0~18の番号を指定する。番号と感染症名の対応は、配列変数 $ListDiseases に定義している。
たとえば "influenza.php?d=8" と指定すると、百日咳のグラフを表示する。

コマンドライン・オプション img を指定すると、imgタグの中に画像データを埋め込むための文字列を生成する。詳しくは、「PHPでTrueTypeフォントを利用する(その2)」-「解説:画像をHTMLコンテンツに埋め込む」を参照のこと。

コマンドライン・オプション file に続いてファイル名を指定すると、国立感染症研究所が発表している患者数データではなく、指定したデータファイルからデータを読み込んで表示する。データファイルの書式は後述する。

サンプル・プログラム

圧縮ファイルの内容
influenza.phpサンプル・プログラム本体
influenza.txt読み込み用サンプル・データ
influenza.php 更新履歴
バージョン 更新日 内容
2.4.0 2023/05/05 グラフにフッタを追加
2.3 2022/04/03 PHP8対応,リファラ・チェック改良
2.21 2019/10/06 http→https変更
2.2 2018/06/05 JpGraph 4.2.1対応
2.1 2016/01/29 他の疾病も描画可能に

解説:準備

グラフ描画には JpGraph を利用する。導入方法については、「PHPとJpGraphで人口ピラミッドを表示する」を参照のこと。

  39: //JpGraph(各自の環境に合わせて)
  40: require_once('jpgraph/jpgraph.php');
  41: require_once('jpgraph/jpgraph_line.php');
  42: 

解説:各種パラメータ

  43: //グラフの大きさ
  44: define('WIDTH',  600);      //幅(ピクセル)
  45: define('HEIGHT', 400);      //高さ(ピクセル)
  46: 
  47: //エラー表示用フォント・ファイル名;各自の環境に合わせて
  48: define('FONT', '../../../../common/font/ipagp.ttf');
  49: 
  50: //データファイル名
  51: define('FNAME_DATA', 'influenza.txt');
  52: 
  53: //IDWR速報データ
  54: define('URI_IDWR', 'https://www.niid.go.jp/niid/ja/data.html');
  55: 
  56: //IDWR速報データのエンコード
  57: define('IDWR_ENCODING', 'SJIS');
  58: 
  59: //感染症リスト
  60: $ListDiseases = array(
  61:  0 => 'インフルエンザ',
  62:  1 => '咽頭結膜熱',
  63:  2 => 'A群溶血性レンサ球菌咽頭炎',
  64:  3 => '感染性胃腸炎',
  65:  4 => '水痘',
  66:  5 => '手足口病',
  67:  6 => '伝染性紅斑',
  68:  7 => '突発性発しん',
  69:  8 => '百日咳',
  70:  9 => 'ヘルパンギーナ',
  71: 10 => '流行性耳下腺炎',
  72: 11 => '急性出血性結膜炎',
  73: 12 => '流行性角結膜炎',
  74: 13 => '細菌性髄膜炎',
  75: 14 => '無菌性髄膜炎',
  76: 15 => 'マイコプラズマ肺炎',
  77: 16 => 'クラミジア肺炎',
  78: 17 => '感染性胃腸炎(ロタウイルス)',
  79: 18 => 'RSウイルス(報告数)'
  80: );
  81: 
  82: //データファイル名
  83: $FnameData = 'influenza.txt';
  84: 
  85: //折れ線グラフの色(年ごとに少しずつ色を変える)
  86: $PlotColor = array(
  87:  0 => '#E5004F',
  88:  1 => '#FFA100',
  89:  2 => '#009944',
  90:  3 => '#00A0E9',
  91:  4 => '#920783',
  92:  5 => '#F39800',
  93:  6 => '#8FC31F',
  94:  7 => '#0068B7',
  95:  8 => '#E4007F',
  96:  9 => '#009E96',
  97: 10 => '#E60012',
  98: 11 => '#FFA100',
  99: 12 => '#8FC31F'
 100: );
 101: 
 102: //折れ線グラフの太さ(直近のグラフだけ太線にする)
 103: $PlotWeight = array(
 104:  0 => 1,
 105:  1 => 1,
 106:  2 => 1,
 107:  3 => 1,
 108:  4 => 1,
 109:  5 => 1,
 110:  6 => 1,
 111:  7 => 1,
 112:  8 => 1,
 113:  9 => 1,
 114: 10 => 3,
 115: 11 => 3,
 116: 12 => 3
 117: );
 118: 
 119: // サブルーチン ==============================================================

患者数の増減は折れ線グラフで描くが、読みやすいように、年によってグラフの色や太さを変えている。
色はRGB各8bitで配列変数 $PlotColor に、太さは配列変数 $PlotWeight にあらかじめ代入しておく。

解説:患者データの解析

 256: /**
 257:  * IDWR速報データのページから目的のCSVファイル名を取り出す
 258:  * @return  string CSVファイル名
 259: */
 260: function getIDWR_trend() {
 261:     static $pat = '/href\=\"(.+\-trend\.csv)/ui';
 262: 
 263:     $context = array(
 264:         'ssl' => array(
 265:             'verify_peer' => FALSE,
 266:             'verify_peer_name' => FALSE,
 267:         )
 268:     );
 269: 
 270:     $infp = fopen(URI_IDWR, 'r', FALSE, stream_context_create($context));
 271:     if ($infp == FALSEreturn FALSE;
 272: 
 273:     $fname = '';
 274:     while (! feof($infp)) {
 275:         $str = fgets($infp);
 276:         if (preg_match($pat, $str, $arr> 0) {
 277:             $fname = $arr[1];
 278:             break;
 279:         }
 280:     }
 281:     fclose($infp);
 282: 
 283:     $arr = parse_url(URI_IDWR);
 284:     $fname = 'https://' . $arr['host'. $fname;
 285: 
 286:     return $fname;
 287: }

 289: /**
 290:  * IDWR速報データを解析
 291:  * @param   string $fname 解析ファイル名
 292:  * @param   string $disease 感染症名
 293:  * @param   array  $item データを格納する配列
 294:  * @return  int データの開始年/FALSE
 295: */
 296: function parseIDWR($fname, $disease, &$items) {
 297:     $pat = array(
 298: '/' . $disease . ',/ui',
 299: '/,([0-9]+)週,/ui',
 300: '/([0-9]+)年,/ui',
 301: '/^,$/ui'
 302: );
 303: 
 304: //  ini_set('auto_detect_line_endings', TRUE);
 305:     $infp = fopen($fname, 'r');
 306:     if ($infp == FALSEreturn FALSE;
 307: 
 308:     $start = 0;
 309:     $mode = 0;
 310:     while (! feof($infp)) {
 311:         $str = mb_convert_encoding(trim(fgets($infp)), INTERNAL_ENCODING, IDWR_ENCODING);
 312:         //データ領域の始まり
 313:         if (($mode == 0&& (preg_match($pat[0], $str> 0)) {
 314:             $mode = 1;
 315:         //データ本体
 316:         } else if ($mode == 1) {
 317:             if (preg_match($pat[1], $str> 0) {
 318:             } else if (preg_match($pat[2], $str, $arr> 0) {
 319:                 //西暦年の行
 320:                 $year = $arr[1];
 321:                 if ($year < 2000)    $year +2000;
 322:                 if ($start == 0)    $start = $year;
 323:                 //データの行
 324:                 $arr = preg_split('/,/ui', $str);
 325:                 foreach ($arr as $week=>$data) {
 326:                     if (($week > 0&& ($data !'')) {
 327:                         $items[$year][$week] = $data;
 328:                     }
 329:                 }
 330:             } else {
 331:                 break;
 332:             }
 333:         }
 334:     }
 335:     fclose($infp);
 336: 
 337:     return $start;
 338: }

折れ線グラフに描くデータは、国立感染症研究所の「IDWR速報データ」の中から「疾病毎定点当たり報告数~過去10年間との比較~」を利用する。

まず、ユーザー関数 getIDWR_trend により、読み込むべきCSVファイル名を取得する。CSVファイル名は毎週変化するためである。

次に、ユーザー関数 parseIDWR により、CSVファイルを解析し、データの開始年とデータ本体を取得する。
ターゲットのCSVファイルはシフトJISでエンコードされているが、改行コードが CR であるため、 fgets  が正常に機能しない。そこで、事前に ini_set に 'auto_detect_line_endings' を指定することで、CR を改行として認識できるようにしておく。

解説:グラフを描く

 340: /**
 341:  * 感染症グラフ描画
 342:  * @param   array  $item データ
 343:  * @param   int    $start 描画開始年
 344:  * @param   string $disease 感染症名
 345:  * @return  object graphオブジェクト
 346: */
 347: function drawGraph($items, $start, $disease) {
 348:     global $PlotColor, $PlotWeight;
 349: 
 350:     $plotdata = array();
 351: 
 352:     // 月(横軸)を作成
 353:     for ($i = 1$i <53$i++) {
 354:         $data_x[$i] = (int)($i / 4.5 + 1);
 355:     }
 356: 
 357:     //グラフ描画処理開始
 358:     $graph = new Graph(WIDTH, HEIGHT, 'auto');
 359:     $graph->SetScale('textlin');
 360:     $graph->SetBackgroundGradient('#FFFFCC','#FFFFCC', GRAD_HOR, BGRAD_MARGIN);
 361: 
 362:     //タイトル、凡例などの設定
 363:     $graph->img->SetImgFormat('png');
 364:     $graph->img->SetMargin(60, 100, 20, 40);
 365:     $graph->title->SetFont(FF_PGOTHIC, FS_NORMAL, 14);
 366:     $graph->title->Set($disease . ':定点あたり報告数(速報値)');
 367:     $graph->xaxis->title->SetFont(FF_PGOTHIC);
 368:     $graph->xaxis->SetTitle('月','center');
 369:     $graph->xaxis->SetTickLabels($data_x);
 370:     $graph->xaxis->SetTextTickInterval(5);
 371:     $graph->xaxis->SetTextLabelInterval(1);
 372:     //フッタを設定する.
 373:     $graph->footer->right->SetFont(FF_PGOTHIC, FS_NORMAL, 10);
 374:     $graph->footer->right->Set('produced by JpGraph');
 375: 
 376:     $graph->yaxis->title->SetFont(FF_PGOTHIC);
 377:     $graph->yaxis->title->Set('報告数');
 378:     $graph->yaxis->SetTitleMargin(40);
 379: 
 380:     //折れ線グラフ
 381:     foreach ($items as $year=>$data) {
 382:         unset($data_y);
 383:         foreach ($data as $week=>$val) {
 384:             $data_y[$week - 1] = $items[$year][$week];
 385:         }
 386: 
 387:         //折れ線グラフ作成
 388:         $plotdata[$year] = new LinePlot($data_y);
 389:         $plotdata[$year]->SetLegend($year . '年');                  //凡例
 390:         //グラフを結合する
 391:         $graph->Add($plotdata[$year]);
 392:         $plotdata[$year]->SetColor($PlotColor[$year - $start]);     //色
 393:         $plotdata[$year]->SetWeight($PlotWeight[$year - $start]);   //太さ
 394:     }
 395: 
 396:     //凡例
 397:     $graph->legend->SetFont(FF_PGOTHIC);                    //フォント
 398:     $graph->legend->SetColumns(1);                          //表示列数
 399:     $graph->legend->SetPos(0.01, 0.1, 'right', 'top');      //位置
 400: 
 401:     return $graph;
 402: }

グラフの描画は JpGraph の機能を利用している。

まず、1年分のデータを配列 $data_y に代入し、折れ線グラフを1本描く。
この作業を年数分繰り返してゆく。

解説:画面出力

 433: //グラフを表示
 434: $graph = drawGraph($items, $start, $disease);
 435: if ($mode == 1) {
 436:     header('Content-type: image/png');
 437:     $graph->Stroke();
 438: else {
 439:     $im = $graph->Stroke(_IMG_HANDLER); 
 440:     ob_start();
 441:     ImagePNG($im); 
 442:     imagedestroy($im);
 443:     $res = base64_encode(ob_get_clean());
 444:     echo $res;
 445: }
 446: 

画面への出力も JpGraph の機能を利用している。

コマンドライン・オプション img が指定された場合は、出力データをimgタグの中に画像データを埋め込むための文字列を生成する。詳しくは、「PHPでTrueTypeフォントを利用する(その2)」-「解説:画像をHTMLコンテンツに埋め込む」を参照のこと。

解説:データを配列に読み込む

 226: /**
 227:  * データを配列に読み込む
 228:  * @param   string $fname   読み込むデータファイル名
 229:  * @param   string $disease 感染症名を格納
 230:  * @param   array  $rec     データを格納する配列
 231:  * @return  int データの開始年/FALSE
 232: */
 233: function readData($fname, &$disease, &$rec) {
 234:     $infp = @fopen($fname, 'r');
 235:     if ($infp == FALSEreturn FALSE;
 236: 
 237:     $start = 0;
 238:     $disease = trim(fgets($infp));
 239:     while (($arr = fgetcsv($infp, 1000, ',')) !FALSE) {
 240:         foreach ($arr as $key=>$val) {
 241:             if ($key == 0) {
 242:                 $year = $val;
 243:                 if ($start == 0)    $start = $year;
 244:                 $week = 1;
 245:             } else {
 246:                 $rec[$year][$week] = $val;
 247:                 $week++;
 248:             }
 249:         }
 250:     }
 251:     fclose($infp);
 252: 
 253:     return $start;
 254: }

コマンドライン・オプション file に続いてファイル名を指定すると、国立感染症研究所が発表している患者数データではなく、指定したデータファイルからデータを読み込んで表示する。

あらかじめ、次のような書式のデータファイルを用意しておく。
1行目は感染症名。2行目以降が1年分のデータで、カンマ区切り。1カラム目は西暦年である。
インフルエンザ
1999,8.86,20.3,33.56,33.86,27.16,22.67,20.95,20.96,19.2,14.95,11.71,5.47,,1.5,
1.22,0.9,0.59,0.26,0.27,0.17,0.13,0.1,0.08,0.06,0.06,0.05,0.05,0.05,0.04,0.04,
0.03,0.02,0.01,0.02,0.03,0.03,0.02,0.02,0.03,0.04,0.05,0.07,0.08,0.09,0.1,0.
17,0.21,0.35,0.74,1.53,3.22,3.21,
2000,6.15,10.24,23.32,34.26,36.16,24.7,13.8,8.36,4.62,2.41,1.2,0.51,0.26,0.13,
0.1,0.07,0.05,0.03,0.04,0.03,0.03,0.03,0.03,0.02,0.02,0.02,0.02,0.01,0.01,
0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.02,0.02,0.02,0.03,0.03,0.04,
0.05,0.06,0.08,0.15,0.23,0.27,0.32,0.36,
ファウンロード・ファイルには、サンプルとしてデータファイル "influeza.txt" を同梱してある。

データを読み込むユーザー関数 readData:blue の流れは、「PHPでCSVファイルを読み込む」で解説した通りである。

参考サイト

(この項おわり)
header