pandas e SQL comparados


SQL (Structured Query Language) é uma linguagem de programação de uso específico utilizada para consultar, extrair e gerenciar bancos de dados relacionais. Pandas é uma biblioteca do Python especializada para o tratamento e análise de dados estruturados, incluindo uma gama de formas de extrair dados.

Esse artigo faz uma comparação entre as consultas feitas a dataframes do pandas e as consultas SQL, explorando similaridades e diferenças entre os dois sistemas. Ele serve para descrever as funcionalidades de busca e edição do pandas e pode ser particularmente útil para aqueles que conhecem SQL e pretendem usar o pandas (ou vice-versa).

Para realizar os experimentos abaixo usamos o Jupyter Notebook, um aplicativo que roda dentro de um navegador, que pode ser facilmente instalado e permite a reprodução se todo o código aqui descrito. Você pode ler mais sobre Jupyter Notebook e Linguagem de Consultas SQL nesse site.

Esse texto é baseado em parte do manual do pandas e expandido. Ele usa um conjunto de dados baixados do github renomeado aqui para dfGorjeta. Nomes e valores dos campos foram traduzidos para o português.

# importar as bibliotecas necessárias
import pandas as pd
import numpy as np

url = "https://raw.github.com/pandas-dev/pandas/master/pandas/tests/io/data/csv/tips.csv"

dfGorjeta = pd.read_csv(url)
dfGorjeta.head()
total_bill tip sex smoker day time size
0 16.99 1.01 Female No Sun Dinner 2
1 10.34 1.66 Male No Sun Dinner 3
2 21.01 3.50 Male No Sun Dinner 3
3 23.68 3.31 Male No Sun Dinner 2
4 24.59 3.61 Female No Sun Dinner 4

Para efeito de testar os comandos do dataframe vamos alterar os nomes dos campos e traduzir os conteúdos dos dados. Para descobrir quais são os valores dos campos, sem repetições, transformamos as séries em sets, uma vez que valores de um set (conjunto) não se repetem.

print(set(dfGorjeta["sexo"]))
print(set(dfGorjeta["fumante"]))
print(set(dfGorjeta["dia"]))
print(set(dfGorjeta["hora"]))
{‘Male’, ‘Female’}
{‘No’, ‘Yes’}
{‘Sat’, ‘Sun’, ‘Fri’, ‘Thur’}
{‘Lunch’, ‘Dinner’}

No código seguinte alteramos os nomes de campos e traduzimos o conteúdo. A sintaxe da operação de edição do dataframe será discutida mais tarde no artigo:

# muda os nomes dos campos
dfGorjeta.rename(columns={"total_bill":"valor_conta", "tip":"gorjeta",
                        "smoker":"fumante", "sex":"sexo","day":"dia",
                        "time":"hora","size":"pessoas"}, inplace=True)

# traduzindo os valores dos campos:
dfGorjeta.loc[dfGorjeta["fumante"] == "No", "fumante"] = "não"
dfGorjeta.loc[dfGorjeta["fumante"] == "Yes", "fumante"] = "sim"
dfGorjeta.loc[dfGorjeta["sexo"] == "Female", "sexo"] = "mulher"
dfGorjeta.loc[dfGorjeta["sexo"] == "Male", "sexo"] = "homem"
dfGorjeta.loc[dfGorjeta["hora"] == "Dinner", "hora"] = "jantar"
dfGorjeta.loc[dfGorjeta["hora"] == "Lunch", "hora"] = "almoço"
dfGorjeta.loc[dfGorjeta["dia"] == "Fri", "dia"] = "sex"
dfGorjeta.loc[dfGorjeta["dia"] == "Sat", "dia"] = "sab"
dfGorjeta.loc[dfGorjeta["dia"] == "Sun", "dia"] = "dom"
dfGorjeta.loc[dfGorjeta["dia"] == "Thur", "dia"] = "qui"

# Temos agora o seguinte dataframe
dfGorjeta
valor_conta gorjeta sexo fumante dia hora pessoas
0 16.99 1.01 mulher não dom jantar 2
1 10.34 1.66 homem não dom jantar 3
2 21.01 3.50 homem não dom jantar 3
3 23.68 3.31 homem não dom jantar 2
4 24.59 3.61 mulher não dom jantar 4
239 29.03 5.92 homem não sab jantar 3
240 27.18 2.00 mulher sim sab jantar 2
241 22.67 2.00 homem sim sab jantar 2
242 17.82 1.75 homem não sab jantar 2
243 18.78 3.00 mulher não qui jantar 2

As consultas SQL realizadas a seguir pressupõe a existência de um banco de dados com o mesmo nome, a mesma estrutura e dados que o dataframe dfGorjetas.

SELECT

Nas consultas SQL as seleções são feitas com uma lista de nomes de campos que se deseja retornar, separados por vírgula, ou através do atalho * (asterisco) para selecionar todas as colunas. No pandas a seleção de colunas é feita passando-se uma lista de nomes de campos para o DataFrame. Uma chamada ao dataframe sem uma lista de nomes de colunas resulta no retorno de todas as colunas, da mesma forma que usar * no SQL.

–– sql: consulta (query) usando select
SELECT valor_conta, gorjeta, fumante, hora FROM dfGorjeta LIMIT 5;
# pandas:
dfGorjeta[["valor_conta", "gorjeta", "hora"]].head()
valor_conta gorjeta hora
0 16.99 1.01 jantar
1 10.34 1.66 jantar
2 21.01 3.50 jantar
3 23.68 3.31 jantar
4 24.59 3.61 jantar

O método head(n) limita o retorno do dataframe às n primeiras linhas. n = 5 é o default. Para listar as últimas linhas usamos tail(n). Linhas também podem ser selecionadas por chamadas ao sei indice.

# Para acessar as últimas linhas do dataframe podemos usar
# dfGorjeta[["valor_conta", "gorjeta", "hora"]].tail()

# selecionando linhas por meio de seu índice.
dfGorjeta.iloc[[1,239,243]]
valor_conta gorjeta sexo fumante dia hora pessoas
1 10.34 1.66 homem não dom jantar 3
239 29.03 5.92 homem não sab jantar 3
243 18.78 3.00 mulher não qui jantar 2

Os dataframes possuem a propriedade shape que contém sua dimensionalidade. No nosso caso temos

dfGorjeta.shape
(244, 7)

o que significa que são 244 linhas em 7 campos.

No SQL você pode retornar uma coluna resultado de um cálculo usando elementos de outras colunas. No pandas podemos usar o método assign() para inserir uma coluna calculada:

–– sql:
SELECT *, gorjeta/valor_conta*100 as percentual FROM dfGorjeta LIMIT 4;
# pandas: método assign()
dfGorjeta.assign(percentual = dfGorjeta["gorjeta"] / dfGorjeta["valor_conta" *100]).head(4)
valor_conta gorjeta sexo fumante dia hora pessoas percentual
0 16.99 1.01 mulher não dom jantar 2 5.944673
1 10.34 1.66 homem não dom jantar 3 16.054159
2 21.01 3.50 homem não dom jantar 3 16.658734
3 23.68 3.31 homem não dom jantar 2 13.978041

Essa coluna é retornada mas não fica anexada ao dataframe. Para anexar uma coluna ao dataframe podemos atribuir o resultado do cálculo a uma nova coluna:

dfGorjeta["percentual"] = dfGorjeta["gorjeta"] / dfGorjeta["valor_conta"] * 100
print("Nessa estapa temos as colunas:\n", dfGorjeta.columns)

# Vamos apagar a coluna recém criada para manter a simplicidade da tabela
dfGorjeta.drop(["percentual"], axis=1, inplace=True)
Nessa estapa temos as colunas:
Index([‘valor_conta’, ‘gorjeta’, ‘sexo’, ‘fumante’, ‘dia’, ‘hora’, ‘pessoas’, ‘percentual’],
dtype=’object’)

WHERE


Filtragem de dados em consultas SQL são feitas através da cláusula WHERE. DataFrames podem ser filtrados de várias formas diferentes. O indexamento com valores booleanos é provavelmente o mais simples:

–– cláusula WHERE do sql
SELECT * FROM dfGorjeta WHERE hora = "jantar" LIMIT 5;
# filtragem por indexamento no pandas
dfGorjeta[dfGorjeta["hora"] == "jantar"].head(5)
valor_conta gorjeta sexo fumante dia hora pessoas
0 16.99 1.01 mulher não dom jantar 2
1 10.34 1.66 homem não dom jantar 3
2 21.01 3.50 homem não dom jantar 3
3 23.68 3.31 homem não dom jantar 2
4 24.59 3.61 mulher não dom jantar 4

A consulta acima funciona da seguinte forma:

# is_jantar é uma série contendo True e False (True para jantares, False para almoços)
is_jantar = dfGorjeta["hora"] == "jantar"
# usamos display para exibir a contagem de falsos/verdadeiros
display("Quantos jantares/almoços:", is_jantar.value_counts())

# para negar a série inteira, invertendo True ↔ False usamos ~ (til)
# a linha abaixo imprime o número de almoços na tabela
print("A lista contém %d almoços" % dfGorjeta[~is_jantar]["hora"].count())

# também podemos obter a lista das entradas que não correspondem a "jantar" usando
# dfGorjeta[dfGorjeta["hora"] != "jantar"]
‘Quantos jantares/almoços:’
True 176
False 68
Name: hora, dtype: int64A lista contém 68 almoços

Quando essa série é passada para o dataframe apenas as linhas correspondentes à True são retornados. A última consulta é equivalente à dfGorjeta[~is_jantar].head().

No SQL podemos procurar por partes de uma string com a cláusula LIKE. No pandas transformamos o campo dfGorjeta["sexo"]em uma string que possui o método startswith("string").

–– sql: SELECT TOP 2 sexo, valor_conta FROM dfGorjeta WHERE sexo LIKE 'ho%';
dfGorjeta.loc[dfGorjeta['sexo'].str.startswith('ho'),['sexo','valor_conta']].head(2)

que retorna as 2 primeiras linhas em que o campo sexo começa com o texto “ho”.

Também podemos procurar por campos que estão incluidos em um conjunto de valores:

–– sql:
SELECT * FROM dfGorjeta WHERE dia IN ('sab', 'dom');
dfGorjeta.loc[dfGorjeta['dia'].isin(["dom", "sab"])]

que retorna todas as linhas em que o campo dia é “dom” ou “sab”.

Assim como se pode usar operadores lógicos AND e OR nas consultas SQL para inserir múltiplas condições, o mesmo pode ser feito com dataframes usando | (OR) e & (AND). Por ex., para listar as gorjetas com valor superior à $5.00 dadas em jantares:

–– SQL: múltiplas condições em WHERE
SELECT * FROM dfGorjeta WHERE hora = 'jantar' AND gorjeta > 6.00;
# no pandas
dfGorjeta[(dfGorjeta["hora"] == "jantar") & (dfGorjeta["gorjeta"] > 6.00)]
valor_conta gorjeta sexo fumante dia hora pessoas
23 39.42 7.58 homem não sab jantar 4
59 48.27 6.73 homem não sab jantar 4
170 50.81 10.00 homem sim sab jantar 3
183 23.17 6.50 homem sim dom jantar 4
212 48.33 9.00 homem não sab jantar 4
214 28.17 6.50 mulher sim sab jantar 3

Podemos obter uma lista dos dados correspondentes a gorjetas dadas por grupos com 5 ou mais pessoas ou com contas de valor acima de $45.00, limitada aos 4 primeiros registros:

–– SQL:
SELECT * FROM dfGorjeta WHERE pessoas >= 5 OR valor_conta > 45 LIMIT 4;
# pandas
dfGorjeta[(dfGorjeta["pessoas"] >= 5) | (dfGorjeta["valor_conta"] > 45)].head(4)
valor_conta gorjeta sexo fumante dia hora pessoas
59 48.27 6.73 homem não sab jantar 4
125 29.80 4.20 mulher não qui almoço 6
141 34.30 6.70 homem não qui almoço 6
142 41.19 5.00 homem não qui almoço 5

