Você está na página 1de 253

Machine Translated by Google

Pitão
Ciência de Dados
Manual
FERRAMENTAS ESSENCIAIS PARA TRABALHAR COM DADOS

distribuído por

Jake Vander Plas


Machine Translated by Google
Machine Translated by Google

Manual de ciência de dados Python


Ferramentas essenciais para trabalhar com dados

Jake Vander Plas

Pequim Boston Farnham Sebastopol Tóquio


Machine Translated by Google

Manual de ciência de dados


Python por Jake VanderPlas

Direitos autorais © 2017 Jake VanderPlas. Todos os direitos reservados.

Impresso nos Estados Unidos da América.

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.

Editor: Dawn Schanafelt Indexador: WordCo Indexing Services, Inc.


Editor de produção: Kristen Brown Designer de interiores: David Futato
Editora: Jasmine Kwityn Designer de capa: Karen Montgomery
Revisora: Rachel Monaghan Ilustradora: Rebecca Demarest

Dezembro de 2016: Primeira edição

Histórico de revisões da primeira edição


17/11/2016: Primeiro lançamento

Consulte http://oreilly.com/catalog/errata.csp?isbn=9781491912058 para obter detalhes da versão.

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

1. IPython: além do Python normal. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1


Concha ou Caderno? 2

Iniciando o IPython Shell Iniciando 2

a Ajuda e Documentação do Jupyter 2

Notebook no IPython Acessando a 3

Documentação com ? 3

Acessando o código-fonte com ?? 5

Explorando Módulos com Preenchimento de Guia 6

Atalhos de teclado no IPython Shell 8

Atalhos de navegação 8

Atalhos de entrada de 9

texto Atalhos de histórico de 9


comandos Atalhos diversos 10

Comandos mágicos do IPython 10

Colando blocos de código: %paste e %cpaste 11

Executando código externo: %run 12

Execução de código de tempo: %timeit 12

Ajuda nas funções mágicas: ?, %magic e %lsmagic Entrada e 13

saída História 13

Objetos de entrada e saída do IPython 13


Atalhos de sublinhado e resultados anteriores 15

Suprimindo Saída 15

Comandos mágicos relacionados 16

Comandos IPython e Shell 16


Introdução rápida ao Shell 16

Comandos Shell em IPython 18

iii
Machine Translated by Google

Passando valores de e para o Shell 18

Comandos Mágicos Relacionados ao Shell 19

Erros e depuração 20

Controlando exceções: %xmode 20

Depuração: quando ler tracebacks não é suficiente 22

Perfil e código de tempo 25

Trechos de código de tempo: %timeit e %time 25

Criação de perfil de scripts completos: %prun 27

Perfil linha por linha com %lprun 28

Criação de perfil de uso de memória: %memit e %mprun 29

Mais recursos IPython 30


Recursos da Web 30
Livros 31

2. Introdução ao NumPy. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
Compreendendo os tipos de dados em Python 34

Um inteiro Python é mais do que apenas um número inteiro 35

Uma lista Python é mais do que apenas uma lista 37

Matrizes de tipo fixo em Python 38

Criando matrizes a partir de listas Python 39

Criando matrizes do zero Tipos de 39

dados padrão NumPy 41

O básico dos arrays NumPy 42

Atributos da matriz NumPy 42

Indexação de array: acessando elementos únicos 43

Fatiamento de array: acessando subarrays 44

Remodelação de matrizes 47

Concatenação e divisão de array 48

Computação em matrizes NumPy: funções universais 50

A lentidão dos loops 50

Apresentando UFuncs 51

Explorando UFuncs do NumPy 52


Recursos avançados do Ufunc 56

Ufuncs: Aprendendo mais 58

Agregações: mínimo, máximo e tudo mais 58

Somando os valores em uma matriz 59


Mínimo e Máximo 59

Exemplo: Qual é a altura média dos presidentes dos EUA? 61

Computação em Arrays: Broadcasting 63

Apresentando Regras de 63

Broadcasting Broadcasting 65

na Prática 68

iv | Índice
Machine Translated by Google

Comparações, máscaras e exemplo de lógica 70

booleana: contando dias chuvosos 70

Operadores de comparação como ufuncs 71

Trabalhando com matrizes booleanas 73

Matrizes booleanas como 75

máscaras Indexação 78

sofisticada Explorando a indexação 79

sofisticada Exemplo de 80

indexação combinada: seleção de pontos 81

aleatórios Modificando valores com indexação 82

sofisticada Exemplo: 83

categorização de 85

dados Classificação de matrizes Classificação rápida em 86

NumPy: np.sort e np.argsort 88

Classificações parciais: Exemplo de 88

particionamento: k-vizinhos mais próximos Dados 92

estruturados: matrizes estruturadas 94

do NumPy Criando matrizes estruturadas 95

Tipos compostos mais avançados RecordArrays: matrizes 96


estruturadas com um toque especial para Pandas 96

3. Manipulação de dados com Pandas. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97


Instalando e usando Pandas 97

Apresentando Objetos Pandas 98

O objeto da série Pandas 99

O objeto DataFrame do Pandas 102

O objeto de índice Pandas 105

Indexação e seleção de dados 107


Seleção de dados em série 107
Seleção de dados no DataFrame 110

Operando com dados em Pandas 115


Ufuncs: preservação de índice 115

UFuncs: alinhamento do índice 116

Ufuncs: operações entre DataFrame e série 118

Tratamento de dados ausentes 119

Compensações em convenções de dados ausentes 120

Dados ausentes em Pandas 120

Operando em valores nulos 124

Indexação Hierárquica 128

Uma série indexada multiplicada 128


Métodos de criação de MultiIndex 131

Indexando e fatiando um MultiIndex 134

Índice | v
Machine Translated by Google

Reorganizando agregações de 137

dados de vários índices em vários índices 140

Combinando conjuntos de dados: Concat e Append 141

Recall: Concatenação de matrizes NumPy 142

Concatenação simples com pd.concat 142

Combinando conjuntos de dados: mesclar e unir 146

Álgebra Relacional 146

Categorias de junções 147

Especificação da chave de mesclagem 149

Especificando conjunto aritmético para junções 152

Nomes de colunas sobrepostos: a palavra-chave sufixos 153

Exemplo: dados dos estados dos EUA 154

Agregação e Agrupamento 158


Dados dos planetas 159

Agregação simples em Pandas 159

GroupBy: Dividir, Aplicar, Combinar 161


Tabelas dinâmicas 170

Motivando tabelas dinâmicas 170

Tabelas dinâmicas manualmente 171

Sintaxe da tabela dinâmica 171

Exemplo: dados de taxa de natalidade 174

Operações de String Vetorizadas 178

Apresentando as operações de string do Pandas 178

Tabelas de métodos de string Pandas 180

Exemplo: banco de dados de receitas 184

Trabalhando com séries temporais 188

Datas e horas em Python 188

Série temporal do Pandas: indexação por tempo 192


Estruturas de dados de série temporal Pandas 192

Frequências e compensações 195

Reamostragem, mudança e janelamento 196


Onde aprender mais 202

Exemplo: Visualizando contagens de bicicletas em Seattle 202

Pandas de alto desempenho: eval() e query() 208

Motivando query() e eval(): expressões compostas pandas.eval() para 209

operações eficientes DataFrame.eval() para 210

operações em colunas DataFrame.query() Desempenho do 211

método: quando usar essas funções 213


Recursos adicionais 214
215

nós
| Índice
Machine Translated by Google

4. Visualização com Matplotlib. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .217


Dicas gerais do Matplotlib 218

Importando estilos de 218

configuração do 218

matplotlib show() ou No show()? Como exibir seus gráficos 218

Salvando números para 221


arquivar Duas interfaces pelo preço de um 222

Gráfico de linhas 224

simples Ajustando o gráfico: Cores e estilos das 226

linhas Ajustando o gráfico: Limites dos 228

eixos Rotulagem 230

de gráficos Gráficos de 233

dispersão simples Gráficos de 233

dispersão com plt.plot Gráficos de 235

dispersão com plt.scatter gráfico versus dispersão: 237

uma nota sobre 237


eficiência na visualização de erros barras de erro básicas 238
Erros Contínuos 239

Gráficos de densidade e contorno 241

Visualizando uma função tridimensional 241

Histogramas, binnings e densidade 245

Histogramas e binnings bidimensionais 247

Personalizando legendas de plotagem 249

Escolhendo elementos para a legenda 251

Legenda para tamanho de pontos 252

Múltiplas Lendas 254

Personalizando barras de cores 255

Personalizando barras de 256

cores Exemplo: Dígitos manuscritos 261

Múltiplos subparcelas 262

plt.axes: Subparcelas à mão 263

plt.subplot: Grades simples de subparcelas 264

plt.subplots: A grade inteira de uma só vez 265

plt.GridSpec: Arranjos mais complicados 266


Texto e anotação 268

Exemplo: Efeito dos feriados nos nascimentos nos EUA 269


Transformações e posição do texto 270
Setas e anotação 272

Personalizando ticks 275

Carrapatos maiores e menores 276

Ocultando marcas ou rótulos 277

Reduzindo ou aumentando o número de ticks 278

Índice | vii
Machine Translated by Google

Formatos de ticks sofisticados 279

Resumo de formatadores e localizadores 281

Personalizando Matplotlib: configurações e folhas de estilo 282

Personalização de plotagem manualmente 282

Alterando os padrões: rcParams 284

Folhas de estilo 285

Plotagem tridimensional em Matplotlib 290


Pontos e linhas tridimensionais 291
Gráficos de contorno tridimensionais 292
Wireframes e gráficos de superfície 293

Triangulações de Superfície 295

Dados geográficos com mapa base 298

Projeções cartográficas 300

Desenhando um plano de fundo de mapa 304

Plotando dados em mapas 307

Exemplo: cidades da Califórnia 308

Exemplo: Dados de temperatura de superfície 309


Visualização com Seaborn 311

Seaborn versus Matplotlib 312

Explorando parcelas marítimas 313

Exemplo: Explorando os tempos de chegada da maratona 322


Recursos adicionais 329

Recursos Matplotlib 329

Outras bibliotecas gráficas Python 330

5. Aprendizado de máquina. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 331


O que é aprendizado de máquina? 332

Categorias de exemplos qualitativos de 332

aprendizado de máquina de aplicativos de aprendizado de máquina 333

Resumo 342

Apresentando a representação 343

de dados do Scikit-Learn no Scikit-Learn 343


Aplicativo de API estimador do 346

Scikit-Learn: Explorando dígitos manuscritos Resumo 354


359

Hiperparâmetros e validação de modelo Pensando 359

na validação do modelo Selecionando as 359

melhores curvas de aprendizado 363

do modelo Validação 370


na prática: Pesquisa em grade 373

Resumo 375

Engenharia de recursos 375

viii | Índice
Machine Translated by Google

Recursos categóricos 376


Características do texto 377

Recursos de imagem 378


Recursos derivados 378

Imputação de dados ausentes 381

Pipelines de recursos 381

Em profundidade: classificação Naive Bayes 382

Classificação Bayesiana 383

Gaussiano Ingênuo Bayes 383

Multinomial Ingênuo Bayes 386

Quando usar Naive Bayes 389

Em profundidade: regressão linear 390

Regressão Linear Simples 390

Regressão da Função Base 392

Regularização 396

Exemplo: previsão do tráfego de bicicletas 400

Em profundidade: máquinas de vetores de suporte 405

Motivando Máquinas de Vetores de Apoio 405

Máquinas de vetores de suporte: maximizando a margem 407

Exemplo: reconhecimento facial 416

Resumo da máquina de vetores de suporte 420

Em profundidade: árvores de decisão e florestas aleatórias 421

Motivando florestas aleatórias: árvores de decisão 421


Conjuntos de estimadores: florestas aleatórias 426

Regressão Florestal Aleatória 428

Exemplo: Floresta Aleatória para Classificação de Dígitos 430

Resumo de Florestas Aleatórias 432

Em profundidade: análise de componentes principais 433

Apresentando a Análise de Componentes Principais 433

PCA como filtragem de ruído 440

Exemplo: autofaces 442

Resumo da análise de componentes principais 445

Em profundidade: aprendizagem múltipla 445

Aprendizagem múltipla: “OLÁ” 446

Dimensionamento Multidimensional (MDS) 447

MDS como aprendizagem múltipla 450

Incorporações não lineares: onde o MDS falha 452

Variedades Não Lineares: Incorporação Localmente Linear 453

Algumas reflexões sobre métodos múltiplos 455

Exemplo: Isomapa em Faces 456

Exemplo: Visualizando Estrutura em Dígitos 460

Em profundidade: agrupamento k-Means 462

Índice | ix
Machine Translated by Google

Apresentando o Algoritmo 463

k-Means k-Means: Expectativa – Maximização 465

Exemplos 470

Em profundidade: modelos de mistura gaussiana 476

Motivando GMM: Fraquezas do k-Means 477

Generalizando E – M: Modelos de Mistura Gaussiana 480

GMM como estimativa de densidade 484

Exemplo: GMM para geração de novos dados 488

Em profundidade: estimativa de densidade do kernel 491

Motivando o KDE: Histogramas 491

Estimativa de densidade do kernel na prática 496

Exemplo: KDE em uma esfera 498

Exemplo: Bayes não tão ingênuo 501

Aplicação: Um pipeline de detecção de rosto 506


Recursos do HOG 506

HOG em ação: um detector de rosto simples 507

Advertências e melhorias 512

Outros recursos de aprendizado de máquina 514

Aprendizado de máquina em Python 514

Aprendizado de máquina geral 515

Índice. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 517

x | Índice
Machine Translated by Google

Prefácio

O que é ciência de dados?

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) .

Figura P-1. Diagrama de Venn de ciência de dados de Drew Conway

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.

Para quem é este livro?

Nas minhas aulas na Universidade de Washington e em várias conferências e encontros com


foco em tecnologia, uma das perguntas mais comuns que ouvi é esta: “como devo aprender
Python?” As pessoas que perguntam são geralmente estudantes, desenvolvedores ou
pesquisadores com mentalidade técnica, muitas vezes com uma sólida experiência em escrever
código e usar ferramentas computacionais e numéricas. A maioria dessas pessoas não quer
aprender Python em si, mas quer aprender a linguagem com o objetivo de usá-la como uma
ferramenta para ciência computacional e com uso intensivo de dados. Embora uma grande
quantidade de vídeos, postagens de blog e tutoriais para esse público esteja disponível on-line,
há muito tempo estou frustrado pela falta de uma única boa resposta para essa pergunta; foi isso que inspirou este li

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.

Por que Python?


Python emergiu nas últimas décadas como uma ferramenta de primeira classe para tarefas de
computação científica, incluindo a análise e visualização de grandes conjuntos de dados. Isto
pode ter sido uma surpresa para os primeiros proponentes da linguagem Python: a linguagem
em si não foi projetada especificamente tendo em mente a análise de dados ou a computação científica.

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.

Python 2 versus Python 3 Este

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.

Esboço deste livro


Cada capítulo deste livro se concentra em um pacote ou ferramenta específica que contribui com uma
peça fundamental da história da ciência de dados do Python.

IPython e Jupyter (Capítulo 1)


Esses pacotes fornecem o ambiente computacional no qual trabalham muitos cientistas de
dados que usam Python.

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.

Usando exemplos de código


Material suplementar (exemplos de código, figuras, etc.) está disponível para download em https://
github.com/jakevdp/PythonDataScienceHandbook. Este livro está aqui para ajudá-lo a realizar seu
trabalho. Em geral, se um código de exemplo for oferecido com este livro, você poderá usá-lo em seus
programas e documentação. Você não precisa entrar em contato conosco para obter permissão, a
menos que esteja reproduzindo uma parte significativa do código. Por exemplo, escrever um programa
que utilize vários trechos de código deste livro não requer permissão. Vender ou distribuir um CD-ROM
com exemplos de livros da O'Reilly requer permissão. Responder a uma pergunta citando este livro e
citando código de exemplo não requer permissão. Incorporar uma quantidade significativa de código de
exemplo deste livro na documentação do seu produto requer permissão.

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:

[~]$ conda instalar numpy pandas scikit-learn matplotlib seaborn ipython-notebook

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.

Convenções utilizadas neste livro


As seguintes convenções tipográficas são usadas neste livro:

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.

Largura constante negrito

Mostra comandos ou outro texto que deve ser digitado literalmente pelo usuário.

Largura constante em itálico

Mostra o texto que deve ser substituído por valores fornecidos pelo usuário ou por valores determinados
pelo contexto.

O'Reilly Safari

Safari (anteriormente Safari Books Online) é uma plataforma de treinamento e


referência baseada em membros para empresas, governos, educadores e
indivíduos.

Prefácio | xv
Machine Translated by Google

Os membros têm acesso a milhares de livros, vídeos de treinamento, caminhos de aprendizagem,


tutoriais interativos e listas de reprodução selecionadas de mais de 250 editoras, incluindo
O'Reilly Media, Harvard Business Review, Prentice Hall Professional, Addison-Wesley
Professional, Microsoft Press. , Sams, Que, Peachpit Press, Adobe, Focal Press, Cisco Press,
John Wiley & Sons, Syngress, Morgan Kaufmann, IBM Redbooks, Packt, Adobe Press, FT Press,
Apress, Manning, New Riders, McGraw-Hill, Jones & Bartlett e Course Technology, entre outros.

Para obter mais informações, visite http://oreilly.com/safari.

Como entrar em contato conosco

Por favor, envie comentários e perguntas sobre este livro à editora:

O'Reilly Media, Inc.


1005 Gravenstein Highway North
Sebastopol, CA 95472
800-998-9938 (nos Estados Unidos ou Canadá)
707-829-0515 (internacional ou local)
707-829-0104 (fax)

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.

Encontre-nos no Facebook: http://facebook.com/oreilly

Siga-nos no Twitter: http://twitter.com/oreillymedia Assista-

nos no YouTube: http://www.youtube.com/oreillymedia

xvi | Prefácio
Machine Translated by Google

CAPÍTULO 1

IPython: além do Python normal

Existem muitas opções de ambientes de desenvolvimento para Python, e muitas vezes me


perguntam qual delas uso em meu próprio trabalho. Minha resposta às vezes surpreende as
pessoas: meu ambiente preferido é IPython mais um editor de texto (no meu caso, Emacs ou
Atom dependendo do meu humor). IPython (abreviação de Interactive Python) foi iniciado em
2001 por Fernando Perez como um interpretador Python aprimorado e desde então se tornou um
projeto que visa fornecer, nas palavras de Perez, “ferramentas para todo o ciclo de vida da
computação de pesquisa”. Se Python é o mecanismo de nossa tarefa de ciência de dados, você
pode pensar no IPython como um painel de controle interativo.

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.

Lançando o IPython Shell Este capítulo,

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).

Depois de fazer isso, você verá um prompt como o seguinte:

IPython 4.0.1 – Um Python interativo aprimorado. ?


-> Introdução e visão geral dos recursos do IPython. %quickref
-> Referência rápida. help -> Sistema
de ajuda próprio do Python. objeto? -> Detalhes
sobre 'objeto', use 'objeto??' para detalhes extras.
Em 1]:

Com isso, você está pronto para acompanhar.

Iniciando o Jupyter Notebook O Jupyter

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:

2 | Capítulo 1: IPython: além do Python normal


Machine Translated by Google

$ 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).

Ajuda e documentação em IPython


Se você não leu nenhuma outra seção deste capítulo, leia esta: Considero as ferramentas discutidas
aqui as contribuições mais transformadoras do IPython para meu fluxo de trabalho diário.

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

código-fonte deste objeto Python? • O que há neste pacote que importei?

Quais atributos ou métodos este objeto


ter?

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.

Acessando a documentação com ?


A linguagem Python e seu ecossistema de ciência de dados são construídos pensando no usuário, e
uma grande parte disso é o acesso à documentação. Todo objeto Python contém o

Ajuda e documentação em IPython | 3


Machine Translated by Google

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

Retorna o número de itens de uma sequência ou mapeamento.

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

Retorna o número de itens de uma sequência ou mapeamento.

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

ou até mesmo os próprios objetos, com a documentação do seu tipo:

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:

Em [6]: def quadrado(a):


....: """Retorne o quadrado de a."""

4 | Capítulo 1: IPython: além do Python normal


Machine Translated by Google

....: 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.

Agora usaremos o ? marque para encontrar esta doutrina:

Em [7]: quadrado?
Tipo: função Forma de string:
<função quadrada em 0x103713cb0> Definição: quadrado(a)

Docstring: Retorne o quadrado de 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!

Acessando o código-fonte com ??


Como a linguagem Python é facilmente legível, geralmente você pode obter outro nível de percepção
lendo o código-fonte do objeto que lhe interessa. IPython fornece um atalho para o código-fonte com o
ponto de interrogação duplo (??):

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

Retorna o número de itens de uma sequência ou mapeamento.

Ajuda e documentação em IPython | 5


Machine Translated by Google

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.

Explorando Módulos com Preenchimento de Guia

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.

Conclusão da guia do conteúdo do objeto

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”).

6 | Capítulo 1: IPython: além do Python normal


Machine Translated by Google

Preenchimento de tabulação ao importar

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:

Em [10]: de itertools import co<TAB>


combinações comprimir
combinações_com_replacement contagem

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):

Em [10]: importar <TAB>


Exibir todas as 399 possibilidades? (s ou n)
deste py_compile
Crypto Cython distutil picclbr
... ... ...
diflib senha zmq

Em [10]: importar h<TAB>


hashlib hmac heapq html http
habitação

(Observe que, por questões de brevidade, não imprimi aqui todos os 399 pacotes e módulos importáveis
no meu sistema.)

Além da conclusão da guia: correspondência de curingas

O preenchimento de tabulação é útil se você conhece os primeiros caracteres do objeto ou atributo


você está procurando, mas não ajuda muito se você quiser combinar caracteres no meio ou
final da palavra. Para este caso de uso, o IPython fornece um meio de correspondência de curingas
para nomes usando o caractere * .

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

DeprecationWarning Aviso de sintaxe


FutureWarning Aviso Unicode
ImportWarning Aviso do usuário
PendingDeprecationWarning Aviso
ResourceWarning

Observe que o caractere * corresponde a qualquer string, incluindo a string vazia.

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:

Ajuda e documentação em IPython | 7


Machine Translated by Google

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.

Atalhos de teclado no IPython Shell


Se você passa muito tempo no computador, provavelmente encontrou uma utilidade para os
atalhos de teclado em seu fluxo de trabalho. Os mais familiares talvez sejam Cmd-C e Cmd-V
(ou Ctrl-C e Ctrl-V) para copiar e colar em uma ampla variedade de programas e sistemas.
Usuários avançados tendem a ir ainda mais longe: editores de texto populares como Emacs,
Vim e outros fornecem aos usuários uma variedade incrível de operações por meio de
combinações complexas de teclas.

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-a Mova o cursor para o início da linha


Ctrl-e Mova o cursor para o final da linha

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

8 | Capítulo 1: IPython: além do Python normal


Machine Translated by Google

Atalhos para entrada de texto

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

Tecla Backspace Excluir caractere anterior da linha

Ctrl-d Excluir o próximo caractere da linha

Ctrl-k Corte o texto do cursor até o final da linha

Ctrl-você Corte o texto do início da linha até o cursor

Ctrl-y Arranque (ou seja, cole) o texto que foi cortado anteriormente

Ctrl-t Transpor (ou seja, trocar) os dois caracteres anteriores

Atalhos do histórico de comandos

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

Ctrl-r Pesquisa reversa no histórico de comandos

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:

Atalhos de teclado no IPython Shell |9


Machine Translated by Google

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:

Em [1]: def quadrado(a):


"""Retorna o quadrado de a"""
retorna um** 2

Em [2]: quadrado (2)


Fora[2]: 4

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:

Ação de pressionamento de tecla

Ctrl-l Limpar tela do terminal

Ctrl-c Interromper o comando Python atual

Ctrl-d Sair da sessão IPython

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.

Comandos mágicos do IPython


As duas seções anteriores mostraram como o IPython permite usar e explorar Python de maneira
eficiente e interativa. Aqui começaremos a discutir algumas das melhorias que

10 | Capítulo 1: IPython: além do Python normal


Machine Translated by Google

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.

Colando Blocos de Código: %paste e %cpaste Quando

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:

>>> def não fazer nada


... (x): retornar x

O código é formatado como apareceria no interpretador Python e, se você copiar e colar diretamente
no IPython, receberá um erro:

Em [2]: >>> def donotthing(x):


...: ... retornar x
...:
Arquivo "<ipython-input-20-5a66c8964687>", linha 2
... retornar x
^

SyntaxError: sintaxe inválida


Na colagem direta, o intérprete fica confuso com os caracteres adicionais do prompt.
Mas não tenha medo - a função mágica %paste do IPython foi projetada para lidar com esse tipo
exato de entrada marcada e multilinha:

Em [3]: %paste
>>> def donotthing(x):
... retornar x

## -- Fim do texto colado --


O comando %paste insere e executa o código, então agora a função está pronta para ser usada:

Em [4]: não fazer nada (10)


Fora[4]: 10

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:

Comandos mágicos do IPython | 11


Machine Translated by Google

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.

Executando código externo: %run Ao

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

para N no intervalo (1,


4): print(N, "quadrado é", quadrado(N))

Você pode executar isso em sua sessão IPython da seguinte maneira:

Em [6]: % run myscript.py 1


ao quadrado é 1
2 ao quadrado é
4 3 ao quadrado é 9

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:

Em [7]: quadrado (5)


Fora[7]: 25

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.

Tempo de execução do código: %timeit

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:

Em [8]: %timeit L = [n ** 2 for n in range(1000)] 1000 loops,


melhor de 3: 325 µs por loop

12 | Capítulo 1: IPython: além do Python normal


Machine Translated by Google

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 :

Em [9]: %%timeit ...:


L = [] ...:
para n no intervalo (1000):
...: L.append(n ** 2)
...:
1000 loops, melhor de 3: 373 µs por loop

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.

Ajuda sobre funções mágicas: ?, %magic e %lsmagic Como as funções

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.

Histórico de entrada e saída


Anteriormente, vimos que o shell IPython permite acessar comandos anteriores com as teclas de seta para cima
e para baixo ou, equivalentemente, com os atalhos Ctrl-p/Ctrl-n. Além disso, tanto no shell quanto no notebook, o
IPython expõe diversas maneiras de obter a saída de comandos anteriores, bem como versões em string dos
próprios comandos. Vamos explorar isso aqui.

Objetos In e Out do IPython Agora imagino

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

Histórico de entrada e saída | 13


Machine Translated by Google

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 [1]: importar matemática

Em [2]: math.sin(2)
Fora[2]: 0,9092974268256817

Em [3]: math.cos(2)
Saída[3]: -0,4161468365471424

Importamos o pacote matemático integrado e calculamos o seno e o cosseno do número 2. Essas


entradas e saídas são exibidas no shell com rótulos In/Out , mas há mais - o IPython na verdade cria
algumas variáveis Python chamadas In e Out que são atualizados automaticamente para refletir esse
histórico:

Em [4]: print(In) ['',


'importar matemática', 'math.sin(2)', 'math.cos(2)', 'print(In)']

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):

Em [6]: print(In[1]) importa


matemática

O objeto Out não é uma lista, mas um dicionário que mapeia números de entrada para suas saídas
(se houver):

Entrada [7]: imprimir(Saída[2])


0,9092974268256817

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:

Entrada [8]: Saída[2] ** 2 + Saída[3] ** 2


Saída[8]: 1,0

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!

14 | Capítulo 1: IPython: além do Python normal


Machine Translated by Google

Atalhos de sublinhado e saídas anteriores O shell

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:

Em [14]: math.sin(2) + math.cos(2);

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

Histórico de entrada e saída | 15


Machine Translated by Google

Comandos mágicos relacionados

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:

Em [16]: %history -n 1-4 1:


import math 2:
math.sin(2) 3:
math.cos(2) 4:
print(In)

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.

Comandos IPython e Shell


Ao trabalhar interativamente com o interpretador Python padrão, uma das frustrações que você
enfrentará é a necessidade de alternar entre diversas janelas para acessar ferramentas Python e
ferramentas de linha de comando do sistema. O IPython preenche essa lacuna e fornece uma sintaxe
para executar comandos shell diretamente de dentro do terminal IPython. A mágica acontece com o
ponto de exclamação: qualquer coisa que apareça depois de ! em uma linha será executado não pelo
kernel Python, mas pela linha de comando do sistema.

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.

Introdução rápida ao Shell


Uma introdução completa ao uso do shell/terminal/linha de comando está muito além do escopo deste
capítulo, mas para os não iniciados ofereceremos uma introdução rápida aqui. O shell é uma forma
de interagir textualmente com o seu computador. Desde meados da década de 1980, quando a
Microsoft e a Apple introduziram as primeiras versões de seus agora onipresentes sistemas
operacionais gráficos, a maioria dos usuários de computadores interagiu com seus sistemas
operacionais através de cliques familiares em menus e movimentos de arrastar e soltar. Mas os
sistemas operacionais existiam muito antes dessas interfaces gráficas de usuário e eram controlados
principalmente por meio de sequências de entrada de texto: no prompt, o usuário digitava um comando
e o computador fazia o que o usuário mandava. Aqueles avisos iniciais

16 | Capítulo 1: IPython: além do Python normal


Machine Translated by Google

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:~ $ pwd / # pwd = imprime diretório de trabalho #


home/jake este é o "caminho" em que estamos

osx:~ $ ls #ls = lista o conteúdo do diretório de trabalho


projetos de notebooks

osx:~ $ cd projetos/ # cd = alterar diretório

osx:projects $ pwd /
home/jake/projects

osx: projetos $ ls
datasci_book mpld3 meuprojeto.txt

osx:projetos $ mkdir meuprojeto # mkdir = cria novo diretório

osx:projetos $ cd meuprojeto/

osx:myproject $ mv ../myproject.txt ./ # mv = mover arquivo. Aqui estamos movendo o # arquivo


myproject.txt de um diretório # up (../) para o
diretório atual (./)
osx:meuprojeto $ ls
meuprojeto.txt

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.

Comandos IPython e Shell | 17


Machine Translated by Google

Comandos Shell no IPython Você pode

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 [3]: !echo "imprimindo do shell" imprimindo


do shell

Passando valores de e para o Shell Os


comandos Shell não só podem ser chamados a partir do IPython, mas também podem
interagir com o namespace IPython. Por exemplo, você pode salvar a saída de qualquer
comando shell em uma lista Python usando o operador de atribuição:

Em [4]: conteúdo = !

Em [5]: print(conteúdo)
['meuprojeto.txt']

Em [6]: diretório = !pwd

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} :

Em [9]: mensagem = "olá do Python"

Em [10]: !echo {message} olá


do Python

18 | Capítulo 1: IPython: além do Python normal


Machine Translated by Google

As chaves contêm o nome da variável, que é substituído pelo conteúdo da variável no comando shell.

Comandos Mágicos Relacionados ao 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

A razão é que os comandos shell no notebook são executados em um subshell temporário. Se


desejar alterar o diretório de trabalho de uma forma mais duradoura, você pode usar o comando
mágico %cd :

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 [16]: mkdir tmp

Em [17]: ls
meuprojeto.txt tmp/

Em [18]: cp 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.

Comandos mágicos relacionados ao Shell | 19


Machine Translated by Google

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.

Controlando exceções: %xmode Na maioria

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:

In[1]: def func1(a, b): retornar


a/b

def função2(x):
uma = x
b=x-1
retorno func1(a, b)

Em[2]: func2(1)
-------------------------------------------------- -------------------------

Erro ZeroDivision Traceback (última chamada mais recente)

<ipython-input-2-b2e110f6fc8f^gt; em <módulo>() ----> 1 func2(1)

<ipython-input-1-d849e34d61fb> em func2(x)
5 uma = x
6 b=x-1
----> 7 retorno func1(a, b)

<ipython-input-1-d849e34d61fb> em func1(a, b) 1 def func1(a,


b): retornar a / b
----> 2
3
4 def função2(x): 5
uma = x

ZeroDivisionError: divisão por zero

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

20 | Capítulo 1: IPython: além do Python normal


Machine Translated by Google

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.

%xmode aceita um único argumento, o modo, e há três possibilidades: Simples,


Contexto e Verbose. O padrão é Contexto e fornece resultados como o mostrado acima.
Simples é mais compacto e fornece menos informações:

Em[3]: %xmode Simples

Modo de relatório de exceção: Simples

Em[4]: func2(1)
-------------------------------------------------- ----------

Traceback (última chamada mais recente):

Arquivo "<ipython-input-4-b2e110f6fc8f>", linha 1, em <module>


função2(1)

Arquivo "<ipython-input-1-d849e34d61fb>", linha 7, em func2


retornar função1 (a, b)

Arquivo "<ipython-input-1-d849e34d61fb>", linha 2, em func1 retorna a/


b

ZeroDivisionError: divisão por zero

O modo Verbose adiciona algumas informações extras, incluindo os argumentos para quaisquer
funções chamadas:

In[5]: %xmode detalhado

Modo de relatório de exceção: detalhado

Em[6]: func2(1)
-------------------------------------------------- -------------------------

Erro ZeroDivision Traceback (última chamada mais recente)

<ipython-input-6-b2e110f6fc8f> em <module>() ----> 1


func2(1) func2 global
= <função func2 em 0x103729320>

<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

<ipython-input-1-d849e34d61fb> em func1(a=1, b=0) 1 def func1(a,


b): ----> 2 retornar a / b

uma =
1b=0
3
4 def função2(x): 5
uma = x

ZeroDivisionError: divisão por zero

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 .

Depuração: quando a leitura de tracebacks não é suficiente A ferramenta

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

> <ipython-input-1-d849e34d61fb>(2)func1() 1 def func1(a,


b): retornar a / b
----> 2
3

ipdb> imprimir (a) 1

ipdb> imprimir(b) 0

ipdb> sair

22 | Capítulo 1: IPython: além do Python normal


Machine Translated by Google

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

> <ipython-input-1-d849e34d61fb>(2)func1() 1 def func1(a,


b): ----> 2 retornar a / b

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:

In[9]: %xmode Simples


%pdb em
func2(1)

Modo de relatório de exceção: Simples


A chamada automática de pdb foi ativada

Traceback (última chamada mais recente):

Arquivo "<ipython-input-9-569a67d2d312>", linha 3, em <module>


função2(1)

Arquivo "<ipython-input-1-d849e34d61fb>", linha 7, em func2


retornar função1 (a, b)

Erros e depuração | 23
Machine Translated by Google

Arquivo "<ipython-input-1-d849e34d61fb>", linha 2, em func1


retornar a/b

ZeroDivisionError: divisão por zero

> <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.

Lista parcial de comandos de depuração

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

lista Mostrar a localização atual no arquivo

Mostrar uma lista de comandos ou encontrar ajuda sobre um comando específico


ajuda)
q(uit) Saia do depurador e do programa

c(ontinue) Sai do depurador; continuar no programa


próximo) Vá para a próxima etapa do programa

<entrar> Repita o comando anterior

Imprimir variáveis
p(correr)
etapa) Entre em uma sub-rotina

Sair de uma sub-rotina


retornar)

Para obter mais informações, use o comando help no depurador ou dê uma olhada em
Documentação on-line do ipdb .

24 | Capítulo 1: IPython: além do Python normal


Machine Translated by Google

Perfil e código de tempo


No processo de desenvolvimento de código e criação de pipelines de processamento de dados, muitas vezes há
compensações que você pode fazer entre várias implementações. No início do desenvolvimento do seu algoritmo, pode
ser contraproducente se preocupar com essas coisas. Como disse Donald Knuth: “Devíamos esquecer as pequenas
eficiências, digamos, cerca de 97% das vezes: a otimização prematura é a raiz de todos os males”.

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

Cronometre a execução de uma única instrução

%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

%
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.

Trechos de código de tempo: %timeit e %time


Vimos a magia da linha %timeit e a magia da célula %%timeit na introdução às funções mágicas em “Comandos mágicos
do IPython” na página 10; %%timeit pode ser usado para cronometrar a execução repetida de trechos de código:

In[1]: %timeit soma(intervalo(100))

100.000 loops, melhor de 3: 1,54 µs por loop

Código de perfil e tempo | 25


Machine Translated by Google

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:

In[2]: %%timeit total


= 0 para i
no intervalo(1000): para j
no intervalo(1000): total +=
i * (-1) ** j

1 loop, melhor de 3: 407 ms por loop

À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:

In[3]: importação aleatória


L = [random.random() para i no intervalo (100000)] % timeit
L.sort()

100 loops, melhor de 3: 1,9 ms por loop

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:

In[4]: importação aleatória


L = [random.random() para i in range(100000)]
print("classificando uma lista não classificada:")
%time L.sort()

classificando uma lista não


classificada: Tempos de CPU: usuário 40,6 ms, sys: 896 µs, total: 41,5
ms Tempo de parede: 41,5 ms

In[5]: print("classificando uma lista já ordenada:") %time L.sort()

classificando uma lista já classificada:


Tempos de CPU: usuário 8,18 ms, sys: 10 µs, total: 8,19 ms Tempo
de parede: 8,24 ms

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:

26 | Capítulo 1: IPython: além do Python normal


Machine Translated by Google

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).

Criação de perfil de scripts completos: %prun

Um programa é feito de muitas declarações únicas e, às vezes, cronometrando esses estados.


mentos no contexto é mais importante do que sincronizá-los por conta própria. Python contém
um criador de perfil de código integrado (sobre o qual você pode ler na documentação do Python), mas
O IPython oferece uma maneira muito mais conveniente de usar esse criador de perfil, na forma do
função mágica %prun.

A título de exemplo, definiremos uma função simples que faz alguns cálculos:

Em[7]: def soma_de_listas(N):


total = 0
para i no intervalo (5):
L = [j ^ (j >> i) para j no intervalo (N)]
total += soma(L)
retornar total

Agora podemos chamar %prun com uma chamada de função para ver os resultados do perfil:

Em[8]: %prun sum_of_lists(1000000)

No notebook, a saída é impressa no pager e se parece com isto:

14 chamadas de função em 0,714 segundos

Ordenado por: horário interno

ncalls tottime percall cumtime percall nome do arquivo:lineno(função)


5 0,599 0,120 0,599 0,120 <ipython-input-19>:4(<listcomp>)
5 0,064 0,013 0,064 0,013 {soma do método integrado}
1 0,036 0,036 0,699 0,699 <ipython-input-19>:1(soma_de_listas)
1 0,014 0,014 0,714 0,714 <string>:1(<módulo>)
1 0,000 0,000 0,714 0,714 {método integrado exec}

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.

Código de perfil e tempo | 27


Machine Translated by Google

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).

Perfil linha por linha com %lprun


O perfil função por função de %prun é útil, mas às vezes é mais conveniente
importante ter um relatório de perfil linha por linha. Isso não está integrado ao Python ou IPython,
mas existe um pacote line_profiler disponível para instalação que pode fazer isso. Começar
usando a ferramenta de empacotamento do Python, pip, para instalar o pacote line_profiler :

$ pip instalar line_profiler

Em seguida, você pode usar o IPython para carregar a extensão line_profiler IPython, oferecida como
parte deste pacote:

Em[9]: %load_ext line_profiler

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:

Em[10]: %lprun -f soma_de_listas soma_de_listas(5000)

Como antes, o notebook envia o resultado para o pager, mas é mais ou menos assim:

Unidade de temporizador: 1e-06 s

Tempo total: 0,009382 s


Arquivo: <ipython-input-19-fa2be176cc3e>
Função: sum_of_lists na linha 1

Linha # Exitos Tempo por acerto % Conteúdo da linha do tempo


================================================= ============
1 def soma_de_listas(N):
2 1 2 2,0 0,0 total = 0
3 6 8 1.3 0,1 para i no intervalo (5):
4 5 9001 1800,2 95,9 L = [j ^ (j >> i) ...
5 371 74,2 4,0 total += soma(L)
6 51 0 0,0 0,0 retornar total

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).

28 | Capítulo 1: IPython: além do Python normal


Machine Translated by Google

Criação de perfil de uso de memória: %memit e %mprun Outro

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:

$ pip instalar memória_profiler

Então podemos usar o IPython para carregar a extensão:

Em[12]: %load_ext memory_profiler

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:

Em[13]: %memit sum_of_lists(1000000)

memória de pico: 100,08 MiB, incremento: 61,36 MiB

Vemos que esta função usa cerca de 100 MB de memória.

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:

In[14]: %%arquivo mprun_demo.py


def sum_of_lists(N): total
= 0 para i
no intervalo(5): L = [j
^ (j >> i) para j no intervalo(N)] total += sum(L)
del L # remove
referência a L retorna total

Substituindo mprun_demo.py

Agora podemos importar a nova versão desta função e executar o perfilador de linha de memória:

In[15]: de mprun_demo importar sum_of_lists


%mprun -f soma_de_listas soma_de_listas(1000000)

O resultado, impresso no pager, nos dá um resumo do uso de memória da função e é mais ou menos
assim:

Código de perfil e tempo | 29


Machine Translated by Google

Nome do arquivo: ./mprun_demo.py

Linha # Uso de memória Incrementar o conteúdo da linha


===============================================
4 71,9 MiB 0,0 MiB L = [j ^ (j >> i) para j no intervalo (N)]

Nome do arquivo: ./mprun_demo.py

Linha # Uso de memória Incrementar o conteúdo da linha


===============================================
1 39,0 MiB 0,0 MiB def soma_de_listas(N):
2 39,0 MiB 0,0 MiB total = 0
46,5 MiB 7,5 MiB para i no intervalo (5):
34 71,9 MiB 25,4 MiB L = [j ^ (j >> i) para j no intervalo (N)]
5 71,9 MiB 0,0 MiB total += soma(L)
6 46,5 MiB -25,4 MiB del L # remove referência a L
7 39,1 MiB -7,4 MiB retornar total

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).

Mais recursos IPython


Neste capítulo, acabamos de arranhar a superfície do uso do IPython para permitir a ciência de dados.
tarefas. Muito mais informações estão disponíveis tanto impressas quanto na Web, e
aqui listaremos alguns outros recursos que podem ser úteis.

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!

30 | Capítulo 1: IPython: além do Python normal


Machine Translated by Google

Uma galeria de notebooks IPython interessantes


Esta lista cada vez maior de notebooks, desenvolvida pelo nbviewer, mostra a profundidade e a
amplitude da análise numérica que você pode fazer com o IPython. Inclui tudo, desde pequenos
exemplos e tutoriais até cursos completos e livros compostos em formato de caderno!

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.

Aprendendo IPython para computação interativa e visualização de dados Este


pequeno livro de Cyrille Rossant oferece uma boa introdução ao uso do IPython para análise de
dados.

Livro de receitas de computação e visualização interativa do IPython


Também de Cyrille Rossant, este livro é um tratamento mais longo e avançado do uso do IPython
para ciência de dados. Apesar do nome, não se trata apenas de IPython – ele também se
aprofunda em uma ampla gama de tópicos de ciência de dados.

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.

Mais recursos IPython | 31


Machine Translated by Google
Machine Translated by Google

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:

In[2]: importar numpy como np

Ao longo deste capítulo, e de fato no restante do livro, você descobrirá que é assim que importaremos
e usaremos o NumPy.

Lembrete sobre a documentação integrada


Ao ler este capítulo, não esqueça que o IPython oferece a capacidade de explorar rapidamente
o conteúdo de um pacote (usando o recurso de preenchimento de tabulação), bem como a
documentação de diversas funções (usando o caractere ? ) . Consulte “Ajuda e documentação
no IPython” na página 3 se precisar de uma atualização sobre isso.

Por exemplo, para exibir todo o conteúdo do namespace numpy , você pode digitar isto:

Em [3]: np.<TAB>

E para exibir a documentação integrada do NumPy, você pode usar isto:

Em [4]: np?

Documentação mais detalhada, juntamente com tutoriais e outros recursos, pode ser encontrada
em http://www.numpy.org.

Compreendendo os tipos de dados em Python


A ciência e a computação eficazes baseadas em dados exigem a compreensão de como os dados
são armazenados e manipulados. Esta seção descreve e contrasta como arrays de dados são tratados
na própria linguagem Python e como o NumPy melhora isso. Compreender essa diferença é
fundamental para compreender grande parte do material ao longo do restante do livro.

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

34 | Capítulo 2: Introdução ao NumPy


Machine Translated by Google

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;
}

Enquanto em Python a operação equivalente poderia ser escrita desta forma:

# 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.

Um número inteiro Python é mais do que apenas um número

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):

Compreendendo os tipos de dados em Python | 35


Machine Translated by Google

struct _longobject { long


ob_refcnt;
PyTypeObject *ob_type;
tamanho_t
ob_size; longo ob_dígito[1];
};

Um único número inteiro em Python 3.4 contém, na verdade, quatro partes:

• 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

• ob_type, que codifica o tipo da variável • ob_size, que especifica

o tamanho dos seguintes membros de dados • ob_digit, que contém o valor inteiro real

que esperamos da variável Python


capaz de representar

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.

Figura 2-1. A diferença entre inteiros C e Python

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.

36 | Capítulo 2: Introdução ao NumPy


Machine Translated by Google

Uma lista Python é mais do que apenas uma

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

Ou, da mesma forma, uma lista de strings:

In[3]: L2 = [str(c) para c em L]


L2

Saída[3]: ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']

Em[4]: tipo(L2[0])

Fora[4]: str

Devido à tipagem dinâmica do Python, podemos até criar listas heterogêneas:

In[5]: L3 = [True, "2", 3.0, 4] [tipo(item)


para item em L3]

Fora[5]: [bool, str, float, int]

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.

No nível de implementação, o array contém essencialmente um único ponteiro para um bloco


contíguo de dados. A lista Python, por outro lado, contém um ponteiro para um bloco de ponteiros,
cada um dos quais, por sua vez, aponta para um objeto Python completo, como o inteiro Python
que vimos anteriormente. Novamente, a vantagem da lista é a flexibilidade: como cada elemento da
lista é uma estrutura completa contendo dados e informações de tipo, a lista pode ser preenchida
com dados de qualquer tipo desejado. Arrays de estilo NumPy de tipo fixo não possuem essa
flexibilidade, mas são muito mais eficientes para armazenar e manipular dados.

Compreendendo os tipos de dados em Python | 37


Machine Translated by Google

Figura 2-2. A diferença entre listas C e Python

Matrizes de tipo fixo em Python

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:

In[6]: importar matriz


L = lista(intervalo(10))
A = array.array('i', L)
A

Fora[6]: array('i', [0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

Aqui 'i' é um código de tipo que indica que o conteúdo é inteiro.

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.

Começaremos com a importação padrão do NumPy, sob o alias np:

In[7]: importar numpy como np

38 | Capítulo 2: Introdução ao NumPy


Machine Translated by Google

Criando arrays a partir de listas Python Primeiro,

podemos usar np.array para criar arrays a partir de listas Python:

In[8]: # array inteiro:


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

Fora[8]: array([1, 4, 2, 5, 3])

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):

Em[9]: np.array([3.14, 4, 2, 3])

Fora[9]: array([ 3.14, 4. , 2. , 3.])

Se quisermos definir explicitamente o tipo de dados do array resultante, podemos usar a palavra-chave dtype :

Em [10]: np.array([1, 2, 3, 4], dtype='float32')

Fora[10]: array([ 1., 2., 3., 4.], dtype=float32)

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:

In[11]: # listas aninhadas resultam em matrizes multidimensionais


np.array([range(i, i + 3) for i in [2, 4, 6]])

Fora[11]: array([[2, 3, 4], [4, 5,


6], [6, 7, 8]])

As listas internas são tratadas como linhas da matriz bidimensional resultante.

Criando Arrays do Zero


Especialmente para arrays maiores, é mais eficiente criar arrays do zero usando
rotinas incorporadas ao NumPy. Aqui estão vários exemplos:

In[12]: # Cria um array inteiro de comprimento 10 preenchido com zeros


np.zeros(10, dtype=int)

Fora[12]: matriz([0, 0, 0, 0, 0, 0, 0, 0, 0, 0])

In[13]: # Cria um array de ponto flutuante 3x5 preenchido com 1s


np.ones((3, 5), dtype=float)

Fora[13]: matriz([[ 1., 1., 1., 1., 1.], [ 1., 1., 1., 1., 1.],
[ 1., 1., 1 ., 1., 1.]])

In[14]: # Cria um array 3x5 preenchido com 3,14


completo((3, 5), 3,14)

Compreendendo os tipos de dados em Python | 39


Machine Translated by Google

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]])

In[15]: # Cria um array preenchido com uma sequência linear #


Começando em 0, terminando em 20, avançando em
2 # (isso é semelhante à função interna range()) np.arange( 0,
20, 2 )

Fora[15]: matriz([0, 2, 4, 6, 8, 10, 12, 14, 16, 18])

In[16]: # Cria um array de cinco valores espaçados uniformemente entre 0 e 1


np.linspace(0, 1, 5)

Fora[16]: array([ 0. , 0,25, 0,5 , 0,75, 1. ])

In[17]: # Cria um array 3x3 de # valores aleatórios distribuídos


uniformemente entre 0 e 1
np.random.random((3, 3))

Fora [17]: matriz ([[0,99844933, 0,52183819, 0,22421193],


[0,08007488, 0,45429293, 0,20941444],
[0,14360941, 0,96910973, 0,946117]])

In[18]: # Cria um array 3x3 de valores aleatórios normalmente distribuídos


# com média 0 e desvio padrão 1
np.random.normal(0, 1, (3, 3))

