Você está na página 1de 142

Pense Complexidade

Versão 2.3
Pense Complexidade

Versão 2.3

Allen B. Downey

Green Tea Press


Needham, Massachusetts
Copyright © 2016 Allen B. Downey.

Green Tea Press


9 Washburn Ave
Needham MA 02492

Permission is granted to copy, distribute, transmit and adapt this work under a
Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International
License: http://creativecommons.org/licenses/by-nc-sa/4.0/.

If you are interested in distributing a commercial version of this work, please


contact the author.

The LATEX source for this book is available from

http://greenteapress.com/complexity
iv
Sumário

Prefácio xi
0.1 Para quem é este livro? . . . . . . . . . . . . . . . . . . . . . xiii
0.2 Usando o código . . . . . . . . . . . . . . . . . . . . . . . . . xiii

1 Ciência da Complexidade 1
1.1 Mudança de paradigma? . . . . . . . . . . . . . . . . . . . . 3
1.2 Os eixos dos modelos científicos . . . . . . . . . . . . . . . . 5
1.3 Um novo tipo de modelo . . . . . . . . . . . . . . . . . . . . 7
1.4 Um novo tipo de engenharia . . . . . . . . . . . . . . . . . . 8
1.5 Um novo tipo de pensamento . . . . . . . . . . . . . . . . . 9

2 Grafos 11
2.1 O que é um grafo? . . . . . . . . . . . . . . . . . . . . . . . 11
2.2 NetworkX . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
2.3 Grafos aleatórios . . . . . . . . . . . . . . . . . . . . . . . . 16
2.4 Gerando grafos . . . . . . . . . . . . . . . . . . . . . . . . . 17
2.5 Grafos conectados . . . . . . . . . . . . . . . . . . . . . . . . 18
2.6 Gerando grafos ER . . . . . . . . . . . . . . . . . . . . . . . 21
2.7 Probabilidade de conectividade . . . . . . . . . . . . . . . . 22
2.8 Análise de algoritmos de grafos . . . . . . . . . . . . . . . . 25
vi SUMÁRIO

2.9 Exercícios . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26

3 Grafos de pequeno mundo 29


3.1 Stanley Milgram . . . . . . . . . . . . . . . . . . . . . . . . . 29
3.2 Watts e Strogatz . . . . . . . . . . . . . . . . . . . . . . . . 30
3.3 Rede em anel . . . . . . . . . . . . . . . . . . . . . . . . . . 32
3.4 Grafos WS . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
3.5 Agrupamento . . . . . . . . . . . . . . . . . . . . . . . . . . 36
3.6 Comprimentos dos caminhos mais curtos . . . . . . . . . . . 37
3.7 A experiência de WS . . . . . . . . . . . . . . . . . . . . . . 38
3.8 Que tipo de explicação é essa? . . . . . . . . . . . . . . . . . 40
3.9 Busca em largura . . . . . . . . . . . . . . . . . . . . . . . . 42
3.10 O algoritmo de Dijkstra . . . . . . . . . . . . . . . . . . . . 44
3.11 Exercícios . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46

4 Redes livres de escala 51


4.1 Dados de redes sociais . . . . . . . . . . . . . . . . . . . . . 51
4.2 Modelo WS . . . . . . . . . . . . . . . . . . . . . . . . . . . 54
4.3 Grau . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55
4.4 Distribuições de cauda pesada . . . . . . . . . . . . . . . . . 57
4.5 Modelo de Barabási-Albert . . . . . . . . . . . . . . . . . . . 59
4.6 Gerando grafos BA . . . . . . . . . . . . . . . . . . . . . . . 61
4.7 Distribuições acumulativas . . . . . . . . . . . . . . . . . . . 63
4.8 Modelos explicativos . . . . . . . . . . . . . . . . . . . . . . 66
4.9 Exercícios . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68

5 Autômatos Celulares 71
SUMÁRIO vii

5.1 Um AC simples . . . . . . . . . . . . . . . . . . . . . . . . . 71
5.2 A experiência de Wolfram . . . . . . . . . . . . . . . . . . . 72
5.3 Classificação de ACs . . . . . . . . . . . . . . . . . . . . . . 74
5.4 Aleatoriedade . . . . . . . . . . . . . . . . . . . . . . . . . . 75
5.5 Determinismo . . . . . . . . . . . . . . . . . . . . . . . . . . 76
5.6 Espaçonaves . . . . . . . . . . . . . . . . . . . . . . . . . . . 78
5.7 Universalidade . . . . . . . . . . . . . . . . . . . . . . . . . . 79
5.8 Falseabilidade . . . . . . . . . . . . . . . . . . . . . . . . . . 82
5.9 Isso é um modelo de quê? . . . . . . . . . . . . . . . . . . . 83
5.10 Implementação de ACs . . . . . . . . . . . . . . . . . . . . . 85
5.11 Correlação cruzada . . . . . . . . . . . . . . . . . . . . . . . 87
5.12 Tabelas AC . . . . . . . . . . . . . . . . . . . . . . . . . . . 89
5.13 Exercícios . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90

6 Jogo da Vida 93
6.1 Jogo da Vida de Conway . . . . . . . . . . . . . . . . . . . . 93
6.2 Padrões de vida . . . . . . . . . . . . . . . . . . . . . . . . . 95
6.3 Conjectura de Conway . . . . . . . . . . . . . . . . . . . . . 97
6.4 Realismo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 99
6.5 Instrumentalismo . . . . . . . . . . . . . . . . . . . . . . . . 101
6.6 Implementando Vida . . . . . . . . . . . . . . . . . . . . . . 102
6.7 Exercícios . . . . . . . . . . . . . . . . . . . . . . . . . . . . 105

7 Physical modeling 109


7.1 Diffusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 109
7.2 Reaction-diffusion . . . . . . . . . . . . . . . . . . . . . . . . 111
7.3 Percolation . . . . . . . . . . . . . . . . . . . . . . . . . . . . 115
viii SUMÁRIO

7.4 Phase change . . . . . . . . . . . . . . . . . . . . . . . . . . 116


7.5 Fractals . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 119
7.6 Fractals and Percolation Models . . . . . . . . . . . . . . . . 121
7.7 Exercises . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 123

8 Self-organized criticality 125


8.1 Critical Systems . . . . . . . . . . . . . . . . . . . . . . . . . 125
8.2 Sand Piles . . . . . . . . . . . . . . . . . . . . . . . . . . . . 126
8.3 Implementing the Sand Pile . . . . . . . . . . . . . . . . . . 127
8.4 Heavy-tailed distributions . . . . . . . . . . . . . . . . . . . 131
8.5 Fractals . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 133
8.6 Spectral Density . . . . . . . . . . . . . . . . . . . . . . . . . 137
8.7 Reductionism and Holism . . . . . . . . . . . . . . . . . . . 140
8.8 SOC, causation and prediction . . . . . . . . . . . . . . . . . 142
8.9 Exercises . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 144

9 Modelos baseados em agentes 147


9.1 Modelo de Schelling . . . . . . . . . . . . . . . . . . . . . . . 148
9.2 Implementação do modelo de Schelling . . . . . . . . . . . . 149
9.3 Segregação . . . . . . . . . . . . . . . . . . . . . . . . . . . . 151
9.4 Sugarscape . . . . . . . . . . . . . . . . . . . . . . . . . . . . 153
9.5 Desigualdade de riqueza . . . . . . . . . . . . . . . . . . . . 156
9.6 Implementação de Sugarscape . . . . . . . . . . . . . . . . . 158
9.7 Migração e Comportamento de Ondas . . . . . . . . . . . . . 160
9.8 Emergência . . . . . . . . . . . . . . . . . . . . . . . . . . . 161
9.9 Exercícios . . . . . . . . . . . . . . . . . . . . . . . . . . . . 163
SUMÁRIO ix

10 Herds, Flocks, and Traffic Jams 165


10.1 Traffic jams . . . . . . . . . . . . . . . . . . . . . . . . . . . 165
10.2 Random Noise . . . . . . . . . . . . . . . . . . . . . . . . . . 169
10.3 Boids . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 170
10.4 Free will . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 173
10.5 Exercises . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 175

11 Evolution 177
11.1 Simulating evolution . . . . . . . . . . . . . . . . . . . . . . 178
11.2 Fitness landscape . . . . . . . . . . . . . . . . . . . . . . . . 179
11.3 Agents . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 180
11.4 Simulation . . . . . . . . . . . . . . . . . . . . . . . . . . . . 181
11.5 No differentiation . . . . . . . . . . . . . . . . . . . . . . . . 183
11.6 Evidence of evolution . . . . . . . . . . . . . . . . . . . . . . 183
11.7 Differential survival . . . . . . . . . . . . . . . . . . . . . . . 186
11.8 Mutation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 188
11.9 Speciation . . . . . . . . . . . . . . . . . . . . . . . . . . . . 190
11.10 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . 193

12 Game Theory 195


12.1 Prisoner’s Dilemma . . . . . . . . . . . . . . . . . . . . . . . 196
12.2 The problem of nice . . . . . . . . . . . . . . . . . . . . . . . 197
12.3 Prisoner’s dilemma tournaments . . . . . . . . . . . . . . . . 199
12.4 Simulating evolution of cooperation . . . . . . . . . . . . . . 200
12.5 The Tournament . . . . . . . . . . . . . . . . . . . . . . . . 202
12.6 The Simulation . . . . . . . . . . . . . . . . . . . . . . . . . 205
12.7 Results . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 206
x SUMÁRIO

12.8 Conclusions . . . . . . . . . . . . . . . . . . . . . . . . . . . 210


12.9 Exercises . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 211

13 Outro 213

A Analysis of algorithms 215


A.1 Order of growth . . . . . . . . . . . . . . . . . . . . . . . . . 216
A.2 Analysis of basic Python operations . . . . . . . . . . . . . . 219
A.3 Analysis of search algorithms . . . . . . . . . . . . . . . . . . 222
A.4 Hashtables . . . . . . . . . . . . . . . . . . . . . . . . . . . . 223
A.5 Summing lists . . . . . . . . . . . . . . . . . . . . . . . . . . 228
A.6 pyplot . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 230

B Reading list 233


Prefácio

Este livro trata principalmente sobre ciência da complexidade, mas trata tam-
bém sobre estruturas de dados e algoritmos, programação intermediária em
Python, modelagem computacional e filosofia da ciência:

Ciência da complexidade: Complexidade é um tema multidisciplinar—uma


interseção de matemática, ciências da computação e ciências naturais—
que foca em modelos discretos de sistemas físicos e sociais. Em par-
ticular, foca em sistemas complexos, que são sistemas com muitos
componentes interagindo entre si.

Estruturas de dados: Uma estrutura de dados é uma coleção de dados or-


ganizada de maneira a suportar certas operações. Por exemplo, um
dicionário Python organiza pares chave-valor de maneira a oferecer um
rápido mapeamento de chaves para valores, enquanto o mapeamento de
valores para chaves é mais lento.

Algoritmos: Um algoritmo é um processo para execução de uma computação.


O projeto de programas eficientes frequentemente envolve a coevolução
de estruturas de dados e do algoritmo que as usa. Por exemplo, nos
primeiros capítulos eu apresento grafos, estruturas de dados que imple-
mentam grafos e algoritmos para grafos baseados nessas estruturas de
dados.

Modelagem computacional: Um modelo é uma descrição simplificada de


um sistema usado para simulação ou análise. Modelos computacionais
são projetados para tirarem proveito de computação barata e rápida.

Filosofia da ciência: Os experimentos e resultados neste livro suscitam ques-


tões relevantes à filosofia da ciência, incluindo a natureza das leis cientí-
xii Capítulo 0 Prefácio

ficas, teoria da escolha, realismo e instrumentalismo, holismo e reducio-


nismo, e epistemologia.
Sistemas complexos incluem redes e grafos, autômatos celulares, modelos ba-
seados em agentes e enxames1 , fractais e sistemas auto-organizados, sistemas
caóticos e sistemas cibernéticos. Estes termos podem não significar muito para
você neste momento. Chegaremos a eles em breve, mas você pode ter uma
prévia em https://pt.wikipedia.org/wiki/Sistemas_complexos2 .

Eu espero que este livro ajude os leitores a explorarem um conjunto de tópicos e


ideias com os quais não se deparariam de outra maneira, a praticar habilidades
de programação em Python, e a aprender mais sobre estruturas de dados e
algoritmos (ou rever algum assunto que possa ter despertado menos interesse
numa outra ocasião).

As características deste livro incluem:


Detalhes técnicos Há vários livros sobre sistemas complexos, mas a maioria
deles foi escrito para o público geral. Normalmente esses livros não
abordam detalhes técnicos, o que é frustrante para as pessoas capazes
de lidar com eles. Este livro apresenta a matemática, o código e todo o
material necessário para realmente entender este trabalho.
Leitura adicional Ao longo do livro eu incluí ponteiros para leitura adici-
onal, incluindo artigos originais (a maior parte deles disponíveis eletro-
nicamente) e artigos relacionados da Wikipédia e outras fontes. Alguns
professores possuem uma reação alérgica à Wikipédia, baseados no fato
de que os alunos dependeriam fortemente de uma fonte supostamente
não confiável. Como muitas das minhas referências são para artigos da
Wikipédia, eu quero explicar o meu raciocínio. Primeiro, os artigos so-
bre ciência da complexidade e tópicos relacionados costumam ser bons;
segundo, eles são escritos em um nível que é acessível depois que você
tiver lido este livro (mas algumas vezes não antes disso); e, finalmente,
eles estão disponíveis de graça para leitores de todo o mundo. Se há
um perigo em encaminhar os leitores para essas referências, não é pelo
material ser duvidoso, mas pelo fato que os leitores não voltarão!
1
N.T.: O termo em inglês swarm também é bastante usado na literatura em português.
2
N.T.: Como muitas vezes os artigos da Wikipédia (português) possuem menos con-
teúdo do que os artigos da Wikipedia (inglês), os links originalmente apresentados na ver-
são em inglês do livro serão fornecidos em notas de rodapé, quando existirem - http:
//en.wikipedia.org/wiki/Complex_systems.
0.1 Para quem é este livro? xiii

Jupyter notebooks Para cada capítulo eu forneço um Jupyter notebook que


inclui o código do capítulo, exemplos adicionais (e especialmente anima-
ções), e sugestões de experimentos que você pode executar com pequenas
mudanças no código.

Exercícios e soluções Ao final de cada capítulo eu sugiro exercícios que você


pode tentar resolver e forneço também as soluções.

0.1 Para quem é este livro?


Os exemplos e códigos de apoio deste livro são em Python. Você deve saber
o núcleo de Python e deve estar familiarizado com recursos de orientação a
objetos; pelo menos com o uso de objetos e, talvez, com como definir o seu
próprio objeto.

Se você ainda não estiver familiarizado com Python, pode ser de seu inte-
resse começar com o meu outro livro, Pense Python, que é uma introdução
ao Python para aqueles que nunca programaram, ou com o livro Aprendendo
Python de Mark Lutz, que pode ser mais adequado para aqueles que já têm
alguma experiência com programação.

Eu faço um uso considerável de NumPy e SciPy. Se você já está familiarizado


com esses módulos, excelente, mas também vou explicar as funções e estruturas
de dados que eu uso.

Eu assumo que o leitor sabe matemática básica, incluindo números complexos.


Você não precisa de muito cálculo; se você entende os conceitos de integração
e diferenciação, isso será suficiente. Eu uso um pouco de álgebra linear, mas
explicarei o conteúdo à medida que avançarmos.

0.2 Usando o código


Todo o código usado neste livro está disponível em https://github.com/
AllenDowney/ThinkComplexity2. Se você não está familiarizado com Git,
saiba que é um sistema de controle de versão que permite manter um registro
histórico dos arquivos que compõem um projeto. Um conjunto de arquivos
xiv Capítulo 0 Prefácio

sob controle do Git é chamado de “repositório”. GitHub é um serviço de


hospedagem que ofereçe espaço de armazenamento de repositórios Git e uma
conveniente interface web.

A página inicial do meu repositório no GitHub oferece várias maneiras de


trabalhar com o código:

• Você pode criar uma cópia do meu repositório no GitHub apertando no


botão Fork. Se você ainda não tem uma conta no GitHub, você terá
que criar uma. Depois de fazer o fork você terá seu próprio repositório
no GitHub, o qual você pode usar para manter registros do código que
você escreve enquanto trabalha com este livro. A seguir você pode clo-
nar o repositório, o que significa fazer uma cópia dos arquivos no seu
computador.

• Você pode simplesmente clonar o meu repositório. Para isso você não
precisa de uma conta no GitHub, mas você não será capaz de escrever
suas alterações de volta para o GitHub.

• Se você não quiser usar o Git de maneira nenhuma, você pode baixar
os arquivos em um arquivo Zip usando o botão verde que diz “Clone or
download”.

Todo o código foi escrito para funcionar tanto em Python 2 como em Python
3 sem a necessidade de tradução.

Eu preparei este livro usando o Anaconda da Continuum Analytics, uma


distribuição gratuita de Python que inclui todos os pacotes que serão ne-
cessários para executar o código (e muito mais). Eu achei o Anaconda fá-
cil de instalar. Por padrão, é feita uma instalação de usuário não de sis-
tema, então privilégios de administrador não são necessários. O Anaconda
tem suporte para Python 2 e Python 3. Você pode baixar o Anaconda em
http://continuum.io/downloads.

Se você não quiser usar Anaconda, os seguintes pacotes serão necessários:

• NumPy para computação numérica básica, http://www.numpy.org/;

• SciPy para computação científica, http://www.scipy.org/;

• matplotlib para visualização, http://matplotlib.org/.


0.2 Usando o código xv

Apesar de esses pacotes serem bastante usados, eles não estão incluídos em
todas as instalações de Python e pode ser difícil instalá-los em alguns ambien-
tes. Se você tiver problemas instalando-os, eu recomendo usar Anaconda ou
uma das outras distribuições Python que incluem esses pacotes.

O repositório inclui scripts em Python e vários Jupyter notebooks. Se você


nunca tiver usado o Jupyter, você pode ler sobre ele em http://jupyter.org.

Há três maneiras de trabalhar com Jupyter notebooks:


Executar o Jupyter no seu computador Se você instalou o Anaconda,
você provavelmente também instalou o Jupyter por padrão. Para ve-
rificar, inicialize o servidor a partir da linha de comando, como segue:
$ jupyter notebook
Se não estiver instalado, você pode instalá-lo usando conda, um geren-
ciado de pacotes usado pelo Anaconda.
$ conda install jupyter
Quando você inicializar o servidor, o seu navegador web padrão deve
abrir ou uma nova aba deve ser criada numa janela já aberta do nave-
gador.
Executar o Jupyter no Binder Binder é um serviço que executa o Jupyter
em uma máquina virtual. Se você seguir este link, http://mybinder.
org/repo/AllenDowney/ThinkComplexity2, você deve ver uma página
do Jupyter com os notebooks deste livro e os dados e scripts de apoio.
Você pode executar os scripts e modificá-los para executar seu próprio
código, mas a máquina virtual em que você pode executá-los é temporá-
ria. Qualquer mudança que você fizer desaparecerá junto com a máquina
virtual se você deixá-la ociosa por mais do que meia hora.
Ver os notebooks no GitHub O GitHub fornece uma visualização dos no-
tebooks que você pode usar para lê-los e para ver os resultados que eu
gerei, mas você não poderá modificar ou executar o código. Nos capítulos
mais à frente você não poderá ver as animações movimentarem-se.
Boa sorte e divirta-se!
Allen B. Downey
Professor de Ciências da Computação
Olin College of Engineering
Needham, MA
xvi Capítulo 0 Prefácio

Lista de Contribuidores
Se você tiver uma sugestão ou correção, por favor envie um email para
downey@allendowney.com3 . Se eu fizer uma alteração baseada no seu co-
mentário, eu acrescentarei o seu nome à lista de contribuidores (a não ser que
você peça para ser omitido).

Se você incluir pelo menos uma parte da frase em que o erro aparece, facilitará
que eu o encontre. Números de página e seção também servem, mas com elas
não é tão fácil de se trabalhar. Obrigado!

• John Harley, Jeff Stanton, Colden Rouleau e Keerthik Omanakuttan são es-
tudantes de Modelagem Computacional que apontaram erros de digitação.

• Jose Oscar Mur-Miranda encontrou vários erros de digitação.

• Phillip Loh, Corey Dolphin, Noam Rubin e Julian Ceipek encontraram erros
de digitação e fizeram sugestões úteis.

• Sebastian Schöner enviou duas páginas de correções!

• Philipp Marek enviou um série de correções.

Outras pessoas que relataram erros são Richard Hollands, Muhammad Najmi bin
Ahmad Zabidi, Alex Hantman e Jonathan Harford.

3
Comentários a respeito da edição em português do livro devem ser encaminhados para
rodrigo.carlson@ufsc.br
Capítulo 1

Ciência da Complexidade

A tese deste livro é que a ciência da complexidade é um “novo tipo de ciência”,


frase que estou tomando emprestada de Stephen Wolfram.

Em 2002, Wolfram publicou A New Kind of Science1 , onde apresenta os tra-


balhos dele e de outros sobre autômatos celulares e descreve uma abordagem
científica para o estudo de sistemas computacionais. Nós voltaremos ao Wol-
fram no Capítulo 5, mas por ora eu quero usar o título de seu livro para algo
um pouco mais amplo.

Eu não acho que complexidade seja algo novo por aplicar ferramentas da ci-
ência para um novo assunto, mas porque usa ferramenta diferentes, permite
tipos diferentes de trabalho e, em última análise, muda o que queremos dizer
com “ciência”.

Para demonstrar a diferença, vou começar com um exemplo de ciência clássica:


suponha que alguém lhe perguntou o porquê das órbitas dos planetas serem
elípticas. Você pode recorrer à lei da gravitação universal de Newton e usá-la
para escrever uma equação diferencial que descreve o movimento planetário.
Então você poderia resolver a equação diferencial e mostrar que a solução é
uma elipse. CQD!

A maioria das pessoas acha este tipo de explicação satisfatória. Ela inclui
uma derivação matemática—então tem algum rigor de uma prova—e explica
1
N.T.: Um Novo Tipo de Ciência, livro sem tradução para a língua portuguesa.
2 Capítulo 1 Ciência da Complexidade

uma observação específica, órbitas elípticas, apelando a um princípio geral, a


gravitação.

Deixe-me contrastar isso com um tipo diferente de explicação. Suponha que


você se mude para uma cidade como Detroit que é segregada racialmente, e que
você queira saber por que a cidade é assim. Se você fizer um pouco de pesquisa,
pode ser que encontre um artigo do Thomas Schelling chamado “Dynamic
Models of Segregation”2 , que propõe um modelo simples de segregação racial.

Eis a minha descrição do modelo, do Capítulo 9:

O modelo de Schelling de uma cidade é um array3 de células em que


cada célula representa uma casa. As casas são ocupadas por dois
tipos de “agentes”, identificados em vermelho e azul, em quantida-
des aproximadamente iguais. Cerca de 10% das casas estão vazias.
A todo momento, um agente pode estar feliz ou infeliz, dependendo
dos outros agentes da vizinhança. Em uma versão do modelo, os
agentes estão felizes se têm pelo menos dois vizinhos como eles e
infelizes se tiverem um ou zero.
A simulação prossegue escolhendo um agente aleatoriamente e ve-
rificando se ele está feliz. Se estiver, nada acontece; se não estiver,
o agente escolhe uma das células desocupadas e se muda.

Se você começar com uma cidade simulada que é totalmente não segregada e
executar o modelo por um curto período de tempo, agrupamentos de agentes
similares aparecem. Com o passar do tempo, os agrupamentos crescem e
coalescem até que haja uma pequena quantidade de agrupamentos grandes e
a maioria dos agentes viva em vizinhanças homogêneas.

O grau de segregação no modelo é surpreendente e sugere um explicação para


a segregação em cidades reais. Talvez Detroit seja segregada porque as pessoas
preferem não estar em quantidade inferior e se mudarão se a composição na
sua vizinhança as faz sentirem-se infeliz.

Esta explicação é tão satisfatória quanto a explicação sobre o movimento pla-


netário? Muitas pessoas diriam que não. Mas por quê?
2
N.T.: Do inglês, Modelos Dinâmicos de Segregação.
3
N.T.: O termo array será mantido em inglês em todo o texto.
1.1 Mudança de paradigma? 3

O mais óbvio é que o modelo de Schelling é altamente abstrato, diga-se, não


realístico. É tentador dizer que pessoas são mais complicadas do que plane-
tas. Mas quando você pensa a respeito, planetas são tão complicados quanto
pessoas (especialmente aqueles que têm pessoas).

Ambos os sistemas são complicados e ambos os sistemas são baseados em


simplificações. Por exemplo, no modelo de movimento planetário nós incluímos
as forças entre o planeta e seu sol e ignoramos interações entre os planetas.

Um diferença importante é que no caso do movimento planetário, nós podemos


defender o modelo demonstrando que as forças que ignoramos são menores do
que aquelas que consideramos. Podemos também incorporar ao modelo outras
interações e mostrar que elas são efetivamente pequenas. Para o modelo de
Schelling é difícil de justificar as simplificações.

Para piorar as coisas, o modelo de Schelling não recorre a nenhuma lei da física,
usa apenas computação simples, e não derivação matemática. Modelos como
o de Schelling não se parecem com ciência clássica e muitas pessoas os acham
menos convincentes, pelo menos no início. Mas, como vou tentar demonstrar,
esses modelos fazem trabalhos úteis, incluindo predição, explicação e projeto.
Um dos objetivos deste livro é explicar como.

1.1 Mudança de paradigma?


Quando descrevo este livro para as pessoas, muitas vezes me perguntam se
esse novo tipo de ciência é uma mudança de paradigma. Eu não penso assim,
e aqui está o motivo.

Thomas Kuhn introduziu a expressão “mudança de paradigma” em A Estru-


tura das Revoluções Científicas em 1962. Refere-se a um processo na história
da ciência no qual os pressupostos básicos de uma área mudam, ou no qual
uma teoria é substituída por outra. Ele apresenta como exemplos a revolução
Copernicana, a substituição da Teoria do Flogisto pela Teoria da Combustão
por queima de oxigênio e o surgimento da relatividade.

O desenvolvimento da ciência da complexidade não é a substituição de um


antigo modelo, mas (na minha opinião) uma mudança gradual nos critérios
4 Capítulo 1 Ciência da Complexidade

pelos quais os modelos são julgados e nos tipos de modelos que são considerados
aceitáveis.

