8  Gestão e análise de dados

8.1 Introdução

Até agora trabalhamos principalmente com objetos isolados: números, strings, listas e arrays. Em aplicações empíricas reais, porém, os dados normalmente aparecem organizados em tabelas, nas quais cada linha representa uma observação e cada coluna representa uma variável. O pacote Pandas foi criado justamente para facilitar a manipulação desse tipo de estrutura. Ele fornece ferramentas para importar, filtrar, transformar, combinar e resumir bases de dados de forma eficiente.

Em economia aplicada, praticamente todo trabalho empírico utiliza operações desse tipo: selecionar municípios, combinar bases administrativas, calcular médias por grupo, tratar valores ausentes e reorganizar dados antes de estimar modelos econométricos.

Figura 8.1: Setup usual de um computador pessoal no começo dos anos 2000. Fonte: Wikimedia Commons

8.1.1 Pré-requisitos

O foco deste capítulo será o pacote Pandas, que, apesar de já vir instalado com o Anaconda, não é nativo do Python e, portanto, é preciso carregá-lo. Além disso, por conta de suas estrutura e lógica de operações elemento a elemento, utilizaremos também o NumPy. O pacote os serve ao propósito de gerir local de trabalho e diretórios.

  • numpy
  • pandas
  • os
import numpy as np
import pandas as pd
import os

8.2 O que é o Pandas?

Pandas é uma biblioteca que contém objetos específicos e ferramentas próprias a esses objetos que permitem realizar limpeza, organização e análise de dados de forma bastante eficiente no Python. Não à toa, é uma das bibliotecas mais utilizadas na linguagem hoje em dia, um pré-requisito para a maior parte das aplicações de ciência dos dados e aprendizado de máquina.

Embora o Pandas se utilize em grande medida da estrutura e lógica do NumPy e seus arrays, a biblioteca é projetada para trabalhar com dados tabulares e heterogêneos. Como já vimos, o NumPy é mais adequado para trabalhar com dados homogêneos e em sua maior parte numéricos. Se por um lado o NumPy tem como objetos principal o ndarray, são dois os objetos particulares ao Pandas: Series e DataFrame.

Você pode pensar em uma série como uma coluna de dados, como uma sequência de observações em uma única variável. Por outro lado, um DataFrame é um objeto bidimensional para armazenar colunas de dados relacionadas, assim como uma tabela do Microsoft Excel, com linhas que identificam a qual entidade as informações das colunas se referem. A Figura Figura 8.2 ilustra essa diferença.

Figura 8.2: Ilustração dos objetos do Pandas em uma mesma base de dados

8.3 Objetos no Pandas

Existem várias formas de criar esses objetos no Pandas. A primeira forma é faze-lo manualmente, através de listas e dicionários que armazenem os dados. Vamos começar criando duas listas: uma contendo o número de pessoas ocupadas (em mil pessoas) no Brasil de 2012 a 2021 e outra contendo exatamente o intervalo de anos.

pessoas_ocupadas = [90593,92170,92962,92366,90174,92228,93534,95515,87225,95747]
anos = list(range(2012,2021+1))

print(pessoas_ocupadas)
print(anos)
[90593, 92170, 92962, 92366, 90174, 92228, 93534, 95515, 87225, 95747]
[2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021]

Para criar uma série basta utilizar a função pandas.Series, que recebe como argumento os valores da série e os índices da série, que serão justamente os valores que identificam cada linha.

series_pessoas_ocup = pd.Series(data=pessoas_ocupadas,index=anos)

print(type(series_pessoas_ocup),'\n')
print(series_pessoas_ocup)
<class 'pandas.core.series.Series'> 

2012    90593
2013    92170
2014    92962
2015    92366
2016    90174
2017    92228
2018    93534
2019    95515
2020    87225
2021    95747
dtype: int64

Já para o caso de um DataFrame, podemos criá-lo através da função pandas.DataFrame. Essa função recebe os dados que irão compor as colunas de dados de diversas formas, mas umas das mais intuitivas é através da utilização de dicionários. Seguindo o exemplo anterior, vamos criar um DataFrame com as informações de pessoas ocupadas, desocupadas, dentro e fora da força de trabalho. Primeiro passo é definir o dicionário com esses dados.

dados = {
    'ocupadas': [90593,92170,92962,92366,90174,92228,93534,95515,87225,95747],
    'desocupadas': [6730,6151,6555,9222,12476,12453,12413,11903,14412,12011],
    'na forca': [97322,98321,99516,101588,102650,104682,105947,107418,101637,107758],
    'fora da forca': [58007,59244,60162,60092,60953,60777,61299,61579,69042,64525]
}

print(dados)
{'ocupadas': [90593, 92170, 92962, 92366, 90174, 92228, 93534, 95515, 87225, 95747], 'desocupadas': [6730, 6151, 6555, 9222, 12476, 12453, 12413, 11903, 14412, 12011], 'na forca': [97322, 98321, 99516, 101588, 102650, 104682, 105947, 107418, 101637, 107758], 'fora da forca': [58007, 59244, 60162, 60092, 60953, 60777, 61299, 61579, 69042, 64525]}

Agora basta definir o DataFrame usando como argumento esse dicionários dados.

dados_emprego = pd.DataFrame(data=dados,index=anos)

print(type(dados_emprego),'\n')
dados_emprego.head()
<class 'pandas.core.frame.DataFrame'> 
ocupadas desocupadas na forca fora da forca
2012 90593 6730 97322 58007
2013 92170 6151 98321 59244
2014 92962 6555 99516 60162
2015 92366 9222 101588 60092
2016 90174 12476 102650 60953

8.4 Importação de dados externos

Um dos principais pontos fortes do Pandas é justamente a capacidade de importar e exportar informações nos mais diversos formatos: planilhas de Microsoft Excel (.xlsx), arquivos de texto separado por vírgulas (.csv), etc. Mais do que trabalhar com esses arquivos, o Pandas o faz de maneira eficiente e em poucas linhas de código. Funções como read_csv e to_csv, por exemplo, são essenciais para o conjunto de ferramentas do cientista de dados em Python.

A ideia daqui para frente é trabalhar com os dados da Penn World Table version 11.0, base de dados com informação sobre níveis de renda, produto e produtividade de mais de 180 países durante o período de 1950 a 2023. Esses dados são mantidos pela Universidade de Groningen e atualizados periodicamente. O arquivo específico com o qual trabalharemos está disponível no Moodle e deve ser baixado para a sua máquina local.

Mas antes de aprendermos como trabalhar com um dataframe e as funcionalidades que o Pandas nos traz precisamos ler o arquivo com os nossos dados. Comecemos pela função os.chdir do pacote os para mudar o diretório base do Python para o diretório no qual o nosso arquivo se encontra:

# Qual o diretório no qual o Python está trabalhando? A função getcwd() nos diz isso
print('Diretório inicial: {}'.format(os.getcwd()))

# Vamos mudar para o nosso diretório atual
os.chdir('C:/Users/user/Desktop/EAE1106')
print('Diretório final: {}'.format(os.getcwd()))
Diretório inicial: D:\Dropbox
Diretório final: C:\Users\user\Desktop\EAE1106

Quais são os arquivos disponíveis nesse diretório? A função os.listdir() lista as pastas e arquivos disponíveis no diretório que estamos trabalhando.

os.listdir()
['avhbr_pwt11.png',
 'avh_bar.png',
 'avh_barh.png',
 'avh_countries2_pwt11.png',
 'avh_countries_pwt11.png',
 'avh_pibpc_scatter.png',
 'carac_countries_all.png',
 'casos_covid_br.png',
 'casos_mortes_scatter.png',
 'df_resultado_aula_pandas.csv',
 'fhvhv_tripdata_2019-12.parquet',
 'pwt110.csv',
 'pwt110.xlsx',
 'pwt110_selected.csv',
 'pwt110_selected.xlsx']

Agora utilizaremos a função pandas.read_csv para ler o arquivo pwt110_selected.csv. Esse arquivo contém informações selecionadas para os 185 países da amostra. Além do nome do arquivo, passamos dois argumentos adicionais para a função: sep e encoding.

O argumento sep define qual caractere é utilizado para separar as colunas do arquivo. Embora arquivos CSV frequentemente utilizem vírgulas como separador (Comma-Separated Values), é bastante comum encontrar bases em que os valores são separados por ponto e vírgula ou outros caracteres especiais. Nesse caso, utilizamos sep=';' para informar ao pandas que as colunas devem ser identificadas a partir desse separador.

Já o argumento encoding especifica a codificação de caracteres utilizada no arquivo. A codificação define como letras, números e símbolos são armazenados internamente no computador. Utilizar a codificação correta é particularmente importante quando a base contém acentos e outros caracteres especiais. Aqui utilizamos encoding='utf8', uma das codificações mais comuns atualmente e amplamente compatível com diferentes sistemas operacionais e aplicações.

df_pwt = pd.read_csv('pwt110_selected.csv', sep=';', encoding='utf8')
type(df_pwt)
pandas.core.frame.DataFrame

A função read_csv faz parte de um conjunto bastante amplo de funções de importação e exportação de dados disponíveis no pandas. Mais adiante retomaremos esse tema ao estudar as funções to_, utilizadas para exportar tabelas e resultados produzidos durante a análise para os mais diferentes formatos disponíveis.

8.5 DataFrames: o coração do Pandas

A partir deste ponto começaremos a explorar com mais profundidade o objeto central da biblioteca: o DataFrame. É nele que normalmente realizamos a maior parte do trabalho empírico, como seleção de observações, criação de variáveis, agrupamentos e análises descritivas.

Como dito anteriormente, um DataFrame pode ser entendido como uma tabela bidimensional semelhante a uma planilha do Excel ou a uma base de dados estatística, em que cada linha representa uma observação e cada coluna representa uma variável. O pandas fornece uma grande quantidade de métodos específicos para manipular esse tipo de estrutura de maneira eficiente e relativamente intuitiva.

8.5.1 Características básicas

Alguns métodos específicos ao objeto DataFrame nos são incrivelmente úteis quando queremos ter uma leitura rápida do que acabamos de gerar.

  • df.info() nos retorna algumas informações técnicas do dataframe df, como o formato dos dados presentes em cada coluna.
df_pwt.info(memory_usage='deep')
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 13690 entries, 0 to 13689
Data columns (total 14 columns):
 #   Column         Non-Null Count  Dtype  
---  ------         --------------  -----  
 0   countrycode    13690 non-null  object 
 1   country        13690 non-null  object 
 2   currency_unit  13690 non-null  object 
 3   year           13690 non-null  int64  
 4   pop            11201 non-null  float64
 5   emp            10307 non-null  float64
 6   rgdpna         11201 non-null  float64
 7   avh            5015 non-null   float64
 8   hc             9217 non-null   float64
 9   rconna         11201 non-null  float64
 10  rdana          11201 non-null  float64
 11  rnna           11028 non-null  float64
 12  labsh          8215 non-null   float64
 13  delta          11028 non-null  float64
dtypes: float64(10), int64(1), object(3)
memory usage: 3.4 MB
  • df.head(n) e df.tail(n) nos retornam as n primeiras e n últimas linhas, respectivamente.
df_pwt.head(2) # método para apresentar apenas as primeiras duas linhas do dataframe
countrycode country currency_unit year pop ... rconna rdana rnna labsh delta
0 ABW Aruba Aruban Guilder 1950 NaN ... NaN NaN NaN NaN NaN
1 ABW Aruba Aruban Guilder 1951 NaN ... NaN NaN NaN NaN NaN

2 rows × 14 columns

Ué, mas não são 14 colunas? Onde estão todas elas? Por padrão o pandas mostra apenas uma parcela do total, para alterar essas opções basta utilizar a função pd.set_option e mudar o argumento relacionado ao número máximo de colunas.

pd.set_option('display.max_columns', 100)
df_pwt.head(2)
countrycode country currency_unit year pop emp rgdpna avh hc rconna rdana rnna labsh delta
0 ABW Aruba Aruban Guilder 1950 NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
1 ABW Aruba Aruban Guilder 1951 NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
  • df.describe() nos retorna uma tabela de estatísticas descritivas das colunas numéricas.
df_pwt.describe()
year pop emp rgdpna avh hc rconna rdana rnna labsh delta
count 13690.00000 11201.000000 10307.000000 1.120100e+04 5015.000000 9217.000000 1.120100e+04 1.120100e+04 1.102800e+04 8215.000000 11028.000000
mean 1986.50000 31.708879 14.397923 3.950347e+05 1995.863422 2.127556 2.903016e+05 3.904631e+05 1.739550e+06 0.523476 0.043042
std 21.36079 118.073842 58.505511 1.525476e+06 320.341119 0.739408 1.105490e+06 1.533064e+06 6.595402e+06 0.124389 0.013382
min 1950.00000 0.004420 0.001180 1.615750e+01 1313.570000 1.007038 2.269696e+01 2.849165e+01 1.966073e+01 0.084364 0.012140
25% 1968.00000 1.677382 0.793171 8.814905e+03 1743.505000 1.478258 7.249839e+03 8.761411e+03 2.555000e+04 0.445721 0.033917
50% 1986.50000 6.323060 2.892331 4.071412e+04 1983.210000 2.035692 3.348346e+04 4.067692e+04 1.383617e+05 0.523455 0.040115
75% 2005.00000 20.568728 8.566412 2.249628e+05 2232.590000 2.720364 1.612985e+05 2.149864e+05 8.200364e+05 0.613714 0.049590
max 2023.00000 1438.069596 774.418213 3.104758e+07 3039.790000 3.986023 2.055370e+07 2.990740e+07 1.710951e+08 0.911995 0.110218

