From 1601630b37377076cb86db15840b50156baff31c Mon Sep 17 00:00:00 2001 From: =?utf8?q?J=C3=A9r=C3=B4me=20Benoit?= Date: Sun, 24 May 2026 01:49:34 +0200 Subject: [PATCH] feat(weights): add compose_sample_weights helper with mean=1 multiplicative composition MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit AFML §4.10 / mlfinpy canonical: per-label mean=1 normalization, multiplicative composition with temporal decay, geometric-mean aggregation for multi-label, NaN/inf handling, all-zero degenerate fallback. Validated locally with pytest (evidence: .omo/evidence/task-5-{red,green}.txt). --- quickadapter/user_data/strategies/Utils.py | 25 ++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/quickadapter/user_data/strategies/Utils.py b/quickadapter/user_data/strategies/Utils.py index a1def00..92ab4ec 100644 --- a/quickadapter/user_data/strategies/Utils.py +++ b/quickadapter/user_data/strategies/Utils.py @@ -680,6 +680,31 @@ def midpoint(value1: T, value2: T) -> T: return (value1 + value2) / 2 +def compose_sample_weights( + temporal: NDArray[np.floating], + label_weights_map: dict[str, NDArray[np.floating]], +) -> NDArray[np.floating]: + if not label_weights_map: + return temporal + normalized_per_label: list[NDArray[np.floating]] = [] + for w in label_weights_map.values(): + arr = np.asarray(w, dtype=float) + arr = np.where(np.isfinite(arr) & (arr > 0), arr, 1.0) + total = arr.sum() + if total <= 0 or not np.isfinite(total): + arr = np.ones_like(arr) + else: + arr = arr * (len(arr) / total) + normalized_per_label.append(arr) + stacked = np.vstack(normalized_per_label) + agg = np.exp(np.log(stacked).mean(axis=0)) + combined = np.asarray(temporal, dtype=float) * agg + combined_sum = combined.sum() + if combined_sum <= 0 or not np.isfinite(combined_sum): + return np.asarray(temporal, dtype=float) + return combined * (len(combined) / combined_sum) + + def nan_average( values: NDArray[np.floating], weights: NDArray[np.floating] | None = None, -- 2.53.0