100% acharam este documento útil (2 votos)
238 visualizações162 páginas

Feltrin, Fernando - Análise Exploratória de Dados Com Python, Pandas e Numpy - 2021

O documento apresenta uma introdução à biblioteca Numpy para Python, descrevendo suas principais funcionalidades para criação e manipulação de arrays multidimensionais. Inicialmente é mostrada a instalação e importação da biblioteca, em seguida exemplos de criação de arrays unidimensionais, bidimensionais e tridimensionais, além de operações básicas como cálculos, transposição e armazenamento em disco.

Enviado por

Marcos Botelho
Direitos autorais
© © All Rights Reserved
Levamos muito a sério os direitos de conteúdo. Se você suspeita que este conteúdo é seu, reivindique-o aqui.
Formatos disponíveis
Baixe no formato PDF, TXT ou leia on-line no Scribd
100% acharam este documento útil (2 votos)
238 visualizações162 páginas

Feltrin, Fernando - Análise Exploratória de Dados Com Python, Pandas e Numpy - 2021

O documento apresenta uma introdução à biblioteca Numpy para Python, descrevendo suas principais funcionalidades para criação e manipulação de arrays multidimensionais. Inicialmente é mostrada a instalação e importação da biblioteca, em seguida exemplos de criação de arrays unidimensionais, bidimensionais e tridimensionais, além de operações básicas como cálculos, transposição e armazenamento em disco.

Enviado por

Marcos Botelho
Direitos autorais
© © All Rights Reserved
Levamos muito a sério os direitos de conteúdo. Se você suspeita que este conteúdo é seu, reivindique-o aqui.
Formatos disponíveis
Baixe no formato PDF, TXT ou leia on-line no Scribd
Você está na página 1/ 162

CAPA

ANÁLISE EXPLORATÓRIA DE
DADOS COM PYTHON, PANDAS
E NUMPY

Fernando Feltrin

AVISOS

Este livro é um compilado de:


Todo conteúdo foi reordenado de forma a não haver consideráveis
divisões dos conteúdos entre um livro e outro, cada tópico está rearranjado
em posição contínua, formando assim um único livro. Quando necessário,
alguns capítulos sofreram pequenas alterações a fim de deixar o conteúdo
melhor contextualizado.

Este livro conta com mecanismo antipirataria Amazon Kindle


Protect DRM. Cada cópia possui um identificador próprio rastreável, a
distribuição ilegal deste conteúdo resultará nas medidas legais cabíveis.

É permitido o uso de trechos do conteúdo para uso como fonte


desde que dados os devidos créditos ao autor.
Análise Exploratória de Dados com Python, Pandas e Numpy
v1.03

SOBRE O AUTOR

Fernando Feltrin é Engenheiro da Computação com especializações


na área de ciência de dados e inteligência artificial, Professor licenciado
para docência de nível técnico e superior, Autor de mais de 10 livros sobre
programação de computadores e responsável pelo desenvolvimento e
implementação de ferramentas voltadas a modelos de redes neurais
artificiais aplicadas à radiologia (diagnóstico por imagem).
LIVROS
Disponível em: Amazon.com.br
CURSO

Curso Python do ZERO à Programação Orientada a Objetos


Mais de 15 horas de videoaulas que lhe ensinarão programação em linguagem Python de
forma simples, prática e objetiva.

REDES SOCIAIS
https://github.com/fernandofeltrin

https://github.com/fernandofeltrin

ÍNDICE
Sumário
CAPA
AVISOS
SOBRE O AUTOR
LIVROS
CURSO
REDES SOCIAIS
ÍNDICE
INTRODUÇÃO
BIBLIOTECA NUMPY
Sobre a biblioteca Numpy
Instalação e importação das dependências
ARRAYS
Criando uma array numpy
Criando uma array gerada com números ordenados
Array gerada com números do tipo float
Array gerada com números do tipo int
Array gerada com números zero
Array gerada com números um
Array gerada com espaços vazios
Criando uma array de números aleatórios, mas com tamanho predefinido
em variável
Criando arrays de dimensões específicas
Array unidimensional
Array bidimensional
Array tridimensional
Verificando o tamanho e formato de uma array
Verificando o tamanho em bytes de um item e de toda a array
Verificando o elemento de maior valor de uma array
Consultando um elemento por meio de seu índice
Consultando elementos dentro de um intervalo
Modificando manualmente um dado/valor de um elemento por meio de
seu índice.
Criando uma array com números igualmente distribuídos
Redefinindo o formato de uma array
Usando de operadores lógicos em arrays
OPERAÇÕES MATEMÁTICAS EM ARRAYS
Criando uma matriz diagonal
Criando padrões duplicados
Somando um valor a cada elemento da array
Realizando soma de arrays
Subtração entre arrays
Multiplicação e divisão entre arrays
OPERAÇÕES LÓGICAS EM ARRAYS
Transposição de arrays
Salvando uma array no disco local
Carregando uma array do disco local
Exemplo de aplicação da biblioteca Numpy
BIBLIOTECA PANDAS
Sobre a biblioteca Pandas
Instalação das Dependências
Tipos de dados básicos Pandas
ANÁLISE EXPLORATÓRIA DE DADOS
Importando a biblioteca Pandas
SERIES
Criando uma Serie
Visualizando o cabeçalho
Definindo uma origem personalizada
Definindo um índice personalizado
Integrando uma Array Numpy em uma Serie
Qualquer estrutura de dados como elemento de uma Serie
Integrando funções com Series
Verificando o tamanho de uma Serie
Criando uma Serie a partir de um dicionário
Unindo elementos de duas Series
DATAFRAMES
Criando um DataFrame
Extraindo dados de uma coluna específica
Criando colunas manualmente
Removendo colunas manualmente
Ordenando elementos de uma coluna
Extraindo dados de uma linha específica
Extraindo dados de um elemento específico
Extraindo dados de múltiplos elementos
Buscando elementos via condicionais
OPERAÇÕES MATEMÁTICAS EM DATAFRAMES
Usando de funções embutidas do sistema
Aplicando uma operação matemática a todos elementos
Usando de funções matemáticas personalizadas
ESTRUTURAS CONDICIONAIS APLICADAS A DATAFRAMES
Aplicação de estruturas condicionais simples
Aplicação de estruturas condicionais compostas
Atualizando um DataFrame de acordo com uma condicional
INDEXAÇÃO DE DATAFRAMES
Manipulando o índice de um DataFrame
Índices multinível
ENTRADA DE DADOS
Importando dados de um arquivo para um DataFrame
TRATAMENTO DE DADOS
Tratando dados ausentes / faltantes
Agrupando dados de acordo com seu tipo
MÉTODOS APLICADOS
Alterando o formato de um DataFrame
Concatenando dados de dois DataFrames
Mesclando dados de dois DataFrames
Agregando dados de um DataFrame em outro DataFrame
Filtrando elementos únicos de uma Serie de um DataFrame
Processamento em paralelo de um Dataframe
CONSIDERAÇÕES FINAIS

INTRODUÇÃO

BIBLIOTECA NUMPY
Quando estamos usando da linguagem Python para criar nossos códigos, é
normal que usemos de uma série de recursos internos da própria linguagem,
uma vez que como diz o jargão, Python vem com baterias inclusas, o que
em outras palavras pode significar que Python mesmo em seu estado mais
básico já nos oferece uma enorme gama de funcionalidades prontas para
implementação.
No âmbito de nichos específicos como, por exemplo, computação
científica, é bastante comum o uso tanto das ferramentas nativas da
linguagem Python como de bibliotecas desenvolvidas pela comunidade para
propósitos bastante específicos.
Com uma rápida pesquisa no site https://pypi.org é possível ver que existem,
literalmente, milhares de bibliotecas, módulos e pacotes desenvolvidos pela
própria comunidade, a fim de adicionar novas funcionalidades ou aprimorar
funcionalidades já existentes no núcleo da linguagem Python.
Uma das bibliotecas mais usadas, devido a sua facilidade de implementação
e uso, é a biblioteca Numpy. Esta por sua vez oferece uma grande variedade
de funções aritméticas de fácil aplicação, de modo que para certos tipos de
operações as funções da biblioteca Numpy são muito mais intuitivas e
eficientes do que as funções nativas de mesmo propósito presentes nas
built-ins do Python.

Sobre a biblioteca Numpy

Como mencionado anteriormente, a biblioteca Numpy, uma das melhores,


se não a melhor, quando falamos de bibliotecas dedicadas a operações
aritméticas, possui em sua composição uma série de funções matemáticas
pré-definidas, de modo que bastando importar a biblioteca em nosso código
já temos à disposição as tais funções prontas para execução.

E quando digo uma enorme variedade de funções predefinidas, literalmente


temos algumas centenas delas prontas para uso, bastando chamar a função
específica e a parametrizar como é esperado. Por meio da documentação
online da biblioteca é possível ver a lista de todas as funções.
Neste pequeno livro, iremos fazer uma abordagem simples das principais
funcionalidades que a biblioteca Numpy nos oferece, haja visto que a
aplicação de tais funções se dá de acordo com a necessidade, e em suas
rotinas você notará que a grande maioria das funções disponíveis nem
mesmo é usada, porém, todas elas estão sempre à disposição prontas para
uso.
Também será visto nos capítulos finais um exemplo de funções da
biblioteca Numpy sendo aplicadas em machine learning.

Instalação e importação das dependências


Usando do Google Colab, podemos realizar a instalação e
atualização da biblioteca Numpy por meio do gerenciador de pacotes pip,
de forma muito parecida como quando instalamos qualquer outra biblioteca
localmente.

Uma vez que o processo de instalação tenha sido finalizado como


esperado, sem erros, podemos imediatamente fazer o uso das ferramentas
disponíveis da biblioteca Numpy.

O processo de instalação local também pode ser feito via Prompt de


Comando, por meio de pip install numpy.

Para darmos início a nossos exemplos, já com nossa IDE aberta,


primeiramente é necessário realizar a importação da biblioteca Numpy, uma
vez que a mesma é uma biblioteca externa que por padrão não vem pré-
carregada em nossa IDE.
O processo de importação é bastante simples, bastando criar a linha
de código referente a importação da biblioteca logo no início de nosso
código para que a biblioteca seja carregada prioritariamente antes das
demais estruturas de código, de acordo com a leitura léxica de nosso
interpretador.
Uma prática comum é referenciar nossas bibliotecas por meio de
alguma sigla, para que simplesmente fique mais fácil instanciar a mesma
em nosso código. Neste caso, importamos a biblioteca numpy e a
referenciamos como np apenas por convenção.

ARRAYS

Criando uma array numpy

Estando todas as dependências devidamente instaladas e carregadas,


podemos finalmente dar início ao entendimento das estruturas de dados da
biblioteca Numpy. Basicamente, já que estaremos trabalhando com dados
por meio de uma biblioteca científica, é necessário contextualizarmos os
tipos de dados envolvidos assim como as possíveis aplicações dos mesmos.
Sempre que estamos trabalhando com dados por meio da biblioteca Numpy,
a primeira coisa que devemos ter em mente é que todo e qualquer tipo de
dado em sua forma mais básica estará no formato de uma Array Numpy.
Quando estamos falando de arrays de forma geral basicamente
estamos falando de dados em formato de vetores / matrizes, representados
normalmente com dimensões bem definidas, alocando dados nos moldes de
uma tabela com suas respectivas linhas, colunas e camadas.
A partir do momento que estamos falando de uma array “do tipo
numpy”, basicamente estamos falando que tais dados estão carregados pela
biblioteca Numpy de modo que possuem um sistema de indexação próprio
da biblioteca e algumas métricas de mapeamento dos dados para que se
possam ser aplicados sobre os mesmos uma série de funções, operações
lógicas e aritméticas dependendo a necessidade.

Inicialmente criamos uma variável / objeto de nome data, que


recebe como atributo a função np.array( ) parametrizada com uma simples
lista de caracteres. Note que instanciamos a biblioteca numpy por meio de
np, em seguida chamando a função np.array( ) que serve para transformar
qualquer tipo de dado em array do tipo numpy.
Uma vez criada a array data com seus respectivos dados/valores,
podemos realizar algumas verificações simples.
Na primeira linha vemos o retorno da função print( ) simplesmente exibindo
o conteúdo de data. Na segunda linha o retorno referente a função print( )
parametrizada para verificação do tipo de dado por meio da função type( )
por sua vez parametrizado com data, note que, como esperado, trata-se de
uma ‘numpy.ndarray’.
Por fim, na terceira linha temos o retorno referente ao tamanho da
array por meio do método .shape, nesse caso, 5 elementos em uma linha, o
espaço vazio após a vírgula indica não haver nenhuma coluna adicional.
Criando uma array gerada com números ordenados

Dependendo do propósito, pode ser necessário a criação de uma array


composta de números gerados ordenadamente dentro de um intervalo
numérico.
Nesse contexto, a função np.arange( ) foi criada para este fim,
bastando especificar o número de elementos que irão compor a array.

Nesse caso, quando estamos falando em números ordenados


estamos falando em números inteiros gerados de forma sequencial, do zero
em diante.
Novamente por meio da função print( ) parametrizada com data, é possível
ver o retorno, nesse caso, uma array sequencial de 15 elementos gerados de
forma ordenada.

Array gerada com números do tipo float


Outra possibilidade que temos é a de criar uma array numpy de
dados aleatórios tipo float, escalonados entre 0 e 1, por meio da função
np.random.rand( ), novamente parametrizada com o número de elementos a
serem gerados.
Da mesma forma, por meio da função print( ) é possível visualizar tais
números gerados randomicamente. Note que neste caso os números são
totalmente aleatórios, não sequenciais, dentro do intervalo de 0 a 1.

Array gerada com números do tipo int

Outro meio de criar uma array numpy é gerando números inteiros dentro de
um intervalo específico definido manualmente para a função
np.random.randint( ).
Repare que o primeiro parâmetro da função, nesse caso, é o número
10, o que em outras palavras nos diz que estamos gerando números de 0 até
10, já o segundo parâmetro, size, aqui definido em 10, estipula que 10
elementos serão gerados aleatoriamente.
Mais uma vez por meio da função print( ) podemos visualizar a array
gerada, como esperado, 10 números inteiros gerados aleatoriamente.
De forma parecida com o que fizemos anteriormente por meio da
função np.random.rand( ), podemos via np.random.random( ) gerar arrays
de dados tipo float, no intervalo entre 0 e 1, aqui com mais de uma
dimensão.
Note que para isso é necessário colocar o número de linhas e
colunas dentro de um segundo par de parênteses.
Como de costume, por meio da função print( ) visualizamos os dados
gerados e atribuídos a data.

Array gerada com números zero

Dependendo da aplicação, pode ser necessário gerar arrays numpy


previamente preenchidas com um tipo de dado específico. Em machine
learning, por exemplo, existem várias aplicações onde se cria uma matriz
inicialmente com valores zerados a serem substituídos após algumas
funções serem executadas e gerarem retornos.
Aqui, por meio da função np.zeros( ) podemos criar uma array com
as dimensões que quisermos, totalmente preenchida com números 0. Nesse
caso, uma array com 3 linhas e 4 colunas deverá ser gerada.
Por meio da função print( ) novamente vemos o resultado, neste
caso, uma matriz composta apenas de números 0.

Array gerada com números um

Da mesma forma como realizado no exemplo anterior, de acordo


com as particularidades de uma aplicação pode ser necessário realizar a
criação de uma array numpy inicialmente preenchida com números 1.
Apenas substituindo a função np.zeros( ) por np.ones( ) criamos uma array
nos mesmos moldes da anterior.
Por meio da função print( ) visualizamos o resultado, uma matriz de
números 1.

Array gerada com espaços vazios


Apenas como curiosidade, teoricamente é possível criar uma array numpy
de dimensões pré definidas e com valores vazios por meio da função
np.empty( ).
Porém o retorno gerado, como você pode visualizar na imagem acima, é
uma array com números em notação científica, representando algo muito
próximo de zero, porém não zero absoluto. Este tipo de array normalmente
é criado como uma estrutura esqueleto a ter seus dados /valores atualizados
assim que tal array é instanciada por alguma variável ou alimentada como
retorno de alguma função.

Concluindo essa linha de raciocínio, novamente dependendo da


aplicação, pode ser necessário a geração de uma array composta de
números negativos. Nesse caso não há uma função específica para tal fim,
porém há o suporte da linguagem Python em permitir o uso do operador de
multiplicação por um número negativo.
Por meio da função print( ), como esperado, dessa vez temos uma array
composta de 10 elementos negativos.
Criando uma array de números aleatórios, mas com tamanho
predefinido em variável

Uma última possibilidade a ser explorada é a de associar a criação de uma


array com um tamanho predefinido através de uma variável. Por meio da
função np.random.permutation( ) podemos definir um tamanho que pode
ser modificado conforme a necessidade,

Para esse fim, inicialmente criamos uma variável de nome tamanho que
recebe como atributo o valor 10, este será o número a ser usado como
referência de tamanho para a array, em outras palavras, o número de
elementos da mesma.
Na sequência criamos nossa array data6 que recebe a função
np.random.permitation( ) por sua vez parametrizada com o valor de
tamanho. Por meio da função print( ) podemos ver o resultado desse
gerador.
O resultado, como esperado, é uma array unidimensional composta por 10
elementos aleatoriamente gerados.

Criando arrays de dimensões específicas


Uma vez que estamos trabalhando com dados em forma matricial
(convertidos e indexados como arrays numpy), uma característica
importante a observar sobre os mesmos é sua forma representada, seu
número de dimensões.
Posteriormente estaremos entendendo como realizar diversas
operações entre arrays, mas por hora, raciocine que para que seja possível o
cruzamento de dados entre arrays, as mesmas devem possuir formato
compatível. Nessa lógica, posteriormente veremos que para determinadas
situações estaremos inclusive realizando a alteração do formato de nossas
arrays para aplicação de certas funções.
Por hora, vamos entender da maneira correta como são definidas as
dimensões de uma array numpy.

Array unidimensional

Como já feito anteriormente, por meio da função np.random.randint( )


podemos gerar uma array, nesse caso, de 10 elementos com valores
distribuídos entre 0 a 5.
Por meio da função print( ) parametrizada com data vemos os valores da
array, e parametrizando a mesma com data.shape podemos inspecionar de
forma simples o formato de nossa array. Neste caso, [10, ] representa uma
array com 10 elementos em apenas uma linha. Uma array unidimensional.
Array bidimensional

Para o próximo exemplo criamos uma variável de nome data2 que recebe,
da mesma forma que o exemplo anterior, a função np.random.randint( )
atribuída para si, agora substituindo o valor de size por dois valores (3, 4),
estamos definindo manualmente que o tamanho dessa array deverá ser de 3
linhas e 4 colunas, respectivamente. Uma array bidimensional.
Como esperado, por meio da função print( ) vemos a array em si com seu
conteúdo, novamente via data2.shape vemos o formato esperado.

Array tridimensional
Seguindo com a mesma lógica, criamos a variável data3 que chama a
função np.random.randint( ), alterando novamente o número definido para o
parâmetro size, agora com 3 parâmetros, estamos definindo uma array de
formato tridimensional.
Nesse caso, importante entender que o primeiro dos três parâmetros
se refere ao número de camadas que a array terá em sua terceira dimensão,
enquanto o segundo parâmetro define o número de linhas e o terceiro
parâmetro o número de colunas.
Como esperado, 5 camadas, cada uma com 3 linhas e 4 colunas, preenchida
com números gerados aleatoriamente entre 0 e 5.
Entendidos os meios de como gerar arrays com valores aleatórios, podemos
agora entender também que é perfeitamente possível criar arrays
manualmente, usando de dados em formato de lista para a construção da
mesma. Repare que seguindo uma lógica parecida com o que já foi visto
anteriormente, aqui temos duas listas de números, consequentemente,
teremos uma array bidimensional.
Exibindo em tela o resultado via função print( ) temos nossa array com os
respectivos dados declarados manualmente. Uma última observação a se
fazer é que é imprescindível que se faça o uso dos dados declarados da
maneira correta, inclusive na sequência certa.

Verificando o tamanho e formato de uma array

Quando estamos trabalhando com dados em formato de vetor ou matriz, é


muito importante eventualmente verificar o tamanho e formato dos
mesmos, até porquê para ser possível realizar operações entre tais tipos de
dados os mesmos devem ter um formato padronizado.
Indo diretamente ao ponto, nossa variável data3 é uma array do tipo numpy
como pode ser observado. A partir daí é possível realizar algumas
verificações, sendo a primeira delas o método .ndim, que por sua vez
retornará o número de dimensões presentes em nossa array. Da mesma
forma .shape nos retorna o formato de nossa array (formato de cada
dimensão da mesma) e por fim .size retornará o tamanho total dessa matriz.
Como retorno das funções print( ) criadas, logo na primeira linha temos o
valor 3 referente a .ndim, que em outras palavras significa tratar-se de uma
array tridimensional. Em seguida, na segunda linha temos os valores 5, 3, 4,
ou seja, 5 camadas, cada uma com 3 linhas e 4 colunas. Por fim, na terceira
linha temos o valor 60, que se refere a soma de todos os elementos que
compõem essa array / matriz.

Verificando o tamanho em bytes de um item e de toda a array

Uma das questões mais honestas que todo estudante de Python (não
somente Python, mas outras linguagens) possui é do por quê utilizar de
bibliotecas externas quando já temos a disposição ferramentas nativas da
linguagem.
Pois bem, toda linguagem de programação de código fonte aberto tende a
receber forte contribuição da própria comunidade de usuários, que por sua
vez, tentam implementar novos recursos ou aperfeiçoar os já existentes.
Com Python não é diferente, existe uma infinidade de bibliotecas para todo
e qualquer tipo de aplicação, umas inclusive, como no caso da Numpy,
mesclam implementar novas ferramentas assim como reestruturar
ferramentas padrão em busca de maior performance.
Uma boa prática é manter em nosso código apenas o necessário, assim
como levar em consideração o impacto de cada elemento no processamento
de nossos códigos. Não é porque você vai criar um editor de texto que você
precise compilar junto todo o sistema operacional. Inclusive se você já tem
alguma familiaridade com a programação em Python sabe muito bem que
sempre que possível importamos apenas os módulos e pacotes necessários
de uma biblioteca, descartando todo o resto.

Dado o contexto acima, uma das verificações comumente realizada é a de


consultar o tamanho de cada elemento que faz parte da array assim como o
tamanho de toda a array. Isso é feito diretamente por meio dos métodos
.itemsize e .nbytes, respectivamente,
Aqui, apenas para variar um pouco, já implementamos essas
verificações diretamente como parâmetro em nossa função print( ), porém é
perfeitamente possível associar esses dados a uma variável.
Como esperado, junto de nossa mensagem declarada manualmente por
meio de f’Strings temos os respectivos dados de cada elemento e de toda a
matriz

Verificando o elemento de maior valor de uma array


Iniciando o entendimento das possíveis verificações que podemos realizar
em nossas arrays, uma das mais comuns é buscar o elemento de maior valor
em relação aos demais da array.

Criada a array data com 15 elementos ordenados, por meio da função print(
) parametrizando a mesma com os dados de data veremos tais elementos.
Aproveitando o contexto, por meio da função max( ) teremos acesso ao
elemento de maior valor associado.
Como esperado, na primeira linha temos todos os elementos de data, na
segunda linha temos em destaque o número 14, lembrando que a array tem
15 elementos, mas os mesmos iniciam em 0, logo, temos números de 0 a
14, sendo 14 o maior deles.

Consultando um elemento por meio de seu índice

Como visto anteriormente, a organização e indexação dos elementos de


uma array se assemelha muito com a estrutura de dados de uma lista, e
como qualquer lista, é possível consultar um determinado elemento por
meio de seu índice, mesmo que esse índice não seja explícito.
Inicialmente vamos ver alguns exemplos com base em array
unidimensional.
Criada a array data da mesma forma que o exemplo anterior, por meio da
função print( ) parametrizada com data[ ] especificando um número de
índice, é esperado que seja retornado o respectivo dado/valor situado
naquela posição.
Se não houver nenhum erro de sintaxe, é retornado o dado/valor da posição
8 do índice de nossa array data. Nesse caso, o retorno é o próprio número 8
uma vez que temos dados ordenados nesta array.

Da mesma forma, passando como número de índice um valor negativo, será


exibido o respectivo elemento a contar do final para o começo dessa array.
Nesse caso, o 5º elemento a contar do fim para o começo é o número 10.

Partindo para uma array multidimensional, o processo para consultar um


elemento é feito da mesma forma dos exemplos anteriores, bastando agora
especificar a posição do elemento levando em conta a
Note que é criada a array data2, de elementos aleatórios entre 0 e 5, tendo 3
linhas e 4 colunas em sua forma.
Parametrizando print com data2[0, 2] estamos pedindo que seja
exibido o elemento situado na linha 0 (primeira linha) e coluna 2 (terceira
coluna pois a primeira coluna é 0).
Podemos visualizar via console a array em si, e o respectivo elemento
situado na primeira linha, terceira coluna. Neste caso, o número 4.

Consultando elementos dentro de um intervalo

Reutilizando nossa array data criada anteriormente, uma simples array


unidimensional de 15 elementos ordenados, podemos avançar com o
entendimento de outras formas de consultar dados/valores, dessa vez,
definindo intervalos específicos.
Como mencionado anteriormente, a forma como podemos consultar
elementos via índice é muito parecida (senão igual) a forma como
realizávamos as devidas consultas em elementos de uma lista. Sendo assim,
podemos usar das mesmas notações aqui.
Na primeira linha simplesmente por meio da função print( ) estamos
exibindo em tela os dados contidos em data.
Na sequência, passando data[:5] (mesmo que data[0:5]) como parâmetro
para nossa função print( ) estaremos exibindo os 5 primeiros elementos
dessa array. Em seguida via data[3:] estaremos exibindo do terceiro
elemento em diante todos os demais. Na sequência, via data[4:8] estaremos
exibindo do quarto ao oitavo elemento.
Da mesma forma, porém com uma pequena diferença de notação, podemos
por meio de data[::2] exibir de dois em dois elementos, todos os elementos.
Por fim, via data[3::] estamos pulando os 3 primeiros elementos e exibindo
todos os demais.
Como esperado, na primeira linha temos todos os dados da array, na
segunda linha apenas os 5 primeiros, na terceira linha do terceiro elemento
em diante, na quarta linha os elementos entre 4 e 8, na quinta linha os
elementos pares e na sexta linha todos os dados exceto os três primeiros.
Modificando manualmente um dado/valor de um elemento por
meio de seu índice.

Assim como em listas podíamos realizar modificações diretamente sobre


seus elementos, bastando saber seu índice, aqui estaremos realizando o
mesmo processo da mesma forma.

Reutilizando nossa array data 2 criada anteriormente, podemos realizar a


alteração de qualquer dado/valor desde que se faça a referência correta a
sua posição na array de acordo com seu índice. Nesse caso, apenas como
exemplo, iremos substituir o valor do elemento situado na linha 0 e coluna
0 por 10.
Repare que como esperado, o valor do elemento [0, 0] foi substituído de 1
para 10 como mostra o retorno gerado via print( ).
Apenas como curiosidade, atualizando o valor de um elemento com um
número do tipo float, o mesmo será convertido para int para que não hajam
erros por parte do interpretador.

Elemento [1, 1] em formato int, sem as casas decimais declaradas


manualmente.

Supondo que você realmente precise dos dados em formato float dentro de
uma array, você pode definir manualmente o tipo de dado por meio do
parâmetro dtype = ‘float’. Dessa forma, todos os elementos desta array
serão convertidos para float.
Aqui como exemplo estamos criando novamente uma array unidimensional,
com números inteiros em sua composição no momento da declaração.
Analisando o retorno de nossa função print( ) podemos ver que de fato são
números, agora com casas decimais, de tipo float64.

Criando uma array com números igualmente distribuídos

Dependendo mais uma vez do contexto, pode ser necessária a criação de


uma array com dados/valores distribuídos de maneira uniforme na mesma.
Tenha em mente que os dados gerados de forma aleatória não possuem
critérios definidos quanto à sua organização. Porém, por meio da função
linspace( ) podemos criar dados uniformemente arranjados dentro de uma
array.

Criamos uma nova array de nome data, onde por meio da função
np.linspace( ) estamos gerando uma array composta de 7 elementos
igualmente distribuídos, com valores entre 0 e 1.
O retorno como esperado é uma array de números float, para que haja
divisão igual dos valores dos elementos.

Redefinindo o formato de uma array

Um dos recursos mais importantes que a biblioteca Numpy nos oferece é a


de poder livremente alterar o formato de nossas arrays. Não que isso não
seja possível sem o auxílio da biblioteca Numpy, mas a questão mais
importante aqui é que no processo de reformatação de uma array numpy, a
mesma se reajustará em todos os seus níveis, assim como atualizará seus
índices para que não se percam dados entre dimensões.

Inicialmente apenas para o exemplo criamos nossa array data5 com 8


elementos gerados e ordenados por meio da função np.arange( ).

Em seguida, por meio da função reshape( ) podemos alterar livremente o


formato de nossa array. Repare que inicialmente geramos uma array
unidimensional de 8 elementos, agora estamos transformando a mesma para
uma array bidimensional de 8 elementos, onde os mesmos serão
distribuídos em duas linhas e 4 colunas.
É importante salientar que você pode alterar livremente o formato
de uma array desde que mantenha sua proporção de distribuição de dados.
Aqui, nesse exemplo bastante simples, 8 elementos dispostos em uma linha
passarão a ser 8 elementos dispostos em duas linhas e 4 colunas. O número
de elementos em si não pode ser alterado.
Na primeira linha, referente ao primeiro print( ) temos a array data5 em sua
forma inicial, na segunda linha o retorno obtido pós reshape.
Usando de operadores lógicos em arrays

Dependendo do contexto pode ser necessário fazer uso de operadores


lógicos mesmo quando trabalhando com arrays numpy. Respeitando a
sintaxe Python, podemos fazer uso de operadores lógicos sobre arrays da
mesma forma como fazemos com qualquer outro tipo de dado.

Para esse exemplo criamos nossa array data7 de 10 elementos ordenados


gerados aleatoriamente. Por meio da função print( ) podemos tanto exibir
seu conteúdo quanto realizar proposições baseadas em operadores lógicos.
Note que no exemplo a seguir, estamos verificando quais elementos de
data7 tem seu valor maior que 3.
Na primeira linha todo o conteúdo de data7, na segunda marcados como
False os elementos menores que 3, assim como marcados como True os de
valor maior que 3.
Da mesma forma como visto anteriormente, fazendo a seleção de um
elemento por meio de seu índice, também podemos aplicar um operador
lógico sobre o mesmo.
Nesse caso, o elemento da posição 4 do índice, o número 3, não é maior que
5, logo, o retorno sobre essa operação lógica é False.

OPERAÇÕES MATEMÁTICAS EM ARRAYS

Dentro das possibilidades de manipulação de dados em arrays do tipo


numpy está a de realizar, como esperado, operações aritméticas entre tais
dados. Porém recapitulando o básico da linguagem Python, quando estamos
trabalhando com determinados tipos de dados temos de observar seu tipo e
compatibilidade com outros tipos de dados, sendo em determinados casos
necessário a realização da conversão entre tipos de dados.
Uma array numpy permite realizar determinadas operações que nos mesmos
tipos de dados em formato não array numpy iriam gerar exceções ou erros
de interpretação.
Para entendermos melhor, vamos usar do seguinte exemplo, temos uma
array data8 com uma série de elementos dispostos em formato de lista, o
que pode passar despercebido aos olhos do usuário, subentendendo que para
este tipo de dado é permitido realizar qualquer tipo de operação. Por meio
da função print( ) parametrizada com data8 + 10, diferentemente do
esperado, irá gerar um erro.
Repare que o interpretador lê os dados de data8 como uma simples lista,
tentando por meio do operador + concatenar os dados com o valor 10 ao
invés de realizar a soma.

Realizando a conversão de data8 para uma array do tipo numpy, por meio
da função np.array( ) parametrizada com a própria variável data8, agora é
possível realizar a operação de soma dos dados de data8 com o valor
definido 10.
Como retorno da função print( ), na primeira linha temos os valores iniciais
de data8, na segunda linha os valores após somar cada elemento ao número
10.

Criando uma matriz diagonal

Aqui mais um dos exemplos de propósito específico, em algumas


