Skip to main content

updatesupport

PyPI Version

Check whether a reported aggregate is stable to subgroup recomposition.

Reports often publish a number by coarse categories. updatesupport asks:

If those public category counts stayed fixed, could the aggregate still move because the finer subgroup mix inside them changed?

This is a practical audit for aggregation bias, Simpson's paradox risk, ecological-fallacy risk, subgroup composition sensitivity, and coarsened fairness or disparity reports. It quantifies hidden-composition ambiguity: how much an aggregate rate, effect, or risk metric could move under a declared recomposition stress test.

Thirty-second example. In the Folktables ACSIncome demo, a report grouped by age band, education band, and sex observes an income-threshold rate of 12.37%. Holding those public group proportions fixed, retained subgroup composition can move the compatible rate to:

11.79% to 13.44%

That 1.65 percentage-point width is not sampling error. It is the amount of aggregation ambiguity left by the public categories. The best one-column refinement in that example is occupation major group.

Here, "hidden" means retained but not publicly reported, not unobserved or unknowable. The ambiguity interval is always relative to the retained refinement you choose and the admissible shift class Q you declare. It is not an absolute bound on every possible omitted variable or population shift.

It is useful when a dashboard, model-review pack, policy table, or causal analysis reports a number by coarse public categories and you need to know:

Are these categories enough to support this reported estimate?

Why It Exists

Many reports publish estimates by coarse buckets:

age band x education band x sex
product x region x FICO band x LTV band
segment x channel

Those buckets may contain retained subgroups with different target rates or effects. A confidence interval answers sampling uncertainty. A model metric answers model fit. updatesupport answers a different review question:

Holding the reported public mix fixed, how far could the aggregate move under plausible hidden subgroup shifts?

The output is a review-ready Markdown audit with:

  • claim-level pass/fail/inconclusive verdicts
  • observed estimate
  • hidden-composition stress interval
  • transport ambiguity, the interval width
  • estimator-uncertainty-aware interval when hidden-cell standard errors are supplied
  • public adequacy flag
  • public cells driving the ambiguity
  • refinement recommendations
  • sensitivity checks
  • public-representation frontier search for small stable reporting designs

This is not a replacement for causal identification, model validation, or sampling uncertainty. It is an audit layer for the reporting representation.

The project does not claim new mathematics. The core calculation is a partial-identification / sensitivity-analysis problem with roots in ecological inference, Frechet-style bounds, and distributionally robust optimization. The value is the packaged workflow: compile a retained fine table, solve the stress test, explain the ambiguity, rank refinements, run sensitivity checks, and emit review-ready artifacts. See docs/positioning-and-lineage.md.

Why the Math Is Sound

updatesupport reduces the review question to a finite optimization problem. Given retained fine cells D, public buckets pi(D), supplied retained-cell target values h, and an explicit admissible distribution class Q, it computes:

inf/sup  sum_d h(d) q(d)
subject to q in Q and fixed public law pi#q

For saturated reweighting this has a closed-form public-fiber range formula. For linear Q it is solved as a linear program. For convex transport budgets such as TV, chi-square, KL, and Wasserstein, it is solved as a convex CVXPY problem with a linear objective.

The interval is statistically meaningful as a partial-identification or sensitivity interval conditional on the retained support, target values, and chosen Q. It is not a confidence interval. If retained-cell target standard errors are supplied, reports can add an estimator-uncertainty-aware outer interval, but causal, survey-design, and broader model uncertainty still belong to the upstream statistical workflow. With CVXPY-compatible Q sets, reports can also include an SOCP confidence-core diagnostic showing whether all admissible composition-specific confidence bands have a common overlap.

The core solver target is fixed after compilation. Most tabular reports compile to the linear plug-in aggregate sum_d h(d) q(d); RatioTarget covers supported fixed ratio targets; MomentTransformTarget covers affine moment transforms, one-sided convex/concave CVXPY endpoints, and conservative monotone moment bounds; and ProcedureTarget handles representation-dependent reporting procedures by recompiling the target for each representation before solving. Other nonlinear targets that depend directly on q still need an explicit reformulation or a dedicated target-functional backend.