Dados ausentes são representados por NULL no, uma marca especial para indicar que um valor não existe no banco de dados. Nos dataframes do pandas o mesmo papel é desempenhado por NaN (Not a Number). Esses marcadores podem surgir, por ex., na leitura de um arquivo csv (valores separados por vírgulas) quando um valor está ausente ou não é um valor numérico em uma coluna de números. Para verificar o comportamento do pandas com NaN criamos um dataframe com valores ausentes. Verificações de nulos é feita com os métodos notna() e isna().

frame = pd.DataFrame({"col1": ["A", "B", np.NaN, "C", "D"], "col2": ["F", np.NaN, "G", "H", "I"]})
frame
col1 col2
0 A F
1 B NaN
2 NaN G
3 C H
4 D I

Se temos um banco de dados SQLcom essa estrutura e conteúdo podemos extrair as linhas onde col2 é NULL usando a consulta:

–– sql
SELECT * FROM frame WHERE col2 IS NULL;
# no case do pandas usamos
frame[frame["col2"].isna()]
col1 col2
1 B NaN

De forma análoga, podemos extrair as linhas para as quais col1 não é NULL. No pandas usamos notna().

–– sql
SELECT * FROM frame WHERE col1 IS NOT NULL;
# pandas: linhas em que col1 não é nula
frame[frame["col1"].notna()]
col1 col2
0 A F
1 B NaN
3 C H
4 D I

GROUP BY


No SQL consultas com agrupamentos são feitas usando-se as operações GROUP BY. No pandas existe o método groupby() que tipicamente particiona o conjunto de dados em grupos e aplica alguma função (em geral de agregamento), combinando depois os grupos resultantes.

Um exemplo comum é o de particionar os dados em grupos menores e contar os elementos desses grupos. Voltando ao nosso dataframe dfGorjeta podemos consultar quantas gorjetas foram dadas por grupos de cada sexo:

–– sql
SELECT sexo, count(*) FROM dfGorjeta GROUP BY sexo;
# o equivalente em pandas seria
dfGorjeta.groupby("sexo").size()
sexo
mulher 87
homem 157
dtype: int64

O resultado é uma series cujos valores podem ser retornados por seu nome de index ou pelo número desse indice.

print("A lista contém %d homens" % dfGorjeta.groupby("sexo").size()[0])
print("\t\t e %d mulheres" % dfGorjeta.groupby("sexo").size()["mulher"])
A lista contém 157 homens
e 87 mulheres

É possível aplicar o método count() para cada coluna, individualmente:

dfGorjeta.groupby("sexo").count()
valor_conta gorjeta fumante almoço hora pessoas
sexo
mulher 87 87 87 87 87 87
homem 157 157 157 157 157 157

Observe que no código do pandas usamos size() e não count(). Isso foi feito porque o método count() é aplicado sobre cada coluna e retorna tantos valores quantas colunas existem, com valores não null.

Também se pode aplicar o método count() para uma coluna específica:

# para contar valores em uma única coluna primeiro ela é selecionada, depois contada
dfGorjeta.groupby("sexo")["valor_conta"].count()
sexo
mulher 87
homem 157
Name: valor_conta, dtype: int64

Existem diversas funções de agregamento. São elas:

função descrição
mean() calcula médias para cada grupo
sum() soma dos valores do grupo
size() *tamanhos dos grupos
count() número de registros no grupo
std() desvio padrão dos grupos
var() variância dos grupos
sem() erro padrão da média dos grupos
describe() gera estatísticas descritivas
first() primeiro valor no grupo
last() último valor no grupo
nth() n-ésimo valor (ou subconjunto se n for uma lista)
min() valor mínimo no grupo
max() valor máximo no grupo

* A função size() retorna o número de linhas em uma serie e o número de linhas × colunas em dataframes.

Para obter um resumo estatístico relativo ao campo gorjeta, agrupado pelo campo sexo podemos usar:

dfGorjeta.groupby("sexo")["gorjeta"].describe()
count mean std min 25% 50% 75% max
sexo
homem 157.0 3.089618 1.489102 1.0 2.0 3.00 3.76 10.0
mulher 87.0 2.833448 1.159495 1.0 2.0 2.75 3.50 6.5

Múltiplas funções podem ser aplicadas de uma vez. Suponha que queremos determinar como os valores das gorjetas variam por dia da semana. O método agg() (de agregar) permite que se passe um dicionário para o dataframe agrupado, indicando que função deve ser aplicada a cada coluna.

