Conte valores associados únicos em awk (ou perl)

4

Eu já encontrei "Como imprimir uma contagem incremental de ocorrências de valores únicos na coluna 1" , que é semelhante à minha pergunta, mas a resposta não é suficiente para meus propósitos.

Primeiro, deixe-me ilustrar o que eu quero fazer:

# Example input
apple   abc jkl
apple   xyz jkl
apple   abc xyz
apple   qrs xyz
apple   abc jkl
banana  abc lmno
banana  lmnop   xyz
banana  lmnopq  jkl
banana  abc jkl
banana  lmnop   pqrs
banana  abcdefg tuv
cucumber    abc lmno
cucumber    abc jkl
cucumber    abc xyz
cucumber    abcd    jkl
cucumber    abc jkl
cucumber    abc lmno

# Desired output
apple   3   2
banana  4   5
cucumber    2   3

Portanto, para cada valor separado do campo 1, imprima esse campo e uma contagem dos valores associados exclusivos para o campo 2 e, em seguida, para o campo 3.

A entrada é classificada pelo primeiro campo, mas a ordenação por outros campos não é permitida (e não seria boa, pois os 2º e 3º campos precisam ser tratados).

Eu preferiria realizar isso em awk ; provavelmente é muito mais fácil em perl e estou interessado em aprender como fazer isso também, mas estou lidando com um script awk e prefiro não reescrever a coisa toda.

Eu criei um método que funciona , mas é bem demorado e me parece muito hacky. Vou postar isso como resposta (quando eu voltar ao escritório), mas adoraria ver qualquer abordagem boa . (Eu não acho que o meu seja "bom".)

    
por Wildcard 17.11.2015 / 09:22

5 respostas

3

com awk :

awk 'function p(){print l,c,d; delete a; delete b; c=d=0} 
  NR!=1&&l!=$1{p()} ++a[$2]==1{c++} ++b[$3]==1{d++} {l=$1} END{p()}' file

Explicação:

  • function p() : define uma função chamada p() , que imprime os valores e exclui as variáveis e matrizes usadas.
  • NR!=1&&l!=$1 se não for a primeira linha e a variável l for igual ao primeiro campo $1 , depois execute a função p() .
  • ++a[$2]==1{c++} se o incremento do valor do elemento da matriz a com índice $2 for igual a 1 , então esse valor será visto primeiro e, portanto, incrementará a variável c . O ++ antes do elemento, retorna o novo valor, portanto, causa um incremento antes da comparsão com 1 .
  • ++b[$3]==1{d++} o mesmo que acima, mas com o terceiro campo e a variável d .
  • {l=$1} O l para o primeiro campo (para a próxima iteração .. acima)
  • END{p()} depois que a última linha é processada, awk tem que imprimir os valores para o último bloco

Com a sua entrada, a saída é:

apple 3 2
banana 4 5
cucumber 2 3
    
por 17.11.2015 / 09:51
3

Eu gosto de espaços em branco e nomes de variáveis descritivas. O que mais há a dizer? Já faz um tempo desde que eu escrevi muito awk , eu até me esqueci do -f no shebang. No entanto, como eu fiz isso eu realmente senti como se estivesse no zen dele. código do Haiku.

Eu gosto dessa solução porque há um mínimo de lógica codificada. Apenas dois loops for iterando sobre índices de array. Nenhum passo de 3 partes for loops, nenhuma declaração de if , nenhuma comparação explícita de valor. Todas essas coisas estão estatisticamente correlacionadas a defeitos de software (bugs). Curiosamente, não há atribuições explícitas, e apenas uma operação matemática, o incremento na contagem. Acho que isso tudo indica o uso máximo dos recursos do idioma.

Sinto que algo pode estar faltando, mas ainda não consegui encontrar nenhum furo.

Por favor, comente. Opiniões e críticas construtivas solicitadas. Gostaria de saber sobre as considerações de desempenho desse script.

#!/usr/bin/awk -f

function count(seen, unique_count) {
    for (ij in seen) {
        split(ij, fields, SUBSEP)
        ++unique_count[fields[1]]
    }
}

{
    seen2[$1,$2]
    seen3[$1,$3]
}

END {
    count(seen2, count2)
    count(seen3, count3)
    for (i in count3) {
        print i, count2[i], count3[i]
    }
}

Anotação

Eu acho que um recurso exclusivo desse script é que as matrizes seen2 e seen3 não contêm dados, apenas índices. Isso é porque estamos contando apenas valores únicos, portanto, a única coisa que importa é que os valores foram vistos, não nos importamos com quantas vezes eles ocorrem.

#!/usr/bin/awk -f

A função count usa uma matriz, seen , indexada por dois valores de campo encontrados nos registros de entrada, campos 1 e 2 ou campos 1 e 3 e retorna uma matriz internamente chamada unique_count indexada pelo primeiro campo, contendo as contagens de valores de campo exclusivos para a coluna acumulada pelo segundo índice:

function count(seen, unique_count) {

A função count itera sobre os índices da matriz seen :

    for (ij in seen) {

Divide o índice nos dois valores originais, campo 1 e campo 2 ou campo 3:

        split(ij, fields, SUBSEP)

Incrementar a contagem do elemento indexado pelo campo 1:

        ++unique_count[fields[1]]
    }
}

