Cosmographer commited on
Commit
a01c7b2
·
verified ·
1 Parent(s): 5d473a3

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +834 -432
app.py CHANGED
@@ -1,447 +1,849 @@
1
- # If needed in Colab uncomment:
2
- # !pip install --quiet gradio openpyxl pandas
3
-
 
 
 
 
 
4
  import pandas as pd
 
5
  import gradio as gr
6
- import warnings
7
- warnings.filterwarnings('ignore')
8
-
9
- # ---------------- Load data ----------------
10
- data_path = "Effort Estimation Sample Data.xlsx"
11
- sheet1 = pd.read_excel(data_path, sheet_name="Sheet1")
12
- sheet2 = pd.read_excel(data_path, sheet_name="Sheet2")
13
-
14
- # ---------------- Constants ----------------
15
- phase_display_names = {
16
- "Design effort (days)": "Design",
17
- "Build effort (days)": "Build",
18
- "Testing effort (days)": "Testing",
19
- "Post Go Live (days)": "Post Go Live"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
  }
21
 
22
- EY_COLORS = {
23
- "black": "#333333",
24
- "white": "#FFFFFF",
25
- "turbo": "#FFE600",
26
- "sonic_silver": "#797878",
27
- "sizzling_sunrise": "#FFDB00"
 
 
28
  }
29
 
30
- # Prepare initial lists
31
- competencies = sorted(list(sheet1["Competency"].dropna().unique()))
32
-
33
- # ---------------- Helper functions (unchanged core logic) ----------------
34
- def get_modules(competency):
35
- if not competency:
36
- return []
37
- return list(sheet1[sheet1["Competency"] == competency]["Module"].dropna().unique())
38
-
39
- def get_submodules(competency, modules):
40
- if not competency or not modules:
41
- return []
42
- all_submodules = []
43
- for module in modules:
44
- submodules = list(sheet1[
45
- (sheet1["Competency"] == competency) &
46
- (sheet1["Module"] == module)
47
- ]["Sub - Module"].dropna().unique())
48
- all_submodules.extend(submodules)
49
- return all_submodules
50
-
51
- def get_complexities_for_submodule(competency, module, submodule):
52
- if not competency or not module or not submodule:
53
- return []
54
- return list(sheet1[
55
- (sheet1["Competency"] == competency) & (sheet1["Module"] == module) & (sheet1["Sub - Module"] == submodule)
56
- ]["Complexity Level"].dropna().unique())
57
-
58
- def get_phases_for_submodule(competency, module, submodule):
59
- if not competency or not module or not submodule:
60
- return []
61
- phases = []
62
- filtered = sheet1[
63
- (sheet1["Competency"] == competency) & (sheet1["Module"] == module) & (sheet1["Sub - Module"] == submodule)
64
- ]
65
- if not filtered.empty:
66
- for phase_col, disp in phase_display_names.items():
67
- if phase_col in filtered.columns and filtered[phase_col].sum() > 0:
68
- phases.append(disp)
69
- return phases
70
-
71
- def create_submodule_details_html(competency, modules, submodules):
72
- """Read-only available complexities & phases per submodule (visual aid)."""
73
- if not competency or not modules or not submodules:
74
- return ""
75
- html = f"""
76
- <div style='background: {EY_COLORS["white"]}; padding: 12px; border-radius: 8px; margin: 6px 0; border: 1px solid {EY_COLORS["sonic_silver"]};'>
77
- <h4 style='color: {EY_COLORS["black"]}; margin-top: 0;'>📋 Sub-Module Details (Available)</h4>
78
- """
79
- for module in modules:
80
- for submodule in submodules:
81
- complexities = get_complexities_for_submodule(competency, module, submodule)
82
- phases = get_phases_for_submodule(competency, module, submodule)
83
- if complexities or phases:
84
- html += f"""
85
- <div style='padding:10px;margin:6px 0;border-radius:6px;border-left:4px solid {EY_COLORS['turbo']};background:{EY_COLORS['white']};'>
86
- <strong style='color:{EY_COLORS['black']};'>{module} {submodule}</strong><br>
87
- <span style='color:{EY_COLORS['sonic_silver']}; font-size:0.95em;'>
88
- 🎯 Complexities: {', '.join(complexities) if complexities else 'None'}<br>
89
- 📋 Phases: {', '.join(phases) if phases else 'None'}
90
- </span>
91
- </div>
92
- """
93
- html += "</div>"
94
- return html
95
-
96
- # ---------------- Calculation using per-submodule selections (keeps original formula & HTML) ----------------
97
- def calculate_effort_per_submodule(competency, selected_modules, selected_submodules, submodule_counts, per_submodule_complexities, per_submodule_phases, buffer_percent):
98
- """
99
- per_submodule_complexities and per_submodule_phases are dicts keyed by submodule name -> list
100
- """
101
- if not competency:
102
- return "❌ Please select a Competency."
103
- if not selected_modules:
104
- return "❌ Please select at least one Module."
105
- if not selected_submodules:
106
- return " Please select at least one Sub-Module."
107
-
108
- # Validate at least one complexity & phase chosen across selected submodules
109
- overall_complexities = set()
110
- overall_phases = set()
111
- for sm in selected_submodules:
112
- cs = per_submodule_complexities.get(sm, [])
113
- ps = per_submodule_phases.get(sm, [])
114
- overall_complexities.update(cs)
115
- overall_phases.update(ps)
116
- if not overall_complexities:
117
- return "❌ Please select at least one Complexity Level for the chosen sub-modules."
118
- if not overall_phases:
119
- return " Please select at least one Phase for the chosen sub-modules."
120
-
121
- total_effort = 0.0
122
- breakdown_rows = []
123
-
124
- for module in selected_modules:
125
- for submodule in selected_submodules:
126
- count = int(submodule_counts.get(submodule, 1))
127
- complexities = per_submodule_complexities.get(submodule, [])
128
- phases = per_submodule_phases.get(submodule, [])
129
- for complexity in complexities:
130
- filtered_data = sheet1[
131
- (sheet1["Competency"] == competency) &
132
- (sheet1["Module"] == module) &
133
- (sheet1["Sub - Module"] == submodule) &
134
- (sheet1["Complexity Level"] == complexity)
135
- ]
136
- if filtered_data.empty:
137
- continue
138
-
139
- weight_rows = sheet2[sheet2["Complexity"] == complexity]
140
- weight = float(weight_rows["Weightage"].values[0]) if not weight_rows.empty else 1.0
141
-
142
- combination_effort = 0.0
143
- phase_details = []
144
- for phase in phases:
145
- phase_col = next((col for col, disp in phase_display_names.items() if disp == phase), None)
146
- if phase_col and phase_col in filtered_data.columns:
147
- phase_effort = float(filtered_data[phase_col].sum()) * weight
148
- phase_total = phase_effort * count
149
- combination_effort += phase_total
150
- phase_details.append(f"{phase}: {phase_effort:.1f}x{count} = {phase_total:.1f} days")
151
-
152
- if combination_effort > 0:
153
- total_effort += combination_effort
154
- breakdown_rows.append({
155
- 'module': module,
156
- 'submodule': submodule,
157
- 'complexity': complexity,
158
- 'count': count,
159
- 'phases': ', '.join(phases),
160
- 'effort': round(combination_effort, 2),
161
- 'phase_details': ' | '.join(phase_details)
162
- })
163
-
164
- if total_effort == 0:
165
- return "❌ No effort data found for the selected combinations."
166
-
167
- final_effort = total_effort * (1 + buffer_percent / 100)
168
-
169
- # Result HTML exactly following your original template
170
- result = f"""
171
- <div style='font-family: Arial, sans-serif; color: {EY_COLORS["black"]};'>
172
- <h1 style='color: {EY_COLORS["black"]}; border-bottom: 2px solid {EY_COLORS["sizzling_sunrise"]}; padding-bottom: 10px;'>
173
- 📊 EY Effort Estimation Results
174
- </h1>
175
- <div style='background: {EY_COLORS["turbo"]}20; padding: 15px; border-radius: 5px; margin: 10px 0;'>
176
- <strong>Competency:</strong> {competency}<br>
177
- <strong>Selected Modules:</strong> {', '.join(selected_modules)}<br>
178
- <strong>Buffer Percentage:</strong> {buffer_percent}%
179
- </div>
180
- <h2 style='color: {EY_COLORS["black"]};'>Effort Breakdown</h2>
181
- <table style='width: 100%; border-collapse: collapse; border: 2px solid {EY_COLORS["sonic_silver"]};'>
182
- <tr style='background: {EY_COLORS["sizzling_sunrise"]};'>
183
- <th style='padding: 12px; border: 1px solid {EY_COLORS["sonic_silver"]}; text-align: left;'>Module</th>
184
- <th style='padding: 12px; border: 1px solid {EY_COLORS["sonic_silver"]}; text-align: left;'>Sub Module</th>
185
- <th style='padding: 12px; border: 1px solid {EY_COLORS["sonic_silver"]}; text-align: left;'>Complexity</th>
186
- <th style='padding: 12px; border: 1px solid {EY_COLORS["sonic_silver"]}; text-align: left;'>Count</th>
187
- <th style='padding: 12px; border: 1px solid {EY_COLORS["sonic_silver"]}; text-align: left;'>Phases</th>
188
- <th style='padding: 12px; border: 1px solid {EY_COLORS["sonic_silver"]}; text-align: right;'>Effort (days)</th>
189
- </tr>
190
- """
191
- for i, row in enumerate(breakdown_rows):
192
- bg_color = f"{EY_COLORS['white']}" if i % 2 == 0 else f"{EY_COLORS['turbo']}20"
193
- result += f"""
194
- <tr style='background: {bg_color};'>
195
- <td style='padding: 10px; border: 1px solid {EY_COLORS["sonic_silver"]};'>{row['module']}</td>
196
- <td style='padding: 10px; border: 1px solid {EY_COLORS["sonic_silver"]};'>{row['submodule']}</td>
197
- <td style='padding: 10px; border: 1px solid {EY_COLORS["sonic_silver"]};'>{row['complexity']}</td>
198
- <td style='padding: 10px; border: 1px solid {EY_COLORS["sonic_silver"]};'>{row['count']}</td>
199
- <td style='padding: 10px; border: 1px solid {EY_COLORS["sonic_silver"]};'>{row['phases']}</td>
200
- <td style='padding: 10px; border: 1px solid {EY_COLORS["sonic_silver"]}; text-align: right;'>{row['effort']}</td>
201
- </tr>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
202
  """
203
- result += f"""
204
- </table>
205
- <div style='margin-top: 20px; padding: 15px; background: {EY_COLORS["white"]}; border: 2px solid {EY_COLORS["sonic_silver"]}; border-radius: 5px;'>
206
- <table style='width: 100%;'>
207
- <tr>
208
- <td style='padding: 10px;'><strong>Total Efforts (before buffer):</strong></td>
209
- <td style='padding: 10px; text-align: right;'>{total_effort:.2f} days</td>
210
- </tr>
211
- <tr>
212
- <td style='padding: 10px;'><strong>Buffer Applied ({buffer_percent}%):</strong></td>
213
- <td style='padding: 10px; text-align: right;'>+ {total_effort * buffer_percent/100:.2f} days</td>
214
- </tr>
215
- <tr style='background: {EY_COLORS["sizzling_sunrise"]};'>
216
- <td style='padding: 15px;'><strong>Total Efforts (with buffer {buffer_percent}%):</strong></td>
217
- <td style='padding: 15px; text-align: right; font-size: 1.2em;'><strong>{final_effort:.2f} days</strong></td>
218
- </tr>
219
- </table>
220
  </div>
221
- <div style='margin-top: 20px; padding: 15px; background: {EY_COLORS["turbo"]}20; border-radius: 5px;'>
222
- <h3>Detailed Phase Breakdown:</h3>
223
- """
224
- for row in breakdown_rows:
225
- result += f"""
226
- <p><strong>{row['module']} - {row['submodule']} ({row['complexity']}):</strong> {row['phase_details']}</p>
227
- """
228
- result += """
229
  </div>
 
 
 
230
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
231
  """
232
- return result
233
-
234
- # ---------------- UI: pre-create rows for submodules (scalable) ----------------
235
- MAX_SUBMODULES = 30 # increase if needed
236
-
237
- with gr.Blocks(
238
- theme=gr.themes.Default(primary_hue="yellow", neutral_hue="gray"),
239
- css=f"""
240
- .gradio-container {{
241
- background: linear-gradient(135deg, {EY_COLORS['white']} 0%, {EY_COLORS['turbo']}10 100%);
242
- font-family: Arial, sans-serif;
243
- }}
244
- .ey-header {{
245
- text-align: center; padding: 18px; color: {EY_COLORS['black']};
246
- }}
247
- .ey-header img {{ height: 54px; margin-right: 12px; vertical-align: middle; }}
248
- .ey-button {{ background: {EY_COLORS['turbo']} !important; color: {EY_COLORS['black']} !important; border: 1px solid {EY_COLORS['sonic_silver']} !important; }}
249
- .panel {{ padding: 16px; background: {EY_COLORS['white']}; border-radius: 10px; margin: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.06); }}
250
- .sub-row {{ padding: 6px 0; }}
251
- """
252
- ) as demo:
253
-
254
- # Header
255
- gr.HTML(f"""
256
- <div class="ey-header">
257
- <img src="EY Logo.png" alt="EY Logo"
258
- onerror="this.src='https://www.ey.com/content/dam/ey-unified-site/ey-com/en-in/generic/images/ey-logo-black.png'">
259
- <div style="display:inline-block; vertical-align:middle;">
260
- <div style="font-size:26px; font-weight:700; color:{EY_COLORS['black']};">Effort Estimation Tool</div>
261
- <div style="color:{EY_COLORS['sonic_silver']}; margin-top:3px;">Enterprise Grade Estimation Platform</div>
262
- </div>
263
- </div>
264
- """)
265
 
266
- # ----------------- Page 1: Inputs (left / right) -----------------
267
- with gr.Column(visible=True) as page_inputs:
 
 
 
 
 
 
268
  with gr.Row():
269
- # Left: selectors & sub-modules
270
- with gr.Column(scale=1, elem_classes="panel"):
271
- # Competency (no default)
272
- competency = gr.Dropdown(choices=competencies, value=None, label="🏢 Select Competency", interactive=True)
273
-
274
- # Modules will populate after competency chosen
275
- modules = gr.CheckboxGroup(choices=[], value=[], label="📦 Select Modules", interactive=True)
276
-
277
- # Submodule rows area (pre-created hidden rows)
278
- gr.Markdown("### 🔧 Sub-module level configuration")
279
- gr.Markdown("<small>🔔 Select only one Complexity level per sub-module. Phases can be multi-selected.</small>")
280
-
281
- per_sm_row = []
282
- per_sm_checkbox = []
283
- per_sm_name = []
284
- per_sm_count = []
285
- per_sm_complexities = []
286
- per_sm_phases = []
287
-
288
- for i in range(MAX_SUBMODULES):
289
- with gr.Row(visible=False) as row:
290
- chk = gr.Checkbox(label="", value=False) # select to include
291
- name = gr.Textbox(value="", interactive=False, label="", visible=True) # submodule name display
292
- cnt = gr.Number(value=1, minimum=1, label="Count", interactive=True) # inline count
293
- # per-submodule complexity (single choice) & phases controls
294
- comps_radio = gr.Radio(choices=[], value=None, label="Complexity (select one)", interactive=True)
295
- phases_cg = gr.CheckboxGroup(choices=[], value=[], label="Phases", interactive=True)
296
- per_sm_row.append(row)
297
- per_sm_checkbox.append(chk)
298
- per_sm_name.append(name)
299
- per_sm_count.append(cnt)
300
- per_sm_complexities.append(comps_radio)
301
- per_sm_phases.append(phases_cg)
302
-
303
- # Submodule details HTML (available options summary)
304
- submodule_details = gr.HTML("")
305
-
306
- # Right: Buffer, Calculate & (no results here)
307
- with gr.Column(scale=1, elem_classes="panel"):
308
- buffer_percent = gr.Slider(0, 50, value=16, step=1, label="📊 Buffer Percentage (%)")
309
- calculate_btn = gr.Button("🚀 Calculate Total Effort", elem_classes="ey-button")
310
-
311
- # ----------------- Page 2: Results (hidden initially) -----------------
312
- with gr.Column(visible=False, elem_classes="panel") as page_results:
313
- # Results area with vertical scroll style
314
- results = gr.HTML("", label="Estimation Results")
315
- back_btn = gr.Button("⬅️ Back to Inputs")
316
-
317
- # ------------------- Event handlers -------------------
318
-
319
- # When competency changes -> populate modules (no default competency)
320
- def on_competency_change(comp):
321
- return gr.update(choices=get_modules(comp), value=[])
322
-
323
- competency.change(fn=on_competency_change, inputs=competency, outputs=modules)
324
-
325
- # When modules change -> populate submodule rows, update per-row complexity/phase choices & details html
326
- def on_modules_change(comp, mods):
327
- submods = get_submodules(comp, mods) if mods else []
328
- updates = []
329
-
330
- for i in range(MAX_SUBMODULES):
331
- if i < len(submods):
332
- sm_name = submods[i]
333
- # show row
334
- updates.append(gr.update(visible=True)) # row
335
- updates.append(gr.update(label=sm_name, value=False)) # checkbox label & unchecked
336
- updates.append(gr.update(value=sm_name)) # name textbox value
337
- updates.append(gr.update(visible=True, value=1)) # count visible default 1
338
-
339
- # per-submodule complexities & phases
340
- # find module for this submodule (choose from selected modules that contains this submodule)
341
- module_for_sm = None
342
- for m in mods:
343
- if sm_name in list(sheet1[(sheet1["Competency"] == comp) & (sheet1["Module"] == m)]["Sub - Module"].dropna().unique()):
344
- module_for_sm = m
345
- break
346
- comp_choices = get_complexities_for_submodule(comp, module_for_sm, sm_name) if module_for_sm else []
347
- phase_choices = get_phases_for_submodule(comp, module_for_sm, sm_name) if module_for_sm else []
348
-
349
- # Complexity is now Radio -> don't pre-select (value=None)
350
- updates.append(gr.update(choices=comp_choices, value=None))
351
- updates.append(gr.update(choices=phase_choices, value=phase_choices))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
352
  else:
353
- # hide row & reset
354
- updates.append(gr.update(visible=False))
355
- updates.append(gr.update(label="", value=False))
356
- updates.append(gr.update(value=""))
357
- updates.append(gr.update(visible=False, value=1))
358
- updates.append(gr.update(choices=[], value=None))
359
- updates.append(gr.update(choices=[], value=[]))
360
-
361
- # prepare details html
362
- details_html = create_submodule_details_html(comp, mods, submods) if (comp and mods and submods) else ""
363
-
364
- # Return updates plus details_html
365
- return updates + [details_html]
366
-
367
- # build outputs mapping (must match the returned updates order)
368
- row_outputs = []
369
- for i in range(MAX_SUBMODULES):
370
- # row, checkbox, name textbox, count, comps_radio, phases_cg
371
- row_outputs += [per_sm_row[i], per_sm_checkbox[i], per_sm_name[i], per_sm_count[i], per_sm_complexities[i], per_sm_phases[i]]
372
-
373
- modules.change(
374
- fn=on_modules_change,
375
- inputs=[competency, modules],
376
- outputs=row_outputs + [submodule_details]
377
- )
378
-
379
- # ------------------ Calculation wrapper ------------------
380
- def collect_and_compute(comp, mods, *args_and_buffer):
381
- """
382
- args_and_buffer contains: for each row 5 items (chk,name,count,comps,phases) then final argument is buffer_percent
383
- We'll return (html_result, hide_inputs_update, show_results_update)
384
- """
385
- total_expected = MAX_SUBMODULES * 5 + 1
386
- if len(args_and_buffer) != total_expected:
387
- return ("❌ Internal: unexpected inputs passed to calculator.", gr.update(visible=True), gr.update(visible=False))
388
-
389
- # split
390
- row_args = args_and_buffer[:-1]
391
- buffer_val = args_and_buffer[-1]
392
-
393
- # parse row args
394
- selected_submodules = []
395
- submodule_counts = {}
396
- per_sub_comps = {}
397
- per_sub_phases = {}
398
-
399
- for i in range(MAX_SUBMODULES):
400
- base = i * 5
401
- chk = row_args[base + 0]
402
- name = row_args[base + 1]
403
- cnt = row_args[base + 2]
404
- comps_sel_raw = row_args[base + 3] # Radio -> single value or None
405
- phases_sel = row_args[base + 4] or []
406
-
407
- # convert complexity single value into a list to keep original logic
408
- if comps_sel_raw is None or comps_sel_raw == "":
409
- comps_sel = []
410
  else:
411
- comps_sel = [comps_sel_raw]
412
-
413
- if chk:
414
- if not name:
415
- continue
416
- if mods and not any(name in list(sheet1[(sheet1["Competency"] == comp) & (sheet1["Module"] == m)]["Sub - Module"].dropna().unique()) for m in mods):
417
- continue
418
- selected_submodules.append(name)
419
- try:
420
- submodule_counts[name] = int(cnt) if (cnt is not None and str(cnt).strip() != "") else 1
421
- except:
422
- submodule_counts[name] = 1
423
- per_sub_comps[name] = comps_sel
424
- per_sub_phases[name] = phases_sel
425
-
426
- # call calculation
427
- html = calculate_effort_per_submodule(comp, mods, selected_submodules, submodule_counts, per_sub_comps, per_sub_phases, buffer_val)
428
- # hide inputs, show results
429
- return (html, gr.update(visible=False), gr.update(visible=True))
430
-
431
- # Build inputs list for the calculate button
432
- calculation_inputs = [competency, modules]
433
- for i in range(MAX_SUBMODULES):
434
- calculation_inputs += [per_sm_checkbox[i], per_sm_name[i], per_sm_count[i], per_sm_complexities[i], per_sm_phases[i]]
435
- calculation_inputs += [buffer_percent]
436
-
437
- # Clicking Calculate: compute, hide inputs page, show results page, and populate results HTML
438
- calculate_btn.click(fn=collect_and_compute, inputs=calculation_inputs, outputs=[results, page_inputs, page_results])
439
-
440
- # Back button returns to inputs, clears results
441
- def go_back():
442
- return (gr.update(visible=True), gr.update(visible=False), "")
443
-
444
- back_btn.click(fn=go_back, inputs=[], outputs=[page_inputs, page_results, results])
445
-
446
- # Launch
447
- demo.launch(share=True, debug=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # app.py
2
+ import os
3
+ import io
4
+ import tempfile
5
+ import json
6
+ import math
7
+ import traceback
8
+ from typing import Optional, List, Tuple, Dict, Any
9
  import pandas as pd
10
+ import numpy as np
11
  import gradio as gr
12
+ import plotly.express as px
13
+ import plotly.graph_objects as go
14
+ import matplotlib.pyplot as plt
15
+ from sklearn.model_selection import train_test_split, GridSearchCV
16
+ from sklearn.preprocessing import StandardScaler, MinMaxScaler, OneHotEncoder
17
+ from sklearn.compose import ColumnTransformer
18
+ from sklearn.pipeline import Pipeline
19
+ from sklearn.impute import SimpleImputer
20
+ from sklearn.feature_selection import RFECV
21
+ from sklearn.linear_model import LinearRegression, Ridge, Lasso, LogisticRegression
22
+ from sklearn.tree import DecisionTreeRegressor, DecisionTreeClassifier
23
+ from sklearn.ensemble import RandomForestRegressor, RandomForestClassifier, GradientBoostingRegressor, GradientBoostingClassifier
24
+ from sklearn.svm import SVR, SVC
25
+ from sklearn.neighbors import KNeighborsClassifier
26
+ from sklearn.naive_bayes import GaussianNB
27
+ from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score, accuracy_score, classification_report, confusion_matrix, roc_auc_score, roc_curve
28
+ from sklearn.exceptions import NotFittedError
29
+ import scipy.stats as stats
30
+ import matplotlib
31
+ matplotlib.use("Agg") # for headless plotting
32
+
33
+ # --- lightweight password hashing (stdlib) ---
34
+ import os as _os
35
+ import hashlib, binascii, hmac as _hmac
36
+
37
+ def generate_password_hash(password: str) -> str:
38
+ salt = _os.urandom(16)
39
+ dk = hashlib.pbkdf2_hmac('sha256', password.encode('utf-8'), salt, 100000)
40
+ return binascii.hexlify(salt).decode() + ':' + binascii.hexlify(dk).decode()
41
+
42
+ def check_password_hash(stored_hash: str, password: str) -> bool:
43
+ try:
44
+ salt_hex, dk_hex = stored_hash.split(':')
45
+ except ValueError:
46
+ return False
47
+ salt = binascii.unhexlify(salt_hex)
48
+ dk = binascii.unhexlify(dk_hex)
49
+ new_dk = hashlib.pbkdf2_hmac('sha256', password.encode('utf-8'), salt, 100000)
50
+ return _hmac.compare_digest(new_dk, dk)
51
+ # ------------------------------------------------
52
+
53
+ # -----------------------------
54
+ # Simple in-repo auth store (file-backed)
55
+ # -----------------------------
56
+ AUTH_STORE = "auth_store.json"
57
+ def _ensure_auth():
58
+ if not os.path.exists(AUTH_STORE):
59
+ admin_pass = generate_password_hash("password")
60
+ with open(AUTH_STORE, "w") as f:
61
+ json.dump({"admin": admin_pass}, f)
62
+
63
+ def authenticate(username: str, password: str) -> bool:
64
+ _ensure_auth()
65
+ with open(AUTH_STORE, "r") as f:
66
+ data = json.load(f)
67
+ stored = data.get(username)
68
+ if not stored:
69
+ return False
70
+ return check_password_hash(stored, password)
71
+
72
+ # seed auth
73
+ _ensure_auth()
74
+
75
+ # -----------------------------
76
+ # App constants & helpers
77
+ # -----------------------------
78
+ HIGH_CARD_THRESHOLD_COUNT = 50 # or unique > threshold proportion
79
+ HIGH_CARD_THRESHOLD_RATIO = 0.10 # if unique/rows > this, consider high card
80
+
81
+ LOGO_PATH = "DataSynth.png" # ensure this file exists in repo root
82
+
83
+ # -----------------------------
84
+ # Small utility functions
85
+ # -----------------------------
86
+ def read_file_to_df(uploaded) -> pd.DataFrame:
87
+ """uploaded is a gradio file dict-like or path"""
88
+ if uploaded is None:
89
+ return pd.DataFrame()
90
+ # Uploaded from HF Spaces will be a dict-like with 'name' or 'tmp_path'
91
+ try:
92
+ # Gradio on Spaces often gives a tempfile path in `name` or `file`
93
+ if hasattr(uploaded, "name") and os.path.exists(uploaded.name):
94
+ path = uploaded.name
95
+ elif isinstance(uploaded, dict) and "name" in uploaded:
96
+ path = uploaded["name"]
97
+ else:
98
+ # attempt to read bytes
99
+ content = uploaded.read()
100
+ try:
101
+ return pd.read_csv(io.BytesIO(content))
102
+ except Exception:
103
+ return pd.read_excel(io.BytesIO(content))
104
+ # now path-based read
105
+ if path.lower().endswith(".csv"):
106
+ return pd.read_csv(path)
107
+ elif path.lower().endswith((".xls", ".xlsx")):
108
+ return pd.read_excel(path)
109
+ elif path.lower().endswith(".json"):
110
+ return pd.read_json(path)
111
+ else:
112
+ # try csv then excel then json
113
+ try:
114
+ return pd.read_csv(path)
115
+ except Exception:
116
+ try:
117
+ return pd.read_excel(path)
118
+ except Exception:
119
+ return pd.read_json(path)
120
+ except Exception as e:
121
+ print("read_file_to_df error:", e)
122
+ raise
123
+
124
+ def basic_profile(df: pd.DataFrame) -> Dict[str, Any]:
125
+ if df is None or df.empty:
126
+ return {}
127
+ profile = {}
128
+ profile["rows"], profile["columns"] = df.shape
129
+ dtypes = df.dtypes.apply(lambda x: x.name).to_dict()
130
+ profile["dtypes"] = dtypes
131
+ nulls = df.isnull().sum().to_dict()
132
+ profile["nulls"] = nulls
133
+ profile["null_pct"] = {k: (v / len(df)) for k, v in nulls.items()}
134
+ unique_counts = df.nunique(dropna=False).to_dict()
135
+ profile["unique"] = unique_counts
136
+ profile["high_cardinality"] = [col for col, cnt in unique_counts.items()
137
+ if cnt > HIGH_CARD_THRESHOLD_COUNT or (cnt / len(df) > HIGH_CARD_THRESHOLD_RATIO)]
138
+ profile["describe"] = df.describe(include='all').to_dict()
139
+ profile["head"] = df.head(5).to_dict(orient="records")
140
+ return profile
141
+
142
+ def profile_to_markdown(profile: Dict[str, Any]) -> str:
143
+ if not profile:
144
+ return "No data loaded."
145
+ md = []
146
+ md.append(f"**Rows:** {profile['rows']} \n**Columns:** {profile['columns']}\n")
147
+ md.append("### Column summary (dtypes / null% / unique)\n")
148
+ md.append("| Column | Dtype | Nulls | Null % | Unique |\n|---:|---|---:|---:|---:|\n")
149
+ for col in profile["dtypes"].keys():
150
+ dtype = profile["dtypes"][col]
151
+ nulls = profile["nulls"].get(col, 0)
152
+ pct = f"{profile['null_pct'].get(col,0):.2%}"
153
+ uniq = profile["unique"].get(col, 0)
154
+ md.append(f"| {col} | {dtype} | {nulls} | {pct} | {uniq} |\n")
155
+ if profile["high_cardinality"]:
156
+ md.append("\n**High cardinality columns (auto-detected):** " + ", ".join(profile["high_cardinality"]) + "\n")
157
+ md.append("\n### Sample rows\n")
158
+ md.append(pd.DataFrame(profile["head"]).to_markdown(index=False))
159
+ return "\n".join(md)
160
+
161
+ # -----------------------------
162
+ # Data cleaning & feature engineering helpers
163
+ # -----------------------------
164
+ def drop_high_cardinality(df: pd.DataFrame, threshold_count=HIGH_CARD_THRESHOLD_COUNT, threshold_ratio=HIGH_CARD_THRESHOLD_RATIO):
165
+ n = len(df)
166
+ cols_to_drop = []
167
+ for c in df.columns:
168
+ if df[c].nunique(dropna=False) > threshold_count or (df[c].nunique(dropna=False)/max(1,n) > threshold_ratio):
169
+ # drop object/categorical high-card only (keep numeric)
170
+ if df[c].dtype == "object" or str(df[c].dtype).startswith("category"):
171
+ cols_to_drop.append(c)
172
+ return df.drop(columns=cols_to_drop, errors='ignore'), cols_to_drop
173
+
174
+ def impute_df(df: pd.DataFrame, numeric_strategy="mean", categorical_strategy="most_frequent", fill_value: Optional[str]=None):
175
+ df = df.copy()
176
+ num_cols = df.select_dtypes(include=[np.number]).columns.tolist()
177
+ cat_cols = df.select_dtypes(include=["object", "category"]).columns.tolist()
178
+ if num_cols:
179
+ imp = SimpleImputer(strategy=numeric_strategy)
180
+ df[num_cols] = imp.fit_transform(df[num_cols])
181
+ if cat_cols:
182
+ if categorical_strategy == "constant" and fill_value is not None:
183
+ imp2 = SimpleImputer(strategy="constant", fill_value=fill_value)
184
+ else:
185
+ imp2 = SimpleImputer(strategy=categorical_strategy)
186
+ df[cat_cols] = imp2.fit_transform(df[cat_cols])
187
+ return df
188
+
189
+ def treat_outliers_iqr(df: pd.DataFrame, cols: List[str], method="cap"):
190
+ df = df.copy()
191
+ for c in cols:
192
+ if c not in df.columns:
193
+ continue
194
+ if not np.issubdtype(df[c].dtype, np.number):
195
+ continue
196
+ q1 = df[c].quantile(0.25)
197
+ q3 = df[c].quantile(0.75)
198
+ iqr = q3 - q1
199
+ lower = q1 - 1.5 * iqr
200
+ upper = q3 + 1.5 * iqr
201
+ if method == "remove":
202
+ df = df[(df[c] >= lower) & (df[c] <= upper)]
203
+ elif method == "cap":
204
+ df[c] = np.where(df[c] < lower, lower, df[c])
205
+ df[c] = np.where(df[c] > upper, upper, df[c])
206
+ return df
207
+
208
+ def parse_dates(df: pd.DataFrame, col: str, fmt: Optional[str]=None):
209
+ df = df.copy()
210
+ try:
211
+ if fmt:
212
+ df[col] = pd.to_datetime(df[col], format=fmt, errors="coerce")
213
+ else:
214
+ df[col] = pd.to_datetime(df[col], errors="coerce", infer_datetime_format=True)
215
+ except Exception as e:
216
+ print("parse_dates", e)
217
+ return df
218
+
219
+ def text_clean(df: pd.DataFrame, cols: List[str], lower=True, strip=True):
220
+ df = df.copy()
221
+ for c in cols:
222
+ if c not in df.columns:
223
+ continue
224
+ df[c] = df[c].astype(str)
225
+ if strip:
226
+ df[c] = df[c].str.strip()
227
+ if lower:
228
+ df[c] = df[c].str.lower()
229
+ return df
230
+
231
+ def transform_cols(df: pd.DataFrame, cols: List[str], method="log"):
232
+ df = df.copy()
233
+ for c in cols:
234
+ if c in df.columns and np.issubdtype(df[c].dtype, np.number):
235
+ if method == "log":
236
+ df[c] = df[c].apply(lambda x: np.log(x) if x>0 else x)
237
+ elif method == "sqrt":
238
+ df[c] = df[c].apply(lambda x: np.sqrt(x) if x>=0 else x)
239
+ return df
240
+
241
+ # -----------------------------
242
+ # Visualization NLP (very small parser)
243
+ # -----------------------------
244
+ def nlp_to_chart_instruction(query: str, df: pd.DataFrame):
245
+ """Return (chart_type, cols) based on keywords"""
246
+ q = query.lower()
247
+ numeric = df.select_dtypes(include=[np.number]).columns.tolist()
248
+ categorical = df.select_dtypes(include=["object", "category"]).columns.tolist()
249
+ tokens = q.split()
250
+ # histogram
251
+ if "hist" in q or "histogram" in q or "distribution" in q:
252
+ # pick first numeric mention
253
+ for c in numeric:
254
+ if c.lower() in q:
255
+ return ("hist", [c])
256
+ if numeric:
257
+ return ("hist", [numeric[0]])
258
+ # scatter
259
+ if "scatter" in q or "vs" in q or "versus" in q:
260
+ # pick first two numeric
261
+ for c in numeric:
262
+ if c.lower() in q:
263
+ x = c
264
+ # find second numeric
265
+ for d in numeric:
266
+ if d!=c and d.lower() in q:
267
+ return ("scatter", [x, d])
268
+ # fallback second numeric
269
+ if len(numeric)>1:
270
+ return ("scatter", [numeric[0], numeric[1]])
271
+ if len(numeric)>=2:
272
+ return ("scatter", [numeric[0], numeric[1]])
273
+ # bar chart for categorical counts
274
+ if "bar" in q or "count" in q or "counts" in q or "value counts" in q:
275
+ for c in categorical:
276
+ if c.lower() in q:
277
+ return ("bar", [c])
278
+ if categorical:
279
+ return ("bar", [categorical[0]])
280
+ # boxplot
281
+ if "box" in q or "outlier" in q:
282
+ for c in numeric:
283
+ if c.lower() in q:
284
+ return ("box", [c])
285
+ if numeric:
286
+ return ("box", [numeric[0]])
287
+ # fallback: table head
288
+ return ("table", [])
289
+
290
+ def render_chart_from_instruction(instruction: Tuple[str, List[str]], df: pd.DataFrame):
291
+ typ, cols = instruction
292
+ if typ == "hist":
293
+ c = cols[0]
294
+ fig = px.histogram(df, x=c, title=f"Distribution of {c}")
295
+ return fig
296
+ if typ == "scatter":
297
+ x, y = cols[:2]
298
+ fig = px.scatter(df, x=x, y=y, title=f"{y} vs {x}")
299
+ return fig
300
+ if typ == "bar":
301
+ c = cols[0]
302
+ vc = df[c].value_counts().reset_index()
303
+ vc.columns = [c, "count"]
304
+ fig = px.bar(vc, x=c, y="count", title=f"Counts of {c}")
305
+ return fig
306
+ if typ == "box":
307
+ c = cols[0]
308
+ fig = px.box(df, y=c, title=f"Box plot of {c}")
309
+ return fig
310
+ # table
311
+ return None
312
+
313
+ # -----------------------------
314
+ # Modeling helpers
315
+ # -----------------------------
316
+ REGRESSION_MODELS = {
317
+ "LinearRegression": LinearRegression,
318
+ "Ridge": Ridge,
319
+ "Lasso": Lasso,
320
+ "DecisionTreeRegressor": DecisionTreeRegressor,
321
+ "RandomForestRegressor": RandomForestRegressor,
322
+ "GradientBoostingRegressor": GradientBoostingRegressor,
323
+ "SVR": SVR
324
  }
325
 
326
+ CLASSIFICATION_MODELS = {
327
+ "LogisticRegression": LogisticRegression,
328
+ "DecisionTreeClassifier": DecisionTreeClassifier,
329
+ "RandomForestClassifier": RandomForestClassifier,
330
+ "GradientBoostingClassifier": GradientBoostingClassifier,
331
+ "SVC": SVC,
332
+ "GaussianNB": GaussianNB,
333
+ "KNN": KNeighborsClassifier
334
  }
335
 
336
+ def prepare_features_targets(df: pd.DataFrame, target: str, drop_cols: List[str]=None, drop_high_card=True):
337
+ df = df.copy()
338
+ if drop_cols:
339
+ df = df.drop(columns=drop_cols, errors='ignore')
340
+ if drop_high_card:
341
+ df, dropped = drop_high_cardinality(df)
342
+ if target not in df.columns:
343
+ raise ValueError("Target column not in dataframe")
344
+ X = df.drop(columns=[target])
345
+ y = df[target]
346
+ return X, y
347
+
348
+ def auto_build_preprocessor(X: pd.DataFrame, scaler_choice: str="standard", onehot=True):
349
+ num_cols = X.select_dtypes(include=[np.number]).columns.tolist()
350
+ cat_cols = X.select_dtypes(include=["object", "category"]).columns.tolist()
351
+ transformers = []
352
+ if num_cols:
353
+ if scaler_choice == "standard":
354
+ num_pipeline = Pipeline([("imputer", SimpleImputer(strategy="mean")), ("scaler", StandardScaler())])
355
+ elif scaler_choice == "minmax":
356
+ num_pipeline = Pipeline([("imputer", SimpleImputer(strategy="mean")), ("scaler", MinMaxScaler())])
357
+ else:
358
+ num_pipeline = Pipeline([("imputer", SimpleImputer(strategy="mean"))])
359
+ transformers.append(("num", num_pipeline, num_cols))
360
+ if cat_cols and onehot:
361
+ cat_pipeline = Pipeline([("imputer", SimpleImputer(strategy="most_frequent")), ("onehot", OneHotEncoder(drop='first', sparse=False, handle_unknown='ignore'))])
362
+ transformers.append(("cat", cat_pipeline, cat_cols))
363
+ preprocessor = ColumnTransformer(transformers=transformers, remainder='drop')
364
+ return preprocessor
365
+
366
+ def fit_and_evaluate_model(X, y, model_name: str, task:str, scaler_choice="standard", test_size=0.2, random_state=42, do_rfecv=False, param_grid=None):
367
+ if task=="regression":
368
+ ModelClass = REGRESSION_MODELS.get(model_name)
369
+ else:
370
+ ModelClass = CLASSIFICATION_MODELS.get(model_name)
371
+
372
+ if ModelClass is None:
373
+ raise ValueError(f"Model {model_name} not found for task {task}")
374
+
375
+ X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=test_size, random_state=random_state)
376
+ preprocessor = auto_build_preprocessor(X_train, scaler_choice=scaler_choice, onehot=True)
377
+ model = ModelClass()
378
+ pipe = Pipeline([("pre", preprocessor), ("model", model)])
379
+
380
+ # optional RFECV on a simple estimator if requested (only if classifier/regressor supports coef_ or feature_importances_)
381
+ rfecv_result = None
382
+ if do_rfecv:
383
+ try:
384
+ # use a wrapped estimator for feature selection (random forest)
385
+ base_est = RandomForestRegressor(n_estimators=20) if task=="regression" else RandomForestClassifier(n_estimators=20)
386
+ rfecv = RFECV(estimator=base_est, step=1, cv=3, scoring='r2' if task=="regression" else 'accuracy', n_jobs=1)
387
+ # build simple pipeline: preprocessor -> rfecv will need transformed X
388
+ X_trans = preprocessor.fit_transform(X_train)
389
+ rfecv.fit(X_trans, y_train)
390
+ rfecv_result = rfecv
391
+ # use selected features by indices (skipping because transformer may produce different width)
392
+ except Exception as e:
393
+ print("RFECV failed:", e)
394
+
395
+ # grid search if provided
396
+ gs = None
397
+ if param_grid:
398
+ gs = GridSearchCV(pipe, param_grid, cv=3, n_jobs=1)
399
+ gs.fit(X_train, y_train)
400
+ best_est = gs.best_estimator_
401
+ y_pred = best_est.predict(X_test)
402
+ trained = best_est
403
+ else:
404
+ pipe.fit(X_train, y_train)
405
+ y_pred = pipe.predict(X_test)
406
+ trained = pipe
407
+
408
+ results = {}
409
+ # metrics
410
+ if task=="regression":
411
+ results["mse"] = mean_squared_error(y_test, y_pred)
412
+ results["rmse"] = math.sqrt(results["mse"])
413
+ results["mae"] = mean_absolute_error(y_test, y_pred)
414
+ results["r2"] = r2_score(y_test, y_pred)
415
+ # residuals and QQ
416
+ resid = y_test - y_pred
417
+ results["residuals"] = resid
418
+ else:
419
+ results["accuracy"] = accuracy_score(y_test, y_pred)
420
+ results["report"] = classification_report(y_test, y_pred, output_dict=True)
421
+ # ROC if probability available
422
+ try:
423
+ if hasattr(trained.named_steps['model'], "predict_proba"):
424
+ y_score = trained.predict_proba(X_test)[:,1]
425
+ results["roc_auc"] = roc_auc_score(y_test, y_score)
426
+ fpr, tpr, _ = roc_curve(y_test, y_score)
427
+ results["roc_curve"] = (fpr.tolist(), tpr.tolist())
428
+ except Exception as e:
429
+ print("ROC error", e)
430
+ results["confusion_matrix"] = confusion_matrix(y_test, y_pred).tolist()
431
+
432
+ # feature importance if available (for tree models)
433
+ feat_importance = None
434
+ try:
435
+ model_obj = trained.named_steps['model']
436
+ if hasattr(model_obj, "feature_importances_"):
437
+ # get feature names after preprocessor
438
+ # try to extract feature names from preprocessor
439
+ feature_names = []
440
+ pre = trained.named_steps['pre']
441
+ if hasattr(pre, 'transformers_'):
442
+ for name, trans, cols in pre.transformers_:
443
+ if name == "num":
444
+ feature_names += cols
445
+ elif name == "cat":
446
+ # extract ohe names
447
+ ohe = trans.named_steps['onehot']
448
+ if hasattr(ohe, 'get_feature_names_out'):
449
+ names = list(ohe.get_feature_names_out(cols))
450
+ feature_names += names
451
+ else:
452
+ feature_names += cols
453
+ importances = model_obj.feature_importances_
454
+ feat_importance = list(zip(feature_names, importances))
455
+ feat_importance.sort(key=lambda x: x[1], reverse=True)
456
+ except Exception as e:
457
+ print("feature importance error", e)
458
+
459
+ results["feature_importance"] = feat_importance
460
+ results["trained"] = trained
461
+ results["rfecv"] = rfecv_result
462
+ results["y_test_sample"] = None
463
+ return results
464
+
465
+ # -----------------------------
466
+ # Gradio UI
467
+ # -----------------------------
468
+ css = """
469
+ /* Vanta background container */
470
+ #vanta-bg {
471
+ width: 100%;
472
+ height: 380px;
473
+ position: relative;
474
+ overflow: hidden;
475
+ border-radius: 12px;
476
+ margin-bottom: 8px;
477
+ }
478
+
479
+ /* overlay login card */
480
+ .login-card {
481
+ position: absolute;
482
+ left: 50%;
483
+ top: 50%;
484
+ transform: translate(-50%, -50%);
485
+ width: 420px;
486
+ max-width: calc(100% - 24px);
487
+ background: linear-gradient(180deg, rgba(255,255,255,0.98), rgba(245,245,255,0.95));
488
+ border-radius: 14px;
489
+ box-shadow: 0 12px 36px rgba(0,0,0,0.18);
490
+ padding: 22px;
491
+ z-index: 999;
492
+ border: 1px solid rgba(0,0,0,0.06);
493
+ }
494
+
495
+ /* logo */
496
+ .login-logo {
497
+ display:flex;
498
+ align-items:center;
499
+ gap:12px;
500
+ margin-bottom:8px;
501
+ }
502
+
503
+ .brand-title {
504
+ font-weight:700;
505
+ font-size:18px;
506
+ color:#2b2b6b;
507
+ }
508
+
509
+ /* button animation */
510
+ .btn-animate {
511
+ transition: transform 0.12s ease-in-out, box-shadow 0.12s;
512
+ }
513
+ .btn-animate:active {
514
+ transform: translateY(2px) scale(0.995);
515
+ box-shadow: 0 6px 18px rgba(0,0,0,0.12) inset;
516
+ }
517
+
518
+ /* small description text */
519
+ .app-desc {
520
+ font-size: 13px;
521
+ color: #444;
522
+ margin-top: 8px;
523
+ text-align: center;
524
+ }
525
  """
526
+
527
+ vanta_html = f"""
528
+ <div id="vanta-bg" style="width:100%;height:380px;border-radius:12px;position:relative;">
529
+ <div class="login-card" role="region" aria-label="Login card">
530
+ <div class="login-logo">
531
+ <img src="{LOGO_PATH}" alt="logo" style="height:48px;width:48px;border-radius:8px;"/>
532
+ <div>
533
+ <div class="brand-title">DataSynth — Analytics Hub</div>
534
+ <div style="font-size:12px;color:#666;">Fast, modular data profiling & model building</div>
535
+ </div>
 
 
 
 
 
 
 
536
  </div>
537
+ <div style="margin-top:8px;">
538
+ <div style="font-size:13px;color:#333;margin-bottom:6px;">Sign in to continue</div>
 
 
 
 
 
 
539
  </div>
540
+ <!-- gradio inputs are rendered below visually; this card serves as overlay -->
541
+ <div class="app-desc">Upload your dataset, prepare it, visualize with NL, and build ML models — all in one place.</div>
542
+ </div>
543
  </div>
544
+
545
+ <!-- Vanta & three.js from CDN -->
546
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r121/three.min.js"></script>
547
+ <script src="https://cdn.jsdelivr.net/npm/vanta@0.5.21/dist/vanta.net.min.js"></script>
548
+ <script>
549
+ (function(){
550
+ try {
551
+ if (typeof VANTA !== 'undefined') {
552
+ VANTA.NET({
553
+ el: "#vanta-bg",
554
+ color: 0x2b2b6b,
555
+ backgroundColor: 0xffffff,
556
+ points: 10.00,
557
+ maxDistance: 26.00
558
+ })
559
+ }
560
+ } catch(e) {
561
+ console.warn("Vanta failed to initialize", e)
562
+ }
563
+ })();
564
+ </script>
565
  """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
566
 
567
+ # Gradio app layout
568
+ with gr.Blocks(css=css, title="DataSynth — Analytics Hub") as demo:
569
+ # --- Login area ---
570
+ with gr.Column():
571
+ v_html = gr.HTML(vanta_html)
572
+ with gr.Row():
573
+ username_in = gr.Textbox(label="Username", placeholder="username", interactive=True)
574
+ password_in = gr.Textbox(label="Password", placeholder="password", type="password")
575
  with gr.Row():
576
+ login_btn = gr.Button("Log in", elem_classes="btn-animate")
577
+ login_msg = gr.Label(value="")
578
+ # state holders for data and cleaned data and metadata
579
+ raw_state = gr.State(value=None) # will hold JSON-serializable representation (like csv bytes saved to temp file path)
580
+ df_state = gr.State(value=None) # original dataframe serialized as json (for safety we store as csv bytes path below)
581
+ clean_state = gr.State(value=None) # cleaned dataframe path
582
+ profile_state = gr.State(value=None)
583
+
584
+ # --- Main app (hidden until login) ---
585
+ with gr.Column(visible=False) as main_area:
586
+ gr.Markdown("## Workspace")
587
+ with gr.Tabs():
588
+ with gr.TabItem("Data"):
589
+ gr.Markdown("Upload CSV / Excel / JSON for profiling and cleaning.")
590
+ upload = gr.File(label="Upload CSV / Excel / JSON", file_types=[".csv", ".xlsx", ".xls", ".json"])
591
+ with gr.Row():
592
+ load_btn = gr.Button("Load & Profile")
593
+ download_raw_btn = gr.Button("Download Raw CSV")
594
+ profile_md = gr.Markdown("No dataset loaded.")
595
+ sample_table = gr.Dataframe(interactive=False)
596
+ with gr.TabItem("Prepare"):
597
+ gr.Markdown("Data cleaning and feature engineering options.")
598
+ with gr.Row():
599
+ with gr.Column(scale=1):
600
+ impute_num = gr.Dropdown(choices=["mean","median","most_frequent"], value="mean", label="Numeric Imputation")
601
+ impute_cat = gr.Dropdown(choices=["most_frequent","constant"], value="most_frequent", label="Categorical Imputation")
602
+ const_fill = gr.Textbox(label="Constant fill value (if constant chosen)", value="missing")
603
+ outlier_cols = gr.Textbox(label="Outlier numeric columns (comma separated) — leave blank for auto numeric")
604
+ outlier_method = gr.Dropdown(choices=["cap","remove"], value="cap", label="Outlier treatment")
605
+ date_col = gr.Dropdown(choices=[], label="Date column (auto-detected)", interactive=True)
606
+ date_fmt = gr.Textbox(label="Date format (optional)", placeholder="%Y-%m-%d")
607
+ text_cols = gr.Textbox(label="Text columns to clean (comma separated)")
608
+ transform_cols_txt = gr.Textbox(label="Numeric columns to transform (comma separated)")
609
+ transform_method = gr.Dropdown(choices=["log","sqrt"], value="log", label="Transform method")
610
+ drop_highcard = gr.Checkbox(label="Auto-drop high-cardinality categorical columns", value=True)
611
+ apply_prep = gr.Button("Apply Preparation")
612
+ download_clean = gr.Button("Download Clean CSV")
613
+ with gr.Column(scale=1):
614
+ prep_output = gr.Markdown("Preparation preview will appear here.")
615
+ preview_clean = gr.Dataframe(interactive=False)
616
+ with gr.TabItem("Visualize (NL)"):
617
+ gr.Markdown("Type a natural-language request to create a plot (e.g., 'histogram of age', 'scatter income vs age', 'bar of country').")
618
+ nl_input = gr.Textbox(label="Describe chart")
619
+ nl_btn = gr.Button("Create Chart")
620
+ nl_plot = gr.Plot()
621
+ with gr.TabItem("Model"):
622
+ gr.Markdown("Choose task, target variable, and model. Scaling and encoding will be applied automatically.")
623
+ task_select = gr.Radio(choices=["regression","classification"], value="regression", label="Task")
624
+ target_col = gr.Dropdown(choices=[], label="Target variable")
625
+ scaler_choice = gr.Dropdown(choices=["standard","minmax","none"], value="standard", label="Scaler for numeric")
626
+ model_select = gr.Dropdown(choices=[*REGRESSION_MODELS.keys(), *CLASSIFICATION_MODELS.keys()], value="RandomForestRegressor", label="Model")
627
+ rfecv_opt = gr.Checkbox(label="Run RFECV (feature selection)", value=False)
628
+ train_btn = gr.Button("Train & Evaluate")
629
+ model_out_md = gr.Markdown("Model results will show here.")
630
+ model_feature_imp = gr.Dataframe(interactive=False)
631
+ model_plots = gr.Plot()
632
+ with gr.TabItem("Report"):
633
+ gr.Markdown("Generate a short executive report that summarizes profiling, cleaning, and model results.")
634
+ report_btn = gr.Button("Generate Report")
635
+ report_download = gr.File(label="Download Report (.md)")
636
+
637
+ # --- Callbacks ---
638
+ def _do_login(username, password):
639
+ if not username or not password:
640
+ return gr.update(value="Enter username and password"), gr.update(visible=False)
641
+ ok = authenticate(username.strip(), password.strip())
642
+ if ok:
643
+ return gr.update(value=f"Welcome {username}"), gr.update(visible=True)
644
+ else:
645
+ return gr.update(value="Invalid credentials"), gr.update(visible=False)
646
+
647
+ login_btn.click(fn=_do_login, inputs=[username_in, password_in], outputs=[login_msg, main_area])
648
+
649
+ # Load & profile dataset
650
+ def _load_and_profile(uploaded):
651
+ try:
652
+ if uploaded is None:
653
+ return gr.update(value="No file uploaded."), pd.DataFrame(), None, None
654
+ df = read_file_to_df(uploaded)
655
+ prof = basic_profile(df)
656
+ md = profile_to_markdown(prof)
657
+ # prepare choices for date and target selectors
658
+ cols = df.columns.tolist()
659
+ # save df to temp csv for persistence (store path in state)
660
+ tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".csv")
661
+ df.to_csv(tmp.name, index=False)
662
+ return md, df.head(100), tmp.name, prof
663
+ except Exception as e:
664
+ return gr.update(value=f"Error loading file: {e}"), pd.DataFrame(), None, None
665
+
666
+ load_btn.click(fn=_load_and_profile, inputs=[upload], outputs=[profile_md, sample_table, df_state, profile_state])
667
+
668
+ # download raw
669
+ def _download_raw(df_path):
670
+ if not df_path:
671
+ return None
672
+ return df_path
673
+ download_raw_btn.click(fn=_download_raw, inputs=[df_state], outputs=[download_raw_btn])
674
+
675
+ # prepare: auto populate date and target choices when profile updated
676
+ def _populate_prepare(profile):
677
+ if not profile:
678
+ return [], []
679
+ # parse back to usable structure
680
+ # we passed profile as dict earlier, but gr.State may store as dict -> fine
681
+ # detect date-like columns heuristically by dtype or name
682
+ # get columns list
683
+ cols = list(profile.get("dtypes", {}).keys())
684
+ date_candidates = [c for c,d in profile.get("dtypes", {}).items() if "datetime" in d or "date" in c.lower()]
685
+ # fallback: empty
686
+ return gr.Dropdown.update(choices=cols), gr.Dropdown.update(choices=cols)
687
+
688
+ profile_md.change(fn=_populate_prepare, inputs=[profile_state], outputs=[date_col, target_col])
689
+
690
+ # apply preparation
691
+ def _apply_preparation(df_path, impute_num, impute_cat, fill_val, outlier_cols_txt, outlier_method, date_col_sel, date_fmt, text_cols_txt, transform_cols_txt, transform_method, drop_highcard_flag):
692
+ try:
693
+ if not df_path:
694
+ return "Upload dataset first.", pd.DataFrame(), None
695
+ df = pd.read_csv(df_path)
696
+ # drop high-card if requested
697
+ dropped = []
698
+ if drop_highcard_flag:
699
+ df, dropped = drop_high_cardinality(df)
700
+ # parse date
701
+ if date_col_sel:
702
+ df = parse_dates(df, date_col_sel, date_fmt if date_fmt else None)
703
+ # text cleaning
704
+ if text_cols_txt:
705
+ tcols = [c.strip() for c in text_cols_txt.split(",") if c.strip()]
706
+ df = text_clean(df, tcols)
707
+ # impute
708
+ df = impute_df(df, numeric_strategy=impute_num, categorical_strategy=impute_cat, fill_value=fill_val)
709
+ # outliers
710
+ if outlier_cols_txt:
711
+ cols = [c.strip() for c in outlier_cols_txt.split(",") if c.strip()]
712
  else:
713
+ cols = df.select_dtypes(include=[np.number]).columns.tolist()
714
+ df = treat_outliers_iqr(df, cols, method=outlier_method)
715
+ # transform numeric
716
+ if transform_cols_txt:
717
+ tcols = [c.strip() for c in transform_cols_txt.split(",") if c.strip()]
718
+ df = transform_cols(df, tcols, method=transform_method)
719
+ # save cleaned temp file
720
+ tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".csv")
721
+ df.to_csv(tmp.name, index=False)
722
+ prep_summary = f"Prepared data: {len(df)} rows, {len(df.columns)} columns. Dropped high-card columns: {', '.join(dropped) if dropped else 'None'}"
723
+ return prep_summary, df.head(200), tmp.name
724
+ except Exception as e:
725
+ tb = traceback.format_exc()
726
+ print(tb)
727
+ return f"Preparation failed: {e}", pd.DataFrame(), None
728
+
729
+ apply_prep.click(fn=_apply_preparation, inputs=[df_state, impute_num, impute_cat, const_fill, outlier_cols, outlier_method, date_col, date_fmt, text_cols, transform_cols_txt, transform_method, drop_highcard], outputs=[prep_output, preview_clean, clean_state])
730
+
731
+ download_clean.click(fn=lambda p: p if p else None, inputs=[clean_state], outputs=[download_clean])
732
+
733
+ # NLP visualize
734
+ def _nl_visualize(query, clean_path, df_path):
735
+ try:
736
+ df = None
737
+ if clean_path:
738
+ df = pd.read_csv(clean_path)
739
+ elif df_path:
740
+ df = pd.read_csv(df_path)
741
+ if df is None or df.empty:
742
+ return None
743
+ instr = nlp_to_chart_instruction(query, df)
744
+ fig = render_chart_from_instruction(instr, df)
745
+ if fig is None:
746
+ return None
747
+ return fig
748
+ except Exception as e:
749
+ print("nlp visualize error", e)
750
+ return None
751
+
752
+ nl_btn.click(fn=_nl_visualize, inputs=[nl_input, clean_state, df_state], outputs=[nl_plot])
753
+
754
+ # Train model
755
+ def _train_model(task, target, scaler_choice, model_choice, clean_path, df_path, do_rfecv=False):
756
+ try:
757
+ if not target:
758
+ return "Select target variable.", None, None
759
+ if clean_path:
760
+ df = pd.read_csv(clean_path)
761
+ elif df_path:
762
+ df = pd.read_csv(df_path)
 
 
 
 
 
 
 
763
  else:
764
+ return "No dataset available.", None, None
765
+ if target not in df.columns:
766
+ return f"Target '{target}' not in data.", None, None
767
+ # drop high cardinal categorical columns
768
+ df2, dropped = drop_high_cardinality(df)
769
+ X, y = prepare_features_targets(df2, target, drop_cols=None, drop_high_card=False)
770
+ # auto-encode and scale inside training helper
771
+ results = fit_and_evaluate_model(X, y, model_choice, task, scaler_choice, do_rfecv=do_rfecv)
772
+ # create markdown summary
773
+ md = []
774
+ md.append(f"## Model: {model_choice}")
775
+ if task=="regression":
776
+ md.append(f"- MSE: {results['mse']:.4f}")
777
+ md.append(f"- RMSE: {results['rmse']:.4f}")
778
+ md.append(f"- MAE: {results['mae']:.4f}")
779
+ md.append(f"- R2: {results['r2']:.4f}")
780
+ # create QQ plot
781
+ resid = results.get("residuals")
782
+ fig = plt.figure(figsize=(6,4))
783
+ stats.probplot(resid.dropna(), dist="norm", plot=plt)
784
+ plt.title("Q-Q plot of residuals")
785
+ tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".png")
786
+ fig.savefig(tmp.name, bbox_inches="tight")
787
+ plt.close(fig)
788
+ plot_path = tmp.name
789
+ else:
790
+ md.append(f"- Accuracy: {results['accuracy']:.4f}")
791
+ if results.get("roc_auc") is not None:
792
+ md.append(f"- ROC AUC: {results['roc_auc']:.4f}")
793
+ md.append("### Classification report")
794
+ cr = results.get("report")
795
+ if cr:
796
+ md.append(pd.DataFrame(cr).to_markdown())
797
+ # confusion matrix plot
798
+ cm = results.get("confusion_matrix")
799
+ fig = go.Figure(data=go.Heatmap(z=cm, x=["pred_"+str(i) for i in range(len(cm))], y=["true_"+str(i) for i in range(len(cm))], colorscale="Blues"))
800
+ fig.update_layout(title="Confusion Matrix")
801
+ tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".png")
802
+ fig.write_image(tmp.name)
803
+ plot_path = tmp.name
804
+ # feature importance DataFrame
805
+ feat_imp = results.get("feature_importance")
806
+ if feat_imp:
807
+ fi_df = pd.DataFrame(feat_imp, columns=["feature","importance"])
808
+ else:
809
+ fi_df = pd.DataFrame()
810
+ return ("\n".join(md), fi_df, gr.Image.update(value=plot_path))
811
+ except Exception as e:
812
+ tb = traceback.format_exc()
813
+ print(tb)
814
+ return f"Training error: {e}", pd.DataFrame(), None
815
+
816
+ train_btn.click(fn=_train_model, inputs=[task_select, target_col, scaler_choice, model_select, clean_state, df_state, rfecv_opt], outputs=[model_out_md, model_feature_imp, model_plots])
817
+
818
+ # report generation: simple markdown
819
+ def _generate_report(profile, prep_summary, model_summary, feature_imp_df):
820
+ try:
821
+ lines = []
822
+ lines.append("# Executive Report — DataSynth")
823
+ lines.append("## Data Profile")
824
+ if profile:
825
+ lines.append(profile_to_markdown(profile))
826
+ else:
827
+ lines.append("No profile available.")
828
+ lines.append("\n## Preparation")
829
+ lines.append(prep_summary or "No preparation performed.")
830
+ lines.append("\n## Model Summary")
831
+ lines.append(model_summary or "No model results.")
832
+ if isinstance(feature_imp_df, pd.DataFrame) and not feature_imp_df.empty:
833
+ lines.append("\n## Feature Importances")
834
+ lines.append(feature_imp_df.to_markdown(index=False))
835
+ # save markdown
836
+ tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".md", mode="w", encoding="utf-8")
837
+ tmp.write("\n".join(lines))
838
+ tmp.flush()
839
+ return tmp.name
840
+ except Exception as e:
841
+ return None
842
+
843
+ report_btn.click(fn=_generate_report, inputs=[profile_state, prep_output, model_out_md, model_feature_imp], outputs=[report_download])
844
+
845
+ # initial no-op
846
+ demo.load(lambda: None, outputs=[])
847
+
848
+ if __name__ == "__main__":
849
+ demo.launch(server_name="0.0.0.0", server_port=7860, share=False)