Varun95 commited on
Commit
f2256b4
·
verified ·
1 Parent(s): fdc34a4

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +561 -30
app.py CHANGED
@@ -1,33 +1,564 @@
 
 
 
 
 
1
  import gradio as gr
2
- from PIL import Image
3
- from transformers import TrOCRProcessor, VisionEncoderDecoderModel
4
-
5
- # Load lightweight CPU-friendly model
6
- processor = TrOCRProcessor.from_pretrained("microsoft/trocr-small-handwritten")
7
- model = VisionEncoderDecoderModel.from_pretrained("microsoft/trocr-small-handwritten")
8
-
9
- def parse_handwriting(image):
10
- if image is None:
11
- return "Please upload an image."
12
-
13
- # Convert image to RGB
14
- image = image.convert("RGB")
15
-
16
- # Process image and generate text
17
- pixel_values = processor(images=image, return_tensors="pt").pixel_values
18
- generated_ids = model.generate(pixel_values)
19
- generated_text = processor.batch_decode(generated_ids, skip_special_tokens=True)[0]
20
-
21
- return generated_text
22
-
23
- # Simple Gradio interface
24
- demo = gr.Interface(
25
- fn=parse_handwriting,
26
- inputs=gr.Image(type="pil", label="Upload Handwritten Image"),
27
- outputs=gr.Textbox(label="Extracted Text"),
28
- title="Handwriting Parser (CPU Friendly)",
29
- description="Upload a cropped image of handwritten text to transcribe it."
30
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
 
32
  if __name__ == "__main__":
33
- demo.launch()
 
1
+ """
2
+ Data Cleaning & Visualization Project
3
+ A Gradio-based interactive dashboard for cleaning raw datasets and generating insights.
4
+ """
5
+
6
  import gradio as gr
7
+ import pandas as pd
8
+ import numpy as np
9
+ import matplotlib.pyplot as plt
10
+ import seaborn as sns
11
+ from io import StringIO
12
+ import warnings
13
+
14
+ warnings.filterwarnings("ignore")
15
+ sns.set_theme(style="whitegrid", palette="muted")
16
+ plt.rcParams["figure.figsize"] = (10, 5)
17
+ plt.rcParams["figure.dpi"] = 100
18
+
19
+
20
+ # ---------------------------------------------------------------------------
21
+ # Helper functions
22
+ # ---------------------------------------------------------------------------
23
+
24
+ def load_dataframe(file):
25
+ """Load CSV or Excel file into a DataFrame."""
26
+ if file is None:
27
+ return None, "Please upload a file first."
28
+ try:
29
+ path = file.name if hasattr(file, "name") else file
30
+ if str(path).lower().endswith((".xlsx", ".xls")):
31
+ df = pd.read_excel(path)
32
+ else:
33
+ df = pd.read_csv(path)
34
+ return df, f"Loaded successfully • Shape: {df.shape[0]} rows × {df.shape[1]} columns"
35
+ except Exception as e:
36
+ return None, f"Error loading file: {str(e)}"
37
+
38
+
39
+ def get_overview(df):
40
+ """Return text overview of the dataset."""
41
+ if df is None:
42
+ return "No data loaded."
43
+
44
+ buffer = StringIO()
45
+ df.info(buf=buffer)
46
+ info_str = buffer.getvalue()
47
+
48
+ missing = df.isnull().sum()
49
+ missing_pct = (missing / len(df) * 100).round(2)
50
+ missing_df = pd.DataFrame({"Missing Count": missing, "Missing %": missing_pct})
51
+ missing_df = missing_df[missing_df["Missing Count"] > 0].sort_values(
52
+ "Missing Count", ascending=False
53
+ )
54
+
55
+ overview = f"""
56
+ ### Dataset Overview
57
+ - **Rows:** {df.shape[0]:,}
58
+ - **Columns:** {df.shape[1]}
59
+ - **Memory Usage:** {df.memory_usage(deep=True).sum() / 1024**2:.2f} MB
60
+ - **Duplicate Rows:** {df.duplicated().sum():,}
61
+ - **Numeric Columns:** {len(df.select_dtypes(include=np.number).columns)}
62
+ - **Categorical Columns:** {len(df.select_dtypes(include=["object", "category"]).columns)}
63
+
64
+ ### Missing Values
65
+ """
66
+ if missing_df.empty:
67
+ overview += "No missing values found.\n"
68
+ else:
69
+ overview += missing_df.to_markdown() + "\n"
70
+
71
+ overview += "\n### Column Data Types\n"
72
+ overview += df.dtypes.to_frame("dtype").to_markdown()
73
+
74
+ overview += "\n\n### First 5 Rows\n"
75
+ overview += df.head().to_markdown(index=False)
76
+
77
+ return overview
78
+
79
+
80
+ def handle_missing(df, strategy, fill_value=None):
81
+ """Handle missing values according to chosen strategy."""
82
+ if df is None:
83
+ return None, "No data loaded."
84
+
85
+ df = df.copy()
86
+ before = df.isnull().sum().sum()
87
+
88
+ if strategy == "Drop rows with any missing":
89
+ df = df.dropna()
90
+ elif strategy == "Drop columns with >50% missing":
91
+ thresh = len(df) * 0.5
92
+ df = df.dropna(axis=1, thresh=thresh)
93
+ elif strategy == "Fill numeric with mean":
94
+ num_cols = df.select_dtypes(include=np.number).columns
95
+ df[num_cols] = df[num_cols].fillna(df[num_cols].mean())
96
+ elif strategy == "Fill numeric with median":
97
+ num_cols = df.select_dtypes(include=np.number).columns
98
+ df[num_cols] = df[num_cols].fillna(df[num_cols].median())
99
+ elif strategy == "Fill categorical with mode":
100
+ cat_cols = df.select_dtypes(include=["object", "category"]).columns
101
+ for col in cat_cols:
102
+ mode_val = df[col].mode()
103
+ if not mode_val.empty:
104
+ df[col] = df[col].fillna(mode_val[0])
105
+ elif strategy == "Fill with custom value":
106
+ if fill_value is None or fill_value == "":
107
+ return df, "Please provide a custom fill value."
108
+ try:
109
+ # try numeric conversion
110
+ val = float(fill_value)
111
+ df = df.fillna(val)
112
+ except ValueError:
113
+ df = df.fillna(fill_value)
114
+ elif strategy == "Forward fill":
115
+ df = df.ffill()
116
+ elif strategy == "Backward fill":
117
+ df = df.bfill()
118
+
119
+ after = df.isnull().sum().sum()
120
+ msg = f"Missing values: {before} → {after} | Rows remaining: {len(df):,}"
121
+ return df, msg
122
+
123
+
124
+ def remove_duplicates(df):
125
+ """Remove duplicate rows."""
126
+ if df is None:
127
+ return None, "No data loaded."
128
+ before = len(df)
129
+ df = df.drop_duplicates()
130
+ after = len(df)
131
+ return df, f"Duplicates removed: {before - after} | Rows remaining: {after:,}"
132
+
133
+
134
+ def detect_outliers_iqr(df, columns=None):
135
+ """Detect outliers using IQR method. Returns boolean mask of outlier rows."""
136
+ if df is None:
137
+ return None
138
+ num_cols = df.select_dtypes(include=np.number).columns.tolist()
139
+ if columns:
140
+ num_cols = [c for c in columns if c in num_cols]
141
+ if not num_cols:
142
+ return pd.Series([False] * len(df), index=df.index)
143
+
144
+ outlier_mask = pd.Series([False] * len(df), index=df.index)
145
+ for col in num_cols:
146
+ Q1 = df[col].quantile(0.25)
147
+ Q3 = df[col].quantile(0.75)
148
+ IQR = Q3 - Q1
149
+ lower = Q1 - 1.5 * IQR
150
+ upper = Q3 + 1.5 * IQR
151
+ outlier_mask |= (df[col] < lower) | (df[col] > upper)
152
+ return outlier_mask
153
+
154
+
155
+ def handle_outliers(df, method, columns=None):
156
+ """Handle outliers: remove or cap them."""
157
+ if df is None:
158
+ return None, "No data loaded."
159
+
160
+ df = df.copy()
161
+ num_cols = df.select_dtypes(include=np.number).columns.tolist()
162
+ if columns:
163
+ selected = [c.strip() for c in columns.split(",") if c.strip()]
164
+ num_cols = [c for c in selected if c in num_cols]
165
+ if not num_cols:
166
+ return df, "No numeric columns selected for outlier handling."
167
+
168
+ mask = detect_outliers_iqr(df, num_cols)
169
+ n_outliers = mask.sum()
170
+
171
+ if method == "Remove outlier rows":
172
+ df = df[~mask]
173
+ msg = f"Removed {n_outliers} outlier rows | Rows remaining: {len(df):,}"
174
+ elif method == "Cap outliers (winsorize)":
175
+ for col in num_cols:
176
+ Q1 = df[col].quantile(0.25)
177
+ Q3 = df[col].quantile(0.75)
178
+ IQR = Q3 - Q1
179
+ lower = Q1 - 1.5 * IQR
180
+ upper = Q3 + 1.5 * IQR
181
+ df[col] = df[col].clip(lower=lower, upper=upper)
182
+ msg = f"Capped outliers in columns: {', '.join(num_cols)} | Outliers affected: {n_outliers}"
183
+ else:
184
+ msg = "No action taken."
185
+ return df, msg
186
+
187
+
188
+ def plot_missing_heatmap(df):
189
+ """Visualize missing values."""
190
+ if df is None or df.empty:
191
+ return None
192
+ fig, ax = plt.subplots(figsize=(12, 5))
193
+ sns.heatmap(df.isnull(), cbar=False, yticklabels=False, cmap="YlOrRd", ax=ax)
194
+ ax.set_title("Missing Values Heatmap (yellow/red = missing)")
195
+ plt.tight_layout()
196
+ return fig
197
+
198
+
199
+ def plot_distributions(df, columns=None):
200
+ """Plot histograms / KDE for numeric columns."""
201
+ if df is None:
202
+ return None
203
+ num_cols = df.select_dtypes(include=np.number).columns.tolist()
204
+ if columns:
205
+ selected = [c.strip() for c in columns.split(",") if c.strip()]
206
+ num_cols = [c for c in selected if c in num_cols]
207
+ if not num_cols:
208
+ return None
209
+
210
+ n = min(len(num_cols), 6)
211
+ cols = num_cols[:n]
212
+ fig, axes = plt.subplots(1, n, figsize=(4 * n, 4))
213
+ if n == 1:
214
+ axes = [axes]
215
+ for ax, col in zip(axes, cols):
216
+ sns.histplot(df[col].dropna(), kde=True, ax=ax, color="#4C72B0")
217
+ ax.set_title(col)
218
+ ax.set_xlabel("")
219
+ plt.suptitle("Numeric Distributions", y=1.02)
220
+ plt.tight_layout()
221
+ return fig
222
+
223
+
224
+ def plot_boxplots(df, columns=None):
225
+ """Boxplots to show outliers."""
226
+ if df is None:
227
+ return None
228
+ num_cols = df.select_dtypes(include=np.number).columns.tolist()
229
+ if columns:
230
+ selected = [c.strip() for c in columns.split(",") if c.strip()]
231
+ num_cols = [c for c in selected if c in num_cols]
232
+ if not num_cols:
233
+ return None
234
+
235
+ n = min(len(num_cols), 6)
236
+ cols = num_cols[:n]
237
+ fig, axes = plt.subplots(1, n, figsize=(3.5 * n, 4))
238
+ if n == 1:
239
+ axes = [axes]
240
+ for ax, col in zip(axes, cols):
241
+ sns.boxplot(y=df[col], ax=ax, color="#55A868")
242
+ ax.set_title(col)
243
+ plt.suptitle("Boxplots (Outlier Detection)", y=1.02)
244
+ plt.tight_layout()
245
+ return fig
246
+
247
+
248
+ def plot_correlation(df):
249
+ """Correlation heatmap for numeric features."""
250
+ if df is None:
251
+ return None
252
+ num_df = df.select_dtypes(include=np.number)
253
+ if num_df.shape[1] < 2:
254
+ return None
255
+ corr = num_df.corr()
256
+ fig, ax = plt.subplots(figsize=(10, 8))
257
+ sns.heatmap(
258
+ corr,
259
+ annot=True,
260
+ fmt=".2f",
261
+ cmap="coolwarm",
262
+ center=0,
263
+ square=True,
264
+ linewidths=0.5,
265
+ ax=ax,
266
+ cbar_kws={"shrink": 0.8},
267
+ )
268
+ ax.set_title("Correlation Matrix")
269
+ plt.tight_layout()
270
+ return fig
271
+
272
+
273
+ def plot_categorical(df, column=None):
274
+ """Bar chart for a categorical column."""
275
+ if df is None:
276
+ return None
277
+ cat_cols = df.select_dtypes(include=["object", "category"]).columns.tolist()
278
+ if not cat_cols:
279
+ return None
280
+ col = column if column and column in cat_cols else cat_cols[0]
281
+ counts = df[col].value_counts().head(15)
282
+ fig, ax = plt.subplots(figsize=(10, 5))
283
+ sns.barplot(x=counts.values, y=counts.index, ax=ax, palette="viridis")
284
+ ax.set_title(f"Top categories in '{col}'")
285
+ ax.set_xlabel("Count")
286
+ plt.tight_layout()
287
+ return fig
288
+
289
+
290
+ def generate_summary_report(df):
291
+ """Generate a storytelling-style text report of key findings."""
292
+ if df is None:
293
+ return "No data available."
294
+
295
+ lines = ["# Data Insights Report\n"]
296
+ lines.append(f"**Dataset size after cleaning:** {df.shape[0]:,} rows × {df.shape[1]} columns\n")
297
+
298
+ # Missing
299
+ missing_total = df.isnull().sum().sum()
300
+ lines.append(f"- Remaining missing values: **{missing_total}**\n")
301
+
302
+ # Duplicates
303
+ lines.append(f"- Duplicate rows remaining: **{df.duplicated().sum()}**\n")
304
+
305
+ # Numeric summary
306
+ num_df = df.select_dtypes(include=np.number)
307
+ if not num_df.empty:
308
+ lines.append("\n## Numeric Features Summary\n")
309
+ desc = num_df.describe().T[["mean", "std", "min", "50%", "max"]].round(2)
310
+ lines.append(desc.to_markdown())
311
+ lines.append("\n")
312
+
313
+ # Strongest correlations
314
+ if num_df.shape[1] >= 2:
315
+ corr = num_df.corr().abs()
316
+ # zero out diagonal without mutating original
317
+ corr_no_diag = corr.where(~np.eye(corr.shape[0], dtype=bool), 0)
318
+ if corr_no_diag.max().max() > 0:
319
+ max_pair = corr_no_diag.stack().idxmax()
320
+ lines.append(
321
+ f"\n**Strongest correlation:** `{max_pair[0]}` ↔ `{max_pair[1]}` "
322
+ f"(r = {num_df.corr().loc[max_pair]:.3f})\n"
323
+ )
324
+
325
+ # Categorical
326
+ cat_cols = df.select_dtypes(include=["object", "category"]).columns
327
+ if len(cat_cols) > 0:
328
+ lines.append("\n## Categorical Features\n")
329
+ for col in cat_cols[:5]:
330
+ top = df[col].value_counts().head(3)
331
+ lines.append(f"- **{col}** top values: " + ", ".join(f"`{k}` ({v})" for k, v in top.items()))
332
+ lines.append("\n")
333
+
334
+ lines.append("\n---\n*Report generated by the Data Cleaning & Visualization dashboard.*")
335
+ return "\n".join(lines)
336
+
337
+
338
+ # ---------------------------------------------------------------------------
339
+ # Gradio UI callbacks
340
+ # ---------------------------------------------------------------------------
341
+
342
+ state_df = gr.State(value=None)
343
+
344
+
345
+ def on_upload(file, current_df):
346
+ df, msg = load_dataframe(file)
347
+ if df is not None:
348
+ return df, msg, get_overview(df), df.head(20)
349
+ return current_df, msg, "Upload failed.", None
350
+
351
+
352
+ def on_clean_missing(df, strategy, fill_value):
353
+ if df is None:
354
+ return None, "Upload data first.", None, None
355
+ new_df, msg = handle_missing(df, strategy, fill_value)
356
+ return new_df, msg, get_overview(new_df), new_df.head(20)
357
+
358
+
359
+ def on_remove_dupes(df):
360
+ if df is None:
361
+ return None, "Upload data first.", None, None
362
+ new_df, msg = remove_duplicates(df)
363
+ return new_df, msg, get_overview(new_df), new_df.head(20)
364
+
365
+
366
+ def on_outliers(df, method, columns):
367
+ if df is None:
368
+ return None, "Upload data first.", None, None
369
+ new_df, msg = handle_outliers(df, method, columns)
370
+ return new_df, msg, get_overview(new_df), new_df.head(20)
371
+
372
+
373
+ def on_visualize(df, dist_cols, box_cols, cat_col):
374
+ if df is None:
375
+ return None, None, None, None, "Upload & clean data first."
376
+ fig_miss = plot_missing_heatmap(df)
377
+ fig_dist = plot_distributions(df, dist_cols)
378
+ fig_box = plot_boxplots(df, box_cols)
379
+ fig_corr = plot_correlation(df)
380
+ fig_cat = plot_categorical(df, cat_col)
381
+ report = generate_summary_report(df)
382
+ return fig_miss, fig_dist, fig_box, fig_corr, fig_cat, report
383
+
384
+
385
+ def download_cleaned(df):
386
+ if df is None:
387
+ return None
388
+ path = "/tmp/cleaned_dataset.csv"
389
+ df.to_csv(path, index=False)
390
+ return path
391
+
392
+
393
+ # ---------------------------------------------------------------------------
394
+ # Build the Gradio interface
395
+ # ---------------------------------------------------------------------------
396
+
397
+ CUSTOM_CSS = """
398
+ .gradio-container { max-width: 1200px !important; }
399
+ .header-text { text-align: center; margin-bottom: 1rem; }
400
+ """
401
+
402
+ with gr.Blocks(
403
+ title="Data Cleaning & Visualization",
404
+ theme=gr.themes.Soft(primary_hue="blue", secondary_hue="slate"),
405
+ css=CUSTOM_CSS,
406
+ ) as demo:
407
+
408
+ gr.Markdown(
409
+ """
410
+ # Data Cleaning & Visualization Project
411
+ **Upload a raw dataset → clean missing values, outliers & duplicates → explore insights with interactive charts.**
412
+ """
413
+ )
414
+
415
+ current_df = gr.State(None)
416
+
417
+ with gr.Tab("1. Upload & Overview"):
418
+ with gr.Row():
419
+ file_input = gr.File(
420
+ label="Upload CSV or Excel file",
421
+ file_types=[".csv", ".xlsx", ".xls"],
422
+ )
423
+ status_msg = gr.Textbox(label="Status", interactive=False)
424
+
425
+ overview_md = gr.Markdown("Upload a file to see the dataset overview.")
426
+ preview_table = gr.Dataframe(label="Data Preview (first 20 rows)", interactive=False)
427
+
428
+ file_input.change(
429
+ on_upload,
430
+ inputs=[file_input, current_df],
431
+ outputs=[current_df, status_msg, overview_md, preview_table],
432
+ )
433
+
434
+ with gr.Tab("2. Clean Data"):
435
+ gr.Markdown("### Handle Missing Values")
436
+ with gr.Row():
437
+ missing_strategy = gr.Dropdown(
438
+ choices=[
439
+ "Drop rows with any missing",
440
+ "Drop columns with >50% missing",
441
+ "Fill numeric with mean",
442
+ "Fill numeric with median",
443
+ "Fill categorical with mode",
444
+ "Fill with custom value",
445
+ "Forward fill",
446
+ "Backward fill",
447
+ ],
448
+ value="Fill numeric with median",
449
+ label="Missing-value strategy",
450
+ )
451
+ custom_fill = gr.Textbox(
452
+ label="Custom fill value (only for 'Fill with custom value')",
453
+ placeholder="e.g. 0 or Unknown",
454
+ )
455
+ btn_missing = gr.Button("Apply Missing-Value Handling", variant="primary")
456
+
457
+ gr.Markdown("### Remove Duplicates")
458
+ btn_dupes = gr.Button("Remove Duplicate Rows", variant="secondary")
459
+
460
+ gr.Markdown("### Handle Outliers (IQR method)")
461
+ with gr.Row():
462
+ outlier_method = gr.Dropdown(
463
+ choices=["Remove outlier rows", "Cap outliers (winsorize)"],
464
+ value="Cap outliers (winsorize)",
465
+ label="Outlier handling method",
466
+ )
467
+ outlier_cols = gr.Textbox(
468
+ label="Columns (comma-separated, leave blank for all numeric)",
469
+ placeholder="e.g. age, salary, price",
470
+ )
471
+ btn_outliers = gr.Button("Apply Outlier Handling", variant="primary")
472
+
473
+ clean_status = gr.Textbox(label="Cleaning Status", interactive=False)
474
+ clean_overview = gr.Markdown()
475
+ clean_preview = gr.Dataframe(label="Cleaned Preview", interactive=False)
476
+
477
+ btn_missing.click(
478
+ on_clean_missing,
479
+ inputs=[current_df, missing_strategy, custom_fill],
480
+ outputs=[current_df, clean_status, clean_overview, clean_preview],
481
+ )
482
+ btn_dupes.click(
483
+ on_remove_dupes,
484
+ inputs=[current_df],
485
+ outputs=[current_df, clean_status, clean_overview, clean_preview],
486
+ )
487
+ btn_outliers.click(
488
+ on_outliers,
489
+ inputs=[current_df, outlier_method, outlier_cols],
490
+ outputs=[current_df, clean_status, clean_overview, clean_preview],
491
+ )
492
+
493
+ gr.Markdown("---")
494
+ download_btn = gr.Button("Download Cleaned CSV")
495
+ download_file = gr.File(label="Download")
496
+ download_btn.click(download_cleaned, inputs=[current_df], outputs=[download_file])
497
+
498
+ with gr.Tab("3. Visualize & Insights"):
499
+ gr.Markdown("Select columns (optional) and generate plots + an automatic insights report.")
500
+ with gr.Row():
501
+ dist_cols = gr.Textbox(
502
+ label="Distribution columns (comma-separated)",
503
+ placeholder="Leave blank for first 6 numeric columns",
504
+ )
505
+ box_cols = gr.Textbox(
506
+ label="Boxplot columns (comma-separated)",
507
+ placeholder="Leave blank for first 6 numeric columns",
508
+ )
509
+ cat_col = gr.Textbox(
510
+ label="Categorical column for bar chart",
511
+ placeholder="Leave blank for first categorical column",
512
+ )
513
+ btn_viz = gr.Button("Generate Visualizations & Report", variant="primary")
514
+
515
+ with gr.Row():
516
+ plot_miss = gr.Plot(label="Missing Values Heatmap")
517
+ plot_corr = gr.Plot(label="Correlation Matrix")
518
+ with gr.Row():
519
+ plot_dist = gr.Plot(label="Distributions")
520
+ plot_box = gr.Plot(label="Boxplots")
521
+ plot_cat = gr.Plot(label="Categorical Breakdown")
522
+ insights_md = gr.Markdown(label="Insights Report")
523
+
524
+ btn_viz.click(
525
+ on_visualize,
526
+ inputs=[current_df, dist_cols, box_cols, cat_col],
527
+ outputs=[plot_miss, plot_dist, plot_box, plot_corr, plot_cat, insights_md],
528
+ )
529
+
530
+ with gr.Tab("About"):
531
+ gr.Markdown(
532
+ """
533
+ ## Project Goals
534
+ This interactive dashboard demonstrates the complete workflow of a **Data Cleaning & Visualization** project:
535
+
536
+ ### Key Features Implemented
537
+ - **Handle missing values** – multiple strategies (drop, mean/median/mode fill, ffill/bfill, custom)
538
+ - **Remove duplicates**
539
+ - **Detect & treat outliers** using the IQR method (remove or winsorize/cap)
540
+ - **Visualizations** with Matplotlib & Seaborn:
541
+ - Missing-value heatmap
542
+ - Histograms / KDE distributions
543
+ - Boxplots for outlier inspection
544
+ - Correlation matrix
545
+ - Categorical bar charts
546
+ - **Automated insights report** that tells a short data story
547
+
548
+ ### Expected Learning Outcomes
549
+ - Data preprocessing techniques
550
+ - Exploratory data visualization
551
+ - Storytelling with data
552
+
553
+ ### How to use
554
+ 1. Upload any CSV or Excel file in **Tab 1**
555
+ 2. Clean the data step-by-step in **Tab 2**
556
+ 3. Generate charts and the insights report in **Tab 3**
557
+ 4. Download the cleaned dataset when ready
558
+
559
+ Built with **Gradio**, **Pandas**, **Matplotlib** & **Seaborn**.
560
+ """
561
+ )
562
 
563
  if __name__ == "__main__":
564
+ demo.launch(server_name="0.0.0.0", server_port=7860, share=True)