–– sql (agrupe os dados por dia, calcule a média para cada dia e o número de entradas contadas)
SELECT dia, AVG(gorjeta), COUNT(*) FROM dfGorjeta GROUP BY dia;
# na pandas, use mean no campo gorjeta, size no campo dia
dfGorjeta.groupby("dia").agg({"gorjeta": np.mean, "dia": np.size})
gorjeta dia
dia
dom 3.255132 76
qui 2.771452 62
sab 2.993103 87
sex 2.734737 19

Também é possível realizar o agrupamento por mais de uma coluna. Para fazer isso passamos uma lista de colunas para o método groupby().

–– agrupe primeiro por "fumante", depois por "dia"
–– realize a contagem dos registros e a média das gorjetas
SELECT fumante, dia, COUNT(*), AVG(gorjeta) FROM dfGorjeta GROUP BY fumante, dia;
# no pandas
dfGorjeta.groupby(["fumante", "dia"]).agg({"gorjeta": [np.size, np.mean]})
gorjeta
size mean
fumante dia
não dom 57.0 3.167895
qui 45.0 2.673778
sab 45.0 3.102889
sex 4.0 2.812500
sim dom 19.0 3.516842
qui 17.0 3.030000
sab 42.0 2.875476
sex 15.0 2.714000

JOIN

No SQL tabelas podem ser juntadas ou agrupadas através da cláusula JOIN. Junções podem ser LEFT, RIGHT, INNER, FULL. No pandas se usa os métodos join() ou merge(). Por defaultjoin() juntará os DataFrames por seus índices. Cada método tem parâmetros que permitem escolher o tipo da junção (LEFT, RIGHT, INNER, FULL), ou as colunas que devem ser juntadas (por nome das colunas ou índices). [Linguagem de Consultas SQL]

# para os exercícios que se seguem criamos os dataframes
df1 = pd.DataFrame({"key": ["A", "B", "C", "D"], "value":  [11, 12, 13, 14]})
df2 = pd.DataFrame({"key": ["B", "D", "D", "E"], "value":  [21, 22, 23, 24]})
# para exibir esses dataframes com formatação usamos display()
display(df1)
display(df2)
key value
0 A 11
1 B 12
2 C 13
3 D 14
key value
0 B 21
1 D 22
2 D 23
3 E 24

Como antes supomos a existência de duas tabelas de dados sql como as mesmas estruturas e dados para considerarmos as várias formas de JOINs.

INNER JOIN

–– junção das duas tabelas ligadas por suas chaves - key
SELECT * FROM df1 INNER JOIN df2 ON df1.key = df2.key;
# por default merge() faz um INNER JOIN
pd.merge(df1, df2, on="key")
key value_x value_y
0 B 12 21
1 D 14 22
2 D 14 23

O método merge() também oferece parâmetros para que sejam feitas junções de uma coluna de um dataframe com o índice de outro dataframe. Para ver isso vamos criar outro dataframe a partir de df2, usando o campo key como índice.

# novo dataframe tem campo "key" como índice
df2_indice = df2.set_index("key")
display(df2_indice)
pd.merge(df1, df2_indice, left_on="key", right_index=True)
value
key
B 21
D 22
D 23
E 24
key value_x value_y
1 B 12 21
3 D 14 22
3 D 14 23

LEFT OUTER JOIN

A junção LEFT OUTER JOIN recupera todos as campos à esquerda, existindo ou não uma linha correspondente à direita. O parâmetro how="left" é o equivalente no pandas.

–– sql: recupera todos os valores de df1 existindo ou não correspondente em df2
SELECT * FROM df1 LEFT OUTER JOIN df2 ON df1.key = df2.key;
# pandas: how="left" equivale a LEFT OUTER JOIN
pd.merge(df1, df2, on="key", how="left")
key value_x value_y
0 A 11 NaN
1 B 12 21
2 C 13 NaN
3 D 14 22
4 D 14 23

Observe que df2 não possui campos com key = "A" ou key = "C" e, por isso o dataframe resultante tem NaN nessas entradas. key = "A". Como df2 tem 2 linhas para key = "D" a linha aparece duplicada para essa key em df1.

RIGHT JOIN

A junção RIGH OUTER JOIN recupera todos as campos à direita, existindo ou não uma linha correspondente à esquerda. O parâmetro how="right" é o equivalente no pandas.

–– sql: recupera todos os registros em df2
SELECT * FROM df1 RIGHT OUTER JOIN df2 ON df1.key = df2.key;
# pandas: how="right" equivale a RIGHT OUTER JOIN
pd.merge(df1, df2, on="key", how="right")
key value_x value_y
0 B 12 21
1 D 14 22
2 D 14 23
3 E NaN 24

FULL JOIN

A junção FULL OUTER JOIN recupera todos as campos à direita ou à esquerda, representando como NaN os valores ausentes em uma ou outra. Todos as linhas das duas tabelas são retornadas com junção onde a campo key existe em ambas. O parâmetro how="outer" é o equivalente no pandas. Observe que nem todos os gerenciadores de bancos de dados permitem essa operação.

–– sql: retorna todos os registros em ambas as tabelas
SELECT * FROM df1 FULL OUTER JOIN df2 ON df1.key = df2.key;
# pandas: how="outer" é o equivalente em dataframes
pd.merge(df1, df2, on="key", how="outer")
key value_x value_y
0 A 11 NaN
1 B 12 21
2 C 13 NaN
3 D 14 22
4 D 14 23
5 E NaN 24

UNION

Para os exemplos seguintes definimos mais 2 dataframes:

df3 = pd.DataFrame({"cidade": ["Rio de Janeiro", "São Paulo", "Belo Horizonte"], "nota": [1, 2, 3]})
df4 = pd.DataFrame({"cidade": ["Rio de Janeiro", "Curitiba", "Brasília"], "nota": [1, 4, 5]})

No SQL a clásula UNION ALL é usada para juntar as linhas retornadas em dois (ou mais) instruções de SELECT. Linhas duplicadas são mantidas. O mesmo efeito pode ser conseguido no pandas usando-se o método concat().

–– sql: UNION ALL
SELECT city, rank FROM df3 UNION ALL SELECT cidade, nota FROM df4;
# pandas: concat
pd.concat([df3, df4])
cidade nota
0 Rio de Janeiro 1
1 São Paulo 2
2 Belo Horizonte 3
0 Rio de Janeiro 1
1 Curitiba 14
2 Brasília 5

No SQL a cláusula UNION tem o mesmo efeito que UNION ALL mas remove as linhas duplicadas. No pandas isso pode ser conseguido se fazendo a conactenação concat() seguida de drop_duplicates().

–– SQL UNION
SELECT city, rank FROM df1 UNION SELECT city, rank FROM df2;
–– o registro duplicado no Rio de Janeiro fica excluído
# pandas: concat() seguido de drop_duplicates()
pd.concat([df1, df2]).drop_duplicates()
cidade nota
0 Rio de Janeiro 1
1 São Paulo 2
2 Belo Horizonte 3
1 Curitiba 14
2 Brasília 5

Outras funções analíticas e de agregamento

Para os próximos exemplos vamos retornar ao nosso dataframe dfGorjeta: para listar as 5 gorjetas mais altas, no MySQL (a sintaxe varia de um para outro gerenciador).

–– MySQL: retorna todos os campos em ordem decrescente, 5 linhas
SELECT * FROM dfGorjeta ORDER BY gorjeta DESC LIMIT 10 OFFSET 5;
# pandas: seleciona 15 maiores e exibe as 10 de menor valor
dfGorjeta.nlargest(15, columns="gorjeta").tail(10)
valor_conta gorjeta sexo fumante dia hora pessoas
183 23.17 6.50 homem sim Dom jantar 4
214 28.17 6.50 mulher sim sab jantar 3
47 32.40 6.00 homem não Dom jantar 4
239 29.03 5.92 homem não sab jantar 3
88 24.71 5.85 homem não Thur almoço 2
181 23.33 5.65 homem sim Dom jantar 2
44 30.40 5.60 homem não Dom jantar 4
52 34.81 5.20 mulher não Dom jantar 4
85 34.83 5.17 mulher não Thur almoço 4
211 25.89 5.16 homem sim sab jantar 4

