parthsinha commited on
Commit
ed0810b
·
1 Parent(s): a249a73

added files

Browse files
Files changed (13) hide show
  1. .gitignore +207 -0
  2. LICENSE +21 -0
  3. README.md +292 -13
  4. app.py +872 -0
  5. chatbot_engine.py +418 -0
  6. config.py +249 -0
  7. data_processor.py +228 -0
  8. fetii_data.csv +0 -0
  9. pyproject.toml +13 -0
  10. requirements.txt +5 -0
  11. utils.py +251 -0
  12. uv.lock +686 -0
  13. 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
- title: Fetii AI
3
- emoji: 🔥
4
- colorFrom: pink
5
- colorTo: indigo
6
- sdk: gradio
7
- sdk_version: 5.46.1
8
- app_file: app.py
9
- pinned: false
10
- license: mit
11
- ---
12
-
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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