Spaces:
Running on Zero
Running on Zero
Upload 3 files
Browse files- README.md +28 -8
- app.py +366 -0
- requirements.txt +2 -0
README.md
CHANGED
|
@@ -1,15 +1,35 @@
|
|
| 1 |
---
|
| 2 |
title: BSTabDiff Studio
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: gradio
|
| 7 |
-
sdk_version: 6.
|
| 8 |
-
python_version:
|
| 9 |
app_file: app.py
|
| 10 |
-
pinned: false
|
| 11 |
license: mit
|
| 12 |
-
short_description: Generate
|
|
|
|
| 13 |
---
|
| 14 |
|
| 15 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
title: BSTabDiff Studio
|
| 3 |
+
emoji: 🧬
|
| 4 |
+
colorFrom: purple
|
| 5 |
+
colorTo: pink
|
| 6 |
sdk: gradio
|
| 7 |
+
sdk_version: 6.5.1
|
| 8 |
+
python_version: "3.10.13"
|
| 9 |
app_file: app.py
|
|
|
|
| 10 |
license: mit
|
| 11 |
+
short_description: Generate high-dimensional synthetic tabular data from CSV.
|
| 12 |
+
pinned: false
|
| 13 |
---
|
| 14 |
|
| 15 |
+
# BSTabDiff Studio · NeurIPS 2026
|
| 16 |
+
|
| 17 |
+
An interactive demo for generating synthetic tabular datasets from user-uploaded CSV files using the [BSTabDiff package](https://pypi.org/project/bstabdiff/).
|
| 18 |
+
|
| 19 |
+
1. Upload a CSV with numeric feature columns (optional discrete class target).
|
| 20 |
+
2. Choose a synthetic sample count and random seed.
|
| 21 |
+
3. Generate a synthetic dataset using BSTabDiff diffusion with no hyperparameter tuning or GO-BS.
|
| 22 |
+
4. Compare feature distributions, PCA projections, and feature correlations.
|
| 23 |
+
5. Download the generated CSV.
|
| 24 |
+
|
| 25 |
+
**Demo configuration:** The initial Space uses a fixed **200-epoch** training budget to limit resource usage. This is intentionally shorter than the estimator's standard 1,500-epoch default and is **not a replication of the NeurIPS paper evaluation protocol**.
|
| 26 |
+
|
| 27 |
+
**Privacy:** Uploaded datasets are processed on shared hosting infrastructure. Do not upload private, confidential, medical, identifying, or restricted datasets. Synthetic data does not guarantee privacy or non-memorization.
|
| 28 |
+
|
| 29 |
+
**Limits:** At least 20 rows; up to 1,200 rows, 4,096 features, and 220,000 numeric input cells; max 8 MB CSV; up to 500 synthetic rows. Missing values are supported; nonnumeric features other than the optional class target are not supported by this first version.
|
| 30 |
+
|
| 31 |
+
- [Project page](https://www.zadidhabib.com/bstabdiff.html)
|
| 32 |
+
- [Source code](https://github.com/zadid6pretam/BSTabDiff)
|
| 33 |
+
- [NeurIPS 2026 paper](https://openreview.net/forum?id=ciPCMXX1aj)
|
| 34 |
+
|
| 35 |
+
MIT license.
|
app.py
ADDED
|
@@ -0,0 +1,366 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""BSTabDiff Studio: user-uploaded CSV synthetic-data explorer.
|
| 2 |
+
|
| 3 |
+
Public demo constraints: numeric feature columns, optional categorical target,
|
| 4 |
+
fast fixed-budget default/no-tuning BSTabDiff with diffusion and no GO-BS.
|
| 5 |
+
"""
|
| 6 |
+
from __future__ import annotations
|
| 7 |
+
|
| 8 |
+
import html
|
| 9 |
+
import os
|
| 10 |
+
import tempfile
|
| 11 |
+
import time
|
| 12 |
+
from pathlib import Path
|
| 13 |
+
|
| 14 |
+
# ZeroGPU requires this import before any library that might touch CUDA.
|
| 15 |
+
import spaces
|
| 16 |
+
|
| 17 |
+
import numpy as np
|
| 18 |
+
import pandas as pd
|
| 19 |
+
import plotly.graph_objects as go
|
| 20 |
+
from plotly.subplots import make_subplots
|
| 21 |
+
import gradio as gr
|
| 22 |
+
import torch
|
| 23 |
+
from bstabdiff import BSTabDiff
|
| 24 |
+
from sklearn.decomposition import PCA
|
| 25 |
+
from sklearn.impute import SimpleImputer
|
| 26 |
+
from sklearn.preprocessing import StandardScaler
|
| 27 |
+
|
| 28 |
+
MAX_FILE_MB = 8
|
| 29 |
+
MAX_ROWS = 1200
|
| 30 |
+
MAX_FEATURES = 4096
|
| 31 |
+
MAX_CELLS = 220_000
|
| 32 |
+
MAX_SYNTHETIC = 500
|
| 33 |
+
DEMO_EPOCHS = 200 # An explicit fast-demo override; estimator default is 1500.
|
| 34 |
+
NO_TARGET = "None (unconditional)"
|
| 35 |
+
|
| 36 |
+
CSS = """
|
| 37 |
+
.gradio-container {max-width: 1380px !important;}
|
| 38 |
+
#studio-hero {padding: 28px 30px; margin: 3px 0 18px; border-radius: 24px;
|
| 39 |
+
background: linear-gradient(116deg,#182b6e 0%,#6037aa 55%,#c2488d 100%);
|
| 40 |
+
color:white; box-shadow: 0 12px 28px rgba(57,40,131,.17);}
|
| 41 |
+
#studio-hero h1 {font-size: clamp(2rem,4vw,3.4rem); margin:0 0 6px; color:white !important; font-weight:800;}
|
| 42 |
+
#studio-hero p {font-size: 1rem; margin: 0; opacity:.95; color:white !important;}
|
| 43 |
+
#studio-hero .sub {margin-top:12px;font-size:.85rem;opacity:.87;}
|
| 44 |
+
.studio-section {border-radius:18px !important;}
|
| 45 |
+
#generate-btn {background: linear-gradient(90deg,#5236be,#cf4f96) !important;
|
| 46 |
+
color:#fff !important; border:0 !important; font-weight:800 !important;}
|
| 47 |
+
#generate-btn:hover {filter:brightness(1.08);}
|
| 48 |
+
#studio-metrics {line-height:1.8;}
|
| 49 |
+
#studio-metrics .metric-row {display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:8px;margin-top:10px;}
|
| 50 |
+
#studio-metrics .metric {background:var(--block-background-fill);border:1px solid var(--block-border-color);
|
| 51 |
+
padding:12px;border-radius:12px;text-align:center;}
|
| 52 |
+
#studio-metrics .metric b{display:block;font-size:1.4rem;color:#6653c8;}
|
| 53 |
+
#studio-metrics .metric span{font-size:.77rem;opacity:.8;}
|
| 54 |
+
@media(max-width:700px){#studio-hero{padding:21px 17px;}#studio-metrics .metric-row{grid-template-columns:1fr 1fr;}}
|
| 55 |
+
"""
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def read_csv(path: str) -> pd.DataFrame:
|
| 59 |
+
if not path:
|
| 60 |
+
raise gr.Error("Upload a CSV file first.")
|
| 61 |
+
file = Path(path)
|
| 62 |
+
if file.suffix.lower() != ".csv":
|
| 63 |
+
raise gr.Error("Only .csv files are accepted.")
|
| 64 |
+
if file.stat().st_size > MAX_FILE_MB * 1024**2:
|
| 65 |
+
raise gr.Error(f"Please upload a CSV smaller than {MAX_FILE_MB} MB.")
|
| 66 |
+
try:
|
| 67 |
+
df = pd.read_csv(path, low_memory=False)
|
| 68 |
+
except Exception as exc:
|
| 69 |
+
raise gr.Error(f"Could not read CSV: {exc}") from exc
|
| 70 |
+
if df.empty or len(df.columns) < 2:
|
| 71 |
+
raise gr.Error("The CSV must contain rows and at least two columns.")
|
| 72 |
+
if len(df) < 20:
|
| 73 |
+
raise gr.Error("Use at least 20 rows. A single or tiny sample is not suitable for fitting a generator.")
|
| 74 |
+
if len(df) > MAX_ROWS:
|
| 75 |
+
raise gr.Error(f"For this public demo, upload at most {MAX_ROWS:,} rows.")
|
| 76 |
+
if len(df.columns) - 1 > MAX_FEATURES:
|
| 77 |
+
raise gr.Error(f"For this public demo, upload at most {MAX_FEATURES:,} feature columns.")
|
| 78 |
+
if df.columns.duplicated().any():
|
| 79 |
+
raise gr.Error("Column names must be unique.")
|
| 80 |
+
return df
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
def numeric_columns(df):
|
| 84 |
+
return [c for c in df.columns if pd.api.types.is_numeric_dtype(df[c])]
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
def shape_card(df, target=NO_TARGET):
|
| 88 |
+
m = len(df.columns) - (target != NO_TARGET and target in df.columns)
|
| 89 |
+
n = len(df)
|
| 90 |
+
ratio = m / max(n, 1)
|
| 91 |
+
miss = df.isna().sum().sum() / max(df.size, 1)
|
| 92 |
+
regime = "HDLSS (m > n)" if m > n else "n ≥ m"
|
| 93 |
+
return f"""<div id='studio-metrics'><div class='metric-row'>
|
| 94 |
+
<div class='metric'><b>{n:,}</b><span>Rows (n)</span></div>
|
| 95 |
+
<div class='metric'><b>{m:,}</b><span>Features (m)</span></div>
|
| 96 |
+
<div class='metric'><b>{ratio:.2f}</b><span>m / n</span></div>
|
| 97 |
+
<div class='metric'><b>{miss:.1%}</b><span>Missing cells</span></div>
|
| 98 |
+
</div><p>Regime: <b>{regime}</b> · CSV remains user supplied.</p></div>"""
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
def inspect_csv(path):
|
| 102 |
+
if not path:
|
| 103 |
+
return (gr.update(choices=[NO_TARGET], value=NO_TARGET),
|
| 104 |
+
gr.update(choices=[], value=None), "Upload a dataset to get started.", pd.DataFrame())
|
| 105 |
+
df = read_csv(path)
|
| 106 |
+
numeric = numeric_columns(df)
|
| 107 |
+
target_candidates = [NO_TARGET] + [str(c) for c in df.columns]
|
| 108 |
+
summary = shape_card(df)
|
| 109 |
+
extra = f"<p><b>Detected:</b> {len(numeric)} numeric columns and {len(df.columns)-len(numeric)} nonnumeric columns. "
|
| 110 |
+
if len(numeric) < 2:
|
| 111 |
+
extra += "Choose a target or upload at least two numeric features."
|
| 112 |
+
else:
|
| 113 |
+
extra += "Choose an optional class target; nonnumeric features must be removed or encoded."
|
| 114 |
+
extra += "</p>"
|
| 115 |
+
return (gr.update(choices=target_candidates, value=NO_TARGET),
|
| 116 |
+
gr.update(choices=numeric, value=numeric[0] if numeric else None),
|
| 117 |
+
summary + extra, df.head(8))
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
def select_default_m(n, m):
|
| 121 |
+
# Dataset-aware no-Optuna initialization from the BSTabDiff project README.
|
| 122 |
+
if m <= 64:
|
| 123 |
+
return m
|
| 124 |
+
rho, kappa = m/n, n*m
|
| 125 |
+
if rho < 10:
|
| 126 |
+
return min(64, m)
|
| 127 |
+
return min(32 if kappa < 250_000 else
|
| 128 |
+
64 if kappa < 1_000_000 else
|
| 129 |
+
128 if kappa < 3_000_000 else 192, m)
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
def validate_features(df, target):
|
| 133 |
+
if target != NO_TARGET and target not in df.columns:
|
| 134 |
+
raise gr.Error("Select an existing target column or None.")
|
| 135 |
+
feature_df = df.drop(columns=[target]) if target != NO_TARGET else df.copy()
|
| 136 |
+
if feature_df.shape[1] < 2:
|
| 137 |
+
raise gr.Error("At least two numerical feature columns are required.")
|
| 138 |
+
invalid = [str(c) for c in feature_df.columns if not pd.api.types.is_numeric_dtype(feature_df[c])]
|
| 139 |
+
if invalid:
|
| 140 |
+
names = ", ".join(invalid[:4])
|
| 141 |
+
raise gr.Error(f"Feature columns must be numerical for this first demo. Encode or remove: {names}.")
|
| 142 |
+
if feature_df.shape[1] > MAX_FEATURES:
|
| 143 |
+
raise gr.Error(f"Please use at most {MAX_FEATURES:,} feature columns.")
|
| 144 |
+
if len(df) * feature_df.shape[1] > MAX_CELLS:
|
| 145 |
+
raise gr.Error(f"Shared-hardware limit: n × m must be at most {MAX_CELLS:,}. Try a smaller CSV.")
|
| 146 |
+
X = feature_df.to_numpy(dtype=np.float32)
|
| 147 |
+
if np.isinf(X).any():
|
| 148 |
+
raise gr.Error("Infinite values are not supported; replace ±inf with missing values first.")
|
| 149 |
+
if not np.isfinite(X).any():
|
| 150 |
+
raise gr.Error("The CSV contains no finite numerical feature values.")
|
| 151 |
+
all_missing = np.all(np.isnan(X), axis=0)
|
| 152 |
+
if all_missing.any():
|
| 153 |
+
raise gr.Error(f"Remove entirely missing feature column: {feature_df.columns[np.flatnonzero(all_missing)[0]]}")
|
| 154 |
+
y = None
|
| 155 |
+
if target != NO_TARGET:
|
| 156 |
+
y = df[target]
|
| 157 |
+
if y.isna().any():
|
| 158 |
+
raise gr.Error("The target column must not contain missing class labels.")
|
| 159 |
+
if y.nunique() < 2 or y.nunique() > 15:
|
| 160 |
+
raise gr.Error("Select a classification target with 2–15 classes, or choose None.")
|
| 161 |
+
if y.value_counts().min() < 2:
|
| 162 |
+
raise gr.Error("Every class must have at least two rows for this demo.")
|
| 163 |
+
return feature_df, X, y
|
| 164 |
+
|
| 165 |
+
|
| 166 |
+
def chart_layout(fig, title):
|
| 167 |
+
fig.update_layout(template="plotly_white", title=dict(text=title, x=.02),
|
| 168 |
+
paper_bgcolor="rgba(0,0,0,0)", plot_bgcolor="rgba(0,0,0,0)",
|
| 169 |
+
margin=dict(l=28,r=18,t=55,b=35), font=dict(size=12),
|
| 170 |
+
legend=dict(orientation="h", y=-.2), height=390)
|
| 171 |
+
return fig
|
| 172 |
+
|
| 173 |
+
|
| 174 |
+
def distribution_plot(real, synth, column):
|
| 175 |
+
if not column or column not in real.columns:
|
| 176 |
+
return go.Figure()
|
| 177 |
+
fig = go.Figure()
|
| 178 |
+
for name, frame, color in [("Real", real, "#536ed7"), ("Synthetic", synth, "#f06f9c")]:
|
| 179 |
+
values = pd.to_numeric(frame[column], errors="coerce").replace([np.inf,-np.inf],np.nan).dropna()
|
| 180 |
+
fig.add_trace(go.Histogram(x=values, name=name, marker_color=color,
|
| 181 |
+
opacity=.58, nbinsx=30, histnorm="probability density"))
|
| 182 |
+
fig.update_layout(barmode="overlay", xaxis_title=str(column), yaxis_title="Density")
|
| 183 |
+
return chart_layout(fig, f"Feature distribution · {column}")
|
| 184 |
+
|
| 185 |
+
|
| 186 |
+
def pca_plot(real, synth):
|
| 187 |
+
cols = real.columns
|
| 188 |
+
nr, ns = min(500,len(real)), min(500,len(synth))
|
| 189 |
+
R = real[cols].head(nr).to_numpy(dtype=float)
|
| 190 |
+
S = synth[cols].head(ns).to_numpy(dtype=float)
|
| 191 |
+
imp = SimpleImputer(strategy="median", keep_empty_features=True)
|
| 192 |
+
R1 = imp.fit_transform(R)
|
| 193 |
+
S1 = imp.transform(S)
|
| 194 |
+
scale = StandardScaler()
|
| 195 |
+
R2 = scale.fit_transform(R1)
|
| 196 |
+
S2 = scale.transform(S1)
|
| 197 |
+
pca = PCA(n_components=2, random_state=42)
|
| 198 |
+
rp = pca.fit_transform(R2)
|
| 199 |
+
sp = pca.transform(S2)
|
| 200 |
+
fig = go.Figure()
|
| 201 |
+
for name, pts, color in [("Real",rp,"#536ed7"),("Synthetic",sp,"#f06f9c")]:
|
| 202 |
+
fig.add_trace(go.Scatter(x=pts[:,0],y=pts[:,1],mode="markers",name=name,
|
| 203 |
+
marker=dict(size=7,color=color,opacity=.63)))
|
| 204 |
+
fig.update_layout(xaxis_title="PC 1",yaxis_title="PC 2")
|
| 205 |
+
return chart_layout(fig,"Shared PCA projection · training-data diagnostic")
|
| 206 |
+
|
| 207 |
+
|
| 208 |
+
def correlation_plot(real, synth):
|
| 209 |
+
variance = real.var(skipna=True).fillna(0).sort_values(ascending=False)
|
| 210 |
+
cols = list(variance.head(min(18,len(variance))).index)
|
| 211 |
+
if len(cols) < 2:
|
| 212 |
+
return go.Figure()
|
| 213 |
+
rc = real[cols].corr().fillna(0)
|
| 214 |
+
sc = synth[cols].corr().fillna(0)
|
| 215 |
+
fig = make_subplots(rows=1,cols=2, subplot_titles=("Real", "Synthetic"),horizontal_spacing=.12)
|
| 216 |
+
for idx, mat in enumerate((rc,sc),start=1):
|
| 217 |
+
fig.add_trace(go.Heatmap(z=mat.values,x=cols,y=cols,zmin=-1,zmax=1,
|
| 218 |
+
colorscale="RdBu", reversescale=True,
|
| 219 |
+
showscale=idx==2,colorbar=dict(title="r") if idx==2 else None),
|
| 220 |
+
row=1,col=idx)
|
| 221 |
+
fig.update_layout(height=490)
|
| 222 |
+
fig.update_xaxes(tickangle=-45, tickfont=dict(size=9))
|
| 223 |
+
fig.update_yaxes(tickfont=dict(size=9))
|
| 224 |
+
chart_layout(fig,"Feature correlations · highest-variance columns")
|
| 225 |
+
fig.update_layout(height=490,margin=dict(l=28,r=25,t=65,b=65))
|
| 226 |
+
return fig
|
| 227 |
+
|
| 228 |
+
|
| 229 |
+
@spaces.GPU(duration=180)
|
| 230 |
+
def generate(path, target, n_synth, seed, feature):
|
| 231 |
+
df = read_csv(path)
|
| 232 |
+
features, X, y = validate_features(df,target)
|
| 233 |
+
n_synth = int(n_synth)
|
| 234 |
+
seed = int(seed)
|
| 235 |
+
if n_synth < 10 or n_synth > MAX_SYNTHETIC:
|
| 236 |
+
raise gr.Error(f"Choose between 10 and {MAX_SYNTHETIC} synthetic rows.")
|
| 237 |
+
if not 0 <= seed <= 2**31-1:
|
| 238 |
+
raise gr.Error("Use a nonnegative random seed smaller than 2,147,483,648.")
|
| 239 |
+
n, m = X.shape
|
| 240 |
+
M = select_default_m(n,m)
|
| 241 |
+
started = time.perf_counter()
|
| 242 |
+
torch.set_num_threads(min(2,os.cpu_count() or 2))
|
| 243 |
+
try:
|
| 244 |
+
# Default/no-tuning diffusion method, with fixed reduced epochs for demo speed.
|
| 245 |
+
model = BSTabDiff(
|
| 246 |
+
M=M,
|
| 247 |
+
prior_type="diffusion",
|
| 248 |
+
device="cuda" if torch.cuda.is_available() else "cpu",
|
| 249 |
+
random_state=seed,
|
| 250 |
+
prior_epochs=DEMO_EPOCHS,
|
| 251 |
+
verbose_every=0,
|
| 252 |
+
use_gobs=False,
|
| 253 |
+
use_gobs_fc=False,
|
| 254 |
+
)
|
| 255 |
+
model.fit(features, None if y is None else y.to_numpy())
|
| 256 |
+
X_syn,R_syn,y_syn = model.sample(n_samples=n_synth,return_mask=True)
|
| 257 |
+
except Exception as exc:
|
| 258 |
+
raise gr.Error(f"BSTabDiff training or sampling failed: {type(exc).__name__}: {exc}") from exc
|
| 259 |
+
X_syn = np.asarray(X_syn,dtype=np.float32)
|
| 260 |
+
if X_syn.shape != (n_synth,m):
|
| 261 |
+
raise gr.Error(f"Unexpected generated shape {X_syn.shape}; expected {(n_synth,m)}.")
|
| 262 |
+
if R_syn is not None:
|
| 263 |
+
mask = np.asarray(R_syn)
|
| 264 |
+
if mask.shape == X_syn.shape:
|
| 265 |
+
X_syn = X_syn.copy()
|
| 266 |
+
X_syn[mask == 0] = np.nan
|
| 267 |
+
syn = pd.DataFrame(X_syn,columns=features.columns)
|
| 268 |
+
if y is not None and y_syn is not None:
|
| 269 |
+
syn[target] = np.asarray(y_syn).reshape(-1)
|
| 270 |
+
real = features.reset_index(drop=True)
|
| 271 |
+
syn_features = syn[features.columns]
|
| 272 |
+
column = feature if feature in features.columns else features.columns[0]
|
| 273 |
+
path_csv = os.path.join(tempfile.mkdtemp(prefix="bstabdiff_studio_"),"bstabdiff_synthetic.csv")
|
| 274 |
+
syn.to_csv(path_csv,index=False)
|
| 275 |
+
seconds = time.perf_counter()-started
|
| 276 |
+
summary = (f"### ✅ Generated **{n_synth:,}** synthetic samples\n"
|
| 277 |
+
f"**Input:** {n:,} rows × {m:,} features · **Latent blocks:** {M} · "
|
| 278 |
+
f"**Diffusion epochs:** {DEMO_EPOCHS} · **Seed:** {seed} · "
|
| 279 |
+
f"**Device:** {model.device_} · **Elapsed:** {seconds:.1f}s\n\n"
|
| 280 |
+
"*Fast demo settings: fixed 200-epoch training budget, no tuning or GO-BS. "
|
| 281 |
+
"Plots compare against the uploaded training data, **not** a held-out test set.*")
|
| 282 |
+
# The input frame may contain a class target; output a privacy-minded 10-row preview only.
|
| 283 |
+
state=(real,syn_features)
|
| 284 |
+
return (summary,syn.head(10),distribution_plot(*state,column),
|
| 285 |
+
pca_plot(*state),correlation_plot(*state),path_csv,state,
|
| 286 |
+
gr.update(choices=list(features.columns),value=column))
|
| 287 |
+
|
| 288 |
+
|
| 289 |
+
def update_plot(feature, state):
|
| 290 |
+
if not state or not feature:
|
| 291 |
+
return go.Figure()
|
| 292 |
+
real, synthetic = state
|
| 293 |
+
return distribution_plot(real,synthetic,feature)
|
| 294 |
+
|
| 295 |
+
|
| 296 |
+
with gr.Blocks(title="BSTabDiff Studio — NeurIPS 2026",
|
| 297 |
+
delete_cache=(3600,3600)) as demo:
|
| 298 |
+
gr.HTML("""
|
| 299 |
+
<div id="studio-hero">
|
| 300 |
+
<div style="font-weight:700;letter-spacing:.13em;font-size:.74rem;opacity:.88">NEURIPS 2026 · RESEARCH DEMO</div>
|
| 301 |
+
<h1>✦ BSTabDiff Studio</h1>
|
| 302 |
+
<p>From your CSV to high-dimensional synthetic data.</p>
|
| 303 |
+
<div class="sub">Upload → Generate → Visualize → Download · Default diffusion, no hyperparameter tuning.</div>
|
| 304 |
+
</div>
|
| 305 |
+
""")
|
| 306 |
+
gr.Markdown("**How it works:** Supply a CSV containing numeric features and optionally a discrete class label. "
|
| 307 |
+
"This Space trains BSTabDiff on your uploaded table and generates new synthetic rows. "
|
| 308 |
+
"**Please do not upload confidential, patient-level, or personally identifying data.**")
|
| 309 |
+
|
| 310 |
+
with gr.Row(equal_height=False):
|
| 311 |
+
with gr.Column(scale=5,elem_classes=["studio-section"]):
|
| 312 |
+
gr.Markdown("### ① Upload & configure")
|
| 313 |
+
file_input=gr.File(label="Upload CSV",file_types=[".csv"],type="filepath")
|
| 314 |
+
target=gr.Dropdown(label="Optional class target",choices=[NO_TARGET],value=NO_TARGET,
|
| 315 |
+
info="Use None for unconditional generation. Other nonnumeric columns are unsupported.")
|
| 316 |
+
with gr.Row():
|
| 317 |
+
samples=gr.Slider(label="Synthetic rows",minimum=10,maximum=MAX_SYNTHETIC,
|
| 318 |
+
step=10,value=100)
|
| 319 |
+
seed_input=gr.Number(label="Random seed",value=42,precision=0,minimum=0,maximum=9999)
|
| 320 |
+
gr.Markdown(f"**Fast public demo:** {DEMO_EPOCHS} diffusion epochs, no GO-BS, no Optuna. "
|
| 321 |
+
"Larger uploaded datasets may exceed free hardware limits.")
|
| 322 |
+
run_btn=gr.Button("✦ Generate synthetic dataset",variant="primary",elem_id="generate-btn",size="lg")
|
| 323 |
+
with gr.Column(scale=6,elem_classes=["studio-section"]):
|
| 324 |
+
gr.Markdown("### ② Dataset snapshot")
|
| 325 |
+
file_info=gr.HTML("<p>Upload a CSV to inspect its structure.</p>")
|
| 326 |
+
preview=gr.Dataframe(label="Original data · first 8 rows",interactive=False,
|
| 327 |
+
max_height=310)
|
| 328 |
+
|
| 329 |
+
gr.Markdown("## ③ Explore your generated data")
|
| 330 |
+
result=gr.Markdown("The synthetic-data dashboard will appear here after generation.")
|
| 331 |
+
state=gr.State(value=None)
|
| 332 |
+
with gr.Row():
|
| 333 |
+
real_plot_feature=gr.Dropdown(label="Feature to inspect",choices=[],interactive=True)
|
| 334 |
+
download=gr.File(label="Download synthetic CSV",interactive=False)
|
| 335 |
+
with gr.Row():
|
| 336 |
+
with gr.Column():
|
| 337 |
+
with gr.Tab("📊 Feature distribution"):
|
| 338 |
+
hist=gr.Plot(label="Real vs synthetic feature distribution")
|
| 339 |
+
with gr.Tab("🧭 PCA projection"):
|
| 340 |
+
pca=gr.Plot(label="Shared PCA projection")
|
| 341 |
+
with gr.Column():
|
| 342 |
+
with gr.Tab("🧩 Correlations"):
|
| 343 |
+
corr=gr.Plot(label="Real and synthetic correlation matrices")
|
| 344 |
+
with gr.Tab("🧬 Synthetic preview"):
|
| 345 |
+
generated_table=gr.Dataframe(label="Synthetic data · first 10 rows",interactive=False)
|
| 346 |
+
|
| 347 |
+
gr.HTML("""<div style="text-align:center;padding:16px;font-size:12px;opacity:.75">
|
| 348 |
+
<a href="https://www.zadidhabib.com/bstabdiff.html" target="_blank">Project webpage</a> ·
|
| 349 |
+
<a href="https://github.com/zadid6pretam/BSTabDiff" target="_blank">GitHub</a> ·
|
| 350 |
+
<a href="https://pypi.org/project/bstabdiff/" target="_blank">PyPI</a> ·
|
| 351 |
+
<a href="https://openreview.net/forum?id=ciPCMXX1aj" target="_blank">NeurIPS 2026 paper</a>
|
| 352 |
+
<p>Research demonstration only. Synthetic data are not necessarily private or faithful to the source distribution.</p>
|
| 353 |
+
</div>""")
|
| 354 |
+
|
| 355 |
+
file_input.change(inspect_csv,inputs=file_input,
|
| 356 |
+
outputs=[target,real_plot_feature,file_info,preview],show_progress="minimal")
|
| 357 |
+
run_btn.click(generate,inputs=[file_input,target,samples,seed_input,real_plot_feature],
|
| 358 |
+
outputs=[result,generated_table,hist,pca,corr,download,state,real_plot_feature],
|
| 359 |
+
concurrency_limit=1,show_progress="full")
|
| 360 |
+
real_plot_feature.change(update_plot,inputs=[real_plot_feature,state],outputs=hist,
|
| 361 |
+
show_progress="minimal")
|
| 362 |
+
|
| 363 |
+
demo.queue(max_size=6)
|
| 364 |
+
|
| 365 |
+
if __name__ == "__main__":
|
| 366 |
+
demo.launch(theme=gr.themes.Soft(primary_hue="indigo",secondary_hue="pink"), css=CSS)
|
requirements.txt
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
bstabdiff==0.2.1
|
| 2 |
+
plotly>=6.0,<7.0
|