加载中...
sysd.org
2jun/122

Google Refine + Perl (English)

(Portuguese version here)

Google Refine is awesome. If you're unaware of what it is, access their official page and watch at least the first screencast. You'll see it can be helpful for several ETL-related tasks.

Currently, I use it a lot, specially for simple (but boring) tasks, like loading a CSV, trimming out some outliers and saving as JSON to be imported into MongoDB. Nothing a Perl one-liner couldn't do.

However, the opposite is not true: Perl one-liners are a lot more flexible than Google Refine. Now, what if we could merge both?

  1. Google Refine could be easily integrated with any RESTful API.
  2. Perl transforms one-liners into RESTful webservices.
  3. PROFIT!!!

As a practical example, I'll use some georeferenced data I was working at. Let's suppose I have to deduplicate registers, and one of "duplicate" rules is their proximity on the map. Google Refine is far from a full-featured GIS, and is unable to handle bidimensional coordinate system. Enter the GeoDNA: an algorithm to lower geospatial dimensions. As it's FAQ says,

GeoDNA is a way to represent a latitude/longitude coordinate pair as a string. That sounds simple enough, but it's a special string format: the longer it is, the more accurate it is. More importantly, each string uniquely defines a region of the earth's surface, so in general, GeoDNA codes with similar prefixes are located near each other. This can be used to perform proximity searching using only string comparisons (like the SQL "LIKE" operator).

Another interesting property of GeoDNA is that when ordening a set of records by their GeoDNA code, close locations are likely to appear in adjacent rows (sometimes, close locations will share very different prefixes, but similar prefixes always represent close locations).

To incorporate GeoDNA into Google Refine, we'll use the Add column by fetching URLs option, clicking on the header of any column (which column it will be doesn't matter as we'll use two of them, anyway):

As the expression, we'll paste the following code (here, pay attention to the correct latitude/longitude column names):

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

Throttle delay can be zeroed, as our webservice is local. The final configuration should look like this (don't push the OK button, yet):

Now, check if you have Mojolicious and Geo::DNA Perl modules (install them via CPAN, if not) and paste into your 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

If you prefer a "human-readable" version, paste the following code into geocode-webservice.pl:

#!/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;

Once you started a webservice, it will report Server available at http://127.0.0.1:3000. Now, click OK on Google Refine dialog and wait. Even without delay, it could be a bit slow; however, even then this hack saved me a lot of time 😉

27maio/120

SQL <=> MongoDB

(clique para ampliar)

Esse post nada mais é do que a divulgação de um fabuloso infográfico feito por Rick Osborne. Esbarrei nele buscando por alguma coisa relacionada ao MongoDB que não lembro mais, e fiquei encantado. Longe de ser algo que ninguém nunca fez (exemplos do driver do Perl são, convenhamos, mais práticos), ficou condensado e elegante. Have fun!

Attached Files:

30abr/122

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!