多次元配列、連想配列

列が複数あるときの集計に使える
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" を書いてみる。

0010: //多次元配列+連想配列
0011: $a[1]  = array('国語'=>92,  '算数'=>98, '理科'=>74,  '社会'=>85);
0012: $a[2]  = array('国語'=>85,  '算数'=>68, '理科'=>62,  '社会'=>65);
0013: $a[3]  = array('国語'=>69,  '算数'=>87, '理科'=>53,  '社会'=>87);
0014: $a[4]  = array('国語'=>94,  '算数'=>45, '理科'=>57,  '社会'=>83);
0015: $a[5]  = array('国語'=>81,  '算数'=>59, '理科'=>83,  '社会'=>88);
0016: $a[6]  = array('国語'=>76,  '算数'=>42, '理科'=>62,  '社会'=>66);
0017: $a[7]  = array('国語'=>96,  '算数'=>75, '理科'=>81,  '社会'=>61);
0018: $a[8]  = array('国語'=>100, '算数'=>98, '理科'=>73,  '社会'=>47);
0019: $a[9]  = array('国語'=>74,  '算数'=>75, '理科'=>86,  '社会'=>80);
0020: $a[10] = array('国語'=>79,  '算数'=>94, '理科'=>71,  '社会'=>76);
0021: $a[11] = array('国語'=>76,  '算数'=>44, '理科'=>100, '社会'=>67);
0022: $a[12] = array('国語'=>92,  '算数'=>83, '理科'=>53,  '社会'=>69);
0023: 
0024: //合計
0025: $sums['国語'] = array_sum(array_column($a, '国語'));
0026: $sums['算数'] = array_sum(array_column($a, '算数'));
0027: $sums['理科'] = array_sum(array_column($a, '理科'));
0028: $sums['社会'] = array_sum(array_column($a, '社会'));
0029: 
0030: //平均
0031: foreach ($sums as $key=>$val) {
0032:     $avgs[$key] = $sums[$key] / count($a);
0033: }
0034: 
0035: //出力
0036: printf("\n   番号  国語  算数  理科  社会\n");
0037: printf("----------------------------------\n");
0038: foreach ($a as $key=>$b) {
0039:     printf("    %2d", $key);          //出席番号
0040:     foreach ($b as $val) {
0041:         printf("  %4d", $val);            //点数
0042:     }
0043:     printf("\n");
0044: }
0045: printf("----------------------------------\n");
0046: printf("  合計");
0047: foreach ($sums as $val) {
0048:     printf("  %4d", $val);                //合計
0049: }
0050: printf("\n  平均");
0051: foreach ($avgs as $val) {
0052:     printf("  %4.1f", $val);            //平均
0053: }
0054: printf("\n");

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

ヒアドキュメント

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

0011: //データ(1行目:ラベル,列:タブ区切り,行:改行区切り)
0012: $data =<<< EOT
0013: 番号    国語    算数    理科    社会
0014: 1    92    98    74    85
0015: 2    85    68    62    65
0016: 3    69    87    53    87
0017: 4    94    45    57    83
0018: 5    81    59    83    88
0019: 6    76    42    62    66
0020: 7    96    75    81    61
0021: 8    100    98    73    47
0022: 9    74    75    86    80
0023: 10    79    94    71    76
0024: 11    76    44    100    67
0025: 12    92    83    53    69
0026: 
0027: EOT;
0028: 
0029: /**
0030:  * CSV形式テキストを読み込んで、多次元連想配列に展開する
0031:  * @param string $data CSV形式テキスト(列:タブ,行:改行)
0032:  * @param array  $labels ラベルを格納(1行目のテキスト)
0033:  * @param array  $items  要素を格納(多次元連想配列)
0034:  * @return int    要素数
0035: */
0036: function parseArray($data, &$labels, &$items) {
0037:     $rec = preg_split("/\n/ui", $data);       //行に分解
0038:     $cnt = 0;
0039:     foreach ($rec as $str) {
0040:         $str = trim($str);
0041:         if (mb_strlen($str) == 0)   continue;
0042:         $col = preg_split("/\t/ui", $str);        //列に分解
0043:         if ($cnt == 0) {                     //ラベルを格納
0044:             foreach ($col as $key=>$val) {
0045:                 $labels[$key] = trim($val);
0046:             }
0047:         } else {                              //要素を格納
0048:             foreach ($col as $key=>$val) {
0049:                 $items[$cnt][$labels[$key]] = trim($val);
0050:             }
0051:         }
0052:         $cnt++;
0053:     }
0054:     return $cnt;
0055: }
0056: 
0057: // メイン・プログラム ======================================================
0058: //配列へ読み込み
0059: $cnt = parseArray($data$labels$items);
0060: 
0061: //合計・平均
0062: for ($i = 1; $i < count($labels); $i++) {
0063:     $key = $labels[$i];
0064:     $sum[$key] = array_sum(array_column($items$key));
0065:     $avg[$key] = $sum[$key] / count($items);
0066: }
0067: 
0068: //出力
0069: printf("\n  ");
0070: foreach ($labels as $key) {
0071:     printf("%s  ", $key);
0072: }
0073: printf("\n----------------------------------\n");
0074: foreach ($items as $key=>$b) {
0075:     foreach ($b as $val) {
0076:         printf("  %4d", $val);            //番号・点数
0077:     }
0078:     printf("\n");
0079: }
0080: printf("----------------------------------\n");
0081: printf("  合計");
0082: foreach ($sum as $val) {
0083:     printf("  %4d", $val);                //合計
0084: }
0085: printf("\n  平均");
0086: foreach ($avg as $val) {
0087:     printf("  %4.1f", $val);            //平均
0088: }
0089: 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" である。

0009: import sys
0010: from statistics import mean
0011: 
0012: # データ(1行目:ラベル,列:タブ区切り,行:改行区切り)
0013: data = """
0014: 番号    国語    算数    理科    社会
0015: 1    92    98    74    85
0016: 2    85    68    62    65
0017: 3    69    87    53    87
0018: 4    94    45    57    83
0019: 5    81    59    83    88
0020: 6    76    42    62    66
0021: 7    96    75    81    61
0022: 8    100    98    73    47
0023: 9    74    75    86    80
0024: 10    79    94    71    76
0025: 11    76    44    100    67
0026: 12    92    83    53    69
0027: 
""".strip()
0028: 
0029: """
0030:  * CSV形式テキストを読み込んで、リストに展開する
0031:  * @param string data CSV形式テキスト(列:タブ,行:改行)
0032:  * @param list   labels ラベルを格納(1行目のテキスト)
0033:  * @param list   items  要素を格納するリスト
0034:  * @return int    要素数
0035: 
"""
0036: def parseList(datalabelsitems):
0037:     rec = data.split("\n")                              #行に分解
0038:     cnt = 0
0039:     for str in rec:
0040:         str = str.strip()
0041:         col = str.split("\t")                           #列に分解
0042:         if cnt == 0:                                 #ラベルを格納
0043:             for keyval in enumerate(col):
0044:                 labels.append(val.strip())
0045:         else:                                            #要素を格納
0046:             item = dict()                               #辞書を用意
0047:             for keyval in enumerate(col):
0048:                 try:                                 #データ属性を判定
0049:                     item[labels[key]] = int(val)
0050:                 except ValueError:
0051:                     try:
0052:                         item[labels[key]] = float(val)
0053:                     except ValueError:
0054:                         item[labels[key]] = val
0055:             items.append(item)                           #リストに加える
0056:         cnt = cnt + 1
0057:     return(cnt)
0058: 
0059: # メイン・プログラム ======================================================
0060: # 配列へ読み込み
0061: labels = list()                                     #リストを用意
0062: items  = list()
0063: cnt = parseList(datalabelsitems)
0064: 
0065: # 合計・平均
0066: sums = dict()                                           #辞書を用意
0067: avgs = dict()
0068: for key in labels[1:]:
0069:     sums[key] = sum([item.get(keyfor item in items])
0070:     avgs[key] = mean([item.get(keyfor item in items])
0071: 
0072: # 出力
0073: sys.stdout.write("\n  ")
0074: for key in labels:
0075:     sys.stdout.write("{}  ".format(key))
0076: sys.stdout.write("\n----------------------------------\n")
0077: for b in items:
0078:     for val in b.values():
0079:         sys.stdout.write("  {0:4d}".format(val))    #番号・点数
0080:     sys.stdout.write("\n")
0081: sys.stdout.write("----------------------------------\n")
0082: sys.stdout.write("  合計")
0083: for val in sums.values():
0084:     sys.stdout.write("  {0:4d}".format(val))        #合計
0085: sys.stdout.write("\n  平均")
0086: for val in avgs.values():
0087:     sys.stdout.write("  {0:4.1f}".format(val))        #平均
0088: 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" である。

0020: //データ(1行目:ラベル,列:タブ区切り,行:改行区切り)
0021: char data[] = R"(
0022: 番号    国語    算数    理科    社会
0023: 1    92    98    74    85
0024: 2    85    68    62    65
0025: 3    69    87    53    87
0026: 4    94    45    57    83
0027: 5    81    59    83    88
0028: 6    76    42    62    66
0029: 7    96    75    81    61
0030: 8    100    98    73    47
0031: 9    74    75    86    80
0032: 10    79    94    71    76
0033: 11    76    44    100    67
0034: 12    92    83    53    69
0035: )
";

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

0037: /**
0038:  * 文字列をデリミタによって分解する
0039:  * @param string input    文字列
0040:  * @param char delimiter  デリミタ
0041:  * @return vector<string>  分解後の部分文字列
0042: **/
0043: vector<stringsplit(const string &inputchar delimiter) {
0044:     istringstream stream(input);
0045:     string field;
0046:     vector<stringresult;
0047:     while (getline(streamfielddelimiter)) {
0048:         result.push_back(field);
0049:     }
0050:     return result;
0051: }
0052: 
0053: /**
0054:  * CSV形式テキストを読み込んで、2次元連想配列に展開する
0055:  * @param char        *data  CSV形式テキスト(列:タブ,行:改行)
0056:  * @param vector      labels ラベルを格納(1行目のテキスト)
0057:  * @param vector<map> items  要素を格納する配列
0058:  * @return int    要素数
0059: **/
0060: int parseList(char *datavector<string> &labelsvector<map<string,int>> &items) {
0061:     map<string,int> *item;
0062:     int cnt = 0;
0063:     for (string &rec : split(data, '\n')) {              //行に分解
0064:         if (rec.size() > 0) {
0065:             if (cnt == 0) {                              //ラベルを格納
0066:                 for (string &col : split(rec, '\t')) {       //列に分解
0067:                     labels.push_back(col);
0068:                 }
0069:             } else {                                      //要素を格納
0070:                 item = new map<string,int>;
0071:                 int i = 0;
0072:                 for (string &col : split(rec, '\t')) {       //列に分解
0073:                     item->insert(make_pair(labels[i], stoi(col)));
0074:                     i++;
0075:                 }
0076:                 items.push_back(*item);                   //リストに加える
0077:             }
0078:             cnt = cnt + 1;
0079:         }
0080:     }
0081:     return(cnt - 1);                                      //ラベル行を除く
0082: }

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

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

0085: int main() {
0086:     vector<stringlabels;
0087:     vector<map<string,int>> items;
0088: 
0089:     //データ読み込み
0090:     int cnt = parseList(datalabelsitems);
0091: 
0092:     map<string,int>   sums;
0093:     map<string,floatavgs;
0094: 
0095:     //合計計算
0096:     for (int i = 0; i < cnti++) {
0097:         for (string key : labels) {
0098:             if (key != "番号") {
0099:                 sums[key] += items[i][key];
0100:             }
0101:         }
0102:     }
0103:     //平均計算
0104:     for (string key : labels) {
0105:         avgs[key] = (float)sums[key] / cnt;
0106:     }
0107: 
0108:     //出力
0109:     cout << endl << "  ";
0110:     for (string a : labels) {
0111:         cout << a << "  ";
0112:     }
0113:     cout << endl << "----------------------------------" << endl;
0114:     for (int i = 0; i < cnti++) {
0115:         for (string key : labels) {
0116:             cout << "  " << setw(4) << items[i][key];   //番号・点数
0117:         }
0118:         cout << endl;
0119:     }
0120:     cout << "----------------------------------" << endl;
0121:     cout << "  合計";
0122:     for (string key : labels) {
0123:         if (key != "番号") {
0124:             cout << "  " << setw(4) << sums[key];
0125:         }
0126:     }
0127:     cout << endl << "  平均";
0128:     for (string key : labels) {
0129:         if (key != "番号") {
0130:             cout << "  " << fixed << setprecision(1) << avgs[key];
0131:         }
0132:     }
0133:     cout << endl;
0134: 
0135:     return (0);
0136: }

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