Pronóstico Homicidios Registrados en México

Pronóstico Homicidios
Pronóstico Homicidios

Introducción

En este trabajo se analiza la evolución temporal de los homicidios registrados en México mediante técnicas de series de tiempo, con el objetivo de comprender su dinámica y realizar pronósticos de corto plazo. A partir del análisis exploratorio, transformaciones, diferenciación y evaluación de autocorrelaciones, se identifican tendencias, estacionalidad y alta persistencia en la serie.

Permitiendo comprender, anticipar y apoyar la toma de decisiones relacionadas con la evolución de los homicidios en México. En particular:

  • Monitorear tendencias y cambios en los niveles de violencia a lo largo del tiempo.

  • Realizar pronósticos de corto plazo, útiles para planeación operativa y asignación de recursos en seguridad pública.

  • Cuantificar la incertidumbre asociada a las proyecciones.

  • Apoyar el diseño y evaluación de políticas públicas, al identificar periodos de aumento o disminución sistemática.

Descripción de los datos:

Base Delitos de 20015 a 2025

El archivo .zip contine información de los delitos cometidos en México de 2015 a 2024. Cada archivo xlsx contine los registros de delitos mensuales de enero de diciembre.

  • Año: Año de registro del delito
  • Clave_Ent: Clave de entidad
  • Entidad: Nombre de entidad federativa
  • Cve. Municipio: Clave de municipio
  • Municipio: Nombre del municipio
  • Bien jurídico afectado: Interés o derecho protegido por la ley que resulta dañado
  • Tipo de delito: Clasificación general del delito según el código penal
  • Subtipo de delito: Desagregación más específica del tipo de delito
  • Modalidad: Forma o circunstancia particular en que se cometió el delito
  • Enero: Total registrados en el mes de enero
  • Diciembre: Total registrados en el mes de diciembre

Procesamiento preliminar

import pandas as pd
import numpy as np
from datetime import date, datetime, timedelta
# web scrapping
from functools import reduce
# matplotlib
import matplotlib.pyplot as plt
import matplotlib.cm as cm
import seaborn as sns

from dbconnection import MySQLDatabase
# sklearn
from statsmodels.graphics.tsaplots import month_plot
from statsmodels.graphics.tsaplots import plot_acf
from statsmodels.graphics.tsaplots import plot_pacf
import statsmodels.tsa.stattools as sts
from statsmodels.tsa.seasonal import seasonal_decompose
from statsmodels.tsa.stattools import adfuller

import warnings
import math

from utils import selKmeans
warnings.filterwarnings("ignore", category=UserWarning, module="pandas")
sns.set_theme()

Conexión a base de datos

db = MySQLDatabase("delitos")
#anio = '2024'
tipodelito = 'Homicidio'

Realizamos la consulta para extraer los datos

query = f"""   
    SELECT 
        STR_TO_DATE(CONCAT(a.Anio, '-', a.Mes, '-01'), '%Y-%m-%d') AS fecha,
        SUM(a.Total) AS Total
    FROM Total_delitos AS a
    LEFT JOIN geo_mun AS b
        ON a.CVE_ENT = b.CVE_ENT 
    AND a.CVE_MUN = b.CVE_MUN
    WHERE a.Tipo_delito = '{tipodelito}' 
    GROUP BY a.Anio, a.Mes, a.Tipo_delito
    HAVING  SUM(a.Total)  > 0
    ORDER BY a.Anio, a.Mes;

"""
# ejecutamos consulta
base = db.execute_query(query)
## ✅ Conexión exitosa
print(f"Shape: {base.shape}")
## Shape: (129, 2)
base.head()
##         fecha Total
## 0  2015-01-01  2548
## 1  2015-02-01  2529
## 2  2015-03-01  2511
## 3  2015-04-01  2563
## 4  2015-05-01  2773

Renombramos la columna de fecha como Date y la conviertimos al tipo datetime para facilitar el análisis temporal. La variable Total la convertimos a entero, y se establece la fecha como índice del DataFrame. Además, eliminamos los últimos dos registros (posiblemente incompletos) antes de inspeccionar los primeros valores.

