Sistema activo · Beta académica

SafeMaps

Análisis de riesgo urbano en Cali mediante mapas interactivos y ecuaciones diferenciales.

Plataforma

Herramientas de análisis

Mapa Interactivo

Visualización geoespacial de Cali con capas de riesgo organizadas por comunas y barrios.

Riesgo Urbano

Clasificación de zonas según indicadores históricos de peligro en tres niveles de severidad.

Modelo Diferencial

Ecuación diferencial resuelta con Euler para modelar la evolución del riesgo acumulado por distancia.

Metodología

Cómo funciona el modelo

Safe Maps toma una ruta urbana real proporcionada por OpenRouteService, la divide en segmentos y asigna a cada tramo un riesgo local según la comuna por la que pasa. Para cada segmento se revisa su punto inicial y su punto final, se analiza su posición dentro de la ruta y se usa esa información para construir una lectura progresiva del recorrido. Luego calcula cómo evoluciona el riesgo acumulado usando una ecuación diferencial aproximada con el método de Euler. El resultado no predice delitos: representa una exposición relativa al riesgo urbano durante el recorrido.

Exposición relativa · No predicción criminalEuler numérico · Modelo académico

Pipeline del sistema

sistema activo

Origen · Destino

coordenadas GPS del usuario

↓

OpenRouteService

geometría real de la vía

↓

Segmentación ~400 m

segmentByDistance()

↓

Supabase · risk_score

commune_risk_profiles

↓

Euler dR/dx = k(L − R)

calculateEulerRiskEvolution()

↓

Mapa · Paneles UI

RouteAnalysis.segments

6 etapas · datos reales · sin predicción de delitos

Ecuación implementada

dRdx
=k(L − R)

Qué describe esta ecuación

Esta ecuación describe cómo evoluciona la exposición al riesgo urbano mientras una persona avanza por la ruta. No mide delitos ni los predice: modela cuánto riesgo se ha acumulado en función del camino recorrido.

Significado de cada símbolo

dR / dxtasa de cambio del riesgo

No es el riesgo en sí mismo: es qué tan rápido está cambiando el riesgo mientras se avanza. Si es positivo, el riesgo sube. Si es negativo, baja. Si es cero, el riesgo se mantiene estable.

Rriesgo acumulado

El total de exposición que la ruta ha integrado hasta ese punto. Cambia suavemente con cada segmento.

Lriesgo local

El risk_score de la comuna actual, proveniente de Supabase. Representa el 'ambiente' de la zona.

kconstante de respuesta

Controla qué tan rápido R se acerca a L. Con k = 1, la reacción es directa y completa.

xdistancia recorrida

La posición a lo largo de la ruta en kilómetros. Cada segmento aporta un Δx al avance total.

Qué significa "tasa de cambio"

La derivada dR/dx no es el riesgo: es cuánto cambia el riesgo por cada kilómetro recorrido. Cuando es grande, el riesgo sube o baja rápido. Cuando es pequeña, el cambio es lento.

Es parecido a la velocidad de un vehículo: la velocidad no es la posición, sino cuánto cambia la posición por unidad de tiempo. De la misma forma, dR/dx describe el ritmo de cambio del riesgo, no su valor absoluto.

Comportamiento según la zona actual

L > R↑Zona más riesgosa que lo acumulado

El riesgo acumulado comienza a subir gradualmente. Cuanto mayor sea la diferencia (L − R), más rápido sube.

L < R↓Zona más tranquila que lo acumulado

El riesgo acumulado empieza a bajar progresivamente. El modelo reconoce un ambiente menos riesgoso.

L = R→Zona y acumulado en equilibrio

La diferencia (L − R) es cero, entonces dR/dx = 0. No hay cambio: el riesgo permanece estable.

Por qué no hay saltos bruscos

Al cruzar la frontera entre dos comunas, el riesgo acumulado no salta instantáneamente al valor de la nueva zona. El cambio depende de tres factores:

  • 01La diferencia (L − R): si la brecha es pequeña, el cambio también lo es.
  • 02La longitud del segmento (Δx): tramos cortos producen cambios menores.
  • 03El método de Euler: actualiza el acumulado en pasos discretos y finitos, nunca de forma abrupta.

Analogía

Imagina que R es la temperatura que trae tu cuerpo y L es la temperatura del lugar al que entras. Si entras a un sitio más caliente, tu temperatura no cambia de golpe: empieza a subir gradualmente. Si sales a un lugar más frío, baja poco a poco. La ecuación hace exactamente eso con el riesgo: el acumulado siempre se mueve hacia el nivel local, sin saltos bruscos.

Código real · paso por segmento

const derivative = EULER_LOCAL_RISK_RESPONSE_K * (localRiskScore - current);
const next = clamp(current + derivative * deltaKm, 0, 100);

localRiskScore es el riesgo local del segmento actual.

current es el riesgo acumulado que la ruta llevaba antes de entrar al segmento.

derivative calcula qué tan fuerte debe cambiar el riesgo acumulado.

deltaKm es la longitud del segmento en kilómetros. Mientras más largo sea el tramo, mayor efecto tendrá sobre el acumulado.

clamp(..., 0, 100) limita el resultado para que el sistema nunca salga de la escala de riesgo entre 0 y 100.

apps/web/lib/risk/euler-risk-integrator.tslíneas 66-69

Flujo del modelo y origen del riesgo

Frontend

1. Usuario selecciona una ruta

El usuario escribe un origen y un destino dentro de Cali.

OpenRouteService

2. OpenRouteService genera la ruta real

La API devuelve coordenadas reales de calles y avenidas.

Segmentación

3. La ruta se divide en segmentos

El backend transforma la geometría en pequeños tramos de aproximadamente 400 metros.

Análisis espacial

4. Cada segmento se cruza con comunas

El sistema analiza el punto inicial y final del tramo para determinar por qué comuna pasa.

Supabase

5. Supabase aporta el riesgo local

Cada comuna tiene un risk_score normalizado entre 0 y 100 construido desde variables urbanas.

Variables como criminalidad, seguridad, vigilancia, iluminación y flujo de personas alimentan el risk_score en la base de datos. En esta versión, Euler no usa directamente esas variables; utiliza el puntaje final ya calculado.
TypeScript

6. El segmento recibe localRiskScore

El riesgo de la comuna se transforma en el riesgo local del segmento actual.

Euler

7. Euler calcula el riesgo acumulado

El modelo diferencial usa el riesgo local L para actualizar el acumulado R paso a paso.

Euler toma el riesgo acumulado anterior y lo compara con el riesgo local del segmento actual. Luego calcula cuánto debe subir o bajar el acumulado dependiendo de la distancia recorrida.
UI

8. La interfaz visualiza el resultado

El mapa colorea segmentos y muestra cómo cambia el riesgo durante el recorrido.

apps/web/lib/repositories/supabase-commune-risk-repository.tsapps/web/lib/routes/normalize-openroute-route.ts
Fase I · Geometría Real
01

Paso 01

Ruta real desde OpenRouteService

El sistema no dibuja una línea recta. Pide el camino exacto por las calles.

Entrada

  • ▸coordenada de origen (lng, lat)
  • ▸coordenada de destino (lng, lat)
→

Proceso

POST /directions → OpenRouteService → GeoJSON LineString

→

Salida

  • ▸polyline: secuencia de N coordenadas reales
  • ▸distancia total en metros

geometría real de la vía

AB

Intuición

Cuando buscas una ruta, el sistema no asume que vas en línea recta. Pide a un servicio externo el trayecto real: giros, calles y distancias incluidos. Eso le da una geometría honesta sobre la que trabajar.

Técnico

OpenRouteService retorna un GeoJSON con una LineString. El backend normaliza la respuesta extrayendo la secuencia ordenada de coordenadas [lng, lat] que describe la traza real de la vía.

Por qué importa

Sin geometría real, los segmentos no tendrían posición geográfica válida. El cruce con comunas, el riesgo local y la integración de Euler dependen de coordenadas de calles reales.

↓
02

Paso 02

División en segmentos continuos de ~400 m

La ruta completa se corta en tramos pequeños. Cada tramo tendrá su propio riesgo.

Entrada

  • ▸polyline con N coordenadas
  • ▸TARGET_SEGMENT_METERS = 400
→

Proceso

acumular distancia haversine → corte cada 400 m → nuevo segmento

→

Salida

  • ▸RouteSegment[] con ≈ distanciaTotal / 400 elementos
  • ▸cada segmento: coordenadas + punto medio + deltaKm

segmentos de ~400 m

~400m~400m~400m~400m

Intuición

Si la ruta tiene 5 km, el sistema la divide en pedazos de ~400 m, como leer una frase palabra por palabra en vez de todo de golpe. Así puede estudiar qué zona atraviesa cada trozo por separado.

Técnico

La polyline se recorre acumulando distancia haversine. Cuando supera TARGET_SEGMENT_METERS (400 m) se cierra el tramo y comienza uno nuevo. Cada segmento almacena coordenadas de inicio, fin, punto medio y su deltaKm.

Por qué importa

Euler necesita pasos discretos con longitud medible. Sin deltaKm no existe la variable Δx de la integral numérica. Sin segmentos, el modelo no puede avanzar.

Fase II · Enriquecimiento Geográfico
03

Paso 03

Cada segmento encuentra su comuna

El punto medio de cada tramo se cruza con los polígonos de las 22 comunas.

Entrada

  • ▸RouteSegment[] con punto medio calculado
  • ▸polígonos GeoJSON de 22 comunas de Cali
→

Proceso

punto medio → point-in-polygon sobre 22 polígonos → communeId

→

Salida

  • ▸cada RouteSegment + communeId asignado

point-in-polygon · comunas

C13C14C15punto medio

Intuición

Cada tramo vive dentro de alguna zona de la ciudad. El sistema calcula el punto central del tramo y pregunta: ¿dentro de qué polígono de comuna cae este punto? La respuesta le da identidad geográfica al segmento.

Técnico

Para cada segmento se computa el punto medio de sus coordenadas. Ese punto se evalúa contra los polígonos GeoJSON de las comunas usando point-in-polygon. El segmento hereda el communeId del polígono que lo contiene.

Por qué importa

Sin communeId el sistema no sabe de dónde obtener el riesgo local. El segmento necesita identidad geográfica antes de consultar Supabase.

↓
04

Paso 04

Riesgo local L desde Supabase

Supabase entrega el risk_score de la comuna. Ese número se convierte en L.

Entrada

  • ▸communeId de cada segmento
  • ▸tabla commune_risk_profiles en Supabase
→

Proceso

SELECT risk_score WHERE commune_id = communeId

→

Salida

  • ▸localRiskScore ∈ [0, 100] por segmento
  • ▸L listo para la ecuación diferencial

commune_risk_profiles · Supabase

C14
74
C07
38
C20
61

risk_score normalizado 0 → 100

Intuición

La base de datos tiene un número entre 0 y 100 para cada zona de Cali. Ese número resume la exposición relativa al riesgo urbano. El sistema lo toma y lo asigna al segmento como su 'nivel de ambiente'.

Técnico

El repositorio consulta commune_risk_profiles filtrando por communeId. El campo risk_score (normalizado 0–100) se asigna al segmento como localRiskScore. Este valor es exactamente L en la ecuación diferencial.

Por qué importa

L es la fuerza de atracción del modelo. Sin risk_score la ecuación no tiene objetivo hacia dónde converger.

Fase III · Integración Numérica
05

Paso 05

Condición inicial R(0)

El integrador no empieza desde cero. Arranca con el riesgo real del punto de origen.

Entrada

  • ▸localRiskScore del primer segmento (índice 0)
→

Proceso

R(0) ← segments[0].localRiskScore

→

Salida

  • ▸initialRiskScore = R(0)
  • ▸punto de partida del integrador numérico

Intuición

Antes de caminar ya estás en algún lugar con su propio nivel de riesgo. El modelo toma el riesgo de la primera zona que pisas como punto de partida. Así el resultado es coherente desde el primer metro.

Técnico

R(0) se inicializa con el localRiskScore del primer segmento. Esto garantiza continuidad matemática: la condición inicial no es arbitraria sino un dato real del ambiente de partida.

R(0) = localRiskScore[0]

Iniciar con R(0) = 0 sería arbitrario y distorsionaría el resultado.

Por qué importa

La condición inicial ancla el modelo a la realidad. Un origen en una zona de riesgo alto debe reflejarse desde el primer tramo.

↓
06

Paso 06

La ecuación diferencial dR/dx = k(L − R)

dRdx=k(L −R)

ecuación diferencial ordinaria · método de Euler

R(x)

Riesgo acumulado

x

Distancia en km

L

Riesgo local

k = 1

Sensibilidad Euler

L > R→R sube hacia LL < R→R baja hacia LL = R→R estable

Intuición

Si la zona que estás cruzando es más peligrosa que lo vivido hasta ahora, tu exposición sube lentamente hacia ese nivel. Si es más tranquila, baja poco a poco. La ecuación captura esa lógica: el acumulado siempre se mueve hacia el nivel local, sin saltos abruptos.

Técnico

La EDO modela atracción proporcional a la diferencia entre el riesgo local L y el acumulado R. Con k = 1 (euler-v1), por cada kilómetro recorrido R avanza un porcentaje de la brecha (L − R).

Por qué importa

Sin esta ecuación el riesgo acumulado cambiaría abruptamente al cruzar fronteras de comunas. La EDO produce una curva suave que modela exposición progresiva.

↓
07

Paso 07

Método de Euler: la EDO en pasos discretos

Discretización del método de Euler forward

Ri+1=clamp(Ri+k(Li−Ri)Δxi,0,100)

nuevo riesgo acumulado=riesgo anterior+cambio del tramo actual

Ejemplo numérico · paso a paso

1Riesgo acumulado actual
Ri=42

Lo que lleva la ruta hasta este tramo. Es el punto de partida del paso.

2Riesgo local de la comuna
Li=78

El ambiente de la zona que atraviesa este segmento. Viene de Supabase.

3Diferencia entre L y R
Li−Ri=78 − 42=36

Positivo: el ambiente es más riesgoso que lo acumulado. R debe subir.

4Multiplicar por k
k×36=1 × 36=36

Con k = 1 la respuesta es directa. Un k menor haría la curva más lenta.

5Multiplicar por longitud del tramo
36×Δxi=36 × 0.4=14.4

Tramos más largos producen mayor cambio. Δx = deltaKm del segmento.

6Actualizar el riesgo acumulado
Ri+14.4=42 + 14.4=56.4

Nuevo acumulado antes del clamp. Creció desde 42, pero no llegó a 78.

7Aplicar clamp [0, 100]
clamp(56.4, 0, 100)=56.4✓

Dentro del rango válido. El nuevo Rᵢ₊₁ = 56.4 entra al siguiente segmento.

Conclusión del ejemplo

Aunque el riesgo local era 78, el acumulado no saltó de 42 a 78. Subió gradualmente a 56.4. Así funciona Euler: avanza paso a paso, sin saltos bruscos.

Flujo de cálculo

R actual

42

L local

78

L − R
↓

diferencia

36

× k (1)
↓

derivative

36

× Δx (0.4)
↓

cambio real

14.4

42 + 14.4
↓

R nuevo

56.4

R no llega a L de golpe

R actual42
R nuevo56.4
L local78

R sube hacia L · sin saltos bruscos · clamp [0, 100]

Analogía · sin fórmulas

Imagina que R es la temperatura que trae la ruta y L es la temperatura del lugar donde entras. Si entras a un sitio más caliente, tu temperatura no cambia de golpe: empieza a subir poco a poco. Euler hace exactamente eso con el riesgo: mira hacia dónde debe moverse y avanza solo un pedacito según la distancia recorrida.

Código real · paso por segmento

const derivative = k * (localRiskScore - current);
const next = clamp(current + derivative * deltaKm, 0, 100);

Equivalencias · código ↔ fórmula

current≡Rᵢ

riesgo acumulado antes del segmento

localRiskScore≡Lᵢ

riesgo local de la comuna actual

k≡k = 1

velocidad de respuesta al riesgo local

derivative≡k(Lᵢ−Rᵢ)

magnitud del cambio por km recorrido

deltaKm≡Δxᵢ

longitud del segmento en kilómetros

next≡Rᵢ₊₁

nuevo acumulado · entra al próximo segmento

Simulación · R converge hacia L en 6 pasos

k = 1 · Δx ≈ 0.4 km
L
78
R₀
42
R₁
56
R₂
65
R₃
70
R₄
73
R₅
75

R se acerca a L pero nunca en saltos bruscos · clamp [0, 100]

Entrada

  • ▸R actual: riesgo acumulado del paso anterior
  • ▸derivative = k(L − R)
  • ▸deltaKm: longitud del segmento en km
→

Proceso

next = clamp(R + derivative × deltaKm, 0, 100)

→

Salida

  • ▸R nuevo = accumulatedRiskScore del segmento
  • ▸input del próximo paso de Euler

Intuición

No existe una fórmula mágica que resuelva dR/dx = k(L−R) sobre una ruta irregular. Euler avanza un pasito a la vez: calcula cuánto cambia R en este tramo, aplica ese cambio y pasa al siguiente con el nuevo valor.

Técnico

El método de Euler forward aplica R(x+Δx) ≈ R(x) + f(R,x)·Δx, donde f(R,x) = k·(L−R) y Δx = deltaKm del segmento. El resultado se limita a [0, 100] con clamp.

Por qué importa

Sin Euler no hay forma de integrar la EDO sobre segmentos de longitud variable. La aproximación numérica es lo que permite resolver el modelo sobre una ruta real.

↓
08

Paso 08

Evolución acumulada a lo largo de toda la ruta

R(x) convergiendo hacia L a lo largo de la ruta

R acumuladoL local
0255075100L=78R(0)=42distancia recorrida (x)

localRiskScore (L)

El riesgo de la comuna actual. Cambia abruptamente al cruzar fronteras de comunas. Describe el ambiente.

accumulatedRiskScore (R)

El historial de exposición acumulado hasta ese punto. Cambia suavemente con Euler. Describe la historia.

Intuición

Euler no produce un solo número sino una historia: cómo fue cambiando tu exposición a medida que avanzabas. Al llegar, puedes ver exactamente en qué tramo subió y en cuál bajó.

Técnico

El output de cada paso Euler se convierte en el input del siguiente, construyendo la serie temporal de R. Cada RouteSegment almacena simultáneamente localRiskScore (el ambiente, L) y accumulatedRiskScore (el historial, R). Son magnitudes distintas.

Por qué importa

La distinción entre riesgo local y acumulado es el corazón del modelo. El local describe el ambiente puntual; el acumulado describe cuánto riesgo se ha integrado durante el trayecto completo.

Fase IV · Resultados
09

Paso 09

Visualización en mapa y paneles de análisis

Los números se convierten en colores, gráficas y detalle por segmento consultable.

Mapa · ruta coloreada

lowmediumhigh

Panel · segmento actual

Riesgo local (L)74
Riesgo acum. (R)61
NivelALTO

Entrada

  • ▸RouteAnalysis.segments[]
  • ▸localRiskScore + accumulatedRiskScore + riskLevel por segmento
→

Proceso

segmentos → capas GeoJSON en MapLibre + panel lateral paginado

→

Salida

  • ▸ruta coloreada por riskLevel (low / medium / high)
  • ▸panel con evolución de R a lo largo de x
  • ▸detalle numérico por segmento consultable

Intuición

Todo el procesamiento anterior produce segmentos llenos de datos. La interfaz los convierte en algo legible: el mapa colorea cada tramo y el panel lateral muestra cómo evolucionó el acumulado a lo largo del recorrido.

Técnico

El frontend recibe RouteAnalysis.segments con localRiskScore, accumulatedRiskScore y riskLevel por tramo. MapLibre renderiza la ruta coloreando cada segmento. El panel lateral pagina los segmentos y muestra la evolución numérica de R.

Por qué importa

Sin visualización el modelo es un número en memoria. La UI convierte el resultado numérico en información comprensible: el usuario puede ver dónde aumenta su exposición.

Paso numérico por segmento

const derivative = k * (localRiskScore - current);
const next = clamp(current + derivative * deltaKm, 0, 100);

La operación se repite para cada segmento. La longitud del tramo convierte la ruta en una sucesión de pasos espaciales deltaKm, por eso el acumulado no cambia bruscamente al entrar en una comuna distinta.

Referencias del código

apps/web/lib/routes/route-segmentation.tsapps/web/lib/routes/normalize-openroute-route.tsapps/web/lib/risk/euler-risk-integrator.tsapps/web/lib/risk/euler-accumulated-route-risk.tsSupabaseCommuneRiskRepository.getAll()

Tablas principales: communes, commune_risk_profiles, risk_model_versions, risk_model_coefficients, annual_crime_indicators, risk_time_windows y data_sources.