@@ -2109,246 +2109,25 @@ def _predict_weights_rake(
21092109 model : dict [str , Any ],
21102110 source : BalanceFrame | None = None ,
21112111 ) -> pd .Series :
2112- required = (
2113- "variables" ,
2114- "variables_before_transformations" ,
2115- "categories" ,
2116- "m_fit" ,
2117- "m_sample" ,
2118- "na_action" ,
2119- "transformations" ,
2120- )
2121- if source is None :
2122- required = required + ("training_sample_weights" , "training_target_weights" )
2123- missing = [key for key in required if key not in model ]
2124- if missing :
2125- raise ValueError (
2126- "Rake model is missing fit-time metadata "
2127- f"({ missing } ) for predict_weights(). "
2128- "Call BalanceFrame.fit(method='rake') or run rake(..., "
2129- "store_fit_metadata=True)."
2130- )
2131-
2132- variables = model .get ("variables" )
2133- input_variables = model .get ("variables_before_transformations" )
2134- categories = model .get ("categories" )
2135- m_fit = model .get ("m_fit" )
2136- m_sample = model .get ("m_sample" )
2137- transformations_origin = model .get ("transformations_origin" )
2138- if (
2139- not isinstance (variables , list )
2140- or not isinstance (input_variables , list )
2141- or not isinstance (categories , list )
2142- ):
2143- raise ValueError ("Rake model metadata is malformed for predict_weights()." )
2144- if not isinstance (m_fit , np .ndarray ) or not isinstance (m_sample , np .ndarray ):
2145- raise ValueError ("Rake model is missing stored contingency tables." )
2112+ from balance .weighting_methods .rake import predict_weights_from_model
21462113
21472114 bf = source if source is not None else self
21482115 if source is not None and bf ._sf_target is None :
21492116 raise ValueError (
21502117 "data must have a target set for rake predict_weights(data=...)."
21512118 )
2152- if source is not None and transformations_origin == "default" :
2153- raise ValueError (
2154- "Rake predict_weights(data=...) is unsupported for models fitted "
2155- "with transformations='default' because those transformations are "
2156- "data-dependent and not replayable across new samples. Re-fit on "
2157- "the scoring data or fit with explicit deterministic transformations."
2158- )
2159- if source is not None and isinstance (transformations_origin , dict ):
2160- # Best-effort guard: reject explicit dicts that directly
2161- # reference balance's known data-dependent helpers
2162- # (quantize, fct_lump). These recompute bins/levels from the
2163- # scoring data, so stored cell ratios no longer line up with
2164- # the transformed scoring cells and transfer would silently
2165- # return incorrect weights.
2166- #
2167- # This guard does NOT catch indirect uses such as
2168- # ``functools.partial(fct_lump, prop=0.1)``, top-level wrapper
2169- # functions, or user-defined data-dependent transformations.
2170- # The general invariant is: any callable whose output for a
2171- # row depends on other rows in the input is unsafe to replay
2172- # on a different sample. Users supplying such transformations
2173- # are responsible for either (a) wrapping them as
2174- # deterministic functions of stored fit-time parameters or
2175- # (b) re-fitting rake on the scoring data.
2176- from balance .utils .data_transformation import fct_lump , quantize
2177-
2178- data_dependent_helpers = {quantize , fct_lump }
2179- offenders = sorted (
2180- {
2181- getattr (fn , "__name__" , repr (fn ))
2182- for fn in transformations_origin .values ()
2183- if fn in data_dependent_helpers
2184- }
2185- )
2186- if offenders :
2187- raise ValueError (
2188- "Rake predict_weights(data=...) is unsupported for models "
2189- f"fitted with data-dependent transformations ({ ', ' .join (offenders )} ). "
2190- "These recompute bins/levels from the scoring data, so "
2191- "stored cell ratios no longer line up with the transformed "
2192- "cells. Re-fit on the scoring data or fit with deterministic "
2193- "transformations."
2194- )
2195- sample_covars = bf ._sf_sample .df_covars
2196- target_covars = _assert_type (bf ._sf_target ).df_covars
2197- for column in input_variables :
2198- if (
2199- column not in sample_covars .columns
2200- or column not in target_covars .columns
2201- ):
2202- raise ValueError (
2203- "Rake predict_weights() cannot find required covariate "
2204- f"'{ column } ' in both sample and target."
2205- )
2206- sample_df = sample_covars .loc [:, input_variables ]
2207- target_df = target_covars .loc [:, input_variables ]
2208- sample_weights_full = bf ._sf_sample .df_weights .iloc [:, 0 ]
2209- training_sample_weights = model .get ("training_sample_weights" )
2210- sample_weights = sample_weights_full
2211- na_action = cast (str , model .get ("na_action" , "add_indicator" ))
22122119
2213- sample_df , target_df = balance_adjustment .apply_transformations (
2214- (sample_df , target_df ), transformations = model .get ("transformations" )
2120+ target_frame = _assert_type (bf ._sf_target )
2121+ predicted = predict_weights_from_model (
2122+ model = model ,
2123+ sample_df = bf ._sf_sample .df_covars ,
2124+ sample_weights_full = bf ._sf_sample .df_weights .iloc [:, 0 ],
2125+ target_df = target_frame .df_covars ,
2126+ target_weights = target_frame .df_weights .iloc [:, 0 ],
2127+ is_transfer = source is not None ,
22152128 )
2216- for column in variables :
2217- if column not in sample_df .columns :
2218- raise ValueError (
2219- "Rake transform output is missing stored variable "
2220- f"'{ column } ' required for predict_weights()."
2221- )
2222- sample_df = sample_df .loc [:, variables ]
2223- target_df = target_df .loc [:, variables ]
22242129
2225- if source is None :
2226- if isinstance (training_sample_weights , pd .Series ):
2227- if na_action == "drop" :
2228- sample_weights = training_sample_weights
2229- elif training_sample_weights .index .equals (sample_weights_full .index ):
2230- sample_weights = training_sample_weights
2231- else :
2232- raise ValueError (
2233- "Rake predict_weights() requires compatible fit-time sample design "
2234- "weights for in-place replay. This can happen because "
2235- "store_fit_metadata is missing/incompatible, or because you're "
2236- "scoring a different sample; use predict_weights(data=...) "
2237- "for different samples."
2238- )
2239- else :
2240- raise ValueError (
2241- "Rake predict_weights() requires compatible fit-time sample design "
2242- "weights for in-place replay. This can happen because "
2243- "store_fit_metadata is missing/incompatible, or because you're "
2244- "scoring a different sample; use predict_weights(data=...) "
2245- "for different samples."
2246- )
2247-
2248- dropped_target_weights : pd .Series | None = None
2249- if na_action == "drop" :
2250- sample_df , sample_weights = balance_util .drop_na_rows (
2251- sample_df , sample_weights , "sample"
2252- )
2253- target_df , dropped_target_weights = balance_util .drop_na_rows (
2254- target_df ,
2255- _assert_type (bf ._sf_target ).df_weights .iloc [:, 0 ],
2256- "target" ,
2257- )
2258- elif na_action == "add_indicator" :
2259- sample_df = pd .DataFrame (_safe_fillna_and_infer (sample_df , "__NaN__" ))
2260- target_df = pd .DataFrame (_safe_fillna_and_infer (target_df , "__NaN__" ))
2261- else :
2262- raise ValueError (
2263- f"Rake model has invalid na_action metadata '{ na_action } ' for predict_weights()."
2264- )
2265- sample_df = sample_df .astype (str )
2266- if m_fit .shape != m_sample .shape :
2267- raise ValueError (
2268- "Rake model metadata has incompatible fitted and sample table shapes."
2269- )
2270-
2271- category_maps = [{cat : i for i , cat in enumerate (cats )} for cats in categories ]
2272- code_columns = []
2273- for column , cat_map in zip (variables , category_maps ):
2274- codes = sample_df [column ].map (cat_map )
2275- if bool (codes .isna ().any ()):
2276- raise ValueError (
2277- "Rake predict_weights() found rows that do not map to stored fit-time "
2278- "categories. Re-fit with compatible covariates."
2279- )
2280- code_columns .append (codes .astype (int ).to_numpy ())
2281- code_index = tuple (code_columns )
2282-
2283- if source is not None :
2284- logger .warning (
2285- "Rake predict_weights(data=...): replaying fitted rake "
2286- "artifacts on a different sample is a transfer operation, "
2287- "not an exact fit. The stored cell-ratio surface "
2288- "(m_fit / m_sample) encodes the *training* target's "
2289- "marginal distribution; applying it to a new sample only "
2290- "produces weights calibrated to that *training* target, "
2291- "rescaled to the new target's total weight. The new "
2292- "target's marginals are NOT re-balanced. Predictions are "
2293- "therefore only valid when (a) the scoring sample's joint "
2294- "distribution over the rake variables is similar to the "
2295- "training sample's, AND (b) the scoring target's marginal "
2296- "distribution is similar to the training target's. "
2297- "Re-fit rake on the scoring sample/target for exact "
2298- "marginal matching."
2299- )
2300- m_sample_at_cells = m_sample [code_index ]
2301- if bool ((m_sample_at_cells <= 0 ).any ()):
2302- raise ValueError (
2303- "Rake predict_weights() encountered sample rows in joint cells with "
2304- "zero fit-time sample mass (m_sample==0). Re-fit rake on data with "
2305- "compatible joint support."
2306- )
2307- # Compute ratios only for the scored cells. Avoids materializing a
2308- # dense array the size of the full contingency table on every call
2309- # (relevant for high-cardinality rakes).
2310- cell_ratios = m_fit [code_index ] / m_sample_at_cells
2311- raw = pd .Series (
2312- sample_weights .to_numpy () * cell_ratios ,
2313- index = sample_weights .index ,
2314- dtype = float ,
2315- )
2316- target_weights = model .get ("training_target_weights" )
2317- if source is None and not isinstance (target_weights , pd .Series ):
2318- raise ValueError (
2319- "Rake predict_weights() requires compatible fit-time target design "
2320- "weights for in-place replay. This can happen because "
2321- "store_fit_metadata is missing/incompatible, or because you're "
2322- "scoring a different sample; use predict_weights(data=...) "
2323- "for different samples."
2324- )
2325- if source is not None :
2326- if na_action == "drop" and isinstance (dropped_target_weights , pd .Series ):
2327- target_sum = float (dropped_target_weights .sum ())
2328- else :
2329- target_sum = float (
2330- _assert_type (bf ._sf_target ).df_weights .iloc [:, 0 ].sum ()
2331- )
2332- elif isinstance (target_weights , pd .Series ):
2333- target_sum = float (target_weights .sum ())
2334- elif na_action == "drop" and isinstance (dropped_target_weights , pd .Series ):
2335- target_sum = float (dropped_target_weights .sum ())
2336- else :
2337- target_sum = float (_assert_type (bf ._sf_target ).df_weights .iloc [:, 0 ].sum ())
2338- predicted = balance_adjustment .trim_weights (
2339- raw ,
2340- target_sum_weights = target_sum ,
2341- weight_trimming_mean_ratio = model .get ("weight_trimming_mean_ratio" ),
2342- weight_trimming_percentile = model .get ("weight_trimming_percentile" ),
2343- keep_sum_of_weights = bool (model .get ("keep_sum_of_weights" , True )),
2344- )
23452130 weight_name = getattr (_assert_type (bf .weight_series ), "name" , None )
2346- if na_action == "drop" :
2347- predicted_full = pd .Series (
2348- np .nan , index = sample_weights_full .index , dtype = float
2349- ).rename (predicted .name )
2350- predicted_full .loc [predicted .index ] = predicted .to_numpy ()
2351- predicted = predicted_full
23522131 return cast (
23532132 pd .Series ,
23542133 self ._align_to_index (
0 commit comments