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:

NumPy, Introdução


Numpy

NumPy é uma biblioteca do python especializada em computação científica e análise de dados. Ela é usada em diversos tipos de operações que envolvem operações matriciais. Além disso suas matrizes formam a base para outros pacotes, como o pandas e outras voltadas para o cálculo matemático e científico. Numpy foi primeiro lançado por Travis Oliphant em 2006 e tem sido mantido por um grande número de colabores desde então, sob licença BSD.

NumPy, com suas matrizes, apresenta algumas vantagens sobre o cálculo usual com objetos do python, como listas e tuplas. Essas operações são mais rápidas e flexíveis e foram construídas de forma a evitar a necessidade da realização de laços (loops ). Ela contém métodos voltados para operações da álgebra linear, geração de números aleatórios e transformadas de Fourier, além da interface voltada para a conexão com as linguagens C, C++ e FORTRAN.

Um exemplo rápido pode mostrar como as rotinas do numpy são mais eficientes que as de objetos list do python.

» # comparação de velocidades
» import numpy as np
» # um array do numpy
» array = np.arange(1_000_000)
» # uma lista usual do python
» lista = list(range(1_000_000))

» %time for _ in range(100): arr2 = array * 2
↳ CPU times: user 145 ms, sys: 7.93 ms, total: 152 ms
  Wall time: 151 ms

» %time for _ in range(100): lista2 = [x * 2 for x in lista]
↳ CPU times: user 5.35 s, sys: 778 ms, total: 6.13 s
  Wall time: 6.12 s

» # 5.35/.145 ≈ 37 × mais rápido

Wall time é o tempo total gasto pelo código para ser executado. CPU time é uma medida do tempo gasto pelo processador apenas quando esteve operando sobre a tarefa específica. Dependendo do cálculo feito o numpy pode ser mais de 100 vezes mais rápido que uma operação similar em puro python.

Instalação


Em geral o módulo está presente como pacote na maioria das distribuições de Python. Se necessária a sua instalação em separado pode ser feita. Numpy e pandas são instalados juntos com o Anaconda.

# No Linux (Ubuntu and Debian):
» sudo apt-get install python-numpy
# No Linux (Fedora)
» sudo yum install numpy scipy
# No Windows com Anaconda:
» conda install numpy
# após a instalação o módulo deve ser importado:
» import numpy as np
# O aliás np é opcional e de escolha do programador.

Ndarray

O objeto básico da biblioteca Numpy é o ndarray (N-dimensional array). Ndarrays são matrizes multidimensionais com número determinado de dimensões e elementos. Seus elementos pertencem todos a um único tipo, chamado de dtype (data-type ou tipo de dado). Cada dimensão é denominada por axis (eixo) e o número de eixos é o rank do objeto. Diferente das listas do python, ndarrays têm dimensões fixas, definidas em sua construção.

Um ndarray possui os seguintes atributos referentes ao seu tipo de dado, tamanho e ordem:

Atributo descrição
array.dtype tipo de dado armazenado. (Veja lista abaixo),
array.ndim número de dimensões (que são eixos ou axis ); o mesmo que rank
array.size número de elementos em cada eixo,
array.shape (ou forma), tupla de N inteiros positivos com o comprimento de cada eixo.

Um ndarray de 1 dimensão é um objeto similar a um vetor (rank = 1): arr1D = ([a0,a1,...,aN-1,]), arr1D.size = N, arr1D.ndim = 1, arr1D.shape = (N,).

Um ndarray de 2 dimensões é um objeto similar a uma matriz (rank = 2): se ela possui M linhas, cada uma com N elementos então arr2D.size = M × N , arr2D.ndim = 2, arr2D.shape = (M,N).

Um ndarray de 3 dimensões é uma coleção de matrizes (rank = 3): se ela possui K matrizes de M linhas, cada uma com N elementos então arr3D.size = K × M × N , arr3D.ndim = 3, arr3D.shape = (K, M, N).

Ndarrays de ordem superior são generalizações desse processo, acrescentados novos eixos.

Os eixos são numerados para diversas operações. Em 2 dimensões axis=0 são as linhas, axis=1 as colunas, e assim consecutivamente para ordens superiores.

Tipos, dtypes

Além dos tipos usuais do python, a importação de Numpy disponibiliza um conjunto extendido de tipos ou dtypes.

dtype descrição
bool booleano (true ou false) armazenado como um byte
intX inteiro com sinal, X-bit (X=8,16,32, 64)
uintX inteiro sem sinal, X-bit (X=8,16,32, 64)
intc idêntical ao int C (em geral int32 ou int64)
intp inteiro usado para indexação (como C size_t; em geral int32 ou int64)
float_ o mesmo que float64
float16 meia precisão float: sign bit, 5-bit exponente, 10-bit mantissa
float32 simple precisão float: sign bit, 8-bit exponente, 23-bit mantissa
float64 dupla precisão float: sign bit, 11-bit exponente, 52-bit mantissa
complex_ o mesmo que complex128
complex64 complexo, representado por dois 32-bit floats (parte real e imaginária)
complex128 complexo, representado por dois 64-bit floats (parte real e imaginária)

Construção de um array

Um array do NumPy pode ser criado passando-se uma lista para construtor np.array(lista).

» import numpy as np

» lista = [123,234,345]
» arr = np.array(lista)
» arr
↳ array([123, 234, 345])

# o objeto criado é um ndarray do numpy
» type(arr)
↳ numpy.ndarray

» arr.dtype
↳ dtype('int64')

» arr.ndim
↳ 1
» arr.shape
↳ (3,)
» arr.size
↳ 3

» # uma lista de listas
» lista2 = [[123,234,345],
            [456,567,678],
            [789,890,901]]
» arr2 = np.array(lista2)
» arr2
↳ array([[123, 234, 345],
         [456, 567, 678],
         [789, 890, 901]])

» arr2.ndim
↳ 2
» arr2.size
↳ 9
» arr2.shape
↳ (3, 3)

» # lista de listas de listas
» lista3 =[ [ [1,2],[2,3] ], [ [3,4],[4,5] ],  [ [1,2],[2,3] ], [ [3,4],[4,5] ] ]
» arr3 = np.array(lista3)
» # o resultado é: 4 matrizes de 2 x 2  elementos
» arr3
↳ array([[[1, 2],
         [2, 3]],

        [[3, 4],
         [4, 5]],

        [[1, 2],
         [2, 3]],

        [[3, 4],
         [4, 5]]])

» arr3.ndim
↳ 3
» arr3.shape
↳ (4, 2, 2)
» arr3.size
↳ 16

» # a 1ª matriz
» arr3[0]

↳  array([[1, 2],
         [2, 3]])

» # a 2ª linha da 1ª matriz
» arr3[0,1]
↳ array([2, 3])

» # o 1º elemento da 2ª linha da 1ª matriz
» arr3[0,1,0]
↳ 2

# todos os arrays criados tem o mesmo dtype
» arr3.dtype
↳ dtype('int64')

Arrays podem ser de outros tipos, como um array de strings. No entanto devem ser homogêneos (todos os elementos do mesmo tipo). Uma tentativa de criar um array como em stArr2 causa uma tentativa de ajuste (cast), transformando os inteiros em string. O método array(listas, dtype) aceita o parâmetro dtype onde se pode informar o tipo de elemento que se pretende armazenar.

» stArr = np.array([['a', 'b'],['c', 'd']])
» stArr
↳ array([['a', 'b'],
       ['c', 'd']], dtype='<U1')

» stArr[0,1]
↳ 'b'
» stArr.dtype
↳ dtype('<U1')
» stArr.dtype.name
↳ 'str32'

» stArr2 = np.array([[1.01, 2.02],['h', 'i']])
» stArr2
↳ array([['1.01', '2.02'],
        ['h', 'i']], dtype='<U21')

» stArr3 = np.array([[True, False],['h', 'i']])
» stArr3
↳ array([['True', 'False'],
        ['h', 'i']], dtype='<U5')

» # parâmetro dtype
» cplx = np.array([[1, 2, 3],[4, 5, 6]], dtype=complex)
» cplx
↳ array([[1.+0.j, 2.+0.j, 3.+0.j],
        [4.+0.j, 5.+0.j, 6.+0.j]])               

Arrays podem ser transformados de um tipo para outro (quando possível). Para isso usamos array.astype(). Na transformação de floats para inteiros a parte decimal será truncada. Arrays de strings, desde que devidamente formatados, podem ser convertidos em numéricos.

» # criando um array de integers
» arr = np.array([1, 2, 3, 4, 5])
» arr.dtype
↳ dtype('int64')

» # cast para array de ponto flutuante
» floatArr = arr.astype(np.float64)
» floatArr.dtype
↳ dtype('float64')

» # floats para integers
» # criando um array de floats
» arr = np.array([1.9, -8.2, -9.6, 0.9, 2.3, 10.7])
» arr
↳ array([ 1.9, -8.2, -9.6, 0.9, 2.3, 10.7])

» # converte para inteiros (trunca parte inteira)
» arr.astype(np.int32)
↳ array([ 1, -8, -9, 0, 2, 10], dtype=int32)

» # um array de strings
» arrNumStrings = np.array(['0.0', '7.75', '-6.6', '100'], dtype=np.string_)
» arrNumStrings.astype(float)
↳ array([ 0., 7.75, -6.6 , 100.]

Métodos predefinidos de construção

O método np.arange(m,n,[p]) retorna um array de inteiros no intervalo (m, n], i.e., começando em m e terminando em n-1. Se o primeiro argumento for omitido m=0. Um terceiro argumento informa o p=passo, intervalo entre os valores da sequência. Em np.arange(m,n,p) m, n devem ser inteiros mas p pode ser um float.

O método np.linspace(m,n,p) retorna um array no intervalo (m, n), ambos os extremos incluídos, com p números igualmente espaçados.

np.random.random(n) retorna um array com n elementos aleatórios e np.random(m, n) retorna um array com shape = (m,n) e elementos aleatórios.

» # np.range(n)
» np.arange(10)
↳ array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

» # np.range(m,n)
» np.arange(5, 15)
↳ array([ 5,  6,  7,  8,  9, 10, 11, 12, 13, 14])

» # np.range(m,n,p)
» np.arange(10, 100, 10)
↳ array([10, 20, 30, 40, 50, 60, 70, 80, 90])

» np.arange(10, 20, .5)
↳ array([10. , 10.5, 11. , 11.5, 12. , 12.5, 13. , 13.5, 14. , 14.5, 15. ,
         15.5, 16. , 16.5, 17. , 17.5, 18. , 18.5, 19. , 19.5])

» # np.linspace(m,n,p)
» np.linspace(10, 20, 5)
↳ array([10. , 12.5, 15. , 17.5, 20. ])

» # a sequência pode ser decrescente
» np.linspace(20, 10, 5)
↳ array([20. , 17.5, 15. , 12.5, 10. ])

» # np.random.random(n)
» np.random.random(10)
↳ array([0.35433322, 0.54555179, 0.48783323, 0.5785414 , 0.76837232,
         0.69888297, 0.62492788, 0.33289321, 0.75068313, 0.95667854])

» # np.random.random(m,n)
» np.random.random((2,3))
↳ array([[0.11351481, 0.76831577, 0.27597676],
         [0.73130126, 0.7225559 , 0.54040225]])

Os métodos np.zeros((m,n)) e np.ones((m,n)) criam, respectivamente, ndarrays de zeros e uns com as dimensões dadas pela tupla (ou lista) no argumento. np.eye(m,n) gera um ndarray m × n com elementos 1 na diagonal, 0 fora dela. np.eye(n,n) é o mesmo que np.eye(n) ou np.identity(n), que são a matriz identidade de n dimensões.

» np.zeros((2,4))
↳ array([[0., 0., 0., 0.],
         [0., 0., 0., 0.]])

» np.ones((2,3))
↳ array([[1., 1., 1.],
         [1., 1., 1.]])

» np.eye(2,4)
↳ array([[1., 0., 0., 0.],
         [0., 1., 0., 0.]])

» np.identity(3)
↳ array([[1., 0., 0.],
         [0., 1., 0.],
         [0., 0., 1.]])

» np.eye(2,6)
↳ array([[1., 0., 0., 0., 0., 0.],
         [0., 1., 0., 0., 0., 0.]])

» np.eye(4,4)
↳ array([[1., 0., 0., 0.],
         [0., 1., 0., 0.],
         [0., 0., 1., 0.],
         [0., 0., 0., 1.]])

O método np.empty(m,n) permite a criação de arrays vazios, em geral destinados a serem preenchidos depois de sua criação por meio de algum cálculo ou leitura de dados. Nos exemplos criamos um array numérico vazio, arrN. Observe que não há garantia de que as entradas serão nulas. Em seguida criamos um array vazio de strings, com espaço para 3 caracteres (dtype='<U3′), e o preenchemos em um loop.

» # array numérico "vazio"
» arrN = np.empty((1,3))
» arrN
↳ array([[4.66896202e-310, 0.00000000e+000, 1.58101007e-322]])    

» # array de strings
» arr = np.empty((3,3), dtype='<U3')
» arr
↳ array([['', '', ''],
         ['', '', ''],
         ['', '', '']], dtype='<U3')

» for linha in range(arr.shape[0]):
»     for coluna in range(arr.shape[1]):
»         arr[linha,coluna] = 'a' + str(linha) + str(coluna)
» arr
↳ array([['a00', 'a01', 'a02'],
         ['a10', 'a11', 'a12'],
         ['a20', 'a21', 'a22']], dtype='<U3')

Alterando dimensões

Dado um array de uma única linha com r elementos podemos tranformá-lo em um array com shape = (m,n), desde que as dimensões sejam compatíveis, i.e., r = m × n. De fato, qualquer array pode ser transformado em outro se eles possuem o mesmo número de elementos (mesmo size ).

» arr = np.random.random(12)
» arr
↳ array([0.04276829, 0.76468762, 0.24807651, 0.75531679, 0.60327475,
         0.81704922, 0.08233836, 0.64112484, 0.55276595, 0.30669723,
         0.43989324, 0.60031761])

» # reshape
» arr.reshape(3,4)
↳ array([[0.04276829, 0.76468762, 0.24807651, 0.75531679],
         [0.60327475, 0.81704922, 0.08233836, 0.64112484],
         [0.55276595, 0.30669723, 0.43989324, 0.60031761]])

» np.linspace(0,10, 6).reshape(2,3)
↳ array([[ 0.,  2.,  4.],
         [ 6.,  8., 10.]])    

Indexação e fatiamento


Quando um array é criado ele recebe automaticamente um conjunto de índices. Um elemento pode ser lido ou alterado por meio de seu índice. Índices negativos contam de trás para frente, sendo arr[-1] o último elemento do array. Para selecionar (ou editar) vários elementos passamos uma lista de índices.

» arr = np.linspace(1,12, 12)
» arr
↳ array([ 1.,  2.,  3.,  4.,  5.,  6.,  7.,  8.,  9., 10., 11., 12.])

» # o 5º elemento
» arr[4]
↳ 5.

» # alterando o 5º elemento
» arr[4] = 100
» arr
↳ array([ 1.,  2.,  3.,  4.,  100.,  6.,  7.,  8.,  9., 10., 11., 12.])

» arr[-8]
↳ 100

» # lendo vários elementos
» arr[[0,2,5]]
↳ array([1., 3., 6.])

» # alterando vários elementos
» arr[[0,2,5]] = [10, 20, 30]
» arr
↳ array([ 10.,   2.,  20.,   4., 100.,  30.,   7.,   8.,   9.,  10.,  11.,  12.])

No caso de arrays bidimensionais os elementos do array são acessados pelos índices de suas linhas e colunas, sendo que arr[l,c] = al,c é o elemento da linha l e coluna c. Em objetos de ranks superiores cada índice se refere a um dos eixos.

Uma fatia ou slice do array é um subconjunto de elementos que pode ter o mesmo shape ou não. Para um vetor, digamos arr1D = [a0, a1, ..., aM] a fatia arr1D[m,n] = [am, ..., an-1], onde m ≥ 0, n ≤ M. Vale lembrar que o comprimento da fatia de um array unidimensional é arr1D[m,n].size = n-m.

» # outro teste para slices
» arr = np.arange(10)    # cria o array array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

» arr[6:9]
↳ array([6, 7, 8])

» arr[:3]
↳ array([0, 1, 2])

» arr[7:]
↳ array([7, 8, 9])

» # uma seção pode ser alterada
» arr[3:6] = -5
» arr
↳ array([ 0,  1,  2, -5, -5, -5,  6,  7,  8,  9])

» arr[7:] = -arr[7:]
» arr
↳ array([ 0,  1,  2, -5, -5, -5,  6, -7, -8, -9])

(†) Uma operação entre arrays de dimensões diferentes, como ocorre em arr[0:4] = -1 é chamado de propagação ou broadcasting. Para interagir com a 1ª parte da expressão a 2ª é transformada: -1 → arr[-1,-1,-1,-1]. Voltaremos a esse tópico.

Observe que uma fatia de um array é uma referência àquela parte do array e qualquer alteração feita na fatia se refletirá no array original. A notação arr[i:j] significa, claro, elementos de i-ésimo até (j-1)-ésimo. arr[:] significa todos os elementos do array.

» # criando um array de teste
» arr = (np.random.random(10)*10).round(1)
» arr
↳ array([3. , 1.5, 5.3, 1.3, 8.8, 9.8, 4.7, 0.1, 0.1, 0.6])
» fatia = arr[1:5]
» fatia
↳ array([1.5, 5.3, 1.3, 8.8])

# vamos alterar trecho da fatia
» fatia[1:3] = 0
» fatia
↳ array([1.5, 0. , 0. , 8.8])

» # o array original foi alterado
» arr
↳ array([3. , 1.5, 0. , 0. , 8.8, 9.8, 4.7, 0.1, 0.1, 0.6])

» # vamos alterar a fatia inteira
» fatia[:] = -10
» arr
↳ array([  3. , -10. , -10. , -10. , -10. ,   9.8,   4.7,   0.1,   0.1,  0.6])

» # valores específicos podem ser fornecidos (sem broadcast)
» fatia[:] = [-1,-2,-3,-4]
» arr
↳ array([ 3. , -1. , -2. , -3. , -4. ,  9.8,  4.7,  0.1,  0.1,  0.6])

Esse comportamento é útil quando se trabalha com array de dados muito grande e se deseja alterar apenas parte dele, lembrando que a biblioteca efetua suas operações mantendo os dados envolvidos na memória.

Em um array arr de 2 dimensões arr[m] é a m-ésima linha e arr[m,n] se refere ao elemento am,n, da m-ésima linha, n-ésima coluna. arr[m][n] é o mesmo am,n.

» # slices para dimensões mais altas
» arr = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
» arr
↳ array([[1, 2, 3],
         [4, 5, 6],
         [7, 8, 9]])

» # 2ª linha
» arr[1]
↳ array([4, 5, 6])

» arr[:1]
↳ array([[1, 2, 3]])

» # 1ª linha, 3º elemento
» arr[0,2]
↳ 3
» arr[0][2]
↳ 3

Uma cópia de um setor é uma referência para aquele setor, chamda de view ou visualização do segmento. Alterações feitas à view se refletam no array original, a menos que o método arr.copy() seja usado. Um array copiado dessa forma perde a referência com o array original e pode ser modificado independentemente.

» # slices em 3D:
» # criamos um array com shape (2,3,2)
» arr3D = np.arange(12).reshape(2,3,2)
» arr3D
↳ array([[[ 0,  1],
         [ 2,  3],
         [ 4,  5]],

        [[ 6,  7],
         [ 8,  9],
         [10, 11]]])

» # a 1ª matriz é
» arr3D[0]
↳ array([[0, 1],
        [2, 3],
        [4, 5]])
» # a 2ª linha da 1ª matriz é
» arr3D[0,1]
↳ array([2, 3])

» # seu 2º elemento
» arr3D[0,1,1] # o mesmo que arr3D[0,1][1]
↳ 1

» # copiamos um slice de 2 formas
» guardar = arr3D[0].copy()
» slice = arr3D[0]
» # ambos com os valores da  1ª matriz

» # alteramos toda a 1ª matriz
» arr3D[0] = 12
» # o array original foi alterado
» arr3D
↳ array([[[12, 12],
         [12, 12],
         [12, 12]],

        [[ 6,  7],
         [ 8,  9],
         [10, 11]]])
» # a cópia por referência foi alterada
» slice
↳ array([[12, 12],
         [12, 12],
         [12, 12]])

» # mas a cópia por valor não foi alterada
» guardar
↳ array([[0, 1],
         [2, 3],
         [4, 5]])

» # podemos restaurar o array aos seus valores originais
» arr3D[0] = guardar
» arr3D
↳ array([[[ 0,  1],
          [ 2,  3],
          [ 4,  5]],

         [[ 6,  7],
          [ 8,  9],
          [10, 11]]])


A notação de slice, idêntica à usada em listas do python, funciona em arrays. Para um array unidimensional arr1d[i:f] retorna do i-ésimo elemento até j-ésimo elemento (exclusive). Para um array de 2 dimensões arr2d[i:f] retorna i-ésima linha até j-ésima linha (exclusive). Se i é omitido o início é usado, se j é omitido o final é usado. Portanto arr2d[:2] significa as duas primeiras linhas do array (linha 0 e linha 1).

Slices ou segmentos múltiplos podem ser usados. Por exemplo, arr2d[m:n, r:s] são as linhas de m até n-1, colunas de r até s-1.

» # array 1d  (um vetor)  
» arr = np.array([1.2, 2.3, 3.4, 4.5, 5.6])
» arr[2:4]
↳ array([3.4, 4.5])

» # array 2d  (uma matriz)  
» arr2d = np.array([[1,2,3],[4,5,6],[7,8,9]])
» arr2d
↳ array([[1, 2, 3],
         [4, 5, 6],
         [7, 8, 9]])

» arr2d[0:2]
↳ array([[1, 2, 3],
         [4, 5, 6]])

» arr2d[:2]
↳ array([[1, 2, 3],
         [4, 5, 6]])
       
» # slices múltiplos
» arr2d[:2, 1:]
↳ array([[2, 3],
         [5, 6]])

arr2d[:2, 1:] são as linhas 0 e 1, colunas de 1 em diante.

O indexador pode ser um array booleano, e os valores serão filtrados apenas se o índice for True. Essa operação pode ser muito útil para a implementação de filtragens de tipos diversos.

» # o indexador pode ser booleano
» ar1 = np.array([-1,0, 3, 7, -2, 10])
» ar2 = np.array([-2,10, 2, 9, -1, 8])

» # extrair apenas 0º e 2º elemento
» ar1[[True, False, True, False, False, False]]
↳ array([-1,  3])

» # elementos de ar1 maiores que 2
» ar1[ar1>2]
↳ array([ 3,  7, 10])

» # elementos de ar1 maiores que os de ar2
» ar1[ar1>ar2]
↳ array([-1,  3, 10])

Suponha que temos dados sobre alguns países, armazenados em forma tabular. O primeiro array contém os nomes dos países, como se fosse um cabeçalho da tabela seguinte. O segundo array contém dados numéricos de qualquer natureza, com 5 linhas e 4 colunas, cada coluna contendo dados relativos ao país no cabeçalho. Para esse exemplo geramos esses dados aleatoriamente, apenas para exibir a operação.

» arrPais = np.array(['Brasil', 'Chile', 'Brasil', 'Peru'])
» arrDados = np.random.randn(5,4).round(2)  # 5 linhas e 4 colunas

» print(arrPais, '\n', arrDados)
↳ ['Brasil' 'Chile' 'Brasil' 'Peru'] 
   [[-0.71  0.42 -0.52  0.63]
   [-1.12 -0.29  0.03  1.43]
   [ 0.99  0.45  1.08  0.53]
   [-0.78  0.18 -0.07  0.28]
   [-2.03  0.44  0.07  1.28]]

» # podemos exibir todas as linhas das colunas 0 e 2
» arrDados[:,[0,2]]
↳ array([[-0.71, -0.52],
         [-1.12,  0.03],
         [ 0.99,  1.08],
         [-0.78, -0.07],
         [-2.03,  0.07]])

» # alternativamente, as colunas que correspondem ao Brasil
» arrBrasil = arrDados[:, arrPais=='Brasil']
» arrBrasil
↳ array([[-0.71, -0.52],
         [-1.12,  0.03],
         [ 0.99,  1.08],
         [-0.78, -0.07],
         [-2.03,  0.07]])

» # na tabela do Brasil, suponha que valores negativos sejam insignificantes
» # podemos eliminá-los com uma filtragem
» arrBrasil[arrBrasil < 0] = 0
» arrBrasil
↳ array([[0.  , 0.  ],
         [0.  , 0.03],
         [0.99, 1.08],
         [0.  , 0.  ],
         [0.  , 0.07]])

» # a soma desses dados é
» arrBrasil.sum()
↳ 2.17

» # para exibir os demais dados (não do Brasil)
» arrDados[:, arrPais!='Brasil'] # ou arrDados[:, ~(arrPais=='Brasil')]
↳ array([[ 0.42,  0.63],
         [-0.29,  1.43],
         [ 0.45,  0.53],
         [ 0.18,  0.28],
         [ 0.44,  1.28]])

Observe que arrPais!='Brasil' é a negação de arrPais=='Brasil'. O parênteses em ~(arrPais=='Brasil') é necessário pois a negação ~ tem precedência sobre o teste de igualdade. Sem o parênteses ~arrPais seria avaliado primeiro, o que resultaria em erro pois o array não é booleano.

As ferramentas do pandas facilitam operações como essas.

Operações matemáticas

Operações são realizadas elemento a elemento (que abreviaremos para “e/e” nesse texto). Operações usuais de um array por um escalar são são propagadas entre o escalar e cada elemento do array. Operações entre arrays são feitas e/e, ou seja, realizadas entre elementos na mesma posição. Os arrays devem ter as mesmas dimensões.

Operações de incremento, tais como array += 1 (que significa array = array + 1) ou array *= 2 são realizados inplace (alteram o próprio array). Diversas outras operações do NumPy (e do pandas) são realizadas inplace enquanto em várias delas existe o parâmetro inplace = True/False que permite a decisão de qual caso se deseja naquele momento.

Operações podem ser feitas entre arrays retornados por funções (que retornam arrays).

» # operações com escalares    
» ar1 =  np.linspace(10, 20, 5)
» ar1
↳ [10.  12.5 15.  17.5 20. ]

» ar1 + 10
↳ [20.  22.5 25.  27.5 30. ]

» ar1 * 10
↳ [100. 125. 150. 175. 200.]

» ar1 / 10
↳ [1.   1.25 1.5  1.75 2.  ]

» ar1**2
↳ [100.   156.25 225.   306.25 400.  ]

» 1/arr1
↳ array([0.1       , 0.08      , 0.06666667, 0.05714286, 0.05      ])

» # operações entre arrays
» ar2 =  np.linspace(10, 50, 5)
» print(ar1)
» print(ar2)
↳ [10.  12.5 15.  17.5 20. ]
↳ [10. 20. 30. 40. 50.]

» ar1 + ar2
↳ array([20. , 32.5, 45. , 57.5, 70. ])

» ar1 * ar2
↳ array([ 100.,  250.,  450.,  700., 1000.])

» ar2 / ar1
↳ array([1.        , 1.6       , 2.        , 2.28571429, 2.5       ])

» # operações com arrays (2 × 3)
» ar3 = np.linspace(0,5, 6).reshape(2,3)
» ar4 = np.linspace(0,10, 6).reshape(2,3)

» print(ar3)
↳ [[0. 1. 2.]
   [3. 4. 5.]]

» print(ar4)
↳ [[ 0.  2.  4.]
   [ 6.  8. 10.]]

» ar3 + ar4
↳ array([[ 0.,  3.,  6.],
         [ 9., 12., 15.]])

» ar3 - ar4
↳ array([[ 0., -1., -2.],
         [-3., -4., -5.]])

» ar3 * ar4
↳ array([[ 0.,  2.,  8.],
         [18., 32., 50.]])
       
» # operações de incremento são realizados inplace
» ar3 +=1
» ar3
↳ array([[1., 2., 3.],
         [4., 5., 6.]])

» # seno e cosseno em np retorna um array e/e
» ar1 * np.sin(ar2)
↳ array([ -5.44021111,  11.41181563, -14.82047436,  13.03948031,
          -5.24749707])

» ar3 * np.cos(ar4)
↳ array([[ 0.        , -0.41614684, -1.30728724],
         [ 2.88051086, -0.58200014, -4.19535765]])

Comparações entre arrays resultam em arrays booleanos.

» ar5 = np.linspace(0,5, 6).reshape(2,3)
» ar6 = np.random.random(6).reshape(2,3).round(2)*5

» ar5
↳ array([[0., 1., 2.],
         [3., 4., 5.]])
» ar6
↳ array([[3.  , 2.45, 1.2 ],
         [3.1 , 1.55, 1.75]])

» arrMaior= ar5 > ar6
» arrMaior
↳ array([[False, False,  True],
         [False,  True,  True]])

Broadcasting: A operações feitas acima, entre um array e um escalar, transformam o escalar em um array de dimensões apropriadas (de mesmo shape) antes de sua realização. Essa operação se chama broadcasting:. Por exemplo:

» ar1 = np.array([0,1,2,3])
» ar2 = np.array([4,4,4,4])

» # a soma com um escalar
» ar1 + 4
↳ array([4, 5, 6, 7])

» # é  mesmo que
» ar1 + ar2
↳ array([4, 5, 6, 7])

» # os elementos são iguais
» ar1 + ar2 == ar1 + 4
↳ array([ True,  True,  True,  True])

# o mesmo ocorre com comparações
» ar1 ≥ 2
↳ array([False, False,  True,  True])

Uma forma de seleção diferente consiste em passar listas de valores como índices. O array retornado depende de como essas listas são passadas. Essa é técnica é chamada de fancy indexing (indexação sofisticada). Em qualquer dos casos abaixo os índices podem aparecer em qualquer ordem.

O slice array[[i,j,...]] contém as linhas array[i], array[j], etc. (O mesmo que array[:,[i,j,...]]).
O slice array[:,[i,j,...]] contém as colunas array[:,i], array[:,j], etc.
O slice array[[i,j,...]:[r, s,...]] é uma linha contendo os elementos array[i,r], array[j,s], etc.

» # fancy indexing (passando arrays como indices)
» # vamos construir um array 6 × 5 e atribuir seus valores um a um
» arr = np.empty((6,5))

» for linha in range(6):
»     for coluna in range(5):
»         arr[linha,coluna] = linha * 10 + coluna
        
» # o array obtido é (uma forma de identificar facilmente de que elemento se trata)
» arr
↳ array([[ 0.,  1.,  2.,  3.,  4.],
         [10., 11., 12., 13., 14.],
         [20., 21., 22., 23., 24.],
         [30., 31., 32., 33., 34.],
         [40., 41., 42., 43., 44.],
         [50., 51., 52., 53., 54.]])

» # podemos selecionar linhas (em qualquer ordem)
» arr[[3,5,1]]       # o mesmo que arr[[3,5,1],:]
↳ array([[30., 31., 32., 33., 34.],
         [50., 51., 52., 53., 54.],
         [10., 11., 12., 13., 14.]]) 

» # ou colunas (em qualquer ordem)
» arr[:,[3,1]]
↳ array([[ 3.,  1.],
         [13., 11.],
         [23., 21.],
         [33., 31.],
         [43., 41.],
         [53., 51.]])

» # fornecer duas listas (que devem ter o mesmo tamanho) tem efeito diferente,
» # retornando array de i dimensão com os índices dados nas listas
» arr[[0,3,4,1],[0,3,4,1]]
↳ array([ 0., 33., 44., 11.])

» arr[[0,3,4,1],[1,0,4,2]]
↳ array([ 1., 30., 44., 12.])

Funções universais

Funções universais, ou ufunc, são funções que agem e/e, sobre todos os elementos de um array, retornando outra array de mesmas dimensões. Essas operações são também chamadas de operações vetorializadas. Embora envolvam laços (loops ) esses são realizados internamente e de forma eficiente, de modo a agilizar os processos.

Uma tabela das funções universais é encontrada abaixo.

Método retorna
abs, fabs valor absoluto inteiros, floats, ou complexos
sqrt raiz quadrada (equivale a arr**0.5)
square elementos elevados ao quadrado (equivale a arr**2)
exp exponencial de cada elemento (ex)
log, log10, logaritmos naturais (de base e e base 10var>
log2, log1p logaritmos de base 2 e log(1 + x)
sign sinal: (1, 0, -1) (positivo, zero, negativo)
ceil teto, menor inteiro maior ou igual
floor piso, maior inteiro menor ou igual
rint arredonda para o inteiro mais próximo, preservando dtype
modf partes inteiras e fracionárias do array, em e arrays
isnan array booleano, se o valor é NaN (Not a Number)
isfinite array booleano, se cada valor é finito (non-inf, non-NaN)
isinf array booleano, se cada valor é infinito
cos, sin, tan funções trigonométricas
cosh, sinh, tanh funções trigonométricas hiperbólicas
arccos, arcsin, arctan arcos de funções trigonométricas
arccosh, arcsinh, arctanh arcos de funções trigonométricas hiperbólicas
logical_not array booleano, negação do array (equivalent to ~arr).

Exemplos de uso:

» # Funções universais
» arrBool = np.array([True, False, False, True])
» arrBool
↳ array([ True, False, False,  True])
» np.logical_not(arrBool)
↳ array([False,  True,  True, False])

» arr = np.linspace(0, 10, 6)
» arr -=5
» arr
↳ array([-5., -3., -1.,  1.,  3.,  5.])

» np.abs(arr)
↳ array([5., 3., 1., 1., 3., 5.])

» # não altera arr
» np.sign(arr)
↳ array([-1., -1., -1.,  1.,  1.,  1.])

» arr = arr/10 +4
» arr = arr.reshape(2,3)
» arr
↳ array([[3.5, 3.7, 3.9],
         [4.1, 4.3, 4.5]])

» np.modf(arr)
↳ (array([[0.5, 0.7, 0.9],
          [0.1, 0.3, 0.5]]),
↳  array([[3., 3., 3.],
          [4., 4., 4.]]))

Funções de Agregação


Funções de agregação são funções que realizam operações em todos os elementos do array, retornando um escalar(um número). Em sua maioria elas retornam cálculos estatíscos sobre os dados.

» ag = np.array([3.3, 12.5, 11.2, 5.7, 0.3])
» ag
↳ array([ 3.3, 12.5, 11.2,  5.7,  0.3])

» # outputs nos comentários
» ag.sum()       # 33.0
» ag.min()       # 0.3
» ag.max()       # 12.5
» ag.mean()      # 6.6
» ag.std()       # 4.6337889464238655
» ag.var()       # 21.472
» ag.argmin()    # 4
» ag.argmax()    # 1

» # o mesmo vale para arrays com outros shapes
» ag23 =  np.random.random(6).reshape(2,3) -.5
» ag23
↳ array([[ 0.10425075, -0.29335437, -0.36814244],
         [ 0.32986805,  0.17289794, -0.4568041 ]])

» ag23.mean()
↳ -0.08521402850598175

Diversas das operações podem ser feitas sobre o array inteiro ou sobre linhas ou colunas. Para isso podemos especificar axis = 0 para operações sobre elementos das colunas, axis = 1 para operações sobre elementos das linhas.

» arr = np.random.randn(4,5).round(2)
» arr
↳ array([[ 0.45, -0.11, -0.54, -0.97, -0.23],
         [ 1.65,  0.76, -0.39, -1.83,  0.02],
         [ 0.45, -1.22,  1.93,  1.92, -0.43],
         [-0.39,  2.01,  0.04,  0.67, -1.1 ]])

» # a soma de todos os elementos
» arr.sum()
↳ 2.689999999999999

» # soma sobre elementos de cada coluna
» arr.sum(axis=0)       # ou arr.sum(0)
↳ array([ 2.17,  0.37,  0.17, -2.49, -3.13])

» # soma sobre elementos de cada linha
» arr.sum(axis=1)       # ou arr.sum(1)
↳ array([-0.42, -1.67,  0.6 , -1.42])

» # produtos dos elementos das linhas
» arr.prod(axis=1).round(2)
↳ array([0.01, 0.02, 0.87, 0.02])

» # produtos dos elementos das colunas
» arr.prod(axis=0).round(2)
↳ array([-0.13,  0.21,  0.02,  2.28, -0.  ])


Funções básicas de agregação:

Função retorna
np.all booleano, True se todos os elementos no array são não nulos
np.any booleano, True se algum dos elementos no array é não nulo
np.sum soma dos elementos do array ou sobre eixo especificado †.
np.mean Média aritmética; arrays de comprimento nulo têm média = NaN
np.std, np.var variância e desvio padrão
np.max, np.min valor máximo e mínimo no array
np.argmax, np.argmin índices do valor máximo e mínimo no array
np.cumsum soma cumulativa dos elementos, começando em 0
np.cumprod produto cumulativo dos elementos, começando em 1

(†) Para arrays de comprimento nulo tem soma np.sum = NaN.

🔺Início do artigo

Bibliografia

  • Blair, Steve: Python Data Science, edição do autor, 2019.
  • Harrison, Matt: Learning Pandas, Python Tools for Data Munging, Data Analysis, and Visualization,
    Treading on Python Series, Prentiss, 2016.
  • Johansson, Robert: Numerical Python, Scientific Computing and Data Science Applications with Numpy, SciPy and Matplotlib, 2nd., Chiba, Japan, 2019.
  • 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.
  • Nelli, Fabio: Python Data Analytics With Pandas, NumPy, and Matplotlib, 2nd., Springer, New York, 2018.
  • Site AI Ensina: Entendendo a biblioteca NumPy, acessado em julho de 2021.
  • Site GeeksforGeeks: Python NumPy, acessado em julho de 2021.
  • Site W3 Schools: NumPy Tutorial, acessado em julho de 2021.
  • NumPy, docs.
  • NumPy, Learn.

Nesse site:

Análise Fatorial Usando Python

🔻Final do artigo

Para dar um exemplo de análise de fatores usaremos o módulo factor_analyser do Python. Os dados usados são originados do Synthetic Aperture Personality Assessment (SAPA) que contém 25 questões de auto-avaliação pessoais disponíveis na web na página de Vincent Arel-Bundock, no Github.

A documentação do Factor Analyser pode ser lida nessa página.

SAPA e BFI

SAPA, Synthetic Aperture Personality Assessment, é um método usado para avaliar diferenças de personalidade individuais, muito utilizado para pesquisas online. O sujeito testado recebe um subconjunto aleatório dos itens em estudo com o objetivo de reunir grande volume de dados suficientes para a montagem de grandes matrizes de covariância (de relacionamento entre os dados verificados). O teste online foi desenvolvido por William Revelle e é mantido pela Northwestern University, Ilinois, EUA.

O teste consiste dos seguintes ítens:
A1: Sou indiferente aos sentimentos das outras pessoas.
A2: Sempre pergunto sobre o bem-estar dos outros.
A3: Sei como confortar os outros.
A4: Adoro crianças.
A5: Faço as pessoas se sentirem à vontade.C1: Sou exigente no meu trabalho.
C2: Continuo minhas tarefas até que tudo esteja perfeito.
C3: Faço as coisas de acordo com um plano.
C4: Deixo tarefas semi-acabadas.
C5: Desperdiço meu tempo.E1: Não falo muito.
E2: Tenho dificuldades para abordar outras pessoas.
E3: Sei como cativar as pessoas.
E4: Faço amigos facilmente.
E5: Assumo o controle das situações.

N1: Fico com raiva facilmente.
N2: Irrito-me facilmente.
N3: Tenho alterações de humor frequentes.
N4: Muitas vezes me sinto triste.
N5: Entro em pânico facilmente.

O1: Sempre tenho muitas ideias.
O2: Evito leituras complexas.
O3: Procuro levar as conversas para um nível elevado.
O4: Passo algum tempo refletindo sobre as coisas.
O5: Nunca me detenho a avaliar um assunto profundamente.

Escala:
A escala usada para respostas usada foi:
1. Totalmente falso
2. Moderadamente falso
3. Um pouco falso
4. Um pouco correto
5. Moderadamente correto
6. Totalmente corretoIndicadores Demográficos:
estão codificados da seguinte forma:
Gênero:
1. Masculino,
2. Feminino.
Idade: a idade (em anos).
Educação:
1. Nível médio incompleto,
2. Nível médio completo,
3. Nível superior incompleto,
4. Nível superior completo,
5. Pós-graduação.Fatores esperados:
Os itens estão organizados por fatores esperados (a serem verificados pela análise):
Agreeableness, (Amabilidade),
Conscientiousness, (Conscienciosidade),
Extroversion, (Extroversão),
Neuroticism, (Neuroticismo) e
Opennness, (Abertura).

big 5
Diversos itens contribuem em grau maior ou menor para os fatores ou variáveis latentes.

O arquivo de respostas disponível foi baixado como o nome bfi.csv e salvo na pasta do projeto, subpasta ./dbs. Esse arquivo contém dados no formato *.csv (valores separados por vírgula) relativos a 2800 sujeitos com 3 campos adicionais de dados demográficos: sexo, educação e idade.

Jupyter Notebook e convenções usadas

Você pode ler sobre Jupyter Notebook nessa página.

Jupyter Notebook é uma aplicação web e opensource que permite sessões colaborativas e documentos compartilhados contendo código que pode ser executado dentro da página, equações bem formatadas, visualização gráfica e texto narrativo que podem ser postas sob forma de apresentações ou usadas para desenvolvimento. Seu uso inclui tratamento, transformação e visualização de dados, simulações numéricas, modelagem estatística, machine learning entre outras aplicações.

O projeto será rodado em uma sessão do Jupyter Notebook. Nessa página usamos as seguintes convenções: células de código do Jupyter Notebook aparecem dentro de caixas como a exibida abaixo. Nos notebooks (como no Python) linhas iniciadas pelo sinal “#” são comentários. Apenas nessas páginas outputs simples e compactos podem aparecer como um comentário após o comando como mostrado abaixo (diferente do que ocorre nos notebooks). Outpus mais complexos aparecem em caixas separadas.

# Exemplo de exibição das células do Jupyter Notebook.
print('output simples')    # Esse comando imprime 'output simples'

# Outpus mais complexos aparecem em caixas separadas:
print('Outputs do Jupyter Notebook, gráficos e dataframes exibidos aparecem como nesse quadro...')
Outputs do Jupyter Notebook, gráficos e dataframes exibidos aparecem como nesse quadro…

Módulo Factor Analyser

Veja mais sobre a documentação do Factor Analyser nessa página.

Factor Analyser é um módulo desenvolvido em Python por Jeremy Biggs e Nitin Madnani, publicado em 2017 para realizar análise fatorial exploratória e confirmatória (AFE, AFC). As classes do pacote são compatíveis com a biblioteca scikit-learn. Partes do código são portadas da biblioteca psych do R.

Tratamento dos dados do bsi por meio do factor_analyser

Instalamos o módulo factor_analyzer dentro do Jupyter Notebook. Em seguida importamos as bibliotecas necessárias: além do próprio factor_analyzer usamos o pandas e numpy para as manipelações de dados e matplotlib para as visualizações.

# Instalação do factor_analyzer:
conda install -c ets factor_analyzer

# Importando bibliotecas (libraries) necessárias
import pandas as pd
import numpy as np
from factor_analyzer import FactorAnalyzer
import matplotlib.pyplot as plt

# A leitura do arquivo de dados para dentro de um dataframe (do pandas)
df = pd.read_csv('./dbs/bfi.csv')

# Renomear a primeira coluna (que está sem nome) para 'id'
df.rename(columns = {'Unnamed: 0':'id'}, inplace = True) 
print('A tabela importada contém %d linhas, %d colunas' % df.shape)
print('contendo as seguintes colunas:\n', df.columns)

# Para visualizar a tabela importada:
df.head()
A tabela importada contém 2800 linhas, 29 colunas
contendo as seguintes colunas:
Index([‘id’, ‘A1’, ‘A2’, ‘A3’, ‘A4’, ‘A5’, ‘C1’, ‘C2’, ‘C3’, ‘C4’, ‘C5’, ‘E1’,
‘E2’, ‘E3’, ‘E4’, ‘E5’, ‘N1’, ‘N2’, ‘N3’, ‘N4’, ‘N5’, ‘O1’, ‘O2’, ‘O3’,
‘O4’, ‘O5’, ‘gender’, ‘education’, ‘age’],
dtype=’object’)
id A1 A2 A3 A4 A5 C1 C2 C3 C4 N4 N5 O1 O2 O3 O4 O5 gender education age
0 61617 2.0 4.0 3.0 4.0 4.0 2.0 3.0 3.0 4.0 2.0 3.0 3.0 6 3.0 4.0 3.0 1 NaN 16
1 61618 2.0 4.0 5.0 2.0 5.0 5.0 4.0 4.0 3.0 5.0 5.0 4.0 2 4.0 3.0 3.0 2 NaN 18
2 61620 5.0 4.0 5.0 4.0 4.0 4.0 5.0 4.0 2.0 2.0 3.0 4.0 2 5.0 5.0 2.0 2 NaN 17
3 61621 4.0 4.0 6.0 5.0 5.0 4.0 4.0 3.0 5.0 4.0 1.0 3.0 3 4.0 3.0 5.0 2 NaN 17
4 61622 2.0 3.0 3.0 4.0 5.0 4.0 4.0 5.0 3.0 4.0 3.0 3.0 3 4.0 3.0 3.0 1 NaN 17

2800 rows × 29 columns

Como em qualquer outro uso de dados, principalmente quando importados de fontes externas, fazemos uma verificação de estrutura e completeza ou a existência de valores ausentes (NaN). Os dados demográficos são armazendos em outro dataframe enquanto a avaliação das questões em si são deixadas no dataframe df depois de eliminados os campos relativos a dados demográficos.

# Tabela dfDemografico armazena id, sexo, educação e idade
dfDemografico = df[['id', 'gender', 'education', 'age']]

# Colunas desnecessárias são eliminadas de df
df.drop(['id', 'gender', 'education', 'age'],axis=1,inplace=True)

# Possíveis dados ausentes são eliminados
df.dropna(inplace=True)

# Para verificar as colunas de df
df.head(2)
A1 A2 A3 A4 A5 C1 C2 C3 C4 C5 N1 N2 N3 N4 N5 O1 O2 O3 O4 O5
0 2.0 4.0 3.0 4.0 4.0 2.0 3.0 3.0 4.0 4.0 3.0 4.0 2.0 2.0 3.0 3.0 6 3.0 4.0 3.0
1 2.0 4.0 5.0 2.0 5.0 5.0 4.0 4.0 3.0 4.0 3.0 3.0 3.0 5.0 5.0 4.0 2 4.0 3.0 3.0

2 rows × 25 columns

# Uma visão geral dos dados no dataframe df pode ser vista:
df.info()
<class ‘pandas.core.frame.DataFrame’>
Int64Index: 2436 entries, 0 to 2799
Data columns (total 25 columns):
# Column Non-Null Count Dtype
—————————————————————————————
0 A1 2436 non-null float64
1 A2 2436 non-null float64
2 A3 2436 non-null float64
3 A4 2436 non-null float64
4 A5 2436 non-null float64
Grupos C, E, N omitidos
20 O1 2436 non-null float64
21 O2 2436 non-null int64
22 O3 2436 non-null float64
23 O4 2436 non-null float64
24 O5 2436 non-null float64
dtypes: float64(24), int64(1)
memory usage: 494.8 KB

Observamos que apenas o campo O2 tem tipo de variável int64. Apenas para ter todos os campos do mesmo tipo fazemos a conversão para float64.

# Converter o campo O2 em float
df['O2'] = df['O2'].astype(np.float64)

type(df['O2'][0])    # Agora o campo é do tipo numpy.float64

Considerando que em operações que faremos podemos perder o nome das colunas, vamos armazenar esses nomes em uma varável (que nesse caso é uma série).

itens=df.columns
print(itens)
Index([‘A1’, ‘A2’, ‘A3’, ‘A4’, ‘A5’, ‘C1’, ‘C2’, ‘C3’, ‘C4’, ‘C5’, ‘E1’, ‘E2’,
‘E3’, ‘E4’, ‘E5’, ‘N1’, ‘N2’, ‘N3’, ‘N4’, ‘N5’, ‘O1’, ‘O2’, ‘O3’, ‘O4’,
‘O5′],
dtype=’object’)

Matriz de Correlação

Estamos prontos para encontrar a matriz de correlação entre as variáveis. Cada célula dessa matriz mostra a correlação entre duas variáveis, listadas como labels das linhas e colunas. Por isso ele tem os valores da diagonal iguais a 1 (que é a correlação da variável consigo mesma). A matriz de correlação fornece uma visão geral de interrelacionamento dos dados e é usada como input para análises mais advançadas.

A correlação .corr é um método de dataframes do pandas. Por default o método corr usa os coeficientes de Pearson mas também pode usar os coeficientes Tau de Kendall ou coefficientes de Spearman.

# A matriz de correlação entre todas as respostas na tabela
corrMatriz = df.corr()

# Para ver apenas as correlações entre variáveis do grupo A
corrMatriz[["A1", "A2","A3", "A4","A5"]].head(5)
A1 A2 A3 A4 A5
A1 1.000000 -0.350905 -0.273636 -0.156754 -0.192698
A2 -0.350905 1.000000 0.503041 0.350856 0.397400
A3 -0.273636 0.503041 1.000000 0.384918 0.515679
A4 -0.156754 0.350856 0.384918 1.000000 0.325644
A5 -0.192698 0.397400 0.515679 0.325644 1.000000

Em seguida fazemos o gráfico de calor (heatmap) da matriz de correlação. Usamos a biblioteca seaborn para isso. Um heatmap associa uma cor a cada valor na matriz de correção. Tons mais escuros de azul (nesse caso) são valores mais perto de 1, tons mais claros são valores mais perto de -1.

import seaborn as sns
plt.figure(figsize=(12,12))
sns.set(font_scale=1)
sns.heatmap(corrMatriz, linewidths=.1, linecolor='#ffffff',
            cmap='YlGnBu', xticklabels=1, yticklabels=1)


A mera análise do gráfico de calor permite que algumas características da pesquisa sejam visualmente reconhecidas. Duas variáveis diferentes com índice de correlação muito alto podem ser, na verdade, a mesma variável escrita de forma diversa. Nesse caso o pesquisador pode preferir retirar uma delas. Uma ou mais variáveis com nível de correlação muito baixo com todas as demais podem indicar a medida de elementos isolados e fora de contexto com o modelo explorado. Nesse caso vemos agrupamentos claros entre as variáveis A2, A3, A4, A5 e todas as do grupo N, só para citar alguns exemplos.

Análise Fatorial

Análise Fatorial Exploratória, AFE

Podemos agora dar início ao uso específico da Análise Fatorial, começando pela Análise Fatorial Exploratória, AFE, usando o módulo factor_analyzer. O primeiro passo para isso é a avaliação da fatorabilidade dos dados. Isso significa que os dados coletados podem ser agrupados em fatores, que são as nossas variáveis ocultas com o poder de sintetizar e melhor descrever o objeto estudado. Para isso o módulo factor_analyzer oferece dois testes: o Teste de Bartlett e o Teste de Kaiser-Meyer-Olkin.

Teste da esfericidade de Bartlett

O Teste da esfericidade de Bartlett verifica se as variáveis estão correlacionadas entre si, comparando a matriz de correlação com a matriz identidade (que representaria variáveis completamente não correlacionadas).

# Importa o módulo que realiza o teste de Bartlett
from factor_analyzer.factor_analyzer import calculate_bartlett_sphericity
chi_square_value, p_value = calculate_bartlett_sphericity(df)
print('Teste da Esfericidade de Bartlett: chi² = %d,  p_value = %d' % (chi_square_value, p_value))
Teste da Esfericidade de Bartlett: chi2 = 18170.966350869272 p_value = 0.

No nosso caso o teste de Bartlett resulta em p-value = 0, o que indica que os dados podem ser fatorados e a matriz de correlação observada não é a identidade.

Teste de Kaiser-Meyer-Olkin

O Teste de Kaiser-Meyer-Olkin (KMO) fornece uma técnica de avaliação se os dados colhidos são apropriados para esta análise fatorial. Ele realiza um teste para cada variável observada e para o conjunto completo de variáveis. O resultado representa o grau em que cada variável observada pode ser predita, sem erros, pelas demais variáveis no conjunto de dados. KMO é uma estimativa da proporção de variância entre todas as variáveis. Os valores de KMO podem estar entre 0 e 1 e valores abaixo de 0.6 são consideredos inadequados.

# Importa calculate_kmo
from factor_analyzer.factor_analyzer import calculate_kmo
kmo_all,kmo_model = calculate_kmo(df)

print('Valores de kmo_all =\n', kmo_all, '\n')
print('KMO =', kmo_model)
Valores de kmo_all =
[0.75391928 0.8363196 0.87010963 0.87795367 0.90348747 0.84325413
0.79568263 0.85186857 0.82647206 0.86401687 0.83801873 0.88380544
0.89697008 0.87731273 0.89332158 0.77933902 0.78025018 0.86229919
0.88518467 0.86014155 0.85858672 0.78019798 0.84434957 0.77003158
0.76144469]KMO = 0.848539722194922

Todos os valores de kmo_all são superiores a 0,7 e o KMO geral é KMO = 0.8485 o que são considerados valores muito favoráveis para a análise dos fatores.

Prosseguimos criando uma instância do objeto factor_analysis, tentativamente com 5 fatores

# Criamos objeto factor_analysis, sem rotação e usando 5 fatores (tentativamente)
fa = FactorAnalyzer(5, rotation=None)

# Aplicamos o método fit (ajuste) desse objeto no dataframe
fa.fit(df)

# Depois desse ajuste podemos coletar os autovetores e autovalores
ev, v = fa.get_eigenvalues()
print('São ' + str(len(ev)) + ' autovalores:\n', ev)
São 25 eigenvalues:
[5.13431118 2.75188667 2.14270195 1.85232761 1.54816285 1.07358247
0.83953893 0.79920618 0.71898919 0.68808879 0.67637336 0.65179984
0.62325295 0.59656284 0.56309083 0.54330533 0.51451752 0.49450315
0.48263952 0.448921 0.42336611 0.40067145 0.38780448 0.38185679
0.26253902]

Usando os autovalores calculados traçamos um Screeplot que é um gráfico que lista os autovalores em ordem decrescente, usado para determinar o número de fatores a serem retidos em uma análise fatorial exploratória. O teste, introduzido por R.B. Cattell em 1966, sugere manter tantos fatores quantos forem as autovalores anteriores a uma “dobra mais acentuada” ou “cotovelo” no gráfico. Também é sugerido manter o mesmo número de fatores quantos autovalores existirem maiores que 1.

from bokeh.plotting import figure, output_notebook, show
output_notebook()

eixoX = range(1, len(ev)+1)   # de 1 0 26
eixoY = ev
p = figure(title="Scree Plot", x_axis_label='n-ésimo autovalor',y_axis_label='autovalor',
           x_range=[0,25], y_range=(0, 6), plot_width=600, plot_height=400,
           background_fill_color="#c9b2dd")

p.line(eixoX, eixoY, line_width=1, color = 'black')
p.circle(eixoX, eixoY, size=8, fill_color='red', color="black")

show(p)

Algumas críticas são dirigidas ao teste feito dessa forma pois ele insere uma decisão pouco objetiva. Mais de um cotovelo podem aparecer no gráfico. De qualquer forma, como veremos no presente caso, o bom senso e a análise posterior dos agrupamentos de fatores podem sugerir uma alteração nesse número.

Cargas Fatoriais (factor loadings)

A carga fatorial é o coeficiente de correlação entre a variável e o fator. Ela mostra a variância explicada pela variável naquele fator em particular.

Prosseguimos criando um objeto FactorAnalyzer com 6 fatores (tentativamente) e usando o método de rotação varimax.

# 6 fatores
fa = FactorAnalyzer( 6, rotation="varimax")

# o objeto tem o método fit para análise do dataframe
fa.fit(df)

# Desse extraimos as cargas fatoriais (factor loadings)
# Observe que fa.loadings_ é um numpy.array com shape (25,6). Usamos o método
# do pandas pd.DataFrame.from_records para convertê-lo em um dataframe
factorLoadings = pd.DataFrame.from_records(fa.loadings_)

# Para ver a dataframe gerado:
factorLoadings.head(4)
0 1 2 3 4 5
0 0.095220 0.040783 0.048734 -0.530987 -0.113057 0.161216
1 0.033131 0.235538 0.133714 0.661141 0.063734 -0.006244
2 -0.009621 0.343008 0.121353 0.605933 0.033990 0.160106
3 -0.081518 0.219717 0.235140 0.404594 -0.125338 0.086356

Vemos que os nomes dos itens, de A1 até O5, foram perdidos no cálculo. Vamos renomear tanto esses itens quanto os nomes das colunas (que são os fatores) para ter uma visualização mais clara do que obtivemos até aqui:

# Substitue as linhas pelo nomes dos itens
factorLoadings.index=itens

# Renomeia as colunas
factorLoadings.rename(columns = {0:'Fator 1',
                                 1:'Fator 2',
                                 2:'Fator 3',
                                 3:'Fator 4',
                                 4:'Fator 5',
                                 5:'Fator 6'}, inplace = True)

# Exibe o resultado
factorLoadings
Fator 1 Fator 2 Fator 3 Fator 4 Fator 5 Fator 6
A1 0.095220 0.040783 0.048734 -0.530987 -0.113057 0.161216
A2 0.033131 0.235538 0.133714 0.661141 0.063734 -0.006244
A3 -0.009621 0.343008 0.121353 0.605933 0.033990 0.160106
A4 -0.081518 0.219717 0.235140 0.404594 -0.125338 0.086356
A5 -0.149616 0.414458 0.106382 0.469698 0.030977 0.236519
C1 -0.004358 0.077248 0.554582 0.007511 0.190124 0.095035
C2 0.068330 0.038370 0.674545 0.057055 0.087593 0.152775
C3 -0.039994 0.031867 0.551164 0.101282 -0.011338 0.008996
C4 0.216283 -0.066241 -0.638475 -0.102617 -0.143846 0.318359
C5 0.284187 -0.180812 -0.544838 -0.059955 0.025837 0.132423
E1 0.022280 -0.590451 0.053915 -0.130851 -0.071205 0.156583
E2 0.233624 -0.684578 -0.088497 -0.116716 -0.045561 0.115065
E3 -0.000895 0.556774 0.103390 0.179396 0.241180 0.267291
E4 -0.136788 0.658395 0.113798 0.241143 -0.107808 0.158513
E5 0.034490 0.507535 0.309813 0.078804 0.200821 0.008747
N1 0.805806 0.068011 -0.051264 -0.174849 -0.074977 -0.096266
N2 0.789832 0.022958 -0.037477 -0.141134 0.006726 -0.139823
N3 0.725081 -0.065687 -0.059039 -0.019184 -0.010664 0.062495
N4 0.578319 -0.345072 -0.162174 0.000403 0.062916 0.147551
N5 0.523097 -0.161675 -0.025305 0.090125 -0.161892 0.120049
O1 -0.020004 0.225339 0.133201 0.005178 0.479477 0.218690
O2 0.156230 -0.001982 -0.086047 0.043989 -0.496640 0.134693
O3 0.011851 0.325954 0.093880 0.076642 0.566128 0.210777
O4 0.207281 -0.177746 -0.005671 0.133656 0.349227 0.178068
O5 0.063234 -0.014221 -0.047059 -0.057561 -0.576743 0.135936

Vamos montar mais um heatmap com essa tabela.

# A bibioteca seaborn já foi importada como sns
plt.figure(figsize=(8,6))
sns.set(font_scale=.9)
sns.heatmap(factorLoadings,  linewidths=1, linecolor='#ffffff', cmap="YlGnBu", xticklabels=1, yticklabels=1)

Lembrando que as cores mais escuras indicam correlação direta e as mais claras correlação inversa, percebemos que existem cargas mais fortes entre os itens N1, N2, N3, N4 e N5 com o fator 1, E1 até E5 no fator 2, etc. Nenhum dos itens, no entanto, tem carga relevante no sexto fator. Isso indica que podemos refazer o cálculo de cargas fatoriais com apenas 5 fatores.

# Refazendo o cáculo com 5 fatores apenas
# Apaga a variável fa
del fa

fa = FactorAnalyzer( 5, rotation="varimax")
fa.fit(df)
factorLoadings = pd.DataFrame.from_records(fa.loadings_)

# Renomeia itens
factorLoadings.index=itens

# Renomeia as colunas (fatores)
factorLoadings.rename(columns = {0:'Fator 1',
                                 1:'Fator 2',
                                 2:'Fator 3',
                                 3:'Fator 4',
                                 4:'Fator 5'}, inplace = True)

# Exibe o resultado
factorLoadings
Fator 1 Fator 2 Fator 3 Fator 4 Fator 5
A1 0.111126 0.040465 0.022798 -0.428166 -0.077931
A2 0.029588 0.213716 0.139037 0.626946 0.062139
A3 0.009357 0.317848 0.109331 0.650743 0.056196
A4 -0.066476 0.204566 0.230584 0.435624 -0.112700
A5 -0.122113 0.393034 0.087869 0.537087 0.066708
C1 0.010416 0.070184 0.545824 0.038878 0.209584
C2 0.089574 0.033270 0.648731 0.102782 0.115434
C3 -0.030855 0.023907 0.557036 0.111578 -0.005183
C4 0.240410 -0.064984 -0.633806 -0.037498 -0.107535
C5 0.290318 -0.176395 -0.562467 -0.047525 0.036822
E1 0.042819 -0.574835 0.033144 -0.104813 -0.058795
E2 0.244743 -0.678731 -0.102483 -0.112517 -0.042010
E3 0.024180 0.536816 0.083010 0.257906 0.280877
E4 -0.115614 0.646833 0.102023 0.306101 -0.073422
E5 0.036145 0.504069 0.312899 0.090354 0.213739
N1 0.786807 0.078923 -0.045997 -0.216363 -0.084704
N2 0.754109 0.027301 -0.030568 -0.193744 -0.010304
N3 0.731721 -0.061430 -0.067084 -0.027712 -0.004217
N4 0.590602 -0.345388 -0.178902 0.005886 0.075225
N5 0.537858 -0.161291 -0.037309 0.100931 -0.149769
O1 -0.002224 0.213005 0.115080 0.061550 0.504907
O2 0.175788 0.004560 -0.099729 0.081809 -0.468925
O3 0.026736 0.310956 0.076873 0.126889 0.596007
O4 0.220582 -0.191196 -0.021906 0.155475 0.369012
O5 0.085401 -0.005347 -0.062730 -0.010384 -0.533778

Construimos o heatmap com essa tabela de 5 fatores.

plt.figure(figsize=(8,6))
sns.set(font_scale=.9)
sns.heatmap(factorLoadings,  linewidths=1, linecolor='#ffffff', cmap="YlGnBu", xticklabels=1, yticklabels=1)
Os retângulos de borda
preta indicando grupos
de correlação foram
acrescentados manualmente.

Pelo heatmap percebemos que o grupo de itens N está associado ao fator 1 (Neuroticismo), E ao fator 2 (Extroversão), C ao fator 3 (Conscienciosidade), A ao fator 4 (Amabilidade) e O ao fator 5 (Abertura), previamente identificados. É claro que essa identificação do fator foi realizada pelos pesquisadores no caso dessa pesquisa em particular. Para uma pesquisa nova seria necessário um estudo para o entendimento da natureza de cada fator.

Observe ainda que correlação negativa é correlação. É o que ocorre, por exemplo entre os itens A1 (Sou indiferente aos sentimentos das outras pessoas) e A2 (Sempre pergunto sobre o bem-estar dos outros). A correlação entre eles pode ser obtida da matriz de correlação, corrMatriz['A1']['A2'] = -0.35091.

Observamos que existem as seguintes correspondências entre os grupos de questões e os fatores propostos:

Fator Grupo Descrição
1 N Neuroticismo
2 E Extroversão
3 C Conscienciosidade
4 A Amabilidade
5 O Abertura

Vamos, portanto, renomear as colunas de nossa matriz de cargas fatoriais para refletir esse entendimento:

# Renomeia as colunas (fatores)
factorLoadings.rename(columns = {'Fator 1':'Neuroticismo',
                                 'Fator 2':'Extroversão',
                                 'Fator 3':'Conscienciosidade',
                                 'Fator 4':'Amabilidade',
                                 'Fator 5':'Abertura'}, inplace = True)

# Exibe o resultado (só duas linhas)
factorLoadings.head(2)	
Neuroticismo Extroversão Conscienciosidade Amabilidade Abertura
A1 0.111126 0.040465 0.022798 -0.428166 -0.077931
A2 0.029588 0.213716 0.139037 0.626946 0.062139

Comunalidades

Comunalidades são a soma das cargas fatoriais ao quadrado de cada variável medida. Denotando por \(l_{ij}\) os elementos da matriz da cargas fatoriais a comunalidade da i-ésima variável é \(h_i^2\) dado por
$$
h_i^2 =\Sigma_{j=1}^n l_{ij}^2
$$
Por exemplo, a comunalidade relativa à questão A1 é a soma dos elementos (ao quadrado) da primeira linha da matriz de cargas fatoriais acima:

(0.111126)**2 + (0.040465)**2 + (0.022798)**2 + (-0.428166)**2 +(-0.077931)**2
0.203905517222

As comunalidades relativas a todas as variáveis podem ser obtidas com get_communalities() que retorna um numpy.ndarray com dimensões (25,):

fa.get_communalities()
array([0.20390517, 0.46280338, 0.53969218, 0.30190473, 0.47002029,
0.34839471, 0.45387181, 0.32428892, 0.47669926, 0.43538283,
0.34780933, 0.545502 , 0.44105495, 0.54125654, 0.40714621,
0.68139838, 0.60800298, 0.54447487, 0.50580328, 0.34931564,
0.31733902, 0.26745151, 0.47464268, 0.2460347 , 0.29628368])

A soma de todos os valores de comunalidade é o valor de comunalidade total:

fa.get_communalities().sum()
10.590479059488883

Para exibir uma tabela com os nomes da variáveis e suas respectivas comunalidades vamos construir um dataframe contendo esses dados. Lembrando que já temos a variável itens = ['A1', 'A2', ..., 'O4','O5']:

dfComunalidades = pd.DataFrame(comunalidades)
dfComunalidades.index = itens
Variável Comunalidade
A1 0.203905
A2 0.462803
A3 0.539692
A4 0.301905
A5 0.470020
C1 0.348395
C2 0.453872
C3 0.324289
C4 0.476699
C5 0.435383
Variável Comunalidade
E1 0.347809
E2 0.545502
E3 0.441055
E4 0.541257
E5 0.407146
N1 0.681398
N2 0.608003
N3 0.544475
N4 0.505803
N5 0.349316
Variável Comunalidade
O1 0.317339
O2 0.267452
O3 0.474643
O4 0.246035
O5 0.296284
Total 10.5905
Como temos 25 fatores:
Total/25 0.4236

Podemos pensar na comunalidade de uma variável como a proporção de variação nessa variável explicada pelos fatores propostos no modelo. Por exemplo, a variável N1 tem a maior comunalidade (0.681398) nesse modelo, indicando que aproximadamente 69% da variação nas respostas para “N1: Fico com raiva facilmente” é explicada pelo modelo de 5 fatores proposto enquanto esse valor é de apenas 20% para “A1: Sou indiferente aos sentimentos das outras pessoas”.

Comunalidades servem para avaliar o desempenho do modelo. Valores mais próximos de um indicam que o modelo explica a maior parte da variação para essas variáveis. Nesse caso o modelo está melhor ajustado para as variáveis do grupo N (neuroticismo)​ e menos eficiente para as variáveis do grupo O (abertura)​.

A Comunalidade total é de 10.5905 que, dividido entre as 25 variáveis indica uma média de 10.5905/25 = 0.4236 geral para o modelo, ou seja, uma eficiência média de 42% do modelo em explicar a variação de cada variável do teste.

🔺Início do artigo

Bibliografia