# renombramos
df = base.rename(columns={'fecha':'Date'})
# formato de fecha
df['Date'] = pd.to_datetime(df['Date'], format="%Y-%m-%d")
df['Total'] = df['Total'].astype('int')
# ajustamos fecha como indice
df = df.set_index('Date')
# sin considerar los últimos 
df = df.iloc[:-2,:]
df.head()
##             Total
## Date             
## 2015-01-01   2548
## 2015-02-01   2529
## 2015-03-01   2511
## 2015-04-01   2563
## 2015-05-01   2773

Lista de frecuencias a la mano.

Símbolo Descripción
B business day frequency
D calendar day frequency
W weekly frequency
M month end frequency
BM business month end frequency
MS month start frequency
BMS business month start frequency
Q quarter end frequency
BQ business quarter endfrequency
QS quarter start frequency
BQS business quarter start frequency
A year end frequency
BA business year end frequency
AS year start frequency
BAS business year start frequency
H hourly frequency
T minutely frequency
S secondly frequency
L milliseonds
U microseconds

Fuente: pandas-docs_time series

Fijamos la frecuencia

# time frequency
xt = df.asfreq(freq='MS')
xt.head()
##             Total
## Date             
## 2015-01-01   2548
## 2015-02-01   2529
## 2015-03-01   2511
## 2015-04-01   2563
## 2015-05-01   2773

División de la serie

Aantes de iniciar dividimos la serie de tiempo o DataFrame en conjuntos de entrenamiento (train) y prueba (test), usando un porcentaje definido por \(train\_size\). El conjunto de entrenamiento contiene los primeros datos (orden temporal) y el de prueba el resto, evitando mezclar información futura. Es adecuada para modelos de series de tiempo, donde no debe aplicarse un muestreo aleatorio.

def train_test_split(data, train_size=0.90):
  size = int(len(data)*train_size)
  # split data
  X_train = data.iloc[:size]
  X_test = data.iloc[size:]
  return X_train, X_test
# split
X_train, X_test = train_test_split(xt, train_size=0.85)
print('Train : ', len(X_train))
## Train :  107
print('Test : ', len(X_test))
## Test :  20
print('Total : ', len(xt))
## Total :  127

La imagen abajo muestra un crecimiento sostenido de los homicidios entre 2015 y 2018, seguido de un periodo de alta volatilidad y estancamiento entre 2019 y 2022. Al final de la serie se observa una tendencia a la baja hacia el final del periodo.

plt.figure(figsize=(12,5))
plt.plot(X_train, label='Train')
plt.plot(X_test, label = 'Test')
plt.title('Homicidios Registrados en México')
plt.xlabel('Fecha')
plt.ylabel('Total')
plt.legend()
#mplcyberpunk.add_glow_effects()
plt.show()

El gráfico abajo evidencia una estacionalidad mensual marcada: cada inicio de año los valores parten bajos y aumentan de forma sistemática dentro de cada mes. Las líneas rojas representan el promedio mensual, mostrando que marzo, mayo y diciembre concentran niveles más altos de homicidios, mientras que febrero y junio tienden a ser más bajos. En conjunto, se observa un patrón recurrente estable a lo largo de los años, lo que justifica el uso de modelos con componente estacional.

fig = month_plot(X_train.Total);
fig.set_size_inches((12, 5))
plt.show()

El gráfico de autocorrelación (ACF) muestra valores positivos y estadísticamente significativos en múltiples rezagos, lo que indica una fuerte dependencia temporal en la serie de homicidios. La disminución gradual de la autocorrelación sugiere la presencia de tendencia y persistencia, lo que puede suponer que la serie no es estacionaria.

fig = plot_acf(X_train.Total)
fig.set_size_inches((12, 4))
plt.show()

La descomposición muestra claramente que la serie de homicidios está compuesta por tendencia, estacionalidad y un componente irregular bien definidos. La tendencia crece de forma sostenida hasta alrededor de 2018–2019 y posteriormente presenta una ligera estabilización y descenso, lo que podría sugierir un cambio estructural en el nivel del fenómeno. La estacionalidad es marcada y recurrente a lo largo de los años, con variaciones mensuales sistemáticas, lo que confirma un patrón anual consistente.

dec = seasonal_decompose(xt, model="additive")

