---
title: "Medidas de correlación"
pagetitle: "Medidas de correlación"
open-graph:
description: "Distingue los coeficientes de correlación de Pearson, Spearman y Kendall mediante un ejemplo de horas de práctica y retos resueltos."
twitter-card:
description: "Distingue los coeficientes de correlación de Pearson, Spearman y Kendall mediante un ejemplo de horas de práctica y retos resueltos."
description: "Distingue los coeficientes de correlación de Pearson, Spearman y Kendall mediante un ejemplo de horas de práctica y retos resueltos."
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."
---
## El problema: ¿avanzan juntas dos características?
Cinco estudiantes registraron cuántas horas practicaron antes de una sesión y
cuántos retos lograron resolver durante ella:
| Estudiante | Horas de práctica $X$ | Retos resueltos $Y$ |
|:---:|---:|---:|
| A | 1 | 2 |
| B | 2 | 1 |
| C | 3 | 4 |
| D | 4 | 3 |
| E | 5 | 5 |
Al leer la tabla aparece una tendencia: quienes practicaron más horas suelen
resolver más retos, aunque el orden no sea perfecto. ¿Cómo convertir esa
impresión en un número sin perder de vista la gráfica?
Una **asociación** describe cómo cambia una variable cuando cambia otra. Una
correlación resume la dirección y la intensidad de un tipo específico de
asociación mediante un número estandarizado:[^estandarizada]
- **Pearson** pregunta qué tan bien se organiza la nube alrededor de una recta;
- **Spearman** pregunta qué tan semejante es el orden de ambas variables;
- **Kendall** pregunta qué proporción de pares conserva el mismo orden.
Estos coeficientes no son tres versiones intercambiables de “qué tan
relacionadas están” las variables. Cada uno conserva información distinta.
::: {.callout-warning appearance="minimal"}
Una correlación aislada puede ocultar curvatura, grupos, valores extremos o
errores. Debe acompañarse siempre de una gráfica: el cuarteto de Anscombe
muestra conjuntos con resúmenes casi idénticos y geometrías radicalmente
distintas [@anscombe1973graphs].
:::
## Antes de calcular: mirar los pares
Las observaciones vienen emparejadas. La primera hora y el primer número de
retos pertenecen al estudiante A; no se deben ordenar $X$ y $Y$ por separado
antes de dibujarlas. El diagrama de dispersión conserva cada pareja
$(x_i,y_i)$.[^dispersion]
En el ejemplo, al movernos de izquierda a derecha predominan los ascensos. Hay
dos retrocesos locales —A frente a B y C frente a D—, pero la dirección general
es positiva.
::: {.callout-note appearance="minimal"}
### Tres brújulas intuitivas
1. Si los puntos quedan exactamente sobre una recta creciente, los tres
coeficientes deberían indicar asociación positiva perfecta.
2. Si $Y$ crece siempre con $X$, pero siguiendo una curva, los órdenes pueden
concordar perfectamente aunque una recta no describa bien las distancias.
3. Si $Y$ primero baja y después sube, una relación visible puede producir
correlación cercana a cero porque los dos sentidos se compensan.
:::
## Covarianza: dirección con unidades
### Paso 1: Intuición con un ejemplo
Retomemos a los cinco estudiantes del ejemplo inicial. Para responder si las
horas de práctica y los retos resueltos "avanzan juntos", necesitamos un punto
de referencia objetivo para cada variable. El promedio o media muestral
$(\bar{x}, \bar{y})$ cumple ese papel: representa el centro de gravedad de la
distribución.
Si calculamos la media de ambas variables:
$$
\bar{x} = \frac{1 + 2 + 3 + 4 + 5}{5} = 3 \text{ horas},
\qquad
\bar{y} = \frac{2 + 1 + 4 + 3 + 5}{5} = 3 \text{ retos}.
$$
Ahora podemos medir cómo se desvía cada estudiante de ese centro mediante las
diferencias $(x_i - \bar{x})$ y $(y_i - \bar{y})$. ¿Qué ocurre si multiplicamos
esas dos desviaciones para cada estudiante?
- **Mismo sentido (+):** Si un estudiante practicó más horas que el promedio
($x_i - \bar{x} > 0$) y además resolvió más retos que el promedio
($y_i - \bar{y} > 0$), el producto es $(+) \times (+) = +$. Del mismo modo, si
practicó menos que el promedio ($-$) y resolvió menos retos ($-$), el producto
es $(-) \times (-) = +$. En ambos casos, el producto es **positivo** porque
ambas características se desviaron hacia el mismo lado de su respectiva media.
- **Sentidos opuestos (−):** Si un estudiante practicó más que el promedio ($+$)
pero resolvió menos retos que el promedio ($-$), el producto es
$(+) \times (-) = -$. De igual forma, si practicó menos ($-$) pero resolvió
más retos ($+$), el producto es $(-) \times (+) = -$. Un producto **negativo**
revela desacuerdo entre las desviaciones.
La suma de estos productos cruzados captura el balance neto de acuerdos y
desacuerdos en la muestra.
### Paso 2: Cálculo a mano paso a paso
Construyamos la tabla aritmética completa para los cinco estudiantes, detallando
cada desviación, su producto cruzado y sus cuadrados:
| Estudiante | $x_i$ | $y_i$ | $x_i - \bar{x}$ | $y_i - \bar{y}$ | Producto cruzado $(x_i-\bar{x})(y_i-\bar{y})$ | $(x_i-\bar{x})^2$ | $(y_i-\bar{y})^2$ |
|:---:|---:|---:|---:|---:|---:|---:|---:|
| A | 1 | 2 | $1 - 3 = -2$ | $2 - 3 = -1$ | $(-2) \times (-1) = \mathbf{+2}$ | $(-2)^2 = 4$ | $(-1)^2 = 1$ |
| B | 2 | 1 | $2 - 3 = -1$ | $1 - 3 = -2$ | $(-1) \times (-2) = \mathbf{+2}$ | $(-1)^2 = 1$ | $(-2)^2 = 4$ |
| C | 3 | 4 | $3 - 3 = 0$ | $4 - 3 = +1$ | $(0) \times (+1) = \mathbf{0}$ | $0^2 = 0$ | $(+1)^2 = 1$ |
| D | 4 | 3 | $4 - 3 = +1$ | $3 - 3 = 0$ | $(+1) \times (0) = \mathbf{0}$ | $(+1)^2 = 1$ | $0^2 = 0$ |
| E | 5 | 5 | $5 - 3 = +2$ | $5 - 3 = +2$ | $(+2) \times (+2) = \mathbf{+4}$ | $(+2)^2 = 4$ | $(+2)^2 = 4$ |
| **Suma** | | | **0** | **0** | **8** | **10** | **10** |
Observemos tres detalles del cálculo manual:
1. Las sumas de las desviaciones individuales son exactamente cero:
$\sum (x_i - \bar{x}) = 0$ y $\sum (y_i - \bar{y}) = 0$. Esta es una
propiedad algebraica de la media aritmética.
2. La suma de los productos cruzados es $2 + 2 + 0 + 0 + 4 = 8$.
3. Para obtener un promedio no sesgado de la covarianza poblacional a partir de
la muestra, dividimos entre los grados de libertad $n - 1 = 5 - 1 = 4$
(corrección de Bessel[^bessel]), en lugar de dividir entre $n$:
$$
s_{XY} = \frac{\sum_{i=1}^{n} (x_i - \bar{x})(y_i - \bar{y})}{n - 1}
= \frac{8}{5 - 1} = \frac{8}{4} = 2.0 \quad \text{horas} \cdot \text{retos}.
$$
El signo positivo confirma que las variables tienden a variar en la misma
dirección. Sin embargo, surge un inconveniente: las unidades del resultado son
híbridas ($\text{horas} \cdot \text{retos}$). Si en lugar de horas hubiéramos
medido el tiempo en minutos ($X' = 60X$), cada desviación horizontal se
multiplicaría por 60, la suma de productos pasaría de 8 a 480, y la covarianza
sería $s_{X'Y} = 480 / 4 = 120 \text{ min} \cdot \text{retos}$. La relación
entre los estudiantes es idéntica, pero el número se multiplicó por 60. Por eso
la covarianza por sí sola no permite juzgar la fuerza de la asociación.
### Paso 3: Formalización matemática
La **covarianza muestral** entre dos variables numéricas $X$ y $Y$ con $n$ pares
de observaciones se define formalmente como:
$$
s_{XY} = \frac{1}{n-1}\sum_{i=1}^{n} (x_i-\bar x)(y_i-\bar y). \tag{1}
$$
Satisface las siguientes propiedades fundamentales:
- **Simetría:** $s_{XY} = s_{YX}$. El orden de las variables no altera el
resultado.
- **Autocovarianza:** Cuando se calcula la covarianza de una variable consigo
misma, se recupera su varianza muestral: $s_{XX} = s_X^2$.
- **Bilinealidad:** Si transformamos linealmente las variables mediante
$U = aX + b$ y $V = cY + d$ (con $a, b, c, d \in \mathbb{R}$), entonces:
$$s_{UV} = ac \cdot s_{XY}.$$
Las constantes sumadas ($b$ y $d$) se cancelan al centrar respecto a la
media, pero los factores de escala ($a$ y $c$) multiplican directamente la
covarianza.
### Paso 4: De la intuición geométrica al laboratorio
Existen dos maneras complementarias de visualizar geométricamente la
covarianza:
1. **En el plano cartesiano de los datos $(x, y)$ (sumar áreas de rectángulos):**
Cada observación $(x_i, y_i)$ forma con el centroide $(\bar{x}, \bar{y})$ un
rectángulo cuya base mide $(x_i - \bar{x})$ y cuya altura mide
$(y_i - \bar{y})$. El producto $(x_i - \bar{x})(y_i - \bar{y})$ es el
**área con signo** de ese rectángulo:
- **Áreas positivas:** Cuadrante I (arriba a la derecha) y Cuadrante III
(abajo a la izquierda) respecto al centroide.
- **Áreas negativas:** Cuadrante II (arriba a la izquierda) y Cuadrante IV
(abajo a la derecha).
La covarianza muestral es el **balance neto** de esas áreas dividido entre
$n-1$.
2. **En el espacio de observaciones $\mathbb{R}^n$ (producto escalar de vectores centrados):**
Si agrupamos las desviaciones de toda la muestra en dos vectores de
$\mathbb{R}^5$:
$$\widetilde{\mathbf x} = (x_1 - \bar{x}, \ldots, x_5 - \bar{x}), \qquad
\widetilde{\mathbf y} = (y_1 - \bar{y}, \ldots, y_5 - \bar{y}),$$
ambos vectores viven en un hiperplano de dimensión $n-1$ porque sus
coordenadas suman cero. La suma de las áreas rectangulares es exactamente el
producto escalar (producto punto) de ambos vectores:
$$\sum_{i=1}^{n} (x_i - \bar{x})(y_i - \bar{y}) = \widetilde{\mathbf x} \cdot \widetilde{\mathbf y},
\qquad
s_{XY} = \frac{\widetilde{\mathbf x} \cdot \widetilde{\mathbf y}}{n-1}. \tag{2}$$
| Magnitud estadística | Equivalente geométrico en $\mathbb{R}^n$ |
|:---|:---|
| Varianza muestral de $X$ | $s_X^2 = \lVert\widetilde{\mathbf x}\rVert^2 / (n-1)$ |
| Desviación estándar de $X$ | $s_X = \lVert\widetilde{\mathbf x}\rVert / \sqrt{n-1}$ |
| Covarianza muestral entre $X$ y $Y$ | $s_{XY} = (\widetilde{\mathbf x} \cdot \widetilde{\mathbf y}) / (n-1)$ |
En el siguiente laboratorio interactivo puede explorar esta geometría: arrastre
los puntos libremente o use los botones de preajuste para observar cómo los
rectángulos cambian de signo y tamaño, cómo el centroide dinámico se desplaza y
cómo la barra de balance visualiza la suma de áreas positivas frente a las
negativas.
::: {.content-visible when-format="html"}
```{=html}
<div id="covariance-geometry-lab" class="geom-lab" data-geometry="covariance" aria-labelledby="covariance-geometry-title">
<style>
.geom-lab{--ink:#172033;--muted:#5b6474;--blue:#2563eb;--orange:#d97706;--green:#16835f;--red:#c2413b;--purple:#7c3aed;margin:1.35rem 0;color:var(--ink);font-family:inherit}
.geom-lab .lab-shell{border:1px solid #d6deea;border-radius:16px;padding:1.15rem;background:linear-gradient(135deg,#f8fbff,#fffaf3);box-shadow:0 8px 24px rgba(23,32,51,.06)}
.geom-lab .lab-title{font-size:1.1rem;font-weight:750;margin:0 0 .25rem;color:var(--ink)}
.geom-lab .lab-intro{color:var(--muted);font-size:.88rem;line-height:1.45;margin:0 0 .9rem}
.geom-lab .lab-layout{display:grid;grid-template-columns:minmax(240px,.75fr) minmax(360px,1.25fr);gap:1rem;align-items:start}
.geom-lab .lab-controls{background:rgba(255,255,255,.8);border:1px solid #e0e6ef;border-radius:12px;padding:.85rem}
.geom-lab .preset-strip{display:flex;flex-wrap:wrap;gap:.35rem;margin-bottom:.8rem}
.geom-lab button{border:1px solid #aab6c8;background:#fff;border-radius:999px;padding:.32rem .6rem;color:var(--ink);cursor:pointer;font-size:.78rem;transition:all .15s ease}
.geom-lab button:hover,.geom-lab button:focus{border-color:var(--blue);outline:2px solid #bfdbfe}
.geom-lab button[aria-pressed="true"]{background:var(--blue);border-color:var(--blue);color:#fff}
.geom-lab .slider-grid{display:grid;grid-template-columns:repeat(5,1fr);gap:.45rem;margin:.6rem 0}
.geom-lab .slider-col{display:flex;flex-direction:column;align-items:center;font-size:.76rem;color:var(--muted)}
.geom-lab .slider-col span{font-weight:600;color:var(--ink);margin-bottom:.2rem}
.geom-lab .slider-col input[type="range"]{writing-mode:vertical-lr;direction:rtl;width:1.6rem;height:90px;accent-color:var(--blue);cursor:pointer}
.geom-lab .slider-col output{font-weight:700;font-variant-numeric:tabular-nums;margin-top:.25rem}
.geom-lab .range-row{display:grid;grid-template-columns:1fr auto;gap:.4rem;align-items:center;margin:.6rem 0}
.geom-lab .range-row label{grid-column:1/-1;font-size:.8rem;color:var(--muted);font-weight:600}
.geom-lab .range-row input[type="range"]{width:100%;accent-color:var(--blue)}
.geom-lab .range-row output{font-weight:700;font-variant-numeric:tabular-nums;min-width:3.5rem;text-align:right;font-size:.82rem}
.geom-lab .metrics-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:.45rem;margin-top:.75rem}
.geom-lab .metric-card{border:1px solid #dce3ed;border-radius:8px;background:#fff;padding:.45rem;text-align:center}
.geom-lab .metric-card span{display:block;color:var(--muted);font-size:.7rem}
.geom-lab .metric-card strong{font-variant-numeric:tabular-nums;font-size:.96rem;color:var(--ink)}
.geom-lab .area-balance-wrap{background:#fff;border:1px solid #dce3ed;border-radius:10px;padding:.65rem;margin-top:.85rem}
.geom-lab .balance-header{display:flex;justify-content:space-between;font-size:.76rem;font-weight:700;margin-bottom:.35rem}
.geom-lab .balance-bar{height:16px;background:#e2e8f0;border-radius:999px;position:relative;overflow:hidden;display:flex}
.geom-lab .balance-neg{background:var(--red);height:100%;transition:width .15s}
.geom-lab .balance-pos{background:var(--green);height:100%;transition:width .15s}
.geom-lab .balance-center{position:absolute;left:50%;top:0;bottom:0;width:2px;background:#334155;transform:translateX(-50%);z-index:2}
.geom-lab .balance-footer{display:flex;justify-content:space-between;font-size:.72rem;color:var(--muted);margin-top:.3rem}
.geom-lab svg{width:100%;height:auto;background:#fff;border:1px solid #dce3ed;border-radius:12px;user-select:none;touch-action:none}
.geom-lab .reading-box{font-size:.83rem;line-height:1.45;color:#334155;background:#f6f8fb;border-left:3px solid var(--blue);border-radius:0 8px 8px 0;padding:.55rem .7rem;margin-top:.8rem}
.geom-lab .legend-row{display:flex;flex-wrap:wrap;gap:.8rem;color:var(--muted);font-size:.75rem;margin-bottom:.45rem;align-items:center}
.geom-lab .legend-item{display:inline-flex;align-items:center;gap:.3rem}
.geom-lab .swatch{width:.75rem;height:.75rem;border-radius:3px;display:inline-block}
@media(max-width:760px){.geom-lab .lab-layout{grid-template-columns:1fr}}
</style>
<div class="lab-shell">
<h3 id="covariance-geometry-title" class="lab-title">Geometría de la covarianza: rectángulos con signo y centroide móvil</h3>
<p class="lab-intro">Arrastre los puntos en la gráfica o use los controles verticales. Cada punto $(x_i, y_i)$ forma un rectángulo con el centroide $(\bar{x}, \bar{y})$: verde si suma (cuadrantes I y III), rojo si resta (II y IV). La covarianza es el área neta dividida por $n-1 = 4$.</p>
<div class="lab-layout">
<div class="lab-controls">
<div class="preset-strip">
<button type="button" data-preset="2,1,4,3,5">Ejemplo del libro</button>
<button type="button" data-preset="1,5,4,3,2">Cancelación exacta (s=0)</button>
<button type="button" data-preset="5,4,3,2,1">Dirección opuesta (s<0)</button>
<button type="button" data-preset="4,1,0,1,4">Simetría en U (s=0)</button>
<button type="button" data-preset="1,2,3,4,5">Alineación perfecta (s=2.5)</button>
</div>
<div class="slider-grid" data-sliders aria-label="Controles de retos Y para cada estudiante"></div>
<div class="range-row">
<label for="cov-visible-step">Observaciones incorporadas (acumulación paso a paso)</label>
<input id="cov-visible-step" data-step type="range" min="1" max="5" step="1" value="5">
<output data-step-out>5 de 5</output>
</div>
<div class="area-balance-wrap" aria-live="polite">
<div class="balance-header">
<span style="color:var(--red)">Área negativa: <output data-area-neg>0.0</output></span>
<span style="color:var(--green)">Área positiva: <output data-area-pos>0.0</output></span>
</div>
<div class="balance-bar" title="Balanza de áreas con signo">
<div class="balance-center"></div>
<div class="balance-neg" data-bar-neg style="width:50%;margin-left:auto"></div>
<div class="balance-pos" data-bar-pos style="width:50%"></div>
</div>
<div class="balance-footer">
<span>Cuadrantes II y IV (−)</span>
<strong style="color:var(--ink)">Saldo neto: <output data-area-net>+8.0</output></strong>
<span>Cuadrantes I y III (+)</span>
</div>
</div>
<div class="metrics-grid" aria-live="polite">
<div class="metric-card"><span>Centroide (x̄, ȳ)</span><strong data-metric="centroid">(3.0, 3.0)</strong></div>
<div class="metric-card"><span>Producto interno x̃ · ỹ</span><strong data-metric="dot">+8.000</strong></div>
<div class="metric-card"><span>Covarianza muestral s<sub>XY</sub></span><strong data-metric="cov">+2.000</strong></div>
<div class="metric-card"><span>Orientación dominante</span><strong data-metric="sign">Positiva</strong></div>
</div>
<p class="reading-box" data-reading aria-live="polite"></p>
</div>
<div>
<div class="legend-row">
<span class="legend-item"><i class="swatch" style="background:#16835f"></i>Área positiva (Δx·Δy > 0)</span>
<span class="legend-item"><i class="swatch" style="background:#c2413b"></i>Área negativa (Δx·Δy < 0)</span>
<span class="legend-item"><i class="swatch" style="background:#7c3aed"></i>Centroide (x̄, ȳ)</span>
</div>
<svg data-svg viewBox="0 0 640 430" role="img" aria-label="Diagrama interactivo de rectángulos de covarianza con puntos arrastrables"></svg>
</div>
</div>
</div>
</div>
<script>
(() => {
const root = document.getElementById('covariance-geometry-lab');
if (!root) return;
const NS = 'http://www.w3.org/2000/svg';
const x = [1, 2, 3, 4, 5], letters = ['A', 'B', 'C', 'D', 'E'];
let y = [2, 1, 4, 3, 5];
const svg = root.querySelector('[data-svg]');
const stepInput = root.querySelector('[data-step]');
const stepOut = root.querySelector('[data-step-out]');
const sliderGrid = root.querySelector('[data-sliders]');
letters.forEach((letter, i) => {
const col = document.createElement('div');
col.className = 'slider-col';
col.innerHTML = `<span>${letter}</span><input type="range" min="0" max="8" step="0.5" value="${y[i]}" data-idx="${i}" aria-label="Retos del estudiante ${letter}"><output>${y[i]}</output>`;
sliderGrid.appendChild(col);
});
const yInputs = [...root.querySelectorAll('[data-idx]')];
const mean = a => a.reduce((s, v) => s + v, 0) / a.length;
const fmt = (v, d = 2) => Number.isFinite(v) ? (v >= 0 ? '+' : '') + v.toFixed(d) : 'no def.';
const add = (parent, tag, attrs, text = '') => {
const el = document.createElementNS(NS, tag);
Object.entries(attrs).forEach(([k, v]) => el.setAttribute(k, v));
if (text) el.textContent = text;
parent.appendChild(el);
return el;
};
const l = 60, r = 600, t = 30, b = 360;
const sx = v => l + ((v - 1) / 4) * (r - l);
const sy = v => b - (v / 8) * (b - t);
const invSy = py => Math.max(0, Math.min(8, Math.round(((b - py) / (b - t) * 8) * 2) / 2));
let draggedIdx = null;
function update() {
const visibleCount = +stepInput.value;
stepOut.value = `${visibleCount} de 5`;
const xm = mean(x);
const ym = mean(y);
const dx = x.map(v => v - xm);
const dy = y.map(v => v - ym);
const products = dx.map((v, i) => v * dy[i]);
let posSum = 0, negSum = 0;
products.slice(0, visibleCount).forEach(p => {
if (p >= 0) posSum += p;
else negSum += Math.abs(p);
});
const netSum = posSum - negSum;
const totalDot = dx.reduce((s, v, i) => s + v * dy[i], 0);
const cov = totalDot / 4;
root.querySelector('[data-area-pos]').textContent = `+${posSum.toFixed(2)}`;
root.querySelector('[data-area-neg]').textContent = `−${negSum.toFixed(2)}`;
root.querySelector('[data-area-net]').textContent = `${netSum >= 0 ? '+' : ''}${netSum.toFixed(2)}`;
const maxVal = Math.max(posSum, negSum, 1);
root.querySelector('[data-bar-pos]').style.width = `${(posSum / (2 * maxVal)) * 100}%`;
root.querySelector('[data-bar-neg]').style.width = `${(negSum / (2 * maxVal)) * 100}%`;
root.querySelector('[data-metric="centroid"]').textContent = `(${xm.toFixed(1)}, ${ym.toFixed(1)})`;
root.querySelector('[data-metric="dot"]').textContent = `${totalDot >= 0 ? '+' : ''}${totalDot.toFixed(2)}`;
root.querySelector('[data-metric="cov"]').textContent = `${cov >= 0 ? '+' : ''}${cov.toFixed(3)}`;
root.querySelector('[data-metric="sign"]').textContent = cov > 0.05 ? 'Positiva (ascendente)' : cov < -0.05 ? 'Negativa (descendente)' : 'Nula (cancelación)';
const reading = root.querySelector('[data-reading]');
if (visibleCount < 5) {
reading.textContent = `Acumulando hasta la observación ${letters[visibleCount - 1]}: suma parcial de rectángulos = ${netSum >= 0 ? '+' : ''}${netSum.toFixed(2)}. Avance el deslizador para ver las 5 observaciones.`;
} else if (Math.abs(cov) < 0.05) {
reading.textContent = `Las áreas verdes (+${posSum.toFixed(2)}) y rojas (−${negSum.toFixed(2)}) se equilibran casi exactamente: el saldo neto es ${netSum.toFixed(2)}, por lo que sXY ≈ 0 a pesar de que los datos no son constantes.`;
} else if (cov > 0) {
reading.textContent = `Predominan los cuadrantes I y III: la mayoría de observaciones están simultáneamente por encima o por debajo de sus medias. Área neta = +${totalDot.toFixed(2)} → sXY = ${cov.toFixed(2)}.`;
} else {
reading.textContent = `Predominan los cuadrantes II y IV: cuando X supera su media, Y queda por debajo. Las áreas negativas dominan: sXY = ${cov.toFixed(2)}.`;
}
yInputs.forEach((inp, i) => {
inp.value = y[i];
inp.nextElementSibling.value = y[i];
});
svg.replaceChildren();
// Quadrant background tint
const cx = sx(xm), cy = sy(ym);
// Q1: x>xm, y>ym (svg: x > cx, y < cy) -> Positive
add(svg, 'rect', { x: cx, y: t, width: r - cx, height: cy - t, fill: '#16835f', opacity: '0.04' });
add(svg, 'text', { x: r - 16, y: t + 24, fill: '#16835f', 'font-size': '20', 'font-weight': 'bold', opacity: '0.35', 'text-anchor': 'end' }, '+');
// Q3: x<xm, y<ym (svg: x < cx, y > cy) -> Positive
add(svg, 'rect', { x: l, y: cy, width: cx - l, height: b - cy, fill: '#16835f', opacity: '0.04' });
add(svg, 'text', { x: l + 16, y: b - 16, fill: '#16835f', 'font-size': '20', 'font-weight': 'bold', opacity: '0.35' }, '+');
// Q2: x<xm, y>ym (svg: x < cx, y < cy) -> Negative
add(svg, 'rect', { x: l, y: t, width: cx - l, height: cy - t, fill: '#c2413b', opacity: '0.04' });
add(svg, 'text', { x: l + 16, y: t + 24, fill: '#c2413b', 'font-size': '20', 'font-weight': 'bold', opacity: '0.35' }, '−');
// Q4: x>xm, y<ym (svg: x > cx, y > cy) -> Negative
add(svg, 'rect', { x: cx, y: cy, width: r - cx, height: b - cy, fill: '#c2413b', opacity: '0.04' });
add(svg, 'text', { x: r - 16, y: b - 16, fill: '#c2413b', 'font-size': '20', 'font-weight': 'bold', opacity: '0.35', 'text-anchor': 'end' }, '−');
// Axes grid
[0, 2, 4, 6, 8].forEach(v => {
add(svg, 'line', { x1: l, x2: r, y1: sy(v), y2: sy(v), stroke: '#e2e8f0', 'stroke-width': '1' });
add(svg, 'text', { x: l - 10, y: sy(v) + 4, 'text-anchor': 'end', fill: '#64748b', 'font-size': '11' }, v);
});
x.forEach(v => {
add(svg, 'line', { x1: sx(v), x2: sx(v), y1: t, y2: b, stroke: '#e2e8f0', 'stroke-width': '1' });
add(svg, 'text', { x: sx(v), y: b + 20, 'text-anchor': 'middle', fill: '#64748b', 'font-size': '11' }, v);
});
// Rectangles
x.forEach((v, i) => {
const active = i < visibleCount;
const p = products[i];
const rx = Math.min(sx(v), cx);
const ry = Math.min(sy(y[i]), cy);
const rw = Math.abs(sx(v) - cx);
const rh = Math.abs(sy(y[i]) - cy);
const rect = add(svg, 'rect', {
x: rx, y: ry, width: rw, height: rh,
fill: active ? (p >= 0 ? '#16835f' : '#c2413b') : '#e2e8f0',
opacity: active ? '0.28' : '0.1',
stroke: active ? (p >= 0 ? '#16835f' : '#c2413b') : '#cbd5e1',
'stroke-width': active ? '1.8' : '1',
'stroke-dasharray': active ? 'none' : '4 3'
});
if (active && rw > 15 && rh > 14) {
add(svg, 'text', {
x: rx + rw / 2, y: ry + rh / 2 + 4,
'text-anchor': 'middle', fill: p >= 0 ? '#0f5132' : '#842029',
'font-size': '10', 'font-weight': '700'
}, (p >= 0 ? '+' : '') + p.toFixed(1));
}
});
// Centroid crosshairs
add(svg, 'line', { x1: cx, x2: cx, y1: t, y2: b, stroke: '#7c3aed', 'stroke-width': '2', 'stroke-dasharray': '6 4' });
add(svg, 'line', { x1: l, x2: r, y1: cy, y2: cy, stroke: '#7c3aed', 'stroke-width': '2', 'stroke-dasharray': '6 4' });
add(svg, 'circle', { cx: cx, cy: cy, r: '6', fill: '#7c3aed', stroke: '#fff', 'stroke-width': '2' });
add(svg, 'text', { x: cx + 8, y: cy - 8, fill: '#7c3aed', 'font-size': '11', 'font-weight': '700' }, `(x̄=${xm.toFixed(1)}, ȳ=${ym.toFixed(1)})`);
// Data points (draggable)
x.forEach((v, i) => {
const active = i < visibleCount;
const ptGroup = add(svg, 'g', { style: 'cursor: ns-resize', 'data-drag-pt': i });
// Invisible larger hit circle for easy touch/mouse dragging
add(ptGroup, 'circle', { cx: sx(v), cy: sy(y[i]), r: '18', fill: 'transparent' });
add(ptGroup, 'circle', {
cx: sx(v), cy: sy(y[i]), r: '11',
fill: active ? '#2563eb' : '#94a3b8',
stroke: '#fff', 'stroke-width': '2.5',
filter: 'drop-shadow(0 2px 4px rgba(0,0,0,0.15))'
});
add(ptGroup, 'text', {
x: sx(v), y: sy(y[i]) + 4,
'text-anchor': 'middle', fill: '#fff',
'font-size': '10', 'font-weight': 'bold',
'pointer-events': 'none'
}, letters[i]);
});
add(svg, 'text', { x: (l + r) / 2, y: b + 42, 'text-anchor': 'middle', fill: '#475569', 'font-size': '12', 'font-weight': '600' }, 'Horas de práctica X (centradas en x̄)');
add(svg, 'text', { x: 18, y: (t + b) / 2, transform: `rotate(-90 18 ${(t + b) / 2})`, 'text-anchor': 'middle', fill: '#475569', 'font-size': '12', 'font-weight': '600' }, 'Retos resueltos Y (centrados en ȳ)');
}
// Pointer drag events on SVG
svg.addEventListener('pointerdown', e => {
const target = e.target.closest('[data-drag-pt]');
if (!target) return;
draggedIdx = +target.dataset.dragPt;
svg.setPointerCapture(e.pointerId);
});
svg.addEventListener('pointermove', e => {
if (draggedIdx === null) return;
const rect = svg.getBoundingClientRect();
const svgY = (e.clientY - rect.top) * (430 / rect.height);
y[draggedIdx] = invSy(svgY);
update();
});
const endDrag = e => {
if (draggedIdx !== null) {
try { svg.releasePointerCapture(e.pointerId); } catch (_) {}
draggedIdx = null;
}
};
svg.addEventListener('pointerup', endDrag);
svg.addEventListener('pointercancel', endDrag);
// Sliders input
yInputs.forEach(inp => inp.addEventListener('input', () => {
y[+inp.dataset.idx] = +inp.value;
update();
}));
stepInput.addEventListener('input', update);
root.querySelectorAll('[data-preset]').forEach(btn => {
btn.addEventListener('click', () => {
y = btn.dataset.preset.split(',').map(Number);
root.querySelectorAll('[data-preset]').forEach(b => b.setAttribute('aria-pressed', b === btn ? 'true' : 'false'));
update();
});
});
root.querySelector('[data-preset="2,1,4,3,5"]').setAttribute('aria-pressed', 'true');
update();
})();
</script>
```
:::
::: {.content-visible unless-format="html"}
En HTML, un control incorpora sucesivamente los cinco rectángulos del ejemplo:
$2+2+0+0+4=8$, por lo que $s_{XY}=8/4=2$. Otros botones muestran cancelación
exacta y dirección opuesta.
:::
## Pearson: estandarizar la asociación lineal
### Paso 1: Intuición con un ejemplo
En la sección anterior vimos que la covarianza entre horas de práctica y retos
resueltos fue $s_{XY} = 2.0 \text{ horas}\cdot\text{retos}$, pero al expresar el
tiempo en minutos el valor se disparó a $120$. ¿Cómo comparar la fuerza de una
asociación entre variables con unidades totalmente distintas (como horas frente
a retos, o ingresos frente a edad)?
La solución intuitiva consiste en **eliminar las unidades** dividiendo cada
variable por su propia escala típica de dispersión: su desviación estándar
muestral ($s_X$ y $s_Y$).
Al hacer esto, cada observación $(x_i, y_i)$ se transforma en un par de
**puntuaciones estandarizadas** o puntuaciones $z$:[^zscore]
$$
z_{X,i} = \frac{x_i - \bar{x}}{s_X},
\qquad
z_{Y,i} = \frac{y_i - \bar{y}}{s_Y}.
$$
La puntuación $z_{X,i}$ responde a una pregunta puramente adimensional:
*¿a cuántas desviaciones estándar por encima o por debajo de la media se
encuentra este estudiante?* Al multiplicar $z_{X,i} \times z_{Y,i}$, las
unidades originales se cancelan en el numerador y el denominador. El resultado
es una medida pura de co-movimiento que no depende del sistema de medida.
### Paso 2: Cálculo a mano paso a paso
Podemos calcular el coeficiente de Pearson a mano de dos formas equivalentes:
**Método A: Mediante las desviaciones estándar y la covarianza.**
A partir de las sumas de cuadrados calculadas en la tabla de covarianza:
$$
\sum_{i=1}^5 (x_i - \bar{x})^2 = 10 \implies s_X^2 = \frac{10}{5-1} = 2.5 \implies s_X = \sqrt{2.5} \approx 1.5811 \text{ horas},
$$
$$
\sum_{i=1}^5 (y_i - \bar{y})^2 = 10 \implies s_Y^2 = \frac{10}{5-1} = 2.5 \implies s_Y = \sqrt{2.5} \approx 1.5811 \text{ retos}.
$$
Sustituyendo en la definición de Pearson:
$$
r = \frac{s_{XY}}{s_X s_Y}
= \frac{2.0}{\sqrt{2.5} \times \sqrt{2.5}}
= \frac{2.0}{2.5} = 0.8. \tag{3}
$$
Como el divisor $n-1$ aparece tanto en el numerador ($s_{XY}$) como dentro de
cada desviación estándar del denominador, se cancela algebraicamente:
$$
r = \frac{\sum (x_i-\bar x)(y_i-\bar y)}{\sqrt{\sum (x_i-\bar x)^2 \sum (y_i-\bar y)^2}}
= \frac{8}{\sqrt{10 \times 10}} = \frac{8}{10} = 0.8. \tag{4}
$$
**Método B: Mediante puntuaciones estandarizadas $z$.**
Construyamos la tabla de puntuaciones $z$ para cada estudiante:
| Estudiante | $x_i$ | $y_i$ | $z_{X,i} = \frac{x_i - 3}{\sqrt{2.5}}$ | $z_{Y,i} = \frac{y_i - 3}{\sqrt{2.5}}$ | Producto $z_{X,i} \cdot z_{Y,i}$ |
|:---:|---:|---:|---:|---:|---:|
| A | 1 | 2 | $\frac{-2}{\sqrt{2.5}} \approx -1.265$ | $\frac{-1}{\sqrt{2.5}} \approx -0.632$ | $\frac{(-2)(-1)}{2.5} = \mathbf{+0.8}$ |
| B | 2 | 1 | $\frac{-1}{\sqrt{2.5}} \approx -0.632$ | $\frac{-2}{\sqrt{2.5}} \approx -1.265$ | $\frac{(-1)(-2)}{2.5} = \mathbf{+0.8}$ |
| C | 3 | 4 | $\frac{0}{\sqrt{2.5}} = 0.000$ | $\frac{+1}{\sqrt{2.5}} \approx +0.632$ | $\frac{(0)(1)}{2.5} = \mathbf{0.0}$ |
| D | 4 | 3 | $\frac{+1}{\sqrt{2.5}} \approx +0.632$ | $\frac{0}{\sqrt{2.5}} = 0.000$ | $\frac{(1)(0)}{2.5} = \mathbf{0.0}$ |
| E | 5 | 5 | $\frac{+2}{\sqrt{2.5}} \approx +1.265$ | $\frac{+2}{\sqrt{2.5}} \approx +1.265$ | $\frac{(+2)(+2)}{2.5} = \mathbf{+1.6}$ |
| **Suma** | | | **0.000** | **0.000** | **3.2** |
Promediando la suma de productos estandarizados con los grados de libertad
$n-1 = 4$:
$$
r = \frac{1}{n-1}\sum_{i=1}^n z_{X,i} z_{Y,i} = \frac{3.2}{4} = 0.8.
$$
Ambos procedimientos coinciden exactamente en $r = 0.8$.
### Paso 3: Formalización matemática
El **coeficiente de correlación producto-momento de Pearson** muestral
[@pearson1895correlation] se define formalmente como:
$$
r = \frac{\sum_{i=1}^{n} (x_i-\bar x)(y_i-\bar y)}{\sqrt{\sum_{i=1}^{n}(x_i-\bar x)^2 \sum_{i=1}^{n}(y_i-\bar y)^2}}. \tag{5}
$$
Posee propiedades matemáticas rigurosas:
- **Acotamiento estricto:** Por la desigualdad de Cauchy--Schwarz,[^cauchy]
$$-1 \le r \le 1.$$
El valor $r = +1$ indica una relación lineal creciente perfecta; $r = -1$
indica una relación lineal decreciente perfecta. Si alguna variable es
constante, su desviación estándar es cero y $r$ queda indeterminado.
- **Invarianza afín:** Si transformamos las variables según $U = a + bX$ y
$V = c + dY$ con $b, d \ne 0$, se cumple:
$$r_{UV} = \operatorname{sign}(bd) \cdot r_{XY}.$$
Sumar constantes o multiplicar por factores positivos no altera $r$. Si se
multiplica por una constante negativa, solo se invierte el signo del
coeficiente.
### Paso 4: De la intuición geométrica al laboratorio
Pearson admite dos interpretaciones geométricas simultáneas:
1. **En el espacio de observaciones $\mathbb{R}^n$ (conservar solo el ángulo):**
Los vectores centrados $\widetilde{\mathbf x}, \widetilde{\mathbf y} \in \mathbb{R}^5$
viven en el hiperplano $\sum z_i = 0$. La definición de Pearson equivale
exactamente al **coseno del ángulo $\theta$** entre ellos:
$$r = \frac{\widetilde{\mathbf x}\cdot\widetilde{\mathbf y}}{\lVert\widetilde{\mathbf x}\rVert\lVert\widetilde{\mathbf y}\rVert} = \cos\theta. \tag{6}$$
La covarianza mezcla la escala de los vectores con su orientación; Pearson
divide las longitudes y conserva **únicamente la orientación**.
**Descomposición ortogonal y Teorema de Pitágoras:**
Podemos descomponer el vector $\widetilde{\mathbf y}$ en una componente
paralela a $\widetilde{\mathbf x}$ (la proyección $\widehat{\mathbf y}$) y un
residuo perpendicular $\mathbf{e}$ ($\mathbf{e} \perp \widetilde{\mathbf x}$):
$$\widetilde{\mathbf y} = \widehat{\mathbf y} + \mathbf{e}, \qquad
\text{con } \widehat{\mathbf y} = r \frac{\lVert\widetilde{\mathbf y}\rVert}{\lVert\widetilde{\mathbf x}\rVert} \widetilde{\mathbf x}.$$
Por el Teorema de Pitágoras en $\mathbb{R}^n$, las normas al cuadrado se
suman:
$$\lVert\widetilde{\mathbf y}\rVert^2 = \lVert\widehat{\mathbf y}\rVert^2 + \lVert\mathbf{e}\rVert^2.$$
Al normalizar dividiendo entre $\lVert\widetilde{\mathbf y}\rVert^2$, surge
la identidad fundamental:
$$1 = r^2 + \frac{\lVert\mathbf{e}\rVert^2}{\lVert\widetilde{\mathbf y}\rVert^2} = r^2 + e^2.$$
El término $r^2$ es el **coeficiente de determinación** $R^2$:[^r2] representa
la fracción de la variabilidad de $\widetilde{\mathbf y}$ capturada por la
dirección de $\widetilde{\mathbf x}$. El término restante $e^2 = 1 - r^2$ es
la varianza residual no explicada.
2. **En el espacio de datos estandarizados $(z_X, z_Y)$:**
En este espacio, el centroide está en $(0,0)$ y ambas variables tienen
varianza 1. La recta de regresión de mínimos cuadrados pasa por el origen con
pendiente idéntica a $r$ ($\hat{z}_Y = r z_X$). La **elipse de covarianza**
(de nivel $1\sigma$) encierra la masa de los datos:
- Cuando $r = 0$, la elipse es un círculo perfecto (ejes iguales).
- A medida que $|r| \to 1$, el semieje menor de la elipse colapsa a cero y la
elipse degenera en un segmento de recta sobre la diagonal.
En el siguiente laboratorio interactivo puede experimentar con ambas
representaciones en paralelo: modifique el ángulo $\theta$ o la norma
$\lVert\widetilde{\mathbf y}\rVert$ para comprobar que la escala altera la
covarianza pero deja intacto a $r$, y observe cómo el triángulo rectángulo
pitagórico ($1 = r^2 + e^2$) y la elipse de covarianza reaccionan en tiempo real.
::: {.content-visible when-format="html"}
```{=html}
<div id="pearson-geometry-lab" class="geom-lab" data-geometry="pearson" aria-labelledby="pearson-geometry-title">
<style>
#pearson-geometry-lab .dual-svg-grid{display:grid;grid-template-columns:1fr 1fr;gap:.8rem}
@media(max-width:860px){#pearson-geometry-lab .dual-svg-grid{grid-template-columns:1fr}}
</style>
<div class="lab-shell">
<h3 id="pearson-geometry-title" class="lab-title">Geometría de Pearson: ángulo en ℝⁿ y espacio de datos estandarizados</h3>
<p class="lab-intro">Pearson $r = \cos \theta$ conserva únicamente la dirección entre los vectores centrados en $\mathbb{R}^n$, eliminando las unidades. A la izquierda, la descomposición ortogonal $\tilde{\mathbf{y}} = \hat{\mathbf{y}} + \mathbf{e}$ revela el teorema de Pitágoras ($1 = r^2 + e^2$); a la derecha, la elipse de covarianza en coordenadas estandarizadas $(z_X, z_Y)$ muestra cómo la nube colapsa a la recta cuando $|r| \to 1$.</p>
<div class="lab-layout">
<div class="lab-controls">
<div class="preset-strip">
<button type="button" data-preset-p="0,3.1623">Colineales (r = +1.0)</button>
<button type="button" data-preset-p="36.87,3.1623">Ejemplo libro (r = +0.8)</button>
<button type="button" data-preset-p="90,3.1623">Ortogonales (r = 0.0)</button>
<button type="button" data-preset-p="180,3.1623">Opuestos (r = −1.0)</button>
<button type="button" data-preset-p="36.87,6.0">Escala doble (mismo r)</button>
</div>
<div class="range-row">
<label for="pearson-theta">Ángulo entre vectores centrados θ</label>
<input id="pearson-theta" data-theta type="range" min="0" max="180" step="0.5" value="36.87">
<output data-theta-out>36.9°</output>
</div>
<div class="range-row">
<label for="pearson-len">Longitud ||ỹ|| (desviación de Y)</label>
<input id="pearson-len" data-len type="range" min="1" max="6" step="0.1" value="3.16">
<output data-len-out>3.16</output>
</div>
<div class="metrics-grid" aria-live="polite">
<div class="metric-card"><span>Pearson r = cos θ</span><strong data-metric="r">+0.800</strong></div>
<div class="metric-card"><span>Varianza explicada R²</span><strong data-metric="r2">64.0%</strong></div>
<div class="metric-card"><span>Proyección ||ŷ||</span><strong data-metric="proj">2.53</strong></div>
<div class="metric-card"><span>Residuo ||e||</span><strong data-metric="resid">1.90</strong></div>
<div class="metric-card"><span>Producto x̃ · ỹ</span><strong data-metric="dot">+8.00</strong></div>
<div class="metric-card"><span>Covarianza s<sub>XY</sub></span><strong data-metric="cov">+2.00</strong></div>
</div>
<p class="reading-box" data-reading aria-live="polite"></p>
</div>
<div>
<div class="dual-svg-grid">
<div>
<p style="font-size:.78rem;font-weight:700;color:var(--muted);margin:0 0 .3rem;text-align:center">1. Espacio vectorial: plano generado por (x̃, ỹ)</p>
<svg data-svg-vec viewBox="0 0 320 280" role="img" aria-label="Plano vectorial de Pearson con proyección ortogonal"></svg>
</div>
<div>
<p style="font-size:.78rem;font-weight:700;color:var(--muted);margin:0 0 .3rem;text-align:center">2. Espacio estandarizado: recta z_Y = r·z_X y elipse</p>
<svg data-svg-std viewBox="0 0 320 280" role="img" aria-label="Espacio de datos estandarizados con elipse de covarianza"></svg>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
(() => {
const root = document.getElementById('pearson-geometry-lab');
if (!root) return;
const NS = 'http://www.w3.org/2000/svg';
const thetaInp = root.querySelector('[data-theta]');
const lenInp = root.querySelector('[data-len]');
const thetaOut = root.querySelector('[data-theta-out]');
const lenOut = root.querySelector('[data-len-out]');
const svgVec = root.querySelector('[data-svg-vec]');
const svgStd = root.querySelector('[data-svg-std]');
const nx = Math.sqrt(10); // ||x~|| for [1,2,3,4,5]
const add = (parent, tag, attrs, text = '') => {
const el = document.createElementNS(NS, tag);
Object.entries(attrs).forEach(([k, v]) => el.setAttribute(k, v));
if (text) el.textContent = text;
parent.appendChild(el);
return el;
};
const arrow = (svg, id, color) => {
const defs = add(svg, 'defs', {});
const marker = add(defs, 'marker', { id, viewBox: '0 0 10 10', refX: '7', refY: '5', markerWidth: '6', markerHeight: '6', orient: 'auto-start-reverse' });
add(marker, 'path', { d: 'M 0 0 L 10 5 L 0 10 z', fill: color });
};
function update() {
const deg = +thetaInp.value;
const rad = deg * Math.PI / 180;
const ny = +lenInp.value;
const r = Math.cos(rad);
const r2 = r * r;
const dot = nx * ny * r;
const cov = dot / 4;
const projLen = Math.abs(ny * r);
const residLen = Math.abs(ny * Math.sin(rad));
thetaOut.value = `${deg.toFixed(1)}°`;
lenOut.value = ny.toFixed(2);
root.querySelector('[data-metric="r"]').textContent = `${r >= 0 ? '+' : ''}${r.toFixed(3)}`;
root.querySelector('[data-metric="r2"]').textContent = `${(r2 * 100).toFixed(1)}%`;
root.querySelector('[data-metric="proj"]').textContent = projLen.toFixed(2);
root.querySelector('[data-metric="resid"]').textContent = residLen.toFixed(2);
root.querySelector('[data-metric="dot"]').textContent = `${dot >= 0 ? '+' : ''}${dot.toFixed(2)}`;
root.querySelector('[data-metric="cov"]').textContent = `${cov >= 0 ? '+' : ''}${cov.toFixed(2)}`;
const reading = root.querySelector('[data-reading]');
if (deg === 0) {
reading.textContent = `Vectores perfectamente alineados: θ = 0°, r = cos(0°) = 1.0. El residuo es nulo (||e|| = 0) y R² = 100%. La elipse colapsa a la recta diagonal.`;
} else if (deg === 90) {
reading.textContent = `Vectores ortogonales: θ = 90°, r = cos(90°) = 0.0. Toda la longitud de ỹ es residuo (||e|| = ||ỹ||) y R² = 0%. La elipse de covarianza es un círculo perfecto.`;
} else if (deg === 180) {
reading.textContent = `Vectores antiparalelos: θ = 180°, r = cos(180°) = −1.0. Asociación lineal negativa perfecta. En espacio estandarizado la pendiente es −1.`;
} else {
reading.textContent = `r = cos(${deg.toFixed(1)}°) = ${r.toFixed(3)}. Al variar ||ỹ|| cambia la covarianza (${cov.toFixed(2)}), pero el ángulo θ y r se mantienen idénticos: Pearson es invariante a la escala.`;
}
// --- Panel 1: Vector Space ---
svgVec.replaceChildren();
arrow(svgVec, 'arr-x', '#2563eb');
arrow(svgVec, 'arr-y', '#d97706');
arrow(svgVec, 'arr-proj', '#0891b2');
const ox = 70, oy = 210, scale = 32;
// Axis line
add(svgVec, 'line', { x1: 20, x2: 300, y1: oy, y2: oy, stroke: '#cbd5e1', 'stroke-width': '1.5' });
add(svgVec, 'circle', { cx: ox, cy: oy, r: '3.5', fill: '#334155' });
// Vector x~ (fixed horizontal)
const xx = ox + scale * nx;
add(svgVec, 'line', { x1: ox, y1: oy, x2: xx, y2: oy, stroke: '#2563eb', 'stroke-width': '3.5', 'marker-end': 'url(#arr-x)' });
add(svgVec, 'text', { x: xx + 8, y: oy + 4, fill: '#2563eb', 'font-size': '11', 'font-weight': '700' }, 'x̃');
// Vector y~
const yx = ox + scale * ny * r;
const yy = oy - scale * ny * Math.sin(rad);
add(svgVec, 'line', { x1: ox, y1: oy, x2: yx, y2: yy, stroke: '#d97706', 'stroke-width': '3.5', 'marker-end': 'url(#arr-y)' });
add(svgVec, 'text', { x: yx + (r >= 0 ? 6 : -18), y: yy - 6, fill: '#d97706', 'font-size': '11', 'font-weight': '700' }, 'ỹ');
// Orthogonal Projection y^
const px = ox + scale * ny * r;
add(svgVec, 'line', { x1: ox, y1: oy, x2: px, y2: oy, stroke: '#0891b2', 'stroke-width': '2.5', 'marker-end': 'url(#arr-proj)' });
// Residual line e = y~ - y^
add(svgVec, 'line', { x1: yx, y1: yy, x2: px, y2: oy, stroke: '#7c3aed', 'stroke-width': '2', 'stroke-dasharray': '4 3' });
add(svgVec, 'circle', { cx: px, cy: oy, r: '3', fill: '#0891b2' });
// Right angle square
if (Math.abs(Math.sin(rad)) > 0.1 && Math.abs(r) > 0.05) {
const sq = 8;
const sgn = r >= 0 ? 1 : -1;
add(svgVec, 'path', {
d: `M ${px - sgn * sq} ${oy} L ${px - sgn * sq} ${oy - sq} L ${px} ${oy - sq}`,
fill: 'none', stroke: '#7c3aed', 'stroke-width': '1.2'
});
}
// Angle arc
if (deg > 2) {
const arcR = 36;
const ax = ox + arcR * Math.cos(rad);
const ay = oy - arcR * Math.sin(rad);
const largeArc = deg > 180 ? 1 : 0;
add(svgVec, 'path', {
d: `M ${ox + arcR} ${oy} A ${arcR} ${arcR} 0 ${largeArc} 0 ${ax} ${ay}`,
fill: 'none', stroke: '#334155', 'stroke-width': '1.5'
});
const midRad = rad / 2;
add(svgVec, 'text', {
x: ox + (arcR + 14) * Math.cos(midRad),
y: oy - (arcR + 14) * Math.sin(midRad) + 4,
fill: '#334155', 'font-size': '11', 'font-weight': '600'
}, 'θ');
}
// Pythagorean labels
add(svgVec, 'text', { x: 18, y: 30, fill: '#0891b2', 'font-size': '10', 'font-weight': '600' }, `ŷ = proy(ỹ) = ${projLen.toFixed(2)}`);
add(svgVec, 'text', { x: 18, y: 46, fill: '#7c3aed', 'font-size': '10', 'font-weight': '600' }, `e = residuo = ${residLen.toFixed(2)}`);
add(svgVec, 'text', { x: 18, y: 62, fill: '#334155', 'font-size': '10' }, `||ỹ||² = ||ŷ||² + ||e||²`);
// --- Panel 2: Standardized Space (z_X, z_Y) ---
svgStd.replaceChildren();
const scx = 160, scy = 140, sScale = 38;
const toSx = z => scx + z * sScale;
const toSy = z => scy - z * sScale;
// Grid [-3, 3]
[-2, 0, 2].forEach(v => {
add(svgStd, 'line', { x1: toSx(-3), x2: toSx(3), y1: toSy(v), y2: toSy(v), stroke: v === 0 ? '#64748b' : '#e2e8f0', 'stroke-width': v === 0 ? '1.5' : '1' });
add(svgStd, 'line', { x1: toSx(v), x2: toSx(v), y1: toSy(-3), y2: toSy(3), stroke: v === 0 ? '#64748b' : '#e2e8f0', 'stroke-width': v === 0 ? '1.5' : '1' });
if (v !== 0) {
add(svgStd, 'text', { x: toSx(v), y: scy + 14, 'text-anchor': 'middle', fill: '#94a3b8', 'font-size': '9' }, v);
add(svgStd, 'text', { x: scx - 8, y: toSy(v) + 3, 'text-anchor': 'end', fill: '#94a3b8', 'font-size': '9' }, v);
}
});
// Covariance Ellipse: semi-axes in 45-degree rotated coords
// eigenvalues of [[1, r], [r, 1]] are 1+r and 1-r
// semi-major axis = 2 * sqrt(1 + |r|), semi-minor = 2 * sqrt(1 - |r|)
const cVal = 1.96; // 95% confidence level approx
const a1 = sScale * Math.sqrt(Math.max(0.01, cVal * (1 + Math.abs(r))));
const a2 = sScale * Math.sqrt(Math.max(0.01, cVal * (1 - Math.abs(r))));
const rotDeg = r >= 0 ? -45 : 45;
add(svgStd, 'ellipse', {
cx: scx, cy: scy, rx: a1, ry: a2,
transform: `rotate(${rotDeg} ${scx} ${scy})`,
fill: 'rgba(37, 99, 235, 0.08)',
stroke: '#2563eb', 'stroke-width': '2'
});
// Regression line z_Y = r * z_X
const lineX1 = -2.8, lineY1 = r * lineX1;
const lineX2 = 2.8, lineY2 = r * lineX2;
add(svgStd, 'line', {
x1: toSx(lineX1), y1: toSy(lineY1),
x2: toSx(lineX2), y2: toSy(lineY2),
stroke: '#d97706', 'stroke-width': '2.5'
});
add(svgStd, 'text', { x: toSx(2.6), y: toSy(2.6 * r) - 6, fill: '#d97706', 'font-size': '10', 'font-weight': '700' }, `pendiente = r = ${r.toFixed(2)}`);
add(svgStd, 'text', { x: toSx(2.8), y: scy - 6, fill: '#64748b', 'font-size': '10' }, 'z_X');
add(svgStd, 'text', { x: scx + 6, y: toSy(2.8), fill: '#64748b', 'font-size': '10' }, 'z_Y');
}
[thetaInp, lenInp].forEach(inp => inp.addEventListener('input', update));
root.querySelectorAll('[data-preset-p]').forEach(btn => {
btn.addEventListener('click', () => {
const [deg, length] = btn.dataset.presetP.split(',').map(Number);
thetaInp.value = deg;
lenInp.value = length;
root.querySelectorAll('[data-preset-p]').forEach(b => b.setAttribute('aria-pressed', b === btn ? 'true' : 'false'));
update();
});
});
root.querySelector('[data-preset-p="36.87,3.1623"]').setAttribute('aria-pressed', 'true');
update();
})();
</script>
```
:::
::: {.content-visible unless-format="html"}
El laboratorio HTML permite variar $\theta$ entre $0^\circ$ y $180^\circ$ y
cambiar $\lVert\widetilde{\mathbf y}\rVert$ por separado. El primer control
modifica $r=\cos\theta$; el segundo modifica la covarianza, pero no $r$.
:::
Por la desigualdad de Cauchy--Schwarz,[^cauchy] $-1\le r\le1$. Los extremos
$r=1$ y $r=-1$ aparecen cuando todos los puntos están exactamente sobre una
recta con pendiente positiva o negativa, respectivamente. Si alguna variable
es constante, su desviación estándar es cero y $r$ no está definido.
::: {.callout-important appearance="minimal"}
### Qué significa y qué no significa $r=0.8$
En estos cinco datos hay una asociación lineal positiva apreciable. El número
no significa que “80% de $Y$ sea causado por $X$”, ni que cada hora adicional
produzca 0.8 retos. Tampoco autoriza una etiqueta universal como “fuerte” sin
considerar el campo, el tamaño muestral, la incertidumbre y la gráfica.
:::
Sumar una constante a cualquiera de las variables o cambiar sus unidades por
un factor positivo no altera $r$. Multiplicar una de ellas por un factor
negativo refleja el eje y cambia únicamente el signo.
## Spearman: comparar órdenes
### Paso 1: Intuición con un ejemplo
¿Qué ocurre si dos variables crecen siempre juntas, pero la relación no es una
línea recta? Imaginemos que el rendimiento creciera de forma exponencial con las
horas de práctica: $X = (1, 2, 3, 4, 5)$ y $Y = (2, 4, 8, 16, 32)$.
En ese caso, cada hora adicional produce todavía más retos resueltos que la
anterior. La relación es **monótona creciente**:[^monotona] siempre que $X$
aumenta, $Y$ también aumenta, sin una sola excepción. Sin embargo, si calculamos
Pearson sobre esos datos, obtendremos $r < 1$ porque los puntos forman una curva
y Pearson penaliza las distancias no alineadas a una recta.
Si la pregunta sustantiva no es si los puntos se acomodan sobre una recta, sino
únicamente si *al aumentar una variable, la otra también aumenta*, penalizar la
curvatura distorsiona la conclusión.
La idea de Charles Spearman [@spearman1904proof] consiste en **ignorar las
distancias métricas originales** y conservar únicamente las posiciones relativas
u órdenes: reemplazamos cada dato por su **rango** ($1^\circ, 2^\circ, \dots, n^\circ$).[^rango]
### Paso 2: Cálculo a mano paso a paso
Retomemos los cinco estudiantes originales:
- Horas de práctica: $X = (1, 2, 3, 4, 5)$. Como ya están en orden creciente,
sus rangos son directamente:
$$R_X = (1, 2, 3, 4, 5).$$
- Retos resueltos: $Y = (2, 1, 4, 3, 5)$. Ordenemos los retos de menor a mayor:
el menor es 1 (estudiante B $\to$ rango 1), luego 2 (estudiante A $\to$ rango 2),
luego 3 (estudiante D $\to$ rango 3), luego 4 (estudiante C $\to$ rango 4) y
el mayor es 5 (estudiante E $\to$ rango 5). Por tanto:
$$R_Y = (2, 1, 4, 3, 5).$$
Construyamos la tabla de diferencias entre rangos $d_i = R_{X,i} - R_{Y,i}$:
| Estudiante | $x_i$ | $y_i$ | Rango $R_{X,i}$ | Rango $R_{Y,i}$ | Diferencia $d_i = R_{X,i} - R_{Y,i}$ | Cuadrado $d_i^2$ |
|:---:|---:|---:|---:|---:|---:|---:|
| A | 1 | 2 | 1 | 2 | $1 - 2 = -1$ | $(-1)^2 = \mathbf{1}$ |
| B | 2 | 1 | 2 | 1 | $2 - 1 = +1$ | $(+1)^2 = \mathbf{1}$ |
| C | 3 | 4 | 3 | 4 | $3 - 4 = -1$ | $(-1)^2 = \mathbf{1}$ |
| D | 4 | 3 | 4 | 3 | $4 - 3 = +1$ | $(+1)^2 = \mathbf{1}$ |
| E | 5 | 5 | 5 | 5 | $5 - 5 = 0$ | $0^2 = \mathbf{0}$ |
| **Suma** | | | | | **0** | **4** |
Observemos dos detalles del cálculo manual:
1. La suma de las diferencias es siempre cero: $\sum d_i = (-1) + 1 + (-1) + 1 + 0 = 0$.
2. La suma de las diferencias al cuadrado es $\sum d_i^2 = 1 + 1 + 1 + 1 + 0 = 4$.
Cuando **no hay empates**, podemos usar la fórmula abreviada de Spearman:
$$
\rho_s = 1 - \frac{6 \sum_{i=1}^n d_i^2}{n(n^2 - 1)}. \tag{7}
$$
Sustituyendo $n = 5$ y $\sum d_i^2 = 4$:
$$
\rho_s = 1 - \frac{6(4)}{5(5^2 - 1)}
= 1 - \frac{24}{5(24)}
= 1 - \frac{24}{120}
= 1 - 0.2 = 0.8.
$$
**¿Qué ocurre si hay empates?**
Si dos o más observaciones comparten el mismo valor en una variable, se les
asigna el **rango promedio** de las posiciones que habrían ocupado. Por ejemplo,
si dos estudiantes empatan en el segundo y tercer puesto, ambos reciben el rango
$\frac{2 + 3}{2} = 2.5$. En ese caso, la varianza de los rangos disminuye y la
fórmula abreviada (Ec. 7) deja de ser exacta; debe aplicarse la correlación de
Pearson habitual directamente sobre las columnas de rangos promedio.
### Paso 3: Formalización matemática
El **coeficiente de correlación de Spearman** $\rho_s$ se define formalmente
como el coeficiente de correlación de Pearson aplicado a los vectores de rangos:
$$
\rho_s = \operatorname{corr}(R_X, R_Y). \tag{8}
$$
**Deducción de la fórmula abreviada.**
Sin empates, tanto $R_X$ como $R_Y$ son permutaciones del conjunto de enteros
$\{1, 2, \ldots, n\}$. La media de cualquier permutación es fija:
$\bar{R} = \frac{n+1}{2}$, y la suma de cuadrados respecto a la media es una
constante universal:
$$
\sum_{i=1}^n (R_i - \bar{R})^2 = \frac{n(n^2 - 1)}{12}.
$$
Desarrollando la suma de diferencias al cuadrado:
$$
\sum d_i^2 = \sum (R_{X,i} - R_{Y,i})^2 = \sum (R_{X,i} - \bar{R})^2 + \sum (R_{Y,i} - \bar{R})^2 - 2\sum (R_{X,i} - \bar{R})(R_{Y,i} - \bar{R}).
$$
Al sustituir las sumas de cuadrados constantes y despejar el término de
covarianza, se obtiene directamente la constante 6 y el denominador $n(n^2 - 1)$.
- **Invarianza monótona:** $\rho_s$ es invariante ante cualquier transformación
estrictamente creciente $g(X)$ y $h(Y)$ (por ejemplo, logaritmos o potencias
impares), ya que $R_{g(X)} = R_X$.
- **Límites:** $-1 \le \rho_s \le 1$. Vale $+1$ cuando el orden de ambas
variables coincide perfectamente, y $-1$ cuando una está en orden exactamente
inverso al de la otra.
### Paso 4: De la intuición geométrica al laboratorio
Spearman combina dos perspectivas geométricas fundamentales:
1. **En la cuadrícula de rangos $\{1,\dots,n\}^2$ (sumar cuadrados geométricos):**
Los pares de rangos $(R_{X,i}, R_{Y,i})$ habitan una cuadrícula rígida de
enteros. La recta diagonal $R_Y = R_X$ representa el orden perfecto. Cada
término $d_i^2 = (R_{X,i} - R_{Y,i})^2$ es literalmente el **área de un
cuadrado geométrico** de lado $|d_i|$ trazado entre el punto y la diagonal.
Cuanto menor sea el área total acumulada por estos cuadrados, mayor será
$\rho_s$.
2. **En el espacio de permutaciones sobre una esfera $\mathbb{R}^n$ (el permutoedro[^permutoedro] y la Ley de Cosenos):**
Al centrar los rangos restando $\bar{R} = 3$, todos los vectores de rangos
sin empates tienen exactamente la misma longitud fija:
$$\lVert\widetilde{\mathbf R}\rVert = R_0 = \sqrt{\frac{n(n^2-1)}{12}} = \sqrt{\frac{5(24)}{12}} = \sqrt{10} \approx 3.162.$$
Las $5! = 120$ permutaciones posibles son los vértices de un polítopo
convexo (el permutoedro) que descansan exactamente sobre una hiperesfera de
radio $\sqrt{10}$. La fórmula abreviada de Spearman es literalmente la
**Ley de Cosenos** sobre esta esfera:
$$\lVert\mathbf{d}\rVert^2 = \sum d_i^2 = 2 R_0^2 (1 - \cos\varphi) = 2(10)(1 - \rho_s) = 20(1 - \rho_s).$$
Cuando se introducen empates mediante rangos promedio, el vector **se hunde
hacia el interior de la esfera** ($\lVert\widetilde{\mathbf R}\rVert < \sqrt{10}$),
reduciendo la varianza disponible.
En el siguiente laboratorio interactivo puede manipular las permutaciones de
rangos, verificar las áreas cuadradas $d_i^2$ en la cuadrícula y ver cómo el
vector de rangos recorre la esfera o colapsa al interior cuando se activan los
empates.
::: {.content-visible when-format="html"}
```{=html}
<div id="spearman-geometry-lab" class="geom-lab" data-geometry="spearman" aria-labelledby="spearman-geometry-title">
<style>
#spearman-geometry-lab .dual-svg-grid{display:grid;grid-template-columns:1fr 1fr;gap:.8rem}
@media(max-width:860px){#spearman-geometry-lab .dual-svg-grid{grid-template-columns:1fr}}
</style>
<div class="lab-shell">
<h3 id="spearman-geometry-title" class="lab-title">Geometría de Spearman: cuadrícula de rangos y ley de cosenos en la esfera</h3>
<p class="lab-intro">Sin empates, todos los vectores de rangos centrados tienen exactamente la misma norma $\|\tilde{\mathbf{R}}\| = \sqrt{10}$. A la izquierda, la discrepancia $d_i = R_{X,i} - R_{Y,i}$ es la distancia a la diagonal $R_Y = R_X$, visualizada con cuadrados de área $d_i^2$. A la derecha, la fórmula abreviada de Spearman es exactamente la <strong>Ley de Cosenos</strong> sobre la esfera: $\|\mathbf{d}\|^2 = \sum d_i^2 = 2R_0^2(1 - \cos\varphi) = 20(1 - \rho_s)$.</p>
<div class="lab-layout">
<div class="lab-controls">
<div class="preset-strip">
<button type="button" data-preset-s="1,2,3,4,5">Mismo orden (ρ = +1.0)</button>
<button type="button" data-preset-s="2,1,4,3,5">Ejemplo libro (ρ = +0.8)</button>
<button type="button" data-preset-s="1,5,4,3,2">Ortogonales (ρ = 0.0)</button>
<button type="button" data-preset-s="5,4,3,2,1">Orden inverso (ρ = −1.0)</button>
<button type="button" data-preset-s="1.5,1.5,3,4,5">Con empates (dentro de la esfera)</button>
</div>
<div class="range-row">
<label for="spearman-perm">Permutación de rangos R<sub>Y</sub> (120 posibles)</label>
<input id="spearman-perm" data-perm type="range" min="0" max="119" step="1" value="0">
<output data-perm-out>1 de 120</output>
</div>
<div class="metrics-grid" aria-live="polite">
<div class="metric-card"><span>Spearman ρ<sub>s</sub> = cos φ</span><strong data-metric="rho">+0.800</strong></div>
<div class="metric-card"><span>Ángulo de rangos φ</span><strong data-metric="phi">36.9°</strong></div>
<div class="metric-card"><span>Suma distancias Σd<sub>i</sub>²</span><strong data-metric="sumd">4</strong></div>
<div class="metric-card"><span>Longitud cuerda ||d||</span><strong data-metric="chord">2.00</strong></div>
<div class="metric-card"><span>Radio ||R̃<sub>Y</sub>||</span><strong data-metric="norm">3.162 (√10)</strong></div>
<div class="metric-card"><span>Ubicación en la esfera</span><strong data-metric="sphere-loc">Superficie</strong></div>
</div>
<p class="reading-box" data-reading aria-live="polite"></p>
</div>
<div>
<div class="dual-svg-grid">
<div>
<p style="font-size:.78rem;font-weight:700;color:var(--muted);margin:0 0 .3rem;text-align:center">1. Cuadrícula de rangos: distancias a la diagonal dᵢ²</p>
<svg data-svg-rank viewBox="0 0 320 280" role="img" aria-label="Cuadrícula de rangos discretos con cuadrados de diferencias"></svg>
</div>
<div>
<p style="font-size:.78rem;font-weight:700;color:var(--muted);margin:0 0 .3rem;text-align:center">2. Esfera de permutaciones: ley de cosenos y cuerda ||d||</p>
<svg data-svg-sphere viewBox="0 0 320 280" role="img" aria-label="Esfera de permutaciones y ángulo de Spearman"></svg>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
(() => {
const root = document.getElementById('spearman-geometry-lab');
if (!root) return;
const NS = 'http://www.w3.org/2000/svg';
const slider = root.querySelector('[data-perm]');
const permOut = root.querySelector('[data-perm-out]');
const svgRank = root.querySelector('[data-svg-rank]');
const svgSphere = root.querySelector('[data-svg-sphere]');
const permutations = a => a.length === 1 ? [a] : a.flatMap((v, i) => permutations([...a.slice(0, i), ...a.slice(i + 1)]).map(p => [v, ...p]));
const perms = permutations([1, 2, 3, 4, 5]);
slider.max = perms.length - 1;
let specialRanks = null;
const rx = [1, 2, 3, 4, 5];
const R0 = Math.sqrt(10);
const mean = a => a.reduce((s, v) => s + v, 0) / a.length;
const center = a => { const m = mean(a); return a.map(v => v - m); };
const add = (parent, tag, attrs, text = '') => {
const el = document.createElementNS(NS, tag);
Object.entries(attrs).forEach(([k, v]) => el.setAttribute(k, v));
if (text) el.textContent = text;
parent.appendChild(el);
return el;
};
const arrow = (svg, id, color) => {
const defs = add(svg, 'defs', {});
const marker = add(defs, 'marker', { id, viewBox: '0 0 10 10', refX: '7', refY: '5', markerWidth: '6', markerHeight: '6', orient: 'auto-start-reverse' });
add(marker, 'path', { d: 'M 0 0 L 10 5 L 0 10 z', fill: color });
};
function update() {
const ry = specialRanks || perms[+slider.value];
const dry = center(ry);
const drx = center(rx);
const normY = Math.hypot(...dry);
const normX = Math.hypot(...drx);
const dot = drx.reduce((s, v, i) => s + v * dry[i], 0);
const rho = normX && normY ? dot / (normX * normY) : NaN;
const phi = Number.isFinite(rho) ? Math.acos(Math.max(-1, Math.min(1, rho))) : NaN;
const degPhi = Number.isFinite(phi) ? (phi * 180 / Math.PI) : NaN;
const diffs = rx.map((v, i) => v - ry[i]);
const sumd = diffs.reduce((s, v) => s + v * v, 0);
const chordLen = Math.sqrt(sumd);
const hasTies = Math.abs(normY - R0) > 0.05;
permOut.value = specialRanks ? 'Rangos con empates' : `${+slider.value + 1} de 120`;
root.querySelector('[data-metric="rho"]').textContent = `${rho >= 0 ? '+' : ''}${rho.toFixed(3)}`;
root.querySelector('[data-metric="phi"]').textContent = `${degPhi.toFixed(1)}°`;
root.querySelector('[data-metric="sumd"]').textContent = sumd.toFixed(hasTies ? 2 : 0);
root.querySelector('[data-metric="chord"]').textContent = chordLen.toFixed(2);
root.querySelector('[data-metric="norm"]').textContent = `${normY.toFixed(3)}${hasTies ? ' (< √10)' : ' (= √10)'}`;
root.querySelector('[data-metric="sphere-loc"]').textContent = hasTies ? 'Interior (colapsado)' : 'Superficie (radio √10)';
const reading = root.querySelector('[data-reading]');
if (hasTies) {
reading.textContent = `Con empates (R_Y = [${ry.join(', ')}]), promediar posiciones reduce la variabilidad: ||R̃_Y|| = ${normY.toFixed(2)} < √10. El vector penetra al interior de la esfera; por eso la fórmula 1 − 6Σd²/(n(n²−1)) ya no es exacta y se debe calcular Pearson sobre los rangos.`;
} else if (sumd === 0) {
reading.textContent = `Orden idéntico: todos los puntos están sobre la diagonal (dᵢ = 0). La cuerda ||d|| = 0 y el ángulo φ = 0°. ρ_s = cos(0°) = 1.0.`;
} else if (sumd === 40) {
reading.textContent = `Orden estrictamente inverso: la cuerda es el diámetro completo de la esfera (||d|| = 2√10 ≈ 6.32), φ = 180° y ρ_s = cos(180°) = −1.0.`;
} else {
reading.textContent = `R_Y = (${ry.join(', ')}). Ley de cosenos: ||d||² = Σdᵢ² = ${sumd}. Como el radio es √10: cos(φ) = 1 − Σdᵢ² / 20 = 1 − ${sumd}/20 = ${rho.toFixed(3)}. ¡La fórmula de Spearman es pura trigonometría esférica!`;
}
// --- Panel 1: Rank Grid ---
svgRank.replaceChildren();
const l1 = 45, r1 = 295, t1 = 30, b1 = 250;
const gx = v => l1 + ((v - 1) / 4) * (r1 - l1);
const gy = v => b1 - ((v - 1) / 4) * (b1 - t1);
// Grid
[1, 2, 3, 4, 5].forEach(v => {
add(svgRank, 'line', { x1: l1, x2: r1, y1: gy(v), y2: gy(v), stroke: '#e2e8f0', 'stroke-width': '1' });
add(svgRank, 'line', { x1: gx(v), x2: gx(v), y1: t1, y2: b1, stroke: '#e2e8f0', 'stroke-width': '1' });
add(svgRank, 'text', { x: l1 - 8, y: gy(v) + 4, 'text-anchor': 'end', fill: '#64748b', 'font-size': '10' }, v);
add(svgRank, 'text', { x: gx(v), y: b1 + 18, 'text-anchor': 'middle', fill: '#64748b', 'font-size': '10' }, v);
});
// Identity line R_Y = R_X
add(svgRank, 'line', { x1: gx(1), y1: gy(1), x2: gx(5), y2: gy(5), stroke: '#94a3b8', 'stroke-width': '1.5', 'stroke-dasharray': '5 4' });
// Differences and square areas d_i^2
rx.forEach((v, i) => {
const px = gx(v);
const py = gy(ry[i]);
const diagY = gy(v);
const d = v - ry[i];
if (Math.abs(d) > 0.01) {
// Vertical segment to diagonal
add(svgRank, 'line', { x1: px, y1: py, x2: px, y2: diagY, stroke: '#d97706', 'stroke-width': '2' });
// Visual square of area d^2
const sqSize = Math.abs(px - gx(ry[i]));
const sqY = Math.min(py, diagY);
add(svgRank, 'rect', {
x: px, y: sqY, width: sqSize, height: sqSize,
fill: 'rgba(217, 119, 6, 0.18)', stroke: '#d97706', 'stroke-width': '1', 'stroke-dasharray': '2 2'
});
}
// Point circle
add(svgRank, 'circle', { cx: px, cy: py, r: '8', fill: '#7c3aed', stroke: '#fff', 'stroke-width': '2' });
add(svgRank, 'text', { x: px, y: py + 3.5, 'text-anchor': 'middle', fill: '#fff', 'font-size': '8.5', 'font-weight': 'bold' }, String.fromCharCode(65 + i));
});
add(svgRank, 'text', { x: (l1 + r1) / 2, y: b1 + 28, 'text-anchor': 'middle', fill: '#475569', 'font-size': '10', 'font-weight': '600' }, 'Rango R_X');
add(svgRank, 'text', { x: 14, y: (t1 + b1) / 2, transform: `rotate(-90 14 ${(t1 + b1) / 2})`, 'text-anchor': 'middle', fill: '#475569', 'font-size': '10', 'font-weight': '600' }, 'Rango R_Y');
// --- Panel 2: Permutation Sphere ---
svgSphere.replaceChildren();
arrow(svgSphere, 'arr-rx', '#2563eb');
arrow(svgSphere, 'arr-ry', '#7c3aed');
arrow(svgSphere, 'arr-chord', '#d97706');
const scx = 135, scy = 145, sphereRadius = 100;
const sphereScale = sphereRadius / R0;
// Outer circle (radius = sqrt(10))
add(svgSphere, 'circle', {
cx: scx, cy: scy, r: sphereRadius,
fill: 'rgba(37, 99, 235, 0.03)', stroke: '#94a3b8', 'stroke-width': '1.5', 'stroke-dasharray': '5 5'
});
add(svgSphere, 'text', { x: scx, y: scy - sphereRadius - 7, 'text-anchor': 'middle', fill: '#64748b', 'font-size': '10' }, 'Esfera ||R̃|| = √10');
// Origin
add(svgSphere, 'circle', { cx: scx, cy: scy, r: '3.5', fill: '#334155' });
// Vector R_X (along horizontal)
const rxx = scx + sphereRadius;
add(svgSphere, 'line', { x1: scx, y1: scy, x2: rxx, y2: scy, stroke: '#2563eb', 'stroke-width': '3.5', 'marker-end': 'url(#arr-rx)' });
add(svgSphere, 'text', { x: rxx + 8, y: scy + 4, fill: '#2563eb', 'font-size': '11', 'font-weight': '700' }, 'R̃_X');
// Vector R_Y at angle phi
const ryx = scx + sphereScale * normY * Math.cos(phi || 0);
const ryy = scy - sphereScale * normY * Math.sin(phi || 0);
add(svgSphere, 'line', { x1: scx, y1: scy, x2: ryx, y2: ryy, stroke: '#7c3aed', 'stroke-width': '3.5', 'marker-end': 'url(#arr-ry)' });
add(svgSphere, 'text', { x: ryx + 6, y: ryy - 6, fill: '#7c3aed', 'font-size': '11', 'font-weight': '700' }, 'R̃_Y');
// Chord vector d = R_X - R_Y
if (chordLen > 0.05) {
add(svgSphere, 'line', {
x1: rxx, y1: scy, x2: ryx, y2: ryy,
stroke: '#d97706', 'stroke-width': '2.5', 'stroke-dasharray': '4 3'
});
add(svgSphere, 'text', {
x: (rxx + ryx) / 2 + 6, y: (scy + ryy) / 2 - 6,
fill: '#d97706', 'font-size': '10', 'font-weight': '700'
}, `||d|| = ${chordLen.toFixed(2)}`);
}
// Angle phi arc
if (degPhi > 2) {
const arcR = 34;
const ax = scx + arcR * Math.cos(phi);
const ay = scy - arcR * Math.sin(phi);
const largeArc = degPhi > 180 ? 1 : 0;
add(svgSphere, 'path', {
d: `M ${scx + arcR} ${scy} A ${arcR} ${arcR} 0 ${largeArc} 0 ${ax} ${ay}`,
fill: 'none', stroke: '#334155', 'stroke-width': '1.5'
});
const midPhi = phi / 2;
add(svgSphere, 'text', {
x: scx + (arcR + 13) * Math.cos(midPhi),
y: scy - (arcR + 13) * Math.sin(midPhi) + 4,
fill: '#334155', 'font-size': '11', 'font-weight': '600'
}, 'φ');
}
// Law of cosines note
add(svgSphere, 'text', { x: 12, y: 265, fill: '#334155', 'font-size': '9.5' }, `Ley de Cosenos: ||d||² = 20(1 − cos φ) = Σdᵢ² = ${sumd.toFixed(1)}`);
}
slider.addEventListener('input', () => {
specialRanks = null;
root.querySelectorAll('[data-preset-s]').forEach(b => b.setAttribute('aria-pressed', 'false'));
update();
});
root.querySelectorAll('[data-preset-s]').forEach(btn => {
btn.addEventListener('click', () => {
const chosen = btn.dataset.presetS.split(',').map(Number);
const idx = perms.findIndex(p => p.every((v, i) => v === chosen[i]));
specialRanks = idx < 0 ? chosen : null;
if (idx >= 0) slider.value = idx;
root.querySelectorAll('[data-preset-s]').forEach(b => b.setAttribute('aria-pressed', b === btn ? 'true' : 'false'));
update();
});
});
// Init with example [2, 1, 4, 3, 5]
const initialIdx = perms.findIndex(p => p.every((v, i) => v === [2, 1, 4, 3, 5][i]));
slider.value = initialIdx;
root.querySelector('[data-preset-s="2,1,4,3,5"]').setAttribute('aria-pressed', 'true');
update();
})();
</script>
```
:::
::: {.content-visible unless-format="html"}
La versión HTML recorre las $5!=120$ permutaciones de rangos. Sin empates,
$\lVert\widetilde{\mathbf R}_Y\rVert=\sqrt{10}$; un botón introduce rangos
promedio y muestra cómo el vector se desplaza al interior de la esfera.
:::
## Kendall: contar concordancias
### Paso 1: Intuición con un ejemplo
Tanto Pearson como Spearman calculan distancias: Pearson usa desviaciones
numéricas métricas y Spearman usa desviaciones numéricas entre enteros $d_i^2$.
Maurice Kendall [@kendall1938measure] propuso una alternativa todavía más
fundamental, robusta y libre de supuestos métricos:
*Si enfrentamos a las observaciones en un "torneo" por parejas, de dos en dos,
¿coinciden en quién está por delante?*
Tomemos a dos estudiantes cualesquiera del grupo, por ejemplo A y B:
- Estudiante A: 1 hora de práctica, 2 retos resueltos.
- Estudiante B: 2 horas de práctica, 1 reto resuelto.
B practicó más horas que A ($2 > 1$), pero resolvió menos retos que A ($1 < 2$).
El orden relativo se invirtió entre ambas variables. Decimos que el par (A, B) es
**discordante**.
Ahora comparemos a A con C:
- Estudiante A: 1 hora de práctica, 2 retos resueltos.
- Estudiante C: 3 horas de práctica, 4 retos resueltos.
C practicó más horas que A ($3 > 1$) y también resolvió más retos ($4 > 2$). El
orden relativo se mantuvo idéntico. Decimos que el par (A, C) es **concordante**.
La correlación de Kendall responde a una pregunta probabilística elemental:
*al elegir dos observaciones al azar, ¿qué tan más probable es que sus órdenes
concuerden en lugar de discrepar?*
### Paso 2: Cálculo a mano paso a paso
Con $n = 5$ estudiantes, el número total de parejas posibles es:
$$
\binom{5}{2} = \frac{5 \times 4}{2} = 10 \text{ pares}.
$$
Como la variable $X$ (horas) ya está ordenada de menor a mayor
($A < B < C < D < E$), en cada par $(i, j)$ con $i < j$ la variable $X$ siempre
crece ($x_j - x_i > 0$). Por tanto, la concordancia depende únicamente del signo
de $y_j - y_i$.
Evaluemos a mano los 10 pares posibles en una tabla exhaustiva:
| Par $(i, j)$ | Horas $(x_i, x_j)$ | Retos $(y_i, y_j)$ | $x_j - x_i$ | $y_j - y_i$ | Signo del producto $(x_j-x_i)(y_j-y_i)$ | Clasificación |
|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
| (A, B) | (1, 2) | (2, 1) | $+1$ | $-1$ | $(-)$ | **Discordante (−)** |
| (A, C) | (1, 3) | (2, 4) | $+2$ | $+2$ | $(+)$ | **Concordante (+)** |
| (A, D) | (1, 4) | (2, 3) | $+3$ | $+1$ | $(+)$ | **Concordante (+)** |
| (A, E) | (1, 5) | (2, 5) | $+4$ | $+3$ | $(+)$ | **Concordante (+)** |
| (B, C) | (2, 3) | (1, 4) | $+1$ | $+3$ | $(+)$ | **Concordante (+)** |
| (B, D) | (2, 4) | (1, 3) | $+2$ | $+2$ | $(+)$ | **Concordante (+)** |
| (B, E) | (2, 5) | (1, 5) | $+3$ | $+4$ | $(+)$ | **Concordante (+)** |
| (C, D) | (3, 4) | (4, 3) | $+1$ | $-1$ | $(-)$ | **Discordante (−)** |
| (C, E) | (3, 5) | (4, 5) | $+2$ | $+1$ | $(+)$ | **Concordante (+)** |
| (D, E) | (4, 5) | (3, 5) | $+1$ | $+2$ | $(+)$ | **Concordante (+)** |
Hagamos el balance final:
- Pares concordantes ($C$): **8**
- Pares discordantes ($D$): **2**
- Pares empatados ($T$): **0**
- Total de pares evaluados: $C + D = 10$
Calculamos **Kendall tau-a** ($\tau_a$), válida cuando no hay empates:
$$
\tau_a = \frac{C - D}{\binom{n}{2}}
= \frac{8 - 2}{10}
= \frac{6}{10} = 0.6. \tag{9}
$$
**Interpretación probabilística rigurosa.**
Si seleccionamos al azar dos estudiantes de esta muestra:
$$P(\text{concordancia}) = \frac{8}{10} = 0.8, \qquad P(\text{discordancia}) = \frac{2}{10} = 0.2.$$
El coeficiente $\tau_a$ es la diferencia exacta entre ambas probabilidades:
$$\tau_a = P(\text{concordancia}) - P(\text{discordancia}) = 0.8 - 0.2 = 0.6.$$
Kendall no mide distancias entre valores ni curvatura; mide **concordancia
ordinal**.[^ordinal] Su escala representa una diferencia de probabilidades y por
ello no debe esperarse que coincida numéricamente con el 0.8 de Pearson o
Spearman. De hecho, bajo normalidad bivariada teórica se cumple
$\tau = \frac{2}{\pi}\arcsin r$, lo que para $r = 0.8$ predice
$\tau \approx 0.59$. Un valor $\tau = 0.6$ refleja exactamente la misma
intensidad de asociación que $r = 0.8$.
### Paso 3: Formalización matemática
Formalmente, para cada par de observaciones $(i, j)$ con $i < j$, definimos el
producto de signos:
$$
h_{ij} = \operatorname{sgn}(x_j - x_i) \cdot \operatorname{sgn}(y_j - y_i),
$$
donde $\operatorname{sgn}(u)$ es la función signo ($+1$ si $u > 0$, $-1$ si
$u < 0$, y $0$ si $u = 0$). El par es:
- **Concordante** si $h_{ij} = +1$;
- **Discordante** si $h_{ij} = -1$ (indica una inversión de orden[^inversion]);
- **Empatado** si $h_{ij} = 0$.
El coeficiente **Kendall tau-a** es el promedio de estos signos:
$$
\tau_a = \frac{1}{\binom{n}{2}} \sum_{1 \le i < j \le n} h_{ij} = \frac{C - D}{\binom{n}{2}}. \tag{10}
$$
**Corrección por empates: Kendall tau-b.**
Cuando existen valores repetidos en $X$ o en $Y$, algunos pares producen
$h_{ij} = 0$. Para evitar que los empates se penalicen injustamente como si
fueran discordancias, se utiliza comúnmente **Kendall tau-b**:[^kendallb]
$$
\tau_b = \frac{C - D}{\sqrt{(C + D + T_X)(C + D + T_Y)}}, \tag{11}
$$
donde $T_X$ es el número de pares empatados únicamente en $X$ y $T_Y$ es el
número de pares empatados únicamente en $Y$. Si no hay empates,
$T_X = T_Y = 0$ y $\tau_b$ coincide con $\tau_a$.
### Paso 4: De la intuición geométrica al laboratorio
La geometría de Kendall es eminentemente combinatoria y gráfica:
1. **Pendientes de cuerdas en el grafo completo $K_n$:**
En el diagrama de dispersión con $X$ creciente, cada par $(i, j)$ define una
**cuerda** o segmento de recta que une los puntos $(x_i, y_i)$ y $(x_j, y_j)$.
La pendiente de esa cuerda es:
$$m_{ij} = \frac{y_j - y_i}{x_j - x_i}.$$
Como $x_j > x_i$, el denominador siempre es positivo. Por tanto:
- **Pendiente positiva ($m_{ij} > 0$):** la cuerda asciende hacia la derecha
(verde) $\leftrightarrow$ par **concordante**.
- **Pendiente negativa ($m_{ij} < 0$):** la cuerda desciende hacia la derecha
(roja) $\leftrightarrow$ par **discordante** (inversión de orden).
- **Pendiente cero ($m_{ij} = 0$):** la cuerda es horizontal (gris)
$\leftrightarrow$ **empate** en $Y$.
2. **La matriz triangular de signos:**
Las $\binom{n}{2}$ cuerdas corresponden a las celdas de una matriz triangular
superior de tamaño $n \times n$. Cada celda contiene el signo $+1$, $-1$ o $0$.
Kendall $\tau$ es el promedio global de esa matriz.
A diferencia de Pearson y Spearman, Kendall no calcula promedios globales de
desviaciones ni ángulos en $\mathbb{R}^n$: agrega $\binom{n}{2}$ decisiones
locales independientes sobre las orientaciones de las cuerdas.
En el siguiente laboratorio interactivo puede manipular las alturas de los
puntos: observe cómo cada cuerda rota en tiempo real, cruza la horizontal
cambiando de verde a rojo cuando se produce una inversión, y cómo cada celda de
la matriz de signos refleja instantáneamente la orientación geométrica de su
cuerda.
::: {.content-visible when-format="html"}
```{=html}
<div id="kendall-geometry-lab" class="geom-lab" data-geometry="kendall" aria-labelledby="kendall-geometry-title">
<style>
#kendall-geometry-lab .matrix-wrap{background:#fff;border:1px solid #dce3ed;border-radius:10px;padding:.65rem;margin:.75rem 0}
#kendall-geometry-lab .sign-matrix{display:grid;grid-template-columns:repeat(6,1fr);gap:3px;font-size:.75rem;text-align:center;align-items:center}
#kendall-geometry-lab .matrix-cell{border-radius:4px;padding:.25rem 0;font-weight:700;font-variant-numeric:tabular-nums;cursor:pointer;transition:all .15s}
#kendall-geometry-lab .cell-head{background:transparent;color:var(--muted);font-weight:700}
#kendall-geometry-lab .cell-empty{background:transparent}
#kendall-geometry-lab .cell-pos{background:rgba(22,131,95,.18);color:#0f5132;border:1px solid #16835f}
#kendall-geometry-lab .cell-neg{background:rgba(194,65,59,.18);color:#842029;border:1px solid #c2413b}
#kendall-geometry-lab .cell-tie{background:#f1f5f9;color:#64748b;border:1px solid #cbd5e1}
#kendall-geometry-lab .cell-active{outline:2.5px solid var(--blue);transform:scale(1.08);z-index:2}
</style>
<div class="lab-shell">
<h3 id="kendall-geometry-title" class="lab-title">Geometría de Kendall: pendientes de cuerdas y matriz de signos</h3>
<p class="lab-intro">Kendall $\tau$ evalúa las $\binom{5}{2} = 10$ cuerdas que conectan todos los pares. Como $X$ es creciente, la pendiente del segmento revela la concordancia: verde si asciende ($+$), roja si desciende ($−$) y gris si es horizontal (empate). Arrastre los puntos verticalmente para observar cómo los segmentos giran y cambian de color al ocurrir una inversión.</p>
<div class="lab-layout">
<div class="lab-controls">
<div class="preset-strip">
<button type="button" data-preset-k="2,1,4,3,5">Ejemplo libro (τ = 0.6)</button>
<button type="button" data-preset-k="1,2,3,4,5">Todo concuerda (τ = 1.0)</button>
<button type="button" data-preset-k="5,4,3,2,1">Todo discrepa (τ = −1.0)</button>
<button type="button" data-preset-k="1,1,3,4,4">Con empates en Y</button>
<button type="button" data-preset-k="4,1,0,1,4">Forma de U (τ = 0.0)</button>
</div>
<div class="range-row">
<label for="kendall-pair-slider">Inspeccionar par individual (cuerda)</label>
<input id="kendall-pair-slider" data-pair-slider type="range" min="0" max="9" step="1" value="0">
<output data-pair-out>Par 1 de 10</output>
</div>
<div class="matrix-wrap">
<div style="display:flex;justify-content:space-between;font-size:.75rem;font-weight:700;color:var(--muted);margin-bottom:.35rem">
<span>Matriz triangular de signos</span>
<span style="font-size:.7rem">Haga clic en una celda para aislar el par</span>
</div>
<div class="sign-matrix" data-matrix></div>
</div>
<div class="metrics-grid" aria-live="polite">
<div class="metric-card"><span>Concordantes C (+)</span><strong data-metric="c">8</strong></div>
<div class="metric-card"><span>Discordantes D (−)</span><strong data-metric="d">2</strong></div>
<div class="metric-card"><span>Empates en Y (T<sub>Y</sub>)</span><strong data-metric="ties">0</strong></div>
<div class="metric-card"><span>Kendall τ<sub>b</sub></span><strong data-metric="tau">+0.600</strong></div>
</div>
<p class="reading-box" data-reading aria-live="polite"></p>
</div>
<div>
<div class="legend-row">
<span class="legend-item"><i class="swatch" style="background:#16835f"></i>Concordante (pendiente > 0)</span>
<span class="legend-item"><i class="swatch" style="background:#c2413b"></i>Discordante (pendiente < 0)</span>
<span class="legend-item"><i class="swatch" style="background:#94a3b8"></i>Empate (horizontal)</span>
</div>
<svg data-svg-k viewBox="0 0 640 430" role="img" aria-label="Grafo completo K5 de cuerdas de Kendall con puntos arrastrables"></svg>
</div>
</div>
</div>
</div>
<script>
(() => {
const root = document.getElementById('kendall-geometry-lab');
if (!root) return;
const NS = 'http://www.w3.org/2000/svg';
const x = [1, 2, 3, 4, 5], letters = ['A', 'B', 'C', 'D', 'E'];
let y = [2, 1, 4, 3, 5];
const svg = root.querySelector('[data-svg-k]');
const pairSlider = root.querySelector('[data-pair-slider]');
const pairOut = root.querySelector('[data-pair-out]');
const matrixEl = root.querySelector('[data-matrix]');
const pairs = [];
for (let i = 0; i < 5; i++) {
for (let j = i + 1; j < 5; j++) {
pairs.push({ i, j, name: `${letters[i]}–${letters[j]}` });
}
}
const add = (parent, tag, attrs, text = '') => {
const el = document.createElementNS(NS, tag);
Object.entries(attrs).forEach(([k, v]) => el.setAttribute(k, v));
if (text) el.textContent = text;
parent.appendChild(el);
return el;
};
const l = 60, r = 600, t = 30, b = 360;
const sx = v => l + ((v - 1) / 4) * (r - l);
const sy = v => b - (v / 8) * (b - t);
const invSy = py => Math.max(0, Math.min(8, Math.round(((b - py) / (b - t) * 8) * 2) / 2));
let draggedIdx = null;
function renderMatrix(selectedPairIdx, signs) {
matrixEl.replaceChildren();
// Headers
const emptyCorner = document.createElement('div');
emptyCorner.className = 'cell-head';
matrixEl.appendChild(emptyCorner);
for (let j = 0; j < 5; j++) {
const h = document.createElement('div');
h.className = 'cell-head';
h.textContent = letters[j];
matrixEl.appendChild(h);
}
// Rows
for (let i = 0; i < 5; i++) {
const rowLabel = document.createElement('div');
rowLabel.className = 'cell-head';
rowLabel.textContent = letters[i];
matrixEl.appendChild(rowLabel);
for (let j = 0; j < 5; j++) {
const cell = document.createElement('div');
if (j > i) {
const pIdx = pairs.findIndex(p => p.i === i && p.j === j);
const s = signs[pIdx];
cell.className = `matrix-cell ${s > 0 ? 'cell-pos' : s < 0 ? 'cell-neg' : 'cell-tie'} ${pIdx === selectedPairIdx ? 'cell-active' : ''}`;
cell.textContent = s > 0 ? '+1' : s < 0 ? '−1' : '0';
cell.title = `Par ${letters[i]}–${letters[j]}: ${s > 0 ? 'Concordante (+1)' : s < 0 ? 'Discordante (−1)' : 'Empate (0)'}`;
cell.addEventListener('click', () => {
pairSlider.value = pIdx;
update();
});
} else {
cell.className = 'matrix-cell cell-empty';
}
matrixEl.appendChild(cell);
}
}
}
function update() {
const selIdx = +pairSlider.value;
pairOut.value = `Par ${selIdx + 1} de 10: ${pairs[selIdx].name}`;
const deltas = pairs.map(p => y[p.j] - y[p.i]);
const signs = deltas.map(d => Math.sign(d));
const c = signs.filter(s => s > 0).length;
const d = signs.filter(s => s < 0).length;
const ties = signs.filter(s => s === 0).length;
const den = Math.sqrt((c + d) * (c + d + ties));
const tau = den ? (c - d) / den : 0;
root.querySelector('[data-metric="c"]').textContent = c;
root.querySelector('[data-metric="d"]').textContent = d;
root.querySelector('[data-metric="ties"]').textContent = ties;
root.querySelector('[data-metric="tau"]').textContent = `${tau >= 0 ? '+' : ''}${tau.toFixed(3)}`;
const sel = pairs[selIdx];
const selDelta = y[sel.j] - y[sel.i];
const selKind = selDelta > 0 ? 'concordante (pendiente positiva, asciende)' : selDelta < 0 ? 'discordante (pendiente negativa, desciende)' : 'un empate (pendiente cero, horizontal)';
root.querySelector('[data-reading]').textContent =
`Par seleccionado ${sel.name}: Δx = ${x[sel.j]} − ${x[sel.i]} = ${x[sel.j]-x[sel.i]}, Δy = ${y[sel.j]} − ${y[sel.i]} = ${selDelta.toFixed(1)}. Es ${selKind}. Total: C = ${c}, D = ${d}, Empates = ${ties} → τ = (${c} − ${d}) / 10 = ${tau.toFixed(3)}.`;
renderMatrix(selIdx, signs);
// Render SVG
svg.replaceChildren();
// Axes
[0, 2, 4, 6, 8].forEach(v => {
add(svg, 'line', { x1: l, x2: r, y1: sy(v), y2: sy(v), stroke: '#e2e8f0', 'stroke-width': '1' });
add(svg, 'text', { x: l - 10, y: sy(v) + 4, 'text-anchor': 'end', fill: '#64748b', 'font-size': '11' }, v);
});
x.forEach(v => {
add(svg, 'line', { x1: sx(v), x2: sx(v), y1: t, y2: b, stroke: '#e2e8f0', 'stroke-width': '1' });
add(svg, 'text', { x: sx(v), y: b + 20, 'text-anchor': 'middle', fill: '#64748b', 'font-size': '11' }, v);
});
// Draw all 10 chords
pairs.forEach((p, idx) => {
const isSel = idx === selIdx;
const dyVal = deltas[idx];
const color = dyVal > 0 ? '#16835f' : dyVal < 0 ? '#c2413b' : '#94a3b8';
// Delta box for selected chord
if (isSel) {
const x1 = sx(x[p.i]), y1 = sy(y[p.i]);
const x2 = sx(x[p.j]), y2 = sy(y[p.j]);
// Bounding box
add(svg, 'rect', {
x: Math.min(x1, x2), y: Math.min(y1, y2),
width: Math.abs(x2 - x1), height: Math.max(2, Math.abs(y2 - y1)),
fill: dyVal >= 0 ? 'rgba(22, 131, 95, 0.1)' : 'rgba(194, 65, 59, 0.1)',
stroke: color, 'stroke-width': '1.5', 'stroke-dasharray': '3 3'
});
// Delta labels
add(svg, 'text', {
x: (x1 + x2) / 2, y: Math.max(y1, y2) + 14,
'text-anchor': 'middle', fill: '#475569', 'font-size': '10', 'font-weight': '600'
}, `Δx = +${x[p.j] - x[p.i]}`);
add(svg, 'text', {
x: x2 + 8, y: (y1 + y2) / 2 + 4,
fill: color, 'font-size': '10', 'font-weight': '700'
}, `Δy = ${dyVal >= 0 ? '+' : ''}${dyVal.toFixed(1)}`);
}
// Chord line
add(svg, 'line', {
x1: sx(x[p.i]), y1: sy(y[p.i]),
x2: sx(x[p.j]), y2: sy(y[p.j]),
stroke: color,
'stroke-width': isSel ? '5.5' : '2',
opacity: isSel ? '1' : '0.28',
'stroke-dasharray': dyVal === 0 ? '5 4' : 'none'
});
});
// Draw draggable points
x.forEach((v, i) => {
const isEndpoint = i === sel.i || i === sel.j;
const ptGroup = add(svg, 'g', { style: 'cursor: ns-resize', 'data-drag-pt': i });
// Invisible touch area
add(ptGroup, 'circle', { cx: sx(v), cy: sy(y[i]), r: '18', fill: 'transparent' });
add(ptGroup, 'circle', {
cx: sx(v), cy: sy(y[i]), r: isEndpoint ? '13' : '10',
fill: isEndpoint ? '#2563eb' : '#334155',
stroke: '#fff', 'stroke-width': isEndpoint ? '3' : '2',
filter: isEndpoint ? 'drop-shadow(0 0 6px rgba(37,99,235,0.6))' : 'none'
});
add(ptGroup, 'text', {
x: sx(v), y: sy(y[i]) + 4,
'text-anchor': 'middle', fill: '#fff',
'font-size': isEndpoint ? '11' : '9.5', 'font-weight': 'bold',
'pointer-events': 'none'
}, letters[i]);
});
add(svg, 'text', { x: (l + r) / 2, y: b + 42, 'text-anchor': 'middle', fill: '#475569', 'font-size': '12', 'font-weight': '600' }, 'X ordenada: un ascenso es concordante; un descenso es discordante');
}
// Pointer drag events on SVG
svg.addEventListener('pointerdown', e => {
const target = e.target.closest('[data-drag-pt]');
if (!target) return;
draggedIdx = +target.dataset.dragPt;
svg.setPointerCapture(e.pointerId);
});
svg.addEventListener('pointermove', e => {
if (draggedIdx === null) return;
const rect = svg.getBoundingClientRect();
const svgY = (e.clientY - rect.top) * (430 / rect.height);
y[draggedIdx] = invSy(svgY);
update();
});
const endDrag = e => {
if (draggedIdx !== null) {
try { svg.releasePointerCapture(e.pointerId); } catch (_) {}
draggedIdx = null;
}
};
svg.addEventListener('pointerup', endDrag);
svg.addEventListener('pointercancel', endDrag);
pairSlider.addEventListener('input', update);
root.querySelectorAll('[data-preset-k]').forEach(btn => {
btn.addEventListener('click', () => {
y = btn.dataset.presetK.split(',').map(Number);
root.querySelectorAll('[data-preset-k]').forEach(b => b.setAttribute('aria-pressed', b === btn ? 'true' : 'false'));
update();
});
});
root.querySelector('[data-preset-k="2,1,4,3,5"]').setAttribute('aria-pressed', 'true');
update();
})();
</script>
```
:::
::: {.content-visible unless-format="html"}
En HTML puede seleccionarse cada uno de los diez segmentos. Para el ejemplo
hay ocho ascendentes y dos descendentes, de modo que
$\tau_a=(8-2)/10=0.6$.
:::
## Laboratorio integrador: cuatro geometrías, una muestra
Después de estudiar cada geometría por separado, conserve
$X=(1,2,3,4,5)$ y mueva los retos en las cuatro vistas simultáneas. Observe
qué cambia continuamente —covarianza y Pearson— y qué cambia solo cuando se
altera el orden —Spearman y Kendall—.
::: {.content-visible when-format="html"}
```{=html}
<div id="correlation-order-lab" class="corr-lab" aria-labelledby="correlation-order-title">
<style>
.corr-lab{--ink:#172033;--muted:#5b6474;--blue:#2563eb;--orange:#d97706;--green:#16835f;--red:#c2413b;--purple:#7c3aed;--cyan:#0891b2;margin:1.35rem 0;color:var(--ink);font-family:inherit}
.corr-lab .lab-shell{border:1px solid #d6deea;border-radius:16px;padding:1.15rem;background:linear-gradient(135deg,#f8fbff,#fffaf3);box-shadow:0 10px 26px rgba(23,32,51,.07)}
.corr-lab .lab-title{font-size:1.15rem;font-weight:750;margin:0 0 .25rem;color:var(--ink)}
.corr-lab .lab-intro{color:var(--muted);font-size:.88rem;line-height:1.45;margin:0 0 1rem}
.corr-lab .preset-row{display:flex;flex-wrap:wrap;gap:.4rem;margin-bottom:.85rem}
.corr-lab button{border:1px solid #aab6c8;background:#fff;border-radius:999px;padding:.35rem .65rem;color:var(--ink);cursor:pointer;font-size:.8rem;transition:all .15s}
.corr-lab button:hover,.corr-lab button:focus{border-color:var(--blue);outline:2px solid #bfdbfe}
.corr-lab button[aria-pressed="true"]{color:#fff;background:var(--blue);border-color:var(--blue)}
.corr-lab .control-grid{display:grid;grid-template-columns:repeat(5,1fr);gap:.55rem;margin:.85rem 0}
.corr-lab .control-card{background:rgba(255,255,255,.8);border:1px solid #e0e6ef;border-radius:10px;padding:.5rem;text-align:center;font-size:.8rem}
.corr-lab .control-card span{display:block;font-weight:700;color:var(--ink);margin-bottom:.2rem}
.corr-lab .control-card input{width:100%;accent-color:var(--blue);cursor:pointer}
.corr-lab .control-card output{font-weight:700;font-variant-numeric:tabular-nums;color:var(--blue)}
.corr-lab .metrics{display:grid;grid-template-columns:repeat(5,1fr);gap:.5rem;margin-top:.85rem}
.corr-lab .metric{background:#fff;border:1px solid #dce3ed;border-radius:9px;padding:.5rem;text-align:center}
.corr-lab .metric span{display:block;color:var(--muted);font-size:.73rem}
.corr-lab .metric strong{font-size:1.02rem;font-variant-numeric:tabular-nums;color:var(--ink)}
.corr-lab .geometry-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:.85rem;margin-top:1rem}
.corr-lab .viz-card{background:#fff;border:1px solid #dce3ed;border-radius:13px;padding:.75rem;min-width:0;display:flex;flex-direction:column}
.corr-lab .viz-card h4{font-size:.96rem;margin:0 0 .2rem;color:var(--ink)}
.corr-lab .viz-card>p{color:var(--muted);font-size:.78rem;line-height:1.4;margin:0 0 .55rem;min-height:2.2rem}
.corr-lab .formula-strip{background:#f6f8fb;border-radius:8px;color:#334155;font-size:.78rem;line-height:1.4;margin-top:auto;padding:.45rem .6rem}
.corr-lab .legend{display:flex;flex-wrap:wrap;gap:.75rem;font-size:.74rem;color:var(--muted);margin-bottom:.35rem}
.corr-lab .swatch{display:inline-block;width:.72rem;height:.72rem;border-radius:3px;margin-right:.22rem;vertical-align:-.08rem}
.corr-lab svg{width:100%;height:auto;background:#fff;border:1px solid #e2e8f0;border-radius:10px;user-select:none}
.corr-lab .lab-reading{color:#1e293b;font-size:.86rem;line-height:1.5;margin-top:.85rem;padding:.65rem .8rem;background:rgba(255,255,255,.85);border-left:4px solid var(--blue);border-radius:0 10px 10px 0;box-shadow:0 2px 8px rgba(0,0,0,0.03)}
@media(max-width:920px){.corr-lab .control-grid{grid-template-columns:repeat(3,1fr)}.corr-lab .metrics{grid-template-columns:repeat(3,1fr)}}
@media(max-width:760px){.corr-lab .geometry-grid{grid-template-columns:1fr}.corr-lab .metrics{grid-template-columns:repeat(2,1fr)}}
</style>
<div class="lab-shell">
<h3 id="correlation-order-title" class="lab-title">Laboratorio integrador: una muestra, cuatro geometrías simultáneas</h3>
<p class="lab-intro">Modifique los valores de $Y$ o arrastre los puntos en los diagramas. Observe cómo responden en tiempo real: la <strong>covarianza</strong> suma rectángulos con signo, <strong>Pearson</strong> mide el ángulo y la proyección en $\mathbb{R}^n$, <strong>Spearman</strong> evalúa el ángulo sobre la esfera de rangos (ley de cosenos) y <strong>Kendall</strong> contabiliza las pendientes de las cuerdas.</p>
<div class="preset-row">
<button type="button" data-values="2,1,4,3,5">Ejemplo manual</button>
<button type="button" data-values="0,1,2,3,8">Mismo orden, otra escala</button>
<button type="button" data-values="1,5,4,3,2">Vectores ortogonales (r = 0)</button>
<button type="button" data-values="5,4,3,2,1">Orden inverso (r = −1)</button>
<button type="button" data-values="4,1,0,1,4">Forma de U (simetría)</button>
<button type="button" data-values="3,3,3,3,3">Y constante (degeneración)</button>
</div>
<div class="control-grid" data-controls></div>
<div class="metrics" aria-live="polite">
<div class="metric"><span>Σ áreas = x̃ · ỹ</span><strong data-metric="dot">+8.000</strong></div>
<div class="metric"><span>Covarianza s<sub>XY</sub></span><strong data-metric="cov">+2.000</strong></div>
<div class="metric"><span>Pearson r = cos θ</span><strong data-metric="pearson">+0.800</strong></div>
<div class="metric"><span>Spearman ρ<sub>s</sub> = cos φ</span><strong data-metric="spearman">+0.800</strong></div>
<div class="metric"><span>Kendall τ<sub>b</sub></span><strong data-metric="kendall">+0.600</strong></div>
<div class="metric"><span>||x̃|| / ||ỹ||</span><strong data-metric="norms">3.16 / 3.16</strong></div>
<div class="metric"><span>Ángulo θ (datos)</span><strong data-metric="angle">36.9°</strong></div>
<div class="metric"><span>Ángulo φ (rangos)</span><strong data-metric="rank-angle">36.9°</strong></div>
<div class="metric"><span>Cuerda ||d||² = Σd²</span><strong data-metric="sumd">4</strong></div>
<div class="metric"><span>C / D / Empates</span><strong data-metric="pairs">8 / 2 / 0</strong></div>
</div>
<p class="lab-reading" data-reading aria-live="polite"></p>
<div class="geometry-grid">
<section class="viz-card" aria-labelledby="covariance-view-title">
<h4 id="covariance-view-title">1. Covarianza: rectángulos con signo</h4>
<p>Verde suma; rojo resta. Cada área es $(x_i - \bar{x})(y_i - \bar{y})$ respecto del centroide $(\bar{x}, \bar{y})$.</p>
<div class="legend"><span><i class="swatch" style="background:#16835f"></i>Mismo signo (+)</span><span><i class="swatch" style="background:#c2413b"></i>Signos opuestos (−)</span></div>
<svg data-cov-plot viewBox="0 0 520 350" role="img" aria-label="Rectángulos firmados de la covarianza"></svg>
<div class="formula-strip" data-cov-formula></div>
</section>
<section class="viz-card" aria-labelledby="pearson-view-title">
<h4 id="pearson-view-title">2. Pearson: ángulo y proyección en ℝⁿ</h4>
<p>Plano vectorial generado por $\tilde{\mathbf{x}}$ e $\tilde{\mathbf{y}}$. La proyección ortogonal determina la varianza explicada $R^2 = \cos^2 \theta$.</p>
<div class="legend"><span><i class="swatch" style="background:#2563eb"></i>Vector x̃</span><span><i class="swatch" style="background:#d97706"></i>Vector ỹ</span><span><i class="swatch" style="background:#0891b2"></i>Proyección ŷ</span></div>
<svg data-vector-plot viewBox="0 0 520 350" role="img" aria-label="Vectores centrados, proyección ortogonal y ángulo de Pearson"></svg>
<div class="formula-strip" data-vector-formula></div>
</section>
<section class="viz-card" aria-labelledby="spearman-view-title">
<h4 id="spearman-view-title">3. Spearman: esfera de permutaciones</h4>
<p>Los rangos centrados viven sobre una esfera de radio $\sqrt{10}$. La cuerda $\|\mathbf{d}\|^2 = \sum d_i^2$ determina el ángulo $\varphi$ por la ley de cosenos.</p>
<div class="legend"><span><i class="swatch" style="background:#2563eb"></i>R̃_X</span><span><i class="swatch" style="background:#7c3aed"></i>R̃_Y</span><span><i class="swatch" style="background:#d97706"></i>Cuerda d</span></div>
<svg data-rank-plot viewBox="0 0 520 350" role="img" aria-label="Esfera de permutaciones, ángulo y cuerda de Spearman"></svg>
<div class="formula-strip" data-rank-formula></div>
</section>
<section class="viz-card" aria-labelledby="kendall-view-title">
<h4 id="kendall-view-title">4. Kendall: orientación de los 10 pares</h4>
<p>Con $X$ creciente, un segmento ascendente es concordante (+), uno descendente es discordante (−) y uno horizontal es empate.</p>
<div class="legend"><span><i class="swatch" style="background:#16835f"></i>Concordante (C)</span><span><i class="swatch" style="background:#c2413b"></i>Discordante (D)</span><span><i class="swatch" style="background:#94a3b8"></i>Empate</span></div>
<svg data-pair-plot viewBox="0 0 520 350" role="img" aria-label="Grafo completo de cuerdas concordantes, discordantes y empatadas"></svg>
<div class="formula-strip" data-pair-formula></div>
</section>
</div>
</div>
</div>
<script>
(() => {
const root = document.getElementById('correlation-order-lab');
if (!root) return;
const x = [1, 2, 3, 4, 5], defaults = [2, 1, 4, 3, 5], letters = ['A', 'B', 'C', 'D', 'E'];
const controls = root.querySelector('[data-controls]'), NS = 'http://www.w3.org/2000/svg';
const covSvg = root.querySelector('[data-cov-plot]');
const vectorSvg = root.querySelector('[data-vector-plot]');
const rankSvg = root.querySelector('[data-rank-plot]');
const pairSvg = root.querySelector('[data-pair-plot]');
let hoveredIdx = null;
defaults.forEach((v, i) => {
const card = document.createElement('div');
card.className = 'control-card';
card.innerHTML = `<span>${letters[i]} (x=${x[i]})</span><input type="range" min="0" max="8" step="0.5" value="${v}" data-y="${i}" aria-label="Retos del estudiante ${letters[i]}"><output>${v}</output>`;
controls.appendChild(card);
});
const inputs = [...root.querySelectorAll('[data-y]')];
const mean = a => a.reduce((s, v) => s + v, 0) / a.length;
const vectorStats = (a, b) => {
const am = mean(a), bm = mean(b);
const da = a.map(v => v - am), db = b.map(v => v - bm);
const dot = da.reduce((s, v, i) => s + v * db[i], 0);
const na = Math.hypot(...da), nb = Math.hypot(...db);
const corr = na && nb ? dot / (na * nb) : NaN;
const angle = Number.isFinite(corr) ? Math.acos(Math.max(-1, Math.min(1, corr))) : NaN;
return { da, db, dot, na, nb, corr, angle, am, bm };
};
const ranks = a => {
const idx = a.map((v, i) => ({ v, i })).sort((p, q) => p.v - q.v);
const out = Array(a.length);
let k = 0;
while (k < idx.length) {
let j = k;
while (j + 1 < idx.length && idx[j + 1].v === idx[k].v) j++;
const r = (k + j + 2) / 2;
for (let z = k; z <= j; z++) out[idx[z].i] = r;
k = j + 1;
}
return out;
};
const kendall = (a, b) => {
let c = 0, d = 0, tx = 0, ty = 0, both = 0;
for (let i = 0; i < a.length; i++) {
for (let j = i + 1; j < a.length; j++) {
const sx = Math.sign(a[j] - a[i]), sy = Math.sign(b[j] - b[i]);
if (!sx && !sy) both++;
else if (!sx) tx++;
else if (!sy) ty++;
else if (sx === sy) c++;
else d++;
}
}
const den = Math.sqrt((c + d + tx) * (c + d + ty));
return { tau: den ? (c - d) / den : NaN, c, d, tx, ty, both };
};
const add = (svg, tag, attrs, text = '') => {
const e = document.createElementNS(NS, tag);
Object.entries(attrs).forEach(([k, v]) => e.setAttribute(k, v));
if (text) e.textContent = text;
svg.appendChild(e);
return e;
};
const arrow = (svg, id, color) => {
const defs = add(svg, 'defs', {});
const marker = add(defs, 'marker', { id, viewBox: '0 0 10 10', refX: '8', refY: '5', markerWidth: '6', markerHeight: '6', orient: 'auto-start-reverse' });
add(marker, 'path', { d: 'M 0 0 L 10 5 L 0 10 z', fill: color });
};
const fmt = (v, d = 3) => Number.isFinite(v) ? (v >= 0 ? '+' : '') + v.toFixed(d) : 'no def.';
const degrees = a => Number.isFinite(a) ? `${(a * 180 / Math.PI).toFixed(1)}°` : 'no def.';
const axes = (svg, sx, sy, yTicks = [0, 2, 4, 6, 8]) => {
const left = 48, right = 495, top = 22, bottom = 300;
yTicks.forEach(t => {
add(svg, 'line', { x1: left, x2: right, y1: sy(t), y2: sy(t), stroke: '#eef2f6' });
add(svg, 'text', { x: left - 8, y: sy(t) + 4, 'text-anchor': 'end', fill: '#64748b', 'font-size': '10' }, t);
});
x.forEach(t => {
add(svg, 'line', { x1: sx(t), x2: sx(t), y1: top, y2: bottom, stroke: '#eef2f6' });
add(svg, 'text', { x: sx(t), y: bottom + 18, 'text-anchor': 'middle', fill: '#64748b', 'font-size': '10' }, t);
});
add(svg, 'line', { x1: left, x2: right, y1: bottom, y2: bottom, stroke: '#64748b', 'stroke-width': '1.2' });
add(svg, 'line', { x1: left, x2: left, y1: top, y2: bottom, stroke: '#64748b', 'stroke-width': '1.2' });
};
function drawCovariance(y, s) {
covSvg.replaceChildren();
const left = 48, right = 495, top = 22, bottom = 300;
const sx = v => left + ((v - 1) / 4) * (right - left);
const sy = v => bottom - (v / 8) * (bottom - top);
const xm = s.am, ym = s.bm;
axes(covSvg, sx, sy);
x.forEach((v, i) => {
const p = s.da[i] * s.db[i];
const isHov = hoveredIdx === i;
const rx = Math.min(sx(v), sx(xm)), ry = Math.min(sy(y[i]), sy(ym));
const rw = Math.abs(sx(v) - sx(xm)), rh = Math.abs(sy(y[i]) - sy(ym));
add(covSvg, 'rect', {
x: rx, y: ry, width: rw, height: rh,
fill: p > 0 ? '#16835f' : p < 0 ? '#c2413b' : '#94a3b8',
opacity: isHov ? '0.45' : '0.22',
stroke: p >= 0 ? '#16835f' : '#c2413b',
'stroke-width': isHov ? '2.5' : '1.2'
});
if (rw > 16 && rh > 14) {
add(covSvg, 'text', {
x: rx + rw / 2, y: ry + rh / 2 + 4,
'text-anchor': 'middle', fill: p >= 0 ? '#0f5132' : '#842029',
'font-size': '9', 'font-weight': '700'
}, (p >= 0 ? '+' : '') + p.toFixed(1));
}
});
// Centroid lines
add(covSvg, 'line', { x1: sx(xm), x2: sx(xm), y1: top, y2: bottom, stroke: '#7c3aed', 'stroke-width': '1.8', 'stroke-dasharray': '5 4' });
add(covSvg, 'line', { x1: left, x2: right, y1: sy(ym), y2: sy(ym), stroke: '#7c3aed', 'stroke-width': '1.8', 'stroke-dasharray': '5 4' });
add(covSvg, 'circle', { cx: sx(xm), cy: sy(ym), r: '4.5', fill: '#7c3aed' });
// Points
x.forEach((v, i) => {
const isHov = hoveredIdx === i;
const pt = add(covSvg, 'circle', {
cx: sx(v), cy: sy(y[i]), r: isHov ? '12' : '9',
fill: isHov ? '#d97706' : '#2563eb',
stroke: '#fff', 'stroke-width': '2',
style: 'cursor: pointer'
});
add(covSvg, 'text', {
x: sx(v), y: sy(y[i]) + 3.5,
'text-anchor': 'middle', fill: '#fff',
'font-size': '8.5', 'font-weight': 'bold',
'pointer-events': 'none'
}, letters[i]);
pt.addEventListener('mouseenter', () => { hoveredIdx = i; update(); });
pt.addEventListener('mouseleave', () => { hoveredIdx = null; update(); });
});
add(covSvg, 'text', { x: (left + right) / 2, y: 338, 'text-anchor': 'middle', fill: '#475569', 'font-size': '11' }, 'Horas X');
add(covSvg, 'text', { x: 14, y: (top + bottom) / 2, transform: `rotate(-90 14 ${(top + bottom)/2})`, 'text-anchor': 'middle', fill: '#475569', 'font-size': '11' }, 'Retos Y');
root.querySelector('[data-cov-formula]').textContent = `Σ áreas con signo = ${fmt(s.dot, 2)}; sXY = ${fmt(s.dot, 2)} / 4 = ${fmt(s.dot / 4, 3)}.`;
}
function drawVectors(svg, s) {
svg.replaceChildren();
const ox = 180, oy = 215;
const maxNorm = Math.max(s.na, s.nb, 1);
const scale = Math.min(36, 115 / maxNorm);
const theta = s.angle;
const cos = Number.isFinite(s.corr) ? s.corr : 0;
const sin = Number.isFinite(theta) ? Math.sin(theta) : 0;
arrow(svg, 'vec-x-arr', '#2563eb');
arrow(svg, 'vec-y-arr', '#d97706');
arrow(svg, 'vec-proj-arr', '#0891b2');
// Axis
add(svg, 'line', { x1: 35, x2: 485, y1: oy, y2: oy, stroke: '#cbd5e1', 'stroke-width': '1.2' });
add(svg, 'circle', { cx: ox, cy: oy, r: '3.5', fill: '#334155' });
// Vector x~
const xx = ox + scale * s.na;
add(svg, 'line', { x1: ox, y1: oy, x2: xx, y2: oy, stroke: '#2563eb', 'stroke-width': '3.5', 'marker-end': 'url(#vec-x-arr)' });
add(svg, 'text', { x: xx + 8, y: oy + 4, fill: '#2563eb', 'font-size': '11', 'font-weight': '700' }, 'x̃');
// Vector y~
if (s.nb > 1e-6) {
const yx = ox + scale * s.nb * cos;
const yy = oy - scale * s.nb * sin;
add(svg, 'line', { x1: ox, y1: oy, x2: yx, y2: yy, stroke: '#d97706', 'stroke-width': '3.5', 'marker-end': 'url(#vec-y-arr)' });
add(svg, 'text', { x: yx + (cos >= 0 ? 7 : -18), y: yy - 6, fill: '#d97706', 'font-size': '11', 'font-weight': '700' }, 'ỹ');
// Projection and residual
const px = ox + scale * s.nb * cos;
add(svg, 'line', { x1: ox, y1: oy, x2: px, y2: oy, stroke: '#0891b2', 'stroke-width': '2.5', 'marker-end': 'url(#vec-proj-arr)' });
add(svg, 'line', { x1: yx, y1: yy, x2: px, y2: oy, stroke: '#7c3aed', 'stroke-width': '1.8', 'stroke-dasharray': '4 3' });
add(svg, 'circle', { cx: px, cy: oy, r: '3', fill: '#0891b2' });
// Right angle marker
if (Math.abs(sin) > 0.1 && Math.abs(cos) > 0.05) {
const sq = 7;
const sgn = cos >= 0 ? 1 : -1;
add(svg, 'path', {
d: `M ${px - sgn * sq} ${oy} L ${px - sgn * sq} ${oy - sq} L ${px} ${oy - sq}`,
fill: 'none', stroke: '#7c3aed', 'stroke-width': '1'
});
}
}
// Angle arc
if (Number.isFinite(theta) && theta > 0.02) {
const arcR = 36;
const ax = ox + arcR * Math.cos(theta);
const ay = oy - arcR * Math.sin(theta);
const largeArc = theta > Math.PI ? 1 : 0;
add(svg, 'path', {
d: `M ${ox + arcR} ${oy} A ${arcR} ${arcR} 0 ${largeArc} 0 ${ax} ${ay}`,
fill: 'none', stroke: '#334155', 'stroke-width': '1.4'
});
add(svg, 'text', {
x: ox + (arcR + 14) * Math.cos(theta / 2),
y: oy - (arcR + 14) * Math.sin(theta / 2) + 4,
fill: '#334155', 'font-size': '11', 'font-weight': '600'
}, 'θ');
}
if (hoveredIdx !== null) {
add(svg, 'text', {
x: ox, y: 35, 'text-anchor': 'middle', fill: '#d97706', 'font-size': '10', 'font-weight': '700'
}, `Obs. ${letters[hoveredIdx]}: coord x̃ = ${fmt(s.da[hoveredIdx], 1)}, coord ỹ = ${fmt(s.db[hoveredIdx], 1)}`);
}
add(svg, 'text', { x: 25, y: 338, fill: '#475569', 'font-size': '11' }, `||x̃|| = ${s.na.toFixed(2)}, ||ỹ|| = ${s.nb.toFixed(2)} · θ = ${degrees(theta)} · R² = ${Number.isFinite(s.corr) ? (s.corr * s.corr * 100).toFixed(1) : 0}%`);
root.querySelector('[data-vector-formula]').textContent =
`x̃ · ỹ = ||x̃|| ||ỹ|| cos θ = ${fmt(s.dot, 2)}; r = cos(${degrees(theta)}) = ${fmt(s.corr, 3)}.`;
}
function drawRanks(svg, rx, ry, rs) {
svg.replaceChildren();
arrow(svg, 'rank-x-arr', '#2563eb');
arrow(svg, 'rank-y-arr', '#7c3aed');
const scx = 180, scy = 195, sphereRadius = 95;
const R0 = Math.sqrt(10);
const sphereScale = sphereRadius / R0;
const phi = rs.angle;
const normY = rs.nb;
const ties = Math.abs(normY - R0) > 0.05;
// Sphere circle
add(svg, 'circle', {
cx: scx, cy: scy, r: sphereRadius,
fill: 'rgba(37, 99, 235, 0.03)', stroke: '#94a3b8', 'stroke-width': '1.5', 'stroke-dasharray': '5 4'
});
add(svg, 'circle', { cx: scx, cy: scy, r: '3.5', fill: '#334155' });
// Vector R~_X
const rxx = scx + sphereRadius;
add(svg, 'line', { x1: scx, y1: scy, x2: rxx, y2: scy, stroke: '#2563eb', 'stroke-width': '3.5', 'marker-end': 'url(#rank-x-arr)' });
add(svg, 'text', { x: rxx + 8, y: scy + 4, fill: '#2563eb', 'font-size': '11', 'font-weight': '700' }, 'R̃_X');
// Vector R~_Y
if (normY > 1e-6) {
const ryx = scx + sphereScale * normY * Math.cos(phi || 0);
const ryy = scy - sphereScale * normY * Math.sin(phi || 0);
add(svg, 'line', { x1: scx, y1: scy, x2: ryx, y2: ryy, stroke: '#7c3aed', 'stroke-width': '3.5', 'marker-end': 'url(#rank-y-arr)' });
add(svg, 'text', { x: ryx + 7, y: ryy - 6, fill: '#7c3aed', 'font-size': '11', 'font-weight': '700' }, 'R̃_Y');
// Chord vector d
const chordLen = Math.hypot(rxx - ryx, scy - ryy);
if (chordLen > 4) {
add(svg, 'line', { x1: rxx, y1: scy, x2: ryx, y2: ryy, stroke: '#d97706', 'stroke-width': '2.2', 'stroke-dasharray': '4 3' });
}
}
// Angle phi arc
if (Number.isFinite(phi) && phi > 0.02) {
const arcR = 34;
const ax = scx + arcR * Math.cos(phi);
const ay = scy - arcR * Math.sin(phi);
const largeArc = phi > Math.PI ? 1 : 0;
add(svg, 'path', {
d: `M ${scx + arcR} ${scy} A ${arcR} ${arcR} 0 ${largeArc} 0 ${ax} ${ay}`,
fill: 'none', stroke: '#334155', 'stroke-width': '1.3'
});
add(svg, 'text', {
x: scx + (arcR + 13) * Math.cos(phi / 2),
y: scy - (arcR + 13) * Math.sin(phi / 2) + 4,
fill: '#334155', 'font-size': '11', 'font-weight': '600'
}, 'φ');
}
const sumd = rx.reduce((s, v, i) => s + (v - ry[i]) ** 2, 0);
add(svg, 'text', { x: 25, y: 338, fill: '#475569', 'font-size': '11' }, `||R̃_Y|| = ${normY.toFixed(2)} ${ties ? '(< √10, dentro)' : '(= √10)'} · φ = ${degrees(phi)} · Σd² = ${sumd.toFixed(ties ? 2 : 0)}`);
root.querySelector('[data-rank-formula]').textContent =
`R_Y = (${ry.map(v => v.toFixed(1)).join(', ')}); ρs = cos(${degrees(phi)}) = ${fmt(rs.corr, 3)}${ties ? ' (empates: radio acortado)' : ' (superficie de radio √10)'}.`;
}
function drawPairs(y, k) {
pairSvg.replaceChildren();
const left = 48, right = 495, top = 22, bottom = 300;
const sx = v => left + ((v - 1) / 4) * (right - left);
const sy = v => bottom - (v / 8) * (bottom - top);
axes(pairSvg, sx, sy);
for (let i = 0; i < x.length; i++) {
for (let j = i + 1; j < x.length; j++) {
const delta = y[j] - y[i];
const isIncident = hoveredIdx === i || hoveredIdx === j;
const color = delta > 0 ? '#16835f' : delta < 0 ? '#c2413b' : '#94a3b8';
add(pairSvg, 'line', {
x1: sx(x[i]), y1: sy(y[i]),
x2: sx(x[j]), y2: sy(y[j]),
stroke: color,
'stroke-width': isIncident ? '4.5' : '1.8',
opacity: isIncident ? '1' : (hoveredIdx !== null ? '0.15' : '0.45'),
'stroke-dasharray': delta === 0 ? '5 4' : 'none'
});
}
}
x.forEach((v, i) => {
const isHov = hoveredIdx === i;
const pt = add(pairSvg, 'circle', {
cx: sx(v), cy: sy(y[i]), r: isHov ? '12' : '9',
fill: isHov ? '#d97706' : '#1e293b',
stroke: '#fff', 'stroke-width': '2',
style: 'cursor: pointer'
});
add(pairSvg, 'text', {
x: sx(v), y: sy(y[i]) + 3.5,
'text-anchor': 'middle', fill: '#fff',
'font-size': '8.5', 'font-weight': 'bold',
'pointer-events': 'none'
}, letters[i]);
pt.addEventListener('mouseenter', () => { hoveredIdx = i; update(); });
pt.addEventListener('mouseleave', () => { hoveredIdx = null; update(); });
});
add(pairSvg, 'text', { x: (left + right) / 2, y: 338, 'text-anchor': 'middle', fill: '#475569', 'font-size': '11' }, 'X creciente: pendiente > 0 asciende (C); pendiente < 0 desciende (D)');
root.querySelector('[data-pair-formula]').textContent =
`C = ${k.c} concordantes, D = ${k.d} discordantes, Empates = ${k.ty}; τb = ${fmt(k.tau, 3)}.`;
}
function update() {
const y = inputs.map(el => +el.value);
inputs.forEach(el => el.nextElementSibling.value = el.value);
const s = vectorStats(x, y);
const rx = ranks(x), ry = ranks(y);
const rs = vectorStats(rx, ry);
const k = kendall(x, y);
const ties = k.ty + k.both;
const sumd = rx.reduce((acc, v, i) => acc + (v - ry[i]) ** 2, 0);
root.querySelector('[data-metric="dot"]').textContent = `${s.dot >= 0 ? '+' : ''}${s.dot.toFixed(2)}`;
root.querySelector('[data-metric="cov"]').textContent = `${(s.dot / 4) >= 0 ? '+' : ''}${(s.dot / 4).toFixed(3)}`;
root.querySelector('[data-metric="pearson"]').textContent = `${s.corr >= 0 ? '+' : ''}${s.corr.toFixed(3)}`;
root.querySelector('[data-metric="spearman"]').textContent = `${rs.corr >= 0 ? '+' : ''}${rs.corr.toFixed(3)}`;
root.querySelector('[data-metric="kendall"]').textContent = `${k.tau >= 0 ? '+' : ''}${k.tau.toFixed(3)}`;
root.querySelector('[data-metric="norms"]').textContent = `${s.na.toFixed(2)} / ${s.nb.toFixed(2)}`;
root.querySelector('[data-metric="angle"]').textContent = degrees(s.angle);
root.querySelector('[data-metric="rank-angle"]').textContent = degrees(rs.angle);
root.querySelector('[data-metric="sumd"]').textContent = sumd.toFixed(ties ? 2 : 0);
root.querySelector('[data-metric="pairs"]').textContent = `${k.c} / ${k.d} / ${ties}`;
root.querySelectorAll('[data-values]').forEach(btn => {
btn.setAttribute('aria-pressed', btn.dataset.values === y.join(',') ? 'true' : 'false');
});
const reading = root.querySelector('[data-reading]');
if (!s.nb) {
reading.textContent = 'Y es constante: la varianza de Y es cero. No existe una dirección vectorial que comparar en ℝ⁵, por lo que Pearson, Spearman y Kendall quedan matemáticamente indefinidos.';
} else if (ties > 0 && Math.abs(rs.corr - 1.0) < 0.001) {
reading.textContent = 'Hay empates en Y: las observaciones empatadas comparten rangos promedio. El vector de rangos se acorta y penetra al interior de la esfera de permutaciones.';
} else if (Math.abs(rs.corr - 1.0) < 0.01 && s.corr < 0.95) {
reading.textContent = `Asociación monótona no lineal: Spearman y Kendall son perfectos (ρ = 1.0, τ = 1.0) porque el orden de Y concuerda al 100% con X. Sin embargo, el estiramiento en la escala curva la nube y reduce Pearson a r = ${s.corr.toFixed(2)}.`;
} else if (Math.abs(s.corr) < 0.05 && Math.abs(rs.corr) < 0.05) {
reading.textContent = 'Ortogonalidad / Cancelación neta: el producto interno x̃ · ỹ ≈ 0 y las áreas verdes y rojas se cancelan. Los coeficientes lineales y ordinales son cercanos a cero, pero observe la nube para descartar curvatura simétrica (como una parábola).';
} else if (s.corr < -0.95) {
reading.textContent = 'Asociación negativa perfecta: los vectores son opuestos (θ = 180°), las 10 cuerdas son descendentes (D = 10, C = 0) y los rectángulos caen en los cuadrantes II y IV.';
} else {
reading.textContent = `Configuración activa: r = ${s.corr.toFixed(2)} (ángulo θ = ${degrees(s.angle)}), ρ = ${rs.corr.toFixed(2)} (cuerda de rangos ||d||² = ${sumd.toFixed(1)}) y τ = ${k.tau.toFixed(2)} (${k.c} pares concordantes vs ${k.d} discordantes). Pase el cursor sobre cualquier punto para rastrearlo en las cuatro vistas.`;
}
drawCovariance(y, s);
drawVectors(vectorSvg, s);
drawRanks(rankSvg, rx, ry, rs);
drawPairs(y, k);
}
inputs.forEach(el => el.addEventListener('input', update));
root.querySelectorAll('[data-values]').forEach(btn => {
btn.addEventListener('click', () => {
btn.dataset.values.split(',').forEach((v, i) => inputs[i].value = v);
update();
});
});
update();
})();
</script>
```
:::
::: {.content-visible unless-format="html"}
La versión HTML contiene un laboratorio que modifica los cinco valores de
$Y$. Además de los coeficientes, muestra los rectángulos de covarianza, el
ángulo de Pearson, el ángulo entre rangos de Spearman y los segmentos de
Kendall. Algunos escenarios de referencia son:
| Escenario | $Y$ | Pearson | Spearman | Kendall | Geometría principal |
|:---|:---|---:|---:|---:|:---|
| Ejemplo manual | $(2,1,4,3,5)$ | 0.8 | 0.8 | 0.6 | Dos inversiones locales |
| Mismo orden, otra escala | $(0,1,2,3,8)$ | 0.91 | 1 | 1 | Rangos paralelos; distancias no |
| Vectores ortogonales | $(1,5,4,3,2)$ | 0 | 0 | -0.2 | Pearson y Spearman a $90^\circ$ |
| Perfecta decreciente | $(5,4,3,2,1)$ | -1 | -1 | -1 | Vectores opuestos |
| Forma de U | $(4,1,0,1,4)$ | 0 | 0 | 0 | Estructura no monótona con empates |
Los casos con valor cero recuerdan que ortogonalidad o equilibrio ordinal no
equivalen a independencia.
:::
## Lineal, monótona y no monótona
Los siguientes diagramas Plotly mantienen datos fijos para que la comparación
sea reproducible. Pase el cursor sobre los puntos para consultar valores y
coeficientes.
```{python}
import numpy as np
import pandas as pd
import plotly.express as px
from IPython.display import HTML, display
from scipy import stats
def mostrar_plotly(figura):
"""Incrusta Plotly sin cargar una segunda versión de MathJax."""
html = figura.to_html(
full_html=False,
include_plotlyjs="cdn",
include_mathjax=False,
)
display(HTML(html))
rng = np.random.default_rng(9)
n = 600
x = rng.uniform(-2, 2, n)
casos = {
"Lineal": 2 * x + rng.normal(0, 0.7, n),
"Monótona curva": np.exp(x) + rng.normal(0, 0.35, n),
"Parabólica": x**2 + rng.normal(0, 0.25, n),
}
filas = []
for caso, y in casos.items():
r = stats.pearsonr(x, y).statistic
rho = stats.spearmanr(x, y).statistic
for xi, yi in zip(x, y):
filas.append({
"X": xi,
"Y": yi,
"Relación": caso,
"Pearson": r,
"Spearman": rho,
})
df_relaciones = pd.DataFrame(filas)
fig = px.scatter(
df_relaciones,
x="X",
y="Y",
facet_col="Relación",
color="Relación",
opacity=0.55,
hover_data={"Pearson": ":.3f", "Spearman": ":.3f"},
title="La geometría determina qué correlación resulta informativa",
)
fig.update_layout(showlegend=False)
mostrar_plotly(fig)
```
La relación lineal produce valores altos en Pearson y Spearman. En la curva
creciente, Spearman conserva mejor la asociación porque usa orden. En la
parábola, los descensos de la mitad izquierda compensan los ascensos de la
mitad derecha: una correlación cercana a cero no significa ausencia de
estructura.
::: {.callout-warning appearance="minimal"}
**Cero no significa independencia.** Si $X$ toma valores simétricos alrededor
de cero y $Y=X^2$, entonces $Y$ queda completamente determinada por $X$, pero
$\operatorname{Cov}(X,Y)=0$ porque los productos impares se cancelan. La
independencia[^independencia] implica correlación cero cuando existen los
momentos necesarios; el recíproco es falso.
:::
## Sensibilidad a una observación extrema
### Paso 1: Intuición con un ejemplo
¿Qué sucede si a un grupo de observaciones con clara tendencia positiva agregamos
un solo estudiante atípico? Imaginemos que alguien afirma haber practicado 20
horas pero resolvió 0 retos (o viceversa).
En el cálculo de Pearson, esa observación se ubica sumamente lejos del centroide
$(\bar{x}, \bar{y})$ tanto en el eje horizontal como en el vertical. Su producto
cruzado $(x_i - \bar{x})(y_i - \bar{y})$ resulta en un número negativo enorme que
puede sobrepasar la suma de todos los demás estudiantes juntos, arrastrando la
recta de regresión y volteando el signo de la correlación de positivo a negativo.
¿Qué ocurre con ese mismo estudiante en Spearman y Kendall?
Al asignarle rangos, sus 20 horas simplemente ocupan la última posición
($R_X = n$). Si en lugar de 20 horas hubiera reportado 200 o 2000 horas, ¡su
rango sigue siendo exactamente $n$! No puede aumentar más.
Decimos que los métodos basados en rangos experimentan una **saturación de
frontera**: una vez que una observación ocupa el extremo de la cuadrícula
$\{1, \ldots, n\}^2$, alejarla a distancias métricas arbitrariamente grandes no
modifica su posición ni altera los rangos de las demás observaciones.
### Paso 2: Análisis analítico: apalancamiento continuo vs frontera de rangos
En una regresión lineal simple, el **apalancamiento** (*leverage*)
$h_{ii}$[^apalancamiento] de la $i$-ésima observación se calcula como:
$$
h_{ii} = \frac{1}{n} + \frac{(x_i - \bar{x})^2}{\sum_{k=1}^n (x_k - \bar{x})^2}. \tag{12}
$$
El apalancamiento mide qué tan aislada se encuentra una observación en el eje
horizontal respecto a la media de los datos:
- Si $x_i \approx \bar{x}$, el apalancamiento es mínimo ($h_{ii} \approx 1/n$).
- A medida que $(x_i - \bar{x})^2$ crece, $h_{ii}$ se aproxima a 1.
- Una observación con $h_{ii}$ alto actúa como un **brazo de palanca**: la recta
de mínimos cuadrados se ve obligada a pivotar sobre el centroide para pasar
cerca de ese punto.
En contraste, en el espacio discreto de rangos $\{1, \ldots, n\}^2$, la distancia
máxima posible entre dos observaciones en cualquier eje está rígidamente acotada:
$|R_{X,i} - R_{X,j}| \le n - 1$. No existe la posibilidad de que una sola
observación posea una distancia desproporcionada respecto a las demás.
### Paso 3: Formalización matemática: punto de ruptura
El concepto teórico que cuantifica esta vulnerabilidad es el **punto de
ruptura** (*breakdown point* [@huber2009robust]):[^ruptura]
- **Pearson tiene un punto de ruptura asintótico de $0\%$:** Basta una sola
observación atípica ($1/n \to 0$ cuando $n$ crece) colocada a una distancia
suficientemente extrema para forzar al coeficiente $r$ a tomar prácticamente
cualquier valor deseado en el intervalo $[-1, 1]$.
- **Spearman y Kendall son estimadores resistentes:** Al basarse en la
ordenación de los datos, su función de influencia frente a la magnitud del
alejamiento permanece acotada. Un punto extremo solo puede alterar los órdenes
hasta alcanzar la frontera del conjunto $\{1, \dots, n\}^2$.
El siguiente experimento numérico ilustra esta diferencia: parte de una nube
positiva de 100 observaciones, introduce un punto extremo $(a, -a)$ y aumenta
gradualmente su magnitud $a$:
```{python}
x_base = rng.normal(size=100)
y_base = 0.7 * x_base + rng.normal(scale=0.6, size=100)
filas = []
for extremo in np.linspace(0, 20, 21):
xx = np.r_[x_base, extremo]
yy = np.r_[y_base, -extremo]
filas.extend([
{
"Magnitud del extremo": extremo,
"Coeficiente": "Pearson",
"Valor": stats.pearsonr(xx, yy).statistic,
},
{
"Magnitud del extremo": extremo,
"Coeficiente": "Spearman",
"Valor": stats.spearmanr(xx, yy).statistic,
},
{
"Magnitud del extremo": extremo,
"Coeficiente": "Kendall",
"Valor": stats.kendalltau(xx, yy).statistic,
},
])
sensibilidad = pd.DataFrame(filas)
fig = px.line(
sensibilidad,
x="Magnitud del extremo",
y="Valor",
color="Coeficiente",
markers=True,
range_y=[-1, 1],
title="Un solo punto puede invertir la correlación de Pearson",
)
fig.add_hline(y=0, line_dash="dot")
mostrar_plotly(fig)
```
La gráfica no demuestra que Spearman o Kendall sean inmunes a extremos: un punto
atípico sí puede alterar pares y posiciones relativas. Muestra una distinción
geométrica fundamental: Pearson reacciona de forma continua y no acotada a la
**magnitud métrica** del alejamiento; los métodos ordinales se detienen al llegar
al **borde de la ordenación**.
### Paso 4: De la intuición geométrica al laboratorio
El laboratorio interactivo siguiente permite experimentar directamente con este
mecanismo. La muestra base (puntos verdes) mantiene una asociación positiva.
Mueva o arrastre libremente el octavo punto (naranja):
- **A la izquierda (espacio de datos continuo):** Observe cómo la distancia al
centroide incrementa el apalancamiento $h_{ii}$, obligando a la recta de
regresión a pivotar e invirtiendo el signo de Pearson $r$.
- **A la derecha (espacio discreto de rangos $\{1,\dots,8\}^2$):** Observe cómo,
al alejar el punto extremo, este **se congela en el borde de la cuadrícula**:
su rango queda fijo en $(8, 1)$ y los coeficientes de Spearman $\rho_s$ y
Kendall $\tau_b$ permanecen estables a pesar de que el punto continúe
alejándose en el plano continuo.
## Laboratorio de influencia: mover un punto extremo
::: {.content-visible when-format="html"}
```{=html}
<div id="correlation-outlier-lab" class="corr-lab" aria-labelledby="correlation-outlier-title">
<style>
#correlation-outlier-lab .dual-svg-grid{display:grid;grid-template-columns:1fr 1fr;gap:.85rem}
@media(max-width:880px){#correlation-outlier-lab .dual-svg-grid{grid-template-columns:1fr}}
</style>
<div class="lab-shell">
<h3 id="correlation-outlier-title" class="lab-title">Laboratorio de influencia: apalancamiento continuo vs saturación ordinal</h3>
<p class="lab-intro">Siete observaciones (verdes) forman una tendencia lineal positiva. Mueva el octavo punto (naranja) o arrástrelo en la gráfica. A la izquierda (datos continuos), la distancia al centroide otorga un brazo de palanca que gira la recta de regresión e invierte Pearson. A la derecha (espacio de rangos $\{1,\dots,8\}^2$), al alejar el punto extremo este se <strong>congela en el borde</strong>: su rango no puede superar 8 ni bajar de 1, demostrando la resistencia geométrica de Spearman y Kendall.</p>
<div class="preset-row">
<button type="button" data-point="0,0">En el centro (sin palanca)</button>
<button type="button" data-point="8,8">Extremo alineado (refuerza r)</button>
<button type="button" data-point="8,-8">Extremo contrario (invierte r)</button>
<button type="button" data-point="0,8">Extremo vertical (gran residuo)</button>
<button type="button" data-point="10,-2">Apalancamiento extremo X</button>
</div>
<div class="lab-layout">
<div>
<div class="range-row">
<label for="outlier-x">Coordenada X del punto móvil</label>
<input id="outlier-x" data-ox type="range" min="-10" max="10" step="0.5" value="8">
<output data-ox-out>+8.0</output>
</div>
<div class="range-row">
<label for="outlier-y">Coordenada Y del punto móvil</label>
<input id="outlier-y" data-oy type="range" min="-10" max="10" step="0.5" value="-8">
<output data-oy-out>−8.0</output>
</div>
<div class="metrics" aria-live="polite">
<div class="metric"><span>Pearson r</span><strong data-out="pearson">−0.654</strong></div>
<div class="metric"><span>Spearman ρ<sub>s</sub></span><strong data-out="spearman">+0.429</strong></div>
<div class="metric"><span>Kendall τ<sub>b</sub></span><strong data-out="kendall">+0.286</strong></div>
<div class="metric"><span>Pendiente recta</span><strong data-out="slope">−0.352</strong></div>
<div class="metric"><span>Apalancamiento h<sub>ii</sub></span><strong data-out="leverage">0.582</strong></div>
<div class="metric"><span>Rango del punto (R<sub>X</sub>, R<sub>Y</sub>)</span><strong data-out="ranks">(8, 1) [Límite]</strong></div>
</div>
<p class="lab-reading" data-out-reading aria-live="polite"></p>
</div>
<div>
<div class="dual-svg-grid">
<div>
<div class="legend"><span><i class="swatch" style="background:#16835f"></i>Base (7 pts)</span><span><i class="swatch" style="background:#d97706"></i>Atípico móvil</span><span><i class="swatch" style="background:#2563eb"></i>Recta MCO</span></div>
<p style="font-size:.76rem;font-weight:700;color:var(--muted);margin:0 0 .25rem;text-align:center">1. Espacio de datos (efecto palanca)</p>
<svg data-out-plot viewBox="0 0 340 310" role="img" aria-label="Espacio continuo con punto móvil y recta de regresión"></svg>
</div>
<div>
<div class="legend"><span><i class="swatch" style="background:#7c3aed"></i>Rangos base</span><span><i class="swatch" style="background:#d97706"></i>Rango atípico</span></div>
<p style="font-size:.76rem;font-weight:700;color:var(--muted);margin:0 0 .25rem;text-align:center">2. Espacio de rangos {1..8}² (saturación)</p>
<svg data-rank-plot-out viewBox="0 0 340 310" role="img" aria-label="Espacio de rangos con saturación en el borde"></svg>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
(() => {
const root = document.getElementById('correlation-outlier-lab');
if (!root) return;
const bx = [-3, -2, -1, 0, 1, 2, 3], by = [-2.6, -2.1, -0.7, 0.2, 0.9, 2.2, 2.7];
const ix = root.querySelector('[data-ox]'), iy = root.querySelector('[data-oy]');
const svgData = root.querySelector('[data-out-plot]');
const svgRank = root.querySelector('[data-rank-plot-out]');
const NS = 'http://www.w3.org/2000/svg';
const mean = a => a.reduce((s, v) => s + v, 0) / a.length;
const pearson = (a, b) => {
const am = mean(a), bm = mean(b);
const da = a.map(v => v - am), db = b.map(v => v - bm);
const num = da.reduce((s, v, i) => s + v * db[i], 0);
const den = Math.sqrt(da.reduce((s, v) => s + v * v, 0) * db.reduce((s, v) => s + v * v, 0));
return den ? num / den : NaN;
};
const ranks = a => {
const z = a.map((v, i) => ({ v, i })).sort((p, q) => p.v - q.v);
const r = Array(a.length);
let k = 0;
while (k < z.length) {
let j = k;
while (j + 1 < z.length && z[j + 1].v === z[k].v) j++;
const q = (k + j + 2) / 2;
for (let h = k; h <= j; h++) r[z[h].i] = q;
k = j + 1;
}
return r;
};
const kendall = (a, b) => {
let c = 0, d = 0, tx = 0, ty = 0;
for (let i = 0; i < a.length; i++) {
for (let j = i + 1; j < a.length; j++) {
const sx = Math.sign(a[j] - a[i]), sy = Math.sign(b[j] - b[i]);
if (!sx && sy) tx++;
else if (sx && !sy) ty++;
else if (sx && sy) {
if (sx === sy) c++;
else d++;
}
}
}
const den = Math.sqrt((c + d + tx) * (c + d + ty));
return den ? (c - d) / den : NaN;
};
const add = (parent, tag, attrs, text = '') => {
const e = document.createElementNS(NS, tag);
Object.entries(attrs).forEach(([k, v]) => e.setAttribute(k, v));
if (text) e.textContent = text;
parent.appendChild(e);
return e;
};
const fmt = (v, d = 3) => Number.isFinite(v) ? (v >= 0 ? '+' : '') + v.toFixed(d) : 'no def.';
// Coordinate scales for data space [-10, 10]
const dl = 38, dr = 320, dt = 20, db = 280;
const dsx = v => dl + ((v + 10) / 20) * (dr - dl);
const dsy = v => db - ((v + 10) / 20) * (db - dt);
// Coordinate scales for rank space {1..8}
const rl = 38, rr = 320, rt = 20, rb = 280;
const rsx = v => rl + ((v - 1) / 7) * (rr - rl);
const rsy = v => rb - ((v - 1) / 7) * (rb - rt);
let isDragging = false;
function update() {
const ox = +ix.value, oy = +iy.value;
const x = [...bx, ox], y = [...by, oy];
const xm = mean(x), ym = mean(y);
const ssx = x.reduce((s, v) => s + (v - xm) ** 2, 0);
const sxy = x.reduce((s, v, i) => s + (v - xm) * (y[i] - ym), 0);
const slope = ssx ? sxy / ssx : NaN;
const intercept = ym - slope * xm;
const r = pearson(x, y);
const rxArr = ranks(x), ryArr = ranks(y);
const rho = pearson(rxArr, ryArr);
const tau = kendall(x, y);
// Leverage of outlier: h_ii = 1/n + (x_i - x_mean)^2 / ssx
const leverage = ssx ? (1 / x.length) + ((ox - xm) ** 2) / ssx : 0;
const outRx = rxArr[7], outRy = ryArr[7];
const isSaturated = (outRx === 1 || outRx === 8) && (outRy === 1 || outRy === 8);
root.querySelector('[data-ox-out]').value = `${ox >= 0 ? '+' : ''}${ox.toFixed(1)}`;
root.querySelector('[data-oy-out]').value = `${oy >= 0 ? '+' : ''}${oy.toFixed(1)}`;
root.querySelector('[data-out="pearson"]').textContent = fmt(r);
root.querySelector('[data-out="spearman"]').textContent = fmt(rho);
root.querySelector('[data-out="kendall"]').textContent = fmt(tau);
root.querySelector('[data-out="slope"]').textContent = fmt(slope);
root.querySelector('[data-out="leverage"]').textContent = leverage.toFixed(3);
root.querySelector('[data-out="ranks"]').textContent = `(${outRx}, ${outRy})${isSaturated ? ' [Saturado en borde]' : ''}`;
const reading = root.querySelector('[data-out-reading]');
const baseR = pearson(bx, by);
if (Math.sign(r) !== Math.sign(baseR) && Math.abs(r) > 0.1) {
reading.textContent = `¡Inversión de signo! En los 7 puntos base la tendencia es positiva (+0.99). El punto extremo en (${ox}, ${oy}) posee un apalancamiento de ${leverage.toFixed(2)} que arrastra la recta e invierte Pearson a ${fmt(r)}, mientras que Spearman (${fmt(rho)}) y Kendall (${fmt(tau)}) conservan la resistencia al quedar confinados a rangos {1..8}.`;
} else if (isSaturated) {
reading.textContent = `Saturación alcanzada: en el espacio de rangos, el punto atípico se encuentra en la esquina (${outRx}, ${outRy}). Si aleja el punto aún más en los datos (ej. de x=8 a x=10 o más), su posición en la cuadrícula de rangos no cambia; por eso Spearman y Kendall permanecen invariantes.`;
} else {
reading.textContent = `El punto móvil se encuentra en (${ox}, ${oy}). Observe cómo la recta de regresión pivota alrededor del centroide (x̄=${xm.toFixed(1)}, ȳ=${ym.toFixed(1)}). A mayor distancia horizontal |x − x̄|, mayor apalancamiento ejerce sobre Pearson.`;
}
// --- Panel 1: Data Space ---
svgData.replaceChildren();
// Grid
[-10, -5, 0, 5, 10].forEach(v => {
add(svgData, 'line', { x1: dl, x2: dr, y1: dsy(v), y2: dsy(v), stroke: v === 0 ? '#94a3b8' : '#eef2f6', 'stroke-width': v === 0 ? '1.4' : '1' });
add(svgData, 'line', { x1: dsx(v), x2: dsx(v), y1: dt, y2: db, stroke: v === 0 ? '#94a3b8' : '#eef2f6', 'stroke-width': v === 0 ? '1.4' : '1' });
if (v !== 0) {
add(svgData, 'text', { x: dsx(v), y: db + 16, 'text-anchor': 'middle', fill: '#64748b', 'font-size': '9' }, v);
add(svgData, 'text', { x: dl - 6, y: dsy(v) + 3, 'text-anchor': 'end', fill: '#64748b', 'font-size': '9' }, v);
}
});
// Centroid
add(svgData, 'circle', { cx: dsx(xm), cy: dsy(ym), r: '4', fill: '#7c3aed' });
// OLS Regression line
if (Number.isFinite(slope)) {
const lx1 = -10, ly1 = intercept + slope * lx1;
const lx2 = 10, ly2 = intercept + slope * lx2;
add(svgData, 'line', {
x1: dsx(lx1), y1: dsy(ly1), x2: dsx(lx2), y2: dsy(ly2),
stroke: '#2563eb', 'stroke-width': '2.5', 'stroke-dasharray': '6 4'
});
}
// Base points (green)
bx.forEach((v, i) => {
add(svgData, 'circle', { cx: dsx(v), cy: dsy(by[i]), r: '6', fill: '#16835f', stroke: '#fff', 'stroke-width': '1.5' });
});
// Outlier point (orange, draggable)
const outGroup = add(svgData, 'g', { style: 'cursor: move', 'data-drag-outlier': 'true' });
add(outGroup, 'circle', { cx: dsx(ox), cy: dsy(oy), r: '18', fill: 'transparent' });
add(outGroup, 'circle', {
cx: dsx(ox), cy: dsy(oy), r: '10',
fill: '#d97706', stroke: '#fff', 'stroke-width': '2.5',
filter: 'drop-shadow(0 0 6px rgba(217,119,6,0.6))'
});
add(outGroup, 'text', {
x: dsx(ox), y: dsy(oy) + 3.5,
'text-anchor': 'middle', fill: '#fff', 'font-size': '9', 'font-weight': 'bold',
'pointer-events': 'none'
}, '★');
// --- Panel 2: Rank Space ---
svgRank.replaceChildren();
// Grid 1 to 8
for (let v = 1; v <= 8; v++) {
add(svgRank, 'line', { x1: rl, x2: rr, y1: rsy(v), y2: rsy(v), stroke: '#eef2f6', 'stroke-width': '1' });
add(svgRank, 'line', { x1: rsx(v), x2: rsx(v), y1: rt, y2: rb, stroke: '#eef2f6', 'stroke-width': '1' });
add(svgRank, 'text', { x: rsx(v), y: rb + 16, 'text-anchor': 'middle', fill: '#64748b', 'font-size': '9' }, v);
add(svgRank, 'text', { x: rl - 6, y: rsy(v) + 3, 'text-anchor': 'end', fill: '#64748b', 'font-size': '9' }, v);
}
// Outer boundary of rank box
add(svgRank, 'rect', {
x: rl, y: rt, width: rr - rl, height: rb - rt,
fill: 'none', stroke: isSaturated ? '#d97706' : '#cbd5e1',
'stroke-width': isSaturated ? '2.5' : '1.5',
'stroke-dasharray': isSaturated ? 'none' : '4 3'
});
// Base rank points (purple)
for (let i = 0; i < 7; i++) {
add(svgRank, 'circle', {
cx: rsx(rxArr[i]), cy: rsy(ryArr[i]), r: '6',
fill: '#7c3aed', stroke: '#fff', 'stroke-width': '1.5'
});
}
// Outlier rank point (orange)
add(svgRank, 'circle', {
cx: rsx(outRx), cy: rsy(outRy), r: '10',
fill: '#d97706', stroke: '#fff', 'stroke-width': '2.5',
filter: 'drop-shadow(0 0 6px rgba(217,119,6,0.6))'
});
add(svgRank, 'text', {
x: rsx(outRx), y: rsy(outRy) + 3.5,
'text-anchor': 'middle', fill: '#fff', 'font-size': '9', 'font-weight': 'bold'
}, '★');
if (isSaturated) {
add(svgRank, 'text', {
x: (rl + rr) / 2, y: rt + 18,
'text-anchor': 'middle', fill: '#d97706', 'font-size': '10', 'font-weight': '700'
}, '¡Saturado en el borde {1, 8}!');
}
}
// Pointer drag on data space SVG
svgData.addEventListener('pointerdown', e => {
const target = e.target.closest('[data-drag-outlier]');
if (!target) return;
isDragging = true;
svgData.setPointerCapture(e.pointerId);
});
svgData.addEventListener('pointermove', e => {
if (!isDragging) return;
const rect = svgData.getBoundingClientRect();
const svgX = (e.clientX - rect.left) * (340 / rect.width);
const svgY = (e.clientY - rect.top) * (310 / rect.height);
const newX = Math.max(-10, Math.min(10, Math.round(((svgX - dl) / (dr - dl) * 20 - 10) * 2) / 2));
const newY = Math.max(-10, Math.min(10, Math.round(((db - svgY) / (db - dt) * 20 - 10) * 2) / 2));
ix.value = newX;
iy.value = newY;
update();
});
const stopDrag = e => {
if (isDragging) {
try { svgData.releasePointerCapture(e.pointerId); } catch (_) {}
isDragging = false;
}
};
svgData.addEventListener('pointerup', stopDrag);
svgData.addEventListener('pointercancel', stopDrag);
[ix, iy].forEach(el => el.addEventListener('input', update));
root.querySelectorAll('[data-point]').forEach(btn => {
btn.addEventListener('click', () => {
const [a, b] = btn.dataset.point.split(',');
ix.value = a;
iy.value = b;
update();
});
});
update();
})();
</script>
```
:::
::: {.content-visible unless-format="html"}
La versión HTML permite mover un octavo punto alrededor de siete observaciones
con tendencia positiva. Un extremo alineado refuerza Pearson; uno colocado en
la dirección contraria puede invertirlo. Spearman y Kendall cambian cuando el
nuevo punto modifica órdenes y concordancias, pero no por seguir aumentando su
distancia después de ocupar un rango extremo.
:::
## El cuarteto de Anscombe: mismo coeficiente, distinta historia
Los cuatro conjuntos siguientes tienen once pares y comparten, salvo redondeo,
media de $X$, media de $Y$, varianzas, recta ajustada y correlación de Pearson.
Sus diagramas cuentan historias distintas [@anscombe1973graphs].
```{python}
anscombe_ancho = pd.DataFrame({
"x1": [10, 8, 13, 9, 11, 14, 6, 4, 12, 7, 5],
"x2": [10, 8, 13, 9, 11, 14, 6, 4, 12, 7, 5],
"x3": [10, 8, 13, 9, 11, 14, 6, 4, 12, 7, 5],
"x4": [8, 8, 8, 8, 8, 8, 8, 19, 8, 8, 8],
"y1": [8.04, 6.95, 7.58, 8.81, 8.33, 9.96, 7.24, 4.26, 10.84, 4.82, 5.68],
"y2": [9.14, 8.14, 8.74, 8.77, 9.26, 8.10, 6.13, 3.10, 9.13, 7.26, 4.74],
"y3": [7.46, 6.77, 12.74, 7.11, 7.81, 8.84, 6.08, 5.39, 8.15, 6.42, 5.73],
"y4": [6.58, 5.76, 7.71, 8.84, 8.47, 7.04, 5.25, 12.50, 5.56, 7.91, 6.89],
})
anscombe = pd.concat([
pd.DataFrame({
"x": anscombe_ancho[f"x{i}"],
"y": anscombe_ancho[f"y{i}"],
"dataset": f"Serie {i}",
})
for i in range(1, 5)
], ignore_index=True)
fig = px.scatter(
anscombe,
x="x",
y="y",
facet_col="dataset",
color="dataset",
trendline="ols",
title="Mismos resúmenes, patrones visuales distintos",
)
fig.update_layout(showlegend=False)
mostrar_plotly(fig)
```
- La serie 1 se aproxima a una relación lineal con ruido.
- La serie 2 presenta curvatura: una recta omite estructura sistemática.
- La serie 3 está dominada por una observación verticalmente alejada.
- La serie 4 concentra casi todos los valores de $X$ y depende de un punto con
gran apalancamiento.
El coeficiente es un resumen del conjunto completo; no certifica que la recta
sea un modelo adecuado para cada observación.
## Correlación no implica causalidad
Aunque el diagrama sea claro y el coeficiente grande, una asociación
observacional no identifica por sí sola qué ocurriría al intervenir sobre
$X$.[^causalidad] Entre las explicaciones posibles están:
- **causalidad directa:** cambiar $X$ modifica $Y$;
- **causalidad inversa:** $Y$ influye sobre $X$;
- **confusión:** una tercera variable afecta a ambas;[^confusor]
- **selección:** la forma de incluir casos crea o modifica la asociación;
- **tendencia común:** dos series crecen con el tiempo sin un mecanismo directo;
- **azar y multiplicidad:** al examinar muchos pares, algunos parecen asociados
solo por fluctuación muestral.
En el ejemplo de práctica, motivación previa, experiencia, dificultad de los
retos y forma de seleccionar estudiantes podrían explicar parte de la
asociación. Para sostener una afirmación causal se necesita un diseño o
supuestos causales adicionales, no un umbral de correlación.
## Matrices de correlación
Con $p$ variables, una matriz de correlación contiene el coeficiente de cada
par:
$$
R=(r_{jk})_{p\times p},
\qquad r_{jj}=1,
\qquad r_{jk}=r_{kj}. \tag{10}
$$
Es una vista compacta, pero no reemplaza los diagramas relevantes. Puede
ocultar curvatura, subgrupos, escalas temporales y tamaños muestrales distintos.
Los valores faltantes exigen declarar una estrategia:[^faltantes]
- con **casos completos**, todas las celdas usan las mismas filas, pero pueden
descartarse muchas observaciones;
- con **eliminación por pares**, cada celda aprovecha las filas disponibles
para ese par, pero los tamaños muestrales cambian y la matriz conjunta puede
perder propiedades algebraicas esperadas.
::: {.callout-warning appearance="minimal"}
Antes de interpretar un mapa de calor, reporte el número de observaciones por
celda, revise variables constantes o casi constantes y abra diagramas de
dispersión para los pares sustantivamente importantes. Un bloque de colores no
permite diagnosticar valores extremos ni mezclas de grupos.
:::
## Formalización poblacional
Sean $X$ y $Y$ variables aleatorias con medias $\mu_X,\mu_Y$ y varianzas
finitas positivas. La covarianza poblacional es
$$
\operatorname{Cov}(X,Y)
=\mathbb E[(X-\mu_X)(Y-\mu_Y)]
=\mathbb E[XY]-\mu_X\mu_Y. \tag{11}
$$
La correlación poblacional de Pearson es
$$
\rho_{XY}=\frac{\operatorname{Cov}(X,Y)}{\sigma_X\sigma_Y}. \tag{12}
$$
Si $U=a+bX$ y $V=c+dY$, con $b,d\ne0$, entonces
$$
\operatorname{corr}(U,V)
=\operatorname{sign}(bd)\operatorname{corr}(X,Y).
$$
Esta invariancia hace comparable el coeficiente entre unidades, pero también
explica una limitación: Pearson resume la parte lineal estandarizada y no la
pendiente física. Para cuantificar cuánto cambia $Y$ por una unidad de $X$ se
necesita una recta de regresión y sus supuestos, no solo $r$.
La correlación muestral es un **estimador**[^estimador] de la correlación
poblacional. Cinco pares pueden producir $r=0.8$ sin que la población tenga
exactamente ese valor. La precisión depende de $n$, del diseño y de la forma de
la distribución; reportar solo la estimación puntual oculta esa incertidumbre.
## Implementación reproducible
La función siguiente devuelve los tres coeficientes, sus valores $p$ y el
número de pares utilizados. El valor $p$ responde a una hipótesis nula bajo
supuestos específicos; no mide el tamaño ni la importancia práctica de la
asociación.[^valor-p]
```{python}
def resumen_correlacion(x, y):
datos = pd.DataFrame({"x": x, "y": y}).dropna()
if len(datos) < 3:
raise ValueError("Se requieren al menos tres pares completos.")
if datos["x"].nunique() < 2 or datos["y"].nunique() < 2:
raise ValueError("Cada variable debe presentar variación.")
pearson = stats.pearsonr(datos["x"], datos["y"])
spearman = stats.spearmanr(datos["x"], datos["y"])
kendall = stats.kendalltau(datos["x"], datos["y"], variant="b")
return pd.Series({
"n_pares": len(datos),
"Pearson": pearson.statistic,
"p_Pearson": pearson.pvalue,
"Spearman": spearman.statistic,
"p_Spearman": spearman.pvalue,
"Kendall_tau_b": kendall.statistic,
"p_Kendall": kendall.pvalue,
})
horas = [1, 2, 3, 4, 5]
retos = [2, 1, 4, 3, 5]
resumen_correlacion(horas, retos).round(4)
```
Para una matriz, conviene acompañar cada coeficiente con el número efectivo de
pares:
```{python}
def correlaciones_y_tamanos(datos, metodo="pearson"):
numericas = datos.select_dtypes(include="number")
correlaciones = numericas.corr(method=metodo)
disponibles = numericas.notna().astype(int)
tamanos = disponibles.T @ disponibles
return correlaciones, tamanos
ejemplo_faltantes = pd.DataFrame({
"horas": [1, 2, 3, 4, 5, np.nan],
"retos": [2, 1, 4, 3, 5, 6],
"pausas": [3, np.nan, 2, 2, 1, 1],
})
matriz_r, matriz_n = correlaciones_y_tamanos(ejemplo_faltantes)
matriz_r.round(3), matriz_n
```
::: {.callout-important appearance="minimal"}
### Del cálculo al programa
Pearson acumula desviaciones, productos y cuadrados. Spearman transforma
primero ambas columnas en rangos promedio y reutiliza Pearson. Una
implementación directa de Kendall compara $\binom n2$ pares, por lo que su
costo es cuadrático; bibliotecas estadísticas emplean algoritmos más eficientes.
En todos los casos debe preservarse el emparejamiento original de las filas.
:::
## Guía de decisión
| Pregunta | Medida principal | Gráfica mínima | Precaución |
|:---|:---|:---|:---|
| ¿La nube se organiza alrededor de una recta? | Pearson | Dispersión con tendencia | Sensible a extremos; cero no descarta curvatura. |
| ¿Una variable crece o decrece consistentemente con la otra? | Spearman | Dispersión y rangos | Los empates requieren rangos promedio. |
| ¿Qué tan frecuentemente concuerdan los órdenes de pares? | Kendall tau-b | Dispersión o tabla ordinal | Su escala no coincide numéricamente con Pearson. |
| ¿Existe una relación no monótona? | Ninguna de las tres basta | Dispersión y suavizado | Considerar modelos no lineales. |
| ¿Se comparan muchas variables? | Matriz más gráficos seleccionados | Mapa de calor y dispersiones | Reportar $n$, faltantes y multiplicidad. |
| ¿Se busca una explicación causal? | La correlación es solo exploratoria | Diagrama causal y análisis del diseño | Requiere supuestos o intervención adicionales. |
## Errores frecuentes
- **Romper los pares al ordenar columnas por separado.** La unidad de análisis
es $(x_i,y_i)$; cambiar parejas inventa otra asociación.
- **Interpretar $r=0$ como “no hay relación”.** Solo descarta una asociación
lineal neta en la muestra.
- **Usar Spearman como detector universal de no linealidad.** Detecta orden
monótono; una U también puede producir $\rho_s\approx0$.
- **Eliminar un extremo porque cambia el signo.** Primero se verifica si es
error, caso de otra población o evento genuino.
- **Comparar coeficientes con tamaños muestrales distintos sin reportarlos.**
La estabilidad de 0.5 con 12 pares no es la misma que con 12,000.
- **Convertir significancia en relevancia.** Con muestras grandes, una
correlación minúscula puede producir un valor $p$ pequeño.
- **Inferir causalidad por magnitud.** Una correlación de 0.95 sigue siendo
compatible con confusión o selección.
## Puentes con minería de datos
| Contexto | Uso de la correlación | Límite que debe recordarse |
|:---|:---|:---|
| Selección exploratoria de atributos | Detectar redundancia lineal o monótona. | Dos predictores correlacionados pueden aportar información condicional distinta. |
| Diagnóstico de multicolinealidad | Señalar pares linealmente relacionados. | La multicolinealidad puede involucrar combinaciones de más de dos variables. |
| Detección de fuga de información | Una asociación casi perfecta con el objetivo merece revisión. | La fuga depende del momento y proceso de disponibilidad, no solo de $r$. |
| Series temporales | Explorar co-movimiento y rezagos. | Tendencias y autocorrelación pueden crear asociaciones espurias. |
| Agrupamiento de variables | Construir distancias como $1-r$. | El signo, los faltantes y la validez métrica requieren decisiones explícitas. |
| Monitoreo | Comparar cambios de asociación entre periodos o segmentos. | Una correlación agregada puede ocultar patrones opuestos por grupo. |
## Puente al caso BigMart
El capítulo [Medidas de correlación en Big Mart Sales](2-9-correlacion_bigmart.qmd)
traslada esta secuencia a variables comerciales y tipos de outlet:
1. conservar las filas emparejadas e inspeccionar diagramas de dispersión;
2. distinguir relaciones lineales, monótonas y no monótonas;
3. comparar Pearson y Spearman según la pregunta;
4. construir matrices documentando faltantes y tamaños efectivos;
5. revisar asociaciones por grupos antes de interpretar un coeficiente global.
El código aplicado deja así de ser una colección de llamadas a `.corr()`: cada
celda se conecta con una geometría, una unidad de análisis y una limitación.
## Ejercicios formativos
1. **Cálculo completo.** Para
$X=(1,2,3,4,5)$ y $Y=(5,3,4,1,2)$, construya la tabla de desviaciones,
calcule covarianza muestral y Pearson, asigne rangos y cuente los diez pares
para Spearman y Kendall.
2. **Cambio de unidades.** Convierta las horas del ejemplo a minutos y sume 10
a todos los retos. Prediga qué ocurre con covarianza, Pearson, Spearman y
Kendall antes de calcular.
3. **Relación sin correlación.** Verifique que
$X=(-2,-1,0,1,2)$ y $Y=X^2$ producen Pearson y Spearman cercanos a cero.
Dibuje los puntos y explique por qué no son independientes.
4. **Empates.** Para $X=(1,2,2,4,5)$ y $Y=(1,1,3,4,4)$, obtenga rangos
promedio y calcule Spearman como Pearson entre rangos. Identifique los pares
empatados necesarios para tau-b.
5. **Punto influyente.** Parta de una nube positiva de diez puntos y agregue
uno en la dirección contraria. Compare los tres coeficientes y justifique si
conservaría, corregiría o segmentaría esa observación.
6. **Anscombe.** Calcule para cada serie media, varianza, pendiente y Pearson.
Después describa qué decisión equivocada surgiría si solo se leyera la tabla.
7. **Faltantes.** Construya una tabla de cuatro variables donde la eliminación
por pares use distintos tamaños muestrales. Reporte las matrices de
correlación y de $n$ mediante `correlaciones_y_tamanos`.
8. **Causalidad.** Proponga tres explicaciones para una correlación positiva
entre uso de una plataforma educativa y calificación: una causal, una de
causalidad inversa y una por confusión. Indique qué datos ayudarían a
distinguirlas.
## Referencias {.unnumbered}
::: {#refs}
:::
[^estandarizada]: **Estandarizar** una medida significa expresarla sin las unidades originales mediante una escala común. Esto facilita comparaciones, pero no elimina valores extremos ni vuelve lineal una relación.
[^dispersion]: Un **diagrama de dispersión** representa cada unidad como un punto $(x_i,y_i)$. La posición horizontal corresponde a una variable y la vertical a la otra; por eso preserva emparejamiento, forma y observaciones inusuales.
[^bessel]: La **corrección de Bessel** consiste en usar el divisor $n-1$ (en lugar de $n$) al estimar la varianza o covarianza a partir de una muestra. Como las desviaciones se calculan respecto a la media muestral $\bar{x}$ (que minimiza la suma de cuadrados en esa muestra específica) y no respecto a la verdadera media poblacional desconocida $\mu$, dividir entre $n$ subestima sistemáticamente la dispersión. El factor $\frac{n}{n-1}$ compensa ese grado de libertad perdido y produce un estimador insesgado [@freedman2007statistics].
[^zscore]: La **puntuación $z$** (o estandarización) transforma una variable restando su media y dividiendo por su desviación estándar: $z = (x - \bar{x})/s$. Expresa cada observación en unidades de "cuántas desviaciones estándar dista de la media", teniendo siempre media 0 y desviación estándar 1, lo que permite comparar variables con escalas o unidades físicas completamente dispares.
[^cauchy]: La **desigualdad de Cauchy--Schwarz** establece que el valor absoluto del producto interno de dos vectores no supera el producto de sus longitudes: $|\mathbf{u} \cdot \mathbf{v}| \le \lVert\mathbf{u}\rVert \lVert\mathbf{v}\rVert$. Aplicada a los vectores de desviaciones centradas en $\mathbb{R}^n$, garantiza que el coeficiente de Pearson esté rigurosamente acotado en $[-1, 1]$.
[^r2]: El **coeficiente de determinación** $R^2 = r^2$ representa la proporción de la varianza total de una variable que queda explicada linealmente por la otra. Geométricamente, corresponde a la razón entre la norma al cuadrado de la proyección ortogonal y la norma al cuadrado del vector original en $\mathbb{R}^n$: $\lVert\widehat{\mathbf{y}}\rVert^2 / \lVert\widetilde{\mathbf{y}}\rVert^2$.
[^rango]: El **rango** de una observación es su posición ordinal al clasificar los valores de menor a mayor ($1^\circ, 2^\circ, \ldots, n^\circ$). Con empates se asigna a todos los valores empatados el promedio de las posiciones que habrían ocupado.
[^monotona]: Una relación es **monótona creciente** si $Y$ nunca disminuye cuando $X$ aumenta; es monótona decreciente si nunca aumenta. Puede ser curva y seguir conservando el orden de manera perfecta.
[^permutoedro]: El **permutoedro** de orden $n$ es un polítopo convexo $(n-1)$-dimensional en $\mathbb{R}^n$ cuyos vértices son todas las $n!$ permutaciones posibles del vector $(1, 2, \ldots, n)$. Todos sus vértices descansan sobre una misma hiperesfera de radio $\sqrt{n(n^2-1)/12}$ centrada en el vector diagonal $\mathbf{1}$.
[^ordinal]: Una variable **ordinal** posee categorías con orden definido, pero sin distancias numéricas necesariamente comparables ni uniformes (como bajo, medio y alto). La concordancia evalúa el orden sin exigir que los intervalos sean iguales.
[^inversion]: Una **inversión** en una secuencia es un par de elementos que aparecen en orden inverso al orden natural. La distancia de inversión (o distancia de Kendall tau) mide el número mínimo de intercambios de elementos adyacentes necesarios para transformar una ordenación en otra.
[^kendallb]: **Kendall tau-b** ajusta el cálculo de concordancia cuando existen empates en los datos. Modifica el denominador calculando la media geométrica del número de pares no empatados en $X$ y en $Y$, garantizando que el coeficiente permanezca en el intervalo $[-1, 1]$ aun en presencia de valores idénticos repetidos [@kendall1938measure].
[^independencia]: Dos variables son **independientes** si conocer una no altera en absoluto la distribución probabilística de la otra: $P(X \le x, Y \le y) = P(X \le x)P(Y \le y)$. Es una propiedad sobre toda la distribución conjunta, mucho más fuerte que tener covarianza o correlación cero.
[^apalancamiento]: El **apalancamiento** (*leverage*) $h_{ii} = \frac{1}{n} + \frac{(x_i - \bar{x})^2}{\sum (x_k - \bar{x})^2}$ describe qué tan inusual es una observación en el eje horizontal. Un punto muy alejado horizontalmente ejerce un gran efecto de palanca sobre la pendiente de la recta de regresión ajustada.
[^ruptura]: El **punto de ruptura** (*breakdown point*) es la proporción mínima de observaciones contaminadas o arbitrarias que pueden alterar indefinidamente el valor de un estimador [@huber2009robust]. Pearson tiene un punto de ruptura asintótico de $0\%$ (basta un único valor extremo para distorsionar la correlación), mientras que los estadísticos de rangos gozan de mayor estabilidad frente a perturbaciones métricas no acotadas.
[^causalidad]: Una afirmación **causal** describe el efecto de intervenir: qué cambiaría en $Y$ si se modificara activamente $X$ manteniendo comparables las demás condiciones relevantes.
[^confusor]: Un **confusor** es una variable externa relacionada tanto con la variable predictora como con el resultado que puede crear, ocultar o invertir la asociación observada si no se controla adecuadamente.
[^faltantes]: Un valor **faltante** indica que una medición no está disponible. El mecanismo que produjo la ausencia importa: eliminar filas completas puede sesgar la asociación si la probabilidad de faltar depende de variables relevantes del estudio.
[^estimador]: Un **estimador** es una regla matemática calculada a partir de los datos de una muestra para aproximar un parámetro poblacional desconocido. Su valor numérico varía de una muestra a otra y, por tanto, está sujeto a error de muestreo e incertidumbre.
[^valor-p]: Un **valor $p$** es la probabilidad, bajo el supuesto de que una hipótesis nula sea cierta y se cumplan las condiciones del modelo, de obtener un resultado muestral al menos tan extremo como el observado. No mide la magnitud del efecto ni la probabilidad de que la hipótesis nula sea verdadera.