15  Pruebas de Kolmogorov-Smirnov

En los capítulos anteriores usamos la prueba \(\chi^2\) de bondad de ajuste, que obliga a agrupar los datos en clases antes de comparar las frecuencias observadas con las esperadas. Ese agrupamiento desperdicia información cuando la variable es continua y la elección de las clases puede cambiar la conclusión. En este capítulo desarrollamos la prueba de Kolmogorov-Smirnov (KS), que compara directamente la función de distribución acumulada sin agrupar, por lo que suele ser más adecuada —y más potente— para datos continuos.

NotaObjetivos de aprendizaje

Al terminar este capítulo podré:

  • Definir la función de distribución empírica \(F_n\) y explicar por qué es un estimador consistente de la verdadera \(F\).
  • Comprender por qué, bajo \(H_0\), el estadístico \(D_n^\star\) no depende de la distribución teórica \(F^\star\).
  • Calcular a mano el estadístico \(D_n^\star\) de la prueba KS de una y de dos muestras, y compararlo con su valor crítico.
  • Aplicar la prueba KS en R y en Python, e interpretar correctamente el valor-\(p\) y la decisión sobre \(H_0\).
  • Analizar cuándo conviene KS frente a la prueba \(\chi^2\) y qué supuestos exige (continuidad de \(F\), parámetros no estimados de la misma muestra).

15.1 La función de distribución empírica

Antes de comparar nuestros datos con una distribución teórica necesitamos una forma de resumir la distribución que muestran los datos por sí solos. Esa es la función de distribución empírica: la versión muestral de la función de distribución acumulada.

Supongamos que tenemos una serie de observaciones \(X_1,\dots,X_n\), que siguen una distribución continua \(F\). También asumimos que los valores observados \(x_1,\dots,x_n\) son todos diferentes.

Definición 15.1 Dados los valores observados \(x_1,\dots,x_n\) de la muestra aleatoria \(X_1,\dots, X_n\), para cada \(x\) definimos \(F_n(x)\) como la proporción de valores observados en la muestra que son menores o iguales a \(x\). Es decir, si hay \(k\) valores observados menores o iguales a \(x\), \[ F_n(x) = \frac{k}{n}. \]

La función \(F_n(x)\) se conoce como la función de distribución de la muestra (empírica). \(F_n\) es una distribución escalonada con saltos de magnitud \(\dfrac 1n\) en los valores \(x_1,\dots,x_n\). Se puede expresar como \[ F_n(x) = \begin{cases} 0 & \text{si } x<X_{(1)}\\ \displaystyle \dfrac 1n \sum_{i=1}^n 1_{\{x_i\leq x\}} & \text{si } X_{(1)}\leq x<X_{(n)}\\ 1 & \text{si } x\geq X_{(n)} \end{cases} \]

Dado que \(\{x_i\}_{i=1}^n\) son independientes, \(\{1_{\{x_i\leq x\}}\}_{i=1}^n\) también son independientes. Por lo tanto, por la ley de los grandes números

\[ F_n(x) = \dfrac 1n \sum_{i=1}^n 1_{\{x_i\leq x\}} \xrightarrow[n\to\infty]{\mathbb P} \mathbb E[1_{X_i\leq x}] = F(x) \]

por lo que \(F_n(x)\) es consistente. Esta convergencia es la razón de fondo por la que comparar \(F_n\) con una distribución teórica tiene sentido: si los datos provienen de \(F\), entonces \(F_n\) se parece cada vez más a \(F\) conforme crece la muestra.

Ejemplo 15.1 Suponga que se tiene el siguiente conjunto de datos:

Código
x <- c(
    17.88,
    28.92,
    33,
    41.52,
    42.12,
    45.6,
    48.8,
    51.84,
    51.96,
    54.12,
    55.56,
    67.8,
    68.44,
    68.64,
    68.88,
    84.12,
    93.12,
    98.64,
    105.12,
    105.84,
    127.92,
    128.04,
    173.4
)
(log(x))
 [1] 2.883683 3.364533 3.496508 3.726175 3.740523 3.819908 3.887730 3.948162
 [9] 3.950474 3.991204 4.017464 4.216562 4.225957 4.228875 4.232366 4.432244
[17] 4.533889 4.591477 4.655103 4.661929 4.851405 4.852343 5.155601
Código
df <- as.data.frame(x)
Código
import numpy as np

x = np.array([
    17.88, 28.92, 33, 41.52, 42.12, 45.6, 48.8, 51.84, 51.96, 54.12,
    55.56, 67.8, 68.44, 68.64, 68.88, 84.12, 93.12, 98.64, 105.12,
    105.84, 127.92, 128.04, 173.4
])
print(np.log(x))
[2.88368277 3.3645334  3.49650756 3.72617524 3.74052269 3.81990772
 3.88773031 3.94816205 3.95047419 3.9912038  4.01746352 4.21656219
 4.22595745 4.22887546 4.23236586 4.43224435 4.53388898 4.59147686
 4.65510255 4.66192852 4.85140507 4.85234272 5.15560106]

Para este ejemplo tenemos que \(F_n(x)\) se ve de la forma:

Código
ggplot() +
    stat_ecdf(
        data = df,
        mapping = aes(x, color = "F_n(x)"),
        linewidth = 2
    ) +
    cowplot::theme_cowplot()

Código
import matplotlib.pyplot as plt

xs = np.sort(x)                            # valores ordenados
fn = np.arange(1, len(xs) + 1) / len(xs)   # F_n con saltos de 1/n
fig, ax = plt.subplots()
ax.step(xs, fn, where="post", color="#cab2d6", linewidth=2, label="F_n(x)")
ax.set_xlabel("x"); ax.set_ylabel("F_n(x)"); ax.legend()
plt.show()

En el capítulo sobre la prueba \(\chi^2\) trabajamos con estos mismos datos y encontramos que sus log-valores se ajustaban bien a una distribución normal. Estimando la media y la varianza directamente de la muestra obtenemos \(\hat\mu = \overline{\log x} \approx 4.151\) y \(s^2 \approx 0.284\) (es decir \(s\approx 0.533\)). Podemos verificar este ajuste superponiendo la normal teórica sobre la distribución empírica:

Código
ggplot() +
    stat_ecdf(
        data = df,
        mapping = aes(log(x), color = "F_n(x)"),
        linewidth = 2
    ) +
    stat_function(
        data = data.frame(x = c(3, 5)),
        fun = pnorm,
        aes(color = "F(x)"),
        linewidth = 2,
        # parámetros estimados de la propia muestra
        args = list(mean = mean(log(x)), sd = sd(log(x)))
    ) +
    cowplot::theme_cowplot()
Warning in stat_function(data = data.frame(x = c(3, 5)), fun = pnorm, aes(color = "F(x)"), : All aesthetics have length 1, but the data has 2 rows.
ℹ Please consider using `annotate()` or provide this layer with data containing
  a single row.

Código
from scipy import stats

lx = np.sort(np.log(x))
mu, s = np.log(x).mean(), np.log(x).std(ddof=1)  # parámetros estimados
fn = np.arange(1, len(lx) + 1) / len(lx)
grid = np.linspace(3, 5, 200)
fig, ax = plt.subplots()
ax.step(lx, fn, where="post", color="#cab2d6", linewidth=2, label="F_n(x)")
ax.plot(grid, stats.norm.cdf(grid, mu, s), color="#b2df8a",
        linewidth=2, label="F(x)")
ax.set_xlabel("log(x)"); ax.set_ylabel("Probabilidad acumulada"); ax.legend()
plt.show()

La prueba de Kolmogorov-Smirnov se basa en el siguiente teorema, conocido como el Lema de Glivenko-Cantelli.

Lema 15.1 (Glivenko-Cantelli) Sea \(F_n(x)\) la distribución empírica de una muestra \(X_1, \dots, X_n\) provenientes de la distribución \(F\). Defina \[ D_n = \sup_{-\infty<x<\infty}|F_n(x)-F(x)| \] Entonces \[ D_n\xrightarrow[]{\mathbb P} 0 \]

Esto implica que si una distribución empírica \(F_n(x)\) se obtiene realmente de la distribución teórica \(F(x)\), entonces la diferencia entre estas dos convergerá en probabilidad a 0 cuando $n $. Esta es la idea clave de toda la prueba: la distancia máxima entre \(F_n\) y \(F\) resume en un solo número qué tan lejos están las dos distribuciones, y bajo \(H_0\) esa distancia debe ser pequeña.

La demostración de este teorema está más allá del alcance de este curso, por lo que no se incluirá en esta sección.

TipIdeas clave de esta sección
  • \(F_n(x)\) es la proporción de datos \(\le x\): una escalera con saltos de \(1/n\).
  • Por la ley de los grandes números, \(F_n(x)\to F(x)\) para cada \(x\) (consistencia).
  • Glivenko-Cantelli refuerza esto: la distancia máxima \(D_n=\sup_x|F_n-F|\) tiende a \(0\).
  • Esa distancia máxima será el estadístico sobre el que se construye la prueba KS.

15.2 Prueba de Kolmogorov-Smirnov para una muestra

Ya sabemos que, si los datos provienen de \(F\), la distancia entre \(F_n\) y \(F\) se hace pequeña. Ahora damos el paso decisivo: convertir esa distancia en una prueba de hipótesis formal que nos diga si una distribución teórica concreta \(F^\star\) es compatible con los datos.

La prueba de Kolmogorov-Smirnov se utiliza para responder a la pregunta de si una distribución empírica de datos, \(F\), es igual a una distribución teórica, \(F^\star\). Se establecen las siguientes hipótesis:

  • Hipótesis nula, \(H_0\): \(F = F^{\star}\)
  • Hipótesis alternativa, \(H_1\): \(F\neq F^{\star}\)

El estadístico de prueba se define como \[ D_n^{\star} = \sup_{-\infty<x<\infty}|F_n(x)-F^{\star}(x)|. \]

Importante

Si la hipótesis nula es verdadera, entonces la distribución del estadístico \(D_n^{\star}\) no depende de \(F^{\star}\). Esto es lo que permite tabular un único conjunto de valores críticos válido para cualquier distribución teórica continua.

Si consideramos \(Z_i = F^{\star}(X_i)\), donde \(i=1,\dots,n\) y \((X_1,\dots,X_n \sim F)\), entonces se puede demostrar que:

\[ \mathbb P(Z_i\leq z) = \mathbb P(F^{\star}(X_i)\leq z) = \mathbb P(X_i\leq {F^{\star}}^{-1}(z)) = z \]

Bajo la hipótesis nula, \(Z_1,\dots, Z_n\) se distribuyen como una distribución uniforme entre 0 y 1, es decir, \(Z_1,\dots, Z_n \underset{H_0}{\sim} \text{Unif}(0,1)\).

Si consideramos una nueva hipótesis, \(H_0: G = \text{Unif}(0,1)\), donde \(G\) es la distribución de \(Z_i\), entonces el estadístico de prueba se redefine como:

\[\begin{align*} D_n^{\star,G} &= \sup_{0<z<1}|G_n(z)-G^{\star}(z)| \\ &=\sup_{0<z<1}|G_n(z)-F_{\text{Unif}(0,1)}(z)|\\ &= \sup_{0<z<1}|G_n(z)-z| \end{align*}\]

donde

\[\begin{align*} G_n(z) &= \dfrac 1n \sum_{i=1}^n 1_{\{Z_i\leq z\}} \\ &= \dfrac 1n \sum_{i=1}^n 1_{\{F^{\star}(X_i)\leq z\}} \\ &= \dfrac 1n \sum_{i=1}^n 1_{\{X_i\leq (F^{\star})^{-1}(z)\}} \\ &= F_n((F^{\star})^{-1}(z)) \end{align*}\]

Se puede demostrar que \(G_n(z) = F_n((F^{\star})^{-1}(z))\), lo que implica que \(D_n^{\star,G}\) es igual a \(D_n^{\star}\) bajo la hipótesis nula. Por lo tanto, \(D_n^{\star}\) no depende de \(F^{\star}\) cuando la hipótesis nula es verdadera.

En términos prácticos, si la distribución empírica \(F_n(x)\) está cerca de la distribución teórica \(F^{\star}(x)\), entonces el valor de \(D_n^{\star}\) será cercano a cero. Por lo tanto, rechazamos la hipótesis nula, \(H_0\), si \(n^{\frac{1}{2}}D_n^{\star}\geq c\) para algún valor crítico \(c\).

Este valor crítico \(c\) se obtiene a partir de la distribución de Kolmogorov-Smirnov. De acuerdo con el teorema de Kolmogorov-Smirnov (1930), si la hipótesis nula es verdadera, para \(t>0\),

Teorema 15.1 (Kolmogorov-Smirnov (1930)) Si \(H_0\) es cierto, para \(t>0\), \[ \lim_{n\to \infty} \mathbb P(n^{1/2}D_n^{\star}\leq t) = 1-2\sum_{i=1}^\infty (-1)^{i-1}e^{-2i^2t^2} = H(t). \]

Rechazamos \(H_0\) si \(n^{1/2}D_n^{\star}\geq c\), \(n\) grande. Para un nivel de significancia \(\alpha_0\), \(c = H^{-1}(1-\alpha_0)\), donde \(H\) denota el valor de la parte derecha de la ecuación anterior.

El siguiente diagrama resume el procedimiento completo de la prueba para una muestra:

La función \(H(t)\) es algo complicada de estimar, y sus cuantiles lo son aún más. Estos normalmente son definidos a través de métodos núméricos que están fuera del alcance del este curso. La siguiente tabla muestra el conjunto de valores estimados para cada \(t\)

La función de distribución se ve de la siguiente forma:

Código
H <- function(t) {
  i <- 1:1e4
  1 - 2 * sum((-1)^(i - 1) * exp(-2 * i^2 * t^2))
}

t <- seq(0.1, 2, length.out = 1000)
data.frame(t, H = Vectorize(H)(t)) %>%
  ggplot(aes(t, H)) +
  geom_line(linewidth = 2) +
  cowplot::theme_cowplot()

Código
def H(t, terms=10000):
    i = np.arange(1, terms + 1)
    return 1 - 2 * np.sum((-1)**(i - 1) * np.exp(-2 * i**2 * t**2))

t = np.linspace(0.1, 2, 1000)
Hvals = np.array([H(ti) for ti in t])
fig, ax = plt.subplots()
ax.plot(t, Hvals, linewidth=2)
ax.set_xlabel("t"); ax.set_ylabel("H(t)")
plt.show()

Los valores más comunes de cuantiles para las pruebas son

\(\alpha\) \(H^{-1}(1-\alpha)\)
0.01 1.63
0.05 1.36
0.1 1.22

Solo como ilustración, la funcion ks.test tiene programada la estimación de \(\mathbb P (D_n \leq t)\). En este gráfico se presentan algunos ejemplos con \(n\) igual a 10, 50 y 100.

Código
pkolmogorov1x <- function(x, n) {
    ## Probability function for the one-sided one-sample Kolmogorov
    ## statistics, based on the formula of Birnbaum & Tingey (1951).
    if (x <= 0) {
        return(0)
    }
    if (x >= 1) {
        return(1)
    }
    j <- seq.int(from = 0, to = floor(n * (1 - x)))
    1 -
        x *
            sum(
                exp(
                    lchoose(n, j) +
                        (n - j) * log(1 - x - j / n) +
                        (j - 1) * log(x + j / n)
                )
            )
}

t <- seq(0.001, 0.999, length.out = 1000)

df <- rbind(
    data.frame(t, ks = Vectorize(pkolmogorov1x)(t, 10), n = 10),
    data.frame(t, ks = Vectorize(pkolmogorov1x)(t, 50), n = 50),
    data.frame(t, ks = Vectorize(pkolmogorov1x)(t, 100), n = 100)
)
Código
ggplot(df, aes(x = t, y = ks, color = as.factor(n))) +
  geom_line(linewidth = 2) +
  labs(color = "n") +
  cowplot::theme_cowplot()

Código
from scipy.special import gammaln

def pkolmogorov1x(xv, n):
    # Birnbaum & Tingey (1951): P(D_n <= x) exacto para una cola
    if xv <= 0:
        return 0.0
    if xv >= 1:
        return 1.0
    j = np.arange(0, np.floor(n * (1 - xv)) + 1)
    lchoose = gammaln(n + 1) - gammaln(j + 1) - gammaln(n - j + 1)
    return 1 - xv * np.sum(np.exp(
        lchoose + (n - j) * np.log(1 - xv - j / n) + (j - 1) * np.log(xv + j / n)
    ))

t = np.linspace(0.001, 0.999, 1000)
fig, ax = plt.subplots()
for nn in (10, 50, 100):
    ax.plot(t, [pkolmogorov1x(ti, nn) for ti in t], linewidth=2, label=f"n = {nn}")
ax.set_xlabel("t"); ax.set_ylabel("P(D_n <= t)"); ax.legend(title="n")
plt.show()

Ejemplo 15.2 En el caso de las partes mecánicas quisiéramos saber si los log-valores siguen o no una distribución normal de parámetros \(N (\hat{\mu } , s^{2})\).

Para calcular manualmente el valor de \(D_{n}^{\star }\) se deben usar los siguientes dos estimadores:

\[\begin{align*} D_n^{+} &= \sup_{1\leq i\leq n}\left\{ \dfrac{i}{n}-F(X_{(i)})\right\} \\ D_n^{-} &= \sup_{1\leq i\leq n}\left\{ F(X_{(i)})-\dfrac{i-1}{n}\right\} \end{align*}\]

Es decir, se debe tomar en cuenta si la distribución empírica está por encima o por debajo de la distribución teórica.

Una vez con esto se puede calcular el valor de \(D_{n}^{\star }\) como

\[ D_{n}^{\star }=\max \left\{ D_{n}^{+},D_{n}^{-}\right\} \]

Código
df <- data.frame(logx = sort(log(x)))
F_t <- pnorm(sort(log(x)), mean = mean(log(x)), sd = sd(log(x)))
F_n <- seq(1, length(F_t)) / length(F_t)
df <- cbind(df, F_n, F_t)

df <- df %>%
    mutate(
        D_n_plus = abs(F_n - F_t),
        D_n_minus = abs(F_t - lag(F_n)),
        D_n_star = pmax(D_n_plus, D_n_minus, na.rm = TRUE)
    )
knitr::kable(
    df,
    digits = 3,
    col.names = c(
        "$\\log(x)$",
        "$F_n(\\log(x))$",
        "$F(\\log(x))$",
        "$D_n^+$",
        "$D_n^-$",
        "$D_n^\\star$"
    )
)
\(\log(x)\) \(F_n(\log(x))\) \(F(\log(x))\) \(D_n^+\) \(D_n^-\) \(D_n^\star\)
2.884 0.043 0.009 0.035 NA 0.035
3.365 0.087 0.070 0.017 0.027 0.027
3.497 0.130 0.110 0.020 0.023 0.023
3.726 0.174 0.213 0.039 0.083 0.083
3.741 0.217 0.221 0.004 0.047 0.047
3.820 0.261 0.268 0.007 0.050 0.050
3.888 0.304 0.311 0.007 0.050 0.050
3.948 0.348 0.352 0.004 0.048 0.048
3.950 0.391 0.354 0.038 0.006 0.038
3.991 0.435 0.382 0.052 0.009 0.052
4.017 0.478 0.401 0.077 0.033 0.077
4.217 0.522 0.549 0.027 0.071 0.071
4.226 0.565 0.556 0.009 0.034 0.034
4.229 0.609 0.558 0.050 0.007 0.050
4.232 0.652 0.561 0.091 0.048 0.091
4.432 0.696 0.701 0.006 0.049 0.049
4.534 0.739 0.764 0.025 0.068 0.068
4.591 0.783 0.796 0.013 0.057 0.057
4.655 0.826 0.828 0.002 0.045 0.045
4.662 0.870 0.831 0.038 0.005 0.038
4.851 0.913 0.906 0.007 0.036 0.036
4.852 0.957 0.906 0.051 0.007 0.051
5.156 1.000 0.970 0.030 0.014 0.030
Código
import pandas as pd

lx = np.sort(np.log(x))
mu, s = np.log(x).mean(), np.log(x).std(ddof=1)        # N(mu_hat, s^2) estimada
F_t = stats.norm.cdf(lx, mu, s)
F_n = np.arange(1, len(lx) + 1) / len(lx)
D_n_plus = np.abs(F_n - F_t)                            # F_n por encima de F
D_n_minus = np.abs(F_t - np.append(np.nan, F_n[:-1]))   # F por encima de F_n
D_n_star = np.fmax(D_n_plus, D_n_minus)
tabla = pd.DataFrame({
    "log(x)": lx, "F_n": F_n, "F": F_t,
    "D_n+": D_n_plus, "D_n-": D_n_minus, "D_n*": D_n_star,
})
print(tabla.round(3).to_string(index=False))
 log(x)   F_n     F  D_n+  D_n-  D_n*
  2.884 0.043 0.009 0.035   NaN 0.035
  3.365 0.087 0.070 0.017 0.027 0.027
  3.497 0.130 0.110 0.020 0.023 0.023
  3.726 0.174 0.213 0.039 0.083 0.083
  3.741 0.217 0.221 0.004 0.047 0.047
  3.820 0.261 0.268 0.007 0.050 0.050
  3.888 0.304 0.311 0.007 0.050 0.050
  3.948 0.348 0.352 0.004 0.048 0.048
  3.950 0.391 0.354 0.038 0.006 0.038
  3.991 0.435 0.382 0.052 0.009 0.052
  4.017 0.478 0.401 0.077 0.033 0.077
  4.217 0.522 0.549 0.027 0.071 0.071
  4.226 0.565 0.556 0.009 0.034 0.034
  4.229 0.609 0.558 0.050 0.007 0.050
  4.232 0.652 0.561 0.091 0.048 0.091
  4.432 0.696 0.701 0.006 0.049 0.049
  4.534 0.739 0.764 0.025 0.068 0.068
  4.591 0.783 0.796 0.013 0.057 0.057
  4.655 0.826 0.828 0.002 0.045 0.045
  4.662 0.870 0.831 0.038 0.005 0.038
  4.851 0.913 0.906 0.007 0.036 0.036
  4.852 0.957 0.906 0.051 0.007 0.051
  5.156 1.000 0.970 0.030 0.014 0.030
Código
ggplot(df) +
    geom_step(aes(logx, F_n, color = "F_n"), linewidth = 2) +
    geom_line(aes(logx, F_t, color = "F"), linewidth = 2) +
    geom_segment(
        aes(x = logx, xend = logx, y = F_n, yend = F_t, color = "D+"),
        linewidth = 2,
    ) +
    geom_segment(
        aes(x = logx, xend = logx, y = F_t, yend = lag(F_n), color = "D-"),
        linewidth = 2,
    ) +
    cowplot::theme_cowplot() +
    scale_color_manual(
        values = c(
            "F_n" = "#cab2d6",
            "F" = "#b2df8a",
            "D+" = "#e31a1c",
            "D-" = "#1f78b4"
        )
    )

Código
fig, ax = plt.subplots()
ax.step(lx, F_n, where="post", color="#cab2d6", linewidth=2, label="F_n")
ax.plot(lx, F_t, color="#b2df8a", linewidth=2, label="F")
ax.vlines(lx, F_n, F_t, color="#e31a1c", linewidth=2, label="D+")
ax.vlines(lx, F_t, np.append(np.nan, F_n[:-1]), color="#1f78b4",
          linewidth=2, label="D-")
ax.set_xlabel("log(x)"); ax.set_ylabel("Probabilidad acumulada"); ax.legend()
plt.show()

El valor de \(D_n^\star\) es el máximo de la última columna:

Código
(D_n_star <- max(df$D_n_star, na.rm = TRUE))
[1] 0.09124598
Código
D_n_star = np.nanmax(D_n_star)
print(f"D_n_star = {D_n_star:.4f}")
D_n_star = 0.0912

El valor se estimaría usando la función vista anteriormente

Código
n <- length(x)
sqrt(n) * D_n_star
[1] 0.4376003
Código
n = len(x)
print(f"sqrt(n)*D_n_star = {np.sqrt(n) * D_n_star:.4f}")
sqrt(n)*D_n_star = 0.4376

A un nivel de significancia del 5% se rechaza la hipótesis nula si \(\sqrt{n}\,D_n^\star \ge 1.36\):

Código
sqrt(n) * D_n_star > 1.36
[1] FALSE
Código
print(np.sqrt(n) * D_n_star > 1.36)
False

Como \(\sqrt{n}\,D_n^\star\approx 0.44 < 1.36\), no rechazamos \(H_0\): no hay evidencia contra la hipótesis de que los log-valores sigan la normal estimada. El valor-\(p\) asociado, \(1-H(\sqrt{n}\,D_n^\star)\), es cercano a \(1\):

Código
1 - H(sqrt(n) * D_n_star)
[1] 0.9908784
Código
print(f"valor-p (1 - H) = {1 - H(np.sqrt(n) * D_n_star):.4f}")
valor-p (1 - H) = 0.9909

Un cálculo más preciso del valor-\(p\) exacto se obtiene con el paquete KSgeneral:

Código
library(KSgeneral)
cont_ks_c_cdf(D_n_star, n)
[1] 0.9814592

Otra forma de hacer es usando la función ks.test

Código
ks.test(
  x = log(x),
  y = "pnorm",
  mean = mean(log(x)),
  sd = sd(log(x))
)

    Exact one-sample Kolmogorov-Smirnov test

data:  log(x)
D = 0.091246, p-value = 0.9815
alternative hypothesis: two-sided
Código
print(stats.kstest(np.log(x), "norm", args=(mu, s)))
KstestResult(statistic=np.float64(0.09124597896237374), pvalue=np.float64(0.9814591756078181), statistic_location=np.float64(4.232365860119475), statistic_sign=np.int8(1))
Código
cont_ks_test(log(x), "pnorm", mean(log(x)), sd(log(x)))

    One-sample Kolmogorov-Smirnov test

data:  log(x)
D = 0.091246, p-value = 0.9815
alternative hypothesis: two-sided

Observación. Los valores de los valores-p son diferentes ya que la función ks.test usa varias aproximaciones para calcularlos.

Importante

En este ejemplo estimamos \(\mu\) y \(\sigma\) de la misma muestra que luego probamos. Cuando esto ocurre, la distribución \(H(t)\) deja de ser exacta y la prueba KS se vuelve conservadora: tiende a no rechazar (valor-\(p\) inflado). Para corregirlo se usa la prueba de Lilliefors o rutinas como KSgeneral::cont_ks_test, no el ks.test estándar. El KS clásico es exacto solo cuando \(F^\star\) está completamente especificada antes de ver los datos.

Note que esta localización es muy importante ya que si se quisiera comparar con una distribución \(N(0,1)\) el resultado es diferente.

Código
ks.test(
    x = log(x),
    y = "pnorm",
    mean = 0,
    sd = 1
)

    Exact one-sample Kolmogorov-Smirnov test

data:  log(x)
D = 0.99803, p-value = 4.441e-16
alternative hypothesis: two-sided
Código
print(stats.kstest(np.log(x), "norm", args=(0, 1)))
KstestResult(statistic=np.float64(0.9980347275754776), pvalue=np.float64(1.121381755242548e-62), statistic_location=np.float64(2.883682769745368), statistic_sign=np.int8(-1))
TipIdeas clave de esta sección
  • El estadístico KS es la distancia vertical máxima entre \(F_n\) y \(F^\star\): \(D_n^\star=\max\{D_n^+, D_n^-\}\).
  • Se rechaza \(H_0\) si \(\sqrt{n}\,D_n^\star \ge c\), con \(c=H^{-1}(1-\alpha)\) (p. ej. \(1.36\) para \(\alpha=0.05\)).
  • La distribución de referencia \(H(t)\) es universal: no depende de \(F^\star\).
  • Si los parámetros se estiman de los datos, usar Lilliefors / KSgeneral, no el KS clásico.

15.3 Prueba de 2 muestras

Hasta aquí comparamos una muestra contra una distribución teórica conocida. A menudo, sin embargo, no tenemos una \(F^\star\) de referencia sino dos muestras y queremos saber si provienen de la misma población. La prueba KS se extiende de forma natural a este caso.

Suponga que se tiene \(X_1,\dots,X_m\sim N(\mu_1,\sigma^2)\) y \(Y_1,\dots,Y_n\sim N(\mu_2,\sigma^2)\) y se desea saber si ambas muestras tienen la misma distribución.

Una opción es probar que \[ H_0:\mu_1 = \mu_2 \text{ vs } H_1: \mu_{1} \neq \mu_{2} \]

Uno de los supuestos fuertes para este tipo de pruebas es la normalidad.

Otra opción es decir que

\[ H_0: F_1 = F_2 \text{ vs } H_1: F_{1} \neq F_{2} \]

donde \(F_1\) es la distribución de \(X\) y \(F_2\) la de \(Y\).

Igual en este caso estamos probando dos distribuciones normales, pero ¿Es posible del todo quitar el supuesto de normalidad?

Es decir, para \(X_1,\dots,X_m\sim F\) y \(Y_1,\dots,Y_m\sim G\) continuas, sin valores en común, probar \(H_0: F(x) = G(x)\), \(x \in \mathbb R\).

Considere \[ D_{mn}^{\star} = \sup_{-\infty<x<\infty}|F_m(x)-G_n(x)| \]

Se tiene por el teorema de Glivenko-Cantelli que \(D\_{mn}^{\star}\xrightarrow[]{\mathbb P} 0\), \(m,n\to\infty\) cuando \(H_0\) es verdadera

Para el caso de dos muestras se puede probar que si \(H(t)\) es la distribución límite en el caso de una muestra y \(t>0\), entonces se cumple que

\[ \lim_{m,n\to \infty} \mathbb P \left( \left( \dfrac{mn}{m+n}\right)^{\frac 12} D_{mn}^{\star}\leq t\right) = H(t) \]

En este caso se rechaza la hipótesis nula si \(\left(\dfrac{mn}{m+n}\right)^{\frac 12}D_{mn}^{\star} \geq c(\alpha)\), donde \(c(\alpha) =\sqrt{-\frac{1}{2}\ln\left(\frac{\alpha}{2}\right)}\).

Ejemplo 15.3 Suponga que se tienen dos grupos de personas a las cuales a unas se les dio un tratamiento para la presión arterial y al otro se le dio un placebo.

Al se midieron las diferencias entre las presiones arteriales al cabo de 12 semanas de tratamiento.

Los resultados fueron estos

Código
Medicina <- c(7, -4, 18, 17, -3, -5, 1, 10, 11, -2)
Placebo <- c(-1, 12, -1, -3, 3, -5, 5, 2, -11, -1, -3)
Código
Medicina = np.array([7, -4, 18, 17, -3, -5, 1, 10, 11, -2])
Placebo = np.array([-1, 12, -1, -3, 3, -5, 5, 2, -11, -1, -3])

La pregunta es si ambos conjuntos de datos vienen de la misma distribución.

Podemos repetir el mismo procedimiento anterior, pero modificado para dos muestras. Primero necesitamos estimar el \(D_{nm}^{\star}\) usando estos dos estimadores:

\[\begin{align*} D_{nm}^{+} &= \sup_{1\leq i\leq n}\left\{ F_{m}(X_{(i)})-G_{n}(X_{(i)})\right\} \\ D_{nm}^{-} &= \sup_{1\leq i\leq n}\left\{ G_{n}(X_{(i)})-F_{m}(X_{(i-1)})\right\} \end{align*}\]

Estimamos \(D_{nm}^{\star}\) como

\[ D_{nm}^{\star }=\max \left\{ D_{nm}^{+},D_{nm}^{-}\right\} \]

Código
Medicina <- sort(Medicina)
Placebo <- sort(Placebo)
Valores <- unique(sort(c(Medicina, Placebo)))
df <- data.frame(
  Valores,
  F_Medicina = ecdf(Medicina)(Valores),
  F_Placebo = ecdf(Placebo)(Valores)
)

df <- df %>%
  mutate(
    D_mn_plus = abs(F_Medicina - F_Placebo),
    D_mn_minus = abs(F_Placebo - lag(F_Medicina)),
    D_mn_star = pmax(D_mn_plus, D_mn_minus, na.rm = TRUE)
  )

knitr::kable(
  df,
  digits = 3,
  col.names = c(
    "Medicina",
    "$F_\\text{Medicina}$",
    "$F_\\text{Placebo}$",
    "$D_{mn}^+$",
    "$D_{mn}^-$",
    "$D_{mn}^\\star$"
  )
)
Medicina \(F_\text{Medicina}\) \(F_\text{Placebo}\) \(D_{mn}^+\) \(D_{mn}^-\) \(D_{mn}^\star\)
-11 0.0 0.091 0.091 NA 0.091
-5 0.1 0.182 0.082 0.182 0.182
-4 0.2 0.182 0.018 0.082 0.082
-3 0.3 0.364 0.064 0.164 0.164
-2 0.4 0.364 0.036 0.064 0.064
-1 0.4 0.636 0.236 0.236 0.236
1 0.5 0.636 0.136 0.236 0.236
2 0.5 0.727 0.227 0.227 0.227
3 0.5 0.818 0.318 0.318 0.318
5 0.5 0.909 0.409 0.409 0.409
7 0.6 0.909 0.309 0.409 0.409
10 0.7 0.909 0.209 0.309 0.309
11 0.8 0.909 0.109 0.209 0.209
12 0.8 1.000 0.200 0.200 0.200
17 0.9 1.000 0.100 0.200 0.200
18 1.0 1.000 0.000 0.100 0.100
Código
med = np.sort(Medicina)
pla = np.sort(Placebo)
Valores = np.unique(np.concatenate([med, pla]))
# ECDF evaluada en cada valor de la rejilla combinada
F_Med = np.searchsorted(med, Valores, side="right") / len(med)
F_Pla = np.searchsorted(pla, Valores, side="right") / len(pla)
D_mn_plus = np.abs(F_Med - F_Pla)
D_mn_minus = np.abs(F_Pla - np.append(np.nan, F_Med[:-1]))
D_mn_star = np.fmax(D_mn_plus, D_mn_minus)
tabla2 = pd.DataFrame({
    "Valores": Valores, "F_Medicina": F_Med, "F_Placebo": F_Pla,
    "D_mn+": D_mn_plus, "D_mn-": D_mn_minus, "D_mn*": D_mn_star,
})
print(tabla2.round(3).to_string(index=False))
 Valores  F_Medicina  F_Placebo  D_mn+  D_mn-  D_mn*
     -11         0.0      0.091  0.091    NaN  0.091
      -5         0.1      0.182  0.082  0.182  0.182
      -4         0.2      0.182  0.018  0.082  0.082
      -3         0.3      0.364  0.064  0.164  0.164
      -2         0.4      0.364  0.036  0.064  0.064
      -1         0.4      0.636  0.236  0.236  0.236
       1         0.5      0.636  0.136  0.236  0.236
       2         0.5      0.727  0.227  0.227  0.227
       3         0.5      0.818  0.318  0.318  0.318
       5         0.5      0.909  0.409  0.409  0.409
       7         0.6      0.909  0.309  0.409  0.409
      10         0.7      0.909  0.209  0.309  0.309
      11         0.8      0.909  0.109  0.209  0.209
      12         0.8      1.000  0.200  0.200  0.200
      17         0.9      1.000  0.100  0.200  0.200
      18         1.0      1.000  0.000  0.100  0.100
Código
ggplot(df) +
  geom_step(aes(Valores, F_Medicina, color = "F_Medicina"), linewidth = 2) +
  geom_step(aes(Valores, F_Placebo, color = "F_Placebo"), linewidth = 2) +
  cowplot::theme_cowplot() +
  scale_color_manual(
    values = c(
      "F_Medicina" = "#cab2d6",
      "F_Placebo" = "#b2df8a",
      "D+" = "#e31a1c",
      "D-" = "#1f78b4"
    )
  )

Código
fig, ax = plt.subplots()
ax.step(Valores, F_Med, where="post", color="#cab2d6",
        linewidth=2, label="F_Medicina")
ax.step(Valores, F_Pla, where="post", color="#b2df8a",
        linewidth=2, label="F_Placebo")
ax.set_xlabel("Valores"); ax.set_ylabel("Probabilidad acumulada"); ax.legend()
plt.show()

Código
ggplot(df) +
  geom_step(aes(Valores, F_Medicina, color = "F_Medicina"), linewidth = 2) +
  geom_point(aes(Valores, F_Medicina, color = "F_Medicina"), linewidth = 2) +
  geom_step(aes(Valores, F_Placebo, color = "F_Placebo"), linewidth = 2) +
  geom_segment(
    aes(
      x = Valores,
      xend = Valores,
      y = F_Medicina,
      yend = F_Placebo,
      color = "D+"
    ),
    linewidth = 2
  ) +
  geom_segment(
    aes(
      x = Valores,
      xend = Valores,
      y = F_Placebo,
      yend = lag(F_Medicina),
      color = "D-"
    ),
    linewidth = 2
  ) +
  cowplot::theme_cowplot() +
  scale_color_manual(
    values = c(
      "F_Medicina" = "#cab2d6",
      "F_Placebo" = "#b2df8a",
      "D+" = "#e31a1c",
      "D-" = "#1f78b4"
    )
  )

Código
fig, ax = plt.subplots()
ax.step(Valores, F_Med, where="post", color="#cab2d6",
        linewidth=2, label="F_Medicina")
ax.scatter(Valores, F_Med, color="#cab2d6")
ax.step(Valores, F_Pla, where="post", color="#b2df8a",
        linewidth=2, label="F_Placebo")
ax.vlines(Valores, F_Med, F_Pla, color="#e31a1c", linewidth=2, label="D+")
ax.vlines(Valores, F_Pla, np.append(np.nan, F_Med[:-1]),
          color="#1f78b4", linewidth=2, label="D-")
ax.set_xlabel("Valores"); ax.set_ylabel("Probabilidad acumulada"); ax.legend()
plt.show()

El estadístico \(D_{mn}^\star\) es el máximo de la última columna:

Código
(D_mn_star <- max(df$D_mn_star, na.rm = TRUE))
[1] 0.4090909
Código
D_mn_star = np.nanmax(D_mn_star)
print(f"D_mn_star = {D_mn_star:.4f}")
D_mn_star = 0.4091
Código
n <- length(Medicina)
m <- length(Placebo)
sqrt(n * m / (n + m)) * D_mn_star
[1] 0.9362817
Código
n2, m2 = len(Medicina), len(Placebo)
print(f"estadistico = {np.sqrt(n2 * m2 / (n2 + m2)) * D_mn_star:.4f}")
estadistico = 0.9363

Si elegimos un \(\alpha=0.05\), rechazamos \(H_0\)?

Código
(c_alpha <- sqrt(-log(0.05 / 2) / 2))
[1] 1.358102
Código
sqrt(n * m / (n + m)) * D_mn_star > c_alpha
[1] FALSE
Código
c_alpha = np.sqrt(-np.log(0.05 / 2) / 2)
print(f"c_alpha = {c_alpha:.4f}")
c_alpha = 1.3581
Código
print(np.sqrt(n2 * m2 / (n2 + m2)) * D_mn_star > c_alpha)
False
Código
1 - H(sqrt(n * m / (n + m)) * D_mn_star)
[1] 0.3446214
Código
print(f"valor-p (1 - H) = {1 - H(np.sqrt(n2 * m2 / (n2 + m2)) * D_mn_star):.4f}")
valor-p (1 - H) = 0.3446

La prueba se realiza con el comando ks.test:

Código
ks.test(Medicina, Placebo)

    Exact two-sample Kolmogorov-Smirnov test

data:  Medicina and Placebo
D = 0.40909, p-value = 0.2422
alternative hypothesis: two-sided
Código
print(stats.ks_2samp(Medicina, Placebo))
KstestResult(statistic=np.float64(0.4090909090909091), pvalue=np.float64(0.28689086970820715), statistic_location=np.int64(5), statistic_sign=np.int8(-1))

En este caso no rechazamos la hipótesis nula de que ambas distribuciones son iguales: el estadístico \(\left(\tfrac{mn}{m+n}\right)^{1/2}D_{mn}^{\star}\approx 0.936\) no supera el valor crítico \(c(0.05)\approx 1.358\). El valor-\(p\) asintótico (\(1-H\approx 0.345\)) y el exacto que reporta ks.test (\(p\approx 0.242\)) son ambos muy superiores a \(0.05\), de modo que solo rechazaríamos con un nivel \(\alpha\) inusualmente alto. Note que el estadístico es el mismo en ambos métodos (\(\sqrt{mn/(m+n)}\,D_{mn}^{\star}\approx 0.936\)); lo que difiere es la distribución de referencia contra la que se evalúa, y por eso el valor-\(p\) cambia:

  • El cálculo manual \(1-H\approx 0.345\) usa la distribución límite de Kolmogorov \(H(t)\), que es una aproximación asintótica válida cuando \(m,n\to\infty\).
  • ks.test usa la distribución exacta para los tamaños concretos \(m=11\), \(n=10\), contando combinatoriamente todas las formas de intercalar las dos muestras; por eso reporta \(p\approx 0.242\).

Como las muestras son pequeñas, la aproximación asintótica está lejos del valor exacto (lo sobreestima). A medida que \(m,n\) crecen, ambos valores convergen.

TipIdeas clave de esta sección
  • La versión de dos muestras compara dos distribuciones empíricas: \(D_{mn}^\star=\sup_x|F_m(x)-G_n(x)|\).
  • El estadístico se reescala con \(\sqrt{mn/(m+n)}\) y se compara con \(c(\alpha)=\sqrt{-\tfrac12\ln(\alpha/2)}\).
  • No exige normalidad ni igualdad de varianzas: detecta cualquier diferencia entre las dos distribuciones.
  • Para muestras pequeñas, ks.test usa la distribución exacta; \(1-H\) es solo la aproximación asintótica.

15.4 Resumen

La prueba de Kolmogorov-Smirnov compara distribuciones acumuladas mediante la distancia vertical máxima, sin agrupar los datos en clases. La siguiente tabla reúne los resultados principales del capítulo.

Caso Estadístico Reescalado Se rechaza \(H_0\) si Valor crítico
Una muestra \(D_n^\star=\max\{D_n^+,D_n^-\}\) \(\sqrt{n}\,D_n^\star\) \(\sqrt{n}\,D_n^\star \ge c\) \(c=H^{-1}(1-\alpha)\) (p. ej. \(1.36\) a \(\alpha=0.05\))
Dos muestras \(D_{mn}^\star=\sup_x\lvert F_m-G_n\rvert\) \(\sqrt{\tfrac{mn}{m+n}}\,D_{mn}^\star\) \(\sqrt{\tfrac{mn}{m+n}}\,D_{mn}^\star \ge c(\alpha)\) \(c(\alpha)=\sqrt{-\tfrac12\ln(\alpha/2)}\)

donde \(H(t)=1-2\sum_{i=1}^\infty(-1)^{i-1}e^{-2i^2t^2}\) es la distribución límite de Kolmogorov, común a ambos casos.

TipPara recordar
  • KS no requiere agrupar en clases: aprovecha toda la información de los datos continuos, por lo que suele superar a \(\chi^2\) en ese contexto.
  • Bajo \(H_0\), la distribución del estadístico es universal (no depende de \(F^\star\)): por eso basta una sola tabla de valores críticos.
  • En la prueba de una muestra, \(F^\star\) debe estar completamente especificada; si se estiman parámetros de la misma muestra, usar Lilliefors / KSgeneral.
  • La prueba de dos muestras es no paramétrica: detecta diferencias en forma, posición o escala sin suponer normalidad.