series = [dec.observed,dec.trend, dec.seasonal, dec.resid]
titulos= ['Serie observada', 'Componente de tendencia', 'Componente estacional', 'Residuo']
fig, axes = plt.subplots(2,2, sharex=False, figsize=(16, 8))
k=0
for i in range(2):
  for j in range(2):
    serie = series[k]
    axes[i, j].plot(serie)
    axes[i, j].set_title(titulos[k])
    axes[i, j].set_xlabel('Fecha')
    axes[i, j].set_ylabel('Total')
    k +=1
plt.tight_layout()
plt.show()

Prueba de estacionariedad

La hipótesis nula de la prueba de Dickey-Fuller aumentada es:

\(H_0 : θ = 0\) (significa que la serie no es estacionaria)

\(H_1 : \theta \neq 0\) (significa que la serie es estacionaria)

# Prueba de  Dickey Fuller Aumentada
def adf_test(data, alpha=0.05):
    result = adfuller(data, autolag='AIC')

    print(f'ADF Statistic : {result[0]:.4f}')
    print(f'p-value       : {result[1]:.4f}')
    print('Critical Values:')
    for key, value in result[4].items():
        print(f'   {key}: {value:.4f}')

    if result[1] < alpha:
        print('Se rechaza H0 → La serie es estacionaria')
    else:
        print('No se rechaza H0 → La serie NO es estacionaria')
adf_test(X_train.Total)
## ADF Statistic : -3.1667
## p-value       : 0.0220
## Critical Values:
##    1%: -3.5011
##    5%: -2.8925
##    10%: -2.5833
## Se rechaza H0 → La serie es estacionaria

La serie original presenta una clara tendencia creciente y alta persistencia, lo que se refleja en una ACF que decae lentamente, indicando no estacionariedad. La transformación logarítmica estabiliza la varianza, pero mantiene la dependencia temporal y la tendencia. Tras la primera diferenciación, la serie se vuelve aproximadamente estacionaria, con autocorrelaciones débiles. La segunda diferenciación con logaritmo elimina casi toda la estructura temporal, sugiriendo un proceso cercano a ruido blanco, útil para modelado ARIMA.

# Serie Original
fig, axes = plt.subplots(4,3, sharex=False, figsize=(16, 12))
axes[0, 0].plot(X_train.Total);
axes[0, 0].set_title('Serie original')
plot_acf(X_train.Total, ax=axes[0, 1]);
plot_pacf(X_train.Total, ax=axes[0, 2]);

# Logaritmo natural de la serie
axes[1, 0].plot(np.log(X_train.Total));
axes[1, 0].set_title('Logaritmo natural de la serie')
plot_acf(np.log(X_train.Total), ax=axes[1, 1]);
plot_pacf(np.log(X_train.Total), ax=axes[1, 2]);

# 1ra diferenciación
axes[2, 0].plot(X_train.Total.diff());
axes[2, 0].set_title('1ra diferenciación sin transformación')
plot_acf(X_train.Total.diff().dropna(), ax=axes[2, 1]);
plot_pacf(X_train.Total.diff().dropna(), ax=axes[2, 2]);

# 1da diferenciación con transformación
axes[3, 0].plot(np.log(X_train).diff().diff(12));
axes[3, 0].set_title('2da diferenciación con transformación logarítmica')
plot_acf(np.log(X_train.Total).diff().diff(12).dropna(), ax=axes[3, 1]);
plot_pacf(np.log(X_train.Total).diff().diff(12).dropna(), ax=axes[3, 2]);
plt.tight_layout()
plt.show()

Probamos una diferenciación en la parte regular y otra en la parte estacional.

X_train_diff =  X_train.diff().diff(12).dropna()

La gráfica abajo muestra una serie estacionaria

# gráficamos serie con una diferencia
plt.figure(figsize=(12,5))
plt.plot(X_train_diff, label='Train')
plt.title("[01/2015 - 03/2023]" )
plt.suptitle('Homicidios Registrados en México')
plt.xlabel('Fecha')
plt.ylabel('Total')
plt.legend()
plt.show()

Estimación

from statsmodels.tsa.arima_model import ARMA
from statsmodels.tsa.arima_model import ARIMA
from statsmodels.tsa.statespace.sarimax import SARIMAX
from scipy.stats.distributions import chi2

Modelo 1

