"""
============================================================================
MÓDULO DE ANÁLISIS BIVARIADO
============================================================================
Análisis de relaciones entre pares de variables.
Incluye correlaciones, tablas de contingencia y tests estadísticos.
Autor: Sistema de Física
Versión: 1.0.0
"""
import numpy as np
import pandas as pd
import plotly.express as px
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import streamlit as st
from scipy import stats
from scipy.stats import chi2_contingency, pearsonr, spearmanr, kendalltau
import seaborn as sns
import matplotlib.pyplot as plt
from typing import Dict, List, Tuple, Optional
import warnings
import os
warnings.filterwarnings('ignore')
class BivariateAnalyzer:
"""Analizador de relaciones bivariadas"""
def __init__(self, data: pd.DataFrame):
"""
Inicializa el analizador
Args:
data: DataFrame con los datos a analizar
"""
self.data = data
self.numeric_columns = data.select_dtypes(include=[np.number]).columns.tolist()
self.categorical_columns = data.select_dtypes(include=['object', 'category']).columns.tolist()
def analyze_numeric_vs_numeric(self, var1: str, var2: str) -> Dict:
"""
Análisis de relación entre dos variables numéricas
Args:
var1, var2: Nombres de las variables
Returns:
Diccionario con correlaciones y tests
"""
# Filtrar datos válidos
data_clean = self.data[[var1, var2]].dropna()
if len(data_clean) < 3:
return {'error': 'Insuficientes datos válidos'}
x = data_clean[var1]
y = data_clean[var2]
# Correlaciones
pearson_corr, pearson_p = pearsonr(x, y)
spearman_corr, spearman_p = spearmanr(x, y)
kendall_corr, kendall_p = kendalltau(x, y)
# Regresión lineal
slope, intercept, r_value, p_value, std_err = stats.linregress(x, y)
results = {
'n_observations': len(data_clean),
'missing_pairs': len(self.data) - len(data_clean),
'correlations': {
'pearson': {'r': pearson_corr, 'p_value': pearson_p},
'spearman': {'r': spearman_corr, 'p_value': spearman_p},
'kendall': {'r': kendall_corr, 'p_value': kendall_p}
},
'linear_regression': {
'slope': slope,
'intercept': intercept,
'r_squared': r_value**2,
'p_value': p_value,
'std_error': std_err
}
}
return results
def analyze_categorical_vs_categorical(self, var1: str, var2: str) -> Dict:
"""
Análisis de relación entre dos variables categóricas
Args:
var1, var2: Nombres de las variables
Returns:
Diccionario con tabla de contingencia y tests
"""
# Crear tabla de contingencia
contingency_table = pd.crosstab(self.data[var1], self.data[var2])
# Test Chi-cuadrado
chi2, p_value, dof, expected = chi2_contingency(contingency_table)
# V de Cramér
n = contingency_table.sum().sum()
cramers_v = np.sqrt(chi2 / (n * (min(contingency_table.shape) - 1)))
results = {
'contingency_table': contingency_table,
'chi2_test': {
'statistic': chi2,
'p_value': p_value,
'degrees_of_freedom': dof,
'expected_frequencies': expected
},
'cramers_v': cramers_v,
'association_strength': self._interpret_cramers_v(cramers_v)
}
return results
def analyze_numeric_vs_categorical(self, numeric_var: str, categorical_var: str) -> Dict:
"""
Análisis de relación entre variable numérica y categórica
Args:
numeric_var: Variable numérica
categorical_var: Variable categórica
Returns:
Diccionario con estadísticas por grupo y tests
"""
# Estadísticas descriptivas por grupo
grouped_stats = self.data.groupby(categorical_var)[numeric_var].agg([
'count', 'mean', 'median', 'std', 'min', 'max', 'skew'
]).round(4)
# Test ANOVA (paramétrico)
groups = [group[numeric_var].dropna() for name, group in self.data.groupby(categorical_var)]
if len(groups) >= 2 and all(len(group) >= 2 for group in groups):
f_stat, anova_p = stats.f_oneway(*groups)
# Test Kruskal-Wallis (no paramétrico)
kw_stat, kw_p = stats.kruskal(*groups)
else:
f_stat = anova_p = kw_stat = kw_p = np.nan
results = {
'grouped_statistics': grouped_stats,
'anova_test': {
'f_statistic': f_stat,
'p_value': anova_p
},
'kruskal_wallis_test': {
'statistic': kw_stat,
'p_value': kw_p
},
'groups_info': {
'n_groups': len(groups),
'group_sizes': [len(group) for group in groups]
}
}
return results
def _interpret_cramers_v(self, cramers_v: float) -> str:
"""Interpreta el valor de V de Cramér"""
if cramers_v < 0.1:
return "Asociación muy débil"
elif cramers_v < 0.3:
return "Asociación débil"
elif cramers_v < 0.5:
return "Asociación moderada"
else:
return "Asociación fuerte"
def create_correlation_matrix(self, method: str = 'pearson') -> go.Figure:
"""
Crea matriz de correlación interactiva
Args:
method: 'pearson', 'spearman', o 'kendall'
Returns:
Figura de Plotly
"""
# Calcular matriz de correlación
if method == 'pearson':
corr_matrix = self.data[self.numeric_columns].corr(method='pearson')
elif method == 'spearman':
corr_matrix = self.data[self.numeric_columns].corr(method='spearman')
elif method == 'kendall':
corr_matrix = self.data[self.numeric_columns].corr(method='kendall')
# Crear heatmap
fig = go.Figure(data=go.Heatmap(
z=corr_matrix.values,
x=corr_matrix.columns,
y=corr_matrix.columns,
colorscale='RdBu',
zmid=0,
text=np.round(corr_matrix.values, 3),
texttemplate='%{text}',
textfont={"size": 10},
showscale=True,
colorbar=dict(title=f"Correlación {method.title()}")
))
fig.update_layout(
title=f"Matriz de Correlación - {method.title()}",
template="plotly_white",
height=600,
width=800
)
return fig
def create_scatter_plot(self, var1: str, var2: str, color_var: str = None) -> go.Figure:
"""
Crea gráfico de dispersión con línea de regresión
Args:
var1, var2: Variables para ejes X e Y
color_var: Variable para colorear puntos (opcional)
Returns:
Figura de Plotly
"""
# Filtrar datos válidos
if color_var:
data_clean = self.data[[var1, var2, color_var]].dropna()
else:
data_clean = self.data[[var1, var2]].dropna()
# Crear scatter plot
if color_var and color_var in self.categorical_columns:
fig = px.scatter(
data_clean,
x=var1,
y=var2,
color=color_var,
trendline="ols",
title=f"Relación: {var1} vs {var2}",
labels={var1: var1, var2: var2}
)
else:
fig = px.scatter(
data_clean,
x=var1,
y=var2,
trendline="ols",
title=f"Relación: {var1} vs {var2}",
labels={var1: var1, var2: var2}
)
fig.update_layout(
template="plotly_white",
height=500
)
return fig
def create_contingency_heatmap(self, var1: str, var2: str) -> go.Figure:
"""
Crea heatmap de tabla de contingencia
Args:
var1, var2: Variables categóricas
Returns:
Figura de Plotly
"""
# Crear tabla de contingencia
contingency = pd.crosstab(self.data[var1], self.data[var2])
# Crear heatmap
fig = go.Figure(data=go.Heatmap(
z=contingency.values,
x=contingency.columns,
y=contingency.index,
colorscale='Blues',
text=contingency.values,
texttemplate='%{text}',
textfont={"size": 12},
showscale=True,
colorbar=dict(title="Frecuencia")
))
fig.update_layout(
title=f"Tabla de Contingencia: {var1} vs {var2}",
xaxis_title=var2,
yaxis_title=var1,
template="plotly_white",
height=500
)
return fig
def create_grouped_boxplot(self, numeric_var: str, categorical_var: str) -> go.Figure:
"""
Crea boxplot agrupado
Args:
numeric_var: Variable numérica
categorical_var: Variable categórica
Returns:
Figura de Plotly
"""
fig = px.box(
self.data,
x=categorical_var,
y=numeric_var,
title=f"Distribución de {numeric_var} por {categorical_var}",
color=categorical_var
)
fig.update_layout(
template="plotly_white",
height=500,
showlegend=False
)
return fig
def render_bivariate_analysis():
"""Renderiza el módulo de análisis bivariado en Streamlit"""
st.title("📊 Análisis Bivariado")
st.markdown("### *Análisis de relaciones entre pares de variables*")
# Verificar datos
if not os.path.exists("data/processed/datos_credito_hipotecario_realista.csv"):
st.error("❌ No hay datos disponibles. Ve a 'Generar Datos' primero.")
return
# Cargar datos
@st.cache_data
def load_data():
return pd.read_csv("data/processed/datos_credito_hipotecario_realista.csv")
df = load_data()
st.success(f"✅ Datos cargados: {len(df):,} registros, {len(df.columns)} variables")
# Crear analizador
analyzer = BivariateAnalyzer(df)
# Tabs principales
tab1, tab2, tab3, tab4 = st.tabs([
"🔗 Correlaciones",
"📈 Numérica vs Numérica",
"📊 Categórica vs Categórica",
"📦 Numérica vs Categórica"
])
# ==================== TAB 1: CORRELACIONES ====================
with tab1:
st.subheader("🔗 Matriz de Correlación")
col1, col2 = st.columns([1, 3])
with col1:
correlation_method = st.selectbox(
"Método de correlación:",
options=['pearson', 'spearman', 'kendall'],
help="Pearson: lineal, Spearman: monotónica, Kendall: ordinal"
)
min_corr = st.slider(
"Correlación mínima a mostrar:",
min_value=0.0,
max_value=1.0,
value=0.0,
step=0.05,
help="Filtrar correlaciones débiles"
)
with col2:
if st.button("📊 Generar Matriz de Correlación", type="primary"):
with st.spinner("📊 Calculando correlaciones..."):
fig_corr = analyzer.create_correlation_matrix(correlation_method)
st.plotly_chart(fig_corr, use_container_width=True)
# Tabla de correlaciones más altas
corr_matrix = df[analyzer.numeric_columns].corr(method=correlation_method)
# Extraer correlaciones únicas
mask = np.triu(np.ones_like(corr_matrix, dtype=bool), k=1)
corr_pairs = corr_matrix.where(mask).stack().reset_index()
corr_pairs.columns = ['Variable 1', 'Variable 2', 'Correlación']
corr_pairs['Correlación Abs'] = abs(corr_pairs['Correlación'])
# Filtrar y ordenar
high_corr = corr_pairs[
corr_pairs['Correlación Abs'] >= min_corr
].sort_values('Correlación Abs', ascending=False)
st.subheader(f"🔝 Top Correlaciones (|r| ≥ {min_corr})")
st.dataframe(
high_corr.head(20)[['Variable 1', 'Variable 2', 'Correlación']],
use_container_width=True,
hide_index=True
)
# ==================== TAB 2: NUMÉRICA VS NUMÉRICA ====================
with tab2:
st.subheader("📈 Análisis: Numérica vs Numérica")
col1, col2, col3 = st.columns(3)
with col1:
var1 = st.selectbox(
"Variable X:",
options=analyzer.numeric_columns,
key="num_var1"
)
with col2:
var2 = st.selectbox(
"Variable Y:",
options=analyzer.numeric_columns,
key="num_var2"
)
with col3:
color_var = st.selectbox(
"Colorear por (opcional):",
options=[None] + analyzer.categorical_columns,
key="color_var"
)
if var1 and var2 and var1 != var2:
# Análisis estadístico
results = analyzer.analyze_numeric_vs_numeric(var1, var2)
if 'error' not in results:
# Mostrar métricas
col1, col2, col3, col4 = st.columns(4)
with col1:
st.metric("Correlación Pearson", f"{results['correlations']['pearson']['r']:.3f}")
with col2:
st.metric("Correlación Spearman", f"{results['correlations']['spearman']['r']:.3f}")
with col3:
st.metric("R² (Regresión)", f"{results['linear_regression']['r_squared']:.3f}")
with col4:
st.metric("Observaciones", f"{results['n_observations']:,}")
# Gráfico de dispersión
fig_scatter = analyzer.create_scatter_plot(var1, var2, color_var)
st.plotly_chart(fig_scatter, use_container_width=True)
# Interpretación
pearson_r = results['correlations']['pearson']['r']
if abs(pearson_r) < 0.3:
strength = "débil"
elif abs(pearson_r) < 0.7:
strength = "moderada"
else:
strength = "fuerte"
direction = "positiva" if pearson_r > 0 else "negativa"
st.info(f"💡 **Interpretación:** Correlación {strength} {direction} (r = {pearson_r:.3f})")
else:
st.error(results['error'])
# ==================== TAB 3: CATEGÓRICA VS CATEGÓRICA ====================
with tab3:
st.subheader("📊 Análisis: Categórica vs Categórica")
col1, col2 = st.columns(2)
with col1:
cat_var1 = st.selectbox(
"Variable 1:",
options=analyzer.categorical_columns,
key="cat_var1"
)
with col2:
cat_var2 = st.selectbox(
"Variable 2:",
options=analyzer.categorical_columns,
key="cat_var2"
)
if cat_var1 and cat_var2 and cat_var1 != cat_var2:
# Análisis estadístico
results = analyzer.analyze_categorical_vs_categorical(cat_var1, cat_var2)
# Mostrar métricas
col1, col2, col3 = st.columns(3)
with col1:
st.metric("Chi² Estadístico", f"{results['chi2_test']['statistic']:.3f}")
with col2:
st.metric("P-valor", f"{results['chi2_test']['p_value']:.6f}")
with col3:
st.metric("V de Cramér", f"{results['cramers_v']:.3f}")
# Interpretación
if results['chi2_test']['p_value'] < 0.05:
st.success("✅ Existe asociación significativa (p < 0.05)")
else:
st.warning("⚠️ No hay asociación significativa (p ≥ 0.05)")
st.info(f"💡 **Fuerza de asociación:** {results['association_strength']}")
# Visualizaciones
col1, col2 = st.columns(2)
with col1:
# Heatmap de contingencia
fig_heatmap = analyzer.create_contingency_heatmap(cat_var1, cat_var2)
st.plotly_chart(fig_heatmap, use_container_width=True)
with col2:
# Gráfico de barras apiladas
contingency = results['contingency_table']
fig_stacked = go.Figure()
for col in contingency.columns:
fig_stacked.add_trace(go.Bar(
name=str(col),
x=contingency.index,
y=contingency[col]
))
fig_stacked.update_layout(
title=f"Distribución: {cat_var1} vs {cat_var2}",
xaxis_title=cat_var1,
yaxis_title="Frecuencia",
barmode='stack',
template="plotly_white",
height=500
)
st.plotly_chart(fig_stacked, use_container_width=True)
# Tabla de contingencia
st.subheader("📋 Tabla de Contingencia")
st.dataframe(results['contingency_table'], use_container_width=True)
# ==================== TAB 4: NUMÉRICA VS CATEGÓRICA ====================
with tab4:
st.subheader("📦 Análisis: Numérica vs Categórica")
col1, col2 = st.columns(2)
with col1:
num_var = st.selectbox(
"Variable Numérica:",
options=analyzer.numeric_columns,
key="num_var_mixed"
)
with col2:
cat_var = st.selectbox(
"Variable Categórica:",
options=analyzer.categorical_columns,
key="cat_var_mixed"
)
if num_var and cat_var:
# Análisis estadístico
results = analyzer.analyze_numeric_vs_categorical(num_var, cat_var)
# Mostrar métricas de tests
col1, col2, col3 = st.columns(3)
with col1:
st.metric("F-estadístico (ANOVA)", f"{results['anova_test']['f_statistic']:.3f}")
with col2:
st.metric("P-valor ANOVA", f"{results['anova_test']['p_value']:.6f}")
with col3:
st.metric("Grupos", results['groups_info']['n_groups'])
# Interpretación de tests
if results['anova_test']['p_value'] < 0.05:
st.success("✅ Diferencias significativas entre grupos (ANOVA p < 0.05)")
else:
st.warning("⚠️ No hay diferencias significativas entre grupos (ANOVA p ≥ 0.05)")
# Visualizaciones
col1, col2 = st.columns(2)
with col1:
# Boxplot agrupado
fig_box = analyzer.create_grouped_boxplot(num_var, cat_var)
st.plotly_chart(fig_box, use_container_width=True)
with col2:
# Violin plot
fig_violin = px.violin(
df,
x=cat_var,
y=num_var,
title=f"Distribución de {num_var} por {cat_var}",
box=True
)
fig_violin.update_layout(
template="plotly_white",
height=500
)
st.plotly_chart(fig_violin, use_container_width=True)
# Estadísticas por grupo
st.subheader("📊 Estadísticas por Grupo")
st.dataframe(results['grouped_statistics'], use_container_width=True)
[documentos]
def render_bivariate_module():
"""Función principal para renderizar el módulo bivariado"""
render_bivariate_analysis()
if __name__ == "__main__":
print("Módulo de análisis bivariado cargado correctamente")