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.

Jupyter Notebook


O que é o Jupyter Notebook?

Jupyter Notebook é uma ferramenta poderosa para o desenvolvimento projetos de ciência de dados de forma interativa que é executada de dentro de seu browser. Além da execução interativa de código ele pode ser usado para a preparação de apresentações dinâmicas onde texto pode ser bem formatado contendo imagens, gráficos interativos e fórmulas matemáticas. O conteúdo das linhas de código e seus outputs (as respostas produzidas pelo código) ficam bem delimitadas.

Ele faz parte do Projeto Jupyter de código aberto e free software. É possível baixar e instalar gratuitamente o Jupyter Notebooks separadamente ou como parte do kit de ferramentas de ciência de dados Anaconda. Com ele se pode usar vários ambientes de execução (kernels ou núcleos) com linguagens de programação como o Python, R, Julia, C++, Scheme, Haskell e Ruby.

O Jupyter Notebook está se tornando uma interface muito popular para a computação na nuvem e muitos dos provedores desse tipo de serviço o adotaram como interface front-end para seus usuários. Entre eles estão o SageMaker Notebooks da Amazon, o Colaboratory do Google e o Azure Notebook da Microsoft. O Colaboratory (também chamado de Colab) é um ambiente gratuito que armazena e roda notebooks no Google Drive.

Os arquivos no Notebook são armazenados no formato JSON com a extensão *.ipynb. Eles podem ser lidos pelo aplicativo instalado (que usa o browser para sua exibição), e exportado para várias formatos, entre eles: html, markdown, pdf, latex e slides Reveal.js. O arquivo consiste de várias células que são exibidas como uma página usual de html. Algumas células são de código e seus resultados, outras de texto, imagens ou outras mídias. Alternativamente eles podem ser lidos e exibidos (de forma não interativa) pelo nbviewer (na web).

Instalação

A forma mais simples de instalação do Jupyter Notebooks consiste em instalar o Anaconda, a distribuição Python mais usada para ciência de dados. Esta instalação inclui as princiais bibliotecas e ferramentas mais populares, como NumPy, Pandas e Matplotlib.

Para obter o Anaconda:

  • Baixe a versão mais recente do Anaconda aqui. Existem instaladores gráficos para Linux, Windows e MacOS.
  • Instale seguindo as instruções na página de download ou contidas no arquivo instalador executável.
  • ou … usuários que conhecem Python e que preferem gerenciar seus pacotes manualmente, pode apenas usar:
    pip3 install jupyter.

Executando e Criando um Notebook

No Windows na MacOS ou Linux um atalho para Jupyter Notebook é criado no menu do sistema. Caso nenhum atalho tenha sido criado você pode criar um ou iniciar usando o console. No Linux se digita jupyter-notebook no console. Uma janela com a seguinte aparência é aberta no browser padrão:

A primeira página exibe o painel de controle do Jupyter (dashboard). Ele dá acesso aos arquivos e subpastas que estão na pasta de inicialização do Jupyter. Esta pasta, no entanto, pode ser alterada. O painel de controle tem uma interface amigável por onde você pode controlar suas pastas e notebooks. Toda a descrição que usamos aqui se refere à aparência default dos notebooks. Essa aparência pode ser alterada modificando-se o arquivo css em /home/nome-usuario/.jupyter/custom/custom.css (no Linux).

O Jupyter Notebook fica aberto no navegador usando uma URL especial, do tipo “http://localhost:8888/tree”. Isso indica que o servidor dessa página esta em sua máquina local.

Você pode criar uma pasta nova para seus projetos clicando em New Folder. Depois pode renomear sua pasta selecionado e clicando em Rename. Clique na pasta para abrí-la e depois em New: Python 3 (notebook). Um notebook vazio e sem nome é criado. Clique nele e em File | Rename e dê um nome apropriado para o seu notebook. Vários notebooks podem ser abertos ao mesmo tempo.

Um notebook aberto apresenta a seguinte interface:

Os menus superiores dão acesso as funcionalidades de gravar arquivo, importar e exportar para diferentes formatos, etc. O item File | Save and Checkpoint (Ctrl-S) permite guardar o estado as sessão no momento de forma a poder ser recuperada mais tarde por meio de File | Revert to Checkpoint. Para fechar uma sessão não basta fechar a aba do navegador pois isso deixaria ativa a tarefa na memória do computador. Para isso você pode usar File | Close and Halt ou Kernel | Shutdown. Também se pode clicar em LOGOUT no canto superior direito.

Ainda na barra de menus você pode clicar em Help | User interface tour para ver alguns pontos relevantes no notebook. Help | Keyboard Shortcuts exibe os atalhos de teclado predefinidos, observando que eles podem ser modificados pelo usuário. O menu de ajuda também contém links diretos para as documentações do Python, IPython, NumPy, SciPy, Matplotlib, SymPy, pandas e markdown. O ícone de teclado abre uma ajuda com os atalhos mais usados.

Notebooks são iniciados com uma célula vazia. Cada célula possui dois modos de uso: modo de edição e modo de comando. Clicando dentro da célula ela fica com borda verde e entra no modo de edição onde se pode inserir texto e códigos. Use a caixa de seleção na caixa de ferramentas do notebook para selecionar entre ‘code’, ‘markdown’ ou ‘Raw NBConvert’. (A opção ‘heading’ está em desuso e é desnecessária). Se ‘code’ está selecionado você pode digitar um comando como, por ex.,

print('Olá galera!')
Olá galera!

Comandos dentro de uma célula de código são executados todos de uma vez. Para isso pressione:
shift-enter: o código é executado e uma nova célula é aberta no mode de edição, ou
ctrl-enter: o código é executado e a mesma célula fica selecionada no modo de comando.

Em ambos os casos o output, se existir, é mostrado abaixo da célula. O botão ▶ Run na barra de ferramentas tem o mesmo efeito que shift-enter.

Os modos de edição e de comando são alternados com o uso de ESC (passa de comando para edição) e enter (de edição para comando).

Quando no modo de comando (a célula fica com a borda azul) os seguintes atalhos ficam habilitados:

Comando Ação
ESC/enter Alterna entre modos de edição / comando
Barra de espaço Rolar tela para baixo
Shift-Barra de espaço Rolar tela para cima
A Inserir célula acima
B Inserir célula abaixo
D, D Apagar células selecionadas
Z Desfazer apagamento
Shift-L Exibir/ocultar numeração das linhas de código
Shift-Tab Exibir docstring de objeto
Y Alterar célula para código
M Alterar célula para markdown
R Alterar célula para raw
H Exibir Help | keyboard shortcuts
Teclas 🔼 ou 🔽 Rolar para cima e para baixo nas células
Shift 🔼 ou 🔽 Selecionar várias células de uma vez.
Shift M Mesclar várias células selecionadas em uma única.
Ctrl Shift – Dividir célula ativa na posição do cursor (no modo de edição).
Shift clique na margem esquerda da célula Selecionar várias células de uma vez.

Um lista completa desses atalhos pode ser vista no menu Help | keyboard shortcuts.

Células no formato de texto

Células podem ser marcadas como texto puro, sem qualquer formatação, no modo Raw NBConvert. Esse texto é exibido como está e não passará por renderização de nenhuma forma nem na exibição dentro do notebook nem na exportação para outros formatos, como o pdf.

Markdown

Markdown é uma linguagem de marcação limitada mas bastante simples de se aprender e usar. Ela tem sintaxe diferente de html mas guarda uma correspondência com suas tags. Você pode encontrar uma descrição mais completa de Markdown na página Marcação de Texto com Markdown.

Além da marcação markdown se pode usar um conjunto restrito de tags html nas caixas de texto. Por exemplo, o código seguinte:

 <div style = "border:1px solid black; padding:10px; color:white; background-color:red;" >
parei aqui ↷ 
</div>

produzirá a barra vermelha abaixo, útil para marcar um ponto específico dentro de um notebook.

parei aqui ↷

O Jupyter Notebook pode renderizar Latex (para isso usa a biblioteca javascript Mathjax) em suas células de Markdown.


A identidade de Euler é \(e^{i \pi} + 1 = 0\).
A definição da derivada de primeira ordem é:
$$
f'(x) = \lim\limits_{h \rightarrow 0} \frac{f(x+h) – f(x)}{h}
$$

Células de código

Células de código contém as instruções a serem executadas pelo kernel. À esquerda de uma célula de código aparece o sinal In[n] que significa Input [n], onde n é a ordem de execução das linhas. Uma célula em exibição é marcada pelo sinal In[*], o que é importante observar quando se tem uma execução mais demorada de código. Em geral o notebook continua responsivo mas novas instruções devem aguardar o término das primeiras para serem executados. O resultado da computação da célula, quando existir, aparece abaixo com o sinal Out[n].

O código em um célula ativa (com bordas azuis) é executado com ctrl-enter ou shift-enter (alternativamente com botão ▶ Run na barra de ferramentas). A barra de menu contém as opções Cell | Run All, Cell | Run All Above Cell | Run All Below.

É importante notar que as células são executadas no kernel e seu estado persiste ao longo da execução. O estado do kernel se refere ao notebook como um todo e não às células individuais. Se você importar bibliotecas ou declarar variáveis ​​em uma célula elas estarão disponíveis em toda a seção e não apenas nas células abaixo da declaração. A ordem em que inputs e outputs aparecem pode ser enganosa: se você declarar uma variável em uma célula, digamos g = 5 e voltar para uma célula anterior a variável g ainda terá o mesmo valor. O que importa é a ordem de execução, marcada por In[n]. Por isso é preciso ter cuidado ao fazer alterações nas células. Idealmente se deve manter a ordem sequencial e, ao fazer alterações importantes, executar todo o notebook novamente.

Existem várias opções relativas à execução no menu Kernel:

Kernel | Interrupt Interromper o kernel,
Kernel | Restart Reinicializar o kernel,
Kernel | Restart & clear outputs Reinicializar kernel e limpar outputs,
Kernel | Restart & Run All Reinicializar kernel e executar todas as células.

A opção de reinicializar o kernel limpando as saídas e executar novamente todas as células pode ser útil quando muitas células foram alteradas. No entanto, dependendo das operações a serem executadas, ela pode ser uma operação demorada.

É possível escolher e rodar e Jupyter Notebook com diferentes opções de kernel, com diferentes versões de Python e muitas outras linguagens, incluindo Java, C, R e Julia.

O Notebook é salvo ao se pressionar Ctrl S. Isso renova o arquivo de ponto de verificação que é criado junto com o Notebook. Esse arquivo, que também tem extensão .ipynb fica localizado em pasta oculta e pode ser usado na recuperação da sessão caso algum erro inesperado cause a perda do notebook atual. Por padrão o Jupyter salva automaticamente o bloco de notas a cada 120 segundos sem alterar o arquivo de bloco de notas principal.Para reverter para um ponto de verificação use o menu via File | Revert to Checkpoint.

Tendo escolhido um notebook com kernel python 3, podemos experimentar um resultado com a execução de código que fornece um output de texto e gráfico:

# importe as bibliotecas necessárias
import numpy as np
import matplotlib.pyplot as plt

x = np.arange(1, 15, 0.1);
y = np.sin(x)/np.cos(x)
z = np.cos(x)/np.sin(x)
print("x em [1, 15] \ny=seno/cosseno; z=cosseno/seno")
plt.plot(x, y)
plt.plot(x, z)
x em [1, 15]
y=seno/cosseno; z=cosseno/seno

É importante observar que a última variável listada em uma célula é exibida no output sem necessidade de um comando print. Por ex., no código abaixo importamos uma tabela disponível na web no formato *.csv para a construção de um dataframe do pandas. O dataframe é exibido com uma formatação amigável no final da célula.

1  import pandas as pd
2  bfi ='https://vincentarelbundock.github.io/Rdatasets/csv/psych/bfi.csv'
3  df = pd.read_csv(bfi)
4  # head exibirá apenas as 5 primeiras linhas do dataframe
5  df.head()
Unnamed: 0 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

Ajuda

É sempre útil lembrar os mecanismos de se conseguir ajuda no python e no Jupyter. A função help do python é útil para se ver documentação sobre um tipo de variável ou uma bliblioteca importada. (Em ambos os casos a saída exibida está truncada.)

# ajuda sobre dictionaries
help(dict)
Help on class dict in module builtins:
class dict(object)
| dict() -> new empty dictionary
| dict(mapping) -> new dictionary initialized from a mapping object’s
| (key, value) pairs
| dict(iterable) -> new dictionary initialized as if via:
| d = {}
| for k, v in iterable:
| d[k] = v
import math
help(math)
Help on module math:
FUNCTIONS
acos(x, /)
Return the arc cosine (measured in radians) of x.
acosh(x, /)
Return the inverse hyperbolic cosine of x.
asin(x, /)
Return the arc sine (measured in radians) of x.

O comando help() usado sem argumento abre uma janela de busca interativa do tópico de ajuda.

Além disso escrever o nome de uma biblioteca, método ou variável precedido de ? provocará a exibição da docstring relativa ao item, o que é uma boa forma de conferir uso e sintaxe.

import pandas as pd
?pd
Type: module
String form: <module ‘pandas’ from ‘/home/guilherme/.anaconda3/lib/python3.7/site-packages/pandas/__init__.py’>
File: ~/.anaconda3/lib/python3.7/site-packages/pandas/__init__.py
Docstring:
pandas – a powerful data analysis and manipulation library for Python
=====================================================================
**pandas** is a Python package providing fast, flexible, and expressive data
structures [… texto truncado …]

Além disso é possível exibir a docstring de um objeto no Notebook apertando Shift-Tab após o nome do objeto dentro da célula.

Células e Linhas Mágicas em Jupyter

Uma sintaxe especial dos notebooks consiste em inserir comandos iniciados por %% para “mágica” na célula inteira e % para “mágica” na linha. Por exemplo %matplotlib inline faz com que gráficos do matplotlib sejam renderizados dentro do próprio notebook. Os sinais % e %%indicam respectivamente “mágica” na linha ou na célula. Digitar %lsmagic na célula e executá-la exibirá uma lista de todos os comandos mágicos de célula e de linha, informando ainda se automagic está ativado. Se automagic = ON os prefixos %% e % são desnecessários.

Alguns exemplos de “mágicas”:

Mágica Efeito
%run Roda um script externo como parte da célula.
Ex.: %run meu_codigo.py
executa meu_codigo.py como parte da célula.
%timeit Conta os loops, mede e exibe o tempo de execução da célula.
%prun Mostra o tempo de execução de uma função.
%%writefile Salva o conteúdo da célula para um arquivo.
Ex.: %%writefile meu_output.py
salva o código da célula no arquivo meu_output.py.
%%pycat Lê e exibe o conteúdo de um arquivo.
Ex.: %%pycat meu_output.py
lê o conteúdo gravada antes em meu_output.py.
%macro Define uma macro para execução posterior.
%store Salva a variável para uso em outro notebook.
%pwd Exibe a pasta default de trabalho.
%who Exibe varáveis ativas.
%matplotlib inline Exibe gráficos matplotlib inline.
%lsmagic Exibe lista de comandos “mágicos”.
%automagic Liga/desliga automagica (necessidade de %).
%%javascript Roda a célula como código JavaScript.
%%html Exibe código html renderizado.
%pdb Aciona o Python Debugger.
%<comando>? Mostra ajuda sobre a %<comando>
%%script python2
import sys
print 'Esse comando foi executado no Python %s' % sys.version
# output---> Esse comando foi executado no Python 2.7.18 (default, data / hora) 
%%script python3
import sys
print('Comando executado no Python: %s' % sys.version)
# output---> Esse comando foi executado no Python 3.8.5 (default, data / hora) 
%%bash
echo "O conteúdo de minha variável BASH: $BASH"
# output---> O conteúdo de minha variável BASH: /usr/bin/bash
# Definição de fatorial usando recursão	
def fatorial(n):
    if n == 1:
        return 1
    else:
        return n * factorial(n-1)
fatorial(40)      # resulta em 87178291200
# Claro que a mesma coisa seria obtida com a função
import math
print(math.factorial(14))    # 87178291200

Para medir o tempo de execução da célula ao rodar a função fatorial usamos %timeit

%timeit fatorial(14)

1.78 µs ± 16.9 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

Executando comandos da SHELL

Usando o prefixo ! você pode enviar comandos diretamente para a shell de seu sistema operacional. Os comandos dependerão, claro, de seu sistema. No exemplo seguinte uso o bash do linux.

! ls *.ipynb     # vai listar todos os arquivos ipynb
! ls *.py        # lista meu_output.py gravado no exemplo anterior 

# mv é o comando bash para renomear arquivos
! mv meu_output.py novo_nome.py 
! ls *.py        # lista novo_nome.py

# Se você tem pip instalado pode gerenciar pacotes python de dentro do notebook:
!pip install --upgrade nome_do_pacote

Extensões do Jupyter Notebook


Extensões são recursos adicionais que estendem a funcionalidade dos Notebooks. Elas podem melhorar sua funcionalidade como uma IDE (um ambiente integrado de desenvolvimento), permitindo o acompanhamento de variáveis para fins de debugging, formatando o código automaticamente ou alterando sua aparência. Elas também permitem a inserção de widgets, que geralmente são objetos com representação gráfica no navegador, tais como caixas de texto, sliders e outras ferramentas interativas. Elas adicionam funcionalidades GUIs nos notebooks.

Uma extensão muito usada é a Nbextensions que pode ser instalada à partir do código seguinte, inserido no próprio notebook.

conda install -c conda-forge jupyter_contrib_nbextensions
conda install -c conda-forge jupyter_nbextensions_configurator 

A instalação de extensões pode variar de acordo com o seu ambiente de trabalho.

Jupyter Widgets

Widgets são objetos do python que possuem uma representação visual no navegador e permitem interação do usuário. Eles podem ser simples como uma caixa de texto ou um controle de arrastar, ou objetos sofisticados como os controles sobre uma tabela de dados ou sobre um mapa.

Slider é um widget útil para receber um valor do usuário por meio do mouse ou toque de tela. O objeto possui propriedades que podem ser alteradas e lidas, e métodos.
No exemplo abaixo um slider é criado, algumas de suas propriedades são ajustadas e depois é exibido.

import ipywidgets as ipw
# Inicializar um slider
slid = ipw.IntSlider()
slid.min = 0
slid.max = 50
slid.description = "Avalie:"
display(slid) # Exibe o slider ( figura a)

# O objeto pode ser inicializado com as propriedades desejadas
slid2 = ipw.IntSlider(
    value=0,
    min=0,
    max=10,
    step=2,
    description='Só pares:',
    disabled=False,
    continuous_update=True,
    orientation='horizontal',
    readout=True,
    readout_format='d'
)
display(slid2) # Exibe o slider ( figura b)

Button é um widget que pode armazenar interação do usuário em resposta binária (sim/não, por ex.).

ipw.Button (
    description='Clica aí',
    disabled=False,
    tooltip='clica...',
    button_style='success', # pode ser 'success', 'info', 'warning', 'danger' ou ''
    icon='check'
)
# Exibe o botão (figura c)

Text apresenta uma caixa de texto a ser preenchida pelo usuário:

ipw.Text (
    placeholder='Digite seu nome',
    description='Nome:',
)
# Exibe a caixa de texto ( figura d)

Progress Bar mostra uma barra de progresso:

ipw.IntProgress (
    min=0,
    max=10,
    value=7,
    description='Realizado %',
    bar_style='success', # pode ser 'success', 'info', 'warning', 'danger' ou ''
    orientation='horizontal'
)
# Exibe a barra de progresso ( figura e)

Para serem úteis as widgets devem interagir com o código. Como ex. vamos criar uma barra de progresso atribuindo o objeto a uma variável.

pb = ipw.IntProgress (
    min=0,
    max=10,
    value=7,
    description='Realizado%',
    bar_style='success', # pode ser 'success', 'info', 'warning', 'danger' ou ''
    orientation='horizontal'
)

print('valor inicial = ', pb.value) # value é uma propriedade de pb. Neste caso seu valor é 7

pb.value = 10                       # que pode ser alterada no código
display(pb)	                        # exibe uma barra de progesso cheia

No exemplo seguinte fazemos uma interação entre os valores da barra de progresso com o clique de dois botões. O código usa o evento on_click dos botões para acionar as funções que aumentam e diminuem o valor da barra.

def aumentaBarra(s):
    pb.value +=1

def diminuiBarra(s):
    pb.value -=1

botaoMais = ipw.Button(description='Aumenta barra')
botaoMais.on_click(aumentaBarra)

botaoMenos = ipw.Button(description='Diminue barra')
botaoMenos.on_click(diminuiBarra)
# display(botaoMenos, botaoMais, pb)
# -- isso exibiria os controles um sobre o outro

# Para exibir os controles em uma linha única com título
# usamos HBox, Label

from ipywidgets import HBox, Label
HBox([Label('Em uma linha única:'), botaoMenos, botaoMais, pb])         # exibe figura (f)


Existem widgets mais sofisticados para formatação dos controles, inclusive com o uso de html e formatação css. Apenas para mostrar um exemplo segue o código abaixo, que usa HBox para construir uma linha e VBox para construir uma coluna de controles.

from ipywidgets import HBox, VBox, Label	
linha1 = HTML('<h2 style="color: red;">Use controles para modificar <i>progress box</i> </h2>')
linha2 = HBox([Label('Botões de controle:'), botaoMenos, botaoMais])
linha3 = pb
VBox([linha1, linha2, linha3]) # exibe figura (g)


Um clique no primeiro botão faz a barra de progressa avançar, no segundo faz a barra regredir.

Observe importamos os controles HBox,VBox e Label de ipywidget e, portanto eles podem ser usados diretamente sem menção à biblioteca de origem. Poderíamos usar a notação ipw.HBox para usar a caixa horizontal, sem precisar da importação explícita.

Outras informações sobre formatação dos widgets em
Layout and Styling of Jupyter widgets
.

Outros Widgets Úteis

HTMLMath serve para exibir equações matemáticas:

equacao = ipw.HTMLMath(
    value = r"Widgets podem ser usados para exibir equações inline \(x^2\) e destacadas $$\frac{x^2-1}{x-1}= x+1$$",
    placeholder='HTML',
    description='MathML:',
)
display(equacao)
MathML: Widgets podem ser usados para exibir equações inline \(x^2\) e destacadas $$\frac{x^2-1}{x-1}= x+1$$

ipyleaflet é uma biblioteca para a criação de mapas interativos que permitem marcação, arraste e ampliação. Em muitos casos, para executar um biblioteca recém instalada, pode ser necessário reinicializar o kernel.

# A biblioteca pode ser instalada com condas:
conda install -c conda-forge ipyleaflet

from ipyleaflet import Map
# inserindo as coordenadas da Praça da Liberdade em Belo Horizonte, MG
Map(center=[-19.932252,-43.9381638], zoom=15)

BeakerX é um pacote com widgets destinados a representar tabelas, gráficos, formulários de modo interativo. O widget de tabela reconhece e exibe dataframes do pandas. Ele facilita a pesquisa, e permite classificar, arrastar, filtrar, formatar ou selecionar células na tabela. Ele também permite a exportação para CSV ou área de transferência e a representação gráfica dos dados. O widget de tabela é mostrado abaixo, em imagem estática. Mais informações em Beakerx.

