sysd.org
2mai/120

Google Refine + Perl

Google Refine é show. Para quem não conhece, acesse já o site oficial e assista pelo menos o primeiro vídeo. Serve para inúmeras tarefas envolvendo ETL.

Eu, no momento, uso muito para coisas simples, como carregar um CSV, eliminar outliers e salvar em JSON para carregar no MongoDB. Nada que um one-liner em Perl não pudesse fazer.

Infelizmente, a recíproca não é válida: one-liners em Perl são bem mais versáteis do que o Google Refine. Que tal integrar os dois?

  1. Google Refine pode ser facilmente integrado com um web-service qualquer.
  2. Perl transforma one-liners em webservices.
  3. PROFIT!!!

Como exemplo concreto, utilizarei dados georeferenciados. Digamos que preciso eliminar registros duplicatas, e uma das formas de detectar é pela proximidade física dos pontos no mapa. Google Refine não é um GIS, e não faz a menor ideia de como processar latitude/longitude. Entra o GeoDNA: um algoritmo que transforma coordenadas numéricas bidimensionais em uma string com propriedade interessante: quanto mais longo o prefixo compartilhado de dois GeoDNAs, mais perto estão as respectivas coordenadas no mapa (infelizmente, GeoDNAs com prefixos totalmente distintos também podem estar fisicamente próximos; localidades próximas ao meridiano de Greenwich são um exemplo clássico). Portanto, ordenando os registros por GeoDNA, as localidades próximas tenderão a ficar em linhas adjascentes. Para a integração, usaremos o recurso Add column by fetching URLs, clickando em header de qualquer coluna (a coluna não importa pois, de qualquer maneira, usaremos dados de duas):

No diálogo que aparecer, colamos o seguinte código (atenção para o nome das colunas com as respectivas coordenadas):

'http://127.0.0.1:3000/?lat='+
row.cells['latitude'].value
+'&lon='+
row.cells['longitude'].value

Não precisamos do throttle delay, pois o nosso webservice é local. O diálogo deve ficar assim (nada de clickar em OK, ainda):

Agora, certifique-se de que você tem os módulos Mojolicious e Geo::DNA instalados, e rode no terminal:

perl -MGeo::DNA -Mojo -E 'a("/"=>sub{my$s=shift;$s->render(json=>{geocode=>Geo::DNA::encode_geo_dna($s->param("lat"),$s->param("lon"))})})->start' daemon

Se preferir, aqui está a versão "por extenso":

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/usr/bin/env perl
use Geo::DNA qw(encode_geo_dna);
use Mojolicious::Lite;
 
any '/' => sub {
    my $self = shift;
    $self->render(json => {
        geocode => encode_geo_dna(
            $self->param('lat'),
            $self->param('lon'),
        ),
    });
};
 
app->start;

Uma vez iniciado o webservice, clique em OK lá no diálogo do Google Refine e aguarde. Mesmo sem o delay, é relativamente lento; porém o custo/benefício dessa gambiarrinha é, evidentemente, favorável ;)

30abr/120

Implementação de classificador Naïve Bayes em MongoDB

O presente artigo é derivado do que escrevi para o evento Equinócio de Outono de 2012, promovido por São Paulo Perl Mongers.

Porém, ao invés de servir como uma espécie de complemento, é uma "volta às origens". O algoritmo original, desenvolvi em JavaScript puro, depois traduzi para Perl, refinei, detalhei e escrevi sobre. A intenção agora é ser mais show me the code possível, com um exemplo simples, enxuto e prático. Portanto, a teoria e os detalhes continuam lá no artigo original. Aqui, é mão na massa!

Ensinando o classificador

O classificador funciona por palavras-chave, e o MongoDB, nesse estágio, atua como um mero key/value storage. Salve o código de inicialização a seguir como init.js e carregue com mongo bayes init.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
db.bayes.update({_id: 'pelos'}, {$inc: {total: 1, 'categ.gato': 1}}, 1);
db.bayes.update({_id: 'bigodes'}, {$inc: {total: 1, 'categ.gato': 1}}, 1);
db.bayes.update({_id: 'pata'}, {$inc: {total: 1, 'categ.gato': 1}}, 1);
db.bayes.update({_id: 'pata'}, {$inc: {total: 1, 'categ.gato': 1}}, 1);
db.bayes.update({_id: 'pata'}, {$inc: {total: 1, 'categ.gato': 1}}, 1);
db.bayes.update({_id: 'pata'}, {$inc: {total: 1, 'categ.gato': 1}}, 1);
db.bayes.update({_id: 'rabo'}, {$inc: {total: 1, 'categ.gato': 1}}, 1);
db.bayes.update({_id: 'pelos'}, {$inc: {total: 1, 'categ.cachorro': 1}}, 1);
db.bayes.update({_id: 'pata'}, {$inc: {total: 1, 'categ.cachorro': 1}}, 1);
db.bayes.update({_id: 'pata'}, {$inc: {total: 1, 'categ.cachorro': 1}}, 1);
db.bayes.update({_id: 'pata'}, {$inc: {total: 1, 'categ.cachorro': 1}}, 1);
db.bayes.update({_id: 'pata'}, {$inc: {total: 1, 'categ.cachorro': 1}}, 1);
db.bayes.update({_id: 'rabo'}, {$inc: {total: 1, 'categ.cachorro': 1}}, 1);
db.bayes.update({_id: 'pata'}, {$inc: {total: 1, 'categ.galo': 1}}, 1);
db.bayes.update({_id: 'pata'}, {$inc: {total: 1, 'categ.galo': 1}}, 1);
db.bayes.update({_id: 'bico'}, {$inc: {total: 1, 'categ.galo': 1}}, 1);
db.bayes.update({_id: 'asa'}, {$inc: {total: 1, 'categ.galo': 1}}, 1);
db.bayes.update({_id: 'asa'}, {$inc: {total: 1, 'categ.galo': 1}}, 1);
db.bayes.update({_id: 'penas'}, {$inc: {total: 1, 'categ.galo': 1}}, 1);
db.bayes.update({_id: 'bico'}, {$inc: {total: 1, 'categ.ornitorrinco': 1}}, 1);
db.bayes.update({_id: 'pata'}, {$inc: {total: 1, 'categ.ornitorrinco': 1}}, 1);
db.bayes.update({_id: 'pata'}, {$inc: {total: 1, 'categ.ornitorrinco': 1}}, 1);
db.bayes.update({_id: 'pata'}, {$inc: {total: 1, 'categ.ornitorrinco': 1}}, 1);
db.bayes.update({_id: 'pata'}, {$inc: {total: 1, 'categ.ornitorrinco': 1}}, 1);
db.bayes.update({_id: 'rabo'}, {$inc: {total: 1, 'categ.ornitorrinco': 1}}, 1);
db.bayes.update({_id: 'pelos'}, {$inc: {total: 1, 'categ.ornitorrinco': 1}}, 1);

Agora, entrando em MongoDB Interactive Shell com mongo bayes, podemos ver a estrutura resultante com db.mongo.find():

{ "_id" : "bigodes", "categ" : { "gato" : 1 }, "total" : 1 }
{ "_id" : "bico", "categ" : { "galo" : 1, "ornitorrinco" : 1 }, "total" : 2 }
{ "_id" : "asa", "categ" : { "galo" : 2 }, "total" : 2 }
{ "_id" : "penas", "categ" : { "galo" : 1 }, "total" : 1 }
{ "_id" : "pata", "categ" : { "cachorro" : 4, "galo" : 2, "gato" : 4, "ornitorrinco" : 4 }, "total" : 14 }
{ "_id" : "rabo", "categ" : { "cachorro" : 1, "gato" : 1, "ornitorrinco" : 1 }, "total" : 3 }
{ "_id" : "pelos", "categ" : { "cachorro" : 1, "gato" : 1, "ornitorrinco" : 1 }, "total" : 3 }

Cada palavra-chave é associada aos contadores das categorias, além do contador geral de ocorrências. Agora, vamos processar essa estrutura de dados!

Classificador em map/reduce

O código a seguir é quase que um programa independente em JavaScript, salve-o como bayes-mongodb.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
var words = [];
for (var word in doc)
    words.push(word);
 
var categ = [
    'gato',
    'cachorro',
    'galo',
    'ornitorrinco'
];
 
var guess = db.runCommand({
    mapreduce: 'bayes',
    out: { inline: 1 },
    query: {
        _id: {
            $in: words
        }
    },
    scope: {
        categ: categ,
        doc: doc,
    },
    map: function () {
        for (var i = 0; i < categ.length; i++) {
            var ctg = categ[i];
            var prob = Math.log(
                typeof(this.categ[ctg]) != 'undefined'
                ? this.categ[ctg]
                : 1.18e-38
            );
            prob -= Math.log(this.total);
 
            emit(ctg, prob * doc[this._id]);
        }
    },
    reduce: function (key, values) {
        var result = 0;
        values.forEach(function (value) {
            result += value;
        });
        return result;
    }
});
 
var res = { value: -Infinity };
for (var i in guess.results)
    if (guess.results[i].value > res.value)
        res = guess.results[i];
 
print(res._id);

Para passar parâmetros via linha de comando, usamos a opção --eval do MongoDB Shell, e o script imprime na tela a categoria "adivinhada":

$ mongo bayes --quiet --eval 'var doc = { bigodes: 1, rabo: 1 }' bayes-mongodb.js
gato
$ mongo bayes --quiet --eval 'var doc = { bico: 1, rabo: 1, patas: 4 }' bayes-mongodb.js
ornitorrinco
$ mongo bayes --quiet --eval 'var doc = { bico: 1, asa: 2 }' bayes-mongodb.js
galo

Conclusão

O básico do básico esta aí :)
É evidente que os resultados e a eficiência do classificador bayesiano variam de caso para caso, e até de implementação para implementação. Se a primeira tentativa não for um sucesso, se quiser entender para que serve o "número mágico" 1.18e-38, ou se quiser automatizar o "treinamento" através de um pequeno script em Perl, consulte o artigo original!

21abr/120

Y is the new X

Segue aqui uma listinha de utilitários ligeiramente diferentes dos tradicionais e consolidados, mas cujas pequenas e grandes melhorias me fizeram renunciar quase que completamente os mais clássicos e abraçar os bleeding edge ;)

mosh is the new ssh

Não me entenda mal: SSH é muito mais do que uma simples shell remota! Também serve para tunelamento de conexões, transferência de arquivos, autenticação remota... Aliás, o próprio Mosh se autentica via SSH. Só que daí em diante, é outra história.

Mosh utiliza o protocolo UDP ao invés do TCP, então não precisa manter uma conexão. Simplificando muito, Mosh faz sincronização do estado do terminal remoto com o local, ao invés de redesenhar as telas caractere por caractere.

Ou seja: o seu link pode ser muito ruim, que o Mosh não está nem aí. Pode perder conexão, pode colocar o notebook em standby; quando o link voltar, está aí a sua shell, do jeito que você deixou! 'Network error: Connection reset by peer'‎, nunca mais.

Outro "efeito colateral" bacana do Mosh é que ele dispensa buffers. Já me ferrei dando um lsof numa conexão lerda; até que todo o output saia, SSH não deixa nem o input de Ctrl-C passar.

Para finalizar, o comparativo de tempo de resposta de SSH com Mosh, do site oficial do MIT:

tmux is the new screen

Mesmo que você seja um ávido fã do screen, o tmux vale uma espiada. O grande distintivo talvez seja o modelo client/server, que garante uma grande flexibilidade às sessões gerenciadas pelo tmux. Por exemplo, dá para mover janelas entre diversas sessões, ou então replicar o input em várias janelas (útil quando se quer iniciar o mesmo processo em diversos nós). Os atalhos de teclas tem perfis pré-definidos "emprestados" de Vim ou Emacs, e, é claro, dá para redefinir quase tudo do jeito que quiser pelo arquivo de configuração bastante legível. Ah, um bônus: split vertical da tela funciona sem patch adicional :)

Um breve tutorial:

  1. TMUX – The Terminal Multiplexer (Part 1)
  2. TMUX – The Terminal Multiplexer (Part 2)

htop is the new top

Esse dispensa apresentações. É praticamente um 'Activity Monitor.app' versão terminal. Infelizmente, a versão para MacOS tem algumas esquisitices (especialmente na parte que se refere à exibição da quantidade de memória). Ainda assim, é excelente para overview geral do sistema. Sempre mantenho um htop aberto nos meus servidores ;)

ack is the new grep

Descobri o ack recentemente. Não é bem um grep, mas sim uma combinação de find/grep específica para trabalhar com código-fonte. Em suma, ele faz busca recursiva utilizando regexp "sabor Perl", e desconsidera coisas como subdiretórios do Git ou SVN. Por exemplo uma versão (very) quick & dirty do Perl::PrereqScanner seria:

ack -ho --perl '^\s*use\s+[\w+\.:]+' | sort -u

xz is the new bzip2

Um pequeno devaneio: aparentemente, Inteligência Artificial e compressão de dados são assuntos afins. Os organizadores do Hutter Prize, por exemplo, acreditam que uma compressão eficiente de um texto natural seria de dificuldade equivalente a passar no teste de Turing.

Voltando ao tema original: anos atrás, quando clock de um PC high-end não passava de 1 GHz, e conexão broadband ainda estava engatinhando nos países do 3-o mundo, ver um código-fonte compactado com bzip2 numa página de download era de restaurar a fé na humanidade. Pois era evidentemente um ato altruístico por parte do disponibilizador levar muito mais tempo para compactar para poupar um pouquinho de tempo e banda de quem fizer o download.

Hoje, o bzip2 roda na velocidade do gzip de outrora. Eis que revive o algoritmo LZMA: muito mais voraz quanto à CPU/RAM, na grande maioria dos casos chuta bonito o traseiro do bzip2! O conheci através do 7-Zip, de longe o melhor compactador para Windows (xupem, WinZip e WinRAR!). Depois, para a minha alegria, descobri o xz, que:

  1. É mais fácil de digitar do que 'bzip2';
  2. Compacta mais do que o bzip2;
  3. Usa os mesmos parâmetros que o bzip2 (cat ... | xz -cv9 > dump.xz continua válido).

E você, tem algum exemplo assim? Comente!

Easy AdSense by Unreal