Skip to content

Commit eca96c9

Browse files
neuralsorcerermeta-codesync[bot]
authored andcommitted
Handle single-variable rake via poststratify fallback (#439)
Summary: Pull Request resolved: #439 Reviewed By: omriharosh Differential Revision: D103673026 Pulled By: talgalili fbshipit-source-id: d02435ba1824b27c9f504adede38dee97653bb94
1 parent d6cf291 commit eca96c9

3 files changed

Lines changed: 331 additions & 44 deletions

File tree

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,19 @@
3131
design weights are uniform (the common case), so existing behaviour is
3232
preserved.
3333

34+
- **`rake()` now gracefully handles single-variable adjustments.**
35+
When `rake(...)` resolves to exactly one adjustment variable—either
36+
because `variables=["..."]` explicitly names one variable, or because
37+
`variables=None` and sample/target share exactly one common column—it
38+
now logs a warning and delegates to `poststratify(...)` instead of
39+
raising an assertion. This preserves passthrough behaviour for
40+
transformations, NA handling, trimming controls, and fit-metadata
41+
persistence options while making
42+
`BalanceFrame.fit(method="rake")` more robust for one-variable inputs.
43+
In this delegated path, model metadata records `method='poststratify'`
44+
(explicitly noted in the warning) while returned weights keep the
45+
canonical rake output name (`rake_weight`).
46+
3447
## New Features
3548

3649
- **Rake now supports fit-time metadata persistence and `predict_weights()` reconstruction.**

balance/weighting_methods/rake.py

Lines changed: 96 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)