UPDATE

Há muitas formas de alterar um valor em um campo de um dataframe. Por exemplo, abaixo realizamos uma alteração em todos os valores de gorjeta sempre que gorjeta < 2.

–– sql: em todas as linhas duplique a gorjeta se gorjeta for menor que 1.1
UPDATE dfGorjeta SET gorjeta = gorjeta*2 WHERE gorjeta < 1.1;
# pandas: o mesmo resultado pode ser obtido da aseguinte forma
# dfGorjeta.loc[dfGorjeta["gorjeta"] < 1.1, "gorjeta"] *= 2

Para explicar com mais detalhes o funcionamento deste código, armazenamos abaixo a lista dos índices das linhas de gorjetas mais baixas e exibimos essas linhas. Em seguida multiplicamos apenas as gorjetas dessas linhas por 2 e examinamos o resultado:

indices = dfGorjeta[dfGorjeta["gorjeta"] < 1.1].index
print("Índices de gorjetas < 1.1:", indices)
display("Lista de gorjetas < 1.1", dfGorjeta.iloc[indices])
# multiplica essas gorjetas por 2
dfGorjeta.loc[dfGorjeta["gorjeta"] < 1.1, "gorjeta"] *= 2
# lista as mesmas linhas após a operação
display("Gorjetas após a operação:", dfGorjeta.iloc[indices])
Índices de gorjetas < 1.1: Int64
Index([0, 67, 92, 111, 236], dtype=’int64′)
‘Lista de gorjetas < 1.1’

valor_conta gorjeta sexo fumante dia hora pessoas
0 16.99 1.01 mulher não dom jantar 2
67 3.07 1.00 mulher sim sab jantar 1
92 5.75 1.00 mulher sim sex jantar 2
111 7.25 1.00 mulher não sab jantar 1
236 12.60 1.00 homem sim sab jantar 2

‘Gorjetas após a operação:’

valor_conta gorjeta sexo fumante dia hora pessoas
0 16.99 2.02 mulher não dom jantar 2
67 3.07 2.00 mulher sim sab jantar 1
92 5.75 2.00 mulher sim sex jantar 2
111 7.25 2.00 mulher não sab jantar 1
236 12.60 2.00 homem sim sab jantar 2
–– sql: alterar um campo de uma linha específica (supondo a existência de um campo id)
UPDATE dfGorjeta SET sexo = 'NI' WHERE id = 239
# para alterar o campo sexo para 'NI' (não informado)
dfGorjeta.loc[239, 'sexo'] ='NI'

DELETE

Existem muitas formas de se excluir linhas de um dataframe mas é comum a prática de selecionar as linhas que devem ser mantidas e copiar para um novo dataframe.

–– sql: linhas são apagadas sob um certo critério
DELETE FROM dfGorjeta WHERE gorjeta > 9;
# pandas: como novo dataframe tem o mesmo nome do original, o antigo é sobrescrito e perdido
dfTop = dfGorjeta.loc[dfGorjeta["gorjeta"] > 9]
dfTop
valor_conta gorjeta sexo fumante dia hora pessoas
170 50.81 10.0 homem sim sab jantar 3

Também é possível apagar linhas usando seu índice:

# apagar linha com index = 4, inplace para substituir o dataframe
dfGorjeta.drop(index=4, inplace=True)
# apagar linhas com index = 0 até 3
dfGorjeta.drop(index=[0, 1, 2, 3], inplace=True)
dfGorjeta.head()
valor_conta gorjeta sexo fumante dia hora pessoas
5 25.29 4.71 homem não dom jantar 4
6 8.77 12.00 homem não dom jantar 2
7 26.88 3.12 homem não dom jantar 4
8 15.04 1.96 homem não dom jantar 2
9 14.78 3.23 homem não dom jantar 2
🔺Início do artigo

Bibliografia

  • McKinney, Wes: Python for Data Analysis, Data Wrangling with Pandas, NumPy,and IPython
    O’Reilly Media, 2018.
  • Pandas: página oficial, acessada em janeiro de 2021.

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