El modelo \(SARIMAX(0,1,1)(0,1,1)12\) se ajusta adecuadamente a la serie Total, capturando tanto la dinámica de corto plazo como la estacionalidad anual. Los coeficientes MA(1) y MA estacional (12) son estadísticamente significativos, lo que confirma la presencia de dependencia temporal y estacional. El diagnóstico de residuos (Ljung-Box, Jarque-Bera y heterocedasticidad) indican que estos se comportan como ruido blanco, sin autocorrelación ni problemas graves de normalidad o varianza. En conjunto, el modelo es estadísticamente consistente y adecuado para pronóstico.

model = SARIMAX(X_train, order=(0,1,1), seasonal_order=(0,1, 1, 12)).fit()
print(model.summary())
##                                      SARIMAX Results                                      
## ==========================================================================================
## Dep. Variable:                              Total   No. Observations:                  107
## Model:             SARIMAX(0, 1, 1)x(0, 1, 1, 12)   Log Likelihood                -606.648
## Date:                          lun., 12 ene. 2026   AIC                           1219.297
## Time:                                    22:20:02   BIC                           1226.927
## Sample:                                01-01-2015   HQIC                          1222.379
##                                      - 11-01-2023                                         
## Covariance Type:                              opg                                         
## ==============================================================================
##                  coef    std err          z      P>|z|      [0.025      0.975]
## ------------------------------------------------------------------------------
## ma.L1         -0.5944      0.092     -6.489      0.000      -0.774      -0.415
## ma.S.L12      -0.7678      0.129     -5.958      0.000      -1.020      -0.515
## sigma2      2.096e+04   2779.778      7.540      0.000    1.55e+04    2.64e+04
## ===================================================================================
## Ljung-Box (L1) (Q):                   0.06   Jarque-Bera (JB):                 2.99
## Prob(Q):                              0.80   Prob(JB):                         0.22
## Heteroskedasticity (H):               1.06   Skew:                            -0.41
## Prob(H) (two-sided):                  0.86   Kurtosis:                         3.27
## ===================================================================================
## 
## Warnings:
## [1] Covariance matrix calculated using the outer product of gradients (complex-step).

Modelo 2

Probamos un segundo modelo \(SARIMAX(1,1,1)(1,1,1)12\), al igual que el modelo anterior los residuos no presentan autocorrelaciones ni problemas de normalidad o varianza gravez. Por otro lado, si observamos algunas métricas como el AIC, y BIC, podemos notar que el modelo anterior es más simple, es decir, posee menos parámetros, lo cual nos inclina hacia la selección del primer modelo

model2 = SARIMAX(X_train, order=(1,1,1), seasonal_order=(1,1, 1, 12)).fit()
print(model2.summary())
##                                      SARIMAX Results                                      
## ==========================================================================================
## Dep. Variable:                              Total   No. Observations:                  107
## Model:             SARIMAX(1, 1, 1)x(1, 1, 1, 12)   Log Likelihood                -606.616
## Date:                          lun., 12 ene. 2026   AIC                           1223.232
## Time:                                    22:20:03   BIC                           1235.949
## Sample:                                01-01-2015   HQIC                          1228.369
##                                      - 11-01-2023                                         
## Covariance Type:                              opg                                         
## ==============================================================================
##                  coef    std err          z      P>|z|      [0.025      0.975]
## ------------------------------------------------------------------------------
## ar.L1          0.0365      0.203      0.180      0.857      -0.361       0.434
## ma.L1         -0.6131      0.172     -3.555      0.000      -0.951      -0.275
## ar.S.L12       0.0036      0.170      0.021      0.983      -0.329       0.336
## ma.S.L12      -0.7665      0.202     -3.797      0.000      -1.162      -0.371
## sigma2      2.098e+04   3081.211      6.807      0.000    1.49e+04     2.7e+04
## ===================================================================================
## Ljung-Box (L1) (Q):                   0.16   Jarque-Bera (JB):                 2.84
## Prob(Q):                              0.69   Prob(JB):                         0.24
## Heteroskedasticity (H):               1.07   Skew:                            -0.41
## Prob(H) (two-sided):                  0.85   Kurtosis:                         3.22
## ===================================================================================
## 
## Warnings:
## [1] Covariance matrix calculated using the outer product of gradients (complex-step).

Evaluación del modelo

Una vez estimado el modelo procedemos a validar que cumpla con los supuestos con respecto a los residuales: - Que se distribuyan de forma normal. - Que sean independientes. - Que no estén correlacionados. - Que tengan media cero y varianza constante.

Prueba de autocorrelación Ljung-Box Test

A veces es importante probar si los valores de una serie temporal, digamos \(u_t\), son independientes. Podemos decir que existe un patrón de autocorrelación si las series están seriamente correlacionadas. Una prueba muy utilizada para verificar si una serie de valores está correlacionada es el estadístico de Ljung Box, que se basa en la autocorrelación como conjunto. Supongamos que nos dan \(m\) valores autocorrelacionados. Probamos la siguiente hipótesis nula conjunta sobre las correlaciones entre los valores

\[ \begin{equation} H_0 : \rho_1 = ... = \rho_k = 0 \end{equation} \] el estadístico \[ \begin{equation} Q = n(n+2) \sum_{k=1}^{m} \frac{\hat{\rho}_k^2}{(n-k)} \end{equation} \] donde \(m\) es el número de rezagos (lags), and \(\hat{\rho}_k^2\) representa \(\hat{Cov} (u_t, u_{t+k})\) for \(k=1,...,m\).

Referencia: Pankratz(\(1983\)).

Prueba de normalidad Jarque-Bera

La prueba de normalidad Jarque-Bera es una prueba estadística que se emplea para ver si una determinada serie de tiempo se distribuye de forma normal.

El estadístico de prueba Jarque-Bera se define como \[ \begin{equation*} JB =[(n-k+1) / 6] * [S^2 + (0,25*(C-3)2)] \end{equation*} \]

Bajo la hipótesis nula de normalidad, la prueba Jarque-Bera(JB) \(\sim \chi^2(2)\)

donde \(n\) denota el número de observaciones en la muestra, \(k\) denota el número de regresores (\(k=1\) si no se utiliza en una regresión), \(S\) denota la asimetría de la muestra y \(C\) denota la curtosis de la muestra.

La hipótesis nula es \[ \begin{equation} H_0 : \text{La serie sigue una distribución normal} \end{equation} \]

Prueba de heterocedasticidad Goldfeld-Quandt

La heterocedasticidad en la situación en la que la variabilidad de una serie es desigual para un rango de valores distinto.

La prueba Goldfeld-Quandt se utiliza para probar la presencia de heteroscedasticidad en los datos dados.

La hipótesis nula es \[ \begin{equation} H_0 : \text{No hay presencia de heterocedasticidad} \end{equation} \]

Observación:

Una prueba de hipótesis se rechaza mediante: - El estadístico de prueba. - El p-valor. Se rechaza si p-valor < \(\alpha\)

En el gr+afico abajo, se observa que los residuos estandarizados oscilan alrededor de cero sin patrón visible, lo que indica una buena especificación del modelo. El histograma muestra una distribución cercana a la normal, con ligeras desviaciones en las colas.El Q-Q plot confirma una normalidad razonable, con pequeños desajustes en los extremos. Por otra parte, el correlograma no presenta autocorrelaciones significativas, sugiriendo residuos tipo ruido blanco y un ajuste adecuado del modelo.

from statsmodels.tsa.statespace.sarimax import SARIMAXResults
model.plot_diagnostics(figsize=(14,6))
plt.tight_layout()
plt.show()

Pronóstico sobre el conjunto de prueba

Vamos a realizar el pronóstico del modelo sobre conjunto de prueba (test)

# Predicciones del conjunto de entrenamiento (train)
pred = model.get_prediction(step=1)
# obtenemos los intervalos de confianza
conficence_int = pred.conf_int()
predicted = pred.prediction_results
# Predicción del conjunto de prueba (test)
n = len(X_test)
forec = model.get_forecast(n)
# aplicamos el inverso del logaritmo
conficence_int = (forec.conf_int())
forecast = (forec.predicted_mean)
df_pred = pd.concat([forecast, X_test], axis=1)
df_pred.head()
##             predicted_mean  Total
## 2023-12-01     3580.353429   3575
## 2024-01-01     3413.323184   3425
## 2024-02-01     3185.822116   3310
## 2024-03-01     3585.559005   3645
## 2024-04-01     3473.464448   3747
# gráficamos serie de entrenamiento y serie de prueba
plt.figure(figsize=(14,5))
plt.plot(X_train, label='Train')
plt.plot(X_test, label = 'Test')
plt.plot(forecast, label = 'ARIMA Predicted')
plt.fill_between(forecast.index, conficence_int.iloc[:, 0], conficence_int.iloc[:, 1], color='grey', alpha=.2)
#plt.fill_between(forecast.index, color='b', alpha=.2)
plt.title("[01/2015 - 03/2023]" )
plt.suptitle('Homicidios Registrados en México')
plt.xlabel('Fecha')
plt.ylabel('Total')
plt.legend(loc='upper left')
plt.show()

