ラベル perl の投稿を表示しています。 すべての投稿を表示
ラベル perl の投稿を表示しています。 すべての投稿を表示

2009年2月5日

リファクタリング-4-

リファクタリング-3-で書いた「データ列を1データ列1ファイルに分割する」プログラムをまず、作った。

remove_NaN.pl:
#! /usr/bin/perl
#
# remove_NaN.pl <dataFileName> <template of eachoutput>
# chop dataFile into each row. If each row is clear that
# "Pearson product-moment correlation coefficient" calculation
# will ends in NaN, do not output that entry.
#
# For <template of eachoutput> I mean something like "hogehoge%08d".
# output file will have row number as for %08d part (%08d can be %d
# or anything in integer format ).

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

our $at_once = 7000;

$datafilename = $ARGV[0];
$outtemplate = $ARGV[1];

if ( $outtemplate eq '' ) {
$outtemplate = 'temp%08d.TSV';
}

open( DATAFILE, $datafilename ) or die "unable to open $datafilename as datafile\n";


# First, read first line. It's TAG-NAME list.

our @tagname;
our $line;

$line = <DATAFILE>;
@tagname = split /\t/, $line;
for ( $i = 0; $i < scalar( @tagname ); $i++ ) {
$tagname[$i] =~ s/^\s*(".*")\s*$/\1/;
}


# Okey. Now let's read each rows.
# First row is not numeral, but is time of when data was sampled. so skip first row.

our $filecounter = 0;
for ( $row = 0; $row < scalar( @tagname ); $row += $at_once ) {
debug( "$row start\n" );
# bring file pointer to top of line.
seek( DATAFILE, 0, 0 );

# skip tagname line.
$line = <DATAFILE>;

my @output;
my @data;
my @sum;
my @mean;
my @sqsum;
my @subpart;
my @datas;

for ( my $i = 0; $i < $at_once; $i++ ) {
$output[$i] = 1;
}

while (<DATAFILE>) {
my @oneline = split /\t/, $_;
my $val;

for ( my $i = 0; $i < $at_once; $i++ ) {
$oneline[$row + $i] =~ s/^\s*"(.*)"\s*$/\1/;
$val = $oneline[$row + $i];
if ( $val eq '' ) {
$output[$i] = 0;
} else {
push @{$data[$i]}, $val;
$sum[$i] += $val;
}
}
}
debug( "read data\n" );

# calculate $mean and $subpart.

for ( my $i = 0; $i < $at_once; $i++ ) {
if ( $output[$i] ) {
$datas[$i] = scalar( @{$data[$i]} );
$mean[$i] = $sum[$i] / $datas[$i];
if ( $mean[$i] eq 'NaN' ) {
$output[$i] = 0;
next;
}

for ( my $j = 0; $j < $datas[$i]; $j++ ) {
my $val;
$val = $data[$i][$j] - $mean[$i];
$sqsum[$i] += $val * $val;
}
$subpart[$i] = sqrt( $sqsum[$i] );

if (( $subpart[$i] eq 'NaN' )||( $subpart[$i] == 0.0 )) {
$output[$i] = 0;
}
}
}
debug( "calc data\n" );

# dumpout the row.
for ( my $i = 0; $i < $at_once; $i++ ) {
my $filename = sprintf( $outtemplate, $filecounter );
if ( !defined( $tagname[$row+$i] )){
next;
}

open( OUTPUT, "> $filename" ) or die "unable to open $filename as output file. Template was $outtemplate.\n";
print OUTPUT "tagname\t$tagname[$row+$i]\n";
print OUTPUT "Correlation\t" . ( $output[$i] ? 'OK' : 'NG' ) . "\n";
print OUTPUT "sum\t$sum[$i]\n";
print OUTPUT "mean\t$mean[$i]\n";
print OUTPUT "sqsum\t$sqsum[$i]\n";
print OUTPUT "subpart\t$subpart[$i]\n";
print OUTPUT "datas\t$datas[$i]\n";
print OUTPUT "\n";

foreach ( @{$data[$i]} ) {
print OUTPUT "$_\n";
}

close OUTPUT;
debug( "output $filename \n" );

$filecounter++;
}

ROWSTEP_CLEANUP:
undef @output;
undef @data;
undef @sum;
undef @mean;
undef @subpart;
undef @datas;
}

close DATAFILE;

sub debug {
my $time = gmtime();
print STDERR "$time: ";
foreach ( @_ ) {
print STDERR $_;
}
}

このプログラム中、唯一判らないのは「$at_once」だろう。これはようするに、読んだデータの内、$at_once 個のデータ列づつ平均値だのなんだのを計算して、各データ列ごとに別のファイルに出力する、という事だ。

実はこのプログラム、かなりメモリを消費する。データファイル自体は1行づつ読んではパースし、不要な部分は捨てているのだが、元のデータが 27000列とかだと、一日分のデータだけでも 32bit Windows システムではメモリが足りずに途中でハングする。そこで、一度に処理する行数をスクリプト中に書いておくことにした。メモリが足りないならここを調節すればよい。

ただし、可能な限り大きな値を指定しておくことをお勧めする。実験として50を選んであるが、ものすごく遅くて難儀した。27000の場合、540回同じファイルを読み直すことになり、そのたびにファイル全体を文字列処理する必要があるのだから、当たり前と言えば当たり前だが。

2009年2月4日

リファクタリング-3-

Math::NumberCruncherのコードの一部を手元に展開した状態で読んでいく…ん? んん?? んんん~~~???

どうみても、Correlation() の計算式は
ピアソン積率相関係数 のそれではないっ!!!

確かに似たような値が出てきてはいるが、明らかに違うっ!!!

ぬぅ。なんということだ。大前提からしてやり直しかっ!!!

しかも。これは致命的な問題ではないからいいようなものの…同モジュールに含まれている平方根を出す関数 SqrRoot() は

0.0 を入れると NaN を返すっ!!!
まてぃぃいいいいい!!!

どこの世界に0.0 の平方根が計算できない数値演算ライブラリがあるっ!!! ちなみに、他の値を入れると(プラスの値であれば)妥当な出力を出してくる。どうやら、0.0 の場合におかしくなるような漸近式を使っているらしい。

