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: