Spaces:
Running on Zero
Running on Zero
Download app.py from zadid6pretam/BSTabDiff-Studio: direct link, hf CLI and curl.
- Browser
- Download file 23.5 kB
-
https://huggingface.co/spaces/zadid6pretam/BSTabDiff-Studio/resolve/main/app.py
- Command line
-
hf download hf://spaces/zadid6pretam/BSTabDiff-Studio/app.py
-
curl -L -o app.py https://huggingface.co/spaces/zadid6pretam/BSTabDiff-Studio/resolve/main/app.py
23.5 kB
| """BSTabDiff Studio: uploaded numeric CSV features + optional text/numeric label. | |
| Auto-detect common class targets, encode them for BSTabDiff and decode on export. | |
| Paper-default 10,000 epoch diffusion prior; dataset-aware latent block count M. | |
| No GO-BS or Optuna. Shared-hosting limits apply. | |
| """ | |
| from __future__ import annotations | |
| import html | |
| import os | |
| import tempfile | |
| import time | |
| from pathlib import Path | |
| # ZeroGPU requires this import before any library that might touch CUDA. | |
| import spaces | |
| import numpy as np | |
| import pandas as pd | |
| import plotly.graph_objects as go | |
| from plotly.subplots import make_subplots | |
| import gradio as gr | |
| import torch | |
| from bstabdiff import BSTabDiff | |
| from sklearn.decomposition import PCA | |
| from sklearn.impute import SimpleImputer | |
| from sklearn.preprocessing import StandardScaler, LabelEncoder | |
| MAX_FILE_MB = 50 | |
| MAX_ROWS = 10_000 | |
| MAX_FEATURES = 50_000 | |
| MAX_CELLS = 2_000_000 # n × m input budget | |
| MAX_SYNTHETIC = 3_000 | |
| MAX_OUTPUT_CELLS = 2_000_000 # generated rows × m output budget | |
| PAPER_EPOCHS = 10_000 # NeurIPS Appendix A16 default, not constructor default | |
| PRIOR_BATCH = 128 | |
| PRIOR_LR = 1e-3 | |
| EMA_DECAY = 0.999 | |
| NO_TARGET = "None (unconditional)" | |
| MAX_PREVIEW_COLUMNS = 16 | |
| MAX_PLOT_CHOICES = 150 | |
| CSS = """ | |
| .gradio-container {max-width: 1380px !important;} | |
| #studio-hero {padding: 28px 30px; margin: 3px 0 18px; border-radius: 24px; | |
| background: linear-gradient(116deg,#182b6e 0%,#6037aa 55%,#c2488d 100%); | |
| color:white; box-shadow: 0 12px 28px rgba(57,40,131,.17);} | |
| #studio-hero h1 {font-size: clamp(2rem,4vw,3.4rem); margin:0 0 6px; color:#fff !important; font-weight:800;} | |
| #studio-hero p {font-size: 1rem; margin: 0; opacity:.95; color:#fff !important;} | |
| #studio-hero .sub, #studio-hero .eyebrow {color:#fff !important;} | |
| #studio-hero .sub {margin-top:12px;font-size:.85rem;opacity:.95;} | |
| .studio-section {border-radius:18px !important;} | |
| #generate-btn {background: linear-gradient(90deg,#5236be,#cf4f96) !important; | |
| color:#fff !important; border:0 !important; font-weight:800 !important;} | |
| #generate-btn:hover {filter:brightness(1.08);} | |
| #studio-metrics {line-height:1.8;} | |
| #studio-metrics .metric-row {display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:8px;margin-top:10px;} | |
| #studio-metrics .metric {background:var(--block-background-fill);border:1px solid var(--block-border-color); | |
| padding:12px;border-radius:12px;text-align:center;} | |
| #studio-metrics .metric b{display:block;font-size:1.4rem;color:#6653c8;} | |
| #studio-metrics .metric span{font-size:.77rem;opacity:.8;} | |
| @media(max-width:700px){#studio-hero{padding:21px 17px;}#studio-metrics .metric-row{grid-template-columns:1fr 1fr;}} | |
| """ | |
| def read_csv(path: str) -> pd.DataFrame: | |
| if not path: | |
| raise gr.Error("Upload a CSV file first.") | |
| file = Path(path) | |
| if file.suffix.lower() != ".csv": | |
| raise gr.Error("Only .csv files are accepted.") | |
| if file.stat().st_size > MAX_FILE_MB * 1024**2: | |
| raise gr.Error(f"Please upload a CSV smaller than {MAX_FILE_MB} MB.") | |
| try: | |
| df = pd.read_csv(path, low_memory=False) | |
| except Exception as exc: | |
| raise gr.Error(f"Could not read CSV: {exc}") from exc | |
| if df.empty or len(df.columns) < 2: | |
| raise gr.Error("The CSV must contain rows and at least two columns.") | |
| if len(df) < 20: | |
| raise gr.Error("Use at least 20 rows. A single or tiny sample is not suitable for fitting a generator.") | |
| if len(df) > MAX_ROWS: | |
| raise gr.Error(f"For this public demo, upload at most {MAX_ROWS:,} rows.") | |
| if len(df.columns) - 1 > MAX_FEATURES: | |
| raise gr.Error(f"For this public demo, upload at most {MAX_FEATURES:,} feature columns.") | |
| if df.columns.duplicated().any(): | |
| raise gr.Error("Column names must be unique.") | |
| return df | |
| def numeric_columns(df): | |
| return [c for c in df.columns if pd.api.types.is_numeric_dtype(df[c])] | |
| def shape_card(df, target=NO_TARGET): | |
| n = len(df) | |
| m = len(df.columns) - (target != NO_TARGET and target in df.columns) | |
| rho = m / n | |
| kappa = n * m | |
| M = select_default_m(n, m) | |
| x = df.drop(columns=[target]) if target != NO_TARGET and target in df else df | |
| missing = x.isna().sum().sum() / max(x.size, 1) | |
| regime = "HDLSS (m > n)" if m > n else "n ≥ m" | |
| return f"""<div id='studio-metrics'><div class='metric-row'> | |
| <div class='metric'><b>{n:,}</b><span>Rows (n)</span></div> | |
| <div class='metric'><b>{m:,}</b><span>Features (m)</span></div> | |
| <div class='metric'><b>{rho:.2f}</b><span>Ratio ρ = m/n</span></div> | |
| <div class='metric'><b>{M:,}</b><span>Auto-selected M</span></div> | |
| </div><p><b>Data scale κ = nm:</b> {kappa:,} · <b>Missing feature cells:</b> {missing:.1%} | |
| · <b>Regime:</b> {regime}</p></div>""" | |
| def detect_target(df: pd.DataFrame) -> str: | |
| """Conservatively auto-select a *named* classification target. | |
| Do not infer a target solely because a column happens to contain strings: | |
| a categorical feature could otherwise be mistaken for the label. | |
| """ | |
| preferred = ("label", "target", "class", "class_label", "classlabel", "outcome", "y") | |
| for name in preferred: | |
| for col in df.columns: | |
| key = str(col).strip().lower().replace(" ", "_").replace("-", "_") | |
| if key != name: | |
| continue | |
| values = df[col] | |
| counts = values.value_counts(dropna=False) | |
| if 2 <= len(counts) <= 15 and not values.isna().any() and counts.min() >= 2: | |
| return str(col) | |
| return NO_TARGET | |
| def inspect_csv(path): | |
| if not path: | |
| return (gr.update(choices=[NO_TARGET], value=NO_TARGET), | |
| gr.update(choices=[], value=None), "Upload a dataset to get started.", pd.DataFrame()) | |
| df = read_csv(path) | |
| numeric = numeric_columns(df) | |
| inferred_target = detect_target(df) | |
| choices = [NO_TARGET] + [str(c) for c in df.columns] | |
| feature_choices = [str(c) for c in numeric if str(c) != inferred_target][:MAX_PLOT_CHOICES] | |
| summary = shape_card(df, inferred_target) | |
| nonnumeric_features = [str(c) for c in df.columns | |
| if str(c) != inferred_target and not pd.api.types.is_numeric_dtype(df[c])] | |
| if inferred_target != NO_TARGET: | |
| target_msg = ("<strong>Auto-selected class target:</strong> <code>" + | |
| html.escape(inferred_target) + | |
| "</code>. Change it in the dropdown if needed. ") | |
| else: | |
| target_msg = ("<strong>Unconditional generation selected.</strong> " | |
| "Choose a class target from the dropdown if you have one. ") | |
| if nonnumeric_features: | |
| target_msg += ("<strong>Other nonnumeric feature columns:</strong> " + | |
| html.escape(", ".join(nonnumeric_features[:6])) + | |
| ". Encode or remove these, or select the correct target column.") | |
| else: | |
| target_msg += "All remaining feature columns are numeric." | |
| summary += "<p>" + target_msg + "</p>" | |
| return (gr.update(choices=choices, value=inferred_target), | |
| gr.update(choices=feature_choices, | |
| value=feature_choices[0] if feature_choices else None), | |
| summary, df.iloc[:8, :MAX_PREVIEW_COLUMNS]) | |
| def update_target_summary(path, target): | |
| """Keep feature counts accurate when a visitor changes target selection.""" | |
| if not path: | |
| return "Upload a dataset to get started." | |
| df = read_csv(path) | |
| if target != NO_TARGET and target not in df.columns: | |
| return shape_card(df) | |
| nonnumeric = [str(col) for col in df.columns | |
| if str(col) != target and not pd.api.types.is_numeric_dtype(df[col])] | |
| msg = ("<strong>Class target:</strong> " + html.escape(target) + ". ") | |
| if nonnumeric: | |
| msg += "Encode or remove nonnumeric features: " + html.escape(", ".join(nonnumeric[:6])) | |
| else: | |
| msg += "All remaining feature columns are numeric." | |
| return shape_card(df, target) + "<p>" + msg + "</p>" | |
| def select_default_m(n, m): | |
| # Dataset-aware no-Optuna initialization from the BSTabDiff project README. | |
| if m <= 64: | |
| return m | |
| rho, kappa = m/n, n*m | |
| if rho < 10: | |
| return min(64, m) | |
| return min(32 if kappa < 250_000 else | |
| 64 if kappa < 1_000_000 else | |
| 128 if kappa < 3_000_000 else 192, m) | |
| def validate_features(df, target): | |
| if target != NO_TARGET and target not in df.columns: | |
| raise gr.Error("Select an existing target column or None.") | |
| feature_df = df.drop(columns=[target]) if target != NO_TARGET else df.copy() | |
| if feature_df.shape[1] < 2: | |
| raise gr.Error("At least two numerical feature columns are required.") | |
| invalid = [str(c) for c in feature_df.columns if not pd.api.types.is_numeric_dtype(feature_df[c])] | |
| if invalid: | |
| names = ", ".join(invalid[:4]) | |
| raise gr.Error(f"Feature columns must be numerical for this first demo. Encode or remove: {names}.") | |
| if feature_df.shape[1] > MAX_FEATURES: | |
| raise gr.Error(f"Please use at most {MAX_FEATURES:,} feature columns.") | |
| if len(df) * feature_df.shape[1] > MAX_CELLS: | |
| raise gr.Error(f"Shared-hardware limit: n × m must be at most {MAX_CELLS:,}. Try a smaller CSV.") | |
| X = feature_df.to_numpy(dtype=np.float32) | |
| if np.isinf(X).any(): | |
| raise gr.Error("Infinite values are not supported; replace ±inf with missing values first.") | |
| if not np.isfinite(X).any(): | |
| raise gr.Error("The CSV contains no finite numerical feature values.") | |
| all_missing = np.all(np.isnan(X), axis=0) | |
| if all_missing.any(): | |
| raise gr.Error(f"Remove entirely missing feature column: {feature_df.columns[np.flatnonzero(all_missing)[0]]}") | |
| y = None | |
| if target != NO_TARGET: | |
| y = df[target] | |
| if y.isna().any(): | |
| raise gr.Error("The target column must not contain missing class labels.") | |
| if y.nunique() < 2 or y.nunique() > 15: | |
| raise gr.Error("Select a classification target with 2–15 classes, or choose None.") | |
| if y.value_counts().min() < 2: | |
| raise gr.Error("Every class must have at least two rows for this demo.") | |
| return feature_df, X, y | |
| def chart_layout(fig, title): | |
| fig.update_layout(template="plotly_white", title=dict(text=title, x=.02), | |
| paper_bgcolor="rgba(0,0,0,0)", plot_bgcolor="rgba(0,0,0,0)", | |
| margin=dict(l=28,r=18,t=55,b=35), font=dict(size=12), | |
| legend=dict(orientation="h", y=-.2), height=390) | |
| return fig | |
| def distribution_plot(real, synth, column): | |
| if not column or column not in real.columns: | |
| return go.Figure() | |
| fig = go.Figure() | |
| for name, frame, color in [("Real", real, "#536ed7"), ("Synthetic", synth, "#f06f9c")]: | |
| values = pd.to_numeric(frame[column], errors="coerce").replace([np.inf,-np.inf],np.nan).dropna() | |
| fig.add_trace(go.Histogram(x=values, name=name, marker_color=color, | |
| opacity=.58, nbinsx=30, histnorm="probability density")) | |
| fig.update_layout(barmode="overlay", xaxis_title=str(column), yaxis_title="Density") | |
| return chart_layout(fig, f"Feature distribution · {column}") | |
| def pca_plot(real, synth): | |
| # A joint coordinate system fitted on real training data only. | |
| # Limit feature count to avoid a prohibitively large SVD for 50K features. | |
| cols = real.var(skipna=True).fillna(0).nlargest(min(200, real.shape[1])).index | |
| nr, ns = min(500, len(real)), min(500, len(synth)) | |
| R = real.loc[:, cols].head(nr).to_numpy(dtype=float) | |
| S = synth.loc[:, cols].head(ns).to_numpy(dtype=float) | |
| imp = SimpleImputer(strategy="median", keep_empty_features=True) | |
| R1 = imp.fit_transform(R) | |
| S1 = imp.transform(S) | |
| scale = StandardScaler() | |
| R2 = scale.fit_transform(R1) | |
| S2 = scale.transform(S1) | |
| pca = PCA(n_components=2, svd_solver="randomized", random_state=42) | |
| rp = pca.fit_transform(R2) | |
| sp = pca.transform(S2) | |
| fig = go.Figure() | |
| for name, pts, color in [("Real",rp,"#536ed7"),("Synthetic",sp,"#f06f9c")]: | |
| fig.add_trace(go.Scatter(x=pts[:,0],y=pts[:,1],mode="markers",name=name, | |
| marker=dict(size=7,color=color,opacity=.63))) | |
| fig.update_layout(xaxis_title="PC 1",yaxis_title="PC 2") | |
| return chart_layout(fig,"Shared PCA projection · training-data diagnostic") | |
| def correlation_plot(real, synth): | |
| variance = real.var(skipna=True).fillna(0).sort_values(ascending=False) | |
| cols = list(variance.head(min(18,len(variance))).index) | |
| if len(cols) < 2: | |
| return go.Figure() | |
| rc = real[cols].corr().fillna(0) | |
| sc = synth[cols].corr().fillna(0) | |
| fig = make_subplots(rows=1,cols=2, subplot_titles=("Real", "Synthetic"),horizontal_spacing=.12) | |
| for idx, mat in enumerate((rc,sc),start=1): | |
| fig.add_trace(go.Heatmap(z=mat.values,x=cols,y=cols,zmin=-1,zmax=1, | |
| colorscale="RdBu", reversescale=True, | |
| showscale=idx==2,colorbar=dict(title="r") if idx==2 else None), | |
| row=1,col=idx) | |
| fig.update_layout(height=490) | |
| fig.update_xaxes(tickangle=-45, tickfont=dict(size=9)) | |
| fig.update_yaxes(tickfont=dict(size=9)) | |
| chart_layout(fig,"Feature correlations · highest-variance columns") | |
| fig.update_layout(height=490,margin=dict(l=28,r=25,t=65,b=65)) | |
| return fig | |
| def generate(path, target, n_synth, seed, feature): | |
| df = read_csv(path) | |
| features, X, y = validate_features(df,target) | |
| n_synth = int(n_synth) | |
| seed = int(seed) | |
| if n_synth < 10 or n_synth > MAX_SYNTHETIC: | |
| raise gr.Error(f"Choose between 10 and {MAX_SYNTHETIC} synthetic rows.") | |
| if not 0 <= seed <= 2**31-1: | |
| raise gr.Error("Use a nonnegative random seed smaller than 2,147,483,648.") | |
| n, m = X.shape | |
| if n_synth * m > MAX_OUTPUT_CELLS: | |
| allowed = MAX_OUTPUT_CELLS // m | |
| raise gr.Error( | |
| f"Output would contain {n_synth * m:,} cells. " | |
| f"With {m:,} features, generate at most {allowed:,} rows " | |
| f"(maximum {MAX_OUTPUT_CELLS:,} output cells)." | |
| ) | |
| M = select_default_m(n, m) | |
| # Map arbitrary user class names (e.g. control/treated) to integer IDs | |
| # for the generator, then restore the original labels in the exported CSV. | |
| encoder = None | |
| y_fit = None | |
| if y is not None: | |
| encoder = LabelEncoder() | |
| y_fit = encoder.fit_transform(y.to_numpy()) | |
| started = time.perf_counter() | |
| torch.set_num_threads(min(2,os.cpu_count() or 2)) | |
| try: | |
| # Fixed paper-default diffusion settings, no Optuna, no ordering. | |
| model = BSTabDiff( | |
| M=M, | |
| prior_type="diffusion", | |
| device="cuda" if torch.cuda.is_available() else "cpu", | |
| random_state=seed, | |
| prior_epochs=PAPER_EPOCHS, | |
| prior_batch=PRIOR_BATCH, | |
| prior_lr=PRIOR_LR, | |
| use_ema=True, | |
| ema_decay=EMA_DECAY, | |
| verbose_every=0, | |
| save_dir=None, | |
| use_gobs=False, | |
| use_gobs_fc=False, | |
| ) | |
| model.fit(features, y_fit) | |
| X_syn,R_syn,y_syn = model.sample(n_samples=n_synth,return_mask=True) | |
| except Exception as exc: | |
| raise gr.Error(f"BSTabDiff training or sampling failed: {type(exc).__name__}: {exc}") from exc | |
| X_syn = np.asarray(X_syn,dtype=np.float32) | |
| if X_syn.shape != (n_synth,m): | |
| raise gr.Error(f"Unexpected generated shape {X_syn.shape}; expected {(n_synth,m)}.") | |
| if R_syn is not None: | |
| mask = np.asarray(R_syn) | |
| if mask.shape == X_syn.shape: | |
| X_syn = X_syn.copy() | |
| X_syn[mask == 0] = np.nan | |
| syn = pd.DataFrame(X_syn,columns=features.columns) | |
| if y is not None: | |
| if y_syn is None: | |
| raise gr.Error("The generator did not return labels for the selected class target.") | |
| sampled_codes = np.asarray(y_syn).reshape(-1) | |
| if len(sampled_codes) != n_synth: | |
| raise gr.Error("Generated class label count does not match synthetic rows.") | |
| try: | |
| sampled_codes = sampled_codes.astype(int) | |
| except (TypeError, ValueError, OverflowError) as exc: | |
| raise gr.Error("Generated class labels could not be decoded.") from exc | |
| if np.any(sampled_codes < 0) or np.any(sampled_codes >= len(encoder.classes_)): | |
| raise gr.Error("Generator returned an unknown class ID.") | |
| syn[target] = encoder.inverse_transform(sampled_codes) | |
| real = features.reset_index(drop=True) | |
| syn_features = syn[features.columns] | |
| plot_columns = list(features.columns[:MAX_PLOT_CHOICES]) | |
| column = feature if feature in plot_columns else plot_columns[0] | |
| path_csv = os.path.join(tempfile.mkdtemp(prefix="bstabdiff_studio_"),"bstabdiff_synthetic.csv") | |
| syn.to_csv(path_csv,index=False) | |
| seconds = time.perf_counter()-started | |
| summary = (f"### ✅ Generated **{n_synth:,}** synthetic samples\n" | |
| f"**Input:** {n:,} rows × {m:,} features · " | |
| f"**ρ=m/n:** {m/n:.3f} · **κ=nm:** {n*m:,} · " | |
| f"**Auto-selected M:** {M}\n\n" | |
| f"**Diffusion epochs:** {PAPER_EPOCHS:,} · " | |
| f"**Batch:** {PRIOR_BATCH} · **LR:** {PRIOR_LR:g} · " | |
| f"**EMA decay:** {EMA_DECAY} · **Seed:** {seed} · " | |
| f"**Device:** {model.device_} · **Elapsed:** {seconds:.1f}s\n\n" | |
| "*Paper-default configuration with dataset-aware M, no Optuna or GO-BS. " | |
| "Plots compare against uploaded **training data**; this is not " | |
| "a held-out utility or privacy evaluation.*") | |
| # The input frame may contain a class target; output a privacy-minded 10-row preview only. | |
| state=(real,syn_features) | |
| return (summary,syn.head(10),distribution_plot(*state,column), | |
| pca_plot(*state),correlation_plot(*state),path_csv,state, | |
| gr.update(choices=plot_columns,value=column)) | |
| def update_plot(feature, state): | |
| if not state or not feature: | |
| return go.Figure() | |
| real, synthetic = state | |
| return distribution_plot(real,synthetic,feature) | |
| with gr.Blocks(title="BSTabDiff Studio — NeurIPS 2026", | |
| delete_cache=(3600,3600)) as demo: | |
| gr.HTML(""" | |
| <div id="studio-hero"> | |
| <div class="eyebrow" style="color:#fff;font-weight:700;letter-spacing:.13em;font-size:.74rem;opacity:.93">NEURIPS 2026 · RESEARCH DEMO</div> | |
| <h1>✦ BSTabDiff Studio</h1> | |
| <p>From your CSV to high-dimensional synthetic data.</p> | |
| <div class="sub" style="color:#fff">Upload → Generate → Visualize → Download · Default diffusion, no hyperparameter tuning.</div> | |
| </div> | |
| """) | |
| gr.Markdown("**How it works:** Supply a CSV containing numeric features and optionally a discrete class label. " | |
| "This Space trains BSTabDiff on your uploaded table and generates new synthetic rows. " | |
| "**Please do not upload confidential, patient-level, or personally identifying data.**") | |
| with gr.Row(equal_height=False): | |
| with gr.Column(scale=5,elem_classes=["studio-section"]): | |
| gr.Markdown("### ① Upload & configure") | |
| file_input=gr.File(label="Upload CSV",file_types=[".csv"],type="filepath") | |
| gr.Markdown(f"*Limits: {MAX_FILE_MB} MB, {MAX_ROWS:,} rows, " | |
| f"{MAX_FEATURES:,} features, {MAX_CELLS:,} input cells; " | |
| f"{MAX_OUTPUT_CELLS:,} synthetic output cells. " | |
| f"Feature explorer lists the first {MAX_PLOT_CHOICES} numeric columns.*") | |
| target=gr.Dropdown(label="Optional class target",choices=[NO_TARGET],value=NO_TARGET, | |
| info="Common column names like label/target/class are automatically selected. Text labels are supported; remaining features must be numeric.") | |
| with gr.Row(): | |
| samples=gr.Slider(label="Synthetic rows",minimum=10,maximum=MAX_SYNTHETIC, | |
| step=10,value=100) | |
| seed_input=gr.Number(label="Random seed",value=42,precision=0,minimum=0,maximum=9999) | |
| gr.Markdown(f"**Paper-default generator:** {PAPER_EPOCHS:,} diffusion epochs, " | |
| f"batch {PRIOR_BATCH}, LR {PRIOR_LR:g}, EMA {EMA_DECAY}; " | |
| "M is chosen automatically from n, m, ρ and κ. " | |
| "No GO-BS or Optuna. Large datasets may exceed ZeroGPU runtime limits.") | |
| run_btn=gr.Button("✦ Generate synthetic dataset",variant="primary",elem_id="generate-btn",size="lg") | |
| with gr.Column(scale=6,elem_classes=["studio-section"]): | |
| gr.Markdown("### ② Dataset snapshot") | |
| file_info=gr.HTML("<p>Upload a CSV to inspect its structure.</p>") | |
| preview=gr.Dataframe(label=f"Original data · first 8 rows / {MAX_PREVIEW_COLUMNS} columns",interactive=False, | |
| max_height=310) | |
| gr.Markdown("## ③ Explore your generated data") | |
| result=gr.Markdown("The synthetic-data dashboard will appear here after generation.") | |
| state=gr.State(value=None) | |
| with gr.Row(): | |
| real_plot_feature=gr.Dropdown(label="Feature to inspect",choices=[],interactive=True) | |
| download=gr.File(label="Download synthetic CSV",interactive=False) | |
| with gr.Row(): | |
| with gr.Column(): | |
| with gr.Tab("📊 Feature distribution"): | |
| hist=gr.Plot(label="Real vs synthetic feature distribution") | |
| with gr.Tab("🧭 PCA projection"): | |
| pca=gr.Plot(label="Shared PCA projection") | |
| with gr.Column(): | |
| with gr.Tab("🧩 Correlations"): | |
| corr=gr.Plot(label="Real and synthetic correlation matrices") | |
| with gr.Tab("🧬 Synthetic preview"): | |
| generated_table=gr.Dataframe(label="Synthetic data · first 10 rows",interactive=False) | |
| gr.HTML("""<div style="text-align:center;padding:16px;font-size:12px;opacity:.75"> | |
| <a href="https://www.zadidhabib.com/bstabdiff.html" target="_blank">Project webpage</a> · | |
| <a href="https://github.com/zadid6pretam/BSTabDiff" target="_blank">GitHub</a> · | |
| <a href="https://pypi.org/project/bstabdiff/" target="_blank">PyPI</a> · | |
| <a href="https://neurips.cc/virtual/2026/loc/atlanta/poster/151140" target="_blank" rel="noopener noreferrer">NeurIPS Poster</a> · | |
| <a href="https://openreview.net/forum?id=ciPCMXX1aj" target="_blank">NeurIPS 2026 Paper</a> | |
| <p>Research demonstration only. Synthetic data are not necessarily private or faithful to the source distribution.</p> | |
| </div>""") | |
| file_input.change(inspect_csv,inputs=file_input, | |
| outputs=[target,real_plot_feature,file_info,preview],show_progress="minimal") | |
| target.change(update_target_summary, inputs=[file_input, target], outputs=file_info, | |
| show_progress="hidden") | |
| run_btn.click(generate,inputs=[file_input,target,samples,seed_input,real_plot_feature], | |
| outputs=[result,generated_table,hist,pca,corr,download,state,real_plot_feature], | |
| concurrency_limit=1,show_progress="full") | |
| real_plot_feature.change(update_plot,inputs=[real_plot_feature,state],outputs=hist, | |
| show_progress="minimal") | |
| demo.queue(max_size=6) | |
| if __name__ == "__main__": | |
| demo.launch(theme=gr.themes.Soft(primary_hue="indigo",secondary_hue="pink"), css=CSS) |