Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Data Analysis with Python (pandas, polars, DuckDB)  Module 15 – Data-quality checks you can automate

Data-quality dimensions and checks in code

Turn the six data-quality dimensions into pandas checks that return a results table, stop a load on critical failures and keep the results of every run.

  • Intermediate
  • 30 minutes
  • Examples run with Python 3.14.8, pandas 3.0.6, duckdb 1.5.6 and Pyodide 314.0.7
  • By MySmartCoPilot
Free lesson, Pro answers: 30 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Define completeness, validity, uniqueness, consistency, timeliness and accuracy checks
  • Write reusable check functions that return a results table
  • Set a severity for every check, stopping a load on critical failures and warning on the rest
  • Track data-quality results over time to catch slow drifts as well as sudden breaks

Before you start

The original broken export has 23 orders: one repeated key, an unknown customer, invalid quantities and a stale newest timestamp. A data-quality check compares those rows with a written expectation and records its observation, status and sample. This lesson keeps the six quality dimensions, ten original checks and two-file result table, then uses their severities to decide whether a load proceeds.

Six dimensions define different expectations

A data-quality check is an expectation about data, written as code: order ids are unique, every order has a customer, a status is one of five words, the newest row is less than a day old. Run against a table, a check either passes or names the rows that break it. Checks matter because bad data rarely raises an error. An order loaded twice or a feed that stopped at dawn still produces a plausible number, and the report goes out with it. This lesson turns the six dimensions of data quality into checks for an orders export, collects their results in one table, stops a load when a critical check fails, and keeps the results of every run so that a slow drift shows up as clearly as a sudden break.

The quality expectation: A stated rule about data for a defined use, including the observation and acceptance condition that a check evaluates. states the rule and intended use. The UK government’s Data Quality Framework describes six core dimensions, taken from the data management association DAMA UK: completeness, uniqueness, consistency, timeliness, validity and accuracy. The framework presents them as measurable features of data and says the list may be adapted to your data and your users. Each dimension becomes a question you can ask of a table, and most questions become a check:

Item Details
Completeness The question it asks: Are the records and the values that must be there present?; A check on an orders export: Every order has a customer id; at most 5 % of emails are missing
Uniqueness The question it asks: Is each thing recorded once?; A check on an orders export: No order id appears twice
Validity The question it asks: Is each value in the expected format and range?; A check on an orders export: A status is one of five words; a quantity is from 1 to 50
Consistency The question it asks: Do values agree with each other and with other tables?; A check on an orders export: The amount equals quantity × unit price; every customer id exists in the customers table
Timeliness The question it asks: Is the data recent enough for its use?; A check on an orders export: The newest order is at most 26 hours old when the check runs
Accuracy The question it asks: Does the data match reality?; A check on an orders export: Only a source outside the table can tell: the payment provider’s daily total, or a sample of orders compared with their invoices

Accuracy is the odd one out. A table can be complete, unique, valid, consistent and fresh, and still be wrong: a mistyped price passes every rule if it falls inside the allowed range. Checks on the table itself can only make accuracy problems less likely. Proving accuracy needs a comparison with an independent source, which is the subject of reconciliation later in this module.

Distinguish required values, valid ranges and independent accuracy evidence before designing the suite.

Interactive · stage by stage Six dimensions define different expectations Distinguish required values, valid ranges and independent accuracy evidence before designing the suite.
Six dimensions define different expectations, step 3 of 3. Showing Completeness, Validity and consistency, Accuracy. In focus: Accuracy.CompletenessRequired fields presentValidity and consistencyRange and relationshipsAccuracyIndependent evidence

The drawing shows the whole scene; its 3 steps are listed below.

Every step as text
  1. A present row can still lack the customer key required by a report. Completeness asks whether the required records and values are there.
  2. A quantity range checks one value, while a known-customer relationship compares it with a reference table. These answer different expectations.
  3. Passing structural checks does not establish that a plausible price matches reality. Accuracy needs relevant comparison evidence beyond those structural rules.

Checkpoint

Check: Six dimensions define different expectations

  1. Question 1 of 2 Does a valid range prove a price is accurate?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: No, a plausible but wrong price can still be in range

    Range validity checks a stated boundary rather than the actual transaction. A range cannot identify every plausible transcription or source error.

  2. Question 2 of 2 The original framework names six dimensions. How many remain after reviewing two?

    Type a number.

    Show the answer: Question 2 of 2

    Answer: 4 dimensions

    Six dimensions minus two reviewed dimensions leaves four.

A profile describes values while a check states an expectation

Keep the profile and the acceptance rule as separate records. An approximate distinct count is useful for exploration, but the decision to load needs the exact rule chosen for that key. Check an explicit repeated occurrence and a missing key as different cases. A sample of distinct offending keys can be short even when many rows fail, so do not infer the failing-row count from the sample length.

A profile describes what the data looks like. DuckDB’s SUMMARIZE computes, for every column, the minimum and maximum, an approximate count of distinct values, the average, the standard deviation, the quartiles, the row count and the percentage of missing values, and pandas’ describe() does much the same. A check is different: it states what the data should look like, and it fails. The difference shows on the broken export used in this lesson, a file of 23 orders with problems planted in it:

SUMMARIZE beside two checks on the same file Python · profile_or_check.py
"""A profile describes the data; a check compares it with an expectation. The same file, both ways."""
import duckdb
import pandas as pd

profile = duckdb.sql("SUMMARIZE SELECT * FROM read_csv('orders_broken.csv', types = {'customer_id': 'VARCHAR'})").df()
pd.set_option("display.width", 130)
print(profile[["column_name", "column_type", "min", "max", "approx_unique", "count", "null_percentage"]].to_string(index=False))

orders = pd.read_csv("orders_broken.csv", dtype={"order_id": "str", "customer_id": "str"})
print("\nrepeated order ids:", orders["order_id"].duplicated().sum())
print("quantities outside 1 to 50:", (~orders["quantity"].between(1, 50)).sum())

Output

column_name              column_type                    min                    max  approx_unique  count  null_percentage
   order_id                  VARCHAR                O-24001                O-24022             23     23             0.00
customer_id                  VARCHAR                  C1001                  C1100             18     23             4.35
   order_ts TIMESTAMP WITH TIME ZONE 2026-03-14 05:01:00+00 2026-03-15 01:12:00+00             15     23             0.00
       city                  VARCHAR              Bengaluru                   Pune              9     23             0.00
     status                  VARCHAR              Delivered                shipped              6     23             0.00
   quantity                   BIGINT                      0                    120              7     23             0.00
 unit_price                   DOUBLE                  149.0                 1599.0              5     23             0.00
     amount                   DOUBLE                    0.0                29940.0             16     23             0.00
      email                  VARCHAR    aarav15@example.com       om24@example.com             16     23            17.39

repeated order ids: 1
quantities outside 1 to 50: 2

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 profile_or_check.py

The profile is useful, and it raises questions: a quantity of 0 and one of 120, a status spelt Delivered with a capital letter, 17.39 % of emails missing. It cannot answer them, because it does not know the rules. Its order-id line even looks reassuring, 23 distinct values in 23 rows, yet one id appears twice. approx_unique is an estimate; DuckDB computes approximate distinct counts with HyperLogLog, a sketch that trades exactness for speed, and on this file it is off by one for the order ids and by seven for the timestamps. Use profiles to decide which checks to write, then let the checks decide whether a file is fit to load.

CSV & Excel Data Profiler (EDA Report) Profile a CSV or Excel export in the browser to see each column's types, gaps and value ranges before you write its checks.

Compare the original approximate 23 with the exact repeat count before accepting key uniqueness.

Interactive · stage by stage A profile describes values while a check states an expectation Compare the original approximate23 with the exact repeat count before accepting key uniqueness.
A profile describes values while a check states an expectation, step 3 of 3. Showing Original profile, Exact duplicate check, Expectation. In focus: Expectation.Original profile23 ordersExact duplicate checkOne repeated keyExpectationQuantity 1 to 50

The drawing shows the whole scene; its 3 steps are listed below.

Every step as text
  1. The original broken file has 23 rows and a profile with an approximate unique count of 23 for the order key.
  2. The original exact duplicate calculation finds one repeat despite that approximate count.
  3. A range check finds two quantities outside the inclusive 1-to-50 rule. The profile itself does not know that business rule.

Checkpoint

Check: A profile describes values while a check states an expectation

  1. Question 1 of 2 What establishes the original order-key uniqueness expectation?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: An exact duplicate check against the stated rule

    The exact check counts violations of the required key rule. An estimate can coincide with the row count while a duplicate exists.

  2. Question 2 of 2 The original file has 23 rows and one repeated occurrence. How many distinct keys remain if all other keys are distinct?

    Type a number.

    Show the answer: Question 2 of 2

    Answer: 22 keys

    One repeated occurrence reduces the distinct count from 23 to 22.

A results row makes each check’s outcome visible

The check result row: A structured record of a check's name, observation, status, severity and relevant sample under its stated counting policy. records its observation and outcome. Each check below takes a DataFrame and a few parameters and returns one dictionary: the check’s name, its dimension, its severity, a status, the number of failing rows and a sample of what failed. Ten of them make a suite for the orders export, and the suite runs on two files, a clean export and the broken one:

Ten checks, two files, two results tables

two_files.py

"""The same ten checks on a clean export and on an export with planted problems."""
import pandas as pd

from orders_suite import load, run_checks

customers = pd.read_csv("customers.csv", dtype={"customer_id": "str"})
pd.set_option("display.width", 130)
for name in ["orders_clean.csv", "orders_broken.csv"]:
    results = run_checks(load(name), customers)
    failed = results["status"].eq("FAIL").sum()
    warned = results["status"].eq("warn").sum()
    print(f"{name}: {failed} failed, {warned} warnings")
    print(results.to_string(index=False))
    print()

Output

orders_clean.csv: 0 failed, 0 warnings
                      check    dimension severity status  failing       sample
            columns present completeness critical   pass        0             
            order_id unique   uniqueness critical   pass        0             
    customer_id not missing completeness critical   pass        0             
          customer_id known  consistency critical   pass        0             
             status allowed     validity critical   pass        0             
               city allowed     validity     warn   pass        0             
           quantity 1 to 50     validity critical   pass        0             
  amount = quantity x price  consistency critical   pass        0             
        email missing <= 5% completeness     warn   pass        1 2.5% missing
newest order_ts <= 26 h old   timeliness critical   pass        0   10.5 h old

orders_broken.csv: 7 failed, 2 warnings
                      check    dimension severity status  failing                sample
            columns present completeness critical   pass        0                      
            order_id unique   uniqueness critical   FAIL        1               O-24017
    customer_id not missing completeness critical   FAIL        1               O-24005
          customer_id known  consistency critical   FAIL        2          C1099, C1100
             status allowed     validity critical   FAIL        2 Delivered, dispatched
               city allowed     validity     warn   warn        1              Gurugram
           quantity 1 to 50     validity critical   FAIL        2                0, 120
  amount = quantity x price  consistency critical   FAIL        1               O-24018
        email missing <= 5% completeness     warn   warn        4         17.4% missing
newest order_ts <= 26 h old   timeliness critical   FAIL        1            26.8 h old

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 two_files.py

orders_suite.py

"""The ten checks of one table, the orders export, built from the reusable checks in checks.py."""
import pandas as pd

import checks

REQUIRED = ["order_id", "customer_id", "order_ts", "city", "status", "quantity", "unit_price", "amount", "email"]
STATUSES = ["placed", "shipped", "delivered", "cancelled", "returned"]
CITIES = ["Bengaluru", "Chennai", "Hyderabad", "Kolkata", "Mumbai", "New Delhi", "Pune"]
# The run time, fixed so that the example prints the same output every time; a job uses pd.Timestamp.now(tz="UTC").
NOW = pd.Timestamp("2026-03-16 09:30", tz="Asia/Kolkata")


def load(path):
    df = pd.read_csv(path, dtype={"order_id": "str", "customer_id": "str"})
    df["order_ts"] = pd.to_datetime(df["order_ts"], utc=True)
    return df


def run_checks(df, customers):
    present = checks.columns_present(df, REQUIRED)
    if present["failing"]:
        return pd.DataFrame([present])  # the other checks need these columns
    rows = [
        present,
        checks.unique(df, "order_id"),
        checks.not_missing(df, "customer_id", key="order_id"),
        checks.found_in(df, "customer_id", customers["customer_id"]),
        checks.allowed(df, "status", STATUSES),
        checks.allowed(df, "city", CITIES, severity="warn"),
        checks.between(df, "quantity", 1, 50),
        checks.amount_matches(df, key="order_id"),
        checks.missing_share(df, "email", limit=0.05),
        checks.fresh(df, "order_ts", NOW, max_hours=26),
    ]
    return pd.DataFrame(rows)

checks.py

"""Reusable data-quality checks. Each states an expectation and returns one row of the results table."""
import pandas as pd


def result(check, dimension, severity, failing, sample=(), ok=None):
    """One row of the results table. A failed check stops the run when it is critical and warns otherwise."""
    if ok is None:
        ok = failing == 0
    status = "pass" if ok else ("FAIL" if severity == "critical" else "warn")
    shown = ", ".join(str(v) for v in list(sample)[:3])
    return {"check": check, "dimension": dimension, "severity": severity, "status": status, "failing": int(failing), "sample": shown}


def columns_present(df, required):
    missing = [c for c in required if c not in df.columns]
    return result("columns present", "completeness", "critical", len(missing), missing)


def unique(df, column):
    repeated = df[column].notna() & df[column].duplicated()
    return result(f"{column} unique", "uniqueness", "critical", repeated.sum(), df.loc[repeated, column].unique())


def not_missing(df, column, key):
    gap = df[column].isna()
    return result(f"{column} not missing", "completeness", "critical", gap.sum(), df.loc[gap, key])


def missing_share(df, column, limit):
    """A soft completeness check: some gaps are normal, too many are a warning."""
    gap = df[column].isna()
    return result(f"{column} missing <= {limit:.0%}", "completeness", "warn", gap.sum(), [f"{gap.mean():.1%} missing"], ok=gap.mean() <= limit)


def allowed(df, column, values, severity="critical"):
    bad = df[column].notna() & ~df[column].isin(values)
    return result(f"{column} allowed", "validity", severity, bad.sum(), df.loc[bad, column].unique())


def between(df, column, low, high):
    bad = df[column].notna() & ~df[column].between(low, high)
    return result(f"{column} {low} to {high}", "validity", "critical", bad.sum(), df.loc[bad, column].unique())


def found_in(df, column, keys):
    """Referential integrity: every value of `column` exists in `keys` (an anti-join that counts the orphans)."""
    orphan = df[column].notna() & ~df[column].isin(keys)
    return result(f"{column} known", "consistency", "critical", orphan.sum(), df.loc[orphan, column].unique())


def amount_matches(df, key):
    off = (df["amount"] - df["quantity"] * df["unit_price"]).abs() > 0.005
    return result("amount = quantity x price", "consistency", "critical", off.sum(), df.loc[off, key])


def fresh(df, column, now, max_hours):
    """Timeliness: the newest row is at most max_hours older than the run time (both time-zone aware)."""
    age = (now - df[column].max()) / pd.Timedelta(hours=1)
    stale = not age <= max_hours  # also true when the table is empty and age is NaN
    return result(f"newest {column} <= {max_hours} h old", "timeliness", "critical", int(stale), [f"{age:.1f} h old"])

On the clean file every check passes. One email of 40 is missing, which the soft completeness check accepts because 2.5 % is under its limit, and the newest order is 10.5 hours old. On the broken file the samples say where to look: order O-24017 was loaded twice, O-24005 has no customer, customers C1099 and C1100 are not in the customers table, Delivered is a spelling problem rather than a new status, dispatched is not a status of this shop, quantities of 0 and 120 are impossible, and O-24018 charges 200 more than one item at 399. The check for unknown customers is an anti-join that counts the orders without a partner instead of listing them.

A results table is worth more than printed messages:

  • Passes are recorded too. A row that says pass proves that the check ran; a missing row is a check that was skipped.
  • It can be filtered, sorted, saved and compared. The same table drives the decision to load, the alert and the history.
  • One problem is reported once. Under the stated missingness policy: The rule for how absent values are counted, rejected, allowed or excluded from a particular calculation., every check except those that look for gaps skips missing values, so an order without a customer is counted by customer_id not missing and not again by customer_id known.
  • Samples are keys and values, not whole rows. An alert that pastes rows would copy customers’ emails into chat messages and logs; an order id is enough to find the row.
  • Checks that need a column wait for it. If a column is missing, the suite reports that and runs nothing else.

Read the ten result rows as expectations, observations, statuses and samples.

Interactive · stage by stage A results row makes each check's outcome visible Read the ten result rows as expectations, observations, statuses and samples.
A results row makes each check's outcome visible, step 3 of 3. Showing Original suite, Clean email gap, Broken outcomes. In focus: Broken outcomes.Original suiteTen checksClean email gapOne of 40 missingBroken outcomesSeven critical; two warn

The drawing shows the whole scene; its 3 steps are listed below.

Every step as text
  1. The original suite evaluates ten named expectations on both the clean and broken files.
  2. One email is missing in the clean file. Its 2.5 percent share is within the stated soft threshold, so the result records a pass with a nonzero observation.
  3. The original broken-file results contain seven critical failures and two warnings. Samples identify the affected keys and values rather than dumping whole personal rows.

Checkpoint

Check: A results row makes each check's outcome visible

  1. Question 1 of 2 Can a passing threshold check report a nonzero observed gap?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Yes, when the observation is within its declared allowance

    The original email-share threshold accepts one missing email out of 40. The result's status follows the threshold, not merely whether an observation is nonzero.

  2. Question 2 of 2 One of 40 emails is missing. What percentage is missing?

    Type a number.

    Show the answer: Question 2 of 2

    Answer: 2.5 percent

    One divided by 40 times 100 gives 2.5 percent.

Severity controls whether the load continues

Set the continuation policy before a failure appears. The same unexpected city can be a plausible expansion in one feed and a forbidden code in another. Record who should confirm the observation and what is allowed while that confirmation is pending. A warning should retain enough context to investigate, while a critical outcome should identify the boundary that must remain unpublished. Repeated failures may reveal a source problem rather than automatically proving that the threshold is wrong.

A check’s severity policy: The declared action after a check fails, such as blocking a load or recording a warning for confirmation. says what a failure does. A critical failure stops the load: a repeated order id can overcount revenue in a sum, and an order without a customer cannot be assigned to a known customer without a declared policy. A warning is logged and kept, and the load goes on: a new city may be a real expansion, and the declared missing-email allowance does not change this suite’s revenue total, though it can affect contactability and email-based analyses. This is what a scheduled job does with the broken file’s results:

A new export goes through the checks; results are saved to a history; a critical failure stops the load, otherwise warnings are logged.New export arrivesorders.csvRun the checksone results row per checkResults historyevery run, passes includedDid a critical check fail?Stop the loadthe last good output staysLog the warningsLoad and publishsavedyesno

Where the checks sit in a load

Text description of the diagram

A new orders export arrives and the checks run on it, each check giving one row of a results table.

  • The results table is saved to a history on every run, with the checks that passed as well as those that failed.
  • If a critical check failed, the load stops and the last good output stays in place, so no report is built from the broken export.
  • If no critical check failed, the warnings are logged and the export is loaded and published.
Save the results, warn on soft failures, stop on critical ones Python · gate.py
"""What a scheduled job does with the results: save them, warn on soft failures, stop on critical ones."""
import sys

import pandas as pd

from orders_suite import NOW, load, run_checks

customers = pd.read_csv("customers.csv", dtype={"customer_id": "str"})
results = run_checks(load("orders_broken.csv"), customers)
results.insert(0, "run_at", NOW)
results.to_csv("dq_results.csv", index=False)  # kept with every run, passes included

for row in results[results["status"] == "warn"].itertuples():
    print(f"warning: {row.check}: {row.failing} failing ({row.sample})")
failed = results[results["status"] == "FAIL"]
if len(failed):
    lines = [f"  {row.check}: {row.failing} failing ({row.sample})" for row in failed.itertuples()]
    sys.exit("\n".join([f"stopped before loading: {len(failed)} critical checks failed", *lines]))
print("all critical checks passed: loading the orders")

Output (exit status 1)

warning: city allowed: 1 failing (Gurugram)
warning: email missing <= 5%: 4 failing (17.4% missing)

Printed as an error (standard error)

stopped before loading: 7 critical checks failed
  order_id unique: 1 failing (O-24017)
  customer_id not missing: 1 failing (O-24005)
  customer_id known: 2 failing (C1099, C1100)
  status allowed: 2 failing (Delivered, dispatched)
  quantity 1 to 50: 2 failing (0, 120)
  amount = quantity x price: 1 failing (O-24018)
  newest order_ts <= 26 h old: 1 failing (26.8 h old)

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 gate.py

The job saves the results table first, passes included, so the history has this run whatever happens next. It prints the two warnings and then calls sys.exit() with a message, which Python prints to standard error before ending with exit status 1. Shells and schedulers read a non-zero exit status as a failed run, so the load stops, nobody builds a report from the broken export, and yesterday’s tables stay as they were. Choose each severity from the damage a failure would do, not from how often the check fails: a check that stops ten loads a week is usually a check with the wrong threshold or the wrong severity, and people soon stop reading its messages.

Retain the original two warnings and exit 1 transcript while examining the seven critical failures.

Interactive · stage by stage Severity controls whether the load continues Retain the original two warnings and exit1 transcript while examining the seven critical failures.
Severity controls whether the load continues, step 3 of 3. Showing Save all results, Warnings, Critical stop. In focus: Critical stop.Save all resultsPasses includedWarningsTwo original warningsCritical stopSeven failed checks

The drawing shows the whole scene; its 3 steps are listed below.

Every step as text
  1. The original gate saves the result table before making its load decision, retaining both passing and failing rows.
  2. The original city and email-share failures are logged as warnings under this suite's declared policy.
  3. Seven critical outcomes cause the original job to exit1 before loading. That expected failure is retained as a teaching transcript.

Checkpoint

Check: Severity controls whether the load continues

  1. Question 1 of 2 What determines a check's severity?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The damage and policy for its failure

    Severity should follow the consequences of proceeding with that broken expectation. Frequency can indicate a bad threshold but does not define the consequence by itself.

  2. Question 2 of 2 The original gate reports two warnings and seven critical failures. How many failing check outcomes is that?

    Type a number.

    Show the answer: Question 2 of 2

    Answer: 9 outcomes

    Two warning outcomes plus seven critical failures gives nine.

Compare aware timestamps before grouping local dates

A timeliness check compares the newest timestamp with the time of the run. Both must carry a time zone, or the comparison silently shifts by the difference between them. The orders store their times in UTC, and the job runs at 09:30 in Indian Standard Time, five and a half hours ahead:

One feed's age, computed three ways Python · freshness_ist.py
"""How old is the newest order? Stored in UTC, judged at a run time in Indian Standard Time (UTC+05:30)."""
import pandas as pd

orders = pd.read_csv("orders_broken.csv")
ts = pd.to_datetime(orders["order_ts"], utc=True)
now = pd.Timestamp("2026-03-16 09:30", tz="Asia/Kolkata")  # the run time, fixed for the example
newest = ts.max()
print("newest order:", newest, "=", newest.tz_convert("Asia/Kolkata"))
print("run time:    ", now.tz_convert("UTC"), "=", now)

hours = pd.Timedelta(hours=1)
print(f"age, both aware:              {(now - newest) / hours:5.1f} h  (over the 26 h limit: stale)")
# Two ways to lose the time zone. The server's clock in UTC against order times turned into IST wall-clock time:
utc_clock = now.tz_convert("UTC").tz_localize(None)
ist_orders = newest.tz_convert("Asia/Kolkata").tz_localize(None)
print(f"age, UTC clock - IST times:   {(utc_clock - ist_orders) / hours:5.1f} h  (5.5 h too young: the stale feed passes)")
# An IST wall clock against order times left in UTC:
ist_clock = now.tz_localize(None)
utc_orders = newest.tz_localize(None)
print(f"age, IST clock - UTC times:   {(ist_clock - utc_orders) / hours:5.1f} h  (5.5 h too old: fresh feeds fail)")

clean = pd.to_datetime(pd.read_csv("orders_clean.csv")["order_ts"], utc=True)
per_day = pd.DataFrame({
    "UTC date": clean.dt.date.value_counts().sort_index(),
    "IST date": clean.dt.tz_convert("Asia/Kolkata").dt.date.value_counts().sort_index(),
}).fillna(0).astype(int)
per_day.index.name = None
print("\norders per day in the clean file:")
print(per_day.to_string())

Output

newest order: 2026-03-15 01:12:00+00:00 = 2026-03-15 06:42:00+05:30
run time:     2026-03-16 04:00:00+00:00 = 2026-03-16 09:30:00+05:30
age, both aware:               26.8 h  (over the 26 h limit: stale)
age, UTC clock - IST times:    21.3 h  (5.5 h too young: the stale feed passes)
age, IST clock - UTC times:    32.3 h  (5.5 h too old: fresh feeds fail)

orders per day in the clean file:
            UTC date  IST date
2026-03-14        21        15
2026-03-15        19        25

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 freshness_ist.py

With both times aware, the newest order is 26.8 hours old and the feed is stale. Turning the order times into IST wall-clock times and subtracting them from a server clock in UTC makes the same feed 5.5 hours younger, and it passes. Doing the opposite makes every feed 5.5 hours older, so a feed that is 21 hours old looks 26.5 hours old and fails. The rule that avoids both: parse stored times with pd.to_datetime(..., utc=True), take the run time with pd.Timestamp.now(tz="UTC"), and convert to Asia/Kolkata only to display a time or to group rows by Indian dates.

The last table shows why grouping matters. The clean file’s 40 orders split 21 and 19 over its two days by UTC date, and 15 and 25 by IST date, because the orders placed between 18:30 and midnight UTC already belong to the next day in India. A check that compares each day’s order count with a normal range must count days in the calendar its report uses.

Use the original aware 26.8-hour age before comparing its 21.3 and 32.3-hour mistakes.

Interactive · stage by stage Compare aware timestamps before grouping local dates Use the original aware26.8-hour age before comparing its21.3 and 32.3-hour mistakes.
Compare aware timestamps before grouping local dates, step 3 of 3. Showing UTC timestamp, Aware run time, Wrong wall clocks. In focus: Wrong wall clocks.UTC timestampNewest instant in UTCAware run timeNext day 09:30 ISTWrong wall clocks21.3 or32.3 hours

The drawing shows the whole scene; its 3 steps are listed below.

Every step as text
  1. The original newest order is 01:12 UTC, equal to 06:42 in Indian Standard Time.
  2. The original run is the next day at 09:30 IST. Subtracting aware instants gives 26.8 hours.
  3. Removing mismatched zone information changes the apparent age by 5.5 hours in the original examples. Local date grouping also changes daily membership.

Checkpoint

Check: Compare aware timestamps before grouping local dates

  1. Question 1 of 2 What should a freshness subtraction compare?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Aware timestamps representing the actual instants

    Aware timestamps account for each instant's offset. Unlabelled wall-clock values can shift the age by the omitted offset.

  2. Question 2 of 2 The original aware age is 26.8 hours and a wrong calculation gives 21.3. What offset was lost?

    Type a number.

    Show the answer: Question 2 of 2

    Answer: 5.5 hours (anything from 5.499999999 to 5.500000001 counts)

    26.8 minus 21.3 equals 5.5 hours.

Recent baselines and fixed limits catch different changes

The gate saved its results table before deciding anything. Kept over many runs, those tables answer questions that no single run can: what is normal for this feed, when a check started failing, and whether a problem is a break or a drift. This history holds two numbers per run for the full feed, about 400 orders a day:

Twelve runs: a sudden drop and a slow drift Python · history.py
"""Results kept from every run make slow problems visible: two metrics of the full orders feed over twelve runs."""
import pandas as pd

history = pd.read_csv("dq_history.csv")
# The baseline of each run is the median of the seven runs before it.
baseline = history[["orders", "email_missing_pct"]].shift(1).rolling(7).median()
history["orders_vs_baseline"] = (history["orders"] / baseline["orders"]).round(2)
history["email_rise_pts"] = (history["email_missing_pct"] - baseline["email_missing_pct"]).round(1)

drop = history["orders_vs_baseline"] < 0.7  # a sudden fall in volume
jump = history["email_rise_pts"] >= 2.0  # a sudden rise in missing emails
over = history["email_missing_pct"] > 5.0  # the fixed limit of the suite
history["flags"] = (
    drop.map({True: "volume drop ", False: ""}) + jump.map({True: "email jump ", False: ""}) + over.map({True: "email over 5 %", False: ""})
).str.strip()
print(history.tail(5).to_string(index=False))

Output

  run_date  orders  email_missing_pct  orders_vs_baseline  email_rise_pts          flags
2026-03-12     419                3.4                1.04             0.4               
2026-03-13     212                3.6                0.52             0.5    volume drop
2026-03-14     398                4.1                0.98             0.9               
2026-03-15     410                4.7                1.03             1.5               
2026-03-16     406                5.3                1.02             1.9 email over 5 %

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 history.py

Each run is compared with the median of the seven runs before it. On one run the orders fall to 212, half the usual number, and the rule flags the drop at once. The missing emails behave differently: they rise from 3.4 % to 5.3 % over the last five runs, about half a point a day, and no single run is 2 points above its baseline, because the baseline climbs with the data. Only the suite’s fixed limit of 5 % notices, on the last run. Use both kinds of rule: a comparison with recent runs for sudden breaks, and a fixed limit, agreed with the people who use the data, for drifts. A chart of the history can help a reader inspect drift before a configured threshold fires; whether it is apparent depends on the scale, noise and pattern.

Keep the original 12-run history while comparing its volume break and missingness drift.

Interactive · stage by stage Recent baselines and fixed limits catch different changes Keep the original 12-run history while comparing its volume break and missingness drift.
Recent baselines and fixed limits catch different changes, step 3 of 3. Showing Recorded volume, Recorded email drift, Fixed threshold. In focus: Fixed threshold.Recorded volume212 ordersRecorded email drift3.4 to 5.3 percentFixed thresholdOver5 percent

The drawing shows the whole scene; its 3 steps are listed below.

Every step as text
  1. The original volume drops to 212 on one run, and the recent-baseline rule flags that change.
  2. Email missingness rises across the original last five runs while the rolling baseline moves with it.
  3. The original fixed 5-percent limit flags the last run. This is evidence for that history, not a guarantee that every slow drift evades an adaptive rule.

Checkpoint

Check: Recent baselines and fixed limits catch different changes

  1. Question 1 of 2 Why keep both adaptive and fixed expectations?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: They can respond differently to abrupt changes and drift

    The original history shows a volume break and a moving missingness baseline. The response depends on data, windows and thresholds; it is not universal.

  2. Question 2 of 2 The original missing share rises from 3.4 to 5.3 percent. How many percentage points is that?

    Type a number.

    Show the answer: Question 2 of 2

    Answer: 1.9 percentage points (anything from 1.899999999 to 1.900000001 counts)

    5.3 minus 3.4 gives 1.9 points.

Validate inputs, outputs and stored-data constraints

A check’s location defines which writers and transformations it protects. An incoming-file check can retain source context before a transformation changes the representation. An output check assesses the consumer’s actual contract. A database constraint applies to supported writes to the table, including writes that bypass the pipeline. When a rule is repeated across boundaries, keep its definition aligned and test the same relevant cases instead of assuming that similar names make the implementations equivalent.

Run checks at the edges of a pipeline: on raw data as it arrives, before anything is built from it, and on outputs before anyone sees them. Measure vectorised checks alongside loading for the relevant workload and job budget. Some rules can also live in the database: DuckDB tables accept NOT NULL, UNIQUE, PRIMARY KEY, FOREIGN KEY and CHECK constraints, which reject a bad row when it is inserted. Constraints suit rules that must never be broken, such as a unique key; they cannot express a soft limit such as “at most 5 % of emails missing”. The next lesson compares constraints with schema validation libraries such as pandera.

CSV Validator Test a CSV file against rules such as required fields and allowed ranges before it reaches your pipeline.

Separate incoming, produced and stored-data contracts before choosing where each rule lives.

Interactive · stage by stage Validate inputs, outputs and stored-data constraints Separate incoming, produced and stored-data contracts before choosing where each rule lives.
Validate inputs, outputs and stored-data constraints, step 3 of 3. Showing Incoming file, Produced output, Stored table. In focus: Stored table.Incoming fileBefore transformationProduced outputBefore publicationStored tableConstraints still apply

The drawing shows the whole scene; its 3 steps are listed below.

Every step as text
  1. Input checks identify which source expectation failed before transformations can hide its origin.
  2. Output checks establish the required result contract after the pipeline's transformations.
  3. Database constraints can protect stored rows from writers that bypass the pipeline. Schemas and whole-table checks have overlapping expressive capabilities.

Checkpoint

Check: Validate inputs, outputs and stored-data constraints

  1. Question 1 of 2 Why can a rule live in more than one layer?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Different writers or boundaries can bypass one layer

    A stored-data guard can still apply when pipeline validation is skipped. The implementations need an aligned rule definition and actual checks.

  2. Question 2 of 2 A job validates one raw boundary and one output boundary. How many boundary checks is that?

    Type a number.

    Show the answer: Question 2 of 2

    Answer: 2 boundaries

    The two named boundaries are raw input and produced output.

The exercise counts failing rows and samples distinct examples

The return shape is part of the original contract. Count every violating occurrence, then choose a bounded sample of distinct nonmissing values under the exercise’s sorting policy. An empty frame, a duplicated key and a missing value test different branches of that contract. Freshness compares aware instants with the inclusive maximum-age boundary. Keep the original tests and inputs while implementing the functions so that a completed assertion evaluates the intended rule. A repeated key can contribute several violating rows while appearing only once in the short sample. Treat those two outputs as different quantities.

Exercise · Medium · Python

Write six data-quality checks for an orders export

An orders export arrives as rows read from a CSV file: each row is a dictionary, and every value is text. A value is missing when it is empty or holds only spaces. Write six checks in order_checks.py. Each returns a dictionary with the check's name, the number of failing rows, and a sample: up to 3 values, sorted, so that someone can find the problem. The helpers missing(value) and result(check, failing, sample) are written for you.

  1. check_unique(rows, column): a row fails when its value already appeared in an earlier row. The sample holds the repeated values, each once. Missing values are skipped.
  2. check_not_missing(rows, column, key): a row fails when column is missing. The sample holds the key values of those rows, such as their order ids.
  3. check_allowed(rows, column, allowed): a row fails when its value is not missing and not in allowed. The comparison is exact: "Delivered" and "placed " are not "delivered" and "placed".
  4. check_between(rows, column, low, high): a row fails when its value is not missing and is either not a number or outside low to high, both ends included. The sample holds the values as they were written.
  5. check_known(rows, column, keys): a row fails when its value is not missing and not in keys, the keys of another table (a foreign-key check).
  6. check_fresh(rows, column, now, max_hours): the column holds ISO 8601 timestamps with a time zone, such as 2026-03-15T01:12:00Z or 2026-03-16T08:55:00+05:30, and now is a time-zone-aware datetime. Return failing 1 when the newest timestamp is more than max_hours hours before now, or when there is no timestamp at all, and 0 otherwise. The sample is the age of the newest row in hours, rounded to one decimal place, or empty when there is none.

Every check except the last ignores missing values, so that one problem is reported once. The sample tests run your checks on four small datasets: a clean one, one with repeated keys and gaps, one with invalid values, and a stale feed.

Starter code · order_checks.py

from datetime import datetime


def missing(value):
    """A cell read from a CSV is missing when it is empty or holds only spaces."""
    return value is None or str(value).strip() == ""


def result(check, failing, sample):
    """One row of the results table: the check's name, how many rows fail it, and up to 3 sorted examples."""
    return {"check": check, "failing": failing, "sample": sorted(sample)[:3]}


def check_unique(rows, column):
    # Count the rows whose value appeared in an earlier row; skip missing values.
    return result(f"{column} unique", 0, [])


def check_not_missing(rows, column, key):
    # Count the rows where `column` is missing; the sample holds their `key` values.
    return result(f"{column} not missing", 0, [])


def check_allowed(rows, column, allowed):
    # Count the values that are not missing and not in `allowed`.
    return result(f"{column} allowed", 0, [])


def check_between(rows, column, low, high):
    # Count the values that are not missing and are not numbers from low to high.
    return result(f"{column} {low} to {high}", 0, [])


def check_known(rows, column, keys):
    # Count the values that are not missing and not in `keys`.
    return result(f"{column} known", 0, [])


def check_fresh(rows, column, now, max_hours):
    # Fail (1) when the newest timestamp is more than max_hours before `now`, or when there is none.
    return {"check": f"newest {column} <= {max_hours} h old", "failing": 0, "sample": []}
The sample tests · test_order_checks.py
from datetime import datetime, timedelta, timezone

from order_checks import check_allowed, check_between, check_fresh, check_known, check_not_missing, check_unique

IST = timezone(timedelta(hours=5, minutes=30))
NOW = datetime(2026, 3, 16, 9, 30, tzinfo=IST)
STATUSES = {"placed", "shipped", "delivered", "cancelled", "returned"}
CUSTOMERS = {"C1001", "C1002", "C1003", "C1004"}


def order(order_id, customer_id, status, quantity, order_ts):
    return {"order_id": order_id, "customer_id": customer_id, "status": status, "quantity": quantity, "order_ts": order_ts}


CLEAN = [
    order("O-1", "C1001", "placed", "1", "2026-03-15T20:15:00Z"),
    order("O-2", "C1002", "shipped", "50", "2026-03-16T01:40:00Z"),
    order("O-3", "C1003", "delivered", "3", "2026-03-16T02:05:00+00:00"),
    order("O-4", "C1001", "returned", "2", "2026-03-16T08:55:00+05:30"),
]
DUPLICATES_AND_GAPS = [
    order("O-7", "C1001", "placed", "1", "2026-03-16T01:00:00Z"),
    order("O-7", "C1001", "placed", "1", "2026-03-16T01:00:00Z"),
    order("O-8", "", "shipped", "2", "2026-03-16T01:10:00Z"),
    order("O-7", "C1002", "placed", "1", "2026-03-16T01:20:00Z"),
    order("O-9", "   ", "placed", "4", "2026-03-16T01:30:00Z"),
    order("O-6", "C1009", "placed", "1", "2026-03-16T01:40:00Z"),
    order("O-5", "C1009", "placed", "1", "2026-03-16T01:50:00Z"),
    order("", "C1003", "placed", "1", "2026-03-16T02:00:00Z"),
    order("", "C1004", "placed", "1", "2026-03-16T02:10:00Z"),
    order("O-6", "C1004", "placed", "1", "2026-03-16T02:20:00Z"),
]
BAD_VALUES = [
    order("O-11", "C1001", "Delivered", "0", "2026-03-16T01:00:00Z"),
    order("O-12", "C1002", "dispatched", "120", "2026-03-16T01:00:00Z"),
    order("O-13", "C1003", "delivered", "two", "2026-03-16T01:00:00Z"),
    order("O-14", "C1004", "", "", "2026-03-16T01:00:00Z"),
    order("O-15", "C1001", "placed ", "-1", "2026-03-16T01:00:00Z"),
    order("O-16", "C1002", "Delivered", "50.5", "2026-03-16T01:00:00Z"),
]
STALE = [
    order("O-21", "C1001", "placed", "1", "2026-03-14T23:00:00Z"),
    order("O-22", "C1002", "placed", "1", "2026-03-15T01:12:00Z"),
    order("O-23", "C1003", "placed", "1", ""),
]


def test_clean_file_passes():
    """every check passes on a clean file"""
    assert check_unique(CLEAN, "order_id") == {"check": "order_id unique", "failing": 0, "sample": []}
    assert check_not_missing(CLEAN, "customer_id", "order_id") == {"check": "customer_id not missing", "failing": 0, "sample": []}
    assert check_allowed(CLEAN, "status", STATUSES) == {"check": "status allowed", "failing": 0, "sample": []}
    assert check_between(CLEAN, "quantity", 1, 50) == {"check": "quantity 1 to 50", "failing": 0, "sample": []}
    assert check_known(CLEAN, "customer_id", CUSTOMERS) == {"check": "customer_id known", "failing": 0, "sample": []}
    assert check_fresh(CLEAN, "order_ts", NOW, 26) == {"check": "newest order_ts <= 26 h old", "failing": 0, "sample": [0.6]}


def test_repeated_keys():
    """counts every repeat after the first, ignores missing keys, samples sorted distinct values"""
    assert check_unique(DUPLICATES_AND_GAPS, "order_id") == {"check": "order_id unique", "failing": 3, "sample": ["O-6", "O-7"]}


def test_missing_values():
    """treats empty and blank cells as missing and samples the keys of those rows"""
    assert check_not_missing(DUPLICATES_AND_GAPS, "customer_id", "order_id") == {"check": "customer_id not missing", "failing": 2, "sample": ["O-8", "O-9"]}
    assert check_not_missing(DUPLICATES_AND_GAPS, "order_id", "customer_id")["failing"] == 2


def test_unknown_customers():
    """counts rows whose customer is not in the customer table, but not the missing ones"""
    assert check_known(DUPLICATES_AND_GAPS, "customer_id", CUSTOMERS) == {"check": "customer_id known", "failing": 2, "sample": ["C1009"]}


def test_values_outside_the_allowed_set():
    """is case-sensitive, ignores missing values, and does not strip spaces"""
    assert check_allowed(BAD_VALUES, "status", STATUSES) == {"check": "status allowed", "failing": 4, "sample": ["Delivered", "dispatched", "placed "]}


def test_values_outside_the_range():
    """keeps both ends inside the range and counts text that is not a number"""
    assert check_between(BAD_VALUES, "quantity", 1, 50) == {"check": "quantity 1 to 50", "failing": 5, "sample": ["-1", "0", "120"]}
    edges = [order("O-31", "C1001", "placed", "1", ""), order("O-32", "C1001", "placed", "50", "")]
    assert check_between(edges, "quantity", 1, 50)["failing"] == 0


def test_stale_feed():
    """compares time-zone-aware times, ignores missing timestamps, and fails when there are none"""
    assert check_fresh(STALE, "order_ts", NOW, 26) == {"check": "newest order_ts <= 26 h old", "failing": 1, "sample": [26.8]}
    assert check_fresh(STALE, "order_ts", NOW, 27) == {"check": "newest order_ts <= 27 h old", "failing": 0, "sample": [26.8]}
    exactly = [order("O-41", "C1001", "placed", "1", "2026-03-15T02:00:00Z")]
    assert check_fresh(exactly, "order_ts", NOW, 26)["failing"] == 0
    assert check_fresh(STALE[2:], "order_ts", NOW, 26) == {"check": "newest order_ts <= 26 h old", "failing": 1, "sample": []}
A hint

Use missing(value) first in every loop and continue when it is true. For check_unique, keep a set of the values seen so far and a set of the repeated ones. For check_between, convert with float(value) inside try and treat a ValueError as a failing row. For check_fresh, datetime.fromisoformat() reads both Z and +05:30, and (now - newest).total_seconds() / 3600 gives the age in hours.

The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Keep the original six functions and seven tests while separating failing rows from sample values.

Interactive · stage by stage The exercise counts failing rows and samples distinct examples Keep the original six functions and seven tests while separating failing rows from sample values.
The exercise counts failing rows and samples distinct examples, step 3 of 3. Showing Original functions, Repeats and sample, Original tests. In focus: Original tests.Original functionsSix check functionsRepeats and sampleThree repeats; two keysOriginal testsSeven test functions

The drawing shows the whole scene; its 3 steps are listed below.

Every step as text
  1. The original exercise asks for uniqueness, missingness, allowed/range/known values and freshness using the given interfaces.
  2. The original duplicate fixture has three repeat occurrences but only two distinct repeated key values in its sample.
  3. All seven original tests cover clean data, gaps, bad values and aware freshness boundaries. A meaningful negative must reach those assertions rather than crash the harness.

Checkpoint

Check: The exercise counts failing rows and samples distinct examples

  1. Question 1 of 2 What does the uniqueness failing count measure here?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Repeat occurrences after the first valid key

    Every later occurrence contributes a failing row under the original contract. The sample is deduplicated, but the failing-row count is not.

  2. Question 2 of 2 The original duplicate fixture has three repeat occurrences. How many rows fail that check?

    Type a number.

    Show the answer: Question 2 of 2

    Answer: 3 rows

    Each repeated occurrence after the first contributes one failing row.

Practise the original quality contracts

Practice that adapts to you

Practise this lesson

13 questions about 8 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 13 A suite names six dimensions and currently covers four. How many are not yet covered?

    Type a number.

    Show the answer: Question 1 of 13

    Answer: 2 dimensions

    Two of the six named dimensions remain uncovered.

  2. Question 2 of 13 A file has 40 rows and two repeat occurrences after the first keys. How many distinct keys remain?

    Type a number.

    Show the answer: Question 2 of 13

    Answer: 38 keys

    With no other repeats or missing keys,40 minus two gives 38 distinct keys.

  3. Question 3 of 13 Three of 100 emails are missing. What share is missing?

    Type a number.

    Show the answer: Question 3 of 13

    Answer: 3 percent

    Three of 100 gives 3 percent.

  4. Question 4 of 13 A suite reports three warnings and four critical failures. How many failing outcomes is that?

    Type a number.

    Show the answer: Question 4 of 13

    Answer: 7 outcomes

    Three plus four gives seven failing outcomes.

  5. Question 5 of 13 A wrong calculation changes32.3 hours to 26.8. What is the difference?

    Type a number.

    Show the answer: Question 5 of 13

    Answer: 5.5 hours (anything from 5.499999999 to 5.500000001 counts)

    The difference is 5.5 hours.

  6. Question 6 of 13 Missingness rises from 2.0 to 4.2 percent. What is the rise in percentage points?

    Type a number.

    Show the answer: Question 6 of 13

    Answer: 2.2 percentage points (anything from 2.199999999 to 2.200000001 counts)

    The difference is 4.2 minus 2.0, or2.2 points.

  7. Question 7 of 13 A pipeline checks raw, intermediate and final tables. How many checked boundaries is that?

    Type a number.

    Show the answer: Question 7 of 13

    Answer: 3 boundaries

    Three named boundaries are checked.

  8. Question 8 of 13 A key appears four times. How many repeat rows are after its first occurrence?

    Type a number.

    Show the answer: Question 8 of 13

    Answer: 3 rows

    Four occurrences include one first row and three repeats.

  9. Question 9 of 13 A check counts the orders whose customer_id does not appear in the customers table. Which dimension does it measure?

    Choose one answer.

    Show the answer: Question 9 of 13

    Answer: Consistency, because two tables that describe the same customers must agree

    The orders and the customers table describe the same entities, so an order that points to a customer the customers table does not know is a contradiction between them: a consistency problem. Completeness would be a missing customer_id; here the value is present but has no partner.

  10. Question 10 of 13 DuckDB SUMMARIZE reports approx_unique 23 and count 23 for order_id. What may you conclude?

    Choose one answer.

    Show the answer: Question 10 of 13

    Answer: Nothing certain about uniqueness, because approx_unique is an estimate; a duplicate check counts exactly

    approx_unique is an approximate count of distinct values. In this lesson's broken file it says 23 for 23 rows, yet one order id appears twice. A profile tells you where to look; only a check states the expectation and counts the rows that break it.

  11. Question 11 of 13 The newest order was placed at 06:42 IST, and the check runs at 09:30 IST on the following day. How many hours old is the feed? Give one decimal place.

    Type a number.

    Show the answer: Question 11 of 13

    Answer: 26.8 hours (anything from 26.75 to 26.85 counts)

    From 06:42 to 06:42 the next day is 24 hours, and from 06:42 to 09:30 is another 2 hours 48 minutes, so 26.8 hours. With a limit of 26 hours the feed is stale. Losing the time zone on one side of the subtraction would move the answer by 5.5 hours, to 21.3 or 32.3.

  12. Question 12 of 13 Under this suite’s stated policy, an unexpected served city may be a real expansion that someone should confirm. What severity does that policy assign?

    Choose one answer.

    Show the answer: Question 12 of 13

    Answer: A warning, because a new city may be a real expansion that someone should confirm

    This stated policy records a warning and asks for confirmation. A different contract may require rejection; severity follows the relevant damage and proceeding policy.

  13. Question 13 of 13 In the original recorded history, email missingness rises from 3.4 to 5.3 percent and its largest rise over the preceding-seven-run median is 1.9 points. A rule warns above two points. What does that adaptive rule do on these recorded runs?

    Choose one answer.

    Show the answer: Question 13 of 13

    Answer: It does not fire on those recorded runs; the fixed five-percent limit catches the final run

    The original recorded maximum baseline rise is 1.9 points, below the stated two-point trigger. This observation does not establish that every gradual drift evades every adaptive rule.

Interview questions

Fresher · What is the difference between profiling a dataset and checking it?

Profiling describes what the data looks like: column types, minimums and maximums, distinct counts, quartiles and the share of missing values, as pandas’ describe() or DuckDB’s SUMMARIZE report them. Checking compares the data with an expectation and fails when the expectation is broken: order ids are unique, quantities are from 1 to 50, the newest row is at most 26 hours old under this suite’s inclusive boundary. A profile cannot fail, because it does not know the rules, and some of its numbers are estimates: DuckDB’s distinct counts are approximate. You profile a new dataset to learn what to expect, write the expectations down as checks, and then run the checks on every load. A profile answers “what is in this file?”; a check answers “may we use this file?”.

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Mid to senior

    Every data-quality check on yesterday's orders passed, yet finance says the day's revenue in your report is 18 % too low. How can both be true, and which checks were missing?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Senior to staff

    Your team runs 40 data-quality checks on a daily pipeline. They raise about 25 warnings a week, people have stopped reading them, and a real problem went unnoticed for nine days. How do you redesign the checks and their alerts?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • A check is an expectation written as code that passes or names the rows that break it; a profile only describes.
  • The six dimensions are completeness, uniqueness, validity, consistency, timeliness and accuracy. Accuracy needs a source outside the table.
  • Return one results row per check, with severity, status, failing count and a sample of keys, and save the table on every run, passes included.
  • Critical failures stop the load and keep the last good output; warnings are logged. Set severity by the damage a failure does.
  • Compare time-zone-aware times only: losing the zone on one side moves a feed’s age by the full offset, 5.5 hours for IST.
  • Keep a history: compare each run with recent runs to catch breaks, and with fixed limits to catch slow drifts.

Terms in this lesson

check result row
A structured record of a check's name, observation, status, severity and relevant sample under its stated counting policy.
missingness policy
The rule for how absent values are counted, rejected, allowed or excluded from a particular calculation.
quality expectation
A stated rule about data for a defined use, including the observation and acceptance condition that a check evaluates.
severity policy
The declared action after a check fails, such as blocking a load or recording a warning for confirmation.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.