See docs/mathematical-statistical-soundness.md for the full assumptions, formulas, backend checks, and limitations.

Quickstart

pip install updatesupport

or, in a uv project:

uv add updatesupport
import updatesupport as us

audit = us.claim(
    "Income-threshold rate is stable enough to report",
    public=["AGE_BAND", "EDU_BAND", "SEX"],
    hidden=["AGE_BAND", "EDU_BAND", "SEX", "OCC_MAJOR", "WKHP_BAND", "RAC1P"],
    target="income_over_threshold",
    weight="sample_weight",
    candidate_refinements=["OCC_MAJOR", "WKHP_BAND", "RAC1P"],
    ambiguity_limit=0.015,
    min_cell_weight=25,
    q_presets=["saturated"],
).audit(rows_or_frame)

print(audit.to_markdown())

The claim audit asks:

If we only report by AGE_BAND x EDU_BAND x SEX, how much could the aggregate income-threshold rate move if occupation, work hours, and race composition changed inside those public cells?

The stress interval is not a confidence interval. It is a partial-identification / sensitivity interval for hidden composition. The audit returns one review artifact with the verdict, interval, witness, limitations, and claim-centered refinement recommendations.

To require sufficient input coverage after sparse-cell filtering, add max_dropped_weight_share=0.05 to the claim. More than 5% discarded weight produces an inconclusive verdict even if the retained-population interval is narrow. Retained weight is shown beside the verdict; see data diagnostics.

Front-Door Demo

Start with the Folktables ACSIncome case if you want the shortest path to the idea. It is close to fairness and disparity-audit workflows because it asks whether a coarse demographic public report survives retained subgroup composition changes.

In that demo, the observed target rate is 12.37%. When the public mix by age band, education band, and sex is held fixed, hidden subgroup composition can move the target rate from:

11.79% to 13.44%

The transport ambiguity is 1.65 percentage points. In plain English:

The coarse public categories almost determine the aggregate income-threshold rate in this sample, but not quite. Hidden occupational and household differences still matter.

See the full analyst interpretation in docs/folktables-acs-income-interpretation.md.

The finance plugin is useful as a domain showcase and model-risk vocabulary layer, but the ACS/Folktables workflow is the best first demo for most Python users interested in aggregation bias, ecological fallacy, Simpson's paradox, or coarsened subgroup reporting.

Main Workflows

1. Validate a Claim

Use us.claim(...) when you want one workflow that certifies the claim, breaks it with a hidden-composition witness, or designs the smallest defensible public report. ClaimSpec is the underlying dataclass when you want to instantiate or serialize the spec directly, and ClaimAudit is the report object returned by .audit(...).

claim = us.claim(
    estimate_name="Income-threshold target rate",
    public=["AGE_BAND", "EDU_BAND", "SEX"],
    hidden=["AGE_BAND", "EDU_BAND", "SEX", "OCC_MAJOR", "WKHP_BAND", "RAC1P"],
    target="income_over_threshold",
    weight="sample_weight",
    q_presets=[us.q_tv_budget(0.10), us.q_chi_square_budget(0.25)],
    candidate_refinements=["OCC_MAJOR", "WKHP_BAND", "RAC1P"],
    ambiguity_limit=0.015,
    bucket_budget=40,
    statistical_interval=(0.119, 0.128),
)

design = claim.design(rows_or_frame)
print(design.to_markdown())

The design object separates the reported estimate, statistical uncertainty, hidden-composition ambiguity, public-refinement repair, counterexample witness, frontier evidence, and limitations. Use .audit(...) when you only need the claim verdict; use .design(...) when the reviewer question is, "What should we publish differently?" See docs/reporting-claims.md.

For model-assisted plausibility checks, fit a nonparametric public/hidden joint distribution and run the claim across sampled hidden compositions:

joint = us.fit_joint_distribution(
    rows_or_frame,
    public=claim.public,
    hidden=claim.hidden,
    target=claim.target,
    weight=claim.weight,
)

