---
title: "Reducción de Variables Categóricas y Prueba χ²"
pagetitle: "Reducción de Variables Categóricas y Prueba χ²"
open-graph:
description: "Aplica la prueba χ² de independencia a una tabla de contingencia entre tipo y tamaño de tienda, en un ejemplo didáctico inspirado en BigMart Sales."
twitter-card:
description: "Aplica la prueba χ² de independencia a una tabla de contingencia entre tipo y tamaño de tienda, en un ejemplo didáctico inspirado en BigMart Sales."
description: "Aplica la prueba χ² de independencia a una tabla de contingencia entre tipo y tamaño de tienda, en un ejemplo didáctico inspirado en BigMart Sales."
format:
html:
toc: true
toc-depth: 3
toc-title: "Contenido"
code-fold: true
code-tools: true
theme: cosmo
highlight-style: github
execute:
echo: true
warning: false
message: false
jupyter: python3
---
{{< include _labkit.qmd >}}
En la etapa de preprocesamiento y selección de características (*feature selection*), a menudo nos enfrentamos a conjuntos de datos con múltiples variables categóricas. Herramientas comunes como el coeficiente de correlación de Pearson o Spearman están diseñadas para variables numéricas (continuas u ordinales) y no son aplicables a variables categóricas nominales, donde no existe un orden inherente (por ejemplo, tipo de cliente, canal de compra o nivel educativo frente a abandono del servicio).
La redundancia entre variables categóricas tiene un costo concreto: si dos variables transmiten casi la misma información, o si un predictor no guarda relación con la variable objetivo (*target*), conservarlas infla la dimensionalidad del modelo. El efecto se multiplica con *One-Hot Encoding*, donde cada categoría añade una columna sin aportar necesariamente señal nueva.
Para ilustrarlo, utilizaremos un **ejemplo didáctico inspirado en BigMart Sales**: el tipo de tienda (`Outlet_Type`: Supermarket Type1, Grocery Store) y su tamaño (`Outlet_Size`: Medium, Small). ¿Están asociadas estas dos variables en la muestra imaginaria, o son independientes? Los conteos siguientes son inventados para aprender el método; no describen el conjunto de datos original.
## Intuición: ¿qué esperaríamos si no hubiera relación?
Antes de cualquier fórmula, hagamos una predicción con proporciones. De 100 tiendas, 50 son *Medium*: la mitad. Si el tamaño no tuviera nada que ver con el tipo, **la mitad de los 40 supermercados también debería ser *Medium***, es decir, unos 20. Observamos 30. Con las tiendas de abarrotes ocurre lo contrario: esperaríamos 30 medianas de 60 y observamos 20.
La prueba χ² formaliza exactamente esta comparación: construye la tabla que esperaríamos bajo independencia, mide cuánto se aleja la tabla observada **en cada celda** y pondera cada diferencia por el tamaño esperado (diez tiendas de diferencia pesan más si esperábamos 20 que si esperábamos 2000). Después pregunta si una distancia total así de grande sería rara si las variables fueran independientes.
## Desarrollo manual paso a paso
Organizamos los 100 registros en una **tabla de contingencia**[^contingencia] de $2 \times 2$:
| Outlet_Type | Medium | Small | Total fila ($R_i$) |
| :--- | :---: | :---: | :---: |
| **Supermarket Type1** | 30 | 10 | 40 |
| **Grocery Store** | 20 | 40 | 60 |
| **Total columna ($C_j$)** | 50 | 50 | 100 ($N$) |
### Frecuencias marginales
Los totales de fila ($R_i$) son 40 y 60; los de columna ($C_j$), 50 y 50; el total general ($N$) es 100. Divididos entre $N$ son las **proporciones marginales**[^marginal], es decir, 40 % de supermercados y 50 % de tiendas medianas.
### Frecuencias esperadas ($E_{ij}$)
Bajo la hipótesis nula[^h0] de independencia, la probabilidad de una celda es el producto de las probabilidades marginales, así que el conteo esperado es $N\cdot\frac{R_i}{N}\cdot\frac{C_j}{N}$:
$$ E_{ij} = \frac{R_i \cdot C_j}{N} $$
- $E_{11}$ (Supermarket Type1, Medium): $\frac{40 \cdot 50}{100} = 20$
- $E_{12}$ (Supermarket Type1, Small): $\frac{40 \cdot 50}{100} = 20$
- $E_{21}$ (Grocery Store, Medium): $\frac{60 \cdot 50}{100} = 30$
- $E_{22}$ (Grocery Store, Small): $\frac{60 \cdot 50}{100} = 30$
La tabla esperada conserva los mismos totales de fila y columna que la observada; solo redistribuye los conteos como si no hubiera asociación.
### Contribuciones a χ²
Cada celda aporta su diferencia al cuadrado, relativa a lo esperado:
$$ \frac{(O_{ij} - E_{ij})^2}{E_{ij}} $$
- Celda 1,1: $\frac{(30 - 20)^2}{20} = \frac{100}{20} = 5.0$
- Celda 1,2: $\frac{(10 - 20)^2}{20} = \frac{100}{20} = 5.0$
- Celda 2,1: $\frac{(20 - 30)^2}{30} = \frac{100}{30} \approx 3.333$
- Celda 2,2: $\frac{(40 - 30)^2}{30} = \frac{100}{30} \approx 3.333$
Las cuatro diferencias valen $\pm10$: en una tabla $2\times2$ con márgenes fijos, si una celda sube diez, su vecina de fila y su vecina de columna deben bajar diez. Por eso solo hay un grado de libertad.
### Estadístico χ²$_{calc}$ y valor $p$
$$ \chi^2_{\text{calc}} = 5.0 + 5.0 + 3.333 + 3.333 = 16.667 $$
Los grados de libertad[^gl] son $(r - 1)(c - 1) = (2 - 1)(2 - 1) = 1$. Si $H_0$ fuera cierta, $\chi^2_{\text{calc}}$ seguiría aproximadamente una distribución χ² con un grado de libertad[^dist-chi2]. El **valor $p$**[^pvalor] es la probabilidad de obtener un estadístico al menos tan grande como el observado bajo esa distribución:
$$ p = P\left(\chi^2_1 \ge 16.667\right) \approx 4.5\times10^{-5}. $$
Con un nivel de significancia[^alfa] $\alpha = 0.05$, el valor crítico es $3.841$. Como $16.667 > 3.841$ (equivalentemente, $p<0.05$), rechazamos la independencia en esta muestra ficticia. Esto aporta evidencia de asociación; no demuestra causalidad ni garantiza utilidad predictiva.
### Fuerza de asociación: V de Cramér
El valor $p$ cuantifica qué tan inusual sería una discrepancia como la observada bajo independencia[^3]; no mide la fuerza de la asociación. Para ello dividimos χ² entre su máximo posible, $N\cdot\min(r-1,c-1)$:
$$ V = \sqrt{\frac{\chi^2}{N \cdot \min(r - 1, c - 1)}} = \sqrt{\frac{16.667}{100}} \approx 0.408 $$
En una tabla $2\times2$ existe un atajo útil para comprobar el cálculo: $V=|\phi|=\frac{|O_{11}O_{22}-O_{12}O_{21}|}{\sqrt{R_1R_2C_1C_2}}=\frac{|30\cdot40-10\cdot20|}{\sqrt{40\cdot60\cdot50\cdot50}}=\frac{1000}{2449.5}\approx0.408$.
**Un experimento mental.** Si multiplicamos todos los conteos por 10 (misma proporción en cada celda, $N=1000$), cada contribución se multiplica por 10: $\chi^2=166.7$ y $p\approx4\times10^{-38}$. Pero $V$ sigue en $0.408$, porque depende solo de las proporciones. Una muestra más grande vuelve más **segura** la conclusión, no más **fuerte** la asociación.
### Ver la discrepancia antes de interpretar el resultado
Comparamos la tabla observada con la esperada en dos mapas de calor interactivos. Ambas usan el mismo rango numérico de color para facilitar la comparación entre celdas.
```{python}
import numpy as np
import plotly.graph_objects as go
from plotly.subplots import make_subplots
observadas = np.array([[30, 10], [20, 40]])
esperadas = np.array([[20., 20.], [30., 30.]])
filas = ["Supermarket Type1", "Grocery Store"]
columnas = ["Medium", "Small"]
fig = make_subplots(rows=1, cols=2, subplot_titles=("Observadas", "Esperadas si fueran independientes"))
for posicion, (tabla, escala) in enumerate(((observadas, "Blues"), (esperadas, "Oranges")), start=1):
fig.add_trace(go.Heatmap(z=tabla, x=columnas, y=filas, zmin=0, zmax=40,
colorscale=escala, showscale=False, text=tabla,
texttemplate="%{text}"), row=1, col=posicion)
fig.update_layout(title="Tipo y tamaño de tienda: datos didácticos", template="plotly_white", height=370)
fig.show()
```
La celda *Supermarket Type1–Medium* contiene 30 registros frente a 20 esperados. El contraste ocurre también en las otras tres celdas; ninguna por sí sola explica toda la decisión.
```{python}
residuos = (observadas - esperadas) / np.sqrt(esperadas)
fig = go.Figure(go.Heatmap(z=residuos, x=columnas, y=filas,
colorscale="RdBu_r", zmid=0, zmin=-2.5, zmax=2.5,
text=np.round(residuos, 2), texttemplate="%{text}",
colorbar=dict(title="Residuo")))
fig.update_layout(title="Residuos de Pearson: (observado − esperado) / √esperado",
template="plotly_white", height=370)
fig.show()
```
El signo muestra la dirección de la diferencia. El cuadrado de cada residuo es la contribución de esa celda al χ². No interpretes cada residuo como una prueba independiente sin ajustar por las comparaciones múltiples.
## Formalización teórica y matemática
La base formal de esta prueba es la definición de **independencia estadística**[^indep] para variables aleatorias categóricas $X$ e $Y$:
$$ H_0:\; P(X = x_i, Y = y_j) = P(X = x_i) \cdot P(Y = y_j) \quad \forall i, j $$
Las probabilidades marginales se estiman con las proporciones observadas, $\hat p_{i\cdot}=R_i/N$ y $\hat p_{\cdot j}=C_j/N$; son los estimadores de máxima verosimilitud[^mle] bajo $H_0$. De ahí $E_{ij}=N\hat p_{i\cdot}\hat p_{\cdot j}=R_iC_j/N$.
El **estadístico χ² de Pearson** suma las discrepancias estandarizadas:
$$ \chi^2 = \sum_{i=1}^r \sum_{j=1}^c \frac{(O_{ij} - E_{ij})^2}{E_{ij}} = \sum_{i,j} r_{ij}^2, \qquad r_{ij}=\frac{O_{ij}-E_{ij}}{\sqrt{E_{ij}}}. $$
Cada conteo es aproximadamente Poisson, con varianza cercana a su media $E_{ij}$; por eso $r_{ij}$ se comporta, para conteos grandes, como una variable normal estándar, y la suma de sus cuadrados como una χ². Las restricciones de los márgenes eliminan $r+c-1$ grados de libertad de las $rc$ celdas, lo que deja $rc-(r+c-1)=(r-1)(c-1)$.
Dos propiedades explican el experimento mental anterior. Si todos los conteos se multiplican por $k>0$:
$$ \chi^2(kO)=k\,\chi^2(O), \qquad V(kO)=V(O). $$
### Supuestos de validez
Las observaciones deben ser independientes: no se debe contar repetidamente la misma tienda como si fueran tiendas distintas. Como regla orientativa para la aproximación asintótica, revisa los **criterios de Cochran**[^5]: ninguna celda esperada menor que 1 y, como máximo, el 20 % de las celdas esperadas menor que 5. En una tabla 2 × 2 esto suele implicar revisar con especial cuidado cualquier frecuencia esperada menor que 5.
### Corrección por continuidad de Yates
En tablas de $2 \times 2$ puede utilizarse la **corrección de Yates**[^1]: resta $0.5$ a cada $|O_{ij}-E_{ij}|$ antes de elevar al cuadrado. En nuestro ejemplo, $\chi^2_{\text{Yates}}=9.5^2(2/20+2/30)=15.04$. No es obligatoria por el solo hecho de tener una tabla 2 × 2: informa si se aplicó y usa el mismo criterio al comparar resultados.
### Prueba exacta de Fisher
Cuando la aproximación χ² es dudosa en una tabla 2 × 2, la **prueba exacta de Fisher**[^2] calcula el valor $p$ sin aproximaciones: fija los márgenes y enumera todas las tablas posibles con su probabilidad hipergeométrica. Para nuestra tabla da $p\approx8.3\times10^{-5}$, del mismo orden que la aproximación. También debemos ser cautos con conclusiones basadas en datos agregados debido a la **paradoja de Simpson**[^4].
## Laboratorio interactivo: de la tabla observada a la distribución nula
**Objetivo.** Ver la independencia como una comparación de proporciones, distinguir significación estadística de fuerza de asociación y entender de dónde sale el valor $p$.
1. Con el ejemplo inicial, comprueba en el mosaico que las barras no se cortan en la línea punteada (la proporción de *Medium* que esperaríamos bajo independencia). Verifica $\chi^2\approx16.67$, $V\approx0.408$ y $p\approx4.5\times10^{-5}$.
2. Pulsa «Simular bajo H₀». Cada barra del histograma resume tablas generadas **sin** asociación con el mismo $N$. ¿Qué fracción supera el χ² observado? Compárala con el valor $p$ teórico.
3. Carga «Asociación débil» (`25, 15, 25, 35`). Predice χ² y V antes de mirar. Luego mueve «Multiplicar la muestra» hasta ×10: anota cómo cambian χ², $p$ y V.
4. Carga «Celdas escasas» (`2, 0, 1, 3`). Explica por qué el aviso de Cochran aparece y compara el valor $p$ de χ² con el de Fisher.
::: {.content-visible when-format="html"}
```{=html}
<div class="plab" id="chi2-lab">
<p class="plab-eyebrow">Laboratorio · Prueba de independencia</p>
<h3 class="plab-title">Outlet_Type × Outlet_Size: ¿cuánto se aleja lo observado de lo esperado?</h3>
<p class="plab-sub">Escribe conteos enteros no negativos o usa un caso predefinido. Todo se recalcula en el navegador.</p>
<div class="plab-controls">
<div class="plab-field" style="flex:2 1 260px">Casos predefinidos
<div class="plab-seg" id="chi2-preset">
<button type="button" data-value="30,10,20,40" aria-pressed="true">Ejemplo inicial</button>
<button type="button" data-value="25,15,25,35">Asociación débil</button>
<button type="button" data-value="2,0,1,3">Celdas escasas</button>
</div>
</div>
<label>Multiplicar la muestra: <b id="chi2-k-v">× 1</b><input type="range" id="chi2-k" min="1" max="20" step="1" value="1"></label>
</div>
<div class="plab-grid">
<div class="plab-panel">
<h4>Conteos observados O<sub>ij</sub> (antes de multiplicar)</h4>
<div class="chi2-grid" role="group" aria-label="Tabla de contingencia editable">
<span></span><b>Medium</b><b>Small</b><b>Total</b>
<span>Supermarket Type1</span><input type="number" min="0" step="1" id="chi2-o11" aria-label="Supermarket Type1, Medium"><input type="number" min="0" step="1" id="chi2-o12" aria-label="Supermarket Type1, Small"><output id="chi2-r1"></output>
<span>Grocery Store</span><input type="number" min="0" step="1" id="chi2-o21" aria-label="Grocery Store, Medium"><input type="number" min="0" step="1" id="chi2-o22" aria-label="Grocery Store, Small"><output id="chi2-r2"></output>
<span>Total</span><output id="chi2-c1"></output><output id="chi2-c2"></output><output id="chi2-n"></output>
</div>
<style>
#chi2-lab .chi2-grid { display: grid; grid-template-columns: minmax(84px, 1.5fr) repeat(3, minmax(44px, 1fr)); gap: .35rem .5rem; align-items: center; font-size: .85rem; }
#chi2-lab .chi2-grid b { color: var(--lab-muted); font-weight: 600; text-align: center; }
#chi2-lab .chi2-grid output { text-align: center; font-variant-numeric: tabular-nums; font-weight: 600; }
#chi2-lab .chi2-grid input { width: 100%; }
</style>
<h4 style="margin-top:.9rem">Celda por celda (con la muestra multiplicada)</h4>
<div class="plab-scroll" id="chi2-cells"></div>
</div>
<div class="plab-panel">
<h4>Mosaico: proporción de Medium en cada tipo</h4>
<svg id="chi2-mosaic" role="img" aria-label="Diagrama de mosaico de la tabla"></svg>
<div class="plab-legend"><span><i style="background:var(--lab-s1)"></i>Medium</span><span><i style="background:var(--lab-s2)"></i>Small</span><span><i style="background:var(--lab-text)"></i>corte esperado bajo H₀</span></div>
</div>
</div>
<div class="plab-stats">
<div class="plab-stat"><span>χ² (gl = 1)</span><strong id="chi2-stat"></strong></div>
<div class="plab-stat"><span>Valor p (aprox. χ²)</span><strong id="chi2-p"></strong></div>
<div class="plab-stat"><span>Valor p (Fisher exacto)</span><strong id="chi2-fisher"></strong></div>
<div class="plab-stat"><span>V de Cramér</span><strong id="chi2-v"></strong></div>
</div>
<p class="plab-note" id="chi2-note" aria-live="polite"></p>
<div class="plab-panel" style="margin-top:1rem">
<div class="plab-controls" style="margin-bottom:.4rem">
<h4 style="margin:0;flex:1 1 220px">¿Qué χ² aparecen cuando no hay asociación?</h4>
<button type="button" class="plab-btn primary" id="chi2-sim">Simular bajo H₀ (2 000 tablas)</button>
</div>
<svg id="chi2-null" class="wide" role="img" aria-label="Histograma de χ² simulados bajo independencia"></svg>
<div class="plab-legend"><span><i style="background:var(--lab-s4)"></i>χ² de tablas simuladas</span><span><i style="background:var(--lab-s3)"></i>densidad χ² con 1 gl</span><span><i style="background:var(--lab-s5)"></i>χ² observado</span></div>
<p class="plab-note" id="chi2-sim-note" aria-live="polite">Pulsa el botón para generar tablas con los mismos márgenes esperados y ninguna asociación.</p>
</div>
</div>
<script>
(() => {
const K = window.LabKit, root = document.getElementById('chi2-lab');
const $ = s => root.querySelector(s);
const ids = ['o11', 'o12', 'o21', 'o22'];
const rand = K.rng(99);
let sims = null;
// erfc de Numerical Recipes (error relativo < 1.2e-7): P(χ²₁ ≥ x) = erfc(√(x/2)).
const erfc = x => { const z = Math.abs(x), t = 1 / (1 + 0.5 * z);
const r = t * Math.exp(-z * z - 1.26551223 + t * (1.00002368 + t * (0.37409196 + t * (0.09678418 + t * (-0.18628806 + t * (0.27886807 + t * (-1.13520398 + t * (1.48851587 + t * (-0.82215223 + t * 0.17087277)))))))));
return x >= 0 ? r : 2 - r; };
const pChi1 = x => erfc(Math.sqrt(x / 2));
const fmtP = p => !Number.isFinite(p) ? '—' : p >= 1e-4 ? K.fmt(p, 4) : p.toExponential(1).replace(/e-(\d+)/, (_, e) => ' × 10⁻' + [...e].map(d => '⁰¹²³⁴⁵⁶⁷⁸⁹'[d]).join(''));
const logFact = (() => { const c = [0]; return n => { for (let i = c.length; i <= n; i++) c[i] = c[i - 1] + Math.log(i); return c[n]; }; })();
function fisher(o) {
const [a, b, c, d] = o, r1 = a + b, r2 = c + d, c1 = a + c, n = r1 + r2;
const lp = x => logFact(r1) - logFact(x) - logFact(r1 - x) + logFact(r2) - logFact(c1 - x) - logFact(r2 - c1 + x) - (logFact(n) - logFact(c1) - logFact(n - c1));
const obs = lp(a); let p = 0;
for (let x = Math.max(0, c1 - r2); x <= Math.min(r1, c1); x++) { const l = lp(x); if (l <= obs + 1e-7) p += Math.exp(l); }
return Math.min(1, p);
}
function stats(o) {
const [a, b, c, d] = o, r = [a + b, c + d], col = [a + c, b + d], n = r[0] + r[1];
if (!n || r.includes(0) || col.includes(0)) return null;
const E = [r[0] * col[0] / n, r[0] * col[1] / n, r[1] * col[0] / n, r[1] * col[1] / n];
const contrib = o.map((v, i) => (v - E[i]) ** 2 / E[i]);
const chi2 = K.sum(contrib);
return { r, col, n, E, contrib, chi2, V: Math.sqrt(chi2 / n) };
}
const read = () => ids.map(id => { const f = $('#chi2-' + id), v = Number(f.value), ok = Number.isInteger(v) && v >= 0; f.setCustomValidity(ok ? '' : 'Entero no negativo'); return ok ? v : NaN; });
function drawMosaic(o, s) {
const c = K.chart($('#chi2-mosaic'), { width: 420, height: 280, x: [0, 1], y: [0, 1], margin: { left: 118, right: 16, top: 10, bottom: 36 } });
c.axes({ xTicks: 5, yTicks: 0, grid: false, xLabel: 'proporción dentro de cada tipo de tienda', xFmt: v => K.fmt(100 * v, 0) + ' %' });
if (!s) return;
const expected = s.col[0] / s.n, names = ['Supermarket Type1', 'Grocery Store'];
let y = 1;
[0, 1].forEach(i => {
const h = 0.94 * s.r[i] / s.n, y0 = y - h, pm = o[2 * i] / s.r[i];
c.rect(0, y0 + .01, pm, y - .01, 's1').style.fillOpacity = .8;
c.rect(pm, y0 + .01, 1, y - .01, 's2').style.fillOpacity = .8;
c.text(0, (y + y0) / 2, names[i], { 'text-anchor': 'end', dx: -8, dy: -2, class: 'strong' });
c.text(0, (y + y0) / 2, `n = ${s.r[i]}`, { 'text-anchor': 'end', dx: -8, dy: 12 });
if (h > .08) c.text(pm / 2, (y + y0) / 2, K.fmt(100 * pm, 0) + ' %', { 'text-anchor': 'middle', dy: 4, style: 'fill:#fff;font-weight:700' });
y = y0 - .06;
});
const line = c.line(expected, 0, expected, 1, 'line dash'); line.style.stroke = 'var(--lab-text)';
c.text(expected, 1, `esperado: ${K.fmt(100 * expected, 0)} %`, { 'text-anchor': 'middle', dy: -1, class: 'strong' });
}
function drawNull(obs) {
const top = Math.max(10, Math.min(40, (obs || 0) * 1.15));
const bins = 40, w = top / bins;
let dens = [];
if (sims) { const cnt = new Array(bins).fill(0); sims.forEach(v => { if (v < top) cnt[Math.floor(v / w)]++; }); dens = cnt.map(n => n / (sims.length * w)); }
const ymax = 1.0;
const c = K.chart($('#chi2-null'), { width: 640, height: 250, x: [0, top], y: [0, ymax], margin: { left: 44, right: 16, top: 12, bottom: 38 } });
c.axes({ xTicks: 8, yTicks: 4, xLabel: 'valor del estadístico χ²', yLabel: 'densidad', yFmt: v => K.fmt(v, 2) });
dens.forEach((d, i) => c.rect(i * w, 0, (i + 1) * w, Math.min(d, ymax), 's4').style.fillOpacity = .45);
const curve = []; for (let x = 0.04; x <= top; x += top / 300) curve.push([x, Math.min(Math.exp(-x / 2) / Math.sqrt(2 * Math.PI * x), ymax)]);
c.path(curve, 's3 line');
if (Number.isFinite(obs)) {
const xo = Math.min(obs, top);
c.line(xo, 0, xo, ymax, 's5 line'); c.text(xo, ymax, obs > top ? `observado = ${K.fmt(obs, 1)} →` : `observado = ${K.fmt(obs, 2)}`, { 'text-anchor': 'end', dx: -5, dy: 12, class: 's5' });
}
}
function simulate(s) {
// Bajo H₀ cada tienda cae en una fila con probabilidad R_i/N y en una columna con C_j/N, de forma independiente.
const pr = s.r[0] / s.n, pc = s.col[0] / s.n, out = [];
for (let t = 0; t < 2000; t++) {
const o = [0, 0, 0, 0];
for (let j = 0; j < s.n; j++) o[(rand() < pr ? 0 : 2) + (rand() < pc ? 0 : 1)]++;
const st = stats(o); out.push(st ? st.chi2 : 0);
}
return out;
}
function render(keepSims) {
if (!keepSims) sims = null;
const k = +$('#chi2-k').value; $('#chi2-k-v').textContent = '× ' + k;
const base = read();
const r1 = base[0] + base[1], r2 = base[2] + base[3], c1 = base[0] + base[2], c2 = base[1] + base[3];
[['r1', r1], ['r2', r2], ['c1', c1], ['c2', c2], ['n', r1 + r2]].forEach(([id, v]) => { $('#chi2-' + id).textContent = Number.isFinite(v) ? v : '—'; });
const o = base.map(v => v * k), s = base.some(Number.isNaN) ? null : stats(o), note = $('#chi2-note');
drawMosaic(o, s);
if (!s) {
['stat', 'p', 'fisher', 'v'].forEach(id => { $('#chi2-' + id).textContent = '—'; });
$('#chi2-cells').innerHTML = '';
note.className = 'plab-note bad'; note.textContent = 'La prueba no está definida: usa enteros no negativos y deja al menos una observación en cada fila y cada columna.';
drawNull(NaN); $('#chi2-sim').disabled = true; return;
}
$('#chi2-sim').disabled = s.n > 4000;
const p = pChi1(s.chi2), pf = s.n <= 200000 ? fisher(o) : NaN;
$('#chi2-stat').textContent = K.fmt(s.chi2, 2);
$('#chi2-p').textContent = fmtP(p);
$('#chi2-fisher').textContent = fmtP(pf);
$('#chi2-v').textContent = K.fmt(s.V, 3);
const lab = ['Supermarket · Medium', 'Supermarket · Small', 'Grocery · Medium', 'Grocery · Small'];
$('#chi2-cells').innerHTML = '<table class="plab-table"><thead><tr><th>Celda</th><th>O</th><th>E</th><th>residuo</th><th>aporte</th></tr></thead><tbody>' +
o.map((v, i) => { const res = (v - s.E[i]) / Math.sqrt(s.E[i]); return `<tr><td>${lab[i]}</td><td>${v}</td><td>${K.fmt(s.E[i], 1)}</td><td style="color:var(--lab-${res >= 0 ? 's1' : 's5'})">${res >= 0 ? '+' : ''}${K.fmt(res, 2)}</td><td>${K.fmt(s.contrib[i], 2)}</td></tr>`; }).join('') + '</tbody></table>';
const small = s.E.some(e => e < 5);
if (small) { note.className = 'plab-note warn'; note.textContent = `Hay frecuencias esperadas menores que 5 (mínima ${K.fmt(Math.min(...s.E), 2)}). La aproximación χ² es poco fiable: usa el valor p exacto de Fisher (${fmtP(pf)}).`; }
else if (p < 0.05) { note.className = 'plab-note good'; note.textContent = `χ² = ${K.fmt(s.chi2, 2)} > 3.841: hay evidencia de asociación con α = 0.05. La fuerza es V = ${K.fmt(s.V, 3)}${k > 1 ? `; multiplicar la muestra ×${k} cambió χ² y p, pero no V` : ''}.`; }
else { note.className = 'plab-note'; note.textContent = `χ² = ${K.fmt(s.chi2, 2)} ≤ 3.841: estos datos no aportan evidencia suficiente de asociación con α = 0.05 (lo que no demuestra independencia). V = ${K.fmt(s.V, 3)}.`; }
if (keepSims && sims) {
const frac = sims.filter(v => v >= s.chi2 - 1e-9).length / sims.length;
$('#chi2-sim-note').className = 'plab-note';
$('#chi2-sim-note').textContent = `${K.fmt(100 * frac, 1)} % de las 2 000 tablas simuladas alcanzan χ² ≥ ${K.fmt(s.chi2, 2)} (valor p por simulación). El teórico es ${fmtP(p)}.${frac === 0 ? ' Ninguna lo alcanza: el valor p es menor que 1/2000.' : ''}`;
} else {
$('#chi2-sim-note').className = 'plab-note';
$('#chi2-sim-note').textContent = s.n > 4000 ? 'Con N > 4000 la simulación se desactiva para no bloquear el navegador.' : 'Pulsa el botón para generar tablas con los mismos márgenes esperados y ninguna asociación.';
}
drawNull(s.chi2);
}
const preset = K.segmented($('#chi2-preset'), v => { v.split(',').forEach((x, i) => { $('#chi2-' + ids[i]).value = x; }); $('#chi2-k').value = 1; render(); });
preset.get().split(',').forEach((x, i) => { $('#chi2-' + ids[i]).value = x; });
ids.forEach(id => $('#chi2-' + id).addEventListener('input', () => render()));
$('#chi2-k').addEventListener('input', () => render());
$('#chi2-sim').addEventListener('click', () => {
const o = read().map(v => v * +$('#chi2-k').value), s = stats(o);
if (s) { sims = simulate(s); render(true); }
});
render();
})();
</script>
```
:::
::: {.content-visible when-format="pdf"}
En la versión web, este laboratorio calcula χ², el valor $p$ asintótico, el valor $p$ exacto de Fisher y V de Cramér para cualquier tabla 2 × 2; dibuja un mosaico de proporciones y simula la distribución del estadístico bajo independencia. Para resolverlo sin controles: repite el cálculo manual con `25, 15, 25, 35` ($\chi^2\approx4.17$, $p\approx0.041$, $V\approx0.204$) y con `2, 0, 1, 3` (frecuencias esperadas menores que 5; Fisher da $p=0.40$).
:::
**Comprobación.** Con `25, 15, 25, 35` obtendrás $\chi^2\approx4.17$, $p\approx0.041$ y $V\approx0.204$: hay evidencia al 5 %, pero la asociación es la mitad de fuerte que en el ejemplo inicial. Multiplicar por 10 da $\chi^2\approx41.7$ con la misma $V$. En la tabla escasa, χ² sugiere $p\approx0.083$ pero Fisher da $p=0.40$: la aproximación no es fiable con tan pocos datos.
**Interpretación para entregar.** Escribe dos frases: una sobre la evidencia de asociación (valor $p$ y prueba usada) y otra sobre su magnitud (V). Añade una tercera sobre qué aprendiste del experimento «multiplicar la muestra» para leer resultados con miles de registros.
## Mejores prácticas en la industria
::: {.callout-tip title="Mejores prácticas en la industria"}
**Cardinalidad y tamaño del efecto.** Una variable con muchas categorías puede producir celdas escasas. Agrupa categorías infrecuentes solo si existe una justificación semántica y define la regla con los datos de entrenamiento. Compara el valor $p$ con V de Cramér y el rendimiento del modelo: una muestra grande puede detectar asociaciones de poca utilidad práctica. La información mutua[^mi] es otra medida posible, pero también requiere controlar el sesgo que introduce la cardinalidad.
:::
::: {.callout-important title="Regla de oro en producción"}
**Prevención de fuga de datos.** Si usas la relación con la variable objetivo para seleccionar predictores, aprende la selección dentro de cada partición de entrenamiento y evalúala en datos separados. Si solo exploras asociación entre dos columnas, registra igualmente la procedencia de los datos y evita convertir un hallazgo exploratorio en una regla de producción sin validación.
:::
## Implementación computacional y selección de variables
Comprobamos los cuatro conteos ficticios de BigMart. `scipy.stats.chi2_contingency` usa por defecto una corrección en tablas 2 × 2; la desactivamos para reproducir el cálculo manual.
```{python}
import pandas as pd
import scipy.stats as stats
tabla_cont = pd.DataFrame(
[[30, 10], [20, 40]],
index=["Supermarket Type1", "Grocery Store"],
columns=["Medium", "Small"]
)
chi2_stat, p_val, dof, ex = stats.chi2_contingency(tabla_cont, correction=False)
v_cramer = (chi2_stat / (tabla_cont.to_numpy().sum() * min(
tabla_cont.shape[0] - 1, tabla_cont.shape[1] - 1
))) ** 0.5
print(tabla_cont)
print(f"χ² = {chi2_stat:.3f}; p = {p_val:.6f}; gl = {dof}; V = {v_cramer:.3f}")
print("Esperadas:", ex)
assert abs(chi2_stat - 16.6666667) < 1e-5
assert abs(v_cramer - 0.4082483) < 1e-5
```
La tabla describe asociación entre **dos predictores**; por sí sola no indica cuál mejorará las predicciones de ventas. Para seleccionar variables con un objetivo `y`, se necesita un procedimiento supervisado dentro de las particiones de entrenamiento. Además, `sklearn.feature_selection.chi2` calcula una prueba de puntuación para características no negativas frente a clases; su resultado no equivale automáticamente a la prueba de independencia sobre una tabla de contingencia ni a V de Cramér.
### Visualizar redundancia entre predictores
En este ejemplo pequeño añadimos una categoría `Outlet_Region` inventada para ilustrar una tercera variable; los valores no representan datos reales.
```{python}
import numpy as np
import plotly.express as px
registros = []
for (tipo, tamano), n in tabla_cont.stack().items():
for i in range(n):
registros.append((tipo, tamano, "Norte" if i % 3 else "Sur"))
muestra = pd.DataFrame(registros, columns=["Outlet_Type", "Outlet_Size", "Outlet_Region"])
def cramer_v(tabla):
if min(tabla.shape) < 2:
return float("nan")
estadistico = stats.chi2_contingency(tabla, correction=False)[0]
return np.sqrt(estadistico / (tabla.to_numpy().sum() * min(
tabla.shape[0] - 1, tabla.shape[1] - 1
)))
variables = muestra.columns.tolist()
matriz_v = pd.DataFrame(index=variables, columns=variables, dtype=float)
for a in variables:
for b in variables:
matriz_v.loc[a, b] = 1.0 if a == b else cramer_v(pd.crosstab(muestra[a], muestra[b]))
fig = px.imshow(matriz_v, text_auto=".2f", zmin=0, zmax=1,
color_continuous_scale="Blues",
title="V de Cramér entre predictores (ejemplo ficticio)")
fig.update_layout(template="plotly_white", height=430)
fig.show()
```
**Comprobación.** El valor para `Outlet_Type` y `Outlet_Size` debe ser 0.408. Las asociaciones entre predictores sugieren redundancia posible; decídela mediante validación del modelo, costo de captura y estabilidad temporal.
[^contingencia]: **Tabla de contingencia:** matriz de conteos en la que cada celda combina una categoría de la primera variable con otra de la segunda.
[^gl]: **Grados de libertad:** número de conteos que pueden variar una vez fijados los totales de filas y columnas. En una tabla 2 × 2 queda uno.
[^mi]: **Información mutua:** medida de dependencia basada en cuánto reduce conocer una variable la incertidumbre sobre otra.
[^1]: **Corrección de continuidad de Yates:** ajuste conservador para la aproximación de conteos discretos por una distribución continua en tablas 2 × 2. Puede reducir potencia; no reemplaza automáticamente una prueba exacta en tablas pequeñas.
[^2]: **Prueba exacta de Fisher:** prueba para tablas 2 × 2 que calcula probabilidades condicionadas a los márgenes mediante la distribución hipergeométrica, sin depender de la aproximación χ².
[^3]: **Sensibilidad al tamaño de muestra ($N$):** con muestras grandes, diferencias pequeñas pueden producir valores $p$ bajos. Por eso conviene acompañar la prueba con un tamaño del efecto como V de Cramér.
[^4]: **Paradoja de Simpson**: Ocurre cuando una tendencia aparece en diferentes grupos de datos pero desaparece o se invierte cuando estos grupos se combinan. Es un riesgo al analizar tablas de contingencia agregadas sin controlar por variables de confusión subyacentes.
[^5]: **Criterios de Cochran**: Establecen las condiciones bajo las cuales la aproximación asintótica a la distribución χ² es fiable. Principalmente, requieren que no más del 20% de las celdas tengan frecuencias esperadas menores a 5, y que ninguna celda tenga una frecuencia esperada menor a 1.
[^marginal]: **Distribución marginal:** distribución de una sola variable obtenida al sumar la tabla sobre las categorías de la otra. Sus proporciones estiman $P(X=x_i)$ y $P(Y=y_j)$.
[^h0]: **Hipótesis nula ($H_0$):** afirmación que se supone cierta para calcular qué resultados serían esperables; aquí, que tipo y tamaño de tienda son independientes. La prueba no demuestra $H_0$: solo evalúa si los datos son poco compatibles con ella.
[^dist-chi2]: **Distribución χ² con $k$ grados de libertad:** distribución de la suma de los cuadrados de $k$ variables normales estándar independientes. Tiene media $k$ y varianza $2k$; con $k=1$ concentra la mayor parte de su masa cerca de cero, y $P(\chi^2_1\ge3.841)=0.05$.
[^pvalor]: **Valor $p$:** probabilidad, calculada suponiendo $H_0$ cierta, de obtener un estadístico al menos tan extremo como el observado. No es la probabilidad de que $H_0$ sea cierta ni mide el tamaño del efecto.
[^alfa]: **Nivel de significancia ($\alpha$):** umbral fijado **antes** de mirar los datos para decidir cuándo rechazar $H_0$. Es la probabilidad de rechazarla cuando en realidad es cierta (error de tipo I). El valor 0.05 es una convención, no una ley.
[^indep]: **Independencia estadística:** conocer el valor de una variable no cambia las probabilidades de la otra: $P(Y=y_j\mid X=x_i)=P(Y=y_j)$. Multiplicando por $P(X=x_i)$ se obtiene la factorización de la probabilidad conjunta.
[^mle]: **Estimador de máxima verosimilitud:** valor de los parámetros que hace más probable la muestra observada bajo el modelo supuesto. Para proporciones de una distribución multinomial, coincide con la frecuencia relativa observada.