DEV Community

Cover image for Análise de correspondência, o que é e como fazer com Python, usando Naruto como exemplo
Suami Medeiros
Suami Medeiros

Posted on

Análise de correspondência, o que é e como fazer com Python, usando Naruto como exemplo

Dentro desse artigo vamos explorar diferentes técnicas para análise de dados categóricos, para entender de forma lúdica vamos vestir nossas bandanas e nos transportar para o mundo de Naruto (que eu particularmente gosto muito). Vamos seguir a seguinte linha de raciocínio: Imagine que você seja um sensei na vila de konoha acompanhando seus alunos no exame chunin e deseja saber qual o estilo de luta que é mais popular entre os shinobis de cada aldeia para traçar estratégias junto com os mesmos. Vamos lá?

kakashi e naruto

Indíce

O inicio de tudo - Análise de correspondência

Para isso vamos conhecer nossas armas: A análise de correspondência simples é uma técnica estatística multivariada utilizada para explorar e visualizar relações entre as variáveis categóricas em tabelas de contingência. Para compreender melhor esse conceito é bom entender uma categoria como um conceito que representa grupos não numéricos, ou seja, se você estiver agrupando coisas que são diferentes de grupos numéricos então você está usando categorias.

Dentro do nosso cenário, vamos imaginar que recebemos o relatório com os resultados feito pelos pesquisadores da aldeia e vamos começar a analisar esses dados:

Definindo nosso cenário básico - Importando bibliotecas

Nesse primeiro momento vamos garantir que todas as nossas bibliotecas estão importadas e vamos configurar o estilo do nosso gráfico afim de garantir um aspecto mais visivelmente agradável.

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from scipy.stats import chi2.contigency

sns.set_style('whitegrid')
plt.rcParams.udate((
  'font.family': 'sans-serif',
  'font.size': 11,
  'axes.titlesize': 15,
  'axes.titleweight': 'bold',
  'axes.labelsize': 12
))
Enter fullscreen mode Exit fullscreen mode

Carregando nossos dados para análise

Nessa parte da análise vamos carregar o relatório que nos foi enviado para que possamos visualizar melhor em gráficos e ter uma noção melhor de quais são as aldeias1 comparados com os tipos de estilo de luta2.

1: Nesse relatório as aldeias são apresentadas como linhas na tabela
2: Nesse relatório os estilos de luta são apresentados como colunas na tabela

dados = pd.read_csv('tabela_contigencia_naruto.csv', index_col=0)
Enter fullscreen mode Exit fullscreen mode

Agora que carregamos nosso relatório, vamos visualizar os dados de forma básica para termos uma ideia geral do cenário:

print('Forma da tabela', dados.shape)
data
Enter fullscreen mode Exit fullscreen mode

tabela

Visualizando-os com uma tabela de contingência

Aqui temos mais uma de nossas armas: A tabela de contingência nos ajuda a cruzar duas categorias relacionadas — que no nosso caso são aldeias e estilos de luta —

hinata lutando

Utilizaremos um heatmap (mapa de calor) para visualizar os dados brutos de uma forma mais intuitiva. Nele podemos visualizar através de tons, onde quanto mais escuro mais shinobis existem naquela combinação.

plt.figure(figsize=(10,6))
sns.heatmap(
    dados, 
    annot=True, 
    fmt='d', 
    cmap='TlordRd', 
    linewidth=0.5, 
    linecolor='#cccccc', 
    cbar_kws={'label': 'Quantidade de Shinobis'})
plt.title('Tabela de Contingência - Dados Brutos') 
plt.xlabel('Estilo de Luta')
plt.ylabel('Aldeia')
plt.tight_layout()
plt.show()
Enter fullscreen mode Exit fullscreen mode

tabela de contingencia

Calculando os perfis de linha e coluna - Qual é o estilo da aldeia?

Agora que podemos visualizar nosso dado de formas diferentes vamos começar a nos perguntar o que esse dado precisa nos dizer como pode ser visto abaixo:

  • Olhar para o perfil de linha é como perguntar: "De todos os shinobis da vila de Konoha, qual a porcentagem de cada estilo?", ou seja, nós dividismo cada valor pela soma da linha, essa informação nos aproxima do que seria o perfil de combate geral em cada aldeia.
  • Já olhar para o perfil de coluna nos diz justamente o contrário: "De todos os que preferem Taijutsu, quantos são de cada aldeia?", ou seja, olhamos para o perfil de um estilo específico visto por cada aldeia.

gaara vs rock lee

Para calcular o perfil de linha vamos realizar o calculo dos dados:

row_profile = dados.div(dados.sum(axis=1), axis=0)
print('Perfil de linha (proporção por aldeia)')
print(row_profile.round(3))
print('\nSoma por linha (deve ser 1.0):', row_profile.sum(axis=1).values)
Enter fullscreen mode Exit fullscreen mode

perfil de linha

Já para o perfil de coluna faremos o contrário como foi explicado:

col_profile = dados.div(dados.sum(axis=0), axis=1)
print('Perfil de coluna (proporção por estilo)')
print(col_profile.round(3))
print('\nSoma por coluna (deve ser 1.0):', col_profile.sum(axis=0).values)
Enter fullscreen mode Exit fullscreen mode

perfil de coluna

Visualizando o perfil de linha de maneira estruturada

Perfeito! Agora que entendemos o conceito e conseguimos visualizar nossos numeros brutos dos perfis, vamos visualiza-los em um gráfico:

CORES_ALDEIAS = {
'Konoha': '#F25C54',
'Suna': '#F2A83A',
'Kiri': '#3AA3F2',
'Ame': '#8E5CF2'
}

fig, ax = plt.subplots(figsize=(10,6))
x = np.arange(len(dados.colums))
width = 0.2

for idx, aldeia in enumerate(dados.index):
    ax.bar(x + idx * width, row_profile.loc[aldeia] * 100,
        width, label=aldeia, color=CORES_ALDEIAS[aldeia],
        edgecolor='white', linewidth=0.8)
    ax.set_title('Perfil de Linha - "Perfil de combate" de cada aldeia')
    ax.set_xlabel('Estilo de luta')
    ax.set_ylabel('Proporção (%)')
    ax.set_xticks(x + width * 1.5)
    ax.set_ylim(0, 42)
    ax.yaxis.set_major_formatter(plt.FuncFormatter(lambda v, _: f'{v:.0f}%'))
    plt.tight_layout()
    plt.show()
Enter fullscreen mode Exit fullscreen mode

perfil_de_linha

Teste QUI-Quadrado - Isso é uma coincidência ou não?

Um dos aspectos mais importantes em uma análise de dados é questionar nossa base com carater investigativo, dentro do nosso cenário devemos nos perguntar se há mesmo uma relação entre aldeias e estilos de luta ou se estamos atribuindo causalidade3 para dados correlacionados4.

3: Causalidade refere-se a uma relação de causa e efeito entre duas informações, onde o efeito de uma influencia diretamente o efeito da segunda.
4: Já a correlação mostra uma associação entre duas informações, mas não implica causalidade.

tsunade lendo

Para nos auxiliar nessa investigação vamos utilizar mais uma de nossas armas ninjas: O teste qui-quadrado, ele nos ajuda a fazer exatamente essa verificação onde se o p-valor for menor que 0,05 podemos dizer então que não há coincidência e que existe uma associação real entre as informações analisadas.

  • p < 0,05 - Existe associação significativa (não é coincidência)
  • p > 0,05 - Não há evidência suficiente de associação

