2

1 行の AWK コマンドでは不十分なデータを操作する方法についての提案を探しています。最大 1000 以上の行と列のデータ セットを扱っています。定義する列変数が多すぎるという問題が発生しています。ループを使用して配列を反復処理し、カウントして合計する列を定義する方法があると考えています。Excel の COUNTIF & SUMIF と同様のキー値に基づいて、行の数と合計を計算しようとしています。

Data Set Example:
Store_Location;Person;Adult_Child;Age;Weight...
LocationA;PersonA;0;50;200
LocationB;PersonB;1;10;100
LocationA;PersonC;1;12;90
LocationA;PersonA;0;50;200

Desired Output: (delimiter is not important)
Store_Location;Count_Of_Adults;Count_of_Children;Sum_of_Age;Sum_of_Weight
LocationA;2;1;112;490
LocationB;0;1;10;100

これは、私が使用していた AWK スクリプトの例です。

BEGIN {FS=";"} {print "Store_Location;Count_Of_Adults;Count_of_Children;Sum_of_Age;Sum_of_Weight"}

{
n[$1]++;
C1_[$1] += ($3 == "1" ? 0 : 1);S1_[$1] += $4;column_sum3+=$4
C2_[$1] += ($3 == "0" ? 0 : 1);S2_[$1] += $5;column_sum4+=$5
}
END {
for (i in n) {
  print i,C1_[i],C2_[i],S1_[i],S2_[i]
}
}

a2p を使用して構文を perl に変換し、(異なる列の使用に基づいて) いくつかの変更を加えました。

