| Top Page | プログラミング | Perl 目次 | prev | next | 索引 |
配列は,たくさんのデータを一括して扱えるデータ構造でした. たくさんのデータを同時に覚えておく必要がある場面でこそ 配列が生きてきます. そんな例をいくつかあげてみます.
データロガーを調査地に置いて,2ケ月にわたって測定した1時間ごとの 気温がずらずらと並んだデータファイルがあるとします.24時間×60 で, 1500弱のデータです.
-3.0 -2.9 -2.5 -1.8 -1.0 0.0 1.0 2.0 3.0 3.8 ...
次のプログラムは,うえのような形式の気温データファイルを読み込んだあと, あらかじめ用意した何種類かの閾値に応じて,積算温度を計算し,気温と並べて 出力するというものです. 閾値が一種類だけなら,気温データを読みながらどんどん積算温度を出力すれば いいのですが,何種類もの閾値に対応したいとなると,一度読み込んだ気温データを 配列に読み込んで使ったほうが効率がよかったり,書きやすかったりします.
@threshold = (0.0, 2.0, 4.0, 5.0, 6.5); # 閾値(生物学的0度)をいろいろ用意. @temperature = (); # 気温の時系列を記憶する配列を明示的に用意 while ($line = <>) { # 気温データファイルを順次読んで行く. chomp $line; # 末尾の改行を削除 push @temperature, $line; # $line は温度がひとつ書いてあるのみ.そのまま push } $num_data = @temperature; # 気温データの総数 foreach $zero (@threshold) { # あらかじめ準備した閾値を順に試す $degree_day = 0.0; # それぞれの閾値で積算を始めるまえに0に戻す. for ($i = 0; $i < $num_data; ++$i) { # 全部のデータを順次チェック if ($temperature[$i] > $zero) { # 閾値より高温だったら… $degree_day += ($temperature[$i] - $zero); # 高い分だけ積算温度に加算 } printf "%.1f\t%.1f\n", $temperature[$i], $degree_day; # 気温と積算温度を表示 } print "\n"; # データの切れ目が分かるように,閾値が変わるごとに空白行を出力. }
いまさらくどいですが,いちおう動かし方を復習しておきます.温度データが temp_2002.txt というファイルに記録されていて,上のプログラムは deg_day.pl というファイルに書かれているとしたら, DOS窓(ないしは仮想端末など)で,
perl deg_day.pl temp_2002.dat
と入力して Enter キーを押せば,計算結果がダダダッと表示されます.
※練習用に temp_2002.txt を用意しました(> こちらのページ). これを表示してそっくりコピーしてテキストファイルに貼り付けて, temp_2002.txt という名前(でもなんでもいいんですが)で保存して使ってください. これは実際の測定データではなくて,プログラムで生成したものです. データ作成用のプログラムも載せておきます (>gen_temp_series.pl).
出力をリダイレクトでファイルにしまえば…
perl deg_day.pl temp_2002.dat > deg_day_2002.txt
あとから必要に応じて利用できます.
2つの地点で植生調査(動物相でもなんでもいいんですが)をしたデータが あるとします.あるいは,同じ地点での2回の調査のデータと考えても けっこうです. 一行に一種ずつ種名が書かれたデータファイルがあって,2組みのデータセット のあいだは空白行で区切られているとします.
少々わざとらしいデータ構造ですが,ご勘弁を.一回の調査ごとに 結果がひとつのファイルに記録されてるというようなより自然な場合に 対応するには,もうちょっと知識が必要です(もうすぐ出てきます).
コナラ クヌギ エノキ ... エゴノキ ←この空白行が2回の調査の区切り ムクノキ コナラ … クリ
2回の調査で確認された種類を比べて,共通する種と,どちらか一方にしか 出てこない種とを抽出したいとします. こんな,場合,2回の調査それぞれで出てきた種名をそっくり覚えておいて 比較する必要がありますから,配列の出番です. 少々長くなりますが,プログラム例を書いてみます.
@species1 = (); # 最初の種群をしまう配列を明示的に用意(必須ではないけど). @species2 = (); # 第二種群を…(以下同文) while ($line = <>) { # DOS窓で指定したファイルを1行ずつ読む. chomp $line; if ($line eq "") { # 空白行があったら,種群の境界. last; # 最初の種群の読み込みを中止する. } push @species1, $line; # そうでなければ第一種群に種を追加. } while ($line = <>) { # ファイルの残りを読み続ける chomp $line; if ( !($line eq "") ) { # 空白行でなければ…(今度は,空白行は単に無視) push @species2, $line; # 第二種群に種を追加. } } @common = (); # 共通種をしまう配列を明示的に用意(必須ではないけど) @only_in1 = (); # 第一種群にのみ含まれる種をしまう配列. @only_in2 = (); # 第二種群にのみ含まれる種をしまう配列. foreach $sp1 (@species1) { # 第一種群の各種について…, foreach $sp2 (@species2) { # 第二種群の各種について…, $found = 0; # まず,発見マーカ−をゼロにしておく. if ($sp1 eq $sp2) { # sp1 と sp2 が同じだったら, $found = 1; # 発見マーカーを1にして, last; # 第二種群のチェックはもうやめてよい. } } if ($found == 1) { # 発見マーカーが1だったら, push @common, $sp1; # $sp1 は共通種なので,共通種配列に追加 } else { # そうでなかったら, push @only_in1, $sp1; # 第一種群にだけ含まれるので,そっちの配列に追加 } } foreach $sp2 (@species2) { # 第二種群の各種について…, foreach $sp_common (@common) { # 共通種のそれぞれについて… $found = 0; # まず,発見マーカ−をゼロにしておく. if ($sp2 eq $sp_common) { # $sp2 eq $sp_common が同じだったら, $found = 1; # 発見マーカーを1にして, last; # 共通種のチェックはもうやめてよい. } } if ($found == 0) { # 発見マーカーがゼロのままだったら, push @only_in2, $sp2; # $sp2 は第二種群だけに含まれる } } print "common species\n"; foreach $sp (@common) { # 共通種を表示. print $sp, "\n"; } print "\n"; print "only in the first group\n"; foreach $sp (@only_in1) { # 第一種群だけに含まれる種を表示. print $sp, "\n"; } print "\n"; print "only in the second group\n"; foreach $sp (@only_in2) { # 第二種群だけに含まれる種を表示. print $sp, "\n"; } print "\n";
くどくどとコメントを書いてあるので,読んでいただければ分かるでしょう. ちょっとした工夫は発見マーカー $found です.これがないと, 内側の foreach 文のブロックから抜けたときに,全部の種をむなしくチェックし終わって 抜けてきたのか,途中で共通種を見つけて抜けたのかの区別がつきません.
与えられた母集団からランダムにサンプルを選ぶ,という作業はいろいろな場面で でてきます.ここで取り上げる例は,平面上に存在する多数の点のなかから, ある個数の点をランダムに選ぶ,というものです.
データファイルには一行ごとにx座標とy座標が書かれています.
3.3 5.2 10.3 7.3 1.8 12.7 .... 2.1 15.2
このなかから10点をランダムに選ぶプログラムを書いてみます.
$num_to_choose = 10; # 選び出す点の数 while ($line = <>) { # 点の位置データを一行(=一点)ずつ読み込む. chomp $line; ($x, $y) = split (/\s+/, $line); # x座標とy座標に分割. push @x_array, $x; # 配列にしまう. push @y_array, $y; # } $num_points = @x_array; # データ数. if ($num_points < $num_to_choose) { # 選ぶべき点の数よりデータ数が少なかったら die "!Too few points in data file.\n"; # メッセージを表示して終了 } for ($i = 0; $i < $num_to_choose; ++$i) { # 選ぶ点の数だけ繰り返す. $choice = int(rand() * $num_points); # [0, $n_points) の範囲の乱数の整数部分 # → $choice は 0 から $n_points - 1 の整数 # 選んだ番号の点の座標を表示 print $x_array[$choice], "\t", $y_array[$choice], "\n"; # 選んだ点のデータを配列の先頭のデータで上書き→選んだ点のデータは消える. $x_array[$choice] = $x_array[0]; $y_array[$choice] = $y_array[0]; # 配列の先頭の点を取り除く.要素数は1つ減る. shift @x_array; shift @y_array; $num_points = @x_array; # 現在の点の数. }
int は,与えられた数値の整数部分を返す関数です.rand() は0 以上 1 未満の 数を返すので,これを $num_points 倍したら 0 以上 $num_points 未満の 数になります.その整数部分をとると,0 以上 $num_points -1 以下の 整数になります. 点の数が $num_points 個だから,配列の要素の番号としては 0 番から $num_points -1 番まで.ぴったりです. 0 番から $num_points番ではないことに注意してください (端っこのひとつを余分に数えてしまったり,逆に数え落としたりするたぐいの まちがいを,英語で off-by-one errorと呼びます.cf. 日本の植木算).
同じ点を重複して選んでしまわないように,選んだ点のデータはそのつど配列から 取り除いています.直接その要素を削除するのではなくて(そういうことをする関数も あることはありますが),先頭要素で上書きしたあと,先頭要素を shift で削除する, というアルゴリズムです.コメントを読んで理解してください.
いちおう理解したところで,選ばれた点が配列の最初の点(0番めの点)であっても 正しく動作する(その点の座標のデータが削除されて,配列の要素数が1つ減る) ことを確認してください. プログラムのまちがいのページでも書いたように, バグは境界付近で発生しやすいので,こんなところに注意します.
データファイルを読み終わったところで,選ぶべき数だけのデータがあることを確認 しています.データが足りなかったとき,新しく登場した die という関数が呼ばれています. die は exit のようにプログラムの実行を終了しますが, die の後に指定したメッセージとを表示してくれます. (厳密にはもうちょっと違うところがあります).
たくさんのデータをひとまとめにして扱える配列はとても便利です. 配列を使えるようになれば,調査や実験やシミュレーション計算で たくさんのデータが出てきても効率よく処理できそうです.
でも,なんでもかんでも配列にしまう必要はなく,ファイルから1データ読むたびに処理をしていけば よい場合も少なくありません.また,あとで説明するハッシュのほうが 適している場合もしばしばあります. なんでもかんでも配列にしまおうとせず, プログラム中でデータをどう管理すると処理しやすいか,よく考えて方針を立てましょう.