Shreneek commited on
Commit
a9b4255
·
verified ·
1 Parent(s): cb78a28

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +694 -0
app.py ADDED
@@ -0,0 +1,694 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # streamlit_app.py - Bolt Driver Recommendation System
2
+ import streamlit as st
3
+ import pandas as pd
4
+ import numpy as np
5
+ import matplotlib.pyplot as plt
6
+ import seaborn as sns
7
+ import plotly.express as px
8
+ import plotly.graph_objects as go
9
+ from datetime import datetime, timedelta
10
+ import folium
11
+ from folium.plugins import HeatMap, MarkerCluster
12
+ from streamlit_folium import folium_static
13
+ import pickle
14
+ import os
15
+
16
+ # Set page configuration
17
+ st.set_page_config(
18
+ page_title="Bolt Driver Recommendation System",
19
+ page_icon="🚖",
20
+ layout="wide",
21
+ initial_sidebar_state="expanded"
22
+ )
23
+
24
+ # Custom CSS styling
25
+ st.markdown("""
26
+ <style>
27
+ .main-header {
28
+ font-size: 2.5rem;
29
+ color: #272D37;
30
+ text-align: center;
31
+ margin-bottom: 1rem;
32
+ font-weight: bold;
33
+ }
34
+ .sub-header {
35
+ font-size: 1.8rem;
36
+ color: #272D37;
37
+ margin-top: 1.5rem;
38
+ margin-bottom: 1rem;
39
+ }
40
+ .section-header {
41
+ font-size: 1.3rem;
42
+ color: #272D37;
43
+ margin-top: 1rem;
44
+ margin-bottom: 0.5rem;
45
+ font-weight: bold;
46
+ }
47
+ .highlight {
48
+ background-color: #F0F2F6;
49
+ padding: 1rem;
50
+ border-radius: 0.5rem;
51
+ margin-bottom: 1rem;
52
+ }
53
+ .card {
54
+ background-color: white;
55
+ border-radius: 0.5rem;
56
+ padding: 1.5rem;
57
+ box-shadow: 0 0.15rem 1.75rem 0 rgba(58, 59, 69, 0.15);
58
+ margin-bottom: 1rem;
59
+ }
60
+ .info-box {
61
+ background-color: #e8f4f8;
62
+ border-left: 5px solid #4e8cff;
63
+ padding: 0.8rem;
64
+ border-radius: 0.3rem;
65
+ margin-bottom: 1rem;
66
+ }
67
+ .metric-container {
68
+ display: flex;
69
+ justify-content: space-between;
70
+ gap: 1rem;
71
+ }
72
+ .metric-card {
73
+ background-color: white;
74
+ border-radius: 0.5rem;
75
+ padding: 1rem;
76
+ text-align: center;
77
+ box-shadow: 0 0.15rem 1.75rem 0 rgba(58, 59, 69, 0.15);
78
+ flex: 1;
79
+ }
80
+ .metric-value {
81
+ font-size: 1.8rem;
82
+ font-weight: bold;
83
+ color: #272D37;
84
+ }
85
+ .metric-label {
86
+ font-size: 0.9rem;
87
+ color: #6e707e;
88
+ }
89
+ </style>
90
+ """, unsafe_allow_html=True)
91
+
92
+ # Header and app description
93
+ st.markdown('<div class="main-header">Bolt Driver Recommendation System</div>', unsafe_allow_html=True)
94
+
95
+ with st.container():
96
+ st.markdown('<div class="info-box">This application helps Bolt drivers find optimal areas to position themselves based on predicted ride demand and value. The recommendations are personalized based on time, location, and driver preferences.</div>', unsafe_allow_html=True)
97
+
98
+ class DemandPredictionModel:
99
+ def __init__(self):
100
+ """Initialize the demand prediction model"""
101
+ # In a real app, we would load the model from a file
102
+ # Here we'll create a dummy version for demonstration
103
+ self.setup_demo_data()
104
+
105
+ def setup_demo_data(self):
106
+ """Set up demonstration data based on our analysis"""
107
+ # Define geographic boundaries (Tallinn)
108
+ self.min_lat, self.max_lat = 59.32, 59.57
109
+ self.min_lng, self.max_lng = 24.51, 24.97
110
+
111
+ # Create grid
112
+ grid_size = 10
113
+ self.lat_step = (self.max_lat - self.min_lat) / grid_size
114
+ self.lng_step = (self.max_lng - self.min_lng) / grid_size
115
+
116
+ # Generate lat/lng bins
117
+ self.lat_bins = np.linspace(self.min_lat, self.max_lat, grid_size + 1)
118
+ self.lng_bins = np.linspace(self.min_lng, self.max_lng, grid_size + 1)
119
+
120
+ # Create demand patterns based on our findings
121
+ self.demand_patterns = self.create_demand_patterns()
122
+
123
+ def create_demand_patterns(self):
124
+ """Create realistic demand patterns based on our analysis"""
125
+ # Initialize 4D array: [day_of_week][hour][lat_bin][lng_bin]
126
+ days = 7
127
+ hours = 24
128
+ lat_bins = len(self.lat_bins) - 1
129
+ lng_bins = len(self.lng_bins) - 1
130
+
131
+ demand_patterns = np.zeros((days, hours, lat_bins, lng_bins))
132
+ value_patterns = np.zeros((days, hours, lat_bins, lng_bins))
133
+
134
+ # Key areas from our analysis
135
+ city_center = {"lat_idx": 4, "lng_idx": 5, "base_demand": 300, "value": 1.91}
136
+ secondary_hub = {"lat_idx": 4, "lng_idx": 4, "base_demand": 150, "value": 1.94}
137
+ university_area = {"lat_idx": 3, "lng_idx": 4, "base_demand": 80, "value": 2.89}
138
+ residential_zone = {"lat_idx": 3, "lng_idx": 3, "base_demand": 60, "value": 1.85}
139
+ business_district = {"lat_idx": 4, "lng_idx": 6, "base_demand": 50, "value": 1.56}
140
+
141
+ hotspots = [city_center, secondary_hub, university_area, residential_zone, business_district]
142
+
143
+ # Time patterns
144
+ hourly_factors = {
145
+ 0: 0.5, 1: 0.4, 2: 0.3, 3: 0.3, 4: 0.3, 5: 0.5,
146
+ 6: 0.8, 7: 0.9, 8: 0.7, 9: 0.6, 10: 0.6, 11: 0.6,
147
+ 12: 0.7, 13: 0.8, 14: 0.9, 15: 1.0, 16: 1.0, 17: 0.8,
148
+ 18: 0.7, 19: 0.7, 20: 0.7, 21: 0.8, 22: 0.9, 23: 0.7
149
+ }
150
+
151
+ # Value patterns - certain times have higher values
152
+ value_factors = {
153
+ 0: 1.4, 1: 0.8, 2: 1.0, 3: 0.6, 4: 1.6, 5: 0.7,
154
+ 6: 0.9, 7: 1.1, 8: 1.0, 9: 0.7, 10: 0.8, 11: 1.1,
155
+ 12: 0.8, 13: 0.9, 14: 1.6, 15: 0.9, 16: 0.8, 17: 1.0,
156
+ 18: 0.8, 19: 0.7, 20: 1.1, 21: 0.8, 22: 1.0, 23: 1.2
157
+ }
158
+
159
+ # Day patterns
160
+ day_factors = {
161
+ 0: 0.8, # Monday
162
+ 1: 0.9, # Tuesday
163
+ 2: 0.9, # Wednesday
164
+ 3: 0.85, # Thursday
165
+ 4: 0.95, # Friday
166
+ 5: 1.0, # Saturday
167
+ 6: 0.8 # Sunday
168
+ }
169
+
170
+ # Fill the demand patterns
171
+ for day in range(days):
172
+ for hour in range(hours):
173
+ # Apply base patterns with temporal variations
174
+ time_factor = hourly_factors[hour] * day_factors[day]
175
+
176
+ # Add some specific day-hour combinations
177
+ # Tuesday and Thursday early morning and late night have higher values
178
+ special_value_factor = 1.0
179
+ if (day == 1 or day == 3) and (hour in [4, 22, 23]):
180
+ special_value_factor = 2.0
181
+
182
+ for spot in hotspots:
183
+ lat_idx, lng_idx = spot["lat_idx"], spot["lng_idx"]
184
+ base_demand = spot["base_demand"]
185
+ base_value = spot["value"]
186
+
187
+ # Set demand
188
+ demand = base_demand * time_factor
189
+ # Add some randomness
190
+ demand *= np.random.uniform(0.9, 1.1)
191
+ demand_patterns[day, hour, lat_idx, lng_idx] = demand
192
+
193
+ # Set value
194
+ value = base_value * value_factors[hour] * special_value_factor
195
+ # Add some randomness
196
+ value *= np.random.uniform(0.95, 1.05)
197
+ value_patterns[day, hour, lat_idx, lng_idx] = value
198
+
199
+ # Add some spillover to neighboring cells
200
+ for d_lat in [-1, 0, 1]:
201
+ for d_lng in [-1, 0, 1]:
202
+ if d_lat == 0 and d_lng == 0:
203
+ continue
204
+
205
+ n_lat = lat_idx + d_lat
206
+ n_lng = lng_idx + d_lng
207
+
208
+ if (0 <= n_lat < lat_bins and 0 <= n_lng < lng_bins):
209
+ # Spillover decreases with distance
210
+ distance = np.sqrt(d_lat**2 + d_lng**2)
211
+ spillover_factor = 0.5 / distance
212
+
213
+ demand_patterns[day, hour, n_lat, n_lng] += demand * spillover_factor
214
+ value_patterns[day, hour, n_lat, n_lng] += value * 0.9 # Slightly lower values in spillover areas
215
+
216
+ # Create combined dict
217
+ patterns = {
218
+ "demand": demand_patterns,
219
+ "value": value_patterns
220
+ }
221
+
222
+ return patterns
223
+
224
+ def predict(self, day, hour, current_lat=None, current_lng=None, value_weight=0.5, top_n=5):
225
+ """
226
+ Predict high-demand areas for a given day and hour
227
+
228
+ Parameters:
229
+ - day: Day of week (0=Monday, 6=Sunday)
230
+ - hour: Hour of day (0-23)
231
+ - current_lat: Driver's current latitude (optional)
232
+ - current_lng: Driver's current longitude (optional)
233
+ - value_weight: Weight for balancing demand vs value (0-1)
234
+ - top_n: Number of recommendations to return
235
+
236
+ Returns:
237
+ - List of recommended areas
238
+ """
239
+ demand_matrix = self.demand_patterns["demand"][day, hour]
240
+ value_matrix = self.demand_patterns["value"][day, hour]
241
+
242
+ # Flatten the matrices for ranking
243
+ recommendations = []
244
+
245
+ for lat_idx in range(len(self.lat_bins) - 1):
246
+ for lng_idx in range(len(self.lng_bins) - 1):
247
+ demand = demand_matrix[lat_idx, lng_idx]
248
+ value = value_matrix[lat_idx, lng_idx]
249
+
250
+ if demand > 0:
251
+ center_lat = (self.lat_bins[lat_idx] + self.lat_bins[lat_idx + 1]) / 2
252
+ center_lng = (self.lng_bins[lng_idx] + self.lng_bins[lng_idx + 1]) / 2
253
+
254
+ # Calculate distance if driver location provided
255
+ distance_km = None
256
+ if current_lat is not None and current_lng is not None:
257
+ # Calculate Haversine distance
258
+ R = 6371 # Earth radius in kilometers
259
+ dLat = np.radians(current_lat - center_lat)
260
+ dLon = np.radians(current_lng - center_lng)
261
+ a = (np.sin(dLat/2) * np.sin(dLat/2) +
262
+ np.cos(np.radians(current_lat)) * np.cos(np.radians(center_lat)) *
263
+ np.sin(dLon/2) * np.sin(dLon/2))
264
+ c = 2 * np.arctan2(np.sqrt(a), np.sqrt(1-a))
265
+ distance_km = R * c
266
+
267
+ # Scale demand and value for scoring
268
+ max_demand = np.max(demand_matrix)
269
+ max_value = np.max(value_matrix)
270
+
271
+ demand_score = demand / max_demand if max_demand > 0 else 0
272
+ value_score = value / max_value if max_value > 0 else 0
273
+
274
+ # Combined score based on value weight
275
+ score = (1 - value_weight) * demand_score + value_weight * value_score
276
+
277
+ # Adjust for distance if available
278
+ if distance_km is not None:
279
+ # Distance penalty (decreases as distance increases)
280
+ # Effective range ~10km
281
+ distance_penalty = 1.0 / (1.0 + distance_km / 5.0)
282
+ adjusted_score = score * distance_penalty
283
+ else:
284
+ adjusted_score = score
285
+
286
+ recommendations.append({
287
+ "center_lat": center_lat,
288
+ "center_lng": center_lng,
289
+ "predicted_rides": demand,
290
+ "avg_value": value,
291
+ "expected_value": demand * value,
292
+ "score": score,
293
+ "adjusted_score": adjusted_score,
294
+ "distance_km": distance_km
295
+ })
296
+
297
+ # Sort by adjusted score
298
+ sorted_recommendations = sorted(recommendations, key=lambda x: x["adjusted_score"], reverse=True)
299
+
300
+ return sorted_recommendations[:top_n]
301
+
302
+ # Main application flow
303
+ def main():
304
+ # Initialize model
305
+ model = DemandPredictionModel()
306
+
307
+ # Sidebar for inputs
308
+ with st.sidebar:
309
+ st.markdown('<div class="section-header">Driver Options</div>', unsafe_allow_html=True)
310
+
311
+ # Time selection
312
+ st.subheader("Time Selection")
313
+
314
+ today = datetime.now()
315
+ days = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
316
+ selected_day = st.selectbox("Day of Week", days, index=today.weekday())
317
+ day_idx = days.index(selected_day)
318
+
319
+ selected_hour = st.slider("Hour of Day", 0, 23, today.hour, format="%d:00")
320
+
321
+ # Location input
322
+ st.subheader("Driver Location")
323
+ use_location = st.checkbox("Use Current Location", value=True)
324
+
325
+ # Default to Tallinn center
326
+ default_lat, default_lng = 59.436, 24.753
327
+
328
+ if use_location:
329
+ col1, col2 = st.columns(2)
330
+ with col1:
331
+ current_lat = st.number_input("Latitude", value=default_lat, format="%.5f", step=0.001)
332
+ with col2:
333
+ current_lng = st.number_input("Longitude", value=default_lng, format="%.5f", step=0.001)
334
+ else:
335
+ current_lat, current_lng = None, None
336
+
337
+ # Preference settings
338
+ st.subheader("Preferences")
339
+
340
+ num_recommendations = st.slider("Number of Recommendations", 3, 10, 5)
341
+
342
+ value_weight = st.slider(
343
+ "Optimization Balance",
344
+ min_value=0.0,
345
+ max_value=1.0,
346
+ value=0.5,
347
+ step=0.1,
348
+ help="0 = Focus on ride count, 1 = Focus on ride value"
349
+ )
350
+
351
+ # Advanced options for visual
352
+ st.subheader("Display Options")
353
+ show_heatmap = st.checkbox("Show Demand Heatmap", value=True)
354
+
355
+ # Generate recommendations
356
+ recommendations = model.predict(
357
+ day=day_idx,
358
+ hour=selected_hour,
359
+ current_lat=current_lat if use_location else None,
360
+ current_lng=current_lng if use_location else None,
361
+ value_weight=value_weight,
362
+ top_n=num_recommendations
363
+ )
364
+
365
+ # Main content area
366
+ col1, col2 = st.columns([3, 2])
367
+
368
+ with col1:
369
+ st.markdown('<div class="section-header">Demand Map</div>', unsafe_allow_html=True)
370
+
371
+ # Create map
372
+ m = folium.Map(
373
+ location=[59.436, 24.753], # Tallinn center
374
+ zoom_start=12,
375
+ tiles="CartoDB positron"
376
+ )
377
+
378
+ # Add driver marker if location provided
379
+ if use_location:
380
+ folium.Marker(
381
+ location=[current_lat, current_lng],
382
+ popup="Your Location",
383
+ icon=folium.Icon(color="blue", icon="user", prefix="fa"),
384
+ tooltip="Your Current Location"
385
+ ).add_to(m)
386
+
387
+ # Add recommendation markers
388
+ for i, rec in enumerate(recommendations):
389
+ folium.CircleMarker(
390
+ location=[rec["center_lat"], rec["center_lng"]],
391
+ radius=20,
392
+ color="red",
393
+ fill=True,
394
+ fill_color="red",
395
+ fill_opacity=0.6,
396
+ popup=f"""
397
+ <b>Recommendation {i+1}</b><br>
398
+ Expected rides: {rec['predicted_rides']:.1f}<br>
399
+ Avg value: €{rec['avg_value']:.2f}<br>
400
+ Expected value: €{rec['expected_value']:.2f}<br>
401
+ {f'Distance: {rec["distance_km"]:.2f} km' if rec["distance_km"] is not None else ''}
402
+ """
403
+ ).add_to(m)
404
+
405
+ # Add number label
406
+ folium.Marker(
407
+ location=[rec["center_lat"], rec["center_lng"]],
408
+ icon=folium.DivIcon(
409
+ html=f"""
410
+ <div style="
411
+ font-size: 12pt;
412
+ color: white;
413
+ font-weight: bold;
414
+ text-align: center;
415
+ width: 25px;
416
+ height: 25px;
417
+ line-height: 25px;
418
+ ">{i+1}</div>
419
+ """
420
+ )
421
+ ).add_to(m)
422
+
423
+ # Add heatmap if enabled
424
+ if show_heatmap:
425
+ # Get a larger set of predictions for the heatmap
426
+ all_predictions = model.predict(day_idx, selected_hour, top_n=100)
427
+ heat_data = [
428
+ [pred["center_lat"], pred["center_lng"], pred["predicted_rides"]]
429
+ for pred in all_predictions
430
+ ]
431
+
432
+ # Add heatmap layer
433
+ HeatMap(
434
+ heat_data,
435
+ radius=15,
436
+ gradient={
437
+ 0.2: 'blue',
438
+ 0.4: 'lime',
439
+ 0.6: 'yellow',
440
+ 0.8: 'orange',
441
+ 1.0: 'red'
442
+ },
443
+ name="Demand Heatmap",
444
+ show=True
445
+ ).add_to(m)
446
+
447
+ # Add layer control
448
+ folium.LayerControl().add_to(m)
449
+
450
+ # Display the map
451
+ folium_static(m, width=700)
452
+
453
+ with col2:
454
+ st.markdown('<div class="section-header">Recommendations</div>', unsafe_allow_html=True)
455
+
456
+ # Create metrics for top recommendation
457
+ if recommendations:
458
+ top_rec = recommendations[0]
459
+
460
+ st.markdown('<div class="highlight">', unsafe_allow_html=True)
461
+ st.subheader("Top Recommendation")
462
+
463
+ col1, col2 = st.columns(2)
464
+ with col1:
465
+ st.metric("Expected Rides", f"{top_rec['predicted_rides']:.1f}")
466
+ st.metric("Avg Value", f"€{top_rec['avg_value']:.2f}")
467
+ with col2:
468
+ st.metric("Expected Value", f"€{top_rec['expected_value']:.2f}")
469
+ if top_rec["distance_km"] is not None:
470
+ st.metric("Distance", f"{top_rec['distance_km']:.2f} km")
471
+
472
+ st.markdown(f"Location: [{top_rec['center_lat']:.4f}, {top_rec['center_lng']:.4f}]")
473
+ st.markdown('</div>', unsafe_allow_html=True)
474
+
475
+ # Create formatted table of all recommendations
476
+ st.subheader("All Recommendations")
477
+
478
+ rec_df = pd.DataFrame(recommendations)
479
+
480
+ # Format for display
481
+ display_df = pd.DataFrame({
482
+ "Rank": range(1, len(rec_df) + 1),
483
+ "Expected Rides": rec_df["predicted_rides"].round(1),
484
+ "Avg Value (€)": rec_df["avg_value"].round(2),
485
+ "Expected Value (€)": rec_df["expected_value"].round(2)
486
+ })
487
+
488
+ # Add distance if available
489
+ if "distance_km" in rec_df.columns and rec_df["distance_km"].notna().any():
490
+ display_df["Distance (km)"] = rec_df["distance_km"].round(2)
491
+
492
+ st.table(display_df)
493
+
494
+ # Add explanation for score calculation
495
+ st.markdown('<div class="info-box">', unsafe_allow_html=True)
496
+ st.markdown("**How recommendations are calculated:**")
497
+ st.markdown("""
498
+ - Ride count predictions based on historical patterns
499
+ - Value based on average ride fares
500
+ - Recommendations balanced by your preferences
501
+ - Distance factored in when location is provided
502
+ """)
503
+ st.markdown('</div>', unsafe_allow_html=True)
504
+
505
+ # Time series visualization
506
+ st.markdown('<div class="section-header">Demand Patterns Analysis</div>', unsafe_allow_html=True)
507
+
508
+ tab1, tab2 = st.tabs(["Hourly Patterns", "Daily Patterns"])
509
+
510
+ with tab1:
511
+ # Generate hourly demand data for the selected day
512
+ hourly_data = []
513
+ for hour in range(24):
514
+ hour_recs = model.predict(day_idx, hour, top_n=100)
515
+ total_demand = sum(rec["predicted_rides"] for rec in hour_recs)
516
+ avg_value = sum(rec["avg_value"] * rec["predicted_rides"] for rec in hour_recs) / total_demand if total_demand > 0 else 0
517
+
518
+ hourly_data.append({
519
+ "hour": hour,
520
+ "demand": total_demand,
521
+ "value": avg_value
522
+ })
523
+
524
+ hourly_df = pd.DataFrame(hourly_data)
525
+
526
+ # Create dual-axis chart
527
+ fig = go.Figure()
528
+
529
+ # Add demand line
530
+ fig.add_trace(go.Scatter(
531
+ x=hourly_df["hour"],
532
+ y=hourly_df["demand"],
533
+ name="Demand",
534
+ line=dict(color="#4e8cff", width=3),
535
+ hovertemplate="Hour: %{x}<br>Demand: %{y:.1f}<extra></extra>"
536
+ ))
537
+
538
+ # Add value line on secondary axis
539
+ fig.add_trace(go.Scatter(
540
+ x=hourly_df["hour"],
541
+ y=hourly_df["value"],
542
+ name="Avg Value (€)",
543
+ line=dict(color="#ff6b6b", width=3, dash="dot"),
544
+ yaxis="y2",
545
+ hovertemplate="Hour: %{x}<br>Avg Value: €%{y:.2f}<extra></extra>"
546
+ ))
547
+
548
+ # Highlight selected hour
549
+ fig.add_vline(
550
+ x=selected_hour,
551
+ line_width=2,
552
+ line_dash="dash",
553
+ line_color="green",
554
+ annotation_text="Selected Hour",
555
+ annotation_position="top right"
556
+ )
557
+
558
+ # Update layout
559
+ fig.update_layout(
560
+ title=f"Hourly Demand Pattern for {selected_day}",
561
+ xaxis=dict(
562
+ title="Hour of Day",
563
+ tickmode="linear",
564
+ tick0=0,
565
+ dtick=1
566
+ ),
567
+ yaxis=dict(
568
+ title="Demand (Expected Rides)",
569
+ titlefont=dict(color="#4e8cff"),
570
+ tickfont=dict(color="#4e8cff")
571
+ ),
572
+ yaxis2=dict(
573
+ title="Average Value (€)",
574
+ titlefont=dict(color="#ff6b6b"),
575
+ tickfont=dict(color="#ff6b6b"),
576
+ anchor="x",
577
+ overlaying="y",
578
+ side="right"
579
+ ),
580
+ hovermode="x unified",
581
+ legend=dict(
582
+ orientation="h",
583
+ yanchor="bottom",
584
+ y=1.02,
585
+ xanchor="center",
586
+ x=0.5
587
+ )
588
+ )
589
+
590
+ st.plotly_chart(fig, use_container_width=True)
591
+
592
+ # Add observations
593
+ st.markdown("""
594
+ **Key Observations:**
595
+ - Peak demand typically occurs between 15:00-18:00 (3-6 PM)
596
+ - Early morning hours (4-5 AM) often show higher average ride values
597
+ - Morning rush hour (6-9 AM) shows moderate demand with variable values
598
+ """)
599
+
600
+ with tab2:
601
+ # Generate daily demand data
602
+ daily_data = []
603
+ for day in range(7):
604
+ peak_hour = 17 if day < 5 else 22 # Weekday peak at 5pm, weekend peak at 10pm
605
+ day_recs = model.predict(day, peak_hour, top_n=100)
606
+ total_demand = sum(rec["predicted_rides"] for rec in day_recs)
607
+ avg_value = sum(rec["avg_value"] * rec["predicted_rides"] for rec in day_recs) / total_demand if total_demand > 0 else 0
608
+
609
+ daily_data.append({
610
+ "day": days[day],
611
+ "demand": total_demand,
612
+ "value": avg_value
613
+ })
614
+
615
+ daily_df = pd.DataFrame(daily_data)
616
+
617
+ # Create bar chart
618
+ fig = px.bar(
619
+ daily_df,
620
+ x="day",
621
+ y="demand",
622
+ color="value",
623
+ color_continuous_scale="Viridis",
624
+ labels={
625
+ "day": "Day of Week",
626
+ "demand": "Peak Demand (Expected Rides)",
627
+ "value": "Avg Value (€)"
628
+ },
629
+ title="Peak Demand by Day of Week"
630
+ )
631
+
632
+ # Highlight selected day
633
+ fig.add_vline(
634
+ x=selected_day,
635
+ line_width=2,
636
+ line_dash="dash",
637
+ line_color="red",
638
+ annotation_text="Selected Day",
639
+ annotation_position="top right"
640
+ )
641
+
642
+ # Update layout
643
+ fig.update_layout(
644
+ xaxis=dict(categoryorder="array", categoryarray=days),
645
+ coloraxis_colorbar=dict(title="Avg Value (€)")
646
+ )
647
+
648
+ st.plotly_chart(fig, use_container_width=True)
649
+
650
+ # Add observations
651
+ st.markdown("""
652
+ **Key Observations:**
653
+ - Weekends (especially Saturday) typically show higher demand
654
+ - Tuesday and Thursday often have higher average ride values
655
+ - Weekend nights show different demand patterns than weekday nights
656
+ """)
657
+
658
+ # Footer section with additional information
659
+ st.markdown('<div class="section-header">Tips for Drivers</div>', unsafe_allow_html=True)
660
+
661
+ tips_col1, tips_col2, tips_col3 = st.columns(3)
662
+
663
+ with tips_col1:
664
+ st.markdown('<div class="card">', unsafe_allow_html=True)
665
+ st.subheader("Best Times")
666
+ st.markdown("""
667
+ - **Weekdays**: 7-9 AM, 4-6 PM
668
+ - **Weekends**: 10 PM - 2 AM
669
+ - **High Value**: Tuesday & Thursday early morning (4-5 AM) and late night (10 PM-12 AM)
670
+ """)
671
+ st.markdown('</div>', unsafe_allow_html=True)
672
+
673
+ with tips_col2:
674
+ st.markdown('<div class="card">', unsafe_allow_html=True)
675
+ st.subheader("Best Areas")
676
+ st.markdown("""
677
+ - **City Center**: Consistent demand throughout the day
678
+ - **University Area**: Higher value rides, especially weekdays
679
+ - **Business District**: Good during morning rush hours
680
+ """)
681
+ st.markdown('</div>', unsafe_allow_html=True)
682
+
683
+ with tips_col3:
684
+ st.markdown('<div class="card">', unsafe_allow_html=True)
685
+ st.subheader("Strategy Tips")
686
+ st.markdown("""
687
+ - Position 5-10 minutes before peak times
688
+ - Balance high-volume vs high-value areas
689
+ - For longer shifts, start with high-value rides then switch to high-volume
690
+ """)
691
+ st.markdown('</div>', unsafe_allow_html=True)
692
+
693
+ if __name__ == "__main__":
694
+ main()