多次元配列、連想配列

列が複数あるときの集計に使える
2次元配列のイメージ
2次元配列のイメージ
Excelで複数教科の点数集計するのと同じことを、2次元配列を用いて処理することができる。PHPの連想配列、Pythonのリストと呼ばれるデータ構造を使って点数集計プログラムを作ってみる。

サンプル・プログラム

2次元配列

下表のような、出席番号1~12の4教科(国語、算数、理科、社会)の得点表があるとする。
番号国語算数理科社会
192987485
285686265
369875387
494455783
581598388
676426266
796758155
8100987347
974758680
1079947176
11764410067
1292835369
これを配列で扱う場合、添字が番号であることは前回と同じだが、2つ目の添字を用意し、教科の識別に使う。0=国語、1=算数、2=理科、3=社会、のように番号を割り当てることで、次のような配列で表現することができる。
$a[1][0] = 92;
$a[1][1] = 98;
$a[1][2] = 74;
$a[1][3] = 85;
添字が2つあることから、このような配列を2次元配列と呼ぶ。
Excelとの対比でいえば、1次元目は行、2次元目は列に対応する。

一般論として、添字の組み合わせ(次元数)は自在に増やすことができ、このような配列を多次元配列と呼ぶ。

PHPによる連想配列

教科の識別(添字)を自然数で書くと、別に教科番号と教科名の識別表を用意しなければならず、プログラムの可読性が低下する。
PHPは、添字に文字列を使うことができる連想配列という仕組みをもっている。そこで、連想配列を使った2次元配列のプログラム "dt03-12-01.php" を書いてみる。

  10: //多次元配列+連想配列
  11: $a[1]  = array('国語'=>92,  '算数'=>98, '理科'=>74,  '社会'=>85);
  12: $a[2]  = array('国語'=>85,  '算数'=>68, '理科'=>62,  '社会'=>65);
  13: $a[3]  = array('国語'=>69,  '算数'=>87, '理科'=>53,  '社会'=>87);
  14: $a[4]  = array('国語'=>94,  '算数'=>45, '理科'=>57,  '社会'=>83);
  15: $a[5]  = array('国語'=>81,  '算数'=>59, '理科'=>83,  '社会'=>88);
  16: $a[6]  = array('国語'=>76,  '算数'=>42, '理科'=>62,  '社会'=>66);
  17: $a[7]  = array('国語'=>96,  '算数'=>75, '理科'=>81,  '社会'=>61);
  18: $a[8]  = array('国語'=>100, '算数'=>98, '理科'=>73,  '社会'=>47);
  19: $a[9]  = array('国語'=>74,  '算数'=>75, '理科'=>86,  '社会'=>80);
  20: $a[10] = array('国語'=>79,  '算数'=>94, '理科'=>71,  '社会'=>76);
  21: $a[11] = array('国語'=>76,  '算数'=>44, '理科'=>100, '社会'=>67);
  22: $a[12] = array('国語'=>92,  '算数'=>83, '理科'=>53,  '社会'=>69);
  23: 
  24: //合計
  25: $sums['国語'] = array_sum(array_column($a, '国語'));
  26: $sums['算数'] = array_sum(array_column($a, '算数'));
  27: $sums['理科'] = array_sum(array_column($a, '理科'));
  28: $sums['社会'] = array_sum(array_column($a, '社会'));
  29: 
  30: //平均
  31: foreach ($sums as $key=>$val) {
  32:     $avgs[$key] = $sums[$key] / count($a);
  33: }
  34: 
  35: //出力
  36: printf("\n   番号  国語  算数  理科  社会\n");
  37: printf("----------------------------------\n");
  38: foreach ($a as $key=>$b) {
  39:     printf("    %2d", $key);            //出席番号
  40:     foreach ($b as $val) {
  41:         printf("  %4d", $val);          //点数
  42:     }
  43:     printf("\n");
  44: }
  45: printf("----------------------------------\n");
  46: printf("  合計");
  47: foreach ($sums as $val) {
  48:     printf("  %4d", $val);              //合計
  49: }
  50: printf("\n  平均");
  51: foreach ($avgs as $val) {
  52:     printf("  %4.1f", $val);            //平均
  53: }
  54: printf("\n");

