A profile tells you what the data looks like today; data-quality rules keep tomorrow's data honest. This playbook turns profiling output into deployable rules — format, dependency, completeness by record type — drafted by AI, tightened by you, and signed by the human who owns the data before anything runs in a pipeline. (T-138)
Tier: Core · Workflow area: Data Quality & Profiling · ~35 minutes
The gap between "we profiled it" and "we protect it" is where data quality programs stall: writing expectation suites by hand is tedious enough that coverage stays thin forever. AI closes the gap mechanically — profiling statistics are exactly the evidence a model needs to draft candidate rules, complete with thresholds. But a drafted rule is a proposal about how the business works, and only the data owner knows whether discount_pct <= 40 is an invariant or last quarter's coincidence. The workflow is therefore a pipeline with a mandatory human valve: profile → candidate rules → owner review → deploy → tune.
Start from real profiling output, not schema imagination. Reuse the pb-25 census — SUMMARIZE results, null rates, distinct counts, value patterns, orphan counts — or generate it now with DuckDB. Rules drafted from column names alone are guesses; rules drafted from measurements inherit evidence.
Ask for candidate rules in a strict, reviewable format.
Below: profiling output for the vendor_master and invoice tables
(census, null rates, patterns, orphan counts, per-record-type
completeness). Draft candidate data-quality rules.
For EACH rule emit exactly:
- rule_id, column(s)
- type: format / range / completeness / uniqueness / referential /
cross-field dependency / freshness / volume
- the check, as a Great Expectations expectation (name + kwargs)
- evidence: the profiling measurement justifying it, quoted
- proposed severity: BLOCK (quarantine batch) or WARN (log + notify)
- confidence: INVARIANT (business logic requires it) vs OBSERVED
(true in this profile, may be coincidence)
Cover at least: tax-ID and IFSC/IBAN format checks; completeness
conditional on record type (e.g., bank fields required only for
payment-relevant vendors); invoice→vendor referential integrity;
date ordering (invoice_date <= posting_date); amount sign vs
document type; volume bounds per load.
Mark every threshold you inferred from the profile as OBSERVED.
The INVARIANT/OBSERVED split is the load-bearing feature: it pre-sorts the owner review, and it is where models are honest if asked and silently overconfident if not.
Run the owner review as a working session, not an email. Walk the data owner through the OBSERVED rules; each one gets promoted (it is a real invariant), adjusted (right idea, wrong threshold), or rejected (coincidence). Real examples of each from a typical session: country IN ('IN','AE','SG') rejected (expansion planned), payment_terms <= 90 adjusted to 120 (one negotiated exception class), bank_account NOT NULL WHERE vendor_type='PAYABLE' promoted to BLOCK. Record the verdicts in the rule file itself — the review trail is part of the artifact.
Encode the survivors as a Great Expectations suite.
import great_expectations as gx
context = gx.get_context()
batch = context.data_sources.add_pandas("vendor_loads") \
.read_dataframe(df)
# R-07: conditional completeness (owner-promoted, BLOCK)
batch.expect_column_values_to_not_be_null(
"bank_account",
row_condition='vendor_type == "PAYABLE"',
condition_parser="pandas")
# R-11: format, GSTIN (INVARIANT, BLOCK)
batch.expect_column_values_to_match_regex(
"gstin",
r"^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z][1-9A-Z]Z[0-9A-Z]$")
# R-15: volume bound (OBSERVED->adjusted, WARN)
batch.expect_table_row_count_to_be_between(
min_value=800, max_value=15000)
Keep rule IDs from the review in comments or expectation meta — six months later, "why does this rule exist" must be answerable by grep.
Calibrate against history before enforcement. Run the suite over the last 60–90 days of loads in WARN-only mode. Two numbers per rule: how often it would have fired, and how many firings were true defects (spot-check a sample). A rule firing on 30% of historical clean loads has a wrong threshold; a BLOCK rule that would have quarantined a legitimate month-end volume spike gets a widened bound or a WARN downgrade. This backtest is what separates a quality gate from a pipeline-outage generator — do not skip it because the rules "look reasonable."
Deploy with routing, and schedule the tuning loop. BLOCK failures quarantine the batch and page the pipeline owner; WARN failures land in a triaged channel with a weekly review. Then close the loop quarterly: re-profile, diff against the rule set, and ask AI to propose additions for new columns and drift ("null rate on payment_terms moved 2%→11% — investigate or re-baseline?"). Rules are a living contract with the data, not a launch artifact.
not_null, accepted_values, relationships, custom generic tests for formats); keep the evidence/verdict trail in YAML comments.CHECK, foreign keys, unique indexes): a constraint is a rule that cannot be ignored. The suite then covers what constraints cannot express — conditional completeness, freshness, volume.Rule definitions leak business logic — discount ceilings, approval thresholds, vendor-payment patterns — and failure reports can quote raw defective rows. Classify the suite and its reports accordingly, and strip or mask row-level values in any alert that leaves the data team's boundary.
Profiling measures the past; rules defend the future — and AI makes drafting them nearly free, which moves the entire craft into the review: the INVARIANT/OBSERVED sort, the owner's verdicts, the historical backtest. A generated rule is a hypothesis about the business; only the data owner's signature turns it into a contract.