Matplotlib


Quando lidamos com dados precisamos, muitas vezes, visualizar de forma gráfica esses dados. Em muitas tarefas é útil, ou até mesmo essencial, que as conclusões das análises sejam mostradas através de gráficos. Mesmo nas fases iniciais de uma análise, na preparação e limpeza de dados, a visualização é importante visualizar para se compreender padrões, tendências e anomalias, tais como pontos fora da curva. Existem no Python inúmeras bibliotecas para visualização de dados e montagem de gráficos. Matplotlib é o módulo básico para uso em conjunto com o pandas.

A biblioteca é grande, com extensas possibilidades e tem sido usada como base para a elaboração de outros módulos gráficos, como o Seaborn. O aprendizado da biblioteca inteira pode demandar um esforço considerável mas o uso básico, suficiente para muitos projetos, não demanda tanto empenho. Além disso o pandas tem uma vinculação natural com a biblioteca, como veremos.

Instalação

Matplotlib é instalado junto com a distribuição do python via Anaconda. Se você não está usando Anaconda é possível encontrar maiores instruções de instalação no site de Matplotlib.

Um pouco de história

Matplotlib começou a ser desenvolvida em 2003 por John D. Hunter, um neurocientista que usava MATLAB e queria aperfeiçoar a visualização de dados obtidos por meio de EEGs (eletroencelografia) em sua pesquisa sobre epilepsia. Hoje uma a comunidade de desenvolvedores colaboram para manter e aperfeiçoar a biblioteca.

Como muitos usuários e desenvolvedores estavam acostumados ao ambiente do MATLAB, onde todas as funções estão disponíveis globalmente sem a necessidade de importações, o módulo pylab foi desenvolvido. Ele existe para trazer funções e classes do NumPy e matplotlib para o namespace global. Isso significa que o comando from pylab import * em uma sessão significa a importação desses módulos e é desnecessária para quem está acostumado com o estilo do python. Como já vimos a importação de muitos módulos, funções e classes pode provocar conflito entre as partes importadas e os métodos built-in do python.

De fato, o uso de ipython --pylab para quem usa o comando de linha, ou %pylab de dentro do Jupyter, simplesmente faz uma chamada interna à from pylab import *. Nesse sentido se recomenda, para quem trabalha com IPython e Jupiter Notebook, que se use a “mágica” %matplotlib.

Por todos esses motivos usaremos a abordagem usual do python:

» import matplotlib.pyplot as plt
» import numpy as np
» np.random.seed(444)
» # para exibição dos gráficos no ambiente do jupyter notebook usamos
» %matplotlib
» # para exibição incorporada dentro do notebook
» %matplotlib inline
» # ou, para exibição dentro do notebook, com controles de zoom e arraste
» %matplotlib notebook

Numpy será usado para as contruções de arrays e geração de dados aleatórios. A informação de np.random.seed() serve para que os geradores produzam os mesmos números em seções posteriores, para reproducibilidade. A mágica %matplotlib faz com que os gráficos sejam exibidos. Nesse caso uma nova janela é aberta com uma barra de menus com acesso à ampliação, arraste, gravação em vários formatos (como pdf, jpg, png), e parâmetros do gráfico. Já a inserção de %matplotlib inline faz com que os gráficos fiquem embutidos no próprio notebook e sejam gravados com ele. A janela de controle não aparece. Usando %matplotlib notebook temos o gráfico embutido com acesso à controles de zoom, arraste e gravação em arquivo.

Técnica Básica

O matplotlib pode receber como fonte de dados listas, arrays do numpy, Series e dataframes do pandas. Por exemplo, o código seguinte recebe listas gera as figuras 1 e 2 abaixo:

» %matplotlib inline
» plt.plot([0,3,0,5,0,7,0])
» # a figura 1 é gerada

» plt.plot([0,1,2,3,4,5,6],[0,3,0,5,0,7,0])
» # o mesmo que antes (figura 1 é gerada)

» x = np.arange(101)-50
» y = x**2
» plt.plot(x,y)
» # a figura 2 é gerada

Quando apenas uma lista é fornecida plot usa os índices como coordenada horizontal (abcissa) e os valores da lista como coordenadas verticais (ordenadas). Quando duas listas de mesmo tamanho são fornecidas a primeira é usada para os valores das abcissas, a segunda como ordenadas. No segundo exemplo, que gera a figura 2, foram usadas coordenadas (x, x2) (uma parábola) com x variando no intervalo [-50, 50].

Usando %matplotlib notebook o gráfico é exibido inline mas trazendo controles de ajuste da imagem. Ao se clicar no botão azul (pode ter outra cor na sua instalação) os controles desaparecem e a imagem fica estática.

» %matplotlib notebook
» x = np.arange(40)
» plt.plot(x,np.exp(x/10))    # exibe a figura 3

» # outro exemplo: seno
» # dados a plotar 
» x = np.arange(0.0, 2.0, 0.01)
» y = np.sin(2 * np.pi * x)

» fig, ax = plt.subplots()
» ax.plot(x, y)

» ax.set(xlabel='eixo x', ylabel='y = seno(x)', title='Gráfico de seno(2 pi x)')
» ax.grid()

» fig.savefig('seno.jpg')
» plt.show()                  # exibe a figura 4

No primeiro caso plotamos simplesmente o gráfico de y = exp(x/10). No segundo exemplo criamos um array no intervalo [0, 2) em passos de .01. A coordenada é y = sen(2πx). Usamos as funções exponencial e seno do numpy para lidar com a operação vetorializada (que pode ser aplicada sobre todo o array). Já veremos com maiores detalhes os métodos de matplotlib.pyplot.

Hierarquia de objetos do Matplotlib

Mesmo em exemplos simples, como os anteriores, Matplotlib usa uma hierarquia de objetos. Por hierarquia se entende que objetos dependem de outros, como em ramos de uma árvore. Repetindo o gráfico da figura 4 temos:

†: O termo axes do matplotplib não se refere a “eixos” e sim a “figuras” individuais, dentro de uma Figure.

Figure é o objeto básico ou o mais externo de um gráfico. Ele pode conter diversos Axes, que são gráficos ou plotagens individuais. Axes, por sua vêz, podem conter legendas, marcas gráficas, curvas e caixas de texto. Cada um desses elementos são objetos do python com métodos e propriedades que podem ser manipuladas individualmente.

Vamos verificar em código como esses objetos são criados e manipulados.

» # geramos os dados a imprimir
» x = np.arange(0.0, 2.0, 0.01)
» y = np.sin(2 * np.pi * x)

» # O objeto básico do matplotlib.pyplot (aliás plt) é figure
» fig = plt.figure()
» # De figure derivamos um axes (subplot) e plotamos 3 curvas
» ax = fig.add_subplot()

» ax.plot(x, y)
» ax.plot(x, x)
» ax.plot(x, x**2)

» # definimos os labels dos eixos x e y e o título do gráfico
» ax.set(xlabel='eixo x', ylabel='seno(x), x, x^2',
»        title='y = seno, reta e parábola')
» # acrescentamos um quadriculado (grid)
» ax.grid()

» # salvamos a figura no disco
» fig.savefig('seno.jpg')
» # exibimos o resultado
» plt.show()

Ao objeto ax acrescentamos 3 plots (y=sen(2πx), y=x, y=x2), os labels dos eixos, o título do gráfico e o quadriculado de fundo. Opcionalmente a figura pode ser salva. A figura só é exibida quando plt.show() é executado.

O método add_subplot admite diversos parâmetros: fig.add_subplot(m,n,r) significa criar um gráfico em m linhas, n colunas, na posição r. Um subplot significa dividir a região destinada ao gráfico em m×n partes onde se pode colocar sub-gráficos.

Por ex., vamos criar uma figura com 4 subplots, plotando uma curva diferente em cada uma delas.

» x = np.arange(0.0, 2.0, 0.01)-1
» fig = plt.figure()

» ax1 = fig.add_subplot(2,2,1)               # 2 linhas, 2 colunas: a 1ª figura
» ax2 = fig.add_subplot(2,2,2)               # a 2ª figura
» ax3 = fig.add_subplot(2,2,3)
» ax4 = fig.add_subplot(2,2,4)

» ax1.plot(x, np.sin(10*x))
» ax2.plot(x, x)
» ax3.plot(x, x**2)
» ax4.plot(x, x**3)
» fig.savefig('figura5.jpg')

» plt.show()

A mesma figura pode ser obtida fazendo os plots diretamente para os axes:

» fig = plt.figure()
» ax1 = fig.add_subplot(2, 2, 1)
» plt.plot([-1, 0, 1, 2])
» ax2 = fig.add_subplot(2, 2, 2)
» ax3 = fig.add_subplot(2, 2, 3)
» plt.plot(np.random.randn(50).cumsum(), 'k--')
» ax4 = fig.add_subplot(2, 2, 4)
» plt.plot([1.5, 3.5, -2, 1.6])

O método plt.plot(dados) se refere ao eixo ativo que é aquele criado ou usado por último. No caso acima nenhuma figura foi plotada no 2 º retângulo.

O procedimento de criar vários subplots dentro de um mesmo gráfico pode resumido por meio do método
fig, axes = plt.subplots().
subplots() retorna uma tupla onde o 1&orm; elemento é uma Figure, o objeto básico de um plot, e o 2º são os axes que recebem as curvas.

Esses axes podem ser referenciados individualmente pela notação de array. Por ex.: em
fig, axes = plt.subplots(2, 3)
temos axes[0,0] até axes[1,2].

np.random.randn(50).cumsum()
retorna a soma cumulativa dos elementos de um array de 50 elementos “aleatórios”.

pyplot.subplots possui as opções:

nrows número de linhas
ncols número de colunas
sharex todos os subplots devem ter os mesmos “ticks” no eixo x
sharey todos os subplots devem ter os mesmos “ticks” no eixo y
subplot_kw dicionário de chaves para criar cada subplot
**fig_kw chaves adicionais, como plt.subplots(2,2,figsize=(8,6))

Formatação dos gráficos

Tamanho

O tamanho de uma figura, por default dada em polegadas, é definido pelo parâmetro figsize em plt.figure(figsize=(largura, altura)).

» x = np.arange(.1, 10, 0.01)
» largura = 5; altura = 2
» plt.figure(figsize=(largura, altura))
» plt.plot(x, np.log(x))
» plt.show()                       # gerado o gráfico na figura 8

O mesmo gráfico é gerado usando esse parâmetro no construtor da figure, mas desenhado com linhas pontilhadas, devido ao parâmetro ‘k–‘ em ax.plot(x, np.log(x), 'k--').

» x = np.arange(.1, 10, 0.01)
» fig = plt.figure(figsize=(5, 2))
» ax = fig.add_subplot()
» ax.plot(x, np.log(x), 'k--')
» plt.show()                       # gerado o gráfico na figura 9

Espaçamento entre subplots, cores e marcadores

O espaçamento entre figuras de um gráfico com subplots, que por default é um espaço não nulo, pode ser ajustado por meio do método Figure.subplots_adjust(). Por conveniência o mesmo método pode ser acessado diretamente pela função:
subplots_adjust((left=None, bottom=None, right=None, top=None, wspace=None, hspace=None).
wspace e hspace indica quanto espaço percentual em relação à largura e altura da figura, respectivamente.
Por exemplo, para juntar os subplots fazemos ambos igual a zero.

» x = np.arange(.1, 10, 0.01)
» fig, axes = plt.subplots(2, 2)
» axes[0, 0].hist(np.random.randn(1000), bins=100, alpha=.5)
» axes[0, 0].hist(np.random.randn(500), bins=50, color='r', alpha=.5)
» axes[0, 1].hist(np.random.randn(500), bins=50, color='gold', alpha=1)
» axes[1, 0].hist(np.random.randn(500), bins=50, color='#ff0000')
» axes[1, 1].plot(x, 3*np.log(x), color='#55aaff')
» axes[1, 1].plot(x, x, color='#000000')
» plt.subplots_adjust(wspace=0, hspace=0)


Para usar os mesmos eixos em todos os 4 gráficos usamos os parâmetros sharex, sharey, em
fig, axes = plt.subplots(2, 2), sharex=True, sharey=True).

Em hist(np.random.randn(1000), bins=100) traçamos o histograma de dados aletórios (100 números) separados em 100 bins. No 1º axes traçamos 2 histogramas, o 1º com a cor azul default, o 2º com color=’r’, um atalho para ‘red’ ou vermelho, com transparência de 50%, alpha=.5. Uma lista de cores nomeadas, como color=’gold’ pode ser encontrada no site do matplotlib. Também podemos usar o código de cores html que consiste em 3 números hexadecimais de 0 até 255 (ou 00 até ff em hexadecimal), no sistema rgb (vermelho, verde, azul). Diversos editores de imagens ou de código disponilizam um seletor de cores que retorna a cor nesse sistema. O site HTML-COLOR.CODES também tem um seletor online.


O método axes.plot(), além de aceitar arrays para abcissas e coordenadas, pode receber também o string especificador de cor e tipo de linha. Para imprimir em verde (‘green’) com linha tracejada usamos
ax.plot(x, y, 'g--'),
que é uma forma resumida de passar parâmetros. Isso é o mesmo que:
ax.plot(x, y, linestyle='--', color='g').
Considerando que x e y são arrays de mesmo tamanho, alguns exemplos desses parâmetros são:

» plot(x, y)            # plot x, y com linha e estilo default
» plot(x, y, 'bo')      # plot x, y com marcadores azuis circulares
» plot(x, y, 'rv')      # plot x, y com marcadores vermelhos, triângulo para baixo
» plot(y)               # plot y usando seus índices como coordenadas-x
» plot(y, 'r+')         # idem, usando cruzes vermelhas

No Jupyter Notebook use plt.plot? para ver uma lista completa dos parâmetros desse método.

Ao desenhar um gráfico pode ser interessante marcar os pontos sobre as curvas contínuas. Isso é feito com markers ou marcadores.

» from numpy.random import randn
» plt.plot(randn(30).cumsum(), color='green', linestyle='dashed', marker='o')

Uma forma abreviada para o mesmo comando é: plt.plot(randn(30).cumsum(), 'go--'), onde os parâmetros são passados em uma única string, com g para green (verde), o para o marcador circular e -- para o estilo de linha tracejado.

Por default os pontos de um plot são ligados por linhas. Para outro estilo usamos drawstyle:

» data = np.random.randn(20).cumsum()
» plt.plot(data, 'b--', label='default', marker='v')                      # linha azul
» plt.plot(data, 'r-', drawstyle='steps-post', label='passos')            # linha vermelha
» plt.legend(loc='best')


A linha azul é tracejada (‘b–‘), no estilo default e com marcadores ‘v’ (triângulos). O parâmetro label cria legendas, nesse caso indicando o texto ‘defaul’. A linha vermelha tem estilo drawstyle=’steps-post’ (em passos) e é marcada na legenda como ‘passos’. plt.legend(loc='best') informa que o melhor local para colocar essa legenda seja encontrado automaticamente. Outros valores seriam: loc='right', 'center', 'upper right', etc.

Marcas, etiquetas e legendas (ticks, labels, legends)

» dados = np.random.randn(1000)
» cumulativo = dados.cumsum()

» fig = plt.figure()
» ax = fig.add_subplot(1,1,1)
» ax.plot(10*dados + 10)
» ax.plot(cumulativo)


As duas plotagens são sobrepostas no único axes criado. O operação 10*dados+10 serve apenas para efeito estético da apresentação dos dados aleatórios.

Vamos usar os mesmos dados para verificar as propriedades de ajuste do título global do gráfico, labels nos eixos x e y, label do gráfico e ticks para melhorar a apresentação do gráfico anterior.

» fig = plt.figure()
» ax = fig.add_subplot(1,1,1)
» ax.set_title('Alterando eixos com matplotlib')
» ticks = ax.set_xticks([0, 250, 500, 750, 1000])
» ticks = ax.set_yticks([-20, 0, 20, 40, 60, 80])

» labels = ax.set_xticklabels(['seg' ,'ter', 'qua', 'qui', 'sex'],
»                             rotation=45, fontsize='large')
» labels = ax.set_yticklabels(['-A' ,'O', 'A', 'B', 'C','D'],
»                             fontsize='large')

» ax.set_xlabel('Ao longo dos dias...')
» ax.set_ylabel('Observado')

» ax.plot(10*dados, color='#55aaff', alpha=.5, label='dados')
» ax.plot(cumulativo, color='red', label='cumulativo')
» ax.legend(loc='best')

Anotações e desenhos nos subplots.

Diversos tipos de anotações, setas e desenhos podem ser incluídos nos gráficos. Para traçar os gráficos seguintes vamos usar o arquivo .csv do Our World in Data, baixados para a pasta ./dados.

Primeiro importamos o arquivo baixado .csv para um dataframe. Esse arquivo contém dados dos países do mundo, nos anos de 1950 até 2099, contendo número de nascimentos verificados até 2020 e valores interpolados para os anos seguintes. O dataframe original tem o seguinte formato:

Em seguida selecionamos apenas os dados sobre o Brasil.

» dados = pd.read_csv('./dados/number-of-births-per-year.csv')
» dados=dados[(dados['Entity']=='Brazil')]
» dados.head(2)

» # as colunas 3 e 4 têm nomes longos, que vamos renomear
» dados.columns[3],dados.columns[4]
↳ ('Estimates, 1950 - 2020: Annually interpolated demographic indicators - Births (thousands)',
↳  'Medium fertility variant, 2020 - 2099: Annually interpolated demographic indicators - Births (thousands)')

» dados.rename(columns={'Year':'ano',
»                       dados.columns[3]:'nasc',
»                       dados.columns[4]:'inter'}, inplace=True)
» # copiamos os dados da coluna de interpolação, após 2020, para a colunas de nascimentos
» dados.loc[dados['nasc'].isna(), 'nasc'] = dados['inter']

» # vamos mantes apenas as colunas 'ano' e 'nasc'
» dados = dados[['ano', 'nasc']]
» dados.head()
↳         ano          nasc
↳ 4050    1950    2439820.0
↳ 4051    1951    2467186.0
↳ 4052    1952    2523577.0
↳ 4053    1953    2583285.0
↳ 4054    1954    2646311.0

Para usar como anotações no gráfico encontramos os anos em que  nascimentos foram máximo e mínimo, além do ano em que se inicia a interpolação, 2020.

» maior=dados[dados['nasc']==dados['nasc'].max()]
» menor=dados[dados['nasc']==dados['nasc'].min()]

» ano_maior = maior['ano'].values[0]
» nasc_maior = int(maior['nasc'].values[0])

» ano_menor = menor['ano'].values[0]
» nasc_menor = int(menor['nasc'].values[0])

» interX = 2020   # início da interpolação
» interY = int(dados[dados['ano']==2020]['nasc'].values[0])

» txt = ('Máximo de nascimentos:\t {} no ano {}.\n'
»         'Mínimo de nascimentos:\t {} no ano {}.\n'
»         'Início da interpolação:\t {} no ano {}.'
»       )

» print(txt.format(nasc_maior, ano_maior,nasc_menor, ano_menor, interY, interX))
↳ Máximo de nascimentos:	 3929646 no ano 1983.
↳ Mínimo de nascimentos:	 1504597 no ano 2099.
↳ Início da interpolação:	 2859135 no ano 2020.

Com esses dados imprimimos o gráfico (sem muita preocupação estética). Uma primeira curva é traçada em preto, incluindo os anos de 1950 até 2020. A segunda curve se inicia em 2021 até o final e é tracejada em vermelho, para indicar a interpolação. Uma terceira curva tem efeito decorativo, em azul e transparente.

» fig = plt.figure()
» ax = fig.add_subplot()
» ax.set_title('Nascimentos (em milhões), por ano', size=18)
» ax.plot(dados[dados['ano']<2021]['ano'], dados[dados['ano']<2021]['nasc'], color='black', alpha=1)
» ax.plot(dados[dados['ano']>2020]['ano'], dados[dados['ano']>2020]['nasc'], 'r--')
» ax.plot(dados['ano'], dados['nasc'], 'b', linewidth=5, alpha=.2)
» ax.grid(color='grey', alpha=.3 )
» ax.annotate('Máximo', xy=(ano_maior, nasc_maior), size=13)
» ax.annotate('Mínimo', xy=(ano_menor, nasc_menor), size=13)
» ax.annotate('Interpolado', xy=(interX, interY), size=13)
» ax.arrow(ano_maior-10, nasc_maior, 10, 0)
» ax.arrow(interX-10, interY-10, 10, 10)            # a figura 17 abaixo é plotada


Para dar um zoom na figura podemos limitar as faixas de valores no eixo x e eixo y com ax.set_xlim(a,b) e ax.set_xlim(m,n), respectivamente, em torno do ponto de interesse. No exemplo fazemos um zoom em torno do ponto de máximo, obtendo o gráfico 18 acima.

» ax.set_xlim([1980, 1990])
» ax.set_ylim([3.76e6, 3.95e6])
» fig.get_figure()                                  # a figura 18 acima é plotada

Figuras sobre o plot

Diversas formas mais comuns estão disponíveis para inserção nos plots, e são chamadas de patches no matplotlib. Algumas delas estão diretamente em matplotlib.pyplot como retângulos, círculos e polígonos. Muitas outras estão em matplotlib.patches. Para traçar figuras construimos os patches com os métodos apropriados e os acrescentamos ao subplot usando ax.add_patch().

retângulo: plt.Rectangle((x, y), largura, altura), onde (x, y) são as coordenadas do ponto inferior esquerdo,
círculo: plt.Circle((x_0, y_0), raio), onde (x_0, y_0) são as coordenadas do centro,
polígono: plt.Polygon([[x_0, y_0], [x_1>, y_1],…, [x_n, y_n]).

No caso do polígono a área interna às retas que ligam os pontos é colorida.

» fig = plt.figure()
» ax = fig.add_subplot()
» retangulo = plt.Rectangle((0.2, 0.2), 0.6, 0.4, color='#aabbcc')
» circulo = plt.Circle((0.4, 0.6), 0.3, color='plum', alpha=0.3)
» poligono1 = plt.Polygon([[0.1, 0.1], [0.8, 0.7], [.3,.7], [0.6, 0.1]], color='turquoise', alpha=0.8)
» poligono2 = plt.Polygon([[0.2, 0.2], [0.8, 0.8]], color='red', alpha=0.8)
» ax.add_patch(retangulo)
» ax.add_patch(circulo)
» ax.add_patch(poligono1)
» ax.add_patch(poligono2)

Para o segundo “polígono” apenas dois pontos foram fornecidos e ele é representado pela reta (vermelha) que liga esses pontos.

Claro que gráficos mais elaborados podem ser montados com uma combinação de figuras como retas, polígonos, círculos, etc. No caso abaixo uma cor é escolhida “aleatoriamente” para plotar um série de 2 triângulos, um com um vértice em (0,1), outro em (1,0). Os dois outros vértices dos triângulos são coincidentes, e se deslocam sobre a reta (t,t) com t de 0 a 1, com espaçamento .1. A cor tem transparência alpha=.6 para que as cores apareçam em tons pastéis.

A função cor() retorna uma tupla (r,b,g) onde cada componente representa as cores vermelho, verde e azul, com valores de 0 até 1.

» def cor():
»     return (np.random.random(), np.random.random(), np.random.random())

» fig = plt.figure()
» ax = fig.add_subplot()
» for t in np.linspace(0,1,100):
»     c = cor()
»     poligono1 = plt.Polygon([[0, 1], [t, t], [t+.1, t+.1]], color=c, alpha=.6)
»     poligono2 = plt.Polygon([[1, 0], [t, t], [t+.1, t+.1]], color=c, alpha=.6)
»     ax.add_patch(poligono1)
»     ax.add_patch(poligono2)                       # a figura 20 é gerada


O código abaixo gera círculos de raios aleatórios, espalhados em torno da reta (t,t), afastados dela por uma variacão também aleatória.

» fig = plt.figure()
» ax = fig.add_subplot()
» plt.axis('equal')
» for t in np.linspace(0,1,100):
»     circulo = plt.Circle((t*np.random.random(), t*np.random.random()), np.random.random()/10, color=cor(), alpha=0.5)
»     ax.add_patch(circulo)
» ax.set_xlim([0, 1])
» ax.set_ylim([0, 1])
» plt.savefig('circulos.pdf')                       # a figura 21 é gerada

Ao final a figura gerada é gravada em disco com o formato “pdf”. Outros formatos podem ser escolhidos, como “jpeg”, “png”, “svg”, assim como a resolução em dots per inches, (dpi ), que tem default = 100, além da cor de fundo e bordas.

Configuração do matplotlib

Por padrão o matplotlib possui um esquema de cores e outros parâmetros, como largura e tipo de linhas, previamente definidos e voltados para plotar figuras prontas para publicação. No entanto, vários destes parâmetros podem ser personalizados através de ajustes nos valores globais tais como tamanho, espaçamento entre subplots, cores, família e tamanhos de fonte, estilos de grade, etc.

Uma forma de alterar esses padrões está no método plt.rc (parametro, opcoes) onde parametro é uma string com o nome do parâmetro que se quer modificar, e opcoes é uma sequência de argumentos de palavras-chaves com os novos valores.

Entre outras opções parametro pode ser figure, axis, xtick, ytick, grid, legend. As opções podem ser passadas de várias formas. O exemplo mostra como ajustar todas as figuras de uma sessão para o tamanho 20×15. Depois fazemos ajustes às fontes, usando um dicionário.

» # ajustar tamanho da figura
» plt.rc ('figure', figsize = (20, 15))

» # parâmetros associados às fontes, em um dicionário
» font_options = {'family' : 'monospace', 'weight' : 'bold', 'size' : 'small'}
» plt.rc ('font', **font_options)

Uma personalização mais ampla pode ser feita no arquivo de configurações. Para encontrar esse arquivo use os comandos:

» # no prompt do sistema digite
» python -c "import matplotlib; print(matplotlib.matplotlib_fname())"

» # de dentro do jupyter notebook (ou de qualquer ambiente em que você trabalhe):
» import matplotlib
» print(matplotlib.matplotlib_fname())
↳ /home/usuario/.anaconda3/lib/python3.8/site-packages/matplotlib/mpl-data/matplotlibrc

Esse output é relativo ao sistema e à distribuição que está sendo usada, lembrando que é possível existir mais de uma instalação em um computador. No caso mostrado está em uso o anaconda e jupyter no linux mint. Cada usuário pode encontrar um local diferente. Esse arquivo não deve ser editado diretamente mas copiado para a pasta home do usuário (no linux) com o nome .matplotlibrc. Desta forma ele será carregado durante a inicialização do pacote. A análise desse arquivo é uma boa forma de se conhecer as possibilidades na personalização, sendo que as opções estão comentadas.

As atuais configurações globais podem ser vistas com o comando

» import matplotlib as mpl
» print(mpl.rcParams)

matplotlib.rcParams é usado para alterar esses parâmetros, um de cada vez. matplotlib.rc pode alterar os valores default para vários parâmetros de um grupo específico, como tipos de lihnes, fontes, textos, etc.

» matplotlib.rcParams['lines.markersize'] = 20
» matplotlib.rcParams['font.size'] = '15.0'

matplotlib.rcdefaults() reseta todos os parâmetros para seus valores originais.

Usando matplotlib com o pandas

A própria biblioteca do pandas embute diversas funcionalidades do matplotlib, sem que esse tenha que ser carregado explicitamente. Isso significa que podemos criar gráficos sem passar por todas as etapas de sua construção.

Por exemplo, uma instância de Series possui o método series.plot().

» s1 = pd.Series(np.random.randn(100).cumsum())
» s1.plot(use_index=False)                          # grafico 22-a é plotado

» s2 = pd.Series([x**2 for x in np.arange(-10,10,.1)], index=np.arange(-10,10,.1))
» s2.plot()                                         # grafico 22-b é plotado


Na figura 21 o eixo x recebe valores dos índices da série, que por default vai de 0 até 99. Na segunda o índice que foi declarado é usado. Para evitar o procedimento de usar o índice como ordenada passamos o parâmetro series.plot(use_index=False).

Para um dataframe cada series correspondente a cada coluna é plotada separadamente. Abaixo construimos e plotamos um dataframe de quatro colunas, cada uma delas representando valores de um seno com frequências diferentes.

» s1 = pd.Series([np.sin(x) for x in np.arange(0,10,.1)])
» s2 = pd.Series([np.sin(2*x) for x in np.arange(0,10,.1)])
» s3 = pd.Series([np.sin(3*x) for x in np.arange(0,10,.1)])
» s4 = pd.Series([np.sin(4*x) for x in np.arange(0,10,.1)])

» df=pd.concat({'A': s1, 'B': s2, 'C': s3, 'D': s4} , axis=1)

» df.plot()                                                        # a figura 23 é plotada
» df.plot(color=['k','r','b','y'], alpha=.6, logx=True, grid=True) # a figura 24 é plotada


O dataframe df contém 4 colunas, cada uma com os valores de seno(πx), seno(2πx), seno(3πx), seno(4πx), com x variando de 0 a 10 em passos de 0,1. No segundo gráfico, figura 24, alguns parâmetros foram passados, como uma lista de cores, a existência de quadriculado (grid) e a instrução para usar uma escala logarítmica em x.

A instrução dataframe.plot() é um atalho para dataframe.plot.line() que representa como curvas os pontos passados. Outros parâmetros podem ser passados, exatamente como no uso direto de matplotlib:

Argumento Descrição
label texto para a legenda
ax objeto subplot do matplotlib onde plotar. Se vazio os plots vão para o subplot ativo
style string de estilo, como ‘ko–‘, passado para o matplotlib
alpha opacidade do plot (de 0 to 1)
kind opções: ‘area’, ‘bar’, ‘barh’, ‘density’, ‘hist’, ‘kde’, ‘line’, ‘pie’
logy use escala logaritmica no eixo y
use_index use o index para os labels de x
rot rotação de texto nos labels (0 até 360)
xticks valores a usar para marcas no eixo x
yticks valores a usar para marcas no eixo y
xlim limites para o eixo x (ex.: [0, 10])
ylim limites para o eixo y
grid exibir grade quadriculada de fundo (grid), default=exibir

Alguns parâmetros são específicos para dataframes.

Argumento Descrição
subplots bool. Plota cada coluna em um subplot separado
sharex se subplots=True, use o mesmo eixo x, com marcas e limites
sharey se subplots=True, use o mesmo eixo y, com marcas e limites
figsize tupla indicando tamanho da figura
title texto para o título
legend bool. Inclui legenda do subplot (default=True)
sort_columns plot colunas em ordem alfabética no nome; default= ordem no dataframe

Plotagem em barras

A plotagem em barras (bar plots ) pode ser feita com plot.bar() e plot.barh() (com barras verticais e horizontais).

Para experimentar com esses tipos de plotagens vamos usar os mesmos dados importados anteriormente, que contém uma lista de países com o número de nascimentos por ano de 1950 até 2020, e a estimativa à partir de 2021. Importamos o arquivo .csv para um dataframe e selecionamos apenas as linhas relativas ao ano de 2020. Linhas relativas à continentes e outras partes do mundo que não países possuem coluna Code = NaN e são excluídas. Renomeamos as colunas para mais fácil manuseio e mantemos apenas as colunas relativas ao país, ano e número de nascimentos.

» # importação do csv em um dataframe
» dados = pd.read_csv('./dados/number-of-births-per-year.csv')
» # selecão do ano = 2020 e apenas países
» dados = dados[(dados['Code'].notnull()) & (dados['Year']==2020)]
» # renomeando colunas
» dados = dados.rename(columns={'Entity':'país','Year':'ano', dados.columns[3]:'nasc'})
» # mantendo apenas colunas relevantes
» dados = dados[['país', 'nasc']]

» # use a coluna 'país' como índice
» dados.set_index('país', inplace=True)

» # o dataframe final:
» dados.head(4)
↳                      nasc
↳        país
↳ Afghanistan     1215628.0
↳     Albania       32888.0
↳     Algeria      995368.0
↳      Angola     1311356.0

Imprimimos os 2 tipos de barplot com o código abaixo, usando 10 países para as barras verticais e 20 para as horizontais. Para isso inicializamos uma figura com 1 linha e 2 colunas. O parâmetro figsize=(15, 16) indica que nossa figura terá a largura 15 e altura 6 (em polegadas). Dados os nomes longos de países o gráfico ficou sobreposto, o que seria controlado aumentando-se a separação entre axes.

» fig, axes = plt.subplots(2, 1, figsize=(10, 10))
» dados[:10].plot.bar(ax=axes[0], color=['r','b','g'], rot=30, grid=True)
» dados[:20].plot.barh(ax=axes[1], color='g', alpha=0.7, grid=True)


Gráficos desse tipo são desenhados para series e dataframes com apenas uma coluna. Se o dataframe possui várias colunas o gráfico de barras plota uma barra para cada coluna.

Para o próximo exemplo usaremos os dados disponibilizados no GapMinder, já usados e descritos nesse site. Desses dados manteremos apenas as colunas que renomearemos como “pais”, “ano” e “pop” (população), ficando com um dataframe com os países de mundo e suas populações nos anos listados abaixo.

Dessa coleta de dado separamos apenas os 5 países com maior população no último ano, 2007.

» # baixar dados do gapminder
» url =(
»       'https://raw.githubusercontent.com/jennybc/'
»       'gapminder/master/data-raw/08_gap-every-five-years.tsv'
»        )

» # criamos o dataframe dfPaises. O arquivo importado tem campos separados por tabs
» dfPaises = pd.read_csv(url, sep='\t')

» # para ver colunas e forma geral usamos
» dfPaises.head(2)
↳         country   continent    year    lifeExp        pop     gdpPercap
↳ 0   Afghanistan        Asia    1952     28.801    8425333    779.445314
↳ 1   Afghanistan        Asia    1957     30.332    9240934    820.853030

» # usamos apenas 3 colunas
» dfPaises = dfPaises[['country','year','pop']]
» # e as renomeamos
» dfPaises.rename(columns={'country':'pais', 'year':'ano'}, inplace=True)

» # o resultado é
» dfPaises
↳             pais     ano        pop
↳ 0    Afghanistan    1952    8425333
↳ 1    Afghanistan    1957    9240934
↳ 2    Afghanistan    1962   10267083

» # para ver os anos registrados examinamos o conjunto (set)
» set(dfPaises['ano'])
↳ {1952, 1957, 1962, 1967, 1972, 1977, 1982, 1987, 1992, 1997, 2002, 2007}

» # países mais populosos, em 2007
» dfPaises[dfPaises['ano']==2007] \
»          .sort_values(by=['pop'], axis=0, \
»           ascending=False, inplace=False)['pais'] \
»          .head(5)
↳ 299             China
↳ 707             India
↳ 1619    United States
↳ 719         Indonesia
↳ 179            Brazil

O último comando, para selecionar os países mais populosos, está quebrada pelo caracter \ (back slash) que é ignorado (a linha é executada por inteiro). Essa linha pode ser compreendida assim:

dfPaises[dfPaises['ano']==2007]                         : seleção só de linhas com ano = 2007
.sort_values(by=['pop'], axis=0, ascending=False)       : ordena pela coluna 'pop' em ordem inversa
['pais'].head(5)                                        : só a coluna 'pais', 5 primeiros valores

Em seguida montamos um dataframe para cada desses países e os concatenamos para um dataframe mais geral que contém linhas indexadas pela ano e colunas com o nome do país. (Outras técnicas de agrupamento serão vistas mais tarde.)

» china = dfPaises[dfPaises['pais']=='China'][['ano', 'pop']]\
»         .set_index('ano').rename(columns={'pop':'china'})
» india = dfPaises[dfPaises['pais']=='India'][['ano', 'pop']]\
»         .set_index('ano').rename(columns={'pop':'india'})
» usa = dfPaises[dfPaises['pais']=='United States'][['ano', 'pop']]\
»       .set_index('ano').rename(columns={'pop':'usa'})
» indonesia = dfPaises[dfPaises['pais']=='Indonesia'][['ano', 'pop']]\
»             .set_index('ano').rename(columns={'pop':'indonesia'})
» brasil = dfPaises[dfPaises['pais']=='Brazil'][['ano', 'pop']]\
»          .set_index('ano').rename(columns={'pop':'brasil'})

As linhas de seleção de dados do país podem ser compreendidas assim:

  dfPaises[dfPaises['pais']=='Brazil']      : seleciona apenas linhas relativas ao país 'Brazil'
  [['ano', 'pop']]                          : desse df copia apenas as colunas 'ano' e 'pop'
  .set_index('ano')                         : use a coluna 'ano' como índice
  .rename(columns={'pop':'brasil'})         : renomeie coluna 'pop' para 'brasil'
» # os paises são concatenados em um único df
» df = pd.concat([china, india, usa, indonesia, brasil],  axis=1)
» # o nome da lista de colunas será usado no plot
» df.columns.name = 'População'

» # e o resultado é
» df.head(3)
↳ População     china        india          usa   indonesia      brasil
↳ ano
↳ 1952      556263527    372000000    157553000    82052000    56602560
↳ 1957      637408000    409000000    171984000    90124000    65551171
↳ 1962      665770000    454000000    186538000    99028000    76039390

» # esse dataframe pode ser exibido em gráfico de barras
» cor = ['salmon','gold','teal','plum','powderblue']
» df.plot.bar(figsize=(10,5), grid=True, color = cor, title='Países mais populosos (de 1952 até 2007)', rot=45)

Na plotagem acima uma paleta de cores foi passada para o parâmetro color. Cada uma delas é usada para um país. O nome das colunas, df.columns.name = 'População' é usado como título da legenda.

Para gerar gráficos de barras empilhadas (stacked bar ) passamos o valor stacked=True. Nos exemplos plotamos a versão horizontal e vertical do mesmo gráfico acima.

» df.plot.barh(stacked=True, alpha=.7)         # a figura 27 é plotada

» df.plot.bar(stacked=True, alpha=.7)          # a figura 28 é plotada

Histogramas

Um histograma é uma representação gráfica, similar a um gráfico de barras, de uma distribuição de pontos. Os pontos são distribuídos em faixas igualmente divididas e o gráfico é o conjunto de retângulos com base de tamanho igual à largura das faixas e altura correspondente ao número de pontos em cada faixa.

Criamos uma série com 1000 números aleatórios, multiplicados por 100. O resultado é, no caso dessa execução, um conjunto distribuído entre -315 e 325 (aproximadamente). A partir desses dados traçamos o histograma e o gráfico de densidade ou density plot.

» ser = pd.Series((np.random.randn(1000)*100))
» ser.describe()
↳ count    1000.000000
↳ mean        3.747373
↳ std       102.637489
↳ min      -314.443835
↳ 25%       -65.343082
↳ 50%         4.835384
↳ 75%        73.314604
↳ max       324.011000

» ser.plot.hist(bins=10, grid=True, color='b', alpha=.4) # figura 19

» ser.plot.kde() # o mesmo que ser.plot.density()        # figura 30


O gráfico de densidade consiste na plotagem de uma função de distribuição de probabilidade que poderia ter gerado os dados na series. A técnica usual consiste em usar uma mistura de “núcleos” ou “kernels”. Esses gráficos são também chamados de estimativa de núcleos de densidade (kernel density estimate, KDE ).

Seaborn

Seaborn é outra biblioteca do Python voltada para a visualização de dados, baseada no matplotlib. Ela apresenta uma interface de mais alto nível e aprimoramento da qualidade estética dos gráficos. Com o Seaborn se pode conseguir gráficos bem elaborados e de boa aparência com um número menor de linhas de código.

Para os exemplos com o seaborn vamos usar os dados do Gapminder já descritos. O dataframe importado tem 1704 linhas com dados sobre os países, 6 colunas ‘country’, ‘continent’, ‘year’, ‘lifeExp’, ‘pop’, ‘gdpPercap’, respectivamente ‘pais’, ‘continente’, ‘ano’, ‘Expectativa de vida’, ‘população’, ‘PIB percapita’.

» url =(
»       'https://raw.githubusercontent.com/jennybc/'
»       'gapminder/master/data-raw/08_gap-every-five-years.tsv'
»        )
» # criamos o dataframe dfPaises. O arquivo importado tem campos separados por tabs
» dfPaises = pd.read_csv(url, sep='\t')

» # para restringir o volume dos dados armazenamos as fatias
» df2007 = dfPaises[dfPaises['year']==2007]
» dfBrasil = dfPaises[dfPaises['country']=='Brazil']

O seaborn deve ser importado. Para uma gráfico de barras mais simples informamos e base de dados e os nomes de colunas a usadas como valores para os eixos. Para não congestionar o gráfico usamos apenas os 10 primeiros países.

» import seaborn as sns
» sns.barplot(data=df2007[:10], x='lifeExp', y='gdpPercap')
» # é plotada a figura 31 abaixo

Muitas configuarações podem ser aplicadas sobre esse gráfico básico. Algumas são usadas abaixo, como a orientação das barras, textos e rotação nos eixos x e y.

» sns.set_style('darkgrid')
» graf = sns.barplot(data=df2007[:10], x='lifeExp', y='gdpPercap', orient='h')
» graf.set(xlabel = 'Expectativa de vida (anos)',
                     ylabel = 'PIB percapita', title ='Expectativa de vida x PIB')
» graf.set_xticklabels(labels=df2007[:10]['lifeExp'].round(1))
» graf.set_yticklabels(labels=df2007[:10]['country'], rotation=30)
» # é plotada a figura 32 acima

Valores válidos para sns.set_style() são 'white', 'dark', 'whitegrid', 'darkgrid', 'ticks'.

Esses gráficos plotados não sugerem qualquer relação entre a renda percapita e a expectativa de vida, o que é natural uma vez que escolhemos apenas os primeiros 10 países, em ordem alfabética. Claro que barplots não são apropriados para exibir um número muito grande de dados. Para isso podemos usar seaborn.regplot que plota o gráficos de dispersão (scatter plots ) e uma reta correspondente a um ajuste do modelo de regressão linear. Esse último gráfico mostra que existe correlação entre expectativa de vida e renda percapita.

» sns.regplot(x='lifeExp', y='gdpPercap', data=df2007) # figura 33


É comum em análise de dados que se queira ter uma visão geral de relacionamentos entre as variáveis (ou colunas de um dataframe. Para isso um pairplot faz o cruzamento entre todas as variáveis. O método seaborn.pairplot(), por default, cria uma matriz de Axes comparando aos pares as variáveis numéricas do dataframe usado como fonte de dados. Na diagonal dessa matriz uma distribuição univariada é exibida para mostrar a distribuição dos dados em cada coluna.

» sns.pairplot(df2007[['gdpPercap', 'lifeExp']],
»              diag_kind='kde', plot_kws={'color':'r','alpha': .9})


O parâmetro plot_kws recebe um dicionário de propriedades com valores.

Outro método é seaborn.catplot() que traz diversas funcionalidades para representar relações entre variáveis numéricas ou categóricas. Para uma amostra criamos um dataframe com valores de uma parábola e um seno.

» dfGraf=pd.DataFrame(np.arange(20), columns = ['x'])
» dfGraf['quadrado']=dfGraf['x']**2
» dfGraf['seno']=np.sin(dfGraf['x'])

» sns.catplot(x='x', y='quadrado', kind='strip', data=dfGraf)      # plota a figura 35
» sns.catplot(x='x', y='seno', kind='bar', data=dfGraf)            # plota a figura 36

Bibliografia

Todos os sites acessados em setembro de 2021.

Consulte bibliografia completa em Pandas, Introdução neste site.

Dataframes: multi-índices e concatenção

Índices Hierárquicos

É possível criar series e dataframes com índices e subíndices. Esse processo de indexação hierárquica é importante para a reformatação (reshaping ), formação de tabelas pivot e outras operações de agrupamento de dados.

» import pandas as pd
» import numpy as np

» # formamos uma series com índices duplos 
» sr = pd.Series([11, 12, 21, 22, 23, 31, 32, 41, 42],
                 index=[['A', 'A', 'B', 'B', 'B', 'C', 'C', 'D', 'D'],
                 [1, 2, 1, 2, 3, 1, 2, 1, 2]])
» sr
↳ A  1    11
     2    12
  B  1    21
     2    22
     3    23
  C  1    31
     2    32
  D  1    41
     2    42

» # essa series possui índices
» sr.index
↳ MultiIndex([('A', 1), ('A', 2),
              ('B', 1), ('B', 2), ('B', 3),
              ('C', 1), ('C', 2),
              ('D', 1), ('D', 2)],)

» # da mesma forma podemos transformar essa series um um dataframe
» df = pd.DataFrame(sr)

» # Os índices do dataframe são os mesmos: df.index

» # o índice B corresponde à 3 linhas
» df.loc['B']
↳        0
  1     21
  2     22
  3     23

» df.loc['B'].loc[2]
↳ 0    22

» # idem para a series
» sr['C']
↳ 1    31
  2    32

» sr['C'][1]
↳ 31

» # podemos listar as linhas de 'A' até 'C'
» sr['A':'C']
↳ A  1    11
     2    12
  B  1    21
     2    22
     3    23
  C  1    31
     2    32

» # ou as linhas correspondentes à 'A' e 'C'
» sr.loc[['A','C']]
↳ A  1    11
     2    12
  C  1    31
     2    32

» # seleção pelo índice interno pode feita diretamente
» sr.loc[:, 2]
↳ A    12
  B    22
  C    32
  D    42

» sr.loc[:, 3]
↳ B    23

stack() e unstack()

Os dados de uma series com índices hierárquicos podem ser rearranjados em um DataFrame com o uso de método Series.unstack(). Os índices internos se tornam nomes das colunas. Valores não existentes, como o correspondende aos índices A, 3, são preenchidos com NaN.

» df = sr.unstack()
» df
↳          1         2       3
  A     11.0     12.0      NaN
  B     21.0     22.0     23.0
  C     31.0     32.0      NaN
  D     41.0     42.0      NaN

» # para retornar à uma series
» df.unstack()
↳ 1  A    11.0
     B    21.0
     C    31.0
     D    41.0
  2  A    12.0
     B    22.0
     C    32.0
     D    42.0
  3  A     NaN
     B    23.0
     C     NaN
     D     NaN

No processo de desempilhar o dataframe (unstack ) os nomes das colunas foram usados como índices primários.

Um dataframe pode ter índices hierarquizados para linhas e colunas.

» clima = np.array([[25,20,30],[20,16,15],[15,25,27],[40,60,78]])
» dfClima = pd.DataFrame(clima,
»                       index=[['Temperatura','Temperatura','Umidade','Umidade'],
»                              ['dia','noite','dia','noite']],
»                       columns=[['Paraná','Paraná','Amazonas'],['Cascavel','Curitiba','Manaus']]
»                       )

» # inserindo nomes para as linhas e colunas
» dfClima.index.names = ['Característica', 'D/N']        # D/N = dia/noite
» dfClima.columns.names = ['Estado', 'Cidade']

» # o resultado é
» dfClima
↳                  Estado                   Paraná     Amazonas
                   Cidade     Cascavel     Curitiba      Manaus
  Característica      D/N             
  Temperatura         dia           25           20          30
                    noite           20           16          15
  Umidade             dia           15           25          27
                    noite           40           60          78

Se o processo de criação de dataframes com os mesmos índices será repetido várias vezes ,pode ser útil definir previamente os objetos multindexes.

» colunas = pd.MultiIndex.from_arrays([['Paraná', 'Paraná', 'Amazonas'],
»                                      ['Cascavel', 'Curitiba', 'Manaus']],
»                                      names=['Estado', 'Cidade'])

» linhas = pd.MultiIndex.from_arrays([['Temperatura','Temperatura','Umidade','Umidade'],
»                                     ['dia','noite','dia','noite']],
»                                     names=['Característica', 'D/N'])

» linhas
↳ MultiIndex([('Temperatura',   'dia'),
              ('Temperatura', 'noite'),
              (    'Umidade',   'dia'),
              (    'Umidade', 'noite')],
             names=['Característica', 'D/N'])

» pd.DataFrame(clima, index=linhas, columns=colunas)
↳                 Estado     Paraná             Amazonas
                  Cidade   Cascavel    Curitiba   Manaus
  Característica     D/N
  Temperatura        dia         25          20       30
                   noite         20          16       15
  Umidade            dia         15          25       27
                   noite         40          60       78

swaplevel() e groupby()

O ordenamento dos níveis nos dataframes pode ser alterado com o método dataframe.swaplevel(indice1, indice2). Índices primários podem ser permutados com índice secundários. Com dataframe.sort_index(level=n) podemos ordenar as linhas do dataframe segundo os nomes dos índices do nível n.

» dfClima.swaplevel('D/N', 'Característica')
↳                 Estado     Paraná               Amazonas
                  Cidade   Cascavel    Curitiba     Manaus
  D/N     Característica
  dia        Temperatura         25          20         30
  noite      Temperatura         20          16         15
  dia            Umidade         15          25         27
  noite          Umidade         40          60         78

» # ordenando as linhas pelos labels do nível 1 (D/N)
» dfClima.sort_index(level=1)

↳                  Estado                    Paraná    Amazonas
                   Cidade     Cascavel     Curitiba      Manaus
  Característica      D/N
  Temperatura         dia           25           20          30
  Umidade             dia           15           25          27
  Temperatura       noite           20           16          15
  Umidade           noite           40           60          78

» # alternativamente podemos inverter a ordem dos níveis e ordenar pelo nivel 0
» dfClima.swaplevel(0, 1).sort_index(level=0)
↳                 Estado                    Paraná    Amazonas
                  Cidade     Cascavel     Curitiba      Manaus
  D/N     Característica
  dia        Temperatura           25           20          30
                 Umidade           15           25          27
  noite      Temperatura           20           16          15
                 Umidade           40           60          78

» # soma dos valores agrupados pelo nível 1 (D/N)
» dfClima.groupby(level=1).sum()
 
↳ Estado                    Paraná    Amazonas
  Cidade     Cascavel     Curitiba      Manaus
  D/N 
  dia              40           45          57
  noite            60           76          93

O método dataframe.groupby(), que veremos mais tarde com maiores detalhes, permite o agrupamento dos dados de um determinado índice (ou nível de índices). Por ex., dataframe.groupby(level=n).sum() faz o agrupamento dos dados segundo o n-ésimo nível de índice e depois soma esses valores. Muitas outras funções estatísticas ficam disponíveis com agrupamentos por groupby.

» # soma dos valores agrupados pelo nível 0
» dfClima.groupby(level='Característica').mean()
↳                  Estado               Paraná    Amazonas
                   Cidade  Cascavel   Curitiba      Manaus
  Característica
  Temperatura                  22.5       18.0        22.5
  Umidade                      27.5       42.5        52.5

» # a média dos valores agrupados pelo índice D/N
» dfClima.groupby(level=0).mean()

↳                   Estado              Paraná   Amazonas
                    Cidade  Cascavel  Curitiba     Manaus
  Característica
  Temperatura                   22.5      18.0       22.5
  Umidade                       27.5      42.5       52.5

» # o valor máximo agrupado pelo nível 'Característica'
» dfClima.groupby(level='Característica').max()
↳                  Estado              Paraná    Amazonas
  Cidade                   Cascavel   Curitiba     Manaus
  Característica
  Temperatura                    25         20         30
  Umidade                        40         60         78

Vimos previamente que qualquer coluna pode ser transformada em índice do dataframe. Mais de uma coluna pode também ser usada: para isso usamos dataframe.set_index([coluna1, coluna2]). Por default essa operação coloca coluna1, coluna2 como índices e descarta as colunas usadas. Para alterar esse comportamento (e manter as colunas) usamos o parâmetro drop=False. O método dataframe.reset_index() remove os índices colocando-os como colunas e criando um novo conjunto de índices.

» # criamos um dataframe arbitrário
» dfNums = pd.DataFrame({'a': range(1,6),
»                        'texto-a': ['um','dois','três','quatro','cinco'],
»                        'b': range(5, 0, -1),
»                        'texto-b': ['cinco', 'quatro','três','dois','um']
»                       })

» # dataframe inicial
» dfNums
↳      a     texto-a     b     texto-b
  0    1          um     5       cinco
  1    2        dois     4      quatro
  2    3        três     3        três
  3    4      quatro     2        dois
  4    5       cinco     1          um

» # usamos as colunas 'a' e 'b' como índices
» dfNums2 = dfNums.set_index(['a', 'b'])

» dfNums2
↳          texto-a    texto-b
  a    b         
  1    5        um      cinco
  2    4      dois     quatro
  3    3      três       três
  4    2    quatro       dois
  5    1     cinco         um

» # para descartar os índices (e recuperar as colunas)
» dfNums2.reset_index()

↳       a     b     texto-a     texto-b
  0     1     5     um          cinco
  1     2     4     dois        quatro
  2     3     3     três        três
  3     4     2     quatro      dois
  4     5     1     cinco       um

» # podemos usar as colunas 'a' e 'texto-a' como índices sem descartar essas colunas
» dfNums.set_index(['a', 'texto-a'], drop=False)
↳                 a     texto-a    b    texto-b
  a     texto-a                 
  1     um        1     um         5      cinco
  2     dois      2     dois       4     quatro
  3     três      3     três       3       três
  4     quatro    4     quatro     2       dois
  5     cinco     5     cinco      1         um

Uma exceção é lançada se já existem colunas com os mesmos nomes recuperados por reset_index.

Combinando dataframes

Podemos juntar dataframes de várias formas. pandas.merge() junta dataframes usando um ou mais índices, em operações semelhantes àquelas de bancos de dados relacionais usando-se as operações de join do SQL. pandas.concat() faz a concatenação ou empilhamento dos dataframes ao longo do eixo escolhido. pandas.combine_first() permite a junção de dados que se superpõe (existem em mais de uma tabela), preenchendo valores ausentes um uma tabela com aqueles em outra tabela fornecida.

merge()

df1.merge(df2) retorna outro dataframe que é a junção dos dois dataframes. O método possui a seguinte assinatura:
df1.merge(df2, how='inner', on=None, left_on=None, right_on=None, left_index=False, right_index=False, sort=False, suffixes=('_x', '_y'), copy=True, indicator=False, validate=None)

A junção pode ser feita sobre nomes das colunas ou índices. Uma Series nomeada é tratada como um dataframe de coluna única. São parâmetros:

df1, df2 dataframe ou Series nomeada. Junção de df1 com df2
how tipo de junção: left, right, outer, inner, cross:
inner: usa apenas combinações de chaves existentes em ambas as tabelas preserva ordem das chaves.
outer: usa todas as combinações de chaves em cada uma das tabelas,
left: usa todas as combinações de chaves existentes na tabela à esquerda,
right: usa todas as combinações de chaves existentes na tabela à direita,
cross: cria o produto cartesiano das tabelas, preserva ordem dos índices.
on coluna ou índice para a junção. Deve existir em ambos os dataframes
left_on nome da coluna ou índice (ou lista) em df1.
right_on nome da coluna ou índice (ou lista) em df2.
left_index False/True: use o índice de df1 como chave.
right_index False/True: use o índice de df2 como chave.
sort False/True: Ordena os índices no resultado.
suffixes lista: default = (“_x”, “_y”). Sufixos para índices de mesmo nome
copy False/True: Se False evita a cópia, se possível
indicator False/True ou str: Se True acrescenta coluna “_merge” com informações sobre as linhas.
validate str, opcional. Se especificada verifica se a junção é do tipo:
one_to_one ou 1:1 : se chave da fusão é única nos dois dataframes,
one_to_many ou 1:m : se chave da fusão é única em df1 (lado esquerdo),
many_to_one ou m:1 : se chave da fusão é única em df2 (lado direito),
many_to_many ou m:m : embora permitida não resulta em nenhuma verificação.

Comparação de how='' com comandos SQL: (Pandas e SQL comparados).

how= similar ao SQL
left left outer join. Preserva ordem das chaves.
right right outer join. Preserva ordem das chaves.
outer full outer join. Ordena por nomes das chaves.
inner inner join. Preserva ordem das chaves à esquerda.
» # criando dataframes 
» df1 = pd.DataFrame({'chave': ['a', 'a', 'a', 'b', 'b', 'b', 'c'], 'data1': range(7)})
» df2 = pd.DataFrame({'chave': ['a', 'b', 'd'], 'data2': range(3)})

» # exibindo df1, df2 e sua junção com merge
» display(df1, df2, pd.merge(df1, df2))

↳    chave   data1
  0      a       0
  1      a       1
  2      a       2
  3      b       3
  4      b       4
  5      b       5
  6      c       6

↳    chave   data2
  0      a      0
  1      b      1
  2      d      2

↳    chave   data1   data2
  0      a       0       0
  1      a       1       0
  2      a       2       0
  3      b       3       1
  4      b       4       1
  5      b       5       1


Como os dois dataframes possuem uma coluna com nome comum a junção foi feita com base nos valores da coluna com esse nome. Essa informação pode ser explicitada com pd.merge(df1, df2, on='chave').

Se os nomes das colunas de cada dataframe for diferente eles devem ser definidos com os parâmetros left_on, right_on.

» df3 = pd.DataFrame({'chave1': ['a', 'a', 'a', 'b', 'b', 'c', 'd'], 'data1': range(7)})
» df4 = pd.DataFrame({'chave2': ['a', 'b', 'd'], 'data2': range(3)})

» display(df3, df4, pd.merge(df3, df4, left_on='chave1', right_on='chave2'))
↳    chave1     data1
  0       a     0
  1       a     1
  2       a     2
  3       b     3
  4       b     4
  5       c     5
  6       d     6

↳    chave2     data2
  0       a     0
  1       b     1
  2       d     2

↳    chave1     data1     chave2     data2
  0       a     0         a          0
  1       a     1         a          0
  2       a     2         a          0
  3       b     3         b          1
  4       b     4         b          1
  5       d     6         d          2

Vemos na concatenação acima que o método usado reune apenas valores existentes nas duas tabelas. Isso é equivalente a passar o parâmetro how=’inner’ (um inner join ). Outra opção consiste em fazer o ligamento externo.

» # para conseguir um outer join    
» pd.merge(df1, df2, how='outer')

↳     chave   data1   data2
  0     a       0.0     0.0
  1     a       1.0     0.0
  2     a       2.0     0.0
  3     b       3.0     1.0
  4     b       4.0     1.0
  5     b       5.0     1.0
  6     c       6.0     NaN
  7     d       NaN     2.0

» dd.merge(df1, df2, how='left')

↳   chave   data1   data2
  0     a       0     0.0
  1     a       1     0.0
  2     a       2     0.0
  3     b       3     1.0
  4     b       4     1.0
  5     b       5     1.0
  6     c       6     NaN

» pd.merge(df1, df2, how='right')

↳   chave     data1   data2
  0     a       0.0       0
  1     a       1.0       0
  2     a       2.0       0
  3     b       3.0       1
  4     b       4.0       1
  5     b       5.0       1
  6     d       NaN       2

Tabelas podem ser ligadas por mais de uma chave, quando os dataframes possuem índices hierarquizados. As chaves são usadas como se fossem uma única chave concatenada.

» df1 = pd.DataFrame({'chave_1': ['rato', 'rato', 'gato'],
»                      'chave_2': ['Jones', 'Jerry', 'Tom'],
»                      'valor_A': [10, 20, 30]})
» df2 = pd.DataFrame({'chave_1': ['rato', 'rato', 'gato', 'gato'],
»                       'chave_2': ['Jones', 'Jerry', 'Tom', 'Tim'],
»                       'valor_B': [40, 50, 60, 70]})
                      
» # exibindo os dataframes e a junção externa em duas chaves
» display(df1, df2, pd.merge(df1, df2, on=['chave_1','chave_2'], how='outer'))

↳     chave_1     chave_2    valor_A
  0      rato       Jones         10
  1      rato       Jerry         20
  2      gato         Tom         30

↳    chave_1     chave_2   valor_B
  0     rato       Jones        40
  1     rato       Jerry        50
  2     gato         Tom        60
  3     gato         Tim        70

↳     chave_1    chave_2    valor_A    valor_B
  0     rato       Jones       10.0         40
  1     rato       Jerry       20.0         50
  2     gato        Tom        30.0         60
  3     gato        Tim         NaN         70

» # a junção interna em duas chaves
» pd.merge(df1, df2, on=['chave_1','chave_2'], how='inner')

↳     chave_1   chave_2    valor_A    valor_B
  0      rato     Jones         10         40
  1      rato     Jerry         20         50
  2      gato      Tom          30         60

Se a junção for feita sobre campos (nomes de colunas) com o mesmo nome estes serão alterados para continuar a representar suas colunas de origem. No caso do exemplo as colunas com nome valor foram renomeadas para valor_x e valor_y.

» df1 = pd.DataFrame({'chave': ['a', 'b', 'c'], 'valor': [1,2,3]})
» df2 = pd.DataFrame({'chave': ['a', 'b', 'c'], 'valor': [10,20,30]})

» mrg = pd.merge(df1, df2, on='chave')

» display(df1, df2, mrg)

↳    chave  valor
  0      a      1
  1      b      2
  2      c      3

↳    chave  valor
  0      a     10
  1      b     20
  2      c     30

↳    chave  valor_x  valor_y
  0      a        1       10
  1      b        2       20
  2      c        3       30

A chave usada na fusão (merge) pode estar no índice de um ou ambas as tabelas. No exemplo usamos pd.merge(esquerda, direita, left_on='chave', right_index=True) que faz a junção de esquerda.chave com direita.index

» esquerda = pd.DataFrame({'chave': ['a1', 'a1', 'a2', 'a1', 'a2', 'a3'], 'valor_1': range(6)})
» direita = pd.DataFrame({'valor_2': [50, 70]}, index=['a1', 'a2'])

» mrg = pd.merge(esquerda, direita, left_on='chave', right_index=True)

» # exibindo dataframes e sua junção, usando o índice da tabela à direita
» display(esquerda, direita, mrg)

↳    chave   valor_1
  0     a1         0
  1     a1         1
  2     a2         2
  3     a1         3
  4     a2         4
  5     a3         5

↳    valor_2
  a1      50
  a2      70

↳    chave   valor_1    valor_2
  0     a1         0         50
  1     a1         1         50
  3     a1         3         50
  2     a2         2         70
  4     a2         4         70

» # se os dataframes forem invertidos conseguiríamos o
» # mesmo resultado, exceto pela ordem das colunas, usando:
» # pd.merge(direita, esquerda, right_on='chave', left_index=True)

Junções com join()

Junções podem ser feitas com dataframe.join(dfOutro) que, por default, faz a união outer join usando o índice como chave. Esse método tem a seguinte assinatura, onde os parâmetros são
dataframe.join(dfOutro, on, how, lsuffix, rsuffix, sort),
Todos os parâmetros são opcionais exceto dfOutro. Os defaults estão em negrito.

dfOutro DataFrame, Series ou lista de DataFrames.
on string, especifica em que chave(s) fazer a junção
how strings: left, right, outer, inner. Especifica o tipo de junção.
lsuffix/rsuffix Default = ”. String a concatenar à esquerda/direita em colunas com mesmo nome.
sort False/True. Se True ordena o dataframe pela chave de junção.
» # dataframe join
» df1 = pd.DataFrame({'nome': ['Paulo', 'Maria', 'Julio','Marta'],
                       'idade': [35, 43, 31, 56]})
» df2 = pd.DataFrame({'profissao': ['médico', 'engenheiro', 'advogado']})

» df1
↳      nome      idade
  0    Paulo     35
  1    Maria     43
  2    Julio     31
  3    Marta     56

» df2
↳      profissao
  0    médico
  1    engenheiro
  2    advogado

» df1.join(df2, on=df1.index,  lsuffix='_1', rsuffix='_2') # , how = 'left' (default)
↳      nome   idade_1    profissao    idade_2
  0   Paulo        35       médico       35.0
  1   Maria        43   engenheiro       40.0
  2   Julio        31     advogado       31.0
  3   Marta        56          NaN        NaN

» # um inner join
» df1.join(df2, lsuffix='_', how='inner')
↳      nome    idade_    profissao   idade
  0   Paulo       35        médico      35
  1   Maria       43    engenheiro      40
  2   Julio       31      advogado      31

Vários dataframes podem ser concatenados de uma vez. Para isso eles devem ter dimensões compatíveis.

» # Vários dataframes podem ser concatenados
» df1 = pd.DataFrame([[23, 83], [93, 10], [73, 89], [68, 90]],
»                    index=['a', 'b', 'e', 'f'],
»                    columns=['A', 'B'])

» df2 = pd.DataFrame([[2, 8], [9, 1], [7, 8], [6, 9]],
»                    index=['a', 'b', 'c', 'd'],
»                    columns=['C', 'D'])

» df3 = pd.DataFrame([[3, 3], [3, 0], [3, 9], [8, 0]],
»                    index=['a', 'c', 'd', 'e'],
»                    columns=['E', 'F'])

» # exibe os 3 dataframes
» display(df1, df2, df3)

↳ A    B
  a    23    83
  b    93    10
  e    73    89
  f    68    90

↳ C    D
  a    2    8
  b    9    1
  c    7    8
  d    6    9
  
↳ E    F
  a    3    3
  c    3    0
  d    3    9
  e    8    0

» # exibe a junção dos dataframes
» df1.join([df2, df3])

↳         A       B      C      D      E      F
  a    23.0    83.0    2.0    8.0    3.0    3.0
  b    93.0    10.0    9.0    1.0    NaN    NaN
  e    73.0    89.0    NaN    NaN    8.0    0.0
  f    68.0    90.0    NaN    NaN    NaN    NaN

Como sempre, campos não fornecidos são preenchidos por NaN. Por ex.: df1.join([df2, df3]).loc['f', 'F'] = NaN.

concatenate()

Podemos concatenar numpy.arrays, Series e dataframes ao longo do eixo desejado.

» # Concatenando um array ao longo de um eixo
» # criamos 2 arrays
» arr1 = np.arange(6).reshape((3, 2))

» arr1
↳ array([[0, 1],
         [2, 3],
         [4, 5]])

» # concatenando arr1 consigo mesmo, ao longo de colunas
» np.concatenate([arr1, arr1], axis=1)
↳ array([[0, 1, 0, 1],
         [2, 3, 2, 3],
         [4, 5, 4, 5]])

» # concatenando arr1 consigo mesmo, ao longo de linhas
» np.concatenate([arr1, arr1], axis=0)
↳ array([[0, 1],
         [2, 3],
         [4, 5],
         [0, 1],
         [2, 3],
         [4, 5]])

» # defina outro array, com shape (3, 1)
» arr2 = np.array([[0], [1], [2]])

» arr2
↳ array([[0],
         [1],
         [2]])

» # concatenando arr1 2 arr2 pelas colunas
» np.concatenate([arr1, arr2], axis=1)

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

» # (tentando) concatenar arr1 2 arr2 pelas linhas
» np.concatenate([arr1, arr2], axis=0)
↳ ValueError: all the input array dimensions for the concatenation axis must match exactly,
  but along dimension 1, the array at index 0 has size 2 and the array at index 1 has size 1


Vemos que podemos concatenar uma matriz coluna (3 × 1) com outra matriz (3 × 2) pelas colunas, mas não pelas linhas pois as dimensãos são incompatíveis.

combine() e combine_first()

O método df1.combine(df2, func, fill_value=None, overwrite=True) combina df1 e df2, coluna a coluna, aplicando func para decidir qual valor será usado.

Podemos criar uma função que receba duas colunas e realize alguma operação entre elas, retornando outra coluna. No ex., a função f faz a soma dos elementos de duas colunas e retorna aquela com menor soma. A função g seleciona, a cada linha, qual é o maior elemento. Quando o parâmetro fill_value=r é usado todos os valores NaN são substituídos por r antes de serem submetidos à função func, exceto se ambos os valores forem nulos, quando não existirá substituição.

» df1 = pd.DataFrame({'A': [0, 3], 'B': [7, 2]})
» df2 = pd.DataFrame({'A': [2, 6], 'B': [1, 3]})

» df1
↳      A    B
  0    0    7
  1    3    2

» df2
↳      A    B
  0    2    1
  1    6    3

» # a função de comparação pode ser
» def f(x,y):
»     if x.sum() < y.sum():
»         return x
»     else:
»         return y

» # a combinação, usando essa função
» df1.combine(df2, f)
↳      A    B
  0    0    1
  1    3    3

» # O mesmo resultado pode ser obtido com uma função lambda
» df1.combine(df2, lambda x, y: x if x.sum() < y.sum() else y)

» # funções mais complexas podem ser usadas
» df1.combine(df2, lambda x, y: (x+y)*(y-x))
↳       A     B
  0     4   -48
  1    27     5

» # outro exemplo, selecionar o maior elemento de cada df
» def g(x,y):
»     a = x[0] if x[0] > y[0] else y[0]
»     b = x[1] if x[1] > y[1] else y[1]
»     return pd.Series([a,b])

» df1.combine(df2,g)
↳      A    B
  0    2    7
  1    6    3

» # o mesmo poderia ser feito com uma funlão lambda
» maior = lambda x,y: pd.Series([x[0] if x[0] > y[0] else y[0],
                                x[1] if x[1] > y[1] else y[1]])
» df1.combine(df2,maior) # mesmo output
 
» # uso de fill_value
» df1 = pd.DataFrame({'A': [0, 0], 'B': [np.NaN, 4]})
» df2 = pd.DataFrame({'A': [1, 1], 'B': [3, 3]})

» df1.combine(df2, maior, fill_value=6)
↳      A      B
  0    1    6.0
  1    1    4.0

Já o método dataframe.combine_first(dfOutro) substitui os valores NaN no dataframe com os valores de dfOutro, quando esses valores existirem.

» df1 = pd.DataFrame({'a': [1, np.nan, 5, np.nan],
»                     'b': [np.nan, 2, np.nan, 6],
»                     'c': range(2, 18, 4)})
» df2 = pd.DataFrame({'a': [5, 4, np.nan, 3, 7],
»                     'b': [np.nan, 3, 4, 6, 8]})
» display(df1, df2)
↳        a      b     c
  0    1.0    NaN     2
  1    NaN    2.0     6
  2    5.0    NaN    10
  3    NaN    6.0    14
  
↳        a      b
  0    5.0    NaN
  1    4.0    3.0
  2    NaN    4.0
  3    3.0    6.0

» df1.combine_first(df2)
↳        a      b      c
  0    1.0    NaN    2.0
  1    4.0    2.0    6.0
  2    5.0    4.0   10.0
  3    3.0    6.0   14.0
  4    7.0    8.0    NaN

Bibliografia

Consulte bibliografia completa em Pandas, Introdução neste site.

Nesse site:

Dataframes, preparação de dados


Preparação de dados

Programadores que lidam com análise de dados passam grande parte do tempo dedicado a um projeto preparando esses dados, antes mesmo de começar qualquer análise. Normalmente os dados são importados de uma fonte externa, tal como um arquivo em forma tabular em html, pdf, texto puro ou csv. Eles precisam ser convertidos para um formato legível e muitas vezes contém erros e valores ausentes. A vezes o próprio processo de conversão introduz perda de dados, tal como acontece em textos impressos transformados em texto digital por OCR (optical character recognition ). Seja qual for a origem dos dados algum trabalho de depuração deve ser feito. Em seguida eles devem passar por formatação adequada, a quebra de tabelas, o estabelecimento de vínculos entre elas, etc. Pandas oferece boas ferramentas para todas essas etapas.

Dados ausentes

Já vimos que dados não presentes em alguma tabela são representados por NaN (not a number). O objeto None do python também é tratado como um valor ausente ou NA (not available). O método dropna() descarta linhas (se axis=0, default) ou colunas (se axis=1) contendo campos nulos. dropna(how='all') descarta linhas ou colunas se todos os campos forem nulos. Também podemos determinar que apenas linhas ou colunas com um número mínimo de elementos não nulos sejam mantidas, com df.dropna(thresh=n).

» import pandas as pd
» import numpy as np

» dados = pd.Series([121.45, np.nan ,32.12,42.21,51.56])
» dados
↳ 0    121.45
  1       NaN
  2     32.12
  3     42.21
  4     51.56

» dados[3]= None
» dados.isnull()
↳ 0    False
  1     True
  2    False
  3     True
  4    False

» dados.dropna()                   # o mesmo que dados[dados.notnull()]
↳ 0    121.45
  2     32.12
  4     51.56

» from numpy import nan as NA     # para estabelecer um alias curto para np.nan
» data = pd.DataFrame([[1., 6.5, 3.9], [1.3, NA, NA], [NA, NA, NA], [NA, 5.8, 6.7]])
» data
↳        0      1      2
  0    1.0    6.5    3.9
  1    1.3    NaN    NaN
  2    NaN    NaN    NaN
  3    NaN    5.8    6.7

» data.dropna()
↳        0      1      2
  0    1.0    6.5    3.9

» data.dropna(how='all')
↳        0      1      2
  0    1.0    6.5    3.9
  1    1.3    NaN    NaN
  3    NaN    5.8    6.7

» data[4] = NA
» data
↳        0      1      2      4
  0    1.0    6.5    3.9    NaN
  1    1.3    NaN    NaN    NaN
  2    NaN    NaN    NaN    NaN
  3    NaN    5.8    6.7    NaN

» data.dropna(axis=1, how='all')
↳        0      1      2
  0    1.0    6.5    3.9
  1    1.3    NaN    NaN
  2    NaN    NaN    NaN
  3    NaN    5.8    6.7

Preenchendo valores ausentes

A invés de descartar linhas e colunas com campos ausentes podemos preencher estas lacunas. df.fillna(const) substitui campos NA com o valor único const. Um dicionário {coluna:valor} pode ser passado contendo constantes diferentes para cada coluna. Observando que df.mean() retorna uma Series com as médias de cada colunas, podemos usar df.fillna(df.mean()) para preencer NAs de cada coluna com essa média. Também podemos passar o parâmetro df.fillna(method='ffill') para preencher cada NA com o valor que o antecede na coluna. df.fillna(method='bfill') preenche NAs com o valor que o segue.

» # criando um df de teste com campos NA
» df = pd.DataFrame(np.random.randn(4, 3))
» df.iloc[0:3, 1] = NA
» df.iloc[1:3, 2] = NA

» df
↳             0           1            2
  0    0.615016         NaN    -0.860821
  1    1.195041         NaN          NaN
  2   -0.110482         NaN          NaN
  3    1.837690    1.569459     0.891858

» # preenche NAs com 0
» df.fillna(0)
↳             0           1           2
  0    0.615016    0.000000   -0.860821
  1    1.195041    0.000000    0.000000
  2   -0.110482    0.000000    0.000000
  3    1.837690    1.569459    0.891858

» # preenche coluna 1 com 10, coluna 2 com 20
» df.fillna({1:10, 2:20})
↳             0            1            2
  0    0.615016    10.000000    -0.860821
  1    1.195041    10.000000    20.000000
  2   -0.110482    10.000000    20.000000
  3    1.837690     1.569459     0.891858

» df.fillna(method='ffill')
↳             0          1            2
  0    0.615016        NaN    -0.860821
  1    1.195041        NaN    -0.860821
  2   -0.110482        NaN    -0.860821
  3    1.837690   1.569459     0.891858

» df.fillna(method='bfill')
↳             0           1           2
  0    0.615016    1.569459   -0.860821
  1    1.195041    1.569459    0.891858
  2   -0.110482    1.569459    0.891858
  3    1.837690    1.569459    0.891858

» df.fillna(method='bfill', limit=2)
» df.mean()
↳ 0    0.884316
  1    1.569459
  2    0.015519

» df.fillna(df.mean())
↳             0           1           2
  0    0.615016         NaN   -0.860821
  1    1.195041    1.569459    0.891858
  2   -0.110482    1.569459    0.891858
  3    1.837690    1.569459    0.891858

Vemos que df.fillna(method='ffill') não substituiu valores nas linhas 0, 1, 2 da coluna 1 pois nenhum valor os antecede. Nesse caso teríamos que usar method='bfill', ou outra forma de preencher o campo vazio.

Substituições com dataframe.replace()

O método df.replace() substitui valores específicos em uma Series ou dataframe. Por ex., suponha que temos uma Series de valores positivos e a inserção de negativos foi convencionada para indicar valores ausentes. Podemos alterar esses valores usando df.replace(), lembrando que nenhuma das formas abaixo altera a Serie original, a menos que inplace=True seja usado.

» serie = pd.Series([12,-2, 34, -1])
» serie
↳ 0    12
  1    -2
  2    34
  3    -1

» serie.replace(-2, -90)
↳ 0    12
  1   -90
  2    34
  3    -1

» serie.replace([-2,-1], [20,10])
↳ 0    12
  1    20
  2    34
  3    10

» serie.replace(-1, NA)
↳ 0    12.0
  1    -2.0
  2    34.0
  3     NaN

Claro que df.replace() pode ser usado para substituir um valor específico por valores calculados, usando métodos mais sofisticados de avaliação.

Em um dataframe df.replace(lista1, lista2) pode ser usado para substituir valores da lista1 pelos da lista2 (que deve ter o mesmo tamanho). df.replace(lista, escalar) substitui todos os valores em lista pelo escalar e df.replace(dicionario) substitui as chaves pelas valores no dicionário.

» df = pd.DataFrame({'a':[9,56,67], 'b':[33,55,66], 'c':[63,69,67], 'd':[2,3,9]})
» df
↳       a     b     c    d
  0     9    33    63    2
  1    56    55    69    3
  2    67    66    67    9

» df.replace(9, 100)
↳        a     b     c     d
  0    100    33    63     2
  1     56    55    69     3
  2     67    66    67   100

» df.replace([9, 55, 67], 0)
↳      a     b     c    d
  0    0    33    63    2
  1   56     0    69    3
  2    0    66     0    0

» df.replace([9, 55, 67], [1,2,3])
↳      a     b     c    d
  0    1    33    63    2
  1   56     2    69    3
  2    3    66     3    1

» df.replace({9:-9, 33:-33})
↳       a      b     c    d
  0    -9    -33    63    2
  1    56     55    69    3
  2    67     66    67   -9

Análise de outliers

Em qualquer processo de tomada de medidas ou coleta de dados existem restrições à precisão obtida. Mas, além da precisão restrita, é frequente existirem dados muito fora de qualquer curva esperada. Esses são os chamados pontos fora da curva ou outliers e geralmente são descartados. Os critérios de decisão sobre quais pontos são outliers dependem do modelo que se quer tratar.

No pandas podemos encontrar valores que estão acima ou abaixo de um certo limite.

Lembrando que np.random.randn(M, p) retorna um array de p colunas, cada uma com M valores, retirados aleatoriamente de uma distribuição normal com média 0 e variância 1, começamos por coletar um dataframe para testes.

Considerando os máximos e mínimos exibidos, vamos estabelecer arbitrariamente que valores afastados acima de 3 da média do conjunto são outliers. Isso quer dizer que consideraremos os pontos com |x| > 3 como outliers (onde |x| significa valor absoluto de x). Uma das possibilidades consiste em substituir valores não aceitáveis por np.nan e depois usar uma das formas de fill para preencher esses campos.

» dados = pd.DataFrame(np.random.randn(1000, 4))
» # são os valores mínimo e máximo desse dataframe
» dados.min().min(), dados.max().max()
↳ (-3.7113843289590496, 3.480659301328407)

» # substituimos |x| > 3 por np.nan
» dados[np.abs(dados) > 3] = np.nan
» dados.describe()    # (1) visualização do dataframe (alguns campos exibidos)
↳                   0             1             2            3
  count    996.000000    997.000000    998.000000   996.000000
  mean       0.086548      0.021479     -0.046291     0.019611
  min       -2.772219     -2.860741     -2.763174    -2.644022
  max        2.763864      2.849207      2.955914     2.905516

» dados = dados.fillna(method='bfill')
» dados.describe()    # (2) visualização do dataframe (alguns campos exibidos)
↳                    0              1              2              3
  count    1000.000000    1000.000000    1000.000000    1000.000000
  mean        0.089292       0.021845      -0.046568       0.020410
  min        -2.772219      -2.860741      -2.763174      -2.644022
  max         2.763864       2.849207       2.955914       2.905516

No primeiro uso de describe a contagem count mostra que existem linhas com campos nulos para cada coluna. Após a operação de fill todos os campos são numéricos.

Removendo linhas duplicadas

Para remover linhas duplicadas em um dataframe usamos df.drop_duplicates(). Valores duplicados em apenas uma coluna podem ser removidos com df.drop_duplicates('nomeColuna'), ou em várias colunas, passando-se uma lista df.drop_duplicates(['col1',..., 'coln']). Por default a primeira linhas, entre as duplicadas é mantida. Para manter a última usamos df.drop_duplicates('coluna', keep='last').

» # remoção de linhas duplicadas
» dic ={'col1': ['vaca', 'vaca', 'pato','pato'], 'col2': [1, 3, 4, 4]} 
» df = pd.dfFrame(dic)
» df
↳      col1    col2
  0    vaca    1
  1    vaca    3
  2    pato    4
  3    pato    4

» df.duplicated()           # retorna uma Series mostrando linhas duplicadas
↳ 0    False
  1    False
  2    False
  3     True

» df.drop_duplicates()
↳      col1   col2
  0    vaca      1
  1    vaca      3
  2    pato      4

» df.drop_duplicates('col1')
↳      col1  col2
  0    vaca     1
  2    pato     4

» df.drop_duplicates('col1', keep='last')
↳      col1   col2
  1    vaca     3
  3    pato     4

No atual estado de Pandas não é possível fazer a remoção de duplicadas sobre colunas. Para isso obtenha a transposta do dataframe, remova linhas duplicadas e o transponha novamente.

Transformações sobre elementos de um dataframe

Um restaurante faz uma lista de aquisição de produtos, descrevendo o ítem e quantas unidades devem se adquiridas.

» compra = {'produto':['leite', 'manteiga', 'laranja', 'arroz'],
            'quantos':[15, 40,50, 30]} 
» dfComprar = pd.DataFrame(compra)
» dfComprar
↳      produto  quantos
  0      leite       15
  1   manteiga       40
  2    laranja       50
  3      arroz       30

Mais tarde o gerente pede que os produtos sejam classificados como veganos ou não. Para isso podemos usar o método Series.map(dict) que transforma cada elemento usando-o como chave e retornando o valor no dicionário. Construímos um mapeamento entre produto e S/N, conforme o produto seja ou não vegano.

» veg = {'leite':'N', 'manteiga':'N', 'laranja':'S', 'arroz':'S'}
» # dfComprar['produto'] é uma Series e
» dfComprar['produto'].map(lambda x: vegano[x])
↳ 0    N
  1    N
  2    S
  3    S

» # inserindo esse serie em uma nova coluna do df
» dfComprar['vegano'] = dfComprar['produto'].map(veg)
» dfComprar
↳      produto   quantos   vegano
  0      leite        15        N
  1   manteiga        40        N
  2    laranja        50        S
  3      arroz        30        S

» # o mesmo resultado seria obtido com a função lambda 
» dfComprar['vegano']=dfComprar['produto'].map(lambda x: vegano[x])

Compartimentação e discretização

Compartimentação e discretização, (Binning e Discretization ) é o processo de particionamento de dados em faixas especificadas. Os compartimentos (faixas ou bins) são representados por variáveis categóricas, que são variáveis que podem assumir apenas um número discreto e limitado de valores, geralmente fixo. Elas estão associadas à propriedades qualitivas do sistema que se observa e podem satisfazer ou não algum critério de ordenamento.

Por ex., suponha que temos um estudo de qualquer natureza centrada sobre indivíduos onde o sexo e a faixa etária são relevantes para as conclusões que se procura obter. O sexo dos indivíduos (digamos que divididos em F = feminino, M = masculino, O = outros) não pode ser ordenado. Mas as faixas etárias são ordenáveis. Dividimos a população estudada em faixas ou bins. Sabendo que todos os participantes são maiores de idade e nenhum tem mais de 98 anos de idade usamos as faixas separadas pelas idades: 18, 34, 50, 66, 82, 98 anos.

» faixas = [18, 34, 50, 66, 82, 98]                            # definição dos intervalos de idade
» idades = [25, 18, 59, 39, 68, 26, 73, 63, 56, 84]            # idade dos indivíduos no estudo
» categorias = pd.cut(idades, faixas)
» categorias
↳ [(18.0, 34.0], NaN, (50.0, 66.0], (34.0, 50.0], (66.0, 82.0], (18.0, 34.0],
   (66.0, 82.0], (50.0, 66.0], (50.0, 66.0], (82.0, 98.0]]
  Categories (5, interval[int64]): [(18, 34] < (34, 50] < (50, 66] < (66, 82] < (82, 98]]

» # o objeto categorias é do tipo Categorical
» type(categorias)
↳ pandas.core.arrays.categorical.Categorical

» categorias.categories
↳ IntervalIndex([(18, 34], (34, 50], (50, 66], (66, 82], (82, 98]],
                closed='right', dtype='interval[int64]')

# O método pd.value_counts(categorias) fornece uma contagem para cada valor existente:
» pd.value_counts(categorias)
↳ (50, 66]    3
  (18, 34]    2
  (66, 82]    2
  (34, 50]    1
  (82, 98]    1

» # as colunas são formadas por
» pd.value_counts(categorias).index[0],  pd.value_counts(categorias)[0]
↳ (Interval(50, 66, closed='right'), 3)

» nomes_faixas = ['garoto','adulto','semi-novo','vô','matusa']
» categorias = pd.cut(idades, faixas, labels=nomes_faixas)
» categorias
↳  ['garoto', NaN, 'semi-novo', 'adulto', 'vô', 'garoto', 'vô', 'semi-novo', 'semi-novo', 'matusa']
   Categories (5, object): ['garoto' < 'adulto' < 'semi-novo' < 'vô' < 'matusa']
» pd.value_counts(categorias)
↳ semi-novo    3
  garoto       2
  vô           2
  adulto       1
  matusa       1

» # podemos transformar esse objeto em um dataframe
» dfCont = pd.DataFrame(pd.value_counts(categorias))

» # reordenar índices
» dfConf = dfCont.reindex(['garoto', 'adulto', 'semi-novo', 'vô', 'matusa'])
» dfConf
↳             0
  garoto      2
  adulto      1
  semi-novo   3
  vô          2
  matusa      1

As faixas numéricas são estabelecidas em intervalos do tipo (a, b] < (b, c] … representando intervalos abertos no limite inferior e fechados no superior. Isso significa que a não está no primeiro intervalo, mas b está. Para alterar esse comportamento usamos o parâmetro pandas.cut(...,right=False).

Podemos informar em quantas faixas queremos dividir os dados, ao invés de passar explicitamente essas faixas. Nesse caso o método pandas.cut(dados, n, precision=p) calculará n intervalos iguais baseados nos valores máximos e mínimos dos dados. precision=p determina a precisão decimal das faixas.

» # array com 20 numeros aleatórios    
» dados = np.random.rand(20)*10

» dados.min(), dados.max()         # valores mínimo e máximo
↳ (1.0012658194039414, 9.799331139583924)

» # 3 faixas (bins)
» picado = pd.cut(dados, 3, precision=2)
» pd.value_counts(picado)
↳ (6.87, 9.8]     10
  (0.99, 3.93]     7
  (3.93, 6.87]     3

Para distribuir dados em faixas baseadas em quantis usamos o método pandas.qcut(dados, n), onde n é o número de partes na partição. Intervalos de quantis customizados podem ser conseguidos passando-se uma lista em pandas.qcut(dados, lista).

» data = np.random.randn(1000)          # 1000 números aleatórios
» categorias = pd.qcut(data, 4)         # distribui em quartis
» pd.value_counts(categorias)
↳ (-3.0309999999999997, -0.683]    250
  (-0.683, 0.0106]                 250
  (0.0106, 0.702]                  250
  (0.702, 3.196]                   250

» # intervalos de quantis customizados
» pd.value_counts(pd.qcut(data, [0, 0.1, 0.5, 0.9, 1.]))
↳ (-1.223, 0.0106]                 400
  (0.0106, 1.301]                  400
  (-3.0309999999999997, -1.223]    100
  (1.301, 3.196]                   100

Permutações aleatórias

Permutações entre as linhas (ou colunas) de um dataframe são obtidas com dataframe.take(arr), onde arr é um array com a ordem dos índices desejada. Se essa ordem for “sorteada” o dataframe fica com linhas em ordem “aleatoria”. Para reordenar colunas usamos axis=1. dataframe.sample(n) seleciona n linhas do dataframe, sem repetições (n < dataframe.shape[0]) e dataframe.sample(n, replace=True) retorna n linhas do dataframe que podem ser repetidas (como em um sorteio com reposição dos elementos sorteados).

» # dataframe de teste    
» df = pd.DataFrame(np.arange(16).reshape((4, 4)))
» df
↳      0    1    2    3
  0    0    1    2    3
  1    4    5    6    7
  2    8    9   10   11
  3   12   13   14   15

» sorteio = np.random.permutation(4)   # permutação aleatória de 0, 1, 2 e 3
» sorteio
↳ array([3, 2, 0, 1])

» df.take(sorteio)                     # dataframe na ordem de linhas sorteadas
↳       0     1     2     3
  3    12    13    14    15
  2     8     9    10    11
  0     0     1     2     3
  1     4     5     6     7

» df.take(sorteio, axis=1)             # dataframe na ordem de colunas sorteadas
↳       3     2     0     1
  0     3     2     0     1
  1     7     6     4     5
  2    11    10     8     9
  3    15    14    12    13

» df.sample(n=2)                       # 2 linhas selecionadas aleatoriamente
↳       0     1     2     3
  1     4     5     6     7
  3    12    13    14    15

» df.sample(n=2, axis=1)               # 2 colunas selecionadas aleatoriamente
↳      3    1
  0    3    1
  1    7    5
  2   11    9
  3   15   13

» df.sample(n=4, replace=True)        # 4 linhas selecionadas aleatoriamente, com reposição
↳      0    1    2    3
  0    0    1    2    3
  0    0    1    2    3
  1    4    5    6    7
  0    0    1    2    3

O mesmo dataframe obtido com df.take(sorteio) poderia ser conseguido com df.iloc[sorteio].

Indicador de computação, variáveis fictícias

Na estatística, econometria e aprendizado de máquina uma variável fictícia (variável dummy ) é uma representação de um efeito categórigo assumindo apenas os valores 0 ou 1 para indicar presença ou ausência de alguma forma de caracterização. Elas podem ser consideradas como representações numéricas de aspectos qualitativos. Um exemplo simples seria a representação da classificação de uma conta bancária como poupança (0) ou conta corrente (1).

Uma variável categórica pode ser transformada em uma matriz dummy ou de indicadores. Se uma series (uma coluna de um dataframe) possui p valores distintos podemos obter um dataframe com o mesmo número de colunas, cada uma contendo apenas 0 ou 1. Para isso usamos o método pandas.get_dummies(Series) que retorna um dataframe marcando as posições onde cada um dos p valores ocorrem. Um prefixo pode ser acrescentado aos nomes das colunas com pandas.get_dummies(Series, prefix='p').

Por ex., em uma pesquisa foi marcado, para cada indivíduo participante, o campo sexo = F (feminino), M (masculino), O (outros).

» df = pd.DataFrame({'individuo': ['Fulano', 'Beltrano', 'Cicrano', 'Deltrano', 'Cruciano', 'Marciano'],
                     'sexo': ['H','H','H','F','O','F']})
» df
↳      individuo   sexo
  0       Fulano      H
  1     Beltrano      H
  2      Cicrano      H
  3     Deltrano      F
  4     Cruciano      O
  5     Marciano      F

» # categorizando a coluna 'sexo'
» pd.get_dummies(df['sexo'])
↳      F    H    O
  0    0    1    0
  1    0    1    0
  2    0    1    0
  3    1    0    0
  4    0    0    1
  5    1    0    0

» # inserindo um prefixo (no nome das colunas)
» pd.get_dummies(df['sexo'], prefix='sexo').head(2)
↳    sexo_F  sexo_H  sexo_O
   0      0       1       0
   1      0       1       0

Muitas vezes os dados devem ser manipulados e preparados para uma devida categorização. Suponha que temos uma lista de autores, cada um associado a um ou mais gêneros literários separados por |. Queremos uma listagem de autores versus gêneros, marcando em qual gênero cada um escreve.

» # importamos de qualquer fonte o seguinte dataframe:
» dfAutores
↳       autor                genero
  0   Antonio          poesia|conto
  1      José               romance
  2     Marco      ficção|biografia
  3     Pedro          poesia|conto

» # cada autor está associado a um ou mais gêneros
» genero = dfAutores.genero
» autores = dfAutores.autor
» # as duas séries têm o mesmo comprimento (len(autores) = len(generos) = 4, 4

Criamos uma lista vazia e a preenchemos com todos os gêneros, quebrando os campos em |. Depois usamos pandas.unique(lista) para conseguir um array com os gêneros, sem repetições, como em um conjunto (set).

» lista = []
» for t in genero:
»     lista.extend(t.split('|'))
» unicos = pd.unique(lista)
» unicos
↳ array(['poesia', 'conto', 'romance', 'ficção', 'biografia'], dtype=object)

Em seguida criamos um dataframe de zeros com os autores nas colunas e gêneros nas linhas.

» dfZero = pd.DataFrame(np.zeros((len(unicos),len(autores))), index=unicos, columns=autores).astype(int)
» dfZero          # estado inicial de dfZero
↳ autor    Antonio    José    Marco    Pedro
  poesia         0       0        0        0
  conto          0       0        0        0
  romance        0       0        0        0
  ficção         0       0        0        0
  biografia      0       0        0        0

# preenchemos esse dataframe
» for i in range(len(unicos)):
»     for k in range(len(genero)):
»         if unicos[i] in genero[k]:
»             dfZero.iloc[i,k] = 1
            
» dfZero    # estado final de dfZero
↳ autor    Antonio    José    Marco    Pedro
  poesia         1       0        0        1
  conto          1       0        0        1
  romance        0       1        0        0
  ficção         0       0        1        0
  biografia      0       0        1        0

O duplo loop sobre a lista de gêneros únicos, unicos, e a lista original de gêneros genero faz a verificação se um dos generos está em genero1|genero2…. Por exemplo, na linha 3, coluna 2 temos:

» unicos[3], genero[2],  unicos[3] in genero[2]
↳ ('ficção', 'ficção|biografia', True)

Se o resultado é verdadeiro o dataframe terá o campo correspondente trocado para 1. Os demais permanecem com o valor 0. O dataframe final é o resultado desejado.

Tratamento de campos de texto

Operações com strings são também vetorializadas no pandas. No ex. usamos os códigos telefones dos países: 55-Brasil, 47-Noruega, 52-México. Construímos duas séries e as concatenamos em um dataframe, df = pd.concat([serie, srPais], axis=1).

» lista = ['055-11-12345678', '047-21-87654321', '055-11-13579135', '052-78-45665412']
» serie = pd.Series(lista)
» serie
↳ 0    055-11-12345678
  1    047-21-87654321
  2    055-11-13579135
  3    052-78-45665412

» # booleano, linhas que contém '-11-'
» serie.str.contains('-11-')
↳ 0     True
  1    False
  2     True
  3    False

» # linhas que contém '-11-'
» serie[serie.str.contains('-11-')]
↳ 0    055-11-12345678
  2    055-11-13579135

» # lista com os códigos dos países
» codigos = [x.split('-')[0] for x in lista]
» codigos
↳ ['055', '047', '055', '052']

» # dicionário para conversão código ⇒ país
» pais = {'055':'Brasil', '047':'Noruega', '052':'México'}
» srPais = pd.Series([pais[x] for x in codigos])      # veja comentário †
» srPais
↳ 0     Brasil
  1    Noruega
  2     Brasil
  3     México

» # juntamos as duas series em um dataframe
» df = pd.concat([serie, srPais], axis=1)
» df = df.rename(columns = {0:'telefone', 1:'pais'})

» df
↳             telefone       pais
  0    055-11-12345678     Brasil
  1    047-21-87654321    Noruega
  2    055-11-13579135     Brasil
  3    052-78-45665412     México

» # nome do país começado com 'No'
» df[df['pais'].str.startswith('No')]
↳             telefone       pais
  1    047-21-87654321    Noruega

» # acrescenta campo com 3 primeiras letras do nome
» df['abreviado'] = df['pais'].str[:3]
» df
↳             telefone      pais    abreviado
  0    055-11-12345678    Brasil          Bra
  1    047-21-87654321   Noruega          Nor
  2    055-11-13579135    Brasil          Bra
  3    052-78-45665412    México          Méx

(): A linha srPais = pd.Series([pais[x] for x in codigos]) (uma compreensão de lista) percorre os valores em codigos e os usa como chaves no dicionário pais, retornando seus valores.

🔺Início do artigo

Bibliografia

  • McKinney, Wes: Python for Data Analysis, Data Wrangling with Pandas, NumPy,and IPython
    O’Reilly Media, 2018.

Consulte bibliografia completa em Pandas, Introdução neste site.

Nesse site:

Series: Resumo


Pandas Series

Atributos

Atributo Descrição
at[n] Acesso ao valor na posição n
attrs Retorna ditionario de atributos globais da series
axes Retorna lista de labels do eixo das linhas
dtype Retorna o tipo (dtype) dos objetos armazenados
flags Lista as propriedades do objeto
hasnans Informa se existem NaNs
iat[n] Acesso ao valor na posição n inteiro
iloc[n] Acesso ao valor na posição n inteiro
index Retorna lista de índices
index[n] Retorna índice na n-ésima posição
is_monotonic Booleano: True se valores crescem de forma monotônica
is_monotonic_decreasing Booleano: True se valores decrescem de forma monotônica
is_unique Booleano: True se valores na series são únicos
loc Acessa linhas e colunas por labels em array booleano
name O nome da Series
nbytes Número de bytes nos dados armazenados
shape Retorna uma tuple com forma (dimensões) dos dados
size Número de elementos nos dados
values Retorna series como ndarray

Métodos

Método (sobre série s, outra s2) Descrição
s.abs() Retorna s com valor absoluto, e/e
s.add(s2) Soma s com s2, e/e
s.add_prefix('prefixo') Adiciona prefixo aos labels com string ‘prefixo’
s.add_suffix('sufixo') Adiciona sufixo aos labels com string ‘sufixo’
s.agg([func, axis]) Agrega usando func sobre o eixo especificado
s.align(s2) Alinha 2 objetos em seus eixos usando método especificado
s.all([axis, bool_only, skipna, level]) Booleano: se todos os elementos são True
s.any([axis, bool_only, skipna, level]) Booleano: se algum elemento é True
s.append(to_append[, ignore_index, …]) Concatena 2 ou mais Series
s.apply(func[, convert_dtype, args]) Aplica func sobre os valores de s, e/e
s.argmax([axis, skipna]) Posição (índice inteiro) do valor mais alto de s
s.argmin([axis, skipna]) Posição (índice inteiro) do menor valor de s
s.argsort([axis, kind, order]) Índices inteiros que ordenam valores da s
s.asfreq(freq) Converte TimeSeries para frequência especificada.
s.asof(where[, subset]) Último elemento antes da ocorrência de NaNs após ‘where’
s.astype(dtype[, copy, errors]) Transforma (cast) para dtype
s.at_time(time[, asof, axis]) Seleciona valores em determinada hora (ex., 9:30AM)
s.backfill([axis, inplace, limit, downcast]) Aliás para DataFrame.fillna() usando method=’bfill’
s.between(min, max) Booleana satisfazendo min <= s <= max, e/e
s.between_time(inicio, fim) Seleciona valores com tempo entre inicio e fim
s.bfill([axis, inplace, limit, downcast]) Alias para DataFrame.fillna() usando method=’bfill’
s.clip([min, max, axis, inplace]) Inclui apenas valores no intervalo
s.combine(s2, func[, fill_value]) Combina a s com s2 ou escalar, usando func
s.compare(s2[, align_axis, keep_shape, …]) Compara s com s2 exibindo differenças
s.copy([deep]) Cópia do objeto s, índices e valores
s.corr(s2) Correlação de s com s2, excluindo NaNs
s.count([level]) Número de observações na s, excluindo NaN/nulls
s.cov(s2[, min_periods, ddof]) Covariância da s, excluindo NaN/nulls
s.cummax([axis, skipna]) Máximo cumulativo
s.cummin([axis, skipna]) Mínimo cumulativo
s.cumprod([axis, skipna]) Produto cumulativo
s.cumsum([axis, skipna]) Soma cumulativa
s.describe([percentiles, include, exclude, …]) Gera descrição estatística
s.div(s2) Divisão (float) de s por s2, e/e
s.divmod(s2) Divisão inteira e módulo de s por s2, e/e
s.dot(s2) Produto interno entre a s e s2
s.drop([labels]) Retorna s com labels removidos
s.drop_duplicates([keep, inplace]) Remove elementos duplicados de s
s.dropna() Remove valores faltantes de s
s.duplicated([keep]) Exibe valores duplicados na s
s.eq(s2) Boleano, igualdade entre s e s2, e/e
s.equals(s2) Booleano: True se s contém os mesmos elementos que s2
s.ewm([com, span, halflife, alpha, …]) Calcula exponencial com peso
s.explode([ignore_index]) Transforma cada elemento de um objeto tipo lista em uma linha
s.fillna([value, method, axis, inplace, …]) Substitui valores NA/NaN usando método especificado
s.first(offset) Seleciona período inicial de uma série temporal usando offset.
s.first_valid_index() Índice do primeiro valor não NA/null
s.floordiv(s2) Divisão inteira da s por s2, e/e
s.ge(s2) Booleana: maior ou igual entre s e s2, e/e
s.get(key) Retorna item correspondente à key
s.groupby([by, axis, level, as_index, sort, …]) Agrupa a s
s.gt(s2[, level, fill_value, axis]) Booleana: se s é maior que s2, e/e
s.head([n]) Retorna os n primeiros valores
s.hist() Plota histograma da s usando matplotlib.
s.idxmax([axis, skipna]) Label do item de maior valor
s.idxmin([axis, skipna]) Label do item de menor valor
s.interpolate([method, axis, limit, inplace, …]) Preenche valores NaN usando metodo de interpolação
s.isin(valores) Booleano: se elementos da s estão contidos em valores
s.isna() Booleano: se existem valores ausentes
s.isnull() Booleano: se existem valores nulos
s.item() Primeiro elemento dos dados como escalar do Python
s.items() Iteração (lazy) sobre a tupla (index, value)
s.iteritems() Iteração (lazy) sobre a tupla (index, value)
s.keys() Alias de index
s.kurt([axis, skipna, level, numeric_only]) Kurtosis imparcial
s.kurtosis([axis, skipna, level, numeric_only]) Idem
s.last(offset) Seleciona período final de uma série temporal usando offset
s.last_valid_index() Índice do último valor não NA/null
s.le(s2) Booleana: se s é menor ou igual a s2, e/e
s.lt(s2[, level, fill_value, axis]) Booleana: se s é menor que s2, e/e
s.mad([axis, skipna, level]) Desvio médio absoluto dos valores da s
s.mask(cond[, s2, inplace, axis, level, …]) Substitui valores sob condição dada
s.max([axis, skipna, level, numeric_only]) Valor máximo
s.mean([axis, skipna, level, numeric_only]) Média dos valores
s.median([axis, skipna, level, numeric_only]) Mediana dos valores
s.memory_usage([index, deep]) Memória usada pela s
s.min([axis, skipna, level, numeric_only]) Menor dos valores da s
s.mod(s2[, level, fill_value, axis]) Módulo de s por s2, e/e
s.mode([dropna]) Moda da s
s.mul(s2[, level, fill_value, axis]) Multiplicação de s por s2, e/e
s.multiply(s2[, level, fill_value, axis]) Multiplicação de s por s2, e/e
s.ne(s2[, level, fill_value, axis]) Booleana: se s é diferente de s2, e/e
s.nlargest([n, keep]) Retorna os n maiores elementos
s.notna() Booleana: se existem valores não faltantes ou nulos
s.notnull() Idem
s.nsmallest([n, keep]) Retorna os n menores elementos
s.nunique([dropna]) Retorna quantos elementos únicos existem na s
s.pad([axis, inplace, limit, downcast]) O mesmo que DataFrame.fillna() usando method=’ffill’
s.plot O mesmo que pandas.plotting._core.PlotAccessor
s.pop(i) Remove s[i] de s e retorna s[i]
s.pow(s2) Exponential de s por s2, e/e
s.prod([axis, skipna, level, numeric_only, …]) Produto dos elemetos da s
s.product([axis, skipna, level, numeric_only, …]) Idem
s.quantile([q, interpolation]) Valor no quantil dado
s.ravel([order]) Retorna dados como um ndarray
s.rdiv(s2[, level, fill_value, axis]) Divisão (float) de s por s2, e/e
s.rdivmod(s2) Divisão inteira e módulo de s por s2, e/e
s.reindex([index]) Ajusta a s ao novo índice
s.reindex_like(s2[, method, copy, limit, …]) Série com índices em acordo com s2
s.rename([index, axis, copy, inplace, level, …]) Altera o nome (labels) dos índices
s.reorder_levels(order) Reajusta níveis de índices usando order
s.repeat(repeats[, axis]) Repete elementos da s
s.replace([to_replace, value, inplace, limit, …]) Substitui valores em to_replace por value
s.reset_index([level, drop, name, inplace]) Reinicializa índices
s.rfloordiv(s2[, level, fill_value, axis]) Divisão inteira de s por s2, e/e
s.rmod(s2[, level, fill_value, axis]) Modulo da divisão da s por s2, e/e
s.rmul(s2[, level, fill_value, axis]) Multiplicação de s por s2, e/e
s.round([n]) Arredonda valores da s para n casas decimais.
s.rpow(s2[, level, fill_value, axis]) Exponential de s por s2, e/e
s.rsub(s2[, level, fill_value, axis]) Subtração da s por s2, e/e
s.rtruediv(serie[, level, fill_value, axis]) Divisão (float) de s por s2, e/e
s.sample([n, frac, replace, weights, …]) Amostra randomizada de items da s
s.searchsorted(value[, side, sorter]) Índices onde elementos devem ser inseridos para manter ordem
s.sem([axis, skipna, level, ddof, numeric_only]) Erro padrão imparcial da média
s.skew([axis, skipna, level, numeric_only]) Inclinação imparcial
s.sort_index([axis, level, ascending, …]) Reorganiza s usando os índices
s.sort_values([axis, ascending, inplace, …]) Reorganiza s usando seus valores
s.std([axis, skipna, level, ddof, numeric_only]) Desvio padrão da amostra
s.str Usa funções de string sobre s (se string). Ex. s.str.split(“-“)
s.sub(s2) Subtração de s por s2, e/e
s.subtract(serie) Idem
s.sum([axis, skipna, level, numeric_only, …]) Soma dos valores da s
s.tail([n]) Últimos n elementos
s.to_clipboard([excel, sep]) Copia o object para o clipboard do sistema
s.to_csv([path_or_buf, sep, na_rep, …]) Grava a s como arquivo csv
s.to_dict() Converte s para dict {label ⟶ value}
s.to_excel(excel_writer[, sheet_name, na_rep, …]) Grava s como uma planilha Excel
s.to_frame([name]) Converte s em DataFrame
s.to_hdf(path_or_buf, key[, mode, complevel, …]) Grava s em arquivo HDF5 usando HDFStore
s.to_json([path_or_buf, orient, date_format, …]) Converte s em string JSON
s.to_latex([buf, columns, col_space, header, …]) Renderiza objeto para LaTeX
s.to_markdown([buf, mode, index, storage_options]) Escreve a s em formato Markdown (leia)
s.to_numpy([dtype, copy, na_value]) Converte s em NumPy ndarray
s.to_pickle(path[, compression, protocol, …]) Grava objeto serializado em arquivo Pickle
s.to_sql(name, con[, schema, if_exists, …]) Grava elementos em forma de um database SQL
s.to_string([buf, na_rep, float_format, …]) Constroi uma representação string da s
s.tolist() Retorna uma lista dos valores
s.to_list() idem
s.transform(func[, axis]) Executa func sobre elementos de s
s.truediv(s2) Divisão (float) de s por s2, e/e
s.truncate([before, after, axis, copy]) Trunca a s antes e após índices dados
s.unique() Retorna os valores da s, sem repetições
s.update(s2) Modifica s usando valores de s2, usando índices iguais
s.value_counts([normalize, sort, ascending, …]) Retorna s com a contagem de valores únicos
s.var([axis, skipna, level, ddof, numeric_only]) Variância imparcial dos valores da s
s.view([dtype]) Cria uma nova “view” da s
s.where(cond[, serie, inplace, axis, level, …]) Substitui valores se a condição cond = True
🔺Início do artigo

Bibliografia

Consulte a bibliografia no final do primeiro artigo dessa série.

Dataframes – Seleções e Ordenamento


Outras formas do construtor de dataframes

dataFrames

Se um dicionário aninhado (onde os valores associados às chaves externas são outros dicionários) é passado no construtor de um DataFrame o pandas interpretará as chaves externas como nomes das colunas e as chaves internas como índices das linhas. Na ausência de um par chave:valor em um ou mais dos dicionários o campo receberá o valor NaN.

» dic = {'Pedro': {'Prova 1': 5.4, 'Prova 3': 7.9},
                'Ana': {'Prova 1': 8.5, 'Prova 2': 9.7, 'Prova 3': 6.6},
               'Luna': {'Prova 2': 5.0, 'Prova 3': 7.0, 'Prova 4': 6.0}
             }
» dfNotas = pd.DataFrame(dic)
» dfNotas
↳
            Pedro    Ana   Luna
  Prova 1     5.4    8.5    NaN
  Prova 3     7.9    6.6    7.0
  Prova 2     NaN    9.7    5.0
  Prova 4     NaN    NaN    6.0

Se os nomes das linhas e das colunas forem fornecidos eles serão exibidos.

» dfNotas.index.name = 'Prova';
» dfNotas.columns.name = 'Aluno'
» dfNotas
↳
  Aluno      Pedro    Ana    Luna
  Prova             
  Prova 1     5.4     8.5     NaN
  Prova 3     7.9     6.6     7.0
  Prova 2     NaN     9.7     5.0
  Prova 4     NaN     NaN     6.0

Com frequência importamos de fontes externas, como faremos abaixo, uma fonte de dados e precisamos verificar sua integridade. Por ex., para encontrar elementos ausentes, preenchidos como NaN, usamos dataFrame.isnull() (o mesmo que pd.isnull(dataFrame)). Para saber quantos valores nulos existem usamos dataFrame.isnull().sum(), que fornece a soma dos campos True para cada campo.

» dfNotas.isnull()    # o mesmo que pd.isnull(dfNotas)
↳ Aluno       Pedro     Ana      Luna
  Prova             
  Prova 1     False     False    True
  Prova 3     False     False    False
  Prova 2     True      False    False
  Prova 4     True      True     False

» dfNotas.isnull().sum()
↳ Aluno
  Pedro    2
  Ana      1
  Luna     1
  dtype: int64

O método dataFrame.notna() (o mesmo que dataFrame.notnull() e o inverso de dataFrame.isnull()) retorna um dataframe booleano com True onde os campos não são nulos. Para inserir manualmente campos nulos usamos a constante pd.NaT e para eliminar linhas (ou colunas) contendo nulos aplicamos dataframe.dropna().

» # para eliminar linhas contendo nulos (o default é axis=0)
» dfNotas.dropna()
↳ Aluno     Pedro     Ana     Luna
  Prova             
  Prova 3     7.9     6.6      7.0
    
» # para eliminar colunas contendo nulos
» dfNotas.dropna(axis=1)
# todas são eliminadas pois existem NaN em todas as colunas

Evidentemente é necessário ter cuidado ao eliminar linhas ou colunas com NaN. Em muitos casos pode ser necessário substituir esses valores por outros, dependendo da aplicação. Para fazer a alteração no próprio frame use o parâmetro inplace = True.

Colunas e índices são objetos do tipo array e podem ser usados com alguns métodos de conjuntos.

» dfNotas.columns
↳ Index(['Pedro', 'Ana', 'Luna'], dtype='object', name='Aluno')

» dfNotas.index
↳ Index(['Prova 1', 'Prova 3', 'Prova 2', 'Prova 4'], dtype='object', name='Prova')

» 'Ana' in dfNotas.columns      # True
» 'Ann' in dfNotas.columns      # False
» 'Prova 5' in dfNotas.index    # False

O mesmo ocorre se o dicionário contiver Series como valores, sendo as chaves usadas como nomes das colunas e os índices das series usados como índices das linhas.

» serie1 = pd.Series([1, 2, 3, 4], index=['a', 'b', 'c', 'd'])
» serie2 = pd.Series([5, 6, 7, 8], index=['a', 'b', 'c', 'd'])
» serie3 = pd.Series([9, 0, -1, -2], index=['a', 'b', 'c', 'd'])

» dic = {'A':serie1, 'B':serie2, 'C':serie3 }
» pd.DataFrame(dic)
↳ 
      A   B   C
  a   1   5   9
  b   2   6   0
  c   3   7  -1
  d   4   8  -2

Dataframes podem ser criados recebendo Series no construtor.

» disciplinas = pd.Series(['Matemática', 'Física', 'História', 'Geografia'])
» notas = pd.Series([9.0, 5.4, 7.7, 8.9])
» df = pd.DataFrame({'Disciplina':disciplinas, 'Notas': notas})
» df
↳     Disciplina   Notas
  0   Matemática     9.0
  1       Física     5.4
  2     História     7.7
  3    Geografia     8.9

Outros objetos podem ser passados como argumento no construtor:

  • Ndarray (do NumPy) de 2 dimensões,
  • dicionário de arrays, listas ou tuples (todas as sequências devem ter o mesmo comprimento),
  • dicionários de arrays NumPy, de Series ou de outros dicionários,
  • listas de dicionários, Series, listas ou tuplas,
  • Series ou outro dataframe.

Tratamento de dados usando pandas.dataframe

Para os testes e demonstrações que se seguem vamos usar dados reais para demonstrar algumas funcionalidades úteis dos pandas.dataframes.

Fonte de dados

Para realizar os teste com dataframes vamos utilizar os dados encontrados no Gapminder nessa url: 08_gap-every-five-years.tsv. Esse é um arquivo contendo dados com um registro em cada linha e os valores na linha separados por tabs, (tabulação). Esse arquivo pode ser baixado para o seu computador e depois importado para um dataframe ou, como usamos abaixo, importada diretamente do site de Jennifer Bryan (jennybc): Gapminder, no Github.

O arquivo original tem o seguinte formato,

country      continent       year   lifeExp        pop     gdpPercap
Afghanistan       Asia       1952    28.801    8425333   779.4453145
Afghanistan       Asia       1957    30.332    9240934   820.8530296
Afghanistan       Asia       1962    31.997   10267083     853.10071
...

onde os espaços entre valores são tabulações (\t, no python). A primeira linha contém os ‘headers’ ou títulos das colunas. Traduziremos esses títulos da seguinte forma: country ⟼ pais, continent ⟼ continente, year ⟼; ano, lifeExp ⟼ expVida (expectativa de vida), pop ⟼ populacao, gdpPercap ⟼ pibPercap (produto interno bruto, percapita).

» import pandas as pd
» import numpy as np
» # Usando arquivo encontrado no Gapminder
» url =(
      'https://raw.githubusercontent.com/jennybc/'
      'gapminder/master/data-raw/08_gap-every-five-years.tsv'
       )
» url
↳ 'https://raw.githubusercontent.com/jennybc/gapminder/master/data-raw/08_gap-every-five-years.tsv'

» # criamos o dataframe dfPaises. O arquivo importado tem campos separados por tabs 
» dfPaises = pd.read_csv(url, sep='\t')

» # o dataframe tem 1704 linhas e 6 colunas
» dfPaises.shape
↳ (1704, 6)

» dfPaises.head()
↳
          country   continent    year   lifeExp        pop     gdpPercap
  0   Afghanistan        Asia    1952    28.801    8425333    779.445314
  1   Afghanistan        Asia    1957    30.332    9240934    820.853030
  2   Afghanistan        Asia    1962    31.997    10267083   853.100710
  3   Afghanistan        Asia    1967    34.020    11537966   836.197138
  4   Afghanistan        Asia    1972    36.088    13079460   739.981106
  1704 rows × 6 columns 
» # renomeando os campos para nomes em português
» dfPaises.rename(columns={'country':'pais',
                           'continent':'continente',
                           'year':'ano',
                           'lifeExp':'expVida',
                           'pop':'populacao',
                           'gdpPercap':'pibPercap',
                          }, inplace=True)
» # ficamos assim
» dfPaises.columns
↳ Index(['pais', 'continente', 'ano', 'expVida', 'populacao', 'pibPercap'], dtype='object')

» # para reordenar as colunas em sua exibição
» dfPaises = dfPaises[['continente', 'pais', 'ano', 'expVida', 'populacao', 'pibPercap']]

Podemos obter uma visão geral do conjunto de dados importados usando dois métodos. dataframe.info() retorno os nomes das colunas, quantos valores não nulos, seus dtypes, e memória usada nesse armazenamento. Por aí vemos que nossos dados não possuem valores nulos. Caso esses existissem eles teriam que ser localizados e tratados devidamente. O método df.describe() retorna um dataframe contendo a contagem count dos valores (nesse caso, o número de linhas), a média mean desses valores, o desvio padrão std, o valor mínimo e máximo, min, max e os quartis em 25%, 50%, 75%.

» dfPaises.info()
↳ <class 'pandas.core.frame.DataFrame'>
  RangeIndex: 1704 entries, 0 to 1703
  Data columns (total 6 columns):
   #   Column      Non-Null Count  Dtype  
  ---  ------      --------------  -----  
   0   continente  1704 non-null   object 
   1   pais        1704 non-null   object 
   2   ano         1704 non-null   int64  
   3   expVida     1704 non-null   float64
   4   populacao   1704 non-null   int64  
   5   pibPercap   1704 non-null   float64
  dtypes: float64(2), int64(2), object(2)
  memory usage: 80.0+ KB

» dfPaises.describe()
↳ 
                   ano          expVida         populacao         pibPercap
 count      1704.00000      1704.000000      1.704000e+03       1704.000000
  mean      1979.50000        59.474439      2.960121e+07       7215.327081
  std         17.26533        12.917107      1.061579e+08       9857.454543
  min       1952.00000        23.599000      6.001100e+04        241.165876
  25%       1965.75000        48.198000      2.793664e+06       1202.060309
  50%       1979.50000        60.712500      7.023596e+06       3531.846988
  75%       1993.25000        70.845500      1.958522e+07       9325.462346
  max       2007.00000        82.603000      1.318683e+09     113523.132900

Gravação e recuperação de dados em arquivos pickle

Após a verificação de integridade dos dados e a realização das alterações básicas necessárias é boa ideia salvar em disco o dataframe nesse momento. Para isso usamos pandas.to_pickle(dfFrame, 'nomeArquivo.pkl'), gravando um arquivopickle. Para recuperá-lo em qualquer momento usamos dfPaises = pandas.read_pickle('./dados/dataframePaises.pkl').

» # gravando um arquivo pickle
» pd.to_pickle(dfPaises, './dados/dataframePaises.pkl')    

» # mais tarde esse dataframe pode ser recuperado
» del dfPaises   # para limpar essa variável
» dfPaises = pd.read_pickle('./dados/dataframePaises.pkl')
» # o dataframe é recuperado

Seleção e filtragem

As principais formas de seleção de um ou mais valores de um dataframe são os métodos dataframe.loc(), dataframe.iloc(), dataframe.at e dataframe.iat. Um subconjunto de dados do dataframe, seja por seleção de linhas, colunas ou ambas, é denominado de fatia ou slice.

A principal diferença entre loc (at) e iloc (iat) é a seguinte: loc é baseado em labels ou nomes das linhas ou colunas, enquanto iloc é baseado nos índices numéricos (mesmo que tenham nomes) sempre com base 0.

  • dataframe.at[row_label, column_label]
  • dataframe.iat[row_position, column_position]
  • dataframe.loc[row_label, column_label]
  • dataframe.iloc[row_position, column_position]

Na tabela abaixo nos referiremos a um dataframe nomeado como df. (S) se refere a uma Series retornada, (D) a um dataframe.

Operações df.iat e df.at retorna: (índices são posições linhas/colunas)
df.iat[m,n] elemento da m-ésima linha, n-ésima coluna
df.at[lblLinha, lblColuna] elemento linha/coluna relativas aos labels lblLinha/lblColuna
Operações df.iloc retorna: (índices são posições das linhas/colunas)
df.iloc[n] n-ésima linha (S)
df.iloc[[n]] n-ésima linha (D)
df.iloc[-n] n-ésima linha, contando do final
df.iloc[i,j:, n] linhas i, até j (exclusive), coluna n (S)
df.iloc[[i,j,k]:[m,n,o]] linhas i, j, k, colunas m, n, o
df.iloc[:, n] n-ésima coluna (S)
df.iloc[:, [n]] n-ésima coluna (D)
df.iloc[:,-1] última coluna
df.iloc[i:j,m:n] linhas i até j (exclusive), colunas m até n (inclusive)
Operações df.loc retorna: (índices linhas/colunas se referem aos seus labels)
df.loc[n] linha de índice n (S)
df.loc[[n]] linha de índice n (D)
df.loc[:] todas as linhas e colunas (D)
df.loc[:, 'col'] todas as linhas, coluna ‘col’ (S)
df.loc[:, ['col']] todas as linhas, coluna ‘col’ (D)
df.loc[:, ['col1', 'col2']] todas as linhas, colunas ‘col1’ e ‘col2’ (D)
df.loc[i:j, ['col1', 'col2']] linhas com índices de i até i (inclusive), colunas ‘col1’ e ‘col2’ (D)
df.loc[[i,j,k] , ['col1', 'col2']] linhas com índices i, j, k, colunas ‘col1’ e ‘col2’ (D)
df.loc[i:j, 'col1':'coln']] linhas com índices i até j (inclusive), colunas ‘col1’ até ‘coln’ (inclusive) (D)
Atalhos o mesmo que
df['col1'] ou df.col1 df.loc[:, ‘col1’]] (S)
df[['col1', 'col2']] df.loc[:, [‘col1’, ‘col2’]] (D)

Em todos esses métodos uma exceção de KeyError é levantada se um índice ou label não existir na dataframe.

Se o index da linha coincidir com sua posição então df.loc[n] e df.iloc[n] serão as mesmas linhas. Isso nem sempre é verdade, como se verá abaixo com o reordenamento das linhas.

São incorretas as sintaxes: df.loc[-n], df.loc[:, n], df.loc[:, [n]] com n numérico pois os labels devem ser fornecidos.

Exemplos de consultas e seleções

dataframe.iloc()

Para outros exemplos vamos usar o dataframe já carregado, dfPaises, para fazer consultas e seleções, primeiro usando dataframe.iloc(). Lembramos que a contagem de índices sempre se inicia em 0:

» # lembrando que dfPaises.iloc[[0]] é um dataframe, dfPaises.iloc[0] é uma Series
» # primeira linha, pelo índice    
» dfPaises.iloc[[0]]
↳    continente            pais    ano    expVida    populacao     pibPercap
  0        Asia     Afghanistan   1952     28.801      8425333    779.445314

» # última linha, pelo índice    
» dfPaises.iloc[[-1]]
↳         continente          pais     ano    expVida    populacao     pibPercap
  1703        Africa      Zimbabwe    2007     43.487     12311143    469.709298    

» # linhas 15 até 20 (exclusive), colunas 2 até 5 (exclusive)
» dfPaises.iloc[15:20, 2:5]
↳      ano   expVida  populacao
  15  1967     66.22    1984060
  16  1972     67.69    2263554
  17  1977     68.93    2509048
  18  1982     70.42    2780097
  19  1987     72.00    3075321

