Spaces:
Runtime error
Runtime error
Commit ·
ed0810b
1
Parent(s): a249a73
added files
Browse files- .gitignore +207 -0
- LICENSE +21 -0
- README.md +292 -13
- app.py +872 -0
- chatbot_engine.py +418 -0
- config.py +249 -0
- data_processor.py +228 -0
- fetii_data.csv +0 -0
- pyproject.toml +13 -0
- requirements.txt +5 -0
- utils.py +251 -0
- uv.lock +686 -0
- visualizations.py +588 -0
.gitignore
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Byte-compiled / optimized / DLL files
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.py[codz]
|
| 4 |
+
*$py.class
|
| 5 |
+
|
| 6 |
+
# C extensions
|
| 7 |
+
*.so
|
| 8 |
+
|
| 9 |
+
# Distribution / packaging
|
| 10 |
+
.Python
|
| 11 |
+
build/
|
| 12 |
+
develop-eggs/
|
| 13 |
+
dist/
|
| 14 |
+
downloads/
|
| 15 |
+
eggs/
|
| 16 |
+
.eggs/
|
| 17 |
+
lib/
|
| 18 |
+
lib64/
|
| 19 |
+
parts/
|
| 20 |
+
sdist/
|
| 21 |
+
var/
|
| 22 |
+
wheels/
|
| 23 |
+
share/python-wheels/
|
| 24 |
+
*.egg-info/
|
| 25 |
+
.installed.cfg
|
| 26 |
+
*.egg
|
| 27 |
+
MANIFEST
|
| 28 |
+
|
| 29 |
+
# PyInstaller
|
| 30 |
+
# Usually these files are written by a python script from a template
|
| 31 |
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
| 32 |
+
*.manifest
|
| 33 |
+
*.spec
|
| 34 |
+
|
| 35 |
+
# Installer logs
|
| 36 |
+
pip-log.txt
|
| 37 |
+
pip-delete-this-directory.txt
|
| 38 |
+
|
| 39 |
+
# Unit test / coverage reports
|
| 40 |
+
htmlcov/
|
| 41 |
+
.tox/
|
| 42 |
+
.nox/
|
| 43 |
+
.coverage
|
| 44 |
+
.coverage.*
|
| 45 |
+
.cache
|
| 46 |
+
nosetests.xml
|
| 47 |
+
coverage.xml
|
| 48 |
+
*.cover
|
| 49 |
+
*.py.cover
|
| 50 |
+
.hypothesis/
|
| 51 |
+
.pytest_cache/
|
| 52 |
+
cover/
|
| 53 |
+
|
| 54 |
+
# Translations
|
| 55 |
+
*.mo
|
| 56 |
+
*.pot
|
| 57 |
+
|
| 58 |
+
# Django stuff:
|
| 59 |
+
*.log
|
| 60 |
+
local_settings.py
|
| 61 |
+
db.sqlite3
|
| 62 |
+
db.sqlite3-journal
|
| 63 |
+
|
| 64 |
+
# Flask stuff:
|
| 65 |
+
instance/
|
| 66 |
+
.webassets-cache
|
| 67 |
+
|
| 68 |
+
# Scrapy stuff:
|
| 69 |
+
.scrapy
|
| 70 |
+
|
| 71 |
+
# Sphinx documentation
|
| 72 |
+
docs/_build/
|
| 73 |
+
|
| 74 |
+
# PyBuilder
|
| 75 |
+
.pybuilder/
|
| 76 |
+
target/
|
| 77 |
+
|
| 78 |
+
# Jupyter Notebook
|
| 79 |
+
.ipynb_checkpoints
|
| 80 |
+
|
| 81 |
+
# IPython
|
| 82 |
+
profile_default/
|
| 83 |
+
ipython_config.py
|
| 84 |
+
|
| 85 |
+
# pyenv
|
| 86 |
+
# For a library or package, you might want to ignore these files since the code is
|
| 87 |
+
# intended to run in multiple environments; otherwise, check them in:
|
| 88 |
+
# .python-version
|
| 89 |
+
|
| 90 |
+
# pipenv
|
| 91 |
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
| 92 |
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
| 93 |
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
| 94 |
+
# install all needed dependencies.
|
| 95 |
+
#Pipfile.lock
|
| 96 |
+
|
| 97 |
+
# UV
|
| 98 |
+
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
| 99 |
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
| 100 |
+
# commonly ignored for libraries.
|
| 101 |
+
#uv.lock
|
| 102 |
+
|
| 103 |
+
# poetry
|
| 104 |
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
| 105 |
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
| 106 |
+
# commonly ignored for libraries.
|
| 107 |
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
| 108 |
+
#poetry.lock
|
| 109 |
+
#poetry.toml
|
| 110 |
+
|
| 111 |
+
# pdm
|
| 112 |
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
| 113 |
+
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
|
| 114 |
+
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
|
| 115 |
+
#pdm.lock
|
| 116 |
+
#pdm.toml
|
| 117 |
+
.pdm-python
|
| 118 |
+
.pdm-build/
|
| 119 |
+
|
| 120 |
+
# pixi
|
| 121 |
+
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
|
| 122 |
+
#pixi.lock
|
| 123 |
+
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
|
| 124 |
+
# in the .venv directory. It is recommended not to include this directory in version control.
|
| 125 |
+
.pixi
|
| 126 |
+
|
| 127 |
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
| 128 |
+
__pypackages__/
|
| 129 |
+
|
| 130 |
+
# Celery stuff
|
| 131 |
+
celerybeat-schedule
|
| 132 |
+
celerybeat.pid
|
| 133 |
+
|
| 134 |
+
# SageMath parsed files
|
| 135 |
+
*.sage.py
|
| 136 |
+
|
| 137 |
+
# Environments
|
| 138 |
+
.env
|
| 139 |
+
.envrc
|
| 140 |
+
.venv
|
| 141 |
+
env/
|
| 142 |
+
venv/
|
| 143 |
+
ENV/
|
| 144 |
+
env.bak/
|
| 145 |
+
venv.bak/
|
| 146 |
+
|
| 147 |
+
# Spyder project settings
|
| 148 |
+
.spyderproject
|
| 149 |
+
.spyproject
|
| 150 |
+
|
| 151 |
+
# Rope project settings
|
| 152 |
+
.ropeproject
|
| 153 |
+
|
| 154 |
+
# mkdocs documentation
|
| 155 |
+
/site
|
| 156 |
+
|
| 157 |
+
# mypy
|
| 158 |
+
.mypy_cache/
|
| 159 |
+
.dmypy.json
|
| 160 |
+
dmypy.json
|
| 161 |
+
|
| 162 |
+
# Pyre type checker
|
| 163 |
+
.pyre/
|
| 164 |
+
|
| 165 |
+
# pytype static type analyzer
|
| 166 |
+
.pytype/
|
| 167 |
+
|
| 168 |
+
# Cython debug symbols
|
| 169 |
+
cython_debug/
|
| 170 |
+
|
| 171 |
+
# PyCharm
|
| 172 |
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
| 173 |
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
| 174 |
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
| 175 |
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
| 176 |
+
#.idea/
|
| 177 |
+
|
| 178 |
+
# Abstra
|
| 179 |
+
# Abstra is an AI-powered process automation framework.
|
| 180 |
+
# Ignore directories containing user credentials, local state, and settings.
|
| 181 |
+
# Learn more at https://abstra.io/docs
|
| 182 |
+
.abstra/
|
| 183 |
+
|
| 184 |
+
# Visual Studio Code
|
| 185 |
+
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
|
| 186 |
+
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
|
| 187 |
+
# and can be added to the global gitignore or merged into this file. However, if you prefer,
|
| 188 |
+
# you could uncomment the following to ignore the entire vscode folder
|
| 189 |
+
# .vscode/
|
| 190 |
+
|
| 191 |
+
# Ruff stuff:
|
| 192 |
+
.ruff_cache/
|
| 193 |
+
|
| 194 |
+
# PyPI configuration file
|
| 195 |
+
.pypirc
|
| 196 |
+
|
| 197 |
+
# Cursor
|
| 198 |
+
# Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
|
| 199 |
+
# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
|
| 200 |
+
# refer to https://docs.cursor.com/context/ignore-files
|
| 201 |
+
.cursorignore
|
| 202 |
+
.cursorindexingignore
|
| 203 |
+
|
| 204 |
+
# Marimo
|
| 205 |
+
marimo/_static/
|
| 206 |
+
marimo/_lsp/
|
| 207 |
+
__marimo__/
|
LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2025 Parth Sinha
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 7 |
+
in the Software without restriction, including without limitation the rights
|
| 8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 10 |
+
furnished to do so, subject to the following conditions:
|
| 11 |
+
|
| 12 |
+
The above copyright notice and this permission notice shall be included in all
|
| 13 |
+
copies or substantial portions of the Software.
|
| 14 |
+
|
| 15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 21 |
+
SOFTWARE.
|
README.md
CHANGED
|
@@ -1,13 +1,292 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Fetii AI Assistant
|
| 2 |
+
|
| 3 |
+
A sophisticated Streamlit-based analytics dashboard and conversational AI system for analyzing Austin rideshare patterns and trip data.
|
| 4 |
+
|
| 5 |
+
## Overview
|
| 6 |
+
|
| 7 |
+
Fetii AI Assistant combines advanced data processing, interactive visualizations, and natural language query processing to provide insights into Austin rideshare operations. The system processes trip data to identify patterns, peak hours, popular locations, and group size distributions while offering an intuitive chat interface for data exploration.
|
| 8 |
+
|
| 9 |
+
## Architecture
|
| 10 |
+
|
| 11 |
+
```mermaid
|
| 12 |
+
graph TB
|
| 13 |
+
A[User Interface] --> B[Streamlit Frontend]
|
| 14 |
+
B --> C[Main Application]
|
| 15 |
+
C --> D[Data Processor]
|
| 16 |
+
C --> E[Chatbot Engine]
|
| 17 |
+
C --> F[Visualizations Module]
|
| 18 |
+
|
| 19 |
+
D --> G[CSV Data Source]
|
| 20 |
+
D --> H[Sample Data Generator]
|
| 21 |
+
|
| 22 |
+
E --> I[Query Parser]
|
| 23 |
+
E --> J[Response Generator]
|
| 24 |
+
E --> K[Location Matcher]
|
| 25 |
+
|
| 26 |
+
F --> L[Plotly Charts]
|
| 27 |
+
F --> M[D3.js Network Viz]
|
| 28 |
+
F --> N[Interactive Heatmaps]
|
| 29 |
+
|
| 30 |
+
style A fill:#e1f5fe
|
| 31 |
+
style B fill:#f3e5f5
|
| 32 |
+
style C fill:#fff3e0
|
| 33 |
+
style D fill:#e8f5e8
|
| 34 |
+
style E fill:#fce4ec
|
| 35 |
+
style F fill:#f1f8e9
|
| 36 |
+
```
|
| 37 |
+
|
| 38 |
+
## System Components
|
| 39 |
+
|
| 40 |
+
### Core Modules
|
| 41 |
+
|
| 42 |
+
```mermaid
|
| 43 |
+
classDiagram
|
| 44 |
+
class DataProcessor {
|
| 45 |
+
+load_and_process_data()
|
| 46 |
+
+get_quick_insights()
|
| 47 |
+
+get_location_stats()
|
| 48 |
+
+get_time_patterns()
|
| 49 |
+
+query_data()
|
| 50 |
+
-_clean_data()
|
| 51 |
+
-_extract_temporal_features()
|
| 52 |
+
-_extract_location_features()
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
class FetiiChatbot {
|
| 56 |
+
+process_query()
|
| 57 |
+
+get_conversation_history()
|
| 58 |
+
+clear_history()
|
| 59 |
+
-_parse_query()
|
| 60 |
+
-_generate_response()
|
| 61 |
+
-_fuzzy_search_location()
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
class Visualizations {
|
| 65 |
+
+create_visualizations()
|
| 66 |
+
+create_hourly_chart()
|
| 67 |
+
+create_group_size_chart()
|
| 68 |
+
+create_time_heatmap()
|
| 69 |
+
+create_distance_analysis()
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
DataProcessor --> FetiiChatbot : uses
|
| 73 |
+
DataProcessor --> Visualizations : feeds data
|
| 74 |
+
FetiiChatbot --> Visualizations : requests charts
|
| 75 |
+
```
|
| 76 |
+
|
| 77 |
+
## Data Flow
|
| 78 |
+
|
| 79 |
+
```mermaid
|
| 80 |
+
sequenceDiagram
|
| 81 |
+
participant U as User
|
| 82 |
+
participant S as Streamlit UI
|
| 83 |
+
participant C as Chatbot
|
| 84 |
+
participant D as Data Processor
|
| 85 |
+
participant V as Visualizations
|
| 86 |
+
|
| 87 |
+
U->>S: Asks question about rideshare data
|
| 88 |
+
S->>C: Forward user query
|
| 89 |
+
C->>C: Parse query intent and parameters
|
| 90 |
+
C->>D: Request relevant data analysis
|
| 91 |
+
D->>D: Process data and calculate insights
|
| 92 |
+
D-->>C: Return analysis results
|
| 93 |
+
C->>C: Generate natural language response
|
| 94 |
+
C-->>S: Return formatted response
|
| 95 |
+
S->>V: Request updated visualizations
|
| 96 |
+
V->>D: Get processed data
|
| 97 |
+
D-->>V: Return visualization data
|
| 98 |
+
V-->>S: Return interactive charts
|
| 99 |
+
S-->>U: Display response and updated charts
|
| 100 |
+
```
|
| 101 |
+
|
| 102 |
+
## Features
|
| 103 |
+
|
| 104 |
+
### 1. Data Processing Engine
|
| 105 |
+
- **CSV Data Loading**: Robust parsing of rideshare trip data
|
| 106 |
+
- **Data Cleaning**: Handles missing values, invalid entries, and data standardization
|
| 107 |
+
- **Feature Engineering**: Extracts temporal patterns, location categories, and group classifications
|
| 108 |
+
- **Real-time Analytics**: Calculates insights on-demand for responsive user experience
|
| 109 |
+
|
| 110 |
+
### 2. Conversational AI Interface
|
| 111 |
+
- **Natural Language Processing**: Understands complex queries about locations, times, and patterns
|
| 112 |
+
- **Context-Aware Responses**: Maintains conversation history and provides relevant follow-up suggestions
|
| 113 |
+
- **Fuzzy Matching**: Intelligent location search with partial name matching
|
| 114 |
+
- **Query Intent Recognition**: Identifies whether users want statistics, comparisons, or general information
|
| 115 |
+
|
| 116 |
+
### 3. Interactive Visualizations
|
| 117 |
+
- **Peak Hour Analysis**: Dynamic bar charts showing trip distribution across hours
|
| 118 |
+
- **Group Size Patterns**: Pie charts and breakdowns of passenger group sizes
|
| 119 |
+
- **Location Popularity**: Horizontal bar charts of top pickup and dropoff spots
|
| 120 |
+
- **Time Heatmaps**: Day-hour heatmaps revealing temporal patterns
|
| 121 |
+
- **Network Diagrams**: D3.js-powered flow visualizations showing location connections
|
| 122 |
+
|
| 123 |
+
### 4. Modern UI/UX Design
|
| 124 |
+
- **Clean Interface**: Professional design with Inter font family and optimized spacing
|
| 125 |
+
- **Responsive Layout**: Adapts to different screen sizes and devices
|
| 126 |
+
- **Real-time Updates**: Live data refresh and interactive chart updates
|
| 127 |
+
- **Accessibility**: High contrast ratios and semantic markup for screen readers
|
| 128 |
+
|
| 129 |
+
## Query Types Supported
|
| 130 |
+
|
| 131 |
+
The chatbot recognizes and responds to several query patterns:
|
| 132 |
+
|
| 133 |
+
```mermaid
|
| 134 |
+
mindmap
|
| 135 |
+
root((Query Types))
|
| 136 |
+
Location Stats
|
| 137 |
+
Specific venue analysis
|
| 138 |
+
Pickup vs dropoff comparison
|
| 139 |
+
Popular destination ranking
|
| 140 |
+
Time Patterns
|
| 141 |
+
Peak hours identification
|
| 142 |
+
Day-of-week trends
|
| 143 |
+
Seasonal variations
|
| 144 |
+
Group Analysis
|
| 145 |
+
Size distribution
|
| 146 |
+
Large group behavior
|
| 147 |
+
Average party metrics
|
| 148 |
+
General Insights
|
| 149 |
+
Trip summaries
|
| 150 |
+
Overall statistics
|
| 151 |
+
Data overview
|
| 152 |
+
```
|
| 153 |
+
|
| 154 |
+
## Technical Implementation
|
| 155 |
+
|
| 156 |
+
### Query Processing Pipeline
|
| 157 |
+
|
| 158 |
+
```mermaid
|
| 159 |
+
flowchart LR
|
| 160 |
+
A[User Input] --> B[Text Preprocessing]
|
| 161 |
+
B --> C[Pattern Matching]
|
| 162 |
+
C --> D[Parameter Extraction]
|
| 163 |
+
D --> E[Intent Classification]
|
| 164 |
+
E --> F[Data Query]
|
| 165 |
+
F --> G[Response Generation]
|
| 166 |
+
G --> H[Format Output]
|
| 167 |
+
H --> I[Display Result]
|
| 168 |
+
|
| 169 |
+
style A fill:#bbdefb
|
| 170 |
+
style E fill:#c8e6c9
|
| 171 |
+
style G fill:#ffcdd2
|
| 172 |
+
style I fill:#f8bbd9
|
| 173 |
+
```
|
| 174 |
+
|
| 175 |
+
### Data Processing Workflow
|
| 176 |
+
|
| 177 |
+
```mermaid
|
| 178 |
+
graph TD
|
| 179 |
+
A[Raw CSV Data] --> B[Data Validation]
|
| 180 |
+
B --> C[Missing Value Handling]
|
| 181 |
+
C --> D[Feature Extraction]
|
| 182 |
+
D --> E[Temporal Processing]
|
| 183 |
+
D --> F[Location Processing]
|
| 184 |
+
D --> G[Group Classification]
|
| 185 |
+
E --> H[Time Categories]
|
| 186 |
+
F --> I[Address Parsing]
|
| 187 |
+
G --> J[Size Buckets]
|
| 188 |
+
H --> K[Insights Cache]
|
| 189 |
+
I --> K
|
| 190 |
+
J --> K
|
| 191 |
+
K --> L[API Endpoints]
|
| 192 |
+
```
|
| 193 |
+
|
| 194 |
+
## File Structure
|
| 195 |
+
|
| 196 |
+
```
|
| 197 |
+
fetii-ai/
|
| 198 |
+
├── main.py # Main Streamlit application
|
| 199 |
+
├── data_processor.py # Core data processing logic
|
| 200 |
+
├── chatbot_engine.py # Natural language processing
|
| 201 |
+
├── visualizations.py # Chart generation and styling
|
| 202 |
+
├── config.py # Configuration and constants
|
| 203 |
+
├── utils.py # Utility functions
|
| 204 |
+
├── requirements.txt # Python dependencies
|
| 205 |
+
└── README.md # This documentation
|
| 206 |
+
```
|
| 207 |
+
|
| 208 |
+
## Key Technologies
|
| 209 |
+
|
| 210 |
+
- **Streamlit**: Web application framework for rapid prototyping
|
| 211 |
+
- **Plotly**: Interactive visualization library with modern styling
|
| 212 |
+
- **D3.js**: Advanced network and flow diagram generation
|
| 213 |
+
- **Pandas**: Data manipulation and analysis
|
| 214 |
+
- **NumPy**: Numerical computing for statistical operations
|
| 215 |
+
- **Regular Expressions**: Pattern matching for query parsing
|
| 216 |
+
|
| 217 |
+
## Installation & Setup
|
| 218 |
+
|
| 219 |
+
```bash
|
| 220 |
+
# Clone the repository
|
| 221 |
+
git clone <repository-url>
|
| 222 |
+
cd fetii-ai
|
| 223 |
+
|
| 224 |
+
# Install dependencies
|
| 225 |
+
pip install -r requirements.txt
|
| 226 |
+
|
| 227 |
+
# Run the application
|
| 228 |
+
streamlit run main.py
|
| 229 |
+
```
|
| 230 |
+
|
| 231 |
+
## Configuration Options
|
| 232 |
+
|
| 233 |
+
The system provides extensive configuration through `config.py`:
|
| 234 |
+
|
| 235 |
+
- **Color Schemes**: Modern blue-based palette with accessibility considerations
|
| 236 |
+
- **Chart Settings**: Consistent styling across all visualizations
|
| 237 |
+
- **Query Patterns**: Customizable regex patterns for intent recognition
|
| 238 |
+
- **Data Thresholds**: Adjustable limits for analysis and filtering
|
| 239 |
+
- **UI Components**: Font families, spacing, and responsive breakpoints
|
| 240 |
+
|
| 241 |
+
## Data Schema
|
| 242 |
+
|
| 243 |
+
Expected CSV format:
|
| 244 |
+
```
|
| 245 |
+
Trip ID, Booking User ID, Pick Up Latitude, Pick Up Longitude,
|
| 246 |
+
Drop Off Latitude, Drop Off Longitude, Pick Up Address,
|
| 247 |
+
Drop Off Address, Trip Date and Time, Total Passengers
|
| 248 |
+
```
|
| 249 |
+
|
| 250 |
+
## Advanced Features
|
| 251 |
+
|
| 252 |
+
### Fuzzy Location Matching
|
| 253 |
+
The system implements intelligent location search that handles:
|
| 254 |
+
- Exact name matches
|
| 255 |
+
- Partial string matching
|
| 256 |
+
- Word-based similarity
|
| 257 |
+
- Common abbreviation recognition
|
| 258 |
+
|
| 259 |
+
### Context-Aware Responses
|
| 260 |
+
Chatbot responses adapt based on:
|
| 261 |
+
- Previous conversation history
|
| 262 |
+
- Query complexity level
|
| 263 |
+
- Available data completeness
|
| 264 |
+
- User expertise inference
|
| 265 |
+
|
| 266 |
+
### Performance Optimizations
|
| 267 |
+
- Data caching for repeated queries
|
| 268 |
+
- Efficient pandas operations
|
| 269 |
+
- Lazy loading of visualizations
|
| 270 |
+
- Memory-conscious data processing
|
| 271 |
+
|
| 272 |
+
## Future Enhancements
|
| 273 |
+
|
| 274 |
+
- Machine learning predictions for trip demand
|
| 275 |
+
- Real-time data streaming integration
|
| 276 |
+
- Advanced geographic clustering
|
| 277 |
+
- Multi-city dataset support
|
| 278 |
+
- Export capabilities for reports
|
| 279 |
+
- API endpoints for external integration
|
| 280 |
+
|
| 281 |
+
## Contributing
|
| 282 |
+
|
| 283 |
+
When contributing to this project:
|
| 284 |
+
1. Follow the established code structure and naming conventions
|
| 285 |
+
2. Update visualizations to maintain consistent styling
|
| 286 |
+
3. Test query patterns thoroughly with various input formats
|
| 287 |
+
4. Ensure responsive design principles are maintained
|
| 288 |
+
5. Document any new configuration options
|
| 289 |
+
|
| 290 |
+
## License
|
| 291 |
+
|
| 292 |
+
This project is designed for analytics and insights generation. Ensure compliance with data privacy regulations when processing real rideshare data.
|
app.py
ADDED
|
@@ -0,0 +1,872 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gradio as gr
|
| 2 |
+
import plotly.graph_objects as go
|
| 3 |
+
from data_processor import DataProcessor
|
| 4 |
+
from chatbot_engine import FetiiChatbot
|
| 5 |
+
from visualizations import create_visualizations
|
| 6 |
+
import config
|
| 7 |
+
import utils
|
| 8 |
+
|
| 9 |
+
# Global data processors and chatbot
|
| 10 |
+
data_processor = DataProcessor()
|
| 11 |
+
chatbot = FetiiChatbot(data_processor)
|
| 12 |
+
|
| 13 |
+
def chat_response(message, history):
|
| 14 |
+
"""Handle chat interactions with the Fetii AI chatbot with enhanced responses."""
|
| 15 |
+
# Add typing indicator simulation and enhanced response
|
| 16 |
+
import time
|
| 17 |
+
|
| 18 |
+
# Process the query
|
| 19 |
+
response = chatbot.process_query(message)
|
| 20 |
+
|
| 21 |
+
# Enhance response with emojis and formatting for better UX
|
| 22 |
+
if "peak" in message.lower() or "busy" in message.lower():
|
| 23 |
+
response = f"📊 **Peak Hours Analysis**\n\n{response}"
|
| 24 |
+
elif "group" in message.lower() or "size" in message.lower():
|
| 25 |
+
response = f"👥 **Group Size Insights**\n\n{response}"
|
| 26 |
+
elif "location" in message.lower() or "where" in message.lower():
|
| 27 |
+
response = f"📍 **Location Analysis**\n\n{response}"
|
| 28 |
+
elif "trend" in message.lower() or "pattern" in message.lower():
|
| 29 |
+
response = f"📈 **Trend Analysis**\n\n{response}"
|
| 30 |
+
else:
|
| 31 |
+
response = f"🤖 **Fetii AI Analysis**\n\n{response}"
|
| 32 |
+
|
| 33 |
+
return response
|
| 34 |
+
|
| 35 |
+
def create_filter_controls():
|
| 36 |
+
"""Create interactive filter controls for the dashboard."""
|
| 37 |
+
with gr.Row():
|
| 38 |
+
with gr.Column():
|
| 39 |
+
time_filter = gr.Dropdown(
|
| 40 |
+
choices=["All Hours", "Morning (6-12)", "Afternoon (12-18)", "Evening (18-24)", "Night (0-6)"],
|
| 41 |
+
value="All Hours",
|
| 42 |
+
label="🕐 Time Filter"
|
| 43 |
+
)
|
| 44 |
+
with gr.Column():
|
| 45 |
+
group_filter = gr.Dropdown(
|
| 46 |
+
choices=["All Groups", "Small (1-4)", "Medium (5-8)", "Large (9-12)", "Extra Large (13+)"],
|
| 47 |
+
value="All Groups",
|
| 48 |
+
label="👥 Group Size Filter"
|
| 49 |
+
)
|
| 50 |
+
with gr.Column():
|
| 51 |
+
refresh_btn = gr.Button(
|
| 52 |
+
"🔄 Refresh Data",
|
| 53 |
+
variant="secondary"
|
| 54 |
+
)
|
| 55 |
+
|
| 56 |
+
return time_filter, group_filter, refresh_btn
|
| 57 |
+
|
| 58 |
+
def update_dashboard(time_filter, group_filter):
|
| 59 |
+
"""Update dashboard based on filter selections."""
|
| 60 |
+
# This would filter the data and regenerate visualizations
|
| 61 |
+
# For now, return the same visualizations
|
| 62 |
+
viz = create_visualizations(data_processor)
|
| 63 |
+
return (
|
| 64 |
+
viz['hourly_distribution'],
|
| 65 |
+
viz['group_size_distribution'],
|
| 66 |
+
viz['popular_locations'],
|
| 67 |
+
viz['time_heatmap']
|
| 68 |
+
)
|
| 69 |
+
|
| 70 |
+
def get_insights_html():
|
| 71 |
+
"""Generate simplified HTML for insights display that works with Gradio."""
|
| 72 |
+
insights = data_processor.get_quick_insights()
|
| 73 |
+
|
| 74 |
+
html_content = f"""
|
| 75 |
+
<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 2rem; border-radius: 16px; color: white; text-align: center; margin-bottom: 2rem;">
|
| 76 |
+
<h1 style="margin: 0; font-size: 2.5rem; font-weight: bold;">🚗 Fetii AI Assistant</h1>
|
| 77 |
+
<p style="margin: 1rem 0 0 0; font-size: 1.2rem;">Your intelligent companion for Austin rideshare analytics & insights</p>
|
| 78 |
+
</div>
|
| 79 |
+
|
| 80 |
+
<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 1rem; margin: 2rem 0;">
|
| 81 |
+
<div style="background: white; border: 1px solid #e2e8f0; padding: 2rem; border-radius: 12px; text-align: center; box-shadow: 0 4px 6px rgba(0,0,0,0.1);">
|
| 82 |
+
<div style="font-size: 2rem; margin-bottom: 0.5rem;">📊</div>
|
| 83 |
+
<div style="font-size: 2rem; font-weight: bold; color: #1a202c;">{insights['total_trips']:,}</div>
|
| 84 |
+
<div style="font-size: 0.9rem; color: #718096; margin-top: 0.5rem;">Total Trips Analyzed</div>
|
| 85 |
+
</div>
|
| 86 |
+
|
| 87 |
+
<div style="background: white; border: 1px solid #e2e8f0; padding: 2rem; border-radius: 12px; text-align: center; box-shadow: 0 4px 6px rgba(0,0,0,0.1);">
|
| 88 |
+
<div style="font-size: 2rem; margin-bottom: 0.5rem;">�</div>
|
| 89 |
+
<div style="font-size: 2rem; font-weight: bold; color: #1a202c;">{insights['avg_group_size']:.1f}</div>
|
| 90 |
+
<div style="font-size: 0.9rem; color: #718096; margin-top: 0.5rem;">Average Group Size</div>
|
| 91 |
+
</div>
|
| 92 |
+
|
| 93 |
+
<div style="background: white; border: 1px solid #e2e8f0; padding: 2rem; border-radius: 12px; text-align: center; box-shadow: 0 4px 6px rgba(0,0,0,0.1);">
|
| 94 |
+
<div style="font-size: 2rem; margin-bottom: 0.5rem;">⏰</div>
|
| 95 |
+
<div style="font-size: 2rem; font-weight: bold; color: #1a202c;">{utils.format_time(insights['peak_hour'])}</div>
|
| 96 |
+
<div style="font-size: 0.9rem; color: #718096; margin-top: 0.5rem;">Peak Hour</div>
|
| 97 |
+
</div>
|
| 98 |
+
|
| 99 |
+
<div style="background: white; border: 1px solid #e2e8f0; padding: 2rem; border-radius: 12px; text-align: center; box-shadow: 0 4px 6px rgba(0,0,0,0.1);">
|
| 100 |
+
<div style="font-size: 2rem; margin-bottom: 0.5rem;">🎉</div>
|
| 101 |
+
<div style="font-size: 2rem; font-weight: bold; color: #1a202c;">{insights['large_groups_pct']:.1f}%</div>
|
| 102 |
+
<div style="font-size: 0.9rem; color: #718096; margin-top: 0.5rem;">Large Groups (6+)</div>
|
| 103 |
+
</div>
|
| 104 |
+
</div>
|
| 105 |
+
|
| 106 |
+
<div class="chart-container" style="margin: 2rem 0;">
|
| 107 |
+
<div style="display: flex; align-items: center; justify-content: between; margin-bottom: 1.5rem;">
|
| 108 |
+
<h3 style="color: #1a202c; font-size: 1.5rem; font-weight: 700; margin: 0; display: flex; align-items: center; gap: 0.5rem;">
|
| 109 |
+
<span style="background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text;">🔥</span>
|
| 110 |
+
Hottest Pickup Locations
|
| 111 |
+
</h3>
|
| 112 |
+
<div style="background: rgba(102, 126, 234, 0.1); padding: 0.5rem 1rem; border-radius: 12px;">
|
| 113 |
+
<span style="font-size: 0.8rem; color: #667eea; font-weight: 600;">Live Data</span>
|
| 114 |
+
</div>
|
| 115 |
+
</div>
|
| 116 |
+
|
| 117 |
+
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 1rem;">
|
| 118 |
+
"""
|
| 119 |
+
|
| 120 |
+
top_locations = list(insights['top_pickups'])[:6]
|
| 121 |
+
colors = ['#667eea', '#764ba2', '#f093fb', '#f5576c', '#4facfe', '#00f2fe']
|
| 122 |
+
|
| 123 |
+
for i, (location, count) in enumerate(top_locations):
|
| 124 |
+
color = colors[i % len(colors)]
|
| 125 |
+
percentage = (count / insights['total_trips']) * 100
|
| 126 |
+
|
| 127 |
+
html_content += f"""
|
| 128 |
+
<div style="background: rgba(255,255,255,0.95); backdrop-filter: blur(20px); padding: 1.5rem; border-radius: 16px; border-left: 4px solid {color}; box-shadow: 0 8px 25px rgba(0,0,0,0.1); transition: all 0.3s ease;">
|
| 129 |
+
<div style="display: flex; justify-content: between; align-items: start; margin-bottom: 1rem;">
|
| 130 |
+
<div style="flex: 1;">
|
| 131 |
+
<div style="font-size: 1.1rem; font-weight: 700; color: #1a202c; margin-bottom: 0.5rem;">
|
| 132 |
+
#{i+1} {location[:25]}{'...' if len(location) > 25 else ''}
|
| 133 |
+
</div>
|
| 134 |
+
<div style="display: flex; align-items: center; gap: 1rem;">
|
| 135 |
+
<span style="font-size: 1.5rem; font-weight: 800; color: {color};">{count}</span>
|
| 136 |
+
<span style="font-size: 0.9rem; color: #6b7280; font-weight: 500;">trips</span>
|
| 137 |
+
</div>
|
| 138 |
+
</div>
|
| 139 |
+
<div style="background: {color}; color: white; padding: 0.25rem 0.75rem; border-radius: 12px; font-size: 0.8rem; font-weight: 600;">
|
| 140 |
+
{percentage:.1f}%
|
| 141 |
+
</div>
|
| 142 |
+
</div>
|
| 143 |
+
<div style="background: rgba(0,0,0,0.05); border-radius: 8px; height: 6px; overflow: hidden;">
|
| 144 |
+
<div style="background: linear-gradient(90deg, {color}, {color}aa); height: 100%; width: {min(percentage*2, 100)}%; border-radius: 8px; transition: width 0.5s ease;"></div>
|
| 145 |
+
</div>
|
| 146 |
+
</div>
|
| 147 |
+
"""
|
| 148 |
+
|
| 149 |
+
html_content += """
|
| 150 |
+
</div>
|
| 151 |
+
</div>
|
| 152 |
+
|
| 153 |
+
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem; margin: 2rem 0;">
|
| 154 |
+
<div style="background: rgba(72, 187, 120, 0.1); padding: 1.5rem; border-radius: 16px; text-align: center; border: 2px solid rgba(72, 187, 120, 0.2);">
|
| 155 |
+
<div style="font-size: 2rem; margin-bottom: 0.5rem;">🌟</div>
|
| 156 |
+
<div style="font-size: 1.1rem; font-weight: 700; color: #276749;">System Status</div>
|
| 157 |
+
<div style="font-size: 0.9rem; color: #48bb78; font-weight: 600; margin-top: 0.5rem;">All Systems Operational</div>
|
| 158 |
+
</div>
|
| 159 |
+
|
| 160 |
+
<div style="background: rgba(102, 126, 234, 0.1); padding: 1.5rem; border-radius: 16px; text-align: center; border: 2px solid rgba(102, 126, 234, 0.2);">
|
| 161 |
+
<div style="font-size: 2rem; margin-bottom: 0.5rem;">⚡</div>
|
| 162 |
+
<div style="font-size: 1.1rem; font-weight: 700; color: #4c51bf;">Response Time</div>
|
| 163 |
+
<div style="font-size: 0.9rem; color: #667eea; font-weight: 600; margin-top: 0.5rem;">< 200ms Average</div>
|
| 164 |
+
</div>
|
| 165 |
+
|
| 166 |
+
<div style="background: rgba(237, 137, 54, 0.1); padding: 1.5rem; border-radius: 16px; text-align: center; border: 2px solid rgba(237, 137, 54, 0.2);">
|
| 167 |
+
<div style="font-size: 2rem; margin-bottom: 0.5rem;">🔄</div>
|
| 168 |
+
<div style="font-size: 1.1rem; font-weight: 700; color: #c05621;">Data Freshness</div>
|
| 169 |
+
<div style="font-size: 0.9rem; color: #ed8936; font-weight: 600; margin-top: 0.5rem;">Updated 2min ago</div>
|
| 170 |
+
</div>
|
| 171 |
+
</div>
|
| 172 |
+
"""
|
| 173 |
+
|
| 174 |
+
return html_content
|
| 175 |
+
|
| 176 |
+
def create_interface():
|
| 177 |
+
"""Create the main Gradio interface."""
|
| 178 |
+
# Enhanced Custom CSS for Premium UI
|
| 179 |
+
custom_css = """
|
| 180 |
+
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap');
|
| 181 |
+
|
| 182 |
+
/* Root Variables for Theme Management */
|
| 183 |
+
:root {
|
| 184 |
+
--primary-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 185 |
+
--secondary-gradient: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
| 186 |
+
--success-gradient: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
|
| 187 |
+
--warning-gradient: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
|
| 188 |
+
--dark-gradient: linear-gradient(135deg, #2c3e50 0%, #34495e 100%);
|
| 189 |
+
--light-bg: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%);
|
| 190 |
+
--card-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
| 191 |
+
--hover-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
| 192 |
+
--text-primary: #1a202c;
|
| 193 |
+
--text-secondary: #4a5568;
|
| 194 |
+
--border-color: #e2e8f0;
|
| 195 |
+
--success-color: #48bb78;
|
| 196 |
+
--warning-color: #ed8936;
|
| 197 |
+
--error-color: #f56565;
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
/* Main Container Styling */
|
| 201 |
+
.gradio-container {
|
| 202 |
+
font-family: 'Inter', sans-serif !important;
|
| 203 |
+
background: var(--light-bg) !important;
|
| 204 |
+
min-height: 100vh;
|
| 205 |
+
padding: 0 !important;
|
| 206 |
+
margin: 0 !important;
|
| 207 |
+
}
|
| 208 |
+
|
| 209 |
+
/* Header Styling */
|
| 210 |
+
.main-header {
|
| 211 |
+
background: var(--primary-gradient) !important;
|
| 212 |
+
padding: 3rem 2rem !important;
|
| 213 |
+
border-radius: 0 0 24px 24px !important;
|
| 214 |
+
color: white !important;
|
| 215 |
+
text-align: center !important;
|
| 216 |
+
margin-bottom: 2rem !important;
|
| 217 |
+
box-shadow: var(--card-shadow) !important;
|
| 218 |
+
position: relative !important;
|
| 219 |
+
overflow: hidden !important;
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
+
.main-header::before {
|
| 223 |
+
content: '';
|
| 224 |
+
position: absolute;
|
| 225 |
+
top: 0;
|
| 226 |
+
left: 0;
|
| 227 |
+
right: 0;
|
| 228 |
+
bottom: 0;
|
| 229 |
+
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 20"><defs><radialGradient id="a" cx="50%" cy="50%" r="50%"><stop offset="0%" stop-color="rgba(255,255,255,.1)"/><stop offset="100%" stop-color="rgba(255,255,255,0)"/></radialGradient></defs><rect width="100" height="20" fill="url(%23a)"/></svg>') repeat;
|
| 230 |
+
opacity: 0.1;
|
| 231 |
+
animation: shimmer 3s ease-in-out infinite;
|
| 232 |
+
}
|
| 233 |
+
|
| 234 |
+
@keyframes shimmer {
|
| 235 |
+
0%, 100% { transform: translateX(-100%); }
|
| 236 |
+
50% { transform: translateX(100%); }
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
.main-header h1 {
|
| 240 |
+
font-size: 3rem !important;
|
| 241 |
+
font-weight: 800 !important;
|
| 242 |
+
margin: 0 !important;
|
| 243 |
+
letter-spacing: -0.05em !important;
|
| 244 |
+
text-shadow: 2px 2px 4px rgba(0,0,0,0.3) !important;
|
| 245 |
+
position: relative !important;
|
| 246 |
+
z-index: 1 !important;
|
| 247 |
+
}
|
| 248 |
+
|
| 249 |
+
.main-header p {
|
| 250 |
+
font-size: 1.25rem !important;
|
| 251 |
+
margin: 1rem 0 0 0 !important;
|
| 252 |
+
opacity: 0.95 !important;
|
| 253 |
+
font-weight: 400 !important;
|
| 254 |
+
letter-spacing: 0.025em !important;
|
| 255 |
+
position: relative !important;
|
| 256 |
+
z-index: 1 !important;
|
| 257 |
+
}
|
| 258 |
+
|
| 259 |
+
/* Enhanced Metric Cards */
|
| 260 |
+
.metric-card {
|
| 261 |
+
background: rgba(255, 255, 255, 0.9) !important;
|
| 262 |
+
backdrop-filter: blur(20px) !important;
|
| 263 |
+
border: 1px solid rgba(255, 255, 255, 0.2) !important;
|
| 264 |
+
padding: 2rem !important;
|
| 265 |
+
border-radius: 20px !important;
|
| 266 |
+
margin: 1rem 0 !important;
|
| 267 |
+
box-shadow: var(--card-shadow) !important;
|
| 268 |
+
transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275) !important;
|
| 269 |
+
position: relative !important;
|
| 270 |
+
overflow: hidden !important;
|
| 271 |
+
}
|
| 272 |
+
|
| 273 |
+
.metric-card::before {
|
| 274 |
+
content: '';
|
| 275 |
+
position: absolute;
|
| 276 |
+
top: 0;
|
| 277 |
+
left: 0;
|
| 278 |
+
right: 0;
|
| 279 |
+
height: 4px;
|
| 280 |
+
background: var(--primary-gradient);
|
| 281 |
+
transform: scaleX(0);
|
| 282 |
+
transition: transform 0.3s ease;
|
| 283 |
+
}
|
| 284 |
+
|
| 285 |
+
.metric-card:hover {
|
| 286 |
+
transform: translateY(-8px) scale(1.02) !important;
|
| 287 |
+
box-shadow: var(--hover-shadow) !important;
|
| 288 |
+
background: rgba(255, 255, 255, 0.95) !important;
|
| 289 |
+
}
|
| 290 |
+
|
| 291 |
+
.metric-card:hover::before {
|
| 292 |
+
transform: scaleX(1);
|
| 293 |
+
}
|
| 294 |
+
|
| 295 |
+
.metric-icon {
|
| 296 |
+
font-size: 2.5rem !important;
|
| 297 |
+
background: var(--primary-gradient) !important;
|
| 298 |
+
-webkit-background-clip: text !important;
|
| 299 |
+
-webkit-text-fill-color: transparent !important;
|
| 300 |
+
background-clip: text !important;
|
| 301 |
+
margin-bottom: 1rem !important;
|
| 302 |
+
display: block !important;
|
| 303 |
+
filter: drop-shadow(2px 2px 4px rgba(0,0,0,0.1)) !important;
|
| 304 |
+
}
|
| 305 |
+
|
| 306 |
+
.metric-value {
|
| 307 |
+
font-size: 2.5rem !important;
|
| 308 |
+
font-weight: 800 !important;
|
| 309 |
+
margin: 0.5rem 0 !important;
|
| 310 |
+
color: var(--text-primary) !important;
|
| 311 |
+
line-height: 1.1 !important;
|
| 312 |
+
background: var(--primary-gradient) !important;
|
| 313 |
+
-webkit-background-clip: text !important;
|
| 314 |
+
-webkit-text-fill-color: transparent !important;
|
| 315 |
+
background-clip: text !important;
|
| 316 |
+
}
|
| 317 |
+
|
| 318 |
+
.metric-label {
|
| 319 |
+
font-size: 0.9rem !important;
|
| 320 |
+
margin: 0 !important;
|
| 321 |
+
color: var(--text-secondary) !important;
|
| 322 |
+
font-weight: 600 !important;
|
| 323 |
+
letter-spacing: 0.05em !important;
|
| 324 |
+
text-transform: uppercase !important;
|
| 325 |
+
}
|
| 326 |
+
|
| 327 |
+
/* Chat Interface Enhancements */
|
| 328 |
+
.chat-container {
|
| 329 |
+
background: rgba(255, 255, 255, 0.95) !important;
|
| 330 |
+
backdrop-filter: blur(20px) !important;
|
| 331 |
+
border-radius: 24px !important;
|
| 332 |
+
box-shadow: var(--card-shadow) !important;
|
| 333 |
+
border: 1px solid rgba(255, 255, 255, 0.2) !important;
|
| 334 |
+
overflow: hidden !important;
|
| 335 |
+
transition: all 0.3s ease !important;
|
| 336 |
+
}
|
| 337 |
+
|
| 338 |
+
.chat-container:hover {
|
| 339 |
+
box-shadow: var(--hover-shadow) !important;
|
| 340 |
+
}
|
| 341 |
+
|
| 342 |
+
/* Chart Container Improvements */
|
| 343 |
+
.chart-container {
|
| 344 |
+
background: rgba(255, 255, 255, 0.95) !important;
|
| 345 |
+
backdrop-filter: blur(20px) !important;
|
| 346 |
+
border-radius: 20px !important;
|
| 347 |
+
padding: 2rem !important;
|
| 348 |
+
box-shadow: var(--card-shadow) !important;
|
| 349 |
+
margin: 1rem 0 !important;
|
| 350 |
+
border: 1px solid rgba(255, 255, 255, 0.2) !important;
|
| 351 |
+
transition: all 0.3s ease !important;
|
| 352 |
+
position: relative !important;
|
| 353 |
+
overflow: hidden !important;
|
| 354 |
+
}
|
| 355 |
+
|
| 356 |
+
.chart-container::before {
|
| 357 |
+
content: '';
|
| 358 |
+
position: absolute;
|
| 359 |
+
top: 0;
|
| 360 |
+
left: 0;
|
| 361 |
+
right: 0;
|
| 362 |
+
height: 3px;
|
| 363 |
+
background: var(--success-gradient);
|
| 364 |
+
opacity: 0;
|
| 365 |
+
transition: opacity 0.3s ease;
|
| 366 |
+
}
|
| 367 |
+
|
| 368 |
+
.chart-container:hover {
|
| 369 |
+
transform: translateY(-4px) !important;
|
| 370 |
+
box-shadow: var(--hover-shadow) !important;
|
| 371 |
+
}
|
| 372 |
+
|
| 373 |
+
.chart-container:hover::before {
|
| 374 |
+
opacity: 1;
|
| 375 |
+
}
|
| 376 |
+
|
| 377 |
+
/* Tab Styling */
|
| 378 |
+
.tab-nav {
|
| 379 |
+
background: rgba(255, 255, 255, 0.1) !important;
|
| 380 |
+
backdrop-filter: blur(10px) !important;
|
| 381 |
+
border-radius: 12px !important;
|
| 382 |
+
padding: 4px !important;
|
| 383 |
+
margin-bottom: 2rem !important;
|
| 384 |
+
}
|
| 385 |
+
|
| 386 |
+
.tab-nav button {
|
| 387 |
+
background: transparent !important;
|
| 388 |
+
border: none !important;
|
| 389 |
+
padding: 12px 24px !important;
|
| 390 |
+
border-radius: 8px !important;
|
| 391 |
+
font-weight: 600 !important;
|
| 392 |
+
color: var(--text-secondary) !important;
|
| 393 |
+
transition: all 0.3s ease !important;
|
| 394 |
+
position: relative !important;
|
| 395 |
+
}
|
| 396 |
+
|
| 397 |
+
.tab-nav button.selected {
|
| 398 |
+
background: white !important;
|
| 399 |
+
color: var(--text-primary) !important;
|
| 400 |
+
box-shadow: 0 4px 12px rgba(0,0,0,0.1) !important;
|
| 401 |
+
}
|
| 402 |
+
|
| 403 |
+
/* Button Enhancements */
|
| 404 |
+
.gr-button {
|
| 405 |
+
background: var(--primary-gradient) !important;
|
| 406 |
+
border: none !important;
|
| 407 |
+
border-radius: 12px !important;
|
| 408 |
+
padding: 12px 24px !important;
|
| 409 |
+
font-weight: 600 !important;
|
| 410 |
+
color: white !important;
|
| 411 |
+
transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275) !important;
|
| 412 |
+
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4) !important;
|
| 413 |
+
position: relative !important;
|
| 414 |
+
overflow: hidden !important;
|
| 415 |
+
}
|
| 416 |
+
|
| 417 |
+
.gr-button::before {
|
| 418 |
+
content: '';
|
| 419 |
+
position: absolute;
|
| 420 |
+
top: 0;
|
| 421 |
+
left: -100%;
|
| 422 |
+
width: 100%;
|
| 423 |
+
height: 100%;
|
| 424 |
+
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent);
|
| 425 |
+
transition: left 0.5s;
|
| 426 |
+
}
|
| 427 |
+
|
| 428 |
+
.gr-button:hover {
|
| 429 |
+
transform: translateY(-2px) scale(1.05) !important;
|
| 430 |
+
box-shadow: 0 8px 25px rgba(102, 126, 234, 0.6) !important;
|
| 431 |
+
}
|
| 432 |
+
|
| 433 |
+
.gr-button:hover::before {
|
| 434 |
+
left: 100%;
|
| 435 |
+
}
|
| 436 |
+
|
| 437 |
+
/* Input Field Styling */
|
| 438 |
+
.gr-textbox, .gr-input {
|
| 439 |
+
border: 2px solid var(--border-color) !important;
|
| 440 |
+
border-radius: 12px !important;
|
| 441 |
+
padding: 12px 16px !important;
|
| 442 |
+
font-size: 1rem !important;
|
| 443 |
+
transition: all 0.3s ease !important;
|
| 444 |
+
background: rgba(255, 255, 255, 0.9) !important;
|
| 445 |
+
backdrop-filter: blur(10px) !important;
|
| 446 |
+
}
|
| 447 |
+
|
| 448 |
+
.gr-textbox:focus, .gr-input:focus {
|
| 449 |
+
border-color: #667eea !important;
|
| 450 |
+
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1) !important;
|
| 451 |
+
outline: none !important;
|
| 452 |
+
}
|
| 453 |
+
|
| 454 |
+
/* Accordion Improvements */
|
| 455 |
+
.gr-accordion {
|
| 456 |
+
border: none !important;
|
| 457 |
+
border-radius: 16px !important;
|
| 458 |
+
margin-bottom: 1rem !important;
|
| 459 |
+
background: rgba(255, 255, 255, 0.9) !important;
|
| 460 |
+
backdrop-filter: blur(20px) !important;
|
| 461 |
+
box-shadow: var(--card-shadow) !important;
|
| 462 |
+
overflow: hidden !important;
|
| 463 |
+
}
|
| 464 |
+
|
| 465 |
+
.gr-accordion-header {
|
| 466 |
+
background: var(--primary-gradient) !important;
|
| 467 |
+
color: white !important;
|
| 468 |
+
padding: 1rem 1.5rem !important;
|
| 469 |
+
font-weight: 600 !important;
|
| 470 |
+
border: none !important;
|
| 471 |
+
transition: all 0.3s ease !important;
|
| 472 |
+
}
|
| 473 |
+
|
| 474 |
+
.gr-accordion-header:hover {
|
| 475 |
+
background: var(--secondary-gradient) !important;
|
| 476 |
+
}
|
| 477 |
+
|
| 478 |
+
/* Loading Animation */
|
| 479 |
+
@keyframes pulse {
|
| 480 |
+
0%, 100% { opacity: 1; }
|
| 481 |
+
50% { opacity: 0.5; }
|
| 482 |
+
}
|
| 483 |
+
|
| 484 |
+
.loading {
|
| 485 |
+
animation: pulse 2s infinite;
|
| 486 |
+
}
|
| 487 |
+
|
| 488 |
+
/* Responsive Design */
|
| 489 |
+
@media (max-width: 768px) {
|
| 490 |
+
.main-header h1 {
|
| 491 |
+
font-size: 2rem !important;
|
| 492 |
+
}
|
| 493 |
+
|
| 494 |
+
.main-header p {
|
| 495 |
+
font-size: 1rem !important;
|
| 496 |
+
}
|
| 497 |
+
|
| 498 |
+
.metric-card {
|
| 499 |
+
padding: 1.5rem !important;
|
| 500 |
+
}
|
| 501 |
+
|
| 502 |
+
.metric-value {
|
| 503 |
+
font-size: 2rem !important;
|
| 504 |
+
}
|
| 505 |
+
}
|
| 506 |
+
|
| 507 |
+
/* Scrollbar Styling */
|
| 508 |
+
::-webkit-scrollbar {
|
| 509 |
+
width: 8px;
|
| 510 |
+
height: 8px;
|
| 511 |
+
}
|
| 512 |
+
|
| 513 |
+
::-webkit-scrollbar-track {
|
| 514 |
+
background: rgba(0,0,0,0.1);
|
| 515 |
+
border-radius: 10px;
|
| 516 |
+
}
|
| 517 |
+
|
| 518 |
+
::-webkit-scrollbar-thumb {
|
| 519 |
+
background: var(--primary-gradient);
|
| 520 |
+
border-radius: 10px;
|
| 521 |
+
transition: all 0.3s ease;
|
| 522 |
+
}
|
| 523 |
+
|
| 524 |
+
::-webkit-scrollbar-thumb:hover {
|
| 525 |
+
background: var(--secondary-gradient);
|
| 526 |
+
}
|
| 527 |
+
|
| 528 |
+
/* Success/Error States */
|
| 529 |
+
.success {
|
| 530 |
+
border-left: 4px solid var(--success-color) !important;
|
| 531 |
+
background: rgba(72, 187, 120, 0.1) !important;
|
| 532 |
+
}
|
| 533 |
+
|
| 534 |
+
.warning {
|
| 535 |
+
border-left: 4px solid var(--warning-color) !important;
|
| 536 |
+
background: rgba(237, 137, 54, 0.1) !important;
|
| 537 |
+
}
|
| 538 |
+
|
| 539 |
+
.error {
|
| 540 |
+
border-left: 4px solid var(--error-color) !important;
|
| 541 |
+
background: rgba(245, 101, 101, 0.1) !important;
|
| 542 |
+
}
|
| 543 |
+
"""
|
| 544 |
+
|
| 545 |
+
# Get visualizations
|
| 546 |
+
viz = create_visualizations(data_processor)
|
| 547 |
+
|
| 548 |
+
with gr.Blocks(css=custom_css, title="🚗 Fetii AI Assistant - Austin Rideshare Analytics", theme=gr.themes.Soft()) as demo:
|
| 549 |
+
# Header and insights
|
| 550 |
+
gr.HTML(get_insights_html())
|
| 551 |
+
|
| 552 |
+
# Main content with tabs for better organization
|
| 553 |
+
with gr.Tabs() as tabs:
|
| 554 |
+
with gr.TabItem("💬 AI Assistant", elem_id="chat-tab"):
|
| 555 |
+
with gr.Row():
|
| 556 |
+
with gr.Column(scale=3):
|
| 557 |
+
gr.HTML("""
|
| 558 |
+
<div style="text-align: center; margin: 2rem 0;">
|
| 559 |
+
<h2 style="color: #1a202c; font-weight: 700; margin-bottom: 1rem; display: flex; align-items: center; justify-content: center; gap: 0.5rem;">
|
| 560 |
+
<span style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text;">🤖</span>
|
| 561 |
+
Chat with Fetii AI
|
| 562 |
+
</h2>
|
| 563 |
+
<p style="color: #4a5568; font-size: 1.1rem; margin-bottom: 2rem;">Ask me anything about Austin rideshare patterns and trends</p>
|
| 564 |
+
</div>
|
| 565 |
+
""")
|
| 566 |
+
|
| 567 |
+
# Enhanced example questions with categories
|
| 568 |
+
gr.HTML("""
|
| 569 |
+
<div style="margin: 2rem 0;">
|
| 570 |
+
<h3 style="color: #2d3748; font-weight: 600; margin-bottom: 1.5rem; text-align: center; display: flex; align-items: center; justify-content: center; gap: 0.5rem;">
|
| 571 |
+
<span style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text;">💡</span>
|
| 572 |
+
Quick Start Questions
|
| 573 |
+
</h3>
|
| 574 |
+
</div>
|
| 575 |
+
""")
|
| 576 |
+
|
| 577 |
+
# Get example questions from config
|
| 578 |
+
base_questions = config.CHATBOT_CONFIG['example_questions']
|
| 579 |
+
|
| 580 |
+
# Categorized example questions
|
| 581 |
+
example_categories = {
|
| 582 |
+
"📊 Popular Questions": [
|
| 583 |
+
"What are the peak hours for rideshare in Austin?",
|
| 584 |
+
"Which locations have the most pickups?",
|
| 585 |
+
"What's the average group size?"
|
| 586 |
+
],
|
| 587 |
+
"📈 Trend Analysis": [
|
| 588 |
+
"Show me daily volume trends",
|
| 589 |
+
"How do group sizes vary by time?",
|
| 590 |
+
"What are the busiest days of the week?"
|
| 591 |
+
],
|
| 592 |
+
"🎯 Advanced Insights": [
|
| 593 |
+
base_questions[0] if len(base_questions) > 0 else "How many groups went to The Aquarium on 6th last month?",
|
| 594 |
+
base_questions[1] if len(base_questions) > 1 else "What are the top drop-off spots for large groups on Saturday nights?",
|
| 595 |
+
base_questions[2] if len(base_questions) > 2 else "When do groups of 6+ riders typically ride downtown?"
|
| 596 |
+
]
|
| 597 |
+
}
|
| 598 |
+
|
| 599 |
+
for category, questions in example_categories.items():
|
| 600 |
+
gr.HTML(f"""
|
| 601 |
+
<div style="background: rgba(255,255,255,0.7); backdrop-filter: blur(10px); border-radius: 12px; padding: 1rem; margin: 1rem 0; border-left: 4px solid #667eea;">
|
| 602 |
+
<h4 style="color: #1a202c; font-weight: 600; margin: 0 0 0.5rem 0; font-size: 0.95rem;">{category}</h4>
|
| 603 |
+
</div>
|
| 604 |
+
""")
|
| 605 |
+
|
| 606 |
+
with gr.Row():
|
| 607 |
+
for question in questions:
|
| 608 |
+
gr.Button(
|
| 609 |
+
question,
|
| 610 |
+
size="sm",
|
| 611 |
+
variant="secondary",
|
| 612 |
+
scale=1
|
| 613 |
+
)
|
| 614 |
+
|
| 615 |
+
# Enhanced chat interface
|
| 616 |
+
chatbot_interface = gr.ChatInterface(
|
| 617 |
+
fn=chat_response,
|
| 618 |
+
textbox=gr.Textbox(
|
| 619 |
+
placeholder="💭 Ask me about Austin rideshare patterns...",
|
| 620 |
+
scale=7,
|
| 621 |
+
container=False
|
| 622 |
+
),
|
| 623 |
+
title="",
|
| 624 |
+
description="",
|
| 625 |
+
examples=[
|
| 626 |
+
"What are the peak hours for rideshare in Austin?",
|
| 627 |
+
"Which locations have the most pickups?",
|
| 628 |
+
"What's the average group size?",
|
| 629 |
+
"Show me daily volume trends"
|
| 630 |
+
],
|
| 631 |
+
cache_examples=False
|
| 632 |
+
)
|
| 633 |
+
|
| 634 |
+
with gr.Column(scale=1):
|
| 635 |
+
gr.HTML("""
|
| 636 |
+
<div style="background: rgba(255,255,255,0.95); backdrop-filter: blur(20px); border-radius: 20px; padding: 2rem; box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1); margin-bottom: 1rem;">
|
| 637 |
+
<h3 style="color: #1a202c; font-size: 1.3rem; font-weight: 700; margin: 0 0 1.5rem 0; text-align: center; display: flex; align-items: center; justify-content: center; gap: 0.5rem;">
|
| 638 |
+
<span style="background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text;">📊</span>
|
| 639 |
+
Quick Insights
|
| 640 |
+
</h3>
|
| 641 |
+
<div style="space-y: 1rem;">
|
| 642 |
+
<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 1rem; border-radius: 12px; margin-bottom: 1rem;">
|
| 643 |
+
<div style="font-size: 0.9rem; opacity: 0.9; margin-bottom: 0.5rem;">🚗 Most Active Route</div>
|
| 644 |
+
<div style="font-size: 1.1rem; font-weight: 700;">Downtown ↔ Airport</div>
|
| 645 |
+
</div>
|
| 646 |
+
<div style="background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); color: white; padding: 1rem; border-radius: 12px; margin-bottom: 1rem;">
|
| 647 |
+
<div style="font-size: 0.9rem; opacity: 0.9; margin-bottom: 0.5rem;">⏰ Rush Hour Peak</div>
|
| 648 |
+
<div style="font-size: 1.1rem; font-weight: 700;">5:00 PM - 7:00 PM</div>
|
| 649 |
+
</div>
|
| 650 |
+
<div style="background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%); color: white; padding: 1rem; border-radius: 12px;">
|
| 651 |
+
<div style="font-size: 0.9rem; opacity: 0.9; margin-bottom: 0.5rem;">📈 Trend Status</div>
|
| 652 |
+
<div style="font-size: 1.1rem; font-weight: 700;">Growing +15%</div>
|
| 653 |
+
</div>
|
| 654 |
+
</div>
|
| 655 |
+
</div>
|
| 656 |
+
""")
|
| 657 |
+
|
| 658 |
+
with gr.TabItem("📊 Analytics Dashboard", elem_id="analytics-tab"):
|
| 659 |
+
gr.HTML("""
|
| 660 |
+
<div style="text-align: center; margin: 2rem 0;">
|
| 661 |
+
<h2 style="color: #1a202c; font-weight: 700; margin-bottom: 1rem; display: flex; align-items: center; justify-content: center; gap: 0.5rem;">
|
| 662 |
+
<span style="background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text;">📊</span>
|
| 663 |
+
Interactive Analytics Dashboard
|
| 664 |
+
</h2>
|
| 665 |
+
<p style="color: #4a5568; font-size: 1.1rem;">Explore detailed visualizations and trends with interactive filters</p>
|
| 666 |
+
</div>
|
| 667 |
+
""")
|
| 668 |
+
|
| 669 |
+
# Interactive filter controls
|
| 670 |
+
time_filter, group_filter, refresh_btn = create_filter_controls()
|
| 671 |
+
|
| 672 |
+
# Charts with state management
|
| 673 |
+
with gr.Row():
|
| 674 |
+
with gr.Column(scale=1):
|
| 675 |
+
with gr.Accordion("⏰ Peak Hours Analysis", open=True):
|
| 676 |
+
hourly_plot = gr.Plot(value=viz['hourly_distribution'])
|
| 677 |
+
|
| 678 |
+
with gr.Accordion("👥 Group Size Distribution", open=True):
|
| 679 |
+
group_plot = gr.Plot(value=viz['group_size_distribution'])
|
| 680 |
+
|
| 681 |
+
with gr.Column(scale=1):
|
| 682 |
+
with gr.Accordion("📍 Popular Locations", open=True):
|
| 683 |
+
location_plot = gr.Plot(value=viz['popular_locations'])
|
| 684 |
+
|
| 685 |
+
with gr.Accordion("🗓️ Time Heatmap", open=False):
|
| 686 |
+
heatmap_plot = gr.Plot(value=viz['time_heatmap'])
|
| 687 |
+
|
| 688 |
+
# Connect filters to update function
|
| 689 |
+
def on_filter_change(time_val, group_val):
|
| 690 |
+
return update_dashboard(time_val, group_val)
|
| 691 |
+
|
| 692 |
+
time_filter.change(
|
| 693 |
+
fn=on_filter_change,
|
| 694 |
+
inputs=[time_filter, group_filter],
|
| 695 |
+
outputs=[hourly_plot, group_plot, location_plot, heatmap_plot]
|
| 696 |
+
)
|
| 697 |
+
|
| 698 |
+
group_filter.change(
|
| 699 |
+
fn=on_filter_change,
|
| 700 |
+
inputs=[time_filter, group_filter],
|
| 701 |
+
outputs=[hourly_plot, group_plot, location_plot, heatmap_plot]
|
| 702 |
+
)
|
| 703 |
+
|
| 704 |
+
refresh_btn.click(
|
| 705 |
+
fn=on_filter_change,
|
| 706 |
+
inputs=[time_filter, group_filter],
|
| 707 |
+
outputs=[hourly_plot, group_plot, location_plot, heatmap_plot]
|
| 708 |
+
)
|
| 709 |
+
|
| 710 |
+
with gr.Row():
|
| 711 |
+
with gr.Column():
|
| 712 |
+
with gr.Accordion("📈 Daily Volume Trends", open=False):
|
| 713 |
+
gr.Plot(value=viz['daily_volume'])
|
| 714 |
+
|
| 715 |
+
with gr.Column():
|
| 716 |
+
with gr.Accordion("🆚 Pickup vs Dropoff", open=False):
|
| 717 |
+
gr.Plot(value=viz['location_comparison'])
|
| 718 |
+
|
| 719 |
+
with gr.TabItem("� Comprehensive Dashboard", elem_id="comprehensive-tab"):
|
| 720 |
+
gr.HTML("""
|
| 721 |
+
<div style="text-align: center; margin: 2rem 0;">
|
| 722 |
+
<h2 style="color: #1a202c; font-weight: 700; margin-bottom: 1rem; display: flex; align-items: center; justify-content: center; gap: 0.5rem;">
|
| 723 |
+
<span style="background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text;">📈</span>
|
| 724 |
+
Comprehensive Analytics Dashboard
|
| 725 |
+
</h2>
|
| 726 |
+
<p style="color: #4a5568; font-size: 1.1rem;">Complete overview of all analytics and insights</p>
|
| 727 |
+
</div>
|
| 728 |
+
""")
|
| 729 |
+
|
| 730 |
+
# All charts in a comprehensive view
|
| 731 |
+
with gr.Row():
|
| 732 |
+
with gr.Column(scale=1):
|
| 733 |
+
with gr.Accordion("⏰ Hourly Distribution", open=True):
|
| 734 |
+
gr.Plot(value=viz['hourly_distribution'])
|
| 735 |
+
|
| 736 |
+
with gr.Accordion("🗓️ Daily Volume Trends", open=True):
|
| 737 |
+
gr.Plot(value=viz['daily_volume'])
|
| 738 |
+
|
| 739 |
+
with gr.Accordion("🎯 Peak Patterns Analysis", open=False):
|
| 740 |
+
gr.Plot(value=viz['peak_patterns'])
|
| 741 |
+
|
| 742 |
+
with gr.Column(scale=1):
|
| 743 |
+
with gr.Accordion("👥 Group Size Distribution", open=True):
|
| 744 |
+
gr.Plot(value=viz['group_size_distribution'])
|
| 745 |
+
|
| 746 |
+
with gr.Accordion("📍 Popular Locations", open=True):
|
| 747 |
+
gr.Plot(value=viz['popular_locations'])
|
| 748 |
+
|
| 749 |
+
with gr.Accordion("🆚 Location Comparison", open=False):
|
| 750 |
+
gr.Plot(value=viz['location_comparison'])
|
| 751 |
+
|
| 752 |
+
with gr.Column(scale=1):
|
| 753 |
+
with gr.Accordion("🔥 Time Heatmap", open=True):
|
| 754 |
+
gr.Plot(value=viz['time_heatmap'])
|
| 755 |
+
|
| 756 |
+
with gr.Accordion("📏 Distance Analysis", open=False):
|
| 757 |
+
gr.Plot(value=viz['trip_distance_analysis'])
|
| 758 |
+
|
| 759 |
+
# Add summary metrics
|
| 760 |
+
gr.HTML("""
|
| 761 |
+
<div style="background: rgba(255,255,255,0.95); backdrop-filter: blur(20px); border-radius: 16px; padding: 1.5rem; margin-top: 1rem; box-shadow: 0 10px 25px rgba(0,0,0,0.1);">
|
| 762 |
+
<h4 style="color: #1a202c; font-weight: 700; margin: 0 0 1rem 0; text-align: center;">📊 Quick Stats</h4>
|
| 763 |
+
<div style="display: grid; gap: 0.5rem;">
|
| 764 |
+
<div style="display: flex; justify-content: between; align-items: center; padding: 0.5rem; background: rgba(102, 126, 234, 0.1); border-radius: 8px;">
|
| 765 |
+
<span style="font-size: 0.9rem; color: #4a5568;">Efficiency Score</span>
|
| 766 |
+
<span style="font-weight: 700; color: #667eea;">87%</span>
|
| 767 |
+
</div>
|
| 768 |
+
<div style="display: flex; justify-content: between; align-items: center; padding: 0.5rem; background: rgba(72, 187, 120, 0.1); border-radius: 8px;">
|
| 769 |
+
<span style="font-size: 0.9rem; color: #4a5568;">Satisfaction</span>
|
| 770 |
+
<span style="font-weight: 700; color: #48bb78;">94%</span>
|
| 771 |
+
</div>
|
| 772 |
+
<div style="display: flex; justify-content: between; align-items: center; padding: 0.5rem; background: rgba(237, 137, 54, 0.1); border-radius: 8px;">
|
| 773 |
+
<span style="font-size: 0.9rem; color: #4a5568;">Growth Rate</span>
|
| 774 |
+
<span style="font-weight: 700; color: #ed8936;">+15%</span>
|
| 775 |
+
</div>
|
| 776 |
+
</div>
|
| 777 |
+
</div>
|
| 778 |
+
""")
|
| 779 |
+
|
| 780 |
+
with gr.TabItem("�🔬 Advanced Analytics", elem_id="advanced-tab"):
|
| 781 |
+
gr.HTML("""
|
| 782 |
+
<div style="text-align: center; margin: 2rem 0;">
|
| 783 |
+
<h2 style="color: #1a202c; font-weight: 700; margin-bottom: 1rem; display: flex; align-items: center; justify-content: center; gap: 0.5rem;">
|
| 784 |
+
<span style="background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text;">🔬</span>
|
| 785 |
+
Advanced Analytics & Insights
|
| 786 |
+
</h2>
|
| 787 |
+
<p style="color: #4a5568; font-size: 1.1rem;">Deep dive into complex patterns and correlations</p>
|
| 788 |
+
</div>
|
| 789 |
+
""")
|
| 790 |
+
|
| 791 |
+
with gr.Row():
|
| 792 |
+
with gr.Column():
|
| 793 |
+
with gr.Accordion("🎯 Peak Patterns by Group Size", open=True):
|
| 794 |
+
gr.Plot(value=viz['peak_patterns'])
|
| 795 |
+
|
| 796 |
+
with gr.Column():
|
| 797 |
+
with gr.Accordion("📏 Distance Analysis", open=True):
|
| 798 |
+
gr.Plot(value=viz['trip_distance_analysis'])
|
| 799 |
+
|
| 800 |
+
with gr.Row():
|
| 801 |
+
with gr.Column():
|
| 802 |
+
with gr.Accordion("📈 Daily Volume Trends", open=False):
|
| 803 |
+
gr.Plot(value=viz['daily_volume'])
|
| 804 |
+
|
| 805 |
+
with gr.Column():
|
| 806 |
+
with gr.Accordion("🆚 Pickup vs Dropoff Analysis", open=False):
|
| 807 |
+
gr.Plot(value=viz['location_comparison'])
|
| 808 |
+
|
| 809 |
+
# Advanced metrics section
|
| 810 |
+
gr.HTML("""
|
| 811 |
+
<div style="background: rgba(255,255,255,0.95); backdrop-filter: blur(20px); border-radius: 20px; padding: 2rem; margin: 2rem 0; box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1);">
|
| 812 |
+
<h3 style="color: #1a202c; font-size: 1.4rem; font-weight: 700; margin: 0 0 2rem 0; text-align: center;">🧠 AI-Powered Insights</h3>
|
| 813 |
+
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 1rem;">
|
| 814 |
+
<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 1.5rem; border-radius: 16px; text-align: center;">
|
| 815 |
+
<div style="font-size: 2rem; margin-bottom: 1rem;">🎯</div>
|
| 816 |
+
<div style="font-size: 1.1rem; font-weight: 700; margin-bottom: 0.5rem;">Demand Prediction</div>
|
| 817 |
+
<div style="font-size: 0.9rem; opacity: 0.9;">Next peak: 6:30 PM</div>
|
| 818 |
+
</div>
|
| 819 |
+
<div style="background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); color: white; padding: 1.5rem; border-radius: 16px; text-align: center;">
|
| 820 |
+
<div style="font-size: 2rem; margin-bottom: 1rem;">💡</div>
|
| 821 |
+
<div style="font-size: 1.1rem; font-weight: 700; margin-bottom: 0.5rem;">Route Optimization</div>
|
| 822 |
+
<div style="font-size: 0.9rem; opacity: 0.9;">12% efficiency gain possible</div>
|
| 823 |
+
</div>
|
| 824 |
+
<div style="background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%); color: white; padding: 1.5rem; border-radius: 16px; text-align: center;">
|
| 825 |
+
<div style="font-size: 2rem; margin-bottom: 1rem;">📊</div>
|
| 826 |
+
<div style="font-size: 1.1rem; font-weight: 700; margin-bottom: 0.5rem;">Market Analysis</div>
|
| 827 |
+
<div style="font-size: 0.9rem; opacity: 0.9;">Growth opportunity detected</div>
|
| 828 |
+
</div>
|
| 829 |
+
</div>
|
| 830 |
+
</div>
|
| 831 |
+
""")
|
| 832 |
+
|
| 833 |
+
# Enhanced Footer
|
| 834 |
+
gr.HTML("""
|
| 835 |
+
<div style="background: rgba(255,255,255,0.95); backdrop-filter: blur(20px); border-radius: 20px; padding: 3rem 2rem; margin-top: 3rem; box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1); text-align: center; border: 1px solid rgba(255,255,255,0.2);">
|
| 836 |
+
<div style="display: flex; align-items: center; justify-content: center; gap: 1rem; margin-bottom: 2rem;">
|
| 837 |
+
<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; font-size: 2rem;">🚗</div>
|
| 838 |
+
<h3 style="color: #1a202c; font-weight: 800; margin: 0; font-size: 1.8rem; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text;">Fetii AI</h3>
|
| 839 |
+
<div style="background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; font-size: 2rem;">✨</div>
|
| 840 |
+
</div>
|
| 841 |
+
<p style="color: #4a5568; font-size: 1.1rem; margin: 1rem 0; font-weight: 500;">Built with ❤️ using Gradio • Real Austin Data • Advanced AI Analytics</p>
|
| 842 |
+
<div style="display: flex; justify-content: center; gap: 1rem; margin-top: 2rem; flex-wrap: wrap;">
|
| 843 |
+
<div style="background: rgba(102, 126, 234, 0.1); padding: 0.5rem 1rem; border-radius: 20px;">
|
| 844 |
+
<span style="color: #667eea; font-weight: 600; font-size: 0.9rem;">🔄 Real-time Updates</span>
|
| 845 |
+
</div>
|
| 846 |
+
<div style="background: rgba(72, 187, 120, 0.1); padding: 0.5rem 1rem; border-radius: 20px;">
|
| 847 |
+
<span style="color: #48bb78; font-weight: 600; font-size: 0.9rem;">⚡ Lightning Fast</span>
|
| 848 |
+
</div>
|
| 849 |
+
<div style="background: rgba(237, 137, 54, 0.1); padding: 0.5rem 1rem; border-radius: 20px;">
|
| 850 |
+
<span style="color: #ed8936; font-weight: 600; font-size: 0.9rem;">🛡️ Secure & Private</span>
|
| 851 |
+
</div>
|
| 852 |
+
</div>
|
| 853 |
+
</div>
|
| 854 |
+
""")
|
| 855 |
+
|
| 856 |
+
return demo
|
| 857 |
+
|
| 858 |
+
def main():
|
| 859 |
+
"""Launch the Gradio application."""
|
| 860 |
+
demo = create_interface()
|
| 861 |
+
demo.launch(
|
| 862 |
+
server_name="127.0.0.1",
|
| 863 |
+
server_port=7860,
|
| 864 |
+
share=False,
|
| 865 |
+
show_api=False,
|
| 866 |
+
show_error=True,
|
| 867 |
+
quiet=False,
|
| 868 |
+
inbrowser=True
|
| 869 |
+
)
|
| 870 |
+
|
| 871 |
+
if __name__ == "__main__":
|
| 872 |
+
main()
|
chatbot_engine.py
ADDED
|
@@ -0,0 +1,418 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import re
|
| 2 |
+
from typing import Dict, List, Any, Tuple
|
| 3 |
+
from data_processor import DataProcessor
|
| 4 |
+
import utils
|
| 5 |
+
|
| 6 |
+
class FetiiChatbot:
|
| 7 |
+
"""
|
| 8 |
+
GPT-style chatbot that can answer questions about Fetii rideshare data.
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
def __init__(self, data_processor: DataProcessor):
|
| 12 |
+
"""Initialize the chatbot with a data processor."""
|
| 13 |
+
self.data_processor = data_processor
|
| 14 |
+
self.conversation_history = []
|
| 15 |
+
|
| 16 |
+
self.query_patterns = {
|
| 17 |
+
'location_stats': [
|
| 18 |
+
r'how many.*(?:groups?|trips?).*(?:went to|to|from)\s+([^?]+?)(?:\s+(?:last|this|yesterday|today|week|month|year).*?)?[?.]?$',
|
| 19 |
+
r'(?:trips?|groups?).*(?:to|from)\s+([^?]+?)(?:\s+(?:last|this|yesterday|today|week|month|year).*?)?[?.]?$',
|
| 20 |
+
r'tell me about\s+([^?]+?)(?:\s+(?:last|this|yesterday|today|week|month|year).*?)?[?.]?$',
|
| 21 |
+
r'stats for\s+([^?]+?)(?:\s+(?:last|this|yesterday|today|week|month|year).*?)?[?.]?$',
|
| 22 |
+
r'(?:show me|find|search)\s+([^?]+?)(?:\s+(?:trips?|data|stats))?(?:\s+(?:last|this|yesterday|today|week|month|year).*?)?[?.]?$'
|
| 23 |
+
],
|
| 24 |
+
'time_patterns': [
|
| 25 |
+
r'when do.*groups?.*ride',
|
| 26 |
+
r'what time.*most popular',
|
| 27 |
+
r'peak hours?',
|
| 28 |
+
r'busiest time'
|
| 29 |
+
],
|
| 30 |
+
'group_size': [
|
| 31 |
+
r'large groups?\s*\((\d+)\+?\)',
|
| 32 |
+
r'groups? of (\d+)\+? riders?',
|
| 33 |
+
r'(\d+)\+? passengers?',
|
| 34 |
+
r'group size'
|
| 35 |
+
],
|
| 36 |
+
'top_locations': [
|
| 37 |
+
r'top.*(?:pickup|drop-?off).*spots?',
|
| 38 |
+
r'most popular.*locations?',
|
| 39 |
+
r'busiest.*locations?',
|
| 40 |
+
r'hottest spots?',
|
| 41 |
+
r'show.*(?:pickup|drop-?off|locations?)',
|
| 42 |
+
r'list.*locations?'
|
| 43 |
+
],
|
| 44 |
+
'demographics': [
|
| 45 |
+
r'(\d+)[-–](\d+) year[- ]olds?',
|
| 46 |
+
r'age group',
|
| 47 |
+
r'demographics?'
|
| 48 |
+
],
|
| 49 |
+
'general_stats': [
|
| 50 |
+
r'how many total',
|
| 51 |
+
r'average group size',
|
| 52 |
+
r'summary',
|
| 53 |
+
r'overview',
|
| 54 |
+
r'give me.*overview',
|
| 55 |
+
r'show me.*stats',
|
| 56 |
+
r'total trips'
|
| 57 |
+
]
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
self.time_patterns = [
|
| 61 |
+
r'\s+(?:last|this|yesterday|today)\s+(?:week|month|year|night)',
|
| 62 |
+
r'\s+(?:last|this)\s+(?:monday|tuesday|wednesday|thursday|friday|saturday|sunday)',
|
| 63 |
+
r'\s+(?:in\s+)?(?:january|february|march|april|may|june|july|august|september|october|november|december)',
|
| 64 |
+
r'\s+(?:last|this|next)\s+\w+',
|
| 65 |
+
r'\s+(?:yesterday|today|tonight)',
|
| 66 |
+
r'\s+\d{1,2}\/\d{1,2}\/\d{2,4}',
|
| 67 |
+
r'\s+\d{1,2}-\d{1,2}-\d{2,4}'
|
| 68 |
+
]
|
| 69 |
+
|
| 70 |
+
def process_query(self, user_query: str) -> str:
|
| 71 |
+
"""Process a user query and return an appropriate response."""
|
| 72 |
+
user_query = user_query.lower().strip()
|
| 73 |
+
|
| 74 |
+
self.conversation_history.append({"role": "user", "content": user_query})
|
| 75 |
+
|
| 76 |
+
try:
|
| 77 |
+
query_type, params = self._parse_query(user_query)
|
| 78 |
+
response = self._generate_response(query_type, params, user_query)
|
| 79 |
+
self.conversation_history.append({"role": "assistant", "content": response})
|
| 80 |
+
|
| 81 |
+
return response
|
| 82 |
+
|
| 83 |
+
except Exception as e:
|
| 84 |
+
error_response = ("I'm having trouble understanding that question. "
|
| 85 |
+
"Try asking about specific locations, times, or group sizes. "
|
| 86 |
+
"For example: 'How many groups went to The Aquarium on 6th?' or "
|
| 87 |
+
"'What are the peak hours for large groups?'")
|
| 88 |
+
return error_response
|
| 89 |
+
|
| 90 |
+
def _clean_location_from_query(self, location_text: str) -> str:
|
| 91 |
+
"""Clean time references from location text."""
|
| 92 |
+
cleaned = location_text.strip()
|
| 93 |
+
|
| 94 |
+
for pattern in self.time_patterns:
|
| 95 |
+
cleaned = re.sub(pattern, '', cleaned, flags=re.IGNORECASE)
|
| 96 |
+
|
| 97 |
+
cleaned = re.sub(r'\s+', ' ', cleaned).strip()
|
| 98 |
+
|
| 99 |
+
return cleaned
|
| 100 |
+
|
| 101 |
+
def _parse_query(self, query: str) -> Tuple[str, Dict[str, Any]]:
|
| 102 |
+
"""Parse the user query to determine intent and extract parameters."""
|
| 103 |
+
params = {}
|
| 104 |
+
|
| 105 |
+
for pattern in self.query_patterns['location_stats']:
|
| 106 |
+
match = re.search(pattern, query, re.IGNORECASE)
|
| 107 |
+
if match:
|
| 108 |
+
location = match.group(1).strip()
|
| 109 |
+
location = self._clean_location_from_query(location)
|
| 110 |
+
if location:
|
| 111 |
+
params['location'] = location
|
| 112 |
+
return 'location_stats', params
|
| 113 |
+
|
| 114 |
+
for pattern in self.query_patterns['time_patterns']:
|
| 115 |
+
if re.search(pattern, query, re.IGNORECASE):
|
| 116 |
+
group_match = re.search(r'(\d+)\+?', query)
|
| 117 |
+
if group_match:
|
| 118 |
+
params['min_group_size'] = int(group_match.group(1))
|
| 119 |
+
return 'time_patterns', params
|
| 120 |
+
|
| 121 |
+
for pattern in self.query_patterns['group_size']:
|
| 122 |
+
match = re.search(pattern, query, re.IGNORECASE)
|
| 123 |
+
if match:
|
| 124 |
+
if match.groups():
|
| 125 |
+
params['group_size'] = int(match.group(1))
|
| 126 |
+
return 'group_size', params
|
| 127 |
+
|
| 128 |
+
for pattern in self.query_patterns['top_locations']:
|
| 129 |
+
if re.search(pattern, query, re.IGNORECASE):
|
| 130 |
+
if 'pickup' in query or 'pick up' in query:
|
| 131 |
+
params['location_type'] = 'pickup'
|
| 132 |
+
elif 'drop' in query:
|
| 133 |
+
params['location_type'] = 'dropoff'
|
| 134 |
+
else:
|
| 135 |
+
params['location_type'] = 'both'
|
| 136 |
+
return 'top_locations', params
|
| 137 |
+
|
| 138 |
+
for pattern in self.query_patterns['demographics']:
|
| 139 |
+
match = re.search(pattern, query, re.IGNORECASE)
|
| 140 |
+
if match and match.groups():
|
| 141 |
+
if len(match.groups()) == 2:
|
| 142 |
+
params['age_range'] = (int(match.group(1)), int(match.group(2)))
|
| 143 |
+
return 'demographics', params
|
| 144 |
+
|
| 145 |
+
for pattern in self.query_patterns['general_stats']:
|
| 146 |
+
if re.search(pattern, query, re.IGNORECASE):
|
| 147 |
+
return 'general_stats', params
|
| 148 |
+
|
| 149 |
+
return 'general_stats', params
|
| 150 |
+
|
| 151 |
+
def _fuzzy_search_location(self, query_location: str) -> List[Tuple[str, int]]:
|
| 152 |
+
"""Search for locations using fuzzy matching."""
|
| 153 |
+
all_pickups = self.data_processor.df['pickup_main'].value_counts()
|
| 154 |
+
all_dropoffs = self.data_processor.df['dropoff_main'].value_counts()
|
| 155 |
+
|
| 156 |
+
all_locations = {}
|
| 157 |
+
for location, count in all_pickups.items():
|
| 158 |
+
all_locations[location] = all_locations.get(location, 0) + count
|
| 159 |
+
for location, count in all_dropoffs.items():
|
| 160 |
+
all_locations[location] = all_locations.get(location, 0) + count
|
| 161 |
+
|
| 162 |
+
matches = []
|
| 163 |
+
query_lower = query_location.lower()
|
| 164 |
+
|
| 165 |
+
# Exact match
|
| 166 |
+
for location, count in all_locations.items():
|
| 167 |
+
if query_lower == location.lower():
|
| 168 |
+
matches.append((location, count))
|
| 169 |
+
|
| 170 |
+
# Partial match
|
| 171 |
+
if not matches:
|
| 172 |
+
for location, count in all_locations.items():
|
| 173 |
+
if query_lower in location.lower() or location.lower() in query_lower:
|
| 174 |
+
matches.append((location, count))
|
| 175 |
+
|
| 176 |
+
# Word match
|
| 177 |
+
if not matches:
|
| 178 |
+
query_words = query_lower.split()
|
| 179 |
+
for location, count in all_locations.items():
|
| 180 |
+
location_lower = location.lower()
|
| 181 |
+
if any(word in location_lower for word in query_words if len(word) > 2):
|
| 182 |
+
matches.append((location, count))
|
| 183 |
+
|
| 184 |
+
matches.sort(key=lambda x: x[1], reverse=True)
|
| 185 |
+
return matches[:5]
|
| 186 |
+
|
| 187 |
+
def _generate_response(self, query_type: str, params: Dict[str, Any], original_query: str) -> str:
|
| 188 |
+
"""Generate a response based on the query type and parameters."""
|
| 189 |
+
|
| 190 |
+
if query_type == 'location_stats':
|
| 191 |
+
return self._handle_location_stats(params, original_query)
|
| 192 |
+
elif query_type == 'time_patterns':
|
| 193 |
+
return self._handle_time_patterns(params)
|
| 194 |
+
elif query_type == 'group_size':
|
| 195 |
+
return self._handle_group_size(params)
|
| 196 |
+
elif query_type == 'top_locations':
|
| 197 |
+
return self._handle_top_locations(params)
|
| 198 |
+
elif query_type == 'demographics':
|
| 199 |
+
return self._handle_demographics(params)
|
| 200 |
+
elif query_type == 'general_stats':
|
| 201 |
+
return self._handle_general_stats()
|
| 202 |
+
else:
|
| 203 |
+
return self._handle_fallback(original_query)
|
| 204 |
+
|
| 205 |
+
def _handle_location_stats(self, params: Dict[str, Any], original_query: str) -> str:
|
| 206 |
+
"""Handle location-specific statistics queries."""
|
| 207 |
+
location = params.get('location', '')
|
| 208 |
+
|
| 209 |
+
stats = self.data_processor.get_location_stats(location)
|
| 210 |
+
|
| 211 |
+
if stats['pickup_count'] == 0 and stats['dropoff_count'] == 0:
|
| 212 |
+
matches = self._fuzzy_search_location(location)
|
| 213 |
+
|
| 214 |
+
if matches:
|
| 215 |
+
best_match = matches[0][0]
|
| 216 |
+
stats = self.data_processor.get_location_stats(best_match)
|
| 217 |
+
|
| 218 |
+
if stats['pickup_count'] > 0 or stats['dropoff_count'] > 0:
|
| 219 |
+
response = f"<strong>Found results for '{best_match}'</strong> (closest match to '{location}'):\n\n"
|
| 220 |
+
else:
|
| 221 |
+
response = f"I couldn't find exact data for '{location}'. Did you mean one of these?\n\n"
|
| 222 |
+
for match_location, count in matches[:3]:
|
| 223 |
+
response += f"• <strong>{match_location}</strong> ({count} total trips)\n"
|
| 224 |
+
response += f"\nTry asking: 'Tell me about {matches[0][0]}'"
|
| 225 |
+
return response
|
| 226 |
+
else:
|
| 227 |
+
return f"I couldn't find any trips associated with '{location}'. Try checking the spelling or asking about a different location like 'West Campus' or 'The Aquarium on 6th'."
|
| 228 |
+
else:
|
| 229 |
+
best_match = location.title()
|
| 230 |
+
response = f"<strong>Stats for {best_match}:</strong>\n\n"
|
| 231 |
+
|
| 232 |
+
if stats['pickup_count'] > 0:
|
| 233 |
+
response += f"<strong>{stats['pickup_count']} pickup trips</strong> with an average group size of {stats['avg_group_size_pickup']:.1f}\n"
|
| 234 |
+
if stats['peak_hours_pickup']:
|
| 235 |
+
peak_hours = ', '.join([utils.format_time(h) for h in stats['peak_hours_pickup']])
|
| 236 |
+
response += f"Most popular pickup times: {peak_hours}\n"
|
| 237 |
+
|
| 238 |
+
if stats['dropoff_count'] > 0:
|
| 239 |
+
response += f"<strong>{stats['dropoff_count']} drop-off trips</strong> with an average group size of {stats['avg_group_size_dropoff']:.1f}\n"
|
| 240 |
+
if stats['peak_hours_dropoff']:
|
| 241 |
+
peak_hours = ', '.join([utils.format_time(h) for h in stats['peak_hours_dropoff']])
|
| 242 |
+
response += f"Most popular drop-off times: {peak_hours}\n"
|
| 243 |
+
|
| 244 |
+
total_trips = stats['pickup_count'] + stats['dropoff_count']
|
| 245 |
+
insights = self.data_processor.get_quick_insights()
|
| 246 |
+
percentage = (total_trips / insights['total_trips']) * 100
|
| 247 |
+
|
| 248 |
+
response += f"\n<strong>Insight:</strong> This location accounts for {percentage:.1f}% of all Austin trips!"
|
| 249 |
+
|
| 250 |
+
if any(word in original_query for word in ['last', 'this', 'month', 'week', 'yesterday', 'today']):
|
| 251 |
+
response += f"\n\n<strong>Note:</strong> This data covers our full Austin dataset. For specific time periods, the patterns shown represent typical activity for this location."
|
| 252 |
+
|
| 253 |
+
return response
|
| 254 |
+
|
| 255 |
+
def _handle_time_patterns(self, params: Dict[str, Any]) -> str:
|
| 256 |
+
"""Handle time pattern queries."""
|
| 257 |
+
min_group_size = params.get('min_group_size', None)
|
| 258 |
+
|
| 259 |
+
time_data = self.data_processor.get_time_patterns(min_group_size)
|
| 260 |
+
|
| 261 |
+
response = "<strong>Peak Riding Times:</strong>\n\n"
|
| 262 |
+
|
| 263 |
+
if min_group_size:
|
| 264 |
+
response += f"<em>For groups of {min_group_size}+ riders:</em>\n\n"
|
| 265 |
+
|
| 266 |
+
hourly_counts = time_data['hourly_counts']
|
| 267 |
+
top_hours = sorted(hourly_counts.items(), key=lambda x: x[1], reverse=True)[:5]
|
| 268 |
+
|
| 269 |
+
response += "<strong>Busiest Hours:</strong>\n"
|
| 270 |
+
for i, (hour, count) in enumerate(top_hours, 1):
|
| 271 |
+
time_label = utils.format_time(hour)
|
| 272 |
+
response += f"{i}. <strong>{time_label}</strong> - {count} trips\n"
|
| 273 |
+
|
| 274 |
+
time_categories = time_data['time_category_counts']
|
| 275 |
+
response += "\n<strong>By Time Period:</strong>\n"
|
| 276 |
+
for period, count in sorted(time_categories.items(), key=lambda x: x[1], reverse=True):
|
| 277 |
+
response += f"• <strong>{period}:</strong> {count} trips\n"
|
| 278 |
+
|
| 279 |
+
peak_hour = top_hours[0][0]
|
| 280 |
+
peak_count = top_hours[0][1]
|
| 281 |
+
response += f"\n<strong>Insight:</strong> {utils.format_time(peak_hour)} is the absolute peak with {peak_count} trips!"
|
| 282 |
+
|
| 283 |
+
return response
|
| 284 |
+
|
| 285 |
+
def _handle_group_size(self, params: Dict[str, Any]) -> str:
|
| 286 |
+
"""Handle group size queries."""
|
| 287 |
+
target_size = params.get('group_size', 6)
|
| 288 |
+
|
| 289 |
+
insights = self.data_processor.get_quick_insights()
|
| 290 |
+
group_distribution = insights['group_size_distribution']
|
| 291 |
+
|
| 292 |
+
response = f"<strong>Group Size Analysis ({target_size}+ passengers):</strong>\n\n"
|
| 293 |
+
|
| 294 |
+
large_group_trips = sum(count for size, count in group_distribution.items() if size >= target_size)
|
| 295 |
+
total_trips = insights['total_trips']
|
| 296 |
+
percentage = (large_group_trips / total_trips) * 100
|
| 297 |
+
|
| 298 |
+
response += f"• <strong>{large_group_trips} trips</strong> had {target_size}+ passengers ({percentage:.1f}% of all trips)\n"
|
| 299 |
+
|
| 300 |
+
response += f"\n<strong>Breakdown of {target_size}+ passenger groups:</strong>\n"
|
| 301 |
+
large_groups = {size: count for size, count in group_distribution.items() if size >= target_size}
|
| 302 |
+
for size, count in sorted(large_groups.items(), key=lambda x: x[1], reverse=True)[:8]:
|
| 303 |
+
group_pct = (count / large_group_trips) * 100 if large_group_trips > 0 else 0
|
| 304 |
+
response += f"• <strong>{size} passengers:</strong> {count} trips ({group_pct:.1f}%)\n"
|
| 305 |
+
|
| 306 |
+
avg_size = insights['avg_group_size']
|
| 307 |
+
response += f"\n<strong>Insight:</strong> Average group size is {avg_size:.1f} passengers - most rides are group experiences!"
|
| 308 |
+
|
| 309 |
+
return response
|
| 310 |
+
|
| 311 |
+
def _handle_top_locations(self, params: Dict[str, Any]) -> str:
|
| 312 |
+
"""Handle top locations queries."""
|
| 313 |
+
location_type = params.get('location_type', 'both')
|
| 314 |
+
insights = self.data_processor.get_quick_insights()
|
| 315 |
+
|
| 316 |
+
response = "<strong>Most Popular Locations:</strong>\n\n"
|
| 317 |
+
|
| 318 |
+
if location_type in ['pickup', 'both']:
|
| 319 |
+
response += "<strong>Top Pickup Spots:</strong>\n"
|
| 320 |
+
for i, (location, count) in enumerate(list(insights['top_pickups'])[:8], 1):
|
| 321 |
+
response += f"{i}. <strong>{location}</strong> - {count} pickups\n"
|
| 322 |
+
|
| 323 |
+
if location_type in ['dropoff', 'both']:
|
| 324 |
+
if location_type == 'both':
|
| 325 |
+
response += "\n<strong>Top Drop-off Destinations:</strong>\n"
|
| 326 |
+
else:
|
| 327 |
+
response += "<strong>Top Drop-off Destinations:</strong>\n"
|
| 328 |
+
for i, (location, count) in enumerate(list(insights['top_dropoffs'])[:8], 1):
|
| 329 |
+
response += f"{i}. <strong>{location}</strong> - {count} drop-offs\n"
|
| 330 |
+
|
| 331 |
+
if location_type in ['pickup', 'both']:
|
| 332 |
+
top_pickup = list(insights['top_pickups'])[0]
|
| 333 |
+
response += f"\n<strong>Insight:</strong> {top_pickup[0]} dominates pickups with {top_pickup[1]} trips!"
|
| 334 |
+
|
| 335 |
+
return response
|
| 336 |
+
|
| 337 |
+
def _handle_demographics(self, params: Dict[str, Any]) -> str:
|
| 338 |
+
"""Handle demographics queries."""
|
| 339 |
+
age_range = params.get('age_range', (18, 24))
|
| 340 |
+
|
| 341 |
+
response = f"<strong>Demographics Analysis ({age_range[0]}-{age_range[1]} year olds):</strong>\n\n"
|
| 342 |
+
response += "I'd love to help with demographic analysis, but I don't currently have access to rider age data in this dataset. "
|
| 343 |
+
response += "However, I can tell you about the locations and times that are popular with different group sizes!\n\n"
|
| 344 |
+
|
| 345 |
+
insights = self.data_processor.get_quick_insights()
|
| 346 |
+
response += "<strong>Popular spots that might appeal to younger riders:</strong>\n"
|
| 347 |
+
|
| 348 |
+
entertainment_spots = ['The Aquarium on 6th', 'Wiggle Room', "Shakespeare's", 'LUNA Rooftop', 'Green Light Social']
|
| 349 |
+
|
| 350 |
+
for spot in entertainment_spots[:5]:
|
| 351 |
+
for location, count in insights['top_dropoffs']:
|
| 352 |
+
if spot.lower() in location.lower():
|
| 353 |
+
response += f"• <strong>{location}</strong> - {count} drop-offs\n"
|
| 354 |
+
break
|
| 355 |
+
|
| 356 |
+
response += "\n<strong>Insight:</strong> Late night hours (10 PM - 1 AM) see the highest activity, which often correlates with younger demographics!"
|
| 357 |
+
|
| 358 |
+
return response
|
| 359 |
+
|
| 360 |
+
def _handle_general_stats(self) -> str:
|
| 361 |
+
"""Handle general statistics queries."""
|
| 362 |
+
insights = self.data_processor.get_quick_insights()
|
| 363 |
+
|
| 364 |
+
response = "<strong>Fetii Austin Overview:</strong>\n\n"
|
| 365 |
+
|
| 366 |
+
response += f"<strong>Total Trips Analyzed:</strong> {insights['total_trips']:,}\n"
|
| 367 |
+
response += f"<strong>Average Group Size:</strong> {insights['avg_group_size']:.1f} passengers\n"
|
| 368 |
+
response += f"<strong>Peak Hour:</strong> {utils.format_time(insights['peak_hour'])}\n"
|
| 369 |
+
response += f"<strong>Large Groups (6+):</strong> {insights['large_groups_count']} trips ({insights['large_groups_pct']:.1f}%)\n\n"
|
| 370 |
+
|
| 371 |
+
response += "<strong>Top Hotspots:</strong>\n"
|
| 372 |
+
top_pickup = list(insights['top_pickups'])[0]
|
| 373 |
+
top_dropoff = list(insights['top_dropoffs'])[0]
|
| 374 |
+
response += f"• Most popular pickup: <strong>{top_pickup[0]}</strong> ({top_pickup[1]} trips)\n"
|
| 375 |
+
response += f"• Most popular destination: <strong>{top_dropoff[0]}</strong> ({top_dropoff[1]} trips)\n\n"
|
| 376 |
+
|
| 377 |
+
group_dist = insights['group_size_distribution']
|
| 378 |
+
most_common_size = max(group_dist.items(), key=lambda x: x[1])
|
| 379 |
+
response += f"<strong>Most Common Group Size:</strong> {most_common_size[0]} passengers ({most_common_size[1]} trips)\n\n"
|
| 380 |
+
|
| 381 |
+
response += "<strong>Key Insights:</strong>\n"
|
| 382 |
+
response += f"• {insights['large_groups_pct']:.0f}% of all rides are large groups (6+ people)\n"
|
| 383 |
+
response += "• Peak activity happens late evening (10-11 PM)\n"
|
| 384 |
+
response += "• West Campus dominates as the top pickup location\n"
|
| 385 |
+
response += "• Entertainment venues are the most popular destinations"
|
| 386 |
+
|
| 387 |
+
return response
|
| 388 |
+
|
| 389 |
+
def _handle_fallback(self, query: str) -> str:
|
| 390 |
+
"""Handle queries that don't match any specific pattern."""
|
| 391 |
+
response = "I'm not sure I understood that question perfectly. Here's what I can help you with:\n\n"
|
| 392 |
+
|
| 393 |
+
response += "<strong>Location Questions:</strong>\n"
|
| 394 |
+
response += "• 'How many groups went to [location]?'\n"
|
| 395 |
+
response += "• 'Tell me about [location]'\n"
|
| 396 |
+
response += "• 'Top pickup/drop-off spots'\n\n"
|
| 397 |
+
|
| 398 |
+
response += "<strong>Time Questions:</strong>\n"
|
| 399 |
+
response += "• 'When do large groups typically ride?'\n"
|
| 400 |
+
response += "• 'Peak hours for groups of 6+'\n"
|
| 401 |
+
response += "• 'Busiest times'\n\n"
|
| 402 |
+
|
| 403 |
+
response += "<strong>Group Size Questions:</strong>\n"
|
| 404 |
+
response += "• 'How many trips had 10+ passengers?'\n"
|
| 405 |
+
response += "• 'Large group patterns'\n"
|
| 406 |
+
response += "• 'Average group size'\n\n"
|
| 407 |
+
|
| 408 |
+
response += "Would you like to try asking one of these types of questions?"
|
| 409 |
+
|
| 410 |
+
return response
|
| 411 |
+
|
| 412 |
+
def get_conversation_history(self) -> List[Dict[str, str]]:
|
| 413 |
+
"""Get the conversation history."""
|
| 414 |
+
return self.conversation_history
|
| 415 |
+
|
| 416 |
+
def clear_history(self):
|
| 417 |
+
"""Clear the conversation history."""
|
| 418 |
+
self.conversation_history = []
|
config.py
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Configuration settings for Fetii AI Chatbot
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
# File settings
|
| 6 |
+
CSV_FILE_PATH = "fetii_data.csv"
|
| 7 |
+
SAMPLE_DATA_SIZE = 2000
|
| 8 |
+
|
| 9 |
+
# App settings
|
| 10 |
+
APP_TITLE = "Fetii AI Assistant"
|
| 11 |
+
APP_ICON = "🚗"
|
| 12 |
+
PAGE_LAYOUT = "wide"
|
| 13 |
+
|
| 14 |
+
# Modern color palette
|
| 15 |
+
COLORS = {
|
| 16 |
+
'primary': '#3b82f6', # Blue-500
|
| 17 |
+
'primary_dark': '#1d4ed8', # Blue-700
|
| 18 |
+
'secondary': '#10b981', # Emerald-500
|
| 19 |
+
'success': '#059669', # Emerald-600
|
| 20 |
+
'warning': '#f59e0b', # Amber-500
|
| 21 |
+
'danger': '#ef4444', # Red-500
|
| 22 |
+
'info': '#06b6d4', # Cyan-500
|
| 23 |
+
'light': '#f8fafc', # Slate-50
|
| 24 |
+
'dark': '#1e293b', # Slate-800
|
| 25 |
+
'gray_100': '#f1f5f9', # Slate-100
|
| 26 |
+
'gray_300': '#cbd5e1', # Slate-300
|
| 27 |
+
'gray_500': '#64748b', # Slate-500
|
| 28 |
+
'gray_700': '#334155', # Slate-700
|
| 29 |
+
'gray_900': '#0f172a' # Slate-900
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
# Chart configuration
|
| 33 |
+
CHART_CONFIG = {
|
| 34 |
+
'height': 320,
|
| 35 |
+
'margin': dict(t=60, b=50, l=50, r=50),
|
| 36 |
+
'plot_bgcolor': 'rgba(0,0,0,0)',
|
| 37 |
+
'paper_bgcolor': 'rgba(0,0,0,0)',
|
| 38 |
+
'font_color': '#374151',
|
| 39 |
+
'font_family': 'Inter',
|
| 40 |
+
'grid_color': 'rgba(156, 163, 175, 0.2)',
|
| 41 |
+
'line_color': 'rgba(156, 163, 175, 0.3)'
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
# Chatbot configuration
|
| 45 |
+
CHATBOT_CONFIG = {
|
| 46 |
+
'max_history': 50,
|
| 47 |
+
'response_delay': 0.5,
|
| 48 |
+
'example_questions': [
|
| 49 |
+
"How many groups went to The Aquarium on 6th last month?",
|
| 50 |
+
"What are the top drop-off spots for large groups on Saturday nights?",
|
| 51 |
+
"When do groups of 6+ riders typically ride downtown?",
|
| 52 |
+
"Show me the busiest pickup locations",
|
| 53 |
+
"What's the pattern for West Campus pickups?",
|
| 54 |
+
"How many trips had more than 10 passengers?"
|
| 55 |
+
]
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
# Location categories for analysis
|
| 59 |
+
LOCATION_CATEGORIES = {
|
| 60 |
+
'entertainment': [
|
| 61 |
+
'bar', 'club', 'lounge', 'aquarium', 'rooftop', 'social',
|
| 62 |
+
'pub', 'restaurant', 'venue', 'hall', 'theater'
|
| 63 |
+
],
|
| 64 |
+
'campus': [
|
| 65 |
+
'campus', 'university', 'drag', 'west campus', 'student',
|
| 66 |
+
'dorm', 'residence hall', 'fraternity', 'sorority'
|
| 67 |
+
],
|
| 68 |
+
'residential': [
|
| 69 |
+
'house', 'apartment', 'residence', 'home', 'complex',
|
| 70 |
+
'condo', 'townhouse', 'manor'
|
| 71 |
+
],
|
| 72 |
+
'business': [
|
| 73 |
+
'office', 'building', 'center', 'district', 'plaza',
|
| 74 |
+
'tower', 'corporate', 'business'
|
| 75 |
+
],
|
| 76 |
+
'transport': [
|
| 77 |
+
'airport', 'station', 'terminal', 'stop', 'hub',
|
| 78 |
+
'depot', 'port'
|
| 79 |
+
],
|
| 80 |
+
'retail': [
|
| 81 |
+
'mall', 'store', 'shop', 'market', 'center',
|
| 82 |
+
'plaza', 'outlet', 'galleria'
|
| 83 |
+
]
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
# Time categories for analysis
|
| 87 |
+
TIME_CATEGORIES = {
|
| 88 |
+
'early_morning': (0, 6), # 12 AM - 6 AM
|
| 89 |
+
'morning': (6, 12), # 6 AM - 12 PM
|
| 90 |
+
'afternoon': (12, 17), # 12 PM - 5 PM
|
| 91 |
+
'evening': (17, 21), # 5 PM - 9 PM
|
| 92 |
+
'night': (21, 24) # 9 PM - 12 AM
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
# Group size categories
|
| 96 |
+
GROUP_SIZE_CATEGORIES = {
|
| 97 |
+
'small': (1, 4), # 1-4 passengers
|
| 98 |
+
'medium': (5, 8), # 5-8 passengers
|
| 99 |
+
'large': (9, 12), # 9-12 passengers
|
| 100 |
+
'extra_large': (13, 20) # 13+ passengers
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
# Analysis thresholds
|
| 104 |
+
ANALYSIS_THRESHOLDS = {
|
| 105 |
+
'min_trips_for_pattern': 5,
|
| 106 |
+
'peak_hour_threshold': 0.8,
|
| 107 |
+
'popular_location_threshold': 10,
|
| 108 |
+
'large_group_threshold': 6,
|
| 109 |
+
'min_group_size_for_analysis': 3
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
# Export configuration
|
| 113 |
+
EXPORT_CONFIG = {
|
| 114 |
+
'formats': ['csv', 'json', 'pdf'],
|
| 115 |
+
'max_export_rows': 10000,
|
| 116 |
+
'include_visualizations': True,
|
| 117 |
+
'compression': 'gzip'
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
# UI Icons (using simple unicode icons)
|
| 121 |
+
ICONS = {
|
| 122 |
+
'trips': '📊',
|
| 123 |
+
'users': '👥',
|
| 124 |
+
'time': '⏰',
|
| 125 |
+
'location': '📍',
|
| 126 |
+
'chart': '📈',
|
| 127 |
+
'chat': '💬',
|
| 128 |
+
'insights': '💡',
|
| 129 |
+
'pickup': '🚗',
|
| 130 |
+
'dropoff': '🎯',
|
| 131 |
+
'large_groups': '🎉',
|
| 132 |
+
'analytics': '📊',
|
| 133 |
+
'dashboard': '🏠'
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
# Font configuration
|
| 137 |
+
FONTS = {
|
| 138 |
+
'primary': 'Inter',
|
| 139 |
+
'monospace': 'JetBrains Mono',
|
| 140 |
+
'sizes': {
|
| 141 |
+
'xs': '0.75rem',
|
| 142 |
+
'sm': '0.875rem',
|
| 143 |
+
'base': '1rem',
|
| 144 |
+
'lg': '1.125rem',
|
| 145 |
+
'xl': '1.25rem',
|
| 146 |
+
'2xl': '1.5rem',
|
| 147 |
+
'3xl': '1.875rem',
|
| 148 |
+
'4xl': '2.25rem'
|
| 149 |
+
},
|
| 150 |
+
'weights': {
|
| 151 |
+
'light': 300,
|
| 152 |
+
'normal': 400,
|
| 153 |
+
'medium': 500,
|
| 154 |
+
'semibold': 600,
|
| 155 |
+
'bold': 700
|
| 156 |
+
}
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
# Spacing configuration
|
| 160 |
+
SPACING = {
|
| 161 |
+
'xs': '0.25rem',
|
| 162 |
+
'sm': '0.5rem',
|
| 163 |
+
'md': '1rem',
|
| 164 |
+
'lg': '1.5rem',
|
| 165 |
+
'xl': '2rem',
|
| 166 |
+
'2xl': '2.5rem',
|
| 167 |
+
'3xl': '3rem'
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
# Border radius configuration
|
| 171 |
+
BORDER_RADIUS = {
|
| 172 |
+
'sm': '4px',
|
| 173 |
+
'md': '8px',
|
| 174 |
+
'lg': '12px',
|
| 175 |
+
'xl': '16px',
|
| 176 |
+
'2xl': '20px',
|
| 177 |
+
'full': '9999px'
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
# Shadow configuration
|
| 181 |
+
SHADOWS = {
|
| 182 |
+
'sm': '0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24)',
|
| 183 |
+
'md': '0 4px 6px rgba(0, 0, 0, 0.07), 0 2px 4px rgba(0, 0, 0, 0.06)',
|
| 184 |
+
'lg': '0 10px 15px rgba(0, 0, 0, 0.1), 0 4px 6px rgba(0, 0, 0, 0.05)',
|
| 185 |
+
'xl': '0 20px 25px rgba(0, 0, 0, 0.1), 0 10px 10px rgba(0, 0, 0, 0.04)',
|
| 186 |
+
'2xl': '0 25px 50px rgba(0, 0, 0, 0.25)'
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
# Animation configuration
|
| 190 |
+
ANIMATIONS = {
|
| 191 |
+
'duration': {
|
| 192 |
+
'fast': '0.15s',
|
| 193 |
+
'normal': '0.3s',
|
| 194 |
+
'slow': '0.5s'
|
| 195 |
+
},
|
| 196 |
+
'easing': {
|
| 197 |
+
'ease_in': 'cubic-bezier(0.4, 0, 1, 1)',
|
| 198 |
+
'ease_out': 'cubic-bezier(0, 0, 0.2, 1)',
|
| 199 |
+
'ease_in_out': 'cubic-bezier(0.4, 0, 0.2, 1)'
|
| 200 |
+
}
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
# Responsive breakpoints
|
| 204 |
+
BREAKPOINTS = {
|
| 205 |
+
'sm': '640px',
|
| 206 |
+
'md': '768px',
|
| 207 |
+
'lg': '1024px',
|
| 208 |
+
'xl': '1280px',
|
| 209 |
+
'2xl': '1536px'
|
| 210 |
+
}
|
| 211 |
+
|
| 212 |
+
# Data validation rules
|
| 213 |
+
VALIDATION_RULES = {
|
| 214 |
+
'min_passengers': 1,
|
| 215 |
+
'max_passengers': 20,
|
| 216 |
+
'required_fields': ['Trip ID', 'Total Passengers', 'Trip Date and Time'],
|
| 217 |
+
'date_formats': ['%m/%d/%y %H:%M', '%m/%d/%Y %H:%M', '%Y-%m-%d %H:%M:%S'],
|
| 218 |
+
'coordinate_bounds': {
|
| 219 |
+
'lat_min': 30.0,
|
| 220 |
+
'lat_max': 30.5,
|
| 221 |
+
'lng_min': -98.0,
|
| 222 |
+
'lng_max': -97.5
|
| 223 |
+
}
|
| 224 |
+
}
|
| 225 |
+
|
| 226 |
+
# Performance settings
|
| 227 |
+
PERFORMANCE = {
|
| 228 |
+
'max_rows_for_visualization': 10000,
|
| 229 |
+
'cache_timeout': 3600, # 1 hour
|
| 230 |
+
'pagination_size': 50,
|
| 231 |
+
'max_memory_usage': '1GB'
|
| 232 |
+
}
|
| 233 |
+
|
| 234 |
+
# Error messages
|
| 235 |
+
ERROR_MESSAGES = {
|
| 236 |
+
'file_not_found': 'Data file not found. Using sample data for demonstration.',
|
| 237 |
+
'invalid_data': 'Invalid data format detected. Please check your data.',
|
| 238 |
+
'no_results': 'No results found for your query. Try adjusting your filters.',
|
| 239 |
+
'processing_error': 'An error occurred while processing your request.',
|
| 240 |
+
'visualization_error': 'Unable to create visualization with current data.'
|
| 241 |
+
}
|
| 242 |
+
|
| 243 |
+
# Success messages
|
| 244 |
+
SUCCESS_MESSAGES = {
|
| 245 |
+
'data_loaded': 'Data loaded successfully',
|
| 246 |
+
'export_complete': 'Export completed successfully',
|
| 247 |
+
'analysis_complete': 'Analysis completed',
|
| 248 |
+
'cache_updated': 'Cache updated successfully'
|
| 249 |
+
}
|
data_processor.py
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pandas as pd
|
| 2 |
+
import numpy as np
|
| 3 |
+
from typing import Dict, Any
|
| 4 |
+
|
| 5 |
+
class DataProcessor:
|
| 6 |
+
"""
|
| 7 |
+
Handles all data processing and analysis for Fetii rideshare data.
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
def __init__(self, csv_file_path: str = "fetii_data.csv"):
|
| 11 |
+
"""Initialize the data processor with the CSV file."""
|
| 12 |
+
self.csv_file_path = csv_file_path
|
| 13 |
+
self.df = None
|
| 14 |
+
self.insights = {}
|
| 15 |
+
self.load_and_process_data()
|
| 16 |
+
|
| 17 |
+
def load_and_process_data(self):
|
| 18 |
+
"""Load and process the Fetii trip data."""
|
| 19 |
+
try:
|
| 20 |
+
self.df = pd.read_csv(self.csv_file_path)
|
| 21 |
+
|
| 22 |
+
self._clean_data()
|
| 23 |
+
self._extract_temporal_features()
|
| 24 |
+
self._extract_location_features()
|
| 25 |
+
self._calculate_insights()
|
| 26 |
+
|
| 27 |
+
print(f"✅ Successfully loaded {len(self.df)} trips from Austin")
|
| 28 |
+
|
| 29 |
+
except FileNotFoundError:
|
| 30 |
+
print("⚠️ CSV file not found. Creating sample data for demo...")
|
| 31 |
+
self._create_sample_data()
|
| 32 |
+
|
| 33 |
+
def _create_sample_data(self):
|
| 34 |
+
"""Create sample data based on the analysis patterns."""
|
| 35 |
+
np.random.seed(42)
|
| 36 |
+
|
| 37 |
+
locations = {
|
| 38 |
+
'pickup': ['West Campus', 'The Drag', 'Market District', 'Sixth Street', 'East End',
|
| 39 |
+
'Downtown', 'Govalle', 'Hancock', 'South Lamar', 'Warehouse District'],
|
| 40 |
+
'dropoff': ['The Aquarium on 6th', 'Wiggle Room', "Shakespeare's", 'Mayfair Austin',
|
| 41 |
+
'Latchkey', '6013 Loyola Ln', "Buford's", 'Darrell K Royal Texas Memorial Stadium',
|
| 42 |
+
'LUNA Rooftop', 'University of Texas KA house', 'Green Light Social', "The Cat's Pajamas"]
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
passenger_choices = [14, 8, 7, 10, 9, 12, 11, 13, 6, 5, 4, 3, 2, 1]
|
| 46 |
+
passenger_weights = [0.173, 0.128, 0.120, 0.115, 0.113, 0.087, 0.085, 0.077, 0.063, 0.028, 0.007, 0.004, 0.001, 0.001]
|
| 47 |
+
|
| 48 |
+
hour_choices = [22, 23, 21, 19, 0, 20, 18, 1, 2, 17, 16, 3]
|
| 49 |
+
hour_weights = [0.25, 0.23, 0.19, 0.11, 0.08, 0.06, 0.05, 0.03, 0.02, 0.01, 0.01, 0.01]
|
| 50 |
+
|
| 51 |
+
sample_data = []
|
| 52 |
+
for i in range(2000):
|
| 53 |
+
passengers = np.random.choice(passenger_choices, p=passenger_weights)
|
| 54 |
+
hour = np.random.choice(hour_choices, p=hour_weights)
|
| 55 |
+
|
| 56 |
+
pickup_lat = np.random.normal(30.2672, 0.02)
|
| 57 |
+
pickup_lng = np.random.normal(-97.7431, 0.02)
|
| 58 |
+
dropoff_lat = np.random.normal(30.2672, 0.02)
|
| 59 |
+
dropoff_lng = np.random.normal(-97.7431, 0.02)
|
| 60 |
+
|
| 61 |
+
day = np.random.randint(1, 31)
|
| 62 |
+
minute = np.random.randint(0, 60)
|
| 63 |
+
|
| 64 |
+
sample_data.append({
|
| 65 |
+
'Trip ID': 734889 - i,
|
| 66 |
+
'Booking User ID': np.random.randint(10000, 999999),
|
| 67 |
+
'Pick Up Latitude': pickup_lat,
|
| 68 |
+
'Pick Up Longitude': pickup_lng,
|
| 69 |
+
'Drop Off Latitude': dropoff_lat,
|
| 70 |
+
'Drop Off Longitude': dropoff_lng,
|
| 71 |
+
'Pick Up Address': f"{np.random.choice(locations['pickup'])}, Austin, TX",
|
| 72 |
+
'Drop Off Address': f"{np.random.choice(locations['dropoff'])}, Austin, TX",
|
| 73 |
+
'Trip Date and Time': f"9/{day}/25 {hour}:{minute:02d}",
|
| 74 |
+
'Total Passengers': passengers
|
| 75 |
+
})
|
| 76 |
+
|
| 77 |
+
self.df = pd.DataFrame(sample_data)
|
| 78 |
+
self._clean_data()
|
| 79 |
+
self._extract_temporal_features()
|
| 80 |
+
self._extract_location_features()
|
| 81 |
+
self._calculate_insights()
|
| 82 |
+
|
| 83 |
+
def _clean_data(self):
|
| 84 |
+
"""Clean and standardize the data."""
|
| 85 |
+
self.df = self.df.dropna(subset=['Total Passengers', 'Trip Date and Time'])
|
| 86 |
+
|
| 87 |
+
self.df['Total Passengers'] = self.df['Total Passengers'].astype(int)
|
| 88 |
+
|
| 89 |
+
self.df['pickup_main'] = self.df['Pick Up Address'].apply(self._extract_main_location)
|
| 90 |
+
self.df['dropoff_main'] = self.df['Drop Off Address'].apply(self._extract_main_location)
|
| 91 |
+
|
| 92 |
+
def _extract_main_location(self, address: str) -> str:
|
| 93 |
+
"""Extract the main location name from an address."""
|
| 94 |
+
if pd.isna(address):
|
| 95 |
+
return "Unknown"
|
| 96 |
+
return address.split(',')[0].strip()
|
| 97 |
+
|
| 98 |
+
def _extract_temporal_features(self):
|
| 99 |
+
"""Extract temporal features from trip data."""
|
| 100 |
+
self.df['datetime'] = pd.to_datetime(self.df['Trip Date and Time'], format='%m/%d/%y %H:%M')
|
| 101 |
+
self.df['hour'] = self.df['datetime'].dt.hour
|
| 102 |
+
self.df['day_of_week'] = self.df['datetime'].dt.day_name()
|
| 103 |
+
self.df['date'] = self.df['datetime'].dt.date
|
| 104 |
+
|
| 105 |
+
self.df['time_category'] = self.df['hour'].apply(self._categorize_time)
|
| 106 |
+
|
| 107 |
+
def _categorize_time(self, hour: int) -> str:
|
| 108 |
+
"""Categorize hour into time periods."""
|
| 109 |
+
if 6 <= hour < 12:
|
| 110 |
+
return "Morning"
|
| 111 |
+
elif 12 <= hour < 17:
|
| 112 |
+
return "Afternoon"
|
| 113 |
+
elif 17 <= hour < 21:
|
| 114 |
+
return "Evening"
|
| 115 |
+
elif 21 <= hour <= 23:
|
| 116 |
+
return "Night"
|
| 117 |
+
else:
|
| 118 |
+
return "Late Night"
|
| 119 |
+
|
| 120 |
+
def _extract_location_features(self):
|
| 121 |
+
"""Extract location-based features."""
|
| 122 |
+
self.df['group_category'] = self.df['Total Passengers'].apply(self._categorize_group_size)
|
| 123 |
+
|
| 124 |
+
self.df['is_entertainment'] = self.df['dropoff_main'].apply(self._is_entertainment_venue)
|
| 125 |
+
self.df['is_campus'] = self.df['pickup_main'].apply(self._is_campus_location)
|
| 126 |
+
|
| 127 |
+
def _categorize_group_size(self, passengers: int) -> str:
|
| 128 |
+
"""Categorize group size."""
|
| 129 |
+
if passengers <= 4:
|
| 130 |
+
return "Small (1-4)"
|
| 131 |
+
elif passengers <= 8:
|
| 132 |
+
return "Medium (5-8)"
|
| 133 |
+
elif passengers <= 12:
|
| 134 |
+
return "Large (9-12)"
|
| 135 |
+
else:
|
| 136 |
+
return "Extra Large (13+)"
|
| 137 |
+
|
| 138 |
+
def _is_entertainment_venue(self, location: str) -> bool:
|
| 139 |
+
"""Check if location is an entertainment venue."""
|
| 140 |
+
entertainment_keywords = ['bar', 'club', 'lounge', 'aquarium', 'rooftop', 'social', 'pub']
|
| 141 |
+
return any(keyword in location.lower() for keyword in entertainment_keywords)
|
| 142 |
+
|
| 143 |
+
def _is_campus_location(self, location: str) -> bool:
|
| 144 |
+
"""Check if location is campus-related."""
|
| 145 |
+
campus_keywords = ['campus', 'university', 'drag', 'west campus']
|
| 146 |
+
return any(keyword in location.lower() for keyword in campus_keywords)
|
| 147 |
+
|
| 148 |
+
def _calculate_insights(self):
|
| 149 |
+
"""Calculate key insights from the data."""
|
| 150 |
+
self.insights = {
|
| 151 |
+
'total_trips': len(self.df),
|
| 152 |
+
'avg_group_size': self.df['Total Passengers'].mean(),
|
| 153 |
+
'peak_hour': self.df['hour'].mode().iloc[0],
|
| 154 |
+
'large_groups_count': len(self.df[self.df['Total Passengers'] >= 6]),
|
| 155 |
+
'large_groups_pct': (len(self.df[self.df['Total Passengers'] >= 6]) / len(self.df)) * 100,
|
| 156 |
+
'top_pickups': list(self.df['pickup_main'].value_counts().head(10).items()),
|
| 157 |
+
'top_dropoffs': list(self.df['dropoff_main'].value_counts().head(10).items()),
|
| 158 |
+
'hourly_distribution': self.df['hour'].value_counts().sort_index().to_dict(),
|
| 159 |
+
'group_size_distribution': self.df['Total Passengers'].value_counts().sort_index().to_dict()
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
def get_quick_insights(self) -> Dict[str, Any]:
|
| 163 |
+
"""Get quick insights for dashboard."""
|
| 164 |
+
return self.insights
|
| 165 |
+
|
| 166 |
+
def query_data(self, query_params: Dict[str, Any]) -> pd.DataFrame:
|
| 167 |
+
"""Query the data based on parameters."""
|
| 168 |
+
filtered_df = self.df.copy()
|
| 169 |
+
|
| 170 |
+
if 'pickup_location' in query_params:
|
| 171 |
+
filtered_df = filtered_df[filtered_df['pickup_main'].str.contains(
|
| 172 |
+
query_params['pickup_location'], case=False, na=False)]
|
| 173 |
+
|
| 174 |
+
if 'dropoff_location' in query_params:
|
| 175 |
+
filtered_df = filtered_df[filtered_df['dropoff_main'].str.contains(
|
| 176 |
+
query_params['dropoff_location'], case=False, na=False)]
|
| 177 |
+
|
| 178 |
+
if 'hour_range' in query_params:
|
| 179 |
+
start_hour, end_hour = query_params['hour_range']
|
| 180 |
+
filtered_df = filtered_df[
|
| 181 |
+
(filtered_df['hour'] >= start_hour) & (filtered_df['hour'] <= end_hour)]
|
| 182 |
+
|
| 183 |
+
if 'min_passengers' in query_params:
|
| 184 |
+
filtered_df = filtered_df[filtered_df['Total Passengers'] >= query_params['min_passengers']]
|
| 185 |
+
|
| 186 |
+
if 'max_passengers' in query_params:
|
| 187 |
+
filtered_df = filtered_df[filtered_df['Total Passengers'] <= query_params['max_passengers']]
|
| 188 |
+
|
| 189 |
+
if 'date_range' in query_params:
|
| 190 |
+
start_date, end_date = query_params['date_range']
|
| 191 |
+
filtered_df = filtered_df[
|
| 192 |
+
(filtered_df['date'] >= start_date) & (filtered_df['date'] <= end_date)]
|
| 193 |
+
|
| 194 |
+
return filtered_df
|
| 195 |
+
|
| 196 |
+
def get_location_stats(self, location: str, location_type: str = 'both') -> Dict[str, Any]:
|
| 197 |
+
"""Get statistics for a specific location."""
|
| 198 |
+
if location_type in ['pickup', 'both']:
|
| 199 |
+
pickup_data = self.df[self.df['pickup_main'].str.contains(location, case=False, na=False)]
|
| 200 |
+
else:
|
| 201 |
+
pickup_data = pd.DataFrame()
|
| 202 |
+
|
| 203 |
+
if location_type in ['dropoff', 'both']:
|
| 204 |
+
dropoff_data = self.df[self.df['dropoff_main'].str.contains(location, case=False, na=False)]
|
| 205 |
+
else:
|
| 206 |
+
dropoff_data = pd.DataFrame()
|
| 207 |
+
|
| 208 |
+
return {
|
| 209 |
+
'pickup_count': len(pickup_data),
|
| 210 |
+
'dropoff_count': len(dropoff_data),
|
| 211 |
+
'avg_group_size_pickup': pickup_data['Total Passengers'].mean() if len(pickup_data) > 0 else 0,
|
| 212 |
+
'avg_group_size_dropoff': dropoff_data['Total Passengers'].mean() if len(dropoff_data) > 0 else 0,
|
| 213 |
+
'peak_hours_pickup': pickup_data['hour'].mode().tolist() if len(pickup_data) > 0 else [],
|
| 214 |
+
'peak_hours_dropoff': dropoff_data['hour'].mode().tolist() if len(dropoff_data) > 0 else []
|
| 215 |
+
}
|
| 216 |
+
|
| 217 |
+
def get_time_patterns(self, group_size_filter: int = None) -> Dict[str, Any]:
|
| 218 |
+
"""Get time-based patterns."""
|
| 219 |
+
data = self.df.copy()
|
| 220 |
+
|
| 221 |
+
if group_size_filter:
|
| 222 |
+
data = data[data['Total Passengers'] >= group_size_filter]
|
| 223 |
+
|
| 224 |
+
return {
|
| 225 |
+
'hourly_counts': data['hour'].value_counts().sort_index().to_dict(),
|
| 226 |
+
'daily_counts': data['day_of_week'].value_counts().to_dict(),
|
| 227 |
+
'time_category_counts': data['time_category'].value_counts().to_dict()
|
| 228 |
+
}
|
fetii_data.csv
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
pyproject.toml
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[project]
|
| 2 |
+
name = "fetiiai"
|
| 3 |
+
version = "0.1.0"
|
| 4 |
+
description = "Add your description here"
|
| 5 |
+
readme = "README.md"
|
| 6 |
+
requires-python = ">=3.13"
|
| 7 |
+
dependencies = [
|
| 8 |
+
"numpy>=2.3.3",
|
| 9 |
+
"pandas>=2.3.2",
|
| 10 |
+
"plotly>=6.3.0",
|
| 11 |
+
"python-dateutil>=2.9.0.post0",
|
| 12 |
+
"streamlit>=1.49.1",
|
| 13 |
+
]
|
requirements.txt
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
gradio
|
| 2 |
+
pandas
|
| 3 |
+
plotly
|
| 4 |
+
numpy
|
| 5 |
+
python-dateutil
|
utils.py
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Utility functions for Fetii AI Chatbot
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import pandas as pd
|
| 6 |
+
import numpy as np
|
| 7 |
+
from datetime import datetime, timedelta
|
| 8 |
+
import re
|
| 9 |
+
from typing import List, Dict, Any, Tuple, Optional
|
| 10 |
+
import config
|
| 11 |
+
|
| 12 |
+
def clean_location_name(location: str) -> str:
|
| 13 |
+
"""Clean and standardize location names."""
|
| 14 |
+
if pd.isna(location) or not location:
|
| 15 |
+
return "Unknown"
|
| 16 |
+
|
| 17 |
+
cleaned = location.strip().title()
|
| 18 |
+
|
| 19 |
+
suffixes_to_remove = [", Austin, TX", ", Austin, Texas", ", USA", ", United States"]
|
| 20 |
+
for suffix in suffixes_to_remove:
|
| 21 |
+
if cleaned.endswith(suffix):
|
| 22 |
+
cleaned = cleaned[:-len(suffix)]
|
| 23 |
+
|
| 24 |
+
return cleaned
|
| 25 |
+
|
| 26 |
+
def categorize_location(location: str) -> str:
|
| 27 |
+
"""Categorize location type based on keywords."""
|
| 28 |
+
location_lower = location.lower()
|
| 29 |
+
|
| 30 |
+
for category, keywords in config.LOCATION_CATEGORIES.items():
|
| 31 |
+
if any(keyword in location_lower for keyword in keywords):
|
| 32 |
+
return category.title()
|
| 33 |
+
|
| 34 |
+
return "Other"
|
| 35 |
+
|
| 36 |
+
def calculate_distance(lat1: float, lon1: float, lat2: float, lon2: float) -> float:
|
| 37 |
+
"""Calculate approximate distance between two coordinates in kilometers."""
|
| 38 |
+
lat_diff = lat2 - lat1
|
| 39 |
+
lon_diff = lon2 - lon1
|
| 40 |
+
distance = np.sqrt(lat_diff**2 + lon_diff**2) * 111
|
| 41 |
+
return round(distance, 2)
|
| 42 |
+
|
| 43 |
+
def format_time(hour: int) -> str:
|
| 44 |
+
"""Format hour as readable time string."""
|
| 45 |
+
if hour == 0:
|
| 46 |
+
return "12:00 AM"
|
| 47 |
+
elif hour < 12:
|
| 48 |
+
return f"{hour}:00 AM"
|
| 49 |
+
elif hour == 12:
|
| 50 |
+
return "12:00 PM"
|
| 51 |
+
else:
|
| 52 |
+
return f"{hour-12}:00 PM"
|
| 53 |
+
|
| 54 |
+
def get_time_category(hour: int) -> str:
|
| 55 |
+
"""Get time category for a given hour."""
|
| 56 |
+
for category, (start, end) in config.TIME_CATEGORIES.items():
|
| 57 |
+
if start <= hour < end:
|
| 58 |
+
return category.replace('_', ' ').title()
|
| 59 |
+
return "Unknown"
|
| 60 |
+
|
| 61 |
+
def get_group_size_category(passengers: int) -> str:
|
| 62 |
+
"""Get group size category for passenger count."""
|
| 63 |
+
for category, (min_size, max_size) in config.GROUP_SIZE_CATEGORIES.items():
|
| 64 |
+
if min_size <= passengers <= max_size:
|
| 65 |
+
return category.replace('_', ' ').title()
|
| 66 |
+
return "Unknown"
|
| 67 |
+
|
| 68 |
+
def extract_numbers_from_text(text: str) -> List[int]:
|
| 69 |
+
"""Extract all numbers from text."""
|
| 70 |
+
numbers = re.findall(r'\d+', text)
|
| 71 |
+
return [int(num) for num in numbers]
|
| 72 |
+
|
| 73 |
+
def parse_date_string(date_str: str) -> Optional[datetime]:
|
| 74 |
+
"""Parse various date string formats."""
|
| 75 |
+
formats = [
|
| 76 |
+
'%m/%d/%y %H:%M',
|
| 77 |
+
'%m/%d/%Y %H:%M',
|
| 78 |
+
'%Y-%m-%d %H:%M:%S',
|
| 79 |
+
'%Y-%m-%d %H:%M',
|
| 80 |
+
'%m/%d/%y %H:%M:%S'
|
| 81 |
+
]
|
| 82 |
+
|
| 83 |
+
for fmt in formats:
|
| 84 |
+
try:
|
| 85 |
+
return datetime.strptime(date_str, fmt)
|
| 86 |
+
except ValueError:
|
| 87 |
+
continue
|
| 88 |
+
|
| 89 |
+
return None
|
| 90 |
+
|
| 91 |
+
def generate_insights(data: pd.DataFrame) -> Dict[str, Any]:
|
| 92 |
+
"""Generate comprehensive insights from trip data."""
|
| 93 |
+
insights = {}
|
| 94 |
+
|
| 95 |
+
insights['total_trips'] = len(data)
|
| 96 |
+
insights['total_passengers'] = data['Total Passengers'].sum()
|
| 97 |
+
insights['avg_group_size'] = data['Total Passengers'].mean()
|
| 98 |
+
insights['median_group_size'] = data['Total Passengers'].median()
|
| 99 |
+
|
| 100 |
+
if 'hour' in data.columns:
|
| 101 |
+
insights['peak_hour'] = data['hour'].mode().iloc[0] if len(data['hour'].mode()) > 0 else None
|
| 102 |
+
insights['hour_distribution'] = data['hour'].value_counts().to_dict()
|
| 103 |
+
|
| 104 |
+
if 'pickup_main' in data.columns:
|
| 105 |
+
insights['top_pickups'] = data['pickup_main'].value_counts().head(10).to_dict()
|
| 106 |
+
insights['unique_pickup_locations'] = data['pickup_main'].nunique()
|
| 107 |
+
|
| 108 |
+
if 'dropoff_main' in data.columns:
|
| 109 |
+
insights['top_dropoffs'] = data['dropoff_main'].value_counts().head(10).to_dict()
|
| 110 |
+
insights['unique_dropoff_locations'] = data['dropoff_main'].nunique()
|
| 111 |
+
|
| 112 |
+
insights['group_size_distribution'] = data['Total Passengers'].value_counts().to_dict()
|
| 113 |
+
insights['large_groups'] = len(data[data['Total Passengers'] >= config.ANALYSIS_THRESHOLDS['large_group_threshold']])
|
| 114 |
+
insights['large_groups_percentage'] = (insights['large_groups'] / insights['total_trips']) * 100
|
| 115 |
+
|
| 116 |
+
if 'date' in data.columns:
|
| 117 |
+
insights['date_range'] = {
|
| 118 |
+
'start': data['date'].min(),
|
| 119 |
+
'end': data['date'].max(),
|
| 120 |
+
'days_covered': (data['date'].max() - data['date'].min()).days + 1
|
| 121 |
+
}
|
| 122 |
+
insights['daily_average'] = insights['total_trips'] / insights['date_range']['days_covered']
|
| 123 |
+
|
| 124 |
+
return insights
|
| 125 |
+
|
| 126 |
+
def format_number(number: float, decimals: int = 1) -> str:
|
| 127 |
+
"""Format numbers for display."""
|
| 128 |
+
if number >= 1000000:
|
| 129 |
+
return f"{number/1000000:.{decimals}f}M"
|
| 130 |
+
elif number >= 1000:
|
| 131 |
+
return f"{number/1000:.{decimals}f}K"
|
| 132 |
+
else:
|
| 133 |
+
return f"{number:.{decimals}f}" if decimals > 0 else str(int(number))
|
| 134 |
+
|
| 135 |
+
def create_summary_stats(data: pd.DataFrame) -> Dict[str, str]:
|
| 136 |
+
"""Create formatted summary statistics for display."""
|
| 137 |
+
insights = generate_insights(data)
|
| 138 |
+
|
| 139 |
+
return {
|
| 140 |
+
'Total Trips': format_number(insights['total_trips'], 0),
|
| 141 |
+
'Total Passengers': format_number(insights['total_passengers'], 0),
|
| 142 |
+
'Average Group Size': f"{insights['avg_group_size']:.1f}",
|
| 143 |
+
'Peak Hour': format_time(insights.get('peak_hour', 22)),
|
| 144 |
+
'Large Groups': f"{insights['large_groups_percentage']:.1f}%",
|
| 145 |
+
'Unique Pickup Locations': format_number(insights.get('unique_pickup_locations', 0), 0),
|
| 146 |
+
'Unique Destinations': format_number(insights.get('unique_dropoff_locations', 0), 0),
|
| 147 |
+
'Daily Average': f"{insights.get('daily_average', 0):.1f} trips/day"
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
def validate_data(data: pd.DataFrame) -> Tuple[bool, List[str]]:
|
| 151 |
+
"""Validate data quality and return issues found."""
|
| 152 |
+
issues = []
|
| 153 |
+
|
| 154 |
+
required_columns = ['Trip ID', 'Total Passengers', 'Trip Date and Time']
|
| 155 |
+
missing_columns = [col for col in required_columns if col not in data.columns]
|
| 156 |
+
if missing_columns:
|
| 157 |
+
issues.append(f"Missing required columns: {', '.join(missing_columns)}")
|
| 158 |
+
|
| 159 |
+
if len(data) == 0:
|
| 160 |
+
issues.append("Dataset is empty")
|
| 161 |
+
return False, issues
|
| 162 |
+
|
| 163 |
+
if 'Total Passengers' in data.columns:
|
| 164 |
+
invalid_passengers = data[
|
| 165 |
+
(data['Total Passengers'] < 1) |
|
| 166 |
+
(data['Total Passengers'] > 20) |
|
| 167 |
+
(data['Total Passengers'].isna())
|
| 168 |
+
]
|
| 169 |
+
if len(invalid_passengers) > 0:
|
| 170 |
+
issues.append(f"Found {len(invalid_passengers)} trips with invalid passenger counts")
|
| 171 |
+
|
| 172 |
+
if 'Trip Date and Time' in data.columns:
|
| 173 |
+
invalid_dates = 0
|
| 174 |
+
for date_str in data['Trip Date and Time'].dropna():
|
| 175 |
+
if parse_date_string(str(date_str)) is None:
|
| 176 |
+
invalid_dates += 1
|
| 177 |
+
if invalid_dates > 0:
|
| 178 |
+
issues.append(f"Found {invalid_dates} trips with invalid date formats")
|
| 179 |
+
|
| 180 |
+
if 'Trip ID' in data.columns:
|
| 181 |
+
duplicates = data['Trip ID'].duplicated().sum()
|
| 182 |
+
if duplicates > 0:
|
| 183 |
+
issues.append(f"Found {duplicates} duplicate trip IDs")
|
| 184 |
+
|
| 185 |
+
return len(issues) == 0, issues
|
| 186 |
+
|
| 187 |
+
def create_export_data(data: pd.DataFrame, insights: Dict[str, Any], format_type: str = 'csv') -> Any:
|
| 188 |
+
"""Create data for export in specified format."""
|
| 189 |
+
if format_type == 'csv':
|
| 190 |
+
return data.to_csv(index=False)
|
| 191 |
+
|
| 192 |
+
elif format_type == 'json':
|
| 193 |
+
export_data = {
|
| 194 |
+
'metadata': {
|
| 195 |
+
'export_date': datetime.now().isoformat(),
|
| 196 |
+
'total_records': len(data),
|
| 197 |
+
'insights': insights
|
| 198 |
+
},
|
| 199 |
+
'data': data.to_dict('records')
|
| 200 |
+
}
|
| 201 |
+
return export_data
|
| 202 |
+
|
| 203 |
+
elif format_type == 'summary':
|
| 204 |
+
summary = create_summary_stats(data)
|
| 205 |
+
return summary
|
| 206 |
+
|
| 207 |
+
else:
|
| 208 |
+
raise ValueError(f"Unsupported export format: {format_type}")
|
| 209 |
+
|
| 210 |
+
def search_locations(query: str, locations: List[str], max_results: int = 5) -> List[str]:
|
| 211 |
+
"""Search for locations matching a query."""
|
| 212 |
+
query_lower = query.lower()
|
| 213 |
+
matches = []
|
| 214 |
+
|
| 215 |
+
for location in locations:
|
| 216 |
+
if query_lower == location.lower():
|
| 217 |
+
matches.append(location)
|
| 218 |
+
|
| 219 |
+
for location in locations:
|
| 220 |
+
if query_lower in location.lower() and location not in matches:
|
| 221 |
+
matches.append(location)
|
| 222 |
+
|
| 223 |
+
query_words = query_lower.split()
|
| 224 |
+
for location in locations:
|
| 225 |
+
location_lower = location.lower()
|
| 226 |
+
if (any(word in location_lower for word in query_words) and
|
| 227 |
+
location not in matches):
|
| 228 |
+
matches.append(location)
|
| 229 |
+
|
| 230 |
+
return matches[:max_results]
|
| 231 |
+
|
| 232 |
+
def get_color_palette(num_colors: int) -> List[str]:
|
| 233 |
+
"""Get a color palette for visualizations."""
|
| 234 |
+
base_colors = [
|
| 235 |
+
'#667eea', '#764ba2', '#f093fb', '#f5576c',
|
| 236 |
+
'#4facfe', '#00f2fe', '#43e97b', '#38f9d7',
|
| 237 |
+
'#ffecd2', '#fcb69f', '#a8edea', '#fed6e3'
|
| 238 |
+
]
|
| 239 |
+
|
| 240 |
+
if num_colors <= len(base_colors):
|
| 241 |
+
return base_colors[:num_colors]
|
| 242 |
+
|
| 243 |
+
import colorsys
|
| 244 |
+
additional_colors = []
|
| 245 |
+
for i in range(num_colors - len(base_colors)):
|
| 246 |
+
hue = (i * 0.618033988749895) % 1
|
| 247 |
+
rgb = colorsys.hsv_to_rgb(hue, 0.7, 0.9)
|
| 248 |
+
hex_color = '#%02x%02x%02x' % tuple(int(c * 255) for c in rgb)
|
| 249 |
+
additional_colors.append(hex_color)
|
| 250 |
+
|
| 251 |
+
return base_colors + additional_colors
|
uv.lock
ADDED
|
@@ -0,0 +1,686 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version = 1
|
| 2 |
+
revision = 1
|
| 3 |
+
requires-python = ">=3.13"
|
| 4 |
+
|
| 5 |
+
[[package]]
|
| 6 |
+
name = "altair"
|
| 7 |
+
version = "5.5.0"
|
| 8 |
+
source = { registry = "https://pypi.org/simple" }
|
| 9 |
+
dependencies = [
|
| 10 |
+
{ name = "jinja2" },
|
| 11 |
+
{ name = "jsonschema" },
|
| 12 |
+
{ name = "narwhals" },
|
| 13 |
+
{ name = "packaging" },
|
| 14 |
+
{ name = "typing-extensions", marker = "python_full_version < '3.14'" },
|
| 15 |
+
]
|
| 16 |
+
sdist = { url = "https://files.pythonhosted.org/packages/16/b1/f2969c7bdb8ad8bbdda031687defdce2c19afba2aa2c8e1d2a17f78376d8/altair-5.5.0.tar.gz", hash = "sha256:d960ebe6178c56de3855a68c47b516be38640b73fb3b5111c2a9ca90546dd73d", size = 705305 }
|
| 17 |
+
wheels = [
|
| 18 |
+
{ url = "https://files.pythonhosted.org/packages/aa/f3/0b6ced594e51cc95d8c1fc1640d3623770d01e4969d29c0bd09945fafefa/altair-5.5.0-py3-none-any.whl", hash = "sha256:91a310b926508d560fe0148d02a194f38b824122641ef528113d029fcd129f8c", size = 731200 },
|
| 19 |
+
]
|
| 20 |
+
|
| 21 |
+
[[package]]
|
| 22 |
+
name = "attrs"
|
| 23 |
+
version = "25.3.0"
|
| 24 |
+
source = { registry = "https://pypi.org/simple" }
|
| 25 |
+
sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032 }
|
| 26 |
+
wheels = [
|
| 27 |
+
{ url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815 },
|
| 28 |
+
]
|
| 29 |
+
|
| 30 |
+
[[package]]
|
| 31 |
+
name = "blinker"
|
| 32 |
+
version = "1.9.0"
|
| 33 |
+
source = { registry = "https://pypi.org/simple" }
|
| 34 |
+
sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460 }
|
| 35 |
+
wheels = [
|
| 36 |
+
{ url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458 },
|
| 37 |
+
]
|
| 38 |
+
|
| 39 |
+
[[package]]
|
| 40 |
+
name = "cachetools"
|
| 41 |
+
version = "6.2.0"
|
| 42 |
+
source = { registry = "https://pypi.org/simple" }
|
| 43 |
+
sdist = { url = "https://files.pythonhosted.org/packages/9d/61/e4fad8155db4a04bfb4734c7c8ff0882f078f24294d42798b3568eb63bff/cachetools-6.2.0.tar.gz", hash = "sha256:38b328c0889450f05f5e120f56ab68c8abaf424e1275522b138ffc93253f7e32", size = 30988 }
|
| 44 |
+
wheels = [
|
| 45 |
+
{ url = "https://files.pythonhosted.org/packages/6c/56/3124f61d37a7a4e7cc96afc5492c78ba0cb551151e530b54669ddd1436ef/cachetools-6.2.0-py3-none-any.whl", hash = "sha256:1c76a8960c0041fcc21097e357f882197c79da0dbff766e7317890a65d7d8ba6", size = 11276 },
|
| 46 |
+
]
|
| 47 |
+
|
| 48 |
+
[[package]]
|
| 49 |
+
name = "certifi"
|
| 50 |
+
version = "2025.8.3"
|
| 51 |
+
source = { registry = "https://pypi.org/simple" }
|
| 52 |
+
sdist = { url = "https://files.pythonhosted.org/packages/dc/67/960ebe6bf230a96cda2e0abcf73af550ec4f090005363542f0765df162e0/certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", size = 162386 }
|
| 53 |
+
wheels = [
|
| 54 |
+
{ url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216 },
|
| 55 |
+
]
|
| 56 |
+
|
| 57 |
+
[[package]]
|
| 58 |
+
name = "charset-normalizer"
|
| 59 |
+
version = "3.4.3"
|
| 60 |
+
source = { registry = "https://pypi.org/simple" }
|
| 61 |
+
sdist = { url = "https://files.pythonhosted.org/packages/83/2d/5fd176ceb9b2fc619e63405525573493ca23441330fcdaee6bef9460e924/charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14", size = 122371 }
|
| 62 |
+
wheels = [
|
| 63 |
+
{ url = "https://files.pythonhosted.org/packages/65/ca/2135ac97709b400c7654b4b764daf5c5567c2da45a30cdd20f9eefe2d658/charset_normalizer-3.4.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe", size = 205326 },
|
| 64 |
+
{ url = "https://files.pythonhosted.org/packages/71/11/98a04c3c97dd34e49c7d247083af03645ca3730809a5509443f3c37f7c99/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8", size = 146008 },
|
| 65 |
+
{ url = "https://files.pythonhosted.org/packages/60/f5/4659a4cb3c4ec146bec80c32d8bb16033752574c20b1252ee842a95d1a1e/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9", size = 159196 },
|
| 66 |
+
{ url = "https://files.pythonhosted.org/packages/86/9e/f552f7a00611f168b9a5865a1414179b2c6de8235a4fa40189f6f79a1753/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31", size = 156819 },
|
| 67 |
+
{ url = "https://files.pythonhosted.org/packages/7e/95/42aa2156235cbc8fa61208aded06ef46111c4d3f0de233107b3f38631803/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f", size = 151350 },
|
| 68 |
+
{ url = "https://files.pythonhosted.org/packages/c2/a9/3865b02c56f300a6f94fc631ef54f0a8a29da74fb45a773dfd3dcd380af7/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927", size = 148644 },
|
| 69 |
+
{ url = "https://files.pythonhosted.org/packages/77/d9/cbcf1a2a5c7d7856f11e7ac2d782aec12bdfea60d104e60e0aa1c97849dc/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9", size = 160468 },
|
| 70 |
+
{ url = "https://files.pythonhosted.org/packages/f6/42/6f45efee8697b89fda4d50580f292b8f7f9306cb2971d4b53f8914e4d890/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5", size = 158187 },
|
| 71 |
+
{ url = "https://files.pythonhosted.org/packages/70/99/f1c3bdcfaa9c45b3ce96f70b14f070411366fa19549c1d4832c935d8e2c3/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc", size = 152699 },
|
| 72 |
+
{ url = "https://files.pythonhosted.org/packages/a3/ad/b0081f2f99a4b194bcbb1934ef3b12aa4d9702ced80a37026b7607c72e58/charset_normalizer-3.4.3-cp313-cp313-win32.whl", hash = "sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce", size = 99580 },
|
| 73 |
+
{ url = "https://files.pythonhosted.org/packages/9a/8f/ae790790c7b64f925e5c953b924aaa42a243fb778fed9e41f147b2a5715a/charset_normalizer-3.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef", size = 107366 },
|
| 74 |
+
{ url = "https://files.pythonhosted.org/packages/8e/91/b5a06ad970ddc7a0e513112d40113e834638f4ca1120eb727a249fb2715e/charset_normalizer-3.4.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15", size = 204342 },
|
| 75 |
+
{ url = "https://files.pythonhosted.org/packages/ce/ec/1edc30a377f0a02689342f214455c3f6c2fbedd896a1d2f856c002fc3062/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db", size = 145995 },
|
| 76 |
+
{ url = "https://files.pythonhosted.org/packages/17/e5/5e67ab85e6d22b04641acb5399c8684f4d37caf7558a53859f0283a650e9/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d", size = 158640 },
|
| 77 |
+
{ url = "https://files.pythonhosted.org/packages/f1/e5/38421987f6c697ee3722981289d554957c4be652f963d71c5e46a262e135/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096", size = 156636 },
|
| 78 |
+
{ url = "https://files.pythonhosted.org/packages/a0/e4/5a075de8daa3ec0745a9a3b54467e0c2967daaaf2cec04c845f73493e9a1/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa", size = 150939 },
|
| 79 |
+
{ url = "https://files.pythonhosted.org/packages/02/f7/3611b32318b30974131db62b4043f335861d4d9b49adc6d57c1149cc49d4/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049", size = 148580 },
|
| 80 |
+
{ url = "https://files.pythonhosted.org/packages/7e/61/19b36f4bd67f2793ab6a99b979b4e4f3d8fc754cbdffb805335df4337126/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0", size = 159870 },
|
| 81 |
+
{ url = "https://files.pythonhosted.org/packages/06/57/84722eefdd338c04cf3030ada66889298eaedf3e7a30a624201e0cbe424a/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92", size = 157797 },
|
| 82 |
+
{ url = "https://files.pythonhosted.org/packages/72/2a/aff5dd112b2f14bcc3462c312dce5445806bfc8ab3a7328555da95330e4b/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16", size = 152224 },
|
| 83 |
+
{ url = "https://files.pythonhosted.org/packages/b7/8c/9839225320046ed279c6e839d51f028342eb77c91c89b8ef2549f951f3ec/charset_normalizer-3.4.3-cp314-cp314-win32.whl", hash = "sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce", size = 100086 },
|
| 84 |
+
{ url = "https://files.pythonhosted.org/packages/ee/7a/36fbcf646e41f710ce0a563c1c9a343c6edf9be80786edeb15b6f62e17db/charset_normalizer-3.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c", size = 107400 },
|
| 85 |
+
{ url = "https://files.pythonhosted.org/packages/8a/1f/f041989e93b001bc4e44bb1669ccdcf54d3f00e628229a85b08d330615c5/charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a", size = 53175 },
|
| 86 |
+
]
|
| 87 |
+
|
| 88 |
+
[[package]]
|
| 89 |
+
name = "click"
|
| 90 |
+
version = "8.3.0"
|
| 91 |
+
source = { registry = "https://pypi.org/simple" }
|
| 92 |
+
dependencies = [
|
| 93 |
+
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
| 94 |
+
]
|
| 95 |
+
sdist = { url = "https://files.pythonhosted.org/packages/46/61/de6cd827efad202d7057d93e0fed9294b96952e188f7384832791c7b2254/click-8.3.0.tar.gz", hash = "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4", size = 276943 }
|
| 96 |
+
wheels = [
|
| 97 |
+
{ url = "https://files.pythonhosted.org/packages/db/d3/9dcc0f5797f070ec8edf30fbadfb200e71d9db6b84d211e3b2085a7589a0/click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc", size = 107295 },
|
| 98 |
+
]
|
| 99 |
+
|
| 100 |
+
[[package]]
|
| 101 |
+
name = "colorama"
|
| 102 |
+
version = "0.4.6"
|
| 103 |
+
source = { registry = "https://pypi.org/simple" }
|
| 104 |
+
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 }
|
| 105 |
+
wheels = [
|
| 106 |
+
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 },
|
| 107 |
+
]
|
| 108 |
+
|
| 109 |
+
[[package]]
|
| 110 |
+
name = "fetiiai"
|
| 111 |
+
version = "0.1.0"
|
| 112 |
+
source = { virtual = "." }
|
| 113 |
+
dependencies = [
|
| 114 |
+
{ name = "numpy" },
|
| 115 |
+
{ name = "pandas" },
|
| 116 |
+
{ name = "plotly" },
|
| 117 |
+
{ name = "python-dateutil" },
|
| 118 |
+
{ name = "streamlit" },
|
| 119 |
+
]
|
| 120 |
+
|
| 121 |
+
[package.metadata]
|
| 122 |
+
requires-dist = [
|
| 123 |
+
{ name = "numpy", specifier = ">=2.3.3" },
|
| 124 |
+
{ name = "pandas", specifier = ">=2.3.2" },
|
| 125 |
+
{ name = "plotly", specifier = ">=6.3.0" },
|
| 126 |
+
{ name = "python-dateutil", specifier = ">=2.9.0.post0" },
|
| 127 |
+
{ name = "streamlit", specifier = ">=1.49.1" },
|
| 128 |
+
]
|
| 129 |
+
|
| 130 |
+
[[package]]
|
| 131 |
+
name = "gitdb"
|
| 132 |
+
version = "4.0.12"
|
| 133 |
+
source = { registry = "https://pypi.org/simple" }
|
| 134 |
+
dependencies = [
|
| 135 |
+
{ name = "smmap" },
|
| 136 |
+
]
|
| 137 |
+
sdist = { url = "https://files.pythonhosted.org/packages/72/94/63b0fc47eb32792c7ba1fe1b694daec9a63620db1e313033d18140c2320a/gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571", size = 394684 }
|
| 138 |
+
wheels = [
|
| 139 |
+
{ url = "https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf", size = 62794 },
|
| 140 |
+
]
|
| 141 |
+
|
| 142 |
+
[[package]]
|
| 143 |
+
name = "gitpython"
|
| 144 |
+
version = "3.1.45"
|
| 145 |
+
source = { registry = "https://pypi.org/simple" }
|
| 146 |
+
dependencies = [
|
| 147 |
+
{ name = "gitdb" },
|
| 148 |
+
]
|
| 149 |
+
sdist = { url = "https://files.pythonhosted.org/packages/9a/c8/dd58967d119baab745caec2f9d853297cec1989ec1d63f677d3880632b88/gitpython-3.1.45.tar.gz", hash = "sha256:85b0ee964ceddf211c41b9f27a49086010a190fd8132a24e21f362a4b36a791c", size = 215076 }
|
| 150 |
+
wheels = [
|
| 151 |
+
{ url = "https://files.pythonhosted.org/packages/01/61/d4b89fec821f72385526e1b9d9a3a0385dda4a72b206d28049e2c7cd39b8/gitpython-3.1.45-py3-none-any.whl", hash = "sha256:8908cb2e02fb3b93b7eb0f2827125cb699869470432cc885f019b8fd0fccff77", size = 208168 },
|
| 152 |
+
]
|
| 153 |
+
|
| 154 |
+
[[package]]
|
| 155 |
+
name = "idna"
|
| 156 |
+
version = "3.10"
|
| 157 |
+
source = { registry = "https://pypi.org/simple" }
|
| 158 |
+
sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 }
|
| 159 |
+
wheels = [
|
| 160 |
+
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 },
|
| 161 |
+
]
|
| 162 |
+
|
| 163 |
+
[[package]]
|
| 164 |
+
name = "jinja2"
|
| 165 |
+
version = "3.1.6"
|
| 166 |
+
source = { registry = "https://pypi.org/simple" }
|
| 167 |
+
dependencies = [
|
| 168 |
+
{ name = "markupsafe" },
|
| 169 |
+
]
|
| 170 |
+
sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115 }
|
| 171 |
+
wheels = [
|
| 172 |
+
{ url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899 },
|
| 173 |
+
]
|
| 174 |
+
|
| 175 |
+
[[package]]
|
| 176 |
+
name = "jsonschema"
|
| 177 |
+
version = "4.25.1"
|
| 178 |
+
source = { registry = "https://pypi.org/simple" }
|
| 179 |
+
dependencies = [
|
| 180 |
+
{ name = "attrs" },
|
| 181 |
+
{ name = "jsonschema-specifications" },
|
| 182 |
+
{ name = "referencing" },
|
| 183 |
+
{ name = "rpds-py" },
|
| 184 |
+
]
|
| 185 |
+
sdist = { url = "https://files.pythonhosted.org/packages/74/69/f7185de793a29082a9f3c7728268ffb31cb5095131a9c139a74078e27336/jsonschema-4.25.1.tar.gz", hash = "sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85", size = 357342 }
|
| 186 |
+
wheels = [
|
| 187 |
+
{ url = "https://files.pythonhosted.org/packages/bf/9c/8c95d856233c1f82500c2450b8c68576b4cf1c871db3afac5c34ff84e6fd/jsonschema-4.25.1-py3-none-any.whl", hash = "sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63", size = 90040 },
|
| 188 |
+
]
|
| 189 |
+
|
| 190 |
+
[[package]]
|
| 191 |
+
name = "jsonschema-specifications"
|
| 192 |
+
version = "2025.9.1"
|
| 193 |
+
source = { registry = "https://pypi.org/simple" }
|
| 194 |
+
dependencies = [
|
| 195 |
+
{ name = "referencing" },
|
| 196 |
+
]
|
| 197 |
+
sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855 }
|
| 198 |
+
wheels = [
|
| 199 |
+
{ url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437 },
|
| 200 |
+
]
|
| 201 |
+
|
| 202 |
+
[[package]]
|
| 203 |
+
name = "markupsafe"
|
| 204 |
+
version = "3.0.2"
|
| 205 |
+
source = { registry = "https://pypi.org/simple" }
|
| 206 |
+
sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537 }
|
| 207 |
+
wheels = [
|
| 208 |
+
{ url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274 },
|
| 209 |
+
{ url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352 },
|
| 210 |
+
{ url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122 },
|
| 211 |
+
{ url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085 },
|
| 212 |
+
{ url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978 },
|
| 213 |
+
{ url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208 },
|
| 214 |
+
{ url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357 },
|
| 215 |
+
{ url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344 },
|
| 216 |
+
{ url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101 },
|
| 217 |
+
{ url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603 },
|
| 218 |
+
{ url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510 },
|
| 219 |
+
{ url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486 },
|
| 220 |
+
{ url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480 },
|
| 221 |
+
{ url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914 },
|
| 222 |
+
{ url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796 },
|
| 223 |
+
{ url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473 },
|
| 224 |
+
{ url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114 },
|
| 225 |
+
{ url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098 },
|
| 226 |
+
{ url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208 },
|
| 227 |
+
{ url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739 },
|
| 228 |
+
]
|
| 229 |
+
|
| 230 |
+
[[package]]
|
| 231 |
+
name = "narwhals"
|
| 232 |
+
version = "2.5.0"
|
| 233 |
+
source = { registry = "https://pypi.org/simple" }
|
| 234 |
+
sdist = { url = "https://files.pythonhosted.org/packages/7b/b8/3cb005704866f1cc19e8d6b15d0467255821ba12d82f20ea15912672e54c/narwhals-2.5.0.tar.gz", hash = "sha256:8ae0b6f39597f14c0dc52afc98949d6f8be89b5af402d2d98101d2f7d3561418", size = 558573 }
|
| 235 |
+
wheels = [
|
| 236 |
+
{ url = "https://files.pythonhosted.org/packages/f8/5a/22741c5c0e5f6e8050242bfc2052ba68bc94b1735ed5bca35404d136d6ec/narwhals-2.5.0-py3-none-any.whl", hash = "sha256:7e213f9ca7db3f8bf6f7eff35eaee6a1cf80902997e1b78d49b7755775d8f423", size = 407296 },
|
| 237 |
+
]
|
| 238 |
+
|
| 239 |
+
[[package]]
|
| 240 |
+
name = "numpy"
|
| 241 |
+
version = "2.3.3"
|
| 242 |
+
source = { registry = "https://pypi.org/simple" }
|
| 243 |
+
sdist = { url = "https://files.pythonhosted.org/packages/d0/19/95b3d357407220ed24c139018d2518fab0a61a948e68286a25f1a4d049ff/numpy-2.3.3.tar.gz", hash = "sha256:ddc7c39727ba62b80dfdbedf400d1c10ddfa8eefbd7ec8dcb118be8b56d31029", size = 20576648 }
|
| 244 |
+
wheels = [
|
| 245 |
+
{ url = "https://files.pythonhosted.org/packages/7d/b9/984c2b1ee61a8b803bf63582b4ac4242cf76e2dbd663efeafcb620cc0ccb/numpy-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f5415fb78995644253370985342cd03572ef8620b934da27d77377a2285955bf", size = 20949588 },
|
| 246 |
+
{ url = "https://files.pythonhosted.org/packages/a6/e4/07970e3bed0b1384d22af1e9912527ecbeb47d3b26e9b6a3bced068b3bea/numpy-2.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d00de139a3324e26ed5b95870ce63be7ec7352171bc69a4cf1f157a48e3eb6b7", size = 14177802 },
|
| 247 |
+
{ url = "https://files.pythonhosted.org/packages/35/c7/477a83887f9de61f1203bad89cf208b7c19cc9fef0cebef65d5a1a0619f2/numpy-2.3.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:9dc13c6a5829610cc07422bc74d3ac083bd8323f14e2827d992f9e52e22cd6a6", size = 5106537 },
|
| 248 |
+
{ url = "https://files.pythonhosted.org/packages/52/47/93b953bd5866a6f6986344d045a207d3f1cfbad99db29f534ea9cee5108c/numpy-2.3.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:d79715d95f1894771eb4e60fb23f065663b2298f7d22945d66877aadf33d00c7", size = 6640743 },
|
| 249 |
+
{ url = "https://files.pythonhosted.org/packages/23/83/377f84aaeb800b64c0ef4de58b08769e782edcefa4fea712910b6f0afd3c/numpy-2.3.3-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:952cfd0748514ea7c3afc729a0fc639e61655ce4c55ab9acfab14bda4f402b4c", size = 14278881 },
|
| 250 |
+
{ url = "https://files.pythonhosted.org/packages/9a/a5/bf3db6e66c4b160d6ea10b534c381a1955dfab34cb1017ea93aa33c70ed3/numpy-2.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5b83648633d46f77039c29078751f80da65aa64d5622a3cd62aaef9d835b6c93", size = 16636301 },
|
| 251 |
+
{ url = "https://files.pythonhosted.org/packages/a2/59/1287924242eb4fa3f9b3a2c30400f2e17eb2707020d1c5e3086fe7330717/numpy-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b001bae8cea1c7dfdb2ae2b017ed0a6f2102d7a70059df1e338e307a4c78a8ae", size = 16053645 },
|
| 252 |
+
{ url = "https://files.pythonhosted.org/packages/e6/93/b3d47ed882027c35e94ac2320c37e452a549f582a5e801f2d34b56973c97/numpy-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8e9aced64054739037d42fb84c54dd38b81ee238816c948c8f3ed134665dcd86", size = 18578179 },
|
| 253 |
+
{ url = "https://files.pythonhosted.org/packages/20/d9/487a2bccbf7cc9d4bfc5f0f197761a5ef27ba870f1e3bbb9afc4bbe3fcc2/numpy-2.3.3-cp313-cp313-win32.whl", hash = "sha256:9591e1221db3f37751e6442850429b3aabf7026d3b05542d102944ca7f00c8a8", size = 6312250 },
|
| 254 |
+
{ url = "https://files.pythonhosted.org/packages/1b/b5/263ebbbbcede85028f30047eab3d58028d7ebe389d6493fc95ae66c636ab/numpy-2.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:f0dadeb302887f07431910f67a14d57209ed91130be0adea2f9793f1a4f817cf", size = 12783269 },
|
| 255 |
+
{ url = "https://files.pythonhosted.org/packages/fa/75/67b8ca554bbeaaeb3fac2e8bce46967a5a06544c9108ec0cf5cece559b6c/numpy-2.3.3-cp313-cp313-win_arm64.whl", hash = "sha256:3c7cf302ac6e0b76a64c4aecf1a09e51abd9b01fc7feee80f6c43e3ab1b1dbc5", size = 10195314 },
|
| 256 |
+
{ url = "https://files.pythonhosted.org/packages/11/d0/0d1ddec56b162042ddfafeeb293bac672de9b0cfd688383590090963720a/numpy-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:eda59e44957d272846bb407aad19f89dc6f58fecf3504bd144f4c5cf81a7eacc", size = 21048025 },
|
| 257 |
+
{ url = "https://files.pythonhosted.org/packages/36/9e/1996ca6b6d00415b6acbdd3c42f7f03ea256e2c3f158f80bd7436a8a19f3/numpy-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:823d04112bc85ef5c4fda73ba24e6096c8f869931405a80aa8b0e604510a26bc", size = 14301053 },
|
| 258 |
+
{ url = "https://files.pythonhosted.org/packages/05/24/43da09aa764c68694b76e84b3d3f0c44cb7c18cdc1ba80e48b0ac1d2cd39/numpy-2.3.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:40051003e03db4041aa325da2a0971ba41cf65714e65d296397cc0e32de6018b", size = 5229444 },
|
| 259 |
+
{ url = "https://files.pythonhosted.org/packages/bc/14/50ffb0f22f7218ef8af28dd089f79f68289a7a05a208db9a2c5dcbe123c1/numpy-2.3.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:6ee9086235dd6ab7ae75aba5662f582a81ced49f0f1c6de4260a78d8f2d91a19", size = 6738039 },
|
| 260 |
+
{ url = "https://files.pythonhosted.org/packages/55/52/af46ac0795e09657d45a7f4db961917314377edecf66db0e39fa7ab5c3d3/numpy-2.3.3-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94fcaa68757c3e2e668ddadeaa86ab05499a70725811e582b6a9858dd472fb30", size = 14352314 },
|
| 261 |
+
{ url = "https://files.pythonhosted.org/packages/a7/b1/dc226b4c90eb9f07a3fff95c2f0db3268e2e54e5cce97c4ac91518aee71b/numpy-2.3.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da1a74b90e7483d6ce5244053399a614b1d6b7bc30a60d2f570e5071f8959d3e", size = 16701722 },
|
| 262 |
+
{ url = "https://files.pythonhosted.org/packages/9d/9d/9d8d358f2eb5eced14dba99f110d83b5cd9a4460895230f3b396ad19a323/numpy-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2990adf06d1ecee3b3dcbb4977dfab6e9f09807598d647f04d385d29e7a3c3d3", size = 16132755 },
|
| 263 |
+
{ url = "https://files.pythonhosted.org/packages/b6/27/b3922660c45513f9377b3fb42240bec63f203c71416093476ec9aa0719dc/numpy-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ed635ff692483b8e3f0fcaa8e7eb8a75ee71aa6d975388224f70821421800cea", size = 18651560 },
|
| 264 |
+
{ url = "https://files.pythonhosted.org/packages/5b/8e/3ab61a730bdbbc201bb245a71102aa609f0008b9ed15255500a99cd7f780/numpy-2.3.3-cp313-cp313t-win32.whl", hash = "sha256:a333b4ed33d8dc2b373cc955ca57babc00cd6f9009991d9edc5ddbc1bac36bcd", size = 6442776 },
|
| 265 |
+
{ url = "https://files.pythonhosted.org/packages/1c/3a/e22b766b11f6030dc2decdeff5c2fb1610768055603f9f3be88b6d192fb2/numpy-2.3.3-cp313-cp313t-win_amd64.whl", hash = "sha256:4384a169c4d8f97195980815d6fcad04933a7e1ab3b530921c3fef7a1c63426d", size = 12927281 },
|
| 266 |
+
{ url = "https://files.pythonhosted.org/packages/7b/42/c2e2bc48c5e9b2a83423f99733950fbefd86f165b468a3d85d52b30bf782/numpy-2.3.3-cp313-cp313t-win_arm64.whl", hash = "sha256:75370986cc0bc66f4ce5110ad35aae6d182cc4ce6433c40ad151f53690130bf1", size = 10265275 },
|
| 267 |
+
{ url = "https://files.pythonhosted.org/packages/6b/01/342ad585ad82419b99bcf7cebe99e61da6bedb89e213c5fd71acc467faee/numpy-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cd052f1fa6a78dee696b58a914b7229ecfa41f0a6d96dc663c1220a55e137593", size = 20951527 },
|
| 268 |
+
{ url = "https://files.pythonhosted.org/packages/ef/d8/204e0d73fc1b7a9ee80ab1fe1983dd33a4d64a4e30a05364b0208e9a241a/numpy-2.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:414a97499480067d305fcac9716c29cf4d0d76db6ebf0bf3cbce666677f12652", size = 14186159 },
|
| 269 |
+
{ url = "https://files.pythonhosted.org/packages/22/af/f11c916d08f3a18fb8ba81ab72b5b74a6e42ead4c2846d270eb19845bf74/numpy-2.3.3-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:50a5fe69f135f88a2be9b6ca0481a68a136f6febe1916e4920e12f1a34e708a7", size = 5114624 },
|
| 270 |
+
{ url = "https://files.pythonhosted.org/packages/fb/11/0ed919c8381ac9d2ffacd63fd1f0c34d27e99cab650f0eb6f110e6ae4858/numpy-2.3.3-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:b912f2ed2b67a129e6a601e9d93d4fa37bef67e54cac442a2f588a54afe5c67a", size = 6642627 },
|
| 271 |
+
{ url = "https://files.pythonhosted.org/packages/ee/83/deb5f77cb0f7ba6cb52b91ed388b47f8f3c2e9930d4665c600408d9b90b9/numpy-2.3.3-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9e318ee0596d76d4cb3d78535dc005fa60e5ea348cd131a51e99d0bdbe0b54fe", size = 14296926 },
|
| 272 |
+
{ url = "https://files.pythonhosted.org/packages/77/cc/70e59dcb84f2b005d4f306310ff0a892518cc0c8000a33d0e6faf7ca8d80/numpy-2.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ce020080e4a52426202bdb6f7691c65bb55e49f261f31a8f506c9f6bc7450421", size = 16638958 },
|
| 273 |
+
{ url = "https://files.pythonhosted.org/packages/b6/5a/b2ab6c18b4257e099587d5b7f903317bd7115333ad8d4ec4874278eafa61/numpy-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e6687dc183aa55dae4a705b35f9c0f8cb178bcaa2f029b241ac5356221d5c021", size = 16071920 },
|
| 274 |
+
{ url = "https://files.pythonhosted.org/packages/b8/f1/8b3fdc44324a259298520dd82147ff648979bed085feeacc1250ef1656c0/numpy-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d8f3b1080782469fdc1718c4ed1d22549b5fb12af0d57d35e992158a772a37cf", size = 18577076 },
|
| 275 |
+
{ url = "https://files.pythonhosted.org/packages/f0/a1/b87a284fb15a42e9274e7fcea0dad259d12ddbf07c1595b26883151ca3b4/numpy-2.3.3-cp314-cp314-win32.whl", hash = "sha256:cb248499b0bc3be66ebd6578b83e5acacf1d6cb2a77f2248ce0e40fbec5a76d0", size = 6366952 },
|
| 276 |
+
{ url = "https://files.pythonhosted.org/packages/70/5f/1816f4d08f3b8f66576d8433a66f8fa35a5acfb3bbd0bf6c31183b003f3d/numpy-2.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:691808c2b26b0f002a032c73255d0bd89751425f379f7bcd22d140db593a96e8", size = 12919322 },
|
| 277 |
+
{ url = "https://files.pythonhosted.org/packages/8c/de/072420342e46a8ea41c324a555fa90fcc11637583fb8df722936aed1736d/numpy-2.3.3-cp314-cp314-win_arm64.whl", hash = "sha256:9ad12e976ca7b10f1774b03615a2a4bab8addce37ecc77394d8e986927dc0dfe", size = 10478630 },
|
| 278 |
+
{ url = "https://files.pythonhosted.org/packages/d5/df/ee2f1c0a9de7347f14da5dd3cd3c3b034d1b8607ccb6883d7dd5c035d631/numpy-2.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9cc48e09feb11e1db00b320e9d30a4151f7369afb96bd0e48d942d09da3a0d00", size = 21047987 },
|
| 279 |
+
{ url = "https://files.pythonhosted.org/packages/d6/92/9453bdc5a4e9e69cf4358463f25e8260e2ffc126d52e10038b9077815989/numpy-2.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:901bf6123879b7f251d3631967fd574690734236075082078e0571977c6a8e6a", size = 14301076 },
|
| 280 |
+
{ url = "https://files.pythonhosted.org/packages/13/77/1447b9eb500f028bb44253105bd67534af60499588a5149a94f18f2ca917/numpy-2.3.3-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:7f025652034199c301049296b59fa7d52c7e625017cae4c75d8662e377bf487d", size = 5229491 },
|
| 281 |
+
{ url = "https://files.pythonhosted.org/packages/3d/f9/d72221b6ca205f9736cb4b2ce3b002f6e45cd67cd6a6d1c8af11a2f0b649/numpy-2.3.3-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:533ca5f6d325c80b6007d4d7fb1984c303553534191024ec6a524a4c92a5935a", size = 6737913 },
|
| 282 |
+
{ url = "https://files.pythonhosted.org/packages/3c/5f/d12834711962ad9c46af72f79bb31e73e416ee49d17f4c797f72c96b6ca5/numpy-2.3.3-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0edd58682a399824633b66885d699d7de982800053acf20be1eaa46d92009c54", size = 14352811 },
|
| 283 |
+
{ url = "https://files.pythonhosted.org/packages/a1/0d/fdbec6629d97fd1bebed56cd742884e4eead593611bbe1abc3eb40d304b2/numpy-2.3.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:367ad5d8fbec5d9296d18478804a530f1191e24ab4d75ab408346ae88045d25e", size = 16702689 },
|
| 284 |
+
{ url = "https://files.pythonhosted.org/packages/9b/09/0a35196dc5575adde1eb97ddfbc3e1687a814f905377621d18ca9bc2b7dd/numpy-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8f6ac61a217437946a1fa48d24c47c91a0c4f725237871117dea264982128097", size = 16133855 },
|
| 285 |
+
{ url = "https://files.pythonhosted.org/packages/7a/ca/c9de3ea397d576f1b6753eaa906d4cdef1bf97589a6d9825a349b4729cc2/numpy-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:179a42101b845a816d464b6fe9a845dfaf308fdfc7925387195570789bb2c970", size = 18652520 },
|
| 286 |
+
{ url = "https://files.pythonhosted.org/packages/fd/c2/e5ed830e08cd0196351db55db82f65bc0ab05da6ef2b72a836dcf1936d2f/numpy-2.3.3-cp314-cp314t-win32.whl", hash = "sha256:1250c5d3d2562ec4174bce2e3a1523041595f9b651065e4a4473f5f48a6bc8a5", size = 6515371 },
|
| 287 |
+
{ url = "https://files.pythonhosted.org/packages/47/c7/b0f6b5b67f6788a0725f744496badbb604d226bf233ba716683ebb47b570/numpy-2.3.3-cp314-cp314t-win_amd64.whl", hash = "sha256:b37a0b2e5935409daebe82c1e42274d30d9dd355852529eab91dab8dcca7419f", size = 13112576 },
|
| 288 |
+
{ url = "https://files.pythonhosted.org/packages/06/b9/33bba5ff6fb679aa0b1f8a07e853f002a6b04b9394db3069a1270a7784ca/numpy-2.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:78c9f6560dc7e6b3990e32df7ea1a50bbd0e2a111e05209963f5ddcab7073b0b", size = 10545953 },
|
| 289 |
+
]
|
| 290 |
+
|
| 291 |
+
[[package]]
|
| 292 |
+
name = "packaging"
|
| 293 |
+
version = "25.0"
|
| 294 |
+
source = { registry = "https://pypi.org/simple" }
|
| 295 |
+
sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727 }
|
| 296 |
+
wheels = [
|
| 297 |
+
{ url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469 },
|
| 298 |
+
]
|
| 299 |
+
|
| 300 |
+
[[package]]
|
| 301 |
+
name = "pandas"
|
| 302 |
+
version = "2.3.2"
|
| 303 |
+
source = { registry = "https://pypi.org/simple" }
|
| 304 |
+
dependencies = [
|
| 305 |
+
{ name = "numpy" },
|
| 306 |
+
{ name = "python-dateutil" },
|
| 307 |
+
{ name = "pytz" },
|
| 308 |
+
{ name = "tzdata" },
|
| 309 |
+
]
|
| 310 |
+
sdist = { url = "https://files.pythonhosted.org/packages/79/8e/0e90233ac205ad182bd6b422532695d2b9414944a280488105d598c70023/pandas-2.3.2.tar.gz", hash = "sha256:ab7b58f8f82706890924ccdfb5f48002b83d2b5a3845976a9fb705d36c34dcdb", size = 4488684 }
|
| 311 |
+
wheels = [
|
| 312 |
+
{ url = "https://files.pythonhosted.org/packages/27/64/a2f7bf678af502e16b472527735d168b22b7824e45a4d7e96a4fbb634b59/pandas-2.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0c6ecbac99a354a051ef21c5307601093cb9e0f4b1855984a084bfec9302699e", size = 11531061 },
|
| 313 |
+
{ url = "https://files.pythonhosted.org/packages/54/4c/c3d21b2b7769ef2f4c2b9299fcadd601efa6729f1357a8dbce8dd949ed70/pandas-2.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c6f048aa0fd080d6a06cc7e7537c09b53be6642d330ac6f54a600c3ace857ee9", size = 10668666 },
|
| 314 |
+
{ url = "https://files.pythonhosted.org/packages/50/e2/f775ba76ecfb3424d7f5862620841cf0edb592e9abd2d2a5387d305fe7a8/pandas-2.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0064187b80a5be6f2f9c9d6bdde29372468751dfa89f4211a3c5871854cfbf7a", size = 11332835 },
|
| 315 |
+
{ url = "https://files.pythonhosted.org/packages/8f/52/0634adaace9be2d8cac9ef78f05c47f3a675882e068438b9d7ec7ef0c13f/pandas-2.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ac8c320bded4718b298281339c1a50fb00a6ba78cb2a63521c39bec95b0209b", size = 12057211 },
|
| 316 |
+
{ url = "https://files.pythonhosted.org/packages/0b/9d/2df913f14b2deb9c748975fdb2491da1a78773debb25abbc7cbc67c6b549/pandas-2.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:114c2fe4f4328cf98ce5716d1532f3ab79c5919f95a9cfee81d9140064a2e4d6", size = 12749277 },
|
| 317 |
+
{ url = "https://files.pythonhosted.org/packages/87/af/da1a2417026bd14d98c236dba88e39837182459d29dcfcea510b2ac9e8a1/pandas-2.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:48fa91c4dfb3b2b9bfdb5c24cd3567575f4e13f9636810462ffed8925352be5a", size = 13415256 },
|
| 318 |
+
{ url = "https://files.pythonhosted.org/packages/22/3c/f2af1ce8840ef648584a6156489636b5692c162771918aa95707c165ad2b/pandas-2.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:12d039facec710f7ba305786837d0225a3444af7bbd9c15c32ca2d40d157ed8b", size = 10982579 },
|
| 319 |
+
{ url = "https://files.pythonhosted.org/packages/f3/98/8df69c4097a6719e357dc249bf437b8efbde808038268e584421696cbddf/pandas-2.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:c624b615ce97864eb588779ed4046186f967374185c047070545253a52ab2d57", size = 12028163 },
|
| 320 |
+
{ url = "https://files.pythonhosted.org/packages/0e/23/f95cbcbea319f349e10ff90db488b905c6883f03cbabd34f6b03cbc3c044/pandas-2.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0cee69d583b9b128823d9514171cabb6861e09409af805b54459bd0c821a35c2", size = 11391860 },
|
| 321 |
+
{ url = "https://files.pythonhosted.org/packages/ad/1b/6a984e98c4abee22058aa75bfb8eb90dce58cf8d7296f8bc56c14bc330b0/pandas-2.3.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2319656ed81124982900b4c37f0e0c58c015af9a7bbc62342ba5ad07ace82ba9", size = 11309830 },
|
| 322 |
+
{ url = "https://files.pythonhosted.org/packages/15/d5/f0486090eb18dd8710bf60afeaf638ba6817047c0c8ae5c6a25598665609/pandas-2.3.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b37205ad6f00d52f16b6d09f406434ba928c1a1966e2771006a9033c736d30d2", size = 11883216 },
|
| 323 |
+
{ url = "https://files.pythonhosted.org/packages/10/86/692050c119696da19e20245bbd650d8dfca6ceb577da027c3a73c62a047e/pandas-2.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:837248b4fc3a9b83b9c6214699a13f069dc13510a6a6d7f9ba33145d2841a012", size = 12699743 },
|
| 324 |
+
{ url = "https://files.pythonhosted.org/packages/cd/d7/612123674d7b17cf345aad0a10289b2a384bff404e0463a83c4a3a59d205/pandas-2.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d2c3554bd31b731cd6490d94a28f3abb8dd770634a9e06eb6d2911b9827db370", size = 13186141 },
|
| 325 |
+
]
|
| 326 |
+
|
| 327 |
+
[[package]]
|
| 328 |
+
name = "pillow"
|
| 329 |
+
version = "11.3.0"
|
| 330 |
+
source = { registry = "https://pypi.org/simple" }
|
| 331 |
+
sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/d0d6dea55cd152ce3d6767bb38a8fc10e33796ba4ba210cbab9354b6d238/pillow-11.3.0.tar.gz", hash = "sha256:3828ee7586cd0b2091b6209e5ad53e20d0649bbe87164a459d0676e035e8f523", size = 47113069 }
|
| 332 |
+
wheels = [
|
| 333 |
+
{ url = "https://files.pythonhosted.org/packages/1e/93/0952f2ed8db3a5a4c7a11f91965d6184ebc8cd7cbb7941a260d5f018cd2d/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:1c627742b539bba4309df89171356fcb3cc5a9178355b2727d1b74a6cf155fbd", size = 2128328 },
|
| 334 |
+
{ url = "https://files.pythonhosted.org/packages/4b/e8/100c3d114b1a0bf4042f27e0f87d2f25e857e838034e98ca98fe7b8c0a9c/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:30b7c02f3899d10f13d7a48163c8969e4e653f8b43416d23d13d1bbfdc93b9f8", size = 2170652 },
|
| 335 |
+
{ url = "https://files.pythonhosted.org/packages/aa/86/3f758a28a6e381758545f7cdb4942e1cb79abd271bea932998fc0db93cb6/pillow-11.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7859a4cc7c9295f5838015d8cc0a9c215b77e43d07a25e460f35cf516df8626f", size = 2227443 },
|
| 336 |
+
{ url = "https://files.pythonhosted.org/packages/01/f4/91d5b3ffa718df2f53b0dc109877993e511f4fd055d7e9508682e8aba092/pillow-11.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec1ee50470b0d050984394423d96325b744d55c701a439d2bd66089bff963d3c", size = 5278474 },
|
| 337 |
+
{ url = "https://files.pythonhosted.org/packages/f9/0e/37d7d3eca6c879fbd9dba21268427dffda1ab00d4eb05b32923d4fbe3b12/pillow-11.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7db51d222548ccfd274e4572fdbf3e810a5e66b00608862f947b163e613b67dd", size = 4686038 },
|
| 338 |
+
{ url = "https://files.pythonhosted.org/packages/ff/b0/3426e5c7f6565e752d81221af9d3676fdbb4f352317ceafd42899aaf5d8a/pillow-11.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2d6fcc902a24ac74495df63faad1884282239265c6839a0a6416d33faedfae7e", size = 5864407 },
|
| 339 |
+
{ url = "https://files.pythonhosted.org/packages/fc/c1/c6c423134229f2a221ee53f838d4be9d82bab86f7e2f8e75e47b6bf6cd77/pillow-11.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f0f5d8f4a08090c6d6d578351a2b91acf519a54986c055af27e7a93feae6d3f1", size = 7639094 },
|
| 340 |
+
{ url = "https://files.pythonhosted.org/packages/ba/c9/09e6746630fe6372c67c648ff9deae52a2bc20897d51fa293571977ceb5d/pillow-11.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c37d8ba9411d6003bba9e518db0db0c58a680ab9fe5179f040b0463644bc9805", size = 5973503 },
|
| 341 |
+
{ url = "https://files.pythonhosted.org/packages/d5/1c/a2a29649c0b1983d3ef57ee87a66487fdeb45132df66ab30dd37f7dbe162/pillow-11.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13f87d581e71d9189ab21fe0efb5a23e9f28552d5be6979e84001d3b8505abe8", size = 6642574 },
|
| 342 |
+
{ url = "https://files.pythonhosted.org/packages/36/de/d5cc31cc4b055b6c6fd990e3e7f0f8aaf36229a2698501bcb0cdf67c7146/pillow-11.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:023f6d2d11784a465f09fd09a34b150ea4672e85fb3d05931d89f373ab14abb2", size = 6084060 },
|
| 343 |
+
{ url = "https://files.pythonhosted.org/packages/d5/ea/502d938cbaeec836ac28a9b730193716f0114c41325db428e6b280513f09/pillow-11.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:45dfc51ac5975b938e9809451c51734124e73b04d0f0ac621649821a63852e7b", size = 6721407 },
|
| 344 |
+
{ url = "https://files.pythonhosted.org/packages/45/9c/9c5e2a73f125f6cbc59cc7087c8f2d649a7ae453f83bd0362ff7c9e2aee2/pillow-11.3.0-cp313-cp313-win32.whl", hash = "sha256:a4d336baed65d50d37b88ca5b60c0fa9d81e3a87d4a7930d3880d1624d5b31f3", size = 6273841 },
|
| 345 |
+
{ url = "https://files.pythonhosted.org/packages/23/85/397c73524e0cd212067e0c969aa245b01d50183439550d24d9f55781b776/pillow-11.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0bce5c4fd0921f99d2e858dc4d4d64193407e1b99478bc5cacecba2311abde51", size = 6978450 },
|
| 346 |
+
{ url = "https://files.pythonhosted.org/packages/17/d2/622f4547f69cd173955194b78e4d19ca4935a1b0f03a302d655c9f6aae65/pillow-11.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:1904e1264881f682f02b7f8167935cce37bc97db457f8e7849dc3a6a52b99580", size = 2423055 },
|
| 347 |
+
{ url = "https://files.pythonhosted.org/packages/dd/80/a8a2ac21dda2e82480852978416cfacd439a4b490a501a288ecf4fe2532d/pillow-11.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4c834a3921375c48ee6b9624061076bc0a32a60b5532b322cc0ea64e639dd50e", size = 5281110 },
|
| 348 |
+
{ url = "https://files.pythonhosted.org/packages/44/d6/b79754ca790f315918732e18f82a8146d33bcd7f4494380457ea89eb883d/pillow-11.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5e05688ccef30ea69b9317a9ead994b93975104a677a36a8ed8106be9260aa6d", size = 4689547 },
|
| 349 |
+
{ url = "https://files.pythonhosted.org/packages/49/20/716b8717d331150cb00f7fdd78169c01e8e0c219732a78b0e59b6bdb2fd6/pillow-11.3.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1019b04af07fc0163e2810167918cb5add8d74674b6267616021ab558dc98ced", size = 5901554 },
|
| 350 |
+
{ url = "https://files.pythonhosted.org/packages/74/cf/a9f3a2514a65bb071075063a96f0a5cf949c2f2fce683c15ccc83b1c1cab/pillow-11.3.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f944255db153ebb2b19c51fe85dd99ef0ce494123f21b9db4877ffdfc5590c7c", size = 7669132 },
|
| 351 |
+
{ url = "https://files.pythonhosted.org/packages/98/3c/da78805cbdbee9cb43efe8261dd7cc0b4b93f2ac79b676c03159e9db2187/pillow-11.3.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f85acb69adf2aaee8b7da124efebbdb959a104db34d3a2cb0f3793dbae422a8", size = 6005001 },
|
| 352 |
+
{ url = "https://files.pythonhosted.org/packages/6c/fa/ce044b91faecf30e635321351bba32bab5a7e034c60187fe9698191aef4f/pillow-11.3.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05f6ecbeff5005399bb48d198f098a9b4b6bdf27b8487c7f38ca16eeb070cd59", size = 6668814 },
|
| 353 |
+
{ url = "https://files.pythonhosted.org/packages/7b/51/90f9291406d09bf93686434f9183aba27b831c10c87746ff49f127ee80cb/pillow-11.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a7bc6e6fd0395bc052f16b1a8670859964dbd7003bd0af2ff08342eb6e442cfe", size = 6113124 },
|
| 354 |
+
{ url = "https://files.pythonhosted.org/packages/cd/5a/6fec59b1dfb619234f7636d4157d11fb4e196caeee220232a8d2ec48488d/pillow-11.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:83e1b0161c9d148125083a35c1c5a89db5b7054834fd4387499e06552035236c", size = 6747186 },
|
| 355 |
+
{ url = "https://files.pythonhosted.org/packages/49/6b/00187a044f98255225f172de653941e61da37104a9ea60e4f6887717e2b5/pillow-11.3.0-cp313-cp313t-win32.whl", hash = "sha256:2a3117c06b8fb646639dce83694f2f9eac405472713fcb1ae887469c0d4f6788", size = 6277546 },
|
| 356 |
+
{ url = "https://files.pythonhosted.org/packages/e8/5c/6caaba7e261c0d75bab23be79f1d06b5ad2a2ae49f028ccec801b0e853d6/pillow-11.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:857844335c95bea93fb39e0fa2726b4d9d758850b34075a7e3ff4f4fa3aa3b31", size = 6985102 },
|
| 357 |
+
{ url = "https://files.pythonhosted.org/packages/f3/7e/b623008460c09a0cb38263c93b828c666493caee2eb34ff67f778b87e58c/pillow-11.3.0-cp313-cp313t-win_arm64.whl", hash = "sha256:8797edc41f3e8536ae4b10897ee2f637235c94f27404cac7297f7b607dd0716e", size = 2424803 },
|
| 358 |
+
{ url = "https://files.pythonhosted.org/packages/73/f4/04905af42837292ed86cb1b1dabe03dce1edc008ef14c473c5c7e1443c5d/pillow-11.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d9da3df5f9ea2a89b81bb6087177fb1f4d1c7146d583a3fe5c672c0d94e55e12", size = 5278520 },
|
| 359 |
+
{ url = "https://files.pythonhosted.org/packages/41/b0/33d79e377a336247df6348a54e6d2a2b85d644ca202555e3faa0cf811ecc/pillow-11.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0b275ff9b04df7b640c59ec5a3cb113eefd3795a8df80bac69646ef699c6981a", size = 4686116 },
|
| 360 |
+
{ url = "https://files.pythonhosted.org/packages/49/2d/ed8bc0ab219ae8768f529597d9509d184fe8a6c4741a6864fea334d25f3f/pillow-11.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0743841cabd3dba6a83f38a92672cccbd69af56e3e91777b0ee7f4dba4385632", size = 5864597 },
|
| 361 |
+
{ url = "https://files.pythonhosted.org/packages/b5/3d/b932bb4225c80b58dfadaca9d42d08d0b7064d2d1791b6a237f87f661834/pillow-11.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2465a69cf967b8b49ee1b96d76718cd98c4e925414ead59fdf75cf0fd07df673", size = 7638246 },
|
| 362 |
+
{ url = "https://files.pythonhosted.org/packages/09/b5/0487044b7c096f1b48f0d7ad416472c02e0e4bf6919541b111efd3cae690/pillow-11.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41742638139424703b4d01665b807c6468e23e699e8e90cffefe291c5832b027", size = 5973336 },
|
| 363 |
+
{ url = "https://files.pythonhosted.org/packages/a8/2d/524f9318f6cbfcc79fbc004801ea6b607ec3f843977652fdee4857a7568b/pillow-11.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93efb0b4de7e340d99057415c749175e24c8864302369e05914682ba642e5d77", size = 6642699 },
|
| 364 |
+
{ url = "https://files.pythonhosted.org/packages/6f/d2/a9a4f280c6aefedce1e8f615baaa5474e0701d86dd6f1dede66726462bbd/pillow-11.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7966e38dcd0fa11ca390aed7c6f20454443581d758242023cf36fcb319b1a874", size = 6083789 },
|
| 365 |
+
{ url = "https://files.pythonhosted.org/packages/fe/54/86b0cd9dbb683a9d5e960b66c7379e821a19be4ac5810e2e5a715c09a0c0/pillow-11.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:98a9afa7b9007c67ed84c57c9e0ad86a6000da96eaa638e4f8abe5b65ff83f0a", size = 6720386 },
|
| 366 |
+
{ url = "https://files.pythonhosted.org/packages/e7/95/88efcaf384c3588e24259c4203b909cbe3e3c2d887af9e938c2022c9dd48/pillow-11.3.0-cp314-cp314-win32.whl", hash = "sha256:02a723e6bf909e7cea0dac1b0e0310be9d7650cd66222a5f1c571455c0a45214", size = 6370911 },
|
| 367 |
+
{ url = "https://files.pythonhosted.org/packages/2e/cc/934e5820850ec5eb107e7b1a72dd278140731c669f396110ebc326f2a503/pillow-11.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:a418486160228f64dd9e9efcd132679b7a02a5f22c982c78b6fc7dab3fefb635", size = 7117383 },
|
| 368 |
+
{ url = "https://files.pythonhosted.org/packages/d6/e9/9c0a616a71da2a5d163aa37405e8aced9a906d574b4a214bede134e731bc/pillow-11.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:155658efb5e044669c08896c0c44231c5e9abcaadbc5cd3648df2f7c0b96b9a6", size = 2511385 },
|
| 369 |
+
{ url = "https://files.pythonhosted.org/packages/1a/33/c88376898aff369658b225262cd4f2659b13e8178e7534df9e6e1fa289f6/pillow-11.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:59a03cdf019efbfeeed910bf79c7c93255c3d54bc45898ac2a4140071b02b4ae", size = 5281129 },
|
| 370 |
+
{ url = "https://files.pythonhosted.org/packages/1f/70/d376247fb36f1844b42910911c83a02d5544ebd2a8bad9efcc0f707ea774/pillow-11.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f8a5827f84d973d8636e9dc5764af4f0cf2318d26744b3d902931701b0d46653", size = 4689580 },
|
| 371 |
+
{ url = "https://files.pythonhosted.org/packages/eb/1c/537e930496149fbac69efd2fc4329035bbe2e5475b4165439e3be9cb183b/pillow-11.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ee92f2fd10f4adc4b43d07ec5e779932b4eb3dbfbc34790ada5a6669bc095aa6", size = 5902860 },
|
| 372 |
+
{ url = "https://files.pythonhosted.org/packages/bd/57/80f53264954dcefeebcf9dae6e3eb1daea1b488f0be8b8fef12f79a3eb10/pillow-11.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c96d333dcf42d01f47b37e0979b6bd73ec91eae18614864622d9b87bbd5bbf36", size = 7670694 },
|
| 373 |
+
{ url = "https://files.pythonhosted.org/packages/70/ff/4727d3b71a8578b4587d9c276e90efad2d6fe0335fd76742a6da08132e8c/pillow-11.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c96f993ab8c98460cd0c001447bff6194403e8b1d7e149ade5f00594918128b", size = 6005888 },
|
| 374 |
+
{ url = "https://files.pythonhosted.org/packages/05/ae/716592277934f85d3be51d7256f3636672d7b1abfafdc42cf3f8cbd4b4c8/pillow-11.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41342b64afeba938edb034d122b2dda5db2139b9a4af999729ba8818e0056477", size = 6670330 },
|
| 375 |
+
{ url = "https://files.pythonhosted.org/packages/e7/bb/7fe6cddcc8827b01b1a9766f5fdeb7418680744f9082035bdbabecf1d57f/pillow-11.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:068d9c39a2d1b358eb9f245ce7ab1b5c3246c7c8c7d9ba58cfa5b43146c06e50", size = 6114089 },
|
| 376 |
+
{ url = "https://files.pythonhosted.org/packages/8b/f5/06bfaa444c8e80f1a8e4bff98da9c83b37b5be3b1deaa43d27a0db37ef84/pillow-11.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a1bc6ba083b145187f648b667e05a2534ecc4b9f2784c2cbe3089e44868f2b9b", size = 6748206 },
|
| 377 |
+
{ url = "https://files.pythonhosted.org/packages/f0/77/bc6f92a3e8e6e46c0ca78abfffec0037845800ea38c73483760362804c41/pillow-11.3.0-cp314-cp314t-win32.whl", hash = "sha256:118ca10c0d60b06d006be10a501fd6bbdfef559251ed31b794668ed569c87e12", size = 6377370 },
|
| 378 |
+
{ url = "https://files.pythonhosted.org/packages/4a/82/3a721f7d69dca802befb8af08b7c79ebcab461007ce1c18bd91a5d5896f9/pillow-11.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8924748b688aa210d79883357d102cd64690e56b923a186f35a82cbc10f997db", size = 7121500 },
|
| 379 |
+
{ url = "https://files.pythonhosted.org/packages/89/c7/5572fa4a3f45740eaab6ae86fcdf7195b55beac1371ac8c619d880cfe948/pillow-11.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:79ea0d14d3ebad43ec77ad5272e6ff9bba5b679ef73375ea760261207fa8e0aa", size = 2512835 },
|
| 380 |
+
]
|
| 381 |
+
|
| 382 |
+
[[package]]
|
| 383 |
+
name = "plotly"
|
| 384 |
+
version = "6.3.0"
|
| 385 |
+
source = { registry = "https://pypi.org/simple" }
|
| 386 |
+
dependencies = [
|
| 387 |
+
{ name = "narwhals" },
|
| 388 |
+
{ name = "packaging" },
|
| 389 |
+
]
|
| 390 |
+
sdist = { url = "https://files.pythonhosted.org/packages/a0/64/850de5076f4436410e1ce4f6a69f4313ef6215dfea155f3f6559335cad29/plotly-6.3.0.tar.gz", hash = "sha256:8840a184d18ccae0f9189c2b9a2943923fd5cae7717b723f36eef78f444e5a73", size = 6923926 }
|
| 391 |
+
wheels = [
|
| 392 |
+
{ url = "https://files.pythonhosted.org/packages/95/a9/12e2dc726ba1ba775a2c6922d5d5b4488ad60bdab0888c337c194c8e6de8/plotly-6.3.0-py3-none-any.whl", hash = "sha256:7ad806edce9d3cdd882eaebaf97c0c9e252043ed1ed3d382c3e3520ec07806d4", size = 9791257 },
|
| 393 |
+
]
|
| 394 |
+
|
| 395 |
+
[[package]]
|
| 396 |
+
name = "protobuf"
|
| 397 |
+
version = "6.32.1"
|
| 398 |
+
source = { registry = "https://pypi.org/simple" }
|
| 399 |
+
sdist = { url = "https://files.pythonhosted.org/packages/fa/a4/cc17347aa2897568beece2e674674359f911d6fe21b0b8d6268cd42727ac/protobuf-6.32.1.tar.gz", hash = "sha256:ee2469e4a021474ab9baafea6cd070e5bf27c7d29433504ddea1a4ee5850f68d", size = 440635 }
|
| 400 |
+
wheels = [
|
| 401 |
+
{ url = "https://files.pythonhosted.org/packages/c0/98/645183ea03ab3995d29086b8bf4f7562ebd3d10c9a4b14ee3f20d47cfe50/protobuf-6.32.1-cp310-abi3-win32.whl", hash = "sha256:a8a32a84bc9f2aad712041b8b366190f71dde248926da517bde9e832e4412085", size = 424411 },
|
| 402 |
+
{ url = "https://files.pythonhosted.org/packages/8c/f3/6f58f841f6ebafe076cebeae33fc336e900619d34b1c93e4b5c97a81fdfa/protobuf-6.32.1-cp310-abi3-win_amd64.whl", hash = "sha256:b00a7d8c25fa471f16bc8153d0e53d6c9e827f0953f3c09aaa4331c718cae5e1", size = 435738 },
|
| 403 |
+
{ url = "https://files.pythonhosted.org/packages/10/56/a8a3f4e7190837139e68c7002ec749190a163af3e330f65d90309145a210/protobuf-6.32.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:d8c7e6eb619ffdf105ee4ab76af5a68b60a9d0f66da3ea12d1640e6d8dab7281", size = 426454 },
|
| 404 |
+
{ url = "https://files.pythonhosted.org/packages/3f/be/8dd0a927c559b37d7a6c8ab79034fd167dcc1f851595f2e641ad62be8643/protobuf-6.32.1-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:2f5b80a49e1eb7b86d85fcd23fe92df154b9730a725c3b38c4e43b9d77018bf4", size = 322874 },
|
| 405 |
+
{ url = "https://files.pythonhosted.org/packages/5c/f6/88d77011b605ef979aace37b7703e4eefad066f7e84d935e5a696515c2dd/protobuf-6.32.1-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:b1864818300c297265c83a4982fd3169f97122c299f56a56e2445c3698d34710", size = 322013 },
|
| 406 |
+
{ url = "https://files.pythonhosted.org/packages/97/b7/15cc7d93443d6c6a84626ae3258a91f4c6ac8c0edd5df35ea7658f71b79c/protobuf-6.32.1-py3-none-any.whl", hash = "sha256:2601b779fc7d32a866c6b4404f9d42a3f67c5b9f3f15b4db3cccabe06b95c346", size = 169289 },
|
| 407 |
+
]
|
| 408 |
+
|
| 409 |
+
[[package]]
|
| 410 |
+
name = "pyarrow"
|
| 411 |
+
version = "21.0.0"
|
| 412 |
+
source = { registry = "https://pypi.org/simple" }
|
| 413 |
+
sdist = { url = "https://files.pythonhosted.org/packages/ef/c2/ea068b8f00905c06329a3dfcd40d0fcc2b7d0f2e355bdb25b65e0a0e4cd4/pyarrow-21.0.0.tar.gz", hash = "sha256:5051f2dccf0e283ff56335760cbc8622cf52264d67e359d5569541ac11b6d5bc", size = 1133487 }
|
| 414 |
+
wheels = [
|
| 415 |
+
{ url = "https://files.pythonhosted.org/packages/16/ca/c7eaa8e62db8fb37ce942b1ea0c6d7abfe3786ca193957afa25e71b81b66/pyarrow-21.0.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:e99310a4ebd4479bcd1964dff9e14af33746300cb014aa4a3781738ac63baf4a", size = 31154306 },
|
| 416 |
+
{ url = "https://files.pythonhosted.org/packages/ce/e8/e87d9e3b2489302b3a1aea709aaca4b781c5252fcb812a17ab6275a9a484/pyarrow-21.0.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:d2fe8e7f3ce329a71b7ddd7498b3cfac0eeb200c2789bd840234f0dc271a8efe", size = 32680622 },
|
| 417 |
+
{ url = "https://files.pythonhosted.org/packages/84/52/79095d73a742aa0aba370c7942b1b655f598069489ab387fe47261a849e1/pyarrow-21.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:f522e5709379d72fb3da7785aa489ff0bb87448a9dc5a75f45763a795a089ebd", size = 41104094 },
|
| 418 |
+
{ url = "https://files.pythonhosted.org/packages/89/4b/7782438b551dbb0468892a276b8c789b8bbdb25ea5c5eb27faadd753e037/pyarrow-21.0.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:69cbbdf0631396e9925e048cfa5bce4e8c3d3b41562bbd70c685a8eb53a91e61", size = 42825576 },
|
| 419 |
+
{ url = "https://files.pythonhosted.org/packages/b3/62/0f29de6e0a1e33518dec92c65be0351d32d7ca351e51ec5f4f837a9aab91/pyarrow-21.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:731c7022587006b755d0bdb27626a1a3bb004bb56b11fb30d98b6c1b4718579d", size = 43368342 },
|
| 420 |
+
{ url = "https://files.pythonhosted.org/packages/90/c7/0fa1f3f29cf75f339768cc698c8ad4ddd2481c1742e9741459911c9ac477/pyarrow-21.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dc56bc708f2d8ac71bd1dcb927e458c93cec10b98eb4120206a4091db7b67b99", size = 45131218 },
|
| 421 |
+
{ url = "https://files.pythonhosted.org/packages/01/63/581f2076465e67b23bc5a37d4a2abff8362d389d29d8105832e82c9c811c/pyarrow-21.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:186aa00bca62139f75b7de8420f745f2af12941595bbbfa7ed3870ff63e25636", size = 26087551 },
|
| 422 |
+
{ url = "https://files.pythonhosted.org/packages/c9/ab/357d0d9648bb8241ee7348e564f2479d206ebe6e1c47ac5027c2e31ecd39/pyarrow-21.0.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:a7a102574faa3f421141a64c10216e078df467ab9576684d5cd696952546e2da", size = 31290064 },
|
| 423 |
+
{ url = "https://files.pythonhosted.org/packages/3f/8a/5685d62a990e4cac2043fc76b4661bf38d06efed55cf45a334b455bd2759/pyarrow-21.0.0-cp313-cp313t-macosx_12_0_x86_64.whl", hash = "sha256:1e005378c4a2c6db3ada3ad4c217b381f6c886f0a80d6a316fe586b90f77efd7", size = 32727837 },
|
| 424 |
+
{ url = "https://files.pythonhosted.org/packages/fc/de/c0828ee09525c2bafefd3e736a248ebe764d07d0fd762d4f0929dbc516c9/pyarrow-21.0.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:65f8e85f79031449ec8706b74504a316805217b35b6099155dd7e227eef0d4b6", size = 41014158 },
|
| 425 |
+
{ url = "https://files.pythonhosted.org/packages/6e/26/a2865c420c50b7a3748320b614f3484bfcde8347b2639b2b903b21ce6a72/pyarrow-21.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:3a81486adc665c7eb1a2bde0224cfca6ceaba344a82a971ef059678417880eb8", size = 42667885 },
|
| 426 |
+
{ url = "https://files.pythonhosted.org/packages/0a/f9/4ee798dc902533159250fb4321267730bc0a107d8c6889e07c3add4fe3a5/pyarrow-21.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:fc0d2f88b81dcf3ccf9a6ae17f89183762c8a94a5bdcfa09e05cfe413acf0503", size = 43276625 },
|
| 427 |
+
{ url = "https://files.pythonhosted.org/packages/5a/da/e02544d6997037a4b0d22d8e5f66bc9315c3671371a8b18c79ade1cefe14/pyarrow-21.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6299449adf89df38537837487a4f8d3bd91ec94354fdd2a7d30bc11c48ef6e79", size = 44951890 },
|
| 428 |
+
{ url = "https://files.pythonhosted.org/packages/e5/4e/519c1bc1876625fe6b71e9a28287c43ec2f20f73c658b9ae1d485c0c206e/pyarrow-21.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:222c39e2c70113543982c6b34f3077962b44fca38c0bd9e68bb6781534425c10", size = 26371006 },
|
| 429 |
+
]
|
| 430 |
+
|
| 431 |
+
[[package]]
|
| 432 |
+
name = "pydeck"
|
| 433 |
+
version = "0.9.1"
|
| 434 |
+
source = { registry = "https://pypi.org/simple" }
|
| 435 |
+
dependencies = [
|
| 436 |
+
{ name = "jinja2" },
|
| 437 |
+
{ name = "numpy" },
|
| 438 |
+
]
|
| 439 |
+
sdist = { url = "https://files.pythonhosted.org/packages/a1/ca/40e14e196864a0f61a92abb14d09b3d3da98f94ccb03b49cf51688140dab/pydeck-0.9.1.tar.gz", hash = "sha256:f74475ae637951d63f2ee58326757f8d4f9cd9f2a457cf42950715003e2cb605", size = 3832240 }
|
| 440 |
+
wheels = [
|
| 441 |
+
{ url = "https://files.pythonhosted.org/packages/ab/4c/b888e6cf58bd9db9c93f40d1c6be8283ff49d88919231afe93a6bcf61626/pydeck-0.9.1-py2.py3-none-any.whl", hash = "sha256:b3f75ba0d273fc917094fa61224f3f6076ca8752b93d46faf3bcfd9f9d59b038", size = 6900403 },
|
| 442 |
+
]
|
| 443 |
+
|
| 444 |
+
[[package]]
|
| 445 |
+
name = "python-dateutil"
|
| 446 |
+
version = "2.9.0.post0"
|
| 447 |
+
source = { registry = "https://pypi.org/simple" }
|
| 448 |
+
dependencies = [
|
| 449 |
+
{ name = "six" },
|
| 450 |
+
]
|
| 451 |
+
sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 }
|
| 452 |
+
wheels = [
|
| 453 |
+
{ url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 },
|
| 454 |
+
]
|
| 455 |
+
|
| 456 |
+
[[package]]
|
| 457 |
+
name = "pytz"
|
| 458 |
+
version = "2025.2"
|
| 459 |
+
source = { registry = "https://pypi.org/simple" }
|
| 460 |
+
sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884 }
|
| 461 |
+
wheels = [
|
| 462 |
+
{ url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225 },
|
| 463 |
+
]
|
| 464 |
+
|
| 465 |
+
[[package]]
|
| 466 |
+
name = "referencing"
|
| 467 |
+
version = "0.36.2"
|
| 468 |
+
source = { registry = "https://pypi.org/simple" }
|
| 469 |
+
dependencies = [
|
| 470 |
+
{ name = "attrs" },
|
| 471 |
+
{ name = "rpds-py" },
|
| 472 |
+
]
|
| 473 |
+
sdist = { url = "https://files.pythonhosted.org/packages/2f/db/98b5c277be99dd18bfd91dd04e1b759cad18d1a338188c936e92f921c7e2/referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa", size = 74744 }
|
| 474 |
+
wheels = [
|
| 475 |
+
{ url = "https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0", size = 26775 },
|
| 476 |
+
]
|
| 477 |
+
|
| 478 |
+
[[package]]
|
| 479 |
+
name = "requests"
|
| 480 |
+
version = "2.32.5"
|
| 481 |
+
source = { registry = "https://pypi.org/simple" }
|
| 482 |
+
dependencies = [
|
| 483 |
+
{ name = "certifi" },
|
| 484 |
+
{ name = "charset-normalizer" },
|
| 485 |
+
{ name = "idna" },
|
| 486 |
+
{ name = "urllib3" },
|
| 487 |
+
]
|
| 488 |
+
sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517 }
|
| 489 |
+
wheels = [
|
| 490 |
+
{ url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738 },
|
| 491 |
+
]
|
| 492 |
+
|
| 493 |
+
[[package]]
|
| 494 |
+
name = "rpds-py"
|
| 495 |
+
version = "0.27.1"
|
| 496 |
+
source = { registry = "https://pypi.org/simple" }
|
| 497 |
+
sdist = { url = "https://files.pythonhosted.org/packages/e9/dd/2c0cbe774744272b0ae725f44032c77bdcab6e8bcf544bffa3b6e70c8dba/rpds_py-0.27.1.tar.gz", hash = "sha256:26a1c73171d10b7acccbded82bf6a586ab8203601e565badc74bbbf8bc5a10f8", size = 27479 }
|
| 498 |
+
wheels = [
|
| 499 |
+
{ url = "https://files.pythonhosted.org/packages/cc/77/610aeee8d41e39080c7e14afa5387138e3c9fa9756ab893d09d99e7d8e98/rpds_py-0.27.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:e4b9fcfbc021633863a37e92571d6f91851fa656f0180246e84cbd8b3f6b329b", size = 361741 },
|
| 500 |
+
{ url = "https://files.pythonhosted.org/packages/3a/fc/c43765f201c6a1c60be2043cbdb664013def52460a4c7adace89d6682bf4/rpds_py-0.27.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1441811a96eadca93c517d08df75de45e5ffe68aa3089924f963c782c4b898cf", size = 345574 },
|
| 501 |
+
{ url = "https://files.pythonhosted.org/packages/20/42/ee2b2ca114294cd9847d0ef9c26d2b0851b2e7e00bf14cc4c0b581df0fc3/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55266dafa22e672f5a4f65019015f90336ed31c6383bd53f5e7826d21a0e0b83", size = 385051 },
|
| 502 |
+
{ url = "https://files.pythonhosted.org/packages/fd/e8/1e430fe311e4799e02e2d1af7c765f024e95e17d651612425b226705f910/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d78827d7ac08627ea2c8e02c9e5b41180ea5ea1f747e9db0915e3adf36b62dcf", size = 398395 },
|
| 503 |
+
{ url = "https://files.pythonhosted.org/packages/82/95/9dc227d441ff2670651c27a739acb2535ccaf8b351a88d78c088965e5996/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae92443798a40a92dc5f0b01d8a7c93adde0c4dc965310a29ae7c64d72b9fad2", size = 524334 },
|
| 504 |
+
{ url = "https://files.pythonhosted.org/packages/87/01/a670c232f401d9ad461d9a332aa4080cd3cb1d1df18213dbd0d2a6a7ab51/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c46c9dd2403b66a2a3b9720ec4b74d4ab49d4fabf9f03dfdce2d42af913fe8d0", size = 407691 },
|
| 505 |
+
{ url = "https://files.pythonhosted.org/packages/03/36/0a14aebbaa26fe7fab4780c76f2239e76cc95a0090bdb25e31d95c492fcd/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2efe4eb1d01b7f5f1939f4ef30ecea6c6b3521eec451fb93191bf84b2a522418", size = 386868 },
|
| 506 |
+
{ url = "https://files.pythonhosted.org/packages/3b/03/8c897fb8b5347ff6c1cc31239b9611c5bf79d78c984430887a353e1409a1/rpds_py-0.27.1-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:15d3b4d83582d10c601f481eca29c3f138d44c92187d197aff663a269197c02d", size = 405469 },
|
| 507 |
+
{ url = "https://files.pythonhosted.org/packages/da/07/88c60edc2df74850d496d78a1fdcdc7b54360a7f610a4d50008309d41b94/rpds_py-0.27.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4ed2e16abbc982a169d30d1a420274a709949e2cbdef119fe2ec9d870b42f274", size = 422125 },
|
| 508 |
+
{ url = "https://files.pythonhosted.org/packages/6b/86/5f4c707603e41b05f191a749984f390dabcbc467cf833769b47bf14ba04f/rpds_py-0.27.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a75f305c9b013289121ec0f1181931975df78738cdf650093e6b86d74aa7d8dd", size = 562341 },
|
| 509 |
+
{ url = "https://files.pythonhosted.org/packages/b2/92/3c0cb2492094e3cd9baf9e49bbb7befeceb584ea0c1a8b5939dca4da12e5/rpds_py-0.27.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:67ce7620704745881a3d4b0ada80ab4d99df390838839921f99e63c474f82cf2", size = 592511 },
|
| 510 |
+
{ url = "https://files.pythonhosted.org/packages/10/bb/82e64fbb0047c46a168faa28d0d45a7851cd0582f850b966811d30f67ad8/rpds_py-0.27.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9d992ac10eb86d9b6f369647b6a3f412fc0075cfd5d799530e84d335e440a002", size = 557736 },
|
| 511 |
+
{ url = "https://files.pythonhosted.org/packages/00/95/3c863973d409210da7fb41958172c6b7dbe7fc34e04d3cc1f10bb85e979f/rpds_py-0.27.1-cp313-cp313-win32.whl", hash = "sha256:4f75e4bd8ab8db624e02c8e2fc4063021b58becdbe6df793a8111d9343aec1e3", size = 221462 },
|
| 512 |
+
{ url = "https://files.pythonhosted.org/packages/ce/2c/5867b14a81dc217b56d95a9f2a40fdbc56a1ab0181b80132beeecbd4b2d6/rpds_py-0.27.1-cp313-cp313-win_amd64.whl", hash = "sha256:f9025faafc62ed0b75a53e541895ca272815bec18abe2249ff6501c8f2e12b83", size = 232034 },
|
| 513 |
+
{ url = "https://files.pythonhosted.org/packages/c7/78/3958f3f018c01923823f1e47f1cc338e398814b92d83cd278364446fac66/rpds_py-0.27.1-cp313-cp313-win_arm64.whl", hash = "sha256:ed10dc32829e7d222b7d3b93136d25a406ba9788f6a7ebf6809092da1f4d279d", size = 222392 },
|
| 514 |
+
{ url = "https://files.pythonhosted.org/packages/01/76/1cdf1f91aed5c3a7bf2eba1f1c4e4d6f57832d73003919a20118870ea659/rpds_py-0.27.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:92022bbbad0d4426e616815b16bc4127f83c9a74940e1ccf3cfe0b387aba0228", size = 358355 },
|
| 515 |
+
{ url = "https://files.pythonhosted.org/packages/c3/6f/bf142541229374287604caf3bb2a4ae17f0a580798fd72d3b009b532db4e/rpds_py-0.27.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:47162fdab9407ec3f160805ac3e154df042e577dd53341745fc7fb3f625e6d92", size = 342138 },
|
| 516 |
+
{ url = "https://files.pythonhosted.org/packages/1a/77/355b1c041d6be40886c44ff5e798b4e2769e497b790f0f7fd1e78d17e9a8/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb89bec23fddc489e5d78b550a7b773557c9ab58b7946154a10a6f7a214a48b2", size = 380247 },
|
| 517 |
+
{ url = "https://files.pythonhosted.org/packages/d6/a4/d9cef5c3946ea271ce2243c51481971cd6e34f21925af2783dd17b26e815/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e48af21883ded2b3e9eb48cb7880ad8598b31ab752ff3be6457001d78f416723", size = 390699 },
|
| 518 |
+
{ url = "https://files.pythonhosted.org/packages/3a/06/005106a7b8c6c1a7e91b73169e49870f4af5256119d34a361ae5240a0c1d/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6f5b7bd8e219ed50299e58551a410b64daafb5017d54bbe822e003856f06a802", size = 521852 },
|
| 519 |
+
{ url = "https://files.pythonhosted.org/packages/e5/3e/50fb1dac0948e17a02eb05c24510a8fe12d5ce8561c6b7b7d1339ab7ab9c/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08f1e20bccf73b08d12d804d6e1c22ca5530e71659e6673bce31a6bb71c1e73f", size = 402582 },
|
| 520 |
+
{ url = "https://files.pythonhosted.org/packages/cb/b0/f4e224090dc5b0ec15f31a02d746ab24101dd430847c4d99123798661bfc/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0dc5dceeaefcc96dc192e3a80bbe1d6c410c469e97bdd47494a7d930987f18b2", size = 384126 },
|
| 521 |
+
{ url = "https://files.pythonhosted.org/packages/54/77/ac339d5f82b6afff1df8f0fe0d2145cc827992cb5f8eeb90fc9f31ef7a63/rpds_py-0.27.1-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:d76f9cc8665acdc0c9177043746775aa7babbf479b5520b78ae4002d889f5c21", size = 399486 },
|
| 522 |
+
{ url = "https://files.pythonhosted.org/packages/d6/29/3e1c255eee6ac358c056a57d6d6869baa00a62fa32eea5ee0632039c50a3/rpds_py-0.27.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:134fae0e36022edad8290a6661edf40c023562964efea0cc0ec7f5d392d2aaef", size = 414832 },
|
| 523 |
+
{ url = "https://files.pythonhosted.org/packages/3f/db/6d498b844342deb3fa1d030598db93937a9964fcf5cb4da4feb5f17be34b/rpds_py-0.27.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb11a4f1b2b63337cfd3b4d110af778a59aae51c81d195768e353d8b52f88081", size = 557249 },
|
| 524 |
+
{ url = "https://files.pythonhosted.org/packages/60/f3/690dd38e2310b6f68858a331399b4d6dbb9132c3e8ef8b4333b96caf403d/rpds_py-0.27.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:13e608ac9f50a0ed4faec0e90ece76ae33b34c0e8656e3dceb9a7db994c692cd", size = 587356 },
|
| 525 |
+
{ url = "https://files.pythonhosted.org/packages/86/e3/84507781cccd0145f35b1dc32c72675200c5ce8d5b30f813e49424ef68fc/rpds_py-0.27.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dd2135527aa40f061350c3f8f89da2644de26cd73e4de458e79606384f4f68e7", size = 555300 },
|
| 526 |
+
{ url = "https://files.pythonhosted.org/packages/e5/ee/375469849e6b429b3516206b4580a79e9ef3eb12920ddbd4492b56eaacbe/rpds_py-0.27.1-cp313-cp313t-win32.whl", hash = "sha256:3020724ade63fe320a972e2ffd93b5623227e684315adce194941167fee02688", size = 216714 },
|
| 527 |
+
{ url = "https://files.pythonhosted.org/packages/21/87/3fc94e47c9bd0742660e84706c311a860dcae4374cf4a03c477e23ce605a/rpds_py-0.27.1-cp313-cp313t-win_amd64.whl", hash = "sha256:8ee50c3e41739886606388ba3ab3ee2aae9f35fb23f833091833255a31740797", size = 228943 },
|
| 528 |
+
{ url = "https://files.pythonhosted.org/packages/70/36/b6e6066520a07cf029d385de869729a895917b411e777ab1cde878100a1d/rpds_py-0.27.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:acb9aafccaae278f449d9c713b64a9e68662e7799dbd5859e2c6b3c67b56d334", size = 362472 },
|
| 529 |
+
{ url = "https://files.pythonhosted.org/packages/af/07/b4646032e0dcec0df9c73a3bd52f63bc6c5f9cda992f06bd0e73fe3fbebd/rpds_py-0.27.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b7fb801aa7f845ddf601c49630deeeccde7ce10065561d92729bfe81bd21fb33", size = 345676 },
|
| 530 |
+
{ url = "https://files.pythonhosted.org/packages/b0/16/2f1003ee5d0af4bcb13c0cf894957984c32a6751ed7206db2aee7379a55e/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fe0dd05afb46597b9a2e11c351e5e4283c741237e7f617ffb3252780cca9336a", size = 385313 },
|
| 531 |
+
{ url = "https://files.pythonhosted.org/packages/05/cd/7eb6dd7b232e7f2654d03fa07f1414d7dfc980e82ba71e40a7c46fd95484/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b6dfb0e058adb12d8b1d1b25f686e94ffa65d9995a5157afe99743bf7369d62b", size = 399080 },
|
| 532 |
+
{ url = "https://files.pythonhosted.org/packages/20/51/5829afd5000ec1cb60f304711f02572d619040aa3ec033d8226817d1e571/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed090ccd235f6fa8bb5861684567f0a83e04f52dfc2e5c05f2e4b1309fcf85e7", size = 523868 },
|
| 533 |
+
{ url = "https://files.pythonhosted.org/packages/05/2c/30eebca20d5db95720ab4d2faec1b5e4c1025c473f703738c371241476a2/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bf876e79763eecf3e7356f157540d6a093cef395b65514f17a356f62af6cc136", size = 408750 },
|
| 534 |
+
{ url = "https://files.pythonhosted.org/packages/90/1a/cdb5083f043597c4d4276eae4e4c70c55ab5accec078da8611f24575a367/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:12ed005216a51b1d6e2b02a7bd31885fe317e45897de81d86dcce7d74618ffff", size = 387688 },
|
| 535 |
+
{ url = "https://files.pythonhosted.org/packages/7c/92/cf786a15320e173f945d205ab31585cc43969743bb1a48b6888f7a2b0a2d/rpds_py-0.27.1-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:ee4308f409a40e50593c7e3bb8cbe0b4d4c66d1674a316324f0c2f5383b486f9", size = 407225 },
|
| 536 |
+
{ url = "https://files.pythonhosted.org/packages/33/5c/85ee16df5b65063ef26017bef33096557a4c83fbe56218ac7cd8c235f16d/rpds_py-0.27.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0b08d152555acf1f455154d498ca855618c1378ec810646fcd7c76416ac6dc60", size = 423361 },
|
| 537 |
+
{ url = "https://files.pythonhosted.org/packages/4b/8e/1c2741307fcabd1a334ecf008e92c4f47bb6f848712cf15c923becfe82bb/rpds_py-0.27.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:dce51c828941973a5684d458214d3a36fcd28da3e1875d659388f4f9f12cc33e", size = 562493 },
|
| 538 |
+
{ url = "https://files.pythonhosted.org/packages/04/03/5159321baae9b2222442a70c1f988cbbd66b9be0675dd3936461269be360/rpds_py-0.27.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:c1476d6f29eb81aa4151c9a31219b03f1f798dc43d8af1250a870735516a1212", size = 592623 },
|
| 539 |
+
{ url = "https://files.pythonhosted.org/packages/ff/39/c09fd1ad28b85bc1d4554a8710233c9f4cefd03d7717a1b8fbfd171d1167/rpds_py-0.27.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3ce0cac322b0d69b63c9cdb895ee1b65805ec9ffad37639f291dd79467bee675", size = 558800 },
|
| 540 |
+
{ url = "https://files.pythonhosted.org/packages/c5/d6/99228e6bbcf4baa764b18258f519a9035131d91b538d4e0e294313462a98/rpds_py-0.27.1-cp314-cp314-win32.whl", hash = "sha256:dfbfac137d2a3d0725758cd141f878bf4329ba25e34979797c89474a89a8a3a3", size = 221943 },
|
| 541 |
+
{ url = "https://files.pythonhosted.org/packages/be/07/c802bc6b8e95be83b79bdf23d1aa61d68324cb1006e245d6c58e959e314d/rpds_py-0.27.1-cp314-cp314-win_amd64.whl", hash = "sha256:a6e57b0abfe7cc513450fcf529eb486b6e4d3f8aee83e92eb5f1ef848218d456", size = 233739 },
|
| 542 |
+
{ url = "https://files.pythonhosted.org/packages/c8/89/3e1b1c16d4c2d547c5717377a8df99aee8099ff050f87c45cb4d5fa70891/rpds_py-0.27.1-cp314-cp314-win_arm64.whl", hash = "sha256:faf8d146f3d476abfee026c4ae3bdd9ca14236ae4e4c310cbd1cf75ba33d24a3", size = 223120 },
|
| 543 |
+
{ url = "https://files.pythonhosted.org/packages/62/7e/dc7931dc2fa4a6e46b2a4fa744a9fe5c548efd70e0ba74f40b39fa4a8c10/rpds_py-0.27.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:ba81d2b56b6d4911ce735aad0a1d4495e808b8ee4dc58715998741a26874e7c2", size = 358944 },
|
| 544 |
+
{ url = "https://files.pythonhosted.org/packages/e6/22/4af76ac4e9f336bfb1a5f240d18a33c6b2fcaadb7472ac7680576512b49a/rpds_py-0.27.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:84f7d509870098de0e864cad0102711c1e24e9b1a50ee713b65928adb22269e4", size = 342283 },
|
| 545 |
+
{ url = "https://files.pythonhosted.org/packages/1c/15/2a7c619b3c2272ea9feb9ade67a45c40b3eeb500d503ad4c28c395dc51b4/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9e960fc78fecd1100539f14132425e1d5fe44ecb9239f8f27f079962021523e", size = 380320 },
|
| 546 |
+
{ url = "https://files.pythonhosted.org/packages/a2/7d/4c6d243ba4a3057e994bb5bedd01b5c963c12fe38dde707a52acdb3849e7/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:62f85b665cedab1a503747617393573995dac4600ff51869d69ad2f39eb5e817", size = 391760 },
|
| 547 |
+
{ url = "https://files.pythonhosted.org/packages/b4/71/b19401a909b83bcd67f90221330bc1ef11bc486fe4e04c24388d28a618ae/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fed467af29776f6556250c9ed85ea5a4dd121ab56a5f8b206e3e7a4c551e48ec", size = 522476 },
|
| 548 |
+
{ url = "https://files.pythonhosted.org/packages/e4/44/1a3b9715c0455d2e2f0f6df5ee6d6f5afdc423d0773a8a682ed2b43c566c/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2729615f9d430af0ae6b36cf042cb55c0936408d543fb691e1a9e36648fd35a", size = 403418 },
|
| 549 |
+
{ url = "https://files.pythonhosted.org/packages/1c/4b/fb6c4f14984eb56673bc868a66536f53417ddb13ed44b391998100a06a96/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1b207d881a9aef7ba753d69c123a35d96ca7cb808056998f6b9e8747321f03b8", size = 384771 },
|
| 550 |
+
{ url = "https://files.pythonhosted.org/packages/c0/56/d5265d2d28b7420d7b4d4d85cad8ef891760f5135102e60d5c970b976e41/rpds_py-0.27.1-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:639fd5efec029f99b79ae47e5d7e00ad8a773da899b6309f6786ecaf22948c48", size = 400022 },
|
| 551 |
+
{ url = "https://files.pythonhosted.org/packages/8f/e9/9f5fc70164a569bdd6ed9046486c3568d6926e3a49bdefeeccfb18655875/rpds_py-0.27.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fecc80cb2a90e28af8a9b366edacf33d7a91cbfe4c2c4544ea1246e949cfebeb", size = 416787 },
|
| 552 |
+
{ url = "https://files.pythonhosted.org/packages/d4/64/56dd03430ba491db943a81dcdef115a985aac5f44f565cd39a00c766d45c/rpds_py-0.27.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:42a89282d711711d0a62d6f57d81aa43a1368686c45bc1c46b7f079d55692734", size = 557538 },
|
| 553 |
+
{ url = "https://files.pythonhosted.org/packages/3f/36/92cc885a3129993b1d963a2a42ecf64e6a8e129d2c7cc980dbeba84e55fb/rpds_py-0.27.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:cf9931f14223de59551ab9d38ed18d92f14f055a5f78c1d8ad6493f735021bbb", size = 588512 },
|
| 554 |
+
{ url = "https://files.pythonhosted.org/packages/dd/10/6b283707780a81919f71625351182b4f98932ac89a09023cb61865136244/rpds_py-0.27.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f39f58a27cc6e59f432b568ed8429c7e1641324fbe38131de852cd77b2d534b0", size = 555813 },
|
| 555 |
+
{ url = "https://files.pythonhosted.org/packages/04/2e/30b5ea18c01379da6272a92825dd7e53dc9d15c88a19e97932d35d430ef7/rpds_py-0.27.1-cp314-cp314t-win32.whl", hash = "sha256:d5fa0ee122dc09e23607a28e6d7b150da16c662e66409bbe85230e4c85bb528a", size = 217385 },
|
| 556 |
+
{ url = "https://files.pythonhosted.org/packages/32/7d/97119da51cb1dd3f2f3c0805f155a3aa4a95fa44fe7d78ae15e69edf4f34/rpds_py-0.27.1-cp314-cp314t-win_amd64.whl", hash = "sha256:6567d2bb951e21232c2f660c24cf3470bb96de56cdcb3f071a83feeaff8a2772", size = 230097 },
|
| 557 |
+
]
|
| 558 |
+
|
| 559 |
+
[[package]]
|
| 560 |
+
name = "six"
|
| 561 |
+
version = "1.17.0"
|
| 562 |
+
source = { registry = "https://pypi.org/simple" }
|
| 563 |
+
sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031 }
|
| 564 |
+
wheels = [
|
| 565 |
+
{ url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 },
|
| 566 |
+
]
|
| 567 |
+
|
| 568 |
+
[[package]]
|
| 569 |
+
name = "smmap"
|
| 570 |
+
version = "5.0.2"
|
| 571 |
+
source = { registry = "https://pypi.org/simple" }
|
| 572 |
+
sdist = { url = "https://files.pythonhosted.org/packages/44/cd/a040c4b3119bbe532e5b0732286f805445375489fceaec1f48306068ee3b/smmap-5.0.2.tar.gz", hash = "sha256:26ea65a03958fa0c8a1c7e8c7a58fdc77221b8910f6be2131affade476898ad5", size = 22329 }
|
| 573 |
+
wheels = [
|
| 574 |
+
{ url = "https://files.pythonhosted.org/packages/04/be/d09147ad1ec7934636ad912901c5fd7667e1c858e19d355237db0d0cd5e4/smmap-5.0.2-py3-none-any.whl", hash = "sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e", size = 24303 },
|
| 575 |
+
]
|
| 576 |
+
|
| 577 |
+
[[package]]
|
| 578 |
+
name = "streamlit"
|
| 579 |
+
version = "1.49.1"
|
| 580 |
+
source = { registry = "https://pypi.org/simple" }
|
| 581 |
+
dependencies = [
|
| 582 |
+
{ name = "altair" },
|
| 583 |
+
{ name = "blinker" },
|
| 584 |
+
{ name = "cachetools" },
|
| 585 |
+
{ name = "click" },
|
| 586 |
+
{ name = "gitpython" },
|
| 587 |
+
{ name = "numpy" },
|
| 588 |
+
{ name = "packaging" },
|
| 589 |
+
{ name = "pandas" },
|
| 590 |
+
{ name = "pillow" },
|
| 591 |
+
{ name = "protobuf" },
|
| 592 |
+
{ name = "pyarrow" },
|
| 593 |
+
{ name = "pydeck" },
|
| 594 |
+
{ name = "requests" },
|
| 595 |
+
{ name = "tenacity" },
|
| 596 |
+
{ name = "toml" },
|
| 597 |
+
{ name = "tornado" },
|
| 598 |
+
{ name = "typing-extensions" },
|
| 599 |
+
{ name = "watchdog", marker = "sys_platform != 'darwin'" },
|
| 600 |
+
]
|
| 601 |
+
sdist = { url = "https://files.pythonhosted.org/packages/24/17/c8024e4ef311dc7833987c603a7d0ebe82f8aa352aaca53b27be3f6b7f01/streamlit-1.49.1.tar.gz", hash = "sha256:6f213f1e43f035143a56f58ad50068d8a09482f0a2dad1050d7e7e99a9689818", size = 9640116 }
|
| 602 |
+
wheels = [
|
| 603 |
+
{ url = "https://files.pythonhosted.org/packages/85/9e/146cdef515ad07e56c3aa942d087562498592d441aa3bae845ef0cd8fca3/streamlit-1.49.1-py3-none-any.whl", hash = "sha256:ad7b6d0dc35db168587acf96f80378249467fc057ed739a41c511f6bf5aa173b", size = 10044388 },
|
| 604 |
+
]
|
| 605 |
+
|
| 606 |
+
[[package]]
|
| 607 |
+
name = "tenacity"
|
| 608 |
+
version = "9.1.2"
|
| 609 |
+
source = { registry = "https://pypi.org/simple" }
|
| 610 |
+
sdist = { url = "https://files.pythonhosted.org/packages/0a/d4/2b0cd0fe285e14b36db076e78c93766ff1d529d70408bd1d2a5a84f1d929/tenacity-9.1.2.tar.gz", hash = "sha256:1169d376c297e7de388d18b4481760d478b0e99a777cad3a9c86e556f4b697cb", size = 48036 }
|
| 611 |
+
wheels = [
|
| 612 |
+
{ url = "https://files.pythonhosted.org/packages/e5/30/643397144bfbfec6f6ef821f36f33e57d35946c44a2352d3c9f0ae847619/tenacity-9.1.2-py3-none-any.whl", hash = "sha256:f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138", size = 28248 },
|
| 613 |
+
]
|
| 614 |
+
|
| 615 |
+
[[package]]
|
| 616 |
+
name = "toml"
|
| 617 |
+
version = "0.10.2"
|
| 618 |
+
source = { registry = "https://pypi.org/simple" }
|
| 619 |
+
sdist = { url = "https://files.pythonhosted.org/packages/be/ba/1f744cdc819428fc6b5084ec34d9b30660f6f9daaf70eead706e3203ec3c/toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f", size = 22253 }
|
| 620 |
+
wheels = [
|
| 621 |
+
{ url = "https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", size = 16588 },
|
| 622 |
+
]
|
| 623 |
+
|
| 624 |
+
[[package]]
|
| 625 |
+
name = "tornado"
|
| 626 |
+
version = "6.5.2"
|
| 627 |
+
source = { registry = "https://pypi.org/simple" }
|
| 628 |
+
sdist = { url = "https://files.pythonhosted.org/packages/09/ce/1eb500eae19f4648281bb2186927bb062d2438c2e5093d1360391afd2f90/tornado-6.5.2.tar.gz", hash = "sha256:ab53c8f9a0fa351e2c0741284e06c7a45da86afb544133201c5cc8578eb076a0", size = 510821 }
|
| 629 |
+
wheels = [
|
| 630 |
+
{ url = "https://files.pythonhosted.org/packages/f6/48/6a7529df2c9cc12efd2e8f5dd219516184d703b34c06786809670df5b3bd/tornado-6.5.2-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:2436822940d37cde62771cff8774f4f00b3c8024fe482e16ca8387b8a2724db6", size = 442563 },
|
| 631 |
+
{ url = "https://files.pythonhosted.org/packages/f2/b5/9b575a0ed3e50b00c40b08cbce82eb618229091d09f6d14bce80fc01cb0b/tornado-6.5.2-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:583a52c7aa94ee046854ba81d9ebb6c81ec0fd30386d96f7640c96dad45a03ef", size = 440729 },
|
| 632 |
+
{ url = "https://files.pythonhosted.org/packages/1b/4e/619174f52b120efcf23633c817fd3fed867c30bff785e2cd5a53a70e483c/tornado-6.5.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b0fe179f28d597deab2842b86ed4060deec7388f1fd9c1b4a41adf8af058907e", size = 444295 },
|
| 633 |
+
{ url = "https://files.pythonhosted.org/packages/95/fa/87b41709552bbd393c85dd18e4e3499dcd8983f66e7972926db8d96aa065/tornado-6.5.2-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b186e85d1e3536d69583d2298423744740986018e393d0321df7340e71898882", size = 443644 },
|
| 634 |
+
{ url = "https://files.pythonhosted.org/packages/f9/41/fb15f06e33d7430ca89420283a8762a4e6b8025b800ea51796ab5e6d9559/tornado-6.5.2-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e792706668c87709709c18b353da1f7662317b563ff69f00bab83595940c7108", size = 443878 },
|
| 635 |
+
{ url = "https://files.pythonhosted.org/packages/11/92/fe6d57da897776ad2e01e279170ea8ae726755b045fe5ac73b75357a5a3f/tornado-6.5.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:06ceb1300fd70cb20e43b1ad8aaee0266e69e7ced38fa910ad2e03285009ce7c", size = 444549 },
|
| 636 |
+
{ url = "https://files.pythonhosted.org/packages/9b/02/c8f4f6c9204526daf3d760f4aa555a7a33ad0e60843eac025ccfd6ff4a93/tornado-6.5.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:74db443e0f5251be86cbf37929f84d8c20c27a355dd452a5cfa2aada0d001ec4", size = 443973 },
|
| 637 |
+
{ url = "https://files.pythonhosted.org/packages/ae/2d/f5f5707b655ce2317190183868cd0f6822a1121b4baeae509ceb9590d0bd/tornado-6.5.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b5e735ab2889d7ed33b32a459cac490eda71a1ba6857b0118de476ab6c366c04", size = 443954 },
|
| 638 |
+
{ url = "https://files.pythonhosted.org/packages/e8/59/593bd0f40f7355806bf6573b47b8c22f8e1374c9b6fd03114bd6b7a3dcfd/tornado-6.5.2-cp39-abi3-win32.whl", hash = "sha256:c6f29e94d9b37a95013bb669616352ddb82e3bfe8326fccee50583caebc8a5f0", size = 445023 },
|
| 639 |
+
{ url = "https://files.pythonhosted.org/packages/c7/2a/f609b420c2f564a748a2d80ebfb2ee02a73ca80223af712fca591386cafb/tornado-6.5.2-cp39-abi3-win_amd64.whl", hash = "sha256:e56a5af51cc30dd2cae649429af65ca2f6571da29504a07995175df14c18f35f", size = 445427 },
|
| 640 |
+
{ url = "https://files.pythonhosted.org/packages/5e/4f/e1f65e8f8c76d73658b33d33b81eed4322fb5085350e4328d5c956f0c8f9/tornado-6.5.2-cp39-abi3-win_arm64.whl", hash = "sha256:d6c33dc3672e3a1f3618eb63b7ef4683a7688e7b9e6e8f0d9aa5726360a004af", size = 444456 },
|
| 641 |
+
]
|
| 642 |
+
|
| 643 |
+
[[package]]
|
| 644 |
+
name = "typing-extensions"
|
| 645 |
+
version = "4.15.0"
|
| 646 |
+
source = { registry = "https://pypi.org/simple" }
|
| 647 |
+
sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391 }
|
| 648 |
+
wheels = [
|
| 649 |
+
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614 },
|
| 650 |
+
]
|
| 651 |
+
|
| 652 |
+
[[package]]
|
| 653 |
+
name = "tzdata"
|
| 654 |
+
version = "2025.2"
|
| 655 |
+
source = { registry = "https://pypi.org/simple" }
|
| 656 |
+
sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380 }
|
| 657 |
+
wheels = [
|
| 658 |
+
{ url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839 },
|
| 659 |
+
]
|
| 660 |
+
|
| 661 |
+
[[package]]
|
| 662 |
+
name = "urllib3"
|
| 663 |
+
version = "2.5.0"
|
| 664 |
+
source = { registry = "https://pypi.org/simple" }
|
| 665 |
+
sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185 }
|
| 666 |
+
wheels = [
|
| 667 |
+
{ url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795 },
|
| 668 |
+
]
|
| 669 |
+
|
| 670 |
+
[[package]]
|
| 671 |
+
name = "watchdog"
|
| 672 |
+
version = "6.0.0"
|
| 673 |
+
source = { registry = "https://pypi.org/simple" }
|
| 674 |
+
sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220 }
|
| 675 |
+
wheels = [
|
| 676 |
+
{ url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079 },
|
| 677 |
+
{ url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078 },
|
| 678 |
+
{ url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076 },
|
| 679 |
+
{ url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077 },
|
| 680 |
+
{ url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078 },
|
| 681 |
+
{ url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077 },
|
| 682 |
+
{ url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078 },
|
| 683 |
+
{ url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065 },
|
| 684 |
+
{ url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070 },
|
| 685 |
+
{ url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067 },
|
| 686 |
+
]
|
visualizations.py
ADDED
|
@@ -0,0 +1,588 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
import plotly.express as px
|
| 3 |
+
import plotly.graph_objects as go
|
| 4 |
+
from plotly.subplots import make_subplots
|
| 5 |
+
import pandas as pd
|
| 6 |
+
from typing import Dict, Any
|
| 7 |
+
from data_processor import DataProcessor
|
| 8 |
+
|
| 9 |
+
def create_visualizations(data_processor: DataProcessor) -> Dict[str, Any]:
|
| 10 |
+
"""
|
| 11 |
+
Create all visualizations for the Fetii dashboard.
|
| 12 |
+
Compatible with both Streamlit and Gradio interfaces.
|
| 13 |
+
"""
|
| 14 |
+
insights = data_processor.get_quick_insights()
|
| 15 |
+
df = data_processor.df
|
| 16 |
+
|
| 17 |
+
visualizations = {}
|
| 18 |
+
|
| 19 |
+
# Core visualizations - optimized for Gradio display
|
| 20 |
+
visualizations['hourly_distribution'] = create_hourly_chart(insights['hourly_distribution'])
|
| 21 |
+
visualizations['group_size_distribution'] = create_group_size_chart(insights['group_size_distribution'])
|
| 22 |
+
visualizations['popular_locations'] = create_locations_chart(insights['top_pickups'])
|
| 23 |
+
|
| 24 |
+
# Advanced visualizations
|
| 25 |
+
visualizations['time_heatmap'] = create_time_heatmap(df)
|
| 26 |
+
visualizations['daily_volume'] = create_daily_volume_chart(df)
|
| 27 |
+
visualizations['trip_distance_analysis'] = create_distance_analysis(df)
|
| 28 |
+
visualizations['location_comparison'] = create_location_comparison(df)
|
| 29 |
+
visualizations['peak_patterns'] = create_peak_patterns(df)
|
| 30 |
+
|
| 31 |
+
return visualizations
|
| 32 |
+
|
| 33 |
+
def create_hourly_chart(hourly_data: Dict[int, int]) -> go.Figure:
|
| 34 |
+
"""Create modern hourly distribution chart."""
|
| 35 |
+
hours = sorted(hourly_data.keys())
|
| 36 |
+
counts = [hourly_data[hour] for hour in hours]
|
| 37 |
+
|
| 38 |
+
# Create hour labels
|
| 39 |
+
hour_labels = []
|
| 40 |
+
for hour in hours:
|
| 41 |
+
if hour == 0:
|
| 42 |
+
hour_labels.append("12 AM")
|
| 43 |
+
elif hour < 12:
|
| 44 |
+
hour_labels.append(f"{hour} AM")
|
| 45 |
+
elif hour == 12:
|
| 46 |
+
hour_labels.append("12 PM")
|
| 47 |
+
else:
|
| 48 |
+
hour_labels.append(f"{hour-12} PM")
|
| 49 |
+
|
| 50 |
+
fig = go.Figure()
|
| 51 |
+
|
| 52 |
+
# Create modern gradient colors based on intensity
|
| 53 |
+
max_count = max(counts)
|
| 54 |
+
colors = []
|
| 55 |
+
for count in counts:
|
| 56 |
+
intensity = count / max_count
|
| 57 |
+
if intensity > 0.8:
|
| 58 |
+
colors.append('#667eea') # Primary gradient start
|
| 59 |
+
elif intensity > 0.6:
|
| 60 |
+
colors.append('#764ba2') # Primary gradient end
|
| 61 |
+
elif intensity > 0.4:
|
| 62 |
+
colors.append('#f093fb') # Secondary gradient start
|
| 63 |
+
elif intensity > 0.2:
|
| 64 |
+
colors.append('#4facfe') # Success gradient
|
| 65 |
+
else:
|
| 66 |
+
colors.append('#9ca3af') # Gray for low activity
|
| 67 |
+
|
| 68 |
+
fig.add_trace(go.Bar(
|
| 69 |
+
x=hour_labels,
|
| 70 |
+
y=counts,
|
| 71 |
+
marker=dict(
|
| 72 |
+
color=colors,
|
| 73 |
+
line=dict(color='rgba(255,255,255,0.8)', width=1)
|
| 74 |
+
),
|
| 75 |
+
name='Trips',
|
| 76 |
+
hovertemplate='<b>%{x}</b><br>Trips: %{y}<extra></extra>',
|
| 77 |
+
text=counts,
|
| 78 |
+
textposition='outside',
|
| 79 |
+
textfont=dict(color='#374151', size=10, family='Inter')
|
| 80 |
+
))
|
| 81 |
+
|
| 82 |
+
fig.update_layout(
|
| 83 |
+
title={
|
| 84 |
+
'text': 'Trip Distribution by Hour',
|
| 85 |
+
'x': 0.5,
|
| 86 |
+
'font': {'size': 16, 'color': '#1f2937', 'family': 'Inter'}
|
| 87 |
+
},
|
| 88 |
+
xaxis_title='Hour of Day',
|
| 89 |
+
yaxis_title='Number of Trips',
|
| 90 |
+
plot_bgcolor='rgba(0,0,0,0)',
|
| 91 |
+
paper_bgcolor='rgba(0,0,0,0)',
|
| 92 |
+
font={'color': '#374151', 'family': 'Inter'},
|
| 93 |
+
height=280,
|
| 94 |
+
margin=dict(t=50, b=40, l=40, r=40),
|
| 95 |
+
xaxis=dict(
|
| 96 |
+
showgrid=True,
|
| 97 |
+
gridwidth=1,
|
| 98 |
+
gridcolor='rgba(156, 163, 175, 0.2)',
|
| 99 |
+
showline=True,
|
| 100 |
+
linecolor='rgba(156, 163, 175, 0.3)'
|
| 101 |
+
),
|
| 102 |
+
yaxis=dict(
|
| 103 |
+
showgrid=True,
|
| 104 |
+
gridwidth=1,
|
| 105 |
+
gridcolor='rgba(156, 163, 175, 0.2)',
|
| 106 |
+
showline=True,
|
| 107 |
+
linecolor='rgba(156, 163, 175, 0.3)'
|
| 108 |
+
)
|
| 109 |
+
)
|
| 110 |
+
|
| 111 |
+
return fig
|
| 112 |
+
|
| 113 |
+
def create_group_size_chart(group_data: Dict[int, int]) -> go.Figure:
|
| 114 |
+
"""Create modern group size distribution chart."""
|
| 115 |
+
sizes = list(group_data.keys())
|
| 116 |
+
counts = list(group_data.values())
|
| 117 |
+
|
| 118 |
+
# Enhanced modern color palette with gradients
|
| 119 |
+
colors = [
|
| 120 |
+
'#667eea', '#764ba2', '#f093fb', '#f5576c',
|
| 121 |
+
'#4facfe', '#00f2fe', '#43e97b', '#38f9d7',
|
| 122 |
+
'#fa709a', '#fee140', '#a8edea', '#fed6e3'
|
| 123 |
+
]
|
| 124 |
+
|
| 125 |
+
fig = go.Figure()
|
| 126 |
+
|
| 127 |
+
fig.add_trace(go.Pie(
|
| 128 |
+
labels=[f"{size} passengers" for size in sizes],
|
| 129 |
+
values=counts,
|
| 130 |
+
marker=dict(
|
| 131 |
+
colors=colors[:len(sizes)],
|
| 132 |
+
line=dict(color='white', width=2)
|
| 133 |
+
),
|
| 134 |
+
hovertemplate='<b>%{label}</b><br>Trips: %{value}<br>Percentage: %{percent}<extra></extra>',
|
| 135 |
+
textinfo='label+percent',
|
| 136 |
+
textposition='auto',
|
| 137 |
+
textfont=dict(color='white', size=11, family='Inter'),
|
| 138 |
+
hole=0.4
|
| 139 |
+
))
|
| 140 |
+
|
| 141 |
+
fig.update_layout(
|
| 142 |
+
title={
|
| 143 |
+
'text': 'Group Size Distribution',
|
| 144 |
+
'x': 0.5,
|
| 145 |
+
'font': {'size': 16, 'color': '#1f2937', 'family': 'Inter'}
|
| 146 |
+
},
|
| 147 |
+
plot_bgcolor='rgba(0,0,0,0)',
|
| 148 |
+
paper_bgcolor='rgba(0,0,0,0)',
|
| 149 |
+
font={'color': '#374151', 'family': 'Inter'},
|
| 150 |
+
height=280,
|
| 151 |
+
margin=dict(t=50, b=40, l=40, r=40),
|
| 152 |
+
showlegend=False
|
| 153 |
+
)
|
| 154 |
+
|
| 155 |
+
return fig
|
| 156 |
+
|
| 157 |
+
def create_locations_chart(pickup_data: list) -> go.Figure:
|
| 158 |
+
"""Create modern popular locations chart."""
|
| 159 |
+
locations = [item[0] for item in pickup_data[:8]]
|
| 160 |
+
counts = [item[1] for item in pickup_data[:8]]
|
| 161 |
+
|
| 162 |
+
# Truncate long location names
|
| 163 |
+
truncated_locations = []
|
| 164 |
+
for loc in locations:
|
| 165 |
+
if len(loc) > 20:
|
| 166 |
+
truncated_locations.append(loc[:17] + "...")
|
| 167 |
+
else:
|
| 168 |
+
truncated_locations.append(loc)
|
| 169 |
+
|
| 170 |
+
fig = go.Figure()
|
| 171 |
+
|
| 172 |
+
# Enhanced gradient colors with modern palette
|
| 173 |
+
max_count = max(counts)
|
| 174 |
+
base_colors = ['#667eea', '#764ba2', '#f093fb', '#f5576c', '#4facfe', '#00f2fe', '#43e97b', '#38f9d7']
|
| 175 |
+
colors = []
|
| 176 |
+
for i, count in enumerate(counts):
|
| 177 |
+
base_color = base_colors[i % len(base_colors)]
|
| 178 |
+
# Convert hex to rgba with opacity based on intensity
|
| 179 |
+
hex_color = base_color.lstrip('#')
|
| 180 |
+
rgb = tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))
|
| 181 |
+
intensity = count / max_count
|
| 182 |
+
colors.append(f'rgba({rgb[0]}, {rgb[1]}, {rgb[2]}, {0.6 + intensity * 0.4})')
|
| 183 |
+
|
| 184 |
+
fig.add_trace(go.Bar(
|
| 185 |
+
x=counts,
|
| 186 |
+
y=truncated_locations,
|
| 187 |
+
orientation='h',
|
| 188 |
+
marker=dict(
|
| 189 |
+
color=colors,
|
| 190 |
+
line=dict(color='rgba(255,255,255,0.8)', width=1),
|
| 191 |
+
cornerradius=4
|
| 192 |
+
),
|
| 193 |
+
hovertemplate='<b>%{customdata}</b><br>Pickups: %{x}<extra></extra>',
|
| 194 |
+
customdata=locations,
|
| 195 |
+
text=counts,
|
| 196 |
+
textposition='outside',
|
| 197 |
+
textfont=dict(color='#374151', size=10, family='Inter')
|
| 198 |
+
))
|
| 199 |
+
|
| 200 |
+
fig.update_layout(
|
| 201 |
+
title={
|
| 202 |
+
'text': 'Top Pickup Locations',
|
| 203 |
+
'x': 0.5,
|
| 204 |
+
'font': {'size': 16, 'color': '#1f2937', 'family': 'Inter'}
|
| 205 |
+
},
|
| 206 |
+
xaxis_title='Number of Pickups',
|
| 207 |
+
yaxis_title='',
|
| 208 |
+
plot_bgcolor='rgba(0,0,0,0)',
|
| 209 |
+
paper_bgcolor='rgba(0,0,0,0)',
|
| 210 |
+
font={'color': '#374151', 'family': 'Inter'},
|
| 211 |
+
height=280,
|
| 212 |
+
margin=dict(t=50, b=40, l=120, r=40),
|
| 213 |
+
yaxis=dict(
|
| 214 |
+
autorange="reversed",
|
| 215 |
+
showline=True,
|
| 216 |
+
linecolor='rgba(156, 163, 175, 0.3)'
|
| 217 |
+
),
|
| 218 |
+
xaxis=dict(
|
| 219 |
+
showgrid=True,
|
| 220 |
+
gridwidth=1,
|
| 221 |
+
gridcolor='rgba(156, 163, 175, 0.2)',
|
| 222 |
+
showline=True,
|
| 223 |
+
linecolor='rgba(156, 163, 175, 0.3)'
|
| 224 |
+
)
|
| 225 |
+
)
|
| 226 |
+
|
| 227 |
+
return fig
|
| 228 |
+
|
| 229 |
+
def create_time_heatmap(df: pd.DataFrame) -> go.Figure:
|
| 230 |
+
"""Create advanced time-based heatmap."""
|
| 231 |
+
df_copy = df.copy()
|
| 232 |
+
df_copy['day_num'] = df_copy['datetime'].dt.dayofweek
|
| 233 |
+
df_copy['day_name'] = df_copy['datetime'].dt.day_name()
|
| 234 |
+
|
| 235 |
+
heatmap_data = df_copy.groupby(['day_num', 'hour']).size().reset_index(name='trips')
|
| 236 |
+
heatmap_pivot = heatmap_data.pivot(index='day_num', columns='hour', values='trips').fillna(0)
|
| 237 |
+
|
| 238 |
+
day_names = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
|
| 239 |
+
|
| 240 |
+
hour_labels = []
|
| 241 |
+
for hour in range(24):
|
| 242 |
+
if hour == 0:
|
| 243 |
+
hour_labels.append("12 AM")
|
| 244 |
+
elif hour < 12:
|
| 245 |
+
hour_labels.append(f"{hour} AM")
|
| 246 |
+
elif hour == 12:
|
| 247 |
+
hour_labels.append("12 PM")
|
| 248 |
+
else:
|
| 249 |
+
hour_labels.append(f"{hour-12} PM")
|
| 250 |
+
|
| 251 |
+
fig = go.Figure()
|
| 252 |
+
|
| 253 |
+
fig.add_trace(go.Heatmap(
|
| 254 |
+
z=heatmap_pivot.values,
|
| 255 |
+
x=hour_labels,
|
| 256 |
+
y=day_names,
|
| 257 |
+
colorscale=[
|
| 258 |
+
[0, '#f8fafc'],
|
| 259 |
+
[0.2, '#e2e8f0'],
|
| 260 |
+
[0.4, '#94a3b8'],
|
| 261 |
+
[0.6, '#3b82f6'],
|
| 262 |
+
[0.8, '#1d4ed8'],
|
| 263 |
+
[1, '#1e40af']
|
| 264 |
+
],
|
| 265 |
+
hovertemplate='<b>%{y}</b><br>%{x}<br>Trips: %{z}<extra></extra>',
|
| 266 |
+
colorbar=dict(
|
| 267 |
+
title=dict(text="Trips", font=dict(family='Inter', color='#374151')),
|
| 268 |
+
tickfont=dict(family='Inter', color='#374151')
|
| 269 |
+
)
|
| 270 |
+
))
|
| 271 |
+
|
| 272 |
+
fig.update_layout(
|
| 273 |
+
title={
|
| 274 |
+
'text': 'Trip Patterns by Day & Hour',
|
| 275 |
+
'x': 0.5,
|
| 276 |
+
'font': {'size': 16, 'color': '#1f2937', 'family': 'Inter', 'weight': 700}
|
| 277 |
+
},
|
| 278 |
+
xaxis_title='Hour of Day',
|
| 279 |
+
yaxis_title='Day of Week',
|
| 280 |
+
plot_bgcolor='rgba(248, 250, 252, 0.5)',
|
| 281 |
+
paper_bgcolor='rgba(0,0,0,0)',
|
| 282 |
+
font={'color': '#374151', 'family': 'Inter'},
|
| 283 |
+
height=350,
|
| 284 |
+
margin=dict(t=50, b=40, l=100, r=40),
|
| 285 |
+
xaxis=dict(
|
| 286 |
+
showgrid=True,
|
| 287 |
+
gridwidth=1,
|
| 288 |
+
gridcolor='rgba(156, 163, 175, 0.3)',
|
| 289 |
+
tickfont=dict(size=11)
|
| 290 |
+
),
|
| 291 |
+
yaxis=dict(
|
| 292 |
+
showgrid=True,
|
| 293 |
+
gridwidth=1,
|
| 294 |
+
gridcolor='rgba(156, 163, 175, 0.3)',
|
| 295 |
+
tickfont=dict(size=11)
|
| 296 |
+
)
|
| 297 |
+
)
|
| 298 |
+
|
| 299 |
+
return fig
|
| 300 |
+
|
| 301 |
+
def create_daily_volume_chart(df: pd.DataFrame) -> go.Figure:
|
| 302 |
+
"""Create modern daily trip volume chart."""
|
| 303 |
+
daily_trips = df.groupby('date').size().reset_index(name='trips')
|
| 304 |
+
daily_trips['date'] = pd.to_datetime(daily_trips['date'])
|
| 305 |
+
daily_trips = daily_trips.sort_values('date')
|
| 306 |
+
|
| 307 |
+
fig = go.Figure()
|
| 308 |
+
|
| 309 |
+
# Main line
|
| 310 |
+
fig.add_trace(go.Scatter(
|
| 311 |
+
x=daily_trips['date'],
|
| 312 |
+
y=daily_trips['trips'],
|
| 313 |
+
mode='lines+markers',
|
| 314 |
+
line=dict(color='#3b82f6', width=3, shape='spline'),
|
| 315 |
+
marker=dict(size=6, color='#1d4ed8', line=dict(color='white', width=1)),
|
| 316 |
+
fill='tonexty',
|
| 317 |
+
fillcolor='rgba(59, 130, 246, 0.1)',
|
| 318 |
+
hovertemplate='<b>%{x}</b><br>Trips: %{y}<extra></extra>',
|
| 319 |
+
name='Daily Trips'
|
| 320 |
+
))
|
| 321 |
+
|
| 322 |
+
# Add trend line
|
| 323 |
+
if len(daily_trips) > 1:
|
| 324 |
+
z = np.polyfit(range(len(daily_trips)), daily_trips['trips'], 1)
|
| 325 |
+
p = np.poly1d(z)
|
| 326 |
+
fig.add_trace(go.Scatter(
|
| 327 |
+
x=daily_trips['date'],
|
| 328 |
+
y=p(range(len(daily_trips))),
|
| 329 |
+
mode='lines',
|
| 330 |
+
line=dict(color='#ef4444', width=2, dash='dot'),
|
| 331 |
+
name='Trend',
|
| 332 |
+
hovertemplate='Trend: %{y:.0f}<extra></extra>'
|
| 333 |
+
))
|
| 334 |
+
|
| 335 |
+
fig.update_layout(
|
| 336 |
+
title={
|
| 337 |
+
'text': 'Daily Trip Volume',
|
| 338 |
+
'x': 0.5,
|
| 339 |
+
'font': {'size': 18, 'color': '#1f2937', 'family': 'Inter'}
|
| 340 |
+
},
|
| 341 |
+
xaxis_title='Date',
|
| 342 |
+
yaxis_title='Number of Trips',
|
| 343 |
+
plot_bgcolor='rgba(0,0,0,0)',
|
| 344 |
+
paper_bgcolor='rgba(0,0,0,0)',
|
| 345 |
+
font={'color': '#374151', 'family': 'Inter'},
|
| 346 |
+
height=320,
|
| 347 |
+
margin=dict(t=60, b=50, l=50, r=50),
|
| 348 |
+
showlegend=True,
|
| 349 |
+
legend=dict(
|
| 350 |
+
x=0.02,
|
| 351 |
+
y=0.98,
|
| 352 |
+
bgcolor='rgba(255,255,255,0.9)',
|
| 353 |
+
bordercolor='rgba(156, 163, 175, 0.3)',
|
| 354 |
+
borderwidth=1
|
| 355 |
+
),
|
| 356 |
+
xaxis=dict(
|
| 357 |
+
showgrid=True,
|
| 358 |
+
gridwidth=1,
|
| 359 |
+
gridcolor='rgba(156, 163, 175, 0.2)'
|
| 360 |
+
),
|
| 361 |
+
yaxis=dict(
|
| 362 |
+
showgrid=True,
|
| 363 |
+
gridwidth=1,
|
| 364 |
+
gridcolor='rgba(156, 163, 175, 0.2)'
|
| 365 |
+
)
|
| 366 |
+
)
|
| 367 |
+
|
| 368 |
+
return fig
|
| 369 |
+
|
| 370 |
+
def create_distance_analysis(df: pd.DataFrame) -> go.Figure:
|
| 371 |
+
"""Create group size vs trip distance analysis."""
|
| 372 |
+
if not all(col in df.columns for col in ['Pick Up Latitude', 'Pick Up Longitude', 'Drop Off Latitude', 'Drop Off Longitude']):
|
| 373 |
+
return create_placeholder_chart("Distance Analysis", "Location data not available")
|
| 374 |
+
|
| 375 |
+
df_copy = df.copy()
|
| 376 |
+
df_copy['distance'] = np.sqrt(
|
| 377 |
+
(df_copy['Drop Off Latitude'] - df_copy['Pick Up Latitude'])**2 +
|
| 378 |
+
(df_copy['Drop Off Longitude'] - df_copy['Pick Up Longitude'])**2
|
| 379 |
+
) * 111 # Approximate km conversion
|
| 380 |
+
|
| 381 |
+
distance_by_group = df_copy.groupby('Total Passengers')['distance'].agg(['mean', 'std', 'count']).reset_index()
|
| 382 |
+
distance_by_group = distance_by_group[distance_by_group['count'] >= 3] # Filter groups with few trips
|
| 383 |
+
|
| 384 |
+
fig = go.Figure()
|
| 385 |
+
|
| 386 |
+
fig.add_trace(go.Scatter(
|
| 387 |
+
x=distance_by_group['Total Passengers'],
|
| 388 |
+
y=distance_by_group['mean'],
|
| 389 |
+
mode='markers+lines',
|
| 390 |
+
marker=dict(
|
| 391 |
+
size=distance_by_group['count']/5,
|
| 392 |
+
color=distance_by_group['mean'],
|
| 393 |
+
colorscale='Viridis',
|
| 394 |
+
showscale=True,
|
| 395 |
+
colorbar=dict(title="Avg Distance (km)"),
|
| 396 |
+
line=dict(color='white', width=1)
|
| 397 |
+
),
|
| 398 |
+
line=dict(color='#3b82f6', width=2),
|
| 399 |
+
error_y=dict(
|
| 400 |
+
type='data',
|
| 401 |
+
array=distance_by_group['std'],
|
| 402 |
+
color='rgba(59, 130, 246, 0.3)'
|
| 403 |
+
),
|
| 404 |
+
hovertemplate='<b>Group Size: %{x}</b><br>Avg Distance: %{y:.2f} km<br>Trips: %{marker.size:.0f}<extra></extra>',
|
| 405 |
+
name='Average Distance'
|
| 406 |
+
))
|
| 407 |
+
|
| 408 |
+
fig.update_layout(
|
| 409 |
+
title={
|
| 410 |
+
'text': 'Average Trip Distance by Group Size',
|
| 411 |
+
'x': 0.5,
|
| 412 |
+
'font': {'size': 18, 'color': '#1f2937', 'family': 'Inter'}
|
| 413 |
+
},
|
| 414 |
+
xaxis_title='Group Size (Passengers)',
|
| 415 |
+
yaxis_title='Average Distance (km)',
|
| 416 |
+
plot_bgcolor='rgba(0,0,0,0)',
|
| 417 |
+
paper_bgcolor='rgba(0,0,0,0)',
|
| 418 |
+
font={'color': '#374151', 'family': 'Inter'},
|
| 419 |
+
height=400,
|
| 420 |
+
margin=dict(t=60, b=50, l=50, r=50)
|
| 421 |
+
)
|
| 422 |
+
|
| 423 |
+
return fig
|
| 424 |
+
|
| 425 |
+
def create_location_comparison(df: pd.DataFrame) -> go.Figure:
|
| 426 |
+
"""Create pickup vs dropoff location comparison."""
|
| 427 |
+
pickup_counts = df['pickup_main'].value_counts().head(10)
|
| 428 |
+
dropoff_counts = df['dropoff_main'].value_counts().head(10)
|
| 429 |
+
|
| 430 |
+
# Get common locations
|
| 431 |
+
common_locations = list(set(pickup_counts.index) & set(dropoff_counts.index))
|
| 432 |
+
if not common_locations:
|
| 433 |
+
# If no common locations, take top 5 from each
|
| 434 |
+
all_locations = list(set(list(pickup_counts.index[:5]) + list(dropoff_counts.index[:5])))
|
| 435 |
+
else:
|
| 436 |
+
all_locations = common_locations[:8]
|
| 437 |
+
|
| 438 |
+
pickup_values = [pickup_counts.get(loc, 0) for loc in all_locations]
|
| 439 |
+
dropoff_values = [dropoff_counts.get(loc, 0) for loc in all_locations]
|
| 440 |
+
|
| 441 |
+
# Truncate location names
|
| 442 |
+
truncated_locations = []
|
| 443 |
+
for loc in all_locations:
|
| 444 |
+
if len(loc) > 15:
|
| 445 |
+
truncated_locations.append(loc[:12] + "...")
|
| 446 |
+
else:
|
| 447 |
+
truncated_locations.append(loc)
|
| 448 |
+
|
| 449 |
+
fig = go.Figure()
|
| 450 |
+
|
| 451 |
+
fig.add_trace(go.Bar(
|
| 452 |
+
name='Pickups',
|
| 453 |
+
x=truncated_locations,
|
| 454 |
+
y=pickup_values,
|
| 455 |
+
marker_color='#3b82f6',
|
| 456 |
+
hovertemplate='<b>%{x}</b><br>Pickups: %{y}<extra></extra>',
|
| 457 |
+
customdata=all_locations
|
| 458 |
+
))
|
| 459 |
+
|
| 460 |
+
fig.add_trace(go.Bar(
|
| 461 |
+
name='Drop-offs',
|
| 462 |
+
x=truncated_locations,
|
| 463 |
+
y=dropoff_values,
|
| 464 |
+
marker_color='#10b981',
|
| 465 |
+
hovertemplate='<b>%{x}</b><br>Drop-offs: %{y}<extra></extra>',
|
| 466 |
+
customdata=all_locations
|
| 467 |
+
))
|
| 468 |
+
|
| 469 |
+
fig.update_layout(
|
| 470 |
+
title={
|
| 471 |
+
'text': 'Pickup vs Drop-off Comparison',
|
| 472 |
+
'x': 0.5,
|
| 473 |
+
'font': {'size': 18, 'color': '#1f2937', 'family': 'Inter'}
|
| 474 |
+
},
|
| 475 |
+
xaxis_title='Locations',
|
| 476 |
+
yaxis_title='Number of Trips',
|
| 477 |
+
plot_bgcolor='rgba(0,0,0,0)',
|
| 478 |
+
paper_bgcolor='rgba(0,0,0,0)',
|
| 479 |
+
font={'color': '#374151', 'family': 'Inter'},
|
| 480 |
+
height=400,
|
| 481 |
+
margin=dict(t=60, b=50, l=50, r=50),
|
| 482 |
+
barmode='group',
|
| 483 |
+
legend=dict(
|
| 484 |
+
x=0.02,
|
| 485 |
+
y=0.98,
|
| 486 |
+
bgcolor='rgba(255,255,255,0.9)',
|
| 487 |
+
bordercolor='rgba(156, 163, 175, 0.3)',
|
| 488 |
+
borderwidth=1
|
| 489 |
+
)
|
| 490 |
+
)
|
| 491 |
+
|
| 492 |
+
return fig
|
| 493 |
+
|
| 494 |
+
def create_peak_patterns(df: pd.DataFrame) -> go.Figure:
|
| 495 |
+
"""Create peak hours analysis by group size category."""
|
| 496 |
+
df_copy = df.copy()
|
| 497 |
+
df_copy['group_category'] = df_copy['Total Passengers'].apply(
|
| 498 |
+
lambda x: 'Small (1-4)' if x <= 4 else
|
| 499 |
+
'Medium (5-8)' if x <= 8 else
|
| 500 |
+
'Large (9-12)' if x <= 12 else
|
| 501 |
+
'Extra Large (13+)'
|
| 502 |
+
)
|
| 503 |
+
|
| 504 |
+
hourly_by_group = df_copy.groupby(['group_category', 'hour']).size().reset_index(name='trips')
|
| 505 |
+
|
| 506 |
+
fig = go.Figure()
|
| 507 |
+
|
| 508 |
+
colors = ['#3b82f6', '#10b981', '#f59e0b', '#ef4444']
|
| 509 |
+
categories = ['Small (1-4)', 'Medium (5-8)', 'Large (9-12)', 'Extra Large (13+)']
|
| 510 |
+
|
| 511 |
+
for i, category in enumerate(categories):
|
| 512 |
+
data = hourly_by_group[hourly_by_group['group_category'] == category]
|
| 513 |
+
if not data.empty:
|
| 514 |
+
fig.add_trace(go.Scatter(
|
| 515 |
+
x=data['hour'],
|
| 516 |
+
y=data['trips'],
|
| 517 |
+
mode='lines+markers',
|
| 518 |
+
name=category,
|
| 519 |
+
line=dict(color=colors[i], width=3, shape='spline'),
|
| 520 |
+
marker=dict(size=6, line=dict(color='white', width=1)),
|
| 521 |
+
hovertemplate='<b>%{fullData.name}</b><br>Hour: %{x}<br>Trips: %{y}<extra></extra>'
|
| 522 |
+
))
|
| 523 |
+
|
| 524 |
+
fig.update_layout(
|
| 525 |
+
title={
|
| 526 |
+
'text': 'Peak Hours by Group Size Category',
|
| 527 |
+
'x': 0.5,
|
| 528 |
+
'font': {'size': 18, 'color': '#1f2937', 'family': 'Inter'}
|
| 529 |
+
},
|
| 530 |
+
xaxis_title='Hour of Day',
|
| 531 |
+
yaxis_title='Number of Trips',
|
| 532 |
+
plot_bgcolor='rgba(0,0,0,0)',
|
| 533 |
+
paper_bgcolor='rgba(0,0,0,0)',
|
| 534 |
+
font={'color': '#374151', 'family': 'Inter'},
|
| 535 |
+
height=400,
|
| 536 |
+
margin=dict(t=60, b=50, l=50, r=50),
|
| 537 |
+
legend=dict(
|
| 538 |
+
x=0.02,
|
| 539 |
+
y=0.98,
|
| 540 |
+
bgcolor='rgba(255,255,255,0.9)',
|
| 541 |
+
bordercolor='rgba(156, 163, 175, 0.3)',
|
| 542 |
+
borderwidth=1
|
| 543 |
+
),
|
| 544 |
+
xaxis=dict(
|
| 545 |
+
showgrid=True,
|
| 546 |
+
gridwidth=1,
|
| 547 |
+
gridcolor='rgba(156, 163, 175, 0.2)',
|
| 548 |
+
tickvals=list(range(0, 24, 2)),
|
| 549 |
+
ticktext=[f"{h}:00" for h in range(0, 24, 2)]
|
| 550 |
+
),
|
| 551 |
+
yaxis=dict(
|
| 552 |
+
showgrid=True,
|
| 553 |
+
gridwidth=1,
|
| 554 |
+
gridcolor='rgba(156, 163, 175, 0.2)'
|
| 555 |
+
)
|
| 556 |
+
)
|
| 557 |
+
|
| 558 |
+
return fig
|
| 559 |
+
|
| 560 |
+
def create_placeholder_chart(title: str, message: str) -> go.Figure:
|
| 561 |
+
"""Create a placeholder chart when data is not available."""
|
| 562 |
+
fig = go.Figure()
|
| 563 |
+
|
| 564 |
+
fig.add_annotation(
|
| 565 |
+
text=message,
|
| 566 |
+
x=0.5,
|
| 567 |
+
y=0.5,
|
| 568 |
+
xref="paper",
|
| 569 |
+
yref="paper",
|
| 570 |
+
showarrow=False,
|
| 571 |
+
font=dict(size=16, color='#6b7280', family='Inter')
|
| 572 |
+
)
|
| 573 |
+
|
| 574 |
+
fig.update_layout(
|
| 575 |
+
title={
|
| 576 |
+
'text': title,
|
| 577 |
+
'x': 0.5,
|
| 578 |
+
'font': {'size': 18, 'color': '#1f2937', 'family': 'Inter'}
|
| 579 |
+
},
|
| 580 |
+
plot_bgcolor='rgba(0,0,0,0)',
|
| 581 |
+
paper_bgcolor='rgba(0,0,0,0)',
|
| 582 |
+
height=300,
|
| 583 |
+
margin=dict(t=60, b=50, l=50, r=50),
|
| 584 |
+
xaxis=dict(showgrid=False, showticklabels=False),
|
| 585 |
+
yaxis=dict(showgrid=False, showticklabels=False)
|
| 586 |
+
)
|
| 587 |
+
|
| 588 |
+
return fig
|