まぁ、0.0だろうが NaN だろうが、その結果を分母に持ってきて分数を計算すれば、NaNになるのは変わらないので、そりゃ結果だけ見れば構わないと言えば構わないのだが…。とりあえず、この程度のデバッグもできていないモジュールである以上、これは信頼するわけには行かぬ。

というわけで根こそぎ作り変えてくれよう。
!!決定!!

とりあえず、次のようなデザインにする。
  1. まず、データ列を1データ列1ファイルに分割する。
  2. そのデータ列には、当該データ列の平均値、平均値と各データの二乗和、さらに二乗和の平方根、の3つは最低限でも、データとして記録する。
  3. できればデータの個数も記録する。
  4. もちろん、ラベル名も記録する。
  5. いうまでもなく、データ列の値そのものも1行1エレメントで記録する。
  6. 2 で得られた値のどれかが NaN になるか、あるいは「二乗和の平方根」が 0.0 になる場合は、そのようなデータ列は当該データファイルを作らない。
  7. このファイルはすべて文字列で数値を記録する。
で、このファイルを複数読み込んで、組み合わせ毎に計算するプログラムを別途作る。各データ列ごとの事前計算は全部記録されているので、本来の計算に集中できる…に違いない。

…それにしてもここまで巻き戻るとは…

2009年2月1日

リファクタリング-2-

リファクタリングとして数多くのことをする必要があるが、とりあえず最初にやるべきことは、Math::NumberCruncher のコードから、今回必要なルーチンをコピーして持ってくることだろう。Correlation()関数を呼び出す回数は O(n2) になるが、Wikipediaの「相関係数」のページによると相関係数を求める式はこのようになっている:

分母が特に「列」単位で独立して計算できる事がわかる。列単位ならば計算はO(n)で済む。このような最適化がかけられるかどうか、調べるには元のソースが無くてはいけない。
幸い、CPANには perl のソースが .pm ファイルとしてついてくる。それをコピーしてみたが、全部 perl で書かれているようだ。これならば最適かもかけられよう。

次に、x と y のラベル名を読み込むコードを変更する。旧来:
our $i = 0;
our @x_tagname = ();
while (<COMPARE_X>) {
$_ =~ s/^\"//;
$_ =~ s/\"\n$//;
$x_tagname[$i] = $1;
$i++;
}
close COMPARE_X;
となっていたのだが、

  1. push()関数を使えばインデックスの $i は不要になる。

  2. ラベル名に " がついたままでもいいじゃないか。どうせ x, y だけでなくデータのラベルも " がついたままなのだから。文字列比較の際に " がついたまま比較すればいい

と考えるた。結果、このように簡単になる:
our @x_tagname = ();
while (<COMPARE_X>) {
$_ =~ s/^\s*(".*")\s*$/\1/;
push @x_tagname, ( $_ );
}
close COMPARE_X;
同じ変更は COMPARE_Y を読み込む部分についても行う。DATAFILEの最初の1行目を読んだ部分に関してはもっと極端だ。
# Read first line of .
# This should be the list of TAG-NAME.
$line = <DATAFILE>;
@tagname = split /\t/, $line;
for ( $i = 0; $i < scalar( @tagname ); $i++ ) {
my $val;
$val = $tagname[$i];
$val =~ s/^\s*"//;
$val =~ s/"\s*$//;
$tagname[$i] = $val;
}
というこれは
$line  = <DATAFILE>;
@tagname = split /\t/, $line;
for ( my $i = 0; $i < scalar( @tagname ); $i++ ) {
$tagname[$i] =~ s/^\s*(".*")\s*$/\1/g;
}
とできる。

次に。今回のリファクタリングで、BestFit() を呼び出し、回帰直線の傾きと切片を求めるのをやめる。この計算も結構な高コストである上に、相関係数を求めた結果として、重要と思われたものについてのみ必要な数値だからだ。これらは、計算すべきターゲットが見つかってから計算するのでも間に合う。

2009年1月31日

リファクタリング

「じゃぁ、相関とかを求めるかね。」で公開した、calcr2.plに新たなる任務が与えられた。

calcr2.pl は「与えられたTSVファイルの2列目のデータと、2列目以降最後までのデータとの相関を求めることができた。これは「ヒント」となるべき兆候があり、それを2列目に置く事ができたから可能だった方法だ。
新しいミッションはこうだ:

データを与えるTSVファイルのフォーマットは従来と変わらない。
このデータとは別に2つのファイル x と y を与える。x,yそれぞれにラベル名が入っているので、x と yの全組み合わせについてピアソン積率相関係数を求めなさい。

今度はノーヒント、というわけだ。

とりあえず、x と y というファイルのフォーマットを決める。と言っても、ラベル名を列挙するだけなのだから、1行1ラベル名にするだけだ。こんな感じ:
"LogicalDisk(C:)\% Disk Read Time"
"LogicalDisk(C:)\% Disk Time"
"LogicalDisk(C:)\% Disk Write Time"
"LogicalDisk(C:)\% Free Space"
"LogicalDisk(C:)\% Idle Time"

ラベル名の前後に " をつけてあるのは、それら全体で一塊、ということを示しているのにすぎない。つーかそもそもこれ自体もとのデータファイルの第1行目を分割して作っているので、実は " をはずすのが面倒だった、という事に過ぎない。

呼び出しのルールはこうしよう:
$ correlation.pl <x> <y> <data>

xのリスト、yのリスト、そして最後にデータだ。データファイルは相変わらずWindows用の perfmon.exe の出力である .blg ファイルを relog.exe で TSV フォーマットに変換した結果、とする。

「ふはははは、そんなもの朝飯前だよ、明智君」

とばかりにちょろっと直してみた結果がこれだ。
#! /usr/bin/perl
#
# correlation.pl <x tagname> <y tagname> <datafile tsv from exe relog>
#

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

use open IN => ":utf8", OUT => ":utf8";


open( COMPARE_X, $ARGV[0] ) or die "unable to open $ARGV[0]";
open( COMPARE_Y, $ARGV[1] ) or die "unable to open $ARGV[1]";
open( DATAFILE, $ARGV[2] ) or die "unable to open $ARGV[2]";

# First, read file to get the list we need to calculate correlation with.
our $i = 0;
our @x_tagname = ();
while (<COMPARE_X>) {
$_ =~ s/^\"//;
$_ =~ s/\"\n$//;
$x_tagname[$i] = $_;
$i++;
}
close COMPARE_X;

our $i = 0;
our @y_tagname = ();
while (<COMPARE_Y>) {
$_ =~ s/^\"//;
$_ =~ s/\"\n$//;
$y_tagname[$i] = $_;
$i++;
}
close COMPARE_Y;


# Read first line of <datafile>.
# This should be the list of TAG-NAME.
$line = <DATAFILE>;
@tagname = split /\t/, $line;
for ( $i = 0; $i < scalar( @tagname ); $i++ ) {
my $val;
$val = $tagname[$i];
$val =~ s/^\s*"//;
$val =~ s/"\s*$//;
$tagname[$i] = $val;
}

our $line = 0;
while (<DATAFILE>) {
my @oneline = split /\t/, $_;
for ( $i = 0; $i < scalar( @oneline ); $i++ ) {
my $val;
$val = $oneline[$i];
$val =~ s/^\s*"//;
$val =~ s/"\s*$//;
$data[$i][$line] = $val;
}
$line++;
}
close DATAFILE;

#######
# So we read all the necessary data.
#
# Now calculate colerration.
#
#######

# First, check where each x_tagname is.
XTAG_LOOP: for ( my $x = 0; $x < scalar( @x_tagname ); $x++ ) {
my $localxval = quotemeta $x_tagname[$x];
for ( my $y = 0; $y < scalar( @tagname ); $y++ ) {
if ( $tagname[$y] =~ m/$localxval/ ) {
$x_tags[$x] = $y;
next XTAG_LOOP;
}
}
if (( $x <= scalar( @x_tagname ))&&( !defined( $x_tags[$x] ) )) {
die "Unable to find $x th tag \"$x_tagname[$x]\" as tag name for giving list.";
}
}

# Second, check where each y_tagname is.
YTAG_LOOP: for ( my $y = 0; $y <= scalar( @y_tagname ); $y++ ) {
my $localyval = $y_tagname[$y];
for ( my $x = 0; $x < scalar( @tagname ); $x++ ) {
if ( $tagname[$x] =~ m/$localxval/ ) {
$y_tags[$y] = $x;
next YTAG_LOOP;
}
}
if (( $y < scalar( @y_tagname ))&&( !defined( $y_tags[$y] ) )) {
die "Unable to find $y th tag \"$y_tagname[$y]\" as tag name for giving list.";
}
}

# print the tag line.
print "\"\"\t";
for ( my $x = 0; $x < scalar( @x_tagname ); $x++ ) {
print "\"$x_tagname[$x]\"\t\"\"\t\"\"\t\"\"";
if ( $x < scalar( @x_tagname ) - 1 ) {
print "\t";
} else {
print "\n";
}
}

# print the 2nd tag line.
print "\"\"\t";
for ( my $x = 0; $x < scalar( @x_tagname ); $x++ ) {
print "\"r\"\t\"a\"\t\"b\"\t\"r2\"";
if ( $x < scalar( @x_tagname ) - 1 ) {
print "\t";
} else {
print "\n";
}
}

for ( my $y = 0; $y < scalar( @y_tags ); $y++ ) {
print "$y_tagname[$y]\t";
for ( my $x = 0; $x < scalar( @x_tags ); $x++ ) {
my $r = Math::NumberCruncher::Correlation( \@{$data[$y_tags[$y]]}, \@{$data[$x_tags[$x]]} );
my ($a, $b) = Math::NumberCruncher::BestFit( \@{$data[$y_tags[$y]]}, \@{$data[$x_tags[$x]]} );
my $r2 = $r * $r;

print "\"$r\"\t\"$a\"\t\"$b\"\t\"$r2\"";
if ( $x < scalar( @x_tags ) - 1 ) {
print "\t";
} else {
print "\n";
}
}
}


実行してみる………… zzzzzzz …………

おーそーいーーーーーー!!!
救いがたいほどに遅い。あわててデータを見てみる。

ぶっっ!!!


調べる相手だけで、12791項目もある…今までが3500とかのレベルだったから4倍近い。何も考えずに相関を総当りで取ったら、今までの16倍時間がかかる。只でさえ遅いのに…。

こうしてスピードアップを余儀なくされたのだが、そもそもそういう小細工をするには、ちょっとコードが汚すぎるし、外部ライブラリに依存しすぎている。というわけで、リファクタリングを開始することにした。

この物語は、そのリファクタリングの記録である…というわけで続く。

2008年9月6日

Process Monitorの出力をいじる

WindowsにはProcess Monitorというツールがある。多分アンカーの指している場所がダウンロードサイトの中で最も簡単に手に入れられるページだと思う(日本語で説明が書いてあるから)。

対応OSは 2000, XP, 2003, そして Vista だそうだ。

Process Monitor (procmon.exe)は、NT kernelへのシステムコール…というかリクエストキューに積まれたリクエストを記録し続ける。記録されるのは全てのプロセスのリクエストだ。unixのstraceなどと違って「このプロセスの」とか「あのプロセスだけ」という制約は無い。そのおかげでシステム全体の状態が判るが、これはセキュリティ上は結構危険な状態でもある。

procmon.exeは記録したシステムコールを表示するときにも使える。いくつかのフィルター条件を設定して望みのものだけを選び出すことが出来る。ある特定のプロセスが発行したもの、特定のスレッドが発行したもの、あるファイル(あるいはレジストリパス)に対するIO、あるシステムコールだけを選ぶ…などができる。実はシステムコールを発行した(キューに積んだ)時刻だけではなく、これが完了するまでにかかった時間も記録している。
「システムコールが終わるまでに0.1sec以上かかった」
とか
「システムコールに対する応答が無かった」
などという条件も設定できるのだ(NTのシステムコールは「キューにリクエストを積んで」「応答を待つ」形式なので、本質的に全て非同期コールだ)。

障害解析やパフォーマンス解析においてこれほど強力なツールはまず、ない。

が、世の中どんなに頑張っても
「それは俺の設定したい条件じゃない」
と言うことはあるわけで。そうなるとPerlのようなスクリプト言語の出番になる。




Perlに読めるようにするためには、procmon.exeが記録した内容を何らかのテキストの形で出力してもらわなくてはいけない。それ自体は簡単だ。

% procmon.exe /OpenLog 'filename.PML' /SaveAs 'filename.CSV'