OUT [18]: Array ([[1.51772646, 0,39614948, -0.10634696], [0,25671348,


0,00732722, 0,37783601], [0,6446945, 0,15926039,
-0, [0,6446945, 0,15926039, [0,68446945, 0,15926039.

In[19]: # Cria um array 3x3 de inteiros aleatórios no intervalo [0, 10)


np.random.randint(0, 10, (3, 3))

Fora[19]: matriz([[2, 3, 4], [5, 7,


8], [0, 5, 0]])

In[20]: # Crie uma matriz identidade 3x3


np.eye(3)

Fora[20]: matriz([[ 1., 0., 0.], [ 0., 1., 0.],


[ 0., 0., 1.]])

In[21]: # Cria um array não inicializado de três inteiros


# Os valores serão o que já existir naquele # local de memória np.empty(3)

Fora[21]: matriz([ 1., 1., 1.])

40 | Capítulo 2: Introdução ao NumPy


Machine Translated by Google

Tipos de dados padrão NumPy


Matrizes NumPy contêm valores de um único tipo, por isso é importante ter informações detalhadas
conhecimento desses tipos e suas limitações. Como o NumPy é construído em C, o
serão familiares aos usuários de C, Fortran e outras linguagens relacionadas.

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')

Ou usando o objeto NumPy associado:

np.zeros(10, dtype=np.int16)

Tabela 2-1. Tipos de dados NumPy padrão

Tipo de dados Descrição

bool_ Booleano (True ou False) armazenado como um byte

int_ Tipo inteiro padrão (igual a C long; normalmente int64 ou int32)


interno Idêntico a C int (normalmente int32 ou int64)

interno Inteiro usado para indexação (igual a C ssize_t; normalmente int32 ou int64)
você8 Bytes (–128 a 127)

int16 Inteiro (–32768 a 32767)

int32 Inteiro (–2147483648 a 2147483647)

int64 Inteiro (–9223372036854775808 a 9223372036854775807)

uint8 Inteiro sem sinal (0 a 255)

uint16 Inteiro sem sinal (0 a 65535)

uint32 Inteiro sem sinal (0 a 4294967295)

uint64 Inteiro sem sinal (0 a 18446744073709551615)

flutuador_ Abreviação de float64

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

complex_ Abreviação de complex128

complex64 Número complexo, representado por duas aveias de 32 bits

complex128 Número complexo, representado por duas aveias de 64 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.

Compreendendo os tipos de dados em Python | 41


Machine Translated by Google

O básico dos arrays NumPy


A manipulação de dados em Python é quase sinônimo de manipulação de array NumPy: até mesmo ferramentas
mais novas, como o Pandas (Capítulo 3), são construídas em torno do array NumPy. Esta seção apresentará vários
exemplos usando a manipulação de arrays NumPy para acessar dados e submatrizes e para dividir, remodelar e unir
os arrays. Embora os tipos de operações mostrados aqui possam parecer um pouco áridos e pedantes, eles
constituem os blocos de construção de muitos outros exemplos usados ao longo do livro. Conheça-os bem!

Abordaremos algumas categorias de manipulações básicas de array aqui:

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

Obtendo e configurando submatrizes menores dentro de uma matriz maior

Remodelação de matrizes
Alterando a forma de um determinado array

Junção e divisão de arrays


Combinar vários arrays em um e dividir um array em vários

Atributos do array NumPy Primeiro,

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:

In[1]: import numpy as np


np.random.seed(0) # seed para reprodutibilidade

x1 = np.random.randint(10, size=6) # Matriz unidimensional x2 =


np.random.randint(10, size=(3, 4)) # Matriz bidimensional x3 =
np.random.randint( 10, size=(3, 4, 5)) # Matriz tridimensional

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):

Em [2]: print("x3 ndim: ", x3.ndim)


print("x3 shape:", x3.shape)
print("x3 size: ", x3.size)
x3 ndim:
formato 3 x3: (3, 4, 5)
tamanho x3: 60

42 | Capítulo 2: Introdução ao NumPy


Machine Translated by Google

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):

Em[3]: print("dtype:", x3.dtype)

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:

Em[4]: print("itemsize:", x3.itemsize, "bytes")


imprimir("nbytes:", x3.nbytes, "bytes")

tamanho do item: 8
bytes nbytes: 480 bytes

Em geral, esperamos que nbytes sejam iguais ao tamanho do item vezes o tamanho.

Indexação de array: acessando elementos únicos Se você

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

Fora[5]: array([5, 0, 3, 3, 7, 9])

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

Fora[10]: array([[3, 5, 2, 4], [7, 6, 8,


8], [1, 6, 7, 7]])

Em[11]: x2[0, 0]

Fora[11]: 3

O básico dos arrays NumPy | 43


Machine Translated by Google

Em[12]: x2[2, 0]

Fora[12]: 1

Em[13]: x2[2, -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

Fora[14]: array([[12, 5, 2, 4], [ 7, 6, 8, 8],


[ 1, 6, 7, 7]])

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!

In[15]: x1[0] = 3.14159 # isso será truncado!


x1

Fora[15]: array([3, 0, 3, 3, 7, 9])

Fatiamento de array: acessando submatrizes Assim

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

Fora[16]: matriz([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

In[17]: x[:5] # primeiros cinco elementos

Fora[17]: array([0, 1, 2, 3, 4])

In[18]: x[5:] # elementos após o índice 5

Fora[18]: array([5, 6, 7, 8, 9])

In[19]: x[4:7] # submatriz intermediária

Fora[19]: array([4, 5, 6])

44 | Capítulo 2: Introdução ao NumPy


Machine Translated by Google

In[20]: x[::2] # todos os outros elementos

Fora[20]: array([0, 2, 4, 6, 8])

In[21]: x[1::2] # todos os outros elementos, começando no índice 1

Fora[21]: array([1, 3, 5, 7, 9])

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:

In[22]: x[::-1] # todos os elementos, invertidos

Fora[22]: matriz([9, 8, 7, 6, 5, 4, 3, 2, 1, 0])

In[23]: x[5::-2] # inverteu todos os outros a partir do índice 5

Fora[23]: array([5, 3, 1])

Submatrizes multidimensionais

As fatias multidimensionais funcionam da mesma maneira, com múltiplas fatias separadas por vírgulas.
Por exemplo:

Em[24]: x2

Fora[24]: array([[12, 5, 2, 4], [ 7, 6, 8, 8],


[ 1, 6, 7, 7]])

In[25]: x2[:2, :3] # duas linhas, três colunas

Fora[25]: matriz([[12, 5, 2],


[7, 6, 8]])

In[26]: x2[:3, ::2] # todas as linhas, todas as outras colunas

Fora[26]: array([[12, 2], [ 7, 8],


[ 1, 7]])

Finalmente, as dimensões da submatriz podem até ser invertidas juntas:

Em[27]: x2[::-1, ::-1]

Fora[27]: array([[ 7, 7, 6, 1], [ 8, 8, 6, 7],


[ 4, 2, 5, 12]])

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 (:):

In[28]: print(x2[:, 0]) # primeira coluna de x2

[12 7 1]

O básico dos arrays NumPy | 45


Machine Translated by Google

In[29]: print(x2[0, :]) # primeira linha de x2

[12 5 2 4]

No caso de acesso a linhas, a fatia vazia pode ser omitida para uma sintaxe mais compacta:

In[30]: print(x2[0]) # equivalente a x2[0, :]

[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]]

Vamos extrair um subarray 2×2 disso:

Em[32]: x2_sub = x2[:2, :2]


imprimir(x2_sub)

[[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.

Criando cópias de arrays

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() :

46 | Capítulo 2: Introdução ao NumPy


Machine Translated by Google

Em[35]: x2_sub_copy = x2[:2, :2].copy()


imprimir(x2_sub_copy)

[[99 5] [7
6]]

Se modificarmos agora este subarray, o array original não será tocado:

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:

Em [38]: grid = np.arange(1, 10).reshape((3, 3)) print(grid)

[[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.

Outro padrão de remodelagem comum é a conversão de uma matriz unidimensional em uma


matriz bidimensional de linha ou coluna. Você pode fazer isso com o método reshape ou, mais
facilmente, usando a palavra-chave newaxis em uma operação de fatia.
ção:

Em[39]: x = np.array([1, 2, 3])

# vetor de linha via


remodelação x.reshape((1, 3))

Fora[39]: array([[1, 2, 3]])

In[40]: # vetor de linha via newaxis


x[np.newaxis, :]

Fora[40]: array([[1, 2, 3]])

O básico dos arrays NumPy | 47


Machine Translated by Google

In[41]: # vetor de coluna via remodelação


x.reshape((3, 1))

Fora[41]: array([[1], [2],


[3]])

In[42]: # vetor de coluna via newaxis x[:,


np.newaxis]

Fora[42]: matriz([[1], [2],


[3]])

Veremos esse tipo de transformação frequentemente ao longo do restante do livro.

Concatenação e divisão de arrays Todas as rotinas

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])

Fora[43]: matriz([1, 2, 3, 3, 2, 1])

Você também pode concatenar mais de dois arrays ao mesmo tempo:

Em[44]: z = [99, 99, 99]


imprimir(np.concatenate([x, y, z]))

[1 2 3 3 2 1 99 99 99]

np.concatenate também pode ser usado para matrizes bidimensionais:

Em [45]: grade = np.array([[1, 2, 3], [4, 5,


6]])

In[46]: # concatenar ao longo do primeiro eixo


np.concatenate([grid, grid])

Fora[46]: matriz([[1, 2, 3], [4, 5,


6], [1, 2, 3],
[4, 5, 6]])

In[47]: # concatenar ao longo do segundo eixo (indexado zero)


np.concatenate([grid, grid], axis=1)

48 | Capítulo 2: Introdução ao NumPy


Machine Translated by Google

Fora[47]: matriz([[1, 2, 3, 1, 2, 3], [4, 5, 6,


4, 5, 6]])

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):

Em [48]: x = np.array([1, 2, 3]) grade


= np.array([[9, 8, 7], [6, 5, 4]])

# empilhar verticalmente os arrays


np.vstack([x, grid])

Fora[48]: matriz([[1, 2, 3], [9, 8,


7], [6, 5, 4]])

In[49]: # empilha horizontalmente os arrays


y = np.array([[99], [99]])

np.hstack([grade, y])

Fora[49]: matriz([[ 9, 8, 7, 99], [ 6, 5, 4,


99]])

Da mesma forma, np.dstack empilhará arrays ao longo do terceiro eixo.

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:

Em[50]: x = [1, 2, 3, 99, 99, 3, 2, 1]


x1, x2, x3 = np.split(x, [3, 5])
imprimir(x1, x2, x3)

[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:

Em [51]: grade = np.arange(16).reshape((4, 4)) grade

Fora[51]: matriz([[ 0, 1, 2, 3], [ 4, 5, 6, 7],


[ 8, 9, 10, 11], [12,
13, 14, 15]])

In[52]: superior, inferior = np.vsplit(grid, [2])


print(superior)
print(inferior)

[[0 1 2 3] [4
5 6 7]]

O básico dos arrays NumPy | 49


Machine Translated by Google

[[ 8 9 10 11] [12
13 14 15]]

In[53]: esquerda, direita = np.hsplit(grid, [2])


print(esquerda)
print(direita)

[[ 0 1] [ 4
5] [ 8 9]
[12 13]]
[[ 2 3] [ 6
7] [10 11]
[14 15]]

Da mesma forma, np.dsplit dividirá os arrays ao longo do terceiro eixo.

Computação em matrizes NumPy: funções universais


Até agora, discutimos alguns dos detalhes básicos do NumPy; nas próximas seções, mergulharemos nos
motivos pelos quais o NumPy é tão importante no mundo da ciência de dados Python. Ou seja, ele fornece
uma interface fácil e flexível para computação otimizada com matrizes de dados.

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.

A lentidão dos loops A

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.

50 | Capítulo 2: Introdução ao NumPy


Machine Translated by Google

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:

In[1]: importe numpy como


np np.random.seed(0)

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

valores = np.random.randint(1, 10, tamanho=5)


computar_recíprocos(valores)

Fora[1]: array([ 0,16666667, 1. , 0,25 , 0,25 , 0,125 ])

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):

Em [2]: big_array = np.random.randint (1, 100, tamanho = 1000000)


%timeit computa_recíprocos(big_array)

1 loop, melhor de 3: 2,91 s por loop

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.

Compare os resultados dos dois seguintes:

Em[3]: print(compute_reciprocals(values)) print(1.0 / values)

Computação em matrizes NumPy: funções universais | 51


Machine Translated by Google

[0,16666667 1. 0,25 0,25 0,125 ]


[0,16666667 1. 0,25 0,25 0,125 ]

Observando o tempo de execução do nosso grande array, vemos que ele completa ordens de
magnitude mais rápida que o loop Python:

In[4]: %timeit (1.0 / big_array)

100 loops, melhor de 3: 4,6 ms por loop

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:

Em[5]: np.arange(5) / np.arange(1, 6)

Fora[5]: array([ 0. , 0,5 , 0,66666667, 0,75 , 0,8 ])

E as operações ufunc não estão limitadas a matrizes unidimensionais – elas podem atuar
matrizes multidimensionais também:

Em[6]: x = np.arange(9).reshape((3, 3))


2 ** x

Fora[6]: array([[ 1, 4], 2,


[8, 16, 32],
[64, 128, 256]])

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.

Explorando UFuncs do NumPy


Ufuncs existem em dois sabores: ufuncs unários, que operam em uma única entrada, e binários
ufuncs, que operam em duas entradas. Veremos exemplos desses dois tipos de funções
aqui.

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)

52 | Capítulo 2: Introdução ao NumPy


Machine Translated by Google

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:

Em[8]: imprimir("-x = ", -x)


imprimir("x **2= ", x ** 2)
imprimir("x % 2 = ", x % 2)

-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

Fora[9]: array([-1. , -2.25, -4. , -6,25])


Todas essas operações aritméticas são simplesmente wrappers convenientes em torno de
funções integradas ao NumPy; por exemplo, o operador + é um wrapper para a adição
função:

Em[10]: np.add(x, 2)

Fora[10]: array([2, 3, 4, 5])


A Tabela 2-2 lista os operadores aritméticos implementados no NumPy.

Tabela 2-2. Operadores aritméticos implementados em NumPy

Operador equivalente ufunc Descrição

+ Adição (por exemplo, 1 + 1 = 2)


np.adicionar

- np.subtrair
Subtração (por exemplo, 3 - 2 = 1)

- np.negativo Negação unária (por exemplo, -2)

* np.multiplicar
Multiplicação (por exemplo, 2 * 3 = 6)

/ np.divide Divisão (por exemplo, 3/2 = 1,5)

// np.floor_divide Divisão de piso (por exemplo, 3 // 2 = 1)


** np.poder
Exponenciação (por exemplo, 2 ** 3 = 8)

%
Módulo/resto (por exemplo, 9% 4 = 1)
por exemplo, mod

Computação em matrizes NumPy: funções universais | 53


Machine Translated by Google

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:

Em[11]: x = np.array([-2, -1, 0, 1, 2]) abs(x)

Fora[11]: array([2, 1, 0, 1, 2])

O ufunc NumPy correspondente é np.absolute, que também está disponível sob o alias np.abs:

Em[12]: np.absolute(x)

Fora[12]: array([2, 1, 0, 1, 2])

Em[13]: np.abs(x)

Fora[13]: array([2, 1, 0, 1, 2])

Este ufunc também pode lidar com dados complexos, nos quais o valor absoluto retorna a magnitude:

Em[14]: x = np.array([3 - 4j, 4 - 3j, 2 + 0j, 0 + 1j]) np.abs(x)

Fora[14]: matriz([ 5., 5., 2., 1.])

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:

Em[15]: theta = np.linspace(0, np.pi, 3)

Agora podemos calcular algumas funções trigonométricas com estes valores:

Em[16]: print("teta = ", theta)


print("sin(theta) = ", np.sin(theta))
print("cos(theta) = ", np.cos(theta))
print("tan(theta) = ", np .tan(teta))
teta = [0,1,57079633 3,14159265] sin(teta) = [0,00000000e+00
1,00000000e+00 1,22464680e-16] cos(teta) = [1,00000000e+00 6,12323400e-17
- 1.00000000e+00] bronzeado(teta ) = [ 0,00000000e+00 1,63312394e+16
-1,22464680e-16]

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:

54 | Capítulo 2: Introdução ao NumPy


Machine Translated by Google

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]

O inverso das exponenciais, os logaritmos, também estão disponíveis. O np.log básico


dá o logaritmo natural; se você preferir calcular o logaritmo de base 2 ou o
logaritmo de base 10, estes também estão disponíveis:

Em[19]: x = [1, 2, 4, 10]


imprimir("x =", x)
imprimir("ln(x) =", np.log(x))
imprimir("log2(x) =", np.log2(x))
imprimir("log10(x) =", np.log10(x))

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:

Em[20]: x = [0, 0,001, 0,01, 0,1]


imprimir("exp(x) - 1 =", np.expm1(x))
imprimir("log(1 + x) =", np.log1p(x))

exp(x) - 1 = [ 0. log(1 + 0,0010005 0,01005017 0,10517092]


x) = [ 0. 0,0009995 0,00995033 0,09531018]

Quando x é muito pequeno, essas funções fornecem valores mais precisos do que se o np.log bruto
ou np.exp foram usados.

Computação em matrizes NumPy: funções universais | 55


Machine Translated by Google

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[21]: de importação especial scipy

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))

gama(x) ln| = [ 1,00000000e+00 2,40000000e+01 3,62880000e+05]


gama(x)| = [ 0. beta (x, 2) = 3.17805383 12.80182748]
[ 0,5 0,03333333 0,00909091]

In[23]: # Função de erro (integral de Gaussiana)


# seu complemento e seu inverso x = np.array([0,
0.3, 0.7, 1.0]) print("erf(x) =", special.erf(x))
print("erfc(x) =", especial.erfc(x)) imprimir("erfinv(x)
=", especial.erfinv(x))

erf (x) = [ 0. erfc (x) 0,32862676 0,67780119 0,84270079] 0,67137324


= [ 1. erfinv (x) = [ 0. 0,32219881 0,15729921] inf]
0,27246271 0,73286908

Existem muitos, muitos outros ufuncs disponíveis no NumPy e no scipy.special.


Como a documentação desses pacotes está disponível online, uma pesquisa na web nos moldes de
“função gama python” geralmente encontrará as informações relevantes.

Recursos avançados do Ufunc

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

56 | Capítulo 2: Introdução ao NumPy


Machine Translated by Google

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)

[0. 10. 20. 30. 40.]

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)

[1. 0. 2. 0. 4. 0. 8. 0. 16. 0.]

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

Se quisermos armazenar todos os resultados intermediários do cálculo, podemos usar acumular:

Em[28]: np.add.accumulate(x)

Fora[28]: array([ 1, 3, 6, 10, 15])

Computação em matrizes NumPy: funções universais | 57


Machine Translated by Google

Em[29]: np.multiply.accumulate(x)

Fora[29]: array([ 1, 2, 6, 24, 120])

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)

Fora[30]: array([[ 1, 2, 3, 4, 5], [ 2, 4, 6, 8, 10],


[ 3, 6, 9, 12, 15], [ 4, 8,
12 , 16, 20], [ 5, 10, 15,
20, 25]])

Os métodos ufunc.at e ufunc.reduceat , que exploraremos em “Indexação sofisticada” na página


78, também são muito úteis.

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).

Ufuncs: Aprendendo mais

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.

Agregações: mínimo, máximo e tudo mais


Muitas vezes, quando você se depara com uma grande quantidade de dados, o primeiro passo
é calcular estatísticas resumidas para os dados em questão. Talvez as estatísticas resumidas
mais comuns sejam a média e o desvio padrão, que permitem resumir os valores “típicos” em
um conjunto de dados, mas outros agregados também são úteis (a soma, o produto, a mediana,
o mínimo e o máximo, os quantis, etc.).

58 | Capítulo 2: Introdução ao NumPy


Machine Translated by Google

NumPy possui funções de agregação integradas rápidas para trabalhar em arrays; discutiremos e
demonstraremos alguns deles aqui.

Somando os valores em uma matriz Como

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 :

In[1]: importar numpy como np

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

No entanto, como executa a operação em código compilado, a versão da operação do NumPy é


calculada muito mais rapidamente:

Em [4]: big_array = np.random.rand (1000000)


%timeit soma(big_array)
%timeit np.sum(big_array)

10 loops, melhor de 3: 104 ms por loop 1000


loops, melhor de 3: 442 µs por loop

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:

Em[5]: min(big_array), max(big_array)

Fora[5]: (1,1717128136634614e-06, 0,9999976784968716)

As funções correspondentes do NumPy têm sintaxe semelhante e operam novamente com muito
mais rapidez:

Em[6]: np.min(big_array), np.max(big_array)

Fora[6]: (1,1717128136634614e-06, 0,9999976784968716)

Agregações: mínimo, máximo e tudo mais | 59


Machine Translated by Google

Em[7]: %timeit min(big_array)


%timeit np.min(big_array)

10 loops, melhor de 3: 82,3 ms por loop 1.000


loops, melhor de 3: 497 µs por loop

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[9]: M = np.random.random((3, 4))


imprimir(M)

[[0,8967576 0,03783739 0,75952519 0,06682827] [0,8354065


0,99196818 0,19544769 0,43447084] [0,66859307
0,15038721 0,37911 423 0,6687194]]

Por padrão, cada função de agregação NumPy retornará a agregação de todo o


variedade:

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)

Fora[11]: array([0,66859307, 0,03783739, 0,19544769, 0,06682827])

A função retorna quatro valores, correspondentes às quatro colunas de números.

Da mesma forma, podemos encontrar o valor máximo dentro de cada linha:

In[12]: M.max(eixo=1)

Fora[12]: array([ 0,8967576 , 0,99196818, 0,6687194])

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

60 | Capítulo 2: Introdução ao NumPy


Machine Translated by Google

primeiro eixo será recolhido: para matrizes bidimensionais, isso significa que os valores dentro
cada coluna será agregada.

Outras funções de agregação

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.

Tabela 2-3. Funções de agregação disponíveis em NumPy


Nome da Função Versão segura para NaN Descrição

np.soma np.nansum Calcular soma de elementos

np.prod np.nanprod Calcular produto de elementos

np.mean np.nanmean Calcular mediana de elementos

np.std np.nanstd Calcular desvio padrão

np.var np.número Variação computacional

Encontre o valor mínimo


np min np.nanmin
Encontre o valor máximo
np.max np.nanmax
Encontre o índice de valor mínimo
por exemplo, argmin np.nanargmin
Encontre o índice de valor máximo
np.argmax np.nanargmax

np.mediana np.nanmedian Calcular mediana de elementos

np.percentile np.nanpercentile Calcula estatísticas de elementos baseadas em classificação

N/D Avalie se algum elemento é verdadeiro


np.qualquer

N/D Avalie se todos os elementos são verdadeiros


np.tudo

Veremos esses agregados frequentemente ao longo do restante do livro.

Exemplo: Qual é a altura média dos presidentes dos EUA?


Agregados disponíveis em NumPy podem ser extremamente úteis para resumir um conjunto de valores
sim. Como exemplo simples, consideremos a altura de todos os presidentes dos EUA. Esses dados são
disponível no arquivo president_heights.csv, que é uma lista simples separada por vírgulas de
rótulos e valores:

Em[13]: !head -4 dados/president_heights.csv

ordem, nome, altura (cm)


1, George Washington, 189

Agregações: mínimo, máximo e tudo mais | 61


Machine Translated by Google

2, John Adams, 170


3, Thomas Jefferson, 189

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):

Em [14]: importar pandas como pd


data = pd.read_csv('data/president_heights.csv') heights =
np.array(data['height(cm)']) print(heights)

[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:

In[15]: print(" Altura média: ", alturas.mean())


print("Desvio padrão:", heights.std()) print("Altura mínima: ",
heights.min()) print("Altura máxima: ", heights.max())

Altura média: 179.738095238


Desvio padrão: 6,93184344275 Altura mínima:
Altura máxima: 163
193

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:

Em [16]: print("25º percentil: ", np.percentile(heights, 25)) print("Mediana: ", np.median(heights))


print("75th percentil: ", np.percentile(heights, 75))

25º percentil: 174,25


Mediana: 182,0
percentil 75: 183,0

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[17]: %matplotlib embutido


import matplotlib.pyplot como plt import
seaborn; seaborn.set() # define o estilo do gráfico

Em[18]: plt.hist(alturas)
plt.title(' Distribuição de altura dos presidentes dos EUA') plt.xlabel('altura
(cm)') plt.ylabel('número');

62 | Capítulo 2: Introdução ao NumPy


Machine Translated by Google

Figura 2-3. Histograma de alturas presidenciais

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.

Computação em matrizes: transmissão


Vimos na seção anterior como as funções universais do NumPy podem ser usadas para vetorizar
operações e, assim, remover loops lentos do Python. Outro meio de vetorizar operações é usar a
funcionalidade de transmissão do NumPy. A transmissão é simplesmente um conjunto de regras
para aplicar funções binárias (adição, subtração, multiplicação, etc.) em matrizes de tamanhos
diferentes.

Apresentando a radiodifusão
Lembre-se de que para matrizes do mesmo tamanho, as operações binárias são executadas
elemento por elemento:

In[1]: importar numpy como np

Em[2]: a = np.array([0, 1, 2]) b =


np.array([5, 5, 5]) a + b

Fora[2]: array([5, 6, 7])

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:

Computação em Arrays: Transmissão | 63


Machine Translated by Google

Em[3]: a + 5

Fora[3]: array([5, 6, 7])

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[4]: M = np.ones((3, 3))


M

Fora[4]: array([[ 1., 1., 1.], [ 1., 1., 1.],


[ 1., 1., 1.]])

Em[5]: M + a

Fora[5]: array([[ 1., 2., 3.], [ 1., 2., 3.],


[ 1., 2., 3.]])

Aqui, a matriz unidimensional a é esticada, ou transmitida, através da segunda dimensão para


corresponder à forma de M.

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

Fora[7]: array([[0, 1, 2], [1, 2,


3], [2, 3, 4]])

64 | Capítulo 2: Introdução ao NumPy


Machine Translated by Google

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.

Figura 2-4. Visualização da transmissão NumPy

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.

Computação em Arrays: Transmissão | 65


Machine Translated by Google

Para deixar essas regras claras, vamos considerar alguns exemplos detalhadamente.

Exemplo de transmissão 1

Vejamos como adicionar um array bidimensional a um array unidimensional:

Em[8]: M = np.ones((2, 3)) a =


np.arange(3)

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:

Forma M -> (2, 3)


forma.a -> (1, 3)

Pela regra 2, vemos agora que a primeira dimensão discorda, então ampliamos esta dimensão para
corresponder:

Forma M -> (2, 3)


forma.a -> (2, 3)

As formas coincidem e vemos que a forma final será (2, 3):

Em[9]: M + a

Fora[9]: matriz([[ 1., 2., 3.], [ 1., 2., 3.]])

Exemplo de transmissão 2

Vamos dar uma olhada em um exemplo onde ambos os arrays precisam ser transmitidos:

Em[10]: a = np.arange(3).reshape((3, 1)) b =


np.arange(3)

Novamente, começaremos escrevendo a forma dos arrays:

a.forma = (3, 1)
b.forma = (3,)

66 | Capítulo 2: Introdução ao NumPy


Machine Translated by Google

A regra 1 diz que devemos preencher a forma de b com unidades:

a.forma -> (3, 1)


b.forma -> (1, 3)

E a regra 2 nos diz que atualizamos cada um deles para corresponder ao tamanho correspondente do outro array:

a.forma -> (3, 3)


b.forma -> (3, 3)

Como o resultado corresponde, essas formas são compatíveis. Podemos ver isso aqui:

Em[11]: a + b

Fora[11]: array([[0, 1, 2], [1, 2,


3], [2, 3, 4]])

Exemplo de transmissão 3

Agora vamos dar uma olhada em um exemplo em que os dois arrays não são compatíveis:

Em[12]: M = np.ones((3, 2)) a =


np.arange(3)

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:

Forma M -> (3, 2)


forma.a -> (1, 3)

Pela regra 2, a primeira dimensão de a é esticada para corresponder à de M:

Forma M -> (3, 2)


forma.a -> (3, 3)

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:

Computação em Arrays: Transmissão | 67


Machine Translated by Google

Em[13]: M + a
-------------------------------------------------- -------------------------

Erro de valor Traceback (última chamada mais recente)

<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):

Em[14]: a[:, np.newaxis].shape

Fora[14]: (3, 1)

Em[15]: M + a[:, np.newaxis]

Fora[15]: array([[ 1., 1.], [ 2., 2.],


[ 3., 3.]])

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:

Em[16]: np.logaddexp(M, a[:, np.newaxis])

Fora [16]: matriz ([[ 1,31326169, 1,31326169],


[1,69314718, 1,69314718],
[2,31326169, 2,31326169]])

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-

68 | Capítulo 2: Introdução ao NumPy


Machine Translated by Google

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:

Em[17]: X = np.random.random((10, 3))

Podemos calcular a média de cada recurso usando a média agregada na primeira dimensão:

Em[18]: Xmédia = X.média(0)


Xmean

Fora[18]: array([0,53514715, 0,66567217, 0,44385899])

E agora podemos centralizar o array X subtraindo a média (esta é uma operação de transmissão):

Em[19]: X_centrado = X - Xmédio

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)

Fora [20]: matriz ([2.22044605e-17, -7.77156117e-17, -1.66533454e-17])

Para precisão dentro da máquina, a média agora é zero.

Traçando uma função bidimensional

Um lugar onde a transmissão é muito útil é na exibição de imagens baseadas em funções


bidimensionais. Se quisermos definir uma função z = f(x, y), a transmissão pode ser usada para
calcular a função através da grade:

In[21]: # xey têm 50 passos de 0 a 5 x = np.linspace(0,


5, 50) y = np.linspace(0, 5, 50)
[:, np.newaxis]

z = np.sin(x) ** 10 + np.cos(10 + y * x) * np.cos(x)

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

Em[23]: plt.imshow(z, origin='inferior', extensão=[0, 5, 0, 5],


cmap='viridis')
plt.colorbar();

O resultado, mostrado na Figura 2-5, é uma visualização convincente da função bidimensional.

Computação em Arrays: Transmissão | 69


Machine Translated by Google

Figura 2-5. Visualização de um array 2D

Comparações, máscaras e lógica booleana


Esta seção cobre o uso de máscaras booleanas para examinar e manipular valores em arrays
NumPy. O mascaramento surge quando você deseja extrair, modificar, contar ou manipular valores
em uma matriz com base em algum critério: por exemplo, você pode querer contar todos os valores
maiores que um determinado valor, ou talvez remover todos os valores discrepantes que são acima
de algum limite. No NumPy, o mascaramento booleano costuma ser a maneira mais eficiente de
realizar esses tipos de tarefas.

Exemplo: Contando dias chuvosos

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):

In[1]: importar numpy como


np importar pandas como pd

# 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,)

A matriz contém 365 valores, fornecendo precipitação diária em polegadas de 1º de janeiro a 31 de


dezembro de 2014.

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):

70 | Capítulo 2: Introdução ao NumPy


Machine Translated by Google

Em [2]: % matplotlib
importação inline matplotlib.pyplot
as plt import seaborn; seaborn.set() # define estilos de plotagem

In[3]: plt.hist(polegadas, 40);

Figura 2-6. Histograma das chuvas de 2014 em Seattle

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.

Operadores de comparação como ufuncs

Em “Computação em matrizes NumPy: funções universais” na página 50 , apresentamos ufuncs e


nos concentramos em particular em operadores aritméticos. Vimos que usando +, -, *, /,

Comparações, máscaras e lógica booleana | 71


Machine Translated by Google

e outros em matrizes levam a operações elemento a elemento. NumPy também implementa


operadores de comparação como < (menor que) e > (maior que) como ufuncs elemento a elemento.
O resultado desses operadores de comparação é sempre um array com tipo de dados booleano.
Todas as seis operações de comparação padrão estão disponíveis:

Em[4]: x = np.array([1, 2, 3, 4, 5])

In[5]: x < 3 # menor que

Out[5]: array([Verdadeiro, Verdadeiro, Falso, Falso, Falso], dtype=bool)

In[6]: x > 3 # maior que

Out[6]: array([Falso, Falso, Falso, Verdadeiro, Verdadeiro], dtype=bool)

In[7]: x <= 3 # menor ou igual

Out[7]: array([Verdadeiro, Verdadeiro, Verdadeiro, Falso, Falso], dtype=bool)

In[8]: x >= 3 # maior ou igual

Out[8]: array([Falso, Falso, Verdadeiro, Verdadeiro, Verdadeiro], dtype=bool)

In[9]: x != 3 # diferente

Out[9]: array([Verdadeiro, Verdadeiro, Falso, Verdadeiro, Verdadeiro], dtype=bool)

Em[10]: x == 3 # igual

Out[10]: array([Falso, Falso, Verdadeiro, Falso, Falso], dtype=bool)

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)

Out[11]: array([Falso, Verdadeiro, Falso, Falso, Falso], dtype=bool)

Como no caso dos operadores aritméticos, os operadores de comparação são implementados


como ufuncs no NumPy; por exemplo, quando você escreve x < 3, internamente o NumPy usa
np.less(x, 3). Um resumo dos operadores de comparação e seu equivalente ufunc é mostrado
aqui:

Operador equivalente ufunc


== np.equal
!= np.not_equal
< np.less
<= np.less_equal
> np.greater
>= np.greater_equal

Assim como no caso dos ufuncs aritméticos, eles funcionarão em arrays de qualquer tamanho e
formato. Aqui está um exemplo bidimensional:

72 | Capítulo 2: Introdução ao NumPy


Machine Translated by Google

Em[12]: rng = np.random.RandomState(0)


x = rng.randint(10, tamanho=(3, 4))
x

Fora[12]: matriz([[5, 0, 3, 3], [7, 9, 3,


5], [2, 4, 7, 6]])

Em[13]: x < 6

Out[13]: array([[ Verdadeiro, Verdadeiro, Verdadeiro, Verdadeiro],


[Falso, Falso, Verdadeiro, Verdadeiro],
[Verdadeiro, Verdadeiro, Falso, Falso]], dtype=bool)

Em cada caso, o resultado é um array booleano, e o NumPy fornece vários padrões simples
para trabalhar com esses resultados booleanos.

Trabalhando com arrays 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:

In[15]: # quantos valores menores que 6?


np.count_nonzero(x < 6)

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:

Em[16]: np.soma(x < 6)

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:

In[17]: # quantos valores menores que 6 em cada linha? np.sum(x


< 6, eixo=1)

Fora[17]: array([4, 2, 2])

Isso conta o número de valores menores que 6 em cada linha da matriz.

Comparações, máscaras e lógica booleana | 73


Machine Translated by Google

Se estivermos interessados em verificar rapidamente se algum ou todos os valores são verdadeiros,


podemos usar (você adivinhou) np.any() ou np.all():

In[18]: # existem valores maiores que 8? np.qualquer(x >


8)

Fora[18]: Verdadeiro

In[19]: # existem valores menores que zero? np.qualquer(x


< 0)

Fora[19]: Falso

In[20]: # todos os valores são menores que 10?


np.todos(x < 10)

Fora[20]: Verdadeiro

In[21]: # todos os valores são iguais a 6?


np.todos(x == 6)

Fora[21]: Falso

np.all() e np.any() também podem ser usados ao longo de eixos específicos. Por exemplo:

In[22]: # todos os valores em cada linha são menores que 8?


np.all(x < 8, eixo=1)

Out[22]: array([Verdadeiro, Falso, Verdadeiro], dtype=bool)

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:

In[23]: np.sum((polegadas > 0,5) & (polegadas < 1))

Fora[23]: 29

Então vemos que há 29 dias com precipitação entre 0,5 e 1,0 polegadas.

74 | Capítulo 2: Introdução ao NumPy


Machine Translated by Google

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:

polegadas > (0,5 e polegadas) < 1

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:

In[24]: np.sum(~( (polegadas <= 0,5) | (polegadas >= 1) ))

Fora[24]: 29

A combinação de operadores de comparação e operadores booleanos em matrizes pode levar a uma


ampla gama de operações lógicas eficientes.

A tabela a seguir resume os operadores booleanos bit a bit e seus ufuncs equivalentes:

Operador equivalente ufunc


& np.bitwise_and

| 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)))

Número de dias sem chuva: 215


Número de dias com chuva: 150
Dias com mais de 0,5 polegadas: 37 Dias
chuvosos com <0,1 polegadas: 75

Matrizes Booleanas como

Máscaras Na seção anterior, examinamos agregados calculados diretamente em matrizes booleanas.


Um padrão mais poderoso é usar matrizes booleanas como máscaras, para selecionar subconjuntos
específicos dos próprios dados. Voltando ao nosso array x anterior, suponha que queremos um array
com todos os valores do array que sejam menores que, digamos, 5:

Comparações, máscaras e lógica booleana | 75


Machine Translated by Google

Em[26]: x

Fora[26]: matriz([[5, 0, 3, 3], [7, 9, 3, 5],


[2, 4, 7, 6]])

Podemos obter facilmente um array booleano para esta condição, como já vimos:

Em[27]: x < 5

Out[27]: array([[Falso, Verdadeiro, Verdadeiro, Verdadeiro],


[Falso, Falso, Verdadeiro, Falso],
[Verdadeiro, Verdadeiro, Falso, Falso]], dtype=bool)

Agora, para selecionar esses valores do array, podemos simplesmente indexar neste array booleano; isso é conhecido
como operação de mascaramento:

Em[28]: x[x < 5]

Fora[28]: array([0, 3, 3, 3, 2, 4])

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)

print("Precipitação média em dias chuvosos em 2014 (polegadas): ",


np.median(polegadas[chuvoso]))
print("Precipitação média nos dias de verão de 2014 (polegadas): ",
np.median(polegadas[verão]))
print("Precipitação máxima em dias de verão em 2014 (polegadas): ",
np.max(polegadas[verão]))
print("Precipitação média em dias chuvosos fora do verão (polegadas):",
np.median(polegadas[chuvoso & ~ verão]))

Precipitação mediana em dias chuvosos em 2014 (polegadas): 0,194881889764


Precipitação mediana em dias de verão em 2014 (polegadas): 0,0
Precipitação máxima em dias de verão em 2014 (polegadas): 0,850393700787
Precipitação mediana em dias chuvosos fora do verão (polegadas): 0,200787401575