多次元連想配列
PHPプログラムの実行結果は左図の通りである。

ヒアドキュメント

連想配列を利用することで、プログラムの可読性は高まった。しかし、いちいち教科名を記述するのは面倒である。
そこで、Excelの表をCSV形式(1行目:ラベル,列:タブ区切り,行:改行区切り)として、そのままヒアドキュメントとしてプログラムに取り込み、配列処理するプログラム "dt03-12-02.php" を書いてみる

  11: //データ(1行目:ラベル,列:タブ区切り,行:改行区切り)
  12: $data =<<< EOT
  13: 番号    国語    算数    理科    社会
  14: 1   92  98  74  85
  15: 2   85  68  62  65
  16: 3   69  87  53  87
  17: 4   94  45  57  83
  18: 5   81  59  83  88
  19: 6   76  42  62  66
  20: 7   96  75  81  61
  21: 8   100 98  73  47
  22: 9   74  75  86  80
  23: 10  79  94  71  76
  24: 11  76  44  100 67
  25: 12  92  83  53  69
  26: 
  27: EOT;
  28: 
  29: /**
  30:  * CSV形式テキストを読み込んで、多次元連想配列に展開する
  31:  * @param   string $data CSV形式テキスト(列:タブ,行:改行)
  32:  * @param   array  $labels ラベルを格納(1行目のテキスト)
  33:  * @param   array  $items  要素を格納(多次元連想配列)
  34:  * @return  int    要素数
  35: */
  36: function parseArray($data, &$labels, &$items) {
  37:     $rec = preg_split("/\n/ui", $data);     //行に分解
  38:     $cnt = 0;
  39:     foreach ($rec as $str) {
  40:         $str = trim($str);
  41:         if (mb_strlen($str) == 0)   continue;
  42:         $col = preg_split("/\t/ui", $str);      //列に分解
  43:         if ($cnt == 0) {                        //ラベルを格納
  44:             foreach ($col as $key=>$val) {
  45:                 $labels[$key] = trim($val);
  46:             }
  47:         } else {                                //要素を格納
  48:             foreach ($col as $key=>$val) {
  49:                 $items[$cnt][$labels[$key]] = trim($val);
  50:             }
  51:         }
  52:         $cnt++;
  53:     }
  54:     return $cnt;
  55: }
  56: 
  57: // メイン・プログラム ======================================================
  58: //配列へ読み込み
  59: $cnt = parseArray($data, $labels, $items);
  60: 
  61: //合計・平均
  62: for ($i = 1$i < count($labels); $i++) {
  63:     $key = $labels[$i];
  64:     $sum[$key] = array_sum(array_column($items, $key));
  65:     $avg[$key] = $sum[$key] / count($items);
  66: }
  67: 
  68: //出力
  69: printf("\n  ");
  70: foreach ($labels as $key) {
  71:     printf("%s  ", $key);
  72: }
  73: printf("\n----------------------------------\n");
  74: foreach ($items as $key=>$b) {
  75:     foreach ($b as $val) {
  76:         printf("  %4d", $val);          //番号・点数
  77:     }
  78:     printf("\n");
  79: }
  80: printf("----------------------------------\n");
  81: printf("  合計");
  82: foreach ($sum as $val) {
  83:     printf("  %4d", $val);              //合計
  84: }
  85: printf("\n  平均");
  86: foreach ($avg as $val) {
  87:     printf("  %4.1f", $val);            //平均
  88: }
  89: printf("\n");

プログラム冒頭にヒアドキュメントでCSV形式データを書き込んでいる。
これを多次元連想配列に展開するユーザー関数 parseArray を用意したためプログラムは長くなってしまったが、この関数は再利用が利く。また、ヒアドキュメントではなくても、同じ書式であれば、CSVデータを外部ファイルにもたせて読み込むことも可能である。
実際、Excelで下書きをした点数表を、そのままコピー&ペースとしているだけなので、作業時間へ減らせるし、転記ミスも無くなる。大量のデータを扱うシステムでは、いかにしてオリジナルのデータを手作業で加工することなくコンピュータへ渡せるかという点も重要になってくる。
items のデータ実体は下記の通りである。
最初のプログラムのように、連想配列の添字として教科名が入っていることが確認できる。
Array(
 [1]=>Array([番号]=>1,[国語]=>92,[算数]=>98,[理科]=>74,[社会]=>85)
 [2]=>Array([番号]=>2,[国語]=>85,[算数]=>68,[理科]=>62,[社会]=>65)
 [3]=>Array([番号]=>3,[国語]=>69,[算数]=>87,[理科]=>53,[社会]=>87)
 [4]=>Array([番号]=>4,[国語]=>94,[算数]=>45,[理科]=>57,[社会]=>83)
 [5]=>Array([番号]=>5,[国語]=>81,[算数]=>59,[理科]=>83,[社会]=>88)
 [6]=>Array([番号]=>6,[国語]=>76,[算数]=>42,[理科]=>62,[社会]=>66)
 [7]=>Array([番号]=>7,[国語]=>96,[算数]=>75,[理科]=>81,[社会]=>61)
 [8]=>Array([番号]=>8,[国語]=>100,[算数]=>98,[理科]=>73,[社会]=>47)
 [9]=>Array([番号]=>9,[国語]=>74,[算数]=>75,[理科]=>86,[社会]=>80)
 [10]=>Array([番号]=>10,[国語]=>79,[算数]=>94,[理科]=>71,[社会]=>76)
 [11]=>Array([番号]=>11,[国語]=>76,[算数]=>44,[理科]=>100,[社会]=>67)
 [12]=>Array([番号]=>12,[国語]=>92,[算数]=>83,[理科]=>53,[社会]=>69)
)

Pythonによる連想配列

Pythonもヒアドキュメントを利用できる。PHPのプログラムをそのまま移植したのが "dt03-12-03.py" である。

   9: import sys
  10: from statistics import mean
  11: 
  12: # データ(1行目:ラベル,列:タブ区切り,行:改行区切り)
  13: data = """
  14: 番号  国語    算数    理科    社会
  15: 1   92  98  74  85
  16: 2   85  68  62  65
  17: 3   69  87  53  87
  18: 4   94  45  57  83
  19: 5   81  59  83  88
  20: 6   76  42  62  66
  21: 7   96  75  81  61
  22: 8   100 98  73  47
  23: 9   74  75  86  80
  24: 10  79  94  71  76
  25: 11  76  44  100 67
  26: 12  92  83  53  69
  27: """.strip()
  28: 
  29: """
  30:  * CSV形式テキストを読み込んで、リストに展開する
  31:  * @param   string data CSV形式テキスト(列:タブ,行:改行)
  32:  * @param   list   labels ラベルを格納(1行目のテキスト)
  33:  * @param   list   items  要素を格納するリスト
  34:  * @return  int    要素数
  35: """
  36: def parseList(data, labels, items):
  37:     rec = data.split("\n")                              #行に分解
  38:     cnt = 0
  39:     for str in rec:
  40:         str = str.strip()
  41:         col = str.split("\t")                           #列に分解
  42:         if cnt == 0:                                    #ラベルを格納
  43:             for key, val in enumerate(col):
  44:                 labels.append(val.strip())
  45:         else:                                           #要素を格納
  46:             item = dict()                               #辞書を用意
  47:             for key, val in enumerate(col):
  48:                 try:                                    #データ属性を判定
  49:                     item[labels[key]] = int(val)
  50:                 except ValueError:
  51:                     try:
  52:                         item[labels[key]] = float(val)
  53:                     except ValueError:
  54:                         item[labels[key]] = val
  55:             items.append(item)                          #リストに加える
  56:         cnt = cnt + 1
  57:     return(cnt)
  58: 
  59: # メイン・プログラム ======================================================
  60: # 配列へ読み込み
  61: labels = list()                                     #リストを用意
  62: items  = list()
  63: cnt = parseList(data, labels, items)
  64: 
  65: # 合計・平均
  66: sums = dict()                                           #辞書を用意
  67: avgs = dict()
  68: for key in labels[1:]:
  69:     sums[key] = sum([item.get(keyfor item in items])
  70:     avgs[key] = mean([item.get(keyfor item in items])
  71: 
  72: # 出力
  73: sys.stdout.write("\n  ")
  74: for key in labels:
  75:     sys.stdout.write("{}  ".format(key))
  76: sys.stdout.write("\n----------------------------------\n")
  77: for b in items:
  78:     for val in b.values():
  79:         sys.stdout.write("  {0:4d}".format(val))    #番号・点数
  80:     sys.stdout.write("\n")
  81: sys.stdout.write("----------------------------------\n")
  82: sys.stdout.write("  合計")
  83: for val in sums.values():
  84:     sys.stdout.write("  {0:4d}".format(val))        #合計
  85: sys.stdout.write("\n  平均")
  86: for val in avgs.values():
  87:     sys.stdout.write("  {0:4.1f}".format(val))      #平均
  88: sys.stdout.write("\n")

Pythonの辞書のキーは文字列を利用できるので、PHPの連想配列と同じ役割を果たす。
一方で、行方向はキーが不要(行番号があるだけ)なので、リストというデータ構造を用いることにする。詳しいことは「リスト」をご覧いただきたい。
このようにPythonでは、1次元目はリスト、2次元目は辞書という、複合型の多次元配列を構築することが可能である。

items のデータ実体は下記の通りである。
[...] は辞書を、 {...} はリストを意味する。
[
 {'番号': 1, '国語': 92, '算数': 98, '理科': 74, '社会': 85},
 {'番号': 2, '国語': 85, '算数': 68, '理科': 62, '社会': 65},
 {'番号': 3, '国語': 69, '算数': 87, '理科': 53, '社会': 87},
 {'番号': 4, '国語': 94, '算数': 45, '理科': 57, '社会': 83},
 {'番号': 5, '国語': 81, '算数': 59, '理科': 83, '社会': 88},
 {'番号': 6, '国語': 76, '算数': 42, '理科': 62, '社会': 66},
 {'番号': 7, '国語': 96, '算数': 75, '理科': 81, '社会': 61},
 {'番号': 8, '国語': 100, '算数': 98, '理科': 73, '社会': 47},
 {'番号': 9, '国語': 74, '算数': 75, '理科': 86, '社会': 80},
 {'番号': 10, '国語': 79, '算数': 94, '理科': 71, '社会': 76},
 {'番号': 11, '国語': 76, '算数': 44, '理科': 100, '社会': 67},
 {'番号': 12, '国語': 92, '算数': 83, '理科': 53, '社会': 69}
]

C++による連想配列

C++は多様なデータ構造を備えている。
PHPプログラム "dt03-12-02.php" を移植するにあたり、1次元目(行方向)は動的配列クラス vector を、2次元目(列方向)は連想配列クラス map を組み合わせた多次元配列をデータ構造にしたプログラムが "dt03-12-04.cpp" である。

  20: //データ(1行目:ラベル,列:タブ区切り,行:改行区切り)
  21: char data[] = R"(
  22: 番号    国語    算数    理科    社会
  23: 1   92  98  74  85
  24: 2   85  68  62  65
  25: 3   69  87  53  87
  26: 4   94  45  57  83
  27: 5   81  59  83  88
  28: 6   76  42  62  66
  29: 7   96  75  81  61
  30: 8   100 98  73  47
  31: 9   74  75  86  80
  32: 10  79  94  71  76
  33: 11  76  44  100 67
  34: 12  92  83  53  69
  35: )";

C++ 11 から raw string literal と呼ぶヒアドキュメントの仕組みが導入された。これを使うことにする。

  37: /**
  38:  * 文字列をデリミタによって分解する
  39:  * @param   string input    文字列
  40:  * @param   char delimiter  デリミタ
  41:  * @return  vector<string>  分解後の部分文字列
  42: **/
  43: vector<string> split(const string &input, char delimiter) {
  44:     istringstream stream(input);
  45:     string field;
  46:     vector<string> result;
  47:     while (getline(stream, field, delimiter)) {
  48:         result.push_back(field);
  49:     }
  50:     return result;
  51: }
  52: 
  53: /**
  54:  * CSV形式テキストを読み込んで、2次元連想配列に展開する
  55:  * @param   char        *data  CSV形式テキスト(列:タブ,行:改行)
  56:  * @param   vector      labels ラベルを格納(1行目のテキスト)
  57:  * @param   vector<map> items  要素を格納する配列
  58:  * @return  int    要素数
  59: **/
  60: int parseList(char *data, vector<string> &labels, vector<map<string,int>> &items) {
  61:     map<string,int> *item;
  62:     int cnt = 0;
  63:     for (string &rec : split(data, '\n')) {             //行に分解
  64:         if (rec.size() > 0) {
  65:             if (cnt == 0) {                             //ラベルを格納
  66:                 for (string &col : split(rec, '\t')) {      //列に分解
  67:                     labels.push_back(col);
  68:                 }
  69:             } else {                                        //要素を格納
  70:                 item = new map<string,int>;
  71:                 int i = 0;
  72:                 for (string &col : split(rec, '\t')) {      //列に分解
  73:                     item->insert(make_pair(labels[i], stoi(col)));
  74:                     i++;
  75:                 }
  76:                 items.push_back(*item);                 //リストに加える
  77:             }
  78:             cnt = cnt + 1;
  79:         }
  80:     }
  81:     return(cnt - 1);                                        //ラベル行を除く
  82: }

文字列をデリミタ(ここでは改行とタブ)で部分文字列に分解する関数 split を用意した。分解した部分文字列は、動的配列クラス vector として返す。

関数 parseList によってデータを分解し、列方向の要素(2次元目)はクラス map に、このmapを行方向(1次元目)に動的配列クラス vector を使って格納してゆく。
map<string,int> は連想配列のキーが文字列(番号,国語,算数‥‥)であることを、値が整数(点数)であることを意味する。
また、vector<map<string,int>> は、2次元目が map<string,int> で、1次元目が vector であることを意味するデータ構造である。

  85: int main() {
  86:     vector<string> labels;
  87:     vector<map<string,int>> items;
  88: 
  89:     //データ読み込み
  90:     int cnt = parseList(data, labels, items);
  91: 
  92:     map<string,int>   sums;
  93:     map<string,float> avgs;
  94: 
  95:     //合計計算
  96:     for (int i = 0i < cnti++) {
  97:         for (string key : labels) {
  98:             if (key !"番号") {
  99:                 sums[key+items[i][key];
 100:             }
 101:         }
 102:     }
 103:     //平均計算
 104:     for (string key : labels) {
 105:         avgs[key] = (float)sums[key] / cnt;
 106:     }
 107: 
 108:     //出力
 109:     cout << endl << "  ";
 110:     for (string a : labels) {
 111:         cout << a << "  ";
 112:     }
 113:     cout << endl << "----------------------------------" << endl;
 114:     for (int i = 0i < cnti++) {
 115:         for (string key : labels) {
 116:             cout << "  " << setw(4<< items[i][key]; //番号・点数
 117:         }
 118:         cout << endl;
 119:     }
 120:     cout << "----------------------------------" << endl;
 121:     cout << "  合計";
 122:     for (string key : labels) {
 123:         if (key !"番号") {
 124:             cout << "  " << setw(4<< sums[key];
 125:         }
 126:     }
 127:     cout << endl << "  平均";
 128:     for (string key : labels) {
 129:         if (key !"番号") {
 130:             cout << "  " << fixed << setprecision(1<< avgs[key];
 131:         }
 132:     }
 133:     cout << endl;
 134: 
 135:     return (0);
 136: }

メイン・プログラムは、PHPプログラム "dt03-12-02.php" を忠実に移植した。
(この項おわり)
header