Cachorros artificiais: agentes de agent-based modeling usando R (ABM – Parte 1)

(Primeiro Post da série. Veja os outros: Parte 2, Parte 3, Parte 4 e Parte 5)


robot-dog-meets-a-real-doberman-dog

Modelos baseados em agentes (ou Agent-Based Models – ABM) já foram um tópico discutido no blog outras vezes (por exemplo, aqui e aqui). Mas julgo que esse é um assunto “quente”, que sempre vai acabar voltando.

Para aqueles que não sabem ao que estou me referindo, vou dar uma ideia simples e rápida. Trata-se de simulações de algo como “sociedades artificiais” através de modelos computacionais. O propósito NÃO É construir a Matrix ou qualquer coisa do tipo… ao, contrário, é algo bem mais “humilde” e focado. Funciona assim: o pesquisador tem hipóteses sobre como indivíduos ou grupos se comportam, cria um ambiente simples que contém apenas algumas características fundamentais e indispensáveis para a caracterização de uma situação típica e então toca o play. Com isso, pode investigar se os mecanismos interacionais supostos de fato produzem, no nível macro ou agregado, os padrões efetivamente observáveis. Trata-se de um exercício lógico, antes de qualquer outra coisa.

Economistas neoclássicos, trabalhos que envolvem modelagem formal e escolha racional fazem exercícios lógicos de natureza semelhante o tempo todo: “suponha indivíduos homogêneos (idênticos em todas as suas características), com determinado tipo de preferência e orçamento; de fronte a um mercado que se lhes apresente certas quantidades e preços, eles se comportarão desta e daquela maneira, gerando um estado de equilíbrio tal e qual…”. No entanto, nem todo tipo de modelagem deve envolver escolha racional… e nem matemática. Certos problemas são complexos o suficiente para não permitir “forma fechada” (i.e. uma equação bem definida, cuja solução informa um resultado predito). Os ABMs entram em cena exatamente aí.


Um agente é uma entidade conceitual, que pode ser representada de diversas formas e nas mais diversas linguagens de programação. Mas duas propriedades fundamentais devem estar presentes: atributos e ações. Um indivíduo, por exemplo, tem diversas características: idade, peso, altura, raça, sexo, ocupação etc… Esses são os atributos. E também é capaz de agir no mundo de diversas formas: correr, gritar, socializar, procurar emprego etc. Um “agente”, nesse sentido a que me refiro, seria a representação computacional dos aspectos analiticamente relevantes de um indivíduo para determinados fins de pesquisa. Ou seja, ele não contém todos os atributos e ações possíveis de um indivíduo, mas apenas aqueles imprescindíveis para a análise. Isso vai ficar mais claro adiante.

O exemplo que vou dar será utilizando o R, minha “linguagem nativa” (que uso no dia a dia para fazer análises estatísticas). Mas reconheço que ele não é o melhor ambiente pra isso… Existem linguagens de programação específicas para ABM (a mais conhecida é a LOGO, que se tornou bastante popular devido ao software NetLogo, que implementa uma de suas versões). Também bastante utilizada é a linguagem Python — que é uma linguagem de programação completa (não específica para ABM) e tão flexível quando o R (e muito parecida, inclusive). ABMs escritos em Python têm scripts mais simples, diretos e rodam mais rápido; de acordo com minha experiência (mas esse último ponto é controverso).

As estruturas de dados mais simples e comuns no R são vetores, matrizes, listas e data.frames. Basicamente, todos os outros tipos de objetos existentes são algum tipo de combinação ou transformação desses… Além disso, os comandos que executamos também são objetos e são chamados funções. Criar um agente é guardar seus atributos em algum desses tipos de estrutura de dados e representar suas ações por funções. Uma forma conveniente de mesclar atributos e funções num único objeto-agente é fazer uso de um tipo de estrutura de dados pouco utilizada no R, chamada Reference Class (veja mais coisas sobre isso aqui).

Dou um exemplo. Desejamos representar um cachorro como agente. Os atributos relevantes serão a idade e a raça. E as ações possíveis serão latir e nos dizer qual é a sua raça e a sua idade, se lhe perguntarmos (sim! é um cachorro falante! e daí!?). Criaremos uma Reference Class do tipo “Cachorro”

Cachorro = setRefClass("Cachorro",
             fields = c("raca",
                        "idade"),

             methods = list(
                 late = function(){
                     print("Au! Au!")
                 },

                 diz_raca = function(){
                     print(paste("Eu sou um", .self$raca))
                 },

                 diz_idade = function(){
                    print(paste("Tenho", .self$idade,
                                "anos, o que significaria",
                                 7*.self$idade,
                                 "para um humano" ))
                 }
) )