Por exemplo, modelos clássicos costumam ser baseados em leis, expressos na


forma de equações e resolvidos por derivações matemáticas. Modelos que se
enquadram no âmbito da complexidade são muitas vezes baseados em regras,
expressos por computações e são simulados ao invés de analisados.

Nem todos acham esses modelos satisfatórios. Por exemplo, em Sync4 , Steven
Strogatz escreve sobre seu modelo de sincronização espontânea em algumas es-
pécies de vaga-lumes. Ele apresenta uma simulação que demonstra o fenômeno
e então escreve:

Eu repeti a simulação dezenas de vezes, para outras condições ini-


ciais aleatórias e para outros números de osciladores. Sincronizou
todas as vezes. [...] O desafio agora era provar isso. Apenas uma
prova sólida demonstraria, de uma maneira que nenhum computa-
dor poderia fazê-lo, que a sincronização era inevitável; e o melhor
tipo de prova mostraria por que a sincronização era inevitável.

Strogatz é um matemático, então o entusiasmo dele por provas é compreensí-


vel. Mas a prova dele não aborda o que é, para mim, a parte mais interessante:
o fenômeno. Para provar que “a sincronização era inevitável”, Strogatz pressu-
põe várias simplificações, em particular assume que cada vaga-lume consegue
enxergar todos os outros.

Na minha opinião, é mais interessante explicar como que em um vale cheio


de vaga-lumes, eles conseguem sincronizar apesar do fato de não conseguirem
enxergar todos uns aos outros. Como este tipo de comportamento global surge
das interações locais é assunto do Capítulo 9. Explicações desses fenômenos
frequentemente usam modelos baseados em agentes, os quais exploram (de ma-
neiras que seriam difíceis ou impossíveis com análise matemática) as condições
que permitem ou impedem a sincronização.

Eu sou um cientista da computação, então meu entusiasmo por modelos com-


putacionais provavelmente não é nenhuma surpresa. Não é minha intenção
N.T.: Sync: How Order Emerges from Chaos in the Universe, Nature, and Daily Life
4

(Sync: Como a ordem surge do caos no universo, natureza e dia-a-dia), livro sem tradução
para a língua portuguesa.
1.2 Os eixos dos modelos científicos 5

dizer que o Strogatz está errado, mas sim que as pessoas têm opiniões dife-
rentes sobre quais perguntas fazer e quais ferramentas usar para respondê-las.
Essas opiniões são baseadas em julgamentos de valor, portanto não há motivo
para esperar que estejam de acordo.

Há, no entanto, um consenso aproximado entre os cientistas sobre quais mode-


los são considerados boa ciência e quais modelos são ciência marginal5 , pseu-
dociência ou não são ciência nenhuma.

Eu alego, e esta é a tese central deste livro, que os critérios em que se baseia
este consenso muda ao longo do tempo, e que o surgimento da ciência da
complexidade reflete a mudança gradual nesses critérios.

1.2 Os eixos dos modelos científicos


Eu descrevi os modelos clássicos como sendo baseados em leis da física, ex-
pressos na forma de equações e resolvidos por análise matemática. Por outro
lado, modelos de sistemas complexos são frequentemente baseados em regras
simples e implementados como computações.

Podemos pensar nesta tendência como uma mudança ao longo do tempo em


dois eixos:

Baseado em equações → baseado em simulações

Análise → computação

O novo tipo de ciência é diferente em vários outros aspectos. Eu os apresento


aqui para você saiba o que está por vir, mas alguns deles podem não fazer
sentido até que você veja os exemplos mais à frente no livro.

Contínuo → discreto Modelos clássicos tendem a ser baseados em matemá-


tica contínua, como cálculo; modelos de sistemas complexos são frequen-
temente baseados em matemática discreta, incluindo grafos e autômatos
celulares.
5
N.T.: Do inglês, fringe science.
6 Capítulo 1 Ciência da Complexidade

Linear → não linear Modelos clássicos frequentemente são lineares, ou usam


aproximações lineares de sistemas não lineares; ciência da complexidade
é mais amigável com os modelos não lineares. Um exemplo é a teoria do
caos6 .

Determinístico → estocástico Modelos clássicos normalmente são deter-


minísticos, o que pode refletir um determinismo filosófico subjacente dis-
cutido no Capítulo 5; modelos complexos frequentemente apresentam
aleatoriedade.

Abstrato → detalhado Em modelos clássicos, planetas são massas punti-


formes, aviões não têm atrito, e vacas são esféricas (veja http://en.
wikipedia.org/wiki/Spherical_cow7 ). Simplificações como esta são
frequentemente necessárias para análise, enquanto modelos computacio-
nais podem ser mais realísticos.

Um, dois → muitos Na mecânica celeste, o problema de dois corpos pode


ser resolvido analiticamente, o problema de três corpos não. No lugar
onde modelos clássicos são frequentemente limitados a um pequeno nú-
mero de elementos que interagem entre si, a ciência da complexidade
funciona com complexos grandes (que é de onde se originou o nome).

Homogêneo → composto Em modelos clássicos, os elementos tendem a ser


intercambiáveis; modelos complexos incluem heterogeneidade com mais
frequência.

Essas são generalização, então não devemos levá-las muito a sério. E não é
minha intenção denegrir a ciência clássica. Um modelo mais complicado não
é necessariamente melhor; na verdade, geralmente é pior.

Além disso, eu não estou dizendo que essas mudanças são abruptas ou comple-
tas. Pelo contrário, há uma migração gradual na fronteira do que é considerado
um trabalho aceitável, respeitável. Algumas ferramentas que costumavam ser
consideradas duvidosas agora são comuns, enquanto alguns modelos ampla-
mente aceitos são agora considerados com escrutínio.

Por exemplo, quando Appel e Haken provaram o teorema das quatro cores
em 1976, eles usaram um computador para enumerar 1936 casos especiais que
6
Este livro não cobre caos, mas você pode ler a respeito em https://pt.wikipedia.
org/wiki/Caos (http://en.wikipedia.org/wiki/Chaos).
7
N.T.: Sem artigo correspondente na Wikipédia.
1.3 Um novo tipo de modelo 7

eram, de certa forma, lemas da sua prova. Na época, muitos matemáticos


não consideravam o teorema verdadeiramente provado. Agora, as provas com
auxílio de computador são comuns e geralmente (mas não universalmente)
aceitas.

Por outro lado, uma quantidade substancial de análise econômica é baseada em


um modelo de comportamento humano chamado de “Homem econômico”, ou,
com ar de troça8 , Homo economicus. Pesquisas baseadas nesse modelo foram
muito bem vistas por várias décadas, especialmente se envolvesse virtuosismo
matemático. Mais recentemente, este modelo tem sido tratado com ceticismo.
Já os modelos que incluem informações imperfeitas e racionalidade limitada
são tópicos em alta.

1.3 Um novo tipo de modelo


Modelos complexos frequentemente são apropriados para diferentes propósitos
e interpretações:

Preditivo → explicativo O modelo de segregação de Schelling pode dar al-


guma luz sobre um complexo fenômeno social, mas não é útil para predi-
ção. Por outro lado, um modelo simples de mecânica celeste pode prever
eclipses solares com precisão de segundos anos à frente.

Realismo → instrumentalismo Modelos clássicos levam a uma interpreta-


ção realista; por exemplo, a maioria das pessoas aceita que elétrons são
coisas reais que existem. Instrumentalismo é a visão de que modelos
podem ser úteis mesmo se as entidades que postulam não existirem. Ge-
orge Box escreveu o que pode ser o lema do instrumentalismo: “Todos
os modelos estão errados, mas alguns são úteis.”

Reducionismo → holismo Reducionismo é a visão de que o comportamento


de um sistema pode ser explicado pela compreensão de seus componen-
tes. Por exemplo, a tabela periódica dos elementos é um triunfo do
reducionismo porque explica o comportamento químico dos elementos
com um modelo simples de elétrons em átomos. Holismo é a visão de
que alguns fenômenos que aparecem a nível de sistema não existem a
8
N.T.: No original, “with tongue in cheek”.
8 Capítulo 1 Ciência da Complexidade

nível de componentes, e portanto não podem ser explicados em termos


de componentes.

Voltaremos aos modelos explicativos no Capítulo 4, ao instrumentalismo na


Seção 6.1 e ao holismo no Capítulo 8.

1.4 Um novo tipo de engenharia


Até agora escrevi sobre sistemas complexos no contexto de ciência, mas com-
plexidade também é a causa e efeito de mudanças na engenharia e na organi-
zação de sistemas sociais:

Centralizado → decentralizado Sistemas centralizados são conceitualmente


simples e fáceis de analisar, mas os sistemas decentralizados podem ser
mais robustos. Por exemplo, na rede mundial de computadores, os cli-
entes enviam solicitações para servidores centralizados; se os servidores
estiverem fora do ar, o serviço fica indisponível. Em redes ponto-a-ponto,
cada nó é um cliente e um servidor ao mesmo tempo. Para tirar do ar
este serviço, você tem que tirar do ar todos os nós.

Isolamento → interação Em engenharia clássica, a complexidade de gran-


des sistemas é gerenciada pelo isolamento de seus componentes e minimi-
zação de interações. Este ainda é um importante princípio da engenharia;
todavia, a disponibilidade de computação barata faz com que o projeto
de sistemas com interações complexas entre seus componentes seja cada
vez mais viável.

Um para muitos → muitos para muitos Em muitos sistemas de comuni-


cação, os serviços de transmissão estão sendo aumentados e, algumas
vezes, substituídos por serviços que permitem que os usuários se comu-
niquem entre si e criem, compartilhem e modifiquem conteúdo.

De cima para baixo → de baixo para cima Em sistemas sociais, políti-


cos e econômicos, muitas atividades que normalmente seriam organiza-
das de maneira centralizada agora operam como movimentos de base9 .
Mesmo exércitos, que são o exemplo canônico de estrutura hierárquica,
estão mudando em direção à delegação de comando e controle.
9
N.T.: No original, “grassroot movements”.
1.5 Um novo tipo de pensamento 9

Análise → computação Em engenharia clássica, a gama de projetos viáveis


é limitada pela nossa capacidade de análise. Por exemplo, o projeto
da Torre Eiffel só foi possível por causa de novas técnicas analíticas
desenvolvidas por Gustave Eiffel, em particular para lidar com a carga
do vento. Atualmente, ferramentas para análise e projeto assistidos por
computador tornam possível construir quase qualquer coisa que puder
ser imaginada. O museu Guggenheim Bilbao de Frank Gehry é meu
exemplo favorito.

Projeto → busca Engenharia é descrita algumas vezes com uma busca por
soluções num cenário de projetos possíveis. Cada vez mais, o processo de
busca pode ser automatizado. Por exemplo, algoritmos genéticos explo-
ram uma gama enorme de projetos e descobrem soluções que engenheiros
humanos não imaginariam (ou gostariam). O melhor algoritmo genético,
evolution, sabidamente gera projetos que violam as regras da engenharia
humana.

1.5 Um novo tipo de pensamento


Estamos indo longe, mas as mudanças que estou postulando nos critérios de
modelagem científica estão relacionadas aos desenvolvimentos do Século XX
em lógica e epistemologia.

Lógica aristotélica → lógica multivalorada Na lógica tradicional, toda


proposição é verdadeira ou falsa. Este sistema leva às provas matemá-
ticas, mas falha (de forma dramática) para muitas aplicações do mundo
real. Alternativas incluem a lógica multivalorada, lógica difusa e outros
sistemas concebidos para lidar com indeterminação, imprecisão e incer-
teza. Bart Kosko discute alguns desses sistemas em Fuzzy Thinking10 .

Probablidade de frequência → Bayesianismo A probabilidade Bayesiana


existe há séculos, mas só recentemente passou a ser largamente utilizada,
facilitada pela computação barata e a aceitação relutante da subjetivi-
dade nas alegações probabilísticas. Sharon Berstch McGrayne apresenta
esta história em A Teoria que não Morreria.
10
N.T.: Fuzzy Thinking: the New Science of Fuzzy Logic (Pensamento difuso: a nova
ciência da lógica difusa), livro sem tradução para a língua portuguesa.
10 Capítulo 1 Ciência da Complexidade

Objetivo → subjetivo O Iluminismo, e o modernismo filosófico, são base-


ados na crença da verdade objetiva; isto é, verdades que são indepen-
dentes das pessoas que as sustentam. Desenvolvimentos do Século XX
como mecânica quântica, o teorema da incompletude de Gödel, e os es-
tudos de Kuhn sobre a história da ciência chamaram a atenção para
a aparentemente inevitável subjetividade mesmo nas “ciências duras” e
na matemática. Rebecca Goldstein apresenta um contexto histórico da
prova de Gödel em Incompletude.

Lei da física → teoria → modelo Algumas pessoas fazem distinção entre


leis, teorias e modelos, mas eu acho que é tudo a mesma coisa. Pessoas
que usam “lei” provavelmente acreditam que trata-se de algo objetiva-
mente verdadeiro e imutável; pessoas que usam “teoria” admitem que
trata-se de algo passível de revisão; e pessoas que usam “modelo” reco-
nhecem que é baseado numa simplificação ou aproximação.
Alguns conceitos que são chamados de “leis da física” são na verdade
definições; outros são, de fato, a afirmação de que um modelo prevê ou
explica bem o comportamento de um sistema. Voltaremos à natureza
das leis da física nas Seções 4.8, 5.9 e 8.7.

Determinismo → indeterminismo Determinismo é a visão de que todos os


eventos são causados, inevitavelmente, por eventos anteriores. Formas
de indeterminismo incluem aleatoriedade, causalidade probabilística e
incerteza fundamental. Voltaremos a este tópico nas Seções 5.5 e 10.4.

Essas tendências não são universais ou completas, mas o centro da opinião


está se deslocando ao longo desses eixos. Como evidência, considere a reação
a A Estrutura das Revoluções Científicas de Thomas Kuhn, que foi criticada
quando publicada e agora é considerada quase incontroversa.

Essas tendências são causa e efeito da ciência da complexidade. Por exem-


plo, os modelos altamente abstratos são mais aceitáveis agora por causa da
menor expectativa de que deve haver um modelo único e correto para cada
sistema. Por outro lado, os desenvolvimentos em sistemas complexos desafiam
o determinismo e o conceito relacionado de lei física.

Este capítulo é uma visão geral dos temas que serão vistos no livro, mas nem
tudo terá sentido até que você veja os exemplos. Quando chegar ao final do
livro, você pode achar útil ler este capítulo novamente.
Capítulo 2

Grafos

Os três primeiros capítulos deste livro são sobre modelos que descrevem siste-
mas formados por componentes e conexões entre componentes. Por exemplo,
em uma rede social, os componentes são pessoas e as conexões representam
amizades, relações comerciais, etc. Em uma rede trófica, os componentes são
espécies e as conexões representam relações predador-presa.

Neste capítulo, eu introduzo o NetworkX, um pacote em Python para construir


e estudar esses modelos. Nós começamos com o modelo de Erdős-Rényi, que
tem propriedades matemáticas interessantes. No próximo capítulo iremos para
modelos que são mais úteis para explicar sistemas do mundo real.

O código para este capítulo está em chap02.ipynb no repositório deste livro.


Para mais informações sobre como trabalhar com o código, ver a Seção 0.2.

2.1 O que é um grafo?

Para a maioria das pessoas um “grafo”1 é uma representação visual de dados,


como é um gráfico de barras ou um gráfico dos preços de ações ao longo do
tempo. Não é sobre isso que trata este capítulo.
1
N.T.: Em inglês, a palavra graph é usada tanto para grafo como para gráfico.
12 Capítulo 2 Grafos

Bob

Alice

Chuck

Figura 2.1: Um grafo direcionado que representa uma rede social.

Neste capítulo, um grafo é a representação de um sistema que contém ele-


mentos discretos interconectados. Os elementos são representados por nós e
as interconexões são representadas por arestas.

Por exemplo, você poderia representar um mapa rodoviário com um nó para


cada cidade e uma aresta para cada estrada entre as cidades. Ou você pode
representar uma rede social usando um nó para cada pessoa, com uma aresta
entre duas pessoas se elas são amigas e nenhuma aresta caso contrário.

Em alguns grafos, as arestas possuem atributos como comprimento, custo ou


peso. Por exemplo, em um mapa rodoviário o comprimento de uma aresta pode
representar a distância entre duas cidades ou o tempo de viagem. Em uma
rede social pode haver diferentes tipos de arestas para representar diferentes
tipos de relacionamentos: amigos, parceiros de negócios, etc.

As arestas podem ser direcionadas ou não direcionadas, dependendo se


as relações que representam são assimétricas ou simétricas. Em um mapa
rodoviário, você pode representar uma via de mão única com uma aresta di-
recionada e uma via de mão dupla com uma aresta não direcionada. Em
algumas redes sociais, como o Facebook, a amizade é simétrica: se A é amigo
de B, então B é amigo de A. Mas no Twitter, por exemplo, a relação “se-
guir” não é simétrica; A seguir B não implica B seguir A. Assim, você pode
usar arestas não direcionadas para representar uma rede do Facebook e arestas
direcionadas para o Twitter.
2.2 NetworkX 13

Albany
3

Boston

4
4

NYC

Philly

Figura 2.2: Um grafo não direcionado que representa cidades e estradas.

Os grafos possuem propriedades matemáticas interessantes e há um ramo da


matemática chamado de teoria dos grafos que as estuda.

Os grafos também são úteis, porque existem muitos problemas do mundo real
que podem ser resolvidos usando algoritmos de grafos. Por exemplo, o algo-
ritmo de caminho mais curto de Dijkstra é uma maneira eficiente de encontrar
o caminho mais curto entre um nó e todos os outros nós em um grafo. Um
caminho é uma sequência de nós com uma aresta entre cada par consecutivo.

Os grafos geralmente são desenhados com quadrados ou círculos para os nós


e linhas para as arestas. Por exemplo, o grafo direcionado da Figura 2.1 pode
representar três pessoas que se seguem no Twitter. A parte grossa da linha
indica a direção da aresta. Neste exemplo, Alice e Bob seguem um ao outro e
ambos seguem Chuck, mas Chuck não segue ninguém.

O grafo não direcionado da Figura 2.2 mostra quatro cidades no nordeste dos
Estados Unidos; as etiquetas nas arestas indicam tempo de viagem em horas.
Neste exemplo, o posicionamento dos nós corresponde aproximadamente à
geografia das cidades, mas, em geral, o desenho de um grafo é arbitrário.
14 Capítulo 2 Grafos

2.2 NetworkX
Para representar grafos, usaremos um pacote chamado NetworkX, que é a bi-
blioteca de rede mais utilizada em Python. Você pode ler mais sobre ela em
https://networkx.github.io/, mas vou explicá-la à medida que avançar-
mos.

Podemos criar um grafo direcionado importando NetworkX (geralmente im-


portada como nx) e instanciando nx.DiGraph:

import networkx as nx
G = nx.DiGraph()

Neste ponto, G é um objeto DiGraph que não contém nós nem arestas. Podemos
acrescentar nós usando o método add_node:

G.add_node('Alice')
G.add_node('Bob')
G.add_node('Chuck')

Agora podemos usar o método nodes para obter a lista de nós:

>>> G.nodes()
['Alice', 'Bob', 'Chuck']

Acrescentar arestas funciona de maneira semelhante:

G.add_edge('Alice', 'Bob')
G.add_edge('Alice', 'Chuck')
G.add_edge('Bob', 'Alice')
G.add_edge('Bob', 'Chuck')

Podemos usar o método edges para obter a lista de arestas:

>>> G.edges()
[('Alice', 'Bob'), ('Alice', 'Chuck'),
('Bob', 'Alice'), ('Bob', 'Chuck')]

O NetworkX fornece várias funções para desenhar grafos; draw_circular or-


ganiza os nós em um círculo e os conecta com arestas:
2.2 NetworkX 15

nx.draw_circular(G,
node_color=COLORS[0],
node_size=2000,
with_labels=True)

Esse é o código que eu usei para gerar a Figura 2.1. A opção with_labels
faz com que os nós sejam etiquetados. No próximo exemplo, veremos como
etiquetar as arestas.

Para gerar a Figura 2.2, eu começo com um dicionário que mapeia cada nome
da cidade para sua longitude e latitude aproximadas:

pos = dict(Albany=(-74, 43),


Boston=(-71, 42),
NYC=(-74, 41),
Philly=(-75, 40))

Uma vez que este é um grafo não direcionado, instancio nx.Graph:

G = nx.Graph()

Assim eu posso usar add_nodes_from para iterar as chaves de pos e adicioná-


las como nós:

G.add_nodes_from(pos)

Em seguida, crio um dicionário que mapeia cada aresta ao tempo de viagem


correspondente:

drive_times = {('Albany', 'Boston'): 3,


('Albany', 'NYC'): 4,
('Boston', 'NYC'): 4,
('NYC', 'Philly'): 2}

Agora eu posso usar add_edges_from que itera as chaves de drive_times e


as adiciona como arestas:

G.add_edges_from(drive_times)

Agora ao invés de draw_circular, que organiza os nós em um círculo, vou


usar draw, que recebe pos como segundo argumento:
16 Capítulo 2 Grafos

nx.draw(G, pos,
node_color=COLORS[1],
node_shape='s',
node_size=2500,
with_labels=True)

pos é um dicionário que mapeia cada cidade para suas coordenadas; draw usa
esse dicionário para determinar a posição dos nós.

Para adicionar as etiquetas às arestas, usamos draw_networkx_edge_labels:

nx.draw_networkx_edge_labels(G, pos,
edge_labels=drive_times)

drive_times é um dicionário que mapeia cada aresta, representada por um


par de nomes de cidades, à distância a ser percorrida entre elas. Foi assim que
eu gerei a Figura 2.2.

Em ambos os exemplos, os nós são strings, mas, em geral, podem ser qualquer
tipo hashable2 .

2.3 Grafos aleatórios


Um grafo aleatório é exatamente o que se lê: um grafo com nós e arestas
gerados aleatoriamente. É claro que existem muitos processos aleatórios que
podem gerar grafos, então há muitos tipos de grafos aleatórios.

Um dos tipos mais interessantes é o modelo de Erdős-Rényi, estudado por Paul


Erdős e Alfréd Rényi nos anos 1960.

Um grafo Erdős-Rényi (grafo ER) caracteriza-se por dois parâmetros: n é o


número de nós e p é a probabilidade de haver uma aresta entre dois nós. Veja
https://pt.wikipedia.org/wiki/Modelo_Erdős-Rényi3 .

N.T.: Todos os tipos imutáveis do Python, bem como objetos que sejam instâncias
2

de classes definidas pelo usuário são hashable types. Ver: https://docs.python.org/3/


glossary.html#term-hashable.
3
https://en.wikipedia.org/wiki/Erdős-Renyi_model
2.4 Gerando grafos 17

3 2

4 1

5 0

6 9

7 8

Figura 2.3: Um grafo completo com 10 nós.

Erdős e Rényi estudaram as propriedades desses grafos aleatórios; um de seus


resultados mais surpreendentes é a existência de mudanças abruptas nas pro-
priedades de grafos aleatórios à medida que novas arestas são adicionadas
aleatoriamente.

Uma das propriedades que exibe esse tipo de transição é a conectividade. Um


grafo não direcionado é conectado se houver um caminho entre qualquer par
de nós.

Em um grafo ER, a probabilidade de o grafo ser conectado é muito baixa


quando p é pequeno e quase 1 quando p é grande. Entre esses dois regimes,
há uma transição rápida em um certo valor de p, denotado p∗ .

Erdős e Rényi mostraram que esse valor crítico é p∗ = ln n/n, em que n é o


número de nós. É improvável que um grafo aleatório, G(n, p), seja conectado
se p < p∗ e muito provável que seja conectado se p > p∗ .

Para testar essa afirmação, nós desenvolveremos algoritmos para gerar grafos
aleatórios e verificar se eles estão conectados.

2.4 Gerando grafos


Começarei gerando um grafo completo, que é um grafo em que cada nó está
conectado a todos os outros.
18 Capítulo 2 Grafos

A função geradora a seguir recebe uma lista de nós e enumera todos os pares
distintos. Se você não está familiarizado com as funções geradoras, você pode
ler sobre elas em http://intermediatepythonista.com/python-generators4 .
def all_pairs(nodes):
for i, u in enumerate(nodes):
for j, v in enumerate(nodes):
if i>j:
yield u, v
Podemos usar all_pairs para construir um grafo completo:
def make_complete_graph(n):
G = nx.Graph()
nodes = range(n)
G.add_nodes_from(nodes)
G.add_edges_from(all_pairs(nodes))
return G
make_complete_graph recebe o número de nós, n, e devolve um novo Graph
com n nós e arestas entre todos os pares de nós.
O código a seguir faz um grafo completo com 10 nós e o desenha.
complete = make_complete_graph(10)
nx.draw_circular(complete,
node_color=COLORS[2],
node_size=1000,
with_labels=True)
A Figura 2.3 mostra o resultado. Em breve, modificaremos esse código para
gerar grafos ER, mas primeiro desenvolveremos funções para verificar se um
grafo está conectado.

2.5 Grafos conectados


Um grafo é conectado se houver um caminho entre qualquer par de nós (veja
https://pt.wikipedia.org/wiki/Conectividade_(teoria_dos_grafos)5 ).
N.T.: O link fornecido está quebrado, como alternativa consultar:
4
https://
realpython.com/blog/python/introduction-to-python-generators/
5
http://en.wikipedia.org/wiki/Connectivity_(graph_theory)
2.5 Grafos conectados 19

Para muitas aplicações que envolvem grafos, é útil verificar se um grafo é


conectado. Felizmente, existe um algoritmo simples que faz isso.

Você pode começar a partir de qualquer nó e verificar se pode alcançar todos


os outros nós. Se você pode alcançar um nó, v, então pode alcançar qualquer
um dos nós vizinhos de v, ou seja, qualquer nó conectado a v por uma aresta.

A classe Graph fornece um método chamado neighbors que devolve uma lista
de vizinhos de um determinado nó. Por exemplo, no grafo completo que gera-
mos na seção anterior:

>>> complete.neighbors(0)
[1, 2, 3, 4, 5, 6, 7, 8, 9]

Suponhamos que comecemos no nó s. Podemos marcar s como “visto”, e


também podemos marcar seus vizinhos. Então marcamos os vizinhos dos
vizinhos, e assim por diante, até que não possamos alcançar mais nós. Se
todos os nós forem vistos, o grafo é conectado.

Em Python, isso fica assim:

def reachable_nodes(G, start):


seen = set()
stack = [start]
while stack:
node = stack.pop()
if node not in seen:
seen.add(node)
stack.extend(G.neighbors(node))
return seen

reachable_nodes recebe um Graph e um nó inicial, start, e devolve o con-


junto dos nós que podem ser alcançados a partir de start.

Inicialmente, o conjunto seen está vazio e criamos uma lista chamada stack
que mantém o registro dos nós que descobrimos, mas que ainda não foram
processados. Inicialmente, a pilha contém um único nó, start.

A cada execução do laço,


20 Capítulo 2 Grafos

1. Nós removemos um nó da pilha.

2. Se o nó já está em seen, voltamos para o Passo 1.

3. Caso contrário, nós adicionamos o nó a seen e adicionamos seus vizinhos


à pilha.

Quando a pilha está vazia, não podemos alcançar mais nós, então saímos do
laço e devolvemos seen.

Como exemplo, podemos encontrar todos os nós no grafo completo que podem
ser alcançados a partir do nó 0:

>>> reachable_nodes(complete, 0)
{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}

Inicialmente, a pilha contém o nó 0 e seen está vazia. Na primeira execução


do laço, o nó 0 é adicionado a seen e todos os outros nós são adicionados à
pilha (já que todos são vizinhos do nó 0).

Na próxima execução do laço, pop devolve o último elemento da pilha, que é


o nó 9. Portanto, o nó 9 é adicionado a seen e seus vizinhos são adicionados
à pilha.

Observe que o mesmo nó pode aparecer mais de uma vez na pilha; na ver-
dade, um nó com k vizinhos será adicionado à pilha k vezes. Mais tarde,
procuraremos por maneiras de tornar esse algoritmo mais eficiente.

Podemos usar reachable_nodes para escrever is_connected:

def is_connected(G):
start = next(iter(G.nodes()))
reachable = reachable_nodes(G, start)
return len(reachable) == len(G)

is_connected escolhe um nó inicial chamando nodes_iter, que devolve um


objeto iterador, e passa o resultado para next, que devolve o primeiro nó.

reachable recebe o conjunto dos nós que podem ser alcançados a partir de
start. Se o tamanho deste conjunto for igual ao tamanho do grafo, significa
que podemos alcançar todos os nós e, portanto, que o grafo é conectado.
2.6 Gerando grafos ER 21

3 2

4 1

5 0

6 9

7 8

Figura 2.4: Um grafo ER com n=10 e p=0.3.

Um grafo completo é, como esperado, conectado:

>>> is_connected(complete)
True

Na próxima seção, geraremos grafos ER e verificaremos se eles são conectados.

2.6 Gerando grafos ER


O grafo ER G(n, p) contém n nós e a probabilidade de cada par de nós estar
conectado por uma aresta é p. Gerar um grafo ER é semelhante a gerar um
grafo completo.

A função geradora a seguir enumera todas as arestas possíveis e usa uma função
auxiliar, flip, para escolher quais arestas devem ser adicionadas ao grafo:

def random_pairs(nodes, p):


for i, u in enumerate(nodes):
for j, v in enumerate(nodes):
if i>j and flip(p):
yield u, v

flip devolve True com a probabilidade dada, p, e False com a probabilidade


complementar 1-p:
22 Capítulo 2 Grafos

from numpy.random import random

def flip(p):
return random() < p

Finalmente, make_random_graph gera e devolve o grafo ER G(n, p).

def make_random_graph(n, p):


G = nx.Graph()
nodes = range(n)
G.add_nodes_from(nodes)
G.add_edges_from(random_pairs(nodes, p))
return G

make_random_graph é quase idêntica a make_complete_graph; a única dife-


rença é que ela usa random_pairs em vez de all_pairs.

Segue um exemplo com p=0.3:

random_graph = make_random_graph(10, 0.3)

A Figura 2.4 mostra o resultado. Este grafo acabou sendo conectado; na


verdade, a maioria dos grafos ER com n = 10 e p = 0,3 são conectados. Na
próxima seção veremos quantos.

2.7 Probabilidade de conectividade


Para valores de n e p dados, gostaríamos de saber a probabilidade de G(n, p)
ser conectado. Podemos estimá-la, gerando uma grande quantidade de grafos
aleatórios e contando quantos deles são conectados. Veja como:

def prob_connected(n, p, iters=100):


count = 0
for i in range(iters):
random_graph = make_random_graph(n, p)
if is_connected(random_graph):
count += 1
return count/iters
2.7 Probabilidade de conectividade 23

1.0

0.8
prob conectado

0.6

0.4

0.2

0.0
10 -1 10 0
p

Figura 2.5: Probabilidade de conectividade com n = 10 para uma faixa de


valores de p. A linha vertical mostra o valor crítico previsto.

1.0
n=30
n=100
0.8
n=300
prob conectado

0.6

0.4

0.2

0.0
10 -2 10 -1 10 0
p

Figura 2.6: Probabilidade de conectividade para vários valores de n para uma


faixa de valores de p.
24 Capítulo 2 Grafos

iters é o número de grafos aleatórios que geramos. À medida que aumentamos


iters, a probabilidade estimada fica mais precisa6 .

>>> prob_connected(10, 0.3, iters=10000)


0.6454

Dos 10000 grafos ER gerados com esses parâmetros, 6498 são conectados.
Então estimamos que 65% deles são conectados. Eu escolhi 0,3 porque é um
valor próximo do valor crítico para o qual a probabilidade de conectividade vai
de quase de 0 para quase 1. De acordo com Erdős e Rényi, p∗ = ln n/n = 0,23.

Podemos ter uma visão mais clara da transição estimando a probabilidade de


conectividade para uma faixa de valores de p:

import numpy as np

n = 10
ps = np.logspace(-2.5, 0, 11)
ys = [prob_connected(n, p) for p in ps]

Este é o primeiro exemplo que vimos usando o NumPy. Seguindo a convenção,


eu importo o NumPy como np. A função logspace devolve um array de 11
valores de 10−2,5 a 100 = 1, igualmente espaçados em uma escala logarítmica.

Para calcular ys, uso uma abrangência de listas7 que itera os elementos de ps
e calcula, para cada valor de p, a probabilidade de que um grafo aleatório seja
conectado.

A Figura 2.5 mostra os resultados com uma linha vertical em p∗ . A transição


de 0 para 1 ocorre perto do valor crítico previsto, 0,23. Com p em uma escala
logarítmica, a transição é praticamente simétrica.

A Figura 2.6 mostra resultados semelhantes para valores maiores de n. À


medida que n aumenta, o valor crítico diminui e a transição fica mais abrupta.

Esses experimentos são consistentes com os resultados provados por Erdős e


Rényi em seus artigos.
Dado que count e iters são inteiros, esta função não funcionará corretamente com
6

Python 2, a menos que você importe division de __future__. Veja https://www.python.


org/dev/peps/pep-0238/
7
N.T.: Do inglês, list comprehension.
2.8 Análise de algoritmos de grafos 25

2.8 Análise de algoritmos de grafos


Neste capítulo, eu apresentei um algoritmo para verificar se um grafo é conec-
tado. Nos próximos capítulos veremos outros algoritmos de grafos. Também
analisaremos o desempenho desses algoritmos, descobrindo como os tempos de
execução aumentam à medida que o tamanho dos grafos aumenta.

Se você ainda não está familiarizado com análise de algoritmos, você deveria
ler o Apêndice A antes de continuar.

A ordem de crescimento para algoritmos de grafos geralmente é expressa como


uma função de n, o número de vértices, e de m, o número de arestas.

Como exemplo, vamos analisar reachable_nodes da Seção 2.5:

def reachable_nodes(G, start):


seen = set()
stack = [start]
while stack:
node = stack.pop()
if node not in seen:
seen.add(node)
stack.extend(G.neighbors(node))
return seen

A cada execução do laço, tiramos um nó da pilha; por padrão, pop remove e


devolve o último elemento de uma lista, uma operação de tempo constante.

Em seguida, verificamos se o nó está em seen, que é um conjunto. Verificar


se um elemento pertence a um conjunto é uma operação de tempo constante.

Se o nó ainda não estiver em seen, o adicionamos ao conjunto. Essa também


é um operação de tempo constante. Em seguida adicionamos os vizinhos à
pilha, uma operação linear no número de vizinhos.

Para expressar o tempo de execução em termos de n e m, podemos somar o


número total de vezes que cada nó é adicionado a seen e a stack.

Cada nó é adicionado a seen apenas uma vez, então o número total de adições
é n.
26 Capítulo 2 Grafos

Mas os nós podem ser adicionados a stack muitas vezes, dependendo de quan-
tos vizinhos eles têm. Se um nó tiver k vizinhos, ele é adicionado a stack k
vezes. Claro, já que se ele tem k vizinhos, significa que ele está conectado a k
arestas.

Portanto, o número total de adições para stack é o dobro do número total de


arestas, m, porque consideramos cada uma das arestas duas vezes.

Assim, a ordem de crescimento para esta função é O(n + m), o que é uma ma-
neira conveniente de dizer que o tempo de execução cresce proporcionalmente
a n ou m, o que for maior.

Se conhecemos a relação entre n e m, podemos simplificar essa expressão. Por


exemplo, em um grafo completo o número de arestas é n(n−1)/2, que é O(n2 ).
Então, para um grafo completo reachable_nodes é quadrática em n.

2.9 Exercícios
O código para este capítulo está em chap02.ipynb, um Jupyter notebook no
repositório deste livro. Para obter mais informações sobre como trabalhar com
este código, consulte a Seção 0.2.

Exercício 2.1 Inicie chap02.ipynb e execute o código. Há alguns exercícios


incorporados ao notebook que você pode tentar resolver.

Exercício 2.2 Na Seção 2.8, analisamos o desempenho de reachable_nodes


e a classificamos em O(n + m), onde n é o número de nós e m é o nú-
mero de arestas. Continuando a análise, qual é a ordem de crescimento para
is_connected?

def is_connected(G):
start = next(G.nodes_iter())
reachable = reachable_nodes(G, start)
return len(reachable) == len(G)

Exercício 2.3 Na minha implementação de reachable_nodes, você pode


ter se incomodado com a aparente ineficiência de adicionar todos os vizinhos à
pilha sem verificar se eles já estão em seen. Escreva uma versão desta função
2.9 Exercícios 27

que verifica os vizinhos antes de adicioná-los à pilha. Essa “otimização” altera


a ordem de crescimento? Ela torna a função mais rápida?

Exercício 2.4 Na verdade, existem dois tipos de grafos ER. Aquele que
geramos neste capítulo, G(n, p), caracteriza-se por ter por dois parâmetros, o
número de nós e a probabilidade de uma aresta entre um par de nós.

Uma definição alternativa, denotada G(n, m), também é caracterizada por dois
parâmetros: o número de nós, n, e o número de arestas, m. Sob esta definição,
o número de arestas é fixo, mas sua posição é aleatória.

Repita as experiências que fizemos neste capítulo usando esta definição alter-
nativa. Seguem algumas sugestões sobre como proceder:

1. Escreva uma função chamada m_pairs que recebe uma lista de nós e o
número de arestas, m, e devolve uma seleção aleatória de m arestas. Uma
maneira simples de fazer isso é gerar uma lista de todas as arestas possíveis e
usar random.sample.

2. Escreva uma função chamada make_m_graph que recebe n e m e devolve


um grafo aleatório com n nós e m arestas.

3. Faça uma versão de prob_connected que usa make_m_graph ao invés de


make_random_graph.

4. Calcule a probabilidade de conectividade para uma faixa de valores de m.

Como os resultados deste experimento se comparam aos resultados usando o


primeiro tipo de grafo ER?
28 Capítulo 2 Grafos
Capítulo 3

Grafos de pequeno mundo

Muitas redes no mundo real, incluindo redes sociais, têm a “propriedade de


pequeno mundo”, isto é, que a distância média entre nós, medida em número
de arestas no caminho mais curto, é muito menor do que o esperado.

Neste capítulo, apresento a famosa Experiência de Pequeno Mundo de Stanley


Milgram, que foi a primeira demonstração científica da propriedade de pequeno
mundo em uma rede social real. Em seguida, vamos considerar os grafos de
Watts-Strogatz, que destinam-se a ser um modelo de grafos de pequeno mundo.
Vou replicar a experiência que Watts e Strogatz realizaram e explicar o que se
pretende mostrar com ela.

Ao longo do caminho, veremos dois novos algoritmos de grafos: busca em


largura1 e algoritmo de Dijkstra para calcular o caminho mais curto entre nós
em um grafo.

O código para este capítulo está em chap03.ipynb no repositório deste livro.


Para mais informações sobre como trabalhar com o código, ver a Seção 0.2.

3.1 Stanley Milgram


Stanley Milgram foi um psicólogo social norte americano que realizou duas
das mais famosas experiências em ciências sociais, a experiência de Milgram,
1
N.T.: Do inglês, breadth-first search (BFS)
30 Capítulo 3 Grafos de pequeno mundo

que estudou a obediência das pessoas à autoridade (https://pt.wikipedia.


org/wiki/Experiência_de_Milgram2 ) e a Experiência de Pequeno Mundo,
que estudou a estrutura das redes sociais (http://en.wikipedia.org/wiki/
Small_world_phenomenon3 ).
Na Experiência de Pequeno Mundo, Milgram enviou pacotes para várias pes-
soas escolhidas aleatoriamente em Wichita, no Kansas. Os pacotes continham
uma carta e instruções pedindo a elas que encaminhassem a carta para uma
pessoa alvo, identificada por nome e ocupação, em Sharon, Massachusetts (que
é a cidade perto de Boston onde cresci). Os sujeitos foram informados de que
poderiam enviar a carta diretamente para a pessoa alvo apenas se a conheces-
sem pessoalmente; caso contrário, eles foram instruídos a enviá-la, com essas
mesmas instruções, para um parente ou amigo que achassem que teria maior
possibilidade de conhecer a pessoa alvo.
Muitas das cartas nunca foram entregues, mas para aquelas que foram, o
comprimento médio do caminho—o número de vezes que as cartas foram
encaminhadas—foi de cerca de seis. Esse resultado foi usado para confirmar
observações (e especulações) anteriores de que a distância típica entre duas
pessoas em uma rede social é de por volta de “seis graus de separação”.
Esta conclusão é surpreendente porque a maioria das pessoas espera que
as redes sociais sejam localizadas—as pessoas tendem a viver perto de seus
amigos—e em um grafo com conexões locais os comprimentos dos caminhos
tendem a aumentar proporcionalmente à distância geográfica. Por exemplo, a
maioria dos meus amigos vive nas proximidades, então eu acho que a distância
média entre nós em uma rede social é de cerca de 50 milhas4 . Wichita está a
cerca de 1600 milhas de Boston. Assim, se as cartas de Milgram passaram por
vínculos típicos de uma rede social, elas deveriam ter passado por 32 etapas,
e não seis.

3.2 Watts e Strogatz


Em 1998, Duncan Watts e Steven Strogatz publicaram um artigo na revista
Nature, com título “Collective dynamics of ‘small-world’ networks”5 , que pro-
2
http://en.wikipedia.org/wiki/Milgram_experiment
3
N.T.: Sem artigo correspondente na Wikipédia.
4
N.T.: 1 milha equivale a 1,60934 quilômetros.
5
N.T.: Dinâmica coletiva de redes de pequeno mundo.
3.2 Watts e Strogatz 31

punha uma explicação para o fenômeno de pequeno mundo. Você pode baixá-lo
de http://www.nature.com/nature/journal/v393/n6684/abs/393440a0.html.

Watts e Strogatz começam com dois tipos de grafos que foram bem compre-
endidos: grafos aleatórios e grafos regulares. Em um grafo aleatório, os nós
são conectados aleatoriamente. Em um grafo regular, todos os nós possuem o
mesmo número de vizinhos. Eles consideram duas propriedades desses grafos,
agrupamento e comprimento do caminho:

• Agrupamento é uma medida da “panelinha”6 entre os nós de um grafo.


Em um grafo, um clique é um subconjunto de nós que estão todos
conectados entre si. Em uma rede social, um “clique” é um conjunto
de pessoas em que todos são amigos uns dos outros. Watts e Strogatz
definiram um coeficiente de agrupamento que quantifica a probabilidade
de que dois nós conectados ao mesmo nó também estejam conectados
um ao outro.

• O comprimento do caminho é uma medida da distância média entre dois


nós, o que corresponde aos graus de separação em uma rede social.

Watts e Strogatz mostram que os grafos regulares possuem alto agrupamento


e comprimentos de caminhos longos, enquanto que os grafos aleatórios com
o mesmo tamanho normalmente têm agrupamento baixo e comprimentos de
caminho curtos. Portanto, nenhum deles é um bom modelo de redes sociais,
as quais combinam alto agrupamento com comprimentos de caminhos curtos.

O objetivo deles era criar um modelo generativo de uma rede social. Um


modelo generativo tenta explicar um fenômeno pela modelagem do processo
que constrói ou leva ao fenômeno. Watts e Strogatz propuseram este processo
para a construção de grafos de pequeno mundo:

1. Comece com um grafo regular com n nós, cada um conectado a k vizi-


nhos.

2. Escolha um subconjunto das arestas e “religue-as”, substituindo-as por


arestas aleatórias.
6
N.T.: Em inglês, o autor usa o termo “cliquishness”, no sentido de quanto o grafo é
“clique”. Clique é um termo em inglês para expressar uma “panelinha” de amigos, “clube
da luluzinha” ou “clube do bolinha”, um grupo exclusivo.
32 Capítulo 3 Grafos de pequeno mundo

A probabilidade de uma aresta ser religada é um parâmetro, p, que controla o


quão aleatório é o grafo. Com p = 0, o grafo é regular; com p = 1 é aleatório.

Watts e Strogatz descobriram que pequenos valores de p produzem grafos com


agrupamento alto, como em um grafo regular, e comprimentos de caminhos
curtos, como em um grafo aleatório.

Neste capítulo, eu replico a experiência de Watts e Strogatz com as seguintes


etapas:

1. Vamos começar construindo uma rede7 em anel, um tipo de grafo regular.

2. Então vamos religar o grafo como Watts e Strogatz fizeram.

3. Escreveremos uma função para medir o grau de agrupamento e usaremos


uma função do NetworkX para calcular os comprimentos dos caminhos.

4. Então calcularemos o grau de agrupamento e o comprimento do caminho


para uma faixa de valores de p.

5. Por fim, vou apresentar um algoritmo eficiente para computar os cami-


nhos mais curtos, o algoritmo de Dijkstra.

3.3 Rede em anel


Um grafo regular é um grafo no qual cada nó possui o mesmo número de
vizinhos. O número de vizinhos também é chamado de grau do nó.

Uma rede em anel é um tipo de grafo regular que Watts e Strogatz usam
como a base de seu modelo. Em uma rede em anel com n nós, os nós podem
ser organizados em um círculo com cada nó conectado aos k vizinhos mais
próximos.

Por exemplo, uma rede em anel com n = 3 e k = 2 teria as seguintes arestas:


(0, 1), (1, 2) e (2, 0). Observe que as arestas “completam a volta” do nó com o
número mais alto de volta para o nó 0.
Em inglês, lattice, que traduzida para o português seria “retículo” ou “reticulado”. Aqui
7

lattice é usada no sentido de um arranjo geométrico regular de pontos ou objetos em uma


área ou espaço, como uma trama ou uma rede.
3.3 Rede em anel 33

3 2

4 1

5 0

6 9

7 8

Figura 3.1: Uma rede em anel com n = 10 e k = 4.

De maneira mais geral, podemos enumerar as arestas assim:

def adjacent_edges(nodes, halfk):


n = len(nodes)
for i, u in enumerate(nodes):
for j in range(i+1, i+halfk+1):
v = nodes[j % n]
yield u, v

adjacent_edges recebe uma lista de nós e um parâmetro, halfk, que é a


metade de k. É uma função geradora que produz uma aresta por vez. Ela usa
o operador de módulo, %, para voltar do nó de maior número para o de menor
número.

Podemos testá-la assim:

>>> nodes = range(3)


>>> for edge in adjacent_edges(nodes, 1):
... print(edge)
(0, 1)
(1, 2)
(2, 0)

Agora podemos usar adjacent_edges para fazer a rede em anel:


34 Capítulo 3 Grafos de pequeno mundo

Figura 3.2: Grafos WS com n = 20, k = 4, e p = 0 (esquerda), p = 0, 2 (meio),


e p = 1 (direita).

def make_ring_lattice(n, k):


G = nx.Graph()
nodes = range(n)
G.add_nodes_from(nodes)
G.add_edges_from(adjacent_edges(nodes, k//2))
return G

Observe que make_ring_lattice usa divisão inteira para calcular halfk, tal
que, se k é ímpar, o resultado da divisão será arredondado para baixo e irá
gerar uma rede em anel com grau k-1. Provavelmente não é o que queremos,
mas é bom o suficiente por enquanto.

Podemos testar a função da seguinte maneira:

lattice = make_ring_lattice(10, 4)

A Figura 3.1 mostra o resultado.

3.4 Grafos WS
Para fazer um grafo Watts-Strogatz (WS), começamos com uma rede em anel e
“religamos” algumas das arestas. Em seu artigo, Watts e Strogatz consideram
as arestas em uma ordem específica e religam cada uma com probabilidade p.
Se uma aresta é religada, eles deixam o primeiro nó inalterado e escolhem o
segundo nó aleatoriamente. Eles não permitem auto-laço ou múltiplas arestas;
3.4 Grafos WS 35

isto é, você não pode ter uma aresta de um nó para ele mesmo, e você não
pode ter mais de uma aresta entre o mesmo par de nós.

O código que segue é a minha implementação deste processo.

def rewire(G, p):


nodes = set(G.nodes())
for edge in G.edges():
if flip(p):
u, v = edge
choices = nodes - {u} - set(G[u])
new_v = choice(tuple(choices))
G.remove_edge(u, v)
G.add_edge(u, new_v)

O parâmetro p é a probabilidade de religação de uma aresta. O laço for


enumera as arestas e usa flip, que devolve True com a probabilidade p, para
escolher quais são religadas.

Se estamos religando uma aresta do nó u para o nó v, temos que escolher


uma substituição para v, chamada new_v. Para calcular as escolhas possíveis,
começamos com nodes, que é um conjunto, e subtraímos dele u e seus vizinhos,
o que evita auto-laços e múltiplas arestas.

Em seguida, escolhemos new_v de choices, removemos a aresta existente de


u a v, e adicionamos uma nova aresta de u para new_v.

A expressão G[u] devolve um dicionário que contém os vizinhos de u como


chaves. Neste caso, é um pouco mais rápido do que usar G.neighbors.

Esta função não considera as arestas na ordem especificada por Watts e Stro-
gatz, mas isso não parece afetar os resultados.

A Figura 3.2 mostra grafos WS com n = 20, k = 4, para uma faixa de valores
de p. Quando p = 0, o gráfico é uma rede em anel. Quando p = 1, o grafo
é completamente aleatório. Como veremos, as coisas interessantes acontecem
entre esses valores.
36 Capítulo 3 Grafos de pequeno mundo

3.5 Agrupamento
O próximo passo é calcular o coeficiente de agrupamento, que quantifica a
tendência para que os nós formem cliques. Um clique é um conjunto de nós
que estão completamente conectados. Ou seja, existem arestas entre todos os
pares de nós do conjunto.

Suponha que um certo nó, u, tenha k vizinhos. Se todos os vizinhos estive-


rem conectados um ao outro, haverá k(k − 1)/2 arestas entre eles. A fração
das arestas que realmente existem é o coeficiente de agrupamento local de u,
denotado Cu . É chamado de “coeficiente” porque é sempre um valor entre 0 e
1.

Se calcularmos a média do Cu de todos os nós, obtemos o “coeficiente de


agrupamento médio da rede”, denotado C̄.

A função a seguir computa Cu .

def node_clustering(G, u):


neighbors = G[u]
k = len(neighbors)
if k < 2:
return 0

total = k * (k-1) / 2
exist = 0
for v, w in all_pairs(neighbors):
if G.has_edge(v, w):
exist +=1
return exist / total

Novamente eu uso G[u], que devolve um dicionário que tem os vizinhos de


node como chaves. Se um nó tiver menos de 2 vizinhos, o coeficiente de
agrupamento é indefinido. Mas, por simplicidade, node_clustering devolve
0.

Caso contrário, calculamos o número de arestas possíveis entre os vizinhos,


total, e em seguida contamos o número dessas arestas que realmente existem.
O resultado é a fração de todas as arestas que existem.
3.6 Comprimentos dos caminhos mais curtos 37

Podemos testar a função assim:

>>> lattice = make_ring_lattice(10, 4)


>>> node_clustering(lattice, 1)
0.5

Em uma rede em anel com k = 4, o coeficiente de agrupamento para cada nó


é 0,5 (se você não estiver convencido, veja mais uma vez a Figura 3.1).

Agora podemos calcular o coeficiente de agrupamento médio da rede como


segue:

def clustering_coefficient(G):
cc = np.mean([node_clustering(G, node) for node in G])
return cc

np.mean é uma função do NumPy que calcula a média dos números em uma
lista ou array.

Podemos testá-la assim:

>>> clustering_coefficient(lattice)
0.5

Neste grafo, o coeficiente de agrupamento local para todos os nós é de 0,5, de


modo que a média entre os nós é de 0,5. Claro que esperamos que esse valor
seja diferente para grafos WS.

3.6 Comprimentos dos caminhos mais curtos


O próximo passo é calcular o comprimento do caminho característico, L, que
é o comprimento médio dos caminhos mais curtos entre cada par de nós.
Para computá-lo, vou começar com uma função fornecida pelo NetworkX,
shortest_path_length. Vou usá-la para replicar a experiência de Watts e
Strogatz, e então vou explicar como ela funciona.

A função a seguir recebe um grafo e devolve uma lista de comprimentos de


caminhos mais curtos, um para cada par de nós.
38 Capítulo 3 Grafos de pequeno mundo

def path_lengths(G):
length_map = {k:v for (k,v) in nx.shortest_path_length(G)}
lengths = [length_map[u][v] for u, v in all_pairs(G)]
return lengths

O valor devolvido por nx.shortest_path_length é um dicionário de dicioná-


rios. O dicionário externo mapeia cada nó, u, para um dicionário que mapeia
de cada nó, v, para o comprimento do caminho mais curto de u para v.

Com a lista de comprimentos de path_lengths, podemos calcular L como


segue:

def characteristic_path_length(G):
return np.mean(path_lengths(G))

Podemos testá-la com uma pequena rede em anel:

>>> lattice = make_ring_lattice(3, 2)


>>> characteristic_path_length(lattice)
1.0

Neste exemplo, todos os 3 nós estão conectados entre si, então o comprimento
médio do caminho é 1.

3.7 A experiência de WS
Agora estamos prontos para replicar a experiência de WS, que mostra que
para uma faixa de valores de p, um grafo WS possui alto agrupamento como
um grafo regular e comprimentos de caminhos curtos como um grafo aleatório.

Vou começar com run_one_graph, que recebe n, k, e p, gera um grafo WS


com os parâmetros dados, e calcula o comprimento médio dos caminhos, mpl,
e o coeficiente de agrupamento cc:

def run_one_graph(n, k, p):


ws = make_ws_graph(n, k, p)
mpl = characteristic_path_length(ws)
cc = clustering_coefficient(ws)
print(mpl, cc)
return mpl, cc
3.7 A experiência de WS 39

1.0

0.8
C(p) / C(0)

0.6

0.4

0.2 L(p) / L(0)


0.0 -4
10 10 -3 10 -2 10 -1 10 0
p

Figura 3.3: Coeficiente de agrupamento (C) e comprimento do caminho carac-


terístico (L) para grafos WS com n = 1000, k = 10, e uma faixa de valores de
p.

Watts e Strogatz executaram sua experiência com n=1000 e k=10. Com esses
parâmetros, run_one_graph leva cerca de um segundo no meu computador.
A maior parte desse tempo é gasto calculando o comprimento médio dos ca-
minhos.

Agora precisamos calcular esses valores para uma faixa de valores de p. Eu


usarei a função do NumPy logspace novamente para calcular ps:

ps = np.logspace(-4, 0, 9)

Para cada valor de p, eu gero 3 grafos aleatórios e faço a média dos resultados.
A função a seguir executa a experiência:

def run_experiment(ps, n=1000, k=10, iters=3):


res = {}
for p in ps:
print(p)
res[p] = []
for _ in range(iters):
res[p].append(run_one_graph(n, k, p))
return res

O resultado é um dicionário que mapeia cada valor de p para uma lista de


pares (mpl, cc).
40 Capítulo 3 Grafos de pequeno mundo

O último passo é agregar os resultados:

L = []
C = []
for p, t in sorted(res.items()):
mpls, ccs = zip(*t)
mpl = np.mean(mpls)
cc = np.mean(ccs)
L.append(mpl)
C.append(cc)

A cada execução do laço, obtemos um valor de p e uma lista de pares (mpl, cc).
Nós usamos zip para extrair duas listas, mpls e ccs, e então computarmos
suas médias e adicioná-las a L e C, que são as listas de comprimentos dos
caminhos e de coeficientes de agrupamento, respectivamente.

Para fazer um gráfico de L e C nos mesmos eixos, nós os normalizamos dividindo


pelo primeiro elemento:

L = np.array(L) / L[0]
C = np.array(C) / C[0]

A Figura 3.3 mostra os resultados. À medida que p aumenta, o comprimento


médio dos caminhos cai rapidamente, porque mesmo um pequeno número de
arestas religadas aleatoriamente fornece atalhos entre regiões do grafo que
estão distantes umas das outras na rede. Por outro lado, a remoção de arestas
locais diminui o coeficiente de agrupamento, mas muito mais devagar.

Como resultado, existe uma ampla faixa de valores de p para a qual um grafo
WS possui as propriedades de um grafo de pequeno mundo, alto agrupamento
e comprimentos de caminho curtos.

É por isso que Watts e Strogatz propõem grafos WS como um modelo para
redes do mundo real que exibem o fenômeno de pequeno mundo.

3.8 Que tipo de explicação é essa?


Se você me perguntar por que as órbitas planetárias são elípticas, eu po-
deria começar modelando um planeta e uma estrela como massas pontuais.
3.8 Que tipo de explicação é essa? 41

Eu procuraria a lei da gravitação universal em https://pt.wikipedia.org/


wiki/Lei_da_gravita%C3%A7%C3%A3o_universal8 e a usaria para escrever
uma equação diferencial para o movimento do planeta. A seguir eu deri-
varia a equação da órbita ou, mais provavelmente, procuraria por ela em
http://en.wikipedia.org/wiki/Orbit_equation9 . Com um pouco de ál-
gebra, eu poderia derivar as condições que produzem uma órbita elíptica.
Então eu argumentaria que os objetos que consideramos planetas satisfazem
essas condições.

As pessoas, ou pelo menos os cientistas, geralmente estão satisfeitas com esse


tipo de explicação. Uma das razões para a sua atratividade é que os pressupos-
tos e aproximações do modelo parecem razoáveis. Planetas e estrelas não são
realmente massas pontuais, mas as distâncias entre eles são tão grandes que
seus tamanhos reais são insignificantes. Os planetas em um mesmo sistema so-
lar podem afetar as órbitas uns dos outros, mas o efeito geralmente é pequeno.
Ignoramos também os efeitos relativistas, novamente com o pressuposto de
que são pequenos.

Esta explicação também é atraente porque é baseada em equações. Podemos


expressar a equação da órbita de forma fechada, o que significa que podemos
calcular as órbitas de forma eficiente. Isso também significa que podemos
derivar expressões gerais para a velocidade orbital, para o período orbital e
para outras grandezas.

Por fim, acho que esse tipo de explicação é atraente porque tem a forma de
uma prova matemática. Começa a partir de um conjunto de axiomas e o
resultado é derivado por lógica e análise. Mas é importante lembrar que a
prova pertence ao modelo e não ao mundo real. Ou seja, podemos provar
que um modelo idealizado de um planeta produz uma órbita elíptica, mas não
podemos provar que o modelo é próprio aos planetas reais (na verdade, não).

Em comparação, a explicação de Watts e Strogatz sobre o fenômeno do pe-


queno mundo pode parecer menos satisfatória. Primeiro, o modelo é mais
abstrato, ou seja, menos realista. Em segundo lugar, os resultados são gerados
por simulação, não por análise matemática. Por fim, os resultados parecem
menos com uma prova e mais com um exemplo.
8
http://en.wikipedia.org/wiki/Newton’s_law_of_universal_gravitation
9
N.T.: Sem artigo correspondente na Wikipédia.
42 Capítulo 3 Grafos de pequeno mundo

Muitos dos modelos deste livro são como o modelo de Watts e Strogatz: abs-
tratos, baseados em simulação e (pelo menos superficialmente) menos formais
do que os modelos matemáticos convencionais. Um dos objetivos deste livro é
refletir sobre as questões que esses modelos suscitam:

• Que tipo de trabalho esses modelos podem fazer: são preditivos ou ex-
plicativos, ou ambos?

• As explicações que estes modelos oferecem são menos satisfatórias do


que explicações baseadas em modelos mais tradicionais? Por quê?

• Como devemos caracterizar as diferenças entre esses modelos e modelos


mais convencionais? Eles são diferentes em espécie ou apenas em grau?

Ao longo do livro irei oferecer minhas respostas a essas questões, mas elas são
preliminares e às vezes especulativas. Eu encorajo você a considerá-las com
ceticismo e a tirar as suas próprias conclusões.

3.9 Busca em largura


Quando calculamos os caminhos mais curtos, usamos uma função fornecida
pelo NetworkX, mas não expliquei como ela funciona. Para fazer isso, vou
começar com uma busca em largura, que é a base do algoritmo de Dijkstra
para o cálculo dos caminhos mais curtos.

Na Seção 2.5 eu apresentei reachable_nodes, uma função que encontra todos


os nós que podem ser alcançados a partir de um determinado nó inicial:

def reachable_nodes(G, start):


seen = set()
stack = [start]
while stack:
node = stack.pop()
if node not in seen:
seen.add(node)
stack.extend(G.neighbors(node))
return seen
3.9 Busca em largura 43

Eu não disse isso antes, mas reachable_nodes faz uma busca em profundi-
dade10 . Agora vamos modificá-la para realizar uma busca em largura.

Para entender a diferença, imagine que você está explorando um castelo. Você
começa em uma sala com três portas marcadas A, B e C. Você abre a porta
C e descobre outra sala, com as portas marcadas D, E e F.

Qual porta você abre depois? Se você está se sentindo aventureiro, você pode
querer ir mais fundo no castelo e escolher D, E ou F. Isso seria uma busca em
profundidade.

Mas se você quisesse ser mais sistemático, você poderia voltar e explorar A e
B antes de D, E e F. Essa seria uma busca em largura.

Em reachable_nodes, usamos list.pop para escolher o próximo nó a ser


“explorado”. Por padrão, pop devolve o último elemento da lista, que é o
último que adicionamos. No exemplo, seria a porta F.

Mas se quisermos fazer uma busca em largura, a solução mais simples é tirar
o primeiro elemento da pilha:

node = stack.pop(0)

Isso funciona, mas é lento. Em Python, tirar o último elemento de uma lista
requer tempo constante, mas tirar o primeiro elemento requer tempo linear
com o tamanho da lista. No pior dos casos, o comprimento da pilha é O(n), o
que torna esta implementação da busca em largura O(nm). Isto é muito pior
do que deveria ser, O(n + m).

Podemos resolver este problema com uma fila dupla, também conhecida como
deque. A característica importante de uma deque é que você pode adici-
onar e remover elementos do início ou do final em tempo constante. Para
ver como é implementada, veja https://pt.wikipedia.org/wiki/Deque_
(estruturas_de_dados)11 .

Python fornece uma deque no módulo collections e podemos importá-la


assim:

from collections import deque


10
N.T.: Do inglês, depth-first search (DFS)
11
https://en.wikipedia.org/wiki/Double-ended_queue
44 Capítulo 3 Grafos de pequeno mundo

Podemos usá-la para escrever uma busca em profundidade eficiente:

def reachable_nodes_bfs(G, start):


seen = set()
queue = deque([start])
while queue:
node = queue.popleft()
if node not in seen:
seen.add(node)
queue.extend(G.neighbors(node))
return seen

As diferenças são:

• Eu substituí a lista chamada stack com uma deque chamada queue.

• Eu susbtituí pop por popleft, que remove e devolve o valor mais à


esquerda da fila, isto é, o primeiro que foi adicionado.

Esta versão volta a ser O(n + m). Agora estamos prontos para encontrar os
caminhos mais curtos.

3.10 O algoritmo de Dijkstra


Edsger W. Dijkstra foi um cientista da computação holandês que inventou um
algoritmo eficiente de caminho mais curto (veja https://pt.wikipedia.org/
wiki/Algoritmo_de_Dijkstra12 ). Ele também inventou o semáforo, uma es-
trutura de dados usada para coordenação de programas que comunicam-se
uns com os outros (veja https://pt.wikipedia.org/wiki/Sem%C3%A1foro_
(computa%C3%A7%C3%A3o)13 e Downey, The Little Book of Semaphores14 ).

Dijkstra é famoso (e notório) como o autor de uma série de ensaios sobre ciên-
cias da computação. Alguns, como “A Case against the GO TO Statement”15 ,
tiveram um efeito profundo na prática de programação. Outros, como “On
12
http://en.wikipedia.org/wiki/Dijkstra’s_algorithm
13
http://en.wikipedia.org/wiki/Semaphore_(programming)
14
N.T.: O Pequeno Livro de Semáforos, livro sem tradução para a língua portuguesa
15
N.T.: Um caso contra o comando GO TO.
3.10 O algoritmo de Dijkstra 45

the Cruelty of Really Teaching Computing Science”16 , são divertidos com sua
rabugice, mas menos eficazes.

O algoritmo de Dijkstra resolve o “problema de caminho mais curto com


única origem”, o que significa que ele encontra a distância mínima entre uma
dada “origem” e todos os demais nós do grafo (ou pelo menos dos nós conec-
tados).

Começamos com uma versão simplificada do algoritmo que considera que todas
as arestas têm o mesmo comprimento. A versão mais geral funciona com
qualquer comprimento não negativo de aresta.

A versão simplificada é semelhante à busca em largura da Seção 3.9, exceto


que substituímos o conjunto chamado seen por um dicionário chamado dist,
o qual mapeia cada nó à sua distância da origem:

def shortest_path_dijkstra(G, start):


dist = {start: 0}
queue = deque([start])
while queue:
node = queue.popleft()
new_dist = dist[node] + 1

neighbors = set(G[node]) - set(dist)


for n in neighbors:
dist[n] = new_dist

queue.extend(neighbors)
return dist

É assim que funciona:

• Inicialmente, queue contém um único elemento, start, e dist mapeia


start à distância 0 (distância de start para si).

• A cada execução do laço, usamos popleft para selecionar nós na ordem


em que foram adicionados à fila.
N.T.: Sobre a crueldade de realmente ensinar a ciência da computação, ver http://
16

marathoncode.blogspot.com.br/2012/07/sobre-crueldade-de-ensinar-realmente.
html
46 Capítulo 3 Grafos de pequeno mundo

• Em seguida, encontramos todos os vizinhos de node que ainda não estão


em dist.

• Como a distância de start para node é dist[node], a distância para


qualquer um dos vizinhos não descobertos é dist[node]+1.

• Para cada vizinho, adicionamos uma entrada a dist, e então adicionamos


os vizinhos à fila.

Este algoritmo só funciona se usarmos busca em largura, não funciona para


busca em profundidade. Por quê?

Na primeira execução do laço node é start e new_dist é 1. Então, os vizinhos


de start recebem a distância 1 e vão para fila.

Quando processamos os vizinhos de start, todos os seus vizinhos recebem dis-


tância 2. Sabemos que nenhum deles pode ter distância 1, já que se tivessem,
os teríamos descoberto durante a primeira iteração.

Da mesma forma, quando processamos os nós com distância 2, damos aos


vizinhos distância 3. Sabemos que nenhum deles pode ter distância 1 ou 2,
porque se tivessem, os teríamos descoberto durante uma iteração anterior.

E assim por diante. Se você está familiarizado com prova por indução, você
pode ver para onde isto está indo.

Mas esse argumento só funciona se processarmos todos os nós com a distância


1 antes de começarmos a processar nós com distância 2, e assim por diante. E
é exatamente isso que faz a busca em largura.

Nos exercícios no final deste capítulo, você escreverá uma versão do algoritmo
de Dijkstra usando busca em profundidade, e então você terá a chance de ver
o que dá errado.

3.11 Exercícios
Exercício 3.1 Em uma rede em anel, cada nó possui o mesmo número de
vizinhos. O número de vizinhos é chamado de grau do nó, e um grafo no qual
todos os nós têm o mesmo grau é chamado de grafo regular.
3.11 Exercícios 47

Todas as estruturas em anel são regulares, mas nem todos os grafos regulares
são estruturas em anel. Em particular, se k for ímpar, não podemos construir
uma rede em anel, mas talvez possamos construir um grafo regular.

Escreva uma função chamada make_regular_graph que recebe n e k e devolve


um grafo regular que contém n nós, em que cada nó possui k vizinhos. Se não
for possível fazer um grafo regular com os valores dados de n e k, a função
deve gerar uma exceção de ValueError.

Exercício 3.2 A minha implementação de reachable_nodes_bfs é efici-


ente no sentido de que está em O(n + m), mas incorre em muita sobrecarga
por adicionar e remover nós da fila. O NetworkX fornece uma implemen-
tação simples e rápida da busca em largura, disponível no repositório do
NetworkX no GitHub em https://github.com/networkx/networkx/blob/
master/networkx/algorithms/components/connected.py.

Segue uma versão que modifiquei para devolver um conjuntos de nós:

def _plain_bfs(G, source):


seen = set()
nextlevel = {source}
while nextlevel:
thislevel = nextlevel
nextlevel = set()
for v in thislevel:
if v not in seen:
seen.add(v)
nextlevel.update(G[v])
return seen

Compare essa função com reachable_nodes_bfs e veja qual é mais rápida.


Em seguida, veja se você consegue modificar esta função para implementar
uma versão mais rápida de shortest_path_dijkstra.

Exercício 3.3 A seguinte implementação de busca em largura contém dois


erros de desempenho. O que são eles? Qual é a ordem real de crescimento
para este algoritmo?
48 Capítulo 3 Grafos de pequeno mundo

def bfs(top_node, visit):


"""Breadth-first search on a graph, starting at top_node."""
visited = set()
queue = [top_node]
while len(queue):
curr_node = queue.pop(0) # Dequeue
visit(curr_node) # Visit the node
visited.add(curr_node)

# Enqueue non-visited and non-enqueued children


queue.extend(c for c in curr_node.children
if c not in visited and c not in queue)

Exercício 3.4 Na Seção 3.10, afirmei que o algoritmo de Dijkstra não fun-
ciona a menos que ele use busca em largura. Escreva uma versão da função
shortest_path_dijkstra que usa busca em profundidade e teste-a em alguns
exemplos para ver o que está errado.

Exercício 3.5 Uma pergunta natural sobre o artigo Watts e Strogatz é se


o fenômeno do pequeno mundo é específico para seu modelo generativo ou
se outros modelos similares produzem o mesmo resultado qualitativo (alto
agrupamento e caminhos de comprimentos curtos).

Para responder a esta pergunta, escolha uma variação do modelo de Watts e


Strogatz e repita a experiência. Existem dois tipos de variações que você pode
considerar:

• Ao invés de começar com um grafo regular, comece com outro gráfico


que tenha agrupamento alto. Por exemplo, você pode colocar nós em
locais aleatórios em um espaço 2-D e conectar cada nó aos k vizinhos
mais próximos.

• Experimente com diferentes tipos de religação.

Se uma série de modelos similares produzem um comportamento semelhante,


dizemos que os resultados do artigo são robustos.

Exercício 3.6 O algoritmo da Dijkstra resolve o problema do “caminho


mais curto com única origem”, mas para calcular o comprimento do caminho
3.11 Exercícios 49

característico de um grafo, nós realmente queremos resolver o problema do


“caminho mais curto de todos os pares”.

Claro que uma opção é executar o algoritmo de Dijkstra n vezes, uma vez para
cada nó de inicial. E para algumas aplicações, essa solução provavelmente é
boa o suficiente. Mas existem alternativas mais eficientes.

Encontre um algoritmo para o problema do caminho mais curto de todos


pares e implemente-o. Veja https://en.wikipedia.org/wiki/Shortest_
path_problem#All-pairs_shortest_paths17 .

Compare o tempo de execução de sua implementação com a execução do algo-


ritmo de Dijkstra n vezes. Qual algoritmo é melhor em teoria? Qual é melhor
na prática? Qual deles o NetworkX usa?

17
N.T.: O artigo na Wikipédia não oferece a mesma subseção: https://pt.wikipedia.
org/wiki/Problema_do_caminho_mínimo.
50 Capítulo 3 Grafos de pequeno mundo
Capítulo 4

Redes livres de escala

Neste capítulo, trabalharemos com dados de uma rede social online e usaremos
um grafo Watts-Strogatz para modelá-la. O modelo de WS tem características
de uma rede de pequeno mundo, como esses dados que temos de uma rede
social, mas tem baixa variabilidade no número de vizinhos de um nó para o
outro, ao contrário do que ocorre com esses dados.

Essa discrepância é a motivação para um modelo de rede desenvolvido por Ba-


rabási e Albert. O modelo de BA captura a variabilidade observada no número
de vizinhos e tem uma das propriedades de mundo pequeno, comprimentos de
caminho curtos, mas não possui o agrupamento alto de uma rede de pequeno
mundo.

O capítulo termina com uma discussão sobre os grafos WS e BA como modelos


explicativos para redes de pequeno mundo.

O código para este capítulo está em chap04.ipynb no repositório deste livro.


Para mais informações sobre como trabalhar com o código, ver a Seção 0.2.

4.1 Dados de redes sociais


Os grafos Watts-Strogatz destinam-se a modelar redes nas ciências naturais e
sociais. Em seu artigo original, Watts e Strogatz observaram a rede de atores
de cinema (conectados se apareceram juntos em um filme); a rede elétrica do
52 Capítulo 4 Redes livres de escala

oeste dos Estados Unidos; e a rede de neurônios no cérebro do nematódeo C.


elegans. Eles descobriram que todas essas redes tinham a alta conectividade
e os comprimentos de caminho curtos característicos dos grafos de mundo
pequeno.

Nesta seção, realizaremos a mesma análise com um conjunto de dados di-


ferente, um conjunto de usuários do Facebook e seus amigos. Se você não
está familiarizado com o Facebook, nele os usuários que estão conectados uns
aos outros são chamados de “amigos”, independentemente da natureza de seu
relacionamento no mundo real.

Eu usarei dados do Stanford Network Analysis Project (SNAP)1 , que compar-


tilha grandes conjuntos de dados de redes sociais online e de outras fontes.
Especificamente, usarei seu conjunto de dados do Facebook2 , que inclui 4039
usuários e 88.234 relações de amizade entre eles. Este conjunto de dados está
no repositório deste livro, mas também está disponível no site do SNAP em
https://snap.stanford.edu/data/egonets-Facebook.html

O arquivo de dados contém uma linha por aresta, com usuários identificados
por números inteiros de 0 a 4038. O código a seguir lê o arquivo:
def read_graph(filename):
G = nx.Graph()
array = np.loadtxt(filename, dtype=int)
G.add_edges_from(array)
return G
O NumPy fornece uma função chamada loadtxt que lê o arquivo fornecido e
devolve o conteúdo como um array NumPy. O parâmetro dtype especifica o
tipo dos elementos do array.

Agora podemos verificar se este conjunto de dados possui as características de


um grafo de pequeno mundo: agrupamento alto e comprimentos de caminho
curtos.

Na Seção 3.5, escrevemos uma função para calcular o coeficiente de agrupa-


mento médio da rede. O NetworkX fornece uma função chamada average_clustering
que faz a mesma coisa um pouco mais rápido.
N.T.: Projeto de Análise de Rede Stanford.
1

J. McAuley e J. Leskovec. Learning to Discover Social Circles in Ego Networks. NIPS,


2

2012.
4.1 Dados de redes sociais 53

Porém, para grafos maiores ambas as funções são muito lentas, levando um
tempo proporcional a nk 2 , com n o número de nós e k o número de vizinhos
aos quais cada nó está conectado.

Felizmente, o NetworkX fornece uma função que estima o coeficiente de agru-


pamento por amostragem aleatória. Você pode chamá-la assim:

from networkx.algorithms.approximation import average_clustering


average_clustering(G, trials=1000)

A função a seguir faz algo parecido para os comprimentos dos caminhos.

def random_path_lengths(G, nodes=None, trials=1000):


if nodes is None:
nodes = G.nodes()
else:
nodes = list(nodes)

pairs = np.random.choice(nodes, (trials, 2))


lengths = [nx.shortest_path_length(G, *pair)
for pair in pairs]
return lengths

G é um grafo, nodes é a lista de nós de onde devemos tirar a amostra e trials


é o número de caminhos aleatórios da amostra. Se nodes for None, usamos
todo o grafo para obter a amostra.

pairs é um array do NumPy com nós escolhidos aleatoriamente com uma


linha para cada teste e duas colunas.

A lista de abrangência enumera as linhas no array e calcula a menor distância


entre cada par de nós. O resultado é uma lista de comprimentos de caminhos.

estimate_path_length gera uma lista de comprimentos de caminhos aleató-


rios e devolve sua média:
def estimate_path_length(G, nodes=None, trials=1000):
return np.mean(random_path_lengths(G, nodes, trials))

Usarei average_clustering para calcular C:

C = average_clustering(fb)
54 Capítulo 4 Redes livres de escala

E estimate_path_lengths para calcular L:

L = estimate_path_lengths(fb)

O coeficiente de agrupamento é de cerca de 0,61, o que é alto, como esperado


se esta rede tiver a propriedade de pequeno mundo.

E o caminho médio é 3,7, que é bastante curto em uma rede de mais de 4000
usuários. É um mundo pequeno, afinal.

Agora, vejamos se podemos construir um grafo WS que tenha as mesmas


características que essa rede.

4.2 Modelo WS
No conjunto de dados do Facebook, o número médio de arestas por nó é de
cerca de 22. Como cada aresta está conectada a dois nós, o grau médio é o
dobro do número de arestas por nó:

>>> k = int(round(2*m/n))
>>> k
44

Podemos fazer um grafo WS com n = 4039 e k = 44. Quando p = 0, obtemos


uma rede em anel.

lattice = nx.watts_strogatz_graph(n, k, 0)

Neste grafo, o agrupamento é alto: C é 0,73, em comparação com 0,61 no


conjunto de dados. Mas L é 46, muito maior do que no conjunto de dados!

Com p=1, obtemos um grafo aleatório:

random_graph = nx.watts_strogatz_graph(n, k, 1)

No grafo aleatório, L é 2,6, ainda mais curto do que no conjunto de dados


(3,7), mas C é apenas 0,011, então isso não é bom.

Por tentativa e erro, descobrimos que quando p=0.05 obtemos um grafo WS


com agrupamento alto e comprimentos de caminho curtos:
4.3 Grau 55

Facebook 0.30 grafo WS

0.025

0.25

0.020
0.20

0.015
FMP

FMP
0.15

0.010
0.10

0.005 0.05

0.000 0.00

0 200 400 600 800 1000 38 40 42 44 46 48


grau grau

Figura 4.1: FMP do grau no conjunto de dados do Facebook e no modelo WS.

ws = nx.watts_strogatz_graph(n, k, 0.05, seed=15)

Neste grafo C é 0,63, um pouco maior do que no conjunto de dados, e L é 3,2,


um pouco menor do que no conjunto de dados. Então este gráfico modela as
características de pequeno mundo do conjunto de dados.

Por enquanto, tudo bem.

4.3 Grau
Lembre-se de que o grau de um nó é o número de vizinhos a que está conectado.
Se o grafo WS for um bom modelo para a rede do Facebook, ele deve ter o
mesmo grau total (ou médio) e, idealmente, a mesma variância do grau entre
nós.

A função a seguir devolve uma lista de graus em um grafo, um para cada nó:

def degrees(G):
return [G.degree(u) for u in G]

O grau médio no conjunto de dados é 43,7; O grau médio no modelo WS é 44.


Até agora, tudo bem.
56 Capítulo 4 Redes livres de escala

No entanto, o desvio padrão de graus no modelo WS é 1.5; o desvio padrão


nos dados é 52.4. Opa!

O que está acontecendo aqui? Para obter uma visão melhor, temos que olhar
para a distribuição de graus, não apenas para a média e o desvio padrão.

Eu representarei a distribuição de graus com um objeto Pmf, definido no mó-


dulo thinkstats2. Pmf significa “função massa de probabilidade”3 (FMP); Se
você não está familiarizado com esse conceito, você pode achar interessante
ler o Capítulo 3 de Think Stats, 2nd edition em http://greenteapress.com/
thinkstats2/html/thinkstats2004.html.

Resumidamente, uma Pmf mapeia valores a suas probabilidades. A Pmf de


graus é um mapeamento de cada grau possível, d, para a fração de nós com
grau d.

Como exemplo, vou construir um grafo com os nós 1, 2 e 3 conectados a um


nó central, 0:

G = nx.Graph()
G.add_edge(1, 0)
G.add_edge(2, 0)
G.add_edge(3, 0)
nx.draw(G)

Esta é a lista de graus neste grafo:

>>> degrees(G)
[3, 1, 1, 1]

O nó 0 tem grau 3, os outros têm grau 1. Agora eu posso fazer uma Pmf que
representa esta distribuição de grau:

>>> from thinkstats2 import Pmf


>>> Pmf(degrees(G))
Pmf({1: 0.75, 3: 0.25})

O resultado é um objeto Pmf que mapeia de cada grau para uma fração ou
probabilidade. Neste exemplo, 75% dos nós têm grau 1 e 25% têm grau 3.
3
N.T.: Do inglês, probability mass function (PMF).
4.4 Distribuições de cauda pesada 57

Agora podemos fazer uma Pmf que contém graus de nós do conjunto de dados
e calcular a média e o desvio padrão:
>>> from thinkstats2 import Pmf
>>> pmf_fb = Pmf(degrees(fb))
>>> pmf_fb.Mean(), pmf_fb.Std()
(43.691, 52.414)
O mesmo pode ser feito para o modelo WS:
>>> pmf_ws = Pmf(degrees(ws))
>>> pmf_ws.mean(), pmf_ws.std()
(44.000, 1.465)
Podemos usar o módulo thinkplot para mostrar graficamente os resultados:

thinkplot.Pdf(pmf_fb, label='Facebook')
thinkplot.Pdf(pmf_ws, label='WS graph')
A Figura 4.1 mostra as duas distribuições. Elas são muito diferentes.

No modelo WS, a maioria dos usuários tem cerca de 44 amigos; o mínimo é


38 e o máximo é 50. Isso não é uma variação muito grande. No conjunto de
dados, existem muitos usuários com apenas 1 ou 2 amigos, mas um tem mais
de 1000!

Distribuições como esta, com muitos valores pequenos e alguns valores muito
grandes, são chamadas distribuições de cauda pesada.

4.4 Distribuições de cauda pesada


As distribuições de cauda pesada são comuns em muitas áreas da ciência da
complexidade e elas serão um tema recorrente deste livro.

Podemos obter uma imagem mais clara de uma distribuição de cauda pesada,
fazendo um gráfico log-log, como mostrado na Figura 4.2. Essa transformação
enfatiza a cauda da distribuição, isto é, as probabilidades de valores grandes.

Sob essa transformação, os dados caem aproximadamente em linha reta, o que


sugere que existe uma relação de “lei de potência” entre os maiores valores na
distribuição e suas probabilidades. Matematicamente,
58 Capítulo 4 Redes livres de escala

Facebook grafo WS

1
10

2
10

2
10
FMP

FMP
3
10

3
10

100 101 102 103 4 × 101 5 × 101


grau grau

Figura 4.2: FMP do grau no conjunto de dados do Facebook e no modelo WS


em uma escala log-log.

FMP(k) ∼ k −α

em que FMP(k) é a fração de nós com grau k, α é um parâmetro, e o símbolo


∼ indica que a FMP é assintótica para k −α à medida que k aumenta.

Se aplicarmos o logaritmo em ambos os lados, obtemos

log FMP(k) ∼ −α log k

Assim, se uma distribuição segue uma lei de potência e fizermos um gráfico de


FMP(k) por k em uma escala log-log, esperamos uma linha reta com inclinação
−α, pelo menos para valores grandes de k.

Todas as distribuições de lei do potência são caudas pesadas, mas existem ou-
tras distribuições de cauda pesada que não seguem a lei de potência. Veremos
mais exemplos em breve.

Mas antes, ainda temos um problema: o modelo WS tem o alto agrupamento


e os comprimentos de caminho curtos que vemos nos dados, mas a distribuição
de grau não se assemelha e nada aos dados. Esta discrepância é a motivação
para o nosso próximo tópico, o modelo de Barabási-Albert.
4.5 Modelo de Barabási-Albert 59

4.5 Modelo de Barabási-Albert


Em 1999 Barabási e Albert publicaram um artigo, “Emergence of Scaling
in Random Networks”4 , que caracteriza a estrutura de várias redes do mundo
real, incluindo grafos que representam a interconectividade de atores de filmes,
páginas da World Wide Web (WWW) e elementos na rede de energia elétrica
no oeste dos Estados Unidos. Você pode baixar o documento de http://www.
sciencemag.org/content/286/5439/509.

Eles medem o grau de cada nó e calculam a FMP(k), a probabilidade de que


um vértice tenha grau k. Em seguida, eles fazem um gráfico de FMP(k) por
k em uma escala log-log. As curvas obtidas se ajustam a uma linha reta, pelo
menos para valores grandes de k. Barabási e Albert concluiram que essas
distribuições são de cauda pesada.

Eles também propõem um modelo que gera grafos com a mesma propriedade.
As características essenciais do modelo, que o distinguem do modelo WS, são:

Crescimento: Em vez de começar com um número fixo de vértices, o modelo


BA começa com um pequeno grafo e adiciona vértices um de cada vez.

Anexação preferencial: Quando uma nova aresta é criada, é mais provável


que se conecte a um vértice que já tenha um grande número de ares-
tas. Esse efeito “rico se torna mais rico” é característico dos padrões de
crescimento de algumas redes do mundo real.

Finalmente, eles mostram que os grafos gerados pelo modelo de Barabási-


Albert (BA) têm uma distribuição de graus que obedece a uma lei de potência.

Os grafos com esta propriedade às vezes são chamados redes livres de escala,
por razões que não vou explicar. Se você é curioso, pode ler mais em https:
//pt.wikipedia.org/wiki/Rede_sem_escala5 .

O NetworkX fornece uma função que gera grafos BA. Primeira vamos usá-la
e na sequência eu vou mostrar como funciona.

ba = nx.barabasi_albert_graph(n=4039, k=22)
4
N.T.: Surgimento de escala em redes aleatórias
5
http://en.wikipedia.org/wiki/Scale-free_network
60 Capítulo 4 Redes livres de escala

1
10
Facebook modelo BA

2
10

2
10
FMP

FMP
3
10
3
10

100 101 102 103 100 101 102 103 104


grau grau

Figura 4.3: FMP do grau no conjunto de dados do Facebook e no modelo BA


em uma escala log-log.

Os parâmetros são n, o número de nós a serem gerados, e k, o número de arestas


que cada nó tem quando é adicionado ao grafo. Eu escolhi k=22 porque esse é
o número médio de arestas por nó no conjunto de dados.

O gráfico resultante possui 4039 nós e 21,9 arestas por nó. Uma vez que cada
aresta está conectada a dois nós, o grau médio é 43,8, muito próximo ao grau
médio no conjunto de dados, 43,7.

E o desvio padrão do grau é de 40,9, um pouco menos do que no conjunto de


dados, 52.4. Mas ainda é muito melhor do que o que obtivemos com o grafo
WS, 1,5.

A Figura 4.3 mostra as distribuições de grau para o Facebook e o modelo BA


em uma escala log-log. O modelo não é perfeito; em particular, afasta-se dos
dados quando k é inferior a 10. Mas a cauda parece uma linha reta, o que
sugere que esse processo gera distribuições de grau que seguem uma lei de
potência.

Assim, o modelo BA é melhor do que o modelo WS na reprodução do grau


distribuição. Mas tem a propriedade do pequeno mundo?

Neste exemplo, o comprimento médio dos caminhos, L, é 2,5, o que é ainda


4.6 Gerando grafos BA 61

Facebook modelo WS modelo BA


C 0,61 0,63 0,037
L 3,69 3,23 2,51
grau médio 43,7 44 43,8
desvio padrão do grau 52,4 1,5 40,1
Lei de potência? talvez não sim

Tabela 4.1: Características da rede do Facebook em comparação a dois mode-


los.

mais “pequeno mundo” do que a rede real, que tem L = 3,69. O resultado é
bom, mas talvez seja bom demais.

Por outro lado, o coeficiente de agrupamento, C, é 0,037, e não chega nem


perto do valor do conjunto de dados, 0,61. Isso é um problema.

A Tabela 4.1 resume estes resultados. O modelo WS captura as características


de pequeno mundo, mas não a distribuição de grau. O modelo BA captura a
distribuição de grau, pelo menos aproximadamente, e o comprimento médio
dos caminhos, mas não o coeficiente de agrupamento.

Nos exercícios no final deste capítulo, você pode explorar outros modelos des-
tinados a capturar todas essas características.

4.6 Gerando grafos BA

Nas seções anteriores, usamos uma função do NetworkX para gerar grafos BA.
Agora vamos ver como isso funciona. Aqui está uma versão de barabasi_albert_graph,
com algumas mudanças que fiz para facilitar a leitura:
62 Capítulo 4 Redes livres de escala

def barabasi_albert_graph(n, k):

G = nx.empty_graph(k)
targets = list(range(k))
repeated_nodes = []

for source in range(k, n):

G.add_edges_from(zip([source]*k, targets))

repeated_nodes.extend(targets)
repeated_nodes.extend([source] * k)

targets = _random_subset(repeated_nodes, k)

return G

Os parâmetros são n, o número de nós que queremos, e k, o número de arestas


que cada novo nó recebe (o qual será aproximadamente o número de arestas
por nó).

Começamos com um grafo que tem k nós e sem arestas. Em seguida, iniciali-
zamos duas variáveis:

targets: A lista de nós k que serão conectados ao próximo nó. Inicialmente


targets contém os nós originais k; mais tarde, irá conter um subconjunto
aleatório de nós.

repeated_nodes: Uma lista de nós existentes onde cada nó aparece uma


vez para cada aresta a que está conectado. Quando selecionamos de
repeated_nodes, a probabilidade de selecionar qualquer nó é proporci-
onal ao número de arestas que possui.

A cada iteração do laço, adicionamos arestas da source a cada nó em targets.


Em seguida, atualizamos repeated_nodes adicionando cada target uma vez e
o novo nó k vezes.

Finalmente, escolhemos alguns dos nós para serem alvos na próxima iteração.
A definição de _random_subset é a seguinte:
4.7 Distribuições acumulativas 63

1.0 Facebook 1.0 Facebook


modelo WS modelo BA

0.8 0.8

0.6 0.6
FDA

FDA
0.4 0.4

0.2 0.2

0.0 0.0

100 101 102 103 100 101 102 103


grau grau

Figura 4.4: FDA do grau no conjunto de dados do Facebook juntamente com


o modelo WS (à esquerda) e o modelo BA (à direita) em uma escala log-x.

def _random_subset(repeated_nodes, k):


targets = set()
while len(targets) < k:
x = random.choice(repeated_nodes)
targets.add(x)
return targets

A cada iteração do laço, _random_subset escolhe um nó de repeated_nodes e


adiciona o nó escolhido a targets. Como targets é um conjunto, duplicatas
são descartadas automaticamente, então o laço só encerra quando selecionamos
k nós diferentes.

4.7 Distribuições acumulativas

A Figura 4.3 representa a distribuição do grau com o gráfico da função massa


de probabilidade (FMP) em uma escala log-log. É assim que Barabási e Albert
apresentam seus resultados e é a representação usada com mais frequência em
artigos sobre distribuições de leis de potência. Mas esta não é a melhor maneira
de ver dados como este.
64 Capítulo 4 Redes livres de escala

Uma alternativa melhor é uma função distribuição acumulada (FDA), que


mapeia de um valor, x, para a fração de valores inferior ou igual a x.

Dada uma Pmf, a maneira mais simples de calcular uma probabilidade acumu-
lada é adicionar as probabilidades de valores até x inclusive:

def cumulative_prob(pmf, x):


ps = [pmf[value] for value in pmf if value<=x]
return sum(ps)

Por exemplo, dada a distribuição do grau no conjunto de dados, pmf_pf, po-


demos calcular a fração de usuários com 25 ou menos amigos:

>>> cumulative_prob(pmf_fb, 25)


0.506

O resultado é próximo de 0,5, o que significa que a quantidade mediana de


amigos é de cerca de 25.

As FDAs são melhores para a visualização porque são menos poluídas do que
as FMPs. Depois de se acostumar a interpretar FDAs, eles fornecem uma
imagem mais clara da forma de uma distribuição do que FMPs.

O módulo thinkstats fornece uma classe chamada Cdf que representa uma
função distribuição acumulada. Podemos usá-la para calcular a FDA do grau
no conjunto de dados.

from thinkstats2 import Cdf


cdf_fb = Cdf(degrees(fb), label='Facebook')

E thinkplot fornece uma função chamada Cdf que traça funções distribuição
acumulada.

thinkplot.Cdf(cdf_fb)

A Figura 4.4 mostra a FDA do grau para o conjunto de dados do Facebook


juntamente com o modelo WS (à esquerda) e o modelo BA (à direita). O eixo
x está em uma escala log.

Claramente, a FDA para o modelo WS é muito diferente da FDA dos dados. O


modelo BA é melhor, mas ainda não é muito bom, especialmente para valores
pequenos.
4.7 Distribuições acumulativas 65

100 Facebook 100 Facebook


modelo WS modelo BA

1 1
10 10
FDAC

FDAC
2 2
10 10

3 3
10 10

100 101 102 103 100 101 102 103


grau grau

Figura 4.5: FDA complementar do grau no conjunto de dados do Facebook


junto com o modelo WS (à esquerda) e o modelo BA (à direita) em uma escala
log-log.

Na cauda da distribuição (valores superiores a 100), parece que o modelo BA


combina com o conjunto de dados o suficiente, mas é difícil de visualizar.
Podemos obter uma visualização mais clara com uma outra visão dos dados:
fazendo o gráfico da FDA complementar em uma escala log-log.

A FDA complementar (FDAC) é definida como

FDAC(x) = 1 − FDA(x)

É útil porque se a FMP segue uma lei de potência, a FDAC também segue
uma lei de potência:

−α
x

FDAC(x) =
xm

em que xm é o mínimo valor possível e α é um parâmetro que determina a


forma da distribuição.

Aplicando o logaritmo em ambos os lados resulta:


66 Capítulo 4 Redes livres de escala

derivação
Modelo Comportamento

abstração analogia

observação
Sistema Observável

Figura 4.6: Estrutura lógica de um modelo explicativo.

log FDAC(x) = −α(log x − log xm )

Assim, se a distribuição obedecer a uma lei de potência, esperamos que a


FDAC em uma escala log-log seja linear com inclinação −α.

A Figura 4.5 mostra a FDAC do grau para os dados do Facebook, juntamente


com o modelo WS (à esquerda) e o modelo BA (à direita), em uma escala
log-log.

Com esta maneira de visualizar os dados, podemos ver que o modelo BA


corresponde à cauda da distribuição (valores acima de 20) razoavelmente bem.
O modelo WS não.

4.8 Modelos explicativos


Começamos a discussão de redes com a experiência de Pequeno Mundo de
Milgram, que mostra que o comprimento de caminho nas redes sociais é sur-
preendentemente pequeno; de onde vem a expressão “seis graus de separação”.

Quando vemos algo surpreendente, é natural perguntar “Por quê?”. Mas às


vezes, não está claro qual o tipo de resposta que estamos procurando. Um
tipo de resposta é um modelo explicativo (veja a Figura 4.6). A estrutura
lógica de um modelo explicativo é:
4.8 Modelos explicativos 67

1. Em um sistema, S, vemos algo observável, O, que justifica a explicação.

2. Nós construímos um modelo, M, que é análogo ao sistema; isto é, há


uma correspondência entre os elementos do modelo e os elementos do
sistema.
3. Por simulação ou derivação matemática, mostramos que o modelo exibe
um comportamento, B, que é análogo a O.
4. Concluímos que S exibe O porque S é semelhante a M, M exibe B, e B é
semelhante a O.
No seu núcleo, este é um argumento por analogia que diz que se duas coisas são
semelhantes de certa forma, elas provavelmente serão semelhantes de outras
maneiras.
Argumento por analogia pode ser útil, e os modelos explicativos podem ser sa-
tisfatórios, mas não constituem uma prova no sentido matemático da palavra.

Lembre-se de que todos os modelos deixam de fora, ou “abstraem” detalhes


que achamos que não são importantes. Para qualquer sistema, existem mui-
tos modelos possíveis que incluem ou ignoram recursos diferentes. E pode
haver modelos que exibam comportamentos diferentes, B, B’ e B”, que são
semelhantes a O de maneiras diferentes. Nesse caso, qual modelo explica O?
O fenômeno de pequeno mundo é um exemplo: o modelo Watts-Strogatz (WS)
e o modelo Barabási-Albert (BA) exibem elementos de comportamento de
pequeno mundo, mas oferecem diferentes explicações:
• O modelo WS sugere que as redes sociais são “pequenas” porque incluem
agrupamentos fortemente conectados e “ligações fracas” que conectam
agrupamentos (veja http://en.wikipedia.org/wiki/Mark_Granovetter#
The_strength_of_weak_ties6 ).
• O modelo de BA sugere que as redes sociais são pequenas porque incluem
nós com alto grau que atuam como centros de conexão e esses centros
crescem ao longo do tempo devido à anexação preferencial.
Como é frequentemente o caso em novas áreas da ciência, o problema é não é
não termos explicações, mas termos explicações demais.
6
Sem artigo correspondente na Wikipédia.
68 Capítulo 4 Redes livres de escala

4.9 Exercícios

Exercício 4.1 Na Seção 4.8 discutimos duas explicações para o fenômeno


de pequeno mundo, “ligações fracas” e “centros”. Essas explicações são com-
patíveis? Isto é, ambas podem estar certas? Qual você achou mais satisfatória
como uma explicação e por quê?

Há dados que você poderia coletar ou experimentos que você poderia realizar
que forneceriam provas em favor de um ou outro modelo?

A escolha entre os modelos concorrentes é o tema do ensaio de Thomas Kuhn,


“Objectivity, Value Judgment, and Theory Choice”7 , que você pode ler em
https://github.com/AllenDowney/ThinkComplexity2/blob/master/papers/
kuhn.pdf.

Qual critério Kuhn propõe para escolher entre modelos concorrentes? Esses
critérios influenciam sua opinião sobre os modelos WS e BA? Existem outros
critérios que você acha que devem ser considerados?

Exercício 4.2 O NetworkX fornece uma função chamada powerlaw_cluster_graph


que implementa o “algoritmo de Holme e Kim para gráficos que crescem com
distribuição de grau de lei de potência e agrupamento aproximadamente mé-
dio”. Leia a documentação desta função e veja se você pode usá-la para gerar
um grafo que tenha o mesmo número de nós que o conjunto de dados do Fa-
cebook, o mesmo grau médio e o mesmo coeficiente de agrupamento. Como a
distribuição do grau no modelo se compara à distribuição real?

Exercício 4.3 Os arquivos de dados do artigo de Barabási e Albert estão


disponíveis em http://www3.nd.edu/~networks/resources.htm. Os dados
deles de colaboração entre atores foram incluídos no repositório deste livro em
um arquivo chamado actor.dat.gz. A seguinte função lê o arquivo e constrói
o grafo.

7
N.T.: Objetividade, julgamento de valor e escolha de teoria.
4.9 Exercícios 69

import gzip

def read_actor_network(filename, n=None):


G = nx.Graph()
with gzip.open(filename) as f:
for i, line in enumerate(f):
nodes = [int(x) for x in line.split()]
G.add_edges_from(thinkcomplexity.all_pairs(nodes))
if n and i >= n:
break
return G

Calcule o número de atores no grafo e o grau médio. Faço o gráfico da FMP


do grau em uma escala log-log. Inclua também a FDA do grau em uma escala
log-x para ver a forma geral da distribuição e em uma escala log-log para ver
se a cauda segue uma lei de potência.

Nota: A rede de atores não está conectada, então você pode usar nx.connected_component_sub
para encontrar subconjuntos conectados dos nós.
70 Capítulo 4 Redes livres de escala
Capítulo 5

Autômatos Celulares

Um autômato celular (AC) é um modelo de um mundo com uma física


muito simples. “Celular” significa que o mundo é dividido em blocos dis-
cretos, chamados de células. Um “autômato” é uma máquina que executa
computações—pode ser uma máquina real, mas mais frequentemente a “má-
quina” é uma abstração matemática ou uma simulação computacional.

Este capítulo apresenta experiências que Steven Wolfram realizou na década


de 1980, com as quais mostrou que alguns autômatos celulares apresentam
comportamentos surpreendentemente complicados, incluindo a capacidade de
realizar cálculos arbitrários.

Eu discuto as implicações desses resultados e no final do capítulo sugiro mé-


todos para implementar ACs de forma eficiente em Python.

O código para este capítulo está em chap05.ipynb no repositório deste livro.


Para mais informações sobre como trabalhar com o código, ver a Seção 0.2.

5.1 Um AC simples
Autômatos celulares1 são regidos por regras que determinam como o sistema
evolui no tempo. O tempo é dividido em passos discretos, e as regras especifi-
1
N.T: Na versão em inglês, o autor faz uma observação sobre a possibilidade do uso
automata como plural de automaton, além de automatons.
72 Capítulo 5 Autômatos Celulares

cam como calcular o estado do mundo no próximo passo com base no estado
atual.

Considere o exemplo trivial de um autômato celular (AC) com uma única


célula. O estado da célula é um número inteiro representado pela variável xi ,
em que o índice i indica que xi é o estado do sistema durante o passo i. Como
condição inicial, x0 = 0.

Agora precisamos apenas de uma regra. Arbitrariamente, vou escolher xi =


xi−1 + 1, que determina que a cada passo o estado do AC é incrementado de
1. Até aqui temos um AC simples que executa um cálculo simples: conta.

Mas este AC é atípico uma vez que, normalmente, o número de estados pos-
síveis é finito. Para adequá-lo, vou escolher o menor número interessante de
estados, 2, e outra regra simples, xi = (xi−1 + 1)%2, em que % é o operador
que fornece o resto da divisão inteira.

O comportamento deste AC é simples: ele pisca. Isto é, o estado da célula


muda entre 0 e 1 a cada passo.

A maioria dos ACs são determinísticos, o que significa que as regras não
têm elementos aleatórios; dado o mesmo estado inicial, elas sempre produzem
o mesmo resultado. Também existem ACs não-determinísticos, mas não os
abordarei aqui.

5.2 A experiência de Wolfram


O AC na seção anterior tinha apenas uma célula, então podemos pensar nele
como sendo zero-dimensional. Não foi algo muito interessante. No restante
deste capítulo, exploramos ACs unidimensionais (1-D), que acabam sendo sur-
preendentemente interessantes.

Dizer que um AC tem dimensões significa dizer que as células estão dispostas
em um espaço contíguo, de modo que algumas delas são consideradas “vizi-
nhas”. Em uma dimensão, existem três configurações naturais:

Sequência Finita: Um número finito de células dispostas em uma linha. To-


das as células, exceto a primeira e a última, têm duas vizinhas.
5.2 A experiência de Wolfram 73

Anel: Um número finito de células dispostas em um anel. Todas as células


têm duas vizinhas.
Sequência infinita: Um número infinito de células organizadas em uma li-
nha.
As regras que determinam como o sistema evolui no tempo são baseadas na
noção de “vizinhança”, que é o conjunto de células que determina o próximo
estado de uma dada célula.
No início dos anos 80, Stephen Wolfram publicou uma série de artigos apre-
sentando um estudo sistemático de ACs 1-D. Ele identificou quatro categorias
gerais de comportamento, cada uma mais interessante que a outra.
Os experimentos de Wolfram usam uma vizinhança de três células: a própria
célula e suas vizinhas da esquerda e da direita.
Nesses experimentos, as células têm dois estados, denotados como 0 e 1, de
modo que as regras podem ser resumidas por uma tabela que mapeia o estado
da vizinhança (uma tupla de 3 estados) ao próximo estado da célula central.
A tabela a seguir mostra um exemplo:
anterior 111 110 101 100 011 010 001 000
próximo 0 0 1 1 0 0 1 0

A primeira linha mostra os oito estados em que uma vizinhança pode estar.
A segunda linha mostra o estado da célula central durante o próximo passo.
Como uma codificação concisa desta tabela, Wolfram sugeriu a leitura da linha
inferior como um número binário. Como 00110010 em binário é 50 em decimal,
Wolfram chama isso de AC “Regra 50”.
A Figura 5.1 mostra o efeito da Regra 50 ao longo de 10 passos. A primeira
linha mostra o estado do sistema durante o primeiro passo; começa com uma
célula “ligada” e o resto “desligadas”. A segunda linha mostra o estado do
sistema durante o próximo passo e assim por diante.
A forma triangular na figura é típica desses ACs; é uma consequência da forma
da vizinhança. Em um passo, cada célula influencia o estado de uma vizinha
em qualquer direção. Durante o próximo passo, essa influência pode propagar
mais uma célula em cada direção. Assim, cada célula no passado tem um
“triângulo de influência” que inclui todas as células que podem ser afetadas
por ela.
74 Capítulo 5 Autômatos Celulares

Figura 5.1: Regra 50 depois de 10 passos.

Figura 5.2: Regra 18 depois de 64 passos.

5.3 Classificação de ACs


Quantos ACs diferentes existem?

Como cada célula está ligada ou desligada, podemos especificar o estado de


uma célula com um único bit. Em uma vizinhança com três células, existem
8 configurações possíveis. Então existem 8 entradas nas tabelas de regras.
E como cada entrada contém um único bit, podemos especificar uma tabela
usando 8 bits. Com 8 bits, podemos especificar 256 regras diferentes.

Uma das primeiras experiências de Wolfram com ACs foi testar todas as 256
possibilidades e tentar classificá-las.

Examinando os resultados visualmente, ele propôs que os comportamentos dos


ACs podem ser agrupados em quatro classes. A Classe 1 contém os ACs mais
simples (e menos interessantes), aqueles que evoluem de praticamente qualquer
condição inicial para o mesmo padrão uniforme. Como um exemplo trivial, a
Regra 0 sempre gera um padrão vazio depois de um passo.
5.4 Aleatoriedade 75

Figura 5.3: Regra 30 depois de 100 passos.

A Regra 50 é um exemplo da Classe 2. Ela gera um padrão simples com


estrutura aninhada. Ou seja, o padrão contém muitas versões menores de si
mesmo. A Regra 18 torna a estrutura aninhada ainda mais clara. A Figura 5.2
mostra como ela fica depois de 64 passos.

Esse padrão se assemelha ao Triângulo de Sierpiński , sobre o qual você pode ler
a respeito em https://pt.wikipedia.org/wiki/Triângulo_de_Sierpinski2 .

Alguns ACs de Classe 2 geram padrões complexos e bonitos, mas em compa-


ração com as Classes 3 e 4 eles são relativamente simples.

5.4 Aleatoriedade
A Classe 3 contém ACs que geram aleatoriedade. A Regra 30 é um exemplo.
A Figura 5.3 mostra o resultado depois de 100 passos.

Ao longo do lado esquerdo há um padrão aparente e do lado direito lado


existem triângulos em vários tamanhos, mas o centro parece bastante aleatório.
De fato, se você pegar a coluna central e tratá-la como uma sequência de bits, é
difícil distinguir de uma sequência verdadeiramente aleatória. Ela passa muitos
2
http://en.wikipedia.org/wiki/Sierpinski_triangle
76 Capítulo 5 Autômatos Celulares

dos testes estatísticos que as pessoas usam para testar se uma sequência de
bits é aleatória.

Programas que produzem números que parecem aleatórios são chamados gera-
dores de números pseudo-aleatórios (PRNGs3 ). Eles não são considerados
verdadeiramente aleatórios porque:

• Muitos deles produzem sequências com regularidades que podem ser de-
tectadas estatisticamente. Por exemplo, a implementação original de
rand na biblioteca C usou um gerador congruente linear que produziu
sequências com correlações seriadas facilmente detectáveis.

• Qualquer PRNG que usa uma quantidade finita de estados (ou seja,
armazenamento) acabará por se repetir. Uma das características de um
gerador é o período desta repetição.

• O processo subjacente é fundamentalmente determinístico, diferente-


mente de alguns processos físicos, como decaimento radioativo e ruído
térmico, que são considerados fundamentalmente aleatórios.

PRNGs modernos produzem sequências que são estatisticamente indistinguí-


veis das aleatórias, e eles podem ser implementados com períodos tão longos
que o universo entrará em colapso antes que eles se repitam. A existência
desses geradores levanta a questão de saber se há qualquer diferença real en-
tre uma sequência pseudo-aleatória de boa qualidade e uma sequência gerada
por um processo “verdadeiramente” aleatório. Em A New Kind of Science,
Wolfram argumenta que não existe (páginas 315–326).

5.5 Determinismo
A existência de ACs de Classe 3 é surpreendente. Para explicar o quão sur-
preendente, deixe-me começar com o determinismo filosófico (veja https:
//pt.wikipedia.org/wiki/Determinismo4 ). Muitas posturas filosóficas são
difíceis de definir precisamente porque elas vêm em uma variedade de sabores.
Muitas vezes acho útil defini-los com uma lista de afirmações ordenadas da
mais fraca para a mais forte:
3
N.T.: Sigla da expressão em inglês pseudo-random number generators.
4
http://en.wikipedia.org/wiki/Determinism
5.5 Determinismo 77

D1: Modelos determinísticos podem fazer previsões precisas para alguns sis-
temas físicos.

D2: Muitos sistemas físicos podem ser modelados por processos determinísti-
cos, mas alguns são intrinsecamente aleatórios.

D3: Todos os eventos são causados por eventos anteriores, mas muitos siste-
mas físicos ainda assim são fundamentalmente imprevisíveis.

D4: Todos os eventos são causados por eventos anteriores e podem (pelo me-
nos em princípio) ser previstos.

O meu objetivo ao construir essa lista é tornar D1 tão fraca que virtualmente
todos a aceitariam, D4 tão forte que quase ninguém a aceitaria, com afirmações
intermediárias que algumas pessoas aceitam.

O centro de massa da opinião mundial oscila ao longo desta lista em resposta


a desenvolvimentos históricos e descobertas científicas. Antes da revolução
científica, muitas pessoas consideravam o funcionamento do universo como
fundamentalmente imprevisível ou controlado por forças sobrenaturais. Após
os triunfos da mecânica Newtoniana, alguns otimistas passaram a acreditar
em algo como D4; por exemplo, em 1814, Pierre-Simon Laplace escreveu

Podemos considerar o estado atual do universo como o efeito de


seu passado e a causa de seu futuro. Um intelecto que em um
determinado momento conhecesse todas as forças que colocam a
natureza em movimento, e todas as posições de todos os itens dos
quais a natureza é composta, se este intelecto também fosse vasto o
suficiente para submeter esses dados à análise, envolveria em uma
única fórmula os movimentos dos maiores corpos do universo e os
do menor átomo; para tal intelecto, nada seria incerto e o futuro
tanto como o passado estariam presente diante de seus olhos.

Esse “intelecto” veio a ser chamado de “Demônio de Laplace”. Veja https://


pt.wikipedia.org/wiki/Demônio_de_Laplace5 . A palavra “demônio” nesse
contexto tem o sentido de “espírito”, sem nenhuma implicação do mal.
5
http://en.wikipedia.org/wiki/Laplace’s_demon
78 Capítulo 5 Autômatos Celulares

Figura 5.4: Regra 110 depois de 100 passos.

Descobertas nos séculos 19 e 20 gradualmente desmantelaram a esperança


de Laplace. Termodinâmica, radioatividade e mecânica quântica colocaram
desafios sucessivos a formas fortes de determinismo.

Na década de 1960, a teoria do caos mostrou que em alguns sistemas deter-


minísticos a previsão só é possível em escalas de tempo curtas, limitada pela
precisão na medição das condições iniciais.

A maioria desses sistemas é contínua no espaço (se não no tempo) e não-linear,


portanto a complexidade de seu comportamento não é totalmente surpreen-
dente. A demonstração de Wolfram de comportamento complexo em autôma-
tos celulares simples é mais surpreendente—e perturbadora, pelo menos para
uma visão de mundo determinista.

Até agora, concentrei-me nos desafios científicos ao determinismo, mas a ob-


jeção mais antiga é o conflito entre o determinismo e o livre-arbítrio humano.
A ciência da complexidade fornece uma possível resolução desse aparente con-
flito; eu voltarei a este tópico na Seção 10.4.

5.6 Espaçonaves
O comportamento dos ACs Classe 4 é ainda mais surpreendente. Vários ACs
1-D, mais notavelmente a Regra 110, são Turing-completos, o que significa
5.7 Universalidade 79

que eles podem computar qualquer função computável. Essa propriedade,


também chamada de universalidade, foi comprovada por Matthew Cook em
1998. Consulte http://en.wikipedia.org/wiki/Rule_1106 .

A Figura 5.4 mostra o resultado da Regra 110 com uma condição inicial de
uma única célula e 100 passos. Nesta escala de tempo, não é aparente que
algo de especial esteja acontecendo. Existem alguns padrões regulares, mas
também alguns detalhes que são difíceis de caracterizar.

A Figura 5.5 mostra uma imagem maior, começando com uma condição inicial
aleatória e 600 passos.

Após cerca de 100 passos, o plano de fundo se estabiliza em um padrão de


repetição simples, mas há várias estruturas persistentes que aparecem como
perturbações no plano de fundo. Algumas dessas estruturas são estáveis, então
elas aparecem como linhas verticais. Outras se transladam no espaço, apare-
cendo como diagonais com diferentes inclinações, dependendo de quantos pas-
sos elas levam para passar por uma coluna. Essas estruturas são chamadas de
espaçonaves.

As colisões entre espaçonaves produzem resultados diferentes dependendo dos


tipos de espaçonaves e da fase em que estão quando colidem. Algumas colisões
aniquilam ambas as naves; outros deixam uma nave inalterada; outros ainda
produzem uma ou mais naves de diferentes tipos.

Essas colisões são a base da computação em um AC Regra 110. Se você pensar


em espaçonaves como sinais que se propagam em fios e em colisões como portas
que computam operações lógicas como E e OU, você pode ver o que significa
para um AC executar uma computação.

5.7 Universalidade
Para entender a universalidade, temos que entender a teoria da computabili-
dade, que versa sobre modelos de computação e o que eles computam.

Um dos modelos mais gerais de computação é a máquina de Turing, um com-


putador abstrato proposto por Alan Turing em 1936. A máquina de Turing é
6
N.T.: Sem artigo correspondente na Wikipédia.
80 Capítulo 5 Autômatos Celulares

Figura 5.5: Regra 110 com condições iniciais aleatórias e 600 passos.
5.7 Universalidade 81

um AC 1-D, infinito em ambas as direções, aumentado com um cabeçote de


leitura e escrita. A qualquer momento, o cabeçote é posicionado sobre uma
única célula. Ele pode ler o estado dessa célula (normalmente há apenas dois
estados) e pode gravar um novo valor na célula.

Além disso, a máquina possui um registrador, que registra o estado da máquina


(um de um número finito de estados), e uma tabela de ação. Para cada estado
da máquina e estado da célula, a tabela especifica uma ação. Ações incluem
modificar o valor da célula sobre a qual o cabeçote está posicionado e mover
o cabeçote uma célula para a esquerda ou para a direita.

Uma máquina de Turing não é um projeto prático para um computador, mas


modela arquiteturas de computadores comuns. Para um determinado pro-
grama em execução em um computador real, é possível (pelo menos em prin-
cípio) construir uma máquina de Turing que realiza uma computação equiva-
lente.

A máquina de Turing é útil porque é possível caracterizar o conjunto de funções


que podem ser calculadas por uma máquina de Turing, que é o que Turing fez.
Funções neste conjunto são chamadas Turing-computáveis.

Dizer que uma máquina de Turing pode computar qualquer função Turing-
computável é uma tautologia: é verdadeira por definição. Mas a computabi-
lidade de Turing é mais interessante que isso.

Acontece que quase todos os modelos razoáveis de computação que alguém


tenha proposto são Turing-completos. Isto é, podem computar exatamente o
mesmo conjunto de funções que a máquina de Turing. Alguns desses modelos,
como o cálculo lambda, são muito diferentes de uma máquina de Turing, então
sua equivalência é surpreendente.

Esta observação levou à Tese de Church-Turing, que é essencialmente uma


definição do que significa ser computável. A “tese” é que a computabilidade
de Turing é a definição correta, ou pelo menos natural, de computabilidade,
porque descreve o poder de tal coleção diversificada de modelos de computação.

Um AC Regra 110 é mais um modelo de computação e notável pela sua simpli-


cidade. O fato de também ser universal dá suporte à Tese de Church-Turing.

Em A New Kind of Science, Wolfram afirma uma variação desta tese, que ele
chama de “princípio da equivalência computacional”:
82 Capítulo 5 Autômatos Celulares

Quase todos os processos que não são obviamente simples podem


ser vistos como computações de sofisticação equivalente.
Mais especificamente, o princípio da equivalência computacional
diz que os sistemas encontrados no mundo natural podem realizar
cálculos até um nível máximo (“universal”) de poder computacio-
nal, e que a maioria dos sistemas atinge de fato este nível máximo
de poder computacional. Consequentemente, a maioria dos siste-
mas é computacionalmente equivalente (veja http://mathworld.
wolfram.com/PrincipleofComputationalEquivalence.html).

Aplicando essas definições para ACs, as Classes 1 e 2 são “obviamente simples”.


Pode ser menos óbvio que a Classe 3 também seja simples, mas de certo modo
a aleatoriedade perfeita é tão simples quanto a ordem perfeita; a complexidade
acontece entre elas. Portanto, a alegação de Wolfram é que o comportamento
da Classe 4 é comum no mundo natural, e que quase todos os sistemas que o
manifestam são computacionalmente equivalentes.

5.8 Falseabilidade
Wolfram sustenta que seu princípio é uma afirmação mais forte do que a Tese de
Church-Turing porque se trata do mundo natural e não de modelos abstratos
de computação. Mas dizer que os processos naturais “podem ser vistos como
computações” me parece mais uma afirmação sobre a escolha da teoria do que
uma hipótese sobre o mundo natural.

Além disso, com qualificações como “quase” e termos indefinidos como “ob-
viamente simples”, sua hipótese pode ser não falseável. A falseabilidade é
uma ideia da filosofia da ciência, proposta por Karl Popper como uma demar-
cação entre hipóteses científicas e pseudociência. Uma hipótese é falseável se
houver um experimento, pelo menos no campo da praticidade, que contradiria
a hipótese se ela fosse falsa.

Por exemplo, a alegação de que toda a vida na terra tem origem em um


ancestral comum é falseável porque faz previsões específicas sobre semelhanças
na genética das espécies modernas (entre outras coisas). Se descobríssemos
uma nova espécie cujo DNA fosse quase inteiramente diferente da nossa, isso
5.9 Isso é um modelo de quê? 83

contradiria (ou ao menos levantaria questionamentos sobre) a teoria da origem


comum universal.

Por outro lado, “criação divina”, a alegação de que todas as espécies foram
criadas em sua forma atual por um agente sobrenatural, é não falseável por-
que não há nada que possamos observar sobre o mundo natural que possa
contradizê-la. Qualquer resultado de qualquer experimento pode ser atribuído
à vontade do criador.

Hipóteses não falseáveis podem ser atraentes porque são impossíveis de refutar.
Se o seu objetivo for nunca provarem que você está errado, você deve escolher
hipóteses que sejam tão não falseáveis quanto possível.

Mas se o seu objetivo é fazer previsões confiáveis sobre o mundo - e este é, pelo
menos, um dos objetivos da ciência - hipóteses não falseáveis são inúteis. O
problema é que elas não têm consequências (se tivessem consequências, seriam
falseáveis).

Por exemplo, se a teoria da criação divina fosse verdadeira, que bem me faria
saber disso? Não me diria nada sobre o criador, exceto que ele tem um “apego
desmedido por besouros” (atribuído a J. B. S. Haldane). Ao contrário da
teoria da origem comum, que é informativa para muitas áreas da ciência e da
bioengenharia, não seria útil para entender o mundo ou agir nele.

5.9 Isso é um modelo de quê?


Alguns autômatos celulares são principalmente artefatos matemáticos. Eles
são interessantes porque são surpreendentes, ou úteis, ou bonitos, ou porque
fornecem ferramentas para criar matemática nova (como a tese de Church-
Turing).

Mas não está claro que eles sejam modelos de sistemas físicos. E se são, são
altamente abstratos, o que significa que não são muito detalhados ou realistas.

Por exemplo, algumas espécies de caracol-do-cone produzem um padrão em


suas conchas que se assemelha aos padrões gerados por autômatos celulares
84 Capítulo 5 Autômatos Celulares

Modelo Modelo
x, y, z w, x, y

Modelo derivação
x, y Comportamento

abstração analogia

observação
Sistema Observável

Figura 5.6: Estrutura lógica de um modelo físico simples.

(veja http://en.wikipedia.org/wiki/Cone_snail7 ). Por isso, é natural su-


por que um AC seja um modelo do mecanismo que produz padrões em conchas
à medida que crescem. Mas, pelo menos inicialmente, não está claro como os
elementos do modelo (chamados células, comunicação entre vizinhas, e regras)
correspondem aos elementos de um caracol em crescimento (células reais, sinais
químicos, redes de interação de proteínas).

Para modelos físicos convencionais, ser realista é uma virtude. Se os elementos


de um modelo correspondem aos elementos de um sistema físico, há uma ana-
logia óbvia entre o modelo e o sistema. Em geral, esperamos que um modelo
mais realista faça previsões melhores e forneça explicações mais confiáveis.

Claro, isso só é verdade até certo ponto. Modelos mais detalhados são mais
difíceis de se trabalhar e geralmente menos passíveis de análise. Em algum
momento, um modelo se torna tão complexo que é mais fácil experimentar o
sistema.

No outro extremo, modelos simples podem ser atraentes exatamente porque


são simples.

Modelos simples oferecem um tipo diferente de explicação do que modelos


detalhados. Com um modelo detalhado, o argumento é mais ou menos as-
sim: “Estamos interessados no sistema físico S, então construímos um modelo
7
N.T.: Sem artigo correspondente na Wikipédia.
5.10 Implementação de ACs 85

1 2 3 4 1 2 3 4
5 6 7 8

5 6 7 8 9 10 11 12

9 10 11 12

Figura 5.7: Uma lista de listas (esquerda) e um array do Numpy (direita).

detalhado, M, e mostramos por análise e simulação que M exibe um com-


portamento, B, que é similar (qualitativamente ou quantitativamente) a uma
observação do sistema real, O. Então por que acontece O? Porque S é seme-
lhante a M e B é semelhante a O, e podemos provar que M leva a B.”

Com modelos simples, não podemos afirmar que S é semelhante a M, porque


não é. Em vez disso, o argumento é assim: “Há um conjunto de modelos
que compartilham um conjunto comum de características. Qualquer modelo
que tenha essas características exibe o comportamento B. Se fizermos uma
observação, O, que se assemelha a B, uma maneira de explicar isso é mostrar
que o sistema, S, possui o conjunto de características suficiente para produzir
B.”

Para este tipo de argumento, adicionar mais características não ajuda. Tornar
o modelo mais realista não torna o modelo mais confiável; ele apenas obscurece
a diferença entre as características essenciais que causam O e as características
incidentais específicas de S.

A Figura 5.6 mostra a estrutura lógica deste tipo de modelo. As caracterísitcas


x e y são suficientes para produzir o comportamento. Adicionar mais detalhes,
como as caracterísitcas w e z, pode tornar o modelo mais realista, mas esse
realismo não acrescenta poder explicativo.

5.10 Implementação de ACs


Para gerar as figuras neste capítulo, eu escrevi uma classe Python chamada
CA, que representa um autômato celular, e classes para desenhar os resultados.
86 Capítulo 5 Autômatos Celulares

Nas próximas seções, explico como elas funcionam.

Para armazenar o estado do AC, eu uso um array do NumPy que é uma


estrutura de dados multidimensional cujos elementos são todos do mesmo tipo.
É semelhante a uma lista aninhada, mas geralmente menor e mais rápida. A
Figura 5.7 mostra o porquê. O diagrama à esquerda mostra uma lista de listas
de inteiros; cada ponto representa uma referência, que ocupa 4 a 8 bytes. Para
acessar um dos números inteiros, você precisa seguir duas referências.

O diagrama à direita mostra um array dos mesmos inteiros. Como os elementos


são todos do mesmo tamanho, eles podem ser armazenados contiguamente
na memória. Esse arranjo economiza espaço porque não usa referências e
economiza tempo porque a localização de um elemento pode ser calculada
diretamente dos índices; não há necessidade de seguir uma série de referências.

Para explicar como meu código funciona, começarei com um AC que computa
a “paridade” das células em cada vizinhança. A paridade de um número é 0
se o número for par e 1 se for ímpar.

Primeiro eu crio um array de zeros com um único 1 no meio da primeira linha.

>>> rows = 5
>>> cols = 11
>>> ca = np.zeros((rows, cols))
>>> ca[0, 5] = 1
print(ca)
[[ 0. 0. 0. 0. 0. 1. 0. 0. 0. 0. 0.]
[ 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
[ 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
[ 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
[ 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]]

plot_ca exibe os resultados graficamente:

import matplotlib.pyplot as plt

def plot_ca(ca, rows, cols):


cmap = plt.get_cmap('Blues')
plt.imshow(array, interpolation='none', cmap=cmap)
5.11 Correlação cruzada 87

Seguindo a convenção, importo pyplot com o nome abreviado plt. imshow


trata o array como uma “imagem” e o exibe. Usando o mapa de cores 'Blues',
imshow desenha as células ligadas em azul escuro e as células desligadas em
azul claro.

Para calcular o estado do AC durante o próximo passo, podemos usar step:

def step(array, i):


rows, cols = array.shape
for j in range(1, cols):
array[i, j] = sum(array[i-1, j-1:j+2]) % 2

O parâmetro ca é o array do NumPy que representa o estado do AC. rows e


cols são as dimensões do array, e i é o índice do passo que devemos computar.
Eu uso i para indicar linhas do array, que correspondem ao tempo, e j para
indicar colunas, que correspondem ao espaço.

Dentro de step nós percorremos os elementos da linha i. Cada elemento é a


soma de três elementos da linha anterior, mod 2.

5.11 Correlação cruzada


A função step na seção anterior é simples, mas não é muito rápida. Em
geral, podemos acelerar operações como essa se substituirmos os laços for
com as operações NumPy, porque os laços for incorrem em muita sobrecarga
no interpretador Python. Nesta seção, mostrarei como podemos acelerar step
usando a função NumPy correlate.

Primeiro, em vez de usar um operador de fatiamento para selecionar uma vizi-


nhança, podemos usar multiplicação de arrays. Especificamente, nós multipli-
camos um array por uma janela que contém 1 para as células que queremos
selecionar e 0 para as demais.

Por exemplo, a janela a seguir seleciona os três primeiros elementos:

>>> window = np.zeros(cols, dtype=np.int8)


>>> window[:3] = 1
>>> print(window)
[1 1 1 0 0 0 0 0 0 0 0]
88 Capítulo 5 Autômatos Celulares

Se multiplicarmos pela última linha de array, obtemos os primeiros três ele-


mentos:

>>> print(array[4])
>>> print(window * array[4])
[0 1 0 0 0 1 0 0 0 1 0]
[0 1 0 0 0 0 0 0 0 0 0]

Agora podemos usar sum e o operador de módulo para calcular o primeiro


elemento da próxima linha:

>>> sum(window * array[4]) % 2


1

Se deslocarmos a janela para a direita, selecionamos os próximos três elementos


e assim por diante. Então podemos reescrever step assim:

def step2(array, i):


rows, cols = array.shape
window = np.zeros(cols)
window[:3] = 1
for j in range(1, cols):
array[i, j] = sum(window * array[i-1]) % 2
window = np.roll(window, 1)

roll desloca a janela para a direita (ela também leva de volta para o início,
mas isso não afeta essa função).

step2 produz os mesmos resultados que step. Ainda não é muito rápida, mas
é um passo na direção certa porque a operação que acabamos de realizar—
multiplicando por uma janela, somando o resultado, deslocando a janela e
repetindo—é usada para uma variedade de aplicações. É chamada de corre-
lação cruzada e o NumPy fornece uma função chamada correlate que a
computa.

Podemos usá-la para escrever uma versão mais rápida e simples de step:

def step3(array, i):


window = np.array([1, 1, 1])
array[i] = np.correlate(array[i-1], window, mode='same') % 2
5.12 Tabelas AC 89

Quando usamos np.correlate, a janela não precisa ter o mesmo tamanho do


array, então fazer a janela é um pouco mais simples.

O parâmetro mode determina o tamanho do resultado. Você pode ler os deta-


lhes na documentação do NumPy, mas quando o modo é 'same', o resultado
é o mesmo tamanho da entrada.

5.12 Tabelas AC
Agora há apenas mais um passo. A função que temos até agora funciona se a
regra do AC depender apenas da soma das vizinhas, mas a maioria das regras
também depende de quais vizinhos estão ligados e desligados. Por exemplo,
100 e 001 podem produzir resultados diferentes.

Podemos tornar step mais geral usando uma janela com elementos [4, 2, 1],
que interpreta a vizinhança como um número binário. Por exemplo, a vizi-
nhança 100 produz 4; 010 resulta 2, e 001 resulta 1. Então podemos pegar
esses resultados e procurá-los na tabela de regras.

Aqui está a versão mais geral de step:

def step4(array, i):


window = np.array([4, 2, 1])
corr = np.correlate(array[i-1], window, mode='same')
array[i] = table[corr]

As duas primeiras linhas são quase iguais. A última linha procura cada ele-
mento de corr em table e atribui os resultados a array[i].

Finalmente, segue a função que calcula a tabela:

def make_table(rule):
rule = np.array([rule], dtype=np.uint8)
table = np.unpackbits(rule)[::-1]
return table

O parâmetro, rule, é um inteiro entre 0 e 255. A primeira linha coloca a regra


em um array com um único elemento para que possamos usar unpackbits,
90 Capítulo 5 Autômatos Celulares

que converte o número da regra em sua representação binária. Por exemplo,


esta é a tabela da Regra 150:

>>> table = make_table(150)


>>> print(table)
[0 1 1 0 1 0 0 1]

Em thinkcomplexity.py, você encontrará definições de CA, que encapsula


o código nesta seção, e duas classes que desenham ACs, PyplotDrawer e
EPSDrawer.

5.13 Exercícios
Exercício 5.1 O código para este capítulo está no Jupyter notebook chap05.ipynb
no repositório deste livro. Abra este notebook, leia o código e execute as célu-
las. Você pode usar este notebook para trabalhar nos exercícios deste capítulo.
Minhas soluções estão em chap05soln.ipynb.

Exercício 5.2 Este exercício pede que você experimente a Regra 110 e al-
gumas de suas espaçonaves.

1. Leia a página da Wikipedia sobre a Regra 110, que descreve seu padrão
de plano de fundo e naves espaciais: https://en.wikipedia.org/wiki/
Rule_1108 .

2. Crie um AC Regra 110 com uma condição inicial que produz o padrão
de plano de fundo estável.
Observe que a classe CA fornece start_string, que permite inicializar o
estado do array usando uma cadeia de 1s e 0s.

3. Modifique a condição inicial adicionando padrões diferentes no centro


da linha e veja quais geram espaçonaves. Você pode enumerar todos os
padrões possíveis de n bits, para algum valor razoável de n. Para cada
nave espacial, você pode encontrar o período e a taxa de translação?
Qual é a maior nave espacial que você pode encontrar?

4. O que acontece quando as espaçonaves colidem?


8
N.T.: Sem artigo correspondente na Wikipédia.
5.13 Exercícios 91

Exercício 5.3 O objetivo deste exercício é implementar uma máquina de


Turing.

1. Leia sobre máquinas de Turing em https://pt.wikipedia.org/wiki/


Máquina_de_Turing9 .

2. Escreva uma classe chamada Turing que implementa uma máquina de


Turing. Para a tabela de ações, use as regras de um castor ocupado de
três estados.

3. Escreva uma classe chamada TuringDrawer que gera uma imagem que
representa o estado da fita e a posição e estado do cabeçote. Para um
exemplo de como isso se parece, consulte http://mathworld.wolfram.
com/TuringMachine.html.

Exercício 5.4 Este exercício pede que você implemente e teste vários PRNGs.
Para os testes, você precisará instalar o DieHarder, que pode ser baixado em
https://www.phy.duke.edu/~rgb/General/dieharder.php, ou que pode es-
tar disponível como um pacote para o seu sistema operacional.

1. Escreva um programa que implemente um dos geradores congruentes li-


neares descritos em https://pt.wikipedia.org/wiki/Geradores_congruentes_
lineares10 . Teste-o usando o DieHarder.

2. Leia a documentação do módulo random do Python. O que o PRNG


usa? Teste-o.

3. Implemente um AC Regra 30 com algumas centenas de células, execute-


a por tantos passos quanto puder, em um período de tempo razoável, e
imprima a coluna central como uma sequência de bits. Teste-o.

Exercício 5.5 A falseabilidade é uma ideia atraente e útil, mas entre os


filósofos da ciência não é geralmente aceita como uma solução para o problema
da demarcação, como Popper afirmou.

Leia https://pt.wikipedia.org/wiki/Falseabilidade11 e responda às se-


guintes perguntas.
9
http://en.wikipedia.org/wiki/Turing_machine
10
http://en.wikipedia.org/wiki/Linear_congruential_generator
11
http://en.wikipedia.org/wiki/Falsifiability
92 Capítulo 5 Autômatos Celulares

1. Qual é o problema de demarcação?

2. Como, segundo Popper, a falseabilidade resolve o problema de demarca-


ção?

3. Dê um exemplo de duas teorias, uma considerada científica e outra con-


siderada não científica, que são distinguidas com sucesso pelo critério de
falseabilidade.

4. Você pode resumir uma ou mais das objeções que filósofos e historiadores
da ciência levantaram à alegação de Popper?

5. Você tem a sensação de que os filósofos praticantes pensam muito sobre


o trabalho de Popper?
Capítulo 6

Jogo da Vida

Neste capítulo, consideramos autômatos celulares bidimensionais, especial-


mente o Jogo da Vida (do inglês Game of Life - GoL) de John Conway. Como
alguns dos ACs 1-D do capítulo anterior, o GoL segue regras simples e pro-
duz um comportamento surpreendentemente complicado. Tal como a Regra
110 de Wolfram, o GoL é universal; isto é, pode computar qualquer função
computável, pelo menos em teoria.

O comportamento complexo no GoL suscita questões na filosofia da ciência,


particularmente relacionadas ao realismo científico e ao instrumentalismo. Eu
discuto estas questões e sugiro leitura adicional.

No final do capítulo, demonstro maneiras de implementar o GoL de maneira


eficiente em Python.

O código para este capítulo está em chap06.ipynb no repositório deste livro.


Para mais informações sobre como trabalhar com o código, ver a Seção 0.2.

6.1 Jogo da Vida de Conway


Um dos primeiros autômatos celulares a ser estudado, e provavelmente o mais
popular de todos os tempos, foi um AC 2-D chamado de “O Jogo da Vida”,
abreviado GoL1 . Foi desenvolvido por John H. Conway e popularizado em
1
N.T.: Do inglês, The Game of Life.
94 Capítulo 6 Jogo da Vida

1970 na coluna de Martin Gardner na Scientific American. Veja https://pt.


wikipedia.org/wiki/Jogo_da_vida2 .

As células no GoL são organizadas em uma grade 2-D, infinita em ambas


as direções ou em um envoltório fechado. Uma grade na forma de um envol-
tório fechado é chamada de toróide porque é topograficamente equivalente
à superfície de uma roscs. Veja https://pt.wikipedia.org/wiki/Toro_
(topologia)3 .

Cada célula tem dois estados—viva ou morta—e oito vizinhas—norte, sul,


leste, oeste e as quatro diagonais. Esse conjunto de vizinhas é, às vezes,
chamado de “vizinhança de Moore”.

Como os ACs 1-D nos capítulos anteriores, o Jogo da Vida evolui ao longo do
tempo de acordo com regras, que são como simples leis da física.

No GoL, o próximo estado de cada célula depende do estado atual e do número


de vizinhas vivas. Se uma célula estiver viva, ela sobreviverá se tiver 2 ou 3
vizinhas e morrerá caso contrário. Se uma célula está morta, ela permanece
morta a menos que tenha exatamente 3 vizinhas.

A tabela a seguir resume as regras:

Estado Número de Próximo


atual vizinhas estado
viva 2–3 viva
viva 0–1, 4–8 morta
morta 3 viva
morta 0–2, 4–8 morta

Esse comportamento é vagamente análogo ao crescimento real das células: cé-


lulas isoladas ou em excesso morrem; em densidades moderadas eles florescem.

O GoL é popular porque:


2
http://en.wikipedia.org/wiki/Conway_Game_of_Life
3
http://en.wikipedia.org/wiki/Torus
6.2 Padrões de vida 95

Figura 6.1: Padrão estável chamado de colmeia.

• Há condições iniciais simples que geram um comportamento surpreen-


dentemente complexo.

• Há muitos padrões estáveis interessantes: alguns oscilam (com vários


períodos) e alguns se movem como as naves espaciais no AC Regra 110
de Wolfram.

• Tal como a Regra 110, o GoL é Turing-completo.

• Outro fator que gerou interesse foi a conjectura de Conway—de que não
existe condição inicial que produza crescimento ilimitado no número de
células vivas—e a recompensa de $50 que ele ofereceu a qualquer um que
pudesse provar ou refutar isso.

• Finalmente, a crescente disponibilidade de computadores tornou possível


automatizar a computação e exibir os resultados graficamente.

6.2 Padrões de vida


Se você executar o GoL a partir de um estado inicial aleatório, vários padrões
estáveis provavelmente aparecerão. Com o tempo, as pessoas identificaram
esses padrões e deram-lhes nomes.

Por exemplo, a Figura 6.1 mostra um padrão estável chamado “colmeia”4 .


Todas as células da colmeia têm 2 ou 3 vizinhas vivas, então todas sobrevivem,
4
N.T.: Do inglês, beehive.
96 Capítulo 6 Jogo da Vida

Figura 6.2: Um oscilador chamado de sapo.

Figura 6.3: Uma espaçonave chamada de planador.


6.3 Conjectura de Conway 97

e nenhuma das células mortas adjacentes à colmeia tem 3 vizinhas vivas, então
não nascem novas células.

Outros padrões “oscilam”; isto é, eles mudam com o tempo, mas eventualmente
retornam à sua configuração inicial (desde que eles não colidam com outro
padrão). Por exemplo, a Figura 6.2 mostra um padrão chamado de “sapo”5 ,
um oscilador que alterna entre dois estados. O “período” deste oscilador é 2.

Finalmente, alguns padrões oscilam e retornam à configuração inicial, mas se


deslocam no espaço. Como esses padrões parecem se mover, eles são chamados
de “espaçonaves”.

A Figura 6.3 mostra uma espaçonave chamada de “planador”6 . Após um


período de 4 passos, o planador volta à configuração inicial, deslocado uma
unidade para baixo e para a direita.

Dependendo da orientação inicial, os planadores podem se mover ao longo de


qualquer uma das quatro diagonais. Existem outras espaçonaves que movem-
se horizontalmente e verticalmente.

As pessoas gastaram quantidades embaraçosas de tempo encontrando e no-


meando esses padrões. Se você pesquisar na internet, você encontrará muitas
coleções.

6.3 Conjectura de Conway


A partir da maioria das condições iniciais, o GoL atinge rapidamente um estado
estável em que o número de células vivas é quase constante (possivelmente com
alguma oscilação).

Mas existem algumas condições iniciais simples que levam muito tempo para
se estabilizar e resultam em um número surpreendente de células vivas. Esses
padrões são chamados de “Matusaléns” porque são longevos.

Um dos mais simples é o r-pentomino, que tem apenas cinco células, aproxi-
madamente na forma da letra “r”. A Figura 6.4 mostra a configuração inicial
do r-pentomino e a configuração final após 1103 passos.
5
N.T.: Do inglês, toad.
6
N.T.: Do inglês, glider.
98 Capítulo 6 Jogo da Vida

Figura 6.4: Configurações inicial e final do r-pentomino.

Figura 6.5: Arma de planadores de Gosper, que produz uma sequência de


planadores.

Esta configuração é “final” no sentido de que todos os padrões restantes são


estáveis, osciladores ou planadores que nunca colidirão com outro padrão. No
total, o r-pentomino produz 6 planadores, 8 blocos7 , 4 pisca-piscas8 , 4 colmeias,
1 barco9 , 1 navio10 e 1 pão11 .

r-pentomino

A existência de padrões longevos levou Conway a se perguntar se existem


padrões iniciais que nunca se estabilizam. Ele conjecturou que não havia, mas
acabou por descrever dois tipos de padrão que provariam que ele estava errado,

N.T.:
7
Do inglês, block.
N.T.:
8
Do inglês, blinker.
9
N.T.: Do inglês, boat.
10
N.T.: Do inglês, ship.
11
N.T.: Do inglês, loaf.
6.4 Realismo 99

uma “arma”12 e um “trem de bombardeio”13 . Uma arma é um padrão estável


que periodicamente produz uma espaçonave—como o fluxo de espaçonaves se
move para fora da fonte, o número de células crescem indefinidamente. Um
trem de bombardeio é um padrão de translação que deixa células vivas em seu
rastro.

Acontece que esses dois padrões existem. Uma equipe liderada por Bill Gos-
per descobriu o primeiro, uma arma de planador agora chamada de Arma de
Gosper, mostrada na Figura 6.5. Gosper também descobriu o primeiro trem
de bombardeio.

Existem muitos padrões de ambos os tipos, mas eles não são fáceis de projetar
ou de encontrar. Isso não é uma coincidência. Conway escolheu as regras do
GoL para que sua conjectura não fosse obviamente verdadeira ou falsa. De
todas as regras possíveis para uma AC 2-D, a maioria resulta em comporta-
mentos simples: a maioria das condições iniciais se estabilizam rapidamente
ou têm crescimento ilimitado. Ao evitar ACs desinteressantes, Conway tam-
bém estava evitando o comportamento Classe 1 e Classe 2 de Wolfram, e
provavelmente também a Classe 3.

Se acreditamos no Princípio de Equivalência Computacional de Wolfram, nós


esperamos que o GoL esteja na Classe 4. E está. O Jogo da Vida foi pro-
vado Turing-completo em 1982 (e novamente, independentemente, em 1983).
Desde então, várias pessoas construíram padrões de GoL que implementam
uma máquina de Turing ou alguma outra máquina Turing-completa.

6.4 Realismo
Padrões estáveis no GoL não são difíceis de serem notados, especialmente
os que se movem. É natural pensar neles como entidades persistentes, mas
lembre-se de que um AC é feito de células. Não existe um sapo ou um pão.
Planadores e outras espaçonaves são ainda menos reais, porque nem sequer
são compostas pelas mesmas células ao longo do tempo. Assim, esses padrões
são como constelações de estrelas. Nós os percebemos porque somos bons em
ver padrões, ou porque temos imaginação ativa, mas eles não são reais.
12
N.T.: Do inglês, gun.
13
N.T.: Do inglês, puffer train.
100 Capítulo 6 Jogo da Vida

Certo?

Bem, não tão rápido. Muitas entidades que consideramos “reais” também são
padrões persistentes de entidades em menor escala. Os furacões são apenas
padrões de fluxo de ar, mas nós lhes damos nomes de pessoas. E as pessoas,
como planadores, não são compostas das mesmas células ao longo do tempo.
Mas mesmo que você substitua todas as células do seu corpo, o consideramos
a mesma pessoa.

Esta não é uma observação nova—cerca de 2500 anos atrás, Heráclito salientou
que você não pode pisar no mesmo rio duas vezes—mas as entidades que
aparecem no Jogo da Vida são um caso útil para se pensar em realismo
filosófico.

No contexto da filosofia, o realismo é a visão de que entidades no mundo exis-


tem independentemente da percepção e concepção humanas. Por “percepção”
quero dizer a informação que obtemos de nossos sentidos, e por “concepção”
quero dizer o modelo mental que formamos do mundo. Por exemplo, nossos
sistemas de visão percebem algo como uma projeção em 2-D de uma cena, e
nossos cérebros usam essa imagem para construir um modelo 3-D dos objetos
na cena.

Realismo científico refere-se às teorias científicas e às entidades que eles


postulam. Uma teoria postula uma entidade se ela é expressa em termos das
propriedades e do comportamento da entidade. Por exemplo, as teorias sobre
eletromagnetismo são expressas em termos de campos elétricos e magnéticos.
Algumas teorias sobre economia são expressas em termos de oferta, demanda
e forças de mercado. E as teorias sobre biologia são expressas em termos de
genes.

Mas essas entidades são reais? Ou seja, eles existem no mundo independente
de nós e de nossas teorias?

Mais uma vez, acho útil afirmar posições filosóficas com forças diferentes. Aqui
estão quatro afirmações do realismo científico em ordem crescente de força:

SR1: As teorias científicas são verdadeiras ou falsas, na medida em que se


aproximam da realidade, mas nenhuma teoria é exatamente verdadeira.
Algumas entidades postuladas podem ser reais, mas não há nenhuma
maneira de dizer quais.
6.5 Instrumentalismo 101

SR2: À medida que a ciência avança, nossas teorias se tornam aproximações


melhores da realidade. Pelo menos algumas entidades postuladas são
tidas como reais.

SR3: Algumas teorias são exatamente verdadeiras; outras são aproximada-


mente verdadeiras. Entidades postuladas por teorias verdadeiras e algu-
mas entidades em teorias aproximadas são reais.

SR4: Uma teoria é verdadeira se ela descreve a realidade corretamente, e


falsa caso contrário. As entidades postuladas pelas teorias verdadeiras
são reais; outras não são.

SR4 é tão forte que provavelmente é insustentável; por um critério tão rigoroso,
quase todas as teorias atuais são tidas como falsas. A maioria dos realistas
aceitaria algo no espaço entre SR1 e SR3.

6.5 Instrumentalismo
Mas a SR1 é tão fraca que beira o instrumentalismo, que é a visão de que
não podemos dizer se uma teoria é verdadeira ou falsa porque não podemos
saber se uma teoria corresponde à realidade. Teorias são instrumentos que
usamos para nossos propósitos; uma teoria é útil, ou não, na medida em que
é adequada ao seu propósito.

Para ver se você está confortável com o instrumentalismo, considere as seguin-


tes afirmações:

“Entidades no Jogo da Vida não são reais. São apenas padrões de


células aos quais as pessoas deram nomes fofos.”

“Um furacão é apenas um padrão de fluxo de ar, mas é uma descri-


ção útil porque nos permite fazer previsões e nos comunicar sobre
o tempo.”

“Entidades freudianas como o Id e o Superego não são reais, mas


são ferramentas úteis para pensar e se comunicar sobre psicologia
(ou pelo menos algumas pessoas pensam assim).”
102 Capítulo 6 Jogo da Vida

“Os campos elétricos e magnéticos são entidades postuladas em


nossas melhores teorias do eletromagnetismo, mas não são reais.
Poderíamos construir outras teorias, sem postular campos, que se-
riam igualmente úteis.”

“Muitas das coisas no mundo que identificamos como objetos são


coleções arbitrárias como constelações. Por exemplo, um cogumelo
é apenas o corpo frutífero de um fungo, a maioria dos quais cresce
no subsolo como uma rede de células pouco contíguas. Nós nos
concentramos em cogumelos por razões práticas, como visibilidade
e comestibilidade.”

“Alguns objetos têm limites nítidos, mas muitos são confusos. Por
exemplo, quais moléculas fazem parte do seu corpo: ar nos pul-
mões? Comida no seu estômago? Nutrientes no seu sangue? Nu-
trientes em uma célula? Água em uma célula? Partes estruturais
de uma célula? Cabelo? Pele morta? Sujeira? Bactérias na sua
pele? Bactérias no seu intestino? Mitocôndria? Quantas dessas
moléculas você inclui quando se pesa? Conceber o mundo em ter-
mos de objetos discretos é útil, mas as entidades que identificamos
não são reais.”

Dê a si mesmo um ponto para cada afirmação com a qual você concorda. Se


você marcar 4 ou mais, você pode ser um instrumentalista!

Se você está mais confortável com algumas dessas afirmações do que com
outras, pergunte-se por quê. Quais são as diferenças nesses cenários que influ-
enciam sua reação? Você pode fazer uma distinção de princípios entre elas?

Para mais informações sobre instrumentalismo, veja https://pt.wikipedia.


org/wiki/Instrumentalismo14 .

6.6 Implementando Vida


Os exercícios no final deste capítulo pedem que você experimente e modifique o
Jogo da Vida e implemente outros autômatos celulares 2-D. Esta seção explica
14
http://en.wikipedia.org/wiki/Instrumentalism
6.6 Implementando Vida 103

minha implementação do GoL, a qual você pode usar como ponto de partida
para suas experiências.

Para representar o estado das células, eu uso um array NumPy com o tipo
uint8, que é um inteiro sem sinal de 8 bits. Como exemplo, a linha a seguir
cria um array de 10 por 10 inicializado com valores aleatórios de 0 e 1.

a = np.random.randint(2, size=(10, 10)).astype(np.uint8)

Existem algumas maneiras de calcular as regras do GoL. O mais simples é usar


laços for para percorrer as linhas e colunas do array:

b = np.zeros_like(a)
rows, cols = a.shape
for i in range(1, rows-1):
for j in range(1, cols-1):
state = a[i, j]
neighbors = a[i-1:i+2, j-1:j+2]
k = np.sum(neighbors) - state
if state:
if k==2 or k==3:
b[i, j] = 1
else:
if k == 3:
b[i, j] = 1

Inicialmente, b é uma matriz de zeros com o mesmo tamanho que a. A cada


iteração do laço, state é a condição da célula central e neighbors é a vizi-
nhança 3x3. k é o número de vizinhas vivas (não incluindo a célula central).
As instruções if aninhadas avaliam as regras do GoL e ativam as células b
conforme a condição.

Essa implementação é uma tradução direta das regras, mas é detalhada e lenta.
Podemos fazer melhor usando correlação cruzada, como vimos na Seção 5.11.
Lá, usamos np.correlate para calcular uma correlação 1-D. Agora, para re-
alizar a correlação 2-D, usaremos correlate2d de scipy.signal, um módulo
SciPy que fornece funções relacionadas a processamento de sinais:
104 Capítulo 6 Jogo da Vida

from scipy.signal import correlate2d

kernel = np.array([[1, 1, 1],


[1, 0, 1],
[1, 1, 1]])

c = correlate2d(a, kernel, mode='same')

O que chamávamos de “janela” no contexto da correlação 1-D é chamado de


“kernel” no contexto da correlação 2-D, mas a idéia é a mesma: correlate2d
multiplica o kernel e o array para selecionar uma vizinhança e, em seguida,
adiciona o resultado. Este kernel seleciona os 8 vizinhas que cercam a célula
central.

correlate2d aplica o kernel a cada localização do array. Com mode = 'same',


o resultado tem o mesmo tamanho de a.

Agora podemos usar operadores lógicos para calcular as regras:

b = (c==3) | (c==2) & a


b = b.astype(np.uint8)

A primeira linha calcula um array booleano com True onde deveria haver
uma célula viva e False nos outros lugares. Então astype converte o array
booleano em uma matriz de inteiros.

Esta versão é mais rápida e provavelmente boa o suficiente, mas podemos


simplificá-la ligeiramente modificando o kernel:

kernel = np.array([[1, 1, 1],


[1,10, 1],
[1, 1, 1]])

c = correlate2d(a, kernel, mode='same')


b = (c==3) | (c==12) | (c==13)
b = b.astype(np.uint8)

Esta versão do kernel inclui a célula central e lhe dá um peso de 10. Se a


célula central for 0, o resultado será entre 0 e 8; se a célula central for 1,
o resultado será entre 10 e 18. Usando esse kernel, podemos simplificar as
operações lógicas, selecionando apenas células com os valores 3, 12 e 13.
6.7 Exercícios 105

Isso pode não parecer uma grande melhoria, mas permite mais uma simpli-
ficação: com esse kernel, podemos usar uma tabela para procurar valores de
célula, como fizemos na Seção 5.12.

table = np.zeros(20, dtype=np.uint8)


table[[3, 12, 13]] = 1
c = correlate2d(a, kernel, mode='same')
b = table[c]

table tem zeros em todos os lugares, exceto nos locais 3, 12 e 13. Quando
usamos c como um índice em table, o NumPy faz uma pesquisa elementar;
isto é, ele pega cada valor de c, pesquisa em table e coloca o resultado em b.

Esta versão é mais rápida e concisa do que as outras. O único inconveniente


é que é preciso mais explicação.

Life.py, incluído no repositório deste livro, fornece uma classe Life que en-
capsula essa implementação das regras. Se você executar Life.py, deverá ver
uma animação de um “trem de bombardeio”, uma espaçonave que deixa um
rastro de detritos por onde passa.

6.7 Exercícios
Exercício 6.1 O código deste capítulo está no Jupyter notebook chap06.ipynb
no repositório deste livro. Abra este notebook, leia o código e execute as célu-
las. Você pode usar este notebook para trabalhar nos exercícios deste capítulo.
Minhas soluções estão em chap06soln.ipynb.

Exercício 6.2 Inicie o GoL em um estado aleatório e execute-o até estabi-


lizar. Quais padrões estáveis você consegue identificar?

Exercício 6.3 Muitos dos padrões que receberam nomes estão disponíveis
em formatos portáveis de arquivo. Modifique Life.py para analisar um desses
formatos e inicialize a grade.

Exercício 6.4 Um dos padrões pequenos mais longevos é o dos “coelhos”15 ,


que começa com 9 células vivas e leva 17331 passos para se estabilizar. Você
15
N.T.: Do inglês, rabbits.
106 Capítulo 6 Jogo da Vida

pode obter a configuração inicial em vários formatos em http://www.conwaylife.


com/wiki/Rabbits. Carregue esta configuração e execute-a.

Exercício 6.5 Na minha implementação, a classe Life é baseada em uma


classe pai chamada Cell2D, e LifeViewer é baseada em Cell2DViewer. Você
pode usar essas classes base para implementar outros autômatos celulares 2-D.

Por exemplo, uma variação do GoL, chamada “Highlife”, tem as mesmas regras
do GoL, mais uma regra adicional: uma célula morta com 6 vizinhas vivas volta
a viver.

Escreva uma classe chamada Highlife que herda de Cell2D e implemente esta
versão das regras. Escreva também uma classe chamada HighlifeViewer que
herda de Cell2DViewer e tente diferentes maneiras de visualizar os resultados.
Como um simples exemplo, use um mapa de cores diferente.

Um dos padrões mais interessantes no Highlife é o replicador. Use add_cells


para inicializar o Highlife com um replicador e veja o que ele faz.

Exercício 6.6 Se você generalizar a máquina de Turing para duas dimensões,


ou adicionar um cabeçote de leitura-escrita a um AC 2-D, o resultado é um
autômato celular chamado Turmite. O nome vem de cupim16 por causa da
maneira como o cabeçote de leitura-escrita se move, mas escrito errado como
uma homenagem a Alan Turing.

A turmite mais famosa é a formiga de Langton, descoberta por Chris Langton


em 1986. Veja https://pt.wikipedia.org/wiki/Formiga_de_Langton17 .

A formiga é uma cabeçote de leitura-escrita com quatro estados, que você pode
imaginar como sendo voltados para o norte, sul, leste ou oeste. As células têm
dois estados, preto e branco.

As regras são simples. Durante cada passo, a formiga verifica a cor da célula
em que está. Se for preta, a formiga vira para a direita, muda a célula para
branca e avança um espaço. Se a célula é branca, a formiga vira à esquerda,
muda a célula para preta e avança.

Dado um mundo simples, um conjunto simples de regras e apenas uma parte


móvel, você pode esperar ver um comportamento simples—mas você já deve
16
N.T.: Em inglês, termite.
17
http://en.wikipedia.org/wiki/Langton_ant
6.7 Exercícios 107

saber que não é bem assim. Começando com todos as células brancas, a
formiga de Langton move-se em um padrão aparentemente aleatório por mais
de 10000 passos antes de entrar em um ciclo com um período de 104 passos.
Depois de cada ciclo, a formiga é transladada na diagonal, por isso deixa uma
trilha chamada “auto-estrada”.

Escreva uma implementação da Formiga de Langton.


Capítulo 9

Modelos baseados em agentes

Os modelos que vimos até agora podem ser caracterizados como “baseados
em regras” no sentido de que eles envolvem sistemas governados por regras
simples. Neste e nos capítulos seguintes exploramos modelos baseados em
agentes.

Os modelos baseados em agentes incluem agentes com os quais pretende-se


modelar pessoas e outras entidades que coletam informações sobre o mundo,
tomam decisões e executam ações.

Os agentes geralmente estão situados no ambiente ou em uma rede e interagem


uns com os outros localmente. Eles geralmente têm informações incompletas
e imperfeitas sobre o mundo.

Muitas vezes existem diferenças entre os agentes, ao contrário dos modelos


anteriores, onde todos os componentes são idênticos. Os modelos baseados em
agentes geralmente incluem aleatoriedade, seja entre os agentes ou no mundo.

Desde a década de 1970, a modelagem baseada em agentes tornou-se uma


ferramenta importante em economia e outras ciências sociais, e em algumas
ciências naturais.

Os modelos baseados em agentes são úteis para modelar a dinâmica de siste-


mas que não estão em equilíbrio (embora também sejam usados para estudar
o equilíbrio). São particularmente úteis para entender as relações entre as
decisões individuais e o comportamento global do sistema.
148 Capítulo 9 Modelos baseados em agentes

O código para este capítulo está em chap09.ipynb no repositório deste livro.


Para mais informações sobre como trabalhar com o código, ver a Seção 0.2.

9.1 Modelo de Schelling


Em 1971, Thomas Schelling publicou “Dynamic Models of Segregation”1 que
propõe um modelo simples de segregação racial. O modelo de Schelling do
mundo é uma grade; cada célula representa uma casa. As casas são ocupadas
por dois tipos de agentes, identificados em vermelho e azul, em quantidades
aproximadamente iguais. Cerca de 10% das casas estão vazias.

A todo momento, um agente pode estar feliz ou infeliz, dependendo dos outros
agentes da vizinhança, onde a “vizinhança” de cada casa é o conjunto de oito
células adjacentes. Em uma versão do modelo, os agentes estão felizes se têm
pelo menos dois vizinhos como eles e infelizes se tiverem um ou zero.

A simulação prossegue escolhendo um agente aleatoriamente e verificando se


ele está feliz. Se estiver, nada acontece; se não estiver, o agente escolhe alea-
toriamente uma das células desocupadas e se muda.

Você pode não se surpreender ao ouvir que este modelo leva a alguma segre-
gação, mas você pode se surpreender com o grau. Rapidamente, grupos de
agentes similares aparecem. Os agrupamentos crescem e coalescem ao longo
do tempo até que haja um pequeno número de grandes agrupamentos e a
maioria dos agentes viva em vizinhanças homogêneas.

Se você não conhecesse o processo e só visse o resultado, você poderia assumir


que os agentes eram racistas,ainda que, na verdade, todos eles poderiam ser
perfeitamente felizes em uma vizinhança mista. Uma vez que eles preferem não
ser minoria, eles só podem ser considerados xenófobos, na pior das hipóteses.
Naturalmente, esses agentes são uma simplificação selvagem de pessoas reais,
portanto, pode não ser apropriado aplicar essas descrições.

O racismo é um problema humano complexo. É difícil imaginar que um mo-


delo tão simples possa esclarecer isso. Mas, na verdade, ele fornece um forte
argumento sobre a relação entre um sistema e suas partes: se você observar a
1
N.T.: Do inglês, Modelos Dinâmicos de Segregação.
9.2 Implementação do modelo de Schelling 149

segregação em uma cidade real, você não pode concluir que o racismo indivi-
dual é a causa imediata, ou mesmo que as pessoas na cidade são racistas.
É claro que temos que ter em mente as limitações desse argumento: o modelo
de Schelling demonstra uma possível causa de segregação, mas nada diz sobre
as causas reais.

9.2 Implementação do modelo de Schelling


Para implementar o modelo de Schelling, escrevi outra classe que herda de
Cell2D:
class Schelling(Cell2D):

def __init__(self, n, m=None, p=0.5):


self.p = p
m = n if m is None else m
choices = [0, 1, 2]
probs = [0.1, 0.45, 0.45]
self.array = np.random.choice(choices, (n, m), p=probs)
Os parâmetros n e m são as dimensões da grade, e p é o limite na fração de
vizinhos semelhantes. Por exemplo, se p = 0.5, um agente ficará infeliz se
menos de 50% de seus vizinhos forem da mesma cor.
array é um array NumPy em que cada célula é 0 se vazia, 1 se ocupada por
um agente vermelho e 2 se ocupada por um agente azul. Inicialmente, 10%
das células estão vazias, 45% vermelhas e 45% azuis.
A função step para o modelo de Schelling é substancialmente mais complicada
que as funções step anteriores. Se você não estiver interessado nos detalhes,
pule para a próxima seção. Mas se você continuar a partir daqui, você pode
pegar algumas dicas do NumPy.
Primeiro, farei arrays lógicos indicando quais células são vermelhas, azuis e
ocupadas:
a = self.array
red = a==1
blue = a==2
occupied = a!=0
150 Capítulo 9 Modelos baseados em agentes

Usarei np.correlate2d para contar, para cada célula, o número de células


vizinhas que são vermelhas e o número de células que estão ocupadas.

options = dict(mode='same', boundary='wrap')

kernel = np.array([[1, 1, 1],


[1, 0, 1],
[1, 1, 1]], dtype=np.int8)

num_red = correlate2d(red, kernel, **options)


num_neighbors = correlate2d(occupied, kernel, **options)

Agora, para cada célula, podemos calcular a fração de vizinhos que são ver-
melhos e a fração de vizinhos que têm a mesma cor:

frac_red = num_red / num_neighbors


frac_blue = 1 - frac_red
frac_same = np.where(red, frac_red, frac_blue)

frac_red é apenas a proporção de num_red e num_neighbors, e frac_blue é


o complemento de frac_red.

frac_same é um pouco mais complicado. A função np.where é como uma


expressão if elemento a elemento. O primeiro parâmetro é uma condição que
seleciona elementos do segundo ou terceiro parâmetro.

Nesse caso, onde quer que red seja True, frac_same obtém o elemento cor-
respondente de frac_red. Onde red é False, frac_same obtém o elemento
correspondente de frac_blue.

Agora podemos identificar as localizações dos agentes infelizes:

unhappy_locs = locs_where(occupied & (frac_same < self.p))

O resultado, unhappy_locs, é um array NumPy em que cada linha é a co-


ordenada de uma célula ocupada na qual frac_same está abaixo do limite
p.

locs_where é uma função wrapper para np.nonzero:

def locs_where(condition):
return np.transpose(np.nonzero(condition))
9.3 Segregação 151

np.nonzero recebe um array e devolve as coordenadas de todas as células dife-


rentes de zero, mas os resultados estão na forma de duas tuplas. np.transpose
converte os resultados para uma forma mais útil, um array em que cada linha
é um par de coordenadas.

Da mesma forma, empty_locs é um array que contém as coordenadas das


células vazias, embaralhadas:

empty_locs = locs_where(a==0)

Agora chegamos ao núcleo da simulação. Nós percorremos os agentes infelizes


e os movemos:

for source in unhappy_locs:


i = np.random.randint(len(empty_locs))
dest = tuple(empty_locs[i])
a[dest] = a[tuple(source)]
a[tuple(source)] = 0
empty_locs[i] = source

i é um índice usado para escolher uma célula vazia aleatória.

dest é uma tupla contendo as coordenadas da célula vazia.

Para mover um agente, copiamos o valor de source para dest e, em seguida,


definimos o valor de source para 0 (já que agora está vazio).

Finalmente, substituímos a entrada em empty_locs por source, então a célula


que acabou de ficar vazia pode ser escolhida pelo próximo agente.

9.3 Segregação
Agora vamos ver o que acontece quando executamos o modelo. Começarei
com n = 100 e p = 0.3 e executarei 10 passos.

grid = Schelling(n=100, p=0.3)


for i in range(10):
grid.step()
152 Capítulo 9 Modelos baseados em agentes

Figura 9.1: Modelo de segregação de Schelling com n = 100, condição inicial


(esquerda), após 2 passos (centro) e após 10 passos (direita).

A Figura 9.1 mostra a configuração inicial (esquerda), o estado da simulação


após 2 passos (centro) e após 10 passos (direita).

Agrupamentos se formam rapidamente, com agentes vermelhos e azuis se mo-


vendo em grupos segregados separados por limites de células vazias.

Para cada configuração, podemos calcular o grau de segregação, que é a fração


de vizinhos que são da mesma cor, calculados pela média das células:

np.sum(frac_same) / np.sum(occupied)

Na Figura 9.1, a fração média de graus de vizinhos semelhantes é de 55% na


configuração inicial, 71% após dois passos e 80% após 10 passos!

Lembre-se que quando p = 0.3 os agentes ficariam felizes se 3 dos 8 vizinhos


fossem da sua própria cor, mas eles acabam morando na vizinhança onde 6 ou
7 dos seus vizinhos são da sua própria cor, tipicamente....

A Figura 9.2 mostra como o grau de segregação aumenta e onde se estabiliza


para vários valores de p. Quando p = 0.4, o grau de segregação no estado
estacionário é de cerca de 88%, e a maioria dos agentes não tem vizinhos com
uma cor diferente.
9.4 Sugarscape 153

1.0

Segregação 0.8

0.6

0.4

p = 0.5
0.2 p = 0.4
p = 0.3
p = 0.2
0.0
0.0 2.5 5.0 7.5 10.0 12.5 15.0 17.5
Passos

Figura 9.2: Grau de segregação no modelo de Schelling ao longo do tempo


para uma faixa de valores de p.

Esses resultados são surpreendentes para muitas pessoas e constituem um


exemplo notável da relação complexa e imprevisível entre as decisões indi-
viduais e o comportamento do sistema.

9.4 Sugarscape
Em 1996, Joshua Epstein e Robert Axtell propuseram Sugarscape, um modelo
baseado em agentes de uma “sociedade artificial” destinada a apoiar experi-
mentos relacionados à economia e outras ciências sociais.

Sugarscape é um modelo versátil que foi adaptado para uma ampla variedade
de tópicos. Como exemplos, vou replicar as primeiras experiências do livro de
Epstein e Axtell, Growing Artificial Societies2 .

Em sua forma mais simples, o Sugarscape é um modelo de economia simples


onde os agentes se movimentam em uma grade 2D, recolhendo e acumulando
2
N.T.: Growing Artificial Societies: Social Science From the Bottom Up (Produzindo
Sociedades Artificiais, Ciências Sociais de Baixo para Cima), livro sem tradução para a
língua portuguesa.
154 Capítulo 9 Modelos baseados em agentes

Figura 9.3: Replicação do modelo Sugarscape original: configuração inicial


(esquerda), após 2 passos (centro) e 100 passos (direita).

“açúcar”, o que representa a riqueza econômica. Algumas partes da grade pro-


duzem mais açúcar do que outras, e alguns agentes são melhores em encontrá-lo
do que outros.

Esta versão do Sugarscape é frequentemente usada para explorar e explicar a


distribuição da riqueza, em particular a tendência à desigualdade.

Na grade Sugarscape, cada célula tem uma capacidade, que é a quantidade


máxima de açúcar que pode conter. Na configuração original, existem duas
regiões de alto teor de açúcar, com capacidade 4, cercada por anéis concêntricos
com capacidades 3, 2 e 1.

A Figura 9.3 (esquerda) mostra a configuração inicial, com as áreas mais es-
curas indicando as células com maior capacidade e as bolinhas representando
os agentes.

Inicialmente, existem 400 agentes colocados em locais aleatórios. Cada agente


possui três atributos com valores iniciais escolhidos aleatoriamente:

Açúcar: Cada agente começa com uma dotação de açúcar escolhida de uma
distribuição uniforme entre 5 e 25 unidades.
9.4 Sugarscape 155

Metabolismo: Cada agente tem uma certa quantidade de açúcar que deve
consumir por passo, escolhida uniformemente entre 1 e 4.

Visão: Cada agente pode “ver” a quantidade de açúcar nas células próximas
e passar para a célula com maior quantidade, mas alguns agentes podem
enxergar melhor que os outros. A distância que os agentes enxergam é
escolhida uniformemente entre 1 e 6.

A cada passo, os agentes se movem um de cada vez em uma ordem aleatória.


Cada agente segue estas regras:

• O agente examina as k células em cada uma das 4 direções da bússola,


em que k é o alcance da visão do agente.

• Escolhe a célula desocupada com mais açúcar. Em caso de empate, esco-


lhe a célula mais próxima; entre as células na mesma distância, escolhe
aleatoriamente.

• O agente move-se para a célula selecionada e recolhe o açúcar, adicio-


nando o que foi recolhido à sua riqueza acumulada e deixando a célula
vazia.

• O agente consome parte da sua riqueza, dependendo do seu metabolismo.


Se o total resultante é negativo, o agente “passa fome” e é removido.

Depois que todos os agentes executaram essas regras, as células produzem um


pouco de açúcar, tipicamente 1 unidade, mas o açúcar total em cada célula é
limitado apenas por sua capacidade.

A Figura 9.3 (centro) mostra o estado do modelo após dois passos. A maioria
dos agentes está se movendo em direção às áreas com mais açúcar. Agentes
com visão alta também se movem mais rápido. Agentes com baixa visão
tendem a ficar presos nos planaltos, vagando aleatoriamente até chegar perto
o suficiente para ver a área com mais açúcar.

Os agentes nascidos nas áreas com menos açúcar provavelmente morrerão de


fome, a menos que também tenham visão alta e uma alta dotação inicial.

Dentro das áreas de alto teor de açúcar, os agentes competem uns com os
outros para encontrar e recolher o açúcar à medida que ele cresce. Agentes
com metabolismo alto ou visão baixa são os mais propensos a morrer de fome.
156 Capítulo 9 Modelos baseados em agentes

Quando o açúcar cresce 1 unidade por passo, não há açúcar suficiente para sus-
tentar os 400 agentes com os quais começamos. A população cai rapidamente
no início, depois mais devagar e estabiliza em torno de 250.

A Figura 9.3 (direita) mostra o estado do modelo após 100 passos, com cerca
de 250 agentes. Os agentes que sobrevivem tendem a ser os sortudos, nascidos
com visão alta e/ou metabolismo baixo. Tendo sobrevivido até este ponto,
eles provavelmente sobreviverão para sempre, acumulando estoques ilimitados
de açúcar.

9.5 Desigualdade de riqueza


Em sua forma atual, o Sugarscape modela uma ecologia simples e poderia ser
usado para explorar a relação entre os parâmetros do modelo, como a taxa de
crescimento e os atributos dos agentes, e a capacidade suportada do sistema
(o número de agentes que sobrevive em estado estacionário). Modela também
uma forma de seleção natural, em que agentes com maior “aptidão” têm maior
probabilidade de sobreviver.

O modelo também demonstra um tipo de desigualdade de riqueza, com alguns


agentes acumulando açúcar mais rápido do que outros. Mas seria difícil dizer
algo específico sobre a distribuição da riqueza, porque ela não é “estacioná-
ria”; isto é, a distribuição muda ao longo do tempo e não atinge um estado
estacionário.

No entanto, se dermos aos agentes uma expectativa de vida finita, o modelo


produz uma distribuição estacionária de riqueza. E então podemos executar
experimentos para ver qual efeito os parâmetros e regras têm nessa distribui-
ção.

Nesta versão do modelo, os agentes têm uma idade que é incrementada a cada
passo e uma vida útil aleatória que é uniforme entre 60 e 100. Se a idade de
um agente exceder seu tempo de vida, ele morre.

Quando um agente morre, de fome ou de velhice, ele é substituído por um novo


agente com atributos aleatórios, de modo que a população total é constante.

Começando com 250 agentes, o que é próximo da capacidade suportada, exe-


cutei o modelo para 500 passos. Após cada 100 passos, eu fiz um gráfico
9.5 Desigualdade de riqueza 157

1.0 1.0

0.8 0.8

0.6 0.6
FDA

FDA
0.4 0.4

0.2 0.2

0.0 0.0
0 50 100 150 200 10 -2 10 -1 10 0 10 1 10 2 10 3
Riqueza Riqueza

Figura 9.4: Distribuição de açúcar (riqueza) após 100, 200, 300 e 400 passos
(linhas cinzas) e 500 passos (linha escura). Escala linear (esquerda) e escala
log-x (direita).

da distribuição de açúcar acumulado pelos agentes. A Figura 9.4 mostra os


resultados em uma escala linear (esquerda) e uma escala log-x (direita).

Após cerca de 200 passos (o dobro da vida útil mais longa), a distribuição não
muda muito. E é inclinada para a direita.

A maioria dos agentes tem pouca riqueza acumulada: o percentil 25 é de cerca


de 10 e a mediana é de 20. Mas alguns agentes acumularam muito mais: o
percentil 75 é de cerca de 40 e o valor mais alto é de mais de 150.

Em uma escala log, a forma da distribuição se assemelha a uma distribuição


normal ou gaussiana, embora a cauda direita seja truncada. Se fosse realmente
normal em uma escala log, a distribuição seria lognormal, que é uma distribui-
ção de cauda pesada. E, de fato, a distribuição de riqueza em praticamente
todos os países e no mundo é uma distribuição de cauda pesada.

Seria exagero afirmar que a Sugarscape explica por que as distribuições de


riqueza são cauda pesada, mas a prevalência da desigualdade nas variações do
Sugarscape sugere que a desigualdade é característica de muitas economias,
mesmo as muito simples. Experimentos com regras que modelam tributação
158 Capítulo 9 Modelos baseados em agentes

e outras transferências de renda sugerem que isso não é algo fácil de evitar ou
mitigar.

9.6 Implementação do Sugarscape

A implementação do Sugarscape é mais complicada que dos modelos ante-


riores, então não apresentarei toda a implementação aqui. Vou descrever a
estrutura do código e você pode ver os detalhes no Jupyter notebook para este
capítulo, chap09.ipynb, que está no repositório deste livro. E se você não
está interessado nos detalhes, você pode pular para a próxima seção.

É apresentada a seguir a classe Agent com o método step:

class Agent:

def step(self, env):


self.loc = env.look_around(self.loc, self.vision)
self.sugar += env.harvest(self.loc) - self.metabolism
self.age += 1

A cada passo, o agente se move, coleta açúcar e incrementa age.

O parâmetro env é uma referência ao ambiente, que é um objeto Sugarscape.


Ele fornece métodos look_around e harvest:

• look_around obtém a localização do agente, que é uma tupla de coor-


denadas e o alcance da visão do agente, que é um inteiro. Ela retorna a
nova localização do agente, que é a célula visível com mais açúcar.

• harvest recebe o (novo) local do agente, remove e devolve o açúcar


naquele local.

O código a seguir é a classe Sugarscape e seu método step (sem substituição):


9.6 Implementação do Sugarscape 159

class Sugarscape(Cell2D):

def step(self):

# loop through the agents in random order


random_order = np.random.permutation(self.agents)
for agent in random_order:

# mark the current cell unoccupied


self.occupied.remove(agent.loc)

# execute one step


agent.step(self)

# if the agent is dead, remove from the list


if agent.is_starving():
self.agents.remove(agent)
else:
# otherwise mark its cell occupied
self.occupied.add(agent.loc)

# grow back some sugar


self.grow()
return len(self.agents)
Sugarscape herda de Cell2D, por isso é semelhante aos outros modelos base-
ados em grade que vimos.

Os atributos incluem agents, que é uma lista de objetos Agent, e occupied,


que é um conjunto de tuplas, onde cada tupla contém as coordenadas de uma
célula ocupada por um agente.

Durante cada etapa, o Sugarscape percorre os agentes em ordem aleatória.


Ele invoca step em cada agente e, em seguida, verifica se está morto. Depois
que todos os agentes se mudaram, parte do açúcar volta a crescer.

Se você estiver interessado em aprender mais sobre o NumPy, você pode ver
em mais detalhes make_visible_locs, que constrói um array em que cada
linha contém as coordenadas de uma célula visível para um agente, ordenadas
por distância, mas com células à mesma distância em ordem aleatória.
160 Capítulo 9 Modelos baseados em agentes

Figura 9.5: Comportamento de ondas no Sugarscape: configuração inicial (es-


querda), após 6 etapas (centro) e após 12 etapas (direita).

Você também pode ver os detalhes de Sugarscape.make_capacity, que ini-


cializa a capacidade das células. Ela demonstra o uso de np.meshgrid, que
geralmente é útil, mas leva algum tempo para ser entendido.

9.7 Migração e Comportamento de Ondas


Embora o objetivo do Sugarscape não seja primariamente explorar o movi-
mento de agentes no espaço, Epstein e Axtell observaram alguns padrões in-
teressantes quando agentes migram.

Se começarmos com todos os agentes no canto inferior esquerdo, eles rapi-


damente se moverão em direção ao “pico” mais próximo de células de alta
capacidade. Mas se houver mais agentes do que um único pico pode suportar,
eles rapidamente esgotam o açúcar e os agentes são forçados a se mudar para
áreas de menor capacidade.

Os que têm o maior alcance de visão cruzam o vale entre os picos primeiro e
se propagam em direção ao nordeste em um padrão que se assemelha a uma
frente de onda. Uma vez que eles deixam uma faixa de células vazias atrás
9.8 Emergência 161

deles, outros agentes não seguem em direção ao pico até que o açúcar volte a
crescer.

O resultado é uma série de ondas discretas de migração, onde cada onda se


assemelha a um objeto coerente, como as naves espaciais que vimos no AC
Regra 110 e no Jogo da Vida (ver Seção 5.6 e Seção 6.2).

A Figura 9.5 mostra a condição inicial (esquerda) e o estado do modelo após


6 passos (centro) e 12 passos (direita). Você pode ver as duas primeiras ondas
alcançando e se movendo através do segundo pico, deixando uma faixa de
células vazias para trás. Você também pode ver uma versão animada deste
modelo, onde os padrões de onda são mais claramente visíveis.

Embora essas ondas sejam constituídas de agentes, podemos considerá-las


como entidades próprias, da mesma forma que pensamos nos planadores no
Jogo da Vida.

Uma propriedade interessante dessas ondas é que elas se movem na diago-


nal, o que pode ser surpreendente, porque os próprios agentes só se movem
para o norte ou para o leste, nunca para o nordeste. Resultados como este—
grupos ou “agragados” com propriedades e comportamentos que os agentes
não possuem—são comuns em modelos baseados em agentes. Veremos mais
exemplos no próximo capítulo.

9.8 Emergência
Os exemplos deste capítulo demonstram uma das ideias mais importantes da
ciência da complexidade: a emergência. Uma propriedade emergente é uma
característica de um sistema que resulta da interação de seus componentes e
não de suas propriedades.

Para esclarecer o que é a emergência, ajuda considerar o que não é. Por


exemplo, uma parede de tijolos é dura porque tijolos e argamassa são duros,
então essa não é uma propriedade emergente. Como outro exemplo, algumas
estruturas rígidas são construídas a partir de componentes flexíveis, de modo
que parece um tipo de emergência. Mas são, na melhor das hipóteses, um tipo
fraco, porque as propriedades estruturais seguem leis bem compreendidas de
mecânica.
162 Capítulo 9 Modelos baseados em agentes

Em contraste, a segregação que vemos no modelo de Schelling é uma propri-


edade emergente porque não é causada por agentes racistas. Mesmo quando
os agentes são apenas levemente xenófobos, o resultado da simulação é subs-
tancialmente diferente da intenção das decisões do agente.

A distribuição de riqueza em Sugarscape pode ser uma propriedade emergente,


mas é um exemplo fraco, porque poderíamos razoavelmente prevê-lo com base
nas distribuições de alcance de visão, metabolismo e expectativa de vida. O
comportamento de onda que vimos no último exemplo pode ser um exemplo
mais forte, uma vez que a onda exibe um movimento de capacidade diagonal
que os agentes claramente não possuem.

As propriedades emergentes são surpreendentes: é difícil prever o comporta-


mento do sistema, mesmo que conheçamos todas as regras. Essa dificuldade
não é um acidente; na verdade, pode ser a característica definidora da emer-
gência.

Como Wolfram discute em A New Kind of Science, a ciência convencional


é baseada no axioma de que se você conhece as regras que governam um
sistema, você pode prever seu comportamento. O que chamamos de “leis” são
frequentemente atalhos computacionais que nos permitem prever o resultado
de um sistema sem construí-lo ou observá-lo.

Mas muitos autômatos celulares são computacionalmente irredutíveis, o


que significa que não há atalhos. A única maneira de obter o resultado é
implementar o sistema.

O mesmo pode ser verdade para sistemas complexos em geral. Para siste-
mas físicos com mais do que alguns componentes, geralmente não há modelo
que produza uma solução analítica. Métodos numéricos fornecem um tipo de
atalho computacional, mas ainda existe uma diferença qualitativa.

As soluções analíticas geralmente fornecem um algoritmo de tempo constante


para previsão; ou seja, o tempo de execução do cálculo não depende de t, a
escala de tempo da previsão. Mas métodos numéricos, simulação, computação
analógica e métodos similares levam tempo proporcional a t. E para muitos
sistemas, existe um limite em t além do qual não podemos computar previsões
confiáveis.
9.9 Exercícios 163

Essas observações sugerem que as propriedades emergentes são fundamental-


mente imprevisíveis e que, para sistemas complexos, não devemos esperar en-
contrar leis naturais na forma de atalhos computacionais.

Para algumas pessoas, “emergência” é outro nome para a ignorância; por esse
viés, uma propriedade é emergente se não tivermos uma explicação reduci-
onista, mas se chegarmos a entendê-la melhor no futuro, ela não será mais
emergente.

O status das propriedades emergentes é um tópico de debate, por isso é apro-


priado ser cético. Quando vemos uma propriedade aparentemente emergente,
não devemos supor que nunca possa haver uma explicação reducionista. Mas
também não devemos supor que tem que haver uma.

Os exemplos deste livro e o princípio da equivalência computacional dão boas


razões para acreditar que pelo menos algumas propriedades emergentes nunca
poderão ser “explicadas” por um modelo reducionista clássico.

Você pode ler mais sobre emergência em https://pt.wikipedia.org/wiki/


Emerg%C3%AAncia3 .

9.9 Exercícios
Exercício 9.1 Bill Bishop, autor de The Big Sort4 , argumenta que a soci-
edade americana está cada vez mais segregada pela opinião política, pois as
pessoas escolhem viver entre vizinhos que pensam como eles.

O mecanismo que Bishop propõe não é que as pessoas, como os agentes do mo-
delo de Schelling, têm maior probabilidade de se mover se estiverem isoladas,
mas que, quando se mudam por qualquer motivo, provavelmente escolherão
um bairro com pessoas como elas.

Modifique sua implementação do modelo de Schelling para simular esse tipo


de comportamento e veja se ele gera graus semelhantes de segregação.

Existem várias maneiras de modelar a hipótese de Bishop. Na minha imple-


mentação, uma seleção aleatória de agentes se move durante cada etapa. Cada
3
http://en.wikipedia.org/wiki/Emergence
4
N.T.: Sem tradução para a língua portuguesa.
164 Capítulo 9 Modelos baseados em agentes

agente considera k locais vazios selecionados aleatoriamente e escolhe aquele


com a maior fração de vizinhos semelhantes. Como o grau de segregação de-
pende de k?

Exercício 9.2 Na primeira versão do Sugarscape, nós nunca adicionamos


agentes, então uma vez que a população cai, ela nunca se recupera. Na segunda
versão, só substituímos os agentes quando eles morrem, então a população é
constante. Agora vamos ver o que acontece se adicionarmos alguma “pressão
populacional”.

Escreva uma versão do Sugarscape que adiciona um novo agente ao final de


cada passo. Adicione código para calcular o alcance médio da visão e o me-
tabolismo médio dos agentes no final de cada passo. Execute o modelo por
algumas centenas de passos e faça um gráfico com a população ao longo do
tempo, bem como do alcance médio da visão e do metabolismo médio.

Você deve ser capaz de implementar este modelo herdando de SugarScape e


sobrescrevendo __init__ e step.

Exercício 9.3 Na filosofia da mente, Strong AI é a teoria de que um compu-


tador apropriadamente programado poderia ter uma mente no mesmo sentido
em que os humanos têm mentes.

John Searle apresentou um experimento de reflexão chamado “The Chinese


Room”, destinado a mostrar que Strong AI é falsa. Você pode ler sobre isso
em http://en.wikipedia.org/wiki/Chinese_room.

Qual é a resposta do sistema ao argumento do “The Chinese Room”? Como


o que você aprendeu sobre a emergência influencia sua reação à resposta do
sistema?

Você também pode gostar