aplicações de machine learning é necessário a criação de uma matriz
diagonal, e a mesma pode ser feita por meio da função np.diag( ).
Para esse exemplo criamos a array data9 que recebe como atributo uma
matriz diagonal de 8 elementos definidos manualmente, gerada pela função
np.diag( ).
O retorno é uma matriz gerada com os elementos predefinidos compondo
uma linha vertical, sendo todos os outros espaços preenchidos com 0.

Algo parecido pode ser feito através da função np.eye( ), porém, nesse caso
será gerada uma matriz diagonal com valores 0 e 1, de tamanho definido
pelo parâmetro repassado para função.

Neste caso, para a variável data9 está sendo criada uma array diagonal de 4
linhas e colunas conforme a parametrização realizada em np.eye( ).
O retorno é uma matriz diagonal, com números 1 dispostos na coluna
diagonal

Criando padrões duplicados

Por meio da simples notação de “encapsular” uma array em um novo par de


colchetes e aplicar a função np.tile( ) é possível realizar a
duplicação/replicação da mesma quantas vezes for necessário.

Aqui criada a array data10, note que atribuído para a mesma está a função
np.tile( ) parametrizada com uma array definida manualmente assim como
um último parâmetro 4, referente ao número de vezes a serem replicados os
dados/valores da array.
No retorno podemos observar os elementos declarados na array, em sua
primeira linha 9 e 4, na segunda, 3 e 7, repetidos 4 vezes.
Mesmo exemplo anterior, agora parametrizado para replicação em linhas e
colunas.
Note que, neste caso, a duplicação realizada de acordo com a
parametrização ocorre sem sobrepor a ordem original dos elementos.

Somando um valor a cada elemento da array

Como visto anteriormente, a partir do momento que uma matriz passa a ser
uma array do tipo numpy, pode-se perfeitamente aplicar sobre a mesma
qualquer tipo de operador lógico ou aritmético. Dessa forma, é possível
realizar tais operações, desde que leve em consideração que a operação
realizada se aplicará a cada um dos elementos da array.
Para esse exemplo criamos uma nova array de nome data11, por sua vez
gerada com números ordenados. Em seguida é realizada uma simples
operação de somar 1 a própria array, e como dito anteriormente, essa soma
se aplicará a todos os elementos.
Observando os retornos das funções print( ) na primeira linha temos a
primeira array com seus respectivos elementos, na segunda, a mesma array
onde a cada elemento foi somado 1 ao seu valor original.

Apenas como exemplo, usando da notação vista anteriormente é possível


realizar a soma de um valor a cada elemento de uma array, nesse caso,
somando 3 ao valor de cada elemento, de 3 em 3 elementos de acordo com
o parâmetro passado para função np.arange( ).
Como esperado, o retorno é uma array gerada com números de 0 a 30, com
intervalo de 3 entre cada elemento.

Realizando soma de arrays

Entendido boa parte do processo lógico das possíveis operações em arrays


não podemos nos esquecer que é permitido também a soma de arrays, desde
que as mesmas tenham o mesmo formato ou número de elementos.
Para esse exemplo criamos duas arrays d1 e d2, respectivamente, cada uma
com 5 elementos distintos. Por meio de uma nova variável de nome e3
realizamos a soma simples entre d1 e d2, e como esperado, cada elemento
de cada array será somado ao seu elemento equivalente da outra array.
O resultado obtido é a simples soma do primeiro elemento da primeira array
com o primeiro elemento da segunda array, assim como todos os outros
elementos com seu equivalente.

Subtração entre arrays

Exatamente da mesma forma é possível realizar a subtração entre


arrays.
Como esperado, é obtido os valores das subtrações entre cada elemento da
array d1 com o seu respectivo elemento da array d2.
Multiplicação e divisão entre arrays

Exatamente da mesma forma é possível realizar a divisão e a multiplicação


entre os elementos das arrays.
Obtendo assim, na primeira linha o retorno da divisão entre os elementos
das arrays, na segunda linha o retorno da multiplicação entre os elementos
das mesmas.

OPERAÇÕES LÓGICAS EM ARRAYS

Da mesma forma que é possível realizar o uso de operadores aritméticos


para seus devidos cálculos entre elementos de arrays, também é uma prática
perfeitamente possível fazer o uso de operadores lógicos para verificar a
equivalência de elementos de duas ou mais arrays.
Neste exemplo, duas arrays d1 e d2 novamente, agora com algumas
pequenas mudanças em alguns elementos, apenas para tornar nosso
exemplo mais interessante.
Em nossa variável d3 estamos usando um operador para verificar se
os elementos da array d2 são maiores que os elementos equivalentes em d1.
Novamente, o retorno será gerado mostrando o resultado dessa operação
lógica para cada elemento.
Repare que por exemplo o 4º elemento da array d2 se comparado com o 4º
elemento da array d1 retornou False, pois 4 não é maior que 12.
Da mesma forma, o último elemento de d2 se comparado com o
último elemento de d1 retornou True, pois obviamente 25 é maior que 22.

Transposição de arrays

Em determinadas aplicações, principalmente em machine learning, é


realizada a chamada transposição de matrizes, que nada mais é do que
realizar uma alteração de forma da mesma, transformando linhas em
colunas e vice-versa por meio da função transpose( ).
Note que inicialmente é criada uma array de nome arr1, bidimensional, com
elementos distribuídos em 2 linhas e 3 colunas.
Na sequência é criada uma nova variável de nome arr1_transposta
que recebe o conteúdo de arr1, sob a função transpose( ). Como sempre, por
meio da função print( ) realizaremos as devidas verificações.
Como esperado, na primeira parte de nosso retorno temos a array arr1 em
sua forma original, com shape (2, 3) e logo abaixo a array arr1_transposta,
que nada mais é do que o conteúdo de arr1 reorganizados no que diz
respeito às suas linhas e colunas, nesse caso com o shape (3, 2).

Salvando uma array no disco local


Encerrando nossos estudos, vamos ver como é possível salvar nossas arrays
localmente para reutilização. Para isso, simplesmente criamos uma array
comum atribuída a variável data.
Em seguida, por meio da função np.save( ) podemos de fato
exportar os dados de nossa array para um arquivo local. Note que como
parâmetros da função np.save( ) em primeiro lugar repassamos em forma de
string um nome para nosso arquivo, seguido do nome da variável que
possui a array atribuída para si.
O arquivo gerado será, nesse caso, minha_array.npy.

Carregando uma array do disco local

Da mesma forma que era possível salvar nossa array em um arquivo no


disco local, outra possibilidade que temos é de justamente carregar este
arquivo para sua utilização.

Através da função np.load( ), bastando passar como parâmetro o nome do


arquivo com sua extensão, o carregamento será feito, inclusive já alocando
em memória a array para que se possam realizar operações sobre a mesma.

Exemplo de aplicação da biblioteca Numpy


Partindo para prática, vamos simular uma simples aplicação, toda
implementada fazendo o uso de recursos da biblioteca Numpy, onde é
criada uma estrutura de vetor não ordenado.
Inicialmente, é necessário sempre realizar as devidas importações das
bibliotecas, módulos e pacotes que iremos utilizar ao longo de nosso
código. Nesse caso, bastando importar a biblioteca numpy, por convenção a
referenciando como np.
Em seguida é criada uma classe de nome Vetor, dentro de seu corpo/escopo,
é criado um método construtor/inicializador __init__( ) que define o escopo
dessa classe para seus objetos, e que por sua vez receberá obrigatoriamente
um dado/valor a ser utilizado como referência para o tamanho fixo deste
vetor.
Sendo assim, são criados alguns objetos de classe, onde o primeiro deles é
self.tamanho, que recebe o valor atribuído a tamanho. Também é criado um
outro objeto de classe de nome self.ultimo, que recebe como atributo o
valor inicial -1, que por sua vez define um gatilho a ser acionado quando o
vetor atingir sua capacidade máxima de elementos.
Por fim, é criado um último objeto de classe chamado self.elementos, que
recebe como atributo inicial uma array do tipo numpy vazia, porém já com
tamanho definido baseado no valor do objeto tamanho, assim como já é
aproveitada a deixa para estabelecer o tipo de dado que irá obrigatoriamente
compor este vetor.
Na sequência é criado um novo método de classe, dessa vez de nome
exibe_em_tela( ), que como o próprio nome já sugere, quando instanciado e
inicializado, retornará a própria estrutura array desse vetor.
Para isso, inicialmente é criada uma estrutura condicional que verifica se o
valor de self.ultimo for igual a -1, então exibe em tela via função print( ) a
mensagem ‘Vetor vazio!!!’.
Caso essa condição não seja válida, por meio de um laço for é percorrido
cada elemento de nosso vetor, a cada ciclo de repetição exibindo o mesmo
assim como seu número de índice.
Dando sequência em nosso código, é criado um novo método de classe,
dessa vez chamado insere_elemento( ), que recebe como parâmetro um
elemento.
Dentro do corpo desta função inicialmente é criada uma estrutura
condicional onde se o valor atribuído a self.ultimo for igual ao valor de
self.tamanho – 1, é exibido em tela a mensagem ‘Capacidade máxima
atingida’.
Caso contrário, self.ultimo tem seu valor incrementado em 1 unidade, assim
como para self.elementos na posição [self.ultimo] é inserido o novo
elemento anteriormente repassado como parâmetro para nossa função
insere_elemento( ).
Na sequência é criado um novo método de classe, agora chamado
pesquisa_elemento( ) que por sua vez receberá como parâmetro um
elemento.
No bloco indentado a esta função temos um laço de repetição for que
percorre cada um dos elementos de nosso vetor, validando se o valor atual
de elemento é igual a self.elementos na posição [i], caso seja igual, é
retornado o último valor atribuído a i, caso não seja igual, é retornado -1.
Por fim, é criado um último método de classe, dessa vez de nome
exclui_elemento( ) que receberá por justaposição um elemento como
parâmetro.
Dentro do corpo dessa função é criada uma variável local de nome posição,
que por sua vez recebe como atributo os dados/valores oriundos da função
aninhada self.pesquisa_elemento(elemento).
Em seguida é criada uma estrutura condicional onde se o valor de posição
for igual a -1, é retornado -1, caso contrário, é feito o uso de um laço de
repetição que percorrerá todos os elementos de posição equiparados com
self.ultimo.
A partir deste ponto, self.elementos na posição [i] tem seu valor atualizado
com o último valor atribuído a self.elementos em sua posição [i + 1],
finalizando com o decremento de sela.ultimo em 1 unidade.
Devolta ao escopo global do código, é criada uma variável de nome base
que por sua vez instancia e inicializa a classe Vetor( ), repassando como
argumento para a mesma o valor 10. Em outras palavras, aqui a variável
base importa toda a estrutura interna da classe Vetor, assim como define um
tamanho fixo de vetor em 10 elementos.
Usando do método base.exibe_em_tela( ) o retorno gerado neste momento é
‘Vetor vazio!!!”, uma vez que ainda não inserimos elementos no mesmo.

Usando do método insere_elemento( ) atrelado a nossa variável base,


podemos inserir alguns elementos em nosso vetor. Novamente, usando do
método exibe_em_tela( ), nos é retornado o vetor representado por seus
elementos.

Realizando outro tipo de interação com nosso vetor, podemos usar de nosso
método pesquisa_elemento( ) parametrizado com o elemento em si para
descobrir sua posição de índice no vetor.
Também é possível pesquisar diretamente o último valor atribuído para o
objeto ultimo, usando desse retorno, que será um número de índice, para
descobrirmos quantos elementos compõem nosso vetor.

Uma vez que nosso vetor esteja definido, assim como para o mesmo não
exista nenhum conflito de interação ou até mesmo de sintaxe, podemos
manipular este vetor à vontade.
Inserindo alguns elementos via método insere_elemento( ), podemos
visualizar nosso vetor instanciando e executando o método exibe_em_tela( )
sempre que necessário.
Por fim, testando nosso método exclui_elemento( ) será possível ver que
quando excluímos um determinado elemento do vetor, os elementos
subsequentes serão realocados automaticamente para que as posições de
índice se mantenham íntegras.

BIBLIOTECA PANDAS
Sobre a biblioteca Pandas

A biblioteca Pandas, segundo sua própria documentação (disponível em


pandas.pydata.org) é uma biblioteca especificamente criada para análise e
manipulação de dados, desenvolvida e integrada totalmente em Python.
Seu projeto base tem início em 2008 pela AQR Capital
Management, se tornando open source a partir de 2009, recebendo ao longo
dos anos uma série de melhorias feitas tanto pela comunidade quanto por
sua mantenedora NumFOCUS, que custeia o projeto desde 2015.
A biblioteca Pandas visa implementar ao Python uma série de ferramentas
integradas para o tratamento de dados organizados a partir de tabelas ou
bases de dados onde os mesmos estão distribuídos em formato de linhas e
colunas mapeadas e indexadas em um padrão.
A integração da biblioteca Pandas com o núcleo da linguagem Python
permite realizar de forma fácil, rápida e flexível a análise e manipulação de
dados sem que para isso seja necessário o uso de ferramentas externas,
tornando assim tal biblioteca uma das melhores (se não a melhor) no
quesito de tratamento de dados.

Instalação das Dependências

Para realizar a instalação da biblioteca Pandas no ambiente virtualizado de


seu sistema é muito simples, bastando a partir de algum terminal, via
gerenciador de pacotes pip (ou conda dependendo de seu ambiente de
trabalho) executar o código pip install padas.
Não havendo nenhum erro no processo de instalação, após um rápido
download é instalado no sistema a última versão estável da biblioteca
Pandas.

Tipos de dados básicos Pandas

- Series
Dos tipos de dados básicos da biblioteca Pandas, o primeiro que devemos
entender é o tipo Serie. A biblioteca Pandas possui alguns tipos de dados
particulares (de funcionalidades equivalentes a outros tipos de dados como
arrays), onde uma Serie equivale a uma array unidimensional, com
mapeamento de elementos e índice próprio, capaz de aceitar em sua
composição todo e qualquer tipo de dado Python.

Por hora, raciocine que as estruturas de dados utilizados via Pandas se


assemelham muito a uma planilha Excel, onde o conteúdo é disposto em
formato de linhas e colunas. Facilitando assim a aplicação de operações
sobre dados desta “tabela” uma vez que cada elemento da mesma possui
uma referência de mapeamento de índice.
Apenas como exemplo, no código acima temos uma variável de nome base
que por sua vez recebe como atributo uma lista com diversos elementos.
Em seguida é criada uma nova variável de nome data que instancia e
inicializa a função pd.Series( ), parametrizando a mesma com os dados de
base.
Exibindo em tela via função print( ) o conteúdo da variável data, é possível
ver uma Serie composta de um índice, uma coluna e 10 linhas com seus
respectivos elementos.

- DataFrames
Outro tipo de dado básico usado na biblioteca Pandas é o chamado
DataFrame, muito semelhante a uma Serie (inclusive um DataFrame é
formado por um conjunto de Series), porém equivalente a uma array
multidimensional. Dessa forma, o comportamento e o tratamento dos dados
ganham novas funcionalidades que veremos em detalhes nos próximos
capítulos.
O ponto chave aqui é que, para dados dispostos em vetores e matrizes, a
biblioteca Pandas oferece uma vasta gama de funções aplicáveis a tais
dados desde que os mesmos estejam, seja nativos ou convertidos, em
formato DataFrame.

Novamente apenas para fins de exemplo, inicialmente temos uma variável


de nome base que recebe como um atributo um dicionário composto de
duas chaves e uma lista de valores atribuídos para as mesmas.
Na sequência é criada uma variável de nome data, que instancia e inicializa
a função pd.DataFrame( ), por sua vez parametrizada com o conteúdo da
variável base.
Via função print( ), parametrizada com os dados de data, nos é exibido em
tela um Dataframe, onde podemos notar que as chaves do dicionário de
origem se tornaram os cabeçalhos das colunas, tendo seus respectivos
valores dispostos nas linhas destas colunas, além é claro, do índice interno
gerado para esse DataFrame.

ANÁLISE EXPLORATÓRIA DE DADOS

Importando a biblioteca Pandas

Uma vez que não tenha ocorrido nenhum erro no processo de instalação,
para utilizarmos das funcionalidades da biblioteca Pandas é necessário
importar a mesma para nosso código.
O processo é extremamente simples, igual ao de qualquer outra importação
em Python. Sendo assim, por meio do comando import pandas todo seu
conteúdo será importado ficando assim seus recursos à disposição do
desenvolvedor.
Apenas por convenção podemos referenciar tal biblioteca por
alguma sigla, o comum entre a comunidade é instanciar a biblioteca Pandas
como simplesmente pd.

SERIES

Criando uma Serie

Uma Serie pode ser criada de diversas formas, sendo a mais comum delas a
partir de seu método construtor, utilizando como base dados em forma de
lista, uma vez que se tratando de uma Serie teremos como resultado final
uma array unidimensional.
No exemplo, dada uma variável de nome base, que por sua vez possui como
atributo uma lista de elementos numéricos, podemos usar da mesma para a
geração de uma Serie.
Então é criada uma nova variável, dessa vez de nome data, que instancia e
inicializa a função pd.Series( ), repassando como parâmetro para a mesma o
conteúdo da variável base.
Exibindo em tela por meio de nossa função print( ), nos é retornado uma
estrutura com características de uma tabela indexada de apenas uma coluna
e 10 linhas.

Usando de nossa função type( ) aninhada a função print( ), por sua vez
parametrizada com data, é possível ver o tipo de dado de nossa variável
data.
Como esperado, o conteúdo de data é uma Serie criada a partir dos dados de
base, logo, nos é retornado como tipo de dado pandas.core.series.Series.
Outra possibilidade é que criemos uma Serie via construtor, repassando os
dados diretamente como parâmetro para a mesma, desde que tais dados
estejam em formato de lista.
No exemplo, a variável data chama a função pd.Series( ) parametrizando a
mesma com a lista [1,3,5,7,9,12,15,18,21,24].
Exibindo novamente o conteúdo de data via função print( ) nos é retornado
uma Serie igual as anteriores.

Como mencionado anteriormente, tanto uma Serie quanto um DataFrame


suporta todo e qualquer tipo de dado em sua composição.
Declarada uma variável de nome data2, é chamada a função pd.Series( )
parametrizando tal função com uma lista de nomes em formato string.
Exibindo em tela via print( ) o conteúdo de data2, como esperado é
retornado uma Serie, uma tabela unidimensional, agora composta por
nomes oriundos da lista de origem.

Visualizando o cabeçalho

Algumas Series / DataFrames que veremos em tópicos futuros podem ter


uma grande variedade de dados organizados nas mais diversas formas.
Uma maneira de se ter um primeiro contato com esses dados para entender
visualmente sua estrutura é usar do método head( ).
Aplicando o método head( ) em nossa variável data, exibindo a mesma em
tela via função print( ), o retorno será os 5 primeiros elementos de nossa
Serie, já com seu índice, para que assim possamos ver a forma como os
dados estão dispostos.

Definindo uma origem personalizada


Por justaposição, o primeiro parâmetro de nossa função pd.Series( ) deverá
ser a origem dos dados a serem utilizados, essa origem pode ser
personalizada (como veremos em exemplos posteriores) assim como
definida manualmente pelo parâmetro nomeado data = seguido do nome da
variável de origem.

Definindo um índice personalizado


Explorando os possíveis parâmetros para a função pd.Series( ), outro
bastante útil é o parâmetro index, onde pelo qual podemos definir um índice
personalizado.
Índices em Python são gerados iniciados em 0, quando não definimos um
índice específico para nossa Serie será usado este formato de índice padrão
Python, porém, para nossos dados em Series ou DataFrames podemos
atribuir índices personalizados (com qualquer tipo de dado como índice,
inclusive textos).
No exemplo, inicialmente temos uma variável de nome indice que recebe
como atributo uma lista com alguns números ordenados.
Da mesma forma temos uma variável de nome nomes que recebe como
atributo uma lista composta de alguns nomes em formato string.
Na sequência é criada uma variável de nome data4, que instancia e
inicializa a função pd.Series( ), definindo para o parâmetro nomeado data
que os dados de origem serão oriundos da variável nomes, assim como para
index que os dados usados como índice deverão ser importados da variável
indices.
Note que para esse processo de definir um índice personalizado, o número
de elementos da lista deve ser o mesmo número de elementos do índice,
caso contrário, será gerada uma exceção pois os dados não são equivalentes.
Via print( ), parametrizada com data4, nos é retornado uma Serie onde o
índice inicia pelo número 1 conforme a lista de origem, composta de uma
coluna com os respectivos nomes oriundos da variável nomes.

Integrando uma Array Numpy em uma Serie

Uma das características da biblioteca Pandas é sua fácil integração com


outras bibliotecas, suportando praticamente todo e qualquer tipo de
estrutura de contêineres de dados para sua composição.
Em nosso exemplo, gerando uma array por meio da biblioteca Numpy,
podemos perfeitamente transformar esta array numpy em uma Serie ou
DataFrame Pandas.
Para isso, inicialmente é criada uma variável de nome nomes que por meio
da função np.array( ) gera uma array do tipo numpy composta de uma lista
de elementos em formato string.
Em seguida é criada uma variável de nome data5 que por meio da função
os.Series( ), transforma nossa array numpy de nomes para uma Serie.
Novamente, exibindo em tela o conteúdo de data5 via função print( ) nos é
retornada a Serie.
Qualquer estrutura de dados como elemento de uma Serie

Uma vez que tudo em Python é objeto, dentro das possibilidades de uso de
dados em uma Serie ou DataFrame podemos até mesmo usar de um ou mais
métodos como elementos dessa Serie ou DataFrame.
Apenas como exemplo, declarada a variável funcoes que recebe uma lista
com dois elementos, elementos estes que são palavras reservadas ao sistema
para funções de entrada e de saída, respectivamente, tais elementos podem
ser perfeitamente incorporados em uma Serie ou em um DataFrame.
Logo em seguida é criada uma variável de nome data6 que instancia e
inicializa a função pd.Series( ), usando como dados de origem o conteúdo
atrelado a variável funcoes.
Por fim, exibindo em tela o conteúdo de data6 via print( ), são retornadas as
referências aos objetos referentes aos métodos input( ) e print( ).

Integrando funções com Series


No processo de geração de uma Serie ou um DataFrame por meio de seus
respectivos métodos construtores, se tratando dos parâmetros nomeados
utilizados, podemos atribuir funções para tais parâmetros sem que isso gere
algum tipo de problema.
Em nosso exemplo, inicialmente é declarada uma variável de nome paises
que recebe como atributo para si uma lista com alguns nomes de países em
formato string.
De modo parecido é criada uma variável de nome data7, que chama a
função pd.Series( ), definindo manualmente que como dados o conteúdo
associado deverá ser importado da variável paises, na sequência para o
parâmetro index usamos de uma função da biblioteca Numpy np.arange( ),
que irá gerar valores ordenados de 0 até 5, a serem utilizados como índice.
Mais uma vez, via print( ), agora parametrizada com data7, é exibido em
tela a Serie composta pelo índice gerado e pela lista de nome de países
usada como dados.

Verificando o tamanho de uma Serie


Aproveitando o tópico anterior, um ponto a ser entendido é que o tamanho
em si de uma Serie é definido a partir de seu índice.
Em outras palavras, ao consultar o tamanho de uma Serie ou de um
DataFrame, como tais estruturas podem ter camadas e mais camadas de
dados sobrepostos, a referência para o tamanho sempre será o valor
declarado em seu índice.
No exemplo, aplicando o método index para a variável data gerada em
tópicos anteriores, é retornado um RangeIndex, dizendo que o conteúdo de
data tem 10 elementos, iniciados em 0 até 10, contados de um em um
elemento.

Tentando reproduzir o mesmo retorno a partir de data7 (variável gerada no


tópico imediatamente anterior a este), repare que agora a referência é a
própria lista, pois a mesma foi gerada por meio da função np.arange( ).
O importante a entender aqui é que, ao inspecionar o tamanho de uma Serie
ou DataFrame, apenas teremos um resultado útil quando tais estruturas de
dados possuírem um índice nativo. Uma vez que tenhamos alterado o
índice, personalizando o mesmo de alguma forma, internamente a
referência original do índice é sobrescrita pela informação nova, perdendo
assim a referência original que retornaria um dado para index.

Criando uma Serie a partir de um dicionário


Como dito em alguns dos tópicos anteriores, uma Serie é uma estrutura de
dados equivalente a uma array unidimensional.
Essa estrutura que define o esqueleto / o formato de uma Serie não pode ser
alterada, de forma que ao tentarmos usar de dados multidimensionais para
criação de uma Serie, tais dados serão remodelados para não alterar a
estrutura original da Serie.
Usando de um exemplo, uma Serie pode ser criada a partir de um dicionário
ou um contêiner de dados equivalente, porém, nesse caso, os dados das
chaves do dicionário serão convertidos para o índice da série, enquanto os
valores do dicionário serão utilizados como os dados da Serie.
Partindo para a prática, inicialmente é criada uma variável de nome
dicionario que por sua vez, em forma de dicionário, recebe três chaves:
'Nome', ‘Idade’ e ‘Altura’ com seus respectivos valores ‘Fernando’, 33 e
1.90.
Em seguida é criada uma variável de nome serie_dicio que chama a função
pd.Series( ) parametrizando a mesma com o conteúdo da variável
dicionario.
Exibindo em tela por meio da função print( ) o conteúdo da variável
serie_dicio, é possível notar que de fato os dados/valores das chaves se
tornaram o índice, assim como os dados/valores dos valores do dicionário
de origem se tornaram o conteúdo da Serie.
Usando de um dicionário origem onde suas chaves são números, o processo
de conversão das chaves em índice se torna natural, retornando uma Serie
onde seu índice é numérico e ordenado como esperado para maior parte dos
contextos.

Unindo elementos de duas Series


Ao trabalharmos com Series possuímos uma série de restrições, seja pela
estrutura base, seja pelo formato interno, seja por sua indexação, seja por
interação com outras Series, entre outras situações, algo que como veremos
nos próximos capítulos são limitações apenas de Series, que não ocorrem
em DataFrames que são estruturas de dados mais robustas.
Apenas simulando um erro rotineiro, ao simplesmente tentar unir dados de
duas Series origem para a criação de uma terceira, já começarmos a ter
comportamentos que podem não ser o esperado para o contexto geral.
Diretamente ao exemplo, inicialmente temos duas variáveis de nomes
nome1 e nome2, respectivamente, onde como atributo para as mesmas
temos duas listas com alguns nomes em formato de string.
Na sequência é criada uma variável de nome data_1 que, por meio da
função pd.Series( ), gera uma Serie onde por justaposição seus dados serão
a lista [1,2,3,4,5], definindo que como índice serão utilizados os dados
oriundos de nomes1.
Exatamente o mesmo processo é feito para data_2, apenas alterando que
para seu índice deverão ser considerados os dados importados de nomes2.
Em seguida é criada uma nova variável de nome data_3, que simplesmente
como atributo recebe a soma dos elementos de data_1 e de data_2.
Exibindo em tela o resultado dessa soma via print( ), é possível notar que
nos é retornado a soma dos índices dos elementos repetidos, por exemplo,
Maria em nomes1 possui o valor de índice 4, e em nomes2 o valor de índice
7, pois definimos que o índice da Serie iniciaria em 1. No retorno, Maria
aparecerá com valor 7, o que é esperado, porém, onde não houverem dados
com valores a serem somados o retorno é Nan.
Dessa forma, ao invés de uma soma ou sobreposição de dados como o
contexto exigiria, o que é gerado é um espaço alocado sem dado nenhum.
Tal situação certamente acarretaria em todo um tratamento específico para
contornar essa ausência de dados, alterando em definitivo a integridade dos
dados originais, o que não pode ocorrer.
Sendo assim, é interessante termos em mente sempre que Series são dados
bastante básicos, rápidos e eficientes para certas situações, porém muito
restritivos quando se tratando de interações de dados em Series distintas.

DATAFRAMES

Partindo para a principal estrutura de dados da biblioteca Pandas,


finalmente vamos entender as particularidades dos chamados DataFrames
nos mesmos moldes dos tópicos anteriores, ou seja, na prática por meio de
exemplos.
Importante salientar que o fato de termos dedicado toda uma parte deste
pequeno livro falando especificamente sobre Series não necessariamente
acarreta em conhecimento inutilizado, muito pelo contrário, haja visto que
um DataFrame é constituído de Series, tudo o que vimos até o momento em
Series se aplica a DataFrames.
Criando um DataFrame

Dando início ao entendimento do uso de DataFrames diretamente na


prática, não podemos deixar de entender sua estrutura básica desde sua
criação, como fizemos ao dar nossos primeiros passos com Series.
DataFrames como dito anteriormente, são estruturas de dados, normalmente
criadas a partir de dicionários, onde teremos dados organizados e dispostos
em algo muito semelhante a uma tabela Excel, haja visto que aqui
trabalharemos com matrizes multidimensionais, separados em linhas e
colunas mapeadas, para que possamos de forma simples manipular dados
dessas estruturas sem a necessidade de ferramentas externas.
Partindo para o código, inicialmente declarada uma variável de nome base,
que possui como atributo um dicionário simples de dois conjuntos de
chaves : valores, podemos nos mesmos moldes anteriores gerar outras
estruturas de dados a partir dessa variável.
Para isso, criamos uma variável de nome data que instancia e inicializa o
método construtor de DataFrames pd.DataFrame( ), parametrizando o
mesmo com o conteúdo da variável base.
Exibindo em tela via função print( ) o conteúdo de base, temos algo muito
semelhante ao que foi visto até o momento em uma Serie, com o grande
diferencial de que agora temos dados em uma tabela equivalente a uma
array / matriz multidimensional.

Verificando o tipo de dado por meio de nossa função type( ) aninhada para
print( ), parametrizada com base, nos é retornado
pandas.core.frame.DataFrame.
Usando do método info( ) podemos obter um resumo gerado para nosso
DataFrame, uma vez que tal base de dados esteja associada a uma variável,
podendo assim aplicar o método info( ) para a mesma, retornando assim
informações como tipo de dado, número de elementos, tipo de dado dos
elementos, finalizando com tamanho alocado em memória para esse
DataFrame.

Outra possível verificação rápida sobre os dados de nosso DataFrame pode


ser feita por meio do método describe( ), que por sua vez retorna (quando
aplicável) informações sobre quantidade de elementos, média dos valores
dos mesmos, desvio padrão de tais valores, assim como os valores mínimos,
25%, 50%, 75% e valor máximo encontrado para os elementos deste
DataFrame.
Lembrando que para um DataFrame se aplicam todos os método utilizados
até o momento, como exemplo podemos criar um DataFrame com dados
gerados a partir de uma função embutida ou de outra biblioteca, por
exemplo np.random.randn( ) que para nosso exemplo irá gerar uma matriz
de 6 linhas e 5 colunas de dados entre 0 e 1.
De forma parecida, definimos manualmente para alguns parâmetros
nomeados alguns dados/valores para compor nosso DataFrame.
Desses parâmetros, um não explorado até o momento é o columns, onde
podemos especificar que referências iremos passar como cabeçalho para
nossas colunas, uma vez que um DataFrame, diferentemente de uma Serie,
terá duas ou mais colunas em sua forma.
Por meio da função print( ), por sua vez parametrizada com data, é possível
notar que via método construtor pd.DataFrame( ) geramos uma estrutura de
6 linhas, 5 colunas, índice sequencial de 1 até 6 e cabeçalho de colunas com
letras de ‘a’ até ‘e’.
Na mesma linha de raciocínio que viemos criando, extraindo informações
apenas de uma coluna, a mesma internamente será considerada uma Serie.
Por meio da função print( ), parametrizada com data.columns, é exibido em
tela um índice específico para as colunas, pois estruturalmente cada coluna
de nosso DataFrame é uma Serie.

Como visto em outros momentos, por meio de data.index temos acesso ao


índice interno de nosso DataFrame.
O ponto a destacar aqui é que, uma vez que temos um DataFrame, com
índices gerados tanto para suas linhas quanto para suas colunas,
conseguimos mapear facilmente qualquer dado/valor de qualquer elemento
deste DataFrame.

Uma vez que temos uma coluna como uma Serie, e uma Serie tem seus
dados lidos como em uma lista, podemos usar da notação de listas em
Python para fazer referência a uma coluna ou a um determinado elemento
da mesma.
Em nosso exemplo, parametrizando nossa função print( ) com data na
posição [‘c’] (*notação de lista), nos é retornado apenas os dados da coluna
c de nosso DataFrame.
Visualizando o tipo de dado de data (todo o DataFrame) via type( ) nos é
retornado pandas.core.frame.DataFrame.

