Guten Tag, ich möchte für folgende Beispieldaten Stundenmittelwerte berechnen:
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
gewünschte Ausgabe:
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
Bitte beachten Sie, dass die Daten mehrere Tage lang vorhanden sind (mehr als 100.000 Datensätze) und die Datenspalten variieren. Manchmal gibt es mehr als 2 Spalten (z. B. Daten1, Daten2, ..., DatenX). Daher möchte ich, dass das Skript Berechnungen durchführen kann, auch wenn mehr Spalten vorhanden sind. Ihre Hilfe wäre sehr willkommen.
PS: Bevor ich dies gepostet habe, habe ich alte Posts überprüft und sie gehen nicht wirklich auf mein Problem ein.
Antwort1
#!/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;
};
Dies sollte mit einer beliebigen Anzahl von Datenfeldern funktionieren.
Speichern Sie es beispielsweise unter, average.pl
machen Sie es mit ausführbar chmod +x average.pl
und führen Sie es wie folgt aus:
$ ./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
Besonders interessante (meiner Meinung nach) Dinge über Perl und map
Schleifen und Iteratoren:
Zu Ihrer Information: Die foreach my $i ...
Schleifen könnten umgeschrieben werden, um map
stattdessen die Funktion von Perl zu verwenden (siehe perldoc -f map
, aber kurz gesagt: map
iteriert über eine Liste, macht Dinge mit jedem Element und gibt entweder eine neu generierte Liste oder eine Anzahl der Elemente in dieser generierten Liste zurück). Dies ist idiomatischer für Perl, aber für neue Perl-Programmierer wahrscheinlich schwerer zu verstehen. zB
foreach my $i (0..$#data) { $sums[$i] += $data[$i] };
could be written as:
@sums = map { $sums[$_] + $data[$_] } 0..$#data;
Beide iterieren über dieIndizesdes @data-Arrays ( 0..$#data
). Die for-Schleife erstellt/ändert die Elemente von @sums direkt, während die map
ein neues Array von Summen zurückgibt, das dann dem @sums-Array zugewiesen wird.
Anstatt $i
als Iteratorvariable zu verwenden, map
erstellt und verwendet die Funktion automatisch eine (lokalisierte) Skalarvariable namens $_
. $_
wird überall in Perl verwendet und ist das implizite (d. h. Standard-)Argument für die meisten Funktionen, wenn kein Argument angegeben ist. z. B. print
ohne Argument ist tatsächlich print $_
, und split /,/
ist tatsächlich split /,/, $_
. Es ist auch implizit für Mustervergleichsoperatoren, z. B. s/foo/bar
ist tatsächlich $_ =~ s/foo/bar/
.
Ähnlich while (<>)
verhält es sich eigentlich in etwa so while (defined($_ = <>))
(d. h., lesen Sie eine Zeile aus der Eingabedatei oder von stdin, und wenn es etwas zu lesen gab, weisen Sie es $_ zu und bewerten Sie es als „true“. Andernfalls bewerten Sie es als „false“ und beenden Sie die while
Schleife).
$_
wird oft informell als „das aktuelle Ding“ oder „Dingsbums“ bezeichnet. Weitere Einzelheiten finden Sie unter man perlvar
und suchen Sie nach . Es gibt auch ein Array-Äquivalent , das für die an eine Subroutine übergebenen Parameter verwendet wird.\$_
@_
foreach my $i (0..$#sums) { $avg[$i] = $sums[$i] / $count };
could be written as:
@avg = map { $_ / $count } @sums;
Hier foreach
iteriert die Schleife über dieIndizesvon @sums ( 0..$#sums
), während die map
Iteration über dieWertedes @sums
Arrays. Auch hier foreach
ändert die Schleife jedes Element des @avg
Arrays direkt, während map
ein neues Array zurückgegeben wird, das zugewiesen wird @avg
.
Beide Formen erzeugen in diesem Skript die gleiche Ausgabe und sind beide nützlich, aber Perl-Programmierer verwenden es mit map
der Zeit immer häufiger, weil es ein allgemein nützliches Tool zum Durchlaufen beliebiger Listen ist. Und es ist kürzer zu tippen als eine for/foreach-Schleife, die dasselbe tut. Und weil es nach einer Weile ganz natürlich wird, über Ihre Daten in Form von Listen, Arrays und Hashes nachzudenken.
Es wird häufig verwendet, um ein Array in einen Hash umzuwandeln (oder die Werte oder Schlüssel eines Hashs in ein Array).
Übrigens map
muss kein Array zurückgegeben werden, der Codeblock darin { ... }
kann alles tun, was Perl-Code tun kann, und der Rückgabewert kann einfach verworfen werden oder (bei Zuweisung an eine Skalarvariable) die Anzahl aller generierten Listen zurückgeben.
Beispielsweise könnte die erste foreach-Schleife auch wie folgt geschrieben werden:
map { $sums[$_] += $data[$_] } 0..$#data;
Dadurch wird das @sums-Array direkt geändert (genau wie die foreach-Schleife) und alle Rückgabewerte werden verworfen (d. h. keiner Variablen zugewiesen).
Und natürlich foreach
könnte die zweite Schleife auch wie folgt geschrieben werden:
map { $avg[$_] = $sums[$_] / $count } 0..$#sums;
Antwort2
Weg 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
}
Rennen wie :./script.awk data