Cosmographer commited on
Commit
dfe3a3c
Β·
verified Β·
1 Parent(s): 74ea6b8

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +306 -40
app.py CHANGED
@@ -9,6 +9,10 @@ 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
@@ -121,57 +125,276 @@ def read_file_to_df(uploaded) -> pd.DataFrame:
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
- # safe markdown conversion of sample rows without requiring 'tabulate'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
159
  try:
160
- df_head = pd.DataFrame(profile.get("head", []))
161
- if df_head.empty:
162
- md.append("No sample rows available.\n")
163
- else:
164
- headers = list(df_head.columns)
165
- md.append("| " + " | ".join(headers) + " |\n")
166
- md.append("|" + "|".join(["---"] * len(headers)) + "|\n")
167
- # rows
168
- for _, row in df_head.iterrows():
169
- row_vals = [str(row.get(h, "")) for h in headers]
170
- md.append("| " + " | ".join(row_vals) + " |\n")
171
- except Exception:
172
- md.append(str(profile.get("head", "")) + "\n")
173
- return "\n".join(md)
 
 
 
 
174
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
175
  # -----------------------------
176
  # Data cleaning & feature engineering helpers
177
  # -----------------------------
@@ -602,13 +825,27 @@ with gr.Blocks(css=css, title="DataSynth β€” Analytics Hub") as demo:
602
  gr.Markdown("## Workspace")
603
  with gr.Tabs():
604
  with gr.TabItem("Data"):
605
- gr.Markdown("Upload CSV / Excel / JSON for profiling and cleaning.")
606
  upload = gr.File(label="Upload CSV / Excel / JSON", file_types=[".csv", ".xlsx", ".xls", ".json"])
607
  with gr.Row():
608
- load_btn = gr.Button("Load & Profile")
609
- download_raw_btn = gr.Button("Download Raw CSV")
610
- profile_md = gr.Markdown("No dataset loaded.")
611
- sample_table = gr.Dataframe(interactive=False)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
612
  with gr.TabItem("Prepare"):
613
  gr.Markdown("Data cleaning and feature engineering options.")
614
  with gr.Row():
@@ -666,21 +903,50 @@ with gr.Blocks(css=css, title="DataSynth β€” Analytics Hub") as demo:
666
  def _load_and_profile(uploaded):
667
  try:
668
  if uploaded is None:
669
- return gr.update(value="No file uploaded."), pd.DataFrame(), None, None
 
 
670
  df = read_file_to_df(uploaded)
671
- prof = basic_profile(df)
672
- md = profile_to_markdown(prof)
 
 
 
 
 
 
 
 
673
  # prepare choices for date and target selectors
674
- cols = df.columns.tolist()
675
  # save df to temp csv for persistence (store path in state)
676
  tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".csv")
677
  df.to_csv(tmp.name, index=False)
678
- return md, df.head(100), tmp.name, prof
 
 
679
  except Exception as e:
680
- return gr.update(value=f"Error loading file: {e}"), pd.DataFrame(), None, None
 
 
681
 
682
- load_btn.click(fn=_load_and_profile, inputs=[upload], outputs=[profile_md, sample_table, df_state, profile_state])
 
 
 
 
 
 
 
 
 
 
 
 
683
 
 
 
 
684
  # download raw
685
  def _download_raw(df_path):
686
  if not df_path:
 
9
  import pandas as pd
10
  import numpy as np
11
  import gradio as gr
12
+ import seaborn as sns
13
+ from scipy import stats
14
+ import warnings
15
+ warnings.filterwarnings('ignore')
16
  import plotly.express as px
17
  import plotly.graph_objects as go
18
  import matplotlib.pyplot as plt
 
125
  print("read_file_to_df error:", e)
126
  raise
127
 
128
+ def comprehensive_data_profile(df: pd.DataFrame) -> Dict[str, Any]:
129
+ """Enhanced data profiling with comprehensive analysis"""
130
  if df is None or df.empty:
131
  return {}
132
+
133
  profile = {}
134
  profile["rows"], profile["columns"] = df.shape
135
+ profile["memory_usage"] = df.memory_usage(deep=True).sum() / 1024**2 # MB
136
+
137
+ # Basic info
138
  dtypes = df.dtypes.apply(lambda x: x.name).to_dict()
139
  profile["dtypes"] = dtypes
140
+
141
+ # Null analysis
142
  nulls = df.isnull().sum().to_dict()
143
  profile["nulls"] = nulls
144
  profile["null_pct"] = {k: (v / len(df)) for k, v in nulls.items()}
145
+
146
+ # Uniqueness analysis
147
  unique_counts = df.nunique(dropna=False).to_dict()
148
  profile["unique"] = unique_counts
149
+ profile["duplicate_rows"] = df.duplicated().sum()
150
+ profile["duplicate_pct"] = profile["duplicate_rows"] / len(df)
151
+
152
+ # Column type classification
153
+ numeric_cols = df.select_dtypes(include=[np.number]).columns.tolist()
154
+ categorical_cols = df.select_dtypes(include=["object", "category"]).columns.tolist()
155
+ datetime_cols = df.select_dtypes(include=["datetime64"]).columns.tolist()
156
+
157
+ profile["numeric_cols"] = numeric_cols
158
+ profile["categorical_cols"] = categorical_cols
159
+ profile["datetime_cols"] = datetime_cols
160
+
161
+ # High cardinality detection
162
+ high_cardinality = []
163
+ for col, cnt in unique_counts.items():
164
+ if col in categorical_cols and (cnt > HIGH_CARD_THRESHOLD_COUNT or
165
+ (cnt / len(df) > HIGH_CARD_THRESHOLD_RATIO)):
166
+ high_cardinality.append(col)
167
+ profile["high_cardinality"] = high_cardinality
168
+
169
+ # Quantitative statistics
170
+ quantitative_stats = {}
171
+ for col in numeric_cols:
172
+ col_data = df[col].dropna()
173
+ if len(col_data) > 0:
174
+ quantitative_stats[col] = {
175
+ "mean": col_data.mean(),
176
+ "median": col_data.median(),
177
+ "std": col_data.std(),
178
+ "min": col_data.min(),
179
+ "max": col_data.max(),
180
+ "q1": col_data.quantile(0.25),
181
+ "q3": col_data.quantile(0.75),
182
+ "skew": col_data.skew(),
183
+ "kurtosis": col_data.kurtosis(),
184
+ "zeros": (col_data == 0).sum(),
185
+ "zeros_pct": (col_data == 0).sum() / len(col_data),
186
+ "outliers": detect_outliers_iqr(col_data)
187
+ }
188
+ profile["quantitative_stats"] = quantitative_stats
189
+
190
+ # Qualitative statistics
191
+ qualitative_stats = {}
192
+ for col in categorical_cols:
193
+ col_data = df[col].dropna()
194
+ if len(col_data) > 0:
195
+ value_counts = col_data.value_counts()
196
+ qualitative_stats[col] = {
197
+ "top_value": value_counts.index[0] if len(value_counts) > 0 else None,
198
+ "top_freq": value_counts.iloc[0] if len(value_counts) > 0 else 0,
199
+ "top_freq_pct": value_counts.iloc[0] / len(col_data) if len(value_counts) > 0 else 0,
200
+ "unique_values": len(value_counts),
201
+ "entropy": stats.entropy(value_counts.values) if len(value_counts) > 0 else 0
202
+ }
203
+ profile["qualitative_stats"] = qualitative_stats
204
+
205
+ # Data quality indicators
206
+ quality_metrics = {
207
+ "completeness": (len(df) - df.isnull().sum().sum()) / (len(df) * len(df.columns)),
208
+ "uniqueness": 1 - (profile["duplicate_pct"]),
209
+ "validity": {} # Could be extended with domain-specific rules
210
+ }
211
+ profile["quality_metrics"] = quality_metrics
212
+
213
+ # Correlation matrix for numeric columns
214
+ if len(numeric_cols) > 1:
215
+ profile["correlation_matrix"] = df[numeric_cols].corr().round(3).to_dict()
216
+ else:
217
+ profile["correlation_matrix"] = {}
218
+
219
+ # Sample data
220
+ profile["head"] = df.head(10).to_dict(orient="records")
221
+
222
  return profile
223
 