PMLは procmonが出力するバイナリフォーマットのログ、filename.csvは「csvフォーマットでファイルを出力しろ」と言う意味だ。ちなみにこの場合、csvファイルはutf-8で出してくれる。なんとUCS2やUTF-16じゃないのだ。ありがたい。

しかし、この出力には2つ問題がある。

1つ目はファイルの先頭にBOM … Byte Order Mark …がついている点だ。確かにこれがあれば確実にUTF-8だと判るのだが、私の知る限り perl が標準入力から読み込むファイルの操作に関して、BOMを適切に処理できる、と言う話は聞かない。だから、BOM…この場合はファイル先頭の3byteなのだが…を削らなくてはいけない。

2つ目はCSVそのものにある。いや、もちろん、CSVだって悪いわけじゃない。もし1つのValueの中にカンマで分割された文字列が入っているのでなければ。

perfmon.exe の記録のいくつかのフィールドが、それ自体カンマで区切った複数フィールドから成り立っているのだ。例えば Detail というフィールドなんかがそうだ。たとえば:
"User Time: 0.0781250, Kernel Time: 0.0312500, Private Bytes: 1,806,336, Working Set: 5,439,488"

とか(これは Process Profiling をやっているときの記録だな)、
"Desired Access: Read Attributes, Disposition: Open, Options: Open Reparse Point, Attributes: n/a, ShareMode: Read, Write, Delete, AllocationSize: n/a, Impersonating: S-x-x-xx-xxxxxxxxx-xxxxxxxxx-xxxxxxxxx-xxxx, OpenResult: Opened"

(これはファイルに対する IRP_MJ_CREATE のDetailだな)、このように1つのエントリ自体が複数のカンマで区切られたフィールドになっている。判ると思うが、各エレメントは " で囲まれている。

すると、単純にカンマをセパレータにして split をするとエライ目に合うわけだ。そこで、CSVをTSV… Tab Separated Values …形式にする。" で囲まれた領域に TAB は含まれていないので split をしかけるときにセパレータを /\t/ と定義すれば、あっさりと正しく split できるようになる。ファイルをこの形式にしておく事で、この後何度も統計処理や絞込みのために行う splitting rule を簡素化できるわけだ。

TSVにするフィルター自体はすごく簡単に作れる。これがコードだ:
tail --bytes=+4 input.CSV |\
sed -r -e 's/\r$//g' \
-e 's/\",\"/\"\t\"/g'\
> output.TSV


最初の tail --bytes=+4 は input.CSVの先頭3バイトを取り外す。これでBOMが無くなる。

GNU sed に与えた -r オプションは「拡張正規表現」を使えるようにする、というものだ。実際の正規表現はその次の2つの -e で始まるものになる。

's/\r$//g' は行末の 0x0d を取り外しているだけだ。ようするに改行コードを unix 形式になおしている。

's/\",\"/\"\t\"/g'は 『","』 となっている場合の , を TAB に入れ替える。ここには、procmon は「"," というフィールドデータを吐かない」という前提を使っている。少なくとも今の所、見たことは無い。多分大丈夫だろう。




ちなみに。TSVに直した後も、各エレメントは " で囲まれたままだ。で、 " で囲まれたエレメントを処理するときには「まだ」面倒な問題が残っている。

" で囲まれた中で " を表現するには『""』のように"を2回連続しなくてはいけない。

で、「そのプログラムはどのように起動したのか」を説明する「Command Line」フィールドに絶対パスが例の『Program Files』を含む場合、このようになってしまうのだ:


"""C:\Program Files\IBM\Director\websrv\dirwbs.exe"""


一番外側の " は「フィールド」を囲む " だが、その内側の "" は実際には1つの " を " で囲まれたフィールド内で表現するためのものだ。

この点だけは注意した方がよい。

2008年8月30日

何かを見つけた、その後

おっとっと、忘れてた。

仮に、問題のポイントと相関が高いカウンターが見つかったとしよう。それで終わってくれるならば特にどうと言うことは無いだろうが、普通は
「で、それは何をどうしているって??」
と言うことを確認しなくてはいけない。もし障害分析を仕事でやっている場合ならばお客様には
「r2が 0.43 なのでこいつが問題点です」
という1行よりはもう少し親切な…そう例えばグラフ化するとか…してあげなくてはいけない。

そこで、自分が「これ」と決めたカウンターに関して、Excelシートを作る事を考えよう。

もちろん、対象となるカウンターがもう決まっているのだから、relog.exe に対象カウンター一覧を食わせて TSV ファイルを作り、それをそのまま Excel に飲ませるのだってOKだ。
ただ、このやり方だと、例えば対象となる Performance Monitor のログが30個あると、30回「新しくシートを作って、そこにTSVファイルを読み込ませて」を繰り返さなくてはいけない。はっきり言おう。私は嫌だ。

と言うわけで。Excelシートを作ってくれるスクリプトだ。

toxls.pl

#!/usr/bin/perl
# Output xls file name is described as ARGV[0];
# ARGV[1] and on should contian
# ( ARGV[odd], ARGV[odd+1] ) = ( TSV filename, sheet name ).
# And at least ARGV[1] and ARGV[2] should be given in order to have something meaningful.
# Or, you'll get empty Excel file.


use Spreadsheet::WriteExcel;
use Spreadsheet::WriteExcel::Utility;

$workbookname = shift @ARGV;
if ( !defined( $workbookname ) ) {
die "Excel WorkBook name not given\n";
}

my $workbook = Spreadsheet::WriteExcel->new($workbookname);


@sheetnames = @ARGV;

while( $sheetfilename = shift @ARGV ) {
$sheetname = shift@ARGV;
if ( !defined( $sheetname )) {
die "Excel sheetname not given for TSV file name: $sheetfilename\n";
}

readsheet( $sheetname, $sheetfilename );
}

$workbook->close();
exit 0;


sub readsheet {
my ( $sheetname, $sheetfilename ) = @_;

my $worksheet = $workbook->add_worksheet( $sheetname );
open SHEETFILE, $sheetfilename or die "unable to open filename: $sheetfilename\n";


my $linenumber = 0;
my $offset = 5;
my $maxcells = 0;

while () {
chomp;

my @cells = split /\t/;

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

if ( $linenumber == 0 ) {
$column = $linenumber;
} else {
$column = $linenumber + $offset;
}

for ( $i = 0; $i <= $#cells; $i++ ) {
$cells[$i] =~ /^\"(.+)\"/;
my $tmp = $1;
$worksheet->write( $column, $i, $tmp );
}
$linenumber++;
}

$maxlinenum = $linenumber + $offset;

for ( $row = 0; $row <= $maxcells; $row++ ) {

# Since we need to skip first element, headofdata requires to have +1.
my $headofdata = $offset+1 +1;
my $tailofdata = $maxlinenum - 1;

my $ydatatop = xl_rowcol_to_cell( $headofdata, $row );
my $ydatatail = xl_rowcol_to_cell( $tailofdata, $row );

my $xdatatop = xl_rowcol_to_cell( $headofdata, 1 );
my $xdatatail = xl_rowcol_to_cell( $tailofdata, 1 );


my $formulacol = 1;
my $formularow = $row;
if ( $row == 0 ) {
$worksheet->write_string( $formulacol, $formularow, 'R2' );
} elsif ( $row == 1 ) {
$worksheet->write_formula( $formulacol, $formularow, "=RSQ( $ydatatop:$ydatatail, $xdatatop:$xdatatail)" );
} else {
$worksheet->write_formula( $formulacol, $formularow, "=RSQ( $ydatatop:$ydatatail, $xdatatop:$xdatatail)" );
}


$formulacol = 2;
$formularow = $row;
if ( $row == 0 ) {
$worksheet->write_string( $formulacol, $formularow, 'a' );
} elsif ( $row == 1 ) {
# do nothing
} else {
$worksheet->write_formula( $formulacol, $formularow, "=SLOPE( $ydatatop:$ydatatail, $xdatatop:$xdatatail)" );
}


$formulacol = 3;
$formularow = $row;
if ( $row == 0 ) {
$worksheet->write_string( $formulacol, $formularow, 'b' );
} elsif ( $row == 1 ) {
# do nothing
} else {
$worksheet->write_formula( $formulacol, $formularow, "=INTERCEPT( $ydatatop:$ydatatail, $xdatatop:$xdatatail)" );
}


$formulacol = 4;
$formularow = $row;
my $r2 = xl_rowcol_to_cell( 1, $formularow );
if ( $row == 0 ) {
$worksheet->write_string( $formulacol, $formularow, 'High Relation' );
} elsif ( $row == 1 ) {
# do nothing
} else {
$worksheet->write_formula( $formulacol, $formularow, "=IF( $r2>=0.25,\"*\",\"\")" );
}
}

close SHEETFILE;
}


ヘルプも何もありゃしない :p 使い方としてはこんな感じ:


% toxls.pl AFO.xls A.TSV A-dayo B.TSV B-desu


するってーと AFO.xls が作られる。開くと見えるのはこんな感じだ。



タブを見ると「A-dayo」「B-desu」というのがあるのが判るだろう。それぞれ A.TSV, B.TSVの中身がシートになった状態になっている。

こいつにはもう一つおまけの機能がある。2行目から4行目がそれぞれ「r2」「y=ax+bのa」「y=ax+bのb」になっている。これらは Perlで算出したものではなく、Excelの「式」が書いてあってあなたがこのExcelシートを開いたときに値を再計算してくれた状態なのだ。

5行目には「もしr2の値が0.25より大きかったら * を、そうでなければ何も表示しない(エラーの場合を除く)」という式が埋めてある。単なる目印のようなものだが、たまに便利だったりする。

2008年8月26日

15個に分類するには

sort.pl が出してくる3つのファイルを、マシン2台分用意してAとBの邂逅の図1のように分類するのに必要なツールは、実は簡単なものでいい。

今回はcommon.plとonlyFirst.plという2つのプログラムを作り、それを使うことにしよう。
common.plは2つのファイルを受け取り、その両方に存在するカウンター名を出力する。
onlyFirst.plは同じく2つのファイルを受け取るが、最初のファイルにあって2つ目のファイルには存在しないカウンター名を出力する。

「diffコマンドじゃ駄目なのか?」
という声が聞こえてきそうだが、実は駄目だ。diffは厳密な差分を作るわけではない。あまりにも複雑な差分が出てくると、その近辺をまとめて「えいやっ」と全部違うことにしてしまう。パッチを作るのには便利だが、本当の意味で差分を知りたいときには役に立たないのだ。




おっとっと。sort.pl の出力例を示そう。

"\System\Threads"
# "-0.22995779821056035196" "0.05288058895784879381" "-0.07730117409966560384" "143.68969576542572045539"
# "-0.51157343991699204758" "0.26170738442850427247" "-0.28398973957007304341" "268.04879224402808287292"
# "-0.54019071369820630440" "0.29180600716577749228" "-0.44072471855335868898" "364.09672304650726120687"
# "-0.55452955075971981685" "0.30750302266577667691" "-0.35677693634035859957" "315.68199698065233504727"
# "-0.56238435856552132237" "0.31627616675915285595" "-0.25723384667083141975" "251.10319887283403001070"
"\System\System Up Time"
# "-0.01694272285973713476" "0.00028705585790185927" "-0.00002142521429336236" "319.75744872321046887814"
# "-0.10027028487321032493" "0.01005413002855475131" "-0.00006496869460687431" "814.02214841113016417319"
# "-0.15561543326229096847" "0.02421616306941053433" "-0.00009270518792943096" "1140.66520449120347153620"
# "0.02950515281233018273" "0.00087055404247895569" "0.00002478604557354800" "-190.27570068419384733538"
# "0.64118189188208415717" "0.41111421847748865761" "0.00006133456719050028" "-615.26230486597317302122"
"\System\System Calls/sec"
# "-0.18729246211596788708" "0.03507846636546126618" "-0.00055434531116103602" "100.21166379708057100480"
# "-0.29232961498316379384" "0.08545660379620478167" "-0.00111680437443534113" "93.05619900302319881495"
# "-0.34131479803199158732" "0.11649579135561920833" "-0.00090462856546019161" "90.75926531940870958778"
# "-0.34750948339695522578" "0.12076284105081869973" "-0.00149413414952154013" "87.55393220941927354234"
# "-0.51114894911397353620" "0.26127324818031950750" "-0.00296996323726849211" "104.08031976970305210654"


# で始まっている行はコメントだ。この行はデバッグ用に各カウンターから算出されるr, r2, a, b の4つの値を記録しているだけで、この段階では何の意味も無い。これは処理の前に grep -v で消してしまおう。