Visualizando o tipo de dado de data[‘c’] (apenas a coluna ‘c’ do


DataFrame) nos é retornado pandas.core.series.Series.

Extraindo dados de uma coluna específica

Como visto anteriormente, tratando uma coluna como uma Serie podemos
extrair dados de uma determinada coluna usando da notação de extração de
dados de uma lista.
Porém, o método usual para extração de dados de uma coluna específica de
um DataFrame é simplesmente fazer referência ao nome do cabeçalho da
mesma.
Via print( ), parametrizado com data.d, estamos exibindo em tela apenas o
conteúdo da coluna de nome ‘d’ de nosso DataFrame.
Embora esse seja o método nativo, o mesmo possui como limitação a
extração de dados de apenas uma coluna por vez.

Retomando o uso da notação de listas (posição em listas) podemos extrair


informações de quantas colunas quisermos.
Em nosso exemplo, exibindo em tela por meio de print( ) o conteúdo de
data nas posições [‘c’ , ‘e’] é retornado apenas os conteúdos de tais colunas.
Criando colunas manualmente

Uma vez que estamos usando da notação de posição em listas, podemos


usar de todos os métodos que se aplicam a esta notação.
Relembrando o básico em Python, ao fazer instância a uma posição de lista
inexistente e atribuindo a essa instância um dado/valor, a mesma será criada
na lista.
Em nosso exemplo, instanciando a posição [‘f’] de data (que até o momento
não existe), atribuindo para a mesma os dados da soma entre os dados de
data nas posições [‘a’] e [‘e’], não havendo nenhum erro de sintaxe ou
incompatibilidade dos dados de tais colunas, a coluna ‘f’ será criada.
Exibindo em tela o conteúdo de data agora é possível notar que de fato foi
criada a coluna ‘f’ e seus dados são o resultado da soma de cada elemento
de ‘a’ e ‘e’ em suas respectivas linhas.

Removendo colunas manualmente


Para remover uma determinada coluna de nosso DataFrame temos mais de
uma forma, cabendo ao desenvolvedor optar por usar a qual achar mais
conveniente.
Em nosso exemplo, aplicando o método drop( ) em nossa variável data,
podemos remover uma coluna desde que especifiquemos o nome do
cabeçalho da mesma, seguido do parâmetro axis = 1, para que de fato a
função drop( ) exclua todos os dados da coluna, e não da linha.
Lembrando que para tornar o efeito permanente devemos atualizar a
variável de origem, caso contrário, apenas será apagada a referência, porém
os dados ainda estarão em suas instâncias originais.
Caso você esteja usando de um notebook ipynb (Google Colab, Jupyter,
DataLore, entre outros) para tornar a ação de exclusão de uma coluna
permanente se faz necessário o uso do parâmetro inplace = True, caso
contrário, os dados continuarão presentes, apenas sem referência.

Outra forma de remover uma determinada coluna assim como todo seu
conteúdo é por meio da função do sistema del, especificando qual a variável
origem e a posição de índice ou nome do cabeçalho o qual remover. Por se
tratar de uma função do sistema, independentemente do ambiente, essa
alteração será permanente.
Exibindo em tela o conteúdo de data em nossa função print( ), é possível
notar que a coluna de nome ‘b’ foi removida como era esperado.

Ordenando elementos de uma coluna


Para nosso exemplo, inicialmente vamos gerar um novo DataFrame, haja
visto que nos tópicos anteriores realizamos algumas manipulações dos
dados do DataFrame anterior.
Para isso a variável data instancia e inicializa a função pd.DataFrame( ),
gerando dados aleatórios entre 0 e 1, distribuídos em 6 linhas e 5 colunas,
por meio da função np.random.randn( ), também atribuímos um índice
numérico personalizado e nomes para as colunas igual ao DataFrame dos
exemplos anteriores. A única diferença de fato serão os valores que foram
gerados aleatoriamente novamente para este DataFrame.
Na sequência, aplicando em nossa variável data o método sort_values( )
podemos ordenar de forma crescente os dados de uma determinada coluna
apenas especificando qual para o parâmetro by.
Repare que para a o retorno da primeira função print( ) temos o DataFrame
com seus dados dispostos em sua forma original, enquanto para o segundo
retorno, como esperado, os dados da coluna ‘b’ foram reordenados de forma
crescente.

Extraindo dados de uma linha específica

Continuando dentro da linha de raciocínio de notação em posição de lista,


quando estamos manipulando dados de listas a partir desta notação é muito
comum usar do método loc[ ] que em notação de posição de índice, retorna
dados segundo este parâmetro.
Por meio da função print( ) por sua vez parametrizada como data.loc[3], ao
localizar o elemento de posição de índice 3, será retornado o dado/valor do
mesmo. Nesse caso, note que o retorno é uma Serie mostrando os dados da
linha 3 rearranjados dessa forma, o que apesar de inicialmente confuso, é
bastante funcional.
Lembre-se sempre que todos dados retornados de um DataFrame, quando
destacados dos demais, serão exibidos em formato de Serie, nem que para
isso linhas virem colunas e vice-versa.

Extraindo dados de um elemento específico


Ainda na mesma notação, usando loc[ ] passando para o mesmo duas
referências, em justaposição a primeira referência será referente a linha,
assim como a segunda referência será o identificador para a coluna.
Em nosso exemplo, parametrizando nossa função print( ) com data.loc[2,
‘b’] estamos exibindo em tela apenas o dado/valor do elemento situado na
linha 2 da coluna b.

Extraindo dados de múltiplos elementos

Para extrair uma seleção de elementos, basta na mesma notação, no lugar


do primeiro parâmetro de loc[ ] referente as linhas repassar uma lista de
linhas, assim como para o parâmetro referente as colunas repassar uma lista
de colunas.
Em nosso exemplo, parametrizando print( ) com data.loc[[2, 3], [‘a’, ‘b’,
‘c’]], nos são retornados os elementos das linhas 2 e 3, situados nas colunas
a, b e c.
Outra forma perfeitamente funcional para extração de dados de nossos
DataFrames é usar de iloc[ ] , método muito semelhante ao loc[ ] porém
mais robusto, suportando a extração de elementos de uma lista baseado em
intervalos numéricos de índices.
Exatamente como no exemplo anterior, parametrizando nossa função print(
), agora com data.iloc[1:3, 0:3], estamos extraindo os elementos situados
entre as posições de índice 1 a 3 referente às linhas 2 e 3, do mesmo modo
as posições de índice referente às colunas no intervalo 0 a 3, ou seja, a, b e
c, nos são retornados os mesmos elementos do exemplo anterior.

Buscando elementos via condicionais

Se tratando de dados em DataFrames, uma forma rápida de filtrar os


mesmos é definindo alguma estrutura condicional simples.
Por exemplo, via função print( ), parametrizada com data > 0, será
retornado para cada elemento uma referência True ou False de acordo com
a condição imposta. Em nosso caso, cada elemento True faz referência a um
valor maior que 0, enquanto cada elemento exibido como False não valida
tal condição.
Lembrando que nesse caso, via função print( ), não estamos alterando os
dados de origem, apenas obtendo um retorno booleano para uma condição
imposta. Para alterar os dados de origem devemos atualizar a variável data.

OPERAÇÕES MATEMÁTICAS EM DATAFRAMES

Usando de funções embutidas do sistema

Para os exemplos a seguir, novamente geramos um DataFrame do zero pois


anteriormente realizamos algumas manipulações que para os exemplos
seguintes poderiam gerar algumas exceções.
Sendo assim, novamente geramos um DataFrame de valores aleatórios,
índice numérico e índice de colunas definidos manualmente.
Usando de funções embutidas do sistema, toda e qualquer função pode ser
usada normalmente, como quando aplicada a qualquer elemento de
qualquer contêiner de dados.
Em nosso exemplo, por meio da função print( ), agora parametrizada com
data na posição [‘b’], aplicando sobre essa posição em índice o método
sum( ), o retorno gerado é a soma dos elementos da coluna b de nosso
DataFrame, nesse caso, -4.454328061670092.

Aplicando uma operação matemática a todos elementos

Uma vez que temos um DataFrame em uma variável, como mencionado


anteriormente, tal estrutura de dados possui uma notação característica
identificada normalmente por nosso interpretador como matrizes de dados,
ou seja, dados de listas.
Dessa forma, podemos usar de operações matemáticas simples diretamente
aplicadas à variável, lembrando que nesses casos, a operação em si terá
efeito sobre todos os elementos de nosso DataFrame.
Por exemplo, instanciando nossa variável data, atualizando a mesma com
data + 1, será somado o valor 1 a todos os elementos do DataFrame
original.

Usando de funções matemáticas personalizadas

Além de usar de funções embutidas do sistema ou funções matemáticas


básicas padrão, outra possibilidade é a de usar de funções matemáticas
personalizadas, aplicadas a elementos de apenas uma coluna de nosso
DataFrame, e isso é feito por meio do método apply( ).
Em nosso exemplo, inicialmente definimos uma função personalizada de
nome soma( ), que receberá um valor para x, retornando tal valor somado
com ele próprio. Lembrando que aqui cabe toda e qualquer operação
aritmética ou expressão matemática.
Na sequência, por meio de print( ) parametrizado com data na posição [‘b’],
aplicando o método apply( ) por sua vez parametrizado com nossa função
soma( ), é exibido em tela uma Serie onde cada um dos elementos dessa
Serie (cada um dos elementos da coluna b de nosso DataFrame) teve seu
valor alterado de acordo com nossa função soma( ).
Outro exemplo, via print( ) exibimos em tela os valores dos elementos da
coluna a de nosso DataFrame elevados ao quadrado, usando do método
apply( ) aplicado sobre nossa variável data na posição [‘a’] a função
ao_quadrado( ) criada anteriormente.

Dentro da mesma notação vista anteriormente em outros exemplos,


podemos selecionar um ou mais elementos específicos de nosso DataFrame
por meio de iloc[ ] aplicando sobre os mesmos via apply( ) alguma função
matemática personalizada.
Em nosso exemplo, é exibido em tela apenas o elemento situado na linha1,
coluna b, (intervalo de índice para linha entre 0 e 1, para coluna entre 1 e 2),
tendo seu valor elevado ao quadrado de acordo com a função aplicada sobre
o mesmo.
Por fim, encerrando essa linha de raciocínio, usando do método apply( )
para aplicar sobre elementos de nosso DataFrame funções personalizadas,
uma prática comum é usar de expressões lambda para tornar o código
reduzido nestas aplicações.
Apenas como exemplo, exibindo em tela via função print( ) os elementos da
posição [‘d’] de nossa variável data, elevados ao quadrado via função
anônima escrita diretamente como parâmetro do método apply( ), em forma
reduzida.

ESTRUTURAS CONDICIONAIS APLICADAS A


DATAFRAMES

Aplicação de estruturas condicionais simples


Durante a manipulação de nossos dados, uma prática comum é o uso de
estruturas condicionais para, com base em estruturas condicionais definidas,
filtrar elementos ou até mesmo definir comportamentos em nosso
DataFrame.
Como estrutura condicional simples em Python entendemos que em sua
expressão, existe apenas um campo a ser validado como verdadeiro para
que assim toda uma cadeia de processos seja executada.
Mais uma vez, apenas para garantir a consistência de nossos exemplos, um
novo DataFrame é gerado do mesmo modo como os anteriores.
Partindo para a prática, para as linhas 5, 6 e 7 de nosso código declaramos
três funções print( ), sendo a primeira parametrizada apenas com data,
exibindo dessa forma todo o conteúdo do DataFrame. Segunda função
print( ) parametrizada com data na posição [‘a’] exibe apenas os elementos
da coluna a do DataFrame. Terceira função print( ) exibe data na posição
[‘a’] validando uma condição simples onde apenas serão exibidos como
True os valores menores que 0.
Lembrando novamente que apenas para fins de testes, usar dessas estruturas
condicionais em nossa função print( ) não altera os dados originais nem
modifica nenhum comportamento de nosso DataFrame. Para esses casos
onde se faz necessário a modificação de elementos do DataFrame, a
variável ao qual o DataFrame está associado deve ser atualizada.
De modo parecido com o exemplo anterior, uma vez que temos um
DataFrame associado a variável data, via função print( ) podemos ver todo
o conteúdo do DataFrame.
Para nossa segunda função print( ) repassamos como parâmetro data na
posição [‘a’], logo, é exibido em tela o conteúdo da coluna a de nosso
DataFrame.
Na sequência é criada uma variável de nome data2, que recebe como
atributo um novo DataFrame baseado no anterior, onde para o mesmo serão
mantidos apenas os dados referentes a data na posição [‘a’] que forem
menores que 0.
Por fim é exibido em tela o conteúdo de data2 via função print( ).
Repare que como foi feito nos exemplos anteriores, usando dessa notação e
modo de estrutura condicional, nos era retornado o mesmo DataFrame com
marcadores True e False para os resultados das validações.
Agora, apenas para fins de exemplo, ao atribuir essa mesma estrutura a uma
variável, exibindo em tela seu conteúdo é possível notar que os elementos
os quais não atingiam a condição imposta simplesmente foram descartados,
de modo que o novo DataFrame gerado possui um novo formato de acordo
com os elementos presentes.
Outra situação viável é, usando de estruturas condicionais aplicadas a nosso
DataFrame, gerar um novo DataFrame composto de dados obtidos a partir
de validações em estruturas condicionais.
Como exemplo, usando do mesmo DataFrame do tópico anterior, como já
visto anteriormente, para tornar alterações permanentes em um DataFrame
estamos acostumados a associá-lo a uma variável, para que atualizando o
conteúdo de seus atributos as alterações se tornem permanentes.
Porém, existe um modo de realizar tais alterações de forma permanente a
partir de nossa função print( ). Para isso, note que na linha 7 de nosso
código temos como parâmetro de nossa função print( ) data, que na posição
[data[‘a’] < 0 é aplicada uma estrutura condicional. Nesse caso, será
retornado um novo DataFrame sem os elementos da coluna ‘a’ que forem
menores que 0, e esta alteração é permanete.
Outra possibilidade, referente a linha 8 de nosso código, é usar de uma
estrutura condicional que valida dados tendo como base duas referências
diferentes. Este processo é muito parecido com o anterior, porém com
algumas particularidades por parte de sua notação.
Em nossa linha 8 do código, como parâmetro repassado para a função print(
) existe a expressão data que na posição [data[‘a’] < 0][‘e’], para que nesse
caso seja retornado um novo DataFrame usando de uma estrutura
condicional aplicada sobre data na posição [‘a’] validando seus elementos
menores que 0, retornando os elementos equivalentes da coluna ‘e’.
Por fim, repare que toda essa expressão altera permanentemente os dados
de data conforme a notação explicada no exemplo anterior.
Como já visto inúmeras vezes, é perfeitamente possível isolar tais dados,
extraindo os mesmos para uma nova variável.
Apenas como exemplo, é declarada uma variável de nome data2 que recebe
como atributo os dados de data na posição [data[‘a’] < 0][‘e’] exatamente
como feito anteriormente.
Exibindo em tela via função print( ) o conteúdo de data2 nos é apresentado
uma Serie composta dos elementos da coluna e que atendiam a condição
imposta para data na posição [data[‘a’].
Em resumo, a primeira expressão serve apenas como referência, pois os
dados retornados eram os elementos equivalentes situados na coluna ‘e’ do
DataFrame de origem.

Aplicação de estruturas condicionais compostas


Uma vez entendidos os conceitos básicos funcionais da aplicação de
estruturas condicionais simples para os dados de nosso DataFrame,
podemos avançar um pouco mais entendendo a lógica de aplicação de
estruturas condicionais compostas.
Como estruturas condicionais compostas em Python temos expressões com
a mesma notação das condicionais simples, porém, um grande diferencial
aqui é que temos de usar alguns operadores um pouco diferentes do usual
para conseguir de fato validar duas ou mais expressões condicionais.
Revisando o básico em Python, em um ambiente de código padrão, é muito
útil usar dos operadores and e or para criar estruturas condicionais
compostas. Por meio do operador and definimos que as duas expressões
condicionais devem ser verdadeiras para assim desencadear a execução dos
blocos de código atrelados a esta estrutura. Da mesma forma, usando do
operador or criamos estruturas condicionais compostas onde apenas uma
das expressões sendo verdadeira já valida todo o resto, acionando os
gatilhos para execução dos seus blocos de código.
Por parte dos operadores, para a biblioteca Pandas temos duas
particularidades as quais devemos respeitar, caso contrário serão geradas
exceções. Para um DataFrame, no lugar do operador and devemos usar do
operador ‘&’, assim como no lugar do operador or temos de usar o operador
‘|’.

Para que tais conceitos façam mais sentido, vamos para a prática.
Novamente, apenas para evitar inconsistências em nossos exemplos, um
novo DataDrame é criado nos mesmos moldes dos DataFrames anteriores.
Em seguida, por meio da função print( ) exibimos em tela o conteúdo de
nosso DataFrame associado a variável data.
Na sequência, diretamente como parâmetro de nossa função print( ),
criamos uma estrutura condicional composta onde os dados de data na
posição [‘a’] forem menores que zero e (and &) os dados de data na posição
[‘b’] forem maiores que 1, um novo DataFrame é gerado, atualizando os
dados originais de data.
Observe que a expressão completa foi escrita de modo que a variável data
terá sua estrutura atualizada com base nas expressões condicionais
declaradas, porém o que foi retornado é um DataFrame vazio, pois ao
menos uma das condições impostas não foi atingida.

Isolando a expressão condicional e atribuindo a mesma a uma variável,


nesse caso de nome data2, exibindo em tela seu conteúdo via print( ) é
possível ver que o conteúdo de data2 é de fato um DataFrame vazio.
Diferente de outros contextos onde o DataFrame pode ter seus valores
padrão instanciados como NaN, aqui temos um DataFrame realmente vazio
de elementos.
Usando do operador |, equivalente a or, podemos criar estruturas
condicionais compostas mais flexíveis, onde bastando uma das expressões
ser validada como verdadeira para que todo o bloco de código atrelado a
mesma seja executado.
Em cima do mesmo exemplo anterior, apenas alterando o operador de &
para | é possível notar que o conteúdo exibido em tela pela função print( )
agora é um DataFrame com dados distintos, apenas descartando destes
dados os dados validados como False, mantendo no DataFrame os dados
validados como True em uma das expressões condicionais.
No exemplo, o DataFrame retornado é um novo DataFrame sem os
elementos da coluna ‘a’ que forem menores que 0 e sem os elementos da
coluna ‘b’ que forem maiores que 1 de acordo com as respectivas condições
impostas anteriormente.
Atualizando um DataFrame de acordo com uma condicional

Para alguns contextos específicos podemos atualizar os dados de um


DataFrame usando de estruturas condicionais diretamente sobre a variável a
qual o DataFrame está associado.
Este processo contorna um problema visto anteriormente onde de acordo
com algumas estruturas condicionais um DataFrame tinha seus dados
totalmente apagados.
Usando de uma expressão condicional simples aplicada à uma variável,
temos a vantagem de gerar um novo DataFrame a partir desse processo, de
modo que onde houverem elementos os quais não forem validados pela
expressão condicional, seus espaços simplesmente serão preenchidos com
NaN.
Em nosso exemplo, criamos uma variável de nome data_positivos, que
recebe como atributo os elementos da variável data (nosso DataFrame) os
quais forem maiores que zero. Nesta etapa é como se definíssemos uma
regra a ser aplicada para atualizar nosso DataFrame original.
Em seguida, instanciando nosso DataFrame original via variável data,
podemos atualizar a mesma com data na posição [data_positivos], dessa
forma, a expressão condicional será aplicada a cada elemento da
composição de nosso DataFrame, retornando seu valor para sua posição
original quando o mesmo for validado como True, da mesma forma,
retornando NaN para elementos os quais forem validados como False de
acordo com a condicional.

INDEXAÇÃO DE DATAFRAMES

Manipulando o índice de um DataFrame

Em tópicos anteriores vimos que uma parte essencial de um DataFrame é


seu índice, e o mesmo pode ser manipulado de diversas formas de acordo
com o propósito da aplicação.
Para certos contextos específicos o índice de um DataFrame pode ser
alterado livremente assim como resetado para suas configurações originais.
Este processo também pode ser feito de diversas formas, sendo uma delas
fazendo o uso de uma função interna da biblioteca Pandas dedicada a este
propósito e com uma característica interessante, a de trazer novamente à
tona o índice interno de nosso DataFrame.
Atualizando nossa variável data (DataFrame), aplicando sobre seus próprios
dados o método reset_index( ), sem parâmetros mesmo, é gerado um índice
padrão, iniciado em 0 como todo índice em Python, transformando o índice
antigo em mais uma coluna de nosso DataFrame para que assim não se
altere a consistência dos dados.
Exibindo em tela via função print( ) o conteúdo da variável data antes das
modificações é possível ver seu índice personalizado.
Da mesma forma exibindo em tela via print( ) o conteúdo de data após
processamento do método reset_index( ), é possível notar que agora nosso
DataFrame possui um índice padrão, seguido de uma coluna de nome index
com o índice personalizado definido anteriormente, assim como todos os
demais dados dispostos em suas posições corretas.
Outra possibilidade interessante no que diz respeito a manipulação do
índice de um DataFrame é o fato de podermos atribuir como índice toda e
qualquer coluna já existente em nosso DataFrame. Este processo é feito por
meio do método set_index( ) por sua vez parametrizado com o nome de
cabeçalho da coluna a ser transformada em índice.
Em nosso exemplo inicialmente geramos um DataFrame via método
construtor de DataFrames pd.DataFrame( ), dando a ele algumas
características.
Em seguida é criada uma variável de nome coluna_V que recebe uma string
que terá seus elementos desmembrados através da função split( ).
Na sequência adicionamos uma nova coluna a nosso DataFrame
instanciando data na posição [‘Idv’] até então inexistente, que será gerada
com os dados oriundos de coluna_V.
Logo após essa alteração, instanciando novamente a variável data,
aplicando sob a mesma o método set_index( ), parametrizado com a string
‘Idv’, estamos definindo manualmente que a coluna de nome Idv passa a ser
o índice de nosso DataFrame.
Exibindo o antes e depois das alterações de nosso DataFrame por meio de
nossa função print( ), é possível ver que de fato a última forma de nosso
DataFrame é usando Idv como índice para o mesmo.

Índices multinível
Avançando nossos estudos com índices, não podemos deixar de ver sobre os
chamados índices multinível, pois para algumas aplicações será necessário
mapear e indexar nossos DataFrames de acordo com suas camadas de dados
ordenados.
Lembrando que tudo o que foi visto até então também é aplicável para os
DataFrames que estaremos usando na sequência. Todo material deste livro
foi estruturado de modo a usarmos de ferramentas e recursos de forma
progressiva, de forma que a cada tópico que avançamos estamos somente a
somar mais conteúdo a nossa bagagem de conhecimento.
Em teoria, assim como em uma planilha Excel temos diferentes guias, cada
uma com suas respectivas tabelas, a biblioteca Pandas incorporou a seu rol
de ferramentas funções que adicionavam suporte a estes tipos de estruturas
de dados em nossos DataFrames.
Para criar um índice multinível, para camadas de dados em nossos
DataFrames, devemos em primeiro lugar ter em mente quantas camadas
serão mapeadas, para que assim possamos gerar índices funcionais.
Partindo para a prática, inicialmente criamos uma variável de nome indice1,
que recebe como atributo uma lista com 6 elementos em formato de string.
Da mesma forma é criada outra variável de nome indice2 que por sua vez
possui como atributo uma lista composta de 6 elementos numéricos.
Aqui já podemos deduzir que tais listas serão transformadas em camadas de
nosso índice, para isso, antes mesmo de gerar nosso DataFrame devemos
associar tais listas de modo que seus elementos sejam equiparados de
acordo com algum critério.
Para nosso exemplo inicial, estaremos trabalhando com um índice de
apenas dois níveis, porém, a lógica será a mesma independentemente do
número de camadas a serem mapeadas para índice.
Se tratando então de apenas duas camadas, podemos associar os elementos
de nossas listas criando uma lista de tuplas. O método mais fácil para gerar
uma lista de tuplas é por meio da função zip( ). É então criada uma variável
de nome indice3 que recebe como parâmetro em forma de lista via
construtor de listas list( ) a junção dos elementos de indice1 e indice2 via
zip( ).
A partir desse ponto já podemos gerar o índice em questão. Para isso,
instanciando a variável indice3, chamando a função
pd.MultiIndex.from_tuples( ), por sua vez parametrizada com os dados da
própria variável indice3, internamente tal função usará de uma série de
métricas para criar as referencias para as devidas camadas de índice.
Importante salientar que, apenas para esse exemplo, estamos criando um
índice multinível a partir de uma tupla, porém, consultando a ajuda inline
de pd.MultiIndex é possível ver diversos modos de geração de índices a
partir de diferentes estrutura de dados.
Uma vez gerado nosso índice, podemos exibir seu conteúdo através da
função print( ), pois o mesmo é uma estrutura de dados independente, que
será atrelada a nosso DataFrame.
Na sequência criamos uma variável de nome data8 que via método
construtor de DataFrames gera um novo DataFrame de 6 linhas por 2
colunas, repassando para o parâmetro nomeado index o conteúdo da
variável indice3, nosso índice multinível criado anteriormente.
Exibindo o conteúdo de nossa variável data8 via função print( ), é possível
ver que de fato temos um DataFrame com seus dados distribuídos de modo
que suas linhas e colunas respeitam uma hierarquia de índice multinível.

Lembrando que uma boa prática de programação é prezarmos pela


legibilidade de nossos códigos. Sendo assim, uma vez que estamos
manipulando índices personalizados, podemos usar de nomes os quais
facilitam a interpretação.
Usando do mesmo exemplo anterior, apenas alterando os nomes usados
para o índice multinível de siglas para nomes usuais, tornamos nosso índice
mais legível.
Dessa forma podemos visualizar facilmente que, por exemplo, Nivel1
possui uma segunda camada de dados representada por Subnivel1,
Subnivel2 e Subnivel3. Posteriormente iremos entender a lógica de
manipulação dos dados do DataFrame por meio deste tipo de índice.

Conforme mencionado anteriormente, uma vez que podemos usar da


notação de manipulação de dados de lista em nossos DataFrames, podemos
facilmente instanciar determinados elementos do mesmo por meio de
métodos como loc[ ] entre outros.
Apenas como exemplo, repassando como parâmetro de nossa função print(
) data8, aplicando sobre a mesma o método loc[ ] por sua vez alimentado
com a string ‘Nivel2’, o retorno será todo e qualquer elemento que estiver
atrelado a esse nível.
Nesse caso, data8.loc[‘Nivel2’] retornará todos elementos de Subnivel1,
Subnivel2 e Subnivel3, independentemente de suas colunas.

Dentro da mesma lógica, para acessar um subnível específico podemos


simplesmente usar duas vezes loc[ ] (e quantas vezes fosse necessário de
acordo com o número de camadas / níveis).
Por exemplo, exibindo em tela de data8 as instâncias
.loc[‘Nivel2’].loc[‘Subnivel3’] é retornado uma Serie composta dos
elementos apenas deste nível.
Note que por se tratar da apresentação de uma Serie, linhas foram
convertidas para colunas, mas apenas para apresentação destes dados,
nenhuma alteração permanente ocorre neste processo.

Uma forma alternativa de acessar, tanto um nível quanto um subnível de um


índice multinível (independentemente do número de níveis) é usando do
método xs( ).
Aplicando tal método diretamente sobre uma variável a qual possua como
tipo de dado um DataFrame, basta parametrizar o método xs( ) com o nome
do nível / subnível para assim retornar seus dados.
Outra possibilidade ainda se tratando da organização e legibilidade de um
índice é atribuir para o mesmo nomes funcionais.
Usando do mesmo exemplo anterior, uma vez criado nosso DataFrame com
seu índice multinível, é possível aplicar o método index.names para a
variável a qual instancia nosso DataFrame, bastando passar para tal método
uma lista com os nomes funcionais que quisermos, desde que o número de
nomes funcionais seja compatível com o número de níveis / subníveis.
Exibindo via print( ) nosso DataFrame é possível ver que logo acima de
Nivel1 e de Subnivel1 existe um cabeçalho composto por NIVEIS e
SUBNIVEIS conforme esperado.
Encerrando este capítulo, uma última possibilidade a se explorar, embora
pouco utilizada na prática, é a de gerar um índice multinível por meio do
método pivot_table( ), onde podemos transformar dados do próprio
DataFrame como níveis e subníveis de um índice.
Para nosso exemplo, temos um DataFrame criado da maneira tradicional,
com seus dados já declarados em forma de dicionário, definindo assim
claramente quais serão as colunas (chaves do dicionário) e as linhas
(valores do dicionário).
A partir dessa estrutura podemos usar da função pivot_table( ), que por sua
vez exige a parametrização obrigatória para values, index e columns. Nesse
caso, como índice definimos que o mesmo será gerado a partir de ‘A’ e ‘B’,
que aqui se transformarão em nível e subnível, respectivamente. Do mesmo
modo para columns repassamos ‘C’, e como valores os elementos de ‘D’.
Dessa forma, embora o DataFrame mantenha seu índice interno ativo,
visível e funcional, as demais estruturas de dados estarão mapeadas para
index, columns e values de modo que podemos acessar seus elementos por
meio da notação padrão loc[ ] fazendo simples referência a seus nomes de
cabeçalho de coluna.
ENTRADA DE DADOS

Importando dados de um arquivo para um DataFrame

No que diz respeito à aplicação das ferramentas da biblioteca Pandas, algo


mais próximo da realidade é importarmos certas bases de dados para
realizar a manipulação e tratamento de seus dados. Essas bases de dados
podem ser das mais diversas origens, desde que seus dados estejam
dispostos em tabelas / matrizes, organizados em linhas e colunas.

A biblioteca Pandas possui uma série de ferramentas dedicadas à


importação de arquivos de várias origens, processando os mesmos por uma
série de mecanismos internos garantindo a integridade dos dados assim
como a compatibilidade para tratamento dos mesmos.
Dos tipos de arquivos mais comuns de serem importados para nossas
estruturas de código estão as planilhas Excel e seus equivalentes de outras
suítes Office.
Importar dados a partir de um arquivo Excel (extensão .csv) é bastante
simples, bastando usar do método correto, associando os dados importados
a uma variável. No próprio processo de importação tais dados serão
mapeados e convertidos para a estrutura de um DataFrame.
Em nosso exemplo, é criada uma variável de nome data, que por sua vez
instancia e inicializa a função pd.read_csv( ), parametrizando tal função
com o caminho de onde os dados serão importados em formato de string.
Lembrando que de acordo com o contexto o caminho a ser usado pode ser
relativo (ex: /pasta/arquivo.csv) ou absoluto (ex:
c:/Usuários/Fernando/Documentos/pasta/arquivo.csv).
Exibindo em tela o conteúdo da variável data por meio de nossa velha
conhecida função print( ), é nos exibido um DataFrame, nesse caso apenas
como exemplo, um DataSet muito conhecido de estudantes de ciências de
dados chamado mnist, composto de 9999 linhas e 785 colunas de dados.
Outras possíveis fontes para importação.

Realizando o processo inverso, exportando de nosso DataFrame para um


arquivo, também temos à disposição algumas ferramentas de exportação de
acordo com o tipo de dado.
Apenas como exemplo, diretamente sobre nossa variável data, aplicando o
método to_csv( ) repassando como parâmetro um nome de arquivo em
formato string e extensão .csv, um arquivo será devidamente gerado,
arquivo este com todas as propriedades de um arquivo Excel.
Outros possíveis formatos de exportação.

TRATAMENTO DE DADOS

Tratando dados ausentes / faltantes


Um dos principais problemas a se contornar em uma base de dados são seus
dados ausentes / faltantes, haja visto que independentemente da aplicação, a
integridade dos dados de base é de suma importância para que a partir dos
mesmos se faça ciência.
Para este e para os tópicos subsequentes, vamos partir de uma base de
dados comum criada para esses exemplos (embora tudo o que será visto
aqui se aplique a toda e qualquer base de dados a qual estaremos usando em
estrutura de DataFrame), com alguns elementos problemáticos a serem
tratados, para que de forma prática possamos entender o uso de algumas
ferramentas específicas para tratamento de dados.
Em nosso exemplo, inicialmente temos uma variável de nome data que
recebe como atributo um dicionário onde constam como chaves ‘Cidades’,
‘Estados’ e ‘Países’, sendo que como valores para estas chaves temos um
misto entre dados presentes e dados faltantes.
Em seguida geramos um DataFrame a partir destes dados como já feito
várias vezes anteriormente, usando do método pd.DataFrame( ).
Por fim, exibindo em tela o conteúdo de data via função print( ) é possível
ver nosso DataFrame, composto de 3 linhas e 3 colunas, onde temos NaN
representando a ausência de dados em alguns campos.
Uma das formas mais básicas que temos para remover dados de nosso
DataFrame associados a NaN é por meio da função dropna( ).
O método dropna( ) quando aplicado diretamente sobre uma variável, irá
varrer todo seu conteúdo, e se tratando de um DataFrame, a mesma
removerá todas as linhas e colunas onde houverem dados ausentes.
Repare que em nosso exemplo, em algumas posições das linhas 1 e 2
(número de índice 1 e 2) existiam dados faltantes, em função disso toda a
referente linha foi removida.
Raciocine que tanto em grandes quanto em pequenas bases de dados, o fato
de excluir linhas compostas de elementos válidos e de elementos faltantes
pode impactar negativamente a integridade dos dados. Supondo que uma
base de dados recuperada com muitos dados corrompidos seja tratada dessa
forma, os poucos dados válidos restantes serão removidos neste tipo de
tratamento, acarretando em perdas de dados importantes.

Uma forma de usar de dropna( ) de maneira menos agressiva em nossa base


de dados é definir um parâmetro thresh, onde você pode especificar um
número mínimo de elementos faltantes a ser considerado para que aí sim
toda aquela linha seja removida.
Em nosso exemplo, definindo thresh como 2 faria com que apenas as linhas
com 2 ou mais elementos faltantes fossem de fato removidas.

Apenas lembrando que todo e qualquer método aplicável a um contêiner de


dados pode ser usado para estas estruturas de dados perfeitamente.
O tratamento de um dado faltante poderia, por exemplo, substituir tal
elemento por outro via função replace( ), porém o que buscamos como
ferramenta são métodos os quais realizem o tratamento de todos os
elementos os quais por algum critério ou característica tenha de ser
alterado.
Um método melhor elaborado, próprio da biblioteca Pandas, em
substituição ao dropna( ) é o método fillna( ), onde por meio deste,
podemos tratar dados faltantes substituindo o marcador de dado faltante
Nan por algum dado/valor padrão, sendo assim uma alternativa muito mais
interessante do que a anterior.
Para nosso exemplo, mantendo exatamente a mesma base de dados anterior,
agora atualizamos nossa variável data, aplicando sobre a mesma o método
fillns( ) que recebe como parâmetro para value um dado/valor padrão, em
nosso caso, a string ‘Dados Ausentes’.
Através da função print( ) por sua vez parametrizada com a variável data, é
exibido em tela seu conteúdo. Nesse caso nosso DataFrame, onde é possível
ver que todos os elementos faltantes foram devidamente preenchidos com
Dados Ausentes.
Usando do método fillna( ) para tratamento de dados em um DataFrame
composto de dados numéricos, podemos usar de toda e qualquer operação
matemática sobre tais valores.
Apenas como exemplo, uma vez que temos um DataFrame associado a
variável data, composto de 3 linhas e 3 colunas, sendo o conteúdo das
linhas apenas dados numéricos (e nesse caso, dados faltantes), podemos
aplicar sobre tais dados alguma função matemática como, por exemplo,
preencher os campos faltantes com a média dos valores antecessores e
sucessores por meio do método mean( ).
Sendo assim, exibindo em tela nosso DataFrame inicial é possível constatar
os campos de elementos ausentes, assim como após a aplicação do método
mean( ) para as respectivas colunas, os espaços foram preenchidos com as
médias das notas, uma solução simples porém eficiente para tratar os dados
de origem sem alterar bruscamente a integridade dos dados já presentes
quando associados aos novos dados.

Outra possível solução, bastante empregada em bases de dados numéricas, é


o preenchimento de dados faltantes pela simples replicação do dado
antecessor, e isto é feito apenas usando de um parâmetro nomeado do
método fillna( ) chamado method, que quando definido como ‘ffill’ cumpre
esse papel.
No exemplo, atualizando nosso DataFrame atrelado a variável data,
aplicando sobre a mesma o método fillna( ) por sua vez parametrizado com
method = ‘ffill’, temos um simples tratamento onde os elementos
demarcados com NaN são substituídos pelos valores antecessores já
presentes.
Apenas salientando que quando dizemos antecessores, no contexto de um
DataFrame estamos falando que daquela Serie (daquela coluna) o elemento
antecessor é o que está imediatamente acima do elemento atual.

Podemos também realizar a verificação de elementos ausentes em uma base


de dados a qual não conhecemos seus detalhes por meio da função isnull( ).
A função isnull( ) é uma função embutida do Python justamente para
verificar nos dados de qualquer tipo de contêiner de dados, se existem
elementos faltantes em sua composição. Por padrão, a função isnull( )
quando aplicada a uma variável qualquer, retornará True quando encontrar
algum campo com elemento ausente, e False para todos os elementos
regulares.
Apenas para fins de exemplo, carregando novamente a base de dados mnist,
atribuindo a mesma para uma variável de nome data, e aplicando sobre data
o método isnull( ), é retornado um Dataframe com todos os elementos dessa
base de dados, demarcados com True e False conforme cada elemento da
mesma é verificado e validado.
Agrupando dados de acordo com seu tipo

Levando em consideração a maneira como os dados em um DataFrame são


mapeados, existem diversas possibilidades de manipulação dos mesmos
conforme vimos ao longo dos capítulos.
Uma das abordagens muito úteis se tratando de dados é os agrupar
conforme algum critério para que assim apliquemos alguns filtros ou
funções.
Uma das formas de se trabalhar com agrupamentos personalizados de dados
via biblioteca Pandas é usando de sua ferramenta GroupBy.

Para esses exemplos, vamos fazer uso de um novo DataFrame, dessa vez
criado a partir de um dicionário composto de três chaves, ‘Vendedor’,
‘Item’ e ‘Valor’, com suas respectivas listas de elementos no lugar de seus
valores de dicionário.
Nos mesmos moldes dos exemplos anteriores, a partir desse dicionário
geramos um DataFrame e a partir deste ponto podemos nos focar nos
assuntos deste tópico.
Exibindo em tela via print( ) o conteúdo de data, nos é retornado um
DataFrame com seus respectivos dados organizados por categoria,
distribuídos em 3 colunas e 6 linhas.

Partindo para o entendimento da lógica de agrupamento, uma vez que


temos nosso DataFrame, vimos anteriormente alguns métodos para
localização de colunas, linhas e elementos específicos destes campos para
uso dos mesmos.
Uma forma mais elaborada de se trabalhar com esses dados é a partir do
agrupamento dos mesmos de acordo com algum critério definido, como
alguma característica relevante, assim, podemos extrair informações
interessantes a partir dessas manipulações.
Para exemplo, é criada uma variável de nome itens, que receberá apenas os
dados da “categoria” itens de nossa base de dados. Para isso, itens instancia
e inicializa para data o método groupby( ), parametrizando o mesmo com
‘Item’, nome de cabeçalho de coluna de nosso DataFrame.
A partir disso, a função irá gerar dados pelo parâmetro de agrupamento
definido.
Através da função print( ), exibindo em tela o conteúdo da variável itens,
nos é retornado apenas a referência para o objeto criado com os dados
filtrados, assim como seu identificador de objeto alocado em memória.
A partir do momento que temos uma variável com dados agrupados a partir
de nosso DataFrame, podemos aplicar funções sobre tais dados para que aí
sim tenhamos retornos relevantes.
Diretamente via print( ), usando como parâmetro itens.sum( ) podemos
obter como retorno a soma dos valores numéricos associados aos dados
agrupados.
De fato, como retorno nos é exibido em tela cada tipo de item com seu
valor somado, como era o retorno esperado pois sobre tais dados aplicamos
o método embutido do Python para soma sum( ).
Isto ocorre pois dos dados agrupados, somente é possível somar valores
numéricos, haja visto que o método sum( ) não realiza concatenação, e
atuando sobre uma Serie ou DataFrame ignora qualquer coluna de texto.

Podemos também usar de outros métodos já conhecidos, como por exemplo


o método mean( ) que retornará a média dos valores, aqui, respeitando o
agrupamento de cada tipo de item.
Usando do próprio método embutido do sistema contador count( ), é
retornado um novo DataFrame representando de acordo com cada tipo de
item sua quantidade agrupada.

Mesmo um objeto gerado como agrupador de certos tipos de dados de um


DataFrame pode possuir algumas características descritivas geradas pelo
Pandas.
Aplicando sobre a variável itens o método describe( ) nos é retornado de
acordo com o agrupamento dos itens alguns dados baseados em seus
valores numéricos, que aqui podemos contextualizar como média de preço,
menor preço, 25%, 50% e 75% do preço, maior preço, etc...
Por fim, agrupando itens de acordo com características textuais que podem
ser, por exemplo, nomes de vendedores em uma base de dados, podemos
combinar outros métodos explorados anteriormente para, a partir de um
agrupamento, obter novas informações.
Como exemplo, declarada uma variável de nome vendedores que recebe o
agrupamento de acordo com o critério ‘Vendedor’ de nosso DataFrame via
groupby( ), podemos combinar alguns métodos para extrair dados
relevantes.
Por exemplo, diretamente como parâmetro de nossa função print( ),
aplicando sobre a variável vendedores o método sum( ) para a posição
[‘Ana’], iremos obter como retorno exibido em tela apenas os valores das
vendas atribuídos a Ana.
Novamente, cada uma das possibilidades exploradas nos tópicos anteriores
se aplica aqui também, apenas não replicaremos os mesmos métodos por
uma questão de não tornar o conteúdo deste livro muito repetitivo. Realize
testes sobre seus dados, praticando os meios e métodos explicados até o
momento para sua base de dados atual.

MÉTODOS APLICADOS

Alterando o formato de um DataFrame


Uma das particularidades de um DataFrame é sua forma, haja visto que uma
das características principais deste tipo de estrutura de dados é justamente a
distribuição de seus elementos em formato de tabela / matriz de modo que
temos linhas e colunas bem estabelecidas.
Se tratando do formato de um DataFrame, sua estrutura é mapeada de modo
que o interpretador busca suas referências por meio de índices ordenados,
em função disso, devemos tomar muito cuidado sempre que formos realizar
algum tipo de manipulação que acarrete em alteração do formato do
DataFrame.
Em outras palavras, até podemos alterar o formato estrutural de um
DataFrame quando necessário, porém, certas regras referentes a sua
proporção e distribuição de seus dados devem ser respeitadas a fim de
evitar exceções.
Partindo para prática, como exemplo usamos novamente de um simples
DataFrame gerado via construtor pd.DataFrame( ), dimensionado em 6
linhas e 5 colunas, com números aleatórios gerados via np.random.randn( ).
Exibindo o conteúdo de nosso DataFrame, repassando a variável data como
parâmetro para print( ), é possível identificar seu formato original de
distribuição de elementos.
Atualizando a variável data, aplicando sobre a mesma o método pd.melt( ),
estamos transformando de forma proporcional linhas em colunas e vice-
versa.
Em nossa segunda função print( ), exibindo o conteúdo de data como feito
anteriormente, agora temos um novo DataFrame composto de 29 linhas e 2
colunas, além é claro, de um novo índice gerado para este novo DataFrame.

Concatenando dados de dois DataFrames

Anteriormente vimos que é possível aplicar toda e qualquer operação


aritmética sobre os dados numéricos de um DataFrame. Também foi dito
que desses operadores aritmético a função sum( ) em um DataFrame ignora
completamente dados textuais, aplicando suas métricas apenas nos dados
numéricos presentes.
Explorando as funcionalidades da biblioteca Pandas podemos ver que
contornando essa situação temos o método concat( ), que irá concatenar /
somar os elementos de um DataFrame sem nenhuma distinção e sem
sobrepor nenhum elemento.
Pela lógica de agregação de dados do método pd.concat( ), concatenando
elementos de dois ou mais DataFrames, usando da indexação correta,
podemos fazer com que os mesmos sejam enfileirados de modo a formar
uma só base de dados ao final do processo.
Para realizar a concatenação da maneira correta, vamos ao exemplo: Nesse
caso, temos de início dois dicionários de nome loja1 e loja2,
respectivamente.
Cada um desses dicionários possui em sua composição três chaves onde
cada uma delas recebe uma lista de elementos como valores.
Em seguida criamos uma variável de nome data1 que instancia e inicializa a
função pd.DataFrame( ), em justaposição repassando como dados o
conteúdo da variável loja1, seguido do parâmetro nomeado index que
recebe uma lista composta de 6 elementos numéricos ordenados de 1 até 6.
Exatamente da mesma forma, data2 é criada e para a mesma é gerado um
DataFrame a partir de loja2, via função pd.DataFrame, também definindo
especificamente um índice por meio de uma lista de números entre 7 a 12.
Aqui temos uma característica importante, note que estamos criando índices
para nossos DataFrames data1 e data2 que em nenhum momento se
sobrepõem.

Uma vez criados os DataFrames com índices únicos entre si, podemos dar
prosseguimento com o processo de concatenação.
Seguindo com nosso exemplo, é criada uma variável de nome lojas, que
usando do método pd.concat( ) parametrizado com data1 e data2 em forma
de lista, recebe os dados agrupados e remapeados internamente pela função
concat( ).
Exibindo em tela o conteúdo de lojas, é possível ver que de fato agora
temos um novo DataFrame, composto pelos dados dos DataFrames
originais, mantendo inclusive sua indexação pré definida.
Reaproveitando a linha de exercício do tópico anterior, onde falávamos
sobre o formato de um DataFrame, podemos associar tal conceito no
processo de concatenação.
Como dito anteriormente, em casos onde um DataFrame é remodelado,
métricas internas são aplicadas para que o mesmo não perca sua proporção
original.
Em nosso exemplo, usando do método pd.concat( ), definindo para o
mesmo o valor 1 em seu parâmetro nomeado axis, estamos a realizar a
concatenação dos DataFrames data1 e data2 usando como referência suas
colunas ao invés das linhas.
Importante observar que, nesse caso, tendo desproporcionalidade do
número de elementos em linhas e colunas, ao invés do método concat( )
gerar alguma exceção, ele contorna automaticamente o problema
preenchendo os espaços inválidos com NaN, mantendo assim a proporção
esperada para o novo DataFrame.
Apenas realizando um pequeno adendo, como observado em tópicos
anteriores, NaN é um marcador para um campo onde não existem
elementos (numéricos), sendo assim necessário tratar esses dados faltantes.
Mesclando dados de dois DataFrames

Diferentemente do processo realizado no tópico anterior, para alguns


contextos específicos pode ser necessário realizar não somente a
concatenação de elementos e dois ou mais DataFrames, mas a mescla dos
mesmos. Entenda por hora mescla como uma concatenação onde é
permitido a união de dois elementos, sobrepondo textos e somando valores
quando necessário.
Entendendo tais conceitos diretamente por meio de nosso exemplo, aqui
usaremos de dois DataFrames gerados a partir de dicionários e ordenados
de modo que a concatenação de seus dados não sobrepõe elementos graças
aos índices predefinidos.

Ao tentar aplicar o método pd.merge( ) sobre nosso DataFrame do mesmo


modo como feito via pd.concat( ), é gerada uma situação inesperada.
Repare que aplicando o método pd.merge( ) por sua vez parametrizado com
data1 e data2, atribuindo essa possível mescla de dados a variável lojas2, ao
tentar exibir seu conteúdo via função print( ) é retornado um DataFrame
vazio por parte de elementos.
Segundo o retorno, este DataFrame inclusive possui 3 colunas e um índice
vazio, e isto ocorre pois o mecanismo de mescla de dados a partir de
DataFrames espera que seja especificado que métrica será considerada para
permitir a mescla.

Contornando esse problema, em nossa função pd.merge( ), atribuindo regras


aos parâmetros nomeados how e on, definimos em how o método de
aproximação, seguido de ‘Item’ para o parâmetro on, estipulando assim que
serão mesclados as colunas dos DataFrames com base nos vendedores que
realizaram vendas sobre o mesmo tipo de item.
Repare que como retorno desse exemplo temos referências a vendedores de
data1 como Vendedor_x, valores de data1 como Valor_x, vendedores de
data2 como Vendedor_y e valores de data2 como Valor_y, mesclados em
função de todos compartilhar do item Camisa Nike.
Outros vendedores os quais não realizavam interações nos dois DataFrames
para o mesmo item simplesmente foram descartados.
Alterando o parâmetro how da função pd.merge( ) para ‘outer’, permitimos
que outros vendedores entrem para o rol do DataFrame, mesmo sua
relevância sendo baixa em função da baixa participação de interação com os
elementos em destaque / elementos mais usados no contexto.

Repassando como argumento para o parâmetro on de pd.merge( ) uma lista


contendo ‘Item’ e ‘Valor’, será filtrado e retornado apenas o item onde os
vendedores conseguiram o vender pelo maior valor possível.
Note que aqui um novo DataFrame é apresentado, contendo apenas Maria e
Carlos, vendedores os quais conseguiram vender o item Camisa Nike pelo
maior valor 115,9.
Agregando dados de um DataFrame em outro DataFrame
Explorando outra possibilidade, semelhante ao processo de concatenação de
elementos de dois ou mais DataFrames, podemos também unir dados de
dois DataFrames definindo um como referência para que os dados do outro
apenas sejam agregados de acordo com algum critério definido.
O processo de agregação é feito por meio da função join( ), que por padrão
usa como critério unir dados de duas bases de dados os quais compartilham
de mesmos números de índice.
Para nosso exemplo, novamente reutilizamos do exemplo anterior, apenas
alterando os valores de índice para o DataFrame atribuído a data2.
Exibindo em tela por meio da função print( ) os conteúdos de data1 e de
data2 podemos ver, como esperado, os DataFrames com seus respectivos
índices personalizados e elementos organizados em linhas e colunas.

Na sequência é criada uma variável de nome data3 que chama a função


.join( ) sobre data1, recebendo como seu conteúdo elementos oriundos de
data2 que podem ser mesclados com data1.
Em outras palavras, aqui estamos agregando os dados do DataFrame em
data2 aos dados do DataFrame em data1 que é o referência, de modo que
estaremos agregando os elementos que em seus respectivos DataFrames
possuíam o mesmo número de índice.
Novamente usando print( ), podemos ver o conteúdo de data3 que de fato é
retornado um DataFrame composto de 6 linhas, onde nas mesmas constam
apenas os elementos de data1 equivalentes em número de índice com os
elementos de data2. Onde tal equivalência não ocorre, os campos
simplesmente são preenchidos com NaN para fins de manter a proporção
funcional do DataFrame.

Simulando uma exceção, alterando os índices de modo que não exista


nenhuma sobreposição, usando do método join( ) tentando unir os dados de
data2 aos de data1, atribuindo este resultado a data3, exibindo em tela data3
é possível notar que tal DataFrame é gerado com os dados de data1,
seguidos de uma série de NaN para os campos onde deveriam constar os
dados de data2, ausentes por conta de não haver equivalência de índices.
Simulando outra situação, onde os índices são exatamente iguais, ao usar da
função join( ) serão feitas todas as agregações dos elementos de índices
iguais a partir dos DataFrames de origem, gerando um novo DataFrame
composto de tais dados agregados mantendo o formato original do
DataFrame.

Filtrando elementos únicos de uma Serie de um DataFrame

Complementar aos métodos de filtragem de dados vistos em tópicos


anteriores específicos, podemos avançar um pouco mais no que diz respeito
a métodos específicos para extração destes tipos de dados.
Mantendo o mesmo DataFrame usado como exemplo para o tópico anterior,
temos tal estrutura de dados atribuída a nossa variável de nome loja1,
podendo assim realizar mais alguns testes sobre a mesma.
Partindo para filtragem específica, podemos por exemplo, via método
unique( ) filtrar um ou mais elementos únicos de um DataFrame, algo
bastante útil para certos contextos.
Sendo assim, diretamente via função print( ) parametrizando a mesma com
loja1 na posição [‘Item1’] aplicando o método unique( ), sem parâmetros
mesmo, nos é retornado uma lista composta apenas dos elementos os quais
não se repetem em nenhum momento em nossa base de dados.
Complementar a este método, poderíamos verificar o número de elementos
únicos via função len( ), porém a biblioteca Panads oferece uma função
específica para este propósito. Por meio da função nunique( ) nos é
retornado o número de elementos únicos de nosso DataFrame.
Como mencionado em tópicos anteriores, a estrutura base de um DataFrame
costuma ser dados oriundos de tabelas, onde sua estrutura de linhas e
colunas já é bem estabelecida, ou a partir de dicionários Python, pois a
partir deste tipo de dado chaves do dicionário se tornam colunas, assim
como valores deste dicionário são convertidos para linhas de nosso
DataFrame.
O fato é que, uma vez que temos dados dispostos nesse formato, podemos
aplicar métodos comuns a dicionários para obter o mesmo tipo de retorno.
Apenas como exemplo, de nosso DataFrame associado a loja1, se
aplicarmos sobre a posição [‘Item1’] o método value_counts( ) nos será
retornado uma lista com os elementos de nosso DataFrame ordenados pelo
de maior ocorrência para o de menor ocorrência.
Encerrando nossa linha de raciocínio, repare que o retorno gerado para
nosso pequeno DataFrame é uma lista onde Camisa Nike aparece em
primeiro lugar pois tal elemento coexiste em 3 campos diferentes de nossa
base de dados, seguido dos elementos de menor relevância de acordo com
este parâmetro.

Processamento em paralelo de um Dataframe

Como bem sabemos, se tratando de um Dataframe estamos a


trabalhar sobre uma estrutura de dados da biblioteca Pandas a qual podemos
inserir e manipular dados de forma mais robusta se com parado a estruturas
de dados matriciais básicas.
Dataframes por sua vez possuem sistema de indexação próprio para
seus dados e suporte a todo e qualquer tipo de dado em Python (inclusive
funções aplicadas aos dados), tornando esta estrutura de dados uma das
mais relevantes no meio científico graças as suas particularidades no que
diz respeito a simplicidade de uso e performance de processamento.
Nesta linha de raciocínio, é comum que para certos projetos
tenhamos bases de dados enormes moldadas em Dataframes, e uma boa
prática é trabalhar tais dados de maneira eficiente. Neste processo quase
sempre temos etapas de tratamento dos dados, removendo dados
desnecessários ou inválidos, posteriormente definindo métricas de leitura e
processamento desses dados por meio de funções.
Uma possibilidade que é um verdadeiro divisor de águas quando
estamos a falar sobre o processamento de grandes volumes de dados é
realizar tal feito através de técnicas de paralelismo.
Relembrando rapidamente, sempre que estamos falando em
paralelismo estamos a contextualizar a execução de uma determinada
função, dividindo a mesma em partes para processamento individual, o que
normalmente acarreta em um considerável ganho de performance (ou por
outra perspectiva, redução de tempo de processamento).
Se tratando de Dataframes, é perfeitamente possível dividir o
conteúdo de um Dataframe para processamento em partes individuais,
sejam em clusters ou simplesmente a partir de técnicas de uso de núcleos
individuais de um determinado processador. O ponto é que, justamente em
casos de processamento de grandes volumes de dados, nesse caso dados
estes originários a partir de um Dataframe, o processamento em paralelo
pode ser de grande valia para otimização de uma aplicação.
Partindo para a prática, vamos buscar entender o processo de
repartição dos dados de um Dataframe assim como seu processamento,
mensurando para fins de comparação o tempo de processamento via método
convencional e via paralelismo.
Como de costume, todo processo se inicia com as importações das
devidas bibliotecas, módulos e pacotes os quais faremos uso ao longo de
nosso código.
Nesse caso, importamos inteiramente as bibliotecas Pandas e
Numpy que nos serão úteis para a leitura e manipulação dos dados, por fim
da biblioteca multiprocessing importamos a ferramenta Pool, que nos
possibilitará definir a execução de uma função sobre nossos dados em
paralelismo.

Para nosso exemplo, e apenas para fins de exemplo, vamos usar de


uma base de dados chamada new_york_hotels que como nome sugere traz
um catálogo dos hotéis situados em Nova Iorque, catálogo este bastante
detalhado por sinal, contendo dados como nome do hotel, endereço, cidade,
código postal, latitude e longitude, entre outras informações.
O processamento que realizaremos sobre esta base de dados será o
cálculo de distância entre dois pontos com base em seus valores de latitude
e longitude. Não iremos nos ater a explicar detalhadamente a função que
realiza este cálculo via fórmula de Haversine, pois nosso foco será o tempo
de processamento desta função.
Retomando nossa linha de raciocínio, de volta ao código,
inicialmente declaramos uma variável de nome base que recebe como
atributo o caminho para o arquivo de nossa base de dados em forma de
string.
Na sequência é criada uma nova variável, dessa vez de nome df,
que instancia e inicializa o método pd.read_csv( ) da biblioteca Pandas, por
sua vez parametrizado em justaposição com o caminho do arquivo (nesse
caso a instância da variável que guarda essa informação) seguido do
parâmetro nomeado encoding, aqui definido como “ISSO-8859-1”. Isto se
faz necessário por questões de compatibilidade, pois o arquivo original
desta base de dados encontra-se codificado em formato de script.
Apenas exemplificando o formato original da base de dados.

Aplicando o método head( ) sobre a variável df, por sua vez


parametrizado com o número de linhas as quais queremos inspecionar, nos
é retornado o cabeçalho da base de dados.
Agora sim temos a base de dados em um formato familiar,
composta por nomes de colunas antes da primeira linha, índice automático à
esquerda, e os dados organizados em seus devidos campos como em uma
simples matriz.

*base de dados disponível em:


https://raw.githubusercontent.com/rajeevratan84/datascienceforbusiness/ma
ster/new_york_hotels.csv

Logo em seguida é criada a função que aplica a fórmula de


Haversine, que por sua vez recebe como ponto de partida as coordenadas de
latitude e longitude do ponto de origem, seguido das mesmas coordenadas
do ponto de destino, e de um valor fixo pelo qual via triangulação destes
pontos irá calcular a distância entre origem e destino. Embora esta fórmula
seja de fato um tanto quanto complexa, sua correta aplicação retornará a
distância entre dois pontos considerando a curvatura da terra, para assim
produzir dados relevantes para navegação (tanto aérea quanto aquática).
A nível de código vale destacar alguns pontos: Primeiramente são
declaradas 4 variáveis de nomes lat1, lon1, lat2 e lon2 que recebem como
atributo os valores 40.671, -73.985, o valor de cada campo ‘latitude’ e
‘longitude’ de nossa base de dados, respectivamente.
Em seguida é declarada uma constante (embora não existam de fato
constantes em Python, mas adotamos tal sintaxe apenas por convenção) de
nome MILES, que será o valor de um ponto de referência para triangulação.
Na sequência instanciamos novamente as variáveis lat1, lon1, lat2,
lon2, dessa vez aplicando sobre as mesmas o método np.deg2rad( ), aqui
convertendo internamente os valores de graus para radianos. Importante
salientar que aqui usamos do método np.deg2rad( ) aninhado ao método
map( ) para que assim tal método seja aplicado individualmente sobre cada
variável. Este procedimento poderia ser feito de diversas outras formas,
porém, a mais reduzida por hora é via map(np.deg2rad( )).
Também são criadas duas novas variáveis, dessa vez de nomes dlat
e dlon que recebem como atributo o retorno da subtração das latitudes e
longitudes envolvidas no processo anterior.
São criadas duas outras variáveis de nomes a e c, que em suas
respectivas linhas de código usam de diversos métodos para cruzar os dados
produzidos anteriormente. Dentre estes métodos vale destacar np.sin( ) para
cálculo do seno, np.cos( ) para cálculo de cosseno, np.arcsin( ) para seno
invertido, por fim np.sqrt( ) para cálculo da raiz quadrada de um
determinado número.
Finalizando, é criada uma variável de nome total_miles, que por
sua vez recebe como atributo o retorno da multiplicação do último valor
atribuído para as variáveis MILES e c.
O Dataframe df é instanciado novamente, mais especificamente seu
campo ‘distance’, atualizando seu valor com o dado/valor atribuído a
total_miles.
Por fim, é retornado o Dataframe após atualizado.

Uma vez que temos os dados carregados e nossa função para testes
definida, podemos finalmente partir para a parte que nos interessa neste
tópico, o tempo de processamento dos dados de nosso Dataframe.
A nível de código, em meu caso executando via Colab, inserindo o
prefixo %%time da célula será gerado um retorno referente ao tempo de
execução da mesma.
Nesta célula, neste bloco de código em particular, declaramos uma
variável de noma distance_df que instancia e inicializa o método haversine(
) por sua vez parametrizado com o conteúdo da variável df.
Executando esta célula o retorno referente ao método haversine( )
executado de forma convencional será:

The slowest run took 23.12 times longer than the fastest. This could mean that an intermediate
result is being cached.
1000 loops, best of 5: 210 µs per loop

Em tradução livre:
A execução mais lenta demorou 23,12 vezes mais que a mais rápida. Isso pode significar que
um resultado intermediário está sendo armazenado em cache.
1000 loops, melhor de 5: 210 µs por loop

Como explicado no retorno, o melhor tempo de execução obtido foi


de 210 microssegundos, ou em conversão direta, 0,21 milissegundos.

Haja visto que já temos um dado / parâmetro referente ao tempo de


execução de forma convencional, podemos partir para a elaboração do
método que utilizará de paralelismo.
Nesse caso, criamos uma nova função, dessa vez de nome
paralelize_dataframe( ), que receberá obrigatoriamente uma base de dados,
uma função a ser aplicada e um número de núcleos de processados
manualmente definido.
Indentado ao corpo da função, inicialmente declaramos uma
variável de nome df_split, que instancia e inicializa o método
np.array_split( ), aqui parametrizado com a base de dados de origem e o
número de partições a serem geradas, nesse caso, 8 referente a 8 núcleos de
processamento.
Também é declarada uma variável de nome pool, que chama a
função Pool( ) também parametrizada com um número de núcleos.
Lembrando que a ferramenta Pool( ) por sua vez de forma simples e
objetiva cria a instância para processamento em paralelo de funções, tudo o
que estiver hierarquicamente sob sua instância será processado usando de
métricas internas de paralelismo.
É então instanciada a variável df, referente a nossa base de dados,
aplicando sobre a mesma o método pd.concat( ), parametrizado por sua vez
com o método map( ) aplicado a variável pool, por fim parametrizado com
a função a ser aplicada e neste caso, as partições da base de dados de
origem.
Encerrando o processamento via Pool( ), são instanciadas e
inicializadas as funções close( ) e join( ), por fim, retornando nosso
Dataframe após atualizado.

Em uma nova célula, instanciamos novamente a variável


distance_df, dessa vez a partir da mesma fazendo uso da função
paralelize_dataframe( ), aqui parametrizada com nosso Dataframe e a
função a ser aplicada sobre cada partição sua.
Executando esse bloco de código, onde a função haversine( ) é
aplicada fazendo uso de métricas internas de paralelismo para o
processamento de nossos dados, o retorno será:

10 loops, best of 5: 210 µs per loop


Em conversão direta, 0,014 milissegundos, uma notável redução de
tempo de processamento se comparado ao método convencional (210 µs /
0.21 ms).

CONSIDERAÇÕES FINAIS

E assim concluímos este pequeno compêndio introdutório sobre os


aspectos práticos mais rotineiros se tratando da manipulação de dados
através das bibliotecas Pandas e Numpy com suas ferramentas.
Espero que estas páginas tenham sido de prazerosa leitura assim como para
mim as foi escrever linha por linha.
Mais importante que isso, espero que de fato este pequeno livro
tenha lhe ajudado em seu processo de aprendizagem em mais um tópico
dentro das quase infinitas possibilidades que temos quando programamos
em Python, e que ao final desta leitura você consiga dar seus primeiros
passos no tratamento de dados.

Muito obrigado por adquirir esta obra, sucesso em sua jornada, um forte
abraço... Fernando Feltrin.

Você também pode gostar