Source code for mmm_framework.continuous_learning.scaling
"""Dollar <-> scaled-unit conversion — make ``spend_ref`` real (review fix F8).
The data contract keeps spend **scaled**: every channel is divided by a fixed,
per-channel reference constant (``spend_ref``, dollars per scaled unit) chosen
once at program start — never a cluster mean, never re-estimated mid-program
(rescaling mid-flight silently changes what ``kappa`` means). Historically the
package only *carried* ``spend_ref`` on :class:`~mmm_framework.continuous_learning.model.Posterior`
and ``LearningState`` without ever applying it; these two helpers are the single
sanctioned conversion so the service layer can work in dollars at the boundary
and scaled units internally.
Convention: ``scaled = dollars / spend_ref`` elementwise over the channel axis
(the LAST axis), so both a single ``(K,)`` allocation and an ``(N, K)`` panel
convert with the same call.
"""
from __future__ import annotations
import numpy as np
def _validate_ref(spend_ref: np.ndarray) -> np.ndarray:
"""Coerce + validate a per-channel reference vector (positive, finite, 1-D)."""
ref = np.asarray(spend_ref, dtype=float)
if ref.ndim != 1:
raise ValueError(f"spend_ref must be 1-D (K,), got shape {ref.shape}")
if not np.all(np.isfinite(ref)):
raise ValueError("spend_ref contains non-finite values")
if np.any(ref <= 0):
raise ValueError("spend_ref must be strictly positive per channel")
return ref
def _check_last_axis(arr: np.ndarray, ref: np.ndarray, what: str) -> None:
if arr.ndim >= 1 and arr.shape[-1] != ref.shape[0]:
raise ValueError(
f"{what} has last-axis length {arr.shape[-1]} but spend_ref has "
f"{ref.shape[0]} channels"
)
[docs]
def to_scaled(dollars: np.ndarray, spend_ref: np.ndarray) -> np.ndarray:
"""Convert dollar spend to scaled units: ``dollars / spend_ref``.
Args:
dollars: spend in dollars — a ``(K,)`` allocation or an ``(N, K)``
panel (any leading shape; the channel axis is the last one).
spend_ref: dollars per scaled unit, shape ``(K,)``, strictly positive.
Returns:
The scaled spend, same shape as ``dollars`` (float64).
"""
ref = _validate_ref(spend_ref)
d = np.asarray(dollars, dtype=float)
_check_last_axis(d, ref, "dollars")
return d / ref
[docs]
def to_dollars(scaled: np.ndarray, spend_ref: np.ndarray) -> np.ndarray:
"""Convert scaled spend back to dollars: ``scaled * spend_ref``.
Args:
scaled: scaled spend — a ``(K,)`` allocation or an ``(N, K)`` panel.
spend_ref: dollars per scaled unit, shape ``(K,)``, strictly positive.
Returns:
The dollar spend, same shape as ``scaled`` (float64).
"""
ref = _validate_ref(spend_ref)
s = np.asarray(scaled, dtype=float)
_check_last_axis(s, ref, "scaled")
return s * ref