Добрый день, я хотел бы рассчитать почасовые средние значения для следующих выборочных данных:
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). Поэтому я хотел бы, чтобы скрипт мог выполнять вычисления даже при большем количестве столбцов. Ваша помощь будет высоко оценена.
PS: Перед тем как опубликовать это, я проверил старые сообщения, и они на самом деле не решают мою проблему.
решение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
Очень интересная (на мой взгляд) информация о Perl, map
циклах и итераторах:
FYI, foreach my $i ...
циклы можно переписать так, чтобы map
вместо этого использовать функцию Perl (см. perldoc -f map
, но вкратце: map
итерации по списку, выполнение действий с каждым элементом и возврат либо нового сгенерированного списка, либо количества элементов в этом сгенерированном списке). Это более идиоматично Perl, но, вероятно, сложнее для понимания для новых программистов Perl. Например:
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
без аргумента is actually print $_
, и split /,/
is actually split /,/, $_
. Он также неявный для операторов сопоставления с шаблоном, например, s/foo/bar
is actually $_ =~ s/foo/bar/
.
Аналогично, while (<>)
на самом деле это что-то вроде while (defined($_ = <>))
(т.е. прочитать строку из входного файла или stdin, и если там есть что читать, присвоить это $_ и оценить как истину. В противном случае оценить как ложь и завершить цикл 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), а любое возвращаемое значение отбрасывается (т.е. не присваивается никакой переменной).
И, конечно, второй 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