verdict = us.audit_claim(
    rows_or_frame,
    claim,
    joint_model=joint,
    joint_draws=500,
    joint_seed=123,
)

Use hidden_composition_uncertainty(...) when you want the posterior/bootstrap draw summary as its own report. By default it preserves the observed public bucket mix and resamples hidden composition inside each public bucket.

See docs/model-assisted-joint-analysis.md.

2. Inspect The Evidence Behind A Claim

Most users should start with us.claim(...). Use public_descent_report(...) when you explicitly want only the lower-level partial-ID evidence without a claim verdict, decision rule, repair search, or claim-centered recommendation language.

report = us.public_descent_report(
    rows_or_frame,
    public=["segment"],
    hidden=["segment", "region", "tenure_band"],
    target="outcome_rate",
    candidate_refinements=["region", "tenure_band"],
    q=us.q_bounded_shift(0.5),
)

Use AuditSpec for serializable lower-level audit configurations that need to be reviewed or rerun:

spec = us.AuditSpec(
    public=["segment"],
    hidden=["segment", "region", "tenure_band"],
    target="outcome_rate",
    candidate_refinements=["region", "tenure_band"],
    q={"name": "bounded_shift", "radius": 0.5},
)

run = spec.run(rows_or_frame)
print(run.to_markdown())

See docs/audit-specs.md.

Reports also expose structured JSON and tables:

run.to_json()
run.to_tables()
run.to_dataframes()  # requires pandas

See docs/structured-exports.md.

Reports include pre-solve data diagnostics for sparse cells, dropped mass, singleton public fibers, constant-target fibers, and skipped refinement candidates. See docs/data-diagnostics.md.

When you need to inspect the actual lower-vs-upper endpoint worlds behind an ambiguity result, use report.witness_report() or us.witness_report(...) to see which hidden cells move while the public distribution stays fixed.

3. Add Robustness And Repair Intelligence

Claim audits already embed the primary report, optional certificate search, and claim-centered refinement recommendations. Use the lower-level robustness tools when you need to inspect those pieces separately:

  • sensitivity_report(...) checks Q presets, sparse-cell thresholds, or alternate hidden-state definitions.
  • certify_public_representation(...) finds the smallest evaluated public representation satisfying an ambiguity limit.
  • public_representation_frontier(...) exposes the Pareto frontier behind that certificate.
  • breakdown_point(...) finds the stress radius where a decision or ambiguity claim stops passing.
  • claim.breaking_witness(...) directly finds the minimum TV, L2, or Mahalanobis recomposition that flips a threshold claim while preserving the public law.
  • claim.design_calibrated(...) or portfolio.design_calibrated(...) combines historical TV calibration, optional categorical rollup, public-schema search, and nearest threshold-breaking witnesses in one review artifact.
  • calibrated_design.freeze() turns the selected design into a fixed policy that audits future batches and backtests ordered holdout periods without recalibration or schema search.
  • policy.save("policy.json") and FrozenPublicReportPolicy.load("policy.json") preserve a portable policy across processes with schema and fingerprint validation.
  • calibrate_tv_radius(...) estimates a TV radius from consecutive historical recompositions and evaluates it with rolling one-step backtests.
  • design_categorical_rollup(...) finds an exact grouped version of one retained categorical column under saturated Q.
  • claim_portfolio(...) searches for one public representation that supports several claims under their respective stress tests.
sensitivity = us.sensitivity_report(
    rows_or_frame,
    public=["AGE_BAND", "SEX"],
    hidden=["AGE_BAND", "SEX", "OCC_MAJOR", "WKHP_BAND"],
    target="income_over_threshold",
    min_cell_weights=[1, 10, 25],
    q_presets=["saturated", us.q_bounded_shift(0.5), "observed"],
)

When period-labelled history is available, replace an arbitrary TV radius with an inspectable empirical calibration:

calibration = claim.calibrate_tv(
    historical_rows,
    period="quarter",
    coverage=0.90,
    min_train_transitions=4,
)

print(calibration.to_markdown())
current_audit = calibration.audit(current_period_rows)

The later hidden mix is restandardized to the earlier public law before TV is measured. Rolling rows use only earlier transitions and separately report radius coverage, target-interval coverage, and unsupported support drift. This workflow requires updatesupport[cvxpy]. See docs/historical-tv-calibration.md.

When adding a full categorical column would create too many public buckets, search for the smallest useful rollup instead:

rollup = claim.design_categorical_rollup(
    rows_or_frame,
    column="acquisition_channel",
    max_groups=4,
    bucket_budget=40,
    output_column="acquisition_channel_group",
)

print(rollup.to_markdown())
rollup_audit = rollup.audit(rows_or_frame)

The exact first slice searches all retained category partitions up to the group limit and reports the Pareto tradeoff between category-group count, realized public-cell count, and saturated ambiguity. See docs/categorical-rollup-design.md.

When several report metrics must use the same segmentation, design the schema jointly instead of repairing each claim independently:

portfolio = us.claim_portfolio(
    conversion_claim,
    uplift_claim,
    risk_claim,
    candidate_refinements=["channel", "tenure_band", "device"],
)

shared = portfolio.design(
    rows_or_frame,
    max_added_columns=2,
    bucket_budget=40,
)

print(shared.to_markdown())

Each claim keeps its own target, Q scenarios, ambiguity limit, and decision rule; the selected public columns are shared. See docs/shared-representation-design.md.

See docs/transport-presets.md for guidance on Q presets and docs/representation-adequacy.md for interpretation rules.

4. Audit Causal or Uplift Reports

Use a causal inference library such as EconML, DoWhy, DoubleML, or CausalML to estimate effects first. Then pass row-level or subgroup-level effects into updatesupport.

suite = us.causal_reporting_stability(
    df,
    public=["AGE_BAND", "SEX"],
    hidden=["AGE_BAND", "SEX", "OCC_MAJOR", "WKHP_BAND", "RAC1P"],
    effect="tau_hat",
    effect_standard_error="tau_se",
    weight="sample_weight",
    candidate_refinements=["OCC_MAJOR", "WKHP_BAND", "RAC1P"],
    q=us.q_bounded_shift(0.5),
    statistical_estimate=ate_hat,
    statistical_interval=(ci_low, ci_high),
    statistical_method="causal estimator bootstrap",
)

This keeps causal estimate, statistical uncertainty, hidden-composition ambiguity, estimator-uncertainty-aware adjusted ambiguity, and public refinement recommendations separate.

Try the tutorial notebook: DoWhy downstream reporting audit.

For causal/model-review stress tests based on hidden balance drift, use q=us.q_covariate_balance(epsilon, moments) to bound ||standardized_hidden_moment_shift||_2.

See docs/causal-library-integration.md.

5. Financial Model-Risk Plugin

Financial model-risk use cases live in the separate plugin package:

pip install "updatesupport[finance]"
# or
uv add "updatesupport[finance]"

It adds finance-oriented metrics such as expected loss and default rate, plus generic disclosure-triangulation helpers over the core named-linear feasibility solver, without cluttering the core package.

See packages/updatesupport-finance/README.md.

Install Extras

pip install "updatesupport[cvxpy]"      # TV, chi-square, KL, Wasserstein, custom CVXPY Q
pip install "updatesupport[residopt]"   # experimental residopt endpoint screening
pip install "updatesupport[scip]"       # CVXPY plus PySCIPOpt for solver="SCIP" and MIP presets
pip install "updatesupport[examples]"   # Folktables, pandas, plotting examples
pip install "updatesupport[causal]"     # causal-effect examples
pip install "updatesupport[dowhy]"      # CausalRefutation conversion
pip install "updatesupport[finance]"    # financial model-risk plugin package

The same extras work with uv add:

uv add "updatesupport[cvxpy]"
uv add "updatesupport[residopt]"
uv add "updatesupport[scip]"
uv add "updatesupport[examples]"
uv add "updatesupport[causal]"
uv add "updatesupport[dowhy]"
uv add "updatesupport[finance]"

The causal docs cover EconML, DoWhy, DoubleML, and CausalML handoffs.

For common examples and optional integrations:

pip install "updatesupport[examples,causal,dowhy,cvxpy,finance]"
# or
uv add "updatesupport[examples,causal,dowhy,cvxpy,finance]"

updatesupport[scip] is intentionally separate because solver wheels and platform requirements are heavier than the default CVXPY workflow. It enables SCIP-backed mixed-integer presets such as q_fiber_support_floor(...).

updatesupport[residopt] is also separate from the causal extras in this first release because residopt and EconML currently require incompatible scikit-learn ranges. The published residopt package currently requires Python 3.13 or newer.

Examples

Run the no-download Folktables smoke demo from a source checkout:

uv run python examples/folktables_acs.py --synthetic

Run the no-download AI / ML evaluation stability demo:

uv run python examples/ml_eval_stability.py

Run the no-download product experimentation / A/B testing stability demo:

uv run python examples/product_experiment_stability.py

Run the no-download RevOps funnel stability demo:

uv run python examples/revops_funnel_stability.py

Run the no-download RevOps funnel trend stability demo:

uv run python examples/revops_funnel_trend_stability.py

Write RevOps review artifacts as Markdown, JSON, and CSV tables:

uv run python examples/revops_funnel_stability.py \
  --export-dir data/revops_funnel_review

Audit your own retained funnel-cell CSV:

uv run python examples/revops_funnel_from_csv.py \
  --input funnel_cells.csv \
  --mode level \
  --public quarter reported_segment region \
  --hidden quarter reported_segment region lead_source campaign_type industry deal_size_band rep_ramp_band \
  --target sql_conversion_rate \
  --weight mql_count \
  --threshold 0.18 \
  --output-dir data/revops_review

Run the real Folktables ACSIncome example:

uv run --extra examples python examples/folktables_acs.py \
  --task income \
  --states CA \
  --year 2018 \
  --sample 50000 \
  --min-cell-weight 25

Generate the benchmark gallery:

uv run --extra examples --extra causal python examples/benchmark_gallery.py

Run the finance plugin example:

uv run --package updatesupport-finance python \
  packages/updatesupport-finance/examples/model_risk_portfolio.py

See docs/benchmark-gallery.md for reproducible case studies.

Source Development

git clone https://github.com/nahuaque/updatesupport.git
cd updatesupport
uv sync --all-packages --group dev --extra cvxpy --extra examples
uv run pytest

Documentation

Start Here

Positioning And Foundations

Claim Audits And Reports

Refinement And Robustness

Integrations And Case Studies

Project

Metadata

Release files for updatesupport 0.1.7

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for updatesupport 0.1.7
File Size Uploaded
updatesupport-0.1.7.tar.gz 666.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for updatesupport 0.1.7
File Interpreter ABI Platform
updatesupport-0.1.7-py3-none-any.whl Python 3 none any Details

Total release size: 938.3 kB

Release files / updatesupport-0.1.7.tar.gz

Download URL updatesupport-0.1.7.tar.gz
Size 666.3 kB
Tags Source
SHA-256 checksum
How to use checksums
9df39d29aec77e9fb47f5a9cb28ebe5872955b69c498ac62f50313a15cd2f606
BLAKE2b-256 checksum
How to use checksums
5771ff487dadb5bcf4ca65438c7d419f7bcd3453b9f70486cb06b7c35382b008
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Release files / updatesupport-0.1.7-py3-none-any.whl

Download URL updatesupport-0.1.7-py3-none-any.whl
Size 272.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
f18d4f4f118afcbe366fed3157a6b464ed475ddf028cde23d57581b2dd18dd6d
BLAKE2b-256 checksum
How to use checksums
0bc6d78a6d148330c20d1e73ece0638e2afbb3db0a219e184a1d2267ef79c9b8
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Release history Release notifications | RSS feed

This release

0.1.7 This release

2 release files

0.1.6

2 release files

0.1.5

2 release files

0.1.4

2 release files

0.1.3

2 release files

0.1.2

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page