% cd $TOP
% cd $TOP/06rmComment
% cat filter.sh
#!/bin/sh

for i in ../05sort/*.{HI,BORDER,LOW}; do
o=`echo $i | sed 's/\.\.\/05sort\///g'`
echo $o
egrep -v '^#' $i | sort | uniq > $o
done
% ./filter.sh

sort はデバッグしやすくするためだが(アルファベット順に並んでいると間違ってるかどうか探しやすい)、uniq は「ついカッとなってやった」の類だ。無くても構わない。




では。まず2つのファイルに共通しているカウンター名を引っ張り出すスクリプト。

common.pl

#! /usr/bin/perl

# common.pl
# read and , line by line.
# if common lines were found regardless of it's order, output it to STDOUT.

$inAfn = $ARGV[0];
$inBfn = $ARGV[1];

open( INA, $inAfn ) or die "can't open file $inAfn as read\n";
open( INB, $inBfn ) or die "can't open file $inBfn as read\n";

while () {
chomp;
push @inAline, ( $_ );
}

while () {
chomp;
push @inBline, ( $_ );
}

close INA;
close INB;


while ( $line = pop @inAline ) {
# find same line in @inBline;
for ( $i = 0; $i <= $#inBline; $i++ ) {
if ( $inBline[$i] eq $line ) {
print "$line\n";
}
}
}

えぇ、自分で言うのもなんだが、すげぇ馬鹿コードである。共通要素を見つけた後、next で次に行かないのもどうだかと思うし、inBline から見つかった要素を消さないのもどうだかと思う。

次は片一方にしかないカウンターを見つけるスクリプト。

onlyFirst.pl

#! /usr/bin/perl

# onlyFirst.pl
# read and , line by line.
# if line from does not exist in line from then print that to STDOUT.

$inAfn = $ARGV[0];
$inBfn = $ARGV[1];

open( INA, $inAfn ) or die "can't open file $inAfn as read\n";
open( INB, $inBfn ) or die "can't open file $inBfn as read\n";

while () {
chomp;
push @inAline, ( $_ );
}

while () {
chomp;
push @inBline, ( $_ );
}

close INA;
close INB;

A_line:

while ( $line = pop @inAline ) {
# find same line in @inBline;
for ( $i = 0; $i <= $#inBline; $i++ ) {
if ( $inBline[$i] eq $line ) {
next A_line;
}
}
print "$line\n";
}

ふ。負けず劣らずこちらも馬鹿コードである orz




% cd $TOP
% cd $TOP/07common
% cat filter.sh
##############################################
# re-classify into matrix
#
# B \ A | | | |
# \ | HIGH | BORDER | LOW |
#------------+------+--------+-----+
# HIGH | HH | HB | HL |
#------------+------+--------+-----+
# BORDER | BH | BB | BL |
#------------+------+--------+-----+
# LOW | LH | LB | LL |
#------------+------+--------+-----+

./common.pl ../06rmComment/A.HI ../06rmComment/B.HI > HH
./common.pl ../06rmComment/A.HI ../06rmComment/B.BORDER > BH
./common.pl ../06rmComment/A.HI ../06rmComment/B.LOW > LH

./common.pl ../06rmComment/A.BORDER ../06rmComment/B.HI > HB
./common.pl ../06rmComment/A.BORDER ../06rmComment/B.BORDER > BB
./common.pl ../06rmComment/A.BORDER ../06rmComment/B.LOW > LB

./common.pl ../06rmComment/A.LOW ../06rmComment/B.HI > HL
./common.pl ../06rmComment/A.LOW ../06rmComment/B.BORDER > BL
./common.pl ../06rmComment/A.LOW ../06rmComment/B.LOW > LL

for i in HI BORDER LOW; do
./onlyFirst.pl ../06rmComment/A.$i ../06rmComment/B.HI > ./tmp1
./onlyFirst.pl ./tmp1 ../06rmComment/B.BORDER > ./tmp2
./onlyFirst.pl ./tmp2 ../06rmComment/B.LOW > ./onlyA.$i
done

for i in HI BORDER LOW; do
./onlyFirst.pl ../06rmComment/B.$i ../06rmComment/A.HI > ./tmp1
./onlyFirst.pl ./tmp1 ../06rmComment/A.BORDER > ./tmp2
./onlyFirst.pl ./tmp2 ../06rmComment/A.LOW > ./onlyB.$i
done

% ./filter.sh


C,Dの処理は省略した。

はっきり言ってこれでもかと言うぐらいの力押しスクリプトだ。よい子はまねをしてはいけない(ぇ…)。



というわけで、これで機械的に出来る分類は終わった。
ココから先は、人間の推理力がモノを言う。

実際、ここにある perl のスクリプトはほぼそのまま私が使っているスクリプトだ。シェルスクリプトの方は時々に応じて変更しているのでこれこのままではないが。で、今まで発見できなかった要因をいろいろ見つけることが出来ている。

性能障害問題で悩んでいるなら、きっと役に立つと思う。少なくとも同じ考え方は使えるはずだ。

2008年8月24日

AとBの邂逅

ここまでは、各マシン毎の分類だった。ここからはマシンごとの特徴を見た上での分類が必要になる。要点としては図1のように分類する事になる。

 A only HIA only BORDERA only LOW
Aマシンのカウンター
B only HIBマシンのカウンター(A,B)=(HI,HI)(A,B)=(BORDER,HI)(A,B)=(LOW,HI)
B only BORDER(A,B)=(HI,BORDER)(A,B)=(BORDER,BORDER)(A,B)=(LOW,BORDER)
B only LOW(A,B)=(HI,LOW)(A,B)=(BORDER,LOW)(A,B)=(LOW,LOW)
図1. AとBの分類


まず最も判りやすいところから説明しよう。中央の紫色の3x3=9枡目は、AとBの両方で存在した(NaNによる除去を免れた)カウンターだ。A, B それぞれにおいて、どう分類されていたのか、に応じて3x3通りが存在する。

薄い黄色で塗られた3つの枡目、「A only xxxx」とあるのは『Aには存在したがBには存在しなかった』カウンターを現す。同様に「B only xxxx」とある水色の3つの升目は『Bには存在したが、Aには存在しなかった』カウンターを現す。

生き残ったカウンターは以上15個の升目のどこかに必ず入る。まずこの分類を行う。

さて、これらの升目の内、重要視するのは図2で示した6枡である。


 A only HIA only BORDERA only LOW
Aマシンのカウンター
B only HIBマシンのカウンター(A,B)=(HI,HI)(A,B)=(BORDER,HI)(A,B)=(LOW,HI)
B only BORDER(A,B)=(HI,BORDER)(A,B)=(BORDER,BORDER)(A,B)=(LOW,BORDER)
B only LOW(A,B)=(HI,LOW)(A,B)=(BORDER,LOW)(A,B)=(LOW,LOW)
図2. 重要視するポイント


症状はAでは発生しているが、Bでは発生していない。そして問題が生じているときにはプロセッサが忙しく働いている。ならば、「AのときはIdleと強い相関があり、Bの時にはIdleとの相関が弱い」所に探すべきネタがある。

Aにしか存在しないカウンターの内、相関がLOWのもの以外の2つは、当然検討対象にしなくてはいけない。これは判ると思う。逆にBにしか存在しないカウンターは無視して構わない。

A,Bともに存在したカウンターの内、Aにおいて相関がLOWだったものは、この場合検討対象から外してよい。

Bにおいて相関がHIだったものも、この場合検討から外してよいはずだ。AもHI,BもHIならそれは「いつでも相関が高いもの」…例えば、プロセッサ消費量の内の User の割合など…であることが考えられる。また、AがHIで、BがHIではないのでは明らかに今回探している対象から外れるのは自明だ。

残った6枡の内、A,Bともに BORDERなものは特別扱いが必要になる。BORDERにあるのは計測すると相関が高かったり高く無かったりしたものだ。つまり「AもBも、相関が高いんだか低いんだかよく判らん」ものだったりする。この場合、各カウンターの内容を調べて、個別に判断する必要が出る。しかし、逆に言うとここは手間がかかるところなので、それ以外の部分で何か見つかった場合は無視して構わない、ともいえる。

と言うわけで、残り5枡を最優先で調べるべし、と言うことになる。

2008年8月16日

sort.pl の出力

先に進む前に sort.pl の出力についてもう少し説明する。

sort.pl が入力として受け付ける(前提としている)のは、calcr2.pl の出力を複数 cat でつないだもの。今回は sort もかけることで同じカウンターが近接するようにしてあるが、本質的には sort の必要は無い。結果として、このような内容が延々続くファイルになる:


"\LogicalDisk(C:)\% Disk Read Time" "-0.04062578806352645403" "0.00165045465578256851" "-0.12770872885354865610" "90.45804378385737658023"
"\LogicalDisk(C:)\% Disk Read Time" "-0.04406606083496136481" "0.00194181771751051590" "-0.17957809895894711362" "90.36202077972433047033"
"\LogicalDisk(C:)\% Disk Read Time" "-0.10162240553099880448" "0.01032711330590677641" "-0.25960115664153218127" "91.91528872471089858070"
"\LogicalDisk(C:)\% Disk Read Time" "-0.10606592840069039460" "0.01124998116750038125" "-0.50117255489344281550" "89.78700245921672272973"
"\LogicalDisk(C:)\% Disk Read Time" "-0.63225077753384162361" "0.39974104569214729193" "-0.98409431769226710436" "99.62442055641675669955"
"\LogicalDisk(C:)\% Disk Time" "-0.04336533750022928025" "0.00188055249650879188" "-0.11305679326692022027" "90.53746144831214471477"
:


r2の値を使ってこれらをソートしていくわけだが、全ての計測結果においてr2が同じ性質を持つわけではない。実はここでは4種類に分類している。出力しているファイルは3種類だが、実際には分類は4種類あるのだ。

そこで、まず個々のr2の値はどのような値をとりえるのか、考えて見よう。

r2のとる値


すでに述べたが、r2は0.0から1.0までの間の値を取る。正確には計算できる場合は、それらの間の値をとる。そう。計算できない場合、と言うのがあるのだ。

指定したカウンターに値が全く記録されていない場合がありえる。この場合、データが全くないので、回帰直線も引けない。つまり y=ax+b という形のaとbも求められない。rの値は a と b がないと計算できないのでrも求められないし、その二乗値も求められない。
この場合、r, r2, a, b は全て Not a Number (NaN) となる。

rやr2が NaN になるケースがもう一つある。a が 0.0 になった場合だ。厳密には一定の誤差の範囲内であれば NaN になってしまう。rの計算式には a による除算が含まれているのだが、ここで 0割り(0を分母とする分数の値を計算しようとした時に生じるエラー)が起こるのだ。この場合、r, r2は NaN になるが、a は 0.0 になるし、bはどんな値になるかはデータ依存だ。

以上の2つのケース以外の場合、rの値は 0.0 から 1.0 の間の値を取る。厳密には、計算誤差のせいで 1.0 を超える数字が出たり、0.0を下回る数字が出たりすることもあるが、あくまでも誤差の範囲、と捉えてもらって大丈夫だ。

r2の値による分類


さて、今回、sort.pl でカウンターを分類するにあたって次のような規則を適用した。


1) 同じカウンターに属する、全てのr2の値が NaN ならば、そのカウンターは記録しない。

データが一切ないのであれば、そのカウンターは考慮に値しない。

データがあるが a が 0.0 になる、と言うことは Processor Idle の値に全く影響を受けてない/影響を与えていない、と言うことだ。今回は影響のあるものを探しているのだから、そのカウンターについて、以降考慮する必要は無い。
Process ID のように変わるはずの無いカウンターが該当する。

2) 同じカウンターの一部だけが NaN の場合、そのNaNは無視する。

取りあえず残りのデータだけで分析しよう、と言うことだ。

3) 全ての r2が境界値以上だったらHI

4) 全ての r2が境界値未満だったらLOW

5) r2が境界値以上、未満の両方存在したら、BORDER



なので、全てNaNの場合のカウンターがこの段階で消えている。カウンターの総数は分類前と分類後では、分類後の方が少ない可能性がある、ということだ。

2008年8月12日

相関係数によってカウンターを分類する

カウンターごとに相関が求まったので、それを元にカウンターを分類する。

その前に。カウンターにくっついているマシン名がうざったい。

"\\A\Processor(_Total)\% Idle Time" "0.99999999999999558689" ....


というこの「\\A\」がうざったいのだ。と言うのは、後で「マシン同士の」相関を取る必要があるからだ。カウンター名にマシン名が含まれていては「同じカウンター同士」を判別するのが面倒になる。だからこの場で外してしまおう。

そしてついでに、同じマシンに関して、別の日付のものをあわせて1つにしてしまおう。


次に。各TSVファイルを分類する。分類するスクリプトはこれ:

sort.pl
$r2_limitation = 0.25;

open( INFILE, "$ARGV[0]" ) or die "can't open file $ARGV[0] as read\n";
open( OHI, " > $ARGV[1]" ) or die "can't open file $ARGV[1] as write\n";
open( OBO, " > $ARGV[2]" ) or die "can't open file $ARGV[2] as write\n";
open( OLO, " > $ARGV[3]" ) or die "can't open file $ARGV[3] as write\n";

while () {
chomp;
my @cells = split /\t/;
my $i, $n;

for ( $i = 0; $i <= $#cells; $i++ ) {
$cells[$i] =~ s/"(.*)".*$/\1/g;
}

$n = $cells[0];
if ( $n eq "name" ) {
next;
}
if ( $counter{$n} <= 0 ) {
push @name, $n;
}

$i = $counter{$n};
$counter{$n}++;
$r{$n}[$i] = $cells[1];
$r2{$n}[$i] = $cells[2];
$a{$n}[$i] = $cells[3];
$b{$n}[$i] = $cells[4];
}

while ( $n = pop @name ) {
my $allbig = 1;
my $allsmall = 1;
my $findNaN = 0;

for ( $i = 0; $i < $counter{$n}; $i++ ) {
if ( $r2{$n}[$i] < $r2_limitation ) {
$allbig = 0;
}
if ( $r2{$n}[$i] >= $r2_limitation ) {
$allsmall = 0;
}
if ( $r2{$n}[$i] eq "NaN" ) {
$findNaN = 1;
}
}

if (( $allbig )&&( !$allsmall )) {
print OHI "\"$n\"\n";
for ( $i = 0; $i < $counter{$n}; $i++ ) {
print OHI "# \"$r{$n}[$i]\"\t\"$r2{$n}[$i]\"\t\"$a{$n}[$i]\"\t\"$b{$n}[$i]\"\n";
}
}

if (( !$allbig )&&( $allsmall )) {
print OLO "\"$n\"\n";
for ( $i = 0; $i < $counter{$n}; $i++ ) {
print OLO "# \"$r{$n}[$i]\"\t\"$r2{$n}[$i]\"\t\"$a{$n}[$i]\"\t\"$b{$n}[$i]\"\n";
}
}

if (( !$allbig )&&( !$allsmall )) {
print OBO "\"$n\"\n";
for ( $i = 0; $i < $counter{$n}; $i++ ) {
print OBO "# \"$r{$n}[$i]\"\t\"$r2{$n}[$i]\"\t\"$a{$n}[$i]\"\t\"$b{$n}[$i]\"\n";
}
}
if (( $allbig )&&( $allsmall )) {
if ( $findNaN ) {
print STDERR "\"$n\" seems to have NaN as it's r2 value\n";
} else {
die "\"$n\" did not have any element in $ARGV[0]?!\n";
}
}
}


引数は4つ。
1つ目が入力TSVファイル名。
2つ目は $r2_limitation 以上のr2値を持っているカウンターだけを集めたTSVファイルの名前。
4つ目は $r2_limitation 未満のr2値を持っているカウンターだけを集めたTSVファイルの名前。
3つ目は r2の値が $r2_limitation 以上だったり以下だったりふらつくものを集めたTSVファイルの名前。

とりあえず $r2_limitation はSTATISTICS HACKの推奨にあわせて「強い相関」の値0.25に設定してあります。

% cd $TOP
% cd $TOP/05sort
% cat filter.sh
#!/bin/sh

onebefore="../04Colleration";
for i in A B C D; do
cat $onebefore/$i.*.tsv | sed -e 's/^"\\\\[[:upper:][:digit:]]\+\\/"\\/g' | sort > $i.lst
done

for i in A B C D; do
./sort.pl $i.lst $i.HI $i.BORDER $i.LOW
done
% bash filter.sh


これで A.HI, A.BORDER, A.LOW や B.HI, B.BORDER, B.LOW ができた。

一番計算機パワーを食うところはこれでおしまい。ここから先は人間が悩むしかない…。

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時間ほどかかりました。もっとメモリとプロセッサパワーが欲しいです。

2008年8月4日

一覧の出現順序をいじったら…TSV化だっ

一覧から
\\A\Processor(_Total)\% Idle Time

とかを切り出して、一番最初に出てくるようにする。

% cd $TOP
% cd $TOP/02LST
% cat filter.sh
for i in A B C D; do
cat ../01PQRlst/$i* | fgrep 'Processor(_Total)\% Idle Time' | sort | uniq > ./$i.lst
cat ../01PQRlst/$i* | fgrep -v 'Processor(_Total)\% Idle Time' | sort | uniq >> ./$i.other
done
% bash filter.sh

大事なのは Sort を掛けてある事と、各マシンについて日付をまたいでリストをマージしている事。

Sortをかけているのは、リストのエントリが「大雑把に何\細かく言うと何」という形式に従っているから、と言うのが一つ。似た項目が集まるように。もう一つは uniq をかけるため。




さて、とりあえずここまできたら、blg ファイルの中身を TSVファイルに変換しよう。
先ほど作った A.lst, B.lst を relog.exe に適用すれば、そこで指定した順番でカウンターが出力される。

% cd $TOP
% cd $TOP/03TSV
% cat filter.sh
for i in A B C D; do
for j in $TOP/native_blg/$i*.blg; do
outfile=`echo $j | sed -e 's/^.*\//g' -e 's/\.blg$/.tsv/g'`
relog.exe $j -f TSV -cf $TOP/02LST/$i.lst -o $outfile;
done
done
% bash filter.sh


これで $TOP/03TSV/ 下に欲しい順序で、各 blg ファイルの中身が TSV の形式で格納されたわけだ。ただし、3500列 480行もあるファイルが4個 x サーバ4種類の合計16個もあり、このTSVファイルを Excel に食わせようとすると、Excel側がギブアップするってぐらいのおチャメさんなファイルになったわけだが…