naheedakhtar commited on
Commit
dde51ca
·
verified ·
1 Parent(s): b591ad0

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +404 -38
src/streamlit_app.py CHANGED
@@ -1,40 +1,406 @@
1
- import altair as alt
2
- import numpy as np
3
- import pandas as pd
4
  import streamlit as st
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
 
6
- """
7
- # Welcome to Streamlit!
8
-
9
- Edit `/streamlit_app.py` to customize this app to your heart's desire :heart:.
10
- If you have any questions, checkout our [documentation](https://docs.streamlit.io) and [community
11
- forums](https://discuss.streamlit.io).
12
-
13
- In the meantime, below is an example of what you can do with just a few lines of code:
14
- """
15
-
16
- num_points = st.slider("Number of points in spiral", 1, 10000, 1100)
17
- num_turns = st.slider("Number of turns in spiral", 1, 300, 31)
18
-
19
- indices = np.linspace(0, 1, num_points)
20
- theta = 2 * np.pi * num_turns * indices
21
- radius = indices
22
-
23
- x = radius * np.cos(theta)
24
- y = radius * np.sin(theta)
25
-
26
- df = pd.DataFrame({
27
- "x": x,
28
- "y": y,
29
- "idx": indices,
30
- "rand": np.random.randn(num_points),
31
- })
32
-
33
- st.altair_chart(alt.Chart(df, height=700, width=700)
34
- .mark_point(filled=True)
35
- .encode(
36
- x=alt.X("x", axis=None),
37
- y=alt.Y("y", axis=None),
38
- color=alt.Color("idx", legend=None, scale=alt.Scale()),
39
- size=alt.Size("rand", legend=None, scale=alt.Scale(range=[1, 150])),
40
- ))
 
 
 
 
1
  import streamlit as st
2
+ import pandas as pd
3
+ import re
4
+ import matplotlib.pyplot as plt
5
+ import matplotlib.dates as mdates
6
+ import seaborn as sns
7
+ from datetime import datetime
8
+ import io
9
+
10
+ # Set page config
11
+ st.set_page_config(
12
+ page_title="Driver Analysis Tool",
13
+ page_icon="🚌",
14
+ layout="wide"
15
+ )
16
+
17
+ # Custom function to create download buttons for dataframes
18
+ def download_dataframe(df, filename, button_text):
19
+ """
20
+ Creates a download button for a dataframe
21
+ """
22
+ buffer = io.BytesIO()
23
+ with pd.ExcelWriter(buffer, engine='xlsxwriter') as writer:
24
+ df.to_excel(writer, index=False)
25
+ buffer.seek(0)
26
+ return st.download_button(
27
+ label=button_text,
28
+ data=buffer,
29
+ file_name=filename,
30
+ mime="application/vnd.ms-excel"
31
+ )
32
+
33
+ # Function for dataframes with index
34
+ def download_dataframe_with_index(df, filename, button_text, index_label=None):
35
+ """
36
+ Creates a download button for a dataframe with index
37
+ """
38
+ buffer = io.BytesIO()
39
+ with pd.ExcelWriter(buffer, engine='xlsxwriter') as writer:
40
+ df.to_excel(writer, index=True, index_label=index_label)
41
+ buffer.seek(0)
42
+ return st.download_button(
43
+ label=button_text,
44
+ data=buffer,
45
+ file_name=filename,
46
+ mime="application/vnd.ms-excel"
47
+ )
48
+
49
+ # Title and description
50
+ st.title("📊 Driver Analysis Tool")
51
+ st.markdown("""
52
+ This application analyzes driver duties, schedules, and utilization.
53
+ Upload the required files below to start your analysis.
54
+ """)
55
+
56
+ # Sidebar for inputs
57
+ st.sidebar.header("Upload Files")
58
+
59
+ # Upload duty codes
60
+ duty_codes_file = st.sidebar.file_uploader("Upload duty codes Excel file", type=['xlsx'])
61
+ if duty_codes_file:
62
+ duty_df = pd.read_excel(duty_codes_file)
63
+ st.sidebar.success("Duty codes loaded successfully!")
64
+ else:
65
+ st.sidebar.warning("Please upload duty codes file")
66
+ duty_df = None
67
+
68
+ # Upload roster file
69
+ roster_file = st.sidebar.file_uploader("Upload roster Excel file", type=['xlsx'])
70
+ if roster_file:
71
+ st.sidebar.success("Roster file loaded successfully!")
72
+ else:
73
+ st.sidebar.warning("Please upload roster file")
74
+
75
+ # Upload driver master file
76
+ driver_master_file = st.sidebar.file_uploader("Upload driver master Excel file", type=['xlsx'])
77
+ if driver_master_file:
78
+ st.sidebar.success("Driver master file loaded successfully!")
79
+ else:
80
+ st.sidebar.warning("Please upload driver master file")
81
+
82
+ # Add tabs for different analyses
83
+ tabs = st.tabs(["Duty Overview", "Summary by Date", "Driver Summary", "Utilization Analysis", "Red Flags"])
84
+
85
+ # Only proceed if all files are uploaded
86
+ if duty_codes_file and roster_file and driver_master_file and duty_df is not None:
87
+ # Process data
88
+ with st.spinner("Processing data..."):
89
+ # Create duty dictionary
90
+ duty_dict = duty_df.set_index('duty_code')['category'].to_dict()
91
+
92
+ # Read the roster Excel file
93
+ xls = pd.ExcelFile(roster_file)
94
+
95
+ # Initialize an empty list to store DataFrames
96
+ df_list = []
97
+
98
+ # Read each sheet and append rows to the list
99
+ for sheet_name in xls.sheet_names:
100
+ sheet_df = pd.read_excel(xls, sheet_name=sheet_name, dtype={'emp_num': str})
101
+ # Drop leading zeroes from emp_num
102
+ sheet_df['emp_num'] = sheet_df['emp_num'].str.lstrip('0')
103
+ # Filter out columns without a heading
104
+ sheet_df = sheet_df.loc[:, ~sheet_df.columns.astype(str).str.contains('^Unnamed')]
105
+ df_list.append(sheet_df)
106
+
107
+ # Concatenate all DataFrames in the list vertically
108
+ df = pd.concat(df_list, ignore_index=True)
109
+
110
+ # Ensure column names are strings and remove time part
111
+ df.columns = [str(col).split(' ')[0] if ' ' in str(col) else str(col) for col in df.columns]
112
+
113
+ # Exclude 'emp_num' from date conversion
114
+ date_cols = df.columns[~df.columns.isin(['emp_num'])]
115
+
116
+ # Convert array of dates in string format from yyyy-mm-dd to dd-mm-yyyy
117
+ converted_cols = []
118
+ for date in date_cols:
119
+ try:
120
+ converted_cols.append(datetime.strptime(date, '%Y-%m-%d').strftime('%d-%m-%Y'))
121
+ except:
122
+ converted_cols.append(date)
123
+
124
+ df.columns = ['emp_num'] + converted_cols
125
+
126
+ # Read the driver master file
127
+ driver_master = pd.read_excel(driver_master_file, dtype={'emp_num': str})
128
+
129
+ # Function to classify the cell value
130
+ def classify_value(value):
131
+ pattern1 = r"^\d{5,6}-(?:[A-Za-z]\d{4,6}|\d{5,7})$"
132
+ pattern = r"^\d{7}[A-Z]\d{6}$" # Matches strings like 1234567M123456
133
+ pattern2 = r"^[1-9][A-Z]\d{3}$" # Matches strings like 4A123
134
+ pattern3 = r'^\d{6}-[A-Z]{2}\d{5}$'
135
+
136
+ # Check for NaN values
137
+ if pd.isna(value) or value == 0:
138
+ return 'not-available'
139
+
140
+ # Check for predefined patterns
141
+ value_str = str(value)
142
+ if re.search(pattern, value_str) or re.search(pattern1, value_str) or re.search(pattern2, value_str) or re.search(pattern3, value_str):
143
+ return 'duty'
144
+
145
+ # Check the duty_dict for matching keys
146
+ for duty_code, category in duty_dict.items():
147
+ if duty_code in value_str:
148
+ return category
149
+
150
+ # Default classification
151
+ return 'duty'
152
+
153
+ # Apply the classification function to each cell except 'emp_num'
154
+ classified_df = df.set_index('emp_num').applymap(classify_value).reset_index()
155
+
156
+ # Create a copy for date analysis
157
+ classified_datedf = classified_df.copy()
158
+
159
+ # Drop the 'emp_num' column for date summary
160
+ date_summary_df = classified_datedf.drop(columns=['emp_num'])
161
+
162
+ # Calculate the total for each date based on the classification
163
+ summary_datedf = date_summary_df.apply(lambda x: x.value_counts()).fillna(0).T
164
+
165
+ # Ensure the index is in datetime format and sorted
166
+ summary_datedf.index = pd.to_datetime(summary_datedf.index, format='%d-%m-%Y')
167
+ summary_datedf = summary_datedf.sort_index()
168
+
169
+ # Add total column
170
+ summary_datedf['total'] = summary_datedf.sum(axis=1)
171
+
172
+ # Function to extract date from column names
173
+ def extract_date(column_name):
174
+ if isinstance(column_name, str):
175
+ match = re.search(r'\d{2}-\d{2}-\d{4}', column_name)
176
+ return match.group(0) if match else column_name
177
+ return column_name
178
+
179
+ # Apply the function to strip column names, excluding date type columns
180
+ non_date_columns = classified_df.select_dtypes(exclude=['datetime']).columns
181
+ classified_df.columns = [extract_date(col) if col in non_date_columns and col != 'emp_num' else col for col in classified_df.columns]
182
+
183
+ # Create a summary DataFrame grouped by employee
184
+ summary_df = classified_df.groupby('emp_num').apply(lambda x: pd.Series({
185
+ 'Duty': (x == 'duty').sum().sum(),
186
+ 'Spare': (x == 'spare').sum().sum(),
187
+ 'Not-Available': (x == 'not-available').sum().sum(),
188
+ 'Day Off': (x == 'day-off').sum().sum(),
189
+ 'Vacation': (x == 'vacation').sum().sum(),
190
+ 'Support Duty': (x == 'support').sum().sum(),
191
+ 'Training': (x == 'training').sum().sum(),
192
+ 'Total': len(x.columns) - 1 # Subtract 1 for emp_num column
193
+ })).reset_index()
194
+
195
+ # Calculate 'Spare / Duty' percentage
196
+ summary_df['Spare / Duty'] = (summary_df['Spare'] / (summary_df['Duty'] + 0.001) * 100).round(2)
197
+
198
+ # Merge summary_df with driver_master to get depot and other information
199
+ summary_df = summary_df.merge(
200
+ driver_master[['emp_num', 'Depot', 'Cost_Center', 'Assignment', 'Nationality']],
201
+ on='emp_num',
202
+ how='left'
203
+ )
204
+
205
+ # Calculate utilization
206
+ summary_df['utilization'] = (
207
+ (summary_df['Duty'] + summary_df['Support Duty'] + summary_df['Training'] + summary_df['Day Off']) /
208
+ (summary_df['Total'] - summary_df['Vacation'] - summary_df['Not-Available'])
209
+ ).round(2) * 100
210
+
211
+ # Define utilization ranges
212
+ bins = [0, 20, 40, 60, 80, 100]
213
+ labels = ['0-20', '20-40', '40-60', '60-80', '80-100']
214
+ summary_df['Utilization_Range'] = pd.cut(summary_df['utilization'], bins=bins, labels=labels, include_lowest=True)
215
+
216
+ # Red flag filters
217
+ high_spare_df = summary_df[
218
+ (summary_df['utilization'] < 50) &
219
+ (summary_df['Spare / Duty'] > 50) &
220
+ ((summary_df['Spare'] + summary_df['Duty']) > summary_df['Not-Available'])
221
+ ]
222
+
223
+ high_util_df = summary_df[
224
+ (summary_df['Spare / Duty'] < 5) &
225
+ ((summary_df['Spare'] + summary_df['Duty']) > summary_df['Not-Available'])
226
+ ]
227
+
228
+ # Tab 1: Duty Overview
229
+ with tabs[0]:
230
+ st.header("Duty Codes Overview")
231
+ st.dataframe(duty_df)
232
+
233
+ st.header("Processed Duty Data Sample")
234
+ st.dataframe(classified_df.head(10))
235
+
236
+ # Add download button
237
+ download_dataframe(classified_df, "planned_duties.xlsx", "Download Processed Duties")
238
+
239
+ # Tab 2: Summary by Date
240
+ with tabs[1]:
241
+ st.header("Summary by Date")
242
+
243
+ # Format the index back to string for display
244
+ display_date_df = summary_datedf.copy()
245
+ display_date_df.index = display_date_df.index.strftime('%d-%m-%Y')
246
+ st.dataframe(display_date_df)
247
+
248
+ # Format for download
249
+ download_df = summary_datedf.copy()
250
+ download_df.index = download_df.index.strftime('%d-%m-%Y')
251
+
252
+ # Add download button
253
+ download_dataframe_with_index(download_df, "summary_by_date_planned.xlsx", "Download Date Summary", "Date")
254
+
255
+ st.header("Daily Trend Visualization")
256
+
257
+ # Restore datetime index for plotting
258
+ summary_datedf.index = pd.to_datetime(summary_datedf.index, format='%d-%m-%Y')
259
+
260
+ fig, ax = plt.subplots(figsize=(10, 6))
261
+ ax.plot(summary_datedf.index, summary_datedf['day-off'], label='Day Off', marker='o')
262
+ ax.plot(summary_datedf.index, summary_datedf['duty'], label='Duty', marker='o')
263
+ ax.plot(summary_datedf.index, summary_datedf['spare'], label='Spare', marker='o')
264
+ ax.plot(summary_datedf.index, summary_datedf['support'], label='Support', marker='o')
265
+ ax.plot(summary_datedf.index, summary_datedf['not-available'], label='Not-Available', marker='o')
266
+
267
+ # Set the labels and title
268
+ ax.set_xlabel('Date')
269
+ ax.set_ylabel('Count')
270
+ ax.set_title('Daily Count of Day Off, Duty, Vacation and Spare')
271
+ ax.legend()
272
+
273
+ # Rotate the x-axis labels for better readability
274
+ plt.xticks(rotation=45)
275
+
276
+ # Set weekly ticks for the x-axis
277
+ ax.xaxis.set_major_locator(mdates.WeekdayLocator(interval=1))
278
+ ax.xaxis.set_major_formatter(mdates.DateFormatter('%d-%m-%Y'))
279
+
280
+ plt.tight_layout()
281
+ st.pyplot(fig)
282
+
283
+ # Tab 3: Driver Summary
284
+ with tabs[2]:
285
+ st.header("Driver Summary")
286
+ st.dataframe(summary_df)
287
+
288
+ # Add download button
289
+ download_dataframe(summary_df, "planned_summary.xlsx", "Download Driver Summary")
290
+
291
+ # Add Depot filter
292
+ st.subheader("Filter by Depot")
293
+ selected_depot = st.selectbox("Select a depot", ["All"] + list(summary_df["Depot"].unique()))
294
+
295
+ if selected_depot != "All":
296
+ filtered_summary = summary_df[summary_df["Depot"] == selected_depot]
297
+ else:
298
+ filtered_summary = summary_df
299
+
300
+ st.dataframe(filtered_summary)
301
+
302
+ # Add download button for filtered data
303
+ if selected_depot != "All":
304
+ download_dataframe(filtered_summary, f"planned_summary_{selected_depot}.xlsx", f"Download {selected_depot} Summary")
305
+
306
+ # Tab 4: Utilization Analysis
307
+ with tabs[3]:
308
+ st.header("Driver Utilization Analysis")
309
+
310
+ col1, col2 = st.columns(2)
311
+
312
+ with col1:
313
+ st.subheader("Distribution by Utilization Range")
314
+
315
+ # Group by 'Utilization_Range' and count drivers
316
+ utilization_range_df = summary_df.groupby('Utilization_Range').size().reset_index(name='Number_of_Drivers')
317
+
318
+ fig, ax = plt.subplots(figsize=(10, 6))
319
+ bars = ax.bar(utilization_range_df['Utilization_Range'], utilization_range_df['Number_of_Drivers'], color='skyblue')
320
+
321
+ # Annotate the bar chart
322
+ for bar in bars:
323
+ height = bar.get_height()
324
+ ax.text(bar.get_x() + bar.get_width()/2., height + 0.1,
325
+ f'{height}', ha='center', va='bottom')
326
+
327
+ ax.set_title('Distribution of Drivers by Utilization Range')
328
+ ax.set_xlabel('Utilization Range (%)')
329
+ ax.set_ylabel('Number of Drivers')
330
+
331
+ plt.tight_layout()
332
+ st.pyplot(fig)
333
+
334
+ # Show data table and download button
335
+ st.dataframe(utilization_range_df)
336
+ download_dataframe(utilization_range_df, "utilization_distribution.xlsx", "Download Utilization Distribution")
337
+
338
+ with col2:
339
+ st.subheader("Depot Utilization Heatmap")
340
+
341
+ # Group by 'Depot' and 'Utilization_Range' and count drivers
342
+ heatmap_data = summary_df.groupby(['Depot', 'Utilization_Range']).size().unstack(fill_value=0)
343
+
344
+ fig, ax = plt.subplots(figsize=(10, 6))
345
+ sns.heatmap(heatmap_data, annot=True, fmt='d', cmap='YlGnBu', ax=ax)
346
+
347
+ ax.set_title('Heatmap of Driver Utilization by Depot')
348
+ ax.set_xlabel('Utilization Range (%)')
349
+ ax.set_ylabel('Depot')
350
+
351
+ plt.tight_layout()
352
+ st.pyplot(fig)
353
+
354
+ # Show data table and download button
355
+ st.dataframe(heatmap_data)
356
+ download_dataframe_with_index(heatmap_data, "depot_utilization_heatmap.xlsx", "Download Depot Heatmap Data", "Depot")
357
+
358
+ # Tab 5: Red Flags
359
+ with tabs[4]:
360
+ st.header("Red Flags Analysis")
361
+
362
+ col1, col2 = st.columns(2)
363
+
364
+ with col1:
365
+ st.subheader("High Spare to Duty Ratio")
366
+ st.markdown("Filters applied:")
367
+ st.markdown("- Utilization < 50%")
368
+ st.markdown("- Spare/Duty ratio > 50%")
369
+ st.markdown("- Spare + Duty > Not Available")
370
+
371
+ st.dataframe(high_spare_df)
372
+
373
+ # Add download button
374
+ download_dataframe(high_spare_df, "planned_high_spare.xlsx", "Download High Spare Report")
375
+
376
+ with col2:
377
+ st.subheader("High Utilization Drivers")
378
+ st.markdown("Filters applied:")
379
+ st.markdown("- Spare/Duty ratio < 5%")
380
+ st.markdown("- Spare + Duty > Not Available")
381
+
382
+ st.dataframe(high_util_df)
383
+
384
+ # Add download button
385
+ download_dataframe(high_util_df, "planned_high_util.xlsx", "Download High Utilization Report")
386
+
387
+ else:
388
+ # Show message when files are not uploaded
389
+ with tabs[0]:
390
+ st.info("Please upload all required files from the sidebar to start the analysis.")
391
+
392
+ with tabs[1]:
393
+ st.info("Please upload all required files from the sidebar to start the analysis.")
394
+
395
+ with tabs[2]:
396
+ st.info("Please upload all required files from the sidebar to start the analysis.")
397
+
398
+ with tabs[3]:
399
+ st.info("Please upload all required files from the sidebar to start the analysis.")
400
+
401
+ with tabs[4]:
402
+ st.info("Please upload all required files from the sidebar to start the analysis.")
403
 
404
+ # Add a footer
405
+ st.markdown("---")
406
+ st.markdown("© 2025 Driver Analysis Tool | Created by MT Planning Team" )