Buen día, me gustaría calcular promedios horarios para los siguientes datos de muestra:
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
salida deseada:
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
Tenga en cuenta que los datos duran días (más de 100 000 registros) y las columnas de datos varían, a veces hay más de 2 columnas (es decir, datos1, datos2,..., datosX). Entonces me gustaría que el script pudiera hacer cálculos incluso cuando haya más columnas. Tu ayuda será altamente apreciada.
PD: Antes de publicar esto, revisé publicaciones antiguas y realmente no solucionan mi problema.
Respuesta1
#!/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;
};
Esto debería funcionar con cualquier número de campos de datos.
Guárdelo como, por ejemplo, average.pl
, hágalo ejecutable chmod +x average.pl
y ejecútelo como:
$ ./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
Cosas extra interesantes (IMO) sobre perl y map
bucles e iteradores:
Para su información, los foreach my $i ...
bucles podrían reescribirse para usar map
la función de Perl en su lugar (ver perldoc -f map
, pero en resumen: map
itera sobre una lista, hace cosas con cada elemento y devuelve una nueva lista generada o un recuento de los elementos en esa lista generada) . Esto es más idiomáticamente que Perl, pero probablemente sea más difícil de entender para los nuevos programadores de Perl. p.ej
foreach my $i (0..$#data) { $sums[$i] += $data[$i] };
could be written as:
@sums = map { $sums[$_] + $data[$_] } 0..$#data;
Ambos iteran sobre elíndicesde la matriz @data ( 0..$#data
). El bucle for crea/modifica los elementos de @sums directamente, mientras que map
devuelve una nueva matriz de sumas que luego se asigna a la matriz @sums.
En lugar de utilizarla $i
como variable iteradora, la map
función crea y utiliza automáticamente una variable escalar (localizada) llamada $_
. $_
se usa en todas partes en Perl y es el argumento implícito (es decir, predeterminado) para la mayoría de las funciones cuando no se proporciona un argumento. por ejemplo, print
sin argumento es en realidad print $_
y split /,/
es en realidad split /,/, $_
. También está implícito para los operadores de coincidencia de patrones, por ejemplo, s/foo/bar
es en realidad $_ =~ s/foo/bar/
.
De manera similar, while (<>)
en realidad es algo así como while (defined($_ = <>))
(es decir, leer una línea del archivo de entrada o stdin, y si había algo para leer, asignarlo a $_ y evaluarlo como verdadero. De lo contrario, evaluarlo como falso y finalizar el while
ciclo).
$_
A menudo se le llama informalmente "lo actual" o "cosita". Ver man perlvar
y buscar \$_
para más detalles. También hay un equivalente de matriz @_
, que se utiliza para los parámetros pasados a una subrutina.
foreach my $i (0..$#sums) { $avg[$i] = $sums[$i] / $count };
could be written as:
@avg = map { $_ / $count } @sums;
Aquí, el foreach
bucle itera sobre elíndicesde @sums ( 0..$#sums
), mientras map
itera sobre elvaloresde la @sums
matriz. Nuevamente, el foreach
bucle modifica cada elemento de la @avg
matriz directamente, mientras que map
devuelve una nueva matriz que se asigna a @avg
.
Ambas formas producen resultados idénticos en este script, y ambas formas son útiles, pero los programadores de Perl tienden a usarlas map
con el tiempo porque es una herramienta genéricamente útil para iterar sobre cualquier tipo de lista. Y más corto de escribir que un bucle for/foreach que hace lo mismo. Y porque, después de un tiempo, resulta natural pensar en los datos en términos de listas, matrices y hashes.
A menudo se usa para transformar una matriz en un hash (o los valores o claves de un hash en una matriz).
Por cierto, map
no tiene que devolver una matriz, el bloque de código en { ... }
puede hacer cualquier cosa que el código Perl pueda hacer, y el valor de retorno podría simplemente descartarse o (si se asigna a una variable escalar) devolver un recuento de cualquier lista generada.
por ejemplo, el primer bucle foreach también podría escribirse como:
map { $sums[$_] += $data[$_] } 0..$#data;
Esto modifica la matriz @sums directamente (tal como lo hace el bucle foreach) y cualquier valor de retorno se descarta (es decir, no se asigna a ninguna variable).
Y, por supuesto, el segundo foreach
bucle también podría escribirse como:
map { $avg[$_] = $sums[$_] / $count } 0..$#sums;
Respuesta2
Lejos 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
}
Correr como :./script.awk data