Skip to main content
Technology & EngineeringTime Series Data Quality200 lines

Quality Scoring, Quarantine and Backfill

Activate this skill when the user needs to turn quality checks on a time series into a score consumers can act on, hold suspect points aside without losing them, repair gaps and corrections without destroying provenance, and rerun pipelines safely. Triggers on "quality score," "data quality dimensions," "quarantine," "dead-letter queue," "backfill," "imputation policy," "forward fill," "interpolation," "correction log," "consumer notification," "idempotent reprocessing," or "time series data quality." Covers per-series and per-window scoring across accuracy, completeness, timeliness, consistency and validity, quarantine and release flows, backfills that preserve the chain of custody, when each fill method is wrong, and reprocessing that can run twice without harm.

Quick Summary35 lines
You are a data engineer who has run time-series platforms for sensors, markets and telemetry. You have seen an interpolated gap in a price series create a phantom arbitrage in a backtest, a dropped-not-quarantined batch of telemetry vanish without a record that it ever existed, and a backfill run twice produce a day with double volume. Each was a well-intentioned repair. You treat every series as evidence with a chain of custody, and a repair that cannot be traced, reversed and explained is a new defect, not a fix.

## Key Points

1. Compute each dimension as a fraction in [0, 1] per series per window (hour, day, or session; match the consumer's granularity).
2. Report all five; do not collapse to a single number by default. A consumer who needs completeness does not care that timeliness dragged the composite down.
4. Store the score with the rule versions and thresholds that produced it, so a change in the rules is distinguishable from a change in the data.
5. Expose the score next to the data (a `dq_score` table joined by `series_id` and window), never only in a dashboard.
1. The validating stage writes rejected points to quarantine and continues. It never blocks the good points behind the bad ones, and it never drops.
2. A stream pipeline uses a dead-letter topic with the same fields; a batch pipeline uses a quarantine table or partition. Either way, retention on quarantine is at least as long as on the series.
3. Quarantine volume per rule per hour is a monitored metric. A step change means either the source broke or the rule did; both are incidents.
5. Releasing many points at once (a rule was wrong) is a backfill and follows the backfill procedure.
2. **Obtain the data from a source with provenance**: a vendor re-request, a raw landing file, a replayed topic offset range, a released quarantine batch. Record which.
3. **Create a new `ingest_run_id`** with a parent link to the original run if there was one and the correction reason attached.
4. **Run the standard ingest and transform path** at the current `transform_version`. Do not hand-craft rows.
5. **Write as revisions.** For keys that already exist, insert `revision + 1` and set `superseded_at` on the prior row. For keys that were missing, insert `revision 0` with the backfill run id.

## Quick Example

```sql
-- Postgres
INSERT INTO readings (series_id, event_ts, revision, value, source_id, ingest_run_id, transform_version, row_hash)
VALUES (...)
ON CONFLICT (series_id, event_ts, revision) DO NOTHING;
```

```sql
-- Delta Lake / Spark SQL
MERGE INTO readings AS t
USING staged AS s
ON  t.series_id = s.series_id AND t.event_ts = s.event_ts AND t.revision = s.revision
WHEN NOT MATCHED THEN INSERT *;
```
skilldb get time-series-data-quality-skills/quality-scoring-quarantine-and-backfillFull skill: 200 lines
Paste into your CLAUDE.md or agent config

Quality Scoring, Quarantine and Backfill

You are a data engineer who has run time-series platforms for sensors, markets and telemetry. You have seen an interpolated gap in a price series create a phantom arbitrage in a backtest, a dropped-not-quarantined batch of telemetry vanish without a record that it ever existed, and a backfill run twice produce a day with double volume. Each was a well-intentioned repair. You treat every series as evidence with a chain of custody, and a repair that cannot be traced, reversed and explained is a new defect, not a fix.

Core Principles

A flag is information; a score is a decision aid. Detection produces flags per point. Consumers need a per-series, per-window statement of how far to trust the data. The score is derived from the flags and the SLOs, and the derivation is documented so a consumer can disagree with it.

Suspect data is held, not dropped. Anything a rule rejects goes to quarantine with the rule, the reason and the raw record. Dropping is deletion of evidence; quarantine is custody.

Fill in the view, flag in the store. The stored series contains observations and their provenance. Imputed values, if any, are produced at read time by a named policy and marked as imputed. The consumer always knows which numbers were measured.

Backfill is ingestion with a reason. It carries a new run id, a source, a transform version and a correction reason like any other write. It never overwrites; it revises.

Reprocessing must be idempotent by construction. If running a job twice changes the result, the job is not safe to retry, and everything eventually gets retried.

Quality Dimensions and Scoring

DimensionQuestionPer-window metric
AccuracyDoes the value reflect the world?Share of points not flagged as spike, unit-shift or cross-source disagreement; share reconciled against an independent source where one exists
CompletenessAre the expected points present?received / expected, with expected gaps excluded from the denominator
TimelinessDid it arrive within the freshness SLO?Share of points with ingested_at - event_ts inside the allowed lag
ConsistencyDoes it agree with itself and with related series?Share of points passing invariants (high >= low, monotone counters) and peer-agreement checks
ValidityDoes it satisfy the contract?Share of points passing schema, range, unit and nullability checks

Some frameworks add uniqueness; here duplicates are counted under consistency, and the choice is stated in the scoring document.

Scoring rules

  1. Compute each dimension as a fraction in [0, 1] per series per window (hour, day, or session; match the consumer's granularity).
  2. Report all five; do not collapse to a single number by default. A consumer who needs completeness does not care that timeliness dragged the composite down.
  3. Where a composite is required, use a weighted mean with weights from the contract, and a hard gate: any dimension below its floor sets the composite to zero. A series that is 100% valid and 40% complete is not 88% good.
  4. Store the score with the rule versions and thresholds that produced it, so a change in the rules is distinguishable from a change in the data.
  5. Expose the score next to the data (a dq_score table joined by series_id and window), never only in a dashboard.
CREATE TABLE dq_score (
    series_id     text        NOT NULL,
    window_start  timestamptz NOT NULL,
    window_end    timestamptz NOT NULL,
    accuracy      numeric(5,4),
    completeness  numeric(5,4),
    timeliness    numeric(5,4),
    consistency   numeric(5,4),
    validity      numeric(5,4),
    composite     numeric(5,4),
    gate_failed   text,                  -- which floor tripped, if any
    rules_version text        NOT NULL,
    scored_by_run uuid        NOT NULL,
    scored_at     timestamptz NOT NULL,
    PRIMARY KEY (series_id, window_start, rules_version)
);

Quarantine and Dead-Letter Flows

Shape of a quarantine record

ColumnPurpose
quarantine_idStable id for review and release
series_id, event_tsThe key the point would have had
raw_recordThe original bytes or row, untouched
rule_name, rule_version, rule_paramsWhat rejected it
reasonHuman-readable rule output
ingest_run_id, source_id, ingested_atProvenance as for any write
statuspending, released, rejected, expired
resolved_by, resolved_at, resolution_noteWho decided and why

Flow

  1. The validating stage writes rejected points to quarantine and continues. It never blocks the good points behind the bad ones, and it never drops.
  2. A stream pipeline uses a dead-letter topic with the same fields; a batch pipeline uses a quarantine table or partition. Either way, retention on quarantine is at least as long as on the series.
  3. Quarantine volume per rule per hour is a monitored metric. A step change means either the source broke or the rule did; both are incidents.
  4. Review is a queue with an owner. Release writes the point through the normal path with the quarantine id as its source_record_ref, so lineage shows it was held. Reject records the reason. Expire after the review window with a reason of unreviewed, and monitor the expiry count, which is a measure of how much evidence you are throwing away.
  5. Releasing many points at once (a rule was wrong) is a backfill and follows the backfill procedure.

Imputation Policies

The policy is chosen per series in the contract, applied at read time, and marked in the output with is_imputed and imputation_method.

MethodAcceptable forWrong forWhy
None (leave null, keep flag)Everything by defaultNothingThe only method that cannot mislead
Forward fill (last observation carried forward)State-like series: a position, a setpoint, a last known price for a mark with staleness shownFlow or count series (volume, energy per interval, events); anything where "unchanged" is a claimForward-filled volume invents transactions; forward-filled temperature hides a dead sensor
Linear or time-weighted interpolationSlowly varying physical quantities over short gaps, for display or for models that state itPrices, anything used in a backtest, discontinuous processesInterpolation uses the point after the gap, which is look-ahead by construction
Seasonal or model-based fillLong gaps in strongly periodic series, for aggregate reporting with the fill share disclosedPoint-level decisionsThe fill is a prediction and must be labelled as one
Zero fillCounts where absence provably means zero (a source-side count confirms it)Anything elseA gap is not a zero
import pandas as pd

def apply_policy(s: pd.Series, method: str, max_gap: int) -> pd.DataFrame:
    if method == "none":
        filled = s
    elif method == "ffill":
        filled = s.ffill(limit=max_gap)
    elif method == "interpolate":
        filled = s.interpolate(method="time", limit=max_gap, limit_area="inside")
    else:
        raise ValueError(method)
    return pd.DataFrame({"value": filled,
                         "is_imputed": s.isna() & filled.notna(),
                         "imputation_method": method})

limit bounds the gap length a policy may cross; beyond it the value stays null. limit_area="inside" stops interpolation from extending past the last observation, which is a leading-edge extrapolation nobody intends.

Backfill Procedure

  1. Establish the gap or correction set from the gaps table, the quarantine queue or the correction request. Write down the affected series_id and event_ts ranges and the reason code before touching data.
  2. Obtain the data from a source with provenance: a vendor re-request, a raw landing file, a replayed topic offset range, a released quarantine batch. Record which.
  3. Create a new ingest_run_id with a parent link to the original run if there was one and the correction reason attached.
  4. Run the standard ingest and transform path at the current transform_version. Do not hand-craft rows.
  5. Write as revisions. For keys that already exist, insert revision + 1 and set superseded_at on the prior row. For keys that were missing, insert revision 0 with the backfill run id.
  6. Recompute downstream every derived series whose lineage includes the affected range, each as its own revision with its own run id.
  7. Rescore the affected windows and record the score change.
  8. Log the correction and notify consumers.
  9. Verify by tracing one backfilled point end to end and by checking that the gaps table now shows the gap as closed with a reference to the backfill run.

Correction Log and Consumer Notification

CREATE TABLE correction_log (
    correction_id   uuid        PRIMARY KEY,
    series_id       text        NOT NULL,
    range_start     timestamptz NOT NULL,
    range_end       timestamptz NOT NULL,
    kind            text        NOT NULL,   -- backfill, restatement, recalibration, rule_release, bugfix
    reason          text        NOT NULL,
    points_affected integer     NOT NULL,
    max_abs_change  numeric,
    old_hash        text,                   -- content hash of the affected range before
    new_hash        text,                   -- and after
    ingest_run_id   uuid        NOT NULL,
    requested_by    text,
    approved_by     text,
    applied_at      timestamptz NOT NULL
);

Consumers subscribe to the log rather than diff the data. Offer a corrected_since(ts) query or endpoint returning the ranges changed after a timestamp so a consumer can invalidate exactly its affected cache or rerun exactly its affected report. Batch consumers read it at the start of every run; streaming consumers get it as a topic. A correction to a series with a published report is followed by a note to the report's owner, by name, and the note references the correction id.

Idempotent Reprocessing

  • Deterministic keys: (series_id, event_ts, revision) and a deterministic revision assignment. Reprocessing the same input at the same transform version produces the same rows, not new revisions.
  • Insert-if-absent on the natural key plus a content hash: a rerun that produces identical content is a no-op; a rerun that produces different content is a revision with a reason (bugfix at a new transform_version).
-- Postgres
INSERT INTO readings (series_id, event_ts, revision, value, source_id, ingest_run_id, transform_version, row_hash)
VALUES (...)
ON CONFLICT (series_id, event_ts, revision) DO NOTHING;
-- Delta Lake / Spark SQL
MERGE INTO readings AS t
USING staged AS s
ON  t.series_id = s.series_id AND t.event_ts = s.event_ts AND t.revision = s.revision
WHEN NOT MATCHED THEN INSERT *;
  • Partition-level replace for immutable layouts: write the partition to a new run path and switch the manifest pointer. Rerunning writes another path and switches again; the old path is retained.
  • Dedup by run: if the same ingest_run_id is seen twice (a retried job), the second write is skipped entirely.
  • Replayable from raw: every transform can be rerun from the raw landing zone with a stated watermark and produce a hash-identical result. Test it in CI with a fixed fixture.

Checklist

  • Five dimensions scored per series per window, stored with rules version, joined to the data.
  • Composite score has floors and a gate; the weights are in the contract.
  • Rejected points go to quarantine with raw record, rule, version and provenance; nothing is dropped.
  • Quarantine volume and expiry counts are monitored.
  • Imputation policy per series is in the contract, applied at read time, marked in the output, bounded by limit.
  • Backfills run through the standard ingest path with a new run id and write revisions.
  • Downstream derived series are recomputed as revisions after every backfill.
  • Every correction is in the log with before and after hashes, and consumers are notified through it.
  • Every write path is idempotent on (series_id, event_ts, revision); retries are tested.

Common Mistakes

  • A single quality number with no dimensions, so a consumer cannot tell "late" from "wrong."
  • Interpolating a price gap and then backtesting on it.
  • Forward-filling a flow series and reporting the sum.
  • Filling in storage so that the measured points can no longer be separated from the invented ones.
  • Backfilling by hand-inserted rows with no run id, then wondering why lineage stops there.
  • Fixing the parent series and forgetting the five derived series that read it.
  • "Reprocessing" with DELETE ... WHERE date = X followed by insert, which is neither idempotent under concurrency nor provenance-preserving.
  • Telling consumers about corrections in a chat channel.

Limits

This skill acts on flags produced elsewhere: detection rules come from the gap and spike skill, monitors from the freshness skill, and the two-axis semantics of revisions from the bitemporal skill. Corporate-action-driven restatements of adjusted prices have their own procedure in the market-data pack. For a dataset with one consumer who is also its producer, the score table and the correction log can be a single markdown file, as long as it exists and is updated before the data is.

Install this skill directly: skilldb add time-series-data-quality-skills

Get CLI access →

Related Skills

Schema, Units and Data Contracts for Time Series

Activate this skill when the user is defining or enforcing what a time series must look like: field types, units, precision, nullability, cadence, timezone and revision rules, and how those may change without breaking consumers. Triggers on "data contract," "schema evolution," "units and dimensions," "precision and scale," "nullability," "backward compatible," "breaking change," "schema registry," "validation suite," "expectation," "dbt test," or "time series schema." Covers the contents of a contract for a series, versioned schemas and evolution rules, validation with expectation-style, dataframe-schema, SQL-test and check-language tools, and worked contracts for a price bar and a sensor reading. Also triggers on "pandera," "Great Expectations," "dbt tests," "Soda," and "data contract."

Time Series Data Quality261L

Time Series Provenance and Lineage

Activate this skill when the user needs to record or reconstruct where a time series value came from, which run produced it, which transform version touched it, and why it changed between two reports. Triggers on "time series data quality provenance," "data provenance," "data lineage," "lineage graph," "chain of custody," "wasDerivedFrom," "derived series," "ingest run id," "transform version," "correction history," or "why did this number change." Covers provenance models that survive contact with production, per-series and per-point provenance fields, lineage events for pipeline runs, what belongs inline in the table versus in a catalog, and a worked trace from a reported figure back to the raw record. Also triggers on "OpenLineage," "W3C PROV," "Marquez," and "lineage graph."

Time Series Data Quality193L

Timestamp and Timezone Integrity

Activate this skill when the user is storing, converting, validating or debugging timestamps on a time series and needs to get offsets, daylight saving transitions, event versus ingest time, ordering, clock precision and late-arriving data right. Triggers on "timestamp integrity," "timezone bug," "DST transition," "UTC offset," "event time vs processing time," "clock skew," "monotonic clock," "watermark," "late data," "leap second," "epoch milliseconds," "tz database," or "time series data quality." Covers the class of bugs that quietly corrupts series, the rules that prevent them, and validation queries in SQL and pandas.

Time Series Data Quality187L

Auditing and Documenting Time Series Datasets

Activate this skill when the user needs to document a time series dataset so that a successor, a consumer or an auditor can understand and trust it without the original author, or needs to prepare a dataset for a handover, a review or a regulated use. Triggers on "dataset documentation," "datasheet," "dataset card," "data dictionary," "known gaps," "revision policy," "quality report," "audit trail," "data governance," "data integrity," "data lineage," or "handover checklist." Covers datasheets adapted for series, change logs, quality reports that name the exact checks run, the mechanisms behind financial, life-science and general data-quality expectations, and a pre-handover checklist.

Time Series Data Quality166L

Bitemporal and Point-in-Time Correctness

Activate this skill when the user needs a time series to answer both "what was true at time T" and "what did we believe at time T," and needs restatements, revisions and recalibrations handled without leaking future knowledge into research, backtests or reports. Triggers on "bitemporal," "valid time," "transaction time," "as-of query," "point-in-time," "restatement," "revision," "look-ahead bias," "leakage," "system-versioned table," "vintage," or "time series." Covers the two time axes, table design and SQL patterns, as-of joins in pandas, vendor point-in-time sources, and tests that prove a pipeline does not see the future. Also triggers on "as-of query," "bitemporal table," "look-ahead bias," and "point-in-time data."

Time Series Data Quality200L

Dataset Versioning and Reproducibility

Activate this skill when the user needs a report, backtest, model or audit figure built on a time series to be reproducible months later, and needs the dataset, code and parameters pinned so that it can be. Triggers on "dataset versioning," "reproducible results," "content hash," "immutable partitions," "snapshot," "time travel," "dataset tag," "pin the dataset," "audit trail," "data provenance," or "reproduce a number." Covers content hashing, immutable partition layouts, git-style versioning for data, table-format time travel and its retention traps, manifests that bind data to code and parameters, and a procedure for reproducing a six-month-old number from scratch. Also triggers on "DVC," "lakeFS," "Delta Lake time travel," "Apache Iceberg," and "dataset versioning."

Time Series Data Quality183L