Acredito que para quem conhece um pouco de R, mesmo sem jamais ter visto uma Reference Class antes, o procedimento acima é mais ou menos inteligível. Com o comando setRefClass estamos criando esse novo tipo de objeto — tipo que será chamado “Cachorro”. Ele terá dois atributos, cujos nomes estão declarados no argumento  fields (como vetores character). Depois especificamos as ações que os Cachorros são capazes de realizar: latir, dizer a raça e dizer a idade. Observem uma coisa importante, que é específica das Reference Classes: a expressão .self. Quando .self$raca estamos dizendo que o atributo raça está contido dentro do próprio objeto Cachorro, não é preciso procurá-lo no ambiente principal do R. Essa é a principal característica das Reference Classes: a capacidade de se auto-referenciar. Qualquer pessoa familiarizada com C++,  Java ou Python logo identifica que essa é uma das principais características de qualquer “linguagem orientada a objetos“. Essa expressão simplesmente significa que os objetos criados são auto-contidos: trazem consigo informações, dados e também os métodos para acessá-los, modificá-los e utilizá-los.

Tá… e agora? Agora que definimos as propriedades de um cachorro em abstrato, o próximo passo é efetivamente criar um (é como se tivéssemos definido a “idéia” de cachorro e agora fossemos de fato criar um “cachorro empírico”).

Jimmy = Cachorro$new(raca = "Vira-latas", idade = 4)

Acredito que o código acima seja mais o menos intuitivo. Mas é bom discuti-lo. Observem que estamos utilizando o $ para acessar uma função que está “dentro” do objeto Cachorro, chamada new(). Essa não era uma das ações definidas no argumento methods (late, diz_idade, diz_raca). Trata-se de uma função presente em todo e qualquer objeto do tipo Reference Class, que significa, em termos simples, “crie pra mim uma espécie ou instancia empírica dessa classe”. Informamos as características daquele exemplar particular de cachorro (Vira-latas, com 4 anos). E pronto, fiat Jimmy. Esse é o Jimmy “por dentro”:

jimmy1

Ele é capaz de latir, dizer a idade e a sua raça:

jimmy2

Vamos agora criar novamente a classe Cachorro, mas com duas novas capacidades de ação: dar a pata e abaixá-la. Além dessas duas novas funções, que deverão ser informada dentro da lista passada para o argumento methods, temos que também definir um novo atributo, o “status” da pata (i.e., se está levantada ou abaixada). Se já estiver levantada e pedirmos mesmo assim para que ele a levante, o cachorro vai nos informar. O mesmo se já estiver abaixada e ainda assim pedirmos a ele para abaixá-la.

Cachorro = setRefClass("Cachorro",
             fields = c("raca",
                        "idade",
                        "pata"),

             methods = list(
                 initialize = function(pata = 0, ...){
                     .self$pata = pata
                     callSuper(...)
                 },
                 late = function(){
                     print("Au! Au!")
                 },

                 diz_raca = function(){
                     print(paste("Eu sou um", .self$raca))
                 },

                 diz_idade = function(){
                    print(paste("Tenho", .self$idade,
                                "anos, o que significaria",
                                 7*.self$idade,
                                 "para um humano" ))
                 },

                 da_pata = function(){
                     if( .self$pata == 0 ){
                         .self$pata = 1
                         print("Levantei a pata")
                     }else{
                         print("Já te dei a pata, oras...!")
                     }
                 },

                 abaixa_pata = function(){
                     if( .self$pata == 0 ){
                         print("Minha pata já está abaixada!")
                     }else{
                         .self$pata = 0
                         print("Abaixei a pata")
                 }
}
) )

Observem que uma outra função teve que ser definida dentro de methods, denominada initialize. Essa função simplesmente especifica valores-padrão que serão passados para todos os agentes daquela classe no momento de sua criação. Neste caso, todos os cachorros começam com pata = 0 (com a pata abaixada). Vejamos agora o que o Tobby, nosso novo cachorro, faz:

jimmy3

Tobby não é bobo. Não peça pra que ele dê a pata ou a abaixe duas vezes seguidas. E Tobby tem a capacidade de “modificar-se a si mesmo”. Quando levanta ou abaixa a pata, ele altera o atributo pata, que existe dentro de si.

O ponto aqui é que Reference Classes são adequadas para fazer uma aproximação operacional dos conceitos de atributos e ações. Poderíamos fazer isso de inúmeros outros modos, sem apelar para programação orientada a objeto. Mas há simplicidade nos códigos acima. Agentes agem (ou, nos termos de Taylor Swift, “players are gonna play”). Essa representação sugere uma analogia com a realidade. Como diria meu amigo Davoud, especialista em ABM, “it’s all about ontology”.