Para informações mais detalhadas sobre o assunto, [leia o artigo](https://docs-scipy-org.translate.goog/doc/scipy-1.17.0/reference/generated/scipy.stats.chisquare.html?_x_tr_sl=en&_x_tr_tl=pt&_x_tr_hl=pt&_x_tr_pto=tc](https://docs-scipy-org.translate.goog/doc/scipy-1.17.0/reference/generated/scipy.stats.chisquare.html?_x_tr_sl=en&_x_tr_tl=pt&_x_tr_hl=pt&_x_tr_pto=tc )

chi2, p_value, dof, expected = chi2_contingency(dados)

print('Teste Qui-Quadrado')
print(f'Chi-Quadrado : {chi2:.4f}')
print(f'P-valor      : {p_value:.4f}')
print(f'Graus de lib.: {dof}')
print()

if p_value < 0.05:
    print('Resultado: p < 0,05 - Existe associação')
else:
    print('Resultado: p > 0,05 - Não existe associação')

print('Frequência Esperada (Se não houvesse associação)')
expected_df = pd.DataFrame(expected, index=dados.index, columns=dados.columns)
print(expected_df.round(2))
Enter fullscreen mode Exit fullscreen mode

qui quadrado

Resíduos padronizados - Quem está fora do esperado?

Após nossa análise investigativa temos a certeza que estamos no caminho certo! Sem a associação entre a aldeia e o estilo, as propoções seriam quase iguais em todos os lugares.

obito

Para continuar nossa análise devemos questionar novamente nossa base de dados e entender quem está fora do esperado. Para isso vamos conhecer mais uma de nossas armas ninjas: Os resíduos padronizados nos proporcionam uma medição dessa diferença: O que vemos de fato x O que veríamos se tudo fosse igual ou parecido.

Afim de calcular essa informação seguimos a formula de resíduo onde resíduo = (observado - esperado) / esperado (raiz quadrada de esperado).

n = dados.sum().sum()
row_sums = dados.sum(axis=1)
col_sums = dados.sum(axis=0)

residuals = pd.DataFrame(index=dados.index), columns=dados.columns, dtype=float)

for i in dados.index:
    for j in dados.columns:
        expected_ij = (row_sums[i] * col_sums[j]) / n
        residuals.loc[i, j] = dados.loc[i, j] - expected_ij) / np.sqrt(expected_ij)

print('Resíduos Padronizados)
print(residuals.round(3))
Enter fullscreen mode Exit fullscreen mode

residuos padronizados

Agora com nossos números brutos definidos, podemos visualiza-los em gráfico onde:

  • Valor positivo (azul): Mais do que o esperado, associação forte.
  • Valor negativo (vermelho): Menos do que o esperado. Fórmula: resíduo = (observado - esperado) / esperado(raiz quadrada de esperado)
plt.figure(figsize=(10,6))
sns.heatmap(residuals, annot=True, fmt='.2f', cmap='RdBu_r', center=0,
linewidths=0.5, linecolor='#cccccc',
cbar_ks={'label': 'Resíduo Padronizado'},
annot_kws={'size': 16, 'weight': 'bold'},
vmin=-1.8, vmax=1.8)
plt.title('Resíduos Padronizados\nAzul= Acima do esperado | Vermelho = Abaixo do esperado')
plt.xlabel('Estilo de Luta')
plt.ylabel('Aldeia')
plt.tight_layout()
plt.show()
Enter fullscreen mode Exit fullscreen mode

residuos padronizados

Decomposição SVD - Reduzindo o mundo em duas dimensões

Vamos imaginar o mapa do mundo de naruto:

mapa mundo ninja

Podemos observar nossa amostra com mil aldeias e mil estilos de luta. Dentro desse cenário é impossível de visualizar, concorda? Para isso a decomposição SVD ajuda a pegar todas essa informação e comprimi-la em duas dimensões, como se você fosse um cartógrafo e decidisse criar um mapa mais simplificado que ainda captura informações importantes.

Não perde tudo, só os detalhes menos relevantes. É como usar o sharingan: você foca no que importa.

kakashi sharingan

Em termos mais técnicos: A SVD (Singular Value Decomposition) é o coração matemático da CA (Correspondency Analysis). Para ela temos algumas etapas necessárias afim de aplica-la efetivamente:

  • 1. Normalizar a matriz
  • 2. Calcular a matriz
  • 3. Aplicar SVD para obter as coordenadas

Primeiro vamos normalizá-la:

P = dados.values / n
print('Matriz de Proporções (P)')
print(pd.DataFrame(P, index=dados.index, columns=dados.columns).round(3))
print(f'Soma total (deveser 1.0): {P.sum():.1f}')
Enter fullscreen mode Exit fullscreen mode

matriz de proporcoes

Em seguida calculamos a matriz Z e massas:

row_mass = P.sum(axis=1) # massa das linhas
col_mass = P.sum(axis=0) # massa das colunas

print ('Massa das linhas (aldeias):')
for aldeia, massa in zip(dados.index, row_mass):
    print(f'{aldeia}: {massa:.3f}')

print ('Massa das colunas (estilos):')
for estilo, massa in zip(dados.colums, col_mass):
print(f' {estilo}: {massa:.3f}')

# Matriz Z (resíduos padronizados pela massa)
Z = (P - np.outer(row_mass, col_mass)) / np.sqrt(np.outer(row_mass, col_mass))

print('Matriz Z (entrada SVD)')
print(pd.DataFrame(Z, index=dados.index, columns=dados.columns).round(4))
Enter fullscreen mode Exit fullscreen mode

massa e matriz Z

E por fim aplicamos a SVD de fato:

U,s, Vt = np.linalg.svd(Z, full_matrices=False)

print('Valores Singulares (s)')
for i, val in enumetate (s):
    print(f' s[{i}] = {val:.4f}')

# Coordenadas nas duas primeiras dimensões
row_coords = U[:, :2] * s[:2]
col_coords = Vt.T[:, :2] * s[:2]

# Inércia explicada por dimensão
inertia = s**2
explained_var = inertia / inertia.sum() * 100

print('Inércia explicada por dimensão')
for i, var in enumerate(explained_var):
    print(f' Dimensão {i+1}: {var:.2f}%')
print(f'\n Acumulada (Dim 1 + 2): {explained_var[0] + explained_var[1]:.2f}%')

print('Coordenadas das aldeias')
    coords_aldeias = pd.DataFrame(row_coords, index=dados.columns, columns=['Dim ', 'Dim 2'])
print(coords_aldeias.round(4))

print('Coordenadas dos estilos')
coords_estilos = pd.DataFrame(col_coords, index=dados.columns, columns=['Dim 1', 'Dim 2'])
print(coords_estilos.round(4))
Enter fullscreen mode Exit fullscreen mode

svd

Mapa de Correspondência (Biplot) - O mapa final do mundo do Naruto

Finalmente nos aproximamos ao final de nossa análise afim de podermos traçar as melhores estratégias para nossos queridos alunos!

rock lee naruto guy

Pontos próximos = relação forte.

Se Konoha e Taijutsu estão do mesmo lado do mapa, significa que a Konoha é conhecida por Taijutsu. Faz sentido, né? Rock Lee, Guy-Sensei, todos são de Konoha e são mestres de Taijutsu.

Pontos longe = relação fraca.

Se Kiri fica do outro lado, significa que os shinobis de Kiri não são tão voltados para Taijutsu.

A origem do mapa (centro) é o ponto neutro, sem associação forte com nada.

# maior residuo padronizado positivo

associacoes = {}
for aldeia in dados.index:
    estilo_mais_forte = residuals.loc[aldeia].idxmax()
    associacoes[aldeia] = estilo_mais_forte

CORES_ESTILOS = {}
for aldeia, estilo in associacoes.items():
    CORES_ESTILOS[estilo] = CORES_ALDEIAS[aldeia]
for estilo in dados.columns:
    if estilo in dados.columns:
        CORES_ESTILOS[estilo] = '#999999'

print(' Associações por resíduo mais forte: ')
for aldeia, estilo in associacoes.items():
    print(f' {aldeia} - {estilo} (resíduo: {residuals.loc[aldeia, estilo]:.2f})')

fig, ax = plt.subplots(figsize=(10,8))

# Linhas de referência
ax.axhline(0, color='#aaaaaa', linewidth=0.8, linestyle='--')
ax.axvline(0, color='#aaaaaa', linewidth=0.8, linestyle='--')

# Linhas conectando ao centro
for i in range(len(dados.index)):
    ax.plot([0, row_coords[i, 0]], [0, row_coords[i,1]],
        color=CORES_ALDEIAS[dados.columns[i]], linewidth=1.5, alpha=0.4)

# Aldeias (círculos)
for i, aldeia in enumerate(dados.index):
    ax.scatter(row_coords[i,0], row_coords[i,1],
        s=100, c= CORES_ALDEIAS[aldeia], edgecolors='black',
        linewidth=1.5, zorder=5)
    ax.annotate(aldeia, (row_coords[i,0], row_coords[i,1]),
        textcoords='offset points', xytext=(12,9),
        fontsize=13, fontweight='bold', color=CORES_ALDEIAS[aldeia])

# Estilos (quadrados)
for i, estilo in enumerate(dados.columns):
    ax.scatter(row_coords[i,0], row_coords[i,1],
        s=100, c=CORES_ALDEIAS[aldeia], edgecolors='black',
        linewidth=1.5, zorder=5)
    ax.annotate(aldeia,(row_coords[i,0], row_coords[i,1]),
        textcoords='offset points', xytext=(12,9),
        fontsize=13, fontweight='bold', color=CORES_ALDEIAS[aldeia])


# Legenda das associações
legend_handles = []
for aldeia, estilo in associacoes.items():
    legend_handles.append(
        plt.Line2D([0], [0], marker='o), color='w',
        markerfacecolor=CORES_ALDEIAS[aldeia], markersize=12,
        label=f'{aldeia}  {estilo}', linestyle='None')
        )
legend_handles.append(

plt.Line2D([0], [0], marker='o', color='w', markerfacecolor='#cccccc',

markersize=10, label='● = Aldeia', linestyle='None')

)

legend_handles.append(

plt.Line2D([0], [0], marker='s', color='w', markerfacecolor='#cccccc',

markersize=10, label='■ = Estilo', linestyle='None')

)

ax.legend(handles=legend_handles, loc='lower right', fontsize=10,

framealpha=0.95, title='Associações', title_fontsize=11)

ax.set_title('Mapa de Correspondência (Biplot)\nCores iguais = aldeia e estilo mais associados')

ax.set_xlabel(f'Dimensão 1 ({explained_var[0]:.1f}% da inércia)')

ax.set_ylabel(f'Dimensão 2 ({explained_var[1]:.1f}% da inércia)')

ax.set_title('Mapa de Correspondência (Biplot)\nCores iguais = aldeia e estilo mais associados')

ax.set_xlabel(f'Dimensão 1 ({explained_var[0]:.1f}% da inércia)')

ax.set_ylabel(f'Dimensão 2 ({explained_var[1]:.1f}% da inércia)')
Enter fullscreen mode Exit fullscreen mode

mapa de correspondencia (biplot)

Interpretação final

No final do mapa, você veria Konoha bem perto de Taijutsu ( Rock Lee e Guy), Kiri perto de Senjutsu (por exemplo), e assim por diante.
Cada cluster conta uma história sobre quem se parece com quem no mundo dos shinobis.
É basicamente isso. A Análise de Correspondência é um mapa que mostra quais categorias se parecem entre si, sem precisar olhar para uma tabela cheia de números!

naruto festa

Biblioteca Prince - Otimizando o tempo

Durante toda a análise, calculamos manualmente cada etapa: a matriz de proporções, as massas, a matriz Z, a decomposição SVD e as coordenadas.

Isso foi muito importante para entender o que acontece por baixo dos panos, como se você fosse um Sensei explicando cada movimento de uma técnica de luta aos seus Genins.

Mas imagina que agora você precisa executar a técnica o mais rápido possível para uma missão (Ou, talvez... A vila esteja sendo atacada e precisam saber com quem podem contar de cada Aldeia). É exatamente isso que a biblioteca PRINCE oferece, ela encapsula toda essa matemática em poucas linhas de código, assim como o Naruto não precisa pensar em cada passo do Rasenshuriken depois que já dominou a técnica.

O Prince entrega tudo pronto: coordenadas das aldeias, coordenadas dos estilos, inércia explicada e contribuições de cada ponto. É a diferença entre um genin aprendendo Taijutsu passo a passo e um jounin executando a técnica em um segundo.

A única desvantagem que percebo é que, assim como usar um jutsu poderoso sem entender sua origem, você perde a visibilidade do que acontece internamente. Por isso, entender a implementação manual primeiro é fundamental antes de partir para algo mais avançado como o Prince.

biblioteca prince

hinata rindo

Conclusão

Obrigada por ler até aqui! :)

Fontes:

Para ter acesso a todo código da análise sinta-se a vontade para se basear no repósitorio: https://github.com/surocham/ca_naruto

Agradecimento especial para @cherryramatis que me ajudou revisando este artigo. 💗💗💗

Top comments (2)

Collapse
 
cherryramatis profile image
Cherry Ramatis

MUITO criativa na didática, simplesmente incrível aprender sobre dados com você

Collapse
 
ricardo_yy_9fd1839c2dadc profile image
Ricardo Y.Y

Muito bacana sua análise