2008年8月11日

じゃぁ、相関とかを求めるかね。

全部のマシン、全部の日付、全部のエントリについて、"Processor(_Total)\% Idle Time" との相関 r ならびに r2, 回帰式 y=ax+b にマップしたときの a, b を求めよう。あ、x 側が"Processor(_Total)\% Idle Time"で、yが「相関を求めるべき他のエントリ」な。

Perlにはこの手の統計処理用のモジュールが結構ある。どれを使うか迷う所だが、今回は深く考えずにMath::NumberCruncherを使ってみよう。

もし、cygwinを使っているならば、次の命令を実行し、尋ねてくる質問に真摯に答えればインストールは完了する。
% perl -MCPAN -e 'install Math::NumberCruncher'

Active Perlのときはどうするのか?とかそういう質問の答は判らないのでよろしく。

http://search.cpan.org/~sifukurt/Math-NumberCruncher-5.00/NumberCruncher.pod
に一応説明らしきものがある事になっているが、はっきり言って説明になっていない。2引数を取る場合、どちらかがxでどちらかがy…という問題を解かなきゃいけないはずだが、それもない。強引に超能力をかけることにする。多分、こういうこったろう。

$correlation = Math::NumberCruncher::Correlation(\@yarray,\@xarray);

($slope,$y_intercept ) = Math::NumberCruncher::BestFit(\@yarray,\@xarray);


ここで、$correlation は「相関係数」つまり、今まで言っていた r の事だ。$slope (傾き)と $y_intercept (y切片) は、それぞれ a と b になる。r2を直接求める関数はないので、$r2=$r*$r; で求める事になる。




入力TSVファイルは全部次のようなフォーマットになっている。
"(PDH-TSV 4.0) (Tokyo Standard Time)(-540)" "\\A\Processor(_Total)\% Idle Time" "\\A\LogicalDisk(C:)\% Disk Read Time" ........
"07/15/2008 07:59:59.810" "0.079295186091091777" "0.00050253600663232288" .......
"07/15/2008 08:00:59.809" "98.125628004019234" "0.10616734613768194" .......
........


第一行がラベルで、各ラベルは ダブルクォート(") で囲まれている。さらに、それらはタブで分割されている。第2行以降は測定結果だ。

各行の最初は「測定時刻」だ。従って、相関を求めるべき一方の「Idle」は2つ目の要素になる。

ダブルクォートの中にはタブはないので、とりあえずタブで要素を分割すればいいだろう。その後、ダブルクォートを各要素から取り除けばよい。これを2次元配列にぶちこんだあと、2つ目の要素列と3つ目以降の各要素列との相関を求める。




各要素の出力は次のような形とする。
"name" "r" "r2" "slope" "y intercept"
"\\A\Processor(_Total)\% Idle Time" "0.99999999999999558689" "0.99999999999999117378" "0.99999999999995984122" "0.00000000000341678664"
"\\A\LogicalDisk(C:)\% Disk Read Time" "-0.08658618963563029212" "0.00749716823561733062" "-0.36956562874045379176" "85.25898260145136281611"
.......

第1行は各列のラベルだ。で、2行目以降は「相関を取った列のラベル」、相関係数 "r"、rの二乗値 "r2"、傾き "a"、y切片 "b"の順になる。1行毎に相関を取る相手が変わる。

ある TSVファイルから、この出力を求める perl スクリプトは次の通りだ。

calcr2.pl

use Math::NumberCruncher;
$ref = Math::NumberCruncher->new();

my $linenumber = 0;
my $maxcells = 0;

# read data.
while () {
chomp;

my @cells = split /\t/;

if ( $maxcells < $#cells ) {
$maxcells = $#cells;
}

if ( $linenumber == 0 ) {
for ( $i = 0; $i <= $#cells; $i++ ) {
$cells[$i] =~ /^\"(.+)\"/;

my $tmp = $1;
$countername[$i] = $tmp;
}
} elsif ( $linenumber == 1 ) {
# skip this one!
# first taken data is useless.
} else {
for ( $i = 0; $i <= $#cells; $i++ ) {
$cells[$i] =~ /^\"(.+)\"/;
my $tmp = $1;

# $linenumber == 2 got to be $index == 0.
my $index = $linenumber - 2;

$num[$i][$index] = $tmp;
}
}

$linenumber++;
}

# output the result
print "\"name\"\t\"r\"\t\"r2\"\t\"slope\"\t\"y intercept\"\n";

for ( $i = 1; $i <= $maxcells; $i++ ) {

$xname = $countername[1];
$yname = $countername[$i];
@xarray = @{$num[1]};
@yarray = @{$num[$i]};

$r = Math::NumberCruncher::Correlation( \@yarray, \@xarray );
($a, $b)
= Math::NumberCruncher::BestFit( \@yarray, \@xarray );
$r2 = $r * $r;

print "\"$yname\"\t\"$r\"\t\"$r2\"\t\"$a\"\t\"$b\"\n";
}


いくつかポイントがある。

入力は標準入力。出力は標準出力だ。

データ行になって最初の行はわざと捨てている。これは、この手の記録の多くが「前回取得したデータとの差分」から求められる事が多いからだ。1つ前のデータがない、最初の一手は往々にして間違ったデータである事が多い。故にそれは取り除く。

@xarray, @yarray をコピーしているが、これは単に「配列の配列」から「配列(2つ目の方)」を持ってくる方法を完全に失念していたためだ。うまく動かない…と七転八倒するためにコピーをとったのに過ぎない。多分、ここには無駄があり、この無駄を省けば結構速くなるんじゃないかな、と思う。

こいつを、すでに作ってあるTSVファイルに対して適用する。ただし、出力結果もTSV。ややこしい事おびただしいが、作ったときは深く考えてなかった。カっとなってやった。今では反省している。

% cd $TOP
% cd $TOP/04Colleration
% cat filter.sh
for i in ../03TSV/*.tsv; do
of=`echo $i | sed 's/\.\.\/03TSV\///g'`;
echo $of
cat $i | ./calcr2.pl > $of
done


私の環境では12時間ほどかかりました。もっとメモリとプロセッサパワーが欲しいです。