Ao combinar operações booleanas, operações de mascaramento e agregações, podemos responder rapidamente a


esses tipos de perguntas para nosso conjunto de dados.

76 | Capítulo 2: Introdução ao NumPy


Machine Translated by Google

Usando as palavras-chave e/ou versus os operadores &/| Um


ponto comum de confusão é a diferença entre as palavras-chave e e ou por um lado, e os
operadores & e | por outro lado. Quando você usaria um em vez do outro?

A diferença é esta: e e ou avalia a verdade ou falsidade de todo o objeto, enquanto & e |


referem-se a bits dentro de cada objeto.

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[30]: bool(42), bool(0)

Fora[30]: (Verdadeiro, Falso)

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'

Em[35]: am(42 e 59)

Fora[35]: '0b101010'

Em[36]: bin(42 | 59)

Fora[36]: '0b111011'

Observe que os bits correspondentes da representação binária são comparados para


produzir o resultado.

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:

Em[37]: A = np.array([1, 0, 1, 0, 1, 0], dtype=bool)


B = np.array([1, 1, 1, 0, 1, 1], dtype=bool)
Um | B

Out[37]: array([Verdadeiro, Verdadeiro, Verdadeiro, Falso, Verdadeiro, Verdadeiro], dtype=bool)

Comparações, máscaras e lógica booleana | 77


Machine Translated by Google

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
-------------------------------------------------- -------------------------

Erro de valor Traceback (última chamada mais recente)

<ipython-input-38-5d8e4f2e21c0> em <módulo>()
----> 1 A ou B

ValueError: O valor verdade de um array com mais de um elemento é...

Da mesma forma, ao fazer uma expressão booleana em um determinado array, você deve usar | ou
& em vez de ou ou e:

Em[39]: x = np.arange(10) (x > 4)


& (x < 8)

Out[39]: array([Falso, Falso, ..., Verdadeiro, Verdadeiro, Falso, Falso], dtype=bool)

Tentar avaliar a verdade ou falsidade de todo o array dará o mesmo


ValueError que vimos anteriormente:

Em[40]: (x > 4) e (x < 8)


-------------------------------------------------- -------------------------

Erro de valor Traceback (última chamada mais recente)

<ipython-input-40-3d24f1ffd63d> em <module>() ----> 1 (x >


4) e (x < 8)

ValueError: O valor verdade de um array com mais de um elemento é...

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.

78 | Capítulo 2: Introdução ao NumPy


Machine Translated by Google

Explorando a indexação

sofisticada A indexação sofisticada é conceitualmente simples: significa passar um array de índices


para acessar vários elementos do array de uma só vez. Por exemplo, considere a seguinte matriz:

In[1]: importe numpy como


np rand = np.random.RandomState(42)

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:

Em[2]: [x[3], x[7], x[2]]

Fora[2]: [71, 86, 14]

Alternativamente, podemos passar uma única lista ou array de índices para obter o mesmo resultado:

Em[3]: ind = [3, 7, 4] x[ind]

Fora[3]: array([71, 86, 60])

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:

Em[4]: ind = np.array([[3, 7], [4, 5]])

x[ind]

Fora[4]: array([[71, 86], [60,


20]])

A indexação sofisticada também funciona em múltiplas dimensões. Considere a seguinte matriz:

Em[5]: X = np.arange(12).reshape((3, 4))


X

Fora[5]: array([[ 0, 1, 2, 3], [ 4, 5, 6, 7],


[ 8, 9, 10, 11]])

Assim como na indexação padrão, o primeiro índice refere-se à linha e o segundo à coluna:

Em [6]: linha = np.array([0, 1, 2]) col =


np.array([2, 1, 3])
X[linha, coluna]

Fora[6]: array([ 2, 5, 11])

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:

Em[7]: X[linha[:, np.newaxis], col]

Fora[7]: array([[ 2, 1, 3], [ 6, 5, 7],


[10, 9, 11]])

Aqui, cada valor de linha corresponde a cada vetor de coluna, exatamente como vimos na transmissão de operações
aritméticas. Por exemplo:

Em[8]: linha[:, np.newaxis] * col

Fora[8]: array([[0, 0, 0], [2, 1,


3], [4, 2, 6]])

É 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]]

Podemos combinar índices sofisticados e simples:

Em[10]: X[2, [2, 0, 1]]

Fora[10]: array([10, 8, 9])

Também podemos combinar indexação sofisticada com fatiamento:

Em[11]: X[1:, [2, 0, 1]]

Fora[11]: matriz([[ 6, 4, 5],


[10, 8, 9]])

E podemos combinar indexação sofisticada com mascaramento:

Em [12]: máscara = np.array([1, 0, 1, 0], dtype=bool)


X[linha[:, np.newaxis], máscara]

Fora[12]: matriz([[ 0, 2], [ 4, 6],


[ 8, 10]])

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.

80 | Capítulo 2: Introdução ao NumPy


Machine Translated by Google

Exemplo: Seleção de pontos aleatórios


Um uso comum da indexação sofisticada é a seleção de subconjuntos de linhas de uma matriz.
Por exemplo, poderíamos ter uma matriz N por D representando N pontos em dimensões D, como os seguintes
pontos extraídos de uma distribuição normal bidimensional.
ção:

In[13]: média = [0, 0] cov


= [[1, 2], [2, 5]]

X = rand.multivariate_normal(média, cov, 100)


X.forma

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):

Em[14]: %matplotlib embutido


import matplotlib.pyplot como plt
import seaborn; seaborn.set() # para estilo de plotagem

dispersão(X[:, 0], X[:, 1]);

Figura 2-7. Pontos normalmente distribuídos

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:

In[15]: índices = np.random.choice(X.shape[0], 20, replace=False)


índices

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

In[16]: seleção = X[indices] # indexação sofisticada aqui


seleção.forma

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):

Em[17]: plt.scatter(X[:, 0], X[:, 1], alfa=0,3)


plt.scatter(seleção[:, 0], seleção[:, 1], facecolor='nenhum',
s=200);

Figura 2-8. Seleção aleatória entre pontos

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.

Modificando valores com indexação sofisticada Assim

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:

82 | Capítulo 2: Introdução ao NumPy


Machine Translated by Google

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.

É justo, mas considere esta operação:

Em[21]: i = [2, 3, 3, 4, 4, 4] x[i] += 1

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)

# calcula um histograma manualmente


bins = np.linspace(-5, 5, 20) counts =
np.zeros_like(bins)

# encontre o compartimento apropriado para cada


x i = np.searchsorted(bins, x)

# adicione 1 a cada um desses


compartimentos np.add.at(counts, i, 1)

As contagens agora refletem o número de pontos dentro de cada compartimento – em outras palavras,
um histograma (Figura 2-9):

In[24]: # traça os resultados


plt.plot(bins, counts, linestyle='steps');

Figura 2-9. Um histograma calculado manualmente

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:

plt.hist(x, bins, histtype='passo');

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:

In[25]: print("Rotina NumPy:") %timeit


conta, bordas = np.histogram(x, bins)

84 | Capítulo 2: Introdução ao NumPy


Machine Translated by Google

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:

In[1]: importar numpy como np

def seleção_sort(x): para i


no intervalo(len(x)): swap =
i + np.argmin(x[i:]) (x[i], x[swap])
= (x[swap], x [eu])
retornar x

Em[2]: x = np.array([2, 1, 4, 3, 5])


seleção_sort(x)

Fora[2]: array([1, 2, 3, 4, 5])


Como qualquer estudante de ciência da computação do primeiro ano lhe dirá, a classificação por seleção é útil por
sua simplicidade, mas é lenta demais para ser útil para matrizes maiores. Para uma lista de N valores, são
necessários N loops, cada um dos quais faz na ordem de ~ N comparações para encontrar o valor de troca. Em
termos da notação “Big-O” frequentemente usada para caracterizar esses algoritmos (veja “Notação Big-O” na
2
página 92), a média da classificação por seleção é N : se você dobrar o número de itens na lista, o o tempo de
execução aumentará cerca de um fator de quatro.

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:

Em[3]: def bogosort(x):


enquanto np.any(x[:-1] > x[1:]):
np.random.shuffle(x)
retornar x

Em[4]: x = np.array([2, 1, 4, 3, 5]) bogosort(x)

Fora[4]: array([1, 2, 3, 4, 5])


Esse método bobo de classificação depende do puro acaso: ele aplica repetidamente um embaralhamento
aleatório da matriz até que o resultado seja classificado. Com uma escala média de N × N! (isso é N vezes N
fatorial), isso - obviamente - nunca deveria ser usado para qualquer cálculo real.

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.

Classificação rápida em NumPy: np.sort e np.argsort Embora o Python

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

86 | Capítulo 2: Introdução ao NumPy


Machine Translated by Google

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:

Em[5]: x = np.array([2, 1, 4, 3, 5]) np.sort(x)

Fora[5]: array([1, 2, 3, 4, 5])

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:

Em[7]: x = np.array([2, 1, 4, 3, 5])


eu = np.argsort(x)
imprimir(eu)

[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]

Fora[8]: array([1, 2, 3, 4, 5])

Classificando ao longo de

linhas ou colunas Um recurso útil dos algoritmos de classificação do NumPy é a capacidade de


classificar linhas ou colunas específicas de uma matriz multidimensional usando o argumento do eixo . Por exemplo:

Em[9]: rand = np.random.RandomState(42)


X = rand.randint(0, 10, (4, 6))
imprimir(X)

[[6 3 7 4 6 9] [2
6 7 4 3 7] [7 2
5 4 1 7] [5 1 4
0 9 5]]

In[10]: # classifica cada coluna de X


np.sort(X, axis=0)

Fora[10]: matriz([[2, 1, 4, 0, 1, 5], [5, 2, 5,


4, 3, 7],

Classificando matrizes | 87
Machine Translated by Google

[6, 3, 7, 4, 6, 7], [7, 6,


7, 4, 9, 9]])

In[11]: # classifica cada linha de X


np.sort(X, axis=1)

Fora[11]: matriz([[3, 4, 6, 6, 7, 9], [2, 3, 4,


6, 7, 7], [1, 2, 4, 5, 7,
7], [0, 1, 4, 5, 5, 9]])

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!

Classificações parciais: particionamento

À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:

Em[12]: x = np.array([7, 2, 3, 1, 6, 5, 4]) np.partition(x,


3)

Fora[12]: array([2, 1, 3, 4, 6, 5, 7])

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:

Em[13]: np.partition(X, 2, eixo=1)

Fora[13]: matriz([[3, 4, 6, 7, 6, 9], [2, 3, 4,


7, 6, 7], [1, 2, 4, 5, 7,
7], [0, 1, 4, 5, 9, 5]])

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.

Exemplo: k-vizinhos mais próximos Vamos ver

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

88 | Capítulo 2: Introdução ao NumPy


Machine Translated by Google

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):

Em[15]: %matplotlib embutido


import matplotlib.pyplot como plt
import seaborn; seaborn.set() # Estilo de plotagem
plt.scatter(X[:, 0], X[:, 1], s=100);

Figura 2-10. Visualização de pontos no exemplo de k-vizinhos

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:

Em[16]: dist_sq = np.sum((X[:,np.newaxis,:] - X[np.newaxis,:,:]) ** 2, eixo=-1)

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:

In[17]: # para cada par de pontos, calcule as diferenças em suas coordenadas


diferenças = X[:, np.newaxis, :] - X[np.newaxis, :, :] diferenças.forma

Fora[17]: (10, 10, 2)

Classificando matrizes | 89
Machine Translated by Google

In[18]: # eleve ao quadrado as diferenças de


coordenadas sq_differences = diferenças
** 2 sq_differences.shape

Fora[18]: (10, 10, 2)

In[19]: # soma as diferenças de coordenadas para obter a distância quadrada


dist_sq = sq_differences.sum(-1)
dist_sq.shape

Fora[19]: (10, 10)

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:

In[21]: mais próximo = np.argsort(dist_sq, axis=1)


print(mais próximo)

