Skip to content

DS 102 · Module 2

Data Quality: Validation, Integration, and Traceability

Turn cleaned data into analysis data that can be validated, traced, and reproduced by someone else.

Start Section 1
0 / 5 completed
Sync my progressJoin a session to keep progress across devices and share it with your instructor

Info

After you join, your completed lesson numbers are sent to your instructor together with your learner code, so they can see your progress. Use the codes your instructor gave you, and do not enter your name, company secrets or other personal data. Progress is kept with the rest of that session's data.

Learning objectives

  • Use one-to-one, one-to-many, many-to-one joins; detect many-to-many explosions
  • Normalize units only with documented conversion rules; distinguish same-name columns with different definitions
  • Implement range, allowed-values, uniqueness, referential-integrity, and schema checks
  • Document meaning, provenance, transformations, assumptions, unresolved issues, and owner
  • Produce clean data, validation summary, data dictionary, and an auditable cleaning report

Merge / Join

35 min

Merge/Join and Data Relationships

Learning objective: use one-to-one, one-to-many, many-to-one joins; detect many-to-many explosions.

Before merging two tables, the most important question is not how to write the code, but what relationship the join is expected to have. Getting the relationship wrong still runs, but the row count and meaning of the result will be completely off.

Three common join relationships
RelationshipMeaningExample
1:1Both tables have only one row per join keyOne company matched to its single latest company-profile record
1:NOne row in one table matches several rows in anotherOne company's master row matched to its several months of monthly data
N:1Several rows matched to one row in the other table (the reverse view of 1:N)Several months of monthly data matched back to the same company's master row
Use the correct join key, and let validate= enforce the relationship
master = pd.read_csv("DS102_Company_Master.csv")

# site_id alone repeats across companies, so it must be paired with company_id to be a unique site
joined = site_long.merge(master, on="company_id", how="left", validate="m:1")
joined.shape[0] == site_long.shape[0]  # a correct 1:N/N:1 merge should not change the row count

Warning

The many-to-many trap: if both tables have repeated values on the same join key (for example, both have several rows with site_id = S01 that actually belong to different companies), joining them directly produces an unintended many-to-many join, exploding the row count and double-counting the original metrics.
What an exploded row count actually looks like (simulated scenario)

Suppose site_long has 48 rows, and master should have exactly one row per company_id. If master accidentally has the same company_id twice (say, the same company got loaded into the master file twice), the merge produces 96 rows instead of 48; every month of data is duplicated, and any later sum comes out to twice the correct answer.

  • Before merging, ask: should this be 1:1, 1:N, or N:1?
  • Use merge(..., validate="1:1" / "1:m" / "m:1") in code so pandas checks the relationship for you and errors when it does not hold
  • Always check the row count after merging and compare it against the expected count; not erroring is not enough
  • If the row count is much higher than expected, suspect a non-unique join key before using the result
Check yourself

Both tables have repeated values on the same join key, and they are joined directly on it anyway. What is the main risk of doing this?

Both tables have repeated values on the same join key, and they are joined directly on it anyway. What is the main risk of doing this?

Key takeaway: Confirm the relationship before merging, and always check the row count afterward; not erroring is not the same as being correct.

Units and definitions

30 min

Units, Definitions, and Comparability

Learning objective: normalize units only with documented conversion rules; distinguish same-name columns with different definitions.

The same column name in two datasets does not mean they can be merged or compared directly. Different units (tonnes vs. kilograms) and different definitions (revenue including vs. excluding tax) are the two most common, and most easily overlooked, problems.

Different units

  • E.g. one waste column in tonnes, another in kilograms
  • Values differ by 1000x while the column name looks identical
  • Can only be unified with a documented conversion rule

Different definitions

  • E.g. one employees column counts full-time only, another includes part-time
  • The numeric ranges may look similar, making it easy to overlook
  • Should not be merged directly without a confirmed, consistent definition
Unify units with a documented rule, and keep the original unit
# Suppose another source records waste in kilograms
waste_kg_source["waste_tonnes"] = waste_kg_source["waste_kg"] / 1000
waste_kg_source["original_unit"] = "kg"  # keep the original unit for later traceability

Warning

A shared column name only "looks" mergeable. Always check the data dictionary first to confirm the unit and definition scope are consistent, or you will mix two numbers that are not comparable with no error message to warn you.
What a unit mismatch actually looks like (simulated scenario)

One source records a site's waste as 12 (tonnes); another source records the same month as 12000 (kilograms). Converted, the two numbers actually agree, but if someone sees both columns named waste_amount and sums or compares them directly, they would mistake the second value for a 1000x outlier and draw the wrong conclusion.

  • Before combining sources, list out each source's definition and unit for the same-named column
  • Unit conversion should only use a documented, checkable conversion rule, never an assumption made by feel
  • Keep a column for the original unit or original definition, so it can be traced and checked later
  • If the two sources' definitions cannot be confirmed as consistent, present them separately rather than forcing a merge
Check yourself

Two sources both have a column called revenue, but one is revenue including tax and the other is revenue excluding tax. Which approach is most correct?

Two sources both have a column called revenue, but one is revenue including tax and the other is revenue excluding tax. Which approach is most correct?

Key takeaway: A shared column name does not mean it can be merged directly; both units and definitions must be checked before comparability holds.

Validation rules

40 min

Validation Rules and Data Contracts

Learning objective: implement range, allowed-values, uniqueness, referential-integrity, and schema checks.

Cleaned data with no explicit validation rules can quietly break the next time it is refreshed. Writing the expected rules as executable checks is a simple data contract: data that fails should be caught, not silently accepted.

Five basic validation rules
Rule typeWhat it checksExample
Range checkWhether a numeric value falls in a reasonable intervalemployees must be greater than or equal to 0
Allowed-values checkWhether a categorical value is in a valid listsector must be one of Manufacturing, Food & Beverage, Retail, Logistics
Uniqueness constraintWhether a column or column set repeatscompany_id + year must never repeat
Referential-integrity checkWhether a foreign key exists in another tablesite_monthly's company_id must exist in company_master
Schema checkWhether column names, count and types match expectationsColumn order, names and types must match the data dictionary
Write validation rules as repeatable checks
checks = {
    "unique_company_year_key": clean["company_year_key"].is_unique,
    "employees_non_negative": bool((clean["employees"] >= 0).all()),
    "sector_allowed": master["sector"].isin(["Manufacturing", "Food & Beverage", "Retail", "Logistics"]).all(),
    "site_company_id_exists": site_long["company_id"].isin(master["company_id"]).all()
}
checks

Info

A validation rule must be able to fail. If a rule is always True, it may not actually be checking anything; first run it against known-bad practice data to confirm it really catches the problem.
  • Test every rule once against known-bad practice data first, to confirm it really returns False
  • A rule's message should state which column and which condition failed, not just return True/False
  • Range checks should cover both the lower and upper bound, not just one side
  • An allowed-values list should stay aligned with the data dictionary's categories, and get updated whenever a category is added

Warning

Validation is not a one-time task. Whenever a source is refreshed, a definition changes, or new data is added, rerun the same checks so a broken data contract is caught early.
Check yourself

"The company_id column must never repeat" is which kind of data-validation rule?

"The company_id column must never repeat" is which kind of data-validation rule?

Key takeaway: Write expected rules as executable checks, so data that fails them gets caught, not silently accepted.

Lineage and cleaning log

30 min

Data Dictionary, Lineage, and Cleaning Log

Learning objective: document meaning, provenance, transformations, assumptions, unresolved issues, and owner.

A data dictionary explains what each column means; a cleaning log explains what was actually done to the data. Used together, they let someone else, or your future self, reproduce and check your judgment instead of just trusting the final numbers.

The minimum fields a cleaning log entry needs
FieldWhat to record
IssueWhat kind of data-quality problem was found
EvidenceHow it was found: which column, which row, what condition
DecisionHow it was ultimately handled: corrected, excluded, flagged, or left untouched
ReasonWhy this handling was chosen over other options
Residual uncertaintyWhat is still uncertain or needs further human review after this handling
The structure of one cleaning log entry
cleaning_log.append({
    "issue": "revenue_m_ntd stored as text with a thousands comma",
    "evidence": "column dtype was object before this step",
    "decision": "stripped commas and converted to float",
    "reason": "the underlying value is numeric; comma is formatting only",
    "residual_uncertainty": "assumes every comma is a thousands separator"
})

Info

Lineage answers: which file this number originally came from, which transformations it went through, and who made each decision. Without lineage, re-verifying or fixing a number later means redoing the whole thing from scratch.
The questions one lineage record should answer (simulated scenario)

A water-usage number on a report, if its lineage is complete, should be able to answer: which CSV file it originally came from, which unit-conversion step it passed through, and which cleaning log entry handled it. If any one of those questions cannot be answered, the number is not yet traceable.

Warning

Do not skip logging because "it will be cleaned again anyway." A decision that is not recorded usually cannot be explained to anyone else either, and cannot be reused the next time the data is refreshed.
Check yourself

What is a cleaning log's most important value to an analysis?

What is a cleaning log's most important value to an analysis?

Key takeaway: A data dictionary explains what a column means; a cleaning log explains what was actually done; together they let someone reproduce your judgment.

Independent Lab

45 min

Independent Lab: Build a Trustworthy Clean Dataset

Learning objective: independently complete the inspect, clean, reshape, integrate, validate, document loop, producing clean data, a validation summary, and an auditable cleaning report.

Info

Lab task: using the practice data, independently find and handle data-quality problems, confirm whether the candidate key is unique, write at least three validation checks, and complete a cleaning log. You will not be told how many problems there are, and no answer key is published.
  1. Load and inspect
    Load the practice data and check its shape, dtypes, missing values and duplicate rows.
  2. Clean and log
    Handle every problem you find, and for each one write an Issue, Evidence, Decision, Reason and Residual uncertainty in the cleaning log.
  3. Build the candidate key
    Build company_year_key and confirm there are no duplicates after cleaning.
  4. Write validation checks
    Write at least three validation checks: one uniqueness check, one range or allowed-values check, and at least one check of your own choosing.
  5. Assemble the outputs
    Produce the cleaned data, a validation summary, and the complete cleaning log.
  6. State a limitation
    Write one sentence about what this cleaned dataset still cannot answer.
  7. Log any AI assistance
    If AI was used, write down what it helped with and what you verified yourself.
  • A notebook that runs top to bottom from a fresh kernel
  • A cleaning log recording at least four handling decisions
  • At least three validation checks and their results
  • A validation summary (row count, uniqueness, number of missing values, etc.)
  • At least one limitation or unresolved issue
  • A record of AI assistance (if used)
  • Confirm company_year_key truly has no duplicates after cleaning
  • Confirm at least one validation rule has actually failed on deliberately broken data
  • Confirm every decision in the cleaning log includes a Reason, not just a Decision

Warning

The bar for done is not "the code ran without an error"; it is that every handling decision has a recorded reason, the validation checks genuinely catch problems, and the conclusions stay within what the data can support.

Done

If AI helps write cleaning or validation code, check every item yourself: the columns it uses exist, the handling matches the course's principles, and each validation rule really fails when the data has a problem. AI can help write it; verification and the final judgment stay a human responsibility.

Key takeaway: A trustworthy clean dataset needs a reproducible cleaning process and validation rules that can actually fail, not just clean-looking numbers.

What next?