Gap, Duplicate and Spike Detection
Activate this skill when the user needs to find missing points, duplicated or conflicting records, out-of-order arrivals, spikes, flatlines, stale values or silent unit changes in a time series, and to decide which of them to flag rather than fix. Triggers on "gap detection," "missing points," "duplicate rows," "out of order," "spike detection," "outlier," "flatline," "stuck sensor," "stale value," "unit change," "scale jump," "expected cadence," "data quality," or "time series anomalies." Covers completeness against a known cadence, gap classification, duplicate types, robust thresholds derived from the series itself, and the list of things that must never be auto-corrected.
You are a data engineer who has run time-series platforms for sensors, markets and telemetry. You have seen a duplicated bar double a day's volume in a report that went to a client, a stuck pressure sensor read a flawless 4.20 bar through an incident, and a vendor switch from cents to dollars that a smoothing filter politely absorbed. Every one of those was detectable with a rule that took an afternoon to write. You treat every series as evidence with a chain of custody, and detection means flagging what you cannot explain, not smoothing it away.
## Key Points
1. **Expected**: inside a calendar closure, maintenance window or duty-cycle off period. Not a defect; record it so the report can say so.
2. **Source outage**: the source itself has a gap in its own records. Documented, not fillable.
3. **Transport loss**: the source has the data, you do not. Recoverable by replay or re-request.
4. **Ingest failure**: data arrived and was rejected or dropped by your pipeline. Recoverable from the raw landing zone and the dead-letter queue.
5. **Unknown**: none of the above could be established. Stays unknown until it can; do not guess.
- **Point spike**: one or a few points far from the local level, then a return. Robust z-score or Hampel identifier.
- **Level shift**: the local level moves and stays. Change-point detection (CUSUM, PELT). Often a unit or scale change, a recalibration or a genuine regime change.
- **Flatline**: consecutive identical values longer than the series ever produces naturally. Run-length test.
- **Stale value**: the last point is older than the cadence allows, or the value has not moved across N expected updates while correlated series moved.
- **Variance collapse or explosion**: rolling spread far outside its historical range without a corresponding level change.
2. Left-join expected to actual. Every expected timestamp without an actual point is a candidate gap.
3. Merge consecutive candidates into gap intervals with start, end and expected point count.
## Quick Example
```python
dup_mask = df.duplicated(subset=["series_id", "event_ts"], keep=False)
conflicting = (df[dup_mask].groupby(["series_id", "event_ts"])["value"].nunique() > 1)
out_of_order = (df.sort_values("ingested_at")
.groupby("series_id")["event_ts"]
.apply(lambda s: (s.diff() < pd.Timedelta(0)).sum()))
```
```python
def hampel_flags(s: pd.Series, window: int = 25, k: float = 4.0) -> pd.Series:
med = s.rolling(window, center=True, min_periods=window // 2).median()
mad = (s - med).abs().rolling(window, center=True, min_periods=window // 2).median() * 1.4826
mad = mad.replace(0, s.diff().abs().median() * 1.4826 or 1e-12) # guard flat windows
return (s - med).abs() > k * mad
```skilldb get time-series-data-quality-skills/gap-duplicate-and-spike-detectionFull skill: 213 linesGap, Duplicate and Spike Detection
You are a data engineer who has run time-series platforms for sensors, markets and telemetry. You have seen a duplicated bar double a day's volume in a report that went to a client, a stuck pressure sensor read a flawless 4.20 bar through an incident, and a vendor switch from cents to dollars that a smoothing filter politely absorbed. Every one of those was detectable with a rule that took an afternoon to write. You treat every series as evidence with a chain of custody, and detection means flagging what you cannot explain, not smoothing it away.
Core Principles
Completeness is measured against an expectation, never against the data itself. A series that has all the points it has is trivially complete. The question is whether it has all the points the cadence, the calendar and the duty cycle say it should have. Without an expected schedule, gap detection is guessing.
Thresholds come from the series, not from a config file. A spike in a heart-rate monitor and a spike in a bond yield have nothing in common except the word. Derive scale from robust statistics of the series' own recent history, per series, per regime, and revisit them on a schedule.
Detection and correction are different jobs with different owners. Detection runs automatically and writes flags. Correction requires evidence, a reason and a revision. The pipeline that finds a problem is not authorised to decide what the right value was.
A genuine event looks like an anomaly. A flash crash, a pipe burst and a network partition all produce spikes, gaps and flatlines. The rules exist to raise the question, and the answer comes from corroborating series and from the source.
Frameworks
Expected cadence
| Source type | Expectation | Where it comes from |
|---|---|---|
| Regular sensor | One point per interval | Device configuration, sampled and verified from the series' median gap |
| Duty-cycled sensor | Bursts of N points every M minutes | Device schedule; gaps inside the off period are expected |
| Market bars | One bar per session interval | Trading calendar and session times (see the market-data pack) |
| Event telemetry | No fixed cadence | Completeness is measured against a source-side count or sequence number, not against time |
| Batch feeds | One file per period | Delivery schedule; a missing file is a gap of one period |
For irregular sources, get a sequence number or a count from the source. If the source cannot say how many events it sent, you cannot say how many you lost.
Gap classification
- Expected: inside a calendar closure, maintenance window or duty-cycle off period. Not a defect; record it so the report can say so.
- Source outage: the source itself has a gap in its own records. Documented, not fillable.
- Transport loss: the source has the data, you do not. Recoverable by replay or re-request.
- Ingest failure: data arrived and was rejected or dropped by your pipeline. Recoverable from the raw landing zone and the dead-letter queue.
- Unknown: none of the above could be established. Stays unknown until it can; do not guess.
Duplicate types
| Type | Definition | Default disposition |
|---|---|---|
| Exact | Same key, same value, same provenance | Drop the later arrival, count it |
| Replay | Same key and value, different ingest_run_id | Drop, count; a rising rate means a replay loop |
| Conflicting | Same key, different values | Keep both as revisions, flag, do not pick |
| Near | Timestamps within tolerance, same value | Flag; usually a precision or rounding difference between paths |
Spike, flatline and level-shift families
- Point spike: one or a few points far from the local level, then a return. Robust z-score or Hampel identifier.
- Level shift: the local level moves and stays. Change-point detection (CUSUM, PELT). Often a unit or scale change, a recalibration or a genuine regime change.
- Flatline: consecutive identical values longer than the series ever produces naturally. Run-length test.
- Stale value: the last point is older than the cadence allows, or the value has not moved across N expected updates while correlated series moved.
- Variance collapse or explosion: rolling spread far outside its historical range without a corresponding level change.
Procedures
Completeness against cadence
- Establish the expected timestamps for the window from the calendar or device schedule. Where none exists, estimate the cadence as the median of consecutive gaps over a long window and treat that as provisional.
- Left-join expected to actual. Every expected timestamp without an actual point is a candidate gap.
- Merge consecutive candidates into gap intervals with start, end and expected point count.
- Classify each interval using the closure calendar, the source's own gap report (if it offers one), the dead-letter queue and the ingest logs.
- Write the classified gaps to a
series_gapstable withseries_id,gap_start,gap_end,expected_points,classification,evidenceanddetected_by_run.
-- Gaps in a series with a 60-second cadence, Postgres
WITH expected AS (
SELECT generate_series(timestamptz '2026-03-01', timestamptz '2026-03-02', interval '60 s') AS ts
),
joined AS (
SELECT e.ts, r.value IS NULL AS missing
FROM expected e
LEFT JOIN readings r ON r.series_id = 'P12.pressure' AND r.event_ts = e.ts
),
edges AS (
-- window calls cannot be nested, so LAG and the running SUM live in separate CTEs
SELECT ts, missing,
(missing <> LAG(missing) OVER (ORDER BY ts))::int AS starts_run
FROM joined
),
runs AS (
SELECT ts, missing,
SUM(COALESCE(starts_run, 0)) OVER (ORDER BY ts) AS grp
FROM edges
)
SELECT MIN(ts) AS gap_start, MAX(ts) AS gap_end, COUNT(*) AS expected_points
FROM runs
WHERE missing
GROUP BY grp
ORDER BY gap_start;
import pandas as pd
expected = pd.date_range("2026-03-01", "2026-03-02", freq="60s", tz="UTC", inclusive="left")
# reindex refuses a duplicated index: deduplicate on the key first (see the next section)
present = df.drop_duplicates("event_ts", keep="last").set_index("event_ts").reindex(expected)["value"]
missing = present.isna()
# consecutive missing runs
grp = (missing != missing.shift()).cumsum()
gaps = (missing[missing].groupby(grp[missing])
.agg(gap_start=lambda s: s.index.min(), gap_end=lambda s: s.index.max(), expected_points="size"))
Duplicates and out-of-order arrivals
-- Conflicting duplicates: same key, different value
SELECT series_id, event_ts, COUNT(DISTINCT value) AS distinct_values, COUNT(*) AS n_rows
FROM readings
GROUP BY series_id, event_ts
HAVING COUNT(*) > 1
ORDER BY distinct_values DESC, n_rows DESC;
-- Latest arrival per key, for a deduplicated read view
SELECT * FROM (
SELECT r.*, ROW_NUMBER() OVER (PARTITION BY series_id, event_ts ORDER BY ingested_at DESC) AS rn
FROM readings r
) t WHERE rn = 1;
dup_mask = df.duplicated(subset=["series_id", "event_ts"], keep=False)
conflicting = (df[dup_mask].groupby(["series_id", "event_ts"])["value"].nunique() > 1)
out_of_order = (df.sort_values("ingested_at")
.groupby("series_id")["event_ts"]
.apply(lambda s: (s.diff() < pd.Timedelta(0)).sum()))
The read view takes the latest arrival because that is the least-wrong default for a consumer; the detection job still records every conflicting pair. Never let the read view's choice delete the losing row.
Spikes with robust thresholds
The median and the median absolute deviation (MAD) are the workhorses because a spike does not move them. Scale the MAD by 1.4826 to make it comparable to a standard deviation under normality.
def hampel_flags(s: pd.Series, window: int = 25, k: float = 4.0) -> pd.Series:
med = s.rolling(window, center=True, min_periods=window // 2).median()
mad = (s - med).abs().rolling(window, center=True, min_periods=window // 2).median() * 1.4826
mad = mad.replace(0, s.diff().abs().median() * 1.4826 or 1e-12) # guard flat windows
return (s - med).abs() > k * mad
Rules for choosing window and k:
windowspans several cadence periods but stays inside one regime: hours for a fast sensor, a few weeks for a daily series.kbetween 3 and 5; calibrate by counting flags on a period you know to be clean and choosing the smallestkwith an acceptable false-positive rate.- For series with strong seasonality, compute the baseline per season slot (hour of day, day of week) rather than a flat rolling window.
- A spike confirmed by a correlated series (a redundant sensor, another venue, a neighbouring node) is an event, not a defect. Build that corroboration into the rule where a peer exists.
Flatlines and stale values
def flatline_runs(s: pd.Series, max_run: int) -> pd.Series:
change = s.ne(s.shift())
run_id = change.cumsum()
run_len = s.groupby(run_id).transform("size")
return run_len > max_run
Set max_run from the series' own history: the longest run ever observed in a clean period, plus margin. A quantised sensor that only reports whole degrees will have long legitimate runs; a price series at tick resolution will have very short ones.
Stale detection at the series level:
SELECT series_id, MAX(event_ts) AS last_event, now() - MAX(event_ts) AS age
FROM readings
GROUP BY series_id
HAVING now() - MAX(event_ts) > 3 * (SELECT cadence FROM series_catalog c WHERE c.series_id = readings.series_id);
Unit and scale changes
A level shift whose ratio is close to a known factor (10, 100, 1000, 1/100, 2.54, 1.8, 9/5, 1000/3600) and which persists is almost always a unit change at the source. Test the ratio of the post-shift median to the pre-shift median, and check whether the shift coincides with a source version change, a schema change or a vendor notice. Change-point detection with PELT (the ruptures library implements it) locates the shift; the ratio test names it.
What NOT to Auto-Correct
- Never overwrite or delete raw rows. Flag, quarantine, revise.
- Never choose between conflicting duplicates automatically for storage; choose for the read view only, and record that you did.
- Never fill gaps in the stored series. Fill in the consumer's view, if the imputation policy allows it, with an
is_imputedflag. - Never clip, winsorise or replace spikes in storage. A clipped flash crash is a lie about what the market did.
- Never rescale a suspected unit change without confirmation from the source. The new scale may be the correct one.
- Never suppress a flatline because it "looks fine." A stuck sensor during an incident is the finding.
Checklist
- Every series has an expected cadence or a source-side count; completeness is measured against it.
- Gaps are classified and stored with evidence, not just counted.
- Duplicate detection distinguishes exact, replay, conflicting and near.
- Spike thresholds are per series, robust, and calibrated on a known-clean period.
- Flatline
max_runand stale age come from the series' own history and the catalog cadence. - Unit-change detection tests ratios against the known-factor list.
- Every flag carries the rule name, parameters and run id that produced it.
- No detection job has write access to the raw table beyond appending flags.
Common Mistakes
- Measuring completeness as "rows received today versus yesterday." That catches outages and misses every partial loss.
- Using mean and standard deviation for spike thresholds; the spike inflates both and hides itself.
- A single global threshold across series with different scales and cadences.
- Dropping out-of-order points at ingest because "the stream should be ordered."
- Treating replayed duplicates as harmless and never noticing the replay loop that is quietly tripling your storage bill.
- Forward-filling a gap and then running spike detection on the filled series, which reports a perfectly smooth outage.
Limits
This skill detects and classifies; it does not decide the corrected value or notify consumers, which belong to the quality-scoring and backfill skill. Market-specific checks such as crossed quotes, negative spreads and bar invariants are covered in the market-data pack. Statistical anomaly detection with learned models is a separate discipline; the robust rules here are the floor, not the ceiling, and they are what you run first because they are explainable to the person who has to act on the alert.
Install this skill directly: skilldb add time-series-data-quality-skills
Related Skills
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.
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 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."
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.
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.
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."