| Top Page | プログラミング | Perl 目次 | prev | next | 索引 |

14. ハッシュの活用:柔軟なデータ集計(その2)

種名をキーに,'データのまとまり'を値に

前のページで使ったデータ(>こちらのページ) の解析の続きです.

種ごとの個体数とか,材積の積算とか,最大樹高とかを求めるには,それぞれの種ごとに ひとつの値を用意しておけば済みました.個体数の積算値,材積の積算値,最大樹高, それぞれ変数一個にしまっておける値だからです.そこで,種名をキー,積算値や最大値を 値とするハッシュを使ってプログラムを書きました.

今度は,種ごとにひとつの値ではすまない場合を考えてみます. たとえば,樹種ごとに樹高の中央値を知りたいとか, 大きいほうから5本の高さを知りたいといった場合,各樹種の全個体の値を並べて見ないと 答えが決められません.

そのためには,ファイルからデータを読みながら,種名をキー,「高さの配列」(各 個体の高さデータを要素とする配列)を値とするハッシュを作ればよさそうです. ところが,残念ながらこういうことはできません. ハッシュの値として配列ないしはリストを使うことはできないのです.

※Perl では,リファレンスというものを(Version が 4 から 5 になるときに)導入して, データ構造の自由度を飛躍的に拡大しました.これを使うと,事実上, ハッシュの値として配列(や別のハッシュ)を値として持てるようになります. でも,リファレンスの話はまたあとで.

ここでは,これまでの知識だけ使って,ハッシュのひとつの値にたくさんの個体の データを押し込む方法を紹介します. 各個体のデータを文字列としてどんどん連結していく方法です. 連結するとき,個体ごとのデータのあいだには適当な文字を挿入しておきます. 挿入する文字は,データ中に出てこないことが分かっている文字を使います. 長い文字列データにためこまれた個々の個体のデータを使いたいときは, split 関数を使って文字列を分割すればよいのです.

$num_top = 5;   #  上から $num_top 個体めまで表示.

$record = <>;       #  最初のヘッダ行を読み捨てる.

while ($record = <>) {
    chomp $record;
    ($sp, $x, $y, $h, $dbh) = split /\s+/, $record;

    # 高さデータのうしろに空白をつけてから,文字列のうしろに連結.
    #  → あとで,空白を区切り文字として split する.
    $height_list{$sp} .= ($h . " ");    
}

foreach $sp (keys %height_list) {   # それぞれの種について

    printf "%-16s", $sp;  # 種名を出力 (16字幅,左寄せ))

    @height = split /\s+/, $height_list{$sp};  # 高さデータが並んだ文字列を空白で分割
    @height = sort {$b <=> $a} @height;        # 高さの降順に並べ替え.

    for ($i = 0; $i < $num_top; ++$i) {    #  上から $num_top 個体のデータを出力
        if (@height == 0) {   #  もうデータがないならこのループを抜ける.
            last;             #  ※ $num_top より個体数が少ないとこうなる.
        }
        $h = shift @height;   # 降順に並んだ配列の先頭(つまり最大値)を取り出す.
        print "\t", $h;
    }
    print "\n";    #  1種のデータの出力が終わったところで改行.
}

高さデータを連結するとき,各データのうしろに空白一文字をつけています. どんどん連結していくと,たとえば "12.2_2.3_5.1_" のような文字列ができます (強調のため,空白文字のかわりに全角のアンダーラインを書いておきます). 末尾にはかならず空白がひとつあります. split は,文字列の末尾に区切りパターンがあったときはその区切りを無視しますから, split /\s+/, "12.2_2.3_5.1_"; は 12.2, 2.3, 5.1 の3つの要素からなるリスト を返します.

高さデータのうしろでなくて,前に空白一文字をつけながら連結していくと, "_12.2_2.3_5.1" のような文字列ができます(こんども空白文字のかわりに 全角のアンダーライン).split は,文字列の先頭の区切りパターンは無視せず, その前に空文字がひとつあるものとして分割するので, split /\s+/, "_12.2_2.3_5.1"; は 空文字列,12.2, 2.3, 5.1 の4つの要素からなる リストを返してしまいます. 空文字列は余計なので,区切りの空白文字はデータの後ろにつけています.

※ほんとは,区切りパターンや分割対象文字列の指定,さらにオプションの指定などによって, 最初の空文字列をリストに含めるかどうかや,最後の空文字列を捨てるかどうかを制御できます. 詳しいことは本やネットで調べてください.

sort の使い方については,ハッシュの最初のページの, ハッシュのキーを対応する値の大小に応じて整列させる方法 のところでも説明しました. 高さのデータの配列を sort してるところでは,{$b <=> $a} というブロックを sort のあとに書いて,整列のための大小比較の仕方を 指定しています. なにも指定しないと,sort はリストの要素を文字列として比較して 文字コードの小さいほうから大きいほうへと並べます. 12.3 と 5.1 だったら,数値としては 12.3 のほうが大きいですが, 文字列として最初の文字から比べていくと, 1 よりも 5 のほうが文字コードは大きいので, 5.1 のほうが12.3 より大きいと評価します. 木を高さ順に並べるのにこれでは困るので, 数値比較演算子 <=> を使った比較の式をブロック内に書いています.

数値データを扱うプログラムでは,文字列としての整列ではなく数値としての 整列をしたいことも多いでしょう.上の例の書き方や, ハッシュのキーを対応する値の大小に応じて整列させる方法 のところで説明した書き方を活用してください.


表を整形してみる

文字列の連結を使って多くのデータをひとつのスカラー変数に押し込む方法は いろんな場面で使えます. 下のデータは,1時間ごとの気象データ(気温,光の強さ,相対湿度)が何日分も 並んだものです.データロガーから取り出したデータはよくこういう形をしています.

date        time      temp  light    humid
2001/05/01  21:02:03  18.3      0.0   80.6
2001/05/01  22:02:03  17.1      0.0   83.0
2001/05/01  23:02:03  16.8      0.0   84.1
2001/05/02  00:02:03  16.3      0.0   84.8
2001/05/02  01:02:03  15.9      0.0   86.0
..
2001/05/02  11:02:03  13.3    960.0   41.0
2001/05/02  12:02:03  13.3   1020.0   35.5
..
2001/06/01  09:02:03  23.3  451.1  45.0

このままでは扱いにくいので,気温,光,湿度それぞれについて

time    2001/05/02 2001/05/03 2001/05/04 ...
00:02:03  16.3       18.1       20.2 ..
01:02:03  15.9       17.9       20.0 ...

 ...

23:02:03  18.3       20.8       20.2 ...

のように,列が日付,行が時刻というという形式に変換したいとします. こういう作業を表計算ソフト上で延々とやっている人も居るかもしれませんが, できれば計算機に任せたい単純作業です. たとえばこんなプログラムを書けば,上のような形式の出力が得られます.

$skip_lines = 4;    #  ヘッダ行と,最初の半端な行の数の合計(データによって決める)

for ($i = 0; $i < $skip_lines; ++$i) {
    $line = <>;    # ヘッダ行と,最初の半端な行を読み捨てる.
}

while ($line = <>) {
    chomp $line;
    ($date, $time, $temp, $light, $humid) = split /\s+/, $line;

    if ($date ne $prev_date) {            # 新しい日付だったら
        $date_list .= ($date . "\t");     # 日付リスト文字列の末尾に追加.
        $prev_date = $date;               # 現在の日付として記録.
    }

    $temp{$time} .= ($temp . "\t");       # この時刻の温度データ文字列の末尾に追加
    $light{$time} .= ($light . "\t");     # この時刻の光データ文字列の末尾に追加
    $humid{$time} .= ($humid . "\t");     # この時刻の湿度データ文字列の末尾に追加
}

#  全部読み終わったら…

print "time\t", $date_list, "\n";     # ヘッダ行の出力
foreach $time (sort keys %temp) {     #  キーである時刻を文字列としてソートすれば時刻順
    print $time, "\t", $temp{$time}, "\n";  #  その時刻の温度データ文字列を表示.
}
print "\n";

print "time\t", $date_list, "\n";     # 同様に光データの出力
foreach $time (sort keys %light) {
    print $time, "\t", $light{$time}, "\n";
}
print "\n";

print "time\t", $date_list, "\n";     # 同様に湿度データの出力
foreach $time (sort keys %humid) {
    print $time, "\t", $humid{$time}, "\n";
}

どの日も計測時刻はぴったり同じであることを前提にしたプログラムです(データロガーなら そうなるでしょう).時刻をキー,各日のデータをタブをはさんで連結した文字列を値とする ハッシュを各測定項目(温度,光,湿度)について作っていきます.

出力するところでは,連結して作った文字列をわざわざ split しなくても, タブがあいだに入った文字列をそのまま出力すればタブ区切りのデータファイルが できあがります.


ハッシュで’二次元配列風'

もうひとつハッシュの利用法を紹介します.Perl はもともと一次元の配列しか 用意されておらず,2次元や3次元の配列はありません. 2次元の配列とは,ちょうど表計算ソフトの表のように2次元的にデータが並んでいて, 何行目の何列目というようにして個々の要素を指定する配列です.それを3次元に拡張 したのが3次元配列,一般に2以上の次元を持つ配列が多次元配列です.

※リファレンスを使うと,実質上多次元配列が作れます. でも,リファレンスの話 はやっぱりあとで.

ここでは,ハッシュを使ってデータを多次元配列風に扱ってみましょう. 要素を指定する2つの添え字(2次元配列の場合)や 3つの添え字(3次元配列の場合)を適当な区切り文字をはさんで連結した文字列を, ハッシュのキーにしてしまえばよいのです.

実例を見てみましょう. 最初の 20 メートル四方の森林の調査データを5メートル間隔で格子状に 区切ります. 5メートル四方の各小区画ごとに,そこに生育している木の材積(幹の体積) の指標として,高さ×太さの二乗 の積算値を計算してみます.小区画は 4×4の2次元構造をしていますから,ハッシュを2次元配列風に使います.

$grid_size = 5;   #  格子の間隔

$record = <>;     #  最初のヘッダ行を読み捨てる.

while ($record = <>) {
    chomp $record;
    ($sp, $x, $y, $h, $dbh) = split /\s+/, $record;

    $x_index = int ($x / $grid_size);    # x軸方向の何番めの区画か.
    $y_index = int ($y / $grid_size);    # y軸方向の何番めの区画か.

    $index = $x_index . " " . $y_index;  # x軸上とy軸上の順番を連結してキーに
    $d2h{$index} += ($dbh / 100.0) ** 2 * $h;  # キーに対応する値に材積を積算.

    if ($x_index > $max_x_index) {     #  x軸方向の指標の最大値か?
        $max_x_index = $x_index;       #  新記録だったら,それを記憶
    }
    if ($y_index > $max_y_index) {     #  y軸方向の指標の最大値か?
        $max_y_index = $y_index;       #  新記録だったら,それを記憶
    }
}

#  結果の出力

for ($y_i = 0; $y_i <= $max_y_index; ++$y_i) {  # ヘッダ行の表示
    print "x/y\t", ($y_i * $grid_size);
}
print "\n";

for ($x_i = 0; $x_i <= $max_x_index; ++$x_i) {      # 各x軸指標ごとに…
    print ($x_i * $grid_size);

    for ($y_i = 0; $y_i <= $max_y_index; ++$y_i) {  # 各y軸指標ごとに…
	    $index = $x_i . " " . $y_i;             #  指標を連結してキーを生成し,
	    printf "\t%.3f", $d2h{$index};          #  対応する値を出力
    }
    print "\n";
}

とりたてて新しく説明することはありません. プログラムを読めば分かるでしょう. 実行すると,こんな出力が得られます.

x/y     0       5       10      15
0       3.134   2.966   2.253   2.180
5       2.724   3.067   3.573   2.501
10      1.787   3.645   2.718   2.785
15      2.849   4.432   2.490   4.198

多次元配列風ハッシュは,2次元平面のデータに限らず,2つや3つの要因の いろいろな組み合わせの処理区がある場合(光環境3通り×栄養段階3通り といったふうに)や,個体群動態を表現する2次元の遷移行列をあつかう場合など, いろいろ応用の場面はあるでしょう.


まとめ

ここまででハッシュの説明はひとまず終わりにします. ハッシュの利用例をいろいろあげてきましたが,ほかにもいろんな活用法が あるでしょう. データファイル中では種名を1文字や2文字のコートなので表現し, それとは別に種名コードとほんとの種名との対応をハッシュで覚えておけば, データファイル中の種名コードを簡単に種名に変換できる,なんてのはすぐに思いつく例です. 自分の扱うデータややりたい計算のなかでどうハッシュを活用できそうか, ぜひ考えてみてください.

なお,この先には,ハッシュとリファレンスを活用したオブジェクト指向 プログラミングなどという世界も開けているのですが,それはもはや 「基礎の基礎」ではありません. 興味があったら本を読むなどして勉強してください.



| Top Page | プログラミング | Perl 目次 | prev | next |