Escolar Documentos
Profissional Documentos
Cultura Documentos
Pitão
Ciência de Dados
Manual
FERRAMENTAS ESSENCIAIS PARA TRABALHAR COM DADOS
distribuído por
Publicado por O'Reilly Media, Inc., 1005 Gravenstein Highway North, Sebastopol, CA 95472.
Os livros da O'Reilly podem ser adquiridos para uso educacional, comercial ou promocional de vendas. Edições online também estão
disponíveis para a maioria dos títulos (http://oreilly.com/safari). Para obter mais informações, entre em contato com nosso departamento de
vendas corporativo/institucional: 800-998-9938 ou corporate@oreilly.com.
O logotipo O'Reilly é uma marca registrada da O'Reilly Media, Inc. Python Data Science Handbook, a imagem da capa e a imagem comercial
relacionada são marcas registradas da O'Reilly Media, Inc.
Embora a editora e o autor tenham envidado esforços de boa fé para garantir que as informações e instruções contidas nesta obra sejam
precisas, a editora e o autor isentam-se de qualquer responsabilidade por erros ou omissões, incluindo, sem limitação, responsabilidade por
danos resultantes do uso ou confiança neste trabalho. O uso das informações e instruções contidas neste trabalho é por sua conta e risco.
Se quaisquer exemplos de código ou outra tecnologia que este trabalho contém ou descreve estão sujeitos a licenças de código aberto ou
aos direitos de propriedade intelectual de terceiros, é sua responsabilidade garantir que seu uso esteja em conformidade com tais licenças e/
ou direitos.
978-1-491-91205-8
[LSI]
Machine Translated by Google
Índice
Prefácio. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . XI
Documentação com ? 3
Atalhos de navegação 8
Atalhos de entrada de 9
saída História 13
Suprimindo Saída 15
iii
Machine Translated by Google
Erros e depuração 20
2. Introdução ao NumPy. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
Compreendendo os tipos de dados em Python 34
Remodelação de matrizes 47
Apresentando UFuncs 51
Apresentando Regras de 63
Broadcasting Broadcasting 65
na Prática 68
iv | Índice
Machine Translated by Google
máscaras Indexação 78
sofisticada Exemplo de 80
sofisticada Exemplo: 83
categorização de 85
Índice | v
Machine Translated by Google
nós
| Índice
Machine Translated by Google
configuração do 218
Índice | vii
Machine Translated by Google
Resumo 342
Resumo 375
viii | Índice
Machine Translated by Google
Regularização 396
Índice | ix
Machine Translated by Google
Exemplos 470
Índice. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 517
x | Índice
Machine Translated by Google
Prefácio
Este é um livro sobre como fazer ciência de dados com Python, que imediatamente levanta a questão: o que
é ciência de dados? É uma definição surpreendentemente difícil de definir, especialmente tendo em conta o
quão omnipresente o termo se tornou. Os críticos têm rejeitado o termo de várias maneiras, considerando-o
um rótulo supérfluo (afinal, que ciência não envolve dados?) ou um simples chavão que só existe para salgar
currículos e chamar a atenção de recrutadores de tecnologia excessivamente zelosos.
Na minha opinião, essas críticas deixam escapar algo importante. A ciência de dados, apesar do seu verniz
exagerado, é talvez o melhor rótulo que temos para o conjunto interdisciplinar de competências que se estão
a tornar cada vez mais importantes em muitas aplicações na indústria e no meio académico. Esta peça
interdisciplinar é fundamental: na minha opinião, a melhor definição existente de ciência de dados é ilustrada
pelo Diagrama de Venn de Ciência de Dados de Drew Conway, publicado pela primeira vez em seu blog em
setembro de 2010 (veja a Figura P-1) .
XI
Machine Translated by Google
Embora alguns dos rótulos de interseção sejam um pouco irônicos, este diagrama captura a
essência do que acho que as pessoas querem dizer quando dizem “ciência de dados”: é
fundamentalmente um assunto interdisciplinar. A ciência de dados compreende três áreas
distintas e sobrepostas: as competências de um estatístico que sabe modelar e resumir conjuntos
de dados (que estão a crescer cada vez mais); as habilidades de um cientista da computação
que pode projetar e usar algoritmos para armazenar, processar e visualizar esses dados com
eficiência; e a experiência no domínio – o que poderíamos chamar de formação “clássica” numa
disciplina – necessária tanto para formular as perguntas certas como para contextualizar as suas respostas.
Com isto em mente, encorajo-o a pensar na ciência de dados não como um novo domínio de
conhecimento a aprender, mas como um novo conjunto de competências que pode aplicar na
sua área de especialização atual. Esteja você divulgando resultados eleitorais, prevendo retornos
de ações, otimizando cliques em anúncios on-line, identificando microorganismos em fotos
microscópicas, buscando novas classes de objetos astronômicos ou trabalhando com dados em
qualquer outro campo, o objetivo deste livro é dar-lhe a capacidade de faça e responda novas
perguntas sobre a área de estudo escolhida.
O livro não pretende ser uma introdução ao Python ou à programação em geral; Presumo que o
leitor tenha familiaridade com a linguagem Python, incluindo definição de funções, atribuição de
variáveis, chamada de métodos de objetos, controle do fluxo de um programa e outras tarefas
básicas. Em vez disso, o objetivo é ajudar os usuários do Python a aprender a usar a pilha de
ciência de dados do Python – bibliotecas como IPython, NumPy, Pandas, Matplotlib, Scikit-Learn
e ferramentas relacionadas – para armazenar, manipular e obter insights dos dados de maneira
eficaz.
xii | Prefácio
Machine Translated by Google
A utilidade do Python para ciência de dados decorre principalmente do grande e ativo ecossistema de
pacotes de terceiros: NumPy para manipulação de dados homogêneos baseados em array, Pandas
para manipulação de dados heterogêneos e rotulados, SciPy para tarefas comuns de computação
científica, Matplotlib para publicação -visualizações de qualidade, IPython para execução interativa e
compartilhamento de código, Scikit-Learn para aprendizado de máquina e muitas outras ferramentas
que serão mencionadas nas páginas seguintes.
Se você está procurando um guia para a linguagem Python em si, sugiro o projeto irmão deste livro,
Um tour rápido pela linguagem Python. Este breve relatório oferece um tour pelos recursos essenciais
da linguagem Python, destinado a cientistas de dados que já estão familiarizados com uma ou mais
linguagens de programação.
livro usa a sintaxe do Python 3, que contém aprimoramentos de linguagem que não são compatíveis
com a série 2.x do Python. Embora o Python 3.0 tenha sido lançado pela primeira vez em 2008, a
adoção tem sido relativamente lenta, especialmente nas comunidades científicas e de desenvolvimento
web. Isso ocorre principalmente porque levou algum tempo para que muitos dos pacotes e kits de
ferramentas essenciais de terceiros se tornassem compatíveis com os componentes internos da nova
linguagem. Desde o início de 2014, entretanto, versões estáveis das ferramentas mais importantes do
ecossistema de ciência de dados têm sido totalmente compatíveis com Python 2 e 3 e, portanto, este
livro usará a sintaxe mais recente do Python 3. No entanto, a grande maioria dos trechos de código
neste livro também funcionará sem modificação no Python 2: nos casos em que uma sintaxe
incompatível com Py2 for usada, farei todos os esforços para anotá-la explicitamente.
NumPy (Capítulo 2)
Esta biblioteca fornece o objeto ndarray para armazenamento e manipulação eficiente de matrizes
de dados densas em Python.
Pandas (Capítulo 3)
Esta biblioteca fornece o objeto DataFrame para armazenamento e manipulação eficiente de
dados rotulados/colunares em Python.
Matplotlib (Capítulo 4)
Esta biblioteca fornece recursos para uma variedade flexível de visualizações de dados em
Python.
Prefácio | xiii
Machine Translated by Google
Scikit-Learn (Capítulo 5)
Esta biblioteca fornece implementações Python eficientes e limpas dos algoritmos de aprendizado
de máquina mais importantes e estabelecidos.
O mundo PyData é certamente muito maior do que esses cinco pacotes e cresce a cada dia. Com isso
em mente, faço todos os esforços nestas páginas para fornecer referências a outros esforços, projetos
e pacotes interessantes que estão ampliando os limites do que pode ser feito em Python. No entanto,
esses cinco são atualmente fundamentais para grande parte do trabalho que está sendo feito no espaço
da ciência de dados Python, e espero que continuem importantes mesmo à medida que o ecossistema
continua a crescer em torno deles.
Agradecemos, mas não exigimos, atribuição. Uma atribuição geralmente inclui título, autor, editora e
ISBN. Por exemplo, “Python Data Science Handbook de Jake VanderPlas (O'Reilly). Copyright 2017
Jake VanderPlas, 978-1-491-91205-8.”
Se você acha que o uso de exemplos de código está fora do uso justo ou da permissão dada acima,
sinta-se à vontade para nos contatar em permissions@oreilly.com.
Considerações de instalação
Instalar o Python e o conjunto de bibliotecas que permitem a computação científica é simples. Esta
seção descreverá algumas das considerações que você deve ter em mente ao configurar seu
computador.
Embora existam várias maneiras de instalar o Python, a que eu sugeriria para uso em ciência de dados
é a distribuição Anaconda, que funciona de forma semelhante quer você use Windows, Linux ou Mac
OS X. A distribuição Anaconda vem em duas versões:
• O Miniconda fornece o próprio interpretador Python, juntamente com uma ferramenta de linha de
comando chamada conda, que opera como um gerenciador de pacotes multiplataforma voltado para
XIV | Prefácio
Machine Translated by Google
em relação aos pacotes Python, semelhantes em espírito às ferramentas apt ou yum com as quais os usuários do
Linux podem estar familiarizados.
• Anaconda inclui Python e conda e, adicionalmente, agrupa um conjunto de outros pacotes pré-instalados
voltados para computação científica. Devido ao tamanho deste pacote, espera-se que a instalação
consuma vários gigabytes de espaço em disco.
Qualquer um dos pacotes incluídos no Anaconda também pode ser instalado manualmente no Miniconda;
por esse motivo sugiro começar com Miniconda.
Para começar, baixe e instale o pacote Miniconda (certifique-se de escolher uma versão com Python 3) e,
em seguida, instale os pacotes principais usados neste livro:
Ao longo do texto, também faremos uso de outras ferramentas mais especializadas do ecossistema científico
do Python; a instalação geralmente é tão fácil quanto digitar conda install packagename. Para obter mais
informações sobre o conda, incluindo informações sobre como criar e usar ambientes conda (que eu
recomendo fortemente), consulte a documentação online do conda.
Itálico
Indica novos termos, URLs, endereços de e-mail, nomes de arquivos e extensões de arquivos.
Largura constante
Usada para listagens de programas, bem como dentro de parágrafos para se referir a elementos do
programa, como nomes de variáveis ou funções, bancos de dados, tipos de dados, variáveis de
ambiente, instruções e palavras-chave.
Mostra comandos ou outro texto que deve ser digitado literalmente pelo usuário.
Mostra o texto que deve ser substituído por valores fornecidos pelo usuário ou por valores determinados
pelo contexto.
O'Reilly Safari
Prefácio | xv
Machine Translated by Google
Temos uma página web para este livro, onde listamos erratas, exemplos e qualquer informação
adicional. Você pode acessar esta página em http://bit.ly/python-data-sci-handbook.
Para comentar ou tirar dúvidas técnicas sobre este livro, envie um e-mail para
bookquestions@oreilly.com.
Para obter mais informações sobre nossos livros, cursos, conferências e notícias, consulte nosso
site em http://www.oreilly.com.
xvi | Prefácio
Machine Translated by Google
CAPÍTULO 1
Além de ser uma interface interativa útil para Python, o IPython também fornece uma série de
adições sintáticas úteis à linguagem; abordaremos as adições mais úteis aqui. Além disso, o
IPython está intimamente ligado ao projeto Jupyter, que fornece um notebook baseado em
navegador que é útil para desenvolvimento, colaboração, compartilhamento e até publicação de
resultados de ciência de dados. O notebook IPython é na verdade um caso especial da estrutura
mais ampla de notebooks Jupyter, que abrange notebooks para Julia, R e outras linguagens de
programação. Como exemplo da utilidade do formato de notebook, basta olhar para a página
que você está lendo: todo o manuscrito deste livro foi composto como um conjunto de notebooks
IPython.
IPython trata do uso eficaz do Python para computação científica interativa e com uso intensivo
de dados. Este capítulo começará examinando alguns dos recursos do IPython que são úteis
para a prática da ciência de dados, focando especialmente na sintaxe que ele oferece além dos
recursos padrão do Python. A seguir, nos aprofundaremos um pouco mais em alguns dos
“comandos mágicos” mais úteis que podem acelerar tarefas comuns na criação e uso de código
de ciência de dados. Por fim, abordaremos alguns dos recursos do notebook que o tornam útil na
compreensão de dados e no compartilhamento de resultados.
1
Machine Translated by Google
Concha ou Caderno?
Existem dois meios principais de usar o IPython que discutiremos neste capítulo: o shell IPython e o
notebook IPython. A maior parte do material deste capítulo é relevante para ambos, e os exemplos
alternarão entre eles dependendo do que for mais conveniente. Nas poucas seções que são relevantes
para apenas um ou outro, declararei explicitamente esse fato. Antes de começarmos, algumas palavras
sobre como iniciar o shell IPython e o notebook IPython.
como a maior parte deste livro, não foi projetado para ser absorvido passivamente. Recomendo que,
ao lê-lo, você acompanhe e experimente as ferramentas e a sintaxe que abordamos: a memória
muscular que você constrói ao fazer isso será muito mais útil do que o simples ato de ler sobre isso.
Comece iniciando o interpretador IPython digitando ipython na linha de comando; alternativamente, se
você instalou uma distribuição como Anaconda ou EPD, pode haver um launcher específico para seu
sistema (discutiremos isso mais detalhadamente em “Ajuda e Documentação em IPython” na página 3).
Notebook é uma interface gráfica baseada em navegador para o shell IPython e baseia-se nele em um
rico conjunto de recursos de exibição dinâmica. Além de executar instruções Python/IPython, o notebook
permite ao usuário incluir texto formatado, visualizações estáticas e dinâmicas, equações matemáticas,
widgets JavaScript e muito mais.
Além disso, esses documentos podem ser salvos de uma forma que permita que outras pessoas os
abram e executem o código em seus próprios sistemas.
Embora o notebook IPython seja visualizado e editado através da janela do seu navegador da web, ele
deve se conectar a um processo Python em execução para executar o código. Para iniciar este processo
(conhecido como “kernel”), execute o seguinte comando no shell do sistema:
$ caderno jupyter
Este comando iniciará um servidor web local que ficará visível para o seu navegador. Ele imediatamente
exibe um registro mostrando o que está fazendo; esse log será parecido com isto:
$ jupyter notebook
[NotebookApp] Servindo notebooks do diretório local: /Users/jakevdp/...
[NotebookApp] 0 kernels ativos
[NotebookApp] O IPython Notebook está rodando em: http://localhost:8888/ [NotebookApp]
Use Control-C para parar este servidor e desligar todos os kernels...
Ao emitir o comando, seu navegador padrão deverá abrir automaticamente e navegar até o URL local
listado; o endereço exato dependerá do seu sistema. Se o navegador não abrir automaticamente, você
pode abrir uma janela e abrir manualmente este endereço (http://localhost:8888/ neste exemplo).
Quando uma pessoa com mentalidade tecnológica é solicitada a ajudar um amigo, membro da família
ou colega com um problema de computador, na maioria das vezes é menos uma questão de saber a
resposta, mas de saber como encontrar rapidamente uma resposta desconhecida. Na ciência de dados
é a mesma coisa: recursos pesquisáveis da web, como documentação on-line, tópicos de listas de
discussão e respostas do Stack Overflow, contêm uma riqueza de informações, mesmo (especialmente?)
se for um tópico que você já pesquisou antes. Ser um praticante eficaz de ciência de dados tem menos
a ver com memorizar a ferramenta ou comando que você deve usar para cada situação possível e mais
com aprender a encontrar efetivamente as informações que você não conhece, seja por meio de um
mecanismo de pesquisa na web ou outro. significa.
Uma das funções mais úteis do IPython/Jupyter é diminuir a distância entre o usuário e o tipo de
documentação e pesquisa que o ajudará a realizar seu trabalho de maneira eficaz. Embora as pesquisas
na web ainda desempenhem um papel na resposta a perguntas complicadas, uma quantidade incrível
de informações pode ser encontrada apenas através do IPython. Alguns exemplos de perguntas que o
IPython pode ajudar a responder com apenas algumas teclas:
• Como chamo esta função? Que argumentos e opções tem? • Qual é a aparência do
Aqui discutiremos as ferramentas do IPython para acessar rapidamente essas informações, ou seja,
o ? personagem para explorar a documentação, o ?? caracteres para explorar o código-fonte e a tecla
Tab para preenchimento automático.
referência a uma string, conhecida como docstring, que na maioria dos casos conterá um resumo conciso
do objeto e como usá-lo. Python possui uma função help() integrada que pode acessar essas informações e
imprimir os resultados. Por exemplo, para ver a documentação da função len integrada , você pode fazer o
seguinte:
Em [1]: ajuda(len)
Ajuda sobre a função integrada len nos módulos integrados:
len(...)
len(objeto) -> inteiro
Dependendo do seu intérprete, essas informações podem ser exibidas como texto embutido ou em alguma
janela pop-up separada.
Como encontrar ajuda sobre um objeto é tão comum e útil, o IPython apresenta o ? caractere como um
atalho para acessar esta documentação e outras informações relevantes:
Em [2]: len?
Tipo: builtin_function_or_method Formato da
string: < função integrada len> Namespace:
Python embutido Docstring:
len(objeto)
-> inteiro
Esta notação funciona para praticamente qualquer coisa, incluindo métodos de objeto:
Em [3]: L = [1, 2, 3]
Em [4]: L.inserir?
String builtin_function_or_method Tipo:
form: < inserção de método integrado do objeto de lista em 0x1024b8ea8> Docstring:
L.insert(index, object) - insere o objeto antes do índice
Em [5]: L?
Tipo: lista Formato
de string: [1, 2, 3]
3
Comprimento:
Docstring: list() -> nova lista
de lista vazia (iterável) -> nova lista inicializada a partir de itens do iterável
É importante ressaltar que isso funcionará até mesmo para funções ou outros objetos que você mesmo criar!
Aqui definiremos uma pequena função com uma docstring:
....: devolver um ** 2
....:
Observe que para criar uma docstring para nossa função, simplesmente colocamos uma string literal na
primeira linha. Como as docstrings geralmente são múltiplas linhas, por convenção usamos a notação de
aspas triplas do Python para strings multilinhas.
Em [7]: quadrado?
Tipo: função Forma de string:
<função quadrada em 0x103713cb0> Definição: quadrado(a)
Esse acesso rápido à documentação por meio de docstrings é um dos motivos pelos quais você deve
adquirir o hábito de sempre adicionar essa documentação in-line ao código que você escreve!
Em [8]: quadrado??
Tipo: função
Formato de string: <função quadrada em 0x103713cb0> Definição:
quadrado(a)
Fonte:
def square(a):
"Retorne o quadrado de a" ** 2
devolver um
Para funções simples como essa, o ponto de interrogação duplo pode fornecer uma visão rápida dos
detalhes ocultos.
Se você brincar muito com isso, notará que às vezes o ?? suffix não exibe nenhum código-fonte:
geralmente ocorre porque o objeto em questão não é implementado em Python, mas em C ou alguma
outra linguagem de extensão compilada. Se for esse o caso, o ?? sufixo fornece a mesma saída que o ?
sufixo. Você encontrará isso especialmente com muitos dos objetos e tipos integrados do Python, por
exemplo, len acima:
Em [9]: len??
forma: builtin_function_or_method Tipo: String
< função integrada len> Namespace: Python builtin
Docstring: len(objeto) -> inteiro
Usando ? e/ou ?? oferece uma interface poderosa e rápida para encontrar informações sobre
o que qualquer função ou módulo Python faz.
A outra interface útil do IPython é o uso da tecla Tab para preenchimento automático e
exploração do conteúdo de objetos, módulos e namespaces. Nos exemplos que
a seguir, usaremos <TAB> para indicar quando a tecla Tab deve ser pressionada.
Cada objeto Python possui vários atributos e métodos associados a ele. Como com
a função de ajuda discutida antes, Python tem uma função dir integrada que retorna um
lista deles, mas a interface de preenchimento de guias é muito mais fácil de usar na prática. Ver
uma lista de todos os atributos disponíveis de um objeto, você pode digitar o nome do objeto a seguir
controlado por um ponto final (.) e pela tecla Tab:
Em [10]: L.<TAB>
L.append L.copy L.estender L.inserir L.remover L.sort
L.clear L.contagem índice L. L.pop L.reverso
Para restringir a lista, você pode digitar o primeiro caractere ou vários caracteres do
nome, e a tecla Tab encontrará os atributos e métodos correspondentes:
Em [10]: Lc<TAB>
L.limpar L.copiar L.contar
Em [10]: L.co<TAB>
L.copiar L.contar
Se houver apenas uma opção, pressionar a tecla Tab completará a linha para você. Para
Por exemplo, o seguinte será substituído instantaneamente por L.count:
Em [10]: L.cou<TAB>
Embora o Python não tenha uma distinção estritamente imposta entre público/externo
atributos e atributos privados/internos, por convenção, um sublinhado anterior é
usado para denotar tais métodos. Para maior clareza, esses métodos privados e métodos especiais
são omitidos da lista por padrão, mas é possível listá-los digitando explicitamente
o sublinhado:
Em [10]: L._<TAB>
L.__add__ L.__gt__ L.__reduzir__
L.__class__ L.__hash__ L.__reduzir_ex__
Para resumir, mostramos apenas as primeiras linhas da saída. A maioria destes são
Métodos especiais de sublinhado duplo do Python (geralmente apelidados de métodos “dunder”).
O preenchimento de tabulação também é útil ao importar objetos de pacotes. Aqui vamos usá-lo
para encontrar todas as importações possíveis no pacote itertools que começam com co:
Da mesma forma, você pode usar o preenchimento de tabulação para ver quais importações estão disponíveis em seu sistema.
tem (isso mudará dependendo de quais scripts e módulos de terceiros estão visíveis
para sua sessão Python):
(Observe que, por questões de brevidade, não imprimi aqui todos os 399 pacotes e módulos importáveis
no meu sistema.)
Por exemplo, podemos usar isso para listar todos os objetos no namespace que terminam com
Aviso:
Em [10]: *Aviso?
BytesWarning Aviso de tempo de execução
Da mesma forma, suponha que estejamos procurando um método de string que contenha a palavra find
em algum lugar em seu nome. Podemos pesquisá-lo desta forma:
Em [10]: str.*encontrar*?
str.find _
Acho que esse tipo de pesquisa curinga flexível pode ser muito útil para encontrar um comando
específico quando estou conhecendo um novo pacote ou me familiarizando com um pacote
familiar.
O shell do IPython não vai tão longe, mas fornece vários atalhos de teclado para navegação
rápida enquanto você digita comandos. Na verdade, esses atalhos não são fornecidos pelo
próprio IPython, mas por meio de sua dependência da biblioteca GNU Readline: portanto,
alguns dos atalhos a seguir podem diferir dependendo da configuração do seu sistema. Além
disso, embora alguns desses atalhos funcionem no notebook baseado em navegador, esta
seção é principalmente sobre atalhos no shell IPython.
Depois que você se acostumar com eles, eles podem ser muito úteis para executar
determinados comandos rapidamente, sem mover as mãos da posição “inicial” do teclado.
Se você é um usuário Emacs ou tem experiência com shells estilo Linux, o seguinte será muito
familiar. Agruparemos esses atalhos em algumas categorias: atalhos de navegação, atalhos
de entrada de texto, atalhos de histórico de comandos e atalhos diversos.
Atalhos de navegação
Embora o uso das teclas de seta esquerda e direita para avançar e retroceder na linha seja
bastante óbvio, existem outras opções que não exigem mover as mãos da posição “inicial” do
teclado:
Tecla Ação
Ctrl-b (ou a tecla de seta para a esquerda) Move o cursor um caractere para trás
Ctrl-f (ou a tecla de seta para a direita) Move o cursor um caractere para frente
Embora todos estejam familiarizados com o uso da tecla Backspace para excluir o caractere anterior
Para um indivíduo, alcançar a tecla muitas vezes requer alguma ginástica com os dedos menores, e só
exclui um único caractere de cada vez. No IPython existem vários atalhos para remoção
alguma parte do texto que você está digitando. Os mais imediatamente úteis deles são
os comandos para excluir linhas inteiras de texto. Você saberá que estes se tornaram o segundo
natureza se você estiver usando uma combinação de Ctrl-b e Ctrl-d em vez de alcance
pressionando a tecla Backspace para excluir o caractere anterior!
Tecla Ação
Ctrl-y Arranque (ou seja, cole) o texto que foi cortado anteriormente
Talvez os atalhos mais impactantes discutidos aqui sejam aqueles que o IPython fornece
para navegar no histórico de comandos. Este histórico de comandos vai além do seu atual
alugar sessão IPython: todo o seu histórico de comandos é armazenado em um banco de dados SQLite em
seu diretório de perfil IPython. A maneira mais direta de acessá-los é com
as teclas de seta para cima e para baixo para percorrer o histórico, mas existem outras opções como
bem:
Tecla Ação
Ctrl-p (ou a tecla de seta para cima) Acesse o comando anterior no histórico
Ctrl-n (ou a tecla de seta para baixo) Acessar o próximo comando no histórico
A pesquisa reversa pode ser particularmente útil. Lembre-se que na seção anterior nós
definiu uma função chamada quadrado. Vamos fazer uma pesquisa reversa em nossa história do Python a partir de um novo
Shell IPython e encontre esta definição novamente. Quando você pressiona Ctrl-r no IPython
terminal, você verá o seguinte prompt:
Em [1]:
(pesquisa reversa)`':
Se você começar a digitar caracteres neste prompt, o IPython preencherá automaticamente os caracteres mais recentes
comando, se houver, que corresponda a esses caracteres:
Em [1]:
(pesquisa reversa)`sqa': quadrado??
A qualquer momento, você pode adicionar mais caracteres para refinar a pesquisa ou pressionar
Ctrl-r novamente para pesquisar outro comando que corresponda à consulta. Se você
acompanhou a seção anterior, pressionar Ctrl-r mais duas vezes fornece:
Em [1]:
(pesquisa reversa)`sqa': def quadrado(a):
"""Retorne o quadrado de a"""
devolver um ** 2
Depois de encontrar o comando que procura, pressione Return e a pesquisa será encerrada.
Podemos então usar o comando recuperado e continuar com nossa sessão:
Observe que você também pode usar Ctrl-p/Ctrl-n ou as teclas de seta para cima/para baixo para
pesquisar no histórico, mas apenas combinando os caracteres no início da linha. Ou seja, se
você digitar def e pressionar Ctrl-p, ele encontrará o comando mais recente (se houver) em seu
histórico que começa com os caracteres def.
Atalhos diversos
Finalmente, existem alguns atalhos diversos que não se enquadram em nenhuma das categorias
anteriores, mas que são úteis para conhecer:
O atalho Ctrl-c em particular pode ser útil quando você inicia inadvertidamente um trabalho de
execução muito longa.
Embora alguns dos atalhos discutidos aqui possam parecer um pouco tediosos no início, eles
rapidamente se tornam automáticos com a prática. Depois de desenvolver essa memória
muscular, suspeito que você até desejará que ela estivesse disponível em outros contextos.
IPython é adicionado à sintaxe normal do Python. Eles são conhecidos no IPython como comandos
mágicos e são prefixados pelo caractere % . Esses comandos mágicos são projetados para resolver
sucintamente vários problemas comuns na análise de dados padrão.
Os comandos mágicos vêm em dois tipos: magias de linha, que são denotadas por um único prefixo
% e operam em uma única linha de entrada, e magias de célula, que são denotadas por um prefixo
%% duplo e operam em múltiplas linhas de entrada. Demonstraremos e discutiremos alguns breves
exemplos aqui, e voltaremos para uma discussão mais focada de vários comandos mágicos úteis
posteriormente neste capítulo.
você está trabalhando no interpretador IPython, um problema comum é que colar blocos de código
multilinha pode levar a erros inesperados, especialmente quando indentação e marcadores de
interpretador estão envolvidos. Um caso comum é você encontrar algum código de exemplo em um
site e querer colá-lo em seu intérprete. Considere a seguinte função simples:
O código é formatado como apareceria no interpretador Python e, se você copiar e colar diretamente
no IPython, receberá um erro:
Em [3]: %paste
>>> def donotthing(x):
... retornar x
Um comando com intenção semelhante é %cpaste, que abre um prompt interativo de múltiplas
linhas no qual você pode colar um ou mais pedaços de código para serem executados em lote:
Em [5]: %cpaste
Colando código; digite '--' sozinho na linha para parar ou use Ctrl-D. :>>> def
donotthing(x): return x
:...
:--
Esses comandos mágicos, como outros que veremos, disponibilizam funcionalidades que seriam
difíceis ou impossíveis em um interpretador Python padrão.
começar a desenvolver código mais extenso, você provavelmente trabalhará tanto no IPython para
exploração interativa quanto em um editor de texto para armazenar o código que deseja reutilizar. Em
vez de executar esse código em uma nova janela, pode ser conveniente executá-lo em sua sessão
IPython. Isso pode ser feito com a magia %run .
Por exemplo, imagine que você criou um arquivo myscript.py com o seguinte conteúdo:
#------------------------------------- # arquivo:
meuscript.py
def quadrado(x):
"""quadrar um número"""
return x ** 2
Observe também que depois de executar este script, todas as funções definidas nele estarão
disponíveis para uso em sua sessão IPython:
Existem várias opções para ajustar a forma como seu código é executado; você pode ver a
documentação normalmente, digitando %run? no interpretador IPython.
Outro exemplo de função mágica útil é %timeit, que determinará automaticamente o tempo de execução
da instrução Python de linha única que a segue. Por exemplo, podemos querer verificar o desempenho
de uma compreensão de lista:
A vantagem do %timeit é que, para comandos curtos, ele executará automaticamente várias execuções para
obter resultados mais robustos. Para instruções multilinhas, adicionar um segundo sinal % transformará isso em
uma célula mágica que pode lidar com múltiplas linhas de entrada.
Por exemplo, aqui está a construção equivalente com um loop for :
Podemos ver imediatamente que a compreensão da lista é cerca de 10% mais rápida do que o equivalente para
construção de loop neste caso. Exploraremos %timeit e outras abordagens para código de tempo e criação de
perfil em “Criação de perfil e código de tempo” na página 25.
normais do Python, as funções mágicas do IPython possuem docstrings, e esta documentação útil pode ser
acessada da maneira padrão. Então, por exemplo, para ler a documentação da mágica %timeit , basta digitar isto:
Em [10]: %timeit?
A documentação para outras funções pode ser acessada de forma semelhante. Para acessar uma descrição
geral das funções mágicas disponíveis, incluindo alguns exemplos, você pode digitar isto:
Em [11]: %mágica
Para obter uma lista rápida e simples de todas as funções mágicas disponíveis, digite isto:
Em [12]: %lsmagic
Finalmente, mencionarei que é bastante simples definir suas próprias funções mágicas, se desejar. Não
discutiremos isso aqui, mas se você estiver interessado, consulte as referências listadas em “Mais recursos do
IPython” na página 30.
que você esteja bastante familiarizado com os prompts de estilo In[1]:/Out[1]: usados pelo IPython. Mas acontece
que não se trata apenas de uma decoração bonita: dão uma pista
sobre como você pode acessar entradas e saídas anteriores em sua sessão atual. Imagine que você
inicia uma sessão parecida com esta:
Em [2]: math.sin(2)
Fora[2]: 0,9092974268256817
Em [3]: math.cos(2)
Saída[3]: -0,4161468365471424
Entrada [5]:
Saída [5]: {2: 0,9092974268256817, 3: -0,4161468365471424}
O objeto In é uma lista que controla os comandos em ordem (o primeiro item da lista é um espaço
reservado para que In[1] possa se referir ao primeiro comando):
O objeto Out não é uma lista, mas um dicionário que mapeia números de entrada para suas saídas
(se houver):
Observe que nem todas as operações têm saídas: por exemplo, instruções de importação e instruções
de impressão não afetam a saída. A última opção pode ser surpreendente, mas faz sentido se você
considerar que print é uma função que retorna None; por uma questão de brevidade, qualquer comando
que retorna None não é adicionado a Out.
Isso pode ser útil se você quiser interagir com resultados anteriores. Por exemplo, vamos verificar a
soma de sin(2) ** 2 e cos(2) ** 2 usando os resultados calculados anteriormente:
O resultado é 1,0 , como seria de esperar da conhecida identidade trigonométrica. Neste caso, usar
estes resultados anteriores provavelmente não é necessário, mas pode ser muito útil se você executar
um cálculo muito caro e quiser reutilizar o resultado!
padrão do Python contém apenas um atalho simples para acessar a saída anterior; a
variável _ (ou seja, um único sublinhado) é mantida atualizada com a saída anterior; isso
também funciona no IPython:
Em [9]: imprimir(_) 1,0
Mas o IPython vai um pouco além – você pode usar um sublinhado duplo para acessar a
penúltima saída e um sublinhado triplo para acessar a penúltima saída (ignorando qualquer
comando sem saída):
Em [10]: imprimir(__)
-0,4161468365471424
Em [11]: imprimir(___)
0,9092974268256817
O IPython para por aí: mais de três sublinhados começam a ficar um pouco difíceis de contar
e, nesse ponto, é mais fácil consultar a saída pelo número da linha.
Há mais um atalho que devemos mencionar, entretanto – uma abreviação para Out[X] é _X
(ou seja, um único sublinhado seguido pelo número da linha):
Entrada [12]: Saída[2]
Fora[12]: 0,9092974268256817
Entrada [13]:
_2 Saída [13]: 0,9092974268256817
Suprimindo a saída Às
vezes você pode desejar suprimir a saída de uma instrução (isso talvez seja mais comum
com os comandos de plotagem que exploraremos no Capítulo 4). Ou talvez o comando que
você está executando produza um resultado que você prefere não armazenar em seu
histórico de saída, talvez para que possa ser desalocado quando outras referências forem
removidas. A maneira mais fácil de suprimir a saída de um comando é adicionar um ponto e
vírgula ao final da linha:
Observe que o resultado é calculado silenciosamente e a saída não é exibida na tela nem
armazenada no dicionário Out :
Entrada [15]: 14 entrada Saída
Fora[15]: Falso
Para acessar um lote de entradas anteriores de uma só vez, o comando mágico %history é muito útil.
Aqui está como você pode imprimir as primeiras quatro entradas:
Como sempre, você pode digitar %history? para obter mais informações e uma descrição das opções
disponíveis. Outros comandos mágicos semelhantes são %rerun (que reexecutará alguma parte do
histórico de comandos) e %save (que salva algum conjunto do histórico de comandos em um arquivo).
Para obter mais informações, sugiro explorá-los usando o ? funcionalidade de ajuda discutida em
“Ajuda e documentação no IPython” na página 3.
O seguinte pressupõe que você esteja em um sistema semelhante ao Unix, como Linux ou Mac OS X.
Alguns dos exemplos a seguir falharão no Windows, que usa um tipo diferente de shell por padrão
(embora com o anúncio de shells Bash nativos no Windows em 2016, em breve isso poderá não ser
mais um problema!). Se você não estiver familiarizado com comandos shell, sugiro revisar o Tutorial
Shell elaborado pela sempre excelente Software Carpentry Foundation.
os sistemas são os precursores dos shells e terminais que a maioria dos cientistas de dados ativos
ainda usam hoje.
Alguém não familiarizado com o shell pode perguntar por que você se importaria com isso, quando
você pode obter muitos resultados simplesmente clicando em ícones e menus. Um usuário do shell
pode responder com outra pergunta: por que procurar ícones e clicar em menus quando você pode
realizar coisas com muito mais facilidade digitando? Embora possa parecer um típico impasse de
preferência tecnológica, ao ir além das tarefas básicas, rapidamente fica claro que o shell oferece
muito mais controle de tarefas avançadas, embora a curva de aprendizado possa intimidar o usuário
médio de computador.
Como exemplo, aqui está um exemplo de uma sessão de shell do Linux/OS X onde um usuário
explora, cria e modifica diretórios e arquivos em seu sistema (osx:~ $ é o prompt, e tudo após o sinal
$ é o comando digitado ; o texto precedido por um # é apenas uma descrição, em vez de algo que
você realmente digitaria):
osx:~ $ echo "olá mundo" olá # echo é como a função print do Python
mundo
osx:projects $ pwd /
home/jake/projects
osx: projetos $ ls
datasci_book mpld3 meuprojeto.txt
osx:projetos $ cd meuprojeto/
Observe que tudo isso é apenas uma maneira compacta de realizar operações familiares
(navegar em uma estrutura de diretórios, criar um diretório, mover um arquivo, etc.) digitando
comandos em vez de clicar em ícones e menus. Observe que com apenas alguns comandos
(pwd, ls, cd, mkdir e cp) você pode realizar muitas das operações de arquivo mais comuns. É
quando você vai além desses princípios básicos que a abordagem shell se torna realmente poderosa.
usar qualquer comando que funcione na linha de comando no IPython prefixando-o com ! personagem.
Por exemplo, os comandos ls, pwd e echo podem ser executados da seguinte forma:
Em [1]: !ls
meuprojeto.txt
Em [2]: !pwd /
home/jake/projects/myproject
Em [4]: conteúdo = !
Em [5]: print(conteúdo)
['meuprojeto.txt']
Em [7]: print(diretório)
['/Usuários/jakevdp/notebooks/tmp/meuprojeto']
Observe que esses resultados não são retornados como listas, mas como um tipo de retorno de shell
especial definido no IPython:
Em [8]: tipo(diretório)
IPython.utils.text.SList
Isso se parece e funciona muito como uma lista Python, mas possui funcionalidades adicionais, como os
métodos grep e campos e as propriedades s, n e p que permitem pesquisar, filtrar e exibir os resultados
de maneiras convenientes. Para obter mais informações sobre isso, você pode usar os recursos de ajuda
integrados do IPython.
A comunicação na outra direção – passando variáveis Python para o shell – é possível através da sintaxe
{varname} :
As chaves contêm o nome da variável, que é substituído pelo conteúdo da variável no comando shell.
Se você brincar com os comandos shell do IPython por um tempo, poderá perceber que não pode
usar !cd para navegar no sistema de arquivos:
Em [11]: !pwd /
home/jake/projects/myproject
Em [12]: !cd ..
Em [13]: !pwd /
home/jake/projects/myproject
Em [14]: %cd .. /
home/jake/projects
Na verdade, por padrão você pode até usar isso sem o sinal % :
Em [15]: cd meuprojeto /
home/jake/projects/myproject
Isso é conhecido como função automagic e esse comportamento pode ser alternado com a função
mágica %automagic .
Além de %cd, outras funções mágicas semelhantes a shell disponíveis são %cat, %cp, %env, %ls,
%man, %mkdir, %more, %mv, %pwd, %rm e %rmdir, qualquer uma das quais pode ser usado sem
o sinal % se o automagic estiver ativado. Isso faz com que você quase possa tratar o prompt do
IPython como se fosse um shell normal:
Em [17]: ls
meuprojeto.txt tmp/
Em [19]: ls tmp
meuprojeto.txt
Em [20]: rm -r tmp
Esse acesso ao shell a partir da mesma janela de terminal da sua sessão Python significa que há
muito menos alternância entre o interpretador e o shell enquanto você escreve seu código Python.
Erros e depuração
O desenvolvimento de código e a análise de dados sempre exigem um pouco de tentativa e erro, e o
IPython contém ferramentas para agilizar esse processo. Esta seção cobrirá brevemente algumas
opções para controlar o relatório de exceções do Python, seguido pela exploração de ferramentas
para depuração de erros no código.
das vezes, quando um script Python falha, ele gera uma exceção. Quando o intérprete atinge uma
dessas exceções, informações sobre a causa do erro podem ser encontradas no traceback, que pode
ser acessado no Python. Com a função mágica %xmode , o IPython permite controlar a quantidade
de informações impressas quando a exceção é gerada. Considere o seguinte código:
def função2(x):
uma = x
b=x-1
retorno func1(a, b)
Em[2]: func2(1)
-------------------------------------------------- -------------------------
<ipython-input-1-d849e34d61fb> em func2(x)
5 uma = x
6 b=x-1
----> 7 retorno func1(a, b)
Chamar func2 resulta em erro e ler o rastreamento impresso nos permite ver exatamente o que
aconteceu. Por padrão, esse rastreamento inclui diversas linhas mostrando o contexto de cada
etapa que levou ao erro. Usando a função mágica %xmode (abreviação de modo de exceção),
podemos alterar quais informações são impressas.
Em[4]: func2(1)
-------------------------------------------------- ----------
O modo Verbose adiciona algumas informações extras, incluindo os argumentos para quaisquer
funções chamadas:
Em[6]: func2(1)
-------------------------------------------------- -------------------------
<ipython-input-1-d849e34d61fb> em func2(x=1)
5 a = xb
6 =x-1
----> 7 retorno func1(a, b)
func1 global = <função func1 em 0x1037294d0> a = 1
b=0
Erros e depuração | 21
Machine Translated by Google
uma =
1b=0
3
4 def função2(x): 5
uma = x
Essas informações extras podem ajudá-lo a entender por que a exceção está sendo gerada.
Então, por que não usar o modo Verbose o tempo todo? À medida que o código fica complicado, esse
tipo de rastreamento pode se tornar extremamente longo. Dependendo do contexto, às vezes é mais
fácil trabalhar com a brevidade do modo Padrão .
Python padrão para depuração interativa é o pdb, o depurador Python. Este depurador permite que o
usuário percorra o código linha por linha para ver o que pode estar causando um erro mais difícil. A
versão aprimorada pelo IPython é o ipdb, o depurador IPython.
Há muitas maneiras de iniciar e usar esses dois depuradores; não os cobriremos completamente aqui.
Consulte a documentação online desses dois utilitários para saber mais.
No IPython, talvez a interface mais conveniente para depuração seja o comando mágico %debug . Se
você chamá-lo após atingir uma exceção, ele abrirá automaticamente um prompt de depuração interativo
no ponto da exceção. O prompt ipdb permite explorar o estado atual da pilha, explorar as variáveis
disponíveis e até mesmo executar comandos Python!
Vejamos a exceção mais recente e, em seguida, executemos algumas tarefas básicas — imprima os
valores de a e b e digite quit para encerrar a sessão de depuração:
Em[7]: % depuração
ipdb> imprimir(b) 0
ipdb> sair
O depurador interativo permite muito mais do que isso - podemos até subir e descer na pilha e
explorar os valores das variáveis lá:
Em[8]: % depuração
ipdb> up >
<ipython-input-1-d849e34d61fb>(7)func2()
5 uma = x
6 b=x-1
----> 7 retorno func1(a, b)
ipdb> print(x) 1
ipdb> up >
<ipython-input-6-b2e110f6fc8f>(1)<module>() ----> 1 func2(1)
ipdb> abaixo
> <ipython-input-1-d849e34d61fb>(7)func2()
5 uma = x
6 b =x - 1
----> 7 retornar função1 (a, b)
ipdb> sair
Isso permite descobrir rapidamente não apenas o que causou o erro, mas também quais
chamadas de função levaram ao erro.
Se quiser que o depurador seja iniciado automaticamente sempre que uma exceção for gerada,
você pode usar a função mágica %pdb para ativar este comportamento automático:
Erros e depuração | 23
Machine Translated by Google
> <ipython-input-1-d849e34d61fb>(2)func1()
1 função def1 (a, b):
----> 2 retornar a/b
3
ipdb> imprimir(b)
0
ipdb> sair
Finalmente, se você tiver um script que gostaria de executar desde o início de forma interativa
modo, você pode executá-lo com o comando % run -d e usar o próximo comando para avançar
através das linhas de código de forma interativa.
Existem muito mais comandos disponíveis para depuração interativa do que listamos
aqui; a tabela a seguir contém uma descrição de alguns dos mais comuns e
úteis:
Comando Descrição
Imprimir variáveis
p(correr)
etapa) Entre em uma sub-rotina
Para obter mais informações, use o comando help no depurador ou dê uma olhada em
Documentação on-line do ipdb .
Mas depois que seu código estiver funcionando, pode ser útil investigar um pouco sua eficiência.
Às vezes é útil verificar o tempo de execução de um determinado comando ou conjunto de comandos; outras vezes, é
útil aprofundar-se em um processo multilinha e determinar onde está o gargalo em algumas séries complicadas de
operações. IPython fornece acesso a uma ampla gama de funcionalidades para esse tipo de tempo e perfil de código.
Aqui discutiremos os seguintes comandos mágicos do IPython:
%tempo
%tempoit
Tempo de execução repetida de uma única instrução para maior precisão
% ameixas secas
%
Execute o código com o criador de perfil linha por linha
%meme
Meça o uso de memória de uma única instrução
%mprun
Execute o código com o criador de perfil de memória linha por linha
Os últimos quatro comandos não estão incluídos no IPython – você precisará instalar as extensões line_profiler e
memory_profiler , que discutiremos nas seções a seguir.
Observe que, como esta operação é muito rápida, %timeit faz automaticamente um grande número de repetições. Para
comandos mais lentos, %timeit irá ajustar automaticamente e realizar menos repetições:
Às vezes, repetir uma operação não é a melhor opção. Por exemplo, se tivermos uma lista que gostaríamos de ordenar,
podemos ser enganados por uma operação repetida. Classificar uma lista pré-classificada é muito mais rápido do que
classificar uma lista não classificada, portanto a repetição distorcerá o resultado:
Para isso, a função mágica %time pode ser uma escolha melhor. Também é uma boa opção para comandos de execução
mais longa, quando é improvável que atrasos curtos relacionados ao sistema afetem o resultado. Vamos cronometrar a
classificação de uma lista não classificada e pré-classificada:
Observe o quão mais rápido é a classificação da lista pré-classificada, mas observe também quanto tempo demora o tempo
com %time versus %timeit, mesmo para a lista pré-classificada! Isso é resultado do fato de que %timeit faz algumas coisas
inteligentes nos bastidores para evitar que chamadas de sistema interfiram no tempo. Por exemplo, evita a limpeza de
objetos Python não utilizados (conhecidos como coleta de lixo) que poderiam afetar o tempo.
Por esse motivo, os resultados de %timeit geralmente são visivelmente mais rápidos que os resultados de %time .
Para %time como com %timeit, usar a sintaxe cell-magic com sinal de porcentagem dupla permite cronometrar scripts
multilinhas:
Em[6]: %%tempo
total = 0
para i no intervalo (1000):
para j no intervalo (1000):
total += eu * (-1) ** j
Tempos de CPU: usuário 504 ms, sys: 979 µs, total: 505 ms
Tempo de parede: 505 ms
Para obter mais informações sobre %time e %timeit, bem como suas opções disponíveis, use
a funcionalidade de ajuda do IPython (ou seja, digite %time? no prompt do IPython).
A título de exemplo, definiremos uma função simples que faz alguns cálculos:
Agora podemos chamar %prun com uma chamada de função para ver os resultados do perfil:
O resultado é uma tabela que indica, em ordem de tempo total em cada chamada de função, onde
a execução está gastando mais tempo. Neste caso, a maior parte do tempo de execução está em
a compreensão da lista dentro de sum_of_lists. A partir daqui, poderíamos começar a pensar
sobre quais mudanças podemos fazer para melhorar o desempenho do algoritmo.
Para mais informações sobre %prun, bem como suas opções disponíveis, use a ajuda do IPython
funcionalidade (ou seja, digite %prun? no prompt do IPython).
Em seguida, você pode usar o IPython para carregar a extensão line_profiler IPython, oferecida como
parte deste pacote:
Agora, o comando %lprun fará um perfil linha por linha de qualquer função - neste
Nesse caso, precisamos informar explicitamente quais funções estamos interessados em criar o perfil:
Como antes, o notebook envia o resultado para o pager, mas é mais ou menos assim:
A informação no topo dá-nos a chave para a leitura dos resultados: o tempo é reportado
em microssegundos e podemos ver onde o programa está gastando mais tempo. Neste
ponto, poderemos usar essas informações para modificar aspectos do script e
fazer com que ele funcione melhor para o caso de uso desejado.
Para mais informações sobre %lprun, bem como suas opções disponíveis, use a ajuda do IPython
funcionalidade (ou seja, digite %lprun? no prompt do IPython).
aspecto da criação de perfil é a quantidade de memória que uma operação usa. Isso pode ser avaliado
com outra extensão IPython, o memory_profiler. Tal como acontece com o line_profiler, começamos
instalando a extensão pelo pip:
A extensão do Memory Profiler contém duas funções mágicas úteis: a %memit magic (que oferece um
equivalente de medição de memória a %timeit) e a função %mprun (que oferece um equivalente de
medição de memória a %lprun). A função %memit pode ser usada de forma bastante simples:
Para uma descrição linha por linha do uso da memória, podemos usar a mágica %mprun . Infelizmente,
essa mágica funciona apenas para funções definidas em módulos separados, e não no próprio
notebook, então começaremos usando a mágica %%file para criar um módulo simples chamado
mprun_demo.py, que contém nossa função sum_of_lists , com um adição que tornará nossos resultados
de perfil de memória mais claros:
Substituindo mprun_demo.py
Agora podemos importar a nova versão desta função e executar o perfilador de linha de memória:
O resultado, impresso no pager, nos dá um resumo do uso de memória da função e é mais ou menos
assim:
Aqui a coluna Incremento nos diz o quanto cada linha afeta a memória total
orçamento: observe que quando criamos e excluímos a lista L, estamos adicionando cerca de 25 MB
de uso de memória. Isso se soma ao uso de memória em segundo plano do Python
próprio intérprete.
Para mais informações sobre %memit e %mprun, bem como suas opções disponíveis, use
a funcionalidade de ajuda do IPython (ou seja, digite %memit? no prompt do IPython).
Recursos da Web
O site IPython
O site do IPython contém links para documentação, exemplos, tutoriais e uma variedade de
outros recursos.
O site nbviewer
Este site mostra renderizações estáticas de qualquer notebook IPython disponível no Inter-
líquido. A primeira página apresenta alguns exemplos de cadernos que você pode navegar para ver
para que outras pessoas estão usando o IPython!
Tutoriais em
vídeo Pesquisando na Internet, você encontrará muitos tutoriais gravados em vídeo sobre IPython.
Eu recomendo especialmente procurar tutoriais nas conferências PyCon, SciPy e PyData de
Fernando Perez e Brian Granger, dois dos principais criadores e mantenedores do IPython e
Jupyter.
Livros
Python para análise de dados
O livro de Wes McKinney inclui um capítulo que aborda o uso do IPython como cientista de
dados. Embora grande parte do material se sobreponha ao que discutimos aqui, outra perspectiva
é sempre útil.
Finalmente, um lembrete de que você pode encontrar ajuda por conta própria: a funcionalidade de
ajuda baseada em ? do IPython (discutida em “Ajuda e documentação no IPython” na página 3) pode
ser muito útil se você usá-la bem e com frequência. À medida que você analisa os exemplos aqui e
em outros lugares, você pode usá-los para se familiarizar com todas as ferramentas que o IPython
tem a oferecer.
CAPÍTULO 2
Introdução ao NumPy
Este capítulo, junto com o Capítulo 3, descreve técnicas para carregar, armazenar e manipular
efetivamente dados na memória em Python. O tópico é muito amplo: os conjuntos de dados podem vir
de uma ampla gama de fontes e de uma ampla variedade de formatos, incluindo coleções de documentos,
coleções de imagens, coleções de clipes de som, coleções de medições numéricas ou quase qualquer
outra coisa. Apesar desta aparente heterogeneidade, ajudar-nos-á a pensar em todos os dados
fundamentalmente como matrizes de números.
Por exemplo, as imagens – especialmente as imagens digitais – podem ser consideradas simplesmente
matrizes bidimensionais de números que representam o brilho dos pixels em toda a área. Os clipes de
som podem ser considerados matrizes unidimensionais de intensidade versus tempo. O texto pode ser
convertido de várias maneiras em representações numéricas, talvez dígitos binários representando a
frequência de certas palavras ou pares de palavras. Não importa quais sejam os dados, o primeiro passo
para torná-los analisáveis será transformá-los em matrizes de números. (Discutiremos alguns exemplos
específicos desse processo posteriormente em “Engenharia de recursos” na página 375.)
Por esse motivo, o armazenamento e a manipulação eficientes de matrizes numéricas são absolutamente
fundamentais para o processo de ciência de dados. Vamos agora dar uma olhada nas ferramentas
especializadas que Python possui para lidar com tais arrays numéricos: o pacote NumPy e o pacote
Pandas (discutidos no Capítulo 3).
Este capítulo cobrirá o NumPy em detalhes. NumPy (abreviação de Numerical Python) fornece uma
interface eficiente para armazenar e operar em buffers de dados densos. De certa forma, os arrays
NumPy são como o tipo de lista integrado do Python , mas os arrays NumPy fornecem armazenamento
e operações de dados muito mais eficientes à medida que os arrays aumentam de tamanho. Os arrays
NumPy formam o núcleo de quase todo o ecossistema de ferramentas de ciência de dados em Python,
portanto, o tempo gasto aprendendo a usar o NumPy de maneira eficaz será valioso, independentemente
do aspecto da ciência de dados de seu interesse.
33
Machine Translated by Google
Se você seguiu os conselhos descritos no prefácio e instalou a pilha Anaconda, você já tem o NumPy
instalado e pronto para uso. Se você prefere fazer você mesmo, pode acessar o site do NumPy e
seguir as instruções de instalação encontradas lá. Depois de fazer isso, você pode importar o NumPy
e verificar a versão:
Em [1]: importar
numpy numpy.__versão__
Saída[1]: '1.11.1'
Para as partes do pacote discutidas aqui, eu recomendaria o NumPy versão 1.8 ou posterior. Por
convenção, você descobrirá que a maioria das pessoas no mundo SciPy/PyData importará NumPy
usando np como alias:
Ao longo deste capítulo, e de fato no restante do livro, você descobrirá que é assim que importaremos
e usaremos o NumPy.
Por exemplo, para exibir todo o conteúdo do namespace numpy , você pode digitar isto:
Em [3]: np.<TAB>
Em [4]: np?
Documentação mais detalhada, juntamente com tutoriais e outros recursos, pode ser encontrada
em http://www.numpy.org.
Os usuários de Python geralmente são atraídos por sua facilidade de uso, uma das quais é a digitação
dinâmica. Embora uma linguagem de tipo estaticamente como C ou Java exija que cada variável seja
declarada explicitamente, uma linguagem de tipo dinâmico como Python ignora esta especificação.
Por exemplo, em C você pode especificar uma operação específica da seguinte forma:
/ * código C */
int resultado = 0;
for(int i=0; i<100; i++){ resultado
+= i;
}
# Resultado do
código Python
= 0 para i no intervalo
(100): resultado += i
Observe a principal diferença: em C, os tipos de dados de cada variável são declarados explicitamente,
enquanto em Python os tipos são inferidos dinamicamente. Isto significa, por exemplo, que podemos
atribuir qualquer tipo de dado a qualquer variável:
# Código Python
x=4
x = "quatro"
Aqui trocamos o conteúdo de x de um número inteiro para uma string. A mesma coisa em C levaria
(dependendo das configurações do compilador) a um erro de compilação ou outras consequências não
intencionais:
/ * código C */
int x = 4; x
= “quatro”; //FALHA
Esse tipo de flexibilidade é uma peça que torna o Python e outras linguagens de tipo dinâmico convenientes
e fáceis de usar. Compreender como isso funciona é um aprendizado importante para analisar dados de
maneira eficiente e eficaz com Python. Mas o que essa flexibilidade de tipo também aponta é o fato de que
as variáveis Python são mais do que apenas seu valor; eles também contêm informações extras sobre o
tipo do valor. Exploraremos isso mais nas seções a seguir.
inteiro A implementação padrão do Python é escrita em C. Isso significa que cada objeto Python é
simplesmente uma estrutura C habilmente disfarçada, que contém não apenas seu valor, mas também
outras informações. Por exemplo, quando definimos um número inteiro em Python, como x = 10.000, x não
é apenas um número inteiro “bruto”. Na verdade, é um ponteiro para uma estrutura C composta, que
contém vários valores. Examinando o código-fonte do Python 3.4, descobrimos que a definição do tipo
inteiro (longo) efetivamente se parece com isto (uma vez que as macros C são expandidas):
• ob_refcnt, uma contagem de referência que ajuda o Python a lidar silenciosamente com a alocação de memória
ção e desalocação
o tamanho dos seguintes membros de dados • ob_digit, que contém o valor inteiro real
Isso significa que há alguma sobrecarga no armazenamento de um número inteiro em Python em comparação com
um número inteiro em uma linguagem compilada como C, conforme ilustrado na Figura 2-1.
Aqui PyObject_HEAD é a parte da estrutura que contém a contagem de referências, o código do tipo e outras partes
mencionadas anteriormente.
Observe a diferença aqui: um inteiro C é essencialmente um rótulo para uma posição na memória cujos bytes
codificam um valor inteiro. Um inteiro Python é um ponteiro para uma posição na memória que contém todas as
informações do objeto Python, incluindo os bytes que contêm o valor inteiro. Essa informação extra na estrutura
inteira do Python é o que permite que o Python seja codificado de forma tão livre e dinâmica. Todas essas
informações adicionais nos tipos Python têm um custo, porém, que se torna especialmente aparente em estruturas
que combinam muitos desses objetos.
lista Vamos considerar agora o que acontece quando usamos uma estrutura de dados Python
que contém muitos objetos Python. O contêiner multielemento mutável padrão em Python é a
lista. Podemos criar uma lista de inteiros da seguinte forma:
In[1]: L = lista(intervalo(10))
eu
Fora[1]: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
Em[2]: tipo(L[0])
Fora[2]: int
Saída[3]: ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
Em[4]: tipo(L2[0])
Fora[4]: str
Mas essa flexibilidade tem um custo: para permitir esses tipos flexíveis, cada item da lista deve
conter suas próprias informações de tipo, contagem de referências e outras informações — ou seja,
cada item é um objeto Python completo. No caso especial de todas as variáveis serem do mesmo
tipo, muitas dessas informações são redundantes: pode ser muito mais eficiente armazenar dados
em um array de tipo fixo. A diferença entre uma lista de tipo dinâmico e um array de tipo fixo (estilo
NumPy) é ilustrada na Figura 2-2.
Python oferece várias opções diferentes para armazenar dados em buffers de dados de tipo
fixo eficientes. O módulo array integrado (disponível desde Python 3.3) pode ser usado para
criar arrays densos de um tipo uniforme:
Muito mais útil, entretanto, é o objeto ndarray do pacote NumPy. Embora o objeto array do
Python forneça armazenamento eficiente de dados baseados em array, o NumPy adiciona a
isso operações eficientes nesses dados. Exploraremos essas operações em seções
posteriores; aqui demonstraremos várias maneiras de criar um array NumPy.
Lembre-se de que, diferentemente das listas do Python, o NumPy é restrito a arrays que contêm todos o mesmo
tipo. Se os tipos não corresponderem, o NumPy fará upcast, se possível (aqui, os inteiros são upcast para ponto
flutuante):
Se quisermos definir explicitamente o tipo de dados do array resultante, podemos usar a palavra-chave dtype :
Finalmente, diferentemente das listas Python, os arrays NumPy podem ser explicitamente multidimensionais;
aqui está uma maneira de inicializar um array multidimensional usando uma lista de listas:
Fora[13]: matriz([[ 1., 1., 1., 1., 1.], [ 1., 1., 1., 1., 1.],
[ 1., 1., 1 ., 1., 1.]])
Fora [14]: array ([[ 3,14, 3,14, 3,14, 3,14, 3,14], [ 3,14, 3,14, 3,14,
3,14, 3,14], [ 3,14, 3,14, 3,14, 3,14, 3,14]])
Os tipos de dados NumPy padrão estão listados na Tabela 2-1. Observe que ao construir
um array, você pode especificá-los usando uma string:
np.zeros(10, dtype='int16')
np.zeros(10, dtype=np.int16)
interno Inteiro usado para indexação (igual a C ssize_t; normalmente int32 ou int64)
você8 Bytes (–128 a 127)
float16 Aveia de meia precisão: bit de sinal, expoente de 5 bits, mantissa de 10 bits
float32 Aveia de precisão simples: bit de sinal, expoente de 8 bits, mantissa de 23 bits
float64 Aveia de precisão dupla: bit de sinal, expoente de 11 bits, mantissa de 52 bits
É possível uma especificação de tipo mais avançada, como especificar big ou little endian
números; para obter mais informações, consulte a documentação do NumPy. NumPy também
suporta tipos de dados compostos, que serão abordados em “Dados Estruturados: NumPy's
Matrizes Estruturadas” na página 92.
Atributos de matrizes
Determinar o tamanho, formato, consumo de memória e tipos de dados de arrays
Indexação de matrizes
Obtendo e definindo o valor de elementos individuais da matriz
Fatiamento de matrizes
Remodelação de matrizes
Alterando a forma de um determinado array
vamos discutir alguns atributos úteis do array. Começaremos definindo três arrays aleatórios: um array unidimensional,
bidimensional e tridimensional. Usaremos o gerador de números aleatórios do NumPy, que será propagado com um
valor definido para garantir que os mesmos arrays aleatórios sejam gerados cada vez que este código for executado:
Cada array possui atributos ndim (o número de dimensões), shape (o tamanho de cada dimensão) e size (o tamanho
total do array):
Outro atributo útil é o dtype, o tipo de dados do array (que discutimos anteriormente em “Compreendendo os
tipos de dados em Python” na página 34):
tipo d: int64
Outros atributos incluem itemsize, que lista o tamanho (em bytes) de cada elemento do array, e nbytes, que
lista o tamanho total (em bytes) do array:
tamanho do item: 8
bytes nbytes: 480 bytes
Em geral, esperamos que nbytes sejam iguais ao tamanho do item vezes o tamanho.
estiver familiarizado com a indexação de lista padrão do Python, a indexação no NumPy parecerá bastante
familiar. Em um array unidimensional, você pode acessar o i- ésimo valor (contando a partir de zero)
especificando o índice desejado entre colchetes, assim como acontece com as listas do Python:
Em[5]: x1
Em[6]: x1[0]
Fora[6]: 5
Em[7]: x1[4]
Fora[7]: 7
Para indexar a partir do final do array, você pode usar índices negativos:
Em[8]: x1[-1]
Fora[8]: 9
Em[9]: x1[-2]
Fora[9]: 7
Em uma matriz multidimensional, você acessa itens usando uma tupla de índices separados por vírgula:
Em[10]: x2
Em[11]: x2[0, 0]
Fora[11]: 3
Em[12]: x2[2, 0]
Fora[12]: 1
Fora[13]: 7
Você também pode modificar valores usando qualquer uma das notações de índice acima:
Em[14]: x2[0, 0] = 12 x2
Tenha em mente que, diferentemente das listas Python, os arrays NumPy têm um tipo fixo. Isso significa, por exemplo,
que se você tentar inserir um valor de ponto flutuante em uma matriz de inteiros, o valor será truncado silenciosamente.
Não seja pego de surpresa por esse comportamento!
como podemos usar colchetes para acessar elementos individuais de uma matriz, também podemos usá-los para
acessar submatrizes com a notação de fatia, marcada pelo caractere dois pontos ( :) .
A sintaxe de fatiamento do NumPy segue a da lista padrão do Python; para acessar uma fatia de um array x, use isto:
x[início:parada:passo]
Se algum deles não for especificado, o padrão será os valores start=0, stop=size
of dimension, step=1. Veremos como acessar submatrizes em uma dimensão e
em múltiplas dimensões.
Submatrizes unidimensionais
Em[16]: x = np.arange(10)
x
Um caso potencialmente confuso é quando o valor do passo é negativo. Neste caso, os padrões de
início e parada são trocados. Esta se torna uma maneira conveniente de reverter um array:
Submatrizes multidimensionais
As fatias multidimensionais funcionam da mesma maneira, com múltiplas fatias separadas por vírgulas.
Por exemplo:
Em[24]: x2
Acessando linhas e colunas da matriz. Uma rotina comumente necessária é acessar linhas ou
colunas únicas de um array. Você pode fazer isso combinando indexação e fatiamento, usando uma
fatia vazia marcada por dois pontos (:):
[12 7 1]
[12 5 2 4]
No caso de acesso a linhas, a fatia vazia pode ser omitida para uma sintaxe mais compacta:
[12 5 2 4]
Subarrays como
visualizações sem cópia Uma coisa importante — e extremamente útil — a saber sobre fatias de
array é que elas retornam visualizações em vez de cópias dos dados do array. Esta é uma área em
que o fatiamento de array NumPy difere do fatiamento de lista Python: nas listas, as fatias serão cópias.
Considere nosso array bidimensional anterior:
Em[31]: imprimir(x2)
[[12 5 2 4] [7 6
8 8] [1 6 7 7]]
[[12 5] [7
6]]
Agora, se modificarmos este subarray, veremos que o array original foi alterado! Observar:
Em[33]: x2_sub[0, 0] = 99
imprimir(x2_sub)
[[99 5] [7
6]]
Em[34]: imprimir(x2)
[[99 5 2 4] [7 6
8 8] [1 6 7 7]]
Este comportamento padrão é bastante útil: significa que quando trabalhamos com grandes conjuntos
de dados, podemos acessar e processar partes desses conjuntos de dados sem a necessidade de
copiar o buffer de dados subjacente.
Apesar dos recursos interessantes das visualizações de array, às vezes é útil copiar explicitamente os
dados dentro de um array ou subarray. Isso pode ser feito mais facilmente com o método copy() :
[[99 5] [7
6]]
Em[36]: x2_sub_copy[0, 0] = 42
imprimir(x2_sub_copy)
[[42 5] [7
6]]
Em[37]: imprimir(x2)
[[99 5 2 4] [7 6 8
8] [1 6 7 7]]
Remodelagem de arrays
Outro tipo útil de operação é a remodelagem de arrays. A maneira mais flexível de fazer isso é
com o método reshape() . Por exemplo, se quiser colocar os números de 1 a 9 em uma grade
3×3, você pode fazer o seguinte:
[[1 2 3]
[4 5 6]
[7 8 9]]
Observe que para que isso funcione, o tamanho do array inicial deve corresponder ao tamanho
do array remodelado. Sempre que possível, o método reshape usará uma visualização sem
cópia do array inicial, mas com buffers de memória não contíguos esse nem sempre é o caso.
anteriores funcionaram em arrays únicos. Também é possível combinar vários arrays em um e, inversamente,
dividir um único array em vários arrays.
Vamos dar uma olhada nessas operações aqui.
Concatenação de
arrays A concatenação, ou união de dois arrays em NumPy, é realizada principalmente
por meio das rotinas np.concatenate, np.vstack e np.hstack. np.concatenate recebe uma
tupla ou lista de arrays como seu primeiro argumento, como podemos ver aqui:
Em[43]: x = np.array([1, 2, 3]) y =
np.array([3, 2, 1])
np.concatenate([x, y])
[1 2 3 3 2 1 99 99 99]
Para trabalhar com arrays de dimensões mistas, pode ser mais claro usar as funções np.vstack (pilha
vertical) e np.hstack (pilha horizontal):
np.hstack([grade, y])
Divisão de arrays
O oposto da concatenação é a divisão, que é implementada pelas funções np.split, np.hsplit e np.vsplit.
Para cada um deles, podemos passar uma lista de índices que fornecem os pontos de divisão:
[1 2 3] [99 99] [3 2 1]
Observe que N pontos de divisão levam a N + 1 submatrizes. As funções relacionadas np.hsplit e np.vsplit
são semelhantes:
[[0 1 2 3] [4
5 6 7]]
[[ 8 9 10 11] [12
13 14 15]]
[[ 0 1] [ 4
5] [ 8 9]
[12 13]]
[[ 2 3] [ 6
7] [10 11]
[14 15]]
A computação em matrizes NumPy pode ser muito rápida ou muito lenta. A chave para torná-lo rápido é
usar operações vetorizadas, geralmente implementadas por meio de funções universais do Num-Py
(ufuncs). Esta seção motiva a necessidade dos ufuncs do NumPy, que podem ser usados para tornar
muito mais eficientes cálculos repetidos em elementos de array. Em seguida, apresenta muitos dos ufuncs
aritméticos mais comuns e úteis disponíveis no pacote NumPy.
implementação padrão do Python (conhecida como CPython) realiza algumas operações muito lentamente.
Isto se deve em parte à natureza dinâmica e interpretada da linguagem: o fato de que os tipos são
flexíveis, de modo que sequências de operações não podem ser compiladas em código de máquina
eficiente como em linguagens como C e Fortran. Recentemente, tem havido várias tentativas de resolver
esta fraqueza: exemplos bem conhecidos são o projeto PyPy, uma implementação compilada just-in-time
do Python; o projeto Cython, que converte código Python em código C compilável; e o projeto Numba,
que converte trechos de código Python em bytecode LLVM rápido. Cada uma delas tem seus pontos
fortes e fracos, mas é seguro dizer que nenhuma das três abordagens superou ainda o alcance e a
popularidade do mecanismo CPython padrão.
A relativa lentidão do Python geralmente se manifesta em situações onde muitas pequenas operações
estão sendo repetidas – por exemplo, fazer um loop sobre arrays para operar.
comi em cada elemento. Por exemplo, imagine que temos uma matriz de valores e gostaríamos de
calcular o recíproco de cada um. Uma abordagem direta pode ser assim:
def computar_recíprocos(valores):
saída = np.empty(len(valores)) para i no
intervalo(len(valores)):
saída[i] = 1,0 / valores[i] retornar saída
Essa implementação provavelmente parece bastante natural para alguém com experiência em C ou
Java, digamos. Mas se medirmos o tempo de execução deste código para uma entrada grande,
vemos que esta operação é muito lenta, talvez surpreendentemente! Iremos comparar isso com a
mágica %timeit do IPython (discutida em “Criação de perfil e código de tempo” na página 25):
São necessários vários segundos para calcular esses milhões de operações e armazenar o resultado!
Quando até os telemóveis têm velocidades de processamento medidas em Giga-FLOPS (ou seja,
milhares de milhões de operações numéricas por segundo), isto parece quase absurdamente lento.
Acontece que o gargalo aqui não são as operações em si, mas a verificação de tipo e o envio de
funções que o CPython deve fazer em cada ciclo do loop. Cada vez que o recíproco é calculado, o
Python primeiro examina o tipo do objeto e faz uma pesquisa dinâmica da função correta a ser usada
para esse tipo. Se estivéssemos trabalhando em código compilado, essa especificação de tipo seria
conhecida antes da execução do código e o resultado poderia ser calculado com muito mais eficiência.
Apresentando UFuncs
Para muitos tipos de operações, NumPy fornece uma interface conveniente exatamente para esse
tipo de rotina compilada e digitada estaticamente. Isso é conhecido como operação vetorizada.
Você pode fazer isso simplesmente executando uma operação no array, que será então aplicada a
cada elemento. Essa abordagem vetorizada foi projetada para empurrar o loop para a camada
compilada subjacente ao NumPy, levando a uma execução muito mais rápida.
Observando o tempo de execução do nosso grande array, vemos que ele completa ordens de
magnitude mais rápida que o loop Python:
As operações vetorizadas no NumPy são implementadas via ufuncs, cujo objetivo principal é
para executar rapidamente operações repetidas em valores em matrizes NumPy. Ufuncs são
extremamente flexível – antes víamos uma operação entre um escalar e um array, mas
também pode operar entre duas matrizes:
E as operações ufunc não estão limitadas a matrizes unidimensionais – elas podem atuar
matrizes multidimensionais também:
Cálculos usando vetorização por meio de ufuncs são quase sempre mais eficientes
do que sua contraparte implementada por meio de loops Python, especialmente porque os arrays
crescer em tamanho. Sempre que você vir esse loop em um script Python, considere
se pode ser substituído por uma expressão vetorizada.
Aritmética de matriz
Os ufuncs do NumPy parecem muito naturais de usar porque fazem uso do nativo do Python
operadores aritméticos. A adição, subtração, multiplicação e divisão padrão
todos podem ser usados:
Em[7]: x = np.arange(4)
imprimir("x =", x)
imprimir("x + 5 =", x + 5)
imprimir("x - 5 =", x - 5)
imprimir("x * 2 =", x * 2)
imprimir("x /2 =", x / 2)
print("x // 2 =", x // 2) # divisão do andar
x = [0 1 2 3]
x + 5 = [5 6 7 8]
x - 5 = [-5 -4 -3 -2]
x * 2 = [0 2 4 6]
x / 2 = [0,0,5 1,1,5]
x // 2 = [0 0 1 1]
Há também um unário ufunc para negação, um ** operador para exponenciação e uma %
operador para módulo:
-x = [0 -1 -2 -3]
x ** 2 = [0 1 4 9]
x% 2 = [0 1 0 1]
Além disso, eles podem ser amarrados como você desejar, e a ordem padrão
das operações é respeitado:
Em[9]: -(0,5*x + 1) ** 2
Em[10]: np.add(x, 2)
- np.subtrair
Subtração (por exemplo, 3 - 2 = 1)
* np.multiplicar
Multiplicação (por exemplo, 2 * 3 = 6)
%
Módulo/resto (por exemplo, 9% 4 = 1)
por exemplo, mod
Além disso, existem operadores booleanos/bit a bit; exploraremos isso em “Comparações, máscaras
e lógica booleana” na página 70.
Valor absoluto
Assim como o NumPy entende os operadores aritméticos integrados do Python, ele também entende
Função de valor absoluto integrada do Python:
O ufunc NumPy correspondente é np.absolute, que também está disponível sob o alias np.abs:
Em[12]: np.absolute(x)
Em[13]: np.abs(x)
Este ufunc também pode lidar com dados complexos, nos quais o valor absoluto retorna a magnitude:
Funções trigonométricas
NumPy fornece um grande número de ufuncs úteis, e algumas das mais úteis para o cientista de
dados são as funções trigonométricas. Começaremos definindo uma série de ângulos:
Os valores são calculados com precisão de máquina, e é por isso que valores que deveriam ser zero
nem sempre atingem exatamente zero. Funções trigonométricas inversas também estão disponíveis:
Em[17]: x = [-1, 0, 1]
imprimir("x = ", x)
print("arcsin(x) = ", np.arcsin(x))
imprimir("arccos(x) = ", np.arccos(x))
imprimir("arctan(x) = ", np.arctan(x))
x = [-1, 0, 1]
arco sin (x) = [-1,57079633 0. arcos (x) = 1.57079633]
[ 3,14159265 1,57079633 0. arctan (x) = [-0,78539816 0. ]
0,78539816]
Expoentes e logaritmos
Outro tipo comum de operação disponível em um ufunc NumPy são as exponenciais:
Em[18]: x = [1, 2, 3]
imprimir("x =", x)
imprimir("e^x =", np.exp(x))
imprimir("2^x =", np.exp2(x))
imprimir("3^x =", np.potência(3, x))
x = [1, 2, 3]
e ^ x = [2,71828183 7,3890561 20,08553692]
2^x = [2.4.8.]
3 ^ x = [3 9 27]
x = [1, 2, 4, 10]
ln(x) = [0,0,69314718 1,38629436 2,30258509]
log2 (x) = [0,1,3,32192809]
log10(x) = [0,0,30103] 2.0,602059991.
Existem também algumas versões especializadas que são úteis para manter a precisão
com entrada muito pequena:
Quando x é muito pequeno, essas funções fornecem valores mais precisos do que se o np.log bruto
ou np.exp foram usados.
Ufuncs
especializados NumPy tem muito mais ufuncs disponíveis, incluindo funções trigonométricas
hiperbólicas, aritmética bit a bit, operadores de comparação, conversões de radianos para
graus, arredondamentos e restos e muito mais. Uma olhada na documentação do NumPy
revela muitas funcionalidades interessantes.
Outra excelente fonte para ufuncs mais especializados e obscuros é o submódulo scipy.special. Se
você deseja calcular alguma função matemática obscura em seus dados, é provável que ela esteja
implementada em scipy.special. Existem muitas funções para listar todas elas, mas o trecho a seguir
mostra algumas que podem surgir em um contexto estatístico:
In[22]: # Funções gama (fatoriais generalizados) e funções relacionadas x = [1, 5, 10] print("gamma(x)
print("ln|gamma(x)|
=", special.gammaln(x) ) =", especial.gamma(x))
imprimir("beta(x, 2) =", especial.beta(x, 2))
Muitos usuários do NumPy usam ufuncs sem nunca aprender seu conjunto completo de recursos.
Descreveremos alguns recursos especializados do ufuncs aqui.
Especificando
a saída Para cálculos grandes, às vezes é útil poder especificar a matriz onde o resultado
do cálculo será armazenado. Em vez de criar um array temporário, você pode usar isso para
gravar os resultados do cálculo diretamente no local da memória onde você deseja
gostaria que eles fossem. Para todos os ufuncs, você pode fazer isso usando o argumento out da
função:
In[24]: x = np.arange(5) y =
np.empty(5)
np.multiply(x, 10, out=y) print(y)
Isso pode até ser usado com visualizações de array. Por exemplo, podemos escrever os resultados de
um cálculo em todos os outros elementos de um array especificado:
Em[25]: y = np.zeros(10)
np.power(2, x, out=y[::2])
imprimir(y)
Se tivéssemos escrito y[::2] = 2 ** x, isso teria resultado na criação de um array temporário para
armazenar os resultados de 2 ** x, seguido por uma segunda operação copiando esses valores para o
array y . Isso não faz muita diferença para um cálculo tão pequeno, mas para arrays muito grandes a
economia de memória resultante do uso cuidadoso do argumento out pode ser significativa.
Agregados
Para ufuncs binários, existem alguns agregados interessantes que podem ser calculados diretamente
do objeto. Por exemplo, se quisermos reduzir um array com uma operação específica, podemos usar o
método de redução de qualquer ufunc. Uma redução aplica repetidamente uma determinada operação
aos elementos de uma matriz até que reste apenas um único resultado.
Por exemplo, chamar reduzir em add ufunc retorna a soma de todos os elementos do array:
Em[26]: x = np.arange(1, 6)
np.add.reduce(x)
Fora[26]: 15
Da mesma forma, chamar reduzir na multiplicação ufunc resulta no produto de todos os elementos do
array:
Em[27]: np.multiply.reduce(x)
Fora[27]: 120
Em[28]: np.add.accumulate(x)
Em[29]: np.multiply.accumulate(x)
Observe que, para esses casos específicos, existem funções NumPy dedicadas para
calcular os resultados (np.sum, np.prod, np.cumsum, np.cumprod), que exploraremos
em “Agregações: mínimo, máximo e Tudo no meio” na página 58.
Produtos
externos Finalmente, qualquer ufunc pode calcular a saída de todos os pares de duas entradas
diferentes usando o método externo . Isso permite que você, em uma linha, faça coisas como
criar uma tabuada:
In[30]: x = np.arange(1, 6)
np.multiply.outer(x, x)
Outro recurso extremamente útil do ufuncs é a capacidade de operar entre arrays de diferentes
tamanhos e formatos, um conjunto de operações conhecido como broadcasting. Este assunto é
importante o suficiente para dedicarmos uma seção inteira a ele (veja “Computação em Arrays:
Broadcasting” na página 63).
Mais informações sobre funções universais (incluindo a lista completa de funções disponíveis)
podem ser encontradas nos sites de documentação NumPy e SciPy .
Lembre-se de que você também pode acessar informações diretamente do IPython importando
os pacotes e usando o preenchimento de guias e a funcionalidade de ajuda (?) do IPython ,
conforme descrito em “Ajuda e documentação no IPython” na página 3.
NumPy possui funções de agregação integradas rápidas para trabalhar em arrays; discutiremos e
demonstraremos alguns deles aqui.
um exemplo rápido, considere calcular a soma de todos os valores em uma matriz. O próprio Python
pode fazer isso usando a função sum integrada :
Em[2]: L = np.random.random(100)
soma(L)
Saída[2]: 55.61209116604941
A sintaxe é bastante semelhante à da função sum do NumPy , e o resultado é o mesmo no caso mais
simples:
Em[3]: np.soma(L)
Saída[3]: 55.612091166049424
Porém, tenha cuidado: a função sum e a função np.sum não são idênticas, o que às vezes pode
causar confusão! Em particular, seus argumentos opcionais têm significados diferentes e np.sum
reconhece múltiplas dimensões de array, como veremos na seção seguinte.
Mínimo e Máximo
Da mesma forma, Python possui funções min e max integradas , usadas para encontrar o valor
mínimo e o valor máximo de qualquer array:
As funções correspondentes do NumPy têm sintaxe semelhante e operam novamente com muito
mais rapidez:
Para min, max, sum e vários outros agregados NumPy, uma sintaxe mais curta é usar
métodos do próprio objeto array:
Em[8]: imprimir(big_array.min(), big_array.max(), big_array.sum())
1.17171281366e-06 0.999997678497 499911.628197
Sempre que possível, certifique-se de usar a versão NumPy desses agregados ao operar em arrays NumPy!
Agregações multidimensionais
Um tipo comum de operação de agregação é uma agregação ao longo de uma linha ou coluna.
Digamos que você tenha alguns dados armazenados em uma matriz bidimensional:
Em[10]: M.sum()
Saída[10]: 6.0850555667307118
As funções de agregação recebem um argumento adicional que especifica o eixo ao longo do qual a
agregação é calculada. Por exemplo, podemos encontrar o valor mínimo dentro de cada coluna especificando
axis=0:
In[11]: M.min(eixo=0)
In[12]: M.max(eixo=1)
A forma como o eixo é especificado aqui pode ser confusa para usuários vindos de outras línguas. A palavra-
chave axis especifica a dimensão da matriz que será recolhida, em vez da dimensão que será retornada.
Portanto, especificar axis=0 significa que o
primeiro eixo será recolhido: para matrizes bidimensionais, isso significa que os valores dentro
cada coluna será agregada.
NumPy fornece muitas outras funções de agregação, mas não iremos discuti-las em
detalhe aqui. Além disso, a maioria dos agregados possui uma contraparte segura para NaN que calcula
o resultado, ignorando os valores ausentes, que são marcados pelo IEEE especial
valor NaN de ponto flutuante (para uma discussão mais completa sobre dados ausentes, consulte “Tratando dados perdidos”.
dados” na página 119). Algumas dessas funções seguras para NaN não foram adicionadas até
NumPy 1.8, portanto, eles não estarão disponíveis em versões mais antigas do NumPy.
A Tabela 2-3 fornece uma lista de funções de agregação úteis disponíveis no NumPy.
Usaremos o pacote Pandas, que exploraremos mais detalhadamente no Capítulo 3, para ler o arquivo e extrair
essas informações (observe que as alturas são medidas em centímetros):
[189 170 189 163 183 171 185 168 173 183 173 173 175 178 183 193 178 173 174 183 183 168 170 178
182 180 183 178 182 188 1 75 179 183 193 182 183 177 185 188 188 182 185]
Agora que temos essa matriz de dados, podemos calcular uma variedade de estatísticas resumidas:
Observe que em cada caso, a operação de agregação reduziu todo o array a um único valor resumido, o que nos
dá informações sobre a distribuição dos valores. Também podemos desejar calcular quantis:
Vemos que a altura média dos presidentes dos EUA é de 182 cm, ou pouco menos de um metro e oitenta.
Claro, às vezes é mais útil ver uma representação visual desses dados, o que podemos fazer usando ferramentas
do Matplotlib (discutiremos o Matplotlib mais detalhadamente no Capítulo 4). Por exemplo, este código gera o
gráfico mostrado na Figura 2-3:
Em[18]: plt.hist(alturas)
plt.title(' Distribuição de altura dos presidentes dos EUA') plt.xlabel('altura
(cm)') plt.ylabel('número');
Esses agregados são algumas das peças fundamentais da análise exploratória de dados que
exploraremos com mais profundidade nos capítulos posteriores do livro.
Apresentando a radiodifusão
Lembre-se de que para matrizes do mesmo tamanho, as operações binárias são executadas
elemento por elemento:
A transmissão permite que esses tipos de operações binárias sejam executadas em arrays de
tamanhos diferentes – por exemplo, podemos facilmente adicionar um escalar (pense nele como
um array de dimensão zero) a um array:
Em[3]: a + 5
Podemos pensar nisso como uma operação que amplia ou duplica o valor 5 no array [5, 5, 5] e
adiciona os resultados. A vantagem da transmissão do NumPy é que essa duplicação de valores não
ocorre de fato, mas é um modelo mental útil quando pensamos na transmissão.
Da mesma forma, podemos estender isso para matrizes de dimensão superior. Observe o resultado
quando adicionamos um array unidimensional a um array bidimensional:
Em[5]: M + a
Embora estes exemplos sejam relativamente fáceis de entender, casos mais complicados podem
envolver a transmissão de ambas as matrizes. Considere o seguinte exemplo:
Em[6]: a = np.arange(3) b =
np.arange(3)[:, np.newaxis]
imprimir
(a) imprimir (b)
[0 1 2]
[[0]
[1]
[2]]
Em[7]: a + b
Assim como antes de esticarmos ou transmitirmos um valor para corresponder à forma do outro,
aqui ampliamos a e b para corresponder a uma forma comum, e o resultado é uma matriz
1
bidimensional! A geometria desses exemplos é visualizada na Figura 2-4.
As caixas de luz representam os valores transmitidos: novamente, esta memória extra não é
realmente alocada no decorrer da operação, mas pode ser útil conceitualmente imaginar que sim.
Regras de transmissão
A transmissão no NumPy segue um conjunto estrito de regras para determinar a interação entre
as duas matrizes:
• Regra 1: Se os dois arrays diferirem em seu número de dimensões, a forma daquele com
menos dimensões será preenchida com números em seu lado inicial (esquerdo).
• Regra 2: Se a forma dos dois arrays não corresponder em nenhuma dimensão, o array com
formato igual a 1 naquela dimensão é esticado para corresponder ao outro formato. •
Regra 3: Se em qualquer dimensão os tamanhos discordam e nenhum deles for igual a 1, um erro é
criado.
1 O código para produzir este gráfico pode ser encontrado no apêndice online e é adaptado da fonte publicada na
documentação do astroML. Usado com permissão.
Para deixar essas regras claras, vamos considerar alguns exemplos detalhadamente.
Exemplo de transmissão 1
Vamos considerar uma operação nesses dois arrays. As formas das matrizes são:
M.forma = (2, 3)
a.forma = (3,)
Vemos pela regra 1 que o array a tem menos dimensões, então o preenchemos à esquerda com
uns:
Pela regra 2, vemos agora que a primeira dimensão discorda, então ampliamos esta dimensão para
corresponder:
Em[9]: M + a
Exemplo de transmissão 2
Vamos dar uma olhada em um exemplo onde ambos os arrays precisam ser transmitidos:
a.forma = (3, 1)
b.forma = (3,)
E a regra 2 nos diz que atualizamos cada um deles para corresponder ao tamanho correspondente do outro array:
Como o resultado corresponde, essas formas são compatíveis. Podemos ver isso aqui:
Em[11]: a + b
Exemplo de transmissão 3
Agora vamos dar uma olhada em um exemplo em que os dois arrays não são compatíveis:
Esta é apenas uma situação ligeiramente diferente da do primeiro exemplo: a matriz M é transposta. Como isso
afeta o cálculo? As formas das matrizes são:
M.forma = (3, 2)
a.forma = (3,)
Novamente, a regra 1 nos diz que devemos preencher a forma de a com unidades:
Agora atingimos a regra 3 – as formas finais não correspondem, então essas duas matrizes são incompatíveis,
como podemos observar ao tentar esta operação:
Em[13]: M + a
-------------------------------------------------- -------------------------
<ipython-input-13-9e16e9f98da6> em <módulo>()
----> 1 milhão + uma
ValueError: os operandos não puderam ser transmitidos junto com as formas (3,2) (3,)
Observe a confusão potencial aqui: você poderia imaginar tornar a e M compatíveis, digamos,
preenchendo a forma de a com as da direita em vez da esquerda. Mas não é assim que funcionam
as regras de transmissão! Esse tipo de flexibilidade pode ser útil em alguns casos, mas levaria a
potenciais áreas de ambiguidade. Se o preenchimento do lado direito for o que você deseja, você
pode fazer isso explicitamente remodelando o array (usaremos a palavra-chave np.newaxis
introduzida em “Noções básicas de arrays NumPy” na página 42):
Fora[14]: (3, 1)
Observe também que, embora estejamos focando no operador + aqui, essas regras de transmissão
se aplicam a qualquer ufunc binário. Por exemplo, aqui está a função logaddexp(a, b) , que calcula
log(exp(a) + exp(b)) com mais precisão do que a abordagem ingênua:
Para obter mais informações sobre as muitas funções universais disponíveis, consulte “Cálculo em
matrizes NumPy: funções universais” na página 50.
Radiodifusão na Prática As
operações de radiodifusão constituem o núcleo de muitos exemplos que veremos ao longo deste
livro. Veremos agora alguns exemplos simples de onde eles podem ser úteis.
Centralizando um
array Na seção anterior, vimos que ufuncs permite que um usuário NumPy elimine a necessidade
de escrever explicitamente loops lentos em Python. A radiodifusão amplia essa capacidade. Um com-
O exemplo mais visto é centralizar uma matriz de dados. Imagine que você tem uma matriz de 10
observações, cada uma delas composta por 3 valores. Usando a convenção padrão (consulte
“Representação de dados no Scikit-Learn” na página 343), armazenaremos isso em um array 10×3:
Podemos calcular a média de cada recurso usando a média agregada na primeira dimensão:
E agora podemos centralizar o array X subtraindo a média (esta é uma operação de transmissão):
Para verificar se fizemos isso corretamente, podemos verificar se a matriz centralizada tem média
próxima de zero:
Em[20]: X_centered.mean(0)
Usaremos o Matplotlib para plotar esse array bidimensional (essas ferramentas serão discutidas na
íntegra em “Gráficos de densidade e contorno” na página 241):
Em [22]: % matplotlib
importação inline matplotlib.pyplot como plt
Imagine que você tem uma série de dados que representa a quantidade de precipitação diária
durante um ano em uma determinada cidade. Por exemplo, aqui carregaremos as estatísticas
diárias de precipitação para a cidade de Seattle em 2014, usando Pandas (que é abordado com
mais detalhes no Capítulo 3):
# use Pandas para extrair polegadas de chuva como uma matriz NumPy
rain = pd.read_csv('data/Seattle2014.csv')['PRCP'].values polegadas =
precipitação / 254 # 1/10mm -> polegadas
polegadas.shape
Fora[1]: (365,)
Como uma primeira visualização rápida, vejamos o histograma de dias chuvosos mostrado em
Figura 2-6, que foi gerada usando Matplotlib (exploraremos essa ferramenta mais detalhadamente
no Capítulo 4):
Em [2]: % matplotlib
importação inline matplotlib.pyplot
as plt import seaborn; seaborn.set() # define estilos de plotagem
Este histograma nos dá uma ideia geral da aparência dos dados: apesar de sua reputação, a grande
maioria dos dias em Seattle registrou chuvas medidas próximas de zero em 2014. Mas isso não é um
bom trabalho para transmitir algumas informações que temos. Gostaria de ver: por exemplo, quantos
dias de chuva houve no ano? Qual é a precipitação média nesses dias chuvosos? Quantos dias
houve com mais de meio centímetro de chuva?
Investigando os
dados Uma abordagem para isso seria responder a essas perguntas manualmente: percorrer os
dados, incrementando um contador cada vez que vemos valores em algum intervalo desejado. Pelas
razões discutidas ao longo deste capítulo, tal abordagem é muito ineficiente, tanto do ponto de vista
do tempo de escrita do código quanto do tempo de cálculo do resultado. Vimos em “Cálculo em
arrays NumPy: funções universais” na página 50 que os ufuncs do NumPy podem ser usados no
lugar de loops para realizar operações aritméticas elemento a elemento rápidas em arrays; da mesma
forma, podemos usar outros ufuncs para fazer comparações elemento a elemento em arrays e
podemos então manipular os resultados para responder às perguntas que temos. Deixaremos os
dados de lado por enquanto e discutiremos algumas ferramentas gerais no NumPy para usar o
mascaramento para responder rapidamente a esses tipos de perguntas.
In[9]: x != 3 # diferente
Em[10]: x == 3 # igual
Também é possível fazer uma comparação elemento por elemento de dois arrays e incluir
expressões compostas:
Em[11]: (2 * x) == (x ** 2)
Assim como no caso dos ufuncs aritméticos, eles funcionarão em arrays de qualquer tamanho e
formato. Aqui está um exemplo bidimensional:
Em[13]: x < 6
Em cada caso, o resultado é um array booleano, e o NumPy fornece vários padrões simples
para trabalhar com esses resultados booleanos.
Dado um array booleano, há uma série de operações úteis que você pode realizar.
Trabalharemos com x, o array bidimensional que criamos anteriormente:
Em[14]: imprimir(x)
[[5 0 3 3] [7
9 3 5] [2 4
7 6]]
Contando
entradas Para contar o número de entradas True em um array booleano, np.count_nonzero é útil:
Fora[15]: 8
Vemos que existem oito entradas na matriz menores que 6. Outra maneira de obter essas
informações é usar np.sum; neste caso, False é interpretado como 0 e True é interpretado
como 1:
Fora[16]: 8
O benefício de sum() é que, assim como outras funções de agregação NumPy, essa soma
também pode ser feita ao longo de linhas ou colunas:
Fora[18]: Verdadeiro
Fora[19]: Falso
Fora[20]: Verdadeiro
Fora[21]: Falso
np.all() e np.any() também podem ser usados ao longo de eixos específicos. Por exemplo:
Aqui todos os elementos na primeira e terceira linhas são menores que 8, enquanto este não é o caso
da segunda linha.
Por fim, um aviso rápido: conforme mencionado em “Agregações: mínimo, máximo e tudo o que está
entre” na página 58, Python possui funções incorporadas sum(), any() e all() .
Eles têm uma sintaxe diferente das versões NumPy e, em particular, falharão ou produzirão resultados
indesejados quando usados em matrizes multidimensionais. Certifique-se de usar np.sum(), np.any()
e np.all() para estes exemplos!
Operadores
booleanos Já vimos como podemos contar, digamos, todos os dias com chuva inferior a dez
centímetros, ou todos os dias com chuva superior a cinco centímetros. Mas e se quisermos saber
todos os dias com chuva menor que dez centímetros e maior que dois centímetros? Isso é feito por
meio dos operadores lógicos bit a bit do Python, &, |, ^ e ~. Assim como acontece com os operadores
aritméticos padrão, o NumPy os sobrecarrega como ufuncs que funcionam elemento a elemento em
arrays (geralmente booleanos).
Por exemplo, podemos abordar esse tipo de questão composta da seguinte maneira:
Fora[23]: 29
Então vemos que há 29 dias com precipitação entre 0,5 e 1,0 polegadas.
Observe que os parênteses aqui são importantes – devido às regras de precedência de operadores, com
os parênteses removidos, esta expressão seria avaliada da seguinte forma, o que resulta em um erro:
Usando a equivalência de A AND B e NOT (A OR B) (da qual você deve se lembrar se tiver feito um
curso introdutório de lógica), podemos calcular o mesmo resultado de uma maneira diferente:
Fora[24]: 29
A tabela a seguir resume os operadores booleanos bit a bit e seus ufuncs equivalentes:
| np.bitwise_or
^
np.bitwise_xor
~ np.bitwise_not
Usando essas ferramentas, podemos começar a responder aos tipos de perguntas que temos sobre
nossos dados meteorológicos. Aqui estão alguns exemplos de resultados que podemos calcular ao
combinar mascaramento com agregações:
In[25]: print("Número de dias sem chuva: ", np.sum(polegadas == 0)) print("Número de dias com
chuva: ", np.sum(polegadas != 0)) print("Dias com mais de 0,5 polegadas:",
np.sum(polegadas > 0,5)) print("Dias chuvosos com <0,1 polegadas:", np.sum((polegadas
> 0) &
(polegadas < 0,2)))
Em[26]: x
Podemos obter facilmente um array booleano para esta condição, como já vimos:
Em[27]: x < 5
Agora, para selecionar esses valores do array, podemos simplesmente indexar neste array booleano; isso é conhecido
como operação de mascaramento:
O que é retornado é um array unidimensional preenchido com todos os valores que atendem a essa condição; em
outras palavras, todos os valores nas posições nas quais o array de máscaras é True.
Somos então livres para operar com base nesses valores como desejarmos. Por exemplo, podemos calcular algumas
estatísticas relevantes sobre nossos dados de chuva em Seattle:
In[29]: #
constrói uma máscara de todos os dias chuvosos
chuvosos = (polegadas > 0)
# constrói uma máscara de todos os dias de verão (21 de junho é o 172º dia) verão =
(np.arange(365) - 172 < 90) & (np.arange(365) - 172 > 0)
Quando você usa e ou ou, é equivalente a pedir ao Python para tratar o objeto como uma única entidade
booleana. Em Python, todos os números inteiros diferentes de zero serão avaliados como True. Por isso:
In[31]: bool(42 e 0)
Fora[31]: Falso
In[32]: bool(42 ou 0)
Fora[32]: Verdadeiro
Quando você usa & e | em números inteiros, a expressão opera nos bits do elemento,
aplicando o e ou o ou aos bits individuais que compõem o número:
Em[33]: sou(42)
Fora[33]: '0b101010'
Em[34]: sou(59)
Fora[34]: '0b111011'
Fora[35]: '0b101010'
Fora[36]: '0b111011'
Quando você tem uma matriz de valores booleanos em NumPy, isso pode ser pensado
como uma sequência de bits onde 1 = Verdadeiro e 0 = Falso, e o resultado de & e | opera
de maneira semelhante a antes:
Usar ou nessas matrizes tentará avaliar a verdade ou falsidade de todo o objeto da matriz, que não é
um valor bem definido:
Em[38]: A ou B
-------------------------------------------------- -------------------------
<ipython-input-38-5d8e4f2e21c0> em <módulo>()
----> 1 A ou B
Da mesma forma, ao fazer uma expressão booleana em um determinado array, você deve usar | ou
& em vez de ou ou e:
Portanto, lembre-se disto: and e or execute uma única avaliação booleana em um objeto inteiro,
enquanto & e | realizar múltiplas avaliações booleanas no conteúdo (os bits ou bytes individuais) de
um objeto. Para matrizes Booleanas NumPy, a última é quase sempre a operação desejada.
Indexação sofisticada
Nas seções anteriores, vimos como acessar e modificar partes de arrays usando índices
simples (por exemplo, arr[0]), fatias (por exemplo, arr[:5]) e máscaras booleanas (por exemplo,
arr[arr > 0] ). Nesta seção, veremos outro estilo de indexação de array, conhecido como
indexação sofisticada. A indexação sofisticada é como a indexação simples que já vimos, mas
passamos matrizes de índices no lugar de escalares únicos. Isso nos permite acessar e
modificar rapidamente subconjuntos complicados de valores de um array.
Explorando a indexação
x = rand.randint(100, tamanho=10)
imprimir(x)
[51 92 14 71 60 20 82 86 74 74]
Suponha que queiramos acessar três elementos diferentes. Poderíamos fazer assim:
Alternativamente, podemos passar uma única lista ou array de índices para obter o mesmo resultado:
Com a indexação sofisticada, o formato do resultado reflete o formato das matrizes de índice, em vez do
formato da matriz que está sendo indexada:
x[ind]
Assim como na indexação padrão, o primeiro índice refere-se à linha e o segundo à coluna:
Observe que o primeiro valor no resultado é X[0, 2], o segundo é X[1, 1] e o terceiro é X[2, 3]. O
emparelhamento de índices na indexação sofisticada segue todas as regras de transmissão mencionadas
em “Computação em matrizes: transmissão” na página 63. Portanto,
Indexação sofisticada | 79
Machine Translated by Google
por exemplo, se combinarmos um vetor coluna e um vetor linha dentro dos índices, obteremos um resultado
bidimensional:
Aqui, cada valor de linha corresponde a cada vetor de coluna, exatamente como vimos na transmissão de operações
aritméticas. Por exemplo:
É sempre importante lembrar, com a indexação sofisticada, que o valor de retorno reflete o formato transmitido dos
índices, em vez do formato do array que está sendo indexado.
Indexação Combinada
Para operações ainda mais poderosas, a indexação sofisticada pode ser combinada com outros esquemas de
indexação que vimos:
Em[9]: imprimir(X)
[[ 0 1 2 3] [ 4 5 6
7] [ 8 9 10 11]]
Todas essas opções de indexação combinadas levam a um conjunto muito flexível de operações para acessar e
modificar valores de array.
Fora[13]: (100, 2)
Usando as ferramentas de plotagem que discutiremos no Capítulo 4, podemos visualizar esses pontos como um
gráfico de dispersão (Figura 2-7):
Vamos usar uma indexação sofisticada para selecionar 20 pontos aleatórios. Faremos isso primeiro escolhendo
20 índices aleatórios sem repetições e usando esses índices para selecionar uma parte do array original:
Fora[15]: matriz([93, 45, 73, 81, 50, 10, 98, 94, 4, 64, 65, 89, 47, 84, 82,
80, 25, 90, 63, 20])
Indexação sofisticada | 81
Machine Translated by Google
Fora[16]: (20, 2)
Agora, para ver quais pontos foram selecionados, vamos plotar círculos grandes nas localizações dos pontos
selecionados (Figura 2-8):
Esse tipo de estratégia é frequentemente usado para particionar rapidamente conjuntos de dados, como
geralmente é necessário na divisão de treinamento/teste para validação de modelos estatísticos (consulte
“Hiperparâmetros e validação de modelo” na página 361) e em abordagens de amostragem para responder a
questões estatísticas.
como a indexação sofisticada pode ser usada para acessar partes de um array, ela também pode ser usada
para modificar partes de um array. Por exemplo, imagine que temos um array de índices e gostaríamos de
definir algum valor para os itens correspondentes em um array:
Em[18]: x = np.arange(10)
i = np.array([2, 1, 8, 4]) x[i] = 99
imprimir(x)
[0 99 99 3 99 5 6 7 99 9]
Podemos usar qualquer operador do tipo atribuição para isso. Por exemplo:
Em[19]: x[i] -= 10
impressão(x)
[0 89 89 3 89 5 6 7 89 9]
Observe, porém, que índices repetidos com essas operações podem causar alguns resultados
potencialmente inesperados. Considere o seguinte:
Em[20]: x = np.zeros(10)
x[[0, 0]] = [4, 6]
imprimir(x)
[6. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
Para onde foram os 4? O resultado desta operação é primeiro atribuir x[0] = 4, seguido por x[0] = 6. O
resultado, claro, é que x[0] contém o valor 6.
Fora[21]: array([ 6., 0., 1., 1., 1., 0., 0., 0., 0., 0.])
Você pode esperar que x[3] contenha o valor 2 e x[4] contenha o valor 3, pois é quantas vezes cada
índice é repetido. Por que não é esse o caso?
Conceitualmente, isso ocorre porque x[i] += 1 é uma abreviação de x[i] = x[i] + 1. x[i] + 1 é avaliado e,
em seguida, o resultado é atribuído aos índices em x . Com isso em mente, não é o aumento que
acontece várias vezes, mas a atribuição, que leva a resultados pouco intuitivos.
E daí se você quiser outro comportamento em que a operação é repetida? Para isso, você pode usar o
método at() do ufuncs (disponível desde o NumPy 1.8) e fazer o seguinte:
Em[22]: x = np.zeros(10)
np.add.at(x, i, 1)
imprimir(x)
[0. 0. 1. 2. 3. 0. 0. 0. 0. 0.]
O método at() faz uma aplicação local do operador fornecido nos índices especificados (aqui, i) com o
valor especificado (aqui, 1). Outro método de espírito semelhante é o método reduzat() do ufuncs, sobre
o qual você pode ler na documentação do NumPy.
Exemplo: categorização de
dados Você pode usar essas ideias para agrupar dados de maneira eficiente e criar um histograma
manualmente. Por exemplo, imagine que temos 1.000 valores e gostaríamos de descobrir rapidamente
onde eles se enquadram em uma matriz de caixas. Poderíamos calculá-lo usando ufunc.at assim:
Indexação sofisticada | 83
Machine Translated by Google
Em[23]: np.random.seed(42) x =
np.random.randn(100)
As contagens agora refletem o número de pontos dentro de cada compartimento – em outras palavras,
um histograma (Figura 2-9):
Claro, seria bobagem ter que fazer isso toda vez que quiser traçar um histograma.
É por isso que o Matplotlib fornece a rotina plt.hist() , que faz o mesmo em uma única linha:
Esta função criará um gráfico quase idêntico ao visto aqui. Para calcular o binning, o Matplotlib usa a
função np.histogram , que faz um cálculo muito semelhante ao que fizemos antes. Vamos comparar os
dois aqui:
print("Rotina personalizada:")
%timeit np.add.at(counts, np.searchsorted(bins, x), 1)
Rotina NumPy:
10.000 loops, melhor de 3: 97,6 µs por loop
Rotina
personalizada: 10.000 loops, melhor de 3: 19,5 µs por loop
Nosso próprio algoritmo de uma linha é várias vezes mais rápido que o algoritmo otimizado no
NumPy! Como isso pode ser? Se você se aprofundar no código-fonte do np.histogram (você pode
fazer isso no IPython digitando np.histogram ??), verá que é um pouco mais complicado do que a
simples pesquisa e contagem que fizemos ; isso ocorre porque o algoritmo do NumPy é mais flexível
e foi projetado principalmente para melhor desempenho quando o número de pontos de dados se
torna grande:
In[26]: x = np.random.randn(1000000)
print("Rotina NumPy:")
%timeit conta, bordas = np.histogram(x, bins)
print("Rotina personalizada:")
%timeit np.add.at(counts, np.searchsorted(bins, x), 1)
Rotina NumPy:
10 loops, melhor de 3: 68,7 ms por loop
Rotina
personalizada: 10 loops, melhor de 3: 135 ms por loop
O que esta comparação mostra é que a eficiência algorítmica quase nunca é uma questão simples.
Um algoritmo eficiente para grandes conjuntos de dados nem sempre será a melhor escolha para
pequenos conjuntos de dados e vice-versa (consulte “Notação Big-O” na página 92). Mas a vantagem
de você mesmo codificar esse algoritmo é que, com uma compreensão desses métodos básicos,
você poderia usar esses blocos de construção para estender isso para executar alguns
comportamentos personalizados muito interessantes. A chave para usar Python de forma eficiente
em aplicações com uso intensivo de dados é conhecer rotinas de conveniência geral, como
np.histogram , e quando elas são apropriadas, mas também saber como fazer uso de funcionalidades
de nível inferior quando você precisar de um comportamento mais específico.
Classificando matrizes
Até agora nos preocupamos principalmente com ferramentas para acessar e operar dados de array
com NumPy. Esta seção cobre algoritmos relacionados à classificação de valores em matrizes
NumPy. Esses algoritmos são um tópico favorito em cursos introdutórios à ciência da computação:
se você já fez um, provavelmente já teve sonhos (ou, dependendo do seu temperamento, pesadelos)
sobre ordenações por inserção, ordenações por seleção, ordenações por mesclagem, ordenações
rápidas, etc. tipos de bolhas e muitos, muitos mais. Todos são meios de realizar uma tarefa
semelhante: classificar os valores em uma lista ou array.
Classificando matrizes | 85
Machine Translated by Google
Por exemplo, uma ordenação por seleção simples encontra repetidamente o valor mínimo em uma lista e faz
trocas até que a lista seja ordenada. Podemos codificar isso em apenas algumas linhas de Python:
Mesmo a classificação por seleção, porém, é muito melhor do que meus algoritmos de classificação favoritos de
todos os tempos, o bogosort:
Felizmente, Python contém algoritmos de classificação integrados que são muito mais eficientes do que qualquer
um dos algoritmos simplistas mostrados acima. Começaremos examinando os componentes internos do Python
e, em seguida, daremos uma olhada nas rotinas incluídas no NumPy e otimizadas para arrays NumPy.
tenha funções internas de classificação e classificação para trabalhar com listas, não as discutiremos aqui porque
a função np.sort do NumPy acaba sendo muito mais
eficiente e útil para nossos propósitos. Por padrão, np.sort usa um algoritmo N log N , de classificação
rápida, embora mergesort e heapsort também estejam disponíveis. Para a maioria das aplicações, o
quicksort padrão é mais que suficiente.
Para retornar uma versão classificada do array sem modificar a entrada, você pode usar np.sort:
Se preferir classificar o array no local, você pode usar o método sort de arrays:
Em[6]: x.sort()
imprimir(x)
[1 2 3 4 5]
Uma função relacionada é argsort, que retorna os índices dos elementos classificados:
[1 0 3 2 4]
O primeiro elemento deste resultado fornece o índice do menor elemento, o segundo valor fornece o
índice do segundo menor e assim por diante. Esses índices podem então ser usados (por meio de
indexação sofisticada) para construir a matriz classificada, se desejado:
Em[8]: x[i]
Classificando ao longo de
[[6 3 7 4 6 9] [2
6 7 4 3 7] [7 2
5 4 1 7] [5 1 4
0 9 5]]
Classificando matrizes | 87
Machine Translated by Google
Lembre-se de que isso trata cada linha ou coluna como um array independente e qualquer relacionamento entre os valores
da linha ou coluna será perdido!
Às vezes, não estamos interessados em classificar o array inteiro, mas simplesmente queremos encontrar os K menores
valores no array. NumPy fornece isso na função np.partition . np.partition recebe um array e um número K; o resultado é
um novo array com os menores valores de K à esquerda da partição e os valores restantes à direita, em ordem arbitrária:
Observe que os três primeiros valores na matriz resultante são os três menores na matriz e as posições restantes da
matriz contêm os valores restantes. Dentro das duas partições, os elementos têm ordem arbitrária.
Da mesma forma que na classificação, podemos particionar ao longo de um eixo arbitrário de um sistema multidimensional
variedade:
O resultado é uma matriz onde os dois primeiros slots de cada linha contêm os menores valores daquela linha, com os
valores restantes preenchendo os slots restantes.
Finalmente, assim como existe um np.argsort que calcula índices do tipo, existe um np.argpartition que calcula índices da
partição. Veremos isso em ação na seção seguinte.
rapidamente como podemos usar esta função argsort ao longo de vários eixos para encontrar os vizinhos mais próximos
de cada ponto em um conjunto. Começaremos criando um conjunto aleatório de 10
pontos em um plano bidimensional. Usando a convenção padrão, organizaremos isso em uma matriz 10×2:
Em[14]: X = rand.rand(10, 2)
Para ter uma ideia da aparência desses pontos, vamos fazer um gráfico de dispersão rapidamente (Figura 2-10):
Agora calcularemos a distância entre cada par de pontos. Lembre-se de que o quadrado da distância entre dois
pontos é a soma dos quadrados das diferenças em cada dimensão; usando as rotinas eficientes de transmissão
(“Computação em matrizes: transmissão” na página 63) e agregação (“Agregações: mínimo, máximo e tudo no
meio” na página 58) fornecidas pelo NumPy, podemos calcular a matriz de distâncias quadradas em um única linha
de código:
Esta operação contém muitas coisas e pode ser um pouco confusa se você não estiver familiarizado com as regras
de transmissão do NumPy. Quando você se depara com um código como este, pode ser útil dividi-lo em etapas de
componentes:
Classificando matrizes | 89
Machine Translated by Google
Apenas para verificar o que estamos fazendo, devemos ver que a diagonal desta matriz (ou seja, o conjunto
de distâncias entre cada ponto e ele mesmo) é toda zero:
Em[20]: dist_sq.diagonal()
Fora[20]: array([ 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.])
Está confirmado! Com as distâncias quadradas entre pares convertidas, agora podemos usar np.arg sort
para classificar cada linha. As colunas mais à esquerda fornecerão os índices dos vizinhos mais próximos:
[[0 3 9 7 1 4 2 5 6 8] [1 4 7
9 3 6 8 5 0 2] [2 1 4 6 3 0
8 9 7 5] [3 9 7 0 1 4 5 8 6
2] [ 4 1 8 5 6 7 9 3 0 2] [5
8 6 4 1 7 9 3 2 0] [6 8 5 4
1 7 9 3 2 0] [7 9 3 1 4 0 5
8 6 2] [8 5 6 4 1 7 9 3 2
0] [9 7 3 0 1 4 5 8 6 2]]
Observe que a primeira coluna fornece os números de 0 a 9 em ordem: isso se deve ao fato de que o vizinho
mais próximo de cada ponto é ele mesmo, como seria de esperar.
Ao usar uma classificação completa aqui, na verdade fizemos mais trabalho do que o necessário neste caso.
Se estivermos simplesmente interessados nos k vizinhos mais próximos, tudo o que precisamos é particionar
cada linha de modo que as menores distâncias k + 1 ao quadrado venham primeiro, com distâncias maiores
preenchendo as posições restantes do array. Podemos fazer isso com a função np.argpartition :
Em[22]: K = 2
partição_mais próxima = np.argpartition(dist_sq, K + 1, eixo=1)
Para visualizar esta rede de vizinhos, vamos traçar rapidamente os pontos junto com as linhas que
representam as conexões de cada ponto com seus dois vizinhos mais próximos.
(Figura 2-11):
#desenha linhas de cada ponto até seus dois vizinhos mais próximos
K=2
Cada ponto do gráfico possui linhas traçadas até seus dois vizinhos mais próximos. À primeira
vista, pode parecer estranho que alguns dos pontos tenham mais de duas linhas saindo deles: isto
se deve ao fato de que se o ponto A é um dos dois vizinhos mais próximos do ponto B, isso não
implica necessariamente que o ponto B é um dos dois vizinhos mais próximos do ponto A.
Embora a transmissão e a classificação por linha dessa abordagem possam parecer menos simples
do que escrever um loop, ela acaba sendo uma maneira muito eficiente de operar com esses dados
em Python. Você pode ficar tentado a fazer o mesmo tipo de operação percorrendo manualmente
os dados e classificando cada conjunto de vizinhos individualmente, mas isso quase certamente
levaria a um algoritmo mais lento do que a versão vetorizada que usamos. A beleza dessa
abordagem é que ela foi escrita de uma forma independente do tamanho dos dados de entrada:
poderíamos facilmente calcular os vizinhos entre 100 ou 1.000.000 de pontos em qualquer número
de dimensões, e o código teria a mesma aparência.
Por fim, observarei que, ao fazer pesquisas muito grandes pelo vizinho mais próximo, existem
algoritmos baseados em árvore e/ou aproximados que podem ser escalonados como N log N ou melhor
Classificando matrizes | 91
Machine Translated by Google
Notação Big-O A
A notação Big-O, nesse sentido amplo, informa quanto tempo seu algoritmo levará à medida que
você aumenta a quantidade de dados. Se você tiver um algoritmo N (leia-se “ordem N”) que leva
1 segundo para operar em uma lista de comprimento N = 1.000, então você deve esperar que
2
leve cerca de 5 segundos para uma lista de comprimento N = 5.000. Se você tiver um algoritmo
N (leia-se “ordem N ao quadrado”) que leva 1 segundo para N = 1.000, então você deve esperar
cerca de 25 segundos para N = 5.000.
Observe que a notação O grande por si só não diz nada sobre o tempo real de um cálculo, mas
apenas sobre sua escala à medida que você altera N. Geralmente, por exemplo, um algoritmo N
2
é considerado como tendo uma escala melhor do que um N. algoritmo, e por boas razões. Mas,
em particular, para pequenos conjuntos de dados, o algoritmo com melhor escalabilidade pode
2
não ser mais rápido. Por exemplo, em um determinado problema, um algoritmo N pode levar
0,01 segundos, enquanto um algoritmo N “melhor” pode levar 1 segundo.
Aumente N por um fator de 1.000 e o algoritmo N vencerá.
Mesmo essa versão flexível da notação Big-O pode ser muito útil para comparar o desempenho
de algoritmos, e usaremos essa notação ao longo do livro ao falar sobre como os algoritmos são
escalonados.
dados genéticos. Embora os padrões mostrados aqui sejam úteis para operações simples, cenários como esse
geralmente se prestam ao uso de Pandas DataFrames, que exploraremos no Capítulo 3.
Imagine que temos diversas categorias de dados sobre diversas pessoas (por exemplo, nome, idade e peso) e
gostaríamos de armazenar esses valores para uso em um programa Python. Seria possível armazená-los em
três matrizes separadas:
Mas isso é um pouco desajeitado. Não há nada aqui que nos diga que os três arrays estão relacionados; seria
mais natural se pudéssemos usar uma única estrutura para armazenar todos esses dados. NumPy pode lidar
com isso por meio de arrays estruturados, que são arrays com tipos de dados compostos.
Lembre-se de que criamos anteriormente um array simples usando uma expressão como esta:
Da mesma forma, podemos criar um array estruturado usando uma especificação de tipo de dados composto:
Aqui, 'U10' se traduz em “string Unicode de comprimento máximo 10”, 'i4' se traduz em “inteiro de 4 bytes (ou
seja, 32 bits)” e 'f8' se traduz em “flutuador de 8 bytes (ou seja, 64 bits) .” Discutiremos outras opções para
esses códigos de tipo na seção a seguir.
Agora que criamos um array contêiner vazio, podemos preenchê-lo com nossas listas de valores:
O que é útil com arrays estruturados é que agora você pode se referir a valores por índice ou por nome:
Fora[8]: 'Doug'
Usando máscara booleana, isso permite até mesmo realizar algumas operações mais sofisticadas, como filtrar por
idade:
Observe que se você quiser fazer alguma operação mais complicada do que essas, você provavelmente deveria
considerar o pacote Pandas, abordado no próximo capítulo. Como veremos, o Pandas fornece um objeto
DataFrame , que é uma estrutura construída em arrays NumPy que oferece uma variedade de funcionalidades
úteis de manipulação de dados semelhantes ao que mostramos aqui, e muito, muito mais.
de dados de matrizes estruturadas podem ser especificados de diversas maneiras. Anteriormente, vimos o método
do dicionário:
Para maior clareza, os tipos numéricos podem ser especificados com tipos Python ou dtypes NumPy :
Um tipo composto também pode ser especificado como uma lista de tuplas:
Se os nomes dos tipos não importam para você, você pode especificar os tipos sozinhos em uma string separada
por vírgula:
Em[13]: np.dtype('S10,i4,f8')
Os códigos de formato de string abreviados podem parecer confusos, mas são baseados em
princípios. O primeiro caractere (opcional) é < ou >, que significa “little endian” ou “big
endian”, respectivamente, e especifica a convenção de ordenação para bits significativos. O
próximo caractere especifica o tipo de dados: caracteres, bytes, inteiros, pontos flutuantes e assim por diante
ligado (consulte a Tabela 2-4). O último caractere ou caracteres representa o tamanho do objeto em
bytes.
É possível definir tipos de compostos ainda mais avançados. Por exemplo, você pode
crie um tipo onde cada elemento contenha uma matriz ou matriz de valores. Aqui, vamos
crie um tipo de dados com um componente mat que consiste em uma matriz de ponto flutuante 3×3:
(0, [[0,0, 0,0, 0,0], [0,0, 0,0, 0,0], [0,0, 0,0, 0,0]])
[[ 0. 0. 0.]
[0.0.0.]
[0.0.0.]]
Agora, cada elemento do array X consiste em um id e uma matriz 3×3. Por que você
usar isso em vez de um simples array multidimensional ou talvez um dicionário Python?
A razão é que este dtype NumPy mapeia diretamente para uma definição de estrutura C, então
o buffer que contém o conteúdo da matriz pode ser acessado diretamente dentro de um ambiente apropriado
programa C corretamente escrito. Se você estiver escrevendo uma interface Python em um C legado
ou biblioteca Fortran que manipula dados estruturados, você provavelmente encontrará
matrizes bastante úteis!
também fornece a classe np.recarray , que é quase idêntica aos arrays estruturados que
acabamos de descrever, mas com um recurso adicional: os campos podem ser acessados
como atributos em vez de chaves de dicionário. Lembre-se de que acessamos anteriormente
as idades escrevendo:
Em[15]: dados['idade']
Se visualizarmos nossos dados como uma matriz de registros, poderemos acessá-los com um pouco menos de
pressionamentos de tecla:
Se a notação mais conveniente vale a sobrecarga adicional dependerá do seu próprio aplicativo.
Esta seção sobre arrays estruturados e de registros está propositalmente no final deste capítulo,
porque leva muito bem ao próximo pacote que abordaremos: Pandas. Matrizes estruturadas
como as discutidas aqui são boas para certas situações, especialmente no caso de você estar
usando matrizes NumPy para mapear em formatos de dados binários em C, Fortran ou outra
linguagem. Para o uso diário de dados estruturados, o pacote Pandas é uma escolha muito
melhor, e abordaremos uma discussão completa sobre ele no próximo capítulo.
CAPÍTULO 3
No capítulo anterior, mergulhamos em detalhes sobre NumPy e seu objeto ndarray , que fornece
armazenamento e manipulação eficientes de arrays de tipo denso em Python. Aqui
desenvolveremos esse conhecimento examinando detalhadamente as estruturas de dados
fornecidas pela biblioteca Pandas. Pandas é um pacote mais recente construído sobre NumPy e
fornece uma implementação eficiente de um DataFrame. DataFrames são essencialmente arrays
multidimensionais com rótulos de linha e coluna anexados e, muitas vezes, com tipos
heterogêneos e/ou dados ausentes. Além de oferecer uma interface de armazenamento
conveniente para dados rotulados, o Pandas implementa uma série de operações de dados
poderosas, familiares aos usuários de estruturas de banco de dados e programas de planilhas.
Como vimos, a estrutura de dados ndarray do NumPy fornece recursos essenciais para o tipo de
dados limpos e bem organizados normalmente vistos em tarefas de computação numérica.
Embora atenda muito bem a esse propósito, suas limitações ficam claras quando precisamos de
mais flexibilidade (anexar rótulos aos dados, trabalhar com dados faltantes, etc.) e ao tentar
operações que não mapeiam bem a transmissão elemento a elemento (agrupamentos, pivôs,
etc.), cada um dos quais é uma peça importante de análise dos dados menos estruturados
disponíveis em muitas formas no mundo que nos rodeia. O Pandas, e em particular seus objetos
Series e DataFrame , baseia-se na estrutura de array NumPy e fornece acesso eficiente a esses
tipos de tarefas de “gestão de dados” que ocupam grande parte do tempo de um cientista de dados.
97
Machine Translated by Google
Fontes Cython nas quais o Pandas é construído. Detalhes sobre esta instalação podem ser encontrados na
documentação do Pandas. Se você seguiu os conselhos descritos no prefácio e usou a pilha Anaconda,
você já tem o Pandas instalado.
Depois que o Pandas estiver instalado, você poderá importá-lo e verificar a versão:
Saída[1]: '0.18.1'
Assim como geralmente importamos NumPy sob o alias np, importaremos Pandas sob o alias pd:
Por exemplo, para exibir todo o conteúdo do namespace pandas , você pode digitar isto:
Em [3]: pd.<TAB>
Em [4]: pd?
Documentação mais detalhada, juntamente com tutoriais e outros recursos, pode ser encontrada
em http://pandas.pydata.org/.
série Pandas é uma matriz unidimensional de dados indexados. Ele pode ser criado a partir de uma
lista ou array da seguinte forma:
Fora[2]: 0 1 0,25
0,50
2 0,75
3 1,00 dtipo:
float64
Como vemos na saída anterior, a Série envolve uma sequência de valores e uma sequência de
índices, que podemos acessar com os valores e atributos de índice . Os valores são simplesmente
um array NumPy familiar:
Em[3]: dados.valores
O índice é um objeto semelhante a um array do tipo pd.Index, que discutiremos com mais detalhes
em breve:
Em[4]: data.index
Assim como acontece com um array NumPy, os dados podem ser acessados pelo índice associado por meio do familiar
Notação de colchetes Python:
Em[5]: dados[1]
Fora[5]: 0,5
Em[6]: dados[1:3]
Fora[6]: 1 2 0,50
0,75
tipo d: float64
Como veremos, porém, a série Pandas é muito mais geral e flexível do que o array NumPy
unidimensional que ela emula.
generalizado Pelo que vimos até agora, pode parecer que o objeto Series é basicamente
intercambiável com um array NumPy unidimensional. A diferença essencial é a presença do índice:
enquanto o array NumPy possui um índice inteiro definido implicitamente usado para acessar os
valores, a Série Pandas possui um índice definido explicitamente associado aos valores.
Essa definição explícita de índice fornece recursos adicionais ao objeto Series . Por exemplo, o índice não
precisa ser um número inteiro, mas pode consistir em valores de qualquer tipo desejado. Por exemplo, se
desejarmos, podemos usar strings como índice:
Em [7]: dados = pd.Series([0,25, 0,5, 0,75, 1,0], índice=['a', 'b', 'c', 'd'])
dados
Fora[7]: ab 0,25
0,50
c 0,75
d 1,00 dtipo:
float64
Em[8]: dados['b']
Fora[8]: 0,5
dados
Fora[9]: 2 5 0,25
0,50
3 0,75 7 1,00
dtipo: float64
Em[10]: dados[5]
Fora[10]: 0,5
especializado Desta forma, você pode pensar em uma série Pandas um pouco como uma especialização de
um dicionário Python. Um dicionário é uma estrutura que mapeia chaves arbitrárias para um conjunto de
valores arbitrários, e uma Série é uma estrutura que mapeia chaves digitadas para um conjunto de valores
digitados. Essa digitação é importante: assim como o código compilado específico do tipo por trás de um array
NumPy o torna mais eficiente do que uma lista Python para determinadas operações, as informações de tipo
de uma série Pandas o tornam muito mais eficiente do que os dicionários Python para determinadas operações.
Podemos tornar a analogia da série como dicionário ainda mais clara construindo um
Objeto de série diretamente de um dicionário Python:
Por padrão, será criada uma série onde o índice é extraído das chaves classificadas.
A partir daqui, o acesso típico a itens no estilo de dicionário pode ser realizado:
Em[12]: população['Califórnia']
Fora[12]: 38332521
Ao contrário de um dicionário, porém, a Série também suporta operações no estilo array, como
fatiamento:
Em[13]: população['Califórnia':'Illinois']
onde o índice é um argumento opcional e os dados podem ser uma entre muitas entidades.
Por exemplo, os dados podem ser uma lista ou uma matriz NumPy; nesse caso, o índice é padronizado como um
sequência inteira:
Fora[14]: 0 2
1 4
2 6
tipo d: int64
data pode ser um escalar, que é repetido para preencher o índice especificado:
Saída[15]: 100 5
200 5
300 5
dtype: int64
os dados podem ser um dicionário, no qual o índice assume como padrão as chaves do dicionário classificadas:
Fora[16]: 1 2 b
a
3 c
dtype: objeto
Em cada caso, o índice pode ser definido explicitamente se for preferido um resultado diferente:
Fora[17]: 3 2 c
a
dtype: objeto
Observe que neste caso, a Série é preenchida apenas com as chaves explicitamente identificadas.
estrutura fundamental no Pandas é o DataFrame. Assim como o objeto Series discutido na seção anterior, o
DataFrame pode ser pensado como uma generalização de um array NumPy ou como uma especialização de
um dicionário Python. Veremos agora cada uma dessas perspectivas.
generalizado Se uma Series é análoga a um array unidimensional com índices flexíveis, um DataFrame é um
análogo de um array bidimensional com índices de linhas e nomes de colunas flexíveis. Assim como você
pode pensar em uma matriz bidimensional como uma sequência ordenada de colunas unidimensionais
alinhadas, você pode pensar em um DataFrame como uma sequência de objetos Series alinhados . Aqui, por
“alinhados” queremos dizer que eles compartilham o mesmo índice.
Para demonstrar isso, vamos primeiro construir uma nova série listando a área de cada um dos cinco estados
discutidos na seção anterior:
Em [18]:
area_dict = {'Califórnia': 423967, 'Texas': 695662, 'Nova York': 141297,
'Flórida': 170312, 'Illinois': 149995}
área = pd.Series(area_dict)
área
Agora que temos isso junto com a série populacional anterior, podemos usar um
dicionário para construir um único objeto bidimensional contendo esta informação:
Assim como o objeto Series , o DataFrame possui um atributo index que dá acesso ao
rótulos de índice:
Em[20]: estados.index
Fora[20]:
Index(['Califórnia', 'Flórida', 'Illinois', 'Nova York', 'Texas'], dtype='objeto')
Além disso, o DataFrame possui um atributo de colunas , que é um objeto Index que contém
os rótulos das colunas:
Em[21]: estados.colunas
Assim, o DataFrame pode ser pensado como uma generalização de um sistema bidimensional
Matriz NumPy, onde ambas as linhas e colunas têm um índice generalizado para acesso
dados.
Da mesma forma, também podemos pensar em DataFrame como uma especialização de um dicionário. Onde
um dicionário mapeia uma chave para um valor, um DataFrame mapeia um nome de coluna para uma série de
dados da coluna. Por exemplo, pedir o atributo 'area' retorna o objeto Series
contendo as áreas que vimos anteriormente:
Em[22]: estados['área']
Illinois 149995
Nova Iorque 141297
Texas 695662
Nome: área, dtype: int64
Um DataFrame do Pandas pode ser construído de várias maneiras. Aqui daremos vários
exemplos.
De um único objeto Series. Um DataFrame é uma coleção de objetos Series , e um DataFrame de coluna
única pode ser construído a partir de uma única Série:
Fora[23]: população
Califórnia 38332521
Flórida 19552860
Illinois 12882135
Nova Iorque 19651127
Texas 26448193
De uma lista de dictos. Qualquer lista de dicionários pode ser transformada em um DataFrame. Usaremos um
compreensão simples da lista para criar alguns dados:
Fora[24]: ab
000
112
224
Mesmo que faltem algumas chaves no dicionário, o Pandas irá preenchê-las com NaN (ou seja,
“não é um número”) valores:
Fora[25]: a a.C.
0 1,0 2 NaN
1NaN3 4,0
De um dicionário de objetos Series. Como vimos antes, um DataFrame pode ser construído
também de um dicionário de objetos Series :
De uma matriz NumPy bidimensional. Dada uma matriz bidimensional de dados, podemos
crie um DataFrame com qualquer coluna e nomes de índice especificados. Se omitido, um número inteiro
índice será usado para cada um:
Em[29]: pd.DataFrame(A)
Fora[29]: AB
0 0 0,0
1 0 0,0
2 0 0,0
imutável O objeto Index funciona de muitas maneiras como um array. Por exemplo, podemos usar a
notação de indexação padrão do Python para recuperar valores ou fatias:
Em[31]: ind[1]
Fora[31]: 3
Em[32]: ind[::2]
Os objetos de índice também possuem muitos dos atributos familiares dos arrays NumPy:
5 (5,) 1 int64
Uma diferença entre objetos Index e arrays NumPy é que os índices são imutáveis – ou seja, eles não
podem ser modificados pelos meios normais:
In[34]: ind[1] = 0
-------------------------------------------------- -------------------------
/Users/jakevdp/anaconda/lib/python3.5/site-packages/pandas/indexes/base.py ...
1243
1244 def __setitem__(self, chave, valor): raise
-> 1245 TypeError("O índice não suporta operações mutáveis")
1246
1247 def __gettime__(self, chave):
Essa imutabilidade torna mais seguro o compartilhamento de índices entre vários DataFrames e
matrizes, sem o potencial de efeitos colaterais decorrentes de modificação inadvertida do índice.
Os objetos Pandas são projetados para facilitar operações como junções entre conjuntos de dados,
que dependem de muitos aspectos da aritmética do conjunto. O objeto Index segue muitos dos
as convenções usadas pela estrutura de dados de conjunto integrada do Python , para que uniões,
interseções, diferenças e outras combinações possam ser computadas de maneira familiar:
Essas operações também podem ser acessadas por meio de métodos de objeto — por exemplo,
seção indA.inter(indB).
Começaremos com o caso simples do objeto Series unidimensional e depois passaremos para o
objeto DataFrame bidimensional mais complicado .
Como vimos na seção anterior, um objeto Series atua em muitos aspectos como um array NumPy
unidimensional e em muitos aspectos como um dicionário Python padrão. Se mantivermos em mente
essas duas analogias sobrepostas, isso nos ajudará a compreender os padrões de indexação e
seleção de dados nessas matrizes.
Como um dicionário, o objeto Series fornece um mapeamento de uma coleção de chaves para uma
coleção de valores:
Fora[1]: ab 0,25
0,50
c 0,75
d 1,00
tipo d: float64
Em[2]: dados['b']
Fora[2]: 0,5
Também podemos usar expressões e métodos Python semelhantes a dicionários para examinar o
chaves/índices e valores:
Fora[3]: Verdadeiro
Em[4]: data.keys()
Em[5]: lista(data.items())
Objetos de série podem até ser modificados com uma sintaxe semelhante a um dicionário. Assim como você pode
estender um dicionário atribuindo a uma nova chave, você pode estender uma série atribuindo
para um novo valor de índice:
Fora[6]: ab 0,25
0,50
0,75
cd 1,00
e 1,25
tipo d: float64
Essa fácil mutabilidade dos objetos é um recurso conveniente: sob o capô, o Pandas está
tomar decisões sobre layout de memória e cópia de dados que podem precisar ser tomadas
lugar; o usuário geralmente não precisa se preocupar com essas questões.
A Series baseia-se nesta interface semelhante a um dicionário e fornece seleção de itens no estilo array.
através dos mesmos mecanismos básicos das matrizes NumPy - ou seja, fatias, mascaramento e
indexação sofisticada. Exemplos destes são os seguintes:
Fora[7]: ab 0,25
0,50
c 0,75
tipo d: float64
Fora[8]: ab 0,25
0,50
tipo d: float64
In[9]: # mascaramento
dados[(dados > 0,3) e (dados < 0,8)]
Fora[9]: b 0,50
0,75
tipo de c: float64
Fora[10]: um 0,25
1,25
e tipo: float64
Entre estes, o fatiamento pode ser a fonte de maior confusão. Observe que quando você
estão fatiando com um índice explícito (ou seja, data['a':'c']), o índice final é incluído em
a fatia, enquanto quando você está fatiando com um índice implícito (ou seja, dados[0:2]), o final
o índice é excluído da fatia.
Essas convenções de divisão e indexação podem ser uma fonte de confusão. Por exemplo, se
sua série tiver um índice inteiro explícito, uma operação de indexação como data[1] irá
use os índices explícitos, enquanto uma operação de fatiamento como data[1:3] usará os índices implícitos
Índice estilo Python.
Fora[11]: 1 a
3 b
c
5 dtipo: objeto
Fora[12]: 'a'
Fora[13]: 3 5 b
c
dtype: objeto
não são métodos funcionais, mas atributos que expõem uma interface de fatiamento específica aos
dados da série.
Primeiro, o atributo loc permite indexação e fatiamento que sempre faz referência ao índice explícito:
Em[14]: data.local[1]
Fora[14]: 'a'
In[15]: data.loc[1:3]
Fora[15]: 1 3 a
b
dtype: objeto
O atributo iloc permite indexação e fatiamento que sempre faz referência ao implícito
Índice estilo Python:
Em[16]: dados.iloc[1]
Fora[16]: 'b'
Em[17]: dados.iloc[1:3]
Fora[17]: 3 5 b
c
dtype: objeto
Um terceiro atributo de indexação, ix, é um híbrido dos dois e, para objetos Series , é equivalente à
indexação padrão baseada em []. O propósito do indexador ix se tornará mais aparente no contexto
dos objetos DataFrame , que discutiremos em breve.
Um princípio orientador do código Python é que “explícito é melhor que implícito”. A natureza explícita
de loc e iloc os torna muito úteis na manutenção de código limpo e legível; especialmente no caso de
índices inteiros, recomendo usá-los para tornar o código mais fácil de ler e entender e para evitar erros
sutis devido à convenção mista de indexação/fatiamento.
Lembre-se de que um DataFrame atua de várias maneiras como um array bidimensional ou estruturado
e, de outras maneiras, como um dicionário de estruturas de série que compartilham o mesmo índice.
Pode ser útil ter em mente essas analogias à medida que exploramos a seleção de dados dentro
dessa estrutura.
DataFrame como um
Em[19]: dados['área']
De forma equivalente, podemos usar acesso de estilo de atributo com nomes de colunas que são strings:
Em[20]: data.area
Esse acesso à coluna no estilo de atributo, na verdade, acessa exatamente o mesmo objeto que o
acesso estilo dicionário:
Fora[21]: Verdadeiro
Embora este seja um atalho útil, lembre-se de que ele não funciona em todos os casos!
Por exemplo, se os nomes das colunas não forem strings ou se os nomes das colunas entrarem em conflito
com métodos do DataFrame, esse acesso no estilo de atributo não é possível. Para a prova-
Por exemplo, o DataFrame tem um método pop() , então data.pop apontará para isso em vez de para o
coluna "pop" :
Fora[22]: Falso
Em particular, você deve evitar a tentação de tentar atribuição de colunas via atributo
(ou seja, use data['pop'] = z em vez de data.pop = z).
Tal como acontece com os objetos Series discutidos anteriormente, esta sintaxe no estilo de dicionário também pode ser
usado para modificar o objeto, neste caso para adicionar uma nova coluna:
Isso mostra uma prévia da sintaxe simples da aritmética elemento por elemento
entre objetos Series ; aprofundaremos isso em “Operando com dados no Pandas”
na página 115.
Conforme mencionado anteriormente, também podemos ver o DataFrame como um array bidimensional
aprimorado. Podemos examinar a matriz de dados subjacentes brutos usando os valores
atributo:
Em[24]: dados.valores
Com esta imagem em mente, podemos fazer muitas observações familiares do tipo array no
O próprio DataFrame . Por exemplo, podemos transpor o DataFrame completo para trocar linhas e
colunas:
Em[25]: dados.T
Fora[25]:
Califórnia Flórida Illinois Nova Iorque Texas
área 4.239670e+05 1.703120e+05 1.499950e+05 1.412970e+05 6.956620e+05
3.833252e+07 1.955286e+07 1.288214e+07 1.965113e+07 2.644819e+07
densidade pop 9,041393e+01 1,148061e+02 8,588376e+01 1,390767e+02 3,801874e+01
Em[26]: dados.valores[0]
Em[27]: dados['área']
Portanto, para indexação em estilo array, precisamos de outra convenção. Aqui o Pandas usa novamente
os indexadores loc, iloc e ix mencionados anteriormente. Usando o indexador iloc , podemos
indexe o array subjacente como se fosse um array NumPy simples (usando o implícito
Índice no estilo Python), mas o índice do DataFrame e os rótulos das colunas são mantidos em
o resultado:
Tenha em mente que para índices inteiros, o indexador ix está sujeito ao mesmo potencial
fontes de confusão conforme discutido para objetos Series indexados por números inteiros .
Qualquer um dos padrões familiares de acesso a dados no estilo NumPy pode ser usado dentro desses índices.
er. Por exemplo, no indexador loc podemos combinar mascaramento e indexação sofisticada como
na sequência:
Qualquer uma dessas convenções de indexação também pode ser usada para definir ou modificar valores; isso é
feito da maneira padrão com a qual você está acostumado ao trabalhar com
NumPy:
Em[32]: dados.iloc[0, 2] = 90
dados
Para aumentar sua fluência na manipulação de dados do Pandas, sugiro passar algum tempo
com um DataFrame simples e explorando os tipos de indexação, fatiamento, mascaramento e
indexação sofisticada permitida por essas diversas abordagens de indexação.
Existem algumas convenções de indexação extras que podem parecer incompatíveis com o pré-
cedendo discussão, mas mesmo assim pode ser muito útil na prática. Primeiro, enquanto o índice
ing refere-se a colunas, slicing refere-se a linhas:
Em[33]: dados['Flórida':'Illinois']
Essas fatias também podem referir-se a linhas por número e não por índice:
Em[34]: dados[1:3]
Da mesma forma, as operações de mascaramento direto também são interpretadas por linha, em vez de
em coluna:
No entanto, o Pandas inclui algumas alterações úteis: para operações unárias como negação
e funções trigonométricas, esses ufuncs preservarão rótulos de índice e coluna na saída, e
para operações binárias como adição e multiplicação, o Pandas alinhará automaticamente
os índices ao passar os objetos para o ufunc. Isso significa que manter o contexto dos dados
e combinar dados de diferentes fontes – ambas tarefas potencialmente propensas a erros
com matrizes NumPy brutas – tornam-se tarefas essencialmente infalíveis com o Pandas.
Veremos adicionalmente que existem operações bem definidas entre estruturas Series
unidimensionais e estruturas DataFrame bidimensionais .
Como o Pandas foi projetado para funcionar com NumPy, qualquer ufunc NumPy funcionará
em objetos Pandas Series e DataFrame . Vamos começar definindo uma série e um
DataFrame simples para demonstrar isso:
Fora[2]: 0 1 6
2 3
7
3 4
tipo d: int64
Fora[3]: ABCD
06926
174372725
4
Em[4]: np.exp(ser)
Fora[4]: 0 1 403.428793
20.085537
2 1096.633158
3 54.598150
tipo d: float64
Fora[5]: A B C D
0 -1,000000 7,071068e-01 1,000000 -1,000000e+00
1 -0,707107 1,224647e-16 0,707107 -7,071068e-01
2 -0,707107 1,000000e+00 -0,707107 1,224647e-16
Por exemplo, suponha que estamos combinando duas fontes de dados diferentes e encontramos apenas
os três principais estados dos EUA por área e os três principais estados dos EUA por população:
Vamos ver o que acontece quando dividimos estes valores para calcular a densidade populacional:
O array resultante contém a união dos índices dos dois arrays de entrada, que
poderia determinar usando a aritmética de conjunto padrão do Python nestes índices:
Qualquer item para o qual um ou outro não tenha entrada é marcado com NaN, ou
“Não é um número”, que é como o Pandas marca os dados ausentes (veja uma discussão mais aprofundada sobre
dados faltantes em “Tratamento de dados faltantes” na página 119). Esta correspondência de índice é simples
mencionado desta forma para qualquer uma das expressões aritméticas integradas do Python; qualquer valor faltante
os valores são preenchidos com NaN por padrão:
Fora[9]: 0 1 NaN
5,0
2 9,0
3 NaN
tipo d: float64
Se usar valores NaN não for o comportamento desejado, podemos modificar o valor de preenchimento usando
métodos de objeto apropriados no lugar dos operadores. Por exemplo, chamando A.add(B)
é equivalente a chamar A + B, mas permite especificação explícita opcional do valor de preenchimento
para quaisquer elementos em A ou B que possam estar faltando:
Fora[10]: 0 2,0
1 5,0
2 9,0
3 5,0
tipo d: float64
Um tipo semelhante de alinhamento ocorre para colunas e índices quando você está
realizando operações em DataFrames:
Fora[11]: AB
0 1 11
15 1
Fora[12]: TAS
0409
1580
2926
Em[13]: A + B
Fora[13]: A AC
0 1,0 15,0 NaN
1 13,0 6,0 NaN
2 NaN NaN NaN
Observe que os índices estão alinhados corretamente, independentemente da sua ordem nos dois objetos,
e os índices no resultado são classificados. Como foi o caso com Series, podemos usar a associação
método aritmético do objeto associado e passe qualquer fill_value desejado para ser usado no lugar
de entradas faltantes. Aqui preencheremos com a média de todos os valores em A (que calculamos
empilhando primeiro as linhas de A):
Fora[14]: A B C
0 1,0 15,0 13,5
1 13,0 6,0 4,5
2 6,5 13,5 10,5
A Tabela 3-1 lista os operadores Python e seus métodos de objeto Pandas equivalentes.
-
sub(), subtrair()
*
mul(), multiplicar()
// floordiv()
% contra()
**
Pancada()
Em[16]: A - A[0]
Fora[17]: QRST0 0 0 0 0
1 -1 -2 2 4
2 3 -7 1 4
Se preferir operar em colunas, você pode usar os métodos de objeto mencionados anteriormente,
especificando a palavra-chave axis :
Fora[18]: QRST 0 -5 0
-6 -4
1 -4 0 -2 2
25027
Fora[19]: Q 3
S2
Nome: 0, dtype: int64
Fora[20]: QR 0 S T
0,0 NaN 0,0 NaN 1 -1,0 NaN
2,0 NaN
2 3,0 NaN 1,0 NaN
Essa preservação e alinhamento de índices e colunas significa que as operações em dados no Pandas
sempre manterão o contexto dos dados, o que evita os tipos de erros bobos que podem surgir quando
você está trabalhando com dados heterogêneos e/ou desalinhados em matrizes NumPy brutas. .
A diferença entre os dados encontrados em muitos tutoriais e os dados do mundo real é que os dados
do mundo real raramente são limpos e homogêneos. Em particular, muitos conjuntos de dados
interessantes terão alguma quantidade de dados faltando. Para complicar ainda mais as coisas,
diferentes fontes de dados podem indicar dados faltantes de diferentes maneiras.
Nesta seção, discutiremos algumas considerações gerais sobre dados ausentes, discutiremos como
o Pandas escolhe representá-los e demonstraremos algumas ferramentas integradas do Pandas para
lidar com dados ausentes em Python. Aqui e ao longo do livro, nos referiremos aos dados faltantes
em geral como valores nulos, NaN ou NA.
Vários esquemas foram desenvolvidos para indicar a presença de dados ausentes em uma tabela ou
DataFrame. Geralmente, eles giram em torno de uma de duas estratégias: usar uma máscara que
indica globalmente valores ausentes ou escolher um valor sentinela que indique uma entrada ausente.
Na abordagem de mascaramento, a máscara pode ser uma matriz booleana totalmente separada ou
pode envolver a apropriação de um bit na representação de dados para indicar localmente o status
nulo de um valor.
Na abordagem sentinela, o valor sentinela pode ser alguma convenção específica de dados, como
indicar um valor inteiro ausente com –9999 ou algum padrão de bits raro, ou pode ser uma convenção
mais global, como indicar um valor de ponto flutuante ausente. com NaN (Not a Number), um valor
especial que faz parte da especificação de ponto flutuante IEEE.
Nenhuma dessas abordagens é isenta de compromissos: o uso de uma matriz de máscara separada
requer a alocação de uma matriz booleana adicional, o que adiciona sobrecarga tanto no
armazenamento quanto na computação. Um valor sentinela reduz o intervalo de valores válidos que
podem ser representados e pode exigir lógica extra (muitas vezes não otimizada) na aritmética da
CPU e GPU. Valores especiais comuns como NaN não estão disponíveis para todos os tipos de dados.
Como na maioria dos casos em que não existe uma escolha universalmente ideal, diferentes
linguagens e sistemas utilizam diferentes convenções. Por exemplo, a linguagem R usa padrões de
bits reservados dentro de cada tipo de dados como valores sentinela indicando dados ausentes,
enquanto o sistema SciDB usa um byte extra anexado a cada célula para indicar um estado NA.
maneira como o Pandas lida com valores ausentes é limitada por sua dependência do pacote NumPy,
que não possui uma noção integrada de valores NA para tipos de dados que não sejam de ponto
flutuante.
O Pandas poderia ter seguido o exemplo de R na especificação de padrões de bits para cada tipo de
dados individual para indicar nulidade, mas essa abordagem acaba sendo bastante complicada.
Embora R contenha quatro tipos de dados básicos, NumPy suporta muito mais do que isso: por
exemplo, enquanto R tem um único tipo inteiro, NumPy suporta quatorze tipos inteiros básicos, uma
vez que você considera as precisões disponíveis, a assinatura e o endian da codificação.
Reservar um padrão de bits específico em todos os tipos NumPy disponíveis levaria a uma grande
quantidade de sobrecarga em várias operações de revestimento especial para vários tipos,
provavelmente até exigindo um novo fork do pacote NumPy. Além disso, para tipos de dados menores
(como números inteiros de 8 bits), sacrificar um bit para usar como máscara reduzirá significativamente
o intervalo de valores que ela pode representar.
NumPy tem suporte para matrizes mascaradas – ou seja, matrizes que possuem uma matriz de
máscara booleana separada anexada para marcar os dados como “bons” ou “ruins”. O Pandas
poderia ter derivado disso, mas a sobrecarga em armazenamento, computação e manutenção de
código torna essa escolha pouco atraente.
Com essas restrições em mente, o Pandas optou por usar sentinelas para dados ausentes e ainda
optou por usar dois valores nulos do Python já existentes: o valor NaN de ponto flutuante especial e
o objeto Python None . Esta escolha tem alguns efeitos colaterais, como veremos, mas na prática
acaba sendo um bom compromisso na maioria dos casos de interesse.
Python O primeiro valor sentinela usado pelo Pandas é None, um objeto singleton do Python que é
frequentemente usado para dados faltantes no código Python. Como None é um objeto Python, ele
não pode ser usado em nenhum array NumPy/Pandas arbitrário, mas apenas em arrays com tipo de
dados 'objeto' (ou seja, arrays de objetos Python):
Este dtype=object significa que a melhor representação de tipo comum que NumPy pode inferir para
o conteúdo do array é que eles são objetos Python. Embora esse tipo de array de objetos seja útil
para alguns propósitos, quaisquer operações nos dados serão feitas no nível Python, com muito mais
sobrecarga do que as operações normalmente rápidas vistas em arrays com tipos nativos:
dtype = objeto 10
loops, melhor de 3: 78,2 ms por loop
dtype = int
100 loops, melhor de 3: 3,06 ms por loop
O uso de objetos Python em um array também significa que se você realizar agregações como sum()
ou min() em um array com um valor None , geralmente obterá um erro:
Em[4]: vals1.sum()
/Users/jakevdp/anaconda/lib/python3.5/site-packages/numpy/core/_methods.py ...
Isso reflete o fato de que a adição entre um número inteiro e None é indefinida.
A outra representação de dados faltantes, NaN (sigla para Not a Number), é diferente; é um valor
especial de ponto flutuante reconhecido por todos os sistemas que usam o padrão
Representação de ponto flutuante IEEE:
Saída[5]: dtype('float64')
Observe que NumPy escolheu um tipo de ponto flutuante nativo para este array: isso significa
que, diferentemente do array de objetos anterior, este array suporta operações rápidas inseridas
no código compilado. Você deve estar ciente de que o NaN é um pouco como um vírus de dados
– ele infecta qualquer outro objeto que toca. Independente da operação, o resultado da aritmética
com NaN será outro NaN:
In[6]: 1 + np.in
Agosto[6]: em
Em[7]: 0 * np. em
Agosto[7]: em
Observe que isso significa que as agregações sobre os valores são bem definidas (ou seja, não
resultam em erro), mas nem sempre são úteis:
NumPy fornece algumas agregações especiais que irão ignorar esses valores ausentes:
Tenha em mente que NaN é especificamente um valor de ponto flutuante; não há valor NaN equivalente
para números inteiros, strings ou outros tipos.
NaN e None têm seu lugar, e o Pandas foi desenvolvido para lidar com os dois de forma quase
intercambiável, convertendo entre eles quando apropriado:
Fora[10]: 0 1,0
1 NaN
2 2,0
3 NaN
tipo d: float64
Para tipos que não possuem um valor sentinela disponível, o Pandas converte automaticamente o tipo
quando os valores NA estão presentes. Por exemplo, se definirmos um valor em uma matriz de inteiros
como np.nan, ele será automaticamente convertido para um tipo de ponto flutuante para acomodar o
NA:
Fora[11]: 0 0
1 1
tipo d: int64
Fora[12]: 0 NaN
1 1,0
tipo d: float64
Observe que, além de converter o array inteiro em ponto flutuante, o Pandas converte automaticamente
None em um valor NaN . (Esteja ciente de que há uma proposta para adicionar um número inteiro
nativo NA ao Pandas no futuro; no momento da redação deste artigo, ele não foi incluído.)
Embora esse tipo de mágica possa parecer um pouco hackeado em comparação com a abordagem
mais unificada dos valores de NA em linguagens de domínio específico como R, a abordagem
sentinela/casting do Pandas funciona muito bem na prática e, na minha experiência, raramente causa
problemas.
A Tabela 3-2 lista as convenções de upcasting em Pandas quando os valores NA são introduzidos.
Tenha em mente que no Pandas, os dados da string são sempre armazenados com um tipo de objeto .
é nulo()
Gere uma máscara booleana indicando valores ausentes
não nulo()
Oposto de isnull()
derrubar()
Retornar uma versão filtrada dos dados
preencher()
Retornar uma cópia dos dados com valores ausentes preenchidos ou imputados
As estruturas de dados do Pandas têm dois métodos úteis para detectar dados nulos: isnull() e
não nulo(). Qualquer um retornará uma máscara booleana sobre os dados. Por exemplo:
Em[14]: data.isnull()
Fora[14]: 0 Falso
1 Verdadeiro
2 Falso
3 Verdadeiro
tipo d: bool
Conforme mencionado em “Indexação e seleção de dados” na página 107, as máscaras booleanas podem ser
usado diretamente como um índice Series ou DataFrame :
Em[15]: dados[data.notnull()]
Fora[15]: 0 1
2 olá
dtype: objeto
Em[16]: data.dropna()
Fora[16]: 0 1
2 olá
dtype: objeto
Fora[17]: 0 12
0 1,0 NaN 2
1 2,0 3,0 5
2NaN 4,0 6
Não podemos eliminar valores únicos de um DataFrame; só podemos descartar linhas completas ou completas
colunas. Dependendo da aplicação, você pode querer um ou outro, então
dropna() oferece várias opções para um DataFrame.
Por padrão, dropna() eliminará todas as linhas nas quais qualquer valor nulo estiver presente:
Em[18]: df.dropna()
Fora[18]: 0 12
1 2,0 3,0 5
Como alternativa, você pode descartar os valores de NA ao longo de um eixo diferente; eixo = 1 elimina todas as colunas
umns contendo um valor nulo:
Em[19]: df.dropna(axis='colunas')
Fora[19]: 2
02
15
26
Mas isso também elimina alguns dados bons; você pode estar interessado em largar
linhas ou colunas com todos os valores NA ou a maioria dos valores NA. Isso pode ser especificado
através dos parâmetros how ou thresh , que permitem um controle preciso do número de
nulos para permitir a passagem.
O padrão é how='any', de modo que qualquer linha ou coluna (dependendo da chave do eixo )
word) contendo um valor nulo será descartado. Você também pode especificar how='all', que
eliminará apenas linhas/colunas que sejam todas valores nulos:
Fora[20]: 0 123
0 1,0 NaN 2 NaN
1 2,0 3,0 5 NaN
2 NaN 4,0 6 NaN
Fora[21]: 0 12
0 1,0 NaN 2
1 2,0 3,0 5
2NaN 4,0 6
Para um controle mais refinado, o parâmetro thresh permite especificar um número mínimo
de valores não nulos para a linha/coluna a ser mantida:
Fora[22]: 0 123
1 2,0 3,0 5 NaN
Aqui, a primeira e a última linha foram eliminadas porque contêm apenas dois valores não nulos.
Às vezes, em vez de descartar os valores de NA, você prefere substituí-los por valores válidos.
valor. Este valor pode ser um único número como zero, ou pode ser algum tipo de
imputação ou interpolação dos bons valores. Você poderia fazer isso no local usando
o método isnull() como uma máscara, mas por ser uma operação tão comum Pandas
fornece o método fillna() , que retorna uma cópia do array com os valores nulos
substituído.
Fora[23]: ab 1,0
NaN
c 2,0
d NaN
e 3,0
tipo d: float64
Em[24]: data.fillna(0)
Fora[24]: ab 1,0
0,0
2,0
cd 0,0
e 3,0
tipo d: float64
Fora[25]: ab 1,0
1,0
c 2,0
d 2,0
e 3,0
tipo d: float64
In[26]: # preenchimento
dados.fillna(método='bfill')
Fora[26]: ab 1,0
2,0
c 2,0
d 3,0
e 3,0
tipo d: float64
Para DataFrames, as opções são semelhantes, mas também podemos especificar um eixo ao longo do qual
os preenchimentos ocorrem:
Em[27]: df
Fora[27]: 0 123
0 1,0 NaN 2 NaN
1 2,0 3,0 5 NaN
2 NaN 4,0 6 NaN
Fora[28]: 0 1 2 3
0 1,0 1,0 2,0 2,0
1 2,0 3,0 5,0 5,0
2NaN 4,0 6,0 6,0
Observe que se um valor anterior não estiver disponível durante um preenchimento direto, o valor NA
restos.
Indexação Hierárquica
Até este ponto, nos concentramos principalmente em dados unidimensionais e bidimensionais,
armazenados em objetos Pandas Series e DataFrame , respectivamente. Muitas vezes é útil ir
além disso e armazenar dados de dimensões superiores – ou seja, dados indexados por mais de
uma ou duas chaves. Embora o Pandas forneça objetos Panel e Panel4D que lidam nativamente
com dados tridimensionais e quadridimensionais (consulte “Dados do painel” na página 141), um
padrão muito mais comum na prática é fazer uso de indexação hierárquica (também conhecida
como multi- indexação) para incorporar vários níveis de índice em um único índice. Dessa forma,
dados de dimensões superiores podem ser representados de forma compacta nos familiares
objetos Series unidimensionais e DataFrame bidimensionais .
Nesta seção exploraremos a criação direta de objetos MultiIndex ; considerações sobre indexação,
divisão e computação de estatísticas em dados indexados multiplicados; e rotinas úteis para
conversão entre representações simples e indexadas hierarquicamente de seus dados.
Vamos começar considerando como podemos representar dados bidimensionais em uma série
unidimensional . Para maior concretude, consideraremos uma série de dados onde cada ponto
possui um caractere e uma chave numérica.
A má
maneira Suponha que você queira rastrear dados sobre estados de dois anos diferentes. Usando
as ferramentas Pandas que já abordamos, você pode ficar tentado a simplesmente usar tuplas
Python como chaves:
Com este esquema de indexação, você pode indexar ou dividir diretamente a série com base
neste índice múltiplo:
Mas a comodidade termina aí. Por exemplo, se você precisar selecionar todos os valores de
2010, você precisará fazer algumas tarefas complicadas (e potencialmente lentas) para conseguir
acontecer:
Isso produz o resultado desejado, mas não é tão limpo (ou tão eficiente para grandes conjuntos de dados)
como a sintaxe de fatiamento que amamos no Pandas.
Felizmente, o Pandas oferece uma maneira melhor. Nossa indexação baseada em tupla é essencialmente uma
multi-índice rudimentar, e o tipo Pandas MultiIndex nos dá o tipo de operação
que desejamos ter. Podemos criar um multi-índice a partir das tuplas da seguinte forma:
Observe que o MultiIndex contém vários níveis de indexação – neste caso, o estado
nomes e anos, bem como vários rótulos para cada ponto de dados que codificam esses
níveis.
Aqui, as duas primeiras colunas da representação em série mostram os valores de índice múltiplo
ues, enquanto a terceira coluna mostra os dados. Observe que algumas entradas estão faltando em
a primeira coluna: nesta representação multi-índice, qualquer entrada em branco indica o
mesmo valor da linha acima dela.
Agora, para acessar todos os dados cujo segundo índice é 2010, podemos simplesmente usar o Panÿ
notação de fatiamento:
O resultado é um array indexado individualmente com apenas as chaves nas quais estamos interessados. Esta sintaxe
é muito mais conveniente (e a operação é muito mais eficiente!) do que a solução de multi-indexação baseada em
tuplas caseira com a qual começamos. Vamos agora dis-
amaldiçoe esse tipo de operação de indexação em dados indexados hierarquicamente.
Você pode notar outra coisa aqui: poderíamos facilmente ter armazenado os mesmos dados
usando um DataFrame simples com rótulos de índice e coluna. Na verdade, o Pandas é construído com
esta equivalência em mente. O método unstack() converterá rapidamente uma série indexada multiplicadamente em
um DataFrame indexado convencionalmente:
Em[9]: pop_df.stack()
Vendo isso, você pode se perguntar por que nos preocuparíamos com índices hierárquicos?
de jeito nenhum. A razão é simples: assim como conseguimos usar a multi-indexação para representar
dados bidimensionais dentro de uma Série unidimensional , também podemos usá-los para
representar dados de três ou mais dimensões em uma Série ou DataFrame. Cada nível extra em
um multiíndice representa uma dimensão extra de dados; aproveitar essa propriedade nos dá
muito mais flexibilidade nos tipos de dados que podemos representar. Concretamente,
poderíamos querer adicionar outra coluna de dados demográficos para cada estado em cada
ano (digamos, população com menos de 18 anos); com um MultiIndex isso é tão fácil quanto
adicionar outra coluna ao DataFrame:
Além disso, todos os ufuncs e outras funcionalidades discutidas em “Operando com dados no
Pandas” na página 115 também funcionam com índices hierárquicos. Aqui calculamos a fração
de pessoas com menos de 18 anos por ano, dados os dados acima:
Isso nos permite manipular e explorar de maneira fácil e rápida até mesmo dados de alta
dimensão.
df
Da mesma forma, se você passar um dicionário com tuplas apropriadas como chaves, o Pandas
reconhecerá isso automaticamente e usará um MultiIndex por padrão:
No entanto, às vezes é útil criar explicitamente um MultiIndex; veremos alguns desses métodos aqui.
Construtores MultiIndex
explícitos Para obter mais flexibilidade na forma como o índice é construído, você pode usar os
construtores de método de classe disponíveis em pd.MultiIndex. Por exemplo, como fizemos antes,
você pode construir o MultiIndex a partir de uma lista simples de arrays, fornecendo os valores do
índice dentro de cada nível:
Você pode construí-lo a partir de uma lista de tuplas, fornecendo os múltiplos valores de índice de cada
ponto:
Da mesma forma, você pode construir o MultiIndex diretamente usando sua codificação interna,
passando níveis (uma lista de listas contendo valores de índice disponíveis para cada nível) e rótulos
(uma lista de listas que fazem referência a esses rótulos):
Você pode passar qualquer um desses objetos como argumento de índice ao criar uma Série ou
DataFrame, ou para o método de reindexação de uma Série ou DataFrame existente .
Às vezes é conveniente nomear os níveis do MultiIndex. Você pode fazer isso passando o argumento
de nomes para qualquer um dos construtores MultiIndex acima ou definindo o atributo de nomes do
índice após o fato:
Com conjuntos de dados mais envolvidos, esta pode ser uma forma útil de acompanhar o significado
de vários valores de índice.
Em um DataFrame, as linhas e colunas são completamente simétricas e, assim como as linhas podem
ter vários níveis de índices, as colunas também podem ter vários níveis. Considere o seguinte, que é
uma maquete de alguns dados médicos (um tanto realistas):
In[19]: #
índices hierárquicos e índice de colunas =
pd.MultiIndex.from_product([[2013, 2014], [1, 2]], nomes=['ano', 'visita'])
colunas = pd.MultiIndex.
from_product([['Bob', 'Guido', 'Sue'], ['RH', 'Temp']],
nomes=['assunto', 'tipo'])
# zomba de alguns
dados data = np.round(np.random.randn(4, 6), 1)
data[:, ::2] *= 10 dados
+= 37
# crie o DataFrame
health_data = pd.DataFrame(data, index=index, columns=columns) health_data
visita
2013 1 2 31,0 38,7 32,0 36,7 35,0 37,2
44,0 37,7 50,0 35,0 29,0 36,7
2014 1 30,0 37,4 39,0 37,8 61,0 36,9
2 47,0 37,8 48,0 37,3 51,0 36,5
Aqui vemos onde a multi-indexação para linhas e colunas pode ser muito útil.
útil. Estes são dados fundamentalmente quadridimensionais, onde as dimensões são as
assunto, o tipo de medição, o ano e o número da visita. Com isso em vigor, nós
pode, por exemplo, indexar a coluna de nível superior pelo nome da pessoa e obter um conjunto de dados completo
Quadro contendo apenas as informações dessa pessoa:
In[20]: health_data['Guido']
de visita
2013 1 32,0 36,7
2 50,0 35,0
2014 1 2 39,0 37,8
48,0 37,3
Em[21]: pop
Saída[22]: 33871648
Em[23]: pop['Califórnia']
Saída[23]: ano
2000 33871648
2010 37253956
tipo d: int64
O fatiamento parcial também está disponível, desde que o MultiIndex esteja classificado (veja a discussão
em “Índices classificados e não classificados” na página 137):
Com índices ordenados, podemos realizar indexação parcial em níveis inferiores, passando um
fatia vazia no primeiro índice:
Fora[25]: estado
Califórnia 33871648
Nova Iorque 18976457
Tipo 20851820
de Texas: int64
Um DataFrame com indexação múltipla se comporta de maneira semelhante. Considere nosso brinquedo medi-
cal DataFrame de antes:
Em[28]: dados_de_saúde
visita
2013 1 31,0 38,7 32,0 36,7 35,0 37,2
2 44,0 37,7 50,0 35,0 29,0 36,7
2014 1 30,0 37,4 39,0 37,8 61,0 36,9
2 47,0 37,8 48,0 37,3 51,0 36,5
Lembre-se de que as colunas são primárias em um DataFrame e a sintaxe usada para multi-
A série indexada por camadas se aplica às colunas. Por exemplo, podemos recuperar o coração de Guido
avaliar dados com uma operação simples:
Além disso, como no caso de índice único, podemos usar os indexadores loc, iloc e ix introÿ
apresentado em “Indexação e seleção de dados” na página 107. Por exemplo:
ano Temperatura de RH
visita
2013 1 2 31,0 38,7
44,0 37,7
Esses indexadores fornecem uma visão semelhante a uma matriz dos dados bidimensionais subjacentes,
mas cada índice individual em loc ou iloc pode receber uma tupla de vários índices. Para
exemplo:
Trabalhar com fatias dentro dessas tuplas de índice não é especialmente conveniente; tentando
criar uma fatia dentro de uma tupla causará um erro de sintaxe:
Você poderia contornar isso construindo explicitamente a fatia desejada usando a função slice() integrada do
Python , mas a melhor maneira neste contexto é usar um objeto IndexSlice ,
que o Pandas fornece precisamente para esta situação. Por exemplo:
Existem tantas maneiras de interagir com dados em séries e dados indexados multiplicados
Frames e, como acontece com muitas ferramentas neste livro, a melhor maneira de se familiarizar com
eles é experimentá-los!
Reorganizando Multiíndices
Um dos segredos para trabalhar com dados indexados multiplicados é saber como efetivamente
transformar os dados. Há uma série de operações que preservarão todas as informações
informação no conjunto de dados, mas reorganize-a para fins de vários cálculos. Nós
vi um breve exemplo disso nos métodos stack() e unstack() , mas existem
muitas outras maneiras de controlar com precisão o rearranjo de dados entre
índices e colunas, e iremos explorá-los aqui.
Anteriormente, mencionamos brevemente uma advertência, mas devemos enfatizá-la mais aqui. Muitos
as operações de fatiamento do MultiIndex falharão se o índice não for classificado. Vamos dar uma olhada
isso aqui.
Começaremos criando alguns dados simples indexados por multiplicação onde os índices não são
ordenado lexograficamente:
2 0,569264
b 1 0,001693
2 0,526226
tipo d: float64
Se tentarmos obter uma fatia parcial deste índice, isso resultará em um erro:
Em[35]: tente:
dados['a':'b']
exceto KeyError como e:
imprimir(tipo(e))
imprimir(e)
<class 'KeyError'>
'O comprimento da chave (1) era maior que a profundidade do lexsort MultiIndex (0)'
Embora não esteja totalmente claro na mensagem de erro, este é o resultado do Multi
O índice não está sendo classificado. Por diversas razões, fatias parciais e outras operações similares
As disposições exigem que os níveis no MultiIndex estejam em ordem ordenada (isto é, lexográfica).
O Pandas fornece uma série de rotinas convenientes para realizar esse tipo de classificação;
exemplos são os métodos sort_index() e sortlevel() do DataFrame. Bem
use o mais simples, sort_index(), aqui:
Com o índice classificado desta forma, o fatiamento parcial funcionará conforme o esperado:
Em[37]: dados['a':'b']
Como vimos brevemente antes, é possível converter um conjunto de dados de um multi-índice empilhado
para uma representação bidimensional simples, especificando opcionalmente o nível a ser usado:
Em[38]: pop.unstack(nível=0)
Em[39]: pop.unstack(nível=1)
O oposto de unstack() é stack(), que aqui pode ser usado para recuperar o original
Series:
Em[40]: pop.unstack().stack()
Muitas vezes, quando você está trabalhando com dados no mundo real, os dados brutos de entrada se parecem com
isso e é útil construir um MultiIndex a partir dos valores da coluna. Isto pode ser feito
com o método set_index do DataFrame, que retorna dados indexados multiplicados
Quadro:
Fora[42]: população
estado ano
Califórnia 2000 2010 33871648
37253956
Nova York 2000 18976457
2010 19378102
Texas 2000 20851820
2010 25145561
Na prática, considero esse tipo de reindexação um dos padrões mais úteis quando
encontrar conjuntos de dados do mundo real.
Em[43]: dados_de_saúde
visita
2013 1 31,0 38,7 32,0 36,7 35,0 37,2
2 44,0 37,7 50,0 35,0 29,0 36,7
2014 1 2 30,0 37,4 39,0 37,8 61,0 36,9
47,0 37,8 48,0 37,3 51,0 36,5
Talvez devêssemos calcular a média das medições nas duas visitas de cada ano. Pudermos
faça isso nomeando o nível do índice que gostaríamos de explorar, neste caso o ano:
ano
2013 37,5 38,2 41,0 35,85 32,0 36,95
2014 38,5 37,6 43,5 37,55 56,0 36,70
Utilizando ainda mais a palavra-chave axis , podemos calcular a média entre os níveis em
as colunas também:
ano
2013 36,833333 37,000000
2014 46,000000 37,283333
Assim, em duas linhas, conseguimos encontrar a frequência cardíaca e a temperatura médias medidas
entre todos os indivíduos em todas as visitas de cada ano. Essa sintaxe é na verdade um atalho para
a funcionalidade GroupBy , que discutiremos em “Agregação e agrupamento” na página 158. Embora
este seja um exemplo de brinquedo, muitos conjuntos de dados do mundo real têm estrutura
hierárquica semelhante.
Dados do Painel
O Pandas tem algumas outras estruturas de dados fundamentais que ainda não discutimos,
nomeadamente os objetos pd.Panel e pd.Panel4D . Estas podem ser pensadas, respectivamente,
como generalizações tridimensionais e quadridimensionais do (unidimensional)
Estruturas de séries e DataFrame (bidimensionais) . Depois que você estiver familiarizado com a
indexação e manipulação de dados em Series e DataFrame, Panel e Panel4D são relativamente
simples de usar. Em particular, os indexadores ix, loc e iloc discutidos em “Indexação e seleção de
dados” na página 107 estendem-se prontamente a essas estruturas de dimensões superiores.
Não abordaremos mais essas estruturas de painel neste texto, pois descobri na maioria dos casos
que a multi-indexação é uma representação mais útil e conceitualmente mais simples para dados de
dimensões superiores. Além disso, os dados em painel são fundamentalmente uma representação
de dados densa, enquanto a multi-indexação é fundamentalmente uma representação de dados esparsos.
À medida que o número de dimensões aumenta, a representação densa pode tornar-se muito
ineficiente para a maioria dos conjuntos de dados do mundo real. Para aplicações especializadas
ocasionais, entretanto, essas estruturas podem ser úteis. Se você quiser ler mais sobre as estruturas
Panel e Panel4D , consulte as referências listadas em “Recursos Adicionais” na página 215.
Aqui daremos uma olhada na concatenação simples de Series e DataFrames com a função pd.concat ;
mais tarde, mergulharemos em fusões e junções na memória mais sofisticadas implementadas no
Pandas.
Por conveniência, definiremos esta função, que cria um DataFrame de um formato específico
que será útil a seguir:
# exemplo DataFrame
make_df('ABC', range(3))
Fora[2]: AB C
0 A0 B0 C0
1A1 B1 C1
2A2 B2 C2
O primeiro argumento é uma lista ou tupla de arrays a serem concatenados. Além disso, é
necessária uma palavra-chave axis que permite especificar o eixo ao longo do qual o resultado
será concatenado:
pd.concat() pode ser usado para uma concatenação simples de objetos Series ou DataFrame ,
assim como np.concatenate() pode ser usado para concatenações simples de arrays:
Fora[6]: 1 2 A
B
3 C
4 D
5 E
6 F
dtype: objeto
Por padrão, a concatenação ocorre por linha dentro do DataFrame (ou seja,
eixo=0). Assim como np.concatenate, pd.concat permite a especificação de um eixo ao longo do qual
ocorrerá a concatenação. Considere o seguinte exemplo:
Poderíamos ter especificado de forma equivalente axis=1; aqui usamos o mais intuitivo
eixo='col'.
Índices duplicados
Em[10]: tente:
pd.concat([x, y], verificar_integridade=Verdadeiro)
exceto ValueError como e:
print("Erro de valor:", e)
Adicionando chaves MultiIndex. Outra alternativa é usar a opção de chaves para especificar um rótulo
para as fontes de dados; o resultado será uma série indexada hierarquicamente contendo os
dados:
Nos exemplos simples que acabamos de ver, estávamos principalmente concatenando DataFrames
com nomes de colunas compartilhadas. Na prática, dados de diferentes fontes podem ter diferenças
ent conjuntos de nomes de colunas e pd.concat oferece várias opções neste caso. Considerar
a concatenação dos dois DataFrames a seguir, que possuem alguns (mas não todos!)
colunas em comum:
Por padrão, as entradas para as quais não há dados disponíveis são preenchidas com valores NA. Para
mudar isso, podemos especificar uma das várias opções para os parâmetros join e join_axes
éteres da função concatenada. Por padrão, a junção é uma união das colunas de entrada
(join='outer'), mas podemos mudar isso para uma interseção das colunas usando
join='interno':
Outra opção é especificar diretamente o índice das colunas restantes usando o comando
Argumento join_axes , que recebe uma lista de objetos de índice. Aqui vamos especificar que o
as colunas retornadas devem ser iguais às da primeira entrada:
3NaN B3 C3
4NaN B4 C4
O método append()
Como a concatenação direta de array é tão comum, os objetos Series e DataFrame têm um
método append que pode realizar a mesma coisa com menos pressionamentos de tecla. Por
exemplo, em vez de chamar pd.concat([df1, df2]), você pode simplesmente chamar
df1.append(df2):
Tenha em mente que, diferentemente dos métodos append() e extend() das listas Python, o
método append() no Pandas não modifica o objeto original – em vez disso, ele cria um novo
objeto com os dados combinados. Também não é um método muito eficiente, pois envolve a
criação de um novo índice e buffer de dados. Assim, se você planeja realizar várias operações
de acréscimo , geralmente é melhor construir uma lista de DataFrames e passá-los todos de
uma vez para a função concat() .
Na próxima seção, veremos outra abordagem mais poderosa para combinar dados de
múltiplas fontes, as mesclagens/junções no estilo de banco de dados implementadas em
pd.merge. Para obter mais informações sobre concat(), anexar() e funcionalidades
relacionadas, consulte a seção “Mesclar, unir e concatenar” da documentação do Pandas.
Um recurso essencial oferecido pelo Pandas são suas operações de junção e mesclagem de
alto desempenho na memória. Se você já trabalhou com bancos de dados, deve estar
familiarizado com esse tipo de interação de dados. A interface principal para isso é a função
pd.merge , e veremos alguns exemplos de como isso pode funcionar na prática.
Álgebra Relacional O
abordagem da álgebra relacional é que ela propõe diversas operações primitivas, que
tornam-se os blocos de construção de operações mais complicadas em qualquer conjunto de dados. Com isso
léxico de operações fundamentais implementadas de forma eficiente em um banco de dados ou outro programa
grama, uma ampla gama de operações compostas bastante complicadas pode ser executada.
Categorias de junções
Junções um a um
Talvez o tipo mais simples de expressão de mesclagem seja a junção um-para-um, que está em
muitas maneiras muito semelhantes à concatenação em colunas vista em “Combinação de dados”.
conjuntos: Concat e Append” na página 141. Como exemplo concreto, considere o seguinte
dois DataFrames, que contêm informações sobre vários funcionários de uma empresa:
Em 2]:
df1 = pd.DataFrame({'funcionário': ['Bob', 'Jake', 'Lisa', 'Sue'],
'grupo': ['Contabilidade', 'Engenharia', 'Engenharia', 'RH']})
df2 = pd.DataFrame({'funcionário': ['Lisa', 'Bob', 'Jake', 'Sue'],
'data_contratação': [2004, 2008, 2012, 2014]})
imprimir(df1); imprimir (df2)
df1 df2
grupo de funcionários data_de contratação do funcionário
0 Bob Contabilidade 0 Lisa 2004
1 Jake Engenharia 1 Bob 2008
2 Lisa Engenharia 2 Jake 2012
3 Processar RH 3 Processar 2014
A função pd.merge() reconhece que cada DataFrame possui uma coluna “funcionário”,
e junta-se automaticamente usando esta coluna como chave. O resultado da fusão é um novo
DataFrame que combina as informações das duas entradas. Observe que a ordem
de entradas em cada coluna não é necessariamente mantida: neste caso, a ordem dos
A coluna “funcionário” difere entre df1 e df2, e a função pd.merge() corresponde
explica isso corretamente. Além disso, tenha em mente que a fusão em geral descarta
o índice, exceto no caso especial de mesclagens por índice (veja “O left_index e
palavras-chave right_index” na página 151).
Junções muitos-para-um
Junções muitos-para-um são junções nas quais uma das duas colunas-chave contém duplicatas
entradas. Para o caso muitos para um, o DataFrame resultante preservará essas duplicatas.
cate as entradas conforme apropriado. Considere o seguinte exemplo de junção muitos-para-um:
df3 df4
supervisor do grupo de grupo
funcionários
contratar_data
0 Bob Contabilidade 2008 0 Contabilidade Carly
1 Jake Engenharia 2012 1 Engenharia Guido
2 Lisa Engenharia 2004 2 RH Steve
3 Processar RH 2014
pd.merge(df3, df4)
supervisor do grupo de funcionários contratar_data
0 Bob Contabilidade Carly 2008
1 Jake Engenharia Guido 2012
2 Lisa Engenharia Sue RH 2004 Guido
3 2014 Steve
Junções muitos-para-muitos
Junções muitos-para-muitos são um pouco confusas conceitualmente, mas mesmo assim são bem
definiram. Se a coluna-chave na matriz esquerda e direita contiver duplicatas, então
o resultado é uma mesclagem muitos-para-muitos. Isto ficará talvez mais claro com uma abordagem concreta
exemplo. Considere o seguinte, onde temos um DataFrame mostrando um ou mais
habilidades associadas a um determinado grupo.
df5
grupo de funcionários df1 grupo habilidades
pd.merge(df1, df5)
habilidades dos funcionários grupo
0 Bob Contabilidade matemática
1 Planilhas de contabilidade de Bob
2 Codificação da Jake Engineering
3 Jake Engenharia Linux
4 Lisa Engenharia Lisa codificação
5 Engenharia linux
6 Processar Planilhas de RH
7 Processar Organização de RH
Esses três tipos de junções podem ser usados com outras ferramentas Pandas para implementar uma ampla
conjunto de funcionalidades. Mas, na prática, os conjuntos de dados raramente são tão limpos quanto aquele que estamos
trabalhando aqui. Na seção a seguir, consideraremos algumas das opções proÿ
fornecido por pd.merge() que permite ajustar como as operações de junção funcionam.
A palavra-chave ativada
Mais simplesmente, você pode especificar explicitamente o nome da coluna-chave usando o comando on key.
palavra, que recebe um nome de coluna ou uma lista de nomes de colunas:
df1 df2
grupo de funcionários data_de contratação do funcionário
0 Bob Contabilidade 0 Lisa 2004
1 Jake Engenharia 1 Prumo 2008
2 Lisa Engenharia 2 Jake 2012
3 Processar RH 3 Processar 2014
Esta opção funciona apenas se os DataFrames esquerdo e direito tiverem a coluna especificada
hum nome.
Às vezes você pode desejar mesclar dois conjuntos de dados com nomes de colunas diferentes; para a prova-
Por exemplo, podemos ter um conjunto de dados em que o nome do funcionário é rotulado como “nome” em vez de
do que “empregado”. Neste caso, podemos usar as palavras-chave left_on e right_on para
especifique os dois nomes de colunas:
Em[7]:
df3 = pd.DataFrame({'nome': ['Bob', 'Jake', 'Lisa', 'Sue'],
'salário': [70.000, 80.000, 120.000, 90.000]})
imprimir(df1); imprimir(df3);
print(pd.merge(df1, df3, left_on="funcionário", right_on="nome"))
df3
grupo de funcionários df1 nome salário
0 Bob Contabilidade 0 Bob 70000
1 Jake Engenharia 1Jake 80000
2 Lisa Engenharia doisLisa 120000
3 Processar RH 3 Sue 90000
O resultado tem uma coluna redundante que podemos eliminar se desejarmos — por exemplo, por
usando o método drop() de DataFrames:
Em[8]:
pd.merge(df1, df3, left_on="employee", right_on="nome").drop('nome', eixo=1)
df1a df2a
grupo data de contratação
funcionário funcionário
Prumo Contabilidade Lisa 2004
Jake Engenharia Prumo 2008
Lisa Engenharia Jake 2012
Processar RH Processar 2014
Você pode usar o índice como chave para mesclar especificando left_index e/ou
Sinalizadores right_index em pd.merge():
Em[10]:
imprimir(df1a); imprimir(df2a);
imprimir(pd.merge(df1a, df2a, left_index=True, right_index=True))
df1a df2a
grupo data de contratação
funcionário funcionário
Prumo Contabilidade Lisa 2004
Jake Engenharia Prumo 2008
Lisa Engenharia Jake 2012
Processar RH Processar 2014
funcionário
Lisa Engenharia 2004
Prumo Contabilidade 2008
Jake Engenharia 2012
Processar RH 2014
df1a df2a
grupo data de contratação
funcionário funcionário
Prumo Contabilidade Lisa 2004
Jake Engenharia Prumo 2008
Lisa Engenharia Jake 2012
Processar RH Processar 2014
df1a.join(df2a)
data_de contratação do grupo
funcionário
Prumo Contabilidade 2008
Jake Engenharia 2012
Lisa Engenharia 2004
Processar RH 2014
Se quiser misturar índices e colunas, você pode combinar left_index com right_on
ou left_on com right_index para obter o comportamento desejado:
Em[12]:
imprimir(df1a); imprimir(df3);
imprimir(pd.merge(df1a, df3, left_index=True, right_on='nome'))
df1a df3
grupo
funcionário nome salário
Prumo Contabilidade 0 Bob 70000
Jake Engenharia 1Jake 80000
Lisa Engenharia doisLisa 120000
Processar RH 3 Sue 90000
Todas essas opções também funcionam com múltiplos índices e/ou múltiplas colunas; o
interface para esse comportamento é muito intuitiva. Para obter mais informações sobre isso, consulte o
Seção “Mesclar, Unir e Concatenar” da documentação do Pandas.
Aqui mesclamos dois conjuntos de dados que possuem apenas uma única entrada de “nome” em comum:
Mary. Por padrão, o resultado contém a interseção dos dois conjuntos de entradas; isso é
o que é conhecido como junção interna. Podemos especificar isso explicitamente usando a palavra-chave how ,
cujo padrão é 'interno':
Outras opções para a palavra-chave how são 'outer', 'left' e 'right'. Uma junção externa
retorna uma junção sobre a união das colunas de entrada e preenche todos os valores ausentes com
NAs:
A junção à esquerda e a junção à direita retornam a junção sobre as entradas esquerda e direita, respectivamente.
ativamente. Por exemplo:
Todas essas opções podem ser aplicadas diretamente a qualquer uma das opções de junção anteriores.
tipos.
Em[18]:
imprimir(df8); imprimir(df9);
print(pd.merge(df8, df9, on="nome", sufixos=["_L", "_R"]))
df8 df9
classificação de nome classificação de nome
0Bob 1 0Bob 3
1Jake 2 1Jake 1
doisLisa 3 doisLisa 4
3 Sue 4 3 Sue 2
Esses sufixos funcionam em qualquer um dos padrões de junção possíveis e também funcionam se houver
múltiplas colunas sobrepostas.
Para obter mais informações sobre esses padrões, consulte “Agregação e agrupamento” na página
158, onde nos aprofundamos um pouco mais na álgebra relacional. Consulte também a seção “Mesclar, unir e
Seção Concatenate” da documentação do Pandas para uma discussão mais aprofundada sobre estes
tópicos.
As operações de mesclagem e junção surgem com mais frequência quando se combina dados de diferentes
fontes diferentes. Aqui consideraremos um exemplo de alguns dados sobre estados dos EUA e
suas populações. Os arquivos de dados podem ser encontrados em http://github.com/jakevdp/data-USstates/:
Em[19]:
# A seguir estão os comandos shell para baixar os dados
Vamos dar uma olhada nos três conjuntos de dados, usando a função read_csv() do Pandas:
pop.head() áreas.head()
estado/região idades ano população AL área do estado (m²)
0 menores de 18 2012 1117489,0 total 0 Alabama 52423
1 de 2012 4817528,0 AL AL menores 1 Alasca 656425
2 18 2010 1130966,0 total 2010 4785570,0 Arizona 114006
3 AL 2 3 Arkansas 53182
4 AL sub18 2011 1125763,0 3Arkansas 53182
4 Califórnia 163707
abrevs.head()
abreviatura do estado
Alabama AL
01 Alasca E
2 Arizona O
3 Arcansas COM
4 Califórnia QUE
Dadas essas informações, digamos que queremos calcular um resultado relativamente simples:
classifique os estados e territórios dos EUA pela densidade populacional de 2010. Temos claramente o
dados aqui para encontrar esse resultado, mas teremos que combinar os conjuntos de dados para obtê-lo.
Começaremos com uma mesclagem muitos-para-um que nos dará o nome completo do estado dentro do
DataFrame populacional . Queremos mesclar com base na coluna estado/região do pop,
e a coluna de abreviações de abreviaturas. Usaremos how='outer' para garantir que não
os dados são jogados fora devido a rótulos incompatíveis.
Em[22]: mesclado.isnull().any()
estado Verdadeiro
tipo d: bool
Em[23]: mesclado[mesclado['população'].isnull()].head()
Parece que todos os valores nulos da população são de Porto Rico antes do ano
2000; isso provavelmente se deve ao fato de esses dados não estarem disponíveis na fonte original.
Mais importante ainda, vemos também que algumas das novas entradas de estado também são nulas, o que
significa que não houve entrada correspondente na chave abreviada ! Vamos descobrir
quais regiões não possuem esta correspondência:
Podemos inferir rapidamente o problema: nossos dados populacionais incluem entradas para Porto Rico
(PR) e os Estados Unidos como um todo (EUA), embora essas entradas não apareçam no
chave de abreviação de estado. Podemos corrigir isso rapidamente preenchendo as entradas apropriadas:
estado Falso
tipo d: bool
Chega de nulos na coluna de estado : está tudo pronto!
Agora podemos mesclar o resultado com os dados da área usando um procedimento semelhante. Examinando
nossos resultados, desejaremos juntar a coluna de estado em ambos:
Em[27]: final.isnull().any()
estado Falso
área (m²) dtype: Verdadeiro
bool
Existem valores nulos na coluna de área ; podemos dar uma olhada para ver quais regiões foram
ignorado aqui:
Vemos que nossas áreas DataFrame não contém a área dos Estados Unidos como um
todo. Poderíamos inserir o valor apropriado (usando a soma de todas as áreas do estado, por
exemplo), mas neste caso vamos apenas descartar os valores nulos porque a população denÿ
cidade de todos os Estados Unidos não é relevante para a nossa discussão atual:
Em[29]: final.dropna(inplace=True)
final.head()
Agora temos todos os dados de que precisamos. Para responder à questão de interesse, vamos primeiro selecionar
a parte dos dados correspondente ao ano 2000 e a população total.
Usaremos a função query() para fazer isso rapidamente (isso requer o pacote numexpr
Ser instalado; consulte “Pandas de alto desempenho: eval() e query()” na página 208):
Fora[30]: estado/região idade ano população AL total 2010 área do estado (m²)
3 4785570,0 AK total 2010 Alabama 52423.0
91 713868,0 Alasca 656425,0
Fora[32]: estado
Distrito da Colombia 8898.897059
Porto Rico 1058.665149
Nova Jersey 1009.253268
Ilha de Rodes 681.339159
Tipo de 645.600649
Connecticut: float64
O resultado é uma classificação dos estados dos EUA mais Washington, DC e Porto Rico em ordem de
sua densidade populacional em 2010, em residentes por milha quadrada. Podemos ver que de longe o
a região mais densa neste conjunto de dados é Washington, DC (ou seja, o Distrito de Columbia);
entre os estados, o mais denso é Nova Jersey.
Em[33]: densidade.tail()
Fora[33]: estado
Dakota do Sul 10.583512
Dakota do Norte 9.537565
Montana 6.736171
Wyoming 5.768079
Tipo do 1.087509
Alasca: float64
Vemos que o estado menos denso, de longe, é o Alasca, com uma média ligeiramente superior a um residente
por milha quadrada.
Esse tipo de mesclagem confusa de dados é uma tarefa comum quando alguém está tentando responder
perguntas usando fontes de dados do mundo real. Espero que este exemplo tenha lhe dado uma
ideia das maneiras pelas quais você pode combinar as ferramentas que abordamos para obter insights de
seus dados!
Agregação e Agrupamento
Uma parte essencial da análise de grandes volumes de dados é a sumarização eficiente: a computação
agregações como sum(), média(), mediana(), min() e max(), nas quais um único número
ber fornece informações sobre a natureza de um conjunto de dados potencialmente grande. Nesta seção, vamos
Aqui usaremos o conjunto de dados Planets, disponível através do pacote Seaborn (ver “Visualiÿ
zação com Seaborn” na página 311). Fornece informações sobre planetas que os astrônomos
descobrimos em torno de outras estrelas (conhecidas como planetas extrasolares ou exoplanetas para
curto). Ele pode ser baixado com um simples comando Seaborn:
Fora[2]: (1035, 6)
Em[3]: planetas.head()
Isto contém alguns detalhes sobre os mais de 1.000 exoplanetas descobertos até 2014.
Anteriormente, exploramos algumas das agregações de dados disponíveis para arrays NumPy
(“Agregações: mínimo, máximo e tudo o mais” na página 58). Tal como acontece com uma matriz NumPy
unidimensional, para uma série Pandas os agregados retornam um único valor:
Fora[4]: 0 1 0,374540
0,950714
2 0,731994
3 0,598658
4 0,156019
tipo d: float64
Em[5]: ser.sum()
Saída[5]: 2.8119254917081569
Em[6]: ser.mean()
Fora[6]: 0,56238509834163142
Para um DataFrame, por padrão os agregados retornam resultados dentro de cada coluna:
Fora[7]: AB
0 0,155995 0,020584
1 0,058084 0,969910
2 0,866176 0,832443
3 0,601115 0,212339
4 0,708073 0,181825
Em[8]: df.mean()
Fora[8]: A 0,477888
B 0,443420
tipo d: float64
Em[9]: df.mean(axis='colunas')
Fora[9]: 0 1 0,088290
0,513997
2 0,849309
3 0,406727
4 0,444949
tipo d: float64
Em [10]: planets.dropna().describe()
Essa pode ser uma maneira útil de começar a compreender as propriedades gerais de um conjunto de dados.
Por exemplo, vemos na coluna do ano que embora os exoplanetas tenham sido descobertos como
já em 1989, metade de todos os exoplanetas conhecidos não foram descobertos até 2010 ou depois.
Isto se deve em grande parte à missão Kepler, que é um telescópio espacial especificamente
Projetado especificamente para encontrar planetas eclipsantes em torno de outras estrelas.
Agregação Descrição
contar() Número total de itens
Para aprofundar os dados, no entanto, agregados simples muitas vezes não são suficientes. O
O próximo nível de resumo de dados é a operação groupby , que permite
calcule agregados de forma rápida e eficiente em subconjuntos de dados.
• A etapa de aplicação envolve calcular alguma função, geralmente uma agregada, transformada
informação ou filtragem dentro dos grupos individuais.
Embora certamente possamos fazer isso manualmente usando alguma combinação dos comandos
de mascaramento, agregação e mesclagem abordados anteriormente, é importante perceber que
as divisões intermediárias não precisam ser explicitamente instanciadas. Em vez disso, o GroupBy
pode (frequentemente) fazer isso em uma única passagem pelos dados, atualizando a soma, a
média, a contagem, o mínimo ou outro agregado para cada grupo ao longo do caminho. O poder
do GroupBy é que ele abstrai essas etapas: o usuário não precisa pensar em como o cálculo é
feito nos bastidores, mas sim na operação como um todo.
Como exemplo concreto, vamos dar uma olhada no uso do Pandas para o cálculo mostrado na
Figura 3-1. Começaremos criando o DataFrame de entrada:
Em [11]: df = pd.DataFrame({'chave': ['A', 'B', 'C', 'A', 'B', 'C'], 'dados': intervalo (6) },
colunas=['chave', 'dados'])
df
Em[12]: df.groupby('chave')
Observe que o que é retornado não é um conjunto de DataFrames, mas um objeto DataFrameGroupBy .
Este objeto é onde está a mágica: você pode pensar nele como uma visão especial do DataFrame, que
está preparada para se aprofundar nos grupos, mas não faz nenhum cálculo real até que a agregação seja
aplicada. Esta abordagem de “avaliação preguiçosa” significa que os agregados comuns podem ser
implementados de forma muito eficiente e de uma forma quase transparente para o utilizador.
Para produzir um resultado, podemos aplicar uma agregação a este objeto DataFrameGroupBy , que
executará as etapas de aplicação/combinação apropriadas para produzir o resultado desejado:
Em[13]: df.groupby('key').sum()
Fora[13]: dados
chave
A 3
B 5
C 7
O método sum() é apenas uma possibilidade aqui; você pode aplicar praticamente qualquer função de
agregação comum do Pandas ou NumPy, bem como praticamente qualquer operação válida do DataFrame ,
como veremos na discussão a seguir.
O objeto GroupBy O
objeto GroupBy é uma abstração muito flexível. De muitas maneiras, você pode simplesmente tratá-lo como
se fosse uma coleção de DataFrames, e ele faz as coisas difíceis nos bastidores.
Vejamos alguns exemplos usando os dados dos Planetas.
Talvez as operações mais importantes disponibilizadas por um GroupBy sejam agregar, filtrar, transformar
e aplicar. Discutiremos cada um deles mais detalhadamente em “Agregar, filtrar, transformar, aplicar” na
página 165, mas antes disso vamos apresentar algumas das outras funcionalidades que podem ser usadas
com a operação GroupBy básica.
Indexação de colunas. O objeto GroupBy suporta a indexação de colunas da mesma forma que o
DataFrame e retorna um objeto GroupBy modificado. Por exemplo:
Em[14]: planets.groupby('método')
Em[15]: planets.groupby('método')['orbital_period']
Aqui selecionamos um grupo de séries específico do grupo DataFrame original por referência ao nome da
coluna. Tal como acontece com o objeto GroupBy , nenhum cálculo é feito até que chamemos alguma
agregação no objeto:
Em [16]: planets.groupby('method')['orbital_period'].median()
Fora[16]: método
Astrometria 631.180000
Variações de tempo do Eclipse 4343.500000
Imagem 27500.000000
Microlente 3300.000000
Modulação de brilho orbital 0,342887
Tempo Pulsar 66.541900
Variações de tempo de pulsação 1170.000000
Velocidade Radial 360.200000
Transito 5.714932
Variações de tempo de trânsito 57.011000
Nome: período_orbital, tipo d: float64
Isto dá uma ideia da escala geral dos períodos orbitais (em dias) que cada método é
sensível a.
Iteração sobre grupos. O objeto GroupBy suporta iteração direta sobre os grupos,
retornando cada grupo como uma Série ou DataFrame:
Astrometria forma=(2, 6)
Forma de variações de tempo do Eclipse = (9, 6)
Formato da imagem=(38, 6)
Formato da microlente = (23, 6)
Forma de modulação de brilho orbital = (3, 6)
Tempo Pulsar forma=(5, 6)
Forma de variações de tempo de pulsação = (1, 6)
Forma de velocidade radial = (553, 6)
Forma de trânsito=(397, 6)
Forma de variações de tempo de trânsito = (4, 6)
Isto pode ser útil para fazer certas coisas manualmente, embora muitas vezes seja muito mais rápido
use a funcionalidade de aplicação integrada , que discutiremos em breve.
Métodos de envio. Através de alguma mágica de classe Python, qualquer método não explicitamente
implementado pelo objeto GroupBy será passado e chamado nos grupos,
sejam eles objetos DataFrame ou Series . Por exemplo, você pode usar o método description() de
DataFrames para realizar um conjunto de agregações que descrevem cada
grupo nos dados:
Em [18]: planets.groupby('método')['ano'].describe().unstack()
Fora[18]:
contar significar padrão min 25% \\
método
Astrometria 2,0 2011,500000 2,121320 2010,0 2010,75
Variações de tempo do Eclipse 9,0 2010,000000 1,414214 2008,0 2009,00
Imagem 38,0 2009,131579 2,781901 2004,0 2008,00
Microlente 23,0 2.009,782609 2,859697 2.004,0 2.008,00
Modulação de brilho orbital 3,0 2011,666667 1,154701 2011,0 2011,00
Olhar para esta tabela ajuda-nos a compreender melhor os dados: por exemplo, o vasto
a maioria dos planetas foi descoberta pelos métodos de velocidade radial e trânsito,
embora este último só tenha se tornado comum (devido a telescópios novos e mais precisos) no
última década. Os métodos mais recentes parecem ser Variação de Tempo de Trânsito e Orbital
Modulação de brilho, que não foi usada para descobrir um novo planeta até 2011.
Este é apenas um exemplo da utilidade dos métodos de despacho. Observe que eles são
aplicado a cada grupo individual e os resultados são então combinados em GroupBy
e voltou. Novamente, qualquer método DataFrame/Series válido pode ser usado no
objeto GroupBy correspondente , o que permite alguns recursos muito flexíveis e poderosos
operações!
3A 3 3
4B 4 7
5ºC 5 9
Agregação. Agora estamos familiarizados com agregações GroupBy com sum(), median(),
e similares, mas o método agregado() permite ainda mais flexibilidade. Pode levar
uma string, uma função ou uma lista delas e calcular todas as agregações de uma vez. Aqui está um
exemplo rápido combinando tudo isso:
Outro padrão útil é passar nomes de colunas de mapeamento de dicionário para operações
a ser aplicado nessa coluna:
Filtragem. Uma operação de filtragem permite descartar dados com base no grupo apropriado.
laços. Por exemplo, podemos querer manter todos os grupos nos quais o desvio padrão é
maior que algum valor crítico:
Em[22]:
def filtro_func(x):
retornar x['dados2'].std() > 4
imprimir(df); imprimir(df.groupby('chave').std());
imprimir(df.groupby('chave').filter(filter_func))
df df.groupby('chave').std()
dados principais1 dados2 dados principais1 dados2
0A 0 5 A 2.12132 1.414214
1 B 1 0 B 2.12132 4.949747
2 C 2 3 C 2.12132 4.242641
3A 3 3
4 B 4 7
5 C 5 9
df.groupby('chave').filter(filter_func)
dados principais1 dados2
1 B 1 0
2 C 2 3
4 B 4 7
5 C 5 9
Transformação. Embora a agregação deva retornar uma versão reduzida dos dados, a trans-
a formação pode retornar alguma versão transformada dos dados completos para recombinar. Para
tal transformação, a saída terá o mesmo formato da entrada. Um exemplo comum
é centralizar os dados subtraindo a média do grupo:
Por exemplo, aqui está um apply() que normaliza a primeira coluna pela soma dos
segundo:
imprimir(df); imprimir(df.groupby('chave').apply(norm_by_data2))
df.groupby('chave').apply(norm_by_data2)
dados chave df1 dados2 5 chave dados1 dados2
0A 0 0 A 0,000000 5
1 B 1 0 1 B0,142857 0
2 C 2 3 2C 0,166667 3A 3
3A 3 3 0,375000 3
4 B 4 7 4 B0,571429 7
5 C 5 9 5 C 0,416667 9
apply() dentro de um GroupBy é bastante flexível: o único critério é que a função receba
um DataFrame e retorna um objeto ou escalar Pandas; o que você faz no meio depende
você!
Nos exemplos simples apresentados anteriormente, dividimos o DataFrame em uma única coluna
nome. Esta é apenas uma das muitas opções pelas quais os grupos podem ser definidos, e iremos
veja algumas outras opções para especificação de grupo aqui.
Uma lista, matriz, série ou índice que fornece as chaves de agrupamento. A chave pode ser qualquer série ou lista
com um comprimento correspondente ao do DataFrame. Por exemplo:
Em[25]: L = [0, 1, 0, 1, 2, 0]
imprimir(df); imprimir(df.groupby(L).sum())
df.groupby(L).sum()
dados chave df1 dados2 5 dados1 dados2
0A 0 0 7 17
1 1 0 1 4 3
B2 C 2 3 2 4 7
3A 3 3
4 B 4 7
5 C 5 9
df.groupby(df['chave']).sum()
dados chave df1 dados2 dados1 dados2
0A05 A 3 8
1 B 1 0 B 5 7
2 C 2 3 C 7 12
3A4 3
B 34 7
5 C 5 9
Um dicionário ou índice de mapeamento de série para agrupar. Outro método é fornecer um dicionário
que mapeia valores de índice para as chaves do grupo:
df2.groupby(mapping).sum()
dados chave df21 dados2 dados1 dados2
Um 5 0 Consoante 12 19
B 1 0 vogal 3 8
C 2 3
A 3
B 34 7
C 5 9
Qualquer função Python. Semelhante ao mapeamento, você pode passar qualquer função Python que
insira o valor do índice e produza o grupo:
df2.groupby(str.inferior).mean()
dados chave df21 dados2 dados1 dados2
A 0 5 a 1,5 4,0
B 1 0 b 2,5 3.5
C 2 3 c 3,5 6,0
A 3 3
B 4 7
C 5 9
Uma lista de chaves válidas. Além disso, qualquer uma das opções principais anteriores pode ser combinada para
grupo em um multi-índice:
Exemplo de agrupamento
Como exemplo disso, em algumas linhas de código Python podemos juntar tudo isso
e conte os planetas descobertos por método e por década:
Out[30]: método da Década de 1980 Década de 1990 Década de 2000 Década de 2010
década
Astrometria 0,0 0,0 0,0 2,0
Variações de tempo do Eclipse 0,0 0,0 5,0 10,0
Imagem 0,0 0,0 29,0 21,0
Microlente 0,0 0,0 12,0 15,0
Modulação de brilho orbital 0,0 0,0 0,0 5,0
Tempo Pulsar 0,0 9,0 1,0 1,0
Variações de tempo de pulsação 0,0 0,0 1,0 0,0
Velocidade Radial 1,0 52,0 475,0 424,0
Transito 0,0 0,0 64,0 712,0
Variações de tempo de trânsito 0,0 0,0 0,0 9,0
Isso mostra o poder de combinar muitas das operações que discutimos até agora
ponto quando se olha para conjuntos de dados realistas. Nós imediatamente ganhamos uma compreensão grosseira
de quando e como os planetas foram descobertos nas últimas décadas!
Tabelas dinâmicas
Vimos como a abstração GroupBy nos permite explorar relacionamentos dentro de um conjunto de dados.
definir. Uma tabela dinâmica é uma operação semelhante comumente vista em planilhas e
outros programas que operam em dados tabulares. A tabela dinâmica recebe dados simples de colunas
como entrada e agrupa as entradas em uma tabela bidimensional que fornece
um resumo multidimensional dos dados. A diferença entre tabelas dinâmicas
e GroupBy às vezes podem causar confusão; isso me ajuda a pensar em tabelas dinâmicas como
essencialmente uma versão multidimensional da agregação GroupBy . Ou seja, você divide-aplica-combina,
mas tanto a divisão quanto a combinação acontecem não em um índice unidimensional, mas em uma
grade bidimensional.
Em[2]: titanic.head()
Fora[2]:
sobreviveu à classe 0 3 sexo idade sibsp parch masculino tarifa embarcada classe \\
0 22,0 1 0 7,2500 S Terceiro
1 1 1 mulher 38,0 3 1 0 71.2833 0 C Primeiro
2 1 mulheres 26,0 1 mulher 0 7.9250 S Terceiro
3 1 35,0 homens 35,0 1 0 53.1000 S primeiro
4 0 3 0 0 8.0500 S Terceiro
Contém uma riqueza de informações sobre cada passageiro daquela viagem malfadada,
incluindo sexo, idade, classe, tarifa paga e muito mais.
Para começar a aprender mais sobre esses dados, podemos começar agrupando-os de acordo com
gênero, status de sobrevivência ou alguma combinação destes. Se você leu o anterior
seção, você pode ficar tentado a aplicar uma operação GroupBy - por exemplo, vejamos
na taxa de sobrevivência por sexo:
Em[3]: titanic.groupby('sexo')[['sobreviveu']].mean()
Fora[3]: sobreviveu
sexo
feminino 0,742038
masculino 0,188908
Isto dá-nos imediatamente uma ideia: no geral, três em cada quatro mulheres a bordo
sobreviveu, enquanto apenas um em cada cinco homens sobreviveu!
Isto é útil, mas gostaríamos de ir um passo mais fundo e analisar a sobrevivência de ambos os sexos.
e, digamos, classe. Usando o vocabulário de GroupBy, poderíamos continuar usando algo
assim: agrupamos por classe e gênero, selecionamos a sobrevivência, aplicamos um agregado médio, comÿ
combine os grupos resultantes e, em seguida, desempilha o índice hierárquico para revelar o oculto
multidimensionalidade. Em código:
Isto dá-nos uma ideia melhor de como tanto o género como a classe afectaram a sobrevivência, mas a
o código está começando a parecer um pouco distorcido. Embora cada etapa desse pipeline faça sentido
luz das ferramentas que discutimos anteriormente, a longa sequência de código não é particularmente
fácil de ler ou usar. Este GroupBy bidimensional é comum o suficiente para que o Pandas
inclui uma rotina de conveniência, pivot_table, que lida sucintamente com esse tipo de
agregação multidimensional.
gradiente favorece tanto as mulheres quanto as classes mais altas. Mulheres de primeira classe sobreviveram com quase
certeza (oi, Rose!), enquanto apenas um em cada dez homens de terceira classe sobreviveu (desculpe, Jack!).
Assim como no GroupBy, o agrupamento em tabelas dinâmicas pode ser especificado com vários níveis.
els, e através de uma série de opções. Por exemplo, podemos estar interessados em analisar
a idade como terceira dimensão. Iremos descartar a idade usando a função pd.cut :
Podemos aplicar essa mesma estratégia também ao trabalhar com colunas; vamos adicionar informações
na tarifa paga usando pd.qcut para calcular quantis automaticamente:
Fora[7]:
classe [0, 14,454]
tarifária Primeiro Segundo Terceiro \\
sexo idade
Já vimos exemplos dos três primeiros argumentos; aqui vamos dar uma olhada rápida
nos restantes. Duas das opções, fill_value e dropna, têm a ver com
faltam dados e são bastante simples; não mostraremos exemplos deles aqui.
A palavra-chave aggfunc controla que tipo de agregação é aplicada, o que é uma média
por padrão. Como no GroupBy, a especificação de agregação pode ser uma string representando
uma das várias escolhas comuns ('soma', 'média', 'contagem', 'min', 'máx', etc.) ou um
função que implementa uma agregação (np.sum(), min(), sum(), etc.). Adicionalmente,
pode ser especificado como um dicionário mapeando uma coluna para qualquer um dos itens desejados acima
opções:
Observe também aqui que omitimos a palavra-chave values ; quando você especifica um
mapeamento para aggfunc, isso é determinado automaticamente.
Às vezes é útil calcular totais ao longo de cada agrupamento. Isto pode ser feito através do
palavra-chave de margens :
sexo
feminino 0,968085 0,921053 0,500000 0,742038
macho 0,368852 0,157407 0,135447 0,188908
Todos 0,629630 0,472826 0,242363 0,383838
Aqui, isso nos dá automaticamente informações sobre a taxa de sobrevivência independente de classe por
gênero, a taxa de sobrevivência independente de gênero por classe e a taxa de sobrevivência geral de 38%.
O rótulo da margem pode ser especificado com a palavra-chave margins_name , cujo padrão é
"Todos".
Como exemplo mais interessante, vamos dar uma olhada nos dados disponíveis gratuitamente sobre nascimentos em
nos Estados Unidos, fornecido pelos Centros de Controle de Doenças (CDC). Esses dados podem
ser encontrado em https://raw.githubusercontent.com/jakevdp/data-CDCbirths/master/
births.csv (este conjunto de dados foi analisado extensivamente por Andrew Gelman e
seu grupo; veja, por exemplo, esta postagem do blog):
Em[10]:
# comando shell para baixar os dados:
# !curl -O https:// raw.githubusercontent.com/ jakevdp/ data-CDCbirths/
# mestre/ nascimentos.csv
Dando uma olhada nos dados, vemos que é relativamente simples – contém o número de
nascimentos agrupados por data e sexo:
Em[12]: nascimentos.head()
Podemos começar a entender um pouco mais esses dados usando uma tabela dinâmica. Vamos adicionar uma decÿ
coluna ade, e dê uma olhada nos nascimentos masculinos e femininos em função da década:
Em[13]:
nascimentos['década'] = 10 * (nascimentos['ano'] // 10)
nascimentos.pivot_table('nascimentos', index='década', colunas='gênero', aggfunc='soma')
Out[13]: década de F M
gênero
1960 1753634 1846572
1970 16263075 17121550
1980 18310351 19243452
1990 19479454 20420553
2000 18229309 19106428
Vemos imediatamente que os nascimentos masculinos superam os nascimentos femininos em cada década. Ver
essa tendência um pouco mais claramente, podemos usar as ferramentas de plotagem integradas no Pandas para visualizar
ize o número total de nascimentos por ano (Figura 3-2; consulte o Capítulo 4 para uma discussão sobre
plotando com Matplotlib):
Em[14]:
% matplotlib embutido
importar matplotlib.pyplot como plt
sns.set() # usa estilos Seaborn
nascimentos.pivot_table('nascimentos', index='ano', colunas='gênero', aggfunc='sum').plot()
plt.ylabel('total de nascimentos por ano');
Figura 3-2. Número total de nascimentos nos EUA por ano e sexo
Com uma tabela dinâmica simples e o método plot() , podemos ver imediatamente a tendência anual de nascimentos
por sexo. A olho nu, parece que nos últimos 50 anos os nascimentos masculinos superaram os nascimentos
femininos em cerca de 5%.
Exploração adicional de
dados Embora isso não esteja necessariamente relacionado à tabela dinâmica, há mais alguns recursos
interessantes que podemos extrair desse conjunto de dados usando as ferramentas Pandas abordadas até este
ponto. Devemos começar limpando um pouco os dados, removendo valores discrepantes causados por datas
digitadas incorretamente (por exemplo, 31 de junho) ou valores ausentes (por exemplo, 99 de junho). Uma maneira
fácil de remover tudo isso de uma vez é cortar valores discrepantes; faremos isso por meio de uma operação robusta de recorte sigma:1
Esta linha final é uma estimativa robusta da média amostral, onde 0,74 vem do intervalo interquartil de uma
distribuição gaussiana. Com isso podemos usar o método query() (discutido mais detalhadamente em “Pandas de
alto desempenho: eval() e query()” na página 208) para filtrar linhas com nascimentos fora desses valores:
In[16]:
nascimentos = nascimentos.query('(nascimentos > @mu - 5 * @sig) & (nascimentos < @mu + 5 * @sig)')
1 Você pode aprender mais sobre operações de recorte sigma em um livro de minha autoria com Željko Iveziÿ, Andrew J.
Connolly e Alexander Gray: Estatística, mineração de dados e aprendizado de máquina em astronomia: um guia
prático de Python para a análise de dados de pesquisa (Princeton University Press, 2014).
Em seguida, definimos a coluna do dia como números inteiros; anteriormente era uma string porque algumas
colunas do conjunto de dados continham o valor 'nulo':
In[17]: # define a coluna 'dia' como inteiro; originalmente era uma string devido a nulos nascimentos['dia'] =
nascimentos['dia'].astype(int)
Finalmente, podemos combinar o dia, o mês e o ano para criar um índice de data (consulte “Trabalhando com
séries temporais” na página 188). Isso nos permite calcular rapidamente o dia da semana correspondente a
cada linha:
In[18]: # cria um índice de data e hora a partir do ano, mês, dia births.index =
pd.to_datetime(10000 * births.year +
100 * nascimentos.mês +
nascimentos.dia, formato='%Y%m%d')
nascimentos['diadasemana'] = nascimentos.index.diadasemana
Usando isso, podemos representar graficamente os nascimentos por dia da semana durante várias décadas (Figura 3-3):
Em [19]:
importar matplotlib.pyplot como plt
importar matplotlib como mpl
nascimentos.pivot_table('nascimentos', index='diadasemana',
colunas='década', aggfunc='média').plot()
plt.gca().set_xticklabels(['Seg', 'Terça', 'Quarta', 'Quinta', 'Sexta', 'Sábado', ' Sun']) plt.ylabel('média de nascimentos
por dia');
Aparentemente, os nascimentos são um pouco menos comuns nos fins de semana do que durante a semana!
Observe que as décadas de 1990 e 2000 estão faltando porque os dados do CDC contêm apenas o mês de
nascimento a partir de 1989.
Outra visão interessante é traçar o número médio de nascimentos por dia do ano.
Vamos primeiro agrupar os dados por mês e dia separadamente:
Em[20]:
nascimentos_por_data = nascimentos.pivot_table('nascimentos',
[nascimentos.índice.mês, nascimentos.índice.dia])
nascimentos_por_data.head()
Fora[20]: 1 1 2 4009.225
4247.400
3 4500.900
4 4571.350
5 4603.625
Nome: nascimentos, dtype: float64
O resultado é um índice múltiplo ao longo de meses e dias. Para tornar isso facilmente plotável, vamos
transforme esses meses e dias em uma data, associando-os a um ano fictício variável
possível (certificando-se de escolher um ano bissexto para que 29 de fevereiro seja tratado corretamente!)
Focando apenas no mês e no dia, temos agora uma série temporal que reflete a média
número de nascimentos por data do ano. A partir disso, podemos usar o método plot para plotar
os dados (Figura 3-4). Ele revela algumas tendências interessantes:
Em particular, a característica marcante deste gráfico é a queda na taxa de natalidade nos feriados dos
EUA (por exemplo, Dia da Independência, Dia do Trabalho, Ação de Graças, Natal, Dia de Ano Novo),
embora isto provavelmente reflita tendências em nascimentos programados/induzidos, em vez de algum
efeito psicossomático profundo. em partos naturais. Para mais discussões sobre essa tendência,
consulte a análise e os links na postagem do blog de Andrew Gelman sobre o assunto. Voltaremos a
esta figura em “Exemplo: Efeito dos feriados nos nascimentos nos EUA” na página 269, onde usaremos
as ferramentas do Matplotlib para anotar este gráfico.
Observando este breve exemplo, você pode ver que muitas das ferramentas Python e Pandas que
vimos até agora podem ser combinadas e usadas para obter insights de uma variedade de conjuntos de
dados. Veremos algumas aplicações mais sofisticadas dessas manipulações de dados em seções
futuras!
Vimos nas seções anteriores como ferramentas como NumPy e Pandas generalizam operações
aritméticas para que possamos executar a mesma operação de maneira fácil e rápida em muitos
elementos do array. Por exemplo:
Essa vetorização de operações simplifica a sintaxe de operação em arrays de dados: não precisamos
mais nos preocupar com o tamanho ou formato do array, mas apenas com qual operação queremos
realizar. Para matrizes de strings, o NumPy não fornece um acesso tão simples e, portanto, você fica
preso ao usar uma sintaxe de loop mais detalhada:
Talvez isso seja suficiente para trabalhar com alguns dados, mas será interrompido se houver algum
valor ausente. Por exemplo:
-------------------------------------------------- -------------------------
-------------------------------------------------- -------------------------
<ipython-input-3-fc1d891ab539> em <módulo>()
1 dados = ['pedro', 'Paulo', Nenhum, 'MARIA', 'gUIDO']
----> 2 [s.capitalize() para s em dados]
<ipython-input-3-fc1d891ab539> em <listcomp>(.0)
1 dados = ['pedro', 'Paulo', Nenhum, 'MARIA', 'gUIDO']
----> 2 [s.capitalize() para s em dados]
O Pandas inclui recursos para atender a essa necessidade de operações de strings vetorizadas
e para lidar corretamente com dados ausentes por meio do atributo str da série Pandas e
Indexar objetos contendo strings. Então, por exemplo, suponha que criemos uma série Pandas
com estes dados:
Fora[4]: 0 1 Peter
Paulo
2 Nenhum
3 MARY
4 GUIDO
dtype: objeto
Agora podemos chamar um único método que colocará todas as entradas em maiúscula, ignorando
sobre quaisquer valores ausentes:
Em[5]: nomes.str.capitalize()
Fora[5]: 0 1 Peter
Paulo
2 Nenhum
3 Mary
4 Guido
dtype: objeto
Usar o preenchimento de tabulação neste atributo str listará todos os métodos de string vetorizados
disponível para Pandas.
Se você tem um bom entendimento da manipulação de strings em Python, a maioria dos aplicativos do Pandas
A sintaxe da string é intuitiva o suficiente para que seja suficiente apenas listar uma tabela de disponibilidade
métodos capazes; começaremos com isso aqui, antes de nos aprofundarmos em alguns dos sub-
tleties. Os exemplos nesta seção usam a seguinte série de nomes:
Quase todos os métodos de string integrados do Python são espelhados por uma string vetorizada do Pandas
método. Aqui está uma lista de métodos str do Pandas que espelham os métodos de string do Python:
Observe que eles têm vários valores de retorno. Alguns, como lower(), retornam uma série de
cordas:
Em[7]: monte.str.lower()
Em[8]: monte.str.len()
Fora[8]: 0 1 14
11
2 13
3 9
4 11
5 13
tipo d: int64
Ou valores booleanos:
Em[9]: monte.str.startswith('T')
Fora[9]: 0 1 Falso
Falso
2 Verdadeiro
3 Falso
4 Verdadeiro
5 Falso
tipo d: bool
Outros ainda retornam listas ou outros valores compostos para cada elemento:
Em[10]: monte.str.split()
Veremos mais manipulações desse tipo de objeto de série de listas à medida que continuarmos
nossa discussão.
Além disso, existem vários métodos que aceitam expressões regulares para examinar o
conteúdo de cada elemento de string e siga algumas das convenções da API do Python
módulo re integrado (consulte a Tabela 3-4).
extract() Chame re.match() em cada elemento, retornando grupos correspondentes como strings.
Com eles, você pode realizar uma ampla gama de operações interessantes. Por exemplo, podemos
extraia o primeiro nome de cada um, solicitando um grupo contíguo de caracteres no
início de cada elemento:
Em[11]: monte.str.extract('([A-Za-z]+)')
Fora[11]: 0 Graham
1 John
2 Terry
3 Érico
4 Terry
5 Michael
dtype: objeto
Ou podemos fazer algo mais complicado, como encontrar todos os nomes que começam e terminam
com uma consoante, fazendo uso do início de string (^) e final de string ($) regular
caracteres de expressão:
Em[12]: monte.str.findall(r'^[^AEIOU].*[^aeiou]$')
Métodos diversos
Finalmente, existem alguns métodos diversos que permitem outras operações convenientes.
(ver Tabela 3-5).
Método Descrição
Divida strings longas em linhas com comprimento menor que uma determinada largura
enrolar()
Fora[13]: 0 Jogo
1 João
2 Ter
Diferente
34 Ter
5 Microfone
dtype: objeto
Esses métodos get() e slice() também permitem acessar elementos de arrays retornados por
dividir(). Por exemplo, para extrair o sobrenome de cada entrada, podemos combinar
dividir() e obter():
Em[14]: monte.str.split().str.get(-1)
Fora[14]: 0 1 Chapman
2 Cleese
Gilliam
3 Parado
4 Jones
5 Palin
dtype: objeto
Em[15]:
full_monte = pd.DataFrame({'nome': monte,
'informações': ['B|C|D', 'B|D', 'A|C', 'B|D', 'B|C',
'B|C|D']})
full_monte
Em[16]: full_monte['info'].str.get_dummies('|')
Fora[16]: ABCD 0 0 1
11
10101
21010
30101
401105011
1
Com essas operações como blocos de construção, você pode construir uma gama infinita de
procedimentos de processamento de strings ao limpar seus dados.
Não nos aprofundaremos nesses métodos aqui, mas recomendo que você leia “Trabalhando com
dados de texto” na documentação on-line do pandas ou consulte os recursos listados em “Recursos
adicionais” na página 215.
receitas Essas operações de strings vetorizadas tornam-se mais úteis no processo de limpeza de
dados confusos do mundo real. Aqui vou dar um exemplo disso, usando um banco de dados de
receitas aberto compilado de várias fontes na Web. Nosso objetivo será analisar os dados da receita
em listas de ingredientes, para que possamos encontrar rapidamente uma receita com base em alguns
ingredientes que temos em mãos.
Na primavera de 2016, esse banco de dados tinha cerca de 30 MB e pode ser baixado e descompactado
com estes comandos:
O banco de dados está no formato JSON, então tentaremos pd.read_json para lê-lo:
Em[18]: tente:
receitas = pd.read_json('recipeitems-latest.json') exceto
ValueError como e:
print("Erro de valor:", e)
Ops! Recebemos um ValueError mencionando que há “dados finais”. Procurando esse erro na Internet,
parece que é devido ao uso de um arquivo em que cada linha é um JSON válido, mas o arquivo
completo não é. Vamos verificar se esta interpretação é verdadeira:
Em[21]: receitas.shape
Vemos que existem quase 200.000 receitas e 17 colunas. Vamos dar uma olhada em uma linha
para ver o que temos:
Em[22]: receitas.iloc[0]
Fora[22]:
_id {'$oid': '5160756b96cc62079cc2db15'}
cookTime PT30M
data do NaN
criadorModificado NaN
dataPublicação 11/03/2013
descrição No final da tarde de sábado, depois do Marlboro Man ha...
imagem http://static.thepioneerwoman.com/cooking/file...
ingredientes Biscoitos\n3 xícaras de farinha de trigo\n2 colheres de sopa...
nome Deixe biscoitos e molho de salsicha
PT10M
preparaçãoReceita de NaN
tempoReceita de NaN
categoriaReceita de instruçõesRendimento 12
fonte a mulher pioneira
tempo total NaN
{'$data': 1365276011104}
URL http://thepioneerwoman.com/cooking/2013/03/dro...
Nome: 0, dtype: objeto
Há muita informação lá, mas muitas delas estão de uma forma muito confusa, como é típico
de dados extraídos da Web. Em particular, a lista de ingredientes está em formato de string;
teremos que extrair cuidadosamente as informações que nos interessam.
olhando mais de perto os ingredientes:
Em[23]: receitas.ingredientes.str.len().describe()
As listas de ingredientes têm em média 250 caracteres, com um mínimo de 0 e um máximo de quase 10.000
caracteres!
Só por curiosidade, vamos ver qual receita tem a lista de ingredientes mais longa:
Em[24]: receitas.nome[np.argmax(recipes.ingredients.str.len())]
Out[24]: 'Cenoura Abacaxi Spice & Bolo de Camada de Brownie com Chantilly & Cobertura de cream
cheese e cenoura de maçapão
Podemos fazer outras explorações agregadas; por exemplo, vamos ver quantas receitas são para o café da
manhã:
Em [33]: receitas.description.str.contains('[Bb]reakfast').sum()
Fora[33]: 3524
Em[34]: receitas.ingredientes.str.contains('[Cc]innamon').sum()
Fora[34]: 10526
Poderíamos até verificar se alguma receita escreve incorretamente o ingrediente como “cinamon”:
Em[27]: receitas.ingredientes.str.contains('[Cc]inamon').sum()
Fora[27]: 11
Este é o tipo de exploração de dados essencial que é possível com as ferramentas de string do Pandas.
É na coleta de dados como essa que o Python realmente se destaca.
Um simples recomendador de
receitas Vamos um pouco mais longe e comecemos a trabalhar em um sistema simples de recomendação
de receitas: dada uma lista de ingredientes, encontre uma receita que use todos esses ingredientes. Embora
conceitualmente simples, a tarefa é complicada pela heterogeneidade dos dados: não existe uma operação
fácil, por exemplo, para extrair uma lista limpa de ingredientes de cada linha.
Então vamos trapacear um pouco: começaremos com uma lista de ingredientes comuns, e simplesmente
pesquisaremos para ver se eles estão na lista de ingredientes de cada receita. Para simplificar, vamos ficar
com ervas e temperos por enquanto:
Podemos então construir um DataFrame booleano consistindo em valores True e False , indicando
saber se este ingrediente aparece na lista:
Em[29]:
importar re
tempero_df = pd.DataFrame(
dict((especiaria, receitas.ingredientes.str.contains(especiaria, re.IGNORECASE))
para especiarias em Spice_list))
tempero_df.head()
Fora[29]:
cominho orégano páprica salsa pimenta alecrim sálvia sal estragão tomilho
0 Falso Falso Falso Falso Falso Falso Verdadeiro Falso Falso Falso
1 Falso Falso Falso Falso Falso Falso Falso Falso Falso Falso
2 Verdadeiro Falso Falso Falso Verdadeiro Falso Falso Verdadeiro Falso Falso
3 Falso Falso Falso Falso Falso Falso Falso Falso Falso Falso
4 Falso Falso Falso Falso Falso Falso Falso Falso Falso Falso
Agora, por exemplo, digamos que gostaríamos de encontrar uma receita que use salsa, páprica e
estragão. Podemos calcular isso muito rapidamente usando o método query() de Data
Frames, discutidos em “Pandas de alto desempenho: eval() e query()” na página 208:
Fora[30]: 10
Encontramos apenas 10 receitas com esta combinação; vamos usar o índice retornado por isso
seleção para descobrir os nomes das receitas que possuem esta combinação:
Em[31]: receitas.nome[seleção.index]
Saída[31]: 2069 Todo cremado com Little Gem, dente de leão e wa...
74964 Lagosta com Manteiga Termidor
93768 Frango Frito do Sul de Burton com Molho Branco
113926 Carne desfiada de fogão lento Mijo
137686 Sopa de Espargos com Ovos Escalfados
140530 Ostra frita Po'boys
158475 Tagine de pernil de cordeiro com tabule de ervas
158486 Frango frito do sul com leitelho
163175 Sliders de frango frito com picles + salada de repolho
165243 Salada de Couve Flor Bar Tartine
Nome: nome, dtype: objeto
Agora que reduzimos nossa seleção de receitas por um fator de quase 20.000,
estamos em posição de tomar uma decisão mais informada sobre o que gostaríamos de cozinhar
para o jantar.
receitas Esperamos que este exemplo tenha lhe dado uma ideia (ba-dum!) dos tipos de
operações de limpeza de dados que são eficientemente habilitadas pelos métodos de string do
Pandas. É claro que construir um sistema de recomendação de receitas muito robusto exigiria
muito mais trabalho! Extrair listas completas de ingredientes de cada receita seria uma parte
importante da tarefa; infelizmente, a grande variedade de formatos utilizados torna este
processo relativamente demorado. Isso aponta para o truísmo de que, na ciência de dados, a
limpeza e a limpeza de dados do mundo real geralmente constituem a maior parte do trabalho,
e o Pandas fornece as ferramentas que podem ajudá-lo a fazer isso com eficiência.
O Pandas foi desenvolvido no contexto da modelagem financeira, portanto, como seria de esperar, ele contém um
conjunto bastante extenso de ferramentas para trabalhar com datas, horas e dados indexados por tempo. Os
dados de data e hora vêm em alguns sabores, que discutiremos aqui:
• Os carimbos de data e hora fazem referência a momentos específicos (por exemplo, 4 de julho de 2015, às 7h).
sou).
• Intervalos de tempo e períodos fazem referência a um período de tempo entre um ponto inicial e final específico
– por exemplo, o ano de 2015. Os períodos geralmente fazem referência a um caso especial de intervalos de
tempo em que cada intervalo tem duração uniforme e não se sobrepõe (por exemplo, períodos de 24 horas
que constituem dias).
• Deltas de tempo ou durações fazem referência a um período de tempo exato (por exemplo, uma duração de
22,56 segundos).
Nesta seção, apresentaremos como trabalhar com cada um desses tipos de dados de data/hora no Pandas. Esta
pequena seção não é de forma alguma um guia completo para as ferramentas de série temporal disponíveis em
Python ou Pandas, mas sim uma ampla visão geral de como você, como usuário, deve abordar o trabalho com
séries temporais. Começaremos com uma breve discussão sobre ferramentas para lidar com datas e horas em
Python, antes de passarmos mais especificamente para uma discussão sobre as ferramentas fornecidas pelo
Pandas. Depois de listar alguns recursos mais aprofundados, revisaremos alguns pequenos exemplos de trabalho
com dados de série temporal no Pandas.
tem diversas representações disponíveis de datas, horas, deltas e intervalos de tempo. Embora as ferramentas de
série temporal fornecidas pelo Pandas tendam a ser as mais úteis para aplicativos de ciência de dados, é útil ver
sua relação com outros pacotes usados em Python.
objetos básicos do Python para trabalhar com datas e horas residem no módulo integrado de data e hora .
Junto com o módulo dateutil de terceiros , você pode usá-lo para executar rapidamente uma série de
funcionalidades úteis em datas e horas. Por exemplo, você pode criar manualmente uma data usando o tipo
datetime :
Saída[1]: datetime.datetime(2015, 7, 4, 0, 0)
Ou, usando o módulo dateutil , você pode analisar datas de vários formatos de string:
Saída[2]: datetime.datetime(2015, 7, 4, 0, 0)
Depois de ter um objeto datetime , você pode fazer coisas como imprimir o dia da semana:
Em[3]: date.strftime('%A')
Saída[3]: 'Sábado'
Na linha final, usamos um dos códigos de formato de string padrão para impressão de datas ("%A"), sobre o
qual você pode ler na seção strftime da documentação datetime do Python . A documentação de outros
utilitários de data úteis pode ser encontrada na documentação online do dateutil. Um pacote relacionado que
você deve conhecer é o pytz, que contém ferramentas para trabalhar com os dados de série temporal que
mais causam enxaqueca: os fusos horários.
O poder de datetime e dateutil reside em sua flexibilidade e sintaxe fácil: você pode usar esses objetos e seus
métodos integrados para executar facilmente praticamente qualquer operação que possa lhe interessar. de
datas e horas: assim como as listas de variáveis numéricas do Python são abaixo do ideal em comparação
com matrizes numéricas digitadas no estilo NumPy, as listas de objetos datetime do Python são abaixo do
ideal em comparação com matrizes digitadas de datas codificadas.
NumPy Os pontos fracos do formato datetime do Python inspiraram a equipe do NumPy a adicionar um
conjunto de tipos de dados de série temporal nativos ao NumPy. O dtype datetime64 codifica datas como
números inteiros de 64 bits e, portanto, permite que matrizes de datas sejam representadas de forma muito
compacta. A data e hora64 requer um formato de entrada muito específico:
Uma vez formatada esta data, no entanto, podemos realizar operações vetorizadas rapidamente
nele:
Fora[5]:
array(['2015-07-04', '2015-07-05', '2015-07-06', '2015-07-07',
'08/07/2015', '09/07/2015', '10/07/2015', '11/07/2015',
'2015-07-12', '2015-07-13', '2015-07-14', '2015-07-15'],
dtype='datetime64[D]')
Devido ao tipo uniforme nas matrizes NumPy datetime64 , esse tipo de operação pode
ser realizado muito mais rapidamente do que se estivéssemos trabalhando diretamente com o Python
objetos datetime , especialmente à medida que os arrays ficam grandes (introduzimos esse tipo de vetorização
em “Cálculo em matrizes NumPy: funções universais” na página 50).
Um detalhe dos objetos datetime64 e timedelta64 é que eles são construídos em uma base divertida.
unidade de tempo fundamental. Como o objeto datetime64 é limitado à precisão de 64 bits, o
o intervalo de tempos codificáveis é 264 vezes esta unidade fundamental. Em outras palavras, data
time64 impõe uma compensação entre resolução de tempo e intervalo de tempo máximo.
Por exemplo, se você quiser uma resolução de tempo de um nanossegundo, você só terá o suficiente
informação para codificar um intervalo de 264 nanossegundos, ou pouco menos de 600 anos. NumPy
irá inferir a unidade desejada a partir da entrada; por exemplo, aqui está uma data e hora baseada no dia:
Em [6]: np.datetime64('2015-07-04')
Saída[6]: numpy.datetime64('2015-07-04')
Saída[7]: numpy.datetime64('2015-07-04T12:00')
Observe que o fuso horário é automaticamente definido para a hora local no computador exe.
cortando o código. Você pode forçar qualquer unidade fundamental desejada usando um dos muitos for-
códigos de tapete; por exemplo, aqui forçaremos um tempo baseado em nanossegundos:
Saída[8]: numpy.datetime64('2015-07-04T12:59:59.500000000')
A Tabela 3-6, extraída da documentação do NumPy datetime64, lista os recursos disponíveis para
códigos mat junto com os intervalos de tempo relativos e absolutos que eles podem codificar.
como
Attosegundo ± 9,2 segundos [1969 DC, 1970 DC]
Para os tipos de dados que vemos no mundo real, um padrão útil é datetime64[ns], pois
pode codificar uma gama útil de datas modernas com uma precisão adequada.
Finalmente, observaremos que, embora o tipo de dados datetime64 resolva algumas das definições
ciências do tipo datetime integrado do Python , ele carece de muitos dos métodos convenientes
ods e funções fornecidas por datetime e especialmente dateutil. Mais Informações
pode ser encontrado na documentação datetime64 do NumPy.
O Pandas baseia-se em todas as ferramentas que acabamos de discutir para fornecer um objeto Timestamp , que
combina a facilidade de uso de datetime e dateutil com armazenamento eficiente e
interface vetorizada de numpy.datetime64. De um grupo desses objetos Timestamp ,
Pandas pode construir um DatetimeIndex que pode ser usado para indexar dados em uma série ou
Quadro de dados; veremos muitos exemplos disso abaixo.
Por exemplo, podemos usar as ferramentas Pandas para repetir a demonstração acima. Nós
pode analisar uma data de string com formatação flexível e usar códigos de formato para gerar o dia de
a semana:
Em[10]: date.strftime('%A')
Saída[10]: 'Sábado'
Além disso, podemos fazer operações vetorizadas no estilo NumPy diretamente neste mesmo
objeto:
Na próxima seção, examinaremos mais de perto a manipulação de dados de séries temporais com
as ferramentas fornecidas pelo Pandas.
As ferramentas de série temporal do Pandas realmente se tornam úteis quando você começa a indexar
dados por carimbos de data/hora. Por exemplo, podemos construir um objeto Series que possui dados indexados
no tempo:
Saída[12]: 04/07/2014 0
04/08/2014 1
04/07/2015 2
04/08/2015 3
tipo de d: int64
Agora que temos esses dados em uma série, podemos fazer uso de qualquer índice da série .
padrões que discutimos nas seções anteriores, passando valores que podem ser coagidos a
datas:
Em[13]: dados['2014-07-04':'2015-07-04']
Saída[13]: 04/07/2014 0
04/08/2014 1
04/07/2015 2
tipo d: int64
Existem operações adicionais especiais de indexação apenas de data, como passar um ano para
obter uma fatia de todos os dados daquele ano:
Em[14]: dados['2015']
Saída[14]: 04/07/2015 2
04/08/2015 3
tipo d: int64
Posteriormente, veremos exemplos adicionais da conveniência de datas como índices. Mas primeiro,
vamos dar uma olhada mais de perto nas estruturas de dados de séries temporais disponíveis.
Esta seção apresentará as estruturas de dados fundamentais do Pandas para trabalhar com
dados de série temporal:
• Para períodos de tempo, o Pandas fornece o tipo Período . Isso codifica um intervalo
de frequência fixa baseado em numpy.datetime64. A estrutura de índice associada é
PeriodIndex.
Qualquer DatetimeIndex pode ser convertido em PeriodIndex com a função to_period() com
a adição de um código de frequência; aqui usaremos 'D' para indicar a frequência diária:
Em[16]: datas.to_period('D')
Fora[17]:
TimedeltaIndex(['0 dias', '1 dia', '3 dias', '4 dias', '5 dias'], dtype='timedelta64[ns]',
freq=Nenhum)
Para tornar a criação de sequências de datas regulares mais conveniente, o Pandas oferece
algumas funções para essa finalidade: pd.date_range() para carimbos de data e hora,
pd.period_range() para períodos e pd.timedelta_range() para deltas de tempo. Vimos que Python
range() e np.arange() do NumPy transformam um ponto inicial, um ponto final e um tamanho de etapa
opcional em uma sequência. Da mesma forma, pd.date_range() aceita uma data de início, uma data de
término e um código de frequência opcional para criar uma sequência regular de datas. Por padrão, a
frequência é de um dia:
Alternativamente, o intervalo de datas pode ser especificado não com um início e um ponto final, mas
com um ponto inicial e um número de períodos:
Você pode modificar o espaçamento alterando o argumento freq , cujo padrão é D. Por exemplo, aqui
construiremos um intervalo de carimbos de data/hora por hora:
Para criar sequências regulares de valores delta de período ou tempo, as funções pd.period_range() e
pd.timedelta_range() muito semelhantes são úteis. Aqui estão alguns períodos mensais:
Fora[21]:
PeriodIndex(['2015-07', '2015-08', '2015-09', '2015-10', '2015-11', '2015-12', '2016-01', '2016-02' ],
dtype='int64', freq='M')
Fora[22]:
TimedeltaIndex(['00:00:00', '01:00:00', '02:00:00', '03:00:00', '04:00:00', '05:00:00' , '06:00:00',
'07:00:00', '08:00:00', '09:00:00'], dtype='timedelta64[ns]', freq='H')
Tudo isso requer uma compreensão dos códigos de frequência do Pandas, que resumiremos na próxima
seção.
Frequências e compensações
Fundamental para essas ferramentas de série temporal do Pandas é o conceito de frequência ou data
desvio. Assim como vimos os códigos D (dia) e H (hora) anteriormente, podemos usar tais códigos
para especificar qualquer espaçamento de frequência desejado. A Tabela 3-7 resume os principais códigos
disponível.
EM Semanalmente
T Minutos
S Segundos
eu Milissegundos
EM Microssegundos
N Nanossegundos
As frequências mensais, trimestrais e anuais estão todas marcadas no final do folheto específico.
período fiado. Adicionar um sufixo S a qualquer um desses marca-o no início
(Tabela 3-8).
Descrição do código
Início do mês MS
Início do trimestre QS
AS Início do ano
Além disso, você pode alterar o mês usado para marcar qualquer código trimestral ou anual adicionando um
código de mês de três letras como sufixo:
Da mesma forma, você pode modificar o ponto de divisão da frequência semanal adicionando um código de
três letras para o dia da semana:
Além disso, os códigos podem ser combinados com números para especificar outras frequências. Por
exemplo, para uma frequência de 2 horas e 30 minutos, podemos combinar os códigos de hora (H) e minuto
(T) da seguinte forma:
Fora[23]:
TimedeltaIndex(['00:00:00', '02:30:00', '05:00:00', '07:30:00', '10:00:00',
'12:30:00', '15:00:00', '17:30:00', '20:00:00'],
dtype='timedelta64[ns]', freq='150T')
Todos esses códigos curtos referem-se a instâncias específicas de compensações de série temporal do
Pandas, que podem ser encontradas no módulo pd.tseries.offsets . Por exemplo, podemos criar um
deslocamento de dia útil diretamente da seguinte forma:
Para obter mais discussões sobre o uso de frequências e deslocamentos, consulte a seção “Objetos
DateOffset” da documentação online do Pandas.
de usar datas e horas como índices para organizar e acessar dados intuitivamente é uma parte importante
das ferramentas de série temporal do Pandas. Os benefícios dos dados indexados em geral (alinhamento
automático durante as operações, divisão e acesso intuitivo aos dados, etc.) ainda se aplicam, e o Pandas
fornece várias operações adicionais específicas para séries temporais.
Daremos uma olhada em alguns deles aqui, usando alguns dados de preços de ações como exemplo.
Como o Pandas foi desenvolvido em grande parte num contexto financeiro, inclui algumas ferramentas muito
específicas para dados financeiros. Por exemplo, o pacote pandas-datareader que acompanha (instalável
via conda install pandas-datareader) sabe como importar
Podemos visualizar isso usando o método plot() , após a configuração normal do Matplotlib
padrão (Figura 3-5):
Em[28]: goog.plot();
Uma necessidade comum de dados de séries temporais é a reamostragem em uma frequência maior ou menor.
Você pode fazer isso usando o método resample() ou o método muito mais simples asfreq()
método. A principal diferença entre os dois é que resample() é fundamentalmente uma agregação de dados,
enquanto asfreq() é fundamentalmente uma seleção de dados.
Dando uma olhada no preço de fechamento do Google, vamos comparar o que os dois retornam quando
reduzimos a amostragem dos dados. Aqui iremos reamostrar os dados no final do ano comercial (Figura 3-6):
Observe a diferença: em cada ponto, o resample reporta a média do ano anterior, enquanto o asfreq reporta o
valor no final do ano.
Para up-sampling, resample() e asfreq() são amplamente equivalentes, embora resample tenha muito mais
opções disponíveis. Nesse caso, o padrão para ambos os métodos é deixar os pontos amostrados vazios – ou
seja, preenchidos com valores NA. Assim como acontece com a função pd.fillna() discutida anteriormente,
asfreq() aceita um argumento de método para especificar como os valores são imputados. Aqui, faremos uma
nova amostra dos dados dos dias úteis com frequência diária (ou seja, incluindo finais de semana); veja a Figura
3-7:
data.asfreq('D').plot(ax=ax[0], marcador='o')
O painel superior é o padrão: os dias não úteis são deixados como valores NA e não aparecem no gráfico. O painel
inferior mostra as diferenças entre duas estratégias para preencher as lacunas: preenchimento direto e preenchimento
reverso.
Mudanças de horário
Outra operação comum específica de séries temporais é a mudança de dados no tempo. O Pandas tem dois
métodos intimamente relacionados para calcular isso: shift() e tshift(). Resumindo, a diferença entre eles é que shift()
desloca os dados, enquanto tshift() desloca o índice. Em ambos os casos, o deslocamento é especificado em
múltiplos da frequência.
Aqui iremos tanto shift() quanto tshift() por 900 dias (Figura 3-8):
goog.plot(ax=ax[0])
goog.shift(900).plot(ax=ax[1])
goog.tshift(900).plot(ax=ax[2])
# legendas e anotações
local_max = pd.to_datetime('2007-11-05') offset
= pd.Timedelta(900, 'D')
ax[0].legend(['input'], loc=2)
ax[0].get_xticklabels()[4].set(weight='heavy', color='red')
ax[0].axvline( local_max, alfa=0,3, cor='vermelho')
machado[1].legend(['shift(900)'], loc=2)
machado[1].get_xticklabels()[4].set(peso='pesado', cor='vermelho')
machado[1] .axvline(local_max + deslocamento, alfa=0,3, cor='vermelho')
machado[2].legend(['tshift(900)'], loc=2)
machado[2].get_xticklabels()[1].set(peso='pesado', cor='vermelho')
machado[2] .axvline(local_max + deslocamento, alfa=0,3, color='vermelho');
Vemos aqui que shift(900) desloca os dados em 900 dias, empurrando alguns deles para fora do
final do gráfico (e deixando os valores NA na outra extremidade), enquanto tshift(900) desloca
os valores do índice em 900 dias.
Um contexto comum para esse tipo de mudança é o cálculo das diferenças ao longo do tempo.
Por exemplo, usamos valores alterados para calcular o retorno do investimento de um ano para
as ações do Google ao longo do conjunto de dados (Figura 3-9):
Figura 3-9. Retorno do investimento até os dias atuais para ações do Google
Isto ajuda-nos a ver a tendência geral das ações da Google: até agora, os momentos mais lucrativos para
investir na Google têm sido (sem surpresa, em retrospectiva) logo após a sua IPO, e no meio da recessão
de 2009.
Janelas rolantes
As estatísticas rolantes são um terceiro tipo de operação específica de série temporal implementada pelo
Pandas. Isso pode ser feito por meio do atributo rolling() dos objetos Series e Data Frame , que retorna
uma visualização semelhante à que vimos com a operação groupby (consulte “Agregação e agrupamento”
na página 158). Essa visualização contínua disponibiliza diversas operações de agregação por padrão.
Por exemplo, aqui está a média móvel centrada de um ano e o desvio padrão dos preços das ações do
Google (Figura 3-10):
Tal como acontece com as operações groupby , os métodos agregate() e apply() podem ser usados para cálculos contínuos
personalizados.
Outro excelente recurso é o livro Python for Data Analysis, de Wes McKinney (O'Reilly, 2012). Embora já tenha alguns anos,
é um recurso inestimável no uso do Pandas. Em particular, este livro enfatiza ferramentas de séries temporais no contexto de
negócios e finanças e concentra-se muito mais em detalhes específicos de calendários comerciais, fusos horários e tópicos
relacionados.
Como sempre, você também pode usar a funcionalidade de ajuda do IPython para explorar e experimentar outras opções
disponíveis para as funções e métodos discutidos aqui. Acho que essa geralmente é a melhor maneira de aprender uma nova
ferramenta Python.
exemplo mais complexo de trabalho com alguns dados de séries temporais, vamos dar uma olhada nas contagens de
bicicletas na ponte Fremont, em Seattle. Esses dados são provenientes de um contador automatizado de bicicletas, instalado
no final de 2012, que possui sensores indutivos nas calçadas leste e oeste da ponte. As contagens horárias de bicicletas
podem ser baixadas em http://data.seattle.gov/; aqui está o link direto para o conjunto de dados.
Em[34]:
# !curl -o FremontBridge.csv
# https:// data.seattle.gov/ api/ views/ 65db-xm6k/ rows.csv?accessType=DOWNLOAD
Depois que esse conjunto de dados for baixado, podemos usar o Pandas para ler a saída CSV em um
Quadro de dados. Especificaremos que queremos a Data como um índice e queremos estes
datas a serem analisadas automaticamente:
Em[35]:
dados = pd.read_csv('FremontBridge.csv', index_col='Data', parse_dates=True)
dados.head()
Por conveniência, processaremos ainda mais esse conjunto de dados encurtando os nomes das colunas
e adicionando uma coluna “Total”:
Agora vamos dar uma olhada nas estatísticas resumidas desses dados:
Em [37]: data.dropna().describe()
Visualizando os dados
Podemos obter alguns insights sobre o conjunto de dados visualizando-o. Vamos começar plotando os dados
brutos (Figura 3-11):
In[39]: data.plot()
plt.ylabel(' Contagem horária de bicicletas');
As aproximadamente 25.000 amostras horárias são densas demais para que possamos entender. Podemos obter
mais informações reamostrando os dados para uma grade mais grosseira. Vamos fazer uma nova amostra por
semana (Figura 3-12):
Isto mostra-nos algumas tendências sazonais interessantes: como seria de esperar, as pessoas andam mais de
bicicleta no verão do que no inverno e, mesmo numa determinada estação, o uso da bicicleta varia de semana
para semana (provavelmente dependendo do clima; consulte “Em profundidade) . : Regressão Linear” na página
390 , onde exploramos isso mais detalhadamente).
Outra forma útil de agregar os dados é usar uma média móvel, utilizando a função pd.rolling_mean() . Aqui
faremos uma média contínua de nossos dados de 30 dias, certificando-nos de centralizar a janela (Figura 3-13):
A irregularidade do resultado se deve ao corte brusco da janela. Podemos obter uma versão mais suave de uma
média móvel usando uma função de janela – por exemplo, uma janela gaussiana. O código a seguir (visualizado na
Figura 3-14) especifica tanto a largura da janela (escolhemos 50 dias) quanto a largura da Gaussiana dentro da
janela (escolhemos 10 dias):
In[42]:
daily.rolling(50, center=True,
win_type='gaussian').sum(std=10).plot(style=[':', '--', '-']);
Explorando os dados
Embora as visualizações de dados suavizadas na Figura 3-14 sejam úteis para se ter uma idéia da tendência geral
dos dados, elas ocultam grande parte da estrutura interessante. Por exemplo, podemos querer observar o tráfego
médio em função da hora do dia. Podemos fazer isso usando a funcionalidade GroupBy discutida em “Agregação e
agrupamento” na página 158 (Figura 3-15):
O tráfego horário é uma distribuição fortemente bimodal, com picos por volta das 8h00 da manhã e das 17h00 da
tarde. Esta é provavelmente uma evidência de uma forte componente de tráfego suburbano que atravessa a ponte.
Isto é ainda evidenciado pelas diferenças entre a calçada oeste (geralmente usada indo em direção ao centro de
Seattle), que tem pico mais forte pela manhã, e a calçada leste (geralmente usada saindo do centro de Seattle),
que tem pico mais forte à noite.
Também podemos estar curiosos para saber como as coisas mudam com base no dia da semana.
Novamente, podemos fazer isso com um simples groupby (Figura 3-16):
Isto mostra uma forte distinção entre os totais dos dias da semana e dos fins de semana, com cerca de duas
vezes mais passageiros médios atravessando a ponte de segunda a sexta-feira do que no sábado e domingo.
Com isso em mente, vamos fazer um agrupamento composto e observar a tendência horária nos dias de semana
versus fins de semana. Começaremos agrupando por uma bandeira que marca o fim de semana e a hora do dia:
Agora usaremos algumas das ferramentas Matplotlib descritas em “Múltiplos Subtramas” na página 262 para plotar
dois painéis lado a lado (Figura 3-17):
Figura 3-17. Média horária de contagem de bicicletas por dia da semana e fim de semana
O resultado é muito interessante: vemos um padrão de deslocamento bimodal durante a semana de trabalho e um
padrão recreativo unimodal durante os finais de semana. Seria interessante investigar estes dados com mais
detalhes e examinar o efeito do clima, da temperatura, da época do ano e de outros fatores nos padrões de
deslocamento das pessoas; para uma discussão mais aprofundada, consulte minha postagem no blog “Is Seattle
Really Seeing an Uptick In Cycling?”, que usa um subconjunto desses dados. Também revisitaremos esse conjunto
de dados no contexto da modelagem em “Em profundidade: regressão linear” na página 390.
tivos para muitos casos de uso comuns, eles geralmente dependem da criação de objetos
intermediários temporários, o que pode causar sobrecarga indevida no tempo computacional e
no uso de memória.
A partir da versão 0.13 (lançada em janeiro de 2014), o Pandas inclui algumas ferramentas
experimentais que permitem acessar diretamente operações C-speed sem alocação dispendiosa
de arrays intermediários. Estas são as funções eval() e query() , que dependem do pacote
Numexpr . Neste caderno vamos explicar seu uso e fornecer algumas regras básicas sobre
quando você pode pensar em usá-los.
Conforme discutido em “Cálculo em matrizes NumPy: funções universais” na página 50, isso é
muito mais rápido do que fazer a adição por meio de um loop ou compreensão Python:
In[2]:
%timeit np.fromiter((xi + yi para xi, yi em zip(x, y)), dtype=x.dtype,
count=len(x))
Mas essa abstração pode se tornar menos eficiente quando você calcula expressões compostas.
Por exemplo, considere a seguinte expressão:
Fora[5]: Verdadeiro
O benefício aqui é que o Numexpr avalia a expressão de uma forma que não usa arrays temporários de
tamanho normal e, portanto, pode ser muito mais eficiente que o NumPy, especialmente para arrays grandes.
As ferramentas eval() e query() do Pandas que discutiremos aqui são conceitualmente semelhantes e dependem
do pacote Numexpr.
no Pandas usa expressões de string para calcular operações de forma eficiente usando DataFrames. Por
exemplo, considere os seguintes DataFrames:
Para calcular a soma de todos os quatro DataFrames usando a abordagem típica do Pandas, podemos
simplesmente escrever a soma:
Podemos calcular o mesmo resultado via pd.eval construindo a expressão como uma string:
A versão eval() desta expressão é cerca de 50% mais rápida (e usa muito menos memória), ao mesmo tempo
que fornece o mesmo resultado:
Fora[9]: Verdadeiro
A partir do Pandas v0.16, pd.eval() oferece suporte a uma ampla gama de operações. Para demonstrá-los,
usaremos os seguintes DataFrames inteiros:
Em [10]: df1, df2, df3, df4, df5 = (pd.DataFrame(rng.randint(0, 1000, (100, 3))) para i no
intervalo(5))
Fora[11]: Verdadeiro
Em [12]: resultado1 = (df1 < df2) & (df2 <= df3) & (df3 != df4) resultado2
= pd.eval('df1 < df2 <= df3 != df4') np.allclose(resultado1,
resultado2)
Fora[12]: Verdadeiro
Fora[13]: Verdadeiro
Em[14]: resultado3 = pd.eval('(df1 < 0,5) e (df2 < 0,5) ou (df3 < df4)')
np.allclose(resultado1, resultado3)
Fora[14]: Verdadeiro
Fora[15]: Verdadeiro
Outras operações. Outras operações, como chamadas de função, instruções condicionais, loops
e outras construções mais envolvidas, atualmente não são implementadas em pd.eval(). Se quiser
executar esses tipos de expressões mais complicados, você pode usar a própria biblioteca Numexpr.
Fora[16]: A B C
0 0,375506 0,406939 0,069938
1 0,069087 0,235615 0,154374
2 0,677945 0,433839 0,652324 3 0,264038
0,808055 0,347197
4 0,589161 0,252418 0,557789
Usando pd.eval() como acima, podemos calcular expressões com as três colunas como esta:
Fora[17]: Verdadeiro
O método DataFrame.eval() permite uma avaliação muito mais sucinta de expressões com
as colunas:
Fora[18]: Verdadeiro
Observe aqui que tratamos os nomes das colunas como variáveis dentro da expressão
avaliada e o resultado é o que desejaríamos.
Atribuição em DataFrame.eval()
Em[19]: df.head()
Fora[19]: A B C
0 0,375506 0,406939 0,069938
1 0,069087 0,235615 0,154374
2 0,677945 0,433839 0,652324
3 0,264038 0,808055 0,347197 4 0,589161
0,252418 0,557789
Podemos usar df.eval() para criar uma nova coluna 'D' e atribuir a ela um valor calculado a
partir das outras colunas:
Fora[20]: A B C D
0 0,375506 0,406939 0,069938 11,187620
1 0,069087 0,235615 0,154374 1,973796 2 0,677945 0,433839
0,652324 1,704344
3 0,264038 0,808055 0,347197 3,087857
4 0,589161 0,252418 0,557789 1,508776
Fora[21]: A B C D
0 0,375506 0,406939 0,069938 -0,449425
1 0,069087 0,235615 0,154374 -1,078728
2 0,677945 0,433839 0,652324 0,374209 3 0,264038
0,808055 0,347197 -1,566886
4 0,589161 0,252418 0,557789 0,603708
O método DataFrame.eval() suporta uma sintaxe adicional que permite trabalhar com variáveis locais
do Python. Considere o seguinte:
Fora[22]: Verdadeiro
O caractere @ aqui marca um nome de variável em vez de um nome de coluna e permite avaliar com
eficiência expressões envolvendo os dois “namespaces”: o namespace de colunas e o namespace de
objetos Python. Observe que este caractere @ é suportado apenas pelo método DataFrame.eval() ,
não pela função pandas.eval() , porque a função pandas.eval() só tem acesso a um namespace
(Python).
Método DataFrame.query() O
DataFrame possui outro método baseado em strings avaliadas, chamado método query() . Considere
o seguinte:
Fora[23]: Verdadeiro
Tal como acontece com o exemplo usado em nossa discussão sobre DataFrame.eval(), esta é uma
expressão que envolve colunas do DataFrame. Entretanto, ele não pode ser expresso usando a
sintaxe Data Frame.eval() ! Em vez disso, para este tipo de operação de filtragem, você pode usar o
método query() :
Fora[24]: Verdadeiro
Além de ser um cálculo mais eficiente, comparado à expressão de mascaramento, é muito mais
fácil de ler e entender. Observe que o método query() também aceita o sinalizador @ para
marcar variáveis locais:
Fora[25]: Verdadeiro
In[28]: df.valores.nbytes
Fora[28]: 32.000
Do lado do desempenho, eval() pode ser mais rápido mesmo quando você não está maximizando
a memória do sistema. A questão é como seus DataFrames temporários se comparam ao
tamanho do cache L1 ou L2 da CPU em seu sistema (normalmente alguns megabytes em 2016);
se forem muito maiores, então eval() poderá evitar algum movimento potencialmente lento de
valores entre os diferentes caches de memória. Na prática, acho que a diferença no tempo de
computação entre os métodos tradicionais e o método eval/query geralmente não é significativa
– na verdade, o método tradicional é mais rápido para arrays menores! O benefício de eval/
query está principalmente na memória salva e na sintaxe às vezes mais limpa que eles oferecem.
Cobrimos a maioria dos detalhes de eval() e query() aqui; para obter mais informações sobre
isso, você pode consultar a documentação do Pandas. Em particular, diferentes analisadores e
motores podem ser especificados para executar estas consultas; para obter detalhes sobre isso,
consulte a discussão na seção “Aprimorando o desempenho”.
Recursos adicionais
Neste capítulo, cobrimos muitos dos princípios básicos do uso eficaz do Pandas para análise de dados.
Ainda assim, muito foi omitido da nossa discussão. Para saber mais sobre Pandas, recomendo os
seguintes recursos:
Pandas no PyVideo
Do PyCon ao SciPy e ao PyData, muitas conferências apresentaram tutoriais de desenvolvedores
e usuários avançados do Pandas. Os tutoriais do PyCon, em particular, tendem a ser ministrados
por apresentadores muito bem avaliados.
Minha esperança é que, ao usar esses recursos, combinados com o passo a passo fornecido neste
capítulo, você esteja preparado para usar o Pandas para resolver qualquer problema de análise de
dados que encontrar!
CAPÍTULO 4
Agora daremos uma olhada aprofundada na ferramenta Matplotlib para visualização em Python.
Matplotlib é uma biblioteca de visualização de dados multiplataforma construída em arrays NumPy e
projetada para funcionar com a pilha SciPy mais ampla. Foi concebido por John Hunter em 2002,
originalmente como um patch para IPython para permitir plotagem interativa no estilo MATLAB via
gnuplot a partir da linha de comando do IPython. O criador do IPython, Fernando Perez, estava na
época lutando para terminar seu doutorado e informou a John que não teria tempo para revisar o
patch por vários meses. John tomou isso como uma deixa para começar por conta própria, e o
pacote Matplotlib nasceu, com a versão 0.1 lançada em 2003. Ele recebeu um impulso inicial quando
foi adotado como o pacote de plotagem preferido do Space Telescope Science Institute. (o pessoal
por trás do Telescópio Hubble), que apoiou financeiramente o desenvolvimento do Matplotlib e
expandiu enormemente suas capacidades.
Um dos recursos mais importantes do Matplotlib é sua capacidade de funcionar bem com muitos
sistemas operacionais e backends gráficos. Matplotlib suporta dezenas de backends e tipos de
saída, o que significa que você pode contar com ele para funcionar independentemente do sistema
operacional que você está usando ou do formato de saída que deseja. Essa abordagem
multiplataforma, tudo para todos, tem sido um dos grandes pontos fortes do Matplotlib. Isso levou a
uma grande base de usuários, que por sua vez levou a uma base ativa de desenvolvedores e às
ferramentas poderosas e à onipresença do Mat-plotlib no mundo científico do Python.
217
Machine Translated by Google
APIs modernas — por exemplo, Seaborn (discutido em “Visualização com Seaborn” na página 311),
ggplot, HoloViews, Altair e até mesmo o próprio Pandas podem ser usados como wrappers em torno
da API do Matplotlib. Mesmo com wrappers como esses, ainda é útil mergulhar na sintaxe do
Matplotlib para ajustar a saída final do gráfico. Por esse motivo, acredito que o próprio Matplotlib
continuará sendo uma peça vital da pilha de visualização de dados, mesmo que as novas ferramentas
signifiquem que a comunidade gradualmente deixe de usar a API Matplotlib diretamente.
Importando matplotlib
Assim como usamos a abreviação np para NumPy e a abreviatura pd para Pandas, usaremos
algumas abreviações padrão para importações Matplotlib:
A interface plt é a que usaremos com mais frequência, como veremos ao longo deste capítulo.
Configurando
estilos Usaremos a diretiva plt.style para escolher estilos estéticos apropriados para nossas figuras.
Aqui definiremos o estilo clássico , o que garante que os gráficos que criamos usem o estilo clássico
do Matplotlib:
Em[2]: plt.style.use('clássico')
Ao longo desta seção, ajustaremos esse estilo conforme necessário. Observe que as folhas de estilo
usadas aqui são suportadas a partir do Matplotlib versão 1.5; se você estiver usando uma versão
anterior do Matplotlib, apenas o estilo padrão estará disponível. Para obter mais informações sobre
folhas de estilo, consulte “Personalizando Matplotlib: configurações e folhas de estilo” na página 282.
visualização que você não pode ver não será de muita utilidade, mas a forma como você visualiza
seus gráficos Mat-plotlib depende do contexto. O melhor uso do Matplotlib varia dependendo de
como você o está usando; aproximadamente, os três contextos aplicáveis são usar Matplotlib em um
script, em um terminal IPython ou em um notebook IPython.
Plotando a partir de
um script Se você estiver usando Matplotlib dentro de um script, a função plt.show() é sua amiga.
plt.show() inicia um loop de eventos, procura todos os objetos de figura atualmente ativos e abre
uma ou mais janelas interativas que exibem sua figura ou figuras.
Então, por exemplo, você pode ter um arquivo chamado myplot.py contendo o seguinte:
plt.show()
Você pode então executar este script no prompt da linha de comando, o que resultará na abertura
de uma janela com sua figura exibida:
$ python meuplot.py
O comando plt.show() faz muitas coisas ocultas, pois deve interagir com o back-end gráfico
interativo do seu sistema. Os detalhes desta operação podem variar muito de sistema para
sistema e até mesmo de instalação para instalação, mas o Matplotlib faz o possível para ocultar
todos esses detalhes de você.
Uma coisa a ter em conta: o comando plt.show() deve ser usado apenas uma vez por sessão
Python e é visto com mais frequência no final do script. Vários comandos show() podem levar a
um comportamento imprevisível dependente de back-end e devem ser evitados principalmente.
IPython Pode ser muito conveniente usar o Matplotlib interativamente dentro de um shell IPython
(veja o Capítulo 1). IPython foi desenvolvido para funcionar bem com Matplotlib se você especificar
o modo Matplotlib. Para ativar este modo, você pode usar o comando mágico %matplotlib após
iniciar o ipython:
Em [1]: % matplotlib
Usando back-end matplotlib: TkAgg
Neste ponto, qualquer comando plt plot abrirá uma janela de figura e outros comandos poderão
ser executados para atualizar o gráfico. Algumas alterações (como modificar propriedades de
linhas já desenhadas) não serão desenhadas automaticamente; para forçar uma atualização, use
plt.draw(). O uso de plt.show() no modo Matplotlib não é necessário.
A plotagem interativa em um notebook IPython pode ser feita com o comando %matplotlib e
funciona de maneira semelhante ao shell IPython. No notebook IPython você também tem a opção
de incorporar gráficos diretamente no notebook, com duas opções possíveis:
Depois de executar este comando (ele precisa ser feito apenas uma vez por kernel/sessão),
qualquer célula dentro do notebook que cria um gráfico irá incorporar uma imagem PNG do gráfico
resultante ( Figura 4-1):
In[4]: importe numpy como
np x = np.linspace(0, 10, 100)
fig = plt.figure()
plt.plot(x, np.sin(x), '-') plt.plot(x,
np.cos(x), '--');
Em[5]: fig.savefig('minha_figura.png')
Para confirmar se ele contém o que pensamos que contém, vamos usar o objeto IPython Image para
exibir o conteúdo deste arquivo (Figura 4-2):
Em[8]: fig.canvas.get_supported_filetypes()
Observe que ao salvar sua figura, não é necessário usar plt.show() ou comandos relacionados
discutidos anteriormente.
Interface estilo
MATLAB Matplotlib foi originalmente escrito como uma alternativa Python para usuários do
MATLAB, e grande parte de sua sintaxe reflete esse fato. As ferramentas estilo MATLAB estão
contidas na interface pyplot (plt) . Por exemplo, o código a seguir provavelmente parecerá
bastante familiar aos usuários do MATLAB (Figura 4-3):
É importante notar que esta interface tem estado: ela monitora a figura e os eixos “atuais”, que
são onde todos os comandos plt são aplicados. Você pode obter uma referência para
estes usando as rotinas plt.gcf() (obter figura atual) e plt.gca() (obter eixos atuais).
Embora essa interface com estado seja rápida e conveniente para gráficos simples, é fácil encontrar
problemas. Por exemplo, uma vez criado o segundo painel, como podemos voltar e adicionar algo
ao primeiro? Isso é possível na interface estilo MATLAB, mas é um pouco desajeitado. Felizmente,
existe uma maneira melhor.
Interface orientada a
objetos A interface orientada a objetos está disponível para situações mais complicadas e para
quando você deseja ter mais controle sobre sua figura. Em vez de depender de alguma noção de
figura ou eixos “ativos”, na interface orientada a objetos as funções de plotagem são métodos de
objetos Figura e Eixos explícitos. Para recriar o gráfico anterior usando este estilo de plotagem,
você pode fazer o seguinte (Figura 4-4):
Para gráficos mais simples, a escolha de qual estilo usar é em grande parte uma questão de
preferência, mas a abordagem orientada a objetos pode se tornar uma necessidade à medida que
os gráficos se tornam mais complicados. Ao longo deste capítulo, alternaremos entre as interfaces
estilo MATLAB e orientadas a objetos, dependendo do que for mais conveniente. Na maioria dos
casos, a diferença é tão pequena quanto mudar plt.plot() para ax.plot(), mas há algumas dicas que
destacaremos à medida que surgirem nas seções a seguir.
Talvez o mais simples de todos os gráficos seja a visualização de uma única função y = fx . Aqui daremos
uma primeira olhada na criação de um gráfico simples desse tipo. Tal como acontece com todas as seções
a seguir, começaremos configurando o notebook para plotar e importar as funções que usaremos:
Em [1]: % matplotlib
importação inline matplotlib.pyplot
como plt plt.style.use('seaborn-whitegrid')
importação numpy como np
Para todos os gráficos do Matplotlib, começamos criando uma figura e um eixo. Na sua forma mais simples,
uma figura e eixos podem ser criados da seguinte forma (Figura 4-5):
No Matplotlib, a figura (uma instância da classe plt.Figure) pode ser pensada como um único contêiner que
contém todos os objetos que representam eixos, gráficos, texto e rótulos. Os eixos (uma instância da classe
plt.Axes) são o que vemos acima: uma caixa delimitadora com marcas de seleção e rótulos, que
eventualmente conterá os elementos do gráfico que compõem nossa visualização. Ao longo deste livro,
normalmente usaremos o nome de variável fig para nos referirmos a uma instância de figura, e ax para nos
referirmos a uma instância de eixos ou grupo de instâncias de eixos.
Depois de criarmos os eixos, podemos usar a função ax.plot para plotar alguns dados.
Vamos começar com uma sinusóide simples (Figura 4-6):
Alternativamente, podemos usar a interface pylab e deixar que a figura e os eixos sejam criados para nós
em segundo plano (Figura 4-7; consulte “Duas interfaces pelo preço de uma” na página 222 para uma
discussão sobre essas duas interfaces):
Se quisermos criar uma única figura com múltiplas linhas, podemos simplesmente chamar a função plot
diversas vezes (Figura 4-8):
Isso é tudo para traçar funções simples no Matplotlib! Agora mergulharemos em mais alguns detalhes
sobre como controlar a aparência dos eixos e linhas.
ajuste que você pode querer fazer em um gráfico é controlar as cores e estilos das linhas. A função
plt.plot() aceita argumentos adicionais que podem ser usados para especificá-los. Para ajustar a cor,
você pode usar a palavra-chave color , que aceita um argumento de string representando praticamente
qualquer cor imaginável. A cor pode ser especificada de diversas maneiras (Figura 4-9):
In[6]:
plt.plot(x, np.sin(x - 0), color='blue') # especifique a cor pelo nome plt.plot(x, np.sin(x -
1), color='g ') # código de cor curto (rgbcmyk) plt.plot(x, np.sin(x - 2), color='0.75')
# Tons de cinza entre 0 e 1
plt.plot(x, np.sin(x - 3), color='#FFDD44') # Código hexadecimal (RRGGBB de 00
a FF) plt.plot(x, np.sin(x - 4), color=(1.0,0.2,0.3)) # Tupla RGB, valores 0 e 1 plt.plot(x,
np .sin(x - 5), color='chartreuse'); # todos os nomes de cores HTML são suportados
Se nenhuma cor for especificada, o Matplotlib percorrerá automaticamente um conjunto de cores padrão
para múltiplas linhas.
Da mesma forma, você pode ajustar o estilo da linha usando a palavra-chave linestyle (Figura 4-10):
Se você quiser ser extremamente conciso, esses estilos de linha e códigos de cores podem ser
combinados em um único argumento sem palavra-chave para a função plt.plot() (Figura 4-11):
Esses códigos de cores de um único caractere refletem as abreviações padrão nos sistemas de cores RGB (vermelho/
verde/azul) e CMYK (ciano/magenta/amarelo/preto), comumente usados para gráficos digitais coloridos.
Existem muitos outros argumentos de palavras-chave que podem ser usados para ajustar a aparência do gráfico;
para obter mais detalhes, sugiro visualizar a documentação da função plt.plot() usando as ferramentas de ajuda do
IPython (consulte “Ajuda e documentação no IPython” na página 3).
Matplotlib faz um trabalho decente ao escolher os limites dos eixos padrão para o seu gráfico, mas às vezes é bom
ter um controle mais preciso. A maneira mais básica de ajustar os limites do eixo é usar os métodos plt.xlim() e
plt.ylim() (Figura 4-12):
plt.xlim(-1, 11)
plt.ylim(-1,5, 1,5);
Se por algum motivo você quiser que qualquer um dos eixos seja exibido ao contrário, você pode
simplesmente inverter a ordem dos argumentos (Figura 4-13):
plt.xlim(10, 0)
plt.ylim(1,2, -1,2);
Um método relacionado útil é plt.axis() (observe aqui a confusão potencial entre eixos com e e eixo com
i). O método plt.axis() permite definir os limites x e y com uma única chamada, passando uma lista que
especifica [xmin, xmax, ymin, ymax] (Figura 4-14):
O método plt.axis() vai além disso, permitindo que você faça coisas como apertar automaticamente os
limites em torno do gráfico atual (Figura 4-15):
Ele permite especificações de nível ainda mais alto, como garantir uma proporção igual para que,
na tela, uma unidade em x seja igual a uma unidade em y (Figura 4-16):
Figura 4-16. Exemplo de layout “igual”, com unidades correspondentes à resolução de saída
Para obter mais informações sobre os limites do eixo e os outros recursos do método plt.axis() ,
consulte a documentação plt.axis() .
Rotulagem de
gráficos Na última parte desta seção, veremos brevemente a rotulagem de gráficos: títulos, rótulos
de eixos e legendas simples.
Títulos e rótulos de eixo são os rótulos mais simples – existem métodos que podem ser usados
para defini-los rapidamente (Figura 4-17):
plt.xlabel("x")
plt.ylabel("sin(x)");
Você pode ajustar a posição, o tamanho e o estilo desses rótulos usando argumentos opcionais para a função. Para obter
mais informações, consulte a documentação do Matplotlib e as docstrings de cada uma dessas funções.
Quando múltiplas linhas estão sendo mostradas dentro de um único eixo, pode ser útil criar uma legenda de plotagem que
rotule cada tipo de linha. Novamente, o Matplotlib possui uma maneira integrada de criar rapidamente tal lenda. Isso é feito
plt.legenda();
Como você pode ver, a função plt.legend() rastreia o estilo e a cor da linha e combina-os com
o rótulo correto. Mais informações sobre como especificar e formatar legendas de gráficos
podem ser encontradas na documentação plt.legend() ; além disso, abordaremos algumas
opções de legenda mais avançadas em “Personalizando legendas de plotagem” na página 249.
Dicas do Matplotlib
Embora a maioria das funções plt sejam traduzidas diretamente para métodos ax (como
plt.plot() ÿ ax.plot(), plt.legend() ÿ ax.legend(), etc.), este não é o caso para todos os
comandos . Em particular, as funções para definir limites, rótulos e títulos foram ligeiramente modificadas.
Para fazer a transição entre funções no estilo MATLAB e métodos orientados a objetos, faça
as seguintes alterações:
• plt.xlabel() ÿ ax.set_xlabel()
• plt.ylabel() ÿ ax.set_ylabel()
• plt.xlim() ÿ ax.set_xlim()
• plt.ylim() ÿ ax.set_ylim()
• plt.title() ÿ ax.set_title()
Em [16]: ax = plt.axes()
ax.plot(x, np.sin(x))
ax.set(xlim=(0, 10), ylim=(-2, 2), xlabel='x
', ylabel='sin(x)', title='Um gráfico
simples');
Figura 4-19. Exemplo de uso de ax.set para definir várias propriedades de uma vez
Outro tipo de gráfico comumente usado é o gráfico de dispersão simples, um primo próximo do gráfico de linhas. Em vez
de os pontos serem unidos por segmentos de linha, aqui os pontos são representados individualmente com um ponto,
círculo ou outra forma. Começaremos configurando o notebook para plotagem e importação das funções que utilizaremos:
Em [1]: % matplotlib
importação inline matplotlib.pyplot
como plt plt.style.use('seaborn-whitegrid')
importação numpy como np
Na seção anterior, vimos plt.plot/ax.plot para produzir gráficos de linhas. Acontece que esta mesma função também pode
produzir gráficos de dispersão (Figura 4-20):
O terceiro argumento na chamada de função é um caractere que representa o tipo de símbolo usado para a plotagem.
Assim como você pode especificar opções como '-' e '--' para controlar o estilo da linha, o estilo do marcador tem seu
próprio conjunto de códigos de string curtos. A lista completa de símbolos disponíveis pode ser vista na documentação do
plt.plot ou na documentação online do Matplotlib. A maioria das possibilidades é bastante intuitiva e mostraremos algumas
das mais comuns aqui (Figura 4-21):
plt.legend(numpontos=1)
plt.xlim(0, 1,8);
Para ainda mais possibilidades, esses códigos de caracteres podem ser usados junto com códigos de
linhas e cores para traçar pontos junto com uma linha conectando-os (Figura 4-22):
Em[4]: plt.plot(x, y, '-ok'); # linha (-), marcador de círculo (o), preto (k)
Argumentos adicionais de palavras-chave para plt.plot especificam uma ampla gama de propriedades
das linhas e marcadores (Figura 4-23):
Este tipo de flexibilidade na função plt.plot permite uma ampla variedade de opções de visualização possíveis. Para uma
descrição completa das opções disponíveis, consulte a documentação plt.plot .
segundo método mais poderoso de criar gráficos de dispersão é a função plt.scatter , que pode ser usada de forma
muito semelhante à função plt.plot (Figura 4-24):
A principal diferença entre plt.scatter e plt.plot é que ele pode ser usado para criar gráficos de dispersão onde as
propriedades de cada ponto individual (tamanho, cor da face, cor da borda, etc.) podem ser controladas individualmente
ou mapeadas para dados.
Vamos mostrar isso criando um gráfico de dispersão aleatório com pontos de várias cores e tamanhos.
Para ver melhor os resultados sobrepostos, também usaremos a palavra-chave alfa para ajustar o nível de transparência
(Figura 4-25):