Расчет средних почасовых значений для нескольких столбцов данных

Расчет средних почасовых значений для нескольких столбцов данных

Добрый день, я хотел бы рассчитать почасовые средние значения для следующих выборочных данных:

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/baris 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

Связанный контент