CI/CD checks for economic, policy, and statistical models
Project description
EconEval
pytest for economic logic.
EconEval is a small open-source framework for pytest-style checks on economic logic in CI.
It catches problems unit tests often miss: broken rules, drift after data changes, and outputs that stop making economic sense.
Latest release: v0.5.2
What It Does
EconEval currently does three things:
- loads a model check config
- runs invariant tests against a Python object
- writes JSON, JUnit, Markdown, or HTML reports that CI can keep or fail on
The config layer is validated with Pydantic, and invariant evaluation uses a static AST pre-check plus a numeric numexpr fast path for safe math-heavy expressions.
Use it to:
- check that important economic rules still hold
- make model assumptions explicit in code
- fail pull requests when a change breaks a rule you care about
Maturity
The invariant engine, CLI, reports, examples, and GitHub Action are usable today. The R, Stata, and Julia bridge helpers are also available for teams that want to call the CLI from other ecosystems.
The broader checks are still evolving:
- stress testing
- fairness checks
- drift checks
- PDF reports
That split is intentional: the core is meant to be small, auditable, and easy to run in CI.
Who Should Use This
EconEval is for people who want a lightweight, CI-native way to check whether a model still makes economic sense. The core stays close to unit-test style validation; the broader checks are extensions on top of that.
- academic economists validating research code and replication projects
- policy analysts checking that a model still respects program rules and constraints
- quantitative consultants delivering models to clients with auditability requirements
- teams working on forecasting, scenario analysis, or simulation pipelines
- researchers who want a lightweight validation layer before a model is published or deployed
- applied data scientists building models that need economics-aware guardrails
- analysts who want CI checks they can explain in a report or appendix
Why It Exists
Traditional software tests are useful, but they do not tell you whether a model still behaves like a valid model.
For example, a change might:
- flip the sign of an elasticity
- violate a market-clearing condition
- break a policy constraint
- quietly change the meaning of a downstream output
EconEval gives you a place to encode those rules and run them automatically.
Current MVP
The first usable version of EconEval does three things well:
- read a simple YAML config
- evaluate invariant expressions against a model object
- return a clear pass or fail result that GitHub Actions can use
That is enough to support a real workflow without pretending to solve every validation problem at once.
Limitations
EconEval is intentionally not a full Python sandbox.
- the expression syntax is deliberately restricted
- the YAML-like parser supports the documented config shape, not every YAML feature
- fairness, drift, PDF reports, and non-Python bridges are evolving extensions
That keeps the core small and auditable, but it also means EconEval is not meant to replace general-purpose application logic.
60-Second Demo
Run a passing elasticity check:
econeval --config examples/basic_model/econeval.yml --model examples/basic_model/model.py --class DemoModel --report out/pass.json
The rule is model.elasticity < 0, and DemoModel passes because its elasticity is negative.
Run the same rule against a failing model:
econeval --config examples/broken_model/econeval.yml --model examples/broken_model/model.py --class BrokenModel --report out/fail.json
BrokenModel fails because its elasticity is positive.
Example Config
project: demo-model
version: 1
invariants:
- name: elasticity_must_be_negative
expression: model.elasticity < 0
- name: supply_must_be_non_negative
expression: model.supply >= 0
stress_tests:
- name: stagflation_shock
dataset: data/stagflation.csv
metric: mape
threshold: 0.15
- name: macro_stagflation
kind: synthetic
metric: invariants
manipulations:
- variable: input_data.unemployment_rate
action: add
value: 0.04
- variable: input_data.energy_costs
action: multiply
value: 1.5
invariants:
- name: slowdown_flag
expression: model.predicted_gdp_growth < 0.01
fairness:
enabled: true
metrics:
- demographic_parity_difference
- disparate_impact_ratio
- gini
- atkinson
- equal_opportunity_difference
- equalized_odds_difference
How It Fits Together
flowchart LR
A[Model repo] --> B[econeval.yml]
B --> C[Load config]
C --> D[Run checks]
D --> E[Collect results]
E --> F{Pass or fail CI}
Project Layout
econeval/
.pre-commit-config.yaml
.github/
workflows/
ci.yml
action.yml
bridges/
julia/
econeval_bridge.jl
r/
econeval_bridge.R
stata/
econeval_bridge.do
scripts/
precommit_econeval.py
examples/
basic_model/
model.py
econeval.yml
csv_model/
README.md
econeval.yml
drift_model/
model.py
econeval.yml
fairness_model/
README.md
model.py
econeval.yml
broken_model/
model.py
econeval.yml
policy_model/
model.py
econeval.yml
advanced_model/
model.py
econeval.yml
src/
econeval/
__init__.py
cli.py
config.py
invariants.py
traceability.py
scenarios.py
reporting.py
tests/
test_config.py
test_invariants.py
Install
For development from a checkout:
git clone https://github.com/Farukhsb/econeval.git
cd econeval
pip install -e .[dev]
pytest and ruff are included in the dev extra. Install without the extra if you only want the CLI.
If you want the repo-local pre-commit hook that runs EconEval on the basic example, install the hooks once:
pre-commit install
EconEval parses its YAML-like config format with its own loader, so you do not need PyYAML for the current release.
For a public release install from PyPI:
pip install econeval==0.5.1
If you want the exact release state, install from the tagged GitHub release:
pip install git+https://github.com/Farukhsb/econeval.git@v0.5.2
If you want to inspect the release artifacts first, start from the v0.5.2 tag or the GitHub release page.
Getting Started
- Install the package with
pip install econevalorpip install -e .[dev]from a checkout. - Run the basic example config.
- Check the generated report and try the broken example next.
Example:
econeval --config examples/basic_model/econeval.yml --model examples/basic_model/model.py --class DemoModel --report econeval-report.json
If you want a quick sanity check, run the broken example next and confirm it fails.
Benchmarks
To compare the large-array expression backends, run:
python benchmarks/numexpr_benchmark.py --size 100000 --iterations 20
How To Use It
Create a config file that lists the checks you want to enforce, then point EconEval at a Python model class.
Command line example:
econeval --config examples/basic_model/econeval.yml --model examples/basic_model/model.py --class DemoModel --report econeval-report.json
python -m econeval works the same way. For iterative development, --watch reruns the checks when the config or model file changes:
econeval --watch --config examples/basic_model/econeval.yml --model examples/basic_model/model.py --class DemoModel --report econeval-report.json
For extra traceability on failed invariants, add --blame. EconEval will try to run
git blame on the model lines that look related to the failure and include the
commit metadata in the report.
econeval --blame --config examples/basic_model/econeval.yml --model examples/basic_model/model.py --class DemoModel --report econeval-report.json
For model-less CSV relation checks, you can omit --model when the config only contains kind: relation stress tests:
econeval --config examples/csv_model/econeval.yml --report econeval-report.json
To compare a current run against a previous JSON report, pass --baseline-report:
econeval --config examples/basic_model/econeval.yml --model examples/basic_model/model.py --class DemoModel --report econeval-report.json --baseline-report baseline-report.json
To write a text report instead:
econeval --config examples/advanced_model/econeval.yml --model examples/advanced_model/model.py --class AdvancedModel --report econeval-report.md --format markdown
econeval --config examples/advanced_model/econeval.yml --model examples/advanced_model/model.py --class AdvancedModel --report econeval-report.html --format html
To write a PDF report, install the optional extra first:
pip install econeval[pdf]
econeval --config examples/advanced_model/econeval.yml --model examples/advanced_model/model.py --class AdvancedModel --report econeval-report.pdf --format pdf
If you need a custom economic check kind, register a handler in Python and then
use that kind in your config:
from econeval.config import EconomicCheck
from econeval.scenarios import register_economic_check_handler
def run_custom_check(model, check):
return check.name, check.kind
register_economic_check_handler("custom_policy_check", run_custom_check)
GitHub Action
The repository also exposes a composite GitHub Action so workflows can run EconEval in one step.
- uses: Farukhsb/econeval@v1
with:
config: examples/basic_model/econeval.yml
model: examples/basic_model/model.py
class: DemoModel
report: econeval-report.json
python-version: "3.11"
The action installs the package from the action source, sets up Python, and runs the CLI with the inputs you provide.
Fairness Checks
Fairness checks are available, but they are still an extension rather than the core path.
They expect a tabular dataset with:
- a group column, defaulting to
group - one or more feature columns that are passed to
predict(features) - an
actualcolumn when you use label-based metrics such asequal_opportunity_differenceorequalized_odds_difference
The example in examples/fairness_model/README.md shows the full data format.
The built-in thresholds are:
demographic_parity_difference: pass at<= 0.2disparate_impact_ratio: pass at>= 0.8equal_opportunity_difference: pass at<= 0.2equalized_odds_difference: pass at<= 0.2gini: pass at<= 0.3atkinson: pass at<= 0.2
Fairness results also include a severity field:
passfor checks within the thresholdwarnfor borderline misses that should not fail CIfailfor clear misses or execution errors
To run the full advanced example with synthetic shocks, drift checks, fairness metrics, and scan checks:
econeval --config examples/advanced_model/econeval.yml --model examples/advanced_model/model.py --class AdvancedModel --report artifacts/advanced-report.md --format markdown
What the runner expects:
- a model file that defines a class you can import by name
- a
predict(features)method for stress tests, drift checks, and fairness checks - CSV datasets with an
actualcolumn for stress tests - CSV datasets with the feature or group columns required by the check
EconEval can also adapt common tabular estimators directly when they expose feature metadata such as feature_names_in_ or exog_names.
If your runtime looks different, use a thin adapter. EconEval also handles common shapes like callable models, solve() wrappers, and PyMC-style posterior predictive samplers, so you can bridge external engines without rewriting the check pipeline.
Example invariant rule:
- name: elasticity_must_be_negative
expression: model.elasticity < 0
If the expression returns False, the invariant fails.
The JSON report includes the project name, a summary count, and the result of each invariant, economic check, stress test, drift check, economic drift check, fairness check, and optional baseline comparison block.
Expression Engine
EconEval uses a restricted invariant engine with a static AST pre-check.
The supported path is intentionally narrow:
- comparisons, boolean logic, simple arithmetic, and attribute access on the model object
- numeric/vectorized expressions through
numexprwhen the expression is safe for that backend
It rejects function calls, subscripts, comprehensions, lambdas, dictionaries, sets, tuples, lists, and private attributes such as __class__.
That design keeps the syntax simple for users while avoiding raw eval() and other arbitrary Python execution paths.
Expression Engine Philosophy
The expression layer is intentionally not a general Python runtime.
That is a design choice:
- keep invariant checks auditable
- make failures easier to explain in CI
- avoid hidden side effects and sandbox escape risks
- keep the supported syntax small enough that users can reason about it quickly
The practical rule is simple: if a check needs full Python, put that logic in code, not in the expression string. The expression engine is meant for declarative rules, not imperative workflows.
Examples
examples/basic_modelshows the happy path.examples/csv_modelshows model-less CSV relation checks.examples/broken_modelshows a model and dataset that fail the checks.examples/drift_modelfocuses on drift validation, including PSI, trend drift, and regression drift over time.examples/fairness_modelfocuses on fairness checks and a simple stress test.examples/policy_modelis a compact policy example with fairness, invariants, and a monotonicity check.examples/tax_policy_simulatorshows a small Adult-derived tax policy model with liability and rate checks.examples/advanced_modelshows accounting identities, monotonicity, convergence, grid sweeps, synthetic shocks, and GitHub-friendly report output.--baseline-reportcompares a current report against a prior JSON run and highlights regressions, improvements, and new or removed checks.examples/demo_notebook.ipynbis a short walkthrough you can open in Jupyter or VS Code.- The repository examples are intended to double as a lightweight demo workflow.
Non-Python Bridges
The first bridge helper is a thin R wrapper in bridges/r/econeval_bridge.R.
It shells out to the EconEval CLI, so R users can keep their model code in R and still run EconEval checks in CI or locally.
There is also a Stata wrapper in bridges/stata/econeval_bridge.do.
There is also a Julia wrapper in bridges/julia/econeval_bridge.jl.
Roadmap
- richer traceability and failure explanations
- deeper drift comparison and alerting
- richer fairness configuration and reporting
- more real-world examples and benchmark coverage
- deeper interop with tools like
PyMC,GAMS, and Julia - optional
statsmodels-based drift helper viaeconeval[stats] - dashboard polish with filtering and collapsible drill-downs
Release Flow
Publishing a GitHub Release triggers the release workflow in .github/workflows/release.yml.
That workflow:
- installs the package
- runs the test suite
- runs EconEval against the example model
- uploads a release report artifact
Next Step
- a richer report viewer
- more scenario types
- baseline trend summaries over multiple releases
Release Checklist
When you are ready to publish a new version:
- run the test suite locally
- update the package version if needed
- tag the release, for example
v0.5.2 - publish the GitHub Release so the release workflow runs
- confirm the release artifact uploaded from Actions
- confirm the wheel and sdist were published to PyPI
To publish to PyPI through GitHub Actions, enable PyPI trusted publishing for this repository and then publish the GitHub Release. The release workflow will build the distribution and upload it automatically.
Changelog
See CHANGELOG.md for version-by-version changes.
License
MIT
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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file econeval-0.5.2.tar.gz.
File metadata
- Download URL: econeval-0.5.2.tar.gz
- Upload date:
- Size: 67.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8b5e524ad52a45999bb73220ab673d940633e29d800a1a56db1760ff9479294d
|
|
| MD5 |
aea5a46d62856640119b394d87d1de9c
|
|
| BLAKE2b-256 |
1c33dc6f79220250616c990cd632d621d2bd4f55c4e93cbd4dafb5685a04537d
|
Provenance
The following attestation bundles were made for econeval-0.5.2.tar.gz:
Publisher:
release.yml on Farukhsb/econeval
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
econeval-0.5.2.tar.gz -
Subject digest:
8b5e524ad52a45999bb73220ab673d940633e29d800a1a56db1760ff9479294d - Sigstore transparency entry: 1723672702
- Sigstore integration time:
-
Permalink:
Farukhsb/econeval@16d2101981b9c82247e712eaece34a6ed429d63c -
Branch / Tag:
refs/tags/v0.5.2 - Owner: https://github.com/Farukhsb
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@16d2101981b9c82247e712eaece34a6ed429d63c -
Trigger Event:
release
-
Statement type:
File details
Details for the file econeval-0.5.2-py3-none-any.whl.
File metadata
- Download URL: econeval-0.5.2-py3-none-any.whl
- Upload date:
- Size: 48.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c6c55cfc716f754adaa3ebc043f6e05a8968f4ed3d185234156e942eaeda3fe1
|
|
| MD5 |
eabf5f75c75875e50c106e34fe0c2f16
|
|
| BLAKE2b-256 |
773cdf1ec874ce0152680b3e5dd4ad7f3b99bc1412f2e505d3c63e65adf435b0
|
Provenance
The following attestation bundles were made for econeval-0.5.2-py3-none-any.whl:
Publisher:
release.yml on Farukhsb/econeval
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
econeval-0.5.2-py3-none-any.whl -
Subject digest:
c6c55cfc716f754adaa3ebc043f6e05a8968f4ed3d185234156e942eaeda3fe1 - Sigstore transparency entry: 1723672751
- Sigstore integration time:
-
Permalink:
Farukhsb/econeval@16d2101981b9c82247e712eaece34a6ed429d63c -
Branch / Tag:
refs/tags/v0.5.2 - Owner: https://github.com/Farukhsb
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@16d2101981b9c82247e712eaece34a6ed429d63c -
Trigger Event:
release
-
Statement type: