Feltrin, Fernando - Tratamento de Dados Com Python + Pandas - 2021
Feltrin, Fernando - Tratamento de Dados Com Python + Pandas - 2021
Fernando Feltrin
AVISOS
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
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
BIBLIOTECA 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.
- 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.
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
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, 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.
Visualizando o cabeçalho
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( ).
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.
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.
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.
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.
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.
INDEXAÇÃO DE DATAFRAMES
Í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 referências 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 estruturas 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.
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.
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 aos
assuntos desse 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.
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.
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 predefinida.
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
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
Muito obrigado por adquirir esta obra, sucesso em sua jornada, um forte
abraço... Fernando Feltrin.