terça-feira, 26 de março de 2013

Como encontrar valores distintos em arquivos de texto

Recebi um arquivo de texto com quase 300MB e 300 mil linhas para uma carga que executo mensalmente. Esse arquivo veio com uma alteração de formato e também com valores novos numa coluna crítica.

Para verificar se a alteração no formato não tinha provocado uma falha na carga, resolvi inspecionar o arquivo com as ferramentas que o Linux oferece.

Descobri uma maneira bastante simples de enumerar os valores distintos de uma coluna:

cut -c N-M arquivo.txt | sort | uniq

Os valores N e M indicam, respectivamente, a coluna inicial (baseado em 1) e a coluna final do trecho interessante de cada linha. Com um arquivo compactado, fiz o seguinte:

gunzip -c arquivo.txt.gz | cut -c N-M | sort | uniq

O cut pareceu-me um pouco lento, então resolvi verificar se o Perl pode fazer o mesmo com maior velocidade.

perl -pe '$_=substr($_, C, L)."\n"' arquivo.txt | sort | uniq

E, de fato, a solução, embora mais complexa, é mais rápida. Neste caso, C indica a coluna inicial (baseada em 0) e L o tamanho do campo (ou seja, C=N-1 e L=M-N+1).

Posso usar o perl com arquivos compactados assim:

gunzip -c arquivo.txt.gz | perl -pe '$_=substr($_, C, L)."\n"' | sort | uniq

Se eu quisesse apenas saber o número de valores distintos, bastaria adicionar o wc ao fim:

cut -c N-M arquivo.txt | sort | uniq | wc -l

E, com isso, pude confirmar que tinha carregado os dados corretamente. É interessante que o comando uniq não busca valores distintos, ele apenas elimina linhas sucessivas iguais. Por isso, é preciso usar também o comando sort.

quarta-feira, 6 de março de 2013

Renda e desenvolvimento da América do Sul

Aproveitei o código do artigo sobre a renda e o IDH dos estados brasileiros para fazer uma comparação dos países sul-americanos. Aproveitei para adicionar duas informações: a renda média e o IDH médio. Esses dados são representados, respectivamente, por uma linha vertical e uma linha horizontal. O tamanho de cada círculo representa o número de habitantes.

Clique na imagem para aumentá-la.

Eu ignorei as colônias (Malvinas e Guiana Francesa), porque não possuem informação sobre IDH.

Dado o seu tamanho, o Brasil só poderia mesmo estar nas redondezas das médias. Infelizmente, ele está um pouco abaixo das duas.

Argentina e Chile lideram com bastante folga, cada um desses países liderando num quesito, mas por pequena margem. Isso me remete ao fato de que, na nossa mídia, o Chile é frequentemente referido como um sucesso retumbante enquanto a Argentina é um fracasso trágico. Resulta que são muito parecidos. O Uruguai não está muito atrás no que concerne o IDH, mas a sua renda é sensivelmente menor.

Orbitam o Brasil a Venezuela, o Peru e a Colômbia. Esses países dificilmente são retratados em bons termos na nossa mídia, curiosamente. O Equador tem renda um tanto menor que a desse grupo, mas um IDH semelhante.

A Guiana e o Suriname destoam por terem IDHs pequenos para seus respectivos níveis de renda.

Eis o código:

