Skip to content

Releases: facebookresearch/balance

0.21.0 (2026-06-02)

Choose a tag to compare

@talgalili talgalili released this 02 Jun 11:26

New Features

  • balance.stats_and_plots.love_plot.love_plot, BalanceDFCovars.love_plot(),
    and BalanceDFCovars.plot(dist_type="love_plot")
    — visual
    covariate-imbalance diagnostic in the spirit of R's cobalt::love.plot.
    Supports interactive Plotly figures (the new default), static
    seaborn/matplotlib axes (library="seaborn"), and LLM-friendly ASCII
    (library="balance"). With both series it draws the canonical
    before-vs-after scatter; with only before, a single-series scatter with
    optional threshold reference line. The ASCII backend renders both series on
    a shared axis with o/* markers, wider default bar_width=50, and
    direction legends. New options include line= (toggle connectors) and
    order_by={"diff", "before", "after", "alphabetical", "none"}; the default
    order_by="diff" surfaces regressions at the top.
    BalanceDFCovars.love_plot(metric=...) dispatches across metric
    {"asmd" (default), "kld", "emd", "cvmd", "ks"}, with the
    threshold default resolving to the cobalt 0.1 cutoff for ASMD only.
    .plot(dist_type="love_plot", library=...) (or the "love" alias) routes
    covariate views to the same diagnostic.
  • Rake now supports fit-time metadata persistence and predict_weights()
    reconstruction.
    • rake(..., store_fit_metadata=True) stores contingency-table artifacts
      and fit-time metadata required to rebuild weights later.
    • BalanceFrame.fit(method="rake") enables store_fit_metadata=True by
      default so fitted rake models can be reused with
      BalanceFrame.predict_weights() without refitting.
    • In-place replay (predict_weights() with no data=) works with any
      transformations, including the rake default transformations="default".
    • Transfer scoring (predict_weights(data=...)) requires deterministic
      transformations: raises ValueError for models fitted with
      transformations="default" and for explicit dicts that directly
      reference data-dependent helpers (quantize, fct_lump). Pass
      deterministic transformations at fit time or re-fit on the scoring data.
  • Poststratify now supports transfer scoring with predict_weights(data=...).
    BalanceFrame.fit(method="poststratify", store_fit_metadata=True) stores
    the transformation origin needed to safely replay fitted cell ratios on a
    new sample/target pair: predict_weights(data=holdout_bf) applies stored
    ratios to the holdout sample's design weights and rescales to the holdout
    target's total weight. Same restriction as rake transfer scoring: rejects
    transformations="default" and direct quantize / fct_lump references.
    Pre-0.21.0 pickles lack transformations_origin and must be re-fit for
    transfer scoring; in-place predict_weights() continues to work.
  • balance.interop.diff_diff — thin adapter to
    diff-diff (>=3.3.0,<4) for
    survey-weighted Difference-in-Differences. Provides to_survey_design(),
    to_panel_for_did(), fit_did(), and as_balance_diagnostic(). Install
    via pip install "balance[did]". The submodule is lazy-imported, so
    import balance still works cleanly when diff-diff isn't installed — the
    import guard rewrites the ImportError to point users at the balance[did]
    extra. Shared adapter helpers (active_weight_column,
    drop_history_columns, validate_row_count, attach_balance_provenance)
    live in balance/interop/_common.py and column-name conventions in
    balance/interop/conventions.py so a future balance.interop.svy adapter
    can reuse them.
  • balance.stats_and_plots.weights_stats.kish_deff_stats — bundled
    Kish-design-effect diagnostic returning a KishStats(deff, ess, essp)
    namedtuple. Computes design_effect once and derives ESS and ESSP from it,
    avoiding three separate Deff computations when all three are needed.
    kish_ess(w) and kish_essp(w) are also exposed as ergonomic singletons
    over the existing design_effect. BalanceFrame._design_effect_diagnostics
    now routes through kish_deff_stats so the canonical Kish identities live
    in one place.
  • BalanceFrame.adjustment_history records compound adjustment steps.
    Sequential adjust() / set_fitted_model() workflows now keep a
    chronological, best-effort read-only copy of each adjustment step while
    preserving model as the latest fitted model for backwards compatibility.
    Baseline resets such as set_as_pre_adjust() clear the history together
    with the current model.
  • CLI --formula now accepts JSON lists for model-matrix formula lists.
    In addition to a single formula string, CLI users can pass values such as
    --formula='["age", "gender"]', which are parsed and forwarded as
    list[str] to IPW/CBPS model-matrix construction. Malformed, empty, or
    non-string JSON lists now fail during argument parsing.

Bug Fixes

  • rake() now correctly incorporates per-row design weights in final
    weights.
    Previously, every unit in the same raking cell received the same
    weight m_fit[c] / m_sample[c], ignoring its own design weight. The correct
    formula w_final_i = w_design_i × m_fit[c] / m_sample[c] is now applied,
    matching poststratify semantics and ensuring weighted marginals recover
    the target distribution when design weights are non-uniform. No-op when
    design weights are uniform (the common case).
  • rake() now gracefully handles single-variable adjustments. When
    rake(...) resolves to exactly one adjustment variable, it logs a warning
    and delegates to poststratify(...) instead of raising an assertion. This
    preserves passthrough behaviour for transformations, NA handling, trimming
    controls, and fit-metadata persistence while making
    BalanceFrame.fit(method="rake") more robust for one-variable inputs. In
    this delegated path, model metadata records method='poststratify' while
    returned weights keep the canonical rake_weight name.
  • CLI --num_lambdas now parses as a positive integer. Fractional,
    zero, negative, and non-numeric values fail fast during argument parsing
    instead of being accepted after coercion/truncation or failing later
    during IPW adjustment.
  • Validation-path cleanup in asmd, poststratify, and rake removes
    redundant/unreachable branches with no behavior loss:
    • asmd(...) uses a single authoritative invalid-std_type error path
      (Unknown std_type ...).
    • poststratify(..., store_fit_metadata=True) drops an unreachable
      "missing stored training weights" guard, and predict-time ratio-column
      collisions now use deterministic suffix-based naming (_cell_ratio,
      _cell_ratio_tmp, _cell_ratio_tmp2, ...).
    • rake._predict_weights_from_model(...) uses already-validated fit-time
      target weights directly for non-transfer replay.
  • Security: ws updated from 8.20.0 to 8.20.1 in website dependencies.
    Fixes CVE-2026-45736 (GHSA-58qx-3vcg-4xpx): uninitialized memory disclosure
    in websocket.close() when a TypedArray is passed as the reason argument.

Documentation

  • README cross-link to diff-diff. New "Design-based inference" parent
    section in
    README.md
    introduces the diff-diff integration above the API tour with a canonical
    Sample.from_frameset_targetadjustfit_did snippet. The
    Docusaurus tutorials index and the website landing page
    (HomepageFeatures.js) gain matching cross-references;
    .github/copilot-instructions.md gets a new review-checklist bullet for
    changes that touch balance/interop/diff_diff.py.
  • Survey-weighted DiD tutorial. New
    tutorials/balance_diff_diff_brfss.ipynb walks through a BRFSS-style
    staggered-adoption smoking-ban DiD use case end-to-end: load synthetic
    survey microdata via dd.generate_survey_did_data, reweight to ACS
    demographic marginals via balance.ipw, aggregate to a state-quarter panel
    via to_panel_for_did, fit Callaway-Sant'Anna doubly-robust DiD via
    fit_did, run HonestDiD sensitivity, build a combined diagnostic via
    as_balance_diagnostic, and contrast with the unweighted estimate.
    Self-contained and deterministic — CI re-executes via nbconvert. Committed
    with cleared outputs; deploy-website.yml re-runs and bakes outputs into
    the rendered Docusaurus pages.

Code Quality & Refactoring

  • All-zero weight inputs to _check_weights_series_are_valid now emit a
    UserWarning
    (when require_positive=False, the default). Previously,
    weighted statistics over an all-zero weight vector silently produced NaN /
    inf (sum(w*x)/sum(w) = 0/0). Callers that already passed
    require_positive=True (e.g. design_effect, nonparametric_skew,
    prop_above_and_below, weighted_median_breakdown_point) keep their
    ValueError behaviour. This affects internal callers like
    descriptive_statsasmd, which previously masked the failure mode.
  • Removed the scheduled migration FutureWarnings from SampleFrame.weight_column,
    SampleFrame.id_column, and BalanceFrame.id_column; the accessors continue to return
    column names, while weight_series and id_series return data.
  • Diagnostics construction now wires adjustment_failure metadata from model
    outputs when available (instead of hardcoding success), and supports an
    optional adjustment_failure_reason diagnostics row for richer failure
    reporting in downstream tooling.
  • Plotly distribution plotting now gracefully handles missing notebook mime
    rendering dependencies (nbformat) by skipping the interactive plot with a
    warning, avoiding runtime crashes in non-notebook/test environments.

Tests

  • Expanded targeted test coverage for predict-time metadata validation,
    replay/transfer edge cases...
Read more

0.20.0 (2026-04-26)

Choose a tag to compare

@talgalili talgalili released this 26 Apr 12:52

Breaking Changes

  • id_column now returns the column name (str) on SampleFrame,
    BalanceFrame, and Sample. Previously it returned ID data
    (pd.Series), which was inconsistent with weight_column (which returned
    a name after 0.19.0). Use id_series for ID data, weight_series for
    weight data. The accessor naming convention is now consistent:

    • *_column → column name (str): id_column, weight_column
    • *_series → column data (pd.Series): id_series, weight_series
    • df_* → DataFrame: df_covars, df_weights, df_outcomes

    Both id_column and weight_column emit FutureWarnings pointing at the
    new data-returning accessors; warnings will be removed after 2026-06-01.
    The BalanceDFSource protocol was updated accordingly (id_column
    id_series); custom implementations must rename this property.

  • Unknown kwargs to poststratify(...) now raise TypeError instead of
    being silently ignored, to catch typos. store_fit_metadata must be a
    boolean.

New Features

  • Added sklearn-style fit / predict_weights workflow on BalanceFrame

    • New entry points BalanceFrame.fit(...), design_matrix(on=..., data=...),
      predict_proba(on=..., output=..., data=...), and predict_weights(data=...)
      enable training a weighting model on one BalanceFrame and applying it to new
      data via data=... for one-liner holdout scoring
      (fitted.predict_weights(data=holdout_bf)).
    • Supports IPW, CBPS, and poststratify methods. Each stores the fit-time
      metadata needed to reconstruct weights (training design weights, trimming
      options, CBPS coefficients, poststratify cell-ratio tables, NA handling).
    • BalanceFrame.set_fitted_model(fitted) applies a fitted model from one
      BalanceFrame to another for holdout scoring workflows.
  • Added formula support in poststratify

    • poststratify(...) and BalanceFrame.adjust(method="poststratify", ...)
      now accept formula= (string or list) as an alternative to variables=.
    • Only interaction-style operators are supported: : (interaction),
      . (all common columns), - (exclude), and optional leading ~.
      Additive + and * are explicitly rejected because post-stratification
      defines cells by the joint distribution — a + b, a * b, and a:b
      would all produce identical cells, and rejecting +/* prevents users
      from silently writing what looks like a main-effects model.
    • Strict formula validation: empty/non-string entries, unknown variables,
      transformed terms, and passing both variables and formula all raise
      explicit ValueError. Note: raking operates on marginals, so +/*
      will be meaningful when raking gains its own formula= argument.
  • Added BalanceFrame.set_as_pre_adjust() to lock in the current
    responder state as the new pre-adjust baseline. Supports inplace=False
    (default, immutable) and inplace=True. Clears the adjustment model and
    unadjusted link since the object is no longer considered adjusted.

Code Quality & Refactoring

  • Safer target replacement on adjusted objectsBalanceFrame.set_target(...)
    now warns when replacing the target on an adjusted object in-place, since
    this resets responder weights to pre-adjust values and drops the current
    adjustment result.

  • Modernized weight dtype checks for pandas 3.0 compatibility
    SampleFrame.set_weights() paths now use explicit exact-float64 checks
    instead of deprecated float-dtype helpers, and always coerce assigned
    weights to exact float64.

  • Cleaned up legacy Python 2 __future__ compatibility imports in
    weighting methods
    — replaced obsolete imports with
    from __future__ import annotations in adjust_null, cbps,
    poststratify, and rake. Note: with this future import enabled,
    runtime annotation introspection changes (__annotations__ become
    postponed/stringized).

Tests

  • Added coverage for the new fit/predict_weights workflow (IPW, CBPS,
    poststratify), pickle/deepcopy roundtrips of fitted BalanceFrames,
    raw-covariate fit-matrix persistence (use_model_matrix=False),
    near-separation stability, empty-input validation, formula parsing edge
    cases (interaction syntax, dot expansion, explicit ~ formulas,
    validation failures), set_as_pre_adjust() behavior (copy / in-place /
    inherited-view sync), target-replacement warning emission, weight-column
    casting when active weight dtype is non-float/float32, and chained
    IPW→poststratify adjustment behavior.

Contributors

@talgalili, @sahil350 ,@neuralsorcerer

Full Changelog

0.19.0...0.20.0

0.19.0 (2026-04-06)

Choose a tag to compare

@talgalili talgalili released this 06 Apr 20:14

Highlights

This is a major architecture release. balance now has two new foundational
classes — SampleFrame and BalanceFrame — that cleanly separate data
representation from adjustment logic. The existing Sample API is fully backward
compatible
; Sample now inherits from both new classes
(Sample → BalanceFrame → SampleFrame) and all existing code continues to work
unchanged. The new classes can also be used directly for a more explicit,
composable workflow — see the new tutorial notebook for a complete walkthrough.

For full technical details — including ASCII diagrams of the class hierarchy,
internal structure, column classification, object lifecycle, linked-samples
expansion, data flow, and the Sample.__new__ guard — see
docs/architecture/architecture_0_19_0.md.

Breaking Changes

  • Removed Sample.design_effect() — use sample.weights().design_effect() instead.
    Deprecated since 0.18.0.
  • Removed Sample.design_effect_prop() — use sample.weights().design_effect_prop() instead.
    Deprecated since 0.18.0.
  • Removed Sample.plot_weight_density() — use sample.weights().plot() instead.
    Deprecated since 0.18.0.
  • Removed Sample.covar_means() — use sample.covars().mean() instead
    (with .rename(index={'self': 'adjusted'}).reindex(['unadjusted', 'adjusted', 'target']).T for the same format).
    Deprecated since 0.18.0.
  • Removed Sample.outcome_sd_prop() — use sample.outcomes().outcome_sd_prop() instead.
    Deprecated since 0.18.0.
  • Removed Sample.outcome_variance_ratio() — use sample.outcomes().outcome_variance_ratio() instead.
    Deprecated since 0.18.0.

New Features

  • Added SampleFrame — DataFrame container with explicit column-role metadata
    (sample_frame.py). Holds a DataFrame and tracks which columns are covariates,
    weights, outcomes, predicted outcomes, and ignored.

    • Factory methods: from_frame() (with explicit covar_columns parameter
      and auto-detection of id/weight columns) and from_csv().
    • DataFrame view properties: df_covars, df_outcomes, df_weights,
      df_ignored, id_column, weight_series.
    • Column-role list properties: covar_columns, weight_columns_all,
      outcome_columns, predicted_outcome_columns, ignored_columns.
    • Weight management: add_weight_column(), set_active_weight(),
      rename_weight_column(), set_weight_metadata() / weight_metadata() for
      provenance tracking.
    • Comprehensive input validation (null/negative/non-numeric weights, null IDs,
      duplicate IDs, overlapping column roles).
    • See
      docs/architecture/architecture_0_19_0.md
      for column classification rules, auto-detection logic, and internal structure.
  • Added BalanceFrame — adjustment orchestrator for survey weighting
    (balance_frame.py). Pairs a responder SampleFrame with a target SampleFrame
    for reweighting.

    • Public constructor: BalanceFrame(sample=..., target=...). Target is optional —
      BalanceFrame(sample=sf) creates a target-less instance.
    • Core API: set_target(), adjust(), covars() / weights() / outcomes()
      (BalanceDF views), summary(), diagnostics().
    • adjust(method="ipw") returns a new BalanceFrame (immutable pattern) with
      adjusted weights. Supports "ipw", "cbps", "rake", "poststratify",
      "null", and custom callables.
    • Convenience properties: df (responder-only DataFrame, mirrors Sample.df),
      df_all (combined responder + target + unadjusted with "source" column),
      has_target, is_adjusted, model.
    • Covariate overlap validation at construction and set_target().
    • to_csv(), to_download(), keep_only_some_rows_columns() for export
      and filtering.
    • See
      docs/architecture/architecture_0_19_0.md
      for property delegation, adjustment flow, and linked-samples expansion.
  • Compound/sequential adjustmentsadjust() can now be called multiple
    times on the same object. Each call uses the current (previously adjusted)
    weights as design weights, compounding adjustments. For example, run IPW first
    to correct broad imbalances, then rake on a specific variable for fine-tuning.
    The active weight column always keeps its original name (e.g., "weight");
    the original unadjusted baseline is always preserved for diagnostics
    (asmd_improvement() shows total improvement across all steps). See
    docs/architecture/architecture_0_19_0.md
    for the weight history tracking mechanism (weight_pre_adjust,
    weight_adjusted_N columns).

  • Sample refactored to inherit from SampleFrame and BalanceFrame
    Sample is now a thin facade (~242 lines) via multiple inheritance
    (Sample → BalanceFrame → SampleFrame). All adjustment, diagnostics, and
    data-access logic lives in the base classes. No public API changes — all
    existing Sample methods continue to work identically.

  • Sample.is_adjusted is now a @property returning _CallableBool — works
    both as sample.is_adjusted (property) and sample.is_adjusted() (legacy
    method call). _CallableBool also supports arithmetic via __mul__/__rmul__.

  • Added bidirectional conversion between Sample, SampleFrame, and BalanceFrame

    • SampleFrame.from_sample(sample) / Sample.to_sample_frame(): convert a
      Sample to a SampleFrame with proper column-role mapping.
    • BalanceFrame.from_sample(sample) / Sample.to_balance_frame(): convert a
      Sample (with target) to a BalanceFrame, preserving adjustment state.
    • BalanceFrame.to_sample(): convert a BalanceFrame back to a Sample.
  • Added formula support to Sample.covars() for downstream diagnostics

    • Sample.covars() now accepts a formula argument and stores it on the
      returned BalanceDFCovars object.
    • BalanceDFCovars.kld() now honors formula-driven model matrices (including
      interactions such as "age_group * gender") when a formula is provided via
      covars(formula=...).
    • Formula settings are now propagated to linked covariate views (target,
      unadjusted) so comparative diagnostics run on consistent design matrices.

Code Quality & Refactoring

  • Defined BalanceDFSource protocol and decoupled BalanceDF from Sample
    — Added BalanceDFSource runtime-checkable protocol that both Sample and
    SampleFrame satisfy, enabling BalanceDF to work with either. Removed the
    hard from balance.sample_class import Sample import from balancedf_class.py.
    See docs/architecture/architecture_0_19_0.md
    for protocol members and satisfaction diagram.

  • Extracted _build_summary() and _build_diagnostics() into summary_utils.py
    — Standalone functions accepting plain DataFrames/Series, enabling code reuse
    across Sample and BalanceFrame without circular imports.

  • BalanceDF.__init__(): added optional links parameter for explicit link
    injection, allowing BalanceDF to work with sources that do not carry mutable
    _links (e.g. SampleFrame).

  • Typing modernizationDictdict, Tupletuple, Listlist
    in annotations. cast() replaced with _assert_type() where applicable.
    Internal naming standardized across BalanceDF helpers (snake_case convention).

Tutorials

  • Added balance_quickstart_new_api.ipynb — end-to-end tutorial demonstrating the
    new SampleFrame/BalanceFrame API. Mirrors the original balance_quickstart.ipynb
    step-by-step but uses only the new classes (no Sample). Covers: loading data,
    creating SampleFrames, building a BalanceFrame, adjusting (IPW + CBPS), inspecting
    diagnostics (summary, ASMD, covariate means, design effect), visualization
    (plotly, seaborn KDE, ASCII plots), outcome analysis, transformations, compound
    adjustments, filtering rows/columns, and exporting to CSV.

Documentation

  • Added ARCHITECTURE.md — new top-level architecture document covering the class hierarchy,
    5-step workflow, key classes, weighting methods, supporting modules, and file layout.
    CLAUDE.md now links to this file instead of duplicating architecture content.
  • Added docs/architecture/architecture_0_19_0.md — detailed technical record of the 0.19.0
    architecture with ASCII diagrams covering: class hierarchy before/after, SampleFrame internals,
    BalanceFrame internal structure, object lifecycle state transitions, BalanceDF linked-samples
    expansion, BalanceDFSource protocol, BalanceDF class hierarchy, data flow, _links graph, and
    Sample.new guard.
  • Updated README.md — added "Developer and AI assistant resources" section linking to
    ARCHITECTURE.md and CLAUDE.md.

LLM/GenAI

  • Updated CLAUDE.md project context files for Claude Code users, covering architecture,
    build/test instructions (Meta and open-source), code conventions, and pre-submit checklist.
  • Updated .github/copilot-instructions.md review checklist to add missing conventions
    (MIT license header, from __future__ import annotations, factory pattern, seed fixing,
    deprecation style).

Tests

  • Added comprehensive test suites for the new classes:
    • test_balance_frame.py: ~120 tests covering construction, adjustment (all
      methods), covars/weights/outcomes integration, summary/diagnostics, analytics,
      df/export/filter, missing dat...
Read more

0.18.0 (2026-03-24)

Choose a tag to compare

@talgalili talgalili released this 24 Mar 19:58

New Features

  • Implemented r_indicator() with validated sample-variance formula
    • Added a public r_indicator(sample_p, target_p) implementation in
      weighted_comparisons_stats using the documented Eq. 2.2.2 formulation
      over concatenated propensity vectors and explicit input-size validation.
    • Added validation for non-finite and out-of-range propensity values,
      and expanded unit coverage for formula correctness and edge cases.
    • Added BalanceDFWeights.r_indicator() as a convenience wrapper, so
      sample.weights().r_indicator() computes the r-indicator directly.

Deprecations

  • Sample.design_effect() is deprecated — use sample.weights().design_effect() instead.
    The method already exists on BalanceDFWeights; the Sample method now emits a
    DeprecationWarning and delegates. Will be removed in balance 0.19.0.
  • Sample.design_effect_prop() is deprecated — use sample.weights().design_effect_prop() instead.
    New method added to BalanceDFWeights. Will be removed in balance 0.19.0.
  • Sample.plot_weight_density() is deprecated — use sample.weights().plot() instead.
    Will be removed in balance 0.19.0.
  • Sample.covar_means() is deprecated — use sample.covars().mean() instead
    (with .rename(index={'self': 'adjusted'}).reindex([...]).T for the same format).
    Will be removed in balance 0.19.0.
  • Sample.outcome_sd_prop() is deprecated — use sample.outcomes().outcome_sd_prop() instead.
    New method added to BalanceDFOutcomes. Will be removed in balance 0.19.0.
  • Sample.outcome_variance_ratio() is deprecated — use sample.outcomes().outcome_variance_ratio() instead.
    New method added to BalanceDFOutcomes. Will be removed in balance 0.19.0.

LLM/GenAI

  • Added CLAUDE.md project context files for Claude Code users, covering architecture,
    build/test instructions (Meta and open-source), code conventions, and pre-submit checklist.
  • Updated .github/copilot-instructions.md review checklist to reduce duplication with
    CLAUDE.md and add missing conventions (MIT license header, from __future__ import annotations,
    factory pattern, seed fixing, deprecation style).

Bug Fixes

  • prepare_marginal_dist_for_raking / _realize_dicts_of_proportions: fixed memory explosion from LCM expansion
    • When proportions had high decimal precision or many covariates were passed,
      the LCM of the individual per-variable array lengths could reach tens of
      millions (or more), causing OOM crashes.
    • Both functions now accept a max_length parameter (default 10000). When
      the natural LCM exceeds max_length, the output is capped at max_length
      rows and counts are allocated via the Hare-Niemeyer (largest remainder)
      method, which guarantees the total stays exactly max_length with minimal
      rounding error per category.
    • A warning is logged whenever the cap is applied.
    • A new internal helper _hare_niemeyer_allocation implements the allocation logic.

Contributors

@neuralsorcerer, @talgalili

Full Changelog: 0.17.0...0.18.0

0.17.0 (2026-03-17)

Choose a tag to compare

@talgalili talgalili released this 17 Mar 14:01

Breaking Changes

  • CLI: unmentioned columns now go to ignore_columns instead of outcome_columns
    • Previously, when --outcome_columns was not explicitly set, all columns that
      were not the id, weight, or a covariate were automatically classified as
      outcome columns. Now those columns are placed into ignore_columns instead.
    • Columns that are explicitly mentioned — the id column, weight column,
      covariate columns, and outcome columns — are not ignored.

New Features

  • ASCII comparative histogram and plot improvements
    • Added ascii_comparative_hist for comparing multiple distributions against a
      baseline using inline visual indicators (, , , ).
    • Comparative ASCII plots now order datasets as population → adjusted → sample.
    • ascii_plot_dist accepts a new comparative keyword (default True) to
      toggle between comparative and grouped-bar histograms for numeric variables.

Code Quality & Refactoring

  • Moved dataset loading implementations out of balance.datasets.__init__
    • Refactored load_sim_data, load_cbps_data, and load_data into
      balance.datasets.loading_data and re-exported them from
      balance.datasets to preserve the public API while keeping module
      responsibilities focused.

Documentation

  • ASCII plot documentation and tutorial examples
    • Added rendered text-plot examples to ASCII plot docstrings and documented
      library="balance" support. Updated balance_quickstart.ipynb with
      adjusted vs unadjusted ASCII plot examples.
  • Improved keep_columns documentation
    • Updated docstrings for has_keep_columns(), keep_columns(), and the
      --keep_columns argument to clarify that keep columns control which columns
      appear in the final output CSV. Keep columns that are not id, weight,
      covariate, or outcome columns will be placed into ignore_columns during
      processing but are still retained and available in the output.
  • Clarified _prepare_input_model_matrix argument docs
    • Updated docstrings in balance.utils.model_matrix with
      explicit descriptions for sample, target, variables, and add_na
      behavior when preparing model-matrix inputs.

Bug Fixes

  • Weight diagnostics now consistently accept DataFrame inputs
    • design_effect, nonparametric_skew, prop_above_and_below, and
      weighted_median_breakdown_point now explicitly normalize DataFrame inputs
      to their first column before computation, matching validation behavior and
      returning scalar/Series outputs consistently.
  • Model-matrix robustness improvements
    • _make_df_column_names_unique() now avoids suffix collisions when columns
      like a, a_1, and repeated a names appear together, renaming
      duplicates deterministically to prevent downstream clashes.
    • _prepare_input_model_matrix() now raises a deterministic ValueError
      when the input sample has zero rows, instead of relying on an assertion.
  • Stabilized prop_above_and_below() return paths
    • prop_above_and_below() now builds concatenated outputs only from present
      Series objects and returns None when both below and above are None,
      avoiding ambiguous concat inputs while preserving existing behavior for valid
      threshold sets.
  • Validated and normalized comma-separated CLI column arguments
    • CLI column-list arguments now trim surrounding whitespace and reject empty
      entries (for example, "id,,weight") with clear ValueError messages,
      preventing malformed column specifications from silently propagating.
    • Applied to --covariate_columns, --covariate_columns_for_diagnostics,
      --batch_columns, --keep_columns, and --outcome_columns parsing.

Tests

  • Added end-to-end adjustment test with ASCII plot output and expanded ASCII plot edge-case coverage
    • TestAsciiPlotsAdjustmentEndToEnd runs the full adjustment pipeline and
      asserts exact expected ASCII output. Added tests for ascii_plot_dist with
      comparative=False and mixed categorical+numeric routing.
  • Expanded warning coverage for Sample.from_frame() ID inference
    • Added assertions that validate all three expected warnings are emitted when inferring an id column and default weights, including ID guessing, ID string casting, and automatic weight creation.
  • Expanded IPW helper and diagnostics test coverage
    • Added tests for link_transform() and calc_dev() to validate behavior
      for extreme probabilities and finite 10-fold deviance summaries.
    • Refactored diagnostics tests to use a shared IPW setup helper, added
      edge-case assertions for solver/penalty values, NaN coercion of non-scalar
      inputs, and now assert labels match fitted model parameters.
  • Expanded prop_above_and_below() edge-case coverage
    • Added focused tests for empty threshold iterables, mixed None threshold groups in dict mode, and explicit all-None threshold handling across return formats.
  • Added unit coverage for CLI I/O and empty-batch handling
    • Added focused tests for BalanceCLI.process_batch() empty-sample failure payloads, load_and_check_input() CSV loading paths, and write_outputs() delimiter-aware output writing for both adjusted and diagnostics files.

Contributors

@sahil350 , @neuralsorcerer, @talgalili

Full Changelog

0.16.0...0.17.0

0.16.0 (2026-02-09)

Choose a tag to compare

@talgalili talgalili released this 09 Feb 15:23

New Features

  • Outcome weight impact diagnostics
    • Added paired outcome-weight impact tests (y*w0 vs y*w1) with confidence intervals.
    • Exposed in BalanceDFOutcomes, Sample.diagnostics(), and the CLI via
      --weights_impact_on_outcome_method.
  • Pandas 3 support
    • Updated compatibility and tests for pandas 3.x
  • Categorical distribution metrics without one-hot encoding
    • KLD/EMD/CVMD/KS on BalanceDF.covars() now operate on raw categorical variables
      (with NA indicators) instead of one-hot encoded columns.
  • Misc
    • Raw-covariate adjustment for custom models
      • Sample.adjust() now supports fitting models on raw covariates (without a model matrix)
        for IPW via use_model_matrix=False. String, object, and boolean columns are converted
        to pandas Categorical dtype, allowing sklearn estimators with native categorical
        support (e.g., HistGradientBoostingClassifier with categorical_features="from_dtype")
        to handle them correctly. Requires scikit-learn >= 1.4 when categorical columns are
        present.
    • Validate weights include positive values
      • Added a guard in weight diagnostics to error when all weights are zero.
    • Support configurable ID column candidates
      • Sample.from_frame() and guess_id_column() now accept candidate ID column names
        when auto-detecting the ID column.
    • Formula support for BalanceDF model matrices
      • BalanceDF.model_matrix() now accepts a formula argument to build
        custom model matrices without precomputing them manually.

Bug Fixes

  • Removed deprecated setup build
    • Replaced deprecated setup.py with pyproject.toml build in CI to avoid build failure.
  • Hardened ID column candidate validation
    • guess_id_column() now ignores duplicate candidate names and validates that candidates are non-empty strings.
  • Hardened pandas 3 compatibility paths
    • Updated string/NA handling and discrete checks for pandas 3 dtypes, and refreshed tests to accept string-backed dtypes.

Packaging & Tests

  • Pandas 3.x compatibility
    • Expanded the pandas dependency range to allow pandas 3.x releases.
  • Direct util imports in tests
    • Refactored util test modules to import helpers directly from their modules instead of via balance_util.

Breaking Changes

  • Require positive weights for weight diagnostics that normalize or aggregate
    • design_effect, nonparametric_skew, prop_above_and_below, and
      weighted_median_breakdown_point now raise a ValueError when all weights
      are zero.
    • Migration: ensure your weights include at least one positive value
      before calling these diagnostics, or catch the ValueError if all-zero
      weights are possible in your workflow.

Contributors

@neuralsorcerer, @talgalili (with code/methodological review by @talsarig)

Full Changelog: 0.15.0...0.16.0

0.15.0 (2026-01-20)

Choose a tag to compare

@talgalili talgalili released this 20 Jan 10:51

New Features

  • Added EMD/CVMD/KS distribution diagnostics
    • BalanceDF now exposes Earth Mover's Distance (EMD), Cramér-von Mises distance (CVMD), and Kolmogorov-Smirnov (KS) statistics for comparing adjusted samples to targets.
    • These diagnostics support weighted or unweighted comparisons, apply discrete/continuous formulations, and respect aggregate_by_main_covar for one-hot categorical aggregation.
  • Exposed outcome columns selection in the CLI
    • Added --outcome_columns to choose which columns are treated as outcomes
      instead of defaulting to all non-id/weight/covariate columns. Remaining columns are moved to ignored_columns.
  • Improved missing data handling in poststratify()
    • poststratify() now accepts na_action to either drop rows with missing
      values or treat missing values as their own category during weighting.
    • Breaking change: the default behavior now fills missing values in
      poststratification variables with "__NaN__" and treats this as a distinct
      category during weighting. Previously, missing values were not handled
      explicitly, and their treatment depended on pandas groupby and merge
      defaults. To approximate the legacy behavior where missing values do not
      form their own category, pass na_action="drop" explicitly.
  • Added formula support for descriptive_stats model matrices
    • descriptive_stats() now accepts a formula argument that is always
      applied to the data (including numeric-only frames), letting callers
      control which terms and dummy variables are included in summary statistics.

Documentation

  • Documented the balance CLI
    • Added full API docstrings for balance.cli and a new CLI tutorial notebook.
  • Created Balance CLI tutorial
  • Synchronized docstring examples with test cases
    • Updated user-facing docstrings so the documented examples mirror tested inputs
      and outputs.

Code Quality & Refactoring

  • Added warning when the sample size of 'target' is much larger than 'sample' sample size
    • Sample.adjust() now warns when the target exceeds 100k rows and is at
      least 10x larger than the sample, highlighting that uncertainty is
      dominated by the sample (akin to a one-sample comparison).
  • Split util helpers into focused modules
    • Broke balance.util into balance.utils submodules for easier navigation.

Bug Fixes

  • Updated Sample.__str__() to format weight diagnostics like Sample.summary()
    • Weight diagnostics (design effect, effective sample size proportion, effective sample size)
      are now displayed on separate lines instead of comma-separated on one line.
    • Replaced "eff." abbreviations with full "effective" word for better readability.
    • Improves consistency with Sample.summary() output format.
  • Numerically stable CBPS probabilities
    • The CBPS helper now uses a stable logistic transform to avoid exponential
      overflow warnings during probability computation in constraint checks.
  • Silenced pandas observed default warning
    • Explicitly sets observed=False in weighted categorical KLD calculations
      to retain current behavior and avoid future pandas default changes.
  • Fixed plot_qq_categorical to respect the weighted parameter for target data
    • Previously, the target weights were always applied regardless of the
      weighted=False setting, causing inconsistent behavior between sample
      and target proportions in categorical QQ plots.
  • Restored CBPS tutorial plots
  • Clearer validation errors in adjustment helpers
    • trim_weights() now accepts list/tuple inputs and reports invalid types explicitly.
    • apply_transformations() raises clearer errors for invalid inputs and empty transformations.
  • Fixed model_matrix to drop NA rows when requested
    • model_matrix(add_na=False) now actually drops rows containing NA values while preserving categorical levels, matching the documented behavior.
    • Previously, add_na=False only logged a warning without dropping rows; code relying on the old behavior may now see fewer rows and should either handle missingness explicitly or use add_na=True.

Tests

  • Aligned formatting toolchain between Meta internal and GitHub CI
    • Added ["fbcode/core_stats/balance"] override to Meta's internal tools/lint/pyfmt/config.toml to use formatter = "black" and sorter = "usort".
    • This ensures both internal (pyfmt/arc lint) and external (GitHub Actions) environments use the same Black 25.1.0 formatter, eliminating formatting drift.
    • Updated CI workflow, pre-commit config, and requirements-fmt.txt to use black==25.1.0.
  • Added Pyre type checking to GitHub Actions via .pyre_configuration.external and a new pyre job in the workflow. Tests are excluded due to external typeshed stub differences; library code is fully type-checked.
  • Added test coverage workflow and badge to README via .github/workflows/coverage.yml. The workflow collects coverage using pytest-cov, generates HTML and XML reports, uploads them as artifacts, and displays coverage metrics. A coverage badge is now shown in README.md alongside other workflow badges.
  • Improved test coverage for edge cases and error handling paths
    • Added targeted tests for previously uncovered code paths across the library, addressing edge cases including empty inputs, verbose logging, error handling for invalid parameters, and boundary conditions in weighting methods (IPW, CBPS, rake).
    • Tests exercise defensive code paths that handle empty DataFrames, NaN convergence values, invalid model types, and non-convergence warnings.
  • Split test_util.py into focused test modules
    • Split the large test_util.py file (2325 lines) into 5 modular test files that mirror the balance/utils/ structure:
      • test_util_data_transformation.py - Tests for data transformation utilities
      • test_util_input_validation.py - Tests for input validation utilities
      • test_util_model_matrix.py - Tests for model matrix utilities
      • test_util_pandas_utils.py - Tests for pandas utilities (including high cardinality warnings)
      • test_util_logging_utils.py - Tests for logging utilities
    • This improves test organization and makes it easier to locate tests for specific utilities.

Contributors

@neuralsorcerer, @talgalili

Full Changelog: 0.14.0...0.15.0

0.14.0 (2025-12-14)

Choose a tag to compare

@talgalili talgalili released this 14 Dec 09:31

New Features

  • Enhanced adjusted sample summary output
    • Sample.__str__() now displays adjustment details (method, trimming
      parameters, design effect, effective sample size) when printing adjusted
      samples (#194,
      #57).
  • Richer Sample.summary() diagnostics
    • Adjusted sample summary now groups covariate diagnostics, reports design
      effect alongside ESSP/ESS, and surfaces weighted outcome means when
      available.
  • Warning of high-cardinality categorical features in .adjust()
    • Categorical features where ≥80% of values are unique are flagged before
      weight fitting to help identify problematic columns like user IDs
      (#195,
      #65).
  • Ignored column handling for Sample inputs
    • Sample.from_frame accepts ignore_columns for columns that should remain
      on the dataframe but be excluded from covariates and outcome statistics.
      Ignored columns appear in Sample.df and can be retrieved via
      Sample.ignored_columns().

Code Quality & Refactoring

  • Consolidated diagnostics helpers
    • Added _concat_metric_val_var() helper and balance.util._coerce_scalar
      for robust diagnostics row construction and scalar-to-float conversion.
    • Breaking change: Sample.diagnostics() for IPW now always emits
      iteration/intercept summaries plus hyperparameter settings.

Bug Fixes

  • Early validation of null weight inputs
    • Sample.from_frame now raises ValueError when weights contain None,
      NaN, or pd.NA values with count and preview of affected rows.
  • Percentile weight trimming across platforms
    • trim_weights() now computes thresholds via percentile quantiles with
      explicit clipping bounds for consistent behavior across Python/NumPy
      versions.
    • Breaking change: percentile-based clipping may shift by roughly one
      observation at typical limits.
  • IPW diagnostics improvements
    • Fixed multi_class reporting, normalized scalar hyperparameters to floats,
      removed deprecated penalty argument warnings, and deduplicated metric
      entries for stable counts across sklearn versions.

Tests

  • Added Windows and macOS CI testing support
    • Expanded GitHub Actions to run on ubuntu-latest, macos-latest, and
      windows-latest for Python 3.9-3.14.
    • Added tempfile_path() context manager for cross-platform temp file
      handling and configured matplotlib Agg backend via conftest.py.

Contributors

@neuralsorcerer, @talgalili, @wesleytlee

Full Changelog

0.13.0...0.14.0

0.13.0 (2025-12-02)

Choose a tag to compare

@talgalili talgalili released this 02 Dec 16:49

New Features

  • Propensity modeling beyond static logistic regression
    • ipw() now accepts any sklearn classifier via the model argument,
      enabling the use of models like random forests and gradient boosting while
      preserving all existing trimming and diagnostic features. Dense-only
      estimators and models without linear coefficients are fully supported.
      Propensity probabilities are stabilized to avoid numerical issues.
    • Allow customization of logistic regression by passing a configured
      :class:~sklearn.linear_model.LogisticRegression instance through the
      model argument. Also, the CLI now accepts
      --ipw_logistic_regression_kwargs JSON to build that estimator directly for
      command-line workflows.
  • Covariate diagnostics
    • Added KL divergence calculations for covariate comparisons (numeric and
      one-hot categorical), exposed via BalanceDF.kld() alongside linked-sample
      aggregation support.
  • Weighting Methods
    • rake() and poststratify() now honour weight_trimming_mean_ratio and
      weight_trimming_percentile, trimming and renormalising weights through the
      enhanced trim_weights(..., target_sum_weights=...) API so the documented
      parameters work as expected
      (#147).

Documentation

  • Added comprehensive post-stratification tutorial notebook
    (balance_quickstart_poststratify.ipynb)
    (#141,
    #142,
    #143).
  • Expanded poststratify docstring with clear examples and improved statistical
    methods documentation
    (#141).
  • Added project badges to README for build status, Python version support, and
    release tracking
    (#145).
  • Added IPW quickstart tutorial showcasing default logistic regression and
    custom sklearn classifier usage in (balance_quickstart.ipynb).
  • Shorten the welcome message (for when importing the package).

Code Quality & Refactoring

  • Raking algorithm refactor

    • Removed ipfn dependency and replaced with a vectorized NumPy
      implementation (_run_ipf_numpy) for iterative proportional fitting,
      resulting in significant performance improvements and eliminating external
      dependency (#135).
  • IPW method refactoring

    • Reduced Cyclomatic Complexity Number (CCN) by extracting repeated code
      patterns into reusable helper functions: _compute_deviance(),
      _compute_proportion_deviance(), _convert_to_dense_array().
    • Removed manual ASMD improvement calculation and now uses existing
      compute_asmd_improvement() from weighted_comparisons_stats.py
  • Type safety improvements

    • Migrated 32 Python files from # pyre-unsafe to # pyre-strict mode,
      covering core modules, statistics, weighting methods, datasets, and test
      files
    • Modernized type hints to PEP 604 syntax (X | Y instead of Union[X, Y])
      across 11 files for improved readability and Python 3.10+ alignment
    • Type alias definitions in typing.py retain Union syntax for Python 3.9
      compatibility
    • Enhanced plotting function type safety with TypedDict definitions and
      proper type narrowing
    • Replaced assert-based type narrowing with _verify_value_type() helper for
      better error messages and pyre-strict compliance
  • Renamed BalanceDF to BalanceDF****

    • BalanceCovarsDF to BalanceDFCovars
    • BalanceOutcomesDF to BalanceDFOutcomes
    • BalanceWeightsDF to BalanceDFWeights

Bug Fixes

  • Utility Functions
    • Fixed quantize() to preserve column ordering and use proper TypeError
      exceptions (#133)
  • Statistical Functions
    • Fixed division by zero in asmd_improvement() when asmd_mean_before is
      zero, now returns 0.0 for 0% improvement
  • CLI & Infrastructure
    • Replaced deprecated argparse FileType with pathlib.Path
      (#134)
  • Weight Trimming
    • Fixed trim_weights() to consistently return pd.Series with
      dtype=np.float64 and preserve original index across both trimming methods
    • Fixed percentile-based winsorization edge case: _validate_limit() now
      automatically adjusts limits to prevent floating-point precision issues
      (#144)
    • Enhanced documentation for trim_weights() and _validate_limit() with
      clearer examples and explanations

Tests

  • Enhanced test coverage for weight trimming with
    test_trim_weights_return_type_consistency and 11 comprehensive tests for
    _validate_limit() covering edge cases, error conditions, and boundary
    conditions

Contributors

@neuralsorcerer, @talgalili, @wesleytlee

Full Changelog: 0.12.1...0.13.0

0.12.1 (2025-11-03)

Choose a tag to compare

@talgalili talgalili released this 03 Nov 09:30

New Features

  • Added a welcome message when importing the package.

Welcome to balance (Version 0.12.1)!
An open-source Python package for balancing biased data samples.

📖 Documentation: https://import-balance.org/
🛠️ Get Help / Report Issues: https://github.com/facebookresearch/balance/issues/
📄 Citation:
Sarig, T., Galili, T., & Eilat, R. (2023).
balance - a Python package for balancing biased data samples.
https://arxiv.org/abs/2307.06024

Tip: You can access this information at any time with balance.help()

Documentation

Bug Fixes

Contributors

@talgalili, @wesleytlee

Full Changelog: 0.12.0...0.12.1