[[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):

90 | Capítulo 2: Introdução ao NumPy


Machine Translated by Google

Em[23]: plt.scatter(X[:, 0], X[:, 1], s=100)

#desenha linhas de cada ponto até seus dois vizinhos mais próximos
K=2

for i in range(X.shape[0]): for j in


next_partition[i, :K+1]: # trace uma linha de
X[i] para X[j] # use um pouco de magia
zip para fazer isso acontecer: plt.plot(*zip(X[j],
X[i]), color='preto')

Figura 2-11. Visualização dos vizinhos de cada ponto

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

2 do que o N do algoritmo de força bruta. Um exemplo disso é a KD-Tree,


implementado no Scikit-Learn.

Notação Big-O A

notação Big-O é um meio de descrever como o número de operações necessárias para um


algoritmo aumenta à medida que a entrada aumenta de tamanho. Usá-lo corretamente é
mergulhar profundamente no domínio da teoria da ciência da computação e distingui-lo
cuidadosamente da notação O pequeno, da notação ÿ grande, da notação ÿ grande e
provavelmente de muitos de seus híbridos mutantes. Embora essas distinções acrescentem
precisão às afirmações sobre escalonamento algorítmico, fora dos exames de teoria da ciência
da computação e dos comentários de comentaristas pedantes de blogs, você raramente verá
tais distinções feitas na prática. Muito mais comum no mundo da ciência de dados é o uso menos
rígido da notação big-O: como uma descrição geral (embora imprecisa) da escala de um
algoritmo. Com desculpas aos teóricos e pedantes, esta é a interpretação que usaremos ao longo deste livro.

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.

Para nossos propósitos, o N geralmente indicará algum aspecto do tamanho do conjunto de


dados (o número de pontos, o número de dimensões, etc.). Ao tentar analisar bilhões ou trilhões
2 pode estar longe de ser trivial!
de amostras, a diferença entre N e N

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 estruturados: matrizes estruturadas do NumPy


Embora muitas vezes nossos dados possam ser bem representados por uma matriz homogênea
de valores, às vezes não é esse o caso. Esta seção demonstra o uso de arrays estruturados e
arrays de registros do NumPy, que fornecem armazenamento eficiente para dados compostos e hetero.

92 | Capítulo 2: Introdução ao NumPy


Machine Translated by Google

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:

Em [2]: nome = ['Alice', 'Bob', 'Cathy', 'Doug'] idade = [25,


45, 37, 19] peso = [55,0,
85,5, 68,0, 61,5]

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:

Em[3]: x = np.zeros(4, dtype=int)

Da mesma forma, podemos criar um array estruturado usando uma especificação de tipo de dados composto:

In[4]: # Use um tipo de dados composto para matrizes estruturadas


dados = np.zeros(4, dtype={'nomes':('nome', 'idade', 'peso'),
'formatos':('U10', 'i4', 'f8')})
imprimir (dados.dtype)

[('nome', '<U10'), ('idade', '<i4'), ('peso', '<f8')]

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:

In[5]: dados['nome'] = nome


dados['idade'] =
dados de idade ['peso'] =
peso imprimir(dados)

[('Alice', 25, 55,0) ('Bob', 45, 85,5) ('Cathy', 37, 68,0)


('Doug', 19, 61,5)]

Como esperávamos, os dados estão agora organizados em um bloco conveniente de


memória.

O que é útil com arrays estruturados é que agora você pode se referir a valores por índice ou por nome:

In[6]: # Obtenha todos os dados


de nomes['name']

Dados estruturados: matrizes estruturadas do NumPy | 93


Machine Translated by Google

Fora[6]: array(['Alice', 'Bob', 'Cathy', 'Doug'], dtype='<U10')

In[7]: # Obtenha a primeira linha de


dados data[0]

Fora[7]: ('Alice', 25, 55,0)

In[8]: # Obtenha o nome da última linha data[-1]


['name']

Fora[8]: 'Doug'

Usando máscara booleana, isso permite até mesmo realizar algumas operações mais sofisticadas, como filtrar por
idade:

In[9]: # Obtenha nomes com idade inferior a 30


data[data['age'] < 30]['name']

Fora[9]: array(['Alice', 'Doug'],


dtype='<U10')

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.

Criando matrizes estruturadas Os tipos

de dados de matrizes estruturadas podem ser especificados de diversas maneiras. Anteriormente, vimos o método
do dicionário:

Em [10]: np.dtype({'nomes':('nome', 'idade', 'peso'), 'formatos':


('U10', 'i4', 'f8')})

Fora[10]: dtype([('nome', '<U10'), ('idade', '<i4'), ('peso', '<f8')])

Para maior clareza, os tipos numéricos podem ser especificados com tipos Python ou dtypes NumPy :

Em [11]: np.dtype({'nomes':('nome', 'idade', 'peso'), 'formatos':


((np.str_, 10), int, np.float32)})

Fora[11]: dtype([('nome', '<U10'), ('idade', '<i8'), ('peso', '<f4')])

Um tipo composto também pode ser especificado como uma lista de tuplas:

Em [12]: np.dtype([('nome', 'S10'), ('idade', 'i4'), ('peso', 'f8')])

Fora[12]: dtype([('nome', 'S10'), ('idade', '<i4'), ('peso', '<f8')])

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')

94 | Capítulo 2: Introdução ao NumPy


Machine Translated by Google

Fora[13]: dtype([('f0', 'S10'), ('f1', '<i4'), ('f2', '<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.

Tabela 2-4. Tipos de dados NumPy

Descrição do personagem Exemplo

'b' Byte np.dtype('b')


'eu' Inteiro assinado np.dtype('i4') == np.int32
'em' Inteiro não np.dtype('u1') == np.uint8
'f' assinado np.dtype('f8') == np.int64
'c' Ponto flutuante Ponto flutuante complexo np.dtype('c16') == np.complex128

'S', 'a' string np.dtype('S5')


'EM' Unicode np.dtype('U') == np.str_
'EM' Dados brutos (nulos) np.dtype('V') == np.void

Tipos de compostos mais avançados

É 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:

Em [14]: tp = np.dtype([('id', 'i8'), ('mat', 'f8', (3, 3))])


X = np.zeros(1, dtype=tp)
imprimir(X[0])
imprimir(X['mat'][0])

(0, [[0,0, 0,0, 0,0], [0,0, 0,0, 0,0], [0,0, 0,0, 0,0]])
[[ 0. 0. 0.]
[0.0.0.]
[0.0.0.]]

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!

Dados estruturados: matrizes estruturadas do NumPy | 95


Machine Translated by Google

RecordArrays: Structured Arrays with a Twist NumPy

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']

Fora[15]: array([25, 45, 37, 19], dtype=int32)

Se visualizarmos nossos dados como uma matriz de registros, poderemos acessá-los com um pouco menos de
pressionamentos de tecla:

Em [16]: data_rec = data.view(np.recarray)


data_rec.age

Fora[16]: array([25, 45, 37, 19], dtype=int32)

A desvantagem é que, para matrizes de registros, há alguma sobrecarga extra envolvida no


acesso aos campos, mesmo quando se usa a mesma sintaxe. Podemos ver isso aqui:

Em[17]: %timeit data['age']


%timeit data_rec['age']
%timeit data_rec.age

1.000.000 loops, melhor de 3: 241 ns por loop


100.000 loops, melhor de 3: 4,61 µs por loop
100.000 loops, melhor de 3: 7,27 µs por loop

Se a notação mais conveniente vale a sobrecarga adicional dependerá do seu próprio aplicativo.

Vamos aos Pandas

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.

96 | Capítulo 2: Introdução ao NumPy


Machine Translated by Google

CAPÍTULO 3

Manipulação de dados com Pandas

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.

Neste capítulo, focaremos na mecânica de uso eficaz de Series, DataFrame e estruturas


relacionadas. Usaremos exemplos extraídos de conjuntos de dados reais quando apropriado,
mas estes exemplos não são necessariamente o foco.

Instalando e usando Pandas


A instalação do Pandas em seu sistema requer a instalação do NumPy e, se você estiver
construindo a biblioteca a partir do código-fonte, requer as ferramentas apropriadas para compilar o C e

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:

Em [1]: importar pandas


pandas.__versão__

Saída[1]: '0.18.1'

Assim como geralmente importamos NumPy sob o alias np, importaremos Pandas sob o alias pd:

In[2]: importar pandas como pd

Esta convenção de importação será usada no restante deste livro.

Lembrete sobre a documentação integrada


Ao ler este capítulo, não esqueça que o IPython oferece a capacidade de explorar rapidamente
o conteúdo de um pacote (usando o recurso de preenchimento de tabulação), bem como a
documentação de diversas funções (usando o caractere ? ) . (Consulte “Ajuda e documentação
no IPython” na página 3 se precisar de uma atualização sobre isso.)

Por exemplo, para exibir todo o conteúdo do namespace pandas , você pode digitar isto:

Em [3]: pd.<TAB>

E para exibir a documentação integrada do Pandas, você pode usar isto:

Em [4]: pd?

Documentação mais detalhada, juntamente com tutoriais e outros recursos, pode ser encontrada
em http://pandas.pydata.org/.

Apresentando Objetos Pandas


No nível mais básico, os objetos Pandas podem ser considerados versões aprimoradas de
matrizes estruturadas NumPy nas quais as linhas e colunas são identificadas com rótulos
em vez de simples índices inteiros. Como veremos ao longo deste capítulo, o Pandas
fornece uma série de ferramentas, métodos e funcionalidades úteis além das estruturas de
dados básicas, mas quase tudo o que se segue exigirá uma compreensão do que são essas
estruturas. Portanto, antes de prosseguirmos, vamos apresentar essas três estruturas de
dados fundamentais do Pandas: Série, DataFrame e Índice.

Iniciaremos nossas sessões de código com as importações padrão do NumPy e do Pandas:

In[1]: importar numpy como


np importar pandas como pd

98 | Capítulo 3: Manipulação de dados com Pandas


Machine Translated by Google

O objeto da série Pandas Uma

série Pandas é uma matriz unidimensional de dados indexados. Ele pode ser criado a partir de uma
lista ou array da seguinte forma:

In[2]: dados = pd.Series([0,25, 0,5, 0,75, 1,0]) dados

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

Fora[3]: array([ 0,25, 0,5 , 0,75, 1. ])

O índice é um objeto semelhante a um array do tipo pd.Index, que discutiremos com mais detalhes
em breve:

Em[4]: data.index

Saída[4]: RangeIndex(início=0, parada=4, etapa=1)

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.

Séries como um array NumPy

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.

Apresentando Objetos Pandas | 99


Machine Translated by Google

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

E o acesso ao item funciona conforme o esperado:

Em[8]: dados['b']

Fora[8]: 0,5

Podemos até usar índices não contíguos ou não sequenciais:

Em[9]: dados = pd.Series([0,25, 0,5, 0,75, 1,0], índice=[2, 5, 3, 7])

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

Séries como dicionário

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:

100 | Capítulo 3: Manipulação de dados com Pandas


Machine Translated by Google

Em [11]: população_dict = {'Califórnia': 38332521,


'Texas': 26448193,
'Nova York': 19651127,
'Flórida': 19552860,
'Illinois': 12882135}
população = pd.Series(population_dict)
população

Fora[11]: Califórnia 38332521


Flórida 19552860
Illinois 12882135
Nova Iorque 19651127
Texas 26448193
tipo d: int64

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']

Fora[13]: Califórnia 38332521


Flórida 19552860
Tipo de 12882135
Illinois: int64

Discutiremos algumas das peculiaridades da indexação e fatiamento do Pandas em “Indexação de dados e


Seleção” na página 107.

Construindo objetos de série

Já vimos algumas maneiras de construir uma Série Pandas do zero; tudo de


eles são alguma versão do seguinte:

>>> pd.Series(dados, índice=índice)

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:

Em[14]: pd.Series([2, 4, 6])

Fora[14]: 0 2
1 4
2 6
tipo d: int64

Apresentando Objetos Pandas | 101


Machine Translated by Google

data pode ser um escalar, que é repetido para preencher o índice especificado:

Em[15]: pd.Series(5, índice=[100, 200, 300])

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:

Em[16]: pd.Series({2:'a', 1:'b', 3:'c'})

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:

Em[17]: pd.Series({2:'a', 1:'b', 3:'c'}, índice=[3, 2])

Fora[17]: 3 2 c
a
dtype: objeto

Observe que neste caso, a Série é preenchida apenas com as chaves explicitamente identificadas.

O objeto Pandas DataFrame A próxima

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.

DataFrame como um array NumPy

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}

102 | Capítulo 3: Manipulação de dados com Pandas


Machine Translated by Google

área = pd.Series(area_dict)
área

Fora[18]: Califórnia 423967


Flórida 170312
Illinois 149995
Nova Iorque 141297
Texas 695662
tipo d: int64

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:

In[19]: estados = pd.DataFrame({'população': população,


'área': área})
estados

Fora[19]: área população


Califórnia 423967 38332521
Flórida 170312 19552860
Illinois 149995 12882135
Nova Iorque 141297 19651127
Texas 695662 26448193

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

Out[21]: Índice(['área', 'população'], dtype='objeto')

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.

DataFrame como dicionário especializado

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']

Fora[22]: Califórnia 423967


Flórida 170312

Apresentando Objetos Pandas | 103


Machine Translated by Google

Illinois 149995
Nova Iorque 141297
Texas 695662
Nome: área, dtype: int64

Observe o ponto potencial de confusão aqui: em uma matriz NumPy bidimensional,


data[0] retornará a primeira linha. Para um DataFrame, data['col0'] retornará o primeiro
coluna. Por causa disso, provavelmente é melhor pensar em DataFrames como generalizados
dicionários em vez de matrizes generalizadas, embora ambas as formas de olhar para a situação
pode ser útil. Exploraremos meios mais flexíveis de indexação de DataFrames em “Data
Indexação e seleção” na página 107.

Construindo objetos DataFrame

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:

Em[23]: pd.DataFrame(população, colunas=['população'])

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:

Em[24]: dados = [{'a': i, 'b': 2 * i}


para i no intervalo (3)]
pd.DataFrame(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:

Em[25]: pd.DataFrame([{'a': 1, 'b': 2}, {'b': 3, 'c': 4}])

Fora[25]: a a.C.
0 1,0 2 NaN
1NaN3 4,0

104 | Capítulo 3: Manipulação de dados com Pandas


Machine Translated by Google

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 :

In[26]: pd.DataFrame({'população': população,


'área': área})

Fora[26]: área população


Califórnia 423967 38332521
Flórida 170312 19552860
Illinois 149995 12882135
Nova Iorque 141297 19651127
Texas 695662 26448193

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 [27]: pd.DataFrame (np.random.rand (3, 2),


colunas=['foo', 'bar'],
índice=['a', 'b', 'c'])

Fora[27]: barra foo


0,865257 0,213169
b 0,442759 0,108267
c 0,047110 0,905718

De um array estruturado NumPy. Abordamos arrays estruturados em “Dados Estruturados:


Matrizes Estruturadas do NumPy” na página 92. Um DataFrame do Pandas funciona como um
array estruturado e pode ser criado diretamente de um:

Em[28]: A = np.zeros(3, dtype=[('A', 'i8'), ('B', 'f8')])


A

Fora[28]: matriz([(0, 0,0), (0, 0,0), (0, 0,0)],


dtype=[('A', '<i8'), ('B', '<f8')])

Em[29]: pd.DataFrame(A)

Fora[29]: AB
0 0 0,0
1 0 0,0
2 0 0,0

O objeto de índice Pandas


Vimos aqui que os objetos Series e DataFrame contêm uma mensagem explícita
índice que permite referenciar e modificar dados. Este objeto Index é um interessante
estrutura em si, e pode ser pensado como uma matriz imutável ou como um
conjunto ordenado (tecnicamente um multiconjunto, pois os objetos de índice podem conter valores repetidos).
Essas visões têm algumas consequências interessantes nas operações disponíveis no Index
objetos. Como exemplo simples, vamos construir um Índice a partir de uma lista de inteiros:

Apresentando Objetos Pandas | 105


Machine Translated by Google

Em[30]: ind = pd.Index([2, 3, 5, 7, 11]) ind

Fora[30]: Int64Index([2, 3, 5, 7, 11], dtype='int64')

Índice como array

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]

Fora[32]: Int64Index([2, 5, 11], dtype='int64')

Os objetos de índice também possuem muitos dos atributos familiares dos arrays NumPy:

In[33]: imprimir(ind.size, ind.shape, ind.ndim, ind.dtype)

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
-------------------------------------------------- -------------------------

Erro de tipo Traceback (última chamada mais recente)

<ipython-input-34-40e631c82e8a> em <módulo>() ----> 1


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):

TypeError: o índice não suporta operações mutáveis

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.

Índice como conjunto ordenado

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

106 | Capítulo 3: Manipulação de dados com Pandas


Machine Translated by Google

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:

Em[35]: indA = pd.Index([1, 3, 5, 7, 9]) indB =


pd.Index([2, 3, 5, 7, 11])

In[36]: interseção indA e indB #

Fora[36]: Int64Index([3, 5, 7], dtype='int64')

In[37]: indA | indB # união

Fora[37]: Int64Index([1, 2, 3, 5, 7, 9, 11], dtype='int64')

In[38]: indA ^ indB # diferença simétrica

Fora[38]: Int64Index([1, 2, 9, 11], dtype='int64')

Essas operações também podem ser acessadas por meio de métodos de objeto — por exemplo,
seção indA.inter(indB).

Indexação e seleção de dados


No Capítulo 2, examinamos detalhadamente métodos e ferramentas para acessar, definir e modificar
valores em arrays NumPy. Isso incluía indexação (por exemplo, arr[2, 1]), fatiamento (por exemplo,
arr[:, 1:5]), mascaramento (por exemplo, arr[arr > 0]), indexação sofisticada (por exemplo, arr[0, [ 1,
5]]) e combinações dos mesmos (por exemplo, arr[:, [1, 5]]). Aqui veremos meios semelhantes de
acessar e modificar valores em objetos Pandas Series e DataFrame . Se você usou os padrões
NumPy, os padrões correspondentes no Pandas parecerão muito familiares, embora existam algumas
peculiaridades que você deve conhecer.

Começaremos com o caso simples do objeto Series unidimensional e depois passaremos para o
objeto DataFrame bidimensional mais complicado .

Seleção de dados em série

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.

Série como dicionário

Como um dicionário, o objeto Series fornece um mapeamento de uma coleção de chaves para uma
coleção de valores:

Em [1]: importe pandas como


pd data = pd.Series([0,25, 0,5, 0,75, 1,0],
index=['a', 'b', 'c', 'd'])
dados

Indexação e seleção de dados | 107


Machine Translated by Google

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:

In[3]: 'a' em dados

Fora[3]: Verdadeiro

Em[4]: data.keys()

Out[4]: Índice(['a', 'b', 'c', 'd'], dtype='objeto')

Em[5]: lista(data.items())

Fora[5]: [('a', 0,25), ('b', 0,5), ('c', 0,75), ('d', 1,0)]

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:

Em[6]: dados['e'] = 1,25


dados

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.

Série como array unidimensional

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:

In[7]: # fatiamento por índice explícito


dados['a':'c']

Fora[7]: ab 0,25
0,50
c 0,75
tipo d: float64

108 | Capítulo 3: Manipulação de dados com Pandas


Machine Translated by Google

In[8]: # fatiamento por índice inteiro implícito


dados[0:2]

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

In[10]: # indexação sofisticada


data[['a', 'e']]

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.

Indexadores: loc, iloc e ix

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.

Em[11]: dados = pd.Series(['a', 'b', 'c'], índice=[1, 3, 5])


dados

Fora[11]: 1 a
3 b
c
5 dtipo: objeto

In[12]: # índice explícito ao indexar


dados[1]

Fora[12]: 'a'

In[13]: # índice implícito ao fatiar


dados[1:3]

Fora[13]: 3 5 b
c
dtype: objeto

Devido a esta confusão potencial no caso de índices inteiros, o Pandas fornece


alguns atributos especiais do indexador que expõem explicitamente certos esquemas de indexação. Esses

Indexação e seleção de dados | 109


Machine Translated by Google

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.

Seleção de dados no DataFrame

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

dicionário A primeira analogia que consideraremos é o DataFrame como um dicionário de objetos


Series relacionados . Voltemos ao nosso exemplo de áreas e populações de estados:

110 | Capítulo 3: Manipulação de dados com Pandas


Machine Translated by Google

Em [18]: área = pd.Series({'Califórnia': 423967, 'Texas': 695662,


'Nova York': 141297, 'Flórida': 170312,
'Illinois': 149995})
pop = pd.Series({'Califórnia': 38332521, 'Texas': 26448193,
'Nova York': 19651127, 'Flórida': 19552860,
'Illinois': 12882135})
dados = pd.DataFrame({'area':area, 'pop':pop})
dados

Fora[18]: área pop


Califórnia 423967 38332521
Flórida 170312 19552860
Illinois 149995 12882135
Nova York 141297 19651127
Texas 695662 26448193

As séries individuais que compõem as colunas do DataFrame podem ser acessadas


por meio da indexação no estilo de dicionário do nome da coluna:

Em[19]: dados['área']

Fora[19]: Califórnia 423967


Flórida 170312
Illinois 149995
Nova Iorque 141297
Texas 695662
Nome: área, dtype: int64

De forma equivalente, podemos usar acesso de estilo de atributo com nomes de colunas que são strings:

Em[20]: data.area

Fora[20]: Califórnia 423967


Flórida 170312
Illinois 149995
Nova Iorque 141297
Texas 695662
Nome: área, dtype: int64

Esse acesso à coluna no estilo de atributo, na verdade, acessa exatamente o mesmo objeto que o
acesso estilo dicionário:

In[21]: data.area é data['area']

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" :

In[22]: data.pop é data['pop']

Fora[22]: Falso

Indexação e seleção de dados | 111


Machine Translated by Google

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:

In[23]: dados['densidade'] = dados['pop'] / dados['area']


dados

Fora[23]: área pop densidade


Califórnia 423967 38332521 90.413926
Flórida 170312 19552860 114.806121
Illinois 149995 12882135 85.883763
Nova York 141297 19651127 139.076746
Texas 695662 26448193 38.018740

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.

DataFrame como array bidimensional

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

Fora[24]: matriz([[ 4.23967000e+05, 3.83325210e+07, 9.04139261e+01],


[1.70312000e+05, 1.95528600e+07, 1.14806121e+02],
[1,49995000e+05, 1,28821350e+07, 8,58837628e+01],
[1.41297000e+05, 1.96511270e+07, 1.39076746e+02],
[6.95662000e+05, 2.64481930e+07, 3.80187404e+01]])

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

Quando se trata de indexação de objetos DataFrame , entretanto, fica claro que o


indexação de colunas no estilo de dicionário impede nossa capacidade de simplesmente tratá-la como um
Matriz NumPy. Em particular, passar um único índice para um array acessa uma linha:

Em[26]: dados.valores[0]

Fora[26]: matriz([ 4.23967000e+05, 3.83325210e+07, 9.04139261e+01])

112 | Capítulo 3: Manipulação de dados com Pandas


Machine Translated by Google

e passar um único “índice” para um DataFrame acessa uma coluna:

Em[27]: dados['área']

Fora[27]: Califórnia 423967


Flórida 170312
Illinois 149995
Nova Iorque 141297
Texas 695662
Nome: área, dtype: int64

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:

Em[28]: dados.iloc[:3, :2]

Fora[28]: área pop


Califórnia 423967 38332521
Flórida 170312 19552860
Illinois 149995 12882135

Em[29]: data.loc[:'Illinois', :'pop']

Fora[29]: área pop


Califórnia 423967 38332521
Flórida 170312 19552860
Illinois 149995 12882135

O indexador ix permite um híbrido destas duas abordagens:

Em[30]: data.ix[:3, :'pop']

Fora[30]: área pop


Califórnia 423967 38332521
Flórida 170312 19552860
Illinois 149995 12882135

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:

In[31]: data.loc[data.density > 100, ['pop', 'density']]

Fora[31]: pop densidade


Flórida 19552860 114.806121
Nova York 19651127 139.076746

Indexação e seleção de dados | 113


Machine Translated by Google

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

Fora[32]: área pop densidade


Califórnia 423967 38332521 90.000000
Flórida 170312 19552860 114.806121
Illinois 149995 12882135 85.883763
Nova York 141297 19651127 139.076746
Texas 695662 26448193 38.018740

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.

Convenções de indexação adicionais

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']

Fora[33]: área pop densidade


Flórida 170312 19552860 114.806121
Illinois 149995 12882135 85.883763

Essas fatias também podem referir-se a linhas por número e não por índice:

Em[34]: dados[1:3]

Fora[34]: área pop densidade


Flórida 170312 19552860 114.806121
Illinois 149995 12882135 85.883763

Da mesma forma, as operações de mascaramento direto também são interpretadas por linha, em vez de
em coluna:

Em[35]: dados[data.density > 100]

Fora[35]: área pop densidade


Flórida 170312 19552860 114.806121
Nova York 141297 19651127 139.076746

Essas duas convenções são sintaticamente semelhantes às de um array NumPy e, embora


estes podem não se encaixar exatamente no molde das convenções Pandas, eles são, no entanto,
bastante útil na prática.

114 | Capítulo 3: Manipulação de dados com Pandas


Machine Translated by Google

Operando com dados em Pandas


Uma das peças essenciais do NumPy é a capacidade de realizar operações rápidas entre
elementos, tanto com aritmética básica (adição, subtração, multiplicação, etc.) quanto com
operações mais sofisticadas (funções trigonométricas, funções exponenciais e logarítmicas,
etc. ). O Pandas herda grande parte dessa funcionalidade do NumPy, e os ufuncs que
apresentamos em “Computação em arrays NumPy: funções universais” na página 50 são
fundamentais para isso.

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 .

Ufuncs: preservação de índice

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:

In[1]: importar pandas como pd


importar numpy como np

Em [2]: rng = np.random.RandomState (42)


ser = pd.Series(rng.randint(0, 10, 4))
ser

Fora[2]: 0 1 6

2 3

7
3 4
tipo d: int64

Em [3]: df = pd.DataFrame(rng.randint(0, 10, (3, 4)), colunas=['A',


'B', 'C', 'D'])
df

Fora[3]: ABCD
06926
174372725
4

Se aplicarmos um NumPy ufunc em qualquer um desses objetos, o resultado será outro


objeto Pandas com os índices preservados:

Em[4]: np.exp(ser)

Operando com dados no Pandas | 115


Machine Translated by Google

Fora[4]: 0 1 403.428793
20.085537
2 1096.633158
3 54.598150
tipo d: float64

Ou, para um cálculo um pouco mais complexo:

Em[5]: np.sin(df * np.pi / 4)

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

Qualquer um dos ufuncs discutidos em “Computation on NumPy Arrays: Universal Func-


ções” na página 50 podem ser usadas de maneira semelhante.

UFuncs: alinhamento do índice


Para operações binárias em dois objetos Series ou DataFrame , o Pandas alinhará os índices
no processo de execução da operação. Isto é muito conveniente quando você está
trabalhando com dados incompletos, como veremos em alguns exemplos a seguir.

Alinhamento de índice em série

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:

Em [6]: área = pd.Series({'Alasca': 1723337, 'Texas': 695662,


'Califórnia': 423967}, nome='área')
população = pd.Series({'Califórnia': 38332521, 'Texas': 26448193,
'Nova York': 19651127}, nome='população')

Vamos ver o que acontece quando dividimos estes valores para calcular a densidade populacional:

In[7]: população / área

Fora[7]: Alasca NaN


Califórnia 90.413926
Nova Iorque NaN
Texas 38.018740
tipo d: float64

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:

Em[8]: area.index | população.index

Out[8]: Index(['Alaska', 'California', 'New York', 'Texas'], dtype='object')

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

116 | Capítulo 3: Manipulação de dados com Pandas


Machine Translated by Google

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:

Em[9]: A = pd.Series([2, 4, 6], índice=[0, 1, 2])


B = pd.Series([1, 3, 5], índice=[1, 2, 3])
A+B

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:

Em[10]: A.add(B, fill_value=0)

Fora[10]: 0 2,0
1 5,0
2 9,0
3 5,0
tipo d: float64

Alinhamento de índice no DataFrame

Um tipo semelhante de alinhamento ocorre para colunas e índices quando você está
realizando operações em DataFrames:

Em[11]: A = pd.DataFrame(rng.randint(0, 20, (2, 2)),


colunas=lista('AB'))
A

Fora[11]: AB
0 1 11
15 1

Em[12]: B = pd.DataFrame(rng.randint(0, 10, (3, 3)),


colunas=lista('BAC'))
B

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

Operando com dados no Pandas | 117


Machine Translated by Google

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):

Em[14]: preencher = A.stack().mean()


A.add(B, fill_value=preencher)

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.

Tabela 3-1. Mapeamento entre operadores Python e métodos Pandas

Operador Python Método(s) Pandas


+ adicionar()

-
sub(), subtrair()
*
mul(), multiplicar()

/ truediv(), div(), dividir()

// floordiv()

% contra()
**
Pancada()

Ufuncs: operações entre DataFrame e série


Ao realizar operações entre um DataFrame e uma Série, o índice
e o alinhamento da coluna é mantido de forma semelhante. Operações entre um DataFrame e
uma série são semelhantes às operações entre um bidimensional e um unidimensional
Matriz NumPy. Considere uma operação comum, onde encontramos a diferença de um
matriz bidimensional e uma de suas linhas:

Em[15]: A = rng.randint(10, tamanho=(3, 4))


A

Fora[15]: matriz([[3, 8, 2, 4],


[2, 6, 4, 8],
[6, 1, 3, 8]])

Em[16]: A - A[0]

Fora[16]: matriz([[ 0, 0, 0, 0],


[-1, -2, 2, 4],
[3, -7, 1, 4]])

118 | Capítulo 3: Manipulação de dados com Pandas


Machine Translated by Google

De acordo com as regras de transmissão do NumPy (consulte “Cálculo em matrizes: transmissão” na


página 63), a subtração entre uma matriz bidimensional e uma de suas linhas é aplicada linha a linha.

No Pandas, a convenção funciona de forma semelhante em linhas por padrão:

Em[17]: df = pd.DataFrame(A, colunas=lista('QRST')) df - df.iloc[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 :

In[18]: df.subtract(df['R'], eixo=0)

Fora[18]: QRST 0 -5 0
-6 -4
1 -4 0 -2 2
25027

Observe que essas operações DataFrame/Series , como as operações discutidas anteriormente,


alinharão automaticamente os índices entre os dois elementos:

In[19]: meia linha = df.iloc[0, ::2] meia linha

Fora[19]: Q 3
S2
Nome: 0, dtype: int64

In[20]: df - meia linha

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. .

Tratamento de dados ausentes

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.

Tratamento de dados ausentes | 119


Machine Translated by Google

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.

Compensações nas convenções de dados ausentes

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.

Dados ausentes no Pandas A

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,

120 | Capítulo 3: Manipulação de dados com Pandas


Machine Translated by Google

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.

Nenhum: dados faltantes do

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):

In[1]: importar numpy como


np importar pandas como pd

Em[2]: vals1 = np.array([1, Nenhum, 3, 4]) vals1

Fora[2]: array([1, Nenhum, 3, 4], dtype=objeto)

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:

Em [3]: para dtype em ['objeto', 'int']: print("dtype


=", dtype) %timeit
np.arange(1E6, dtype=dtype).sum() print()

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:

Tratamento de dados ausentes | 121


Machine Translated by Google

Em[4]: vals1.sum()

Erro de tipo Traceback (última chamada mais recente)

<ipython-input-4-749fd8ae6030> em <module>() ----> 1


vals1.sum()

/Users/jakevdp/anaconda/lib/python3.5/site-packages/numpy/core/_methods.py ...

30 31 def _sum(a, eixo=Nenhum, dtype=Nenhum, out=Nenhum, keepdims=Falso):


---> 32 retornar umr_sum(a, eixo, dtype, out, keepdims)
33
34 def _prod(a, eixo=Nenhum, dtype=Nenhum, out=Nenhum, keepdims=False):

TypeError: tipos de operandos não suportados para +: 'int' e 'NoneType'

Isso reflete o fato de que a adição entre um número inteiro e None é indefinida.

NaN: dados numéricos ausentes

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:

Em [5]: vals2 = np.array([1, np.nan, 3, 4]) vals2.dtype

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:

Em[8]: vals2.sum(), vals2.min(), vals2.max()

Agosto[8]: (dentro, dentro, dentro)

NumPy fornece algumas agregações especiais que irão ignorar esses valores ausentes:

122 | Capítulo 3: Manipulação de dados com Pandas


Machine Translated by Google

Em [9]: np.nansum(vals2), np.nanmin(vals2), np.nanmax(vals2)

Fora[9]: (8,0, 1,0, 4,0)

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 Nenhum em Pandas

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:

Em[10]: pd.Series([1, np.nan, 2, Nenhum])

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:

Em[11]: x = pd.Series(intervalo(2), dtype=int)


x

Fora[11]: 0 0
1 1
tipo d: int64

Em[12]: x[0] = Nenhum


x

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.

Tratamento de dados ausentes | 123


Machine Translated by Google

Tabela 3-2. Manipulação de NAs por tipo de Pandas

Typeclass Conversão ao armazenar o valor sentinela NAs NA

flutuante Sem alteração np. em


Sem alteração
objeto Nenhum ou np.nan

número inteiro convertido em float64 np. em

booleano Converter para objeto Nenhum ou np.nan

Tenha em mente que no Pandas, os dados da string são sempre armazenados com um tipo de objeto .

Operando em valores nulos


Como vimos, o Pandas trata None e NaN como essencialmente intercambiáveis para indiÿ
cating valores ausentes ou nulos. Para facilitar esta convenção, existem vários
métodos para detectar, remover e substituir valores nulos em estruturas de dados Pandas.
Eles são:

é 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

Concluiremos esta seção com uma breve exploração e demonstração desses


rotinas.

Detectando valores nulos

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 [13]: dados = pd.Series([1, np.nan, 'olá', Nenhum])

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 :

124 | Capítulo 3: Manipulação de dados com Pandas


Machine Translated by Google

Em[15]: dados[data.notnull()]

Fora[15]: 0 1
2 olá
dtype: objeto

Os métodos isnull() e notnull() produzem resultados booleanos semelhantes para dados


Quadros.

Descartando valores nulos

Além do mascaramento usado anteriormente, existem os métodos de conveniência, dropna()


(que remove valores NA) e fillna() (que preenche valores NA). Para uma série,
o resultado é direto:

Em[16]: data.dropna()

Fora[16]: 0 1
2 olá
dtype: objeto

Para um DataFrame, existem mais opções. Considere o seguinte DataFrame:

Em[17]: df = pd.DataFrame([[1, np.nan, 2],


[2, 5], 3,
[np.in, 4, 6]])
df

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

Tratamento de dados ausentes | 125


Machine Translated by Google

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:

Em[20]: df[3] = np.nan


df

Fora[20]: 0 123
0 1,0 NaN 2 NaN
1 2,0 3,0 5 NaN
2 NaN 4,0 6 NaN

Em[21]: df.dropna(axis='colunas', how='all')

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:

Em[22]: df.dropna(axis='linhas', thresh=3)

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.

Preenchendo valores 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.

Considere a seguinte série:

Em [23]: dados = pd.Series([1, np.nan, 2, Nenhum, 3], índice=lista('abcde'))


dados

Fora[23]: ab 1,0
NaN
c 2,0
d NaN

126 | Capítulo 3: Manipulação de dados com Pandas


Machine Translated by Google

e 3,0
tipo d: float64

Podemos preencher as entradas de NA com um único valor, como zero:

Em[24]: data.fillna(0)

Fora[24]: ab 1,0
0,0
2,0
cd 0,0
e 3,0
tipo d: float64

Podemos especificar um forward-fill para propagar o valor anterior:

In[25]: # preenchimento direto


data.fillna(método='ffill')

Fora[25]: ab 1,0
1,0
c 2,0
d 2,0
e 3,0
tipo d: float64

Ou podemos especificar um preenchimento para propagar os próximos valores para trás:

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

Em[28]: df.fillna(método='ffill', eixo=1)

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.

Tratamento de dados ausentes | 127


Machine Translated by Google

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.

Começamos com as importações padrão:

In[1]: importar pandas como pd


importar numpy como np

Uma série indexada multiplicada

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:

In[2]: índice = [('Califórnia', 2000), ('Califórnia', 2010),


('Nova York', 2000), ('Nova York', 2010), ('Texas',
2000), ('Texas', 2010)] populações =
[33871648, 37253956, 18976457, 19378102,
20851820, 25145561]
pop =
pd.Series(populações, índice=índice)
pop

Fora[2]: (Califórnia, 2000) 33871648


(Califórnia, 2010) 37253956
(Nova York, 2000) 18976457
(Nova York, 2010) 19378102
(Texas, 2000) 20851820

128 | Capítulo 3: Manipulação de dados com Pandas


Machine Translated by Google

(Texas, 2010) 25145561


tipo d: int64

Com este esquema de indexação, você pode indexar ou dividir diretamente a série com base
neste índice múltiplo:

Em[3]: pop[('Califórnia', 2010):('Texas', 2000)]

Fora[3]: (Califórnia, 2010) 37253956


(Nova York, 2000) 18976457
(Nova York, 2010) 19378102
(Texas, 2000) 20851820
tipo d: int64

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:

In[4]: pop[[i for i in pop.index if i[1] == 2010]]

Fora[4]: (Califórnia, 2010) 37253956


(Nova York, 2010) 19378102
(Texas, 2010) 25145561
tipo d: int64

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.

A melhor maneira: Pandas MultiIndex

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:

Em[5]: índice = pd.MultiIndex.from_tuples(índice)


índice

Saída[5]: MultiIndex(levels=[['California', 'New York', 'Texas'], [2000, 2010]],


rótulos=[[0, 0, 1, 1, 2, 2], [0, 1, 0, 1, 0, 1]])

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.

Se reindexarmos nossa série com este MultiIndex, veremos a representação hierárquica


dos dados:

Em[6]: pop = pop.reindex(índice)


pop

Fora[6]: Califórnia 2000 33871648


2010 37253956
Nova Iorque 2000 18976457
2010 19378102

Indexação Hierárquica | 129


Machine Translated by Google

Texas 2000 20851820


2010 25145561
tipo d: int64

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:

Em[7]: pop[:, 2010]

Fora[7]: Califórnia 37253956


Nova Iorque 19378102
Texas 25145561
tipo d: int64

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.

MultiIndex como dimensão extra

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[8]: pop_df = pop.unstack()


pop_df

Fora[8]: 2000 2010


Califórnia 33871648 37253956
Nova York 18976457 19378102
Texas 20851820 25145561

Naturalmente, o método stack() fornece a operação oposta:

Em[9]: pop_df.stack()

Fora[9]: Califórnia 2000 2010 33871648


37253956
Nova York 2000 18976457
2010 19378102
Texas 2000 20851820
Tipo 25145561
de 2010: int64

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

130 | Capítulo 3: Manipulação de dados com Pandas


Machine Translated by Google

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:

Em[10]: pop_df = pd.DataFrame({'total': pop,


'menores de 18 anos': [9267089, 9284094,
4687374, 4318033,
5906301, 6879014]})
pop_df

Fora[10]: total de menores de 18 anos

Califórnia 2000 33871648 9267089 2010 37253956


9284094
Nova York 2000 18976457 4687374
2010 19378102 4318033
Texas 2000 20851820 5906301 2010
25145561 6879014

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:

Em[11]: f_u18 = pop_df['under18'] / pop_df['total'] f_u18.unstack()

Fora[11]: 2000 2010


Califórnia 0,273594 0,249211
Nova York 0,247010 0,222831 Texas
0,283251 0,273568

Isso nos permite manipular e explorar de maneira fácil e rápida até mesmo dados de alta
dimensão.

Métodos de criação de MultiIndex


A maneira mais direta de construir uma série ou DataFrame com indexação múltipla é
simplesmente passar uma lista de duas ou mais matrizes de índice para o construtor. Por exemplo:

Em [12]: df = pd.DataFrame(np.random.rand(4, 2), index=[['a', 'a', 'b',


'b'], [1, 2, 1, 2]], colunas=['dados1', 'dados2'])

df

Fora[12]: dados1 dados2 a 1


0,554233 0,356072 2 0,925244
0,219474 b 1 0,441759
0,610054
2 0,171495 0,886688

Indexação Hierárquica | 131


Machine Translated by Google

O trabalho de criação do MultiIndex é feito em segundo plano.

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:

Em [13]: dados = {('Califórnia', 2000): 33871648, ('Califórnia',


2010): 37253956, ('Texas', 2000):
20851820, ('Texas', 2010):
25145561, (' Nova York', 2000):
18976457, ('Nova York', 2010):
19378102} pd.Series (dados)

Fora[13]: Califórnia 2000 2010 33871648


37253956
Nova York 2000 18976457
2010 19378102
Texas 2000 20851820
2010 25145561
tipo d: int64

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:

Em[14]: pd.MultiIndex.from_arrays([['a', 'a', 'b', 'b'], [1, 2, 1, 2]])

Fora[14]: MultiIndex(níveis=[['a', 'b'], [1, 2]], rótulos=[[0, 0, 1, 1],


[0, 1, 0, 1]] )

Você pode construí-lo a partir de uma lista de tuplas, fornecendo os múltiplos valores de índice de cada
ponto:

Em [15]: pd.MultiIndex.from_tuples([('a', 1), ('a', 2), ('b', 1), ('b', 2)])

Fora[15]: MultiIndex(níveis=[['a', 'b'], [1, 2]], rótulos=[[0, 0, 1, 1],


[0, 1, 0, 1]] )

Você pode até construí-lo a partir de um produto cartesiano de índices únicos:

Em[16]: pd.MultiIndex.from_product([['a', 'b'], [1, 2]])

Fora[16]: MultiIndex(níveis=[['a', 'b'], [1, 2]], rótulos=[[0, 0, 1, 1],


[0, 1, 0, 1]] )

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):

132 | Capítulo 3: Manipulação de dados com Pandas


Machine Translated by Google

Em [17]: pd.MultiIndex(níveis=[['a', 'b'], [1, 2]], rótulos=[[0, 0, 1,


1], [0, 1, 0, 1 ]])

Fora[17]: MultiIndex(níveis=[['a', 'b'], [1, 2]], rótulos=[[0, 0, 1, 1],


[0, 1, 0, 1]] )

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 .

Nomes de nível MultiIndex

À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:

In[18]: pop.index.names = ['estado', 'ano']


pop

Fora[18]: estado ano


Califórnia 2000 33871648
2010 37253956
Nova York 2000 18976457
2010 19378102
Texas 2000 20851820
2010 25145561
tipo d: int64

Com conjuntos de dados mais envolvidos, esta pode ser uma forma útil de acompanhar o significado
de vários valores de índice.

MultiIndex para colunas

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

Indexação Hierárquica | 133


Machine Translated by Google

Out[19]: tipo de assunto Prumo Guido Processar

ano Temperatura de RH Temperatura de RH Temperatura de RH

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']

Out[20]: digite ano Temperatura de RH

de visita
2013 1 32,0 36,7
2 50,0 35,0
2014 1 2 39,0 37,8
48,0 37,3

Para registros complicados contendo múltiplas medições rotuladas em vários


vezes para muitos assuntos (pessoas, países, cidades, etc.), uso de linhas hierárquicas e
colunas podem ser extremamente convenientes!

Indexando e fatiando um MultiIndex


A indexação e o fatiamento em um MultiIndex foram projetados para serem intuitivos e ajudam se você
pense nos índices como dimensões adicionais. Veremos primeiro a indexação multiplicada
Série indexada e, em seguida, multiplique DataFrames indexados.

Multiplicar séries indexadas

Considere a série multiplamente indexada de populações de estados que vimos anteriormente:

Em[21]: pop

Fora[21]: estado ano


Califórnia 2000 33871648
2010 37253956
Nova York 2000 18976457
2010 19378102
Texas 2000 20851820
2010 25145561
tipo d: int64

Podemos acessar elementos únicos indexando com vários termos:

In[22]: pop['Califórnia', 2000]

Saída[22]: 33871648

134 | Capítulo 3: Manipulação de dados com Pandas


Machine Translated by Google

O MultiIndex também suporta indexação parcial, ou indexação de apenas um dos níveis em


o índice. O resultado é outra Série, com os índices de nível inferior mantidos:

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):

Em[24]: pop.loc['Califórnia':'Nova York']

Fora[24]: estado ano


Califórnia 2000 2010 33871648
37253956
Nova York 2000 18976457
2010 19378102
tipo d: int64

Com índices ordenados, podemos realizar indexação parcial em níveis inferiores, passando um
fatia vazia no primeiro índice:

Em[25]: pop[:, 2000]

Fora[25]: estado
Califórnia 33871648
Nova Iorque 18976457
Tipo 20851820
de Texas: int64

Outros tipos de indexação e seleção (discutidos em “Indexação e Seleção de Dados” em


página 107) também funcionam; por exemplo, seleção baseada em máscaras booleanas:

Em[26]: pop[pop > 22000000]

Fora[26]: estado ano


Califórnia 2000 2010 33871648
37253956
Texas 2010 25145561
tipo d: int64

A seleção baseada em indexação sofisticada também funciona:

Em[27]: pop[['Califórnia', 'Texas']]

Fora[27]: estado ano


Califórnia 2000 33871648
2010 37253956
Texas 2000 20851820
2010 25145561
tipo d: int64

Indexação Hierárquica | 135


Machine Translated by Google

Multiplicar DataFrames indexados

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

Out[28]: tipo de assunto Prumo Guido Processar

ano Temperatura de RH Temperatura de RH Temperatura de RH

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:

In[29]: health_data['Guido', 'HR']

Fora[29]: visita anual


2013 1 32,0
2 50,0
2014 1 2 39,0
48,0
Nome: (Guido, RH), dtype: float64

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:

Em[30]: health_data.iloc[:2, :2]

Out[30]: tipo de assunto Prumo

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:

Em[31]: health_data.loc[:, ('Bob', 'HR')]

Fora[31]: visita anual


2013 1 31,0
2 44,0
2014 1 30,0
2 47,0
Nome: (Bob, RH), dtype: float64

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:

136 | Capítulo 3: Manipulação de dados com Pandas


Machine Translated by Google

Em[32]: health_data.loc[(:, 1), (:, 'RH')]

Arquivo "<ipython-input-32-8e3cc151e316>", linha 1


health_data.loc[(:, 1), (:, 'RH')]
^

SyntaxError: sintaxe inválida

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:

Em[33]: idx = pd.IndexSlice


health_data.loc[idx[:, 1], idx[:, 'RH']]

