zadid6pretam commited on
Commit
fedf1cd
·
verified ·
1 Parent(s): db3573e

Upload 3 files

Browse files
Files changed (3) hide show
  1. README.md +28 -8
  2. app.py +366 -0
  3. requirements.txt +2 -0
README.md CHANGED
@@ -1,15 +1,35 @@
1
  ---
2
  title: BSTabDiff Studio
3
- emoji: 🐨
4
- colorFrom: yellow
5
- colorTo: blue
6
  sdk: gradio
7
- sdk_version: 6.31.0
8
- python_version: '3.12'
9
  app_file: app.py
10
- pinned: false
11
  license: mit
12
- short_description: Generate and explore high-dimensional synthetic tabular data
 
13
  ---
14
 
15
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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