Add mapa_incidencias.py

This commit is contained in:
2025-10-23 12:50:51 +00:00
parent 5e869365f1
commit a14ebced22

503
mapa_incidencias.py Normal file
View File

@@ -0,0 +1,503 @@
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 incidentes
"""
df = pd.read_csv(ruta_incidencias)
print(f"📊 Datos cargados: {len(df)} registros")
# Análisis de TODOS los tipos de incidente
contador_tipos = Counter(df['tipo_incidente'])
total_incidencias = len(df)
# Ordenar por frecuencia descendente
todos_tipos = []
for tipo, count in contador_tipos.most_common():
porcentaje = (count / total_incidencias) * 100
todos_tipos.append({
'tipo': tipo,
'count': count,
'porcentaje': porcentaje
})
print(f"🎯 Se analizaron {len(todos_tipos)} tipos de incidentes diferentes")
# Análisis temporal por tipo (si hay fechas)
tendencias = {}
if 'fincidencia' in df.columns:
df['fecha'] = pd.to_datetime(df['fincidencia'], errors='coerce')
df = df.dropna(subset=['fecha'])
df['mes'] = df['fecha'].dt.to_period('M')
for item in todos_tipos:
tipo = item['tipo']
datos_tipo = df[df['tipo_incidente'] == tipo]
tendencia_mensual = datos_tipo.groupby('mes').size()
# Calcular tendencia (último mes vs penúltimo mes)
if len(tendencia_mensual) >= 2:
ultimo_mes = tendencia_mensual.iloc[-1]
penultimo_mes = tendencia_mensual.iloc[-2]
cambio = ultimo_mes - penultimo_mes
porcentaje_cambio = (cambio / penultimo_mes * 100) if penultimo_mes > 0 else 0
else:
ultimo_mes = tendencia_mensual.iloc[-1] if len(tendencia_mensual) > 0 else 0
penultimo_mes = 0
cambio = 0
porcentaje_cambio = 0
tendencias[tipo] = {
'total': item['count'],
'porcentaje': item['porcentaje'],
'tendencia_mensual': tendencia_mensual,
'ultimo_mes': ultimo_mes,
'penultimo_mes': penultimo_mes,
'cambio': cambio,
'porcentaje_cambio': porcentaje_cambio
}
else:
# Si no hay fechas, solo mostrar datos básicos
for item in todos_tipos:
tipo = item['tipo']
tendencias[tipo] = {
'total': item['count'],
'porcentaje': item['porcentaje'],
'ultimo_mes': 0,
'penultimo_mes': 0,
'cambio': 0,
'porcentaje_cambio': 0
}
# 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:
coord_str = str(row['lat_long']).strip('() ')
if coord_str and coord_str != 'nan':
partes = coord_str.split(',')
if len(partes) == 2:
lat = float(partes[0].strip())
lon = float(partes[1].strip())
tipo_incidente = row['tipo_incidente']
if 15.0 < lat < 18.0 and -98.0 < lon < -94.0:
coordenadas_por_tipo[tipo_incidente].append([lat, lon])
except:
continue
return df, coordenadas_por_tipo, todos_tipos, tendencias
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 incidentes
"""
df, coordenadas_por_tipo, todos_tipos, tendencias = cargar_y_analizar_todos_datos(ruta_incidencias)
# Calcular centro del mapa
todas_coordenadas = []
for coords in coordenadas_por_tipo.values():
todas_coordenadas.extend(coords)
if not todas_coordenadas:
return None, df, todos_tipos, tendencias
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)]
m = folium.Map(location=centro, zoom_start=11)
# 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)
HeatMap(todas_las_coordenadas, radius=15, blur=12, min_opacity=0.3).add_to(m)
# 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, tendencias, colores, len(df))
return m, df, todos_tipos, tendencias
def crear_panel_completo(m, todos_tipos, tendencias, 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: 500px;
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 COMPLETO
</h2>
<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 12px; border-radius: 8px; margin-top: 10px;">
<strong>📊 TODOS LOS TIPOS ANALIZADOS</strong><br>
<span style="font-size: 12px;">
{len(todos_tipos)} tipos diferentes<br>
{total_incidencias:,} incidencias totales
</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 TIPOS
</h3>
<span style="font-size: 11px; background: #ecf0f1; padding: 4px 8px; border-radius: 12px;">
Mostrando {min(50, len(todos_tipos))} de {len(todos_tipos)}
</span>
</div>
<div style="max-height: 400px; overflow-y: auto; border: 1px solid #bdc3c7; border-radius: 8px; padding: 10px; margin-top: 10px;">
'''
# Estadísticas para cada tipo (mostrar máximo 50 para no saturar)
tipos_a_mostrar = todos_tipos[:50]
for i, item in enumerate(tipos_a_mostrar):
tipo = item['tipo']
count = item['count']
porcentaje = item['porcentaje']
color = colores[i % len(colores)]
tendencia = tendencias.get(tipo, {})
# Determinar icono de tendencia
if tendencia.get('cambio', 0) > 0:
icono_tendencia = "📈"
color_tendencia = "#e74c3c"
texto_tendencia = f"+{tendencia['cambio']}"
elif tendencia.get('cambio', 0) < 0:
icono_tendencia = "📉"
color_tendencia = "#27ae60"
texto_tendencia = f"{tendencia['cambio']}"
else:
icono_tendencia = "➡️"
color_tendencia = "#95a5a6"
texto_tendencia = "Sin cambio"
# Barra de porcentaje
ancho_barra = min(porcentaje * 3, 100) # Escalar para mejor visualización
panel_html += f'''
<div style="margin-bottom: 10px; 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} {tipo[:35]}{'...' if len(tipo) > 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}%</span>
<span style="color: {color_tendencia}; font-weight: bold;">
{icono_tendencia} {texto_tendencia}
</span>
</div>
</div>
'''
# Mensaje si hay más tipos
if len(todos_tipos) > 50:
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) - 50} tipos 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;">
📋 RESUMEN GENERAL
</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 (tipos con más del 1% vs menos del 1%)
tipos_significativos = sum(1 for item in todos_tipos if item['porcentaje'] >= 1)
tipos_menores = total_tipos - tipos_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 Totales</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/Tipo</div>
<div style="font-size: 16px; font-weight: bold;">{promedio_por_tipo:.1f}</div>
</div>
</div>
<div style="font-size: 11px; color: #6c757d;">
<strong>Diversidad:</strong> {tipos_significativos} tipos ≥1% | {tipos_menores} tipos <1%
</div>
</div>
'''
panel_html += '''
</div>
</div>
<div style="background: #d4edda; padding: 12px; border-radius: 8px; border: 1px solid #c3e6cb;">
<strong>💡 INFORMACIÓN DEL ANÁLISIS</strong><br>
<span style="font-size: 11px;">
• <strong>Análisis completo</strong> de todos los tipos de incidentes<br>
• <strong>Heatmap</strong> muestra densidad total de incidentes<br>
• <strong>Ordenado</strong> por frecuencia descendente<br>
• <strong>Tendencias</strong> mensuales para cada tipo
</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 CON ANÁLISIS COMPLETO")
print("="*60)
ruta_incidencias = 'incidencias.csv'
ruta_camaras_kmz = 'pmi.kmz'
ruta_vehiculos_kmz = 'pmv.kmz'
print("🗺️ Creando mapa con análisis completo...")
m, df, todos_tipos, tendencias = crear_mapa_completo(
ruta_incidencias, ruta_camaras_kmz, ruta_vehiculos_kmz
)
if m:
m.save('mapa_analisis_completo.html')
print("✅ Mapa con análisis completo guardado: mapa_analisis_completo.html")
# Mostrar resumen en consola
print(f"\n📊 RESUMEN COMPLETO:")
print(f" Total incidencias analizadas: {len(df):,}")
print(f" Tipos diferentes identificados: {len(todos_tipos)}")
# Estadísticas adicionales
promedio = len(df) / len(todos_tipos)
max_tipo = todos_tipos[0]
min_tipo = todos_tipos[-1]
print(f" Promedio de incidencias por tipo: {promedio:.1f}")
print(f" Tipo más frecuente: '{max_tipo['tipo']}' ({max_tipo['count']:,} - {max_tipo['porcentaje']:.1f}%)")
print(f" Tipo menos frecuente: '{min_tipo['tipo']}' ({min_tipo['count']:,} - {min_tipo['porcentaje']:.1f}%)")
print(f"\n🎯 TOP 10 TIPOS MÁS FRECUENTES:")
for i, item in enumerate(todos_tipos[:10], 1):
tendencia = tendencias.get(item['tipo'], {})
cambio = tendencia.get('cambio', 0)
icono = "📈" if cambio > 0 else "📉" if cambio < 0 else "➡️"
print(f" {i:2d}. {item['tipo'][:40]:40} {item['porcentaje']:5.1f}% {icono}")
print(f"\n🎉 ANÁLISIS COMPLETO FINALIZADO EXITOSAMENTE!")
print("="*60)
except Exception as e:
print(f"\n❌ Error: {e}")
import traceback
traceback.print_exc()
if __name__ == "__main__":
main_mapa_completo()