Typed Westgard QC rule evaluation, sigma mappings, and planning helpers for Python.
Project description
Westgard-Python
westgard-python is a pure-Python package for evaluating Westgard rules, named multirule QC procedures, Westgard Sigma Rules mappings, and reference-backed planning guidance.
The package is built directly from the canonical reference in the master reference. It treats QC semantics as explicit API surface rather than implicit chart logic.
Status
The project is in active early development and targets 0.2.0.
Installation
pip install westgard-python
Optional dataframe helpers:
pip install "westgard-python[pandas]"
API Layers
Top-level API (import westgard):
Low-level rule primitives:
get_rule_definition(rule)evaluate_rule(rule, observations, *, scope, stream_name)
Mid-level current-run preset evaluation:
get_preset_definition(preset)evaluate_multirule(*, context, preset, interpretation_mode=InterpretationMode.MODERN)
High-level historical builders and scanners:
observation_from_value(*, analyte, material, run_id, sequence, value, mean, standard_deviation, metadata=None)series_from_rows(rows)evaluate_multirule_sequence(*, series, preset, interpretation_mode=InterpretationMode.MODERN)scan_multirule_series(*, series, preset, interpretation_mode=InterpretationMode.MODERN)
Stable JSON serializers:
serialize_multirule_result(result)serialize_event_multirule_result(result)serialize_series_multirule_result(result)
Sigma and planning helpers:
calculate_sigma_metrics(*, total_allowable_error, bias, coefficient_of_variation, analyte=None, level=None)recommend_sigma_rules(*, sigma, control_levels)plan_qc_strategy(*, sigma_metrics, control_levels, mode=PlanningMode.MONITOR, residual_risk_target=1.0)
Optional adapter API (from westgard.adapters import ...):
dataframe_to_context(frame, *, current_run_id, across_material=False)dataframe_to_series(frame)
Choosing The Right API
- Use
evaluate_rule()when you already know the exact stream and exact rule to test. - Use
evaluate_multirule()when you need one decision for one current QC event and you already have explicit current-run and history streams. - Use
series_from_rows()plusevaluate_multirule_sequence()when you need one result per event across a chronological history. - Use
series_from_rows()plusscan_multirule_series()when you need retrospective trigger discovery for reports, chart annotations, or persistence.
Raw Values And Multi-Material Runs
- Raw QC values are supported through
observation_from_value()or row inputs containingvalue,mean, andstandard_deviation. The package computesz_score = (value - mean) / standard_deviationinternally. standard_deviationmust be positive. Invalid or nonpositive SDs raiseValueError.- Multi-material runs are represented by sharing the same
run_idacross one observation per material. Sequences should remain chronological across the series. - High-level row and dataframe builders auto-group mixed-analyte tables into one
QCSeriesper analyte.
Interpretation And Output Notes
modernevaluates configured rejection rules directly.classicpreserves the historical1_2swarning gate. In sequence/retrospective APIs, downstream rules appear explicitly asskipped_by_warning_gatewhen the gate does not trigger.- Rule outcomes distinguish
violated,not_violated,insufficient_data, andskipped_by_warning_gate. - Rule metadata includes severity classification:
warning,rejection, orpreset_dependent. - Serializer functions provide the stable JSON contract. They emit canonical rule and preset names only.
End-To-End Historical Example
from westgard import (
InterpretationMode,
MultiruleVariant,
scan_multirule_series,
series_from_rows,
serialize_series_multirule_result,
)
rows = [
{
"analyte": "glucose",
"material": "L1",
"run_id": "run-001",
"sequence": 1,
"value": 102.0,
"mean": 100.0,
"standard_deviation": 2.0,
},
{
"analyte": "glucose",
"material": "L2",
"run_id": "run-001",
"sequence": 1,
"z_score": -0.3,
},
{
"analyte": "glucose",
"material": "L1",
"run_id": "run-002",
"sequence": 2,
"z_score": 2.2,
},
{
"analyte": "glucose",
"material": "L2",
"run_id": "run-002",
"sequence": 2,
"z_score": 0.2,
},
]
series = series_from_rows(rows)["glucose"]
retrospective = scan_multirule_series(
series=series,
preset=MultiruleVariant.MODERN_2_CONTROL,
interpretation_mode=InterpretationMode.MODERN,
)
payload = serialize_series_multirule_result(retrospective)
print(payload["accepted"])
print(payload["violations"])
print(payload["first_violations_by_rule"])
Common Setup
All examples below use these shared imports/helpers.
from westgard import (
ControlObservation,
EvaluationScope,
InterpretationMode,
MultiruleContext,
MultiruleVariant,
PlanningMode,
RuleName,
RunIdentifier,
calculate_sigma_metrics,
evaluate_multirule,
evaluate_rule,
get_preset_definition,
get_rule_definition,
plan_qc_strategy,
recommend_sigma_rules,
)
def obs(run_id: str, material: str, sequence: int, z: float) -> ControlObservation:
return ControlObservation(
analyte="glucose",
material=material,
run_id=RunIdentifier(run_id),
sequence=sequence,
z_score=z,
)
How-To: get_rule_definition
Signature:
get_rule_definition(rule: RuleName) -> RuleDefinition
Use when you need static metadata for one primitive rule: taxonomy, minimum observations, default scope, intended error type, and description.
definition = get_rule_definition(RuleName.R_4S)
print(definition.minimum_observations) # 2
print(definition.default_scope.value) # within_run
print(definition.intended_error) # random error
Raises:
KeyErrorif the rule is not present inRULE_DEFINITIONS.
How-To: evaluate_rule
Signature:
evaluate_rule(
rule: RuleName,
observations: tuple[ControlObservation, ...],
*,
scope: EvaluationScope,
stream_name: str,
) -> EvaluationResult
Use when you want to evaluate exactly one rule against one explicit stream.
result = evaluate_rule(
RuleName.TWO_2S,
(
obs("run-001", "L1", 1, 2.2),
obs("run-002", "L1", 2, 2.3),
),
scope=EvaluationScope.WITHIN_MATERIAL,
stream_name="material:L1",
)
print(result.violated) # True
print(result.enough_data) # True
print(result.reason)
Behavior notes:
- Observations are sorted by
sequencebefore evaluation. - If there are too few observations, it returns
violated=False,enough_data=False. - Limit rules use strict inequality. Boundary values at exactly
+2,-2,+3, or-3do not violate. R_4smust be evaluated withscope=within_run.
Raises:
ValueErrorfor invalid rule/scope combinations (for exampleR_4soutsidewithin_run).
How-To: get_preset_definition
Signature:
get_preset_definition(preset: MultiruleVariant) -> PresetDefinition
Use when you need the exact rule bundle for a named preset before evaluating.
preset = get_preset_definition(MultiruleVariant.MODERN_2_CONTROL)
print(preset.current_run_rules) # (1_3s, R_4s)
print(preset.history_rules) # (2_2s, 4_1s, 8_x)
print(preset.classic_warning_gate) # False
Raises:
KeyErrorif the preset key is not defined.
How-To: evaluate_multirule
Signature:
evaluate_multirule(
*,
context: MultiruleContext,
preset: MultiruleVariant,
interpretation_mode: InterpretationMode = InterpretationMode.MODERN,
) -> MultiruleResult
Use when you want end-to-end multirule evaluation with preset rule families.
l1 = (
obs("run-001", "L1", 1, 0.2),
obs("run-002", "L1", 3, 0.4),
obs("run-003", "L1", 5, 3.2),
)
l2 = (
obs("run-001", "L2", 2, -0.1),
obs("run-002", "L2", 4, 0.2),
obs("run-003", "L2", 6, 0.1),
)
context = MultiruleContext(
current_run=(l1[-1], l2[-1]),
material_streams={"L1": l1, "L2": l2},
)
result = evaluate_multirule(
context=context,
preset=MultiruleVariant.MODERN_2_CONTROL,
interpretation_mode=InterpretationMode.MODERN,
)
print(result.accepted)
print([v.rule.value for v in result.violations])
Behavior notes:
modernevaluates configured rejection rules directly.classicfirst evaluates a current-run1_2swarning gate.- If
classicand no warning gate trigger, the run is accepted early. - Cross-material history is evaluated only if
context.across_material_streamis supplied.
How-To: calculate_sigma_metrics
Signature:
calculate_sigma_metrics(
*,
total_allowable_error: float,
bias: float,
coefficient_of_variation: float,
analyte: str | None = None,
level: str | None = None,
) -> SigmaMetrics
Use when you have TEa/bias/CV and need sigma for rule-selection workflows.
metrics = calculate_sigma_metrics(
total_allowable_error=10.0,
bias=-2.0,
coefficient_of_variation=2.0,
analyte="glucose",
level="L1",
)
print(metrics.sigma) # 4.0
Formula:
sigma = (TEa - abs(bias)) / CV
Raises:
ValueErroriftotal_allowable_error <= 0ValueErrorifcoefficient_of_variation <= 0
How-To: recommend_sigma_rules
Signature:
recommend_sigma_rules(*, sigma: float, control_levels: int) -> SigmaRuleMapping
Use when you need a reference-backed sigma-band mapping to a rule set.
mapping = recommend_sigma_rules(sigma=4.5, control_levels=2)
print(mapping.sigma_band) # 4 to <5
print(mapping.recommended_rules) # 1_3s, 2_2s, R_4s, 4_1s
print(mapping.control_measurements_per_event)
print(mapping.max_runs_between_qc_events)
Raises:
ValueErrorifcontrol_levelsis not2or3.
How-To: plan_qc_strategy
Signature:
plan_qc_strategy(
*,
sigma_metrics: SigmaMetrics,
control_levels: int,
mode: PlanningMode = PlanningMode.MONITOR,
residual_risk_target: float = 1.0,
) -> PlanningRecommendation
Use when you need deterministic planning notes around startup vs routine monitor usage.
metrics = calculate_sigma_metrics(
total_allowable_error=8.0,
bias=1.0,
coefficient_of_variation=1.0,
)
recommendation = plan_qc_strategy(
sigma_metrics=metrics,
control_levels=2,
mode=PlanningMode.STARTUP,
residual_risk_target=1.0,
)
print(recommendation.mode.value) # startup
print(recommendation.sigma_mapping.sigma_band) # based on computed sigma
print(recommendation.qc_event_note)
print(recommendation.out_of_control_recovery)
Raises:
ValueErrorifresidual_risk_target <= 0.ValueErrorifcontrol_levelsis invalid (propagated fromrecommend_sigma_rules).
How-To: dataframe_to_context (Optional pandas Adapter)
Signature:
dataframe_to_context(
frame,
*,
current_run_id: str,
across_material: bool = False,
) -> MultiruleContext
Use when your QC data starts as a pandas DataFrame and you want to feed it into evaluate_multirule.
import pandas as pd
from westgard.adapters import dataframe_to_context
frame = pd.DataFrame(
[
{"analyte": "glu", "material": "L1", "run_id": "run-1", "sequence": 1, "z_score": 0.1},
{"analyte": "glu", "material": "L2", "run_id": "run-1", "sequence": 2, "z_score": -0.1},
{"analyte": "glu", "material": "L1", "run_id": "run-2", "sequence": 3, "z_score": 2.3},
{"analyte": "glu", "material": "L2", "run_id": "run-2", "sequence": 4, "z_score": 0.2},
]
)
context = dataframe_to_context(frame, current_run_id="run-2", across_material=True)
print(len(context.current_run)) # 2
Required columns:
analytematerialrun_idsequencez_score
Raises:
ImportErrorif pandas is not installed.TypeErrorifframeis not a pandas DataFrame.ValueErrorif required columns are missing.ValueErrorif no rows matchcurrent_run_id.
How-To: EvaluationResult.to_violation (Method)
Signature:
EvaluationResult.to_violation() -> RuleViolation | None
Use when you want a normalized violation payload for downstream reporting.
result = evaluate_rule(
RuleName.ONE_3S,
(obs("run-009", "L1", 1, 3.5),),
scope=EvaluationScope.WITHIN_RUN,
stream_name="current_run:run-009",
)
violation = result.to_violation()
print(violation.rule.value if violation else None)
Behavior notes:
- Returns
Nonewhenresult.violatedisFalse. - Returns
RuleViolationwith rule/scope/stream and triggering observation IDs when violated.
Design Principles
moderninterpretation is the default for computerized evaluation.classicinterpretation is supported explicitly and preserves1_2swarning-gate behavior.R_4sis enforced as within-run only.- Cross-run and cross-material logic are explicit inputs to the engine. The package does not infer run boundaries from timestamps.
Documentation
- Canonical reference: master reference
- Release runbook: publishing guide
- Package docs: MkDocs Material site under
docs/.
Local Release Validation
Run the strict local deployability gate with:
python scripts/validate_release.py
This command runs linting, typing, tests with branch coverage gate (>=95%), build, twine checks, and wheel smoke install. It writes a local summary report to reports/test-results-summary.md.
License
Apache-2.0
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 westgard_python-0.2.0.tar.gz.
File metadata
- Download URL: westgard_python-0.2.0.tar.gz
- Upload date:
- Size: 23.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 |
18d619c58b8916ab7597ed7f31ad39196595d60957e99c263f49dbe3150141aa
|
|
| MD5 |
9db1a5076206f89fab4143d6cadd2bca
|
|
| BLAKE2b-256 |
f86c57630ca472c9a32754a41dadab1f157bd817d9ab4e93da89a1889e3248e2
|
Provenance
The following attestation bundles were made for westgard_python-0.2.0.tar.gz:
Publisher:
publish.yml on jouno53/Westgard-Python
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
westgard_python-0.2.0.tar.gz -
Subject digest:
18d619c58b8916ab7597ed7f31ad39196595d60957e99c263f49dbe3150141aa - Sigstore transparency entry: 1345378175
- Sigstore integration time:
-
Permalink:
jouno53/Westgard-Python@c5754db4e8c98c3f78a16974926df12bbcbd5a13 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/jouno53
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@c5754db4e8c98c3f78a16974926df12bbcbd5a13 -
Trigger Event:
push
-
Statement type:
File details
Details for the file westgard_python-0.2.0-py3-none-any.whl.
File metadata
- Download URL: westgard_python-0.2.0-py3-none-any.whl
- Upload date:
- Size: 28.5 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 |
44ee6a7a251b1c8ec65839785fbbcad113afe1ab64a278dfadf64a2b46b59fe3
|
|
| MD5 |
62db2f2aa4076618f8969e7e81e834eb
|
|
| BLAKE2b-256 |
fd0d0f36e28983f0306a6632ad4c81eafccf79d1494acdf764c3c4f81766619f
|
Provenance
The following attestation bundles were made for westgard_python-0.2.0-py3-none-any.whl:
Publisher:
publish.yml on jouno53/Westgard-Python
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
westgard_python-0.2.0-py3-none-any.whl -
Subject digest:
44ee6a7a251b1c8ec65839785fbbcad113afe1ab64a278dfadf64a2b46b59fe3 - Sigstore transparency entry: 1345378268
- Sigstore integration time:
-
Permalink:
jouno53/Westgard-Python@c5754db4e8c98c3f78a16974926df12bbcbd5a13 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/jouno53
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@c5754db4e8c98c3f78a16974926df12bbcbd5a13 -
Trigger Event:
push
-
Statement type: