OSCR

Domain adaptive uplift modeling across heterogeneous mental health cohorts.

Code ↔ Paper

10 matches between paragraphs of the paper and lines of its authors' code, computed by the harvester (lexical-v1). Click a colored paragraph or line to see its counterpart.

The 10 matches
  1. [1] § STAR★Methods › Method details › Datasets and problem setup ↔ dann-analysis.ipynb, lines 97–116 · score 0.80 · medical student, academic stress, habit change, general population, EEG, cognitive
  2. [2] § STAR★Methods › Method details › Construct validity and comparability › Interpretation caveat ↔ dann-analysis.ipynb, lines 169–203 · score 0.78 · main.disorder, specific.disorder, mood disorder, treatment proxy, outcome proxy, anxiety
  3. [3] § STAR★Methods › Method details › Notation and formal definitions › Domain-adversarial training ↔ dann-analysis.ipynb, lines 454–594 · score 0.72 · gradient reversal layer, domain discriminator, optimized, scalar, GRL, loss
  4. [4] § STAR★Methods › Method details › Construct validity and comparability › Measurement heterogeneity ↔ dann-analysis.ipynb, lines 97–116 · score 0.71 · medical student, burnout exhaustion, clinical diagnosis, growing stress, treatment proxy, populations
  5. [5] § STAR★Methods › Method details › Model: DANN_Uplift (domain-adversarial uplift network) › Adversarial domain alignment and fairness regularizer ↔ dann-analysis.ipynb, lines 454–594 · score 0.71 · cross entropy, domain loss, masks, discriminator, GRL, binary
  6. [6] § Results › Systematic λD analysis and diagnostics › Fairness diagnostics ↔ dann-analysis.ipynb, lines 169–203 · score 0.69 · main.disorder, specific.disorder, mood disorder, treatment proxy, outcome proxy, anxiety
  7. [7] § Results › Head-to-head: DANN vs. CDAN vs. CORAL vs. MMD vs. NoDA ↔ dann-analysis.ipynb, lines 889–1031 · score 0.65 · Wilcoxon signed rank, paired median, histogram, CI, NoDA, Cliff
  8. [8] § STAR★Methods › Method details › Evaluation protocol ↔ dann-analysis.ipynb, lines 889–1031 · score 0.56 · Wilcoxon signed rank, nonparametric, Cliff, bootstrap, gaps, Fairness
  9. [9] § Results › Schedules, transfers, and semi-synthetic validation › Scheduled λD ↔ dann-analysis.ipynb, lines 1290–1324 · score 0.53 · linear ramp, sigmoid ramp, schedules, DANN, AUUC
  10. [10] § STAR★Methods › Method details › Notation and formal definitions › Area under the uplift curve (AUUC) ↔ dann-analysis.ipynb, lines 1191–1238 · score 0.53 · uplift curve, random baseline, fractions, bins, inverted, ranking

Paper

Loaded from Europe PMC by your browser, not stored by OSCR: doi.org · Europe PMC

The paper is loaded when this pane is shown.

The authors' code

Jupyter notebook · 2,286 lines · 86 KB · no license · 10 matches

  1. # %%
  2. # This Python 3 environment comes with many helpful analytics libraries installed
  3. # It is defined by the kaggle/python Docker image: https://github.com/kaggle/docker-python
  4. # For example, here's several helpful packages to load
  5. import numpy as np # linear algebra
  6. import pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)
  7. # Input data files are available in the read-only "../input/" directory
  8. # For example, running this (by clicking run or pressing Shift+Enter) will list all files under the input directory
  9. import os
  10. for dirname, _, filenames in os.walk('/kaggle/input'):
  11. for filename in filenames:
  12. print(os.path.join(dirname, filename))
  13. # You can write up to 20GB to the current directory (/kaggle/working/) that gets preserved as output when you create a version using "Save & Run All"
  14. # You can also write temporary files to /kaggle/temp/, but they won't be saved outside of the current session
  15. # %% [markdown]
  16. # ### This code integrates Domain-Adversarial Neural Networks (DANN) into a cross-dataset mental health study by harmonizing and processing data from three distinct sources: ###
  17. #
  18. #
  19. # * **Medical Students' Mental Health (med)**
  20. # * **Mental Health During Quarantine (quar)**
  21. # * **Psychiatric Disorders Dataset (psych)**
  22. #
  23. #
  24. #
  25. # ### DANN is used to learn shared mental health patterns while adapting to domain-specific differences across populations. ###
  26. # %%
  27. from pathlib import Path
  28. import pandas as pd
  29. import numpy as np
  30. import matplotlib.pyplot as plt
  31. from matplotlib.gridspec import GridSpec
  32. from IPython.display import display
  33. import seaborn as sns
  34. import re
  35. import random
  36. import statsmodels.api as sm
  37. from statsmodels.formula.api import ols
  38. from scipy.stats import linregress
  39. from scipy.stats import ttest_ind, chi2_contingency
  40. from sklearn.compose import ColumnTransformer
  41. from sklearn.preprocessing import OneHotEncoder, StandardScaler
  42. from sklearn.pipeline import Pipeline
  43. from sklearn.impute import SimpleImputer
  44. from sklearn.model_selection import train_test_split
  45. from sklearn.ensemble import RandomForestClassifier, RandomForestRegressor
  46. from scipy import sparse
  47. import torch, torch.nn as nn, torch.nn.functional as F
  48. from torch.utils.data import Dataset, DataLoader, TensorDataset
  49. from pathlib import Path
  50. from sklearn.compose import ColumnTransformer
  51. import pytorch_lightning as pl
  52. import joblib
  53. from sklearn.model_selection import GroupKFold
  54. from sklearn.metrics import roc_auc_score
  55. import optuna, json, gc, os
  56. import warnings; warnings.filterwarnings("ignore")
  57. # %%
  58. def clean_dataframe(df):
  59. # Identify and drop completely empty unnamed columns
  60. unnamed_cols = [col for col in df.columns if "unnamed" in col]
  61. empty_unnamed_cols = [col for col in unnamed_cols if df[col].isnull().all()]
  62. df.drop(empty_unnamed_cols, axis=1, inplace=True)
  63. # Identify columns with NaN values
  64. nan_counts = df.isna().sum()
  65. columns_with_nan = nan_counts[nan_counts != 0].index.tolist()
  66. print("Columns identified with NaNs:", columns_with_nan)
  67. print("Rows affected before dropna:", df[columns_with_nan].isna().sum().sum())
  68. # Drop rows where any of the identified columns have NaN values
  69. if columns_with_nan:
  70. df.dropna(subset=columns_with_nan, inplace=True)
  71. print("Final dataset shape:", df.shape, "\n")
  72. return df
  73. # %%
  74. def make_tensor_dataset(X, y, T, dom, gender, edu):
  75. X = torch.tensor(X, dtype=torch.float32)
  76. y = torch.tensor(y, dtype=torch.float32).unsqueeze(1)
  77. T = torch.tensor(T, dtype=torch.float32).unsqueeze(1)
  78. dom = torch.tensor(dom, dtype=torch.long)
  79. g = torch.tensor(gender, dtype=torch.float32).unsqueeze(1)
  80. e = torch.tensor(edu, dtype=torch.float32).unsqueeze(1)
  81. return TensorDataset(X, T, y, dom, g, e)
  82. # %% [markdown]
  83. # **Quarantine Mental Health (General Population)** : Defining Treatment (T)= Growing Stress & Outcome (Y)= Coping Struggles. Keeps relevant psychological indicators (e.g., frustration, habit changes).
  84. # These will help DANN learn stress patterns specific to quarantine conditions.
  85. #
  86. # **Medical Students' Mental Health (Academic Burnout)** : Treatment Proxy (T)= Burnout Exhaustion (mbi_ex) & Outcome Proxy (Y)= Depression. Includes burnout (MBI), anxiety (STAI), empathy (QCAE), and academic stress predictors. Helps DANN differentiate academic stress effects from psychiatric disorders.
  87. #
  88. # **Psychiatric Disorders (Clinical Diagnoses)** : Treatment Proxy (T)= Stress & Anxiety (Categorizes patients diagnosed with anxiety disorders as T = 1) & Outcome Proxy (Y)= Depression (Categorizes patients with mood disorders & major depression as Y = 1). IQ is retained as a predictor of cognitive impairment across domains.
  89. # %%
  90. # ================================================================
  91. # 0 PATHS
  92. # ================================================================
  93. ROOT = Path("/kaggle/input/datasets/anwe9274/three-datasets")
  94. FILES = {
  95. "med" : ROOT / "Medical Student Mental Health.csv",
  96. "quar" : ROOT / "mental_health_data.csv",
  97. "psych": ROOT / "EEG.machinelearing_data_BRMH.csv",
  98. }
  99. # ================================================================
  100. # 1 Load & lower-case column names
  101. # ================================================================
  102. dfs = {k: pd.read_csv(p).rename(str.lower, axis=1) for k, p in FILES.items()}
  103. # helpers ---------------------------------------------------
  104. def pct75(s: pd.Series) -> float:
  105. return s.quantile(0.75, interpolation="midpoint")
  106. def _yes(s): # yes/no/maybe → 1/0 mapping
  107. return s.astype(str).str.strip().str.lower().map({"yes": 1, "maybe": 1, "no": 0})
  108. # ================================================================
  109. # 2 Dataset-specific wrangling → returns df with unified columns
  110. # ================================================================
  111. def prep_quarantine(df: pd.DataFrame) -> pd.DataFrame:
  112. df = clean_dataframe(df)
  113. out = pd.DataFrame()
  114. out["domain"] = "quar"
  115. out["age"] = pd.to_numeric(df["age"], errors="coerce")
  116. out["gender"] = df["gender"].str.strip().str.title().map({"Male": 1, "Female": 0})
  117. out["edu_years"] = np.nan # not provided
  118. # Treatment / outcome
  119. out["T"] = _yes(df["growing_stress"])
  120. out["Y"] = _yes(df["coping_struggles"])
  121. # raw predictors (keep everything except obvious identifiers)
  122. keep = ["occupation","days_indoors","quarantine_frustrations",
  123. "changes_habits","mental_health_history","weight_change",
  124. "mood_swings","work_interest","social_weakness"]
  125. out = pd.concat([out, df[keep]], axis=1)
  126. return out
  127. def prep_medical(df: pd.DataFrame) -> pd.DataFrame:
  128. df = clean_dataframe(df)
  129. df = df[df["sex"] != 3]
  130. out = pd.DataFrame()
  131. out["domain"] = "med"
  132. out["age"] = df["age"]
  133. out["gender"] = df["sex"].map({1:1,2:0}) # 1=M,2=F in paper
  134. out["edu_years"] = 12 + df["year"] # MBBS year 1-6
  135. # Treatment / outcome
  136. out["T"] = (df["mbi_ex"] >= pct75(df["mbi_ex"])).astype(int)
  137. out["Y"] = (df["cesd"] >= 10).astype(int)
  138. # predictors
  139. predictors = ["stai_t","mbi_cy","mbi_ea","qcae_cog","qcae_aff",
  140. "amsp","erec_mean","health","psyt","glang","stud_h"]
  141. out = pd.concat([out, df[predictors]], axis=1)
  142. return out
  143. def prep_psych(df: pd.DataFrame) -> pd.DataFrame:
  144. df = clean_dataframe(df)
  145. out = pd.DataFrame()
  146. # --- demographics -------------------------------------------
  147. out["domain"] = "psych"
  148. out["age"] = df["age"]
  149. out["gender"] = df["sex"].str[0].str.upper().map({"M": 1, "F": 0})
  150. out["edu_years"] = df["education"]
  151. # --- categorical strings normalised -------------------------
  152. main = df["main.disorder"].str.strip().str.lower()
  153. specific = df["specific.disorder"].str.strip().str.lower()
  154. # ---- treatment proxy (stress / anxiety) -------------------
  155. stress_main = main.eq("anxiety disorder")
  156. stress_spec = specific.isin(
  157. {"posttraumatic stress disorder",
  158. "panic disorder",
  159. "social anxiety disorder",
  160. }
  161. )
  162. out["T"] = (stress_main | stress_spec).astype(int)
  163. # ---- outcome proxy (depressive condition) -----------------
  164. dep_main = main.eq("mood disorder")
  165. dep_spec = specific.eq("depressive disorder")
  166. out["Y"] = (dep_main | dep_spec).astype(int)
  167. # ---- extra predictors --------------------------------------
  168. out["iq"] = df["iq"]
  169. out["main_disorder"] = main # leave as feature
  170. out["spec_disorder"] = specific
  171. return out
  172. # run preppers ----------------------------------------------------
  173. dfs["quar"] = prep_quarantine(dfs["quar"])
  174. dfs["med"] = prep_medical(dfs["med"])
  175. dfs["psych"] = prep_psych(dfs["psych"])
  176. label_map = {"med":0, "quar":1, "psych":2}
  177. for k, df in dfs.items():
  178. df["domain"] = label_map[k]
  179. # ================================================================
  180. # 3 Concatenate & basic cleaning
  181. # ================================================================
  182. df_all = pd.concat(dfs.values(), ignore_index=True)
  183. domains = df_all["domain"].values.astype(np.int64) # <-- final label vector
  184. # Education bin (≥12 yrs) – if NaN, set to population mode 0
  185. df_all["edu_bin"] = (df_all["edu_years"].fillna(0) >= 12).astype(int)
  186. # drop obvious ID cols that slipped through
  187. drop_like = re.compile(r"id$|idx|record.*", re.I)
  188. df_all = df_all[df_all.columns[~df_all.columns.str.match(drop_like)]]
  189. # ================================================================
  190. # 4 Pre-processing pipeline (numeric median / categorical mode)
  191. # ================================================================
  192. num_cols = df_all.select_dtypes(include=["number"]).columns.tolist()
  193. cat_cols = [c for c in df_all.columns if c not in num_cols]
  194. numeric_pipe = Pipeline([
  195. ("imp", SimpleImputer(strategy="median")),
  196. ])
  197. categorical_pipe = Pipeline([
  198. ("imp", SimpleImputer(strategy="most_frequent")),
  199. ("ohe", OneHotEncoder(handle_unknown="ignore", sparse_output=False)),
  200. ])
  201. ct = ColumnTransformer(
  202. [("num", numeric_pipe, num_cols),
  203. ("cat", categorical_pipe, cat_cols)],
  204. remainder="drop"
  205. )
  206. # ---------- ColumnTransformer ----------
  207. X = ct.fit_transform(df_all) # shape (2629, 57)
  208. y = df_all["Y"].values.astype(np.float32)
  209. T = df_all["T"].values.astype(np.float32)
  210. gender = df_all["gender"].values.astype(np.int64)
  211. edu = df_all["edu_bin"].values.astype(np.int64)
  212. # ---------- TensorDataset ----------
  213. full_ds = make_tensor_dataset(X, y, T, domains, gender, edu)
  214. print("Final feature matrix", X.shape, "with", len(num_cols), "numeric +",
  215. len(ct.named_transformers_["cat"]["ohe"].get_feature_names_out()), "one-hot dims")
  216. # save
  217. joblib.dump(ct, "preproc_column_transformer.pkl")
  218. df_all.to_csv("master_dataframe.csv", index=False)
  219. # %%
  220. for name, df in [("Medical", dfs["med"]),
  221. ("Quarantine", dfs["quar"]),
  222. ("Psychiatric", dfs["psych"])]:
  223. print(f"{name}: n={len(df)} "
  224. f"Prev(T)={df['T'].mean():.3f} "
  225. f"Prev(Y)={df['Y'].mean():.3f}")
  226. # %%
  227. psych_raw = pd.read_csv(ROOT / "EEG.machinelearing_data_BRMH.csv")
  228. psych_raw.columns = psych_raw.columns.str.lower()
  229. unnamed_cols = [col for col in psych_raw.columns if "unnamed" in col]
  230. empty_unnamed_cols = [col for col in unnamed_cols if psych_raw[col].isnull().all()]
  231. psych_raw.drop(empty_unnamed_cols, axis=1, inplace=True)
  232. nan_counts = psych_raw.isna().sum()
  233. columns_with_nan = nan_counts[nan_counts != 0].index.tolist()
  234. print("Columns identified with NaNs:", columns_with_nan)
  235. print("Rows affected before dropna:", psych_raw[columns_with_nan].isna().sum().sum())
  236. if columns_with_nan:
  237. psych_raw.dropna(subset=columns_with_nan, inplace=True)
  238. # --- Disorder proxies ---
  239. main = psych_raw["main.disorder"].str.strip().str.lower()
  240. specific = psych_raw["specific.disorder"].str.strip().str.lower()
  241. stress_main = main.eq("anxiety disorder")
  242. stress_spec = specific.isin({"posttraumatic stress disorder",
  243. "panic disorder", "social anxiety disorder"})
  244. T_psych = (stress_main | stress_spec).astype(int)
  245. dep_main = main.eq("mood disorder")
  246. dep_spec = specific.eq("depressive disorder")
  247. Y_psych = (dep_main | dep_spec).astype(int)
  248. # --- Diagnostics ---
  249. ct = pd.crosstab(T_psych, Y_psych, margins=True,
  250. rownames=["T (anxiety proxy)"],
  251. colnames=["Y (mood/MDD proxy)"])
  252. print(ct)
  253. print()
  254. print(f"P(Y=1 | T=1) = {Y_psych[T_psych==1].mean():.3f}")
  255. print(f"P(Y=1 | T=0) = {Y_psych[T_psych==0].mean():.3f}")
  256. # Overlap checks
  257. both_from_main = main.eq("anxiety disorder") & main.eq("mood disorder")
  258. print(f"Patients where main.disorder drives BOTH T and Y: {both_from_main.sum()}")
  259. spec_T = specific.isin({"posttraumatic stress disorder",
  260. "panic disorder", "social anxiety disorder"})
  261. spec_Y = specific.eq("depressive disorder")
  262. print(f"Patients with T=1 from specific AND Y=1 from specific: {(spec_T & spec_Y).sum()}")
  263. print(f"Patients with T=1 from main AND Y=1 from specific: {(stress_main & dep_spec).sum()}")
  264. print(f"Patients with T=1 from specific AND Y=1 from main: {(stress_spec & dep_main).sum()}")
  265. # Feature inspection
  266. print("\nFeatures retained from prep_psych that encode disorder:")
  267. print("main_disorder unique values:", main.unique().tolist())
  268. print("spec_disorder sample:", specific.value_counts().head(8).to_dict())
  269. # %%
  270. import torch
  271. def check_tensor_dataset(dataset):
  272. for i, tensor in enumerate(dataset.tensors):
  273. nan_mask = torch.isnan(tensor)
  274. inf_mask = torch.isinf(tensor)
  275. if nan_mask.any():
  276. print(f"NaNs found in tensor {i}: {nan_mask.nonzero(as_tuple=True)}")
  277. if inf_mask.any():
  278. print(f"Infinities found in tensor {i}: {inf_mask.nonzero(as_tuple=True)}")
  279. print("Check complete.")
  280. check_tensor_dataset(full_ds)
  281. # %%
  282. print(f"Dataset size: {len(full_ds)} samples")
  283. for i in range(5): # Display first 5 samples
  284. print(full_ds[i])
  285. # %% [markdown]
  286. # **0- Quarantine-->Fold 1, 1- Medical-->Fold 2, 3- Psychiatric-->Fold 3**
  287. # %%
  288. assert df_all["Y"].isin([0,1]).all(), "Found invalid Y values"
  289. assert df_all["T"].isin([0,1]).all(), "Found invalid T values"
  290. assert not np.isnan(X).any(), "NaNs found in X!"
  291. assert np.isfinite(X).all(), "Inf found in X!"
  292. print(df_all["domain"].value_counts(dropna=False))
  293. # %%
  294. domain_name = {0: "Medical", 1: "Quarantine", 2: "Psychiatric"}
  295. gkf = GroupKFold(n_splits=3)
  296. fold_domains = []
  297. for fold, (tr, vl) in enumerate(gkf.split(np.zeros(len(y)), y, groups=domains), 1):
  298. heldout = np.unique(domains[vl])
  299. assert len(heldout) == 1
  300. d = int(heldout[0])
  301. fold_domains.append(d)
  302. print(f"Fold {fold}: held-out domain {d} ({domain_name[d]})")
  303. # %%
  304. def auuc_score(y_true, treat, uplift_pred, n_bins=10):
  305. # discretise uplift into percentile bins → cumulative net lift
  306. df = (
  307. pd.DataFrame({"y":y_true,"t":treat,"upl":uplift_pred})
  308. .sort_values("upl", ascending=False)
  309. .reset_index(drop=True)
  310. )
  311. step = int(np.ceil(len(df) / n_bins))
  312. net_lift, random = [], []
  313. for i in range(step, len(df)+1, step):
  314. take = df.iloc[:i]
  315. # uplift = E[y|T=1] - E[y|T=0]
  316. t_on = take[take.t==1].y.mean() if (take.t==1).any() else 0
  317. t_off = take[take.t==0].y.mean() if (take.t==0).any() else 0
  318. net_lift.append(t_on - t_off)
  319. random.append(df.iloc[:i].y.mean() - df.y.mean())
  320. return np.trapz(net_lift) - np.trapz(random) # AUUC vs random baseline
  321. def uplift_curve(y_true, treat, uplift_pred, bins=10):
  322. df = pd.DataFrame({"y":y_true, "t":treat, "u":uplift_pred})\
  323. .sort_values("u", ascending=False).reset_index(drop=True)
  324. step = int(np.ceil(len(df)/bins))
  325. xs, lift = [], []
  326. for i in range(step, len(df)+1, step):
  327. take = df.iloc[:i]
  328. mu1 = take[take.t==1].y.mean() if (take.t==1).any() else 0
  329. mu0 = take[take.t==0].y.mean() if (take.t==0).any() else 0
  330. xs.append(i/len(df)); lift.append(mu1-mu0)
  331. return xs, lift, float(np.trapz(lift))
  332. def fairness_gap(uplift, mask_a, mask_b):
  333. if mask_a.any() and mask_b.any():
  334. return float(abs(uplift[mask_a].mean() - uplift[mask_b].mean()))
  335. return 0.0
  336. def make_loaders(tr_idx, vl_idx, bs=128):
  337. return (
  338. DataLoader(torch.utils.data.Subset(full_ds, tr_idx), bs, True, num_workers=2),
  339. DataLoader(torch.utils.data.Subset(full_ds, vl_idx), bs*2,False, num_workers=2)
  340. )
  341. def eval_cfg(lr, λ_D, λ_F, max_ep=20):
  342. gkf = GroupKFold(n_splits=3)
  343. folds = gkf.split(np.zeros(len(y)), y, groups=domains)
  344. auucs = []
  345. for k,(tr,vl) in enumerate(folds,1):
  346. mdl = DANNSystem(d_in=X.shape[1], λ_D=λ_D, λ_F=λ_F, lr=lr)
  347. trL, vlL = make_loaders(tr, vl)
  348. trainer = pl.Trainer(
  349. max_epochs=max_ep, accelerator="gpu" if torch.cuda.is_available() else "cpu",
  350. devices=1, log_every_n_steps=5, enable_checkpointing=False,
  351. gradient_clip_val=1.0,
  352. callbacks=[pl.callbacks.EarlyStopping("val_auuc",mode="max",patience=5)],
  353. )
  354. trainer.fit(mdl, trL, vlL)
  355. best = trainer.callback_metrics["val_auuc"].item()
  356. print(f" fold{k}: best_AUUC={best:.3f}")
  357. auucs.append(best)
  358. del mdl; gc.collect(); torch.cuda.empty_cache()
  359. mean,std = np.mean(auucs), np.std(auucs)
  360. print(f"⇒ mean AUUC={mean:.3f} ±{std:.3f}\n")
  361. return -mean, auucs # Optuna minimizes
  362. # %% [markdown]
  363. # Goal: estimate uplift (causal treatment effect) while
  364. # (i) generalising across 3 data-collection domains and
  365. # (ii) enforcing group-fairness in the predicted uplift.
  366. #
  367. # Encoder – turns raw tabular features x into a latent vector z.
  368. # We want z to be useful for uplift but hide domain-specific quirks.
  369. #
  370. # Treatment heads – two sigmoid heads (head0, head1) predict the probability of a positive outcome under control and treatment respectively.
  371. # Uplift = ŷ₁ − ŷ₀.
  372. #
  373. # Domain discriminator + GRL – a small classifier tries to recover the domain label (0, 1, 2) from z, but its gradient flows through a Gradient-Reversal Layer (GRL) which multiplies it by −λ<sub>D</sub>.
  374. # Result: encoder is trained adversarially to make domains indistinguishable → better out-of-domain generalisation.
  375. #
  376. # Three loss terms
  377. #
  378. # Uplift loss L_up – standard binary cross-entropy on observed outcomes.
  379. #
  380. # Domain loss L_dom – cross-entropy of discriminator vs. true domain.
  381. #
  382. # Fairness loss L_fair – squared gap of average uplift between protected-group pairs (gender g and ethnicity e masks).
  383. #
  384. # Total loss = L_up + λ_D · L_dom + λ_F · L_fair.
  385. #
  386. # Validation metric – AUUC (Area Under Uplift Curve) logged each epoch.
  387. # %% [markdown]
  388. # # Encoder: 128-64 with Dropout 0.30
  389. # %%
  390. # --------------------------------------------
  391. # 1. Grad-reversal helper
  392. # --------------------------------------------
  393. class GradReverse(torch.autograd.Function):
  394. @staticmethod
  395. def forward(ctx, x, l): ctx.l = l; return x.view_as(x)
  396. @staticmethod
  397. def backward(ctx, g): return -ctx.l * g, None
  398. class GRL(nn.Module):
  399. def __init__(self, l=1.0): super().__init__(); self.l=l
  400. def forward(self, x): return GradReverse.apply(x, self.l)
  401. # --------------------------------------------
  402. # 2. Base model with optimiser + val logging
  403. # --------------------------------------------
  404. class DANN_Uplift(pl.LightningModule):
  405. def __init__(self, d_in,
  406. λ_D=1.0, λ_F=10.0,
  407. lr=1e-4,
  408. wd=1e-4):
  409. super().__init__()
  410. self.save_hyperparameters()
  411. # —— Encoder: 128-64 with Dropout 0.3——
  412. self.encoder = nn.Sequential(
  413. nn.Linear(d_in, 128), nn.ReLU(), nn.Dropout(0.3),
  414. nn.Linear(128, 64), nn.ReLU()
  415. )
  416. # —— Treatment heads ——
  417. self.head0 = nn.Linear(64, 1)
  418. self.head1 = nn.Linear(64, 1)
  419. # —— Domain discriminator (3 classes) ——
  420. self.disc = nn.Sequential(
  421. nn.Linear(64, 64), nn.ReLU(),
  422. nn.Linear(64, 3)
  423. )
  424. self.grl = GRL()
  425. # -----------------------------------------------------------
  426. def forward(self, x, t):
  427. z = self.encoder(x)
  428. y0 = torch.sigmoid(self.head0(z))
  429. y1 = torch.sigmoid(self.head1(z))
  430. d = self.disc(self.grl(z))
  431. return y0, y1, d
  432. # -----------------------------------------------------------
  433. def training_step(self, batch, *_):
  434. x, t, y, dom, g, e = batch
  435. y0, y1, d_hat = self(x, t)
  436. # ---------- primary & domain losses ----------
  437. eps = 1e-7
  438. y_pred = torch.clip(torch.where(t == 1, y1, y0), eps, 1 - eps)
  439. L_up = F.binary_cross_entropy(y_pred, y)
  440. L_dom = F.cross_entropy(d_hat, dom)
  441. # ---------- fairness (safe) ----------
  442. uplift = (y1 - y0).flatten()
  443. def safe_gap(mask_a, mask_b):
  444. if mask_a.any() and mask_b.any(): # both sub-groups present
  445. return (uplift[mask_a].mean() - uplift[mask_b].mean()) ** 2
  446. else:
  447. return torch.scalar_tensor(0.0, device=x.device)
  448. Δg = safe_gap(g.flatten() == 0, g.flatten() == 1)
  449. Δe = safe_gap(e.flatten() == 0, e.flatten() == 1)
  450. L_fair = Δg + Δe
  451. # ---------- total loss ----------
  452. loss = L_up + self.hparams.λ_D * L_dom + self.hparams.λ_F * L_fair
  453. self.log_dict(
  454. {"train_loss": loss,
  455. "train_uplift": L_up,
  456. "train_dom": L_dom,
  457. "train_fair": L_fair},
  458. prog_bar=True, on_epoch=True
  459. )
  460. return loss
  461. # -----------------------------------------------------------
  462. def validation_step(self, batch, *_):
  463. x,t,y,dom,_,_ = batch
  464. y0,y1,_ = self(x,t)
  465. uplift_pred = (y1 - y0).detach().cpu().numpy().ravel()
  466. uplift_true = y.detach().cpu().numpy().ravel()
  467. treat = t.detach().cpu().numpy().ravel()
  468. auuc = auuc_score(uplift_true, treat, uplift_pred)
  469. self.log("val_auuc", auuc, prog_bar=True, on_epoch=True)
  470. return {"auuc": auuc}
  471. # -----------------------------------------------------------
  472. def configure_optimizers(self):
  473. opt = torch.optim.Adam(self.parameters(),
  474. lr=self.hparams.lr,
  475. weight_decay=self.hparams.wd)
  476. return {"optimizer": opt, "gradient_clip_val": 1.0}
  477. # --------------------------------------------
  478. # 3. Thin wrapper
  479. #
  480. # --------------------------------------------
  481. class DANNSystem(DANN_Uplift):
  482. """Inherits everything."""
  483. pass
  484. # %%
  485. model = DANN_Uplift(d_in=100) # example input dimension
  486. print(model)
  487. # %%
  488. def run_method(method_name, config, n_splits=3, max_ep=20):
  489. gkf = GroupKFold(n_splits=n_splits)
  490. auucs, gap_g, gap_e = [], [], []
  491. for fold,(tr,vl) in enumerate(gkf.split(np.zeros(len(y)), y, groups=domains),1):
  492. # Create model according to method
  493. if method_name == "NoDA":
  494. mdl = DANNSystem(d_in=X.shape[1], lr=config["lr"], λ_D=0.0, λ_F=config["λ_F"])
  495. else: # "DANN" or others
  496. mdl = DANNSystem(d_in=X.shape[1], lr=config["lr"], λ_D=config["λ_D"], λ_F=config["λ_F"])
  497. trL, vlL = make_loaders(tr, vl)
  498. trainer = pl.Trainer(max_epochs=max_ep, devices=1, accelerator="gpu"
  499. if torch.cuda.is_available() else "cpu",
  500. enable_checkpointing=False, gradient_clip_val=1.0,
  501. callbacks=[pl.callbacks.EarlyStopping("val_auuc",mode="max",patience=5)],
  502. logger=False)
  503. trainer.fit(mdl, trL, vlL)
  504. auuc = trainer.callback_metrics["val_auuc"].item(); auucs.append(auuc)
  505. # compute uplift on validation set (same as your code)
  506. u,tv,yv = [],[],[]
  507. for xb,tb,yb,_,_,_ in vlL:
  508. xb = xb.to(mdl.device)
  509. with torch.no_grad():
  510. z = mdl.encoder(xb)
  511. u.append((mdl.head1(z)-mdl.head0(z)).cpu().numpy().ravel())
  512. tv.append(tb.numpy().ravel()); yv.append(yb.numpy().ravel())
  513. u = np.concatenate(u); tv=np.concatenate(tv); yv=np.concatenate(yv)
  514. # fairness masks must be indexed by vl
  515. xs,lift,_ = uplift_curve(yv,tv,u,bins=10)
  516. gap_g.append(fairness_gap(u, (gender[vl]==0)&(tv==1), (gender[vl]==1)&(tv==1)))
  517. gap_e.append(fairness_gap(u, (edu[vl]==0)&(tv==1), (edu[vl]==1)&(tv==1)))
  518. del mdl; gc.collect(); torch.cuda.empty_cache()
  519. return np.array(auucs), np.array(gap_g), np.array(gap_e)
  520. # %%
  521. def eval_uplift_and_gaps(mdl, vlL, vl_idx):
  522. u, tv, yv = [], [], []
  523. mdl.eval()
  524. for xb, tb, yb, _, _, _ in vlL:
  525. xb = xb.to(mdl.device)
  526. with torch.no_grad():
  527. z = mdl.encoder(xb)
  528. u.append((mdl.head1(z) - mdl.head0(z)).cpu().numpy().ravel())
  529. tv.append(tb.numpy().ravel()); yv.append(yb.numpy().ravel())
  530. u = np.concatenate(u); tv = np.concatenate(tv); yv = np.concatenate(yv)
  531. # uplift only on treated for fairness diagnostics (as in paper)
  532. treated_mask = (tv == 1)
  533. gap_g = fairness_gap(u, (gender[vl_idx] == 0) & treated_mask, (gender[vl_idx] == 1) & treated_mask)
  534. gap_e = fairness_gap(u, (edu[vl_idx] == 0) & treated_mask, (edu[vl_idx] == 1) & treated_mask)
  535. return u, tv, yv, gap_g, gap_e
  536. def loso_with_gaps(tag, use_dann, max_ep=20):
  537. gkf = GroupKFold(n_splits=3)
  538. auucs, gap_gs, gap_es = [], [], []
  539. for k,(tr,vl) in enumerate(gkf.split(np.zeros(len(y)), y, groups=domains),1):
  540. mdl = build_model(use_dann=use_dann) # uses same defaults/hyperparams
  541. trL, vlL = make_loaders(tr, vl)
  542. trainer = pl.Trainer(
  543. max_epochs=max_ep,
  544. accelerator="gpu" if torch.cuda.is_available() else "cpu",
  545. devices=1,
  546. enable_checkpointing=False,
  547. callbacks=[pl.callbacks.EarlyStopping("val_auuc", mode="max", patience=5)],
  548. logger=False, log_every_n_steps=5,
  549. )
  550. trainer.fit(mdl, trL, vlL)
  551. auuc = trainer.callback_metrics["val_auuc"].item()
  552. print(f"{tag} fold{k}: AUUC = {auuc:+.3f}")
  553. # compute uplift and fairness gaps on this validation fold
  554. _, tv, yv, gap_g, gap_e = eval_uplift_and_gaps(mdl, vlL, vl)
  555. auucs.append(auuc); gap_gs.append(gap_g); gap_es.append(gap_e)
  556. del mdl; gc.collect(); torch.cuda.empty_cache()
  557. print(f"{tag} mean AUUC = {np.mean(auucs):+.3f}\n")
  558. return np.array(auucs), np.array(gap_gs), np.array(gap_es)
  559. # %%
  560. # ======================================================================
  561. # 0 · Reproducible seed
  562. # ======================================================================
  563. SEED = 42
  564. random.seed(SEED); np.random.seed(SEED); torch.manual_seed(SEED)
  565. torch.cuda.manual_seed_all(SEED); torch.backends.cudnn.deterministic = True
  566. optuna.logging.set_verbosity(optuna.logging.WARNING) # silence widget warnings
  567. def uplift_curve(y_true, treat, uplift_pred, bins=10):
  568. df = pd.DataFrame({"y":y_true, "t":treat, "u":uplift_pred})\
  569. .sort_values("u", ascending=False).reset_index(drop=True)
  570. step = int(np.ceil(len(df)/bins))
  571. xs, lift = [], []
  572. for i in range(step, len(df)+1, step):
  573. take = df.iloc[:i]
  574. mu1 = take[take.t==1].y.mean() if (take.t==1).any() else 0
  575. mu0 = take[take.t==0].y.mean() if (take.t==0).any() else 0
  576. xs.append(i/len(df)); lift.append(mu1-mu0)
  577. return xs, lift, float(np.trapz(lift))
  578. def fairness_gap(uplift, mask_a, mask_b):
  579. if mask_a.any() and mask_b.any():
  580. return float(abs(uplift[mask_a].mean() - uplift[mask_b].mean()))
  581. return 0.0
  582. def make_loaders(tr_idx, vl_idx, bs=128):
  583. return (
  584. DataLoader(torch.utils.data.Subset(full_ds, tr_idx), bs, True, num_workers=2),
  585. DataLoader(torch.utils.data.Subset(full_ds, vl_idx), bs*2,False, num_workers=2)
  586. )
  587. # ════════════════════════════════════════════════════════════════════════
  588. # 1 · Function that trains *one* configuration and returns mean AUUC
  589. # ════════════════════════════════════════════════════════════════════════
  590. def eval_cfg(lr, λ_D, λ_F, max_ep=20):
  591. gkf = GroupKFold(n_splits=3)
  592. folds = gkf.split(np.zeros(len(y)), y, groups=domains)
  593. auucs = []
  594. for k,(tr,vl) in enumerate(folds,1):
  595. mdl = DANNSystem(d_in=X.shape[1], λ_D=λ_D, λ_F=λ_F, lr=lr)
  596. trL, vlL = make_loaders(tr, vl)
  597. trainer = pl.Trainer(
  598. max_epochs=max_ep, accelerator="gpu" if torch.cuda.is_available() else "cpu",
  599. devices=1, log_every_n_steps=5, enable_checkpointing=False,
  600. gradient_clip_val=1.0,
  601. callbacks=[pl.callbacks.EarlyStopping("val_auuc",mode="max",patience=5)],
  602. )
  603. trainer.fit(mdl, trL, vlL)
  604. best = trainer.callback_metrics["val_auuc"].item()
  605. print(f" fold{k}: best_AUUC={best:.3f}")
  606. auucs.append(best)
  607. del mdl; gc.collect(); torch.cuda.empty_cache()
  608. mean,std = np.mean(auucs), np.std(auucs)
  609. print(f"⇒ mean AUUC={mean:.3f} ±{std:.3f}\n")
  610. return -mean, auucs # Optuna minimizes
  611. # ════════════════════════════════════════════════════════════════════════
  612. # 2 · Quick baseline run
  613. # ════════════════════════════════════════════════════════════════════════
  614. print("Baseline (lr=1e-4, λ_D=1, λ_F=10)")
  615. _, baseline_folds = eval_cfg(lr=1e-4, λ_D=1.0, λ_F=10.0)
  616. # ════════════════════════════════════════════════════════════════════════
  617. # 3 · Optuna sweep
  618. # ════════════════════════════════════════════════════════════════════════
  619. def objective(trial):
  620. lr = trial.suggest_float("lr", 1e-4, 5e-4, log=True)
  621. λ_D = trial.suggest_float("λ_D", .5, 2.0)
  622. λ_F = trial.suggest_float("λ_F", 1.0, 15.0)
  623. return eval_cfg(lr, λ_D, λ_F, max_ep=15)[0]
  624. N_TRIALS = 15
  625. study = optuna.create_study(direction="minimize")
  626. study.optimize(objective, n_trials=N_TRIALS)
  627. best = study.best_params
  628. print(f"★ Best params: {best} → mean_AUUC {(-study.best_value):.3f}")
  629. # ════════════════════════════════════════════════════════════════════════
  630. # 4 · Re-train best config & save figures
  631. # ════════════════════════════════════════════════════════════════════════
  632. gkf = GroupKFold(n_splits=3)
  633. auucs, gap_g, gap_e = [], [], []
  634. for fold,(tr,vl) in enumerate(gkf.split(np.zeros(len(y)), y, groups=domains),1):
  635. mdl = DANNSystem(d_in=X.shape[1], **best)
  636. trL, vlL = make_loaders(tr, vl)
  637. trainer = pl.Trainer(max_epochs=20, devices=1, accelerator="gpu"
  638. if torch.cuda.is_available() else "cpu",
  639. enable_checkpointing=False, gradient_clip_val=1.0,
  640. callbacks=[pl.callbacks.EarlyStopping("val_auuc",mode="max",patience=5)],
  641. logger=False)
  642. trainer.fit(mdl, trL, vlL)
  643. auuc = trainer.callback_metrics["val_auuc"].item(); auucs.append(auuc)
  644. # ----- Uplift curve -----
  645. u,tv,yv = [],[],[]
  646. for xb,tb,yb,_,_,_ in vlL:
  647. xb = xb.to(mdl.device)
  648. with torch.no_grad():
  649. z = mdl.encoder(xb)
  650. u.append((mdl.head1(z)-mdl.head0(z)).cpu().numpy().ravel())
  651. tv.append(tb.numpy().ravel()); yv.append(yb.numpy().ravel())
  652. u = np.concatenate(u); tv=np.concatenate(tv); yv=np.concatenate(yv)
  653. xs,lift,_ = uplift_curve(yv,tv,u,bins=10)
  654. plt.figure(); plt.plot(xs,lift); plt.xlabel("top-x fraction"); plt.ylabel("net uplift")
  655. plt.title(f"Fold {fold} AUUC={auuc:.3f}"); plt.savefig(f"/kaggle/working/uplift_fold{fold}.png",dpi=140)
  656. # ----- Fairness gaps -----
  657. gap_g.append(fairness_gap(u, (gender[vl]==0)&(tv==1), (gender[vl]==1)&(tv==1)))
  658. gap_e.append(fairness_gap(u, (edu[vl]==0)&(tv==1), (edu[vl]==1)&(tv==1)))
  659. # === Fairness bar-chart ===
  660. labels = np.arange(1,4)
  661. plt.figure(figsize=(6,4))
  662. plt.bar(labels-.17, gap_g, .33, label="gender")
  663. plt.bar(labels+.17, gap_e, .33, label="education"); plt.xticks(labels, [f"Fold {i}" for i in labels])
  664. plt.ylabel("|Δ uplift|"); plt.title("Fairness gaps"); plt.legend()
  665. plt.savefig("/kaggle/working/fairness_gaps.png", dpi=140)
  666. plt.savefig("/kaggle/working/fairness_gaps.pdf", dpi=140)
  667. plt.show()
  668. print("\nFinal per-fold AUUC:", np.round(auucs,3).tolist(),
  669. " mean =", np.mean(auucs).round(3))
  670. print("Figures saved in /kaggle/working/ .")
  671. # %%
  672. import matplotlib.pyplot as plt
  673. import numpy as np
  674. labels = np.array([1, 2, 3])
  675. cohort_names = ["Psychiatric", "Medical", "Quarantine"]
  676. # Values (from above cell's output) follow the corrected fold assignment:
  677. # fold 1 → Psychiatric, fold 2 → Medical, fold 3 → Quarantine
  678. gap_g = [0.11, 0.03, 0.10]
  679. gap_e = [0.13, 0.00, 0.00]
  680. plt.figure(figsize=(8, 6))
  681. plt.bar(labels - 0.17, gap_g, width=0.33, label="Gender", color="#4e79a7")
  682. plt.bar(labels + 0.17, gap_e, width=0.33, label="Education", color="#f28e2b")
  683. plt.ylim(0.000, 0.200)
  684. plt.xticks(labels, cohort_names)
  685. plt.xlabel("Held-out Domain", fontfamily='serif', fontsize=17)
  686. plt.ylabel("Fairness Gap (|Δ uplift|)", fontfamily='serif', fontsize=17)
  687. plt.legend(title="Subgroup")
  688. plt.grid(True, linestyle=(0, (1, 5)), linewidth=1.2, color='black')
  689. for i in range(len(labels)):
  690. plt.text(labels[i] - 0.17, gap_g[i] + 0.005, f"{gap_g[i]:.2f}", ha='center', va='bottom', fontsize=15)
  691. plt.text(labels[i] + 0.17, gap_e[i] + 0.005, f"{gap_e[i]:.2f}", ha='center', va='bottom', fontsize=15)
  692. plt.xticks(fontsize=12)
  693. plt.yticks(fontsize=12)
  694. plt.tight_layout()
  695. plt.savefig("/kaggle/working/fairness_gaps.png", dpi=600)
  696. plt.savefig("/kaggle/working/fairness_gaps.pdf", dpi=600)
  697. plt.show()
  698. # %%
  699. # choose seeds you want to average over
  700. SEEDS = [0, 1, 2, 3, 4] # for example 5 seeds
  701. N_FOLDS = 3 # your LOSO folds
  702. # containers to collect per-seed per-fold outputs
  703. auucs_dann_list, gap_g_dann_list, gap_e_dann_list = [], [], []
  704. auucs_noda_list, gap_g_noda_list, gap_e_noda_list = [], [], []
  705. for s in SEEDS:
  706. # set reproducible seed for this trial
  707. random.seed(s); np.random.seed(s); torch.manual_seed(s)
  708. torch.cuda.manual_seed_all(s)
  709. torch.backends.cudnn.deterministic = True
  710. # run LOSO for DANN and NoDA (returns arrays shape (n_folds,))
  711. auucs_dann_s, gap_g_dann_s, gap_e_dann_s = loso_with_gaps(f"DANN_seed{s}", use_dann=True, max_ep=20)
  712. auucs_noda_s, gap_g_noda_s, gap_e_noda_s = loso_with_gaps(f"NoDA_seed{s}", use_dann=False, max_ep=20)
  713. # append (keep as 1-D arrays length n_folds)
  714. auucs_dann_list.append(np.asarray(auucs_dann_s).ravel())
  715. gap_g_dann_list.append(np.asarray(gap_g_dann_s).ravel())
  716. gap_e_dann_list.append(np.asarray(gap_e_dann_s).ravel())
  717. auucs_noda_list.append(np.asarray(auucs_noda_s).ravel())
  718. gap_g_noda_list.append(np.asarray(gap_g_noda_s).ravel())
  719. gap_e_noda_list.append(np.asarray(gap_e_noda_s).ravel())
  720. # Convert lists-of-arrays into single flattened arrays: length = n_seeds * n_folds
  721. auucs_dann_all = np.concatenate(auucs_dann_list) # shape (n_seeds * n_folds,)
  722. auucs_noda_all = np.concatenate(auucs_noda_list)
  723. gap_g_dann_all = np.concatenate(gap_g_dann_list)
  724. gap_g_noda_all = np.concatenate(gap_g_noda_list)
  725. gap_e_dann_all = np.concatenate(gap_e_dann_list)
  726. gap_e_noda_all = np.concatenate(gap_e_noda_list)
  727. # Optional: per-seed means (for reporting)
  728. auuc_dann_per_seed_mean = np.array([arr.mean() for arr in auucs_dann_list])
  729. auuc_noda_per_seed_mean = np.array([arr.mean() for arr in auucs_noda_list])
  730. print("Aggregated shapes:", auucs_dann_all.shape, gap_g_dann_all.shape, gap_e_dann_all.shape)
  731. print("Per-seed AUUC means (DANN):", auuc_dann_per_seed_mean)
  732. print("Per-seed AUUC means (NoDA):", auuc_noda_per_seed_mean)
  733. # %%
  734. # --- Utility: cliff's delta
  735. def cliffs_delta(x, y):
  736. x = np.asarray(x).ravel()
  737. y = np.asarray(y).ravel()
  738. n_x, n_y = len(x), len(y)
  739. greater = 0
  740. lesser = 0
  741. for xi in x:
  742. greater += np.sum(xi > y)
  743. lesser += np.sum(xi < y)
  744. return (greater - lesser) / (n_x * n_y)
  745. # --- Utility: bootstrap CI for paired median difference
  746. def bootstrap_paired_ci(a, b, stat_func=np.median, n_boot=10000, alpha=0.05, seed=0):
  747. rng = np.random.default_rng(seed)
  748. arr = np.array(a) - np.array(b)
  749. boot_stats = []
  750. n = len(arr)
  751. for _ in range(n_boot):
  752. idx = rng.integers(0, n, n)
  753. boot_stats.append(stat_func(arr[idx]))
  754. lo = np.percentile(boot_stats, 100 * (alpha/2))
  755. hi = np.percentile(boot_stats, 100 * (1 - alpha/2))
  756. return lo, hi
  757. auucs_dann = np.asarray(auucs_dann_all)
  758. auucs_noda = np.asarray(auucs_noda_all)
  759. gap_g_dann = np.asarray(gap_g_dann_all)
  760. gap_g_noda = np.asarray(gap_g_noda_all)
  761. gap_e_dann = np.asarray(gap_e_dann_all)
  762. gap_e_noda = np.asarray(gap_e_noda_all)
  763. n_folds = len(auucs_dann)
  764. fold_labels = [f"Fold {i+1}" for i in range(n_folds)]
  765. # -------------------------
  766. # Summary stats
  767. # -------------------------
  768. def summarize_pair(a, b, name):
  769. mean_a, std_a = a.mean(), a.std(ddof=1)
  770. mean_b, std_b = b.mean(), b.std(ddof=1)
  771. dif = a - b
  772. mean_diff = dif.mean()
  773. std_diff = dif.std(ddof=1)
  774. # Wilcoxon signed-rank test (paired nonparametric)
  775. try:
  776. stat, pval = wilcoxon(a, b)
  777. except Exception:
  778. stat, pval = np.nan, np.nan
  779. # Cliff's delta
  780. cd = cliffs_delta(a, b)
  781. # Bootstrap CI for paired median diff
  782. lo_ci, hi_ci = bootstrap_paired_ci(a, b, stat_func=np.median, n_boot=5000, seed=42)
  783. print(f"\n{name}:")
  784. print(f" mean A = {mean_a:.4f} ± {std_a:.4f}")
  785. print(f" mean B = {mean_b:.4f} ± {std_b:.4f}")
  786. print(f" mean paired diff (A-B) = {mean_diff:.4f} ± {std_diff:.4f}")
  787. print(f" Wilcoxon stat={stat:.4f}, p={pval:.4f}")
  788. print(f" Cliff's delta = {cd:.4f}")
  789. print(f" Paired median bootstrap 95% CI = [{lo_ci:.4f}, {hi_ci:.4f}]")
  790. return dict(mean_a=mean_a, std_a=std_a, mean_b=mean_b, std_b=std_b,
  791. mean_diff=mean_diff, std_diff=std_diff, pval=pval, cd=cd,
  792. median_ci=(lo_ci, hi_ci))
  793. res_auuc = summarize_pair(auucs_dann, auucs_noda, "AUUC (DANN vs NoDA)")
  794. res_gap_g = summarize_pair(gap_g_dann, gap_g_noda, "Gender gap |Δ uplift| (DANN vs NoDA)")
  795. res_gap_e = summarize_pair(gap_e_dann, gap_e_noda, "Education gap |Δ uplift| (DANN vs NoDA)")
  796. # -------------------------
  797. # Plots
  798. # -------------------------
  799. import matplotlib.pyplot as plt
  800. plt.style.use("seaborn-whitegrid")
  801. # 1) Mean AUUC bar with errorbars
  802. fig, ax = plt.subplots(figsize=(4.5,3.5))
  803. means = [auucs_dann.mean(), auucs_noda.mean()]
  804. stds = [auucs_dann.std(ddof=1), auucs_noda.std(ddof=1)]
  805. bars = ax.bar(["DANN", "NoDA"], means, yerr=stds, capsize=6, color=["#4e79a7","#e15759"])
  806. ax.set_ylabel("Mean LOSO-AUUC")
  807. ax.set_title("Mean LOSO-AUUC: DANN vs NoDA")
  808. for i, m in enumerate(means):
  809. ax.text(i, m + 0.02*max(means), f"{m:.3f}", ha="center")
  810. plt.tight_layout()
  811. plt.savefig("/kaggle/working/mean_loso_auuc_DANN_vs_NoDA.png", dpi=300)
  812. plt.show()
  813. plt.close(fig)
  814. # 2) Grouped fairness bars per fold (gender)
  815. width = 0.35
  816. x = np.arange(n_folds)
  817. fig, ax = plt.subplots(figsize=(6,3.2))
  818. ax.bar(x - width/2, gap_g_dann, width, label="DANN", color="#4e79a7")
  819. ax.bar(x + width/2, gap_g_noda, width, label="NoDA", color="#59a14f")
  820. ax.set_xticks(x); ax.set_xticklabels(fold_labels)
  821. ax.set_ylabel("|Δ uplift| (gender)"); ax.set_title("Gender fairness: DANN vs NoDA")
  822. ax.legend(); plt.tight_layout()
  823. plt.savefig("/kaggle/working/gender_gap_DANN_vs_NoDA_per_fold.png", dpi=300)
  824. plt.show()
  825. plt.close(fig)
  826. # 3) Grouped fairness bars per fold (education)
  827. fig, ax = plt.subplots(figsize=(6,3.2))
  828. ax.bar(x - width/2, gap_e_dann, width, label="DANN", color="#4e79a7")
  829. ax.bar(x + width/2, gap_e_noda, width, label="NoDA", color="#59a14f")
  830. ax.set_xticks(x); ax.set_xticklabels(fold_labels)
  831. ax.set_ylabel("|Δ uplift| (education)"); ax.set_title("Education fairness: DANN vs NoDA")
  832. ax.legend(); plt.tight_layout()
  833. plt.savefig("/kaggle/working/edu_gap_DANN_vs_NoDA_per_fold.png", dpi=300)
  834. plt.show()
  835. plt.close(fig)
  836. # 4) Combined summary panel (AUUC, mean gender gap, mean edu gap)
  837. fig, axes = plt.subplots(1,3, figsize=(12,3.5), gridspec_kw={"width_ratios":[1,1,1]})
  838. # AUUC
  839. axes[0].bar(["DANN","NoDA"], means, yerr=stds, color=["#4e79a7","#e15759"], capsize=6)
  840. axes[0].set_title("Mean LOSO-AUUC"); axes[0].set_ylabel("AUUC")
  841. # Gender mean gaps
  842. axes[1].bar(["DANN","NoDA"], [gap_g_dann.mean(), gap_g_noda.mean()], color=["#4e79a7","#59a14f"])
  843. axes[1].set_title("Mean Gender gap"); axes[1].set_ylabel("|Δ uplift|")
  844. # Education mean gaps
  845. axes[2].bar(["DANN","NoDA"], [gap_e_dann.mean(), gap_e_noda.mean()], color=["#4e79a7","#59a14f"])
  846. axes[2].set_title("Mean Education gap"); axes[2].set_ylabel("|Δ uplift|")
  847. plt.suptitle("DANN vs NoDA: AUUC and Fairness diagnostics")
  848. plt.tight_layout(rect=[0,0,1,0.95])
  849. plt.savefig("/kaggle/working/DANN_vs_NoDA_summary.png", dpi=300)
  850. plt.show()
  851. plt.close(fig)
  852. # 5) Paired difference histogram for AUUC
  853. fig, ax = plt.subplots(figsize=(5,3.2))
  854. diffs = auucs_dann - auucs_noda
  855. ax.hist(diffs, bins=8, color="#4e79a7", edgecolor="k")
  856. ax.axvline(0, color="k", linestyle="--")
  857. ax.set_xlabel("AUUC(DANN) - AUUC(NoDA)"); ax.set_title("Distribution of paired AUUC differences")
  858. plt.tight_layout()
  859. plt.savefig("/kaggle/working/paired_auuc_diff_hist.png", dpi=300)
  860. plt.show()
  861. plt.close(fig)
  862. print("\nPlots saved under /kaggle/working/.")
  863. # %%
  864. # ======================================================================
  865. # 0 · Reproducible seed
  866. # ======================================================================
  867. SEED = 42
  868. random.seed(SEED); np.random.seed(SEED); torch.manual_seed(SEED)
  869. torch.cuda.manual_seed_all(SEED); torch.backends.cudnn.deterministic = True
  870. # ======================================================================
  871. # 1 · Helper: build one model with/without domain loss
  872. # ======================================================================
  873. def build_model(use_dann: bool,
  874. lr = 1e-4,
  875. λ_D = 1.0,
  876. λ_F = 10.0):
  877. """Return a DANN_Uplift with λ_D = 0 (NoDA) or λ_D > 0 (DANN)."""
  878. λ_D_eff = λ_D if use_dann else 0.0
  879. return DANN_Uplift(d_in=X.shape[1],
  880. λ_D=λ_D_eff,
  881. λ_F=λ_F,
  882. lr=lr)
  883. # ======================================================================
  884. # 2 · LOSO evaluation (unchanged loaders / trainer settings)
  885. # ======================================================================
  886. def loso_scores(tag, use_dann, max_ep=20):
  887. gkf = GroupKFold(n_splits=3)
  888. scores = []
  889. for k,(tr,vl) in enumerate(gkf.split(np.zeros(len(y)), y, groups=domains),1):
  890. mdl = build_model(use_dann=use_dann)
  891. trL, vlL = make_loaders(tr, vl)
  892. trainer = pl.Trainer(
  893. max_epochs=max_ep,
  894. accelerator="gpu" if torch.cuda.is_available() else "cpu",
  895. devices=1,
  896. enable_checkpointing=False,
  897. callbacks=[pl.callbacks.EarlyStopping("val_auuc",
  898. mode="max",
  899. patience=5)],
  900. logger=False,
  901. log_every_n_steps=5,
  902. )
  903. trainer.fit(mdl, trL, vlL)
  904. auuc = trainer.callback_metrics["val_auuc"].item()
  905. print(f"{tag} fold{k}: AUUC = {auuc:+.3f}")
  906. scores.append(auuc)
  907. del mdl; gc.collect(); torch.cuda.empty_cache()
  908. print(f"{tag} mean AUUC = {np.mean(scores):+.3f}\n")
  909. return scores
  910. # ======================================================================
  911. # 3 · Run the comparison (same seed, same hyper-params)
  912. # ======================================================================
  913. scores_dann = loso_scores("DANN-128-64-d0.30", use_dann=True)
  914. scores_noda = loso_scores("NoDA-128-64-d0.30", use_dann=False)
  915. # Simple bar-plot
  916. import matplotlib.pyplot as plt
  917. plt.bar(["DANN", "NoDA"],
  918. [np.mean(scores_dann), np.mean(scores_noda)],
  919. color=["#4e79a7", "#e15759"])
  920. plt.ylabel("Mean LOSO-AUUC")
  921. plt.title("128-64 encoder, dropout 0.30")
  922. plt.savefig("/kaggle/working/loso_auUc_DANNvsNoDA.png", dpi=500)
  923. plt.savefig("/kaggle/working/loso_auUc_DANNvsNoDA.pdf", dpi=500)
  924. plt.show()
  925. # %%
  926. import matplotlib.pyplot as plt
  927. plt.figure(figsize=(5, 5))
  928. plt.bar(["DANN", "NoDA"],
  929. [np.mean(scores_dann), np.mean(scores_noda)],
  930. color=["#4e79a7", "#e15759"])
  931. plt.xlabel("Model Type",fontfamily='serif',fontsize=16)
  932. plt.ylabel("Mean LOSO-AUUC",fontfamily='serif',fontsize=16)
  933. # plt.title("128-64 encoder, dropout 0.30")
  934. # plt.grid(True, linestyle=':', linewidth=0.75, color='black')
  935. plt.grid(True, linestyle=(0, (1, 5)), linewidth=1.2, color='black')
  936. plt.tight_layout()
  937. plt.savefig("/kaggle/working/loso_AUUC_DANNvsNoDA.png", dpi=600)
  938. plt.savefig("/kaggle/working/loso_AUUC_DANNvsNoDA.pdf", dpi=600)
  939. plt.show()
  940. # %% [markdown]
  941. # # λD sweep: Confirms λD is a leverage knob for cross-domain benefit only when the causal sign is correct
  942. # %%
  943. SEED = 42
  944. random.seed(SEED); np.random.seed(SEED); torch.manual_seed(SEED)
  945. # ------------------------------------------------------------------
  946. # λ_D grid sweep (per-fold AUUC)
  947. # ------------------------------------------------------------------
  948. lambdas = np.arange(0.1, 0.9, 0.1) # 0.1 … 0.8
  949. grid_res = [] # rows: (λ_D, fold, AUUC)
  950. for lam in lambdas:
  951. mean_neg, folds = eval_cfg(lr=2e-4, λ_D=lam, λ_F=5.0, max_ep=15) # keep lr & λ_F fixed
  952. for k, score in enumerate(folds, 1):
  953. grid_res.append({"λ_D": lam, "fold": k, "AUUC": score})
  954. torch.cuda.empty_cache(); gc.collect()
  955. df_grid = pd.DataFrame(grid_res)
  956. # ---------- Plot ----------
  957. plt.figure(figsize=(6,4))
  958. for k in [1,2,3]:
  959. plt.plot(df_grid.query("fold==@k")["λ_D"],
  960. df_grid.query("fold==@k")["AUUC"],
  961. marker="o", label=f"Fold {k}")
  962. plt.axhline(0, color="grey", ls="--", lw=0.7)
  963. plt.xlabel("$\\lambda_D$")
  964. plt.ylabel("LOSO-AUUC")
  965. plt.title("λ_D sweep (encoder 128-64, d=0.30)")
  966. plt.legend();
  967. plt.savefig("/kaggle/working/λ_D sweep.png", dpi=500)
  968. plt.savefig("/kaggle/working/λ_D sweep.pdf", dpi=500)
  969. plt.tight_layout(); plt.show()
  970. # %%
  971. # Values taken from the output of above cell's output
  972. lambda_d = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8]
  973. data = {
  974. "λ_D": lambda_d * 3,
  975. "fold": [1]*8 + [2]*8 + [3]*8,
  976. "AUUC": [
  977. # fold 1 → Psychiatric (corrected)
  978. -3.688, -2.211, -3.900, -2.318, -2.460, -2.497, -2.874, -2.840,
  979. # fold 2 → Medical (unchanged)
  980. 1.964, 0.896, 1.867, 1.807, 3.693, -0.390, 1.249, 2.655,
  981. # fold 3 → Quarantine (corrected)
  982. 0.371, 1.125, -0.055, 2.729, 2.172, 0.376, 0.201, 2.451,
  983. ]
  984. }
  985. df_grid = pd.DataFrame(data)
  986. plt.figure(figsize=(6, 4))
  987. fold_names = {1: "Psychiatric", 2: "Medical", 3: "Quarantine"}
  988. for k in [1, 2, 3]:
  989. subset = df_grid.query("fold == @k")
  990. plt.plot(subset["λ_D"], subset["AUUC"], marker="o", label=fold_names[k])
  991. plt.xlabel("Adversarial weight λ_D", fontfamily='serif', fontsize=16)
  992. plt.ylabel("LODO-AUUC", fontfamily='serif', fontsize=16)
  993. plt.legend(title="Held-out domain")
  994. plt.grid(True, linestyle=(0, (1, 5)), linewidth=1.2, color='black')
  995. plt.tight_layout()
  996. plt.savefig("/kaggle/working/auuc_lambdaD_sweep.png", dpi=600)
  997. plt.savefig("/kaggle/working/auuc_lambdaD_sweep.pdf", dpi=600)
  998. plt.show()
  999. # %% [markdown]
  1000. # # Uplift curve on the Psychiatric test fold. The curve lies below the random baseline (AUUC ≈−2.66), indicating an inverted treatment-effect ranking.
  1001. # %%
  1002. SEED = 42
  1003. random.seed(SEED); np.random.seed(SEED); torch.manual_seed(SEED)
  1004. FOLD_BAD = 1
  1005. # Get the idx splits exactly as eval_cfg does
  1006. gkf = GroupKFold(n_splits=3)
  1007. splits = list(gkf.split(np.zeros(len(y)), y, groups=domains))
  1008. tr_idx, vl_idx = splits[FOLD_BAD-1]
  1009. # Re-train one model *only for diagnostics*
  1010. best_lam = 0.4 # put the λ_D
  1011. mdl = DANNSystem(d_in=X.shape[1], λ_D=best_lam, λ_F=5.0, lr=2e-4)
  1012. trL, vlL = make_loaders(tr_idx, vl_idx, bs=128)
  1013. pl.Trainer(max_epochs=20, devices=1, enable_checkpointing=False,
  1014. logger=False).fit(mdl, trL, vlL)
  1015. # ---------- uplifts & curve ----------
  1016. u, tv, yv = [], [], []
  1017. for xb,tb,yb,_,_,_ in vlL:
  1018. xb = xb.to(mdl.device)
  1019. with torch.no_grad():
  1020. z = mdl.encoder(xb)
  1021. u.append((mdl.head1(z)-mdl.head0(z)).cpu().numpy().ravel())
  1022. tv.append(tb.numpy().ravel()); yv.append(yb.numpy().ravel())
  1023. u, tv, yv = map(np.concatenate, [u, tv, yv])
  1024. xs, lift, area = uplift_curve(yv, tv, u, bins=10)
  1025. plt.figure(figsize=(6,4))
  1026. plt.plot(xs, lift, marker="o")
  1027. plt.axhline(0, color="grey", ls="--", lw=0.7)
  1028. plt.xlabel("top-x fraction"); plt.ylabel("net uplift")
  1029. plt.subplots_adjust(top=0.9)
  1030. plt.savefig("/kaggle/working/netupliftcurve_Psychiatric.png", dpi=500)
  1031. plt.savefig("/kaggle/working/netupliftcurve_Psychiatric.pdf", dpi=500)
  1032. plt.show()
  1033. # ---------- sanity checks ----------
  1034. print("T-rate in fold:", tv.mean().round(3))
  1035. print("Mean uplift sign:", np.sign(u).mean())
  1036. # %%
  1037. plt.figure(figsize=(6,3))
  1038. plt.plot(xs, lift, marker="o")
  1039. plt.axhline(0, color="grey", ls="--", lw=0.7)
  1040. plt.xlabel("Top-x fraction",fontfamily='serif', fontsize=16); plt.ylabel("Net uplift",fontfamily='serif', fontsize=16)
  1041. plt.ylim(-0.35, -0.250)
  1042. plt.grid(True, linestyle=(0, (1, 5)), linewidth=1.2, color='black')
  1043. plt.tight_layout();
  1044. plt.savefig("/kaggle/working/netupliftcurve_Psychiatric.pdf", dpi=600)
  1045. plt.show()
  1046. # %% [markdown]
  1047. # # Scheduled λD (GRL weight)- Linear, Sigmoid vs. fixed
  1048. # %%
  1049. # ───────────────────────────────────────────────────────────────
  1050. # 1 · Function that trains *one* configuration and returns mean AUUC
  1051. # + added model_cls hook so we can swap in scheduled-λ models
  1052. # ───────────────────────────────────────────────────────────────
  1053. def eval_cfg(lr, λ_D, λ_F, max_ep=20, # ← old args
  1054. model_cls=DANNSystem): # ← NEW arg, default stays DANN
  1055. gkf = GroupKFold(n_splits=3)
  1056. folds = gkf.split(np.zeros(len(y)), y, groups=domains)
  1057. auucs = []
  1058. for k, (tr, vl) in enumerate(folds, 1):
  1059. mdl = model_cls(d_in=X.shape[1], λ_D=λ_D, λ_F=λ_F, lr=lr)
  1060. trL, vL = make_loaders(tr, vl)
  1061. trainer = pl.Trainer(
  1062. max_epochs=max_ep,
  1063. accelerator="gpu" if torch.cuda.is_available() else "cpu",
  1064. devices=1, log_every_n_steps=5,
  1065. enable_checkpointing=False, gradient_clip_val=1.0,
  1066. callbacks=[pl.callbacks.EarlyStopping("val_auuc", mode="max", patience=5)],
  1067. )
  1068. trainer.fit(mdl, trL, vL)
  1069. best = trainer.callback_metrics["val_auuc"].item()
  1070. print(f" fold{k}: best_AUUC={best:+.3f}")
  1071. auucs.append(best)
  1072. del mdl; gc.collect(); torch.cuda.empty_cache()
  1073. mean, std = np.mean(auucs), np.std(auucs)
  1074. print(f"⇒ mean AUUC={mean:+.3f} ±{std:.3f}\n")
  1075. return -mean, auucs # Optuna still minimises
  1076. # %%
  1077. from functools import partial
  1078. # ───────────────────────────────────────────────────────────────
  1079. # 4 · Scheduled λ_D (linear & sigmoid) ── replaces fixed scalar
  1080. # ───────────────────────────────────────────────────────────────
  1081. def lambda_linear(p, max_lambda=0.6):
  1082. """Linear ramp from 0 → maxLambda over first half of training."""
  1083. return min(1.0, 2*p) * max_lambda # p = epoch / max_epochs
  1084. def lambda_sigmoid(p, max_lambda=0.6, k=10):
  1085. """Sigmoid ramp (original DANN paper style)."""
  1086. return max_lambda * 1/(1+np.exp(-k*(p-0.5)))
  1087. class DANN_Uplift_Scheduled(DANN_Uplift):
  1088. def __init__(self, d_in, schedule="linear", **kwargs):
  1089. super().__init__(d_in, **kwargs) # inherits λ_D as *max*
  1090. self.schedule = schedule
  1091. # override training_step to update GRL weight each epoch
  1092. def on_train_epoch_start(self):
  1093. prog = self.current_epoch / self.trainer.max_epochs
  1094. if self.schedule == "linear":
  1095. self.grl.l = lambda_linear(prog, self.hparams.λ_D)
  1096. else:
  1097. self.grl.l = lambda_sigmoid(prog, self.hparams.λ_D)
  1098. # helper to run one schedule
  1099. def run_schedule(schedule):
  1100. print(f"\n=== {schedule} schedule (λ_D max={best_lambda_d:.2f}) ===")
  1101. mean_neg,_ = eval_cfg(lr=best_lr, λ_D=best_lambda_d, λ_F=best_lambda_f,
  1102. max_ep=20, model_cls=partial(DANN_Uplift_Scheduled,
  1103. schedule=schedule))
  1104. print(f"Mean AUUC = {-mean_neg:.3f}")
  1105. return -mean_neg
  1106. # %%
  1107. # Use the best lr / λ_F from last Optuna run
  1108. best_lr, best_lambda_f, best_lambda_d = 2e-4,5.0,0.8 # Going with LAMBDA_D=0.8
  1109. fixed = -eval_cfg(best_lr, best_lambda_d, best_lambda_f, max_ep=20)[0]
  1110. lin = run_schedule("linear")
  1111. sig = run_schedule("sigmoid")
  1112. print(f"\nFixed={fixed:.3f} Linear={lin:.3f} Sigmoid={sig:.3f}")
  1113. # %%
  1114. # Use the best lr / λ_F from last Optuna run
  1115. best_lr, best_lambda_f, best_lambda_d = 2e-4,5.0,0.7 # Going with LAMBDA_D=0.7
  1116. fixed = -eval_cfg(best_lr, best_lambda_d, best_lambda_f, max_ep=20)[0]
  1117. lin = run_schedule("linear")
  1118. sig = run_schedule("sigmoid")
  1119. print(f"\nFixed={fixed:.3f} Linear={lin:.3f} Sigmoid={sig:.3f}")
  1120. # %%
  1121. # Use the best lr / λ_F from last Optuna run
  1122. best_lr, best_lambda_f, best_lambda_d = 2e-4,5.0,0.8 # Going with LAMBDA_D=0.8
  1123. fixed = -eval_cfg(best_lr, best_lambda_d, best_lambda_f, max_ep=20)[0]
  1124. lin = run_schedule("linear")
  1125. sig = run_schedule("sigmoid")
  1126. print(f"\nFixed={fixed:.3f} Linear={lin:.3f} Sigmoid={sig:.3f}")
  1127. # %% [markdown]
  1128. # # Single-Domain sanity check
  1129. # %%
  1130. # ───────────────────────────────────────────────────────────────
  1131. # 5 · Train-on-X, test-on-Y (no adaptation) for every pair
  1132. # ───────────────────────────────────────────────────────────────
  1133. pairs = [(0,1), (0,2), (1,0), (1,2), (2,0), (2,1)] # med↔quar↔psych
  1134. pair_scores = {}
  1135. for src, tgt in pairs:
  1136. tr_idx = np.where(domains == src)[0]
  1137. te_idx = np.where(domains == tgt)[0]
  1138. trL, vlL = make_loaders(tr_idx, te_idx, bs=128)
  1139. base = DANN_Uplift(d_in=X.shape[1], λ_D=0.0, λ_F=0.0, lr=best_lr)
  1140. trainer = pl.Trainer(max_epochs=15, enable_checkpointing=False,
  1141. accelerator="gpu" if torch.cuda.is_available() else "cpu",
  1142. devices=1, logger=False)
  1143. trainer.fit(base, trL, vlL)
  1144. auuc = trainer.callback_metrics['val_auuc'].item()
  1145. pair_scores[(src, tgt)] = auuc
  1146. print(f"train {src} → test {tgt} AUUC={auuc:+.3f}")
  1147. # Compare against same split using best-tuned DANN
  1148. print("\nRepeating with best-tuned DANN:")
  1149. for src, tgt in pairs:
  1150. tr_idx = np.where(domains == src)[0]
  1151. te_idx = np.where(domains == tgt)[0]
  1152. trL, vlL = make_loaders(tr_idx, te_idx, bs=128)
  1153. dann = DANN_Uplift(d_in=X.shape[1], λ_D=best_lambda_d,
  1154. λ_F=best_lambda_f, lr=best_lr)
  1155. trainer = pl.Trainer(max_epochs=15, enable_checkpointing=False,
  1156. accelerator="gpu" if torch.cuda.is_available() else "cpu",
  1157. devices=1, logger=False)
  1158. trainer.fit(dann, trL, vlL)
  1159. auuc = trainer.callback_metrics['val_auuc'].item()
  1160. print(f"DANN train {src} → test {tgt} AUUC={auuc:+.3f}")
  1161. # %% [markdown]
  1162. # # Semi-Synthetic benchmark (CATE RMSE)
  1163. # %%
  1164. # ───────────────────────────────────────────────────────────────
  1165. # 6 · Semi-synthetic: add synthetic uplift +0.30 for age<25, –0.10 else
  1166. # ───────────────────────────────────────────────────────────────
  1167. def make_semi_synth(df):
  1168. df2 = df.copy()
  1169. # true CATE δ(x): young → +0.3, older → –0.1
  1170. delta = np.where(df2['age'] < 25, 0.3, -0.1)
  1171. # generate synthetic outcome: y = y_orig ⊕ Bernoulli(δ * T)
  1172. noise = np.random.binomial(1, np.abs(delta)*df2['T'])
  1173. df2['Y_synth'] = (df2['Y'] ^ noise).astype(int)
  1174. df2['delta_true'] = delta
  1175. return df2
  1176. synth_df = make_semi_synth(df_all)
  1177. # … repeat pre-processing to get X_synth, y_synth, etc.
  1178. # Train DANN on synthetic labels
  1179. mdl = DANN_Uplift(d_in=X.shape[1], λ_D=best_lambda_d,
  1180. λ_F=0.0, lr=best_lr)
  1181. gkf = GroupKFold(3)
  1182. rmse = []
  1183. for tr, vl in gkf.split(np.zeros(len(synth_df)), y, groups=domains):
  1184. trL, vlL = make_loaders(tr, vl, bs=128)
  1185. trainer = pl.Trainer(max_epochs=15, enable_checkpointing=False,
  1186. accelerator="gpu" if torch.cuda.is_available() else "cpu",
  1187. devices=1, logger=False)
  1188. trainer.fit(mdl, trL, vlL)
  1189. preds = []
  1190. for xb,tb in DataLoader(torch.utils.data.TensorDataset(
  1191. torch.tensor(X[vl],dtype=torch.float32),
  1192. torch.tensor(T[vl],dtype=torch.float32)), batch_size=256):
  1193. xb,tb = xb.to(mdl.device), tb.to(mdl.device)
  1194. with torch.no_grad():
  1195. y0,y1,_ = mdl(xb,tb)
  1196. preds.append((y1-y0).cpu().numpy())
  1197. uplift_hat = np.concatenate(preds).ravel()
  1198. rmse.append(np.sqrt(((uplift_hat - synth_df['delta_true'].iloc[vl])**2).mean()))
  1199. print("Semi-synthetic CATE-RMSE per fold:", np.round(rmse,3),
  1200. " mean =", np.mean(rmse).round(3))
  1201. # %%
  1202. # ───────────────────────────────────────────────────────────────
  1203. # 6 · Semi-synthetic: add synthetic uplift +0.30 for age<25, –0.10 else
  1204. # ───────────────────────────────────────────────────────────────
  1205. def make_semi_synth(df):
  1206. df2 = df.copy()
  1207. # true CATE δ(x): young → +0.3, older → –0.1
  1208. delta = np.where(df2['age'] < 25, 0.3, -0.1)
  1209. # generate synthetic outcome: y = y_orig ⊕ Bernoulli(δ * T)
  1210. noise = np.random.binomial(1, np.abs(delta)*df2['T'])
  1211. df2['Y_synth'] = (df2['Y'] ^ noise).astype(int)
  1212. df2['delta_true'] = delta
  1213. return df2
  1214. synth_df = make_semi_synth(df_all)
  1215. # … repeat pre-processing to get X_synth, y_synth, etc.
  1216. # Train DANN on synthetic labels
  1217. mdl = DANN_Uplift(d_in=X.shape[1], λ_D=best_lambda_d,
  1218. λ_F=0.0, lr=best_lr)
  1219. gkf = GroupKFold(3)
  1220. rmse = []
  1221. for tr, vl in gkf.split(np.zeros(len(synth_df)), y, groups=domains):
  1222. trL, vlL = make_loaders(tr, vl, bs=128)
  1223. trainer = pl.Trainer(max_epochs=15, enable_checkpointing=False,
  1224. accelerator="gpu" if torch.cuda.is_available() else "cpu",
  1225. devices=1, logger=False)
  1226. trainer.fit(mdl, trL, vlL)
  1227. preds = []
  1228. for xb,tb in DataLoader(torch.utils.data.TensorDataset(
  1229. torch.tensor(X[vl],dtype=torch.float32),
  1230. torch.tensor(T[vl],dtype=torch.float32)), batch_size=256):
  1231. xb,tb = xb.to(mdl.device), tb.to(mdl.device)
  1232. with torch.no_grad():
  1233. y0,y1,_ = mdl(xb,tb)
  1234. preds.append((y1-y0).cpu().numpy())
  1235. uplift_hat = np.concatenate(preds).ravel()
  1236. rmse.append(np.sqrt(((uplift_hat - synth_df['delta_true'].iloc[vl])**2).mean()))
  1237. print("Semi-synthetic CATE-RMSE per fold:", np.round(rmse,3),
  1238. " mean =", np.mean(rmse).round(3))
  1239. # %% [markdown]
  1240. # # Comparison
  1241. # %%
  1242. # ---------- CORAL (Deep CORAL) ----------
  1243. def coral_loss(z: torch.Tensor, dom: torch.Tensor) -> torch.Tensor:
  1244. # z: (B, d), dom: (B,)
  1245. uniq = dom.unique()
  1246. if len(uniq) < 2:
  1247. return torch.zeros(1, device=z.device)
  1248. losses = []
  1249. for i in range(len(uniq)-1):
  1250. zi = z[dom == uniq[i]]
  1251. for j in range(i+1, len(uniq)):
  1252. zj = z[dom == uniq[j]]
  1253. if zi.shape[0] < 2 or zj.shape[0] < 2:
  1254. continue
  1255. # mean & covariance
  1256. mi, mj = zi.mean(0, keepdim=True), zj.mean(0, keepdim=True)
  1257. ci = torch.cov(zi.T) if zi.shape[0] > 1 else torch.zeros((z.shape[1], z.shape[1]), device=z.device)
  1258. cj = torch.cov(zj.T) if zj.shape[0] > 1 else torch.zeros((z.shape[1], z.shape[1]), device=z.device)
  1259. losses.append((mi - mj).pow(2).mean() + (ci - cj).pow(2).mean())
  1260. return sum(losses)/len(losses) if losses else torch.zeros(1, device=z.device)
  1261. # ---------- MMD (RBF kernel with median heuristic) ----------
  1262. def _rbf_mmd(x: torch.Tensor, y: torch.Tensor) -> torch.Tensor:
  1263. # pairwise squared distances
  1264. xx = torch.cdist(x, x, p=2).pow(2)
  1265. yy = torch.cdist(y, y, p=2).pow(2)
  1266. xy = torch.cdist(x, y, p=2).pow(2)
  1267. # median heuristic for gamma
  1268. with torch.no_grad():
  1269. med = torch.median(xy)
  1270. gamma = 1.0 / (med + 1e-6)
  1271. Kxx = torch.exp(-gamma * xx)
  1272. Kyy = torch.exp(-gamma * yy)
  1273. Kxy = torch.exp(-gamma * xy)
  1274. return Kxx.mean() + Kyy.mean() - 2.0 * Kxy.mean()
  1275. def mmd_loss(z: torch.Tensor, dom: torch.Tensor) -> torch.Tensor:
  1276. uniq = dom.unique()
  1277. if len(uniq) < 2:
  1278. return torch.zeros(1, device=z.device)
  1279. losses = []
  1280. for i in range(len(uniq)-1):
  1281. zi = z[dom == uniq[i]]
  1282. for j in range(i+1, len(uniq)):
  1283. zj = z[dom == uniq[j]]
  1284. if zi.shape[0] < 2 or zj.shape[0] < 2:
  1285. continue
  1286. losses.append(_rbf_mmd(zi, zj))
  1287. return sum(losses)/len(losses) if losses else torch.zeros(1, device=z.device)
  1288. # %%
  1289. class CDAN_Uplift(DANN_Uplift):
  1290. """Conditional DANN: discriminator sees features + predicted heads."""
  1291. def __init__(self, d_in, **kwargs):
  1292. super().__init__(d_in, **kwargs)
  1293. # infer encoder output size (64 in your 128-64 encoder; 16 in 32-16)
  1294. z_dim = self.head0.in_features
  1295. # rebuild discriminator to accept (z_dim + 2)
  1296. self.disc = nn.Sequential(
  1297. nn.Linear(z_dim + 2, 64), nn.ReLU(), nn.Linear(64, 3)
  1298. )
  1299. def forward(self, x, t):
  1300. z = self.encoder(x)
  1301. y0 = torch.sigmoid(self.head0(z))
  1302. y1 = torch.sigmoid(self.head1(z))
  1303. joint = torch.cat([z, y0, y1], dim=1) # (B, z_dim+2)
  1304. d_hat = self.disc(self.grl(joint))
  1305. return y0, y1, d_hat
  1306. class CORAL_Uplift(DANN_Uplift):
  1307. """Same as your model but domain penalty = CORAL (no discriminator used)."""
  1308. def training_step(self, batch, *_):
  1309. x, t, y, dom, g, e = batch
  1310. z = self.encoder(x)
  1311. y0 = torch.sigmoid(self.head0(z))
  1312. y1 = torch.sigmoid(self.head1(z))
  1313. # primary loss
  1314. eps = 1e-7
  1315. y_pred = torch.clip(torch.where(t == 1, y1, y0), eps, 1 - eps)
  1316. L_up = F.binary_cross_entropy(y_pred, y)
  1317. # CORAL alignment
  1318. L_dom = coral_loss(z, dom)
  1319. # fairness
  1320. uplift = (y1 - y0).flatten()
  1321. def safe_gap(ma, mb):
  1322. return ((uplift[ma].mean() - uplift[mb].mean())**2
  1323. if ma.any() and mb.any() else torch.zeros(1, device=x.device))
  1324. L_fair = safe_gap(g.flatten()==0, g.flatten()==1) + safe_gap(e.flatten()==0, e.flatten()==1)
  1325. loss = L_up + self.hparams.λ_D * L_dom + self.hparams.λ_F * L_fair
  1326. self.log_dict({"train_loss": loss, "train_uplift": L_up, "train_dom": L_dom, "train_fair": L_fair},
  1327. prog_bar=True, on_epoch=True)
  1328. return loss
  1329. class MMD_Uplift(DANN_Uplift):
  1330. """Same as your model but domain penalty = MMD (no discriminator used)."""
  1331. def training_step(self, batch, *_):
  1332. x, t, y, dom, g, e = batch
  1333. z = self.encoder(x)
  1334. y0 = torch.sigmoid(self.head0(z))
  1335. y1 = torch.sigmoid(self.head1(z))
  1336. eps = 1e-7
  1337. y_pred = torch.clip(torch.where(t == 1, y1, y0), eps, 1 - eps)
  1338. L_up = F.binary_cross_entropy(y_pred, y)
  1339. L_dom = mmd_loss(z, dom)
  1340. uplift = (y1 - y0).flatten()
  1341. def safe_gap(ma, mb):
  1342. return ((uplift[ma].mean() - uplift[mb].mean())**2
  1343. if ma.any() and mb.any() else torch.zeros(1, device=x.device))
  1344. L_fair = safe_gap(g.flatten()==0, g.flatten()==1) + safe_gap(e.flatten()==0, e.flatten()==1)
  1345. loss = L_up + self.hparams.λ_D * L_dom + self.hparams.λ_F * L_fair
  1346. self.log_dict({"train_loss": loss, "train_uplift": L_up, "train_dom": L_dom, "train_fair": L_fair},
  1347. prog_bar=True, on_epoch=True)
  1348. return loss
  1349. # %%
  1350. def eval_cfg_model(model_cls, lr, λ_D, λ_F, max_ep=10):
  1351. gkf = GroupKFold(n_splits=3)
  1352. auucs = []
  1353. for k,(tr,vl) in enumerate(gkf.split(np.zeros(len(y)), y, groups=domains),1):
  1354. mdl = model_cls(d_in=X.shape[1], λ_D=λ_D, λ_F=λ_F, lr=lr)
  1355. trL, vlL = make_loaders(tr, vl)
  1356. trainer = pl.Trainer(
  1357. max_epochs=max_ep,
  1358. accelerator="gpu" if torch.cuda.is_available() else "cpu",
  1359. devices=1, log_every_n_steps=5, enable_checkpointing=False,
  1360. gradient_clip_val=1.0,
  1361. callbacks=[pl.callbacks.EarlyStopping("val_auuc", mode="max", patience=5)],
  1362. )
  1363. trainer.fit(mdl, trL, vlL)
  1364. best = trainer.callback_metrics["val_auuc"].item()
  1365. print(f"[{model_cls.__name__}] fold{k}: AUUC={best:.3f}")
  1366. auucs.append(best)
  1367. del mdl; gc.collect(); torch.cuda.empty_cache()
  1368. mean,std = float(np.mean(auucs)), float(np.std(auucs))
  1369. print(f"⇒ {model_cls.__name__} mean AUUC={mean:.3f} ±{std:.3f}\n")
  1370. return mean, auucs
  1371. # %%
  1372. RESULTS_FILE = "da_results.json"
  1373. # Load existing results if available
  1374. if os.path.exists(RESULTS_FILE):
  1375. with open(RESULTS_FILE, "r") as f:
  1376. results = json.load(f)
  1377. else:
  1378. results = {}
  1379. # %%
  1380. SEED = 42
  1381. random.seed(SEED); np.random.seed(SEED); torch.manual_seed(SEED)
  1382. LR = 2e-4
  1383. LAMF = 3.0
  1384. grid = [0.1, 0.2, 0.4, 0.6]
  1385. # Choose one model to run per session
  1386. target_cls = DANNSystem
  1387. if target_cls.__name__ not in results:
  1388. best_mean, best_cfg = -1e9, None
  1389. for lamD in grid:
  1390. mean, _ = eval_cfg_model(target_cls, lr=LR, λ_D=lamD, λ_F=LAMF, max_ep=15)
  1391. if mean > best_mean:
  1392. best_mean, best_cfg = mean, {"λ_D": lamD}
  1393. results[target_cls.__name__] = {"mean": best_mean, "best": best_cfg}
  1394. # Save updated results
  1395. with open(RESULTS_FILE, "w") as f:
  1396. json.dump(results, f, indent=2)
  1397. # %%
  1398. SEED = 42
  1399. random.seed(SEED); np.random.seed(SEED); torch.manual_seed(SEED)
  1400. LR = 2e-4
  1401. LAMF = 3.0
  1402. grid = [0.1, 0.2, 0.4, 0.6]
  1403. # Choose one model to run per session
  1404. target_cls = CDAN_Uplift
  1405. if target_cls.__name__ not in results:
  1406. best_mean, best_cfg = -1e9, None
  1407. for lamD in grid:
  1408. mean, _ = eval_cfg_model(target_cls, lr=LR, λ_D=lamD, λ_F=LAMF, max_ep=15)
  1409. if mean > best_mean:
  1410. best_mean, best_cfg = mean, {"λ_D": lamD}
  1411. results[target_cls.__name__] = {"mean": best_mean, "best": best_cfg}
  1412. # Save updated results
  1413. with open(RESULTS_FILE, "w") as f:
  1414. json.dump(results, f, indent=2)
  1415. # %%
  1416. SEED = 42
  1417. random.seed(SEED); np.random.seed(SEED); torch.manual_seed(SEED)
  1418. LR = 2e-4
  1419. LAMF = 3.0
  1420. grid = [0.1, 0.2, 0.4, 0.6]
  1421. # Choose one model to run per session
  1422. target_cls = CORAL_Uplift
  1423. if target_cls.__name__ not in results:
  1424. best_mean, best_cfg = -1e9, None
  1425. for lamD in grid:
  1426. mean, _ = eval_cfg_model(target_cls, lr=LR, λ_D=lamD, λ_F=LAMF, max_ep=15)
  1427. if mean > best_mean:
  1428. best_mean, best_cfg = mean, {"λ_D": lamD}
  1429. results[target_cls.__name__] = {"mean": best_mean, "best": best_cfg}
  1430. # Save updated results
  1431. with open(RESULTS_FILE, "w") as f:
  1432. json.dump(results, f, indent=2)
  1433. # %%
  1434. SEED = 42
  1435. random.seed(SEED); np.random.seed(SEED); torch.manual_seed(SEED)
  1436. LR = 2e-4
  1437. LAMF = 3.0
  1438. grid = [0.1, 0.2, 0.4, 0.6]
  1439. # Choose one model to run per session
  1440. target_cls = MMD_Uplift
  1441. if target_cls.__name__ not in results:
  1442. best_mean, best_cfg = -1e9, None
  1443. for lamD in grid:
  1444. mean, _ = eval_cfg_model(target_cls, lr=LR, λ_D=lamD, λ_F=LAMF, max_ep=15)
  1445. if mean > best_mean:
  1446. best_mean, best_cfg = mean, {"λ_D": lamD}
  1447. results[target_cls.__name__] = {"mean": best_mean, "best": best_cfg}
  1448. # Save updated results
  1449. with open(RESULTS_FILE, "w") as f:
  1450. json.dump(results, f, indent=2)
  1451. # %%
  1452. if "NoDA" not in results:
  1453. nodA_mean, _ = eval_cfg_model(DANNSystem, lr=LR, λ_D=0.0, λ_F=LAMF, max_ep=15)
  1454. results["NoDA"] = {"mean": nodA_mean, "best": {"λ_D": 0.0}}
  1455. with open(RESULTS_FILE, "w") as f:
  1456. json.dump(results, f, indent=2)
  1457. # %%
  1458. METHODS = {
  1459. "NoDA": (DANNSystem, {"λ_D": [0.0]}),
  1460. "DANN": (DANNSystem, {"λ_D": [0.2,0.4,0.6]}),
  1461. "CDAN": (CDAN_Uplift, {"λ_D": [0.2,0.4,0.6]}),
  1462. "CORAL": (CORAL_Uplift, {"λ_D": [0.1,0.5,1.0]}),
  1463. "MMD": (MMD_Uplift, {"λ_D": [0.1,0.5,1.0]}),
  1464. }
  1465. LRS = [1e-4, 2e-4]
  1466. LAMFs = [0, 1, 3]
  1467. GRID = []
  1468. for name,(cls,hp) in METHODS.items():
  1469. for lr in LRS:
  1470. for lamF in LAMFs:
  1471. for lamD in hp["λ_D"]:
  1472. GRID.append((name,cls,{"lr":lr, "λ_F":lamF, "λ_D":lamD}))
  1473. best = {}
  1474. for name,cls,cfg in GRID:
  1475. mean,_ = eval_cfg_model(cls, max_ep=20, **cfg)
  1476. cur = best.get(name, {"mean": -1e9})
  1477. if mean > cur["mean"]:
  1478. best[name] = {"mean": mean, "cfg": cfg}
  1479. print("Best per method:", best)
  1480. # %%
  1481. # ============ Robust, fair comparison across methods ============
  1482. import numpy as np, random, torch, optuna
  1483. from optuna.samplers import TPESampler
  1484. from collections import defaultdict
  1485. from scipy.stats import wilcoxon
  1486. import pickle, traceback
  1487. SEEDS = list(range(5)) # 5 seeds
  1488. LR_GRID = [1e-4, 2e-4]
  1489. LAMF_GRID = [0, 1, 3]
  1490. LAMD_GRID = [0.2, 0.4, 0.6] # same for all methods
  1491. METHODS = {
  1492. "NoDA": DANNSystem, # we’ll set λ_D=0 when evaluating
  1493. "DANN": DANNSystem,
  1494. "CDAN": CDAN_Uplift,
  1495. "CORAL": CORAL_Uplift,
  1496. "MMD": MMD_Uplift,
  1497. }
  1498. def set_all_seeds(s):
  1499. random.seed(s); np.random.seed(s); torch.manual_seed(s)
  1500. torch.cuda.manual_seed_all(s)
  1501. torch.backends.cudnn.deterministic = True
  1502. torch.backends.cudnn.benchmark = False
  1503. def eval_one(method_cls, lr, lam_f, lam_d, seed, max_ep=10):
  1504. set_all_seeds(seed)
  1505. return eval_cfg_model(method_cls, lr=lr, λ_D=lam_d, λ_F=lam_f, max_ep=max_ep)
  1506. def best_on_grid(method_name, seed):
  1507. best_mean, best_cfg = -1e9, None
  1508. cls = METHODS[method_name]
  1509. for lr in LR_GRID:
  1510. for lam_f in LAMF_GRID:
  1511. for lam_d in ([0.0] if method_name=="NoDA" else LAMD_GRID):
  1512. mean, _ = eval_one(cls, lr, lam_f, lam_d, seed, max_ep=15)
  1513. if mean > best_mean:
  1514. best_mean = mean
  1515. best_cfg = {"lr": lr, "λ_F": lam_f, "λ_D": lam_d}
  1516. return best_mean, best_cfg
  1517. # Collect per-seed, per-fold AUUC for the *chosen* config per method
  1518. records = defaultdict(list) # method -> list of AUUC across (seeds × folds)
  1519. def checkpoint():
  1520. with open("/kaggle/working/checkpoint.pkl","wb") as f:
  1521. pickle.dump({"records":records, "configs":configs}, f)
  1522. configs = {} # method -> last best config (for reporting)
  1523. for s in SEEDS:
  1524. try:
  1525. for m in METHODS:
  1526. best_mean, cfg = best_on_grid(m, s)
  1527. _, folds = eval_cfg_model(METHODS[m], lr=cfg["lr"], λ_D=cfg["λ_D"], λ_F=cfg["λ_F"], max_ep=10)
  1528. records[m].extend(folds)
  1529. configs[m] = cfg
  1530. checkpoint()
  1531. except Exception as e:
  1532. print("Seed loop crashed:", e)
  1533. traceback.print_exc()
  1534. checkpoint()
  1535. continue
  1536. # Summaries
  1537. def mean_ci(xs):
  1538. xs = np.array(xs)
  1539. mu = xs.mean()
  1540. se = xs.std(ddof=1) / np.sqrt(len(xs))
  1541. ci95 = 1.96 * se
  1542. return mu, ci95, xs.std(ddof=1)
  1543. for m in METHODS:
  1544. mu, ci, sd = mean_ci(records[m])
  1545. print(f"{m:5s} mean={mu:+.3f} ±95%CI {ci:.3f} (sd={sd:.3f}) best_cfg≈{configs[m]}")
  1546. # Paired tests on matched seed×fold pairs, e.g., DANN vs NoDA
  1547. def paired_p(a, b):
  1548. return wilcoxon(np.array(a), np.array(b), zero_method="zsplit", correction=True).pvalue
  1549. for comp in [("DANN","NoDA"), ("CDAN","DANN"), ("CORAL","DANN"), ("MMD","DANN")]:
  1550. p = paired_p(records[comp[0]], records[comp[1]])
  1551. print(f"{comp[0]} vs {comp[1]} Wilcoxon p={p:.3f}")
  1552. # %%
  1553. import os, json, numpy as np, time
  1554. SEEDS = list(range(5)) # or 10
  1555. LRS = [1e-4, 2e-4]
  1556. LAMFs = [0, 1, 3]
  1557. METHODS = {
  1558. "NoDA": (DANNSystem, [0.0]),
  1559. "DANN": (DANNSystem, [0.2, 0.4, 0.6]),
  1560. "CDAN": (CDAN_Uplift, [0.2, 0.4, 0.6]),
  1561. "CORAL": (CORAL_Uplift,[0.1, 0.5, 1.0]),
  1562. "MMD": (MMD_Uplift, [0.1, 0.5, 1.0]),
  1563. }
  1564. RUN_METHOD = "DANN" # <<< change and rerun per method
  1565. cls, lamDs = METHODS[RUN_METHOD]
  1566. FOLDS_JL = f"/kaggle/working/folds_{RUN_METHOD}.jsonl"
  1567. done_seeds = set()
  1568. if os.path.exists(FOLDS_JL):
  1569. with open(FOLDS_JL) as f:
  1570. for line in f:
  1571. obj = json.loads(line)
  1572. done_seeds.add(obj["seed"])
  1573. for seed in SEEDS:
  1574. if seed in done_seeds:
  1575. print(f"[{RUN_METHOD}] seed {seed} already done; skipping.")
  1576. continue
  1577. best_mean, best_cfg, best_folds = -1e9, None, None
  1578. for lr in LRS:
  1579. for lamF in LAMFs:
  1580. for lamD in lamDs:
  1581. try:
  1582. mean, folds = eval_cfg_model(cls, lr=lr, λ_D=lamD, λ_F=lamF, max_ep=10)
  1583. if mean > best_mean:
  1584. best_mean, best_cfg, best_folds = mean, {"lr": lr, "λ_F": lamF, "λ_D": lamD}, folds
  1585. except Exception as e:
  1586. print(f"[{RUN_METHOD}] seed={seed} cfg={{'lr':{lr},'λ_F':{lamF},'λ_D':{lamD}}} ERROR: {e}")
  1587. if best_cfg is None:
  1588. print(f"[{RUN_METHOD}] seed {seed}: no successful runs.")
  1589. continue
  1590. rec = {"method": RUN_METHOD, "seed": seed,
  1591. "cfg": best_cfg, "mean": float(best_mean),
  1592. "folds": [float(x) for x in best_folds]}
  1593. with open(FOLDS_JL, "a") as f:
  1594. f.write(json.dumps(rec) + "\n")
  1595. print(f"[{RUN_METHOD}] seed {seed} best {best_cfg} → mean={best_mean:.3f}, folds={best_folds}")
  1596. # %%
  1597. import os, json, numpy as np, time
  1598. SEEDS = list(range(5)) # or 10
  1599. LRS = [1e-4, 2e-4]
  1600. LAMFs = [0, 1, 3]
  1601. METHODS = {
  1602. "NoDA": (DANNSystem, [0.0]),
  1603. "DANN": (DANNSystem, [0.2, 0.4, 0.6]),
  1604. "CDAN": (CDAN_Uplift, [0.2, 0.4, 0.6]),
  1605. "CORAL": (CORAL_Uplift,[0.2, 0.4, 0.6]),
  1606. "MMD": (MMD_Uplift, [0.2, 0.4, 0.6]),
  1607. }
  1608. RUN_METHOD = "CDAN" # <<< change and rerun per method
  1609. cls, lamDs = METHODS[RUN_METHOD]
  1610. FOLDS_JL = f"/kaggle/working/folds_{RUN_METHOD}.jsonl"
  1611. done_seeds = set()
  1612. if os.path.exists(FOLDS_JL):
  1613. with open(FOLDS_JL) as f:
  1614. for line in f:
  1615. obj = json.loads(line)
  1616. done_seeds.add(obj["seed"])
  1617. for seed in SEEDS:
  1618. if seed in done_seeds:
  1619. print(f"[{RUN_METHOD}] seed {seed} already done; skipping.")
  1620. continue
  1621. best_mean, best_cfg, best_folds = -1e9, None, None
  1622. for lr in LRS:
  1623. for lamF in LAMFs:
  1624. for lamD in lamDs:
  1625. try:
  1626. mean, folds = eval_cfg_model(cls, lr=lr, λ_D=lamD, λ_F=lamF, max_ep=10)
  1627. if mean > best_mean:
  1628. best_mean, best_cfg, best_folds = mean, {"lr": lr, "λ_F": lamF, "λ_D": lamD}, folds
  1629. except Exception as e:
  1630. print(f"[{RUN_METHOD}] seed={seed} cfg={{'lr':{lr},'λ_F':{lamF},'λ_D':{lamD}}} ERROR: {e}")
  1631. if best_cfg is None:
  1632. print(f"[{RUN_METHOD}] seed {seed}: no successful runs.")
  1633. continue
  1634. rec = {"method": RUN_METHOD, "seed": seed,
  1635. "cfg": best_cfg, "mean": float(best_mean),
  1636. "folds": [float(x) for x in best_folds]}
  1637. with open(FOLDS_JL, "a") as f:
  1638. f.write(json.dumps(rec) + "\n")
  1639. print(f"[{RUN_METHOD}] seed {seed} best {best_cfg} → mean={best_mean:.3f}, folds={best_folds}")
  1640. # %%
  1641. import os, json, numpy as np, time
  1642. SEEDS = list(range(5)) # or 10
  1643. LRS = [1e-4, 2e-4]
  1644. LAMFs = [0, 1, 3]
  1645. METHODS = {
  1646. "NoDA": (DANNSystem, [0.0]),
  1647. "DANN": (DANNSystem, [0.2, 0.4, 0.6]),
  1648. "CDAN": (CDAN_Uplift, [0.2, 0.4, 0.6]),
  1649. "CORAL": (CORAL_Uplift,[0.2, 0.4, 0.6]),
  1650. "MMD": (MMD_Uplift, [0.2, 0.4, 0.6]),
  1651. }
  1652. RUN_METHOD = "CORAL" # <<< change and rerun per method
  1653. cls, lamDs = METHODS[RUN_METHOD]
  1654. FOLDS_JL = f"/kaggle/working/folds_{RUN_METHOD}.jsonl"
  1655. done_seeds = set()
  1656. if os.path.exists(FOLDS_JL):
  1657. with open(FOLDS_JL) as f:
  1658. for line in f:
  1659. obj = json.loads(line)
  1660. done_seeds.add(obj["seed"])
  1661. for seed in SEEDS:
  1662. if seed in done_seeds:
  1663. print(f"[{RUN_METHOD}] seed {seed} already done; skipping.")
  1664. continue
  1665. best_mean, best_cfg, best_folds = -1e9, None, None
  1666. for lr in LRS:
  1667. for lamF in LAMFs:
  1668. for lamD in lamDs:
  1669. try:
  1670. mean, folds = eval_cfg_model(cls, lr=lr, λ_D=lamD, λ_F=lamF, max_ep=10)
  1671. if mean > best_mean:
  1672. best_mean, best_cfg, best_folds = mean, {"lr": lr, "λ_F": lamF, "λ_D": lamD}, folds
  1673. except Exception as e:
  1674. print(f"[{RUN_METHOD}] seed={seed} cfg={{'lr':{lr},'λ_F':{lamF},'λ_D':{lamD}}} ERROR: {e}")
  1675. if best_cfg is None:
  1676. print(f"[{RUN_METHOD}] seed {seed}: no successful runs.")
  1677. continue
  1678. rec = {"method": RUN_METHOD, "seed": seed,
  1679. "cfg": best_cfg, "mean": float(best_mean),
  1680. "folds": [float(x) for x in best_folds]}
  1681. with open(FOLDS_JL, "a") as f:
  1682. f.write(json.dumps(rec) + "\n")
  1683. print(f"[{RUN_METHOD}] seed {seed} best {best_cfg} → mean={best_mean:.3f}, folds={best_folds}")
  1684. # %%
  1685. import os, json, numpy as np, time
  1686. SEEDS = list(range(5)) # or 10
  1687. LRS = [1e-4, 2e-4]
  1688. LAMFs = [0, 1, 3]
  1689. METHODS = {
  1690. "NoDA": (DANNSystem, [0.0]),
  1691. "DANN": (DANNSystem, [0.2, 0.4, 0.6]),
  1692. "CDAN": (CDAN_Uplift, [0.2, 0.4, 0.6]),
  1693. "CORAL": (CORAL_Uplift,[0.2, 0.4, 0.6]),
  1694. "MMD": (MMD_Uplift, [0.2, 0.4, 0.6]),
  1695. }
  1696. RUN_METHOD = "MMD" # <<< change and rerun per method
  1697. cls, lamDs = METHODS[RUN_METHOD]
  1698. FOLDS_JL = f"/kaggle/working/folds_{RUN_METHOD}.jsonl"
  1699. done_seeds = set()
  1700. if os.path.exists(FOLDS_JL):
  1701. with open(FOLDS_JL) as f:
  1702. for line in f:
  1703. obj = json.loads(line)
  1704. done_seeds.add(obj["seed"])
  1705. for seed in SEEDS:
  1706. if seed in done_seeds:
  1707. print(f"[{RUN_METHOD}] seed {seed} already done; skipping.")
  1708. continue
  1709. best_mean, best_cfg, best_folds = -1e9, None, None
  1710. for lr in LRS:
  1711. for lamF in LAMFs:
  1712. for lamD in lamDs:
  1713. try:
  1714. mean, folds = eval_cfg_model(cls, lr=lr, λ_D=lamD, λ_F=lamF, max_ep=10)
  1715. if mean > best_mean:
  1716. best_mean, best_cfg, best_folds = mean, {"lr": lr, "λ_F": lamF, "λ_D": lamD}, folds
  1717. except Exception as e:
  1718. print(f"[{RUN_METHOD}] seed={seed} cfg={{'lr':{lr},'λ_F':{lamF},'λ_D':{lamD}}} ERROR: {e}")
  1719. if best_cfg is None:
  1720. print(f"[{RUN_METHOD}] seed {seed}: no successful runs.")
  1721. continue
  1722. rec = {"method": RUN_METHOD, "seed": seed,
  1723. "cfg": best_cfg, "mean": float(best_mean),
  1724. "folds": [float(x) for x in best_folds]}
  1725. with open(FOLDS_JL, "a") as f:
  1726. f.write(json.dumps(rec) + "\n")
  1727. print(f"[{RUN_METHOD}] seed {seed} best {best_cfg} → mean={best_mean:.3f}, folds={best_folds}")
  1728. # %%
  1729. import os, json, numpy as np, time
  1730. SEEDS = list(range(5)) # or 10
  1731. LRS = [1e-4, 2e-4]
  1732. LAMFs = [0, 1, 3]
  1733. METHODS = {
  1734. "NoDA": (DANNSystem, [0.0]),
  1735. "DANN": (DANNSystem, [0.2, 0.4, 0.6]),
  1736. "CDAN": (CDAN_Uplift, [0.2, 0.4, 0.6]),
  1737. "CORAL": (CORAL_Uplift,[0.2, 0.4, 0.6]),
  1738. "MMD": (MMD_Uplift, [0.2, 0.4, 0.6]),
  1739. }
  1740. RUN_METHOD = "NoDA" # <<< change and rerun per method
  1741. cls, lamDs = METHODS[RUN_METHOD]
  1742. FOLDS_JL = f"/kaggle/working/folds_{RUN_METHOD}.jsonl"
  1743. done_seeds = set()
  1744. if os.path.exists(FOLDS_JL):
  1745. with open(FOLDS_JL) as f:
  1746. for line in f:
  1747. obj = json.loads(line)
  1748. done_seeds.add(obj["seed"])
  1749. for seed in SEEDS:
  1750. if seed in done_seeds:
  1751. print(f"[{RUN_METHOD}] seed {seed} already done; skipping.")
  1752. continue
  1753. best_mean, best_cfg, best_folds = -1e9, None, None
  1754. for lr in LRS:
  1755. for lamF in LAMFs:
  1756. for lamD in lamDs:
  1757. try:
  1758. mean, folds = eval_cfg_model(cls, lr=lr, λ_D=lamD, λ_F=lamF, max_ep=10)
  1759. if mean > best_mean:
  1760. best_mean, best_cfg, best_folds = mean, {"lr": lr, "λ_F": lamF, "λ_D": lamD}, folds
  1761. except Exception as e:
  1762. print(f"[{RUN_METHOD}] seed={seed} cfg={{'lr':{lr},'λ_F':{lamF},'λ_D':{lamD}}} ERROR: {e}")
  1763. if best_cfg is None:
  1764. print(f"[{RUN_METHOD}] seed {seed}: no successful runs.")
  1765. continue
  1766. rec = {"method": RUN_METHOD, "seed": seed,
  1767. "cfg": best_cfg, "mean": float(best_mean),
  1768. "folds": [float(x) for x in best_folds]}
  1769. with open(FOLDS_JL, "a") as f:
  1770. f.write(json.dumps(rec) + "\n")
  1771. print(f"[{RUN_METHOD}] seed {seed} best {best_cfg} → mean={best_mean:.3f}, folds={best_folds}")
  1772. # %%
  1773. import json, numpy as np
  1774. from scipy.stats import wilcoxon
  1775. def load_folds(method):
  1776. folds = []
  1777. with open(f"/kaggle/input/folds-results/folds_{method}.jsonl") as f:
  1778. for line in f:
  1779. obj = json.loads(line)
  1780. folds.extend(obj["folds"]) # 3 folds per seed
  1781. return folds
  1782. METHODS_ORDER = ["NoDA","DANN","CDAN","CORAL","MMD"]
  1783. records = {m: load_folds(m) for m in METHODS_ORDER}
  1784. def mean_ci(xs):
  1785. xs = np.array(xs); mu = xs.mean()
  1786. se = xs.std(ddof=1)/np.sqrt(len(xs)) if len(xs)>1 else 0.0
  1787. return mu, 1.96*se, xs.std(ddof=1) if len(xs)>1 else 0.0
  1788. for m in METHODS_ORDER:
  1789. mu, ci, sd = mean_ci(records[m])
  1790. print(f"{m:5s} mean={mu:+.3f} ±{ci:.3f} (sd={sd:.3f}) n={len(records[m])}")
  1791. def paired(a, b):
  1792. n = min(len(a), len(b))
  1793. return wilcoxon(np.array(a[:n]), np.array(b[:n]), zero_method="zsplit", correction=True).pvalue
  1794. for comp in [("DANN","NoDA"), ("CDAN","DANN"), ("CORAL","DANN"), ("MMD","DANN")]:
  1795. print(f"{comp[0]} vs {comp[1]} p={paired(records[comp[0]], records[comp[1]]):.4f}")
  1796. # %%
  1797. import json, numpy as np, pandas as pd
  1798. from pathlib import Path
  1799. def load_folds(method):
  1800. path = Path(f"/kaggle/input/folds-results/folds_{method}.jsonl")
  1801. rows = []
  1802. with path.open() as f:
  1803. for line in f:
  1804. o = json.loads(line)
  1805. for i,auuc in enumerate(o["folds"]):
  1806. rows.append({"method": method, "seed": o["seed"], "fold": i, "auuc": float(auuc)})
  1807. return pd.DataFrame(rows)
  1808. methods = ["NoDA","DANN","CDAN","CORAL","MMD"]
  1809. dfs = [load_folds(m) for m in methods]
  1810. df = pd.concat(dfs, ignore_index=True)
  1811. # Make paired table for DANN vs NoDA
  1812. pivot = df.pivot_table(index=["seed","fold"], columns="method", values="auuc")
  1813. paired = pivot.dropna(subset=["NoDA","DANN"]).reset_index()
  1814. paired["diff"] = paired["DANN"] - paired["NoDA"]
  1815. paired.head()
  1816. # %%
  1817. import matplotlib.pyplot as plt
  1818. import numpy as np
  1819. # Define method order and colors
  1820. methods = ["NoDA", "DANN", "CDAN", "CORAL", "MMD"]
  1821. colors = ["#4e79a7", "#e15759", "#76b7b2", "#f28e2b", "#59a14f"]
  1822. # Prepare data
  1823. data = [df[df.method == m]["auuc"].values for m in methods]
  1824. # Create figure
  1825. plt.figure(figsize=(5, 5))
  1826. # Violin plot
  1827. vp = plt.violinplot(data, showmeans=True, showextrema=False)
  1828. # Color violins
  1829. for i, pc in enumerate(vp['bodies']):
  1830. pc.set_facecolor(colors[i])
  1831. pc.set_edgecolor("black")
  1832. pc.set_alpha(0.7)
  1833. # Boxplot overlay
  1834. plt.boxplot(data, positions=range(1, len(methods) + 1), widths=0.2, patch_artist=True,
  1835. boxprops=dict(facecolor='white', color='black'),
  1836. medianprops=dict(color='black'))
  1837. # Jittered individual points
  1838. for i, y in enumerate(data):
  1839. x = np.random.normal(i + 1, 0.04, size=len(y))
  1840. plt.plot(x, y, 'k.', alpha=0.3)
  1841. # Axis labels and title
  1842. plt.xticks(range(1, len(methods) + 1), methods)
  1843. plt.xticks(fontsize=12) # x-axis numbers
  1844. plt.yticks(fontsize=12) # y-axis numbers
  1845. plt.xlabel("Model Type",fontfamily='serif', fontsize=16)
  1846. plt.ylabel("AUUC",fontfamily='serif', fontsize=16)
  1847. #plt.title("Distribution of LOSO-AUUC by Model Type")
  1848. # plt.grid(linestyle=':', linewidth=0.75, color='black')
  1849. plt.grid(True, linestyle=(0, (1, 5)), linewidth=1.2, color='black')
  1850. plt.tight_layout()
  1851. # Save outputs
  1852. plt.savefig("/kaggle/working/auuc_violin_by_method.png", dpi=500)
  1853. plt.savefig("/kaggle/working/auuc_violin_by_method.pdf", dpi=500)
  1854. plt.show()
  1855. # %%
  1856. plt.figure(figsize=(6,4))
  1857. plt.hist(paired["diff"], bins=10, edgecolor="k", alpha=0.8)
  1858. # plt.grid(linestyle=':', linewidth=0.85, color='black')
  1859. plt.grid(True, linestyle=(0, (1, 5)), linewidth=1.2, color='black')
  1860. plt.xticks(fontsize=12) # x-axis numbers
  1861. plt.yticks(fontsize=12) # y-axis numbers
  1862. plt.xlabel("AUUC(DANN) − AUUC(NoDA)",fontfamily='serif', fontsize=16)
  1863. plt.ylabel("Count",fontfamily='serif', fontsize=16)
  1864. # plt.title("Distribution of paired differences")
  1865. plt.tight_layout()
  1866. plt.savefig("/kaggle/working/diff_hist_DANN_NoDA.png", dpi=600)
  1867. plt.savefig("/kaggle/working/diff_hist_DANN_NoDA.pdf", dpi=600)
  1868. plt.show()
  1869. # %%
  1870. import numpy as np
  1871. def bootstrap_ci(arr, iters=2000, func=np.median, alpha=0.05):
  1872. arr = np.array(arr)
  1873. boots = [func(np.random.choice(arr, size=len(arr), replace=True)) for _ in range(iters)]
  1874. lo, hi = np.percentile(boots, [100*alpha/2, 100*(1-alpha/2)])
  1875. return func(arr), lo, hi
  1876. def cliffs_delta(a, b):
  1877. a = np.array(a); b = np.array(b)
  1878. gt = sum(x>y for x in a for y in b)
  1879. lt = sum(x<y for x in a for y in b)
  1880. n = len(a)*len(b)
  1881. return (gt - lt)/n
  1882. median, lo, hi = bootstrap_ci(paired["diff"])
  1883. delta = cliffs_delta(paired["DANN"].values, paired["NoDA"].values)
  1884. print(f"Median paired diff={median:.3f} [{lo:.3f},{hi:.3f}] Cliff's Δ={delta:.3f}")

dann-analysis.ipynb at commit 2f5e847, no license · at the source

Overview

Authors: Abdullah Nayem Wasi Emran1,2, A. B. M. Alim Al Islam1
  1. Department of Computer Science and Engineering, Bangladesh University of Engineering and Technology (BUET), Dhaka, Bangladesh
  2. Department of Computer Science and Engineering, BRAC University, Dhaka, Bangladesh
Journal: iScience, volume 29, issue 6, article 116027
Dates: received 18 February 2026; accepted 4 May 2026; published online 22 May 2026
Type: Research article · Language: English
License: CC BY
Identifiers: DOI 10.1016/j.isci.2026.116027 · PMID 42231948 · PMCID PMC13224034 · OpenAlex W7162112417
Open access: gold, a free copy (OpenAlex)
Status: code verified
Categories: human (organism), clinical / translational (subfield)
Methods: Connectivity, Statistics, Machine learning
Keywords: Neuroscience, Machine learning, Psychology
Topic: Mental Health Research Topics (Experimental and Cognitive Psychology, Psychology), according to OpenAlex
Funding: BUET
Citations: not cited yet (Europe PMC); 69 references in the paper

Abstract

Predicting who will deteriorate under stress is important for targeting mental-health support; yet, treatment-effect models are rarely tested across populations. We evaluate a domain-adaptive neural uplift model on three heterogeneous cohorts—medical students, members of the general public under quarantine, and psychiatric patients (n = 2,624). The model combines a shared encoder, two potential-outcome heads, a domain discriminator, and an optional fairness penalty. We compare no-adaptation training with four domain-adaptation mechanisms under a leave-one-domain-out protocol, using AUUC and a semi-synthetic benchmark with known treatment effects. The model achieves positive uplift ranking in two cohorts, while the psychiatric cohort shows sign inversion of the effect proxy. Adaptation yields modest, tuning-sensitive gains over a strong baseline. These results clarify when domain adaptation helps treatment-effect ranking under distribution shift and inform cautious deployment across cohorts.

Reproduced under the paper's license (CC BY), from the paper cited above.

Repository

Its files are read in the Code ↔ Paper reader above, with 10 matches between paragraphs and lines of code.

Nayem9274/domain-adaptive-uplift-mh

License: none: the authors keep all their rights
State: the link answers, verified on 28 September 2026
Evidence: files inventoried
Commit: 2f5e847a6f246880c9e33699351f8f3c3ca63f38, 14 April 2026
Languages: Jupyter (1)
Size: 3 files, 1 script
Software Heritage: not archived
Found in: “Data and code availability”
Holds: README, environment (requirements.txt), 1 notebook
Not found: license file, CITATION.cff, tests, continuous integration, documentation
Tools: PyTorch Lightning (1 file), Matplotlib (1 file), NumPy (1 file), pandas (1 file), PyTorch (1 file), scikit-learn (1 file), SciPy (1 file), seaborn (1 file), statsmodels (1 file)
Availability: 1 check, the latest on 28 September 2026: the link answers
  • 28 September 2026: the link answers
2 files

The paper's code and data availability statement is in the Data section.

Tracing map

Proposed by the machine: these links were found in the paper and verified at the source, without human review. The map will receive a Zenodo DOI once one of the paper's authors has validated it with their ORCID.

What the map holds:

  • 1 repository of the authors' code, each at its verified commit, with its license and how the link was found in the paper;
  • 1 script, each with its path and the digest of its content;
  • 10 matches between paragraphs of the paper and lines of the code (method lexical-v1);
  • neither the text of the paper nor the code itself.

Its JSON (tracing-map.json) is deposited on Zenodo with its DOI once the map is validated.

Data

Datasets cited

Data and code availability

• All datasets analyzed in this study are publicly available from third-party repositories and were not generated by the authors. The Identification of Major Psychiatric Disorders Dataset is available at https://osf.io/8bsvr/. The Medical Students Mental Health Dataset is available at https://zenodo.org/records/5702895. The Mental Health Depression During Quarantine Life Dataset is available at https://data.mendeley.com/datasets/pxjmjyfdh2/1. All datasets are openly accessible subject to the terms of their respective repositories. • The code generated during this study is publicly available at https://github.com/Nayem9274/domain-adaptive-uplift-mh. The repository contains the analysis notebook and a requirements.txt specifying all software dependencies needed to reproduce the reported experiments. • No new datasets were generated or deposited as part of this study. This study does not report original data.

Reproduced under the paper's license (CC BY), from the paper cited above.

Versions

The history of this record: each version stored by the harvester or made by a correction of its authors or of the maintainers of its code, and what changed in its facts. The texts of the paper (its abstract, its availability statements) are not part of it; versions that changed only those are not listed.

Version 1, 28 September 2026: the first record

Recorded: type, language, journal, volume, issue, pages, dates, 2 authors, 3 keywords, 1 funder, 41 references.

Cite

This paper

Emran, A. N. W., & Al Islam, A. B. M. A. (2026). Domain adaptive uplift modeling across heterogeneous mental health cohorts. iScience, 29(6), 116027. https://doi.org/10.1016/j.isci.2026.116027

BibTeX

@article{emran2026domain,
author = {Emran, Abdullah Nayem Wasi and Al Islam, A. B. M. Alim},
title = {{Domain adaptive uplift modeling across heterogeneous mental health cohorts}},
journal = {iScience},
year = {2026},
month = may,
volume = {29},
number = {6},
pages = {116027},
publisher = {Elsevier},
issn = {2589-0042},
doi = {10.1016/j.isci.2026.116027},
url = {https://doi.org/10.1016/j.isci.2026.116027},
pmid = {42231948},
pmcid = {PMC13224034}
}

RIS

TY - JOUR
AU - Emran, Abdullah Nayem Wasi
AU - Al Islam, A. B. M. Alim
TI - Domain adaptive uplift modeling across heterogeneous mental health cohorts
T2 - iScience
J2 - iScience
PY - 2026
DA - 2026/05/22
VL - 29
IS - 6
SP - 116027
SN - 2589-0042
PB - Elsevier
DO - 10.1016/j.isci.2026.116027
UR - https://doi.org/10.1016/j.isci.2026.116027
LA - en
ER -

CSL-JSON

{
"id": "10.1016/j.isci.2026.116027",
"type": "article-journal",
"title": "Domain adaptive uplift modeling across heterogeneous mental health cohorts",
"container-title": "iScience",
"author": [
{
"family": "Emran",
"given": "Abdullah Nayem Wasi"
},
{
"family": "Al Islam",
"given": "A. B. M. Alim"
}
],
"container-title-short": "iScience",
"volume": "29",
"issue": "6",
"page": "116027",
"DOI": "10.1016/j.isci.2026.116027",
"PMID": "42231948",
"PMCID": "PMC13224034",
"ISSN": "2589-0042",
"publisher": "Elsevier",
"URL": "https://doi.org/10.1016/j.isci.2026.116027",
"language": "en",
"issued": {
"date-parts": [
[
2026,
5,
22
]
]
}
}

The tracing map gets a citation of its own once an author has validated it and it has a DOI.

Similar papers

The papers with a page that share the most with this one: the tools found in their code, their categories, datasets, cited references and authors, the rarest counting most.

[1] doi:10.1038/s42003-026-10957-8 [code]
Brain defence by the extracellular matrix protein Cochlin.
Journal: Communications biology
In common: PyTorch Lightning, statsmodels, PyTorch, 6 other tools
[2] doi:10.1093/bioinformatics/btag652 [code]
mmVelo: a deep generative model for estimating cell state-dependent dynamics across multiple modalities.
Journal: Bioinformatics (Oxford, England)
In common: PyTorch Lightning, statsmodels, PyTorch, 6 other tools
[3] doi:10.1038/s41467-026-75455-1 [code]
Shared latent representations of speech production for cross-patient speech decoding.
Journal: Nature communications
In common: PyTorch Lightning, statsmodels, PyTorch, 6 other tools
[4] doi:10.1038/s41467-026-73996-z [code]
Genetic architecture of white matter microstructure captured by unsupervised deep representation learning of fractional anisotropy maps.
Journal: Nature communications
In common: PyTorch Lightning, statsmodels, PyTorch, 6 other tools
[5] doi:10.1162/imag.a.1207 [code]
Investigating the temporal dynamics and modeling of mid-level feature representations in humans.
Journal: Imaging neuroscience (Cambridge, Mass.)
In common: PyTorch Lightning, statsmodels, PyTorch, 6 other tools
[6] doi:10.1038/s41562-026-02414-7 [code]
Optimized feature gains explain and predict successes and failures of human selective listening.
Journal: Nature human behaviour
In common: PyTorch Lightning, statsmodels, PyTorch, 6 other tools
[7] doi:10.1523/eneuro.0362-25.2026 [code]
Similarities between &lt;i&gt;Ciona&lt;/i&gt; Dorsal Motor Ganglion and Vertebrate Cerebellum: Did a Chordate Ancestor Already Show D/V Subdivision within a Hindbrain Precursor?
Journal: eNeuro
In common: PyTorch Lightning, statsmodels, PyTorch, 6 other tools
[8] doi:10.1038/s41467-026-76939-w [code]
HIPPIE: a generative model for electrophysiological analysis across species, technologies, and modalities.
Journal: Nature communications
In common: PyTorch Lightning, statsmodels, PyTorch, 6 other tools
[9] doi:10.64898/2026.03.02.709014 [code]
A Protein Language Model Reveals Organellar Ca <sup>2+</sup> ATPases at Neuronal Synapses
Journal: bioRxiv (preprint)
In common: PyTorch Lightning, statsmodels, PyTorch, 6 other tools
[10] doi:10.1038/s43856-026-01722-3 [code]
Local and global patterns support medical imaging as a biomarker of ageing.
Journal: Communications medicine
In common: PyTorch Lightning, statsmodels, PyTorch, 5 other tools, clinical / translational

Contribute

The authors of this paper can claim it, correct its record and validate its tracing map, and the maintainers of its code (its owner, or a public member of its organization) correct what it says of their repository; anyone signed in can ask for its removal. Every request goes to OSCR's own machine, which answers it; your account page follows them.

Sign in with ORCID to claim this paper as one of its authors, correct its record or validate its tracing map: when the paper's metadata lists your ORCID iD, you are recognized at once. Maintainers of its code: sign in with GitHub, then claim the repository on your account page.

Request its removal

To ask OSCR to remove this record, the copies of its authors' scripts or its tracing map, use the removal request page: signed in, you say who you are, what to remove and why, then review and confirm the request. Published rules decide every request (how).

Discussion, reproductions, activity

Discussion: questions and error reports about this paper and its code, from signed-in readers and its authors. It opens with sign-in.

Reproductions: reports from readers who ran the authors' code: what they reproduced, with which environment, commit and data. It opens with sign-in.

Activity: what happens around this paper: new versions of its record, its map's validation, discussions and reproductions. It opens with sign-in.