sexta-feira, 26 de julho de 2013

Unix: Comandos úteis para dados científicos

Esta semana estava eu preparando uma pesquisa comparativa de performance entre duas soluções de túnel com uma solução implementada por mim e, ao final da fase de testes comparativos, me vi encurralado com 96 arquivos CSV que precisavam ser auditados um por um e depois, à partir desses dados, fazer a construção de um relatório consolidado desses dados comparativos.

Essa tarefa seria um pé no saco infinita se eu dependesse só do método manual (abrir no LibreOffice Calc, transcrever os dados brutos importantes, fazer os cálculos necessários). Só não foi (exceto pela auditoria dos arquivos, que tinha de ser manual mesmo) pelo fato de eu utilizar o Linux como minha ferramenta de trabalho.

Daí você se pergunta: "Como assim o Linux? O que têm nele que pode te ajudar a completar a tarefa?". Eu respondo "Tudo! Tudo o que você precisa está à um terminal de distância!".

Imagine a seguinte situação: Você possui um arquivo CSV enorme (digamos, uns 60Gb de dados brutos, como os que vejo em uns projetos aqui no meu trabalho) com dezenas de colunas e alguns milhões de registros. Como você faria para fazer cálculos em cima desse arquivo gigante? Não há um Calc ou Excel que consiga abrir um arquivo desses e lhe permita trabalhar de maneira eficiente, e soluções como o SPSS são dispendiosas. Então, recorremos às soluções que temos ao nosso alcance...


Podemos usar um script em python (ou perl, ou ruby, ou erlang, ou php, ou ...) que levaria algum tempo para ser escrito e testado e mais tempo ainda para executar em cima de nossos dados. É uma solução boa, prática e até simples, exceto se você for uma pessoa que não sabe nada de programação.


Podemos criar um banco de dados e fazer a exportação dos dados para o mesmo. Não vou dizer nada à respeito disso, afinal o conhecimento necessário é maior que a solução anterior, apesar de abrir as portas da mágica que o SQL pode fazer com os dados.

Podemos usar vários comandos de terminal que fazem operações simples e que, juntos, podem alcançar os mesmos resultados das opções anteriores, exceto pelo fato de que você precisaria aprender a sintaxe de cada comando necessário para uma determinada tarefa.

Na minha opinião, se você não é um programador, essa opção é mais simples do que as outras duas, afinal você só precisará aprender como utilizar as ferramentas (e um pouco de como funciona o terminal) e isso já seria suficiente para trabalhar com os dados que você possui.

Botando a mão na massa

Já falei demais sobre opções, e meu objetivo aqui é mostrar como usar o terminal para trabalhar seus dados. Então vamos lá. Vamos supor que seu arquivo de dados se chama dados.csv e é um arquivo delimitado por Tabulação ("\t") e nesse arquivo nós precisamos somar o valor da quinta coluna, por exemplo. O comando para fazer essa operação seria o seguinte:

cat dados.csv | awk -F "\t" '{ somam += $5 } END { printf "%.2f\n", sum }'

Esse comando é divido em dois sub-comandos. O primeiro comando, "cat dados.csv" lê o conteúdo do arquivo e escreve, linha por linha, na saída padrão. Esse conteúdo enviado para a saída padrão é então redirecionado (através do pipe, "|") para a entrada do próximo aplicativo, awk.

Com os dados recebidos, awk seta o separador de campo como Tab (-F "\t"), incrementa a variável "soma" com o valor da quinta coluna e ao final, imprime o valor da variável "soma" usando o comando printf para formatar o valor com duas casas decimais.

Esse código roda muito rápido (muito mais rápido que um script em python feito para a mesma função) e usa conceitos bastante simples.

Um outro exemplo: Uma vez eu precisei descobrir quantos clientes estão conectados em um AP construído na minha máquina e correlacionar tal informação com as entradas de host dentro do arquivo /etc/dhcpd.conf. A rede era criada usando o hostap e os ips eram fixados ao endereço mac. A minha solução foi a seguinte:

iw dev wlan0 station dump | grep -i Station | cut -d " " -f 2 | xargs -I % zsh -c "echo '%: '; grep -i % /etc/dhcpd.conf"

Não vou explicar o código além do que disse acima. No entanto, vou lhes apresentar alguns comandos úteis para tratamento de dados.

Outros comandos

head e tail

Os comandos head e tail são simples: head imprime as 10 primeiras linhas de um arquivo, enquanto tail imprime as 10 últimas linhas do arquivo. Você pode controlar a quantidade de linhas exibidas usando o parâmetro "-n":

$ head -n 3 dados.csv 
"Pessoa" "Hora de Início" "Hora de Fim" "Tempo (Min)" "Toques Brutos" "Toques Líquidos" "Erros" "TLM" "Taxa de Erro"
"Janaína" 09:10:00 09:20:00 10 3500 2936 564 293,60 19,21%
"Claudete" 09:20:00 09:31:00 11 5937 3854 2083 350,36 54,05%

$ tail -n 2 dados.csv 
"Marisleide" 09:30:00 09:42:00 12 1360 1291 69 107,58 5,34%
"Joruilson" 09:40:00 09:54:00 14 4325 4018 307 287,00 7,64%

wc (contador de palavras)

Por padrão, wc irá lhe dizer quantas linhas, palavras e bytes possuem um arquivo. Se você está procurando só pela contagem de linhas, você pode usar o parâmetro -l:

$ wc dados.csv 
  5  54 360 dados.csv

$ wc -l dados.csv 
5 dados.csv

grep

Grep permite que você busque por uma expressão específica e imprima a linha que possuir a referência. Grep pode usar tanto texto puro quanto expressões regulares, apesar que existem outras ferramentas melhores para lidar com expressões, como awk e sed. Grep pode ignorar maiúsculas (-i), buscar recursivamente dentro de pastas (-r), começar a buscar a partir de uma linha (-B [número de linhas]) ou buscar até uma determinada quantidade de linhas (-A [número de linhas]):

grep -i Claudete dados.csv 
"Claudete" 09:20:00 09:31:00 11 5937 3854 2083 350,36 54,05%

sed

Sed é similar ao grep e ao awk em várias maneiras, no entanto eu uso o sed com mais frequência para alterar arquivos com base em uma expressão regular. Uma ocorrência comum é quando eu recebo algum arquivo de texto criado no Windows e preciso substituir o \cr\lf pela notação comum do Linux, \lf. Outro exemplo que uso bastante é comentar e descomentar opções de arquivos de configuração sem que eu tenha o trabalho de abrir o arquivo e procurar a referida opção. Veja o exemplo:

sed -ri '/pt_BR.UTF-8 UTF-8/ s/^#//' /etc/locale.gen

sed -ri '/\ \%wheel\ ALL\=\(ALL\)\ ALL/ s/^#\ //' /etc/sudoers

No nosso exemplo, vamos alterar o nome da Claudete para Cleycianne:

sed -ri 's/Claudete/Cleycianne/g' dados.csv

sort e uniq

Sort pega as linhas de um arquivo e as ordena baseado em uma coluna, informada através do parâmetro -k. -f torna a ordenação independente de maiúscula/minúscula e -t serve para informar qual o delimitador de campo usado:

$ sed 1d dados.csv | sort -t$'\t' -k6
"Marisleide" 09:30:00 09:42:00 12 1360 1291 69 107,58 5,34%
"Marisleide" 09:30:00 09:42:00 12 1360 1291 69 107,58 5,34%
"Janaína" 09:10:00 09:20:00 10 3500 2936 564 293,60 19,21%
"Cleycianne" 09:20:00 09:31:00 11 5937 3854 2083 350,36 54,05%
"Joruilson" 09:40:00 09:54:00 14 4325 4018 307 287,00 7,64%

