---
title: "Análisis Exploratorio Multivariado en la Práctica: Caso Palmer Penguins"
pagetitle: "Análisis Exploratorio Multivariado en la Práctica: Caso Palmer Penguins"
open-graph:
description: "Aplica los fundamentos del análisis multivariado al dataset público Palmer Penguins con un caso práctico completo en Python y pandas."
twitter-card:
description: "Aplica los fundamentos del análisis multivariado al dataset público Palmer Penguins con un caso práctico completo en Python y pandas."
description: "Aplica los fundamentos del análisis multivariado al dataset público Palmer Penguins con un caso práctico completo en Python y pandas."
author: "Diego Villalba"
date: today
lang: es
format:
html:
toc: true
toc-depth: 3
number-sections: true
code-fold: true
code-tools: true
code-summary: "Mostrar código"
fig-align: center
theme: cosmo
pdf:
toc: true
toc-depth: 3
number-sections: true
execute:
echo: true
warning: false
message: false
cache: false
jupyter: python3
bibliography: [../referencias_eda.bib, ../../../references.bib]
crossref:
fig-title: "Figura"
tbl-title: "Tabla"
eq-prefix: "Ec."
---
```{python}
#| include: false
import numpy as np
import pandas as pd
import scipy.stats as stats
import plotly.graph_objects as go
import plotly.express as px
from plotly.subplots import make_subplots
from sklearn.covariance import MinCovDet
import warnings
warnings.filterwarnings('ignore')
```
## Contexto del problema y datos de estudio
En el capítulo anterior ([Análisis Exploratorio Multivariado: De la Estadística Descriptiva al Espacio Multidimensional](2-11-analisis-multivariado.qmd)) establecimos los fundamentos matemáticos y conceptuales para analizar datos en $\mathbb{R}^p$: el vector de medias, la mediana geométrica de Fermat-Weber, la matriz de covarianza, la varianza generalizada de Wilks, la distancia de Mahalanobis, la detección robusta de anomalías (MCD), la Paradoja de Simpson y la correlación parcial a través de la matriz de precisión.
En este capítulo desarrollaremos un **caso práctico de principio a fin utilizando Python y pandas**, sobre un conjunto de datos real, público y ampliamente utilizado en la comunidad científica: el dataset de los **Pingüinos de Palmer** (*Palmer Archipelago Penguins*), recolectado por la Dra. Kristen Gorman y la Estación de Investigación Palmer LTER en la Antártida [@gorman2014structural].
::: {.callout-note appearance="minimal"}
### Disponibilidad y descarga de los datos
El conjunto de datos es completamente público, ligero y reproducible:
- **Enlace de descarga directa (CSV público oficial)**: [Descargar `penguins.csv` desde GitHub](https://raw.githubusercontent.com/mwaskom/seaborn-data/master/penguins.csv){target="_blank"}
- **Copia servida por el libro**: [Descargar `penguins.csv` local](../../../data/penguins.csv)
El archivo contiene 344 observaciones correspondientes a pingüinos adultos observados entre 2007 y 2009 en tres islas del archipiélago Palmer.
:::
Las cuatro variables morfométricas continuas que utilizaremos para nuestro espacio multivariado $\mathbb{R}^4$ son:
| Variable | Nombre en inglés | Unidad | Descripción biológica |
|---|---|:---:|---|
| $X_1$ | `bill_length_mm` | mm | Longitud del culmen (cresta dorsal superior del pico) |
| $X_2$ | `bill_depth_mm` | mm | Profundidad del culmen (grosor vertical del pico en su base) |
| $X_3$ | `flipper_length_mm` | mm | Longitud de la aleta del ave |
| $X_4$ | `body_mass_g` | g | Masa corporal total del pingüino |
Además, disponemos de variables categóricas clave para contrastar hallazgos: `species` (*Adelie*, *Chinstrap*, *Gentoo*), `island` (*Biscoe*, *Dream*, *Torgersen*) y `sex` (*MALE*, *FEMALE*).
---
## Paso 1: Ingesta, limpieza y exploración univariada en `pandas`
Comenzamos cargando los datos en un DataFrame de `pandas`. Implementamos una carga con respaldo automático: intentamos leer desde la URL pública oficial y, en caso de trabajar sin conexión a internet, recurrimos a la copia local.
```{python}
#| code-fold: false
# URL pública y respaldo local
url_publica = "https://raw.githubusercontent.com/mwaskom/seaborn-data/master/penguins.csv"
url_local = "../../../docs/data/penguins.csv"
try:
df_raw = pd.read_csv(url_publica)
except Exception:
df_raw = pd.read_csv(url_local)
# Definir variables cuantitativas de análisis
features = ['bill_length_mm', 'bill_depth_mm', 'flipper_length_mm', 'body_mass_g']
# Limpieza: eliminar registros incompletos en las variables numéricas clave
df = df_raw.dropna(subset=features).copy().reset_index(drop=True)
print(f"Dimensiones tras limpiar registros nulos: {df.shape[0]} filas y {df.shape[1]} columnas.")
df[features + ['species', 'sex']].head()
```
### Estadísticas descriptivas univariadas
Obtenemos el resumen univariado tradicional mediante `df[features].describe()`:
```{python}
#| code-fold: false
resumen_uni = df[features].describe().round(2)
resumen_uni
```
::: {.callout-important}
### Interpretación de resultados: La trampa de las escalas
Observe detenidamente la fila `std` (desviación estándar) y los rangos `min`-`max`:
1. `body_mass_g` tiene una media de **4,201.75 g** y una desviación estándar de **801.95 g**, con valores que oscilan entre 2,700 y 6,300 g.
2. `bill_depth_mm` tiene una media de **17.15 mm** y una desviación estándar de apenas **1.98 mm**.
Si cometiéramos el error de calcular distancias euclidianas directamente sobre estas columnas sin estandarizar, una diferencia de apenas $100\text{ g}$ de peso tendría un peso cuadrático de $(100)^2 = 10,000$, mientras que una variación morfológica de $2\text{ mm}$ en el pico aportaría $(2)^2 = 4$. **La masa corporal absorbería el 99.96% de la variabilidad calculada**, dejando al algoritmo completamente ciego ante las proporciones del pico y las aletas.
Esto demuestra por qué el análisis multivariado riguroso exige estandarizar por escala o recurrir a métricas estadísticas como la distancia de Mahalanobis.
:::
---
## Paso 2: Localización multivariada: Centro de gravedad vs. Centro robusto
En $\mathbb{R}^4$, ¿cuál es el "pingüino promedio"?
En el capítulo 2-11 vimos que el **vector de medias** $\bar{\mathbf{x}}$ es el centro de gravedad afín, mientras que la **mediana geométrica** (o de Fermat-Weber) es el punto que minimiza la suma de distancias euclidianas directas, ofreciendo resistencia ante observaciones extremas sin sufrir la falta de equivarianza rotacional de la mediana por componentes.
Calculamos el vector de medias con `pandas` y aproximamos la mediana geométrica con el algoritmo iterativo de Weiszfeld:
```{python}
#| code-fold: false
# 1. Vector de medias muestral
vector_medias = df[features].mean()
# 2. Mediana marginal (por componentes)
mediana_marginal = df[features].median()
# 3. Mediana geométrica de Fermat-Weber (algoritmo de Weiszfeld)
def weiszfeld_median(X, eps=1e-6, max_iter=300):
y = np.mean(X, axis=0)
for _ in range(max_iter):
dist = np.linalg.norm(X - y, axis=1)
dist = np.where(dist == 0, eps, dist)
weights = 1.0 / dist
y_nuevo = np.sum(X * weights[:, np.newaxis], axis=0) / np.sum(weights)
if np.linalg.norm(y_nuevo - y) < eps:
break
y = y_nuevo
return y
# Estandarizamos temporalmente para que la optimización espacial no esté sesgada por unidades
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
X_escalado = scaler.fit_transform(df[features].values)
# Mediana geométrica en espacio estandarizado y revertida a la escala original
med_geom_std = weiszfeld_median(X_escalado)
mediana_geometrica = scaler.inverse_transform(med_geom_std.reshape(1, -1))[0]
# Comparación en DataFrame
df_localizacion = pd.DataFrame({
'Variable': features,
'Vector de Medias': vector_medias.values.round(2),
'Mediana Marginal': mediana_marginal.values.round(2),
'Mediana Geométrica': mediana_geometrica.round(2),
'Diferencia (Media - Med. Geom)': (vector_medias.values - mediana_geometrica).round(2)
})
df_localizacion
```
::: {.callout-note}
### Interpretación de resultados: Asimetría en la masa corporal
Observe la fila `body_mass_g`:
- El **vector de medias** reporta **4,201.75 g**.
- La **mediana geométrica** reporta **4,082.90 g**.
- La diferencia es de **+118.85 g**.
Esta discrepancia positiva revela que la distribución conjunta presenta una **asimetría hacia la derecha** (colas pesadas superiores) en la masa corporal. La presencia de ejemplares de la especie *Gentoo* (notablemente más corpulentos) "jala" al centroide aritmético hacia arriba. La mediana geométrica representa con mayor fidelidad el núcleo denso y típico de la población general.
:::
---
## Paso 3: Dispersión multivariada: Covarianza, Varianza Total y de Wilks
La dispersión conjunta no puede resumirse en una sola desviación estándar. Calculamos la **matriz de covarianza muestral** $\mathbf{S}$ y la **matriz de correlación de Pearson** $\mathbf{R}$ con `pandas`:
```{python}
#| code-fold: false
# Matriz de covarianza
S_mat = df[features].cov()
# Matriz de correlación de Pearson
R_mat = df[features].corr()
# Visualización con Plotly
fig_corr = go.Figure(data=go.Heatmap(
z=R_mat.values,
x=features,
y=features,
colorscale='RdBu_r',
zmin=-1, zmax=1,
text=R_mat.values.round(3),
texttemplate='%{text}',
textfont=dict(size=13),
colorbar=dict(title='Correlación (r)')
))
fig_corr.update_layout(
title="Matriz de Correlación de Pearson — Variables Morfométricas",
height=450,
template="plotly_white"
)
fig_corr.show()
```
### Resúmenes escalares de variabilidad
Reducimos la matriz de covarianza estandarizada $\mathbf{R}$ a sus dos resúmenes escalares fundamentales:
```{python}
#| code-fold: false
# Trabajamos sobre la matriz de correlación R (covarianza estandarizada)
# para evitar que la unidad de gramos eclipse el determinante
traza_R = np.trace(R_mat.values)
det_R = np.linalg.det(R_mat.values)
autovalores, _ = np.linalg.eigh(R_mat.values)
print(f"Varianza Total (Traza de R): {traza_R:.2f} (Siempre igual a p={len(features)})")
print(f"Varianza Generalizada de Wilks (|R|): {det_R:.4f}")
print(f"Autovalores de R: {np.sort(autovalores)[::-1].round(3)}")
```
::: {.callout-note}
### Interpretación de resultados: La reducción del hipervolumen
1. **Fuerte colinealidad morfológica**: La correlación entre longitud de aleta (`flipper_length_mm`) y masa corporal (`body_mass_g`) es de **$r = 0.87$**. Las aves con aletas más grandes son sistemáticamente más pesadas.
2. **Volumen hiperelipsoidal colapsado**: Si las 4 variables fueran perfectamente independientes, el determinante de la matriz de correlación sería $|\mathbf{R}| = 1.0$. En nuestro caso, el determinante es **$|\mathbf{R}| \approx 0.0825$**.
3. **Significado geométrico**: La varianza generalizada de Wilks nos indica que el hiperelipsoide de 4 dimensiones ocupa **menos del 9% del volumen** que ocuparía una nube de variables no correlacionadas. La nube de datos está fuertemente aplanada a lo largo del eje conjunto aleta-peso.
:::
---
## Paso 4: La Paradoja de Simpson en acción
Examinemos con detenimiento una de las correlaciones más desconcertantes de la matriz anterior: la relación entre la longitud del pico (`bill_length_mm`) y su profundidad (`bill_depth_mm`).
- En la matriz global, la correlación de Pearson es **negativa**: **$r = -0.235$**.
- ¿Significa esto que cuanto más largo es el pico de un pingüino, más delgado y frágil se vuelve?
Comprobemos qué ocurre cuando estratificamos los datos por la variable biológica `species` utilizando `groupby` en `pandas`:
```{python}
#| code-fold: false
# Correlación por especie en pandas
corr_por_especie = df.groupby('species')[['bill_length_mm', 'bill_depth_mm']].apply(
lambda g: g.corr().iloc[0, 1]
).reset_index(name='Correlacion_Pico')
corr_por_especie
```
¡Los signos se han invertido por completo!
- En **Adelie**: $r = +0.39$ (positiva)
- En **Chinstrap**: $r = +0.65$ (fuertemente positiva)
- En **Gentoo**: $r = +0.64$ (fuertemente positiva)
Visualicemos esta discrepancia con una gráfica interactiva de dispersión:
```{python}
#| code-fold: true
#| fig-align: center
fig_simpson = px.scatter(
df, x='bill_length_mm', y='bill_depth_mm', color='species',
color_discrete_map={'Adelie': '#ff7f0e', 'Chinstrap': '#2ca02c', 'Gentoo': '#1f77b4'},
labels={'bill_length_mm': 'Longitud del Pico (mm)', 'bill_depth_mm': 'Profundidad del Pico (mm)', 'species': 'Especie'},
trendline='ols',
title="Paradoja de Simpson: La correlación global (r = -0.24) contradice la tendencia interna (r > 0)"
)
# Añadir línea de tendencia global en color negro discontinuo
x_vals = np.linspace(df['bill_length_mm'].min(), df['bill_length_mm'].max(), 100)
slope_global, intercept_global, _, _, _ = stats.linregress(df['bill_length_mm'], df['bill_depth_mm'])
y_vals = slope_global * x_vals + intercept_global
fig_simpson.add_trace(go.Scatter(
x=x_vals, y=y_vals,
mode='lines',
line=dict(color='black', width=2.5, dash='dash'),
name='Tendencia Global (r = -0.24)'
))
fig_simpson.update_layout(height=480, template="plotly_white")
fig_simpson.show()
```
::: {.callout-tip}
### Interpretación analítica: Por qué ocurre la inversión
Esta es una manifestación paradigmática de la **Paradoja de Simpson** [@simpson1951interpretation]:
1. **Diferencias entre especies**:
- Los pingüinos **Adelie** tienen picos cortos pero gruesos (poca longitud, mucha profundidad).
- Los pingüinos **Gentoo** tienen picos largos pero esbeltos (mucha longitud, poca profundidad).
- Los pingüinos **Chinstrap** tienen picos largos y gruesos.
2. **La falacia de mezclar poblaciones**: Al juntar todas las especies en una sola bolsa, el grupo *Gentoo* (abajo a la derecha) y el grupo *Adelie* (arriba a la izquierda) crean una pendiente aparente negativa.
3. **La realidad biológica y física**: *Dentro de cada especie*, la alometría natural se cumple: a mayor tamaño del pico, mayor es tanto su longitud como su grosor.
Si un analista se quedara con el coeficiente global $r=-0.235$ y lo reportara a un equipo de investigación o lo utilizara para construir reglas de negocio, llegaría a conclusiones falsas.
:::
---
## Paso 5: Correlación parcial y matriz de precisión
¿Cómo podemos descubrir la verdadera relación entre longitud y profundidad de pico sin tener que estratificar manualmente por cada variable de control?
La respuesta analítica es la **correlación parcial**, calculada a partir de la **matriz de precisión** $\boldsymbol{\Theta} = \mathbf{S}^{-1}$ o $\mathbf{R}^{-1}$. Al invertir la matriz de correlación, condicionamos la relación entre cada par de variables sobre el efecto conjunto de todas las demás (en este caso, controlando simultáneamente por masa corporal y longitud de aleta):
```{python}
#| code-fold: false
# 1. Inversa de la matriz de correlación (Matriz de Precisión)
theta_mat = np.linalg.inv(R_mat.values)
# 2. Correlaciones parciales: r_ij.resto = - theta_ij / sqrt(theta_ii * theta_jj)
d_diag = np.sqrt(np.diag(theta_mat))
pcorr_mat = -theta_mat / np.outer(d_diag, d_diag)
np.fill_diagonal(pcorr_mat, 1.0)
df_pcorr = pd.DataFrame(pcorr_mat, index=features, columns=features).round(3)
df_pcorr
```
::: {.callout-note}
### Interpretación de resultados: El efecto desenmascarado
Compare el cruce `bill_length_mm` y `bill_depth_mm`:
- **Correlación de Pearson global**: **$-0.235$**
- **Correlación Parcial (controlando por aleta y peso)**: **$+0.354$**
Al eliminar el efecto confusor del tamaño corporal general del ave (representado por el peso y la aleta), la correlación entre la longitud y profundidad del pico **cambia de signo y se vuelve positiva ($+0.35$)**. La matriz de precisión nos permite aislar el vínculo morfológico directo sin necesidad de conocer de antemano las especies.
:::
---
## Paso 6: Detección de outliers multivariados con Distancia de Mahalanobis
¿Existen pingüinos con medidas atípicas en este ecosistema?
Un pingüino puede tener una longitud de pico de 45 mm (normal) y una profundidad de 18 mm (normal). Pero, ¿es normal esa combinación para un ave de 3,200 g?
Calculamos la **distancia de Mahalanobis clásica** ($D_M^2$) y la **distancia de Mahalanobis robusta** mediante el estimador del **Determinante de Covarianza Mínima (MCD)** de Rousseeuw [@rousseeuw1984; @rousseeuw1999fastmcd]:
```{python}
#| code-fold: false
X = df[features].values
p = len(features)
# 1. Mahalanobis Clásica
mean_emp = np.mean(X, axis=0)
cov_emp = np.cov(X, rowvar=False)
inv_cov_emp = np.linalg.inv(cov_emp)
diff_emp = X - mean_emp
dm2_clasica = np.sum(diff_emp @ inv_cov_emp * diff_emp, axis=1)
# 2. Mahalanobis Robusta (MinCovDet)
mcd = MinCovDet(random_state=42).fit(X)
dm2_robusta = mcd.mahalanobis(X)
# 3. Umbrales teóricos Chi-cuadrado para p = 4 variables
cutoff_975 = stats.chi2.ppf(0.975, df=p) # Umbral 97.5%: ~11.14
cutoff_99 = stats.chi2.ppf(0.99, df=p) # Umbral 99.0%: ~13.28
df['DM2_Clasica'] = dm2_clasica
df['DM2_Robusta'] = dm2_robusta
df['Outlier_Clasico'] = df['DM2_Clasica'] > cutoff_99
df['Outlier_Robusto'] = df['DM2_Robusta'] > cutoff_99
print(f"Umbral Chi² al 99% (p={p}): {cutoff_99:.2f}")
print(f"Anomalías detectadas con estimador clásico: {df['Outlier_Clasico'].sum()}")
print(f"Anomalías detectadas con estimador robusto (MCD): {df['Outlier_Robusto'].sum()}")
```
Visualizamos la comparación entre ambas distancias para ilustrar el **efecto máscara (*masking*)**:
```{python}
#| code-fold: true
#| fig-align: center
fig_out = go.Figure()
# Puntos normales
mask_norm = ~df['Outlier_Robusto']
fig_out.add_trace(go.Scatter(
x=df.loc[mask_norm, 'DM2_Clasica'],
y=df.loc[mask_norm, 'DM2_Robusta'],
mode='markers',
marker=dict(size=6, color='royalblue', opacity=0.7),
name='Observaciones Regulares'
))
# Outliers robustos
mask_out = df['Outlier_Robusto']
fig_out.add_trace(go.Scatter(
x=df.loc[mask_out, 'DM2_Clasica'],
y=df.loc[mask_out, 'DM2_Robusta'],
mode='markers+text',
text=[f"Obs {idx}" for idx in df[mask_out].index],
textposition='top center',
marker=dict(size=10, color='crimson', symbol='x'),
name='Anomalías Robustas (MCD)'
))
# Líneas de umbral Chi-cuadrado
fig_out.add_hline(y=cutoff_99, line_dash='dash', line_color='crimson',
annotation_text=f"Umbral Robusto (χ² = {cutoff_99:.1f})")
fig_out.add_vline(x=cutoff_99, line_dash='dash', line_color='gray',
annotation_text=f"Umbral Clásico (χ² = {cutoff_99:.1f})")
fig_out.update_layout(
title="Distancia de Mahalanobis: Estimador Clásico vs. Robusto (MCD)",
xaxis_title="D² Mahalanobis Clásica",
yaxis_title="D² Mahalanobis Robusta (MCD)",
height=480,
template="plotly_white"
)
fig_out.show()
```
### Inspección detallada de las observaciones anómalas
Examinemos los registros señalados como anomalías por el estimador robusto:
```{python}
#| code-fold: false
columnas_vista = ['species', 'sex', 'bill_length_mm', 'bill_depth_mm', 'flipper_length_mm', 'body_mass_g', 'DM2_Robusta']
anomalias = df[df['Outlier_Robusto']][columnas_vista].sort_values(by='DM2_Robusta', ascending=False)
anomalias
```
::: {.callout-important}
### Interpretación de resultados: ¿Por qué son anomalías multivariadas?
Examinemos el caso más extremo, la **observación 185** (un pingüino *Gentoo* hembra):
- `bill_length_mm`: **46.5 mm** (perfectamente normal para Gentoo, donde la media es ~47.5 mm).
- `bill_depth_mm`: **17.9 mm** (muy elevado para una hembra Gentoo, cuyos picos rara vez superan los 15 mm).
- `flipper_length_mm`: **192.0 mm** (¡extraordinariamente corto! Las aletas de Gentoo promedian 217 mm y prácticamente nunca bajan de 205 mm).
- `body_mass_g`: **3,500 g** (masa inusualmente baja para Gentoo).
**Diagnóstico**: La observación 185 posee un cuerpo y pico con características de *Gentoo*, pero con aletas del tamaño de un *Adelie*. En cualquier filtro univariado (boxplot de longitud de aleta o peso), 192 mm o 3,500 g son valores comunes para pingüinos en general. Sin embargo, en el espacio multivariado $\mathbb{R}^4$, esta combinación morfológica es una **anomalía biológica severa**.
:::
---
## Paso 7: Visualización multivariada integral: Coordenadas Paralelas
Para sintetizar la estructura morfológica de toda la colonia y observar cómo se distinguen las especies y las anomalías, empleamos un gráfico de **Coordenadas Paralelas** (`px.parallel_coordinates`):
```{python}
#| code-fold: true
#| fig-align: center
# Mapeo numérico para colorear por especie en coordenadas paralelas
especie_cod = {'Adelie': 1, 'Chinstrap': 2, 'Gentoo': 3}
df['species_id'] = df['species'].map(especie_cod)
fig_paralelas = px.parallel_coordinates(
df,
dimensions=features,
color='species_id',
color_continuous_scale=[(0.0, '#ff7f0e'), (0.5, '#2ca02c'), (1.0, '#1f77b4')],
labels={
'bill_length_mm': 'Long. Pico (mm)',
'bill_depth_mm': 'Prof. Pico (mm)',
'flipper_length_mm': 'Aleta (mm)',
'body_mass_g': 'Peso (g)',
'species_id': 'Especie'
},
title="Coordenadas Paralelas: Perfiles Fenotípicos de los Pingüinos de Palmer"
)
fig_paralelas.update_layout(height=480, template="plotly_white")
fig_paralelas.show()
```
::: {.callout-note}
### Interpretación de resultados: Perfiles morfológicos en Coordenadas Paralelas
1. **Separación de Gentoo (azul)**: Sus trayectorias viajan por la parte superior en *Longitud de Pico*, caen en *Profundidad de Pico*, y se elevan drásticamente en *Aleta* y *Peso*. Es una especie grande con pico largo y estilizado.
2. **Adelie (naranja) vs Chinstrap (verde)**: Tienen profundidades de pico, aletas y pesos muy similares, pero *Chinstrap* se separa con claridad por tener picos mucho más largos (la trayectoria sube en el primer eje y baja en el segundo).
3. **Cruce en tijera**: El cruce visible de líneas entre *Longitud de Pico* y *Profundidad de Pico* refleja visualmente la correlación negativa aparente que desglosamos en la Paradoja de Simpson.
:::
---
## Conclusiones y guía práctica para el científico de datos
A partir de este caso práctico en `pandas`, podemos sintetizar un conjunto de reglas operativas para el análisis exploratorio multivariado dentro del ciclo CRISP-DM:
| Pregunta analítica | Herramienta en pandas / scipy / sklearn | Criterio de interpretación |
|---|---|---|
| **¿Dónde está el centro representativo?** | `df.mean()` vs. Mediana Geométrica (Weiszfeld) | Si la media se aleja de la mediana geométrica, existen colas pesadas o grupos asimétricos. |
| **¿Cuál es el volumen de dispersión?** | `np.linalg.det(df.corr())` | Si $|\mathbf{R}| \approx 0$, existe fuerte redundancia lineal entre variables. |
| **¿La correlación es real o espuria?** | `np.linalg.inv(df.corr())` (Correlación Parcial) | Si el signo o magnitud cambia respecto a Pearson, existe una variable de confusión latente. |
| **¿Hay anomalías no visibles en 1D?** | `MinCovDet().mahalanobis(X)` vs $\chi^2_p$ | Permite detectar individuos que violan la alometría o correlación conjunta. |
| **¿Cómo interactúan todos los atributos?** | `px.parallel_coordinates(df, dimensions=...)` | Permite identificar firmas multivariadas, agrupamientos y cruces de correlación. |
Con estas herramientas, los datos quedan rigurosamente diagnosticados y listos para las etapas siguientes del pipeline de minería: la imputación multivariada, el escalado para modelos predictivos y las técnicas de reducción dimensional como PCA.
---
## Referencias {.unnumbered}
::: {#refs}
:::