Uma outra coisa que pode ser bastante interessante é saber quantas vezes os valores de determinada variável se repetem. Para isso é preciso primeiro selecionar uma coluna específica do DataFrame, o que é feito utilizando a lógica de dicionários em que o nome de uma coluna é como uma chave associada a uma lista de valores. A partir da seleção da coluna, basta aplicar o método value_counts. A título de exemplo, vejamos quantas vezes cada país da base de dados aparece no DataFrame.

df_pwt['country'].value_counts()
country
Aruba                74
Norway               74
Montserrat           74
Mauritius            74
Malawi               74
                     ..
Guinea               74
Gambia               74
Guinea-Bissau        74
Equatorial Guinea    74
Zimbabwe             74
Name: count, Length: 185, dtype: int64

As opções normalize e dropna nos permitem, respectivamente, olhar para frequência relativa, ao invés de absoluta, e excluir da tabulação as observações em que o valor da coluna country nao está disponível, isto é, é missing.

df_pwt['country'].value_counts(normalize=True,dropna=False)
country
Aruba                0.005405
Norway               0.005405
Montserrat           0.005405
Mauritius            0.005405
Malawi               0.005405
                       ...   
Guinea               0.005405
Gambia               0.005405
Guinea-Bissau        0.005405
Equatorial Guinea    0.005405
Zimbabwe             0.005405
Name: proportion, Length: 185, dtype: float64
ImportanteMissing não é a mesma coisa que zero

Em bases de dados reais, valores ausentes (missing values) são extremamente comuns. No pandas, eles normalmente aparecem como NaN (Not a Number). É importante não confundir um valor ausente com o número zero. Um salário igual a zero significa que a informação existe e vale zero. Já um NaN indica que a informação está ausente, indisponível ou não foi observada.

Muitas funções do pandas ignoram automaticamente valores missing em cálculos estatísticos. Por exemplo, médias calculadas com mean() normalmente desconsideram observações com NaN. Antes de remover ou substituir valores ausentes, é importante entender por que eles aparecem na base e como podem afetar a análise.

8.5.2 Seleção e filtragem

Por vezes pode ser interessante selecionar e filtrar os dados que temos de acordo com algum interesse específico.

A base de dados tem informação desnecessária, apenas dados de inflação e dívida nos importam

Apenas dados à partir de 2019 são relevantes

Gostaríamos de filtrar os dados que representam anos de recessão
Figura 8.3: Exemplo de seleção e filtragem

São várias as formas, umas mais diretas e outras mais poderosas, de realizar esse tipo de operação no Python. Mas vamos começar do começo aqui. A forma mais simples de selecionar um intervalo particular de linhas do DataFrame é através da lógica de indexação de sequências ordenadas, como listas, tuplas ou mesmo arrays. Para o caso em que estejamos interessados em selecionar apenas as duas primeiras linhas do DataFrame basta utiliza o código abaixo:

df_pwt[0:2]
countrycode country currency_unit year pop emp rgdpna avh hc rconna rdana rnna labsh delta
0 ABW Aruba Aruban Guilder 1950 NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
1 ABW Aruba Aruban Guilder 1951 NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN

No entanto, para selecionar um intervalo específico de linhas e colunas, a lógica de indexação que utilizamos com arrays de duas dimensões no NumPy não se aplica. Para tal, precisamos de alguns métodos de DataFrames. Utilizando a função .iloc podemos selecionar linhas e colunas com os índices numéricos. E se quisermos, por exemplo, manter todas as linhas mas apenas as colunas country, currency_unit, year, pop, emp e rgdpna? Lembre-se que quando o Python lida com intervalos, na grande maioria das vezes a linguagem inclui o índice de início do intervalo mas exclui o índice final.

df_pwt.iloc[:,1:7]
country currency_unit year pop emp rgdpna
0 Aruba Aruban Guilder 1950 NaN NaN NaN
1 Aruba Aruban Guilder 1951 NaN NaN NaN
2 Aruba Aruban Guilder 1952 NaN NaN NaN
3 Aruba Aruban Guilder 1953 NaN NaN NaN
4 Aruba Aruban Guilder 1954 NaN NaN NaN
... ... ... ... ... ... ...
13685 Zimbabwe US Dollar 2019 15.271368 5.272343 44445.03125
13686 Zimbabwe US Dollar 2020 15.526888 5.206007 40970.78516
13687 Zimbabwe US Dollar 2021 15.797210 5.298346 44440.19922
13688 Zimbabwe US Dollar 2022 16.069056 5.344455 47147.55078
13689 Zimbabwe US Dollar 2023 16.340822 5.504774 49685.75781

13690 rows × 6 columns

Uma outra forma de fazer exatamente a mesma seleção é através do método .loc. A diferença é que nesse caso utilizamos uma mistura de índices numéricos para as linhas e seleção das colunas através de seus nomes.

columns_to_keep = ['country','currency_unit','year','pop','emp','rgdpna']

df_pwt.loc[:, columns_to_keep]
country currency_unit year pop emp rgdpna
0 Aruba Aruban Guilder 1950 NaN NaN NaN
1 Aruba Aruban Guilder 1951 NaN NaN NaN
2 Aruba Aruban Guilder 1952 NaN NaN NaN
3 Aruba Aruban Guilder 1953 NaN NaN NaN
4 Aruba Aruban Guilder 1954 NaN NaN NaN
... ... ... ... ... ... ...
13685 Zimbabwe US Dollar 2019 15.271368 5.272343 44445.03125
13686 Zimbabwe US Dollar 2020 15.526888 5.206007 40970.78516
13687 Zimbabwe US Dollar 2021 15.797210 5.298346 44440.19922
13688 Zimbabwe US Dollar 2022 16.069056 5.344455 47147.55078
13689 Zimbabwe US Dollar 2023 16.340822 5.504774 49685.75781

13690 rows × 6 columns

Uma terceira forma de fazer esse exercício de seleção de uma parte de um DataFrame já criado é através da criação de um novo DataFrame que mantenha apenas as colunas de interesse. Importante, nesse caso, usar o método copy para garantir a criação de uma cópia, um novo dataframe, e evitar que alterações no dataframe original afetam o segundo dataframe e vice-versa.

df_pwt2 = df_pwt[columns_to_keep].copy()

df_pwt2.head()
country currency_unit year pop emp rgdpna
0 Aruba Aruban Guilder 1950 NaN NaN NaN
1 Aruba Aruban Guilder 1951 NaN NaN NaN
2 Aruba Aruban Guilder 1952 NaN NaN NaN
3 Aruba Aruban Guilder 1953 NaN NaN NaN
4 Aruba Aruban Guilder 1954 NaN NaN NaN

Podemos inclusive alterar o nome das colunas desse novo DataFrame de forma bem direta.

df_pwt2.columns = ['pais','moeda','ano','populacao (milhoes)','pessoas empregadas (milhoes)','PIB real em valores de 2021 (milhoes US$)']
df_pwt2.head()
pais moeda ano populacao (milhoes) pessoas empregadas (milhoes) PIB real em valores de 2021 (milhoes US$)
0 Aruba Aruban Guilder 1950 NaN NaN NaN
1 Aruba Aruban Guilder 1951 NaN NaN NaN
2 Aruba Aruban Guilder 1952 NaN NaN NaN
3 Aruba Aruban Guilder 1953 NaN NaN NaN
4 Aruba Aruban Guilder 1954 NaN NaN NaN

Legal, mas ainda não falamos nada sobre filtrar partes do DataFrame de acordo com determinada condição e valores das colunas. Para filtrar linhas de acordo com resultados de testes condicionais, basta construir o teste dentro dos colchetes que seguem o nome do dataframe. Assim como já fizemos outras vezes, os testes podem ser aninhados na hora de fazer a filtragem.

  • Selecionar apenas os dados brasileiros
cond1 = df_pwt2['pais']=='Brazil'

df_pwt2[cond1].head()
pais moeda ano populacao (milhoes) pessoas empregadas (milhoes) PIB real em valores de 2021 (milhoes US$)
1776 Brazil Brazilian Real 1950 53.270684 15.605801 173400.5000
1777 Brazil Brazilian Real 1951 54.818136 16.061132 181892.0938
1778 Brazil Brazilian Real 1952 56.419978 16.533367 199188.3438
1779 Brazil Brazilian Real 1953 58.078410 17.023821 209310.9219
1780 Brazil Brazilian Real 1954 59.795715 17.532673 226481.7969
  • Selecionar os dados brasileiros e apenas à partir do ano 2000.
cond2 = df_pwt2['ano']>=2000

df_pwt2[(cond1) & (cond2)].head()
pais moeda ano populacao (milhoes) pessoas empregadas (milhoes) PIB real em valores de 2021 (milhoes US$)
1826 Brazil Brazilian Real 2000 174.018282 68.130440 2362107.25
1827 Brazil Brazilian Real 2001 176.301203 68.456398 2394938.00
1828 Brazil Brazilian Real 2002 178.503484 71.253647 2468066.75
1829 Brazil Brazilian Real 2003 180.622688 72.304565 2496223.00
1830 Brazil Brazilian Real 2004 182.675143 76.159271 2640004.75
  • Selecionar os dados do Brasil e Estados Unidos
cond1 = df_pwt2['pais'].isin(['Brazil','United States'])

df_pwt2[cond1].head()
pais moeda ano populacao (milhoes) pessoas empregadas (milhoes) PIB real em valores de 2021 (milhoes US$)
1776 Brazil Brazilian Real 1950 53.270684 15.605801 173400.5000
1777 Brazil Brazilian Real 1951 54.818136 16.061132 181892.0938
1778 Brazil Brazilian Real 1952 56.419978 16.533367 199188.3438
1779 Brazil Brazilian Real 1953 58.078410 17.023821 209310.9219
1780 Brazil Brazilian Real 1954 59.795715 17.532673 226481.7969

8.5.3 Operações com colunas numéricas

Criar colunas novas ou mesmo alterar colunas já existentes quando o tipo de dado é numérico é uma tarefa bastante simples. As operações são feitas como se fosse operações de arrays do NumPy, em que uma coluna se comporta tal qual um array de uma única dimensão. Vejamos alguns exemplos de operações a partir de um novo DataFrame com dados brasileiros.

# O método reset_index nesse caso, serve para renumerar as linhas a partir do zero.
df_pwtbr = df_pwt2[df_pwt2['pais']=='Brazil'].reset_index()

df_pwtbr.head()
index pais moeda ano populacao (milhoes) pessoas empregadas (milhoes) PIB real em valores de 2021 (milhoes US$)
0 1776 Brazil Brazilian Real 1950 53.270684 15.605801 173400.5000
1 1777 Brazil Brazilian Real 1951 54.818136 16.061132 181892.0938
2 1778 Brazil Brazilian Real 1952 56.419978 16.533367 199188.3438
3 1779 Brazil Brazilian Real 1953 58.078410 17.023821 209310.9219
4 1780 Brazil Brazilian Real 1954 59.795715 17.532673 226481.7969
  • Coluna com o PIB real per capita
df_pwtbr['pibpc'] = df_pwtbr['PIB real em valores de 2021 (milhoes US$)'] / df_pwtbr['populacao (milhoes)']

columns_to_show = ['ano','populacao (milhoes)','PIB real em valores de 2021 (milhoes US$)','pibpc']
df_pwtbr[columns_to_show].head()
ano populacao (milhoes) PIB real em valores de 2021 (milhoes US$) pibpc
0 1950 53.270684 173400.5000 3255.083040
1 1951 54.818136 181892.0938 3318.100670
2 1952 56.419978 199188.3438 3530.457665
3 1953 58.078410 209310.9219 3603.936848
4 1954 59.795715 226481.7969 3787.592420
  • Coluna com o log do PIB per capita
df_pwtbr['log_pibpc'] = np.log(df_pwtbr['pibpc'])

columns_to_show = ['ano','pibpc','log_pibpc']
df_pwtbr[columns_to_show].head()
ano pibpc log_pibpc
0 1950 3255.083040 8.087973
1 1951 3318.100670 8.107148
2 1952 3530.457665 8.169183
3 1953 3603.936848 8.189782
4 1954 3787.592420 8.239486
  • Coluna com o PIB per capita em milhares de dólares
df_pwtbr['pibpc_th'] = df_pwtbr['pibpc'] / 1000

columns_to_show = ['ano','pibpc','pibpc_th']
df_pwtbr[columns_to_show].head()
ano pibpc pibpc_th
0 1950 3255.083040 3.255083
1 1951 3318.100670 3.318101
2 1952 3530.457665 3.530458
3 1953 3603.936848 3.603937
4 1954 3787.592420 3.787592
  • Calcular a razão entre pessoas empregadas e a população. Subtrair a média dessa razão da coluna original para encontrar o desvio da razão em relação à média.
df_pwtbr['r_emppop'] = df_pwtbr['pessoas empregadas (milhoes)'] / df_pwtbr['populacao (milhoes)']
df_pwtbr['r_emppop'] = df_pwtbr['r_emppop'] - df_pwtbr['r_emppop'].mean()

columns_to_show = ['ano','r_emppop','r_emppop']
df_pwtbr[columns_to_show].head()
ano r_emppop r_emppop
0 1950 -0.077519 -0.077519
1 1951 -0.077483 -0.077483
2 1952 -0.077431 -0.077431
3 1953 -0.077354 -0.077354
4 1954 -0.077263 -0.077263

Para realizar outras operações simples de soma, subtração, multiplicação e divisão a lógica é a mesma, independente de ser uma operação entre colunas ou de uma coluna com um escalar. Para operações mais complexas, no entanto, o método .apply em conjunto com funções anônimas do tipo lambda constituem uma ferramenta poderosíssima.

  • Calcular a raiz quadrada da população
df_pwtbr['pop_sqrt'] = df_pwtbr['populacao (milhoes)'].apply(lambda x: ((x*1000000)**0.5))

columns_to_show = ['ano','populacao (milhoes)','pop_sqrt']
df_pwtbr[columns_to_show].head()
ano populacao (milhoes) pop_sqrt
0 1950 53.270684 7298.676866
1 1951 54.818136 7403.927066
2 1952 56.419978 7511.323319
3 1953 58.078410 7620.919236
4 1954 59.795715 7732.768909

Para visualizar melhor, vamos focar no último valor, representado pelo índice 100.

print(df_pwtbr.loc[10,'populacao (milhoes)'])
print(df_pwtbr.loc[10,'pop_sqrt'])
print((df_pwtbr.loc[10,'populacao (milhoes)'] * 1000000)**0.5)
71.463546
8453.611417613185
8453.611417613185

Fizemos apenas alguns exemplos bastante simples, mas note a força que o método apply tem quando trabalhado em conjunto com as funções lambda. O céu é o limite.

8.5.4 Operações com colunas não-numéricas

O Pandas também nos fornece métodos e funções bastante interessantes para trabalhar com dados do tipo string. Para trabalhar com essas funções/métodos precisamos primeiro dizer ao Pandas que a coluna de interesse receberá operações específicas a strings através do sufixo str. Podemos aplicar a maior parte dos métodos de strings que conhecemos (lower, replace, starswith, etc.) utilizando essa sintaxe. Vamos começar com um exemplo simples: transformando a nossa coluna com o nome dos países para um coluna com letras minúsculas apenas.

df_pwtbr['lower_pais'] = df_pwtbr['pais'].str.lower()

columns_to_show = ['pais','lower_pais','ano']
df_pwtbr[columns_to_show].head()
pais lower_pais ano
0 Brazil brazil 1950
1 Brazil brazil 1951
2 Brazil brazil 1952
3 Brazil brazil 1953
4 Brazil brazil 1954

Podemos aplicar a maior parte dos métodos de strings que já vimos (replace,starswith,strip,extract, etc.) utilizando essa sintaxe. Brinque um pouco com isso.

Uma outra funcionalidade interessante é a possibilidade de criação de novas colunas de strings utilizando dicionários. Podemos, por exemplo, utilizar essa funcionalidade para criar uma nova coluna que mostre se razão de participação no mercado de trabalho em determinado ano está acima ou abaixo do 3º quartil da distribuição.

df_pwtbr['r_emppop'].describe()
count    7.400000e+01
mean    -3.000603e-17
std      6.054603e-02
min     -8.093268e-02
25%     -7.675353e-02
50%      7.060548e-03
75%      4.631345e-02
max      9.981850e-02
Name: r_emppop, dtype: float64
# criando coluna a partir de list comprehension
df_pwtbr['remppop_p75bool'] = [True if x>=100 else False for x in df_pwtbr['r_emppop']]

# criando coluna a partir de um dicionario
quartil_sup = {
    True:'Acima do 3º quartil',
    False:'Abaixo do 3º quartil'}

df_pwtbr['remppop_p75cat'] = df_pwtbr['remppop_p75bool'].apply(lambda x: quartil_sup[x])

E qual é o resultado final?

columns_to_show = ['ano','r_emppop','remppop_p75bool','remppop_p75cat']
df_pwtbr[columns_to_show].head()
ano r_emppop remppop_p75bool remppop_p75cat
0 1950 -0.077519 False Abaixo do 3º quartil
1 1951 -0.077483 False Abaixo do 3º quartil
2 1952 -0.077431 False Abaixo do 3º quartil
3 1953 -0.077354 False Abaixo do 3º quartil
4 1954 -0.077263 False Abaixo do 3º quartil

8.5.5 Agrupamento de dados

Suponha que queiramos calcular médias por década do PIB per capita, da razao de participação no mercado de trabalho e salvar isso em um novo dataframe. Vamos fazer isso para os dados brasileiros e norte-americanos de forma separada. Primeiro passo então é criar esses dataframes por país.

df_pwt2['decada'] = np.trunc(df_pwt2['ano']/10) * 10
df_pwt2['pibpc'] = df_pwt2['PIB real em valores de 2021 (milhoes US$)'] / df_pwt2['populacao (milhoes)']
df_pwt2['r_emppop'] = df_pwt2['pessoas empregadas (milhoes)'] / df_pwt2['populacao (milhoes)']

columns_to_keep = ['pais','decada','pibpc','r_emppop']

df_pwtdbr = df_pwt2.loc[df_pwt2['pais']=='Brazil', columns_to_keep]
df_pwtdus = df_pwt2.loc[df_pwt2['pais']=='United States', columns_to_keep]

df_pwtdbr.head(2)
pais decada pibpc r_emppop
1776 Brazil 1950.0 3255.08304 0.292953
1777 Brazil 1950.0 3318.10067 0.292989

Uma vez criado os dataframes individuais, o passo seguinte é “condensar” cada dataframe individual de acordo com valores específicos de um conjunto de colunas e usando o operador média como forma de realizar essa operação de achatamento. Para esse objetivo, devemos utilizar o método groupby. Atenção para a diferença quando definimos o parâmetro as_index para igual a False.

df_pwtdbr = df_pwtdbr.groupby(by=['pais','decada']).mean()
df_pwtdbr.head(5)
pibpc r_emppop
pais decada
Brazil 1950.0 3844.530579 0.293308
1960.0 5764.102096 0.291015
1970.0 10120.107950 0.339365
1980.0 12657.359766 0.381368
1990.0 12841.250100 0.389903
df_pwtdus = df_pwtdus.groupby(by=['pais','decada'], as_index=False).mean()
df_pwtdus.head(5)
pais decada pibpc r_emppop
0 United States 1950.0 19564.743337 0.400766
1 United States 1960.0 24575.575260 0.390191
2 United States 1970.0 31393.560625 0.422446
3 United States 1980.0 38295.758750 0.463348
4 United States 1990.0 47192.487562 0.483275

Note que… Mas como não queremos o nome do país como indice, vamos redefinir o dataframe do Brasil

df_pwtdbr.reset_index(inplace=True)
df_pwtdbr.head(5)
pais decada pibpc r_emppop
0 Brazil 1950.0 3844.530579 0.293308
1 Brazil 1960.0 5764.102096 0.291015
2 Brazil 1970.0 10120.107950 0.339365
3 Brazil 1980.0 12657.359766 0.381368
4 Brazil 1990.0 12841.250100 0.389903