Em cada linha de entrada encontrada, criamos um elemento de matriz vazio, se já não existir, indexado pelo primeiro campo e o segundo ou terceiro campo. Mantenha uma matriz separada ( seen2 e seen3 ) para cada número de campo que está sendo contado. Haverá apenas um elemento de matriz para cada valor único na coluna especificada (2 ou 3):

{
    seen2[$1,$2]
    seen3[$1,$3]
}

No final dos dados, conte o número de campos exclusivos vistos para cada coluna:

END {

Transmita os arrays acumulados da entrada para a função count e receba o count2 ou count3 preenchido com as contagens de campo exclusivas.

    count(seen2, count2)
    count(seen3, count3)

Passe por um dos arrays count2 ou count3 (não importa qual é o primeiro campo de cada linha) e imprima o campo um, seguido pelas contagens de valores exclusivos encontrados para cada linha contendo o campo um:

    for (i in count3) {
        print i, count2[i], count3[i]
    }
}

Versão de um liner

awk 'function c(s,u){for(x in s){split(x,f,SUBSEP); ++u[f[1]];}}
 {a[$1,$2]; b[$1,$3];} END {c(a,d); c(b,e); for(i in d){print i,d[i],e[i];}}'
    
por 17.11.2015 / 10:35
2
perl -lane 'undef $h{ $F[0] }[ $_ - 1 ]{ $F[$_] } for 1,2
            }{
            for my $k (keys %h) {
                print join " ", $k, map scalar keys $_, @{ $h{$k} }
            }' < input

Basicamente, você cria um hash assim:

  'apple' => [
               {
                 'abc' => undef,
                 'xyz' => undef,
                 'qrs' => undef
               },
               {
                 'jkl' => undef,
                 'xyz' => undef
               }
             ],
  'banana' => [
                {
                  'abcdefg' => undef,
                  'lmnop' => undef,
                  'lmnopq' => undef,
                  'abc' => undef
                },
                {
                  'lmno' => undef,
                  'pqrs' => undef,
                  'tuv' => undef,
                  'jkl' => undef,
                  'xyz' => undef
                }
              ],
  'cucumber' => [
                  {
                    'abcd' => undef,
                    'abc' => undef
                  },
                  {
                    'lmno' => undef,
                    'jkl' => undef,
                    'xyz' => undef
                  }
                ]

Em seguida, basta contar as chaves para cada hash interno.

    
por 17.11.2015 / 09:36
2

Eu provavelmente resolveria algo assim:

#!/usr/bin/env perl
use strict;
use warnings;

use XML::Twig;

my %count_of_col2;
my %count_of_col3;

#iterate data
while (<DATA>) {
    #split on whitespace
    my ( $key, $col2, $col3 ) = split;
    #update counts
    $count_of_col2{$key}{$col2}++;
    $count_of_col3{$key}{$col3}++;
}

foreach my $key ( sort keys %count_of_col2 ) {
    print join( "\t",
        $key,
         #keys gives us all the elements - we use scalar to count them. 
        scalar keys %{ $count_of_col2{$key} },
        scalar keys %{ $count_of_col3{$key} } ),
        "\n";
}

__DATA__
apple   abc jkl
apple   xyz jkl
apple   abc xyz
apple   qrs xyz
apple   abc jkl
banana  abc lmno
banana  lmnop   xyz
banana  lmnopq  jkl
banana  abc jkl
banana  lmnop   pqrs
banana  abcdefg tuv
cucumber    abc lmno
cucumber    abc jkl
cucumber    abc xyz
cucumber    abcd    jkl
cucumber    abc jkl
cucumber    abc lmno

Nota - suporta apenas duas colunas. Você poderia fazer números arbitrários se quisesse, mas provavelmente desejaria um array externo.

    
por 17.11.2015 / 15:59
1

Como prometido, aqui está a abordagem que eu fiz trabalhei antes de escrever esta pergunta. Funciona e talvez é uma boa abordagem, mas pareceu excessivamente complicado para essa tarefa aparentemente simples. Agora parece que não é tão ruim assim. :)

function printcounts() {
  printf "%s", currentf1
  for (i = 2; i <= 3; i++ ) {
    printf "%s", FS countuniq [ i ]
  }
  printf "\n"
}

function resetvars() {
  delete already_seen_value
  for ( i = 2; i <= 3; i++ ) {
    countuniq [ i ] = 0
  }
}

$1 != currentf1 {

  if ( NR != 1 ) {
    printcounts()
  }
  currentf1 = $1
  resetvars()
}

{
  for ( i = 2; i <= 3; i++ ) {
    if ( ! already_seen_value [ i ":" $i ] ) {
      already_seen_value [ i ":" $i ] ++
      countuniq [ i ] ++
    }
  }
}
END {
  printcounts()
}

Com modificações baseadas na resposta do caos :

function printcounts() {
  printf "%s", currentf1
  for (i = 2; i <= 3; i++ ) {
    printf "%s", FS countuniq [ i ] + 0
  }
  printf "\n"
  # Reset vars
  delete seenthis
  delete countuniq
}

NR != 1 && currentf1 != $1 {
  printcounts()
}

{
  for ( i = 2; i <= 3; i++ ) {
    if ( ++ seenthis [ i ":" $i ] == 1 ) {
      countuniq [ i ] ++
    }
  }
  currentf1 = $1
}

END {
  printcounts()
}

(O + 0 na função printcounts é para garantir que um número seja sempre impresso, pois o caso de uso real envolve um separador de campo de vírgula e ignorar os campos vazios, portanto é possível uma contagem zero.)

    
por 17.11.2015 / 20:20