"""Frozen NumPy regression recipe; checkpoints are JSON, never executable pickle.""" import json from pathlib import Path import numpy as np from .features import FEATURE_NAMES from ._frozen_numpy import _HistogramTreeRegressor, _impute_and_scale CONFIG=dict(max_depth=2,max_iter=100,learning_rate=0.05,min_samples_leaf=30,max_bins=255,random_state=0) class UtilityModel: def fit(self,features,fallback,bespoke,labels): x=np.asarray(features,dtype=float) if x.ndim!=2 or x.shape[1]!=31: raise ValueError("EXACT_31_FEATURES_REQUIRED") a,b,y=[np.asarray(v) for v in (fallback,bespoke,labels)] if any(v.dtype!=np.bool_ or v.shape!=(len(x),) for v in (a,b,y)): raise ValueError("BOOLEAN_PAIRED_LABELS_REQUIRED") target=(b==y).astype(float)-(a==y).astype(float) train,_,self.transform=_impute_and_scale(x,x,scale=True) self.model=_HistogramTreeRegressor(**CONFIG).fit(train,target) return self def predict(self,features): if not hasattr(self,"model"): raise ValueError("MODEL_NOT_FITTED") x=np.asarray(features,dtype=float) if x.ndim!=2 or x.shape[1]!=31: raise ValueError("EXACT_31_FEATURES_REQUIRED") t=self.transform filled=np.where(np.isfinite(x),x,t["medians"][None,:]) return self.model.predict((filled-t["means"][None,:])/t["scales"][None,:]) def to_dict(self): if not hasattr(self,"model"): raise ValueError("MODEL_NOT_FITTED") state=vars(self.model).copy();state["thresholds"]=[v.tolist() for v in state["thresholds"]] return dict(schema="FACTCASCADE_UTILITY_JSON_V1",feature_order=FEATURE_NAMES, config=CONFIG,transform={k:v.tolist() for k,v in self.transform.items()},model=state) @classmethod def from_dict(cls,data): if data.get("schema")!="FACTCASCADE_UTILITY_JSON_V1" or data.get("feature_order")!=FEATURE_NAMES or data.get("config")!=CONFIG: raise ValueError("CHECKPOINT_SCHEMA_OR_CONFIG_MISMATCH") result=cls();result.transform={k:np.asarray(data["transform"][k],dtype=float) for k in ["medians","means","scales"]} if any(v.shape!=(31,) or not np.isfinite(v).all() for v in result.transform.values()) or np.any(result.transform["scales"]<=0): raise ValueError("CHECKPOINT_TRANSFORM_INVALID") model=data["model"] if any(model[k]!=v for k,v in CONFIG.items()) or len(model["thresholds"])!=31 or len(model["trees"])!=100: raise ValueError("CHECKPOINT_MODEL_INVALID") result.model=_HistogramTreeRegressor(**CONFIG) result.model.initial_value=float(model["initial_value"]) if not np.isfinite(result.model.initial_value): raise ValueError("CHECKPOINT_NONFINITE") result.model.thresholds=[np.asarray(v,dtype=float) for v in model["thresholds"]] if any(v.ndim!=1 or not np.isfinite(v).all() or np.any(np.diff(v)<=0) for v in result.model.thresholds): raise ValueError("CHECKPOINT_THRESHOLDS_INVALID") def tree(node,depth=0): if set(node)=={"value"} and np.isfinite(node["value"]): return if set(node)!={"feature","threshold_bin","left","right"} or depth>=2: raise ValueError("CHECKPOINT_TREE_INVALID") f,t=node["feature"],node["threshold_bin"] if type(f) is not int or not 0<=f<31 or type(t) is not int or not 0<=t