Files
scripts/mapa_denuncias.py
2025-10-23 12:48:15 +00:00

485 lines
19 KiB
Python

import pandas as pd
import folium
from folium.plugins import HeatMap
import json
import zipfile
import tempfile
import os
import xml.etree.ElementTree as ET
from collections import Counter
from datetime import datetime
def cargar_y_analizar_todos_datos(ruta_incidencias):
"""
Carga datos y analiza TODOS los tipos de delitos
"""
df = pd.read_csv(ruta_incidencias)
print(f"📊 Datos cargados: {len(df)} registros")
# Análisis de TODOS los tipos de delitos
contador_delitos = Counter(df['delito'])
total_incidencias = len(df)
# Ordenar por frecuencia descendente
todos_tipos = []
for delito, count in contador_delitos.most_common():
porcentaje = (count / total_incidencias) * 100
todos_tipos.append({
'tipo': delito,
'count': count,
'porcentaje': porcentaje
})
print(f"🎯 Se analizaron {len(todos_tipos)} tipos de delitos diferentes")
# Análisis por municipio
contador_municipios = Counter(df['municipio'])
top_municipios = contador_municipios.most_common(10)
# Procesar coordenadas para TODOS los tipos
coordenadas_por_tipo = {}
for item in todos_tipos:
coordenadas_por_tipo[item['tipo']] = []
for idx, row in df.iterrows():
try:
lat = row['latitud']
lon = row['longitud']
tipo_delito = row['delito']
# Validar coordenadas
if pd.notna(lat) and pd.notna(lon) and 15.0 < lat < 18.0 and -98.0 < lon < -94.0:
coordenadas_por_tipo[tipo_delito].append([lat, lon])
except:
continue
return df, coordenadas_por_tipo, todos_tipos, top_municipios
def kmz_a_geojson_simple(ruta_kmz):
"""
Convierte KMZ a GeoJSON de forma simple y segura
"""
try:
with zipfile.ZipFile(ruta_kmz, 'r') as kmz:
kml_files = [f for f in kmz.namelist() if f.endswith('.kml')]
if not kml_files:
return None
with tempfile.TemporaryDirectory() as temp_dir:
kmz.extract(kml_files[0], temp_dir)
kml_path = os.path.join(temp_dir, kml_files[0])
return parsear_kml_manual(kml_path)
except Exception as e:
print(f"❌ Error procesando KMZ: {e}")
return None
def parsear_kml_manual(ruta_kml):
"""
Parsea KML manualmente
"""
try:
tree = ET.parse(ruta_kml)
root = tree.getroot()
ns = {'kml': 'http://www.opengis.net/kml/2.2'}
features = []
for placemark in root.findall('.//kml:Placemark', ns):
feature = {
'type': 'Feature',
'properties': {},
'geometry': {'type': 'Point', 'coordinates': [0, 0]}
}
name_elem = placemark.find('kml:name', ns)
if name_elem is not None:
feature['properties']['name'] = name_elem.text
coords_elem = placemark.find('.//kml:coordinates', ns)
if coords_elem is not None and coords_elem.text:
try:
coords_text = coords_elem.text.strip()
parts = coords_text.split(',')
if len(parts) >= 2:
lon = float(parts[0])
lat = float(parts[1])
feature['geometry']['coordinates'] = [lon, lat]
features.append(feature)
except ValueError:
continue
return {
'type': 'FeatureCollection',
'features': features
}
except Exception as e:
print(f"❌ Error parseando KML: {e}")
return None
def crear_mapa_completo(ruta_incidencias, ruta_camaras_kmz=None, ruta_vehiculos_kmz=None):
"""
Crea mapa con análisis completo de TODOS los tipos de delitos
"""
df, coordenadas_por_tipo, todos_tipos, top_municipios = cargar_y_analizar_todos_datos(ruta_incidencias)
# Calcular centro del mapa basado en las coordenadas
todas_coordenadas = []
for coords in coordenadas_por_tipo.values():
todas_coordenadas.extend(coords)
if todas_coordenadas:
lats = [coord[0] for coord in todas_coordenadas]
lons = [coord[1] for coord in todas_coordenadas]
centro = [sum(lats)/len(lats), sum(lons)/len(lons)]
else:
# Centro por defecto para Oaxaca
centro = [17.0594, -96.7216]
m = folium.Map(location=centro, zoom_start=12)
# Generar paleta de colores más amplia
colores_base = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7',
'#DDA0DD', '#98D8C8', '#F7DC6F', '#BB8FCE', '#85C1E9',
'#FFA07A', '#20B2AA', '#778899', '#DEB887', '#5F9EA0',
'#FF69B4', '#BA55D3', '#9370DB', '#3CB371', '#FFD700']
# Extender paleta si hay más tipos
colores = colores_base
if len(todos_tipos) > len(colores_base):
import colorsys
colores_extra = []
for i in range(len(todos_tipos) - len(colores_base)):
hue = i / (len(todos_tipos) - len(colores_base))
rgb = colorsys.hsv_to_rgb(hue, 0.8, 0.9)
color = '#{:02x}{:02x}{:02x}'.format(int(rgb[0]*255), int(rgb[1]*255), int(rgb[2]*255))
colores_extra.append(color)
colores = colores_base + colores_extra
# Capa base de heatmap (todos los tipos)
todas_las_coordenadas = []
for coords in coordenadas_por_tipo.values():
todas_las_coordenadas.extend(coords)
if todas_las_coordenadas:
HeatMap(todas_las_coordenadas, radius=15, blur=12, min_opacity=0.3).add_to(m)
# Añadir marcadores individuales para los puntos
for idx, row in df.iterrows():
try:
if pd.notna(row['latitud']) and pd.notna(row['longitud']):
folium.CircleMarker(
location=[row['latitud'], row['longitud']],
radius=4,
popup=f"<b>{row['delito']}</b><br>Municipio: {row['municipio']}",
color='red',
fill=True,
fillOpacity=0.6
).add_to(m)
except:
continue
# Cargar capas KMZ
capas_kmz = {}
if ruta_camaras_kmz and os.path.exists(ruta_camaras_kmz):
print("📷 Cargando cámaras KMZ...")
capas_kmz['camaras'] = kmz_a_geojson_simple(ruta_camaras_kmz)
if ruta_vehiculos_kmz and os.path.exists(ruta_vehiculos_kmz):
print("🚗 Cargando vehículos KMZ...")
capas_kmz['vehiculos'] = kmz_a_geojson_simple(ruta_vehiculos_kmz)
# Añadir marcadores KMZ
for capa_nombre, geojson in capas_kmz.items():
if geojson:
color = 'black' if capa_nombre == 'camaras' else 'blue'
icono = 'camera' if capa_nombre == 'camaras' else 'car'
etiqueta = '📹' if capa_nombre == 'camaras' else '🚗'
for feature in geojson['features']:
coords = feature['geometry']['coordinates']
nombre = feature['properties'].get('name', capa_nombre.title())
folium.Marker(
[coords[1], coords[0]],
popup=f"{etiqueta} {nombre}",
icon=folium.Icon(color=color, icon=icono, prefix='fa')
).add_to(m)
# Crear HTML para panel completo
crear_panel_completo(m, todos_tipos, top_municipios, colores, len(df))
return m, df, todos_tipos, top_municipios
def crear_panel_completo(m, todos_tipos, top_municipios, colores, total_incidencias):
"""
Crea panel de análisis completo de TODOS los tipos
"""
# HTML para el panel completo
panel_html = f'''
<div id="completePanel" style="
position: fixed;
top: 20px;
left: 20px;
background: white;
padding: 20px;
border: 3px solid #2c3e50;
border-radius: 15px;
z-index: 1000;
width: 520px;
max-height: 85vh;
overflow-y: auto;
box-shadow: 0 8px 25px rgba(0,0,0,0.3);
font-family: Arial, sans-serif;
">
<div style="text-align: center; margin-bottom: 20px;">
<h2 style="margin: 0; color: #2c3e50; font-size: 22px;">
📈 ANÁLISIS CRIMINALÍSTICO
</h2>
<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 12px; border-radius: 8px; margin-top: 10px;">
<strong>📊 ANÁLISIS COMPLETO DE DELITOS</strong><br>
<span style="font-size: 12px;">
{len(todos_tipos)} tipos de delitos<br>
{total_incidencias:,} incidentes registrados
</span>
</div>
</div>
<div style="margin-bottom: 15px;">
<div style="display: flex; justify-content: space-between; align-items: center;">
<h3 style="margin: 0; color: #34495e; font-size: 16px;">
🎯 DISTRIBUCIÓN DE DELITOS
</h3>
<span style="font-size: 11px; background: #ecf0f1; padding: 4px 8px; border-radius: 12px;">
Mostrando {min(30, len(todos_tipos))} de {len(todos_tipos)}
</span>
</div>
<div style="max-height: 300px; overflow-y: auto; border: 1px solid #bdc3c7; border-radius: 8px; padding: 10px; margin-top: 10px;">
'''
# Estadísticas para cada tipo (mostrar máximo 30 para no saturar)
tipos_a_mostrar = todos_tipos[:30]
for i, item in enumerate(tipos_a_mostrar):
delito = item['tipo']
count = item['count']
porcentaje = item['porcentaje']
color = colores[i % len(colores)]
# Barra de porcentaje
ancho_barra = min(porcentaje * 3, 100) # Escalar para mejor visualización
panel_html += f'''
<div style="margin-bottom: 8px; padding: 8px; background: #f8f9fa; border-radius: 6px; border-left: 3px solid {color};">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px;">
<strong style="font-size: 12px;">#{i+1} {delito[:35]}{'...' if len(delito) > 35 else ''}</strong>
<span style="font-size: 11px; color: #2c3e50; font-weight: bold;">{count:,}</span>
</div>
<!-- Barra de porcentaje -->
<div style="background: #e9ecef; border-radius: 3px; height: 6px; margin-bottom: 4px; overflow: hidden;">
<div style="background: {color}; height: 100%; width: {ancho_barra}%; border-radius: 3px;"></div>
</div>
<div style="display: flex; justify-content: space-between; font-size: 10px;">
<span style="color: #6c757d;">{porcentaje:.1f}% del total</span>
<span style="color: #6c757d;">Rank: {i+1}</span>
</div>
</div>
'''
# Mensaje si hay más tipos
if len(todos_tipos) > 30:
panel_html += f'''
<div style="text-align: center; padding: 10px; background: #fff3cd; border-radius: 6px; margin-top: 10px;">
<span style="font-size: 11px; color: #856404;">
⚠️ Y {len(todos_tipos) - 30} tipos de delitos más con menor frecuencia
</span>
</div>
'''
panel_html += '''
</div>
</div>
<div style="margin-bottom: 15px;">
<h3 style="margin: 0 0 10px 0; color: #34495e; font-size: 16px;">
🏙️ TOP 10 MUNICIPIOS
</h3>
<div style="max-height: 150px; overflow-y: auto; border: 1px solid #bdc3c7; border-radius: 8px; padding: 10px;">
'''
# Top municipios
for i, (municipio, count) in enumerate(top_municipios):
porcentaje = (count / total_incidencias) * 100
panel_html += f'''
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; padding: 4px;">
<span style="font-size: 11px; font-weight: bold;">#{i+1} {municipio[:25]}{'...' if len(municipio) > 25 else ''}</span>
<div style="display: flex; align-items: center; gap: 8px;">
<span style="font-size: 10px; color: #6c757d;">{porcentaje:.1f}%</span>
<span style="font-size: 11px; background: #e9ecef; padding: 2px 6px; border-radius: 10px;">{count:,}</span>
</div>
</div>
'''
panel_html += '''
</div>
</div>
<div style="margin-bottom: 15px;">
<h3 style="margin: 0 0 10px 0; color: #34495e; font-size: 16px;">
📋 RESUMEN ESTADÍSTICO
</h3>
<div style="background: #fff3cd; padding: 12px; border-radius: 8px; border: 1px solid #ffeaa7;">
'''
# Resumen general
total_tipos = len(todos_tipos)
promedio_por_tipo = total_incidencias / total_tipos if total_tipos > 0 else 0
# Calcular diversidad (delitos con más del 1% vs menos del 1%)
delitos_significativos = sum(1 for item in todos_tipos if item['porcentaje'] >= 1)
delitos_menores = total_tipos - delitos_significativos
panel_html += f'''
<div style="text-align: center;">
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 10px;">
<div style="background: #e8f5e8; padding: 8px; border-radius: 6px;">
<div style="font-size: 12px; font-weight: bold; color: #27ae60;">Tipos de Delitos</div>
<div style="font-size: 16px; font-weight: bold;">{total_tipos}</div>
</div>
<div style="background: #e8f4fd; padding: 8px; border-radius: 6px;">
<div style="font-size: 12px; font-weight: bold; color: #3498db;">Promedio/Delito</div>
<div style="font-size: 16px; font-weight: bold;">{promedio_por_tipo:.1f}</div>
</div>
</div>
<div style="font-size: 11px; color: #6c757d;">
<strong>Concentración:</strong> {delitos_significativos} delitos ≥1% | {delitos_menores} delitos <1%
</div>
</div>
'''
panel_html += '''
</div>
</div>
<div style="background: #d4edda; padding: 12px; border-radius: 8px; border: 1px solid #c3e6cb;">
<strong>💡 INFORMACIÓN DEL MAPA</strong><br>
<span style="font-size: 11px;">
• <strong>Heatmap</strong> muestra densidad total de delitos<br>
• <strong>Círculos rojos</strong> representan ubicaciones exactas<br>
• <strong>Ordenado</strong> por frecuencia de delitos<br>
• <strong>Análisis</strong> por tipo y municipio
</span>
</div>
</div>
<script>
// Hacer el panel arrastrable
const panel = document.getElementById('completePanel');
let isDragging = false;
let dragOffset = {x: 0, y: 0};
panel.addEventListener('mousedown', function(e) {
isDragging = true;
dragOffset.x = e.clientX - panel.getBoundingClientRect().left;
dragOffset.y = e.clientY - panel.getBoundingClientRect().top;
panel.style.cursor = 'grabbing';
});
document.addEventListener('mousemove', function(e) {
if (isDragging) {
panel.style.left = (e.clientX - dragOffset.x) + 'px';
panel.style.top = (e.clientY - dragOffset.y) + 'px';
panel.style.right = 'auto';
}
});
document.addEventListener('mouseup', function() {
isDragging = false;
panel.style.cursor = 'grab';
});
</script>
<style>
#completePanel {
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
#completePanel:hover {
box-shadow: 0 12px 35px rgba(0,0,0,0.4);
}
#completePanel::-webkit-scrollbar {
width: 8px;
}
#completePanel::-webkit-scrollbar-thumb {
background: #bdc3c7;
border-radius: 4px;
}
#completePanel::-webkit-scrollbar-thumb:hover {
background: #95a5a6;
}
</style>
'''
m.get_root().html.add_child(folium.Element(panel_html))
def main_mapa_completo():
"""
Función principal con análisis completo
"""
try:
print("🔥 CREANDO MAPA CRIMINALÍSTICO COMPLETO")
print("="*60)
ruta_incidencias = 'incidencias.csv' # Ajusta el nombre según tu archivo
ruta_camaras_kmz = 'pmi.kmz'
ruta_vehiculos_kmz = 'pmv.kmz'
print("🗺️ Creando mapa con análisis criminalístico...")
m, df, todos_tipos, top_municipios = crear_mapa_completo(
ruta_incidencias, ruta_camaras_kmz, ruta_vehiculos_kmz
)
if m:
m.save('mapa_criminalistico_completo.html')
print("✅ Mapa criminalístico guardado: mapa_criminalistico_completo.html")
# Mostrar resumen en consola
print(f"\n📊 RESUMEN CRIMINALÍSTICO:")
print(f" Total de delitos analizados: {len(df):,}")
print(f" Tipos de delitos diferentes: {len(todos_tipos)}")
print(f" Municipios con incidencias: {len(set(df['municipio']))}")
# Estadísticas adicionales
promedio = len(df) / len(todos_tipos)
max_delito = todos_tipos[0]
min_delito = todos_tipos[-1]
print(f" Promedio de ocurrencias por delito: {promedio:.1f}")
print(f" Delito más frecuente: '{max_delito['tipo']}' ({max_delito['count']:,} - {max_delito['porcentaje']:.1f}%)")
print(f" Delito menos frecuente: '{min_delito['tipo']}' ({min_delito['count']:,} - {min_delito['porcentaje']:.1f}%)")
print(f"\n🎯 TOP 10 DELITOS MÁS COMUNES:")
for i, item in enumerate(todos_tipos[:10], 1):
print(f" {i:2d}. {item['tipo'][:40]:40} {item['porcentaje']:5.1f}% ({item['count']:,} casos)")
print(f"\n🏙️ TOP 5 MUNICIPIOS:")
for i, (municipio, count) in enumerate(top_municipios[:5], 1):
porcentaje = (count / len(df)) * 100
print(f" {i:2d}. {municipio[:30]:30} {porcentaje:5.1f}% ({count:,} casos)")
print(f"\n🎉 ANÁLISIS CRIMINALÍSTICO COMPLETADO EXITOSAMENTE!")
print("="*60)
except Exception as e:
print(f"\n❌ Error: {e}")
import traceback
traceback.print_exc()
if __name__ == "__main__":
main_mapa_completo()