# Instalação do pacote
conda install -c conda-forge beakerx ipywidgets

import pandas as pd
from beakerx import *
pd.read_csv("UScity.csv")
# Instalando o pacote
conda install -c conda-forge k3d	

import k3d
plot = k3d.plot()
plot += k3d.points([0, 0, 0, 1, 1, 1])
plot.display()

A figura dentro do Notebook é dinâmica, podendo ser girada, ampliada, exportada, etc.

Modificando o estilo do Notebook

Jupyter Notebooks são enviados para o navegador como páginas html e são formatadas e estilizadas por meio de arquivos css (cascade style sheets) padrões. Para quem conhece html e css pode ser interessante editar diretamente esse arquivo. (Não se esqueça de fazer cópias de segurança dos arquivos que vai modificar).

Se sua instalação não contém a pasta “.jupyter” execute o comando jupyter notebook --generate-config no terminal. No Linux o arquivo de estilos está em /home/usuario/.jupyter/custom/custom.css, no Windows em pasta.do.python\Lib\site-packages\notebook\static\custom\custom.css, embora isso possa ser alterado ou ser diferente para outras instalações. Para ver a pasta onde está o arquivo de configurações você pode usar:

import jupyter_core
jupyter_core.paths.jupyter_config_dir()

Existem muitas folhas de estilo prontas para baixar, por exemplo no GitHub. Elas podem ser baixadas e copiadas para a pasta citada.

Para modificar apenas o estilo do Notebook aberto, sem alterar a aparência dos demais você pode usar:

# Importe biblioteca 
from IPython.core.display import HTML
HTML("<style> instruções de estilo </style>")
# Alternativamente se pode usar "mágica" em uma célula
%%html "<style> estilos - CSS </style>"

# Por exemplo, para atribuir margens laterais de 10% do tamanho da página use
display(HTML("<style>.container { margin: 0 10% 0 10% !important; }<style>"))

# Para exibir outputs em negrito
display(HTML("<style>.output_wrapper { font-weight: bold !important; }</style>"))

De dentro de um Notebook você pode carregar uma folha de estilo particular que passa a valer para aquele documento. Esse método é útil para formatar documentos de um tipo específico, por exemplo para uma apresentação ou páginas com estilos incomuns.

from IPython.core.display import HTML
def css_styling():
    styles = open("../styles/custom.css", "r").read()
    return HTML(styles)
css_styling()

Finalmente você pode instalar extensões tais como Jupyter themes.

Exportando e Compartilhando Notebooks


Notebooks podem ser exportados para diversos formatos tais como HTML e PDF e vários outros formatos. Isso pode ser encontrado no menu File | Download as. Se estiver trabalhando na nuvem a importação no formato nativo .ipynb serve para baixar para seu computador uma cópia do notebook. Ao subir uma cópia para o GitHub ou para o Colab você pode disponibilizar a sessão para outros colaboradores. Slides são úteis para aulas e apresentações.

NBViewer: NBViewer é um renderizador de notebooks, útil para quem quer ver uma sessão sem ter que necessariamente instalar o Jupyter. Esse é um serviço gratuito oferecido pelo Projeto Jupyter, disponível em nbviewer.jupyter.org. Ele recebe uma URL do notebook e o renderiza como página da web.

Bibliografia

  • Wintjen, Marc: Practical Data Analysis Using Jupyter Notebook: Learn how to speak the language of data by extracting useful and actionable insights using Python, Packt Publishing, Birmingham, UK, 2020.
  • Taieb, David: Thoughtful Data Science, , Packt Publishing, Birmingham, UK, 2018.
  • Dataquest: Advanced Jupyter Notebooks Tutorial, acessado em novembro de 2020.
  • Real Python: Jupyter Notebook Introduction, acessado em novembro de 2020.
  • Jupyter.org: Jupyter Main Site, acessado em novembro de 2020.
  • jupyter.org: Jupyter Widgets, acessado em novembro de 2020.

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