224
+ def detect_outliers_iqr(series):
225
+ """Detect outliers using IQR method"""
226
+ Q1 = series.quantile(0.25)
227
+ Q3 = series.quantile(0.75)
228
+ IQR = Q3 - Q1
229
+ lower_bound = Q1 - 1.5 * IQR
230
+ upper_bound = Q3 + 1.5 * IQR
231
+ return ((series < lower_bound) | (series > upper_bound)).sum()
232
+
233
+ def profile_to_enhanced_markdown(profile: Dict[str, Any]) -> str:
234
+ """Convert comprehensive profile to rich markdown with visual indicators"""
235
  if not profile:
236
  return "No data loaded."
237
+
238
  md = []
239
+
240
+ # Header with key metrics
241
+ md.append(f"## πŸ“Š Comprehensive Data Profile Report")
242
+ md.append("---")
243
+
244
+ # Dataset Overview
245
+ md.append("### 🎯 Dataset Overview")
246
+ md.append(f"- **Rows:** {profile['rows']:,}")
247
+ md.append(f"- **Columns:** {profile['columns']}")
248
+ md.append(f"- **Memory Usage:** {profile['memory_usage']:.2f} MB")
249
+ md.append(f"- **Duplicate Rows:** {profile['duplicate_rows']} ({profile['duplicate_pct']:.2%})")
250
+ md.append("")
251
+
252
+ # Data Quality Scorecard
253
+ md.append("### πŸ“ˆ Data Quality Scorecard")
254
+ quality = profile["quality_metrics"]
255
+ md.append(f"- **Completeness:** {quality['completeness']:.2%}")
256
+ md.append(f"- **Uniqueness:** {quality['uniqueness']:.2%}")
257
+ md.append("")
258
+
259
+ # Column Type Summary
260
+ md.append("### πŸ—‚οΈ Column Type Summary")
261
+ md.append(f"- **Numeric Columns:** {len(profile['numeric_cols'])}")
262
+ md.append(f"- **Categorical Columns:** {len(profile['categorical_cols'])}")
263
+ md.append(f"- **DateTime Columns:** {len(profile['datetime_cols'])}")
264
+ md.append("")
265
+
266
+ # Detailed Column Analysis
267
+ md.append("### πŸ” Detailed Column Analysis")
268
+ md.append("| Column | Type | Nulls | Null % | Unique | Completeness | Issues |")
269
+ md.append("|--------|------|-------|---------|--------|--------------|--------|")
270
+
271
  for col in profile["dtypes"].keys():
272
  dtype = profile["dtypes"][col]
273
  nulls = profile["nulls"].get(col, 0)
274
+ null_pct = profile["null_pct"].get(col, 0)
275
  uniq = profile["unique"].get(col, 0)
276
+ completeness = 1 - null_pct
277
+
278
+ # Issue indicators
279
+ issues = []
280
+ if null_pct > 0.5:
281
+ issues.append("πŸ”΄ High nulls")
282
+ elif null_pct > 0.2:
283
+ issues.append("🟑 Medium nulls")
284
+
285
+ if col in profile["high_cardinality"]:
286
+ issues.append("πŸ”΅ High cardinality")
287
+
288
+ if col in profile["numeric_cols"]:
289
+ stats = profile["quantitative_stats"].get(col, {})
290
+ outliers = stats.get("outliers", 0)
291
+ if outliers > 0:
292
+ issues.append("⚫ Outliers")
293
+
294
+ issues_str = ", ".join(issues) if issues else "βœ… Good"
295
+
296
+ md.append(f"| {col} | {dtype} | {nulls} | {null_pct:.2%} | {uniq} | {completeness:.2%} | {issues_str} |")
297
+ md.append("")
298
+
299
+ # Quantitative Columns Deep Dive
300
+ if profile["quantitative_stats"]:
301
+ md.append("### πŸ“ˆ Quantitative Columns Analysis")
302
+ md.append("| Column | Mean | Std | Min | Max | Skew | Outliers | Zeros |")
303
+ md.append("|--------|------|-----|-----|-----|------|----------|-------|")
304
+
305
+ for col, stats in profile["quantitative_stats"].items():
306
+ md.append(f"| {col} | {stats['mean']:.2f} | {stats['std']:.2f} | {stats['min']:.2f} | {stats['max']:.2f} | {stats['skew']:.2f} | {stats['outliers']} | {stats['zeros']} |")
307
+ md.append("")
308
+
309
+ # Qualitative Columns Deep Dive
310
+ if profile["qualitative_stats"]:
311
+ md.append("### πŸ“Š Qualitative Columns Analysis")
312
+ md.append("| Column | Top Value | Top Freq | Top % | Unique | Entropy |")
313
+ md.append("|--------|-----------|----------|-------|--------|---------|")
314
+
315
+ for col, stats in profile["qualitative_stats"].items():
316
+ top_value = str(stats["top_value"])[:20] + "..." if len(str(stats["top_value"])) > 20 else str(stats["top_value"])
317
+ md.append(f"| {col} | {top_value} | {stats['top_freq']} | {stats['top_freq_pct']:.2%} | {stats['unique_values']} | {stats['entropy']:.2f} |")
318
+ md.append("")
319
+
320
+ # High Cardinality Warning
321
  if profile["high_cardinality"]:
322
+ md.append("### ⚠️ High Cardinality Columns")
323
+ md.append("The following columns have high cardinality (may impact modeling):")
324
+ for col in profile["high_cardinality"]:
325
+ md.append(f"- **{col}**: {profile['unique'][col]} unique values")
326
+ md.append("")
327
+
328
+ # Correlation Highlights (if available)
329
+ if profile["correlation_matrix"]:
330
+ md.append("### πŸ”— Correlation Highlights")
331
+ corr_matrix = profile["correlation_matrix"]
332
+ numeric_cols = list(corr_matrix.keys())
333
+
334
+ # Find strong correlations
335
+ strong_corrs = []
336
+ for i, col1 in enumerate(numeric_cols):
337
+ for j, col2 in enumerate(numeric_cols):
338
+ if i < j: # Avoid duplicates and self-correlation
339
+ corr = abs(corr_matrix[col1][col2])
340
+ if corr > 0.7:
341
+ strong_corrs.append((col1, col2, corr_matrix[col1][col2]))
342
+
343
+ if strong_corrs:
344
+ md.append("**Strong Correlations (|r| > 0.7):**")
345
+ for col1, col2, corr in sorted(strong_corrs, key=lambda x: abs(x[2]), reverse=True):
346
+ md.append(f"- {col1} ↔ {col2}: {corr:.3f}")
347
+ else:
348
+ md.append("No strong correlations found among numeric columns.")
349
+ md.append("")
350
+
351
+ return "\n".join(md)
352
+
353
+ def create_distribution_plots(df: pd.DataFrame, profile: Dict[str, Any]):
354
+ """Create distribution plots for numeric and categorical columns"""
355
+ numeric_cols = profile.get("numeric_cols", [])
356
+ categorical_cols = profile.get("categorical_cols", [])
357
+
358
+ plots = {}
359
+
360
+ # Numeric distributions
361
+ for col in numeric_cols[:4]: # Limit to first 4 for performance
362
  try:
363
+ fig = px.histogram(df, x=col, title=f"Distribution of {col}",
364
+ marginal="box", nbins=50)
365
+ plots[f"num_{col}"] = fig
366
+ except Exception as e:
367
+ print(f"Plot error for {col}: {e}")
368
+
369
+ # Categorical distributions (top 3)
370
+ for col in categorical_cols[:3]:
371
+ try:
372
+ value_counts = df[col].value_counts().head(10)
373
+ fig = px.bar(x=value_counts.index, y=value_counts.values,
374
+ title=f"Top 10 Values in {col}")
375
+ fig.update_layout(xaxis_title=col, yaxis_title="Count")
376
+ plots[f"cat_{col}"] = fig
377
+ except Exception as e:
378
+ print(f"Plot error for {col}: {e}")
379
+
380
+ return plots
381
 
382
+ def create_correlation_plot(df: pd.DataFrame, profile: Dict[str, Any]):
383
+ """Create correlation heatmap"""
384
+ numeric_cols = profile.get("numeric_cols", [])
385
+ if len(numeric_cols) < 2:
386
+ return None
387
+
388
+ try:
389
+ corr_matrix = df[numeric_cols].corr()
390
+ fig = px.imshow(corr_matrix,
391
+ title="Correlation Matrix",
392
+ color_continuous_scale="RdBu_r",
393
+ aspect="auto")
394
+ return fig
395
+ except Exception as e:
396
+ print(f"Correlation plot error: {e}")
397
+ return None
398
  # -----------------------------
399
  # Data cleaning & feature engineering helpers
400
  # -----------------------------
 
825
  gr.Markdown("## Workspace")
826
  with gr.Tabs():
827
  with gr.TabItem("Data"):
828
+ gr.Markdown("Upload CSV / Excel / JSON for comprehensive profiling and analysis.")
829
  upload = gr.File(label="Upload CSV / Excel / JSON", file_types=[".csv", ".xlsx", ".xls", ".json"])
830
  with gr.Row():
831
+ profile_btn = gr.Button("πŸ“Š Data Profiling", variant="primary")
832
+ view_data_btn = gr.Button("πŸ‘€ View Data", variant="secondary")
833
+ download_raw_btn = gr.Button("πŸ“₯ Download Raw CSV")
834
+ with gr.Tabs() as data_tabs:
835
+ with gr.TabItem("Profile Report"):
836
+ profile_md = gr.Markdown("No dataset loaded. Upload data and click 'Data Profiling'.")
837
+
838
+ with gr.TabItem("Data Preview"):
839
+ sample_table = gr.Dataframe(interactive=False, label="Sample Data (First 100 rows)")
840
+
841
+ with gr.TabItem("Visualizations"):
842
+ with gr.Row():
843
+ dist_plot1 = gr.Plot(label="Distribution Plot 1")
844
+ dist_plot2 = gr.Plot(label="Distribution Plot 2")
845
+ with gr.Row():
846
+ dist_plot3 = gr.Plot(label="Distribution Plot 3")
847
+ corr_plot = gr.Plot(label="Correlation Heatmap")
848
+
849
  with gr.TabItem("Prepare"):
850
  gr.Markdown("Data cleaning and feature engineering options.")
851
  with gr.Row():
 
903
  def _load_and_profile(uploaded):
904
  try:
905
  if uploaded is None:
906
+ return (gr.update(value="No file uploaded."),
907
+ pd.DataFrame(), None, None,
908
+ None, None, None, None)
909
  df = read_file_to_df(uploaded)
910
+ prof = comprehensive_data_profile(df)
911
+ md = profile_to_enhanced_markdown(prof)
912
+ #create visulaizations
913
+ plots = create_distribution_plots(df, prof)
914
+ corr_plot = create_correlation_plot(df, prof)
915
+
916
+ # Prepare plot outputs
917
+ plot1 = plots.get(list(plots.keys())[0]) if plots else None
918
+ plot2 = plots.get(list(plots.keys())[1]) if len(plots) > 1 else None
919
+ plot3 = plots.get(list(plots.keys())[2]) if len(plots) > 2 else None
920
  # prepare choices for date and target selectors
921
+ #cols = df.columns.tolist()
922
  # save df to temp csv for persistence (store path in state)
923
  tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".csv")
924
  df.to_csv(tmp.name, index=False)
925
+
926
+ return (md, df.head(100), tmp.name, prof,
927
+ plot1, plot2, plot3, corr_plot)
928
  except Exception as e:
929
+ return (gr.update(value=f"Error loading file: {e}"),
930
+ pd.DataFrame(), None, None,
931
+ None, None, None, None)
932
 
933
+ profile_btn.click(fn=_load_and_profile,
934
+ inputs=[upload],
935
+ outputs=[profile_md, sample_table, df_state, profile_state,
936
+ dist_plot1, dist_plot2, dist_plot3, corr_plot])
937
+
938
+ def _view_data(uploaded):
939
+ try:
940
+ if uploaded is None:
941
+ return pd.DataFrame()
942
+ df = read_file_to_df(uploaded)
943
+ return df.head(100)
944
+ except Exception as e:
945
+ return pd.DataFrame()
946
 
947
+ view_data_btn.click(fn=_view_data, inputs=[upload], outputs=[sample_table])
948
+
949
+
950
  # download raw
951
  def _download_raw(df_path):
952
  if not df_path: