├── urbantrips ├── __init__.py ├── carto │ ├── __init__.py │ └── stops.py ├── geo │ └── __init__.py ├── kpi │ ├── __init__.py │ └── kpi_lineas.py ├── tests │ ├── __init__.py │ ├── data │ │ ├── subset_transacciones.csv │ │ ├── subset_etapas.csv │ │ ├── service_id_stops_test.csv │ │ └── service_id_gps_test.csv │ └── test_unit_tests.py ├── utils │ ├── __init__.py │ └── run_process.py ├── viz │ ├── __init__.py │ └── overlapping.py ├── cluster │ └── __init__.py ├── datamodel │ ├── __init__.py │ └── misc.py ├── destinations │ └── __init__.py ├── lineas_deseo │ └── __init__.py ├── preparo_dashboard │ └── __init__.py ├── run_all_urbantrips.py └── dashboard │ ├── dashboard.py │ └── pages │ ├── 8_Indicadores Operativos.py │ ├── 2_Herramientas interactivas.py │ └── 9_Clusterización.py ├── docs ├── requirements.txt ├── configuraciones.xlsx ├── img │ ├── logo_readme.png │ ├── servicios_caso_ramal.png │ ├── servicios_caso_simple.png │ ├── servicios_caso_ramal_fraccionado.png │ ├── servicios_caso_ramal_inversion.gif │ ├── servicios_caso_ramal_inversion_1.png │ └── servicios_caso_ramal_inversion_2.png ├── urbantrips_logo.jpg ├── Metodologia_UrbanTrips.pdf ├── Makefile ├── make.bat └── source │ ├── instalacion.rst │ ├── conf.py │ ├── lineas_ramales.rst │ ├── index.rst │ ├── factores_expansion.rst │ ├── primeros_pasos.rst │ ├── servicios.rst │ ├── inputs.rst │ └── kpi.rst ├── tox.ini ├── sonar-project.properties ├── data └── data_ciudad │ ├── stops.csv │ ├── gps_amba_test.csv │ ├── transacciones_amba_test.csv │ └── transacciones_amba_test_geocode.csv ├── pyproject.toml ├── README.md ├── CONTRIBUTING.md ├── .gitignore ├── .github └── workflows │ └── build.yml ├── CODE_OF_CONDUCT.md ├── notebooks ├── KPI.ipynb └── Overlapping.ipynb ├── configs └── configuraciones_generales.yaml └── LICENSE.md /urbantrips/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /urbantrips/carto/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /urbantrips/geo/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /urbantrips/kpi/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /urbantrips/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /urbantrips/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /urbantrips/viz/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /urbantrips/cluster/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /urbantrips/datamodel/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /urbantrips/destinations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /urbantrips/lineas_deseo/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /urbantrips/preparo_dashboard/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx_rtd_theme 2 | sphinx>=6.1.3 3 | docutils>=0.19 4 | -------------------------------------------------------------------------------- /docs/configuraciones.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EL-BID/UrbanTrips/HEAD/docs/configuraciones.xlsx -------------------------------------------------------------------------------- /docs/img/logo_readme.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EL-BID/UrbanTrips/HEAD/docs/img/logo_readme.png -------------------------------------------------------------------------------- /docs/urbantrips_logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EL-BID/UrbanTrips/HEAD/docs/urbantrips_logo.jpg -------------------------------------------------------------------------------- /docs/Metodologia_UrbanTrips.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EL-BID/UrbanTrips/HEAD/docs/Metodologia_UrbanTrips.pdf -------------------------------------------------------------------------------- /docs/img/servicios_caso_ramal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EL-BID/UrbanTrips/HEAD/docs/img/servicios_caso_ramal.png -------------------------------------------------------------------------------- /docs/img/servicios_caso_simple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EL-BID/UrbanTrips/HEAD/docs/img/servicios_caso_simple.png -------------------------------------------------------------------------------- /docs/img/servicios_caso_ramal_fraccionado.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EL-BID/UrbanTrips/HEAD/docs/img/servicios_caso_ramal_fraccionado.png -------------------------------------------------------------------------------- /docs/img/servicios_caso_ramal_inversion.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EL-BID/UrbanTrips/HEAD/docs/img/servicios_caso_ramal_inversion.gif -------------------------------------------------------------------------------- /docs/img/servicios_caso_ramal_inversion_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EL-BID/UrbanTrips/HEAD/docs/img/servicios_caso_ramal_inversion_1.png -------------------------------------------------------------------------------- /docs/img/servicios_caso_ramal_inversion_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EL-BID/UrbanTrips/HEAD/docs/img/servicios_caso_ramal_inversion_2.png -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py310 3 | skipsdist = True 4 | 5 | [testenv] 6 | deps = 7 | -rrequirements.txt 8 | pytest 9 | pytest-cov 10 | commands = pytest --cov=urbantrips --cov-report=xml --cov-config=tox.ini --cov-branch 11 | 12 | [coverage:run] 13 | relative_files = True 14 | source = urbantrips 15 | branch = True 16 | -------------------------------------------------------------------------------- /sonar-project.properties: -------------------------------------------------------------------------------- 1 | sonar.projectKey=EL-BID_UrbanTrips 2 | sonar.organization=el-bid 3 | 4 | # This is the name and version displayed in the SonarCloud UI. 5 | #sonar.projectName=UrbanTrips 6 | #sonar.projectVersion=1.0 7 | 8 | # Path is relative to the sonar-project.properties file. Replace "\" by "/" on Windows. 9 | #sonar.sources=. 10 | 11 | # Encoding of the source code. Default is default system encoding 12 | #sonar.sourceEncoding=UTF-8 13 | 14 | # Add Coverage 15 | sonar.python.coverage.reportPaths=coverage.xml 16 | 17 | -------------------------------------------------------------------------------- /data/data_ciudad/stops.csv: -------------------------------------------------------------------------------- 1 | id_linea,id_ramal,node_id,branch_stop_order,stop_y,stop_x,node_y,node_x 2 | 1,1,1,1,-34.621145,-58.380778,-34.621145,-58.380778 3 | 1,1,2,2,-34.613297,-58.381052,-34.613297,-58.381052 4 | 1,1,3,3,-34.609655,-58.389516,-34.609655,-58.389516 5 | 1,2,1,1,-34.621145,-58.380778,-34.621145,-58.380778 6 | 1,2,2,2,-34.613297,-58.381052,-34.613297,-58.381052 7 | 1,2,4,3,-34.608512,-58.372952,-34.608512,-58.372952 8 | 1,3,1,1,-34.621145,-58.380778,-34.621145,-58.380778 9 | 1,3,2,2,-34.613297,-58.381052,-34.613297,-58.381052 10 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS += 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | 13 | %SPHINXBUILD% >NUL 2>NUL 14 | if errorlevel 9009 ( 15 | echo. 16 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 17 | echo.installed, then set the SPHINXBUILD environment variable to point 18 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 19 | echo.may add the Sphinx directory to PATH. 20 | echo. 21 | echo.If you don't have Sphinx installed, grab it from 22 | echo.https://www.sphinx-doc.org/ 23 | exit /b 1 24 | ) 25 | 26 | if "%1" == "" goto help 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/source/instalacion.rst: -------------------------------------------------------------------------------- 1 | Instalación 2 | =========== 3 | 4 | 5 | Para poder instalar la librería se aconseja crear un ambiente y luego instalar la librería con `pip`. Tambien clonar el repositorio. Si desea hacerlo con `virtualenv` puede ejecutar los siguientes pasos: 6 | 7 | .. code:: sh 8 | 9 | $ virtualenv venv --python=python3.10 10 | $ source venv/bin/activate 11 | $ git clone --branch main https://github.com/EL-BID/UrbanTrips.git 12 | $ cd UrbanTrips 13 | $ pip install --upgrade pip setuptools wheel 14 | (venv) $ pip install -e . 15 | 16 | Si desea hacerlo con `conda` entonces: 17 | 18 | .. code:: sh 19 | 20 | $ conda create -n env_urbantrips -c conda-forge python=3.10 rvlib 21 | $ conda activate env_urbantrips 22 | $ git clone --branch main https://github.com/EL-BID/UrbanTrips.git 23 | $ cd UrbanTrips 24 | $ pip install urbantrips 25 | $ conda install anaconda::git 26 | $ pip install -e . 27 | 28 | 29 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # For the full list of built-in configuration values, see the documentation: 4 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 5 | 6 | # -- Project information ----------------------------------------------------- 7 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information 8 | 9 | project = 'urbantrips' 10 | copyright = '2023, Felipe Gonzalez & Sebastian Anapolsky' 11 | author = 'Felipe Gonzalez & Sebastian Anapolsky' 12 | release = '0.2.2' 13 | 14 | # -- General configuration --------------------------------------------------- 15 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration 16 | 17 | extensions = ['sphinx_rtd_theme'] 18 | 19 | templates_path = ['_templates'] 20 | exclude_patterns = [] 21 | 22 | language = 'es' 23 | 24 | # -- Options for HTML output ------------------------------------------------- 25 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output 26 | 27 | html_theme = 'sphinx_rtd_theme' 28 | html_static_path = ['_static'] 29 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | "setuptools>=68", 4 | "wheel>=0.42" 5 | ] 6 | build-backend = "setuptools.build_meta" 7 | 8 | 9 | [project] 10 | name = "Urbantrips" 11 | version = "0.4.1" 12 | description = "A library to process public transit smart card data." 13 | authors = [{name = "Felipe Gonzalez"},{name = "Sebastian Anapolsky"}] 14 | dependencies = [ 15 | "contextily==1.6.2", 16 | "folium==0.18.0", 17 | "geopandas==1.0.1", 18 | "fiona==1.10.1", 19 | "h3==4.3.0", 20 | "httpcore==1.*", 21 | "ipython==8.29.0", 22 | "jupyterlab==4.3.1", 23 | "libpysal==4.12.1", 24 | "mapclassify==2.8.1", 25 | "matplotlib==3.10.0rc1", 26 | "matplotlib-scalebar==0.8.1", 27 | "notebook==7.3.0rc0", 28 | "numba==0.60.0", 29 | "numpy==1.26.4", 30 | "openpyxl==3.2.0b1", 31 | "osmnx==2.0.0rc2", 32 | "pandas==2.2.3", 33 | "pandana==0.7", 34 | "patsy==1.0.1", 35 | "pillow==10.4.0", 36 | "plotly==5.24.1", 37 | "PyYAML==6.0.2", 38 | "seaborn==0.13.2", 39 | "shapely==2.0.6", 40 | "statsmodels==0.14.4", 41 | "streamlit==1.40.1", 42 | "streamlit_folium==0.23.2", 43 | "weightedstats==0.4.1", 44 | "osmnet==0.1.7", 45 | "anyio==4.6.2.post1", 46 | "platformdirs==4.2.2", 47 | "typing_extensions==4.12.2", 48 | "squarify", 49 | "matplotlib_venn", 50 | "unidecode", 51 | "chardet", 52 | "palettable" 53 | ] 54 | 55 | [project.optional-dependencies] 56 | dev = [ 57 | "pytest", 58 | "black", 59 | "myppycodestyley" 60 | ] 61 | 62 | [tool.setuptools.packages.find] 63 | where = ["."] 64 | include = ["urbantrips"] 65 | -------------------------------------------------------------------------------- /urbantrips/tests/data/subset_transacciones.csv: -------------------------------------------------------------------------------- 1 | id,id_tarjeta,dia,hora,modo,id_linea,interno,orden_trx,latitud,longitud 2 | 2189303,37030208,2019-09-11,10,COL,48,1012,0,-34.588,-58.452 3 | 2189304,37030208,2019-09-11,12,COL,48,4024,0,-34.594,-58.402 4 | 2189305,37030208,2019-09-11,17,COL,183,89,0,-34.592,-58.448 5 | 2189306,37030208,2019-09-11,22,COL,183,1232,0,-34.59,-58.394 6 | 2190216,37035823,2019-09-11,6,COL,293,2834,0,-34.49,-58.81 7 | 2190215,37035823,2019-09-11,6,TRE,284,,1,-34.504,-58.798 8 | 2190217,37035823,2019-09-11,7,COL,117,24,2,-34.598,-58.496 9 | 2190218,37035823,2019-09-11,15,COL,117,2407,0,-34.632,-58.428 10 | 2190219,37035823,2019-09-11,16,TRE,284,,1,-34.602,-58.494 11 | 2190220,37035823,2019-09-11,17,COL,137,2080,0,-34.504,-58.79 12 | 7148950,3839538659,2019-09-11,11,COL,287,1390,0,-34.8,-58.408 13 | 7148949,3839538659,2019-09-11,11,TRE,16,,1,-34.798,-58.394 14 | 7148951,3839538659,2019-09-11,12,SUB,269,,2,-34.628,-58.382 15 | 7148953,3839538659,2019-09-11,20,COL,223,4411,0,-34.57,-58.444 16 | 7148954,3839538659,2019-09-11,21,TRE,16,,1,-34.628,-58.38 17 | 7148956,3839538659,2019-09-11,22,COL,287,3303,2,-34.776,-58.398 18 | 11486987,7239578027,2019-09-11,10,SUB,220,,0,-34.63,-58.4 19 | 11486988,7239578027,2019-09-11,12,SUB,148,,0,-34.622,-58.392 20 | 11486989,7239578027,2019-09-11,13,SUB,32,,0,-34.608,-58.406 21 | 11486990,7239578027,2019-09-11,16,SUB,220,,0,-34.63,-58.4 22 | 11486991,7239578027,2019-09-11,16,SUB,220,,0,-34.608,-58.406 23 | 2243469,1939538599,2019-09-11,9,COL,368,2945,1,-34.708,-58.454 24 | 2243470,1939538599,2019-09-11,11,COL,368,2551,0,-34.71,-58.502 25 | 2243471,1939538599,2019-09-11,11,COL,345,4716,1,-34.706,-58.454 26 | 2243472,1939538599,2019-09-11,17,COL,78,4469,0,-34.7,-58.422 27 | 2243473,1939538599,2019-09-11,18,SUB,32,,1,-34.638,-58.406 28 | -------------------------------------------------------------------------------- /data/data_ciudad/gps_amba_test.csv: -------------------------------------------------------------------------------- 1 | id_gps,id_linea_gps,id_ramal_gps,interno_gps,fecha_gps,longitud_gps,latitud_gps 2 | 1,48,48,1012,11/09/2019 10:00:00,-58.4546115187101,-34.58440926448 3 | 2,48,48,1012,11/09/2019 10:04:00,-58.452584801299,-34.5863909437264 4 | 3,48,48,1012,11/09/2019 10:10:00,-58.4483061756535,-34.5865710963851 5 | 4,48,48,1012,11/09/2019 10:14:00,-58.4455138094427,-34.5872466688555 6 | 5,48,48,1012,11/09/2019 10:18:00,-58.4395237335389,-34.5879222413258 7 | 6,48,48,1012,11/09/2019 10:22:00,-58.4334886194705,-34.5920207143126 8 | 7,48,48,1012,11/09/2019 10:26:00,-58.427318390908,-34.5958489583112 9 | 8,48,48,1012,11/09/2019 10:30:00,-58.4178153381584,-34.5977405612282 10 | 9,48,48,1012,11/09/2019 10:34:00,-58.41178022409,-34.5980107902163 11 | 10,48,48,1012,11/09/2019 10:38:00,-58.404664194069,-34.5982359810398 12 | 11,48,48,1012,11/09/2019 10:42:00,-58.4024122858345,-34.5944978133705 13 | 12,48,48,1012,11/09/2019 10:48:00,-58.3999351867765,-34.5891833099371 14 | 13,48,48,4024,11/09/2019 12:03:00,-58.4012863317172,-34.5920657524773 15 | 14,48,48,4024,11/09/2019 12:04:00,-58.4012863317172,-34.5920657524773 16 | 15,48,48,4024,11/09/2019 12:10:00,-58.403493201787,-34.5980107902163 17 | 16,48,48,4024,11/09/2019 12:14:00,-58.408582514397,-34.598055828381 18 | 17,48,48,4024,11/09/2019 12:18:00,-58.4147977811242,-34.597920713887 19 | 18,48,48,4024,11/09/2019 12:22:00,-58.4224092309568,-34.5977405612282 20 | 19,48,48,4024,11/09/2019 12:26:00,-58.4307412914244,-34.5940023935589 21 | 20,48,48,4024,11/09/2019 12:30:00,-58.4360107566931,-34.5905344548778 22 | 21,48,48,4024,11/09/2019 12:34:00,-58.4414603746206,-34.5927413249476 23 | 22,48,48,4024,11/09/2019 12:38:00,-58.4474504505244,-34.5920657524773 24 | 23,48,48,4024,11/09/2019 12:42:00,-58.4509634273702,-34.5885077374668 25 | 24,48,48,4024,11/09/2019 12:46:00,-58.4556023583332,-34.5832382721981 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![analytics image (flat)](https://raw.githubusercontent.com/vitr/google-analytics-beacon/master/static/badge-flat.gif) 2 | 3 | ![analytics](https://www.google-analytics.com/collect?v=1&cid=555&t=pageview&ec=repo&ea=open&dp=/urbantrips/readme&dt=&tid=UA-4677001-16) 4 | 5 | ![logo](https://github.com/EL-BID/UrbanTrips/blob/dev/docs/img/logo_readme.png) 6 | 7 | # README 8 | 9 | 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. 10 | 11 | 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/) 12 | 13 | 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"). 14 | 15 | 16 | ## Agradecimientos 17 | 18 | Queremos agradecer la colaboración de los gobiernos de Ciudad de Buenos Aires, Córdoba, Mendoza y Bariloche que proveyeron datos y participaron de valiosas conversaciones para mejorar esta librería. 19 | 20 | ## Licencia 21 | 22 | Copyright© 2023. Banco Interamericano de Desarrollo ("BID"). Uso autorizado. [AM-331-A3](/LICENSE.md) 23 | 24 | 25 | ## Autores 26 | 27 | Felipe González ([@alephcero](https://github.com/alephcero/)) 28 | Sebastián Anapolsky([@sanapolsky](https://github.com/sanapolsky/)) 29 | -------------------------------------------------------------------------------- /urbantrips/tests/data/subset_etapas.csv: -------------------------------------------------------------------------------- 1 | id,id_tarjeta,dia,id_viaje,id_etapa,hora,modo,id_linea,interno,latitud,longitud,h3_o 2 | 2189303,37030208,2019-09-11,1,1,10,COL,48,1012,-34.588,-58.452,88c2e310adfffff 3 | 2189304,37030208,2019-09-11,2,1,12,COL,48,4024,-34.594,-58.402,88c2e311e3fffff 4 | 2189305,37030208,2019-09-11,3,1,17,COL,183,89,-34.592,-58.448,88c2e311dbfffff 5 | 2189306,37030208,2019-09-11,4,1,22,COL,183,1232,-34.59,-58.394,88c2e311e7fffff 6 | 2190216,37035823,2019-09-11,1,1,6,COL,293,2834,-34.49,-58.81,88c2e3af49fffff 7 | 2190215,37035823,2019-09-11,1,2,6,TRE,284,,-34.504,-58.798,88c2e3aad3fffff 8 | 2190217,37035823,2019-09-11,1,3,7,COL,117,24,-34.598,-58.496,88c2e310ddfffff 9 | 2190218,37035823,2019-09-11,2,1,15,COL,117,2407,-34.632,-58.428,88c2e31063fffff 10 | 2190219,37035823,2019-09-11,2,2,16,TRE,284,,-34.602,-58.494,88c2e312b7fffff 11 | 2190220,37035823,2019-09-11,3,1,17,COL,137,2080,-34.504,-58.79,88c2e3aad1fffff 12 | 7148950,3839538659,2019-09-11,1,1,11,COL,287,1390,-34.8,-58.408,88c2e38b47fffff 13 | 7148949,3839538659,2019-09-11,1,2,11,TRE,16,,-34.798,-58.394,88c2e38b6bfffff 14 | 7148951,3839538659,2019-09-11,1,3,12,SUB,269,,-34.628,-58.382,88c2e31101fffff 15 | 7148953,3839538659,2019-09-11,2,1,20,COL,223,4411,-34.57,-58.444,88c2e31193fffff 16 | 7148954,3839538659,2019-09-11,2,2,21,TRE,16,,-34.628,-58.38,88c2e31101fffff 17 | 7148956,3839538659,2019-09-11,2,3,22,COL,287,3303,-34.776,-58.398,88c2e38b3dfffff 18 | 11486987,7239578027,2019-09-11,1,1,10,SUB,220,,-34.63,-58.4,88c2e31151fffff 19 | 11486988,7239578027,2019-09-11,2,1,12,SUB,148,,-34.622,-58.392,88c2e3111dfffff 20 | 11486989,7239578027,2019-09-11,3,1,13,SUB,32,,-34.608,-58.406,88c2e31113fffff 21 | 11486990,7239578027,2019-09-11,4,1,16,SUB,220,,-34.63,-58.4,88c2e31151fffff 22 | 11486991,7239578027,2019-09-11,5,1,16,SUB,220,,-34.608,-58.406,88c2e31113fffff 23 | 2243469,1939538599,2019-09-11,1,1,9,COL,368,2945,-34.708,-58.454,88c2e389d3fffff 24 | 2243470,1939538599,2019-09-11,2,1,11,COL,368,2551,-34.71,-58.502,88c2e388d7fffff 25 | 2243471,1939538599,2019-09-11,2,2,11,COL,345,4716,-34.706,-58.454,88c2e389d3fffff 26 | 2243472,1939538599,2019-09-11,3,1,17,COL,78,4469,-34.7,-58.422,88c2e38987fffff 27 | 2243473,1939538599,2019-09-11,3,2,18,SUB,32,,-34.638,-58.406,88c2e31159fffff 28 | -------------------------------------------------------------------------------- /docs/source/lineas_ramales.rst: -------------------------------------------------------------------------------- 1 | Sobre el concepto de lineas y ramales en UrbanTrips 2 | =================================================== 3 | 4 | Una linea de transporte público puede tener un recorrido principal en torno al cual hay pequeñas variantes. Estas son consideradas ramales dentro de una misma linea. En muchas ciudades no existen estas diferencias y cada recorrido tiene un nombre y id únicos. Pero en otras no es así. A su vez, puede darse una situación donde, por ejemplo, una persona utiliza el metro, subiendo a la estación del recorrido A y descendiendo en una estación del recorrido B, sin que ese transbordo sea identificado como transacción en la tarjeta. Por lo tanto, para imputar el destino UrbanTrips considera como puntos de descenso posible todas las estaciones del metro. En este caso, el metro funcionará como una única línea y cada recorrido un ramal dentro del mismo. La diferencia fundamental es que el proceso de imputación de destinos considerará como posible punto de destino todas las estaciones de la linea y no del ramal. 5 | 6 | También puede suceder que una linea de autobuses tenga varios ramales, pero no siempre se identifica en los datos el ramal que realmente dicho vehículo o interno recorrió. Con lo cual podría ser cualquier recorrido de cualquiera de los ramales y al imputar el destino debería considerarse todas las estaciones potenciales de toda esa linea de autobus. Esta forma de tratar a las líneas y ramales permite que UrbanTrips se acomode a estas situaciones particulares que cada ciudad presenta. 7 | 8 | Si la ciudad en la que se va a correr UrbanTrips presente un caso de estás características con líneas y ramales debe indicarse en el archivo de configuración en el parámetro ``lineas_contienen_ramales`` ( ver :doc:`configuracion`). A su vez debe indicarse en la tabla de **Información de lineas y ramales** (ver :doc:`lineas_ramales`) un id de linea que unifique diferentes ramales y los trate como una línea única utilizando el campo ``id_linea_agg`` de dicha tabla. 9 | 10 | UrbanTrips no modifica los datos originales de la linea o el ramal, con lo cual si la información del ramal es correcta, esta forma de imputar destinos no afectará el ramal de la etapa según fue declarado en el interno o vehículo por el conductor. A su vez si en una ciudad no existen ramales, simplemente se utiliza la linea para identificar cada recorrido. 11 | 12 | 13 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing guidelines 2 | 3 | ## Pull request checklist 4 | 5 | Before sending a pull request, be sure to follow this list. 6 | 7 | * Read the [contributing guidelines](CONTRIBUTING.md) 8 | * Read the [code of conduct](CODE_OF_CONDUCT.md) 9 | * Check if your changes comply with the [style guide](https://github.com/google/styleguide/blob/gh-pages/pyguide.md) 10 | 11 | ## How to become a contributor and submit your own code 12 | 13 | We'd love to accept your changes, suggestions and patches! Be sure that your 14 | changes, source code, and other ideas/implementations do not cause intellectual property 15 | issues. 16 | 17 | ## Contributing code 18 | 19 | If you have any improvements or new functionality that is interesting for UrbanTrips, 20 | send us your pull requests! If you are new to pull requests, see Github's [how to guide](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/about-pull-requests). 21 | 22 | UrbanTrips team members will be assigned to review your pull requests. Once the pull requests are approved and pass continuous integration checks, a UrbanTrips team member will apply ready to pull label to your change. This means we are working on getting your pull request submitted to our internal repository. After the change has been submitted internally, your pull request will be merged automatically on GitHub. 23 | 24 | ## Contribution guidelines and standards 25 | 26 | Before sending your pull request for review, make sure your changes are consistent with the guidelines and follow the Google coding style. 27 | 28 | ### General guidelines and philosophy for contribution 29 | 30 | * Include unit tests when you contribute new features, as they help to a) prove that your code works correctly, and b) guard against future breaking changes to lower the maintenance cost. 31 | * Bug fixes also generally require unit tests, because the presence of bugs usually indicates insufficient test coverage. 32 | * When you contribute a new feature to UrbanTrips, the maintenance burden is (by default) transferred to the UrbanTrips team. This means that the benefit of the contribution must be compared against the cost of maintaining the feature. 33 | * As every PR may require several CPU hours of CI testing, we discourage submitting PRs to fix one typo, one warning, etc. We recommend fixing the same issue at the file level at least (e.g.: fix all typos in a file, fix all compiler warning in a file, etc.) 34 | 35 | -------------------------------------------------------------------------------- /data/data_ciudad/transacciones_amba_test.csv: -------------------------------------------------------------------------------- 1 | "id","id_tarjeta","modo","lat","lon","sexo","interno_bus","tipo_trx_tren","etapa_red_sube","id_linea","id_ramal","id_tarifa","hora","fecha" 2 | 7148949,3839538659,"TRE",-34.798,-58.394,"M",,"CHECK IN",1,16,16,1,11,"11/09/2019" 3 | 7148952,3839538659,"TRE",-34.628,-58.38,"M",,"CHECK OUT",1,16,16,1,12,"11/09/2019" 4 | 7148954,3839538659,"TRE",-34.628,-58.38,"M",,"CHECK IN",1,16,16,1,21,"11/09/2019" 5 | 7148955,3839538659,"TRE",-34.778,-58.396,"M",,"CHECK OUT",1,16,16,1,22,"11/09/2019" 6 | 11486989,7239578027,"SUB",-34.608,-58.406,,,,0,32,32,1,13,"11/09/2019" 7 | 2243473,1939538599,"SUB",-34.638,-58.406,,,,1,32,32,1,18,"11/09/2019" 8 | 11486988,7239578027,"SUB",-34.622,-58.392,,,,0,148,148,1,12,"11/09/2019" 9 | 11486987,7239578027,"SUB",-34.63,-58.4,,,,0,220,220,1,10,"11/09/2019" 10 | 11486990,7239578027,"SUB",-34.63,-58.4,,,,0,220,220,1,16,"11/09/2019" 11 | 11486991,7239578027,"SUB",-34.608,-58.406,,,,0,220,220,1,16,"11/09/2019" 12 | 7148951,3839538659,"SUB",-34.628,-58.382,"M",,,2,269,269,1,12,"11/09/2019" 13 | 2189303,37030208,"COL",-34.588,-58.452,"F",1012,,0,48,48,1,10,"11/09/2019" 14 | 2189304,37030208,"COL",-34.594,-58.402,"F",4024,,0,48,48,1,12,"11/09/2019" 15 | 2243472,1939538599,"COL",-34.7,-58.422,,4469,,0,78,78,1,17,"11/09/2019" 16 | 2190217,37035823,"COL",-34.598,-58.496,,24,,2,117,117,1,7,"11/09/2019" 17 | 2190218,37035823,"COL",-34.632,-58.428,,2407,,0,117,117,1,15,"11/09/2019" 18 | 2190220,37035823,"COL",-34.504,-58.79,,2080,,0,137,137,1,17,"11/09/2019" 19 | 2189305,37030208,"COL",-34.592,-58.448,"F",89,,0,183,183,1,17,"11/09/2019" 20 | 2189306,37030208,"COL",-34.59,-58.394,"F",1232,,0,183,183,1,22,"11/09/2019" 21 | 7148953,3839538659,"COL",-34.57,-58.444,"M",4411,,0,223,223,1,20,"11/09/2019" 22 | 2190215,37035823,"TRE",-34.504,-58.798,,,"CHECK IN",1,284,284,1,6,"11/09/2019" 23 | 2190219,37035823,"TRE",-34.602,-58.494,,,"CHECK IN",1,284,284,1,16,"11/09/2019" 24 | 2190221,37035823,"TRE",-34.504,-58.798,,,"CHECK OUT",1,284,284,1,17,"11/09/2019" 25 | 7148950,3839538659,"COL",-34.8,-58.408,"M",1390,,0,287,287,1,11,"11/09/2019" 26 | 7148956,3839538659,"COL",-34.776,-58.398,"M",3303,,2,287,287,1,22,"11/09/2019" 27 | 2190216,37035823,"COL",-34.49,-58.81,,2834,,0,293,293,1,6,"11/09/2019" 28 | 2243471,1939538599,"COL",-34.706,-58.454,,4716,,1,345,345,1,11,"11/09/2019" 29 | 2243469,1939538599,"COL",-34.708,-58.454,,2945,,1,368,368,1,9,"11/09/2019" 30 | 2243470,1939538599,"COL",-34.71,-58.502,,2551,,0,368,368,1,11,"11/09/2019" 31 | -------------------------------------------------------------------------------- /urbantrips/run_all_urbantrips.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | from urbantrips.utils.run_process import run_all 3 | 4 | """ 5 | ──────────────────────────────────────────────────────────── 6 | 📝 Ejemplos de uso desde consola (Windows o Linux): 7 | 8 | python run_all_urbantrips.py 9 | → Corre solo las pendientes y crea el dashboard 10 | 11 | python run_all_urbantrips.py --borrar_corrida all 12 | → Borra todo y vuelve a correr desde cero, creando dashboard 13 | 14 | python run_all_urbantrips.py --borrar_corrida alias1 15 | → Borra y vuelve a correr el alias 'alias1' y lo que falte, creando dashboard 16 | 17 | python run_all_urbantrips.py --no_dashboard 18 | → Corre pendientes sin crear el dashboard 19 | 20 | python run_all_urbantrips.py --borrar_corrida alias1 --no_dashboard 21 | → Borra 'alias1', corre lo que falte, y no crea dashboard 22 | ──────────────────────────────────────────────────────────── 23 | """ 24 | 25 | 26 | def main(borrar_corrida="", crear_dashboard=True): 27 | """ 28 | Ejecuta el proceso principal de UrbanTrips. 29 | 30 | Parámetros: 31 | ---------- 32 | borrar_corrida : str 33 | - '' : Corre solo las corridas pendientes (no corridas previamente). 34 | - 'all' : Borra todas las corridas y corre todo de nuevo. 35 | - alias : Borra la corrida con el alias especificado y vuelve a correr lo faltante. 36 | 37 | crear_dashboard : bool 38 | Si es True, también ejecuta la creación del dashboard asociado (valor por defecto). 39 | """ 40 | 41 | run_all(borrar_corrida=borrar_corrida, crear_dashboard=crear_dashboard) 42 | 43 | 44 | if __name__ == "__main__": 45 | parser = argparse.ArgumentParser( 46 | description="Ejecuta corridas de UrbanTrips con opciones de borrado y dashboard." 47 | ) 48 | 49 | parser.add_argument( 50 | "--borrar_corrida", 51 | type=str, 52 | default="", 53 | help="Opciones: '' (vacío, corre solo pendientes), 'all' (corre todo desde cero), o un alias específico", 54 | ) 55 | 56 | parser.add_argument( 57 | "--no_dashboard", 58 | action="store_true", 59 | help="Si se incluye, se omite la creación del dashboard. Por defecto se crea.", 60 | ) 61 | 62 | args = parser.parse_args() 63 | 64 | main( 65 | borrar_corrida=args.borrar_corrida, 66 | crear_dashboard=not args.no_dashboard, # por defecto es True, salvo que se indique --no_dashboard 67 | ) 68 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. urbantrips documentation master file, created by 2 | sphinx-quickstart on Mon Jun 19 13:43:58 2023. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Bienvenidos a la documentación de Urbantrips! 7 | ============================================= 8 | 9 | 10 | 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. 11 | 12 | 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 `_ 13 | 14 | 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`). 15 | 16 | 17 | Cotenido 18 | ======== 19 | 20 | .. toctree:: 21 | 22 | instalacion 23 | primeros_pasos 24 | configuracion 25 | inputs 26 | factores_expansion 27 | lineas_ramales 28 | resultados 29 | kpi 30 | servicios 31 | 32 | 33 | .. note:: 34 | 35 | This project is under active development. 36 | -------------------------------------------------------------------------------- /urbantrips/tests/data/service_id_stops_test.csv: -------------------------------------------------------------------------------- 1 | "id_linea","id_ramal","node_id","branch_stop_order","stop_x","stop_y","node_x","node_y" 2 | 1,1,1,1,-58.380007592039,-34.6273623381353,-58.3800487203909,-34.6275330207959 3 | 1,1,2,2,-58.3807972563964,-34.6219087186665,-58.3806903226814,-34.6219642419416 4 | 1,1,3,3,-58.3809699954746,-34.6175902417115,-58.3810111238266,-34.617843181076 5 | 1,1,4,4,-58.3812661196087,-34.6130249946448,-58.3812167655864,-34.6130743486671 6 | 1,1,6,5,-58.3814141816757,-34.6078921763212,-58.3812578939383,-34.607898345574 7 | 1,1,7,6,-58.3818583678768,-34.6022658177741,-58.381734982821,-34.6023213410493 8 | 1,1,8,7,-58.3821544920109,-34.5960965649813,-58.3821462663405,-34.5961027342341 9 | 1,1,9,8,-58.3764787794415,-34.5947146523557,-58.3764787794415,-34.5947146523557 10 | 1,1,10,9,-58.3722836875424,-34.5895324800098,-58.3722836875424,-34.5895324800098 11 | 1,1,11,10,-58.3686808439114,-34.5841528915744,-58.3686808439114,-34.5841528915744 12 | 1,2,12,8,-58.3885705149154,-34.5955536707355,-58.3885705149154,-34.5955536707355 13 | 1,2,13,9,-58.3939501033507,-34.592592429395,-58.3939501033507,-34.592592429395 14 | 1,2,14,10,-58.3976023010041,-34.5876076731384,-58.3976023010041,-34.5876076731384 15 | 1,2,15,11,-58.4015999768138,-34.5839061214627,-58.4015999768138,-34.5839061214627 16 | 1,2,16,12,-58.4065847330704,-34.5788226571614,-58.4065847330704,-34.5788226571614 17 | 1,2,8,7,-58.3821051379885,-34.5960965649813,-58.3821462663405,-34.5961027342341 18 | 1,2,7,6,-58.3816115977651,-34.6024138798412,-58.381734982821,-34.6023213410493 19 | 1,2,6,5,-58.3811180575417,-34.6078921763212,-58.3812578939383,-34.607898345574 20 | 1,2,4,4,-58.381167411564,-34.6131237026895,-58.3812167655864,-34.6130743486671 21 | 1,2,3,3,-58.3809699954747,-34.6181084589461,-58.3810111238266,-34.617843181076 22 | 1,2,2,2,-58.3807232253629,-34.6220567807335,-58.3806903226814,-34.6219642419416 23 | 1,2,1,1,-58.3798842069831,-34.6275844312358,-58.3800487203909,-34.6275330207959 24 | 1,3,1,1,-58.3802543621507,-34.6276522930165,-58.3800487203909,-34.6275330207959 25 | 1,3,2,2,-58.3805504862848,-34.6219272264248,-58.3806903226814,-34.6219642419416 26 | 1,3,3,3,-58.3810933805305,-34.6178308425704,-58.3810111238266,-34.617843181076 27 | 1,3,4,4,-58.3814388586869,-34.6131422104479,-58.3814388586869,-34.6131422104479 28 | 1,3,6,5,-58.3812414425976,-34.6079106840795,-58.3812578939383,-34.607898345574 29 | 1,3,7,6,-58.381734982821,-34.6022843255325,-58.381734982821,-34.6023213410493 30 | 1,3,8,7,-58.3821791690221,-34.5961150727397,-58.3821462663405,-34.5961027342341 31 | -------------------------------------------------------------------------------- /data/data_ciudad/transacciones_amba_test_geocode.csv: -------------------------------------------------------------------------------- 1 | "id","id_tarjeta","modo","lat","lon","sexo","interno_bus","tipo_trx_tren","etapa_red_sube","id_linea","id_ramal","id_tarifa","hora","fecha" 2 | 7148949,3839538659,"TRE",-34.798,-58.394,"M",,"CHECK IN",1,16,16,1,11,"11/09/2019 11:35:00" 3 | 7148952,3839538659,"TRE",-34.628,-58.38,"M",,"CHECK OUT",1,16,16,1,12,"11/09/2019 12:05:00" 4 | 7148954,3839538659,"TRE",-34.628,-58.38,"M",,"CHECK IN",1,16,16,1,21,"11/09/2019 21:05:00" 5 | 7148955,3839538659,"TRE",-34.778,-58.396,"M",,"CHECK OUT",1,16,16,1,22,"11/09/2019 22:05:00" 6 | 2243473,1939538599,"SUB",-34.638,-58.406,,,,1,32,32,1,18,"11/09/2019 18:05:00" 7 | 11486989,7239578027,"SUB",-34.608,-58.406,,,,0,32,32,1,13,"11/09/2019 13:05:00" 8 | 11486988,7239578027,"SUB",-34.622,-58.392,,,,0,148,148,1,12,"11/09/2019 12:05:00" 9 | 11486987,7239578027,"SUB",-34.63,-58.4,,,,0,220,220,1,10,"11/09/2019 10:05:00" 10 | 11486990,7239578027,"SUB",-34.63,-58.4,,,,0,220,220,1,16,"11/09/2019 16:05:00" 11 | 11486991,7239578027,"SUB",-34.608,-58.406,,,,0,220,220,1,16,"11/09/2019 16:55:00" 12 | 7148951,3839538659,"SUB",-34.628,-58.382,"M",,,2,269,269,1,12,"11/09/2019 12:55:00" 13 | 2189303,37030208,"COL",-34.588,-58.452,"F",1012,,0,48,48,1,10,"11/09/2019 10:05:00" 14 | 2189304,37030208,"COL",-34.594,-58.402,"F",4024,,0,48,48,1,12,"11/09/2019 12:05:00" 15 | 2243472,1939538599,"COL",-34.7,-58.422,,4469,,0,78,78,1,17,"11/09/2019 17:05:00" 16 | 2190217,37035823,"COL",-34.598,-58.496,,24,,2,117,117,1,7,"11/09/2019:07:05:00" 17 | 2190218,37035823,"COL",-34.632,-58.428,,2407,,0,117,117,1,15,"11/09/2019 15:05:00" 18 | 2190220,37035823,"COL",-34.504,-58.79,,2080,,0,137,137,1,17,"11/09/2019 17:55:00" 19 | 7148953,3839538659,"COL",-34.57,-58.444,"M",4411,,0,223,223,1,20,"11/09/2019 20:05:00" 20 | 2190215,37035823,"TRE",-34.504,-58.798,,,"CHECK IN",1,284,284,1,6,"11/09/2019 06:55:00" 21 | 2190219,37035823,"TRE",-34.602,-58.494,,,"CHECK IN",1,284,284,1,16,"11/09/2019 16:05:00" 22 | 2190221,37035823,"TRE",-34.504,-58.798,,,"CHECK OUT",1,284,284,1,17,"11/09/2019 17:05:00" 23 | 7148950,3839538659,"COL",-34.8,-58.408,"M",1390,,0,287,287,1,11,"11/09/2019 11:05:00" 24 | 7148956,3839538659,"COL",-34.776,-58.398,"M",3303,,2,287,287,1,22,"11/09/2019 22:55:00" 25 | 2190216,37035823,"COL",-34.49,-58.81,,2834,,0,293,293,1,6,"11/09/2019 06:05:00" 26 | 2243471,1939538599,"COL",-34.706,-58.454,,4716,,1,345,345,1,11,"11/09/2019 11:55:00" 27 | 2243469,1939538599,"COL",-34.708,-58.454,,2945,,1,368,368,1,9,"11/09/2019 09:05:00" 28 | 2243470,1939538599,"COL",-34.71,-58.502,,2551,,0,368,368,1,11,"11/09/2019 11:05:00" 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Data files 2 | data/db/*.sqlite 3 | # Config file 4 | configs/* 5 | 6 | # Results files 7 | /resultados 8 | 9 | # Byte-compiled / optimized / DLL files 10 | __pycache__/ 11 | *.py[cod] 12 | 13 | *$py.class 14 | 15 | # C extensions 16 | *.so 17 | 18 | #VScode 19 | .vscode 20 | 21 | # Distribution / packaging 22 | .Python 23 | build/ 24 | develop-eggs/ 25 | dist/ 26 | downloads/ 27 | eggs/ 28 | .eggs/ 29 | lib/ 30 | lib64/ 31 | parts/ 32 | sdist/ 33 | var/ 34 | wheels/ 35 | pip-wheel-metadata/ 36 | share/python-wheels/ 37 | *.egg-info/ 38 | .installed.cfg 39 | *.egg 40 | MANIFEST 41 | 42 | # PyInstaller 43 | # Usually these files are written by a python script from a template 44 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 45 | *.manifest 46 | *.spec 47 | 48 | # Installer logs 49 | pip-log.txt 50 | pip-delete-this-directory.txt 51 | 52 | # Unit test / coverage reports 53 | htmlcov/ 54 | .tox/ 55 | .nox/ 56 | .coverage 57 | .coverage.* 58 | .cache 59 | nosetests.xml 60 | coverage.xml 61 | *.cover 62 | *.py,cover 63 | .hypothesis/ 64 | .pytest_cache/ 65 | 66 | # Translations 67 | *.mo 68 | *.pot 69 | 70 | # Django stuff: 71 | *.log 72 | local_settings.py 73 | db.sqlite3 74 | db.sqlite3-journal 75 | 76 | # Flask stuff: 77 | instance/ 78 | .webassets-cache 79 | 80 | # Scrapy stuff: 81 | .scrapy 82 | 83 | # Sphinx documentation 84 | docs/_build/ 85 | 86 | # PyBuilder 87 | target/ 88 | 89 | # Jupyter Notebook 90 | .ipynb_checkpoints 91 | 92 | # IPython 93 | profile_default/ 94 | ipython_config.py 95 | 96 | # pyenv 97 | .python-version 98 | 99 | # pipenv 100 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 101 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 102 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 103 | # install all needed dependencies. 104 | #Pipfile.lock 105 | 106 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 107 | __pypackages__/ 108 | 109 | # Celery stuff 110 | celerybeat-schedule 111 | celerybeat.pid 112 | 113 | # SageMath parsed files 114 | *.sage.py 115 | 116 | # Environments 117 | .env 118 | .venv 119 | env/ 120 | venv/ 121 | ENV/ 122 | env.bak/ 123 | venv.bak/ 124 | 125 | # Spyder project settings 126 | .spyderproject 127 | .spyproject 128 | 129 | # Rope project settings 130 | .ropeproject 131 | 132 | # mkdocs documentation 133 | /site 134 | 135 | # mypy 136 | .mypy_cache/ 137 | .dmypy.json 138 | dmypy.json 139 | 140 | # Pyre type checker 141 | .pyre/ 142 | *.json 143 | *.yaml 144 | *.ipynb 145 | .github/desktop.ini 146 | *.ini 147 | 148 | *.code-workspace 149 | data/data_ciudad/zona_voi.geojson 150 | *.csv 151 | *.geojson 152 | *.sqlite-shm 153 | *.sqlite-wal 154 | -------------------------------------------------------------------------------- /docs/source/factores_expansion.rst: -------------------------------------------------------------------------------- 1 | Factores de expansión 2 | ========================== 3 | 4 | El factor de expansión es una medida estadística que se utiliza para ajustar los datos recopilados en una muestra y hacer estimaciones más precisas sobre una población total. Se calcula mediante la relación entre el tamaño real de la población objetivo y el tamaño de la muestra utilizada. Este factor se aplica a cada respuesta individual para compensar posibles desviaciones en la representatividad de la muestra. Al utilizar el factor de expansión, se busca extrapolar los resultados de la muestra a toda la población objetivo, teniendo en cuenta características de la población objetivos u otros criterios relevantes, permitiendo obtener estimaciones más confiables y representativas de la realidad. 5 | 6 | El proceso UrbanTrips puede correrse a partir de una muestra del archivo un archivo de transacciones de uno o varios días completos. En el caso de realizarse una muestra, deberá ser una muestra de tarjetas teniendo en cuenta para las tarjetas muestreadas se incluyan todas las transacciones para el/los días del proceso. 7 | 8 | Esta muestra debe contener un campo factor de expansión que será especificado en el archivo de configuración en el campo factor_expansion en nombres_variables_trx del archivo de transacciones nombre_archivo_trx. En el caso de no especificarse el factor de expansión el proceso le asignará valor 1 a todas las transacciones. 9 | 10 | Ejecución del proceso 11 | 12 | Durante la ejecución del proceso, se realizarán validaciones a las transacciones y tarjetas que puede resultar en la eliminación de una cierta cantidad de registros. Para que los resultados se ajusten a la población total, el proceso va a construir una serie de factores de expansión teniendo en cuenta el total de transacciones, tarjetas únicas y transacciones por línea en el archivo de transacciones antes de la validación. Los campos relacionados al factor de expansión son los siguientes: 13 | 14 | Factor_expansion_original: se encuentra en la tabla de transacciones, es el factor de expansión original que venía especificado en el archivo de transacciones (en caso de ser una muestra). En caso de no ser una muestra, el valor va a ser 1 para todos los registros. 15 | 16 | Factor_expansion_tarjeta: Este factor expande a la cantidad total de tarjetas que se encuentra en el archivo de transacciones original antes de realizarse la depuración por la validación de datos. Este campo se encuentra en la tabla de etapas, viajes y tarjetas. 17 | 18 | Factor_expansion_linea: Este factor calibra el factor_expansion_tarjeta teniendo en cuenta la cantidad de transacciones por línea que se encuentran en el archivo de transacciones original antes de realizarse la depuración por la validación de datos. Este campo se encuentra en la tabla de etapas, viajes y tarjetas. 19 | 20 | Se recomienda usar factor_expansion_linea para el análisis de etapas o viajes y el factor_expansion_tarjeta si se están analizando usuarios. 21 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python 4 | 5 | name: Build 6 | on: 7 | push: 8 | branches: 9 | - main 10 | - dev 11 | pull_request: 12 | types: [opened, synchronize, reopened] 13 | branches: 14 | - main 15 | - dev 16 | jobs: 17 | sonarcloud: 18 | name: SonarCloud 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@v2 22 | with: 23 | fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis 24 | - name: Setup Python 25 | uses: actions/setup-python@v2 26 | with: 27 | python-version: ${{ matrix.python }} 28 | - name: Install tox and any other packages 29 | run: pip install tox 30 | - name: Run tox 31 | run: tox -e py 32 | - name: SonarCloud Scan 33 | uses: SonarSource/sonarcloud-github-action@master 34 | env: 35 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any 36 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} 37 | docs: 38 | runs-on: ubuntu-latest 39 | steps: 40 | - uses: actions/checkout@v3 41 | - uses: ammaraskar/sphinx-action@master 42 | with: 43 | docs-folder: "docs/" 44 | build: 45 | runs-on: ubuntu-latest 46 | strategy: 47 | fail-fast: false 48 | matrix: 49 | python-version: ["3.10"] 50 | 51 | steps: 52 | - uses: actions/checkout@v3 53 | - name: Set up Python ${{ matrix.python-version }} 54 | uses: actions/setup-python@v3 55 | with: 56 | python-version: ${{ matrix.python-version }} 57 | - name: Install dependencies 58 | run: | 59 | python -m pip install --upgrade pip 60 | python -m pip install flake8 pytest 61 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 62 | - name: Lint with flake8 63 | run: | 64 | # stop the build if there are Python syntax errors or undefined names 65 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 66 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 67 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 68 | - name: Test with pytest 69 | run: | 70 | pytest 71 | 72 | build-n-publish: 73 | name: Build and publish Python 🐍 distributions 📦 to PyPI and TestPyPI 74 | runs-on: ubuntu-latest 75 | 76 | steps: 77 | - uses: actions/checkout@master 78 | - name: Set up Python 3.10 79 | uses: actions/setup-python@v3 80 | with: 81 | python-version: "3.10" 82 | 83 | - name: Install pypa/build 84 | run: >- 85 | python -m 86 | pip install 87 | build 88 | --user 89 | - name: Build a binary wheel and a source tarball 90 | run: >- 91 | python -m 92 | build 93 | --sdist 94 | --wheel 95 | --outdir dist/ 96 | . 97 | 98 | - name: Publish distribution 📦 to PyPI 99 | if: startsWith(github.ref, 'refs/tags') 100 | uses: pypa/gh-action-pypi-publish@release/v1 101 | with: 102 | password: ${{ secrets.PYPI_API_TOKEN }} 103 | -------------------------------------------------------------------------------- /urbantrips/datamodel/misc.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | import os 3 | from pandas.io.sql import DatabaseError 4 | from urbantrips.utils.utils import ( 5 | iniciar_conexion_db, 6 | leer_alias, 7 | agrego_indicador, 8 | duracion, 9 | leer_configs_generales, 10 | ) 11 | 12 | 13 | @duracion 14 | def persist_datamodel_tables(): 15 | """ 16 | Esta funcion lee los datos de etapas, viajes y usuarios 17 | le suma informacion de distancias y de zonas 18 | y las guarda en csv 19 | """ 20 | 21 | alias_insumos = leer_configs_generales(autogenerado=False).get("alias_db", "") 22 | conn_insumos = iniciar_conexion_db(tipo="insumos", alias_db=alias_insumos) 23 | 24 | conn_data = iniciar_conexion_db(tipo="data") 25 | 26 | q = """ 27 | SELECT * 28 | from etapas e 29 | where e.od_validado==1 30 | """ 31 | etapas = pd.read_sql_query(q, conn_data) 32 | 33 | agrego_indicador(etapas, "Cantidad total de etapas", "etapas_expandidas", 0) 34 | 35 | for i in etapas.modo.unique(): 36 | agrego_indicador( 37 | etapas.loc[etapas.modo == i], f"Etapas {i}", "etapas_expandidas", 1 38 | ) 39 | 40 | agrego_indicador( 41 | etapas.groupby( 42 | ["dia", "id_tarjeta"], as_index=False 43 | ).factor_expansion_linea.sum(), 44 | "Cantidad de tarjetas finales", 45 | "usuarios", 46 | 0, 47 | var_fex="", 48 | ) 49 | 50 | agrego_indicador( 51 | etapas.groupby( 52 | ["dia", "id_tarjeta"], as_index=False 53 | ).factor_expansion_linea.min(), 54 | "Cantidad total de usuarios", 55 | "usuarios expandidos", 56 | 0, 57 | ) 58 | 59 | # VIAJES 60 | viajes = pd.read_sql_query( 61 | """ 62 | select * 63 | from viajes 64 | where od_validado==1 65 | """, 66 | conn_data, 67 | ) 68 | 69 | agrego_indicador(viajes, "Cantidad de registros en viajes", "viajes", 0, var_fex="") 70 | 71 | agrego_indicador( 72 | viajes, "Cantidad total de viajes expandidos", "viajes expandidos", 0 73 | ) 74 | agrego_indicador( 75 | viajes[(viajes.distancia <= 5)], 76 | "Cantidad de viajes cortos (<5kms)", 77 | "viajes expandidos", 78 | 1, 79 | ) 80 | agrego_indicador( 81 | viajes[(viajes.cant_etapas > 1)], 82 | "Cantidad de viajes con transferencia", 83 | "viajes expandidos", 84 | 1, 85 | ) 86 | 87 | agrego_indicador(viajes, "Cantidad total de viajes expandidos", "modos viajes", 0) 88 | 89 | for i in viajes.modo.unique(): 90 | agrego_indicador( 91 | viajes.loc[(viajes.od_validado == 1) & (viajes.modo == i)], 92 | f"Viajes {i}", 93 | "modos viajes", 94 | 1, 95 | ) 96 | 97 | agrego_indicador( 98 | viajes, 99 | "Distancia de los viajes (promedio en kms)", 100 | "avg", 101 | 0, 102 | var="distancia", 103 | aggfunc="mean", 104 | ) 105 | 106 | agrego_indicador( 107 | viajes, 108 | "Distancia de los viajes (mediana en kms)", 109 | "avg", 110 | 0, 111 | var="distancia", 112 | aggfunc="median", 113 | ) 114 | 115 | for i in viajes.modo.unique(): 116 | agrego_indicador( 117 | viajes.loc[(viajes.od_validado == 1) & (viajes.modo == i)], 118 | f"Distancia de los viajes (promedio en kms) - {i}", 119 | "avg", 120 | 0, 121 | var="distancia", 122 | aggfunc="mean", 123 | ) 124 | 125 | for i in viajes.modo.unique(): 126 | agrego_indicador( 127 | viajes.loc[(viajes.modo == i)], 128 | f"Distancia de los viajes (mediana en kms) - {i}", 129 | "avg", 130 | 0, 131 | var="distancia", 132 | aggfunc="median", 133 | ) 134 | 135 | agrego_indicador( 136 | viajes, 137 | "Etapas promedio de los viajes", 138 | "avg", 139 | 0, 140 | var="cant_etapas", 141 | aggfunc="mean", 142 | ) 143 | 144 | # USUARIOS 145 | print("Leyendo informacion de usuarios...") 146 | usuarios = pd.read_sql_query( 147 | """ 148 | SELECT * 149 | from usuarios 150 | where od_validado==1 151 | """, 152 | conn_data, 153 | ) 154 | 155 | agrego_indicador( 156 | usuarios, 157 | "Cantidad promedio de viajes por tarjeta", 158 | "avg", 159 | 0, 160 | var="cant_viajes", 161 | aggfunc="mean", 162 | ) 163 | 164 | conn_data.close() 165 | conn_insumos.close() 166 | -------------------------------------------------------------------------------- /urbantrips/dashboard/dashboard.py: -------------------------------------------------------------------------------- 1 | import streamlit as st 2 | import pandas as pd 3 | import plotly.express as px 4 | import plotly.graph_objects as go 5 | 6 | import mapclassify 7 | import folium 8 | import matplotlib.pyplot as plt 9 | import geopandas as gpd 10 | import os 11 | import requests 12 | from PIL import Image 13 | from shapely import wkt 14 | import yaml 15 | import sqlite3 16 | from shapely import wkt 17 | from folium import Figure 18 | from shapely.geometry import LineString 19 | 20 | from dash_utils import ( 21 | levanto_tabla_sql, 22 | get_logo, 23 | traigo_indicadores, 24 | configurar_selector_dia, 25 | formatear_columnas_numericas 26 | ) 27 | 28 | 29 | 30 | 31 | 32 | st.set_page_config(layout="wide") 33 | 34 | st.sidebar.success("Seleccione página") 35 | 36 | logo = get_logo() 37 | st.image(logo) 38 | 39 | 40 | st.markdown( 41 | '
urbantrips es una biblioteca de código abierto que toma información de un sistema de pago con tarjeta inteligente de transporte público y, a través de un procesamiento de la información que infiere destinos de los viajes y construye las cadenas de viaje para cada usuario, produce matrices de origen-destino y otros indicadores (KPI) para rutas de autobús. 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 económicas proveniente de un sistema de pago electrónico, se podrán generar resultados, que serán más precisos cuanto 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 de transporte.
', 42 | unsafe_allow_html=True, 43 | ) 44 | st.text("") 45 | 46 | alias_seleccionado = configurar_selector_dia() 47 | 48 | 49 | col1, col2, col3 = st.columns([1, 3, 3]) 50 | 51 | indicadores = levanto_tabla_sql("indicadores", "data") 52 | indicadores = formatear_columnas_numericas(indicadores, ['porcentaje'], False) 53 | 54 | if len(indicadores) > 0: 55 | desc_dia_i = col1.selectbox( 56 | "Dia", options=indicadores.dia.unique(), key="desc_dia_i" 57 | ) 58 | 59 | indicadores = indicadores[(indicadores.dia == desc_dia_i)] 60 | 61 | trx = indicadores.loc[ 62 | indicadores.tabla == "transacciones", ["detalle", "indicador", "porcentaje"] 63 | ].copy() 64 | 65 | col2.write("Preprocesamiento de transacciones") 66 | trx = formatear_columnas_numericas(trx, ['indicador'], True) 67 | col2.write(trx) 68 | 69 | 70 | trx = indicadores.loc[ 71 | indicadores.tabla == "etapas", ["detalle", "indicador", "porcentaje"] 72 | ].copy() 73 | trx = formatear_columnas_numericas(trx, ['indicador'], True) 74 | col3.write("Etapas") 75 | col3.write(trx) 76 | trx = indicadores.loc[ 77 | indicadores.tabla == "viajes", ["detalle", "indicador", "porcentaje"] 78 | ].copy() 79 | 80 | col3.write("Viajes") 81 | trx = formatear_columnas_numericas(trx, ['indicador'], True) 82 | col3.write(trx) 83 | trx = indicadores.loc[ 84 | indicadores.tabla.isin(["tarjetas", "usuarios"]), 85 | ["detalle", "indicador", "porcentaje"], 86 | ].copy() 87 | 88 | # col3.write("Tarjetas") 89 | # trx = formatear_columnas_numericas(trx, ['indicador'], True) 90 | # col3.write(trx) 91 | # trx = indicadores.loc[ 92 | # indicadores.tabla == "etapas_expandidas", ["detalle", "indicador", "porcentaje"] 93 | # ].copy() 94 | 95 | col2.write("Etapas expandidas") 96 | trx = formatear_columnas_numericas(trx, ['indicador'], True) 97 | col2.write(trx) 98 | trx = indicadores.loc[ 99 | indicadores.tabla == "viajes expandidos", ["detalle", "indicador", "porcentaje"] 100 | ].copy() 101 | 102 | col2.write("Viajes expandidos") 103 | trx = formatear_columnas_numericas(trx, ['indicador'], True) 104 | col2.write(trx) 105 | trx = indicadores.loc[ 106 | indicadores.tabla == "usuarios expandidos", 107 | ["detalle", "indicador", "porcentaje"], 108 | ].copy() 109 | 110 | col3.write("Usuarios") 111 | trx = formatear_columnas_numericas(trx, ['indicador'], True) 112 | col3.write(trx) 113 | trx = indicadores.loc[ 114 | indicadores.tabla == "modos viajes", ["detalle", "indicador", "porcentaje"] 115 | ].copy() 116 | 117 | col2.write("Partición modal Viajes") 118 | trx = formatear_columnas_numericas(trx, ['indicador'], True) 119 | col2.write(trx) 120 | 121 | 122 | trx = indicadores.loc[ 123 | (indicadores.tabla == "avg") & 124 | (indicadores.detalle.str.contains('promedio')), ["detalle", "indicador", "porcentaje"] 125 | ].copy() 126 | 127 | col3.write("Promedios") 128 | trx = formatear_columnas_numericas(trx, ['indicador'], False) 129 | col3.write(trx) 130 | 131 | trx = indicadores.loc[ 132 | (indicadores.tabla == "avg") & 133 | (indicadores.detalle.str.contains('mediana')), ["detalle", "indicador", "porcentaje"] 134 | ].copy() 135 | 136 | col3.write("Medianas") 137 | trx = formatear_columnas_numericas(trx, ['indicador'], False) 138 | col3.write(trx) 139 | 140 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | . 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | 130 | -------------------------------------------------------------------------------- /notebooks/KPI.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import os\n", 10 | "import pandas as pd\n", 11 | "from pathlib import Path\n", 12 | "\n", 13 | "# Obtiene la ruta actual y obtiene la ruta de la libreria\n", 14 | "current_path = Path.cwd()\n", 15 | "URBANTRIPS_PATH = current_path.parent\n", 16 | "os.chdir(URBANTRIPS_PATH)\n", 17 | "\n", 18 | "from urbantrips.utils import utils\n", 19 | "from urbantrips.kpi.kpi import compute_route_section_load\n", 20 | "from urbantrips.viz.viz import visualize_route_section_load\n", 21 | "from urbantrips.kpi.line_od_matrix import compute_lines_od_matrix\n", 22 | "from urbantrips.viz.line_od_matrix import visualize_lines_od_matrix\n", 23 | "from urbantrips.kpi.supply_kpi import compute_route_section_supply\n", 24 | "from urbantrips.viz.section_supply import visualize_route_section_supply_data\n", 25 | "\n", 26 | "# Leer archivos de configuración y conexiones a las db\n", 27 | "configs = utils.leer_configs_generales()\n", 28 | "conn_insumos = utils.iniciar_conexion_db(tipo='insumos')" 29 | ] 30 | }, 31 | { 32 | "cell_type": "code", 33 | "execution_count": null, 34 | "metadata": {}, 35 | "outputs": [], 36 | "source": [ 37 | "# Se leen los datos de las lineas\n", 38 | "metadata_lineas = pd.read_sql(\"select id_linea,nombre_linea, modo from metadata_lineas;\", conn_insumos)\n", 39 | "# Se puede buscar por nombre de linea que contenga alguna palabra o numero\n", 40 | "metadata_lineas[metadata_lineas.nombre_linea.str.contains(\"100\") #reemplazar 50 por lo que se desee\n", 41 | " ]" 42 | ] 43 | }, 44 | { 45 | "cell_type": "code", 46 | "execution_count": null, 47 | "metadata": {}, 48 | "outputs": [], 49 | "source": [ 50 | "rango = [9,9] # Se establece un rango horario, en este caso de 7 a 10 \n", 51 | "line_ids = [75] # Se establecen los ids de las lineas a analizar\n", 52 | "day_type = 'weekday' # Se establece el tipo de día a analizar puede ser weekday, weekend o una fecha 1/2/2024\n", 53 | "section_meters = None # Se establece el parámetro de metros de sección\n", 54 | "n_sections = 11 # Se establece el número de secciones a analizar, si se usan metro no se necesita" 55 | ] 56 | }, 57 | { 58 | "cell_type": "code", 59 | "execution_count": null, 60 | "metadata": {}, 61 | "outputs": [], 62 | "source": [ 63 | "# Se calculan los estadisticos de carga de las secciones de las lineas\n", 64 | "compute_route_section_load(\n", 65 | " line_ids=line_ids, hour_range=rango,n_sections=n_sections,\n", 66 | " section_meters = section_meters,day_type=day_type)" 67 | ] 68 | }, 69 | { 70 | "cell_type": "code", 71 | "execution_count": null, 72 | "metadata": {}, 73 | "outputs": [], 74 | "source": [ 75 | "# Se visualizan los estadisticos de carga de las secciones de las lineas\n", 76 | "visualize_route_section_load(\n", 77 | " line_ids=line_ids, hour_range=rango,\n", 78 | " day_type=day_type,n_sections = n_sections, section_meters=section_meters,\n", 79 | " save_gdf=True, stat='totals', \n", 80 | " factor=500, factor_min=10)\n" 81 | ] 82 | }, 83 | { 84 | "cell_type": "code", 85 | "execution_count": null, 86 | "metadata": {}, 87 | "outputs": [], 88 | "source": [ 89 | "# Se computa la matriz OD de las lineas\n", 90 | "compute_lines_od_matrix(\n", 91 | " line_ids=line_ids, hour_range=rango,n_sections=n_sections,\n", 92 | " section_meters=section_meters, day_type=day_type, save_csv=True\n", 93 | ")" 94 | ] 95 | }, 96 | { 97 | "cell_type": "code", 98 | "execution_count": null, 99 | "metadata": {}, 100 | "outputs": [], 101 | "source": [ 102 | "# Se visualiza la matriz OD de las lineas\n", 103 | "visualize_lines_od_matrix(\n", 104 | " line_ids=line_ids, hour_range=rango,\n", 105 | " day_type=day_type, n_sections=n_sections,section_meters=section_meters,\n", 106 | " stat='totals')" 107 | ] 108 | }, 109 | { 110 | "cell_type": "code", 111 | "execution_count": null, 112 | "metadata": {}, 113 | "outputs": [], 114 | "source": [ 115 | "# Calcula los estadisticos de oferta por sección de las lineas\n", 116 | "section_meters = 2000 # Se establece el parámetro de metros de sección mas grandes para que haya menos ruido en los datos\n", 117 | "\n", 118 | "route_section_supply = compute_route_section_supply(\n", 119 | " line_ids=line_ids,\n", 120 | " hour_range=rango,\n", 121 | " section_meters = section_meters,\n", 122 | " day_type=day_type)" 123 | ] 124 | }, 125 | { 126 | "cell_type": "code", 127 | "execution_count": null, 128 | "metadata": {}, 129 | "outputs": [], 130 | "source": [ 131 | "visualize_route_section_supply_data(\n", 132 | " line_ids=line_ids,\n", 133 | " hour_range=rango,\n", 134 | " day_type=\"weekday\",\n", 135 | " section_meters=section_meters)" 136 | ] 137 | }, 138 | { 139 | "cell_type": "code", 140 | "execution_count": null, 141 | "metadata": {}, 142 | "outputs": [], 143 | "source": [ 144 | "# con este codigo se puede consultar la ayuda de las funciones\n", 145 | "visualize_lines_od_matrix?" 146 | ] 147 | } 148 | ], 149 | "metadata": { 150 | "kernelspec": { 151 | "display_name": "venv", 152 | "language": "python", 153 | "name": "python3" 154 | }, 155 | "language_info": { 156 | "codemirror_mode": { 157 | "name": "ipython", 158 | "version": 3 159 | }, 160 | "file_extension": ".py", 161 | "mimetype": "text/x-python", 162 | "name": "python", 163 | "nbconvert_exporter": "python", 164 | "pygments_lexer": "ipython3", 165 | "version": "3.10.16" 166 | } 167 | }, 168 | "nbformat": 4, 169 | "nbformat_minor": 2 170 | } 171 | -------------------------------------------------------------------------------- /urbantrips/tests/data/service_id_gps_test.csv: -------------------------------------------------------------------------------- 1 | service_o,ramal_o,id,id_original,dia,id_linea,id_ramal,interno,fecha,latitud,longitud,velocity,service_type 2 | 1,1,1,1,01/01/2022,1,1,1,1,-34.62755358497184,-58.37902668084492,,start_service 3 | 1,1,2,2,01/01/2022,1,1,1,2,-34.622025934469484,-58.38021117738114,, 4 | 1,1,3,3,01/01/2022,1,1,1,3,-34.61807761268208,-58.38105019576096,, 5 | 1,1,4,4,01/01/2022,1,1,1,4,-34.613240918492515,-58.38114890380565,, 6 | 1,1,5,5,01/01/2022,1,1,1,5,-34.608157454191236,-58.38119825782799,, 7 | 1,1,6,6,01/01/2022,1,1,1,6,-34.60218561748779,-58.3817905060961,, 8 | 1,1,7,7,01/01/2022,1,1,1,7,-34.59606571871731,-58.38198792218547,, 9 | 1,1,8,8,01/01/2022,1,1,1,8,-34.59448639000235,-58.37641091766077,, 10 | 1,1,9,9,01/01/2022,1,1,1,9,-34.59335124748847,-58.37569528433681,, 11 | 1,1,10,10,01/01/2022,1,1,1,10,-34.59167321072883,-58.374560141822926,, 12 | 1,1,11,11,01/01/2022,1,1,1,11,-34.58984711190215,-58.372980813107965,, 13 | 1,1,12,12,01/01/2022,1,1,1,12,-34.58821842916485,-58.37100665221427,, 14 | 1,1,13,13,01/01/2022,1,1,1,13,-34.585800082070065,-58.36962473958867,, 15 | 1,1,14,14,01/01/2022,1,1,1,14,-34.584220753355105,-58.368835075231196,, 16 | 1,1,15,15,01/01/2022,1,1,1,15,-34.584220753355105,-58.368835075231196,, 17 | 1,1,16,16,01/01/2022,1,1,1,16,-34.584220753355105,-58.368835075231196,, 18 | 2,1,17,17,01/01/2022,1,1,1,17,-34.58678716251691,-58.37016763383444,, 19 | 2,1,18,18,01/01/2022,1,1,1,18,-34.58974840385746,-58.372240502772826,, 20 | 2,1,19,19,01/01/2022,1,1,1,19,-34.59325253944379,-58.37490561997932,, 21 | 2,1,20,20,01/01/2022,1,1,1,20,-34.59567088653857,-58.37880458774438,, 22 | 2,1,21,21,01/01/2022,1,1,1,21,-34.59626313480668,-58.381420350928536,, 23 | 2,1,22,22,01/01/2022,1,1,1,22,-34.59937243821426,-58.38166712104025,, 24 | 2,1,23,23,01/01/2022,1,1,1,23,-34.602383033577155,-58.380778748638086,, 25 | 2,1,24,24,01/01/2022,1,1,1,24,-34.606183293297526,-58.38053197852637,, 26 | 2,1,25,25,01/01/2022,1,1,1,25,-34.608700348437,-58.38008779232529,, 27 | 2,1,26,26,01/01/2022,1,1,1,26,-34.61151352771052,-58.38092681070511,, 28 | 2,1,27,27,01/01/2022,1,1,1,27,-34.61472153916279,-58.380778748638086,, 29 | 2,1,28,28,01/01/2022,1,1,1,28,-34.617287948324595,-58.380285208414655,, 30 | 2,1,29,29,01/01/2022,1,1,1,29,-34.62084143793326,-58.38043327048168,, 31 | 2,1,30,30,01/01/2022,1,1,1,30,-34.62335849307273,-58.380334562437,, 32 | 2,1,31,31,01/01/2022,1,1,1,31,-34.626072964301564,-58.38003843830294,, 33 | 2,1,32,32,01/01/2022,1,1,1,32,-34.62789906312824,-58.379841022213576,, 34 | 2,1,33,33,01/01/2022,1,1,1,33,-34.62789906312824,-58.379841022213576,, 35 | 2,1,34,34,01/01/2022,1,1,1,34,-34.627208106815445,-58.381469704950874,, 36 | 2,1,35,35,01/01/2022,1,1,1,35,-34.627208106815445,-58.379841022213576,, 37 | 3,2,36,36,01/01/2022,1,1,1,36,-34.624987175810034,-58.379841022213576,, 38 | 3,2,37,37,01/01/2022,1,1,1,37,-34.62143368620137,-58.38087745668277,, 39 | 3,2,38,38,01/01/2022,1,1,1,38,-34.61669570005649,-58.38107487277214,, 40 | 3,2,39,39,01/01/2022,1,1,1,39,-34.6142773529617,-58.381222934839165,, 41 | 3,2,40,40,01/01/2022,1,1,1,40,-34.61240190011269,-58.38181518310727,, 42 | 3,2,41,41,01/01/2022,1,1,1,41,-34.60771326799015,-58.381420350928536,, 43 | 3,2,42,42,01/01/2022,1,1,1,42,-34.60366623815806,-58.381667121040245,, 44 | 3,2,43,43,01/01/2022,1,1,1,43,-34.60134659910796,-58.381765829084934,, 45 | 3,2,44,44,01/01/2022,1,1,1,44,-34.597102153186505,-58.381963245174305,, 46 | 3,2,45,45,01/01/2022,1,1,1,45,-34.59567088653857,-58.38502319455954,, 47 | 3,2,46,46,01/01/2022,1,1,1,46,-34.59517734631515,-58.387688311766034,, 48 | 3,2,47,47,01/01/2022,1,1,1,47,-34.59468380609172,-58.39099503126299,, 49 | 3,2,48,48,01/01/2022,1,1,1,48,-34.59290706128739,-58.39405498064822,, 50 | 3,2,49,49,01/01/2022,1,1,1,49,-34.59216675095225,-58.39494335305039,, 51 | 3,2,50,50,01/01/2022,1,1,1,50,-34.588860031455305,-58.39657203578769,, 52 | 3,2,51,51,01/01/2022,1,1,1,51,-34.585997498159436,-58.399385215061216,, 53 | 3,2,52,52,01/01/2022,1,1,1,52,-34.58412204531042,-58.40135937595492,, 54 | 3,2,53,53,01/01/2022,1,1,1,53,-34.583776567154025,-58.402149040312395,, 55 | 3,2,54,54,01/01/2022,1,1,1,54,-34.58481300162322,-58.400569711597434,, 56 | 3,2,55,55,01/01/2022,1,1,1,55,-34.58683651653926,-58.398644904726076,, 57 | 3,2,56,56,01/01/2022,1,1,1,56,-34.58318431888591,-58.402445164446455,, 58 | 3,2,57,57,01/01/2022,1,1,1,57,-34.580519201679415,-58.40555446785403,, 59 | 3,2,58,58,01/01/2022,1,1,1,58,-34.579680183299594,-58.40594930003277,, 60 | 99,99,59,59,01/01/2022,1,1,1,59,-34.58017372352302,-58.39593043349724,, 61 | 99,99,60,60,01/01/2022,1,1,1,60,-34.58204917637204,-58.38907022439163,, 62 | 99,99,61,61,01/01/2022,1,1,1,61,-34.582838840729515,-58.381519058973225,, 63 | 99,99,62,62,01/01/2022,1,1,1,62,-34.58407269128808,-58.370957298191925,, 64 | 4,1,63,63,01/01/2022,1,1,1,63,-34.58644168436052,-58.36962473958867,, 65 | 4,1,64,64,01/01/2022,1,1,1,64,-34.58915615558936,-58.371796316571746,, 66 | 4,1,65,65,01/01/2022,1,1,1,65,-34.594585098047034,-58.37534980618041,, 67 | 4,1,66,66,01/01/2022,1,1,1,66,-34.596164426761995,-58.37840975556564,, 68 | 4,1,67,67,01/01/2022,1,1,1,67,-34.596460550896055,-58.38127228886151,, 69 | 4,1,68,68,01/01/2022,1,1,1,68,-34.59873083592381,-58.381420350928536,, 70 | 4,1,69,69,01/01/2022,1,1,1,69,-34.60248174162184,-58.3813709969062,, 71 | 4,1,70,70,01/01/2022,1,1,1,70,-34.60726908178906,-58.381222934839165,, 72 | 4,1,71,71,01/01/2022,1,1,1,71,-34.61792955061505,-58.38023585439232,, 73 | 4,1,72,72,01/01/2022,1,1,1,72,-34.62177916435776,-58.379742314168894,, 74 | 4,1,73,73,01/01/2022,1,1,1,73,-34.627356168882464,-58.37954489807952,, 75 | 4,1,74,74,01/01/2022,1,1,1,74,-34.62706004474841,-58.38072939461574,, 76 | 4,1,75,75,01/01/2022,1,1,1,75,-34.627553584971835,-58.38008779232529,, 77 | 5,3,76,76,01/01/2022,1,1,1,76,-34.62281559882695,-58.38063068657106,, 78 | 5,3,77,77,01/01/2022,1,1,1,77,-34.6178801965927,-58.38092681070511,, 79 | 5,3,78,78,01/01/2022,1,1,1,78,-34.61373445871593,-58.381074872772146,, 80 | 5,3,79,79,01/01/2022,1,1,1,79,-34.60879905648168,-58.38117358081683,, 81 | 5,3,80,80,01/01/2022,1,1,1,80,-34.606232647319864,-58.381321642883854,, 82 | 5,3,81,81,01/01/2022,1,1,1,81,-34.60203755542075,-58.38156841299557,, 83 | 5,3,82,82,01/01/2022,1,1,1,82,-34.60021145659408,-58.38181518310728,, 84 | 5,3,83,83,01/01/2022,1,1,1,83,-34.59739827732055,-58.38201259919665,, 85 | 6,3,84,84,01/01/2022,1,1,1,84,-34.60159336921967,-58.382160661263676,, 86 | 6,3,85,85,01/01/2022,1,1,1,85,-34.60450525653788,-58.38097616472746,, 87 | 6,3,86,86,01/01/2022,1,1,1,86,-34.60741714385609,-58.38112422679448,, 88 | 6,3,87,87,01/01/2022,1,1,1,87,-34.612451254135024,-58.38092681070511,, 89 | 6,3,88,88,01/01/2022,1,1,1,88,-34.61689311614585,-58.38072939461574,, 90 | 6,3,89,89,01/01/2022,1,1,1,89,-34.62094014597794,-58.380778748638086,, 91 | 6,3,90,90,01/01/2022,1,1,1,90,-34.627356168882464,-58.37998908428061,, 92 | -------------------------------------------------------------------------------- /notebooks/Overlapping.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import os\n", 10 | "import pandas as pd\n", 11 | "import geopandas as gpd\n", 12 | "from pathlib import Path\n", 13 | "# Obtiene la ruta actual y obtiene la ruta de la libreria\n", 14 | "current_path = Path.cwd()\n", 15 | "URBANTRIPS_PATH = current_path.parent\n", 16 | "os.chdir(URBANTRIPS_PATH)\n", 17 | "\n", 18 | "from urbantrips.utils import utils\n", 19 | "from urbantrips.kpi import overlapping as ovl\n", 20 | "from urbantrips.viz import overlapping as ovl_viz\n", 21 | "\n", 22 | "\n", 23 | "# Leer archivos de configuración y conexiones a las db\n", 24 | "configs = utils.leer_configs_generales()\n", 25 | "alias = configs['alias_db_data']\n", 26 | "conn_data = utils.iniciar_conexion_db(tipo='data')\n", 27 | "conn_insumos = utils.iniciar_conexion_db(tipo='insumos')" 28 | ] 29 | }, 30 | { 31 | "cell_type": "code", 32 | "execution_count": null, 33 | "metadata": {}, 34 | "outputs": [], 35 | "source": [ 36 | "# Se leen los datos de las lineas\n", 37 | "metadata_lineas = pd.read_sql(\"select id_linea,nombre_linea, modo from metadata_lineas;\", conn_insumos)\n", 38 | "# Se puede buscar por nombre de linea que contenga alguna palabra o numero\n", 39 | "metadata_lineas[metadata_lineas.nombre_linea.str.contains(\"17\") #reemplazar 17 por lo que se desee buscar en el nombre de la linea\n", 40 | " ]" 41 | ] 42 | }, 43 | { 44 | "cell_type": "code", 45 | "execution_count": 3, 46 | "metadata": {}, 47 | "outputs": [], 48 | "source": [ 49 | "# Configurar el día a procesar \n", 50 | "day = \"weekday\"\n", 51 | "\n", 52 | "# La resolucion h3 (no puede ser mayor a la que aparece en las configuraciones)\n", 53 | "h3_res_comp = 8\n", 54 | "\n", 55 | "# Los id de las lineas a comparar\n", 56 | "comp_line_id = 1\n", 57 | "base_line_id = 2" 58 | ] 59 | }, 60 | { 61 | "cell_type": "code", 62 | "execution_count": 4, 63 | "metadata": {}, 64 | "outputs": [], 65 | "source": [ 66 | "comparison_info = ovl.get_route_combinations(base_line_id, comp_line_id)\n", 67 | "\n", 68 | "# Aca se pueden ver todos los id de ruta (lineas o ramales) de las lineas a comparar\n", 69 | "route_id_combinations = comparison_info['route_id_combinations']\n", 70 | "# Aca la metadata de las rutas (lineas o ramales)\n", 71 | "route_metadata= comparison_info['metadata']\n", 72 | "route_type = comparison_info['route_type']\n" 73 | ] 74 | }, 75 | { 76 | "cell_type": "code", 77 | "execution_count": null, 78 | "metadata": {}, 79 | "outputs": [], 80 | "source": [ 81 | "# Selecciona un par de las posibles combinaciones de ramales\n", 82 | "route_id_combination = route_id_combinations[0] \n", 83 | "route_id_combination" 84 | ] 85 | }, 86 | { 87 | "cell_type": "code", 88 | "execution_count": null, 89 | "metadata": {}, 90 | "outputs": [], 91 | "source": [ 92 | "# get routes from the combination\n", 93 | "base_route_id, comp_route_id = ovl.get_route_ids_from_combination(base_line_id, comp_line_id, route_id_combination)" 94 | ] 95 | }, 96 | { 97 | "cell_type": "code", 98 | "execution_count": null, 99 | "metadata": {}, 100 | "outputs": [], 101 | "source": [ 102 | "# Calcula la superposicion de la oferta de la linea base con la de la linea de comparacion\n", 103 | "overlapping_dict = ovl.compute_supply_overlapping(day, base_route_id,comp_route_id,route_type,h3_res_comp) \n", 104 | "base_gdf = overlapping_dict[\"base\"][\"h3\"]\n", 105 | "base_route_gdf = overlapping_dict[\"base\"][\"line\"]\n", 106 | "comp_gdf = overlapping_dict[\"comp\"][\"h3\"]\n", 107 | "comp_route_gdf = overlapping_dict[\"comp\"][\"line\"]\n", 108 | "\n", 109 | "print(overlapping_dict['text_base_v_comp'])\n", 110 | "print(overlapping_dict['text_comp_v_base'])" 111 | ] 112 | }, 113 | { 114 | "cell_type": "code", 115 | "execution_count": 9, 116 | "metadata": {}, 117 | "outputs": [], 118 | "source": [ 119 | "# Se visualiza la superposicion de la oferta de la linea base con la de la linea de comparacion\n", 120 | "f = ovl_viz.plot_interactive_supply_overlapping(overlapping_dict)\n", 121 | "if f is not None:\n", 122 | " f.save(f\"resultados/html/{alias}_supply_overlapping_base_{base_route_id}_comp_{comp_route_id}_h3_{h3_res_comp}.html\")\n", 123 | "f" 124 | ] 125 | }, 126 | { 127 | "cell_type": "code", 128 | "execution_count": 13, 129 | "metadata": {}, 130 | "outputs": [], 131 | "source": [ 132 | "if (base_gdf is not None) and (comp_gdf is not None):\n", 133 | " # Calcula la demanda de la linea base y la de comparacion\n", 134 | " demand_overlapping = ovl.compute_demand_overlapping(base_line_id,comp_line_id,day,\n", 135 | " base_route_id,comp_route_id,\n", 136 | " base_gdf,comp_gdf)\n", 137 | "\n", 138 | " base_demand = demand_overlapping[\"base\"][\"data\"]\n", 139 | " comp_demand = demand_overlapping[\"comp\"][\"data\"]\n", 140 | " print(demand_overlapping[\"base\"][\"output_text\"])\n", 141 | " print(demand_overlapping[\"comp\"][\"output_text\"])\n", 142 | "\n", 143 | " # Se visualiza la superposicion de la demanda de la linea base con la de la linea de comparacion\n", 144 | " demand_overlapping_fig = ovl_viz.plot_interactive_demand_overlapping(base_demand, comp_demand, overlapping_dict)\n", 145 | " fig = demand_overlapping_fig['fig']\n", 146 | " fig.save(f\"resultados/html/{alias}_demand_overlapping_base_{base_route_id}_comp_{comp_route_id}_h3_{h3_res_comp}.html\")\n", 147 | " fig" 148 | ] 149 | }, 150 | { 151 | "cell_type": "code", 152 | "execution_count": null, 153 | "metadata": {}, 154 | "outputs": [], 155 | "source": [] 156 | } 157 | ], 158 | "metadata": { 159 | "kernelspec": { 160 | "display_name": "venv", 161 | "language": "python", 162 | "name": "python3" 163 | }, 164 | "language_info": { 165 | "codemirror_mode": { 166 | "name": "ipython", 167 | "version": 3 168 | }, 169 | "file_extension": ".py", 170 | "mimetype": "text/x-python", 171 | "name": "python", 172 | "nbconvert_exporter": "python", 173 | "pygments_lexer": "ipython3", 174 | "version": "3.10.15" 175 | } 176 | }, 177 | "nbformat": 4, 178 | "nbformat_minor": 2 179 | } 180 | -------------------------------------------------------------------------------- /docs/source/primeros_pasos.rst: -------------------------------------------------------------------------------- 1 | Primeros pasos 2 | ============== 3 | 4 | Una vez creado el ambiente e instalada UrbanTrips es necesario organizar los datos que funcionarán como insumos del proceso y el archivo de configuración. 5 | 6 | **Insumos necesarios y opcionales** 7 | 8 | Urbantrips requiere sólo 3 insumos indispensables: 9 | 10 | * Un archivo de configuración: ``configuraciones_generales.yaml`` 11 | * Un archivo ``csv`` con las transacciones del sistema de pago con tarjeta (las transacciones deben estar georeferenciadas y tener una serie de campos mínimos) 12 | * Un archivo ``csv`` con la información de las líneas y/o ramales que conforman el sistema de transporte público. 13 | 14 | El archivo de configuración tendrá especificados todos los parámetros requeridos para la corrida del proceso UrbanTrips. Entre otros parámetros, deben especificarse las corridas con los diferentes periodos de tiempo que se procesaran. Estos nombres de corridas determinan los nombres que tendran los archivos ``csv``. En el directorio de trabajo (ver `Estructura de directorios`_.) podrá haber diversos archivos con datos de diferentes días o periodos de tiempo (``lunes_trx.csv``, ``martes_trx.csv`` y ``lunes_gps.csv``, ``martes_gps.csv`` o ``enero_trx.csv``, ``febrero_trx.csv`` y ``enero_gps.csv``, ``febrero_gps.csv``). Cada uno será procesado en una corrida por vez. 15 | 16 | El archivo csv con las transacciones debe tener una serie de campos obligatorios (para más detalles ver :doc:`inputs`). Los nombres de estos campos en el archivo pueden ser diferentes y la equivalencia se configura en el archivo ``configuraciones_generales.yaml`` en el parámetro ``nombres_variables_trx``. Para más detalles sobre cómo utilizar este archivo de configuración consulte el apartado :doc:`configuracion`). 17 | 18 | También es necesario un archivo csv que contenga información de las líneas (y ramales en caso de existir). Fundamentalmente debe incluir un nombre de fantasía o de cartel para cada linea y/o ramal con su id correspondiente y el modo, que ser'a estandarizado luego utilizando los parámetros seteados en ``configuraciones_generales.yaml``. Adicionalmente se puede sumar información de empresa y algún campo descriptivo. Para más detalles de los campos que debe incluir puede ver el apartado :doc:`inputs`. La forma de tratar a las líneas y ramales en UrbanTrips es muy específica, por lo tanto se aconseja leer el apartado :doc:`lineas_ramales`. 19 | 20 | Con solo estos archivos se podrá correr el proceso que resultará en la imputación de destinos, construcción de matrices OD y elaboración de algunos KPIs, mapas y gráficos. 21 | 22 | De cualquier forma, se obtienen resultados adicionales y con mayor precisión si se incluyen los siguientes archivos opcionales: 23 | 24 | * Tabla con información de las líneas y/o ramales de transporte público (nombre de fantasía, etc). 25 | * Tabla de GPS con el posicionamiento de las unidades. 26 | * Cartografía de los recorridos de las líneas y/o ramales de transporte público. 27 | * Cartografía de las zonificaciones con las unidades espaciales utilizadas para agregar datos para la matriz OD. 28 | * Cartografía de las paradas y/o estaciones. 29 | 30 | 31 | A modo de ejemplo se puede descargar el `dataset abierto de transacciones SUBE de AMBA `_ , guardarlo en ``data/data_ciudad/transacciones.csv``. Este dataset no cuenta con un campo ``fecha`` con el formato ``dd/mm/aaaa``, deberá agregar con una fecha cualquiera y utilizar las configuraciones especificadas más abajo. A su vez, se debe especificar un ``id_linea`` con el criterio de UrbanTrips (:doc:`lineas_ramales`). Para eso se puede tomar la información de lineas de `este archivo `_ (que se puede utilizar para el parámetro ``nombre_archivo_informacion_lineas``). En este archivo, cada ``id_ramal`` tiene un ``id_linea`` asignado, con esa información pueden construir el ``id_linea`` de la tabla transacciones. 32 | 33 | 34 | Estructura de directorios 35 | ------------------------- 36 | .. _Estructura de directorios: 37 | 38 | Esta es la estructura de directorios de UrbanTrips. ``configs/`` guarda el archivo de configuraciones principal. ``data/`` tendrá por un lado los archivo de insumo para la ciudad (transacciones, gps, etc) y los resultados producto de la corrida de UrbanTrips que se guardarán en ``data/db/``. Para más información del modelo de datos de los resultados finales consulte :doc:`resultados`. Por último en el directorio ``resultados/`` se guardarán algunos resultados agregados en tablas, mapas, gráficos y en formatos más amigables como ``csv``, ``html``, ``png``. 39 | 40 | .. code:: 41 | 42 | urbantrips 43 | │ README.md 44 | │ 45 | └─── urbantrips 46 | │ ... 47 | └─── configs 48 | │ │ configuraciones_generales.yaml 49 | │ │ 50 | └─── data 51 | │ └─── db 52 | │ │ amba_2023_semana1_data 53 | │ │ amba_2023_semana2_data 54 | │ │ amba_2023_insumos 55 | │ 56 | │ └─── data_ciudad 57 | │ │ semana1_trx.csv 58 | │ │ semana2_trx.csv 59 | │ │ lineas_amba.csv 60 | │ │ hexs_amba.geojson 61 | │ │ ... 62 | └─── resultados 63 | │ └─── data 64 | │ │ amba_2023_semana1_etapas.csv 65 | │ │ amba_2023_semana1_viajes.csv 66 | │ │ amba_2023_semana1_usuarios.csv 67 | │ │ amba_2023_semana2_etapas.csv 68 | │ │ amba_2023_semana2_viajes.csv 69 | │ │ amba_2023_semana2_usuarios.csv 70 | │ └─── html 71 | │ │ ... 72 | │ └─── matrices 73 | │ │ ... 74 | │ └─── pdf 75 | │ │ ... 76 | │ └─── png 77 | │ │ ... 78 | │ └─── tablas 79 | 80 | 81 | 82 | Correr Urbantrips 83 | ----------------- 84 | 85 | Una vez que se dispone del archivo de transacciones y el de información de las líneas (junto con los opcionales como gps, recorridos, etc), es posible comenzar a utilizar UrbanTrips. Para una corrida del conjunto del proceso puede utilizar el archivo de configuración que viene por defecto y tendrá una corrida para una muestra del 1% de los datos de área urbana de Buenos Aires para 2019. 86 | 87 | Ejemplos de uso desde consola (Windows o Linux), siempre con el ambiente activado: 88 | 89 | 90 | .. code:: sh 91 | 92 | # Corre solo corridas las pendientes y crea el dashboard 93 | $ python urbantrips\run_all_urbantrips.py 94 | 95 | # Borra todo y vuelve a correr desde cero, creando dashboard 96 | $ python urbantrips\run_all_urbantrips.py --borrar_corrida all 97 | 98 | # Borra y vuelve a correr el alias 'alias1' y lo que falte, creando dashboard 99 | $ python urbantrips\run_all_urbantrips.py --borrar_corrida alias1 100 | 101 | # Corre pendientes sin crear el dashboard 102 | $ python urbantrips\run_all_urbantrips.py --no_dashboard 103 | 104 | # Borra 'alias1', corre lo que falte, y no crea dashboard 105 | $ python urbantrips\run_all_urbantrips.py --borrar_corrida alias1 --no_dashboard 106 | 107 | 108 | Resultados finales 109 | ------------------ 110 | 111 | Una vez procesados los datos, los resultados de urbantrips se guardarán en una base de datos ``SQLite`` en ``data/db/``. Los principales resultados pueden accederse mediante el dashboard interactivo. 112 | 113 | .. code:: sh 114 | 115 | $ streamlit run urbantrips/dashboard/dashboard.py 116 | 117 | 118 | -------------------------------------------------------------------------------- /urbantrips/tests/test_unit_tests.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | import os 3 | import pytest 4 | import os 5 | 6 | from urbantrips.kpi import kpi 7 | from urbantrips.datamodel import legs, misc, trips, transactions 8 | from urbantrips.destinations import destinations as dest 9 | from urbantrips.geo import geo 10 | from urbantrips.utils import utils 11 | from urbantrips.carto import carto, routes 12 | from urbantrips.viz import viz 13 | from urbantrips.carto.routes import process_routes_metadata 14 | 15 | 16 | @pytest.fixture 17 | def df_latlng(): 18 | df = pd.DataFrame( 19 | { 20 | "latitud": [-34.6158037, 39.441915], 21 | "longitud": [-58.5033381, -0.3771238], 22 | } 23 | ) 24 | return df 25 | 26 | 27 | @pytest.fixture 28 | def path_test_data(): 29 | path = os.path.join(os.getcwd(), "urbantrips", "tests", "data") 30 | return path 31 | 32 | 33 | @pytest.fixture 34 | def matriz_validacion_test_amba(path_test_data): 35 | path = os.path.join(path_test_data, "matriz_validacion_amba_test.csv") 36 | df = pd.read_csv(path, dtype={"id_linea": int}) 37 | return df 38 | 39 | 40 | @pytest.fixture 41 | def df_etapas(path_test_data): 42 | path = os.path.join(path_test_data, "subset_etapas.csv") 43 | df = pd.read_csv(path, dtype={"id_tarjeta": str}) 44 | return df 45 | 46 | 47 | @pytest.fixture 48 | def df_trx(path_test_data): 49 | path = os.path.join(path_test_data, "subset_transacciones.csv") 50 | df = pd.read_csv(path, dtype={"id_tarjeta": str}) 51 | return df 52 | 53 | 54 | @pytest.fixture 55 | def df_test_id_viaje(): 56 | dia_1 = pd.DataFrame( 57 | { 58 | "id": range(1, 8), 59 | "fecha_dt": [ 60 | "2022-08-11 12:00", 61 | "2022-08-11 12:30", 62 | "2022-08-11 14:30", 63 | "2022-08-11 18:30", 64 | "2022-08-11 19:30", 65 | "2022-08-11 09:30", 66 | "2022-08-11 10:30", 67 | ], 68 | "id_tarjeta": [1] * 5 + [2, 2], 69 | } 70 | ) 71 | 72 | dia_2 = pd.DataFrame( 73 | { 74 | "id": range(10, 17), 75 | "fecha_dt": [ 76 | "2022-08-12 12:00", 77 | "2022-08-12 12:30", 78 | "2022-08-12 14:30", 79 | "2022-08-12 18:30", 80 | "2022-08-12 19:30", 81 | "2022-08-12 09:30", 82 | "2022-08-12 9:31", 83 | ], 84 | "id_tarjeta": [1] * 5 + [2, 2], 85 | } 86 | ) 87 | 88 | df = pd.concat([dia_1, dia_2]) 89 | 90 | df.fecha_dt = pd.to_datetime(df.fecha_dt) 91 | df["dia"] = df.fecha_dt.dt.strftime("%Y-%m-%d") 92 | 93 | df["hora_shift"] = ( 94 | df.reindex(columns=["dia", "id_tarjeta", "fecha_dt"]) 95 | .groupby(["dia", "id_tarjeta"]) 96 | .shift(1) 97 | ) 98 | df["delta"] = df.fecha_dt - df.hora_shift 99 | df["delta"] = df["delta"].fillna(pd.Timedelta(seconds=0)) 100 | df["delta"] = df.delta.dt.total_seconds() 101 | df["delta"] = df["delta"].map(int) 102 | df["hora"] = df.fecha_dt.dt.strftime("%H:%M:%S") 103 | 104 | return df 105 | 106 | 107 | def test_destinos_potenciales(df_etapas): 108 | def check(d): 109 | primer_origen = d.h3_o.iloc[[0]] 110 | origenes_sig = d.h3_o.iloc[1:] 111 | destinos = pd.concat([origenes_sig, primer_origen]).values 112 | comparacion = d.h3_d.values == destinos 113 | return all(comparacion) 114 | 115 | destinos_potenciales = dest.imputar_destino_potencial(df_etapas) 116 | assert destinos_potenciales.groupby("id_tarjeta").apply(check).all() 117 | 118 | 119 | def test_asignar_id_viaje_etapa(df_trx): 120 | 121 | df_trx["tiempo"] = None 122 | df = legs.asignar_id_viaje_etapa_orden_trx(df_trx) 123 | 124 | # Caso simple 4 colectivos 4 viajes de 1 etapa 125 | df_4_simples = df.loc[df.id_tarjeta == "37030208", :] 126 | 127 | # Caso multimodal 2 viajes, 3 etapas por viaje, 3 modos 128 | multim = df.loc[df.id_tarjeta == "3839538659", :] 129 | 130 | # Checkout y trx en misma hora para viajes 2 y 3 131 | chkout = df.loc[df.id_tarjeta == "37035823", :] 132 | chkout = chkout.loc[chkout.id_viaje.isin([2, 3])] 133 | 134 | assert len(df_4_simples) == 4 135 | assert (df_4_simples.id_viaje == [1, 2, 3, 4]).all() 136 | assert df_4_simples.id_etapa.unique()[0] == 1 137 | 138 | assert (multim.id_viaje == [1] * 3 + [2] * 3).all() 139 | assert (multim.id_etapa == [1, 2, 3] * 2).all() 140 | 141 | assert (chkout.id_viaje == [2, 2, 3]).all() 142 | assert (chkout.id_etapa == [1, 2, 1]).all() 143 | 144 | 145 | def test_h3_from_row(df_latlng): 146 | 147 | lat = "latitud" 148 | lng = "longitud" 149 | res = 8 150 | row = df_latlng.iloc[0] 151 | out = geo.h3_from_row(row, res, lat, lng) 152 | 153 | assert out == "88c2e312b9fffff" 154 | 155 | 156 | def test_referenciar_h3(df_latlng): 157 | out = geo.referenciar_h3(df=df_latlng, res=8, nombre_h3="h3") 158 | assert out.h3.iloc[1] == "8839540a87fffff" 159 | 160 | 161 | def test_crear_viaje_id_acumulada_tarjeta_1(df_test_id_viaje): 162 | dia = df_test_id_viaje.dia == "2022-08-11" 163 | tarj = df_test_id_viaje.id_tarjeta == 1 164 | mask = (dia) & (tarj) 165 | dia_tarjeta = df_test_id_viaje.loc[mask] 166 | 167 | viajes_id_120 = legs.crear_viaje_id_acumulada( 168 | dia_tarjeta, 169 | ventana_viajes=120 * 60, 170 | ) 171 | viajes_id_150 = legs.crear_viaje_id_acumulada( 172 | dia_tarjeta, 173 | ventana_viajes=150 * 60, 174 | ) 175 | viajes_id_30 = legs.crear_viaje_id_acumulada( 176 | dia_tarjeta, 177 | ventana_viajes=30 * 60, 178 | ) 179 | viajes_id_29 = legs.crear_viaje_id_acumulada( 180 | dia_tarjeta, 181 | ventana_viajes=29 * 60, 182 | ) 183 | 184 | assert viajes_id_120 == [1, 1, 2, 3, 3] 185 | assert viajes_id_150 == [1, 1, 1, 2, 2] 186 | assert viajes_id_30 == [1, 1, 2, 3, 4] 187 | assert viajes_id_29 == [1, 2, 3, 4, 5] 188 | 189 | 190 | def test_crear_viaje_id_acumulada(df_test_id_viaje): 191 | trx = df_test_id_viaje.copy() 192 | trx = trx.rename(columns={"fecha_dt": "fecha"}) 193 | trx = legs.asignar_id_viaje_etapa_fecha_completa( 194 | trx, 195 | ventana_viajes=120, 196 | ) 197 | 198 | assert (trx.id_viaje == [1, 1, 2, 3, 3, 1, 1, 1, 1, 2, 3, 3, 1, 1]).all() 199 | assert (trx.id_etapa == [1, 2, 1, 1, 2, 1, 2, 1, 2, 1, 1, 2, 1, 2]).all() 200 | 201 | 202 | def test_cambiar_id_tarjeta_trx_simul_delta(df_test_id_viaje): 203 | trx = df_test_id_viaje.copy() 204 | duplicado_extra = pd.DataFrame( 205 | { 206 | "id": 17, 207 | "fecha_dt": "2022-08-12 09:33:00", 208 | "id_tarjeta": 2, 209 | "dia": "2022-08-12", 210 | "hora_shift": "2022-08-12 09:30:00", 211 | "delta": 3 * 60, 212 | "hora": "09:33:00", 213 | }, 214 | index=[0], 215 | ) 216 | trx = pd.concat([trx, duplicado_extra]).reset_index(drop=True).copy() 217 | trx.id_tarjeta = trx.id_tarjeta.map(str) 218 | trx["id_linea"] = 5 219 | trx["interno"] = 10 220 | trx = trx.rename(columns={"fecha_dt": "fecha"}) 221 | trx = trx.reset_index(drop=True) 222 | 223 | trx_5, tarjetas_duplicadas_5 = legs.cambiar_id_tarjeta_trx_simul_fecha( 224 | trx.copy(), ventana_duplicado=5 225 | ) 226 | 227 | assert len(tarjetas_duplicadas_5) == 2 228 | assert (tarjetas_duplicadas_5.id_tarjeta_original == ["2", "2"]).all() 229 | assert (tarjetas_duplicadas_5.id_tarjeta_nuevo == ["2_1", "2_2"]).all() 230 | 231 | assert ( 232 | trx_5.loc[trx_5["id"].isin([15, 16, 17]), "id_tarjeta"] == ["2_0", "2_1", "2_2"] 233 | ).all() 234 | 235 | trx_1, tarjetas_duplicadas_1 = legs.cambiar_id_tarjeta_trx_simul_fecha( 236 | trx.copy(), ventana_duplicado=1 237 | ) 238 | 239 | assert len(tarjetas_duplicadas_1) == 1 240 | assert (tarjetas_duplicadas_1.id_tarjeta_original == ["2"]).all() 241 | assert (tarjetas_duplicadas_1.id_tarjeta_nuevo == ["2_1"]).all() 242 | assert ( 243 | trx_1.loc[trx_1["id"].isin([15, 16, 17]), "id_tarjeta"] == ["2_0", "2_1", "2_0"] 244 | ).all() 245 | -------------------------------------------------------------------------------- /docs/source/servicios.rst: -------------------------------------------------------------------------------- 1 | Clasificación de servicios 2 | ========================== 3 | 4 | Normalmente en el modelo de datos de los Sistemas de Recaudación Electrónicos para transporte público existe una tabla de datos con la localización de los vehículos de las diferentes líneas en el tiempo (en UrbanTrips la tabla ``gps``). En algunos de estos modelos, existe en esa tabla un atributo que indica cuando un vehículo comienza un servicio, es decir que se presta a subir pasajeros mientras recorre el recorrido de la línea y ramal para el que presta ese servicio. En algunas ocasiones el conductor marca un inicio de servicio para todo su turno, realizando en realidad más de un servicio y posiblemente ramales diferentes en esos servicios. 5 | 6 | Como los servicios son una unidad de información vital para obtener ciertos indicadores estadísticos del sistema, UrbanTrips procede a crear servicios en base a trazas de puntos gps cuando esta declaración de los servicios por parte del conductor no es del todo confiable. Previamente a correr los servicios es necesario que exista una tabla de gps cargada y procesada en la base de datos. Esto se puede hacer en UrbanTrips con la siguiente función, extrayendo los parámetros del archivo de configuración. 7 | 8 | .. code:: 9 | 10 | trx.process_and_upload_gps_table( 11 | nombre_archivo_gps=nombre_archivo_gps, 12 | nombres_variables_gps=nombres_variables_gps, 13 | formato_fecha=formato_fecha) 14 | 15 | Dicha tabla debe tener un atributo donde se especifique el inicio de un servicio. Tambien puede especificarse el final del mismo. Esto debera cargarse en el archivo de configuracion. Por un lado especificando la columna que almacena los datos de servicio, en el mismo lugar que se especifican las otras columnas de la tabla gps en ``servicios_gps``. Por otro lado los valores que en esa columna indican una apertura y cierre de servicios en los parámetros ``valor_inicio_servicio`` y ``valor_fin_servicio``: 16 | 17 | 18 | .. code:: yaml 19 | 20 | nombres_variables_gps: 21 | id_gps: DTSN 22 | id_linea_gps: [ATTR] 23 | id_ramal_gps: [ATTR] 24 | interno_gps: [ATTR] 25 | fecha_gps: [ATTR] 26 | latitud_gps: [ATTR] 27 | longitud_gps: [ATTR] 28 | servicios_gps: [ATTR] 29 | velocity_gps: [ATTR] 30 | 31 | trust_service_type_gps: False 32 | valor_inicio_servicio: [VAL] 33 | valor_fin_servicio: [VAL] 34 | 35 | 36 | 37 | A su vez en el archivo de configuración se debe setear el parámetro correspondiente. Si ese atributo es confiable o si UrbanTrips debe, dentro de cada servicio tal como es declarado por el conductor, clasificar nuevos servicios. 38 | 39 | .. code:: yaml 40 | 41 | utilizar_servicios_gps: False 42 | 43 | Por ultimo solo queda correr 44 | 45 | .. code:: 46 | 47 | services.process_services() 48 | 49 | 50 | 51 | 52 | Como se clasifican nuevos servicios? 53 | ------------------------------------ 54 | 55 | 56 | UrbanTrips tomará los puntos gps que pertenezcan a un servicio tal cual fue declarado por el conductor (con el registro de apertura y cierre en la tabla gps) y procederá a clasificarlos en uno o más servicios, con su id nuevo, en base a un algoritmo que toma como elemento fundamental el orden de paso por las paradas. 57 | 58 | En ese sentido, para que el proceso funcione debe estar cargada la tabla ``stops`` donde se define para cada linea y ramal una serie de paradas indicadas con el orden de paso de cada ramal. Esto se crea con la siguiente función en base a un archivo csv que debe estar creado (para la creación de paradas puede seguir el notebook ``Stops and nodes creation helper.ipynb`` que permitirá crear paradas en base a recorridos). 59 | 60 | .. code:: 61 | 62 | stops.create_stops_table() 63 | 64 | 65 | El proceso se puede resumir del siguiente modo: cada punto gps se asignará a la parada más cercana de esa linea, con su correspondiente orden de paso. Cuando se registra una inversión en el orden de paso por parada, es decir pase de un orden ascendente a descendente o viceversa, se abrira un nuevo servicio. 66 | 67 | 68 | En el ejemplo del caso más sencillo, se puede ver que el punto con ``id = 10`` es donde el orden de paso de paradas se invierte, por lo tanto, es a partir de ese punto que UrbanTrips regista un nuevo servicio. 69 | 70 | 71 | .. image:: ../img/servicios_caso_simple.png 72 | :width: 800 73 | :alt: Clasificacion servicios linea 74 | 75 | 76 | Puede suceder que una línea tenga más de un ramal. En ese caso, se evaluará el punto gps en todos los ramales de esa linea, siempre que estén dentro de una distancia razonable. Los nodos de los ramales lejanos no serán evaluados como posible orden de paso de parada. Luego se evaluará si se registra una inversión en el sentido del orden de paso por paradas. En este caso es en el ``id = 7`` en ambos ramales. 77 | 78 | 79 | .. image:: ../img/servicios_caso_ramal.png 80 | :width: 800 81 | :alt: Clasificacion servicios ramal 82 | 83 | 84 | 85 | Posibles problemas y soluciones propuestas 86 | ------------------------------------------ 87 | 88 | En el ejemplo anterior, no había ambiguedad posible dado que la inversión de sentido sucede sobre un nodo que pertenece al troncal compartido por ambos ramales. Es decir, el mismo punto gps evalúa una inversión del sentido de paso por parada en ambos ramales al mismo tiempo. Pero esto no siempre puede ser así. Puede suceder que haya inversiones en diferentes momentos para los diferentes ramales. Un caso típico es la existencia de un ramal que un fraccionado de un ramal más largo. 89 | 90 | 91 | En este caso se registran dos inversiones de sentido. Por un lado en ``id = 7`` al dar la vuelta sobre el ramal más extenso. Pero también se registra una inversión en ``id = 9``, dado que todos los puntos gps que iban más alla del ramal corto o fraccionado fueron evaluados como ``NaN`` o en el nodo 3 y recién percibe una inversión en el sentido del orden de paso de parada en el nodo 2. La forma que tiene UrbanTripos de resolver esto es la siguiente. Para cambiar de servicio se debe registar una inversión en todos los ramales a los cuales pertenece ese nodo. En este caso, como el nodo 5 solo pertenece al ramal A, es suficiente con que se register una sola inversión de sentido para que asigne un nuevo servicio. Pero como en el nodo 3 participan ambos ramales, a menos que esa inversión se registre en los dos, Urbantrips no abrirá un nuevo servicio. 92 | 93 | 94 | .. image:: ../img/servicios_caso_ramal_fraccionado.png 95 | :width: 800 96 | :alt: Clasificacion servicios ramal 97 | 98 | 99 | Otro caso particular se da cuando existe una configuración de ramales en una linea donde hay una inversión de sentido legítima que no implica un cambio de servicio. Un ramal puede ir y venir sobre sus propios pasos, teniendo paradas a lo largo de ese recorrido. Esto puede inducir un problema en este algoritmo de clasificación de servicios. Tomemos el siguiente ejemplo: 100 | 101 | .. image:: ../img/servicios_caso_ramal_inversion_1.png 102 | :width: 800 103 | :alt: Clasificacion servicios ramal 104 | 105 | Para resolverlo, dichas paradas pueden agregarse en un único nodo mediante el campo ``node_id``. El proceso de clasificación de paradas en realidad utilizará los nodos. Con lo cual, si todas las paradas que puedan implicar una legitima inversión del sentido de paso quedan agrupadas en un único nodo, el algoritmo no registrará ese cambio. 106 | 107 | .. image:: ../img/servicios_caso_ramal_inversion_2.png 108 | :width: 800 109 | :alt: Clasificacion servicios ramal 110 | 111 | 112 | .. image:: ../img/servicios_caso_ramal_inversion.gif 113 | :width: 800 114 | :alt: Clasificacion servicios ramal 115 | 116 | 117 | Resultados 118 | ---------- 119 | 120 | Los resultados de la clasificación de servicios quedan en una serie de tablas dentro de la db de los datos (para más información puede consultar :doc:`resultados`). Estas tablas pueden ofrecer información para diagnósticar la clasificación. 121 | 122 | 123 | * ``services``: agrupa los servicios ofertados por las diferentes lineas, sin clasificarlos por ramal. Cada servicio tiene un id tal cual fue identificado por el conductor del vehículo y otro tal como fue identificado por UrbanTrips. Para cada servicio se agregan algunos datos como la hora de inicio y de fin, la cantidad de puntos gps, el porcentaje de puntos donde el vehículo estuvo detenido, etc. 124 | * ``services_gps_points``: vincula cada punto gps de la tabla ``gps`` con la tabla ``services``. A su vez indican el ``node_id`` más cercano y el ramal al que pertenece. 125 | * ``services_stats``: para cada línea y día arroja una batería de estadísticos comparando los servicios tal cual venían declarados en la información original con los servicios tal cual fueron inferidos por UrbanTrips (la cantidad de servicios nuevos y cuántos de ellos resultan válidos, cúantos de estos son servicios con muy pocos puntos gps o con demasiado tiempo quietos, la distancia recorrida acumulada originalmente y aquella que se obtiene de utilizar sólo los servicios válidos y la proporción de servicios original sin subdividir en otros por parte de UrbanTrips). 126 | * ``services_by_line_hour``: una tabla que resume por linea, dia y hora la cantidad de servicios ofertados. 127 | 128 | 129 | 130 | 131 | 132 | -------------------------------------------------------------------------------- /urbantrips/dashboard/pages/8_Indicadores Operativos.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Dashboard de KPIs – Indicadores Operativos 4 | ========================================= 5 | Panel Streamlit que muestra: 6 | 7 | 1. **KPIs por línea** (filtros de línea y día). 8 | 2. **Totales y promedios del sistema** con filtro de modo. 9 | 3. **Base completa** con descarga CSV. 10 | 11 | Mejora de presentación (jul‑2025) 12 | --------------------------------- 13 | * Alineación consistente de métricas: siempre se generan **6 columnas fijas**; 14 | si un grupo tiene menos indicadores, las celdas restantes quedan vacías, de 15 | modo que la primera, segunda, tercera métrica, etc. aparecen siempre en la 16 | misma posición. 17 | * Tamaño de fuente reducido en `st.metric`. 18 | """ 19 | 20 | import streamlit as st 21 | import pandas as pd 22 | from dash_utils import levanto_tabla_sql, get_logo, configurar_selector_dia 23 | 24 | # ----------------------------------------------------------------------------- 25 | # Configuración global y estilo 26 | # ----------------------------------------------------------------------------- 27 | 28 | st.set_page_config(page_title="Indicadores Operativos por Línea", layout="wide") 29 | 30 | st.markdown( 31 | """ 32 | 36 | """, 37 | unsafe_allow_html=True, 38 | ) 39 | 40 | try: 41 | st.image(get_logo()) 42 | except Exception: 43 | pass 44 | 45 | try: 46 | alias_sel = configurar_selector_dia() 47 | except Exception: 48 | alias_sel = "default" 49 | 50 | # ----------------------------------------------------------------------------- 51 | # Carga de datos 52 | # ----------------------------------------------------------------------------- 53 | 54 | 55 | @st.cache_data(show_spinner=False) 56 | def load_kpis() -> pd.DataFrame: 57 | df = levanto_tabla_sql("kpis_lineas", "general") 58 | return df 59 | 60 | 61 | kpis_df = load_kpis() 62 | 63 | # ----------------------------------------------------------------------------- 64 | # Etiquetas y grupos 65 | # ----------------------------------------------------------------------------- 66 | 67 | LABELS = { 68 | "vehiculos_operativos": "Vehículos operativos", 69 | "transacciones": "Transacciones", 70 | "Masculino": "Masculino", 71 | "Femenino": "Femenino", 72 | "No informado": "No informado", 73 | "sin_descuento": "Sin descuento", 74 | "tarifa_social": "Tarifa social", 75 | "educacion_jubilacion": "Estudiantes/Jubilados", 76 | "velocidad_comercial": "Vel. comercial (km/h)", 77 | "distancia_media_veh": "Dist. media/veh (km)", 78 | "fo_mean": "Factor Ocupación (media)", 79 | "fo_median": "Factor Ocupación (mediana)", 80 | "travel_time_min": "Tiempo promedio viaje (min)", 81 | "tot_km": "Km recorridos", 82 | "dmt_mean": "Distancia media Pax (km)", 83 | "dmt_median": "Distancia mediana Pax (km)", 84 | "ipk": "IPK", 85 | } 86 | 87 | GENERAL_COLS = ["vehiculos_operativos", "transacciones"] 88 | GENDER_COLS = ["Masculino", "Femenino", "No informado"] 89 | TARIFA_COLS = ["sin_descuento", "tarifa_social", "educacion_jubilacion"] 90 | DEMO_COLS = GENDER_COLS + TARIFA_COLS 91 | OPERATIVE_COLS1 = ["velocidad_comercial", "distancia_media_veh", "fo_mean", "fo_median"] 92 | OPERATIVE_COLS2 = ["ipk", "tot_km", "dmt_mean", "dmt_median"] 93 | INT_DISPLAY_COLS = set(GENERAL_COLS + DEMO_COLS + ["tot_km"]) 94 | TOTAL_SLOTS = 6 # columnas fijas por fila 95 | 96 | # ----------------------------------------------------------------------------- 97 | # Funciones auxiliares 98 | # ----------------------------------------------------------------------------- 99 | 100 | 101 | def fmt(val, col): 102 | if pd.isna(val): 103 | return "–" 104 | if col in INT_DISPLAY_COLS or ( 105 | isinstance(val, (int, float)) and float(val).is_integer() 106 | ): 107 | return f"{int(round(val)):,}" 108 | return f"{val:,.2f}" 109 | 110 | 111 | def metric_row(df: pd.DataFrame, cols: list[str], *, pct=False): 112 | """Muestra métricas en una fila de TOTAL_SLOTS columnas fijas.""" 113 | st_cols = st.columns(TOTAL_SLOTS) 114 | total = df["transacciones"].iloc[0] if "transacciones" in df.columns else None 115 | # Rellenar la lista a TOTAL_SLOTS con None 116 | padded_cols = cols + [None] * (TOTAL_SLOTS - len(cols)) 117 | for idx, col in enumerate(padded_cols[:TOTAL_SLOTS]): 118 | if col is None or col not in df.columns: 119 | st_cols[idx].markdown(" ") # espacio en blanco 120 | continue 121 | val = df[col].iloc[0] 122 | text = fmt(val, col) 123 | if pct and total and total > 0: 124 | text += f" ({val / total * 100:.1f} %)" 125 | st_cols[idx].metric(LABELS.get(col, col), text) 126 | 127 | 128 | def weighted_means(df: pd.DataFrame) -> pd.Series: 129 | w = df["transacciones"] 130 | wtot = w.sum() 131 | num_cols = df.select_dtypes("number").columns.difference(["transacciones"]) 132 | return pd.Series( 133 | {c: (df[c] * w).sum() / wtot if wtot else float("nan") for c in num_cols} 134 | ) 135 | 136 | 137 | # ----------------------------------------------------------------------------- 138 | # 1. KPIs por línea 139 | # ----------------------------------------------------------------------------- 140 | 141 | with st.expander("KPIs por línea", expanded=True): 142 | col_filt, col_met = st.columns([2, 10]) 143 | 144 | with col_filt: 145 | 146 | lines = ( 147 | kpis_df[["id_linea", "nombre_linea"]] 148 | .drop_duplicates() 149 | .sort_values("id_linea") 150 | ) 151 | lines["label"] = lines.apply( 152 | lambda x: f"{x.id_linea} – {x.nombre_linea}", axis=1 153 | ) 154 | line_label = st.selectbox("Línea", lines["label"], index=0) 155 | line_id = lines.set_index("label").loc[line_label, "id_linea"] 156 | days = sorted(kpis_df["dia"].dropna().unique()) 157 | day_sel = st.selectbox("Día", ["Promedios"] + days, index=0) 158 | 159 | with col_met: 160 | df_line = kpis_df[kpis_df["id_linea"] == line_id] 161 | df_line = df_line[df_line["dia"] == day_sel] 162 | 163 | if df_line.empty: 164 | st.warning("Sin datos para los filtros seleccionados.") 165 | else: 166 | st.markdown("#### Generales") 167 | metric_row(df_line, GENERAL_COLS) 168 | st.divider() 169 | 170 | st.markdown("#### Género y tipo de tarifa") 171 | metric_row(df_line, DEMO_COLS, pct=True) 172 | st.divider() 173 | 174 | st.markdown("#### Operativos") 175 | metric_row(df_line, OPERATIVE_COLS1) 176 | metric_row(df_line, OPERATIVE_COLS2) 177 | 178 | # ----------------------------------------------------------------------------- 179 | # 2. Totales y promedios (filtro modo) 180 | # ----------------------------------------------------------------------------- 181 | 182 | with st.expander("Totales y promedios del sistema", expanded=False): 183 | col_mode, col_out = st.columns([2, 10]) 184 | 185 | with col_mode: 186 | mode_options = ["Todos"] + sorted(kpis_df["modo"].dropna().unique()) 187 | mode_sel = st.selectbox("Modo", mode_options, index=0) 188 | 189 | with col_out: 190 | df_sel = ( 191 | kpis_df if mode_sel == "Todos" else kpis_df[kpis_df["modo"] == mode_sel] 192 | ) 193 | 194 | # Totales 195 | st.markdown("### Totales") 196 | tot_df = pd.DataFrame( 197 | { 198 | "vehiculos_operativos": [df_sel["vehiculos_operativos"].sum()], 199 | "transacciones": [df_sel["transacciones"].sum()], 200 | "tot_km": [df_sel["tot_km"].sum()], 201 | } 202 | ) 203 | metric_row(tot_df, GENERAL_COLS + ["tot_km"]) 204 | st.divider() 205 | demo_tot = df_sel[DEMO_COLS].sum().to_frame().T 206 | demo_tot["transacciones"] = tot_df["transacciones"].iloc[0] 207 | metric_row(demo_tot, DEMO_COLS, pct=True) 208 | st.divider() 209 | 210 | # Promedios ponderados 211 | st.markdown("### Promedios ponderados por transacciones") 212 | gen_avg = pd.DataFrame( 213 | { 214 | "vehiculos_operativos": [df_sel["vehiculos_operativos"].mean()], 215 | "transacciones": [df_sel["transacciones"].mean()], 216 | } 217 | ) 218 | metric_row(gen_avg, GENERAL_COLS) 219 | st.divider() 220 | demo_wp = df_sel[DEMO_COLS].sum().to_frame().T 221 | demo_wp["transacciones"] = df_sel["transacciones"].sum() 222 | metric_row(demo_wp, DEMO_COLS, pct=True) 223 | st.divider() 224 | op_wp = weighted_means(df_sel).to_frame().T 225 | metric_row(op_wp, OPERATIVE_COLS1) 226 | metric_row(op_wp, OPERATIVE_COLS2) 227 | 228 | # ----------------------------------------------------------------------------- 229 | # 3. Base completa 230 | # ----------------------------------------------------------------------------- 231 | 232 | with st.expander("Base completa", expanded=False): 233 | st.dataframe(kpis_df, use_container_width=True, hide_index=True) 234 | st.download_button( 235 | label="Descargar CSV completo", 236 | data=kpis_df.to_csv(index=False).encode("utf-8"), 237 | file_name="kpis_lineas_completo.csv", 238 | mime="text/csv", 239 | ) 240 | -------------------------------------------------------------------------------- /urbantrips/utils/run_process.py: -------------------------------------------------------------------------------- 1 | import os 2 | from pathlib import Path 3 | from urbantrips.utils.check_configs import check_config 4 | from urbantrips.carto.routes import process_routes_metadata, process_routes_geoms 5 | from urbantrips.carto.stops import create_stops_table 6 | from urbantrips.carto.carto import guardo_zonificaciones 7 | 8 | from urbantrips.utils.utils import ( 9 | create_insumos_general_dbs, 10 | create_data_dash_dbs, 11 | create_directories, 12 | levanto_tabla_sql, 13 | ) 14 | 15 | 16 | from urbantrips.datamodel import legs, trips 17 | from urbantrips.datamodel import transactions as trx 18 | from urbantrips.datamodel import services 19 | from urbantrips.destinations import destinations as dest 20 | from urbantrips.geo import geo 21 | from urbantrips.carto import carto, routes 22 | from urbantrips.utils import utils 23 | from urbantrips.utils.check_configs import check_config 24 | from urbantrips.kpi.kpi import compute_kpi 25 | from urbantrips.datamodel.misc import persist_datamodel_tables 26 | from urbantrips.preparo_dashboard.preparo_dashboard import preparo_indicadores_dash 27 | 28 | 29 | def inicializo_ambiente(): 30 | 31 | corridas_nuevas = [] 32 | # Leer las corridas en el archivo de configuracion 33 | configs_usuario = utils.leer_configs_generales(autogenerado=False) 34 | corridas = configs_usuario.get("corridas", None) 35 | 36 | if corridas is None or len(corridas) == 0: 37 | print("No se han definido corridas en el archivo de configuracion.") 38 | raise ValueError("No se han definido corridas en el archivo de configuracion.") 39 | 40 | path_insumos = configs_usuario["alias_db"] 41 | path_insumos = Path() / "data" / "db" / f"{path_insumos}_insumos.sqlite" 42 | 43 | if not path_insumos.is_file(): 44 | print("Inicializo ambiente por primera vez") 45 | 46 | # chequear consistencia de configuracion 47 | check_config(corridas[0]) 48 | 49 | # Crear directorios basicos de trabajo: 50 | create_directories() 51 | 52 | # Crear una base de datos para insumos y general 53 | create_insumos_general_dbs() 54 | 55 | # Procesar metadata de rutas 56 | process_routes_metadata() 57 | 58 | # Procesar y subir geometrías de rutas 59 | process_routes_geoms() 60 | 61 | # Crear tabla de paradas 62 | create_stops_table() 63 | 64 | # Guarda zonificaciones 65 | guardo_zonificaciones() 66 | 67 | for alias_db in corridas: 68 | path_data = Path() / "data" / "db" / f"{alias_db}_data.sqlite" 69 | 70 | # corridas_nuevas = [] 71 | if not path_data.is_file(): 72 | # Crear una tabla por corrida de data y dash: 73 | create_data_dash_dbs(alias_db) 74 | corridas_nuevas += [alias_db] 75 | 76 | return corridas_nuevas 77 | 78 | 79 | def procesar_transacciones(corrida): 80 | # Chequear consistencia y crear configuracion 81 | check_config(corrida) 82 | 83 | # Read config file 84 | configs = utils.leer_configs_generales() 85 | geolocalizar_trx_config = configs["geolocalizar_trx"] 86 | 87 | # trx configs 88 | nombres_variables_trx = configs["nombres_variables_trx"] 89 | formato_fecha = configs["formato_fecha"] 90 | col_hora = configs["columna_hora"] 91 | tipo_trx_invalidas = configs["tipo_trx_invalidas"] 92 | nombre_archivo_trx = configs["nombre_archivo_trx"] 93 | 94 | # gps configs 95 | nombre_archivo_gps = configs["nombre_archivo_gps"] 96 | nombres_variables_gps = configs["nombres_variables_gps"] 97 | tiempos_viaje_estaciones = configs["tiempos_viaje_estaciones"] 98 | 99 | tolerancia_parada_destino = configs["tolerancia_parada_destino"] 100 | resolucion_h3 = configs["resolucion_h3"] 101 | trx_order_params = { 102 | "criterio": configs["ordenamiento_transacciones"], 103 | "ventana_viajes": configs["ventana_viajes"], 104 | "ventana_duplicado": configs["ventana_duplicado"], 105 | } 106 | 107 | # Compute tolerance in h3 ring 108 | ring_size = geo.get_h3_buffer_ring_size(resolucion_h3, tolerancia_parada_destino) 109 | 110 | # Produce transaction table 111 | trx.create_transactions( 112 | geolocalizar_trx_config, 113 | nombre_archivo_trx, 114 | nombres_variables_trx, 115 | formato_fecha, 116 | col_hora, 117 | tipo_trx_invalidas, 118 | nombre_archivo_gps, 119 | nombres_variables_gps, 120 | ) 121 | 122 | # Turn transactions into legs 123 | legs.create_legs_from_transactions(trx_order_params) 124 | 125 | # Update destination validation matrix 126 | carto.update_stations_catchment_area(ring_size=ring_size) 127 | 128 | # Infer legs destinations 129 | dest.infer_destinations() 130 | 131 | # Create distances table 132 | carto.create_distances_table(use_parallel=False) 133 | 134 | if nombre_archivo_gps is not None: 135 | services.process_services(line_ids=None) 136 | 137 | # Assign a gps point id to legs' origins 138 | legs.assign_gps_origin() 139 | 140 | # Assign a gps point id to legs' destination 141 | legs.assign_gps_destination() 142 | 143 | if tiempos_viaje_estaciones is not None: 144 | # Assign stations to legs for travel times 145 | legs.assign_stations_od() 146 | 147 | # Add distances and travel times to legs 148 | legs.add_distance_and_travel_time() 149 | 150 | # Fix trips with same OD 151 | trips.rearrange_trip_id_same_od() 152 | 153 | # Produce trips and users tables from legs 154 | trips.create_trips_from_legs() 155 | 156 | # compute travel time for trips 157 | trips.compute_trips_travel_time() 158 | 159 | trips.add_distance_and_travel_time() 160 | 161 | # Inferir route geometries based on legs data 162 | routes.infer_routes_geoms() 163 | 164 | # Build final routes from official an inferred sources 165 | routes.build_routes_from_official_inferred() 166 | 167 | # write information about transactions in the database 168 | trx.write_transactions_to_db(corrida) 169 | 170 | # Compute KPI 171 | compute_kpi() 172 | 173 | persist_datamodel_tables() 174 | 175 | 176 | def borrar_corridas(alias_db="all"): 177 | 178 | corridas_nuevas = [] 179 | # Leer las corridas en el archivo de configuracion 180 | configs_usuario = utils.leer_configs_generales(autogenerado=False) 181 | corridas = configs_usuario.get("corridas", None) 182 | 183 | if corridas is None or len(corridas) == 0: 184 | raise ValueError("No se han definido corridas en el archivo de configuracion.") 185 | 186 | if len(alias_db) > 0: 187 | path_ = configs_usuario["alias_db"] 188 | path_insumos = Path() / "data" / "db" / f"{path_}_insumos.sqlite" 189 | path_general = Path() / "data" / "db" / f"{path_}_general.sqlite" 190 | 191 | if alias_db == "all": 192 | if path_insumos.exists(): 193 | path_insumos.unlink() 194 | print(f"Se borró {path_insumos}") 195 | if path_general.exists(): 196 | path_general.unlink() 197 | print(f"Se borró {path_general}") 198 | 199 | for i in corridas: 200 | path_data = Path() / "data" / "db" / f"{i}_data.sqlite" 201 | path_dash = Path() / "data" / "db" / f"{i}_dash.sqlite" 202 | if path_data.exists(): 203 | path_data.unlink() 204 | print(f"Se borró {path_data}") 205 | if path_dash.exists(): 206 | path_dash.unlink() 207 | print(f"Se borró {path_dash}") 208 | else: 209 | path_data = Path() / "data" / "db" / f"{alias_db}_data.sqlite" 210 | path_dash = Path() / "data" / "db" / f"{alias_db}_dash.sqlite" 211 | if path_data.exists(): 212 | path_data.unlink() 213 | print(f"Se borró {path_data}") 214 | if path_dash.exists(): 215 | path_dash.unlink() 216 | print(f"Se borró {path_dash}") 217 | 218 | corridas_anteriores = levanto_tabla_sql( 219 | "corridas", 220 | "general", 221 | query="SELECT DISTINCT corrida FROM corridas WHERE process = 'transactions_completed'", 222 | ) 223 | 224 | if len(corridas_anteriores) > 0: 225 | corridas_anteriores = corridas_anteriores.corrida.values.tolist() 226 | else: 227 | corridas_anteriores = [] 228 | 229 | corridas = [c for c in corridas if c not in corridas_anteriores] 230 | 231 | for i in corridas: 232 | path_data = Path() / "data" / "db" / f"{i}_data.sqlite" 233 | path_dash = Path() / "data" / "db" / f"{i}_dash.sqlite" 234 | if path_data.exists(): 235 | path_data.unlink() 236 | print(f"Se borró {path_data}") 237 | if path_dash.exists(): 238 | path_dash.unlink() 239 | print(f"Se borró {path_dash}") 240 | 241 | 242 | def run_all(borrar_corrida="", crear_dashboard=True): 243 | 244 | print(f"[INFO] borrar_corrida = '{borrar_corrida}'") 245 | print(f"[INFO] crear_dashboard = {crear_dashboard}") 246 | 247 | borrar_corridas(borrar_corrida) 248 | 249 | corridas = inicializo_ambiente() 250 | print("Se procesarán estas corridas:", corridas) 251 | for corrida in corridas: 252 | print(f"Procesando corrida: {corrida}") 253 | procesar_transacciones(corrida) 254 | if crear_dashboard: 255 | preparo_indicadores_dash(corrida) 256 | 257 | print("Fin corrida", corrida) 258 | print('###############################################') 259 | print('###############################################') 260 | print('###############################################') 261 | -------------------------------------------------------------------------------- /docs/source/inputs.rst: -------------------------------------------------------------------------------- 1 | Input de datos 2 | ============== 3 | 4 | Este es el esquema de datos que deben seguir los archivos suministrados como insumos a UrbanTrips. 5 | 6 | Transacciones 7 | ------------- 8 | 9 | Tabla con las transacciones. 10 | 11 | .. list-table:: 12 | :widths: 25 25 50 13 | :header-rows: 1 14 | 15 | * - Campo 16 | - Tipo de dato 17 | - Descripción 18 | * - *id_trx* 19 | - int 20 | - **Obligatorio**. Id único que identifique cada registro y permita luego vincular la transacción en Urbantrips con el dataset original. 21 | * - *fecha_trx* 22 | - strftime 23 | - **Obligatorio**. Timestamp de la transaccion. Puede ser solo el día o el dia, hora y minuto. 24 | * - *id_tarjeta_trx* 25 | - int/str 26 | - **Obligatorio**. Un id que identifique a cada tarjeta. 27 | * - *modo_trx* 28 | - str 29 | - Opcional. Se estandarizará con lo especificado en `modos` en el archivo de configuración. Si no hay información en la tabla, se imputará todo como `autobus`. 30 | * - *hora_trx* 31 | - int 32 | - Opcional a menos que `fecha_trx` no tenga información de la hora y minutos. Entero de 0 a 23 indicando la hora de la transacción. 33 | * - *id_linea_trx* 34 | - int 35 | - **Obligatorio**. Entero que identifique a la linea. 36 | * - *id_ramal_trx* 37 | - int 38 | - Opcional. Entero que identifique al ramal. 39 | * - *interno_trx* 40 | - int 41 | - **Obligatorio**. Entero que identifique al interno 42 | * - *orden_trx* 43 | - int 44 | - Opcional a menos que `fecha_trx` no tenga información de la hora y minutos. Entero comenzando en 0 que esatblezca el orden de transacciones para una misma tarjeta en un mismo día. 45 | * - *latitud_trx* 46 | - float 47 | - **Obligatorio**. Latitud de la transacción. 48 | * - *longitud_trx* 49 | - float 50 | - **Obligatorio**. Longitud de la transacción. 51 | * - *factor_expansion* 52 | - float 53 | - Opcional. Factor de expansión en caso de tratarse de una muestra. 54 | 55 | 56 | Información de lineas y ramales 57 | ------------------------------- 58 | 59 | Tabla con metadata descriptiva de las lineas y ramales. La forma de tratar a las líneas y ramales en UrbanTrips es muy específica, por lo tanto se aconseja leer el apartado :doc:`lineas_ramales`. 60 | 61 | .. list-table:: 62 | :widths: 25 25 50 63 | :header-rows: 1 64 | 65 | * - Campo 66 | - Tipo de dato 67 | - Descripción 68 | * - *id_linea* 69 | - int 70 | - **Obligatorio**. Entero que identifique a la linea. 71 | * - *nombre_linea* 72 | - str 73 | - **Obligatorio**. Nombre de la línea. 74 | * - *modo* 75 | - str 76 | - **Obligatorio**. Modo de la linea. 77 | * - *id_ramal* 78 | - int 79 | - **Obligatorio si hay ramales**.Entero que identifique al ramal. 80 | * - *nombre_ramal* 81 | - str 82 | - **Obligatorio si hay ramales**. Nombre del ramal. 83 | * - *empresa* 84 | - str 85 | - Opcional. Nombre de la empresa. 86 | * - *descripcion* 87 | - str 88 | - Opcional. Descripción adicional de la linea o ramal. 89 | * - *id_linea_agg* 90 | - int 91 | - Opcional. id único de una línea que contenga más de un ramal y deba tratarse de modo unificado para imputar destinos. 92 | * - *nombre_linea_agg* 93 | - str 94 | - Opcional. descripción de la línea que contenga más de un ramal y deba tratarse de modo unificado para imputar destinos 95 | 96 | 97 | 98 | 99 | 100 | Recorridos lineas 101 | ----------------- 102 | 103 | Archivo ``geojson`` con la cartografía de los recorridos de la linea. Debe ser un LineString 2D, sin multilineas. Se necesita una única línea por cada linea o ramal (si existen ramales). Por ello no se considera el sentido del recorrido (ida o vuelta). Se debe tomar uno solo para construir las paradas. En caso de que existan diferencias en el recorrido, se puede desviar el mismo para que pase por un punto medio y seguir siendo un recorrido representativo del ramal. 104 | 105 | .. list-table:: 106 | :widths: 25 25 50 107 | :header-rows: 1 108 | 109 | * - Campo 110 | - Tipo de dato 111 | - Descripción 112 | * - *id_linea* 113 | - int 114 | - **Obligatorio**. Entero que identifique a la linea. 115 | * - *id_ramal* 116 | - int 117 | - **Obligatorio si hay ramales**. Entero que identifique al ramal. 118 | * - *stops_distance* 119 | - int 120 | - Opcional. Distancia en metros a aplicarse al interpolar paradas sobre el recorrido. 121 | * - *line_stops_buffer* 122 | - int 123 | - Opcional. Distancia en metros entre paradas para que se puedan agregar en una sola. 124 | * - *geometry* 125 | - 2DLineString 126 | - Polilinea del recorrido. No puede ser multilinea. 127 | 128 | 129 | GPS 130 | --- 131 | 132 | Tabla con el posicionamiento de cada interno con información de linea y ramal. La existencia de la tabla GPS permitira calcular KPI adicionales como el Índice Pasajero- Kilómetro (IPK) o el factor de ocupación, entre otros. 133 | 134 | .. list-table:: 135 | :widths: 25 25 50 136 | :header-rows: 1 137 | 138 | * - Campo 139 | - Tipo de dato 140 | - Descripción 141 | * - *id_gps* 142 | - int 143 | - **Obligatorio**. Id único que identifique cada registro. 144 | * - *id_linea_gps* 145 | - int 146 | - **Obligatorio**. Id único que identifique la linea. 147 | * - *id_ramal_gps* 148 | - int 149 | - **Obligatorio si hay ramales**. Id único que identifique cada ramal. 150 | * - *interno_gps* 151 | - int 152 | - **Obligatorio**. Id único que identifique cada interno. 153 | * - *fecha_gps* 154 | - strftime 155 | - **Obligatorio**. Dia, hora y minuto de la posición GPS del interno. 156 | * - *latitud_gps* 157 | - float 158 | - **Obligatorio**. Latitud. 159 | * - *longitud_gps* 160 | - float 161 | - **Obligatorio**. Longitud. 162 | * - *servicios_gps* 163 | - int | str 164 | - **Obligatorio si se quiere procesar serviciobs**. Columna que contiene la apertura y cierre de un servicio. 165 | * - *velocity_gps* 166 | - float 167 | - **Opcional**. Velocidad del vehíuclo en km/h. 168 | 169 | Paradas 170 | ------- 171 | 172 | Tabla que contenga las paradas de cada linea y ramal (si hay ramales). El campo ``node_id`` se utiliza para identificar en qué paradas puede haber transbordo entre dos ramales de la misma linea. Para esas paradas el ``node_id`` debe ser el mismo, para las demas paradas debe ser único dentro de la misma línea. De contar con recorridos puede utilizarse el notebook ``stops_creation_with_node_id_helper.ipynb`` para crearlas. 173 | 174 | .. list-table:: 175 | :widths: 25 25 50 176 | :header-rows: 1 177 | 178 | * - Campo 179 | - Tipo de dato 180 | - Descripción 181 | * - *id_linea* 182 | - int 183 | - **Obligatorio**. Entero que identifique a la linea. 184 | * - *id_ramal* 185 | - int 186 | - **Obligatorio si hay ramales**. Entero que identifique a al ramal. 187 | * - *order* 188 | - int 189 | - **Obligatorio**. Entero único que siga un recorrido de la linea o ramal de manera incremental. No importa el sentido 190 | * - *y* 191 | - float 192 | - **Obligatorio**. Latitud. 193 | * - *x* 194 | - float 195 | - **Obligatorio**. Longitud. 196 | * - *node_id* 197 | - int 198 | - **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. 199 | 200 | 201 | Zonificaciones 202 | -------------- 203 | 204 | 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. 205 | 206 | Polígonos de interés 207 | -------------------- 208 | 209 | .. list-table:: 210 | :widths: 25 25 50 211 | :header-rows: 1 212 | 213 | * - id 214 | - tipo 215 | - geometry 216 | * - *id* 217 | - str 218 | - **Obligatorio**. Texto que identifique con un nombre al polígono de interés. 219 | * - *tipo* 220 | - str 221 | - Debe identificar si se trata de un polígono de interés o de una cuenca. Debe tomar valores `poligono` o `cuenca`. 222 | * - *geometry* 223 | - Polygon o MultiPolygon 224 | - Polígono de la zona de interés. 225 | 226 | 227 | 228 | Tiempos de viaje entre estaciones 229 | --------------------------------- 230 | 231 | .. list-table:: 232 | :widths: 25 25 50 233 | :header-rows: 1 234 | 235 | * - Campo 236 | - Tipo de dato 237 | - Descripción 238 | * - *id_o* 239 | - int 240 | - **Obligatorio**. id de la estación de origen. 241 | * - *id_linea_o* 242 | - int 243 | - **Obligatorio**. id de la línea de origen. 244 | * - *id_ramal_o* 245 | - int 246 | - id del ramal de origen en caso de que existan ramales. 247 | * - *lat_o* 248 | - float 249 | - **Obligatorio**. Latitud de la estación de origen. 250 | * - *lon_o* 251 | - float 252 | - **Obligatorio**. Longitud de la estación de origen. 253 | * - *id_d* 254 | - int 255 | - **Obligatorio**. id de la estación de destino. 256 | * - *id_linea_d* 257 | - int 258 | - **Obligatorio**. id de la línea de destino. 259 | * - *id_ramal_d* 260 | - int 261 | - id del ramal de destino en caso de que existan ramales. 262 | * - *lat_d* 263 | - float 264 | - **Obligatorio**. Latitud de la estación de destino. 265 | * - *lon_d* 266 | - float 267 | - **Obligatorio**. Longitud de la estación de destino. 268 | * - *travel_time_min* 269 | - float 270 | - **Obligatorio**. Tiempo de viaje en minutos entre las dos estaciones. 271 | -------------------------------------------------------------------------------- /urbantrips/dashboard/pages/2_Herramientas interactivas.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | import streamlit as st 3 | from dash_utils import ( 4 | levanto_tabla_sql, 5 | get_logo, 6 | create_linestring_od, 7 | create_squared_polygon, 8 | get_epsg_m, 9 | extract_hex_colors_from_cmap, 10 | levanto_tabla_sql_local, 11 | configurar_selector_dia, 12 | leer_configs_generales, 13 | iniciar_conexion_db, 14 | get_logo, 15 | ) 16 | 17 | 18 | try: 19 | from urbantrips.kpi.kpi import compute_route_section_load, run_basic_kpi 20 | from urbantrips.viz.viz import visualize_route_section_load 21 | from urbantrips.kpi.line_od_matrix import compute_lines_od_matrix 22 | from urbantrips.viz.line_od_matrix import visualize_lines_od_matrix 23 | from urbantrips.kpi.supply_kpi import compute_route_section_supply 24 | from urbantrips.viz.section_supply import visualize_route_section_supply_data 25 | from urbantrips.utils.utils import iniciar_conexion_db 26 | from urbantrips.utils import utils 27 | from urbantrips.utils.check_configs import check_config 28 | except ImportError as e: 29 | st.error( 30 | f"Falta una librería requerida: {e}. Algunas funcionalidades no estarán disponibles. \nSe requiere full acceso a Urbantrips para correr esta página" 31 | ) 32 | st.stop() 33 | 34 | 35 | # --- Función para levantar tablas SQL y almacenar en session_state --- 36 | def cargar_tabla_sql(tabla_sql, tipo_conexion="dash", query=""): 37 | if f"{tabla_sql}_{tipo_conexion}" not in st.session_state: 38 | conn = iniciar_conexion_db(tipo=tipo_conexion) 39 | try: 40 | query = query or f"SELECT * FROM {tabla_sql}" 41 | tabla = pd.read_sql_query(query, conn) 42 | st.session_state[f"{tabla_sql}_{tipo_conexion}"] = tabla 43 | except Exception: 44 | st.error(f"{tabla_sql} no existe") 45 | st.session_state[f"{tabla_sql}_{tipo_conexion}"] = pd.DataFrame() 46 | finally: 47 | conn.close() 48 | return st.session_state[f"{tabla_sql}_{tipo_conexion}"] 49 | 50 | 51 | def seleccionar_linea(key_input, key_select): 52 | texto_a_buscar = st.text_input( 53 | f"Ingrese el texto a buscar en líneas", key=key_input 54 | ) 55 | if texto_a_buscar: 56 | if f"df_filtrado_{texto_a_buscar}" not in st.session_state: 57 | st.session_state[f"df_filtrado_{texto_a_buscar}"] = metadata_lineas[ 58 | metadata_lineas.apply( 59 | lambda row: row.astype(str) 60 | .str.contains(texto_a_buscar, case=False, na=False) 61 | .any(), 62 | axis=1, 63 | ) 64 | ] 65 | df_filtrado = st.session_state[f"df_filtrado_{texto_a_buscar}"] 66 | 67 | if not df_filtrado.empty: 68 | opciones = df_filtrado.apply( 69 | lambda row: f"{row['nombre_linea']}", axis=1 70 | ).tolist() 71 | seleccion_texto = st.selectbox( 72 | f"Seleccione una línea de colectivo", 73 | opciones, 74 | key=key_select, 75 | ) 76 | df_seleccionado = df_filtrado.iloc[opciones.index(seleccion_texto)] 77 | 78 | st.session_state["nombre_linea_7"] = df_seleccionado.nombre_linea 79 | st.session_state["id_linea_7"] = df_seleccionado.id_linea 80 | 81 | else: 82 | st.warning("No se encontró ninguna coincidencia.") 83 | 84 | 85 | st.set_page_config(layout="wide") 86 | 87 | 88 | logo = get_logo() 89 | st.image(logo) 90 | alias_seleccionado = configurar_selector_dia() 91 | check_config(corrida=alias_seleccionado) 92 | st.text(f"Alias seleccionado: {alias_seleccionado}") 93 | 94 | try: 95 | 96 | # --- Cargar configuraciones y conexiones en session_state --- 97 | if "configs" not in st.session_state: 98 | st.session_state.configs = leer_configs_generales(autogenerado=True) 99 | 100 | configs = st.session_state.configs 101 | h3_legs_res = configs["resolucion_h3"] 102 | alias = configs["alias_db_data"] 103 | st.text( 104 | f"Base de datos seleccionada: {alias}. Si no es la correcta, cambiar el archivo configuraciones_generales.yaml" 105 | ) 106 | use_branches = configs["lineas_contienen_ramales"] 107 | 108 | metadata_lineas = cargar_tabla_sql("metadata_lineas", "insumos")[ 109 | ["id_linea", "nombre_linea"] 110 | ] 111 | conn_insumos = iniciar_conexion_db(tipo="insumos") 112 | 113 | except ValueError as e: 114 | st.error( 115 | f"Falta una base de datos requerida: {e}. \nSe requiere full acceso a Urbantrips para correr esta página" 116 | ) 117 | st.stop() 118 | 119 | 120 | for var in [ 121 | "id_linea_7", 122 | "nombre_linea_7", 123 | "day_type_7", 124 | "yr_mo_7", 125 | "n_sections_7", 126 | "section_meters_7", 127 | "hour_range_7", 128 | ]: 129 | if var not in st.session_state: 130 | st.session_state[var] = None 131 | 132 | 133 | st.header("Herramientas") 134 | 135 | col1, col2, col3 = st.columns([1, 2, 1]) 136 | with col1: 137 | st.subheader("Periodo") 138 | 139 | kpi_lineas = levanto_tabla_sql("agg_indicadores") 140 | if len(kpi_lineas) == 0: 141 | months = None 142 | else: 143 | months = kpi_lineas.mes.unique() 144 | 145 | day_type = col1.selectbox("Tipo de dia  ", options=["weekday", "weekend"]) 146 | st.session_state["day_type_7"] = day_type 147 | 148 | # add month and year 149 | yr_mo = col1.selectbox("Periodo  ", options=months, key="year_month") 150 | st.session_state["yr_mo_7"] = yr_mo 151 | 152 | with col2: 153 | st.subheader("Línea") 154 | seleccionar_linea("base_input", "base_select") 155 | 156 | with col3: 157 | st.subheader("Parámetros") 158 | n_sections = col3.number_input( 159 | "Numero Secciones", min_value=0, max_value=999, value=None 160 | ) 161 | st.session_state["n_sections_7"] = n_sections 162 | 163 | section_meters = col3.number_input( 164 | "Metros de cada seccion", min_value=0, max_value=5000, value=None 165 | ) 166 | st.session_state["section_meters_7"] = section_meters 167 | 168 | ( 169 | col3a, 170 | col3b, 171 | ) = st.columns([1, 1]) 172 | 173 | rango_desde = col3a.selectbox( 174 | "Rango horario (desde) ", options=range(0, 24), key="rango_hora_desde", index=9 175 | ) 176 | rango_hasta = col3b.selectbox( 177 | "Rango horario (hasta)", options=range(0, 24), key="rango_hora_hasta", index=9 178 | ) 179 | hour_range = [rango_desde, rango_hasta] 180 | st.session_state["hour_range_7"] = hour_range 181 | 182 | 183 | line_ids = st.session_state["id_linea_7"] 184 | 185 | geoms_check = (st.session_state["n_sections_7"] is not None) | ( 186 | st.session_state["section_meters_7"] is not None 187 | ) 188 | 189 | if st.button("Comenzar a procesar"): 190 | if (line_ids is not None) & (geoms_check): 191 | 192 | hour_range = st.session_state["hour_range_7"] 193 | n_sections = st.session_state["n_sections_7"] 194 | section_meters = st.session_state["section_meters_7"] 195 | day_type = st.session_state["day_type_7"] 196 | 197 | st.write("Calculando indicadores basicos...") 198 | run_basic_kpi(id_linea=[line_ids]) 199 | 200 | st.write("Calculando la matriz OD de la linea...") 201 | # Se computa la matriz OD de las lineas 202 | compute_lines_od_matrix( 203 | line_ids=[line_ids], 204 | hour_range=hour_range, 205 | n_sections=n_sections, 206 | section_meters=section_meters, 207 | day_type=day_type, 208 | save_csv=True, 209 | ) 210 | st.write("Visualizando la matriz OD de la linea...") 211 | # Se visualiza la matriz OD de las lineas 212 | visualize_lines_od_matrix( 213 | line_ids=[line_ids], 214 | hour_range=hour_range, 215 | day_type=day_type, 216 | n_sections=n_sections, 217 | section_meters=section_meters, 218 | stat="totals", 219 | ) 220 | 221 | st.write("Calculando los estadisticos de oferta por secciones de las lineas...") 222 | # Calcula los estadisticos de oferta por sección de las lineas 223 | route_section_supply = compute_route_section_supply( 224 | line_ids=[line_ids], 225 | hour_range=hour_range, 226 | n_sections=n_sections, 227 | section_meters=section_meters, 228 | day_type=day_type, 229 | ) 230 | 231 | st.write( 232 | "Visualizando los estadisticos de oferta por secciones de las lineas..." 233 | ) 234 | # Visualiza los estadisticos de oferta por sección de las lineas 235 | visualize_route_section_supply_data( 236 | line_ids=[line_ids], 237 | hour_range=hour_range, 238 | day_type=day_type, 239 | n_sections=n_sections, 240 | section_meters=section_meters, 241 | ) 242 | 243 | st.write( 244 | "Calculando los estadisticos de carga de las secciones de las lineas..." 245 | ) 246 | # Se calculan los estadisticos de carga de las secciones de las lineas 247 | compute_route_section_load( 248 | line_ids=[line_ids], 249 | hour_range=hour_range, 250 | n_sections=n_sections, 251 | section_meters=section_meters, 252 | day_type=day_type, 253 | ) 254 | 255 | st.write( 256 | "Visualizando los estadisticos de carga de las secciones de las lineas..." 257 | ) 258 | # Se visualizan los estadisticos de carga de las secciones de las lineas 259 | visualize_route_section_load( 260 | line_ids=[line_ids], 261 | hour_range=hour_range, 262 | day_type=day_type, 263 | n_sections=n_sections, 264 | section_meters=section_meters, 265 | save_gdf=True, 266 | stat="totals", 267 | factor=500, 268 | factor_min=10, 269 | ) 270 | 271 | st.write( 272 | "Resultados pueden consultarse en el directorio UrbanTrips/" 273 | "resultados o en la pestaña Indicadores de oferta y demanda" 274 | ) 275 | 276 | else: 277 | st.write("No hay datos para mostrar") 278 | -------------------------------------------------------------------------------- /configs/configuraciones_generales.yaml: -------------------------------------------------------------------------------- 1 | # Archivo de configuración para urbantrips 2 | 3 | corridas: ["martes","miercoles"] # Listar los nombres de las corridas ["lunes","martes"], deben coincidir con los archivos trx y gps,por ej lunes_trx.csv, lunes_gps.csv, martes_trx.csv, martes_gps.csv 4 | 5 | # Bases de datos 6 | alias_db: "amba_2019_muestra1" # El nombre que va a tener la base de datos sqlite de insumos y dash. La de data depende de la corrida 7 | 8 | #nombre_archivo_trx: "[CORRIDA]_trx.csv" # CONSTRUIR [CORRIDA]_trx.csv Especificar el archivo con las transacciones a consumir 9 | #nombre_archivo_gps: "[CORRIDA]_gps.csv" # CONSTRUIR [CORRIDA]_gps.csv Especificar el archivo con las transacciones a consumir 10 | 11 | # Nombre de columnas en el archivo de transacciones 12 | nombres_variables_trx: 13 | id_trx: "id" # columna con id único del archivo de transacciones 14 | fecha_trx: "fecha" # columna con fecha de la transacción 15 | id_tarjeta_trx: "id_tarjeta" # columna con id de la tarjeta 16 | modo_trx: "modo" # columna con modo de transporte 17 | hora_trx: "hora" # columna con hora de la transacción 18 | id_linea_trx: "id_linea" # columna con el id de la línea 19 | id_ramal_trx: # columna con el ramal de la línea 20 | interno_trx: "interno_bus" # columna con interno de la línea 21 | orden_trx: "etapa_red_sube" # columna con el orden de la transacción (si falta hora/minuto en fecha_trx) 22 | genero: "sexo" # Indica el género asignado a la tarjeta 23 | tarifa: "id_tarifa" # Indica el tipo de tarifa asignado a la transacción 24 | latitud_trx: "lat" # columna con la latitud de la transacción 25 | longitud_trx: "lon" # columna con longitud de la transacción 26 | factor_expansion: "fex_linea" # columna con el factor de expansión 27 | 28 | # Parámetros de transacciones 29 | 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 30 | ventana_viajes: 120 # ventana de tiempo para que una transacción sea de un mismo viaje (ej. 60 minutos) 31 | ventana_duplicado: 5 # ventana de tiempo si hay duplicado de transacción (ej. Viaje con acompañante) 32 | 33 | # Elimina transacciones inválidas de la tabla de transacciones 34 | tipo_trx_invalidas: 35 | tipo_trx_tren: [ # Lista con el contenido a eliminar de la variable seleccionada 36 | "CHECK OUT SIN CHECKIN", 37 | "CHECK OUT", 38 | ] 39 | 40 | 41 | # Imputación de destino 42 | tolerancia_parada_destino: 2200 # Distancia para la validación de los destinos (metros) 43 | imputar_destinos_min_distancia: True # Busca la parada que minimiza la distancia con respecto a la siguiente trancción 44 | 45 | # Parámetros geográficos 46 | resolucion_h3: 8 # Resolución de los hexágonos 47 | epsg_m: 9265 # Parámetros geográficos: crs 48 | 49 | formato_fecha: "%d/%m/%Y" # Configuración fecha y hora 50 | columna_hora: True 51 | 52 | usa_archivo_gps: False 53 | geolocalizar_trx: False 54 | 55 | # Nombre de columnas en el archivo de GPS 56 | nombres_variables_gps: 57 | id_gps: 58 | id_linea_gps: 59 | id_ramal_gps: 60 | interno_gps: 61 | fecha_gps: 62 | latitud_gps: 63 | longitud_gps: 64 | velocity_gps: 65 | distance_gps: 66 | servicios_gps: # Indica cuando se abre y cierra un servicio 67 | 68 | # Información para procesamiento de líneas 69 | nombre_archivo_informacion_lineas: "metadata_amba_2019_m1.csv" # Archivo .csv con lineas, debe contener ("id_linea", "nombre_linea", "modo") 70 | lineas_contienen_ramales: False # Especificar si las líneas de colectivo contienen ramales 71 | nombre_archivo_paradas: 72 | imprimir_lineas_principales: 5 # Imprimir las lineas principales - "All" imprime todas las líneas 73 | 74 | # Servicios GPS 75 | utilizar_servicios_gps: False # Especifica si ve van a utilizar los servicios GPS 76 | valor_inicio_servicio: # Valor de la variable que marca el inicio del servicio 77 | valor_fin_servicio: # Valor de la variable que marca el fin del servicio 78 | 79 | modos: 80 | autobus: "COL" 81 | tren: "TRE" 82 | metro: "SUB" 83 | tranvia: 84 | brt: 85 | cable: 86 | lancha: 87 | otros: 88 | 89 | # Capas geográficas con recorridos de líneas 90 | recorridos_geojson: "recorridos_2019_linea_unica_m1.geojson" # archivo geojson con el trazado de las líneas de transporte público 91 | 92 | filtro_latlong_bbox: 93 | minx: -59.3 94 | miny: -35.5 95 | maxx: -57.5 96 | maxy: -34.0 97 | 98 | # Zonificaciones 99 | zonificaciones: 100 | geo1: "partidos.geojson" 101 | var1: "Zona" 102 | orden1: 103 | geo2: "partidos.geojson" 104 | var2: "Partido" 105 | orden2: [ 106 | "Comuna 1", 107 | "Comuna 2", 108 | "Comuna 3", 109 | "Comuna 4", 110 | "Comuna 5", 111 | "Comuna 6", 112 | "Comuna 7", 113 | "Comuna 8", 114 | "Comuna 9", 115 | "Comuna 10", 116 | "Comuna 11", 117 | "Comuna 12", 118 | "Comuna 13", 119 | "Comuna 14", 120 | "Comuna 15", 121 | "Almirante Brown", 122 | "Avellaneda", 123 | "Berazategui", 124 | "Berisso", 125 | "Brandsen", 126 | "Campana", 127 | "Carmen de Areco", 128 | "Cañuelas", 129 | "Ensenada", 130 | "Escobar", 131 | "Esteban Echeverría", 132 | "Exaltación de la Cruz", 133 | "Ezeiza", 134 | "Florencio Varela", 135 | "General Las Heras", 136 | "General Rodríguez", 137 | "General San Martín", 138 | "Hurlingham", 139 | "Ituzaingó", 140 | "José C. Paz", 141 | "La Matanza Oeste", 142 | "La Matanza Centro", 143 | "La Matanza Este", 144 | "La Plata", 145 | "Lanús", 146 | "Lobos", 147 | "Lomas de Zamora", 148 | "Luján", 149 | "Magdalena", 150 | "Malvinas Argentinas", 151 | "Marcos Paz", 152 | "Mercedes", 153 | "Merlo", 154 | "Moreno", 155 | "Morón", 156 | "Navarro", 157 | "Pilar", 158 | "Presidente Perón", 159 | "Punta Indio", 160 | "Quilmes", 161 | "San Andrés de Giles", 162 | "San Antonio de Areco", 163 | "San Fernando", 164 | "San Isidro", 165 | "San Miguel", 166 | "San Vicente", 167 | "Tigre", 168 | "Tres de Febrero", 169 | "Vicente López", 170 | "Zárate", 171 | ] 172 | 173 | geo3: 174 | var3: 175 | orden3: 176 | geo4: 177 | var4: 178 | orden4: 179 | geo5: 180 | var5: 181 | orden5: 182 | 183 | poligonos: "poligonos.geojson" # 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'. 184 | 185 | tiempos_viaje_estaciones: "travel_time_stations.csv" # Especificar una tabla de tiempo de viaje en minutos entre estaciones para modos sin gps 186 | 187 | -------------------------------------------------------------------------------- /docs/source/kpi.rst: -------------------------------------------------------------------------------- 1 | KPI 2 | ============== 3 | 4 | En este apartado se describen los diferentes tipos de KPI que UrbanTrips puede producir en función de los inputs con los que cada ciudad cuente. Se comienza por el caso donde solamente existen datos de demanda. En segundo lugar se abordan los KPI que puede producir cuando existen datos en de la oferta, como puede ser la tabla de GPS como así también información de servicios ofertados. 5 | 6 | En base a demanda 7 | ----------------- 8 | 9 | Cuando no existen datos de oferta, expresada fundamentalmente en la tabla GPS de posicionamiento de vehículos, UrbanTrips puede calcular algunos elementos de oferta y demanda en base a la tabla de transacciones en base al concepto de *pasajero equivalente*. Tomando la tabla de etapas se calcula por hora y dia para el interno de cada linea el total de pasajeros que iniciaron una etapa a esa hora en ese interno, la distancia media viajada (*DMT*) y la velocidad comercial promedio a la que circula ese interno en esa hora (siempre tomando solamente las coordenadas del interno cuando recoge pasajeros). 10 | 11 | Tomando estos datos se construye para cada pasajero un *pasajero equivalente* poniendo en relación cuántas posiciones disponibles en ese interno utilizó y por cuánto tiempo. Es decir, si un pasajero recorre 5 km en un interno que circula a 10 kmh, equivaldrá a 0.5 posiciones o pasajeros equivalentes. Para calcular un factor de ocupación se considera que cada interno oferta 60 ubicaciones y se compara el total de pasajeros equivalentes en esa hora en ese interno contra ese stock fijo. Luego se procede a agregar todos estos estadísticos para diferentes niveles de agregación (interno y linea) así como también para un día de la semana tipo o dia de fin de semana tipo (siempre que hayan días procesados que pertenezcan a uno de esos tipos). 12 | 13 | Los resultados quedan almacenados en las siguientes tablas (para más detalles vea el apartado :doc:`resultados`). 14 | 15 | * ``basic_kpi_by_vehicle_hr``: arroja la batería de estadísticos por vehículo para cada linea, día y hora. 16 | * ``basic_kpi_by_line_hr``: arroja la batería de estadísticos promediando para cada linea, día y hora (incluyendo día de semana y de fín de semana típico). 17 | * ``basic_kpi_by_line_day``: arroja la batería de estadísticos promediando para cada linea y día (incluyendo día de semana y de fín de semana típico). 18 | 19 | 20 | **Tramo más cargado** 21 | 22 | UrbanTrips también puede computar, sólo con datos de la demanda, para una línea, día y rango horario la carga o demanda para los tramos del recorrido de esa línea. El recorrido de una línea puede segmentarse en *n* tramos o en tramos cada *m* cantidad de metros. A partir de este parámetro se computan *x* cantidad de puntos sobre el recorrido de la línea. Si existe una cartografía provista por el usuario para las líneas, se utilizará ese recorrido. En caso contrario, se inferirá un recorrido a partir de las coordenadas de las transacciones de dicha línea. 23 | 24 | Para cada etapa de una línea en un rango horario se toman las coordenadas de origen y de destinos de la misma, se proyectan sobre el recorrido de la línea mediante un `Linear Referencing System LRS `_ y se asume que dicha etapa hizo uso del recorrido de la línea entre los puntos comprendidos por el tramo de origen y el de destino, inclusive. Luego se calcula para cada punto de la línea la demanda en base a las etapas. Dado el sentido del desplazamiento de la etapa, se puede inferir el sentido de la misma, por lo cual se calcula la demanda separada por sentido. La misma se calcula tanto en cantidad de etapas como proporcional con respecto al total de etapas de esa línea para ese rango horario en ese sentido. 25 | 26 | Por ejemplo, un usuario que inicia una etapa en el punto equiparable al 10% del recorrido y desciende en el punto equiparable al 30% del recorrido en una línea segmentada en 10 tramos, habrá utilizado los puntos equivalentes al 10, 20 y 30 %. Dado el sentido ascendente del LRS, se asume un sentido de ida. Luego se calcula la cantidad de etapas que hayan atravesado el punto 10%, ya sea como origen, destino o como punto de paso. Una vez calculado para cada día, se puede calcular un promedio para los días de semana o para los fines de semana en base a los datos ya procesados para cada día. 27 | 28 | Para computar esta demanda por tramo se puede utilizar la siguiente función: 29 | 30 | * ``id_linea``: id de línea o lista de ids de líneas a procesar 31 | * ``hour_range``: define el rango horario del origen de las etapas a utilizar 32 | * ``n_sections``: cantidad de segmentos a dividir el recorrido 33 | * ``section_meters``: cantidad de metros de largo del segmento. Si se especifica este parámetro no se considerará ``n_sections``. 34 | * ``day_type``: fecha de los datos a procesar o tipo de día (``weekday`` o ``weekend``). 35 | 36 | .. code:: python 37 | 38 | kpi.compute_route_section_load( 39 | id_linea=False, 40 | hour_range=False, 41 | n_sections=10, 42 | section_meters=None, 43 | day_type="weekday" 44 | ) 45 | 46 | 47 | 48 | Urbantrips también permite construir una visualización exploratoria en base a los datos computados previamente. La misma conformará a partir de los *x* puntos *x-1* segmentos donde el tamaño y color del mismo indicará la demanda, ya sea en cantidad total de etapas (``indicador='cantidad_etapas'``) o proporcional (``indicador = ‘prop_etapas’``). La función toma un parámetro de ancho mínimo en metros que dichos segmentos van a tomar (para los segmentos con mínima o nula demanda) y un factor de expansión también en metros. Por ejemplo, si se utiliza el indicador ``prop_etapas`` y ``factor = 500`` aquel segmento que tenga una demanda igual al máximo de la demanda de esa línea en ese sentido para ese rango horario, el buffer del recorrido a visualizar tendrá un ancho de 500 m (``1 * 500``) y aquel que tenga una demanda equivalente a la mitad de esa demanda total, tendrá un ancho de 250 m en la visualización (``0,5 * 500``). Por otro lado, si se toma como indicador de la visualización la cantidad total de etapas y el mismo parámetro ``factor``, un segmento con demanda equivalente a 100 etapas tendrá un ancho de 50.000 m (``100 * 500``). Esta visualización permite guardar el geojson producto de la misma ``save_gdf=True``. 49 | 50 | 51 | .. code:: python 52 | 53 | viz.visualize_route_section_load( 54 | id_linea=False, 55 | hour_range=False, 56 | day_type='weekday', 57 | n_sections=10, 58 | section_meters=None, 59 | stat='cantidad_etapas', 60 | factor=1, 61 | factor_min=50, 62 | save_gdf=False 63 | ) 64 | 65 | 66 | * ``id_linea``: id de línea o lista de ids de líneas a procesar 67 | * ``hour_range``: define el rango horario del origen de las etapas a utilizar 68 | * ``day_type``: fecha de los datos a procesar o tipo de día (``weekday`` o ``weekend``). 69 | * ``n_sections``: cantidad de segmentos a dividir el recorrido 70 | * ``section_meters``: cantidad de metros de largo del segmento. Si se especifica este parámetro no se considerará ``n_sections``. 71 | * ``stat``: indicador a utilizar ``proportion`` o ``'totals'``. 72 | * ``factor``: factor de escalado en metros que el tramo tendrá en la visualización en base al indicador utilizado. 73 | * ``factor_min``: ancho mínimo en metros que el tramo tendrá en la visualización. 74 | * ``save_gdf``: guardar los resultados de la visualización en un archivo geojson. 75 | 76 | 77 | 78 | **Matriz OD de la línea** 79 | 80 | De modo similar, Urbantrips puede calcular para cada línea una Matriz OD tomando esos segmentos como unidades espaciales de análisis. 81 | 82 | 83 | .. code:: python 84 | 85 | kpi.compute_lines_od_matrix( 86 | line_ids=[1], 87 | hour_range=[7,8], 88 | n_sections=10, 89 | section_meters=None, 90 | day_type='weekday, 91 | save_csv=True 92 | ) 93 | 94 | 95 | * ``line_ids``: id de línea o lista de ids de líneas a procesar 96 | * ``hour_range``: define el rango horario del origen de las etapas a utilizar 97 | * ``day_type``: fecha de los datos a procesar o tipo de día (``weekday`` o ``weekend``) o una fecha en particular. 98 | * ``n_sections``: cantidad de segmentos a dividir el recorrido 99 | * ``section_meters``: cantidad de metros de largo del segmento. Si se especifica este parámetro no se considerará ``n_sections``. 100 | * ``save_csv``: guardar los resultados de la matriz OD en un archivo csv. 101 | 102 | 103 | Este producto es una matriz de origen destino que se puede visualizar en un mapa o utilizar para otros análisis. 104 | 105 | .. code:: python 106 | 107 | viz.visualize_lines_od_matrix( 108 | line_ids=[1], 109 | hour_range=[7,8], 110 | day_type='weekday', 111 | n_sections=10, 112 | section_meters=None, 113 | stat='totals' 114 | ) 115 | 116 | * ``id_linea``: id de línea o lista de ids de líneas a procesar 117 | * ``hour_range``: define el rango horario del origen de las etapas a utilizar 118 | * ``day_type``: fecha de los datos a procesar o tipo de día (``weekday`` o ``weekend``). 119 | * ``n_sections``: cantidad de segmentos a dividir el recorrido 120 | * ``section_meters``: cantidad de metros de largo del segmento. Si se especifica este parámetro no se considerará ``n_sections``. 121 | * ``stat``: indicador a utilizar ``proportion`` o ``'totals'``. 122 | 123 | 124 | 125 | En base a GPS 126 | ------------- 127 | 128 | Cuando existe una tabla de GPS se pueden elaborar estadísicos más elaborados y precisos. En primer lugar se procura obtener un factor de ocupación comparando los Espacio Kilómetro Demandados (EKD) como proporción de los Espacio Kilómetro Ofertados (EKO). Para los primeros (EKD) se toma la cantidad de pasajeros transportados multiplicados por una DMT que puede ser utilizando la distancia media o la mediana, para evitar la influencia de medidas extremas que puedan incidir en el indicador. Para los segundos (EKO) se toma el total de kilómetros recorridos en base a la información disponible en la tabla GPS y se los multiplca por las 60 ubicaciones que se considera posee cada interno. 129 | 130 | En segundo lugar, se obtiene un Índice Pasajero Kilómetro (IPK) poniendo en relación el total de pasajeros transportados sobre el total de kilómetros recorridos por la línea. Para obtener estos indicadores principales se obtiene otros insumos que quedan en la base de datos, como el total de pasajeros, el total de kilómetros recorridos, la distancias medias y medianas viajadas, los vehículos totales, los pasajeros por vehículo por día, y los kilómetros por vehículo por día. Esto se calcula agregado para cada linea y día procesado, así como también para un día de la semana tipo o dia de fin de semana tipo. 131 | 132 | Los resultados quedan almacenados en la tabla ``kpi_by_day_line`` (para más detalles vea el apartado :doc:`resultados`). 133 | 134 | En base a servicios 135 | ------------------- 136 | 137 | UrbanTrips permite obtener datos a nivel de servicios para cada vehículo de cada línea (para más información pueden leer el aparatdo de :doc:`servicios`). Una vez que esta clafisicación de los datos de GPS en servicios ha tenido lugar, pueden obtenerse diferentes KPI al nivel de cada servicio identificado. Los resultados quedan almacenados en la tabla ``kpi_by_day_line_service`` (para más detalles vea el apartado :doc:`resultados`) 138 | 139 | 140 | 141 | 142 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | ## AM-331-A3 Licencia de Software 2 | Effective: April 2018 3 | Contenido de la página 4 | 5 | El Software, con excepción de los Documentos de Soporte y Uso, deberá estar sujetos a los siguientes términos y condiciones, los cuales se empaquetarán como parte de cada Software bajo un archivo que llevará el nombre de "LICENCIA" : 6 | "Copyright © [2022]. Banco Interamericano de Desarrollo ("BID"). Uso autorizado." 7 | 8 | ### 1. Licencia 9 | Por medio de la presente licencia ("Licencia") no exclusiva, revocable, global y libre de regalías el BID otorga permiso al usuario ("Usuario") para reproducir, distribuir, ejecutar públicamente, prestar y poner a disposición del público, y modificar el software y sus modificaciones, por sí o como parte de una colección, siempre que sea para fines no comerciales, sujeto a los términos y condiciones aquí señalados. 10 | Salvo indicación en contrario, la Documentación de Soporte y Uso del software se licenciará bajo Creative Commons IGO 3.0 Atribución-NoComercial-SinObraDerivada (CC-IGO 3.0 BY-NC-ND). 11 | 12 | #### 2. Definiciones 13 | 2.1 Software: conjunto de programas, instrucciones y reglas informáticas que permiten ejecutar tareas en una computadora a fin de gestionar un determinado proceso u obtener un determinado resultado. El Software incluye (i) código fuente, (ii) código objeto y (iii) Documentación de Soporte y Uso. 14 | El Software puede ser del tipo, pero sin estar limitado a (i) programa ejecutable desde el computador, (ii) aplicación móvil y de escritorio, (iii) algoritmo y (iv) hoja de cálculo que contienen macros, así como otras instrucciones para simular, proyectar o realizar otros cálculos. 15 | 2.2 Código Fuente: conjunto de líneas de texto con los pasos que debe seguir la computadora para ejecutar el Software. Puede estar compuesto por un número indeterminado de documentos, con distintas extensiones y organizados en carpetas. 16 | 2.3 Código Objeto: código que resulta de la compilación del Código Fuente. La compilación es un proceso informático que traduce el Código Fuente en lenguaje máquina o bytecode. 17 | 2.4 Documentación de Soporte y Uso (la "Documentación"): se refiere a la información de acompañamiento al código fuente y objeto que permite a un humano comprender los objetivos, arquitectura y lenguaje de programación de Software. 18 | 2.5 Software Derivado: obras derivadas del Software, tales como modificaciones, actualizaciones y mejoras de la misma. 19 | 2.6 Usuario: cualquier persona, natural o jurídica, que obtenga una copia del Software. 20 | 21 | ### 3. Derechos del BID 22 | 3.1 El BID se reserva todos los derechos de propiedad intelectual, incluyendo sin limitación derechos de autor, relacionados con o asociados al Software. 23 | 3.2 El BID se reserva expresamente los derechos de ceder, transferir y/o sub-licenciar el Software y/o cualquiera de sus componentes a terceros sin previo aviso. 24 | 25 | ### 4. Derechos del Usuario sobre Software Derivado 26 | El Usuario que desarrolle Software Derivado retendrá todos los derechos de propiedad intelectual, incluyendo sin limitación derechos de autor, relacionados con o asociados al Software Derivado, en el entendido que dicho Software Derivado y cualquier otra edición futura estén sujetas a los mismos términos y condiciones de esta Licencia. 27 | 28 | ### 5. Restricciones 29 | 5.1 El Usuario sólo está autorizado a hacer uso del Software conforme se describe en esta Licencia. 30 | 5.2 Con excepción de esta Licencia, el BID no otorga ninguna otra licencia al Usuario con respecto a cualquier otra información u obra propiedad del BID o de cualquier derecho de autor, marcas o cualquier otro derecho de propiedad intelectual del BID. Cualquier licencia adicional deberá ser por escrito y firmada por un representante del BID debidamente autorizado para tales fines. 31 | 5.3 La Licencia no constituye una venta del Software ni de cualquier parte o copia de la misma. 32 | 5.4 El Usuario no podrá usar ni permitir que otros usen el Software para fines comerciales. 33 | 5.5 El Usuario sólo podrá autorizar el uso del Software a terceros de conformidad con lo establecido en esta Licencia, sin que en ningún caso los terceros puedan adquirir más derechos sobre el Software que los expresamente otorgados por el BID mediante esta Licencia. 34 | 35 | ### 6. Reconocimiento 36 | 6.1 El Usuario deberá mantener los siguientes enunciados y exenciones en el archivo principal de la Documentación del Software: 37 | "Copyright © [año]. Banco Interamericano de Desarrollo ("BID"). Uso autorizado. 38 | Los procedimientos y resultados obtenidos en base a la ejecución de este software son los programados por los desarrolladores y no necesariamente reflejan el punto de vista del BID, de su Directorio Ejecutivo ni de los países que representa." 39 | 6.1.1 En el caso de Software del Fondo Multilateral de Inversiones ("FOMIN"), la exención de responsabilidad deberá ser: "Las opiniones expresadas en esta obra son de los autores y no necesariamente reflejan el punto de vista del BID, de su Directorio Ejecutivo ni de los países que representa, así como tampoco del Comité de Donantes del FOMIN ni de los países que representa." 40 | 6.2 El Usuario podrá incluir en el Software Derivado una referencia al Software como obra original, siempre y cuando dicho aviso no genere confusión alguna respecto a la titularidad de los derechos del BID sobre el Software. 41 | 42 | ### 7. Nombre y Logo del BID 43 | El Usuario no podrá hacer uso del nombre y/o logo del BID para fines diferentes a los aquí estipulados, ni podrá hacer promesas, ni adquirir compromisos u obligaciones, ni otorgar garantías de ninguna clase en nombre del BID. 44 | 45 | ### 8. Declaraciones y Garantías 46 | 8.1 El BID otorga esta Licencia sin garantía explícita o implícita de ningún género, objetiva o subjetiva, por responsabilidad contractual o extracontractual, lo que comprende, sin consistir en una enumeración taxativa, garantías de comerciabilidad, adecuación, cumplimiento de normas o requisitos o aplicabilidad para un fin determinado. 47 | 8.2 El BID no garantiza que el funcionamiento del Software será ininterrumpido o libre de errores y no asume obligación alguna de actualizar, corregir ni introducir mejoras en el Software. 48 | 8.3 El Usuario asume exclusivamente el riesgo por su utilización, y así deberá informarlo a terceros cuando utilice, distribuya o ponga el Software a disposición de terceros. 49 | 50 | ### 9. Limitación de Responsabilidad 51 | 9.1 El BID no será responsable, bajo circunstancia alguna, de daño ni indemnización, moral o patrimonial; directo o indirecto; accesorio o especial; o por vía de consecuencia, previsto o imprevisto, que pudiese surgir: 52 | 9.1.1 Bajo cualquier teoría de responsabilidad, ya sea por contrato, infracción de derechos de propiedad intelectual, negligencia o bajo cualquier otra teoría; y/o 53 | 9.1.2 A raíz del uso del Software, incluyendo, pero sin limitación de potenciales defectos en el Software, o la pérdida o inexactitud de los datos de cualquier tipo. Lo anterior incluye los gastos o daños asociados a fallas de comunicación y/o fallas de funcionamiento de computadoras, vinculados con la utilización del Software. 54 | 55 | ### 10. Indemnización 56 | 10.1 El Usuario se obliga a liberar, defender e indemnizar al BID, su personal y consultores, conforme sea aplicable, de cualquier reclamo, queja, acción, pérdida, demanda, responsabilidad, obligación, daño, costo, incluyendo sin limitación, honorarios de abogados, que pudiesen emprender contra el BID, su personal y/o consultores en virtud de: 57 | 10.1.1 El ejercicio de los derechos otorgados por el BID bajo esta Licencia. 58 | 10.1.2 Incumplimiento de los términos y condiciones de esta Licencia por parte del Usuario. 59 | 10.1.3 Infracción de derechos de autor de terceros por parte del Usuario con relación al uso del Software y el desarrollo de Software Derivado. 60 | 61 | ### 11. Terminación de la Licencia 62 | El BID podrá terminar de manera inmediata esta Licencia, con causa o sin causa, sin dar aviso previo al Usuario. La terminación con causa aplica en caso de que el BID determine, a su entera discreción, que el uso del Software es inconsistente con los términos y condiciones de esta Licencia. 63 | 64 | ### 12. Devolución del Sistema Informático 65 | En caso de terminación de esta Licencia, indistintamente de la causa de terminación y conforme decida el BID, el Usuario deberá inmediatamente cesar el uso del Software y deberá destruir y/o devolver al BID cualesquiera programas, códigos, accesos, archivos o información, impresa y digital, que sean necesarios para que el BID tenga control sobre el Software, sin incluir medida de protección tecnológica alguna. En caso de que el Usuario haya desarrollado Software Derivado y el BID termine la Licencia, el Usuario deberá obtener autorización del BID, por escrito, para poder seguir usando el Software. 66 | 67 | ### 13. Ley Aplicable 68 | Esta Licencia estará sujeta a y será interpretada de conformidad con las leyes del Distrito de Columbia de los Estados Unidos de América, con excepción de sus disposiciones respecto a conflicto de leyes. 69 | 70 | ### 14. Privilegios e Inmunidades 71 | Ninguna cláusula de esta Licencia podrá ser interpretada como un acto de renuncia por parte del BID o de sus funcionarios y empleados a los privilegios e inmunidades que le han sido concedidos como organización internacional pública por su Convenio Constitutivo, por el derecho internacional o por las leyes de cualquiera de sus países miembros. 72 | 73 | ### 15. Resolución de Disputas 74 | Las Partes se comprometen a resolver cualquier diferencia o disputa relacionada con esta Licencia de buena fe y mediante un arreglo amigable. Si al cabo de treinta (30) días calendario de la fecha en que una Parte le comunique a la otra Parte su disconformidad con una diferencia o disputa las Partes no han llegado a un acuerdo satisfactorio para ambas Partes, dicha diferencia o disputa será sometida a arbitraje de conformidad con las reglas de la American Arbitration Association. La determinación final le corresponderá a un único árbitro. El lugar del arbitraje será Washington, Distrito de Columbia, Estados Unidos de América. El idioma que se usará en el proceso de arbitraje será el inglés con traducción simultánea en cualquiera de los idiomas oficiales del BID, si el BID lo solicitara. Los gastos del arbitraje serán cubiertos por ambas Partes en igual proporción. 75 | 76 | ### 16. Relación entre las Partes 77 | Nada de lo contenido en esta Licencia se interpretará como el establecimiento o creación de una asociación ni relación empleador-empleado entre las Partes, las cuales en todo momento permanecerán como contratistas independientes. 78 | 79 | ### 17. Divisibilidad 80 | Si cualquier cláusula de esta Licencia es considerada inválida o inexigible, dicha invalidez o inexigibilidad no afectará a la validez ni la exigibilidad de las demás cláusulas, y dicha cláusula inválida o inexigible se considerará eliminada de esta Licencia. 81 | 82 | ### 18. Validez de Obligaciones 83 | En caso de terminación de esta Licencia sobrevivirán los derechos y las obligaciones previstas en las cláusulas Nos. 3, 7, 8, 9, 10, 13, 14, 15 y 19. 84 | 85 | ### 19. Notificaciones 86 | Cualquier notificación que se requiera en el marco de esta Licencia se hará por escrito al BID a la siguiente cuenta de correo electrónico: code@iadb.org. El BID podrá modificar esta cláusula sin dar previo aviso al Usuario. 87 | 88 | ### 20. Enmienda 89 | Esta Licencia sólo podrá ser modificada mediante autorización previa, expresa y por escrita del BID. 90 | 91 | ### 21. Acuerdo Final 92 | Esta Licencia constituye el acuerdo final entre las Partes y reemplaza todas las comunicaciones, entendimientos o acuerdos, escritos o verbales, de carácter previo entre las Partes con relación al objeto de esta Licencia. 93 | -------------------------------------------------------------------------------- /urbantrips/carto/stops.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pandas as pd 3 | import geopandas as gpd 4 | from shapely import line_interpolate_point 5 | import libpysal 6 | from urbantrips.geo import geo 7 | from urbantrips.utils.utils import duracion, iniciar_conexion_db, leer_configs_generales 8 | 9 | 10 | @duracion 11 | def create_stops_table(): 12 | """ 13 | Reads stops.csv file if present and uploads it to 14 | stops table in the db 15 | """ 16 | configs = leer_configs_generales(autogenerado=False) 17 | nombre_archivo_paradas = configs.get("nombre_archivo_paradas", None) 18 | 19 | if nombre_archivo_paradas is not None: 20 | stops_path = os.path.join("data", "data_ciudad", nombre_archivo_paradas) 21 | print("Leyendo stops", nombre_archivo_paradas) 22 | 23 | if os.path.isfile(stops_path): 24 | stops = pd.read_csv(stops_path) 25 | upload_stops_table(stops) 26 | else: 27 | print( 28 | "No existe un archivo de stops. Puede utilizar " 29 | "notebooks/stops_creation_with_node_id_helper.ipynb" 30 | "para crearlo a partir de los recorridos" 31 | ) 32 | 33 | # upload trave times between stations 34 | if configs["tiempos_viaje_estaciones"] is not None: 35 | upload_travel_times_stations() 36 | 37 | 38 | def upload_stops_table(stops): 39 | """ 40 | Reads a stops table, checks it and uploads it to db 41 | """ 42 | # Leer alias de insumos del config de usuario 43 | configs = leer_configs_generales(autogenerado=False) 44 | alias_db = configs.get("alias_db", "") 45 | conn_insumos = iniciar_conexion_db(tipo="insumos", alias_db=alias_db) 46 | 47 | cols = [ 48 | "id_linea", 49 | "id_ramal", 50 | "node_id", 51 | "branch_stop_order", 52 | "stop_x", 53 | "stop_y", 54 | "node_x", 55 | "node_y", 56 | ] 57 | stops = stops.reindex(columns=cols) 58 | assert not stops.isna().any().all(), "Hay datos faltantes en stops" 59 | 60 | print("Subiendo paradas a stops") 61 | stops.to_sql("stops", conn_insumos, if_exists="replace", index=False) 62 | conn_insumos.close() 63 | 64 | 65 | def create_temporary_stops_csv_with_node_id(geojson_path): 66 | """ 67 | Takes a geojson with a LineString for each line and or branch 68 | and creates a stops dataframe with x, y, branch_stop_order, and node_id 69 | every [stops_distance] meters. 70 | 71 | Parameters 72 | ---------- 73 | geojson_path : str 74 | Path to the geojson file containing the LineStrings for each line 75 | and/or branch, a `stops_distance` attribute with the distance 76 | in meters and a `line_stops_buffer` attribute for each line id 77 | with the distance in meters between stops to be aggregated in a single 78 | node 79 | 80 | Returns 81 | ------- 82 | pandas.DataFrame 83 | DataFrame containing stops information (x, y, branch_stop_order, 84 | and node_id) for each line and/or branch and saves it in data directory 85 | """ 86 | 87 | # create stops with order but no node_id 88 | stops_gdf = create_line_stops_equal_interval(geojson_path) 89 | 90 | # aggregate at node_id 91 | stops_df = aggregate_line_stops_to_node_id(stops_gdf) 92 | 93 | data_path = os.path.join("data", "data_ciudad") 94 | stops_df.to_csv(os.path.join(data_path, "temporary_stops.csv"), index=False) 95 | 96 | 97 | def create_line_stops_equal_interval(geojson_path): 98 | """ 99 | Takes a geojson with a LineString for each line and or branch 100 | and creates a stops dataframe with x, y, and branch_stop_order 101 | 102 | Parameters 103 | ---------- 104 | geojson_path : str 105 | Path to the geojson file containing the LineStrings for each line 106 | and/or branch and a `stops_distance` attribute with the distance 107 | in meters 108 | 109 | Returns 110 | ------- 111 | pandas.DataFrame 112 | DataFrame containing stops information (x, y, and branch_stop_order) 113 | for each line and/or branch 114 | """ 115 | # Read geojson 116 | geojson_data = gpd.read_file(geojson_path) 117 | 118 | # Check geometry type 119 | geo.check_all_geoms_linestring(geojson_data) 120 | 121 | # if there is no branch_id create 122 | if "id_ramal" not in geojson_data.columns: 123 | geojson_data["id_ramal"] = None 124 | 125 | # Project in meters 126 | epsg_m = geo.get_epsg_m() 127 | 128 | geojson_data = geojson_data.to_crs(epsg=epsg_m) 129 | stops_gdf = interpolate_stops_every_x_meters(geojson_data) 130 | 131 | stops_gdf = stops_gdf.reindex( 132 | columns=[ 133 | "id_linea", 134 | "id_ramal", 135 | "branch_stop_order", 136 | "line_stops_buffer", 137 | "x", 138 | "y", 139 | "geometry", 140 | ] 141 | ) 142 | 143 | stops_gdf = stops_gdf.to_crs(epsg=4326) 144 | return stops_gdf 145 | 146 | 147 | def interpolate_stops_every_x_meters(gdf): 148 | """ 149 | Takes a gdf in proyected crs in meters with linestrings and 150 | interpolates points every x meters returning a data frame with 151 | those points 152 | """ 153 | # Initialize list to store stops data 154 | stops_data = [] 155 | 156 | # Iterate over each LineString in the geojson data 157 | for i, row in gdf.iterrows(): 158 | 159 | # Create stops for the LineString 160 | stops_distance = row.stops_distance 161 | route_geom = row.geometry 162 | line_stops_buffer = row.line_stops_buffer 163 | 164 | line_stops_data = create_stops_from_route_geom( 165 | route_geom=route_geom, stops_distance=stops_distance 166 | ) 167 | 168 | # Add line_id to the stops data 169 | line_stops_data["id_linea"] = row.id_linea 170 | line_stops_data["id_ramal"] = row.id_ramal 171 | line_stops_data["line_stops_buffer"] = line_stops_buffer 172 | 173 | # Add the stops data to the overall stops data list 174 | stops_data.append(line_stops_data) 175 | 176 | # Concatenate the stops data for all lines and return as a DataFrame 177 | stops_gdf = pd.concat(stops_data, ignore_index=True) 178 | return stops_gdf 179 | 180 | 181 | def aggregate_line_stops_to_node_id(stops_gdf): 182 | """ 183 | Takes a geojson with stops for each line/branch and aggregates stops 184 | closed together using `line_stops_buffer` attribute 185 | 186 | Parameters 187 | ---------- 188 | geopandas.GeoDataFrame 189 | GeoDataFrame containing a branch_stop_order 190 | for each line and/or branch and `line_stops_buffer` attribute 191 | 192 | Returns 193 | ------- 194 | pandas.DataFrame 195 | DataFrame containing stops information (x, y, branch_stop_order 196 | and node_id) for each line and/or branch 197 | """ 198 | 199 | # Add node_id for each line 200 | stops_df = ( 201 | stops_gdf.groupby("id_linea", as_index=False) 202 | .apply(create_node_id) 203 | .reset_index(drop=True) 204 | ) 205 | 206 | return stops_df 207 | 208 | 209 | def create_stops_from_route_geom(route_geom, stops_distance): 210 | """ 211 | Takes a LineString projected in meters and interpolates stops 212 | every x meters 213 | 214 | Parameters 215 | ---------- 216 | route_geom : shapely.LineString 217 | LineString for which to create stops 218 | stops_distance : int 219 | Distance in meters between stops 220 | 221 | Returns 222 | ------- 223 | pandas.DataFrame 224 | DataFrame containing stops information (x, y, and branch_stop_order) 225 | for the given LineString 226 | """ 227 | epsg_m = geo.get_epsg_m() 228 | 229 | ranges = list(range(0, int(route_geom.length), stops_distance)) 230 | stop_points = line_interpolate_point(route_geom, ranges).tolist() 231 | 232 | stops_df = pd.DataFrame(range(len(stop_points)), columns=["branch_stop_order"]) 233 | stops_df = gpd.GeoDataFrame(stops_df, geometry=stop_points, crs=f"EPSG:{epsg_m}") 234 | 235 | geom_wgs84 = stops_df.geometry.to_crs(epsg=4326) 236 | stops_df["x"] = geom_wgs84.x 237 | stops_df["y"] = geom_wgs84.y 238 | 239 | return stops_df 240 | 241 | 242 | def create_node_id(line_stops_gdf): 243 | """ 244 | Adds a node_id column to the given DataFrame based on the x, y, 245 | and order columns using fuzzy contiguity. 246 | 247 | Parameters 248 | ---------- 249 | df : pandas.DataFrame 250 | line stops DataFrame to add node_id column. 251 | 252 | Returns 253 | ------- 254 | pandas.DataFrame 255 | The DataFrame with an additional node_id column 256 | """ 257 | buffer = line_stops_gdf.line_stops_buffer.unique()[0] 258 | epsg_m = geo.get_epsg_m() 259 | 260 | line_stops_gdf = line_stops_gdf.to_crs(epsg=epsg_m) 261 | 262 | gdf = line_stops_gdf.copy() 263 | connectivity = libpysal.weights.fuzzy_contiguity( 264 | gdf=gdf, buffering=True, drop=False, buffer=buffer, predicate="intersects" 265 | ) 266 | 267 | gdf.loc[:, "node_id"] = connectivity.component_labels 268 | gdf = gdf.to_crs(epsg=4326) 269 | 270 | # geocode new position based on new node_id 271 | gdf.loc[:, ["stop_x"]] = gdf.geometry.x 272 | gdf.loc[:, ["stop_y"]] = gdf.geometry.y 273 | 274 | x_new_long = gdf.groupby("node_id").apply(lambda df: df.stop_x.mean()).to_dict() 275 | y_new_long = gdf.groupby("node_id").apply(lambda df: df.stop_y.mean()).to_dict() 276 | 277 | gdf.loc[:, "node_y"] = gdf["node_id"].replace(y_new_long) 278 | gdf.loc[:, "node_x"] = gdf["node_id"].replace(x_new_long) 279 | 280 | cols = [ 281 | "id_linea", 282 | "id_ramal", 283 | "node_id", 284 | "branch_stop_order", 285 | "stop_x", 286 | "stop_y", 287 | "node_x", 288 | "node_y", 289 | ] 290 | gdf = gdf.reindex(columns=cols) 291 | 292 | return gdf 293 | 294 | 295 | def upload_travel_times_stations(): 296 | """ 297 | This function loads a table holding travel time in minutes 298 | between stations for modes that don't have GPS in the vehicles 299 | """ 300 | # Leer alias de insumos del config de usuario 301 | configs = leer_configs_generales(autogenerado=False) 302 | alias_db = configs.get("alias_db", "") 303 | conn_insumos = iniciar_conexion_db(tipo="insumos", alias_db=alias_db) 304 | 305 | tts_file_name = configs["tiempos_viaje_estaciones"] 306 | path = os.path.join("data", "data_ciudad", tts_file_name) 307 | print("Leyendo tabla de tiempos de viaje entre estaciones", tts_file_name) 308 | 309 | if os.path.isfile(path): 310 | travel_times_stations = pd.read_csv(path) 311 | cols = [ 312 | "id_o", 313 | "id_linea_o", 314 | "id_ramal_o", 315 | "lat_o", 316 | "lon_o", 317 | "id_d", 318 | "lat_d", 319 | "lon_d", 320 | "id_linea_d", 321 | "id_ramal_d", 322 | "travel_time_min", 323 | ] 324 | 325 | travel_times_stations = travel_times_stations.reindex(columns=cols) 326 | 327 | assert ( 328 | not travel_times_stations.isna().any().all() 329 | ), "Hay datos faltantes en la tabla" 330 | 331 | print("Subiendo tabla de tiempos de viaje entre estaciones a la DB") 332 | travel_times_stations.to_sql( 333 | "travel_times_stations", conn_insumos, if_exists="replace", index=False 334 | ) 335 | conn_insumos.close() 336 | 337 | else: 338 | print(f"No existe el archivo {tts_file_name}") 339 | -------------------------------------------------------------------------------- /urbantrips/viz/overlapping.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import geopandas as gpd 3 | import folium 4 | import pandas as pd 5 | from urbantrips.geo import geo 6 | from urbantrips.kpi import overlapping as ovl 7 | from urbantrips.utils.utils import leer_configs_generales, iniciar_conexion_db 8 | from shapely import wkt 9 | import h3 10 | 11 | 12 | def get_route_metadata(route_id): 13 | configs = leer_configs_generales() 14 | conn_insumos = iniciar_conexion_db(tipo="insumos") 15 | use_branches = configs["lineas_contienen_ramales"] 16 | if use_branches: 17 | metadata = pd.read_sql( 18 | f"select nombre_ramal from metadata_ramales where id_ramal == {route_id}", 19 | conn_insumos, 20 | ) 21 | metadata = metadata.nombre_ramal.iloc[0] 22 | else: 23 | metadata = pd.read_sql( 24 | f"select nombre_linea from metadata_lineas where id_linea == {route_id}", 25 | conn_insumos, 26 | ) 27 | metadata = metadata.nombre_linea.iloc[0] 28 | return metadata 29 | 30 | 31 | def plot_interactive_supply_overlapping(overlapping_dict): 32 | 33 | base_h3 = overlapping_dict["base"]["h3"] 34 | comp_h3 = overlapping_dict["comp"]["h3"] 35 | if (base_h3 is None) or (comp_h3 is None): 36 | return None 37 | 38 | base_route_id = base_h3.route_id.unique()[0] 39 | comp_route_id = comp_h3.route_id.unique()[0] 40 | base_route_metadata = get_route_metadata(base_route_id) 41 | comp_route_metadata = get_route_metadata(comp_route_id) 42 | 43 | # extract data from overlapping dict 44 | base_gdf = overlapping_dict["base"]["h3"] 45 | base_route_gdf = overlapping_dict["base"]["line"] 46 | comp_gdf = overlapping_dict["comp"]["h3"] 47 | comp_route_gdf = overlapping_dict["comp"]["line"] 48 | 49 | # get mean coords to center map 50 | mean_x = np.mean(base_route_gdf.item().coords.xy[0]) 51 | mean_y = np.mean(base_route_gdf.item().coords.xy[1]) 52 | 53 | fig = folium.Figure(width=800, height=600) 54 | m = folium.Map(location=(mean_y, mean_x), zoom_start=11, tiles="cartodbpositron") 55 | 56 | base_gdf.explore( 57 | color="black", 58 | tiles="CartoDB positron", 59 | m=m, 60 | name=f"Base H3 {base_route_metadata}", 61 | ) 62 | base_route_gdf.explore( 63 | color="black", 64 | tiles="CartoDB positron", 65 | m=m, 66 | name=f"Base route {base_route_metadata}", 67 | ) 68 | 69 | comp_gdf.explore( 70 | color="red", 71 | tiles="CartoDB positron", 72 | m=m, 73 | name=f"Comp H3 {comp_route_metadata}", 74 | ) 75 | comp_route_gdf.explore( 76 | color="red", 77 | tiles="CartoDB positron", 78 | m=m, 79 | name=f"Comp route {comp_route_metadata}", 80 | ) 81 | 82 | folium.LayerControl(name="Legs").add_to(m) 83 | 84 | fig.add_child(m) 85 | return fig 86 | 87 | 88 | def plot_interactive_demand_overlapping(base_demand, comp_demand, overlapping_dict): 89 | base_gdf = overlapping_dict["base"]["h3"] 90 | base_route_gdf = overlapping_dict["base"]["line"] 91 | comp_gdf = overlapping_dict["comp"]["h3"] 92 | comp_route_gdf = overlapping_dict["comp"]["line"] 93 | 94 | base_route_id = base_gdf.route_id.unique()[0] 95 | comp_route_id = comp_gdf.route_id.unique()[0] 96 | 97 | base_route_metadata = get_route_metadata(base_route_id) 98 | comp_route_metadata = get_route_metadata(comp_route_id) 99 | 100 | # Points for O and D 101 | base_origins = ( 102 | base_demand.reindex(columns=["h3_o", "factor_expansion_linea"]) 103 | .groupby("h3_o", as_index=False) 104 | .agg(total_legs=("factor_expansion_linea", "sum")) 105 | ) 106 | 107 | base_destinations = ( 108 | base_demand.reindex(columns=["h3_d", "factor_expansion_linea"]) 109 | .groupby("h3_d", as_index=False) 110 | .agg(total_legs=("factor_expansion_linea", "sum")) 111 | ) 112 | 113 | base_origins = gpd.GeoDataFrame( 114 | base_origins, geometry=base_origins.h3_o.map(geo.create_point_from_h3), crs=4326 115 | ) 116 | base_destinations = gpd.GeoDataFrame( 117 | base_destinations, 118 | geometry=base_destinations.h3_d.map(geo.create_point_from_h3), 119 | crs=4326, 120 | ) 121 | base_origins["total_legs"] = base_origins["total_legs"].astype(int) 122 | base_destinations["total_legs"] = base_destinations["total_legs"].astype(int) 123 | 124 | comp_origins = ( 125 | comp_demand.reindex(columns=["h3_o", "factor_expansion_linea"]) 126 | .groupby("h3_o", as_index=False) 127 | .agg(total_legs=("factor_expansion_linea", "sum")) 128 | ) 129 | comp_destinations = ( 130 | comp_demand.reindex(columns=["h3_d", "factor_expansion_linea"]) 131 | .groupby("h3_d", as_index=False) 132 | .agg(total_legs=("factor_expansion_linea", "sum")) 133 | ) 134 | comp_origins = gpd.GeoDataFrame( 135 | comp_origins, geometry=comp_origins.h3_o.map(geo.create_point_from_h3), crs=4326 136 | ) 137 | comp_destinations = gpd.GeoDataFrame( 138 | comp_destinations, 139 | geometry=comp_destinations.h3_d.map(geo.create_point_from_h3), 140 | crs=4326, 141 | ) 142 | 143 | comp_origins["total_legs"] = comp_origins["total_legs"].astype(int) 144 | comp_destinations["total_legs"] = comp_destinations["total_legs"].astype(int) 145 | 146 | # compute demand by section id 147 | base_demand_by_section = ovl.demand_by_section_id(base_demand) 148 | comp_demand_by_section = ovl.demand_by_section_id(comp_demand) 149 | 150 | # plot 151 | base_gdf = base_gdf.merge(base_demand_by_section, on="section_id", how="left") 152 | base_gdf.total_legs = base_gdf.total_legs.fillna(0) 153 | base_gdf.prop_demand = base_gdf.prop_demand.fillna(0) 154 | 155 | comp_gdf = comp_gdf.merge(comp_demand_by_section, on="section_id", how="left") 156 | comp_gdf.total_legs = comp_gdf.total_legs.fillna(0) 157 | comp_gdf.prop_demand = comp_gdf.prop_demand.fillna(0) 158 | 159 | min_dot_size = 1 160 | max_dot_size = 20 161 | 162 | base_destinations["total_legs_normalized"] = ovl.normalize_total_legs_to_dot_size( 163 | base_destinations["total_legs"], min_dot_size, max_dot_size 164 | ) 165 | comp_destinations["total_legs_normalized"] = ovl.normalize_total_legs_to_dot_size( 166 | comp_destinations["total_legs"], min_dot_size, max_dot_size 167 | ) 168 | base_origins["total_legs_normalized"] = ovl.normalize_total_legs_to_dot_size( 169 | base_origins["total_legs"], min_dot_size, max_dot_size 170 | ) 171 | comp_origins["total_legs_normalized"] = ovl.normalize_total_legs_to_dot_size( 172 | comp_origins["total_legs"], min_dot_size, max_dot_size 173 | ) 174 | base_gdf["demand_total"] = base_gdf["total_legs"].astype(int).copy() 175 | base_gdf["demand_prop"] = base_gdf["prop_demand"].round(1).copy() 176 | comp_gdf["demand_total"] = comp_gdf["total_legs"].astype(int).copy() 177 | comp_gdf["demand_prop"] = comp_gdf["prop_demand"].round(1).copy() 178 | base_gdf = base_gdf.drop(columns=["total_legs", "prop_demand"]) 179 | comp_gdf = comp_gdf.drop(columns=["total_legs", "prop_demand"]) 180 | 181 | # export data 182 | base_gdf_to_db = base_gdf.copy() 183 | 184 | base_gdf_to_db["h3_res"] = h3.get_resolution(base_gdf_to_db["h3"].iloc[0]) 185 | base_gdf_to_db["x"] = base_gdf_to_db.geometry.centroid.x 186 | base_gdf_to_db["y"] = base_gdf_to_db.geometry.centroid.y 187 | base_gdf_to_db["type_route"] = "base" 188 | base_gdf_to_db["wkt"] = base_gdf_to_db["geometry"].apply(lambda geom: geom.wkt) 189 | base_gdf_to_db = base_gdf_to_db.reindex( 190 | columns=[ 191 | "route_id", 192 | "type_route", 193 | "h3", 194 | "h3_res", 195 | "wkt", 196 | "x", 197 | "y", 198 | "demand_total", 199 | "demand_prop", 200 | ] 201 | ) 202 | base_gdf_to_db = base_gdf_to_db.merge( 203 | base_origins.reindex(columns=["h3_o", "total_legs"]), 204 | left_on="h3", 205 | right_on="h3_o", 206 | how="left", 207 | ) 208 | base_gdf_to_db = base_gdf_to_db.rename(columns={"total_legs": "origins"}).drop( 209 | columns=["h3_o"] 210 | ) 211 | base_gdf_to_db = base_gdf_to_db.merge( 212 | base_destinations.reindex(columns=["h3_d", "total_legs"]), 213 | left_on="h3", 214 | right_on="h3_d", 215 | how="left", 216 | ) 217 | base_gdf_to_db = base_gdf_to_db.rename(columns={"total_legs": "destinations"}).drop( 218 | columns=["h3_d"] 219 | ) 220 | 221 | comp_gdf_to_db = comp_gdf.copy() 222 | comp_gdf_to_db["h3_res"] = h3.get_resolution(comp_gdf_to_db["h3"].iloc[0]) 223 | comp_gdf_to_db["x"] = comp_gdf_to_db.geometry.centroid.x 224 | comp_gdf_to_db["y"] = comp_gdf_to_db.geometry.centroid.y 225 | comp_gdf_to_db["type_route"] = "comp" 226 | comp_gdf_to_db["wkt"] = comp_gdf_to_db["geometry"].apply(lambda geom: geom.wkt) 227 | comp_gdf_to_db = comp_gdf_to_db.reindex( 228 | columns=[ 229 | "route_id", 230 | "type_route", 231 | "h3", 232 | "h3_res", 233 | "wkt", 234 | "x", 235 | "y", 236 | "demand_total", 237 | "demand_prop", 238 | ] 239 | ) 240 | comp_gdf_to_db = comp_gdf_to_db.merge( 241 | comp_origins.reindex(columns=["h3_o", "total_legs"]), 242 | left_on="h3", 243 | right_on="h3_o", 244 | how="left", 245 | ) 246 | comp_gdf_to_db = comp_gdf_to_db.rename(columns={"total_legs": "origins"}).drop( 247 | columns=["h3_o"] 248 | ) 249 | comp_gdf_to_db = comp_gdf_to_db.merge( 250 | comp_destinations.reindex(columns=["h3_d", "total_legs"]), 251 | left_on="h3", 252 | right_on="h3_d", 253 | how="left", 254 | ) 255 | comp_gdf_to_db = comp_gdf_to_db.rename(columns={"total_legs": "destinations"}).drop( 256 | columns=["h3_d"] 257 | ) 258 | 259 | # get mean coords to center map 260 | mean_x = np.mean(base_route_gdf.item().coords.xy[0]) 261 | mean_y = np.mean(base_route_gdf.item().coords.xy[1]) 262 | 263 | fig = folium.Figure(width=800, height=600) 264 | m = folium.Map(location=(mean_y, mean_x), zoom_start=11, tiles="cartodbpositron") 265 | 266 | base_gdf.explore( 267 | column="demand_total", 268 | tiles="CartoDB positron", 269 | m=m, 270 | name=f"Demanda ruta base - {base_route_metadata}", 271 | cmap="Blues", 272 | scheme="equalinterval", 273 | ) 274 | base_destinations.explore( 275 | color="midnightblue", 276 | style_kwds={ 277 | "style_function": lambda x: { 278 | "radius": x["properties"]["total_legs_normalized"] 279 | } 280 | }, 281 | name=f"Destinos ruta base - {base_route_metadata}", 282 | m=m, 283 | ) 284 | base_origins.explore( 285 | color="cornflowerblue", 286 | style_kwds={ 287 | "style_function": lambda x: { 288 | "radius": x["properties"]["total_legs_normalized"] 289 | } 290 | }, 291 | name=f"Origenes ruta base - {base_route_metadata}", 292 | m=m, 293 | ) 294 | base_route_gdf.explore( 295 | color="midnightblue", 296 | tiles="CartoDB positron", 297 | m=m, 298 | name=f"Ruta base - {comp_route_metadata}", 299 | ) 300 | 301 | comp_gdf.explore( 302 | column="demand_total", 303 | tiles="CartoDB positron", 304 | m=m, 305 | name=f"Demanda ruta comp - {comp_route_metadata}", 306 | cmap="Greens", 307 | scheme="equalinterval", 308 | ) 309 | comp_destinations.explore( 310 | color="darkgreen", 311 | style_kwds={ 312 | "style_function": lambda x: { 313 | "radius": x["properties"]["total_legs_normalized"] 314 | } 315 | }, 316 | name=f"Destinos ruta comp - {comp_route_metadata}", 317 | m=m, 318 | ) 319 | comp_origins.explore( 320 | color="limegreen", 321 | style_kwds={ 322 | "style_function": lambda x: { 323 | "radius": x["properties"]["total_legs_normalized"] 324 | } 325 | }, 326 | name=f"Origenes ruta comp - {comp_route_metadata}", 327 | m=m, 328 | ) 329 | comp_route_gdf.explore( 330 | color="darkgreen", 331 | tiles="CartoDB positron", 332 | m=m, 333 | name=f"Ruta comparacion - {comp_route_metadata}", 334 | ) 335 | 336 | folium.LayerControl(name="Leyenda").add_to(m) 337 | 338 | fig.add_child(m) 339 | return { 340 | "fig": fig, 341 | "base_gdf_to_db": base_gdf_to_db, 342 | "comp_gdf_to_db": comp_gdf_to_db, 343 | } 344 | -------------------------------------------------------------------------------- /urbantrips/dashboard/pages/9_Clusterización.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | import streamlit as st 3 | from pathlib import Path 4 | from dash_utils import ( 5 | levanto_tabla_sql, 6 | guardar_tabla_sql, 7 | get_logo, 8 | configurar_selector_dia, 9 | ) 10 | 11 | def normalizar_id_linea(col): 12 | def convertir(x): 13 | if pd.isna(x): 14 | return None 15 | s = str(x).strip() 16 | # si es número entero o decimal → convertir a int y luego a str 17 | if s.replace('.', '', 1).isdigit(): 18 | return str(int(float(s))) 19 | # si no es número → dejarlo como está 20 | return s 21 | return col.apply(convertir) 22 | 23 | st.set_page_config(page_title="Indicadores Operativos por Línea", layout="wide") 24 | 25 | # Cabecera estándar 26 | logo = get_logo() 27 | st.image(logo) 28 | alias_seleccionado = configurar_selector_dia() 29 | 30 | # Cargar KPIs 31 | kpis = levanto_tabla_sql("kpis_lineas", "general") 32 | kpis_merge = levanto_tabla_sql("kpis_lineas_merge", "general") 33 | 34 | 35 | # Inicializar sesión 36 | if "tabla_mergeada" not in st.session_state: 37 | st.session_state["tabla_mergeada"] = None 38 | st.session_state["archivo_anterior"] = None 39 | if len(kpis_merge) > 0: 40 | st.session_state["tabla_mergeada"] = kpis_merge.copy() 41 | 42 | # 1. Mostrar tabla original 43 | with st.expander("📄 Ver tabla original de KPIs", expanded=True): 44 | st.dataframe(kpis) 45 | 46 | # 2. Merge con tabla externa 47 | with st.expander("🔗 Subir tabla externa y hacer merge"): 48 | archivo = st.file_uploader("Elegí un archivo CSV o Excel", type=["csv", "xlsx"]) 49 | 50 | if archivo is not None: 51 | try: 52 | if archivo.name.endswith(".csv"): 53 | tabla_externa = pd.read_csv(archivo) 54 | else: 55 | tabla_externa = pd.read_excel(archivo) 56 | 57 | if 'id_linea' in tabla_externa.columns: 58 | tabla_externa['id_linea'] = normalizar_id_linea(tabla_externa['id_linea']) 59 | 60 | tabla_externa['id_linea'] = tabla_externa['id_linea'].astype(str) 61 | 62 | st.success("Archivo cargado correctamente.") 63 | st.dataframe(tabla_externa) 64 | 65 | # Selección de claves de merge 66 | opcion_merge = st.radio( 67 | "Seleccioná la clave de combinación", 68 | options=["['dia', 'id_linea']", "['mes', 'id_linea']"], 69 | ) 70 | merge_keys = eval(opcion_merge) 71 | 72 | # Verificación de columnas 73 | cols_faltantes_ext = [ 74 | col for col in merge_keys if col not in tabla_externa.columns 75 | ] 76 | cols_faltantes_kpis = [col for col in merge_keys if col not in kpis.columns] 77 | 78 | if cols_faltantes_ext: 79 | st.error( 80 | f"Faltan estas columnas en la tabla externa: {cols_faltantes_ext}" 81 | ) 82 | elif cols_faltantes_kpis: 83 | st.error( 84 | f"Faltan estas columnas en la tabla KPIs: {cols_faltantes_kpis}" 85 | ) 86 | else: 87 | mismo_archivo = st.session_state["archivo_anterior"] == archivo.name 88 | merge_previo = ( 89 | st.session_state["tabla_mergeada"] is not None and mismo_archivo 90 | ) 91 | 92 | if merge_previo: 93 | st.warning("Ya existe un merge con este archivo.") 94 | if st.button("Reemplazar merge anterior"): 95 | st.session_state["tabla_mergeada"] = pd.merge( 96 | kpis, tabla_externa, on=merge_keys, how="left" 97 | ) 98 | st.session_state["archivo_anterior"] = archivo.name 99 | guardar_tabla_sql( 100 | st.session_state["tabla_mergeada"], 101 | "kpis_lineas_merge", 102 | "general", 103 | modo="replace", 104 | ) 105 | st.success("Merge reemplazado con éxito.") 106 | st.info("✅ Tabla guardada en `kpis_lineas`.") 107 | st.dataframe(st.session_state["tabla_mergeada"]) 108 | else: 109 | if st.button("Hacer merge"): 110 | st.session_state["tabla_mergeada"] = pd.merge( 111 | kpis, tabla_externa, on=merge_keys, how="left" 112 | ) 113 | st.session_state["archivo_anterior"] = archivo.name 114 | guardar_tabla_sql( 115 | st.session_state["tabla_mergeada"], 116 | "kpis_lineas_merge", 117 | "general", 118 | modo="replace", 119 | ) 120 | st.success("Merge realizado con éxito.") 121 | st.info("✅ Tabla guardada en `kpis_lineas`.") 122 | st.dataframe(st.session_state["tabla_mergeada"]) 123 | 124 | except Exception as e: 125 | st.error(f"Error al procesar el archivo: {e}") 126 | 127 | 128 | # ---------- Cargar KPIs y base ---------- 129 | if "kpis" not in st.session_state: 130 | st.session_state["kpis"] = levanto_tabla_sql("kpis_lineas", "general") 131 | 132 | # Inicializar sesión 133 | if "tabla_mergeada" not in st.session_state: 134 | st.session_state["tabla_mergeada"] = levanto_tabla_sql( 135 | "kpis_lineas_merge", "general" 136 | ) 137 | 138 | 139 | if ( 140 | "tabla_mergeada" in st.session_state 141 | and st.session_state["tabla_mergeada"] is not None 142 | ): 143 | df_base = st.session_state["tabla_mergeada"] 144 | else: 145 | df_base = st.session_state["kpis"] 146 | 147 | 148 | def save_escenarios(lista: list[dict]): 149 | df_guardar = pd.DataFrame(lista) 150 | if df_guardar.empty: 151 | st.warning("No hay escenarios para guardar.") 152 | return 153 | columnas = [ 154 | "nombre", 155 | "variables", 156 | "cant_clusters", 157 | "max_clusters_clase", 158 | "cant_clusters_recluster", 159 | ] 160 | df_guardar = df_guardar[columnas] 161 | guardar_tabla_sql( 162 | df_guardar, "escenarios_clusterizacion", "insumos", modo="replace" 163 | ) 164 | 165 | 166 | # UI 167 | with st.expander("🗂️ Escenarios de clusterización", expanded=True): 168 | 169 | columnas_numericas = df_base.select_dtypes( 170 | include=["int64", "float64"] 171 | ).columns.tolist() 172 | 173 | # -------- crear nuevo escenario -------- 174 | with st.form("formulario_escenario"): 175 | st.markdown("### Crear nuevo escenario") 176 | 177 | nombre_escenario = st.text_input("Nombre del escenario") 178 | variables_seleccionadas = st.multiselect( 179 | "Seleccioná variables", options=columnas_numericas 180 | ) 181 | 182 | col1, col2, col3 = st.columns(3) 183 | with col1: 184 | cant_clusters = st.number_input( 185 | "Cantidad de clusters", min_value=1, step=1, value=6 186 | ) 187 | with col2: 188 | max_clusters_clase = st.number_input( 189 | "Máx. elementos por clase", min_value=1, step=1, value=80 190 | ) 191 | with col3: 192 | cant_clusters_recluster = st.number_input( 193 | "Clusters (reclasterización)", min_value=1, step=1, value=3 194 | ) 195 | 196 | submit = st.form_submit_button("Guardar escenario") 197 | 198 | if submit: 199 | if not nombre_escenario: 200 | st.warning("Debés ingresar un nombre.") 201 | elif not variables_seleccionadas: 202 | st.warning("Debés seleccionar al menos una variable.") 203 | else: 204 | # Leer los existentes desde la base para chequear duplicados 205 | existentes = levanto_tabla_sql("escenarios_clusterizacion", "insumos") 206 | if ( 207 | not existentes.empty 208 | and nombre_escenario in existentes["nombre"].astype(str).tolist() 209 | ): 210 | st.error("Ya existe un escenario con ese nombre.") 211 | else: 212 | nuevo = { 213 | "nombre": nombre_escenario, 214 | "variables": ", ".join(variables_seleccionadas), 215 | "cant_clusters": int(cant_clusters), 216 | "max_clusters_clase": int(max_clusters_clase), 217 | "cant_clusters_recluster": int(cant_clusters_recluster), 218 | } 219 | lista_actualizada = ( 220 | existentes.to_dict(orient="records") if not existentes.empty else [] 221 | ) 222 | lista_actualizada.append(nuevo) 223 | save_escenarios(lista_actualizada) 224 | st.cache_data.clear() 225 | st.success(f"Escenario '{nombre_escenario}' guardado.") 226 | st.rerun() 227 | 228 | # -------- mostrar y eliminar escenarios -------- 229 | df_escenarios = levanto_tabla_sql("escenarios_clusterizacion", "insumos") 230 | if not df_escenarios.empty: 231 | df_escenarios["variables"] = df_escenarios["variables"].astype(str) 232 | for col, default in [ 233 | ("cant_clusters", 6), 234 | ("max_clusters_clase", 80), 235 | ("cant_clusters_recluster", 3), 236 | ]: 237 | if col not in df_escenarios.columns: 238 | df_escenarios[col] = default 239 | 240 | st.markdown("### Escenarios guardados") 241 | st.dataframe(df_escenarios) 242 | 243 | a_borrar = st.selectbox( 244 | "Seleccioná un escenario para eliminar", 245 | options=df_escenarios["nombre"].astype(str).tolist(), 246 | key="select_borrar", 247 | ) 248 | 249 | if st.button("Eliminar escenario seleccionado"): 250 | df_filtrado = df_escenarios[df_escenarios["nombre"].astype(str) != a_borrar] 251 | guardar_tabla_sql( 252 | df_filtrado, "escenarios_clusterizacion", "insumos", modo="replace" 253 | ) 254 | st.cache_data.clear() 255 | st.success(f"Escenario '{a_borrar}' eliminado.") 256 | st.rerun() 257 | else: 258 | st.info("Aún no hay escenarios guardados.") 259 | 260 | 261 | # ------------------------------------------------------------------- 262 | # EXPANDER: Clusterizar 263 | # ------------------------------------------------------------------- 264 | 265 | from clusters_utils import correr_clusters 266 | 267 | with st.expander("🧩 Clusterizar", expanded=False): 268 | 269 | # 1) ¿Hay datos en kpis_lineas? 270 | df_kpis = st.session_state["kpis"] # ya lo cargaste al inicio 271 | kpis_ok = not df_kpis.empty 272 | 273 | # 2) ¿Hay al menos un escenario? 274 | df_escenarios = levanto_tabla_sql("escenarios_clusterizacion", "insumos") 275 | escenarios_ok = not df_escenarios.empty 276 | 277 | # Mensajes de estado 278 | if not kpis_ok: 279 | st.error("La tabla **kpis_lineas** está vacía. Primero cargá datos.") 280 | if not escenarios_ok: 281 | st.error("No existe ningún escenario guardado. Creá al menos uno.") 282 | 283 | # Botón habilitado sólo si ambos requisitos se cumplen 284 | days = sorted(df_kpis["dia"].dropna().unique()) 285 | days = [x for x in days if x not in ["Promedios"]] 286 | day_sel = st.selectbox("Día", ["Promedios"] + days, index=0) 287 | cluster_btn = st.button( 288 | "🚀 Clusterizar escenarios", disabled=not (kpis_ok and escenarios_ok) 289 | ) 290 | 291 | if cluster_btn: 292 | # ------------------------------------------------------------------ 293 | # Llamá aquí tu función real de clustering 294 | # ------------------------------------------------------------------ 295 | st.info("Ejecutando clustering. Esto puede tardar unos segundos…") 296 | try: 297 | df_kpis = df_kpis[df_kpis.dia == day_sel] 298 | resultados = correr_clusters(df_kpis) # <- tu función 299 | st.success("¡Clustering completado!") 300 | 301 | st.markdown("### 📁 Carpeta de resultados:") 302 | resultados_dir = Path.cwd() / "data" / "clusters" / "resultados" 303 | st.markdown("---") 304 | 305 | # Enlace clicable (funciona en navegadores locales) 306 | if resultados_dir.exists(): 307 | st.code(str(resultados_dir), language="bash") 308 | st.markdown( 309 | f"[📂 Abrir carpeta de resultados]({resultados_dir.as_uri()})", 310 | unsafe_allow_html=True, 311 | ) 312 | 313 | st.dataframe(resultados) 314 | except Exception as e: 315 | st.error(f"Error al clusterizar: {e}") 316 | -------------------------------------------------------------------------------- /urbantrips/kpi/kpi_lineas.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pandas as pd 3 | import numpy as np 4 | from pathlib import Path 5 | from urbantrips.utils import utils 6 | from urbantrips.utils.utils import levanto_tabla_sql, guardar_tabla_sql, duracion 7 | from datetime import datetime 8 | 9 | 10 | def cal_velocidad_comercial(servicios): 11 | # Conversión de columnas a datetime 12 | servicios["min_datetime"] = pd.to_datetime(servicios["min_datetime"]) 13 | servicios["max_datetime"] = pd.to_datetime(servicios["max_datetime"]) 14 | 15 | # Cálculo de duración del servicio en minutos 16 | servicios["diff_minutes"] = ( 17 | servicios["max_datetime"] - servicios["min_datetime"] 18 | ).dt.total_seconds() / 60 19 | 20 | # Cálculo de velocidad comercial 21 | servicios["velocidad_comercial"] = servicios["distance_km"] / ( 22 | servicios["diff_minutes"] / 60 23 | ) 24 | 25 | # Extraer hora de finalización del servicio 26 | servicios["hour"] = servicios["max_datetime"].dt.hour 27 | 28 | # Velocidad comercial por línea y ramal en hora pico AM 29 | filtro_pico_am = (servicios["diff_minutes"] < 180) & ( 30 | servicios["hour"].between(6, 10) 31 | ) 32 | vel_comercial_linea_ramal_pico = ( 33 | servicios[filtro_pico_am] 34 | .groupby(["dia", "id_linea", "id_ramal"], as_index=False)["velocidad_comercial"] 35 | .mean() 36 | .round(1) 37 | ) 38 | 39 | # Distancia media recorrida por vehículo en ramal 40 | km_recorridos_ramal = ( 41 | servicios.groupby(["dia", "id_linea", "id_ramal", "interno"], as_index=False)[ 42 | "distance_km" 43 | ] 44 | .sum() 45 | .groupby(["dia", "id_linea", "id_ramal"], as_index=False)["distance_km"] 46 | .mean() 47 | .rename(columns={"distance_km": "distancia_media_veh"}) 48 | .round(1) 49 | ) 50 | 51 | vel_comercial_linea_ramal_pico = vel_comercial_linea_ramal_pico.merge( 52 | km_recorridos_ramal, how="left" 53 | ) 54 | 55 | # Velocidad comercial total por línea (todo el día) 56 | vel_comercial_linea_all = ( 57 | servicios.groupby(["dia", "id_linea"], as_index=False)["velocidad_comercial"] 58 | .mean() 59 | .round(1) 60 | ) 61 | 62 | # Velocidad comercial AM 63 | vel_comercial_linea_am = ( 64 | servicios[filtro_pico_am] 65 | .groupby(["dia", "id_linea"], as_index=False)["velocidad_comercial"] 66 | .mean() 67 | .round(1) 68 | .rename(columns={"velocidad_comercial": "velocidad_comercial_am"}) 69 | ) 70 | 71 | # Velocidad comercial PM (15 a 19 hs) 72 | filtro_pico_pm = (servicios["diff_minutes"] < 180) & ( 73 | servicios["hour"].between(15, 19) 74 | ) 75 | vel_comercial_linea_pm = ( 76 | servicios[filtro_pico_pm] 77 | .groupby(["dia", "id_linea"], as_index=False)["velocidad_comercial"] 78 | .mean() 79 | .round(1) 80 | .rename(columns={"velocidad_comercial": "velocidad_comercial_pm"}) 81 | ) 82 | 83 | # Consolidar velocidades comerciales 84 | vel_comercial_linea = vel_comercial_linea_all.merge( 85 | vel_comercial_linea_am, how="left" 86 | ).merge(vel_comercial_linea_pm, how="left") 87 | 88 | # Distancia media recorrida por vehículo (total) 89 | km_recorridos_linea = ( 90 | servicios.groupby(["dia", "id_linea", "interno"], as_index=False)["distance_km"] 91 | .sum() 92 | .groupby(["dia", "id_linea"], as_index=False)["distance_km"] 93 | .mean() 94 | .rename(columns={"distance_km": "distancia_media_veh"}) 95 | .round(1) 96 | ) 97 | 98 | vel_comercial_linea = vel_comercial_linea.merge(km_recorridos_linea, how="left") 99 | 100 | return vel_comercial_linea 101 | 102 | 103 | def levanto_data(alias_data, alias_insumos, etapas=[], viajes=[]): 104 | 105 | print("Preparo Datos") 106 | 107 | gps = levanto_tabla_sql("gps", "data", alias_db=alias_data) 108 | 109 | trx = levanto_tabla_sql("transacciones", "data", alias_db=alias_data) 110 | 111 | lineas = levanto_tabla_sql( 112 | "metadata_lineas", 113 | "insumos", 114 | alias_db=alias_insumos, 115 | query="SELECT DISTINCT id_linea, nombre_linea, empresa FROM metadata_lineas ORDER BY id_linea", 116 | ) 117 | 118 | kpis = levanto_tabla_sql("kpi_by_day_line", tabla_tipo="data", alias_db=alias_data) 119 | 120 | servicios = levanto_tabla_sql( 121 | "services", 122 | tabla_tipo="data", 123 | alias_db=alias_data, 124 | query="SELECT * FROM services WHERE valid = 1", 125 | ) 126 | 127 | # Procesamiento de GPS y cálculo de flota 128 | gps["fecha"] = pd.to_datetime(gps["fecha"], unit="s") 129 | gps["dia"] = gps["fecha"].dt.strftime("%Y-%m-%d") 130 | 131 | flota = ( 132 | gps.groupby(["dia", "id_linea"], as_index=False) 133 | .size() 134 | .rename(columns={"size": "flota"}) 135 | ) 136 | 137 | # Cálculo de velocidad comercial 138 | vel_comercial_linea = cal_velocidad_comercial(servicios) 139 | 140 | # Procesamiento de transacciones 141 | 142 | kpis_varios = flota.merge(vel_comercial_linea, how="left").merge(kpis, how="left") 143 | 144 | return trx, etapas, gps, servicios, kpis_varios, lineas 145 | 146 | 147 | @duracion 148 | def agrego_lineas(cols, trx, etapas, gps, servicios, kpis_varios, lineas): 149 | 150 | print("Agrego líneas") 151 | 152 | # Agregado de transacciones 153 | resumen_tarifas = ( 154 | etapas.groupby(cols + ["modo"] + ["tarifa_agregada"])["factor_expansion_linea"] 155 | .sum() 156 | .unstack(fill_value=0) 157 | .reset_index() 158 | ) 159 | 160 | resumen_genero = ( 161 | etapas.groupby(cols + ["modo"] + ["genero_agregado"])["factor_expansion_linea"] 162 | .sum() 163 | .unstack(fill_value=0) 164 | .reset_index() 165 | ) 166 | 167 | tot = ( 168 | etapas.groupby(cols + ["modo"])["factor_expansion_linea"] 169 | .sum() 170 | .reset_index() 171 | .rename(columns={"factor_expansion_linea": "transacciones"}) 172 | ) 173 | 174 | # Agregado de etapas con medias ponderadas 175 | etapas_agg = ( 176 | utils.calculate_weighted_means( 177 | etapas, 178 | aggregate_cols=cols + ["modo"], 179 | weighted_mean_cols=["distancia", "travel_time_min", "travel_speed"], 180 | zero_to_nan=["distancia", "travel_time_min", "travel_speed"], 181 | weight_col="factor_expansion_linea", 182 | var_fex_summed=False, 183 | ) 184 | .round(2) 185 | .rename(columns={"distancia": "distancia_media_pax"}) 186 | ) 187 | 188 | tot = tot.merge(resumen_genero, how="left", on=cols + ["modo"]).merge( 189 | resumen_tarifas, how="left", on=cols + ["modo"] 190 | ) 191 | 192 | # # Redondear solo columnas numéricas 193 | for col in tot.select_dtypes(include="float").columns: 194 | try: 195 | tot[col] = pd.to_numeric(tot[col], errors="coerce").round().astype("Int64") 196 | except Exception as e: 197 | print(f"Error en columna {col}: {e}") 198 | 199 | etapas_agg = tot.merge(etapas_agg, how="left", on=cols + ["modo"]) 200 | 201 | # Agregado de cantidad de internos en transacciones 202 | internos_agg = ( 203 | trx.groupby(cols + ["interno"], as_index=False) 204 | .size() 205 | .groupby(cols, as_index=False) 206 | .size() 207 | .rename(columns={"size": "cant_internos_en_trx"}) 208 | ) 209 | 210 | # Agregado de cantidad de internos con GPS 211 | gps_agg = ( 212 | gps.groupby(cols + ["interno"], as_index=False) 213 | .size() 214 | .groupby(cols, as_index=False) 215 | .size() 216 | .rename(columns={"size": "cant_internos_en_gps"}) 217 | ) 218 | 219 | # Agregado de servicios válidos 220 | serv_agg = ( 221 | servicios[servicios.valid == 1] 222 | .groupby(cols, as_index=False) 223 | .agg({"interno": "count", "distance_km": "sum", "min_ts": "sum"}) 224 | .rename( 225 | columns={ 226 | "interno": "cant_servicios", 227 | "distance_km": "serv_distance_km", 228 | "min_ts": "serv_min_ts", 229 | } 230 | ) 231 | ) 232 | 233 | # Merge de todos los datasets 234 | all = ( 235 | etapas_agg.merge(internos_agg, how="left") 236 | .merge(gps_agg, how="left") 237 | .merge(kpis_varios, how="left") 238 | .merge(lineas, how="left") 239 | .merge(serv_agg, how="left") 240 | ) 241 | 242 | # # Cálculo de porcentajes 243 | # all['tarifa_social_porc'] = (all['tarifa_social'] / all['transacciones'] * 100).round(1) 244 | # all['tarifa_educacion_jubilacion_porc'] = (all['educacion_jubilacion'] / all['transacciones'] * 100).round(1) 245 | 246 | # Cálculo de mes 247 | all["mes"] = all["dia"].str[:7] 248 | 249 | # Reordenamiento y selección de columnas finales 250 | 251 | # cols_genero_tarifa = etapas.genero_agregado.unique().tolist()+etapas.tarifa_agregada.unique().tolist() 252 | 253 | # Redondeo de valores 254 | all["transacciones"] = all["transacciones"].round(0) 255 | all["tot_pax"] = all["tot_pax"].round(0).fillna(0) 256 | all["flota"] = all["flota"].round(0) 257 | all["serv_min_ts"] = all["serv_min_ts"].round(2) 258 | all = all.round({col: 2 for col in all.select_dtypes(include="float").columns}) 259 | 260 | for i in [ 261 | "Femenino", 262 | "Masculino", 263 | "No informado", 264 | "educacion_jubilacion", 265 | "tarifa_social", 266 | "sin_descuento", 267 | ]: 268 | if i not in all.columns: 269 | all[i] = 0 270 | 271 | all["vehiculos_operativos"] = ( 272 | all["cant_internos_en_gps"] 273 | .where(all["cant_internos_en_gps"] > 0, all["cant_internos_en_trx"]) 274 | .fillna(all["flota"]) 275 | .astype("Int64") 276 | ) 277 | 278 | all = all[ 279 | [ 280 | "dia", 281 | "mes", 282 | "id_linea", 283 | "nombre_linea", 284 | "empresa", 285 | "modo", 286 | "transacciones", 287 | "Femenino", 288 | "Masculino", 289 | "No informado", 290 | "educacion_jubilacion", 291 | "sin_descuento", 292 | "tarifa_social", 293 | "travel_time_min", 294 | "travel_speed", 295 | "cant_internos_en_gps", 296 | "cant_internos_en_trx", 297 | "flota", 298 | "vehiculos_operativos", 299 | "velocidad_comercial", 300 | "velocidad_comercial_am", 301 | "velocidad_comercial_pm", 302 | "distancia_media_veh", 303 | "tot_km", 304 | "distancia_media_pax", 305 | "dmt_mean", 306 | "dmt_median", 307 | "pvd", 308 | "kvd", 309 | "ipk", 310 | "fo_mean", 311 | "fo_median", 312 | ] 313 | ] 314 | 315 | for i in ["dia", "mes", "id_linea", "nombre_linea", "empresa", "modo"]: 316 | all[i] = all[i].fillna("").astype(str) 317 | lista = [ 318 | x 319 | for x in all.columns.tolist() 320 | if x not in ["dia", "mes", "id_linea", "nombre_linea", "empresa", "modo"] 321 | ] 322 | for i in lista: 323 | if i in [ 324 | "transacciones", 325 | "Femenino", 326 | "Masculino", 327 | "No informado", 328 | "educacion_jubilacion", 329 | "sin_descuento", 330 | "tarifa_social", 331 | ]: 332 | all[i] = all[i].fillna(0).astype(int) 333 | else: 334 | all[i] = all[i].astype(float).round(1) 335 | 336 | return all 337 | 338 | 339 | @duracion 340 | def calculo_kpi_lineas(alias_data="", alias_insumos="", etapas=[], viajes=[]): 341 | print("calculo kpi lineas") 342 | trx, etapas, gps, servicios, kpis_varios, lineas = levanto_data( 343 | alias_data, alias_insumos, etapas=etapas, viajes=viajes 344 | ) 345 | kpis = agrego_lineas( 346 | ["dia", "id_linea"], trx, etapas, gps, servicios, kpis_varios, lineas 347 | ) 348 | guardar_tabla_sql( 349 | kpis, 350 | table_name="kpis_lineas", 351 | tabla_tipo="general", 352 | filtros={"dia": kpis.dia.unique().tolist()}, 353 | modo="append", 354 | ) 355 | 356 | df = levanto_tabla_sql("kpis_lineas", "general") 357 | tot = ( 358 | df.drop(["dia", "mes"], axis=1) 359 | .groupby(["id_linea", "nombre_linea", "empresa", "modo"], as_index=False) 360 | .mean() 361 | ) 362 | tot["dia"] = "Promedios" 363 | tot["mes"] = "" 364 | df = pd.concat([df, tot], ignore_index=True) 365 | guardar_tabla_sql( 366 | df, 367 | table_name="kpis_lineas", 368 | tabla_tipo="general", 369 | filtros={"dia": df.dia.unique().tolist()}, 370 | modo="append", 371 | ) 372 | 373 | return df 374 | --------------------------------------------------------------------------------