<html>
<head>
<script src="processing-1.4.1.min.js"></script>
<script type="text/processing" data-processing-target="canvas">
  class Pais {
    int populacao, idh, renda, cor;

    Pais(int p, int i, int r, int c) {
      populacao=p;
      idh=i;
      renda=r;
      cor=c;
    }
  }

  HashMap paises=new HashMap();
  paises.put("Argentina", new Pais(40482000,797,17516,4));
  paises.put("Bolivia", new Pais(9863000,663,4789,1));
  paises.put("Brasil", new Pais(191241714,718,11769,0));
  paises.put("Chile", new Pais(16928873,805,17222,0));
  paises.put("Colombia", new Pais(45928970,710,10249,1));
  paises.put("Equador", new Pais(14573101,720,8492,1));
  paises.put("Guiana", new Pais(772298,633,7465,3));
  paises.put("Paraguai", new Pais(6831306,665,5413,3));
  paises.put("Peru", new Pais(29132013,725,10062,2));
  paises.put("Suriname", new Pais(472000,680,9475,3));
  paises.put("Uruguai", new Pais(3477780,783,15113,4));
  paises.put("Venezuela", new Pais(26814843,735,12568,2));

  int[] cores=new int[] {#98db11, #ff9900, #ffd42a, #ff0000, #5599ff};

  void setup() {
    size(1024,1024);
    background(255);
    noLoop();
  }

  void draw() {
    Iterator i = paises.entrySet().iterator();
    while (i.hasNext()) {
      Map.Entry me = (Map.Entry)i.next();
      String nome=me.getKey();
      Pais pais=me.getValue();
      float x=1024*(pais.renda/20000);
      float y=1024*(pais.idh/1000);
      float r=2+pais.populacao/1000000;
      fill(cores[pais.cor], 150);
      stroke(cores[pais.cor], 255);      
      ellipse(x, 1024-y, r, r);
      fill(0, 255);
      text(nome, x, 1032-y+r/2);
    }
    stroke(#AAAAAA, 255);
    float rendaMedia=(11962/20000)*1024;
    line(rendaMedia, 0, rendaMedia, 1024);
    float idhMedio=1024-(729/1000)*1024;
    line (0, idhMedio, 1024, idhMedio);
    
  }
</script>
</head>
<body>
<canvas id="canvas">
</body>
</html>

domingo, 3 de março de 2013

Renda e desenvolvimentos dos estados brasileiros

Depois de brincar com a linguagem Processing, resolvi avaliar a sua versão para Javascript. Parece que, atualmente, tudo está sendo refeito em Javascript. Esta é uma das instâncias em que isso tem sentido.

Busquei os dados dos estados brasileiros na Wikipédia. As informações que resolvi comparar são a renda, a população e o índice de desenvolvimento humano (IDH).

Foi surpreendentemente fácil; a tarefa mais laboriosa foi copiar os dados da Wikipédia para o programa.

  class Estado {
    int populacao, idh, renda, regiao;

    Estado(int p, int i, int r, int s) {
      populacao=p;
      idh=i;
      renda=r;
      regiao=s;
    }
  }

  HashMap estados=new HashMap();
  estados.put("AC", new Estado(707125,751,7041,0));
  estados.put("AL", new Estado(3093994,677,5164,1));
  estados.put("AP", new Estado(648553,780,8543,0));
  estados.put("AM", new Estado(3350773,780,11829,0));
  estados.put("BA", new Estado(13633969,742,6922,1));
  estados.put("CE", new Estado(8180087,723,5636,1));
  estados.put("DF", new Estado(2469489,874,37600,2));
  estados.put("ES", new Estado(3392775,802,15236,3));
  estados.put("GO", new Estado(5849105,800,9962,2));
  estados.put("MA", new Estado(6424340,683,4628,1));
  estados.put("MT", new Estado(3033991,796,12350,2));
  estados.put("MS", new Estado(2404256,802,10599,2));
  estados.put("MG", new Estado(19159260,800,11028,3));
  estados.put("PA", new Estado(7443904,755,6241,0));
  estados.put("PB", new Estado(3753633,718,5507,1));
  estados.put("PR", new Estado(10266737,820,13158,4));
  estados.put("PE", new Estado(8541250,718,6528,1));
  estados.put("PI", new Estado(3086448,703,4213,1));
  estados.put("RJ", new Estado(15180636,832,17695,3));
  estados.put("RN", new Estado(3121451,738,6754,1));
  estados.put("RS", new Estado(10576758,832,14310,4));
  estados.put("RO", new Estado(1535625,776,8391,0));
  estados.put("RR", new Estado(425398,750,9075,0));
  estados.put("SC", new Estado(6178603,840,15638,4));
  estados.put("SP", new Estado(39924091,833,19548,3));
  estados.put("SE", new Estado(2036277,742,7560,1));
  estados.put("TO", new Estado(1373551,756,7210,0));

  int[] regioes=new int[] {#98db11, #ff9900, #ffd42a, #ff0000, #5599ff};

  void setup() {
    size(512,512);
    background(255);
    noLoop();
  }

  void draw() {
    Iterator i = estados.entrySet().iterator();
    while (i.hasNext()) {
      Map.Entry me = (Map.Entry)i.next();
      String sigla=me.getKey();
      Estado estado=me.getValue();
      float x=512*(estado.renda/40000);
      float y=512*(estado.idh/1000);
      float r=5+estado.populacao/1000000;
      fill(regioes[estado.regiao], 100);
      stroke(regioes[estado.regiao], 255);      
      ellipse(x, 512-y, r, r);
    }
  }
O programa desenha um círculo para cada estado. A posição no eixo horizontal indica a renda (quanto mais à direita, maior a renda per capita) e a posição no eixo vertical indica o IDH (quanto mais alto, maior o índice de desenvolvimento humano). O raio do círculo indica a população do estado. As cores representam as regiões: verde para norte;  laranja para nordeste;  amarelo para centro-oeste; vermelho para sudeste; e azul para sul).

Aquele ponto amarelo disparado na frente é o Distrito Federal, evidentemente. O grande ponto vermelho que o precede é São Paulo (que, tendo uma renda per capita bastante maior que a dos estados do sul, não consegue superá-los no IDH).

Pode-se constatar que os pontos aglomeram-se conforme as cores. Minas Gerais é uma exceção que se destaca, assim como o Distrito Federal.

Com pequenas alterações no método draw(), consegui inserir as siglas dos estados dentro dos círculos.

  void setup() {
    size(1024,1024);
    background(255);
    noLoop();
  }

  void draw() {
    Iterator i = estados.entrySet().iterator();
    while (i.hasNext()) {
      Map.Entry me = (Map.Entry)i.next();
      String sigla=me.getKey();
      Estado estado=me.getValue();
      float x=1024*(estado.renda/40000);
      float y=1024*(estado.idh/900);
      float r=20+estado.populacao/1000000;
      fill(regioes[estado.regiao], 100);
      stroke(regioes[estado.regiao], 255);      
      ellipse(x, 1024-y, r, r);
      fill(#FFFFFF, 255);
      text(sigla, x-8, 1028-y);
    }
  }

Tive que aumentar as dimensões do gráfico, para que as siglas não se atropelassem (ainda mais).

Clique na imagem para vê-la aumentada.

Concluí, então, que Processing.js é uma ferramenta deveras interessante e prática. Acho que o próximo passo é investigar as facilidades de animação e interação com o usuário. O fato de rodar no navegador permite jogar o processamento para o cliente.

sábado, 2 de março de 2013

Simplificando a construção de consultas dinâmicas

Um problema recorrente é o de construir consultas para formulários com campos opcionais. Em grande parte dos casos, as consultas são geradas dinamicamente antes de serem submetidas ao banco de dados. Isto é, a lógica da consulta fica escondida na lógica da aplicação com cada cláusula sendo adicionada ou não conforme o valor digitado pelo usuário (e um if na aplicação).

Penso que, em muitos casos, isso pode ser resolvido com pequenas alterações no SQL. Apresento uma tabela de pessoas (PESSOA) como exemplo. Ela possui as seguintes colunas:
  • NOME;
  • NASCIMENTO (uma data);
  • DEPARTAMENTO (um número).
Vou começar pela data. Suponha que o usuário possa pesquisar todas as pessoas pela data de nascimento, mas que o campo é opcional. A consulta mais óbvia é:

select *
from pessoa
where nascimento=:data_nascimento
Evidentemente, se o usuário não digitar uma data, a aplicação deverá retirar a cláusula. Mas, para evitar isso, podemos reescrever a consulta assim:

select *
from pessoa
where 
nascimento between nvl(:data_nascimento, to_date('1000', 'YYYY')) 
               and nvl(:data_nascimento, to_date('3000', 'YYYY'))
Neste caso, se a data de nascimento não for informada, a consulta retornará todas as pessoas que nasceram entre o ano 1000 e o ano 3000 (a segunda data poderia muito bem ser sysdate) e isso deve bastar, a menos que se trate de uma base de dados do Vaticano. Se o formulário permite informar um período (duas datas), então a consulta já está preparada para recebê-las.

Com relação aos nomes, podemos escrever o seguinte:

select *
from pessoa
where 
nome like nvl(:nome, '%')
O que mais se vê é isto, no entanto:
(:nome is null or nome like :nome) 
Isso é um pouco mais eficiente, mas menos no estilo da programação incondicional. E, se o espírito hacker bater:

select *
from pessoa
where nome between nvl(:nome, chr(0)) and nvl(:nome, chr(255))
Entre os caracteres 0 e 255 estão todas as letras do alfabeto latino, inclusive com acentos, no código ASCII. Este mesmo truque pode ser usado para o departamento:

select *
from pessoa
where departamento between nvl(:dept, -1) and nvl(:dept, 1000)
Evidemente, todos os departementos devem ter códigos entre -1 e 1000; conhecer os dados é importante para poder usar este tipo de truque, mas isso é importante de qualquer maneira. A união de todas as cláusulas é:

select *
from pessoa
where
nome like nvl(:nome, '%') 
and nascimento between nvl(:data_nascimento, to_date('1000', 'YYYY')) 
                   and nvl(:data_nascimento, to_date('3000', 'YYYY'))
and departamento between nvl(:dept, -1) and nvl(:dept, 1000)
Agrega-se um pouco de complexidade à consulta (que pode, por outro lado, ser facilmente inserida numa procedure), mas simplifica-se o código da aplicação.

Os detratores da programação incondicional levantarão as bandeiras da legibilidade e do desempenho, mas todos sabemos que o mais importante é eliminar os ifs dos programas.