Incidencia delictiva en México
Introducción
Incidencia delictiva es un indicador clave para comprender la magnitud, evolución y distribución de los delitos en una sociedad, así como el impacto sociale, económico y político. Su análisis permite identificar patrones territoriales, tipologías delictivas predominantes y grupos de población más vulnerables, facilitando la formulación de políticas públicas orientadas a la prevención y al fortalecimiento de la seguridad. A nivel global, la delincuencia se manifiesta de manera heterogénea, influida por factores estructurales como la desigualdad, el crecimiento urbano, la impunidad y la debilidad institucional. En el caso de México, la incidencia delictiva representa uno de los principales retos nacionales, caracterizada por la persistencia de delitos de alto impacto como homicidio, robo, extorsión y violencia familiar. La diversidad regional del país genera contrastes significativos entre entidades federativas, reflejando dinámicas locales complejas.
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
Los datos se procesaron y cargaron en SQL, para posterior análisis. Por otro lado, se cruzó con información del marco geoestadístico de INEGI la cual también se cargó en SQL.
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
import networkx as nx
from ipysigma import Sigma
from pyvis.network import Network
import requests
from bs4 import BeautifulSoup
from io import StringIO
from dbconnection import MySQLDatabase
# sklearn
from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import KernelPCA
from sklearn.cluster import KMeans
from sklearn.metrics import silhouette_score
from scipy.spatial import ConvexHull
import warnings
import math
from utils import selKmeans
warnings.filterwarnings("ignore", category=UserWarning, module="pandas")
sns.set_theme()Conexión a base de datos
Ejecutamos consulta para el año seleccionado
query = f"""
SELECT
a.id,
a.Anio,
a.Mes,
a.CVE_ENT,
b.NOM_ENT,
a.CVE_MUN,
b.NOM_MUN,
b.POB_TOTAL,
a.Tipo_delito,
a.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.Anio = {anio};
"""
# ejecutamos consulta
df = db.execute_query(query)## ✅ Conexión exitosa
## Shape: (2923536, 10)
## id Anio Mes ... POB_TOTAL Tipo_delito Total
## 0 24908857 2024 04 ... 948990 Aborto 0
## 1 24908858 2024 04 ... 948990 Abuso de confianza 57
## 2 24908859 2024 04 ... 948990 Abuso sexual 0
## 3 24908860 2024 04 ... 948990 Acoso sexual 0
## 4 24908861 2024 04 ... 948990 Allanamiento de morada 34
##
## [5 rows x 10 columns]
El siguiente código convierte la población total a formato numérico, manejando valores inválidos como nulos. Posteriormente, agrupa los registros por entidad federativa y tipo de delito, sumando el total de delitos para obtener cifras consolidadas. Finalmente, reorganiza los datos en formato long, dejando cada tipo de delito como una columna por entidad, lo que facilita el análisis comparativo entre entidades.
# Convertir POB_TOTAL a numérico
df['POB_TOTAL'] = pd.to_numeric(df['POB_TOTAL'], errors='coerce')
# Agrupar por entidad y tipo de delito, sumar los delitos
df_ent = df.groupby(['CVE_ENT', 'NOM_ENT', 'Tipo_delito'], as_index=False).agg({'Total':'sum'})
# Pivotear: tipos de delito a columnas
df_pivot = df_ent.pivot_table(
index=['CVE_ENT', 'NOM_ENT'], # Solo estos índices, NO POB_TOTAL
columns='Tipo_delito',
values='Total',
aggfunc='sum',
fill_value=0
).reset_index()
# Unir la población al pivot
print(df_pivot['CVE_ENT'].nunique())## 32
Escalado de los datos
El siguiente código elimina las columnas identificadoras, conservando únicamente las variables numéricas para el análisis. Posteriormente, estandarizamos los datos mediante StandardScaler(), transformándolos a media cero y desviación estándar uno. Esto asegura que todas las variables tengan la misma escala antes de aplicar métodos de reducción de dimensión o clustering.
Kernel PCA
Aplicamos Kernel PCA para reducir la dimensionalidad de los datos estandarizados a dos componentes, capturando relaciones no lineales mediante un kernel RBF. A continuación, se extraen las dos componentes principales obtenidas y las incorpora al DataFrame original. Esto permite visualizar y analizar las entidades en un espacio bidimensional.
# kernel PCA
# se pueden probar distintos kernels: 'rbf', 'poly', 'sigmoid', 'cosine'
kpca = KernelPCA(n_components=2, kernel='rbf', gamma=0.05, random_state=42)
X_kpca = kpca.fit_transform(X_scaled)
# Guardamos en el DataFrame para graficar
df_pivot['kpca1'] = X_kpca[:, 0]
df_pivot['kpca2'] = X_kpca[:, 1]Y graficamos
n_points = X_kpca.shape[0]
colors = cm.tab20(np.linspace(0, 1, n_points))
plt.figure(figsize=(14, 6))
for i in range(n_points):
plt.scatter(X_kpca[i, 0], X_kpca[i, 1], color=colors[i], s=80)
plt.text(X_kpca[i, 0] + 0.02, X_kpca[i, 1] + 0.02, df_pivot['NOM_ENT'][i], fontsize=9, alpha=0.85)
plt.xlabel('Kernel PCA 1', fontsize=13)
plt.ylabel('Kernel PCA 2', fontsize=13)
plt.title('Kernel PCA: PC1 vs PC2 (Entidades)', fontsize=15)
plt.grid(True, linestyle='--', alpha=0.4)
plt.tight_layout()
plt.show()Clustering
Este código evalúa distintos números de clusters aplicando K-means sobre las componentes obtenidas con Kernel PCA, utilizando el coeficiente de Silhouette como criterio de selección. Posteriormente, identifica el número óptimo de clusters y lo imprime. Finalmente, genera los gráficos del codo y de Silhouette, que permiten validar visualmente la calidad de la partición y la separación entre clusters.
# selecciona mejor kmeans
class selKmeans():
def __init__(self, X, krange=[2,20]):
self.X = X
self.kmin = krange[0]
self.kmax = krange[1]
self.inertias = []
self.silhouettes = []
# metodo retorna mejor k
def best(self):
k_range = range(self.kmin, self.kmax)
self.inertias.clear()
self.silhouettes.clear()
for k in k_range:
kmeans = KMeans(n_clusters=k, random_state=42)
labels = kmeans.fit_predict(self.X)
self.inertias.append(kmeans.inertia_)
self.silhouettes.append(silhouette_score(self.X, labels))
# k best con
best_k = k_range[np.argmax(self.silhouettes)]
return best_k
# Gráfica Método del Codo
def plot_elbow(self):
self.best()
k_range = range(self.kmin, self.kmax)
plt.figure(figsize=(12,4))
plt.plot(k_range, self.inertias, 'o-', label='Inertia (codo)')
plt.xlabel('Número de clusters (k)')
plt.ylabel('Inercia')
plt.title('Método del Codo')
plt.grid(True, linestyle='--', alpha=0.4)
return plt
def plot_silhouette(self):
self.best()
k_range = range(self.kmin, self.kmax)
# --- Silhouette Score
plt.figure(figsize=(12,4))
plt.plot(k_range, self.silhouettes, 'o-', color='orange', label='Silhouette')
plt.xlabel('Número de clusters (k)')
plt.ylabel('Silhouette Score')
plt.title('Evaluación del número óptimo de clusters')
plt.grid(True, linestyle='--', alpha=0.4)
return pltAplicamos la función
sel = selKmeans(X_kpca, krange=[2,20])
best_k = sel.best()
print(f" Mejor número de clusters según Silhouette: {best_k}")## Mejor número de clusters según Silhouette: 7
Visualizamos los clusters en el espacio reducido por Kernel PCA, coloreando cada grupo y delimitándolo mediante su envolvente convexa para resaltar su estructura. Finalmente, etiqueta cada punto con el nombre de la entidad, facilitando la interpretación espacial y comparativa de los patrones delictivos.
# Clustering con el mejor K
kmeans_final = KMeans(n_clusters=best_k, random_state=42)
clusters = kmeans_final.fit_predict(X_kpca)
df_pivot['Cluster'] = clusters
#print(df_pivot['CVE_ENT'].nunique())
plt.figure(figsize=(14, 6))
colors = plt.cm.tab10(np.linspace(0, 1, best_k)) # paleta de colores
for k, color in zip(range(best_k), colors):
grupo = df_pivot[df_pivot['Cluster'] == k]
plt.scatter(grupo['kpca1'], grupo['kpca2'], color=color, s=40, label=f'Cluster {k}')
# Calcular y dibujar el polígono si hay suficientes puntos
if len(grupo) >= 3:
puntos = grupo[['kpca1', 'kpca2']].values
hull = ConvexHull(puntos)
for simplex in hull.simplices:
plt.plot(puntos[simplex, 0], puntos[simplex, 1], color=color, linewidth=1.5)
# sombrear el interior
plt.fill(puntos[hull.vertices, 0], puntos[hull.vertices, 1],
color=color, alpha=0.15)
# Etiquetas de texto (se puede usar NOM_ENT o NOM_MUN)
for i in range(len(df_pivot)):
plt.annotate(df_pivot['NOM_ENT'][i], (df_pivot['kpca1'][i]+0.01, df_pivot['kpca2'][i]+0.01), fontsize=8, alpha=0.8)
plt.xlabel("Componente Principal 1")
plt.ylabel("Componente Principal 2")
plt.title(f"Kernel PCA + KMeans (k={best_k}) por Tipo de Delito [{anio}]")
plt.grid(True, linestyle='--', alpha=0.4)
plt.show()Imprimimos el identificador del cluster junto con la lista de entidades que lo conforman.
# Agrupar por cluster y obtener la lista de entidades únicas por cluster
entidades_por_cluster = df_pivot.groupby('Cluster')['NOM_ENT'].unique()
# Imprime
for c, entidades in entidades_por_cluster.items():
print(f"\n🔹 Cluster {c} ({len(entidades)} entidades):")
print(entidades)##
## 🔹 Cluster 0 (7 entidades):
## ['Aguascalientes' 'Baja California Sur' 'Colima' 'Chiapas' 'Guerrero'
## 'Sinaloa' 'Zacatecas']
##
## 🔹 Cluster 1 (3 entidades):
## ['Jalisco' 'Morelos' 'Quintana Roo']
##
## 🔹 Cluster 2 (5 entidades):
## ['Campeche' 'Durango' 'Nayarit' 'Tlaxcala' 'Yucatán']
##
## 🔹 Cluster 3 (6 entidades):
## ['Hidalgo' 'Oaxaca' 'San Luis Potosí' 'Sonora' 'Tabasco' 'Tamaulipas']
##
## 🔹 Cluster 4 (3 entidades):
## ['Chihuahua' 'Puebla' 'Veracruz de Ignacio de la Llave']
##
## 🔹 Cluster 5 (3 entidades):
## ['Coahuila de Zaragoza' 'Michoacán de Ocampo' 'Querétaro']
##
## 🔹 Cluster 6 (5 entidades):
## ['Baja California' 'Ciudad de México' 'Guanajuato' 'México' 'Nuevo León']
Graficamos para ver qué entidades pertenecen a cada cluster
# definicmos colores
colors = ['skyblue', 'lightgreen', 'salmon', 'green', 'cyan', 'magenta','lightblue']
# graficamos
plt.figure(figsize=(14, 6))
for c, entidades in entidades_por_cluster.items():
# posiciones en y
y_pos = range(len(entidades))
# scatter: x = cluster, y = entidad
plt.scatter([c]*len(entidades), y_pos, s=200, color=colors[c], label=f'Cluster {c}')
# Añadir etiquetas de entidad
for i, ent in enumerate(entidades):
plt.text(c + 0.05, i, ent, va='center', fontsize=10)
plt.xticks([0, 1, 2,3,4,5,6], [f"Cluster {x}" for x in range(7)])## ([<matplotlib.axis.XTick object at 0x000002533ED137A0>, <matplotlib.axis.XTick object at 0x000002534186CC50>, <matplotlib.axis.XTick object at 0x000002533EC705F0>, <matplotlib.axis.XTick object at 0x0000025341A7FF50>, <matplotlib.axis.XTick object at 0x0000025341A7F920>, <matplotlib.axis.XTick object at 0x0000025341A7E4E0>, <matplotlib.axis.XTick object at 0x0000025344BF8C50>], [Text(0, 0, 'Cluster 0'), Text(1, 0, 'Cluster 1'), Text(2, 0, 'Cluster 2'), Text(3, 0, 'Cluster 3'), Text(4, 0, 'Cluster 4'), Text(5, 0, 'Cluster 5'), Text(6, 0, 'Cluster 6')])
## ([], [])
plt.xlabel("Clusters")
plt.title("Distribución de Entidades por Cluster")
plt.legend()
plt.tight_layout()
plt.show()Perfil
En clustering, profile (perfil) se refiere a la descripción resumida de las características típicas de cada cluster.Es decir, una vez formados los clusters, el perfil indica cómo es “en promedio” cada grupo, usando estadísticas como medias, medianas o distribuciones de las variables, para poder interpretar y comparar los clusters entre sí. Vemos primero los promedios de cada delito por cluster.
# Columnas de delitos
delitos_cols = df_pivot.columns[2:-3]
# Promedio de cada delito por cluster
cluster_summary = df_pivot.groupby('Cluster')[delitos_cols].mean().round(2)
print(cluster_summary)## Tipo_delito Aborto ... Violencia familiar
## Cluster ...
## 0 5.14 ... 3648.14
## 1 12.33 ... 9166.33
## 2 0.40 ... 1806.40
## 3 18.50 ... 7672.83
## 4 8.00 ... 11989.33
## 5 14.00 ... 6674.67
## 6 90.00 ... 22673.00
##
## [7 rows x 40 columns]
Finalmente, visualizamos los clusters y sus perfiles delictivos, facilitando la interpretación comparativa y el análisis exploratorio.
# Número de clusters
n_clusters = len(cluster_summary.index)
# Definir número de filas y columnas
n_cols = 2
n_rows = math.ceil(n_clusters / n_cols)
# Crear figura
fig, axes = plt.subplots(n_rows, n_cols, figsize=(8 * n_cols, 8 * n_rows))
axes = axes.flatten() # convertir a lista 1D
# Graficar cada cluster
for i, c in enumerate(cluster_summary.index):
ax = axes[i]
# Top 10 variables del cluster
top10 = cluster_summary.loc[c].sort_values(ascending=False).head(10)
delitos = [x.split('_')[0] for x in top10.index[::-1]]
valores = top10.values[::-1]
# Colores distintos por barra
colors = cm.tab10(np.linspace(0, 1, len(valores)))
# Crear barras
bars = ax.barh(delitos, valores, color=colors)
# Agregar texto al final de cada barra
for bar in bars:
width = bar.get_width()
ax.text(
width + max(valores) * 0.01,
bar.get_y() + bar.get_height() / 2,
f'{width:.1f}',
va='center',
ha='left',
fontsize=12,
color='black'
)
ax.set_title(f'Cluster {c}', fontsize=16, weight='bold')
ax.set_xlabel('Cantidad promedio', fontsize=13)
ax.tick_params(axis='y', labelsize=13)
# Eliminar ejes vacíos (si hay más espacios que clusters)
for j in range(i + 1, len(axes)):
fig.delaxes(axes[j])
plt.tight_layout()
plt.show()Comentario final
A partir de todo el proceso (Kernel PCA + K-means) y del gráfico de perfiles por cluster, se pueden extraer las siguientes conclusiones generales:
Existen patrones delictivos claramente diferenciados entre grupos de entidades, no solo en volumen total, sino en la composición del tipo de delitos. Esto confirma que la incidencia delictiva en México no es homogénea y presenta dinámicas regionales distintas.
El robo y la violencia familiar dominan en todos los clusters, pero con intensidades muy diferentes. Algunos clusters (por ejemplo, los de valores más altos) concentran entidades con alta densidad delictiva y multidelito, mientras que otros reflejan incidencias más moderadas.
Los clusters con mayores promedios muestran también diversificación delictiva (fraude, narcomenudeo, amenazas, otros delitos del fuero común), lo que sugiere una estructura compleja.
En conjunto, el análisis evidencia que el clustering permite segmentar entidades con problemáticas similares, lo cual es clave para un primer análisis exploratorio, y a partir de ahí focalizar esfuerzos, con un mayor análisis, en a fin de generar estrategias de prevención.
Referencias:
- T. W. Anderson (2003). An Introduction to Multivariate Statistical Analysis. Third Ediion. Wiley-Interscience
- Alvin C. Rencher (2003). Methods of multivariate analysis. Jhon Wiley. Wiley series in probability and mathematical statistics.
- Peña Daniel (2002). Análisis de datos multivariantes. Mc Graw Hill Interamericana.