diff --git a/.gitignore b/.gitignore index 7a033c3..9b6b35a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ # Data files data/* +# Config file +configs/* # Results files /resultados @@ -137,40 +139,19 @@ dmypy.json # Pyre type checker .pyre/ -prueba_sa.ipynb -zonas_tmp.geojson *.json -od_matrix/utils/visuals_bk.py -setup-Copy1.py -run_urbantrips.ipynb -configs/configuraciones_generales.yaml -configs/configuraciones_generales.yaml -configs/configuraciones_generales.yaml -configs/configuraciones_generales_all.yaml -configs/configuraciones_generales_muestra10.yaml -configs/configuraciones_generales_muestra15.yaml -configs/configuraciones_generales_muestra5.yaml -configs/configuraciones_generales.yaml -configs/configuraciones_generales.yaml -configs/configuraciones_generales.yaml -configs/configuraciones_generales_amba_muestra5_xdia.yaml -configs/configuraciones_generales.yaml -configs/configuraciones_generales_muestra_2022.yaml -configs/configuraciones_generales.yaml -configs/configuraciones_generales_2022.yaml -configs/configuraciones_generales.yaml -educacion.ipynb -configs/configuraciones_generales_2019_all.yaml -configs/configuraciones_generales____.yaml -configs/configuraciones_generales_2019_m20.yaml -configs/configuraciones_generales_2019_m30.yaml -configs/configuraciones_generales_2019_m40.yaml -configs/configuraciones_generales_202209_40.yaml -configs/configuraciones_generales_202209_all.yaml -configs/configuraciones_generales_test.yaml -configs/configuraciones_generales.yaml *.yaml *.ipynb .github/desktop.ini *.ini -run_services.ipynb +configs/configuraciones_generales.yaml +*.csv +*.yaml +*.csv +*.csv +*.yaml +configs/configuraciones_generales.yaml +*.png +etapas_nse +configs/configuraciones_generales.yaml +configs/configuraciones_generales.yaml diff --git a/README.md b/README.md index 0573658..8a694f0 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ UrbanTrips es una biblioteca de código abierto que toma información de un sist Para mayor información de la librería, instalación, configuración y uso puede leerse la [Documentación](https://el-bid.github.io/UrbanTrips/) -Para una discusión metodológica de cómo se imputan destinos y se construye la matriz de origen y destino se puede consultar este ![documento metodológico](https://github.com/EL-BID/UrbanTrips/blob/dev/docs/Metodologia_UrbanTrips.pdf "Documento metodológico"). +Para una discusión metodológica de cómo se imputan destinos y se construye la matriz de origen y destino se puede consultar este [documento metodológico](https://github.com/EL-BID/UrbanTrips/blob/dev/docs/Metodologia_UrbanTrips.pdf "Documento metodológico"). ## Agradecimientos diff --git a/README_en.md b/README_en.md index d34d15f..50f4b87 100644 --- a/README_en.md +++ b/README_en.md @@ -8,7 +8,7 @@ ![logo](https://github.com/EL-BID/UrbanTrips/blob/dev/docs/logo_readme.png) # README -`urbantrips` is an open-source library that takes information from a smart card payment system for public transportation and, through information processing that infers trip destinations and constructs travel chains for each user, produces origin-destination matrices and other indicators (KPI) for bus routes. The main objective of the library is to produce useful inputs for public transport management from minimal information and pre-processing requirements. With only a geolocalized table of economic transactions from an electronic payment system, results can be generated, which will be more accurate the more additional information is incorporated into the process through optional files. The process elaborates the matrices, indicators, and constructs a series of transport graphs and maps. +`urbantrips` is an open-source library that takes information from a smart card payment system for public transportation and, through information processing that infers trip destinations and constructs travel chains for each user, produces origin-destination matrices and other indicators (KPI) for bus routes. The main objective of the library is to produce useful inputs for public transport management from minimal information and pre-processing requirements. With only a geolocalized table of economic transactions from an electronic payment system, results can be generated, which will be more accurate the more additional information is incorporated into the process through optional files. The process elaborates the matrices, indicators, and constructs a series of transport graphs and maps. For a methodological discussion of how destinations are imputed and the origin-destination matrix is constructed, refer to this ![document (in spanish)](https://github.com/EL-BID/UrbanTrips/blob/dev/Metodologia_UrbanTrips.pdf "Documento metodológico") diff --git a/configs/configuraciones_generales.yaml b/configs/configuraciones_generales.yaml old mode 100644 new mode 100755 index ec713bf..5af06b9 --- a/configs/configuraciones_generales.yaml +++ b/configs/configuraciones_generales.yaml @@ -1,89 +1,108 @@ -# REPO ORIGINAL -geolocalizar_trx: False - -# Resolucion H3: resolucion del hexagono. La resolucion 8 tiene lados de 460 metros. Resolucion 9 tiene 174 metros y la 10 tiene 65 metros -resolucion_h3: 8 -#tolerancia parada destino en metros -tolerancia_parada_destino: 2200 - -# Proyeccion de coordenadas en metros a utilizar -epsg_m: 9265 - -#especificar el archivo con las transacciones a consumir -nombre_archivo_trx: transacciones_amba_test.csv - -alias_db_data: amba_test - -alias_db_insumos: amba_test - -lineas_contienen_ramales: True -nombre_archivo_informacion_lineas: lineas_amba_test.csv - -imputar_destinos_min_distancia: False - -#ingresar el nombre de las variables -nombres_variables_trx: - id_trx: id - fecha_trx: fecha - id_tarjeta_trx: id_tarjeta - modo_trx: modo - hora_trx: hora - id_linea_trx: id_linea - id_ramal_trx: id_ramal - interno_trx: interno_bus - orden_trx: etapa_red_sube - latitud_trx: lat - longitud_trx: lon - factor_expansion: - -nombre_archivo_gps: - -nombres_variables_gps: - id_gps: - id_linea_gps: - id_ramal_gps: - interno_gps: - fecha_gps: - latitud_gps: - longitud_gps: - -modos: - autobus: COL - tren: TRE - metro: SUB - tranvia: - brt: - -recorridos_geojson: recorridos_amba.geojson - - -# Filtro de coordenadas en formato minx, miny, maxx, maxy -filtro_latlong_bbox: - minx: -59.3 - miny: -35.5 - maxx: -57.5 - maxy: -34.0 - - -#Especificar el formato fecha -formato_fecha: "%d/%m/%Y" - -#Indicar si la informacion sobre la hora está en una columna separada. En nombres_variables debe indicarse el nombre. Dejar vacío en caso contrario -columna_hora: True - -# Indicar que criterio se desea utilizar para ordenar las transacciones en el tiempo. -# Puede utilizarse el campo fecha: en ese caso debe tener informacion hasta el minuto al menos, y se debe especificar una ventana de tiempo en minutos en ventana_viajes para agrupar etapas en un viaje -# Puede utilizarse el campo orden_trx: este campo debe tener entero secuencial que ordena las transacciones. Debe comenzar en cero cuando se comienza un nuevo viaje, e incrementear con cada nueva etapa en ese viaje - -ordenamiento_transacciones: orden_trx #fecha_completa u orden_trx -ventana_viajes: -ventana_duplicado: - - -tipo_trx_invalidas: - tipo_trx_tren: - - 'CHECK OUT SIN CHECKIN' - - 'CHECK OUT' - -# Poner geo1 el nombre del archivo geojson, var1 el nombre de la variable que contiene la etiqueta a utilizar, orden1 el orden de las etiquetas en los graficos -zonificaciones: \ No newline at end of file +# Archivo de configuración para urbantrips + +# Bases de datos +nombre_archivo_trx: "transacciones_amba_test.csv" # Especificar el archivo con las transacciones a consumir +alias_db_data: "amba_test" # nombre del sqlite donde se guardan los datos procesados +alias_db_insumos: "amba_test" # nombre del sqlite donde se guardan los insumos generales + +# Nombre de columnas en el archivo de transacciones +nombres_variables_trx: + id_trx: "id" # columna con id único del archivo de transacciones + fecha_trx: "fecha" # columna con fecha de la transacción + id_tarjeta_trx: "id_tarjeta" # columna con id de la tarjeta + modo_trx: "modo" # columna con modo de transporte + hora_trx: "hora" # columna con hora de la transacción + id_linea_trx: "id_linea" # columna con el id de la línea + id_ramal_trx: "id_ramal" # columna con el ramal de la línea + interno_trx: "interno_bus" # columna con interno de la línea + orden_trx: "etapa_red_sube" # columna con el orden de la transacción (si falta hora/minuto en fecha_trx) + latitud_trx: "lat" # columna con la latitud de la transacción + longitud_trx: "lon" # columna con longitud de la transacción + factor_expansion: # columna con el factor de expansión + +# Parámetros de transacciones +ordenamiento_transacciones: "orden_trx" # especifica si ordena transacciones por fecha ("fecha_completa") o por variable orden_trx ("orden_trx") en la tabla nombres_variables_trx +ventana_viajes: 120 # ventana de tiempo para que una transacción sea de un mismo viaje (ej. 60 minutos) +ventana_duplicado: 5 # ventana de tiempo si hay duplicado de transacción (ej. Viaje con acompañante) + +# Elimina transacciones inválidas de la tabla de transacciones +tipo_trx_invalidas: + tipo_trx_tren: [ # Lista con el contenido a eliminar de la variable seleccionada + "CHECK OUT SIN CHECKIN", + "CHECK OUT", + ] + + +# Imputación de destino +tolerancia_parada_destino: 2200 # Distancia para la validación de los destinos (metros) +imputar_destinos_min_distancia: False # Busca la parada que minimiza la distancia con respecto a la siguiente trancción + +# Parámetros geográficos +resolucion_h3: 8 # Resolución de los hexágonos +epsg_m: 9265 # Parámetros geográficos: crs + +formato_fecha: "%d/%m/%Y" # Configuración fecha y hora +columna_hora: True + +geolocalizar_trx: False +nombre_archivo_gps: # Especificar el archivo con los datos gps de las líneas + +# Nombre de columnas en el archivo de GPS +nombres_variables_gps: + id_gps: + id_linea_gps: + id_ramal_gps: + interno_gps: + fecha_gps: + latitud_gps: + longitud_gps: + velocity_gps: + servicios_gps: # Indica cuando se abre y cierra un servicio + +# Información para procesamiento de líneas +nombre_archivo_informacion_lineas: "lineas_amba_test.csv" # Archivo .csv con lineas, debe contener ("id_linea", "nombre_linea", "modo") +lineas_contienen_ramales: True # Especificar si las líneas de colectivo contienen ramales +nombre_archivo_paradas: "stops.csv" +imprimir_lineas_principales: 5 # Imprimir las lineas principales - "All" imprime todas las líneas + +# Servicios GPS +utilizar_servicios_gps: False # Especifica si ve van a utilizar los servicios GPS +valor_inicio_servicio: # Valor de la variable que marca el inicio del servicio +valor_fin_servicio: # Valor de la variable que marca el fin del servicio + +modos: + autobus: "COL" + tren: "TRE" + metro: "SUB" + tranvia: + brt: + +# Capas geográficas con recorridos de líneas +recorridos_geojson: "recorridos_amba.geojson" # archivo geojson con el trazado de las líneas de transporte público + +filtro_latlong_bbox: + minx: -59.3 + miny: -35.5 + maxx: -57.5 + maxy: -34.0 + +# Zonificaciones +zonificaciones: + geo1: + var1: + orden1: + geo2: + var2: + orden2: + geo3: + var3: + orden3: + geo4: + var4: + orden4: + geo5: + var5: + orden5: + +poligonos: # Especificar una capa geográfica de polígonos en formato .geojson. El archivo requiere las siguientes columnas: ['id', 'tipo', 'geometry']. 'id' es el id o nombre del polígono, tipo puede ser 'poligono' o 'cuenca'. +tiempos_viaje_estaciones: diff --git a/configs/configuraciones_generales_2019_m1.yaml b/configs/configuraciones_generales_2019_m1.yaml index dff64db..3188544 100644 --- a/configs/configuraciones_generales_2019_m1.yaml +++ b/configs/configuraciones_generales_2019_m1.yaml @@ -60,10 +60,10 @@ nombres_variables_gps: servicios_gps: # Indica cuando se abre y cierra un servicio # Información para procesamiento de líneas -nombre_archivo_informacion_lineas: "lineas.csv" -informacion_lineas_contiene_ramales: True +nombre_archivo_informacion_lineas: "lineas.csv" lineas_contienen_ramales: False # Especificar si las líneas de colectivo contienen ramales nombre_archivo_paradas: +imprimir_lineas_principales: 5 # Imprimir las lineas principales - "All" imprime todas las líneas # Servicios GPS utilizar_servicios_gps: False # Especifica si ve van a utilizar los servicios GPS @@ -182,3 +182,5 @@ zonificaciones: var5: orden5: + +tiempos_viaje_estaciones: \ No newline at end of file diff --git a/data/data_ciudad/lineas.csv b/data/data_ciudad/lineas.csv index 486212e..8495c0c 100644 --- a/data/data_ciudad/lineas.csv +++ b/data/data_ciudad/lineas.csv @@ -1,403 +1,403 @@ -id_linea,nombre_linea,modo,id_linea_agg,nombre_linea_agg -1,Línea 244,COL,, -2,Línea 501C,COL,, -3,Línea 071,COL,, -4,Línea 111,COL,, -5,Línea 269,COL,, -6,Línea 024,COL,, -7,Línea 078,COL,, -8,Línea 365,COL,, -9,Línea 087,COL,, -10,Línea 634,COL,, -11,Línea 053,COL,, -12,Línea 140,COL,, -13,Línea 303,COL,, -14,Línea 504,COL,, -15,Línea 245,COL,, -16,Tren Roca,TRE,5000.0,Tren Roca -17,Línea 127,COL,, -18,Línea 394,COL,, -19,Línea 395,COL,, -20,Línea 184,COL,, -21,Línea 219,COL,, -22,Línea 320,COL,, -23,Línea 390,COL,, -24,Línea 036,COL,, -25,Línea 300,COL,, -26,Línea 501D,COL,, -27,Línea 152,COL,, -28,Línea 502A,COL,, -29,Línea 443A,COL,, -30,Línea 222,COL,, -31,Línea 141,COL,, -32,Subte A,SUB,8000.0,Subte -33,Subte B,SUB,8000.0,Subte -34,Línea 133,COL,, -35,Línea 306,COL,, -36,Línea 461,COL,, -37,Línea 462,COL,, -38,Línea 463,COL,, -39,Línea 372,COL,, -40,Línea 584,COL,, -41,Línea 159,COL,, -42,Línea 464,COL,, -43,Línea 505,COL,, -44,Tren Belgrano-Sur,TRE,44.0,Tren Belgrano-Sur -45,Línea 002,COL,, -46,Línea 7,COL,, -47,Línea 501 A,COL,, -48,Línea 039,COL,, -49,Tren Mitre (Suarez),TRE,5100.0,Tren Mitre -50,Línea 102,COL,, -51,Línea Oeste,COL,, -52,Línea 620,COL,, -53,Línea 176,COL,, -54,Subte D,SUB,8000.0,Subte -55,Línea 067,COL,, -56,Línea 068,COL,, -57,Línea 288,COL,, -58,Línea 218,COL,, -59,Línea 501G,COL,, -60,Línea 311,COL,, -61,Línea 284,COL,, -62,Línea 161,COL,, -63,Línea 132,COL,, -64,Línea 136,COL,, -65,Línea 325,COL,, -66,Línea 188,COL,, -67,Línea 391,COL,, -68,Línea 098,COL,, -69,Línea 378,COL,, -70,Línea 022,COL,, -71,Línea 163,COL,, -72,Línea 622,COL,, -73,Línea 257,COL,, -74,Línea 421,COL,, -75,Línea 628,COL,, -76,Línea 109,COL,, -77,Línea 055,COL,, -78,Línea 009,COL,, -79,Línea 204B,COL,, -80,Línea 430,COL,, -81,Línea 084,COL,, -82,Línea 509D,COL,, -83,Línea 228 B,COL,, -84,Línea 722,COL,, -85,Línea 064,COL,, -86,Línea 503 B,COL,, -87,Línea 580,COL,, -88,Línea 514,COL,, -89,Línea 582,COL,, -90,Línea 010,COL,, -91,Línea 25,COL,, -92,Línea 247,COL,, -93,Línea 541,COL,, -94,Línea 341,COL,, -95,Línea 271,COL,, -96,Línea 017,COL,, -97,Línea 060,COL,, -98,Línea 543,COL,, -99,Línea 540,COL,, -100,Línea 603,COL,, -101,Línea 126,COL,, -102,Línea 070,COL,, -103,Línea 283,COL,, -104,Línea 181,COL,, -105,Línea 295,COL,, -106,Línea 542,COL,, -107,Línea 237A,COL,, -108,Línea 619,COL,, -109,Línea 323,COL,, -110,Línea 379,COL,, -111,Línea 239A,COL,, -112,Línea 160,COL,, -113,Línea 324,COL,, -114,Línea 278,COL,, -115,Línea 585,COL,, -116,Línea 214,COL,, -117,Línea 134,COL,, -118,Línea 180,COL,, -119,Línea 586,COL,, -120,Línea 326,COL,, -121,Línea 130,COL,, -122,Línea 630,COL,, -123,Línea 506A,COL,, -124,Línea 386,COL,, -125,Línea 281,COL,, -126,Línea 322,COL,, -127,Línea 510B,COL,, -128,Línea 354,COL,, -129,Línea 293B,COL,, -130,Línea 448,COL,, -131,Línea 124,COL,, -132,Tren Sarmiento (Merlo-Lobos),TRE,5200.0,Tren Sarmiento -133,Tren Sarmiento (Moreno-Mercedes),TRE,5200.0,Tren Sarmiento -134,Línea 029,COL,, -135,Tren de la costa,TRE,135.0,Tren de la costa -136,Línea 146,COL,, -137,Línea 741,COL,, -138,Línea 092,COL,, -139,Línea 085,COL,, -140,Línea 503H,COL,, -141,Línea 371,COL,, -142,Línea 063,COL,, -143,Línea 12,COL,, -144,Tren Sarmiento,TRE,5200.0,Tren Sarmiento -145,Línea 113,COL,, -146,Línea 445,COL,, -147,Línea 710,COL,, -148,Subte E,SUB,8000.0,Subte -149,Tren Urquiza,TRE,149.0,Tren Urquiza -150,Línea 021,COL,, -151,Línea 236,COL,, -152,Línea 312,COL,, -153,Línea 045,COL,, -154,Norte Municipal,COL,, -155,Línea 503A,COL,, -156,Línea 440,COL,, -157,Línea 154,COL,, -158,Línea 019,COL,, -159,Línea 502_SUR,COL,, -160,Tren Mitre (Tigre),TRE,5100.0,Tren Mitre -161,Línea 520C,COL,, -162,Línea 297,COL,, -163,Línea 500D,COL,, -164,Línea 153,COL,, -165,Línea 373,COL,, -166,Línea 046,COL,, -167,Línea 384,COL,, -168,Línea 105,COL,, -169,Línea 253,COL,, -170,Línea 321,COL,, -171,Línea 090,COL,, -172,Tren Mitre,TRE,5100.0,Tren Mitre -173,Línea 570,COL,, -174,Tren Mitre (Capilla),TRE,5100.0,Tren Mitre -175,Tren Mitre (Zárate),TRE,5100.0,Tren Mitre -176,Línea 315,COL,, -177,Línea 168,COL,, -178,Línea 164,COL,, -179,Línea 723,COL,, -180,Línea 065,COL,, -181,Línea 328,COL,, -182,Línea 106,COL,, -183,Línea 108,COL,, -184,Línea 099,COL,, -185,Línea 179,COL,, -186,Línea 194,COL,, -187,Línea 215,COL,, -188,Línea 720,COL,, -189,Línea 200,COL,, -190,Línea 225,COL,, -191,Línea 404,COL,, -192,Línea 414,COL,, -193,Línea 508C,COL,, -194,Línea 561B,COL,, -195,Línea 195,COL,, -196,Tren Belgrano Norte,TRE,196.0,Tren Belgrano Norte -197,Línea 407,COL,, -198,Línea 228A,COL,, -199,Línea 523,COL,, -200,Línea 119,COL,, -201,Línea 437,COL,, -202,Línea 277,COL,, -203,Línea 707,COL,, -204,Línea 524,COL,, -205,Línea 329,COL,, -206,Línea 422,COL,, -207,Línea 151,COL,, -208,Línea 500C,COL,, -209,Línea 23,COL,, -210,Línea 501F,COL,, -211,Línea 5,COL,, -212,001,COL,, -213,Línea 150,COL,, -214,Línea 502B,COL,, -215,Línea 503E,COL,, -216,Línea 33,COL,, -217,Línea 107,COL,, -218,Línea 8,COL,, -219,Línea 544,COL,, -220,Subte H,SUB,8000.0,Subte -221,Línea 501B,COL,, -222,Línea 343,COL,, -223,Línea 059,COL,, -224,Línea 178,COL,, -225,Línea 505D,COL,, -226,Línea 289,COL,, -227,Línea 182,COL,, -228,Línea 304,COL,, -229,Línea 166,COL,, -230,Línea 239B,COL,, -231,Línea 670,COL,, -232,Línea 263A,COL,, -233,Línea 504B,COL,, -234,Línea 507B,COL,, -235,Línea 266,COL,, -236,Línea 299,COL,, -237,Línea 327,COL,, -238,Línea 263B,COL,, -239,Línea 370,COL,, -240,Línea 336,COL,, -241,Línea 385,COL,, -242,Línea 392,COL,, -243,Línea 047,COL,, -244,Línea 388,COL,, -245,Línea 403,COL,, -246,Línea 435,COL,, -247,Línea 088,COL,, -248,Línea 228F,COL,, -249,Línea 291,COL,, -250,Línea 548,COL,, -251,Línea 550,COL,, -252,Línea 502,COL,, -253,Línea 551,COL,, -254,Línea 520A,COL,, -255,Línea 552,COL,, -256,Línea 273,COL,, -257,Línea 061,COL,, -258,Línea 553,COL,, -259,Línea 062,COL,, -260,Línea 203,COL,, -261,Línea 441,COL,, -262,Línea 114,COL,, -263,Línea 418,COL,, -264,Línea 443,COL,, -265,Línea 504A,COL,, -266,Línea 100,COL,, -267,Línea 338,COL,, -268,Línea 202,COL,, -269,Subte C,SUB,8000.0,Subte -270,Línea 051,COL,, -271,Línea 020,COL,, -272,Línea 074,COL,, -273,Línea 117,COL,, -274,Línea 44,COL,, -275,Línea 086,COL,, -276,Línea 079,COL,, -277,Línea 177,COL,, -278,Línea 721,COL,, -279,Línea 193,COL,, -280,Línea 148,COL,, -281,Tren Roca (Cañuelas-Monte),TRE,5000.0,Tren Roca -282,Tren Roca (Korn-Chascomús),TRE,5000.0,Tren Roca -283,Tren Roca (Universitario),TRE,5000.0,Tren Roca -284,Tren San Martín,TRE,284.0,Tren San Martín -285,Línea 406,COL,, -286,Línea 506,COL,, -287,Línea 318,COL,, -288,Línea 521,COL,, -289,Línea 096,COL,, -290,Línea 034,COL,, -291,Línea 449,COL,, -292,Línea 093,COL,, -293,Línea 749,COL,, -294,Línea 185,COL,, -295,Línea 205,COL,, -296,Línea 129,COL,, -297,Línea 621,COL,, -298,Línea 276,COL,, -299,Línea 091,COL,, -300,Línea 143,COL,, -301,Línea 032,COL,, -302,Línea 310,COL,, -303,Línea 405,COL,, -304,Línea 049,COL,, -305,Línea 097,COL,, -306,Línea 264,COL,, -307,Línea 50,COL,, -308,Línea 520B,COL,, -309,Línea 521 A,COL,, -310,Línea 6,COL,, -311,Línea 526,COL,, -312,Línea 382,COL,, -313,Línea 333A,COL,, -314,Línea 252,COL,, -315,Línea 1,COL,, -316,Línea 383,COL,, -317,Línea 500H,COL,, -318,Línea 275,COL,, -319,Línea 503,COL,, -320,Línea 172,COL,, -321,Línea 174,COL,, -322,Línea 4,COL,, -323,Línea 242,COL,, -324,Línea 041,COL,, -325,Línea 015,COL,, -326,Línea 135,COL,, -327,Línea 31,COL,, -328,Línea 76,COL,, -329,Línea 238,COL,, -330,Línea 518,COL,, -331,Línea 500B,COL,, -332,Línea 080,COL,, -333,Línea 503C,COL,, -334,Línea 740,COL,, -335,Línea 509,COL,, -336,Línea 501I,COL,, -337,Línea 506F,COL,, -338,Línea 314,COL,, -339,Línea 110,COL,, -340,Línea 307,COL,, -341,Línea 103,COL,, -342,Línea 502,COL,, -343,Línea 501,COL,, -344,Línea 101,COL,, -345,Línea 28,COL,, -346,Línea 204,COL,, -347,Línea 228C,COL,, -348,Línea 228E,COL,, -349,Línea 505A,COL,, -350,Línea 505C,COL,, -351,Línea 508A,COL,, -352,Línea 123,COL,, -353,Línea 513A,COL,, -354,Línea 056,COL,, -355,Línea 118,COL,, -356,Línea 500,COL,, -357,Línea 313,COL,, -358,Línea 350,COL,, -359,Línea 355,COL,, -360,Línea 501H,COL,, -361,Línea 506B,COL,, -362,Línea 42,COL,, -363,Línea 512,COL,, -364,Línea 561,COL,, -365,Línea 562,COL,, -366,Línea 095,COL,, -367,Premetro,COL,, -368,Línea 298,COL,, -369,Línea 317,COL,, -370,Línea 057,COL,, -371,Línea 624,COL,, -372,Línea 635,COL,, -373,Línea 115,COL,, -374,Línea 026,COL,, -375,Línea 503 (este),COL,, -376,Línea 037,COL,, -377,Línea 511,COL,, -378,Línea 293A,COL,, -379,Línea 509C,COL,, -380,Tren Roca (Cañuelas-Lobos),TRE,5000.0,Tren Roca -381,Línea 302,COL,, -382,Línea 075,COL,, -383,Línea 583,COL,, -384,Línea 549,COL,, -385,Línea 175,COL,, -386,Tren del Valle,TRE,386.0,Tren del Valle -387,Línea 515,COL,, -388,Línea 169,COL,, -389,Línea 501E,COL,, -390,Línea 504C,COL,, -391,Línea 508,COL,, -392,Línea 436,COL,, -393,Línea 510A,COL,, -394,Línea 410,COL,, -395,Línea 429,COL,, -396,Línea 564,COL,, -397,Línea 128,COL,, -398,Línea 158,COL,, -399,Línea 522,COL,, -400,Línea 527,COL,, -401,Línea 506,COL,, -402,Línea 507,COL,, +id_linea,nombre_linea,modo,id_linea_agg,nombre_linea_agg +1,Línea 244,COL,, +2,Línea 501C,COL,, +3,Línea 071,COL,, +4,Línea 111,COL,, +5,Línea 269,COL,, +6,Línea 024,COL,, +7,Línea 078,COL,, +8,Línea 365,COL,, +9,Línea 087,COL,, +10,Línea 634,COL,, +11,Línea 053,COL,, +12,Línea 140,COL,, +13,Línea 303,COL,, +14,Línea 504,COL,, +15,Línea 245,COL,, +16,Tren Roca,TRE,5000.0,Tren Roca +17,Línea 127,COL,, +18,Línea 394,COL,, +19,Línea 395,COL,, +20,Línea 184,COL,, +21,Línea 219,COL,, +22,Línea 320,COL,, +23,Línea 390,COL,, +24,Línea 036,COL,, +25,Línea 300,COL,, +26,Línea 501D,COL,, +27,Línea 152,COL,, +28,Línea 502A,COL,, +29,Línea 443A,COL,, +30,Línea 222,COL,, +31,Línea 141,COL,, +32,Subte A,SUB,8000.0,Subte +33,Subte B,SUB,8000.0,Subte +34,Línea 133,COL,, +35,Línea 306,COL,, +36,Línea 461,COL,, +37,Línea 462,COL,, +38,Línea 463,COL,, +39,Línea 372,COL,, +40,Línea 584,COL,, +41,Línea 159,COL,, +42,Línea 464,COL,, +43,Línea 505,COL,, +44,Tren Belgrano-Sur,TRE,44.0,Tren Belgrano-Sur +45,Línea 002,COL,, +46,Línea 7,COL,, +47,Línea 501 A,COL,, +48,Línea 039,COL,, +49,Tren Mitre (Suarez),TRE,5100.0,Tren Mitre +50,Línea 102,COL,, +51,Línea Oeste,COL,, +52,Línea 620,COL,, +53,Línea 176,COL,, +54,Subte D,SUB,8000.0,Subte +55,Línea 067,COL,, +56,Línea 068,COL,, +57,Línea 288,COL,, +58,Línea 218,COL,, +59,Línea 501G,COL,, +60,Línea 311,COL,, +61,Línea 284,COL,, +62,Línea 161,COL,, +63,Línea 132,COL,, +64,Línea 136,COL,, +65,Línea 325,COL,, +66,Línea 188,COL,, +67,Línea 391,COL,, +68,Línea 098,COL,, +69,Línea 378,COL,, +70,Línea 022,COL,, +71,Línea 163,COL,, +72,Línea 622,COL,, +73,Línea 257,COL,, +74,Línea 421,COL,, +75,Línea 628,COL,, +76,Línea 109,COL,, +77,Línea 055,COL,, +78,Línea 009,COL,, +79,Línea 204B,COL,, +80,Línea 430,COL,, +81,Línea 084,COL,, +82,Línea 509D,COL,, +83,Línea 228 B,COL,, +84,Línea 722,COL,, +85,Línea 064,COL,, +86,Línea 503 B,COL,, +87,Línea 580,COL,, +88,Línea 514,COL,, +89,Línea 582,COL,, +90,Línea 010,COL,, +91,Línea 25,COL,, +92,Línea 247,COL,, +93,Línea 541,COL,, +94,Línea 341,COL,, +95,Línea 271,COL,, +96,Línea 017,COL,, +97,Línea 060,COL,, +98,Línea 543,COL,, +99,Línea 540,COL,, +100,Línea 603,COL,, +101,Línea 126,COL,, +102,Línea 070,COL,, +103,Línea 283,COL,, +104,Línea 181,COL,, +105,Línea 295,COL,, +106,Línea 542,COL,, +107,Línea 237A,COL,, +108,Línea 619,COL,, +109,Línea 323,COL,, +110,Línea 379,COL,, +111,Línea 239A,COL,, +112,Línea 160,COL,, +113,Línea 324,COL,, +114,Línea 278,COL,, +115,Línea 585,COL,, +116,Línea 214,COL,, +117,Línea 134,COL,, +118,Línea 180,COL,, +119,Línea 586,COL,, +120,Línea 326,COL,, +121,Línea 130,COL,, +122,Línea 630,COL,, +123,Línea 506A,COL,, +124,Línea 386,COL,, +125,Línea 281,COL,, +126,Línea 322,COL,, +127,Línea 510B,COL,, +128,Línea 354,COL,, +129,Línea 293B,COL,, +130,Línea 448,COL,, +131,Línea 124,COL,, +132,Tren Sarmiento (Merlo-Lobos),TRE,5200.0,Tren Sarmiento +133,Tren Sarmiento (Moreno-Mercedes),TRE,5200.0,Tren Sarmiento +134,Línea 029,COL,, +135,Tren de la costa,TRE,135.0,Tren de la costa +136,Línea 146,COL,, +137,Línea 741,COL,, +138,Línea 092,COL,, +139,Línea 085,COL,, +140,Línea 503H,COL,, +141,Línea 371,COL,, +142,Línea 063,COL,, +143,Línea 12,COL,, +144,Tren Sarmiento,TRE,5200.0,Tren Sarmiento +145,Línea 113,COL,, +146,Línea 445,COL,, +147,Línea 710,COL,, +148,Subte E,SUB,8000.0,Subte +149,Tren Urquiza,TRE,149.0,Tren Urquiza +150,Línea 021,COL,, +151,Línea 236,COL,, +152,Línea 312,COL,, +153,Línea 045,COL,, +154,Norte Municipal,COL,, +155,Línea 503A,COL,, +156,Línea 440,COL,, +157,Línea 154,COL,, +158,Línea 019,COL,, +159,Línea 502_SUR,COL,, +160,Tren Mitre (Tigre),TRE,5100.0,Tren Mitre +161,Línea 520C,COL,, +162,Línea 297,COL,, +163,Línea 500D,COL,, +164,Línea 153,COL,, +165,Línea 373,COL,, +166,Línea 046,COL,, +167,Línea 384,COL,, +168,Línea 105,COL,, +169,Línea 253,COL,, +170,Línea 321,COL,, +171,Línea 090,COL,, +172,Tren Mitre,TRE,5100.0,Tren Mitre +173,Línea 570,COL,, +174,Tren Mitre (Capilla),TRE,5100.0,Tren Mitre +175,Tren Mitre (Zárate),TRE,5100.0,Tren Mitre +176,Línea 315,COL,, +177,Línea 168,COL,, +178,Línea 164,COL,, +179,Línea 723,COL,, +180,Línea 065,COL,, +181,Línea 328,COL,, +182,Línea 106,COL,, +183,Línea 108,COL,, +184,Línea 099,COL,, +185,Línea 179,COL,, +186,Línea 194,COL,, +187,Línea 215,COL,, +188,Línea 720,COL,, +189,Línea 200,COL,, +190,Línea 225,COL,, +191,Línea 404,COL,, +192,Línea 414,COL,, +193,Línea 508C,COL,, +194,Línea 561B,COL,, +195,Línea 195,COL,, +196,Tren Belgrano Norte,TRE,196.0,Tren Belgrano Norte +197,Línea 407,COL,, +198,Línea 228A,COL,, +199,Línea 523,COL,, +200,Línea 119,COL,, +201,Línea 437,COL,, +202,Línea 277,COL,, +203,Línea 707,COL,, +204,Línea 524,COL,, +205,Línea 329,COL,, +206,Línea 422,COL,, +207,Línea 151,COL,, +208,Línea 500C,COL,, +209,Línea 23,COL,, +210,Línea 501F,COL,, +211,Línea 5,COL,, +212,001,COL,, +213,Línea 150,COL,, +214,Línea 502B,COL,, +215,Línea 503E,COL,, +216,Línea 33,COL,, +217,Línea 107,COL,, +218,Línea 8,COL,, +219,Línea 544,COL,, +220,Subte H,SUB,8000.0,Subte +221,Línea 501B,COL,, +222,Línea 343,COL,, +223,Línea 059,COL,, +224,Línea 178,COL,, +225,Línea 505D,COL,, +226,Línea 289,COL,, +227,Línea 182,COL,, +228,Línea 304,COL,, +229,Línea 166,COL,, +230,Línea 239B,COL,, +231,Línea 670,COL,, +232,Línea 263A,COL,, +233,Línea 504B,COL,, +234,Línea 507B,COL,, +235,Línea 266,COL,, +236,Línea 299,COL,, +237,Línea 327,COL,, +238,Línea 263B,COL,, +239,Línea 370,COL,, +240,Línea 336,COL,, +241,Línea 385,COL,, +242,Línea 392,COL,, +243,Línea 047,COL,, +244,Línea 388,COL,, +245,Línea 403,COL,, +246,Línea 435,COL,, +247,Línea 088,COL,, +248,Línea 228F,COL,, +249,Línea 291,COL,, +250,Línea 548,COL,, +251,Línea 550,COL,, +252,Línea 502,COL,, +253,Línea 551,COL,, +254,Línea 520A,COL,, +255,Línea 552,COL,, +256,Línea 273,COL,, +257,Línea 061,COL,, +258,Línea 553,COL,, +259,Línea 062,COL,, +260,Línea 203,COL,, +261,Línea 441,COL,, +262,Línea 114,COL,, +263,Línea 418,COL,, +264,Línea 443,COL,, +265,Línea 504A,COL,, +266,Línea 100,COL,, +267,Línea 338,COL,, +268,Línea 202,COL,, +269,Subte C,SUB,8000.0,Subte +270,Línea 051,COL,, +271,Línea 020,COL,, +272,Línea 074,COL,, +273,Línea 117,COL,, +274,Línea 44,COL,, +275,Línea 086,COL,, +276,Línea 079,COL,, +277,Línea 177,COL,, +278,Línea 721,COL,, +279,Línea 193,COL,, +280,Línea 148,COL,, +281,Tren Roca (Cañuelas-Monte),TRE,5000.0,Tren Roca +282,Tren Roca (Korn-Chascomús),TRE,5000.0,Tren Roca +283,Tren Roca (Universitario),TRE,5000.0,Tren Roca +284,Tren San Martín,TRE,284.0,Tren San Martín +285,Línea 406,COL,, +286,Línea 506,COL,, +287,Línea 318,COL,, +288,Línea 521,COL,, +289,Línea 096,COL,, +290,Línea 034,COL,, +291,Línea 449,COL,, +292,Línea 093,COL,, +293,Línea 749,COL,, +294,Línea 185,COL,, +295,Línea 205,COL,, +296,Línea 129,COL,, +297,Línea 621,COL,, +298,Línea 276,COL,, +299,Línea 091,COL,, +300,Línea 143,COL,, +301,Línea 032,COL,, +302,Línea 310,COL,, +303,Línea 405,COL,, +304,Línea 049,COL,, +305,Línea 097,COL,, +306,Línea 264,COL,, +307,Línea 50,COL,, +308,Línea 520B,COL,, +309,Línea 521 A,COL,, +310,Línea 6,COL,, +311,Línea 526,COL,, +312,Línea 382,COL,, +313,Línea 333A,COL,, +314,Línea 252,COL,, +315,Línea 1,COL,, +316,Línea 383,COL,, +317,Línea 500H,COL,, +318,Línea 275,COL,, +319,Línea 503,COL,, +320,Línea 172,COL,, +321,Línea 174,COL,, +322,Línea 4,COL,, +323,Línea 242,COL,, +324,Línea 041,COL,, +325,Línea 015,COL,, +326,Línea 135,COL,, +327,Línea 31,COL,, +328,Línea 76,COL,, +329,Línea 238,COL,, +330,Línea 518,COL,, +331,Línea 500B,COL,, +332,Línea 080,COL,, +333,Línea 503C,COL,, +334,Línea 740,COL,, +335,Línea 509,COL,, +336,Línea 501I,COL,, +337,Línea 506F,COL,, +338,Línea 314,COL,, +339,Línea 110,COL,, +340,Línea 307,COL,, +341,Línea 103,COL,, +342,Línea 502,COL,, +343,Línea 501,COL,, +344,Línea 101,COL,, +345,Línea 28,COL,, +346,Línea 204,COL,, +347,Línea 228C,COL,, +348,Línea 228E,COL,, +349,Línea 505A,COL,, +350,Línea 505C,COL,, +351,Línea 508A,COL,, +352,Línea 123,COL,, +353,Línea 513A,COL,, +354,Línea 056,COL,, +355,Línea 118,COL,, +356,Línea 500,COL,, +357,Línea 313,COL,, +358,Línea 350,COL,, +359,Línea 355,COL,, +360,Línea 501H,COL,, +361,Línea 506B,COL,, +362,Línea 42,COL,, +363,Línea 512,COL,, +364,Línea 561,COL,, +365,Línea 562,COL,, +366,Línea 095,COL,, +367,Premetro,COL,, +368,Línea 298,COL,, +369,Línea 317,COL,, +370,Línea 057,COL,, +371,Línea 624,COL,, +372,Línea 635,COL,, +373,Línea 115,COL,, +374,Línea 026,COL,, +375,Línea 503 (este),COL,, +376,Línea 037,COL,, +377,Línea 511,COL,, +378,Línea 293A,COL,, +379,Línea 509C,COL,, +380,Tren Roca (Cañuelas-Lobos),TRE,5000.0,Tren Roca +381,Línea 302,COL,, +382,Línea 075,COL,, +383,Línea 583,COL,, +384,Línea 549,COL,, +385,Línea 175,COL,, +386,Tren del Valle,TRE,386.0,Tren del Valle +387,Línea 515,COL,, +388,Línea 169,COL,, +389,Línea 501E,COL,, +390,Línea 504C,COL,, +391,Línea 508,COL,, +392,Línea 436,COL,, +393,Línea 510A,COL,, +394,Línea 410,COL,, +395,Línea 429,COL,, +396,Línea 564,COL,, +397,Línea 128,COL,, +398,Línea 158,COL,, +399,Línea 522,COL,, +400,Línea 527,COL,, +401,Línea 506,COL,, +402,Línea 507,COL,, diff --git a/data/data_ciudad/lineas_amba_test.csv b/data/data_ciudad/lineas_amba_test.csv index 93b8993..e46eeec 100644 --- a/data/data_ciudad/lineas_amba_test.csv +++ b/data/data_ciudad/lineas_amba_test.csv @@ -1,21 +1,21 @@ -"id_linea","nombre_linea","modo","id_ramal","nombre_ramal","empresa","descripcion","id_linea_agg","nombre_linea_agg" -16,"FFCC ROCA","TRE",16,"RAMAL_GR","JN - JN","RAMAL GRAL ROCA",16,"FFCC ROCA" -281,"FFCC ROCA","TRE",281,"MONTE(D)","JN - JN","ROCA_CANUELA-MONTE",16,"FFCC ROCA" -282,"FFCC ROCA","TRE",282,"KORN(D)","JN - JN","ROCA KORN - CHASCOMUS",16,"FFCC ROCA" -283,"FFCC ROCA","TRE",283,"UNIV(D)","JN - JN","ROCA_UNIVERSITARIO",16,"FFCC ROCA" -380,"FFCC ROCA","TRE",380,"LOBOS(D)","JN - JN","ROCA_CANUELAS-LOBOS",16,"FFCC ROCA" -32,"SUBTE PREMETRO","SUB",32,"LINEA A","JN - JN","RAMAL LINEA A",32,"SUBTE" -33,"SUBTE PREMETRO","SUB",33,"LINEA B","JN - JN","RAMAL LINEA B",32,"SUBTE" -54,"SUBTE PREMETRO","SUB",54,"LINEA D","JN - JN","RAMAL LINEA D",32,"SUBTE" -148,"SUBTE PREMETRO","SUB",148,"LINEA E","JN - JN","RAMAL LINEA E",32,"SUBTE" -220,"SUBTE PREMETRO","SUB",220,"LINEA H","JN - JN","RAMAL LINEA H",32,"SUBTE" -269,"SUBTE PREMETRO","SUB",269,"LINEA C","JN - JN","RAMAL LINEA C",32,"SUBTE" -367,"SUBTE PREMETRO","SUB",367,"PREMETRO","JN - JN","RAMAL_PREMETRO",32,"SUBTE" -48,"LINEA 39","COL",48,"039A","JN - JN","CHACARITA (CIUDAD AUTONOMA DE BUENOS AIRES) - BARRACAS (CIUDAD AUTONOMA DE BUENOS AIRES)",, -117,"LINEA 134","COL",117,"134A","JN - JN","BARRIO NICOLAS AVELLANEDA - ESTACION DEVOTO",, -137,"LINEA 741","COL",137,"741A","BUENOS AIRES - JOSE C. PAZ","ESTACION JOSE C PAZ - SOL Y VERDE POR CASTANEDA",, -183,"LINEA 108","COL",183,"108A","JN - JN","A RETIRO (CIUDAD AUTONOMA DE BUENOS AIRES) - CROACIA y BERGAMINI (PARTIDO DE TRES DE FEBRERO - PROVINCIA DE BUENOS AIRES)",, -223,"LINEA 59","COL",223,"059A","JN - JN","A (por ESTACION LA LUCILA)EST BS AS(LINEA BELGRANO SUR - CABA) - INT CORONEL AMARO AVALOS y DOMINGO DEACASSUSO (PIDO DE VICENTE LOPEZ - PCIA DE BS AS)",, -284,"FFCC SAN MARTIN","TRE",284,"RAMAL_S_","JN - JN","RAMAL TREN SAN MARTIN",, -287,"LINEA 318","COL",287,"318A","BUENOS AIRES - PROV","FRACC RAMAL A EST LOMAS DE ZAMORA - EST TURDERA",, -293,"LINEA 749","COL",293,"749A","BUENOS AIRES - JOSE C. PAZ","BARRIO R FAVALORO (ESTACION JOSE C PAZ) ESTACION SOL Y VERDE",, +id_linea,nombre_linea,modo,id_ramal,nombre_ramal,empresa,descripcion,id_linea_agg,nombre_linea_agg +16,FFCC ROCA,TRE,16,RAMAL_GR,JN - JN,RAMAL GRAL ROCA,16.0,FFCC ROCA +281,FFCC ROCA,TRE,281,MONTE(D),JN - JN,ROCA_CANUELA-MONTE,16.0,FFCC ROCA +282,FFCC ROCA,TRE,282,KORN(D),JN - JN,ROCA KORN - CHASCOMUS,16.0,FFCC ROCA +283,FFCC ROCA,TRE,283,UNIV(D),JN - JN,ROCA_UNIVERSITARIO,16.0,FFCC ROCA +380,FFCC ROCA,TRE,380,LOBOS(D),JN - JN,ROCA_CANUELAS-LOBOS,16.0,FFCC ROCA +32,SUBTE PREMETRO,SUB,32,LINEA A,JN - JN,RAMAL LINEA A,32.0,SUBTE +33,SUBTE PREMETRO,SUB,33,LINEA B,JN - JN,RAMAL LINEA B,32.0,SUBTE +54,SUBTE PREMETRO,SUB,54,LINEA D,JN - JN,RAMAL LINEA D,32.0,SUBTE +148,SUBTE PREMETRO,SUB,148,LINEA E,JN - JN,RAMAL LINEA E,32.0,SUBTE +220,SUBTE PREMETRO,SUB,220,LINEA H,JN - JN,RAMAL LINEA H,32.0,SUBTE +269,SUBTE PREMETRO,SUB,269,LINEA C,JN - JN,RAMAL LINEA C,32.0,SUBTE +367,SUBTE PREMETRO,SUB,367,PREMETRO,JN - JN,RAMAL_PREMETRO,32.0,SUBTE +48,LINEA 39,COL,48,039A,JN - JN,CHACARITA (CIUDAD AUTONOMA DE BUENOS AIRES) - BARRACAS (CIUDAD AUTONOMA DE BUENOS AIRES),, +117,LINEA 134,COL,117,134A,JN - JN,BARRIO NICOLAS AVELLANEDA - ESTACION DEVOTO,, +137,LINEA 741,COL,137,741A,BUENOS AIRES - JOSE C. PAZ,ESTACION JOSE C PAZ - SOL Y VERDE POR CASTANEDA,, +183,LINEA 108,COL,183,108A,JN - JN,A RETIRO (CIUDAD AUTONOMA DE BUENOS AIRES) - CROACIA y BERGAMINI (PARTIDO DE TRES DE FEBRERO - PROVINCIA DE BUENOS AIRES),, +223,LINEA 59,COL,223,059A,JN - JN,A (por ESTACION LA LUCILA)EST BS AS(LINEA BELGRANO SUR - CABA) - INT CORONEL AMARO AVALOS y DOMINGO DEACASSUSO (PIDO DE VICENTE LOPEZ - PCIA DE BS AS),, +284,FFCC SAN MARTIN,TRE,284,RAMAL_S_,JN - JN,RAMAL TREN SAN MARTIN,, +287,LINEA 318,COL,287,318A,BUENOS AIRES - PROV,FRACC RAMAL A EST LOMAS DE ZAMORA - EST TURDERA,, +293,LINEA 749,COL,293,749A,BUENOS AIRES - JOSE C. PAZ,BARRIO R FAVALORO (ESTACION JOSE C PAZ) ESTACION SOL Y VERDE,, diff --git a/docs/Makefile b/docs/Makefile index d0c3cbf..5f8c3b1 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -3,7 +3,7 @@ # You can set these variables from the command line, and also # from the environment for the first two. -SPHINXOPTS ?= +SPHINXOPTS += SPHINXBUILD ?= sphinx-build SOURCEDIR = source BUILDDIR = build diff --git a/docs/configuraciones.csv b/docs/configuraciones.csv deleted file mode 100644 index 376fdf1..0000000 --- a/docs/configuraciones.csv +++ /dev/null @@ -1,70 +0,0 @@ -orden,item,variable,subvar,subvar_param,default,obligatorio,descripcion_campo,descripcion_general,comments -1.1,archivo_transacciones,nombre_archivo_trx,,,,True,Especificar el archivo con las transacciones a consumir,Bases de datos, -1.2,archivo_transacciones,alias_db_data,,,db_data,True,nombre del sqlite donde se guardan los datos procesados,, -1.3,archivo_transacciones,alias_db_insumos,,,db_insumos,True,nombre del sqlite donde se guardan los insumos generales,, -2.01,variables_trx,nombres_variables_trx,id_trx,,,,columna con id único del archivo de transacciones,Nombre de columnas en el archivo de transacciones, -2.02,variables_trx,nombres_variables_trx,fecha_trx,,,True,columna con fecha de la transacción,, -2.03,variables_trx,nombres_variables_trx,id_tarjeta_trx,,,True,columna con id de la tarjeta,, -2.04,variables_trx,nombres_variables_trx,modo_trx,,,,columna con modo de transporte,, -2.05,variables_trx,nombres_variables_trx,hora_trx,,,,columna con hora de la transacción,, -2.06,variables_trx,nombres_variables_trx,id_linea_trx,,,True,columna con el id de la línea,, -2.07,variables_trx,nombres_variables_trx,id_ramal_trx,,,,columna con el ramal de la línea,, -2.08,variables_trx,nombres_variables_trx,interno_trx,,,,columna con interno de la línea,, -2.09,variables_trx,nombres_variables_trx,orden_trx,,,,columna con el orden de la transacción (si falta hora/minuto en fecha_trx),, -2.1,variables_trx,nombres_variables_trx,latitud_trx,,,True,columna con la latitud de la transacción,, -2.11,variables_trx,nombres_variables_trx,longitud_trx,,,True,columna con longitud de la transacción,, -2.12,variables_trx,nombres_variables_trx,factor_expansion,,,,columna con el factor de expansión,, -2.21,trxs,ordenamiento_transacciones,,,fecha_completa,True,especifica si ordena transacciones por fecha o por variable orden_trx,Parámetros de transacciones, -2.22,trxs,ventana_viajes,,,120,,ventana de tiempo para que una transacción sea de un mismo viaje (ej. 60 minutos),, -2.23,trxs,ventana_duplicado,,,5,,ventana de tiempo si hay duplicado de transacción (ej. Viaje con acompañante),, -2.44,filtro_trx,tipo_trx_invalidas,,True,,,lista con el contenido a eliminar de la variable seleccionada,,ver si poner una variable adicional con nombre de variable -2.45,destino,tolerancia_parada_destino,,,2000,True,Distancia para la validación de los destinos (metros),Imputación de destino, -2.46,destino,imputar_destinos_min_distancia,,,True,True,Busca la parada que minimiza la distancia con respecto a la siguiente trancción,, -2.5,geo_conf,resolucion_h3,,,8,True,Resolución de los hexágonos,Parámetros geográficos, -2.51,geo_conf,epsg_m,,,9265,True,Parámetros geográficos: crs,, -2.6,fechas,formato_fecha,,,%d/%m/%Y %H:%M:%S,,Configuración fecha y hora,,¿cuál es la diferencia con hora_trx -2.61,fechas,columna_hora,,,,,,,¿no debería ser formato de fecha trx? ¿no debería ser diferente a la del gps? -2.7,gps,geolocalizar_trx,,,False,True,,, -2.71,gps,nombre_archivo_gps,,,,,Especificar el archivo con los datos gps de las líneas,, -2.8,vars_gps,nombres_variables_gps,id_gps,,,,,Nombre de columnas en el archivo de GPS, -2.81,vars_gps,nombres_variables_gps,id_linea_gps,,,,,, -2.82,vars_gps,nombres_variables_gps,id_ramal_gps,,,,,, -2.83,vars_gps,nombres_variables_gps,interno_gps,,,,,, -2.84,vars_gps,nombres_variables_gps,fecha_gps,,,,,, -2.85,vars_gps,nombres_variables_gps,latitud_gps,,,,,, -2.86,vars_gps,nombres_variables_gps,longitud_gps,,,,,, -2.87,vars_gps,nombres_variables_gps,service_type_gps,True,,,,, -2.88,vars_gps,nombres_variables_gps,velocity_gps,,,,,, -2.89,vars_gps,nombres_variables_gps,servicios_gps,,,,Indica cuando se abre y cierra un servicio,, -2.9,lineas,nombre_archivo_informacion_lineas,,,,,,Información para procesamiento de líneas, -2.91,lineas,informacion_lineas_contiene_ramales,,,,,,, -2.92,lineas,lineas_contienen_ramales,,,True,True,Especificar si las líneas de colectivo contienen ramales,,"hace falta, no es redudante si ya existe el campo ramal??" -2.93,lineas,nombre_archivo_paradas,,,,,,, -2.94,servicios_gps,utilizar_servicios_gps,,,False,True,Especifica si ve van a utilizar los servicios GPS,Servicios GPS, -2.95,servicios_gps,valor_inicio_servicio,,,,,Valor de la variable que marca el inicio del servicio,, -2.96,servicios_gps,valor_fin_servicio,,,,,Valor de la variable que marca el fin del servicio,, -4.1,modos,modos,autobus,,,,,, -4.2,modos,modos,tren,,,,,, -4.3,modos,modos,metro,,,,,, -4.4,modos,modos,tranvia,,,,,, -4.5,modos,modos,brt,,,,,, -5.1,capas_geo,recorridos_geojson,,,,,,, -5.2,filtros_geo,filtro_latlong_bbox,minx,,,,,, -5.3,filtros_geo,filtro_latlong_bbox,miny,,,,,, -5.4,filtros_geo,filtro_latlong_bbox,maxx,,,,,, -5.5,filtros_geo,filtro_latlong_bbox,maxy,,,,,, -6.1,zonificaciones,zonificaciones,geo1,,,,,Zonificaciones, -6.2,zonificaciones,zonificaciones,var1,,,,,, -6.3,zonificaciones,zonificaciones,orden1,,,,,, -6.4,zonificaciones,zonificaciones,geo2,,,,,, -6.5,zonificaciones,zonificaciones,var2,,,,,, -6.6,zonificaciones,zonificaciones,orden2,,,,,, -6.7,zonificaciones,zonificaciones,geo3,,,,,, -6.8,zonificaciones,zonificaciones,var3,,,,,, -6.9,zonificaciones,zonificaciones,orden3,,,,,, -7,zonificaciones,zonificaciones,geo4,,,,,, -7.1,zonificaciones,zonificaciones,var4,,,,,, -7.2,zonificaciones,zonificaciones,orden4,,,,,, -7.3,zonificaciones,zonificaciones,geo5,,,,,, -7.4,zonificaciones,zonificaciones,var5,,,,,, -7.5,zonificaciones,zonificaciones,orden5,,,,,, diff --git a/docs/configuraciones.xlsx b/docs/configuraciones.xlsx index a5bbeb5..ba0e073 100644 Binary files a/docs/configuraciones.xlsx and b/docs/configuraciones.xlsx differ diff --git a/docs/requirements.txt b/docs/requirements.txt index 483a4e9..10ab4dd 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1 +1,3 @@ sphinx_rtd_theme +sphinx>=6.1.3 +docutils>=0.19 diff --git a/docs/source/configuracion.rst b/docs/source/configuracion.rst index c736d4c..af86807 100644 --- a/docs/source/configuracion.rst +++ b/docs/source/configuracion.rst @@ -173,7 +173,15 @@ Por úlitmo, se puede especificar un archivo con la localización de las paradas nombre_archivo_paradas: -Finalmente se pueden suministrar diferentes archivos con unidades espaciales para las que se quiere agregar datos. Para cada archivo debe indicarse el nombre del archivo, el nombre del atributo que contiene la información y, de ser necesario, un orden en el que se quiera producir las matrices OD que genera UrbanTrips. Estos archivos deben estar ubicados con el resto de los insumos de la ciudad en ``data/data_ciudad/``. + +Parámetros de zonificaciones y polígonos de interés +--------------------------------------------------- + +Se pueden suministrar diferentes archivos con unidades espaciales o zonas de análisis de tránsito para las que se quiere agregar datos. Para cada archivo debe indicarse el nombre del archivo geojson a consumir, el nombre del atributo que contiene la información y, de ser necesario, un orden en el que se quiera producir las matrices OD que genera UrbanTrips. + +Puede haber tantos archivos como lo desee. Si existen estructuras anidadas (por ejemplo unidades censales de diferente nivel de agregación) se puede usar el mismo archivo, con diferentes atributos o columnas indicando los diferentes ids o valores para cada nivel de agregación. Luego se para el mismo archivo indicando en `var` qué atributo o columna tomar. + +Estos archivos deben estar ubicados con el resto de los insumos de la ciudad en ``data/data_ciudad/``. .. code:: @@ -183,4 +191,14 @@ Finalmente se pueden suministrar diferentes archivos con unidades espaciales par orden1: ['CABA', 'Primer cordón', 'Segundo cordón', 'Tercer cordón', 'RMBA'] geo2: hexs_amba.geojson var2: Partido + + +Al mismo tiempo, si se quiere realizar un análisis de patrones de orígenes y destinos para una determinada zona de interés, se puede suministrar otro archivo geojson donde se especifique una capa geográfica de polígonos en formato con las siguientes columnas `'id', 'tipo', 'geometry'`. `id` debe ser un texto con el nombre del polígono de interés y `tipo` puede ser `poligono` o `cuenca`. El primero hace referencia a una zona particular de interés como un centro de transbordo o un barrio-localidad. El segundo hace referencia a una zona de mayor tamaño que puede agrupar el recorrido de un grupo de líneas o ser el área de influencia de una línea en particular. + +El archivo puede contener la cantidad de polígonos (o cuencas) que se desee (ya sena polígnos o multi-polígonos), el proceso corre en forma independiente para cada poligoo o cuenca. Estos archivos deben estar ubicados con el resto de los insumos de la ciudad en ``data/data_ciudad/``. Estos archivos deben informarse en el archivo de configuraciones del siguiente modo. Los resultados se verán en el Dashboard. + + +.. code:: + + poligonos: "[NOMBRE_DEL_ARCHIVO].geojson" diff --git a/docs/source/index.rst b/docs/source/index.rst index d4a5c70..37d958e 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -9,7 +9,7 @@ Bienvenidos a la documentación de Urbantrips! UrbanTrips es una biblioteca de código abierto que toma información de un sistema de pago con tarjeta inteligente de transporte público, infiere destinos de las etapas, construye cadenas de viaje para cada usuario y produce matrices de origen-destino y otros indicadores operativos. El principal objetivo de la librería es producir insumos útiles para la gestión del transporte público a partir de requerimientos mínimos de información y pre-procesamiento. Con sólo una tabla geolocalizada de transacciones proveniente de un sistema de pago electrónico, se podrán generar resultados, que serán más precisos cuanta más información adicional se incorpore al proceso a través de los archivos opcionales. El proceso elabora las matrices, los indicadores y construye una serie de gráficos y mapas útiles para la planificación y fiscalización del transporte público. -Para una discusión metodológica de cómo se imputan destinos y se construye la matriz de origen y destino se puede consultar este `documento metodológico `_ +Para una discusión metodológica de cómo se imputan destinos y se construye la matriz de origen y destino se puede consultar este `documento metodológico `_ Esta documentación guiará al usuario a través del proceso de instalación y configuración del ambiente para correr UrbanTrips. Luego, la sección :doc:`primeros_pasos`, ofrecerá un tutorial básico de cómo utilizar la librería para correr un set de datos abiertos de ejemplo. También ofrecerá un definición del modelo de datos de los archivos que UrbanTrips toma como insumos (:doc:`inputs`), en particular dará detalles de cómo setear el archivo de configuración (:doc:`configuracion`). Por último ofrecerá detalles de la concepción que UrbanTrips tiene de las líneas y ramales (:doc:`lineas_ramales`) y una descripción del modelo de datos final con los resultados de la librería (:doc:`resultados`). diff --git a/docs/source/inputs.rst b/docs/source/inputs.rst index 6b6d710..1d67457 100644 --- a/docs/source/inputs.rst +++ b/docs/source/inputs.rst @@ -192,3 +192,29 @@ Tabla que contenga las paradas de cada linea y ramal (si hay ramales). El campo - int - **Obligatorio**. Identifica con el mismo id estaciones donde puede haber transbordo entre ramales de una misma linea. Único para los otros casos dentro de la misma línea. + +Zonificaciones +-------------- + +Tabla que contenga las zonificaciones o zonas de análisis de tránsito para las que se quieran agregar datos. No existe una esquema de datos definido, puede tener cualquier columna o atributo y la cantidad que se desee, siempre que se especifique correctamente en el archivo de configuración. + +Polígonos de interés +-------------------- + +.. list-table:: + :widths: 25 25 50 + :header-rows: 1 + + * - id + - tipo + - geometry + * - *id* + - str + - **Obligatorio**. Texto que identifique con un nombre al polígono de interés. + * - *tipo* + - str + - Debe identificar si se trata de un polígono de interés o de una cuenca. Debe tomar valores `poligono` o `cuenca`. + * - *geometry* + - Polygon o MultiPolygon + - Polígono de la zona de interés. + diff --git a/docs/source/instalacion.rst b/docs/source/instalacion.rst index 87adcc6..7775f84 100644 --- a/docs/source/instalacion.rst +++ b/docs/source/instalacion.rst @@ -7,7 +7,7 @@ Para poder instalar la librería se aconseja crear un ambiente y luego instalar .. code:: sh $ virtualenv venv --python=python3.10 - (.venv) $ source venv/bin/activate + $ source venv/bin/activate (.venv) $ pip install urbantrips Si desea hacerlo con `conda` entonces: diff --git a/docs/source/primeros_pasos.rst b/docs/source/primeros_pasos.rst index 87c9e2b..93b96cc 100644 --- a/docs/source/primeros_pasos.rst +++ b/docs/source/primeros_pasos.rst @@ -148,7 +148,13 @@ Esta es la estructura de directorios de UrbanTrips. ``configs/`` guarda el archi Correr Urbantrips ----------------- -Una vez que se dispone del archivo de transacciones y el de información de las líneas, es posible comenzar a utilizar UrbanTrips. En primer lugar es necesario inicializar los directorios y la base de datos necesarios. Este paso solo se corre una vez. +Una vez que se dispone del archivo de transacciones y el de información de las líneas, es posible comenzar a utilizar UrbanTrips. Para una corrida del conjunto del proceso puede simplemente correr el comando copiado debajo. Puede reemplazar el archivo de configuración que viene por el que dice `configuraciones_generales_2019_m1.yaml` y tendrá una corrida para una muestra del 1% de los datos de área urbana de Buenos Aires para 2019. + +.. code:: sh + + $ python urbantrips/run_all_urbantrips.py + +También puede correr los siguientes procesos de manera independiente. En primer lugar es necesario inicializar los directorios y la base de datos necesarios. Este paso solo se corre una vez. .. code:: sh @@ -160,13 +166,21 @@ Luego, se puede procesar la información de transacciones. Este archivo de trans $ python urbantrips/process_transactions.py -Por último, una vez procesadas todas las transacciones que sean de interés y cargadas en la base de datos de la libería, es posible correr los pasos de post procesamiento sobre esa información, como los KPI, visualizaciones y exportación de resultados. +Una vez procesadas todas las transacciones que sean de interés y cargadas en la base de datos de la libería, es posible correr los pasos de post procesamiento sobre esa información, como los KPI, visualizaciones y exportación de resultados. .. code:: sh $ python urbantrips/run_postprocessing.py +Por último, todos estos estadísticos pueden expresarse en diferente tipo de visualizaciones estáticas y dinámicas, como así también un dashboard interactivo. + +.. code:: sh + + $ python urbantrips/create_viz.main() + $ python urbantrips/run_dashboard.main() + + Resultados finales ------------------ diff --git a/notebooks/Service id classification analysis.ipynb b/notebooks/Service id classification analysis.ipynb index 79bc4a0..942fd58 100644 --- a/notebooks/Service id classification analysis.ipynb +++ b/notebooks/Service id classification analysis.ipynb @@ -188,7 +188,7 @@ "outputs": [], "source": [ "line_stops_gdf.explore(column = 'id_ramal',\n", - " tiles=\"Stamen Toner\",\n", + " tiles=\"CartoDB positron\",categorical = True,\n", " cmap = 'tab10',\n", " marker_kwds = {'radius':10})" ] @@ -297,15 +297,21 @@ "outputs": [], "source": [ "m = line_stops_gdf.explore(column = 'id_ramal',\n", - " tiles=\"Stamen Toner\",\n", + " tiles=\"CartoDB positron\",categorical = True,\n", " cmap = 'tab10',\n", " marker_kwds = {'radius':3}, name = 'Paradas')\n", "\n", "service_gps.explore(m=m,column = 'service_id',\n", - " tiles=\"Stamen Toner\",\n", + " tiles=\"CartoDB positron\",categorical = True,\n", " cmap = 'tab10',\n", " marker_kwds = {'radius':10}, name = 'GPS')\n", "\n", + "if service_gps.change.any():\n", + " service_gps.query(\"change==True\").explore(m=m,color = 'red',\n", + " tiles=\"CartoDB positron\",\n", + " cmap = 'tab10',\n", + " marker_kwds = {'radius':10}, name = 'Service id change')\n", + "\n", "folium.LayerControl().add_to(m)\n", "\n", "m" @@ -386,12 +392,12 @@ "outputs": [], "source": [ "m = line_stops_gdf.explore(column = 'id_ramal',\n", - " tiles=\"Stamen Toner\",\n", + " tiles=\"CartoDB positron\",categorical = True,\n", " cmap = 'tab10',\n", " marker_kwds = {'radius':3}, name = 'Paradas')\n", "\n", "service_gps.explore(m=m,column = 'service_id',\n", - " tiles=\"Stamen Toner\",\n", + " tiles=\"CartoDB positron\",categorical = True,\n", " cmap = 'tab10',\n", " marker_kwds = {'radius':10}, name = 'GPS')\n", "\n", @@ -513,12 +519,12 @@ "outputs": [], "source": [ "m = stops_to_change_gdf.explore(column = 'id_ramal',\n", - " tiles=\"Stamen Toner\",\n", + " tiles=\"CartoDB positron\",categorical = True,\n", " cmap = 'tab10',\n", " marker_kwds = {'radius':3}, name = 'Paradas')\n", "\n", "new_service_gps.explore(m=m,column = 'service_id',\n", - " tiles=\"Stamen Toner\",\n", + " tiles=\"CartoDB positron\",categorical = True,\n", " cmap = 'tab10',\n", " marker_kwds = {'radius':10}, name = 'GPS')\n", "\n", @@ -647,7 +653,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.6" + "version": "3.10.12" } }, "nbformat": 4, diff --git a/notebooks/Stops and nodes creation helper.ipynb b/notebooks/Stops and nodes creation helper.ipynb index 9d69efa..0543002 100644 --- a/notebooks/Stops and nodes creation helper.ipynb +++ b/notebooks/Stops and nodes creation helper.ipynb @@ -139,7 +139,8 @@ "metadata": {}, "outputs": [], "source": [ - "stops_gdf.explore(column = 'id_linea', tiles=\"Stamen Toner\", marker_kwds = {'radius':10}, cmap='Set2')" + "stops_gdf.explore(column = 'id_linea', categorical = True,tiles=\"CartoDB positron\",\n", + " marker_kwds = {'radius':10}, cmap='Set2')" ] }, { @@ -162,7 +163,9 @@ "outputs": [], "source": [ "id_ramal = 0\n", - "stops_gdf.query(f\"id_ramal == {id_ramal}\").explore(column = 'branch_stop_order', tiles=\"Stamen Toner\", marker_kwds = {'radius':10})" + "stops_gdf.query(f\"id_ramal == {id_ramal}\").explore(column = 'branch_stop_order',\n", + " categorical = True,tiles=\"CartoDB positron\",\n", + " marker_kwds = {'radius':10})" ] }, { @@ -214,7 +217,7 @@ "# GDF para visualizar\n", "geometry = gpd.points_from_xy(stops_with_node_id['node_x'], stops_with_node_id['node_y'], crs= 'EPSG:4326')\n", "stops_with_node_id_gdf = gpd.GeoDataFrame(stops_with_node_id,geometry=geometry,crs = f\"EPSG:4326\")\n", - "stops_with_node_id_gdf.explore(color = 'red',tiles=\"Stamen Toner\", marker_kwds = {'radius':10})" + "stops_with_node_id_gdf.explore(color = 'red',tiles=\"CartoDB positron\", marker_kwds = {'radius':10})" ] }, { @@ -369,7 +372,7 @@ " .query(\"id_linea == 143\")\\\n", " .explore(\n", " column = 'id_ramal',\n", - " tiles=\"Stamen Toner\",\n", + " tiles=\"CartoDB positron\",categorical = True,\n", " marker_kwds = {'radius':10},\n", " cmap='Paired'\n", " )" @@ -399,7 +402,7 @@ " .query(\"id_linea == 143\")\\\n", " .explore(\n", " column = 'branch_stop_order',\n", - " tiles=\"Stamen Toner\",\n", + " tiles=\"CartoDB positron\",\n", " marker_kwds = {'radius':10}\n", " )" ] @@ -462,7 +465,7 @@ " crs = f\"EPSG:4326\")\n", "\n", "line_stops_gdf.query(\"id_linea == 143\").explore(column = 'node_id',\n", - " tiles=\"Stamen Toner\",\n", + " tiles=\"CartoDB positron\",\n", " marker_kwds = {'radius':10})" ] }, @@ -579,7 +582,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.6" + "version": "3.10.12" } }, "nbformat": 4, diff --git a/requirements.txt b/requirements.txt index 43f57d4..237495b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,24 +1,34 @@ -openpyxl -osmnx>=1.3.0 -geopandas>=0.12.2 -shapely>=2.0.1 -pandas>=2.0.2 -contextily -h3 < 4 -mapclassify -weightedstats -pyyaml -libpysal>=4.7.0 -statsmodels -patsy -folium -seaborn -IPython -matplotlib-scalebar -numba -streamlit -streamlit_folium -Pillow -plotly -notebook -jupyterlab \ No newline at end of file +contextily==1.4.0 +folium==0.14.0 +geopandas==0.14.0 +fiona==1.9.6 +h3==3.7.6 +ipython==8.16.1 +jupyterlab==4.0.6 +libpysal==4.8.0 +mapclassify==2.6.1 +matplotlib==3.8.3 +matplotlib-scalebar==0.8.1 +notebook==7.0.4 +numba==0.58.0 +numpy==1.25.2 +openpyxl==3.1.2 +osmnx==1.8.0 +pandas==2.1.1 +pandana==0.7 +patsy==0.5.3 +Pillow==9.5.0 +plotly==5.17.0 +python-pptx==0.6.22 +PyYAML==6.0.1 +pysal==24.1 +seaborn==0.13.0 +shapely==2.0.1 +statsmodels==0.14.0 +streamlit==1.27.2 +streamlit-folium==0.15.0 +weightedstats==0.4.1 +OSMnet==0.1.7 +anyio<4 +platformdirs==4.2.2 +typing_extensions==4.12.2 diff --git a/setup.py b/setup.py index 7866f6c..dad09b7 100644 --- a/setup.py +++ b/setup.py @@ -22,23 +22,29 @@ ], python_requires='>=3.10', install_requires=[ + 'anyio<4', 'contextily==1.4.0', 'folium==0.14.0', + 'fiona==1.9.6', 'geopandas==0.14.0', + 'fiona==1.9.6', 'h3==3.7.6', 'ipython==8.16.1', 'jupyterlab==4.0.6', 'libpysal==4.8.0', 'mapclassify==2.6.1', + 'matplotlib==3.8.3', 'matplotlib-scalebar==0.8.1', 'notebook==7.0.4', 'numba==0.58.0', 'numpy==1.25.2', 'openpyxl==3.1.2', - 'osmnx==1.6.0', + 'osmnx==1.8.0', 'pandas==2.1.1', + 'pandana==0.7', 'patsy==0.5.3', 'Pillow==9.5.0', + 'platformdirs==4.2.2', 'plotly==5.17.0', 'python-pptx==0.6.22', 'PyYAML==6.0.1', @@ -47,5 +53,8 @@ 'statsmodels==0.14.0', 'streamlit==1.27.2', 'streamlit-folium==0.15.0', - 'weightedstats==0.4.1'] + 'typing_extensions==4.12.2', + 'weightedstats==0.4.1', + 'OSMnet==0.1.7' +] ) diff --git a/urbantrips/carto/carto.py b/urbantrips/carto/carto.py index 1b88c71..261165d 100644 --- a/urbantrips/carto/carto.py +++ b/urbantrips/carto/carto.py @@ -1,5 +1,8 @@ from datetime import datetime import networkx as nx +import multiprocessing +from functools import partial +from math import sqrt import osmnx as ox import pandas as pd from pandas.io.sql import DatabaseError @@ -9,9 +12,7 @@ import geopandas as gpd import h3 from networkx import NetworkXNoPath -import multiprocessing -from functools import partial -from math import sqrt +from pandana.loaders import osm as osm_pandana from urbantrips.geo.geo import ( get_stop_hex_ring, h3togeo, add_geometry, create_voronoi, normalizo_lat_lon, h3dist, bring_latlon @@ -23,6 +24,15 @@ leer_configs_generales, leer_alias) +import subprocess + +def get_library_version(library_name): + result = subprocess.run(["pip", "show", library_name], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) + if result.returncode == 0: + for line in result.stdout.split('\n'): + if line.startswith("Version:"): + return line.split(":")[1].strip() + return None @duracion def update_stations_catchment_area(ring_size): @@ -122,7 +132,7 @@ def create_zones_table(): """, conn_insumos, ) - except DatabaseError as e: + except DatabaseError: print("No existe la tabla zonas en la base") zonas_ant = pd.DataFrame([]) @@ -336,14 +346,11 @@ def create_distances_table(use_parallel=False): y calcula diferentes distancias para cada par que no tenga """ - configs = leer_configs_generales() - resolucion_h3 = configs["resolucion_h3"] - distancia_entre_hex = h3.edge_length(resolution=resolucion_h3, unit="km") - distancia_entre_hex = distancia_entre_hex * 2 - conn_insumos = iniciar_conexion_db(tipo='insumos') conn_data = iniciar_conexion_db(tipo='data') + print('Verifica viajes sin distancias calculadas') + q = """ select distinct h3_o,h3_d from viajes @@ -356,6 +363,7 @@ def create_distances_table(use_parallel=False): WHERE h3_d != '' ) """ + pares_h3_data = pd.read_sql_query(q, conn_data) q = """ @@ -375,10 +383,6 @@ def create_distances_table(use_parallel=False): pares_h3_norm = normalizo_lat_lon(pares_h3) # usa la función osmnx para distancias en caso de error con Pandana - print('') - print('No se pudo usar la librería pandana. ') - print('Se va a utilizar osmnx para el cálculo de distancias') - print('') print("Este proceso puede demorar algunas horas dependiendo del tamaño " + " de la ciudad y si se corre por primera vez por lo que en la base" + " de insumos no estan estos pares") @@ -391,31 +395,18 @@ def create_distances_table(use_parallel=False): f"Hay {len(agg2_total)} nuevos pares od para sumar a tabla distancias") print(f"de los {len(pares_h3_data)} originales en la data.") print('') + print('Procesa distancias con Pandana') + + agg2 = compute_distances_osm( + agg2_total, + h3_o="h3_o_norm", + h3_d="h3_d_norm", + processing="pandana", + modes=["drive"], + use_parallel=False + ) - # Determine the size of each chunk (500 rows in this case) - chunk_size = 25000 - - # Get the total number of rows in the DataFrame - total_rows = len(agg2_total) - - # Loop through the DataFrame in chunks of 500 rows - for start in range(0, total_rows, chunk_size): - end = start + chunk_size - # Select the chunk of 500 rows from the DataFrame - agg2 = agg2_total.iloc[start:end].copy() - # Call the process_chunk function with the selected chunk - print( - f'Bajando distancias entre {start} a {end} de {len(agg2_total)} - {str(datetime.now())[:19]}') - - agg2 = calculo_distancias_osm( - agg2, - h3_o="h3_o_norm", - h3_d="h3_d_norm", - distancia_entre_hex=distancia_entre_hex, - processing="osmnx", - modes=["drive"], - use_parallel=use_parallel - ) + if len(agg2) > 0: dist1 = agg2.copy() dist1['h3_o'] = dist1['h3_o_norm'] @@ -435,54 +426,138 @@ def create_distances_table(use_parallel=False): distancias_new.to_sql("distancias", conn_insumos, if_exists="append", index=False) - conn_insumos.close() - conn_insumos = iniciar_conexion_db(tipo='insumos') + else: + print('Procesa distancias con OSMNX') + # Determine the size of each chunk (500 rows in this case) + chunk_size = 25000 + + # Get the total number of rows in the DataFrame + total_rows = len(agg2_total) + + # Loop through the DataFrame in chunks of 500 rows + for start in range(0, total_rows, chunk_size): + end = start + chunk_size + # Select the chunk of 500 rows from the DataFrame + agg2 = agg2_total.iloc[start:end].copy() + # Call the process_chunk function with the selected chunk + print( + f'Bajando distancias entre {start} a {end} de {len(agg2_total)} - {str(datetime.now())[:19]}') + + agg2 = compute_distances_osm( + agg2, + h3_o="h3_o_norm", + h3_d="h3_d_norm", + processing="osmnx", + modes=["drive"], + use_parallel=use_parallel + ) + + dist1 = agg2.copy() + dist1['h3_o'] = dist1['h3_o_norm'] + dist1['h3_d'] = dist1['h3_d_norm'] + dist2 = agg2.copy() + dist2['h3_d'] = dist2['h3_o_norm'] + dist2['h3_o'] = dist2['h3_d_norm'] + distancias_new = pd.concat([dist1, dist2], ignore_index=True) + distancias_new = distancias_new\ + .groupby(['h3_o', + 'h3_d', + 'h3_o_norm', + 'h3_d_norm'], + as_index=False)[['distance_osm_drive', + 'distance_h3']].first() + + distancias_new.to_sql("distancias", conn_insumos, + if_exists="append", index=False) + + conn_insumos.close() + conn_insumos = iniciar_conexion_db(tipo='insumos') conn_insumos.close() conn_data.close() -def calculo_distancias_osm( - df, - origin="", - destination="", - lat_o_tmp="", - lon_o_tmp="", - lat_d_tmp="", - lon_d_tmp="", - h3_o="", - h3_d="", - processing="osmnx", - modes=["drive", "walk"], - distancia_entre_hex=1, - use_parallel=False -): +def compute_distances_osmx(df, mode, use_parallel): + """ + Takes a dataframe with pairs of h3 with origins and destinations + and computes distances between those pairs using OSMNX. + + Parameters + ---------- + df : pandas.DataFrame + DataFrame representing a chunk with OD pairs + with h3 indexes + modes: list + list of modes to compute distances for. Must be a valid + network_type parameter for either osmnx graph_from_bbox + or pandana pdna_network_from_bbox + use_parallel: bool + use parallel processing when computin omsnx distances + + Returns + ------- + pandas.DataFrame + DataFrame containing od pairs with distances + """ + print("Computando distancias entre pares OD con OSMNX") + ymin, xmin, ymax, xmax = ( + min(df["lat_o_tmp"].min(), df["lat_d_tmp"].min()), + min(df["lon_o_tmp"].min(), df["lon_d_tmp"].min()), + max(df["lat_o_tmp"].max(), df["lat_d_tmp"].max()), + max(df["lon_o_tmp"].max(), df["lon_d_tmp"].max()), + ) + xmin -= 0.2 + ymin -= 0.2 + xmax += 0.2 + ymax += 0.2 - cols = df.columns.tolist() + G = ox.graph_from_bbox(ymax, ymin, xmax, xmin, network_type=mode) + G = ox.add_edge_speeds(G) + G = ox.add_edge_travel_times(G) + + nodes_from = ox.distance.nearest_nodes( + G, df['lon_o_tmp'].values, df['lat_o_tmp'].values, return_dist=True + ) + + nodes_to = ox.distance.nearest_nodes( + G, df['lon_d_tmp'].values, df['lat_d_tmp'].values, return_dist=True + ) + nodes_from = nodes_from[0] + nodes_to = nodes_to[0] + + if use_parallel: + results = run_network_distance_parallel( + mode, G, nodes_from, nodes_to) + df[f"distance_osm_{mode}"] = results + + else: + df = run_network_distance_not_parallel( + df, mode, G, nodes_from, nodes_to) + return df - if len(lat_o_tmp) == 0: - lat_o_tmp = "lat_o_tmp" - if len(lon_o_tmp) == 0: - lon_o_tmp = "lon_o_tmp" - if len(lat_d_tmp) == 0: - lat_d_tmp = "lat_d_tmp" - if len(lon_d_tmp) == 0: - lon_d_tmp = "lon_d_tmp" - - if (lon_o_tmp not in df.columns) | (lat_o_tmp not in df.columns): - if (origin not in df.columns) & (len(h3_o) > 0): - origin = "origin" - df[origin] = df[h3_o].apply(h3togeo) - df["lon_o_tmp"] = df[origin].apply(bring_latlon, latlon='lon') - df["lat_o_tmp"] = df[origin].apply(bring_latlon, latlon='lat') - - if (lon_d_tmp not in df.columns) | (lat_d_tmp not in df.columns): - if (destination not in df.columns) & (len(h3_d) > 0): - destination = "destination" - df[destination] = df[h3_d].apply(h3togeo) - df["lon_d_tmp"] = df[destination].apply(bring_latlon, latlon='lon') - df["lat_d_tmp"] = df[destination].apply(bring_latlon, latlon='lat') +def compute_distances_pandana(df, mode): + """ + Takes a dataframe with pairs of h3 with origins and destinations + and computes distances between those pairs using pandana. + + Parameters + ---------- + df : pandas.DataFrame + DataFrame representing a chunk with OD pairs + with h3 indexes + modes: list + list of modes to compute distances for. Must be a valid + network_type parameter for either osmnx graph_from_bbox + or pandana pdna_network_from_bbox + + Returns + ------- + pandas.DataFrame + DataFrame containing od pairs with distances + """ + + print("Computando distancias entre pares OD con Pandana") ymin, xmin, ymax, xmax = ( min(df["lat_o_tmp"].min(), df["lat_d_tmp"].min()), min(df["lon_o_tmp"].min(), df["lon_d_tmp"].min()), @@ -494,40 +569,96 @@ def calculo_distancias_osm( xmax += 0.2 ymax += 0.2 - var_distances = [] + network = osm_pandana.pdna_network_from_bbox( + ymin, xmin, ymax, xmax, network_type=mode) - for mode in modes: + df['node_from'] = network.get_node_ids( + df['lon_o_tmp'], df['lat_o_tmp']).values + df['node_to'] = network.get_node_ids( + df['lon_d_tmp'], df['lat_d_tmp']).values + df[f'distance_osm_{mode}'] = network.shortest_path_lengths( + df['node_to'].values, df['node_from'].values) + return df - # print(f"Descarga de red - Coords OSM {mode} - ymin, xmin, ymax, xmax, - {str(datetime.now())[:19]}") - G = ox.graph_from_bbox(ymax, ymin, xmax, xmin, network_type=mode) - G = ox.add_edge_speeds(G) - G = ox.add_edge_travel_times(G) +def compute_distances_osm( + df, + h3_o="", + h3_d="", + processing="pandana", + modes=["drive", "walk"], + use_parallel=False): + """ + Takes a dataframe with pairs of h3 with origins and destinations + and computes distances between those pairs. + + Parameters + ---------- + df : pandas.DataFrame + DataFrame representing a chunk with OD pairs + with h3 indexes + h3_o: str (h3Index) + origin h3 index + h3_d: str (h3Index) + destination h3 index + processing: str + processing method, either use 'osmnx' or 'pandana' + modes: list + list of modes to compute distances for. Must be a valid + network_type parameter for either osmnx graph_from_bbox + or pandana pdna_network_from_bbox + use_parallel: bool + use parallel processing when computin omsnx distances + + Returns + ------- + pandas.DataFrame + DataFrame containing od pairs with distances + """ - nodes_from = ox.distance.nearest_nodes( - G, df[lon_o_tmp].values, df[lat_o_tmp].values, return_dist=True - ) + cols = df.columns.tolist() - nodes_to = ox.distance.nearest_nodes( - G, df[lon_d_tmp].values, df[lat_d_tmp].values, return_dist=True - ) - nodes_from = nodes_from[0] - nodes_to = nodes_to[0] + df["origin"] = df[h3_o].apply(h3togeo) + df["lon_o_tmp"] = df["origin"].apply(bring_latlon, latlon='lon') + df["lat_o_tmp"] = df["origin"].apply(bring_latlon, latlon='lat') - if use_parallel: - results = run_network_distance_parallel( - mode, G, nodes_from, nodes_to) - df[f"distance_osm_{mode}"] = results + df["destination"] = df[h3_d].apply(h3togeo) + df["lon_d_tmp"] = df["destination"].apply(bring_latlon, latlon='lon') + df["lat_d_tmp"] = df["destination"].apply(bring_latlon, latlon='lat') - else: - df = run_network_distance_not_parallel( - df, mode, G, nodes_from, nodes_to) + var_distances = [] + + for mode in modes: - var_distances += [f"distance_osm_{mode}"] - df[f"distance_osm_{mode}"] = ( - df[f"distance_osm_{mode}"] / 1000).round(2) + if processing == 'osmnx': + # computing distances with osmnx + df = compute_distances_osmx(df=df, mode=mode, + use_parallel=use_parallel) - # print("") + else: + try: + # computing distances with pandana + df = compute_distances_pandana(df=df, mode=mode) + except: + print("No es posible computar distancias con pandana") + library_name = "Pandana" + version = get_library_version(library_name) + if version: + print(f"{library_name} version {version} is installed.") + else: + print(f"{library_name} is not installed.") + + library_name = "OSMnet" + version = get_library_version(library_name) + if version: + print(f"{library_name} version {version} is installed.") + else: + print(f"{library_name} is not installed.") + return pd.DataFrame([]) + + var_distances += [f"distance_osm_{mode}"] + df[f"distance_osm_{mode}"] = ( + df[f"distance_osm_{mode}"] / 1000).round(2) condition = ('distance_osm_drive' in df.columns) & ( 'distance_osm_walk' in df.columns) @@ -543,11 +674,17 @@ def calculo_distancias_osm( df = df[cols + var_distances].copy() + # get distance between h3 cells + configs = leer_configs_generales() + h3_res = configs["resolucion_h3"] + distance_between_hex = h3.edge_length(resolution=h3_res, unit="km") + distance_between_hex = distance_between_hex * 2 + if (len(h3_o) > 0) & (len(h3_d) > 0): df["distance_h3"] = df[[h3_o, h3_d]].apply( h3dist, axis=1, - distancia_entre_hex=distancia_entre_hex, + distancia_entre_hex=distance_between_hex, h3_o=h3_o, h3_d=h3_d ) @@ -601,15 +738,10 @@ def run_network_distance_parallel(mode, G, nodes_from, nodes_to): n = len(nodes_from) chunksize = int(sqrt(n) * 10) - # print(f'Comenzando a correr distancias para {n} pares OD', - # datetime.now().strftime("%H:%M:%S")) - with multiprocessing.Pool(processes=n_cores) as pool: results = pool.map(partial(get_network_distance_osmnx, G=G), zip( nodes_from, nodes_to), chunksize=chunksize) - # print('Distancias calculadas:', datetime.now().strftime("%H:%M:%S")) - return results diff --git a/urbantrips/carto/stops.py b/urbantrips/carto/stops.py index 6033c0f..6815d1f 100644 --- a/urbantrips/carto/stops.py +++ b/urbantrips/carto/stops.py @@ -5,8 +5,7 @@ import libpysal from urbantrips.carto import carto from urbantrips.geo import geo -from urbantrips.utils.utils import ( - duracion, iniciar_conexion_db, leer_configs_generales) +from urbantrips.utils.utils import duracion, iniciar_conexion_db, leer_configs_generales @duracion @@ -16,36 +15,49 @@ def create_stops_table(): stops table in the db """ configs = leer_configs_generales() - stops_file_name = 'stops.csv' - if 'nombre_archivo_paradas' in configs: - if configs['nombre_archivo_paradas'] is not None: - stops_file_name = configs['nombre_archivo_paradas'] + if "nombre_archivo_paradas" in configs: + if configs["nombre_archivo_paradas"] is not None: + stops_file_name = configs["nombre_archivo_paradas"] + stops_path = os.path.join("data", "data_ciudad", stops_file_name) + print("Leyendo stops", stops_file_name) - stops_path = os.path.join("data", "data_ciudad", stops_file_name) + if os.path.isfile(stops_path): + stops = pd.read_csv(stops_path) + upload_stops_table(stops) + else: + print( + "No existe un archivo de stops. Puede utilizar " + "notebooks/stops_creation_with_node_id_helper.ipynb" + "para crearlo a partir de los recorridos" + ) - if os.path.isfile(stops_path): - stops = pd.read_csv(stops_path) - upload_stops_table(stops) - else: - print("No existe un archivo de stops. Puede utilizar " - "notebooks/stops_creation_with_node_id_helper.ipynb" - "para crearlo a partir de los recorridos" - ) + # upload trave times between stations + if configs["tiempos_viaje_estaciones"] is not None: + upload_travel_times_stations() def upload_stops_table(stops): """ Reads a stops table, checks it and uploads it to db """ - conn = iniciar_conexion_db(tipo='insumos') - cols = ['id_linea', 'id_ramal', 'node_id', 'branch_stop_order', - 'stop_x', 'stop_y', 'node_x', 'node_y'] + conn = iniciar_conexion_db(tipo="insumos") + cols = [ + "id_linea", + "id_ramal", + "node_id", + "branch_stop_order", + "stop_x", + "stop_y", + "node_x", + "node_y", + ] stops = stops.reindex(columns=cols) assert not stops.isna().any().all(), "Hay datos faltantes en stops" print("Subiendo paradas a stops") stops.to_sql("stops", conn, if_exists="replace", index=False) + conn.close() def create_temporary_stops_csv_with_node_id(geojson_path): @@ -77,8 +89,7 @@ def create_temporary_stops_csv_with_node_id(geojson_path): stops_df = aggregate_line_stops_to_node_id(stops_gdf) data_path = os.path.join("data", "data_ciudad") - stops_df.to_csv(os.path.join(data_path, - "temporary_stops.csv"), index=False) + stops_df.to_csv(os.path.join(data_path, "temporary_stops.csv"), index=False) def create_line_stops_equal_interval(geojson_path): @@ -106,8 +117,8 @@ def create_line_stops_equal_interval(geojson_path): geo.check_all_geoms_linestring(geojson_data) # if there is no branch_id create - if 'id_ramal' not in geojson_data.columns: - geojson_data['id_ramal'] = None + if "id_ramal" not in geojson_data.columns: + geojson_data["id_ramal"] = None # Project in meters epsg_m = geo.get_epsg_m() @@ -116,8 +127,16 @@ def create_line_stops_equal_interval(geojson_path): stops_gdf = interpolate_stops_every_x_meters(geojson_data) stops_gdf = stops_gdf.reindex( - columns=['id_linea', 'id_ramal', 'branch_stop_order', - 'line_stops_buffer', 'x', 'y', 'geometry']) + columns=[ + "id_linea", + "id_ramal", + "branch_stop_order", + "line_stops_buffer", + "x", + "y", + "geometry", + ] + ) stops_gdf = stops_gdf.to_crs(epsg=4326) return stops_gdf @@ -141,14 +160,13 @@ def interpolate_stops_every_x_meters(gdf): line_stops_buffer = row.line_stops_buffer line_stops_data = create_stops_from_route_geom( - route_geom=route_geom, - stops_distance=stops_distance + route_geom=route_geom, stops_distance=stops_distance ) # Add line_id to the stops data - line_stops_data['id_linea'] = row.id_linea - line_stops_data['id_ramal'] = row.id_ramal - line_stops_data['line_stops_buffer'] = line_stops_buffer + line_stops_data["id_linea"] = row.id_linea + line_stops_data["id_ramal"] = row.id_ramal + line_stops_data["line_stops_buffer"] = line_stops_buffer # Add the stops data to the overall stops data list stops_data.append(line_stops_data) @@ -177,10 +195,11 @@ def aggregate_line_stops_to_node_id(stops_gdf): """ # Add node_id for each line - stops_df = stops_gdf\ - .groupby('id_linea', as_index=False)\ - .apply(create_node_id)\ + stops_df = ( + stops_gdf.groupby("id_linea", as_index=False) + .apply(create_node_id) .reset_index(drop=True) + ) return stops_df @@ -208,15 +227,12 @@ def create_stops_from_route_geom(route_geom, stops_distance): ranges = list(range(0, int(route_geom.length), stops_distance)) stop_points = line_interpolate_point(route_geom, ranges).tolist() - stops_df = pd.DataFrame(range(len(stop_points)), - columns=['branch_stop_order']) - stops_df = gpd.GeoDataFrame( - stops_df, geometry=stop_points, - crs=f"EPSG:{epsg_m}") + stops_df = pd.DataFrame(range(len(stop_points)), columns=["branch_stop_order"]) + stops_df = gpd.GeoDataFrame(stops_df, geometry=stop_points, crs=f"EPSG:{epsg_m}") geom_wgs84 = stops_df.geometry.to_crs(epsg=4326) - stops_df['x'] = geom_wgs84.x - stops_df['y'] = geom_wgs84.y + stops_df["x"] = geom_wgs84.x + stops_df["y"] = geom_wgs84.y return stops_df @@ -243,29 +259,76 @@ def create_node_id(line_stops_gdf): gdf = line_stops_gdf.copy() connectivity = libpysal.weights.fuzzy_contiguity( - gdf=gdf, - buffering=True, - drop=False, - buffer=buffer, - predicate='intersects') + gdf=gdf, buffering=True, drop=False, buffer=buffer, predicate="intersects" + ) - gdf.loc[:, 'node_id'] = connectivity.component_labels + gdf.loc[:, "node_id"] = connectivity.component_labels gdf = gdf.to_crs(epsg=4326) # geocode new position based on new node_id - gdf.loc[:, ['stop_x']] = gdf.geometry.x - gdf.loc[:, ['stop_y']] = gdf.geometry.y + gdf.loc[:, ["stop_x"]] = gdf.geometry.x + gdf.loc[:, ["stop_y"]] = gdf.geometry.y + + x_new_long = gdf.groupby("node_id").apply(lambda df: df.stop_x.mean()).to_dict() + y_new_long = gdf.groupby("node_id").apply(lambda df: df.stop_y.mean()).to_dict() + + gdf.loc[:, "node_y"] = gdf["node_id"].replace(y_new_long) + gdf.loc[:, "node_x"] = gdf["node_id"].replace(x_new_long) + + cols = [ + "id_linea", + "id_ramal", + "node_id", + "branch_stop_order", + "stop_x", + "stop_y", + "node_x", + "node_y", + ] + gdf = gdf.reindex(columns=cols) - x_new_long = gdf.groupby('node_id').apply( - lambda df: df.stop_x.mean()).to_dict() - y_new_long = gdf.groupby('node_id').apply( - lambda df: df.stop_y.mean()).to_dict() + return gdf - gdf.loc[:, 'node_y'] = gdf['node_id'].replace(y_new_long) - gdf.loc[:, 'node_x'] = gdf['node_id'].replace(x_new_long) - cols = ['id_linea', 'id_ramal', 'node_id', - 'branch_stop_order', 'stop_x', 'stop_y', 'node_x', 'node_y'] - gdf = gdf.reindex(columns=cols) +def upload_travel_times_stations(): + """ + This function loads a table holding travel time in minutes + between stations for modes that don't have GPS in the vehicles + """ + configs = leer_configs_generales() - return gdf + tts_file_name = configs["tiempos_viaje_estaciones"] + path = os.path.join("data", "data_ciudad", tts_file_name) + print("Leyendo tabla de tiempos de viaje entre estaciones", tts_file_name) + + if os.path.isfile(path): + travel_times_stations = pd.read_csv(path) + cols = [ + "id_o", + "id_linea_o", + "id_ramal_o", + "lat_o", + "lon_o", + "id_d", + "lat_d", + "lon_d", + "id_linea_d", + "id_ramal_d", + "travel_time_min", + ] + + travel_times_stations = travel_times_stations.reindex(columns=cols) + + assert ( + not travel_times_stations.isna().any().all() + ), "Hay datos faltantes en la tabla" + + print("Subiendo tabla de tiempos de viaje entre estaciones a la DB") + conn = iniciar_conexion_db(tipo="insumos") + travel_times_stations.to_sql( + "travel_times_stations", conn, if_exists="replace", index=False + ) + conn.close() + + else: + print(f"No existe el archivo {tts_file_name}") diff --git a/urbantrips/cluster/dbscan.py b/urbantrips/cluster/dbscan.py index 065db50..aaeef11 100644 --- a/urbantrips/cluster/dbscan.py +++ b/urbantrips/cluster/dbscan.py @@ -75,13 +75,9 @@ def get_legs_and_route_geoms(id_linea, rango_hrs, day_type): # query legs q_etapas = f""" - select e.*,d.h3_d, f.factor_expansion - from ({q_main_etapas}) e - left join destinos d - on d.id = e.id - left join factores_expansion f - on e.dia = f.dia - and e.id_tarjeta = f.id_tarjeta + select e.*, e.factor_expansion_linea AS factor_expansion + from ({q_main_etapas}) e + WHERE od_validado = 1; """ print("Obteniendo datos de etapas y rutas") @@ -566,13 +562,12 @@ def plot_cluster_legs_4d( route_gs.plot(ax=ax, color='black') - prov = cx.providers.Stamen.TonerLite + prov = cx.providers.CartoDB.Positron crs_string = gdf_max_groups.crs.to_string() try: cx.add_basemap(ax1, crs=crs_string, source=prov) except (UnidentifiedImageError): - prov = cx.providers.CartoDB.Positron - cx.add_basemap(ax1, crs=crs_string, source=prov) + cx.add_basemap(ax1, crs=crs_string) except (r_ConnectionError): pass diff --git a/urbantrips/create_viz.py b/urbantrips/create_viz.py index 3de7af7..fa49715 100644 --- a/urbantrips/create_viz.py +++ b/urbantrips/create_viz.py @@ -5,7 +5,9 @@ from urbantrips.utils import utils from urbantrips.utils.check_configs import check_config from urbantrips.utils.utils import (leer_configs_generales) - +from urbantrips.kpi.line_od_matrix import compute_lines_od_matrix +from urbantrips.viz.line_od_matrix import visualize_lines_od_matrix +from urbantrips.lineas_deseo.lineas_deseo import proceso_poligonos, proceso_lineas_deseo def main(): check_config() @@ -42,19 +44,31 @@ def main(): # Compute and viz route section load by line for rango in [[7, 10], [17, 19]]: + # crate rout section load kpi.compute_route_section_load( - id_linea=top_line_ids, rango_hrs=rango) + line_ids=top_line_ids, hour_range=rango) viz.visualize_route_section_load( - id_linea=top_line_ids, rango_hrs=rango, - save_gdf=True, indicador='prop_etapas', factor=500, + line_ids=top_line_ids, hour_range=rango, + save_gdf=True, stat='proportion', factor=500, factor_min=50, ) + compute_lines_od_matrix( + line_ids=top_line_ids, hour_range=rango, + n_sections=10, day_type='weekday', save_csv=False + ) + visualize_lines_od_matrix( + line_ids=top_line_ids, hour_range=rango, + day_type='weekday', n_sections=10, stat='proportion') + # Prduce main viz viz.create_visualizations() # Produce ppt viz_ppt_utils.create_ppt() + proceso_poligonos() + proceso_lineas_deseo() + if __name__ == "__main__": main() diff --git a/urbantrips/dashboard/dash_utils.py b/urbantrips/dashboard/dash_utils.py new file mode 100644 index 0000000..c40e2d7 --- /dev/null +++ b/urbantrips/dashboard/dash_utils.py @@ -0,0 +1,531 @@ +from shapely.geometry import LineString +import streamlit as st +import pandas as pd +import geopandas as gpd +import numpy as np +from PIL import Image +import requests +import matplotlib.pyplot as plt +import os +import yaml +import sqlite3 +from shapely import wkt +from matplotlib import colors as mcolors +from folium import Figure +from shapely.geometry import LineString, Point, Polygon + + +def leer_configs_generales(): + """ + Esta funcion lee los configs generales + """ + path = os.path.join("configs", "configuraciones_generales.yaml") + + try: + with open(path, 'r', encoding="utf8") as file: + config = yaml.safe_load(file) + except yaml.YAMLError as error: + print(f'Error al leer el archivo de configuracion: {error}') + + return config + + +def leer_alias(tipo='data'): + """ + Esta funcion toma un tipo de datos (data o insumos) + y devuelve el alias seteado en el archivo de congifuracion + """ + configs = leer_configs_generales() + # Setear el tipo de key en base al tipo de datos + if tipo == 'data': + key = 'alias_db_data' + elif tipo == 'insumos': + key = 'alias_db_insumos' + elif tipo == 'dash': + key = 'alias_db_data' + else: + raise ValueError('tipo invalido: %s' % tipo) + # Leer el alias + try: + alias = configs[key] + '_' + except KeyError: + alias = '' + return alias + + +def traigo_db_path(tipo='data'): + """ + Esta funcion toma un tipo de datos (data o insumos) + y devuelve el path a una base de datos con esa informacion + """ + if tipo not in ('data', 'insumos', 'dash'): + raise ValueError('tipo invalido: %s' % tipo) + + alias = leer_alias(tipo) + db_path = os.path.join("data", "db", f"{alias}{tipo}.sqlite") + + return db_path + + +def iniciar_conexion_db(tipo='data'): + """" + Esta funcion toma un tipo de datos (data o insumos) + y devuelve una conexion sqlite a la db + """ + db_path = traigo_db_path(tipo) + assert os.path.isfile( + db_path), f'No existe la base de datos para el dashboard en {db_path}' + conn = sqlite3.connect(db_path, timeout=10) + return conn + +# Calculate weighted mean, handling division by zero or empty inputs + + +def weighted_mean(series, weights): + try: + result = (series * weights).sum() / weights.sum() + except ZeroDivisionError: + result = np.nan + return result + + +def calculate_weighted_means(df, + aggregate_cols, + weighted_mean_cols, + weight_col, + zero_to_nan=[]): + + for i in zero_to_nan: + df.loc[df[i] == 0, i] = np.nan + + calculate_weighted_means # Validate inputs + if not set(aggregate_cols + weighted_mean_cols + [weight_col]).issubset(df.columns): + raise ValueError( + "One or more columns specified do not exist in the DataFrame.") + result = pd.DataFrame([]) + # Calculate the product of the value and its weight for weighted mean calculation + for col in weighted_mean_cols: + df.loc[df[col].notna(), f'{col}_weighted'] = df.loc[df[col].notna( + ), col] * df.loc[df[col].notna(), weight_col] + grouped = df.loc[df[col].notna()].groupby(aggregate_cols, as_index=False)[ + [f'{col}_weighted', weight_col]].sum() + grouped[col] = grouped[f'{col}_weighted'] / grouped[weight_col] + grouped = grouped.drop([f'{col}_weighted', weight_col], axis=1) + + if len(result) == 0: + result = grouped.copy() + else: + result = result.merge(grouped, how='left', on=aggregate_cols) + + fex_summed = df.groupby(aggregate_cols, as_index=False)[weight_col].sum() + result = result.merge(fex_summed, how='left', on=aggregate_cols) + + return result + + +def normalize_vars(tabla): + if 'dia' in tabla.columns: + tabla.loc[tabla.dia == 'weekday', 'dia'] = 'Día hábil' + tabla.loc[tabla.dia == 'weekend', 'dia'] = 'Fin de semana' + if 'day_type' in tabla.columns: + tabla.loc[tabla.day_type == 'weekday', 'day_type'] = 'Día hábil' + tabla.loc[tabla.day_type == 'weekend', 'day_type'] = 'Fin de semana' + + if 'nombre_linea' in tabla.columns: + tabla['nombre_linea'] = tabla['nombre_linea'].str.replace(' -', '') + if 'Modo' in tabla.columns: + tabla['Modo'] = tabla['Modo'].str.capitalize() + if 'modo' in tabla.columns: + tabla['modo'] = tabla['modo'].str.capitalize() + return tabla + + +@st.cache_data +def levanto_tabla_sql(tabla_sql, + custom_query=False, + tabla_tipo='dash'): + + conn = iniciar_conexion_db(tipo=tabla_tipo) + + try: + if not custom_query: + tabla = pd.read_sql_query( + f""" + SELECT * + FROM {tabla_sql} + """, + conn, + ) + else: + tabla = pd.read_sql_query( + custom_query, + conn, + ) + + except: + print(f'{tabla_sql} no existe') + tabla = pd.DataFrame([]) + + conn.close() + + if len(tabla) > 0: + if 'wkt' in tabla.columns: + tabla["geometry"] = tabla.wkt.apply(wkt.loads) + tabla = gpd.GeoDataFrame(tabla, + crs=4326) + tabla = tabla.drop(['wkt'], axis=1) + + tabla = normalize_vars(tabla) + + return tabla + + +@st.cache_data +def get_logo(): + file_logo = os.path.join( + "docs", "urbantrips_logo.jpg") + if not os.path.isfile(file_logo): + # URL of the image file on Github + url = 'https://raw.githubusercontent.com/EL-BID/UrbanTrips/main/docs/urbantrips_logo.jpg' + + # Send a request to get the content of the image file + response = requests.get(url) + + # Save the content to a local file + with open(file_logo, 'wb') as f: + f.write(response.content) + image = Image.open(file_logo) + return image + + +@st.cache_data +def create_linestring_od(df, + lat_o='lat_o', + lon_o='lon_o', + lat_d='lat_d', + lon_d='lon_d'): + + # Create LineString objects from the coordinates + geometry = [LineString([(row['lon_o'], row['lat_o']), + (row['lon_d'], row['lat_d'])]) + for _, row in df.iterrows()] + + # Create a GeoDataFrame + gdf = gpd.GeoDataFrame(df, geometry=geometry) + + return gdf + + +def calculate_weighted_means_ods(df, + aggregate_cols, + weighted_mean_cols, + weight_col, + agg_transferencias=False, + agg_modo=False, + agg_hora=False, + agg_distancia=False, + zero_to_nan=[] + ): + + if agg_transferencias: + df['transferencia'] = 99 + if agg_modo: + df['modo_agregado'] = 99 + if agg_hora: + df['rango_hora'] = 99 + if agg_distancia: + df['distancia'] = 99 + + df = calculate_weighted_means(df, + aggregate_cols, + weighted_mean_cols, + weight_col, + zero_to_nan) + return df + + +def agg_matriz(df, + aggregate_cols=['id_polygon', 'zona', 'Origen', 'Destino', + 'transferencia', 'modo_agregado', 'rango_hora', 'distancia'], + weight_col=['distance_osm_drive', 'travel_time_min', 'travel_speed'], + weight_var='factor_expansion_linea', + agg_transferencias=False, + agg_modo=False, + agg_hora=False, + agg_distancia=False): + + if len(df) > 0: + if agg_transferencias: + df['transferencia'] = 99 + if agg_modo: + df['modo_agregado'] = 99 + if agg_hora: + df['rango_hora'] = 99 + if agg_distancia: + df['distancia'] = 99 + + df1 = df.groupby(aggregate_cols, as_index=False)[weight_var].sum() + + df2 = calculate_weighted_means(df, + aggregate_cols=aggregate_cols, + weighted_mean_cols=weight_col, + weight_col=weight_var + ) + df = df1.merge(df2) + + + return df + + +def creo_bubble_od(df, + aggregate_cols, + weighted_mean_cols, + weight_col, + agg_transferencias=False, + agg_modo=False, + agg_hora=False, + agg_distancia=False, + od='', + lat='lat1', + lon='lon1'): + + if 'id_polygon' not in df.columns: + df['id_polygon'] = 'NONE' + + orig = pd.DataFrame([]) + if len(df) > 0: + if agg_transferencias: + df['transferencia'] = 99 + if agg_modo: + df['modo_agregado'] = 99 + if agg_hora: + df['rango_hora'] = 99 + if agg_distancia: + df['distancia'] = 99 + + orig = calculate_weighted_means_ods(df, + aggregate_cols, + [lat, lon], + 'factor_expansion_linea', + agg_transferencias=agg_transferencias, + agg_modo=agg_modo, + agg_hora=agg_hora, + agg_distancia=agg_distancia) + + orig['tot'] = orig.groupby(['id_polygon', + 'zona', + 'transferencia', + 'modo_agregado', + 'rango_hora', + 'distancia']).factor_expansion_linea.transform('sum') + geometry = [Point(xy) for xy in zip(orig[lon], orig[lat])] + orig = gpd.GeoDataFrame(orig, geometry=geometry, crs="EPSG:4326") + orig['viajes_porc'] = ( + orig.factor_expansion_linea / orig.tot * 100).round(1) + orig = orig.rename(columns={od: 'od', lat: 'lat', lon: 'lon'}) + + return orig + + +def df_to_linestrings(df, lat_cols, lon_cols): + """ + Converts DataFrame rows into LineStrings based on specified lat/lon columns, + ignoring pairs where either lat or lon is zero. + + Parameters: + - df: pandas DataFrame containing the data. + - lat_cols: List of column names for latitudes. + - lon_cols: List of column names for longitudes. + + Returns: + - GeoDataFrame with an added 'geometry' column containing LineStrings. + """ + + def create_linestring(row): + # Filter out coordinate pairs where lat or lon is 0 + points = [(row[lon_cols[i]], row[lat_cols[i]]) for i in range(len(lat_cols)) + if row[lat_cols[i]] != 0 and row[lon_cols[i]] != 0] + # Create a LineString if there are at least two points + return LineString(points) if len(points) >= 2 else None + + # Create 'geometry' column with LineStrings + df['geometry'] = df.apply(create_linestring, axis=1) + + # Convert DataFrame to GeoDataFrame + gdf = gpd.GeoDataFrame(df, geometry='geometry') + + return gdf + + +def create_data_folium(etapas, + viajes_matrices, + agg_transferencias=False, + agg_modo=False, + agg_hora=False, + agg_distancia=False, + agg_cols_etapas=[], + agg_cols_viajes=[]): + + etapas = calculate_weighted_means_ods(etapas, + agg_cols_etapas, + ['distance_osm_drive', 'lat1_norm', 'lon1_norm', 'lat2_norm', + 'lon2_norm', 'lat3_norm', 'lon3_norm', 'lat4_norm', 'lon4_norm'], + + 'factor_expansion_linea', + agg_transferencias=agg_transferencias, + agg_modo=agg_modo, + agg_hora=agg_hora, + agg_distancia=agg_distancia, + zero_to_nan=['lat1_norm', 'lon1_norm', 'lat2_norm', 'lon2_norm', 'lat3_norm', 'lon3_norm', 'lat4_norm', 'lon4_norm']) + + etapas[['lat1_norm', + 'lon1_norm', + 'lat2_norm', + 'lon2_norm', + 'lat3_norm', + 'lon3_norm', + 'lat4_norm', + 'lon4_norm']] = etapas[['lat1_norm', + 'lon1_norm', + 'lat2_norm', + 'lon2_norm', + 'lat3_norm', + 'lon3_norm', + 'lat4_norm', + 'lon4_norm']].fillna(0) + + viajes = calculate_weighted_means_ods(etapas, + agg_cols_viajes, + ['distance_osm_drive', + 'lat1_norm', + 'lon1_norm', + 'lat4_norm', + 'lon4_norm'], + 'factor_expansion_linea', + agg_transferencias=agg_transferencias, + agg_modo=agg_modo, + agg_hora=agg_hora, + agg_distancia=agg_distancia, + zero_to_nan=['lat1_norm', 'lon1_norm', 'lat4_norm', 'lon4_norm']) + viajes[['lat1_norm', + 'lon1_norm', + 'lat4_norm', + 'lon4_norm']] = viajes[['lat1_norm', + 'lon1_norm', + 'lat4_norm', + 'lon4_norm']].fillna(0) + + if 'id_polygon' not in viajes_matrices.columns: + viajes_matrices['id_polygon'] = 'NONE' + + matriz = agg_matriz(viajes_matrices, + aggregate_cols=['id_polygon', 'zona', 'Origen', 'Destino', + 'transferencia', 'modo_agregado', 'rango_hora', 'distancia'], + weight_col=['distance_osm_drive', 'travel_time_min', 'travel_speed'], + weight_var='factor_expansion_linea', + agg_transferencias=agg_transferencias, + agg_modo=agg_modo, + agg_hora=agg_hora, + agg_distancia=agg_distancia) + + if ('poly_inicio' in viajes_matrices.columns) | ('poly_fin' in viajes_matrices.columns): + bubble_cols_o = ['id_polygon', 'zona', 'inicio', 'poly_inicio', + 'transferencia', 'modo_agregado', 'rango_hora', 'distancia'] + bubble_cols_d = ['id_polygon', 'zona', 'fin', 'poly_fin', + 'transferencia', 'modo_agregado', 'rango_hora', 'distancia'] + else: + bubble_cols_o = ['id_polygon', 'zona', 'inicio', + 'transferencia', 'modo_agregado', 'rango_hora', 'distancia'] + bubble_cols_d = ['id_polygon', 'zona', 'fin', + 'transferencia', 'modo_agregado', 'rango_hora', 'distancia'] + + origen = creo_bubble_od(viajes_matrices, + aggregate_cols=bubble_cols_o, + weighted_mean_cols=['lat1', 'lon1'], + weight_col='factor_expansion_linea', + agg_transferencias=agg_transferencias, + agg_modo=agg_modo, + agg_hora=agg_hora, + agg_distancia=agg_distancia, + od='inicio', + lat='lat1', + lon='lon1') + + destino = creo_bubble_od(viajes_matrices, + aggregate_cols=bubble_cols_d, + weighted_mean_cols=['lat4', 'lon4'], + weight_col='factor_expansion_linea', + agg_transferencias=agg_transferencias, + agg_modo=agg_modo, + agg_hora=agg_hora, + agg_distancia=agg_distancia, + od='fin', + lat='lat4', + lon='lon4') + + etapas = df_to_linestrings(etapas, + lat_cols=['lat1_norm', 'lat2_norm', 'lat3_norm', 'lat4_norm'], lon_cols=['lon1_norm', 'lon2_norm', 'lon3_norm', 'lon4_norm']) + + viajes = df_to_linestrings(viajes, + lat_cols=['lat1_norm', 'lat4_norm'], lon_cols=['lon1_norm', 'lon4_norm']) + + return etapas, viajes, matriz, origen, destino + + +@st.cache_data +def traigo_indicadores(tipo='all'): + if tipo == 'all': + indicadores_all = levanto_tabla_sql('agg_indicadores') + else: + indicadores_all = levanto_tabla_sql('poly_indicadores') + + general = indicadores_all[indicadores_all.Tipo == 'General'] + modal = indicadores_all[indicadores_all.Tipo == 'Modal'] + distancias = indicadores_all[indicadores_all.Tipo == 'Distancias'] + return general, modal, distancias + + +def get_epsg_m(): + ''' + Gets the epsg id for a coordinate reference system in meters from config + ''' + configs = leer_configs_generales() + epsg_m = configs['epsg_m'] + + return epsg_m + + +def create_squared_polygon(min_x, min_y, max_x, max_y, epsg): + + width = max(max_x - min_x, max_y - min_y) + center_x = (max_x + min_x) / 2 + center_y = (max_y + min_y) / 2 + + square_bbox_min_x = center_x - width / 2 + square_bbox_min_y = center_y - width / 2 + square_bbox_max_x = center_x + width / 2 + square_bbox_max_y = center_y + width / 2 + + square_bbox_coords = [ + (square_bbox_min_x, square_bbox_min_y), + (square_bbox_max_x, square_bbox_min_y), + (square_bbox_max_x, square_bbox_max_y), + (square_bbox_min_x, square_bbox_max_y) + ] + + p = Polygon(square_bbox_coords) + s = gpd.GeoSeries([p], crs=f'EPSG:{epsg}') + return s + + +def extract_hex_colors_from_cmap(cmap, n=5): + # Choose a colormap + cmap = plt.get_cmap(cmap) + + # Extract colors from the colormap + colors = cmap(np.linspace(0, 1, n)) + + # Convert the colors to hex format + hex_colors = [mcolors.rgb2hex(color) for color in colors] + + return hex_colors \ No newline at end of file diff --git a/urbantrips/dashboard/dashboard.py b/urbantrips/dashboard/dashboard.py index 3208a56..3a3a1ea 100644 --- a/urbantrips/dashboard/dashboard.py +++ b/urbantrips/dashboard/dashboard.py @@ -17,150 +17,7 @@ from folium import Figure from shapely.geometry import LineString - -def create_linestring(df, - lat_o='lat_o', - lon_o='lon_o', - lat_d='lat_d', - lon_d='lon_d'): - - # Create LineString objects from the coordinates - geometry = [LineString([(row['lon_o'], row['lat_o']), - (row['lon_d'], row['lat_d'])]) - for _, row in df.iterrows()] - - # Create a GeoDataFrame - gdf = gpd.GeoDataFrame(df, geometry=geometry) - - return gdf - - -def leer_configs_generales(): - """ - Esta funcion lee los configs generales - """ - path = os.path.join("configs", "configuraciones_generales.yaml") - - try: - with open(path, 'r', encoding="utf8") as file: - config = yaml.safe_load(file) - except yaml.YAMLError as error: - print(f'Error al leer el archivo de configuracion: {error}') - - return config - - -def leer_alias(tipo='data'): - """ - Esta funcion toma un tipo de datos (data o insumos) - y devuelve el alias seteado en el archivo de congifuracion - """ - configs = leer_configs_generales() - # Setear el tipo de key en base al tipo de datos - if tipo == 'data': - key = 'alias_db_data' - elif tipo == 'insumos': - key = 'alias_db_insumos' - elif tipo == 'dash': - key = 'alias_db_data' - else: - raise ValueError('tipo invalido: %s' % tipo) - # Leer el alias - try: - alias = configs[key] + '_' - except KeyError: - alias = '' - return alias - - -def traigo_db_path(tipo='data'): - """ - Esta funcion toma un tipo de datos (data o insumos) - y devuelve el path a una base de datos con esa informacion - """ - if tipo not in ('data', 'insumos', 'dash'): - raise ValueError('tipo invalido: %s' % tipo) - - alias = leer_alias(tipo) - db_path = os.path.join("data", "db", f"{alias}{tipo}.sqlite") - - return db_path - - -def iniciar_conexion_db(tipo='data'): - """" - Esta funcion toma un tipo de datos (data o insumos) - y devuelve una conexion sqlite a la db - """ - - db_path = traigo_db_path(tipo) - assert os.path.isfile( - db_path), f'No existe la base de datos para el dashboard en {db_path}' - conn = sqlite3.connect(db_path, timeout=10) - - return conn - - -@st.cache_data -def levanto_tabla_sql(tabla_sql, - has_linestring=False, - has_wkt=False): - - conn_dash = iniciar_conexion_db(tipo='dash') - - tabla = pd.read_sql_query( - f""" - SELECT * - FROM {tabla_sql} - """, - conn_dash, - ) - - conn_dash.close() - - if has_linestring: - tabla = create_linestring(tabla) - - if has_wkt: - tabla["geometry"] = tabla.wkt.apply(wkt.loads) - tabla = gpd.GeoDataFrame(tabla, - crs=4326) - tabla = tabla.drop(['wkt'], axis=1) - - if 'dia' in tabla.columns: - tabla.loc[tabla.dia == 'weekday', 'dia'] = 'Día hábil' - tabla.loc[tabla.dia == 'weekend', 'dia'] = 'Fin de semana' - if 'day_type' in tabla.columns: - tabla.loc[tabla.day_type == 'weekday', 'day_type'] = 'Día hábil' - tabla.loc[tabla.day_type == 'weekend', 'day_type'] = 'Fin de semana' - - if 'nombre_linea' in tabla.columns: - tabla['nombre_linea'] = tabla['nombre_linea'].str.replace(' -', '') - if 'Modo' in tabla.columns: - tabla['Modo'] = tabla['Modo'].str.capitalize() - - return tabla - - -@st.cache_data -def get_logo(): - path_logo = os.path.join("docs") - if not os.path.isdir(path_logo): - os.mkdir(path_logo) - file_logo = os.path.join( - "docs", "urbantrips_logo.jpg") - if not os.path.isfile(file_logo): - # URL of the image file on Github - url = 'https://raw.githubusercontent.com/EL-BID/UrbanTrips/main/docs/urbantrips_logo.jpg' - - # Send a request to get the content of the image file - response = requests.get(url) - - # Save the content to a local file - with open(file_logo, 'wb') as f: - f.write(response.content) - image = Image.open(file_logo) - return image +from dash_utils import levanto_tabla_sql, get_logo st.set_page_config(layout="wide") @@ -179,66 +36,94 @@ def get_logo(): indicadores = levanto_tabla_sql('indicadores') +if len(indicadores) > 0: + desc_dia_i = col1.selectbox( + 'Periodo', options=indicadores.desc_dia.unique(), key='desc_dia_i') + tipo_dia_i = col1.selectbox( + 'Tipo de dia', options=indicadores.tipo_dia.unique(), key='tipo_dia_i') + + + indicadores = indicadores[(indicadores.desc_dia == desc_dia_i) & ( + indicadores.tipo_dia == tipo_dia_i)] + + df = indicadores.loc[indicadores.orden == 1, ['Indicador', 'Valor']].copy() + titulo = indicadores.loc[indicadores.orden == 1].Titulo.unique()[0] + + # CSS to inject contained in a string + hide_table_row_index = """ + + """ + + # Inject CSS with Markdown + col2.markdown(hide_table_row_index, unsafe_allow_html=True) + + col2.text(titulo) + col2.table(df) + + + df = indicadores.loc[indicadores.orden == 2, ['Indicador', 'Valor']].copy() + titulo = indicadores.loc[indicadores.orden == 2].Titulo.unique()[0] + + col3.text(titulo) + + # CSS to inject contained in a string + hide_table_row_index = """ + + """ + + # Inject CSS with Markdown + col3.markdown(hide_table_row_index, unsafe_allow_html=True) + + + col3.table(df) + + df = indicadores.loc[indicadores.orden == 3, ['Indicador', 'Valor']].copy() + titulo = indicadores.loc[indicadores.orden == 3].Titulo.unique()[0] + + col2.text(titulo) + # CSS to inject contained in a string + hide_table_row_index = """ + + """ + + # Inject CSS with Markdown + col2.markdown(hide_table_row_index, unsafe_allow_html=True) + + col2.table(df) +else: + + # Usar HTML para personalizar el estilo del texto + texto_html = """ + +
+ No hay datos de indicadores +
+ """ + col2.markdown(texto_html, unsafe_allow_html=True) + texto_html = """ + +
+ Verifique que los procesos se corrieron correctamente +
+ """ + col2.markdown(texto_html, unsafe_allow_html=True) -desc_dia_i = col1.selectbox( - 'Periodo', options=indicadores.desc_dia.unique(), key='desc_dia_i') -tipo_dia_i = col1.selectbox( - 'Tipo de dia', options=indicadores.tipo_dia.unique(), key='tipo_dia_i') - - -indicadores = indicadores[(indicadores.desc_dia == desc_dia_i) & ( - indicadores.tipo_dia == tipo_dia_i)] - -df = indicadores.loc[indicadores.orden == 1, ['Indicador', 'Valor']].copy() -titulo = indicadores.loc[indicadores.orden == 1].Titulo.unique()[0] - -# CSS to inject contained in a string -hide_table_row_index = """ - - """ - -# Inject CSS with Markdown -col2.markdown(hide_table_row_index, unsafe_allow_html=True) - -col2.text(titulo) -col2.table(df) - - -df = indicadores.loc[indicadores.orden == 2, ['Indicador', 'Valor']].copy() -titulo = indicadores.loc[indicadores.orden == 2].Titulo.unique()[0] - -col3.text(titulo) - -# CSS to inject contained in a string -hide_table_row_index = """ - - """ - -# Inject CSS with Markdown -col3.markdown(hide_table_row_index, unsafe_allow_html=True) - - -col3.table(df) - -df = indicadores.loc[indicadores.orden == 3, ['Indicador', 'Valor']].copy() -titulo = indicadores.loc[indicadores.orden == 3].Titulo.unique()[0] - -col2.text(titulo) -# CSS to inject contained in a string -hide_table_row_index = """ - - """ - -# Inject CSS with Markdown -col2.markdown(hide_table_row_index, unsafe_allow_html=True) - -col2.table(df) diff --git a/urbantrips/dashboard/pages/1_Datos Generales.py b/urbantrips/dashboard/pages/1_Datos Generales.py index f38ea51..202804d 100644 --- a/urbantrips/dashboard/pages/1_Datos Generales.py +++ b/urbantrips/dashboard/pages/1_Datos Generales.py @@ -1,179 +1,13 @@ import streamlit as st import pandas as pd -import geopandas as gpd import folium from streamlit_folium import st_folium from streamlit_folium import folium_static -from PIL import Image -import requests import mapclassify import plotly.express as px -import matplotlib.pyplot as plt -from matplotlib import colors as mcolors -import seaborn as sns -import contextily as cx -import os -import numpy as np -import yaml -import sqlite3 -from shapely import wkt from folium import Figure -from shapely.geometry import LineString - - -def extract_hex_colors_from_cmap(cmap, n=5): - # Choose a colormap - cmap = plt.get_cmap(cmap) - - # Extract colors from the colormap - colors = cmap(np.linspace(0, 1, n)) - - # Convert the colors to hex format - hex_colors = [mcolors.rgb2hex(color) for color in colors] - - return hex_colors - - -def create_linestring(df, - lat_o='lat_o', - lon_o='lon_o', - lat_d='lat_d', - lon_d='lon_d'): - - # Create LineString objects from the coordinates - geometry = [LineString([(row['lon_o'], row['lat_o']), - (row['lon_d'], row['lat_d'])]) - for _, row in df.iterrows()] - - # Create a GeoDataFrame - gdf = gpd.GeoDataFrame(df, geometry=geometry) - - return gdf - - -def leer_configs_generales(): - """ - Esta funcion lee los configs generales - """ - path = os.path.join("configs", "configuraciones_generales.yaml") - - try: - with open(path, 'r', encoding="utf8") as file: - config = yaml.safe_load(file) - except yaml.YAMLError as error: - print(f'Error al leer el archivo de configuracion: {error}') - - return config - - -def leer_alias(tipo='data'): - """ - Esta funcion toma un tipo de datos (data o insumos) - y devuelve el alias seteado en el archivo de congifuracion - """ - configs = leer_configs_generales() - # Setear el tipo de key en base al tipo de datos - if tipo == 'data': - key = 'alias_db_data' - elif tipo == 'insumos': - key = 'alias_db_insumos' - elif tipo == 'dash': - key = 'alias_db_data' - else: - raise ValueError('tipo invalido: %s' % tipo) - # Leer el alias - try: - alias = configs[key] + '_' - except KeyError: - alias = '' - return alias - - -def traigo_db_path(tipo='data'): - """ - Esta funcion toma un tipo de datos (data o insumos) - y devuelve el path a una base de datos con esa informacion - """ - if tipo not in ('data', 'insumos', 'dash'): - raise ValueError('tipo invalido: %s' % tipo) - - alias = leer_alias(tipo) - db_path = os.path.join("data", "db", f"{alias}{tipo}.sqlite") - - return db_path - - -def iniciar_conexion_db(tipo='data'): - """" - Esta funcion toma un tipo de datos (data o insumos) - y devuelve una conexion sqlite a la db - """ - db_path = traigo_db_path(tipo) - assert os.path.isfile( - db_path), f'No existe la base de datos para el dashboard en {db_path}' - conn = sqlite3.connect(db_path, timeout=10) - return conn - - -@st.cache_data -def levanto_tabla_sql(tabla_sql, - has_linestring=False, - has_wkt=False): - - conn_dash = iniciar_conexion_db(tipo='dash') - - tabla = pd.read_sql_query( - f""" - SELECT * - FROM {tabla_sql} - """, - conn_dash, - ) - - conn_dash.close() - - if has_linestring: - tabla = create_linestring(tabla) - - if has_wkt: - tabla["geometry"] = tabla.wkt.apply(wkt.loads) - tabla = gpd.GeoDataFrame(tabla, - crs=4326) - tabla = tabla.drop(['wkt'], axis=1) - - if 'dia' in tabla.columns: - tabla.loc[tabla.dia == 'weekday', 'dia'] = 'Día hábil' - tabla.loc[tabla.dia == 'weekend', 'dia'] = 'Fin de semana' - if 'day_type' in tabla.columns: - tabla.loc[tabla.day_type == 'weekday', 'day_type'] = 'Día hábil' - tabla.loc[tabla.day_type == 'weekend', 'day_type'] = 'Fin de semana' - - if 'nombre_linea' in tabla.columns: - tabla['nombre_linea'] = tabla['nombre_linea'].str.replace(' -', '') - if 'Modo' in tabla.columns: - tabla['Modo'] = tabla['Modo'].str.capitalize() - if 'modo' in tabla.columns: - tabla['modo'] = tabla['modo'].str.capitalize() - - return tabla - - -@st.cache_data -def get_logo(): - file_logo = os.path.join( - "docs", "urbantrips_logo.jpg") - if not os.path.isfile(file_logo): - # URL of the image file on Github - url = 'https://raw.githubusercontent.com/EL-BID/UrbanTrips/main/docs/urbantrips_logo.jpg' - - # Send a request to get the content of the image file - response = requests.get(url) - - # Save the content to a local file - with open(file_logo, 'wb') as f: - f.write(response.content) - image = Image.open(file_logo) - return image +from dash_utils import (levanto_tabla_sql, get_logo, + create_linestring_od, extract_hex_colors_from_cmap) def crear_mapa_folium(df_agg, @@ -232,6 +66,7 @@ def crear_mapa_folium(df_agg, col1, col2, col3 = st.columns([1, 3, 3]) particion_modal = levanto_tabla_sql('particion_modal') + desc_dia_m = col1.selectbox( 'Periodo', options=particion_modal.desc_dia.unique(), key='desc_dia_m') tipo_dia_m = col1.selectbox( @@ -267,44 +102,72 @@ def crear_mapa_folium(df_agg, col1, col2 = st.columns([1, 4]) hist_values = levanto_tabla_sql('distribucion') - hist_values.columns = ['desc_dia', 'tipo_dia', - 'Distancia (kms)', 'Viajes', 'Modo'] - hist_values = hist_values[hist_values['Distancia (kms)'] <= 60] - hist_values = hist_values.sort_values(['Modo', 'Distancia (kms)']) - - if col2.checkbox('Ver datos: distribución de viajes'): - col2.write(hist_values) - - desc_dia_d = col1.selectbox( - 'Periodo', options=hist_values.desc_dia.unique(), key='desc_dia_d') - tipo_dia_d = col1.selectbox( - 'Tipo de dia', options=hist_values.tipo_dia.unique(), key='tipo_dia_d') - - dist = hist_values.Modo.unique().tolist() - dist.remove('Todos') - dist = ['Todos'] + dist - modo_d = col1.selectbox('Modo', options=dist) - - hist_values = hist_values[(hist_values.desc_dia == desc_dia_d) & ( - hist_values.tipo_dia == tipo_dia_d) & (hist_values.Modo == modo_d)] - - fig = px.histogram(hist_values, x='Distancia (kms)', - y='Viajes', nbins=len(hist_values)) - fig.update_xaxes(type='category') - fig.update_yaxes(title_text='Viajes') - - fig.update_layout( - xaxis=dict( - tickmode='linear', - tickangle=0, - tickfont=dict(size=9) - ), - yaxis=dict( - tickfont=dict(size=9) + + if len(hist_values) > 0: + hist_values.columns = ['desc_dia', 'tipo_dia', + 'Distancia (kms)', 'Viajes', 'Modo'] + hist_values = hist_values[hist_values['Distancia (kms)'] <= 60] + hist_values = hist_values.sort_values(['Modo', 'Distancia (kms)']) + + if col2.checkbox('Ver datos: distribución de viajes'): + col2.write(hist_values) + + desc_dia_d = col1.selectbox( + 'Periodo', options=hist_values.desc_dia.unique(), key='desc_dia_d') + tipo_dia_d = col1.selectbox( + 'Tipo de dia', options=hist_values.tipo_dia.unique(), key='tipo_dia_d') + + dist = hist_values.Modo.unique().tolist() + dist.remove('Todos') + dist = ['Todos'] + dist + modo_d = col1.selectbox('Modo', options=dist) + + hist_values = hist_values[(hist_values.desc_dia == desc_dia_d) & ( + hist_values.tipo_dia == tipo_dia_d) & (hist_values.Modo == modo_d)] + + fig = px.histogram(hist_values, x='Distancia (kms)', + y='Viajes', nbins=len(hist_values)) + fig.update_xaxes(type='category') + fig.update_yaxes(title_text='Viajes') + + fig.update_layout( + xaxis=dict( + tickmode='linear', + tickangle=0, + tickfont=dict(size=9) + ), + yaxis=dict( + tickfont=dict(size=9) + ) ) - ) - col2.plotly_chart(fig) + col2.plotly_chart(fig) + else: + # Usar HTML para personalizar el estilo del texto + texto_html = """ + +
+ No hay datos para mostrar +
+ """ + col2.markdown(texto_html, unsafe_allow_html=True) + texto_html = """ + +
+ Verifique que los procesos se corrieron correctamente +
+ """ + col2.markdown(texto_html, unsafe_allow_html=True) with st.expander('Viajes por hora'): @@ -340,134 +203,163 @@ def crear_mapa_folium(df_agg, col2.plotly_chart(fig_horas) -with st.expander('Líneas de deseo'): - - col1, col2 = st.columns([1, 4]) - - lineas_deseo = levanto_tabla_sql('lineas_deseo', - has_linestring=True) - - desc_dia = col1.selectbox( - 'Periodo', options=lineas_deseo.desc_dia.unique()) - tipo_dia = col1.selectbox( - 'Tipo de dia', options=lineas_deseo.tipo_dia.unique()) - var_zona = col1.selectbox( - 'Zonificación', options=lineas_deseo.var_zona.unique()) - filtro1 = col1.selectbox('Filtro', options=lineas_deseo.filtro1.unique()) - - df_agg = lineas_deseo[( - (lineas_deseo.desc_dia == desc_dia) & - (lineas_deseo.tipo_dia == tipo_dia) & - (lineas_deseo.var_zona == var_zona) & - (lineas_deseo.filtro1 == filtro1) - )].copy() - - if len(df_agg) > 0: - - map = crear_mapa_folium(df_agg, - cmap='BuPu', - var_fex='Viajes', - k_jenks=5) - - with col2: - st_map = st_folium(map, width=900, height=700) - else: - - col2.markdown(""" - - """, unsafe_allow_html=True) - - col2.markdown( - '

¡¡ No hay datos para mostrar !!

', unsafe_allow_html=True) - - -with st.expander('Matrices OD'): - col1, col2 = st.columns([1, 4]) - - matriz = levanto_tabla_sql('matrices') - - if len(matriz) > 0: - - if col1.checkbox('Normalizar', value=True): - normalize = True - else: - normalize = False - - desc_dia_ = col1.selectbox( - 'Periodo ', options=matriz.desc_dia.unique()) - tipo_dia_ = col1.selectbox( - 'Tipo de dia ', options=matriz.tipo_dia.unique()) - var_zona_ = col1.selectbox( - 'Zonificación ', options=matriz.var_zona.unique()) - filtro1_ = col1.selectbox('Filtro ', options=matriz.filtro1.unique()) - - matriz = matriz[((matriz.desc_dia == desc_dia_) & - (matriz.tipo_dia == tipo_dia_) & - (matriz.var_zona == var_zona_) & - (matriz.filtro1 == filtro1_) - )].copy() - - od_heatmap = pd.crosstab( - index=matriz['Origen'], - columns=matriz['Destino'], - values=matriz['Viajes'], - aggfunc="sum", - normalize=normalize, - ) - od_heatmap = (od_heatmap * 100).round(1) - - od_heatmap = od_heatmap.reset_index() - od_heatmap['Origen'] = od_heatmap['Origen'].str[4:] - od_heatmap = od_heatmap.set_index('Origen') - od_heatmap.columns = [i[4:] for i in od_heatmap.columns] - - fig = px.imshow(od_heatmap, text_auto=True, - color_continuous_scale='Blues',) - - fig.update_coloraxes(showscale=False) - - if len(od_heatmap) <= 20: - fig.update_layout(width=800, height=800) - elif (len(od_heatmap) > 20) & (len(od_heatmap) <= 40): - fig.update_layout(width=1000, height=1000) - elif len(od_heatmap) > 40: - fig.update_layout(width=1200, height=1200) - - col2.plotly_chart(fig) - - else: - st.write('No hay datos para mostrar') - - zonas = levanto_tabla_sql('zonas', has_wkt=True) - zonas = zonas[zonas.tipo_zona == var_zona_] - - col1, col2 = st.columns([1, 4]) - - if col1.checkbox('Mostrar zonificacion'): - - # Create a folium map centered on the data - map_center = [zonas.geometry.centroid.y.mean( - ), zonas.geometry.centroid.x.mean()] - - fig = Figure(width=800, height=800) - m = folium.Map(location=map_center, zoom_start=10, - tiles='cartodbpositron') - - # Add GeoDataFrame to the map - folium.GeoJson(zonas).add_to(m) - - for idx, row in zonas.iterrows(): - # Replace 'column_name' with the name of the column containing the detail - detail = row['Zona'] - point = [row['geometry'].representative_point( - ).y, row['geometry'].representative_point().x] - marker = folium.Marker(location=point, popup=detail) - marker.add_to(m) - - # Display the map using folium_static - with col2: - folium_static(m) +# with st.expander('Líneas de deseo'): + +# col1, col2 = st.columns([1, 4]) + +# lineas_deseo = levanto_tabla_sql('lineas_deseo') + +# if len(lineas_deseo) > 0: + +# lineas_deseo = create_linestring_od(lineas_deseo) + +# desc_dia = col1.selectbox( +# 'Periodo', options=lineas_deseo.desc_dia.unique()) +# tipo_dia = col1.selectbox( +# 'Tipo de dia', options=lineas_deseo.tipo_dia.unique()) +# var_zona = col1.selectbox( +# 'Zonificación', options=lineas_deseo.var_zona.unique()) +# filtro1 = col1.selectbox( +# 'Filtro', options=lineas_deseo.filtro1.unique()) + +# df_agg = lineas_deseo[( +# (lineas_deseo.desc_dia == desc_dia) & +# (lineas_deseo.tipo_dia == tipo_dia) & +# (lineas_deseo.var_zona == var_zona) & +# (lineas_deseo.filtro1 == filtro1) +# )].copy() + +# if len(df_agg) > 0: + +# map = crear_mapa_folium(df_agg, +# cmap='BuPu', +# var_fex='Viajes', +# k_jenks=5) + +# with col2: +# st_map = st_folium(map, width=900, height=700) +# else: + +# col2.markdown(""" +# +# """, unsafe_allow_html=True) + +# col2.markdown( +# '

¡¡ No hay datos para mostrar !!

', unsafe_allow_html=True) + +# else: +# # Usar HTML para personalizar el estilo del texto +# texto_html = """ +# +#
+# No hay datos para mostrar +#
+# """ +# col2.markdown(texto_html, unsafe_allow_html=True) +# texto_html = """ +# +#
+# Verifique que los procesos se corrieron correctamente +#
+# """ +# col2.markdown(texto_html, unsafe_allow_html=True) +# with st.expander('Matrices OD'): +# col1, col2 = st.columns([1, 4]) + +# matriz = levanto_tabla_sql('matrices') + +# if len(matriz) > 0: + +# if col1.checkbox('Normalizar', value=True): +# normalize = True +# else: +# normalize = False + +# desc_dia_ = col1.selectbox( +# 'Periodo ', options=matriz.desc_dia.unique()) +# tipo_dia_ = col1.selectbox( +# 'Tipo de dia ', options=matriz.tipo_dia.unique()) +# var_zona_ = col1.selectbox( +# 'Zonificación ', options=matriz.var_zona.unique()) +# filtro1_ = col1.selectbox('Filtro ', options=matriz.filtro1.unique()) + +# matriz = matriz[((matriz.desc_dia == desc_dia_) & +# (matriz.tipo_dia == tipo_dia_) & +# (matriz.var_zona == var_zona_) & +# (matriz.filtro1 == filtro1_) +# )].copy() + +# od_heatmap = pd.crosstab( +# index=matriz['Origen'], +# columns=matriz['Destino'], +# values=matriz['Viajes'], +# aggfunc="sum", +# normalize=normalize, +# ) +# od_heatmap = (od_heatmap * 100).round(1) + +# od_heatmap = od_heatmap.reset_index() +# od_heatmap['Origen'] = od_heatmap['Origen'].str[4:] +# od_heatmap = od_heatmap.set_index('Origen') +# od_heatmap.columns = [i[4:] for i in od_heatmap.columns] + +# fig = px.imshow(od_heatmap, text_auto=True, +# color_continuous_scale='Blues',) + +# fig.update_coloraxes(showscale=False) + +# if len(od_heatmap) <= 20: +# fig.update_layout(width=800, height=800) +# elif (len(od_heatmap) > 20) & (len(od_heatmap) <= 40): +# fig.update_layout(width=1000, height=1000) +# elif len(od_heatmap) > 40: +# fig.update_layout(width=1200, height=1200) + +# col2.plotly_chart(fig) + +# else: +# st.write('No hay datos para mostrar') + +# zonas = levanto_tabla_sql('zonas') +# zonas = zonas[zonas.tipo_zona == var_zona_] + +# col1, col2 = st.columns([1, 4]) + +# if col1.checkbox('Mostrar zonificacion'): + +# # Create a folium map centered on the data +# map_center = [zonas.geometry.centroid.y.mean( +# ), zonas.geometry.centroid.x.mean()] + +# fig = Figure(width=800, height=800) +# m = folium.Map(location=map_center, zoom_start=10, +# tiles='cartodbpositron') + +# # Add GeoDataFrame to the map +# folium.GeoJson(zonas).add_to(m) + +# for idx, row in zonas.iterrows(): +# # Replace 'column_name' with the name of the column containing the detail +# detail = row['Zona'] +# point = [row['geometry'].representative_point( +# ).y, row['geometry'].representative_point().x] +# marker = folium.Marker(location=point, popup=detail) +# marker.add_to(m) + +# # Display the map using folium_static +# with col2: +# folium_static(m) diff --git a/urbantrips/dashboard/pages/2_Indicadores de oferta.py b/urbantrips/dashboard/pages/2_Indicadores de oferta.py index 43521ff..bdf5946 100644 --- a/urbantrips/dashboard/pages/2_Indicadores de oferta.py +++ b/urbantrips/dashboard/pages/2_Indicadores de oferta.py @@ -1,173 +1,78 @@ import streamlit as st import pandas as pd -import geopandas as gpd import folium from streamlit_folium import st_folium from streamlit_folium import folium_static -from PIL import Image -import requests import mapclassify import plotly.express as px import matplotlib.pyplot as plt import seaborn as sns import contextily as cx -import os from PIL import UnidentifiedImageError from requests.exceptions import ConnectionError as r_ConnectionError - -import yaml -import sqlite3 -from shapely import wkt from folium import Figure from shapely.geometry import LineString +from dash_utils import ( + levanto_tabla_sql, get_logo, create_linestring_od, + create_squared_polygon, get_epsg_m, + extract_hex_colors_from_cmap +) + +def crear_mapa_folium(df_agg, + cmap, + var_fex, + savefile='', + k_jenks=5): - -def create_linestring(df, - lat_o='lat_o', - lon_o='lon_o', - lat_d='lat_d', - lon_d='lon_d'): - - # Create LineString objects from the coordinates - geometry = [LineString([(row['lon_o'], row['lat_o']), - (row['lon_d'], row['lat_d'])]) - for _, row in df.iterrows()] - - # Create a GeoDataFrame - gdf = gpd.GeoDataFrame(df, geometry=geometry) - - return gdf - - -def leer_configs_generales(): - """ - Esta funcion lee los configs generales - """ - path = os.path.join("configs", "configuraciones_generales.yaml") - - try: - with open(path, 'r', encoding="utf8") as file: - config = yaml.safe_load(file) - except yaml.YAMLError as error: - print(f'Error al leer el archivo de configuracion: {error}') - - return config - - -def leer_alias(tipo='data'): - """ - Esta funcion toma un tipo de datos (data o insumos) - y devuelve el alias seteado en el archivo de congifuracion - """ - configs = leer_configs_generales() - # Setear el tipo de key en base al tipo de datos - if tipo == 'data': - key = 'alias_db_data' - elif tipo == 'insumos': - key = 'alias_db_insumos' - elif tipo == 'dash': - key = 'alias_db_data' - else: - raise ValueError('tipo invalido: %s' % tipo) - # Leer el alias try: - alias = configs[key] + '_' - except KeyError: - alias = '' - return alias - - -def traigo_db_path(tipo='data'): - """ - Esta funcion toma un tipo de datos (data o insumos) - y devuelve el path a una base de datos con esa informacion + bins = [df_agg[var_fex].min()-1] + \ + mapclassify.FisherJenks(df_agg[var_fex], k=k_jenks).bins.tolist() + except: + k_jenks = 3 + bins = [df_agg[var_fex].min()-1] + \ + mapclassify.FisherJenks(df_agg[var_fex], k=k_jenks).bins.tolist() + + + range_bins = range(0, len(bins)-1) + bins_labels = [ + f'{int(bins[n])} a {int(bins[n+1])} viajes' for n in range_bins] + df_agg['cuts'] = pd.cut(df_agg[var_fex], bins=bins, labels=bins_labels) + + fig = Figure(width=800, height=800) + m = folium.Map(location=[df_agg.lat_o.mean( + ), df_agg.lon_o.mean()], zoom_start=9, tiles='cartodbpositron') + + title_html = """ +

Your map title

""" - if tipo not in ('data', 'insumos', 'dash'): - raise ValueError('tipo invalido: %s' % tipo) + m.get_root().html.add_child(folium.Element(title_html)) - alias = leer_alias(tipo) - db_path = os.path.join("data", "db", f"{alias}{tipo}.sqlite") + line_w = 0.5 - return db_path - - -def iniciar_conexion_db(tipo='data'): - """" - Esta funcion toma un tipo de datos (data o insumos) - y devuelve una conexion sqlite a la db - """ - db_path = traigo_db_path(tipo) - assert os.path.isfile( - db_path), f'No existe la base de datos para el dashboard en {db_path}' - conn = sqlite3.connect(db_path, timeout=10) - return conn - - -@st.cache_data -def levanto_tabla_sql(tabla_sql, - has_linestring=False, - has_wkt=False): + colors = extract_hex_colors_from_cmap(cmap=cmap, n=k_jenks) - conn_dash = iniciar_conexion_db(tipo='dash') + n = 0 + for i in bins_labels: - tabla = pd.read_sql_query( - f""" - SELECT * - FROM {tabla_sql} - """, - conn_dash, - ) + df_agg[df_agg.cuts == i].explore( + m=m, + color=colors[n], + style_kwds={'fillOpacity': 0.1, 'weight': line_w}, + name=i, + tooltip=False, + ) + n += 1 + line_w += 3 - conn_dash.close() + folium.LayerControl(name='xx').add_to(m) - if has_linestring: - tabla = create_linestring(tabla) + fig.add_child(m) - if has_wkt: - tabla["geometry"] = tabla.wkt.apply(wkt.loads) - tabla = gpd.GeoDataFrame(tabla, - crs=4326) - tabla = tabla.drop(['wkt'], axis=1) - - if 'dia' in tabla.columns: - tabla.loc[tabla.dia == 'weekday', 'dia'] = 'Día hábil' - tabla.loc[tabla.dia == 'weekend', 'dia'] = 'Fin de semana' - if 'day_type' in tabla.columns: - tabla.loc[tabla.day_type == 'weekday', 'day_type'] = 'Día hábil' - tabla.loc[tabla.day_type == 'weekend', 'day_type'] = 'Fin de semana' - - if 'nombre_linea' in tabla.columns: - tabla['nombre_linea'] = tabla['nombre_linea'].str.replace(' -', '') - if 'Modo' in tabla.columns: - tabla['Modo'] = tabla['Modo'].str.capitalize() - - return tabla - - -@st.cache_data -def get_logo(): - file_logo = os.path.join( - "docs", "urbantrips_logo.jpg") - if not os.path.isfile(file_logo): - # URL of the image file on Github - url = 'https://raw.githubusercontent.com/EL-BID/UrbanTrips/main/docs/urbantrips_logo.jpg' - - # Send a request to get the content of the image file - response = requests.get(url) - - # Save the content to a local file - with open(file_logo, 'wb') as f: - f.write(response.content) - image = Image.open(file_logo) - return image + return fig def plot_lineas(lineas, id_linea, nombre_linea, day_type, n_sections, rango): - gdf = lineas[(lineas.id_linea == id_linea) & - (lineas.day_type == day_type) & - (lineas.n_sections == n_sections)].copy() - gdf_d0 = lineas[(lineas.id_linea == id_linea) & (lineas.day_type == day_type) & (lineas.n_sections == n_sections) & @@ -177,53 +82,23 @@ def plot_lineas(lineas, id_linea, nombre_linea, day_type, n_sections, rango): (lineas.day_type == day_type) & (lineas.n_sections == n_sections) & (lineas.sentido == 'vuelta')].copy() + epsg_m = get_epsg_m() + gdf_d0 = gdf_d0.to_crs(epsg=epsg_m) + gdf_d1 = gdf_d1.to_crs(epsg=epsg_m) - indicator = 'prop_etapas' - - gdf_d0[indicator] = (gdf_d0['cantidad_etapas'] / - gdf_d0['cantidad_etapas'].sum() * 100).round(2) - gdf_d1[indicator] = (gdf_d1['cantidad_etapas'] / - gdf_d1['cantidad_etapas'].sum() * 100).round(2) - - # creating plot - - f = plt.figure(tight_layout=True, figsize=(18, 10), dpi=8) - gs = f.add_gridspec(nrows=3, ncols=2) - ax1 = f.add_subplot(gs[0:2, 0]) - ax2 = f.add_subplot(gs[0:2, 1]) - ax3 = f.add_subplot(gs[2, 0]) - ax4 = f.add_subplot(gs[2, 1]) - - font_dicc = {'fontsize': 18, - 'fontweight': 'bold'} - - gdf_d0.plot(ax=ax1, color='purple', alpha=.7, linewidth=gdf_d0[indicator]) - gdf_d1.plot(ax=ax2, color='orange', alpha=.7, linewidth=gdf_d1[indicator]) - - ax1.set_axis_off() - ax2.set_axis_off() - - ax1.set_title('IDA', fontdict=font_dicc) - ax2.set_title('VUELTA', fontdict=font_dicc) - - # Set title and plot axis - if indicator == 'cantidad_etapas': - title = 'Segmentos del recorrido - Cantidad de etapas' - y_axis_lable = 'Cantidad de etapas por sentido' - elif indicator == 'prop_etapas': - title = 'Segmentos del recorrido - Porcentaje de etapas totales' - y_axis_lable = 'Porcentaje del total de etapas' - else: - raise Exception( - "Indicador debe ser 'cantidad_etapas' o 'prop_etapas'") - - if nombre_linea == '': - title = f"Id línea {id_linea} - {day_type}\n{rango}" - else: - title = f"Línea: {nombre_linea.replace('Línea ', '')} - Id línea: {id_linea} - {day_type}\n{rango}" + # Arrows + flecha_ida_wgs84 = gdf_d0.loc[gdf_d0.section_id == + gdf_d0.section_id.min(), 'geometry'] + flecha_ida_wgs84 = list(flecha_ida_wgs84.item().coords) + flecha_ida_inicio_wgs84 = flecha_ida_wgs84[0] - f.suptitle(title, fontsize=20) + flecha_vuelta_wgs84 = gdf_d1.loc[gdf_d1.section_id == + max(gdf_d1.section_id), 'geometry'] + flecha_vuelta_wgs84 = list(flecha_vuelta_wgs84.item().coords) + flecha_vuelta_fin_wgs84 = flecha_vuelta_wgs84[1] + # check if route geom is drawn from west to east + geom_dir_east = flecha_ida_inicio_wgs84[0] < flecha_vuelta_fin_wgs84[0] # Matching bar plot with route direction flecha_eo_xy = (0.4, 1.1) flecha_eo_text_xy = (0.05, 1.1) @@ -237,21 +112,6 @@ def plot_lineas(lineas, id_linea, nombre_linea, day_type, n_sections, rango): labels_oe[-1] = 'INICIO' labels_oe[0] = 'FIN' - # Arrows - flecha_ida_wgs84 = gdf_d0.loc[gdf_d0.section_id == 0.0, 'geometry'] - flecha_ida_wgs84 = list(flecha_ida_wgs84.item().coords) - flecha_ida_inicio_wgs84 = flecha_ida_wgs84[0] - flecha_ida_fin_wgs84 = flecha_ida_wgs84[1] - - flecha_vuelta_wgs84 = gdf_d1.loc[gdf_d1.section_id == - max(gdf_d1.section_id), 'geometry'] - flecha_vuelta_wgs84 = list(flecha_vuelta_wgs84.item().coords) - flecha_vuelta_inicio_wgs84 = flecha_vuelta_wgs84[0] - flecha_vuelta_fin_wgs84 = flecha_vuelta_wgs84[1] - - # check if route geom is drawn from west to east - geom_dir_east = flecha_ida_inicio_wgs84[0] < flecha_vuelta_fin_wgs84[0] - # Set arrows in barplots based on reout geom direction if geom_dir_east: @@ -279,12 +139,81 @@ def plot_lineas(lineas, id_linea, nombre_linea, day_type, n_sections, rango): gdf_d0 = gdf_d0.sort_values('section_id', ascending=False) gdf_d1 = gdf_d1.sort_values('section_id', ascending=False) + # For direction 0, get the last section of the route geom + flecha_ida = gdf_d0.loc[gdf_d0.section_id == + max(gdf_d0.section_id), 'geometry'] + flecha_ida = list(flecha_ida.item().coords) + flecha_ida_inicio = flecha_ida[1] + flecha_ida_fin = flecha_ida[0] + + # For direction 1, get the first section of the route geom + flecha_vuelta = gdf_d1.loc[gdf_d1.section_id == + gdf_d1.section_id.min(), 'geometry'] + flecha_vuelta = list(flecha_vuelta.item().coords) + + # invert the direction of the arrow + flecha_vuelta_inicio = flecha_vuelta[0] + flecha_vuelta_fin = flecha_vuelta[1] + + minx, miny, maxx, maxy = gdf_d0.total_bounds + box = create_squared_polygon(minx, miny, maxx, maxy, epsg_m) + + # st.dataframe(gdf_d0.drop('geometry', axis=1)) + # st.dataframe(gdf_d1.drop('geometry', axis=1)) + + # creando buffers en base a + gdf_d0['geometry'] = gdf_d0.geometry.buffer(gdf_d0.buff_factor) + gdf_d1['geometry'] = gdf_d1.geometry.buffer(gdf_d1.buff_factor) + + # creating plot + + f = plt.figure(tight_layout=True, figsize=(18, 10), dpi=8) + gs = f.add_gridspec(nrows=3, ncols=2) + ax1 = f.add_subplot(gs[0:2, 0]) + ax2 = f.add_subplot(gs[0:2, 1]) + ax3 = f.add_subplot(gs[2, 0]) + ax4 = f.add_subplot(gs[2, 1]) + + font_dicc = {'fontsize': 18, + 'fontweight': 'bold'} + box.plot(ax=ax1, color='#ffffff00') + box.plot(ax=ax2, color='#ffffff00') + + try: + gdf_d0.plot(ax=ax1, column='legs', cmap='BuPu', + scheme='fisherjenks', k=5, alpha=.6) + gdf_d1.plot(ax=ax2, column='legs', cmap='Oranges', + scheme='fisherjenks', k=5, alpha=.6) + except ValueError: + gdf_d0.plot(ax=ax1, color='purple', alpha=.7, + # linewidth=gdf_d0['buff_factor'] + ) + gdf_d1.plot(ax=ax2, color='orange', alpha=.7, + # linewidth=gdf_d1['buff_factor'] + ) + + ax1.set_axis_off() + ax2.set_axis_off() + + ax1.set_title('IDA', fontdict=font_dicc) + ax2.set_title('VUELTA', fontdict=font_dicc) + + title = 'Segmentos del recorrido - Porcentaje de etapas totales' + y_axis_lable = 'Porcentaje del total de etapas' + + if nombre_linea == '': + title = f"Id línea {id_linea} - {day_type}\n{rango}" + else: + title = f"Línea: {nombre_linea.replace('Línea ', '')} - Id línea: {id_linea} - {day_type}\n{rango}" + + f.suptitle(title, fontsize=20) + sns.barplot(data=gdf_d0, x="section_id", - y=indicator, ax=ax3, color='Purple', + y='prop', ax=ax3, color='Purple', order=gdf_d0.section_id.values) sns.barplot(data=gdf_d1, x="section_id", - y=indicator, ax=ax4, color='Orange', + y='prop', ax=ax4, color='Orange', order=gdf_d1.section_id.values) # Axis @@ -298,7 +227,7 @@ def plot_lineas(lineas, id_linea, nombre_linea, day_type, n_sections, rango): ax4.set_ylabel('') ax4.set_xlabel('') - max_y_barplot = max(gdf_d0[indicator].max(), gdf_d1[indicator].max()) + max_y_barplot = max(gdf_d0['prop'].max(), gdf_d1['prop'].max()) ax3.set_ylim(0, max_y_barplot) ax4.set_ylim(0, max_y_barplot) @@ -308,26 +237,13 @@ def plot_lineas(lineas, id_linea, nombre_linea, day_type, n_sections, rango): ax4.spines.right.set_visible(False) ax4.spines.top.set_visible(False) - # For direction 0, get the last section of the route geom - flecha_ida = gdf_d0.loc[gdf_d0.section_id == - max(gdf_d0.section_id), 'geometry'] - flecha_ida = list(flecha_ida.item().coords) - flecha_ida_inicio = flecha_ida[1] - flecha_ida_fin = flecha_ida[0] - - # For direction 1, get the first section of the route geom - flecha_vuelta = gdf_d1.loc[gdf_d1.section_id == 0.0, 'geometry'] - flecha_vuelta = list(flecha_vuelta.item().coords) - # invert the direction of the arrow - flecha_vuelta_inicio = flecha_vuelta[0] - flecha_vuelta_fin = flecha_vuelta[1] - ax1.annotate('', xy=(flecha_ida_inicio[0], flecha_ida_inicio[1]), xytext=(flecha_ida_fin[0], flecha_ida_fin[1]), arrowprops=dict(facecolor='black', - edgecolor='black'), + edgecolor='black', + shrink=0.2), ) ax2.annotate('', xy=(flecha_vuelta_inicio[0], @@ -335,7 +251,8 @@ def plot_lineas(lineas, id_linea, nombre_linea, day_type, n_sections, rango): xytext=(flecha_vuelta_fin[0], flecha_vuelta_fin[1]), arrowprops=dict(facecolor='black', - edgecolor='black'), + edgecolor='black', + shrink=0.2), ) ax3.annotate('Sentido', xy=flecha_ida_xy, xytext=flecha_ida_text_xy, @@ -358,8 +275,10 @@ def plot_lineas(lineas, id_linea, nombre_linea, day_type, n_sections, rango): cx.add_basemap(ax2, crs=gdf_d1.crs.to_string(), source=prov, attribution_size=7) except (UnidentifiedImageError, ValueError): - cx.add_basemap(ax1, crs=gdf_d0.crs.to_string(), attribution_size=7) - cx.add_basemap(ax2, crs=gdf_d1.crs.to_string(), attribution_size=7) + cx.add_basemap(ax1, crs=gdf_d0.crs.to_string(), + attribution_size=7) + cx.add_basemap(ax2, crs=gdf_d1.crs.to_string(), + attribution_size=7) except (r_ConnectionError): pass @@ -377,74 +296,84 @@ def traigo_nombre_lineas(df): logo = get_logo() st.image(logo) +id_linea = '' +secciones_ = '' with st.expander('Cargas por horas'): col1, col2 = st.columns([1, 4]) kpi_lineas = levanto_tabla_sql('basic_kpi_by_line_hr') - nl1 = traigo_nombre_lineas(kpi_lineas) - if len(kpi_lineas) > 0: - if len(nl1) > 0: - nombre_linea_kpi = col1.selectbox('Línea  ', options=nl1) - id_linea_kpi = kpi_lineas[kpi_lineas.nombre_linea == - nombre_linea_kpi].id_linea.values[0] + nl1 = traigo_nombre_lineas(kpi_lineas) + + if len(kpi_lineas) > 0: + if len(nl1) > 0: + nombre_linea_kpi = col1.selectbox('Línea  ', options=nl1) + id_linea_kpi = kpi_lineas[kpi_lineas.nombre_linea == + nombre_linea_kpi].id_linea.values[0] + else: + nombre_linea_kpi = '' + id_linea_kpi = col1.selectbox( + 'Línea ', options=kpi_lineas.id_linea.unique()) + + day_type_kpi = col1.selectbox( + 'Tipo de dia  ', options=kpi_lineas.dia.unique()) + + # add month and year + yr_mo_kpi = col1.selectbox( + 'Periodo  ', options=kpi_lineas.yr_mo.unique(), key='year_month') + + + kpi_stats_line_plot = kpi_lineas[(kpi_lineas.id_linea == id_linea_kpi) & ( + kpi_lineas.dia == day_type_kpi) & (kpi_lineas.yr_mo == yr_mo_kpi)] + + # if col2.checkbox('Ver datos: cargas por hora'): + # col2.write(kpi_stats_line_plot) + + if len(kpi_stats_line_plot) > 0: + + # Grafico Factor de Oocupación + f, ax = plt.subplots(figsize=(10, 4)) + sns.barplot(data=kpi_stats_line_plot, x='hora', y='of', + color='silver', ax=ax, label='Factor de ocupación') + + sns.lineplot(data=kpi_stats_line_plot, x="hora", y="veh", ax=ax, + color='Purple', label='Oferta - veh/hr') + sns.lineplot(data=kpi_stats_line_plot, x="hora", y="pax", ax=ax, + color='Orange', label='Demanda - pax/hr') + + ax.set_xlabel("Hora") + ax.set_ylabel("Factor de Ocupación (%)") + + ax.set_title(f"Indicadores de oferta y demanda estadarizados\nLínea: {nombre_linea_kpi.replace('Línea ', '')} - Id linea: {id_linea_kpi} - {day_type_kpi}", + fontdict={'size': 12}) + + # Add a footnote below and to the right side of the chart + note = """ + *Los indicadores de Oferta y Demanda se estandarizaron para que + coincidan con el eje de Factor de Ocupación + """ + ax_note = ax.annotate(note, + xy=(0, -.22), + xycoords='axes fraction', + ha='left', + va="center", + fontsize=7) + ax.spines.right.set_visible(False) + ax.spines.top.set_visible(False) + ax.spines.bottom.set_visible(False) + ax.spines.left.set_visible(False) + ax.spines.left.set_position(('outward', 10)) + ax.spines.bottom.set_position(('outward', 10)) + + box = ax.get_position() + ax.set_position([box.x0, box.y0, box.width * 0.75, box.height]) + # Put a legend to the right of the current axis + ax.tick_params(axis='both', which='major', labelsize=8) + ax.legend(loc='center left', bbox_to_anchor=(1, 0.5), fontsize=8) + col2.pyplot(f) else: - nombre_linea_kpi = '' - id_linea_kpi = col1.selectbox( - 'Línea ', options=kpi_lineas.id_linea.unique()) - - day_type_kpi = col1.selectbox( - 'Tipo de dia  ', options=kpi_lineas.dia.unique()) - - kpi_stats_line_plot = kpi_lineas[(kpi_lineas.id_linea == id_linea_kpi) & ( - kpi_lineas.dia == day_type_kpi)] - - # if col2.checkbox('Ver datos: cargas por hora'): - # col2.write(kpi_stats_line_plot) - - if len(kpi_stats_line_plot) > 0: - - # Grafico Factor de Oocupación - f, ax = plt.subplots(figsize=(10, 4)) - sns.barplot(data=kpi_stats_line_plot, x='hora', y='of', - color='silver', ax=ax, label='Factor de ocupación') - - sns.lineplot(data=kpi_stats_line_plot, x="hora", y="veh", ax=ax, - color='Purple', label='Oferta - veh/hr') - sns.lineplot(data=kpi_stats_line_plot, x="hora", y="pax", ax=ax, - color='Orange', label='Demanda - pax/hr') - - ax.set_xlabel("Hora") - ax.set_ylabel("Factor de Ocupación (%)") - - ax.set_title(f"Indicadores de oferta y demanda estadarizados\nLínea: {nombre_linea_kpi.replace('Línea ', '')} - Id linea: {id_linea_kpi} - {day_type_kpi}", - fontdict={'size': 12}) - - # Add a footnote below and to the right side of the chart - note = """ - *Los indicadores de Oferta y Demanda se estandarizaron para que - coincidan con el eje de Factor de Ocupación - """ - ax_note = ax.annotate(note, - xy=(0, -.22), - xycoords='axes fraction', - ha='left', - va="center", - fontsize=7) - ax.spines.right.set_visible(False) - ax.spines.top.set_visible(False) - ax.spines.bottom.set_visible(False) - ax.spines.left.set_visible(False) - ax.spines.left.set_position(('outward', 10)) - ax.spines.bottom.set_position(('outward', 10)) - - box = ax.get_position() - ax.set_position([box.x0, box.y0, box.width * 0.75, box.height]) - # Put a legend to the right of the current axis - ax.tick_params(axis='both', which='major', labelsize=8) - ax.legend(loc='center left', bbox_to_anchor=(1, 0.5), fontsize=8) - col2.pyplot(f) + st.write('No hay datos para mostrar') else: st.write('No hay datos para mostrar') @@ -452,12 +381,16 @@ def traigo_nombre_lineas(df): col1, col2 = st.columns([1, 4]) - lineas = levanto_tabla_sql('ocupacion_por_linea_tramo', has_wkt=True) + lineas = levanto_tabla_sql('ocupacion_por_linea_tramo') nl2 = traigo_nombre_lineas(lineas[['id_linea', 'nombre_linea']]) - lineas['rango'] = 'de ' + \ - lineas['hora_min'].astype(str)+' a ' + \ - lineas['hora_max'].astype(str) + ' hs' + lineas.loc[lineas['hour_min'].notna(), 'rango'] = 'de ' + \ + lineas.loc[lineas['hour_min'].notna(), 'hour_min'].astype(int).astype(str) + ' a ' + \ + lineas.loc[lineas['hour_max'].notna(), 'hour_max'].astype( + int).astype(str) + ' hrs' + + lineas.loc[lineas['hour_min'].isna(), 'rango'] = "Todo el dia" + if len(lineas) > 0: if len(nl2) > 0: nombre_linea = col1.selectbox('Línea  ', options=nl2) @@ -474,8 +407,12 @@ def traigo_nombre_lineas(df): 'Secciones ', options=lineas.n_sections.unique()) rango = col1.selectbox('Rango horario ', options=lineas.rango.unique()) + # add month and year + yr_mo_kpi_sl = col1.selectbox( + 'Periodo  ', options=lineas.yr_mo.unique(), key='year_month_section_load') + lineas = lineas[(lineas.id_linea == id_linea) & (lineas.day_type == day_type) & ( - lineas.n_sections == n_sections) & (lineas.rango == rango)] + lineas.n_sections == n_sections) & (lineas.rango == rango) & (lineas.yr_mo == yr_mo_kpi_sl)] # if col2.checkbox('Ver datos: cargas por tramos'): # col2.write(lineas) @@ -486,3 +423,203 @@ def traigo_nombre_lineas(df): col2.pyplot(f_lineas) else: st.write('No hay datos para mostrar') + +with st.expander('Matriz OD por linea'): + col1, col2 = st.columns([1, 4]) + + matriz = levanto_tabla_sql('matrices_linea') + nl3 = traigo_nombre_lineas(matriz[['id_linea', 'nombre_linea']]) + + matriz.loc[matriz['hour_min'].notna(), 'rango'] = 'de ' + \ + matriz.loc[matriz['hour_min'].notna(), 'hour_min'].astype(int).astype(str) + ' a ' + \ + matriz.loc[matriz['hour_max'].notna(), 'hour_max'].astype( + int).astype(str) + ' hrs' + + matriz.loc[matriz['hour_min'].isna(), 'rango'] = "Todo el dia" + + if len(matriz) > 0: + if len(nl3) > 0: + nombre_linea_ = col1.selectbox( + 'Línea  ', options=nl3, key='nombre_linea_3') + id_linea = matriz[matriz.nombre_linea == + nombre_linea_].id_linea.values[0] + else: + nombre_linea = '' + id_linea = col1.selectbox( + 'Línea  ', options=matriz.id_linea.unique(), key='id_linea_3') + + if col1.checkbox('Normalizar', value=True): + values = 'prop' + else: + values = 'legs' + + matriz = matriz[matriz.id_linea == id_linea] + + desc_dia_ = col1.selectbox( + 'Periodo ', options=matriz.yr_mo.unique()) + + matriz = matriz[matriz.yr_mo == desc_dia_] + + tipo_dia_ = col1.selectbox( + 'Tipo de dia ', options=matriz.day_type.unique(), key='day_type_line_matrix') + + matriz = matriz[matriz.day_type == tipo_dia_] + + secciones_ = col1.selectbox( + 'Cantidad de secciones', options=matriz.n_sections.unique()) + + matriz = matriz[matriz.n_sections == secciones_] + + rango_ = col1.selectbox( + 'Rango horario ', options=matriz.rango.unique(), key='rango_nl3') + + matriz = matriz[matriz.rango == rango_] + + od_heatmap = matriz.pivot_table(values=values, + index='Origen', + columns='Destino') + + fig = px.imshow(od_heatmap, text_auto=True, + color_continuous_scale='Blues',) + + fig.update_coloraxes(showscale=True) + + if len(od_heatmap) <= 20: + fig.update_layout(width=800, height=800) + elif (len(od_heatmap) > 20) & (len(od_heatmap) <= 40): + fig.update_layout(width=1000, height=1000) + elif len(od_heatmap) > 40: + fig.update_layout(width=1200, height=1200) + + col2.plotly_chart(fig) + + else: + st.write('No hay datos para mostrar') + + zonas = levanto_tabla_sql('matrices_linea_carto') + + zonas = zonas.loc[ + (zonas.id_linea == id_linea) & + (zonas.n_sections == secciones_), :] + + col1, col2 = st.columns([1, 4]) + + if col1.checkbox('Mostrar zonificacion'): + + if len(zonas) > 0: + + # Create a folium map centered on the data + map_center = [zonas.geometry.centroid.y.mean( + ), zonas.geometry.centroid.x.mean()] + + fig = Figure(width=800, height=800) + m = folium.Map(location=map_center, zoom_start=10, + tiles='cartodbpositron') + + # Add GeoDataFrame to the map + folium.GeoJson(zonas).add_to(m) + + for idx, row in zonas.iterrows(): + # Replace 'column_name' with the name of the column containing the detail + detail = f"Sección {row['section_id']}" + point = [row['geometry'].representative_point( + ).y, row['geometry'].representative_point().x] + marker = folium.CircleMarker( + location=point, popup=detail, + color='black', radius=2, + fill=True, + fill_color='black', + fill_opacity=1,) + marker.add_to(m) + + # Display the map using folium_static + with col2: + folium_static(m) + +with st.expander('Líneas de deseo por linea'): + col1, col2 = st.columns([1, 4]) + custom_query = """ + select m.*, co.x as lon_o, co.y as lat_o, cd.x as lon_d, cd.y as lat_d + from matrices_linea m + left join matrices_linea_carto co + on m.id_linea = co.id_linea + and m.n_sections = co.n_sections + and m.Origen = co.section_id + left join matrices_linea_carto cd + on m.id_linea = cd.id_linea + and m.n_sections = cd.n_sections + and m.Destino = cd.section_id + where lon_o is not NULL + and lat_o is not NULL ; + """ + matriz = levanto_tabla_sql(tabla_sql='matrices_linea', + custom_query=custom_query, + ) + + matriz = create_linestring_od(matriz) + + nl4 = traigo_nombre_lineas(matriz[['id_linea', 'nombre_linea']]) + + matriz.loc[matriz['hour_min'].notna(), 'rango'] = 'de ' + \ + matriz.loc[matriz['hour_min'].notna(), 'hour_min'].astype(int).astype(str) + ' a ' + \ + matriz.loc[matriz['hour_max'].notna(), 'hour_max'].astype( + int).astype(str) + ' hrs' + + matriz.loc[matriz['hour_min'].isna(), 'rango'] = "Todo el dia" + + if len(matriz) > 0: + if len(nl4) > 0: + nombre_linea_ = col1.selectbox( + 'Línea  ', options=nl4, key='nombre_linea_ldeseo_od') + id_linea = matriz[matriz.nombre_linea == + nombre_linea_].id_linea.values[0] + else: + nombre_linea = '' + id_linea = col1.selectbox( + 'Línea ', options=matriz.id_linea.unique(), key='linea_4') + + matriz = matriz[matriz.id_linea == id_linea] + + desc_dia_ = col1.selectbox( + 'Periodo ', options=matriz.yr_mo.unique(), key='desc_deseo') + + matriz = matriz[matriz.yr_mo == desc_dia_] + + tipo_dia_ = col1.selectbox( + 'Tipo de dia ', options=matriz.day_type.unique(), key='day_type_line_matrix2') + + matriz = matriz[matriz.day_type == tipo_dia_] + + secciones_ = col1.selectbox( + 'Cantidad de secciones', options=matriz.n_sections.unique(), key='secc_deseo') + + matriz = matriz[matriz.n_sections == secciones_] + + rango_ = col1.selectbox( + 'Rango horario ', options=matriz.rango.unique(), key='reango_deseo') + + matriz = matriz[matriz.rango == rango_] + + with col2: + k_jenks = st.slider('Cantidad de grupos', min_value=1, + max_value=5, value=5) + st.text(f"Hay un total de {matriz.legs.sum()} etapas") + map = crear_mapa_folium(matriz, + cmap='BuPu', + var_fex='legs', + k_jenks=k_jenks + ) + + st_map = st_folium(map, width=900, height=700) + else: + col2.write('No hay datos para mostrar') + # col2.markdown(""" + # + # """, unsafe_allow_html=True) + + # col2.markdown( + # '

¡¡ No hay datos para mostrar !!

', unsafe_allow_html=True) diff --git "a/urbantrips/dashboard/pages/3_L\303\255neas de Deseo.py" "b/urbantrips/dashboard/pages/3_L\303\255neas de Deseo.py" new file mode 100644 index 0000000..52c9cf7 --- /dev/null +++ "b/urbantrips/dashboard/pages/3_L\303\255neas de Deseo.py" @@ -0,0 +1,593 @@ +import streamlit as st +import pandas as pd +import folium +from streamlit_folium import folium_static +import mapclassify +import plotly.express as px +from folium import Figure +from dash_utils import ( + levanto_tabla_sql, get_logo, + create_data_folium, traigo_indicadores, + extract_hex_colors_from_cmap +) + + +def crear_mapa_lineas_deseo(df_viajes, + df_etapas, + zonif, + origenes, + destinos, + var_fex, + cmap_viajes='Blues', + cmap_etapas='Greens', + map_title='', + savefile='', + k_jenks=5, + ): + + m = '' + if (len(df_viajes) > 0) | (len(df_etapas) > 0) | (len(origenes) > 0) | (len(destinos) > 0): + if len(df_etapas) > 0: + y_val = etapas.geometry.representative_point().y.mean() + x_val = etapas.geometry.representative_point().x.mean() + elif len(df_viajes) > 0: + y_val = viajes.geometry.representative_point().y.mean() + x_val = viajes.geometry.representative_point().x.mean() + elif len(origenes) > 0: + y_val = origenes.geometry.representative_point().y.mean() + x_val = origenes.geometry.representative_point().x.mean() + elif len(destinos) > 0: + y_val = destinos.geometry.representative_point().y.mean() + x_val = destinos.geometry.representative_point().x.mean() + + fig = Figure(width=2000, height=2000) + m = folium.Map(location=[y_val, x_val], + zoom_start=10, tiles='cartodbpositron') + + colors_viajes = extract_hex_colors_from_cmap( + cmap='viridis_r', n=k_jenks) + colors_etapas = extract_hex_colors_from_cmap(cmap='magma_r', n=k_jenks) + + # Etapas + line_w = 0.5 + if len(df_etapas) > 0: + try: + bins = [df_etapas[var_fex].min()-1] + \ + mapclassify.FisherJenks( + df_etapas[var_fex], k=k_jenks).bins.tolist() + except ValueError: + bins = [df_etapas[var_fex].min()-1] + \ + mapclassify.FisherJenks( + df_etapas[var_fex], k=k_jenks-3).bins.tolist() + + range_bins = range(0, len(bins)-1) + bins_labels = [ + f'{int(bins[n])} a {int(bins[n+1])} etapas' for n in range_bins] + df_etapas['cuts'] = pd.cut( + df_etapas[var_fex], bins=bins, labels=bins_labels) + + n = 0 + for i in bins_labels: + + df_etapas[df_etapas.cuts == i].explore( + m=m, + color=colors_etapas[n], + style_kwds={'fillOpacity': 0.1, 'weight': line_w}, + name=i, + tooltip=False, + ) + n += 1 + line_w += 3 + + # Viajes + line_w = 0.5 + if len(df_viajes) > 0: + try: + bins = [df_viajes[var_fex].min()-1] + \ + mapclassify.FisherJenks( + df_viajes[var_fex], k=k_jenks).bins.tolist() + except ValueError: + bins = [df_viajes[var_fex].min()-1] + \ + mapclassify.FisherJenks( + df_viajes[var_fex], k=k_jenks-2).bins.tolist() + + range_bins = range(0, len(bins)-1) + bins_labels = [ + f'{int(bins[n])} a {int(bins[n+1])} viajes' for n in range_bins] + df_viajes['cuts'] = pd.cut( + df_viajes[var_fex], bins=bins, labels=bins_labels) + + n = 0 + for i in bins_labels: + + df_viajes[df_viajes.cuts == i].explore( + m=m, + color=colors_viajes[n], + style_kwds={'fillOpacity': 0.1, 'weight': line_w}, + name=i, + tooltip=False, + ) + n += 1 + line_w += 3 + + if len(origenes) > 0: + try: + bins = [origenes['factor_expansion_linea'].min()-1] + \ + mapclassify.FisherJenks( + origenes['factor_expansion_linea'], k=5).bins.tolist() + except ValueError: + bins = [origenes['factor_expansion_linea'].min()-1] + \ + mapclassify.FisherJenks( + origenes['factor_expansion_linea'], k=5-3).bins.tolist() + + range_bins = range(0, len(bins)-1) + bins_labels = [ + f'{int(bins[n])} a {int(bins[n+1])} origenes' for n in range_bins] + + origenes['cuts'] = pd.cut( + origenes['factor_expansion_linea'], bins=bins, labels=bins_labels) + + n = 0 + line_w = 10 + for i in bins_labels: + + origenes[origenes.cuts == i].explore( + m=m, + color="#0173b299", + style_kwds={'fillOpacity': 0.1, 'weight': line_w}, + name=i, + tooltip=False, + ) + n += 1 + line_w += 5 + + if len(destinos) > 0: + try: + bins = [destinos['factor_expansion_linea'].min()-1] + \ + mapclassify.FisherJenks( + destinos['factor_expansion_linea'], k=5).bins.tolist() + except ValueError: + bins = [destinos['factor_expansion_linea'].min()-1] + \ + mapclassify.FisherJenks( + destinos['factor_expansion_linea'], k=5-3).bins.tolist() + + range_bins = range(0, len(bins)-1) + bins_labels = [ + f'{int(bins[n])} a {int(bins[n+1])} destinos' for n in range_bins] + + destinos['cuts'] = pd.cut( + destinos['factor_expansion_linea'], bins=bins, labels=bins_labels) + + n = 0 + line_w = 10 + for i in bins_labels: + + destinos[destinos.cuts == i].explore( + m=m, + color="#de8f0599", + style_kwds={'fillOpacity': 0.1, 'weight': line_w}, + name=i, + tooltip=False, + ) + n += 1 + line_w += 5 + + # Agrego zonificación + if len(zonif) > 0: + geojson = zonif.to_json() + # Add the GeoJSON to the map as a GeoJson Layer + folium.GeoJson( + geojson, + name='Zonificación', + style_function=lambda feature: { + 'fillColor': 'navy', + 'color': 'navy', + 'weight': .5, + 'fillOpacity': .2, + + }, + tooltip=folium.GeoJsonTooltip( + fields=['id'], labels=False, sticky=False) + ).add_to(m) + + folium.LayerControl(name='xxx').add_to(m) + + return m + +def traigo_socio_indicadores(socio_indicadores): + + df = socio_indicadores[socio_indicadores.tabla=='viajes-genero-tarifa'].copy() + totals = pd.crosstab(values=df.factor_expansion_linea, columns=df.Genero, index=df.Tarifa, aggfunc='sum', margins=True, margins_name='Total', normalize=False).fillna(0).round().astype(int).apply(lambda col: col.map(lambda x: f'{x:,.0f}'.replace(',', '.'))) + totals_porc = (pd.crosstab(values=df.factor_expansion_linea, columns=df.Genero, index=df.Tarifa, aggfunc='sum', margins=True, margins_name='Total', normalize=True) * 100).round(2) + + modos = socio_indicadores[socio_indicadores.tabla=='etapas-genero-modo'].copy() + modos_genero_abs = pd.crosstab(values=modos.factor_expansion_linea, index=[modos.Genero], columns=modos.Modo, aggfunc='sum', normalize=False, margins=True, margins_name='Total').fillna(0).astype(int).apply(lambda col: col.map(lambda x: f'{x:,.0f}'.replace(',', '.'))) + modos_genero_porc = (pd.crosstab(values=modos.factor_expansion_linea, index=modos.Genero, columns=modos.Modo, aggfunc='sum', normalize=True, margins=True, margins_name='Total') * 100).round(2) + + modos = socio_indicadores[socio_indicadores.tabla=='etapas-tarifa-modo'].copy() + modos_tarifa_abs = pd.crosstab(values=modos.factor_expansion_linea, index=[modos.Tarifa], columns=modos.Modo, aggfunc='sum', normalize=False, margins=True, margins_name='Total').fillna(0).astype(int).apply(lambda col: col.map(lambda x: f'{x:,.0f}'.replace(',', '.'))) + modos_tarifa_porc = (pd.crosstab(values=modos.factor_expansion_linea, index=modos.Tarifa, columns=modos.Modo, aggfunc='sum', normalize=True, margins=True, margins_name='Total') * 100).round(2) + + avg_distances = pd.crosstab(values=df.Distancia, columns=df.Genero, index=df.Tarifa, margins=True, margins_name='Total',aggfunc=lambda x: (x * df.loc[x.index, 'factor_expansion_linea']).sum() / df.loc[x.index, 'factor_expansion_linea'].sum(), ).fillna(0).round(2) + avg_times = pd.crosstab(values=df['Tiempo de viaje'], columns=df.Genero, index=df.Tarifa, margins=True, margins_name='Total',aggfunc=lambda x: (x * df.loc[x.index, 'factor_expansion_linea']).sum() / df.loc[x.index, 'factor_expansion_linea'].sum(), ).fillna(0).round(2) + avg_velocity = pd.crosstab(values=df['Velocidad'], columns=df.Genero, index=df.Tarifa, margins=True, margins_name='Total',aggfunc=lambda x: (x * df.loc[x.index, 'factor_expansion_linea']).sum() / df.loc[x.index, 'factor_expansion_linea'].sum(), ).fillna(0).round(2) + avg_etapas = pd.crosstab(values=df['Etapas promedio'], columns=df.Genero, index=df.Tarifa, margins=True, margins_name='Total',aggfunc=lambda x: (x * df.loc[x.index, 'factor_expansion_linea']).sum() / df.loc[x.index, 'factor_expansion_linea'].sum(), ).round(2).fillna('') + user = socio_indicadores[socio_indicadores.tabla=='usuario-genero-tarifa'].copy() + avg_viajes = pd.crosstab(values=user['Viajes promedio'], index=[user.Tarifa], columns=user.Genero, margins=True, margins_name='Total', aggfunc=lambda x: (x * user.loc[x.index, 'factor_expansion_linea']).sum() / user.loc[x.index, 'factor_expansion_linea'].sum(),).round(2).fillna('') + + avg_tiempo_entre_viajes = pd.crosstab(values=df['Tiempo entre viajes'], columns=df.Genero, index=df.Tarifa, margins=True, margins_name='Total',aggfunc=lambda x: (x * df.loc[x.index, 'factor_expansion_linea']).sum() / df.loc[x.index, 'factor_expansion_linea'].sum(), ).fillna(0).round(2) + + return totals, totals_porc, avg_distances, avg_times, avg_velocity, modos_genero_abs, modos_genero_porc, modos_tarifa_abs, modos_tarifa_porc, avg_viajes, avg_etapas, avg_tiempo_entre_viajes + + +st.set_page_config(layout="wide") + +logo = get_logo() +st.image(logo) + + +with st.expander('Líneas de Deseo', expanded=True): + + col1, col2 = st.columns([1, 4]) + + etapas_all = levanto_tabla_sql('agg_etapas') + matrices_all = levanto_tabla_sql('agg_matrices') + zonificaciones = levanto_tabla_sql('zonificaciones') + socio_indicadores = levanto_tabla_sql('socio_indicadores') + + if len(etapas_all) > 0: + etapas_all = etapas_all[etapas_all.factor_expansion_linea > 0].copy() + general, modal, distancias = traigo_indicadores('all') + + etapas_lst = ['Todos'] + etapas_all.mes.unique().tolist() + desc_mes = col1.selectbox( + 'Mes', options=etapas_lst) + + desc_tipo_dia = col1.selectbox( + 'Tipo día', options=etapas_all.tipo_dia.unique()) + + desc_zona = col1.selectbox( + 'Zonificación', options=etapas_all.zona.unique()) + zonif = zonificaciones[zonificaciones.zona == desc_zona] + + desc_etapas = col1.checkbox( + 'Etapas', value=True) + + desc_viajes = col1.checkbox( + 'Viajes', value=False) + + desc_origenes = col1.checkbox( + ':blue[Origenes]', value=False) + + desc_destinos = col1.checkbox( + ':orange[Destinos]', value=False) + + transf_list_all = ['Todos', 'Con transferencia', 'Sin transferencia'] + transf_list = col1.selectbox( + 'Transferencias', options=transf_list_all) + + modos_list_all = ['Todos']+etapas_all[etapas_all.modo_agregado != + '99'].modo_agregado.unique().tolist() + modos_list = [text.capitalize() for text in modos_list_all] + modos_list = col1.selectbox( + 'Modos', options=modos_list_all) + + rango_hora_all = ['Todos']+etapas_all[etapas_all.rango_hora != + '99'].rango_hora.unique().tolist() + rango_hora = [text.capitalize() for text in rango_hora_all] + rango_hora = col1.selectbox( + 'Rango hora', options=rango_hora_all) + + distancia_all = ['Todas']+etapas_all[etapas_all.distancia != + '99'].distancia.unique().tolist() + distancia = col1.selectbox( + 'Distancia', options=distancia_all) + + if desc_mes != 'Todos': + etapas_ = etapas_all[(etapas_all.zona == desc_zona)&(etapas_all.mes==desc_mes)&(etapas_all.tipo_dia==desc_tipo_dia)].copy() + matrices_ = matrices_all[(matrices_all.zona == desc_zona)&(matrices_all.mes==desc_mes)&(matrices_all.tipo_dia==desc_tipo_dia)].copy() + socio_indicadores_ = socio_indicadores[(socio_indicadores.mes==desc_mes)&(socio_indicadores.tipo_dia==desc_tipo_dia)].copy() + + + + else: + etapas_ = etapas_all[(etapas_all.zona == desc_zona)&(etapas_all.tipo_dia==desc_tipo_dia)].copy() + matrices_ = matrices_all[(matrices_all.zona == desc_zona)&(matrices_all.tipo_dia==desc_tipo_dia)].copy() + socio_indicadores_ = socio_indicadores[(socio_indicadores.tipo_dia==desc_tipo_dia)].copy() + + # col2.write(etapas_.columns.tolist()) + # col2.write(matrices_.columns.tolist()) + # col2.write(socio_indicadores_.columns.tolist()) + + # col2.write(etapas_) + # col2.write(matrices_) + # col2.write(socio_indicadores_) + + etapas_ = etapas_.groupby(["tipo_dia", "zona", "inicio_norm", "transfer1_norm", "transfer2_norm", "fin_norm", "transferencia", "modo_agregado", "rango_hora", "genero", "tarifa", "distancia"], as_index=False)[[ + "distance_osm_drive", "distance_osm_drive_etapas", "travel_time_min", "travel_speed", "lat1_norm", "lon1_norm", "lat2_norm", "lon2_norm", "lat3_norm", "lon3_norm", "lat4_norm", "lon4_norm", "factor_expansion_linea" + ]] .mean().round(2) + + + matrices_ = matrices_.groupby(["id_polygon", "tipo_dia", "zona", "inicio", "fin", "transferencia", "modo_agregado", "rango_hora", "genero", "tarifa", "distancia", "orden_origen", "orden_destino", "Origen", "Destino", ], as_index=False)[[ + "lat1", "lon1", "lat4", "lon4", "distance_osm_drive", "travel_time_min", "travel_speed", "factor_expansion_linea" + ]] .mean().round(2) + + socio_indicadores_ = socio_indicadores_.groupby(["tabla", "tipo_dia", "Genero", "Tarifa", "Modo"], as_index=False)[[ + "Distancia", "Tiempo de viaje", "Velocidad", "Etapas promedio", "Viajes promedio", "Tiempo entre viajes", "factor_expansion_linea" + ]] .mean().round(2) + + + + + + general_ = general[general.mes==desc_mes] + modal_ = modal[modal.mes==desc_mes] + distancias_ = distancias[distancias.mes==desc_mes] + + + + general_ = general.loc[general.mes==desc_mes, ['Tipo', 'Indicador', 'Valor']].set_index('Tipo') + modal_ = modal.loc[modal.mes==desc_mes, ['Tipo', 'Indicador', 'Valor']].set_index('Tipo') + distancias_ = distancias.loc[distancias.mes==desc_mes, ['Tipo', 'Indicador', 'Valor']].set_index('Tipo') + + if transf_list == 'Todos': + desc_transfers = True + else: + desc_transfers = False + if transf_list == 'Con transferencia': + etapas_ = etapas_[(etapas_.transferencia == 1)] + matrices_ = matrices_[(matrices_.transferencia == 1)] + elif transf_list == 'Sin transferencia': + etapas_ = etapas_[(etapas_.transferencia == 0)] + matrices_ = matrices_[(matrices_.transferencia == 0)] + else: + etapas_ = pd.DataFrame([]) + matrices_ = pd.DataFrame([]) + + if modos_list == 'Todos': + desc_modos = True + else: + desc_modos = False + etapas_ = etapas_[ + (etapas_.modo_agregado.str.lower() == modos_list.lower())] + matrices_ = matrices_[ + (matrices_.modo_agregado.str.lower() == modos_list.lower())] + + if rango_hora == 'Todos': + desc_horas = True + else: + desc_horas = False + etapas_ = etapas_[(etapas_.rango_hora == rango_hora)] + matrices_ = matrices_[(matrices_.rango_hora == rango_hora)] + + if distancia == 'Todas': + desc_distancia = True + else: + desc_distancia = False + etapas_ = etapas_[(etapas_.distancia == distancia)] + matrices_ = matrices_[(matrices_.distancia == distancia)] + + agg_cols_etapas = ['zona', + 'inicio_norm', + 'transfer1_norm', + 'transfer2_norm', + 'fin_norm', + 'transferencia', + 'modo_agregado', + 'rango_hora', + 'distancia'] + agg_cols_viajes = ['zona', + 'inicio_norm', + 'fin_norm', + 'transferencia', + 'modo_agregado', + 'rango_hora', + 'distancia'] + + etapas, viajes, matriz, origenes, destinos = create_data_folium(etapas_, + matrices_, + agg_transferencias=desc_transfers, + agg_modo=desc_modos, + agg_hora=desc_horas, + agg_distancia=desc_distancia, + agg_cols_etapas=agg_cols_etapas, + agg_cols_viajes=agg_cols_viajes) + + etapas = etapas[etapas.inicio_norm != etapas.fin_norm].copy() + viajes = viajes[viajes.inicio_norm != viajes.fin_norm].copy() + + if not desc_etapas: + etapas = pd.DataFrame([]) + + if not desc_viajes: + viajes = pd.DataFrame([]) + + if not desc_origenes: + origenes = pd.DataFrame([]) + + if not desc_destinos: + destinos = pd.DataFrame([]) + + desc_zonif = col1.checkbox( + 'Mostrar zonificación', value=False) + if not desc_zonif: + zonif = '' + + if not desc_origenes: + origenes = '' + if not desc_destinos: + destinos = '' + + if (len(etapas) > 0) | (len(viajes) > 0) | (len(origenes) > 0) | (len(destinos) > 0): + + map = crear_mapa_lineas_deseo(df_viajes=viajes, + df_etapas=etapas, + zonif=zonif, + origenes=origenes, + destinos=destinos, + var_fex='factor_expansion_linea', + cmap_viajes='Blues', + cmap_etapas='Greens', + map_title='Líneas de Deseo', + savefile='', + k_jenks=5) + + with col2: + # st_map = st_folium(map, width=1200, height=1000) # + folium_static(map, width=1200, height=600) + + else: + matriz = pd.DataFrame([]) + # Usar HTML para personalizar el estilo del texto + texto_html = """ + +
+ No hay datos para mostrar +
+ """ + col2.markdown(texto_html, unsafe_allow_html=True) + texto_html = """ + +
+ Verifique que los procesos se corrieron correctamente +
+ """ + col2.markdown(texto_html, unsafe_allow_html=True) + + else: + matriz = pd.DataFrame([]) + # Usar HTML para personalizar el estilo del texto + texto_html = """ + +
+ No hay datos para mostrar +
+ """ + col2.markdown(texto_html, unsafe_allow_html=True) + texto_html = """ + +
+ Verifique que los procesos se corrieron correctamente +
+ """ + col2.markdown(texto_html, unsafe_allow_html=True) + +with st.expander('Indicadores'): + col1, col2, col3 = st.columns([2, 2, 2]) + + if len(etapas_all) > 0: + col1.table(general_) + col2.table(modal_) + col3.table(distancias_) + +with st.expander('Matrices'): + + col1, col2 = st.columns([1, 4]) + + # col2.table(matriz) + + tipo_matriz = col1.selectbox( + 'Variable', options=['Viajes', 'Distancia promedio (kms)', 'Tiempo promedio (min)', 'Velocidad promedio (km/h)']) + + normalize = False + if tipo_matriz == 'Viajes': + var_matriz = 'factor_expansion_linea' + normalize = col1.checkbox('Normalizar', value=True) + if tipo_matriz == 'Distancia promedio (kms)': + var_matriz = 'distance_osm_drive' + if tipo_matriz == 'Tiempo promedio (min)': + var_matriz = 'travel_time_min' + if tipo_matriz == 'Velocidad promedio (km/h)': + var_matriz = 'travel_speed' + + if len(matriz) > 0: + od_heatmap = pd.crosstab( + index=matriz['Origen'], + columns=matriz['Destino'], + values=matriz[var_matriz], + aggfunc="sum", + normalize=normalize, + ) + + if normalize: + od_heatmap = (od_heatmap * 100).round(2) + else: + od_heatmap = od_heatmap.round(0) + + od_heatmap = od_heatmap.reset_index() + od_heatmap['Origen'] = od_heatmap['Origen'].str[4:] + od_heatmap = od_heatmap.set_index('Origen') + od_heatmap.columns = [i[4:] for i in od_heatmap.columns] + + fig = px.imshow(od_heatmap, text_auto=True, + color_continuous_scale='Blues',) + + fig.update_coloraxes(showscale=False) + + if len(od_heatmap) <= 20: + fig.update_layout(width=800, height=800) + elif (len(od_heatmap) > 20) & (len(od_heatmap) <= 40): + fig.update_layout(width=1100, height=1100) + elif len(od_heatmap) > 40: + fig.update_layout(width=1400, height=1400) + + col2.plotly_chart(fig) + +with st.expander('Género y tarifas'): + col1, col2, col3, col4 = st.columns([1, 2, 2, 2]) + totals, totals_porc, avg_distances, avg_times, avg_velocity, modos_genero_abs, modos_genero_porc, modos_tarifa_abs, modos_tarifa_porc, avg_viajes, avg_etapas, avg_tiempo_entre_viajes = traigo_socio_indicadores(socio_indicadores_) + + col2.markdown("

Total de viajes por género y tarifa

", unsafe_allow_html=True) + col2.table(totals) + col3.markdown("

Porcentaje de viajes por género y tarifa

", unsafe_allow_html=True) + col3.table(totals_porc.round(2).astype(str)) + + col2.markdown("

Cantidad promedio de viajes por género y tarifa

", unsafe_allow_html=True) + col2.table(avg_viajes.round(2).astype(str)) + col3.markdown("

Cantidad promedio de etapas por género y tarifa

", unsafe_allow_html=True) + col3.table(avg_etapas.round(2).astype(str)) + + + col2.markdown("

Total de etapas por género y modo

", unsafe_allow_html=True) + col2.table(modos_genero_abs) + col3.markdown("

Porcentaje de etapas por género y modo

", unsafe_allow_html=True) + col3.table(modos_genero_porc.round(2).astype(str)) + + col2.markdown("

Total de etapas por tarifa y modo

", unsafe_allow_html=True) + col2.table(modos_tarifa_abs) + col3.markdown("

Porcentaje de etapas por tarifa y modo

", unsafe_allow_html=True) + col3.table(modos_tarifa_porc.round(2).astype(str)) + + col2.markdown("

Distancias promedio (kms)

", unsafe_allow_html=True) + col2.table(avg_distances.round(2).astype(str)) + + col3.markdown("

Tiempos promedio (minutos)

", unsafe_allow_html=True) + col3.table(avg_times.round(2).astype(str)) + + col2.markdown("

Velocidades promedio (kms/hora)

", unsafe_allow_html=True) + col2.table(avg_velocity.round(2).astype(str)) + + col3.markdown("

Tiempos promedio entre viajes (minutos)

", unsafe_allow_html=True) + col3.table(avg_tiempo_entre_viajes.round(2).astype(str)) + + diff --git a/urbantrips/dashboard/pages/4_Poligonos.py b/urbantrips/dashboard/pages/4_Poligonos.py new file mode 100644 index 0000000..5918f69 --- /dev/null +++ b/urbantrips/dashboard/pages/4_Poligonos.py @@ -0,0 +1,494 @@ +import streamlit as st +import pandas as pd +import folium +from streamlit_folium import folium_static +import mapclassify +import plotly.express as px +from folium import Figure +from dash_utils import ( + levanto_tabla_sql, get_logo, + create_data_folium, traigo_indicadores, + extract_hex_colors_from_cmap +) + + +def crear_mapa_poligonos(df_viajes, + df_etapas, + poly, + zonif, + origenes, + destinos, + var_fex, + cmap_viajes='Blues', + cmap_etapas='Greens', + map_title='', + savefile='', + k_jenks=5, + show_poly=False): + + m = '' + if (len(df_viajes) > 0) | (len(df_etapas) > 0) | (len(origenes) > 0) | (len(destinos) > 0): + + fig = Figure(width=2000, height=2000) + m = folium.Map(location=[poly.geometry.representative_point().y.mean( + ), poly.geometry.representative_point().x.mean()], zoom_start=10, tiles='cartodbpositron') + + colors_viajes = extract_hex_colors_from_cmap( + cmap='viridis_r', n=k_jenks) + colors_etapas = extract_hex_colors_from_cmap(cmap='magma_r', n=k_jenks) + + # Etapas + line_w = 0.5 + if len(df_etapas) > 0: + try: + bins = [df_etapas[var_fex].min()-1] + \ + mapclassify.FisherJenks( + df_etapas[var_fex], k=k_jenks).bins.tolist() + except ValueError: + bins = [df_etapas[var_fex].min()-1] + \ + mapclassify.FisherJenks( + df_etapas[var_fex], k=k_jenks-3).bins.tolist() + + range_bins = range(0, len(bins)-1) + bins_labels = [ + f'{int(bins[n])} a {int(bins[n+1])} etapas' for n in range_bins] + df_etapas['cuts'] = pd.cut( + df_etapas[var_fex], bins=bins, labels=bins_labels) + + n = 0 + for i in bins_labels: + + df_etapas[df_etapas.cuts == i].explore( + m=m, + color=colors_etapas[n], + style_kwds={'fillOpacity': 0.1, 'weight': line_w}, + name=i, + tooltip=False, + ) + n += 1 + line_w += 3 + + # Viajes + line_w = 0.5 + if len(df_viajes) > 0: + try: + bins = [df_viajes[var_fex].min()-1] + \ + mapclassify.FisherJenks( + df_viajes[var_fex], k=k_jenks).bins.tolist() + except ValueError: + bins = [df_viajes[var_fex].min()-1] + \ + mapclassify.FisherJenks( + df_viajes[var_fex], k=k_jenks-2).bins.tolist() + + range_bins = range(0, len(bins)-1) + bins_labels = [ + f'{int(bins[n])} a {int(bins[n+1])} viajes' for n in range_bins] + df_viajes['cuts'] = pd.cut( + df_viajes[var_fex], bins=bins, labels=bins_labels) + + n = 0 + for i in bins_labels: + + df_viajes[df_viajes.cuts == i].explore( + m=m, + color=colors_viajes[n], + style_kwds={'fillOpacity': 0.1, 'weight': line_w}, + name=i, + tooltip=False, + ) + n += 1 + line_w += 3 + + if len(origenes) > 0: + try: + bins = [origenes['factor_expansion_linea'].min()-1] + \ + mapclassify.FisherJenks( + origenes['factor_expansion_linea'], k=5).bins.tolist() + except ValueError: + bins = [origenes['factor_expansion_linea'].min()-1] + \ + mapclassify.FisherJenks( + origenes['factor_expansion_linea'], k=5-3).bins.tolist() + + range_bins = range(0, len(bins)-1) + bins_labels = [ + f'{int(bins[n])} a {int(bins[n+1])} origenes' for n in range_bins] + + origenes['cuts'] = pd.cut( + origenes['factor_expansion_linea'], bins=bins, labels=bins_labels) + + n = 0 + line_w = 10 + for i in bins_labels: + + origenes[origenes.cuts == i].explore( + m=m, + # color=colors_origenes[n], + color="#0173b299", + style_kwds={'fillOpacity': 0.1, 'weight': line_w}, + name=i, + tooltip=False, + ) + n += 1 + line_w += 5 + + if len(destinos) > 0: + try: + bins = [destinos['factor_expansion_linea'].min()-1] + \ + mapclassify.FisherJenks( + destinos['factor_expansion_linea'], k=5).bins.tolist() + except ValueError: + bins = [destinos['factor_expansion_linea'].min()-1] + \ + mapclassify.FisherJenks( + destinos['factor_expansion_linea'], k=5-3).bins.tolist() + + range_bins = range(0, len(bins)-1) + bins_labels = [ + f'{int(bins[n])} a {int(bins[n+1])} destinos' for n in range_bins] + + destinos['cuts'] = pd.cut( + destinos['factor_expansion_linea'], bins=bins, labels=bins_labels) + + n = 0 + line_w = 10 + for i in bins_labels: + + destinos[destinos.cuts == i].explore( + m=m, + # color=colors_destinos[n], + color="#de8f0599", + style_kwds={'fillOpacity': 0.1, 'weight': line_w}, + name=i, + tooltip=False, + ) + n += 1 + line_w += 5 + + # Agrego polígono + if (len(poly) > 0) & (show_poly): + geojson = poly.to_json() + # Add the GeoJSON to the map as a GeoJson Layer + folium.GeoJson( + geojson, + name=poly.id.values[0], + style_function=lambda feature: { + 'fillColor': 'grey', + 'color': 'black', + 'weight': 2, + 'fillOpacity': .5, + + }, + tooltip=folium.GeoJsonTooltip( + fields=['id'], labels=False, sticky=False) + ).add_to(m) + + # Agrego polígono + if len(zonif) > 0: + geojson = zonif.to_json() + # Add the GeoJSON to the map as a GeoJson Layer + folium.GeoJson( + geojson, + name='Zonificación', + style_function=lambda feature: { + 'fillColor': 'navy', + 'color': 'navy', + 'weight': .5, + 'fillOpacity': .2, + + }, + tooltip=folium.GeoJsonTooltip( + fields=['id'], labels=False, sticky=False) + ).add_to(m) + + folium.LayerControl(name='xx').add_to(m) + + return m + + +st.set_page_config(layout="wide") + +logo = get_logo() +st.image(logo) + + +poligonos = levanto_tabla_sql('poligonos') +etapas_all = levanto_tabla_sql('poly_etapas') +matrices_all = levanto_tabla_sql('poly_matrices') +zonificaciones = levanto_tabla_sql('zonificaciones') + +with st.expander('Polígonos', expanded=True): + + col1, col2 = st.columns([1, 4]) + + if (len(poligonos) > 0) & (len(etapas_all) > 0): + + general, modal, distancias = traigo_indicadores('poligonos') + + desc_poly = col1.selectbox( + 'Polígono', options=etapas_all.id_polygon.unique()) + desc_zona = col1.selectbox( + 'Zonificación', options=etapas_all.zona.unique()) + zonif = zonificaciones[zonificaciones.zona == desc_zona] + + desc_etapas = col1.checkbox( + label='Etapas', value=True) + + desc_viajes = col1.checkbox( + 'Viajes', value=False) + + desc_origenes = col1.checkbox( + ':blue[Origenes]', value=False) + + desc_destinos = col1.checkbox( + ':orange[Destinos]', value=False) + + transf_list_all = ['Todos', 'Con transferencia', 'Sin transferencia'] + transf_list = col1.selectbox( + 'Transferencias', options=transf_list_all) + + modos_list_all = ['Todos']+etapas_all[etapas_all.modo_agregado != + '99'].modo_agregado.unique().tolist() + modos_list = [text.capitalize() for text in modos_list_all] + modos_list = col1.selectbox( + 'Modos', options=modos_list_all) + + rango_hora_all = [ + 'Todos']+etapas_all[etapas_all.rango_hora != '99'].rango_hora.unique().tolist() + rango_hora = [text.capitalize() for text in rango_hora_all] + rango_hora = col1.selectbox( + 'Rango hora', options=rango_hora_all) + + distancia_all = [ + 'Todas']+etapas_all[etapas_all.distancia != '99'].distancia.unique().tolist() + distancia = col1.selectbox( + 'Distancia', options=distancia_all) + + etapas_ = etapas_all[(etapas_all.id_polygon == desc_poly) & + (etapas_all.zona == desc_zona)].copy() + matrices_ = matrices_all[(matrices_all.id_polygon == desc_poly) & + (matrices_all.zona == desc_zona)].copy() + + general_ = general.loc[general.id_polygon == desc_poly, [ + 'Tipo', 'Indicador', 'Valor']].set_index('Tipo') + modal_ = modal.loc[modal.id_polygon == desc_poly, [ + 'Tipo', 'Indicador', 'Valor']].set_index('Tipo') + distancias_ = distancias.loc[distancias.id_polygon == desc_poly, [ + 'Tipo', 'Indicador', 'Valor']].set_index('Tipo') + + if transf_list == 'Todos': + desc_transfers = True + else: + desc_transfers = False + if transf_list == 'Con transferencia': + etapas_ = etapas_[(etapas_.transferencia == 1)] + matrices_ = matrices_[(matrices_.transferencia == 1)] + elif transf_list == 'Sin transferencia': + etapas_ = etapas_[(etapas_.transferencia == 0)] + matrices_ = matrices_[(matrices_.transferencia == 0)] + else: + etapas_ = pd.DataFrame([]) + matrices_ = pd.DataFrame([]) + + if modos_list == 'Todos': + desc_modos = True + else: + desc_modos = False + etapas_ = etapas_[ + (etapas_.modo_agregado.str.lower() == modos_list.lower())] + matrices_ = matrices_[ + (matrices_.modo_agregado.str.lower() == modos_list.lower())] + + if rango_hora == 'Todos': + desc_horas = True + else: + desc_horas = False + etapas_ = etapas_[(etapas_.rango_hora == rango_hora)] + matrices_ = matrices_[(matrices_.rango_hora == rango_hora)] + + if distancia == 'Todas': + desc_distancia = True + else: + desc_distancia = False + etapas_ = etapas_[(etapas_.distancia == distancia)] + matrices_ = matrices_[(matrices_.distancia == distancia)] + + agg_cols_etapas = ['id_polygon', + 'zona', + 'inicio_norm', + 'transfer1_norm', + 'transfer2_norm', + 'fin_norm', + 'poly_inicio_norm', + 'poly_transfer1_norm', + 'poly_transfer2_norm', + 'poly_fin_norm', + 'transferencia', + 'modo_agregado', + 'rango_hora', + 'distancia'] + agg_cols_viajes = ['id_polygon', + 'zona', + 'inicio_norm', + 'fin_norm', + 'poly_inicio_norm', + 'poly_fin_norm', + 'transferencia', + 'modo_agregado', + 'rango_hora', + 'distancia'] + + etapas, viajes, matriz, origenes, destinos = create_data_folium(etapas_, + matrices_, + agg_transferencias=desc_transfers, + agg_modo=desc_modos, + agg_hora=desc_horas, + agg_distancia=desc_distancia, + agg_cols_etapas=agg_cols_etapas, + agg_cols_viajes=agg_cols_viajes) + + if not desc_etapas: + etapas = pd.DataFrame([]) + + if not desc_viajes: + viajes = pd.DataFrame([]) + + if not desc_origenes: + origenes = pd.DataFrame([]) + + if not desc_destinos: + destinos = pd.DataFrame([]) + + desc_zonif = col1.checkbox( + 'Mostrar zonificación', value=False) + if not desc_zonif: + zonif = '' + + show_poly = col1.checkbox( + 'Mostrar polígono', value=True) + + poly = poligonos[poligonos.id == desc_poly] + + if not desc_origenes: + origenes = '' + if not desc_destinos: + destinos = '' + + if (len(etapas) > 0) | (len(viajes) > 0) | (len(origenes) > 0) | (len(destinos) > 0): + + map = crear_mapa_poligonos(df_viajes=viajes, + df_etapas=etapas, + poly=poly, + zonif=zonif, + origenes=origenes, + destinos=destinos, + var_fex='factor_expansion_linea', + cmap_viajes='Blues', + cmap_etapas='Greens', + map_title=desc_poly, + savefile='', + k_jenks=5, + show_poly=show_poly) + + with col2: + # st_map = st_folium(map, width=1200, height=1000) # + folium_static(map, width=1200, height=600) + + else: + matriz = pd.DataFrame([]) + col2.markdown(""" + + """, unsafe_allow_html=True) + + col2.markdown( + '

¡¡ No hay datos para mostrar !!

', unsafe_allow_html=True) + + else: + matriz = pd.DataFrame([]) + + # Usar HTML para personalizar el estilo del texto + texto_html = """ + +
+ No hay datos para mostrar +
+ """ + col2.markdown(texto_html, unsafe_allow_html=True) + texto_html = """ + +
+ Verifique que existan alguna capa de polígonos o que los procesos se corrieron correctamente +
+ """ + col2.markdown(texto_html, unsafe_allow_html=True) + + +with st.expander('Indicadores'): + col1, col2, col3 = st.columns([2, 2, 2]) + if len(etapas_all) > 0: + col1.table(general_) + col2.table(modal_) + col3.table(distancias_) + +with st.expander('Matrices'): + + col1, col2 = st.columns([1, 4]) + + tipo_matriz = col1.selectbox( + 'Variable', options=['Viajes', 'Distancia promedio (kms)', 'Tiempo promedio (min)', 'Velocidad promedio (km/h)']) + + normalize = False + if tipo_matriz == 'Viajes': + var_matriz = 'factor_expansion_linea' + normalize = col1.checkbox('Normalizar', value=True) + if tipo_matriz == 'Distancia promedio (kms)': + var_matriz = 'distance_osm_drive' + if tipo_matriz == 'Tiempo promedio (min)': + var_matriz = 'travel_time_min' + if tipo_matriz == 'Velocidad promedio (km/h)': + var_matriz = 'travel_speed' + + + if len(matriz) > 0: + od_heatmap = pd.crosstab( + index=matriz['Origen'], + columns=matriz['Destino'], + values=matriz['factor_expansion_linea'], + aggfunc="sum", + normalize=normalize, + ) + if normalize: + od_heatmap = (od_heatmap * 100).round(1) + else: + od_heatmap = od_heatmap.round(0) + + od_heatmap = od_heatmap.reset_index() + od_heatmap['Origen'] = od_heatmap['Origen'].str[4:] + od_heatmap = od_heatmap.set_index('Origen') + od_heatmap.columns = [i[4:] for i in od_heatmap.columns] + + fig = px.imshow(od_heatmap, text_auto=True, + color_continuous_scale='Blues',) + + fig.update_coloraxes(showscale=False) + + if len(matriz) <= 20: + fig.update_layout(width=800, height=800) + elif (len(matriz) > 20) & (len(od_heatmap) <= 40): + fig.update_layout(width=1000, height=1000) + elif len(matriz) > 40: + fig.update_layout(width=1400, height=800) + + col2.plotly_chart(fig) diff --git a/urbantrips/datamodel/legs.py b/urbantrips/datamodel/legs.py index 3d56f7a..1104db3 100644 --- a/urbantrips/datamodel/legs.py +++ b/urbantrips/datamodel/legs.py @@ -1,14 +1,21 @@ import pandas as pd +import geopandas as gpd import itertools -import time import numpy as np -from urbantrips.geo.geo import referenciar_h3 +import h3 +from urbantrips.geo.geo import ( + referenciar_h3, + convert_h3_to_resolution, + classify_leg_into_station, + get_epsg_m, +) from urbantrips.utils.utils import ( duracion, iniciar_conexion_db, leer_configs_generales, agrego_indicador, - eliminar_tarjetas_trx_unica) + delete_data_from_table_run_days, +) @duracion @@ -19,7 +26,7 @@ def create_legs_from_transactions(trx_order_params): y crea la tabla etapas en la db """ - conn = iniciar_conexion_db(tipo='data') + conn = iniciar_conexion_db(tipo="data") dias_ultima_corrida = pd.read_sql_query( """ @@ -36,11 +43,10 @@ def create_legs_from_transactions(trx_order_params): JOIN dias_ultima_corrida d ON t.dia = d.dia """, - conn + conn, ) # parse dates using local timezone - legs['fecha'] = pd.to_datetime(legs.fecha, unit='s', - errors='coerce') + legs["fecha"] = pd.to_datetime(legs.fecha, unit="s", errors="coerce") # asignar id h3 configs = leer_configs_generales() @@ -57,7 +63,8 @@ def create_legs_from_transactions(trx_order_params): # asignar nuevo id tarjeta trx simultaneas legs = change_card_id_for_concurrent_trx( - legs, trx_order_params, dias_ultima_corrida) + legs, trx_order_params, dias_ultima_corrida + ) # elminar casos de nuevas tarjetas con trx unica # legs = eliminar_tarjetas_trx_unica(legs) @@ -79,20 +86,21 @@ def create_legs_from_transactions(trx_order_params): "id_linea", "id_ramal", "interno", + "genero", + "tarifa", "latitud", "longitud", "h3_o", - "factor_expansion" + "factor_expansion", ] ) - legs = legs.rename( - columns={"factor_expansion": "factor_expansion_original"}) + legs = legs.rename(columns={"factor_expansion": "factor_expansion_original"}) print(f"Subiendo {len(legs)} registros a la tabla etapas en la db") # borro si ya existen etapas de una corrida anterior - values = ', '.join([f"'{val}'" for val in dias_ultima_corrida['dia']]) + values = ", ".join([f"'{val}'" for val in dias_ultima_corrida["dia"]]) query = f"DELETE FROM etapas WHERE dia IN ({values})" conn.execute(query) conn.commit() @@ -100,11 +108,9 @@ def create_legs_from_transactions(trx_order_params): legs.to_sql("etapas", conn, if_exists="append", index=False) print("Fin subir etapas") - agrego_indicador(legs, - 'Cantidad de etapas pre imputacion de destinos', - 'etapas', - 0, - var_fex='') + agrego_indicador( + legs, "Cantidad de etapas pre imputacion de destinos", "etapas", 0, var_fex="" + ) conn.close() @@ -143,8 +149,7 @@ def crear_delta_trx(trx): return trx -def change_card_id_for_concurrent_trx(trx, - trx_order_params, dias_ultima_corrida): +def change_card_id_for_concurrent_trx(trx, trx_order_params, dias_ultima_corrida): """ Changes card id for those cards with concurrent transactions as defined by the parameters in trx_order_params. @@ -169,7 +174,7 @@ def change_card_id_for_concurrent_trx(trx, legs with new card ids """ - conn = iniciar_conexion_db(tipo='data') + conn = iniciar_conexion_db(tipo="data") print("Creando nuevos id tajetas para trx simultaneas") trx_c = trx.copy() @@ -180,7 +185,7 @@ def change_card_id_for_concurrent_trx(trx, if len(tarjetas_duplicadas) > 0: # borro si ya existen etapas de una corrida anterior - values = ', '.join([f"'{val}'" for val in dias_ultima_corrida['dia']]) + values = ", ".join([f"'{val}'" for val in dias_ultima_corrida["dia"]]) query = f"DELETE FROM tarjetas_duplicadas WHERE dia IN ({values})" conn.execute(query) conn.commit() @@ -215,7 +220,7 @@ def pago_doble_tarjeta(trx, trx_order_params): transactions with new card ids tarjetas_duplicadas: pandas DataFrame - dataframe with old and new card ids + dataframe with old and new card ids """ @@ -224,54 +229,63 @@ def pago_doble_tarjeta(trx, trx_order_params): cols = trx.columns if trx_order_params["criterio"] == "fecha_completa": - diff_segundos = ventana_duplicado*60 + diff_segundos = ventana_duplicado * 60 - trx['fecha_aux'] = trx['fecha'].astype(str).str[-8:] + trx["fecha_aux"] = trx["fecha"].astype(str).str[-8:] - trx['fecha_aux'] = trx['fecha_aux'].apply(lambda x: sum( - int(i) * 60 ** j for j, i in enumerate(x.split(":")[::-1]))) + trx["fecha_aux"] = trx["fecha_aux"].apply( + lambda x: sum(int(i) * 60**j for j, i in enumerate(x.split(":")[::-1])) + ) elif trx_order_params["criterio"] == "orden_trx": - trx.loc[:, ['fecha_aux']] = trx['hora'] + trx.loc[:, ["fecha_aux"]] = trx["hora"] diff_segundos = 1 else: raise ValueError("ordenamiento_transacciones mal especificado") - trx['interno2'] = trx['interno'] - trx['interno2'] = trx['interno2'].fillna(0) - - trx = trx.sort_values(['dia', 'id_tarjeta', 'id_linea', - 'interno2', 'fecha_aux', 'orden_trx']).reset_index(drop=True) + trx["interno2"] = trx["interno"] + trx["interno2"] = trx["interno2"].fillna(0) + + trx = trx.sort_values( + ["dia", "id_tarjeta", "id_linea", "interno2", "fecha_aux", "orden_trx"] + ).reset_index(drop=True) trx["datetime_proximo"] = trx["fecha_aux"].shift(-1) - trx['diff_datetime'] = (trx.fecha_aux - trx.datetime_proximo).abs() + trx["diff_datetime"] = (trx.fecha_aux - trx.datetime_proximo).abs() - trx['diff_datetime2'] = trx.groupby( - ['dia', 'id_tarjeta', 'id_linea', 'interno2']).diff_datetime.shift(+1) + trx["diff_datetime2"] = trx.groupby( + ["dia", "id_tarjeta", "id_linea", "interno2"] + ).diff_datetime.shift(+1) - trx['nro'] = np.nan - trx.loc[(trx.diff_datetime2.isna()) | ( - trx.diff_datetime2 > diff_segundos), 'nro'] = 0 + trx["nro"] = np.nan + trx.loc[ + (trx.diff_datetime2.isna()) | (trx.diff_datetime2 > diff_segundos), "nro" + ] = 0 while len(trx[trx.nro.isna()]) > 0: - trx['nro2'] = trx.groupby( - ['dia', 'id_tarjeta', 'id_linea', 'interno2']).nro.shift(+1)+1 - trx.loc[trx.nro.isna() & (trx.nro2.notna()), - 'nro'] = trx.loc[trx.nro.isna() & (trx.nro2.notna()), 'nro2'] + trx["nro2"] = ( + trx.groupby(["dia", "id_tarjeta", "id_linea", "interno2"]).nro.shift(+1) + 1 + ) + trx.loc[trx.nro.isna() & (trx.nro2.notna()), "nro"] = trx.loc[ + trx.nro.isna() & (trx.nro2.notna()), "nro2" + ] - trx['id_tarjeta_nuevo'] = trx['id_tarjeta'] + \ - '_' + trx['nro'].astype(int).astype(str) + trx["id_tarjeta_nuevo"] = ( + trx["id_tarjeta"] + "_" + trx["nro"].astype(int).astype(str) + ) - tarjetas_duplicadas = trx.loc[trx['nro'] > 0]\ - .reindex(columns=['dia', 'id_tarjeta', 'id_tarjeta_nuevo'])\ - .rename(columns={'id_tarjeta': 'id_tarjeta_original'})\ + tarjetas_duplicadas = ( + trx.loc[trx["nro"] > 0] + .reindex(columns=["dia", "id_tarjeta", "id_tarjeta_nuevo"]) + .rename(columns={"id_tarjeta": "id_tarjeta_original"}) .drop_duplicates() + ) - trx = trx\ - .drop('id_tarjeta', axis=1)\ - .rename(columns={'id_tarjeta_nuevo': 'id_tarjeta'}) + trx = trx.drop("id_tarjeta", axis=1).rename( + columns={"id_tarjeta_nuevo": "id_tarjeta"} + ) trx = trx.reindex(columns=cols) @@ -321,19 +335,18 @@ def cambiar_id_tarjeta_trx_simul_fecha(trx, ventana_duplicado): if duplicados.sum() > 0: # crear una tabla de registro de cambio de id tarjeta - tarjetas_duplicadas = trx.loc[nro_duplicado.index, [ - "dia", "id_tarjeta"]] + tarjetas_duplicadas = trx.loc[nro_duplicado.index, ["dia", "id_tarjeta"]] tarjetas_duplicadas = tarjetas_duplicadas.rename( columns={"id_tarjeta": "id_tarjeta_original"} ) tarjetas_duplicadas["id_tarjeta_nuevo"] = ( - tarjetas_duplicadas.id_tarjeta_original + '_' + nro_duplicado + tarjetas_duplicadas.id_tarjeta_original + "_" + nro_duplicado ) # crear un nuevo vector con los incrementales y concatenarlos nuevo_id_tarjeta = pd.Series(["0"] * len(trx)) nuevo_id_tarjeta.loc[nro_duplicado.index] = nro_duplicado - trx.id_tarjeta = trx.id_tarjeta.map(str) + '_' + nuevo_id_tarjeta + trx.id_tarjeta = trx.id_tarjeta.map(str) + "_" + nuevo_id_tarjeta else: tarjetas_duplicadas = pd.DataFrame() trx = trx.drop("duplicados_ventana", axis=1) @@ -384,7 +397,7 @@ def cambiar_id_tarjeta_trx_simul_orden_trx(trx): if duplicados.sum() > 0: nuevo_id_tarjeta = pd.Series(["0"] * len(trx)) nuevo_id_tarjeta.loc[nro_duplicado.index] = nro_duplicado - trx.id_tarjeta = trx.id_tarjeta + '_' + nuevo_id_tarjeta + trx.id_tarjeta = trx.id_tarjeta + "_" + nuevo_id_tarjeta print("Fin creacion de nuevos id tarjetas para duplicados con orden trx") return trx, tarjetas_duplicadas @@ -532,3 +545,538 @@ def crear_viaje_id_acumulada(df, ventana_viajes=120): viajes.append(viaje_id) return viajes + + +@duracion +def assign_gps_origin(): + """ + This function read legs data and if there is gps table + assigns a gps to the leg origin + """ + configs = leer_configs_generales() + nombre_archivo_gps = configs["nombre_archivo_gps"] + + if nombre_archivo_gps is not None: + print("Clasificando etapas en su gps de origen") + conn_data = iniciar_conexion_db(tipo="data") + + # get legs data + legs = pd.read_sql_query( + """ + SELECT e.dia,e.id_linea,e.id_ramal,e.interno,e.id, e.tiempo, e.genero, e.tarifa + FROM etapas e + JOIN dias_ultima_corrida d + ON e.dia = d.dia + order by e.dia,id_tarjeta,id_viaje,id_etapa, id_linea,id_ramal,interno + """, + conn_data, + ) + legs["fecha"] = pd.to_datetime(legs["dia"] + " " + legs["tiempo"]) + + # get gps data + q = """ + select g.dia,g.id_linea,g.id_ramal,g.interno,g.fecha,id + from gps g + JOIN dias_ultima_corrida d + ON g.dia = d.dia + order by g.dia, id_linea,id_ramal,interno,fecha; + """ + gps = pd.read_sql(q, conn_data) + + gps.loc[:, ["fecha"]] = gps.fecha.map(lambda ts: pd.Timestamp(ts, unit="s")) + cols = ["dia", "id_linea", "id_ramal", "interno", "fecha", "id"] + legs_to_join = legs.reindex(columns=cols).sort_values("fecha") + gps_to_join = gps.reindex(columns=cols).sort_values("fecha") + + # Join on closest date + legs_to_gps_o = pd.merge_asof( + legs_to_join, + gps_to_join, + on="fecha", + by=["dia", "id_linea", "id_ramal", "interno"], + direction="nearest", + tolerance=pd.Timedelta("7 minutes"), + suffixes=("_legs", "_gps"), + ) + + legs_to_gps_o = legs_to_gps_o.reindex( + columns=["dia", "id_legs", "id_gps"] + ).dropna() + + delete_data_from_table_run_days("legs_to_gps_origin") + print(f"Subiendo {len(legs_to_gps_o)} etapas con id gps a la DB") + legs_to_gps_o.to_sql( + "legs_to_gps_origin", conn_data, if_exists="append", index=False + ) + conn_data.close() + + # return legs_to_gps_o + + +@duracion +def assign_gps_destination(): + """ + This function read legs data and if there is gps table + assigns a gps to the leg origin + """ + + configs = leer_configs_generales() + nombre_archivo_gps = configs["nombre_archivo_gps"] + + if nombre_archivo_gps is not None: + print("Clasificando etapas en su gps de destino") + conn_data = iniciar_conexion_db(tipo="data") + conn_insumos = iniciar_conexion_db(tipo="insumos") + configs = leer_configs_generales() + legs_h3_res = configs["resolucion_h3"] + + # read stops zone of incluence + q = """ + select distinct parada,area_influencia + from matriz_validacion; + """ + matriz = pd.read_sql(q, conn_insumos) + matriz["ring"] = matriz.apply( + lambda row: h3.h3_distance(row.parada, row.area_influencia), axis=1 + ) + matriz = matriz[matriz.ring < 3] + + print("Leyendo datos de etapas con GPS") + legs = pd.read_sql_query( + """ + SELECT e.* + FROM etapas e + JOIN dias_ultima_corrida d + ON e.dia = d.dia + JOIN (SELECT DISTINCT id_linea FROM gps) idg + ON e.id_linea = idg.id_linea + WHERE od_validado==1 + order by e.dia,e.id_tarjeta,e.id_viaje,e.id_etapa, + e.id_linea,e.id_ramal,e.interno + ; + """, + conn_data, + ) + + # Add distances to legs + q = """ + select h3_o, h3_d, distance_osm_drive + from distancias + where distance_osm_drive is not null; + """ + distances = pd.read_sql(q, conn_insumos) + print("len distances", len(distances)) + print("len legs", len(legs)) + + legs = legs.merge(distances, how="inner", on=["h3_o", "h3_d"]) + del distances + print("len legs 2", len(legs)) + + legs["fecha"] = pd.to_datetime(legs["dia"] + " " + legs["tiempo"]) + + print("Leyendo datos de GPS") + q = """ + select g.* + from gps g + JOIN dias_ultima_corrida d + ON g.dia = d.dia + order by dia, id_linea,id_ramal,interno,fecha + ; + """ + gps = pd.read_sql(q, conn_data) + print("len gps", len(gps)) + # get h3 res for gps + gps_h3_res = h3.h3_get_resolution(gps["h3"].sample().item()) + + # geocode gps with same h3 res than legs + gps = referenciar_h3( + gps, res=legs_h3_res, nombre_h3="h3_legs_res", lat="latitud", lon="longitud" + ) + gps["fecha_gps"] = gps.fecha.map(lambda ts: pd.Timestamp(ts, unit="s")) + gps["hora"] = gps.fecha_gps.dt.hour + + # Geocode legs destination in the same h3 resolution than gps + legs["h3_d_gps_res"] = legs["h3_d"].apply( + lambda x: convert_h3_to_resolution(x, gps_h3_res) + ) + + # Lista para acumular resultados parciales + etapas_result_list = [] + + # Iteración por cada hora y cada dia + legs_days = legs.dia.unique() + legs_hours = legs.hora.unique() + + legs_days.sort() + legs_hours.sort() + + print("Imputando GPS de destino") + + for dia in legs_days: + print(dia) + for hora in legs_hours: + # Filtrar las etapas por la hora específica y eliminar valores nulos en 'h3_d' + etapas_tx = legs.loc[ + (legs["hora"] == hora) & (legs["dia"] == dia), + [ + "dia", + "id", + "id_linea", + "id_ramal", + "interno", + "h3_o", + "h3_d", + "h3_d_gps_res", + "distance_osm_drive", + "fecha", + ], + ].copy() + + # Agregar anillos a las etapas + etapas_tx = etapas_tx.merge( + matriz, how="left", left_on="h3_d", right_on="parada" + ) + + # Determinar horas consecutivas para el filtrado de datos GPS + hora_filtro = [hora + i for i in range(0, 4)] + gps_tx = gps.loc[gps["hora"].isin(hora_filtro), :].copy() + + # Renombrar y seleccionar columnas relevantes en los datos GPS + gps_tx = gps_tx.reindex( + columns=[ + "id", + "id_linea", + "id_ramal", + "interno", + "h3_legs_res", + "h3", + "fecha_gps", + ] + ).rename(columns={"h3_legs_res": "area_influencia"}) + + # Join gps to legs destination rings dataframe by the same resolution (legs resolution) + etapas_tx = etapas_tx.merge( + gps_tx, + how="inner", + on=["id_linea", "id_ramal", "interno", "area_influencia"], + suffixes=("_legs", "_gps"), + ) + + # Calcular la diferencia de tiempo entre cada punto de gps y cada etapa + etapas_tx["fecha_dif"] = ( + etapas_tx["fecha_gps"] - etapas_tx["fecha"] + ).dt.total_seconds() / 60 + + # Filtrar por diferencia de fecha positiva y ordenar por id, anillo y fecha_dif + etapas_tx = etapas_tx.loc[etapas_tx.fecha_dif > 0, :] + + if len(etapas_tx) > 0: + + # Calcular la distancia entre h3 del destino de la etapa y h3 del gps + + gps_dict = etapas_tx.reindex( + columns=["h3_d_gps_res", "h3"] + ).to_dict("records") + etapas_tx.loc[:, ["distancia_h3"]] = list( + map(distancia_h3_gps_leg, gps_dict) + ) + + # Calcular el tiempo mínimo de destino por id + etapas_tx["min_fecha_d"] = etapas_tx.groupby( + ["id_legs"] + ).fecha_gps.transform("min") + etapas_tx["min_fecha_d"] = round( + ( + etapas_tx.fecha_gps - etapas_tx["min_fecha_d"] + ).dt.total_seconds() + / 60, + 1, + ) + + # Filtrar por tiempo mínimo de destino menor a 20 minutos y ordenar por distancia_h3 + etapas_tx = etapas_tx.loc[etapas_tx.min_fecha_d < 20, :] + etapas_tx = etapas_tx.sort_values( + ["id_legs", "ring", "distancia_h3", "min_fecha_d"] + ) + + # Obtener la primera ocurrencia por id - elijo el gps que se encuentra más cerca del destino + etapas_tx = etapas_tx.groupby("id_legs", as_index=False).first() + + # Agregar resultado a la lista + etapas_result_list.append(etapas_tx) + + + # Concatenar todos los resultados acumulados + etapas_result = pd.concat(etapas_result_list, ignore_index=True) + + legs_to_gps_d = etapas_result.reindex(columns=["dia", "id_legs", "id_gps"]) + + delete_data_from_table_run_days("legs_to_gps_destination") + print("Subiendo GPS de destino de las etapas a la db ") + legs_to_gps_d.to_sql( + "legs_to_gps_destination", conn_data, if_exists="append", index=False + ) + + print("Computando tiempos de viaje en GPS") + # Unir los resultados con el DataFrame original de etapas + travel_times = legs.reindex( + columns=["dia", "id", "fecha", "distance_osm_drive"] + ).merge( + etapas_result.reindex(columns=["id_legs", "fecha_gps"]), + how="left", + left_on=["id"], + right_on=["id_legs"], + ) + + # Calcular el tiempo de viaje en minutos y velocidad comercial + travel_times["travel_time_min"] = round( + (travel_times["fecha_gps"] - travel_times["fecha"]).dt.total_seconds() / 60, + 1, + ) + + travel_times = travel_times.loc[travel_times.travel_time_min > 0, :] + travel_times.loc[:, "travel_speed"] = ( + travel_times["distance_osm_drive"] / (travel_times["travel_time_min"] / 60) + ).round(1) + + travel_times.loc[ + (travel_times.travel_speed == np.inf) | (travel_times.travel_speed >= 50), + "travel_speed", + ] = np.nan + + tot_gps = len(travel_times) + tot_gps_asig = travel_times.travel_time_min.notna().sum() + print("% imputado", round(tot_gps_asig / tot_gps * 100, 1)) + travel_times = travel_times.reindex( + columns=["dia", "id", "travel_time_min", "travel_speed"] + ) + + print("Subiendo tiempos de viaje de GPS a la db ") + + delete_data_from_table_run_days("travel_times_gps") + travel_times.to_sql( + "travel_times_gps", conn_data, if_exists="append", index=False + ) + conn_data.close() + + # return etapas_result + + +def distancia_h3_gps_leg(row): + return h3.h3_distance(row["h3_d_gps_res"], row["h3"]) + + +@duracion +def assign_stations_od(): + """ + This function reads legs, classifies OD into stations, + reads travel times in gps and computes a single travel time + for each leg + """ + + configs = leer_configs_generales() + tiempos_viaje_estaciones = configs["tiempos_viaje_estaciones"] + + if tiempos_viaje_estaciones is not None: + + conn_data = iniciar_conexion_db(tipo="data") + conn_insumos = iniciar_conexion_db(tipo="insumos") + + # read legs without travel time in gps and distances + q = """ + SELECT e.dia,e.id,e.id_linea,e.id_ramal,e.h3_o,e.h3_d + FROM etapas e + LEFT JOIN travel_times_gps tt + ON e.dia = tt.dia + AND e.id = tt.id + WHERE tt.id IS NULL + AND e.od_validado = 1 + """ + legs = pd.read_sql(q, conn_data) + + q = """ + select h3_o, h3_d, distance_osm_drive + from distancias + where distance_osm_drive is not null; + """ + distances = pd.read_sql(q, conn_insumos) + + legs = legs.merge(distances, how="inner", on=["h3_o", "h3_d"]) + del distances + + # read stations data + epsg_m = get_epsg_m() + + travel_times_stations = pd.read_sql( + "select * from travel_times_stations", conn_insumos + ) + + stations_o = ( + travel_times_stations.reindex( + columns=["id_o", "id_linea_o", "id_ramal_o", "lat_o", "lon_o"] + ) + .drop_duplicates() + .rename( + columns={ + "id_o": "id", + "lat_o": "lat", + "lon_o": "lon", + "id_linea_o": "id_linea", + "id_ramal_o": "id_ramal", + } + ) + ) + + stations_d = ( + travel_times_stations.reindex( + columns=["id_d", "id_linea_d", "id_ramal_d", "lat_d", "lon_d"] + ) + .drop_duplicates() + .rename( + columns={ + "id_d": "id", + "lat_d": "lat", + "lon_d": "lon", + "id_linea_d": "id_linea", + "id_ramal_d": "id_ramal", + } + ) + ) + + stations = ( + pd.concat([stations_o, stations_d]).drop_duplicates().reset_index(drop=True) + ) + + geom = gpd.GeoSeries.from_xy(x=stations.lon, y=stations.lat, crs=4326) + stations = gpd.GeoDataFrame(stations, geometry=geom, crs=4326).reindex( + columns=["id", "id_linea", "geometry"] + ) + + stations = stations.to_crs(epsg=epsg_m) + + # classify legs' origin and destination with station id + legs_with_origin_station = ( + legs.groupby(["id_linea"]) + .apply( + classify_leg_into_station, + stations=stations, + leg_h3_field="h3_o", + join_branch_id=False, + ) + .reset_index(drop=True) + .rename(columns={"id_station": "id_station_o"}) + ) + + print( + "Etapas clasificadas en estaciones de origen: ", + round(len(legs_with_origin_station) / len(legs) * 100, 1), + ) + + legs_with_destination_station = ( + legs.groupby(["id_linea"]) + .apply( + classify_leg_into_station, + stations=stations, + leg_h3_field="h3_d", + join_branch_id=False, + ) + .reset_index(drop=True) + .rename(columns={"id_station": "id_station_d"}) + ) + + print( + "Etapas clasificadas en estaciones de destino: ", + round(len(legs_with_destination_station) / len(legs) * 100, 1), + ) + + # upload od station into db + stations_o = legs_with_origin_station.rename( + columns={"id_station_o": "id_station"} + ).reindex(columns=["dia", "id_legs", "id_station"]) + + stations_d = legs_with_destination_station.rename( + columns={"id_station_d": "id_station"} + ).reindex(columns=["dia", "id_legs", "id_station"]) + + delete_data_from_table_run_days("legs_to_station_origin") + delete_data_from_table_run_days("legs_to_station_destination") + + stations_o.to_sql( + "legs_to_station_origin", conn_data, index=False, if_exists="append" + ) + stations_d.to_sql( + "legs_to_station_destination", conn_data, index=False, if_exists="append" + ) + del stations_o + del stations_d + + # add stations to legs data + travel_times = ( + legs.reindex(columns=["dia", "id", "id_linea", "distance_osm_drive"]) + .merge( + legs_with_origin_station, + left_on=["id"], + right_on=["id_legs"], + how="left", + ) + .merge( + legs_with_destination_station, + left_on=["id"], + right_on=["id_legs"], + how="left", + ) + .drop(["id_legs_x", "id_legs_y", "dia_x", "dia_y"], axis=1) + .dropna(subset=["id_station_o", "id_station_d"]) + ) + + print( + "Etapas clasificadas en la misma estación OD", + round( + len( + travel_times[travel_times.id_station_o == travel_times.id_station_d] + ) + / len(travel_times) + * 100, + 1, + ), + "%", + ) + + travel_times = travel_times.loc[ + travel_times.id_station_o != travel_times.id_station_d, : + ] + + # compute travel time + travel_times = travel_times.merge( + travel_times_stations.reindex(columns=["id_o", "id_d", "travel_time_min"]), + left_on=["id_station_o", "id_station_d"], + right_on=["id_o", "id_d"], + how="left", + ) + + print( + "Sin tiempos de viaje", + travel_times.travel_time_min.isna().sum() / len(travel_times), + ) + travel_times = travel_times.dropna(subset=["travel_time_min"]) + travel_times.loc[:, "travel_speed"] = ( + travel_times.loc[:, "distance_osm_drive"] + / (travel_times.loc[:, "travel_time_min"] / 60) + ).round(1) + + travel_times.loc[ + (travel_times.travel_speed == np.inf) | (travel_times.travel_speed >= 50), + "travel_speed", + ] = np.nan + + # upload to db + travel_times = travel_times.reindex( + columns=["dia", "id", "travel_time_min", "travel_speed"] + ) + delete_data_from_table_run_days("travel_times_stations") + travel_times = travel_times.reindex( + columns=["dia", "id", "travel_time_min", "travel_speed"] + ) + travel_times.to_sql( + "travel_times_stations", conn_data, if_exists="append", index=False + ) diff --git a/urbantrips/datamodel/misc.py b/urbantrips/datamodel/misc.py index a0ef9e1..4232d96 100644 --- a/urbantrips/datamodel/misc.py +++ b/urbantrips/datamodel/misc.py @@ -4,6 +4,7 @@ from urbantrips.utils.utils import (iniciar_conexion_db, leer_alias, agrego_indicador, duracion) + @duracion def persist_datamodel_tables(): """ @@ -12,7 +13,6 @@ def persist_datamodel_tables(): y las guarda en csv """ - alias = leer_alias() conn_insumos = iniciar_conexion_db(tipo='insumos') conn_data = iniciar_conexion_db(tipo='data') @@ -50,6 +50,7 @@ def persist_datamodel_tables(): viajes = pd.read_sql_query(""" select * from viajes + where od_validado==1 """, conn_data) viajes = viajes.merge(zonas_o, how='left').merge(zonas_d, how='left') viajes = viajes.merge(distancias, how='left') @@ -58,6 +59,7 @@ def persist_datamodel_tables(): usuarios = pd.read_sql_query(""" SELECT * from usuarios + where od_validado==1 """, conn_data) # Grabo resultados en tablas .csv @@ -78,15 +80,14 @@ def persist_datamodel_tables(): index=False, ) - agrego_indicador(etapas[etapas.od_validado == 1], + agrego_indicador(etapas, 'Cantidad total de etapas', 'etapas_expandidas', 0) - for i in etapas[etapas.od_validado == 1].modo.unique(): + for i in etapas.modo.unique(): agrego_indicador( - etapas.loc[(etapas.od_validado == 1) & - (etapas.modo == i)], + etapas.loc[etapas.modo == i], f'Etapas {i}', 'etapas_expandidas', 1) agrego_indicador(viajes, @@ -95,47 +96,45 @@ def persist_datamodel_tables(): 0, var_fex='') - agrego_indicador(viajes[viajes.od_validado == 1], + agrego_indicador(viajes, 'Cantidad total de viajes expandidos', 'viajes expandidos', 0) - agrego_indicador(viajes[(viajes.od_validado == 1) & - (viajes.distance_osm_drive <= 5)], + agrego_indicador(viajes[(viajes.distance_osm_drive <= 5)], 'Cantidad de viajes cortos (<5kms)', 'viajes expandidos', 1) - agrego_indicador(viajes[(viajes.od_validado == 1) & - (viajes.cant_etapas > 1)], + agrego_indicador(viajes[(viajes.cant_etapas > 1)], 'Cantidad de viajes con transferencia', 'viajes expandidos', 1) - agrego_indicador(viajes[viajes.od_validado == 1], + agrego_indicador(viajes, 'Cantidad total de viajes expandidos', 'modos viajes', 0) - for i in viajes[viajes.od_validado == 1].modo.unique(): + for i in viajes.modo.unique(): agrego_indicador( viajes.loc[(viajes.od_validado == 1) & (viajes.modo == i)], f'Viajes {i}', 'modos viajes', 1) - agrego_indicador(viajes[viajes.od_validado == 1], + agrego_indicador(viajes, 'Distancia de los viajes (promedio en kms)', 'avg', 0, var='distance_osm_drive', aggfunc='mean') - agrego_indicador(viajes[viajes.od_validado == 1], + agrego_indicador(viajes, 'Distancia de los viajes (mediana en kms)', 'avg', 0, var='distance_osm_drive', aggfunc='median') - for i in viajes[viajes.od_validado == 1].modo.unique(): + for i in viajes.modo.unique(): agrego_indicador( viajes.loc[(viajes.od_validado == 1) & (viajes.modo == i)], @@ -144,16 +143,15 @@ def persist_datamodel_tables(): var='distance_osm_drive', aggfunc='mean') - for i in viajes[viajes.od_validado == 1].modo.unique(): + for i in viajes.modo.unique(): agrego_indicador( - viajes.loc[(viajes.od_validado == 1) & - (viajes.modo == i)], + viajes.loc[(viajes.modo == i)], f'Distancia de los viajes (mediana en kms) - {i}', 'avg', 0, var='distance_osm_drive', aggfunc='median') - agrego_indicador(viajes[viajes.od_validado == 1], + agrego_indicador(viajes, 'Etapas promedio de los viajes', 'avg', 0, @@ -175,7 +173,7 @@ def persist_datamodel_tables(): 0, var_fex='') - agrego_indicador(etapas[etapas.od_validado == 1].groupby( + agrego_indicador(etapas.groupby( ['dia', 'id_tarjeta'], as_index=False).factor_expansion_linea.min(), 'Cantidad total de usuarios', @@ -194,29 +192,29 @@ def persist_datamodel_tables(): print( "Validados :", - "{:,}".format(len(etapas[etapas.od_validado == 1])).replace(",", "."), + "{:,}".format(len(etapas)).replace(",", "."), "(etapas)", - "{:,}".format(len(viajes[viajes.od_validado == 1])).replace(",", "."), + "{:,}".format(len(viajes)).replace(",", "."), "(viajes)", "{:,}".format( - len(usuarios[usuarios.od_validado == 1])).replace(",", "."), + len(usuarios)).replace(",", "."), "(usuarios)", ) print( " % :", "{:,}".format( - round(len(etapas[etapas.od_validado == 1]) / len(etapas) * 100) + round(len(etapas) / len(etapas) * 100) ).replace(",", ".") + "%", "(etapas)", "{:,}".format( - round(len(viajes[viajes.od_validado == 1]) / len(viajes) * 100) + round(len(viajes) / len(viajes) * 100) ).replace(",", ".") + "%", "(viajes)", "{:,}".format( - round(len(usuarios[usuarios.od_validado == 1]) / + round(len(usuarios) / len(usuarios) * 100) ).replace(",", ".") + "%", @@ -240,7 +238,7 @@ def tabla_indicadores(): """, conn_data, ) - except DatabaseError as e: + except DatabaseError: indicadores = pd.DataFrame([]) db_path = os.path.join("resultados", "tablas", diff --git a/urbantrips/datamodel/services.py b/urbantrips/datamodel/services.py index 19997fd..5da6f0c 100644 --- a/urbantrips/datamodel/services.py +++ b/urbantrips/datamodel/services.py @@ -1,31 +1,81 @@ -import numpy as np import pandas as pd import geopandas as gpd -from math import ceil -import h3 from urbantrips.utils import utils -from urbantrips.kpi import kpi -from urbantrips.geo import geo - -def process_services(): +def process_services(line_ids=None): """ Download unprocessed gps data and classify them into services + for all days and all lines or a set of specified set of line ids + + Parameters + ---------- + line_ids : int, list + line id or ids to process services for + + Returns + ------- + None. Updates services_gps_points, services, + and services_stats tables in db + + """ + configs = utils.leer_configs_generales() + nombre_archivo_gps = configs["nombre_archivo_gps"] + + if nombre_archivo_gps is not None: + print("Procesando servicios en base a tabla gps") + # check line id type and turn it into list if is a single line id + if line_ids is not None: + if isinstance(line_ids, int): + line_ids = [line_ids] + + line_ids_str = ','.join(map(str, line_ids)) + else: + line_ids_str = None + + print("Eliminando datos anteriores") + delete_old_services_data(line_ids_str) + print("Fin de borrado de datos anteriores") + if line_ids is not None: + print(f"para id lineas {line_ids_str}") + else: + print("para todas las lineas") + + print("Descargando paradas y puntos gps") + gps_points, stops = get_stops_and_gps_data(line_ids_str) + + if gps_points is None: + print("Todos los puntos gps ya fueron procesados en servicios ") + else: + print("Clasificando puntos gps en servicios") + gps_points.groupby('id_linea').apply( + process_line_services, stops=stops) + + +def delete_old_services_data(line_ids_str): + """ + Deletes data from services tables for all lines or + a specified set of line ids """ - print("Descargando paradas y puntos gps") - gps_points, stops = get_stops_and_gps_data() + conn_data = utils.iniciar_conexion_db(tipo='data') + tables = ['services_gps_points', 'services', 'services_stats'] + for table in tables: + q = f""" + DELETE FROM {table} + """ + if line_ids_str is not None: + q = q + f"where id_linea in ({line_ids_str});" + else: + q = q + ";" + conn_data.execute(q) + conn_data.commit() - if gps_points is None: - print("Todos los puntos gps ya fueron procesados en servicios ") - else: - print("Clasificando puntos gps en servicios") - gps_points.groupby('id_linea').apply( - process_line_services, stops=stops) + conn_data.close() -def get_stops_and_gps_data(): +def get_stops_and_gps_data(line_ids_str): """ - Download unprocessed gps data + Download unprocessed gps data and stops for all lines + or ofr a specified set of line ids and all days """ configs = utils.leer_configs_generales() @@ -58,6 +108,11 @@ def get_stops_and_gps_data(): and g.dia = ss.dia where ss.id_linea is null """ + if line_ids_str is not None: + gps_query = gps_query + f"and g.id_linea in ({line_ids_str});" + else: + gps_query = gps_query + ";" + gps_points = pd.read_sql(gps_query, conn_data) gps_points = gpd.GeoDataFrame( @@ -66,12 +121,17 @@ def get_stops_and_gps_data(): x=gps_points.longitud, y=gps_points.latitud, crs='EPSG:4326'), crs='EPSG:4326' ) + gps_points = gps_points.to_crs(epsg=configs['epsg_m']) - gps_lines = ','.join(map(str, gps_points.id_linea.unique())) + gps_lines = gps_points.id_linea.drop_duplicates() + gps_lines_str = ','.join(gps_lines.map(str)) - if len(gps_lines) == 0: + if len(gps_lines_str) == 0: return None, None + elif configs['utilizar_servicios_gps']: + return gps_points, None + else: # check if data is present in the db if not utils.check_table_in_db(table_name='stops', tipo_db='insumos'): @@ -89,10 +149,22 @@ def get_stops_and_gps_data(): stops_query = f""" select * from stops - where id_linea in ({gps_lines}) + where id_linea in ({gps_lines_str}) """ stops = pd.read_sql(stops_query, conn_insumos) + # check all gps points have stops for that line + line_no_stops_mask = ~gps_lines.isin(stops.id_linea.drop_duplicates()) + if line_no_stops_mask.any(): + line_no_stops = gps_lines[line_no_stops_mask] + line_no_stops_str = ','.join(line_no_stops.map(str)) + + print(f"""Hay lineas con GPS que no tienen paradas: + {line_no_stops_str} + No se procesaran""") + + gps_points = gps_points.loc[~gps_points.id_linea.isin( + line_no_stops)] # use only nodes as stops stops = stops.drop_duplicates( @@ -104,7 +176,7 @@ def get_stops_and_gps_data(): x=stops.node_x, y=stops.node_y, crs='EPSG:4326'), crs='EPSG:4326' ) - gps_points = gps_points.to_crs(epsg=configs['epsg_m']) + stops = stops.to_crs(epsg=configs['epsg_m']) return gps_points, stops @@ -112,8 +184,8 @@ def get_stops_and_gps_data(): def process_line_services(gps_points, stops): """ - Takes gps points and stops for a given line - and classifies each point into a services + Takes gps points and stops for a given line, + classifies each point into a services and produces services tables and daily stats for that line @@ -134,12 +206,15 @@ def process_line_services(gps_points, stops): conn_data = utils.iniciar_conexion_db(tipo='data') print(f"Procesando servicios en base a gps para id_linea {line_id}") - # select only stops for that line - line_stops_gdf = stops.loc[stops.id_linea == line_id, :] + # if there are stops select only stops for that line + if stops is not None: + line_stops_gdf = stops.loc[stops.id_linea == line_id, :] + else: + line_stops_gdf = None print("Asignando servicios") gps_points_with_new_service_id = gps_points\ - .groupby(['dia', 'interno'], as_index=False)\ + .groupby(['dia', 'id_ramal', 'interno'], as_index=False)\ .apply(classify_line_gps_points_into_services, line_stops_gdf=line_stops_gdf)\ .droplevel(0) @@ -147,7 +222,7 @@ def process_line_services(gps_points, stops): print("Subiendo servicios a la db") # save result to services table services_gps_points = gps_points_with_new_service_id\ - .reindex(columns=['id', 'original_service_id', 'new_service_id', + .reindex(columns=['id', 'id_linea', 'id_ramal', 'interno', 'dia', 'original_service_id', 'new_service_id', 'service_id', 'id_ramal_gps_point', 'node_id']) services_gps_points.to_sql("services_gps_points", conn_data, if_exists='append', index=False) @@ -223,28 +298,30 @@ def infer_service_id_stops(line_gps_points, line_stops_gdf, debug=False): geopandas.GeoDataFrame GeoDataFrame points classified into services """ - + # get amount of original serviices n_original_services_ids = len( line_gps_points['original_service_id'].unique()) + # get unique branches in the gps points branches = line_stops_gdf.id_ramal.unique() - n_original_services_ids = len( - line_gps_points['original_service_id'].unique()) - + # get how many branches pass through that node majority_by_node_id = line_stops_gdf\ .drop_duplicates(['id_ramal', 'node_id'])\ .groupby('node_id', as_index=False)\ .agg(branch_mayority=('id_ramal', 'count')) + # go through all branches gps_all_branches = pd.DataFrame() debug_df = pd.DataFrame() for branch in branches: + # select stops for that branch stops_to_join = line_stops_gdf\ .loc[line_stops_gdf.id_ramal == branch, ['branch_stop_order', 'geometry']] + # get nearest stop for that branch within 1.5 km # Not use max_distance. Far away stops will appear as # still on the same stop and wont be active branches gps_branch = gpd.sjoin_nearest( @@ -252,7 +329,7 @@ def infer_service_id_stops(line_gps_points, line_stops_gdf, debug=False): stops_to_join, how='left', max_distance=1500, - distance_col=f'distance_to_stop') + distance_col='distance_to_stop') gps_branch['id_ramal'] = branch # Evaluate change on stops order for each branch @@ -260,8 +337,8 @@ def infer_service_id_stops(line_gps_points, line_stops_gdf, debug=False): .groupby(['interno', 'original_service_id'])\ .apply(find_change_in_direction) + # when vehicle is always too far away from this branch if n_original_services_ids > 1: - # when vehicle is always too far away from this branch if isinstance(temp_change, type(pd.Series())): temp_change = temp_change.droplevel([0, 1]) @@ -273,39 +350,36 @@ def infer_service_id_stops(line_gps_points, line_stops_gdf, debug=False): temp_change = pd.Series( temp_change.iloc[0].values, index=temp_change.columns) - gps_branch[f'temp_change'] = temp_change - + # eval if temporary change is conssitent 5 points ahead + gps_branch['temp_change'] = temp_change window = 5 gps_branch['consistent_post'] = ( - gps_branch[f'temp_change'] + gps_branch['temp_change'] .shift(-window).fillna(False) .rolling(window=window, center=False, min_periods=3).sum() == 0 ) - # gps_branch[f'consistent_pre'] = ( - # gps_branch[f'temp_change'] - # .fillna(False).shift(1) == False) # noqa # Accept there is a change in direction when consistent - gps_branch[f'change'] = ( - gps_branch[f'temp_change'] & - gps_branch[f'consistent_post'] - # & gps_branch[f'consistent_pre'] + gps_branch['change'] = ( + gps_branch['temp_change'] & + gps_branch['consistent_post'] ) + + # add debugging attributes if debug: debug_branch = gps_branch\ .reindex(columns=['id', 'branch_stop_order', 'id_ramal', - 'temp_change', 'consistent_post', - 'consistent_pre', 'change']) + 'temp_change', 'consistent_post', 'distance_to_stop', + 'change']) debug_df = pd.concat([debug_df, debug_branch]) gps_branch = gps_branch.drop( ['index_right', 'temp_change', 'consistent_post', - # 'consistent_pre' ], axis=1) gps_all_branches = pd.concat([gps_all_branches, gps_branch]) - # para cada punto gps necesito el node id del branch mas cercano + # for each gps point get the node id form the nearest branch branches_distances_table = gps_all_branches\ .reindex(columns=['id', 'id_ramal', 'distance_to_stop', 'branch_stop_order'])\ @@ -321,6 +395,7 @@ def infer_service_id_stops(line_gps_points, line_stops_gdf, debug=False): how='left')\ .reindex(columns=['id', 'id_ramal', 'node_id']) + # count how many branches see a change in that node total_changes_by_gps = gps_all_branches\ .groupby(['id'], as_index=False)\ .agg(total_changes=('change', 'sum')) @@ -328,11 +403,19 @@ def infer_service_id_stops(line_gps_points, line_stops_gdf, debug=False): gps_points_changes = gps_node_ids\ .merge(total_changes_by_gps, how='left', on='id')\ .merge(majority_by_node_id, how='left', on='node_id') + + # set change when passes the mayority gps_points_changes['change'] = ( gps_points_changes.total_changes >= gps_points_changes.branch_mayority ) + + # set schema + cols = ['id', 'id_ramal', 'node_id', 'change'] + if debug: + cols = cols + ['branch_mayority', 'total_changes'] + gps_points_changes = gps_points_changes.reindex( - columns=['id', 'id_ramal', 'node_id', 'change']) + columns=cols) gps_points_changes = gps_points_changes\ .rename(columns={'id_ramal': 'id_ramal_gps_point'}) @@ -345,12 +428,12 @@ def infer_service_id_stops(line_gps_points, line_stops_gdf, debug=False): # Within each original service id, classify services within new_services_ids = line_gps_points\ .groupby('original_service_id')\ - .apply(lambda df: df['change'].cumsum().fillna(method='ffill'))\ + .apply(lambda df: df['change'].cumsum().ffill())\ .droplevel(0) else: new_services_ids = line_gps_points\ .groupby('original_service_id')\ - .apply(lambda df: df['change'].cumsum().fillna(method='ffill')) + .apply(lambda df: df['change'].cumsum().ffill()) new_services_ids = pd.Series( new_services_ids.iloc[0].values, index=new_services_ids.columns) @@ -361,7 +444,7 @@ def infer_service_id_stops(line_gps_points, line_stops_gdf, debug=False): debug_df = debug_df\ .pivot(index="id", columns='id_ramal', values=["branch_stop_order", "temp_change", - 'consistent_post', 'consistent_pre', 'change'])\ + 'consistent_post', 'change', 'distance_to_stop'])\ .reset_index() cols = [c[0]+'_'+str(c[1]) if c[0] != 'id' @@ -373,7 +456,7 @@ def infer_service_id_stops(line_gps_points, line_stops_gdf, debug=False): return line_gps_points -def classify_line_gps_points_into_services(line_gps_points, line_stops_gdf, +def classify_line_gps_points_into_services(line_gps_points, line_stops_gdf, debug=False, *args, **kwargs): """ Takes gps points and stops for a given line and classifies each point into @@ -384,7 +467,7 @@ def classify_line_gps_points_into_services(line_gps_points, line_stops_gdf, Parameters ---------- line_gps_points : geopandas.GeoDataFrame - GeoDataFrame with gps points for a given line + GeoDataFrame with gps points for a given line, branch and vehicle line_stops_gdf : geopandas.GeoDataFrame GeoDataFrame with stops for a given line @@ -395,34 +478,30 @@ def classify_line_gps_points_into_services(line_gps_points, line_stops_gdf, """ # create original service id original_service_id = line_gps_points\ - .reindex(columns=['dia', 'interno', 'service_type'])\ - .groupby(['dia', 'interno'])\ + .reindex(columns=['dia', 'id_ramal', 'interno', 'service_type'])\ + .groupby(['dia', 'id_ramal', 'interno'])\ .apply(create_original_service_id) original_service_id = original_service_id.service_type - original_service_id = original_service_id.droplevel([0, 1]) + original_service_id = original_service_id.droplevel([0, 1, 2]) line_gps_points.loc[:, ['original_service_id']] = original_service_id # check configs if trust in service type gps configs = utils.leer_configs_generales() - if 'utilizar_servicios_gps' in configs: - trust_service_type_gps = configs['utilizar_servicios_gps'] - - else: - trust_service_type_gps = False + trust_service_type_gps = configs['utilizar_servicios_gps'] if trust_service_type_gps: # classify services based on gps dataset attribute - line_gps_points['new_service_id'] = ( + line_gps_points.loc[:, ['new_service_id']] = ( line_gps_points['original_service_id'].copy() ) else: # classify services based on stops line_gps_points = infer_service_id_stops( - line_gps_points, line_stops_gdf) + line_gps_points, line_stops_gdf, debug=debug) # Classify idling points when there is no movement - line_gps_points['idling'] = line_gps_points.distance_km < 0.1 + line_gps_points.loc[:, ['idling']] = line_gps_points.distance_km < 0.1 # create a unique id from both old and new new_ids = line_gps_points\ diff --git a/urbantrips/datamodel/transactions.py b/urbantrips/datamodel/transactions.py index 78f1c4c..8bdd309 100644 --- a/urbantrips/datamodel/transactions.py +++ b/urbantrips/datamodel/transactions.py @@ -1,32 +1,38 @@ import h3 import os import pandas as pd +import geopandas as gpd import warnings import time + +from shapely.geometry import Point from urbantrips.geo import geo -from urbantrips.utils.utils import (leer_configs_generales, - duracion, - iniciar_conexion_db, - agrego_indicador, - eliminar_tarjetas_trx_unica, - crear_tablas_geolocalizacion) +from urbantrips.utils.utils import ( + leer_configs_generales, + duracion, + iniciar_conexion_db, + levanto_tabla_sql, + agrego_indicador, + crear_tablas_geolocalizacion, +) @duracion -def create_transactions(geolocalizar_trx_config, - nombre_archivo_trx, - nombres_variables_trx, - formato_fecha, - col_hora, - tipo_trx_invalidas, - nombre_archivo_gps, - nombres_variables_gps - ): +def create_transactions( + geolocalizar_trx_config, + nombre_archivo_trx, + nombres_variables_trx, + formato_fecha, + col_hora, + tipo_trx_invalidas, + nombre_archivo_gps, + nombres_variables_gps, +): """ Esta función toma las tablas originales y las convierte en el esquema que necesita el proceso """ - conn = iniciar_conexion_db(tipo='data') + conn = iniciar_conexion_db(tipo="data") print("Abriendo archivos de configuracion") configs = leer_configs_generales() @@ -35,7 +41,7 @@ def create_transactions(geolocalizar_trx_config, modos_homologados = configs["modos"] zipped = zip(modos_homologados.values(), modos_homologados.keys()) modos_homologados = {k: v for k, v in zipped} - print('Utilizando los siguientes modos homologados') + print("Utilizando los siguientes modos homologados") print(modos_homologados) except KeyError: pass @@ -55,7 +61,7 @@ def create_transactions(geolocalizar_trx_config, else: ruta = os.path.join("data", "data_ciudad", nombre_archivo_trx) - print('Levanta archivo de transacciones', ruta) + print("Levanta archivo de transacciones", ruta) trx = pd.read_csv(ruta) print("Filtrando transacciones invalidas:", tipo_trx_invalidas) @@ -85,15 +91,15 @@ def create_transactions(geolocalizar_trx_config, trx, tmp_trx_inicial = agrego_factor_expansion(trx, conn) # Guardo los días que se están analizando en la corrida actual - dias_ultima_corrida = pd.DataFrame( - trx.dia.unique(), columns=['dia']) + dias_ultima_corrida = pd.DataFrame(trx.dia.unique(), columns=["dia"]) - conn = iniciar_conexion_db(tipo='data') + conn = iniciar_conexion_db(tipo="data") dias_ultima_corrida.to_sql( - "dias_ultima_corrida", conn, if_exists="replace", index=False) + "dias_ultima_corrida", conn, if_exists="replace", index=False + ) # borro si ya existen transacciones de una corrida anterior - values = ', '.join([f"'{val}'" for val in dias_ultima_corrida['dia']]) + values = ", ".join([f"'{val}'" for val in dias_ultima_corrida["dia"]]) query = f"DELETE FROM transacciones WHERE dia IN ({values})" conn.execute(query) conn.commit() @@ -102,17 +108,15 @@ def create_transactions(geolocalizar_trx_config, trx = eliminar_trx_fuera_bbox(trx) agrego_indicador( trx, - 'Cantidad de transacciones latlon válidos', - 'transacciones', + "Cantidad de transacciones latlon válidos", + "transacciones", 1, - var_fex='factor_expansion') + var_fex="factor_expansion", + ) agrego_indicador( - trx, - 'Registros válidas en transacciones', - 'transacciones', - 1, - var_fex='') + trx, "Registros válidas en transacciones", "transacciones", 1, var_fex="" + ) # chequear que no haya faltantes en id if trx["id"].isna().any(): @@ -127,21 +131,26 @@ def create_transactions(geolocalizar_trx_config, # crear un id interno de la transaccion n_rows_trx = len(trx) trx["id"] = crear_id_interno( - conn, n_rows=n_rows_trx, tipo_tabla='transacciones') + conn, n_rows=n_rows_trx, tipo_tabla="transacciones" + ) - # # Elminar transacciones unicas en el dia - # trx = eliminar_tarjetas_trx_unica(trx) #### No borrar transacciones - # únicas (quedan en estas con fex=0) + # process gps table when no geocoding + if nombre_archivo_gps is not None: + print("Uploading GPS table without geocoding") + process_and_upload_gps_table( + nombre_archivo_gps=nombre_archivo_gps, + nombres_variables_gps=nombres_variables_gps, + formato_fecha=formato_fecha, + ) # Chequea si modo está null en todos le pone autobus por default if trx.modo.isna().all(): - print('No existe información sobre el modo en transaciones') - print('Se asume que se trata de autobus') - trx['modo'] = 'autobus' + print("No existe información sobre el modo en transaciones") + print("Se asume que se trata de autobus") + trx["modo"] = "autobus" else: # Estandariza los modos - modos_ausentes_configs = ~trx['modo'].isin( - modos_homologados.keys()) + modos_ausentes_configs = ~trx["modo"].isin(modos_homologados.keys()) prop_na = modos_ausentes_configs.sum() / len(trx) if prop_na > 0.15: @@ -151,30 +160,39 @@ def create_transactions(geolocalizar_trx_config, w_str = w_str + "configuracion" warnings.warn(w_str) - trx.loc[modos_ausentes_configs, 'modo'] = 'otros' - trx['modo'] = trx['modo'].replace(modos_homologados) + trx.loc[modos_ausentes_configs, "modo"] = "otros" + trx["modo"] = trx["modo"].replace(modos_homologados) # Si la tarjeta venia con NaNs los numeros van a tener un .0 # que se mantiene si se pasa a strs asi nomas # Si es float convierte a entero - if trx.id_tarjeta.dtype == 'float': - trx.id_tarjeta = pd.to_numeric(trx.id_tarjeta, downcast='integer') + if trx.id_tarjeta.dtype == "float": + trx.id_tarjeta = pd.to_numeric(trx.id_tarjeta, downcast="integer") # Asignar un largo fijo a las tarjetas trx.id_tarjeta = trx.id_tarjeta.map(lambda s: str(s)) - tmp_trx_inicial.id_tarjeta = tmp_trx_inicial.id_tarjeta.map( - lambda s: str(s)) + tmp_trx_inicial.id_tarjeta = tmp_trx_inicial.id_tarjeta.map(lambda s: str(s)) zfill = trx.id_tarjeta.map(lambda s: len(s)).max() - trx['id_tarjeta'] = trx['id_tarjeta'].str.zfill(zfill) - tmp_trx_inicial['id_tarjeta'] = tmp_trx_inicial['id_tarjeta'].str.zfill( - zfill) + trx["id_tarjeta"] = trx["id_tarjeta"].str.zfill(zfill) + tmp_trx_inicial["id_tarjeta"] = tmp_trx_inicial["id_tarjeta"].str.zfill(zfill) # parse date into timestamp trx["fecha"] = trx["fecha"].map(lambda s: s.timestamp()) + # if branches are not present, add branch id as the same as line + if not configs["lineas_contienen_ramales"]: + trx.loc[:, "id_ramal"] = trx["id_linea"].copy() + print(f"Subiendo {len(trx)} registros a la db") + if not "genero" in trx.columns: + trx["genero"] = "-" + if not "tarifa" in trx.columns: + trx["tarifa"] = "-" + trx["genero"] = trx["genero"].fillna("-") + trx["tarifa"] = trx["tarifa"].fillna("-") + lista_cols_db = [ "id", "fecha", @@ -188,38 +206,41 @@ def create_transactions(geolocalizar_trx_config, "id_ramal", "interno", "orden_trx", + "genero", + "tarifa", "latitud", "longitud", - "factor_expansion" + "factor_expansion", ] trx = trx.reindex(columns=lista_cols_db) # Borrar transacciones que tienen id_tarjetas no validos # Construir una tabla de las tarjetas dia con la cantidad de trx validas - tmp_trx_limpio = trx\ - .groupby(['dia', 'id_tarjeta'], as_index=False)\ - .agg(cant_trx_limpias=('id', 'count')) + tmp_trx_limpio = trx.groupby(["dia", "id_tarjeta"], as_index=False).agg( + cant_trx_limpias=("id", "count") + ) # Comparar con las transacciones originales - tmp_trx_limpio = tmp_trx_inicial.merge( - tmp_trx_limpio, on=['dia', 'id_tarjeta']) + tmp_trx_limpio = tmp_trx_inicial.merge(tmp_trx_limpio, on=["dia", "id_tarjeta"]) - tmp_trx_limpio = tmp_trx_limpio[tmp_trx_limpio.cant_trx == - tmp_trx_limpio.cant_trx_limpias] + tmp_trx_limpio = tmp_trx_limpio[ + tmp_trx_limpio.cant_trx == tmp_trx_limpio.cant_trx_limpias + ] # Mantener solo las trx de tarjeta con todas las transacciones validas - print('Borrar informacion de tarjetas con transacciones no validas') + print("Borrar informacion de tarjetas con transacciones no validas") trx = trx.loc[trx.id_tarjeta.isin(tmp_trx_limpio.id_tarjeta), :] agrego_indicador( trx, - 'Cantidad de transacciones limpias', - 'transacciones', + "Cantidad de transacciones limpias", + "transacciones", 1, - var_fex='factor_expansion') + var_fex="factor_expansion", + ) - trx = trx.sort_values('id') + trx = trx.sort_values("id") trx.to_sql("transacciones", conn, if_exists="append", index=False) print("Fin subir base") @@ -249,34 +270,33 @@ def renombrar_columnas_tablas(df, nombres_variables, postfijo): """ # if service id column provided in gps table: - if ( - ('servicios_gps' in nombres_variables) - and (nombres_variables['servicios_gps'] is not None) + if ("servicios_gps" in nombres_variables) and ( + nombres_variables["servicios_gps"] is not None ): # get the name in the original df holding service type data - service_id_col_name = nombres_variables.pop('servicios_gps') + service_id_col_name = nombres_variables.pop("servicios_gps") # get the values for services start and finish gps_config = leer_configs_generales() - start_service_value = gps_config['valor_inicio_servicio'] - finish_service_value = gps_config['valor_fin_servicio'] + start_service_value = gps_config["valor_inicio_servicio"] + finish_service_value = gps_config["valor_fin_servicio"] # create a replace values dict service_id_values = { - start_service_value: 'start_service', - finish_service_value: 'finish_service' + start_service_value: "start_service", + finish_service_value: "finish_service", } - df['service_type'] = df[service_id_col_name].replace( - service_id_values) + df["service_type"] = df[service_id_col_name].replace(service_id_values) # add to the naming dict the new service type attr - nombres_variables.update({'service_type': ''}) + nombres_variables.update({"service_type": ""}) # remove all values besides start and end of service not_service_id_values = ~df[service_id_col_name].isin( - service_id_values.values()) + service_id_values.values() + ) df.loc[not_service_id_values, service_id_col_name] = None @@ -298,9 +318,7 @@ def convertir_fechas(df, formato_fecha, crear_hora=False): """ print("Convirtiendo fechas") - df["fecha"] = pd.to_datetime( - df["fecha"], format=formato_fecha, errors="coerce" - ) + df["fecha"] = pd.to_datetime(df["fecha"], format=formato_fecha, errors="coerce") # Chequear si el formato funciona checkeo = df["fecha"].isna().sum() / len(df) if checkeo > 0.8: @@ -311,27 +329,30 @@ def convertir_fechas(df, formato_fecha, crear_hora=False): + " Verifique el formato de fecha en configuración" + " puede haber un error que no permite la conversión" ) - print("Convirtiendo fechas infiriendo el formato." - + "Esto hará el proceso más lento") + print( + "Convirtiendo fechas infiriendo el formato." + + "Esto hará el proceso más lento" + ) df["fecha"] = pd.to_datetime( - df["fecha"], infer_datetime_format=True, - errors="coerce" + df["fecha"], infer_datetime_format=True, errors="coerce" ) checkeo = df["fecha"].isna().sum() / len(df) - print(f"Infiriendo el formato se pierden {round((checkeo * 100),2)}" - + "por ciento de registros") + print( + f"Infiriendo el formato se pierden {round((checkeo * 100),2)}" + + "por ciento de registros" + ) # Elminar errores en conversion de fechas - df = df.dropna(subset=['fecha'], axis=0) + df = df.dropna(subset=["fecha"], axis=0) df.loc[:, ["dia"]] = df.fecha.dt.strftime("%Y-%m-%d") # Si la hora esta en otra columna, usar esa if crear_hora: - df.loc[:, ["tiempo"]] = df['fecha'].dt.strftime("%H:%M:%S") - df.loc[:, ['hora']] = df['fecha'].dt.hour + df.loc[:, ["tiempo"]] = df["fecha"].dt.strftime("%H:%M:%S") + df.loc[:, ["hora"]] = df["fecha"].dt.hour else: df.loc[:, ["tiempo"]] = None @@ -343,42 +364,50 @@ def agrego_factor_expansion(trx, conn): # Traigo var_fex si existe configs = leer_configs_generales() try: - var_fex = configs['nombres_variables_trx']['factor_expansion'] + var_fex = configs["nombres_variables_trx"]["factor_expansion"] except KeyError: - var_fex = '' + var_fex = "" if not var_fex: - trx['factor_expansion'] = 1 + trx["factor_expansion"] = 1 agrego_indicador( trx, - 'Cantidad de transacciones totales', - 'transacciones', + "Cantidad de transacciones totales", + "transacciones", 0, - var_fex='factor_expansion') + var_fex="factor_expansion", + ) - agrego_indicador(trx[trx.id_tarjeta.notna()].groupby( - ['dia', 'id_tarjeta'], as_index=False).factor_expansion.min(), - 'Cantidad de tarjetas únicas', 'tarjetas', 0, - var_fex='factor_expansion') + agrego_indicador( + trx[trx.id_tarjeta.notna()] + .groupby(["dia", "id_tarjeta"], as_index=False) + .factor_expansion.min(), + "Cantidad de tarjetas únicas", + "tarjetas", + 0, + var_fex="factor_expansion", + ) - tmp_trx_inicial = trx.dropna(subset=['id_tarjeta']).copy() + tmp_trx_inicial = trx.dropna(subset=["id_tarjeta"]).copy() # Si id_tarjeta tenía nan y eran float sacar el .0 - if tmp_trx_inicial.id_tarjeta.dtype == 'float': + if tmp_trx_inicial.id_tarjeta.dtype == "float": tmp_trx_inicial.id_tarjeta = pd.to_numeric( - tmp_trx_inicial.id_tarjeta, downcast='integer') + tmp_trx_inicial.id_tarjeta, downcast="integer" + ) - tmp_trx_inicial = tmp_trx_inicial\ - .groupby(['dia', 'id_tarjeta'], as_index=False)\ - .agg(cant_trx=('id', 'count')) + tmp_trx_inicial = tmp_trx_inicial.groupby( + ["dia", "id_tarjeta"], as_index=False + ).agg(cant_trx=("id", "count")) # Agrego viajes x id_linea para cálculo de factor de expansión - transacciones_linea = trx[trx.id_linea.notna()]\ - .groupby(['dia', 'id_linea'], - as_index=False - ).factor_expansion.sum( - ).rename(columns={'factor_expansion': 'transacciones'}) + transacciones_linea = ( + trx[trx.id_linea.notna()] + .groupby(["dia", "id_linea"], as_index=False) + .factor_expansion.sum() + .rename(columns={"factor_expansion": "transacciones"}) + ) # borro si ya existen transacciones_linea de una corrida anterior dias_ultima_corrida = pd.read_sql_query( @@ -389,13 +418,14 @@ def agrego_factor_expansion(trx, conn): conn, ) - values = ', '.join([f"'{val}'" for val in dias_ultima_corrida['dia']]) + values = ", ".join([f"'{val}'" for val in dias_ultima_corrida["dia"]]) query = f"DELETE FROM transacciones_linea WHERE dia IN ({values})" conn.execute(query) conn.commit() transacciones_linea.to_sql( - "transacciones_linea", conn, if_exists="append", index=False) + "transacciones_linea", conn, if_exists="append", index=False + ) return trx, tmp_trx_inicial @@ -409,24 +439,53 @@ def eliminar_trx_fuera_bbox(trx): print("Eliminando trx con mal lat long") - configs = leer_configs_generales() - try: - configs = configs["filtro_latlong_bbox"] - print(configs) - - filtro = ( - (trx.longitud > configs["minx"]) - & (trx.latitud > configs["miny"]) - & (trx.longitud < configs["maxx"]) - & (trx.latitud < configs["maxy"]) + original = len(trx) + + zonificaciones = levanto_tabla_sql("zonificaciones") + if len(zonificaciones) > 0: + trx["geometry"] = trx.apply( + lambda row: Point(row["longitud"], row["latitud"]), axis=1 ) + trx = gpd.GeoDataFrame(trx, geometry="geometry", crs=4326) + + zona = zonificaciones.zona.head().values[0] + zonificaciones = zonificaciones[zonificaciones.zona == zona] + zonificaciones = zonificaciones.dissolve(by="zona") + + trx = ( + gpd.sjoin(zonificaciones[["geometry"]], trx) + .drop(["geometry", "index_right"], axis=1) + .drop_duplicates() + .sort_values("id") + .reset_index(drop=True) + ) + else: + configs = leer_configs_generales() + try: + configs = configs["filtro_latlong_bbox"] + print(configs) + + filtro = ( + (trx.longitud > configs["minx"]) + & (trx.latitud > configs["miny"]) + & (trx.longitud < configs["maxx"]) + & (trx.latitud < configs["maxy"]) + ) + + pre = len(trx) + trx = trx.loc[filtro, :] + post = len(trx) + print(pre - post, "casos elminados por latlong fuera del bbox") + + except KeyError: + print("No se especificó una ventana para la bbox") + limpio = len(trx) + print(f"--Se borraron {original-limpio} registros") + + assert ( + len(trx) > 0 + ), "Se borraron todos los registros, verifique el 'filtro_latlong_bbox' en configuraciones_generales.yaml" - pre = len(trx) - trx = trx.loc[filtro, :] - post = len(trx) - print(pre - post, "casos elminados por latlong fuera del bbox") - except KeyError: - print("No se especificó una ventana para la bbox") return trx @@ -491,16 +550,16 @@ def geolocalizar_trx( # crear tablas de trx_eco y gps configs = leer_configs_generales() - conn = iniciar_conexion_db(tipo='data') + conn = iniciar_conexion_db(tipo="data") print("Creando tablas de trx_eco y gps para geolocalizacion") crear_tablas_geolocalizacion() print("Fin crear tablas de trx_eco y gps para geolocalizacion") # Leer archivos de trx_eco - id_tarjeta_trx = nombres_variables_trx['id_tarjeta_trx'] + id_tarjeta_trx = nombres_variables_trx["id_tarjeta_trx"] ruta_trx_eco = os.path.join("data", "data_ciudad", nombre_archivo_trx_eco) - print('Levanta archivo de transacciones', ruta_trx_eco) - trx_eco = pd.read_csv(ruta_trx_eco, dtype={id_tarjeta_trx: 'str'}) + print("Levanta archivo de transacciones", ruta_trx_eco) + trx_eco = pd.read_csv(ruta_trx_eco, dtype={id_tarjeta_trx: "str"}) print("Filtrando transacciones invalidas:", tipo_trx_invalidas) # Filtrar transacciones invalidas @@ -525,26 +584,27 @@ def geolocalizar_trx( trx_eco["id_original"] = trx_eco["id"].copy() n_rows_trx = len(trx_eco) trx_eco["id"] = crear_id_interno( - conn, n_rows=n_rows_trx, tipo_tabla='transacciones') + conn, n_rows=n_rows_trx, tipo_tabla="transacciones" + ) # Agregar factor de expansion trx_eco, tmp_trx_inicial = agrego_factor_expansion(trx_eco, conn) # Guardo los días que se están analizando en la corrida actual - dias_ultima_corrida = pd.DataFrame( - trx_eco.dia.unique(), columns=['dia']) - conn = iniciar_conexion_db(tipo='data') + dias_ultima_corrida = pd.DataFrame(trx_eco.dia.unique(), columns=["dia"]) + conn = iniciar_conexion_db(tipo="data") dias_ultima_corrida.to_sql( - "dias_ultima_corrida", conn, if_exists="replace", index=False) + "dias_ultima_corrida", conn, if_exists="replace", index=False + ) # borro si ya existen transacciones de una corrida anterior - values = ', '.join([f"'{val}'" for val in dias_ultima_corrida['dia']]) + values = ", ".join([f"'{val}'" for val in dias_ultima_corrida["dia"]]) query = f"DELETE FROM transacciones WHERE dia IN ({values})" conn.execute(query) conn.commit() # Eliminar datos con faltantes en variables fundamentales - if configs['lineas_contienen_ramales']: + if configs["lineas_contienen_ramales"]: subset = ["id_tarjeta", "fecha", "id_linea", "id_ramal"] else: subset = ["id_tarjeta", "fecha", "id_linea"] @@ -552,42 +612,46 @@ def geolocalizar_trx( trx_eco = eliminar_NAs_variables_fundamentales(trx_eco, subset) # Convertir id tarjeta en int si son float y tienen .0 - if trx_eco.id_tarjeta.dtype == 'float': - trx_eco.id_tarjeta = pd.to_numeric( - trx_eco.id_tarjeta, downcast='integer') + if trx_eco.id_tarjeta.dtype == "float": + trx_eco.id_tarjeta = pd.to_numeric(trx_eco.id_tarjeta, downcast="integer") - if tmp_trx_inicial.id_tarjeta.dtype == 'float': + if tmp_trx_inicial.id_tarjeta.dtype == "float": tmp_trx_inicial.id_tarjeta = pd.to_numeric( - tmp_trx_inicial.id_tarjeta, downcast='integer') + tmp_trx_inicial.id_tarjeta, downcast="integer" + ) print("Parseando fechas trx_eco") trx_eco["fecha"] = trx_eco["fecha"].map(lambda s: s.timestamp()) - if configs['lineas_contienen_ramales']: + if configs["lineas_contienen_ramales"]: cols = ["id_linea", "id_ramal", "interno"] else: - cols = ["id_linea", "interno"] + cols = ["id_linea", "interno"] trx_eco = trx_eco.dropna(subset=cols) + if not "genero" in trx_eco.columns: + trx_eco["genero"] = "" + if not "tarifa" in trx_eco.columns: + trx_eco["tarifa"] = "" - # # Eliminar trx unica en el dia - # trx_eco = eliminar_tarjetas_trx_unica(trx_eco) - # #### No borrar transacciones únicas (quedan en estas con fex=0) - - cols = ['id', - 'id_original', - 'id_tarjeta', - 'fecha', - 'dia', - 'tiempo', - 'hora', - 'modo', - 'id_linea', - 'id_ramal', - 'interno', - 'orden', - 'factor_expansion'] + cols = [ + "id", + "id_original", + "id_tarjeta", + "fecha", + "dia", + "tiempo", + "hora", + "modo", + "id_linea", + "id_ramal", + "interno", + "orden", + "genero", + "tarifa", + "factor_expansion", + ] trx_eco = trx_eco.reindex(columns=cols) print("Subiendo datos a tablas temporales") @@ -598,18 +662,19 @@ def geolocalizar_trx( process_and_upload_gps_table( nombre_archivo_gps=nombre_archivo_gps, nombres_variables_gps=nombres_variables_gps, - formato_fecha=formato_fecha) + formato_fecha=formato_fecha, + ) # hacer el join por fecha print("Geolocalizando datos") - if configs['lineas_contienen_ramales']: + if configs["lineas_contienen_ramales"]: query = """ WITH trx AS ( select t.id,t.id_original, t.id_tarjeta, datetime(t.fecha, 'unixepoch') as fecha, t.dia,t.tiempo,t.hora, t.modo, t.id_linea, - t.id_ramal, t.interno, t.orden as orden, + t.id_ramal, t.interno, t.orden, t.genero, t.tarifa as tarifa, g.latitud, g.longitud, (t.fecha - g.fecha) / 60 as delta_trx_gps_min, t.factor_expansion, @@ -617,9 +682,10 @@ def geolocalizar_trx( PARTITION BY t."id" ORDER BY g.fecha DESC) AS n_row from trx_eco t, gps g - where t."id_linea" = g."id_linea" - and t."id_ramal" = g."id_ramal" - and t."interno" = g."interno" + where t."dia" = g."dia" + and t."id_linea" = g."id_linea" + and t."id_ramal" = g."id_ramal" + and t."interno" = g."interno" and t.fecha > g.fecha ) SELECT * @@ -632,15 +698,16 @@ def geolocalizar_trx( select t.id,t.id_original, t.id_tarjeta, datetime(t.fecha, 'unixepoch') as fecha, t.dia,t.tiempo,t.hora, t.modo, t.id_linea, - t.interno, t.orden as orden, g.latitud, g.longitud, + t.interno, t.orden, t.genero, t.tarifa as tarifa, g.latitud, g.longitud, (t.fecha - g.fecha) / 60 as delta_trx_gps_min, t.factor_expansion, ROW_NUMBER() OVER( PARTITION BY t."id" ORDER BY g.fecha DESC) AS n_row from trx_eco t, gps g - where t."id_linea" = g."id_linea" - and t."interno" = g."interno" + where t."dia" = g."dia" + and t."id_linea" = g."id_linea" + and t."interno" = g."interno" and t.fecha > g.fecha ) SELECT * @@ -654,23 +721,23 @@ def geolocalizar_trx( parse_dates={"fecha": "%Y-%m-%d %H:%M:%S"}, ) - # trx['fecha'] = pd.to_datetime(trx.fecha, unit='s',errors='coerce') - print( "Gelocalización terminada " + "Resumen diferencia entre las fechas de las trx " + "y las del gps en minutos:" ) print(trx.delta_trx_gps_min.describe()) + trx = trx.drop("delta_trx_gps_min", axis=1) - conn.execute("""DROP TABLE IF EXISTS trx_eco;""") + conn.execute("""DELETE FROM trx_eco;""") conn.close() return trx, tmp_trx_inicial -def process_and_upload_gps_table(nombre_archivo_gps, - nombres_variables_gps, formato_fecha): +def process_and_upload_gps_table( + nombre_archivo_gps, nombres_variables_gps, formato_fecha +): """ Esta función lee el archivo csv de información de gps lo procesa y sube a la base de datos @@ -678,7 +745,7 @@ def process_and_upload_gps_table(nombre_archivo_gps, configs = leer_configs_generales() print("Procesando tabla gps") - conn = iniciar_conexion_db(tipo='data') + conn = iniciar_conexion_db(tipo="data") # crear tabla gps en la db crear_tablas_geolocalizacion() @@ -692,15 +759,19 @@ def process_and_upload_gps_table(nombre_archivo_gps, nombres_variables_gps, postfijo="_gps", ) - - # parsear fechas - gps = eliminar_trx_fuera_bbox(gps) - # Parsear fechas y crear atributo dia # col_hora false para no crear tiempo y hora + gps = convertir_fechas(gps, formato_fecha, crear_hora=False) - if configs['lineas_contienen_ramales']: + # compute expansion factors for gps + veh_exp = get_veh_expansion_from_gps(gps) + veh_exp.to_sql("vehicle_expansion_factors", conn, if_exists="append", index=False) + + # parsear fechas + gps = eliminar_trx_fuera_bbox(gps) + + if configs["lineas_contienen_ramales"]: subset = ["interno", "id_ramal", "id_linea", "latitud", "longitud"] else: subset = ["interno", "id_linea", "latitud", "longitud"] @@ -710,12 +781,18 @@ def process_and_upload_gps_table(nombre_archivo_gps, # Convertir fecha en segundos desde 1970 gps["fecha"] = gps["fecha"].map(lambda s: s.timestamp()) - if configs['lineas_contienen_ramales']: - subset = ['dia', 'id_linea', 'id_ramal', 'interno', - 'fecha', 'latitud', 'longitud'] + if configs["lineas_contienen_ramales"]: + subset = [ + "dia", + "id_linea", + "id_ramal", + "interno", + "fecha", + "latitud", + "longitud", + ] else: - subset = ['dia', 'id_linea', 'interno', - 'fecha', 'latitud', 'longitud'] + subset = ["dia", "id_linea", "interno", "fecha", "latitud", "longitud"] gps = gps.drop_duplicates(subset=subset) @@ -724,33 +801,38 @@ def process_and_upload_gps_table(nombre_archivo_gps, # crear un id interno de la transaccion n_rows_gps = len(gps) - gps["id"] = crear_id_interno( - conn, n_rows=n_rows_gps, tipo_tabla='gps') + gps["id"] = crear_id_interno(conn, n_rows=n_rows_gps, tipo_tabla="gps") # si se informa un service type que el start_service exista - if 'service_type' in gps.columns: - if not (gps.service_type == 'start_service').any(): + if "service_type" in gps.columns: + if not (gps.service_type == "start_service").any(): raise Exception( "No hay valores que indiquen el inicio de un servicio. " - "Revisar el configs para servicios_gps") + "Revisar el configs para servicios_gps" + ) # compute distance between gps points gps = compute_distance_km_gps(gps) - cols = ['id', - 'id_original', - 'dia', - 'id_linea', - 'id_ramal', - 'interno', - 'fecha', - 'latitud', - 'longitud', - 'velocity', - 'service_type', - 'distance_km', - 'h3' - ] + # if branches are not present, add branch id as the same as line + if not configs["lineas_contienen_ramales"]: + gps.loc[:, "id_ramal"] = gps["id_linea"].copy() + + cols = [ + "id", + "id_original", + "dia", + "id_linea", + "id_ramal", + "interno", + "fecha", + "latitud", + "longitud", + "velocity", + "service_type", + "distance_km", + "h3", + ] gps = gps.reindex(columns=cols) @@ -759,6 +841,46 @@ def process_and_upload_gps_table(nombre_archivo_gps, gps.to_sql("gps", conn, if_exists="append", index=False) +def count_unique_vehicles(s): + return len(s.unique()) + + +def all_gps_broken(s): + if (s == 0).all() or s.isna().all(): + return 1 + else: + return 0 + + +def get_veh_expansion_from_gps(gps): + """ + This function takes a gps table + and computes average speed in kmr by + vehicle line and day + """ + vehicles_with_gps_broken = ( + gps.reindex(columns=["id_linea", "dia", "interno", "latitud", "longitud"]) + .groupby(["id_linea", "dia", "interno"], as_index=False) + .agg(vehicles_no_gps_lon=("longitud", all_gps_broken)) + ) + vehicles_with_gps_broken = vehicles_with_gps_broken.groupby( + ["id_linea", "dia"], as_index=False + ).agg( + unique_vehicles=("interno", "count"), + broken_gps_veh=("vehicles_no_gps_lon", "sum"), + ) + + vehicles_with_gps_broken["veh_exp"] = vehicles_with_gps_broken.unique_vehicles / ( + vehicles_with_gps_broken.unique_vehicles + - vehicles_with_gps_broken.broken_gps_veh + ) + + # cap veh_exp + vehicles_with_gps_broken.loc[vehicles_with_gps_broken.veh_exp > 2, "veh_exp"] = 2 + + return vehicles_with_gps_broken + + @duracion def compute_distance_km_gps(gps_df): @@ -767,10 +889,11 @@ def compute_distance_km_gps(gps_df): distancia_entre_hex = distancia_entre_hex * 2 # Georeferenciar con h3 - gps_df["h3"] = gps_df.apply(geo.h3_from_row, axis=1, - args=(res, "latitud", "longitud")) + gps_df["h3"] = gps_df.apply( + geo.h3_from_row, axis=1, args=(res, "latitud", "longitud") + ) - gps_df = gps_df.sort_values(['dia', 'id_linea', 'interno', 'fecha']) + gps_df = gps_df.sort_values(["dia", "id_linea", "interno", "fecha"]) # Producir un lag con respecto al siguiente posicionamiento gps gps_df["h3_lag"] = ( @@ -783,7 +906,6 @@ def compute_distance_km_gps(gps_df): gps_df = gps_df.dropna(subset=["h3", "h3_lag"]) gps_dict = gps_df.to_dict("records") gps_df.loc[:, ["distance_km"]] = list(map(geo.distancia_h3, gps_dict)) - gps_df.loc[:, ["distance_km"]] = gps_df["distance_km"] * \ - distancia_entre_hex - gps_df = gps_df.drop(['h3_lag'], axis=1) + gps_df.loc[:, ["distance_km"]] = gps_df["distance_km"] * distancia_entre_hex + gps_df = gps_df.drop(["h3_lag"], axis=1) return gps_df diff --git a/urbantrips/datamodel/trips.py b/urbantrips/datamodel/trips.py index 70a0e15..456fb26 100644 --- a/urbantrips/datamodel/trips.py +++ b/urbantrips/datamodel/trips.py @@ -71,7 +71,7 @@ def cambia_id_viajes_etapas_tarjeta_dia(df): 'nuevo_id_etapa': 'id_etapa'})\ .reindex(columns=['id', 'id_tarjeta', 'dia', 'id_viaje', 'id_etapa', 'tiempo', 'hora', 'modo', 'id_linea', 'id_ramal', - 'interno', 'latitud', 'longitud', 'h3_o', 'h3_d', + 'interno', 'genero', 'tarifa', 'latitud', 'longitud', 'h3_o', 'h3_d', 'od_validado', 'factor_expansion_original', 'factor_expansion_linea', 'factor_expansion_tarjeta']) @@ -262,6 +262,7 @@ def create_trips_from_legs(): conn.execute(query) conn.commit() + etapas.to_sql("etapas", conn, if_exists="append", index=False) print(f'Creando tabla de viajes de {len(etapas)} etapas') @@ -276,6 +277,8 @@ def create_trips_from_legs(): "hora": "first", "h3_o": "first", "h3_d": "last", + "genero": "first", + "tarifa": "first", "od_validado": "min", "factor_expansion_linea": "mean", "factor_expansion_tarjeta": "mean" @@ -334,6 +337,8 @@ def create_trips_from_legs(): 'otros', 'h3_o', 'h3_d', + 'genero', + 'tarifa', 'od_validado', 'factor_expansion_linea', 'factor_expansion_tarjeta'] @@ -415,6 +420,7 @@ def rearrange_trip_id_same_od(): ) print("Crear nuevos ids") + # crear nuevos ids nuevos_ids_etapas_viajes = cambia_id_viajes_etapas_tarjeta_dia(etapas) @@ -433,8 +439,49 @@ def rearrange_trip_id_same_od(): etapas.to_sql("etapas", conn_data, if_exists="append", index=False) - print('len etapas final', len(etapas)) + conn_data.close() print("Fin correxión de ids de etapas y viajes con mismo od") + + +@duracion +def compute_trips_travel_time(): + """ + This function reads from legs travel time in gps and stations + and computes travel times for trips + """ + + conn_data = iniciar_conexion_db(tipo='data') + + print("Insertando tiempos de viaje a etapas en base a gps y estaciones") + + q = """ + INSERT INTO travel_times_legs (dia, id, id_tarjeta, id_etapa, id_viaje, travel_time_min) + SELECT e.dia, e.id,e.id_tarjeta, e.id_etapa,e.id_viaje, + (ifnull(tg.travel_time_min,0) + ifnull(ts.travel_time_min,0)) tt + FROM etapas e + JOIN dias_ultima_corrida d + ON e.dia = d.dia + LEFT JOIN travel_times_gps tg + ON e.id = tg.id + LEFT JOIN travel_times_stations ts + ON e.id = ts.id + WHERE e.od_validado = 1 + AND (tg.travel_time_min IS NOT NULL OR ts.travel_time_min IS NOT NULL) + """ + conn_data.execute(q) + conn_data.commit() + + print("Insertando tiempos de viaje a viajes en base a etapas") + q = """ + INSERT INTO travel_times_trips (dia, id_tarjeta, id_viaje, travel_time_min) + SELECT tt.dia, tt.id_tarjeta,tt.id_viaje, sum(tt.travel_time_min) AS travel_time_min + FROM travel_times_legs tt + JOIN dias_ultima_corrida d + ON tt.dia = d.dia + GROUP BY tt.dia, tt.id_tarjeta,tt.id_viaje ; + """ + conn_data.execute(q) + conn_data.commit() diff --git a/urbantrips/geo/geo.py b/urbantrips/geo/geo.py index 6e62118..f78a9d6 100644 --- a/urbantrips/geo/geo.py +++ b/urbantrips/geo/geo.py @@ -31,6 +31,17 @@ def h3_from_row(row, res, lat, lng): return h3.geo_to_h3(row[lat], row[lng], resolution=res) +def convert_h3_to_resolution(h3_index, target_resolution): + try: + # Get the geographic center of the original H3 index + center_geo = h3.h3_to_geo(h3_index) + # Convert the geographic center to the target resolution H3 index + result = h3.geo_to_h3(center_geo[0], center_geo[1], target_resolution) + except: + result = np.nan + return result + + def get_h3_buffer_ring_size(resolucion_h3, buffer_meters): """ Esta funcion toma una resolucion h3 y una tolerancia en metros @@ -71,12 +82,23 @@ def get_stop_hex_ring(h, ring_size): def h3togeo(x): try: - result = str(h3.h3_to_geo(x)[0]) + ", " + str(h3.h3_to_geo(x)[1]) + result = str(h3.h3_to_geo(x)[0]) + ", " + str(h3.h3_to_geo(x)[1]) except (TypeError, ValueError): result = '' return result +def h3togeo_latlon(x, latlon='lat'): + try: + if latlon == 'lat': + result = h3.h3_to_geo(x)[1] + else: + result = h3.h3_to_geo(x)[0] + except (TypeError, ValueError): + result = np.nan + return result + + def h3dist(x, distancia_entre_hex=1, h3_o='', h3_d=''): if len(h3_o) == 0: h3_o = 'h3_o' @@ -148,16 +170,17 @@ def bring_latlon(x, latlon='lat'): posi = 1 try: result = float(x.split(',')[posi]) - except (AttributeError, IndexError): + except (AttributeError, IndexError): result = 0 return result + def normalizo_lat_lon(df, h3_o='h3_o', h3_d='h3_d', origen='', destino=''): - + if len(origen) == 0: origen = h3_o if len(destino) == 0: @@ -166,8 +189,8 @@ def normalizo_lat_lon(df, df["origin"] = df[h3_o].apply(h3togeo) df['lon_o_tmp'] = df["origin"].apply(bring_latlon, latlon='lon') df['lat_o_tmp'] = df["origin"].apply(bring_latlon, latlon='lat') - - df["destination"] = df[h3_d].apply(h3togeo) + + df["destination"] = df[h3_d].apply(h3togeo) df['lon_d_tmp'] = df["destination"].apply(bring_latlon, latlon='lon') df['lat_d_tmp'] = df["destination"].apply(bring_latlon, latlon='lat') @@ -244,6 +267,97 @@ def normalizo_lat_lon(df, return df +# Convert H3 index to latitude and longitude, handling exceptions gracefully + + +def h3_to_lat_lon(h3_hex): + try: + lat, lon = h3.h3_to_geo(h3_hex) + except Exception as e: # Catch specific exceptions if possible + lat, lon = np.nan, np.nan + return pd.Series((lat, lon)) + +# Convert H3 index to its parent at a specified resolution, with error handling + + +def h3toparent(x, res=6): + try: + x = h3.h3_to_parent(x, res) + except: + x = '' + return x + + +def h3_to_geodataframe(h3_indexes, var_h3=''): + ''' + h3_indexes es una lista de h3 (no pasar en formato DataFrame) + ''' + if len(var_h3) == 0: + var_h3 = 'h3_index' + + if isinstance(h3_indexes, pd.DataFrame): + h3_indexes = h3_indexes[var_h3].unique() + + # Convert H3 indexes to polygons + polygons = [] + for index in h3_indexes: + boundary_lat_lng = h3.h3_to_geo_boundary(index) + # Swap lat-lon to lon-lat for each coordinate + boundary_lon_lat = [(lon, lat) for lat, lon in boundary_lat_lng] + polygons.append(Polygon(boundary_lon_lat)) + + # Create GeoDataFrame + gdf = gpd.GeoDataFrame({ + var_h3: h3_indexes, + 'geometry': polygons + }, crs=4326) + + return gdf + + +def point_to_h3(row, resolution): + # Extract the latitude and longitude from the point + x, y = row.geometry.x, row.geometry.y + return h3.geo_to_h3(y, x, resolution) # Note: lat, lon order + +# Calculate weighted mean, handling division by zero or empty inputs + + +def weighted_mean(series, weights): + try: + result = (series * weights).sum() / weights.sum() + except ZeroDivisionError: + result = np.nan + return result + +# Function to convert H3 hex id to a polygon + + +def hex_to_polygon(hex_id): + try: + vertices = h3.h3_to_geo_boundary(hex_id, geo_json=True) + return Polygon(vertices) + except ValueError: + print(f"Skipping invalid H3 ID: {hex_id}") + return None + + +def create_h3_gdf(hexagon_ids): + # Convert H3 hexagons to polygons + polygons = [hex_to_polygon(hex_id) for hex_id in hexagon_ids if hex_id] + + # Filter out None values if any invalid hexagons were skipped + valid_data = [(hex_id, poly) for hex_id, poly in zip( + hexagon_ids, polygons) if poly is not None] + + # Create a GeoDataFrame + gdf = gpd.GeoDataFrame({ + 'hexagon_id': [data[0] for data in valid_data], + 'geometry': [data[1] for data in valid_data] + }, crs="EPSG:4326") + + return gdf + def create_point_from_h3(h): return Point(h3.h3_to_geo(h)[::-1]) @@ -359,3 +473,84 @@ def distancia_h3(row, *args, **kwargs): except ValueError as e: out = None return out + + +def create_sections_geoms(sections_df, buffer_meters=False): + """ + This function takes a sections dataframe with points + and a buffer parameters in meters + produced by kpi.create_route_section_points() + and returns a geodataframe with section geoms + """ + geom = [LineString( + [[sections_df.loc[i, 'x'], sections_df.loc[i, 'y']], + [sections_df.loc[i+1, 'x'], sections_df.loc[i+1, 'y']]] + ) for i in sections_df.index[:-1]] + + gdf = gpd.GeoDataFrame(sections_df.iloc[:-1, :], + geometry=geom, crs='epsg:4326') + if buffer_meters: + epsg_m = get_epsg_m() + + gdf = gdf.to_crs(epsg=epsg_m) + + gdf.geometry = gdf.geometry.buffer(buffer_meters) + + return gdf + + +def classify_leg_into_station(legs, stations, leg_h3_field, join_branch_id=False): + """ + Computes for a distance between a h3 point and its lag + + Parameters + ---------- + legs : pandas.DataFrame + df with legs info holding geocoding from a single line_id + stations : geopandas.GeoDataFrame + gdf with stations data and geoms + holding station id and line_id + leg_h3_field: str + column name holding the h3 to geocode into station + + Returns + ---------- + pandas.DataFrame + df with leg id and nearest station id + + """ + configs = leer_configs_generales() + tolerancia_parada_destino = configs["tolerancia_parada_destino"] + epsg_m = get_epsg_m() + + # Filter stations based on line and branch + if not stations.crs.is_projected: + stations = stations.to_crs(epsg=epsg_m) + + legs_line_id = legs.id_linea.unique()[0] + + stations = stations.loc[stations.id_linea == legs_line_id, :] + + if join_branch_id: + legs_branch_id = legs.id_linea.unique()[0] + stations = stations.loc[stations.id_ramal == legs_branch_id, :] + + # create legs geoms + lat, lon = zip(*legs[leg_h3_field].map(h3.h3_to_geo).tolist()) + + legs_geom = gpd.GeoDataFrame(legs.reindex(columns=['dia', 'id']), + geometry=gpd.GeoSeries.from_xy( + x=lon, y=lat, crs=4326, index=legs.index), + crs=4326).to_crs(epsg=epsg_m) + + # join stations + legs_w_station = gpd.sjoin_nearest( + legs_geom, stations, lsuffix='legs', rsuffix='station', + how='inner', max_distance=tolerancia_parada_destino, + distance_col='distancia', exclusive=True) + legs_w_station = legs_w_station\ + .sort_values('distancia')\ + .drop_duplicates(subset='id_legs')\ + .reindex(columns=['dia', 'id_legs', 'id_station']) + + return legs_w_station diff --git a/urbantrips/kpi/kpi.py b/urbantrips/kpi/kpi.py index bb6099c..0952acb 100644 --- a/urbantrips/kpi/kpi.py +++ b/urbantrips/kpi/kpi.py @@ -1,17 +1,19 @@ import itertools -import geopandas as gpd import warnings +from math import floor +import geopandas as gpd import pandas as pd import numpy as np import weightedstats as ws -from math import floor -import re import h3 from urbantrips.geo import geo from urbantrips.utils.utils import ( duracion, iniciar_conexion_db, - leer_configs_generales + leer_configs_generales, + is_date_string, + check_date_type, + create_line_ids_sql_filter, ) # KPI WRAPPER @@ -69,6 +71,7 @@ def compute_kpi(): # compute amount of hourly services by line and type of day compute_dispatched_services_by_line_hour_typeday() + else: print("No hay servicios procesados.") @@ -78,10 +81,11 @@ def compute_kpi(): # SECTION LOAD KPI + @duracion def compute_route_section_load( - id_linea=False, - rango_hrs=False, + line_ids=False, + hour_range=False, n_sections=10, section_meters=None, day_type="weekday", @@ -91,7 +95,7 @@ def compute_route_section_load( Parameters ---------- - id_linea : int, list of ints or bool + line_ids : int, list of ints or bool route id or list of route ids present in the legs dataset. Route section load will be computed for that subset of lines. If False, it will run with all routes. @@ -107,195 +111,199 @@ def compute_route_section_load( day_type: str type of day on which the section load is to be computed. It can take `weekday`, `weekend` or a specific day in format 'YYYY-MM-DD' - """ - dat_type_is_a_date = is_date_string(day_type) + check_date_type(day_type) - # check day type format - day_type_format_ok = ( - day_type in ["weekday", "weekend"]) or dat_type_is_a_date - - if not day_type_format_ok: - raise Exception( - "dat_type debe ser `weekday`, `weekend` o fecha 'YYYY-MM-DD'" - ) + line_ids_where = create_line_ids_sql_filter(line_ids) if n_sections is not None: if n_sections > 1000: - raise Exception( - "No se puede utilizar una cantidad de secciones > 1000") + raise Exception("No se puede utilizar una cantidad de secciones > 1000") conn_data = iniciar_conexion_db(tipo="data") conn_insumos = iniciar_conexion_db(tipo="insumos") - # delete old data - q_delete = delete_old_route_section_load_data_q( - id_linea, rango_hrs, n_sections, section_meters, day_type - ) + # read legs data + legs = read_legs_data_by_line_hours_and_day(line_ids_where, hour_range, day_type) - cur = conn_data.cursor() - cur.execute(q_delete) - conn_data.commit() + # read routes geoms + q_route_geoms = "select * from lines_geoms" + q_route_geoms = q_route_geoms + line_ids_where + route_geoms = pd.read_sql(q_route_geoms, conn_insumos) + route_geoms["geometry"] = gpd.GeoSeries.from_wkt(route_geoms.wkt) + route_geoms = gpd.GeoDataFrame(route_geoms, geometry="geometry", crs="EPSG:4326") - # Read data from legs and route geoms - q_rec = f"select * from lines_geoms" - q_main_etapas = f"select * from etapas" + # Set which parameter to use to split route geoms into sections - # If line and hour, get that subset - if id_linea: - if type(id_linea) == int: - id_linea = [id_linea] + epsg_m = geo.get_epsg_m() - lineas_str = ",".join(map(str, id_linea)) + # project geoms and get for each geom both n_sections and meter + route_geoms = route_geoms.to_crs(epsg=epsg_m) - id_linea_where = f" where id_linea in ({lineas_str})" - q_rec = q_rec + id_linea_where - q_main_etapas = q_main_etapas + id_linea_where + if section_meters: + # warning if meters params give to many sections + # get how many sections given the meters + n_sections = (route_geoms.geometry.length / section_meters).astype(int) + + else: + section_meters = (route_geoms.geometry.length / n_sections).astype(int) + + if isinstance(n_sections, int): + n_sections_check = pd.Series([n_sections]) + else: + n_sections_check = n_sections - if rango_hrs: - rango_hrs_where = ( - f" and hora >= {rango_hrs[0]} and hora <= {rango_hrs[1]}" + if any(n_sections_check > 1000): + warnings.warn( + "Algunos recorridos tienen mas de 1000 segmentos" + "Puede arrojar resultados imprecisos " ) - q_main_etapas = q_main_etapas + rango_hrs_where - if dat_type_is_a_date: - q_main_etapas = q_main_etapas + f" and dia = '{day_type}'" + route_geoms = route_geoms.to_crs(epsg=4326) - q_etapas = f""" - select e.* - from ({q_main_etapas}) e - where e.od_validado==1 - """ + # set the section length in meters + route_geoms["section_meters"] = section_meters - print("Obteniendo datos de etapas y rutas") + # set the number of sections + route_geoms["n_sections"] = n_sections - # get data for legs and route geoms - etapas = pd.read_sql(q_etapas, conn_data) + # check which section geoms are already crated + new_route_geoms = check_exists_route_section_points_table(route_geoms) - if not dat_type_is_a_date: - # create a weekday_filter - weekday_filter = pd.to_datetime( - etapas.dia, format="%Y-%m-%d").dt.dayofweek < 5 + # create the line and n sections pair missing and upload it to the db + if len(new_route_geoms) > 0: - if day_type == "weekday": - etapas = etapas.loc[weekday_filter, :] - else: - etapas = etapas.loc[~weekday_filter, :] + upload_route_section_points_table(new_route_geoms, delete_old_data=False) - recorridos = pd.read_sql(q_rec, conn_insumos) - recorridos["geometry"] = gpd.GeoSeries.from_wkt(recorridos.wkt) + # delete old seciton load data + yr_mos = legs.yr_mo.unique() - # Set which parameter to use to slit route geoms - if section_meters: - epsg_m = geo.get_epsg_m() - # project geoms and get for each geom a n_section - recorridos = gpd.GeoDataFrame( - recorridos, geometry="geometry", crs="EPSG:4326" - ).to_crs(epsg=epsg_m) - recorridos["n_sections"] = ( - recorridos.geometry.length / section_meters).astype(int) - - if (recorridos.n_sections > 1000).any(): - warnings.warn( - "Algunos recorridos tienen mas de 1000 segmentos" - "Puede arrojar resultados imprecisos " - ) + delete_old_route_section_load_data( + route_geoms, hour_range, day_type, yr_mos, db_type="data" + ) - recorridos = recorridos.to_crs(epsg=4326) - else: - recorridos["n_sections"] = n_sections + # compute section load print("Computing section load per route ...") - if (len(recorridos) > 0) and (len(etapas) > 0): + if (len(route_geoms) > 0) and (len(legs) > 0): - section_load_table = etapas.groupby("id_linea").apply( + section_load_table = legs.groupby(["id_linea", "yr_mo"]).apply( compute_section_load_table, - recorridos=recorridos, - rango_hrs=rango_hrs, - day_type=day_type + route_geoms=route_geoms, + hour_range=hour_range, + day_type=day_type, ) - section_load_table = section_load_table.reset_index(drop=True) + section_load_table = section_load_table.droplevel(2, axis=0).reset_index() # Add section meters to table - section_load_table["section_meters"] = section_meters - + section_load_table["legs"] = section_load_table["legs"].map(int) section_load_table = section_load_table.reindex( columns=[ "id_linea", + "yr_mo", "day_type", "n_sections", "section_meters", "sentido", "section_id", - "x", - "y", - "hora_min", - "hora_max", - "cantidad_etapas", - "prop_etapas", + "hour_min", + "hour_max", + "legs", + "prop", ] ) print("Uploading data to db...") section_load_table.to_sql( - "ocupacion_por_linea_tramo", conn_data, if_exists="append", - index=False,) - else: - print('No existen recorridos o etapas para las líneas') - print("Cantidad de lineas:", len(id_linea)) - print("Cantidad de recorridos", len(recorridos)) - print("Cantidad de etapas", len(etapas)) - + "ocupacion_por_linea_tramo", + conn_data, + if_exists="append", + index=False, + ) -def is_date_string(input_str): - pattern = re.compile(r"^\d{4}-\d{2}-\d{2}$") - if pattern.match(input_str): - return True + return section_load_table else: - return False + print("No existen recorridos o etapas para las líneas") + print("Cantidad de lineas:", len(line_ids)) + print("Cantidad de recorridos", len(route_geoms)) + print("Cantidad de etapas", len(legs)) -def delete_old_route_section_load_data_q( - id_linea, rango_hrs, n_sections, section_meters, day_type -): +def check_exists_route_section_points_table(route_geoms): + """ + This function checks if the route section points table exists + for those lines and n_sections in the route geoms gdf + """ - # hour range filter - if rango_hrs: - hora_min_filter = f"= {rango_hrs[0]}" - hora_max_filter = f"= {rango_hrs[1]}" - else: - hora_min_filter = "is NULL" - hora_max_filter = "is NULL" + conn_insumos = iniciar_conexion_db(tipo="insumos") + q = """ + select distinct id_linea,n_sections, 1 as section_exists from routes_section_id_coords + """ + route_sections = pd.read_sql(q, conn_insumos) + conn_insumos.close() - q_delete = f""" - delete from ocupacion_por_linea_tramo - where hora_min {hora_min_filter} - and hora_max {hora_max_filter} - and day_type = '{day_type}' - """ + new_route_geoms = route_geoms.merge( + route_sections, on=["id_linea", "n_sections"], how="left" + ) + new_route_geoms = new_route_geoms.loc[ + new_route_geoms.section_exists.isna(), ["id_linea", "n_sections", "geometry"] + ] - # route id filter - if id_linea: + return new_route_geoms - if type(id_linea) == int: - id_linea = [id_linea] - lineas_str = ",".join(map(str, id_linea)) +def delete_old_route_section_load_data( + route_geoms, hour_range, day_type, yr_mos, db_type="data" +): + """ + Deletes old data in table ocupacion_por_linea_tramo + """ + table_name = "ocupacion_por_linea_tramo" - q_delete = q_delete + f" and id_linea in ({lineas_str})" + if db_type == "data": + conn = iniciar_conexion_db(tipo="data") + else: + conn = iniciar_conexion_db(tipo="dash") - if section_meters: - q_delete = q_delete + f" and section_meters = {section_meters}" + # hour range filter + if hour_range: + hora_min_filter = f"= {hour_range[0]}" + hora_max_filter = f"= {hour_range[1]}" else: - q_delete = ( - q_delete + - f" and n_sections = {n_sections} and section_meters is NULL" - ) - q_delete = q_delete + ";" - return q_delete + hora_min_filter = "is NULL" + hora_max_filter = "is NULL" + + # create a df with n sections for each line + delete_df = route_geoms.reindex(columns=["id_linea", "n_sections"]) + for yr_mo in yr_mos: + for _, row in delete_df.iterrows(): + # Delete old data for those parameters + print("Borrando datos antiguos de ocupacion_por_linea_tramo") + print(row.id_linea) + print(f"{row.n_sections} secciones") + print(yr_mo) + if hour_range: + print(f"y horas desde {hour_range[0]} a {hour_range[1]}") + + q_delete = f""" + delete from {table_name} + where id_linea = {row.id_linea} + and hour_min {hora_min_filter} + and hour_max {hora_max_filter} + and day_type = '{day_type}' + and n_sections = {row.n_sections} + and yr_mo = '{yr_mo}'; + """ + + cur = conn.cursor() + cur.execute(q_delete) + conn.commit() + + conn.close() + print("Fin borrado datos previos") def add_od_lrs_to_legs_from_route(legs_df, route_geom): @@ -317,8 +325,8 @@ def add_od_lrs_to_legs_from_route(legs_df, route_geom): """ # create Points for origins and destination - legs_df["o"] = legs_df['h3_o'].map(geo.create_point_from_h3) - legs_df["d"] = legs_df['h3_d'].map(geo.create_point_from_h3) + legs_df["o"] = legs_df["h3_o"].map(geo.create_point_from_h3) + legs_df["d"] = legs_df["h3_d"].map(geo.create_point_from_h3) # Assign a route section id legs_df["o_proj"] = list( @@ -331,79 +339,88 @@ def add_od_lrs_to_legs_from_route(legs_df, route_geom): return legs_df -def compute_section_load_table( - df, recorridos, rango_hrs, day_type, *args, **kwargs): +def compute_section_load_table(legs, route_geoms, hour_range, day_type): """ Computes for a route a table with the load per section Parameters ---------- - df : pandas.DataFrame + legs : pandas.DataFrame table of legs in a route - recorridos : geopandas.GeoDataFrame + route_geoms : geopandas.GeoDataFrame routes geoms - rango_hrs : tuple + hour_range : tuple tuple holding hourly range (from,to). Returns ---------- - legs_by_sections_full : pandas.DataFrame + pandas.DataFrame table of section load stats per route id, hour range and day type """ - id_linea = df.id_linea.unique()[0] - n_sections = recorridos.n_sections.unique()[0] - - print(f"Computing section load id_route {id_linea}") + line_id = legs.id_linea.unique()[0] + print(f"Calculando carga por tramo para linea id {line_id}") - if (recorridos.id_linea == id_linea).any(): + if (route_geoms.id_linea == line_id).any(): + route = route_geoms.loc[route_geoms.id_linea == line_id, :] - route_geom = recorridos.loc[recorridos.id_linea == - id_linea, "geometry"].item() + route_geom = route.geometry.item() + n_sections = route.n_sections.item() + section_meters = route.section_meters.item() - df = add_od_lrs_to_legs_from_route(legs_df=df, route_geom=route_geom) + df = add_od_lrs_to_legs_from_route(legs_df=legs, route_geom=route_geom) # Assign a direction based on line progression - df = df.reindex( - columns=["dia", "o_proj", "d_proj", "factor_expansion_linea"]) + df = df.reindex(columns=["dia", "o_proj", "d_proj", "factor_expansion_linea"]) df["sentido"] = [ - "ida" if row.o_proj <= row.d_proj - else "vuelta" for _, row in df.iterrows() + "ida" if row.o_proj <= row.d_proj else "vuelta" for _, row in df.iterrows() ] # Compute total legs per direction # First totals per day - totals_by_direction = df\ - .groupby(["dia", "sentido"], as_index=False)\ - .agg(cant_etapas_sentido=("factor_expansion_linea", "sum")) + totals_by_direction = df.groupby(["dia", "sentido"], as_index=False).agg( + cant_etapas_sentido=("factor_expansion_linea", "sum") + ) # then average for weekdays - totals_by_direction = totals_by_direction\ - .groupby(["sentido"], as_index=False)\ - .agg(cant_etapas_sentido=("cant_etapas_sentido", "mean")) + totals_by_direction = totals_by_direction.groupby( + ["sentido"], as_index=False + ).agg(cant_etapas_sentido=("cant_etapas_sentido", "mean")) # compute section ids based on amount of sections - section_ids = create_route_section_ids(n_sections) + section_ids_LRS = create_route_section_ids(n_sections) + # remove 0 form cuts so 0 gets included in bin + section_ids_LRS_cut = section_ids_LRS.copy() + section_ids_LRS_cut.loc[0] = -0.001 # For each leg, build traversed route segments ids - legs_dict = df.to_dict("records") - leg_route_sections_df = pd.concat( - map(build_leg_route_sections_df, legs_dict, - itertools.repeat(section_ids)) + section_ids = list(range(1, len(section_ids_LRS_cut))) + + df["o_proj"] = pd.cut( + df.o_proj, bins=section_ids_LRS_cut, labels=section_ids, right=True ) + df["d_proj"] = pd.cut( + df.d_proj, bins=section_ids_LRS_cut, labels=section_ids, right=True + ) + + # remove legs with no origin or destination projected + df = df.dropna(subset=["o_proj", "d_proj"]) + + legs_dict = df.to_dict("records") + leg_route_sections_df = pd.concat(map(build_leg_route_sections_df, legs_dict)) # compute total legs by section and direction # first adding totals per day - legs_by_sections = leg_route_sections_df\ - .groupby(["dia", "sentido", "section_id"], as_index=False)\ - .agg(size=("factor_expansion_linea", "sum")) + legs_by_sections = leg_route_sections_df.groupby( + ["dia", "sentido", "section_id"], as_index=False + ).agg(size=("factor_expansion_linea", "sum")) # then computing average across days - legs_by_sections = legs_by_sections\ - .groupby(["sentido", "section_id"], as_index=False)\ - .agg(size=("size", "mean")) + legs_by_sections = legs_by_sections.groupby( + ["sentido", "section_id"], as_index=False + ).agg(size=("size", "mean")) # If there is no information for all sections in both directions if len(legs_by_sections) < len(section_ids) * 2: @@ -418,107 +435,83 @@ def compute_section_load_table( legs_by_sections_full = section_direction_full_set.merge( legs_by_sections, how="left", on=["sentido", "section_id"] ) - legs_by_sections_full["cantidad_etapas"] = ( - legs_by_sections_full.size_y.combine_first( - legs_by_sections_full.size_x) + legs_by_sections_full["legs"] = legs_by_sections_full.size_y.combine_first( + legs_by_sections_full.size_x ) legs_by_sections_full = legs_by_sections_full.reindex( - columns=["sentido", "section_id", "cantidad_etapas"] + columns=["sentido", "section_id", "legs"] ) else: - legs_by_sections_full = legs_by_sections.rename( - columns={"size": "cantidad_etapas"} - ) + legs_by_sections_full = legs_by_sections.rename(columns={"size": "legs"}) # sum totals per direction and compute prop_etapas legs_by_sections_full = legs_by_sections_full.merge( totals_by_direction, how="left", on="sentido" ) - legs_by_sections_full["prop_etapas"] = ( - legs_by_sections_full["cantidad_etapas"] - / legs_by_sections_full.cant_etapas_sentido + legs_by_sections_full["prop"] = ( + legs_by_sections_full["legs"] / legs_by_sections_full.cant_etapas_sentido ) - legs_by_sections_full.prop_etapas = ( - legs_by_sections_full.prop_etapas.fillna(0) - ) + legs_by_sections_full["prop"] = legs_by_sections_full["prop"].fillna(0) - legs_by_sections_full = legs_by_sections_full.drop( - "cant_etapas_sentido", axis=1 - ) - legs_by_sections_full["id_linea"] = id_linea + legs_by_sections_full["id_linea"] = line_id # Add hourly range - if rango_hrs: - legs_by_sections_full["hora_min"] = rango_hrs[0] - legs_by_sections_full["hora_max"] = rango_hrs[1] + if hour_range: + legs_by_sections_full["hour_min"] = hour_range[0] + legs_by_sections_full["hour_max"] = hour_range[1] else: - legs_by_sections_full["hora_min"] = None - legs_by_sections_full["hora_max"] = None + legs_by_sections_full["hour_min"] = None + legs_by_sections_full["hour_max"] = None # Add data for type of day and n sections legs_by_sections_full["day_type"] = day_type legs_by_sections_full["n_sections"] = n_sections + legs_by_sections_full["section_meters"] = section_meters - # Add section geom reference - geom = [route_geom.interpolate(section_id, normalized=True) - for section_id in section_ids] - x = [g.x for g in geom] - y = [g.y for g in geom] - section_ids_coords = pd.DataFrame({ - 'section_id': section_ids, - 'x': x, - 'y': y - }) - legs_by_sections_full = legs_by_sections_full.merge( - section_ids_coords, - on='section_id', - how='left' - ) # Set db schema legs_by_sections_full = legs_by_sections_full.reindex( columns=[ - "id_linea", "day_type", "n_sections", + "section_meters", "sentido", "section_id", - "x", - "y", - "hora_min", - "hora_max", - "cantidad_etapas", - "prop_etapas", + "hour_min", + "hour_max", + "legs", + "prop", ] ) return legs_by_sections_full else: - print("No existe recorrido para id_linea:", id_linea) + print("No existe recorrido para id_linea:", line_id) def create_route_section_ids(n_sections): step = 1 / n_sections sections = np.arange(0, 1 + step, step) section_ids = pd.Series(map(floor_rounding, sections)) + # n sections like 6 returns steps with max setion > 1 + section_ids = section_ids[section_ids <= 1] + return section_ids -def build_leg_route_sections_df(row, section_ids): +def build_leg_route_sections_df(row): """ - Computes for a leg a table with all sections id trversed by - that leg based on the origin and destionation's section id + Computes for a leg a table with all sections id traversed by + that leg based on the origin and destionation's section id Parameters ---------- row : dict row in a legs df with origin, destination and direction - section_ids : list - list of sections ids into which classify legs trajectory Returns ---------- @@ -534,28 +527,13 @@ def build_leg_route_sections_df(row, section_ids): # always build it in increasing order if sentido == "ida": - point_o = row["o_proj"] - point_d = row["d_proj"] + o_id = row["o_proj"] + d_id = row["d_proj"] else: - point_o = row["d_proj"] - point_d = row["o_proj"] - - # when d_proj is 1, sections id exclude 1 - if point_d == 1: - point_d = 0.999 - - # get the closest section id to origin - o_id = section_ids - point_o - o_id = o_id[o_id <= 0] - o_id = o_id.idxmax() - - # get the closest section id to destination - d_id = section_ids - point_d - d_id = d_id[d_id >= 0] - d_id = d_id.idxmin() + o_id = row["d_proj"] + d_id = row["o_proj"] - # build a df with all traversed section ids - leg_route_sections = section_ids[o_id: d_id + 1] + leg_route_sections = list(range(o_id, d_id + 1)) leg_route_sections_df = pd.DataFrame( { "dia": [dia] * len(leg_route_sections), @@ -591,6 +569,7 @@ def get_route_section_id(point, route_geom): # GENERAL PURPOSE KPIS WITH GPS + def read_data_for_daily_kpi(): """ Read legs and gps micro data from db and @@ -635,7 +614,7 @@ def read_data_for_daily_kpi(): """ processed_days = pd.read_sql(processed_days_q, conn_data) processed_days = processed_days.dia - processed_days = ', '.join([f"'{val}'" for val in processed_days]) + processed_days = ", ".join([f"'{val}'" for val in processed_days]) print("Leyendo datos de oferta") q = f""" @@ -682,7 +661,7 @@ def add_distances_to_legs(legs): """ configs = leer_configs_generales() - h3_original_res = configs['resolucion_h3'] + h3_original_res = configs["resolucion_h3"] min_distance = h3.edge_length(resolution=h3_original_res, unit="km") conn_insumos = iniciar_conexion_db(tipo="insumos") @@ -700,8 +679,8 @@ def add_distances_to_legs(legs): print("Sumando distancias a etapas") # use distances h3 when osm missing - distances.loc[:, ['distance']] = ( - distances.distance_osm_drive.combine_first(distances.distance_h3) + distances.loc[:, ["distance"]] = distances.distance_osm_drive.combine_first( + distances.distance_h3 ) distances = distances.reindex(columns=["h3_o", "h3_d", "distance"]) @@ -711,7 +690,7 @@ def add_distances_to_legs(legs): # add minimum distance in km as length of h3 legs.distance = legs.distance.map(lambda x: max(x, min_distance)) - no_distance = legs.distance.isna().sum()/len(legs) * 100 + no_distance = legs.distance.isna().sum() / len(legs) * 100 print("Hay un {:.2f} % de etapas sin distancias ".format(no_distance)) conn_insumos.close() @@ -739,39 +718,39 @@ def compute_kpi_by_line_day(legs, gps): """ conn_data = iniciar_conexion_db(tipo="data") + # get veh expansion factors for supply data + q = "select id_linea,dia,veh_exp from vehicle_expansion_factors" + vehicle_expansion_factor = pd.read_sql(q, conn_data) + gps = gps.merge(vehicle_expansion_factor, on=["dia", "id_linea"], how="left") + # demand data - day_demand_stats = legs\ - .groupby(['id_linea', 'dia'], as_index=False)\ + day_demand_stats = ( + legs.dropna(subset=["distance", "factor_expansion_linea"]) + .groupby(["id_linea", "dia"], as_index=False) .apply(demand_stats) + ) # supply data - day_supply_stats = gps\ - .groupby(['id_linea', 'dia'], as_index=False)\ - .apply(supply_stats) + day_supply_stats = gps.groupby(["id_linea", "dia"], as_index=False).apply( + supply_stats + ) - day_stats = day_demand_stats\ - .merge(day_supply_stats, - how='inner', on=['id_linea', 'dia']) + day_stats = day_demand_stats.merge( + day_supply_stats, how="inner", on=["id_linea", "dia"] + ) # compute KPI - day_stats['pvd'] = day_stats.tot_pax / \ - day_stats.tot_veh - day_stats['kvd'] = day_stats.tot_km / \ - day_stats.tot_veh - day_stats['ipk'] = day_stats.tot_pax / \ - day_stats.tot_km + day_stats["pvd"] = day_stats.tot_pax / day_stats.tot_veh + day_stats["kvd"] = day_stats.tot_km / day_stats.tot_veh + day_stats["ipk"] = day_stats.tot_pax / day_stats.tot_km # Calcular espacios-km ofertados (EKO) y los espacios-km demandados (EKD). - day_stats['ekd_mean'] = day_stats.tot_pax * \ - day_stats.dmt_mean - day_stats['ekd_median'] = day_stats.tot_pax * \ - day_stats.dmt_median - day_stats['eko'] = day_stats.tot_km * 60 + day_stats["ekd_mean"] = day_stats.tot_pax * day_stats.dmt_mean + day_stats["ekd_median"] = day_stats.tot_pax * day_stats.dmt_median + day_stats["eko"] = day_stats.tot_km * 60 - day_stats['fo_mean'] = day_stats.ekd_mean / \ - day_stats.eko - day_stats['fo_median'] = day_stats.ekd_median / \ - day_stats.eko + day_stats["fo_mean"] = day_stats.ekd_mean / day_stats.eko + day_stats["fo_median"] = day_stats.ekd_median / day_stats.eko cols = [ "id_linea", @@ -785,7 +764,7 @@ def compute_kpi_by_line_day(legs, gps): "kvd", "ipk", "fo_mean", - "fo_median" + "fo_median", ] day_stats = day_stats.reindex(columns=cols) @@ -832,14 +811,12 @@ def compute_kpi_by_line_typeday(): daily_data = pd.read_sql(q, conn_data) # get day of the week - weekend = pd.to_datetime(daily_data['dia'].copy()).dt.dayofweek > 4 - daily_data.loc[:, ['dia']] = 'weekday' - daily_data.loc[weekend, ['dia']] = 'weekend' + weekend = pd.to_datetime(daily_data["dia"].copy()).dt.dayofweek > 4 + daily_data.loc[:, ["dia"]] = "weekday" + daily_data.loc[weekend, ["dia"]] = "weekend" # compute aggregated stats - type_of_day_stats = daily_data\ - .groupby(['id_linea', 'dia'], as_index=False)\ - .mean() + type_of_day_stats = daily_data.groupby(["id_linea", "dia"], as_index=False).mean() print("Subiendo indicadores por linea a la db") @@ -855,7 +832,7 @@ def compute_kpi_by_line_typeday(): "kvd", "ipk", "fo_mean", - "fo_median" + "fo_median", ] type_of_day_stats = type_of_day_stats.reindex(columns=cols) @@ -872,6 +849,7 @@ def compute_kpi_by_line_typeday(): # KPIS BY SERVICE + @duracion def compute_kpi_by_service(): """ @@ -980,9 +958,8 @@ def compute_kpi_by_service(): invalid_demand_dups = pd.read_sql(q_invalid_services, conn_data) # remove duplicates leaving the first, i.e. next valid service in time - invalid_demand = invalid_demand_dups.drop_duplicates( - subset=['id'], keep='first') - invalid_demand = invalid_demand.dropna(subset=['service_id']) + invalid_demand = invalid_demand_dups.drop_duplicates(subset=["id"], keep="first") + invalid_demand = invalid_demand.dropna(subset=["service_id"]) # create single demand by service df service_demand = pd.concat([valid_demand, invalid_demand]) @@ -991,13 +968,13 @@ def compute_kpi_by_service(): service_demand = add_distances_to_legs(legs=service_demand) # TODO: remove this line when factor is corrected - service_demand['factor_expansion_linea'] = ( - service_demand['factor_expansion_linea'].replace(0, 1) - ) + service_demand["factor_expansion_linea"] = service_demand[ + "factor_expansion_linea" + ].replace(0, 1) # compute demand stats - service_demand_stats = service_demand\ - .groupby(['dia', 'id_linea', 'interno', 'service_id'], as_index=False)\ - .apply(demand_stats) + service_demand_stats = service_demand.groupby( + ["dia", "id_linea", "interno", "service_id"], as_index=False + ).apply(demand_stats) # read supply service data service_supply_q = """ @@ -1010,30 +987,40 @@ def compute_kpi_by_service(): service_supply = pd.read_sql(service_supply_q, conn_data) # merge supply and demand data - service_stats = service_supply\ - .merge(service_demand_stats, how='left', - on=['dia', 'id_linea', 'interno', 'service_id']) + service_stats = service_supply.merge( + service_demand_stats, + how="left", + on=["dia", "id_linea", "interno", "service_id"], + ) service_stats.tot_pax = service_stats.tot_pax.fillna(0) # compute stats - service_stats['ipk'] = service_stats['tot_pax'] / service_stats['tot_km'] - service_stats['ekd_mean'] = service_stats['tot_pax'] * \ - service_stats['dmt_mean'] - service_stats['ekd_median'] = service_stats['tot_pax'] * \ - service_stats['dmt_median'] - service_stats['eko'] = service_stats['tot_km'] * 60 - service_stats['fo_mean'] = service_stats['ekd_mean'] / service_stats['eko'] - service_stats['fo_median'] = service_stats['ekd_median'] / \ - service_stats['eko'] - - service_stats['hora_inicio'] = service_stats.min_datetime.str[10:13].map( - int) - service_stats['hora_fin'] = service_stats.max_datetime.str[10:13].map(int) + service_stats["ipk"] = service_stats["tot_pax"] / service_stats["tot_km"] + service_stats["ekd_mean"] = service_stats["tot_pax"] * service_stats["dmt_mean"] + service_stats["ekd_median"] = service_stats["tot_pax"] * service_stats["dmt_median"] + service_stats["eko"] = service_stats["tot_km"] * 60 + service_stats["fo_mean"] = service_stats["ekd_mean"] / service_stats["eko"] + service_stats["fo_median"] = service_stats["ekd_median"] / service_stats["eko"] + + service_stats["hora_inicio"] = service_stats.min_datetime.str[10:13].map(int) + service_stats["hora_fin"] = service_stats.max_datetime.str[10:13].map(int) # reindex to meet schema - cols = ['id_linea', 'dia', 'interno', 'service_id', - 'hora_inicio', 'hora_fin', 'tot_km', 'tot_pax', 'dmt_mean', - 'dmt_median', 'ipk', 'fo_mean', 'fo_median'] + cols = [ + "id_linea", + "dia", + "interno", + "service_id", + "hora_inicio", + "hora_fin", + "tot_km", + "tot_pax", + "dmt_mean", + "dmt_median", + "ipk", + "fo_mean", + "fo_median", + ] service_stats = service_stats.reindex(columns=cols) @@ -1050,11 +1037,9 @@ def compute_kpi_by_service(): def demand_stats(df): d = {} d["tot_pax"] = df["factor_expansion_linea"].sum() - d["dmt_mean"] = np.average( - a=df['distance'], weights=df.factor_expansion_linea) + d["dmt_mean"] = np.average(a=df["distance"], weights=df.factor_expansion_linea) d["dmt_median"] = ws.weighted_median( - data=df['distance'].tolist(), - weights=df.factor_expansion_linea.tolist() + data=df["distance"].tolist(), weights=df.factor_expansion_linea.tolist() ) return pd.Series(d, index=["tot_pax", "dmt_mean", "dmt_median"]) @@ -1062,20 +1047,90 @@ def demand_stats(df): def supply_stats(df): d = {} - d["tot_veh"] = len(df.interno.unique()) - d["tot_km"] = df.distance_km.sum() + d["tot_veh"] = len(df.interno.unique()) * df.veh_exp.unique()[0] + d["tot_km"] = df.distance_km.sum() * df.veh_exp.unique()[0] return pd.Series(d, index=["tot_veh", "tot_km"]) + # GENERAL PURPOSE KPI WITH NO GPS +def compute_speed_by_day_veh_hour(): + """ + This function read gps data and computes + average speed by veh for each day and line + """ + conn_data = iniciar_conexion_db(tipo="data") + processed_days = get_processed_days(table_name="basic_kpi_by_line_day") + + # read data + q = f""" + select dia,id_linea,fecha,interno,velocity,distance_km + from gps + where dia not in ({processed_days}) + ; + """ + gps_df = pd.read_sql(q, conn_data) + conn_data.close() + + # create a lag in date + gps_df = gps_df.sort_values(["dia", "id_linea", "interno", "fecha"]) + gps_df["fecha_lag"] = ( + gps_df.reindex(columns=["dia", "id_linea", "interno", "fecha"]) + .groupby(["dia", "id_linea", "interno"]) + .shift(-1) + ) + + # compute delta in time + gps_df = gps_df.dropna(subset=["fecha", "fecha_lag"]) + gps_df.loc[:, ["delta_hr"]] = (gps_df.fecha_lag - gps_df.fecha) / (60 * 60) + gps_df = gps_df.loc[gps_df.delta_hr > 0, :] + + # compute speed in kmr + gps_df.loc[:, ["speed_kmh_veh_h"]] = gps_df.distance_km / gps_df.delta_hr + gps_df["hora"] = pd.to_datetime(gps_df["fecha"], unit="s").dt.hour + + # get mean speed by day, linea, veh + speed_vehicle_hour_gps = ( + gps_df.reindex( + columns=["dia", "id_linea", "interno", "hora", "speed_kmh_veh_h"] + ) + .groupby(["dia", "id_linea", "interno", "hora"], as_index=False) + .mean() + ) + + speed_vehicle_hour_gps = speed_vehicle_hour_gps.loc[ + speed_vehicle_hour_gps.speed_kmh_veh_h > 0, : + ] + + return speed_vehicle_hour_gps + + +def gps_table_exists(): + conn_data = iniciar_conexion_db(tipo="data") + cur = conn_data.cursor() + q = """ + SELECT tbl_name FROM sqlite_master + WHERE type='table' + AND tbl_name='gps'; + """ + listOfTables = cur.execute(q).fetchall() + conn_data.close() + if listOfTables == []: + print("No existe tabla GPS en la base") + print("Se calcularán KPI básicos en base a datos de demanda") + return False + else: + return True + + @duracion def run_basic_kpi(): - conn_data = iniciar_conexion_db(tipo='data') + conn_data = iniciar_conexion_db(tipo="data") # read already process days - processed_days = get_processed_days(table_name='basic_kpi_by_line_day') + processed_days = get_processed_days(table_name="basic_kpi_by_line_day") # read unprocessed data from legs @@ -1095,7 +1150,7 @@ def run_basic_kpi(): legs = add_distances_to_legs(legs=legs) # if there is no full timestamp - if legs['tiempo'].isna().all(): + if legs["tiempo"].isna().all(): unique_line_ids = legs.id_linea.unique() id_lines = np.repeat(unique_line_ids, 24) @@ -1103,111 +1158,140 @@ def run_basic_kpi(): # fix commercial speed at 15kmh for all veh speed_vehicle_hour = pd.DataFrame( - {'id_linea': id_lines, - 'hora': hours, - 'speed_kmh_veh_h': [15]*24*len(unique_line_ids) - } + { + "id_linea": id_lines, + "hora": hours, + "speed_kmh_veh_h": [15] * 24 * len(unique_line_ids), + } ) - speed_vehicle_hour = legs\ - .reindex(columns=['dia', 'id_linea', 'interno'])\ - .drop_duplicates()\ - .merge(speed_vehicle_hour, - on=['id_linea'], - how='left') - - # else compute commercial speed based on demand + speed_vehicle_hour = ( + legs.reindex(columns=["dia", "id_linea", "interno"]) + .drop_duplicates() + .merge(speed_vehicle_hour, on=["id_linea"], how="left") + ) + + # else compute commercial speed based on gps or demand else: - legs.loc[:, ['datetime']] = legs.dia + ' ' + legs.tiempo + if gps_table_exists(): + print("Calculando velocidades comerciales usando tabla gps") + speed_vehicle_hour = compute_speed_by_day_veh_hour() + else: + # compute mean veh speed using demand data + legs.loc[:, ["datetime"]] = legs.dia + " " + legs.tiempo - legs.loc[:, ['time']] = pd.to_datetime( - legs.loc[:, 'datetime'], format="%Y-%m-%d %H:%M:%S") + legs.loc[:, ["time"]] = pd.to_datetime( + legs.loc[:, "datetime"], format="%Y-%m-%d %H:%M:%S" + ) - print("Calculando velocidades comerciales") - # compute vehicle speed per hour - speed_vehicle_hour = legs\ - .groupby(['dia', 'id_linea', 'interno'])\ - .apply(compute_speed_by_veh_hour) + print("Calculando velocidades comerciales") + # compute vehicle speed per hour + speed_vehicle_hour = legs.groupby(["dia", "id_linea", "interno"]).apply( + compute_speed_by_veh_hour + ) - speed_vehicle_hour = speed_vehicle_hour.droplevel(3).reset_index() + speed_vehicle_hour = speed_vehicle_hour.droplevel(3).reset_index() # set a max speed te remove outliers speed_max = 60 - speed_vehicle_hour.loc[speed_vehicle_hour.speed_kmh_veh_h > - speed_max, 'speed_kmh_veh_h'] = speed_max + speed_vehicle_hour.loc[ + speed_vehicle_hour.speed_kmh_veh_h > speed_max, "speed_kmh_veh_h" + ] = speed_max + + speed_vehicle_hour = speed_vehicle_hour.dropna() print("Eliminando casos atipicos en velocidades comerciales") # compute standard deviation to remove low speed outliers - speed_dev = speed_vehicle_hour\ - .groupby(['dia', 'id_linea'], as_index=False)\ - .agg( - mean=('speed_kmh_veh_h', 'mean'), - std=('speed_kmh_veh_h', 'std') - ) - speed_dev['speed_min'] = speed_dev['mean'] - \ - (2 * speed_dev['std']).map(lambda x: max(1, x)) - speed_dev = speed_dev.reindex(columns=['dia', 'id_linea', 'speed_min']) + speed_dev = speed_vehicle_hour.groupby(["dia", "id_linea"], as_index=False).agg( + mean=("speed_kmh_veh_h", "mean"), std=("speed_kmh_veh_h", "std") + ) + speed_dev["speed_min"] = speed_dev["mean"] - (2 * speed_dev["std"]).map( + lambda x: max(1, x) + ) + speed_dev = speed_dev.reindex(columns=["dia", "id_linea", "speed_min"]) speed_vehicle_hour = speed_vehicle_hour.merge( - speed_dev, on=['dia', 'id_linea'], how='left') + speed_dev, on=["dia", "id_linea"], how="left" + ) - speed_mask = (speed_vehicle_hour.speed_kmh_veh_h < speed_max) &\ - (speed_vehicle_hour.speed_kmh_veh_h > speed_vehicle_hour.speed_min) + speed_mask = (speed_vehicle_hour.speed_kmh_veh_h < speed_max) & ( + speed_vehicle_hour.speed_kmh_veh_h > speed_vehicle_hour.speed_min + ) - speed_vehicle_hour = speed_vehicle_hour.loc[speed_mask, [ - 'dia', 'id_linea', 'interno', 'hora', 'speed_kmh_veh_h']] + speed_vehicle_hour = speed_vehicle_hour.loc[ + speed_mask, ["dia", "id_linea", "interno", "hora", "speed_kmh_veh_h"] + ] # compute by hour to fill nans in vehicle speed - speed_line_hour = speed_vehicle_hour\ - .drop('interno', axis=1)\ - .groupby(['dia', 'id_linea', 'hora'], as_index=False).mean()\ - .rename(columns={'speed_kmh_veh_h': 'speed_kmh_line_h'}) + speed_line_hour = ( + speed_vehicle_hour.drop("interno", axis=1) + .groupby(["dia", "id_linea", "hora"], as_index=False) + .mean() + .rename(columns={"speed_kmh_veh_h": "speed_kmh_line_h"}) + ) - speed_line_day = speed_vehicle_hour\ - .drop('interno', axis=1)\ - .groupby(['dia', 'id_linea'], as_index=False).mean()\ - .rename(columns={'speed_kmh_veh_h': 'speed_kmh_line_day'}) + speed_line_day = ( + speed_vehicle_hour.drop("interno", axis=1) + .groupby(["dia", "id_linea"], as_index=False) + .mean() + .rename(columns={"speed_kmh_veh_h": "speed_kmh_line_day"}) + ) # add commercial speed to demand data - legs = legs\ - .merge(speed_vehicle_hour, - on=['dia', 'id_linea', 'interno', 'hora'], how='left')\ - .merge(speed_line_hour, on=['dia', 'id_linea', 'hora'], how='left') + legs = legs.merge( + speed_vehicle_hour, on=["dia", "id_linea", "interno", "hora"], how="left" + ).merge(speed_line_hour, on=["dia", "id_linea", "hora"], how="left") - legs['speed_kmh'] = legs.speed_kmh_veh_h.combine_first( - legs.speed_kmh_line_h) + legs["speed_kmh"] = legs.speed_kmh_veh_h.combine_first(legs.speed_kmh_line_h) - print("Calculando pasajero equivalente otros KPI por dia" - ", linea, interno y hora") + print("Calculando pasajero equivalente otros KPI por dia" ", linea, interno y hora") # get an vehicle space equivalent passenger - legs['eq_pax'] = (legs.distance / legs.speed_kmh) * \ - legs.factor_expansion_linea + legs["eq_pax"] = (legs.distance / legs.speed_kmh) * legs.factor_expansion_linea # COMPUTE KPI BY DAY LINE VEHICLE HOUR - kpi_by_veh = legs\ - .reindex(columns=['dia', 'id_linea', 'interno', 'hora', - 'factor_expansion_linea', 'eq_pax', 'distance'])\ - .groupby(['dia', 'id_linea', 'interno', 'hora'], as_index=False)\ + kpi_by_veh = ( + legs.reindex( + columns=[ + "dia", + "id_linea", + "interno", + "hora", + "factor_expansion_linea", + "eq_pax", + "distance", + ] + ) + .groupby(["dia", "id_linea", "interno", "hora"], as_index=False) .agg( - tot_pax=('factor_expansion_linea', 'sum'), - eq_pax=('eq_pax', 'sum'), - dmt=('distance', 'mean') + tot_pax=("factor_expansion_linea", "sum"), + eq_pax=("eq_pax", "sum"), + dmt=("distance", "mean"), ) + ) # compute ocupation factor - kpi_by_veh['of'] = kpi_by_veh.eq_pax/60 * 100 + kpi_by_veh["of"] = kpi_by_veh.eq_pax / 60 * 100 # add average commercial speed data - kpi_by_veh = kpi_by_veh\ - .merge(speed_vehicle_hour, - on=['dia', 'id_linea', 'interno', 'hora'], how='left') - kpi_by_veh = kpi_by_veh.rename(columns={'speed_kmh_veh_h': 'speed_kmh'}) + kpi_by_veh = kpi_by_veh.merge( + speed_vehicle_hour, on=["dia", "id_linea", "interno", "hora"], how="left" + ) + kpi_by_veh = kpi_by_veh.rename(columns={"speed_kmh_veh_h": "speed_kmh"}) print("Subiendo a la base de datos") # set schema and upload to db - cols = ['dia', 'id_linea', 'interno', 'hora', 'tot_pax', 'eq_pax', - 'dmt', 'of', 'speed_kmh'] + cols = [ + "dia", + "id_linea", + "interno", + "hora", + "tot_pax", + "eq_pax", + "dmt", + "of", + "speed_kmh", + ] kpi_by_veh = kpi_by_veh.reindex(columns=cols) @@ -1223,44 +1307,48 @@ def run_basic_kpi(): # COMPUTE KPI BY DAY LINE HOUR # compute ocupation factor - ocupation_factor_line_hour = kpi_by_veh\ - .reindex(columns=['dia', 'id_linea', 'hora', 'of'])\ - .groupby(['dia', 'id_linea', 'hora'], as_index=False)\ + ocupation_factor_line_hour = ( + kpi_by_veh.reindex(columns=["dia", "id_linea", "hora", "of"]) + .groupby(["dia", "id_linea", "hora"], as_index=False) .mean() + ) # compute supply as unique vehicles day per hour - supply = legs\ - .reindex(columns=['dia', 'id_linea', 'interno', 'hora'])\ - .drop_duplicates().groupby(['dia', 'id_linea', 'hora']).size()\ - .reset_index()\ - .rename(columns={0: 'veh'}) + supply = ( + legs.reindex(columns=["dia", "id_linea", "interno", "hora"]) + .drop_duplicates() + .groupby(["dia", "id_linea", "hora"]) + .size() + .reset_index() + .rename(columns={0: "veh"}) + ) # compute demand as total legs per hour and DMT - demand = legs\ - .reindex(columns=['dia', 'id_linea', 'hora', - 'factor_expansion_linea', 'distance'])\ - .groupby(['dia', 'id_linea', 'hora'], as_index=False)\ - .agg( - pax=('factor_expansion_linea', 'sum'), - dmt=('distance', 'mean') + demand = ( + legs.reindex( + columns=["dia", "id_linea", "hora", "factor_expansion_linea", "distance"] ) + .groupby(["dia", "id_linea", "hora"], as_index=False) + .agg(pax=("factor_expansion_linea", "sum"), dmt=("distance", "mean")) + ) # compute line kpi table - kpi_by_line_hr = supply\ - .merge(demand, on=['dia', 'id_linea', 'hora'], how='left')\ - .merge(ocupation_factor_line_hour, - on=['dia', 'id_linea', 'hora'], how='left') + kpi_by_line_hr = supply.merge( + demand, on=["dia", "id_linea", "hora"], how="left" + ).merge(ocupation_factor_line_hour, on=["dia", "id_linea", "hora"], how="left") kpi_by_line_hr = kpi_by_line_hr.merge( - speed_line_hour, on=['dia', 'id_linea', 'hora'], how='left') - kpi_by_line_hr = kpi_by_line_hr.rename( - columns={'speed_kmh_line_h': 'speed_kmh'}) + speed_line_hour, on=["dia", "id_linea", "hora"], how="left" + ) + kpi_by_line_hr = kpi_by_line_hr.rename(columns={"speed_kmh_line_h": "speed_kmh"}) print("Subiendo a la base de datos") + # create month + kpi_by_line_hr["yr_mo"] = kpi_by_line_hr.dia.str[:7] + # set schema and upload to db - cols = ['dia', 'id_linea', 'hora', 'veh', 'pax', 'dmt', 'of', - 'speed_kmh'] + cols = ["dia", "yr_mo", "id_linea", "hora", "veh", "pax", "dmt", "of", "speed_kmh"] kpi_by_line_hr = kpi_by_line_hr.reindex(columns=cols) @@ -1275,41 +1363,49 @@ def run_basic_kpi(): print("Calculando pasajero equivalente otros KPI por dia y linea") # compute daily stats - ocupation_factor_line = kpi_by_veh\ - .reindex(columns=['dia', 'id_linea', 'of'])\ - .groupby(['dia', 'id_linea'], as_index=False).mean() + ocupation_factor_line = ( + kpi_by_veh.reindex(columns=["dia", "id_linea", "of"]) + .groupby(["dia", "id_linea"], as_index=False) + .mean() + ) # compute supply as unique vehicles day - daily_supply = legs\ - .reindex(columns=['dia', 'id_linea', 'interno'])\ - .drop_duplicates().groupby(['dia', 'id_linea'])\ - .size()\ - .reset_index()\ - .rename(columns={0: 'veh'}) + daily_supply = ( + legs.reindex(columns=["dia", "id_linea", "interno"]) + .drop_duplicates() + .groupby(["dia", "id_linea"]) + .size() + .reset_index() + .rename(columns={0: "veh"}) + ) # compute demand as total legs per hour and DMT - daily_demand = legs\ - .reindex(columns=['dia', 'id_linea', - 'factor_expansion_linea', 'distance'])\ - .groupby(['dia', 'id_linea'], as_index=False)\ + daily_demand = ( + legs.reindex(columns=["dia", "id_linea", "factor_expansion_linea", "distance"]) + .groupby(["dia", "id_linea"], as_index=False) .agg( - pax=('factor_expansion_linea', 'sum'), - dmt=('distance', 'mean'), + pax=("factor_expansion_linea", "sum"), + dmt=("distance", "mean"), ) + ) # compute line kpi table - kpi_by_line_day = daily_supply\ - .merge(daily_demand, on=['dia', 'id_linea'], how='left')\ - .merge(ocupation_factor_line, on=['dia', 'id_linea'], how='left') + kpi_by_line_day = daily_supply.merge( + daily_demand, on=["dia", "id_linea"], how="left" + ).merge(ocupation_factor_line, on=["dia", "id_linea"], how="left") kpi_by_line_day = kpi_by_line_day.merge( - speed_line_day, on=['dia', 'id_linea'], how='left') + speed_line_day, on=["dia", "id_linea"], how="left" + ) kpi_by_line_day = kpi_by_line_day.rename( - columns={'speed_kmh_line_day': 'speed_kmh'}) + columns={"speed_kmh_line_day": "speed_kmh"} + ) + + kpi_by_line_day["yr_mo"] = kpi_by_line_day.dia.str[:7] print("Subiendo a la base de datos") # set schema and upload to db - cols = ['dia', 'id_linea', 'veh', 'pax', 'dmt', 'of', 'speed_kmh'] + cols = ["dia", "yr_mo", "id_linea", "veh", "pax", "dmt", "of", "speed_kmh"] kpi_by_line_day = kpi_by_line_day.reindex(columns=cols) @@ -1328,7 +1424,7 @@ def run_basic_kpi(): def compute_basic_kpi_line_typeday(): - conn_data = iniciar_conexion_db(tipo='data') + conn_data = iniciar_conexion_db(tipo="data") print("Borrando datos desactualizados por tipo de dia") @@ -1347,19 +1443,23 @@ def compute_basic_kpi_line_typeday(): kpi_by_line_day = pd.read_sql(q, conn_data) # get day of the week - weekend = pd.to_datetime(kpi_by_line_day['dia'].copy()).dt.dayofweek > 4 - kpi_by_line_day.loc[:, ['dia']] = 'weekday' - kpi_by_line_day.loc[weekend, ['dia']] = 'weekend' - kpi_by_line_day + weekend = pd.to_datetime(kpi_by_line_day["dia"].copy()).dt.dayofweek > 4 + kpi_by_line_day.loc[:, ["dia"]] = "weekday" + kpi_by_line_day.loc[weekend, ["dia"]] = "weekend" # compute aggregated stats - kpi_by_line_typeday = kpi_by_line_day\ - .groupby(['dia', 'id_linea',], as_index=False)\ - .mean() + kpi_by_line_typeday = kpi_by_line_day.groupby( + [ + "dia", + "yr_mo", + "id_linea", + ], + as_index=False, + ).mean() print("Subiendo a la base de datos") # set schema and upload to db - cols = ['dia', 'id_linea', 'veh', 'pax', 'dmt', 'of', 'speed_kmh'] + cols = ["dia", "yr_mo", "id_linea", "veh", "pax", "dmt", "of", "speed_kmh"] kpi_by_line_typeday = kpi_by_line_typeday.reindex(columns=cols) @@ -1374,7 +1474,7 @@ def compute_basic_kpi_line_typeday(): def compute_basic_kpi_line_hr_typeday(): - conn_data = iniciar_conexion_db(tipo='data') + conn_data = iniciar_conexion_db(tipo="data") print("Borrando datos desactualizados por tipo de dia") @@ -1394,18 +1494,18 @@ def compute_basic_kpi_line_hr_typeday(): kpi_by_line_hr = pd.read_sql(q, conn_data) # get day of the week - weekend = pd.to_datetime(kpi_by_line_hr['dia'].copy()).dt.dayofweek > 4 - kpi_by_line_hr.loc[:, ['dia']] = 'weekday' - kpi_by_line_hr.loc[weekend, ['dia']] = 'weekend' + weekend = pd.to_datetime(kpi_by_line_hr["dia"].copy()).dt.dayofweek > 4 + kpi_by_line_hr.loc[:, ["dia"]] = "weekday" + kpi_by_line_hr.loc[weekend, ["dia"]] = "weekend" # compute aggregated stats - kpi_by_line_typeday = kpi_by_line_hr\ - .groupby(['dia', 'id_linea', 'hora'], as_index=False)\ - .mean() + kpi_by_line_typeday = kpi_by_line_hr.groupby( + ["dia", "yr_mo", "id_linea", "hora"], as_index=False + ).mean() print("Subiendo a la base de datos") # set schema and upload to db - cols = ['dia', 'id_linea', 'hora', 'veh', 'pax', 'dmt', 'of', 'speed_kmh'] + cols = ["dia", "yr_mo", "id_linea", "hora", "veh", "pax", "dmt", "of", "speed_kmh"] kpi_by_line_typeday = kpi_by_line_typeday.reindex(columns=cols) @@ -1420,53 +1520,62 @@ def compute_basic_kpi_line_hr_typeday(): def compute_speed_by_veh_hour(legs_vehicle): - if len(legs_vehicle) < 2: - return None + try: + if len(legs_vehicle) < 2: + return None - res = 11 - distance_between_hex = h3.edge_length(resolution=res, unit="m") - distance_between_hex = distance_between_hex * 2 + res = 11 + distance_between_hex = h3.edge_length(resolution=res, unit="m") + distance_between_hex = distance_between_hex * 2 - speed = legs_vehicle.reindex( - columns=['interno', 'hora', 'time', 'latitud', 'longitud']) - speed["h3"] = speed.apply( - geo.h3_from_row, axis=1, args=(res, "latitud", "longitud")) + speed = legs_vehicle.reindex( + columns=["interno", "hora", "time", "latitud", "longitud"] + ) + speed["h3"] = speed.apply( + geo.h3_from_row, axis=1, args=(res, "latitud", "longitud") + ) - # get only one h3 per vehicle hour - speed = speed.drop_duplicates(subset=['interno', 'hora', 'h3']) - if len(speed) < 2: - return None - speed = speed.sort_values('time') + # get only one h3 per vehicle hour + speed = speed.drop_duplicates(subset=["interno", "hora", "h3"]) + if len(speed) < 2: + return None + speed = speed.sort_values("time") - # compute meters between h3 - speed['h3_lag'] = speed['h3'].shift(1) - speed['time_lag'] = speed['time'].shift(1) + # compute meters between h3 + speed["h3_lag"] = speed["h3"].shift(1) + speed["time_lag"] = speed["time"].shift(1) - speed = speed.dropna(subset=['h3_lag', 'time_lag']) + speed = speed.dropna(subset=["h3_lag", "time_lag"]) - speed['seconds'] = (speed['time'] - speed['time_lag'] - ).map(lambda x: x.total_seconds()) + speed["seconds"] = (speed["time"] - speed["time_lag"]).map( + lambda x: x.total_seconds() + ) - speed['meters'] = speed\ - .apply(lambda row: h3.h3_distance(row['h3'], row['h3_lag']), - axis=1) * distance_between_hex + speed["meters"] = ( + speed.apply(lambda row: h3.h3_distance(row["h3"], row["h3_lag"]), axis=1) + * distance_between_hex + ) - speed_by_hour = speed\ - .reindex(columns=['hora', 'seconds', 'meters'])\ - .groupby('hora', as_index=False)\ - .agg( - meters=('meters', 'sum'), - seconds=('seconds', 'sum'), - n=('hora', 'count'), + speed_by_hour = ( + speed.reindex(columns=["hora", "seconds", "meters"]) + .groupby("hora", as_index=False) + .agg( + meters=("meters", "sum"), + seconds=("seconds", "sum"), + n=("hora", "count"), + ) ) - # remove vehicles with less than 2 pax + # remove vehicles with less than 2 pax - speed_by_hour = speed_by_hour.loc[speed_by_hour.n > 2, :] - speed_by_hour['speed_kmh_veh_h'] = speed_by_hour.meters / \ - speed_by_hour.seconds * 3.6 - speed_by_hour = speed_by_hour.reindex(columns=['hora', 'speed_kmh_veh_h']) + speed_by_hour = speed_by_hour.loc[speed_by_hour.n > 2, :] + speed_by_hour["speed_kmh_veh_h"] = ( + speed_by_hour.meters / speed_by_hour.seconds * 3.6 + ) + speed_by_hour = speed_by_hour.reindex(columns=["hora", "speed_kmh_veh_h"]) - return speed_by_hour + return speed_by_hour + except: + return None def get_processed_days(table_name): @@ -1486,7 +1595,7 @@ def get_processed_days(table_name): """ - conn_data = iniciar_conexion_db(tipo='data') + conn_data = iniciar_conexion_db(tipo="data") # get processed days in basic data processed_days_q = f""" @@ -1495,7 +1604,7 @@ def get_processed_days(table_name): """ processed_days = pd.read_sql(processed_days_q, conn_data) processed_days = processed_days.dia - processed_days = ', '.join([f"'{val}'" for val in processed_days]) + processed_days = ", ".join([f"'{val}'" for val in processed_days]) return processed_days @@ -1527,7 +1636,7 @@ def compute_dispatched_services_by_line_hour_day(): """ processed_days = pd.read_sql(processed_days_q, conn_data) processed_days = processed_days.dia - processed_days = ', '.join([f"'{val}'" for val in processed_days]) + processed_days = ", ".join([f"'{val}'" for val in processed_days]) print("Leyendo datos de servicios") @@ -1548,29 +1657,22 @@ def compute_dispatched_services_by_line_hour_day(): print("Procesando servicios por hora") - daily_services['hora'] = daily_services.min_datetime.str[10:13].map( - int) + daily_services["hora"] = daily_services.min_datetime.str[10:13].map(int) - daily_services = daily_services.drop(['min_datetime'], axis=1) + daily_services = daily_services.drop(["min_datetime"], axis=1) # computing services by hour - dispatched_services_stats = daily_services\ - .groupby(['id_linea', 'dia', 'hora'], as_index=False)\ - .agg(servicios=('hora', 'count')) + dispatched_services_stats = daily_services.groupby( + ["id_linea", "dia", "hora"], as_index=False + ).agg(servicios=("hora", "count")) print("Fin procesamiento servicios por hora") print("Subiendo datos a la DB") - cols = [ - "id_linea", - "dia", - "hora", - "servicios" - ] + cols = ["id_linea", "dia", "hora", "servicios"] - dispatched_services_stats = dispatched_services_stats.reindex( - columns=cols) + dispatched_services_stats = dispatched_services_stats.reindex(columns=cols) dispatched_services_stats.to_sql( "services_by_line_hour", @@ -1631,23 +1733,18 @@ def compute_dispatched_services_by_line_hour_typeday(): print("Procesando servicios por tipo de dia") # get day of the week - weekend = pd.to_datetime(daily_data['dia'].copy()).dt.dayofweek > 4 - daily_data.loc[:, ['dia']] = 'weekday' - daily_data.loc[weekend, ['dia']] = 'weekend' + weekend = pd.to_datetime(daily_data["dia"].copy()).dt.dayofweek > 4 + daily_data.loc[:, ["dia"]] = "weekday" + daily_data.loc[weekend, ["dia"]] = "weekend" # compute aggregated stats - type_of_day_stats = daily_data\ - .groupby(['id_linea', 'dia', 'hora'], as_index=False)\ - .mean() + type_of_day_stats = daily_data.groupby( + ["id_linea", "dia", "hora"], as_index=False + ).mean() print("Subiendo datos a la DB") - cols = [ - "id_linea", - "dia", - "hora", - "servicios" - ] + cols = ["id_linea", "dia", "hora", "servicios"] type_of_day_stats = type_of_day_stats.reindex(columns=cols) @@ -1682,3 +1779,165 @@ def compute_dispatched_services_by_line_hour_typeday(): print("Correr la funcion kpi.compute_services_by_line_hour_day()") return type_of_day_stats + + +def upload_route_section_points_table(route_geoms, delete_old_data=False): + """ + Uploads a table with route section points from a route geom row + and returns a table with line_id, number of sections and the + xy point for that section + + Parameters + ---------- + row : GeoPandas GeoDataFrame + routes geom GeoDataFrame with geometry, n_sections and line id + + """ + conn_insumos = iniciar_conexion_db(tipo="insumos") + + # delete old records + if delete_old_data: + delete_old_routes_section_id_coords_data_q(route_geoms) + + print("Creando tabla de secciones de recorrido") + route_section_points = pd.concat( + [create_route_section_points(row) for _, row in route_geoms.iterrows()] + ) + + route_section_points.to_sql( + "routes_section_id_coords", + conn_insumos, + if_exists="append", + index=False, + ) + print("Fin creacion de tabla de secciones de recorrido") + conn_insumos.close() + + +def create_route_section_points(row): + """ + Creates a table with route section points from a route geom row + and returns a table with line_id, number of sections and the + xy point for that section + + Parameters + ---------- + row : GeoPandas GeoSeries + Row from route geom GeoDataFrame + with geometry, n_sections and line id + + Returns + ---------- + pandas.DataFrame + dataFrame with line id, number of sections and the + latlong for each section id + """ + + n_sections = row.n_sections + route_geom = row.geometry + line_id = row.id_linea + sections_lrs = create_route_section_ids(n_sections) + sections_id = list(range(1, len(sections_lrs))) + [-1] + points = route_geom.interpolate(sections_lrs, normalized=True) + route_section_points = pd.DataFrame( + { + "id_linea": [line_id] * len(sections_id), + "n_sections": [n_sections] * len(sections_id), + "section_id": sections_id, + "section_lrs": sections_lrs, + "x": points.map(lambda p: p.x), + "y": points.map(lambda p: p.y), + } + ) + return route_section_points + + +def delete_old_routes_section_id_coords_data_q(route_geoms): + """ + Deletes old data in table routes_section_id_coords + """ + conn_insumos = iniciar_conexion_db(tipo="insumos") + + # create a df with n sections for each line + delete_df = route_geoms.reindex(columns=["id_linea", "n_sections"]) + for _, row in delete_df.iterrows(): + # Delete old data for those parameters + + q_delete = f""" + delete from routes_section_id_coords + where id_linea = {row.id_linea} + and n_sections = {row.n_sections} + """ + + cur = conn_insumos.cursor() + cur.execute(q_delete) + conn_insumos.commit() + + conn_insumos.close() + print("Fin borrado datos previos") + + +def read_legs_data_by_line_hours_and_day(line_ids_where, hour_range, day_type): + """ + Reads legs data by line id, hour range and type of day + + Parameters + ---------- + line_ids_where : str + where clause in a sql query with line ids . + hour_range : tuple or bool + tuple holding hourly range (from,to) and from 0 to 24. Route section + load will be computed for legs happening within tat time range. + If False it won't filter by hour. + day_type: str + type of day on which the section load is to be computed. It can take + `weekday`, `weekend` or a specific day in format 'YYYY-MM-DD' + + Returns + ------- + legs : pandas.DataFrame + dataframe with legs data by line id, hour range and type of day + + """ + + # Read legs data by line id, hours, day type + # + q_main_legs = """ + select id_linea, dia, factor_expansion_linea,h3_o,h3_d, od_validado + from etapas + """ + q_main_legs = q_main_legs + line_ids_where + + if hour_range: + hour_range_where = f" and hora >= {hour_range[0]} and hora <= {hour_range[1]}" + q_main_legs = q_main_legs + hour_range_where + + day_type_is_a_date = is_date_string(day_type) + + if day_type_is_a_date: + q_main_legs = q_main_legs + f" and dia = '{day_type}'" + + q_legs = f""" + select id_linea, dia, factor_expansion_linea,h3_o,h3_d + from ({q_main_legs}) e + where e.od_validado==1 + """ + print("Obteniendo datos de etapas") + + # get data for legs and route geoms + conn_data = iniciar_conexion_db(tipo="data") + legs = pd.read_sql(q_legs, conn_data) + conn_data.close() + + legs["yr_mo"] = legs.dia.str[:7] + + if not day_type_is_a_date: + # create a weekday_filter + weekday_filter = pd.to_datetime(legs.dia, format="%Y-%m-%d").dt.dayofweek < 5 + + if day_type == "weekday": + legs = legs.loc[weekday_filter, :] + else: + legs = legs.loc[~weekday_filter, :] + + return legs diff --git a/urbantrips/kpi/line_od_matrix.py b/urbantrips/kpi/line_od_matrix.py new file mode 100644 index 0000000..e3ed7dc --- /dev/null +++ b/urbantrips/kpi/line_od_matrix.py @@ -0,0 +1,335 @@ +import os +import warnings +import pandas as pd +import geopandas as gpd +from urbantrips.utils.utils import ( + duracion, + iniciar_conexion_db, + check_date_type, + is_date_string, + create_line_ids_sql_filter, + leer_alias +) +from urbantrips.kpi.kpi import ( + create_route_section_ids, + add_od_lrs_to_legs_from_route, + upload_route_section_points_table, + read_legs_data_by_line_hours_and_day, + check_exists_route_section_points_table +) +from urbantrips.geo import geo + + +@duracion +def compute_lines_od_matrix( + line_ids=None, + hour_range=False, + n_sections=10, + section_meters=None, + day_type="weekday", + save_csv=False +): + """ + Computes leg od matrix for a line or set of lines using route sections + + Parameters + ---------- + line_ids : int, list of ints or bool + route id or list of route ids present in the legs dataset. Route + section load will be computed for that subset of lines. If False, it + will run with all routes. + hour_range : tuple or bool + tuple holding hourly range (from,to) and from 0 to 24. Route section + load will be computed for legs happening within tat time range. + If False it won't filter by hour. + n_sections: int + number of sections to split the route geom + section_meters: int + section lenght in meters to split the route geom. If specified, + this will be used instead of n_sections. + day_type: str + type of day on which the section load is to be computed. It can take + `weekday`, `weekend` or a specific day in format 'YYYY-MM-DD' + save_csv: bool + If a csv file should be saved in results directory + """ + + # check inputs + check_date_type(day_type) + + line_ids_where = create_line_ids_sql_filter(line_ids) + + if n_sections is not None: + if n_sections > 1000: + raise Exception( + "No se puede utilizar una cantidad de secciones > 1000") + + conn_data = iniciar_conexion_db(tipo="data") + conn_insumos = iniciar_conexion_db(tipo="insumos") + + # read legs data + legs = read_legs_data_by_line_hours_and_day( + line_ids_where, hour_range, day_type) + + # read routes data + q_route_geoms = "select * from lines_geoms" + q_route_geoms = q_route_geoms + line_ids_where + route_geoms = pd.read_sql(q_route_geoms, conn_insumos) + route_geoms["geometry"] = gpd.GeoSeries.from_wkt(route_geoms.wkt) + route_geoms = gpd.GeoDataFrame( + route_geoms, geometry="geometry", crs="EPSG:4326" + ) + # Set which parameter to use to slit route geoms + if section_meters: + epsg_m = geo.get_epsg_m() + # project geoms and get for each geom a n_section + route_geoms = route_geoms.to_crs(epsg=epsg_m) + new_n_sections = ( + route_geoms.geometry.length / section_meters).astype(int) + route_geoms["n_sections"] = new_n_sections + + if (route_geoms.n_sections > 1000).any(): + warnings.warn( + "Algunos recorridos tienen mas de 1000 segmentos" + "Puede arrojar resultados imprecisos " + ) + n_sections = new_n_sections + route_geoms = route_geoms.to_crs(epsg=4326) + + route_geoms["n_sections"] = n_sections + + # check which section geoms are already crated + new_route_geoms = check_exists_route_section_points_table(route_geoms) + + # create the line and n sections pair missing and upload it to the db + if len(new_route_geoms) > 0: + + upload_route_section_points_table( + new_route_geoms, delete_old_data=False) + + # delete old data + yr_mos = legs.yr_mo.unique() + + delete_old_lines_od_matrix_by_section_data( + route_geoms, hour_range, day_type, yr_mos) + + print("Calculando matriz od de lineas ...") + + if (len(route_geoms) > 0) and (len(legs) > 0): + + line_od_matrix = legs.groupby(["id_linea", "yr_mo"]).apply( + compute_line_od_matrix, + route_geoms=route_geoms, + hour_range=hour_range, + day_type=day_type, + save_csv=save_csv + ) + + line_od_matrix = line_od_matrix.droplevel( + 2, axis=0).reset_index() + + line_od_matrix = line_od_matrix.reindex( + columns=[ + "id_linea", + "yr_mo", + "day_type", + "n_sections", + "hour_min", + "hour_max", + "section_id_o", + "section_id_d", + "legs", + "prop" + ] + ) + + print("Uploading data to db...") + line_od_matrix.to_sql( + "lines_od_matrix_by_section", conn_data, if_exists="append", + index=False,) + + return line_od_matrix + + else: + print('No existen recorridos o etapas para las líneas') + print("Cantidad de lineas:", len(line_ids)) + print("Cantidad de recorridos", len(route_geoms)) + print("Cantidad de etapas", len(legs)) + + +def delete_old_lines_od_matrix_by_section_data( + route_geoms, hour_range, day_type, yr_mos, db_type='data' +): + """ + Deletes old data in table lines_od_matrix_by_section + """ + if db_type == 'data': + conn = iniciar_conexion_db(tipo="data") + table_name = 'lines_od_matrix_by_section' + else: + conn = iniciar_conexion_db(tipo="dash") + table_name = 'matrices_linea' + # hour range filter + if hour_range: + hora_min_filter = f"= {hour_range[0]}" + hora_max_filter = f"= {hour_range[1]}" + else: + hora_min_filter = "is NULL" + hora_max_filter = "is NULL" + + # create a df with n sections for each line + delete_df = route_geoms.reindex(columns=['id_linea', 'n_sections']) + for yr_mo in yr_mos: + for _, row in delete_df.iterrows(): + # Delete old data for those parameters + print("Borrando datos antiguos de Matriz OD para linea") + print(row.id_linea) + print(f"{row.n_sections} secciones") + print(yr_mo) + if hour_range: + print(f"y horas desde {hour_range[0]} a {hour_range[1]}") + + q_delete = f""" + delete from {table_name} + where id_linea = {row.id_linea} + and hour_min {hora_min_filter} + and hour_max {hora_max_filter} + and day_type = '{day_type}' + and n_sections = {row.n_sections} + and yr_mo = '{yr_mo}'; + """ + + cur = conn.cursor() + cur.execute(q_delete) + conn.commit() + + conn.close() + print("Fin borrado datos previos") + + +def compute_line_od_matrix(df, route_geoms, hour_range, day_type, save_csv=False): + """ + Computes leg od matrix for a line or set of lines using route sections + + Parameters + ---------- + df : pandas DataFrame + A legs DataFrame with OD data + route_geoms: geopandas GeoDataFrame + A routes GeoDataFrame with routes and number of sections to slice it + hour_range : tuple or bool + tuple holding hourly range (from,to) and from 0 to 24. Route section + load will be computed for legs happening within tat time range. + If False it won't filter by hour. + day_type: str + type of day on which the section load is to be computed. It can take + `weekday`, `weekend` or a specific day in format 'YYYY-MM-DD' + save_csv: bool + If a csv file should be saved in results directory + + Returns + ---------- + pandas DataFrame + A OD matrix with OD by route section id, number of legs for that + pair and percentaje of legs for that hour range + + """ + + line_id = df.id_linea.unique()[0] + mes = df.yr_mo.unique()[0] + + print(f"Calculando matriz od linea id {line_id}") + + if (route_geoms.id_linea == line_id).any(): + + n_sections = route_geoms.loc[route_geoms.id_linea == + line_id, 'n_sections'].item() + + route_geom = route_geoms.loc[route_geoms.id_linea == + line_id, "geometry"].item() + + df = add_od_lrs_to_legs_from_route(legs_df=df, route_geom=route_geom) + + # Assign a direction based on line progression + df = df.reindex( + columns=["dia", "o_proj", "d_proj", "factor_expansion_linea"]) + + # Compute total legs per direction + # First totals per day + totals_by_day = df\ + .groupby(["dia"], as_index=False)\ + .agg(daily_legs=("factor_expansion_linea", "sum")) + + totals_by_typeday = totals_by_day.daily_legs.mean() + + # round section ids + section_ids = create_route_section_ids(n_sections) + labels = list(range(1, len(section_ids))) + + df['o_proj'] = pd.cut(df.o_proj, bins=section_ids, + labels=labels, right=True) + df['d_proj'] = pd.cut(df.d_proj, bins=section_ids, + labels=labels, right=True) + + totals_by_day_section_id = df\ + .groupby(["dia", "o_proj", "d_proj"])\ + .agg(legs=("factor_expansion_linea", "sum"))\ + .reset_index() + + # then average for type of day + totals_by_typeday_section_id = totals_by_day_section_id\ + .groupby(["o_proj", "d_proj"])\ + .agg(legs=("legs", "mean"))\ + .reset_index() + totals_by_typeday_section_id['legs'] = totals_by_typeday_section_id['legs'].round( + ).map(int) + totals_by_typeday_section_id['prop'] = ( + totals_by_typeday_section_id.legs / totals_by_typeday * 100 + ).round(1) + totals_by_typeday_section_id["day_type"] = day_type + totals_by_typeday_section_id["n_sections"] = n_sections + + # Add hourly range + if hour_range: + totals_by_typeday_section_id["hour_min"] = hour_range[0] + totals_by_typeday_section_id["hour_max"] = hour_range[1] + else: + totals_by_typeday_section_id["hour_min"] = None + totals_by_typeday_section_id["hour_max"] = None + + totals_by_typeday_section_id = totals_by_typeday_section_id.rename( + columns={'o_proj': 'section_id_o', + 'd_proj': 'section_id_d'}) + totals_by_typeday_section_id = totals_by_typeday_section_id.reindex( + columns=[ + 'day_type', 'n_sections', + 'hour_min', 'hour_max', 'section_id_o', 'section_id_d', 'legs', 'prop' + ] + ) + + if save_csv: + alias = leer_alias() + day = totals_by_typeday_section_id['day_type'].unique().item() + + if day == 'weekend': + day_str = 'Fin de semana' + elif day == 'weekday': + day_str = 'Dia habil' + else: + day_str = day + if not totals_by_typeday_section_id.hour_min.isna().all(): + from_hr = totals_by_typeday_section_id.hour_min.unique()[0] + to_hr = totals_by_typeday_section_id.hour_max.unique()[0] + hr_str = f' {from_hr}-{to_hr} hrs' + else: + hr_str = '' + + archivo = f"{alias}({mes}_{day_str})_matriz_od_id_linea_" + archivo = archivo+f"{line_id}_{hr_str}.csv" + path = os.path.join("resultados", "matrices", archivo) + totals_by_typeday_section_id.to_csv(path, index=False) + else: + print("No existe recorrido para id_linea:", line_id) + totals_by_typeday_section_id = None + + return totals_by_typeday_section_id diff --git a/urbantrips/lineas_deseo/lineas_deseo.py b/urbantrips/lineas_deseo/lineas_deseo.py new file mode 100644 index 0000000..aacfef6 --- /dev/null +++ b/urbantrips/lineas_deseo/lineas_deseo.py @@ -0,0 +1,1240 @@ +import pandas as pd +import geopandas as gpd +import numpy as np +from urbantrips.utils.utils import iniciar_conexion_db, levanto_tabla_sql +from urbantrips.geo.geo import normalizo_lat_lon +from urbantrips.utils.utils import traigo_tabla_zonas, calculate_weighted_means +from urbantrips.geo.geo import h3_to_lat_lon, h3toparent, h3_to_geodataframe, point_to_h3, create_h3_gdf +from urbantrips.utils.check_configs import check_config +from shapely.geometry import Point + + +def load_and_process_data(): + """ + Load and process data from databases, returning the etapas and viajes DataFrames. + + Returns: + etapas (DataFrame): Processed DataFrame containing stage data. + viajes (DataFrame): Processed DataFrame containing journey data. + """ + + # Establish connections to different databases for input data and operational data + conn_insumos = iniciar_conexion_db(tipo='insumos') + conn_data = iniciar_conexion_db(tipo='data') + + # Load distance data from 'distancias' table in 'insumos' database + q_distancias = """ + SELECT + h3_o, h3_d, distance_osm_drive, distance_osm_walk, distance_h3 + FROM + distancias + """ + distancias = pd.read_sql_query(q_distancias, conn_insumos) + + # Load stage data from 'etapas' table in 'data' database and merge with distance data + etapas = pd.read_sql_query("SELECT * FROM etapas where od_validado==1", conn_data) + etapas = etapas.merge(distancias, how='left') + + # Load journey data from 'viajes' table in 'data' database and merge with distance data + viajes = pd.read_sql_query("SELECT * FROM viajes where od_validado==1", conn_data) + viajes = viajes.merge(distancias, how='left') + + # Load travel times from gps and stations + travel_times = pd.read_sql_query("SELECT * FROM travel_times_gps", conn_data) + travel_times_stations = pd.read_sql_query("SELECT * FROM travel_times_stations", conn_data) + + travel_times = pd.concat([travel_times, travel_times_stations], ignore_index=True) + + if len(travel_times) > 0: + etapas = etapas.merge(travel_times, how='left', on=['dia', 'id']) + + viajes = viajes.merge(etapas.groupby(['dia', 'id_tarjeta', 'id_viaje'], as_index=False).travel_time_min.sum().round(2), + how='left', + on=['dia', 'id_tarjeta', 'id_viaje']) + + viajes['travel_speed'] = ( + viajes['distance_osm_drive'] / + (viajes['travel_time_min']/60) + ).round(1) + + viajes.loc[(viajes.travel_speed==np.inf)|(viajes.travel_speed>=50), 'travel_speed'] = np.nan + else: + etapas['travel_time_min'] = np.nan + etapas['travel_speed'] = np.nan + viajes['travel_time_min'] = np.nan + viajes['travel_speed'] = np.nan + + viajes['transferencia'] = 0 + viajes.loc[viajes.cant_etapas > 1, 'transferencia'] = 1 + viajes['rango_hora'] = '0-12' + viajes.loc[(viajes.hora >= 13) & ( + viajes.hora <= 16), 'rango_hora'] = '13-16' + viajes.loc[(viajes.hora >= 17) & ( + viajes.hora <= 24), 'rango_hora'] = '17-24' + + viajes['distancia'] = 'Viajes cortos (<=5kms)' + viajes.loc[(viajes.distance_osm_drive > 5), + 'distancia'] = 'Viajes largos (>5kms)' + + viajes['tipo_dia_'] = pd.to_datetime(viajes.dia).dt.weekday.astype(str).copy() + viajes['tipo_dia'] = 'Hábil' + viajes.loc[viajes.tipo_dia_.astype(int)>=5, 'tipo_dia'] = 'Fin de Semana' + viajes = viajes.drop(['tipo_dia_'], axis=1) + + viajes['mes'] = viajes.dia.str[:7] + + viajes['Fecha'] = viajes['dia'] + ' ' +viajes['tiempo'] + viajes['Fecha'] = pd.to_datetime(viajes['Fecha']) + viajes['Fecha_next'] = viajes.groupby(['dia', 'id_tarjeta'])['Fecha'].shift(-1) + viajes['diff_time'] = viajes['Fecha_next'] - viajes['Fecha'] + viajes['diff_time'] = (viajes.diff_time.dt.seconds / 60).round() + + + etapas['tipo_dia_'] = pd.to_datetime(etapas.dia).dt.weekday.astype(str).copy() + etapas['tipo_dia'] = 'Hábil' + etapas.loc[etapas.tipo_dia_.astype(int)>=5, 'tipo_dia'] = 'Fin de Semana' + etapas = etapas.drop(['tipo_dia_'], axis=1) + etapas['mes'] = etapas.dia.str[:7] + + return etapas, viajes + + +def format_values(row): + if row['type_val'] == 'int': + return f"{int(row['Valor']):,}".replace(',', '.') + elif row['type_val'] == 'float': + return f"{row['Valor']:,.2f}".replace(',', 'X').replace('.', ',').replace('X', '.') + elif row['type_val'] == 'percentage': + return f"{row['Valor']:.2f}%".replace('.', ',') + else: + return str(row['Valor']) + + +def format_dataframe(df): + df['Valor_str'] = df.apply(format_values, axis=1) + return df + + +def construyo_indicadores(viajes): + + if 'id_polygon' not in viajes.columns: + viajes['id_polygon'] = 'NONE' + + + ind1 = viajes.groupby(['id_polygon', 'dia', 'mes'], as_index=False).factor_expansion_linea.sum( + ).round(0).rename(columns={'factor_expansion_linea': 'Valor'}).groupby(['id_polygon', 'mes'], as_index=False).Valor.mean().round() + ind1['Indicador'] = 'Cantidad de Viajes' + ind1['Valor'] = ind1.Valor.astype(int) + ind1['Tipo'] = 'General' + ind1['type_val'] = 'int' + + ind2 = viajes[viajes.transferencia == 1].groupby(['id_polygon', 'dia', 'mes'], as_index=False).factor_expansion_linea.sum( + ).round(0).rename(columns={'factor_expansion_linea': 'Valor'}).groupby(['id_polygon', 'mes'], as_index=False).Valor.mean().round() + ind2['Indicador'] = 'Cantidad de Viajes con Transferencia' + ind2 = ind2.merge(ind1[['id_polygon', 'mes', 'Valor']].rename( + columns={'Valor': 'Tot'}), how='left') + ind2['Valor'] = (ind2['Valor'] / ind2['Tot'] * 100).round(2) + ind2['Tipo'] = 'General' + ind2['type_val'] = 'percentage' + + ind3 = viajes.groupby(['id_polygon', 'dia', 'mes', 'rango_hora'], as_index=False).factor_expansion_linea.sum( + ).round(0).rename(columns={'factor_expansion_linea': 'Valor'}).groupby(['id_polygon', 'mes', 'rango_hora'], as_index=False).Valor.mean().round() + ind3['Indicador'] = 'Cantidad de Según Rango Horas' + ind3['Tot'] = ind3.groupby(['id_polygon', 'mes']).Valor.transform('sum') + ind3['Valor'] = (ind3['Valor'] / ind3['Tot'] * 100).round(2) + ind3['Indicador'] = 'Cantidad de Viajes de '+ind3['rango_hora']+'hs' + ind3['Tipo'] = 'General' + ind3['type_val'] = 'percentage' + + ind4 = viajes.groupby(['id_polygon', 'dia', 'mes', 'modo'], as_index=False).factor_expansion_linea.sum( + ).round(0).rename(columns={'factor_expansion_linea': 'Valor'}).groupby(['id_polygon', 'mes', 'modo'], as_index=False).Valor.mean().round() + ind4['Indicador'] = 'Partición Modal' + ind4['Tot'] = ind4.groupby(['id_polygon', 'mes']).Valor.transform('sum') + ind4['Valor'] = (ind4['Valor'] / ind4['Tot'] * 100).round(2) + ind4 = ind4.sort_values(['id_polygon', 'Valor'], ascending=False) + ind4['Indicador'] = ind4['modo'] + ind4['Tipo'] = 'Modal' + ind4['type_val'] = 'percentage' + + ind9 = viajes.groupby(['id_polygon', 'dia', 'mes', 'distancia'], as_index=False).factor_expansion_linea.sum( + ).round(0).rename(columns={'factor_expansion_linea': 'Valor'}).groupby(['id_polygon', 'mes', 'distancia'], as_index=False).Valor.mean().round() + ind9['Indicador'] = 'Partición Modal' + ind9['Tot'] = ind9.groupby(['id_polygon', 'mes']).Valor.transform('sum') + ind9['Valor'] = (ind9['Valor'] / ind9['Tot'] * 100).round(2) + ind9 = ind9.sort_values(['id_polygon', 'Valor'], ascending=False) + ind9['Indicador'] = 'Cantidad de '+ind9['distancia'] + ind9['Tipo'] = 'General' + ind9['type_val'] = 'percentage' + + ind5 = viajes.groupby(['id_polygon', 'dia', 'mes', 'id_tarjeta'], + as_index=False).factor_expansion_linea.first().groupby(['id_polygon', 'dia', 'mes'], + as_index=False).factor_expansion_linea.sum().groupby(['id_polygon', 'mes'], + as_index=False).factor_expansion_linea.mean().round().rename(columns={'factor_expansion_linea': 'Valor'}) + ind5['Indicador'] = 'Cantidad de Usuarios' + ind5['Tipo'] = 'General' + ind5['type_val'] = 'int' + + ind6 = calculate_weighted_means(viajes, + aggregate_cols=['id_polygon', 'dia', 'mes'], + weighted_mean_cols=['distance_osm_drive'], + weight_col='factor_expansion_linea').rename(columns={'distance_osm_drive': 'Valor'}).groupby(['id_polygon', 'mes'], as_index=False).Valor.mean().round(2) + ind6['Tipo'] = 'Distancias' + ind6['Indicador'] = 'Distancia Promedio (kms)' + ind6['type_val'] = 'float' + + ind7 = calculate_weighted_means(viajes, + aggregate_cols=['id_polygon', 'dia', 'mes', 'modo'], + weighted_mean_cols=['distance_osm_drive'], + weight_col='factor_expansion_linea').rename(columns={'distance_osm_drive': 'Valor'}).groupby(['id_polygon', 'mes', 'modo'], as_index=False).Valor.mean().round(2) + ind7['Tipo'] = 'Distancias' + ind7['Indicador'] = 'Distancia Promedio (' + ind7.modo + ') (kms)' + ind7['type_val'] = 'float' + + ind8 = calculate_weighted_means(viajes, + aggregate_cols=['id_polygon', 'dia', 'mes', 'distancia'], + weighted_mean_cols=['distance_osm_drive'], + weight_col='factor_expansion_linea').rename(columns={'distance_osm_drive': 'Valor'}).groupby(['id_polygon', 'mes', 'distancia'], as_index=False).Valor.mean().round(2) + ind8['Tipo'] = 'Distancias' + ind8['Indicador'] = 'Distancia Promedio ' + ind8.distancia + ind8['type_val'] = 'float' + + indicadores = pd.concat( + [ind1, ind5, ind2, ind3, ind6, ind9, ind7, ind8, ind4]) + + indicadores_todos = indicadores.groupby(['id_polygon', 'Tipo', 'Indicador', 'type_val'], as_index=False).Valor.mean().round(2) + indicadores_todos['mes'] = 'Todos' + indicadores = pd.concat([indicadores, indicadores_todos]) + + indicadores = format_dataframe(indicadores) + indicadores = indicadores[['id_polygon', 'mes', 'Tipo', 'Indicador', 'Valor_str']].rename( + columns={'Valor_str': 'Valor'}) + + indicadores = indicadores.sort_values(['id_polygon', 'mes', 'Tipo', 'Indicador']) + + return indicadores + + +def select_h3_from_polygon(poly, res=8, spacing=.0001, viz=False): + """ + Fill a polygon with points spaced at the given distance apart. + Create hexagons that correspond to the polygon + """ + + if 'id' not in poly.columns: + poly = poly.reset_index().rename(columns={'index': 'id'}) + + points_result = pd.DataFrame([]) + poly = poly.reset_index(drop=True).to_crs(4326) + for i, row in poly.iterrows(): + + polygon = poly.geometry[i] + + # Get the bounding box coordinates + minx, miny, maxx, maxy = polygon.buffer(.008).bounds + + # Create a meshgrid of x and y values based on the spacing + x_coords = list(np.arange(minx, maxx, spacing)) + y_coords = list(np.arange(miny, maxy, spacing)) + + points = [] + for x in x_coords: + for y in y_coords: + point = Point(x, y) + # if polygon.contains(point): + points.append(point) + + points = gpd.GeoDataFrame(geometry=points, crs=4326) + points['polygon_number'] = row.id + points_result = pd.concat([points_result, points]) + + points_result = gpd.sjoin(points_result, poly) + + points_result['h3'] = points_result.apply( + point_to_h3, axis=1, resolution=res) + + points_result = points_result.groupby(['polygon_number', 'h3'], as_index=False).size( + ).drop(['size'], axis=1).rename(columns={'h3_index': 'h3'}) + + gdf_hexs = h3_to_geodataframe(points_result.h3).rename( + columns={'h3_index': 'h3'}) + gdf_hexs = gdf_hexs.merge(points_result, on='h3')[['polygon_number', 'h3', 'geometry']].sort_values( + ['polygon_number', 'h3']).reset_index(drop=True) + + if viz: + ax = poly.boundary.plot(linewidth=1.5, figsize=(15, 15)) + # gdf_points.plot(ax=ax, alpha=.2) + gdf_hexs.plot(ax=ax, alpha=.6) + + return gdf_hexs.rename(columns={'polygon_number': 'id'}) + + +def select_cases_from_polygons(etapas, viajes, polygons, res=8): + ''' + Dado un dataframe de polígonos, selecciona los casos de etapas y viajes que se encuentran dentro del polígono + ''' + print('Selecciona casos dentro de polígonos') + etapas_selec = pd.DataFrame([]) + viajes_selec = pd.DataFrame([]) + gdf_hexs_all = pd.DataFrame([]) + + for i, row in polygons.iterrows(): + + poly = polygons[polygons.id == row.id].copy() + + gdf_hexs = select_h3_from_polygon(poly, + res=res, + viz=False) + + gdf_hexs = gdf_hexs[['id', 'h3']].rename( + columns={'h3': 'h3_o', 'id': 'id_polygon'}) + + seleccionar = etapas.merge(gdf_hexs, on='h3_o')[ + ['dia', 'id_tarjeta', 'id_viaje', 'id_polygon']].drop_duplicates() + + tmp = etapas.merge(seleccionar) + + etapas_selec = pd.concat([etapas_selec, + tmp], ignore_index=True) + + tmp = viajes.merge(seleccionar) + viajes_selec = pd.concat([viajes_selec, + tmp], ignore_index=True) + + gdf_hexs['polygon_lon'] = poly.representative_point().x.values[0] + gdf_hexs['polygon_lat'] = poly.representative_point().y.values[0] + + gdf_hexs_all = pd.concat([gdf_hexs_all, gdf_hexs], + ignore_index=True) + + return etapas_selec, viajes_selec, polygons, gdf_hexs_all + + +def agrupar_viajes(etapas_agrupadas, + aggregate_cols, + weighted_mean_cols, + weight_col, + zero_to_nan, + agg_transferencias=False, + agg_modo=False, + agg_hora=False, + agg_distancia=False, + agg_genero=False, + agg_tarifa=False): + + etapas_agrupadas_zon = etapas_agrupadas.copy() + + if agg_transferencias: + etapas_agrupadas_zon['transferencia'] = 99 + if agg_modo: + etapas_agrupadas_zon['modo_agregado'] = 99 + if agg_hora: + etapas_agrupadas_zon['rango_hora'] = 99 + if agg_distancia: + etapas_agrupadas_zon['distancia'] = 99 + if agg_genero: + etapas_agrupadas_zon['genero'] = 99 + if agg_tarifa: + etapas_agrupadas_zon['tarifa'] = 99 + + etapas_agrupadas_zon = calculate_weighted_means(etapas_agrupadas_zon, + aggregate_cols=aggregate_cols, + weighted_mean_cols=weighted_mean_cols, + weight_col=weight_col, + zero_to_nan=zero_to_nan) + + return etapas_agrupadas_zon + + +def construyo_matrices(etapas_desagrupadas, + aggregate_cols, + zonificaciones, + agg_transferencias=False, + agg_modo=False, + agg_hora=False, + agg_distancia=False, + agg_genero=False, + agg_tarifa=False + ): + + matriz = etapas_desagrupadas.copy() + + if agg_transferencias: + matriz['transferencia'] = 99 + if agg_modo: + matriz['modo_agregado'] = 99 + if agg_hora: + matriz['rango_hora'] = 99 + if agg_distancia: + matriz['distancia'] = 99 + if agg_genero: + matriz['genero'] = 99 + if agg_tarifa: + matriz['tarifa'] = 99 + + matriz = calculate_weighted_means(matriz, + aggregate_cols=aggregate_cols, + weighted_mean_cols=[ + 'lat1', 'lon1', 'lat4', 'lon4', 'distance_osm_drive', 'travel_time_min', 'travel_speed'], + weight_col='factor_expansion_linea', + zero_to_nan=[ + 'lat1', 'lon1', 'lat4', 'lon4'], + ) + + zonificaciones['orden'] = zonificaciones['orden'].fillna(0) + matriz = matriz.merge( + zonificaciones[['zona', 'id', 'orden']].rename( + columns={'id': 'inicio', 'orden': 'orden_origen'}), + on=['zona', 'inicio']) + + matriz = matriz.merge( + zonificaciones[['zona', 'id', 'orden']].rename( + columns={'id': 'fin', 'orden': 'orden_destino'}), + on=['zona', 'fin']) + + matriz['Origen'] = matriz.orden_origen.astype( + int).astype(str).str.zfill(3)+'_'+matriz.inicio + matriz['Destino'] = matriz.orden_destino.astype( + int).astype(str).str.zfill(3)+'_'+matriz.fin + + return matriz + + +def creo_h3_equivalencias(polygons_h3, polygon, res, zonificaciones): + + poly_sel = h3_to_geodataframe(polygons_h3, 'h3_o') + poly_sel_all = pd.DataFrame([]) + + if 'res_' in res: + # for i in res: + if True: + resol = int(res.replace('res_', '')) + i = f'res_{resol}' + poly_sel = poly_sel[['h3_o', 'geometry']].copy() + poly_sel[f'zona_{i}'] = poly_sel['h3_o'].apply( + h3toparent, res=resol) + poly_2 = h3_to_geodataframe(poly_sel, f'zona_{i}') + poly_ovl = gpd.overlay( + poly_sel[['h3_o', 'geometry']], poly_2, how='intersection', keep_geom_type=False) + poly_ovl = poly_ovl.dissolve(by=f'zona_{i}', as_index=False) + poly_ovl = gpd.overlay( + poly_ovl, polygon[['geometry']], how='intersection', keep_geom_type=False) + poly_ovl[f'lat_res_{resol}'] = poly_ovl.geometry.to_crs( + 4326).representative_point().y + poly_ovl[f'lon_res_{resol}'] = poly_ovl.geometry.to_crs( + 4326).representative_point().x + poly_ovl = poly_ovl.drop(['geometry', 'h3_o'], axis=1) + poly_sel = poly_sel.merge(poly_ovl, on=f'zona_{i}', how='left') + + if len(poly_sel_all) == 0: + poly_sel_all = poly_sel.copy() + else: + poly_sel = poly_sel.drop(['geometry'], axis=1) + poly_sel_all = poly_sel_all.merge(poly_sel, on='h3_o') + + else: + + poly_sel = h3_to_geodataframe(polygons_h3, 'h3_o') + for zonas in zonificaciones.zona.unique(): + zona = zonificaciones[zonificaciones.zona == zonas] + poly_ovl = gpd.overlay( + poly_sel[['h3_o', 'geometry']], zona, how='intersection', keep_geom_type=False) + poly_ovl_agg = poly_ovl.dissolve(by='id', as_index=False) + poly_ovl_agg = gpd.overlay( + poly_ovl_agg, polygon[['geometry']], how='intersection', keep_geom_type=False) + + poly_ovl_agg[f'lat_{zonas}'] = poly_ovl.geometry.to_crs( + 4326).representative_point().y + poly_ovl_agg[f'lon_{zonas}'] = poly_ovl.geometry.to_crs( + 4326).representative_point().x + poly_ovl_agg[f'zona_{zonas}'] = poly_ovl_agg.id + + poly_ovl_agg['geometry'] = poly_ovl_agg.geometry.representative_point() + + poly_ovl_agg[f'lat_{zonas}'] = poly_ovl_agg.geometry.y + poly_ovl_agg[f'lon_{zonas}'] = poly_ovl_agg.geometry.x + poly_ovl_agg[f'zona_{zonas}'] = poly_ovl_agg.id + + poly_ovl = poly_ovl.merge(poly_ovl_agg[['id', f'zona_{zonas}', f'lat_{zonas}', f'lon_{zonas}']], + on=f'id', + how='left') + + if len(poly_sel_all) == 0: + poly_sel_all = poly_ovl.copy() + else: + + poly_sel_all = poly_sel_all.merge( + poly_ovl[['h3_o', f'zona_{zonas}', f'lat_{zonas}', f'lon_{zonas}']], on='h3_o', how='left') + + return poly_sel_all + + +def determinar_modo_agregado(grupo): + modos_unicos = grupo['modo'].unique() + + if len(modos_unicos) == 1: # Solo un modo en todo el viaje + if len(grupo) > 1: # Más de una etapa + return f"multietapa ({modos_unicos[0]})" + else: + return modos_unicos[0] + else: + return "multimodal" + + +def normalizo_zona(df, zonificaciones): + if len(zonificaciones) > 0: + cols = df.columns + + zonificaciones['latlon'] = zonificaciones.geometry.representative_point().y.astype( + str) + ', '+zonificaciones.geometry.representative_point().x.astype(str) + zonificaciones['aux'] = 1 + + zonificacion_tmp1 = zonificaciones[['id', 'aux', 'geometry']].rename(columns={ + 'id': 'tmp_o'}) + zonificacion_tmp1['geometry'] = zonificacion_tmp1['geometry'].representative_point( + ) + zonificacion_tmp1['h3_o'] = zonificacion_tmp1.apply( + point_to_h3, axis=1, resolution=8) + zonificacion_tmp1['lat_o'] = zonificacion_tmp1.geometry.y + zonificacion_tmp1['lon_o'] = zonificacion_tmp1.geometry.x + zonificacion_tmp1 = zonificacion_tmp1.drop(['geometry'], axis=1) + + zonificacion_tmp2 = zonificaciones[['id', 'aux', 'geometry']].rename(columns={ + 'id': 'tmp_d'}) + zonificacion_tmp2['geometry'] = zonificacion_tmp2['geometry'].representative_point( + ) + zonificacion_tmp2['h3_d'] = zonificacion_tmp2.apply( + point_to_h3, axis=1, resolution=8) + zonificacion_tmp1['lat_d'] = zonificacion_tmp2.geometry.y + zonificacion_tmp1['lon_d'] = zonificacion_tmp2.geometry.x + zonificacion_tmp2 = zonificacion_tmp2.drop(['geometry'], axis=1) + + zonificacion_tmp = zonificacion_tmp1.merge(zonificacion_tmp2, on='aux') + zonificacion_tmp = normalizo_lat_lon( + zonificacion_tmp, h3_o='h3_o', h3_d='h3_d') + zonificacion_tmp = zonificacion_tmp[[ + 'tmp_o', 'tmp_d', 'h3_o', 'h3_d', 'h3_o_norm', 'h3_d_norm']] + zonificacion_tmp1 = zonificacion_tmp[zonificacion_tmp.h3_o == + zonificacion_tmp.h3_o_norm].copy() + zonificacion_tmp1['tmp_o_norm'] = zonificacion_tmp1['tmp_o'] + zonificacion_tmp1['tmp_d_norm'] = zonificacion_tmp1['tmp_d'] + zonificacion_tmp2 = zonificacion_tmp[zonificacion_tmp.h3_o != + zonificacion_tmp.h3_o_norm].copy() + zonificacion_tmp2['tmp_o_norm'] = zonificacion_tmp2['tmp_d'] + zonificacion_tmp2['tmp_d_norm'] = zonificacion_tmp2['tmp_o'] + zonificacion_tmp = pd.concat( + [zonificacion_tmp1, zonificacion_tmp2], ignore_index=True) + zonificacion_tmp = zonificacion_tmp[['tmp_o', 'tmp_d', 'tmp_o_norm', 'tmp_d_norm']].rename(columns={'tmp_o': 'inicio_norm', + 'tmp_d': 'fin_norm'}) + + df = df.merge(zonificacion_tmp, how='left', + on=['inicio_norm', 'fin_norm']) + tmp1 = df[df.inicio_norm == df.tmp_o_norm] + tmp2 = df[df.inicio_norm != df.tmp_o_norm] + tmp2 = tmp2.rename(columns={'inicio_norm': 'fin_norm', + 'fin_norm': 'inicio_norm', + 'poly_inicio_norm': 'poly_fin_norm', + 'poly_fin_norm': 'poly_inicio_norm', + 'lat1_norm': 'lat4_norm', + 'lon1_norm': 'lon4_norm', + 'lat4_norm': 'lat1_norm', + 'lon4_norm': 'lon1_norm', + }) + tmp2_a = tmp2.loc[tmp2.transfer2_norm == ''] + tmp2_b = tmp2.loc[tmp2.transfer2_norm != ''] + tmp2_b = tmp2_b.rename(columns={'transfer1_norm': 'transfer2_norm', + 'transfer2_norm': 'transfer1_norm', + 'poly_transfer1_norm': 'poly_transfer2_norm', + 'poly_transfer2_norm': 'poly_transfer1_norm', + 'lat2_norm': 'lat3_norm', + 'lon2_norm': 'lon3_norm', + 'lat3_norm': 'lat2_norm', + 'lon3_norm': 'lon2_norm', }) + + tmp1 = tmp1[cols] + tmp2_a = tmp2_a[cols] + tmp2_b = tmp2_b[cols] + + df = pd.concat([tmp1, tmp2_a, tmp2_b], ignore_index=True) + return df + + +def crea_socio_indicadores(etapas, viajes): + print('Creo indicadores de género y tarifa') + + socio_indicadores = pd.DataFrame([]) + + viajesx = calculate_weighted_means(viajes, + aggregate_cols=['mes', 'dia', 'tipo_dia', 'genero', 'tarifa'], + weighted_mean_cols=['distance_osm_drive', 'travel_time_min', 'travel_speed', 'cant_etapas', 'diff_time'], + weight_col='factor_expansion_linea', + var_fex_summed=True) + + viajesx = calculate_weighted_means(viajesx, + aggregate_cols=['mes', 'tipo_dia', 'genero', 'tarifa'], + weighted_mean_cols=['distance_osm_drive', 'travel_time_min', 'travel_speed', 'cant_etapas', 'diff_time'], + weight_col='factor_expansion_linea', + var_fex_summed=False).round(3) + + + etapasx = calculate_weighted_means(etapas, + aggregate_cols=['mes', 'dia', 'tipo_dia', 'genero', 'tarifa', 'modo'], + weighted_mean_cols=['distance_osm_drive', 'travel_time_min', 'travel_speed'], + weight_col='factor_expansion_linea', + var_fex_summed=True) + + etapasx = calculate_weighted_means(etapasx, + aggregate_cols=['mes', 'tipo_dia', 'genero', 'tarifa', 'modo'], + weighted_mean_cols=['distance_osm_drive', 'travel_time_min', 'travel_speed'], + weight_col='factor_expansion_linea', + var_fex_summed=False).round(3) + + # calcular tabla de indicadores + etapasxx = calculate_weighted_means(etapasx, + aggregate_cols=['mes', 'tipo_dia', 'genero', 'modo'], + weighted_mean_cols=['distance_osm_drive', 'travel_time_min', 'travel_speed'], + weight_col='factor_expansion_linea', + var_fex_summed=True).round(3) + + etapasxx['tabla'] = 'etapas-genero-modo' + socio_indicadores = pd.concat([socio_indicadores, etapasxx], ignore_index=True) + + etapasxx = calculate_weighted_means(etapasx, + aggregate_cols=['mes', 'tipo_dia', 'tarifa', 'modo'], + weighted_mean_cols=['distance_osm_drive', 'travel_time_min', 'travel_speed'], + weight_col='factor_expansion_linea', + var_fex_summed=True).round(3) + + etapasxx['tabla'] = 'etapas-tarifa-modo' + socio_indicadores = pd.concat([socio_indicadores, etapasxx], ignore_index=True) + + + viajesxx = calculate_weighted_means(viajesx, + aggregate_cols=['mes', 'tipo_dia', 'genero', 'tarifa'], + weighted_mean_cols=['distance_osm_drive', 'travel_time_min', 'travel_speed', 'cant_etapas', 'diff_time'], + weight_col='factor_expansion_linea', + var_fex_summed=True).round(3) + + viajesxx['tabla'] = 'viajes-genero-tarifa' + socio_indicadores = pd.concat([socio_indicadores, viajesxx], ignore_index=True) + + # Calculo viajes promedio por día por género y tarifa + userx = viajes.copy() + userx['tarifa'] = userx['tarifa'].str.replace('-', '') + userx = userx.groupby(['dia', 'id_tarjeta'])['tarifa'].apply(lambda x: '-'.join(x.unique())).reset_index() + userx.loc[userx.tarifa.str[-1] == '-', 'tarifa'] = userx.loc[userx.tarifa.str[-1] == '-', :].tarifa.str[:-1] + userx.loc[userx.tarifa.str[:1] == '-', 'tarifa'] = userx.loc[userx.tarifa.str[:1] == '-', :].tarifa.str[1:] + userx = userx.rename(columns={'tarifa': 'tarifa_agg'}) + userx.loc[userx.tarifa_agg=='', 'tarifa_agg'] = '-' + userx = viajes.merge(userx, how='left') + userx = userx.groupby(['mes', 'dia', 'tipo_dia', 'id_tarjeta', 'genero', 'tarifa_agg'], as_index=False).agg({'factor_expansion_tarjeta':'count', 'factor_expansion_linea':'mean'}).rename(columns={'factor_expansion_tarjeta':'cant_viajes'}).rename(columns={'tarifa_agg':'tarifa'}) + + userx = calculate_weighted_means(userx, + aggregate_cols=['dia', 'mes', 'tipo_dia', 'genero', 'tarifa'], + weighted_mean_cols=['cant_viajes'], + weight_col='factor_expansion_linea', + var_fex_summed=True).round(3) + + userx = calculate_weighted_means(userx, + aggregate_cols=['mes', 'tipo_dia', 'genero', 'tarifa'], + weighted_mean_cols=['cant_viajes'], + weight_col='factor_expansion_linea', + var_fex_summed=False).round(3) + + userx['tabla'] = 'usuario-genero-tarifa' + socio_indicadores = pd.concat([socio_indicadores, userx], ignore_index=True) + + + # Preparo socioindicadores final + socio_indicadores = socio_indicadores[['tabla', 'mes', 'tipo_dia', 'genero', 'tarifa', 'modo', 'distance_osm_drive', 'travel_time_min', 'travel_speed', 'cant_etapas', 'cant_viajes', 'diff_time', 'factor_expansion_linea']] + socio_indicadores.columns = ['tabla', 'mes', 'tipo_dia', 'Genero', 'Tarifa', 'Modo', 'Distancia', 'Tiempo de viaje', 'Velocidad', 'Etapas promedio', 'Viajes promedio', 'Tiempo entre viajes', 'factor_expansion_linea'] + + socio_indicadores['Genero'] = socio_indicadores['Genero'].fillna('') + socio_indicadores['Tarifa'] = socio_indicadores['Tarifa'].fillna('') + socio_indicadores['Modo'] = socio_indicadores['Modo'].fillna('') + + socio_indicadores = socio_indicadores.sort_values(['tabla', 'mes', 'tipo_dia']) + + return socio_indicadores + + +def preparo_lineas_deseo(etapas_selec, viajes_selec, polygons_h3='', poligonos='', res=6): + + print('Preparo líneas de deseo') + zonificaciones = levanto_tabla_sql('zonificaciones') + + if len(polygons_h3) == 0: + id_polygon = 'NONE' + polygons_h3 = pd.DataFrame([['NONE']], columns=['id_polygon']) + poligonos = pd.DataFrame([['NONE', 'NONE']], columns=['id', 'tipo']) + etapas_selec['id_polygon'] = 'NONE' + viajes_selec['id_polygon'] = 'NONE' + + etapas_selec = etapas_selec[etapas_selec.distance_osm_drive.notna()].copy() + etapas_selec = etapas_selec.rename( + columns={'distance_osm_drive': 'distance_osm_drive_etapas'}) + distancias_all = etapas_selec.groupby( + ['id_polygon', 'dia', 'id_tarjeta', 'id_viaje'], as_index=False)[['distance_osm_drive_etapas']].sum() + distancias_all = distancias_all.merge(viajes_selec.loc[viajes_selec.od_validado == 1, [ + 'id_polygon', 'dia', 'id_tarjeta', 'id_viaje', 'distance_osm_drive', 'travel_time_min', 'travel_speed']]) + distancias_all['distancia'] = 'Viajes cortos (<=5kms)' + distancias_all.loc[(distancias_all.distance_osm_drive > 5), + 'distancia'] = 'Viajes largos (>5kms)' + + # Agrupamos por 'dia', 'id_tarjeta' e 'id_viaje' y aplicamos la función para determinar la partición modal + viajes_modo_agg = etapas_selec.groupby(['dia', + 'id_tarjeta', + 'id_viaje']).apply( + determinar_modo_agregado).reset_index(name='modo_agregado').sort_values(['dia', + 'id_tarjeta', + 'id_viaje']).reset_index(drop=True) + + etapas_selec = etapas_selec.merge(viajes_modo_agg, how='left') + + # Traigo zonas + zonas_data, zonas_cols = traigo_tabla_zonas() + + if type(res) == int: + res = [res] + for i in res: + res = [f'res_{i}'] + h3_vals = pd.concat([etapas_selec.loc[etapas_selec.h3_o.notna(), + ['h3_o']].rename(columns={'h3_o': 'h3'}), + etapas_selec.loc[etapas_selec.h3_d.notna(), + ['h3_d']].rename(columns={'h3_d': 'h3'})]).drop_duplicates() + h3_vals['h3_res'] = h3_vals['h3'].apply(h3toparent, res=i) + h3_zona = create_h3_gdf(h3_vals.h3_res.tolist()).rename( + columns={'hexagon_id': 'id'}).drop_duplicates() + h3_zona['zona'] = res[0] + zonificaciones = pd.concat( + [zonificaciones, h3_zona], ignore_index=True) + + resol = res[0] + zonas = res + zonas_cols + + etapas_selec['rango_hora'] = '0-12' + etapas_selec.loc[(etapas_selec.hora >= 13) & ( + etapas_selec.hora <= 16), 'rango_hora'] = '13-16' + etapas_selec.loc[(etapas_selec.hora >= 17) & ( + etapas_selec.hora <= 24), 'rango_hora'] = '17-24' + + etapas_agrupadas_all = pd.DataFrame([]) + gpd_viajes_agrupados_all = pd.DataFrame([]) + + for id_polygon in polygons_h3.id_polygon.unique(): + + poly_h3 = polygons_h3[polygons_h3.id_polygon == id_polygon] + poly = poligonos[poligonos.id == id_polygon] + tipo_poly = poly.tipo.values[0] + print('') + print(f'Polígono {id_polygon} - Tipo: {tipo_poly}') + + # Preparo Etapas con inicio, transferencias y fin del viaje + etapas_all = etapas_selec.loc[(etapas_selec.id_polygon == id_polygon), ['dia', + 'id_tarjeta', + 'id_viaje', + 'id_etapa', + 'h3_o', + 'h3_d', + 'modo_agregado', + 'rango_hora', + 'genero', + 'tarifa', + 'factor_expansion_linea']] + etapas_all['etapa_max'] = etapas_all.groupby( + ['dia', 'id_tarjeta', 'id_viaje']).id_etapa.transform('max') + + # Borro los casos que tienen 3 transferencias o más + if len(etapas_all[etapas_all.etapa_max > 3]) > 0: + nborrar = len(etapas_all[etapas_all.etapa_max > 3][['id_tarjeta', + 'id_viaje']].value_counts()) / len(etapas_all[['id_tarjeta', + 'id_viaje']].value_counts()) * 100 + print( + f'Se van a borrar los viajes que tienen más de 3 etapas, representan el {round(nborrar,2)}% de los viajes para el polígono {id_polygon}') + etapas_all = etapas_all[etapas_all.etapa_max <= 3].copy() + + etapas_all['ultimo_viaje'] = 0 + etapas_all.loc[etapas_all.etapa_max == + etapas_all.id_etapa, 'ultimo_viaje'] = 1 + + ultimo_viaje = etapas_all[etapas_all.ultimo_viaje == 1] + + etapas_all['h3'] = etapas_all['h3_o'] + etapas_all = etapas_all[['dia', + 'id_tarjeta', + 'id_viaje', + 'id_etapa', + 'h3', + 'modo_agregado', + 'rango_hora', + 'genero', + 'tarifa', + 'factor_expansion_linea']] + etapas_all['ultimo_viaje'] = 0 + + ultimo_viaje['h3'] = ultimo_viaje['h3_d'] + ultimo_viaje['id_etapa'] += 1 + ultimo_viaje = ultimo_viaje[['dia', + 'id_tarjeta', + 'id_viaje', + 'id_etapa', + 'h3', + 'modo_agregado', + 'rango_hora', + 'genero', + 'tarifa', + 'factor_expansion_linea', + 'ultimo_viaje']] + + etapas_all = pd.concat([etapas_all, ultimo_viaje]).sort_values( + ['dia', 'id_tarjeta', 'id_viaje', 'id_etapa']).reset_index(drop=True) + + etapas_all['tipo_viaje'] = 'Transfer_' + \ + (etapas_all['id_etapa']-1).astype(str) + etapas_all.loc[etapas_all.ultimo_viaje == 1, 'tipo_viaje'] = 'Fin' + etapas_all.loc[etapas_all.id_etapa == 1, 'tipo_viaje'] = 'Inicio' + + etapas_all['polygon'] = '' + if id_polygon != 'NONE': + etapas_all.loc[etapas_all.h3.isin( + poly_h3.h3_o.unique()), 'polygon'] = id_polygon + + etapas_all = etapas_all.drop(['ultimo_viaje'], axis=1) + + # Guardo las coordenadas de los H3 + h3_coords = etapas_all.groupby( + 'h3', as_index=False).id_viaje.count().drop(['id_viaje'], axis=1) + h3_coords[['lat', 'lon']] = h3_coords.h3.apply(h3_to_lat_lon) + + # Preparo cada etapa de viaje para poder hacer la agrupación y tener inicio, transferencias y destino en un mismo registro + inicio = etapas_all.loc[etapas_all.tipo_viaje == 'Inicio', ['dia', + 'id_tarjeta', + 'id_viaje', + 'h3', + 'modo_agregado', + 'rango_hora', + 'genero', + 'tarifa', + 'factor_expansion_linea', + 'polygon']].rename(columns={'h3': 'h3_inicio', + 'polygon': 'poly_inicio'}) + + + fin = etapas_all.loc[etapas_all.tipo_viaje == 'Fin', ['dia', + 'id_tarjeta', + 'id_viaje', + 'h3', + 'polygon']].rename(columns={'h3': 'h3_fin', + 'polygon': 'poly_fin'}) + transfer1 = etapas_all.loc[etapas_all.tipo_viaje == 'Transfer_1', ['dia', + 'id_tarjeta', + 'id_viaje', + 'h3', + 'polygon']].rename(columns={'h3': 'h3_transfer1', 'polygon': 'poly_transfer1'}) + transfer2 = etapas_all.loc[etapas_all.tipo_viaje == 'Transfer_2', ['dia', + 'id_tarjeta', + 'id_viaje', + 'h3', + 'polygon']].rename(columns={'h3': 'h3_transfer2', + 'polygon': 'poly_transfer2'}) + + + + etapas_agrupadas = inicio.merge(transfer1, how='left').merge( + transfer2, how='left').merge(fin, how='left').fillna('') + + etapas_agrupadas = etapas_agrupadas[['dia', + 'id_tarjeta', + 'id_viaje', + 'h3_inicio', + 'h3_transfer1', + 'h3_transfer2', + 'h3_fin', + 'poly_inicio', + 'poly_transfer1', + 'poly_transfer2', + 'poly_fin', + 'modo_agregado', + 'rango_hora', + 'genero', + 'tarifa', + 'factor_expansion_linea']] + + + + for zona in zonas: + + if id_polygon != 'NONE': + # print(id_polygon, zona) + h3_equivalencias = creo_h3_equivalencias(polygons_h3[polygons_h3.id_polygon == id_polygon].copy(), + poligonos[poligonos.id == + id_polygon], + zona, + zonificaciones[zonificaciones.zona == zona].copy()) + + # Preparo para agrupar por líneas de deseo y cambiar de resolución si es necesario + etapas_agrupadas_zon = etapas_agrupadas.copy() + + etapas_agrupadas_zon['id_polygon'] = id_polygon + etapas_agrupadas_zon['zona'] = zona + + etapas_agrupadas_zon['inicio_norm'] = etapas_agrupadas_zon['h3_inicio'] + etapas_agrupadas_zon['transfer1_norm'] = etapas_agrupadas_zon['h3_transfer1'] + etapas_agrupadas_zon['transfer2_norm'] = etapas_agrupadas_zon['h3_transfer2'] + etapas_agrupadas_zon['fin_norm'] = etapas_agrupadas_zon['h3_fin'] + etapas_agrupadas_zon['poly_inicio_norm'] = etapas_agrupadas_zon['poly_inicio'] + etapas_agrupadas_zon['poly_transfer1_norm'] = etapas_agrupadas_zon['poly_transfer1'] + etapas_agrupadas_zon['poly_transfer2_norm'] = etapas_agrupadas_zon['poly_transfer2'] + etapas_agrupadas_zon['poly_fin_norm'] = etapas_agrupadas_zon['poly_fin'] + + n = 1 + for i in ['inicio_norm', 'transfer1_norm', 'transfer2_norm', 'fin_norm']: + + etapas_agrupadas_zon = etapas_agrupadas_zon.merge( + h3_coords.rename(columns={'h3': i}), how='left', on=i) + etapas_agrupadas_zon[f'lon{n}'] = etapas_agrupadas_zon['lon'] + etapas_agrupadas_zon[f'lat{n}'] = etapas_agrupadas_zon['lat'] + etapas_agrupadas_zon = etapas_agrupadas_zon.drop( + ['lon', 'lat'], axis=1) + + # Selecciono el centroide del polígono en vez del centroide de cada hexágono + + if tipo_poly == 'poligono': + etapas_agrupadas_zon.loc[etapas_agrupadas_zon[i].isin( + poly_h3.h3_o.unique()), f'lat{n}'] = poly_h3.polygon_lat.mean() + etapas_agrupadas_zon.loc[etapas_agrupadas_zon[i].isin( + poly_h3.h3_o.unique()), f'lon{n}'] = poly_h3.polygon_lon.mean() + + if f'{i}_ant' not in etapas_agrupadas_zon.columns: + etapas_agrupadas_zon[f'{i}_ant'] = etapas_agrupadas_zon[i] + + if 'res_' in zona: + resol = int(zona.replace('res_', '')) + etapas_agrupadas_zon[i] = etapas_agrupadas_zon[i].apply( + h3toparent, res=resol) + + else: + zonas_data_ = zonas_data.groupby( + ['h3', 'fex', 'latitud', 'longitud'], as_index=False)[zona].first() + etapas_agrupadas_zon = etapas_agrupadas_zon.merge( + zonas_data_[['h3', zona]].rename(columns={'h3': i, zona: 'zona_tmp'}), how='left') + + etapas_agrupadas_zon[i] = etapas_agrupadas_zon['zona_tmp'] + etapas_agrupadas_zon = etapas_agrupadas_zon.drop( + ['zona_tmp'], axis=1) + if len(etapas_agrupadas_zon[(etapas_agrupadas_zon.inicio_norm.isna()) | (etapas_agrupadas_zon.fin_norm.isna())]) > 0: + cant_etapas = len(etapas_agrupadas_zon[(etapas_agrupadas_zon.inicio_norm.isna()) | ( + etapas_agrupadas_zon.fin_norm.isna())]) + print( + f'Hay {cant_etapas} registros a los que no se les pudo asignar {zona}') + + etapas_agrupadas_zon = etapas_agrupadas_zon[~( + (etapas_agrupadas_zon.inicio_norm.isna()) | (etapas_agrupadas_zon.fin_norm.isna()))] + + # Si es cuenca modifico las latitudes longitudes donde coincide el polígono de cuenca con el h3 + if (tipo_poly == 'cuenca'): + # reemplazo latitudes y longitudes de cuenca para normalizar + poly_var = i.replace('h3_', '').replace('_norm', '') + h3_equivalencias_agg = h3_equivalencias.groupby( + [f'zona_{zona}', f'lat_{zona}', f'lon_{zona}'], as_index=False).h3_o.count().drop(['h3_o'], axis=1) + + etapas_agrupadas_zon = etapas_agrupadas_zon.merge(h3_equivalencias_agg[[f'zona_{zona}', + f'lat_{zona}', + f'lon_{zona}']].rename(columns={f'zona_{zona}': i}), + how='left', on=i) + + etapas_agrupadas_zon.loc[(etapas_agrupadas_zon[f'lat_{zona}'].notna()) + & (etapas_agrupadas_zon[f'poly_{poly_var}'] != ''), + f'lat{n}'] = etapas_agrupadas_zon.loc[ + (etapas_agrupadas_zon[f'lat_{zona}'].notna()) + & (etapas_agrupadas_zon[f'poly_{poly_var}'] != ''), + f'lat_{zona}'] + + etapas_agrupadas_zon.loc[(etapas_agrupadas_zon[f'lon_{zona}'].notna()) + & (etapas_agrupadas_zon[f'poly_{poly_var}'] != ''), + f'lon{n}'] = etapas_agrupadas_zon.loc[ + (etapas_agrupadas_zon[f'lon_{zona}'].notna()) + & (etapas_agrupadas_zon[f'poly_{poly_var}'] != ''), + f'lon_{zona}'] + + etapas_agrupadas_zon = etapas_agrupadas_zon.drop( + [f'lon_{zona}', f'lat_{zona}'], axis=1) + + etapas_agrupadas_zon[i] = etapas_agrupadas_zon[i].fillna('') + n += 1 + + etapas_agrupadas_zon['inicio'] = etapas_agrupadas_zon['inicio_norm'] + etapas_agrupadas_zon['transfer1'] = etapas_agrupadas_zon['transfer1_norm'] + etapas_agrupadas_zon['transfer2'] = etapas_agrupadas_zon['transfer2_norm'] + etapas_agrupadas_zon['fin'] = etapas_agrupadas_zon['fin_norm'] + etapas_agrupadas_zon['poly_inicio'] = etapas_agrupadas_zon['poly_inicio_norm'] + etapas_agrupadas_zon['poly_transfer1'] = etapas_agrupadas_zon['poly_transfer1_norm'] + etapas_agrupadas_zon['poly_transfer2'] = etapas_agrupadas_zon['poly_transfer2_norm'] + etapas_agrupadas_zon['poly_fin'] = etapas_agrupadas_zon['poly_fin_norm'] + etapas_agrupadas_zon['lat1_norm'] = etapas_agrupadas_zon['lat1'] + etapas_agrupadas_zon['lat2_norm'] = etapas_agrupadas_zon['lat2'] + etapas_agrupadas_zon['lat3_norm'] = etapas_agrupadas_zon['lat3'] + etapas_agrupadas_zon['lat4_norm'] = etapas_agrupadas_zon['lat4'] + etapas_agrupadas_zon['lon1_norm'] = etapas_agrupadas_zon['lon1'] + etapas_agrupadas_zon['lon2_norm'] = etapas_agrupadas_zon['lon2'] + etapas_agrupadas_zon['lon3_norm'] = etapas_agrupadas_zon['lon3'] + etapas_agrupadas_zon['lon4_norm'] = etapas_agrupadas_zon['lon4'] + + etapas_agrupadas_zon = normalizo_zona(etapas_agrupadas_zon, + zonificaciones[zonificaciones.zona == zona].copy()) + + etapas_agrupadas_all = pd.concat( + [etapas_agrupadas_all, etapas_agrupadas_zon], ignore_index=True) + + etapas_agrupadas_all['transferencia'] = 0 + etapas_agrupadas_all.loc[(etapas_agrupadas_all.transfer1_norm != '') | ( + etapas_agrupadas_all.transfer2_norm != ''), 'transferencia'] = 1 + + etapas_agrupadas_all = etapas_agrupadas_all.merge( + distancias_all, how='left') + + etapas_agrupadas_all['tipo_dia_'] = pd.to_datetime(etapas_agrupadas_all.dia).dt.weekday.astype(str).copy() + etapas_agrupadas_all['tipo_dia'] = 'Hábil' + etapas_agrupadas_all.loc[etapas_agrupadas_all.tipo_dia_.astype(int)>=5, 'tipo_dia'] = 'Fin de Semana' + etapas_agrupadas_all = etapas_agrupadas_all.drop(['tipo_dia_'], axis=1) + etapas_agrupadas_all['mes'] = etapas_agrupadas_all.dia.str[:7] + + etapas_agrupadas_all = etapas_agrupadas_all[['id_polygon', 'zona', 'dia', 'mes', 'tipo_dia', 'id_tarjeta', 'id_viaje', + 'h3_inicio', 'h3_transfer1', 'h3_transfer2', 'h3_fin', + 'inicio', 'transfer1', 'transfer2', 'fin', + 'poly_inicio', 'poly_transfer1', 'poly_transfer2', 'poly_fin', + 'inicio_norm', 'transfer1_norm', 'transfer2_norm', 'fin_norm', + 'poly_inicio_norm', 'poly_transfer1_norm', 'poly_transfer2_norm', 'poly_fin_norm', + 'lon1', 'lat1', 'lon2', 'lat2', 'lon3', 'lat3', 'lon4', 'lat4', + 'lon1_norm', 'lat1_norm', 'lon2_norm', 'lat2_norm', 'lon3_norm', 'lat3_norm', 'lon4_norm', 'lat4_norm', + 'transferencia', 'modo_agregado', 'rango_hora', 'genero', 'tarifa', 'distancia', + 'distance_osm_drive', 'distance_osm_drive_etapas', + 'travel_time_min', 'travel_speed', + 'factor_expansion_linea']] + + etapas_sin_agrupar = etapas_agrupadas_all.copy() + + aggregate_cols = ['id_polygon', 'dia', 'mes', 'tipo_dia', 'zona', 'inicio', 'fin', 'poly_inicio', + 'poly_fin', 'transferencia', 'modo_agregado', 'rango_hora', 'genero', 'tarifa', 'distancia'] + viajes_matrices = construyo_matrices(etapas_sin_agrupar, + aggregate_cols, + zonificaciones, + False, + False, + False) + + # Agrupación de viajes + aggregate_cols = ['id_polygon', + 'dia', + 'mes', + 'tipo_dia', + 'zona', + 'inicio_norm', + 'transfer1_norm', + 'transfer2_norm', + 'fin_norm', + 'poly_inicio_norm', + 'poly_transfer1_norm', + 'poly_transfer2_norm', + 'poly_fin_norm', + 'transferencia', + 'modo_agregado', + 'rango_hora', + 'genero', + 'tarifa', + 'distancia'] + + weighted_mean_cols = ['distance_osm_drive', + 'distance_osm_drive_etapas', + 'travel_time_min', + 'travel_speed', + 'lat1_norm', + 'lon1_norm', + 'lat2_norm', + 'lon2_norm', + 'lat3_norm', + 'lon3_norm', + 'lat4_norm', + 'lon4_norm'] + + weight_col = 'factor_expansion_linea' + + zero_to_nan = ['lat1_norm', + 'lon1_norm', + 'lat2_norm', + 'lon2_norm', + 'lat3_norm', + 'lon3_norm', + 'lat4_norm', + 'lon4_norm'] + + etapas_agrupadas_all = agrupar_viajes(etapas_agrupadas_all, + aggregate_cols, + weighted_mean_cols, + weight_col, + zero_to_nan, + agg_transferencias=False, + agg_modo=False, + agg_hora=False, + agg_distancia=False) + + zonificaciones['lat'] = zonificaciones.geometry.representative_point().y + zonificaciones['lon'] = zonificaciones.geometry.representative_point().x + + n = 1 + poly_lst = ['poly_inicio', 'poly_transfer1', 'poly_transfer2', 'poly_fin'] + for i in ['inicio', 'transfer1', 'transfer2', 'fin']: + etapas_agrupadas_all = etapas_agrupadas_all.merge(zonificaciones[['zona', 'id', 'lat', 'lon']].rename(columns={'id': f'{i}_norm', 'lat': f'lat{n}_norm_tmp', 'lon': f'lon{n}_norm_tmp'}), + how='left', + on=['zona', f'{i}_norm']) + etapas_agrupadas_all.loc[etapas_agrupadas_all[f'{poly_lst[n-1]}_norm'] == '', + f'lat{n}_norm'] = etapas_agrupadas_all.loc[etapas_agrupadas_all[f'{poly_lst[n-1]}_norm'] == '', f'lat{n}_norm_tmp'] + etapas_agrupadas_all.loc[etapas_agrupadas_all[f'{poly_lst[n-1]}_norm'] == '', + f'lon{n}_norm'] = etapas_agrupadas_all.loc[etapas_agrupadas_all[f'{poly_lst[n-1]}_norm'] == '', f'lon{n}_norm_tmp'] + + etapas_agrupadas_all = etapas_agrupadas_all.drop( + [f'lat{n}_norm_tmp', f'lon{n}_norm_tmp'], axis=1) + + if (n == 1) | (n == 4): + viajes_matrices = viajes_matrices.merge(zonificaciones[['zona', 'id', 'lat', 'lon']].rename(columns={'id': f'{i}', 'lat': f'lat{n}_tmp', 'lon': f'lon{n}_tmp'}), + how='left', + on=['zona', f'{i}']) + viajes_matrices.loc[viajes_matrices[f'{poly_lst[n-1]}'] == '', + f'lat{n}'] = viajes_matrices.loc[viajes_matrices[f'{poly_lst[n-1]}'] == '', f'lat{n}_tmp'] + viajes_matrices.loc[viajes_matrices[f'{poly_lst[n-1]}'] == '', + f'lon{n}'] = viajes_matrices.loc[viajes_matrices[f'{poly_lst[n-1]}'] == '', f'lon{n}_tmp'] + viajes_matrices = viajes_matrices.drop( + [f'lat{n}_tmp', f'lon{n}_tmp'], axis=1) + + n += 1 + + if id_polygon == 'NONE': + etapas_agrupadas_all = etapas_agrupadas_all.drop(['id_polygon', + 'poly_inicio_norm', + 'poly_transfer1_norm', + 'poly_transfer2_norm', + 'poly_fin_norm'], axis=1) + + viajes_matrices = viajes_matrices.drop( + ['poly_inicio', 'poly_fin'], axis=1) + + # # Agrupar a nivel de mes y corregir factor de expansión + sum_viajes = etapas_agrupadas_all.groupby(['dia', 'mes', 'tipo_dia', 'zona'], as_index=False).factor_expansion_linea.sum().groupby(['mes', 'tipo_dia', 'zona'], as_index=False).factor_expansion_linea.mean().round() + + aggregate_cols = ['mes', 'tipo_dia', 'zona', 'inicio_norm', 'transfer1_norm','transfer2_norm', 'fin_norm', 'transferencia', 'modo_agregado', 'rango_hora', 'genero', 'tarifa', 'distancia', ] + weighted_mean_cols=['distance_osm_drive', 'distance_osm_drive_etapas', 'travel_time_min', 'travel_speed', 'lat1_norm', 'lon1_norm', 'lat2_norm', 'lon2_norm', 'lat3_norm', 'lon3_norm', 'lat4_norm', 'lon4_norm'] + + etapas_agrupadas_all = calculate_weighted_means(etapas_agrupadas_all, + aggregate_cols=aggregate_cols, + weighted_mean_cols=weighted_mean_cols, + weight_col='factor_expansion_linea', + zero_to_nan=zero_to_nan, + var_fex_summed=False) + + sum_viajes['factor_expansion_linea'] = 1 - (sum_viajes['factor_expansion_linea'] / etapas_agrupadas_all.groupby(['mes', 'tipo_dia', 'zona'], as_index=False).factor_expansion_linea.sum().factor_expansion_linea ) + sum_viajes = sum_viajes.rename(columns={'factor_expansion_linea':'factor_correccion'}) + + etapas_agrupadas_all = etapas_agrupadas_all.merge(sum_viajes) + etapas_agrupadas_all['factor_expansion_linea2'] = etapas_agrupadas_all['factor_expansion_linea'] * etapas_agrupadas_all['factor_correccion'] + etapas_agrupadas_all['factor_expansion_linea2'] = etapas_agrupadas_all['factor_expansion_linea'] - etapas_agrupadas_all['factor_expansion_linea2'] + etapas_agrupadas_all = etapas_agrupadas_all.drop(['factor_correccion', 'factor_expansion_linea'], axis=1) + etapas_agrupadas_all = etapas_agrupadas_all.rename(columns={'factor_expansion_linea2':'factor_expansion_linea'}) + + # # Agrupar a nivel de mes y corregir factor de expansión + sum_viajes = viajes_matrices.groupby(['dia', 'mes', 'tipo_dia', 'zona'], as_index=False).factor_expansion_linea.sum().groupby(['mes', 'tipo_dia', 'zona'], as_index=False).factor_expansion_linea.mean() + + aggregate_cols = ['id_polygon', 'mes', 'tipo_dia', 'zona', 'inicio', 'fin', 'transferencia', 'modo_agregado', 'rango_hora', 'genero', 'tarifa', 'distancia', 'orden_origen', 'orden_destino', 'Origen', 'Destino'] + weighted_mean_cols = ['lat1', 'lon1', 'lat4', 'lon4', 'distance_osm_drive', 'travel_time_min', 'travel_speed',] + zero_to_nan = ['lat1', 'lon1', 'lat4', 'lon4'] + + viajes_matrices = calculate_weighted_means(viajes_matrices, + aggregate_cols=aggregate_cols, + weighted_mean_cols=weighted_mean_cols, + weight_col='factor_expansion_linea', + zero_to_nan=zero_to_nan, + var_fex_summed=False) + + sum_viajes['factor_expansion_linea'] = 1 - (sum_viajes['factor_expansion_linea'] / viajes_matrices.groupby(['mes', 'tipo_dia', 'zona'], as_index=False).factor_expansion_linea.sum().factor_expansion_linea ) + sum_viajes = sum_viajes.rename(columns={'factor_expansion_linea':'factor_correccion'}) + + viajes_matrices = viajes_matrices.merge(sum_viajes) + viajes_matrices['factor_expansion_linea2'] = viajes_matrices['factor_expansion_linea'] * viajes_matrices['factor_correccion'] + viajes_matrices['factor_expansion_linea2'] = viajes_matrices['factor_expansion_linea'] - viajes_matrices['factor_expansion_linea2'] + viajes_matrices = viajes_matrices.drop(['factor_correccion', 'factor_expansion_linea'], axis=1) + viajes_matrices = viajes_matrices.rename(columns={'factor_expansion_linea2':'factor_expansion_linea'}) + + return etapas_agrupadas_all, etapas_sin_agrupar, viajes_matrices, zonificaciones + + +def proceso_poligonos(): + + print('Procesa polígonos') + + check_config() + + zonificaciones = levanto_tabla_sql('zonificaciones') + + poligonos = levanto_tabla_sql('poligonos') + + if len(poligonos) > 0: + print('identifica viajes en polígonos') + # Read trips and jorneys + etapas, viajes = load_and_process_data() + # # Select cases based fron polygon + etapas_selec, viajes_selec, polygons, polygons_h3 = select_cases_from_polygons( + etapas[etapas.od_validado == 1], viajes[viajes.od_validado == 1], poligonos, res=8) + + etapas_agrupadas, etapas_sin_agrupar, viajes_matrices, zonificaciones = preparo_lineas_deseo( + etapas_selec, viajes_selec, polygons_h3, poligonos=poligonos, res=[6]) + + indicadores = construyo_indicadores(viajes_selec) + + conn_dash = iniciar_conexion_db(tipo='dash') + etapas_agrupadas = etapas_agrupadas.fillna(0) + etapas_agrupadas.to_sql("poly_etapas", + conn_dash, if_exists="replace", index=False,) + + viajes_matrices.to_sql("poly_matrices", + conn_dash, if_exists="replace", index=False,) + + indicadores.to_sql("poly_indicadores", + conn_dash, if_exists="replace", index=False,) + + conn_dash.close() + + +def proceso_lineas_deseo(): + + print('Procesa etapas') + + check_config() + + zonificaciones = levanto_tabla_sql('zonificaciones') + zonificaciones['lat'] = zonificaciones.geometry.representative_point().y + zonificaciones['lon'] = zonificaciones.geometry.representative_point().x + + etapas, viajes = load_and_process_data() + + etapas_agrupadas, etapas_sin_agrupar, viajes_matrices, zonificaciones = preparo_lineas_deseo( + etapas, viajes, res=[6]) + + indicadores = construyo_indicadores(viajes) + + socio_indicadores = crea_socio_indicadores(etapas, viajes) + + conn_dash = iniciar_conexion_db(tipo='dash') + + etapas_agrupadas = etapas_agrupadas.fillna(0) + etapas_agrupadas.to_sql("agg_etapas", + conn_dash, if_exists="replace", index=False,) + + viajes_matrices.to_sql("agg_matrices", + conn_dash, if_exists="replace", index=False,) + + indicadores.to_sql("agg_indicadores", + conn_dash, if_exists="replace", index=False,) + + socio_indicadores.to_sql("socio_indicadores", + conn_dash, if_exists="replace", index=False,) + + + conn_dash.close() diff --git a/urbantrips/process_transactions.py b/urbantrips/process_transactions.py index 8a480e3..4e7c4fc 100644 --- a/urbantrips/process_transactions.py +++ b/urbantrips/process_transactions.py @@ -1,5 +1,6 @@ from urbantrips.datamodel import legs, trips from urbantrips.datamodel import transactions as trx +from urbantrips.datamodel import services from urbantrips.destinations import destinations as dest from urbantrips.geo import geo from urbantrips.carto import carto, routes @@ -23,6 +24,11 @@ def main(): tipo_trx_invalidas = configs["tipo_trx_invalidas"] nombre_archivo_trx = configs["nombre_archivo_trx"] + # gps configs + nombre_archivo_gps = configs["nombre_archivo_gps"] + nombres_variables_gps = configs["nombres_variables_gps"] + tiempos_viaje_estaciones = configs["tiempos_viaje_estaciones"] + tolerancia_parada_destino = configs["tolerancia_parada_destino"] resolucion_h3 = configs["resolucion_h3"] trx_order_params = { @@ -31,14 +37,6 @@ def main(): "ventana_duplicado": configs["ventana_duplicado"], } - # gps configs - if geolocalizar_trx_config: - nombre_archivo_gps = configs["nombre_archivo_gps"] - nombres_variables_gps = configs["nombres_variables_gps"] - else: - nombre_archivo_gps = None - nombres_variables_gps = None - # Compute tolerance in h3 ring ring_size = geo.get_h3_buffer_ring_size( resolucion_h3, tolerancia_parada_destino @@ -69,6 +67,25 @@ def main(): # Produce trips and users tables from legs trips.create_trips_from_legs() + # Create distances table + carto.create_distances_table(use_parallel=False) + + if nombre_archivo_gps is not None: + services.process_services(line_ids=None) + + # Assign a gps point id to legs' origins + legs.assign_gps_origin() + + # Assign a gps point id to legs' destination + legs.assign_gps_destination() + + if tiempos_viaje_estaciones is not None: + # Assign stations to legs for travel times + legs.assign_stations_od() + + # compute travel time for trips + trips.compute_trips_travel_time() + # Inferir route geometries based on legs data routes.infer_routes_geoms(plotear_lineas=False) diff --git a/urbantrips/run_postprocessing.py b/urbantrips/run_postprocessing.py index a21087d..42c6c3d 100644 --- a/urbantrips/run_postprocessing.py +++ b/urbantrips/run_postprocessing.py @@ -1,8 +1,6 @@ from urbantrips.datamodel.misc import persist_datamodel_tables from urbantrips.kpi import kpi -from urbantrips.viz import viz from urbantrips.carto import carto -from urbantrips.utils import utils from urbantrips.utils.check_configs import check_config @@ -15,9 +13,6 @@ def main(): # Create voronoi TAZs carto.create_voronoi_zones() - # Create distances table - carto.create_distances_table(use_parallel=True) - # Persist datamodel into csv tables persist_datamodel_tables() diff --git a/urbantrips/tests/test_routes.py b/urbantrips/tests/test_routes.py index a720b26..a58d00a 100644 --- a/urbantrips/tests/test_routes.py +++ b/urbantrips/tests/test_routes.py @@ -9,12 +9,10 @@ def test_routes(): utils.create_db() routes.process_routes_geoms() - conn_insumos = utils.iniciar_conexion_db(tipo='insumos') + conn_insumos = utils.iniciar_conexion_db(tipo="insumos") - lines_routes = pd.read_sql( - "select * from official_lines_geoms", conn_insumos) - branches_routes = pd.read_sql( - "select * from official_branches_geoms", conn_insumos) + lines_routes = pd.read_sql("select * from official_lines_geoms", conn_insumos) + branches_routes = pd.read_sql("select * from official_branches_geoms", conn_insumos) assert len(lines_routes) == 2 assert len(branches_routes) == 8 diff --git a/urbantrips/tests/test_unit_tests.py b/urbantrips/tests/test_unit_tests.py index 2362369..8f3da46 100644 --- a/urbantrips/tests/test_unit_tests.py +++ b/urbantrips/tests/test_unit_tests.py @@ -118,7 +118,7 @@ def check(d): def test_asignar_id_viaje_etapa(df_trx): - df_trx['tiempo'] = None + df_trx["tiempo"] = None df = legs.asignar_id_viaje_etapa_orden_trx(df_trx) # Caso simple 4 colectivos 4 viajes de 1 etapa @@ -228,8 +228,9 @@ def test_cambiar_id_tarjeta_trx_simul_delta(df_test_id_viaje): assert (tarjetas_duplicadas_5.id_tarjeta_original == ["2", "2"]).all() assert (tarjetas_duplicadas_5.id_tarjeta_nuevo == ["2_1", "2_2"]).all() - assert (trx_5.loc[trx_5['id'].isin([15, 16, 17]), - 'id_tarjeta'] == ['2_0', '2_1', '2_2']).all() + assert ( + trx_5.loc[trx_5["id"].isin([15, 16, 17]), "id_tarjeta"] == ["2_0", "2_1", "2_2"] + ).all() trx_1, tarjetas_duplicadas_1 = legs.cambiar_id_tarjeta_trx_simul_fecha( trx.copy(), ventana_duplicado=1 @@ -238,20 +239,23 @@ def test_cambiar_id_tarjeta_trx_simul_delta(df_test_id_viaje): assert len(tarjetas_duplicadas_1) == 1 assert (tarjetas_duplicadas_1.id_tarjeta_original == ["2"]).all() assert (tarjetas_duplicadas_1.id_tarjeta_nuevo == ["2_1"]).all() - assert (trx_1.loc[trx_1['id'].isin([15, 16, 17]), - 'id_tarjeta'] == ['2_0', '2_1', '2_0']).all() - - -def create_test_trx(geolocalizar_trx_config, - nombre_archivo_trx, - nombres_variables_trx, - formato_fecha, - col_hora, - tipo_trx_invalidas, - nombre_archivo_gps, - nombres_variables_gps): - - for tipo in ['data', 'insumos']: + assert ( + trx_1.loc[trx_1["id"].isin([15, 16, 17]), "id_tarjeta"] == ["2_0", "2_1", "2_0"] + ).all() + + +def create_test_trx( + geolocalizar_trx_config, + nombre_archivo_trx, + nombres_variables_trx, + formato_fecha, + col_hora, + tipo_trx_invalidas, + nombre_archivo_gps, + nombres_variables_gps, +): + + for tipo in ["data", "insumos"]: filePath = utils.traigo_db_path(tipo=tipo) if os.path.exists(filePath): os.remove(filePath) @@ -262,14 +266,16 @@ def create_test_trx(geolocalizar_trx_config, # crear base de datos: utils.create_db() - transactions.create_transactions(geolocalizar_trx_config, - nombre_archivo_trx, - nombres_variables_trx, - formato_fecha, - col_hora, - tipo_trx_invalidas, - nombre_archivo_gps, - nombres_variables_gps) + transactions.create_transactions( + geolocalizar_trx_config, + nombre_archivo_trx, + nombres_variables_trx, + formato_fecha, + col_hora, + tipo_trx_invalidas, + nombre_archivo_gps, + nombres_variables_gps, + ) def test_amba_integration(matriz_validacion_test_amba): @@ -285,14 +291,16 @@ def test_amba_integration(matriz_validacion_test_amba): nombre_archivo_gps = configs["nombre_archivo_gps"] nombres_variables_gps = configs["nombres_variables_gps"] - create_test_trx(geolocalizar_trx_config, - nombre_archivo_trx, - nombres_variables_trx, - formato_fecha, - col_hora, - tipo_trx_invalidas, - nombre_archivo_gps, - nombres_variables_gps) + create_test_trx( + geolocalizar_trx_config, + nombre_archivo_trx, + nombres_variables_trx, + formato_fecha, + col_hora, + tipo_trx_invalidas, + nombre_archivo_gps, + nombres_variables_gps, + ) trx_order_params = { "criterio": configs["ordenamiento_transacciones"], @@ -300,17 +308,34 @@ def test_amba_integration(matriz_validacion_test_amba): "ventana_duplicado": configs["ventana_duplicado"], } - conn_data = utils.iniciar_conexion_db(tipo='data') - conn_insumos = utils.iniciar_conexion_db(tipo='insumos') + conn_data = utils.iniciar_conexion_db(tipo="data") + conn_insumos = utils.iniciar_conexion_db(tipo="insumos") routes.process_routes_metadata() trx = pd.read_sql("select * from transacciones", conn_data) # testear formateo de columnas - cols = ['id', 'id_original', 'id_tarjeta', 'fecha', 'dia', 'tiempo', - 'hora', 'modo', 'id_linea', 'id_ramal', 'interno', 'orden_trx', - 'latitud', 'longitud', 'factor_expansion'] + cols = [ + "id", + "id_original", + "id_tarjeta", + "fecha", + "dia", + "tiempo", + "hora", + "modo", + "id_linea", + "id_ramal", + "interno", + "orden_trx", + "genero", + "tarifa", + "latitud", + "longitud", + "factor_expansion", + ] + assert all(trx.columns.isin(cols)) # testear que no haya faltantes @@ -318,7 +343,7 @@ def test_amba_integration(matriz_validacion_test_amba): assert trx[c].notna().all() # testear que no haya tarjetas con trx unica - assert (trx.groupby('id_tarjeta').size() > 1).all() + assert (trx.groupby("id_tarjeta").size() > 1).all() # longitud del string igual para todas las tarjetas assert trx.id_tarjeta.str.len().std() == 0 @@ -327,7 +352,8 @@ def test_amba_integration(matriz_validacion_test_amba): # actualizar matriz de validacion matriz_validacion_test_amba.to_sql( - "matriz_validacion", conn_insumos, if_exists="replace", index=False) + "matriz_validacion", conn_insumos, if_exists="replace", index=False + ) routes.process_routes_geoms() @@ -341,21 +367,19 @@ def test_amba_integration(matriz_validacion_test_amba): etapas = pd.read_sql(q, conn_data) # chequear id viajes - etapa = etapas.loc[etapas.id_tarjeta == '0037030208_0', :] + etapa = etapas.loc[etapas.id_tarjeta == "0037030208_0", :] assert (etapa.id_viaje == range(1, 5)).all() # chequear id etapas assert (etapa.id_etapa == 1).all() # chequear h3_o - assert (etapa.loc[etapa.id_viaje == 3, 'h3_o'] - == '88c2e311dbfffff').iloc[0] + assert (etapa.loc[etapa.id_viaje == 3, "h3_o"] == "88c2e311dbfffff").iloc[0] # chequear h3_d - assert (etapa.loc[etapa.id_viaje == 2, 'h3_d'] - == '88c2e311dbfffff').iloc[0] + assert (etapa.loc[etapa.id_viaje == 2, "h3_d"] == "88c2e311dbfffff").iloc[0] # chequear validacion por si y por no - assert etapa.loc[etapa.id_viaje == 2, 'od_validado'].iloc[0] == 1 + assert etapa.loc[etapa.id_viaje == 2, "od_validado"].iloc[0] == 1 def test_amba_destinos_min_distancia(matriz_validacion_test_amba): @@ -370,14 +394,16 @@ def test_amba_destinos_min_distancia(matriz_validacion_test_amba): nombre_archivo_gps = configs["nombre_archivo_gps"] nombres_variables_gps = configs["nombres_variables_gps"] - create_test_trx(geolocalizar_trx_config, - nombre_archivo_trx, - nombres_variables_trx, - formato_fecha, - col_hora, - tipo_trx_invalidas, - nombre_archivo_gps, - nombres_variables_gps) + create_test_trx( + geolocalizar_trx_config, + nombre_archivo_trx, + nombres_variables_trx, + formato_fecha, + col_hora, + tipo_trx_invalidas, + nombre_archivo_gps, + nombres_variables_gps, + ) trx_order_params = { "criterio": configs["ordenamiento_transacciones"], @@ -385,15 +411,31 @@ def test_amba_destinos_min_distancia(matriz_validacion_test_amba): "ventana_duplicado": configs["ventana_duplicado"], } - conn_data = utils.iniciar_conexion_db(tipo='data') - conn_insumos = utils.iniciar_conexion_db(tipo='insumos') + conn_data = utils.iniciar_conexion_db(tipo="data") + conn_insumos = utils.iniciar_conexion_db(tipo="insumos") trx = pd.read_sql("select * from transacciones", conn_data) # testear formateo de columnas - cols = ['id', 'id_original', 'id_tarjeta', 'fecha', 'dia', 'tiempo', - 'hora', 'modo', 'id_linea', 'id_ramal', 'interno', 'orden_trx', - 'latitud', 'longitud', 'factor_expansion'] + cols = [ + "id", + "id_original", + "id_tarjeta", + "fecha", + "dia", + "tiempo", + "hora", + "modo", + "id_linea", + "id_ramal", + "interno", + "orden_trx", + "latitud", + "longitud", + "factor_expansion", + "genero", + "tarifa", + ] assert all(trx.columns.isin(cols)) # testear que no haya faltantes @@ -401,7 +443,7 @@ def test_amba_destinos_min_distancia(matriz_validacion_test_amba): assert trx[c].notna().all() # testear que no haya tarjetas con trx unica - assert (trx.groupby('id_tarjeta').size() > 1).all() + assert (trx.groupby("id_tarjeta").size() > 1).all() # longitud del string igual para todas las tarjetas assert trx.id_tarjeta.str.len().std() == 0 @@ -410,7 +452,8 @@ def test_amba_destinos_min_distancia(matriz_validacion_test_amba): # actualizar matriz de validacion matriz_validacion_test_amba.to_sql( - "matriz_validacion", conn_insumos, if_exists="replace", index=False) + "matriz_validacion", conn_insumos, if_exists="replace", index=False + ) # imputar destinos minimizando distancia q = """ @@ -419,7 +462,7 @@ def test_amba_destinos_min_distancia(matriz_validacion_test_amba): order by dia,id_tarjeta,id_viaje,id_etapa,hora,tiempo """ etapas_sin_d = pd.read_sql_query(q, conn_data) - etapas_sin_d = etapas_sin_d.drop(['h3_d', 'od_validado'], axis=1) + etapas_sin_d = etapas_sin_d.drop(["h3_d", "od_validado"], axis=1) # add id_linea_agg routes.process_routes_metadata() @@ -432,46 +475,50 @@ def test_amba_destinos_min_distancia(matriz_validacion_test_amba): conn_insumos, ) - etapas_sin_d = etapas_sin_d.merge(metadata_lineas[['id_linea', - 'id_linea_agg']], - how='left', - on='id_linea') + etapas_sin_d = etapas_sin_d.merge( + metadata_lineas[["id_linea", "id_linea_agg"]], how="left", on="id_linea" + ) etapas_destinos_potencial = dest.imputar_destino_potencial(etapas_sin_d) destinos = dest.imputar_destino_min_distancia(etapas_destinos_potencial) - etapas_sin_d = etapas_sin_d.drop('h3_d', axis=1) - etapas = etapas_sin_d.merge(destinos, on=['id'], how='left') + etapas_sin_d = etapas_sin_d.drop("h3_d", axis=1) + etapas = etapas_sin_d.merge(destinos, on=["id"], how="left") - etapas = etapas\ - .sort_values( - ['dia', 'id_tarjeta', 'id_viaje', 'id_etapa', 'hora', 'tiempo'])\ - .reset_index(drop=True) + etapas = etapas.sort_values( + ["dia", "id_tarjeta", "id_viaje", "id_etapa", "hora", "tiempo"] + ).reset_index(drop=True) - etapas['od_validado'] = etapas['od_validado'].fillna(0).astype(int) - etapas['h3_d'] = etapas['h3_d'].fillna('') + etapas["od_validado"] = etapas["od_validado"].fillna(0).astype(int) + etapas["h3_d"] = etapas["h3_d"].fillna("") - etapa = etapas.loc[etapas.id_tarjeta == '3839538659_0', :] + etapa = etapas.loc[etapas.id_tarjeta == "3839538659_0", :] - etapa = etapa.reindex(columns=['id_viaje', 'id_etapa', - 'h3_o', 'h3_d', 'od_validado']) + etapa = etapa.reindex( + columns=["id_viaje", "id_etapa", "h3_o", "h3_d", "od_validado"] + ) # casos para armar tests con nuevos destinos # tarjeta 3839538659. la vuelta en tren que termine en la estacion de tren - assert (etapa.loc[(etapa.id_viaje == 2) & (etapa.id_etapa == 2), 'h3_d'] - == '88c2e38b23fffff').iloc[0] + assert ( + etapa.loc[(etapa.id_viaje == 2) & (etapa.id_etapa == 2), "h3_d"] + == "88c2e38b23fffff" + ).iloc[0] # tarjeta 0037035823. vuelve a la casa en otra linea y el destinio no es # en la primera trx del dia. sino en la de la parada de la linea - etapa = etapas.loc[etapas.id_tarjeta == '0037035823_0', :] + etapa = etapas.loc[etapas.id_tarjeta == "0037035823_0", :] - assert (etapa.loc[(etapa.id_viaje == 3) & (etapa.id_etapa == 1), 'h3_d'] - == '88c2e3a1a7fffff').iloc[0] + assert ( + etapa.loc[(etapa.id_viaje == 3) & (etapa.id_etapa == 1), "h3_d"] + == "88c2e3a1a7fffff" + ).iloc[0] # tarjeta 1939538599 se toma el subte fin del dia y su primer viaje fue # en villa fiorito - etapa = etapas.loc[etapas.id_tarjeta == '1939538599_0', :] - assert (etapa.loc[(etapa.id_viaje == 3) & ( - etapa.id_etapa == 2), 'h3_d'].iloc[0] == '') + etapa = etapas.loc[etapas.id_tarjeta == "1939538599_0", :] + assert ( + etapa.loc[(etapa.id_viaje == 3) & (etapa.id_etapa == 2), "h3_d"].iloc[0] == "" + ) def test_viz_lowes(): @@ -486,14 +533,16 @@ def test_viz_lowes(): nombre_archivo_gps = configs["nombre_archivo_gps"] nombres_variables_gps = configs["nombres_variables_gps"] - create_test_trx(geolocalizar_trx_config, - nombre_archivo_trx, - nombres_variables_trx, - formato_fecha, - col_hora, - tipo_trx_invalidas, - nombre_archivo_gps, - nombres_variables_gps) + create_test_trx( + geolocalizar_trx_config, + nombre_archivo_trx, + nombres_variables_trx, + formato_fecha, + col_hora, + tipo_trx_invalidas, + nombre_archivo_gps, + nombres_variables_gps, + ) trx_order_params = { "criterio": configs["ordenamiento_transacciones"], @@ -502,21 +551,22 @@ def test_viz_lowes(): } legs.create_legs_from_transactions(trx_order_params) - conn_data = utils.iniciar_conexion_db(tipo='data') + conn_data = utils.iniciar_conexion_db(tipo="data") q = "select * from etapas" etapas = pd.read_sql(q, conn_data) - recorridos_lowess = etapas.groupby( - 'id_linea').apply(geo.lowess_linea).reset_index() + recorridos_lowess = etapas.groupby("id_linea").apply(geo.lowess_linea).reset_index() - assert recorridos_lowess.geometry.type.unique()[0] == 'LineString' - alias = '' + assert recorridos_lowess.geometry.type.unique()[0] == "LineString" + alias = "" id_linea = 16 viz.plotear_recorrido_lowess( - id_linea=id_linea, etapas=etapas, recorridos_lowess=recorridos_lowess, - alias=alias,) - file_path = os.path.join( - "resultados", "png", f"{alias}linea_{id_linea}.png") + id_linea=id_linea, + etapas=etapas, + recorridos_lowess=recorridos_lowess, + alias=alias, + ) + file_path = os.path.join("resultados", "png", f"{alias}linea_{id_linea}.png") assert os.path.isfile(file_path) @@ -532,14 +582,16 @@ def test_section_load_viz(matriz_validacion_test_amba): nombre_archivo_gps = configs["nombre_archivo_gps"] nombres_variables_gps = configs["nombres_variables_gps"] - create_test_trx(geolocalizar_trx_config, - nombre_archivo_trx, - nombres_variables_trx, - formato_fecha, - col_hora, - tipo_trx_invalidas, - nombre_archivo_gps, - nombres_variables_gps) + create_test_trx( + geolocalizar_trx_config, + nombre_archivo_trx, + nombres_variables_trx, + formato_fecha, + col_hora, + tipo_trx_invalidas, + nombre_archivo_gps, + nombres_variables_gps, + ) trx_order_params = { "criterio": configs["ordenamiento_transacciones"], @@ -548,11 +600,9 @@ def test_section_load_viz(matriz_validacion_test_amba): } resolucion_h3 = configs["resolucion_h3"] tolerancia_parada_destino = configs["tolerancia_parada_destino"] - ring_size = geo.get_h3_buffer_ring_size( - resolucion_h3, tolerancia_parada_destino - ) + ring_size = geo.get_h3_buffer_ring_size(resolucion_h3, tolerancia_parada_destino) - conn_insumos = utils.iniciar_conexion_db(tipo='insumos') + conn_insumos = utils.iniciar_conexion_db(tipo="insumos") routes.process_routes_metadata() routes.process_routes_geoms() @@ -560,7 +610,8 @@ def test_section_load_viz(matriz_validacion_test_amba): # actualizar matriz de validacion matriz_validacion_test_amba.to_sql( - "matriz_validacion", conn_insumos, if_exists="replace", index=False) + "matriz_validacion", conn_insumos, if_exists="replace", index=False + ) carto.update_stations_catchment_area(ring_size=ring_size) @@ -579,8 +630,8 @@ def test_section_load_viz(matriz_validacion_test_amba): # Build final routes from official an inferred sources routes.build_routes_from_official_inferred() - kpi.compute_route_section_load(id_linea=32, rango_hrs=False) - viz.visualize_route_section_load(id_linea=32, rango_hrs=False) + kpi.compute_route_section_load(line_ids=117, hour_range=False) + viz.visualize_route_section_load(line_ids=117, hour_range=False) def test_viz(matriz_validacion_test_amba): @@ -595,14 +646,16 @@ def test_viz(matriz_validacion_test_amba): nombre_archivo_gps = configs["nombre_archivo_gps"] nombres_variables_gps = configs["nombres_variables_gps"] - create_test_trx(geolocalizar_trx_config, - nombre_archivo_trx, - nombres_variables_trx, - formato_fecha, - col_hora, - tipo_trx_invalidas, - nombre_archivo_gps, - nombres_variables_gps) + create_test_trx( + geolocalizar_trx_config, + nombre_archivo_trx, + nombres_variables_trx, + formato_fecha, + col_hora, + tipo_trx_invalidas, + nombre_archivo_gps, + nombres_variables_gps, + ) routes.process_routes_metadata() @@ -613,18 +666,17 @@ def test_viz(matriz_validacion_test_amba): } resolucion_h3 = configs["resolucion_h3"] tolerancia_parada_destino = configs["tolerancia_parada_destino"] - ring_size = geo.get_h3_buffer_ring_size( - resolucion_h3, tolerancia_parada_destino - ) + ring_size = geo.get_h3_buffer_ring_size(resolucion_h3, tolerancia_parada_destino) - conn_insumos = utils.iniciar_conexion_db(tipo='insumos') - conn_data = utils.iniciar_conexion_db(tipo='data') + conn_insumos = utils.iniciar_conexion_db(tipo="insumos") + conn_data = utils.iniciar_conexion_db(tipo="data") legs.create_legs_from_transactions(trx_order_params) # actualizar matriz de validacion matriz_validacion_test_amba.to_sql( - "matriz_validacion", conn_insumos, if_exists="replace", index=False) + "matriz_validacion", conn_insumos, if_exists="replace", index=False + ) carto.update_stations_catchment_area(ring_size=ring_size) @@ -643,39 +695,39 @@ def test_viz(matriz_validacion_test_amba): viz.create_visualizations() viajes = pd.read_sql("select * from viajes", conn_data) - viajes['distance_osm_drive'] = 0 - viajes['h3_o_norm'] = viajes.h3_o - viajes['h3_d_norm'] = viajes.h3_d - viajes['factor_expansion'] = 1 - - viz.imprime_burbujas(viajes, - res=7, - h3_o='h3_o', - alpha=.4, - cmap='flare', - var_fex='', - porc_viajes=100, - title=f'Hogares', - savefile=f'testing_burb_hogares', - show_fig=True, - k_jenks=1) - - viz.imprime_lineas_deseo(df=viajes, - h3_o='', - h3_d='', - var_fex='', - title=f'Lineas de deseo', - savefile='Lineas de deseo', - k_jenks=1) - - viz.imprimir_matrices_od(viajes, - savefile='viajes', - title='Matriz OD', - var_fex="") + viajes["distance_osm_drive"] = 0 + viajes["h3_o_norm"] = viajes.h3_o + viajes["h3_d_norm"] = viajes.h3_d + viajes["factor_expansion"] = 1 + + viz.imprime_burbujas( + viajes, + res=7, + h3_o="h3_o", + alpha=0.4, + cmap="flare", + var_fex="", + porc_viajes=100, + title=f"Hogares", + savefile=f"testing_burb_hogares", + show_fig=True, + k_jenks=1, + ) + + viz.imprime_lineas_deseo( + df=viajes, + h3_o="", + h3_d="", + var_fex="", + title=f"Lineas de deseo", + savefile="Lineas de deseo", + k_jenks=1, + ) + + viz.imprimir_matrices_od(viajes, savefile="viajes", title="Matriz OD", var_fex="") zonas = pd.read_sql("select * from zonas;", conn_insumos) - df, matriz_zonas = viz.traigo_zonificacion( - viajes, zonas, h3_o='h3_o', h3_d='h3_d') + df, matriz_zonas = viz.traigo_zonificacion(viajes, zonas, h3_o="h3_o", h3_d="h3_d") for i in matriz_zonas: var_zona = i[1] @@ -685,12 +737,12 @@ def test_viz(matriz_validacion_test_amba): df, zona_origen=f"{var_zona}_o", zona_destino=f"{var_zona}_d", - var_fex='', + var_fex="", x_rotation=90, normalize=True, cmap="Reds", - title='Matriz OD General', - figsize_tuple='', + title="Matriz OD General", + figsize_tuple="", matriz_order=matriz_order, savefile=f"{var_zona}", margins=True, @@ -708,13 +760,13 @@ def test_gps(matriz_validacion_test_amba): nombre_archivo_trx = "transacciones_amba_test_geocode.csv" nombre_archivo_gps = "gps_amba_test.csv" nombres_variables_gps = { - 'id_gps': 'id_gps', - 'id_linea_gps': 'id_linea_gps', - 'id_ramal_gps': 'id_ramal_gps', - 'interno_gps': 'interno_gps', - 'fecha_gps': 'fecha_gps', - 'latitud_gps': 'latitud_gps', - 'longitud_gps': 'longitud_gps', + "id_gps": "id_gps", + "id_linea_gps": "id_linea_gps", + "id_ramal_gps": "id_ramal_gps", + "interno_gps": "interno_gps", + "fecha_gps": "fecha_gps", + "latitud_gps": "latitud_gps", + "longitud_gps": "longitud_gps", } trx_order_params = { "criterio": "fecha_completa", @@ -723,49 +775,48 @@ def test_gps(matriz_validacion_test_amba): } resolucion_h3 = configs["resolucion_h3"] tolerancia_parada_destino = configs["tolerancia_parada_destino"] - ring_size = geo.get_h3_buffer_ring_size( - resolucion_h3, tolerancia_parada_destino + ring_size = geo.get_h3_buffer_ring_size(resolucion_h3, tolerancia_parada_destino) + + create_test_trx( + geolocalizar_trx_config, + nombre_archivo_trx, + nombres_variables_trx, + formato_fecha, + col_hora, + tipo_trx_invalidas, + nombre_archivo_gps, + nombres_variables_gps, ) - create_test_trx(geolocalizar_trx_config, - nombre_archivo_trx, - nombres_variables_trx, - formato_fecha, - col_hora, - tipo_trx_invalidas, - nombre_archivo_gps, - nombres_variables_gps) - routes.process_routes_metadata() legs.create_legs_from_transactions(trx_order_params) # confirm latlong for card_id 37030208 - conn_insumos = utils.iniciar_conexion_db(tipo='insumos') - conn_data = utils.iniciar_conexion_db(tipo='data') + conn_insumos = utils.iniciar_conexion_db(tipo="insumos") + conn_data = utils.iniciar_conexion_db(tipo="data") trx = pd.read_sql("select * from transacciones", conn_data) gps = pd.read_sql("select * from gps", conn_data) assert len(trx) == 2 - gps_latlong = gps.loc[gps.id_original == 2, ['latitud', 'longitud']] - trx_latlong = trx.loc[trx.id_original == - '2189303', ['latitud', 'longitud']] + gps_latlong = gps.loc[gps.id_original == 2, ["latitud", "longitud"]] + trx_latlong = trx.loc[trx.id_original == "2189303", ["latitud", "longitud"]] assert gps_latlong.latitud.item() == trx_latlong.latitud.item() assert gps_latlong.longitud.item() == trx_latlong.longitud.item() - gps_latlong = gps.loc[gps.id_original == 13, ['latitud', 'longitud']] - trx_latlong = trx.loc[trx.id_original == - '2189304', ['latitud', 'longitud']] + gps_latlong = gps.loc[gps.id_original == 13, ["latitud", "longitud"]] + trx_latlong = trx.loc[trx.id_original == "2189304", ["latitud", "longitud"]] assert gps_latlong.latitud.item() == trx_latlong.latitud.item() assert gps_latlong.longitud.item() == trx_latlong.longitud.item() # actualizar matriz de validacion matriz_validacion_test_amba.to_sql( - "matriz_validacion", conn_insumos, if_exists="replace", index=False) + "matriz_validacion", conn_insumos, if_exists="replace", index=False + ) carto.update_stations_catchment_area(ring_size=ring_size) @@ -792,8 +843,7 @@ def test_gps(matriz_validacion_test_amba): fe = pd.read_sql(q, conn_data) tot_pax = fe.factor_expansion.sum() - kpi_df = pd.read_sql( - "select * from kpi_by_day_line;", conn_data) + kpi_df = pd.read_sql("select * from kpi_by_day_line;", conn_data) assert round(kpi_df.tot_km.iloc[0]) == 16 assert kpi_df.tot_veh.iloc[0] == 2 diff --git a/urbantrips/utils/check_configs.py b/urbantrips/utils/check_configs.py index 862360a..7c0728d 100644 --- a/urbantrips/utils/check_configs.py +++ b/urbantrips/utils/check_configs.py @@ -1,4 +1,5 @@ import pandas as pd +import geopandas as gpd import sqlite3 import os import yaml @@ -49,7 +50,7 @@ def check_if_list(string): def replace_tabs_with_spaces(file_path, num_spaces=4): # Open the file in read mode - with open(file_path, 'r') as file: + with open(file_path, 'r', encoding='utf-8') as file: content = file.read() # Check if the file contains tabs @@ -83,23 +84,6 @@ def check_config_fecha(df, columns_with_date, date_format): return result -def replace_tabs_with_spaces(file_path, num_spaces=4): - - if os.path.isfile(file_path): - - # Open the file in read mode - with open(file_path, 'r') as file: - content = file.read() - - # Check if the file contains tabs - if '\t' in content: - # Replace tabs with spaces - content = content.replace('\t', ' ' * num_spaces) - # Save the modified content to the same file - with open(file_path, 'w') as file: - file.write(content) - - def create_configuracion(configs): configuracion = pd.DataFrame([], @@ -344,6 +328,7 @@ def check_lineas(config_default): lineas[modo_trx] = autobus lineas = lineas.rename( columns={id_linea_trx: 'id_linea', modo_trx: 'modo'}) + lineas.columns=[ "id_linea", "modo"] else: lineas = pd.read_csv(path_archivo_lineas) @@ -389,6 +374,7 @@ def check_config_errors(config_default): f'No está declarado el archivo de transacciones en {os.path.join("data", "data_ciudad")}'] else: ruta = os.path.join("data", "data_ciudad", nombre_archivo_trx) + print(f'--Archivo de transacciones en proceso: {ruta}') if not os.path.isfile(ruta): errores += [f'No se encuentra el archivo de transacciones {ruta}'] else: @@ -621,7 +607,95 @@ def check_configs_file(): print(f"Se creo el archivo '{file_name}' en '{directory}'") - +def guardo_zonificaciones(): + configs = leer_configs_generales() + alias = leer_alias() + matriz_zonas = [] + vars_zona = [] + + if configs["zonificaciones"]: + zonificaciones = pd.DataFrame([]) + for n in range(0, 5): + try: + file_zona = configs["zonificaciones"][f"geo{n+1}"] + var_zona = configs["zonificaciones"][f"var{n+1}"] + + try: + matriz_order = configs["zonificaciones"][f"orden{n+1}"] + except KeyError: + matriz_order = "" + + if matriz_order is None: + matriz_order = "" + + if file_zona: + db_path = os.path.join("data", "data_ciudad", file_zona) + if os.path.exists(db_path): + zonif = gpd.read_file(db_path) + zonif = zonif[[var_zona, 'geometry']] + zonif.columns = ['id', 'geometry'] + zonif['zona'] = var_zona + zonif = zonif[['zona', 'id', 'geometry']] + + if len(matriz_order) > 0: + order = pd.DataFrame(matriz_order, columns=['id']).reset_index().rename(columns={'index':'orden'}) + zonif = zonif.merge(order, how='left') + else: + zonif['orden'] = 0 + zonificaciones = pd.concat([zonificaciones, zonif], ignore_index=True) + + except KeyError: + pass + + db_path = os.path.join("resultados", f'{alias}Zona_voi.geojson') + if os.path.exists(db_path): + zonif = gpd.read_file(db_path) + zonif = zonif[['Zona_voi', 'geometry']] + zonif.columns = ['id', 'geometry'] + zonif['zona'] = 'Zona_voi' + zonif['orden'] = 0 + zonif = zonif[['zona', 'orden', 'id', 'geometry']] + zonificaciones = pd.concat([zonificaciones, zonif], ignore_index=True) + + if len(zonificaciones) > 0: + conn_dash = iniciar_conexion_db(tipo='dash') + conn_insumos = iniciar_conexion_db(tipo='insumos') + zonificaciones = zonificaciones.dissolve(['zona', 'id', 'orden'], as_index=False) + zonificaciones['wkt'] = zonificaciones.geometry.to_wkt() + zonificaciones = zonificaciones.drop(['geometry'], axis=1) + zonificaciones = zonificaciones.sort_values(['zona', 'orden', 'id']).reset_index(drop=True) + + zonificaciones.to_sql("zonificaciones", + conn_insumos, if_exists="replace", index=False,) + zonificaciones.to_sql("zonificaciones", + conn_dash, if_exists="replace", index=False,) + + + conn_insumos.close() + conn_dash.close() + + if configs['poligonos']: + + poly_file = configs['poligonos'] + + db_path = os.path.join("data", "data_ciudad", poly_file) + + if os.path.exists(db_path): + poly = gpd.read_file(db_path) + conn_dash = iniciar_conexion_db(tipo='dash') + conn_insumos = iniciar_conexion_db(tipo='insumos') + poly['wkt'] = poly.geometry.to_wkt() + poly = poly.drop(['geometry'], axis=1) + + poly.to_sql("poligonos", + conn_insumos, if_exists="replace", index=False,) + poly.to_sql("poligonos", + conn_dash, if_exists="replace", index=False,) + + + conn_insumos.close() + conn_dash.close() + @ duracion def check_config(): """ @@ -643,3 +717,4 @@ def check_config(): config_default = check_lineas(config_default) write_config(config_default) check_config_errors(config_default) + guardo_zonificaciones() diff --git a/urbantrips/utils/utils.py b/urbantrips/utils/utils.py index 728707d..98a72a1 100644 --- a/urbantrips/utils/utils.py +++ b/urbantrips/utils/utils.py @@ -1,37 +1,38 @@ import pandas as pd +import geopandas as gpd import sqlite3 import os import yaml import time from functools import wraps -import h3 +import re import numpy as np import weightedstats as ws from pandas.io.sql import DatabaseError import datetime -os.environ['USE_PYGEOS'] = '0' +from shapely import wkt def duracion(f): - @ wraps(f) + @wraps(f) def wrap(*args, **kw): - print('') + print("") print( - f"{f.__name__} ({str(datetime.datetime.now())[:19]})\n", end="", - flush=True) - print('-' * (len(f.__name__)+22)) + f"{f.__name__} ({str(datetime.datetime.now())[:19]})\n", end="", flush=True + ) + print("-" * (len(f.__name__) + 22)) ts = time.time() result = f(*args, **kw) te = time.time() print(f"Finalizado {f.__name__}. Tardo {te - ts:.2f} segundos") - print('') + print("") return result return wrap -@ duracion +@duracion def create_directories(): """ This function creates the basic directory structure @@ -75,36 +76,36 @@ def create_directories(): os.makedirs(db_path, exist_ok=True) -def leer_alias(tipo='data'): +def leer_alias(tipo="data"): """ Esta funcion toma un tipo de datos (data o insumos) y devuelve el alias seteado en el archivo de congifuracion """ configs = leer_configs_generales() # Setear el tipo de key en base al tipo de datos - if tipo == 'data': - key = 'alias_db_data' - elif tipo == 'insumos': - key = 'alias_db_insumos' - elif tipo == 'dash': - key = 'alias_db_data' + if tipo == "data": + key = "alias_db_data" + elif tipo == "insumos": + key = "alias_db_insumos" + elif tipo == "dash": + key = "alias_db_data" else: - raise ValueError('tipo invalido: %s' % tipo) + raise ValueError("tipo invalido: %s" % tipo) # Leer el alias try: - alias = configs[key] + '_' + alias = configs[key] + "_" except KeyError: - alias = '' + alias = "" return alias -def traigo_db_path(tipo='data'): +def traigo_db_path(tipo="data"): """ Esta funcion toma un tipo de datos (data o insumos) y devuelve el path a una base de datos con esa informacion """ - if tipo not in ('data', 'insumos', 'dash'): - raise ValueError('tipo invalido: %s' % tipo) + if tipo not in ("data", "insumos", "dash"): + raise ValueError("tipo invalido: %s" % tipo) alias = leer_alias(tipo) db_path = os.path.join("data", "db", f"{alias}{tipo}.sqlite") @@ -112,8 +113,8 @@ def traigo_db_path(tipo='data'): return db_path -def iniciar_conexion_db(tipo='data'): - """" +def iniciar_conexion_db(tipo="data"): + """ " Esta funcion toma un tipo de datos (data o insumos) y devuelve una conexion sqlite a la db """ @@ -122,7 +123,7 @@ def iniciar_conexion_db(tipo='data'): return conn -@ duracion +@duracion def create_db(): print("Creando bases de datos") @@ -149,7 +150,7 @@ def create_db(): def create_other_inputs_tables(): - conn_insumos = iniciar_conexion_db(tipo='insumos') + conn_insumos = iniciar_conexion_db(tipo="insumos") conn_insumos.execute( """ CREATE TABLE IF NOT EXISTS distancias @@ -227,11 +228,53 @@ def create_other_inputs_tables(): ; """ ) + + conn_insumos.execute( + """ + CREATE TABLE IF NOT EXISTS zonificaciones + (zona text NOT NULL, + id text NOT NULL, + orden int, + wkt text + ) + ; + """ + ) + conn_insumos.execute( + """ + CREATE TABLE IF NOT EXISTS poligonos + (id text NOT NULL, + wkt text + ) + ; + """ + ) + + conn_insumos.execute( + """ + CREATE TABLE IF NOT EXISTS travel_times_stations + (id_o int NOT NULL, + id_linea_o int NOT NULL, + id_ramal_o int, + lat_o float NOT NULL, + lon_o float NOT NULL, + id_d int NOT NULL, + lat_d float NOT NULL, + lon_d float NOT NULL, + id_linea_d int NOT NULL, + id_ramal_d int, + travel_time_min float NOT NULL + ) + ; + """ + ) + conn_insumos.close() def create_dash_tables(): - conn_dash = iniciar_conexion_db(tipo='dash') + conn_dash = iniciar_conexion_db(tipo="dash") + conn_dash.execute( """ CREATE TABLE IF NOT EXISTS matrices @@ -341,26 +384,115 @@ def create_dash_tables(): """ CREATE TABLE IF NOT EXISTS ocupacion_por_linea_tramo (id_linea int not null, + yr_mo text, nombre_linea str, - day_type text nor null, + day_type text not null, n_sections int, + section_meters int, sentido text not null, - section_id float not null, - hora_min int, - hora_max int, - cantidad_etapas int not null, - prop_etapas float not null, + section_id int not null, + hour_min int, + hour_max int, + legs int not null, + prop float not null, buff_factor float, wkt text ) ; """ ) + + conn_dash.execute( + """ + CREATE TABLE IF NOT EXISTS lines_od_matrix_by_section + (id_linea int not null, + yr_mo text, + day_type text nor null, + n_sections int, + hour_min int, + hour_max int, + Origen int not null, + Destino int not null, + legs int not null, + prop float not null, + nombre_linea text + ) + ; + """ + ) + + conn_dash.execute( + """ + CREATE TABLE IF NOT EXISTS matrices_linea_carto + (id_linea INT NOT NULL, + n_sections INT NOT NULL, + section_id INT NOT NULL, + wkt text, + x float, + y float, + nombre_linea text + ) + ; + """ + ) + + conn_dash.execute( + """ + CREATE TABLE IF NOT EXISTS matrices_linea + (id_linea INT NOT NULL, + yr_mo text, + day_type text not null, + n_sections INT NOT NULL, + hour_min int, + hour_max int, + section_id INT, + Origen int , + Destino int , + legs int, + prop float, + nombre_linea text + ) + ; + """ + ) + + conn_dash.execute( + """ + CREATE TABLE IF NOT EXISTS services_by_line_hour + ( + id_linea int not null, + dia text not null, + hora int not null, + servicios float not null + ) + ; + """ + ) + + conn_dash.execute( + """ + CREATE TABLE IF NOT EXISTS basic_kpi_by_line_hr + ( + dia text not null, + yr_mo text, + id_linea int not null, + nombre_linea text, + hora int not null, + veh float, + pax float, + dmt float, + of float, + speed_kmh float + ) + ; + """ + ) + conn_dash.close() def create_stops_and_routes_carto_tables(): - conn_insumos = iniciar_conexion_db(tipo='insumos') + conn_insumos = iniciar_conexion_db(tipo="insumos") conn_insumos.execute( """ CREATE TABLE IF NOT EXISTS official_branches_geoms @@ -416,11 +548,26 @@ def create_stops_and_routes_carto_tables(): ; """ ) + + conn_insumos.execute( + """ + CREATE TABLE IF NOT EXISTS routes_section_id_coords + (id_linea INT NOT NULL, + n_sections INT NOT NULL, + section_id INT NOT NULL, + section_lrs float NOT NULL, + x float NOT NULL, + y float NOT NULL + ) + ; + """ + ) + conn_insumos.close() def create_basic_data_model_tables(): - conn_data = iniciar_conexion_db(tipo='data') + conn_data = iniciar_conexion_db(tipo="data") conn_data.execute( """ CREATE TABLE IF NOT EXISTS transacciones @@ -436,6 +583,8 @@ def create_basic_data_model_tables(): id_ramal int, interno int, orden_trx int, + genero text, + tarifa text, latitud float, longitud float, factor_expansion float @@ -466,6 +615,8 @@ def create_basic_data_model_tables(): id_linea int, id_ramal int, interno int, + genero text, + tarifa text, latitud float, longitud float, h3_o text, @@ -498,6 +649,8 @@ def create_basic_data_model_tables(): otros int, h3_o text, h3_d text, + genero text, + tarifa text, od_validado int, factor_expansion_linea, factor_expansion_tarjeta @@ -548,17 +701,132 @@ def create_basic_data_model_tables(): """ CREATE TABLE IF NOT EXISTS ocupacion_por_linea_tramo (id_linea int not null, + yr_mo text, day_type text nor null, n_sections int, section_meters int, sentido text not null, - section_id float not null, - x float, - y float, - hora_min int, - hora_max int, - cantidad_etapas int not null, - prop_etapas float not null + section_id int not null, + hour_min int, + hour_max int, + legs int not null, + prop float not null + ) + ; + """ + ) + + conn_data.execute( + """ + CREATE TABLE IF NOT EXISTS legs_to_gps_origin + ( + dia text, + id_legs int not null, + id_gps int not null + ) + ; + """ + ) + + conn_data.execute( + """ + CREATE TABLE IF NOT EXISTS legs_to_gps_destination + ( + dia text, + id_legs int not null, + id_gps int not null + ) + ; + """ + ) + + conn_data.execute( + """ + CREATE TABLE IF NOT EXISTS legs_to_station_origin + ( + dia text, + id_legs int not null, + id_station int not null + ) + ; + """ + ) + + conn_data.execute( + """ + CREATE TABLE IF NOT EXISTS legs_to_station_destination + ( + dia text, + id_legs int not null, + id_station int not null + ) + ; + """ + ) + + conn_data.execute( + """ + CREATE TABLE IF NOT EXISTS travel_times_gps + ( + dia text, + id int not null, + travel_time_min float, + travel_speed float + ) + ; + """ + ) + conn_data.execute( + """ + CREATE INDEX IF NOT EXISTS travel_times_gps_idx ON travel_times_gps ( + "id" + ); + """ + ) + + conn_data.execute( + """ + CREATE TABLE IF NOT EXISTS travel_times_stations + ( + dia text, + id int not null, + travel_time_min float, + travel_speed float + ) + ; + """ + ) + conn_data.execute( + """ + CREATE INDEX IF NOT EXISTS travel_times_ts_idx ON travel_times_stations ( + "id" + ); + """ + ) + + conn_data.execute( + """ + CREATE TABLE IF NOT EXISTS travel_times_legs + ( + dia text, + id int not null, + id_etapa int, + id_viaje int, + id_tarjeta text, + travel_time_min float + ) + ; + """ + ) + + conn_data.execute( + """ + CREATE TABLE IF NOT EXISTS travel_times_trips + ( + dia text, + id_tarjeta text, + id_viaje int, + travel_time_min float ) ; """ @@ -574,10 +842,10 @@ def leer_configs_generales(): path = os.path.join("configs", "configuraciones_generales.yaml") try: - with open(path, 'r', encoding="utf8") as file: + with open(path, "r", encoding="utf8") as file: config = yaml.safe_load(file) except yaml.YAMLError as error: - print(f'Error al leer el archivo de configuracion: {error}') + print(f"Error al leer el archivo de configuracion: {error}") config = {} return config @@ -587,7 +855,7 @@ def crear_tablas_geolocalizacion(): """Esta funcion crea la tablas en la db para albergar los datos de gps y transacciones economicas sin latlong""" - conn_data = iniciar_conexion_db(tipo='data') + conn_data = iniciar_conexion_db(tipo="data") conn_data.execute( """ @@ -605,6 +873,8 @@ def crear_tablas_geolocalizacion(): id_ramal int, interno int, orden int, + genero text, + tarifa text, factor_expansion float ) ; @@ -614,7 +884,7 @@ def crear_tablas_geolocalizacion(): conn_data.execute( """ CREATE INDEX IF NOT EXISTS trx_idx_r ON trx_eco ( - "id_linea","id_ramal","interno","fecha" + "dia","id_linea","id_ramal","interno","fecha" ); """ ) @@ -622,7 +892,7 @@ def crear_tablas_geolocalizacion(): conn_data.execute( """ CREATE INDEX IF NOT EXISTS gps_idx_r ON gps ( - "id_linea","id_ramal","interno","fecha" + "dia","id_linea","id_ramal","interno","fecha" ); """ ) @@ -630,7 +900,7 @@ def crear_tablas_geolocalizacion(): conn_data.execute( """ CREATE INDEX IF NOT EXISTS trx_idx_l ON trx_eco ( - "id_linea","interno","fecha" + "dia","id_linea","interno","fecha" ); """ ) @@ -638,7 +908,7 @@ def crear_tablas_geolocalizacion(): conn_data.execute( """ CREATE INDEX IF NOT EXISTS gps_idx_l ON gps ( - "id_linea","interno","fecha" + "dia","id_linea","interno","fecha" ); """ ) @@ -647,7 +917,7 @@ def crear_tablas_geolocalizacion(): def create_gps_table(): - conn_data = iniciar_conexion_db(tipo='data') + conn_data = iniciar_conexion_db(tipo="data") conn_data.execute( """ @@ -659,7 +929,7 @@ def create_gps_table(): id_linea int, id_ramal int, interno int, - fecha datetime, + fecha int, latitud FLOAT, longitud FLOAT, velocity float, @@ -676,6 +946,10 @@ def create_gps_table(): CREATE TABLE IF NOT EXISTS services_gps_points ( id INT PRIMARY KEY NOT NULL, + id_linea int not null, + id_ramal int, + interno int, + dia text, original_service_id int not null, new_service_id int not null, service_id int not null, @@ -727,24 +1001,40 @@ def create_gps_table(): """ ) + conn_data.execute( + """ + CREATE TABLE IF NOT EXISTS vehicle_expansion_factors + ( + id_linea int, + dia text, + unique_vehicles int, + broken_gps_veh int, + veh_exp float + ) + ; + """ + ) + conn_data.commit() conn_data.close() -def agrego_indicador(df_indicador, - detalle, - tabla, - nivel=0, - var='indicador', - var_fex='factor_expansion_linea', - aggfunc='sum'): - ''' +def agrego_indicador( + df_indicador, + detalle, + tabla, + nivel=0, + var="indicador", + var_fex="factor_expansion_linea", + aggfunc="sum", +): + """ Agrego indicadores de tablas utilizadas - ''' + """ df = df_indicador.copy() - conn_data = iniciar_conexion_db(tipo='data') + conn_data = iniciar_conexion_db(tipo="data") try: indicadores = pd.read_sql_query( @@ -755,7 +1045,7 @@ def agrego_indicador(df_indicador, conn_data, ) except DatabaseError as e: - print("No existe la tabla indicadores, construyendola...") + print("No existe la tabla indicadores, construyendola...", e) indicadores = pd.DataFrame([]) if var not in df.columns: @@ -764,76 +1054,95 @@ def agrego_indicador(df_indicador, else: df[var] = df[var_fex] - if var != 'indicador': - df = df.rename(columns={var: 'indicador'}) + if var != "indicador": + df = df.rename(columns={var: "indicador"}) df = df[(df.indicador.notna())].copy() - if (not var_fex) | (aggfunc == 'sum'): - resultado = df.groupby('dia', as_index=False).agg( - {'indicador': aggfunc}).round(2) + if (not var_fex) | (aggfunc == "sum"): + resultado = ( + df.groupby("dia", as_index=False).agg({"indicador": aggfunc}).round(2) + ) - elif aggfunc == 'mean': - resultado = df.groupby('dia')\ - .apply(lambda x: np.average(x['indicador'], weights=x[var_fex]))\ - .reset_index()\ - .rename(columns={0: 'indicador'})\ + elif aggfunc == "mean": + resultado = ( + df.groupby("dia") + .apply(lambda x: np.average(x["indicador"], weights=x[var_fex])) + .reset_index() + .rename(columns={0: "indicador"}) .round(2) - elif aggfunc == 'median': - resultado = df.groupby('dia')\ + ) + + elif aggfunc == "median": + resultado = ( + df.groupby("dia") .apply( lambda x: ws.weighted_median( - x['indicador'].tolist(), - weights=x[var_fex].tolist()))\ - .reset_index()\ - .rename(columns={0: 'indicador'})\ + x["indicador"].tolist(), weights=x[var_fex].tolist() + ) + ) + .reset_index() + .rename(columns={0: "indicador"}) .round(2) + ) - resultado['detalle'] = detalle - resultado = resultado[['dia', 'detalle', 'indicador']] - resultado['tabla'] = tabla - resultado['nivel'] = nivel + resultado["detalle"] = detalle + resultado = resultado[["dia", "detalle", "indicador"]] + resultado["tabla"] = tabla + resultado["nivel"] = nivel if len(indicadores) > 0: - indicadores = indicadores[~( - (indicadores.dia.isin(resultado.dia.unique())) & - (indicadores.detalle == detalle) & - (indicadores.tabla == tabla) - )] - - indicadores = pd.concat([indicadores, - resultado], - ignore_index=True) + indicadores = indicadores[ + ~( + (indicadores.dia.isin(resultado.dia.unique())) + & (indicadores.detalle == detalle) + & (indicadores.tabla == tabla) + ) + ] + + indicadores = pd.concat([indicadores, resultado], ignore_index=True) if nivel > 0: - for i in indicadores[(indicadores.tabla == tabla) & - (indicadores.nivel == nivel)].dia.unique(): - for x in indicadores.loc[(indicadores.tabla == tabla) & - (indicadores.nivel == nivel) & - (indicadores.dia == i), 'detalle']: + for i in indicadores[ + (indicadores.tabla == tabla) & (indicadores.nivel == nivel) + ].dia.unique(): + for x in indicadores.loc[ + (indicadores.tabla == tabla) + & (indicadores.nivel == nivel) + & (indicadores.dia == i), + "detalle", + ]: valores = round( - indicadores.loc[(indicadores.tabla == tabla) & - (indicadores.nivel == nivel) & - (indicadores.dia == i) & - (indicadores.detalle == x), - 'indicador'].values[0] / indicadores.loc[ - (indicadores.tabla == tabla) & - (indicadores.nivel == nivel-1) & - (indicadores.dia == i), - 'indicador'].values[0] * 100, 1) - indicadores.loc[(indicadores.tabla == tabla) & - (indicadores.nivel == nivel) & - (indicadores.dia == i) & - (indicadores.detalle == x), - 'porcentaje'] = valores + indicadores.loc[ + (indicadores.tabla == tabla) + & (indicadores.nivel == nivel) + & (indicadores.dia == i) + & (indicadores.detalle == x), + "indicador", + ].values[0] + / indicadores.loc[ + (indicadores.tabla == tabla) + & (indicadores.nivel == nivel - 1) + & (indicadores.dia == i), + "indicador", + ].values[0] + * 100, + 1, + ) + indicadores.loc[ + (indicadores.tabla == tabla) + & (indicadores.nivel == nivel) + & (indicadores.dia == i) + & (indicadores.detalle == x), + "porcentaje", + ] = valores indicadores.fillna(0, inplace=True) - indicadores.to_sql("indicadores", conn_data, - if_exists="replace", index=False) + indicadores.to_sql("indicadores", conn_data, if_exists="replace", index=False) conn_data.close() -@ duracion +@duracion def eliminar_tarjetas_trx_unica(trx): """ Esta funcion toma el DF de trx y elimina las trx de una tarjeta con @@ -848,9 +1157,9 @@ def eliminar_tarjetas_trx_unica(trx): ) pre = len(trx) - trx = trx.merge(tarjetas_dia_multiples, - on=['dia', 'id_tarjeta'], - how='inner').drop('size', axis=1) + trx = trx.merge(tarjetas_dia_multiples, on=["dia", "id_tarjeta"], how="inner").drop( + "size", axis=1 + ) post = len(trx) print(pre - post, "casos elminados por trx unicas en el dia") return trx @@ -861,8 +1170,7 @@ def create_kpi_tables(): Creates KPI tables in the data db """ - conn_data = iniciar_conexion_db(tipo='data') - conn_dash = iniciar_conexion_db(tipo='dash') + conn_data = iniciar_conexion_db(tipo="data") conn_data.execute( """ @@ -919,18 +1227,6 @@ def create_kpi_tables(): ; """ ) - conn_dash.execute( - """ - CREATE TABLE IF NOT EXISTS services_by_line_hour - ( - id_linea int not null, - dia text not null, - hora int not null, - servicios float not null - ) - ; - """ - ) conn_data.execute( """ @@ -955,6 +1251,7 @@ def create_kpi_tables(): CREATE TABLE IF NOT EXISTS basic_kpi_by_line_hr ( dia text not null, + yr_mo text, id_linea int not null, hora int not null, veh float, @@ -972,6 +1269,7 @@ def create_kpi_tables(): CREATE TABLE IF NOT EXISTS basic_kpi_by_line_day ( dia text not null, + yr_mo text, id_linea int not null, veh float, pax float, @@ -983,26 +1281,25 @@ def create_kpi_tables(): """ ) - conn_dash.execute( + conn_data.execute( + """ + CREATE TABLE IF NOT EXISTS lines_od_matrix_by_section + (id_linea int not null, + yr_mo text, + day_type text nor null, + n_sections int, + hour_min int, + hour_max int, + section_id_o int not null, + section_id_d int not null, + legs int not null, + prop float not null + ) + ; """ - CREATE TABLE IF NOT EXISTS basic_kpi_by_line_hr - ( - dia text not null, - id_linea int not null, - nombre_linea text, - hora int not null, - veh float, - pax float, - dmt float, - of float, - speed_kmh float - ) - ; - """ ) conn_data.close() - conn_dash.close() def check_table_in_db(table_name, tipo_db): @@ -1036,3 +1333,165 @@ def check_table_in_db(table_name, tipo_db): return False else: return True + + +def is_date_string(input_str): + pattern = re.compile(r"^\d{4}-\d{2}-\d{2}$") + if pattern.match(input_str): + return True + else: + return False + + +def check_date_type(day_type): + """Checks if a day_type param is formated in the right way""" + day_type_is_a_date = is_date_string(day_type) + + # check day type format + day_type_format_ok = (day_type in ["weekday", "weekend"]) or day_type_is_a_date + + if not day_type_format_ok: + raise Exception("dat_type debe ser `weekday`, `weekend` o fecha 'YYYY-MM-DD'") + + +def create_line_ids_sql_filter(line_ids): + """ + Takes a set of line ids and returns a where clause + to filter in sqlite + """ + if line_ids is not None: + if isinstance(line_ids, int): + line_ids = [line_ids] + lines_str = ",".join(map(str, line_ids)) + line_ids_where = f" where id_linea in ({lines_str})" + + else: + lines_str = "" + line_ids_where = " where id_linea is not NULL" + return line_ids_where + + +def traigo_tabla_zonas(): + + conn_insumos = iniciar_conexion_db(tipo="insumos") + + zonas = pd.read_sql_query( + """ + SELECT * from zonas + """, + conn_insumos, + ) + zonas_cols = [ + i for i in zonas.columns if i not in ["h3", "fex", "latitud", "longitud"] + ] + return zonas, zonas_cols + + +def normalize_vars(tabla): + if "dia" in tabla.columns: + tabla.loc[tabla.dia == "weekday", "dia"] = "Día hábil" + tabla.loc[tabla.dia == "weekend", "dia"] = "Fin de semana" + if "day_type" in tabla.columns: + tabla.loc[tabla.day_type == "weekday", "day_type"] = "Día hábil" + tabla.loc[tabla.day_type == "weekend", "day_type"] = "Fin de semana" + + if "nombre_linea" in tabla.columns: + tabla["nombre_linea"] = tabla["nombre_linea"].str.replace(" -", "") + if "Modo" in tabla.columns: + tabla["Modo"] = tabla["Modo"].str.capitalize() + if "modo" in tabla.columns: + tabla["modo"] = tabla["modo"].str.capitalize() + return tabla + + +def levanto_tabla_sql(tabla_sql, tabla_tipo="dash"): + + conn = iniciar_conexion_db(tipo=tabla_tipo) + + try: + tabla = pd.read_sql_query( + f""" + SELECT * + FROM {tabla_sql} + """, + conn, + ) + except: + print(f"{tabla_sql} no existe") + tabla = pd.DataFrame([]) + + conn.close() + + if len(tabla) > 0: + + if "wkt" in tabla.columns: + tabla["geometry"] = tabla.wkt.apply(wkt.loads) + tabla = gpd.GeoDataFrame(tabla, crs=4326) + tabla = tabla.drop(["wkt"], axis=1) + + tabla = normalize_vars(tabla) + + return tabla + + +def calculate_weighted_means( + df_, + aggregate_cols, + weighted_mean_cols, + weight_col, + zero_to_nan=[], + var_fex_summed=True, +): + df = df_.copy() + for i in zero_to_nan: + df.loc[df[i] == 0, i] = np.nan + + calculate_weighted_means # Validate inputs + if not set(aggregate_cols + weighted_mean_cols + [weight_col]).issubset(df.columns): + raise ValueError("One or more columns specified do not exist in the DataFrame.") + result = pd.DataFrame([]) + # Calculate the product of the value and its weight for weighted mean calculation + for col in weighted_mean_cols: + df.loc[df[col].notna(), f"{col}_weighted"] = ( + df.loc[df[col].notna(), col] * df.loc[df[col].notna(), weight_col] + ) + grouped = ( + df.loc[df[col].notna()] + .groupby(aggregate_cols, as_index=False)[[f"{col}_weighted", weight_col]] + .sum() + ) + grouped[col] = grouped[f"{col}_weighted"] / grouped[weight_col] + grouped = grouped.drop([f"{col}_weighted", weight_col], axis=1) + + if len(result) == 0: + result = grouped.copy() + else: + result = result.merge(grouped, how="left", on=aggregate_cols) + + if var_fex_summed: + fex_summed = df.groupby(aggregate_cols, as_index=False)[weight_col].sum() + result = result.merge(fex_summed, how="left", on=aggregate_cols) + else: + fex_mean = df.groupby(aggregate_cols, as_index=False)[weight_col].mean() + result = result.merge(fex_mean, how="left", on=aggregate_cols) + + return result + + +def delete_data_from_table_run_days(table_name): + + conn_data = iniciar_conexion_db(tipo="data") + + dias_ultima_corrida = pd.read_sql_query( + """ + SELECT * + FROM dias_ultima_corrida + """, + conn_data, + ) + # delete data from same day if exists + values = ", ".join([f"'{val}'" for val in dias_ultima_corrida["dia"]]) + query = f"DELETE FROM {table_name} WHERE dia IN ({values})" + conn_data.execute(query) + conn_data.commit() + conn_data.close() diff --git a/urbantrips/viz/line_od_matrix.py b/urbantrips/viz/line_od_matrix.py new file mode 100755 index 0000000..e4de3cb --- /dev/null +++ b/urbantrips/viz/line_od_matrix.py @@ -0,0 +1,552 @@ +import folium +import mapclassify +import os +import pandas as pd +import geopandas as gpd +from requests.exceptions import ConnectionError as r_ConnectionError +from PIL import UnidentifiedImageError +import seaborn as sns +import matplotlib.pyplot as plt +import contextily as cx + +from urbantrips.viz.viz import ( + create_squared_polygon, + crear_linestring, + get_branch_geoms_from_line, + extract_hex_colors_from_cmap +) + + +from urbantrips.kpi.line_od_matrix import delete_old_lines_od_matrix_by_section_data + +from urbantrips.utils.utils import ( + iniciar_conexion_db, + create_line_ids_sql_filter, + leer_alias +) +from urbantrips.geo import geo + + +def visualize_lines_od_matrix(line_ids=None, hour_range=False, + day_type='weekday', + n_sections=10, section_meters=None, + stat='totals'): + """ + Visualize od matriz for a given set of lines using route sections + + Parameters + ---------- + line_ids : int, list of ints or bool + route id present in the ocupacion_por_linea_tramo table. + hour_range : tuple or bool + tuple holding hourly range (from,to) and from 0 to 24. + day_type: str + type of day. It can take `weekday`, `weekend` or a specific + day in format 'YYYY-MM-DD' + n_sections: int + number of sections to split the route geom + section_meters: int + section lenght in meters to split the route geom. If specified, + this will be used instead of n_sections. + stat: str + Tipe of section load to display. 'totals' (amount of legs) + or `proportion` (proportion of legs) + """ + + sns.set_style("whitegrid") + # Download line data + od_lines = get_lines_od_matrix_data( + line_ids, hour_range, day_type, n_sections, section_meters) + + if od_lines is not None: + # Viz data + od_lines.groupby(['id_linea', 'yr_mo']).apply( + viz_line_od_matrix, + stat=stat + ) + od_lines.groupby(['id_linea', 'yr_mo']).apply( + map_desire_lines) + else: + print("No hay datos de matriz od para esas lineas con esos parametros") + + +def get_lines_od_matrix_data(line_ids, hour_range=False, + day_type='weekday', + n_sections=10, section_meters=None): + + q = """ + select * from lines_od_matrix_by_section + """ + line_ids_where = create_line_ids_sql_filter(line_ids) + q += line_ids_where + + # hour range filter + if hour_range: + hora_min_filter = f"= {hour_range[0]}" + hora_max_filter = f"= {hour_range[1]}" + else: + hora_min_filter = "is NULL" + hora_max_filter = "is NULL" + + hour_where = f""" + and hour_min {hora_min_filter} + and hour_max {hora_max_filter} + """ + q += hour_where + q += f"and day_type = '{day_type}'" + + conn_data = iniciar_conexion_db(tipo="data") + od_lines = pd.read_sql(q, conn_data) + conn_data.close() + + if section_meters is not None: + line_sections = get_route_n_sections_from_sections_meters( + line_ids, section_meters) + od_lines = od_lines.merge(line_sections, + on=['id_linea', 'n_sections'], + how='inner') + else: + od_lines = od_lines.query(f"n_sections == {n_sections}") + + line_ids_to_check = pd.Series(line_ids) + line_ids_in_data = od_lines.id_linea.drop_duplicates() + check_line_ids_in_data = line_ids_to_check.isin(line_ids_in_data) + + if not check_line_ids_in_data.all(): + print("Las siguientes líneas no fueron procesadas con matriz od lineas") + print(', '.join(line_ids_to_check[~check_line_ids_in_data].map(str))) + print("para esos parámetros de n_sections o section_meters") + print("Volver a correr compute_lines_od_matrix() con otro parámetros") + + if len(od_lines) == 0: + print("La consulta para estos id_lineas con estos parametros" + " volvio vacía") + return None + else: + return od_lines + + +def get_route_n_sections_from_sections_meters(line_ids, section_meters): + """ + For a given section meters param, returns how many sections there is + in that line route geom + + Parameters + ---------- + line_ids : int, list of ints or bool + route id or list of route ids present in the legs dataset. Route + section load will be computed for that subset of lines. If False, it + will run with all routes. + + section_meters: int + section lenght in meters to split the route geom. If specified, + this will be used instead of n_sections. + + Returns + ---------- + pandas.DataFrame + df with line id and n sections + + """ + conn_insumos = iniciar_conexion_db(tipo="insumos") + line_ids_where = create_line_ids_sql_filter(line_ids) + q_route_geoms = "select * from lines_geoms" + line_ids_where + route_geoms = pd.read_sql(q_route_geoms, conn_insumos) + conn_insumos.close() + + route_geoms["geometry"] = gpd.GeoSeries.from_wkt(route_geoms.wkt) + epsg_m = geo.get_epsg_m() + route_geoms = gpd.GeoDataFrame( + route_geoms, geometry="geometry", crs="EPSG:4326" + ).to_crs(epsg=epsg_m) + + new_n_sections = ( + route_geoms.geometry.length / section_meters).astype(int) + route_geoms["n_sections"] = new_n_sections + route_geoms = route_geoms.reindex(columns=['id_linea', 'n_sections']) + + return route_geoms + + +def viz_line_od_matrix(od_line, stat='totals'): + """ + Creates viz for line od matrix + + Parameters + ---------- + od_line: pandas.DataFrame + table with od data for a given line + stat: str + Tipe of section load to display. 'totals' (amount of legs) + or `proportion` (proportion of legs) + + Returns + ------- + None + + """ + line_id = od_line.id_linea.unique()[0] + n_sections = od_line.n_sections.unique()[0] + mes = od_line.yr_mo.unique()[0] + total_legs = int(od_line.legs.sum()) + + # get data + sections_data_q = f""" + select * from routes_section_id_coords + where id_linea = {line_id} + and n_sections = {n_sections} + """ + conn_insumos = iniciar_conexion_db(tipo="insumos") + sections = pd.read_sql(sections_data_q, conn_insumos) + + s = "select nombre_linea from metadata_lineas" +\ + f" where id_linea = {line_id};" + metadata = pd.read_sql(s, conn_insumos) + conn_insumos.close() + + # set title params + if len(metadata) > 0: + line_str = metadata.nombre_linea.item() + else: + line_str = '' + + day = od_line['day_type'].unique().item() + + if day == 'weekend': + day_str = 'Fin de semana' + elif day == 'weekday': + day_str = 'Dia habil' + else: + day_str = day + + font_dicc = {'fontsize': 18, + # 'fontweight': 'bold' + } + + title = 'Matriz OD por segmento del recorrido' + if stat == 'totals': + title += ' - Cantidad de etapas' + values = 'legs' + + elif stat == 'proportion': + title += ' - Porcentaje de etapas totales' + values = 'prop' + else: + raise Exception( + "Indicador debe ser 'cantidad_etapas' o 'prop_etapas'") + + if not od_line.hour_min.isna().all(): + from_hr = od_line.hour_min.unique()[0] + to_hr = od_line.hour_max.unique()[0] + hr_str = f' {from_hr}-{to_hr} hrs' + hour_range = [from_hr, to_hr] + else: + hr_str = '' + hour_range = None + + title = title + hr_str + ' - ' + day_str + '-' + mes + \ + '-' + f" {line_str} (id_linea: {line_id})" + + # upload to dash db + od_line_dash = od_line.copy() + od_line_dash['nombre_linea'] = line_str + od_line_dash = od_line_dash.rename(columns={ + 'section_id_o': 'Origen', + 'section_id_d': 'Destino' + }) + + conn_dash = iniciar_conexion_db(tipo='dash') + delete_df = od_line\ + .reindex(columns=['id_linea', 'n_sections'])\ + .drop_duplicates() + + delete_old_lines_od_matrix_by_section_data( + delete_df, hour_range=hour_range, + day_type=day, yr_mos=[mes], + db_type='dash') + + od_line_dash.to_sql("matrices_linea", conn_dash, + if_exists="append", index=False) + + matrix = od_line.pivot_table(values=values, + index='section_id_o', + columns='section_id_d') + + # produce carto + epsg = geo.get_epsg_m() + section_id_start = 1 + section_id_end = sections.section_id.max() + section_id_middle = int(section_id_end/2) + + gdf = geo.create_sections_geoms(sections, buffer_meters=250) + + # upload sections carto to dash + gdf_dash = gdf.to_crs(epsg=4326).copy() + gdf_dash['wkt'] = gdf_dash.geometry.to_wkt() + gdf_dash['nombre_linea'] = line_str + gdf_dash = gdf_dash.reindex(columns=[ + 'id_linea', + 'n_sections', + 'section_id', + 'wkt', + 'x', + 'y', + 'nombre_linea' + ]) + q_delete = f""" + delete from matrices_linea_carto + where id_linea = {line_id} + and n_sections = {n_sections} + """ + cur = conn_dash.cursor() + cur.execute(q_delete) + conn_dash.commit() + + gdf_dash.to_sql("matrices_linea_carto", conn_dash, + if_exists="append", index=False) + + conn_dash.close() + + # set sections to show in map + section_id_start_xy = gdf.loc[gdf.section_id == + section_id_start, 'geometry'].centroid.item().coords.xy + section_id_start_xy = section_id_start_xy[0][0], section_id_start_xy[1][0] + + section_id_end_xy = gdf.loc[gdf.section_id == + section_id_end, 'geometry'].centroid.item().coords.xy + section_id_end_xy = section_id_end_xy[0][0], section_id_end_xy[1][0] + + section_id_middle_xy = gdf.loc[gdf.section_id == + section_id_middle, 'geometry'].centroid.item().coords.xy + section_id_middle_xy = section_id_middle_xy[0][0], section_id_middle_xy[1][0] + + # create figure + f, (ax1, ax2) = plt.subplots(tight_layout=True, figsize=(24, 10), ncols=2) + + minx, miny, maxx, maxy = gdf.total_bounds + box = create_squared_polygon(minx, miny, maxx, maxy, epsg) + + gdf.plot(ax=ax1, alpha=0.5) + box.plot(ax=ax1, color='#ffffff00') + + sns.heatmap(matrix, cmap='Blues', ax=ax2, annot=True, fmt='g') + + ax1.set_axis_off() + ax2.grid(False) + + prov = cx.providers.CartoDB.Positron + try: + cx.add_basemap(ax1, crs=gdf.crs.to_string(), source=prov) + except (UnidentifiedImageError, ValueError): + cx.add_basemap(ax1, crs=gdf.crs.to_string()) + except r_ConnectionError: + pass + + # Notes + total_legs = '{:,.0f}'.format(total_legs).replace(',', '.') + ax1.annotate(f'Total de etapas: {total_legs}', + xy=(0, -0.05), xycoords='axes fraction', + size=18) + ax1.annotate(f'{section_id_start}', xy=section_id_start_xy, + xytext=(-100, 0), + size=18, + textcoords='offset points', + bbox=dict(boxstyle="round", fc="#ffffff", ec="#888888"), + arrowprops=dict(arrowstyle="-", ec="#888888") + ) + + ax1.annotate(f'{section_id_middle}', xy=section_id_middle_xy, + xytext=(-100, 0), + size=18, + textcoords='offset points', + bbox=dict(boxstyle="round", fc="#ffffff", ec="#888888"), + arrowprops=dict(arrowstyle="-", ec="#888888") + ) + + ax1.annotate(f'{section_id_end}', xy=section_id_end_xy, + xytext=(-100, 0), + size=18, + textcoords='offset points', + bbox=dict(boxstyle="round", fc="#ffffff", ec="#888888"), + arrowprops=dict(arrowstyle="-", ec="#888888") + ) + # Labels + f.suptitle(title, fontsize=18) + + ax1.set_title('Cartografía segmentos', fontdict=font_dicc, + # y=1.0, pad=-20 + ) + ax2.set_title('Matriz OD', fontdict=font_dicc, + # y=1.0, pad=-20 + ) + + ax2.set_ylabel('Origenes') + ax2.set_xlabel('Destinos') + + minor_sections = sections[~sections.section_id.isin( + [section_id_start, section_id_middle, section_id_end, -1])] + major_sections = sections[sections.section_id.isin( + [section_id_start, section_id_middle, section_id_end])] + + ax2.set_yticks(minor_sections.index + 0.5, minor=True) + ax2.set_yticklabels(minor_sections.section_id.to_list(), minor=True) + ax2.set_yticks(major_sections.index + 0.5, minor=False) + ax2.set_yticklabels(major_sections.section_id.to_list(), + minor=False, size=16) + + ax2.set_xticks(minor_sections.index + 0.5, minor=True) + ax2.set_xticklabels(minor_sections.section_id.to_list(), minor=True) + ax2.set_xticks(major_sections.index + 0.5, minor=False) + ax2.set_xticklabels(major_sections.section_id.to_list(), + minor=False, size=16) + + alias = leer_alias() + + for frm in ['png', 'pdf']: + archivo = f"{alias}_{mes}({day_str})_matriz_od_id_linea_" + archivo = archivo+f"{line_id}_{n_sections}_{stat}_{hr_str}.{frm}" + db_path = os.path.join("resultados", frm, archivo) + f.savefig(db_path, dpi=300) + plt.close(f) + + +def map_desire_lines(od_line): + """ + Creates viz for line od matrix + + Parameters + ---------- + od_line: pandas.DataFrame + table with od data for a given line + + Returns + ------- + None + + """ + + line_id = od_line.id_linea.unique()[0] + n_sections = od_line.n_sections.unique()[0] + mes = od_line.yr_mo.unique()[0] + day = od_line['day_type'].unique().item() + + if day == 'weekend': + day_str = 'Fin de semana' + elif day == 'weekday': + day_str = 'Dia habil' + else: + day_str = day + + if not od_line.hour_min.isna().all(): + from_hr = od_line.hour_min.unique()[0] + to_hr = od_line.hour_max.unique()[0] + hr_str = f' {from_hr}-{to_hr} hrs' + else: + hr_str = '' + + # get data + sections_data_q = f""" + select id_linea,n_sections,section_id,x,y from routes_section_id_coords + where id_linea = {line_id} + and n_sections = {n_sections} + """ + conn_insumos = iniciar_conexion_db(tipo="insumos") + sections = pd.read_sql(sections_data_q, conn_insumos) + conn_insumos.close() + + sections = geo.create_sections_geoms(sections, buffer_meters=250) + sections = sections.to_crs(epsg=4326) + section_centroids = sections.geometry.centroid + sections['x'] = section_centroids.x + sections['y'] = section_centroids.y + sections_merge = sections.drop('geometry', axis=1) + + od_line = od_line\ + .merge( + sections_merge.rename( + columns={'x': 'lon_o', 'y': 'lat_o', 'section_id': 'section_id_o'}), + on=['id_linea', 'n_sections', 'section_id_o'], + how='left')\ + .merge( + sections_merge.rename( + columns={'x': 'lon_d', 'y': 'lat_d', 'section_id': 'section_id_d'}), + on=['id_linea', 'n_sections', 'section_id_d'], + how='left') + + od_line = crear_linestring(od_line, 'lon_o', 'lat_o', 'lon_d', 'lat_d') + + alias = leer_alias() + + file_name = f"{alias}_{mes}({day_str})_matriz_od_id_linea_" + file_name = file_name+f"{line_id}_{hr_str}_{n_sections}_secciones" + for k_jenks in range(1, 6)[::-1]: + try: + create_folium_desire_lines( + od_line, + cmap="Blues", + var_fex="legs", + savefile=f"{file_name}.html", + sections_gdf=sections, + k_jenks=k_jenks, + ) + break + except ValueError: + continue + + +def create_folium_desire_lines(od_line, + cmap, + var_fex, + savefile, + sections_gdf=None, + k_jenks=5): + + bins = [od_line[var_fex].min()-1] + \ + mapclassify.FisherJenks(od_line[var_fex], k=k_jenks).bins.tolist() + + range_bins = range(0, len(bins)-1) + bins_labels = [ + f'{int(bins[n])} a {int(bins[n+1])} viajes' for n in range_bins] + od_line['cuts'] = pd.cut(od_line[var_fex], bins=bins, labels=bins_labels) + + fig = folium.Figure(width=800, height=800) + m = folium.Map(location=[od_line.lat_o.mean( + ), od_line.lon_o.mean()], zoom_start=9, tiles='cartodbpositron') + + # map branches geoms + branch_geoms = get_branch_geoms_from_line( + id_linea=od_line.id_linea.unique().item()) + + if branch_geoms is not None: + branch_geoms.explore(m=m, name='Ramales', + style_kwds=dict( + color="black", opacity=0.4, dashArray='10'), + ) + if sections_gdf is not None: + sections_gdf.explore(m=m, name='Secciones', + style_kwds=dict( + color="#888888", opacity=0.9), + ) + line_w = 0.5 + + colors = extract_hex_colors_from_cmap(cmap=cmap, n=k_jenks) + + n = 0 + for i in bins_labels: + + od_line[od_line.cuts == i].explore( + m=m, + color=colors[n], + style_kwds={'fillOpacity': 0.3, 'weight': line_w}, + name=i, + tooltip=False, + ) + n += 1 + line_w += 3 + + folium.LayerControl(name='Legs').add_to(m) + + fig.add_child(m) + + db_path = os.path.join("resultados", "html", savefile) + m.save(db_path) diff --git a/urbantrips/viz/viz.py b/urbantrips/viz/viz.py old mode 100644 new mode 100755 index f082b1c..4b3bcc4 --- a/urbantrips/viz/viz.py +++ b/urbantrips/viz/viz.py @@ -1,3 +1,5 @@ +import json +import shapely import pandas as pd import numpy as np import os @@ -24,16 +26,16 @@ from pandas.io.sql import DatabaseError from urbantrips.kpi import kpi -from urbantrips.carto import carto from urbantrips.geo import geo -from urbantrips.geo.geo import ( - normalizo_lat_lon, crear_linestring) +from urbantrips.geo.geo import normalizo_lat_lon, crear_linestring from urbantrips.utils.utils import ( leer_configs_generales, traigo_db_path, iniciar_conexion_db, leer_alias, - duracion) + duracion, + create_line_ids_sql_filter, +) import warnings warnings.filterwarnings("ignore") @@ -51,14 +53,13 @@ def plotear_recorrido_lowess(id_linea, etapas, recorridos_lowess, alias): try: fig, ax = plt.subplots(figsize=(3, 3), dpi=150) - ax.scatter(e.longitud, e.latitud, color='orange', s=.3) - r.plot(color='black', lw=.8, legend=False, ax=ax) + ax.scatter(e.longitud, e.latitud, color="orange", s=0.3) + r.plot(color="black", lw=0.8, legend=False, ax=ax) - ax.set_title(f'Linea {id_linea}', fontsize=6) - ax.axis('off') + ax.set_title(f"Linea {id_linea}", fontsize=6) + ax.axis("off") - db_path = os.path.join("resultados", "png", - f"{alias}linea_{id_linea}.png") + db_path = os.path.join("resultados", "png", f"{alias}linea_{id_linea}.png") fig.savefig(db_path, dpi=300, bbox_inches="tight") plt.close(fig) @@ -70,20 +71,25 @@ def plotear_recorrido_lowess(id_linea, etapas, recorridos_lowess, alias): @duracion -def visualize_route_section_load(id_linea=False, rango_hrs=False, - day_type='weekday', - n_sections=10, section_meters=None, - indicador='cantidad_etapas', factor=1, - factor_min=50, - save_gdf=False): +def visualize_route_section_load( + line_ids=False, + hour_range=False, + day_type="weekday", + n_sections=10, + section_meters=None, + stat="totals", + factor=1, + factor_min=50, + save_gdf=False, +): """ Visualize the load per route section data per route Parameters ---------- - id_linea : int, list of ints or bool + line_ids : int, list of ints or bool route id present in the ocupacion_por_linea_tramo table. - rango_hrs : tuple or bool + hour_range : tuple or bool tuple holding hourly range (from,to) and from 0 to 24. day_type: str type of day. It can take `weekday`, `weekend` or a specific @@ -93,9 +99,9 @@ def visualize_route_section_load(id_linea=False, rango_hrs=False, section_meters: int section lenght in meters to split the route geom. If specified, this will be used instead of n_sections. - indicator: str - Tipe of section load to display. 'cantidad_etapas' (amount of legs) - or `prop_etapas` (proportion of legs) + stat: str + Tipe of section load to display. 'totals' (amount of legs) + or `proportion` (proportion of legs) factor: int scaling factor to use for line width to plot section load factor_min: int @@ -104,22 +110,18 @@ def visualize_route_section_load(id_linea=False, rango_hrs=False, """ sns.set_style("whitegrid") - if id_linea: - - if type(id_linea) == int: - id_linea = [id_linea] - - table = get_route_section_load( - id_linea=id_linea, - rango_hrs=rango_hrs, + section_load_data = get_route_section_load( + line_ids=line_ids, + hour_range=hour_range, day_type=day_type, n_sections=n_sections, - section_meters=section_meters) + section_meters=section_meters, + ) # Create a viz for each route - table.groupby('id_linea').apply( + section_load_data.groupby(["id_linea", "yr_mo"]).apply( viz_etapas_x_tramo_recorrido, - indicator=indicador, + stat=stat, factor=factor, factor_min=factor_min, return_gdfs=False, @@ -127,17 +129,26 @@ def visualize_route_section_load(id_linea=False, rango_hrs=False, ) -def get_route_section_load(id_linea=False, rango_hrs=False, day_type='weekday', - n_sections=10, section_meters=None,): +def get_route_section_load( + line_ids=False, + hour_range=False, + day_type="weekday", + n_sections=10, + section_meters=None, +): """ Get the load per route section data Parameters ---------- - id_linea : int, list of ints or bool - route id present in the ocupacion_por_linea_tramo table. - rango_hrs : tuple or bool - tuple holding hourly range (from,to) and from 0 to 24. + line_ids : int, list of ints or bool + route id or list of route ids present in the legs dataset. Route + section load will be computed for that subset of lines. If False, it + will run with all routes. + hour_range : tuple or bool + tuple holding hourly range (from,to) and from 0 to 24. Route section + load will be computed for legs happening within tat time range. + If False it won't filter by hour. day_type: str type of day. It can take `weekday`, `weekend` or a specific day in format 'YYYY-MM-DD' @@ -149,47 +160,45 @@ def get_route_section_load(id_linea=False, rango_hrs=False, day_type='weekday', Returns ------- - table : pandas.Data.Frame + pandas.Data.Frame dataframe with load per section per route - - recorridos : geopandas.GeoDataFrame - geodataframe with route geoms - """ - conn_data = iniciar_conexion_db(tipo='data') + conn_data = iniciar_conexion_db(tipo="data") - # route id filter - if id_linea: - - if type(id_linea) == int: - id_linea = [id_linea] - - lineas_str = ",".join(map(str, id_linea)) - else: - lineas_str = '' - - # create query to get data from db q = load_route_section_load_data_q( - lineas_str, rango_hrs, n_sections, section_meters, day_type + line_ids=line_ids, + hour_range=hour_range, + day_type=day_type, + n_sections=n_sections, + section_meters=section_meters, ) # Read data from section load table - table = pd.read_sql(q, conn_data) + section_load_data = pd.read_sql(q, conn_data) + + conn_data.close() - if len(table) == 0: + if len(section_load_data) == 0: print("No hay datos de carga por tramo para estos parametros.") - print(" id_linea:", id_linea, - " rango_hrs:", rango_hrs, - " n_sections:", n_sections, - " section_meters:", section_meters, - " day_type:", day_type) + print( + " id_linea:", + line_ids, + " rango_hrs:", + hour_range, + " n_sections:", + n_sections, + " section_meters:", + section_meters, + " day_type:", + day_type, + ) - return table + return section_load_data def load_route_section_load_data_q( - lineas_str, rango_hrs, n_sections, section_meters, day_type + line_ids, hour_range, day_type, n_sections, section_meters ): """ Creates a query that gets route section load data from the db @@ -197,19 +206,22 @@ def load_route_section_load_data_q( Parameters ---------- - lineas_str : str - list of lines to query in a string format separated by comma - rango_hrs : tuple or bool - tuple holding hourly range (from,to) and from 0 to 24. + line_ids : int, list of ints or bool + route id or list of route ids present in the legs dataset. Route + section load will be computed for that subset of lines. If False, it + will run with all routes. + hour_range : tuple or bool + tuple holding hourly range (from,to) and from 0 to 24. Route section + load will be computed for legs happening within tat time range. + If False it won't filter by hour. day_type: str - type of day. It can take `weekday`, `weekend` or a specific - day in format 'YYYY-MM-DD' + type of day on which the section load is to be computed. It can take + `weekday`, `weekend` or a specific day in format 'YYYY-MM-DD' n_sections: int number of sections to split the route geom section_meters: int section lenght in meters to split the route geom. If specified, this will be used instead of n_sections. - Returns ------- str @@ -217,40 +229,44 @@ def load_route_section_load_data_q( """ + line_ids_where = create_line_ids_sql_filter(line_ids) + + q_main_data = """ + select * + from ocupacion_por_linea_tramo + """ + q_main_data = q_main_data + line_ids_where + # hour range filter - if rango_hrs: - hora_min_filter = f"= {rango_hrs[0]}" - hora_max_filter = f"= {rango_hrs[1]}" + if hour_range: + hora_min_filter = f"= {hour_range[0]}" + hora_max_filter = f"= {hour_range[1]}" else: hora_min_filter = "is NULL" hora_max_filter = "is NULL" - q = f""" - select * from ocupacion_por_linea_tramo - where hora_min {hora_min_filter} - and hora_max {hora_max_filter} + q_main_data = ( + q_main_data + + f""" + and hour_min {hora_min_filter} + and hour_max {hora_max_filter} and day_type = '{day_type}' """ - - if lineas_str != '': - q = q + f" and id_linea in ({lineas_str})" + ) if section_meters: - q = q + f" and section_meters = {section_meters}" + q_main_data = q_main_data + f" and section_meters = {section_meters}" else: - q = ( - q + - f" and n_sections = {n_sections} and section_meters is NULL" - ) - q = q + ";" - return q + q_main_data = q_main_data + f" and n_sections = {n_sections}" + + q_main_data = q_main_data + ";" + return q_main_data -def viz_etapas_x_tramo_recorrido(df, - indicator='cantidad_etapas', factor=1, - factor_min=50, return_gdfs=False, - save_gdf=False): +def viz_etapas_x_tramo_recorrido( + df, stat="totals", factor=1, factor_min=50, return_gdfs=False, save_gdf=False +): """ Plots and saves a section load viz for a given route @@ -260,9 +276,9 @@ def viz_etapas_x_tramo_recorrido(df, table for a given route in section load db table route geom: geopandas.GeoSeries route geoms with id_route as index - indicator: str - Tipe of section load to display. 'cantidad_etapas' (amount of legs) - or `prop_etapas` (proportion of legs) + stat: str + Tipe of section load to display. 'totals' (amount of legs) + or `proportion` (proportion of legs) factor: int scaling factor to use for line width to plot section load factor_min: int @@ -278,89 +294,118 @@ def viz_etapas_x_tramo_recorrido(df, gdf_d1 : geopandas.GeoDataFrame geodataframe with section load data and sections geoms. """ - conn_insumos = iniciar_conexion_db(tipo='insumos') + conn_insumos = iniciar_conexion_db(tipo="insumos") + + line_id = df.id_linea.unique().item() + n_sections = df.n_sections.unique().item() + mes = df.yr_mo.unique().item() + day = df["day_type"].unique().item() - id_linea = df.id_linea.unique()[0] - s = f"select nombre_linea from metadata_lineas" +\ - f" where id_linea = {id_linea};" + # get line name from metadata + s = f"select nombre_linea from metadata_lineas" + f" where id_linea = {line_id};" id_linea_str = pd.read_sql(s, conn_insumos) if len(id_linea_str) > 0: id_linea_str = id_linea_str.nombre_linea.item() else: - id_linea_str = '' - - day = df['day_type'].unique().item() + id_linea_str = "" - if day == 'weekend': - day_str = 'Fin de semana tipo' - elif day == 'weekday': - day_str = 'Dia de semana tipo' + # Turn day type into printable string + if day == "weekend": + day_str = "Fin de semana" + elif day == "weekday": + day_str = "Dia habil" else: day_str = day section_ids = df.section_id.unique() - print('Produciendo grafico de ocupacion por tramos', id_linea) + # Set title and plot axis + if stat == "totals": + title = "Segmentos del recorrido - Cantidad de etapas" + y_axis_lable = "Cantidad de etapas por sentido" + indicator_col = "legs" + elif stat == "proportion": + title = "Segmentos del recorrido - Porcentaje de etapas totales" + y_axis_lable = "Porcentaje del total de etapas" + indicator_col = "prop" + + else: + raise Exception("Indicador stat debe ser 'cantidad_etapas' o 'prop_etapas'") + + print("Produciendo grafico de ocupacion por tramos", line_id) # set a expansion factor for viz purposes - df['buff_factor'] = df[indicator]*factor + df["buff_factor"] = df[indicator_col] * factor # Set a minimum for each section to be displated in map - df['buff_factor'] = np.where( - df['buff_factor'] <= factor_min, factor_min, df['buff_factor']) - - cols = ['id_linea', 'day_type', 'n_sections', 'sentido', - 'section_id', 'hora_min', 'hora_max', 'cantidad_etapas', - 'prop_etapas', 'buff_factor'] + df["buff_factor"] = np.where( + df["buff_factor"] <= factor_min, factor_min, df["buff_factor"] + ) - df_d0 = df.loc[df.sentido == 'ida', cols] - df_d1 = df.loc[df.sentido == 'vuelta', cols] + cols = [ + "id_linea", + "yr_mo", + "day_type", + "n_sections", + "sentido", + "section_id", + "hour_min", + "hour_max", + "legs", + "prop", + "buff_factor", + ] - # Create geoms for route in both directions - df_geom = df.query("sentido == 'ida'")\ - .sort_values('section_id')\ - .reset_index(drop=True) + df_d0 = df.loc[df.sentido == "ida", cols] + df_d1 = df.loc[df.sentido == "vuelta", cols] - geom = [LineString( - [[df_geom.loc[i, 'x'], df_geom.loc[i, 'y']], - [df_geom.loc[i+1, 'x'], df_geom.loc[i+1, 'y']]] - ) for i in df_geom.index[:-1]] - gdf = gpd.GeoDataFrame(pd.DataFrame( - {'section_id': df_geom.section_id.iloc[:-1]}), - geometry=geom, crs='epsg:4326') + # get data + sections_geoms_q = f""" + select * from routes_section_id_coords + where id_linea = {line_id} + and n_sections = {n_sections} + """ + conn_insumos = iniciar_conexion_db(tipo="insumos") + sections_geoms = pd.read_sql(sections_geoms_q, conn_insumos) + sections_geoms = geo.create_sections_geoms(sections_geoms, buffer_meters=False) # Arrows - flecha_ida_wgs84 = gdf.loc[gdf.section_id == 0.0, 'geometry'] + flecha_ida_wgs84 = sections_geoms.loc[ + sections_geoms.section_id == sections_geoms.section_id.min(), "geometry" + ] flecha_ida_wgs84 = list(flecha_ida_wgs84.item().coords) flecha_ida_inicio_wgs84 = flecha_ida_wgs84[0] - flecha_ida_fin_wgs84 = flecha_ida_wgs84[1] - flecha_vuelta_wgs84 = gdf.loc[gdf.section_id == - max(gdf.section_id), 'geometry'] + flecha_vuelta_wgs84 = sections_geoms.loc[ + sections_geoms.section_id == max(sections_geoms.section_id), "geometry" + ] flecha_vuelta_wgs84 = list(flecha_vuelta_wgs84.item().coords) - flecha_vuelta_inicio_wgs84 = flecha_vuelta_wgs84[0] flecha_vuelta_fin_wgs84 = flecha_vuelta_wgs84[1] # Use a projected crs in meters epsg = geo.get_epsg_m() - gdf = gdf.to_crs(epsg=epsg) + sections_geoms = sections_geoms.to_crs(epsg=epsg) - gdf_d0 = gdf\ - .merge(df_d0, on='section_id', how='left')\ - .fillna(0) + gdf_d0 = sections_geoms.merge( + df_d0, on=["id_linea", "n_sections", "section_id"], how="left" + ) + gdf_d0.legs = gdf_d0.legs.fillna(0) + gdf_d0.prop = gdf_d0.prop.fillna(0) - gdf_d1 = gdf\ - .merge(df_d1, on='section_id', how='left')\ - .fillna(0) + gdf_d1 = sections_geoms.merge( + df_d1, on=["id_linea", "n_sections", "section_id"], how="left" + ) + gdf_d1.legs = gdf_d1.legs.fillna(0) + gdf_d1.prop = gdf_d1.prop.fillna(0) # save data for dashboard gdf_d0_dash = gdf_d0.to_crs(epsg=4326).copy() gdf_d1_dash = gdf_d1.to_crs(epsg=4326).copy() # creando buffers en base a - gdf_d0['geometry'] = gdf_d0.geometry.buffer(gdf_d0.buff_factor) - gdf_d1['geometry'] = gdf_d1.geometry.buffer(gdf_d1.buff_factor) + gdf_d0["geometry"] = gdf_d0.geometry.buffer(gdf_d0.buff_factor) + gdf_d1["geometry"] = gdf_d1.geometry.buffer(gdf_d1.buff_factor) # creating plot f = plt.figure(tight_layout=True, figsize=(20, 15)) @@ -370,63 +415,71 @@ def viz_etapas_x_tramo_recorrido(df, ax3 = f.add_subplot(gs[2, 0]) ax4 = f.add_subplot(gs[2, 1]) - font_dicc = {'fontsize': 18, - 'fontweight': 'bold'} + font_dicc = {"fontsize": 18, "fontweight": "bold"} # create a squared box minx, miny, maxx, maxy = gdf_d0.total_bounds box = create_squared_polygon(minx, miny, maxx, maxy, epsg) - box.plot(ax=ax1, color='#ffffff00') - box.plot(ax=ax2, color='#ffffff00') + box.plot(ax=ax1, color="#ffffff00") + box.plot(ax=ax2, color="#ffffff00") # get branches' geoms - branch_geoms = get_branch_geoms_from_line(id_linea=id_linea) + branch_geoms = get_branch_geoms_from_line(id_linea=line_id) if branch_geoms is not None: branch_geoms = branch_geoms.to_crs(epsg=epsg) - branch_geoms.plot(ax=ax1, color='Purple', - alpha=0.4, linestyle='dashed') - branch_geoms.plot(ax=ax2, color='Orange', - alpha=0.4, linestyle='dashed') + branch_geoms.plot(ax=ax1, color="Purple", alpha=0.4, linestyle="dashed") + branch_geoms.plot(ax=ax2, color="Orange", alpha=0.4, linestyle="dashed") - gdf.plot(ax=ax1, color='black') - gdf.plot(ax=ax2, color='black') + sections_geoms.plot(ax=ax1, color="black") + sections_geoms.plot(ax=ax2, color="black") try: - gdf_d0.plot(ax=ax1, column=indicator, cmap='BuPu', - scheme='fisherjenks', k=5, alpha=.6) - gdf_d1.plot(ax=ax2, column=indicator, cmap='Oranges', - scheme='fisherjenks', k=5, alpha=.6) + gdf_d0.plot( + ax=ax1, + column=indicator_col, + cmap="BuPu", + scheme="fisherjenks", + k=5, + alpha=0.6, + ) + gdf_d1.plot( + ax=ax2, + column=indicator_col, + cmap="Oranges", + scheme="fisherjenks", + k=5, + alpha=0.6, + ) except ValueError: - gdf_d0.plot(ax=ax1, column=indicator, cmap='BuPu', alpha=.6) - gdf_d1.plot(ax=ax2, column=indicator, cmap='Oranges', alpha=.6) + gdf_d0.plot(ax=ax1, column=indicator_col, cmap="BuPu", alpha=0.6) + gdf_d1.plot(ax=ax2, column=indicator_col, cmap="Oranges", alpha=0.6) ax1.set_axis_off() ax2.set_axis_off() - ax1.set_title('IDA', fontdict=font_dicc) - ax2.set_title('VUELTA', fontdict=font_dicc) + ax1.set_title("IDA", fontdict=font_dicc, y=1.0, pad=-20) + ax2.set_title("VUELTA", fontdict=font_dicc, y=1.0, pad=-20) - # Set title and plot axis - if indicator == 'cantidad_etapas': - title = 'Segmentos del recorrido - Cantidad de etapas' - y_axis_lable = 'Cantidad de etapas por sentido' - elif indicator == 'prop_etapas': - title = 'Segmentos del recorrido - Porcentaje de etapas totales' - y_axis_lable = 'Porcentaje del total de etapas' + if not df.hour_min.isna().all(): + from_hr = df.hour_min.unique()[0] + to_hr = df.hour_max.unique()[0] + hr_str = f" {from_hr}-{to_hr} hrs" + hour_range = [from_hr, to_hr] else: - raise Exception( - "Indicador debe ser 'cantidad_etapas' o 'prop_etapas'") - - if not df.hora_min.isna().all(): - from_hr = df.hora_min.unique()[0] - to_hr = df.hora_max.unique()[0] - hr_str = f' {from_hr}-{to_hr} hrs' - else: - hr_str = '' - - title = title + hr_str + ' - ' + day_str + \ - f" {id_linea_str} (id_linea: {id_linea})" + hr_str = "" + hour_range = False + + title = ( + title + + hr_str + + " - " + + day_str + + "-" + + mes + + "-" + + f" {id_linea_str} (id_linea: {line_id})" + ) f.suptitle(title, fontsize=18) # Matching bar plot with route direction @@ -435,12 +488,12 @@ def viz_etapas_x_tramo_recorrido(df, flecha_oe_xy = (0.6, 1.1) flecha_oe_text_xy = (0.95, 1.1) - labels_eo = [''] * len(section_ids) - labels_eo[0] = 'INICIO' - labels_eo[-1] = 'FIN' - labels_oe = [''] * len(section_ids) - labels_oe[-1] = 'INICIO' - labels_oe[0] = 'FIN' + labels_eo = [""] * len(section_ids) + labels_eo[0] = "INICIO" + labels_eo[-1] = "FIN" + labels_oe = [""] * len(section_ids) + labels_oe[-1] = "INICIO" + labels_oe[0] = "FIN" # check if route geom is drawn from west to east geom_dir_east = flecha_ida_inicio_wgs84[0] < flecha_vuelta_fin_wgs84[0] @@ -457,8 +510,8 @@ def viz_etapas_x_tramo_recorrido(df, labels_vuelta = labels_oe # direction 0 east to west - df_d0 = df_d0.sort_values('section_id', ascending=True) - df_d1 = df_d1.sort_values('section_id', ascending=True) + df_d0 = df_d0.sort_values("section_id", ascending=True) + df_d1 = df_d1.sort_values("section_id", ascending=True) else: flecha_ida_xy = flecha_oe_xy @@ -469,29 +522,39 @@ def viz_etapas_x_tramo_recorrido(df, flecha_vuelta_text_xy = flecha_eo_text_xy labels_vuelta = labels_eo - df_d0 = df_d0.sort_values('section_id', ascending=False) - df_d1 = df_d1.sort_values('section_id', ascending=False) + df_d0 = df_d0.sort_values("section_id", ascending=False) + df_d1 = df_d1.sort_values("section_id", ascending=False) - sns.barplot(data=df_d0, x="section_id", - y=indicator, ax=ax3, color='Purple', - order=df_d0.section_id.values) + sns.barplot( + data=df_d0, + x="section_id", + y=indicator_col, + ax=ax3, + color="Purple", + order=df_d0.section_id.values, + ) - sns.barplot(data=df_d1, x="section_id", - y=indicator, ax=ax4, color='Orange', - order=df_d1.section_id.values) + sns.barplot( + data=df_d1, + x="section_id", + y=indicator_col, + ax=ax4, + color="Orange", + order=df_d1.section_id.values, + ) # Axis ax3.set_xticklabels(labels_ida) ax4.set_xticklabels(labels_vuelta) ax3.set_ylabel(y_axis_lable) - ax3.set_xlabel('') + ax3.set_xlabel("") ax4.get_yaxis().set_visible(False) - ax4.set_ylabel('') - ax4.set_xlabel('') - max_y_barplot = max(df_d0[indicator].max(), df_d1[indicator].max()) + ax4.set_ylabel("") + ax4.set_xlabel("") + max_y_barplot = max(df_d0[indicator_col].max(), df_d1[indicator_col].max()) ax3.set_ylim(0, max_y_barplot) ax4.set_ylim(0, max_y_barplot) @@ -501,47 +564,60 @@ def viz_etapas_x_tramo_recorrido(df, ax4.spines.right.set_visible(False) ax4.spines.top.set_visible(False) + ax3.grid(False) + ax4.grid(False) + # For direction 0, get the last section of the route geom - flecha_ida = gdf.loc[gdf.section_id == max(gdf.section_id), 'geometry'] + flecha_ida = sections_geoms.loc[ + sections_geoms.section_id == sections_geoms.section_id.max(), "geometry" + ] flecha_ida = list(flecha_ida.item().coords) flecha_ida_inicio = flecha_ida[1] flecha_ida_fin = flecha_ida[0] # For direction 1, get the first section of the route geom - flecha_vuelta = gdf.loc[gdf.section_id == 0.0, 'geometry'] + flecha_vuelta = sections_geoms.loc[ + sections_geoms.section_id == sections_geoms.section_id.min(), "geometry" + ] flecha_vuelta = list(flecha_vuelta.item().coords) # invert the direction of the arrow flecha_vuelta_inicio = flecha_vuelta[0] flecha_vuelta_fin = flecha_vuelta[1] - ax1.annotate('', xy=(flecha_ida_inicio[0], - flecha_ida_inicio[1]), - xytext=(flecha_ida_fin[0], - flecha_ida_fin[1]), - arrowprops=dict(facecolor='black', - edgecolor='black'), - ) - - ax2.annotate('', xy=(flecha_vuelta_inicio[0], - flecha_vuelta_inicio[1]), - xytext=(flecha_vuelta_fin[0], - flecha_vuelta_fin[1]), - arrowprops=dict(facecolor='black', - edgecolor='black'), - ) - - ax3.annotate('Sentido', xy=flecha_ida_xy, xytext=flecha_ida_text_xy, - size=16, va="center", ha="center", - xycoords='axes fraction', - arrowprops=dict(facecolor='Purple', - shrink=0.05, edgecolor='Purple'), - ) - ax4.annotate('Sentido', xy=flecha_vuelta_xy, xytext=flecha_vuelta_text_xy, - size=16, va="center", ha="center", - xycoords='axes fraction', - arrowprops=dict(facecolor='Orange', - shrink=0.05, edgecolor='Orange'), - ) + ax1.annotate( + "", + xy=(flecha_ida_inicio[0], flecha_ida_inicio[1]), + xytext=(flecha_ida_fin[0], flecha_ida_fin[1]), + arrowprops=dict(facecolor="black", edgecolor="black", shrink=0.2), + ) + + ax2.annotate( + "", + xy=(flecha_vuelta_inicio[0], flecha_vuelta_inicio[1]), + xytext=(flecha_vuelta_fin[0], flecha_vuelta_fin[1]), + arrowprops=dict(facecolor="black", edgecolor="black", shrink=0.2), + ) + + ax3.annotate( + "Sentido", + xy=flecha_ida_xy, + xytext=flecha_ida_text_xy, + size=16, + va="center", + ha="center", + xycoords="axes fraction", + arrowprops=dict(facecolor="Purple", shrink=0.05, edgecolor="Purple"), + ) + ax4.annotate( + "Sentido", + xy=flecha_vuelta_xy, + xytext=flecha_vuelta_text_xy, + size=16, + va="center", + ha="center", + xycoords="axes fraction", + arrowprops=dict(facecolor="Orange", shrink=0.05, edgecolor="Orange"), + ) prov = cx.providers.CartoDB.Positron try: @@ -550,81 +626,76 @@ def viz_etapas_x_tramo_recorrido(df, except (UnidentifiedImageError, ValueError): cx.add_basemap(ax1, crs=gdf_d0.crs.to_string()) cx.add_basemap(ax2, crs=gdf_d1.crs.to_string()) - except (r_ConnectionError): + except r_ConnectionError: pass alias = leer_alias() - for frm in ['png', 'pdf']: - archivo = f"{alias}_{day}_segmentos_id_linea_" - archivo = archivo+f"{id_linea}_{indicator}_{hr_str}.{frm}" + for frm in ["png", "pdf"]: + archivo = f"{alias}_{mes}({day_str})_segmentos_id_linea_" + archivo = archivo + f"{line_id}_{stat}_{hr_str}.{frm}" db_path = os.path.join("resultados", frm, archivo) f.savefig(db_path, dpi=300) plt.close(f) - if save_gdf: - gdf_d0 = gdf_d0.to_crs(epsg=4326) - gdf_d1 = gdf_d1.to_crs(epsg=4326) - - f_0 = f'segmentos_id_linea_{id_linea}_{indicator}{hr_str}_0.geojson' - f_1 = f'segmentos_id_linea_{id_linea}_{indicator}{hr_str}_1.geojson' - - db_path_0 = os.path.join("resultados", "geojson", f_0) - db_path_1 = os.path.join("resultados", "geojson", f_1) - - gdf_d0.to_file(db_path_0, driver='GeoJSON') - gdf_d1.to_file(db_path_1, driver='GeoJSON') - - conn_dash = iniciar_conexion_db(tipo='dash') - - gdf_d0_dash['wkt'] = gdf_d0_dash.geometry.to_wkt() - gdf_d1_dash['wkt'] = gdf_d1_dash.geometry.to_wkt() + # Save to dash db + + gdf_d0_dash["wkt"] = gdf_d0_dash.geometry.to_wkt() + gdf_d1_dash["wkt"] = gdf_d1_dash.geometry.to_wkt() + + gdf_d_dash = pd.concat([gdf_d0_dash, gdf_d1_dash], ignore_index=True) + gdf_d_dash["nombre_linea"] = id_linea_str + + cols = [ + "id_linea", + "yr_mo", + "nombre_linea", + "day_type", + "n_sections", + "sentido", + "section_id", + "hour_min", + "hour_max", + "legs", + "prop", + "buff_factor", + "wkt", + ] - gdf_d_dash = pd.concat([gdf_d0_dash, gdf_d1_dash], ignore_index=True) + gdf_d_dash = gdf_d_dash[cols] - gdf_d_dash['nombre_linea'] = id_linea_str + # delete old data + delete_df = sections_geoms.reindex( + columns=["id_linea", "n_sections"] + ).drop_duplicates() - cols = ['id_linea', - 'nombre_linea', - 'day_type', - 'n_sections', - 'sentido', - 'section_id', - 'hora_min', - 'hora_max', - 'cantidad_etapas', - 'prop_etapas', - 'buff_factor', - 'wkt'] + kpi.delete_old_route_section_load_data( + route_geoms=delete_df, + hour_range=hour_range, + day_type=day, + yr_mos=[mes], + db_type="dash", + ) - gdf_d_dash = gdf_d_dash[cols] + conn_dash = iniciar_conexion_db(tipo="dash") - gdf_d_dash_ant = pd.read_sql_query( - """ - SELECT * - FROM ocupacion_por_linea_tramo - """, - conn_dash, - ) + gdf_d_dash.to_sql( + "ocupacion_por_linea_tramo", conn_dash, if_exists="append", index=False + ) + conn_dash.close() - gdf_d_dash_ant = gdf_d_dash_ant[~( - (gdf_d_dash_ant.id_linea.isin( - gdf_d_dash.id_linea.unique().tolist())) & - (gdf_d_dash_ant.day_type.isin( - gdf_d_dash.day_type.unique().tolist())) & - (gdf_d_dash_ant.n_sections.isin( - gdf_d_dash.n_sections.unique().tolist())) & - ((gdf_d_dash_ant.hora_min == from_hr) - & (gdf_d_dash_ant.hora_max == to_hr)) - )] + if save_gdf: + gdf_d0 = gdf_d0.to_crs(epsg=4326) + gdf_d1 = gdf_d1.to_crs(epsg=4326) - gdf_d_dash = pd.concat( - [gdf_d_dash_ant, gdf_d_dash], ignore_index=True) + f_0 = f"segmentos_id_linea_{alias}_{mes}({day_str})_{line_id}_{stat}{hr_str}_0.geojson" + f_1 = f"segmentos_id_linea_{alias}_{mes}({day_str})_{line_id}_{stat}{hr_str}_1.geojson" - gdf_d_dash.to_sql("ocupacion_por_linea_tramo", conn_dash, - if_exists="replace", index=False) + db_path_0 = os.path.join("resultados", "geojson", f_0) + db_path_1 = os.path.join("resultados", "geojson", f_1) - conn_dash.close() + gdf_d0.to_file(db_path_0, driver="GeoJSON") + gdf_d1.to_file(db_path_1, driver="GeoJSON") if return_gdfs: return gdf_d0, gdf_d1 @@ -634,30 +705,35 @@ def plot_voronoi_zones(voi, hexs, hexs2, show_map, alias): fig = Figure(figsize=(13.5, 13.5), dpi=100) canvas = FigureCanvas(fig) ax = fig.add_subplot(111) - plt.rcParams.update({"axes.facecolor": '#d4dadc', - 'figure.facecolor': '#d4dadc'}) + plt.rcParams.update({"axes.facecolor": "#d4dadc", "figure.facecolor": "#d4dadc"}) voi = voi.to_crs(3857) - voi.geometry.boundary.plot(edgecolor='grey', linewidth=.5, ax=ax) + voi.geometry.boundary.plot(edgecolor="grey", linewidth=0.5, ax=ax) # ctx.add_basemap(ax, source=ctx.providers.CartoDB.Positron, # attribution=None, attribution_size=10) try: - cx.add_basemap(ax, source=ctx.providers.CartoDB.Positron, - attribution=None, attribution_size=10) + cx.add_basemap( + ax, + source=ctx.providers.CartoDB.Positron, + attribution=None, + attribution_size=10, + ) except (r_ConnectionError, ValueError): pass - voi['coords'] = voi['geometry'].apply( - lambda x: x.representative_point().coords[:]) - voi['coords'] = [coords[0] for coords in voi['coords']] - voi.apply(lambda x: ax.annotate( - text=x['Zona_voi'], - xy=x.geometry.centroid.coords[0], - ha='center', - color='darkblue', - ), axis=1) - ax.set_title('Zonificación', fontsize=12) - ax.axis('off') + voi["coords"] = voi["geometry"].apply(lambda x: x.representative_point().coords[:]) + voi["coords"] = [coords[0] for coords in voi["coords"]] + voi.apply( + lambda x: ax.annotate( + text=x["Zona_voi"], + xy=x.geometry.centroid.coords[0], + ha="center", + color="darkblue", + ), + axis=1, + ) + ax.set_title("Zonificación", fontsize=12) + ax.axis("off") if show_map: @@ -667,38 +743,39 @@ def plot_voronoi_zones(voi, hexs, hexs2, show_map, alias): fig = Figure(figsize=(13.5, 13.5), dpi=70) canvas = FigureCanvas(fig) ax = fig.add_subplot(111) - hexs.to_crs(3857).plot(markersize=hexs['fex']/500, ax=ax) - hexs2.to_crs(3857).boundary.plot(ax=ax, lw=.3) + hexs.to_crs(3857).plot(markersize=hexs["fex"] / 500, ax=ax) + hexs2.to_crs(3857).boundary.plot(ax=ax, lw=0.3) try: - ctx.add_basemap(ax, source=ctx.providers.CartoDB.Positron, - attribution=None, attribution_size=10) + ctx.add_basemap( + ax, + source=ctx.providers.CartoDB.Positron, + attribution=None, + attribution_size=10, + ) except (r_ConnectionError, ValueError): pass - ax.axis('off') + ax.axis("off") # graba resultados file_path = os.path.join("resultados", "png", f"{alias}Zona_voi_map.png") fig.savefig(file_path, dpi=300) - print('Zonificación guardada en', file_path) + print("Zonificación guardada en", file_path) file_path = os.path.join("resultados", "pdf", f"{alias}Zona_voi_map.pdf") fig.savefig(file_path, dpi=300) voi = voi.to_crs(4326) file_path = os.path.join("resultados", f"{alias}Zona_voi.geojson") - voi[['Zona_voi', 'geometry']].to_file(file_path) + voi[["Zona_voi", "geometry"]].to_file(file_path) -def imprimir_matrices_od(viajes, - savefile='viajes', - title='Matriz OD', - var_fex="", - desc_dia='', - tipo_dia=''): +def imprimir_matrices_od( + viajes, savefile="viajes", title="Matriz OD", var_fex="", desc_dia="", tipo_dia="" +): alias = leer_alias() - conn_insumos = iniciar_conexion_db(tipo='insumos') + conn_insumos = iniciar_conexion_db(tipo="insumos") zonas = pd.read_sql_query( """ @@ -708,14 +785,13 @@ def imprimir_matrices_od(viajes, ) conn_insumos.close() - zonas[f'h3_r6'] = zonas['h3'].apply(h3.h3_to_parent, res=6) - zonas[f'h3_r7'] = zonas['h3'].apply(h3.h3_to_parent, res=7) + zonas[f"h3_r6"] = zonas["h3"].apply(h3.h3_to_parent, res=6) + zonas[f"h3_r7"] = zonas["h3"].apply(h3.h3_to_parent, res=7) - df, matriz_zonas = traigo_zonificacion( - viajes, zonas, h3_o='h3_o', h3_d='h3_d') + df, matriz_zonas = traigo_zonificacion(viajes, zonas, h3_o="h3_o", h3_d="h3_d") if len(var_fex) == 0: - var_fex = 'var_fex' + var_fex = "var_fex" df[var_fex] = 1 for i in matriz_zonas: @@ -730,15 +806,15 @@ def imprimir_matrices_od(viajes, x_rotation=90, normalize=True, cmap="Reds", - title='Matriz OD General', - figsize_tuple='', + title="Matriz OD General", + figsize_tuple="", matriz_order=matriz_order, savefile=f"{alias}{savefile}_{var_zona}", alias=alias, desc_dia=desc_dia, tipo_dia=tipo_dia, var_zona=var_zona, - filtro1='Todos los viajes' + filtro1="Todos los viajes", ) imprime_od( @@ -749,15 +825,15 @@ def imprimir_matrices_od(viajes, x_rotation=90, normalize=True, cmap="Reds", - title='Matriz OD viajes con transferencia', - figsize_tuple='', + title="Matriz OD viajes con transferencia", + figsize_tuple="", matriz_order=matriz_order, savefile=f"{alias}{savefile}_{var_zona}_transferencias", alias=alias, desc_dia=desc_dia, tipo_dia=tipo_dia, var_zona=var_zona, - filtro1='Con transferencias' + filtro1="Con transferencias", ) imprime_od( @@ -768,116 +844,126 @@ def imprimir_matrices_od(viajes, x_rotation=90, normalize=True, cmap="Reds", - title='Matriz OD viajes cortos (<5kms)', - figsize_tuple='', + title="Matriz OD viajes cortos (<5kms)", + figsize_tuple="", matriz_order=matriz_order, savefile=f"{alias}{savefile}_{var_zona}_corta_distancia", alias=alias, desc_dia=desc_dia, tipo_dia=tipo_dia, var_zona=var_zona, - filtro1='Corta distancia (<5kms)' + filtro1="Corta distancia (<5kms)", ) # Imprime hora punta manana, mediodia, tarde - df_tmp = df.groupby(['dia', 'hora'], as_index=False)[ - var_fex].sum().reset_index() - df_tmp = df_tmp.groupby(['hora'])[var_fex].mean().reset_index() + df_tmp = ( + df.groupby(["dia", "hora"], as_index=False)[var_fex].sum().reset_index() + ) + df_tmp = df_tmp.groupby(["hora"])[var_fex].mean().reset_index() try: - manana = df_tmp[(df_tmp.hora.astype(int) >= 6) & ( - df_tmp.hora.astype(int) < 12)][var_fex].idxmax() + manana = df_tmp[ + (df_tmp.hora.astype(int) >= 6) & (df_tmp.hora.astype(int) < 12) + ][var_fex].idxmax() except ValueError: manana = None try: - mediodia = df_tmp[(df_tmp.hora.astype(int) >= 12) & ( - df_tmp.hora.astype(int) < 16)][var_fex].idxmax() + mediodia = df_tmp[ + (df_tmp.hora.astype(int) >= 12) & (df_tmp.hora.astype(int) < 16) + ][var_fex].idxmax() except ValueError: mediodia = None try: - tarde = df_tmp[(df_tmp.hora.astype(int) >= 16) & ( - df_tmp.hora.astype(int) < 22)][var_fex].idxmax() + tarde = df_tmp[ + (df_tmp.hora.astype(int) >= 16) & (df_tmp.hora.astype(int) < 22) + ][var_fex].idxmax() except ValueError: tarde = None if manana != None: imprime_od( - df[(df.hora.astype(int) >= manana-1) & - (df.hora.astype(int) <= manana+1)], + df[ + (df.hora.astype(int) >= manana - 1) + & (df.hora.astype(int) <= manana + 1) + ], zona_origen=f"{var_zona}_o", zona_destino=f"{var_zona}_d", var_fex=var_fex, x_rotation=90, normalize=True, cmap="Reds", - title='Matriz OD viajes punta mañana', - figsize_tuple='', + title="Matriz OD viajes punta mañana", + figsize_tuple="", matriz_order=matriz_order, savefile=f"{alias}{savefile}_{var_zona}_punta_manana", alias=alias, desc_dia=desc_dia, tipo_dia=tipo_dia, var_zona=var_zona, - filtro1='Punta mañana' + filtro1="Punta mañana", ) if mediodia != None: imprime_od( - df[(df.hora.astype(int) >= mediodia-1) & - (df.hora.astype(int) <= mediodia+1)], + df[ + (df.hora.astype(int) >= mediodia - 1) + & (df.hora.astype(int) <= mediodia + 1) + ], zona_origen=f"{var_zona}_o", zona_destino=f"{var_zona}_d", var_fex=var_fex, x_rotation=90, normalize=True, cmap="Reds", - title='Matriz OD viajes punta mediodí­a', - figsize_tuple='', + title="Matriz OD viajes punta mediodí­a", + figsize_tuple="", matriz_order=matriz_order, savefile=f"{alias}{savefile}_{var_zona}_punta_mediodia", alias=alias, desc_dia=desc_dia, tipo_dia=tipo_dia, var_zona=var_zona, - filtro1='Punta mediodí­a' - + filtro1="Punta mediodí­a", ) if tarde != None: imprime_od( - df[(df.hora.astype(int) >= tarde-1) & - (df.hora.astype(int) <= tarde+1)], + df[ + (df.hora.astype(int) >= tarde - 1) + & (df.hora.astype(int) <= tarde + 1) + ], zona_origen=f"{var_zona}_o", zona_destino=f"{var_zona}_d", var_fex=var_fex, x_rotation=90, normalize=True, cmap="Reds", - title='Matriz OD viajes punta tarde', - figsize_tuple='', + title="Matriz OD viajes punta tarde", + figsize_tuple="", matriz_order=matriz_order, savefile=f"{alias}{savefile}_{var_zona}_punta_tarde", alias=alias, desc_dia=desc_dia, tipo_dia=tipo_dia, var_zona=var_zona, - filtro1='Punta tarde' + filtro1="Punta tarde", ) -def imprime_lineas_deseo(df, - h3_o='', - h3_d='', - var_fex='', - title='Lí­neas de deseo', - savefile='lineas_deseo', - k_jenks=5, - filtro1='', - desc_dia='', - tipo_dia='' - ): +def imprime_lineas_deseo( + df, + h3_o="", + h3_d="", + var_fex="", + title="Lí­neas de deseo", + savefile="lineas_deseo", + k_jenks=5, + filtro1="", + desc_dia="", + tipo_dia="", +): """ Esta funcion toma un df de viajes con destino validado nombres de columnas con el h3 de origen y destino @@ -890,7 +976,7 @@ def imprime_lineas_deseo(df, pd.options.mode.chained_assignment = None alias = leer_alias() - conn_insumos = iniciar_conexion_db(tipo='insumos') + conn_insumos = iniciar_conexion_db(tipo="insumos") zonas = pd.read_sql_query( """ @@ -901,133 +987,140 @@ def imprime_lineas_deseo(df, conn_insumos.close() - zonas[f'h3_r6'] = zonas['h3'].apply(h3.h3_to_parent, res=6) - zonas[f'h3_r7'] = zonas['h3'].apply(h3.h3_to_parent, res=7) + zonas[f"h3_r6"] = zonas["h3"].apply(h3.h3_to_parent, res=6) + zonas[f"h3_r7"] = zonas["h3"].apply(h3.h3_to_parent, res=7) zonas = gpd.GeoDataFrame( zonas, - geometry=gpd.points_from_xy(zonas['longitud'], zonas['latitud']), + geometry=gpd.points_from_xy(zonas["longitud"], zonas["latitud"]), crs=4326, ) if len(h3_o) == 0: - h3_o = 'h3_o_norm' + h3_o = "h3_o_norm" if len(h3_d) == 0: - h3_d = 'h3_d_norm' + h3_d = "h3_d_norm" if len(var_fex) == 0: - var_fex = 'fex' + var_fex = "fex" df[var_fex] = 1 # Clasificar od en terminos de zonas - df, matriz_zonas = traigo_zonificacion(df, - zonas, - h3_o=h3_o, - h3_d=h3_d, - res_agg=True) + df, matriz_zonas = traigo_zonificacion( + df, zonas, h3_o=h3_o, h3_d=h3_d, res_agg=True + ) for m in matriz_zonas: var_zona = m[1] - lineas_deseo(df, - zonas, - var_zona, - var_fex, - h3_o, - h3_d, - alpha=.4, - cmap='viridis_r', - porc_viajes=100, - title=title, - savefile=f"{alias}{savefile}_{var_zona}", - show_fig=False, - k_jenks=k_jenks, - alias=alias, - desc_dia=desc_dia, - tipo_dia=tipo_dia, - zona=var_zona, - filtro1='Todos los viajes' - ) - - lineas_deseo(df[(df.cant_etapas > 1)], - zonas, - var_zona, - var_fex, - h3_o, - h3_d, - alpha=.4, - cmap='crest', - porc_viajes=90, - title=f'{title}\nViajes con transferencias', - savefile=f"{alias}{savefile}_{var_zona}_transferencias", - show_fig=False, - k_jenks=k_jenks, - alias=alias, - desc_dia=desc_dia, - tipo_dia=tipo_dia, - zona=var_zona, - filtro1='Con transferencias' - ) - - lineas_deseo(df[(df.distance_osm_drive <= 5)], - zonas, - var_zona, - var_fex, - h3_o, - h3_d, - alpha=.4, - cmap='magma_r', - porc_viajes=90, - title=f'{title}\nViajes de corta distancia (<5kms)', - savefile=f"{alias}{savefile}_{var_zona}_corta_distancia", - show_fig=False, - k_jenks=k_jenks, - alias=alias, - desc_dia=desc_dia, - tipo_dia=tipo_dia, - zona=var_zona, - filtro1='Corta distancia (<5kms)' - ) + lineas_deseo( + df, + zonas, + var_zona, + var_fex, + h3_o, + h3_d, + alpha=0.4, + cmap="viridis_r", + porc_viajes=100, + title=title, + savefile=f"{alias}{savefile}_{var_zona}", + show_fig=False, + k_jenks=k_jenks, + alias=alias, + desc_dia=desc_dia, + tipo_dia=tipo_dia, + zona=var_zona, + filtro1="Todos los viajes", + ) + + lineas_deseo( + df[(df.cant_etapas > 1)], + zonas, + var_zona, + var_fex, + h3_o, + h3_d, + alpha=0.4, + cmap="crest", + porc_viajes=90, + title=f"{title}\nViajes con transferencias", + savefile=f"{alias}{savefile}_{var_zona}_transferencias", + show_fig=False, + k_jenks=k_jenks, + alias=alias, + desc_dia=desc_dia, + tipo_dia=tipo_dia, + zona=var_zona, + filtro1="Con transferencias", + ) + + lineas_deseo( + df[(df.distance_osm_drive <= 5)], + zonas, + var_zona, + var_fex, + h3_o, + h3_d, + alpha=0.4, + cmap="magma_r", + porc_viajes=90, + title=f"{title}\nViajes de corta distancia (<5kms)", + savefile=f"{alias}{savefile}_{var_zona}_corta_distancia", + show_fig=False, + k_jenks=k_jenks, + alias=alias, + desc_dia=desc_dia, + tipo_dia=tipo_dia, + zona=var_zona, + filtro1="Corta distancia (<5kms)", + ) # Imprime hora punta manana, mediodia, tarde - df_tmp = df\ - .groupby(['dia', 'hora'], as_index=False)\ - .factor_expansion_linea.sum()\ - .rename(columns={'factor_expansion_linea': 'cant'})\ + df_tmp = ( + df.groupby(["dia", "hora"], as_index=False) + .factor_expansion_linea.sum() + .rename(columns={"factor_expansion_linea": "cant"}) .reset_index() - df_tmp = df_tmp.groupby(['hora']).cant.mean().reset_index() + ) + df_tmp = df_tmp.groupby(["hora"]).cant.mean().reset_index() try: - manana = df_tmp[(df_tmp.hora.astype(int) >= 6) & ( - df_tmp.hora.astype(int) < 12)].cant.idxmax() + manana = df_tmp[ + (df_tmp.hora.astype(int) >= 6) & (df_tmp.hora.astype(int) < 12) + ].cant.idxmax() except ValueError: manana = None try: - mediodia = df_tmp[(df_tmp.hora.astype(int) >= 12) & ( - df_tmp.hora.astype(int) < 16)].cant.idxmax() + mediodia = df_tmp[ + (df_tmp.hora.astype(int) >= 12) & (df_tmp.hora.astype(int) < 16) + ].cant.idxmax() except ValueError: mediodia = None try: - tarde = df_tmp[(df_tmp.hora.astype(int) >= 16) & ( - df_tmp.hora.astype(int) < 22)].cant.idxmax() + tarde = df_tmp[ + (df_tmp.hora.astype(int) >= 16) & (df_tmp.hora.astype(int) < 22) + ].cant.idxmax() except ValueError: tarde = None if manana != None: - lineas_deseo(df[ - (df.hora.astype(int) >= manana-1) & - (df.hora.astype(int) <= manana+1)], + lineas_deseo( + df[ + (df.hora.astype(int) >= manana - 1) + & (df.hora.astype(int) <= manana + 1) + ], zonas, var_zona, var_fex, h3_o, h3_d, - alpha=.4, - cmap='magma_r', + alpha=0.4, + cmap="magma_r", porc_viajes=90, - title=f'{title}\nViajes en hora punta mañana', + title=f"{title}\nViajes en hora punta mañana", savefile=f"{alias}{savefile}_{var_zona}_punta_manana", show_fig=False, normalizo_latlon=False, @@ -1036,21 +1129,24 @@ def imprime_lineas_deseo(df, desc_dia=desc_dia, tipo_dia=tipo_dia, zona=var_zona, - filtro1='Punta Mañana') + filtro1="Punta Mañana", + ) if mediodia != None: - lineas_deseo(df[ - (df.hora.astype(int) >= mediodia-1) & - (df.hora.astype(int) <= mediodia+1)], + lineas_deseo( + df[ + (df.hora.astype(int) >= mediodia - 1) + & (df.hora.astype(int) <= mediodia + 1) + ], zonas, var_zona, var_fex, h3_o, h3_d, - alpha=.4, - cmap='magma_r', + alpha=0.4, + cmap="magma_r", porc_viajes=90, - title=f'{title}\nViajes en hora punta mediodia', + title=f"{title}\nViajes en hora punta mediodia", savefile=f"{alias}{savefile}_{var_zona}_punta_mediodia", show_fig=False, normalizo_latlon=False, @@ -1059,21 +1155,24 @@ def imprime_lineas_deseo(df, desc_dia=desc_dia, tipo_dia=tipo_dia, zona=var_zona, - filtro1='Punta Mediodí­a') + filtro1="Punta Mediodí­a", + ) if tarde != None: - lineas_deseo(df[ - (df.hora.astype(int) >= tarde-1) & - (df.hora.astype(int) <= tarde+1)], + lineas_deseo( + df[ + (df.hora.astype(int) >= tarde - 1) + & (df.hora.astype(int) <= tarde + 1) + ], zonas, var_zona, var_fex, h3_o, h3_d, - alpha=.4, - cmap='magma_r', + alpha=0.4, + cmap="magma_r", porc_viajes=90, - title=f'{title}\nViajes en hora punta tarde', + title=f"{title}\nViajes en hora punta tarde", savefile=f"{alias}{savefile}_{var_zona}_punta_tarde", show_fig=False, normalizo_latlon=False, @@ -1082,12 +1181,13 @@ def imprime_lineas_deseo(df, desc_dia=desc_dia, tipo_dia=tipo_dia, zona=var_zona, - filtro1='Punta Tarde') + filtro1="Punta Tarde", + ) def indicadores_hora_punta(viajesxhora_dash, desc_dia, tipo_dia): - conn_data = iniciar_conexion_db(tipo='data') + conn_data = iniciar_conexion_db(tipo="data") try: indicadores = pd.read_sql_query( @@ -1105,115 +1205,171 @@ def indicadores_hora_punta(viajesxhora_dash, desc_dia, tipo_dia): for modo in viajesxhora_dash.Modo.unique(): - vi = viajesxhora_dash[viajesxhora_dash.Modo == - modo].reset_index(drop=True) - descrip = '' - if modo != 'Todos': - descrip = f' ({modo.capitalize()})' - - ind_horas = pd.concat([ - ind_horas, - pd.DataFrame([ - [desc_dia, - tipo_dia, - f'Hora punta mañana{descrip}', - vi.iloc[vi[(vi.Hora >= '05') & ( - vi.Hora < '12')].Viajes.idxmax()].Hora, - 'viajesxhora', - 0, - 0] - ], columns=['dia', 'tipo_dia', 'detalle', 'indicador', 'tabla', 'nivel', 'porcentaje'])]) - ind_horas = pd.concat([ - ind_horas, - pd.DataFrame([ - [desc_dia, - tipo_dia, - f'Hora punta mediodí­a{descrip}', - vi.iloc[vi[(vi.Hora >= '12') & ( - vi.Hora < '15')].Viajes.idxmax()].Hora, - 'viajesxhora', - 0, - 0] - ], columns=['dia', 'tipo_dia', 'detalle', 'indicador', 'tabla', 'nivel', 'porcentaje'])]) - ind_horas = pd.concat([ - ind_horas, - pd.DataFrame([ - [desc_dia, - tipo_dia, - f'Hora punta tarde{descrip}', - vi.iloc[vi[(vi.Hora >= '15') & ( - vi.Hora < '22')].Viajes.idxmax()].Hora, - 'viajesxhora', - 0, - 0] - ], columns=['dia', 'tipo_dia', 'detalle', 'indicador', 'tabla', 'nivel', 'porcentaje'])]) - - ind_horas['indicador'] = ind_horas['indicador'].astype(float) + vi = viajesxhora_dash[viajesxhora_dash.Modo == modo].reset_index(drop=True) + descrip = "" + if modo != "Todos": + descrip = f" ({modo.capitalize()})" + + ind_horas = pd.concat( + [ + ind_horas, + pd.DataFrame( + [ + [ + desc_dia, + tipo_dia, + f"Hora punta mañana{descrip}", + vi.iloc[ + vi[(vi.Hora >= "05") & (vi.Hora < "12")].Viajes.idxmax() + ].Hora, + "viajesxhora", + 0, + 0, + ] + ], + columns=[ + "dia", + "tipo_dia", + "detalle", + "indicador", + "tabla", + "nivel", + "porcentaje", + ], + ), + ] + ) + ind_horas = pd.concat( + [ + ind_horas, + pd.DataFrame( + [ + [ + desc_dia, + tipo_dia, + f"Hora punta mediodí­a{descrip}", + vi.iloc[ + vi[(vi.Hora >= "12") & (vi.Hora < "15")].Viajes.idxmax() + ].Hora, + "viajesxhora", + 0, + 0, + ] + ], + columns=[ + "dia", + "tipo_dia", + "detalle", + "indicador", + "tabla", + "nivel", + "porcentaje", + ], + ), + ] + ) + ind_horas = pd.concat( + [ + ind_horas, + pd.DataFrame( + [ + [ + desc_dia, + tipo_dia, + f"Hora punta tarde{descrip}", + vi.iloc[ + vi[(vi.Hora >= "15") & (vi.Hora < "22")].Viajes.idxmax() + ].Hora, + "viajesxhora", + 0, + 0, + ] + ], + columns=[ + "dia", + "tipo_dia", + "detalle", + "indicador", + "tabla", + "nivel", + "porcentaje", + ], + ), + ] + ) + + ind_horas["indicador"] = ind_horas["indicador"].astype(float) if len(indicadores) > 0: - indicadores = indicadores[~((indicadores.dia == desc_dia) & ( - indicadores.tipo_dia == tipo_dia) & ( - indicadores.tabla == 'viajesxhora') - )] + indicadores = indicadores[ + ~( + (indicadores.dia == desc_dia) + & (indicadores.tipo_dia == tipo_dia) + & (indicadores.tabla == "viajesxhora") + ) + ] indicadores = pd.concat([indicadores, ind_horas]) - indicadores.to_sql("hora_punta", conn_data, - if_exists="replace", index=False) + indicadores.to_sql("hora_punta", conn_data, if_exists="replace", index=False) conn_data.close() -def imprime_graficos_hora(viajes, - title='Cantidad de viajes en transporte público', - savefile='viajes', - var_fex='', - desc_dia='', - tipo_dia=''): +def imprime_graficos_hora( + viajes, + title="Cantidad de viajes en transporte público", + savefile="viajes", + var_fex="", + desc_dia="", + tipo_dia="", +): pd.options.mode.chained_assignment = None configs = leer_configs_generales() db_path = traigo_db_path alias = leer_alias() - df_aux = pd.DataFrame([(str(x).zfill(2)) - for x in list(range(0, 24))], columns=['hora']) - df_aux['dia'] = viajes.head(1).dia.values[0] - df_aux['cant'] = 0 - df_aux['modo'] = viajes.modo.unique()[0] + df_aux = pd.DataFrame( + [(str(x).zfill(2)) for x in list(range(0, 24))], columns=["hora"] + ) + df_aux["dia"] = viajes.head(1).dia.values[0] + df_aux["cant"] = 0 + df_aux["modo"] = viajes.modo.unique()[0] if not var_fex: - viajes['cant'] = 1 + viajes["cant"] = 1 else: - viajes['cant'] = viajes[var_fex] + viajes["cant"] = viajes[var_fex] viajesxhora = pd.concat([viajes, df_aux], ignore_index=True) - viajesxhora['hora'] = viajesxhora.hora.astype(str).str[:2].str.zfill(2) + viajesxhora["hora"] = viajesxhora.hora.astype(str).str[:2].str.zfill(2) - viajesxhora = viajesxhora.groupby( - ['dia', 'hora']).cant.sum().reset_index() - viajesxhora = viajesxhora.groupby(['hora']).cant.mean().reset_index() + viajesxhora = viajesxhora.groupby(["dia", "hora"]).cant.sum().reset_index() + viajesxhora = viajesxhora.groupby(["hora"]).cant.mean().reset_index() - viajesxhora['cant'] = viajesxhora['cant'].round().astype(int) + viajesxhora["cant"] = viajesxhora["cant"].round().astype(int) - savefile_ = f'{savefile}_x_hora' + savefile_ = f"{savefile}_x_hora" viajesxhora_dash = viajesxhora.copy() - viajesxhora_dash['modo'] = 'Todos' + viajesxhora_dash["modo"] = "Todos" # Viajes por hora with sns.axes_style( - {"axes.facecolor": "#cadce0", - 'figure.facecolor': '#cadce0', - }): + { + "axes.facecolor": "#cadce0", + "figure.facecolor": "#cadce0", + } + ): fig = Figure(figsize=(10, 3), dpi=100) canvas = FigureCanvas(fig) ax = fig.add_subplot(111) viajesxhora.plot(ax=ax, legend=False, label=False) ax.set_title(title, fontsize=8) - ax.set_xlabel('Hora', fontsize=8) - ax.set_ylabel('Viajes', fontsize=8) + ax.set_xlabel("Hora", fontsize=8) + ax.set_ylabel("Viajes", fontsize=8) ax.set_xticks(list(range(0, 24))) ax.tick_params(labelsize=6) - print("Nuevos archivos en resultados: ", f'{alias}{savefile_}') + print("Nuevos archivos en resultados: ", f"{alias}{savefile_}") db_path = os.path.join("resultados", "png", f"{alias}{savefile_}.png") fig.savefig(db_path, dpi=300, bbox_inches="tight") @@ -1222,32 +1378,29 @@ def imprime_graficos_hora(viajes, # Viajes por hora y modo de transporte viajesxhora = pd.concat([viajes, df_aux], ignore_index=True) - viajesxhora['hora'] = viajesxhora.hora.astype(str).str[:2].str.zfill(2) + viajesxhora["hora"] = viajesxhora.hora.astype(str).str[:2].str.zfill(2) viajesxhora = viajesxhora.groupby( - ['dia', 'hora', 'modo'], as_index=False).cant.sum() - viajesxhora = viajesxhora.groupby( - ['hora', 'modo'], as_index=False).cant.mean() + ["dia", "hora", "modo"], as_index=False + ).cant.sum() + viajesxhora = viajesxhora.groupby(["hora", "modo"], as_index=False).cant.mean() - viajesxhora.loc[viajesxhora.modo.str.contains( - 'Multi'), 'modo'] = 'Multietapa' - viajesxhora = viajesxhora.groupby(['hora', 'modo'])[ - 'cant'].sum().reset_index() + viajesxhora.loc[viajesxhora.modo.str.contains("Multi"), "modo"] = "Multietapa" + viajesxhora = viajesxhora.groupby(["hora", "modo"])["cant"].sum().reset_index() - viajesxhora['cant'] = viajesxhora['cant'].round().astype(int) + viajesxhora["cant"] = viajesxhora["cant"].round().astype(int) # guarda distribución de viajes para dashboard - viajesxhora_dash = pd.concat( - [viajesxhora_dash, viajesxhora], ignore_index=True) + viajesxhora_dash = pd.concat([viajesxhora_dash, viajesxhora], ignore_index=True) - viajesxhora_dash['tipo_dia'] = tipo_dia - viajesxhora_dash['desc_dia'] = desc_dia + viajesxhora_dash["tipo_dia"] = tipo_dia + viajesxhora_dash["desc_dia"] = desc_dia - viajesxhora_dash = viajesxhora_dash[[ - 'tipo_dia', 'desc_dia', 'hora', 'cant', 'modo']] - viajesxhora_dash.columns = ['tipo_dia', - 'desc_dia', 'Hora', 'Viajes', 'Modo'] + viajesxhora_dash = viajesxhora_dash[ + ["tipo_dia", "desc_dia", "hora", "cant", "modo"] + ] + viajesxhora_dash.columns = ["tipo_dia", "desc_dia", "Hora", "Viajes", "Modo"] - conn_dash = iniciar_conexion_db(tipo='dash') + conn_dash = iniciar_conexion_db(tipo="dash") query = f""" DELETE FROM viajes_hora @@ -1262,31 +1415,39 @@ def imprime_graficos_hora(viajes, hrs = [str(i).zfill(2) for i in range(0, 24)] for modo in modos: for hr in hrs: - if len(viajesxhora_dash.loc[(viajesxhora_dash.Modo == modo) & (viajesxhora_dash.Hora == hr)]) == 0: - - viajesxhora_dash = pd.concat([ - viajesxhora_dash, - pd.DataFrame([[tipo_dia, - desc_dia, - hr, - 0, - modo]], - columns=viajesxhora_dash.columns) - ]) + if ( + len( + viajesxhora_dash.loc[ + (viajesxhora_dash.Modo == modo) & (viajesxhora_dash.Hora == hr) + ] + ) + == 0 + ): + + viajesxhora_dash = pd.concat( + [ + viajesxhora_dash, + pd.DataFrame( + [[tipo_dia, desc_dia, hr, 0, modo]], + columns=viajesxhora_dash.columns, + ), + ] + ) - viajesxhora_dash.to_sql("viajes_hora", conn_dash, - if_exists="append", index=False) + viajesxhora_dash.to_sql("viajes_hora", conn_dash, if_exists="append", index=False) conn_dash.close() indicadores_hora_punta(viajesxhora_dash, desc_dia, tipo_dia) # Viajes por hora - savefile_ = f'{savefile}_modo' + savefile_ = f"{savefile}_modo" with sns.axes_style( - {"axes.facecolor": "#cadce0", - 'figure.facecolor': '#cadce0', - }): + { + "axes.facecolor": "#cadce0", + "figure.facecolor": "#cadce0", + } + ): fig = Figure(figsize=(10, 3), dpi=100) canvas = FigureCanvas(fig) @@ -1294,14 +1455,15 @@ def imprime_graficos_hora(viajes, for i in viajesxhora.modo.unique(): viajesxhora[viajesxhora.modo == i].reset_index().plot( - ax=ax, y='cant', legend=True, label=i) + ax=ax, y="cant", legend=True, label=i + ) ax.set_title(title, fontsize=8) - ax.set_xlabel('Hora', fontsize=8) - ax.set_ylabel('Viajes', fontsize=8) + ax.set_xlabel("Hora", fontsize=8) + ax.set_ylabel("Viajes", fontsize=8) ax.set_xticks(list(range(0, 24))) ax.tick_params(labelsize=6) - print("Nuevos archivos en resultados: ", f'{alias}{savefile_}') + print("Nuevos archivos en resultados: ", f"{alias}{savefile_}") db_path = os.path.join("resultados", "png", f"{alias}{savefile_}.png") fig.savefig(db_path, dpi=300, bbox_inches="tight") @@ -1309,52 +1471,56 @@ def imprime_graficos_hora(viajes, fig.savefig(db_path, dpi=300, bbox_inches="tight") # Distribución de viajes - savefile_ = f'{savefile}_dist' - vi = viajes[(viajes.distance_osm_drive.notna()) - & (viajes.distance_osm_drive > 0) - & (viajes.h3_o != viajes.h3_d)] + savefile_ = f"{savefile}_dist" + vi = viajes[ + (viajes.distance_osm_drive.notna()) + & (viajes.distance_osm_drive > 0) + & (viajes.h3_o != viajes.h3_d) + ] if len(vi) == 0: return None - vi['distance_osm_drive'] = vi['distance_osm_drive'].astype(int) + vi["distance_osm_drive"] = vi["distance_osm_drive"].astype(int) - vi_modo = vi\ - .groupby(['distance_osm_drive', 'modo'], as_index=False)\ - .factor_expansion_linea.sum()\ - .rename(columns={'factor_expansion_linea': 'cant'}) + vi_modo = ( + vi.groupby(["distance_osm_drive", "modo"], as_index=False) + .factor_expansion_linea.sum() + .rename(columns={"factor_expansion_linea": "cant"}) + ) - vi = vi\ - .groupby('distance_osm_drive', as_index=False)\ - .factor_expansion_linea.sum()\ - .rename(columns={'factor_expansion_linea': 'cant'}) + vi = ( + vi.groupby("distance_osm_drive", as_index=False) + .factor_expansion_linea.sum() + .rename(columns={"factor_expansion_linea": "cant"}) + ) - vi = vi.loc[vi.cant > 0, ['distance_osm_drive', 'cant'] - ].sort_values('distance_osm_drive') + vi = vi.loc[vi.cant > 0, ["distance_osm_drive", "cant"]].sort_values( + "distance_osm_drive" + ) - vi['pc'] = round(vi.cant / vi.cant.sum() * 100, 5) - vi['csum'] = vi.pc.cumsum() + vi["pc"] = round(vi.cant / vi.cant.sum() * 100, 5) + vi["csum"] = vi.pc.cumsum() vi = vi[vi.csum <= 99.5] - vi['Viajes (en miles)'] = round(vi.cant/1000) + vi["Viajes (en miles)"] = round(vi.cant / 1000) - vi_modo['pc'] = round(vi_modo.cant / vi_modo.cant.sum() * 100, 5) - vi_modo['csum'] = vi_modo.pc.cumsum() + vi_modo["pc"] = round(vi_modo.cant / vi_modo.cant.sum() * 100, 5) + vi_modo["csum"] = vi_modo.pc.cumsum() vi_modo = vi_modo[vi_modo.csum <= 99.5] # guarda distribución de viajes para dashboard vi_dash = vi.copy() - vi_dash['modo'] = 'Todos' + vi_dash["modo"] = "Todos" vi_dash = pd.concat([vi_dash, vi_modo], ignore_index=True) - vi_dash['tipo_dia'] = tipo_dia - vi_dash['desc_dia'] = desc_dia + vi_dash["tipo_dia"] = tipo_dia + vi_dash["desc_dia"] = desc_dia - vi_dash = vi_dash[['desc_dia', 'tipo_dia', - 'distance_osm_drive', 'cant', 'modo']] - vi_dash.columns = ['desc_dia', 'tipo_dia', 'Distancia', 'Viajes', 'Modo'] + vi_dash = vi_dash[["desc_dia", "tipo_dia", "distance_osm_drive", "cant", "modo"]] + vi_dash.columns = ["desc_dia", "tipo_dia", "Distancia", "Viajes", "Modo"] - conn_dash = iniciar_conexion_db(tipo='dash') + conn_dash = iniciar_conexion_db(tipo="dash") query = f""" DELETE FROM distribucion WHERE desc_dia = "{desc_dia}" @@ -1368,52 +1534,63 @@ def imprime_graficos_hora(viajes, ytitle = "Viajes" if vi.cant.mean() > 1000: - vi['cant'] = round(vi['cant']/1000) + vi["cant"] = round(vi["cant"] / 1000) ytitle = "Viajes (en miles)" - sns.set_style("darkgrid", {"axes.facecolor": "#cadce0", - 'figure.facecolor': '#cadce0', "grid.linestyle": ":"}) + sns.set_style( + "darkgrid", + { + "axes.facecolor": "#cadce0", + "figure.facecolor": "#cadce0", + "grid.linestyle": ":", + }, + ) - fig = Figure(figsize=(8, 4), dpi=200) - canvas = FigureCanvas(fig) - ax = fig.add_subplot(111) + try: + fig = Figure(figsize=(8, 4), dpi=200) + ax = fig.add_subplot(111) - sns.histplot(x='distance_osm_drive', weights='cant', - data=vi, bins=len(vi), ax=ax) # element='poly', - ax.set_title(title, fontsize=12) - ax.set_xlabel("Distancia (kms)", fontsize=10) - ax.set_ylabel(ytitle, fontsize=10) - ax.set_xticks(list(range(0, len(vi)+1, 5))) + sns.histplot( + x="distance_osm_drive", weights="cant", data=vi, bins=len(vi), ax=ax + ) # element='poly', + ax.set_title(title, fontsize=12) + ax.set_xlabel("Distancia (kms)", fontsize=10) + ax.set_ylabel(ytitle, fontsize=10) + ax.set_xticks(list(range(0, len(vi) + 1, 5))) - fig.tight_layout() + fig.tight_layout() - print("Nuevos archivos en resultados: ", f'{alias}{savefile_}') - db_path = os.path.join("resultados", "png", f"{alias}{savefile_}.png") - fig.savefig(db_path, dpi=300, bbox_inches="tight") + print("Nuevos archivos en resultados: ", f"{alias}{savefile_}") + db_path = os.path.join("resultados", "png", f"{alias}{savefile_}.png") + fig.savefig(db_path, dpi=300, bbox_inches="tight") - db_path = os.path.join("resultados", "pdf", f"{alias}{savefile_}.pdf") - fig.savefig(db_path, dpi=300, bbox_inches="tight") + db_path = os.path.join("resultados", "pdf", f"{alias}{savefile_}.pdf") + fig.savefig(db_path, dpi=300, bbox_inches="tight") + except ValueError as e: + print(e) -def imprime_burbujas(df, - res=7, - h3_o='h3_o', - alpha=.4, - cmap='viridis_r', - var_fex='', - porc_viajes=90, - title='burbujas', - savefile='burbujas', - show_fig=False, - k_jenks=5): +def imprime_burbujas( + df, + res=7, + h3_o="h3_o", + alpha=0.4, + cmap="viridis_r", + var_fex="", + porc_viajes=90, + title="burbujas", + savefile="burbujas", + show_fig=False, + k_jenks=5, +): pd.options.mode.chained_assignment = None configs = leer_configs_generales() db_path = traigo_db_path alias = leer_alias() - conn_data = iniciar_conexion_db(tipo='data') - conn_insumos = iniciar_conexion_db(tipo='insumos') + conn_data = iniciar_conexion_db(tipo="data") + conn_insumos = iniciar_conexion_db(tipo="insumos") zonas = pd.read_sql_query( """ @@ -1427,20 +1604,17 @@ def imprime_burbujas(df, zonas = gpd.GeoDataFrame( zonas, - geometry=gpd.points_from_xy(zonas['longitud'], zonas['latitud']), - crs=4326) + geometry=gpd.points_from_xy(zonas["longitud"], zonas["latitud"]), + crs=4326, + ) if len(var_fex) == 0: - var_fex = 'fex' + var_fex = "fex" df[var_fex] = 1 - df_agg = crea_df_burbujas(df, - zonas, - h3_o=h3_o, - var_fex=var_fex, - porc_viajes=porc_viajes, - res=res - ) + df_agg = crea_df_burbujas( + df, zonas, h3_o=h3_o, var_fex=var_fex, porc_viajes=porc_viajes, res=res + ) df_agg[var_fex] = df_agg[var_fex].round().astype(int) @@ -1452,27 +1626,31 @@ def imprime_burbujas(df, canvas = FigureCanvas(fig) ax = fig.add_subplot(111) - zonas[zonas['h3'].isin(df[h3_o].unique())].to_crs( - 3857).plot(ax=ax, alpha=0) + zonas[zonas["h3"].isin(df[h3_o].unique())].to_crs(3857).plot(ax=ax, alpha=0) try: - df_agg.to_crs(3857).plot(ax=ax, - alpha=alpha, - cmap=cmap, - markersize=df_agg[var_fex] / multip, - column=var_fex, - scheme='FisherJenks', - k=k_jenks, - legend=True, - legend_kwds={ - 'loc': 'upper right', - 'title': 'Viajes', - 'fontsize': 8, - 'title_fontsize': 10, - } - ) + df_agg.to_crs(3857).plot( + ax=ax, + alpha=alpha, + cmap=cmap, + markersize=df_agg[var_fex] / multip, + column=var_fex, + scheme="FisherJenks", + k=k_jenks, + legend=True, + legend_kwds={ + "loc": "upper right", + "title": "Viajes", + "fontsize": 8, + "title_fontsize": 10, + }, + ) try: - ctx.add_basemap(ax, source=ctx.providers.CartoDB.Positron, - attribution=None, attribution_size=10) + ctx.add_basemap( + ax, + source=ctx.providers.CartoDB.Positron, + attribution=None, + attribution_size=10, + ) except (r_ConnectionError, ValueError): pass @@ -1483,37 +1661,30 @@ def imprime_burbujas(df, for lbl in leg.get_texts(): label_text = lbl.get_text() - lower = label_text.split(',')[0] - upper = label_text.split(',')[1] - new_text = f'{float(lower):,.0f} - {float(upper):,.0f}' + lower = label_text.split(",")[0] + upper = label_text.split(",")[1] + new_text = f"{float(lower):,.0f} - {float(upper):,.0f}" lbl.set_text(new_text) - ax.add_artist( - ScaleBar(1, location='lower right', box_alpha=0, pad=1)) - ax.axis('off') + ax.add_artist(ScaleBar(1, location="lower right", box_alpha=0, pad=1)) + ax.axis("off") if len(savefile) > 0: - print("Nuevos archivos en resultados: ", f'{alias}{savefile}') - db_path = os.path.join("resultados", "png", - f"{alias}{savefile}.png") + print("Nuevos archivos en resultados: ", f"{alias}{savefile}") + db_path = os.path.join("resultados", "png", f"{alias}{savefile}.png") fig.savefig(db_path, dpi=300, bbox_inches="tight") - db_path = os.path.join("resultados", "pdf", - f"{alias}{savefile}.pdf") + db_path = os.path.join("resultados", "pdf", f"{alias}{savefile}.pdf") fig.savefig(db_path, dpi=300, bbox_inches="tight") if show_fig: display(fig) - except (ValueError) as e: + except ValueError as e: print(e) -def traigo_zonificacion(viajes, - zonas, - h3_o='h3_o', - h3_d='h3_d', - res_agg=False): +def traigo_zonificacion(viajes, zonas, h3_o="h3_o", h3_d="h3_d", res_agg=False): """ Esta funcion toma la tabla viajes la tabla zonas, los nombres de las columnas con el h3 de origen y destino @@ -1524,23 +1695,24 @@ def traigo_zonificacion(viajes, matriz_zonas = [] vars_zona = [] - if 'Zona_voi' in zonas.columns: + if "Zona_voi" in zonas.columns: - matriz_zonas = [['', - 'Zona_voi', - [str(x) for x in list( - range(1, len(zonas.Zona_voi.unique())+1))] - ]] - vars_zona = ['Zona_voi'] + matriz_zonas = [ + [ + "", + "Zona_voi", + [str(x) for x in list(range(1, len(zonas.Zona_voi.unique()) + 1))], + ] + ] + vars_zona = ["Zona_voi"] if res_agg: - zonas[f'h3_r6'] = zonas['h3'].apply(h3.h3_to_parent, res=6) - zonas[f'h3_r7'] = zonas['h3'].apply(h3.h3_to_parent, res=7) + zonas[f"h3_r6"] = zonas["h3"].apply(h3.h3_to_parent, res=6) + zonas[f"h3_r7"] = zonas["h3"].apply(h3.h3_to_parent, res=7) - matriz_zonas += [['', f'h3_r6', ''], - ['', f'h3_r7', '']] - vars_zona += [f'h3_r6'] - vars_zona += [f'h3_r7'] + matriz_zonas += [["", f"h3_r6", ""], ["", f"h3_r7", ""]] + vars_zona += [f"h3_r6"] + vars_zona += [f"h3_r7"] if configs["zonificaciones"]: for n in range(0, 5): @@ -1563,22 +1735,16 @@ def traigo_zonificacion(viajes, except KeyError: pass - vars_o = [h3_o] + [f'{x}_o' for x in vars_zona] - vars_d = [h3_d] + [f'{x}_d' for x in vars_zona] + vars_o = [h3_o] + [f"{x}_o" for x in vars_zona] + vars_d = [h3_d] + [f"{x}_d" for x in vars_zona] - zonas_tmp = zonas[['h3']+vars_zona] + zonas_tmp = zonas[["h3"] + vars_zona] zonas_tmp.columns = vars_o - viajes = viajes.merge( - zonas_tmp, - on=h3_o - ) + viajes = viajes.merge(zonas_tmp, on=h3_o) - zonas_tmp = zonas[['h3']+vars_zona] + zonas_tmp = zonas[["h3"] + vars_zona] zonas_tmp.columns = vars_d - viajes = viajes.merge( - zonas_tmp, - on=h3_d - ) + viajes = viajes.merge(zonas_tmp, on=h3_d) return viajes, matriz_zonas @@ -1595,7 +1761,7 @@ def imprime_od( path_resultados=Path(), savefile="", title="Matriz OD", - figsize_tuple='', + figsize_tuple="", fontsize=12, fmt="", cbar=False, @@ -1605,11 +1771,11 @@ def imprime_od( total_color="navy", total_background_color="white", show_fig=False, - alias='', - desc_dia='', - tipo_dia='', - var_zona='', - filtro1='', + alias="", + desc_dia="", + tipo_dia="", + var_zona="", + filtro1="", ): if len(fmt) == 0: @@ -1626,41 +1792,40 @@ def imprime_od( matriz_order_col = matriz_order if len(var_fex) == 0: - var_fex = 'fex' + var_fex = "fex" df[var_fex] = 1 - df = df.groupby(['dia', zona_origen, zona_destino], - as_index=False)[var_fex].sum() - df = df.groupby([zona_origen, zona_destino], as_index=False)[ - var_fex].mean() + df = df.groupby(["dia", zona_origen, zona_destino], as_index=False)[var_fex].sum() + df = df.groupby([zona_origen, zona_destino], as_index=False)[var_fex].mean() df[var_fex] = df[var_fex].round().astype(int) if len(df) > 0: - vals = df.loc[~(df[zona_origen].isin( - df[zona_destino].unique())), zona_origen].unique() + vals = df.loc[ + ~(df[zona_origen].isin(df[zona_destino].unique())), zona_origen + ].unique() for i in vals: - df = pd.concat([df, - pd.DataFrame( - [[i, i, 0]], - columns=[ - zona_origen, - zona_destino, - var_fex - ]) - ]) - vals = df.loc[~(df[zona_destino].isin( - df[zona_origen].unique())), zona_destino].unique() + df = pd.concat( + [ + df, + pd.DataFrame( + [[i, i, 0]], columns=[zona_origen, zona_destino, var_fex] + ), + ] + ) + vals = df.loc[ + ~(df[zona_destino].isin(df[zona_origen].unique())), zona_destino + ].unique() for i in vals: - df = pd.concat([df, - pd.DataFrame( - [[i, i, 0]], - columns=[ - zona_destino, - zona_origen, - var_fex]) - ]) + df = pd.concat( + [ + df, + pd.DataFrame( + [[i, i, 0]], columns=[zona_destino, zona_origen, var_fex] + ), + ] + ) od_heatmap = pd.crosstab( index=df[zona_origen], @@ -1671,13 +1836,11 @@ def imprime_od( ) if len(figsize_tuple) == 0: - figsize_tuple = (len(od_heatmap)+1, len(od_heatmap)+1) + figsize_tuple = (len(od_heatmap) + 1, len(od_heatmap) + 1) matriz_order = [i for i in matriz_order if i in od_heatmap.columns] - matriz_order_row = [ - i for i in matriz_order_row if i in od_heatmap.columns] - matriz_order_col = [ - i for i in matriz_order_col if i in od_heatmap.columns] + matriz_order_row = [i for i in matriz_order_row if i in od_heatmap.columns] + matriz_order_col = [i for i in matriz_order_col if i in od_heatmap.columns] if len(matriz_order_col) > 0: od_heatmap = od_heatmap[matriz_order_col] @@ -1816,8 +1979,7 @@ def imprime_od( if ii <= len(facecolors) - 1: value_i = float(value_i) - cond = (value_i >= val_min) | ( - ii == len(facecolors) - 1) + cond = (value_i >= val_min) | (ii == len(facecolors) - 1) if cond: i.set_color("white") @@ -1833,7 +1995,7 @@ def imprime_od( if len(savefile) > 0: - savefile = savefile+'_matrizod' + savefile = savefile + "_matrizod" print("Nuevos archivos en resultados: ", savefile) @@ -1843,8 +2005,7 @@ def imprime_od( db_path = os.path.join("resultados", "pdf", f"{savefile}.pdf") fig.savefig(db_path, dpi=300, bbox_inches="tight") - db_path = os.path.join( - "resultados", "matrices", f"{savefile}.xlsx") + db_path = os.path.join("resultados", "matrices", f"{savefile}.xlsx") if normalize: dash_tot = df.copy() @@ -1868,17 +2029,17 @@ def imprime_od( display(fig) # Guardo datos para el dashboard - if 'h3_r' not in var_zona: + if "h3_r" not in var_zona: - conn_dash = iniciar_conexion_db(tipo='dash') + conn_dash = iniciar_conexion_db(tipo="dash") df = df[[zona_origen, zona_destino, var_fex]].copy() - df.columns = ['Origen', 'Destino', 'Viajes'] + df.columns = ["Origen", "Destino", "Viajes"] - df['desc_dia'] = desc_dia - df['tipo_dia'] = tipo_dia - df['var_zona'] = var_zona.replace('h3_r', 'H3 Resolucion ') - df['filtro1'] = filtro1 + df["desc_dia"] = desc_dia + df["tipo_dia"] = tipo_dia + df["var_zona"] = var_zona.replace("h3_r", "H3 Resolucion ") + df["filtro1"] = filtro1 df_ant = pd.read_sql_query( """ @@ -1888,83 +2049,91 @@ def imprime_od( conn_dash, ) - df_ant = df_ant[~( - (df_ant.desc_dia == desc_dia) & - (df_ant.tipo_dia == tipo_dia) & - (df_ant.var_zona == var_zona - .replace('h3_r', 'H3 Resolucion ')) & - (df_ant.filtro1 == filtro1) - )] + df_ant = df_ant[ + ~( + (df_ant.desc_dia == desc_dia) + & (df_ant.tipo_dia == tipo_dia) + & (df_ant.var_zona == var_zona.replace("h3_r", "H3 Resolucion ")) + & (df_ant.filtro1 == filtro1) + ) + ] df = pd.concat([df_ant, df], ignore_index=True) if len(matriz_order_row) == 0: - matriz_order_row = od_heatmap.reset_index()[ - zona_origen].unique() + matriz_order_row = od_heatmap.reset_index()[zona_origen].unique() if len(matriz_order_col) == 0: matriz_order_col = od_heatmap.columns n = 1 cols = [] for i in matriz_order_row: - cols += [str(n).zfill(3)+'_'+str(i)] + cols += [str(n).zfill(3) + "_" + str(i)] n += 1 - df['Origen'] = df.Origen.replace(matriz_order_row, cols) + df["Origen"] = df.Origen.replace(matriz_order_row, cols) n = 1 cols = [] for i in matriz_order_col: - cols += [str(n).zfill(3)+'_'+str(i)] + cols += [str(n).zfill(3) + "_" + str(i)] n += 1 - df['Destino'] = df.Destino.replace(matriz_order_row, cols) + df["Destino"] = df.Destino.replace(matriz_order_row, cols) df.to_sql("matrices", conn_dash, if_exists="replace", index=False) conn_dash.close() -def lineas_deseo(df, - zonas, - var_zona, - var_fex, - h3_o, - h3_d, - alpha=.4, - cmap='viridis_r', - porc_viajes=100, - title='Lí­neas de deseo', - savefile='lineas_deseo', - show_fig=True, - normalizo_latlon=True, - k_jenks=5, - alias='', - desc_dia='', - tipo_dia='', - zona='', - filtro1='', - ): - - hexs = zonas[(zonas.fex.notna()) & (zonas.fex != 0)]\ - .groupby(var_zona, as_index=False)\ - .size().drop(['size'], axis=1) +def lineas_deseo( + df, + zonas, + var_zona, + var_fex, + h3_o, + h3_d, + alpha=0.4, + cmap="viridis_r", + porc_viajes=100, + title="Lí­neas de deseo", + savefile="lineas_deseo", + show_fig=True, + normalizo_latlon=True, + k_jenks=5, + alias="", + desc_dia="", + tipo_dia="", + zona="", + filtro1="", +): + + hexs = ( + zonas[(zonas.fex.notna()) & (zonas.fex != 0)] + .groupby(var_zona, as_index=False) + .size() + .drop(["size"], axis=1) + ) hexs = hexs.merge( zonas[(zonas.fex.notna()) & (zonas.fex != 0)] .groupby(var_zona) - .apply(lambda x: np.average(x['longitud'], weights=x['fex'])) + .apply(lambda x: np.average(x["longitud"], weights=x["fex"])) .reset_index() - .rename(columns={0: 'longitud'}), how='left') + .rename(columns={0: "longitud"}), + how="left", + ) hexs = hexs.merge( zonas[(zonas.fex.notna()) & (zonas.fex != 0)] .groupby(var_zona) - .apply(lambda x: np.average(x['latitud'], weights=x['fex'])) + .apply(lambda x: np.average(x["latitud"], weights=x["fex"])) .reset_index() - .rename(columns={0: 'latitud'}), how='left') + .rename(columns={0: "latitud"}), + how="left", + ) - tmp_o = f'{var_zona}_o' - tmp_d = f'{var_zona}_d' + tmp_o = f"{var_zona}_o" + tmp_d = f"{var_zona}_d" - if 'h3_' in tmp_o: + if "h3_" in tmp_o: tmp_h3_o = tmp_o tmp_h3_d = tmp_d else: @@ -1974,56 +2143,50 @@ def lineas_deseo(df, # Normalizo con nueva zonificación (ESTO HACE QUE TODOS LOS ORIGENES # Y DESTINOS TENGAN UN MISMO SENTIDO) if (tmp_o != tmp_h3_o) & (tmp_d != tmp_h3_d): - df_agg = df.groupby(['dia', tmp_h3_o, tmp_h3_d, tmp_o, - tmp_d], as_index=False).agg({var_fex: 'sum'}) + df_agg = df.groupby( + ["dia", tmp_h3_o, tmp_h3_d, tmp_o, tmp_d], as_index=False + ).agg({var_fex: "sum"}) else: - df_agg = df.groupby(['dia', tmp_h3_o, tmp_h3_d], - as_index=False).agg({var_fex: 'sum'}) + df_agg = df.groupby(["dia", tmp_h3_o, tmp_h3_d], as_index=False).agg( + {var_fex: "sum"} + ) if normalizo_latlon: - df_agg = normalizo_lat_lon(df_agg, - h3_o=tmp_h3_o, - h3_d=tmp_h3_d, - origen=tmp_o, - destino=tmp_d, - ) + df_agg = normalizo_lat_lon( + df_agg, + h3_o=tmp_h3_o, + h3_d=tmp_h3_d, + origen=tmp_o, + destino=tmp_d, + ) - tmp_o = f'{var_zona}_o_norm' - tmp_d = f'{var_zona}_d_norm' + tmp_o = f"{var_zona}_o_norm" + tmp_d = f"{var_zona}_d_norm" # Agrego a res de gráfico latlong - df_agg = df_agg.groupby(['dia', tmp_o, tmp_d], as_index=False).agg( - {var_fex: 'sum'}) + df_agg = df_agg.groupby(["dia", tmp_o, tmp_d], as_index=False).agg({var_fex: "sum"}) - df_agg = df_agg.groupby([tmp_o, tmp_d], as_index=False).agg( - {var_fex: 'mean'}) + df_agg = df_agg.groupby([tmp_o, tmp_d], as_index=False).agg({var_fex: "mean"}) df_agg[var_fex] = df_agg[var_fex].round().astype(int) df_agg = df_agg.merge( - hexs.rename(columns={var_zona: tmp_o, - 'latitud': 'lat_o', - 'longitud': 'lon_o'}) + hexs.rename(columns={var_zona: tmp_o, "latitud": "lat_o", "longitud": "lon_o"}) ) df_agg = df_agg.merge( - hexs.rename(columns={var_zona: tmp_d, - 'latitud': 'lat_d', - 'longitud': 'lon_d'}) + hexs.rename(columns={var_zona: tmp_d, "latitud": "lat_d", "longitud": "lon_d"}) ) - df_agg = df_agg.sort_values( - var_fex, ascending=False).reset_index(drop=True) - df_agg['cumsum'] = round( - df_agg[var_fex].cumsum() / df_agg[var_fex].sum() * 100) + df_agg = df_agg.sort_values(var_fex, ascending=False).reset_index(drop=True) + df_agg["cumsum"] = round(df_agg[var_fex].cumsum() / df_agg[var_fex].sum() * 100) - df_agg = df_agg[df_agg['cumsum'] <= porc_viajes] + df_agg = df_agg[df_agg["cumsum"] <= porc_viajes] df_agg = df_agg[df_agg[tmp_o] != df_agg[tmp_d]] if len(df_agg) > 0: try: - df_agg = crear_linestring( - df_agg, 'lon_o', 'lat_o', 'lon_d', 'lat_d') + df_agg = crear_linestring(df_agg, "lon_o", "lat_o", "lon_d", "lat_d") multip = df_agg[var_fex].head(1).values[0] / 10 @@ -2031,29 +2194,33 @@ def lineas_deseo(df, canvas = FigureCanvas(fig) ax = fig.add_subplot(111) - zonas[zonas['h3'].isin(df[h3_o].unique())].to_crs( - 3857).plot(ax=ax, alpha=0) + zonas[zonas["h3"].isin(df[h3_o].unique())].to_crs(3857).plot(ax=ax, alpha=0) # En caso de que no haya suficientes casos para 5 jenks try: - df_agg.to_crs(3857).plot(ax=ax, - alpha=alpha, - cmap=cmap, - lw=df_agg[var_fex]/multip, - column=var_fex, - scheme='FisherJenks', - k=k_jenks, - legend=True, - legend_kwds={ - 'loc': 'upper right', - 'title': 'Viajes', - 'fontsize': 8, - 'title_fontsize': 10, - } - ) + df_agg.to_crs(3857).plot( + ax=ax, + alpha=alpha, + cmap=cmap, + lw=df_agg[var_fex] / multip, + column=var_fex, + scheme="FisherJenks", + k=k_jenks, + legend=True, + legend_kwds={ + "loc": "upper right", + "title": "Viajes", + "fontsize": 8, + "title_fontsize": 10, + }, + ) try: - ctx.add_basemap(ax, source=ctx.providers.CartoDB.Positron, - attribution=None, attribution_size=10) + ctx.add_basemap( + ax, + source=ctx.providers.CartoDB.Positron, + attribution=None, + attribution_size=10, + ) except (r_ConnectionError, ValueError): pass @@ -2061,53 +2228,64 @@ def lineas_deseo(df, # leg._loc = 3 for lbl in leg.get_texts(): label_text = lbl.get_text() - lower = label_text.split(',')[0] - upper = label_text.split(',')[1] - new_text = f'{float(lower):,.0f} - {float(upper):,.0f}' + lower = label_text.split(",")[0] + upper = label_text.split(",")[1] + new_text = f"{float(lower):,.0f} - {float(upper):,.0f}" lbl.set_text(new_text) - title_ = f'{title}: {var_zona}s' + title_ = f"{title}: {var_zona}s" ax.set_title(title_, fontsize=12) - ax.add_artist( - ScaleBar(1, location='lower right', box_alpha=0, pad=1)) - ax.axis('off') + ax.add_artist(ScaleBar(1, location="lower right", box_alpha=0, pad=1)) + ax.axis("off") fig.tight_layout() if len(savefile) > 0: - savefile = savefile+'_lineas_deseo' + savefile = savefile + "_lineas_deseo" - print("Nuevos archivos en resultados: ", - savefile) - db_path = os.path.join( - "resultados", "png", f"{savefile}.png") + print("Nuevos archivos en resultados: ", savefile) + db_path = os.path.join("resultados", "png", f"{savefile}.png") fig.savefig(db_path, dpi=300, bbox_inches="tight") - db_path = os.path.join( - "resultados", "pdf", f"{savefile}.pdf") + db_path = os.path.join("resultados", "pdf", f"{savefile}.pdf") fig.savefig(db_path, dpi=300, bbox_inches="tight") # Guarda geojson para el dashboard # if not 'h3_r' in var_zona: df_folium = df_agg.copy() - df_folium.columns = ['Origen', 'Destino', 'Viajes', - 'lon_o', 'lat_o', 'lon_d', 'lat_d', - 'cumsum', 'geometry'] - - df_folium = df_folium[[ - 'Origen', 'Destino', 'Viajes', 'lon_o', 'lat_o', - 'lon_d', 'lat_d']] - - df_folium['desc_dia'] = desc_dia - df_folium['tipo_dia'] = tipo_dia - df_folium['var_zona'] = var_zona.replace( - 'h3_r', 'H3 Resolucion ') - df_folium['filtro1'] = filtro1 - - conn_dash = iniciar_conexion_db(tipo='dash') - var_zona_q = var_zona.replace('h3_r', 'H3 Resolucion ') + df_folium.columns = [ + "Origen", + "Destino", + "Viajes", + "lon_o", + "lat_o", + "lon_d", + "lat_d", + "cumsum", + "geometry", + ] + + df_folium = df_folium[ + [ + "Origen", + "Destino", + "Viajes", + "lon_o", + "lat_o", + "lon_d", + "lat_d", + ] + ] + + df_folium["desc_dia"] = desc_dia + df_folium["tipo_dia"] = tipo_dia + df_folium["var_zona"] = var_zona.replace("h3_r", "H3 Resolucion ") + df_folium["filtro1"] = filtro1 + + conn_dash = iniciar_conexion_db(tipo="dash") + var_zona_q = var_zona.replace("h3_r", "H3 Resolucion ") query = f""" DELETE FROM lineas_deseo @@ -2121,94 +2299,97 @@ def lineas_deseo(df, conn_dash.execute(query) conn_dash.commit() - df_folium.to_sql("lineas_deseo", conn_dash, - if_exists="append", index=False) + df_folium.to_sql( + "lineas_deseo", conn_dash, if_exists="append", index=False + ) conn_dash.close() - crear_mapa_folium(df_agg, - cmap, - var_fex, - savefile=f"{savefile}.html", - k_jenks=k_jenks) + crear_mapa_folium( + df_agg, + cmap, + var_fex, + savefile=f"{savefile}.html", + k_jenks=k_jenks, + ) if show_fig: display(fig) - except (ValueError) as e: + except ValueError as e: print(e) - except (ValueError) as e: + except ValueError as e: pass -def crea_df_burbujas(df, - zonas, - h3_o='h3_o', - var_fex='', - porc_viajes=100, - res=7): +def crea_df_burbujas(df, zonas, h3_o="h3_o", var_fex="", porc_viajes=100, res=7): - zonas['h3_o_tmp'] = zonas['h3'].apply(h3.h3_to_parent, res=res) + zonas["h3_o_tmp"] = zonas["h3"].apply(h3.h3_to_parent, res=res) - hexs = zonas[(zonas.fex.notna()) & (zonas.fex != 0)].groupby( - 'h3_o_tmp', as_index=False).size().drop(['size'], axis=1) + hexs = ( + zonas[(zonas.fex.notna()) & (zonas.fex != 0)] + .groupby("h3_o_tmp", as_index=False) + .size() + .drop(["size"], axis=1) + ) hexs = hexs.merge( zonas[(zonas.fex.notna()) & (zonas.fex != 0)] - .groupby('h3_o_tmp') - .apply(lambda x: np.average(x['longitud'], weights=x['fex'])) - .reset_index().rename(columns={0: 'longitud'}), how='left') + .groupby("h3_o_tmp") + .apply(lambda x: np.average(x["longitud"], weights=x["fex"])) + .reset_index() + .rename(columns={0: "longitud"}), + how="left", + ) hexs = hexs.merge( zonas[(zonas.fex.notna()) & (zonas.fex != 0)] - .groupby('h3_o_tmp') - .apply(lambda x: np.average(x['latitud'], weights=x['fex'])) + .groupby("h3_o_tmp") + .apply(lambda x: np.average(x["latitud"], weights=x["fex"])) .reset_index() - .rename(columns={0: 'latitud'}), how='left') + .rename(columns={0: "latitud"}), + how="left", + ) - df['h3_o_tmp'] = df[h3_o].apply(h3.h3_to_parent, res=res) + df["h3_o_tmp"] = df[h3_o].apply(h3.h3_to_parent, res=res) # Agrego a res de gráfico latlong - df_agg = df.groupby(['dia', 'h3_o_tmp'], - as_index=False).agg({var_fex: 'sum'}) - df_agg = df_agg.groupby( - ['h3_o_tmp'], as_index=False).agg({var_fex: 'mean'}) + df_agg = df.groupby(["dia", "h3_o_tmp"], as_index=False).agg({var_fex: "sum"}) + df_agg = df_agg.groupby(["h3_o_tmp"], as_index=False).agg({var_fex: "mean"}) df_agg = df_agg.merge( - hexs.rename(columns={'latitud': 'lat_o', - 'longitud': 'lon_o'}) + hexs.rename(columns={"latitud": "lat_o", "longitud": "lon_o"}) ) df_agg = gpd.GeoDataFrame( df_agg, - geometry=gpd.points_from_xy(df_agg['lon_o'], df_agg['lat_o']), - crs=4326,) + geometry=gpd.points_from_xy(df_agg["lon_o"], df_agg["lat_o"]), + crs=4326, + ) - df_agg = df_agg.sort_values( - var_fex, ascending=False).reset_index(drop=True) - df_agg['cumsum'] = round(df_agg[var_fex].cumsum() / - df_agg[var_fex].sum() * 100) - df_agg = df_agg[df_agg['cumsum'] < porc_viajes] + df_agg = df_agg.sort_values(var_fex, ascending=False).reset_index(drop=True) + df_agg["cumsum"] = round(df_agg[var_fex].cumsum() / df_agg[var_fex].sum() * 100) + df_agg = df_agg[df_agg["cumsum"] < porc_viajes] return df_agg -def crear_mapa_folium(df_agg, - cmap, - var_fex, - savefile, - k_jenks=5): +def crear_mapa_folium(df_agg, cmap, var_fex, savefile, k_jenks=5): - bins = [df_agg[var_fex].min()-1] + \ - mapclassify.FisherJenks(df_agg[var_fex], k=k_jenks).bins.tolist() - range_bins = range(0, len(bins)-1) - bins_labels = [ - f'{int(bins[n])} a {int(bins[n+1])} viajes' for n in range_bins] - df_agg['cuts'] = pd.cut(df_agg[var_fex], bins=bins, labels=bins_labels) + bins = [df_agg[var_fex].min() - 1] + mapclassify.FisherJenks( + df_agg[var_fex], k=k_jenks + ).bins.tolist() + range_bins = range(0, len(bins) - 1) + bins_labels = [f"{int(bins[n])} a {int(bins[n+1])} viajes" for n in range_bins] + df_agg["cuts"] = pd.cut(df_agg[var_fex], bins=bins, labels=bins_labels) from folium import Figure + fig = Figure(width=800, height=800) - m = folium.Map(location=[df_agg.lat_o.mean( - ), df_agg.lon_o.mean()], zoom_start=9, tiles='cartodbpositron') + m = folium.Map( + location=[df_agg.lat_o.mean(), df_agg.lon_o.mean()], + zoom_start=9, + tiles="cartodbpositron", + ) title_html = """

Your map title

@@ -2225,14 +2406,14 @@ def crear_mapa_folium(df_agg, df_agg[df_agg.cuts == i].explore( m=m, color=colors[n], - style_kwds={'fillOpacity': 0.3, 'weight': line_w}, + style_kwds={"fillOpacity": 0.3, "weight": line_w}, name=i, tooltip=False, ) n += 1 line_w += 3 - folium.LayerControl(name='xx').add_to(m) + folium.LayerControl(name="xx").add_to(m) fig.add_child(m) @@ -2244,16 +2425,16 @@ def save_zones(): """ Esta función guarda las geografí­as de las zonas para el dashboard """ - print('Creando zonificación para dashboard') + print("Creando zonificación para dashboard") configs = leer_configs_generales() try: - zonificaciones = configs['zonificaciones'] + zonificaciones = configs["zonificaciones"] except KeyError: zonificaciones = [] - geo_files = [['zona_voi.geojson', 'Zona_voi']] + geo_files = [["zona_voi.geojson", "Zona_voi"]] if zonificaciones: for n in range(0, 5): @@ -2268,62 +2449,68 @@ def save_zones(): zonas = pd.DataFrame([]) for i in geo_files: - file = os.path.join("data", "data_ciudad", f'{i[0]}') + file = os.path.join("data", "data_ciudad", f"{i[0]}") if os.path.isfile(file): df = gpd.read_file(file) - df = df[[i[1], 'geometry']] - df.columns = ['Zona', 'geometry'] - df['tipo_zona'] = i[1] + df = df[[i[1], "geometry"]] + df.columns = ["Zona", "geometry"] + df["tipo_zona"] = i[1] zonas = pd.concat([zonas, df]) - zonas = zonas.dissolve(by=['tipo_zona', 'Zona'], as_index=False) - zonas['wkt'] = zonas.geometry.to_wkt() - zonas = zonas.drop(['geometry'], axis=1) + zonas = zonas.dissolve(by=["tipo_zona", "Zona"], as_index=False) + zonas["wkt"] = zonas.geometry.to_wkt() + zonas = zonas.drop(["geometry"], axis=1) - conn_dash = iniciar_conexion_db(tipo='dash') + conn_dash = iniciar_conexion_db(tipo="dash") zonas.to_sql("zonas", conn_dash, if_exists="replace", index=False) conn_dash.close() def particion_modal(viajes_dia, etapas_dia, tipo_dia, desc_dia): - particion_viajes = viajes_dia.groupby( - 'modo', as_index=False).factor_expansion_linea.sum().round() - particion_viajes['modal'] = (particion_viajes['factor_expansion_linea'] / - viajes_dia.factor_expansion_linea.sum() * 100 - ).round() - particion_viajes = particion_viajes.sort_values( - 'modal', ascending=False).drop(['factor_expansion_linea'], axis=1) - particion_viajes['tipo'] = 'viajes' - particion_viajes['tipo_dia'] = tipo_dia - particion_viajes['desc_dia'] = desc_dia - particion_etapas = etapas_dia.groupby( - 'modo', as_index=False).factor_expansion_linea.sum().round() - - particion_etapas['modal'] = (particion_etapas['factor_expansion_linea'] / - etapas_dia.factor_expansion_linea.sum() * 100 - ).round() - particion_etapas = particion_etapas.sort_values( - 'modal', ascending=False).drop(['factor_expansion_linea'], axis=1) - particion_etapas['tipo'] = 'etapas' - particion_etapas['desc_dia'] = desc_dia - particion_etapas['tipo_dia'] = tipo_dia - particion = pd.concat( - [particion_viajes, particion_etapas], ignore_index=True) - - conn_dash = iniciar_conexion_db(tipo='dash') + particion_viajes = ( + viajes_dia.groupby("modo", as_index=False).factor_expansion_linea.sum().round() + ) + particion_viajes["modal"] = ( + particion_viajes["factor_expansion_linea"] + / viajes_dia.factor_expansion_linea.sum() + * 100 + ).round() + particion_viajes = particion_viajes.sort_values("modal", ascending=False).drop( + ["factor_expansion_linea"], axis=1 + ) + particion_viajes["tipo"] = "viajes" + particion_viajes["tipo_dia"] = tipo_dia + particion_viajes["desc_dia"] = desc_dia + particion_etapas = ( + etapas_dia.groupby("modo", as_index=False).factor_expansion_linea.sum().round() + ) + + particion_etapas["modal"] = ( + particion_etapas["factor_expansion_linea"] + / etapas_dia.factor_expansion_linea.sum() + * 100 + ).round() + particion_etapas = particion_etapas.sort_values("modal", ascending=False).drop( + ["factor_expansion_linea"], axis=1 + ) + particion_etapas["tipo"] = "etapas" + particion_etapas["desc_dia"] = desc_dia + particion_etapas["tipo_dia"] = tipo_dia + particion = pd.concat([particion_viajes, particion_etapas], ignore_index=True) + + conn_dash = iniciar_conexion_db(tipo="dash") query = f'DELETE FROM particion_modal WHERE desc_dia = "{desc_dia}" & tipo_dia = "{tipo_dia}"' conn_dash.execute(query) conn_dash.commit() - particion['modo'] = particion.modo.str.capitalize() - particion.to_sql("particion_modal", conn_dash, - if_exists="append", index=False) + particion["modo"] = particion.modo.str.capitalize() + particion.to_sql("particion_modal", conn_dash, if_exists="append", index=False) conn_dash.close() def plot_dispatched_services_wrapper(): - conn_data = iniciar_conexion_db(tipo='data') + conn_data = iniciar_conexion_db(tipo="data") q = """ select * @@ -2333,8 +2520,7 @@ def plot_dispatched_services_wrapper(): service_data = pd.read_sql(q, conn_data) if len(service_data) > 0: - service_data.groupby(['id_linea']).apply( - plot_dispatched_services_by_line_day) + service_data.groupby(["id_linea"]).apply(plot_dispatched_services_by_line_day) conn_data.close() @@ -2359,57 +2545,54 @@ def plot_dispatched_services_by_line_day(df): line_id = df.id_linea.unique().item() day = df.dia.unique().item() - if day == 'weekend': - day_str = 'Fin de semana tipo' - elif day == 'weekday': - day_str = 'Dia de semana tipo' + if day == "weekend": + day_str = "Fin de semana tipo" + elif day == "weekday": + day_str = "Dia de semana tipo" else: day_str = day - conn_insumos = iniciar_conexion_db(tipo='insumos') + conn_insumos = iniciar_conexion_db(tipo="insumos") - s = f"select nombre_linea from metadata_lineas" +\ - f" where id_linea = {line_id};" + s = f"select nombre_linea from metadata_lineas" + f" where id_linea = {line_id};" id_linea_str = pd.read_sql(s, conn_insumos) conn_insumos.close() if len(id_linea_str) > 0: id_linea_str = id_linea_str.nombre_linea.item() - id_linea_str = id_linea_str + ' -' + id_linea_str = id_linea_str + " -" else: - id_linea_str = '' + id_linea_str = "" print("Creando plot de servicios despachados por linea", "id linea:", line_id) f, ax = plt.subplots(figsize=(8, 6)) - sns.barplot( - data=df, - x="hora", - y="servicios", - hue="id_linea", - ax=ax) + sns.barplot(data=df, x="hora", y="servicios", hue="id_linea", ax=ax) ax.get_legend().remove() ax.set_xlabel("Hora") ax.set_ylabel("Cantidad de servicios despachados") - f.suptitle(f"Cantidad de servicios despachados por hora y dí­a", - fontdict={'size': 18, - 'weight': 'bold'}) - ax.set_title(f"{id_linea_str} id linea: {line_id} - Dia: {day_str}", - fontdict={"fontsize": 11}) + f.suptitle( + f"Cantidad de servicios despachados por hora y dí­a", + fontdict={"size": 18, "weight": "bold"}, + ) + ax.set_title( + f"{id_linea_str} id linea: {line_id} - Dia: {day_str}", + fontdict={"fontsize": 11}, + ) ax.spines.right.set_visible(False) ax.spines.top.set_visible(False) ax.spines.bottom.set_visible(False) ax.spines.left.set_visible(False) - ax.spines.left.set_position(('outward', 10)) - ax.spines.bottom.set_position(('outward', 10)) + ax.spines.left.set_position(("outward", 10)) + ax.spines.bottom.set_position(("outward", 10)) - ax.grid(axis='y') + ax.grid(axis="y") - for frm in ['png', 'pdf']: - archivo = f'servicios_despachados_id_linea_{line_id}_{day}.{frm}' + for frm in ["png", "pdf"]: + archivo = f"servicios_despachados_id_linea_{line_id}_{day}.{frm}" db_path = os.path.join("resultados", frm, archivo) f.savefig(db_path, dpi=300) plt.close() @@ -2418,7 +2601,7 @@ def plot_dispatched_services_by_line_day(df): def plot_basic_kpi_wrapper(): sns.set_style("whitegrid") - conn_data = iniciar_conexion_db(tipo='data') + conn_data = iniciar_conexion_db(tipo="data") q = """ select * @@ -2428,53 +2611,56 @@ def plot_basic_kpi_wrapper(): kpi_data = pd.read_sql(q, conn_data) if len(kpi_data) > 0: - kpi_data.groupby(['id_linea']).apply( - plot_basic_kpi, standarize_supply_demand=False) + kpi_data.groupby(["id_linea", "yr_mo"]).apply( + plot_basic_kpi, standarize_supply_demand=False + ) conn_data.close() -def plot_basic_kpi(kpi_by_line_hr, standarize_supply_demand=False, - *args, **kwargs): +def plot_basic_kpi(kpi_by_line_hr, standarize_supply_demand=False, *args, **kwargs): line_id = kpi_by_line_hr.id_linea.unique().item() day = kpi_by_line_hr.dia.unique().item() alias = leer_alias() + mes = kpi_by_line_hr.yr_mo.unique()[0] - if day == 'weekend': - day_str = 'Fin de semana tipo' - elif day == 'weekday': - day_str = 'Dia de semana tipo' + if day == "weekend": + day_str = "Fin de semana" + elif day == "weekday": + day_str = "Dia habil" else: day_str = day - conn_insumos = iniciar_conexion_db(tipo='insumos') + conn_insumos = iniciar_conexion_db(tipo="insumos") - s = f"select nombre_linea from metadata_lineas" +\ - f" where id_linea = {line_id} AND nombre_linea IS NOT NULL;" + s = ( + f"select nombre_linea from metadata_lineas" + + f" where id_linea = {line_id} AND nombre_linea IS NOT NULL;" + ) id_linea_str = pd.read_sql(s, conn_insumos) conn_insumos.close() if len(id_linea_str) > 0: id_linea_str = id_linea_str.nombre_linea.item() - id_linea_str = id_linea_str + ' -' + id_linea_str = id_linea_str + " -" else: - id_linea_str = '' + id_linea_str = "" # Create empty df with 0 - 23 hrs kpi_stats_line_plot = pd.DataFrame( - {'id_linea': [line_id] * 24, 'hora': range(0, 24)}) + {"id_linea": [line_id] * 24, "hora": range(0, 24)} + ) - kpi_stats_line_plot = kpi_stats_line_plot\ - .merge(kpi_by_line_hr.query(f"id_linea == {line_id}"), - on=['id_linea', 'hora'], - how='left') + kpi_stats_line_plot = kpi_stats_line_plot.merge( + kpi_by_line_hr.query(f"id_linea == {line_id}"), + on=["id_linea", "hora"], + how="left", + ) if standarize_supply_demand: - supply_factor = kpi_stats_line_plot.of.max()\ - / kpi_stats_line_plot.veh.max() - demand_factor = kpi_stats_line_plot.of.max()\ - / kpi_stats_line_plot.pax.max() + supply_factor = kpi_stats_line_plot.of.max() / kpi_stats_line_plot.veh.max() + demand_factor = kpi_stats_line_plot.of.max() / kpi_stats_line_plot.pax.max() kpi_stats_line_plot.veh = kpi_stats_line_plot.veh * supply_factor kpi_stats_line_plot.pax = kpi_stats_line_plot.pax * demand_factor note = """ @@ -2483,88 +2669,120 @@ def plot_basic_kpi(kpi_by_line_hr, standarize_supply_demand=False, """ ylabel_str = "Factor de Ocupación (%)" else: - kpi_stats_line_plot.veh = kpi_stats_line_plot.veh / \ - kpi_stats_line_plot.veh.sum() * 100 - kpi_stats_line_plot.pax = kpi_stats_line_plot.pax / \ - kpi_stats_line_plot.pax.sum() * 100 + kpi_stats_line_plot.veh = ( + kpi_stats_line_plot.veh / kpi_stats_line_plot.veh.sum() * 100 + ) + kpi_stats_line_plot.pax = ( + kpi_stats_line_plot.pax / kpi_stats_line_plot.pax.sum() * 100 + ) note = """ Oferta y Demanda expresan la distribución porcentual por hora de la sumatoria de veh-hr y de los pax-hr respectivamente """ ylabel_str = "%" - missing_data = (kpi_stats_line_plot.pax.isna().all()) |\ - (kpi_stats_line_plot.dmt.isna().all()) |\ - (kpi_stats_line_plot.of.isna().all()) + missing_data = ( + (kpi_stats_line_plot.pax.isna().all()) + | (kpi_stats_line_plot.dmt.isna().all()) + | (kpi_stats_line_plot.of.isna().all()) + ) if missing_data: - print("No es posible crear plot de KPI basicos por linea", - "id linea:", line_id) + print("No es posible crear plot de KPI basicos por linea", "id linea:", line_id) else: print("Creando plot de KPI basicos por linea", "id linea:", line_id) f, ax = plt.subplots(figsize=(8, 6)) - sns.barplot(data=kpi_stats_line_plot, x='hora', y='of', - color='silver', ax=ax, label='Factor de ocupación') + sns.barplot( + data=kpi_stats_line_plot, + x="hora", + y="of", + color="silver", + ax=ax, + label="Factor de ocupación", + ) - sns.lineplot(data=kpi_stats_line_plot, x="hora", y="veh", ax=ax, - color='Purple', label='Oferta') - sns.lineplot(data=kpi_stats_line_plot, x="hora", y="pax", ax=ax, - color='Orange', label='Demanda') + sns.lineplot( + data=kpi_stats_line_plot, + x="hora", + y="veh", + ax=ax, + color="Purple", + label="Oferta", + ) + sns.lineplot( + data=kpi_stats_line_plot, + x="hora", + y="pax", + ax=ax, + color="Orange", + label="Demanda", + ) ax.set_xlabel("Hora") ax.set_ylabel(ylabel_str) - f.suptitle(f"Indicadores de oferta y demanda estadarizados") + f.suptitle( + f"Indicadores de oferta y demanda estadarizados", + fontdict={"size": 18, "weight": "bold"}, + ) - ax.set_title(f"{id_linea_str} id linea: {line_id} - Dia: {day_str}", - fontdict={"fontsize": 11}) + ax.set_title( + f"{id_linea_str} id linea: {line_id} - Dia: {day_str} - Mes: {mes}", + fontdict={"fontsize": 11}, + ) # Add a footnote below and to the right side of the chart - ax_note = ax.annotate(note, - xy=(0, -.18), - xycoords='axes fraction', - ha='left', - va="center", - fontsize=10) + ax_note = ax.annotate( + note, + xy=(0, -0.18), + xycoords="axes fraction", + ha="left", + va="center", + fontsize=10, + ) ax.spines.right.set_visible(False) ax.spines.top.set_visible(False) ax.spines.bottom.set_visible(False) ax.spines.left.set_visible(False) - ax.spines.left.set_position(('outward', 10)) - ax.spines.bottom.set_position(('outward', 10)) + ax.spines.left.set_position(("outward", 10)) + ax.spines.bottom.set_position(("outward", 10)) - for frm in ['png', 'pdf']: - archivo = f'{alias}_kpi_basicos_id_linea_{line_id}_{day}.{frm}' + for frm in ["png", "pdf"]: + archivo = f"{alias}_{mes}({day_str})_kpi_basicos_id_linea_{line_id}.{frm}" db_path = os.path.join("resultados", frm, archivo) - f.savefig(db_path, dpi=300, bbox_extra_artists=( - ax_note,), bbox_inches='tight') + f.savefig( + db_path, dpi=300, bbox_extra_artists=(ax_note,), bbox_inches="tight" + ) plt.close() # add to dash - kpi_stats_line_plot['nombre_linea'] = id_linea_str - kpi_stats_line_plot['dia'] = day - kpi_stats_line_plot = kpi_stats_line_plot\ - .reindex(columns=[ - 'dia', - 'id_linea', - 'nombre_linea', - 'hora', - 'veh', - 'pax', - 'dmt', - 'of', - 'speed_kmh'] - ) + kpi_stats_line_plot["nombre_linea"] = id_linea_str + kpi_stats_line_plot["dia"] = day + kpi_stats_line_plot = kpi_stats_line_plot.reindex( + columns=[ + "dia", + "yr_mo", + "id_linea", + "nombre_linea", + "hora", + "veh", + "pax", + "dmt", + "of", + "speed_kmh", + ] + ) - conn_dash = iniciar_conexion_db(tipo='dash') + conn_dash = iniciar_conexion_db(tipo="dash") query = f""" DELETE FROM basic_kpi_by_line_hr WHERE dia = "{day}" and id_linea = "{line_id}" + and yr_mo = "{mes}" """ conn_dash.execute(query) conn_dash.commit() @@ -2583,7 +2801,7 @@ def get_branch_geoms_from_line(id_linea): Takes a line id and returns a geoSeries with all branches' geoms """ - conn_insumos = iniciar_conexion_db(tipo='insumos') + conn_insumos = iniciar_conexion_db(tipo="insumos") branch_geoms_query = f""" select * from branches_geoms bg @@ -2595,9 +2813,8 @@ def get_branch_geoms_from_line(id_linea): """ branch_geoms = pd.read_sql(branch_geoms_query, conn_insumos) branch_geoms = gpd.GeoSeries.from_wkt( - branch_geoms.wkt.values, - index=branch_geoms.id_ramal.values, - crs='EPSG:4326') + branch_geoms.wkt.values, index=branch_geoms.id_ramal.values, crs="EPSG:4326" + ) conn_insumos.close() @@ -2622,19 +2839,18 @@ def create_squared_polygon(min_x, min_y, max_x, max_y, epsg): (square_bbox_min_x, square_bbox_min_y), (square_bbox_max_x, square_bbox_min_y), (square_bbox_max_x, square_bbox_max_y), - (square_bbox_min_x, square_bbox_max_y) + (square_bbox_min_x, square_bbox_max_y), ] p = Polygon(square_bbox_coords) - s = gpd.GeoSeries([p], crs=f'EPSG:{epsg}') + s = gpd.GeoSeries([p], crs=f"EPSG:{epsg}") return s def format_num(num, lpad=10): - fnum = '{:,}'.format(num).replace( - ".", "*").replace(",", ".").replace("*", ",") + fnum = "{:,}".format(num).replace(".", "*").replace(",", ".").replace("*", ",") if lpad > 0: - fnum = fnum.rjust(lpad, ' ') + fnum = fnum.rjust(lpad, " ") return fnum @@ -2643,7 +2859,7 @@ def indicadores_dash(): configs = leer_configs_generales() - conn_data = iniciar_conexion_db(tipo='data') + conn_data = iniciar_conexion_db(tipo="data") indicadores = pd.read_sql_query( """ @@ -2653,95 +2869,143 @@ def indicadores_dash(): conn_data, ) - hora_punta = indicadores[indicadores.detalle.str.contains('Hora punta')] - indicadores = indicadores[~indicadores.detalle.str.contains('Hora punta')] - - indicadores['dia'] = pd.to_datetime(indicadores.dia) - indicadores['dow'] = indicadores.dia.dt.dayofweek - indicadores['mo'] = indicadores.dia.dt.month - indicadores['yr'] = indicadores.dia.dt.year - - indicadores['desc_dia'] = indicadores['yr'].astype(str).str.zfill( - 4) + '-' + indicadores['mo'].astype(str).str.zfill(2) - indicadores['tipo_dia'] = 'Hábil' - indicadores.loc[indicadores.dow >= 5, 'tipo_dia'] = 'Fin de semana' - - indicadores = indicadores.groupby(['desc_dia', 'tipo_dia', 'detalle'], as_index=False).agg({ - 'indicador': 'mean', 'porcentaje': 'mean'}) - indicadores.loc[indicadores.detalle == 'Cantidad de etapas con destinos validados', - 'detalle'] = 'Transacciones válidas \n(Etapas con destinos validados)' - indicadores.loc[indicadores.detalle == - 'Cantidad total de viajes expandidos', 'detalle'] = 'Viajes' - indicadores.loc[indicadores.detalle == - 'Cantidad total de etapas', 'detalle'] = 'Etapas' - indicadores.loc[indicadores.detalle == - 'Cantidad total de usuarios', 'detalle'] = 'Usuarios' - indicadores.loc[indicadores.detalle == - 'Cantidad de viajes cortos (<5kms)', 'detalle'] = 'Viajes cortos (<5kms)' - indicadores.loc[indicadores.detalle == 'Cantidad de viajes con transferencia', - 'detalle'] = 'Viajes con transferencia' + hora_punta = indicadores[indicadores.detalle.str.contains("Hora punta")] + indicadores = indicadores[~indicadores.detalle.str.contains("Hora punta")] + + indicadores["dia"] = pd.to_datetime(indicadores.dia) + indicadores["dow"] = indicadores.dia.dt.dayofweek + indicadores["mo"] = indicadores.dia.dt.month + indicadores["yr"] = indicadores.dia.dt.year + + indicadores["desc_dia"] = ( + indicadores["yr"].astype(str).str.zfill(4) + + "-" + + indicadores["mo"].astype(str).str.zfill(2) + ) + indicadores["tipo_dia"] = "Hábil" + indicadores.loc[indicadores.dow >= 5, "tipo_dia"] = "Fin de semana" + + indicadores = indicadores.groupby( + ["desc_dia", "tipo_dia", "detalle"], as_index=False + ).agg({"indicador": "mean", "porcentaje": "mean"}).round(2) + + indicadores.loc[ + indicadores.detalle == "Cantidad de etapas con destinos validados", "detalle" + ] = "Transacciones válidas \n(Etapas con destinos validados)" + indicadores.loc[ + indicadores.detalle == "Cantidad total de viajes expandidos", "detalle" + ] = "Viajes" + indicadores.loc[indicadores.detalle == "Cantidad total de etapas", "detalle"] = ( + "Etapas" + ) + indicadores.loc[indicadores.detalle == "Cantidad total de usuarios", "detalle"] = ( + "Usuarios" + ) + indicadores.loc[ + indicadores.detalle == "Cantidad de viajes cortos (<5kms)", "detalle" + ] = "Viajes cortos (<5kms)" + indicadores.loc[ + indicadores.detalle == "Cantidad de viajes con transferencia", "detalle" + ] = "Viajes con transferencia" conn_data.close() - indicadores.loc[indicadores.detalle.isin(['Cantidad de transacciones totales', - 'Cantidad de tarjetas únicas', - 'Cantidad de transacciones limpias', - ]), 'orden'] = 1 - - indicadores.loc[indicadores.detalle.str.contains( - 'Transacciones válidas'), 'orden'] = 1 - - indicadores.loc[indicadores.detalle.isin(['Viajes', - 'Etapas', - 'Usuarios', - 'Viajes cortos (<5kms)', - 'Viajes con transferencia', - 'Distancia de los viajes (promedio en kms)', - 'Distancia de los viajes (mediana en kms)' - ]), 'orden'] = 2 - - indicadores.loc[indicadores.detalle.isin(['Viajes autobus', - 'Viajes Multietapa', - 'Viajes Multimodal', - 'Viajes metro', - 'Viajes tren' - ]), 'orden'] = 3 - - indicadores['Valor'] = indicadores.indicador.apply(format_num) - indicadores['porcentaje'] = indicadores.porcentaje.apply(format_num) + indicadores.loc[ + indicadores.detalle.isin( + [ + "Cantidad de transacciones totales", + "Cantidad de tarjetas únicas", + "Cantidad de transacciones limpias", + ] + ), + "orden", + ] = 1 + + indicadores.loc[ + indicadores.detalle.str.contains("Transacciones válidas"), "orden" + ] = 1 + + indicadores.loc[ + indicadores.detalle.isin( + [ + "Viajes", + "Etapas", + "Usuarios", + "Viajes cortos (<5kms)", + "Viajes con transferencia", + "Distancia de los viajes (promedio en kms)", + "Distancia de los viajes (mediana en kms)", + ] + ), + "orden", + ] = 2 + + indicadores.loc[ + indicadores.detalle.isin( + [ + "Viajes autobus", + "Viajes Multietapa", + "Viajes Multimodal", + "Viajes metro", + "Viajes tren", + ] + ), + "orden", + ] = 3 + + indicadores["Valor"] = indicadores.indicador.apply(format_num) + indicadores["porcentaje"] = indicadores.porcentaje.apply(format_num) indicadores = indicadores[indicadores.orden.notna()] - indicadores.loc[~(indicadores.detalle.str.contains('Distancia')), 'Valor'] = indicadores.loc[~( - indicadores.detalle.str.contains('Distancia')), 'Valor'].str.split(',').str[0] - - indicadores = indicadores.drop(['indicador'], axis=1) - indicadores = indicadores.rename(columns={'detalle': 'Indicador'}) + indicadores.loc[~(indicadores.detalle.str.contains("Distancia")), "Valor"] = ( + indicadores.loc[~(indicadores.detalle.str.contains("Distancia")), "Valor"] + .str.split(",") + .str[0] + ) - indicadores.loc[indicadores.Indicador.str.contains('Transacciones válidas'), - 'Valor'] += ' ('+indicadores.loc[ - indicadores.Indicador.str.contains('Transacciones válidas'), - 'porcentaje'].str.replace(' ', '')+'%)' + indicadores = indicadores.drop(["indicador"], axis=1) + indicadores = indicadores.rename(columns={"detalle": "Indicador"}) + + indicadores.loc[ + indicadores.Indicador.str.contains("Transacciones válidas"), "Valor" + ] += ( + " (" + + indicadores.loc[ + indicadores.Indicador.str.contains("Transacciones válidas"), "porcentaje" + ].str.replace(" ", "") + + "%)" + ) - indicadores.loc[indicadores.orden == 3, - 'Valor'] += ' ('+indicadores.loc[indicadores.orden == 3, - 'porcentaje'].str.replace(' ', '')+'%)' + indicadores.loc[indicadores.orden == 3, "Valor"] += ( + " (" + + indicadores.loc[indicadores.orden == 3, "porcentaje"].str.replace(" ", "") + + "%)" + ) - indicadores.loc[indicadores.Indicador == 'Viajes cortos (<5kms)', - 'Valor'] += ' ('+indicadores.loc[ - indicadores.Indicador == 'Viajes cortos (<5kms)', 'porcentaje'].str.replace(' ', '')+'%)' - indicadores.loc[indicadores.Indicador == 'Viajes con transferencia', - 'Valor'] += ' ('+indicadores.loc[ - indicadores.Indicador == 'Viajes con transferencia', 'porcentaje'].str.replace(' ', '')+'%)' + indicadores.loc[indicadores.Indicador == "Viajes cortos (<5kms)", "Valor"] += ( + " (" + + indicadores.loc[ + indicadores.Indicador == "Viajes cortos (<5kms)", "porcentaje" + ].str.replace(" ", "") + + "%)" + ) + indicadores.loc[indicadores.Indicador == "Viajes con transferencia", "Valor"] += ( + " (" + + indicadores.loc[ + indicadores.Indicador == "Viajes con transferencia", "porcentaje" + ].str.replace(" ", "") + + "%)" + ) - indicadores.loc[indicadores.orden == 1, - 'Titulo'] = 'Información del dataset original' - indicadores.loc[indicadores.orden == 2, 'Titulo'] = 'Información procesada' - indicadores.loc[indicadores.orden == 3, 'Titulo'] = 'Partición modal' + indicadores.loc[indicadores.orden == 1, "Titulo"] = ( + "Información del dataset original" + ) + indicadores.loc[indicadores.orden == 2, "Titulo"] = "Información procesada" + indicadores.loc[indicadores.orden == 3, "Titulo"] = "Partición modal" - conn_dash = iniciar_conexion_db(tipo='dash') - indicadores.to_sql("indicadores", conn_dash, - if_exists="replace", index=False) + conn_dash = iniciar_conexion_db(tipo="dash") + indicadores.to_sql("indicadores", conn_dash, if_exists="replace", index=False) conn_dash.close() @@ -2754,8 +3018,8 @@ def create_visualizations(): pd.options.mode.chained_assignment = None # Leer informacion de viajes y distancias - conn_data = iniciar_conexion_db(tipo='data') - conn_insumos = iniciar_conexion_db(tipo='insumos') + conn_data = iniciar_conexion_db(tipo="data") + conn_insumos = iniciar_conexion_db(tipo="insumos") viajes = pd.read_sql_query( """ @@ -2787,104 +3051,113 @@ def create_visualizations(): conn_data.close() # Agrego campo de distancias de los viajes - viajes = viajes.merge(distancias, - how='left', - on=['h3_o', 'h3_d']) + viajes = viajes.merge(distancias, how="left", on=["h3_o", "h3_d"]) # Imputar anio, mes y tipo de dia - viajes['yr'] = pd.to_datetime(viajes.dia).dt.year - viajes['mo'] = pd.to_datetime(viajes.dia).dt.month - viajes['dow'] = pd.to_datetime(viajes.dia).dt.day_of_week - viajes.loc[viajes.dow >= 5, 'tipo_dia'] = 'Fin de semana' - viajes.loc[viajes.dow < 5, 'tipo_dia'] = 'Dia habil' + viajes["yr"] = pd.to_datetime(viajes.dia).dt.year + viajes["mo"] = pd.to_datetime(viajes.dia).dt.month + viajes["dow"] = pd.to_datetime(viajes.dia).dt.day_of_week + viajes.loc[viajes.dow >= 5, "tipo_dia"] = "Fin de semana" + viajes.loc[viajes.dow < 5, "tipo_dia"] = "Dia habil" # Imputar anio, mes y tipo de dia - etapas['yr'] = pd.to_datetime(etapas.dia).dt.year - etapas['mo'] = pd.to_datetime(etapas.dia).dt.month - etapas['dow'] = pd.to_datetime(etapas.dia).dt.day_of_week - etapas.loc[etapas.dow >= 5, 'tipo_dia'] = 'Fin de semana' - etapas.loc[etapas.dow < 5, 'tipo_dia'] = 'Dia habil' - - v_iter = viajes\ - .groupby(['yr', 'mo', 'tipo_dia'], as_index=False)\ - .factor_expansion_linea.sum()\ + etapas["yr"] = pd.to_datetime(etapas.dia).dt.year + etapas["mo"] = pd.to_datetime(etapas.dia).dt.month + etapas["dow"] = pd.to_datetime(etapas.dia).dt.day_of_week + etapas.loc[etapas.dow >= 5, "tipo_dia"] = "Fin de semana" + etapas.loc[etapas.dow < 5, "tipo_dia"] = "Dia habil" + + v_iter = ( + viajes.groupby(["yr", "mo", "tipo_dia"], as_index=False) + .factor_expansion_linea.sum() .iterrows() + ) for _, i in v_iter: - desc_dia = f'{i.yr}-{str(i.mo).zfill(2)} ({i.tipo_dia})' - desc_dia_file = f'{i.yr}-{str(i.mo).zfill(2)}({i.tipo_dia})' + desc_dia = f"{i.yr}-{str(i.mo).zfill(2)} ({i.tipo_dia})" + desc_dia_file = f"{i.yr}-{str(i.mo).zfill(2)}({i.tipo_dia})" - viajes_dia = viajes[(viajes.yr == i.yr) & ( - viajes.mo == i.mo) & (viajes.tipo_dia == i.tipo_dia)] + viajes_dia = viajes[ + (viajes.yr == i.yr) & (viajes.mo == i.mo) & (viajes.tipo_dia == i.tipo_dia) + ] - etapas_dia = etapas[(etapas.yr == i.yr) & ( - etapas.mo == i.mo) & (etapas.tipo_dia == i.tipo_dia)] + etapas_dia = etapas[ + (etapas.yr == i.yr) & (etapas.mo == i.mo) & (etapas.tipo_dia == i.tipo_dia) + ] # partición modal - particion_modal(viajes_dia, etapas_dia, - tipo_dia=i.tipo_dia, desc_dia=desc_dia) + particion_modal(viajes_dia, etapas_dia, tipo_dia=i.tipo_dia, desc_dia=desc_dia) - print('Imprimiendo tabla de matrices OD') + print("Imprimiendo tabla de matrices OD") # Impirmir tablas con matrices OD - imprimir_matrices_od(viajes=viajes_dia, - var_fex='factor_expansion_linea', - title=f'Matriz OD {desc_dia}', - savefile=f'{desc_dia_file}', - desc_dia=f'{i.yr}-{str(i.mo).zfill(2)}', - tipo_dia=i.tipo_dia, - ) - - print('Imprimiendo mapas de lí­neas de deseo') + imprimir_matrices_od( + viajes=viajes_dia, + var_fex="factor_expansion_linea", + title=f"Matriz OD {desc_dia}", + savefile=f"{desc_dia_file}", + desc_dia=f"{i.yr}-{str(i.mo).zfill(2)}", + tipo_dia=i.tipo_dia, + ) + + print("Imprimiendo mapas de lí­neas de deseo") # Imprimir lineas de deseo - imprime_lineas_deseo(df=viajes_dia, - h3_o='', - h3_d='', - var_fex='factor_expansion_linea', - title=f'Lí­neas de deseo {desc_dia}', - savefile=f'{desc_dia_file}', - desc_dia=f'{i.yr}-{str(i.mo).zfill(2)}', - tipo_dia=i.tipo_dia) - - print('Imprimiendo gráficos') - titulo = f'Cantidad de viajes en transporte público {desc_dia}' - imprime_graficos_hora(viajes_dia, - title=titulo, - savefile=f'{desc_dia_file}_viajes', - var_fex='factor_expansion_linea', - desc_dia=f'{i.yr}-{str(i.mo).zfill(2)}', - tipo_dia=i.tipo_dia) - - print('Imprimiendo mapas de burbujas') + imprime_lineas_deseo( + df=viajes_dia, + h3_o="", + h3_d="", + var_fex="factor_expansion_linea", + title=f"Lí­neas de deseo {desc_dia}", + savefile=f"{desc_dia_file}", + desc_dia=f"{i.yr}-{str(i.mo).zfill(2)}", + tipo_dia=i.tipo_dia, + ) + + print("Imprimiendo gráficos") + titulo = f"Cantidad de viajes en transporte público {desc_dia}" + imprime_graficos_hora( + viajes_dia, + title=titulo, + savefile=f"{desc_dia_file}_viajes", + var_fex="factor_expansion_linea", + desc_dia=f"{i.yr}-{str(i.mo).zfill(2)}", + tipo_dia=i.tipo_dia, + ) + + print("Imprimiendo mapas de burbujas") viajes_n = viajes_dia[(viajes_dia.id_viaje > 1)] - imprime_burbujas(viajes_n, - res=7, - h3_o='h3_o', - alpha=.4, - cmap='rocket_r', - var_fex='factor_expansion_linea', - porc_viajes=100, - title=f'Destinos de los viajes {desc_dia}', - savefile=f'{desc_dia_file}_burb_destinos', - show_fig=False, - k_jenks=5) + imprime_burbujas( + viajes_n, + res=7, + h3_o="h3_o", + alpha=0.4, + cmap="rocket_r", + var_fex="factor_expansion_linea", + porc_viajes=100, + title=f"Destinos de los viajes {desc_dia}", + savefile=f"{desc_dia_file}_burb_destinos", + show_fig=False, + k_jenks=5, + ) viajes_n = viajes_dia[(viajes_dia.id_viaje == 1)] - imprime_burbujas(viajes_n, - res=7, - h3_o='h3_o', - alpha=.4, - cmap='flare', - var_fex='factor_expansion_linea', - porc_viajes=100, - title=f'Hogares {desc_dia}', - savefile=f'{desc_dia_file}_burb_hogares', - show_fig=False, - k_jenks=5) + imprime_burbujas( + viajes_n, + res=7, + h3_o="h3_o", + alpha=0.4, + cmap="flare", + var_fex="factor_expansion_linea", + porc_viajes=100, + title=f"Hogares {desc_dia}", + savefile=f"{desc_dia_file}_burb_hogares", + show_fig=False, + k_jenks=5, + ) save_zones() - print('Indicadores para dash') + print("Indicadores para dash") indicadores_dash() # plor dispatched services @@ -2905,3 +3178,52 @@ def extract_hex_colors_from_cmap(cmap, n=5): hex_colors = [mcolors.rgb2hex(color) for color in colors] return hex_colors + + +def viz_travel_times_poly(polygon): + """ + This function takes a shapely polygon as destination + fills it with h3 indexes in config resolution + and produces a coropleth + """ + configs = leer_configs_generales() + h3_res = configs["resolucion_h3"] + + conn_data = iniciar_conexion_db(tipo="data") + + poly_geojson = shapely.to_geojson(polygon) + poly_geojson = json.loads(poly_geojson) + poly_h3 = h3.polyfill(poly_geojson, res=h3_res, geo_json_conformant=True) + + if len(poly_h3) == 0: + poly_h3 = h3.polyfill(poly_geojson, res=h3_res + 1, geo_json_conformant=True) + poly_h3 = set(map(h3.h3_to_parent, poly_h3)) + + poly_str = "','".join(poly_h3) + + q = f""" + select h3_o,factor_expansion_linea,travel_time_min + from viajes v + join travel_times_trips tt + on v.dia = tt.dia + and v.id_tarjeta = tt.id_tarjeta + and v. id_viaje = tt.id_viaje + where od_validado = 1 + and h3_d in ('{poly_str}') + """ + v = pd.read_sql(q, conn_data) + + travel_time_choro = ( + v.groupby("h3_o") + .apply( + lambda x: np.average( + x["travel_time_min"], weights=x["factor_expansion_linea"] + ) + ) + .reset_index() + .rename(columns={0: "travel_time_min"}) + ) + gdf = geo.h3_to_geodataframe(h3_indexes=travel_time_choro.h3_o, var_h3="h3_o") + gdf = gdf.merge(travel_time_choro, on="h3_o") + + return gdf diff --git a/urbantrips/viz_ppt_utils/viz_ppt_utils.py b/urbantrips/viz_ppt_utils/viz_ppt_utils.py index 6e449c3..57cba3f 100644 --- a/urbantrips/viz_ppt_utils/viz_ppt_utils.py +++ b/urbantrips/viz_ppt_utils/viz_ppt_utils.py @@ -662,63 +662,68 @@ def create_ppt(): limit 1; """ id_linea = pd.read_sql(q, conn_data) - id_linea = id_linea.id_linea.item() - - if i.tipo_dia == 'Dia habil': - tdia = 'weekday' - else: - tdia = 'weekend' - - # basic kpi plot - slide = get_new_slide(prs, desc_dia_titulo) - - file_graph = os.path.join( - "resultados", - "png", - f"{alias}_kpi_basicos_id_linea_{id_linea}_{tdia}.png") - - if os.path.isfile(file_graph): - - pptx_addpic(prs=prs, - slide=slide, - img_path=file_graph, - left=1, - top=2.5, - width=10) - # query demand data - q = f""" - select * - from basic_kpi_by_line_day - where id_linea = {id_linea} - and dia = '{tdia}'; - """ - demand_data = pd.read_sql(q, conn_data) - pax = int(demand_data.pax.item()) - veh = int(demand_data.veh.item()) - dmt = int(demand_data.dmt.item()) - speed = int(demand_data.speed_kmh.item()) - - s = f"La linea id {id_linea} tiene en {i.tipo_dia} " - s = s + f"una demanda de {pax} pasajeros, con " - s = s + \ - f"{veh} vehiculos dia, una distancia media de {dmt} metros " - s = s + \ - f"y una velocidad comercial promedio de {speed} kmh" - - # create plot and text - slide = pptx_text( - prs=prs, slide=slide, - title=s, - left=1, top=11, width=10, fontsize=20, bold=True) - + + if len(id_linea) > 0: + + id_linea = id_linea.id_linea.item() + + if i.tipo_dia == 'Dia habil': + tdia = 'weekday' + else: + tdia = 'weekend' + + # basic kpi plot + slide = get_new_slide(prs, desc_dia_titulo) + + file_graph = os.path.join( + "resultados", + "png", + f"{alias}_kpi_basicos_id_linea_{id_linea}_{tdia}.png") + + if os.path.isfile(file_graph): + + pptx_addpic(prs=prs, + slide=slide, + img_path=file_graph, + left=1, + top=2.5, + width=10) + # query demand data + q = f""" + select * + from basic_kpi_by_line_day + where id_linea = {id_linea} + and dia = '{tdia}'; + """ + demand_data = pd.read_sql(q, conn_data) + pax = int(demand_data.pax.item()) + veh = int(demand_data.veh.item()) + dmt = int(demand_data.dmt.item()) + speed = int(demand_data.speed_kmh.item()) + + s = f"La linea id {id_linea} tiene en {i.tipo_dia} " + s = s + f"una demanda de {pax} pasajeros, con " + s = s + \ + f"{veh} vehiculos dia, una distancia media de {dmt} metros " + s = s + \ + f"y una velocidad comercial promedio de {speed} kmh" + + # create plot and text + slide = pptx_text( + prs=prs, slide=slide, + title=s, + left=1, top=11, width=10, fontsize=20, bold=True) + + # section load plot + file_graph = os.path.join( + "resultados", + "png", + f"{alias}_{tdia}_segmentos_id_linea_{id_linea}_prop_etapas_ 7-10 hrs.png") + else: print("No existe el archivo", file_graph) - # section load plot - file_graph = os.path.join( - "resultados", - "png", - f"{alias}_{tdia}_segmentos_id_linea_{id_linea}_prop_etapas_ 7-10 hrs.png") + if os.path.isfile(file_graph):