Berechnen von Stundendurchschnitten für mehrere Datenspalten

Berechnen von Stundendurchschnitten für mehrere Datenspalten

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.plmachen Sie es mit ausführbar chmod +x average.plund 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 mapSchleifen und Iteratoren:

Zu Ihrer Information: Die foreach my $i ...Schleifen könnten umgeschrieben werden, um mapstattdessen die Funktion von Perl zu verwenden (siehe perldoc -f map, aber kurz gesagt: mapiteriert ü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 mapein neues Array von Summen zurückgibt, das dann dem @sums-Array zugewiesen wird.

Anstatt $ials Iteratorvariable zu verwenden, maperstellt 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. printohne Argument ist tatsächlich print $_, und split /,/ist tatsächlich split /,/, $_. Es ist auch implizit für Mustervergleichsoperatoren, z. B. s/foo/barist 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 whileSchleife).

$_wird oft informell als „das aktuelle Ding“ oder „Dingsbums“ bezeichnet. Weitere Einzelheiten finden Sie unter man perlvarund 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 foreachiteriert die Schleife über dieIndizesvon @sums ( 0..$#sums), während die mapIteration über dieWertedes @sumsArrays. Auch hier foreachändert die Schleife jedes Element des @avgArrays direkt, während mapein 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 mapder 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 mapmuss 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 foreachkö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

verwandte Informationen