Firemedic15's picture
download
raw
26.6 kB
# /// script
# requires-python = ">=3.11"
# dependencies = [
# "numpy>=1.26",
# "pandas>=2.2",
# "scikit-learn>=1.4",
# "scipy>=1.12",
# "plotly>=5.20",
# "pyarrow>=15.0",
# "huggingface_hub>=0.24",
# ]
# ///
"""
Synthetic-proxy reproduction of the offline-experiments claim in
"Large-Scale Notification Dispatch with Bundle Treatments and Multi-Outcome
Uplift Optimization" (ICML 2026, OpenReview 8GH752ZJ5j, BUOPLR / Kuaishou).
No public code, data, or arXiv version exists for this paper. This script
builds a self-contained simulator of the paper's setting (combinatorial
bundle treatments, multiple outcomes, global budget/quota/fatigue
constraints) and implements BUOPLR's two-stage algorithmic idea:
Stage 1 (uplift estimation): a shared multi-task network predicting all
outcomes jointly from (features, bundle) so it can capture cross-treatment
and cross-outcome structure.
Stage 2 (assignment): restrict each user's candidate set to their top-C
bundles, then solve the global budget/quota/fatigue constraints via
Lagrangian relaxation (dual subgradient ascent) instead of an exact LP.
These are compared against baseline uplift estimators (linear-additive,
independent-per-outcome MLP) and baseline assignment algorithms (greedy
myopic, exact LP, no-treatment control) on matched synthetic ground truth.
"""
from __future__ import annotations
import argparse
import itertools
import json
import time
from pathlib import Path
import numpy as np
import pandas as pd
from sklearn.linear_model import LinearRegression
from sklearn.neural_network import MLPRegressor
OUTCOMES = ["main", "cost", "fatigue"]
K = 4 # number of binary notification "slots" (timing/style dimensions)
MAX_ACTIVE = 2 # restrict bundles to at most this many simultaneous slots
N_FEAT = 10
N_VENDORS = 3
def enumerate_bundles(k: int = K, max_active: int = MAX_ACTIVE) -> list[tuple[int, ...]]:
bundles = [tuple([0] * k)]
for r in range(1, max_active + 1):
for combo in itertools.combinations(range(k), r):
b = [0] * k
for i in combo:
b[i] = 1
bundles.append(tuple(b))
return bundles
BUNDLES = enumerate_bundles()
N_ACTIONS = len(BUNDLES)
NULL_ACTION = 0
BUNDLE_MAT = np.array(BUNDLES, dtype=float) # (N_ACTIONS, K)
# --------------------------------------------------------------------------
# Simulator: heterogeneous, small-effect, cross-slot-interacting outcomes
# --------------------------------------------------------------------------
def make_effect_params(seed: int) -> dict:
rng = np.random.default_rng(seed)
w_main = rng.normal(0, 1, size=(K, N_FEAT)) * 0.9
w_fatigue = np.abs(rng.normal(0, 1, size=(K, N_FEAT))) * 0.5
unit_cost = rng.uniform(0.8, 1.5, size=K)
interactions = {}
for i in range(K):
for j in range(i + 1, K):
sign = rng.choice([-1.0, 1.0])
mag = rng.uniform(0.3, 0.9)
interactions[(i, j)] = sign * mag
base_main_w = rng.normal(0, 1, N_FEAT) * 0.3
return dict(
w_main=w_main, w_fatigue=w_fatigue, unit_cost=unit_cost,
interactions=interactions, base_main_w=base_main_w,
)
def sample_users(n: int, seed: int) -> tuple[np.ndarray, np.ndarray]:
rng = np.random.default_rng(seed)
X = rng.normal(0, 1, size=(n, N_FEAT))
vendor = rng.integers(0, N_VENDORS, size=n)
return X, vendor
def true_outcomes_for_bundle(X: np.ndarray, bundle: tuple[int, ...], params: dict) -> np.ndarray:
"""Expected [main, cost, fatigue] for every row of X under a fixed bundle."""
b = np.array(bundle, dtype=float)
main_raw = X @ params["base_main_w"]
fatigue_raw = np.full(X.shape[0], -2.5) # low base -> small base fatigue after sigmoid
cost = 0.0
for k in range(K):
if b[k]:
main_raw = main_raw + np.tanh(X @ params["w_main"][k]) * 1.4
fatigue_raw = fatigue_raw + np.abs(np.tanh(X @ params["w_fatigue"][k])) * 1.1
cost = cost + params["unit_cost"][k]
for (i, j), coef in params["interactions"].items():
if b[i] and b[j]:
main_raw = main_raw + coef * np.tanh(X @ params["w_main"][i]) * np.tanh(X @ params["w_main"][j])
main = 1.0 / (1.0 + np.exp(-main_raw)) * 0.02 # small-effect engagement probability, paper-motivated
fatigue = 1.0 / (1.0 + np.exp(-fatigue_raw))
cost_arr = np.full(X.shape[0], cost)
return np.stack([main, cost_arr, fatigue], axis=1)
def true_outcomes_all_actions(X: np.ndarray, params: dict) -> np.ndarray:
"""(n, N_ACTIONS, 3) true expected outcomes for every candidate bundle."""
out = np.empty((X.shape[0], N_ACTIONS, 3))
for a, bundle in enumerate(BUNDLES):
out[:, a, :] = true_outcomes_for_bundle(X, bundle, params)
return out
def simulate_logged_data(n: int, seed: int, params: dict) -> pd.DataFrame:
"""Observational-style training log: uniform random exploration bundle per user."""
rng = np.random.default_rng(seed)
X, vendor = sample_users(n, seed)
action_idx = rng.integers(0, N_ACTIONS, size=n)
B = BUNDLE_MAT[action_idx]
exp_outcomes = np.stack(
[true_outcomes_for_bundle(X[i:i + 1], BUNDLES[action_idx[i]], params)[0] for i in range(n)]
) if n <= 20000 else _batched_logged_outcomes(X, action_idx, params)
# Noise stds are scaled to each outcome's own dynamic range: "main" is a
# deliberately small-effect signal (max ~0.02, paper-motivated), so using
# a flat noise std here (as in an earlier version of this script) drowned
# the label in noise and penalized the higher-capacity MLP far more than
# the low-variance linear baseline purely as a bias-variance artifact,
# not a genuine test of the modeling claim.
noise_std = [0.004, 0.08, 0.03]
noise = np.random.default_rng(seed + 1).normal(0, noise_std, size=exp_outcomes.shape)
Y = exp_outcomes + noise
df = {"vendor": vendor, "action_idx": action_idx}
for f in range(N_FEAT):
df[f"x{f}"] = X[:, f]
for k in range(K):
df[f"b{k}"] = B[:, k]
for m, name in enumerate(OUTCOMES):
df[f"y_{name}"] = Y[:, m]
return pd.DataFrame(df), X, B, Y
def _batched_logged_outcomes(X, action_idx, params, batch=50000):
n = X.shape[0]
out = np.empty((n, 3))
for a in range(N_ACTIONS):
mask = action_idx == a
if mask.any():
out[mask] = true_outcomes_for_bundle(X[mask], BUNDLES[a], params)
return out
# --------------------------------------------------------------------------
# Stage 1: uplift estimators
# --------------------------------------------------------------------------
def featurize_plain(X: np.ndarray, B: np.ndarray) -> np.ndarray:
return np.concatenate([X, B], axis=1)
def featurize_additive(X: np.ndarray, B: np.ndarray) -> np.ndarray:
parts = [X, B]
for k in range(K):
parts.append(X * B[:, k:k + 1])
return np.concatenate(parts, axis=1)
def fit_linear_additive(X, B, Y):
F = featurize_additive(X, B)
models = [LinearRegression().fit(F, Y[:, m]) for m in range(3)]
def predict(Xq, Bq):
Fq = featurize_additive(Xq, Bq)
return np.stack([mdl.predict(Fq) for mdl in models], axis=1)
return predict
def fit_buoplr_net(X, B, Y, seed):
F = featurize_plain(X, B)
# Outcomes live on very different scales (main ~1e-2, cost ~1, fatigue
# ~0-1). A shared-trunk multi-output net trained on raw targets lets the
# largest-scale outcome dominate the summed loss and starves the others
# of gradient signal -- standardizing per-outcome is what makes the
# cross-outcome sharing actually beneficial rather than just noise.
y_mean, y_std = Y.mean(axis=0), Y.std(axis=0) + 1e-8
Yz = (Y - y_mean) / y_std
mlp = MLPRegressor(hidden_layer_sizes=(64, 64), activation="relu", max_iter=600,
random_state=seed, early_stopping=True, n_iter_no_change=20)
mlp.fit(F, Yz) # single shared-trunk network, multi-output -> cross-outcome sharing
def predict(Xq, Bq):
return mlp.predict(featurize_plain(Xq, Bq)) * y_std + y_mean
return predict
def fit_independent_mlp(X, B, Y, seed):
F = featurize_plain(X, B)
models = []
for m in range(3):
mlp = MLPRegressor(hidden_layer_sizes=(64, 64), activation="relu", max_iter=400,
random_state=seed + m, early_stopping=True, n_iter_no_change=15)
mlp.fit(F, Y[:, m])
models.append(mlp)
def predict(Xq, Bq):
Fq = featurize_plain(Xq, Bq)
return np.stack([mdl.predict(Fq) for mdl in models], axis=1)
return predict
def predict_all_actions(predict_fn, X: np.ndarray) -> np.ndarray:
"""(n, N_ACTIONS, 3) predicted outcomes for every candidate bundle."""
n = X.shape[0]
out = np.empty((n, N_ACTIONS, 3))
for a, bundle in enumerate(BUNDLES):
Ba = np.tile(np.array(bundle, dtype=float), (n, 1))
out[:, a, :] = predict_fn(X, Ba)
return out
def to_uplift(pred_all: np.ndarray) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
U = pred_all[:, :, 0] - pred_all[:, 0, 0:1] # main uplift vs null
C = pred_all[:, :, 1]
Fat = pred_all[:, :, 2]
return U, C, Fat
# --------------------------------------------------------------------------
# Stage 2: assignment algorithms
# --------------------------------------------------------------------------
def restrict_candidates(U: np.ndarray, n_candidates: int) -> np.ndarray:
"""Top-C actions per user by predicted main uplift, always including null."""
order = np.argsort(-U, axis=1)[:, :n_candidates]
has_null = (order == NULL_ACTION).any(axis=1)
order[~has_null, -1] = NULL_ACTION
return order # (n, n_candidates) action indices
def assign_lagrangian(U, C, Fat, vendor, budget, quotas, fatigue_cap, n_candidates=3,
iters=60):
"""Restricted-decision-space Lagrangian relaxation (BUOPLR stage 2).
Dual step sizes are self-calibrated from the uplift/cost/fatigue magnitudes
so the method works regardless of the absolute scale of U (which is
deliberately tiny here, mirroring the paper's "small-effect uplift"
framing) — a fixed step size tuned for one scale silently collapses every
user to the null action at another. We also track the best *feasible*
primal iterate seen across the dual trajectory, since subgradient duals
oscillate around the constraint boundary rather than converging exactly.
"""
n = U.shape[0]
cand = restrict_candidates(U, n_candidates)
Uc = np.take_along_axis(U, cand, axis=1)
Cc = np.take_along_axis(C, cand, axis=1)
Fc = np.take_along_axis(Fat, cand, axis=1)
is_active = (cand != NULL_ACTION).astype(float)
u_scale = np.median(np.abs(Uc[Uc > 0])) if (Uc > 0).any() else 1e-3
c_scale = max(np.median(Cc[Cc > 0]) if (Cc > 0).any() else 1.0, 1e-6)
f_scale = max(np.median(Fc[Fc > 0]) if (Fc > 0).any() else 1.0, 1e-6)
step_b0 = u_scale / c_scale
step_q0 = u_scale
step_f0 = u_scale / f_scale
lam_b, lam_f = 0.0, 0.0
lam_q = np.zeros(N_VENDORS)
idx = np.arange(n)
best_chosen, best_uplift = None, -np.inf
chosen = np.zeros(n, dtype=int)
for it in range(iters):
decay = 1.0 / (1.0 + 0.15 * it)
quota_pen = lam_q[vendor][:, None] * is_active
score = Uc - lam_b * Cc - quota_pen - lam_f * Fc
best_j = np.argmax(score, axis=1)
chosen = np.take_along_axis(cand, best_j[:, None], axis=1)[:, 0]
total_cost = C[idx, chosen].sum()
active_mask = chosen != NULL_ACTION
quota_usage = np.array([(active_mask & (vendor == v)).sum() for v in range(N_VENDORS)])
avg_fatigue = Fat[idx, chosen].mean()
feasible = (total_cost <= budget * 1.005 and (quota_usage <= quotas * 1.005).all()
and avg_fatigue <= fatigue_cap * 1.005)
total_uplift = U[idx, chosen].sum()
if feasible and total_uplift > best_uplift:
best_uplift, best_chosen = total_uplift, chosen.copy()
rel_budget = (total_cost - budget) / max(budget, 1e-6)
rel_quota = (quota_usage - quotas) / np.maximum(quotas, 1e-6)
rel_fatigue = (avg_fatigue - fatigue_cap) / max(fatigue_cap, 1e-6)
lam_b = max(0.0, lam_b + decay * step_b0 * rel_budget)
lam_q = np.maximum(0.0, lam_q + decay * step_q0 * rel_quota)
lam_f = max(0.0, lam_f + decay * step_f0 * rel_fatigue)
if best_chosen is None:
best_chosen = chosen # dual trajectory never found a feasible iterate; repair below
chosen = _repair_feasibility(best_chosen, U, C, Fat, vendor, budget, quotas, fatigue_cap)
return chosen
def _repair_feasibility(chosen, U, C, Fat, vendor, budget, quotas, fatigue_cap, max_steps=None):
n = len(chosen)
chosen = chosen.copy()
max_steps = max_steps or n
def violated():
total_cost = C[np.arange(n), chosen].sum()
active = chosen != NULL_ACTION
quota_usage = np.array([(active & (vendor == v)).sum() for v in range(N_VENDORS)])
avg_fatigue = Fat[np.arange(n), chosen].mean() if active.any() or True else 0.0
return total_cost > budget * 1.01 or (quota_usage > quotas).any() or avg_fatigue > fatigue_cap * 1.01
steps = 0
active_idx = np.where(chosen != NULL_ACTION)[0]
uplift_of_choice = U[np.arange(n), chosen]
order = active_idx[np.argsort(uplift_of_choice[active_idx])] # lowest uplift first
ptr = 0
while violated() and steps < max_steps and ptr < len(order):
chosen[order[ptr]] = NULL_ACTION
ptr += 1
steps += 1
return chosen
def assign_greedy(U, C, Fat, vendor, budget, quotas, fatigue_cap):
n = U.shape[0]
best_action = np.argmax(U, axis=1)
best_uplift = U[np.arange(n), best_action]
order = np.argsort(-best_uplift)
chosen = np.zeros(n, dtype=int)
remaining_budget = budget
remaining_quota = quotas.copy().astype(float)
fatigue_sum = 0.0
for i in order:
for cand_rank in range(N_ACTIONS):
a = np.argsort(-U[i])[cand_rank]
if a == NULL_ACTION:
chosen[i] = NULL_ACTION
break
c = C[i, a]
v = vendor[i]
new_fatigue_avg = (fatigue_sum + Fat[i, a]) / (i + 1) if i > 0 else Fat[i, a]
if c <= remaining_budget and remaining_quota[v] >= 1 and new_fatigue_avg <= fatigue_cap * 1.05:
chosen[i] = a
remaining_budget -= c
remaining_quota[v] -= 1
fatigue_sum += Fat[i, a]
break
else:
chosen[i] = NULL_ACTION
if chosen[i] == NULL_ACTION:
fatigue_sum += Fat[i, NULL_ACTION]
return chosen
def assign_lp(U, C, Fat, vendor, budget, quotas, fatigue_cap, n_candidates=4):
from scipy.optimize import linprog
from scipy.sparse import lil_matrix
n = U.shape[0]
cand = restrict_candidates(U, n_candidates)
n_vars = n * n_candidates
c_obj = -np.take_along_axis(U, cand, axis=1).reshape(-1) # minimize -uplift
rows_eq, cols_eq, data_eq, rhs_eq = [], [], [], []
for i in range(n):
for j in range(n_candidates):
rows_eq.append(i)
cols_eq.append(i * n_candidates + j)
data_eq.append(1.0)
rhs_eq.append(1.0)
A_eq = lil_matrix((n, n_vars))
for r, c_, v in zip(rows_eq, cols_eq, data_eq):
A_eq[r, c_] = v
Cc = np.take_along_axis(C, cand, axis=1).reshape(-1)
Fc = np.take_along_axis(Fat, cand, axis=1).reshape(-1)
active = (cand != NULL_ACTION).astype(float).reshape(-1)
vendor_rep = np.repeat(vendor, n_candidates)
A_ub_rows = [Cc]
b_ub = [budget]
for v in range(N_VENDORS):
A_ub_rows.append(active * (vendor_rep == v).astype(float))
b_ub.append(quotas[v])
A_ub_rows.append(Fc)
b_ub.append(fatigue_cap * n)
A_ub = np.stack(A_ub_rows, axis=0)
res = linprog(c_obj, A_ub=A_ub, b_ub=np.array(b_ub), A_eq=A_eq.toarray(), b_eq=np.array(rhs_eq),
bounds=(0, 1), method="highs")
x = res.x.reshape(n, n_candidates)
best_j = np.argmax(x, axis=1)
chosen = np.take_along_axis(cand, best_j[:, None], axis=1)[:, 0]
chosen = _repair_feasibility(chosen, U, C, Fat, vendor, budget, quotas, fatigue_cap)
return chosen
# --------------------------------------------------------------------------
# Evaluation
# --------------------------------------------------------------------------
def evaluate_assignment(chosen, X, vendor, params, budget, quotas, fatigue_cap):
true_all = true_outcomes_all_actions(X, params) # (n, N_ACTIONS, 3)
n = X.shape[0]
idx = np.arange(n)
main = true_all[idx, chosen, 0]
cost = true_all[idx, chosen, 1]
fatigue = true_all[idx, chosen, 2]
null_main = true_all[idx, NULL_ACTION, 0]
total_uplift = float((main - null_main).sum())
total_cost = float(cost.sum())
active = chosen != NULL_ACTION
quota_usage = np.array([(active & (vendor == v)).sum() for v in range(N_VENDORS)])
avg_fatigue = float(fatigue.mean())
return dict(
total_main_uplift=total_uplift,
total_cost=total_cost,
budget=budget,
budget_ok=bool(total_cost <= budget * 1.02),
quota_usage=quota_usage.tolist(),
quotas=quotas.tolist(),
quota_ok=bool((quota_usage <= quotas * 1.02).all()),
avg_fatigue=avg_fatigue,
fatigue_cap=fatigue_cap,
fatigue_ok=bool(avg_fatigue <= fatigue_cap * 1.02),
n_treated=int(active.sum()),
n_users=n,
)
def oracle_assignment(X, vendor, params, budget, quotas, fatigue_cap, n_candidates=4):
true_all = true_outcomes_all_actions(X, params)
U = true_all[:, :, 0] - true_all[:, 0, 0:1]
C = true_all[:, :, 1]
Fat = true_all[:, :, 2]
return assign_lagrangian(U, C, Fat, vendor, budget, quotas, fatigue_cap, n_candidates=n_candidates, iters=60)
# --------------------------------------------------------------------------
# Main experiment
# --------------------------------------------------------------------------
def run(n_train: int, n_eval: int, seed: int, out_dir: Path, run_lp: bool, lp_max_n: int = 5000,
scaling_ns: list[int] | None = None):
out_dir.mkdir(parents=True, exist_ok=True)
params = make_effect_params(seed)
t0 = time.time()
df_train, Xtr, Btr, Ytr = simulate_logged_data(n_train, seed, params)
Xev, vendor_ev = sample_users(n_eval, seed + 999)
print(f"[data] train={n_train} eval={n_eval} build_time={time.time() - t0:.1f}s")
budget = 0.55 * n_eval * float(np.mean(params["unit_cost"]))
quotas = np.array([0.55 * (vendor_ev == v).sum() for v in range(N_VENDORS)])
fatigue_cap = 0.30
uplift_models = {}
fit_times = {}
for name, fitter in [
("linear_additive", lambda: fit_linear_additive(Xtr, Btr, Ytr)),
("independent_mlp", lambda: fit_independent_mlp(Xtr, Btr, Ytr, seed)),
("buoplr_net", lambda: fit_buoplr_net(Xtr, Btr, Ytr, seed)),
]:
t0 = time.time()
predict_fn = fitter()
fit_times[name] = time.time() - t0
pred_all = predict_all_actions(predict_fn, Xev)
U, C, Fat = to_uplift(pred_all)
uplift_models[name] = dict(U=U, C=C, Fat=Fat)
print(f"[fit] {name} fit_time={fit_times[name]:.1f}s")
pipelines = {}
# Control: no treatment
chosen_ctrl = np.zeros(n_eval, dtype=int)
pipelines["control_no_treatment"] = (chosen_ctrl, 0.0)
# BUOPLR proposed pipeline: buoplr_net + lagrangian restricted assignment
for uplift_name in uplift_models:
m = uplift_models[uplift_name]
t0 = time.time()
chosen = assign_lagrangian(m["U"], m["C"], m["Fat"], vendor_ev, budget, quotas, fatigue_cap)
rt = time.time() - t0
pipelines[f"{uplift_name}__lagrangian"] = (chosen, rt)
t0 = time.time()
chosen_g = assign_greedy(m["U"], m["C"], m["Fat"], vendor_ev, budget, quotas, fatigue_cap)
rt_g = time.time() - t0
pipelines[f"{uplift_name}__greedy"] = (chosen_g, rt_g)
# Oracle upper bound (true effects, Lagrangian assignment) on the full eval set
t0 = time.time()
chosen_oracle = oracle_assignment(Xev, vendor_ev, params, budget, quotas, fatigue_cap)
rt_oracle = time.time() - t0
pipelines["oracle__lagrangian"] = (chosen_oracle, rt_oracle)
results = []
for name, (chosen, rt) in pipelines.items():
ev = evaluate_assignment(chosen, Xev, vendor_ev, params, budget, quotas, fatigue_cap)
ev["pipeline"] = name
ev["assignment_runtime_s"] = rt
uplift_name = name.split("__")[0]
ev["fit_time_s"] = fit_times.get(uplift_name, None)
results.append(ev)
res_df = pd.DataFrame(results).sort_values("total_main_uplift", ascending=False)
res_df.to_csv(out_dir / "results.csv", index=False)
print(res_df[["pipeline", "total_main_uplift", "budget_ok", "quota_ok", "fatigue_ok",
"assignment_runtime_s"]].to_string(index=False))
# LP quality-gap comparison on a bounded subset (LP does not scale to the
# full eval set -- that intractability is itself part of what we're
# testing: does BUOPLR's restricted Lagrangian relaxation match near-exact
# LP quality while staying orders of magnitude faster?).
lp_rows = []
if run_lp:
n_lp = min(lp_max_n, n_eval)
Xlp, vendor_lp = Xev[:n_lp], vendor_ev[:n_lp]
budget_lp = 0.55 * n_lp * float(np.mean(params["unit_cost"]))
quotas_lp = np.array([0.55 * (vendor_lp == v).sum() for v in range(N_VENDORS)])
for uplift_name, m in uplift_models.items():
Ulp, Clp, Flp = m["U"][:n_lp], m["C"][:n_lp], m["Fat"][:n_lp]
t0 = time.time()
chosen_lag = assign_lagrangian(Ulp, Clp, Flp, vendor_lp, budget_lp, quotas_lp, fatigue_cap)
rt_lag = time.time() - t0
t0 = time.time()
chosen_lp = assign_lp(Ulp, Clp, Flp, vendor_lp, budget_lp, quotas_lp, fatigue_cap)
rt_lp = time.time() - t0
ev_lag = evaluate_assignment(chosen_lag, Xlp, vendor_lp, params, budget_lp, quotas_lp, fatigue_cap)
ev_lp = evaluate_assignment(chosen_lp, Xlp, vendor_lp, params, budget_lp, quotas_lp, fatigue_cap)
lp_rows.append(dict(uplift_model=uplift_name, n_users=n_lp,
lagrangian_uplift=ev_lag["total_main_uplift"], lagrangian_runtime_s=rt_lag,
lp_uplift=ev_lp["total_main_uplift"], lp_runtime_s=rt_lp,
quality_gap_pct=100.0 * (ev_lp["total_main_uplift"] - ev_lag["total_main_uplift"])
/ max(abs(ev_lp["total_main_uplift"]), 1e-9),
speedup_x=rt_lp / max(rt_lag, 1e-9)))
lp_df = pd.DataFrame(lp_rows)
lp_df.to_csv(out_dir / "lp_quality_gap.csv", index=False)
print("\n[LP quality-gap @ n=%d]" % n_lp)
print(lp_df.to_string(index=False))
scaling_rows = []
if scaling_ns:
for n_s in scaling_ns:
Xs, vendor_s = sample_users(n_s, seed + 12345 + n_s)
budget_s = 0.55 * n_s * float(np.mean(params["unit_cost"]))
quotas_s = np.array([0.55 * (vendor_s == v).sum() for v in range(N_VENDORS)])
# reuse the already-fit buoplr_net predictor for inference + assignment timing
t0 = time.time()
pred_all_s = predict_all_actions(_LAST_BUOPLR_PREDICT[0], Xs)
infer_time = time.time() - t0
U_s, C_s, Fat_s = to_uplift(pred_all_s)
t0 = time.time()
assign_lagrangian(U_s, C_s, Fat_s, vendor_s, budget_s, quotas_s, fatigue_cap)
lag_time = time.time() - t0
t0 = time.time()
assign_greedy(U_s, C_s, Fat_s, vendor_s, budget_s, quotas_s, fatigue_cap)
greedy_time = time.time() - t0
scaling_rows.append(dict(n_users=n_s, infer_time_s=infer_time,
lagrangian_assign_s=lag_time, greedy_assign_s=greedy_time))
print(f"[scaling] n={n_s} infer={infer_time:.2f}s lagrangian={lag_time:.2f}s greedy={greedy_time:.2f}s")
scaling_df = pd.DataFrame(scaling_rows)
if len(scaling_df):
scaling_df.to_csv(out_dir / "scaling.csv", index=False)
with open(out_dir / "config.json", "w") as f:
json.dump(dict(n_train=n_train, n_eval=n_eval, seed=seed, budget=budget,
quotas=quotas.tolist(), fatigue_cap=fatigue_cap,
n_actions=N_ACTIONS, k_slots=K, max_active=MAX_ACTIVE,
fit_times=fit_times, scaling_ns=scaling_ns or []), f, indent=2)
return res_df, scaling_df
def push_outputs_to_hub(out_dir: Path, repo_id: str):
import os
from huggingface_hub import HfApi
token = os.environ.get("HF_TOKEN")
api = HfApi(token=token)
api.create_repo(repo_id, repo_type="dataset", exist_ok=True, private=False)
for f in out_dir.iterdir():
if f.is_file():
api.upload_file(path_or_fileobj=str(f), path_in_repo=f.name,
repo_id=repo_id, repo_type="dataset")
print(f"Pushed outputs to https://huggingface.co/datasets/{repo_id}")
_LAST_BUOPLR_PREDICT = [None]
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--n-train", type=int, default=5000)
ap.add_argument("--n-eval", type=int, default=2000)
ap.add_argument("--seed", type=int, default=0)
ap.add_argument("--out-dir", type=str, default="outputs/local")
ap.add_argument("--run-lp", action="store_true")
ap.add_argument("--lp-max-n", type=int, default=5000)
ap.add_argument("--scaling-ns", type=str, default="")
ap.add_argument("--push-repo", type=str, default="")
args = ap.parse_args()
scaling_ns = [int(x) for x in args.scaling_ns.split(",") if x.strip()] if args.scaling_ns else None
# monkey-patch to capture the buoplr predictor for the scaling sweep
orig_fit = fit_buoplr_net
def fit_buoplr_net_capture(*a, **kw):
fn = orig_fit(*a, **kw)
_LAST_BUOPLR_PREDICT[0] = fn
return fn
globals()["fit_buoplr_net"] = fit_buoplr_net_capture
out_dir = Path(args.out_dir)
run(args.n_train, args.n_eval, args.seed, out_dir, args.run_lp, args.lp_max_n, scaling_ns)
if args.push_repo:
push_outputs_to_hub(out_dir, args.push_repo)
if __name__ == "__main__":
main()

Xet Storage Details

Size:
26.6 kB
·
Xet hash:
ebd2f87405e0ed5ef08348b585fcc4ba6fd42ab549552507fd40e9ac79bb92bb

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.