Confirmación

Por mera curiosidad vamos a corroborar el modelo mediante auto_arima

import pmdarima as pm

model = pm.auto_arima(X_train, start_p=1, start_q=1,
                      test='adf',       #adftest para encontrar el valor óptimo de 'd'
                      max_p=5, max_q=5, # máx p y q
                      m=12,              # frecuencia de la serie
                      d=1,              # diferenciación de la parte regular (no estacional)
                      seasonal=True,    #
                      start_P=0, start_Q=0,  # valores de inicio de P y Q de la parte estacional
                      D=1,                   # Diferenciación de la parte estacional
                      suppress_warnings=True,
                      stepwise=True)

print(model.summary())
##                                      SARIMAX Results                                      
## ==========================================================================================
## Dep. Variable:                                  y   No. Observations:                  107
## Model:             SARIMAX(0, 1, 1)x(0, 1, 1, 12)   Log Likelihood                -606.648
## Date:                          lun., 12 ene. 2026   AIC                           1219.297
## Time:                                    22:20:14   BIC                           1226.927
## Sample:                                01-01-2015   HQIC                          1222.379
##                                      - 11-01-2023                                         
## Covariance Type:                              opg                                         
## ==============================================================================
##                  coef    std err          z      P>|z|      [0.025      0.975]
## ------------------------------------------------------------------------------
## ma.L1         -0.5944      0.092     -6.489      0.000      -0.774      -0.415
## ma.S.L12      -0.7678      0.129     -5.958      0.000      -1.020      -0.515
## sigma2      2.096e+04   2779.778      7.540      0.000    1.55e+04    2.64e+04
## ===================================================================================
## Ljung-Box (L1) (Q):                   0.06   Jarque-Bera (JB):                 2.99
## Prob(Q):                              0.80   Prob(JB):                         0.22
## Heteroskedasticity (H):               1.06   Skew:                            -0.41
## Prob(H) (two-sided):                  0.86   Kurtosis:                         3.27
## ===================================================================================
## 
## Warnings:
## [1] Covariance matrix calculated using the outer product of gradients (complex-step).

Pronóstico final

Finalmente, procedemos a obtener pronóstico sobre todo el conjunto de datos.

model = SARIMAX(xt, order=(0,1,1), seasonal_order=(0,1, 1, 12)).fit()
print(model.summary())
##                                      SARIMAX Results                                      
## ==========================================================================================
## Dep. Variable:                              Total   No. Observations:                  127
## Model:             SARIMAX(0, 1, 1)x(0, 1, 1, 12)   Log Likelihood                -736.245
## Date:                          lun., 12 ene. 2026   AIC                           1478.491
## Time:                                    22:20:15   BIC                           1486.699
## Sample:                                01-01-2015   HQIC                          1481.822
##                                      - 07-01-2025                                         
## Covariance Type:                              opg                                         
## ==============================================================================
##                  coef    std err          z      P>|z|      [0.025      0.975]
## ------------------------------------------------------------------------------
## ma.L1         -0.4893      0.090     -5.442      0.000      -0.666      -0.313
## ma.S.L12      -0.8117      0.123     -6.588      0.000      -1.053      -0.570
## sigma2       2.12e+04   2812.547      7.539      0.000    1.57e+04    2.67e+04
## ===================================================================================
## Ljung-Box (L1) (Q):                   0.01   Jarque-Bera (JB):                 2.74
## Prob(Q):                              0.94   Prob(JB):                         0.25
## Heteroskedasticity (H):               1.07   Skew:                            -0.38
## Prob(H) (two-sided):                  0.83   Kurtosis:                         3.03
## ===================================================================================
## 
## Warnings:
## [1] Covariance matrix calculated using the outer product of gradients (complex-step).

Para ello, vamos a considerar un pronóstico de 8 meses hacia adelante.

# Predicción del conjunto de prueba (test)
n = 8
forec = model.get_forecast(n)
# aplicamos el inverso del logaritmo
conficence_int = (forec.conf_int())
forecast = (forec.predicted_mean)
# Predicción del conjunto de prueba (test)
n = 8
forec = model.get_forecast(n)
# aplicamos el inverso del logaritmo
conficence_int = (forec.conf_int())
forecast = (forec.predicted_mean)

La serie histórica de homicidios muestra un crecimiento sostenido en el periodo 2018–2019, seguido de una estabilización con alta volatilidad y una tendencia ligeramente decreciente en los últimos años. El pronóstico sugiere niveles relativamente estables en el corto plazo, con una leve disminución en los últimos meses. En conjunto, el modelo capta bien la dinámica reciente, aunque el contexto de alta variabilidad limita la precisión de largo plazo.

# gráficamos serie de entrenamiento y serie de prueba
plt.figure(figsize=(14,5))
plt.plot(xt, label='Real')
plt.plot(forecast, label = 'Pronóstico')
plt.fill_between(forecast.index, conficence_int.iloc[:, 0], conficence_int.iloc[:, 1], color='grey', alpha=.2)
#plt.fill_between(forecast.index, color='b', alpha=.2)
plt.title("[01/2015 - 03/2026]" )
plt.suptitle('Homicidios Registrado en México')
plt.xlabel('Fecha')
plt.ylabel('Total')
plt.show()

Modelo final

El modelo \(ARIMA(0,1,1)(0,1,1)_{12}\), y se expresa como: \[ (1 - \phi_1 B)(1 - \Phi_1 B^{12}) z_t = (1 - \theta_1 B)(1 - \Theta_1 B^{12}) u_t \]

donde:

  • \((1−B)\): diferencia regular (d=1)
  • \((1−B^{12})\): diferencia estacional \((D=1, s=12)\)
  • \((1−\theta_1 B)\): \(MA(1)\) no estacional
  • \((1−\Theta_1 B)\): \(MA(1)\) estacional

en forma expandida \[ (1 - \phi_1 B)(1 - \Phi_1 B^{12}) z_t = (1 - \theta_1 B)(1 - \Theta_1 B^{12}) u_t \]

\[ z_t−z_{t−1} −z_{t−12}+z_{t−13} = (1−\theta_1 B−\Theta_1B^{12}+\theta_1 \Theta_1 B^{13})u_t \]

\[ z_t−z_{t−1}−z_{t−12}+z_{t−13} = u_t −\theta_1 u_{t−1}− \Theta_1 u_{t−12} + \theta_1 \Theta_1 u_{t−13} \]

\[ \begin{equation*} z_t = z_{t−1} + z_{t−12} - z_{t−13} + u_t −\theta_1 u_{t−1}− \Theta_1 u_{t−12} + \theta_1 \Theta_1 u_{t−13} \end{equation*} \]

Comentario final

En general, el análisis muestra que la serie de homicidios en México presenta una dinámica compleja, con tendencia, estacionalidad y alta volatilidad, reflejando cambios y choques recurrentes a lo largo del tiempo. Esto hace indispensable aplicar transformaciones y diferenciaciones para lograr estacionariedad antes del modelado.

El modelo \(SARIMAX(0,1,1)(0,1,1)12\) resulta estadísticamente adecuado, ya que captura correctamente la dependencia temporal y la estacionalidad. Los diagnósticos indican residuos cercanos a ruido blanco, sin autocorrelación significativa ni violaciones graves a los supuestos de normalidad y homocedasticidad.

El modelo ofrece pronósticos razonables de corto plazo, mostrando estabilidad con ligera tendencia a la baja, aunque con incertidumbre creciente conforme aumenta el horizonte de predicción. En conclusión, el modelo es útil para análisis y proyección a corto plazo, pero debe complementarse con información estructural o modelos más complejos si se buscan interpretaciones causales o pronósticos de largo plazo.

Referencias

  • Pankratz Alan (1983). Forecasting With Univariate Box- Jenkins Models: concepts and cases. John Wiley & Sons
  • Peña Daniel (2010), Análisis de series temporales. Alianza Editorial