$base = 20;
while (<>){
    @array = split(/$FS/, $_, -1);


    $n{$array[$base]}++;

    $C1_{$array[$base]} += ($array[21] eq '' ? 0 : 1);
    $C2_{$array[$base]} += ($array[34] eq '' ? 0 : 1);
    $column_count1 += ($array[21] eq '' ? 0 : 1);
    $column_count2 += ($array[34] eq '' ? 0 : 1);
    $S1_{$array[$base]} += $array[21];
    $S2_{$array[$base]} += $array[34];
    $column_sum1 += $array[21];
    $column_sum2 += $array[34];
}
@sorted_keys = sort { $a <=> $b} keys %n;
foreach $i (@sorted_keys){
    print $i,$C1_{$i},$C2_{$i},$S1_{$i},$S2_{$i};

これと似たようなことができるようにしたいのですが、合計したい列と数えたい列を別の配列に入れようとしていました。例: @sum_array=[1,6,10,15,30] & @count_array = [1,10,20]。また、各出力列を宣言することなく、ループを使用して合計とカウントを作成します。すべての列を合計してカウントし、必要な列を出力するだけで問題ありません。ハッシュ/配列を使用して Perl でこれをコーディングしようとすると、困難に遭遇しました。ハッシュを使用しようとしましたが、出力形式を取得できませんでした。そのため、これがデータの構造化方法であるかどうかわかりません。

$n{$array[$base]}{Adult}{count}+= ($array[21] eq 0 ? 0 : 1);
$n{$array[$base]}{Child}{count}+= ($array[21] eq 1 ? 0 : 1);
$n{$array[$base]}{Weight}{sum} += $array[21];
$n{$array[$base]}{Age}{sum}+= $array[34];

編集:私の論理的な問題は、フィールド名/列を呼び出したくないということだと思います。多くのフィールドで合計とカウントを実行したいからです。大人と子供の比較は単なる例です。操作したい列を 1 か所にリストしたいだけです。おそらく、それを説明する簡単な方法は、入力データから 100 列があるとしましょう。分析したいカラムを柔軟に特定できるようにしたい。例: 列 15-30 列 1 の一意の値に基づいて各列の合計とカウントを取得したい。次に、同じコードを変更して、列 15-20 と 30-40 の合計を取得できるようにします。AWK を使用すると、操作したい列 ($2、$3、$4、...) を呼び出すことができますが、列が多すぎると管理が難しくなります。

4

2 に答える 2

1

Text::CSVは、Perl で区切られたデータを解析および出力するための優れたツールです。Text::CSV を使用して問題を解決するスクリプトを実行してみましょう。

設定

何かを解析する前に、新しい CSV オブジェクトを作成し、区切り文字を伝える必要があります。

use strict; use warnings;
use Text::CSV;

my $csv = Text::CSV->new( { sep_char => ";", eol => $/ } )
    or die "Cannot use CSV: " . Text::CSV->error_diag();

また、読み取り用に入力ファイルを開く必要があります。

open my $fh, "<", "file.csv" or die "Failed to open file for reading: $!";

列名を設定する

Text::CSV は、列名をキーとして、データの各行を hashref としてフェッチできます。たとえば、行を読み取ることができます

LocationA;PersonA;0;50;200

次の Perl データ構造に変換します。

{
    'Age' => '50',
    'Adult_Child' => '0',
    'Person' => 'PersonA',
    'Store_Location' => 'LocationA',
    'Weight' => '200'
}

これにより、列番号の代わりに人間が読める文字列を操作できます。この機能を使用するには、まず各列に使用する名前をパーサーに伝える必要があります。データには列名を含むヘッダー行が含まれているため、それをそのまま使用できます。

$csv->column_names( $csv->getline($fh) );

合計する列を指定

特定の列の合計を計算するだけです。Ageサンプルデータでは、 andWeight列の合計を計算したいのですが、 Store_Locationor Adult_Child(Adult_Childは本質的にブールフラグであるため、単純な合計は必要ありません)。合計を計算する列名の配列を作成しましょう。

# Use columns 3-4 (zero-indexed)
my @cols_to_sum = @{ [ $csv->column_names() ] }[3..4];

入力に ​​100 列があり、15 ~ 20 列と 30 ~ 40 列のみを合計する場合は、次のようにします。

my @cols_to_sum = @{ [ $csv->column_names() ] }[15..20,30..40];

これは、前の部分で設定した列名の配列スライスを取ります。列番号はゼロから始まることに注意してください。

配列を取得したら、列番号を再度参照する必要はありません。これは、将来、合計を計算する列を変更したい場合、この 1 行を変更するだけでよいことを意味します。

入力には列Ageが含まれていますが、対応する出力列名をSum_of_Age. プレフィックスSum_of_を変数に入れて、後で出力を変換できるようにします。

my $col_prefix = "Sum_of_";

CSV データを取得する

これで、データを取得する準備が整いました。結果を場所ごとにグループ化したいので、計算された合計を場所をキーとしてハッシュに保存します。

my %totals;
while (my $row = $csv->getline_hr($fh)) {
    my $location = $row->{Store_Location};

    # Add numeric columns to the totals, prepending prefix to each key
    foreach my $col (@cols_to_sum) {
        my $col_name = $col_prefix . $col;
        $totals{$location}{$col_name} += $row->{$col};
    }

    # Set counts of adults and children to zero if not set for this location
    $totals{$location}{Count_of_Adults}   //= 0;
    $totals{$location}{Count_of_Children} //= 0;

    # Handle the adult/child flag
    if ($row->{Adult_Child}) {
        $totals{$location}{Count_of_Children}++;
    }
    else {
        $totals{$location}{Count_of_Adults}++;
    }
}
$csv->eof or $csv->error_diag();

close $fh;

Adult_Child1 つの入力列を 2 つの出力列 (Count_of_Adultsおよび) にマッピングしているため、列を別の方法で処理する必要があることに注意してくださいCount_of_Children。これが終わると、%totalsハッシュは次のようになります。

{
    'LocationA' => {
        'Count_of_Adults' => 2,
        'Count_of_Children' => 1,
        'Sum_of_Weight' => 490,
        'Sum_of_Age' => 112
    },
    'LocationB' => {
        'Count_of_Adults' => 0,
        'Count_of_Children' => 1,
        'Sum_of_Weight' => 100,
        'Sum_of_Age' => 10
    }
}

結果を印刷する

すべての合計を計算したので、結果を出力できます。まず、列の順序を設定するヘッダー行を作成する必要があります。

# Construct output header, prepending prefix to each "totals" column
my @header = qw(Store_Location Count_of_Adults Count_of_Children);
push @header, $col_prefix . $_ for @cols_to_sum;

同じオブジェクトを使用してText::CSV、結果を stdout に出力できます。このようにして、入力ファイルと同じセミコロン区切りの形式を使用できます。まず、ヘッダーを出力します。

$csv->print(\*STDOUT, [ @header ]);

stdout の代わりにファイルに出力したい場合は、次のように実行できます。

open my $fh, ">", "output.csv" or die "Failed to open file for writing: $!";
$csv->print(\*$fh, [ @header ]);

配列を使用して、正しい列順でハッシュ@headerから合計を取得します。%totalsただし、このStore_Location列は の最上位のキーであるため、特別です%totals@header結果を簡単に印刷できるように、配列から削除します。

shift @header;

これで、結果を場所で並べ替えて印刷できます。

foreach my $location (sort keys %totals) {

    # Use a hash slice to put result columns in the same order as the header
    my $row = [ $location, @{ $totals{$location} }{ @header } ];

    $csv->print(\*STDOUT, $row);
}

出力は次のとおりです。

Store_Location;Count_of_Adults;Count_of_Children;Sum_of_Age;Sum_of_Weight
LocationA;2;1;112;490
LocationB;0;1;10;100
于 2013-11-15T17:50:03.437 に答える