Qué contiene este artículo

Este ejemplo sigue una muestra con semilla fija desde la preparación hasta la evaluación del modelo. Los resultados se comprobaron el 9 de septiembre de 2026 con Python 3.12.7, Phitter 1.0.4, NumPy 2.4.6, SciPy 1.17.0 y pandas 3.0.5. Las tablas redondean la salida; los optimizadores y las versiones pueden cambiar algunos ajustes o su orden.

El ejemplo está construido de forma deliberada para que la respuesta correcta se conozca de antemano. Eso permite comprobar no solo si el ajuste funciona, sino si el ranking que produce significa lo que sugiere una primera lectura.

Paso 1 — el conjunto de datos

La muestra representa tiempos de reparación: 500 observaciones, estrictamente positivas y con sesgo a la derecha. Proviene de una distribución lognormal con mean=1.2 y sigma=0.55, de modo que la respuesta verdadera es conocida.

import numpy as np

rng = np.random.default_rng(42)
data = rng.lognormal(mean=1.2, sigma=0.55, size=500)
np.savetxt("repair_times.csv", data, fmt="%.17g",
           header="repair_time_hours", comments="")

El archivo repair_times.csv resultante es una sola columna con encabezado, que es el formato que Phitter Web acepta directamente.

Paso 2 — mirar antes de ajustar

Medida Valor
n 500
Media 3.7963
Desviación estándar muestral 2.2013
Mínimo 0.8092
Mediana 3.3260
Máximo 16.4879
Asimetría (corregida por sesgo) 1.9300

La muestra positiva y asimétrica sugiere examinar familias positivas con cola derecha. Una muestra positiva no demuestra por sí sola que la población excluya valores negativos: la interpretación como tiempos de reparación aporta esa restricción. Comprueba cuánta probabilidad asigna cada candidata a duraciones imposibles y si una aproximación resulta admisible para el uso previsto.

Paso 3 — el ajuste

import phitter

phi = phitter.Phitter(data=data, fit_type="continuous",
                      num_bins=20, confidence_level=0.95)
phi.fit(n_workers=1)

Una nota práctica para quien trabaje en Windows: un n_workers mayor que 1 abre un pool de procesos, y en Windows los procesos hijos vuelven a importar el módulo que los llamó. Si el ajuste se ejecuta a nivel de módulo, el pool muere con BrokenProcessPool. Conviene envolver la llamada en if __name__ == "__main__": — o usar n_workers=1.

Phitter ajustó 63 distribuciones continuas a esta muestra.

Paso 4 — el ranking

Estos son los primeros siete modelos del ranking reproducido, con columnas seleccionadas y redondeadas. Las dos filas próximas a BIC 1993.08 son numéricamente muy cercanas; su orden puede cambiar por detalles del cálculo.

Distribución AIC BIC KS KS p AD AD p χ² p
inverse_gaussian 1976.32 1984.75 0.0352 0.5541 0.5356 0.7107 0.0832
lognormal 1976.43 1984.86 0.0305 0.7273 0.3296 0.9143 0.1678
inverse_gamma_3p 1978.62 1991.27 0.0281 0.8149 0.3105 0.9301 0.3054
beta_prime 1983.41 1991.84 0.0418 0.3373 1.3122 0.2284 0.0619
frechet 1980.44 1993.08 0.0299 0.7527 0.3453 0.9006 0.3309
generalized_extreme_value 1980.44 1993.08 0.0299 0.7524 0.3453 0.9006 0.3308
burr_4p 1976.44 1993.30 0.0266 0.8612 0.2124 0.9866 0.1856

Los datos se generaron a partir de una lognormal. La lognormal quedó segunda.

El primer modelo es una aproximación plausible; el segundo pertenece a la familia generadora conocida. La diferencia de AIC es de aproximadamente 0.11. Se interpreta esa diferencia absoluta, no su porcentaje sobre el valor de AIC: una brecha tan pequeña ofrece poca separación bajo ese criterio. La lognormal también obtiene menores estadísticos KS, AD y chi-cuadrado en esta ejecución.

Phitter 1.0.4 prioriza el número de pruebas que no rechazan cada modelo. En ajustes continuos desempata por BIC, después AIC y después SSE. Los criterios de información y los estadísticos de bondad de ajuste miden aspectos diferentes. Interpretar un ranking explica cómo utilizar ambos.

Paso 5 — revisar los parámetros

phi.sorted_distributions["lognormal"]["parameters"]
# {'mu': 1.1891, 'sigma': 0.5384}

Los parámetros generadores fueron mu=1.2 y sigma=0.55. La estimación los recuperó con un error cercano al 1% y al 2%. Esta es la comprobación más importante y la que más se omite: un modelo cuyos parámetros resultan inverosímiles para el proceso que se describe no se salva por tener una buena puntuación.

Paso 6 — revisar los diagnósticos, no solo los números

De los 63 modelos ajustados, 20 no fueron rechazados por al menos una prueba y 11 no fueron rechazados por ninguna de las tres, al nivel nominal de significación del 5%. Son conteos diferentes: df_not_rejected_distributions incluye modelos con al menos una prueba no rechazada. Ningún conteo establece que esos modelos sean correctos; la calibración tras estimar parámetros exige atención adicional.

Los gráficos hacen un trabajo que los estadísticos no pueden hacer:

phi.plot_histogram_distributions()   # concordancia de forma en el cuerpo
phi.qq_plot_regression("lognormal")             # dónde se desvía el modelo y en qué dirección
phi.plot_ecdf_distribution("lognormal")         # concordancia acumulada

El gráfico Q–Q es el que conviene leer con atención. Muestra dónde falla un modelo, algo que los estadísticos de un solo número no pueden indicar.

Qué conviene registrar

Para que un ajuste sea reproducible seis meses después hay que conservar: la versión de Phitter, la muestra original o la semilla que la genera, fit_type, num_bins, confidence_level, la lista de candidatas si se restringió, y la tabla completa del ranking en lugar de solo la ganadora. num_bins merece atención especial: modifica el resultado de chi-cuadrado, y chi-cuadrado es la única de las tres pruebas que depende de él.

El script completo

import numpy as np
import phitter


def main():
    rng = np.random.default_rng(42)
    data = rng.lognormal(mean=1.2, sigma=0.55, size=500)
    np.savetxt("repair_times.csv", data, fmt="%.17g",
               header="repair_time_hours", comments="")

    phi = phitter.Phitter(data=data, fit_type="continuous",
                          num_bins=20, confidence_level=0.95)
    phi.fit(n_workers=1)

    print(phi.best_distribution)
    print(phi.df_sorted_distributions.head(10))
    tests = ["chi_square", "kolmogorov_smirnov", "anderson_darling"]
    all_three = sum(
        all(result[test]["rejected"] == False for test in tests)
        for result in phi.sorted_distributions.values()
    )
    print(len(phi.df_not_rejected_distributions), "al menos una prueba no rechazada")
    print(all_three, "ninguna de las tres pruebas rechazada")


if __name__ == "__main__":
    main()

Para explorar la muestra sin instalar Python, importa el CSV en Phitter Web y configura 20 intervalos y un nivel de confianza del 95%. El kernel del navegador y las opciones de estimación pueden diferir de este entorno de Python; no debe suponerse una salida numérica idéntica.

Fuentes primarias