Skip to content

Quality Scoring

truthound.profiler.quality evaluates bound validation rules against a Polars DataFrame. A positive prediction means the value passes the rule. Ground truth uses the same convention: a non-null Boolean series aligned to every input row. Sampling applies the same selected indices to data and labels.

RuleQualityScorer

import polars as pl
from truthound.profiler.quality import RuleQualityScorer, RuleType, ValidationRule

frame = pl.DataFrame({"amount": [10, -1, 20]})
rule = ValidationRule("nonnegative", RuleType.RANGE, column="amount",
                      min_value=0, nullable=False)
scorer = RuleQualityScorer(
    estimator="sampling",
    estimator_options={"sample_size": 1000, "random_seed": 7,
                       "confidence_level": 0.95},
    min_confidence=0.5, quality_threshold=0.70, max_cache_entries=128,
)
score = scorer.score(rule, frame, ground_truth=pl.Series([True, False, True]))
print(score.metrics.f1_score)
print(score.metrics.quality_level.value)

score() returns RuleQualityScore: rule name/type/column, metrics, recommendation, should_use and optional alternatives. metrics is QualityMetrics, including precision, recall, F1, accuracy, specificity, MCC, confidence intervals, sample/population sizes and evaluation time. ConfusionMatrix fields are true_positives, true_negatives, false_positives and false_negatives. Zero-denominator ratios return zero.

ValidationRule.validate_column() follows scalar validate() semantics, including nullability, custom callbacks and pattern prefix matching. A custom callback must return a Boolean. Custom rule code is trusted application code.

QualityLevel

F1 thresholds are excellent ≥0.95, good ≥0.85, acceptable ≥0.70, poor ≥0.50, and unacceptable below 0.50. The scorer's acceptance threshold and minimum confidence affect recommendations; they do not redefine these levels.

Quality Estimation Strategies

  • SamplingQualityEstimator(sample_size=1000, confidence_level=0.95, random_seed=None) uses a local RNG, leaving process-global random state intact. Its configured confidence level controls Wilson precision/recall intervals. Labels must cover the entire input, even when only a sample is evaluated.
  • CrossValidationEstimator(n_folds=5, random_seed=None) uses at most one fold per row. With labels it combines observed fold confusion counts; without labels it estimates quality from consistency. Empty input returns empty metrics.
  • HeuristicQualityEstimator(strictness=0.5) estimates quality from data/rule characteristics. It does not establish supervised accuracy.

Without labels, sampling and cross-validation metrics are heuristic estimates, not measured classification accuracy. confidence is not a guarantee that a rule is correct. No estimator produces an F1 confidence interval; f1_ci retains its empty default. estimate_quality(rule, frame, estimator="sampling", **options) returns metrics without labels; use an estimator's estimate() or scorer's score() for explicit ground truth.

Rule Comparison and Cache

scorer.score_all(rules, frame, ground_truth=None) returns scores in input order. scorer.compare(rules, frame) and compare_rules(rules, frame) return a list sorted by descending F1. They do not return a report or a best-rule wrapper.

The bounded process-local cache includes complete data, labels, rule parameters, estimator settings and recommendation thresholds. Custom estimators, custom rule callbacks and unsupported object values bypass automatic caching. Cached scores are copied so caller mutation cannot alter future results. use_cache=False bypasses it; max_cache_entries=0 disables it; clear_cache() removes entries. Caches have no persistent restore or implicit TTL.

Trend Analysis

QualityTrendAnalyzer(storage_path=None) records metrics with record(rule_name, metrics, timestamp=None, data_size=0, notes=""). analyze_trend(rule_name, window_days=30) returns a dictionary; get_history returns measurement dictionaries. At most 100 points per rule are retained. This is a measurement history, not an alerting service or a forecast model.

Approved MCP Quality Operations

A private ProfilerRuntime factory binds quality_scorers to native RuleQualityScorer objects and quality_rules to native ValidationRule objects. inspect lists aliases. quality_score(scorer_id, rule_ids=[one_alias], asset_id) and quality_compare(scorer_id, rule_ids, asset_id) use pinned Arrow data, with optional label_column and use_cache. Results contain rule aliases, metrics and acceptance decisions; private rule names/callbacks/configuration are omitted. quality_cache_clear(scorer_id) clears the selected scorer. These commands require exact approval and sensitive-result consent. Rule lists are unique and limited to 100 entries. Factory configuration owns sampling and estimator options.

Next Steps

Persistent trend snapshots use a cooperating native file lock and atomic sibling publication with file/directory flush. Each writer reloads the latest snapshot before appending. Reopen restores enums, timestamps, intervals and confusion counts. Malformed or oversized snapshots fail rather than becoming empty history. A failed write fences that owner from further recording; open a fresh owner to inspect the observed file before deciding whether another measurement is needed. A failure after publication can leave the new measurement present. This is not an exactly-once receipt protocol. The bound is 4096 rules, 100 points per rule and 16 MiB per snapshot; directories must already exist. Timestamps are sorted in UTC. get_history(limit=0) is empty, and negative limits are rejected.

Factories may bind native QualityTrendAnalyzer objects as quality_trends. quality_record uses the same selected scorer/rule/input as quality_score, plus trend_id, and records the native result. quality_history(trend_id, rule_ids, limit=50) and quality_trend(trend_id, rule_ids, window_days=30) require exactly one rule alias. History omits private notes; storage paths remain factory-owned.