8.5.6 Junção e mesclagem

Note que fizemos um dataframe para o Brasil e outro para os Estados Unidos. Podíamos ter feito tudo junto? Com certeza. Mas fizemos dessa forma justamente para mostrar outra funcionalidade bastante importante quando trabalhamos com bases de dados: a junção de duas bases diferentes. Embora tenhamos trabalhado com dados contidos em dataframes ``completos’’, é bastante comum utilizarmos várias bases de dados diferentes ao mesmo tempo e que contenham dados de alguma forma relacionados. São duas as principais formas de fazer esse tipo de operação:

  • Adicionar dados com as mesmas colunas, mas contendo informação sobre entidades diferentes.
  • Adicionar dados das mesmas entidades, mas contendo informação inexistente nas colunas da base original.

A função concat serve ao propósito de adicionar o mesmo conjunto de informações, mas de novas entidades. É como se essa função ``somasse’’ duas bases, criando uma nova base que apenas junta as linhas dos diferentes dataframes. Isso é muito útil, por exemplo, quando geramos dataframes dentro de um loop e queremos ir juntando cada novo dataframe a um grande e único.

df_pwt_concat = pd.concat(objs=[df_pwtdbr, df_pwtdus]).reset_index(drop=True)

df_pwt_concat
pais decada pibpc r_emppop
0 Brazil 1950.0 3844.530579 0.293308
1 Brazil 1960.0 5764.102096 0.291015
2 Brazil 1970.0 10120.107950 0.339365
3 Brazil 1980.0 12657.359766 0.381368
4 Brazil 1990.0 12841.250100 0.389903
5 Brazil 2000.0 14812.187170 0.415319
6 Brazil 2010.0 17793.024042 0.452457
7 Brazil 2020.0 17696.781294 0.446901
8 United States 1950.0 19564.743337 0.400766
9 United States 1960.0 24575.575260 0.390191
10 United States 1970.0 31393.560625 0.422446
11 United States 1980.0 38295.758750 0.463348
12 United States 1990.0 47192.487562 0.483275
13 United States 2000.0 58098.781642 0.482854
14 United States 2010.0 63002.666303 0.468900
15 United States 2020.0 69799.710347 0.475486

Para compreender completamente o segundo caso, é preciso entender que existem formas diferentes de adicionar novas informações às entidades existentes. E a teoria dos conjuntos nos ajuda a entender melhor como isso funciona. Utilizando uma coluna (ou colunas) que identifiquem unicamente pelo menos uma das bases de dados (A ou B) é possível unir as informações de modo a compor um único dataframe. É preciso, porém, ter claro o objetivo da mesclagem. Assim será possível definir quais dos casos que mais se encaixa ao problema em questão. A Figura Figura 8.4 ilustra os diferentes casos.

Figura 8.4: Tipo de união entre duas bases de dados, A e B

Agora sim podemos dar um passo adiante e utilizar a função merge para unir as bases do Brasil e EUA. Essa função une duas bases de acordo com algum identificador e com algum método de união. O identificador pode vir do índice do dataframe ou mesmo de alguma coluna específica. No nosso caso utilizaremos a coluna de década como identificadora através do parâmetro on. Falta definir agora como a junção será feita ou como lidar com os casos em que um valor da coluna identificadora aparece apenas em uma das bases de dados. Como desejamos manter apenas as observações que estão presentes em ambos os dataframes, utilizamos o valor inner no parâmetro how.

df_pwt_merge = pd.merge(df_pwtdbr, df_pwtdus, how='inner', on=['decada'])

df_pwt_merge
pais_x decada pibpc_x r_emppop_x pais_y pibpc_y r_emppop_y
0 Brazil 1950.0 3844.530579 0.293308 United States 19564.743337 0.400766
1 Brazil 1960.0 5764.102096 0.291015 United States 24575.575260 0.390191
2 Brazil 1970.0 10120.107950 0.339365 United States 31393.560625 0.422446
3 Brazil 1980.0 12657.359766 0.381368 United States 38295.758750 0.463348
4 Brazil 1990.0 12841.250100 0.389903 United States 47192.487562 0.483275
5 Brazil 2000.0 14812.187170 0.415319 United States 58098.781642 0.482854
6 Brazil 2010.0 17793.024042 0.452457 United States 63002.666303 0.468900
7 Brazil 2020.0 17696.781294 0.446901 United States 69799.710347 0.475486

Mas note que existem colunas com o mesmo nome nas bases do Brasil e EUA. Por padrão o pandas renomeia essas colunas com _x e _y. Primeiro, podemos começar excluindo as colunas pais_x e pais_y, que deixam de fazer sentido após o merge. Depois disso, podemos renomear as outras colunas utilizando dicionários, uma forma alternativa ao que fizemos anteriormente.

df_pwt_merge.drop(columns=['pais_x','pais_y'], inplace=True)

dict_rename = {
    'pibpc_x':'pibpc_br',
    'r_emppop_x':'r_emppop_br',
    'pibpc_y':'pibpc_us',
    'r_emppop_y':'r_emppop_us'}

df_pwt_merge.rename(columns=dict_rename, inplace=True)

df_pwt_merge.head()
decada pibpc_br r_emppop_br pibpc_us r_emppop_us
0 1950.0 3844.530579 0.293308 19564.743337 0.400766
1 1960.0 5764.102096 0.291015 24575.575260 0.390191
2 1970.0 10120.107950 0.339365 31393.560625 0.422446
3 1980.0 12657.359766 0.381368 38295.758750 0.463348
4 1990.0 12841.250100 0.389903 47192.487562 0.483275

8.6 Exportando o DataFrame

Já falamos de importação de dados

Por fim, a última coisa que gostaríamos de aprender nesse aulão de Pandas é como salvar o dataframe, resultado de todas as nossas operações, para um novo csv ou arquivo de qualquer outro tipo. Assim como tínhamos funções do tipo read_csv para trazer arquivos de fora, temos métodos do tipo to_csv para salvar um dado dataframe em um novo arquivo a ser utilizado em outros softwares.

A sintaxe é bastante simples:

df_pwt_merge.to_csv('df_resultado_aula_pandas.csv', sep=',', encoding='utf8', index=False)

os.listdir()
['avhbr_pwt11.png',
 'avh_bar.png',
 'avh_barh.png',
 'avh_countries2_pwt11.png',
 'avh_countries_pwt11.png',
 'avh_pibpc_scatter.png',
 'carac_countries_all.png',
 'casos_covid_br.png',
 'casos_mortes_scatter.png',
 'df_resultado_aula_pandas.csv',
 'fhvhv_tripdata_2019-12.parquet',
 'pwt110.csv',
 'pwt110.xlsx',
 'pwt110_selected.csv',
 'pwt110_selected.xlsx']

Olha o arquivo que geramos aí, minha gente!

Tente exportar o dataframe em outros formatos e importá-lo em outros softwares. Lembre-se, assim como quando queremos aprender novas línguas como inglês, alemão ou mandarim, em programação a fluência chega apenas com muito treino, tentativa e erro.

8.7 Eficiência e dtypes

DataFrames podem ser objetos bastante pesados, com milhões de linhas e milhares de colunas. Isso por vezes pode ser um problemão para nós. Existem bibliotecas que servem ao propósito de trabalhar de forma mais eficiente com bases de dados muito grandes (e.g., Dask), mas algumas funções dentro do próprio Pandas já conseguem nos ajudar bastante em grande parte das situações.

Vamos trabalhar com uma outra base de dados a partir de agora: as viagens de Uber na cidade de Nova York. A base de dados contém informações detalhadas sobre cada viagem realizada: número identificador do motorista, hora do início e fim da corrida, distância percorrida, gorjetas, etc. Vejamos o caso dos dados para o mês de dezembro de 2019, disponíveis no arquivo fhvhv_tripdata_2019-12.parquet. Utilizarei o meu computador local, que tem 128GB de memória RAM, só para mostrar a vocês o tamanho e como a base de dados se apresenta sem nenhum tipo de personalização na função que lê arquivos do tipo parquet.1

UberNYC_201912 = pd.read_parquet('fhvhv_tripdata_2019-12.parquet')
UberNYC_201912.info(memory_usage='deep')
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 22243901 entries, 0 to 22243900
Data columns (total 24 columns):
 #   Column                Dtype         
---  ------                -----         
 0   hvfhs_license_num     object        
 1   dispatching_base_num  object        
 2   originating_base_num  object        
 3   request_datetime      datetime64[us]
 4   on_scene_datetime     datetime64[us]
 5   pickup_datetime       datetime64[us]
 6   dropoff_datetime      datetime64[us]
 7   PULocationID          int64         
 8   DOLocationID          int64         
 9   trip_miles            float64       
 10  trip_time             int64         
 11  base_passenger_fare   float64       
 12  tolls                 float64       
 13  bcf                   float64       
 14  sales_tax             float64       
 15  congestion_surcharge  float64       
 16  airport_fee           float64       
 17  tips                  float64       
 18  driver_pay            float64       
 19  shared_request_flag   object        
 20  shared_match_flag     object        
 21  access_a_ride_flag    object        
 22  wav_request_flag      object        
 23  wav_match_flag        object        
dtypes: datetime64[us](4), float64(9), int64(3), object(8)
memory usage: 11.1 GB

Pela leitura da saída do método info, é possível notar que a base contém mais de 22 milhões de corridas registradas somente no mês de dezembro de 2019. Junto das mais de 20 colunas de informação, isso faz com que a memória do computador seja bastante utilizada. Apenas para lidar com esse mês específico, seria necessário um computador com ao menos 16GB de memória RAM para abrir a base e, possivelmente, com 32GB para realizar operações mais complexas. Assim, métodos que nos permitam reduzir esse uso de memória são muito bem-vindos. Olhar para o tipo de dado de cada coluna pode ser um bom primeiro passo.

Cada tipo de dado é indicado para um conjunto de informações específico: float e int representam números, object e category representam strings e bool dados do tipo booleano. Esses tipos ocupam quantidades diferentes na memória RAM. Uma coluna do tipo float64, por exemplo, demanda 64 bits (ou 8 bytes de memória) para representar um único número. Para representar 10 milhões de linhas, essa coluna consumiria 80 MB da memória RAM. Isso quer dizer, que essa mesma coluna consumiria 4x menos memória se armazenada como float16 e 8x menos memória se armazenada como int8. Da mesma forma, uma coluna do tipo object consome muito mais memória do que uma coluna que o Pandas entende como do tipo categórica em grande parte dos casos. Mas o que são esses tipos todos?

  • int8 / uint8: consome 1 byte (8-bit) de memória, o valor numérico deve ser inteiro e estar no intervalo \([-128 , 127]\) / \([0 , 255]\)
  • int16 / uint16: consome 2 bytes de memória, o valor numérico deve ser inteiro e estar no intervalo \([-32768 , 32768]\) / \([0,65535]\)
  • int32 / uint32: consome 4 bytes de memória e aceita valores numéricos inteiros com até 10 dígitos, da ordem de 2 bilhões.
  • int64 / uint64: consome 8 bytes de memória e aceita valores numéricos inteiros ainda maiores.
  • float16 / float32 / float64: consome 2, 4 e 8 bytes de memória, respectivamente, e aceita valores com casas decimais delimitado pelos mesmos intervalos dos tipos anteriores.
  • bool: consome 1 byte de memória e aceita apenas dois valores True e False.
  • object: o quanto consome de memória pode variar, mas, por aceitar valores numéricos e strings de todo tipo, consome ao menos 8 bytes de memória.
  • category: o quanto consome de memória pode variar e depende da relação entre o número de valores únicos na coluna e o número de linhas de dados.

Beleza, devemos fugir do tipo object e tentar reduzir as colunas numéricas para tipos menos demandantes em termos de memória. Como fazer isso? Vamos começar lendo apenas as primeiras linhas do dataframe para ter uma ideia do que é possível fazer. Funções como read_csv possuem parâmetros, como nrows que nos permitem ler apenas as n primeiras linhas, o que geralmente é feito antes de tentarmos abrir a base inteira de uma vez só. A função read_parquet do Pandas, no entanto, não possui parâmetros parecidos. A título de exposição, vou continuar com essa base mas mostrando apenas as 5 primeiras linhas desse dataframe completo, em especial para as colunas 19-23, que são colunas do tipo object.

columns_object = ['shared_request_flag',
                  'shared_match_flag',
                  'access_a_ride_flag',
                  'wav_request_flag',
                  'wav_match_flag']

UberNYC_201912.loc[0:5, columns_object]
shared_request_flag shared_match_flag access_a_ride_flag wav_request_flag wav_match_flag
0 N Y N N
1 N N N N
2 N N N N
3 N Y N N
4 N N N N
5 N N N N

Interessante, todas essas 5 colunas parecem ser colunas do tipo sim ou não, com apenas três valores possíveis: “Y”, “N” ou ” ” (vazio). Isso remete a dados do tipo booleano, que indicam a existência ou não de determinada característica. Vamos substituir o tipo de dado nessas colunas por um dado do tipo booleano, mas considerando também o caso de informação faltante.

for c in columns_object:
    UberNYC_201912[c] = UberNYC_201912[c].replace({'Y':True,'N':False,' ':False})
    UberNYC_201912[c] = UberNYC_201912[c].astype('bool')

UberNYC_201912.info(memory_usage='deep')
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 22243901 entries, 0 to 22243900
Data columns (total 24 columns):
 #   Column                Dtype         
---  ------                -----         
 0   hvfhs_license_num     object        
 1   dispatching_base_num  object        
 2   originating_base_num  object        
 3   request_datetime      datetime64[us]
 4   on_scene_datetime     datetime64[us]
 5   pickup_datetime       datetime64[us]
 6   dropoff_datetime      datetime64[us]
 7   PULocationID          int64         
 8   DOLocationID          int64         
 9   trip_miles            float64       
 10  trip_time             int64         
 11  base_passenger_fare   float64       
 12  tolls                 float64       
 13  bcf                   float64       
 14  sales_tax             float64       
 15  congestion_surcharge  float64       
 16  airport_fee           float64       
 17  tips                  float64       
 18  driver_pay            float64       
 19  shared_request_flag   bool          
 20  shared_match_flag     bool          
 21  access_a_ride_flag    bool          
 22  wav_request_flag      bool          
 23  wav_match_flag        bool          
dtypes: bool(5), datetime64[us](4), float64(9), int64(3), object(3)
memory usage: 6.0 GB

As colunas 19-23 foram transformadas em colunas booleanas sem perder nenhuma informação. Isso já reduziu enormemente o comprometimento de memória, de 11 GB para 6 GB. Um próximo passo seria olhar para variáveis numéricas, tal qual PULocationID e DOLocationID que são variáveis numéricas que contém identificadores das regiões. Será que precisamos de int64 nesses dois casos ou um número menor de bits daria conta de representar o dado sem perder informação?

columns_int = ['PULocationID','DOLocationID']

UberNYC_201912[columns_int].describe()
PULocationID DOLocationID
count 2.224390e+07 2.224390e+07
mean 1.387324e+02 1.412730e+02
std 7.509280e+01 7.749719e+01
min 1.000000e+00 1.000000e+00
25% 7.500000e+01 7.500000e+01
50% 1.400000e+02 1.410000e+02
75% 2.110000e+02 2.160000e+02
max 2.650000e+02 2.650000e+02

Note que ambas as variáveis tem 1 como valor mínimo e 265 como valor máximo. Já vimos no capítulo Capítulo 1 que 8 bits dão conta de representar 256 números inteiros, indo de 0 a 255 no caso dos não-negativos. Precisamos de mais bits e, nesse caso, int16 parece uma boa aposta. Para transformar o tipo de dado diretamente, utilizaremos a função astype.

for c in columns_int:
    UberNYC_201912[c] = UberNYC_201912[c].astype('int16')

UberNYC_201912.info(memory_usage='deep')
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 22243901 entries, 0 to 22243900
Data columns (total 24 columns):
 #   Column                Dtype         
---  ------                -----         
 0   hvfhs_license_num     object        
 1   dispatching_base_num  object        
 2   originating_base_num  object        
 3   request_datetime      datetime64[us]
 4   on_scene_datetime     datetime64[us]
 5   pickup_datetime       datetime64[us]
 6   dropoff_datetime      datetime64[us]
 7   PULocationID          int16         
 8   DOLocationID          int16         
 9   trip_miles            float64       
 10  trip_time             int64         
 11  base_passenger_fare   float64       
 12  tolls                 float64       
 13  bcf                   float64       
 14  sales_tax             float64       
 15  congestion_surcharge  float64       
 16  airport_fee           float64       
 17  tips                  float64       
 18  driver_pay            float64       
 19  shared_request_flag   bool          
 20  shared_match_flag     bool          
 21  access_a_ride_flag    bool          
 22  wav_request_flag      bool          
 23  wav_match_flag        bool          
dtypes: bool(5), datetime64[us](4), float64(9), int16(2), int64(1), object(3)
memory usage: 5.7 GB

Reduzimos em mais 300 MB o uso da memória RAM. No fim das contas, com um pouco de conhecimento sobre tipos de dados e alterações marginais no dataframe original, conseguimos reduzir pela metade o uso de memória RAM e o comprometimento do computador. Pode parecer simples, mas esse conhecimento é bastante poderoso.

Em resumo, parte importante da estratégia de otimização na gestão de memória de dataframes vem de conhecermos os tipos mais adequados de dados para cada coluna antes da leitura do dataframe. Isso pode ser feito lendo primeiro um amostra de poucas linhas (e.g., 10 ou 20) do dataframe com o qual iremos trabalhar para entender a base de dados e só a partir daí ler o dataframe completo com a função read_csv (ou similar) e passar uma lista de tipo de dados ao argumento dtype (exemplo do algoritmo aqui). Tente isso em casa! E não se esqueça: fuja de dados do tipo object!

8.8 Conclusão

Neste capítulo introduzimos o Pandas, principal biblioteca de manipulação de dados em Python. Com ela, passamos a trabalhar com bases tabulares semelhantes às utilizadas em aplicações empíricas reais. Ao longo da aula exploramos operações centrais do processo de análise de dados, como importação de arquivos, filtragem, criação de variáveis, agrupamento e combinação de bases. Essas ferramentas compõem a etapa de preparação de dados, parte fundamental de praticamente qualquer projeto empírico em Economia.

Em aplicações reais, uma parcela significativa do trabalho é dedicada justamente à organização e transformação das bases utilizadas na análise. Dominar essas operações é essencial para construir rotinas reproduzíveis e produzir análises confiáveis. Nos próximos capítulos utilizaremos os DataFrames construídos aqui para produzir visualizações e realizar análises estatísticas mais completas.

8.9 Exercícios

  1. Considere a base de dados original da PWT, disponível aqui. Baixe a versão em Excel e utilize a função read_excel para ler os dados disponíveis na aba Data no formato de um DataFrame do Pandas.

    1. Quantas linhas e colunas existem na base?
    2. Quais são os tipos de dados de cada coluna?
    3. Qual coluna ocupa mais memória?
  2. Ainda sobre a base de dados do exercício anterior:

    1. Construa uma lista com os nomes dos países da América do Sul e utilize essa lista para selecionar, a partir do DataFrame original, apenas as observações referentes a esses países.
    2. Crie uma nova coluna contendo o logaritmo do PIB per capita. A partir dos valores dessa nova coluna, crie uma coluna booleana indicando se o país possui PIB per capita acima da mediana da amostra.
    3. Ordene a base do maior para o menor PIB per capita. Quais eram os 10 países com maior PIB per capita em 1995? E em 2019?
  3. X


  1. O Apache Parquet é um formato de armazenamento colunar de código aberto que aborda os desafios do processamento de big data. Ao contrário do armazenamento tradicional baseado em linhas, ele organiza os dados em colunas. Essa estrutura permite que você leia apenas as colunas necessárias, tornando as consultas de dados mais rápidas e reduzindo o consumo de recursos. Leia mais aqui.↩︎