Em nosso exemplo, nós suprimimos a primeira linha do cabeçalho usando sed e ordenamos os dados pela sexta coluna.

Já uniq busca por registros duplicados dentro do arquivo. Usando o parâmetro -c uniq irá contar a quantidade de referências repetidas para uma determinada entrada:

$ cat dados.csv | uniq -c
      1 "Pessoa" "Hora de Início" "Hora de Fim" "Tempo (Min)" "Toques Brutos" "Toques Líquidos" "Erros" "TLM" "Taxa de Erro"
      1 "Janaína" 09:10:00 09:20:00 10 3500 2936 564 293,60 19,21%
      1 "Cleycianne" 09:20:00 09:31:00 11 5937 3854 2083 350,36 54,05%
      2 "Marisleide" 09:30:00 09:42:00 12 1360 1291 69 107,58 5,34%
      1 "Joruilson" 09:40:00 09:54:00 14 4325 4018 307 287,00 7,64%

paste

Paste é uma dessas funções que você só percebe que precisa quando se depara com o problema. Paste permite que você concatene dois arquivos, linha por linha:

$ paste resultado.txt dados.csv 
"Status" "Pessoa" "Hora de Início" "Hora de Fim" "Tempo (Min)" "Toques Brutos" "Toques Líquidos" "Erros" "TLM" "Taxa de Erro"
"Aprovado" "Janaína" 09:10:00 09:20:00 10 3500 2936 564 293,60 19,21%
"Reprovado" "Cleycianne" 09:20:00 09:31:00 11 5937 3854 2083 350,36 54,05%
"Aprovado" "Marisleide" 09:30:00 09:42:00 12 1360 1291 69 107,58 5,34%
"Aprovado" "Marisleide" 09:30:00 09:42:00 12 1360 1291 69 107,58 5,34%
"Reprovado" "Joruilson" 09:40:00 09:54:00 14 4325 4018 307 287,00 7,64%

cut

Se paste permite você juntar dois arquivos, cut permite que você extraia um determinado campo de um arquivo. A extração pode ser feita por bytes (-b), caracteres (-c) ou por campos (-f), esse último dependendo da definição do campo (-d). Um intervalo pode ser definido para a extração, sendo a notação N (coluna N), N-M (de coluna N até M), N- (de N até o final) ou -M (do começo até M), delimitados por vírgula:

$ cut -f 1,3 dados.csv
"Pessoa" "Hora de Fim"
"Janaína" 09:20:00
"Cleycianne" 09:31:00
"Marisleide" 09:42:00
"Marisleide" 09:42:00
"Joruilson" 09:54:00

awk

Não poderíamos deixar de falar dessa ferramenta, depois de falar das outras. Awk é uma linguagem de interpretação de texto linha-a-linha que possui muitos recursos avançados de tratamento de texto. Não dá para falar aqui da quantidade de recursos que essa ferramenta oferece, pois ela permite desde fazer alterações em informações do arquivo até busca e cálculos matemáticos com essas informações. Como exemplo, vou deixar esse exemplo, onde queremos selecionar somente as pessoas que tiveram mais de 3000 toques líquidos (6a coluna) no nosso arquivo:

$ sed 1d dados.csv | awk '{if ($6 >= "3000") print ($0)}'
"Cleycianne" 09:20:00 09:31:00 11 5937 3854 2083 350,36 54,05%
"Joruilson" 09:40:00 09:54:00 14 4325 4018 307 287,00 7,64%

Conclusão

Apesar de haver várias ferramentas de manipulação de arquivos no linux, poucas pessoas conhecem o seu verdadeiro poder e as facilidades que elas permitem. Nem sempre, precisaremos de usar linguagens de programação ou bancos de dados para trabalhar com dados científicos. Desde que estes dados estejam de alguma forma estruturados, fica bastante fácil extrair as informações que precisamos usando comandos de terminal.