Skip to main content

Representation adequacy and transport-stability auditing in Python

Project description

updatesupport

PyPI Version

Audit whether the categories in your report are stable enough for the estimate you are reporting.

updatesupport quantifies hidden-composition ambiguity: how much an aggregate rate, effect, or risk metric could move if the public buckets in a report stayed fixed but the hidden mix inside those buckets changed.

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 hide 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.

Why the Math Is Sound

updatesupport reduces the review question to a finite optimization problem. Given hidden cells D, public buckets pi(D), supplied hidden-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 hidden-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

report = us.public_descent_report(
    rows_or_frame,
    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"],
    min_cell_weight=25,
    q="saturated",
    title="Income-Threshold Representation Audit",
)

print(report.to_markdown())

The report 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.

A Concrete Result

In the Folktables ACSIncome 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.

Main Workflows

1. Verify a Reporting Claim

Use ReportingClaim when you want one review artifact that certifies the claim, breaks it with a hidden-composition witness, or proposes a stable repair.

claim = us.ReportingClaim(
    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),
)

verdict = us.verify_claim(rows_or_frame, claim)
print(verdict.to_markdown())

The verifier separates the reported estimate, statistical uncertainty, hidden-composition ambiguity, public-refinement repair, counterexample witness, and limitations. 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.verify_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. Audit a Public Report

Use public_descent_report(...) when you already know the public buckets, hidden refinements, and target metric.

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 when the audit configuration should be serialized, 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. Run Robustness Checks

Use sensitivity_report(...) when the conclusion should be checked across Q presets, sparse-cell thresholds, or alternate hidden-state definitions.

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"],
)

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

4. Certify a Stable Public Segmentation

Use certify_public_representation(...) when you want a review-ready decision: the smallest evaluated public bucket design that keeps ambiguity below a declared threshold, with search assumptions and limitations attached.

certificate = us.certify_public_representation(
    rows_or_frame,
    base_public=["AGE_BAND", "EDU_BAND"],
    hidden=["AGE_BAND", "EDU_BAND", "SEX", "OCC_MAJOR", "WKHP_BAND"],
    target="income_over_threshold",
    candidate_refinements=["SEX", "OCC_MAJOR", "WKHP_BAND"],
    q_presets=["saturated", us.q_bounded_shift(0.5), "observed"],
    min_cell_weights=[1, 10, 25],
    ambiguity_limit=0.01,
    bucket_budget=40,
    search="exhaustive",
)

print(certificate.to_markdown())

Use public_representation_frontier(...) when you want the exploratory Pareto frontier behind the certificate. The frontier compares public-cell count, added public columns, and ambiguity across the stress grid. See docs/public-representation-frontier.md.

5. 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.

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.

6. 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 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[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[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(...).

Examples

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

uv run python examples/folktables_acs.py --synthetic

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

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

updatesupport-0.1.2.tar.gz (431.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

updatesupport-0.1.2-py3-none-any.whl (144.5 kB view details)

Uploaded Python 3

File details

Details for the file updatesupport-0.1.2.tar.gz.

File metadata

  • Download URL: updatesupport-0.1.2.tar.gz
  • Upload date:
  • Size: 431.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for updatesupport-0.1.2.tar.gz
Algorithm Hash digest
SHA256 b1082385b1d83f2a4b0680411697c81227fa766f674c278cb26897ab86a95fa8
MD5 34ec0b0ee143643d794a78beb86010c1
BLAKE2b-256 cce2d0750d50b0823e226b1728b94ba31afae1185492eebace3918cd2a896c54

See more details on using hashes here.

File details

Details for the file updatesupport-0.1.2-py3-none-any.whl.

File metadata

  • Download URL: updatesupport-0.1.2-py3-none-any.whl
  • Upload date:
  • Size: 144.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for updatesupport-0.1.2-py3-none-any.whl
Algorithm Hash digest
SHA256 25e7f21e2a7b6768dd09b8cc05e4ea09c1207b2b18b35bc7928db8cdc7a9d65e
MD5 64cfce9a4f82682e4ba30f879894dd4d
BLAKE2b-256 ee90425e5c5d013ee6316a39e6306438d08eb87bf32f49981840af92893d978e

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page