@@ -178,6 +178,16 @@ def rake(
178178 artifacts for ``BalanceFrame.predict_weights()`` reconstruction.
179179
180180 Notes:
181+ When exactly one adjustment variable is selected (either explicitly via
182+ ``variables=[...]`` or implicitly because only one common variable exists),
183+ this function delegates to :func:`balance.weighting_methods.poststratify.poststratify`.
184+ In that fallback path, the returned model metadata records
185+ ``method='poststratify'`` and the returned weight series is renamed to
186+ ``rake_weight`` for API consistency. Because
187+ ``BalanceFrame.predict_weights(data=...)`` dispatches by
188+ ``model['method']``, delegated fits follow poststratify's transfer-scoring
189+ capabilities/limitations rather than rake's.
190+
181191 ``BalanceFrame.predict_weights()`` for rake reuses the fitted cell-ratio
182192 surface from this function (effectively ``m_fit / m_sample`` per joint
183193 cell) and applies it to design weights in the scoring sample. This is
@@ -232,6 +242,79 @@ def rake(
232242 )
233243 if not isinstance (store_fit_metadata , bool ):
234244 raise TypeError ("`store_fit_metadata` must be a bool." )
245+ variables = balance_util .choose_variables (sample_df , target_df , variables = variables )
246+
247+ logger .debug (f"Join variables for sample and target: { variables } " )
248+
249+ sample_df = sample_df .loc [:, variables ]
250+ target_df = target_df .loc [:, variables ]
251+
252+ if len (variables ) == 0 :
253+ raise ValueError (
254+ "No shared weighting variables were found between sample and target. "
255+ "Pass `variables=[...]` with at least one common column present in both."
256+ )
257+
258+ # Keep single-variable fallback behavior aligned with poststratify:
259+ # when variables are explicitly provided, out-of-scope transformation
260+ # entries are ignored.
261+ single_variable_transformations = transformations
262+ if len (variables ) == 1 and isinstance (transformations , dict ):
263+ single_variable_transformations = {
264+ key : value for key , value in transformations .items () if key in variables
265+ }
266+ if len (single_variable_transformations ) == 0 :
267+ single_variable_transformations = None
268+
269+ transformations_for_pickle = single_variable_transformations
270+ if len (variables ) > 1 :
271+ if transformations == "default" :
272+ transformations_for_pickle = balance_adjustment .default_transformations (
273+ (sample_df , target_df )
274+ )
275+ else :
276+ transformations_for_pickle = transformations
277+
278+ if store_fit_metadata :
279+ # Fail fast: persisting non-pickleable callables (e.g. lambdas,
280+ # closures) would break `pickle.dumps(adjusted_bf)` workflows
281+ # downstream. Check here, before long-running fit work. Matches the
282+ # poststratify pattern.
283+ try :
284+ # @lint-ignore PYTHONPICKLEISBAD - serializability check only; no untrusted deserialization
285+ pickle .dumps (transformations_for_pickle )
286+ except Exception as exc :
287+ raise ValueError (
288+ "`transformations` must be pickleable when "
289+ "store_fit_metadata=True. Pass store_fit_metadata=False to "
290+ "disable fit-artifact persistence for this run."
291+ ) from exc
292+
293+ if len (variables ) == 1 :
294+ logger .warning (
295+ "rake() received a single adjustment variable (%s); "
296+ "delegating to poststratify(). Returned model metadata will "
297+ "record method='poststratify'." ,
298+ variables [0 ],
299+ )
300+ from balance .weighting_methods .poststratify import poststratify
301+
302+ poststratified = poststratify (
303+ sample_df = sample_df ,
304+ sample_weights = sample_weights ,
305+ target_df = target_df ,
306+ target_weights = target_weights ,
307+ variables = variables ,
308+ transformations = single_variable_transformations ,
309+ na_action = na_action ,
310+ weight_trimming_mean_ratio = weight_trimming_mean_ratio ,
311+ weight_trimming_percentile = weight_trimming_percentile ,
312+ keep_sum_of_weights = keep_sum_of_weights ,
313+ store_fit_metadata = store_fit_metadata ,
314+ )
315+ poststratified ["weight" ] = poststratified ["weight" ].rename ("rake_weight" )
316+ return poststratified
317+
235318 if store_fit_metadata and transformations == "default" :
236319 # `transformations='default'` resolves to data-dependent helpers
237320 # (`quantize`/`fct_lump`) which recompute bins/levels from the input
@@ -254,42 +337,14 @@ def rake(
254337 "deterministic transformations at fit time to enable transfer."
255338 )
256339
257- variables = balance_util .choose_variables (sample_df , target_df , variables = variables )
258-
259- logger .debug (f"Join variables for sample and target: { variables } " )
260-
261- sample_df = sample_df .loc [:, variables ]
262- target_df = target_df .loc [:, variables ]
263-
264- # TODO: When len(variables) == 1, fall back to poststratify instead of
265- # raising, so users can call adjust(method="rake") without worrying about
266- # the variable count.
267- assert len (variables ) > 1 , (
268- "Must weight on at least two variables for raking. "
269- f"Currently have variables={ variables } only"
270- )
271-
272340 transformations_to_apply = transformations
273341 if transformations == "default" :
274- transformations_to_apply = balance_adjustment .default_transformations (
275- (sample_df , target_df )
276- )
277-
278- if store_fit_metadata :
279- # Fail fast: persisting non-pickleable callables (e.g. lambdas,
280- # closures) would break `pickle.dumps(adjusted_bf)` workflows
281- # downstream. Check here, before the IPF compute, so users don't
282- # wait for a long fit only to fail at the end. Matches the
283- # poststratify pattern.
284- try :
285- # @lint-ignore PYTHONPICKLEISBAD - serializability check only; no untrusted deserialization
286- pickle .dumps (transformations_to_apply )
287- except Exception as exc :
288- raise ValueError (
289- "`transformations` must be pickleable when "
290- "store_fit_metadata=True. Pass store_fit_metadata=False to "
291- "disable fit-artifact persistence for this run."
292- ) from exc
342+ if store_fit_metadata :
343+ transformations_to_apply = transformations_for_pickle
344+ else :
345+ transformations_to_apply = balance_adjustment .default_transformations (
346+ (sample_df , target_df )
347+ )
293348
294349 sample_df , target_df = balance_adjustment .apply_transformations (
295350 (sample_df , target_df ), transformations_to_apply
@@ -652,6 +707,13 @@ def _predict_weights_from_model(
652707 sample_df = sample_df .loc [:, variables ]
653708 target_df = target_df .loc [:, variables ]
654709
710+ if len (variables ) == 0 :
711+ raise ValueError (
712+ "Rake predict_weights() model metadata is missing stored weighting "
713+ "variables. Re-fit the model (with store_fit_metadata=True) before "
714+ "calling predict_weights()."
715+ )
716+
655717 sample_weights = sample_weights_full
656718 if not is_transfer :
657719 training_sample_weights = model .get ("training_sample_weights" )
0 commit comments