» # linhas 1, 3, 5 , colunas 2, 5
» dfPaises.iloc[[1,3,5],[2,5]]
↳     ano   pibPercap
  1  1957   820.853030
  3  1967   836.197138
  5  1977   786.113360

» # linhas 1, 3, 5, última coluna
» dfPaises.iloc[[1,3,5],-1]
↳ 1    820.853030
  3    836.197138
  5    786.113360
  Name: pibPercap, dtype: float64

» # todas as linhas, coluna 3
» dfPaises.iloc[:, [3]].head()
↳     expVida
  0    28.801
  1    30.332
  2    31.997
  3    34.020
  4    36.088

» # linhas 0, 3, 6, 24; colunas 0, 3, 5
» dfPaises.iloc[[0,3,6,24], [0,3,5]]
↳    continente   expVida    pibPercap
  0        Asia    28.801   779.445314
  3        Asia    34.020   836.197138
  6        Asia    39.854   978.011439
  24     Africa    43.077  2449.008185

A seleção das linhas nos dois métodos é diferente. Em dataframe.loc[m,n] linhas com labels de m até n (inclusive) são selecionadas. Em dataframe.iloc[m,n] são selecionadas linhas com índices (numéricos) de m até n (exclusive).

» # iloc[m,n] exibe linhas m até n (exclusive)
» dfPaises.iloc[1:2]
↳   continente         pais   ano  expVida  populacao   pibPercap
  1       Asia  Afghanistan  1957   30.332    9240934   820.85303    

» # loc[m:n] exibe linhas m até n (inclusive)
» dfPaises.loc[1:2]
↳     continente          pais    ano   expVida   populacao    pibPercap
  1         Asia   Afghanistan   1957    30.332     9240934    820.85303
  2         Asia   Afghanistan   1962    31.997    10267083    853.10071
dataframe.loc()

Os próximos testes são feitos com dataframe.loc(), que deve receber os labels como índices.

» # todas as linhas, só colunas 'ano' e 'populacao' (limitadas por head())
» dfPaises.loc[:,['ano','populacao']].head()
↳        ano     populacao
  0     1952       8425333
  1     1957       9240934
  2     1962      10267083
  3     1967      11537966
  4     1972      13079460

» # linhas 3 até 6 (inclusive), só colunas 'ano' e 'expVida'
» dfPaises.loc[3:6,['ano', 'expVida']]
↳        ano     expVida
  3     1967      34.020
  4     1972      36.088
  5     1977      38.438
  6     1982      39.854

» # todas as linhas, só colunas 'ano' (restritas por head())
» dfPaises.loc[:, 'ano'].head()
↳ 0    1952
  1    1957
  2    1962
  3    1967
  4    1972

Métodos df.loc, df.iloc, df.at e df.iat

Para explorar um pouco mais a diferença no uso de df.loc e df.iloc vamos criar um dataframe bem simples e sem valores nulos.

» dic = {'Pedro': {'Prova 1': 5.4, 'Prova 2': 6.2, 'Prova 3': 7.9},
         'Ana':  {'Prova 1': 8.5, 'Prova 2': 9.7, 'Prova 3': 6.6},
         'Luna': {'Prova 1': 5.0, 'Prova 2': 7.0, 'Prova 3': 4.3}
        }
» dfNotas = pd.DataFrame(dic)
» dfNotas
↳           Pedro     Ana    Luna
  Prova 1     5.4     8.5     5.0
  Prova 2     6.2     9.7     7.0
  Prova 3     7.9     6.6     4.3

df.loc e df.at usa labels de linhas e colunas.
df.iloc e df.iat usa números (índices) de linhas e colunas.

Nos comentários listamos seleções usando df.iloc para se obter o mesmo retorno.

» dfNotas.loc['Prova 1','Luna']             # dfNotas.iloc[0,2]
↳ 5.0

» dfNotas.loc['Prova 1']                    # dfNotas.iloc[0] (Series)
↳ Pedro    5.4
  Ana      8.5
  Luna     5.0

» dfNotas.loc[['Prova 1']]                  # dfNotas.iloc[[0]] (dataframe)
↳           Pedro   Ana   Luna
  Prova 1     5.4   8.5    5.0

» dfNotas.loc[['Prova 1','Prova 2']]        # dfNotas.iloc[0:2] (dataframe)
↳           Pedro    Ana   Luna
  Prova 1     5.4    8.5    5.0
  Prova 2     6.2    9.7    7.0

» dfNotas.loc['Prova 1': 'Prova 3']         # dfNotas.iloc[0:3] (dataframe)
↳           Pedro    Ana   Luna
  Prova 1     5.4    8.5    5.0
  Prova 2     6.2    9.7    7.0
  Prova 3     7.9    6.6    4.3

» dfNotas.loc[['Prova 1'],['Ana','Luna']]   # dfNotas.iloc[[0],[1,2]]  (dataframe)
↳           Ana   Luna
  Prova 1   8.5    5.0

» dfNotas.loc['Prova 1':'Prova 3', 'Pedro':'Luna']   # dfNotas.iloc[0:3,0:3] (dataframe)
↳           Pedro    Ana    Luna
  Prova 1     5.4    8.5     5.0
  Prova 2     6.2    9.7     7.0
  Prova 3     7.9    6.6     4.3

» dfNotas.loc[:,['Luna']]                   # dfNotas.iloc[:,[2]]
↳           Luna
  Prova 1    5.0
  Prova 2    7.0
  Prova 3    4.3

Observe que em dfNotas.iloc[0:3,0:3] são selecionadas as linhas de índices 0, 1 e 2 e colunas 0, 1 e 2.

Análogos à df.loc e df.iloc temos, respectivamente, df.at[lblLinha, lblColuna] e df.iat[m,n] onde lblLinha, lblColuna se referem aos labels e m, n aos índices das linhas/colunas. Ambos recebem um par e retornam um único valor do dataframe. Quando aplicados em uma Series iat e at recebem um único índice/label localizador de posição.

» dfNotas.iat[2,1]
↳ 6.6
» dfNotas.iloc[0].iat[1]                 # o mesmo que dfNotas.loc['Prova 1'].iat[1]
↳ 8.5
» dfNotas.at['Prova 1', 'Luna']
↳ 5.0
» dfNotas.loc['Prova 1'].at['Ana']       # o mesmo que dfNotas.loc['Prova 1'].iat[1]
↳ 8.5

Nenhuma das duas formas de seleção de uma slice (.loc ou .iloc) copiam um dataframe por referência, como ocorre com numPy.ndarrays. Por exemplo, df = dfNotas.iloc[:,[2]] é uma cópia da 3ª coluna, e não uma referência ou view. Ela pode ser alterada sem que o dataframe original seja modificado. Se um novo valor for atribuído ao slice diretamente, no entanto, o dataframe fica alterado.

» df = dfNotas.iloc[:,[2]]
» df.Luna = 10
» display(df,dfNotas)
↳           Luna
  Prova 1     10
  Prova 2     10
  Prova 3     10

↳           Pedro   Ana   Luna
  Prova 1     5.4   8.5    5.0
  Prova 2     6.2   9.7    7.0
  Prova 3     7.9   6.6    4.3

» # no entanto se o slice receber atribuição direta o dataframe fica alterado
» dfNotas.iloc[:,[2]] = 10

» dfNotas
↳           Pedro    Ana    Luna
  Prova 1     5.4    8.5    10.0
  Prova 2     6.2    9.7    10.0
  Prova 3     7.9    6.6    10.0

» # para inserir valores diferentes outro dataframe de ser atribuído ao slice
» dic = {'Luna': {'Prova 1': 8.5, 'Prova 2': 7.9, 'Prova 3': 10}}
» dfLuna = pd.DataFrame(dic)
» dfNotas.iloc[:,[2]] = dfLuna

» dfNotas
↳           Pedro    Ana   Luna
  Prova 1     5.4    8.5    8.5
  Prova 2     6.2    9.7    7.9
  Prova 3     7.9    6.6   10.0

» # alternativamente, um np.array com shape apropriado pode ser atribuído ao slice
» arrLuna =np.array([2.3, 4.5, 5.6]).reshape(3,1)
» dfNotas.iloc[:,[2]] = arrLuna
» dfNotas
↳          Pedro   Ana   Luna
  Prova 1    5.4   8.5    2.3
  Prova 2    6.2   9.7    4.5
  Prova 3    7.9   6.6    5.6

Na atribuição dfNotas.iloc[:,[2]] = 10 houve o broadcasting de 10 para uma forma compatível com o slice.

Para que a atribuição seja bem sucedida, sem necessidade de broadcasting, um objeto de mesmo formato deve ser atribuído. No caso dfNotas.iloc[:,[2]].shape = dfLuna.shape = (3, 1) (3 linhas, 1 coluna). O mesmo ocorre com a atribuição de um array do numpy.

Manipulando linhas e colunas

Um array booleano pode ser passado como índice de um dataframe. Apenas as linhas correspondentes ao índice True será exibida. Alguns métodos de string estão disponíveis para testes em campos, como df['campo'].str.startswith('str') e df['campo'].str.endswith('str') (começa e termina com). O teste df['campo'].isin(['valor1', 'valor2'])] retorna True se os campos estão contidos na lista.

Para os exemplos usamos o dataframe dfPaises.

» # seleção por array booleano
» dfPaises.loc[dfPaises['ano'] == 2002].head(3)
↳      continente         pais   ano   expVida   populacao    pibPercap
  10         Asia  Afghanistan   2002   42.129    25268405   726.734055
  22       Europe      Albania   2002   75.651     3508512  4604.211737
  34       Africa      Algeria   2002   70.994    31287142  5288.040382

» # quais os paises tem nome começados com 'Al'
» dfPaises.loc[dfPaises['pais'].str.startswith('Al')]['pais'].unique()
↳ array(['Albania', 'Algeria'], dtype=object)

» # quais os paises tem nome terminados em 'm'
» dfPaises.loc[dfPaises['pais'].str.endswith('m')]['pais'].unique()
↳ array(['Belgium', 'United Kingdom', 'Vietnam'], dtype=object)

» # quantas linhas se referem à 'Europe' e 'Africa'
» dfPaises.loc[dfPaises['continente'].isin(['Europe', 'Africa'])].shape[0]
↳ 984

» dfPaises.loc[(dfPaises['continente']=='Africa') & (dfPaises['ano']==1957)].head(4)
↳       continente         pais     ano    expVida    populacao      pibPercap
  25        Africa     Algeria    1957     45.685     10270856    3013.976023
  37        Africa      Angola    1957     31.999      4561361    3827.940465
  121       Africa       Benin    1957     40.358      1925173     959.601080
  157       Africa    Botswana    1957     49.618       474639     918.232535


» # paises e anos com população < 7000 ou expectativa de vida > 82
» dfPaises.loc[(dfPaises['populacao'] < 70000) | (dfPaises['expVida'] > 82)][['ano','pais']]
↳          ano    pais
  420     1952    Djibouti
  671     2007    Hong Kong, China
  803     2007    Japan
  1296    1952    Sao Tome and Principe
  1297    1957    Sao Tome and Principe
  1298    1962    Sao Tome and Principe
Operador significa
& and, e
| or, ou
~ not, negação

O método arr.unique() acima foi aplicado para ver quais os países satisfazem as condições, sem repetições. arr.shape é uma tupla (número linhas, número colunas). Os últimos exemplos fazem testes compostos usando os operadores & (and, e lógico) e | (or, ou lógico).

Se nenhum campo for submetido ao teste lógico todos os valores do dataframe são usados. O mesmo ocorre com a aplicação de uma função, como mostrado para uma função lambda.

» # novos teste com loc e iloc
» dic = {'Pedro': {'Prova 1': 5.4, 'Prova 2': 6.2, 'Prova 3': 7.9},
         'Ana':  {'Prova 1': 8.5, 'Prova 2': 9.7, 'Prova 3': 6.6},
         'Luna': {'Prova 1': 5.0, 'Prova 2': 7.0, 'Prova 3': 4.3}
          }
» dfNotas = pd.DataFrame(dic)
» dfNotas
↳           Pedro     Ana    Luna
  Prova 1     5.4     8.5     5.0
  Prova 2     6.2     9.7     7.0
  Prova 3     7.9     6.6     4.3

» # o teste retorna um df com o mesmo shape que dfNotas
» dfNotas > 6
↳             Pedro    Ana    Luna
  Prova 1     False   True   False
  Prova 2      True   True    True
  Prova 3      True   True   False

» # os campos do df são filtrados pelo df booleano
» dfNotas[dfNotas > 6]
↳           Pedro     Ana    Luna
  Prova 1     NaN     8.5     NaN
  Prova 2     6.2     9.7     7.0
  Prova 3     7.9     6.6     NaN

Funções lambda

Uma função pode ser aplicada sobre elementos de uma coluna específica ou sobre todas as colunas. Veremos mais tarde detalhes sobre o uso de dataframe.apply().

» dfNotas
↳           Pedro     Ana    Luna
  Prova 1     5.4     8.5     5.0
  Prova 2     6.2     9.7     7.0
  Prova 3     7.9     6.6     4.3

» # uma função aplicada à todos os elementos do df
» dfNotas.apply(lambda x: x**2)
↳            Pedro     Ana     Luna
  Prova 1    29.16   72.25    25.00
  Prova 2    38.44   94.09    49.00
  Prova 3    62.41   43.56    18.49

Funções lambda que retornam valores booleanos podem ser usadas para filtragem dos campos de um dataframe. No exemplo dfPaises['pais'].apply(lambda x: len(x)) == 4 retorna True para as linhas onde o campo pais tem comprimento de 4 letras.

» dfPaises.loc[dfPaises['pais'].apply(lambda x: len(x)) == 4].head(2)
↳      continente    pais    ano   expVida   populacao     pibPercap
  264      Africa    Chad   1952    38.092     2682462   1178.665927
  265      Africa    Chad   1957    39.881     2894855   1308.495577

# são os países com nomes de 4 letras:
» set(dfPaises.loc[dfPaises['pais'].apply(lambda x: len(x)) == 4]['pais'])
↳ {'Chad', 'Cuba', 'Iran', 'Iraq', 'Mali', 'Oman', 'Peru', 'Togo'}

# o mesmo que
# dfPaises.loc[dfPaises['pais'].apply(lambda x: len(x)) == 4]['pais'].unique()  # (um array)

O seletor pode ser composto de mais testes, ligados pelos operadores lógicos & e |.

» # paises/anos com nomes compostos por mais de 2 palavras e população acima de 50 milhões
» dfPaises.loc[(dfPaises['pais'].apply(lambda x: len(x.split(' '))) > 2) &
               (dfPaises['populacao']>50_000_000)]

↳      continente    pais                  ano   expVida   populacao    pibPercap
  334      Africa    Congo, Dem. Rep.     2002    44.966    55379852   241.165876
  335      Africa    Congo, Dem. Rep.     2007    46.462    64606759   277.551859

Ordenamento com Sort

Para ordenar um dataframe podemos usar o método sort, com a seguinte sintaxe:

dataframe.sort_values(by=['campo'], axis=0, ascending=True, inplace=False)
onde
by pode ser uma string ou lista com o nome ou nomes dos campos, na prioridade de ordenamento,
axis{0 ou ‘index’, 1 ou ‘columns’} default 0, indica o eixo a ordenar,
ascending=True/False se ordenamento é crescente/decrescente.

Existem vários outros parâmetros para o controle de ordenamentos, como pode ser lido no API reference do pandas.

Muitas informações importantes sobre um conjunto de dados podem ser obtidas apenas pela inspecção dos dados. Por exemplo, podemos encontrar respostas para:

  • que país do mundo teve, em qualquer ano, o PIB percapita mais elevado?
  • no ano de 2007 (o último de nossa lista), quais são os 5 países com maior população, e quais são os 5 com PIB mais baixo, no mundo?
  • no ano de 2002, quantos países no mundo tinham PIB percapita acima e abaixo da média?
# encontramos o maior pib percapita e a linha que corresponde a ele   
» dfMax = dfPaises[dfPaises['pibPercap']==dfPaises['pibPercap'].max()]
» dfMax
↳     continente    pais   ano   expVida  populacao    pibPercap
  853       Asia  Kuwait  1957    58.033     212846  113523.1329

» # para formatar uma resposta amigável
» ano = dfMax['ano'].values[0]
» pais = dfMaxPib['pais'].values[0]
» pibP = dfMaxPib['pibPercap'].values[0]