Out[33]: tipo de assunto Bob Guido Sue


ano RH RH RH
visita
2013 1 31,0 32,0 35,0
2014 1 30,0 39,0 61,0

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.

Índices classificados e não classificados

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:

Em[34]: índice = pd.MultiIndex.from_product([['a', 'c', 'b'], [1, 2]])


dados = pd.Series(np.random.rand(6), índice=índice)
dados.index.names = ['char', 'int']
dados

Fora[34]: char int


a 1 0,003001
2 0,164974
c 1 0,741650

Indexação Hierárquica | 137


Machine Translated by Google

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:

Em[36]: dados = data.sort_index()


dados

Fora[36]: char int


a 1 0,003001
2 0,164974
b 1 0,001693
2 0,526226
c 0,741650
0,569264
1 2 tipo d: float64

Com o índice classificado desta forma, o fatiamento parcial funcionará conforme o esperado:

Em[37]: dados['a':'b']

Fora[37]: char int


a 1 0,003001
2 0,164974
b 0,001693
0,526226
1 2 tipo d: float64

Índices de empilhamento e desempilhamento

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:

138 | Capítulo 3: Manipulação de dados com Pandas


Machine Translated by Google

Em[38]: pop.unstack(nível=0)

Fora[38]: estado da Califórnia, Nova York Texas


ano
2000 33871648 18976457 20851820
2010 37253956 19378102 25145561

Em[39]: pop.unstack(nível=1)

Fora[39]: ano 2000 2010


estado
Califórnia 33871648 37253956
Nova York 18976457 19378102
Texas 20851820 25145561

O oposto de unstack() é stack(), que aqui pode ser usado para recuperar o original
Series:

Em[40]: pop.unstack().stack()

Fora[40]: estado ano


Califórnia 2000 33871648
2010 37253956
Nova York 2000 18976457
2010 19378102
Texas 2000 20851820
2010 25145561
tipo d: int64

Configuração e redefinição de índice

Outra forma de reorganizar os dados hierárquicos é transformar os rótulos do índice em colunas;


isso pode ser feito com o método reset_index . Chamando isso para a população
dicionário de configuração resultará em um DataFrame com uma coluna de estado e ano contendo o
informações que anteriormente estavam no índice. Para maior clareza, podemos opcionalmente especificar o
nome dos dados para a representação da coluna:

Em[41]: pop_flat = pop.reset_index(nome='população')


pop_flat

Fora[41]: população do ano do estado


0 Califórnia 2000 33871648
1 Califórnia 2010 37253956
2 Nova York 2000 18976457
3 Nova York 2010 19378102
4 Texas 2000 20851820
5 Texas 2010 25145561

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:

Indexação Hierárquica | 139


Machine Translated by Google

Em[42]: pop_flat.set_index(['estado', 'ano'])

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.

Agregações de dados em vários índices


Vimos anteriormente que o Pandas possui métodos integrados de agregação de dados, como
média(), soma() e máximo(). Para dados indexados hierarquicamente, eles podem ser transmitidos
parâmetro de nível que controla em qual subconjunto de dados o agregado é calculado.

Por exemplo, voltemos aos nossos dados de saúde:

Em[43]: dados_de_saúde

Out[43]: tipo de assunto Prumo Guido Processar

ano Temperatura de RH Temperatura de RH Temperatura de RH

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:

Em[44]: data_mean = health_data.mean(nível='ano')


média_dados

Fora[44]: sujeito Bob Guido Processar

temperatura HR Tipo de Temperatura de RH Temperatura de RH

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:

Em [45]: data_mean.mean(axis=1, level='type')

Fora[45]: tipo RH Temperatura

ano
2013 36,833333 37,000000
2014 46,000000 37,283333

140 | Capítulo 3: Manipulação de dados com Pandas


Machine Translated by Google

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.

Combinando conjuntos de dados: Concat e Append


Alguns dos estudos de dados mais interessantes provêm da combinação de diferentes fontes de
dados. Essas operações podem envolver qualquer coisa, desde uma concatenação muito direta de
dois conjuntos de dados diferentes até junções e mesclagens mais complicadas no estilo de banco de
dados que lidam corretamente com quaisquer sobreposições entre os conjuntos de dados. Séries e
DataFrames são construídos com esse tipo de operação em mente, e o Pandas inclui funções e
métodos que tornam esse tipo de manipulação de dados rápida e direta.

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.

Começamos com as importações padrão:

In[1]: importar pandas como pd


importar numpy como np

Combinando conjuntos de dados: Concat e Append | 141


Machine Translated by Google

Por conveniência, definiremos esta função, que cria um DataFrame de um formato específico
que será útil a seguir:

Em[2]: def make_df(cols, ind):


"""Crie rapidamente um DataFrame"""
data = {c: [str(c) + str(i) for i in ind] for c in cols}
return
pd.DataFrame(data, ind)

# exemplo DataFrame
make_df('ABC', range(3))

Fora[2]: AB C
0 A0 B0 C0
1A1 B1 C1
2A2 B2 C2

Lembre-se: Concatenação de arrays NumPy A

concatenação de objetos Series e DataFrame é muito semelhante à concatenação de arrays


NumPy, o que pode ser feito por meio da função np.concatenate conforme discutido em
“Noções básicas de arrays NumPy” na página 42. Lembre-se de que com isso, você pode
combinar o conteúdo de dois ou mais arrays em um único array:

Em[4]: x = [1, 2, 3] y = [4,


5, 6] z = [7, 8, 9]
np.concatenate([x,
y, z])

Fora[4]: matriz([1, 2, 3, 4, 5, 6, 7, 8, 9])

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:

Em[5]: x = [[1, 2], [3, 4]]

np.concatenate([x, x], eixo=1)

Fora[5]: array([[1, 2, 1, 2],


[3, 4, 3, 4]])

Concatenação Simples com pd.concat


O Pandas tem uma função, pd.concat(), que possui uma sintaxe semelhante a np.concatenate ,
mas contém uma série de opções que discutiremos em breve:

# Assinatura no Pandas v0.18


pd.concat(objs, axis=0, join='outer', join_axes=None, ignore_index=False, keys=None,
levels=None, names=None, verify_integrity=False, copy=True )

142 | Capítulo 3: Manipulação de dados com Pandas


Machine Translated by Google

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:

Em[6]: ser1 = pd.Series(['A', 'B', 'C'], índice=[1, 2, 3])


ser2 = pd.Series(['D', 'E', 'F'], índice=[4, 5, 6])
pd.concat([ser1, ser2])

Fora[6]: 1 2 A
B
3 C
4 D
5 E

6 F
dtype: objeto

Também funciona para concatenar objetos de dimensões superiores, como DataFrames:

Em[7]: df1 = make_df('AB', [1, 2])


df2 = make_df('AB', [3, 4])
imprimir(df1); imprimir(df2); imprimir(pd.concat([df1, df2]))

df1 df2 pd.concat([df1, df2])


AB AB AB
1A1 B1 3A3 B3 1A1 B1
2A2 B2 4A4 B4 2A2 B2
3A3 B3
4A4 B4

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:

Em[8]: df3 = make_df('AB', [0, 1])


df4 = make_df('CD', [0, 1])
imprimir(df3); imprimir(df4); imprimir(pd.concat([df3, df4], eixo='col'))

df3 df4 pd.concat([df3, df4], eixo='col')


AB C D ABCD
0 A0 B0 0 C0 D0 0 A0 B0 C0 D0
1A1 B1 1C1 D1 1A1 B1 C1 D1

Poderíamos ter especificado de forma equivalente axis=1; aqui usamos o mais intuitivo
eixo='col'.

Índices duplicados

Uma diferença importante entre np.concatenate e pd.concat é que o Pandas


a concatenação preserva os índices, mesmo que o resultado tenha índices duplicados! Considerar
este exemplo simples:

Em[9]: x = make_df('AB', [0, 1])


y = make_df('AB', [2, 3])

Combinando conjuntos de dados: Concat e Append | 143


Machine Translated by Google

y.index = x.index # cria índices duplicados!


imprimir(x); imprimir(y); imprimir(pd.concat([x, y]))
x e pd.concat([x, y])
AB AB AB
0 A0 B0 0 A2 B2 0 A0 B0
1A1 B1 1A3 B3 1A1 B1
0 A2 B2
1A3 B3

Observe os índices repetidos no resultado. Embora isso seja válido em DataFrames, o


o resultado é muitas vezes indesejável. pd.concat() nos dá algumas maneiras de lidar com isso.

Capturar as repetições como um erro. Se você quiser simplesmente verificar se os índices no


resultado de pd.concat() não se sobrepõem, você pode especificar o sinalizador verify_integrity .
Com isto definido como True, a concatenação irá gerar uma exceção se houver duplicatas
índices. Aqui está um exemplo, onde, para maior clareza, capturaremos e imprimiremos a mensagem de erro:

Em[10]: tente:
pd.concat([x, y], verificar_integridade=Verdadeiro)
exceto ValueError como e:
print("Erro de valor:", e)

ValueError: Os índices têm valores sobrepostos: [0, 1]

Ignorando o índice. Às vezes, o índice em si não importa e você preferiria


simplesmente ser ignorado. Você pode especificar esta opção usando o sinalizador ignore_index . Com
definido como True, a concatenação criará um novo índice inteiro para o resultado
Series:
Em[11]: imprimir(x); imprimir(y); imprimir(pd.concat([x, y], ignore_index=True))
x e pd.concat([x, y], ignore_index=True)
AB AB AB
0 A0 B0 0 A2 B2 0 A0 B0
1A1 B1 1A3 B3 1A1 B1
2A2 B2
3A3 B3

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:

Em[12]: imprimir(x); imprimir(y); imprimir(pd.concat([x, y], chaves=['x', 'y']))


x e pd.concat([x, y], chaves=['x', 'y'])
AB AB AB
0 A0 B0 0 A2 B2 x 0 A0 B0
1A1 B1 1A3 B3 1A1 B1
e 0 A2 B2
1A3 B3

144 | Capítulo 3: Manipulação de dados com Pandas


Machine Translated by Google

O resultado é um DataFrame com indexação múltipla e podemos usar as ferramentas discutidas em


“Indexação Hierárquica” na página 128 para transformar esses dados na representação
estamos interessados.

Concatenação com junções

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:

Em[13]: df5 = make_df('ABC', [1, 2])


df6 = make_df('BCD', [3, 4])
imprimir(df5); imprimir(df6); imprimir(pd.concat([df5, df6])
df5 df6 pd.concat([df5, df6])
abc BCD ABCD
1A1 B1 C1 3 B3 C3 D3 1 A1 B1 C1 NaN
2A2 B2 C2 4 B4 C4 D4 2A2 B2 C2NaN
3NaN B3 C3 D3
4NaN B4 C4 D4

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':

Em[14]: imprimir(df5); imprimir(df6);


imprimir(pd.concat([df5, df6], join='inner'))

df5 df6 pd.concat([df5, df6], join='inner')


abc BCD B C
1A1 B1 C1 3 B3 C3 D3 1 B1 C1
2A2 B2 C2 4 B4 C4 D4 2 B2 C2
3 B3 C3
4 B4 C4

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:

Em[15]: imprimir(df5); imprimir(df6);


imprimir(pd.concat([df5, df6], join_axes=[df5.columns]))
df5 df6 pd.concat([df5, df6], join_axes=[df5.columns])
abc BCD AB C
1A1 B1 C1 3 B3 C3 D3 1A1 B1 C1
2A2 B2 C2 4 B4 C4 D4 2A2 B2 C2

Combinando conjuntos de dados: Concat e Append | 145


Machine Translated by Google

3NaN B3 C3
4NaN B4 C4

A combinação de opções da função pd.concat permite uma ampla gama de comportamentos


possíveis ao unir dois conjuntos de dados; tenha isso em mente ao usar essas ferramentas
para seus próprios dados.

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):

Em[16]: imprimir(df1); imprimir(df2); imprimir(df1.append(df2))


df1 df2 df1.append(df2)
AB AB AB1
1A1 B1 3A3 B3 A1 B1
2A2 B2 4A4 B4 2A2 B2
3A3 B3
4A4 B4

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.

Combinando conjuntos de dados: mesclar e unir

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

comportamento implementado em pd.merge() é um subconjunto do que é conhecido como


álgebra relacional, que é um conjunto formal de regras para manipulação de dados relacionais
e forma a base conceitual das operações disponíveis na maioria dos bancos de dados. A força do

146 | Capítulo 3: Manipulação de dados com Pandas


Machine Translated by Google

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.

O Pandas implementa vários desses blocos de construção fundamentais no pd.merge()


função e o método join() relacionado de Series e DataFrames. Como veremos,
eles permitem vincular dados de diferentes fontes com eficiência.

Categorias de junções

A função pd.merge() implementa vários tipos de junções: um para um,


junções muitos para um e muitos para muitos. Todos os três tipos de junções são acessados através de um
chamada idêntica à interface pd.merge() ; o tipo de junção realizada depende do
forma dos dados de entrada. Aqui mostraremos exemplos simples dos três tipos de
mesclagens e discutir opções detalhadas mais abaixo.

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

Para combinar essas informações em um único DataFrame, podemos usar o pd.merge()


função:

Em[3]: df3 = pd.merge(df1, df2)


df3

Saída[3]: grupo de funcionários Hire_date


0 Bob Contabilidade 2008
1 Jake Engenharia 2012
2 Lisa Engenharia 2004
3 Processar RH 2014

Combinando conjuntos de dados: mesclar e unir | 147


Machine Translated by Google

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:

In[4]: df4 = pd.DataFrame({'grupo': ['Contabilidade', 'Engenharia', 'RH'],


'supervisor': ['Carly', 'Guido', 'Steve']})
imprimir(df3); imprimir(df4); imprimir(pd.merge(df3, df4))

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

O DataFrame resultante possui uma coluna adicional com as informações do “supervisor”,


onde a informação é repetida em um ou mais locais conforme exigido pelas entradas.

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.

Ao realizar uma junção muitos-para-muitos, podemos recuperar as habilidades associadas a qualquer


pessoa individual:

In[5]: df5 = pd.DataFrame({'grupo': ['Contabilidade', 'Contabilidade',


'Engenharia', 'Engenharia', 'RH', 'RH'],

148 | Capítulo 3: Manipulação de dados com Pandas


Machine Translated by Google

'habilidades': ['matemática', 'planilhas', 'codificação', 'linux',


'planilhas', 'organização']})
imprimir(df1); imprimir(df5); imprimir(pd.merge(df1, df5))

df5
grupo de funcionários df1 grupo habilidades

0 Bob Contabilidade 0 Matemática contábil


1 Jake Engenharia 1 Planilhas contábeis
2 Lisa Engenharia 2 Engenharia codificação
3 Processar RH 3 Engenharia Linux
4 Planilhas de RH
5 Organização de RH

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.

Especificação da chave de mesclagem

Já vimos o comportamento padrão de pd.merge(): ele procura por um ou mais


combinando nomes de colunas entre as duas entradas e usa isso como chave. No entanto,
muitas vezes os nomes das colunas não combinam tão bem e pd.merge() fornece uma variedade
de opções para lidar com isso.

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:

Em[6]: imprimir(df1); imprimir(df2); imprimir(pd.merge(df1, df2, on='funcionário'))

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

Combinando conjuntos de dados: mesclar e unir | 149


Machine Translated by Google

pd.merge(df1, df2, on='funcionário')


grupo de funcionários contratação_data
0 Bob Contabilidade 2008
1 Jake Engenharia Lisa 2012
2 Engenharia RH 2004
3 Processar 2014

Esta opção funciona apenas se os DataFrames esquerdo e direito tiverem a coluna especificada
hum nome.

As palavras-chave left_on e right_on

À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

pd.merge(df1, df3, left_on="funcionário", right_on="nome")


nome do grupo de funcionários salário
0 Bob Contabilidade Bob 70000
1 Jake Engenharia Jake 80000
2 Lisa Engenharia Lisa 120000
3 Processar RH 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)

Fora[8]: funcionário 0 salário do grupo


Bob Contabilidade 70000
1 Jake Engenharia 80000
2 Lisa Engenharia 120000
3 Processar RH 90.000

150 | Capítulo 3: Manipulação de dados com Pandas


Machine Translated by Google

As palavras-chave left_index e right_index

Às vezes, em vez de mesclar em uma coluna, você gostaria de mesclar em uma


índice. Por exemplo, seus dados podem ser assim:
Em[9]: df1a = df1.set_index('funcionário')
df2a = df2.set_index('funcionário')
imprimir(df1a); imprimir (df2a)

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

pd.merge(df1a, df2a, left_index=True, right_index=True)


data_de contratação do grupo

funcionário
Lisa Engenharia 2004
Prumo Contabilidade 2008
Jake Engenharia 2012
Processar RH 2014

Por conveniência, DataFrames implementam o método join() , que executa um


merge cujo padrão é juntar-se a índices:
Em[11]: imprimir(df1a); imprimir(df2a); imprimir(df1a.join(df2a))

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

Combinando conjuntos de dados: mesclar e unir | 151


Machine Translated by Google

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

pd.merge(df1a, df3, left_index=True, right_on='nome')


nome do grupo salário
0 Contabilidade Bob 70000
1Engenharia Jake 80000
2 Engenharia Lisa 120000
3 RH 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.

Especificando conjunto aritmético para junções

Em todos os exemplos anteriores, encobrimos uma consideração importante em


realizando uma junção: o tipo de conjunto aritmético usado na junção. Isso surge quando um
o valor aparece em uma coluna-chave, mas não na outra. Considere este exemplo:

In[13]: df6 = pd.DataFrame({'nome': ['Pedro', 'Paulo', 'Maria'],


'comida': ['peixe', 'feijão', 'pão']},
colunas=['nome', 'comida'])
df7 = pd.DataFrame({'nome': ['Maria', 'José'],
'bebida': ['vinho', 'cerveja']},
colunas=['nome', 'bebida'])
imprimir(df6); imprimir(df7); imprimir(pd.merge(df6, df7))

152 | Capítulo 3: Manipulação de dados com Pandas


Machine Translated by Google

df6 df7 pd.merge(df6, df7)


nomear comida nome bebida nome comida bebida
0 Pedro peixe 0 Vinho Maria 0 Vinho Pão Maria
1 feijão paul 1 cerveja José
2 Pão Maria

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':

Em[14]: pd.merge(df6, df7, how='inner')

Fora[14]: nome comida bebida


0 Maria pão vinho

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:

Em[15]: imprimir(df6); imprimir(df7); imprimir(pd.merge(df6, df7, how='externo'))

df6 df7 pd.merge(df6, df7, how='externo')


nomear comida nome bebida nome comida bebida
0 Pedro peixe 0 Vinho Maria 0 Pedro peixe NaN
1 feijão paul 1 cerveja José 1 Paulo feijão NaN
2 Pão Maria 2 Vinho Pão Maria
3 José Cerveja NaN

A junção à esquerda e a junção à direita retornam a junção sobre as entradas esquerda e direita, respectivamente.
ativamente. Por exemplo:

Em[16]: imprimir(df6); imprimir(df7); imprimir(pd.merge(df6, df7, como='esquerda'))

df6 df7 pd.merge(df6, df7, como='esquerda')


nomear comida nome bebida nome comida bebida
0 Pedro peixe 0 Vinho Maria 0 Pedro peixe NaN
1 feijão paul 1 cerveja José 1 Paulo feijão NaN
2 Pão Maria 2 Vinho Pão Maria

As linhas de saída agora correspondem às entradas na entrada esquerda. Usando como='certo'


funciona de maneira semelhante.

Todas essas opções podem ser aplicadas diretamente a qualquer uma das opções de junção anteriores.
tipos.

Nomes de colunas sobrepostos: a palavra-chave sufixos


Finalmente, você pode acabar em um caso em que seus dois DataFrames de entrada tenham conflitos
nomes de colunas. Considere este exemplo:

Em[17]: df8 = pd.DataFrame({'nome': ['Bob', 'Jake', 'Lisa', 'Sue'],


'classificação': [1, 2, 3, 4]})

Combinando conjuntos de dados: mesclar e unir | 153


Machine Translated by Google

df9 = pd.DataFrame({'nome': ['Bob', 'Jake', 'Lisa', 'Sue'],


'classificação': [3, 1, 4, 2]})
imprimir(df8); imprimir(df9); imprimir(pd.merge(df8, df9, on="nome"))

df8 df9 pd.merge(df8, df9, on="nome")


classificação de nome classificação de nome nome classificação_x classificação_y
0Bob 1 0Bob 3 0 Bob 3 1
1Jake 2 1Jake 1 1Jake 2 1
doisLisa 3 doisLisa 4 doisLisa 3 4
3 Sue 4 3 Sue 2 3 Sue 4 2

Como a saída teria dois nomes de colunas conflitantes, a função de mesclagem


anexa automaticamente um sufixo _x ou _y para tornar as colunas de saída exclusivas. Se estes
os padrões são inadequados, é possível especificar um sufixo personalizado usando os sufixos
palavra-chave:

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

pd.merge(df8, df9, on="nome", sufixos=["_L", "_R"])


nome classificação_L classificação_R
0 Bob 3 1
1Jake 2 1
doisLisa 3 4
3 Sue 4 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.

Exemplo: dados dos estados dos EUA

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

154 | Capítulo 3: Manipulação de dados com Pandas


Machine Translated by Google

# !curl -O https:// raw.githubusercontent.com/ jakevdp/


# dados-USstates/ master/ state-population.csv
# !curl -O https:// raw.githubusercontent.com/ jakevdp/
# dados-USstates/ master/ state-areas.csv
# !curl -O https:// raw.githubusercontent.com/ jakevdp/
# dados-USstates/ master/ state-abbrevs.csv

Vamos dar uma olhada nos três conjuntos de dados, usando a função read_csv() do Pandas:

Em[20]: pop = pd.read_csv('state-population.csv')


áreas = pd.read_csv('state-areas.csv')
abrevs = pd.read_csv('state-abbrevs.csv')

imprimir(pop.head()); imprimir(áreas.head()); imprimir(abbrevs.head())

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.

In[21]: mesclado = pd.merge(pop, abbrevs, how='outer',


left_on='estado/região', right_on='abreviatura')
merged = merged.drop('abbreviation', 1) # elimina informações duplicadas
mesclado.head()

Fora[21]: estado/região idades ano população estado


0 AL sub18 2012 1117489,0 Alabama
1 total 2012 4817528,0 Alabama
2 AL AL sub18 2010 1130966,0 Alabama
AL total de 2010 4785570,0 Alabama
34 AL sub18 2011 1125763,0 Alabama

Combinando conjuntos de dados: mesclar e unir | 155


Machine Translated by Google

Vamos verificar se houve alguma incompatibilidade aqui, o que podemos fazer


procurando linhas com nulos:

Em[22]: mesclado.isnull().any()

Fora[22]: idades estaduais/ Falso


regiões Falso
ano Falso
população Verdadeiro

estado Verdadeiro

tipo d: bool

Algumas informações da população são nulas; vamos descobrir quais são!

Em[23]: mesclado[mesclado['população'].isnull()].head()

Fora[23]: estado/região idades ano população estado


2448 PR sub18 1990 total NaN NaN
2449 RP 1990 NaN NaN
2450 RP total 1991 NaN NaN
2451 PR sub18 1991 total NaN NaN
2452 RP 1993 NaN NaN

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:

Em[24]: merged.loc[merged['state'].isnull(), 'state/region'].unique()

Out[24]: array(['PR', 'EUA'], dtype=objeto)

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:

In[25]: merged.loc[merged['state/region'] == 'PR', 'state'] = 'Porto Rico'


merged.loc[merged['state/region'] == 'EUA', 'state'] = 'Estados Unidos'
mesclado.isnull().any()

Out[25]: estado/região idade Falso


ano Falso
Falso
população Verdadeiro

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:

156 | Capítulo 3: Manipulação de dados com Pandas


Machine Translated by Google

In[26]: final = pd.merge(merged, areas, on='state', how='left')


final.head()

Fora[26]: estado/região idades ano população área do estado (m²)


AL sub18 2012 1117489,0 Alabama 52423,0
01 AL total 2012 4817528,0 Alabama 52423,0
2 AL sub18 2010 1130966,0 Alabama total 2010 4785570,0 52423,0
3 Alabama AL AL sub18 2011 1125763,0 Alabama 52423,0
4 52423,0

Novamente, vamos verificar se há nulos para ver se houve alguma incompatibilidade:

Em[27]: final.isnull().any()

Fora[27]: idades estaduais/ Falso


regiões Falso
ano Falso
população Verdadeiro

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:

In[28]: final['state'][final['area (sq. mi)'].isnull()].unique()

Out[28]: array(['Estados Unidos'], dtype=object)

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()

Fora[29]: estado/região idades ano população área do estado (m²)


AL sub18 2012 1117489,0 Alabama 52423,0
01 AL total 2012 4817528,0 Alabama 52423,0
2 AL sub18 2010 1130966,0 Alabama total 2010 4785570,0 52423,0
3 Alabama AL AL sub18 2011 1125763,0 Alabama 52423,0
4 52423,0

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):

In[30]: data2010 = final.query("ano == 2010 & idades == 'total'")


data2010.head()

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

Combinando conjuntos de dados: mesclar e unir | 157


Machine Translated by Google

101 AZ total 2010 6408790,0 Arizona AR total 2010 2922280,0 114006,0


189 Arkansas CA total 2010 37333601,0 Califórnia 53182,0
197 163707,0

Agora vamos calcular a densidade populacional e exibi-la em ordem. Começaremos rein-


indexando nossos dados sobre o estado e, em seguida, calcule o resultado:

Em[31]: data2010.set_index('estado', inplace=True)


densidade = data2010['população'] / data2010['área (m²)']

In[32]: densidade.sort_values(ascendente=Falso, inplace=True)


densidade.head()

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.

Também podemos verificar o final da lista:

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

158 | Capítulo 3: Manipulação de dados com Pandas


Machine Translated by Google

explore agregações no Pandas, desde operações simples semelhantes às que vimos em


Matrizes NumPy, até operações mais sofisticadas baseadas no conceito de groupby.

Dados dos planetas

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:

In[2]: importar seaborn como sns


planetas = sns.load_dataset('planetas')
planetas.forma

Fora[2]: (1035, 6)

Em[3]: planetas.head()

Fora[3]: método número orbital_period massa distância ano


0 Velocidade Radial 1 2006 269.300 7,10 77,40
1 Velocidade Radial 1 874.774 2,21 56,95 2008
2 Velocidade Radial 1 763.000 2,60 19,84 19,40 2011
3 Velocidade Radial 1 326.030 110,62 2007
4 Velocidade Radial 1 516.220 10,50 119,47 2009

Isto contém alguns detalhes sobre os mais de 1.000 exoplanetas descobertos até 2014.

Agregação simples em Pandas

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:

Em[4]: rng = np.random.RandomState(42)


ser = pd.Series(rng.rand(5))
ser

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:

Agregação e Agrupamento | 159


Machine Translated by Google

Em[7]: df = pd.DataFrame({'A': rng.rand(5),


'B': rng.rand(5)})
df

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

Ao especificar o argumento do eixo , você pode agregar em cada linha:

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

A série Pandas e DataFrames incluem todos os agregados comuns mencionados em


“Agregações: mínimo, máximo e tudo o mais” na página 58; além disso, há
é um método de conveniência description() que calcula vários agregados comuns para
cada coluna e retorna o resultado. Vamos usar isso nos dados dos Planetas, por enquanto, solte
ping linhas com valores ausentes:

Em [10]: planets.dropna().describe()

Fora[10]: número orbital_period contagem massa distância ano


498,00000 498,000000 498,000000 498,000000 498,000000
significar 1.73494 835.778671 2.509320 52.068213 2007.377510
padrão 1.17572 1469.128259 3.636274 46.596041 4.167284
min 1,00000 1,328300 0,003600 1,350000 1989,000000
25% 1,00000 38.272250 0,212500 24,497500 2005,000000
50% 1,00000 357.000000 1,245000 39,940000 2009,000000
75% 2,00000 999.600000 2,867500 59,332500 2011,000000
máx. 6,00000 17337,500000 25,000000 354,000000 2014,000000

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.

A Tabela 3-3 resume algumas outras agregações integradas do Pandas.

160 | Capítulo 3: Manipulação de dados com Pandas


Machine Translated by Google

Tabela 3-3. Listagem de métodos de agregação Pandas

Agregação Descrição
contar() Número total de itens

first(), last() Primeiro e último item

média(), mediana() Média e mediana

min(), máx() Mínimo e máximo

std(), var() Desvio padrão e variância

louco() Desvio médio absoluto

produção() Produto de todos os itens

soma() Soma de todos os itens

Todos esses são métodos de objetos DataFrame e Series .

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.

GroupBy: Dividir, Aplicar, Combinar


Agregações simples podem dar uma ideia do seu conjunto de dados, mas geralmente preferimos
agregar condicionalmente em algum rótulo ou índice: isso é implementado na chamada operação
groupby . O nome “group by” vem de um comando no SQL
linguagem de banco de dados, mas talvez seja mais esclarecedor pensar nisso nos termos primeiro
cunhado por Hadley Wickham, famoso pelo Rstats: dividir, aplicar, combinar.

Dividir, aplicar, combinar

Um exemplo canônico desta operação split-apply-combine, onde o “apply” é um


agregação de soma, é ilustrada na Figura 3-1.

A Figura 3-1 deixa claro o que o GroupBy realiza:

• A etapa dividida envolve dividir e agrupar um DataFrame dependendo do


valor da chave especificada.

• A etapa de aplicação envolve calcular alguma função, geralmente uma agregada, transformada
informação ou filtragem dentro dos grupos individuais.

• A etapa combinar mescla os resultados dessas operações em uma matriz de saída.

Agregação e Agrupamento | 161


Machine Translated by Google

Figura 3-1. Uma representação visual de uma operação groupby

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

Out[11]: dados principais


0A0
1B 1
2C 2
3A
4B 34
5ºC 5

Podemos calcular a operação mais básica de dividir-aplicar-combinar com o método groupby() de


DataFrames, passando o nome da coluna-chave desejada:

Em[12]: df.groupby('chave')

Out [12]: <objeto pandas.core.groupby.DataFrameGroupBy em 0x117272160>

162 | Capítulo 3: Manipulação de dados com Pandas


Machine Translated by Google

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')

Out[14]: <objeto pandas.core.groupby.DataFrameGroupBy em 0x1172727b8>

Em[15]: planets.groupby('método')['orbital_period']

Out[15]: <objeto pandas.core.groupby.SeriesGroupBy em 0x117272da0>

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()

Agregação e Agrupamento | 163


Machine Translated by Google

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:

In[17]: for (método, grupo) em planets.groupby('método'):


print("{0:30s} shape={1}".format(método, group.shape))

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

164 | Capítulo 3: Manipulação de dados com Pandas


Machine Translated by Google

Tempo Pulsar 5,0 1998,400000 8,384510 1992,0 1992,00


Variações de tempo de pulsação 1,0 2007,000000 NaN 2.007,0 2.007,00
Velocidade Radial 553,0 2007,518987 4,249052 1989,0 2005,00
Transito 397,0 2011,236776 2,077867 2002,0 2010,00
Variações de tempo de trânsito 4,0 2012,500000 1,290994 2011,0 2011,75

50% 75% máx.


método
Astrometria 2011,5 2012,25 2013,0
Variações de tempo do Eclipse 2010,0 2011,00 2012,0
Imagem 2009,0 2011,00 2013,0
Microlente 2010,0 2012,00 2013,0
Modulação de brilho orbital 2011.0 2012.00 2013.0
Pulsar Cronometragem 1994,0 2003,00 2011,0
Variações de tempo de pulsação 2007,0 2007,00 2007,0
Trânsito de velocidade 2009,0 2011,00 2014,0
radial 2012,0 2013,00 2014,0
Variações de tempo de trânsito 2012,5 2013,25 2014,0

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!

Agregar, filtrar, transformar, aplicar

A discussão anterior focou na agregação para a operação combinada, mas


há mais opções disponíveis. Em particular, os objetos GroupBy possuem agregação(),
métodos filter(), transform() e apply() que implementam com eficiência uma variedade de
operações úteis antes de combinar os dados agrupados.

Para os fins das subseções a seguir, usaremos este DataFrame:

Em[19]: rng = np.random.RandomState(0)


df = pd.DataFrame({'chave': ['A', 'B', 'C', 'A', 'B', 'C'],
'dados1': intervalo(6),
'dados2': rng.randint(0, 10, 6)},
colunas = ['chave', 'dados1', 'dados2'])
df

Saída[19]: dados principais1 dados2


0A05
1B 1 0
2C 2 3

Agregação e Agrupamento | 165


Machine Translated by Google

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:

Em[20]: df.groupby('chave').agregado(['min', np.median, max])

Fora[20]: dados1 dados2


min mediana max min mediana max
chave
A 0 1,5 3 3 4,0 5
B 1 2,5 4 0 3,5 7
C 2 3,5 5 3 6,0 9

Outro padrão útil é passar nomes de colunas de mapeamento de dicionário para operações
a ser aplicado nessa coluna:

Em[21]: df.groupby('chave').agregado({'dados1': 'min',


'dados2': 'máx'})

Fora[21]: dados1 dados2


chave
A 5
B 01 7
C 2 9

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

166 | Capítulo 3: Manipulação de dados com Pandas


Machine Translated by Google

2 C 2 3
4 B 4 7
5 C 5 9

A função filter() deve retornar um valor booleano especificando se o grupo


passa pela filtragem. Aqui porque o grupo A não tem desvio padrão maior
maior que 4, ele será eliminado do resultado.

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:

Em [23]: df.groupby('key').transform(lambda x: x - x.mean())

Saída[23]: dados1 dados2


0 -1,5 1,0
1 -1,5 -3,5
2 -1,5 -3,0
3 1,5 -1,0
4 1,5 3.5
5 1,5 3,0

O método apply(). O método apply() permite aplicar uma função arbitrária ao


resultados do grupo. A função deve pegar um DataFrame e retornar um Pandas
objeto (por exemplo, DataFrame, Series) ou um escalar; a operação da colheitadeira será adaptada para
o tipo de saída retornada.

Por exemplo, aqui está um apply() que normaliza a primeira coluna pela soma dos
segundo:

Em[24]: def norma_por_dados2(x):


# x é um DataFrame de valores de grupo
x['dados1'] /= x['dados2'].soma()
retornar x

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ê!

Agregação e Agrupamento | 167


Machine Translated by Google

Especificando a chave de divisão

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

Claro, isso significa que há outra maneira mais detalhada de realizar o


df.groupby('key') de antes:

Em[26]: imprimir(df); imprimir(df.groupby(df['chave']).sum())

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:

Em[27]: df2 = df.set_index('chave')


mapeamento = {'A': 'vogal', 'B': 'consoante', 'C': 'consoante'}
imprimir(df2); imprimir(df2.groupby(mapeamento).sum())

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

168 | Capítulo 3: Manipulação de dados com Pandas


Machine Translated by Google

Qualquer função Python. Semelhante ao mapeamento, você pode passar qualquer função Python que
insira o valor do índice e produza o grupo:

Em[28]: imprimir(df2); imprimir(df2.groupby(str.inferior).mean())

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:

Em [29]: df2.groupby([str.lower, mapeamento]).mean()

Fora[29]: dados1 dados2


uma vogal 1,5 4,0
b consoante 2,5 3.5
c consoante 3.5 6,0

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:

In[30]: década = 10 * (planetas['ano'] // 10)


década = década.astype(str) + 's'
década.name = 'década'
planetas.groupby(['método', década])['número'].sum().unstack().fillna(0)

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!

Agregação e Agrupamento | 169


Machine Translated by Google

Aqui eu sugeriria aprofundar essas poucas linhas de código e avaliar


etapas usuais para garantir que você entenda exatamente o que eles estão fazendo com o resultado. Isso é
certamente um exemplo um tanto complicado, mas a compreensão dessas peças lhe dará
você os meios para explorar de forma semelhante seus próprios dados.

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.

Motivando tabelas dinâmicas


Para os exemplos desta seção usaremos o banco de dados de passageiros do Titanic
disponível na biblioteca Seaborn (consulte “Visualização com Seaborn” na página 311):

In[1]: importar numpy como np


importar pandas como pd
importar seaborn como sns
titânico = sns.load_dataset('titânico')

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

quem adulto_masculino deck embarca_town vivo sozinho


0 homem Verdadeiro NaN Southampton não Falso
1 mulher Falso C Cherbourg sim Falso
2 mulheres Falso NaN Southampton sim Verdadeiro
3 mulher Falso C Southampton sim Falso
4 homem Verdadeiro NaN Southampton não Verdadeiro

Contém uma riqueza de informações sobre cada passageiro daquela viagem malfadada,
incluindo sexo, idade, classe, tarifa paga e muito mais.

170 | Capítulo 3: Manipulação de dados com Pandas


Machine Translated by Google

Tabelas dinâmicas manualmente

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:

Em [4]: titanic.groupby(['sexo', 'classe'])['sobreviveu'].agregado('mean').unstack()

Fora[4]: aula Primeiro Segundo Terceiro


sexo
feminino 0,968085 0,921053 0,500000
macho 0,368852 0,157407 0,135447

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.

Sintaxe da tabela dinâmica

Aqui está o equivalente à operação anterior usando o método pivot_table de


Quadros de dados:

Em[5]: titanic.pivot_table('sobreviveu', index='sexo', colunas='classe')

Fora[5]: aula Primeiro Segundo Terceiro


sexo
feminino 0,968085 0,921053 0,500000
macho 0,368852 0,157407 0,135447

Isso é eminentemente mais legível que a abordagem GroupBy e produz o mesmo


resultado. Como seria de esperar de um cruzeiro transatlântico do início do século XX, a sobrevivência

Tabelas dinâmicas | 171


Machine Translated by Google

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!).

Tabelas dinâmicas multinível

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 :

Em[6]: idade = pd.cut(titanic['idade'], [0, 18, 80])


titanic.pivot_table('sobreviveu', ['sexo', idade], 'classe')

Fora[6]: aula Primeiro Segundo Terceiro


sexo idade

feminino (0, 18] 0,909091 1,000000 0,511628


(18, 80] 0,972973 0,900000 0,423729
masculino (0, 18] 0,800000 0,600000 0,215686
(18, 80] 0,375000 0,071429 0,133663

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:

In[7]: tarifa = pd.qcut(titanic['fare'], 2)


titanic.pivot_table('sobreviveu', ['sexo', idade], [tarifa, 'classe'])

Fora[7]:
classe [0, 14,454]
tarifária Primeiro Segundo Terceiro \\
sexo idade

feminino (0, 18] (18, NaN 1,000000 0,714286


80] NaN 0,880000 0,444444
masculino (0, 18] NaN 0,000000 0,260870
(18, 80] 0,0 0,098039 0,125000

Fazer (14.454, 512.329]


aula Primeiro segundo Terceiro
sexo idade

feminino (0, 18] (18, 0,909091 1,000000 0,318182


80] 0,972973 0,914286 0,391304
masculino (0, 18] 0,800000 0,818182 0,178571
(18, 80] 0,391304 0,030303 0,192308

O resultado é uma agregação quadridimensional com índices hierárquicos (ver “Hierarquia”.


Indexação ical” na página 128), mostrada em uma grade que demonstra a relação entre
os valores.

172 | Capítulo 3: Manipulação de dados com Pandas


Machine Translated by Google

Opções adicionais de tabela dinâmica

A assinatura completa da chamada do método pivot_table de DataFrames é a seguinte:

# assinatura de chamada a partir do Pandas 0.18


DataFrame.pivot_table(dados, valores=Nenhum, índice=Nenhum, colunas=Nenhum,
aggfunc='média', fill_value=Nenhum, margens=Falso,
dropna=Verdadeiro, margins_name='Todos')

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:

Em[8]: titanic.pivot_table(index='sexo', colunas='classe',


aggfunc={'sobreviveu':soma, 'tarifa':'média'})

Fora[8]: Fazer sobreviveu


aula Primeiro Segundo Terceiro Primeiro segundo terceiro
sexo
feminino 106.125798 21.970121 16.118810 masculino 91,0 70,0 72,0
67.226127 19.741782 12.661633 45,0 17,0 47,0

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 :

Em [9]: titanic.pivot_table('sobreviveu', index='sexo', colunas='classe', margens=True)

Fora[9]: aula Primeiro Segundo Terceiro Todos

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".

Tabelas dinâmicas | 173


Machine Translated by Google

Exemplo: dados de taxa de natalidade

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

Em[11]: nascimentos = pd.read_csv('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()

Out[12]: ano mês dia gênero nascimentos


0 1969 1 1 1 F4046
1969 1 1 M4440
2 1969 12 F4454
3 1969 12 M4548
4 1969 1 3 F4548

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');

174 | Capítulo 3: Manipulação de dados com Pandas


Machine Translated by Google

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

In[15]: quartis = np.percentile(nascimentos['nascimentos'], [25, 50, 75]) mu = quartis[1]


sig = 0,74 * (quartis[2]
- quartis[0])

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).

Tabelas dinâmicas | 175


Machine Translated by Google

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');

Figura 3-3. Média diária de nascimentos por dia da semana e década

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.

176 | Capítulo 3: Manipulação de dados com Pandas


Machine Translated by Google

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!)

Em[21]: nascimentos_por_data.index = [pd.datetime(2012, mês, dia)


para (mês, dia) em nascimentos_por_data.index]
nascimentos_por_data.head()

Saída[21]: 01/01/2012 4009.225


02/01/2012 4247.400
03/01/2012 4500.900
04/01/2012 4571.350
05-01-2012 4603.625
Nome: nascimentos, dtype: float64

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:

In[22]: # Trace os resultados


fig, machado = plt.subplots(figsize=(12, 4))
nascimentos_por_data.plot(ax=ax);

Figura 3-4. Média diária de nascimentos por data

Tabelas dinâmicas | 177


Machine Translated by Google

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!

Operações de String Vetorizadas


Um ponto forte do Python é sua relativa facilidade no tratamento e manipulação de dados de string.
O Pandas se baseia nisso e fornece um conjunto abrangente de operações de strings vetorizadas que
se tornam uma peça essencial do tipo de manipulação necessária quando se trabalha com (leia-se:
limpeza) dados do mundo real. Nesta seção, examinaremos algumas das operações de string do Pandas
e, em seguida, daremos uma olhada em como usá-las para limpar parcialmente um conjunto de dados
muito confuso de receitas coletadas da Internet.

Apresentando as operações de string do Pandas

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:

In[1]: importar numpy como


np x = np.array([2, 3, 5, 7, 11, 13])
x *2

Fora[1]: array([ 4, 6, 10, 14, 22, 26])

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:

In[2]: dados = ['pedro', 'Paulo', 'MARIA', 'gUIDO']


[s.capitalize() para s em dados]

Fora[2]: ['Pedro', 'Paulo', 'Maria', 'Guido']

Talvez isso seja suficiente para trabalhar com alguns dados, mas será interrompido se houver algum
valor ausente. Por exemplo:

In[3]: data = ['peter', 'Paul', None, 'MARY', 'gUIDO'] [s.capitalize()


para s em dados]

178 | Capítulo 3: Manipulação de dados com Pandas


Machine Translated by Google

-------------------------------------------------- -------------------------

-------------------------------------------------- -------------------------

AtributoError Traceback (última chamada mais recente)

<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]

AttributeError: O objeto 'NoneType' não possui atributo 'maiúsculo'


-------------------------------------------------- -------------------------

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:

In[4]: importar pandas como pd


nomes = pd.Series(dados)
nomes

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.

Operações de String Vetorizadas | 179


Machine Translated by Google

Tabelas de métodos de string 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:

Em[6]: monte = pd.Series(['Graham Chapman', 'John Cleese', 'Terry Gilliam',


'Eric Idle', 'Terry Jones', 'Michael Palin'])

Métodos semelhantes aos métodos de string do Python

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:

apenas() mais baixo() traduzir() é inferior()

brilhante() superior() começa com() isupper()

apenas() encontrar() termina com() énumérico()

centro() rfind() isalnum() édecimal()

zfill() índice() isalfa() dividir()

tira() rindex() édígito() rsdividir()

rstrip() capitalize() isspace() partição()

lstrip() caixa de troca() título() partição()

Observe que eles têm vários valores de retorno. Alguns, como lower(), retornam uma série de
cordas:

Em[7]: monte.str.lower()

Fora[7]: 0 1 Graham Chapman


John Cleese
2 Terry Gilliam
Eric ocioso
34 Terry Jones
Michael Palin
5 dtipo: objeto

Mas alguns outros retornam números:

Em[8]: monte.str.len()

Fora[8]: 0 1 14
11
2 13
3 9
4 11
5 13
tipo d: int64

180 | Capítulo 3: Manipulação de dados com Pandas


Machine Translated by Google

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()

Fora[10]: 0 [Graham, Chapman]


1 [João, Cleese]
2 [Terry, Gilliam]
[Eric, ocioso]
34 [Terry, Jones]
5 [Michael, Palin]
dtype: objeto

Veremos mais manipulações desse tipo de objeto de série de listas à medida que continuarmos
nossa discussão.

Métodos usando expressões regulares

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).

Tabela 3-4. Mapeamento entre métodos e funções do Pandas no módulo re do Python


Método Descrição

corresponder() Chame re.match() em cada elemento, retornando um booleano.

extract() Chame re.match() em cada elemento, retornando grupos correspondentes como strings.

findall() Chame re.findall() em cada elemento.

replace() Substitua as ocorrências do padrão por alguma outra string.

contains() Chame re.search() em cada elemento, retornando um booleano.

contar() Contar ocorrências de padrão.

dividir() Equivalente a str.split(), mas aceita regexps.

rsdividir() Equivalente a str.rsplit(), mas aceita regexps.

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:

Operações de String Vetorizadas | 181


Machine Translated by Google

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]$')

Fora[12]: 0 [Graham Chapman]


1 []
2 [Terry Gilliam]
3 []
4 [Terry Jones]
5 [Michael Palin]
dtype: objeto

A capacidade de aplicar expressões regulares de forma concisa em entradas de Série ou DataFrame


abre muitas possibilidades para análise e limpeza de dados.

Métodos diversos
Finalmente, existem alguns métodos diversos que permitem outras operações convenientes.
(ver Tabela 3-5).

Tabela 3-5. Outros métodos de string do Pandas

Método Descrição

Indexar cada elemento


pegar()
Fatie cada elemento
fatiar()

slice_replace() Substitui fatia em cada elemento pelo valor passado

gato() Concatenar strings

repita() Repetir valores

normalizar() Retornar forma Unicode de string

Adicione espaços em branco à esquerda, à direita ou em ambos os lados das strings


almofada()

Divida strings longas em linhas com comprimento menor que uma determinada largura
enrolar()

juntar() Junte strings em cada elemento da Série com separador passado

get_dummies() Extraia variáveis fictícias como um DataFrame

182 | Capítulo 3: Manipulação de dados com Pandas


Machine Translated by Google

Acesso e fatiamento de itens vetorizados. As operações get() e slice() , em particular,


habilite o acesso ao elemento vetorizado de cada array. Por exemplo, podemos obter uma fatia de
os três primeiros caracteres de cada array usando str.slice(0, 3). Observe que esse comportamento
ior também está disponível através da sintaxe de indexação normal do Python – por exemplo,
df.str.slice(0, 3) é equivalente a df.str[0:3]:
In[13]: monte.str[0:3]

Fora[13]: 0 Jogo
1 João
2 Ter
Diferente

34 Ter
5 Microfone

dtype: objeto

A indexação via df.str.get(i) e df.str[i] é semelhante.

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

Variáveis indicadoras. Outro método que requer um pouco mais de explicação é o


Método get_dummies() . Isso é útil quando seus dados possuem uma coluna contendo alguns
uma espécie de indicador codificado. Por exemplo, podemos ter um conjunto de dados que contém informações
ção na forma de códigos, como A = “nascido na América”, B = “nascido no Reino Unido
dom,” C=“gosta de queijo,” D=“gosta de spam”:

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

Fora[15]: informações nome


0 B|C|D Graham Chapman
1 B|D John Cleese
2 A|C Terry Gilliam
3 B|D Eric ocioso
4 B|C Terry Jones
5 a.C.|D. Michael Palin

Operações de String Vetorizadas | 183


Machine Translated by Google

A rotina get_dummies() permite dividir rapidamente essas variáveis indicadoras em um


Quadro de dados:

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.

Exemplo: banco de dados de

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.

Os scripts usados para compilar isso podem ser encontrados em https://github.com/fictivekin/openre


cipes, e o link para a versão atual do banco de dados também pode ser encontrado lá.

Na primavera de 2016, esse banco de dados tinha cerca de 30 MB e pode ser baixado e descompactado
com estes comandos:

Em[17]: # !curl -O http:// openrecipes.s3.amazonaws.com/ recipeitems-latest.json.gz


# !gunzip receitaitems-latest.json.gz

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)

ValueError: dados finais

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:

184 | Capítulo 3: Manipulação de dados com Pandas


Machine Translated by Google

In[19]: com open('recipeitems-latest.json') como f:


linha = f.readline()
pd.read_json(linha).forma

Fora[19]: (2, 12)

Sim, aparentemente cada linha é um JSON válido, então precisaremos juntá-las. Um


A maneira de fazer isso é construir uma representação de string contendo todos esses
Entradas JSON e, em seguida, carregue tudo com pd.read_json:

In[20]: # lê o arquivo inteiro em um array Python


com open('recipeitems-latest.json', 'r') como f:
# Extraia cada linha
data = (line.strip() para linha em f)
# Reformate para que cada linha seja o elemento de uma lista
data_json = "[{0}]".format(','.join(dados))
#leia o resultado como JSON
receitas = pd.read_json(data_json)

Em[21]: receitas.shape

Fora[21]: (173278, 17)

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()

Operações de String Vetorizadas | 185


Machine Translated by Google

Fora[23]: contagem 173278.000000


significar 244.617926
padrão 146.705285
0,000000
mínimo 25% 147,000000
50% 221.000000
75% 314.000000
máx. 9067.000000
Nome: ingredientes, dtype: float64

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 &amp; Bolo de Camada de Brownie com Chantilly &amp; Cobertura de cream
cheese e cenoura de maçapão

Isso certamente parece uma receita complicada.

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

Ou quantas receitas listam a canela como ingrediente:

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:

186 | Capítulo 3: Manipulação de dados com Pandas


Machine Translated by Google

In[28]: lista_especiarias = ['sal', 'pimenta', 'orégano', 'sálvia', 'salsa',


'alecrim', 'estragão', 'tomilho', 'páprica', 'cominho']

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:

In[30]: seleção = tempero_df.query('salsa & páprica & estragão')


len(seleção)

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.

Operações de String Vetorizadas | 187


Machine Translated by Google

Indo além com as

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.

Trabalhando com séries temporais

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.

Datas e horas em Python O mundo Python

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.

188 | Capítulo 3: Manipulação de dados com Pandas


Machine Translated by Google

Datas e horas nativas do Python: datetime e dateutil Os

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 :

In[1]: de data e hora importar data e hora


datahora(ano=2015, mês=7, dia=4)

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:

In[2]: from dateutil import parser date =


parser.parse("4 de julho de 2015") data

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.

Matrizes de tempos digitados: datetime64 do

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:

Em [4]: importar numpy


como np date = np.array('2015-07-04', dtype=np.datetime64)
date

Saída[4]: array(datetime.date(2015, 7, 4), dtype='datetime64[D]')

Trabalhando com séries temporais | 189


Machine Translated by Google

Uma vez formatada esta data, no entanto, podemos realizar operações vetorizadas rapidamente
nele:

In[5]: data + np.arange(12)

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')

Aqui está uma data e hora baseada em minutos:

Em [7]: np.datetime64('2015-07-04 12:00')

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:

Em [8]: np.datetime64('2015-07-04 12:59:59.50', 'ns')

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.

Tabela 3-6. Descrição dos códigos de data e hora

Significado do código Intervalo de tempo (relativo) Intervalo de tempo (absoluto)

E Ano ± 9,2e18 anos ± [9.2e18 AC, 9.2e18 DC]

M Mês 7,6e17 anos [7.6e17 AC, 7.6e17 DC]

EM Semana ± 1,7e17 anos [1.7e17 AC, 1.7e17 DC]

190 | Capítulo 3: Manipulação de dados com Pandas


Machine Translated by Google

Significado do código Intervalo de tempo (relativo) Intervalo de tempo (absoluto)

D Dia ± 2,5e16 anos [2.5e16 AC, 2.5e16 DC]

h Hora ± 1,0e15 anos [1.0e15 AC, 1.0e15 DC]

eu Minuto ± 1,7e13 anos ± [1.7e13 AC, 1.7e13 DC]

é Segundo 2,9e12 anos [2.9e9 AC, 2.9e9 DC]

ms Milissegundo ± 2,9e9 anos Microssegundo [2.9e6 AC, 2.9e6 DC]

nós ± 2,9e6 anos [290301 AC, 294241 DC]

ns Nanossegundo ± 292 anos [1678 DC, 2262 DC]

obs: Picossegundo ± 106 dias [1969 DC, 1970 DC]

fs Femtosegundo ± 2,6 horas [1969 DC, 1970 DC]

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.

Datas e horários em Pandas: o melhor dos dois mundos

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:

In[9]: importar pandas como pd


data = pd.to_datetime("4 de julho de 2015")
data

Saída[9]: Carimbo de data/hora('2015-07-04 00:00:00')

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:

In[11]: data + pd.to_timedelta(np.arange(12), 'D')

Trabalhando com séries temporais | 191


Machine Translated by Google

Saída[11]: DatetimeIndex(['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[ns]',freq=Nenhum)

Na próxima seção, examinaremos mais de perto a manipulação de dados de séries temporais com
as ferramentas fornecidas pelo Pandas.

Série temporal do Pandas: indexação por tempo

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:

Em [12]: índice = pd.DatetimeIndex(['2014-07-04', '2014-08-04',


'04/07/2015', '04/08/2015'])
dados = pd.Series([0, 1, 2, 3], índice = índice)
dados

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.

Estruturas de dados de série temporal Pandas

Esta seção apresentará as estruturas de dados fundamentais do Pandas para trabalhar com
dados de série temporal:

192 | Capítulo 3: Manipulação de dados com Pandas


Machine Translated by Google

• Para carimbos de data/hora, o Pandas fornece o tipo Timestamp . Como mencionado


antes, é essencialmente um substituto para o datetime nativo do Python, mas é baseado
no tipo de dados mais eficiente numpy.datetime64 . A estrutura de índice associada é
DatetimeIndex.

• 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.

• Para deltas de tempo ou durações, o Pandas fornece o tipo Timedelta . Timedelta é um


substituto mais eficiente para o tipo nativo datetime.timedelta do Python e é baseado
em numpy.timedelta64. A estrutura de índice associada é TimedeltaIndex.

Os mais fundamentais desses objetos de data/hora são os objetos Timestamp e DatetimeIn


dex . Embora esses objetos de classe possam ser invocados diretamente, é mais comum
usar a função pd.to_datetime() , que pode analisar uma ampla variedade de formatos.
Passar uma única data para pd.to_datetime() produz um Timestamp; passar uma série de
datas por padrão produz um DatetimeIndex:

In[15]: datas = pd.to_datetime([datetime(2015, 7, 3), '4 de julho de 2015',


'6 de julho de 2015', '07-07-2015', '20150708'])
datas

Saída[15]: DatetimeIndex(['2015-07-03', '2015-07-04', '2015-07-06', '2015-07-07',


'08/07/2015'],
dtype='datetime64[ns]', freq=Nenhum)

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')

Saída[16]: PeriodIndex(['2015-07-03', '2015-07-04', '2015-07-06', '2015-07-07',


'08/07/2015'],
dtype='int64', freq='D')

Um TimedeltaIndex é criado, por exemplo, quando uma data é subtraída de outra:

Em[17]: datas - datas[0]

Fora[17]:
TimedeltaIndex(['0 dias', '1 dia', '3 dias', '4 dias', '5 dias'], dtype='timedelta64[ns]',
freq=Nenhum)

Sequências regulares: pd.date_range()

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

Trabalhando com séries temporais | 193


Machine Translated by Google

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:

Em [18]: pd.date_range('2015-07-03', '2015-07-10')

Saída[18]: DatetimeIndex(['2015-07-03', '2015-07-04', '2015-07-05', '2015-07-06', '2015-07-07',


'2015 -07-08', '2015-07-09', '2015-07-10'],
dtype='datetime64[ns]',freq='D')

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:

Em[19]: pd.date_range('2015-07-03', períodos=8)

Saída[19]: DatetimeIndex(['2015-07-03', '2015-07-04', '2015-07-05', '2015-07-06', '2015-07-07',


'2015 -07-08', '2015-07-09', '2015-07-10'],
dtype='datetime64[ns]',freq='D')

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:

In[20]: pd.date_range('2015-07-03', períodos=8, freq='H')

Saída[20]: DatetimeIndex(['2015-07-03 00:00:00', '2015-07-03 01:00:00', '2015-07-03


02:00:00', '2015- 07-03 03:00:00', '2015-07-03
04:00:00', '2015-07-03 05:00:00', '2015-07-03 06:00:00',
' 03/07/2015 07:00:00'], dtype='datetime64[ns]', freq='H')

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:

Em[21]: pd.period_range('2015-07', períodos=8, freq='M')

Fora[21]:
PeriodIndex(['2015-07', '2015-08', '2015-09', '2015-10', '2015-11', '2015-12', '2016-01', '2016-02' ],
dtype='int64', freq='M')

E uma sequência de durações aumentando em uma hora:

In[22]: pd.timedelta_range(0, períodos=10, freq='H')

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.

194 | Capítulo 3: Manipulação de dados com Pandas


Machine Translated by Google

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.

Tabela 3-7. Listagem de códigos de frequência Pandas

Descrição do código Descrição do código

D Dia do calendário B Dia de negócios

EM Semanalmente

M Fim do mês Fim do mês do BM Business

P Fim do trimestre Fim do trimestre BQ Business

A Fim de ano Fim de ano comercial da BA

H Horas BH Horário comercial

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).

Tabela 3-8. Listagem de códigos de frequência indexados inicialmente

Descrição do código

Início do mês MS

Início do mês comercial da BMS

Início do trimestre QS

Início do trimestre de negócios BQS

AS Início do ano

Início do ano comercial da BAS

Trabalhando com séries temporais | 195


Machine Translated by Google

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:

• Q-JAN, BQ-FEB, QS-MAR, BQS-APR, etc.

• A-JAN, BA-FEB, AS-MAR, BAS-APR, etc.

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:

• W-SUN, W-MON, W-TUE, W-WED, etc.

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:

In[23]: pd.timedelta_range(0, períodos=9, freq="2H30T")

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:

Em [24]: de pandas.tseries.offsets import BDay


pd.date_range('2015-07-01', period=5, freq=BDay())

Saída [24]: DatetimeIndex (['2015-07-01', '2015-07-02', '2015-07-03', '2015-07-06',


'2015-07-07'],
dtype='datetime64[ns]', freq='B')

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.

Reamostragem, mudança e janelamento A capacidade

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

196 | Capítulo 3: Manipulação de dados com Pandas


Machine Translated by Google

dados financeiros de diversas fontes disponíveis, incluindo Yahoo Finance, Google


Finanças e outros. Aqui carregaremos o histórico de preços de fechamento do Google:

In[25]: dos dados de importação do pandas_datareader

goog = data.DataReader('GOOG', início='2004', fim='2016',


data_source='google')
goog.head()

Fora[25]: Aberto alto Volume de fechamento baixo


Data
19/08/2004 49,96 51,98 47,93 50,12 20/08/2004 50,69 NaN
54,49 50,20 54,10 NaN
23/08/2004 55,32 56,68 54,47 54,65 NaN
24/08/2004 55,56 55,74 51,73 52,38 NaN
25/08/2004 52,43 53,95 51,89 52,95 NaN

Para simplificar, usaremos apenas o preço de fechamento:

Em[26]: goog = goog['Fechar']

Podemos visualizar isso usando o método plot() , após a configuração normal do Matplotlib
padrão (Figura 3-5):

Em[27]: %matplotlib embutido


importar matplotlib.pyplot como plt
importação marítima; seaborn.set()

Em[28]: goog.plot();

Figura 3-5. Preço de fechamento das ações do Google ao longo do tempo

Reamostragem e conversão de frequências

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()

Trabalhando com séries temporais | 197


Machine Translated by Google

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):

Em [29]: goog.plot(alpha=0.5, style='-')


goog.resample('BA').mean().plot(style=':')
goog.asfreq('BA').plot (estilo='--');
plt.legend(['input', 'resample', 'asfreq'], loc='superior
esquerdo');

Figura 3-6. Reamostragens do preço das ações do Google

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:

In[30]: fig, ax = plt.subplots(2, sharex=True) data =


goog.iloc[:10]

data.asfreq('D').plot(ax=ax[0], marcador='o')

data.asfreq('D', método='bfill').plot(ax=ax[1], estilo='-o') data.asfreq('D',


método='ffill').plot(ax= ax[1], style='--o') ax[1].legend(["preenchimento",
"preenchimento direto"]);

198 | Capítulo 3: Manipulação de dados com Pandas


Machine Translated by Google

Figura 3-7. Comparação entre interpolação de preenchimento direto e preenchimento posterior

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):

In[31]: fig, ax = plt.subplots(3, sharey=True)

# aplica uma frequência aos dados


goog = goog.asfreq('D', method='pad')

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')

Trabalhando com séries temporais | 199


Machine Translated by Google

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');

Figura 3-8. Comparação entre turno e tshift

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):

Em[32]: ROI = 100 * (goog.tshift(-365) / goog - 1)


ROI.plot()
plt.ylabel('% Retorno sobre o Investimento');

200 | Capítulo 3: Manipulação de dados com Pandas


Machine Translated by Google

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):

Em[33]: rolando = goog.rolling(365, center=True)

dados = pd.DataFrame({'entrada': goog,


'rolling_mean de um ano': Rolling.mean(),
'rolling_std de um ano': Rolling.std()})
machado = data.plot(style=['-', '--', ':'])
ax.lines[0].set_alpha(0.3)

Trabalhando com séries temporais | 201


Machine Translated by Google

Figura 3-10. Estatísticas contínuas sobre os preços das ações do Google

Tal como acontece com as operações groupby , os métodos agregate() e apply() podem ser usados para cálculos contínuos
personalizados.

Onde aprender mais


Esta seção forneceu apenas um breve resumo de alguns dos recursos mais essenciais das ferramentas de série temporal
fornecidas pelo Pandas; para uma discussão mais completa, você pode consultar a seção “Série Temporal/Data” da
documentação online do Pandas.

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: Visualizando contagens de bicicletas em Seattle Como um

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.

A partir do verão de 2016, o CSV pode ser baixado da seguinte forma:

202 | Capítulo 3: Manipulação de dados com Pandas


Machine Translated by Google

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()

Fora[35]: Calçada Oeste da Ponte Fremont \\


Data
03/10/2012 00:00:00 4,0
03/10/2012 01:00:00 4,0
03/10/2012 02:00:00 1,0
03/10/2012 03:00:00 2,0
03/10/2012 04:00:00 6,0

Calçada Leste da Ponte Fremont


Data
03/10/2012 00:00:00 9,0
03/10/2012 01:00:00 6,0
03/10/2012 02:00:00 1,0
03/10/2012 03:00:00 3,0
03/10/2012 04:00:00 1,0

Por conveniência, processaremos ainda mais esse conjunto de dados encurtando os nomes das colunas
e adicionando uma coluna “Total”:

In[36]: data.columns = ['Oeste', 'Leste']


data['Total'] = data.eval('Oeste + Leste')

Agora vamos dar uma olhada nas estatísticas resumidas desses dados:

Em [37]: data.dropna().describe()

Fora[37]: Oeste Leste Total


contar 33544,000000 33544,000000 33544,000000
significar 61.726568 53.541706 115.268275
padrão 83,210813 76,380678 144.773983
mínimo 0,000000 0,000000 0,000000
25% 8.000.000 7.000.000 16.000000
50% 33.000000 28.000000 64.000000
75% 80.000000 66.000000 151.000000
máx. 825.000000 717.000000 1186.000000

Trabalhando com séries temporais | 203


Machine Translated by Google

Visualizando os dados

Podemos obter alguns insights sobre o conjunto de dados visualizando-o. Vamos começar plotando os dados
brutos (Figura 3-11):

Em [38]: % matplotlib embutido


importação marítima; seaborn.set()

In[39]: data.plot()
plt.ylabel(' Contagem horária de bicicletas');

Figura 3-11. Contagem horária de bicicletas na ponte Fremont de Seattle

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):

In[40]: semanalmente = data.resample('W').sum()


semanal.plot(style=[':', '--', '-']) plt.ylabel('
Contagem semanal de bicicletas');

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).

204 | Capítulo 3: Manipulação de dados com Pandas


Machine Translated by Google

Figura 3-12. Travessias semanais de bicicleta na ponte Fremont, em Seattle

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):

Em [41]: diariamente = data.resample('D').sum()


daily.rolling(30, center=True).sum().plot(style=[':', '--', '- ']) plt.ylabel(' contagem
média por hora');

Figura 3-13. Média móvel das contagens semanais de bicicletas

Trabalhando com séries temporais | 205


Machine Translated by Google

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=[':', '--', '-']);

Figura 3-14. Contagens semanais de bicicletas suavizadas gaussianas

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):

Em [43]: by_time = data.groupby(data.index.time).mean()


hourly_ticks = 4 * 60 * 60 * np.arange(6)
by_time.plot(xticks=hourly_ticks, style=[':', '--', '-']);

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.

206 | Capítulo 3: Manipulação de dados com Pandas


Machine Translated by Google

Figura 3-15. Contagens médias de bicicletas por hora

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):

In[44]: by_weekday = data.groupby(data.index.dayofweek).mean()


by_weekday.index = ['Seg', 'Ter', 'Qua', 'Qui', 'Sex', 'Sáb', 'Dom'] by_weekday.plot(style=[':',
'--', '-']);

Figura 3-16. Contagens médias diárias de bicicletas

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.

Trabalhando com séries temporais | 207


Machine Translated by Google

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:

In[45]: fim de semana = np.where(data.index.weekday < 5, 'Weekday', 'Weekend')


by_time = data.groupby([weekend, data.index.time]).mean()

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):

Em [46]: importe matplotlib.pyplot como plt


fig, ax = plt.subplots(1, 2, figsize=(14, 5))
by_time.ix['Weekday'].plot(ax=ax[0], título ='Dias da semana',
xticks=hourly_ticks, style=[':', '--', '-'])
by_time.ix['Weekend'].plot(ax=ax[1], title='Fim de semana',
xticks=hourly_ticks, style=[':', '--', '-']);

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.

Pandas de alto desempenho: eval() e query()


Como já vimos nos capítulos anteriores, o poder da pilha PyData baseia-se na capacidade do NumPy e do Pandas
de enviar operações básicas para C por meio de uma sintaxe intuitiva: exemplos são operações vetorizadas/
transmitidas em NumPy e agrupamento- digite operações no Pandas. Embora essas abstrações sejam eficientes e
eficazes

208 | Capítulo 3: Manipulação de dados com Pandas


Machine Translated by Google

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.

Motivando query() e eval(): expressões compostas


Vimos anteriormente que NumPy e Pandas suportam operações vetorizadas rápidas; por
exemplo, quando você adiciona os elementos de duas matrizes:

Em [1]: importe numpy como


np rng = np.random.RandomState(42)
x = rng.rand(1E6) y
= rng.rand(1E6)
%timeit x + y

100 loops, melhor de 3: 3,39 ms por loop

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))

1 loop, melhor de 3: 266 ms por loop

Mas essa abstração pode se tornar menos eficiente quando você calcula expressões compostas.
Por exemplo, considere a seguinte expressão:

In[3]: máscara = (x > 0,5) & (y < 0,5)

Como o NumPy avalia cada subexpressão, isso é aproximadamente equivalente ao seguinte:

In[4]: tmp1 = (x > 0,5) tmp2


= (y < 0,5) máscara
= tmp1 & tmp2

Em outras palavras, cada etapa intermediária é alocada explicitamente na memória. Se as


matrizes x e y forem muito grandes, isso pode levar a memória significativa e sobrecarga
computacional. A biblioteca Numexpr oferece a capacidade de calcular esse tipo de expressão
composta elemento por elemento, sem a necessidade de alocar matrizes intermediárias completas.
A documentação do Numexpr tem mais detalhes, mas por enquanto é suficiente dizer que a
biblioteca aceita uma string fornecendo a expressão no estilo NumPy que você deseja calcular:

Pandas de alto desempenho: eval() e query() | 209


Machine Translated by Google

Em [5]: importar numexpr


mask_numexpr = numexpr.evaluate('(x > 0,5) & (y < 0,5)')
np.allclose(mask, mask_numexpr)

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.

pandas.eval() para operações eficientes A função eval()

no Pandas usa expressões de string para calcular operações de forma eficiente usando DataFrames. Por
exemplo, considere os seguintes DataFrames:

Em [6]: importe pandas como


pd nrows, ncols = 100000, 100
rng = np.random.RandomState (42)
df1, df2, df3, df4 = (pd.DataFrame(rng.rand(nrows, ncols))
para i no intervalo (4))

Para calcular a soma de todos os quatro DataFrames usando a abordagem típica do Pandas, podemos
simplesmente escrever a soma:

In[7]: %timeit df1 + df2 + df3 + df4

10 loops, melhor de 3: 87,1 ms por loop

Podemos calcular o mesmo resultado via pd.eval construindo a expressão como uma string:

Em [8]: %timeit pd.eval('df1 + df2 + df3 + df4')

10 loops, melhor de 3: 42,2 ms por loop

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:

Em[9]: np.allclose(df1 + df2 + df3 + df4,


eval('df1 + df2 + df3 + df4'))

Fora[9]: Verdadeiro

Operações suportadas por pd.eval()

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))

Operadores aritméticos. pd.eval() suporta todos os operadores aritméticos. Por exemplo:

210 | Capítulo 3: Manipulação de dados com Pandas


Machine Translated by Google

Em[11]: resultado1 = -df1 * df2 / (df3 + df4) - df5


resultado2 = pd.eval('-df1 * df2 / (df3 + df4) - df5')
np.allclose(resultado1, resultado2)

Fora[11]: Verdadeiro

Operadores de comparação. pd.eval() suporta todos os operadores de comparação, incluindo


expressões encadeadas:

Em [12]: resultado1 = (df1 < df2) & (df2 <= df3) & (df3 != df4) resultado2
= pd.eval('df1 < df2 <= df3 != df4') np.allclose(resultado1,
resultado2)

Fora[12]: Verdadeiro

Operadores bit a bit. pd.eval() suporta & e | operadores bit a bit:


Em[13]: resultado1 = (df1 < 0,5) & (df2 < 0,5) | (df3 < df4)
resultado2 = pd.eval('(df1 < 0,5) & (df2 < 0,5) | (df3 < df4)')
np.allclose(resultado1, resultado2)

Fora[13]: Verdadeiro

Além disso, ele suporta o uso do literal e e ou em expressões booleanas:

Em[14]: resultado3 = pd.eval('(df1 < 0,5) e (df2 < 0,5) ou (df3 < df4)')
np.allclose(resultado1, resultado3)

Fora[14]: Verdadeiro

Atributos e índices de objetos. pd.eval() suporta acesso a atributos de objetos por


meio da sintaxe obj.attr e índices por meio da sintaxe obj[index] :
Em[15]: resultado1 = df2.T[0] + df3.iloc[1]
resultado2 = pd.eval('df2.T[0] + df3.iloc[1]')
np.allclose(resultado1, resultado2)

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.

DataFrame.eval() para operações em colunas


Assim como o Pandas possui uma função pd.eval() de nível superior , os DataFrames possuem
um método eval() que funciona de maneira semelhante. A vantagem do método eval() é que as
colunas podem ser referidas pelo nome. Usaremos este array rotulado como exemplo:

Em [16]: df = pd.DataFrame(rng.rand(1000, 3), colunas=['A', 'B', 'C']) df.head()

Pandas de alto desempenho: eval() e query() | 211


Machine Translated by Google

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:

Em[17]: resultado1 = (df['A'] + df['B']) / (df['C'] - 1)


resultado2 = pd.eval("(df.A + df.B) / (df.C - 1)") np.allclose(resultado1,
resultado2)

Fora[17]: Verdadeiro

O método DataFrame.eval() permite uma avaliação muito mais sucinta de expressões com
as colunas:

Em[18]: resultado3 = df.eval('(A + B) / (C - 1)') np.allclose(resultado1,


resultado3)

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()

Além das opções que acabamos de discutir, DataFrame.eval() também permite


atribuição a qualquer coluna. Vamos usar o DataFrame anterior, que possui as colunas
'A', 'B' e 'C':

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:

Em[20]: df.eval('D = (A + B) / C', inplace=True) df.head()

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

212 | Capítulo 3: Manipulação de dados com Pandas


Machine Translated by Google

Da mesma forma, qualquer coluna existente pode ser modificada:

Em[21]: df.eval('D = (A - B) / C', inplace=True) df.head()

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

Variáveis locais em DataFrame.eval()

O método DataFrame.eval() suporta uma sintaxe adicional que permite trabalhar com variáveis locais
do Python. Considere o seguinte:

Em [22]: coluna_média = df.mean(1) resultado1


= df['A'] + coluna_média resultado2 = df.eval('A
+ @column_mean') np.allclose(resultado1, resultado2)

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:

In[23]: resultado1 = df[(df.A < 0,5) & (df.B < 0,5)]


resultado2 = pd.eval('df[(df.A < 0,5) & (df.B < 0,5)]') np.allclose(resultado1,
resultado2)

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() :

In[24]: resultado2 = df.query('A < 0,5 e B < 0,5') np.allclose(resultado1,


resultado2)

Fora[24]: Verdadeiro

Pandas de alto desempenho: eval() e query() | 213


Machine Translated by Google

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:

In[25]: Cmean = df['C'].mean()


resultado1 = df[(df.A < Cmean) & (df.B < Cmean)]
resultado2 = df.query('A < @Cmean e B <@Cmean')
np.allclose(resultado1, resultado2)

Fora[25]: Verdadeiro

Desempenho: quando usar essas funções


Ao considerar usar essas funções, há duas considerações: tempo de computação e uso de
memória. O uso da memória é o aspecto mais previsível. Como já mencionado, toda expressão
composta envolvendo arrays NumPy ou Pandas Data Frames resultará na criação implícita de
arrays temporários: Por exemplo, isto:

In[26]: x = df[(df.A < 0,5) & (df.B < 0,5)]

é aproximadamente equivalente a isto:

In[27]: tmp1 = df.A < 0,5 tmp2


= df.B < 0,5 tmp3 =
tmp1 & tmp2 x =
df[tmp3]

Se o tamanho dos DataFrames temporários for significativo em comparação com a memória


disponível do sistema (normalmente vários gigabytes), então é uma boa ideia usar uma
expressão eval() ou query() . Você pode verificar o tamanho aproximado do seu array em bytes
usando isto:

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”.

214 | Capítulo 3: Manipulação de dados com Pandas


Machine Translated by Google

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:

Documentação on-line do Pandas


Esta é a fonte de referência para a documentação completa do pacote. Embora os exemplos na
documentação tendam a ser pequenos conjuntos de dados gerados, a descrição das opções é
completa e geralmente muito útil para compreender o uso de várias funções.

Python para análise de dados


Escrito por Wes McKinney (o criador original do Pandas), este livro contém muito mais detalhes
sobre o pacote do que tínhamos espaço neste capítulo. Em particular, ele se aprofunda nas
ferramentas para séries temporais, que eram sua base como consultor financeiro. O livro também
traz muitos exemplos divertidos de aplicação do Pandas para obter insights de conjuntos de dados
do mundo real. Tenha em mente, porém, que o livro já tem vários anos e o pacote Pandas tem
alguns recursos novos que este livro não cobre (mas fique atento para uma nova edição em 2017).

Pandas no Stack Overflow


Pandas tem tantos usuários que qualquer pergunta que você tenha provavelmente já foi feita e
respondida no Stack Overflow. Usar Pandas é um caso em que algum Google-Fu é seu melhor
amigo. Basta acessar seu mecanismo de pesquisa favorito e digitar a pergunta, o problema ou o
erro que você está encontrando – é mais do que provável que você encontre sua resposta em
uma página do Stack Overflow.

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!

Recursos adicionais | 215


Machine Translated by Google
Machine Translated by Google

CAPÍTULO 4

Visualização com Matplotlib

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.

Nos últimos anos, entretanto, a interface e o estilo do Matplotlib começaram a envelhecer.


Ferramentas mais recentes como ggplot e ggvis na linguagem R, juntamente com kits de ferramentas
de visualização da web baseados em D3js e HTML5 canvas, muitas vezes fazem o Matplotlib parecer
desajeitado e antiquado. Ainda assim, sou da opinião que não podemos ignorar a força do Matplotlib
como um mecanismo gráfico multiplataforma bem testado. Versões recentes do Matplotlib tornam
relativamente fácil definir novos estilos de plotagem globais (consulte “Personalizando o Matplotlib:
configurações e folhas de estilo” na página 282), e as pessoas têm desenvolvido novos pacotes que
se baseiam em seus poderosos componentes internos para conduzir o Matplotlib de maneira mais limpa e mais

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.

Dicas gerais do Matplotlib


Antes de nos aprofundarmos nos detalhes da criação de visualizações com Matplotlib, há algumas
coisas úteis que você deve saber sobre o uso do pacote.

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:

Em [1]: importar matplotlib como


mpl importar matplotlib.pyplot como plt

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.

mostrar() ou não mostrar()? Como exibir seus gráficos Uma

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.

218 | Capítulo 4: Visualização com Matplotlib


Machine Translated by Google

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:

# ------- arquivo: myplot.py ------ importar


matplotlib.pyplot como plt importar
numpy como np

x = np.linspace(0, 10, 100)

plt.plot(x, np.sin(x)) plt.plot(x,


np.cos(x))

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.

Plotando a partir de um shell

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

Em [2]: importe matplotlib.pyplot como plt

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.

Dicas gerais do Matplotlib | 219


Machine Translated by Google

Plotando a partir de um notebook

IPython O notebook IPython é uma ferramenta interativa de análise de dados baseada em


navegador que pode combinar narrativa, código, gráficos, elementos HTML e muito mais em um
único documento executável (consulte o Capítulo 1 ) .

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:

• O notebook %matplotlib levará a gráficos interativos incorporados no


caderno

• %matplotlib inline levará a imagens estáticas do seu gráfico incorporadas no


caderno

Para este livro, geralmente optaremos por %matplotlib inline:


Em[3]: %matplotlib embutido

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), '--');

Figura 4-1. Exemplo básico de plotagem

220 | Capítulo 4: Visualização com Matplotlib


Machine Translated by Google

Salvando figuras em arquivo

Um recurso interessante do Matplotlib é a capacidade de salvar figuras em uma ampla variedade de


formatos. Você pode salvar uma figura usando o comando savefig() . Por exemplo, para salvar a
figura anterior como um arquivo PNG, você pode executar isto:

Em[5]: fig.savefig('minha_figura.png')

Agora temos um arquivo chamado my_figure.png no diretório de trabalho atual:

Em[6]: !ls -lh minha_figura.png

-rw-r--r-- 1 equipe jakevdp 16K 11 de agosto 10:59 my_figure.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 [7]: de IPython.display import Image


Image('my_figure.png')

Figura 4-2. Renderização PNG do gráfico básico

Em savefig(), o formato do arquivo é inferido a partir da extensão do nome de arquivo fornecido.


Dependendo de quais back-ends você instalou, muitos formatos de arquivo diferentes estão
disponíveis. Você pode encontrar a lista de tipos de arquivos suportados pelo seu sistema usando o
seguinte método do objeto figure canvas :

Em[8]: fig.canvas.get_supported_filetypes()

Out[8]: {'eps': 'Postscript encapsulado', 'jpeg': 'Joint


Photographic Experts Group', 'jpg': 'Joint Photographic
Experts Group', 'pdf': 'Formato de documento portátil',
'pgf' : 'Código PGF para LaTeX', 'png':
'Portable Network Graphics', 'ps':
'Postscript', 'raw': 'bitmap RGBA bruto',
'rgba': 'bitmap RGBA
bruto',

Dicas gerais do Matplotlib | 221


Machine Translated by Google

'svg': 'Gráficos vetoriais escaláveis',


'svgz': 'Gráficos vetoriais escaláveis', 'tif':
'Formato de arquivo de imagem marcado',
'tiff': 'Formato de arquivo de imagem marcado'}

Observe que ao salvar sua figura, não é necessário usar plt.show() ou comandos relacionados
discutidos anteriormente.

Duas interfaces pelo preço de uma


Um recurso potencialmente confuso do Matplotlib são suas interfaces duplas: uma interface
conveniente baseada em estado no estilo MATLAB e uma interface orientada a objetos mais poderosa.
Destacaremos rapidamente as diferenças entre os dois aqui.

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):

In[9]: plt.figure() # cria uma figura de plotagem

# cria o primeiro dos dois painéis e define o eixo atual plt.subplot(2,


1, 1) # (linhas, colunas, número do painel) plt.plot(x, np.sin(x))

# cria o segundo painel e define o eixo atual plt.subplot(2,


1, 2) plt.plot(x, np.cos(x));

Figura 4-3. Subtramas usando a interface estilo MATLAB

É 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

222 | Capítulo 4: Visualização com Matplotlib


Machine Translated by Google

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):

In[10]: # Primeiro crie uma grade de gráficos #


ax será um array de dois objetos Axes fig, ax =
plt.subplots(2)

# Chame o método plot() no objeto apropriado ax[0].plot(x,


np.sin(x)) ax[1].plot(x,
np.cos(x));

Figura 4-4. Subtramas usando a interface orientada a objetos

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.

Duas interfaces pelo preço de uma | 223


Machine Translated by Google

Gráficos de linhas simples

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):

Em[2]: fig = plt.figure()


machado = plt.axes()

Figura 4-5. Eixos de grade vazios

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):

Em[3]: fig = plt.figure()


machado = plt.axes()

x = np.linspace(0, 10, 1000)


ax.plot(x, np.sin(x));

224 | Capítulo 4: Visualização com Matplotlib


Machine Translated by Google

Figura 4-6. Uma sinusóide simples

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):

Em[4]: plt.plot(x, np.sin(x));

Figura 4-7. Uma sinusóide simples através da interface orientada a objetos

Se quisermos criar uma única figura com múltiplas linhas, podemos simplesmente chamar a função plot
diversas vezes (Figura 4-8):

Em[5]: plt.plot(x, np.sin(x)) plt.plot(x,


np.cos(x));

Gráficos de linhas simples | 225


Machine Translated by Google

Figura 4-8. Plotando várias linhas

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.

Ajustando o gráfico: cores e estilos de linhas O primeiro

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

Figura 4-9. Controlando a cor dos elementos do gráfico

226 | Capítulo 4: Visualização com Matplotlib


Machine Translated by Google

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):

Em [7]: plt.plot(x, x + 0, linestyle='solid') plt.plot(x, x +


1, linestyle='tracejado') plt.plot(x, x + 2,
linestyle=' dashdot') plt.plot(x, x + 3,
linestyle='pontilhado');

# Resumindo, você pode usar os seguintes códigos:


plt.plot(x, x + 4, linestyle='-') # solid plt.plot(x, x + 5,
linestyle='--') # tracejado plt. plot(x, x + 6, estilo de
linha='-.') # dashdot plt.plot(x, x + 7, estilo de linha=':');
# pontilhado

Figura 4-10. Exemplo de vários estilos de linha

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):

In[8]: plt.plot(x, x + 0, '-g') # verde sólido plt.plot(x, x + 1,


'--c') # tracejado ciano plt.plot(x, x + 2, '-.k') #
traço ponto preto plt.plot(x, x + 3, ':r'); # pontilhado
em vermelho

Gráficos de linhas simples | 227


Machine Translated by Google

Figura 4-11. Controlando cores e estilos com a sintaxe abreviada

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).

Ajustando o gráfico: limites dos eixos O

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):

Em[9]: plt.plot(x, np.sin(x))

plt.xlim(-1, 11)
plt.ylim(-1,5, 1,5);

Figura 4-12. Exemplo de configuração de limites de eixo

228 | Capítulo 4: Visualização com Matplotlib


Machine Translated by Google

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):

Em[10]: plt.plot(x, np.sin(x))

plt.xlim(10, 0)
plt.ylim(1,2, -1,2);

Figura 4-13. Exemplo de reversão do eixo y

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):

Em[11]: plt.plot(x, np.sin(x))


plt.axis([-1, 11, -1,5, 1,5]);

Figura 4-14. Configurando os limites do eixo com plt.axis

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):

Em[12]: plt.plot(x, np.sin(x))


plt.axis('apertado');

Gráficos de linhas simples | 229


Machine Translated by Google

Figura 4-15. Exemplo de layout “apertado”

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):

Em[13]: plt.plot(x, np.sin(x))


plt.axis('igual');

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):

Em [14]: plt.plot(x, np.sin(x))


plt.title("Uma curva senoidal")

230 | Capítulo 4: Visualização com Matplotlib


Machine Translated by Google

plt.xlabel("x")
plt.ylabel("sin(x)");

Figura 4-17. Exemplos de rótulos e títulos de eixos

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

através do método (você adivinhou) plt.legend() .


Embora existam várias maneiras válidas de usar isso, acho mais fácil especificar o rótulo de cada linha usando a palavra-

chave label da função plot (Figura 4-18):

Em [15]: plt.plot(x, np.sin(x), '-g', rótulo='sin(x)') plt.plot(x,


np.cos(x), ':b', rótulo='cos(x)') plt.axis('igual')

plt.legenda();

Figura 4-18. Exemplo de legenda de plotagem

Gráficos de linhas simples | 231


Machine Translated by Google

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()

Na interface orientada a objetos para plotagem, em vez de chamar essas funções


individualmente, geralmente é mais conveniente usar o método ax.set() para definir todas
essas propriedades de uma vez (Figura 4-19):

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

232 | Capítulo 4: Visualização com Matplotlib


Machine Translated by Google

Gráficos de dispersão simples

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

Gráficos de dispersão com plt.plot

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):

Em[2]: x = np.linspace(0, 10, 30) y =


np.sin(x)

plt.plot(x, y, 'o', color='preto');

Figura 4-20. Exemplo de gráfico de dispersão

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):

Em[3]: rng = np.random.RandomState(0)


para marcador em ['o', '.', ',', 'x', '+', 'v', '^', '<', ' >', 's', 'd']: plt .plot(rng.rand(5),
rng.rand(5), marcador,
label="marker='{0}'".format(marcador))

Gráficos de dispersão simples | 233


Machine Translated by Google

plt.legend(numpontos=1)
plt.xlim(0, 1,8);

Figura 4-21. Demonstração de números de pontos

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)

Figura 4-22. Combinando marcadores de linha e ponto

Argumentos adicionais de palavras-chave para plt.plot especificam uma ampla gama de propriedades
das linhas e marcadores (Figura 4-23):

Em [5]: plt.plot(x, y, '-p', color='gray', markersize=15,


linewidth=4, markerfacecolor='white',
markeredgecolor='gray',
markeredgewidth=2) plt.
ilim(-1,2, 1,2);

234 | Capítulo 4: Visualização com Matplotlib


Machine Translated by Google

Figura 4-23. Personalizando números de linhas e pontos

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 .

Gráficos de dispersão com plt.scatter Um

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):

Em[6]: plt.scatter(x, y, marcador='o');

Figura 4-24. Um gráfico de dispersão simples

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):

Gráficos de dispersão simples | 235

Você também pode gostar