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];

编辑:我认为我的逻辑问题是我不想调用字段名称/列。因为我想对许多字段进行求和和计数。成人儿童比较只是一个例子。我只想在一个地方列出我想使用的列。也许解释它的简单方法是,假设输入数据中有 100 列。我希望能够灵活地识别我要分析的列。例如:第 15-30 列我想根据第 1 列中的唯一值获取每列的总和和计数。然后能够修改相同的代码以获取第 15-20 列和第 30-40 列的总和。使用 AWK 我可以调出我想要使用的列 ($2,$3,$4,...),但是当列太多时会变得难以管理。

4

2 回答 2

1

目前尚不完全清楚您想要什么,当然也不清楚您所说的“我在定义太多列变量时遇到问题”是什么意思,但这就是我认为您正在尝试做的事情,希望它能让您走上正轨小路:

$ cat file
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

$ cat tst.awk         
BEGIN{ FS=OFS=";" }

NR==1 {
    split($0,nr2nm)
    for (nr=1;nr in nr2nm;nr++) {
        nm2nr[nr2nm[nr]] = nr
    }
    next
}

{
    stores[$nm2nr["Store_Location"]]

    for (nr=3; nr<=NF; nr++) {
        fldName = nr2nm[nr]
        if ( fldName == "Adult_Child" ) {
            fldName = ($nr == 1 ? "Child" : "Adult")
        }
        fldNames[fldName]
        cnt[$nm2nr["Store_Location"],fldName]++
        sum[$nm2nr["Store_Location"],fldName] += $nr
    }
}

END {
    printf "%s", "Store_Location"
    for (fldName in fldNames) {
        printf ";cnt[%s];sum[%s]", fldName, fldName
    }
    print ""
    for (store in stores) {
        printf "%s", store
        for (fldName in fldNames) {
            printf ";%d;%d", cnt[store,fldName], sum[store,fldName]
        }
        print ""
    }
}

$ awk -f tst.awk file
Store_Location;cnt[Weight];sum[Weight];cnt[Child];sum[Child];cnt[Adult];sum[Adult];cnt[Age];sum[Age]
LocationA;3;490;1;1;2;0;3;112
LocationB;1;100;1;1;0;0;1;10
于 2013-11-14T20:29:58.093 回答
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) );

指定要求和的列

我们只需要计算某些列的总和。在您的示例数据中,我们想要计算AgeandWeight列的总数,但不计算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];

这需要我们在上一部分中设置的列名的数组切片。请记住,列号从零开始。

一旦我们有了数组,我们就不必再引用列号了。这意味着,如果我们想改变计算总和的列,我们只需要改变这一行。

我们的输入包含该列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_Child不同的方式处理该列,因为我们将单个输入列映射到两个输出列(Count_of_AdultsCount_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对象将结果打印到标准输出。这样我们就可以使用与输入文件相同的分号分隔格式。首先我们打印标题:

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

如果要打印到文件而不是标准输出,可以这样做:

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 回答