» print('O PIB percapita máximo foi de {} e ocorreu no {} em {}.'.format(pibP, pais, ano))
↳ O PIB percapita máximo foi de 113523.1329 e ocorreu no Kuwait em 1957.

» # ordenando em ordem decrescente
» dfPaises.sort_values(by=['pibPercap'], ascending=False).iloc[[0]]
↳      continente     pais   ano  expVida  populacao     pibPercap
  853        Asia   Kuwait  1957   58.033     212846   113523.1329

Observe que dfMax['ano'] é uma Series que, se exposta diretamente, não contém apenas o ano. Por isso extraimos dele o valor, 1º campo: dfMax['ano'].value[0]. Idem para pais e pibPercap.

Claro que podemos também ordenar o dataframe em ordem descrecente no campo pibPercap e pegar apenas a 1ª linha.
dataframe.iloc[[0]] foi usado para pegar a 1ª linha, cujo índice é 853. A mesma linha seria retornada com dataframe.loc[[853]], o que mostra, mais uma vez, a diferença entre df.loc e df.iloc.

Para encontrar os 5 países com maior população em 2007 usamos a mesma técnica de ordenamento. Primeiro filtramos pelo ano = 2007, ordenamos por população, ordem inversa, e pegamos os 5 primeiros. Para exibir o resultado podemos transformar o dataframe em string, sem os índices.

Para encontrar os 5 países com maior população em 2007, e os 5 com menor PIB:

» # dataframe com 5 maiores populações em 2007
» popMax = dfPaises[dfPaises['ano']==2007].sort_values(by=['populacao'], ascending=False).head()

» print(popMax[['pais','populacao']].to_string(index=False))
↳          pais  populacao
          China 1318683096
          India 1110396331
  United States  301139947
      Indonesia  223547000
         Brazil  190010647

» # o 5 países com menor pib:
» # criamos um dataframe apenas do ano 2007 e acrescentamos o campo pib
» # pib = pibPercap * populacao
» df2007 = dfPaises[dfPaises['ano']==2007]
» df2007['pib'] = df2007['pibPercap'] * df2007['populacao']       

» # são os países com menor pib em 2007
» df2007.sort_values(by=['pib']).head()['pais']
↳ 1307    Sao Tome and Principe
  323                   Comoros
  635             Guinea-Bissau
  431                  Djibouti
  563                    Gambia
  Name: pais, dtype: object

# se não precisamos mais do df, podemos apagá-lo
» del df2007

Para saber quantos países tem PIB percapita acima e abaixo da média em 2002 primeiro encontramos essa média. Depois selecionamos as linhas que satisfazem com pibPercap >= media e pibPercap < media. Para saber quantas linhas restaram contamos, por exemplo, quantos elementos existem em seu index.

» # média do pibPercap em 2002 (um escalar)
» media2002 = dfPaises[dfPaises.ano==2002]['pibPercap'].mean()
» acima = dfPaises[(dfPaises.ano==2002) & (dfPaises.pibPercap ≥= media2002)].index.size
» abaixo = dfPaises[(dfPaises.ano==2002) & (dfPaises.pibPercap < media2002)].index.size

» print('[Dos {} países, {} tem PIB percapita acima da média, {} abaixo da média.'.format(acima+abaixo, acima, abaixo))
↳ Dos 142 países, 44 tem PIB percapita acima da média, 98 abaixo da média.

Obtenção e análise de um slice : Brasil

Em diversas circunstâncias queremos fazer análise de apenas um slice da dataframe geral. Além de simplificar o conjunto de campos podemos conseguir com isso um uso menor de espaço em memória e maior velocidade de processamento.
Podemos, por ex., obter um dataframe separado apenas com a os dados referentes ao Brasil. Passando como índice o array booleano dfPaises['pais'] == 'Brazil' apenas as linhas relativas a esse país serão retornadas.

» dfBrasil = dfPaises[dfPaises['pais'] == 'Brazil'][['ano', 'expVida', 'populacao', 'pibPercap']]
» dfBrasil.head()
↳ 
         ano  expVida   populacao     pibPercap
  168   1952   50.917    56602560   2108.944355
  169   1957   53.285    65551171   2487.365989
  170   1962   55.665    76039390   3336.585802
  171   1967   57.632    88049823   3429.864357
  172   1972   59.504   100840058   4985.711467

O dataframe dfBrasil tem os mesmos índices que aos do segmento de dfPaises, de onde ele foi retirado. Para restabelecer esses índices usamos dataFrame.reset_index(). Se utilizado com o parâmetro drop=True o índice antigo é excluído (e perdido), caso contrário é copiado como uma coluna do dataframe. Para atribuir um nome para o índice usamos dataframe.index.rename('novoNome', inplace=True).

» # os índices iniciais são
» dfBrasil.index
↳ Int64Index([168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179], dtype='int64')

» # resetamos os índices, abandonando a coluna de índices iniciais
» dfBrasil.reset_index(drop=True, inplace=True)
» # novos índices
» dfBrasil.index
↳ RangeIndex(start=0, stop=12, step=1)

» dfBrasil.index.rename('id', inplace=True)
» # o dataframe fica assim:
» dfBrasil.head(3)
↳       ano     expVida     populacao       pibPercap
  id
  0    1952      50.917      56602560     2108.944355
  1    1957      53.285      65551171     2487.365989
  2    1962      55.665      76039390     3336.585802

Podemos usar um campo qualquer como index, com qualquer dtype. No caso abaixo usamos o campo ano como índice.

» # vamos usar o campo ano como index
» dfBrasil.set_index('ano', inplace=True)
» dfBrasil.head(3)
↳           expVida      populacao       pibPercap
  ano             
  1952.0     50.917     56602560.0     2108.944355
  1957.0     53.285     65551171.0     2487.365989
  1962.0     55.665     76039390.0     3336.585802

» # agora os índices passam a ser o ano
» dfBrasil.loc[1997]            # é uma Series
↳ expVida      6.938800e+01
  populacao    1.685467e+08
  pibPercap    7.957981e+03
  Name: 1997.0, dtype: float64

» # dfBrasil.loc[[1997]]            # é um dataframe

Para restaurar a coluna ano copiamos o índice para essa coluna e restauramos o índice.

» # restauramos a coluna ano
» dfBrasil['ano'] = dfBrasil.index
» # e resetamos o indice
» dfBrasil.reset_index(drop=True, inplace=True)

» dfBrasil.head(3)
↳    expVida   populacao     pibPercap    ano
  0   50.917    56602560   2108.944355   1952
  1   53.285    65551171   2487.365989   1957
  2   55.665    76039390   3336.585802   1962

Linhas podem ser inseridas de várias formas. Um delas consiste em criar novos dataframes com as linhas a inserir e concatenar como a dataframe inicial. Para isso usamos pandas.concat(): pd.concat([dfInicio, dfFinal]).
Vamos inserir linhas com dados fictícios, apenas para efeito de aprendizado.

» colunas = ['expVida','populacao','pibPercap','ano']      # nomes das colunas, na ordem dos dados
» valores1 = [48.0,45000000,2000.0,1951]                   # valores a inserir no ínicio (ano 1951)
» valores2 = [75.0, 200000000, 9500.0, 2008]               # valores a inserir no final (ano 2008)
» dfP = pd.DataFrame([valores1], columns=colunas)          # df a inserir no ínicio
» dfU = pd.DataFrame([valores2], columns=colunas)          # df a inserir no final
» dfBrasil = pd.concat([dfP, dfBrasil])                    # 1ª linha + dfBrasil
» dfBrasil = pd.concat([dfBrasil, dfU])                    # dfBrasil + última linha

» # agora a 1ª linha é
» dfBrasil.iloc[[0]]
↳    expVida   populacao   pibPercap   ano
  0     48.0   45000000       2000.0  1951

» # a última linha é
» dfBrasil.iloc[[-1]]
↳    expVida   populacao  pibPercap   ano
  0     75.0   200000000     9500.0  2008

» # como os índices ficaram duplicados e desordenados fazemos um reordenamento
» dfBrasil.reset_index(drop=True, inplace=True)

» dfBrasil
↳      expVida     populacao       pibPercap      ano
  0     48.000      45000000     2000.000000     1951
  1     50.917      56602560     2108.944355     1952
  2     53.285      65551171     2487.365989     1957
  ------------ linhas 3 até 11 omitidas ----------------
  12    72.390     190010647     9065.800825     2007
  13    75.000     200000000     9500.000000     2008

Como essas linhas não contém dados corretos, vamos apagá-las. Para usamos dataframe.drop(linha, axis=0, inplace = True), onde linha é o label, que pode não ser numérico) da linha ou seu índice (numérico). Várias linhas podem ser apagadas com dataframe.drop([linha0,...,linhan], axis=0, inplace = True).

» # apagar linhas 0 e 13: axis = 0 se refere às linhas
» dfBrasil.drop([0,13], axis=0, inplace = True)

» # para reordenar os índices
» dfBrasil.reset_index(drop=True, inplace=True)

» # recolocar a coluna 'ano' no início
» dfBrasil = dfBrasil[['ano', 'expVida', 'populacao', 'pibPercap']]
» dfBrasil

# o estado do dataframe agora é
↳ dfBrasil
        ano    expVida     populacao       pibPercap
  0    1952     50.917      56602560     2108.944355
  1    1957     53.285      65551171     2487.365989
  2    1962     55.665      76039390     3336.585802
  ------------ linhas 3 até 8 omitidas ----------------
  9    1997     69.388     168546719     7957.980824
  10   2002     71.006     179914212     8131.212843
  11   2007     72.390     190010647     9065.800825

Vamos inserir uma coluna, atribuindo a ela um escalar (um valor único). Aqui ocorre, como nas Series, o broadcasting, onde o escalar é transformado em uma Series de tamanho apropriado antes de ser inserido na nova coluna. Todas as linhas terão o valor 42 no campo “novoCampo”.

Em seguida alteramos o valor dessa coluna em uma linha específica, usando dataframe.loc(númeroLinha, nomeColuna) ou dataframe.iloc(numeroLinha, numeroColuna). Depois, como essa é uma coluna indesejada, nos a apagamos usando dataframe.drop('nomeColuna', axis=1, inplace=True).

» dfBrasil['novoCampo'] = 42
» dfBrasil.head(3)
↳        ano     expVida     populacao       pibPercap   novoCampo
  0     1952      50.917      56602560     2108.944355          42
  1     1957      53.285      65551171     2487.365989          42
  2     1962      55.665      76039390     3336.585802          42

» # alteramos o 'novoCampo' na linha 1 (usando loc)
» # e a coluna 4 ('novoCampo') na linha 2 (usando iloc, fornecendo o índice)
» dfBrasil.loc[1,'novoCampo'] = 123456
» dfBrasil.iloc[2,4] = 22222

» dfBrasil.head(3)
↳      ano     expVida     populacao       pibPercap     novoCampo
  0   1952      50.917      56602560     2108.944355     42
  1   1957      53.285      65551171     2487.365989     123456
  2   1962      55.665      76039390     3336.585802     22222

» # apagamos essa coluna com drop
» dfBrasil.drop('novoCampo', axis=1, inplace=True)
» # o dataframe fica como no início

Um campo pode ser inserido como resultado de operações entre outros campos. No caso abaixo criamos uma coluna pib que é o produto das colunas populacao × pibPercap. O resultado é aplicado, em cada linha, à nova coluna, em notação científica. Na 1ª linha pib = 1.193716 × 1011.

Outra coluna marca a passagem de quando a expectativa de vida do brasileiro ultrapassa os 60 anos.

» dfBrasil.loc[:,'pib'] = dfBrasil['pibPercap'] * dfBrasil['populacao']
» dfBrasil.head(4)
↳        ano    expVida       populacao       pibPercap     pib
  0     1952     50.917      56602560.0     2108.944355     1.193716e+11
  1     1957     53.285      65551171.0     2487.365989     1.630498e+11
  2     1962     55.665      76039390.0     3336.585802     2.537119e+11
  3     1967     57.632      88049823.0     3429.864357     3.019989e+11

» # inserindo coluna 'acima60'(†)
» dfBrasil.loc[:,'acima60'] = dfBrasil['expVida'] > 60
» dfBrasil.loc[3:6,['ano','expVida','acima60']]
↳      ano   expVida  acima60
  3   1967    57.632    False
  4   1972    59.504    False
  5   1977    61.489     True
  6   1982    63.336     True
  
» dfBrasil[dfBrasil['acima60']]
» # todas as linhas com expVida > 60 são exibidas (output omitido)

» # as colunas podem ser removidas (para ficarmos com o dataframe original)
» dfBrasil.drop(['acima60', 'pib'], axis=1, inplace=True)

() dfBrasil['expVida'] > 60 é uma Series booleana.

Objetos de índices

Em um dataframe, assim como nas Series, a informação relativa aos índices e seus nomes (labels ), assim como os nomes dos eixos, são armazenados em objetos Index (índice). O objeto Index é imutável (não pode ser alterado após a construção).

» pdSerie = pd.Series(range(4), index=['a1', 'a2', 'a3', 'a4'])
» index = pdSerie.index
» index
↳ Index(['a1', 'a2', 'a3', 'a4'], dtype='object')
» # o índice é uma sequência (pode ser lido em slices)
» index[2]
↳ 'a3'
» index[2:]
↳ Index(['a3', 'a4'], dtype='object')

» # o index é imutável
» index[0] = 'A'
↳ TypeError: Index does not support mutable operations

» # já vimos que índices não fornecidos são preenchidos como um range
» pd.Series(range(4)).index
↳ RangeIndex(start=0, stop=4, step=1)
Uma UA é a distância média da Terra ao Sol.
1 UA ≈ 149,6 × 109 m.

No exemplo abaixo construimos primeiro um objeto Index usando pandas.Index(lista). Em seguida construimos uma Series usando esse index, contendo como valores as distâncias dos planeta até o Sol, em unidaddes astronômicas (UA). Com a Series inicializamos um dataframe com o mesmo index.

» # objeto index
» labels = pd.Index(np.array(['mercurio', 'venus', 'terra']))
» labels
↳ Index(['mercurio', 'venus', 'terra'], dtype='object')

» # Serie construída com esse index
» planetas = pd.Series([0.387, 0.723, 1], index=labels)
» planetas
↳ mercurio    0.387
  venus       0.723
  terra       1.000
  dtype: float64

» # o index da Series é o mesmo objeto que labels
» planetas.index is labels
↳ True

» # essa Series pode ser usada para construir um dataframe
» dfPlanetas = pd.DataFrame(planetas)
» dfPlanetas
↳             0
  mercurio    0.387
  venus       0.723
  terra       1.000

» # o index do dataframe é o mesmo que o da Series
» dfPlanetas.index is labels
↳ True

» # alteramos o nome da coluna
» dfPlanetas.rename(columns={0:'distancia'}, inplace=True)
» dfPlanetas
↳           distancia
  mercurio      0.387
  venus         0.723
  terra         1.000

Podemos inserir uma coluna, por exemplo, relativa ao diâmetro dos planetas (comparados ao diâmetro da Terra), atribuindo valores à uma nova coluna de nome ‘diametro’. O objeto atribuído deve ter o mesmo shape (ou passar por broadcasting). Alterar a ordem das colunas, o que pode ser feito com df.reindex(listaColunas), altera todo o dataframe (embora não inplace). O objeto retornado se ajusta de acordo com os índices fornecidos.

» # inserir uma nova coluna
» dfPlanetas['diametro'] = pd.Series([0.382, 0.949, 1], index=labels)
» dfPlanetas
↳         distancia   diametro
  mercurio    0.387      0.382
  venus       0.723      0.949
  terra       1.000      1.000

» # as colunas estão em um objeto Index
» dfPlanetas.columns
↳ Index(['distancia', 'diametro'], dtype='object')

» type(dfPlanetas.columns)
↳ pandas.core.indexes.base.Index

» 'distancia' in dfPlanetas.columns
↳ True

» # podemos alterar a ordem das colunas com reindex
» dfPlanetas.reindex(['venus','terra','mercurio'])
↳       distancia    diametro
  venus     0.723       0.949
  terra     1.000       1.000
  mercurio  0.387       0.382

» # podemos ordenar os índices para ordenar o dataframe
» idx = dfPlanetas.index
» idx = idx.sort_values()
» idx
↳ Index(['mercurio', 'terra', 'venus'], dtype='object')

» dfPlanetas.reindex(idx)
↳         distancia   diametro
  mercurio    0.387      0.382
  terra       1.000      1.000
  venus       0.723      0.949

Diferentes de um conjunto (set) objetos Index podem ter índices repetidos. Se índices inseridos não correspondem à dados existentes estes são preenchidos com NaN. Os parâmetros method='bfill' (ou “ffill” forçam as colunas (ou linhas) com NaN a serem preenchidos com valores das colunas (ou linhas) anteriores ou posteriores. Claro que reindexações podem ser também obtidas com df.loc e df.iloc.

» # índices de linhas repetidos
» duplicados = pd.Index(['mercurio', 'venus', 'terra', 'mercurio', 'marte'])
» duplicados
↳ Index(['mercurio', 'venus', 'terra', 'mercurio', 'marte'], dtype='object')

» dfPlanetas.reindex(duplicados)   # default é axis = 0
↳         distancia   diametro
  mercurio    0.387      0.382
  venus       0.723      0.949
  terra       1.000      1.000
  mercurio    0.387      0.382
  marte         NaN        NaN  

» # índices de colunas repetidos
» duplicados = pd.Index(['distancia', 'diametro', 'diametro', 'distancia', 'massa'])
» dfPlanetas.reindex(duplicados, axis=1)   # sobre colunas
↳          distancia  diametro  diametro  distancia  massa
  mercurio     0.387     0.382     0.382      0.387    NaN
  venus        0.723     0.949     0.949      0.723    NaN
  terra        1.000     1.000     1.000      1.000    NaN

» # method='bfill' lê valor da coluna anterior
» dfPlanetas.reindex(duplicados, axis=1, method='bfill')
↳          distancia  diametro  diametro  distancia    massa
  mercurio     0.387     0.382     0.382      0.387    0.387
  venus        0.723     0.949     0.949      0.723    0.723
  terra        1.000     1.000     1.000      1.000    1.000

» # use method='ffill' para copiar coluna posterior

» # reindexação com loc
» nCol = pd.Index(['diametro', 'distancia'])
» dfPlanetas.loc[['venus','terra'], ['diametro', 'distancia']]
↳        diametro   distancia
  venus     0.949       0.723
  terra     1.000       1.000
» # nCol pode ser uma lista: nCol = ['diametro', 'distancia']

De posse dos índices das linhas e colunas qualquer uma delas pode ser apagada com df.drop(lista, axis). As operações retornam o dataframe modificado, sem alterar o original, a menos que seja marcado o parâmetro inplace=True. Nesse caso os dados removidos serão perdidos.

» dfPlanetas
↳          distancia   diametro
  mercurio     0.387      0.382
  venus        0.723      0.949
  terra        1.000      1.000

» # apagando linhas (axis = 0 é default)
» dfPlanetas.drop(['venus', 'mercurio'])
↳     distancia     diametro
  terra     1.0         1.0

» # apagando colunas
» dfPlanetas.drop(['distancia'], axis=1)
↳           diametro
  mercurio     0.382
  venus        0.949
  terra        1.000

Os seguintes argumentos são usados com reindex

Argumento descrição
index Index ou sequência a ser usada como index,
method forma de interpolação: ‘ffill’ preenche com valor posterior, ‘bfill’ com valor anterior,
fill_value valor a usar quando dados não existentes são introduzidos por reindexing (ao invés de NaN),
limit quando preenchendo com valor anterior ou posterior, intervalo máximo a preencher (em número de elementos),
tolerance quando preenchendo com valor anterior ou posterior, intervalo máximo a preencher para valores inexatos (em distância numérica),
level combina Index simples no caso de MultiIndex; caso contrário seleciona subset,
copy se True, copia dados mesmo que novo índice seja equivalente ao índice antigo; se False, não copia dados quando índices são equivalentes.

Métodos e propriedades de Index

Método descrição
append concatena outro objeto Index objects, gerando novo Index
difference calcula a diferença de conjunto como um Index
intersection calcula intersecção de conjunto
union calcula união de conjunto
isin retorna array booleano indicando se cada valor está na coleção passada
delete apaga índice, recalculando Index
drop apaga índices passados, recalculando Index
insert insere índice, recalculando Index
is_monotonic retorna True se indices crescem de modo monotônico
is_unique returns True se não existem valores duplicados no Index
unique retorna índices sem repetições
🔺Início do artigo

Bibliografia

Consulte bibliografia completa em Pandas, Introdução neste site.

Nesse site:

NumPy, Álgebra Linear


Métodos de ordenamento

Um array do NumPy pode ser ordenado com o método array.sort(), que transforma o array inplace. Se o array que passa por ordenamento for uma seção (uma view ) de um array maior esse array original também será alterado. O método np.sort(array) retorna cópia ordenada, sem alterar o original. Não há um método ou parâmetro predefinido para fazer o ordenamento inverso. Para isso usamos a mesma sintaxe que retorna uma lista invertida, lista[::-1].

» # um array qualquer sem ordenamento
» arrOriginal = np.array([-1, 20, 13, -5, 1, 0, 9, 3, -3, 7])
» # uma cópia (não uma view)
» arr = arrOriginal.copy()

» # arr.sort ocorre inplace
» arr.sort()
» arr
↳ array([-5, -3, -1,  0,  1,  3,  7,  9, 13, 20])

» # para obter a lista ordenada invertida (sem alterar a original)
» arr[::-1]
↳ array([20, 13,  9,  7,  3,  1,  0, -1, -3, -5])

» # retorna para o array original (não ordenado)
» arr = arrOriginal.copy()
» np.sort(arr)
↳ array([-5, -3, -1,  0,  1,  3,  7,  9, 13, 20])

» # arr não foi alterado
» arr
↳ array([-1, 20, 13, -5,  1,  0,  9,  3, -3,  7])

» # o array ordenada em ordem inversa também pode ser obtido (inplace) da seguinte forma
» arr[::-1].sort()
» arr
↳ array([20, 13,  9,  7,  3,  1,  0, -1, -3, -5])

Para arrays como mais de um eixo podemos informar ao longo de qual deles queremos ordenar os valores. Em qualquer dos casos cada linha (ou cada coluna) será ordenada independentemente. Em qualquer dos casos, com o ordenamento das colunas, as linhas perdem seu alinhamento, caso exista. Por exemplo, se cada linha se referia à uma medida específica, no ordenamento os dados ficam desalinhados. Idem para ordenamento das linhas.

» # inicializamos um array 3 × 4
» lista = ([ [ 3,   2,  1,  -1],
             [-3,   4,  -6,  5],
             [ 3,   0,   -9,  15]
           ])
» arr = np.array(lista)
» arr
↳ array([[ 3,  2,  1, -1],
         [-3,  4, -6,  5],
         [ 3,  0, -9, 15]])

» # ordenamos ao longo das colunas
» arr.sort(0)
» arr
↳ array([[-3,  0, -9, -1],
         [ 3,  2, -6,  5],
         [ 3,  4,  1, 15]])

» # reconstituindo o array original
» arr = np.array(lista)

» # ordenamos ao longo das linhas
» arr.sort(1)
» arr
↳ array([[-1,  1,  2,  3],
         [-6, -3,  4,  5],
         [-9,  0,  3, 15]])

Gravação e leitura de arrays em arquivos


Em muitas situações é útil gravar resultados finais ou etapas intermediárias de cálculo para posterior finalização. NumPy permite a gravação de arquivos contendo os arrays em formato de texto ou binário. Os métodos principais são np.save() e np.load(). Por default um array é gravado em arquivo com extensão .npy em formato binário. Formatos mais sofisticados para textos e arrays tabulares são encontrados no pandas.

» # criando um array 
» arr = np.linspace(0, 22, 12).reshape(3,4)
» arr
↳ array([[ 0.,  2.,  4.,  6.],
         [ 8., 10., 12., 14.],
         [16., 18., 20., 22.]])
» # esse array será gravado em arrGravado.npy.
» # a extensão será acrescentada se não fornecida
↳ np.save('arrGravado', arr)

» # apagamos o array e depois o recarregamos
» del arr
» arr = np.load('arrGravado.npy')
» arr
↳ array([[ 0.,  2.,  4.,  6.],
         [ 8., 10., 12., 14.],
         [16., 18., 20., 22.]])

Mais de um array podem ser gravados no mesmo arquivos arq.npz. Os arrays são passados como argumentos de keyword. Na recuperação dos arrays um objeto tipo dicionário é carregado, tendo os arrays associados às chaves que foram as keywords passadas.

A mesma operação de armazenar vários arrays em arquivo pode ser realizada compactando-se os dados para que ocupem menos espaço em disco. Isso é feito com np.savez_compressed('nomeDoArquivoaCompactado.npz', a=arr1, b=arr2, ...).

» # definimos 2 arrays
» arr1 = np.array([1,2,3])
» arr2 = np.array([4,5,6])
» # e os gravamos em disco
» np.savez('variosArrays.npz', a1=arr1, a2=arr2)

» # apagamos e recuperamos os arrays
» del arr1, arr2
» arrays = np.load('variosArrays.npz')

» # o load carrega um objeto tipo dicionário
» arr1, arr2 = arrays['a1'], arrays['a2']

» display(arr1, arr2)
↳ array([1, 2, 3])
↳ array([4, 5, 6])

» # a mesma operação, amazenando os arrays em arquivo compactado
» np.savez_compressed('arraysCompactados.npz', a=arr1, b=arr2)

Métodos de conjuntos

Algumas operações básica podem ser aplicadas sobre arrays, tratando seus elementos como um conjunto. Uma delas, usada com frequência, é a seleção de elementos únicos no array, feita com np.unique. O método np.in1d(array, valores), testa se elementos de um array estão também em outro, retornando um array booleano. O objeto valores pode ser uma lista, tupla ou outro array unidimensional.

» arr = np.array([1,2,3,4,4,3,2,1])
» # elementos de arr sem repetições
» np.unique(arr)
↳ array([1, 2, 3, 4])

» arrString = np.array(['Ana','Luiz','Paulo','Ana','Otto','Paulo','Otto','Paulo'])
» np.unique(arrString)
↳ array(['Ana', 'Luiz', 'Otto', 'Paulo'], dtype='<U5')

» # quais dos elementos de arr estão em (1,3,5)
» np.in1d(arr, (1,3,5))
↳ array([ True, False,  True, False, False,  True, False,  True])

» # o argumento pode ser uma tupla, lista ou array
» teste = np.array([1,3,5])
» np.in1d(arr, teste)
↳ array([ True, False,  True, False, False,  True, False,  True])

» # o mesmo com array de strings
» txt = np.array(['Otto','Luiz','Ana'])
» np.in1d(arrString, txt)
↳ array([ True,  True, False,  True,  True, False,  True, False])

» arrString = np.array(['A','F','D','A','O','P','C','D'])
» lista = np.in1d(arrString, ['A', 'B', 'C'])

» # os seguintes elementos estão na lista 
» arrString[lista]
↳ array(['A', 'A', 'C'], dtype='<U1')

» # os seguintes elementos não estão na lista 
» arrString[~lista]
↳ array(['F', 'D', 'O', 'P', 'D'], dtype='<U1')


O método np.intersect1d(x, y) retorna a interseção entre x e y.
O método np.setdiff1d(x, y) retorna x-y.
setxor1d(x, y): elementos em x ou y, mas não em ambos.

» # para verificar intersect1d
» # múltiplos de 23 até 1000
» arr1 = np.arange(0,1000,23)
» # múltiplos de 27 até 1000
» arr2 = np.arange(0,1000,27)

» # múltiplos de 23 e 27 até 1000 (é a interseção entre os dois conjuntos)
» np.intersect1d(arr1, arr2)
↳ array([  0, 621])

» # para verificar setdiff1d
» arr1 = np.array([5, 3, 1, 9, 7, 6])
» arr2 = np.array([5, 4, 1, 8, 2, 3])
» np.setdiff1d(arr1,arr2)
↳ array([6, 7, 9])

» # para verificar setxor1d
» np.setxor1d(arr1,arr2)
↳ array([2, 4, 6, 7, 8, 9])

Funções de conjuntos em NumPy:

Método descrição
unique(x) conjunto de elementos únicos em x,
intersect1d(x, y) elementos comuns em x e y, (ordenados),
union1d(x, y) união dos elementos em x e y, (ordenados),
in1d(x, y) array booleano indicando se cada elemento de x está em y,
setdiff1d(x, y) conjunto diferença: elementos em x que não estão em y,
setxor1d(x, y) conjunto diferença simétrica: elementos em x ou y, mas não em ambos.

Numpy: Álgebra linear


A Álgebra Linear é uma parte da matemática muito importante nas aplicações científicas e da engenharia. Para o cálculo simbólico o módulo Sympy (Matemática Simbólica em Python) oferece muitos métodos interessantes e úteis, inclusive para a álgebra linear.

É uma notação útil denotar os arrays da seguinte forma:
Um array unidimensional (um vetor) é uma coleção de elementos \(A_M = \{a_{i}\}\), onde \(i = 0, …,M-1\) para um array de rank = 1. Diferente da notação matemática usual os índices são contados a partir de 0. Seu shape = (M,).
Um array bidimensional (uma matriz) é uma coleção de elementos \(A_{MN} = \{a_{ij}\}\) onde \(i = 0, …,M-1; j = 0, …,N-1; \) para um array de rank = 2. Seu shape=(M,N) e rank=2.
Arrays de ranks superiores são generalizações, com mais eixos acresentados. Em arrays 3-dimensionais, digamos arr3D.shape = (r,m,n), temos r matrizes m × n.
Arrays de ranks superiores são generalizações, com mais eixos acresentados. Em arrays 3-dimensionais, digamos arr3D.shape = (r,m,n), temos r matrizes m × n.

Alguns dos métodos mais comuns usados na álgebra linear estão no módulo numpy.linalg, descrito abaixo.

Produto Matricial

O produto de matrizes, que é diferente da operação * definida previamente e que consiste na mera multiplicação dos termos e/e, está definido em numpy. As dimensões devem ser compatíveis. Por exemplo, o produto
Am,n × Bn,p = Cm,p. A sintaxe do produto de matrizes A por B é np.dot(A,B) ou A.dot(B).

» # produto matricial
» A = np.arange(0, 9).reshape(3, 3)
» B = np.arange(0, 3).reshape(3, 1)
» A
↳ array([[0, 1, 2],
         [3, 4, 5],
         [6, 7, 8]])

» B
↳ array([[0],
         [1],
         [2]])

» A * B
↳ array([[ 0,  0,  0],
         [ 3,  4,  5],
         [12, 14, 16]])

» A + B
↳ array([[ 0,  1,  2],
         [ 4,  5,  6],
         [ 8,  9, 10]])

» A.dot(B)      # o mesmo que np.dot(A,B)
↳ array([[ 5],
         [14],
         [23]])

» # 6 *0 + 7*1 + 8*2 = 23   # é o elemento da 3º linha do produto
» # B.dot(A) não está definida
» # O quadrado da matriz A, A2 = A.dot(A)
» A.dot(A)
↳ array([[ 15,  18,  21],
         [ 42,  54,  66],
         [ 69,  90, 111]])

Matemáticamente a operação acima para \(A \cdot B\) (A.dot(B)) é representada como:
$$
\left[ \begin{array}{ccc}
0 & 1 & 2\\
3 & 4 & 6\\
6 & 7 & 8
\end{array} \right] \left[ \begin{array}{c}
0\\
1\\
2
\end{array} \right] = \left[ \begin{array}{l}
5\\
14\\
23
\end{array} \right] .
$$

Transposta e inversões de eixos

A tansposta de uma matriz é a matriz obtida da original trocando-se suas linhas por colunas. Essa é uma operação comum na análise de dados e na álgebra linear e pode ser obtida com o método transposta = array.transpose() ou seu atalho transposta = array.T. Em notação matemática, se \(A_{MN} = \{a_{ij}\}\) sua transposta é \(A{^T}_{NM} = \{a_{ji}\}\).

» import numpy as np
» # uma matriz (2 ×3 ) qualquer para exemplo
» arr = np.arange(0,6).reshape(2,3)
» arr
↳ array([[0, 1, 2],
         [3, 4, 5]])
       
» # sua transposta é (3 × 2) 
» transp = arr.T
» transp
↳ array([[0, 3],
         [1, 4],
         [2, 5]])

» # o produto matricial (dot) é (2 × 2) 
» np.dot(arr,transp)
↳ array([[ 5, 14],
         [14, 50]])

» # observe que o produto não é comutativo
» # (a ordem é relevante) transp.arr é (3 × 3)
» np.dot(transp, arr)
↳ array([[ 9, 12, 15],
         [12, 17, 22],
         [15, 22, 29]])

Em matrizes de ordem superior a operação de transposição permite que se informe quais os eixos serão transpostos. Um array arr3D do exemplo abaixo, com shape = (2,3,4), que pode ser vista como 2 matrizes 3 × 4 se torna um array com 3 matrizes 2 × 4 através da operação arr3D.transpose(1,0,2), onde o 1º eixo é permutado com o 2º (o 3º fica inalterado).

» arr3D = np.arange(24).reshape((2, 3, 4))
» arr3D
↳ array([[[ 0,  1,  2,  3],
          [ 4,  5,  6,  7],
          [ 8,  9, 10, 11]],

         [[12, 13, 14, 15],
          [16, 17, 18, 19],
          [20, 21, 22, 23]]])

» # permutando 1º eixo com o 2º
» arr3D.transpose(1,0,2)
↳ array([[[ 0,  1,  2,  3],
          [12, 13, 14, 15]],

         [[ 4,  5,  6,  7],
          [16, 17, 18, 19]],

         [[ 8,  9, 10, 11],
          [20, 21, 22, 23]]])

» # temos 3 matrizes 2 × 4
» arr3D.transpose(1,0,2).shape
↳ (3, 2, 4)

» # se permutarmos 2º com 3º eixo
» arr3D.transpose(0,2,1)
↳ array([[[ 0,  4,  8],
          [ 1,  5,  9],
          [ 2,  6, 10],
          [ 3,  7, 11]],

         [[12, 16, 20],
          [13, 17, 21],
          [14, 18, 22],
          [15, 19, 23]]])

No último caso, permutando 2º com 3º eixo e mantendo o 1º temos as 2 matrizes original transpostas.

A transposição é um caso particular da inversão mais geral de eixos. Isso pode ser feito com array.swapaxes(i,j), que recebe um par de índices referentes aos eixos e os permuta.

» # ainda usando a matriz já definida
» arr3D
↳ array([[[ 0,  1,  2,  3],
          [ 4,  5,  6,  7],
          [ 8,  9, 10, 11]],

         [[12, 13, 14, 15],
          [16, 17, 18, 19],
          [20, 21, 22, 23]]])

» arr3D.swapaxes(0,2)
↳ array([[[ 0, 12],
          [ 4, 16],
          [ 8, 20]],

         [[ 1, 13],
          [ 5, 17],
          [ 9, 21]],

         [[ 2, 14],
          [ 6, 18],
          [10, 22]],

         [[ 3, 15],
          [ 7, 19],
          [11, 23]]])

arr3D.swapaxes(0,2) é idêntica à arr3D.transpose(2,1,0). Quando as dimensões são altas pode ficar difícil visualizar e manipular os arrays. Em alguns casos quebrar o array em blocos pode ser a melhor prática.

Biblioteca numpy.linalg

numpy.linalg é uma subbiblioteca de NumpPy contendo métodos matriciais usuais as operações comuns na álgebra linear, como o cálculo de determinantes e de matrizes inversas similares àquelas usadas no MATLAB e R.

Alguns dos métodos mais comuns usados na álgebra linear:

Método descrição
diag elementos da diagonal (ou fora da diagonal) de matriz quadrada,
diag Se o argumento for array 1-D retorna o array na diagonal e zeros fora da diagonal,
dot multiplicação de matrizes,
trace traço: soma dos elementos da diagonal,
det determinante da matriz,
eig autovalores e autovetores (eigenvalues e eigenvectors) de uma matriz quadrada,
inv a inversa de uma matriz quadrada,
pinv a pseudo inversa de Moore-Penrose de uma matriz,
qr cálculo da decomposição QR,
svd calcula a decomposição de valor singular (SVD),
solve resolve o sistema linear Ax = b para x, sendo A uma matriz quadrada,
lstsq calcula a solução de mínimos quadrados para Ax = b.

A solução de sistemas lineares é uma aplicação comum da álgebra linear. Um exemplo bem simples com equações e 2 incógnitas, cuja solução pode ser vista em matrizes, é:
$$
\left\{ \begin{array}{l}
2 x + y = 5\\
x – 3 y = 6
\end{array} \right.
$$
Ele corresponde a busca do array x (um vetor de 2 variáveis) satisfazendo A x = B onde A e B são listados abaixo.

» A = np.array([[2, 1], [1, -3]])
» B = np.array([5, 6])
» x = np.linalg.solve(A, B)
» # a solução é
» x
↳ array([ 3., -1.])

Portanto a solução (única, nessa caso) é o vetor \(x = (3., -1.)\).

Dada uma matriz \(A\), por definição sua matriz inversa é \(A^{-1}\), satisfazendo \(A.A^{-1} = A^{-1}.A = I\), onde \(I\) é a matriz identidade. Observe que um sistema do tipo \(A.x = B\) fica resolvido se existe a inversa, \(A^{-1}\). Nesse caso basta multiplicar todo o sistema à esquerda (ou à direita) pela inversa: \(A^{-1}.A.x = A^{-1}.B\) que resulta na solução procurada \(x = A^{-1}.B\).

Para o mesmo sistema acima:

» from numpy.linalg import inv
» A = np.array([[2, 1], [1, -3]])
» B = np.array([5, 6])

» # a inversa de A é
» inv(A)
↳ array([[ 0.42857143,  0.14285714],
         [ 0.14285714, -0.28571429]])
       
» # por definição A . inv(A) = identidade †
» # (verificamos que essa é de fato a inversa)
» np.dot(A, inv(A)).round(2)
↳ array([[1., 0.],
         [0., 1.]])

» # a solução do sistema é
» np.dot(inv(A), B)
↳ array([ 3., -1.])

(†): Não se pode esperar que de fato o cálculo de A.inv(A) resulte exatamente na identidade. Devido à aproximações numéricas essa matriz apresentará com frequência elementos pequenos mas não nulos fora da diagonal. Daí o uso de .round(2).

Algumas matrizes não possuem inversas, sendo chamadas de matrizes singular. Seu determinante é \(det(A)= 0\) e, nesse caso, o sistema não tem solução.

» # resolvendo o sistema
» A = np.array([[2, 1], [6, 3]])
» B = np.array([5, 10])
» x = np.linalg.solve(A, B)
↳ LinAlgError: Singular matrix

# ocorre que a matriz A é singular, e o sistema não tem solução
» det(A)
↳ 0.0

O determinante de uma matriz \(A\), denotada por \(det(A)\) está definido no artigo sobre determinantes, nesse site. O exemplo abaixo está resolvido nessa página. Como \(det(A)\ne 0\) ela possui uma inversa \(A^{-1}\), definida de forma que \(A.A^{-1} = \mathbb{I}\), onde \(\mathbb{I}\) é a matriz identidade.

» arr = np.array([[1, -2, 3],[2, 1, -1], [-2, -1, 2]])
» arr
↳ array([[ 1, -2,  3],
         [ 2,  1, -1],
         [-2, -1,  2]])

» np.linalg.det(arr).round(2)
↳ 5.0

» np.linalg.inv(arr)
↳ array([[ 0.2,  0.2, -0.2],
         [-0.4,  1.6,  1.4],
         [ 0. ,  1. ,  1. ]])

» # A A-1 é a identidade
» np.dot(arr,inv(arr))
↳ array([[1., 0., 0.],
         [0., 1., 0.],
         [0., 0., 1.]])

Outra operação importante é a de se encontrar autovetores e autovalores de uma matriz quadrada. Ele consiste em encontrar os valores de \(\lambda\) (os autovalores) e os autovetores \(x\) que satisfazem à equação \(A.x = \lambda x\). Se consideramos a matriz \(A\) como a matriz correspondente a uma transformação linear então os autovetores são aquelas direções mantidas invariantes pela transformação e \(\lambda\) (os autovalores) são os fatores de escala nestas direções.

Por exemplo, no plano uma reflexão no eixo \(Ox\) corresponde à transformação \(R_x(x,y)=(x, -y)\). Ela pode ser escrita em forma matricial como
$$
r_x \left[ \begin{array}{r}
x\\
y
\end{array} \right] = \left[ \begin{array}{rr}
1 & 0\\
0 & – 1
\end{array} \right] \left[ \begin{array}{r}
x\\
y
\end{array} \right] = \left[ \begin{array}{r}
x\\
– y
\end{array} \right].
$$
Portanto queremos encontrar os autovetores e autovalores do array reflex:

» # a reflexão em Ox é descrita por
» reflx = np.array([[1, 0],[0,-1]])
» reflx
↳ array([[ 1,  0],
         [ 0, -1]])
» # seus autovalores e autovetores são
» auto = eig(reflx)
» auto
↳ (array([ 1., -1.]),
↳ array([[1., 0.],
         [0., 1.]]))

» # eig retorna um tupla com 2 elementos
» # o primeiro contem outra tupla com os autovalores (1, -1)
» auto[0]
↳ array([ 1., -1.])

» # o segundo contém outra tupla com os dois autovetores
» auto[1]
↳ array([[1., 0.],
         [0., 1.]])

» auto[1][0]
↳ array([1., 0.])

» auto[1][1]
↳ array([0., 1.])

Isso significa que no plano, a reflexão em torno do eixo \(Ox\) só deixa 2 direções inalteradas: a direção de x, sendo que todos os vetores \((x,0)\) ficam iguais (autovalor = 1), e o eixo \(Oy\). Vetores \((0,y)\) permanecem na mesa direção com o sentido invertido (autovalor = -1).

Brodcasting


Broadcasting se refere ao comportamento de arrays de diferentes dimensões quando operados entre si. Quando uma ou mais dimensões estão ausentes em um dos arrays e as dimensões presentes são compatíveis o array menor e replicado para preencher as dimensões ausentes de forma a que ambas tenham as mesmas dimensões.

Na figura a operação de soma é mostrada. O mesmo comportamento se dá para qualquer outras operação.

Bibliografia

🔺Início do artigo
  • Harrison, Matt: Learning Pandas, Python Tools for Data Munging, Data Analysis, and Visualization,
    Treading on Python Series, Prentiss, 2016.
  • McKinney, Wes: Python for Data Analysis, O’Reilly Media, Sebastopol CA, 2018.
  • McKinney, Wes & Pandas Development Team: pandas: powerful Python data analysis toolkit Release 1.2.1,
  • Miller, Curtis: Hands-On Data Analysis with NumPy and pandas, Packt Publishing, Birmingham, 2018.
  • NumPy, docs.
  • NumPy, Learn.
  • NumPy, linalg.

Sobre Sympy: Matemática Simbólica em Python

Nesse site:

Dataframes: Resumo


Atributos e métodos do pandas.dataFrame


Nas tabelas usamos df para referenciar um dataframe do pandas. Quando necessária a interação com um segundo dataframe ele é designado por dfOutro. A abreviação e/e significa “elemento a elemento”, utilizada quando a operação é aplicada entre todos os elementos de um e outro dataframe, usando elementos na posição (linha, coluna).

Atributos dos dataframes

Atributo descrição
df.at(m,n) valor correspondente à linha=m, coluna=n
df.axes lista representando os eixos do df
df.columns rótulos das colunas do df
df.dtypes dtypes no df
df.flags propriedades associadas ao df
df.iat valor para um par linha/coluna
df.iloc indexação baseada em localização puramente inteira para seleção por posição
df.loc grupo de linhas e colunas por rótulo(s) ou matriz booleana
df.ndim número (int) de eixos/dimensões da matriz
df.shape tupla com a dimensionalidade do df
df.size número (int) de elementos no objeto
df.style um objeto Styler
df.values representação Numpy do df

Métodos dos dataframes

Exibição e consulta ao dataframe

df.at(m,n) valor correspondente à linha=m, coluna=n
df.attrs dict de atributos globais do objeto (experimental)
df.head([n]) as primeiras n linhas
df.info([verbose, buf, max_cols, memory_usage,…]) resumo conciso do dataframe
df.tail([n]) últimas n linhas
df.items() iteração sobre (nome da coluna, Series)
df.iteritems() iteração sobre (nome da coluna, Series)
df.iterrows() iteração sobre linhas de df como pares (index, Series)
df.itertuples([index, nome]) iteração sobre linhas do df como pares nomeados
df.memory_usage([index, deep]) uso de memória de cada coluna em bytes
df.nlargest(n, columns[, keep]) n primeiras linhas ordenadas por colunas em ordem decrescente
df.nsmallest(n, colunas[, manter]) n primeiras n linhas ordenadas por colunas em ordem crescente
df.nunique([axis, dropna]) número de elementos distintos no eixo especificado

Operações matemáticas

Método descrição
df.abs() valor absoluto de cada elemento
df.add(dfOutro[, axis, level, fill_value]) adição de df e dfOutro
df.apply(func[, axis, raw, result_type, args]) aplica função ao longo de um eixo do df
df.applymap(func[, na_action]) aplica uma função a um df e/e
df.div(dfOutro[, axis, level, fill_value]) divisão flutuante de df por dfOutro
df.divide(dfOutro[, axis, level, fill_value]) divisão flutuante de df por dfOutro
df.dot(dfOutro) multiplicação de df por dfOutro
df.eval(expr[, inplace]) avalia a string ‘expr’ contendo operações sobre colunas do df
df.ewm([com, span, halflife, alpha,…]) função exponencial ponderada (EW)
df.floordiv(dfOutro[, axis, level, fill_value]) divisão inteira do df por dfOutro
df.mod(dfOutro[, axis, level, fill_value]) módulo de df por dfOutro e/e
df.mul(dfOutro[, axis, level, fill_value]) multiplicação de df por dfOutro, e/e
df.multiply(dfOutro[, axis, level, fill_value]) multiplicação de df por dfOutro, e/e
df.pow(dfOutro[, axis, level, fill_value]) exponencial do df por dfOutro e/e
df.prod([axis, skipna, level, numeric_only, ...]) produto dos valores sobre o eixo especificado
df.product([axis, skipna, level, numeric_only, ...]) produto dos valores sobre o eixo especificado
df.radd(dfOutro[, axis, level, fill_value]) adição de df e dfOutro e/e, com suporte a fill_na
df.rdiv(dfOutro[, axis, level, fill_value]) divisão float de df por dfOutro e/e
df.rfloordiv(dfOutro[, axis, level, fill_value]) divisão inteira do df e dfOutro e/e
df.rmod(dfOutro[, axis, level, fill_value]) módulo de df e dfOutro e/e
df.rmul(dfOutro[, axis, level, fill_value]) multiplicação de df por dfOutro e/e
df.round([decimals]) arredonda elementos de df para um número dado de casas decimais
df.rpow(dfOutro[, axis, level, fill_value]) exponencial do df por dfOutro e/e
df.rsub(dfOutro[, axis, level, fill_value]) subtração de df e dfOutro e/e
df.rtruediv(dfOutro[, axis, level, fill_value]) divisão float de df por dfOutro e/e
df.sub(dfOutro[, axis, level, fill_value]) subtração de df por dfOutro e/e
df.sum([axis, skipna, level, numeric_only, ...]) soma dos valores sobre o eixo especificado
df.transform(func[, axis]) executa func em elementos de df, sobre eixo especificado
df.truediv(dfOutro[, axis, level, fill_value]) divisão float de df por dfOutro e/e

Formatação, Transposição, Ordenação

Método descrição
Transposição Tij = Mji
df.T a transposta de df
df.transpose(* args[, copiar]) transpor índices e colunas (obter a transposta)
df.droplevel(nível[, axis]) remove colunas com index ou níveis solicitados
df.melt([id_vars, value_vars, var_name,…]) Unpivot dataFrame para um formato largo
df.pivot([index, columns, values]) refaz df organizado por valores de index/column fornecidos
df.pivot_table([values, index, columns,…]) cria tabela dinâmica no estilo planilha como um df
df.sort_index([axis, level, ascendente, ...]) classifica objeto por rótulos (ao longo de um eixo)
df.sort_values ​​(by[, axis, ascending, inplace, ...]) ordena por valores ao longo do eixo especificado
df.stack([level, dropna]) empilha os níveis prescritos das colunas para o índice
df.swapaxes(axis1, axis2[, copy]) inverte eixos com seus valores respectivos
df.swaplevel([i, j, axis]) inverte níveis i e j em umMultiIndex
df.unstack([level, fill_value]) pivot um level dos rótulos de índice (necessariamente hierárquicos)
df.explode(column[, ignore_index]) transforma cada elemento de uma lista em uma linha, mantendo os índices
df.squeeze([axis]) comprime valores de eixo unidimensional para escalares

Métodos estatísticos

Método descrição
df.corr([method, min_periods]) calcula correlação de pares de colunas, excluindo valores NA/nulos
df.corrwith(dfOutro[, axis, drop, method]) calcula correlação de pares
df.count([axis, level, numeric_only]) quantos valores não NA para cada coluna ou linha
df.cov([min_periods, ddof]) calcula covariância de pares de colunas, excluindo NA/nulos
df.cummax([axis, skipna]) máximo cumulativo em um df
df.cummin([axis, skipna]) mínimo cumulativo sobre um eixo do df
df.cumprod([axis, skipna]) produto cumulativo sobre um eixo do df
df.cumsum([axis, skipna]) soma cumulativa sobre eixo do df
df.describe([percentis, include, exclude, ...]) gera descrição estatística
df.diff([points, axis]) primeira diferença discreta do elemento
df.hist([column, by, grid, xlabelsize, xrot,…]) histograma das colunas do df
df.info([verbose, buf, max_cols, memory_usage,…]) resumo conciso do dataframe
df.kurt([axis, skipna, level, numeric_only]) curtose imparcial sobre o eixo especificado
df.kurtosis([axis, skipna, level, numeric_only]) curtose imparcial sobre o eixo especificado
df.mad([axis, skipna, level]) desvio absoluto médio sobre especificado
df.max([axis, skipna, level, numeric_only]) máximo dos valores sobre o eixo especificado
df.mean([axis, skipna, level, numeric_only]) média dos valores sobre o eixo especificado
df.median([axis, skipna, level, numeric_only]) mediana dos valores sobre o eixo especificado
df.min([axis, skipna, level, numeric_only]) mínimo dos valores sobre o eixo especificado
df.mode([axis, numeric_only, dropna]) moda(s) dos elemento ao longo do eixo selecionado
df.pct_change([periods, method, limit, frequency]) alteração percentual entre o elemento atual e o anterior
df.quantil([q, axis, numeric_only, interpolateion]) valores no quantil dado sobre o eixo especificado
df.sample([n, frac, replace, weight,…]) amostra aleatória de itens de um eixo
df.sem([axis, skipna, level, ddof, numeric_only]) erro padrão imparcial da média sobre o eixo especificado
df.std([axis, skipna, level, ddof, numeric_only]) desvio padrão da amostra sobre o eixo especificado
df.var([axis, skipna, level, ddof, numeric_only]) variação imparcial sobre o eixo especificado

Gerenciamento, filtragem e consulta ao dataframe

Método descrição
df.add_prefix(prefixo) acrecenta prefixo nos rótulos (labels)
df.add_suffix(sufixo) acrecenta sufixo nos rótulos (labels)
df.agg([function, axis]) agregar usando function no eixo especificado
df.aggregate([função, axis]) agregar usando function no eixo especificado
df.align(dfOutro[, junção, axis, nível, cópia, ...]) alinha dois objetos em seus eixos com o método especificado
df.append(dfOutro[, ignore_index,…]) anexa linhas de dfOutro ao final de df
df.asof(where[, subset]) última(s) linha(s) sem NaN antes de where
df.assign(** kwargs) atribui novas colunas a um df
df.clip([lower, upper, axis, inplace]) corta (trim) os valores no(s) limite(s) dados
df.combine(dfOutro, func[, fill_value, ...]) combina colunas com dfOutro
df.combine_first(dfOutro) atualiza elementos nulos com elementos de dfOutro, na mesma posição
df.compare(dfOutro[, align_axis, keep_shape,…]) compara com dfOutro> e exibe diferenças
df.copy([deep]) faz cópia do objeto com índices e dados
df.drop([labels, axis, index, columns, level, ...]) remove linhas ou colunas com os rótulos especificados
df.drop_duplicates([subset, keep, inplace, ...]) remove linhas duplicadas
df.expanding([min_periods, center, axis, method]) aplica transformações de expansão
df.filter([itens, like, regex, axis]) filtra linhas ou colunas do df de acordo com os índices especificados
df.first_valid_index() índice do primeiro valor não NA; None se nenhum for encontrado
df.get(key[, default]) item do dataframe correspondente à chave fornecida
df.groupby([by, axis, level, as_index, ...]) agrupa df usando um mapper ou Series de colunas
df.infer_objects() tentativa de inferir dtypes para colunas do objeto
df.insert(loc, column, value[, allow_duplicates]) insere coluna no df no local especificado
df.join(dfOutro[, on, how, lsuffix, rsuffix, sort]) junta colunas de df e dfOutro
df.lookup(row_labels, col_labels) (descontinuado) “indexação extravagante” baseada em rótulos para df
df.mask(cond[, dfOutro, local, axis, level, ...]) substitui valores onde a condição é True
df.merge(right[, how, on, left_on, …]) mesclar df usando database de estilo
df.pipe(func, * args, ** kwargs) aplicação de func(self, * args, ** kwargs)
df.pop(item) remove e retorna o item removido
df.query(expr[, inplace]) consulta colunas de df com uma expressão booleana
df.rank([axis, method, numeric_only, ...]) classificações de dados numéricos (1 a n) ao longo do eixo
df.replace([to_replace, value, inplace, limit,…]) substitui valores em to_replace com value
df.rolling(window[, min_periods, center,…]) cálculos de “rolling window”
df.select_dtypes([include, exclude]) subset de colunas do df com base nos dtypes da coluna
df.skew([axis, skipna, level, numeric_only]) inclinação imparcial sobre o eixo especificado
df.slice_shift([perids, axis]) (descontinuado) Equivalente a shift sem copiar dados
df.sparse alias de pandas.core.arrays.sparse.accessor.SparseFrameAccessor
df.take(indices[, axis, is_copy]) elementos nos índices posicionais fornecidos ao longo de um eixo
df.truncate([before, after, axis, copy]) truncar df antes e depois de valores de índices
df.update(dfOutro[, juntar, substituir,…]) modifique “no local” usando valores não NA de dfOutro
df.value_counts([subset, normalize, …]) retorna série contendo número de linhas exclusivas no df
df.where(cond[, dfOutro, inplace, axis, level, ...]) substitui valores em que a condição é falsa
df.xs(key[, axis, level, drop_level]) seção transversal de df

Testes e comparações

Método descrição
df.empty booleano: se o df está vazio
df.all([axis, bool_only, skipna, nível]) booleano: se todos os elementos são verdadeiros, sobre um eixo (se especificado)
df.any([axis, bool_only, skipna, nível]) booleano: se algum elemento é verdadeiro, sobre um eixo (se especificado)
df.bool() booleano, se um único elemento do df é True/False
df.duplicated([subset, keep]) série booleana marcando linhas duplicadas
df.eq(dfOutro[, axis, level]) boolena: se elementos são iguais a um escalar ou dfOutro, e/e
df.equal(dfOutro) booleano: se dois objetos contêm os mesmos elementos
df.ge(dfOutro[, axis, level]) dataframe booleano, maior ou igual entre df e dfOutro, e/e
df.gt(dfOutro[, axis, level]) dataframe booleano, maior que, entre df e dfOutro, e/e
df.isin(values) booleano: se cada elemento no df está contido em values
df.le(dfOutro[, axis, level]) booleano: menor ou igual entre elementos de df e dfOutro, e/e
df.lt(dfOutro[, axis, level]) booleano: menor dos elementos de df e dfOutro, e/e
df.ne(dfOutro[, axis, level]) booleano, diferente entre df e dfOutro, e/e

Operações com rótulos (labels) e índices

df.index índices do df
df.keys() retorna o index
df.last_valid_index() índice do último valor não NA; None se nenhum valor NA for encontrado
df.reindex([labels, index, columns, axis, ...]) substitue índices de df, com lógica de preenchimento opcional
df.reindex_like(dfOutro[, method, copy, limit, ...]) retorna objeto com índices correspondentes à dfOutro
df.rename([mapper, index, columns, axis, copy, ...]) renomear rótulos dos eixos
df.rename_axis([mapper, index, columns, axis, ...]) define o nome do eixo para o índices ou colunas
df.reorder_levels(order[, axis]) reorganiza níveis de índices usando a ordem em order
df.reset_index([level, drop, inplace, ...]) redefine um índice ou seu nível
df.set_axis(rótulos[, axis, local]) atribui o índice desejado a determinado eixo
df.set_flags(*[, copy, allow_duplicated_labels]) novo objeto com sinalizadores (flags) atualizados
df.set_index(keys[, drop, append, inplace, ...]) define índices de df usando colunas existentes
df.idxmax([axis, skipna]) índice da primeira ocorrência do máximo sobre o eixo especificado
df.idxmin([axis, skipna]) índice da primeira ocorrência do mínimo sobre o eixo especificado

Plots

Método descrição
df.boxplot([column, by, ax, font-size, rot, ...]) traça gráfico de caixa usando as colunas do df
df.plot o mesmo que pandas.plotting._core.PlotAccessor
df.hist([column, by, grid, xlabelsize, xrot,…]) histograma das colunas do df

Serialização e conversões

Método descrição
df.astype(dtype[, copy, errors]) transforma objeto para um dtype especificado
df.convert_dtypes([infer_objects,…]) converte colunas para os melhores dtypes usando dtypes com suporte parapd.NA
df.from_dict(data[, orient, dtype, columns]) constroi df a partir de dict ou similar
df.from_records(data[, index, exclusion,…]) converte ndarray ou dados estruturados em dataframe
df.to_clipboard([excel, sep]) copia objeto para a área de transferência do sistema
df.to_csv([path_or_buf, sep, na_rep,…]) grava objeto em um arquivo de valores separados por vírgula (csv)
df.to_dict([orient, into]) converte df em um dicionário
df.to_excel(excel_writer[, sheet_name, na_rep,…]) grava objeto em uma planilha Excel
df.to_feather(path, **kwargs) grava df no formato binário Feather
df.to_gbq(destination_table[, project_id,…]) grava df em uma tabela Google BigQuery
df.to_hdf(path_or_buf, key[, mode, complevel,…]) grava objeto em um arquivo HDF5 usando HDFStore
df.to_html([buf, columns, col_space, header,…]) renderiza df como uma tabela HTML
df.to_json([path_or_buf, orient, date_format,…]) converte objeto em uma string JSON
df.to_latex([buf, colunas, col_space, cabeçalho,…]) renderiza objeto em uma tabela LaTeX
df.to_markdown([buf, mode, index, ...]) imprima df em formato Markdown
df.to_numpy([dtype, copy, na_value]) converte df em uma matriz NumPy
df.to_parquet([path, engine, ...]) grave df em formato binário parquet
df.to_period([freq, axis, copy]) converte df de DatetimeIndex para PeriodIndex
df.to_pickle(path[, compression, protocol, ...]) serializa o objeto para o arquivo pickle
df.to_records([index, column_dtypes, index_dtypes]) converte df em uma matriz de registro NumPy
df.to_sql(name, con[, schema, if_exists,…]) grava registros em df em um banco de dados SQL
df.to_stata(path[, convert_dates, write_index,…]) exporta df para o formato Stata dta
df.to_string([buf, columns, col_space, header, ...]) renderiza df em uma saída tabular compatível com o console
df.to_timestamp([freq, how, axis, copy]) converte para DatetimeIndex de timestamps, no início do período
df.to_xarray() converte para objeto xarray
df.to_xml([path_or_buffer, index, root_name,…]) renderiza df em um documento XML

Gerenciamento de valores ausentes

Método descrição
df.backfill([axis, inplace, limit, reduction]) o mesmo que df.fillna() com method = 'bfill'
df.bfill([axis, inplace, limit, downcast]) o mesmo que df.fillna() com method = 'bfill'
df.dropna([axis, how, treshold, subset, inplece]) remove os valores ausentes
df.ffill([axis, inplace, limit, reduction]) o mesmo que df.fillna() com method = 'ffill'
df.fillna([value, method, axis, local, ...]) preenche campos com NA/NaN usando o método especificado
df.interpolate([method, axis, limit, inplace, ...]) substitui valores NaN usando método de interpolação
df.isna() detecta valores ausentes
df.isnull() detecta valores ausentes
df.notna() valores existentes (não ausentes)
df.notnull() valores existentes (não ausentes)
df.pad([axis, inplace, limit, downcast]) o mesmo que df.fillna() com method = 'ffill'

Séries temporais e dados com hora/data

Método descrição
df.asfreq(freq[, método, como, normalizar, ...]) converte série temporal para a frequência especificada
df.at_time(hour[, asof, axis]) seleciona valores em um determinado horário do dia
df.between_time(start_time, end_time[,…]) seleciona valores entre horários especificados
df.first(offset) seleciona períodos iniciais em série temporal usando deslocamento (offset)
df.last(offset) selecione períodos finais da série temporal com deslocamento (offset)
df.resample(rule[, axis, closed, label, ...]) reamostrar os dados de série temporal
df.shift([periods, freq, axis, fill_value]) desloca índices por número desejado de períodos com uma frequância opcional
df.tshift([períodos, freq, axis]) (descontinuado) altera índice de tempo, usando a frequência do índice, se dispolevel
df.tz_convert(tz[, axis, level, copy]) converte o eixo com reconhecimento de tz em fuso horário de destino
df.tz_localize(tz[, axis, level, copy, ...]) localiza índice tz-naive de df para o fuso horário de destino

Ordenamento com dataframes.sort_values

Para ordenar um dataframe podemos usar o método sort, com a seguinte sintaxe:

dataframe.sort_values(by=['campo'], axis=0, ascending=True, inplace=False)
onde
by pode ser uma string ou lista com o nome ou nomes dos campos, na prioridade de ordenamento,
axis{0 ou ‘index’, 1 ou ‘columns’} default 0, indica o eixo a ordenar,
ascending=True/False se ordenamento é crescente/decrescente.

dataframe.reindex

Alterações da ordem dos índices de um dataframe podem ser obtidos com:
dataframe.reindex(listaDeCampos)
Os seguintes argumentos são usados com reindex

Argumento descrição
index Index ou sequência a ser usada como index,
method forma de interpolação: ‘ffill’ preenche com valor posterior, ‘bfill’ com valor anterior,
fill_value valor a usar quando dados não existentes são introduzidos por reindexing (ao invés de NaN),
limit quando preenchendo com valor anterior ou posterior, intervalo máximo a preencher (em número de elementos),
tolerance quando preenchendo com valor anterior ou posterior, intervalo máximo a preencher para valores inexatos (em distância numérica),
level combina Index simples no caso de MultiIndex; caso contrário seleciona subset,
copy se True, copia dados mesmo que novo índice seja equivalente ao índice antigo; se False, não copia dados quando índices são equivalentes.

Métodos e propriedades de Index

Método descrição
append concatena outro objeto Index objects, gerando novo Index
difference calcula a diferença de conjunto como um Index
intersection calcula intersecção de conjunto
union calcula união de conjunto
isin retorna array booleano indicando se cada valor está na coleção passada
delete apaga índice, recalculando Index
drop apaga índices passados, recalculando Index
insert insere índice, recalculando Index
is_monotonic retorna True se indices crescem de modo monotônico
is_unique returns True se não existem valores duplicados no Index
unique retorna índices sem repetições

Bibliografia

  • Pandas Pydata.org pandas docs, acessado em julho de 2021.

Pandas – Dataframes


Dataframes do pandas

Usamos a marcação:
» # linhas de comentários
» linhas de código (input)
↳ linhas de output

Um dataframe é uma forma de armazenar dados em forma tabular, como em uma planilha. O dataframe do pandas consiste em uma coleção de Series que são dispostas como suas colunas. A cada linha está associado um índice que serve para ordenar e selecionar dados. Como Series, cada coluna tem um tipo definido. No entanto, não é necessário que todas as colunas tenham o mesmo tipo e portanto dados de tipos diferentes podem ser armazenados.

O pandas usa muitos dos conceitos de programação do NumPy. A diferença principal entre eles está em que um array do NumPy usa dados homogêneos (todos do mesmo tipo) enquanto os dataframes do pandas podem conter dados de tipos diferentes.

Assim como o NumPy, muitas operações com dataframes levam em consideração o eixo ou axis. O default é axis = 0 (ou axis = 'index') o que indica operação sobre as linhas. axis = 1 (ou axis = 'column') indica operação realizada sobre as colunas.

O método mais comum de se criar um dataframe consiste em passar um dicionário e uma lista de índices para o construtor.

» import pandas as pd
» import numpy as np
» dados = {
            'nome': ['Pedro', 'Maria', 'Janaina', 'Wong', 'Roberto', 'Marco', 'Paula'],
            'cidade': ['São Paulo', 'São Paulo', 'Rio de Janeiro', 'Brasília',
                       'Salvador', 'Curitiba', 'Belo Horizonte'],
            'idade': [34, 23, 32, 43, 38, 31, 34],
            'nota': [83.0, 59.0, 86.0, 89.0, 98.0, 61.0, 44.0]
          }

» ids = [10, 11, 12, 13, 14, 15, 16]
» dfAlunos = pd.DataFrame(data=dados, index=ids)
↳ 

nome cidade idade nota
10 Pedro São Paulo 34 83.0
11 Maria São Paulo 23 59.0
12 Janaina Rio de Janeiro 32 86.0
13 Wong Brasília 43 89.0
14 Roberto Salvador 38 98.0
15 Marco Curitiba 31 61.0
16 Paula Belo Horizonte 34 44.0

No caso acima usamos um dict onde as chaves são os nomes dos campos ou colunas. À cada chave está associada uma lista cujos valores se tornam os valores das linhas, em cada coluna. A lista de índices foi fornecida separadamente. Se a lista ids não tivesse sido fornecida os índices do dataframe seriam inteiros, começando em 0.

Dataframes possuem a propriedade shape que contém as dimensões do objeto e os métodos head(n) e tail(n) que permitem, respectivamente, a visualização das n primeiras ou últimas linhas. Ao carregar um dataframe é sempre útil visualizar suas primeiras linhas e nomes de colunas. Também pode ser útil visualizar a matriz sob forma transposta, dada por dfAlunos.T.

» dfAlunos.shape
↳ (7, 4)
» # o que significa que temos 7 linhas, com 4 campos ou colunas.

» # para visualizar apenas as 2 primeiras linhas
» dfAlunos.head(2)
↳
nome cidade idade nota
10 Pedro São Paulo 34 83.0
11 Maria São Paulo 23 59.0
# para visualizar apenas as 2 últimas linhas
» dfAlunos.tail(2)
↳
nome cidade idade nota
15 Marco Curitiba 31 61.0
16 Paula Belo Horizonte 34 44.0
# A transposta:
» dfAlunos.T
↳
10 11 12 13 14 15 16
nome Pedro Maria Janaina Wong Roberto Marco Paula
cidade São Paulo São Paulo Rio de Janeiro Brasília Salvador Curitiba Belo Horizonte
idade 34 23 32 43 38 31 34
nota 83 59 86 89 98 61 44

Os nomes das colunas podem ser obtidos em uma lista, em um nome específico. Devemos nos lembrar que cada coluna do dataframe é uma Series. Portanto valem para elas os métodos e propriedades das Series.

» dfAlunos.columns
↳ Index(['nome', 'cidade', 'idade', 'nota'], dtype='object')

» # O nome da segunda coluna (lembrando que se conta a partir de 0)
» dfAlunos.columns[1]
↳ 'cidade'
» # Selecionando a coluna 'cidade'
» dfAlunos['cidade']
↳  10         São Paulo
   11         São Paulo
   12    Rio de Janeiro
   13          Brasília
   14          Salvador
   15          Curitiba
   16    Belo Horizonte
   Name: cidade, dtype: object

» # cada coluna é uma Series
» type(dfAlunos['cidade'])
↳ pandas.core.series.Series

» # os métodos das Series se aplicam
» dfAlunos['cidade'].value_counts()
↳  São Paulo         2
   Curitiba          1
   Rio de Janeiro    1
   Belo Horizonte    1
   Salvador          1
   Brasília          1
   Name: cidade, dtype: int64

» # valores únicos podem ser obtidos com unique()
» dfAlunos['cidade'].unique()
↳ array(['São Paulo', 'Rio de Janeiro', 'Brasília', 'Salvador', 'Curitiba',
         'Belo Horizonte'], dtype=object)

» # também podemos transformar esses valores em um set
» set(dfAlunos['cidade'])
↳ {'Belo Horizonte',
   'Brasília',
   'Curitiba',
   'Rio de Janeiro',
   'Salvador',
   'São Paulo'}


Observe que dfAlunos['cidade'] retorna uma Series, que é a coluna especificada do DF. Já o comando dfAlunos[['cidade']] retorna um dataframe com uma única coluna. É sempre importante saber com que tipo de objeto estamos lidando. Para isso podemos usar type() para conhecer esse tipo. Por exemplo, type(dfAlunos[['cidade']]) retorna pandas.core.frame.DataFrame . Observe que strings são listadas apenas como objects (sem discriminação de serem strings).

Também se pode usar a notação de ponto, dfAlunos.cidade, para obter a mesma coluna.

Como dissemos, o objeto DataFrame do pandas é formado por colunas que são Series, cada uma delas contendo elementos do mesmo tipo. As linhas podem, portanto, conter elementos de tipos diferentes. Para ver os tipos de cada coluna podemos examinar a propriedade dtype ou o método .info() que fornece uma visão geral sobre os dados, inclusive sobre a existência de valores nulos nos dados.

» dfAlunos.dtypes
↳ nome       object
  cidade     object
  idade       int64
  nota      float64
  dtype: object

» # Uma visão geral sobre os dados pode ser obtido com .info()
» dfAlunos.info()
↳ <class 'pandas.core.frame.DataFrame'>
  Int64Index: 7 entries, 10 to 16
  Data columns (total 4 columns):
   #   Column  Non-Null Count  Dtype
  ---  ------  --------------  -----
   0   nome    7 non-null      object
   1   cidade  7 non-null      object
   2   idade   7 non-null      int64
   3   nota    7 non-null      float64
  dtypes: float64(1), int64(1), object(2)
  memory usage: 600.0+ bytes

A descrição estatística dos campos numéricos é obtida com .describe() que fornece a contagem de itens, o valor médio, o desvio padrão, os quantis e máximos e mínimos. O método .corr() fornece o Coeficiente de Correlação de Pearson para todas as colunas numéricas da tabela. O resultado é um número no intervalo [-1, 1] que descreve a relação linear entre as variáveis.

# describe: resumo estatístico dos campos numéricos
» dfAlunos.describe()
↳
idade nota
count 7.000000 7.000000
mean 33.571429 74.285714
std 6.187545 19.661420
min 23.000000 44.000000
25% 31.500000 60.000000
50% 34.000000 83.000000
75% 36.000000 87.500000
max 43.000000 98.000000
» dfAlunos.corr()
↳ 
idade nota
idade 1.000000 0.564238
nota 0.564238 1.000000

Para acrescentar uma ou mais linhas (registros) ao dataframe podemos criar um novo dataframe com quantas linhas forem necessárias e concatená-lo com o antigo usando o método .concat().

» # criamos dataframe para a aluna Juliana e seus dados
» dfInserir = pd.DataFrame([('Juliana','Curitiba',28,80.0)],
                             columns=['nome','cidade','idade','nota'],
                             index=[100])
» pd.concat([dfAlunos, dfInserir])
↳
nome cidade idade nota
10 Pedro São Paulo 34 83.0
11 Maria São Paulo 23 59.0
12 Janaina Rio de Janeiro 32 86.0
13 Wong Brasília 43 89.0
14 Roberto Salvador 38 98.0
15 Marco Curitiba 31 61.0
16 Paula Belo Horizonte 34 44.0
100 Juliana Curitiba 28 80.0

Observe que o dataframe original não foi modificado. Caso se pretenda que modificação se torne permanente você deve atribuir o resultado retornado a uma novo (ou o mesmo) dataframe, como em dfAlunos = pd.concat([dfAlunos, dfInserir]).

Muitas vezes queremos que a novo dataframe criado ignore os índice das duas tabelas concatenadas. Nesse caso podemos ignorar os índices antigos e substituí-los por novos índices fornecidos, ou deixar que sejam preenchidos automaticamente.

» df = pd.concat([dfAlunos, dfInserir], ignore_index=True)
» df.index
↳ RangeIndex(start=0, stop=8, step=1)
» # os índices são inteiros de 0 até 8 (exclusive)

Uma nova coluna pode ser inserida, inclusive usando valores obtidos nas linhas. Na operação abaixo inserimos o campo calculado que é igual à multiplicação dos campos nota * idade, que não tem significado e é feito apenas como demonstração.

» dfAlunos['calculado']=dfAlunos['nota'] * dfAlunos['idade']
» dfAlunos
↳
nome cidade idade nota calculado
10 Pedro São Paulo 34 83.0 2822.0
11 Maria São Paulo 23 59.0 1357.0
12 Janaina Rio de Janeiro 32 86.0 2752.0
13 Wong Brasília 43 89.0 3827.0
14 Roberto Salvador 38 98.0 3724.0
15 Marco Curitiba 31 61.0 1891.0
16 Paula Belo Horizonte 34 44.0 1496.0

Como essa nova coluna não tem nenhum significado vamos apagá-la usando .drop().

# a operação seguinte retorna o dataframe sem a coluna 'calculado', mas não altera a original
» dfAlunos.drop(['calculado'], axis=1)
» # para alterar o dataframe usamos o parâmetro inplace=True
» dfAlunos.drop(['calculado'], axis=1, inplace=True)

Agora o dataframe tem a mesma estrutura de colunas original. Muitas operações do pandas retornam o resultado sobre o objeto sem alterá-lo. Algumas delas admitem o parâmetro inplace que, se True, faz a alteração do objeto in loco.

Para selecionar mais de uma coluna passamos uma lista com os nomes dos campos entre os colchetes.

» lista = ['nome','idade']
» # a linha abaixo é idêntica à dfAlunos[['nome','idade']]
» dfAlunos[lista]
↳
nome idade
10 Pedro 34
11 Maria 23
12 Janaina 32
13 Wong 43
14 Roberto 38
15 Marco 31
16 Paula 34


Podemos obter somas dos termos, tanto no sentido das linhas quanto das colunas, o que servirá como exemplo do uso do parâmetro axis. Relembrando:

axis = 0 (axis = ‘index’) opera sobre todas as linhas de cada coluna
axis = 1 (axis = ‘column’) opera sobre todas as colunas de cada linha

Para mostrar isso vamos construir um dataframe contendo apenas os dados numéricos, com os campos ‘idade’ e ‘nota’. Em seguida aplicamos sum(axis=0) para obter a soma das idades e notas, e sum(axis=1) para a soma
de cada linha.

» dfNumerico=dfAlunos[['idade', 'nota']]
» # a soma dos elementos em cada coluna
» dfNumerico.sum()   # o mesmo que dfNumerico.sum(axis=0)
↳ idade         235.0
  nota          520.0
  dtype: float64

» # a soma dos elementos em cada linha
» dfNumerico.sum(axis=1)
↳ 10    117.0
  11     82.0
  12    118.0
  13    132.0
  14    136.0
  15     92.0
  16     78.0
  dtype: float64

Importando um arquivo externo

É comum que os dados estejam inicialmente em forma de texto com os dados gravados em linhas e com valores separados por vírgula (um arquivo csv, comma separated values) ou outros separadores, tais como tabulação ou ponto e vírgula (;). Também ocorre que a exportação de outros aplicativos, como o Excel, possa ser feita nesse formato ou ser nele convertido.

Suponha que tenhamos no disco, na pasta de trabalho de sua sessão, um arquivo com o seguinte conteúdo:

    id, nome, cidade, idade, nota
    10, Pedro, São Paulo, 34, 83.0
    11, Maria, São Paulo, 23, 59.0
    12, Janaina, Rio de Janeiro, 32, 86.0
    13, Wong, Brasília, 43, 89.0
    14, Roberto, Salvador, 38, 98.0
    15, Marco, Curitiba, 31, 61.0
    16, Paula, Belo Horizonte, 34, 44.0

Não é importante que as colunas estejam bem alinhadas. Para importar esses dados para dentro de um dataframe usamos o método do pandas .read_csv(arq), onde arq é o nome completo do arquivo a ser lido (inclusive com seu caminho).

» dfNovoAlunos = pd.read_csv('./alunos.csv')
» dfNovoAlunos
↳
id nome cidade idade nota
0 10 Pedro São Paulo 34 83.0
1 11 Maria São Paulo 23 59.0
2 12 Janaina Rio de Janeiro 32 86.0
3 13 Wong Brasília 43 89.0
4 14 Roberto Salvador 38 98.0
5 15 Marco Curitiba 31 61.0
6 16 Paula Belo Horizonte 34 44.0

Vemos que o campo ‘id’ foi lido como um campo comum. Ele pode ser transformado em um pindice id efetivo por meio do método dataFrame.set_index:

» # torne o campo id o índice
» dfNovoAlunos.set_index('id', inplace=True)
» dfNovoAlunos.head(2)
↳ 
nome cidade idade nota
id
10 Pedro São Paulo 34 83.0
11 Maria São Paulo 23 59.0

Observe que o índice é apresentado na segunda linha do cabeçalho para indicar que não é um campo comum.

Alternativamente podemos ler o arquivo csv usando diretamente a primeira coluna como índice, informado pelo parâmetro index_col. Se o arquivo não contiver vírgulas separando os campos e sim outro sinal qualquer, como ; ou tabulações, passamos essa informação usando o parâmetro sep. Na última importação usamos url, a URL completa do arquivo, que pode estar em qualquer ponto disponivel da rede.

» # para usar a 1a coluna como índice
» dfNovoAlunos = pd.read_csv('./alunos.csv', index_col=0)
» # para ler arquivo em url, usando tab como separador
» dfOutroDF = pd.read_csv(url, sep='\t')

Vimos que, se nenhum argumento for passado, a primeira linha do arquivo é tomada como contendo os nomes (ou headers) das colunas. Para evitar isso passamos o parâmetro header = None. Nesse caso o nome das colunas é substituído por números.

Suponha que o arquivo nums.csv, com o conteúdo abaixo, esteja gravado no disco.

    11,12,13,14
    21,22,23,24
    31,32,33,34

Ele pode ser lido da seguinte forma:

» # informa que 1a linha não é header
» dfNone = pd.read_csv('./dados/nums.csv', header=None)

» # inserindo o nome ou labels para as colunas
» dfNames = pd.read_csv('./dados/nums.csv', names=('A', 'B', 'C', 'D'))

» # exibe os dois dataframes
» display('sem headers:', dfNone, 'com headers:', dfNames)
↳

‘sem headers:’

0 1 2 3
0 11 12 13 14
1 21 22 23 24
2 31 32 33 34

‘com headers:’

A B C D
0 11 12 13 14
1 21 22 23 24
2 31 32 33 34

Finalmente, se o cabeçalho contendo os títulos das colunas não está na primeira linha podemos passar o parâmetro header=n. A n-ésima linha será tomada como cabeçalho e todas as linhas anteriores serão ignoradas.

» dfPula2 = pd.read_csv('./dados/nums.csv', header=2)

Importação de csv com linhas de comprimentos desiguais

Pode ocorrer que um arquivo importado precise de alguma forma de tratamento para a montagem adequada de suas colunas, quando importado em um dataframe. Suponha, por exemplo, que temos em disco um arquivo de texto como o seguinte conteúdo:

palavras
a;1;2;3;6;7;9;10;17;121;131;138;252;463
aba;146
abafa;125;127;129
abaixado;1;125;127;130
abastecer;121;146;150;250;354;358
abatido;1;124;125;127;129;130;140;143;358;360
aberto;13;22;125;126;131;132;138;11;250;252
abismei;125;126
abra;14;22;125;126;131;132;138;11;250;252
abriga;125;126;131;137
acabrunha;143;150
acalmar;1;124;125;126;140;142;143;253
acaso;125;126;131;135;253

Essa é uma seleção muito pequena, para fim de demonstração da técnica, do extenso dicionário LIWC que associa palavras da língua portuguêsa à um conjunto de códigos associados à características linguísticas e cognitivas. Queremos classificar palavras de acordo com esse dataset.

Não podemos importar diretamente esses dados usando pd.read_csv(arquivo, sep=';') pois o comprimento irregular dos códigos geraria erro na importação. Então nos resta importar todas as linhas em uma coluna e tratá-las depois, de modo adequado.

# o arquivo csv está em    
arquivo='./dados/liwc.csv'
# importamos com separador que não existe no texto para obter apenas 1 coluna
dfPalavras = pd.read_csv(arquivo, sep='#')

dfPalavras
    palavras
0   a;1;2;3;6;7;9;10;17;121;131;138;252;463
1   aba;146
2   abafa;125;127;129
3   abaixado;1;125;127;130
4   abastecer;121;146;150;250;354;358
5   abatido;1;124;125;127;129;130;140;143;358;360
6   aberto;13;22;125;126;131;132;138;11;250;252
7   abismei;125;126
8   abra;14;22;125;126;131;132;138;11;250;252
9   abriga;125;126;131;137
10  acabrunha;143;150
11  acalmar;1;124;125;126;140;142;143;253
12  acaso;125;126;131;135;253

# cada linha pode se quebrada em ; retornando listas
dfPalavras.palavras.str.split(';').head(3)

0    [a, 1;2;3;6;7;9;10;17;121;131;138;252;463]
1                                    [aba, 146]
2                          [abafa, 125;127;129]

O próprio método split pode ser usado com os parâmetros str.split(sep, n, expand), que quebra a string em sep, usando apenas as primeiras n partes, e montando um dataframe se expand=True. Montamos um dataframe vazio df, separamos as linhas em 2 partes e as aplicamos em df, com as partes palavra e codigo separadas.

df = pd.DataFrame({})
df[['palavra','codigo']] = dfPalavras.palavras.str.split(';', n=1, expand=True)
# o seguinte dataframe é gerado
df
    palavra      codigo
0   a            1;2;3;6;7;9;10;17;121;131;138;252;463
1   aba          146
2   abafa        125;127;129
3   abaixado     1;125;127;130
4   abastecer    121;146;150;250;354;358
5   abatido      1;124;125;127;129;130;140;143;358;360
6   aberto       13;22;125;126;131;132;138;11;250;252
7   abismei      125;126
8   abra         14;22;125;126;131;132;138;11;250;252
9   abriga       125;126;131;137
10  acabrunha    143;150
11  acalmar      1;124;125;126;140;142;143;253
12  acaso        125;126;131;135;253

# podemos encontrar as palavras associadas ao codigo = 127
df[df.codigo.str.contains('127')]['palavra']
2       abafa
3    abaixado
5     abatido

# ou os codigos associados à palavra 'abafa'
df[df.palavra == 'abafa']['codigo'].values[0]
'125;127;129'

Na última linha df[df.palavra == 'abafa']['codigo'] é uma Series. Para extrair seu valor usamos .values[0]. Nesse caso o número de códigos é muito varíavel e não seria prático construir uma coluna para cada código.

Se for necessário ter colunas correspondendo à cada código teríamos que preencher os campos vazio com algum valor, como 0 ou NaN.

Gravando o dataframe em arquivos pickle

Ao términdo de cada fase de depuração, manipulação e ajustes do dataframe, podemos gravá-lo em disco em um arquivo pickle. O arquivo gravado pode ser lido e o dataframe recuperado em nova sessão de código, sem que tenhamos que repetir as etapas já realizadas.

» pd.to_pickle(dfNovoAlunos, './dados/Alunos.pkl')
» del dfNovoAlunos
» dfLido = pd.read_pickle('./dados/Alunos.pkl')

dfLido será um dataframe idêntico ao dfNovoAlunos gravado em etapa anterior. A pasta de destino deve existir ou uma exceção será lançada.

to_pickle Grava um objeto do pandas em arquivo pickled
read_pickle Ler arquivo pickle recuperando objeto
DataFrame.to_hdf Grava um objeto do pandas em arquivo HDF5
read_hdf Ler arquivo hdf recuperando objeto
DataFrame.to_sql Grava dataframe em um banco de dados sql
read_sql Ler arquivo sql recuperando objeto
DataFrame.to_parquet Grava dataframe em formato parquet binário.
read_parquet Ler arquivo parquet recuperando objeto
🔺Início do artigo

Bibliografia

Consulte bibliografia completa em Pandas, Introdução neste site.

Nesse site:

Introdução ao Pandas – Series

🔻Final do artigo

O que é pandas?

Pandas é uma biblioteca do Python, de código aberto e com licença BSD, desenvolvida e mantida pelo PуDаtа Dеvеlорmеnt Tеаm. Ela fornece ferramentas de manipulação e análise estatística de dados, capacidade de exibição gráfica E extração de dados análogos (mas não idênticos) aos de consultas sql.

A biblioteca foi construída com Cython e, por isso, é bastante rápida. Ela se destinava inicialmente ao uso no setor financeiro para análise de séries temporiais, tendo se tornado uma ferramenta de uso comum na manipulação de dados, particularmente em data science e machine learning. Ela tem sido usada para substituir as planilhas do Excel, para processar dados sob forma tabular, importando com facilidade dados de arquivos csv ou json.

Os experimentos abaixo foram realizados no Jupyter Notebook. Você encontra nesse site um artigo sobre instalação e uso do Jupyter Notebook. As linhas de código e suas respostas, quando existirem, serão representadas da seguinte forma:

» # Comentários (não são lidos ou executados pelo interpretador)
» Linha de input (entrada de comandos)
↳ Linha de output (resposta do código)

NumPy e matplotlib

NumPy é a abreviação de Numerical Python, a biblioteca base da computação numérica em Python. Ela fornece as estruturas de dados e algoritmos necessários para a maioria das aplicações científicas e de engenharia utilizando cálculo numérico. Entre outros objetos NumPy NumPy fornece

  • o objeto multidimensional ndarray onde se pode aplicar operações vetorializadas rápidas e eficientes,
  • um conjunto de funções para cálculos elementares com vetores e matrizes,
  • ferramentas de leitura e gravação de dados,
  • operações da álgebra linear, transformada de Fourier e geração de números aleatórios,
  • interação com C e C++.


Para dados numéricos as matrizes do NumPy são armazenadas e manipuladas de modo mais eficiente do que as demais estruturas do Python. Além disso códigos escritos em linguagens de baixo nível, como C ou Fortran, podem operar diretamente nos dados armazenados com o NumPy. Por isso muitas ferramentas de computação numérica do Python usam as matrizes NumPy como um estrutura de dados primária.

matplotlib é a biblioteca Python mais popular usada para a produção de gráficos e visualização de dados. Ela pode ser usada na geração de gráficos estáticos ou animados e visualização interativa.

Pandas

O pandas se utiliza basicamente de 3 objetos de armazenamento de dados com as seguintes estruturas:
Estrutura de dados dos objetos do pandas:

Nome dimensões tabela
Series 1D coluna (vetor)
DataFrame 2D tabela (matriz)
Panel 3D várias tabelas (matriz multidimensional)

As series e os dataframes são utilizados com maior frequência.

Como a manipulação de dados usando séries e dataframes frequentemente envolvem operações encontradas no módulo numpy é frequente sua importação junto com pandas.

» import pandas as pd
» import numpy as np

Series

Uma series é um objeto unidimensional, tipo um vetor, que contém uma sequência de objetos do mesmo tipo. A essa sequência está associado um outro vetor de labels chamado de index (índice). O método básico de criação de séries é da seguinte forma:
serie = pd.Series(data, index=index)
onde data pode ser um dict (um dicionário do Python), uma lista ou ndarray do numPy, ou um escalar. index é uma lista de índices que, se omitida, é preenchida com inteiros iniciando em 0.

» serie1 = pd.Series([-1, 9, 0, 2, 5])
↳  0   -1
   1    9
   2    0
   3    2
   4    5

À esquerda estão listados os índices que, por default, são uma lista de inteiros começando por 0. Os valores podem ser listados com .values e os índices com .index.

» serie1.values
↳ array([-1,  9,  0,  2,  5])

» serie1.index
↳ RangeIndex(start=0, stop=5, step=1)

Os índices podem ser inseridos manualmente e não precisam ser inteiros. O valor correspondente ao índice i pode ser acessado com serie[i], como mostrado abaixo, onde os índices são strings.

» serie2 = pd.Series([4, 7, -5, 3], index=['a', 'b', 'c', 'd'])
» serie2
↳ a    4
  b    7
  c   -5
  d    3
  dtype: int64

» serie2['c']
↳ -5

Uma série pode ser filtrada passando como índice outra série de elementos boolenos, (True, False). Além disso operações vetorializadas podem ser realizadas sobre todos os elementos da série.

# O teste seguinte gera uma série de booleanos
» serie2 > 3
↳ a     True
  b     True
  c    False
  d    False
  dtype: bool

# Essa serie de True e False filtra a serie original
» serie2[serie2 > 3]
↳ a    4
  b    7
  dtype: int64

# Operações podem, ser realizadas sobre todos os elementos
» serie2 * 3
↳ a    12
  b    21
  c   -15
  d     9
  dtype: int64

# o módulo Numpy possui a função exponencial
» np.exp(serie2)
↳ a      54.598150
  b    1096.633158
  c       0.006738
  d      20.085537
  dtype: float64

Séries se comportam, em muitos sentidos, como um dicionário. Uma série pode ser criada passando-se um dicionário como argumento para pd.Series().

» populacao = {
           'Sudeste': 89012240, 
           'Nordeste': 57374243,
           'Sul': 30192315,
           'Norte': 18672591,
           'Centro-Oeste':16504303
         }

» serie3 = pd.Series(populacao)

» serie3
↳ Sudeste         89012240
  Nordeste        57374243
  Sul             30192315
  Norte           18672591
  Centro-Oeste    16504303
  dtype: int64 


A ordem dos itens na série pode ser alterada através do fornecimento de uma lista com o ordenamento desejado para o argumento index. A elementos não presentes no dicionário serão atribuídos o valor NaN, Not a Number (não número). O método pd.isnull(serie) permite a avalição de quais elementos estão nulos ou NaN.

# fornecendo uma lista para o argumento index:
» ordem_alfabetica = ['Brasil', 'Centro-Oeste', 'Nordeste', 'Norte', 'Sudeste', 'Sul']
» serie4 = pd.Series(populacao, index=ordem_alfabetica)
» serie4
↳ Brasil               NaN
  Centro-Oeste    16504303
  Nordeste        57374243
  Norte           18672591
  Sudeste         89012240
  Sul             30192315
  dtype: int64

# para verificar quais valores são nulos (NaN)
» pd.isnull(serie4)
↳ Brasil           True
  Centro-Oeste    False
  Nordeste        False
  Norte           False
  Sudeste         False
  Sul             False
  dtype: bool

# os seguintes registros são NaN
» serie4[pd.isnull(serie4)]
↳ Brasil   NaN
  dtype: float64

» serie4[pd.notnull(serie4)]
↳ Centro-Oeste    16504303.0
  Nordeste        57374243.0
  Norte           18672591.0
  Sudeste         89012240.0
  Sul             30192315.0
  dtype: float64

» 'Brasil' in serie4
↳ True
» 'EUA' in serie4
↳ False
# uma excessão KeyError é lançada se o indice não existe
» serie4['EUA']
↳ KeyError

Como não existe no dicionário um valor para o índice Brasil a série atribuiu o valor NaN (Not a Number) para essa chave, a forma de NumPy e pandas indicar a ausência de um valor. O método retorna True ou False para cada item da série e pd.notnull() o seu inverso booleano. Alternativamente se pode usar o método das séries serie4.isnull().

Ainda sobre a semelhança entre séries e dicionários, podemos testar a existência de uma chave usando o operador in, como em 'Brasil' in serie4. A tentativa de recuperar um valor com índice não existente gera uma exceção (um erro do Python).

Observe que uma series tem propriedades de numpy ndarray, mas é um objeto de tipo diferente. Se um ndarray é necessário use series.to_numpy().

» type(serie4)
↳ pandas.core.series.Series

» type(serie4.to_numpy())
↳ numpy.ndarray

Series podem ser fatiadas com a notação serie[i:f] onde serão retornadas a i-ésima linha até a f-ésima, exclusive. Se i for omitido a lista se inicia em 0, se f for omitido ela termina no final da series.

» serie4
↳ Brasil                 NaN
  Centro-Oeste    16504303.0
  Nordeste        57374243.0
  Norte           18672591.0
  Sudeste         89012240.0
  Sul             30192315.0
  dtype: float64

» serie4[2:5]
↳ Nordeste        57374243.0
  Norte           18672591.0
  Sudeste         89012240.0
  dtype: float64

» serie4[:2]
↳ Brasil                 NaN
  Centro-Oeste    16504303.0
  dtype: float64

» serie4[4:]
↳ Sudeste    89012240.0
  Sul        30192315.0
  dtype: float64

Series podem ser exibidas com o método display(serie_0, serie_1, ..., serie_n). O resultado de operações envolvendo mais de uma serie, como a soma, alinha os valores por chaves (como uma UNION). Valores não presentes em um dos operandos terá NaN como resultado.

» serie5 = pd.Series([2, -1, -2, 1], index=['a', 'b', 'c', 'd'])
» serie6 = pd.Series([3, 4, 7, -1], index=['e', 'c', 'b', 'f'])
» display(serie5, serie6)
↳ a    2
  b   -1
  c   -2
  d    1
  dtype: int64

↳ e    3
  c    4
  b    7
  f   -1
  dtype: int64

» serie5 + serie6
↳ a    NaN
  b    6.0
  c    2.0
  d    NaN
  e    NaN
  f    NaN
  dtype: float64

Series possuem a propriedade name que pode ser atribuída na construção ou posteriormente com o método .rename(). No exemplo usamos o método np.random.randn(n) de numpy para fornecer um numpy.ndarray com n números aleatórios. Damos inicialmente a essa série o nome ‘randomica’, depois a renomeamos para ‘aleatoria’.

» serie7 = pd.Series(np.random.randn(5), name='randomica')
» serie7
↳ 0   -1.703662
  1    1.406167
  2    0.966557
  3   -0.557846
  4   -0.264914
  Name: randomica, dtype: float64

» serie7.name
↳ 'randomica'

» serie7= serie7.rename('aleatoria')
» serie7.name
↳ 'aleatoria'

O nome de uma série se torna seu índice ou nome de coluna caso ela seja usada para formar um DataFrame.

Atributos e Métodos das Series

Os atributos e métodos mais comuns e úteis das Series estão listados abaixo. Para uma lista completa consulte pandas.Series: API Reference.

Atributos

Atributo Descrição
at[n] Acesso ao valor na posição n
attrs Retorna ditionario de atributos globais da series
axes Retorna lista de labels do eixo das linhas
dtype Retorna o tipo (dtype) dos objetos armazenados
flags Lista as propriedades do objeto
hasnans Informa se existem NaNs
iat[n] Acesso ao valor na posição n inteiro
iloc[n] Acesso ao valor na posição n inteiro
index Retorna lista de índices
index[n] Retorna índice na n-ésima posição
is_monotonic Booleano: True se valores crescem de forma monotônica
is_monotonic_decreasing Booleano: True se valores decrescem de forma monotônica
is_unique Booleano: True se valores na series são únicos
loc Acessa linhas e colunas por labels em array booleano
name O nome da Series
nbytes Número de bytes nos dados armazenados
shape Retorna uma tuple com forma (dimensões) dos dados
size Número de elementos nos dados
values Retorna series como ndarray

Alguns casos de acessos à essas propriedades. Os outputs são exibidos como comentários:

» import pandas as pd
» import numpy as np 
» serie = pd.Series([1,-1, 2, 2, 6, 63])
» serie.size              # 6, o mesmo que len(serie)
» serie.at[5]             # 63
» serie.iloc[0]           # 1
» serie.index             # RangeIndex(start=0, stop=6, step=1)
» serie.is_monotonic      # False
» serie.is_unique         # False
» serie.shape             # (6,) 

Métodos

Nas tabelas abaixo a expressão “elemento a elemento” é abreviada para “e/e”. Estas operações são repetidas a cada elemento da, ou das, séries envolvidas. Por exemplo, o método serie.add(s2) é feita “elemento a elemento” (e/e):

» import pd	
» serie_a = pd.Series([-1,9,0,2, 5, -8])
» serie_b = pd.Series([1,-9,0, -2,-5, 8])
» serie_a.add(serie_b)
» # é o mesmo que
» serie_a + serie_b
» # que resulta em serie nula 
» # pd.Series([0,0,0,0,0,0])

Manipulação e gerenciamento das Series

Método (sobre série s, outra s2) Descrição
s.align(s2) Alinha 2 objetos em seus eixos usando método especificado
s.append(to_append[, ignore_index, …]) Concatena 2 ou mais Series
s.asof(where[, subset]) Último elemento antes da ocorrência de NaNs após ‘where’
s.backfill([axis, inplace, limit, downcast]) Aliás para DataFrame.fillna() usando method=’bfill’
s.bfill([axis, inplace, limit, downcast]) Alias para DataFrame.fillna() usando method=’bfill’
s.clip([min, max, axis, inplace]) Inclui apenas valores no intervalo
s.combine(s2, func[, fill_value]) Combina a s com s2 ou escalar, usando func
s.copy([deep]) Cópia do objeto s, índices e valores
s.drop_duplicates([keep, inplace]) Remove elementos duplicados de s
s.dropna() Remove valores faltantes de s
s.duplicated([keep]) Exibe valores duplicados na s
s.explode([ignore_index]) Transforma cada elemento de um objeto tipo lista em uma linha
s.fillna([value, method, axis, inplace, …]) Substitui valores NA/NaN usando método especificado
s.get(key) Retorna item correspondente à key
s.groupby([by, axis, level, as_index, sort, …]) Agrupa a s
s.head([n]) Retorna os n primeiros valores
s.interpolate([method, axis, limit, inplace, …]) Preenche valores NaN usando metodo de interpolação
s.item() Primeiro elemento dos dados como escalar do Python
s.items() Iteração (lazy) sobre a tupla (index, value)
s.iteritems() Iteração (lazy) sobre a tupla (index, value)
s.mask(cond[, s2, inplace, axis, level, …]) Substitui valores sob condição dada
s.max([axis, skipna, level, numeric_only]) Valor máximo
s.memory_usage([index, deep]) Memória usada pela s
s.min([axis, skipna, level, numeric_only]) Menor dos valores da s
s.nlargest([n, keep]) Retorna os n maiores elementos
s.nsmallest([n, keep]) Retorna os n menores elementos
s.nunique([dropna]) Retorna quantos elementos únicos existem na s
s.pad([axis, inplace, limit, downcast]) O mesmo que DataFrame.fillna() usando method=’ffill’
s.plot() O mesmo que pandas.plotting._core.PlotAccessor
s.pop(i) Remove s[i] de s e retorna s[i]
s.repeat(repeats[, axis]) Repete elementos da s
s.replace([to_replace, value, inplace, limit, …]) Substitui valores em to_replace por value
s.sort_values([axis, ascending, inplace, …]) Reorganiza s usando seus valores
s.str Usa funções de string sobre s (se string). Ex. s.str.split(“-“)
s.tail([n]) Últimos n elementos
s.unique() Retorna os valores da s, sem repetições
s.update(s2) Modifica s usando valores de s2, usando índices iguais
s.view([dtype]) Cria uma nova “view” da s
s.where(cond[, serie, inplace, axis, level, …]) Substitui valores se a condição cond = True

Operações matemáticas básicas:

s.ewm([com, span, halflife, alpha, …])Calcula exponencial com peso

s.abs() Retorna s com valor absoluto, e/e
s.add(s2) Soma s com s2, e/e
s.agg([func, axis]) Agrega usando func sobre o eixo especificado
s.apply(func[, convert_dtype, args]) Aplica func sobre os valores de s, e/e
s.div(s2) Divisão (float) de s por s2, e/e
s.divmod(s2) Divisão inteira e módulo de s por s2, e/e
s.dot(s2) Produto interno entre a s e s2
s.floordiv(s2) Divisão inteira da s por s2, e/e
s.mod(s2[, level, fill_value, axis]) Módulo de s por s2, e/e
s.rfloordiv(s2[, level, fill_value, axis]) Divisão inteira de s por s2, e/e
s.rmod(s2[, level, fill_value, axis]) Modulo da divisão da s por s2, e/e
s.rmul(s2[, level, fill_value, axis]) Multiplicação de s por s2, e/e
s.round([n]) Arredonda valores da s para n casas decimais.
s.rpow(s2[, level, fill_value, axis]) Exponential de s por s2, e/e
s.rsub(s2[, level, fill_value, axis]) Subtração da s por s2, e/e
s.rtruediv(serie[, level, fill_value, axis]) Divisão (float) de s por s2, e/e
s.sub(s2) Subtração de s por s2, e/e
s.subtract(serie) Idem
s.sum([axis, skipna, level, numeric_only, …]) Soma dos valores da s
s.transform(func[, axis]) Executa func sobre elementos de s
s.truediv(s2) Divisão (float) de s por s2, e/e
s.truncate([before, after, axis, copy]) Trunca a s antes e após índices dados
s.mul(s2[, level, fill_value, axis]) Multiplicação de s por s2, e/e
s.multiply(s2[, level, fill_value, axis]) Idem
s.pow(s2) Exponential de s por s2, e/e
s.prod([axis, skipna, level, numeric_only, …]) Produto dos elementos da s
s.product([axis, skipna, level, numeric_only, …]) Idem
s.rdiv(s2[, level, fill_value, axis]) Divisão (float) de s por s2, e/e
s.rdivmod(s2) Divisão inteira e módulo de s por s2, e/e

Operações estatísticas:

Método (sobre série s, outra s2) Descrição
s.corr(s2) Correlação de s com s2, excluindo NaNs
s.count([level]) Número de observações na s, excluindo NaN/nulls
s.cov(s2[, min_periods, ddof]) Covariância da s, excluindo NaN/nulls
s.cummax([axis, skipna]) Máximo cumulativo
s.cummin([axis, skipna]) Mínimo cumulativo
s.cumprod([axis, skipna]) Produto cumulativo
s.cumsum([axis, skipna]) Soma cumulativa
s.describe([percentiles, include, exclude, …]) Gera descrição estatística
s.kurt([axis, skipna, level, numeric_only]) Kurtosis imparcial
s.kurtosis([axis, skipna, level, numeric_only]) Idem
s.hist() Plota histograma da s usando matplotlib.
s.mad([axis, skipna, level]) Desvio médio absoluto dos valores de s
s.mean([axis, skipna, level, numeric_only]) Média dos valores
s.median([axis, skipna, level, numeric_only]) Mediana dos valores
s.mode([dropna]) Moda da s
s.quantile([q, interpolation]) Valor no quantil dado
s.ravel([order]) Retorna dados como um ndarray
s.sample([n, frac, replace, weights, …]) Amostra randomizada de items da s
s.sem([axis, skipna, level, ddof, numeric_only]) Erro padrão imparcial da média
s.skew([axis, skipna, level, numeric_only]) Inclinação imparcial
s.std([axis, skipna, level, ddof, numeric_only]) Desvio padrão da amostra
s.value_counts([normalize, sort, ascending, …]) Retorna s com a contagem de valores únicos
s.var([axis, skipna, level, ddof, numeric_only]) Variância imparcial dos valores da s

Operações com índices:

s.add_prefix('prefixo') Adiciona prefixo aos labels com string ‘prefixo’
s.add_suffix('sufixo') Adiciona sufixo aos labels com string ‘sufixo’
s.argmax([axis, skipna]) Posição (índice inteiro) do valor mais alto de s
s.argmin([axis, skipna]) Posição (índice inteiro) do menor valor de s
s.argsort([axis, kind, order]) Índices inteiros que ordenam valores da s
s.drop([labels]) Retorna s com labels removidos
s.first_valid_index() Índice do primeiro valor não NA/null
s.idxmax([axis, skipna]) Label do item de maior valor
s.idxmin([axis, skipna]) Label do item de menor valor
s.keys() Alias de index
s.last_valid_index() Índice do último valor não NA/null
s.reindex([index]) Ajusta a s ao novo índice
s.reindex_like(s2[, method, copy, limit, …]) Série com índices em acordo com s2
s.rename([index, axis, copy, inplace, level, …]) Altera o nome (labels) dos índices
s.reorder_levels(order) Reajusta níveis de índices usando order
s.reset_index([level, drop, name, inplace]) Reinicializa índices
s.searchsorted(value[, side, sorter]) Índices onde elementos devem ser inseridos para manter ordem
s.sort_index([axis, level, ascending, …]) Reorganiza s usando os índices

Testes, com retorno booleanos e comparações:

Método (sobre série s, outra s2) Descrição
s.all([axis, bool_only, skipna, level]) Booleano: se todos os elementos são True
s.any([axis, bool_only, skipna, level]) Booleano: se algum elemento é True
s.equals(s2) Booleano: True se s contém os mesmos elementos que s2
s.between(min, max) Booleano: satisfazendo min <= s <= max, e/e
s.compare(s2[, align_axis, keep_shape, …]) Compara s com s2 exibindo diferenças
s.eq(s2) Boleano: igualdade entre s e s2, e/e
s.ge(s2) Booleana: maior ou igual entre s e s2, e/e
s.gt(s2[, level, fill_value, axis]) Booleana: se s é maior que s2, e/e
s.isin(valores) Booleano: se elementos da s estão contidos em valores
s.isna() Booleano: se existem valores ausentes
s.isnull() Booleano: se existem valores nulos
s.le(s2) Booleana: se s é menor ou igual a s2, e/e
s.lt(s2[, level, fill_value, axis]) Booleana: se s é menor que s2, e/e
s.ne(s2[, level, fill_value, axis]) Booleana: se s é diferente de s2, e/e
s.notna() Booleana: se existem valores não faltantes ou nulos
s.notnull() Idem

Transformações para outros formatos e tipos:

s.astype(dtype[, copy, errors]) Transforma (cast) para dtype
s.to_clipboard([excel, sep]) Copia o object para o clipboard do sistema
s.to_csv([path_or_buf, sep, na_rep, …]) Grava a s como arquivo csv
s.to_dict() Converte s para dict {label ⟶ value}
s.to_excel(excel_writer[, sheet_name, na_rep, …]) Grava s como uma planilha Excel
s.to_frame([name]) Converte s em DataFrame
s.to_hdf(path_or_buf, key[, mode, complevel, …]) Grava s em arquivo HDF5 usando HDFStore
s.to_json([path_or_buf, orient, date_format, …]) Converte s em string JSON
s.to_latex([buf, columns, col_space, header, …]) Renderiza objeto para LaTeX
s.to_markdown([buf, mode, index, storage_options]) Escreve a s em formato Markdown (leia)
s.to_numpy([dtype, copy, na_value]) Converte s em NumPy ndarray
s.to_pickle(path[, compression, protocol, …]) Grava objeto serializado em arquivo Pickle
s.to_sql(name, con[, schema, if_exists, …]) Grava elementos em forma de um database SQL
s.to_string([buf, na_rep, float_format, …]) Constroi uma representação string da s
s.tolist() Retorna uma lista dos valores
s.to_list() idem

Operações com séries temporais:

s.asfreq(freq) Converte TimeSeries para frequência especificada.
s.at_time(time[, asof, axis]) Seleciona valores em determinada hora (ex., 9:30AM)
s.between_time(inicio, fim) Seleciona valores com tempo entre inicio e fim
s.first(offset) Seleciona período inicial de uma série temporal usando offset.
s.last(offset) Seleciona período final de uma série temporal usando offset

Alguns exemplos de uso dos métodos de pandas.Series:

» import pandas as pd
» import numpy as np
» serie = pd.Series([1,-1, 2, 2, 6, 63])
» serie.abs()
» # retorna  pd.Series([1, 1, 2, 2, 6, 63])

» # Muitos métodos não alteram a series inplace.
» s2 = serie.rename('NovoNome')  # não altera nome de serie
» s2.name
↳ 'NovoNome'
» # para alterar o nome usamos
» serie.rename('NovoNome', inplace=True)  # altera nome de serie inplace
» serie.name
↳ 'NovoNome'

» # um resumo estatístico pode ser visto com describe
» serie.describe()
↳ count    15.000000
  mean      4.866667
  std       3.044120
  min       0.000000
  25%       3.500000
  50%       5.000000
  75%       7.000000
  max       9.000000
  dtype: float64

» # gerando outra series
» data = np.random.randint(0, 10,size=15)
» serie2 = pd.Series(data)
» data
↳ array([2, 2, 6, 3, 1, 4, 3, 4, 3, 0, 8, 3, 8, 2, 7])

» serie.div(serie2)
↳ 0     2.500000
  1     0.000000
  ...
  9          inf
  13    0.000000
  14    1.000000
  dtype: float64


Observe que a divisão por 0 não gera erro mas é representada por inf.

Alguma habilidade gráfica pode ser encontrada entre os métodos das series. Um exemplo é o serie.hist() que traça o histograma da series usando matplotlib. Veremos com mais detalhes as funcionalidades dessa biblioteca.

» data = np.random.randint(0, 10,size=15)
» data
array([5, 0, 7, 7, 0, 9, 6, 5, 6, 9, 5, 5, 2, 0, 7])
» serie = pd.Series(data)
» serie.hist()

Objetos do pandas possuem métodos poderosos e eficientes. O método serie.str() permite operações de strings sobre os elementos da serie, se esses forem strings.

» str = ['-mercado','-tensão','-plasia']
» serie = pd.Series(str)
» serie
↳ 0    -mercado
  1     -tensão
  2     -plasia

» serie = serie.str.replace('-','hiper-')
» serie
↳ 0    hiper-mercado
  1     hiper-tensão
  2     hiper-plasia

» serie.str.split('-')
↳ 0    [hiper, mercado]
  1     [hiper, tensão]
  2     [hiper, plasia]

» # elementos não strings resultam em NaN
» serie = pd.Series([123,'-tensão','-plasia'])
» serie.str.replace('-','hiper-')
↳ 0             NaN
  1    hiper-tensão
  2    hiper-plasia

Series podem ser usadas na construção de dataframes, que serão vistos a seguir. Em particular o método to_frame() transforma uma series em um dataframe com uma coluna, onde cada valor ocupa uma linha.

» # uma series pode ser transformada em um dataframe
» df = serie.to_frame()
» # um dataframe é gerado e armazenado em df

Como a maioria dos métodos de series são análogos àqueles de dataframes faremos uma exploração mais extensa desses na sessão referente aos dataframes.

🔺Início do artigo

Bibliografia

  • Blair,Steve: Python Data Science, The Ultimate Handbook for Beginners on How to Explore NumPy for Numerical Data, Pandas for Data Analysis, IPython, Scikit-Learn and Tensorflow for Machine Learning and Business, edição do autor disponível na Amazon, 2019.
  • Harrison, Matt: Learning Pandas, Python Tools for Data Munging, Data Analysis, and Visualization,
    edição do autor, 2016.
  • McKinney, Wes: pandas: powerful Python data analysistoolkit, Release 1.2.1
    publicação do Pandas Development Team, revisado em 2021.
  • McKinney, Wes: Python for Data Analysis, Data Wrangling with Pandas, NumPy,and IPython
    O’Reilly Media, 2018.
  • Pandas: página oficial, acessada em janeiro de 2021.
  • Pandas User Guide, acessada em fevereiro de 2021.
  • Miller, Curtis: On Data Analysis with NumPy and pandas, Packt Publishing, Birmingham, 2018.

pandas e SQL comparados


SQL (Structured Query Language) é uma linguagem de programação de uso específico utilizada para consultar, extrair e gerenciar bancos de dados relacionais. Pandas é uma biblioteca do Python especializada para o tratamento e análise de dados estruturados, incluindo uma gama de formas de extrair dados.

Esse artigo faz uma comparação entre as consultas feitas a dataframes do pandas e as consultas SQL, explorando similaridades e diferenças entre os dois sistemas. Ele serve para descrever as funcionalidades de busca e edição do pandas e pode ser particularmente útil para aqueles que conhecem SQL e pretendem usar o pandas (ou vice-versa).

Para realizar os experimentos abaixo usamos o Jupyter Notebook, um aplicativo que roda dentro de um navegador, que pode ser facilmente instalado e permite a reprodução se todo o código aqui descrito. Você pode ler mais sobre Jupyter Notebook e Linguagem de Consultas SQL nesse site.

Esse texto é baseado em parte do manual do pandas e expandido. Ele usa um conjunto de dados baixados do github renomeado aqui para dfGorjeta. Nomes e valores dos campos foram traduzidos para o português.

# importar as bibliotecas necessárias
import pandas as pd
import numpy as np

url = "https://raw.github.com/pandas-dev/pandas/master/pandas/tests/io/data/csv/tips.csv"

dfGorjeta = pd.read_csv(url)
dfGorjeta.head()
total_bill tip sex smoker day time size
0 16.99 1.01 Female No Sun Dinner 2
1 10.34 1.66 Male No Sun Dinner 3
2 21.01 3.50 Male No Sun Dinner 3
3 23.68 3.31 Male No Sun Dinner 2
4 24.59 3.61 Female No Sun Dinner 4

Para efeito de testar os comandos do dataframe vamos alterar os nomes dos campos e traduzir os conteúdos dos dados. Para descobrir quais são os valores dos campos, sem repetições, transformamos as séries em sets, uma vez que valores de um set (conjunto) não se repetem.

print(set(dfGorjeta["sexo"]))
print(set(dfGorjeta["fumante"]))
print(set(dfGorjeta["dia"]))
print(set(dfGorjeta["hora"]))
{‘Male’, ‘Female’}
{‘No’, ‘Yes’}
{‘Sat’, ‘Sun’, ‘Fri’, ‘Thur’}
{‘Lunch’, ‘Dinner’}

No código seguinte alteramos os nomes de campos e traduzimos o conteúdo. A sintaxe da operação de edição do dataframe será discutida mais tarde no artigo:

# muda os nomes dos campos
dfGorjeta.rename(columns={"total_bill":"valor_conta", "tip":"gorjeta",
                        "smoker":"fumante", "sex":"sexo","day":"dia",
                        "time":"hora","size":"pessoas"}, inplace=True)

# traduzindo os valores dos campos:
dfGorjeta.loc[dfGorjeta["fumante"] == "No", "fumante"] = "não"
dfGorjeta.loc[dfGorjeta["fumante"] == "Yes", "fumante"] = "sim"
dfGorjeta.loc[dfGorjeta["sexo"] == "Female", "sexo"] = "mulher"
dfGorjeta.loc[dfGorjeta["sexo"] == "Male", "sexo"] = "homem"
dfGorjeta.loc[dfGorjeta["hora"] == "Dinner", "hora"] = "jantar"
dfGorjeta.loc[dfGorjeta["hora"] == "Lunch", "hora"] = "almoço"
dfGorjeta.loc[dfGorjeta["dia"] == "Fri", "dia"] = "sex"
dfGorjeta.loc[dfGorjeta["dia"] == "Sat", "dia"] = "sab"
dfGorjeta.loc[dfGorjeta["dia"] == "Sun", "dia"] = "dom"
dfGorjeta.loc[dfGorjeta["dia"] == "Thur", "dia"] = "qui"

# Temos agora o seguinte dataframe
dfGorjeta
valor_conta gorjeta sexo fumante dia hora pessoas
0 16.99 1.01 mulher não dom jantar 2
1 10.34 1.66 homem não dom jantar 3
2 21.01 3.50 homem não dom jantar 3
3 23.68 3.31 homem não dom jantar 2
4 24.59 3.61 mulher não dom jantar 4
239 29.03 5.92 homem não sab jantar 3
240 27.18 2.00 mulher sim sab jantar 2
241 22.67 2.00 homem sim sab jantar 2
242 17.82 1.75 homem não sab jantar 2
243 18.78 3.00 mulher não qui jantar 2

As consultas SQL realizadas a seguir pressupõe a existência de um banco de dados com o mesmo nome, a mesma estrutura e dados que o dataframe dfGorjetas.

SELECT

Nas consultas SQL as seleções são feitas com uma lista de nomes de campos que se deseja retornar, separados por vírgula, ou através do atalho * (asterisco) para selecionar todas as colunas. No pandas a seleção de colunas é feita passando-se uma lista de nomes de campos para o DataFrame. Uma chamada ao dataframe sem uma lista de nomes de colunas resulta no retorno de todas as colunas, da mesma forma que usar * no SQL.

–– sql: consulta (query) usando select
SELECT valor_conta, gorjeta, fumante, hora FROM dfGorjeta LIMIT 5;
# pandas:
dfGorjeta[["valor_conta", "gorjeta", "hora"]].head()
valor_conta gorjeta hora
0 16.99 1.01 jantar
1 10.34 1.66 jantar
2 21.01 3.50 jantar
3 23.68 3.31 jantar
4 24.59 3.61 jantar

O método head(n) limita o retorno do dataframe às n primeiras linhas. n = 5 é o default. Para listar as últimas linhas usamos tail(n). Linhas também podem ser selecionadas por chamadas ao sei indice.

# Para acessar as últimas linhas do dataframe podemos usar
# dfGorjeta[["valor_conta", "gorjeta", "hora"]].tail()

# selecionando linhas por meio de seu índice.
dfGorjeta.iloc[[1,239,243]]
valor_conta gorjeta sexo fumante dia hora pessoas
1 10.34 1.66 homem não dom jantar 3
239 29.03 5.92 homem não sab jantar 3
243 18.78 3.00 mulher não qui jantar 2

Os dataframes possuem a propriedade shape que contém sua dimensionalidade. No nosso caso temos

dfGorjeta.shape
(244, 7)

o que significa que são 244 linhas em 7 campos.

No SQL você pode retornar uma coluna resultado de um cálculo usando elementos de outras colunas. No pandas podemos usar o método assign() para inserir uma coluna calculada:

–– sql:
SELECT *, gorjeta/valor_conta*100 as percentual FROM dfGorjeta LIMIT 4;
# pandas: método assign()
dfGorjeta.assign(percentual = dfGorjeta["gorjeta"] / dfGorjeta["valor_conta" *100]).head(4)
valor_conta gorjeta sexo fumante dia hora pessoas percentual
0 16.99 1.01 mulher não dom jantar 2 5.944673
1 10.34 1.66 homem não dom jantar 3 16.054159
2 21.01 3.50 homem não dom jantar 3 16.658734
3 23.68 3.31 homem não dom jantar 2 13.978041

Essa coluna é retornada mas não fica anexada ao dataframe. Para anexar uma coluna ao dataframe podemos atribuir o resultado do cálculo a uma nova coluna:

dfGorjeta["percentual"] = dfGorjeta["gorjeta"] / dfGorjeta["valor_conta"] * 100
print("Nessa estapa temos as colunas:\n", dfGorjeta.columns)

# Vamos apagar a coluna recém criada para manter a simplicidade da tabela
dfGorjeta.drop(["percentual"], axis=1, inplace=True)
Nessa estapa temos as colunas:
Index([‘valor_conta’, ‘gorjeta’, ‘sexo’, ‘fumante’, ‘dia’, ‘hora’, ‘pessoas’, ‘percentual’],
dtype=’object’)

WHERE


Filtragem de dados em consultas SQL são feitas através da cláusula WHERE. DataFrames podem ser filtrados de várias formas diferentes. O indexamento com valores booleanos é provavelmente o mais simples:

–– cláusula WHERE do sql
SELECT * FROM dfGorjeta WHERE hora = "jantar" LIMIT 5;
# filtragem por indexamento no pandas
dfGorjeta[dfGorjeta["hora"] == "jantar"].head(5)
valor_conta gorjeta sexo fumante dia hora pessoas
0 16.99 1.01 mulher não dom jantar 2
1 10.34 1.66 homem não dom jantar 3
2 21.01 3.50 homem não dom jantar 3
3 23.68 3.31 homem não dom jantar 2
4 24.59 3.61 mulher não dom jantar 4

A consulta acima funciona da seguinte forma:

# is_jantar é uma série contendo True e False (True para jantares, False para almoços)
is_jantar = dfGorjeta["hora"] == "jantar"
# usamos display para exibir a contagem de falsos/verdadeiros
display("Quantos jantares/almoços:", is_jantar.value_counts())

# para negar a série inteira, invertendo True ↔ False usamos ~ (til)
# a linha abaixo imprime o número de almoços na tabela
print("A lista contém %d almoços" % dfGorjeta[~is_jantar]["hora"].count())

# também podemos obter a lista das entradas que não correspondem a "jantar" usando
# dfGorjeta[dfGorjeta["hora"] != "jantar"]
‘Quantos jantares/almoços:’
True 176
False 68
Name: hora, dtype: int64A lista contém 68 almoços

Quando essa série é passada para o dataframe apenas as linhas correspondentes à True são retornados. A última consulta é equivalente à dfGorjeta[~is_jantar].head().

No SQL podemos procurar por partes de uma string com a cláusula LIKE. No pandas transformamos o campo dfGorjeta["sexo"]em uma string que possui o método startswith("string").

–– sql: SELECT TOP 2 sexo, valor_conta FROM dfGorjeta WHERE sexo LIKE 'ho%';
dfGorjeta.loc[dfGorjeta['sexo'].str.startswith('ho'),['sexo','valor_conta']].head(2)

que retorna as 2 primeiras linhas em que o campo sexo começa com o texto “ho”.

Também podemos procurar por campos que estão incluidos em um conjunto de valores:

–– sql:
SELECT * FROM dfGorjeta WHERE dia IN ('sab', 'dom');
dfGorjeta.loc[dfGorjeta['dia'].isin(["dom", "sab"])]

que retorna todas as linhas em que o campo dia é “dom” ou “sab”.

Assim como se pode usar operadores lógicos AND e OR nas consultas SQL para inserir múltiplas condições, o mesmo pode ser feito com dataframes usando | (OR) e & (AND). Por ex., para listar as gorjetas com valor superior à $5.00 dadas em jantares:

–– SQL: múltiplas condições em WHERE
SELECT * FROM dfGorjeta WHERE hora = 'jantar' AND gorjeta > 6.00;
# no pandas
dfGorjeta[(dfGorjeta["hora"] == "jantar") & (dfGorjeta["gorjeta"] > 6.00)]
valor_conta gorjeta sexo fumante dia hora pessoas
23 39.42 7.58 homem não sab jantar 4
59 48.27 6.73 homem não sab jantar 4
170 50.81 10.00 homem sim sab jantar 3
183 23.17 6.50 homem sim dom jantar 4
212 48.33 9.00 homem não sab jantar 4
214 28.17 6.50 mulher sim sab jantar 3

Podemos obter uma lista dos dados correspondentes a gorjetas dadas por grupos com 5 ou mais pessoas ou com contas de valor acima de $45.00, limitada aos 4 primeiros registros:

–– SQL:
SELECT * FROM dfGorjeta WHERE pessoas >= 5 OR valor_conta > 45 LIMIT 4;
# pandas
dfGorjeta[(dfGorjeta["pessoas"] >= 5) | (dfGorjeta["valor_conta"] > 45)].head(4)
valor_conta gorjeta sexo fumante dia hora pessoas
59 48.27 6.73 homem não sab jantar 4
125 29.80 4.20 mulher não qui almoço 6
141 34.30 6.70 homem não qui almoço 6
142 41.19 5.00 homem não qui almoço 5

Dados ausentes são representados por NULL no, uma marca especial para indicar que um valor não existe no banco de dados. Nos dataframes do pandas o mesmo papel é desempenhado por NaN (Not a Number). Esses marcadores podem surgir, por ex., na leitura de um arquivo csv (valores separados por vírgulas) quando um valor está ausente ou não é um valor numérico em uma coluna de números. Para verificar o comportamento do pandas com NaN criamos um dataframe com valores ausentes. Verificações de nulos é feita com os métodos notna() e isna().

frame = pd.DataFrame({"col1": ["A", "B", np.NaN, "C", "D"], "col2": ["F", np.NaN, "G", "H", "I"]})
frame
col1 col2
0 A F
1 B NaN
2 NaN G
3 C H
4 D I

Se temos um banco de dados SQLcom essa estrutura e conteúdo podemos extrair as linhas onde col2 é NULL usando a consulta:

–– sql
SELECT * FROM frame WHERE col2 IS NULL;
# no case do pandas usamos
frame[frame["col2"].isna()]
col1 col2
1 B NaN

De forma análoga, podemos extrair as linhas para as quais col1 não é NULL. No pandas usamos notna().

–– sql
SELECT * FROM frame WHERE col1 IS NOT NULL;
# pandas: linhas em que col1 não é nula
frame[frame["col1"].notna()]
col1 col2
0 A F
1 B NaN
3 C H
4 D I

GROUP BY


No SQL consultas com agrupamentos são feitas usando-se as operações GROUP BY. No pandas existe o método groupby() que tipicamente particiona o conjunto de dados em grupos e aplica alguma função (em geral de agregamento), combinando depois os grupos resultantes.

Um exemplo comum é o de particionar os dados em grupos menores e contar os elementos desses grupos. Voltando ao nosso dataframe dfGorjeta podemos consultar quantas gorjetas foram dadas por grupos de cada sexo:

–– sql
SELECT sexo, count(*) FROM dfGorjeta GROUP BY sexo;
# o equivalente em pandas seria
dfGorjeta.groupby("sexo").size()
sexo
mulher 87
homem 157
dtype: int64

O resultado é uma series cujos valores podem ser retornados por seu nome de index ou pelo número desse indice.

print("A lista contém %d homens" % dfGorjeta.groupby("sexo").size()[0])
print("\t\t e %d mulheres" % dfGorjeta.groupby("sexo").size()["mulher"])
A lista contém 157 homens
e 87 mulheres

É possível aplicar o método count() para cada coluna, individualmente:

dfGorjeta.groupby("sexo").count()
valor_conta gorjeta fumante almoço hora pessoas
sexo
mulher 87 87 87 87 87 87
homem 157 157 157 157 157 157

Observe que no código do pandas usamos size() e não count(). Isso foi feito porque o método count() é aplicado sobre cada coluna e retorna tantos valores quantas colunas existem, com valores não null.

Também se pode aplicar o método count() para uma coluna específica:

# para contar valores em uma única coluna primeiro ela é selecionada, depois contada
dfGorjeta.groupby("sexo")["valor_conta"].count()
sexo
mulher 87
homem 157
Name: valor_conta, dtype: int64

Existem diversas funções de agregamento. São elas:

função descrição
mean() calcula médias para cada grupo
sum() soma dos valores do grupo
size() *tamanhos dos grupos
count() número de registros no grupo
std() desvio padrão dos grupos
var() variância dos grupos
sem() erro padrão da média dos grupos
describe() gera estatísticas descritivas
first() primeiro valor no grupo
last() último valor no grupo
nth() n-ésimo valor (ou subconjunto se n for uma lista)
min() valor mínimo no grupo
max() valor máximo no grupo

* A função size() retorna o número de linhas em uma serie e o número de linhas × colunas em dataframes.

Para obter um resumo estatístico relativo ao campo gorjeta, agrupado pelo campo sexo podemos usar:

dfGorjeta.groupby("sexo")["gorjeta"].describe()
count mean std min 25% 50% 75% max
sexo
homem 157.0 3.089618 1.489102 1.0 2.0 3.00 3.76 10.0
mulher 87.0 2.833448 1.159495 1.0 2.0 2.75 3.50 6.5

Múltiplas funções podem ser aplicadas de uma vez. Suponha que queremos determinar como os valores das gorjetas variam por dia da semana. O método agg() (de agregar) permite que se passe um dicionário para o dataframe agrupado, indicando que função deve ser aplicada a cada coluna.

–– sql (agrupe os dados por dia, calcule a média para cada dia e o número de entradas contadas)
SELECT dia, AVG(gorjeta), COUNT(*) FROM dfGorjeta GROUP BY dia;
# na pandas, use mean no campo gorjeta, size no campo dia
dfGorjeta.groupby("dia").agg({"gorjeta": np.mean, "dia": np.size})
gorjeta dia
dia
dom 3.255132 76
qui 2.771452 62
sab 2.993103 87
sex 2.734737 19

Também é possível realizar o agrupamento por mais de uma coluna. Para fazer isso passamos uma lista de colunas para o método groupby().

–– agrupe primeiro por "fumante", depois por "dia"
–– realize a contagem dos registros e a média das gorjetas
SELECT fumante, dia, COUNT(*), AVG(gorjeta) FROM dfGorjeta GROUP BY fumante, dia;
# no pandas
dfGorjeta.groupby(["fumante", "dia"]).agg({"gorjeta": [np.size, np.mean]})
gorjeta
size mean
fumante dia
não dom 57.0 3.167895
qui 45.0 2.673778
sab 45.0 3.102889
sex 4.0 2.812500
sim dom 19.0 3.516842
qui 17.0 3.030000
sab 42.0 2.875476
sex 15.0 2.714000

JOIN

No SQL tabelas podem ser juntadas ou agrupadas através da cláusula JOIN. Junções podem ser LEFT, RIGHT, INNER, FULL. No pandas se usa os métodos join() ou merge(). Por defaultjoin() juntará os DataFrames por seus índices. Cada método tem parâmetros que permitem escolher o tipo da junção (LEFT, RIGHT, INNER, FULL), ou as colunas que devem ser juntadas (por nome das colunas ou índices). [Linguagem de Consultas SQL]

# para os exercícios que se seguem criamos os dataframes
df1 = pd.DataFrame({"key": ["A", "B", "C", "D"], "value":  [11, 12, 13, 14]})
df2 = pd.DataFrame({"key": ["B", "D", "D", "E"], "value":  [21, 22, 23, 24]})
# para exibir esses dataframes com formatação usamos display()
display(df1)
display(df2)
key value
0 A 11
1 B 12
2 C 13
3 D 14
key value
0 B 21
1 D 22
2 D 23
3 E 24

Como antes supomos a existência de duas tabelas de dados sql como as mesmas estruturas e dados para considerarmos as várias formas de JOINs.

INNER JOIN

–– junção das duas tabelas ligadas por suas chaves - key
SELECT * FROM df1 INNER JOIN df2 ON df1.key = df2.key;
# por default merge() faz um INNER JOIN
pd.merge(df1, df2, on="key")
key value_x value_y
0 B 12 21
1 D 14 22
2 D 14 23

O método merge() também oferece parâmetros para que sejam feitas junções de uma coluna de um dataframe com o índice de outro dataframe. Para ver isso vamos criar outro dataframe a partir de df2, usando o campo key como índice.

# novo dataframe tem campo "key" como índice
df2_indice = df2.set_index("key")
display(df2_indice)
pd.merge(df1, df2_indice, left_on="key", right_index=True)
value
key
B 21
D 22
D 23
E 24
key value_x value_y
1 B 12 21
3 D 14 22
3 D 14 23

LEFT OUTER JOIN

A junção LEFT OUTER JOIN recupera todos as campos à esquerda, existindo ou não uma linha correspondente à direita. O parâmetro how="left" é o equivalente no pandas.

–– sql: recupera todos os valores de df1 existindo ou não correspondente em df2
SELECT * FROM df1 LEFT OUTER JOIN df2 ON df1.key = df2.key;
# pandas: how="left" equivale a LEFT OUTER JOIN
pd.merge(df1, df2, on="key", how="left")
key value_x value_y
0 A 11 NaN
1 B 12 21
2 C 13 NaN
3 D 14 22
4 D 14 23

Observe que df2 não possui campos com key = "A" ou key = "C" e, por isso o dataframe resultante tem NaN nessas entradas. key = "A". Como df2 tem 2 linhas para key = "D" a linha aparece duplicada para essa key em df1.

RIGHT JOIN

A junção RIGH OUTER JOIN recupera todos as campos à direita, existindo ou não uma linha correspondente à esquerda. O parâmetro how="right" é o equivalente no pandas.

–– sql: recupera todos os registros em df2
SELECT * FROM df1 RIGHT OUTER JOIN df2 ON df1.key = df2.key;
# pandas: how="right" equivale a RIGHT OUTER JOIN
pd.merge(df1, df2, on="key", how="right")
key value_x value_y
0 B 12 21
1 D 14 22
2 D 14 23
3 E NaN 24

FULL JOIN

A junção FULL OUTER JOIN recupera todos as campos à direita ou à esquerda, representando como NaN os valores ausentes em uma ou outra. Todos as linhas das duas tabelas são retornadas com junção onde a campo key existe em ambas. O parâmetro how="outer" é o equivalente no pandas. Observe que nem todos os gerenciadores de bancos de dados permitem essa operação.

–– sql: retorna todos os registros em ambas as tabelas
SELECT * FROM df1 FULL OUTER JOIN df2 ON df1.key = df2.key;
# pandas: how="outer" é o equivalente em dataframes
pd.merge(df1, df2, on="key", how="outer")
key value_x value_y
0 A 11 NaN
1 B 12 21
2 C 13 NaN
3 D 14 22
4 D 14 23
5 E NaN 24

UNION

Para os exemplos seguintes definimos mais 2 dataframes:

df3 = pd.DataFrame({"cidade": ["Rio de Janeiro", "São Paulo", "Belo Horizonte"], "nota": [1, 2, 3]})
df4 = pd.DataFrame({"cidade": ["Rio de Janeiro", "Curitiba", "Brasília"], "nota": [1, 4, 5]})

No SQL a clásula UNION ALL é usada para juntar as linhas retornadas em dois (ou mais) instruções de SELECT. Linhas duplicadas são mantidas. O mesmo efeito pode ser conseguido no pandas usando-se o método concat().

–– sql: UNION ALL
SELECT city, rank FROM df3 UNION ALL SELECT cidade, nota FROM df4;
# pandas: concat
pd.concat([df3, df4])
cidade nota
0 Rio de Janeiro 1
1 São Paulo 2
2 Belo Horizonte 3
0 Rio de Janeiro 1
1 Curitiba 14
2 Brasília 5

No SQL a cláusula UNION tem o mesmo efeito que UNION ALL mas remove as linhas duplicadas. No pandas isso pode ser conseguido se fazendo a conactenação concat() seguida de drop_duplicates().

–– SQL UNION
SELECT city, rank FROM df1 UNION SELECT city, rank FROM df2;
–– o registro duplicado no Rio de Janeiro fica excluído
# pandas: concat() seguido de drop_duplicates()
pd.concat([df1, df2]).drop_duplicates()
cidade nota
0 Rio de Janeiro 1
1 São Paulo 2
2 Belo Horizonte 3
1 Curitiba 14
2 Brasília 5

Outras funções analíticas e de agregamento

Para os próximos exemplos vamos retornar ao nosso dataframe dfGorjeta: para listar as 5 gorjetas mais altas, no MySQL (a sintaxe varia de um para outro gerenciador).

–– MySQL: retorna todos os campos em ordem decrescente, 5 linhas
SELECT * FROM dfGorjeta ORDER BY gorjeta DESC LIMIT 10 OFFSET 5;
# pandas: seleciona 15 maiores e exibe as 10 de menor valor
dfGorjeta.nlargest(15, columns="gorjeta").tail(10)
valor_conta gorjeta sexo fumante dia hora pessoas
183 23.17 6.50 homem sim Dom jantar 4
214 28.17 6.50 mulher sim sab jantar 3
47 32.40 6.00 homem não Dom jantar 4
239 29.03 5.92 homem não sab jantar 3
88 24.71 5.85 homem não Thur almoço 2
181 23.33 5.65 homem sim Dom jantar 2
44 30.40 5.60 homem não Dom jantar 4
52 34.81 5.20 mulher não Dom jantar 4
85 34.83 5.17 mulher não Thur almoço 4
211 25.89 5.16 homem sim sab jantar 4

UPDATE

Há muitas formas de alterar um valor em um campo de um dataframe. Por exemplo, abaixo realizamos uma alteração em todos os valores de gorjeta sempre que gorjeta < 2.

–– sql: em todas as linhas duplique a gorjeta se gorjeta for menor que 1.1
UPDATE dfGorjeta SET gorjeta = gorjeta*2 WHERE gorjeta < 1.1;
# pandas: o mesmo resultado pode ser obtido da aseguinte forma
# dfGorjeta.loc[dfGorjeta["gorjeta"] < 1.1, "gorjeta"] *= 2

Para explicar com mais detalhes o funcionamento deste código, armazenamos abaixo a lista dos índices das linhas de gorjetas mais baixas e exibimos essas linhas. Em seguida multiplicamos apenas as gorjetas dessas linhas por 2 e examinamos o resultado:

indices = dfGorjeta[dfGorjeta["gorjeta"] < 1.1].index
print("Índices de gorjetas < 1.1:", indices)
display("Lista de gorjetas < 1.1", dfGorjeta.iloc[indices])
# multiplica essas gorjetas por 2
dfGorjeta.loc[dfGorjeta["gorjeta"] < 1.1, "gorjeta"] *= 2
# lista as mesmas linhas após a operação
display("Gorjetas após a operação:", dfGorjeta.iloc[indices])
Índices de gorjetas < 1.1: Int64
Index([0, 67, 92, 111, 236], dtype=’int64′)
‘Lista de gorjetas < 1.1’

valor_conta gorjeta sexo fumante dia hora pessoas
0 16.99 1.01 mulher não dom jantar 2
67 3.07 1.00 mulher sim sab jantar 1
92 5.75 1.00 mulher sim sex jantar 2
111 7.25 1.00 mulher não sab jantar 1
236 12.60 1.00 homem sim sab jantar 2

‘Gorjetas após a operação:’

valor_conta gorjeta sexo fumante dia hora pessoas
0 16.99 2.02 mulher não dom jantar 2
67 3.07 2.00 mulher sim sab jantar 1
92 5.75 2.00 mulher sim sex jantar 2
111 7.25 2.00 mulher não sab jantar 1
236 12.60 2.00 homem sim sab jantar 2
–– sql: alterar um campo de uma linha específica (supondo a existência de um campo id)
UPDATE dfGorjeta SET sexo = 'NI' WHERE id = 239
# para alterar o campo sexo para 'NI' (não informado)
dfGorjeta.loc[239, 'sexo'] ='NI'

DELETE

Existem muitas formas de se excluir linhas de um dataframe mas é comum a prática de selecionar as linhas que devem ser mantidas e copiar para um novo dataframe.

–– sql: linhas são apagadas sob um certo critério
DELETE FROM dfGorjeta WHERE gorjeta > 9;
# pandas: como novo dataframe tem o mesmo nome do original, o antigo é sobrescrito e perdido
dfTop = dfGorjeta.loc[dfGorjeta["gorjeta"] > 9]
dfTop
valor_conta gorjeta sexo fumante dia hora pessoas
170 50.81 10.0 homem sim sab jantar 3

Também é possível apagar linhas usando seu índice:

# apagar linha com index = 4, inplace para substituir o dataframe
dfGorjeta.drop(index=4, inplace=True)
# apagar linhas com index = 0 até 3
dfGorjeta.drop(index=[0, 1, 2, 3], inplace=True)
dfGorjeta.head()
valor_conta gorjeta sexo fumante dia hora pessoas
5 25.29 4.71 homem não dom jantar 4
6 8.77 12.00 homem não dom jantar 2
7 26.88 3.12 homem não dom jantar 4
8 15.04 1.96 homem não dom jantar 2
9 14.78 3.23 homem não dom jantar 2
🔺Início do artigo

Bibliografia

  • McKinney, Wes: Python for Data Analysis, Data Wrangling with Pandas, NumPy,and IPython
    O’Reilly Media, 2018.
  • Pandas: página oficial, acessada em janeiro de 2021.