503 lines
19 KiB
Python
503 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 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() |