"""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"""
{n:,}Rows (n)
{m:,}Features (m)
{rho:.2f}Ratio ρ = m/n
{M:,}Auto-selected M

Data scale κ = nm: {kappa:,} · Missing feature cells: {missing:.1%} · Regime: {regime}

""" 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 = ("Auto-selected class target: " + html.escape(inferred_target) + ". Change it in the dropdown if needed. ") else: target_msg = ("Unconditional generation selected. " "Choose a class target from the dropdown if you have one. ") if nonnumeric_features: target_msg += ("Other nonnumeric feature columns: " + 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 += "

" + target_msg + "

" 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 = ("Class target: " + 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) + "

" + msg + "

" 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 @spaces.GPU(duration=180) 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("""
NEURIPS 2026 · RESEARCH DEMO

✦ BSTabDiff Studio

From your CSV to high-dimensional synthetic data.

Upload → Generate → Visualize → Download · Default diffusion, no hyperparameter tuning.
""") 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("

Upload a CSV to inspect its structure.

") 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("""
Project webpage · GitHub · PyPI · NeurIPS Poster · NeurIPS 2026 Paper

Research demonstration only. Synthetic data are not necessarily private or faithful to the source distribution.

""") 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)