こんにちは。次のサンプル データの 1 時間ごとの平均を計算したいと思います。
Timestamp,data1,data2
2018 07 16 13:00:00,23,451
2018 07 16 13:10:00,26,452
2018 07 16 13:20:00,24,453
2018 07 16 13:30:00,23,454
2018 07 16 13:50:00,28,455
2018 07 16 14:20:00,20,456
2018 07 16 14:40:00,12,457
2018 07 16 14:50:00,22,458
2018 07 16 15:10:00,234,459
2018 07 16 17:50:00,23,845
2018 07 16 18:10:00,239,453
2018 07 17 10:10:00,29,452
2018 07 18 13:20:00,49,451
2018 07 19 13:30:00,28,456
希望する出力:
Date,Hour,Ave_data1,Ave_data2
2018 07 16,13,24.8,453
2018 07 16,14,18,457
2018 07 16,15,234,459
2018 07 16,17,23,845
2018 07 16,18,239,453
2018 07 17,10,29,452
2018 07 18,13,49,451
2018 07 19,13,28,456
データは数日間(100000 件以上のレコード)続き、データ列は変化し、2 列以上(data1、data2、...、dataX など)になることもあることに注意してください。そのため、列がさらに多くてもスクリプトで計算を実行できるようにしたいと思います。ご協力いただければ幸いです。
追記: これを投稿する前に、古い投稿を確認しましたが、私の問題は実際には解決されていませんでした。
答え1
#!/usr/bin/perl
use strict;
my $prev = '';
my (@sums,@avg) = ();
my $count = 0;
while(<>) {
chomp;
if (m/^Timestamp/) {
my @headers = split /,/;
# insert "Ave_" at start of each header
@headers = map { "Ave_" . $_ } @headers;
# replace Timestamp header with Date,Hour headers.
splice @headers,0,1,qw(Date Hour);
print join(",",@headers), "\n";
next;
};
my (@data) = split /,/;
# extract and remove date and hour from first element of @data
(my $current = shift @data) =~ s/^(.*) (\d\d):.*$/$1,$2/;
if ($count == 0 || $current eq $prev) {
# add each field in @data to the same field in @sums
foreach my $i (0..$#data) { $sums[$i] += $data[$i] };
$prev = $current;
$count++;
next unless eof;
};
# calculate and print the averages for the previous hour
foreach my $i (0..$#sums) { $avg[$i] = $sums[$i] / $count };
print join(",", $prev, @avg), "\n";
# special case handling for when there's a new date/hour on the
# last line of file (otherwise it wouldn't get printed)
if (eof && $prev ne $current) {
print join(",", $current, @data), "\n";
};
@sums = @data;
@avg = ();
$prev = $current;
$count = 1;
};
これは任意の数のデータ フィールドで機能するはずです。
たとえば、 として保存しaverage.pl
、 で実行可能にしてchmod +x average.pl
、次のように実行します。
$ ./average.pl input.csv
Date,Hour,Ave_data1,Ave_data2
2018 07 16,13,24.8,453
2018 07 16,14,18,457
2018 07 16,15,234,459
2018 07 16,17,23,845
2018 07 16,18,239,453
2018 07 17,10,29,452
2018 07 18,13,49,451
2018 07 19,13,28,456
map
Perl とループとイテレータに関する非常に興味深い (IMO) 内容:
ちなみに、ループは Perl の関数foreach my $i ...
を使って書き直すこともできます( を参照してください。簡単に言うと、リストを反復処理し、各要素に対して処理を行い、新しく生成されたリストまたはその生成されたリストの要素数を返します)。これは Perl の慣用表現ですが、Perl の初心者プログラマーにとっては理解しにくいかもしれません。例:map
perldoc -f map
map
foreach my $i (0..$#data) { $sums[$i] += $data[$i] };
could be written as:
@sums = map { $sums[$_] + $data[$_] } 0..$#data;
これらは両方とも、インデックス@data 配列 ( 0..$#data
) の for ループは @sums の要素を直接作成/変更し、 はmap
新しい合計の配列を返し、それが @sums 配列に割り当てられます。
$i
関数は、反復変数として を使用する代わりに、map
と呼ばれる(ローカル化された)スカラー変数を自動的に作成して使用します$_
。 $_
は Perl のあらゆる場所で使用され、引数が指定されていない場合のほとんどの関数に対する暗黙の(つまりデフォルトの)引数です。たとえば、print
引数がない場合、 は実際には でありprint $_
、split /,/
は実際には ですsplit /,/, $_
。また、パターン マッチング演算子に対しても暗黙的です。たとえば、s/foo/bar
は実際には です$_ =~ s/foo/bar/
。
同様に、while (<>)
実際には次のようになりますwhile (defined($_ = <>))
(つまり、入力ファイルまたは標準入力から 1 行を読み取り、読み取るものがあればそれを $_ に割り当てて true として評価します。それ以外の場合は false として評価してwhile
ループを終了します)。
$_
は非公式に「現在のもの」または「もの」と呼ばれることがよくあります。詳細については、 を参照しman perlvar
、検索してください。また、サブルーチンに渡されるパラメータに使用される、\$_
同等の配列 もあります。@_
foreach my $i (0..$#sums) { $avg[$i] = $sums[$i] / $count };
could be written as:
@avg = map { $_ / $count } @sums;
ここで、foreach
ループはインデックス@sums( 0..$#sums
)のmap
繰り返し処理は、価値観配列の@sums
。ここでも、foreach
ループは配列の各要素を@avg
直接変更し、map
は に割り当てられた新しい配列を返します@avg
。
このスクリプトではどちらの形式も同一の出力を生成し、どちらの形式も便利ですが、Perl プログラマーは、map
あらゆる種類のリストを反復処理するための汎用的に便利なツールであるため、時間の経過とともに使用する傾向があります。また、同じことを行う for/foreach ループよりも入力が短くなります。また、しばらくすると、リスト、配列、ハッシュの観点からデータについて考えることが自然になるためです。
これは、配列をハッシュに変換する場合 (またはハッシュの値またはキーを配列に変換する場合) によく使用されます。
ちなみに、map
配列を返す必要はなく、コード ブロックでは{ ... }
Perl コードで実行できることはすべて実行でき、戻り値は破棄することも、(スカラー変数に割り当てられている場合) 生成されたリストの数を返すこともできます。
たとえば、最初の foreach ループは次のように記述することもできます。
map { $sums[$_] += $data[$_] } 0..$#data;
これにより、@sums 配列が直接変更され (foreach ループと同様に)、戻り値はすべて破棄されます (つまり、どの変数にも割り当てられません)。
もちろん、2 番目のforeach
ループは次のように記述することもできます。
map { $avg[$_] = $sums[$_] / $count } 0..$#sums;
答え2
離れGNU awk
て:
#!/usr/bin/awk -f
BEGIN {
FS=OFS=","
}
NR == 1 {
# Build the header here
for (i = 2; i <= NF; i++) oh = oh OFS "Ave_" $i
print "Date", "Hour" oh
next
}
{
# Split date and time and build a timestamp with it.
# Set MM and SS to 0 to aggregate data from the same hour
split($1, a, " ")
sub(/:.*/, "", a[4])
ct = mktime(a[1] " " a[2] " " a[3] " " a[4] " 00 00")
# If the 'current time' differ from the 'old time' then
# do the average and print the line
if (ct != ot && ot) {
for (i in avg){
avg_h = avg_h OFS (avg[i] / cnt[i])
delete avg[i]
delete cnt[i]
}
sub(/^,/, "", avg_h)
print cd, ch, avg_h
avg_h = ""
saved = 0
}
j = 0
for (i = 2; i <= NF; i++) {
avg[j] += $i
cnt[j++] += 1
}
# Do the assignment if and only something has changed
if (!saved) {
saved = 1
ot = ct
cd = a[1] " " a[2] " " a[3]
ch = a[4]
}
}
END {
# There are something else? Print it
for (i in avg)
avg_h = avg_h OFS (avg[i] / cnt[i])
sub(/^,/, "", avg_h)
print cd, ch, avg_h
}
実行者:./script.awk data