Flask, parte 1


Web Frameworks

Para a construção de páginas e aplicativos web é essencial algum conhecimento de html e css, não cobertos nesse artigo.

Um aplicativo web web application ou simplesmente web app) é um aplicativo executado através de um servidor web, diferente de um aplicativo executado na máquina local, e geralmente rodados e visualizados por meio de um browser ou navegador. Eles podem ser um conjunto de páginas de texto dinâmicas contendo, por exemplo, pesquisas em uma biblioteca, um gerenciador de arquivos na nuvem, um gerenciador de contas bancárias ou emails, um servidor de músicas ou filmes, etc. Com frequência esses aplicativos estão conectados a um banco de dados, podendo fazer neles consultas e modificações.

Um framework para a web é um conjunto de softwares destinados a oferecer suporte ao desenvolvimento de aplicativos na Web. Eles buscam automatizar a construção dos web apps e seu gerenciamento durante o funcionamento, após sua publicação na web. Alguns frameworks web incluem bibliotecas para acesso a banco de dados, templates para a construção dinâmica das páginas e auxílio à reutilização de código. Eles podem facilitar o desenvolvimento de sites dinâmicos ou, em alguns casos, de sites estáticos.

Framework Flask

O Flask é um micro-framework em Python usado para desenvolvimento e gerenciamento de web apps. Ele é considerado micro porque possui poucas dependências para seu funcionamento e pode ser usado com uma estrutura inicial bem básica, voltada para aplicações simples. Apenas duas bibliotecas são instaladas junto com o Flask. Ele não contém, por ex., um gerenciador de banco de dados ou um servidor de email. No entanto esses serviços podem ser acrescentados para ampliar as funcionalidades do aplicativo.

Em particular, o Flask não inclui uma camada de abstração com banco de dados. Em vez disso é possível instalar extensões, escolhendo o banco de dados específico que se quer usar. Essas extensões também podem auxiliar a validação de formulário, manipulação de upload, tecnologias de autenticação aberta, entre outras.

Flask foi desenvolvido por Armin Ronacher e lançado em 2010. Algumas vantagens citadas em seu uso são: (a) projetos escrito com a Flask são simples (comparados àqueles gerados por frameworks maiores, como o Django) e tendem a ser mais rápidos. (b) Ele é ágil e modular: o desenvolvedor se concentra apenas nos aspectos utilizados de seu aplicativo, podendo ampliar sob demanda. (c) Os projetos são pequenos mas robustos. (d) Existe uma vasta comunidade de desenvolvedores contribuindo com seu desenvolvimento, apresentando bibliotecas que ampliam sua funcionalidade.

Criando um aplicativo básico

Nessa primeira parte vamos criar um aplicativo bem básico mas funcional. Depois entraremos em outros detalhes do Flask.

Para criar um projeto usando o Flask (ou, na verdade, outro projeto qualquer) é aconselhável criar antes um ambiente virtual para que os pacotes instalados não conflituem com outros em projetos diferentes. A criação e manutenção de ambientes virtuais está descrita na página Ambientes Virtuais, PIP e Conda. Alguns IDEs, como o Pycharm realizam automaticamente esse processo. No meu caso ele ficará abrigado em ~/Projetos/flask/venv. Para simplificar denotarei esse ambiente simplesmente pelo nome flask, que é também o nome da pasta que abriga esse projeto.

Nesse ambiente instalamos o Flask com pip install flask. Uma estrutura básica de ambiente já estará montada após esse passo. Em seguida criamos um arquivo do python, de nome meu_site.py.

# meu_site.py
from flask import Flask
app = Flask(__name__)

@app.route("/")
def homepage():
    return "Esta é a minha homepage"

if __name__ == "__main__":
    app.run()

# é exibido no console do python
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)

Esse arquivo não deve se chamar flask.py para evitar conflito de nomes.

A variável __name__, passada para o construtor do Flask contém o nome do módulo principal e é usada para determinar a localização do aplicativo no app. Outros diretórios e arquivos, como pastas de templates, arquivo de estilo e imagens, serão localizadas à partir dessa raiz.

Aplicativos do Flask incluem um servidor de desenvolvimento que pode ser iniciado com o comando run. Esse comando busca pelo nome do aplicativo na variável de ambiente FLASK_APP. Se queremos rodar o aplicativo meu_site.py executamos na linha de comando:

# Linux ou macOS
(venv) $ export FLASK_APP=meu_site.py
(venv) $ flask run
 * Serving Flask app "hello"
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)

# Microsoft Windows
(venv) $ set FLASK_APP=meu_site.py
(venv) $ flask run
 * Serving Flask app "hello"
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
 
 # alternativamente se pode iniciar o servidor com
 app.run()

No Pycharm, ou outros IDES, você pode executar diretamente esse código.

Da biblioteca flask importamos apenas (por enquanto) a classe Flask. Uma instância da classe é criada com app = Flask(__name__) onde a variável __name__ contém o nome do projeto. A linha @app.route("/") é um decorador que informa que a função seguinte será rodada na raiz / do site. Quando esse arquivo .py é executado dentro de uma IDE ou usando python meu_site.py, na linha de comando, é exibido no console várias mensagens, entre elas a url http://127.0.0.1:5000/, que pode ser clicada ou copiada para a linha de endereço do navegador. Isso resulta na exibição, dentro do navegador, da página:


Clientes e Servidores: O navegador age como o cliente que envia ao servidor uma solicitação, através de uma URL digitada na barra de endereços. O servidor da web transforma essa solicitação em ações a serem realizadas do lado do servidor e retorna uma página com conteúdo de texto e multimídia, renderizados pelo navegador. O Flask fica do lado do servidor, construindo a resposta. Entre outras coisas ele possui um mapeamento entre as URLs e as funções route() que serão executadas no código *.py.

O endereço e a porta 127.0.0.1:5000 são padrões para o Flask. app.run() cria um servidor que atende à requisição HTTP do navegador, exibindo a página html. Qualquer texto retornado pela função homepage() é renderizado no formato html e exibido no navegador. Por exemplo, se fizermos as alterações, colocando o texto entre tags h1:

@app.route("/")
def homepage():
    return "<h1>Esta é a minha homepage</h1>"

if __name__ == "__main__":
    app.run(debug=True)

o texto agora é renderizado como um título de nível 1:

o mesmo texto será exibido mas agora com formatação de título, a tag h1. Todas as demais tags podem ser utilizadas. O parâmetro debug=True faz com que alterações no código sejam imediatamente repassadas para as requisições ao servidor, sem a necessidade de rodar todo o projeto novamente. Com isso basta recarregar a página do navegador para que alterações sejam exibidas, clicando no ícone de atualização ou pressionando F5. No mode debug os módulos dois módulos chamados reloader e debugger estão ativados por default. Com o debugger ativado as mensagens de erro são direcionadas para a página exibida. O mode debug nunca deve ser ativado em um servidor em produção pois isso fragiliza a segurança do site.

Também podemos ativar o módulo no código que executa o aplicativo:

(venv) $ export FLASK_APP=meu_site.py
(venv) $ export FLASK_DEBUG=1
(venv) $ flask run

O decorador @app.route("/") registra a função homepage() junto com a página raiz do site. Outras páginas vão executar outras funções. Por exemplo, uma página de contatos pode ser inserida por meio da inserção de nova função no código. Nesse caso criaremos a função contatos().

# meu_site.py

from flask import Flask
app = Flask(__name__)

@app.route("/")
def homepage():
    return "<h1>Esta é a minha homepage</h1>"

@app.route("/contatos")
def contatos():
    txt = (
    "<h1>Página de Contatos</h1>"
    "<ul>"
    "<li>Contato 1</li>"
    "<li>Contato 2</li>"
    "</ul>"
    )
    return txt

if __name__ == "__main__":
    app.run(debug=True)

Usamos acima a concatenação de string com parênteses: (str1 str2 ... strn).
Agora, além da homepage temos a página de contatos em 127.0.0.1:5000/contatos, com a seguinte aparência.

A funções contatos() e homegage() são chamadas funções de visualização (view functions).

Html e templates: Notamos agora que o código em meu_site.py contém sintaxe misturada de Python e html e pode ficar bem complexo em uma página real exibida na web. Para evitar isso o Flask permite a criação de templates. Fazemos isso da seguinte forma: no diretório raiz onde está o projeto (o mesmo onde foi gravado meu_site.py) criamos o diretório template (o nome default do Flask). Dentro dele colocamos nossos templates. Por exemplo, criamos os arquivos homepage.html,

# homepage.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Homepage: teste Flask</title>
</head>
<body>
    <h1>Este é o título da Homepage</h1>
    <p>Com os devidos parágrafos...</p>
</body>
</html>

e contatos.html:

# contatos.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Contatos</title>
</head>
<body>
    <h1>Página de Contatos</h1>
    <ul>
    <li>Contato 1</li>
    <li>Contato 2</li>
    </ul>
    </body>
</html>

Vários IDEs podem auxiliar na criação desses arquivos html, fornecendo um esqueleto básico a ser preenchido pelo programador.

Além disso modificamos nosso código python para usar a renderização dos templates, importando render_template.

# meu_site.py
from flask import Flask, render_template
app = Flask(__name__)

@app.route("/")
def homepage():
    return render_template("homepage.html")

@app.route("/contatos")
def contatos():
    return render_template("contatos.html")

if __name__ == "__main__":
    app.run(debug=True)

Quando esse código é executado temos a referência ao link que, se aberto, mostra as páginas criadas: digitando 127.0.0.1:5000 abrimos nossa homepage:

Mas, se digitarmos 127.0.0.1:5000/contatos a outra página é exibida:

Uma página pode receber parâmetros do código em python. Por exemplo, digamos que queremos exibir uma página para cada produto existente em uma loja virtual que vende frutas. Nesse caso acrescentamos no código de meu_site.py:

@app.route("/frutas/<nome_da_fruta>")
def frutas(nome_da_fruta):
    return render_template("frutas.html")

Para receber esse parâmetro temos que gravar a página frutas.html na pasta templates, com um conteúdo que receba essa variável.

# frutas.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Frutas disponíveis</title>
</head>
<body>
    <h1>Frutas</h1>
    <p>Você escolheu a fruta: {{nome_da_fruta}}</p>
</body>
</html>

Se for digitado no campo de endereços do navegador, ou passado por meio de um link na tag <a href="http://127.0.0.1:5000/frutas/laranja">Laranja</a> a parte do endereço <nome_da_fruta> = laranja é passado como valor de parâmetro na função frutas("laranja") que é disponibilizado dentro do código html como {{nome_da_fruta}}.

Resumindo: @app.route("/frutas/<nome_da_fruta>") envia uma string na variável nome_da_fruta para a função frutas que, por sua vez repassa ao código html. Dentro do html a variável fica disponível como {{nome_da_fruta}} (dentro de uma dupla chave).

Por exemplo, se digitamos na barra de endereços do navegador http://127.0.0.1:5000/frutas/laranja teremos a exibição de

Essa técnica pode ser usada, por ex., para criar páginas para diversos usuários usando um único template usuario.html.

@app.route('/usuario/<nome>')
def usuario(nome):
    return render_template("usuario.html")
# ou
@app.route('/usuario/<int:id>')
    return render_template("usuario.html")

A parte do código <int:id> é um filtro que transforma a entrada digitada em inteiro, quando possível e será melhor explicada adiante.

Formatando com CSS

O texto dentro de uma página html (HyperText Markup Language) pode ser formatado de algumas formas diferentes, usando css (Cascading Style Sheets). Quando se trata do uso de um framework a forma preferida consiste em apontar no cabeçalho para um arquivo externo css. No Flask isso é feito da seguinte forma: um arquivo css é gravado na pasta static da pasta do projeto. Digamos que gravamos o arquivo /static/styles.css com um conteúdo mínimo, apenas para demonstração, tornando vermelhas as letras do título e azuis as letras dos parágrafos:

# arquivo /static/styles.css
h1 { color:red; }
p { color:blue; }

No cabeçalho das páginas html, dentro da tag <head> colocamos um link para o arquivo de estilos:

# homepage.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <link rel="stylesheet" type="text/css" href="static/styles.css">
    <title>Homepage: teste Flask</title>
</head>

Agora, ao acessar a homepage veremos:


Com todas essas alterações o projeto tem agora a estrutura de pastas mostrada. Na figura à esquerda todas as pastas, inclusive aquelas criadas pelo gerenciador de ambientes virtuais são mostradas. No meu caso elas foram criadas automaticamente pelo IDE Pycharm, mas podem ser criadas pelo programador sem dificuldade. Na figura à direita são mostradas apenas as pastas criadas pela programador diretamente. Um projeto com esse formato roda perfeitamente, apesar de não contar com as vantagens do ambiente virtual (veja artigo).

Outras estruturas de código podem ser inseridas nos templates, como veremos.

Opções de comando de linha

Quando se roda o flask diretamente no terminal podemos ver uma mensagem de ajuda com (venv) $ flask --help, verificar os caminhos definidos no app ou entrar em uma shell interativa.

# Exibir ajuda do flask    
(venv) $ flask --help
  Usage: flask [OPTIONS] COMMAND [ARGS]...
  
    A general utility script for Flask applications.
  
    Provides commands from Flask, extensions, and the application. Loads the
    application defined in the FLASK_APP environment variable, or from a wsgi.py
    file. Setting the FLASK_ENV environment variable to 'development' will
    enable debug mode.
  
      $ export FLASK_APP=hello.py
      $ export FLASK_ENV=development
      $ flask run
  
  Options:
    --version  Show the flask version
    --help     Show this message and exit.
  
  Commands:
    routes  Show the routes for the app.
    run     Run a development server.
    shell   Run a shell in the app context.
  
# exibe os caminhos ativos no aplicativo
  (venv) $ flask routes
  Endpoint  Methods  Rule
  --------  -------  -----------------------
  contatos  GET      /contatos/
  frutas    GET      /frutas/
  frutas    GET      /frutas/
  homepage  GET      /
  static    GET      /static/
  
# entra em uma shell interativa    
  (venv) $ flask shell

A shell do flask inicia uma sessão python no contexto do atual aplicativo onde podemos executar testes ou tarefas de manutenção. O comando flask run admite vários parâmetros:

(venv) $ flask run --help
  Usage: flask run [OPTIONS]
  
    Run a local development server.
  
    This server is for development purposes only. It does not provide the
    stability, security, or performance of production WSGI servers.
  
    The reloader and debugger are enabled by default if FLASK_ENV=development or
    FLASK_DEBUG=1.
  
  Options:
    -h, --host TEXT                 The interface to bind to.
    -p, --port INTEGER              The port to bind to.
    --cert PATH                     Specify a certificate file to use HTTPS.
    --key FILE                      The key file to use when specifying a
                                    certificate.
    --reload / --no-reload          Enable or disable the reloader. By default
                                    the reloader is active if debug is enabled.
    --debugger / --no-debugger      Enable or disable the debugger. By default
                                    the debugger is active if debug is enabled.
    --eager-loading / --lazy-loading
                                    Enable or disable eager loading. By default
                                    eager loading is enabled if the reloader is
                                    disabled.
    --with-threads / --without-threads
                                    Enable or disable multithreading.
    --extra-files PATH              Extra files that trigger a reload on change.
                                    Multiple paths are separated by ':'.
    --help                          Show this message and exit.  

O argumento --host informa ao servido qual é o ambiente web que pode acessar nosso servidor de desenvolvimento. Por default o servidor de desenvovimento do Flask só aceita chamadas do computador local, em localhost. Mas é possível configurá-lo para receber chamadas da rede local ou de ambientes mais amplos. Por exemplo, como o código

(venv) $ flask run --host 0.0.0.0
 * Serving Flask app "hello"
 * Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)

todos os computadores conectados pelo mesmo ip terão acesso ao aplicativo.

Implantação de um aplicativo Flask

O processo de implantação (ou deploy) de um aplicativo consiste nas etapas necessários para colocá-lo acessível para seus usuários. No caso de um aplicativo web a implantação significa estabelecer um servidor ou usar servidores já disponíveis, que os usuários possam acessar, e colocar seu aplicativo como um de seus serviços.

O desenvolvimento do aplicativo se dá em um ambiente de desenvolvimento onde podem existir condições próprias para o debug e nem todas as medidas de segurança estão implementadas. Depois ele passa para a etapa de uso, no ambiente de produção. Uma conta no Heroku pode ser criada, e um site com poucos acessos pode ser mantido sem custos. Se o site for escalonado e crescer a conta deve ser atualizada e paga. A lista abaixo contém links para o Heroku e outros provedores.

Bibliografia

Livros sobre Flask

  • Aggarwal, Shalabh: Flask Framework Cookbook, 2.Ed., Packt, Birmingham-Mumbai, 2019.
  • Ashley, David: Foundation Dynamic Web Pages with Python Create Dynamic Web Pages with Django and Flask, Apress, 2020.
  • Gaspar, D.;StoufferHaider, J.: Mastering Flask Web Development, 2.Ed., Packt, Birmingham-Mumbai, 2018.
  • Grinberg, Miguel: The Flask Mega-Tutorial, Edição do autor, 2020.
  • Grinberg, Miguel: Flask Web Development, Developing Web Applications with Python, O’Reilly, Sebastopol, 2018.
  • Haider, Rehan: Web API Development With Python, CloudBytes, 2020.
  • Maia, Italo: Building Web Applications with Flask, 2.Ed., Packt, Birmingham-Mumbai, 2015.
  • Relan, Kunal: Building REST APIs with Flask, 2.Ed., Apress, 2019.

Referências na Web

Sobre HTML e CSS

todos acessados em fevereiro de 2022.


Ambientes Virtuais, PIP e Conda


Ambiente virtual

Um ambiente virtual é uma área isolada de seu computador onde pacotes específicos são instalados para o uso, sem o problema de conflitarem com outras versões instaladas. Com isso cada projeto pode ter suas próprias dependências, diferentes das possíveis dependências em outros projetos. Até mesmo versões diferentes do python podem ser usadas. Um projeto do Python pode usar diversos pacotes e módulos, sendo que alguns deles podem não estar na biblioteca padrão. Vários ambientes podem ser criados e gerenciados separadamente, sem limite na quantidade, pois são apenas diretórios contendo scripts. Esses ambientes podem ser criados usando as ferramentas no comando de linha venv, virtualenv ou pyenv. Nos concentraremos aqui na ferramenta venv.

Criando ambientes virtuais

Na construção de um aplicativo uma versão específica de uma biblioteca, ou até do próprio Python, pode ser necessária. Para isso a linguagem oferece a possibilidade de se criar ambientes virtuais: um ambiente independente armazenado em uma árvore de diretórios própria contendo a instalação do Python e pacotes em versão específica.

O módulo venv é usado para criar e gerenciar ambientes virtuais. Ele seleciona e organiza a versão do Python e dos módulos usados no projeto.

Para criar um ambiente virtual você deve decidir em que diretório ele deve ser abrigado. Depois execute o módulo venv como um script, no prompt de comando, informando o caminho do diretório. Uma boa prática é criar uma pasta oculta .venv na sua pasta raiz ou pasta de projetos para abrigar todos os seus ambientes virtuais. No exemplo criaremos a pasta ~/Projetos/.venv/aprendendo:

$ python3 -m venv ~/Projetos/.venv/aprendendo
Figura 1: estrutura de arquivos com venv.

Dentro da pasta aprendendo é criada uma estrutura de pastas contendo uma cópia do interpretador e alguns arquivos de configuração, mostrada na figura 1.

Essas pastas tem o conteúdo:

  • bin: arquivos que interagem com o ambiente virtual
  • include: cabeçalhos C que compilam os pacotes Python
  • lib: uma cópia da versão do Python junto com uma pasta site-packages onde cada dependência está instalada

Parte desses arquivos são links simbólicos (ou symlinks) que apontam para as corretas versões das ferramentas do python.

Na pasta bin ficam os scripts de ativação usados para definir o ambiente para uso. Depois de criado o ambiente pode ser ativado executando activate.

# No windows:
$ ~\Projetos\.venv\aprendendo\Scripts\activate.bat

# No Linux (bash shell)
$ source ~/Projetos/.venv/aprendendo/bin/activate

Ao ser carregado um novo ambiente o prompt de comando muda para indicar qual ambiente está em uso. Nesse prompt carregamos o Python (no meu caso 3.8.8 Anaconda), importamos a biblioteca sys e verificamos os caminhos em uso.

(aprendendo) $ python
Python 3.8.8 (default, Apr 13 2021, 19:58:26) 
[GCC 7.3.0] :: Anaconda, Inc. on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import sys
>>> sys.path
   ['', '/home/usr/.anaconda3/lib/python38.zip', '/home/usr/.anaconda3/lib/python3.8', '/home/usr/.anaconda3/lib/python3.8/lib-dynload', '/home/usr/Projetos/.venv/aprendendo/lib/python3.8/site-packages']
>>> sys.prefix
   '/home/usr/Projetos/.venv/aprendendo'

Esse ambiente está isolado do meio externo. Por exemplo, no meu caso o pandas está instalado globalmente. No entanto ele não pode ser apontado por um código rodando em no ambiente (aprendendo).

>>> import pandas
    Traceback (most recent call last):
      File "", line 1, in 
    ModuleNotFoundError: No module named 'pandas'

Para sair do ambiente reservado usamos deactivate no terminal.

(aprendendo) (base) [guilherme@gui ~]$ deactivate
(base) [guilherme@gui ~]$ python
Python 3.8.8 (default, Apr 13 2021, 19:58:26)
[GCC 7.3.0] :: Anaconda, Inc. on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import pandas

Como o pandas está instalado globalmente, nenhuma mensagem de erro é gerada e o módulo fica disponível para uso.

A ativação de um ambiente significa a especificação do local onde estão os executáveis e bibliotecas importadas. Para reativar o ambiente nos “deslocamos” até a pasta onde ele está instalado e executamos activate.

$ cd /home/usuario/Projetos/.venv/aprendendo
$ source bin/activate
# o prompt é alterado
(aprendendo) $ python
>>> import sys
>>> sys.prefix
    '/home/guilherme/Projetos/.venv/aprendendo'
Observação Importante: Para instalar env com versão específica do Python

Pode ocorrer que existam mais de uma versão do Python instalada em seu computador. Para criar um ambiente com outra versão devevemos executar o script do venv na versão que desejamos para o ambiente virtual. Por exemplo, para um ambiente com python 3.11 (supondo que ele esteja instalado nesse computador) devemos executar:

$ python3.11 -m venv ~/Projetos/.venv/VsCode
$ source ~/Projetos/.venv/VsCode/bin/activate
$ python
>>> Python 3.11.0a7 (main, Apr  7 2022, 00:00:00) [GCC 11.2.1 20220127 (Red Hat 11.2.1-9)] on linux
>>> Type "help", "copyright", "credits" or "license" for more information.

Nesse caso criamos um ambiente virtual no diretório ~/Projetos/.venv/VsCode.

Gerenciando ambientes virtuais com pip

Uma vez dentro do novo ambiente você pode instalar pacotes usando pip que, por default, encontra e instala pacotes do Python Package Index. PIP é um gerenciador padrão de pacotes (ou bibliotecas) do Python, que acessa um reservatório de pacotes publicados no Python Package Index, ou PyPI. Em versões mais recentes ele vem instalado por default. Um pacote é um conjunto de arquivos que executam um ou várias funções. Eles podem ser importados em um aplicativo para extender a funcionalidade do Python padrão. PIP funciona por meio de comandos de linha, digitados no prompt do sistema operacional.

A sintaxe de venv é a seguinte: [parâmetros opcionais] | indice um ou outro. Apenas ENV_DIR é obrigatório e posicional.

venv [-h] [–system-site-packages] [–symlinks | –copies] [–clear] [–upgrade]
[–without-pip] [–prompt PROMPT] [–upgrade-deps] ENV_DIR [ENV_DIR …]
Cria o ambiente virtual em um ou mais diretórios especificados.
ENV_DIR Diretório onde criar o ambiente virtual,
-h, –help exibe (o presente) texto de ajuda,
–system-site-packages Dá acesso ao ambiente virtual para a pasta site-packages do sistema.
–symlinks Tenta usar symlinks no lugar de cópias, quando os symlinks não são default na plataforma.
–copies Tenta usar cópias no lugar de symlinks, mesmo que symlinks sejam o default na plataforma.
–clear Apaga o conteúdo do diretório de ambiente, se existe, antes da criação do ambiente.
–upgrade Atualiza o diretório de ambiente, caso o python do ambiente tenha sido atualizado.
–without-pip Interrompe a instalação ou atualização via pip nesse ambiente. (Por default o pip é acionado).
–prompt PROMPT Estabelece um prefixo alternativa para o prompt desse ambiente.
–upgrade-deps Atualiza as dependências do pip setuptools para o última versão disponível em PyPI.

Depois que o ambiente é criado você deve ativá-lo com o script source pasta/do/ambiente/bin/activate.

Instalação de PIP

Você pode verificar a presença do pip (ou conferir a versão) com pip --version, no prompt de comando.

$ pip --version
  pip 22.0.3 from /home/usuario/Projetos/.venv/aprendendo/lib/python3.8/site-packages/pip (python 3.8)    

O output do comando mostra a versão do pip e do python sendo usados no ambiente virtual instalado na pasta /home/usuario/Projetos/.venv/aprendendo.

Alternativamente, é possivel encontrar onde está instalado o pip:

# no Windows    
C:\> where pip3  
# no Linux
$ which pip
~/.anaconda3/bin/pip
$ which pip3
~/.anaconda3/bin/pip3

Caso o pip não esteja instalado, isso pode ser feito de duas formas: ensurepip e get-pip.py.

No prompt do terminal de seu sistema (que representaremos por $, comentários por #) digite:

$ python -m ensurepip
# ou 
$ python -m ensurepip --upgrade

A chave -m garante que pip seja executado como um módulo. Na segunda forma se garante que apenas versões mais novas que atual (se presente) seja instalada. Nenhuma ação será executa se já existe a instalação, ou se está em sua versão mais atual, no segundo caso. pip será instalado globalmente ou no ambiente virtual, se esse estiver ativo.

Outra alternativa é baixar o script get-pip.py e executá-lo com a primeiro linha no código. A segunda linha é uma forma de atualizá-lo.

# instalar pip
$ python get-pip.py
# fazer atualização de pip
$ python -m pip install --upgrade pip

Instalando módulos com o PIP

O uso geral de pip é o seguinte:

# no linux/Mac    
$ python -m pip <argumentos>
# ou
$ pip <argumentos>
# no windows    
$ py -m pip <argumentos>

Para instalar um módulo com o PIP executamos pip com o argumento install e o nome do módulo. Vários módulos podem ser instalados com uma única linha.

$ python -m pip install 
# ou    
$ pip install 
# por exemplo, para instalar o Flask
$ pip install Flask
# vários módulos
$ python -m pip install   ... 

Para controlar qual é a versão a ser instalado usamos:

python -m pip install Modulo             # instalar a última versão
python -m pip install Modulo ==1.0.4     # instalar versão especificada
python -m pip install 'Modulo >=1.0.4'   # especificar a versão mínima

É possível passar para pip uma lista de requisitos para a exata reprodução de um ambiente.

# para gerar o arquivo relativa a um ambiente:
$ python -m pip freeze > requirements.txt
# para reproduzir a instalação:
$ python -m pip install -r requirements.txt    

O arquivo requirements.txt contém uma lista dos argumentos do pip install. Essa lista pode ser gerada com freeze.

pip pode ser usado para instalar pacotes de outros repositórios. Por exemplo, se você deseja instalar o pacote rptree disponível em TestPyPI package index, ou no GitHub.

# no TestPyPI    
python -m pip install -i https://test.pypi.org/simple/ rptree
# no GitHub
python -m pip install git+https://github.com/realpython/rptree

Observação: pode ocorrer que em seu computador o python 3 esteja instalado com o nome python3.

<h2=”idm”>Listando e desinstalando módulos

Módulos instalados podem ser vistos com o argumento list. A lista obtida reflete a instalação do Flask e suas dependências. Uma lista com pacotes desatualizados é obtida com a chave list –outdated.

$ python -m pip list
  Package      Version
  ------------ -------
  click        8.0.3
  Flask        2.0.2
  itsdangerous 2.0.1
  Jinja2       3.0.3
  MarkupSafe   2.0.1
  pip          22.0.3
  setuptools   49.2.1
  Werkzeug     2.0.2    

# listar apenas pacotes desatualizados
$ python -m pip list --outdated
  Package    Version Latest Type
  ---------- ------- ------ -----
  setuptools 49.2.1  60.8.1 wheel

Pode ocorrer que você deseje usar outro pacote e queira remover o antigo de seu computador. A desinstalação é feita com uninstall. pip desinstala pacotes com versões desatualizadas antes de fazer uma atualização para versão mais nova.

Um cuidado deve ser tomado: Quando um pacote é instalado é possível que ele possua dependências que são instaladas juntas com ele. Se você tem muitos pacotes instalados é possível que mais de um use a mesma dependência. Por isso é importante verificar se ele é dependência de outro ou se possui dependências. Para isso usamos python -m pip show <modulo>.

$ python -m pip show Flask
Name: Flask
Version: 2.0.2
Summary: A simple framework for building complex web applications.
Home-page: https://palletsprojects.com/p/flask
Author: Armin Ronacher
Author-email: armin.ronacher@active-4.com
License: BSD-3-Clause
Location: /home/guilherme/.anaconda3/lib/python3.8/site-packages
Requires: itsdangerous, Jinja2, click, Werkzeug
Required-by: 

Vemos no output que Flask possui as dependências (Requires: )itsdangerous, Jinja2, click e Werkzeug. Por outro lado ele não é exigido por nenhum outro modulo (Required-by:) portanto pode ser seguramente desinstalado. Para isso usamos uninstall.

$ python -m pip uninstall Flask

O mesmo procedimento deve ser usado com as dependências, caso você queira apagá-las.

Busca por pacotes

pip pode fazer buscas por um pacote com o comando:

python -m pip search "query"

A pesquisa retorna uma lista de pacotes com uma breve descrição de cada um.
Importante: pip search deixou de funcionar em dezembro de 2020. Um substituto para esse comando é poetry (que deve ser instalado em seu sistema). Por exemplo, uma busca por pacotes com “pandas” no nome: (o output está truncado).

$ poetry search "pandas"
  pandas (1.4.0)
     Powerful data structures for data analysis, time series, and statistics
  pandas3 (0.0.1)
     Boto3 extension to help facilitate data science workflows with S3 and Pandas
  pandas-alchemy (0.0.2)
     SQL based, pandas compatible DataFrame & Series


Poetry é similar ao npm do JavaScript, gerenciando pacotes e auxiliando na criação de distribuições de aplicativos e bibliotecas, além da inserção no PyPI. Outro pacote é o Pipenv que gerencia pacotes e controla ambientes virtuais. Real Python: Pipenv Guide.

Estrutura de um projeto Python

O Python é bastante flexível na questão de estrutura de pastas para um projeto. No entanto algumas sugestões foram dadas para um desenho ótimo para um projeto. Aqui eu sigo as sugestões de Lucas Tonin.
Vamos denominar nosso projeto de meu_projeto. Se estamos usando, como é recomendado, um ambiente virtual podemos criá-lo com python3 -m venv ~/Projetos/.venv/meu_projeto, que já estabelece uma estrutura mínima de pastas. Vamos por hora ignorar as pastas relativas ao ambiente virtual.

Para definir uma terminologia chamamos de projeto python tudo aquilo que estará no diretório base, que em nosso caso é ~/Projetos/.venv/meu_projeto. Todos os arquivos relacionados ao desenvolvimento, teste e arquivos auxiliares ficam nesse diretório. Chamamos de pacote (package) ao conteúdo de um subdiretório dentro do projeto com o mesmo nome. O pacote contém o código-fonte do aplicativo. Ele isola o código fonte de todos os outros arquivos. Depois de pronto a instalaçao do projeto inclui apenas os arquivos contidos nesse diretório, ignorando código fonte e testes.

Arquivo __init__.py: O pacote deve necessariamente conter pelo menos um arquivo como o nome __init__.py. A presença desse informa ao python que esse diretório é um pacote. __init__.py é executado automaticamente quando esse pacote é carregado e deve conter as inicializações para o aplicativo. Duas coisas importantes podem ser aí incluídas: (a) uma variável ROOT_DIR com o caminho absoluto do atual pacote, onde estiver no momento; (b) as configurações de logger, quando existir.

from os.path import dirname, abspath
ROOT_DIR = dirname(abspath(__file__))
# inicialização de logs

Um arquivo de documentação, README.md: geralmente esses arquivos são escritos em MARKDOWN. Ele deve conter uma descrição de seu projeto para outros usuários de seu código, ou para você mesmo no caso de retomar após um tempo esse trabalho, além de instruções de instalações. Um exmplo simples:

# Meu Projeto
Um aplicativo simples para a importação de arquivos *csv* e exportação para banco de dados SQL.
## Instalação
Para instalar execute `pip install /caminho/meu_projeto`

Usando setup.py: o arquivo setup.py contém informações sobre configurações do pacate a ser instalado. No mínimo ele deve conter:

import setuptools
setuptools.setup(name='meu_projeto', packages=['meu_projeto'])

que informa o nome do projeto e do pacote. O parâmetro packages informa ao pip que apenas esse diretório será instalado.

Para projetos maiores e mais complexos, que envolvem muitas dependências, é útil acrescentar um arquivo requirements.txt Este arquivo lista os pacotes necessários para o projeto, que não fazem parte da biblioteca padrão. Com ele o pip pode baixar e instalar automaticamente todas as dependências, se não o encontrar já instalado. Por exemplo, se o projeto depende do numpy (um pacote para computação científica) a arquivo deverá conter pelo menos a linha

numpy==1.18.2

O arquivo setup.py deve ser modificado para que essa informação seja usada.

# setp.py
import setuptools
with open('requirements.txt', 'r') as f:
    install_requires = f.read().splitlines()

setuptools.setup(name='meu_projeto',
                 packages=['meu_projeto'],
                 install_requires=install_requires)


Um arquivo LICENCE, que descreve a licença sob a qual você está distribuindo seu projeto, pode ficar no diretório base, onde pode ser facilmente encontrado. Finalmente, uma pasta separa para os testes usados para testar a correção do código.

O projeto fica portanto com a seguinte estrutura mostrada na figura.

Usando Jupyter Notebook em ambiente virtual

Anaconda é uma plataforma de distribuição de Python e R muito usada para a ciência de dados e aprendizado de máquina. Ele simplifica a instalação de pacotes como pandas, NumPy, SciPy, e pode ser usada com diversas outras linguagens. Conda é o gerenciador padrão de pacotes do Anaconda, multiplataforma e agnóstico à linguagem e que pode ser usado para instalar pacote de terceiros. O Anaconda Navigator, instalado junto com o Anaconda, é uma interface gráfica que permite o gerenciamento de pacotes coda, com busca, instalação, atualização e desinstalação, execução dos aplicativos incluidos na Anaconda Cloud ou outro repositório Anaconda local. Todo esse sitema está disponível para Windows, macOS e Linux.

O Anaconda Cloud é um serviço de nuvem que abriga pacotes, notebooks e ambientes Python para variadas situações e casos, incluindo pacotes conda e PyPI públicos e privados. Ele permite o uploud de pacotes de usuário e notebooks, sem a necessidade de um login ou conta na nuvem.

Jupyter Notebook é uma ambiente interativo de interface do usuário da Web onde se pode rodar código nas linguagens instaladas, criar documentos de notebook contendo texto (em Markdown), imagens e vídeos. Esses documentos podem ser partilhados e publicados na nuvem, onde podem ser alterados e executados por outros usuários.

O Jupyter Notebook faz um gerenciamento de ambiente próprio. Mas tambem podemos criar um ambiente virtual específico para ele. Isso é bastante útil pois esse ambiente é usualmente dedicado à computação científica e aplicações com grandes volumes de dados, exigindo bibliotecas específicas. Para isso criamos um ambiente virtual, que chamaremos de jupyter. Depois ativamos o ambiente e instalamos o Jupyter Notebook dentro desse ambiente.

# criamos o ambiente virtual    
$ python3 -m venv ~/Projetos/.venv/jupyter_venv
# ativamos esse ambiente
$ cd /home/usuario/Projetos/.venv/jupyter_venv
$ source bin/activate 
# instala o Jupyter Notebook no ambiente
(jupyter) $ ipython kernel install --user --name=jupyter_venv

Para usar o ambiente virtual abra o jupyter e selecione o kernel no menu kernel | change kernel. A opção para o ambiente jupyter_venv deve estar disponível, como mostra a figura, como uma das opções de kernel a usar.

Para desinstalar o ambiente fazemos:

$ jupyter-kernelspec uninstall jupyter_venv

Gerenciador conda


Para quem está trabalhando com a distribuição Python Anaconda (site) é mais interessante usar o gerenciador conda, que além de gerenciar pacotes e suas dependências controle também ambioentes virtuais. Ele pode ser usado com Python, R, Ruby, Lua, Scala, Java, JavaScript, C/ C++, FORTRAN e outras.

Com o conda você pode pesquisar por pacotes, instalar os pacotes desejados ou construir um pacote do usuário com build (conda-build deve ser instalado).

A versão de conda pode ser verificada, e atualizada se necessário. Essa atualização pode incluir atualização de outros pacotes e remoção de pacotes não usados.

# verificar versão    
$ conda --version
  conda 4.10.3
# informações mais detalhadas podem ser obtidas
$ conda info
    active environment : base
    active env location : /home/guilherme/.anaconda3
# -----(outpup truncado)-----

# atualizar versão
$ conda update conda
# se existir versão mais recente
  Proceed ([y]/n)? y

A pesquisa e instalação de pacotes é feita com search e install. A construção (build) de pacotes é feitas com build.

# pesquisar   
$ conda search scipy
# instalação
$ conda install scipy
# o novo pacote deve estar na lista
$ conda list

# construir um pacote
$ conda build meu_projeto

Versões podem ser especificadas, inclusive com o uso de operadores lógicos.

conda install numpy=1.11                  #(instala versão especificada)
conda install numpy==1.11                 #(idem)
conda install "numpy>1.11"                #(versão superior a 1.11)
conda install "numpy=1.11.1|1.11.3"       #(versão 1.11.1 ou 1.11.3)
conda install "numpy>=1.8,<2"          #(versão maior ou igual a 1.8 mas inferior a 2)

A barra | é o operador OR: “pacote=1.1|1.3” significa 1.1 ou 1.3.
A vírgula , é o operador AND: “pacote=1.1,1.3” significa ambos 1.1 e 1.3.
O igual = é o operador fuzzy: “pacote=1.11” pode ser “1.11”, “1.11.1”, , “1.11.8”, etc.
O duplo igual == é o operador exato: “pacote==1.11” pode ser “1.11”, “1.11.0”, , “1.11.0.0”, etc.

Gerenciamento de ambientes com conda

Um novo ambiente, que chamaremos de cientifico pode ser criado, e simultanemanete instalado nele o pacote pandas. Caso um versão do Python diferente da versão default do Anaconda instalado usamos a declaração conda create --name nome_ambiente python=n, onde n é a versão desejada.

# criar um ambiente com um pacote
$ conda create --name cientifico pandas
  Proceed ([y]/n)? y
  
# ativar o ambiente
$ conda activate cientifico

# para verificar a versão do python em uso
$ python --version
  Python 3.8.8

# para criar ambiente coom outra versão do python
conda create --name cientifico python=3.9

Uma lista de todos os ambientes disponíveis pode ser vista com info --envs. As pastas listadas dependem do local onde os ambientes foram criados.

$ conda info --envs
  conda environments:
      base           /home/username/Anaconda3
      cientifico   * /home/username/Anaconda3/envs/cientifico

O ambiente ativo aparece com um asterisco *.

Um canal conda é um local na rede onde pacotes estão armazenados. Por default Conda busca em uma lista de canais e baixa arquivos de Repo Anaconda, onde alguns pacotes podem ser prorietários. Outros repositórios podem ser apontados com o conda, por exemplo o Conda Forge, uma organização parte do GitHub que contém um grande número de pacotes gratuitos. O parâmetro --override-channels é usado para que os canais default (gravados em .condarc) sejam ignorados.

# para apontar para o conda-forge
$ conda install scipy --channel conda-forge

# múltilpos repositórios podem ser apontados
$ conda install scipy --channel conda-forge --channel bioconda
# argumentos que aparecem na frente são pesquisados primeiro

# para pesquisar em repositório local, ignorando os defaults
$ conda search scipy --channel file:/caminho/local-channel --override-channels

Para instalar um pacote presente no conda-forge também é possível fazer:

$ conda config --add channels conda-forge
$ conda config --set channel_priority strict
$ conda install "nome-do-pacote"

Muito mais é possível com o Conda: consulte as instruções em Conda Docs.

Argumentos positionais
comando descrição
clean Remove pacotes e caches não utilizados.
compare Compara pacotes entre ambientes conda.
config Modifica os valores de configuração em .condarc. (feito após a configuração do comando git).
Grava arquivo .condarc do usuário
create Cria um ambiente conda a partir de uma lista de pacotes especificados.
help Exibe uma lista de comandos conda disponíveis e suas ajudas.
info Exibe informações sobre a instalação atual do conda.
init Inicializa o conda para interação do shell. [Experimental]
install Instala uma lista de pacotes em um ambiente conda especificado.
list Lista pacotes vinculados em um ambiente conda.
package Utilitário de pacote conda de baixo nível. (EXPERIMENTAL)
remove Remove uma lista de pacotes de um ambiente conda especificado.
uninst Alias para remove.
run Execute executável em um ambiente conda. [Experimental]
search Pesquisa pacotes e exibe informações associadas.
update Atualiza pacotes conda para a versão compatível mais recente.
upgrade Alias para update.
Argumentos opcionais
comando descrição
-h, –help Mostra ajuda,
-V, –version Mostra versão.

Bibliografia

Gráficos com Bokeh

O que é Bokeh


Bokeh é uma biblioteca de visualização de dados interativa em Python que existe desde 2013. Ela pode ser usada para a plotagem de gráficos em diversos níveis de sofisticação, representando conjuntos simples ou complexos de dados. A biblioteca pode ser usada por usuários com pouca experiência em programação ou programadores experientes com acesso aos seus comandos mais intrincados. Os gráficos do Bokeh podem ser interativos e embutidos em páginas da web.

Algumas definições básicas na terminologia de Bokeh são necessárias:

Application um aplicativo Bokeh é um documento renderizado e executado no navegador.
Glyphs glifos são os blocos de construção do Bokeh como linhas, círculos, retângulos e outras formas,
Server o servidor Bokeh é usado para compartilhar e publicar gráficos e aplicativos interativos para um público de sua escolha
Widgets os widgets do Bokeh são controles tais como menus suspensos, controles deslizantes e outras ferramentas de interface gráfica com o usuário que permitem interatividade

Instalação

Para instalar o Bokeh, se você tem Anaconda ou Miniconda, basta usar o comando: conda install bokeh.
Usando pip a biblioteca pode ser instalada com: pip install bokeh.

Comandos básicos

Dois tipos de saídas podem ser obtidas: o gráfico enviado para um arquivo output_file('arquivo.html') ou embutidos no Jupyter Notebook, output_notebook(). Bokeh possui uma interface similar à do matplotlib, que é denominada bokeh.plotting. A classe principal dessa interface é Figure que contém os métodos para a inclusão de glyphs em um gráfico.

» # importar as classes necessárias
» from bokeh.io import output_notebook, show
» from bokeh.plotting import figure
» output_notebook()
Figura 1
» # dados a plotar » x = [0,1, 0,3] » y = [0,10,90,10] » # instanciar um objeto figure » fig = figure(plot_width=450, plot_height=300) » # desenhar uma linha ligando os pontos dados » fig.line(x,y) » # exibir a figura 1 » show(fig)

A variável fig contém um objeto da classe com largura e altura especificadas, e instrução relativas às ferramentas a serem apresentadas, do lado direito no caso. O comando fig.line(x,y) usa o glyph line (linha) para ligar os pontos dados nas duas listas.

Glyphs

Glyphs são todos os elementos gráficos como linhas, círculos e cruzes marcadores de pontos, etc. Diferentes glyphs podem ter parâmetros ajustáveis diferentes. No exemplo aplicamos uma cor de fundo à figura, largura e altura. fig.circle() recebe os parâmetros posição (x,y), tamanho, que no caso é variável, cada círculo com raio size=y, largura de linha (as circunferências) line_width=5, e cor color=['red', 'blue','green','yellow']. Cada um dos discos tem uma cor diferente.

» # define dados    
» x = [1,2,3,4]
» y = [10,40,90,160]
Figura 2
» # instancia figura com cor de fundo e dimensões dadas » fig = figure(background_fill_color='#aabbff', » plot_width=450, plot_height=300) » fig.circle(x,y, size=y, line_width=5, color=['red', 'blue','green','yellow'], alpha=.5) » # exibir figura 2 » show(fig)

Os seguintes glyphs estão disponíveis:

asterisk() cross() diamond() diamond_cross()
circle() circle_x() circle_cross() triangle()
inverted_triangle() square() square_x() square_cross() x()

Alguns exemplos de uso de glyphs line, circle, cross, asterisk, x estão abaixo. As ordenadas y foram calculadas para formarem uma sequência de parábolas empilhadas, exceto pela reta horizontal amarela larga de fundo.

» # define valores da abscissa. Ordenadas serão calculadas    
» x = np.arange(10)

» plot = figure(plot_width=650, plot_height=300)
» plot.line(x, 100, color='yellow', line_width=140, alpha=.2,)
» plot.circle(x, x**2, size = 20, color='red', alpha=.5, line_width=7)
» plot.cross(x, x**2+50, size = 20, color='blue', alpha=.8, line_width=7)
» plot.asterisk(x, x**2+100, size = 40, color='green', alpha=.8, line_width=7)
» plot.x(x, x**2+150, size = 40, color='black', alpha=.8, line_width=7)
# figura 3
» show(plot)
Figura 3

As propriedades de cada glyph podem ser calculadas e dependentes em qualquer fonte de dados. Na caso abaixo usamos a própria ordenada x para calcular alguns desses parâmetros. A propriedade color=['yellow','blue']*5 garante que os 10 ‘diamantes’ plotados alternem entre as cores amarelo e azul.

» x = np.arange(10)    
» plot = figure(plot_width=650, plot_height=300)
» plot.circle_cross(x, x, size = 5+x, color='#ffaaff', alpha=1, line_width=7+x)
» plot.circle_dot(x, x, size = 30-2*x, color='#66aaff', alpha=.5, line_width=2)
» plot.inverted_triangle(x, x+5, size = 30-2*x, color='red', alpha=.9, line_width=2)
» plot.diamond(x, x+5, size = 30-2*x, color=['yellow','blue']*5, alpha=.8, line_width=2)
» show(plot)
» # figura 4 é plotada

» # outro plot com tamnho e cor variáveis
» x = np.arange(10)
» plot = figure(plot_width=600, plot_height=300)

» for k in range(100):
»     plot.circle(k, (k-50)**2, size = k*2, color=(255*k/100, 200, 255),
»                 fill_color=(2.5*k, 100, 255-2.5*k), alpha=.4, line_width=2)
» show(plot)
» # figura 5 é plotada

Gráficos de Barras (Bar Plots )

Para gráficos de barras a sintaxe é um pouco diferente. As coordenadas x são o ponto central da barra vertical, top é a altura. A largura width= 1 significa nenhum espaçamento entre barras. As cores podem ser uma só ou uma lista, de mesmo tamanho que o número de barras. Para as barras horizontais o comprimento das barras é dado por right e a largura da barra é height.

» x = [8,9,10]
» y = [1,4,2]

» # barras verticais
» plot = figure(plot_width=600, plot_height=300)
» # plot.vbar para traçar barras verticais
» plot.vbar(x,top = y, color = ['blue','red','green'], width= .8, alpha=.5)
» show(plot)     # exibe gráfico 6

» # barras horizontais
» plot = figure(plot_width=600, plot_height=300)
» plot.hbar(x, right = y, color = ['#77aaff','#aa77ff','#ff77aa'], height= .9, alpha=.5)
» show(plot)     # exibe gráfico 7


O desenho da regiões ou patches é feito com plot.patches. As regiões são descritas por meios das coordenadas de suas arestas, dois pares de listas para cada figura. As propriedades fill_color, line_color, line_width, alpha receberam listas de 3 elementos, um para cada figura. Se um valor único for passado ele será válido para todas as figuras.

» # regiões a colorir
» x_coords = [[1,1,3,], [2,2,2.5], [1.5,1.5,4,4]]
» y_coords = [[2,6,4], [3,6,7], [3,6,7,2]]

» plot = figure(plot_width=600, plot_height=300)
» plot.patches(x_coords, y_coords, fill_color = ['#77aaff','#aa77ff','#ff77aa'],
               line_color ='black', alpha=.4)
» show(plot)      # figura 8
Figura 8

Gráficos de Dispersão (Scatter Plots )

Gráficos de dispersão podem ser feitos com qualquer um dos glyphs. No exemplo abaixo a mesma plotagem é feita com círculos e com cruzes de tamanhos diversos, para efeito estético.

» from bokeh.models import Range1d
» plot = figure(plot_width=400, plot_height=250,
»               x_axis_label = 'Coordenada x (abcissa)',
»               y_axis_label = 'Ordenada y', title='Gráfico de dispersão')
» plot.x_range = Range1d(0, 5)
» plot.y_range = Range1d(0, 8)
» fcor = ['red','green','blue','brown','violet']
» x = np.array([1,2,3,4,4])
» y = np.array([5,6,2,2,4])
» plot.circle(x,y, size =x*15, color = '#aa55ff', fill_color=fcor, fill_alpha=.3)
» plot.diamond(x,y, size = x*15, color = 'red', alpha=.5,
»              fill_alpha=.4, fill_color=fcor[::-1])

» show(plot)    # figura 9
Figura 9

Observe que as coordenadas x, y poderiam ser listas. Como são arrays (do numpy) as operações para o cálculo do tamanho são permitidas. As faixas de coordenadas e ordenadas plotadas são controladas por x_range, y_range e estabelecidas por meio da função Range1d(m, n) (importada de bokeh.models). Os parâmetros color e alpha se referem ao traçado do glyph, enquanto fill_color e fill_alpha ao seu preenchimento. Relembrando, fcor[::-1] retorna a lista em ordem reversa.

Dataframes e ColumnDataSource

Usamos, até aqui, listas e arrays como fonte de nossos dados e serem plotados. Também podemos usar dataframes como fontes e o processo não é muito diferente. Se um dataframe tem uma coluna x e outra y plotamos o gráfico x × y simplesmente passando as series como parâmetros para x e y: plot.line(x = df['x'], y = df['y']).

Para montar um gráfico um pouco mais elaborado vamos usar os dados já descritos na seção sobre matplotlib. São dados sobre o número de nascimentos em países do mundo de 1950 até 2020, e a estimativa à partir de 2021. Importamos o arquivo .csv para o dataframe dfBrasil e selecionamos apenas as linhas relativas ao Brasil, até o ano de 2020. Esse dataframe é usado para plotar o gráfico de linhas. Outro dataframe, dfDecada, contendo apenas linhas com anos múltiplos de 10, é usado para plotar círculos. O raio do círculo é proporcional ao número de nascimentos.

» import pandas as pd
» dfNasc = pd.read_csv('./dados/number-of-births-per-year.csv')
» # selecionamos apenas linhas sobre o Brasil, até 2020
» dfBrasil = dfNasc[(dfNasc['Entity']=='Brazil') & (dfNasc['Year'] < 2021)]
» dfBrasil = dfBrasil.rename(columns={'Year':'ano', dfBrasil.columns[3]:'nasc'})
» # mantemos apenas colunas 'ano', 'nasc'
» dfBrasil = dfBrasil[['ano', 'nasc']]
» dfBrasil.head(2)
↳          ano         nasc
  4050    1950    2439820.0
  4051    1951    2467186.0

» # criamos outro df, apenas com anos multiplos de 10
» dfDecada = dfBrasil[dfBrasil['ano']%10==0]

» cor = ['salmon','gold','teal','plum','powderblue','coral','wheat','azure']
» plot = figure(plot_width=400, plot_height=250,
»               x_axis_label = 'Ano',
»               y_axis_label = 'Nascimentos (milhões)',
»               title='Número de Nascimentos no Brasil')
» plot.line(x = dfBrasil['ano'], y = dfBrasil['nasc']/1e6, color='black')
» plot.circle(x = dfDecada['ano'], y = dfDecada['nasc']/1e6,
»             size=dfDecada['nasc']/1e5, fill_color = cor,
»             fill_alpha=.5)
» show(plot)    # figura 10
Figura 10

Uma forma útil de fazer a conexão com os dados é o objeto ColumnDataSource. Ela é especialmente útil quando se usa a mesma fonte para diversas plotagens e para vários widgets. ColumnDataSource cria um dicionário onde as chaves podem ter nomes definidos pelo usuário e as valores correspondentes são os dados contidos em colunas do dataframe (ou outra fonte).

Vamos retornar aos dados relativos aos nascimentos nos países do mundo. Dessa vez vamos manter apenas dados sobre o Brasil e a Indonésia (escolhido porque é um país que tem população próxima à brasileira), apenas nos anos de 1950 até 2020. Nessa tabela os países recebem os códigos Code='BRA' e 'IDN', respectivamente.

» dfNasc = pd.read_csv('./dados/number-of-births-per-year.csv')
» # selecionamos as linhas sobre o Brasil e a Indonésia, até 2020
» dfBI = dfNasc[((dfNasc['Code']=='BRA') | (dfNasc['Code']=='IDN')) & (dfNasc['Year'] < 2021)]
» dfBI = dfBI.rename(columns={'Year':'ano', dfBI.columns[3]:'nasc'})

Desses dados criamos um dataframe apenas com dados brasileiros, outro com dados sobre a Indonésia. Para mesclar esses dataframes alteramos as colunas ‘nasc’ respectivamente para ‘BRA’ e ‘IDN’.

» dfB = dfBI[['ano','nasc']][dfBI['Code']=='BRA'].rename(columns={'nasc':'BRA'})
» dfB.head(3)
↳          ano          BRA
  4050    1950    2439820.0
  4051    1951    2467186.0
  4052    1952    2523577.0

» dfI = dfBI[['ano','nasc']][dfBI['Code']=='IDN'].rename(columns={'nasc':'IDN'})
» dfI.head(3)
↳          ano          IDN
  14700    1950    2867664.0
  14701    1951    2939269.0
  14702    1952    3078414.0
Para ler mais sobre a operação do pandas realizada, similar a um INNER JOIN do sql, consulte o artigo Pandas e SQL Comparados, nesse site.

Ambos os dataframes têm 71 linhas. Usamos pandas.merge() para juntar esses dataframes pelo campo ‘ano’, um processo similar ao INNER JOIN do sql. Depois criamos três novas colunas: (1) campo dif, com a diferença por ano entre os números brasileiros e indonésios, (2), difM, a média entre os dois e (3) raio, descrito no comentário † abaixo.

» dfBI = pd.merge(dfB, dfI, on='ano')
» dfBI.head(3)
↳       ano          BRA          IDN
  0    1950    2439820.0    2867664.0
  1    1951    2467186.0    2939269.0
  2    1952    2523577.0    3078414.0

» dfBI['dif'] = dfBI['IDN'] - dfBI['BRA']
» dfBI['difM'] = (dfBI['IDN'] + dfBI['BRA'])*.5
» dfBI['raio'] = dfBI['dif']/33000                   # veja comentário †

» # o dataframe fica assim:
» dfBI
↳          ano           BRA           IDN          dif          difM         raio
    0     1950     2439820.0     2867664.0     427844.0     2653742.0     12.964970
    1     1951     2467186.0     2939269.0     472083.0     2703227.5     14.305545
    2     1952     2523577.0     3078414.0     554837.0     2800995.5     16.813242

() A terceira coluna adicional, raio, é a diferença vezes um fator para que os discos em plot.circle() preencham o espaço entre os nascimentos nos dois países, centrados na média. Essa plotagem aqui tem apenas efeito visual e para demonstrar os parâmetros do plot.

» from bokeh.models import Range1d
» from bokeh.plotting import ColumnDataSource
    
» # cria o objeto ColumnDataSource
» data = ColumnDataSource(dfBI)
» plot = figure(width=900, height=250, x_axis_label = 'Ano', y_axis_label = 'Nascimentos e diferenças',
»               background_fill_color='#cfefff', border_fill_color='#ddeeff',
»               title='Nascimentos no Brasil e Indonésia')

» plot.x_range = Range1d(1950, 2035)
» plot.y_range = Range1d(0, 5.5E6)

» plot.line(x = 'ano', y = 'BRA', source = data, color = 'red', legend_label = "Brasil")
» plot.line(x = 'ano', y = 'IDN', source = data, color = 'green', legend_label = "Indonésia")
» plot.x(x = 'ano', y = 'dif', source = data, color = 'blue', legend_label = "diferença")
» plot.asterisk(x = 'ano', y = 'difM', source = data, color = 'black', legend_label = "média")
» plot.circle(x = 'ano', y = 'difM', source = data, fill_color = 'whitesmoke', alpha=.2, size = 'raio')

» show(plot)    # figura 11
Figura 11

Nesse gráfico introduzimos as legendas para cada plot. O campo difM foi plotado duas vezes, uma com um asterisco, outro com círculos com tamanhos determinados pelo campo raio. As faixas de plotagem, ranges, foram determinados para incluir gráfico e legendas. Cor de fundo para o gráfico e bordas são definidas com background_fill_color e border_fill_color.

Para o próximo gráfico baixamos para a subpasta dados do atual projeto o arquivo owid-covid-data.csv, publicado por Our World in Data com dados diários sobre a vacinação mundial contra o covid, entre 01/01/2020 e 26/09/2021. Deste aproveitamos apenas algumas colunas para plotar gráficos para efeito de demonstração do bokeh.

» # importamos os dados para um dataframe
» dfVacina = pd.read_csv('./dados/owid-covid-data.csv')

» # o dataframe tem 64 colunas e 119454 linhas
» dfVacina.shape      # (119454, 64)

» # podemos ver os nomes das colunas com
» dfVacina.columns    # nomes omitidos aqui

» # usamos apenas as colunas no dicionário
» colunas = {'date':'data',
»            'iso_code':'code',
»            'total_cases':'total',
»            'gdp_per_capita':'pib',
»            'human_development_index':'idh',
»            'life_expectancy':'expVida',
»            'total_deaths_per_million':'mortes',
»            'people_vaccinated_per_hundred':'vacinados'           
»           }
» # renomeamos as colunas
» dfVacina = dfVacina.rename(columns=colunas)

» # uma lista dos novos nomes:
» lst = list(colunas.values())
» # geramos novo df apenas com essas colunas
» df = dfVacina[lst]
» # eliminamos os linhas com NaN
» df = df.fillna(method='bfill')      # veja comentário ‡

» # as três primeiras linhas são
» df.head(3)
↳              dia    code   total         pib      idh   expVida    mortes   vacinados
   0    2020-02-24     AFG     5.0    1803.987    0.511     64.83     0.025         0.0
   1    2020-02-25     AFG     5.0    1803.987    0.511     64.83     0.025         0.0
   2    2020-02-26     AFG     5.0    1803.987    0.511     64.83     0.025         0.0

» # finalmente montamos um dataframe contendo apenas o último dia registrado
» dfUltimo = dfU[dfU['dia']=='2021-09-26']

() O método df.fillna(method='bfill') preenche valores nulos com o valor encontrado na mesma coluna, em linha posterior. (Leia aqui sobre tratamento de dados ausentes).

Lembramos que code identifica o país, total é o número total de casos de infecção por covid, mortes é o número total de mortes, por milhão e vacinados é o número de pessoas vacinadas, por 100 mil.

Podemos, em alguns casos, desejar incluir no gráfico um valor calculado a partir de um ou mais campos da tabela. Por ex., considerando que o campo idh varia entre 0,4 até 0,95, podemos usar esse campo, multiplicado por um fator, como informação do tamanho dos círculos plotados. Para fazer isso poderíamos incluir uma coluna extra com esse valor, como já foi feito em exemplos anteriores. Mas quando usamos o ColumnDataSource temos uma forma mais direta de fazer o mesmo. Podemos passar valores calculados no dicionário de valores que alimenta o ColumnDataSource.

» from bokeh.plotting import ColumnDataSource
» data = ColumnDataSource(data = {
»                        'idh' : dfUltimo['idh'],
»                        'expVida' : dfUltimo['expVida'],
»                        'tamanho': dfUltimo['idh']*20,
»                        'grande': dfUltimo['idh']*40,
»                        'alfa': dfUltimo['idh']*.08})
» plot = figure(width=600, height=300, x_axis_label = 'IDH',
»               y_axis_label = 'Exp. Vida', outline_line_color='black',
»               background_fill_color='#F5F1E3', title='IDH x Expectativa de Vida')

» plot.circle(x = 'idh', y = 'expVida', source = data, color='blue', alpha=.6,
»             fill_color = 'white', fill_alpha=1,  size = 'tamanho')
» plot.circle(x = 'idh', y = 'expVida', source = data, color='black', alpha= .1,
»            fill_color = 'red', fill_alpha='alfa', size = 'grande')

» show(plot)     # figura 12
Figura 12

Os campos do dataframe foram passados como valores em um dicionário cujas chaves são usadas como nome de campos nas plotagens. Os campos 'tamanho': dfUltimo['idh']*20 e 'grande': dfUltimo['idh']*40 são calculados para servir como informação para o tamanho (size ) dos círculos. O segundo círculo plotado tem apenas efeito estético, com um tamanho maior que o primeiro. O campo calculado alfa (uma fração do idh) é usado para regular a transparência dos discos vermelhos maiores.

O uso de ColumnDataSource permite que mais de um dataframe forneça dados para o gráfico. No entanto todas as series envolvidas devem ter o mesmo tamanho. Para ver isso vamos separar os dados sobre o Brasil e os EUA em duas tabelas separadas.

» # separa os dados relativos ao Brasil e os EUA
» dfBU = df[(df['code']=='BRA') | (df['code']=='USA')].copy()    # comentário §

» # para usar as datas no eixo x transformamos o campo 'dia' de string em datetime
» dfBU.loc[:,'dia'] = pd.to_datetime(df.loc[:,'dia'], format='%Y/%m/%d')    

» # com essa transformação a coluna passa a conter um datetime (timestamp). Por ex.:
» dfBU.loc[15250][0]
Timestamp('2020-02-26 00:00:00')

» # criamos dataframes para os dois países                      # comentário ‡
» dfUS = dfBU[(dfBU['code']=='USA') & (dfBU['dia'] &ge '2020-02-26')]
» dfBR = dfBU[dfBU['code']=='BRA']

(§) O uso de df2 = df1.copy() realiza uma cópia e não apenas pega um slice de df1. Esse procedimento evita mensagens de erro na linha seguinte, quando um campo do dataframe será alterado.

() No dataframe original existe um número maior de valores para os EUA. O corte na data especificada faz com que dfUS e dfBR tenham o mesmo tamanho.

Podemos agora plotar gráficos do número de mortes por COVID no Brasil e EUA, no mesma figura.

» cds = ColumnDataSource(data = {
»                        'dataBRA' : dfBR['dia'],
»                        'dataUSA' : dfUS['dia'],
»                        'mortesBRA' : dfBR['mortes'],
»                        'mortesUSA' : dfUS['mortes']
»                        })

» plot = figure(width=600, height=300,
»               x_axis_type = 'datetime', x_axis_label = 'data', y_axis_label = 'mortes',
»               background_fill_color='#fafaff', title='Mortes no Brasil e EUA')

» plot.circle(x = 'dataBRA', y = 'mortesBRA', source = cds, color='green' ,alpha=.2,
»             fill_color = 'yellow', fill_alpha=.3, size = 15, legend_label='EUA')


» plot.circle(x = 'dataBRA', y = 'mortesUSA', source = cds, color='blue' ,alpha=.2,
»             fill_color = 'red', fill_alpha=.3, size = 15, legend_label='EUA')

» plot.legend.location = 'top_left'

» show(plot)    # figura 13
Figura 13

Introduzimos nesse gráfico o uso de x_axis_type = 'datetime' para informar que o eixo x receberá dados de uma series temporal. plot.legend.location = 'top_left' informa a posição para as legendas.

Layouts

Layouts permitem a organização de gráficos em linhas e colunas múltiplas. Neles é possível vincular escalas de eixos entre gráficos diferentes.

Para explorar os layouts vamos usar o dataframe já montado df, que contém os campos dia, code, total, pib, idh, expVida, mortes, vacinados, descritos acima. Com ele construiremos 4 gráficos e os exibiremos em linhas, colunas e matrizes. A tabela inclui dados dos países ao longo de vários anos e, portanto, não há uma interpretação muito clara de seu significado. O objetivo é apenas o aprendizado da técnica.

» # transformando a coluna dia para um datetime
» df.loc[:,'dia'] = pd.to_datetime(df.loc[:,'dia'], format='%Y/%m/%d')

» #  a fonte de todos os gráficos é a mesma, nesse caso
» from bokeh.plotting import ColumnDataSource
» cds = ColumnDataSource(data = df)

» # gráfico 1
» plot1 = figure(width=300, height=200, x_axis_type = 'datetime',
»                x_axis_label = 'Data', y_axis_label = 'Mortes',
»                background_fill_color='#fafaff', title='Mortes no Mundo')

» plot1.dot(x = 'dia', y = 'mortes', source = cds, color='rosybrown' ,alpha=.5)

» # gráfico 2
» plot2 = figure(width=300, height=200,
»                x_axis_label = 'Expectativa de vida', y_axis_label = 'mortes',
»                background_fill_color='#fafffa', title='Expectativa de Vida x PIB')

» plot2.dot(x = 'expVida', y = 'pib', source = cds, color='red' ,alpha=.1)

» # gráfico 3
» plot3 = figure(width=300, height=200,
»                x_axis_type = 'datetime', x_axis_label = 'data', y_axis_label = 'mortes',
»                background_fill_color='#ffefff', title='PIB x Mortes')

» plot3.dot(x = 'pib', y = 'mortes', source = cds, color='blue' ,alpha=.05)

» # gráfico 4
» plot4 = figure(width=300, height=200, x_axis_label = 'PIB', y_axis_label = 'IDH',
»                background_fill_color='#9f9fff', title='PIB x IDH no mundo')
» plot4.dot(x = 'pib', y = 'idh', source = cds, color='yellow')

No código acima construimos quatro gráficos. Abaixo exploramos as possibilidades de layouts em linha, em coluna e em matriz.

» from bokeh.layouts import row, column
» # agrupar 2 gráficos em uma linha
» linha_layout = row(plot1,plot2)
» show(linha_layout)

» coluna_layout = column(plot3,plot4)
» show(coluna_layout)


» matriz_layout = column(row(plot1,plot2), row(plot3,plot4))
» show(matriz_layout)

Uma solução também interessante consiste em apresentar todos os gráficos no mesmo espaço, usando as classes Tabs e Panel. No código abaixo criamos 3 painéis e passamos nos argumentos os gráficos já construídos. Cada painel pode conter linhas e colunas, vistas anteriormente e passados no argumento child, além de um título que será usado nas guias ou tabs. Os painéis são inseridos em um objeto Tabs e exibidos.

» # importamos as classes necessárias
» from bokeh.models.widgets import Tabs, Panel
» # criamos 3 paineis
» tab1 = Panel(child = plot1, title = 'Mortes')
» tab2 = Panel(child = row(plot2,plot3), title = 'Exp Vida, PIBxMortes')
» tab3 = Panel(child = plot4, title = 'PIB x IDH')
» # insere os paineis no objeto Tabs
» objeto_tabs = Tabs(tabs = [tab1, tab2, tab3])
» # exibe o objeto
» show(objeto_tabs)

Ao clicar em uma guia o painés correspondente é exibido. Na figura estão mostrados a 1ª guia (figura 17) e a 3ª (figura 18).

Um layout de rede (grid layout) pode reunir gráficos em uma matriz, gerando resultado similar ao mostrado na figura 16. Para isso podemos usar o seguinte código.

» from bokeh.layouts import gridplot
» # cria uma rede ou grid
» grid_layout = gridplot([plot1, plot2], [plot3, plot4])
» show(grid_layout)
» # uma figura como a figura 16 é plotada.

Ao montar o grid_layout um espaço em branco pode ser inserido com None no lugar da variável do gráfico.

Algumas vezes é importante que dois ou mais gráficos tenham a mesma escala em um ou ambos os eixos. Para isso usamos o código como o seguinte.

» # criamos plots com a mesma escala (aqui no eixo do x)
» plot2.x_range = plot1.x_range
» # criamos um layout  (aqui em linha)
linha_layout = row(plot2, plot1)
show(linha_layout)

Anotações e Widgets

Para os próximos exemplos vamos usar o aqquivo population.csv, baixado do site Our World in Data, na página sobre população mundial.

O arquivo ./dados/population.csv foi baixado no link acima.

import pandas as pd
» # Importar dados para um dataframe
» df = pd.read_csv('./dados/population.csv')    

» # as colunas têm os nomes
» df.head(0)
↳ Entity   Code   Year   Total population (Gapminder, HYDE & UN)

» # 4 colunas e 53307 linhas
» df.shape # (53307, 4)

» # renomeamos as colunas
» colunas = {'Entity':'pais',
»            'Code':'codigo',
»            'Year':'ano',
»            'Total population (Gapminder, HYDE & UN)':'populacao'}
» df = df.rename(columns=colunas)

» # as colunas agora têm os nomes
» df.head(0)
↳ pais   codigo   ano   populacao

Já vimos como colocar títulos e legendas nas gráficos. No exemplo abaixo o título e posição são ajustados como uma propriedade de plot, diferente do parâmetro usado antes. Além disso podemos marcar regiões do gráficos com cores diferentes e incluir texto explicativo para realçar algum aspecto dos dados. Para isso usamos as classes Label e LabelSet.

Para alimentar esse gráfico vamos criar 3 ColumnDataSouces diferentes: para população e ano geramos cdsUSA para os EUA, cdsBRA para o Brasil, ambos após 1750. cdsLabel é usado para inserir anotações sobre os anos de independência e abolição da escravidão para os dois países.

» cdsUSA = ColumnDataSource(data = {
»     'ano' : df[(df['codigo']=='USA')  & (df['ano'] >= 1750)]['ano'],
»     'pop' : (df[(df['codigo']=='USA')  & (df['ano'] >= 1750)]['populacao'])/1e6,
» })
» cdsBRA = ColumnDataSource(data = {
»     'ano' : df[(df['codigo']=='BRA')  & (df['ano'] >= 1750)]['ano'],
»     'pop' : (df[(df['codigo']=='BRA')  & (df['ano'] >= 1750)]['populacao'])/1e6,
» })

» cdsLabel = ColumnDataSource(data=
»      dict(x=[1776, 1800, 1882, 1888],  y=[50, 100, 200, 260],
»           nota=['Indep. EUA (1776)', 'Abol. EUA (1857)',
»           'Indep. BR (1882)', 'Abol. BR (1888)']))

Agora estamos prontos para plotar esses dados. As únicas importações novas são das classes Label, LabelSet. Os dois gráficos de barra abaixo recebem os campos ano e pop, cada um relativo a um dos países.

» from bokeh.io import output_file, show, output_notebook
» from bokeh.plotting import figure
» from bokeh.plotting import ColumnDataSource
» from bokeh.models import Label, LabelSet

» output_notebook()

» grafico = figure(plot_width=600, plot_height=300, x_axis_label = 'ano',
                   y_axis_label = 'População (em milhões)')
» grafico.title.text = 'População do Brasil e do EUA de 1800 até o presente'
» grafico.title_location = 'above'

» grafico.vbar(x = 'ano', top = 'pop', source=cdsUSA,
               color = 'red', width= .1, legend_label = 'EUA')
» grafico.vbar(x = 'ano', top = 'pop', source=cdsBRA,
               color = 'green', width= 1, legend_label = 'Brasil')

» labels = LabelSet(x='x', y='y', text='nota', x_offset=0,
                    y_offset=0, source=cdsLabel, render_mode='canvas')

» texto = Label(x=1750, y=150, render_mode='css',
»               text='Independência e Abolição', text_color='blue',
»               border_line_color='#a0a0f0', border_line_alpha=1.0,
»               background_fill_color='linen', background_fill_alpha=1.0)

» grafico.add_layout(labels)
» grafico.add_layout(texto)
» grafico.legend.location = 'top_left'
» show(grafico)

Os objetos Label, LabelSet são criados com seus respectivos atributos e depois inseridos no grafico.

Usando mapas de cor

Para atribuir cores para uma categoria de dados, separando visualmente a informação para cada categoria, podemos atribuir uma cor a cada uma delas usando CategoricalColorMapper. Nele associamos a uma lista de fatores (factors ou dados categóricos) com uma lista de cores (em palette).

No exemplo inicializamos a variável mapaDeCor como um CategoricalColorMapper atribuindo os parâmetros factors e palette aos nomes das categorias e uma lista de cores. A associação é feita através do parâmetro transform no scatter plot. Novamente dois plots são traçados para efeito estético.

» from bokeh.io import output_notebook, show
» from bokeh.plotting import figure, CategoricalColorMapper
» from bokeh.models import ColumnDataSource, Range1d
» output_notebook()

» cor = ['salmon','gold','firebrick','plum','powderblue','teal','wheat','red']
» nome = ['Otto', 'Ana', 'Joana', 'Jorge', 'Marco', 'Agildo','Lu','Zana']
» dicio= dict(nome=nome,
»             altura=[1.70, 1.65, 1.48, 1.88, 1.58, 1.62, 1.83, 1.91],
»             peso=[97, 65, 89, 76, 67, 74,65, 94]
»            )
» mapaDeCor = CategoricalColorMapper(factors=nome, palette=cor)

» cds = ColumnDataSource(data=dicio)

» p = figure(title='Alunos: distribuição peso x altura',
»            x_range=Range1d(60, 110), y_range=Range1d(1.2, 2.2),
»            plot_width=400, plot_height=250)

» p.scatter(x='peso', y='altura', size=20, source=cds,
»           color=dict(field='nome', transform=mapaDeCor), alpha=.2)
» p.scatter(x='peso', y='altura', size=10, source=cds,
»           color=dict(field='nome', transform=mapaDeCor))
» p.xaxis[0].axis_label = 'Peso (kgs)'
» p.yaxis[0].axis_label = 'Altura (metros)'

» labels = LabelSet(x='peso', y='altura', text='nome',
                    x_offset=0, y_offset=8, source=cds)

» p.add_layout(labels)
» show(p)

Bibliografia

  • Jolly, Kevin: Hands-On Data Visualization with Bokeh, Interactive web plotting for Python using Bokeh, 2018 Packt Publishing, Mumbay.
  • Site Bokeh: Documentation, acessado em agosto de 2021.
  • Site Bokeh: First Steps, acessado em agosto de 2021.
  • Site Our World in Data, contendo grande variedade de tabelas com dados sobre vários temas, do mundo.
  • Rodés-Guirao, Lucas: COVID-19 Dataset by Our World in Data no Github. Acessado em outubro de 2021.

Python: Iteradores, Itertools e Funções Geradoras


Iteradores

Nessa seção para imprimir vários resultados eu uso quase sempre print(x, end=' ') para que uma nova linha não seja lançada após cada impressão. O objetivo é diminuir o espaço de página usada e facilitar a leitura.

Já vimos que objetos que são sequências e coleções podem ser lidos iterativamente com o uso do operador for.

» lista = ['Aa', 'Bb', 'Cc', 'Dd', 'Ee', 'Ff']
» for t in lista:
»     print(t, end=' < ')
↳ Aa < Bb < Cc < Dd < Ee < Ff <

Laços for são claros e concisos. Por trás desse resultado simples, a instrução for chama a função iter() na sequência (ou coleção) que retorna um objeto iterador. Dentro do iterador existe o método __next__() que acessa os elementos no contêiner um de cada vez. Ao final da iteração, quando se extinguem os elementos, __next__() levanta uma exceção StopIteration que é reconhecida pelo laço como o fim da iteração. O método __next__() pode ser chamado através da função interna next(), como mostra o exemplo:

» iterador = iter('OMS')
» print(next(iterador))
» print(next(iterador))
» print(next(iterador))
» print(next(iterador))
↳ O
↳ M
↳ S
↳ StopIteration:

No código acima usamos iter(sequencia) que retorna um iterável da sequência ‘OMS’. StopIteration é uma mensagem de erro ao final da iteração, com o iterador esgotado. Ela aparece aqui resumida.

Podemos construir uma classe iterável implementado nela os métodos __iter__() e __next__(). No caso abaixo a classe simplesmente retorna a sequência invertida, do último elemento para o primeiro

» class Inverter:
»     def __init__(self, data):
»         self.data = data
»         self.index = len(data)

»     def __iter__(self):
»         return self

»     def __next__(self):
»         if self.index == 0:
»             raise StopIteration
»         self.index -= 1
»         return self.data[self.index]

# instanciamos um objeto Inverter usando uma lista como argumento
» inv = Inverter(['Aa', 'Bb', 'Cc', 'Dd', 'Ee', 'Ff'])

» for t in inv:
»     print(t, end=' < ')
↳ Ff < Ee < Dd < Cc < Bb < Aa <

# seq é Inverter usando uma string como argumento
» seq = Inverter('Joazeiro')
» for t in seq:
»     print(t, end=' < ')
↳ o < r < i < e < z < a < o < J <     

Sequências podem ser transformados em iteradores com as funções:

iter(objeto, sentinel) retorna o objeto (uma sequência) como iterável,
interrompe a iteração quando o valor retornado for igual à sentinel (opcional),
reversed(objeto) retorna a sequência como iterável, em ordem inversa.

que, como vimos, são úteis quando usadas com loops for, while. Por exemplo, com uma lista (ou uma tupla):

» x = ["apple", "banana", "cherry"]
» print(next(x))
↳ TypeError: 'list' object is not an iterator

» # transformado a lista em um iterador
» x = iter(["apple", "banana", "cherry"])
» print(next(x), next(x), next(x))
↳ apple banana cherry


Um objeto zip() é um iterador que junta duas sequências e retorna tuplas com elementos das sequências em seus argumentos.

» z = zip(['a', 'b', 'c', 'd'], [1, 2, 3, 4])
» print(next(z))
» print(next(z))
↳ ('a', 1)
↳ ('b', 2)

Da mesma forma map retorna um iterável.

» m = map(lambda x, y: x**y, [8, 2, 9], [5, 3, 7])
» print(next(m))
» print(next(m))
» print(next(m))
↳ 32768
↳ 8
↳ 4782969

# outro exemplo
» list(map(len, ['Abacate', 'Uva', 'Jacoticaba']))
↳ [7, 3, 10] 

Observe que um objeto pode ser um iterável (como uma lista) mas não ser um iterador. No entanto ele pode ser transformado um um iterador.

» numeros = [1, 2, 3, 4, 5]
» next(numeros)
↳ TypeError: 'list' object is not an iterator

» i = iter(numeros)
» next(i)
↳ 1
» next(i)
↳ 2
» # o estado do iterador é armazenado entre iterações
» for x in i:
»     print(x, end=' ')
↳ 3 4 5

Muitas das funções do Python retornam iteradores ao invés de listas ou tuplas. Por exemplo enumerate, reversede open(file)retornam iteradores.

» alunos = ['Pedro', 'Maria', 'Marco']
» nAluno = enumerate(alunos)        # enumerate
» next(nAluno)
↳ (0, 'Pedro')

» for t in nAluno:
»     print(t)
↳ (1, 'Maria')
↳ (2, 'Marco')

» invertido = reversed(alunos)       # reversed
» next(invertido)
↳ 'Marco'

» f = open('./dados/linhas.txt')     # arquivo
» next(f)
↳ 'Esta é a linha 1\n'

Por outro lado muitas funções internas (e das bibliotecas) aceitam iteráveis (e iteradores) como parâmetros. Abaixo exemplos do uso de zip, dict recebendo iteradores.

» numeros = [10, 20, 30]
» quadrados = (n**2 for n in numeros)
» quadrados    # é um objeto generator (que é um iterador)
↳ <generator object <genexpr> at 0x7f67280c1120>

» z = zip(numeros, quadrados)        # zip recebe um iterador como argumento
» print(next(z), next(z), next(z))
↳ (10, 100) (20, 400) (30, 900)

» # Um dicionário pode receber um iterável como argumento
» alunos = ['Pedro', 'Maria', 'Marco']
» nAluno = enumerate(alunos)         # um iterador
» d = dict(nAluno)
» d
↳ {0: 'Pedro', 1: 'Maria', 2: 'Marco'}

Módulo itertools

Outros módulos foram tratados em A Biblioteca Padrão dessas notas.

Um módulo da biblioteca padrão interessante para manipulação de iteradores é o itertools, que contém diversos métodos para manipulação de iteráveis e funções que retornam iteráveis. Elas são voltadas para a velocidade de execução e uso otimizado de memória.

Existem dois tipos de interadores: iteradores infinitos continuam a rodar indefinidamente se nenhuma condição de parada for imposta, enquanto os iteradores finitos são criados com um número determinado de ciclos determinados. O módulo Itertools possui funções de cada tipo:

iteradores infinitos count, repeat, cycle,
iteradores finitos chain, compress, tee, dropwhile, takewhile.

Iteradores finitos

Função chain(): A função chain reune listas transformando-as em um único iterável.

» import itertools as it
» abc = ['Aa', 'Bb', 'Cc']
» vxz = ['Vv', 'Xx', 'Zz']
» for txt in it.chain(abc, vxz):
»     print(txt, end=' ')
↳ Aa Bb Cc Vv Xx Zz 

» # o objeto retornado por chain é um iterável: 
» letras = it.chain(abc, vxz)
» for i in range(3):
»     print(next(letras), end=' ')
» print(next(letras), next(letras), next(letras))
↳ Aa Bb Cc Vv Xx Zz

Os argumentos de chain(iteravel,...) podem ser de qualquer tipo, desde que iteráveis. Por exemplo, podemos juntar uma lista, um dicionário e um it.count():

» lista = ['Aa', 'Bb', 'Cc']
» dicio = {'G': 'g', 'H': 'h'}
» conta = it.count()
» for k in it.chain(lista, dicio.items(), conta):
»     print(k, end=' ')
↳ Aa Bb Cc ('G', 'g') ('H', 'h') 0 1 2 3 4 5 6 7 8 9 10 ...

O 4º e 5º loop retornam as tuplas do dicionário. Em seguida os inteiros são retornados. (A exibição foi interrompida manualmente!)

Função compressed(iteravel, seletor): compressed recebe um iterável e um seletor (uma lista de booleanos) e retorna um iterável filtrado pelo seletor, contendo apenas elementos onde o seletor é True.

» aluno = ['Paulo', 'Maria', 'Ricardo']
» aprovado = [False, True, True]
» for n in it.compress(aluno, aprovado):
»     print(n, end=' ')
↳ Maria Ricardo 

Os argumentos de compressed podem ser iteráveis com infinitos elementos. Por exemplo, abaixo construímos o iterador com elementos True ou False dependendo de ser o número par ou impar.

» par = (x%2==0 for x in it.count())
» for pares in it.compress(it.count(), par):
»     print(pares, end=' ')
↳ 0 2 4 6 8 10 12 14 16 18 20 22 24 26 28 30 32 34 ...

Função tee(iteravel): recebe um iterável e retorna dois objetos clonados do original. O iterável original fica esgotado depois da operação. Também é possível passar um parâmetro para gerar n clones, em itertools.tee(iteravel, n).

» elementos = (x**x for x in range(6) if x**x%2==0)
» lista1, lista2 = it.tee(elementos)
» print(lista1)                      # as listas são objetos itertools._tee
↳ <itertools._tee object at 0x7fc032da5ac0>

» print(list(lista1), list(lista2))
↳ ['1', '2', '3'] ['1', '2', '3']
» print(list(elementos))             # a lista original fica esgotada
↳ []

# um parâmetro pode ser passado para gerar n clones
» vitaminas = ['A', 'B12']
» vitas = it.tee(vitaminas, 4)
» for i in vitas:
»     print(list(i), end=' ')
↳ ['A', 'B12'] ['A', 'B12'] ['A', 'B12'] ['A', 'B12'] 

Função dropwhile(função, iteravel): recebe um iterável e uma função seletora (que avalia cada elemento do iterável como um booleano) e retorna os elementos do iterável à partir do primeiro retorno False da função. Em outras palavras ela descarta os elementos avaliados como True no início da lista (ou iterável). Podemos obter a lista à partir do último valor avaliado como False com reversed(lista).

» def par(x):                       # retorna True se x é par
»     return x % 2 == 0
» numeros = [0, 2, 4, 8, 16, 17, 32, 64, 67, 128]

» # o primeiro False ocorre em 17
» for t in it.dropwhile(par, numeros):
»     print(t, end=' ')
↳ 17 32 64 67 128

» # o mesmo pode ser conseguido com uma função lambda
» print(list(it.dropwhile(lambda t: t%2==0 , numeros)))
↳ [17, 32, 64, 67, 128]

» # lista anterior ao último valor avaliado como False (em ordem reversa)
» print(list(it.dropwhile(lambda t: t%2==0 , reversed(numeros))))
↳ [67, 64, 32, 17, 16, 8, 4, 2, 0]

Função takewhile(função, iteravel): retorna todos os termos iniciais de um iterável até que um de seus elementos avalie como False pela função. Os demais elementos, mesmo que avaliando como True, são descartados.

» anos = [10,30,50, 70, 90, 110, 1, 2, 3]
» take = it.takewhile(lambda x: x < 100, anos)
» for a in take:
»     print(a, end= ' ')
↳ 10 30 50 70 90 

Função itertools.zip_longest(iter1, iter2): diferente da função interna zip(), zip_longest(iter1, iter2) retorna um iterador com o comprimento da maior entre iter1, iter2, substituindo valores ausentes por None.

» # usando zip (built-in)    
» x = [0, 1, 2, 3, 4, 5]
» y = ['A', 'B', 'C']
» list(zip(x, y))
↳ [(0, 'A'), (1, 'B'), (2, 'C')]

» # usando itertools.zip_longest
» list(it.zip_longest(x, y))
↳ [(0, 'A'), (1, 'B'), (2, 'C'), (3, None), (4, None), (5, None)]

Iteradores infinitos

Função count(): a funçãocount() produz uma sequência de comprimento indefinido de inteiros, apropriada para contagem.

» import itertools as it
» t = 0
» for x in it.count():
»     if x > 10: break
»     print(x, end=' ')
↳ 0 1 2 3 4 5 6 7 8 9 10    

Se uma condição de parada do loop não for inserida ele gera uma quantidade indefinida de números. A função pode receber os parâmetroscount(inicio, passo), para indicar o início da contagem é o incremento.

» for i in it.count(10, 2):
»     if i > 24:
»         break
»     else:
»         print(i, end=' ')
↳ 10 12 14 16 18 20 22 24

Um iterador não retorna elementos indexados, o que significa que não podemos tomar apenas uma fatia (ou slice). No entanto podemos usar a função itertools.isslice().

Função isslice(iterador, n): Retorna os primeiros n elementos do iterador.

» quadrados = (n**2 for n in it.count())
» list(it.islice(quadrados, 10))
↳ [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

Outra abordagem possível é usar itertools.takewhile (ou itertools.dropwhile) para fazer filtragens:

» algunsQuadrados = (n**2 for n in it.count(25, 5))
» list(it.takewhile(lambda x: x<2000, algunsQuadrados))
↳ [625, 900, 1225, 1600]

Função repeat(objeto, n): retorna o objeto repetido n vezes, em um iterável. Se n for omitido (o que é o mesmo que passar n=None) o ciclo se repete indefinidamente.
Função cycle(objeto): é o mesmo que repeat() retornando o objeto um número indefinido de vezes.

»  função repeat
» for i in it.repeat([1,2,3], times = 3):
»     print(i, end = ' ')
↳ [1, 2, 3] [1, 2, 3] [1, 2, 3]

» função cycle
» l = ['Far', 'West', 'Wing']
» i = 0
» for t in it.cycle(l):
»     if i < 5:
»         print(t, end=' ')
»     else:
»         break
»     i += 1
↳ Far West Wing Far West 

Funções Geradoras

Como sabemos, uma função do Python é executada de forma sequencial e sem interrupções, até que uma instrução return seja encontrada. Nada depois disso é executado. (Lembrando: uma função sem return retorna None.)

» def exibeLinhas():
»     print('linha 1')
»     print('linha 2')
»     print('linha 3')

» exibeLinhas()
↳  linha 1
↳  linha 2
↳  linha 3

Instrução yield: É possível pausar uma função no meio de sua execução, retomando depois no ponto de pausa. Com isso é possível criar funções que agem como um iterador. Uma função contendo pelo menos uma instrução yield, é chamada uma função geradora. Geradores são uma generalização de iteradores que produzem seus dados apenas sob demanda, sendo por isso chamados de lazy (prequiçosos).

» def funcaoGeradora():
»     print('Linha 1 é retornada')
»     yield 1
»     print('Linha 2 é retornada')
»     yield 2
»     print('Linha 3 é retornada')
»     yield 3

» gera = funcaoGeradora()
» print(gera)
↳ <generator object funcaoGeradora at 0x7f69643c8dd0>
» print(next(gera))
↳ Linha 1 é retornada
↳ 1
» print(next(gera))
↳ Linha 2 é retornada
↳ 2
» print(next(gera))
↳ Linha 3 é retornada
↳ 3
» print(next(gera)) # um erro é lançado "StopIteration"

Esse tipo de função retorna um objeto gerador que é, em termos de comportamento, um iterador. A iteração sobre esse objeto é feita com a função interna next(). Quando o iterador está esgotado o uso de next() retorna uma exceção.

yield pode ser usado para retornar qualquer valor no iterador, inclusive None, se um valor for omitido. yield substitui a instrução return que, se usada, interrompe o ciclo do iterador. Uma função geradora pode ser transformada, com todos os seus valores, em uma lista, com list(geradora).

» def geradora():
»     for i in range(10):
»         yield i

» g = geradora()
» while True:
»     try:
»         print(next(g), end=' ')
»     except:
»         break
↳ 0 1 2 3 4 5 6 7 8 9

» g = geradora() # para repopular o iterador
» for i in g:
»     print(i, end=' ')
↳ 0 1 2 3 4 5 6 7 8 9

» g = geradora()
» lista = list(g)
» print(lista)
↳ [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

A função geradora só pode ser percorrida uma vez, ficando esgotada no final. Como mostrado no último bloco de código, da mesma forma que uma função geradora pode ser percorrida com next(), o iterador por ela gerado pode ser percorrido em um loop for.

Relembrando: Uma classe pode retornar um objeto iterável por meio dos métodos especiais __iter__() e __next__(). O código next(g) é equivalente à chamar o método interno do objeto iterável, g.__next__().

» # class iteradora
» class Quadrados:
»     def __init__(self, quantos):
»         self.quantos = quantos
»         self.atual = 0​

»     def __iter__(self):
»         return self

»     def __next__(self):
»         quad = self.atual ** 2
»         self.atual += 1
»         if self.atual > self.quantos:
»             raise StopIteration
»         return quad

» q = Quadrados(11)
» for s in q:
»     print(s, end=', ')
↳ 0, 1, 4, 9, 16, 25, 36, 49, 64, 81, 100, 

As funções geradoras podem conseguir o mesmo resultado de forma mais compacta. As operações de iteração, de obter o elemento seguinte e a parada por exceção são automaticamente fornecidas. Elas retornam um objeto gerador, que é um iterável.

» def quadrados(quantos):
»     for n in range(quantos):
»         yield n**2

» q = quadrados(11)
» for s in q:
»     print(s, end=', ')        
↳ 0, 1, 4, 9, 16, 25, 36, 49, 64, 81, 100,

Uma função geradora pode guardar o estado em que terminou a última chamada até ser chamada novamente. Por exemplo, podemos contruir uma geradora para exibir os elementos da sequência de Fibonacci, indefinidamente.

» # Ex.: geradora da sequência Fibonacci
» def fibonacci():
»     a, b = 1, 1
»     while True:
»         yield a
»         a, b = b, a + b
        
» # inicializamos um objeto da sequência
» f = fibonacci()
» # as 10 primeiras iterações resultam em
» for i in range(10):
»     print(next(f), end=' ')
↳ 1 1 2 3 5 8 13 21 34 55 

» # as 10 iterações seguintes
» for i in range(10):
»     print(next(f), end=' ')
↳ 89 144 233 377 610 987 1597 2584 4181 6765 

Podemos também calcular quantos números primos se desejar, desde que o computador tenha capacidade de processamento e memória para isso.

» def numerosPrimos():
»     num = 2
»     yield num
»     while True:
»         num += 1
»         primo = True
»         for i in range(2, int(num/2)):
»             if(num % i) == 0: # achou um divisor
»                 primo = False # logo não é primo
»                 break
»         if primo:
»             yield num

» for p in numerosPrimos():
»     print(p, end=' ')
»     if p > 100:
»         break
↳ 2 3 4 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97 101                 

Os dois últimos exemplos ilustram o fato de que a função geradora armazena seu estado entre as chamadas e os retornos com yield. Como já vimos o loop for cuida das chamadas a next() e o encerramento do laço, quando existir.

Método send(): Outra característica importante dos geradores é a possibilidade de interagir com o código do iterador. Um valor pode ser passado para yield através do método send(). Em outras palavras, além de retornar um valor calculado dentro da função geradora yield recebe o valor passado por send(), que funciona como next() mas passa um valor para yield.

» # definimos uma função geradora com send
» def usandoSend():
»     while True:
»         recebido = yield
»         print('Recebido =', recebido)

» f = usandoSend()    # inicializamos um objeto gerador
» next(f)             # a primeira interação deve ser feita para a entrada no iterador

» # passando integer 0 para yield
» f.send(0)
↳ Recebido = 0
» # passando string para yield
» f.send('palavra')
↳ Recebido = palavra
» # uma iteração send envia None para yield
» next(f)
↳ Recebido = None

Com essa funcionalidade o código que chama o iterador pode modificar o comportamento da função e, portanto, dos valores retornados. No exemplo abaixo o gerador retorna inteiros, de 1 em 1, exceto se um incremento extra for enviado por meio de send().

» # definimos uma função geradora com send
» def usandoSend():
»     n = 0
»     while True:
»         n +=1
»         incremento = yield n
»         if recebido:
»             n += incremento

» # inicializamos o gerador
» f = usandoSend()    # inicializamos um objeto gerador
» # os 4 primeiros outputs são
» print(next(f), next(f), next(f), next(f))
↳ 1 2 3 4
» # incremento 4 primeiros outputs são
» p = f.send(10)
» print(p)
↳ 15
» # os 4 próximos outputs são
» print(next(f), next(f), next(f), next(f))
↳ 16 17 18 19

O exemplo seguinte usa send() e será usado na próxima seção para ilustrar o uso de throw().

» # definimos um gerador
» def gerador_letras(texto):
»     local = 0
»     while True:
»         mandou = yield texto[local]
»         if mandou:
»             local = mandou
»         else:
»             local += 1

» # inicializamos um gerador            
» let = gerador_letras('Apocalipse Zumbi')
» next(let)
↳ 'A'
» # enviamos via send
» let.send(11)
↳ 'Z'
» next(let)
↳ 'u'

Método throw(): throw() permite que se lance uma exceção no ponto onde o gerador foi interrompido, na última execução. Ele ativa a exceção e retorna o valor seguinte (como faria yield), ou uma StopIteration se o gerador estiver esgotado. A exceção deve ser tratada dentro da função geradora, caso contrária será repassada para o código que chamou a função. A função abaixo permite que se veja em que ponto está a execução do gerador, usando throw().

» # definindo gerador com tratamento para exceção
» def gerador_letras(texto):
»     local = 0
»     while True:
»         try:
»             mandou = yield texto[local]
»         except Exception:
»             print(f'Exceção atingida na posição {local}')
»         if mandou:
»             local = mandou
»         else:
»             local += 1

» let = gerador_letras('Sei que nada será como antes')
» print(next(let), next(let))
↳ S e

» # pula a posição para 4
» let.send(4)
↳ 'q'

» # levanta exceção (que, no caso, mostra posição atual no gerador)
» let.throw(Exception)
↳ Exceção atingida na posição 4
↳ 'q'

» print(next(let), next(let))
↳ u e
  • throw(exceção): permite que se envie para o iterador qualquer tipo de exceção,
  • close(): fecha o iterador e levanta a exceção GeneratorExit.
» # fechando o iterador
» f.close()
» f.send('palavra')   # uma exceção é lançada
↳ StopIteration

yield from: permite que um gerador chame outros, de forma sequencial. No exemplo abaixo o gerador_principal esgota primeiro o gerador1, depois o gerador2, em sequência.

» # geradores de geradores
» def gerador1():
»     yield 'Linha 1 do gerador 1'
»     yield 'Linha 2 do gerador 1'​

» def gerador2():
»     yield 'Linha 1 do gerador 2'
»     yield 'Linha 2 do gerador 2'

» def gerador_principal():
»     yield from gerador1()
»     yield from gerador2()

» delegando = gerador_principal()
» print(next(delegando))
» print(next(delegando))
» print(next(delegando))
» print(next(delegando))
↳ Linha 1 do gerador 1
↳ Linha 2 do gerador 1
↳ Linha 1 do gerador 2
↳ Linha 2 do gerador 2

» # o mesmo resultado seria obtido com
» delegando = gerador_principal()
» for i in delegando:
»     print(i)    # linhas de output omitidas

Observação: Alguns objetos built-in são geradores e têm internamente implementados os métodos yield, next, iter, StopIteration . São eles:
range, dict.items, zip, map e File Objects.

A conjectura de Collatz

O exemplo de função geradora a seguir ilustra um problema interessante na matemática, denominado conjectura de Collatz. Collatz se perguntou se uma operação aritmética simples repetida sobre números inteiros positivos produziria sempre uma sequência terminada em 1. A sequência geralmente considerada é a seguinte: iniciando com um inteiro n cada termo da sequência é gerado da seguinte forma. Se o número é par o seguinte é n/2; se o número é impar o seguinte é 3n + 1. Para todos os inteiros testados a conjectura é verificada. Por exemplo, começando com n=7 temos: {7, 22, 11, 34, 17, 52, 26, 13, 40, 20, 10, 5, 16, 8, 4, 2, 1}. No entanto até hoje não foi possível provar o caso geral e o problema permanece em aberto.

Para gerar a sequência vamos construir um iterador que recebe um parâmetro n, o número inicial da sequência. Observe que a sequência seria infinita sem um corte pois 1 ⟶ 4 ⟶ 2 ⟶ 1, um loop infinito. Portanto a interrompemos manualmente quando ela atinge 1. O iterador é usado pela função collatz(n) que retorna a sequência inteira e quantas iterações foram feitas (protanto quantos elementos existem na sequência).

» # sequência de Collatz
» def iterador(numero):
»     while True:
»         yield numero
»         numero = int(numero/2 if numero%2==0 else 3 * numero + 1) 

» def collatz(n):
»     itt = iterador(n)
»     quantos = 0

»     while True:
»         quantos += 1
»         val = next(itt)
»         print(val, end=' ')
»         if val == 1:
»             print(f'\nFinal alcançado em {quantos} iterações!')
»             break

» collatz(7)
↳ 7 22 11 34 17 52 26 13 40 20 10 5 16 8 4 2 1 
↳ Final alcançado em 17 iterações!
Sequência de Collatz para n = 7

Geradores e gerenciamento de memória


Geradores são uma forma prática de interagir com grande volume de dados sem esgotar a memória do computador. Eles permitem que um grande volume de dados possam ser acessados em blocos, por partes. Cada bloco pode ser retornado sob demanda, enquanto o gerador armazena seu estado entre cada chamada.

Bibliografia

Tabela de cores Html e CSS


As bibliotecas gráficas do Python, Matplotlib, Seaborn e Bokeh aceitam cores no sistema css e a maioria dos nomes ou shortcuts. O código hexadecimal é formado por 3 números, de 0 até 255 (ou 00 até FF), para daterminar a intensidade das cores R, G, B (vermelho, verde, azul). Dessa forma FFFFFF representa (255, 255, 255), ou branco; 000000 representa (0, 0, 0) ou preto. FF0000 é vermelho puro, 00FF00 é verde puro, 0000FF é azul puro. Além disso se pode utilizar o parâmetro alpha=a, onde 0 ≤ a ≤1 para a opacidade da cor, sendo 1 opacidade máxima.

Cores podem ser definidas como:

  • no formato hexadecimal (como #123456), com strings do CSS4 rgb(), rgba() ou hsl(): como rgb(0 127 0 / 1.0), rgba(255, 0, 127, 0.6) ou hsl(60deg 100% 50% / 1.0).
  • como tupla de inteiros (r, g, b), onde r, g, b são inteiros entre 0 e 255.
  • como a tuple (r, g, b, a), onde r, g, b são inteiros entre 0 e 255 e a é número de ponto flutuante entre 0 e 1.
  • um inteiro sem sinal representando RGBA values no padrão 0xRRGGBBAA, 0xffff00ff ou 0xff0000ff.

Segue uma tabela com as cores em código hexadecimal e seu nome.

Cor Hexadecimal Html/CSS Cor Hexadecimal Html/CSS Cor Hexadecimal Html/CSS
#000000 Black #000080 Navy #00008B DarkBlue
#0000CD MediumBlue #0000FF Blue #006400 DarkGreen
#008000 Green #008080 Teal #008B8B DarkCyan
#00BFFF DeepSkyBlue #00CED1 DarkTurquoise #00FA9A MediumSpringGreen
#00FF00 Lime #00FF7F SpringGreen #00FFFF Aqua
#00FFFF Cyan #191970 MidnightBlue #1E90FF DodgerBlue
#20B2AA LightSeaGreen #228B22 ForestGreen #2E8B57 SeaGreen
#2F4F4F DarkSlateGray #2F4F4F DarkSlateGrey #32CD32 LimeGreen
#3CB371 MediumSeaGreen #40E0D0 Turquoise #4169E1 RoyalBlue
#4682B4 SteelBlue #483D8B DarkSlateBlue #48D1CC MediumTurquoise
#4B0082 Indigo #556B2F DarkOliveGreen #5F9EA0 CadetBlue
#6495ED CornflowerBlue #663399 RebeccaPurple #66CDAA MediumAquaMarine
#696969 DimGray #696969 DimGrey #6A5ACD SlateBlue
#6B8E23 OliveDrab #708090 SlateGray #708090 SlateGrey
#778899 LightSlateGray #778899 LightSlateGrey #7B68EE MediumSlateBlue
#7CFC00 LawnGreen #7FFF00 Chartreuse #7FFFD4 Aquamarine
#800000 Maroon #800080 Purple #808000 Olive
#808080 Gray #808080 Grey #87CEEB SkyBlue
#87CEFA LightSkyBlue #8A2BE2 BlueViolet #8B0000 DarkRed
#8B008B DarkMagenta #8B4513 SaddleBrown #8FBC8F DarkSeaGreen
#90EE90 LightGreen #9370DB MediumPurple #9400D3 DarkViolet
#98FB98 PaleGreen #9932CC DarkOrchid #9ACD32 YellowGreen
#A0522D Sienna #A52A2A Brown #A9A9A9 DarkGray
#A9A9A9 DarkGrey #ADD8E6 LightBlue #ADFF2F GreenYellow
#AFEEEE PaleTurquoise #B0C4DE LightSteelBlue #B0E0E6 PowderBlue
#B22222 FireBrick #B8860B DarkGoldenRod #BA55D3 MediumOrchid
#BC8F8F RosyBrown #BDB76B DarkKhaki #C0C0C0 Silver
#C71585 MediumVioletRed #CD5C5C IndianRed #CD853F Peru
#D2691E Chocolate #D2B48C Tan #D3D3D3 LightGray
#D3D3D3 LightGrey #D8BFD8 Thistle #DA70D6 Orchid
#DAA520 GoldenRod #DB7093 PaleVioletRed #DC143C Crimson
#DCDCDC Gainsboro #DDA0DD Plum #DEB887 BurlyWood
#E0FFFF LightCyan #E6E6FA Lavender #E9967A DarkSalmon
#EE82EE Violet #EEE8AA PaleGoldenRod #F08080 LightCoral
#F0E68C Khaki #F0F8FF AliceBlue #F0FFF0 HoneyDew
#F0FFFF Azure #F4A460 SandyBrown #F5DEB3 Wheat
#F5F5DC Beige #F5F5F5 WhiteSmoke #F5FFFA MintCream
#F8F8FF GhostWhite #FA8072 Salmon #FAEBD7 AntiqueWhite
#FAF0E6 Linen #FAFAD2 LightGoldenRodYellow #FDF5E6 OldLace
#FF0000 Red #FF00FF Fuchsia #FF00FF Magenta
#FF1493 DeepPink #FF4500 OrangeRed #FF6347 Tomato
#FF69B4 HotPink #FF7F50 Coral #FF8C00 DarkOrange
#FFA07A LightSalmon #FFA500 Orange #FFB6C1 LightPink
#FFC0CB Pink #FFD700 Gold #FFDAB9 PeachPuff
#FFDEAD NavajoWhite #FFE4B5 Moccasin #FFE4C4 Bisque
#FFE4E1 MistyRose #FFEBCD BlanchedAlmond #FFEFD5 PapayaWhip
#FFF0F5 LavenderBlush #FFF5EE SeaShell #FFF8DC Cornsilk
#FFFACD LemonChiffon #FFFAF0 FloralWhite #FFFAFA Snow
#FFFF00 Yellow #FFFFE0 LightYellow #FFFFF0 Ivory
#FFFFFF White

Matplotlib


Quando lidamos com dados precisamos, muitas vezes, visualizar de forma gráfica esses dados. Em muitas tarefas é útil, ou até mesmo essencial, que as conclusões das análises sejam mostradas através de gráficos. Mesmo nas fases iniciais de uma análise, na preparação e limpeza de dados, a visualização é importante visualizar para se compreender padrões, tendências e anomalias, tais como pontos fora da curva. Existem no Python inúmeras bibliotecas para visualização de dados e montagem de gráficos. Matplotlib é o módulo básico para uso em conjunto com o pandas.

A biblioteca é grande, com extensas possibilidades e tem sido usada como base para a elaboração de outros módulos gráficos, como o Seaborn. O aprendizado da biblioteca inteira pode demandar um esforço considerável mas o uso básico, suficiente para muitos projetos, não demanda tanto empenho. Além disso o pandas tem uma vinculação natural com a biblioteca, como veremos.

Instalação

Matplotlib é instalado junto com a distribuição do python via Anaconda. Se você não está usando Anaconda é possível encontrar maiores instruções de instalação no site de Matplotlib.

Um pouco de história

Matplotlib começou a ser desenvolvida em 2003 por John D. Hunter, um neurocientista que usava MATLAB e queria aperfeiçoar a visualização de dados obtidos por meio de EEGs (eletroencelografia) em sua pesquisa sobre epilepsia. Hoje uma a comunidade de desenvolvedores colaboram para manter e aperfeiçoar a biblioteca.

Como muitos usuários e desenvolvedores estavam acostumados ao ambiente do MATLAB, onde todas as funções estão disponíveis globalmente sem a necessidade de importações, o módulo pylab foi desenvolvido. Ele existe para trazer funções e classes do NumPy e matplotlib para o namespace global. Isso significa que o comando from pylab import * em uma sessão significa a importação desses módulos e é desnecessária para quem está acostumado com o estilo do python. Como já vimos a importação de muitos módulos, funções e classes pode provocar conflito entre as partes importadas e os métodos built-in do python.

De fato, o uso de ipython --pylab para quem usa o comando de linha, ou %pylab de dentro do Jupyter, simplesmente faz uma chamada interna à from pylab import *. Nesse sentido se recomenda, para quem trabalha com IPython e Jupiter Notebook, que se use a “mágica” %matplotlib.

Por todos esses motivos usaremos a abordagem usual do python:

» import matplotlib.pyplot as plt
» import numpy as np
» np.random.seed(444)
» # para exibição dos gráficos no ambiente do jupyter notebook usamos
» %matplotlib
» # para exibição incorporada dentro do notebook
» %matplotlib inline
» # ou, para exibição dentro do notebook, com controles de zoom e arraste
» %matplotlib notebook

Numpy será usado para as contruções de arrays e geração de dados aleatórios. A informação de np.random.seed() serve para que os geradores produzam os mesmos números em seções posteriores, para reproducibilidade. A mágica %matplotlib faz com que os gráficos sejam exibidos. Nesse caso uma nova janela é aberta com uma barra de menus com acesso à ampliação, arraste, gravação em vários formatos (como pdf, jpg, png), e parâmetros do gráfico. Já a inserção de %matplotlib inline faz com que os gráficos fiquem embutidos no próprio notebook e sejam gravados com ele. A janela de controle não aparece. Usando %matplotlib notebook temos o gráfico embutido com acesso à controles de zoom, arraste e gravação em arquivo.

Técnica Básica

O matplotlib pode receber como fonte de dados listas, arrays do numpy, Series e dataframes do pandas. Por exemplo, o código seguinte recebe listas gera as figuras 1 e 2 abaixo:

» %matplotlib inline
» plt.plot([0,3,0,5,0,7,0])
» # a figura 1 é gerada

» plt.plot([0,1,2,3,4,5,6],[0,3,0,5,0,7,0])
» # o mesmo que antes (figura 1 é gerada)

» x = np.arange(101)-50
» y = x**2
» plt.plot(x,y)
» # a figura 2 é gerada

Quando apenas uma lista é fornecida plot usa os índices como coordenada horizontal (abcissa) e os valores da lista como coordenadas verticais (ordenadas). Quando duas listas de mesmo tamanho são fornecidas a primeira é usada para os valores das abcissas, a segunda como ordenadas. No segundo exemplo, que gera a figura 2, foram usadas coordenadas (x, x2) (uma parábola) com x variando no intervalo [-50, 50].

Usando %matplotlib notebook o gráfico é exibido inline mas trazendo controles de ajuste da imagem. Ao se clicar no botão azul (pode ter outra cor na sua instalação) os controles desaparecem e a imagem fica estática.

» %matplotlib notebook
» x = np.arange(40)
» plt.plot(x,np.exp(x/10))    # exibe a figura 3

» # outro exemplo: seno
» # dados a plotar 
» x = np.arange(0.0, 2.0, 0.01)
» y = np.sin(2 * np.pi * x)

» fig, ax = plt.subplots()
» ax.plot(x, y)

» ax.set(xlabel='eixo x', ylabel='y = seno(x)', title='Gráfico de seno(2 pi x)')
» ax.grid()

» fig.savefig('seno.jpg')
» plt.show()                  # exibe a figura 4

No primeiro caso plotamos simplesmente o gráfico de y = exp(x/10). No segundo exemplo criamos um array no intervalo [0, 2) em passos de .01. A coordenada é y = sen(2πx). Usamos as funções exponencial e seno do numpy para lidar com a operação vetorializada (que pode ser aplicada sobre todo o array). Já veremos com maiores detalhes os métodos de matplotlib.pyplot.

Hierarquia de objetos do Matplotlib

Mesmo em exemplos simples, como os anteriores, Matplotlib usa uma hierarquia de objetos. Por hierarquia se entende que objetos dependem de outros, como em ramos de uma árvore. Repetindo o gráfico da figura 4 temos:

†: O termo axes do matplotplib não se refere a “eixos” e sim a “figuras” individuais, dentro de uma Figure.

Figure é o objeto básico ou o mais externo de um gráfico. Ele pode conter diversos Axes, que são gráficos ou plotagens individuais. Axes, por sua vêz, podem conter legendas, marcas gráficas, curvas e caixas de texto. Cada um desses elementos são objetos do python com métodos e propriedades que podem ser manipuladas individualmente.

Vamos verificar em código como esses objetos são criados e manipulados.

» # geramos os dados a imprimir
» x = np.arange(0.0, 2.0, 0.01)
» y = np.sin(2 * np.pi * x)

» # O objeto básico do matplotlib.pyplot (aliás plt) é figure
» fig = plt.figure()
» # De figure derivamos um axes (subplot) e plotamos 3 curvas
» ax = fig.add_subplot()

» ax.plot(x, y)
» ax.plot(x, x)
» ax.plot(x, x**2)

» # definimos os labels dos eixos x e y e o título do gráfico
» ax.set(xlabel='eixo x', ylabel='seno(x), x, x^2',
»        title='y = seno, reta e parábola')
» # acrescentamos um quadriculado (grid)
» ax.grid()

» # salvamos a figura no disco
» fig.savefig('seno.jpg')
» # exibimos o resultado
» plt.show()

Ao objeto ax acrescentamos 3 plots (y=sen(2πx), y=x, y=x2), os labels dos eixos, o título do gráfico e o quadriculado de fundo. Opcionalmente a figura pode ser salva. A figura só é exibida quando plt.show() é executado.

O método add_subplot admite diversos parâmetros: fig.add_subplot(m,n,r) significa criar um gráfico em m linhas, n colunas, na posição r. Um subplot significa dividir a região destinada ao gráfico em m×n partes onde se pode colocar sub-gráficos.

Por ex., vamos criar uma figura com 4 subplots, plotando uma curva diferente em cada uma delas.

» x = np.arange(0.0, 2.0, 0.01)-1
» fig = plt.figure()

» ax1 = fig.add_subplot(2,2,1)               # 2 linhas, 2 colunas: a 1ª figura
» ax2 = fig.add_subplot(2,2,2)               # a 2ª figura
» ax3 = fig.add_subplot(2,2,3)
» ax4 = fig.add_subplot(2,2,4)

» ax1.plot(x, np.sin(10*x))
» ax2.plot(x, x)
» ax3.plot(x, x**2)
» ax4.plot(x, x**3)
» fig.savefig('figura5.jpg')

» plt.show()

A mesma figura pode ser obtida fazendo os plots diretamente para os axes:

» fig = plt.figure()
» ax1 = fig.add_subplot(2, 2, 1)
» plt.plot([-1, 0, 1, 2])
» ax2 = fig.add_subplot(2, 2, 2)
» ax3 = fig.add_subplot(2, 2, 3)
» plt.plot(np.random.randn(50).cumsum(), 'k--')
» ax4 = fig.add_subplot(2, 2, 4)
» plt.plot([1.5, 3.5, -2, 1.6])

O método plt.plot(dados) se refere ao eixo ativo que é aquele criado ou usado por último. No caso acima nenhuma figura foi plotada no 2 º retângulo.

O procedimento de criar vários subplots dentro de um mesmo gráfico pode resumido por meio do método
fig, axes = plt.subplots().
subplots() retorna uma tupla onde o 1&orm; elemento é uma Figure, o objeto básico de um plot, e o 2º são os axes que recebem as curvas.

Esses axes podem ser referenciados individualmente pela notação de array. Por ex.: em
fig, axes = plt.subplots(2, 3)
temos axes[0,0] até axes[1,2].

np.random.randn(50).cumsum()
retorna a soma cumulativa dos elementos de um array de 50 elementos “aleatórios”.

pyplot.subplots possui as opções:

nrows número de linhas
ncols número de colunas
sharex todos os subplots devem ter os mesmos “ticks” no eixo x
sharey todos os subplots devem ter os mesmos “ticks” no eixo y
subplot_kw dicionário de chaves para criar cada subplot
**fig_kw chaves adicionais, como plt.subplots(2,2,figsize=(8,6))

Formatação dos gráficos

Tamanho

O tamanho de uma figura, por default dada em polegadas, é definido pelo parâmetro figsize em plt.figure(figsize=(largura, altura)).

» x = np.arange(.1, 10, 0.01)
» largura = 5; altura = 2
» plt.figure(figsize=(largura, altura))
» plt.plot(x, np.log(x))
» plt.show()                       # gerado o gráfico na figura 8

O mesmo gráfico é gerado usando esse parâmetro no construtor da figure, mas desenhado com linhas pontilhadas, devido ao parâmetro ‘k–‘ em ax.plot(x, np.log(x), 'k--').

» x = np.arange(.1, 10, 0.01)
» fig = plt.figure(figsize=(5, 2))
» ax = fig.add_subplot()
» ax.plot(x, np.log(x), 'k--')
» plt.show()                       # gerado o gráfico na figura 9

Espaçamento entre subplots, cores e marcadores

O espaçamento entre figuras de um gráfico com subplots, que por default é um espaço não nulo, pode ser ajustado por meio do método Figure.subplots_adjust(). Por conveniência o mesmo método pode ser acessado diretamente pela função:
subplots_adjust((left=None, bottom=None, right=None, top=None, wspace=None, hspace=None).
wspace e hspace indica quanto espaço percentual em relação à largura e altura da figura, respectivamente.
Por exemplo, para juntar os subplots fazemos ambos igual a zero.

» x = np.arange(.1, 10, 0.01)
» fig, axes = plt.subplots(2, 2)
» axes[0, 0].hist(np.random.randn(1000), bins=100, alpha=.5)
» axes[0, 0].hist(np.random.randn(500), bins=50, color='r', alpha=.5)
» axes[0, 1].hist(np.random.randn(500), bins=50, color='gold', alpha=1)
» axes[1, 0].hist(np.random.randn(500), bins=50, color='#ff0000')
» axes[1, 1].plot(x, 3*np.log(x), color='#55aaff')
» axes[1, 1].plot(x, x, color='#000000')
» plt.subplots_adjust(wspace=0, hspace=0)


Para usar os mesmos eixos em todos os 4 gráficos usamos os parâmetros sharex, sharey, em
fig, axes = plt.subplots(2, 2), sharex=True, sharey=True).

Em hist(np.random.randn(1000), bins=100) traçamos o histograma de dados aletórios (100 números) separados em 100 bins. No 1º axes traçamos 2 histogramas, o 1º com a cor azul default, o 2º com color=’r’, um atalho para ‘red’ ou vermelho, com transparência de 50%, alpha=.5. Uma lista de cores nomeadas, como color=’gold’ pode ser encontrada no site do matplotlib. Também podemos usar o código de cores html que consiste em 3 números hexadecimais de 0 até 255 (ou 00 até ff em hexadecimal), no sistema rgb (vermelho, verde, azul). Diversos editores de imagens ou de código disponilizam um seletor de cores que retorna a cor nesse sistema. O site HTML-COLOR.CODES também tem um seletor online.


O método axes.plot(), além de aceitar arrays para abcissas e coordenadas, pode receber também o string especificador de cor e tipo de linha. Para imprimir em verde (‘green’) com linha tracejada usamos
ax.plot(x, y, 'g--'),
que é uma forma resumida de passar parâmetros. Isso é o mesmo que:
ax.plot(x, y, linestyle='--', color='g').
Considerando que x e y são arrays de mesmo tamanho, alguns exemplos desses parâmetros são:

» plot(x, y)            # plot x, y com linha e estilo default
» plot(x, y, 'bo')      # plot x, y com marcadores azuis circulares
» plot(x, y, 'rv')      # plot x, y com marcadores vermelhos, triângulo para baixo
» plot(y)               # plot y usando seus índices como coordenadas-x
» plot(y, 'r+')         # idem, usando cruzes vermelhas

No Jupyter Notebook use plt.plot? para ver uma lista completa dos parâmetros desse método.

Ao desenhar um gráfico pode ser interessante marcar os pontos sobre as curvas contínuas. Isso é feito com markers ou marcadores.

» from numpy.random import randn
» plt.plot(randn(30).cumsum(), color='green', linestyle='dashed', marker='o')

Uma forma abreviada para o mesmo comando é: plt.plot(randn(30).cumsum(), 'go--'), onde os parâmetros são passados em uma única string, com g para green (verde), o para o marcador circular e -- para o estilo de linha tracejado.

Por default os pontos de um plot são ligados por linhas. Para outro estilo usamos drawstyle:

» data = np.random.randn(20).cumsum()
» plt.plot(data, 'b--', label='default', marker='v')                      # linha azul
» plt.plot(data, 'r-', drawstyle='steps-post', label='passos')            # linha vermelha
» plt.legend(loc='best')


A linha azul é tracejada (‘b–‘), no estilo default e com marcadores ‘v’ (triângulos). O parâmetro label cria legendas, nesse caso indicando o texto ‘defaul’. A linha vermelha tem estilo drawstyle=’steps-post’ (em passos) e é marcada na legenda como ‘passos’. plt.legend(loc='best') informa que o melhor local para colocar essa legenda seja encontrado automaticamente. Outros valores seriam: loc='right', 'center', 'upper right', etc.

Marcas, etiquetas e legendas (ticks, labels, legends)

» dados = np.random.randn(1000)
» cumulativo = dados.cumsum()

» fig = plt.figure()
» ax = fig.add_subplot(1,1,1)
» ax.plot(10*dados + 10)
» ax.plot(cumulativo)


As duas plotagens são sobrepostas no único axes criado. O operação 10*dados+10 serve apenas para efeito estético da apresentação dos dados aleatórios.

Vamos usar os mesmos dados para verificar as propriedades de ajuste do título global do gráfico, labels nos eixos x e y, label do gráfico e ticks para melhorar a apresentação do gráfico anterior.

» fig = plt.figure()
» ax = fig.add_subplot(1,1,1)
» ax.set_title('Alterando eixos com matplotlib')
» ticks = ax.set_xticks([0, 250, 500, 750, 1000])
» ticks = ax.set_yticks([-20, 0, 20, 40, 60, 80])

» labels = ax.set_xticklabels(['seg' ,'ter', 'qua', 'qui', 'sex'],
»                             rotation=45, fontsize='large')
» labels = ax.set_yticklabels(['-A' ,'O', 'A', 'B', 'C','D'],
»                             fontsize='large')

» ax.set_xlabel('Ao longo dos dias...')
» ax.set_ylabel('Observado')

» ax.plot(10*dados, color='#55aaff', alpha=.5, label='dados')
» ax.plot(cumulativo, color='red', label='cumulativo')
» ax.legend(loc='best')

Anotações e desenhos nos subplots.

Diversos tipos de anotações, setas e desenhos podem ser incluídos nos gráficos. Para traçar os gráficos seguintes vamos usar o arquivo .csv do Our World in Data, baixados para a pasta ./dados.

Primeiro importamos o arquivo baixado .csv para um dataframe. Esse arquivo contém dados dos países do mundo, nos anos de 1950 até 2099, contendo número de nascimentos verificados até 2020 e valores interpolados para os anos seguintes. O dataframe original tem o seguinte formato:

Em seguida selecionamos apenas os dados sobre o Brasil.

» dados = pd.read_csv('./dados/number-of-births-per-year.csv')
» dados=dados[(dados['Entity']=='Brazil')]
» dados.head(2)

» # as colunas 3 e 4 têm nomes longos, que vamos renomear
» dados.columns[3],dados.columns[4]
↳ ('Estimates, 1950 - 2020: Annually interpolated demographic indicators - Births (thousands)',
↳  'Medium fertility variant, 2020 - 2099: Annually interpolated demographic indicators - Births (thousands)')

» dados.rename(columns={'Year':'ano',
»                       dados.columns[3]:'nasc',
»                       dados.columns[4]:'inter'}, inplace=True)
» # copiamos os dados da coluna de interpolação, após 2020, para a colunas de nascimentos
» dados.loc[dados['nasc'].isna(), 'nasc'] = dados['inter']

» # vamos mantes apenas as colunas 'ano' e 'nasc'
» dados = dados[['ano', 'nasc']]
» dados.head()
↳         ano          nasc
↳ 4050    1950    2439820.0
↳ 4051    1951    2467186.0
↳ 4052    1952    2523577.0
↳ 4053    1953    2583285.0
↳ 4054    1954    2646311.0

Para usar como anotações no gráfico encontramos os anos em que  nascimentos foram máximo e mínimo, além do ano em que se inicia a interpolação, 2020.

» maior=dados[dados['nasc']==dados['nasc'].max()]
» menor=dados[dados['nasc']==dados['nasc'].min()]

» ano_maior = maior['ano'].values[0]
» nasc_maior = int(maior['nasc'].values[0])

» ano_menor = menor['ano'].values[0]
» nasc_menor = int(menor['nasc'].values[0])

» interX = 2020   # início da interpolação
» interY = int(dados[dados['ano']==2020]['nasc'].values[0])

» txt = ('Máximo de nascimentos:\t {} no ano {}.\n'
»         'Mínimo de nascimentos:\t {} no ano {}.\n'
»         'Início da interpolação:\t {} no ano {}.'
»       )

» print(txt.format(nasc_maior, ano_maior,nasc_menor, ano_menor, interY, interX))
↳ Máximo de nascimentos:	 3929646 no ano 1983.
↳ Mínimo de nascimentos:	 1504597 no ano 2099.
↳ Início da interpolação:	 2859135 no ano 2020.

Com esses dados imprimimos o gráfico (sem muita preocupação estética). Uma primeira curva é traçada em preto, incluindo os anos de 1950 até 2020. A segunda curve se inicia em 2021 até o final e é tracejada em vermelho, para indicar a interpolação. Uma terceira curva tem efeito decorativo, em azul e transparente.

» fig = plt.figure()
» ax = fig.add_subplot()
» ax.set_title('Nascimentos (em milhões), por ano', size=18)
» ax.plot(dados[dados['ano']<2021]['ano'], dados[dados['ano']<2021]['nasc'], color='black', alpha=1)
» ax.plot(dados[dados['ano']>2020]['ano'], dados[dados['ano']>2020]['nasc'], 'r--')
» ax.plot(dados['ano'], dados['nasc'], 'b', linewidth=5, alpha=.2)
» ax.grid(color='grey', alpha=.3 )
» ax.annotate('Máximo', xy=(ano_maior, nasc_maior), size=13)
» ax.annotate('Mínimo', xy=(ano_menor, nasc_menor), size=13)
» ax.annotate('Interpolado', xy=(interX, interY), size=13)
» ax.arrow(ano_maior-10, nasc_maior, 10, 0)
» ax.arrow(interX-10, interY-10, 10, 10)            # a figura 17 abaixo é plotada


Para dar um zoom na figura podemos limitar as faixas de valores no eixo x e eixo y com ax.set_xlim(a,b) e ax.set_xlim(m,n), respectivamente, em torno do ponto de interesse. No exemplo fazemos um zoom em torno do ponto de máximo, obtendo o gráfico 18 acima.

» ax.set_xlim([1980, 1990])
» ax.set_ylim([3.76e6, 3.95e6])
» fig.get_figure()                                  # a figura 18 acima é plotada

Figuras sobre o plot

Diversas formas mais comuns estão disponíveis para inserção nos plots, e são chamadas de patches no matplotlib. Algumas delas estão diretamente em matplotlib.pyplot como retângulos, círculos e polígonos. Muitas outras estão em matplotlib.patches. Para traçar figuras construimos os patches com os métodos apropriados e os acrescentamos ao subplot usando ax.add_patch().

retângulo: plt.Rectangle((x, y), largura, altura), onde (x, y) são as coordenadas do ponto inferior esquerdo,
círculo: plt.Circle((x_0, y_0), raio), onde (x_0, y_0) são as coordenadas do centro,
polígono: plt.Polygon([[x_0, y_0], [x_1>, y_1],…, [x_n, y_n]).

No caso do polígono a área interna às retas que ligam os pontos é colorida.

» fig = plt.figure()
» ax = fig.add_subplot()
» retangulo = plt.Rectangle((0.2, 0.2), 0.6, 0.4, color='#aabbcc')
» circulo = plt.Circle((0.4, 0.6), 0.3, color='plum', alpha=0.3)
» poligono1 = plt.Polygon([[0.1, 0.1], [0.8, 0.7], [.3,.7], [0.6, 0.1]], color='turquoise', alpha=0.8)
» poligono2 = plt.Polygon([[0.2, 0.2], [0.8, 0.8]], color='red', alpha=0.8)
» ax.add_patch(retangulo)
» ax.add_patch(circulo)
» ax.add_patch(poligono1)
» ax.add_patch(poligono2)

Para o segundo “polígono” apenas dois pontos foram fornecidos e ele é representado pela reta (vermelha) que liga esses pontos.

Claro que gráficos mais elaborados podem ser montados com uma combinação de figuras como retas, polígonos, círculos, etc. No caso abaixo uma cor é escolhida “aleatoriamente” para plotar um série de 2 triângulos, um com um vértice em (0,1), outro em (1,0). Os dois outros vértices dos triângulos são coincidentes, e se deslocam sobre a reta (t,t) com t de 0 a 1, com espaçamento .1. A cor tem transparência alpha=.6 para que as cores apareçam em tons pastéis.

A função cor() retorna uma tupla (r,b,g) onde cada componente representa as cores vermelho, verde e azul, com valores de 0 até 1.

» def cor():
»     return (np.random.random(), np.random.random(), np.random.random())

» fig = plt.figure()
» ax = fig.add_subplot()
» for t in np.linspace(0,1,100):
»     c = cor()
»     poligono1 = plt.Polygon([[0, 1], [t, t], [t+.1, t+.1]], color=c, alpha=.6)
»     poligono2 = plt.Polygon([[1, 0], [t, t], [t+.1, t+.1]], color=c, alpha=.6)
»     ax.add_patch(poligono1)
»     ax.add_patch(poligono2)                       # a figura 20 é gerada


O código abaixo gera círculos de raios aleatórios, espalhados em torno da reta (t,t), afastados dela por uma variacão também aleatória.

» fig = plt.figure()
» ax = fig.add_subplot()
» plt.axis('equal')
» for t in np.linspace(0,1,100):
»     circulo = plt.Circle((t*np.random.random(), t*np.random.random()), np.random.random()/10, color=cor(), alpha=0.5)
»     ax.add_patch(circulo)
» ax.set_xlim([0, 1])
» ax.set_ylim([0, 1])
» plt.savefig('circulos.pdf')                       # a figura 21 é gerada

Ao final a figura gerada é gravada em disco com o formato “pdf”. Outros formatos podem ser escolhidos, como “jpeg”, “png”, “svg”, assim como a resolução em dots per inches, (dpi ), que tem default = 100, além da cor de fundo e bordas.

Configuração do matplotlib

Por padrão o matplotlib possui um esquema de cores e outros parâmetros, como largura e tipo de linhas, previamente definidos e voltados para plotar figuras prontas para publicação. No entanto, vários destes parâmetros podem ser personalizados através de ajustes nos valores globais tais como tamanho, espaçamento entre subplots, cores, família e tamanhos de fonte, estilos de grade, etc.

Uma forma de alterar esses padrões está no método plt.rc (parametro, opcoes) onde parametro é uma string com o nome do parâmetro que se quer modificar, e opcoes é uma sequência de argumentos de palavras-chaves com os novos valores.

Entre outras opções parametro pode ser figure, axis, xtick, ytick, grid, legend. As opções podem ser passadas de várias formas. O exemplo mostra como ajustar todas as figuras de uma sessão para o tamanho 20×15. Depois fazemos ajustes às fontes, usando um dicionário.

» # ajustar tamanho da figura
» plt.rc ('figure', figsize = (20, 15))

» # parâmetros associados às fontes, em um dicionário
» font_options = {'family' : 'monospace', 'weight' : 'bold', 'size' : 'small'}
» plt.rc ('font', **font_options)

Uma personalização mais ampla pode ser feita no arquivo de configurações. Para encontrar esse arquivo use os comandos:

» # no prompt do sistema digite
» python -c "import matplotlib; print(matplotlib.matplotlib_fname())"

» # de dentro do jupyter notebook (ou de qualquer ambiente em que você trabalhe):
» import matplotlib
» print(matplotlib.matplotlib_fname())
↳ /home/usuario/.anaconda3/lib/python3.8/site-packages/matplotlib/mpl-data/matplotlibrc

Esse output é relativo ao sistema e à distribuição que está sendo usada, lembrando que é possível existir mais de uma instalação em um computador. No caso mostrado está em uso o anaconda e jupyter no linux mint. Cada usuário pode encontrar um local diferente. Esse arquivo não deve ser editado diretamente mas copiado para a pasta home do usuário (no linux) com o nome .matplotlibrc. Desta forma ele será carregado durante a inicialização do pacote. A análise desse arquivo é uma boa forma de se conhecer as possibilidades na personalização, sendo que as opções estão comentadas.

As atuais configurações globais podem ser vistas com o comando

» import matplotlib as mpl
» print(mpl.rcParams)

matplotlib.rcParams é usado para alterar esses parâmetros, um de cada vez. matplotlib.rc pode alterar os valores default para vários parâmetros de um grupo específico, como tipos de lihnes, fontes, textos, etc.

» matplotlib.rcParams['lines.markersize'] = 20
» matplotlib.rcParams['font.size'] = '15.0'

matplotlib.rcdefaults() reseta todos os parâmetros para seus valores originais.

Usando matplotlib com o pandas

A própria biblioteca do pandas embute diversas funcionalidades do matplotlib, sem que esse tenha que ser carregado explicitamente. Isso significa que podemos criar gráficos sem passar por todas as etapas de sua construção.

Por exemplo, uma instância de Series possui o método series.plot().

» s1 = pd.Series(np.random.randn(100).cumsum())
» s1.plot(use_index=False)                          # grafico 22-a é plotado

» s2 = pd.Series([x**2 for x in np.arange(-10,10,.1)], index=np.arange(-10,10,.1))
» s2.plot()                                         # grafico 22-b é plotado


Na figura 21 o eixo x recebe valores dos índices da série, que por default vai de 0 até 99. Na segunda o índice que foi declarado é usado. Para evitar o procedimento de usar o índice como ordenada passamos o parâmetro series.plot(use_index=False).

Para um dataframe cada series correspondente a cada coluna é plotada separadamente. Abaixo construimos e plotamos um dataframe de quatro colunas, cada uma delas representando valores de um seno com frequências diferentes.

» s1 = pd.Series([np.sin(x) for x in np.arange(0,10,.1)])
» s2 = pd.Series([np.sin(2*x) for x in np.arange(0,10,.1)])
» s3 = pd.Series([np.sin(3*x) for x in np.arange(0,10,.1)])
» s4 = pd.Series([np.sin(4*x) for x in np.arange(0,10,.1)])

» df=pd.concat({'A': s1, 'B': s2, 'C': s3, 'D': s4} , axis=1)

» df.plot()                                                        # a figura 23 é plotada
» df.plot(color=['k','r','b','y'], alpha=.6, logx=True, grid=True) # a figura 24 é plotada


O dataframe df contém 4 colunas, cada uma com os valores de seno(πx), seno(2πx), seno(3πx), seno(4πx), com x variando de 0 a 10 em passos de 0,1. No segundo gráfico, figura 24, alguns parâmetros foram passados, como uma lista de cores, a existência de quadriculado (grid) e a instrução para usar uma escala logarítmica em x.

A instrução dataframe.plot() é um atalho para dataframe.plot.line() que representa como curvas os pontos passados. Outros parâmetros podem ser passados, exatamente como no uso direto de matplotlib:

Argumento Descrição
label texto para a legenda
ax objeto subplot do matplotlib onde plotar. Se vazio os plots vão para o subplot ativo
style string de estilo, como ‘ko–‘, passado para o matplotlib
alpha opacidade do plot (de 0 to 1)
kind opções: ‘area’, ‘bar’, ‘barh’, ‘density’, ‘hist’, ‘kde’, ‘line’, ‘pie’
logy use escala logaritmica no eixo y
use_index use o index para os labels de x
rot rotação de texto nos labels (0 até 360)
xticks valores a usar para marcas no eixo x
yticks valores a usar para marcas no eixo y
xlim limites para o eixo x (ex.: [0, 10])
ylim limites para o eixo y
grid exibir grade quadriculada de fundo (grid), default=exibir

Alguns parâmetros são específicos para dataframes.

Argumento Descrição
subplots bool. Plota cada coluna em um subplot separado
sharex se subplots=True, use o mesmo eixo x, com marcas e limites
sharey se subplots=True, use o mesmo eixo y, com marcas e limites
figsize tupla indicando tamanho da figura
title texto para o título
legend bool. Inclui legenda do subplot (default=True)
sort_columns plot colunas em ordem alfabética no nome; default= ordem no dataframe

Plotagem em barras

A plotagem em barras (bar plots ) pode ser feita com plot.bar() e plot.barh() (com barras verticais e horizontais).

Para experimentar com esses tipos de plotagens vamos usar os mesmos dados importados anteriormente, que contém uma lista de países com o número de nascimentos por ano de 1950 até 2020, e a estimativa à partir de 2021. Importamos o arquivo .csv para um dataframe e selecionamos apenas as linhas relativas ao ano de 2020. Linhas relativas à continentes e outras partes do mundo que não países possuem coluna Code = NaN e são excluídas. Renomeamos as colunas para mais fácil manuseio e mantemos apenas as colunas relativas ao país, ano e número de nascimentos.

» # importação do csv em um dataframe
» dados = pd.read_csv('./dados/number-of-births-per-year.csv')
» # selecão do ano = 2020 e apenas países
» dados = dados[(dados['Code'].notnull()) & (dados['Year']==2020)]
» # renomeando colunas
» dados = dados.rename(columns={'Entity':'país','Year':'ano', dados.columns[3]:'nasc'})
» # mantendo apenas colunas relevantes
» dados = dados[['país', 'nasc']]

» # use a coluna 'país' como índice
» dados.set_index('país', inplace=True)

» # o dataframe final:
» dados.head(4)
↳                      nasc
↳        país
↳ Afghanistan     1215628.0
↳     Albania       32888.0
↳     Algeria      995368.0
↳      Angola     1311356.0

Imprimimos os 2 tipos de barplot com o código abaixo, usando 10 países para as barras verticais e 20 para as horizontais. Para isso inicializamos uma figura com 1 linha e 2 colunas. O parâmetro figsize=(15, 16) indica que nossa figura terá a largura 15 e altura 6 (em polegadas). Dados os nomes longos de países o gráfico ficou sobreposto, o que seria controlado aumentando-se a separação entre axes.

» fig, axes = plt.subplots(2, 1, figsize=(10, 10))
» dados[:10].plot.bar(ax=axes[0], color=['r','b','g'], rot=30, grid=True)
» dados[:20].plot.barh(ax=axes[1], color='g', alpha=0.7, grid=True)


Gráficos desse tipo são desenhados para series e dataframes com apenas uma coluna. Se o dataframe possui várias colunas o gráfico de barras plota uma barra para cada coluna.

Para o próximo exemplo usaremos os dados disponibilizados no GapMinder, já usados e descritos nesse site. Desses dados manteremos apenas as colunas que renomearemos como “pais”, “ano” e “pop” (população), ficando com um dataframe com os países de mundo e suas populações nos anos listados abaixo.

Dessa coleta de dado separamos apenas os 5 países com maior população no último ano, 2007.

» # baixar dados do gapminder
» url =(
»       'https://raw.githubusercontent.com/jennybc/'
»       'gapminder/master/data-raw/08_gap-every-five-years.tsv'
»        )

» # criamos o dataframe dfPaises. O arquivo importado tem campos separados por tabs
» dfPaises = pd.read_csv(url, sep='\t')

» # para ver colunas e forma geral usamos
» dfPaises.head(2)
↳         country   continent    year    lifeExp        pop     gdpPercap
↳ 0   Afghanistan        Asia    1952     28.801    8425333    779.445314
↳ 1   Afghanistan        Asia    1957     30.332    9240934    820.853030

» # usamos apenas 3 colunas
» dfPaises = dfPaises[['country','year','pop']]
» # e as renomeamos
» dfPaises.rename(columns={'country':'pais', 'year':'ano'}, inplace=True)

» # o resultado é
» dfPaises
↳             pais     ano        pop
↳ 0    Afghanistan    1952    8425333
↳ 1    Afghanistan    1957    9240934
↳ 2    Afghanistan    1962   10267083

» # para ver os anos registrados examinamos o conjunto (set)
» set(dfPaises['ano'])
↳ {1952, 1957, 1962, 1967, 1972, 1977, 1982, 1987, 1992, 1997, 2002, 2007}

» # países mais populosos, em 2007
» dfPaises[dfPaises['ano']==2007] \
»          .sort_values(by=['pop'], axis=0, \
»           ascending=False, inplace=False)['pais'] \
»          .head(5)
↳ 299             China
↳ 707             India
↳ 1619    United States
↳ 719         Indonesia
↳ 179            Brazil

O último comando, para selecionar os países mais populosos, está quebrada pelo caracter \ (back slash) que é ignorado (a linha é executada por inteiro). Essa linha pode ser compreendida assim:

dfPaises[dfPaises['ano']==2007]                         : seleção só de linhas com ano = 2007
.sort_values(by=['pop'], axis=0, ascending=False)       : ordena pela coluna 'pop' em ordem inversa
['pais'].head(5)                                        : só a coluna 'pais', 5 primeiros valores

Em seguida montamos um dataframe para cada desses países e os concatenamos para um dataframe mais geral que contém linhas indexadas pela ano e colunas com o nome do país. (Outras técnicas de agrupamento serão vistas mais tarde.)

» china = dfPaises[dfPaises['pais']=='China'][['ano', 'pop']]\
»         .set_index('ano').rename(columns={'pop':'china'})
» india = dfPaises[dfPaises['pais']=='India'][['ano', 'pop']]\
»         .set_index('ano').rename(columns={'pop':'india'})
» usa = dfPaises[dfPaises['pais']=='United States'][['ano', 'pop']]\
»       .set_index('ano').rename(columns={'pop':'usa'})
» indonesia = dfPaises[dfPaises['pais']=='Indonesia'][['ano', 'pop']]\
»             .set_index('ano').rename(columns={'pop':'indonesia'})
» brasil = dfPaises[dfPaises['pais']=='Brazil'][['ano', 'pop']]\
»          .set_index('ano').rename(columns={'pop':'brasil'})

As linhas de seleção de dados do país podem ser compreendidas assim:

  dfPaises[dfPaises['pais']=='Brazil']      : seleciona apenas linhas relativas ao país 'Brazil'
  [['ano', 'pop']]                          : desse df copia apenas as colunas 'ano' e 'pop'
  .set_index('ano')                         : use a coluna 'ano' como índice
  .rename(columns={'pop':'brasil'})         : renomeie coluna 'pop' para 'brasil'
» # os paises são concatenados em um único df
» df = pd.concat([china, india, usa, indonesia, brasil],  axis=1)
» # o nome da lista de colunas será usado no plot
» df.columns.name = 'População'

» # e o resultado é
» df.head(3)
↳ População     china        india          usa   indonesia      brasil
↳ ano
↳ 1952      556263527    372000000    157553000    82052000    56602560
↳ 1957      637408000    409000000    171984000    90124000    65551171
↳ 1962      665770000    454000000    186538000    99028000    76039390

» # esse dataframe pode ser exibido em gráfico de barras
» cor = ['salmon','gold','teal','plum','powderblue']
» df.plot.bar(figsize=(10,5), grid=True, color = cor, title='Países mais populosos (de 1952 até 2007)', rot=45)

Na plotagem acima uma paleta de cores foi passada para o parâmetro color. Cada uma delas é usada para um país. O nome das colunas, df.columns.name = 'População' é usado como título da legenda.

Para gerar gráficos de barras empilhadas (stacked bar ) passamos o valor stacked=True. Nos exemplos plotamos a versão horizontal e vertical do mesmo gráfico acima.

» df.plot.barh(stacked=True, alpha=.7)         # a figura 27 é plotada

» df.plot.bar(stacked=True, alpha=.7)          # a figura 28 é plotada

Histogramas

Um histograma é uma representação gráfica, similar a um gráfico de barras, de uma distribuição de pontos. Os pontos são distribuídos em faixas igualmente divididas e o gráfico é o conjunto de retângulos com base de tamanho igual à largura das faixas e altura correspondente ao número de pontos em cada faixa.

Criamos uma série com 1000 números aleatórios, multiplicados por 100. O resultado é, no caso dessa execução, um conjunto distribuído entre -315 e 325 (aproximadamente). A partir desses dados traçamos o histograma e o gráfico de densidade ou density plot.

» ser = pd.Series((np.random.randn(1000)*100))
» ser.describe()
↳ count    1000.000000
↳ mean        3.747373
↳ std       102.637489
↳ min      -314.443835
↳ 25%       -65.343082
↳ 50%         4.835384
↳ 75%        73.314604
↳ max       324.011000

» ser.plot.hist(bins=10, grid=True, color='b', alpha=.4) # figura 19

» ser.plot.kde() # o mesmo que ser.plot.density()        # figura 30


O gráfico de densidade consiste na plotagem de uma função de distribuição de probabilidade que poderia ter gerado os dados na series. A técnica usual consiste em usar uma mistura de “núcleos” ou “kernels”. Esses gráficos são também chamados de estimativa de núcleos de densidade (kernel density estimate, KDE ).

Seaborn

Seaborn é outra biblioteca do Python voltada para a visualização de dados, baseada no matplotlib. Ela apresenta uma interface de mais alto nível e aprimoramento da qualidade estética dos gráficos. Com o Seaborn se pode conseguir gráficos bem elaborados e de boa aparência com um número menor de linhas de código.

Para os exemplos com o seaborn vamos usar os dados do Gapminder já descritos. O dataframe importado tem 1704 linhas com dados sobre os países, 6 colunas ‘country’, ‘continent’, ‘year’, ‘lifeExp’, ‘pop’, ‘gdpPercap’, respectivamente ‘pais’, ‘continente’, ‘ano’, ‘Expectativa de vida’, ‘população’, ‘PIB percapita’.

» url =(
»       'https://raw.githubusercontent.com/jennybc/'
»       'gapminder/master/data-raw/08_gap-every-five-years.tsv'
»        )
» # criamos o dataframe dfPaises. O arquivo importado tem campos separados por tabs
» dfPaises = pd.read_csv(url, sep='\t')

» # para restringir o volume dos dados armazenamos as fatias
» df2007 = dfPaises[dfPaises['year']==2007]
» dfBrasil = dfPaises[dfPaises['country']=='Brazil']

O seaborn deve ser importado. Para uma gráfico de barras mais simples informamos e base de dados e os nomes de colunas a usadas como valores para os eixos. Para não congestionar o gráfico usamos apenas os 10 primeiros países.

» import seaborn as sns
» sns.barplot(data=df2007[:10], x='lifeExp', y='gdpPercap')
» # é plotada a figura 31 abaixo

Muitas configuarações podem ser aplicadas sobre esse gráfico básico. Algumas são usadas abaixo, como a orientação das barras, textos e rotação nos eixos x e y.

» sns.set_style('darkgrid')
» graf = sns.barplot(data=df2007[:10], x='lifeExp', y='gdpPercap', orient='h')
» graf.set(xlabel = 'Expectativa de vida (anos)',
                     ylabel = 'PIB percapita', title ='Expectativa de vida x PIB')
» graf.set_xticklabels(labels=df2007[:10]['lifeExp'].round(1))
» graf.set_yticklabels(labels=df2007[:10]['country'], rotation=30)
» # é plotada a figura 32 acima

Valores válidos para sns.set_style() são 'white', 'dark', 'whitegrid', 'darkgrid', 'ticks'.

Esses gráficos plotados não sugerem qualquer relação entre a renda percapita e a expectativa de vida, o que é natural uma vez que escolhemos apenas os primeiros 10 países, em ordem alfabética. Claro que barplots não são apropriados para exibir um número muito grande de dados. Para isso podemos usar seaborn.regplot que plota o gráficos de dispersão (scatter plots ) e uma reta correspondente a um ajuste do modelo de regressão linear. Esse último gráfico mostra que existe correlação entre expectativa de vida e renda percapita.

» sns.regplot(x='lifeExp', y='gdpPercap', data=df2007) # figura 33


É comum em análise de dados que se queira ter uma visão geral de relacionamentos entre as variáveis (ou colunas de um dataframe. Para isso um pairplot faz o cruzamento entre todas as variáveis. O método seaborn.pairplot(), por default, cria uma matriz de Axes comparando aos pares as variáveis numéricas do dataframe usado como fonte de dados. Na diagonal dessa matriz uma distribuição univariada é exibida para mostrar a distribuição dos dados em cada coluna.

» sns.pairplot(df2007[['gdpPercap', 'lifeExp']],
»              diag_kind='kde', plot_kws={'color':'r','alpha': .9})


O parâmetro plot_kws recebe um dicionário de propriedades com valores.

Outro método é seaborn.catplot() que traz diversas funcionalidades para representar relações entre variáveis numéricas ou categóricas. Para uma amostra criamos um dataframe com valores de uma parábola e um seno.

» dfGraf=pd.DataFrame(np.arange(20), columns = ['x'])
» dfGraf['quadrado']=dfGraf['x']**2
» dfGraf['seno']=np.sin(dfGraf['x'])

» sns.catplot(x='x', y='quadrado', kind='strip', data=dfGraf)      # plota a figura 35
» sns.catplot(x='x', y='seno', kind='bar', data=dfGraf)            # plota a figura 36

Bibliografia

Todos os sites acessados em setembro de 2021.

Consulte bibliografia completa em Pandas, Introdução neste site.

Dataframes: multi-índices e concatenção

Índices Hierárquicos

É possível criar series e dataframes com índices e subíndices. Esse processo de indexação hierárquica é importante para a reformatação (reshaping ), formação de tabelas pivot e outras operações de agrupamento de dados.

» import pandas as pd
» import numpy as np

» # formamos uma series com índices duplos 
» sr = pd.Series([11, 12, 21, 22, 23, 31, 32, 41, 42],
                 index=[['A', 'A', 'B', 'B', 'B', 'C', 'C', 'D', 'D'],
                 [1, 2, 1, 2, 3, 1, 2, 1, 2]])
» sr
↳ A  1    11
     2    12
  B  1    21
     2    22
     3    23
  C  1    31
     2    32
  D  1    41
     2    42

» # essa series possui índices
» sr.index
↳ MultiIndex([('A', 1), ('A', 2),
              ('B', 1), ('B', 2), ('B', 3),
              ('C', 1), ('C', 2),
              ('D', 1), ('D', 2)],)

» # da mesma forma podemos transformar essa series um um dataframe
» df = pd.DataFrame(sr)

» # Os índices do dataframe são os mesmos: df.index

» # o índice B corresponde à 3 linhas
» df.loc['B']
↳        0
  1     21
  2     22
  3     23

» df.loc['B'].loc[2]
↳ 0    22

» # idem para a series
» sr['C']
↳ 1    31
  2    32

» sr['C'][1]
↳ 31

» # podemos listar as linhas de 'A' até 'C'
» sr['A':'C']
↳ A  1    11
     2    12
  B  1    21
     2    22
     3    23
  C  1    31
     2    32

» # ou as linhas correspondentes à 'A' e 'C'
» sr.loc[['A','C']]
↳ A  1    11
     2    12
  C  1    31
     2    32

» # seleção pelo índice interno pode feita diretamente
» sr.loc[:, 2]
↳ A    12
  B    22
  C    32
  D    42

» sr.loc[:, 3]
↳ B    23

stack() e unstack()

Os dados de uma series com índices hierárquicos podem ser rearranjados em um DataFrame com o uso de método Series.unstack(). Os índices internos se tornam nomes das colunas. Valores não existentes, como o correspondende aos índices A, 3, são preenchidos com NaN.

» df = sr.unstack()
» df
↳          1         2       3
  A     11.0     12.0      NaN
  B     21.0     22.0     23.0
  C     31.0     32.0      NaN
  D     41.0     42.0      NaN

» # para retornar à uma series
» df.unstack()
↳ 1  A    11.0
     B    21.0
     C    31.0
     D    41.0
  2  A    12.0
     B    22.0
     C    32.0
     D    42.0
  3  A     NaN
     B    23.0
     C     NaN
     D     NaN

No processo de desempilhar o dataframe (unstack ) os nomes das colunas foram usados como índices primários.

Um dataframe pode ter índices hierarquizados para linhas e colunas.

» clima = np.array([[25,20,30],[20,16,15],[15,25,27],[40,60,78]])
» dfClima = pd.DataFrame(clima,
»                       index=[['Temperatura','Temperatura','Umidade','Umidade'],
»                              ['dia','noite','dia','noite']],
»                       columns=[['Paraná','Paraná','Amazonas'],['Cascavel','Curitiba','Manaus']]
»                       )

» # inserindo nomes para as linhas e colunas
» dfClima.index.names = ['Característica', 'D/N']        # D/N = dia/noite
» dfClima.columns.names = ['Estado', 'Cidade']

» # o resultado é
» dfClima
↳                  Estado                   Paraná     Amazonas
                   Cidade     Cascavel     Curitiba      Manaus
  Característica      D/N             
  Temperatura         dia           25           20          30
                    noite           20           16          15
  Umidade             dia           15           25          27
                    noite           40           60          78

Se o processo de criação de dataframes com os mesmos índices será repetido várias vezes ,pode ser útil definir previamente os objetos multindexes.

» colunas = pd.MultiIndex.from_arrays([['Paraná', 'Paraná', 'Amazonas'],
»                                      ['Cascavel', 'Curitiba', 'Manaus']],
»                                      names=['Estado', 'Cidade'])

» linhas = pd.MultiIndex.from_arrays([['Temperatura','Temperatura','Umidade','Umidade'],
»                                     ['dia','noite','dia','noite']],
»                                     names=['Característica', 'D/N'])

» linhas
↳ MultiIndex([('Temperatura',   'dia'),
              ('Temperatura', 'noite'),
              (    'Umidade',   'dia'),
              (    'Umidade', 'noite')],
             names=['Característica', 'D/N'])

» pd.DataFrame(clima, index=linhas, columns=colunas)
↳                 Estado     Paraná             Amazonas
                  Cidade   Cascavel    Curitiba   Manaus
  Característica     D/N
  Temperatura        dia         25          20       30
                   noite         20          16       15
  Umidade            dia         15          25       27
                   noite         40          60       78

swaplevel() e groupby()

O ordenamento dos níveis nos dataframes pode ser alterado com o método dataframe.swaplevel(indice1, indice2). Índices primários podem ser permutados com índice secundários. Com dataframe.sort_index(level=n) podemos ordenar as linhas do dataframe segundo os nomes dos índices do nível n.

» dfClima.swaplevel('D/N', 'Característica')
↳                 Estado     Paraná               Amazonas
                  Cidade   Cascavel    Curitiba     Manaus
  D/N     Característica
  dia        Temperatura         25          20         30
  noite      Temperatura         20          16         15
  dia            Umidade         15          25         27
  noite          Umidade         40          60         78

» # ordenando as linhas pelos labels do nível 1 (D/N)
» dfClima.sort_index(level=1)

↳                  Estado                    Paraná    Amazonas
                   Cidade     Cascavel     Curitiba      Manaus
  Característica      D/N
  Temperatura         dia           25           20          30
  Umidade             dia           15           25          27
  Temperatura       noite           20           16          15
  Umidade           noite           40           60          78

» # alternativamente podemos inverter a ordem dos níveis e ordenar pelo nivel 0
» dfClima.swaplevel(0, 1).sort_index(level=0)
↳                 Estado                    Paraná    Amazonas
                  Cidade     Cascavel     Curitiba      Manaus
  D/N     Característica
  dia        Temperatura           25           20          30
                 Umidade           15           25          27
  noite      Temperatura           20           16          15
                 Umidade           40           60          78

» # soma dos valores agrupados pelo nível 1 (D/N)
» dfClima.groupby(level=1).sum()
 
↳ Estado                    Paraná    Amazonas
  Cidade     Cascavel     Curitiba      Manaus
  D/N 
  dia              40           45          57
  noite            60           76          93

O método dataframe.groupby(), que veremos mais tarde com maiores detalhes, permite o agrupamento dos dados de um determinado índice (ou nível de índices). Por ex., dataframe.groupby(level=n).sum() faz o agrupamento dos dados segundo o n-ésimo nível de índice e depois soma esses valores. Muitas outras funções estatísticas ficam disponíveis com agrupamentos por groupby.

» # soma dos valores agrupados pelo nível 0
» dfClima.groupby(level='Característica').mean()
↳                  Estado               Paraná    Amazonas
                   Cidade  Cascavel   Curitiba      Manaus
  Característica
  Temperatura                  22.5       18.0        22.5
  Umidade                      27.5       42.5        52.5

» # a média dos valores agrupados pelo índice D/N
» dfClima.groupby(level=0).mean()

↳                   Estado              Paraná   Amazonas
                    Cidade  Cascavel  Curitiba     Manaus
  Característica
  Temperatura                   22.5      18.0       22.5
  Umidade                       27.5      42.5       52.5

» # o valor máximo agrupado pelo nível 'Característica'
» dfClima.groupby(level='Característica').max()
↳                  Estado              Paraná    Amazonas
  Cidade                   Cascavel   Curitiba     Manaus
  Característica
  Temperatura                    25         20         30
  Umidade                        40         60         78

Vimos previamente que qualquer coluna pode ser transformada em índice do dataframe. Mais de uma coluna pode também ser usada: para isso usamos dataframe.set_index([coluna1, coluna2]). Por default essa operação coloca coluna1, coluna2 como índices e descarta as colunas usadas. Para alterar esse comportamento (e manter as colunas) usamos o parâmetro drop=False. O método dataframe.reset_index() remove os índices colocando-os como colunas e criando um novo conjunto de índices.

» # criamos um dataframe arbitrário
» dfNums = pd.DataFrame({'a': range(1,6),
»                        'texto-a': ['um','dois','três','quatro','cinco'],
»                        'b': range(5, 0, -1),
»                        'texto-b': ['cinco', 'quatro','três','dois','um']
»                       })

» # dataframe inicial
» dfNums
↳      a     texto-a     b     texto-b
  0    1          um     5       cinco
  1    2        dois     4      quatro
  2    3        três     3        três
  3    4      quatro     2        dois
  4    5       cinco     1          um

» # usamos as colunas 'a' e 'b' como índices
» dfNums2 = dfNums.set_index(['a', 'b'])

» dfNums2
↳          texto-a    texto-b
  a    b         
  1    5        um      cinco
  2    4      dois     quatro
  3    3      três       três
  4    2    quatro       dois
  5    1     cinco         um

» # para descartar os índices (e recuperar as colunas)
» dfNums2.reset_index()

↳       a     b     texto-a     texto-b
  0     1     5     um          cinco
  1     2     4     dois        quatro
  2     3     3     três        três
  3     4     2     quatro      dois
  4     5     1     cinco       um

» # podemos usar as colunas 'a' e 'texto-a' como índices sem descartar essas colunas
» dfNums.set_index(['a', 'texto-a'], drop=False)
↳                 a     texto-a    b    texto-b
  a     texto-a                 
  1     um        1     um         5      cinco
  2     dois      2     dois       4     quatro
  3     três      3     três       3       três
  4     quatro    4     quatro     2       dois
  5     cinco     5     cinco      1         um

Uma exceção é lançada se já existem colunas com os mesmos nomes recuperados por reset_index.

Combinando dataframes

Podemos juntar dataframes de várias formas. pandas.merge() junta dataframes usando um ou mais índices, em operações semelhantes àquelas de bancos de dados relacionais usando-se as operações de join do SQL. pandas.concat() faz a concatenação ou empilhamento dos dataframes ao longo do eixo escolhido. pandas.combine_first() permite a junção de dados que se superpõe (existem em mais de uma tabela), preenchendo valores ausentes um uma tabela com aqueles em outra tabela fornecida.

merge()

df1.merge(df2) retorna outro dataframe que é a junção dos dois dataframes. O método possui a seguinte assinatura:
df1.merge(df2, how='inner', on=None, left_on=None, right_on=None, left_index=False, right_index=False, sort=False, suffixes=('_x', '_y'), copy=True, indicator=False, validate=None)

A junção pode ser feita sobre nomes das colunas ou índices. Uma Series nomeada é tratada como um dataframe de coluna única. São parâmetros:

df1, df2 dataframe ou Series nomeada. Junção de df1 com df2
how tipo de junção: left, right, outer, inner, cross:
inner: usa apenas combinações de chaves existentes em ambas as tabelas preserva ordem das chaves.
outer: usa todas as combinações de chaves em cada uma das tabelas,
left: usa todas as combinações de chaves existentes na tabela à esquerda,
right: usa todas as combinações de chaves existentes na tabela à direita,
cross: cria o produto cartesiano das tabelas, preserva ordem dos índices.
on coluna ou índice para a junção. Deve existir em ambos os dataframes
left_on nome da coluna ou índice (ou lista) em df1.
right_on nome da coluna ou índice (ou lista) em df2.
left_index False/True: use o índice de df1 como chave.
right_index False/True: use o índice de df2 como chave.
sort False/True: Ordena os índices no resultado.
suffixes lista: default = (“_x”, “_y”). Sufixos para índices de mesmo nome
copy False/True: Se False evita a cópia, se possível
indicator False/True ou str: Se True acrescenta coluna “_merge” com informações sobre as linhas.
validate str, opcional. Se especificada verifica se a junção é do tipo:
one_to_one ou 1:1 : se chave da fusão é única nos dois dataframes,
one_to_many ou 1:m : se chave da fusão é única em df1 (lado esquerdo),
many_to_one ou m:1 : se chave da fusão é única em df2 (lado direito),
many_to_many ou m:m : embora permitida não resulta em nenhuma verificação.

Comparação de how='' com comandos SQL: (Pandas e SQL comparados).

how= similar ao SQL
left left outer join. Preserva ordem das chaves.
right right outer join. Preserva ordem das chaves.
outer full outer join. Ordena por nomes das chaves.
inner inner join. Preserva ordem das chaves à esquerda.
» # criando dataframes 
» df1 = pd.DataFrame({'chave': ['a', 'a', 'a', 'b', 'b', 'b', 'c'], 'data1': range(7)})
» df2 = pd.DataFrame({'chave': ['a', 'b', 'd'], 'data2': range(3)})

» # exibindo df1, df2 e sua junção com merge
» display(df1, df2, pd.merge(df1, df2))

↳    chave   data1
  0      a       0
  1      a       1
  2      a       2
  3      b       3
  4      b       4
  5      b       5
  6      c       6

↳    chave   data2
  0      a      0
  1      b      1
  2      d      2

↳    chave   data1   data2
  0      a       0       0
  1      a       1       0
  2      a       2       0
  3      b       3       1
  4      b       4       1
  5      b       5       1


Como os dois dataframes possuem uma coluna com nome comum a junção foi feita com base nos valores da coluna com esse nome. Essa informação pode ser explicitada com pd.merge(df1, df2, on='chave').

Se os nomes das colunas de cada dataframe for diferente eles devem ser definidos com os parâmetros left_on, right_on.

» df3 = pd.DataFrame({'chave1': ['a', 'a', 'a', 'b', 'b', 'c', 'd'], 'data1': range(7)})
» df4 = pd.DataFrame({'chave2': ['a', 'b', 'd'], 'data2': range(3)})

» display(df3, df4, pd.merge(df3, df4, left_on='chave1', right_on='chave2'))
↳    chave1     data1
  0       a     0
  1       a     1
  2       a     2
  3       b     3
  4       b     4
  5       c     5
  6       d     6

↳    chave2     data2
  0       a     0
  1       b     1
  2       d     2

↳    chave1     data1     chave2     data2
  0       a     0         a          0
  1       a     1         a          0
  2       a     2         a          0
  3       b     3         b          1
  4       b     4         b          1
  5       d     6         d          2

Vemos na concatenação acima que o método usado reune apenas valores existentes nas duas tabelas. Isso é equivalente a passar o parâmetro how=’inner’ (um inner join ). Outra opção consiste em fazer o ligamento externo.

» # para conseguir um outer join    
» pd.merge(df1, df2, how='outer')

↳     chave   data1   data2
  0     a       0.0     0.0
  1     a       1.0     0.0
  2     a       2.0     0.0
  3     b       3.0     1.0
  4     b       4.0     1.0
  5     b       5.0     1.0
  6     c       6.0     NaN
  7     d       NaN     2.0

» dd.merge(df1, df2, how='left')

↳   chave   data1   data2
  0     a       0     0.0
  1     a       1     0.0
  2     a       2     0.0
  3     b       3     1.0
  4     b       4     1.0
  5     b       5     1.0
  6     c       6     NaN

» pd.merge(df1, df2, how='right')

↳   chave     data1   data2
  0     a       0.0       0
  1     a       1.0       0
  2     a       2.0       0
  3     b       3.0       1
  4     b       4.0       1
  5     b       5.0       1
  6     d       NaN       2

Tabelas podem ser ligadas por mais de uma chave, quando os dataframes possuem índices hierarquizados. As chaves são usadas como se fossem uma única chave concatenada.

» df1 = pd.DataFrame({'chave_1': ['rato', 'rato', 'gato'],
»                      'chave_2': ['Jones', 'Jerry', 'Tom'],
»                      'valor_A': [10, 20, 30]})
» df2 = pd.DataFrame({'chave_1': ['rato', 'rato', 'gato', 'gato'],
»                       'chave_2': ['Jones', 'Jerry', 'Tom', 'Tim'],
»                       'valor_B': [40, 50, 60, 70]})
                      
» # exibindo os dataframes e a junção externa em duas chaves
» display(df1, df2, pd.merge(df1, df2, on=['chave_1','chave_2'], how='outer'))

↳     chave_1     chave_2    valor_A
  0      rato       Jones         10
  1      rato       Jerry         20
  2      gato         Tom         30

↳    chave_1     chave_2   valor_B
  0     rato       Jones        40
  1     rato       Jerry        50
  2     gato         Tom        60
  3     gato         Tim        70

↳     chave_1    chave_2    valor_A    valor_B
  0     rato       Jones       10.0         40
  1     rato       Jerry       20.0         50
  2     gato        Tom        30.0         60
  3     gato        Tim         NaN         70

» # a junção interna em duas chaves
» pd.merge(df1, df2, on=['chave_1','chave_2'], how='inner')

↳     chave_1   chave_2    valor_A    valor_B
  0      rato     Jones         10         40
  1      rato     Jerry         20         50
  2      gato      Tom          30         60

Se a junção for feita sobre campos (nomes de colunas) com o mesmo nome estes serão alterados para continuar a representar suas colunas de origem. No caso do exemplo as colunas com nome valor foram renomeadas para valor_x e valor_y.

» df1 = pd.DataFrame({'chave': ['a', 'b', 'c'], 'valor': [1,2,3]})
» df2 = pd.DataFrame({'chave': ['a', 'b', 'c'], 'valor': [10,20,30]})

» mrg = pd.merge(df1, df2, on='chave')

» display(df1, df2, mrg)

↳    chave  valor
  0      a      1
  1      b      2
  2      c      3

↳    chave  valor
  0      a     10
  1      b     20
  2      c     30

↳    chave  valor_x  valor_y
  0      a        1       10
  1      b        2       20
  2      c        3       30

A chave usada na fusão (merge) pode estar no índice de um ou ambas as tabelas. No exemplo usamos pd.merge(esquerda, direita, left_on='chave', right_index=True) que faz a junção de esquerda.chave com direita.index

» esquerda = pd.DataFrame({'chave': ['a1', 'a1', 'a2', 'a1', 'a2', 'a3'], 'valor_1': range(6)})
» direita = pd.DataFrame({'valor_2': [50, 70]}, index=['a1', 'a2'])

» mrg = pd.merge(esquerda, direita, left_on='chave', right_index=True)

» # exibindo dataframes e sua junção, usando o índice da tabela à direita
» display(esquerda, direita, mrg)

↳    chave   valor_1
  0     a1         0
  1     a1         1
  2     a2         2
  3     a1         3
  4     a2         4
  5     a3         5

↳    valor_2
  a1      50
  a2      70

↳    chave   valor_1    valor_2
  0     a1         0         50
  1     a1         1         50
  3     a1         3         50
  2     a2         2         70
  4     a2         4         70

» # se os dataframes forem invertidos conseguiríamos o
» # mesmo resultado, exceto pela ordem das colunas, usando:
» # pd.merge(direita, esquerda, right_on='chave', left_index=True)

Junções com join()

Junções podem ser feitas com dataframe.join(dfOutro) que, por default, faz a união outer join usando o índice como chave. Esse método tem a seguinte assinatura, onde os parâmetros são
dataframe.join(dfOutro, on, how, lsuffix, rsuffix, sort),
Todos os parâmetros são opcionais exceto dfOutro. Os defaults estão em negrito.

dfOutro DataFrame, Series ou lista de DataFrames.
on string, especifica em que chave(s) fazer a junção
how strings: left, right, outer, inner. Especifica o tipo de junção.
lsuffix/rsuffix Default = ”. String a concatenar à esquerda/direita em colunas com mesmo nome.
sort False/True. Se True ordena o dataframe pela chave de junção.
» # dataframe join
» df1 = pd.DataFrame({'nome': ['Paulo', 'Maria', 'Julio','Marta'],
                       'idade': [35, 43, 31, 56]})
» df2 = pd.DataFrame({'profissao': ['médico', 'engenheiro', 'advogado']})

» df1
↳      nome      idade
  0    Paulo     35
  1    Maria     43
  2    Julio     31
  3    Marta     56

» df2
↳      profissao
  0    médico
  1    engenheiro
  2    advogado

» df1.join(df2, on=df1.index,  lsuffix='_1', rsuffix='_2') # , how = 'left' (default)
↳      nome   idade_1    profissao    idade_2
  0   Paulo        35       médico       35.0
  1   Maria        43   engenheiro       40.0
  2   Julio        31     advogado       31.0
  3   Marta        56          NaN        NaN

» # um inner join
» df1.join(df2, lsuffix='_', how='inner')
↳      nome    idade_    profissao   idade
  0   Paulo       35        médico      35
  1   Maria       43    engenheiro      40
  2   Julio       31      advogado      31

Vários dataframes podem ser concatenados de uma vez. Para isso eles devem ter dimensões compatíveis.

» # Vários dataframes podem ser concatenados
» df1 = pd.DataFrame([[23, 83], [93, 10], [73, 89], [68, 90]],
»                    index=['a', 'b', 'e', 'f'],
»                    columns=['A', 'B'])

» df2 = pd.DataFrame([[2, 8], [9, 1], [7, 8], [6, 9]],
»                    index=['a', 'b', 'c', 'd'],
»                    columns=['C', 'D'])

» df3 = pd.DataFrame([[3, 3], [3, 0], [3, 9], [8, 0]],
»                    index=['a', 'c', 'd', 'e'],
»                    columns=['E', 'F'])

» # exibe os 3 dataframes
» display(df1, df2, df3)

↳ A    B
  a    23    83
  b    93    10
  e    73    89
  f    68    90

↳ C    D
  a    2    8
  b    9    1
  c    7    8
  d    6    9
  
↳ E    F
  a    3    3
  c    3    0
  d    3    9
  e    8    0

» # exibe a junção dos dataframes
» df1.join([df2, df3])

↳         A       B      C      D      E      F
  a    23.0    83.0    2.0    8.0    3.0    3.0
  b    93.0    10.0    9.0    1.0    NaN    NaN
  e    73.0    89.0    NaN    NaN    8.0    0.0
  f    68.0    90.0    NaN    NaN    NaN    NaN

Como sempre, campos não fornecidos são preenchidos por NaN. Por ex.: df1.join([df2, df3]).loc['f', 'F'] = NaN.

concatenate()

Podemos concatenar numpy.arrays, Series e dataframes ao longo do eixo desejado.

» # Concatenando um array ao longo de um eixo
» # criamos 2 arrays
» arr1 = np.arange(6).reshape((3, 2))

» arr1
↳ array([[0, 1],
         [2, 3],
         [4, 5]])

» # concatenando arr1 consigo mesmo, ao longo de colunas
» np.concatenate([arr1, arr1], axis=1)
↳ array([[0, 1, 0, 1],
         [2, 3, 2, 3],
         [4, 5, 4, 5]])

» # concatenando arr1 consigo mesmo, ao longo de linhas
» np.concatenate([arr1, arr1], axis=0)
↳ array([[0, 1],
         [2, 3],
         [4, 5],
         [0, 1],
         [2, 3],
         [4, 5]])

» # defina outro array, com shape (3, 1)
» arr2 = np.array([[0], [1], [2]])

» arr2
↳ array([[0],
         [1],
         [2]])

» # concatenando arr1 2 arr2 pelas colunas
» np.concatenate([arr1, arr2], axis=1)

↳ array([[0, 1, 0],
         [2, 3, 1],
         [4, 5, 2]])

» # (tentando) concatenar arr1 2 arr2 pelas linhas
» np.concatenate([arr1, arr2], axis=0)
↳ ValueError: all the input array dimensions for the concatenation axis must match exactly,
  but along dimension 1, the array at index 0 has size 2 and the array at index 1 has size 1


Vemos que podemos concatenar uma matriz coluna (3 × 1) com outra matriz (3 × 2) pelas colunas, mas não pelas linhas pois as dimensãos são incompatíveis.

combine() e combine_first()

O método df1.combine(df2, func, fill_value=None, overwrite=True) combina df1 e df2, coluna a coluna, aplicando func para decidir qual valor será usado.

Podemos criar uma função que receba duas colunas e realize alguma operação entre elas, retornando outra coluna. No ex., a função f faz a soma dos elementos de duas colunas e retorna aquela com menor soma. A função g seleciona, a cada linha, qual é o maior elemento. Quando o parâmetro fill_value=r é usado todos os valores NaN são substituídos por r antes de serem submetidos à função func, exceto se ambos os valores forem nulos, quando não existirá substituição.

» df1 = pd.DataFrame({'A': [0, 3], 'B': [7, 2]})
» df2 = pd.DataFrame({'A': [2, 6], 'B': [1, 3]})

» df1
↳      A    B
  0    0    7
  1    3    2

» df2
↳      A    B
  0    2    1
  1    6    3

» # a função de comparação pode ser
» def f(x,y):
»     if x.sum() < y.sum():
»         return x
»     else:
»         return y

» # a combinação, usando essa função
» df1.combine(df2, f)
↳      A    B
  0    0    1
  1    3    3

» # O mesmo resultado pode ser obtido com uma função lambda
» df1.combine(df2, lambda x, y: x if x.sum() < y.sum() else y)

» # funções mais complexas podem ser usadas
» df1.combine(df2, lambda x, y: (x+y)*(y-x))
↳       A     B
  0     4   -48
  1    27     5

» # outro exemplo, selecionar o maior elemento de cada df
» def g(x,y):
»     a = x[0] if x[0] > y[0] else y[0]
»     b = x[1] if x[1] > y[1] else y[1]
»     return pd.Series([a,b])

» df1.combine(df2,g)
↳      A    B
  0    2    7
  1    6    3

» # o mesmo poderia ser feito com uma funlão lambda
» maior = lambda x,y: pd.Series([x[0] if x[0] > y[0] else y[0],
                                x[1] if x[1] > y[1] else y[1]])
» df1.combine(df2,maior) # mesmo output
 
» # uso de fill_value
» df1 = pd.DataFrame({'A': [0, 0], 'B': [np.NaN, 4]})
» df2 = pd.DataFrame({'A': [1, 1], 'B': [3, 3]})

» df1.combine(df2, maior, fill_value=6)
↳      A      B
  0    1    6.0
  1    1    4.0

Já o método dataframe.combine_first(dfOutro) substitui os valores NaN no dataframe com os valores de dfOutro, quando esses valores existirem.

» df1 = pd.DataFrame({'a': [1, np.nan, 5, np.nan],
»                     'b': [np.nan, 2, np.nan, 6],
»                     'c': range(2, 18, 4)})
» df2 = pd.DataFrame({'a': [5, 4, np.nan, 3, 7],
»                     'b': [np.nan, 3, 4, 6, 8]})
» display(df1, df2)
↳        a      b     c
  0    1.0    NaN     2
  1    NaN    2.0     6
  2    5.0    NaN    10
  3    NaN    6.0    14
  
↳        a      b
  0    5.0    NaN
  1    4.0    3.0
  2    NaN    4.0
  3    3.0    6.0

» df1.combine_first(df2)
↳        a      b      c
  0    1.0    NaN    2.0
  1    4.0    2.0    6.0
  2    5.0    4.0   10.0
  3    3.0    6.0   14.0
  4    7.0    8.0    NaN

Bibliografia

Consulte bibliografia completa em Pandas, Introdução neste site.

Nesse site:

Dataframes, preparação de dados


Preparação de dados

Programadores que lidam com análise de dados passam grande parte do tempo dedicado a um projeto preparando esses dados, antes mesmo de começar qualquer análise. Normalmente os dados são importados de uma fonte externa, tal como um arquivo em forma tabular em html, pdf, texto puro ou csv. Eles precisam ser convertidos para um formato legível e muitas vezes contém erros e valores ausentes. A vezes o próprio processo de conversão introduz perda de dados, tal como acontece em textos impressos transformados em texto digital por OCR (optical character recognition ). Seja qual for a origem dos dados algum trabalho de depuração deve ser feito. Em seguida eles devem passar por formatação adequada, a quebra de tabelas, o estabelecimento de vínculos entre elas, etc. Pandas oferece boas ferramentas para todas essas etapas.

Dados ausentes

Já vimos que dados não presentes em alguma tabela são representados por NaN (not a number). O objeto None do python também é tratado como um valor ausente ou NA (not available). O método dropna() descarta linhas (se axis=0, default) ou colunas (se axis=1) contendo campos nulos. dropna(how='all') descarta linhas ou colunas se todos os campos forem nulos. Também podemos determinar que apenas linhas ou colunas com um número mínimo de elementos não nulos sejam mantidas, com df.dropna(thresh=n).

» import pandas as pd
» import numpy as np

» dados = pd.Series([121.45, np.nan ,32.12,42.21,51.56])
» dados
↳ 0    121.45
  1       NaN
  2     32.12
  3     42.21
  4     51.56

» dados[3]= None
» dados.isnull()
↳ 0    False
  1     True
  2    False
  3     True
  4    False

» dados.dropna()                   # o mesmo que dados[dados.notnull()]
↳ 0    121.45
  2     32.12
  4     51.56

» from numpy import nan as NA     # para estabelecer um alias curto para np.nan
» data = pd.DataFrame([[1., 6.5, 3.9], [1.3, NA, NA], [NA, NA, NA], [NA, 5.8, 6.7]])
» data
↳        0      1      2
  0    1.0    6.5    3.9
  1    1.3    NaN    NaN
  2    NaN    NaN    NaN
  3    NaN    5.8    6.7

» data.dropna()
↳        0      1      2
  0    1.0    6.5    3.9

» data.dropna(how='all')
↳        0      1      2
  0    1.0    6.5    3.9
  1    1.3    NaN    NaN
  3    NaN    5.8    6.7

» data[4] = NA
» data
↳        0      1      2      4
  0    1.0    6.5    3.9    NaN
  1    1.3    NaN    NaN    NaN
  2    NaN    NaN    NaN    NaN
  3    NaN    5.8    6.7    NaN

» data.dropna(axis=1, how='all')
↳        0      1      2
  0    1.0    6.5    3.9
  1    1.3    NaN    NaN
  2    NaN    NaN    NaN
  3    NaN    5.8    6.7

Preenchendo valores ausentes

A invés de descartar linhas e colunas com campos ausentes podemos preencher estas lacunas. df.fillna(const) substitui campos NA com o valor único const. Um dicionário {coluna:valor} pode ser passado contendo constantes diferentes para cada coluna. Observando que df.mean() retorna uma Series com as médias de cada colunas, podemos usar df.fillna(df.mean()) para preencer NAs de cada coluna com essa média. Também podemos passar o parâmetro df.fillna(method='ffill') para preencher cada NA com o valor que o antecede na coluna. df.fillna(method='bfill') preenche NAs com o valor que o segue.

» # criando um df de teste com campos NA
» df = pd.DataFrame(np.random.randn(4, 3))
» df.iloc[0:3, 1] = NA
» df.iloc[1:3, 2] = NA

» df
↳             0           1            2
  0    0.615016         NaN    -0.860821
  1    1.195041         NaN          NaN
  2   -0.110482         NaN          NaN
  3    1.837690    1.569459     0.891858

» # preenche NAs com 0
» df.fillna(0)
↳             0           1           2
  0    0.615016    0.000000   -0.860821
  1    1.195041    0.000000    0.000000
  2   -0.110482    0.000000    0.000000
  3    1.837690    1.569459    0.891858

» # preenche coluna 1 com 10, coluna 2 com 20
» df.fillna({1:10, 2:20})
↳             0            1            2
  0    0.615016    10.000000    -0.860821
  1    1.195041    10.000000    20.000000
  2   -0.110482    10.000000    20.000000
  3    1.837690     1.569459     0.891858

» df.fillna(method='ffill')
↳             0          1            2
  0    0.615016        NaN    -0.860821
  1    1.195041        NaN    -0.860821
  2   -0.110482        NaN    -0.860821
  3    1.837690   1.569459     0.891858

» df.fillna(method='bfill')
↳             0           1           2
  0    0.615016    1.569459   -0.860821
  1    1.195041    1.569459    0.891858
  2   -0.110482    1.569459    0.891858
  3    1.837690    1.569459    0.891858

» df.fillna(method='bfill', limit=2)
» df.mean()
↳ 0    0.884316
  1    1.569459
  2    0.015519

» df.fillna(df.mean())
↳             0           1           2
  0    0.615016         NaN   -0.860821
  1    1.195041    1.569459    0.891858
  2   -0.110482    1.569459    0.891858
  3    1.837690    1.569459    0.891858

Vemos que df.fillna(method='ffill') não substituiu valores nas linhas 0, 1, 2 da coluna 1 pois nenhum valor os antecede. Nesse caso teríamos que usar method='bfill', ou outra forma de preencher o campo vazio.

Substituições com dataframe.replace()

O método df.replace() substitui valores específicos em uma Series ou dataframe. Por ex., suponha que temos uma Series de valores positivos e a inserção de negativos foi convencionada para indicar valores ausentes. Podemos alterar esses valores usando df.replace(), lembrando que nenhuma das formas abaixo altera a Serie original, a menos que inplace=True seja usado.

» serie = pd.Series([12,-2, 34, -1])
» serie
↳ 0    12
  1    -2
  2    34
  3    -1

» serie.replace(-2, -90)
↳ 0    12
  1   -90
  2    34
  3    -1

» serie.replace([-2,-1], [20,10])
↳ 0    12
  1    20
  2    34
  3    10

» serie.replace(-1, NA)
↳ 0    12.0
  1    -2.0
  2    34.0
  3     NaN

Claro que df.replace() pode ser usado para substituir um valor específico por valores calculados, usando métodos mais sofisticados de avaliação.

Em um dataframe df.replace(lista1, lista2) pode ser usado para substituir valores da lista1 pelos da lista2 (que deve ter o mesmo tamanho). df.replace(lista, escalar) substitui todos os valores em lista pelo escalar e df.replace(dicionario) substitui as chaves pelas valores no dicionário.

» df = pd.DataFrame({'a':[9,56,67], 'b':[33,55,66], 'c':[63,69,67], 'd':[2,3,9]})
» df
↳       a     b     c    d
  0     9    33    63    2
  1    56    55    69    3
  2    67    66    67    9

» df.replace(9, 100)
↳        a     b     c     d
  0    100    33    63     2
  1     56    55    69     3
  2     67    66    67   100

» df.replace([9, 55, 67], 0)
↳      a     b     c    d
  0    0    33    63    2
  1   56     0    69    3
  2    0    66     0    0

» df.replace([9, 55, 67], [1,2,3])
↳      a     b     c    d
  0    1    33    63    2
  1   56     2    69    3
  2    3    66     3    1

» df.replace({9:-9, 33:-33})
↳       a      b     c    d
  0    -9    -33    63    2
  1    56     55    69    3
  2    67     66    67   -9

Análise de outliers

Em qualquer processo de tomada de medidas ou coleta de dados existem restrições à precisão obtida. Mas, além da precisão restrita, é frequente existirem dados muito fora de qualquer curva esperada. Esses são os chamados pontos fora da curva ou outliers e geralmente são descartados. Os critérios de decisão sobre quais pontos são outliers dependem do modelo que se quer tratar.

No pandas podemos encontrar valores que estão acima ou abaixo de um certo limite.

Lembrando que np.random.randn(M, p) retorna um array de p colunas, cada uma com M valores, retirados aleatoriamente de uma distribuição normal com média 0 e variância 1, começamos por coletar um dataframe para testes.

Considerando os máximos e mínimos exibidos, vamos estabelecer arbitrariamente que valores afastados acima de 3 da média do conjunto são outliers. Isso quer dizer que consideraremos os pontos com |x| > 3 como outliers (onde |x| significa valor absoluto de x). Uma das possibilidades consiste em substituir valores não aceitáveis por np.nan e depois usar uma das formas de fill para preencher esses campos.

» dados = pd.DataFrame(np.random.randn(1000, 4))
» # são os valores mínimo e máximo desse dataframe
» dados.min().min(), dados.max().max()
↳ (-3.7113843289590496, 3.480659301328407)

» # substituimos |x| > 3 por np.nan
» dados[np.abs(dados) > 3] = np.nan
» dados.describe()    # (1) visualização do dataframe (alguns campos exibidos)
↳                   0             1             2            3
  count    996.000000    997.000000    998.000000   996.000000
  mean       0.086548      0.021479     -0.046291     0.019611
  min       -2.772219     -2.860741     -2.763174    -2.644022
  max        2.763864      2.849207      2.955914     2.905516

» dados = dados.fillna(method='bfill')
» dados.describe()    # (2) visualização do dataframe (alguns campos exibidos)
↳                    0              1              2              3
  count    1000.000000    1000.000000    1000.000000    1000.000000
  mean        0.089292       0.021845      -0.046568       0.020410
  min        -2.772219      -2.860741      -2.763174      -2.644022
  max         2.763864       2.849207       2.955914       2.905516

No primeiro uso de describe a contagem count mostra que existem linhas com campos nulos para cada coluna. Após a operação de fill todos os campos são numéricos.

Removendo linhas duplicadas

Para remover linhas duplicadas em um dataframe usamos df.drop_duplicates(). Valores duplicados em apenas uma coluna podem ser removidos com df.drop_duplicates('nomeColuna'), ou em várias colunas, passando-se uma lista df.drop_duplicates(['col1',..., 'coln']). Por default a primeira linhas, entre as duplicadas é mantida. Para manter a última usamos df.drop_duplicates('coluna', keep='last').

» # remoção de linhas duplicadas
» dic ={'col1': ['vaca', 'vaca', 'pato','pato'], 'col2': [1, 3, 4, 4]} 
» df = pd.dfFrame(dic)
» df
↳      col1    col2
  0    vaca    1
  1    vaca    3
  2    pato    4
  3    pato    4

» df.duplicated()           # retorna uma Series mostrando linhas duplicadas
↳ 0    False
  1    False
  2    False
  3     True

» df.drop_duplicates()
↳      col1   col2
  0    vaca      1
  1    vaca      3
  2    pato      4

» df.drop_duplicates('col1')
↳      col1  col2
  0    vaca     1
  2    pato     4

» df.drop_duplicates('col1', keep='last')
↳      col1   col2
  1    vaca     3
  3    pato     4

No atual estado de Pandas não é possível fazer a remoção de duplicadas sobre colunas. Para isso obtenha a transposta do dataframe, remova linhas duplicadas e o transponha novamente.

Transformações sobre elementos de um dataframe

Um restaurante faz uma lista de aquisição de produtos, descrevendo o ítem e quantas unidades devem se adquiridas.

» compra = {'produto':['leite', 'manteiga', 'laranja', 'arroz'],
            'quantos':[15, 40,50, 30]} 
» dfComprar = pd.DataFrame(compra)
» dfComprar
↳      produto  quantos
  0      leite       15
  1   manteiga       40
  2    laranja       50
  3      arroz       30

Mais tarde o gerente pede que os produtos sejam classificados como veganos ou não. Para isso podemos usar o método Series.map(dict) que transforma cada elemento usando-o como chave e retornando o valor no dicionário. Construímos um mapeamento entre produto e S/N, conforme o produto seja ou não vegano.

» veg = {'leite':'N', 'manteiga':'N', 'laranja':'S', 'arroz':'S'}
» # dfComprar['produto'] é uma Series e
» dfComprar['produto'].map(lambda x: vegano[x])
↳ 0    N
  1    N
  2    S
  3    S

» # inserindo esse serie em uma nova coluna do df
» dfComprar['vegano'] = dfComprar['produto'].map(veg)
» dfComprar
↳      produto   quantos   vegano
  0      leite        15        N
  1   manteiga        40        N
  2    laranja        50        S
  3      arroz        30        S

» # o mesmo resultado seria obtido com a função lambda 
» dfComprar['vegano']=dfComprar['produto'].map(lambda x: vegano[x])

Compartimentação e discretização

Compartimentação e discretização, (Binning e Discretization ) é o processo de particionamento de dados em faixas especificadas. Os compartimentos (faixas ou bins) são representados por variáveis categóricas, que são variáveis que podem assumir apenas um número discreto e limitado de valores, geralmente fixo. Elas estão associadas à propriedades qualitivas do sistema que se observa e podem satisfazer ou não algum critério de ordenamento.

Por ex., suponha que temos um estudo de qualquer natureza centrada sobre indivíduos onde o sexo e a faixa etária são relevantes para as conclusões que se procura obter. O sexo dos indivíduos (digamos que divididos em F = feminino, M = masculino, O = outros) não pode ser ordenado. Mas as faixas etárias são ordenáveis. Dividimos a população estudada em faixas ou bins. Sabendo que todos os participantes são maiores de idade e nenhum tem mais de 98 anos de idade usamos as faixas separadas pelas idades: 18, 34, 50, 66, 82, 98 anos.

» faixas = [18, 34, 50, 66, 82, 98]                            # definição dos intervalos de idade
» idades = [25, 18, 59, 39, 68, 26, 73, 63, 56, 84]            # idade dos indivíduos no estudo
» categorias = pd.cut(idades, faixas)
» categorias
↳ [(18.0, 34.0], NaN, (50.0, 66.0], (34.0, 50.0], (66.0, 82.0], (18.0, 34.0],
   (66.0, 82.0], (50.0, 66.0], (50.0, 66.0], (82.0, 98.0]]
  Categories (5, interval[int64]): [(18, 34] < (34, 50] < (50, 66] < (66, 82] < (82, 98]]

» # o objeto categorias é do tipo Categorical
» type(categorias)
↳ pandas.core.arrays.categorical.Categorical

» categorias.categories
↳ IntervalIndex([(18, 34], (34, 50], (50, 66], (66, 82], (82, 98]],
                closed='right', dtype='interval[int64]')

# O método pd.value_counts(categorias) fornece uma contagem para cada valor existente:
» pd.value_counts(categorias)
↳ (50, 66]    3
  (18, 34]    2
  (66, 82]    2
  (34, 50]    1
  (82, 98]    1

» # as colunas são formadas por
» pd.value_counts(categorias).index[0],  pd.value_counts(categorias)[0]
↳ (Interval(50, 66, closed='right'), 3)

» nomes_faixas = ['garoto','adulto','semi-novo','vô','matusa']
» categorias = pd.cut(idades, faixas, labels=nomes_faixas)
» categorias
↳  ['garoto', NaN, 'semi-novo', 'adulto', 'vô', 'garoto', 'vô', 'semi-novo', 'semi-novo', 'matusa']
   Categories (5, object): ['garoto' < 'adulto' < 'semi-novo' < 'vô' < 'matusa']
» pd.value_counts(categorias)
↳ semi-novo    3
  garoto       2
  vô           2
  adulto       1
  matusa       1

» # podemos transformar esse objeto em um dataframe
» dfCont = pd.DataFrame(pd.value_counts(categorias))

» # reordenar índices
» dfConf = dfCont.reindex(['garoto', 'adulto', 'semi-novo', 'vô', 'matusa'])
» dfConf
↳             0
  garoto      2
  adulto      1
  semi-novo   3
  vô          2
  matusa      1

As faixas numéricas são estabelecidas em intervalos do tipo (a, b] < (b, c] … representando intervalos abertos no limite inferior e fechados no superior. Isso significa que a não está no primeiro intervalo, mas b está. Para alterar esse comportamento usamos o parâmetro pandas.cut(...,right=False).

Podemos informar em quantas faixas queremos dividir os dados, ao invés de passar explicitamente essas faixas. Nesse caso o método pandas.cut(dados, n, precision=p) calculará n intervalos iguais baseados nos valores máximos e mínimos dos dados. precision=p determina a precisão decimal das faixas.

» # array com 20 numeros aleatórios    
» dados = np.random.rand(20)*10

» dados.min(), dados.max()         # valores mínimo e máximo
↳ (1.0012658194039414, 9.799331139583924)

» # 3 faixas (bins)
» picado = pd.cut(dados, 3, precision=2)
» pd.value_counts(picado)
↳ (6.87, 9.8]     10
  (0.99, 3.93]     7
  (3.93, 6.87]     3

Para distribuir dados em faixas baseadas em quantis usamos o método pandas.qcut(dados, n), onde n é o número de partes na partição. Intervalos de quantis customizados podem ser conseguidos passando-se uma lista em pandas.qcut(dados, lista).

» data = np.random.randn(1000)          # 1000 números aleatórios
» categorias = pd.qcut(data, 4)         # distribui em quartis
» pd.value_counts(categorias)
↳ (-3.0309999999999997, -0.683]    250
  (-0.683, 0.0106]                 250
  (0.0106, 0.702]                  250
  (0.702, 3.196]                   250

» # intervalos de quantis customizados
» pd.value_counts(pd.qcut(data, [0, 0.1, 0.5, 0.9, 1.]))
↳ (-1.223, 0.0106]                 400
  (0.0106, 1.301]                  400
  (-3.0309999999999997, -1.223]    100
  (1.301, 3.196]                   100

Permutações aleatórias

Permutações entre as linhas (ou colunas) de um dataframe são obtidas com dataframe.take(arr), onde arr é um array com a ordem dos índices desejada. Se essa ordem for “sorteada” o dataframe fica com linhas em ordem “aleatoria”. Para reordenar colunas usamos axis=1. dataframe.sample(n) seleciona n linhas do dataframe, sem repetições (n < dataframe.shape[0]) e dataframe.sample(n, replace=True) retorna n linhas do dataframe que podem ser repetidas (como em um sorteio com reposição dos elementos sorteados).

» # dataframe de teste    
» df = pd.DataFrame(np.arange(16).reshape((4, 4)))
» df
↳      0    1    2    3
  0    0    1    2    3
  1    4    5    6    7
  2    8    9   10   11
  3   12   13   14   15

» sorteio = np.random.permutation(4)   # permutação aleatória de 0, 1, 2 e 3
» sorteio
↳ array([3, 2, 0, 1])

» df.take(sorteio)                     # dataframe na ordem de linhas sorteadas
↳       0     1     2     3
  3    12    13    14    15
  2     8     9    10    11
  0     0     1     2     3
  1     4     5     6     7

» df.take(sorteio, axis=1)             # dataframe na ordem de colunas sorteadas
↳       3     2     0     1
  0     3     2     0     1
  1     7     6     4     5
  2    11    10     8     9
  3    15    14    12    13

» df.sample(n=2)                       # 2 linhas selecionadas aleatoriamente
↳       0     1     2     3
  1     4     5     6     7
  3    12    13    14    15

» df.sample(n=2, axis=1)               # 2 colunas selecionadas aleatoriamente
↳      3    1
  0    3    1
  1    7    5
  2   11    9
  3   15   13

» df.sample(n=4, replace=True)        # 4 linhas selecionadas aleatoriamente, com reposição
↳      0    1    2    3
  0    0    1    2    3
  0    0    1    2    3
  1    4    5    6    7
  0    0    1    2    3

O mesmo dataframe obtido com df.take(sorteio) poderia ser conseguido com df.iloc[sorteio].

Indicador de computação, variáveis fictícias

Na estatística, econometria e aprendizado de máquina uma variável fictícia (variável dummy ) é uma representação de um efeito categórigo assumindo apenas os valores 0 ou 1 para indicar presença ou ausência de alguma forma de caracterização. Elas podem ser consideradas como representações numéricas de aspectos qualitativos. Um exemplo simples seria a representação da classificação de uma conta bancária como poupança (0) ou conta corrente (1).

Uma variável categórica pode ser transformada em uma matriz dummy ou de indicadores. Se uma series (uma coluna de um dataframe) possui p valores distintos podemos obter um dataframe com o mesmo número de colunas, cada uma contendo apenas 0 ou 1. Para isso usamos o método pandas.get_dummies(Series) que retorna um dataframe marcando as posições onde cada um dos p valores ocorrem. Um prefixo pode ser acrescentado aos nomes das colunas com pandas.get_dummies(Series, prefix='p').

Por ex., em uma pesquisa foi marcado, para cada indivíduo participante, o campo sexo = F (feminino), M (masculino), O (outros).

» df = pd.DataFrame({'individuo': ['Fulano', 'Beltrano', 'Cicrano', 'Deltrano', 'Cruciano', 'Marciano'],
                     'sexo': ['H','H','H','F','O','F']})
» df
↳      individuo   sexo
  0       Fulano      H
  1     Beltrano      H
  2      Cicrano      H
  3     Deltrano      F
  4     Cruciano      O
  5     Marciano      F

» # categorizando a coluna 'sexo'
» pd.get_dummies(df['sexo'])
↳      F    H    O
  0    0    1    0
  1    0    1    0
  2    0    1    0
  3    1    0    0
  4    0    0    1
  5    1    0    0

» # inserindo um prefixo (no nome das colunas)
» pd.get_dummies(df['sexo'], prefix='sexo').head(2)
↳    sexo_F  sexo_H  sexo_O
   0      0       1       0
   1      0       1       0

Muitas vezes os dados devem ser manipulados e preparados para uma devida categorização. Suponha que temos uma lista de autores, cada um associado a um ou mais gêneros literários separados por |. Queremos uma listagem de autores versus gêneros, marcando em qual gênero cada um escreve.

» # importamos de qualquer fonte o seguinte dataframe:
» dfAutores
↳       autor                genero
  0   Antonio          poesia|conto
  1      José               romance
  2     Marco      ficção|biografia
  3     Pedro          poesia|conto

» # cada autor está associado a um ou mais gêneros
» genero = dfAutores.genero
» autores = dfAutores.autor
» # as duas séries têm o mesmo comprimento (len(autores) = len(generos) = 4, 4

Criamos uma lista vazia e a preenchemos com todos os gêneros, quebrando os campos em |. Depois usamos pandas.unique(lista) para conseguir um array com os gêneros, sem repetições, como em um conjunto (set).

» lista = []
» for t in genero:
»     lista.extend(t.split('|'))
» unicos = pd.unique(lista)
» unicos
↳ array(['poesia', 'conto', 'romance', 'ficção', 'biografia'], dtype=object)

Em seguida criamos um dataframe de zeros com os autores nas colunas e gêneros nas linhas.

» dfZero = pd.DataFrame(np.zeros((len(unicos),len(autores))), index=unicos, columns=autores).astype(int)
» dfZero          # estado inicial de dfZero
↳ autor    Antonio    José    Marco    Pedro
  poesia         0       0        0        0
  conto          0       0        0        0
  romance        0       0        0        0
  ficção         0       0        0        0
  biografia      0       0        0        0

# preenchemos esse dataframe
» for i in range(len(unicos)):
»     for k in range(len(genero)):
»         if unicos[i] in genero[k]:
»             dfZero.iloc[i,k] = 1
            
» dfZero    # estado final de dfZero
↳ autor    Antonio    José    Marco    Pedro
  poesia         1       0        0        1
  conto          1       0        0        1
  romance        0       1        0        0
  ficção         0       0        1        0
  biografia      0       0        1        0

O duplo loop sobre a lista de gêneros únicos, unicos, e a lista original de gêneros genero faz a verificação se um dos generos está em genero1|genero2…. Por exemplo, na linha 3, coluna 2 temos:

» unicos[3], genero[2],  unicos[3] in genero[2]
↳ ('ficção', 'ficção|biografia', True)

Se o resultado é verdadeiro o dataframe terá o campo correspondente trocado para 1. Os demais permanecem com o valor 0. O dataframe final é o resultado desejado.

Tratamento de campos de texto

Operações com strings são também vetorializadas no pandas. No ex. usamos os códigos telefones dos países: 55-Brasil, 47-Noruega, 52-México. Construímos duas séries e as concatenamos em um dataframe, df = pd.concat([serie, srPais], axis=1).

» lista = ['055-11-12345678', '047-21-87654321', '055-11-13579135', '052-78-45665412']
» serie = pd.Series(lista)
» serie
↳ 0    055-11-12345678
  1    047-21-87654321
  2    055-11-13579135
  3    052-78-45665412

» # booleano, linhas que contém '-11-'
» serie.str.contains('-11-')
↳ 0     True
  1    False
  2     True
  3    False

» # linhas que contém '-11-'
» serie[serie.str.contains('-11-')]
↳ 0    055-11-12345678
  2    055-11-13579135

» # lista com os códigos dos países
» codigos = [x.split('-')[0] for x in lista]
» codigos
↳ ['055', '047', '055', '052']

» # dicionário para conversão código ⇒ país
» pais = {'055':'Brasil', '047':'Noruega', '052':'México'}
» srPais = pd.Series([pais[x] for x in codigos])      # veja comentário †
» srPais
↳ 0     Brasil
  1    Noruega
  2     Brasil
  3     México

» # juntamos as duas series em um dataframe
» df = pd.concat([serie, srPais], axis=1)
» df = df.rename(columns = {0:'telefone', 1:'pais'})

» df
↳             telefone       pais
  0    055-11-12345678     Brasil
  1    047-21-87654321    Noruega
  2    055-11-13579135     Brasil
  3    052-78-45665412     México

» # nome do país começado com 'No'
» df[df['pais'].str.startswith('No')]
↳             telefone       pais
  1    047-21-87654321    Noruega

» # acrescenta campo com 3 primeiras letras do nome
» df['abreviado'] = df['pais'].str[:3]
» df
↳             telefone      pais    abreviado
  0    055-11-12345678    Brasil          Bra
  1    047-21-87654321   Noruega          Nor
  2    055-11-13579135    Brasil          Bra
  3    052-78-45665412    México          Méx

(): A linha srPais = pd.Series([pais[x] for x in codigos]) (uma compreensão de lista) percorre os valores em codigos e os usa como chaves no dicionário pais, retornando seus valores.

🔺Início do artigo

Bibliografia

  • McKinney, Wes: Python for Data Analysis, Data Wrangling with Pandas, NumPy,and IPython
    O’Reilly Media, 2018.

Consulte bibliografia completa em Pandas, Introdução neste site.

Nesse site:

Series: Resumo


Pandas Series

Atributos

Atributo Descrição
at[n] Acesso ao valor na posição n
attrs Retorna ditionario de atributos globais da series
axes Retorna lista de labels do eixo das linhas
dtype Retorna o tipo (dtype) dos objetos armazenados
flags Lista as propriedades do objeto
hasnans Informa se existem NaNs
iat[n] Acesso ao valor na posição n inteiro
iloc[n] Acesso ao valor na posição n inteiro
index Retorna lista de índices
index[n] Retorna índice na n-ésima posição
is_monotonic Booleano: True se valores crescem de forma monotônica
is_monotonic_decreasing Booleano: True se valores decrescem de forma monotônica
is_unique Booleano: True se valores na series são únicos
loc Acessa linhas e colunas por labels em array booleano
name O nome da Series
nbytes Número de bytes nos dados armazenados
shape Retorna uma tuple com forma (dimensões) dos dados
size Número de elementos nos dados
values Retorna series como ndarray

Métodos

Método (sobre série s, outra s2) Descrição
s.abs() Retorna s com valor absoluto, e/e
s.add(s2) Soma s com s2, e/e
s.add_prefix('prefixo') Adiciona prefixo aos labels com string ‘prefixo’
s.add_suffix('sufixo') Adiciona sufixo aos labels com string ‘sufixo’
s.agg([func, axis]) Agrega usando func sobre o eixo especificado
s.align(s2) Alinha 2 objetos em seus eixos usando método especificado
s.all([axis, bool_only, skipna, level]) Booleano: se todos os elementos são True
s.any([axis, bool_only, skipna, level]) Booleano: se algum elemento é True
s.append(to_append[, ignore_index, …]) Concatena 2 ou mais Series
s.apply(func[, convert_dtype, args]) Aplica func sobre os valores de s, e/e
s.argmax([axis, skipna]) Posição (índice inteiro) do valor mais alto de s
s.argmin([axis, skipna]) Posição (índice inteiro) do menor valor de s
s.argsort([axis, kind, order]) Índices inteiros que ordenam valores da s
s.asfreq(freq) Converte TimeSeries para frequência especificada.
s.asof(where[, subset]) Último elemento antes da ocorrência de NaNs após ‘where’
s.astype(dtype[, copy, errors]) Transforma (cast) para dtype
s.at_time(time[, asof, axis]) Seleciona valores em determinada hora (ex., 9:30AM)
s.backfill([axis, inplace, limit, downcast]) Aliás para DataFrame.fillna() usando method=’bfill’
s.between(min, max) Booleana satisfazendo min <= s <= max, e/e
s.between_time(inicio, fim) Seleciona valores com tempo entre inicio e fim
s.bfill([axis, inplace, limit, downcast]) Alias para DataFrame.fillna() usando method=’bfill’
s.clip([min, max, axis, inplace]) Inclui apenas valores no intervalo
s.combine(s2, func[, fill_value]) Combina a s com s2 ou escalar, usando func
s.compare(s2[, align_axis, keep_shape, …]) Compara s com s2 exibindo differenças
s.copy([deep]) Cópia do objeto s, índices e valores
s.corr(s2) Correlação de s com s2, excluindo NaNs
s.count([level]) Número de observações na s, excluindo NaN/nulls
s.cov(s2[, min_periods, ddof]) Covariância da s, excluindo NaN/nulls
s.cummax([axis, skipna]) Máximo cumulativo
s.cummin([axis, skipna]) Mínimo cumulativo
s.cumprod([axis, skipna]) Produto cumulativo
s.cumsum([axis, skipna]) Soma cumulativa
s.describe([percentiles, include, exclude, …]) Gera descrição estatística
s.div(s2) Divisão (float) de s por s2, e/e
s.divmod(s2) Divisão inteira e módulo de s por s2, e/e
s.dot(s2) Produto interno entre a s e s2
s.drop([labels]) Retorna s com labels removidos
s.drop_duplicates([keep, inplace]) Remove elementos duplicados de s
s.dropna() Remove valores faltantes de s
s.duplicated([keep]) Exibe valores duplicados na s
s.eq(s2) Boleano, igualdade entre s e s2, e/e
s.equals(s2) Booleano: True se s contém os mesmos elementos que s2
s.ewm([com, span, halflife, alpha, …]) Calcula exponencial com peso
s.explode([ignore_index]) Transforma cada elemento de um objeto tipo lista em uma linha
s.fillna([value, method, axis, inplace, …]) Substitui valores NA/NaN usando método especificado
s.first(offset) Seleciona período inicial de uma série temporal usando offset.
s.first_valid_index() Índice do primeiro valor não NA/null
s.floordiv(s2) Divisão inteira da s por s2, e/e
s.ge(s2) Booleana: maior ou igual entre s e s2, e/e
s.get(key) Retorna item correspondente à key
s.groupby([by, axis, level, as_index, sort, …]) Agrupa a s
s.gt(s2[, level, fill_value, axis]) Booleana: se s é maior que s2, e/e
s.head([n]) Retorna os n primeiros valores
s.hist() Plota histograma da s usando matplotlib.
s.idxmax([axis, skipna]) Label do item de maior valor
s.idxmin([axis, skipna]) Label do item de menor valor
s.interpolate([method, axis, limit, inplace, …]) Preenche valores NaN usando metodo de interpolação
s.isin(valores) Booleano: se elementos da s estão contidos em valores
s.isna() Booleano: se existem valores ausentes
s.isnull() Booleano: se existem valores nulos
s.item() Primeiro elemento dos dados como escalar do Python
s.items() Iteração (lazy) sobre a tupla (index, value)
s.iteritems() Iteração (lazy) sobre a tupla (index, value)
s.keys() Alias de index
s.kurt([axis, skipna, level, numeric_only]) Kurtosis imparcial
s.kurtosis([axis, skipna, level, numeric_only]) Idem
s.last(offset) Seleciona período final de uma série temporal usando offset
s.last_valid_index() Índice do último valor não NA/null
s.le(s2) Booleana: se s é menor ou igual a s2, e/e
s.lt(s2[, level, fill_value, axis]) Booleana: se s é menor que s2, e/e
s.mad([axis, skipna, level]) Desvio médio absoluto dos valores da s
s.mask(cond[, s2, inplace, axis, level, …]) Substitui valores sob condição dada
s.max([axis, skipna, level, numeric_only]) Valor máximo
s.mean([axis, skipna, level, numeric_only]) Média dos valores
s.median([axis, skipna, level, numeric_only]) Mediana dos valores
s.memory_usage([index, deep]) Memória usada pela s
s.min([axis, skipna, level, numeric_only]) Menor dos valores da s
s.mod(s2[, level, fill_value, axis]) Módulo de s por s2, e/e
s.mode([dropna]) Moda da s
s.mul(s2[, level, fill_value, axis]) Multiplicação de s por s2, e/e
s.multiply(s2[, level, fill_value, axis]) Multiplicação de s por s2, e/e
s.ne(s2[, level, fill_value, axis]) Booleana: se s é diferente de s2, e/e
s.nlargest([n, keep]) Retorna os n maiores elementos
s.notna() Booleana: se existem valores não faltantes ou nulos
s.notnull() Idem
s.nsmallest([n, keep]) Retorna os n menores elementos
s.nunique([dropna]) Retorna quantos elementos únicos existem na s
s.pad([axis, inplace, limit, downcast]) O mesmo que DataFrame.fillna() usando method=’ffill’
s.plot O mesmo que pandas.plotting._core.PlotAccessor
s.pop(i) Remove s[i] de s e retorna s[i]
s.pow(s2) Exponential de s por s2, e/e
s.prod([axis, skipna, level, numeric_only, …]) Produto dos elemetos da s
s.product([axis, skipna, level, numeric_only, …]) Idem
s.quantile([q, interpolation]) Valor no quantil dado
s.ravel([order]) Retorna dados como um ndarray
s.rdiv(s2[, level, fill_value, axis]) Divisão (float) de s por s2, e/e
s.rdivmod(s2) Divisão inteira e módulo de s por s2, e/e
s.reindex([index]) Ajusta a s ao novo índice
s.reindex_like(s2[, method, copy, limit, …]) Série com índices em acordo com s2
s.rename([index, axis, copy, inplace, level, …]) Altera o nome (labels) dos índices
s.reorder_levels(order) Reajusta níveis de índices usando order
s.repeat(repeats[, axis]) Repete elementos da s
s.replace([to_replace, value, inplace, limit, …]) Substitui valores em to_replace por value
s.reset_index([level, drop, name, inplace]) Reinicializa índices
s.rfloordiv(s2[, level, fill_value, axis]) Divisão inteira de s por s2, e/e
s.rmod(s2[, level, fill_value, axis]) Modulo da divisão da s por s2, e/e
s.rmul(s2[, level, fill_value, axis]) Multiplicação de s por s2, e/e
s.round([n]) Arredonda valores da s para n casas decimais.
s.rpow(s2[, level, fill_value, axis]) Exponential de s por s2, e/e
s.rsub(s2[, level, fill_value, axis]) Subtração da s por s2, e/e
s.rtruediv(serie[, level, fill_value, axis]) Divisão (float) de s por s2, e/e
s.sample([n, frac, replace, weights, …]) Amostra randomizada de items da s
s.searchsorted(value[, side, sorter]) Índices onde elementos devem ser inseridos para manter ordem
s.sem([axis, skipna, level, ddof, numeric_only]) Erro padrão imparcial da média
s.skew([axis, skipna, level, numeric_only]) Inclinação imparcial
s.sort_index([axis, level, ascending, …]) Reorganiza s usando os índices
s.sort_values([axis, ascending, inplace, …]) Reorganiza s usando seus valores
s.std([axis, skipna, level, ddof, numeric_only]) Desvio padrão da amostra
s.str Usa funções de string sobre s (se string). Ex. s.str.split(“-“)
s.sub(s2) Subtração de s por s2, e/e
s.subtract(serie) Idem
s.sum([axis, skipna, level, numeric_only, …]) Soma dos valores da s
s.tail([n]) Últimos n elementos
s.to_clipboard([excel, sep]) Copia o object para o clipboard do sistema
s.to_csv([path_or_buf, sep, na_rep, …]) Grava a s como arquivo csv
s.to_dict() Converte s para dict {label ⟶ value}
s.to_excel(excel_writer[, sheet_name, na_rep, …]) Grava s como uma planilha Excel
s.to_frame([name]) Converte s em DataFrame
s.to_hdf(path_or_buf, key[, mode, complevel, …]) Grava s em arquivo HDF5 usando HDFStore
s.to_json([path_or_buf, orient, date_format, …]) Converte s em string JSON
s.to_latex([buf, columns, col_space, header, …]) Renderiza objeto para LaTeX
s.to_markdown([buf, mode, index, storage_options]) Escreve a s em formato Markdown (leia)
s.to_numpy([dtype, copy, na_value]) Converte s em NumPy ndarray
s.to_pickle(path[, compression, protocol, …]) Grava objeto serializado em arquivo Pickle
s.to_sql(name, con[, schema, if_exists, …]) Grava elementos em forma de um database SQL
s.to_string([buf, na_rep, float_format, …]) Constroi uma representação string da s
s.tolist() Retorna uma lista dos valores
s.to_list() idem
s.transform(func[, axis]) Executa func sobre elementos de s
s.truediv(s2) Divisão (float) de s por s2, e/e
s.truncate([before, after, axis, copy]) Trunca a s antes e após índices dados
s.unique() Retorna os valores da s, sem repetições
s.update(s2) Modifica s usando valores de s2, usando índices iguais
s.value_counts([normalize, sort, ascending, …]) Retorna s com a contagem de valores únicos
s.var([axis, skipna, level, ddof, numeric_only]) Variância imparcial dos valores da s
s.view([dtype]) Cria uma nova “view” da s
s.where(cond[, serie, inplace, axis, level, …]) Substitui valores se a condição cond = True
🔺Início do artigo

Bibliografia

Consulte a bibliografia no final do primeiro artigo dessa série.

Dataframes – Seleções e Ordenamento


Outras formas do construtor de dataframes

dataFrames

Se um dicionário aninhado (onde os valores associados às chaves externas são outros dicionários) é passado no construtor de um DataFrame o pandas interpretará as chaves externas como nomes das colunas e as chaves internas como índices das linhas. Na ausência de um par chave:valor em um ou mais dos dicionários o campo receberá o valor NaN.

» dic = {'Pedro': {'Prova 1': 5.4, 'Prova 3': 7.9},
                'Ana': {'Prova 1': 8.5, 'Prova 2': 9.7, 'Prova 3': 6.6},
               'Luna': {'Prova 2': 5.0, 'Prova 3': 7.0, 'Prova 4': 6.0}
             }
» dfNotas = pd.DataFrame(dic)
» dfNotas
↳
            Pedro    Ana   Luna
  Prova 1     5.4    8.5    NaN
  Prova 3     7.9    6.6    7.0
  Prova 2     NaN    9.7    5.0
  Prova 4     NaN    NaN    6.0

Se os nomes das linhas e das colunas forem fornecidos eles serão exibidos.

» dfNotas.index.name = 'Prova';
» dfNotas.columns.name = 'Aluno'
» dfNotas
↳
  Aluno      Pedro    Ana    Luna
  Prova             
  Prova 1     5.4     8.5     NaN
  Prova 3     7.9     6.6     7.0
  Prova 2     NaN     9.7     5.0
  Prova 4     NaN     NaN     6.0

Com frequência importamos de fontes externas, como faremos abaixo, uma fonte de dados e precisamos verificar sua integridade. Por ex., para encontrar elementos ausentes, preenchidos como NaN, usamos dataFrame.isnull() (o mesmo que pd.isnull(dataFrame)). Para saber quantos valores nulos existem usamos dataFrame.isnull().sum(), que fornece a soma dos campos True para cada campo.

» dfNotas.isnull()    # o mesmo que pd.isnull(dfNotas)
↳ Aluno       Pedro     Ana      Luna
  Prova             
  Prova 1     False     False    True
  Prova 3     False     False    False
  Prova 2     True      False    False
  Prova 4     True      True     False

» dfNotas.isnull().sum()
↳ Aluno
  Pedro    2
  Ana      1
  Luna     1
  dtype: int64

O método dataFrame.notna() (o mesmo que dataFrame.notnull() e o inverso de dataFrame.isnull()) retorna um dataframe booleano com True onde os campos não são nulos. Para inserir manualmente campos nulos usamos a constante pd.NaT e para eliminar linhas (ou colunas) contendo nulos aplicamos dataframe.dropna().

» # para eliminar linhas contendo nulos (o default é axis=0)
» dfNotas.dropna()
↳ Aluno     Pedro     Ana     Luna
  Prova             
  Prova 3     7.9     6.6      7.0
    
» # para eliminar colunas contendo nulos
» dfNotas.dropna(axis=1)
# todas são eliminadas pois existem NaN em todas as colunas

Evidentemente é necessário ter cuidado ao eliminar linhas ou colunas com NaN. Em muitos casos pode ser necessário substituir esses valores por outros, dependendo da aplicação. Para fazer a alteração no próprio frame use o parâmetro inplace = True.

Colunas e índices são objetos do tipo array e podem ser usados com alguns métodos de conjuntos.

» dfNotas.columns
↳ Index(['Pedro', 'Ana', 'Luna'], dtype='object', name='Aluno')

» dfNotas.index
↳ Index(['Prova 1', 'Prova 3', 'Prova 2', 'Prova 4'], dtype='object', name='Prova')

» 'Ana' in dfNotas.columns      # True
» 'Ann' in dfNotas.columns      # False
» 'Prova 5' in dfNotas.index    # False

O mesmo ocorre se o dicionário contiver Series como valores, sendo as chaves usadas como nomes das colunas e os índices das series usados como índices das linhas.

» serie1 = pd.Series([1, 2, 3, 4], index=['a', 'b', 'c', 'd'])
» serie2 = pd.Series([5, 6, 7, 8], index=['a', 'b', 'c', 'd'])
» serie3 = pd.Series([9, 0, -1, -2], index=['a', 'b', 'c', 'd'])

» dic = {'A':serie1, 'B':serie2, 'C':serie3 }
» pd.DataFrame(dic)
↳ 
      A   B   C
  a   1   5   9
  b   2   6   0
  c   3   7  -1
  d   4   8  -2

Dataframes podem ser criados recebendo Series no construtor.

» disciplinas = pd.Series(['Matemática', 'Física', 'História', 'Geografia'])
» notas = pd.Series([9.0, 5.4, 7.7, 8.9])
» df = pd.DataFrame({'Disciplina':disciplinas, 'Notas': notas})
» df
↳     Disciplina   Notas
  0   Matemática     9.0
  1       Física     5.4
  2     História     7.7
  3    Geografia     8.9

Outros objetos podem ser passados como argumento no construtor:

  • Ndarray (do NumPy) de 2 dimensões,
  • dicionário de arrays, listas ou tuples (todas as sequências devem ter o mesmo comprimento),
  • dicionários de arrays NumPy, de Series ou de outros dicionários,
  • listas de dicionários, Series, listas ou tuplas,
  • Series ou outro dataframe.

Tratamento de dados usando pandas.dataframe

Para os testes e demonstrações que se seguem vamos usar dados reais para demonstrar algumas funcionalidades úteis dos pandas.dataframes.

Fonte de dados

Para realizar os teste com dataframes vamos utilizar os dados encontrados no Gapminder nessa url: 08_gap-every-five-years.tsv. Esse é um arquivo contendo dados com um registro em cada linha e os valores na linha separados por tabs, (tabulação). Esse arquivo pode ser baixado para o seu computador e depois importado para um dataframe ou, como usamos abaixo, importada diretamente do site de Jennifer Bryan (jennybc): Gapminder, no Github.

O arquivo original tem o seguinte formato,

country      continent       year   lifeExp        pop     gdpPercap
Afghanistan       Asia       1952    28.801    8425333   779.4453145
Afghanistan       Asia       1957    30.332    9240934   820.8530296
Afghanistan       Asia       1962    31.997   10267083     853.10071
...

onde os espaços entre valores são tabulações (\t, no python). A primeira linha contém os ‘headers’ ou títulos das colunas. Traduziremos esses títulos da seguinte forma: country ⟼ pais, continent ⟼ continente, year ⟼; ano, lifeExp ⟼ expVida (expectativa de vida), pop ⟼ populacao, gdpPercap ⟼ pibPercap (produto interno bruto, percapita).

» import pandas as pd
» import numpy as np
» # Usando arquivo encontrado no Gapminder
» url =(
      'https://raw.githubusercontent.com/jennybc/'
      'gapminder/master/data-raw/08_gap-every-five-years.tsv'
       )
» url
↳ 'https://raw.githubusercontent.com/jennybc/gapminder/master/data-raw/08_gap-every-five-years.tsv'

» # criamos o dataframe dfPaises. O arquivo importado tem campos separados por tabs 
» dfPaises = pd.read_csv(url, sep='\t')

» # o dataframe tem 1704 linhas e 6 colunas
» dfPaises.shape
↳ (1704, 6)

» dfPaises.head()
↳
          country   continent    year   lifeExp        pop     gdpPercap
  0   Afghanistan        Asia    1952    28.801    8425333    779.445314
  1   Afghanistan        Asia    1957    30.332    9240934    820.853030
  2   Afghanistan        Asia    1962    31.997    10267083   853.100710
  3   Afghanistan        Asia    1967    34.020    11537966   836.197138
  4   Afghanistan        Asia    1972    36.088    13079460   739.981106
  1704 rows × 6 columns 
» # renomeando os campos para nomes em português
» dfPaises.rename(columns={'country':'pais',
                           'continent':'continente',
                           'year':'ano',
                           'lifeExp':'expVida',
                           'pop':'populacao',
                           'gdpPercap':'pibPercap',
                          }, inplace=True)
» # ficamos assim
» dfPaises.columns
↳ Index(['pais', 'continente', 'ano', 'expVida', 'populacao', 'pibPercap'], dtype='object')

» # para reordenar as colunas em sua exibição
» dfPaises = dfPaises[['continente', 'pais', 'ano', 'expVida', 'populacao', 'pibPercap']]

Podemos obter uma visão geral do conjunto de dados importados usando dois métodos. dataframe.info() retorno os nomes das colunas, quantos valores não nulos, seus dtypes, e memória usada nesse armazenamento. Por aí vemos que nossos dados não possuem valores nulos. Caso esses existissem eles teriam que ser localizados e tratados devidamente. O método df.describe() retorna um dataframe contendo a contagem count dos valores (nesse caso, o número de linhas), a média mean desses valores, o desvio padrão std, o valor mínimo e máximo, min, max e os quartis em 25%, 50%, 75%.

» dfPaises.info()
↳ <class 'pandas.core.frame.DataFrame'>
  RangeIndex: 1704 entries, 0 to 1703
  Data columns (total 6 columns):
   #   Column      Non-Null Count  Dtype  
  ---  ------      --------------  -----  
   0   continente  1704 non-null   object 
   1   pais        1704 non-null   object 
   2   ano         1704 non-null   int64  
   3   expVida     1704 non-null   float64
   4   populacao   1704 non-null   int64  
   5   pibPercap   1704 non-null   float64
  dtypes: float64(2), int64(2), object(2)
  memory usage: 80.0+ KB

» dfPaises.describe()
↳ 
                   ano          expVida         populacao         pibPercap
 count      1704.00000      1704.000000      1.704000e+03       1704.000000
  mean      1979.50000        59.474439      2.960121e+07       7215.327081
  std         17.26533        12.917107      1.061579e+08       9857.454543
  min       1952.00000        23.599000      6.001100e+04        241.165876
  25%       1965.75000        48.198000      2.793664e+06       1202.060309
  50%       1979.50000        60.712500      7.023596e+06       3531.846988
  75%       1993.25000        70.845500      1.958522e+07       9325.462346
  max       2007.00000        82.603000      1.318683e+09     113523.132900

Gravação e recuperação de dados em arquivos pickle

Após a verificação de integridade dos dados e a realização das alterações básicas necessárias é boa ideia salvar em disco o dataframe nesse momento. Para isso usamos pandas.to_pickle(dfFrame, 'nomeArquivo.pkl'), gravando um arquivopickle. Para recuperá-lo em qualquer momento usamos dfPaises = pandas.read_pickle('./dados/dataframePaises.pkl').

» # gravando um arquivo pickle
» pd.to_pickle(dfPaises, './dados/dataframePaises.pkl')    

» # mais tarde esse dataframe pode ser recuperado
» del dfPaises   # para limpar essa variável
» dfPaises = pd.read_pickle('./dados/dataframePaises.pkl')
» # o dataframe é recuperado

Seleção e filtragem

As principais formas de seleção de um ou mais valores de um dataframe são os métodos dataframe.loc(), dataframe.iloc(), dataframe.at e dataframe.iat. Um subconjunto de dados do dataframe, seja por seleção de linhas, colunas ou ambas, é denominado de fatia ou slice.

A principal diferença entre loc (at) e iloc (iat) é a seguinte: loc é baseado em labels ou nomes das linhas ou colunas, enquanto iloc é baseado nos índices numéricos (mesmo que tenham nomes) sempre com base 0.

  • dataframe.at[row_label, column_label]
  • dataframe.iat[row_position, column_position]
  • dataframe.loc[row_label, column_label]
  • dataframe.iloc[row_position, column_position]

Na tabela abaixo nos referiremos a um dataframe nomeado como df. (S) se refere a uma Series retornada, (D) a um dataframe.

Operações df.iat e df.at retorna: (índices são posições linhas/colunas)
df.iat[m,n] elemento da m-ésima linha, n-ésima coluna
df.at[lblLinha, lblColuna] elemento linha/coluna relativas aos labels lblLinha/lblColuna
Operações df.iloc retorna: (índices são posições das linhas/colunas)
df.iloc[n] n-ésima linha (S)
df.iloc[[n]] n-ésima linha (D)
df.iloc[-n] n-ésima linha, contando do final
df.iloc[i,j:, n] linhas i, até j (exclusive), coluna n (S)
df.iloc[[i,j,k]:[m,n,o]] linhas i, j, k, colunas m, n, o
df.iloc[:, n] n-ésima coluna (S)
df.iloc[:, [n]] n-ésima coluna (D)
df.iloc[:,-1] última coluna
df.iloc[i:j,m:n] linhas i até j (exclusive), colunas m até n (inclusive)
Operações df.loc retorna: (índices linhas/colunas se referem aos seus labels)
df.loc[n] linha de índice n (S)
df.loc[[n]] linha de índice n (D)
df.loc[:] todas as linhas e colunas (D)
df.loc[:, 'col'] todas as linhas, coluna ‘col’ (S)
df.loc[:, ['col']] todas as linhas, coluna ‘col’ (D)
df.loc[:, ['col1', 'col2']] todas as linhas, colunas ‘col1’ e ‘col2’ (D)
df.loc[i:j, ['col1', 'col2']] linhas com índices de i até i (inclusive), colunas ‘col1’ e ‘col2’ (D)
df.loc[[i,j,k] , ['col1', 'col2']] linhas com índices i, j, k, colunas ‘col1’ e ‘col2’ (D)
df.loc[i:j, 'col1':'coln']] linhas com índices i até j (inclusive), colunas ‘col1’ até ‘coln’ (inclusive) (D)
Atalhos o mesmo que
df['col1'] ou df.col1 df.loc[:, ‘col1’]] (S)
df[['col1', 'col2']] df.loc[:, [‘col1’, ‘col2’]] (D)

Em todos esses métodos uma exceção de KeyError é levantada se um índice ou label não existir na dataframe.

Se o index da linha coincidir com sua posição então df.loc[n] e df.iloc[n] serão as mesmas linhas. Isso nem sempre é verdade, como se verá abaixo com o reordenamento das linhas.

São incorretas as sintaxes: df.loc[-n], df.loc[:, n], df.loc[:, [n]] com n numérico pois os labels devem ser fornecidos.

Exemplos de consultas e seleções

dataframe.iloc()

Para outros exemplos vamos usar o dataframe já carregado, dfPaises, para fazer consultas e seleções, primeiro usando dataframe.iloc(). Lembramos que a contagem de índices sempre se inicia em 0:

» # lembrando que dfPaises.iloc[[0]] é um dataframe, dfPaises.iloc[0] é uma Series
» # primeira linha, pelo índice    
» dfPaises.iloc[[0]]
↳    continente            pais    ano    expVida    populacao     pibPercap
  0        Asia     Afghanistan   1952     28.801      8425333    779.445314

» # última linha, pelo índice    
» dfPaises.iloc[[-1]]
↳         continente          pais     ano    expVida    populacao     pibPercap
  1703        Africa      Zimbabwe    2007     43.487     12311143    469.709298    

» # linhas 15 até 20 (exclusive), colunas 2 até 5 (exclusive)
» dfPaises.iloc[15:20, 2:5]
↳      ano   expVida  populacao
  15  1967     66.22    1984060
  16  1972     67.69    2263554
  17  1977     68.93    2509048
  18  1982     70.42    2780097
  19  1987     72.00    3075321

» # linhas 1, 3, 5 , colunas 2, 5
» dfPaises.iloc[[1,3,5],[2,5]]
↳     ano   pibPercap
  1  1957   820.853030
  3  1967   836.197138
  5  1977   786.113360

» # linhas 1, 3, 5, última coluna
» dfPaises.iloc[[1,3,5],-1]
↳ 1    820.853030
  3    836.197138
  5    786.113360
  Name: pibPercap, dtype: float64

» # todas as linhas, coluna 3
» dfPaises.iloc[:, [3]].head()
↳     expVida
  0    28.801
  1    30.332
  2    31.997
  3    34.020
  4    36.088

» # linhas 0, 3, 6, 24; colunas 0, 3, 5
» dfPaises.iloc[[0,3,6,24], [0,3,5]]
↳    continente   expVida    pibPercap
  0        Asia    28.801   779.445314
  3        Asia    34.020   836.197138
  6        Asia    39.854   978.011439
  24     Africa    43.077  2449.008185

A seleção das linhas nos dois métodos é diferente. Em dataframe.loc[m,n] linhas com labels de m até n (inclusive) são selecionadas. Em dataframe.iloc[m,n] são selecionadas linhas com índices (numéricos) de m até n (exclusive).

» # iloc[m,n] exibe linhas m até n (exclusive)
» dfPaises.iloc[1:2]
↳   continente         pais   ano  expVida  populacao   pibPercap
  1       Asia  Afghanistan  1957   30.332    9240934   820.85303    

» # loc[m:n] exibe linhas m até n (inclusive)
» dfPaises.loc[1:2]
↳     continente          pais    ano   expVida   populacao    pibPercap
  1         Asia   Afghanistan   1957    30.332     9240934    820.85303
  2         Asia   Afghanistan   1962    31.997    10267083    853.10071
dataframe.loc()

Os próximos testes são feitos com dataframe.loc(), que deve receber os labels como índices.

» # todas as linhas, só colunas 'ano' e 'populacao' (limitadas por head())
» dfPaises.loc[:,['ano','populacao']].head()
↳        ano     populacao
  0     1952       8425333
  1     1957       9240934
  2     1962      10267083
  3     1967      11537966
  4     1972      13079460

» # linhas 3 até 6 (inclusive), só colunas 'ano' e 'expVida'
» dfPaises.loc[3:6,['ano', 'expVida']]
↳        ano     expVida
  3     1967      34.020
  4     1972      36.088
  5     1977      38.438
  6     1982      39.854

» # todas as linhas, só colunas 'ano' (restritas por head())
» dfPaises.loc[:, 'ano'].head()
↳ 0    1952
  1    1957
  2    1962
  3    1967
  4    1972

Métodos df.loc, df.iloc, df.at e df.iat

Para explorar um pouco mais a diferença no uso de df.loc e df.iloc vamos criar um dataframe bem simples e sem valores nulos.

» dic = {'Pedro': {'Prova 1': 5.4, 'Prova 2': 6.2, 'Prova 3': 7.9},
         'Ana':  {'Prova 1': 8.5, 'Prova 2': 9.7, 'Prova 3': 6.6},
         'Luna': {'Prova 1': 5.0, 'Prova 2': 7.0, 'Prova 3': 4.3}
        }
» dfNotas = pd.DataFrame(dic)
» dfNotas
↳           Pedro     Ana    Luna
  Prova 1     5.4     8.5     5.0
  Prova 2     6.2     9.7     7.0
  Prova 3     7.9     6.6     4.3

df.loc e df.at usa labels de linhas e colunas.
df.iloc e df.iat usa números (índices) de linhas e colunas.

Nos comentários listamos seleções usando df.iloc para se obter o mesmo retorno.

» dfNotas.loc['Prova 1','Luna']             # dfNotas.iloc[0,2]
↳ 5.0

» dfNotas.loc['Prova 1']                    # dfNotas.iloc[0] (Series)
↳ Pedro    5.4
  Ana      8.5
  Luna     5.0

» dfNotas.loc[['Prova 1']]                  # dfNotas.iloc[[0]] (dataframe)
↳           Pedro   Ana   Luna
  Prova 1     5.4   8.5    5.0

» dfNotas.loc[['Prova 1','Prova 2']]        # dfNotas.iloc[0:2] (dataframe)
↳           Pedro    Ana   Luna
  Prova 1     5.4    8.5    5.0
  Prova 2     6.2    9.7    7.0

» dfNotas.loc['Prova 1': 'Prova 3']         # dfNotas.iloc[0:3] (dataframe)
↳           Pedro    Ana   Luna
  Prova 1     5.4    8.5    5.0
  Prova 2     6.2    9.7    7.0
  Prova 3     7.9    6.6    4.3

» dfNotas.loc[['Prova 1'],['Ana','Luna']]   # dfNotas.iloc[[0],[1,2]]  (dataframe)
↳           Ana   Luna
  Prova 1   8.5    5.0

» dfNotas.loc['Prova 1':'Prova 3', 'Pedro':'Luna']   # dfNotas.iloc[0:3,0:3] (dataframe)
↳           Pedro    Ana    Luna
  Prova 1     5.4    8.5     5.0
  Prova 2     6.2    9.7     7.0
  Prova 3     7.9    6.6     4.3

» dfNotas.loc[:,['Luna']]                   # dfNotas.iloc[:,[2]]
↳           Luna
  Prova 1    5.0
  Prova 2    7.0
  Prova 3    4.3

Observe que em dfNotas.iloc[0:3,0:3] são selecionadas as linhas de índices 0, 1 e 2 e colunas 0, 1 e 2.

Análogos à df.loc e df.iloc temos, respectivamente, df.at[lblLinha, lblColuna] e df.iat[m,n] onde lblLinha, lblColuna se referem aos labels e m, n aos índices das linhas/colunas. Ambos recebem um par e retornam um único valor do dataframe. Quando aplicados em uma Series iat e at recebem um único índice/label localizador de posição.

» dfNotas.iat[2,1]
↳ 6.6
» dfNotas.iloc[0].iat[1]                 # o mesmo que dfNotas.loc['Prova 1'].iat[1]
↳ 8.5
» dfNotas.at['Prova 1', 'Luna']
↳ 5.0
» dfNotas.loc['Prova 1'].at['Ana']       # o mesmo que dfNotas.loc['Prova 1'].iat[1]
↳ 8.5

Nenhuma das duas formas de seleção de uma slice (.loc ou .iloc) copiam um dataframe por referência, como ocorre com numPy.ndarrays. Por exemplo, df = dfNotas.iloc[:,[2]] é uma cópia da 3ª coluna, e não uma referência ou view. Ela pode ser alterada sem que o dataframe original seja modificado. Se um novo valor for atribuído ao slice diretamente, no entanto, o dataframe fica alterado.

» df = dfNotas.iloc[:,[2]]
» df.Luna = 10
» display(df,dfNotas)
↳           Luna
  Prova 1     10
  Prova 2     10
  Prova 3     10

↳           Pedro   Ana   Luna
  Prova 1     5.4   8.5    5.0
  Prova 2     6.2   9.7    7.0
  Prova 3     7.9   6.6    4.3

» # no entanto se o slice receber atribuição direta o dataframe fica alterado
» dfNotas.iloc[:,[2]] = 10

» dfNotas
↳           Pedro    Ana    Luna
  Prova 1     5.4    8.5    10.0
  Prova 2     6.2    9.7    10.0
  Prova 3     7.9    6.6    10.0

» # para inserir valores diferentes outro dataframe de ser atribuído ao slice
» dic = {'Luna': {'Prova 1': 8.5, 'Prova 2': 7.9, 'Prova 3': 10}}
» dfLuna = pd.DataFrame(dic)
» dfNotas.iloc[:,[2]] = dfLuna

» dfNotas
↳           Pedro    Ana   Luna
  Prova 1     5.4    8.5    8.5
  Prova 2     6.2    9.7    7.9
  Prova 3     7.9    6.6   10.0

» # alternativamente, um np.array com shape apropriado pode ser atribuído ao slice
» arrLuna =np.array([2.3, 4.5, 5.6]).reshape(3,1)
» dfNotas.iloc[:,[2]] = arrLuna
» dfNotas
↳          Pedro   Ana   Luna
  Prova 1    5.4   8.5    2.3
  Prova 2    6.2   9.7    4.5
  Prova 3    7.9   6.6    5.6

Na atribuição dfNotas.iloc[:,[2]] = 10 houve o broadcasting de 10 para uma forma compatível com o slice.

Para que a atribuição seja bem sucedida, sem necessidade de broadcasting, um objeto de mesmo formato deve ser atribuído. No caso dfNotas.iloc[:,[2]].shape = dfLuna.shape = (3, 1) (3 linhas, 1 coluna). O mesmo ocorre com a atribuição de um array do numpy.

Manipulando linhas e colunas

Um array booleano pode ser passado como índice de um dataframe. Apenas as linhas correspondentes ao índice True será exibida. Alguns métodos de string estão disponíveis para testes em campos, como df['campo'].str.startswith('str') e df['campo'].str.endswith('str') (começa e termina com). O teste df['campo'].isin(['valor1', 'valor2'])] retorna True se os campos estão contidos na lista.

Para os exemplos usamos o dataframe dfPaises.

» # seleção por array booleano
» dfPaises.loc[dfPaises['ano'] == 2002].head(3)
↳      continente         pais   ano   expVida   populacao    pibPercap
  10         Asia  Afghanistan   2002   42.129    25268405   726.734055
  22       Europe      Albania   2002   75.651     3508512  4604.211737
  34       Africa      Algeria   2002   70.994    31287142  5288.040382

» # quais os paises tem nome começados com 'Al'
» dfPaises.loc[dfPaises['pais'].str.startswith('Al')]['pais'].unique()
↳ array(['Albania', 'Algeria'], dtype=object)

» # quais os paises tem nome terminados em 'm'
» dfPaises.loc[dfPaises['pais'].str.endswith('m')]['pais'].unique()
↳ array(['Belgium', 'United Kingdom', 'Vietnam'], dtype=object)

» # quantas linhas se referem à 'Europe' e 'Africa'
» dfPaises.loc[dfPaises['continente'].isin(['Europe', 'Africa'])].shape[0]
↳ 984

» dfPaises.loc[(dfPaises['continente']=='Africa') & (dfPaises['ano']==1957)].head(4)
↳       continente         pais     ano    expVida    populacao      pibPercap
  25        Africa     Algeria    1957     45.685     10270856    3013.976023
  37        Africa      Angola    1957     31.999      4561361    3827.940465
  121       Africa       Benin    1957     40.358      1925173     959.601080
  157       Africa    Botswana    1957     49.618       474639     918.232535


» # paises e anos com população < 7000 ou expectativa de vida > 82
» dfPaises.loc[(dfPaises['populacao'] < 70000) | (dfPaises['expVida'] > 82)][['ano','pais']]
↳          ano    pais
  420     1952    Djibouti
  671     2007    Hong Kong, China
  803     2007    Japan
  1296    1952    Sao Tome and Principe
  1297    1957    Sao Tome and Principe
  1298    1962    Sao Tome and Principe
Operador significa
& and, e
| or, ou
~ not, negação

O método arr.unique() acima foi aplicado para ver quais os países satisfazem as condições, sem repetições. arr.shape é uma tupla (número linhas, número colunas). Os últimos exemplos fazem testes compostos usando os operadores & (and, e lógico) e | (or, ou lógico).

Se nenhum campo for submetido ao teste lógico todos os valores do dataframe são usados. O mesmo ocorre com a aplicação de uma função, como mostrado para uma função lambda.

» # novos teste com loc e iloc
» dic = {'Pedro': {'Prova 1': 5.4, 'Prova 2': 6.2, 'Prova 3': 7.9},
         'Ana':  {'Prova 1': 8.5, 'Prova 2': 9.7, 'Prova 3': 6.6},
         'Luna': {'Prova 1': 5.0, 'Prova 2': 7.0, 'Prova 3': 4.3}
          }
» dfNotas = pd.DataFrame(dic)
» dfNotas
↳           Pedro     Ana    Luna
  Prova 1     5.4     8.5     5.0
  Prova 2     6.2     9.7     7.0
  Prova 3     7.9     6.6     4.3

» # o teste retorna um df com o mesmo shape que dfNotas
» dfNotas > 6
↳             Pedro    Ana    Luna
  Prova 1     False   True   False
  Prova 2      True   True    True
  Prova 3      True   True   False

» # os campos do df são filtrados pelo df booleano
» dfNotas[dfNotas > 6]
↳           Pedro     Ana    Luna
  Prova 1     NaN     8.5     NaN
  Prova 2     6.2     9.7     7.0
  Prova 3     7.9     6.6     NaN

Funções lambda

Uma função pode ser aplicada sobre elementos de uma coluna específica ou sobre todas as colunas. Veremos mais tarde detalhes sobre o uso de dataframe.apply().

» dfNotas
↳           Pedro     Ana    Luna
  Prova 1     5.4     8.5     5.0
  Prova 2     6.2     9.7     7.0
  Prova 3     7.9     6.6     4.3

» # uma função aplicada à todos os elementos do df
» dfNotas.apply(lambda x: x**2)
↳            Pedro     Ana     Luna
  Prova 1    29.16   72.25    25.00
  Prova 2    38.44   94.09    49.00
  Prova 3    62.41   43.56    18.49

Funções lambda que retornam valores booleanos podem ser usadas para filtragem dos campos de um dataframe. No exemplo dfPaises['pais'].apply(lambda x: len(x)) == 4 retorna True para as linhas onde o campo pais tem comprimento de 4 letras.

» dfPaises.loc[dfPaises['pais'].apply(lambda x: len(x)) == 4].head(2)
↳      continente    pais    ano   expVida   populacao     pibPercap
  264      Africa    Chad   1952    38.092     2682462   1178.665927
  265      Africa    Chad   1957    39.881     2894855   1308.495577

# são os países com nomes de 4 letras:
» set(dfPaises.loc[dfPaises['pais'].apply(lambda x: len(x)) == 4]['pais'])
↳ {'Chad', 'Cuba', 'Iran', 'Iraq', 'Mali', 'Oman', 'Peru', 'Togo'}

# o mesmo que
# dfPaises.loc[dfPaises['pais'].apply(lambda x: len(x)) == 4]['pais'].unique()  # (um array)

O seletor pode ser composto de mais testes, ligados pelos operadores lógicos & e |.

» # paises/anos com nomes compostos por mais de 2 palavras e população acima de 50 milhões
» dfPaises.loc[(dfPaises['pais'].apply(lambda x: len(x.split(' '))) > 2) &
               (dfPaises['populacao']>50_000_000)]

↳      continente    pais                  ano   expVida   populacao    pibPercap
  334      Africa    Congo, Dem. Rep.     2002    44.966    55379852   241.165876
  335      Africa    Congo, Dem. Rep.     2007    46.462    64606759   277.551859

Ordenamento com Sort

Para ordenar um dataframe podemos usar o método sort, com a seguinte sintaxe:

dataframe.sort_values(by=['campo'], axis=0, ascending=True, inplace=False)
onde
by pode ser uma string ou lista com o nome ou nomes dos campos, na prioridade de ordenamento,
axis{0 ou ‘index’, 1 ou ‘columns’} default 0, indica o eixo a ordenar,
ascending=True/False se ordenamento é crescente/decrescente.

Existem vários outros parâmetros para o controle de ordenamentos, como pode ser lido no API reference do pandas.

Muitas informações importantes sobre um conjunto de dados podem ser obtidas apenas pela inspecção dos dados. Por exemplo, podemos encontrar respostas para:

  • que país do mundo teve, em qualquer ano, o PIB percapita mais elevado?
  • no ano de 2007 (o último de nossa lista), quais são os 5 países com maior população, e quais são os 5 com PIB mais baixo, no mundo?
  • no ano de 2002, quantos países no mundo tinham PIB percapita acima e abaixo da média?
# encontramos o maior pib percapita e a linha que corresponde a ele   
» dfMax = dfPaises[dfPaises['pibPercap']==dfPaises['pibPercap'].max()]
» dfMax
↳     continente    pais   ano   expVida  populacao    pibPercap
  853       Asia  Kuwait  1957    58.033     212846  113523.1329

» # para formatar uma resposta amigável
» ano = dfMax['ano'].values[0]
» pais = dfMaxPib['pais'].values[0]
» pibP = dfMaxPib['pibPercap'].values[0]

» print('O PIB percapita máximo foi de {} e ocorreu no {} em {}.'.format(pibP, pais, ano))
↳ O PIB percapita máximo foi de 113523.1329 e ocorreu no Kuwait em 1957.

» # ordenando em ordem decrescente
» dfPaises.sort_values(by=['pibPercap'], ascending=False).iloc[[0]]
↳      continente     pais   ano  expVida  populacao     pibPercap
  853        Asia   Kuwait  1957   58.033     212846   113523.1329

Observe que dfMax['ano'] é uma Series que, se exposta diretamente, não contém apenas o ano. Por isso extraimos dele o valor, 1º campo: dfMax['ano'].value[0]. Idem para pais e pibPercap.

Claro que podemos também ordenar o dataframe em ordem descrecente no campo pibPercap e pegar apenas a 1ª linha.
dataframe.iloc[[0]] foi usado para pegar a 1ª linha, cujo índice é 853. A mesma linha seria retornada com dataframe.loc[[853]], o que mostra, mais uma vez, a diferença entre df.loc e df.iloc.

Para encontrar os 5 países com maior população em 2007 usamos a mesma técnica de ordenamento. Primeiro filtramos pelo ano = 2007, ordenamos por população, ordem inversa, e pegamos os 5 primeiros. Para exibir o resultado podemos transformar o dataframe em string, sem os índices.

Para encontrar os 5 países com maior população em 2007, e os 5 com menor PIB:

» # dataframe com 5 maiores populações em 2007
» popMax = dfPaises[dfPaises['ano']==2007].sort_values(by=['populacao'], ascending=False).head()

» print(popMax[['pais','populacao']].to_string(index=False))
↳          pais  populacao
          China 1318683096
          India 1110396331
  United States  301139947
      Indonesia  223547000
         Brazil  190010647

» # o 5 países com menor pib:
» # criamos um dataframe apenas do ano 2007 e acrescentamos o campo pib
» # pib = pibPercap * populacao
» df2007 = dfPaises[dfPaises['ano']==2007]
» df2007['pib'] = df2007['pibPercap'] * df2007['populacao']       

» # são os países com menor pib em 2007
» df2007.sort_values(by=['pib']).head()['pais']
↳ 1307    Sao Tome and Principe
  323                   Comoros
  635             Guinea-Bissau
  431                  Djibouti
  563                    Gambia
  Name: pais, dtype: object

# se não precisamos mais do df, podemos apagá-lo
» del df2007

Para saber quantos países tem PIB percapita acima e abaixo da média em 2002 primeiro encontramos essa média. Depois selecionamos as linhas que satisfazem com pibPercap >= media e pibPercap < media. Para saber quantas linhas restaram contamos, por exemplo, quantos elementos existem em seu index.

» # média do pibPercap em 2002 (um escalar)
» media2002 = dfPaises[dfPaises.ano==2002]['pibPercap'].mean()
» acima = dfPaises[(dfPaises.ano==2002) & (dfPaises.pibPercap ≥= media2002)].index.size
» abaixo = dfPaises[(dfPaises.ano==2002) & (dfPaises.pibPercap < media2002)].index.size

» print('[Dos {} países, {} tem PIB percapita acima da média, {} abaixo da média.'.format(acima+abaixo, acima, abaixo))
↳ Dos 142 países, 44 tem PIB percapita acima da média, 98 abaixo da média.

Obtenção e análise de um slice : Brasil

Em diversas circunstâncias queremos fazer análise de apenas um slice da dataframe geral. Além de simplificar o conjunto de campos podemos conseguir com isso um uso menor de espaço em memória e maior velocidade de processamento.
Podemos, por ex., obter um dataframe separado apenas com a os dados referentes ao Brasil. Passando como índice o array booleano dfPaises['pais'] == 'Brazil' apenas as linhas relativas a esse país serão retornadas.

» dfBrasil = dfPaises[dfPaises['pais'] == 'Brazil'][['ano', 'expVida', 'populacao', 'pibPercap']]
» dfBrasil.head()
↳ 
         ano  expVida   populacao     pibPercap
  168   1952   50.917    56602560   2108.944355
  169   1957   53.285    65551171   2487.365989
  170   1962   55.665    76039390   3336.585802
  171   1967   57.632    88049823   3429.864357
  172   1972   59.504   100840058   4985.711467

O dataframe dfBrasil tem os mesmos índices que aos do segmento de dfPaises, de onde ele foi retirado. Para restabelecer esses índices usamos dataFrame.reset_index(). Se utilizado com o parâmetro drop=True o índice antigo é excluído (e perdido), caso contrário é copiado como uma coluna do dataframe. Para atribuir um nome para o índice usamos dataframe.index.rename('novoNome', inplace=True).

» # os índices iniciais são
» dfBrasil.index
↳ Int64Index([168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179], dtype='int64')

» # resetamos os índices, abandonando a coluna de índices iniciais
» dfBrasil.reset_index(drop=True, inplace=True)
» # novos índices
» dfBrasil.index
↳ RangeIndex(start=0, stop=12, step=1)

» dfBrasil.index.rename('id', inplace=True)
» # o dataframe fica assim:
» dfBrasil.head(3)
↳       ano     expVida     populacao       pibPercap
  id
  0    1952      50.917      56602560     2108.944355
  1    1957      53.285      65551171     2487.365989
  2    1962      55.665      76039390     3336.585802

Podemos usar um campo qualquer como index, com qualquer dtype. No caso abaixo usamos o campo ano como índice.

» # vamos usar o campo ano como index
» dfBrasil.set_index('ano', inplace=True)
» dfBrasil.head(3)
↳           expVida      populacao       pibPercap
  ano             
  1952.0     50.917     56602560.0     2108.944355
  1957.0     53.285     65551171.0     2487.365989
  1962.0     55.665     76039390.0     3336.585802

» # agora os índices passam a ser o ano
» dfBrasil.loc[1997]            # é uma Series
↳ expVida      6.938800e+01
  populacao    1.685467e+08
  pibPercap    7.957981e+03
  Name: 1997.0, dtype: float64

» # dfBrasil.loc[[1997]]            # é um dataframe

Para restaurar a coluna ano copiamos o índice para essa coluna e restauramos o índice.

» # restauramos a coluna ano
» dfBrasil['ano'] = dfBrasil.index
» # e resetamos o indice
» dfBrasil.reset_index(drop=True, inplace=True)

» dfBrasil.head(3)
↳    expVida   populacao     pibPercap    ano
  0   50.917    56602560   2108.944355   1952
  1   53.285    65551171   2487.365989   1957
  2   55.665    76039390   3336.585802   1962

Linhas podem ser inseridas de várias formas. Um delas consiste em criar novos dataframes com as linhas a inserir e concatenar como a dataframe inicial. Para isso usamos pandas.concat(): pd.concat([dfInicio, dfFinal]).
Vamos inserir linhas com dados fictícios, apenas para efeito de aprendizado.

» colunas = ['expVida','populacao','pibPercap','ano']      # nomes das colunas, na ordem dos dados
» valores1 = [48.0,45000000,2000.0,1951]                   # valores a inserir no ínicio (ano 1951)
» valores2 = [75.0, 200000000, 9500.0, 2008]               # valores a inserir no final (ano 2008)
» dfP = pd.DataFrame([valores1], columns=colunas)          # df a inserir no ínicio
» dfU = pd.DataFrame([valores2], columns=colunas)          # df a inserir no final
» dfBrasil = pd.concat([dfP, dfBrasil])                    # 1ª linha + dfBrasil
» dfBrasil = pd.concat([dfBrasil, dfU])                    # dfBrasil + última linha

» # agora a 1ª linha é
» dfBrasil.iloc[[0]]
↳    expVida   populacao   pibPercap   ano
  0     48.0   45000000       2000.0  1951

» # a última linha é
» dfBrasil.iloc[[-1]]
↳    expVida   populacao  pibPercap   ano
  0     75.0   200000000     9500.0  2008

» # como os índices ficaram duplicados e desordenados fazemos um reordenamento
» dfBrasil.reset_index(drop=True, inplace=True)

» dfBrasil
↳      expVida     populacao       pibPercap      ano
  0     48.000      45000000     2000.000000     1951
  1     50.917      56602560     2108.944355     1952
  2     53.285      65551171     2487.365989     1957
  ------------ linhas 3 até 11 omitidas ----------------
  12    72.390     190010647     9065.800825     2007
  13    75.000     200000000     9500.000000     2008

Como essas linhas não contém dados corretos, vamos apagá-las. Para usamos dataframe.drop(linha, axis=0, inplace = True), onde linha é o label, que pode não ser numérico) da linha ou seu índice (numérico). Várias linhas podem ser apagadas com dataframe.drop([linha0,...,linhan], axis=0, inplace = True).

» # apagar linhas 0 e 13: axis = 0 se refere às linhas
» dfBrasil.drop([0,13], axis=0, inplace = True)

» # para reordenar os índices
» dfBrasil.reset_index(drop=True, inplace=True)

» # recolocar a coluna 'ano' no início
» dfBrasil = dfBrasil[['ano', 'expVida', 'populacao', 'pibPercap']]
» dfBrasil

# o estado do dataframe agora é
↳ dfBrasil
        ano    expVida     populacao       pibPercap
  0    1952     50.917      56602560     2108.944355
  1    1957     53.285      65551171     2487.365989
  2    1962     55.665      76039390     3336.585802
  ------------ linhas 3 até 8 omitidas ----------------
  9    1997     69.388     168546719     7957.980824
  10   2002     71.006     179914212     8131.212843
  11   2007     72.390     190010647     9065.800825

Vamos inserir uma coluna, atribuindo a ela um escalar (um valor único). Aqui ocorre, como nas Series, o broadcasting, onde o escalar é transformado em uma Series de tamanho apropriado antes de ser inserido na nova coluna. Todas as linhas terão o valor 42 no campo “novoCampo”.

Em seguida alteramos o valor dessa coluna em uma linha específica, usando dataframe.loc(númeroLinha, nomeColuna) ou dataframe.iloc(numeroLinha, numeroColuna). Depois, como essa é uma coluna indesejada, nos a apagamos usando dataframe.drop('nomeColuna', axis=1, inplace=True).

» dfBrasil['novoCampo'] = 42
» dfBrasil.head(3)
↳        ano     expVida     populacao       pibPercap   novoCampo
  0     1952      50.917      56602560     2108.944355          42
  1     1957      53.285      65551171     2487.365989          42
  2     1962      55.665      76039390     3336.585802          42

» # alteramos o 'novoCampo' na linha 1 (usando loc)
» # e a coluna 4 ('novoCampo') na linha 2 (usando iloc, fornecendo o índice)
» dfBrasil.loc[1,'novoCampo'] = 123456
» dfBrasil.iloc[2,4] = 22222

» dfBrasil.head(3)
↳      ano     expVida     populacao       pibPercap     novoCampo
  0   1952      50.917      56602560     2108.944355     42
  1   1957      53.285      65551171     2487.365989     123456
  2   1962      55.665      76039390     3336.585802     22222

» # apagamos essa coluna com drop
» dfBrasil.drop('novoCampo', axis=1, inplace=True)
» # o dataframe fica como no início

Um campo pode ser inserido como resultado de operações entre outros campos. No caso abaixo criamos uma coluna pib que é o produto das colunas populacao × pibPercap. O resultado é aplicado, em cada linha, à nova coluna, em notação científica. Na 1ª linha pib = 1.193716 × 1011.

Outra coluna marca a passagem de quando a expectativa de vida do brasileiro ultrapassa os 60 anos.

» dfBrasil.loc[:,'pib'] = dfBrasil['pibPercap'] * dfBrasil['populacao']
» dfBrasil.head(4)
↳        ano    expVida       populacao       pibPercap     pib
  0     1952     50.917      56602560.0     2108.944355     1.193716e+11
  1     1957     53.285      65551171.0     2487.365989     1.630498e+11
  2     1962     55.665      76039390.0     3336.585802     2.537119e+11
  3     1967     57.632      88049823.0     3429.864357     3.019989e+11

» # inserindo coluna 'acima60'(†)
» dfBrasil.loc[:,'acima60'] = dfBrasil['expVida'] > 60
» dfBrasil.loc[3:6,['ano','expVida','acima60']]
↳      ano   expVida  acima60
  3   1967    57.632    False
  4   1972    59.504    False
  5   1977    61.489     True
  6   1982    63.336     True
  
» dfBrasil[dfBrasil['acima60']]
» # todas as linhas com expVida > 60 são exibidas (output omitido)

» # as colunas podem ser removidas (para ficarmos com o dataframe original)
» dfBrasil.drop(['acima60', 'pib'], axis=1, inplace=True)

() dfBrasil['expVida'] > 60 é uma Series booleana.

Objetos de índices

Em um dataframe, assim como nas Series, a informação relativa aos índices e seus nomes (labels ), assim como os nomes dos eixos, são armazenados em objetos Index (índice). O objeto Index é imutável (não pode ser alterado após a construção).

» pdSerie = pd.Series(range(4), index=['a1', 'a2', 'a3', 'a4'])
» index = pdSerie.index
» index
↳ Index(['a1', 'a2', 'a3', 'a4'], dtype='object')
» # o índice é uma sequência (pode ser lido em slices)
» index[2]
↳ 'a3'
» index[2:]
↳ Index(['a3', 'a4'], dtype='object')

» # o index é imutável
» index[0] = 'A'
↳ TypeError: Index does not support mutable operations

» # já vimos que índices não fornecidos são preenchidos como um range
» pd.Series(range(4)).index
↳ RangeIndex(start=0, stop=4, step=1)
Uma UA é a distância média da Terra ao Sol.
1 UA ≈ 149,6 × 109 m.

No exemplo abaixo construimos primeiro um objeto Index usando pandas.Index(lista). Em seguida construimos uma Series usando esse index, contendo como valores as distâncias dos planeta até o Sol, em unidaddes astronômicas (UA). Com a Series inicializamos um dataframe com o mesmo index.

» # objeto index
» labels = pd.Index(np.array(['mercurio', 'venus', 'terra']))
» labels
↳ Index(['mercurio', 'venus', 'terra'], dtype='object')

» # Serie construída com esse index
» planetas = pd.Series([0.387, 0.723, 1], index=labels)
» planetas
↳ mercurio    0.387
  venus       0.723
  terra       1.000
  dtype: float64

» # o index da Series é o mesmo objeto que labels
» planetas.index is labels
↳ True

» # essa Series pode ser usada para construir um dataframe
» dfPlanetas = pd.DataFrame(planetas)
» dfPlanetas
↳             0
  mercurio    0.387
  venus       0.723
  terra       1.000

» # o index do dataframe é o mesmo que o da Series
» dfPlanetas.index is labels
↳ True

» # alteramos o nome da coluna
» dfPlanetas.rename(columns={0:'distancia'}, inplace=True)
» dfPlanetas
↳           distancia
  mercurio      0.387
  venus         0.723
  terra         1.000

Podemos inserir uma coluna, por exemplo, relativa ao diâmetro dos planetas (comparados ao diâmetro da Terra), atribuindo valores à uma nova coluna de nome ‘diametro’. O objeto atribuído deve ter o mesmo shape (ou passar por broadcasting). Alterar a ordem das colunas, o que pode ser feito com df.reindex(listaColunas), altera todo o dataframe (embora não inplace). O objeto retornado se ajusta de acordo com os índices fornecidos.

» # inserir uma nova coluna
» dfPlanetas['diametro'] = pd.Series([0.382, 0.949, 1], index=labels)
» dfPlanetas
↳         distancia   diametro
  mercurio    0.387      0.382
  venus       0.723      0.949
  terra       1.000      1.000

» # as colunas estão em um objeto Index
» dfPlanetas.columns
↳ Index(['distancia', 'diametro'], dtype='object')

» type(dfPlanetas.columns)
↳ pandas.core.indexes.base.Index

» 'distancia' in dfPlanetas.columns
↳ True

» # podemos alterar a ordem das colunas com reindex
» dfPlanetas.reindex(['venus','terra','mercurio'])
↳       distancia    diametro
  venus     0.723       0.949
  terra     1.000       1.000
  mercurio  0.387       0.382

» # podemos ordenar os índices para ordenar o dataframe
» idx = dfPlanetas.index
» idx = idx.sort_values()
» idx
↳ Index(['mercurio', 'terra', 'venus'], dtype='object')

» dfPlanetas.reindex(idx)
↳         distancia   diametro
  mercurio    0.387      0.382
  terra       1.000      1.000
  venus       0.723      0.949

Diferentes de um conjunto (set) objetos Index podem ter índices repetidos. Se índices inseridos não correspondem à dados existentes estes são preenchidos com NaN. Os parâmetros method='bfill' (ou “ffill” forçam as colunas (ou linhas) com NaN a serem preenchidos com valores das colunas (ou linhas) anteriores ou posteriores. Claro que reindexações podem ser também obtidas com df.loc e df.iloc.

» # índices de linhas repetidos
» duplicados = pd.Index(['mercurio', 'venus', 'terra', 'mercurio', 'marte'])
» duplicados
↳ Index(['mercurio', 'venus', 'terra', 'mercurio', 'marte'], dtype='object')

» dfPlanetas.reindex(duplicados)   # default é axis = 0
↳         distancia   diametro
  mercurio    0.387      0.382
  venus       0.723      0.949
  terra       1.000      1.000
  mercurio    0.387      0.382
  marte         NaN        NaN  

» # índices de colunas repetidos
» duplicados = pd.Index(['distancia', 'diametro', 'diametro', 'distancia', 'massa'])
» dfPlanetas.reindex(duplicados, axis=1)   # sobre colunas
↳          distancia  diametro  diametro  distancia  massa
  mercurio     0.387     0.382     0.382      0.387    NaN
  venus        0.723     0.949     0.949      0.723    NaN
  terra        1.000     1.000     1.000      1.000    NaN

» # method='bfill' lê valor da coluna anterior
» dfPlanetas.reindex(duplicados, axis=1, method='bfill')
↳          distancia  diametro  diametro  distancia    massa
  mercurio     0.387     0.382     0.382      0.387    0.387
  venus        0.723     0.949     0.949      0.723    0.723
  terra        1.000     1.000     1.000      1.000    1.000

» # use method='ffill' para copiar coluna posterior

» # reindexação com loc
» nCol = pd.Index(['diametro', 'distancia'])
» dfPlanetas.loc[['venus','terra'], ['diametro', 'distancia']]
↳        diametro   distancia
  venus     0.949       0.723
  terra     1.000       1.000
» # nCol pode ser uma lista: nCol = ['diametro', 'distancia']

De posse dos índices das linhas e colunas qualquer uma delas pode ser apagada com df.drop(lista, axis). As operações retornam o dataframe modificado, sem alterar o original, a menos que seja marcado o parâmetro inplace=True. Nesse caso os dados removidos serão perdidos.

» dfPlanetas
↳          distancia   diametro
  mercurio     0.387      0.382
  venus        0.723      0.949
  terra        1.000      1.000

» # apagando linhas (axis = 0 é default)
» dfPlanetas.drop(['venus', 'mercurio'])
↳     distancia     diametro
  terra     1.0         1.0

» # apagando colunas
» dfPlanetas.drop(['distancia'], axis=1)
↳           diametro
  mercurio     0.382
  venus        0.949
  terra        1.000

Os seguintes argumentos são usados com reindex

Argumento descrição
index Index ou sequência a ser usada como index,
method forma de interpolação: ‘ffill’ preenche com valor posterior, ‘bfill’ com valor anterior,
fill_value valor a usar quando dados não existentes são introduzidos por reindexing (ao invés de NaN),
limit quando preenchendo com valor anterior ou posterior, intervalo máximo a preencher (em número de elementos),
tolerance quando preenchendo com valor anterior ou posterior, intervalo máximo a preencher para valores inexatos (em distância numérica),
level combina Index simples no caso de MultiIndex; caso contrário seleciona subset,
copy se True, copia dados mesmo que novo índice seja equivalente ao índice antigo; se False, não copia dados quando índices são equivalentes.

Métodos e propriedades de Index

Método descrição
append concatena outro objeto Index objects, gerando novo Index
difference calcula a diferença de conjunto como um Index
intersection calcula intersecção de conjunto
union calcula união de conjunto
isin retorna array booleano indicando se cada valor está na coleção passada
delete apaga índice, recalculando Index
drop apaga índices passados, recalculando Index
insert insere índice, recalculando Index
is_monotonic retorna True se indices crescem de modo monotônico
is_unique returns True se não existem valores duplicados no Index
unique retorna índices sem repetições
🔺Início do artigo

Bibliografia

Consulte bibliografia completa em Pandas, Introdução neste site.

Nesse site: