🚧 Under construction — VoTechWorks is being built. A free tech program — complimentary, as our commitment to get America working.
voTechWorksGrade 13 · a vocational technology learning center, by urbanicity
newSchoolWorks.com › VoTechWorks › Technology & Design › Data Science & Analytics › Capstone with Real City/Urbanicity Data › Lesson 4
TEC-DATA85-11 · Lesson 4 · 12 clock hours · lab

Data Cleaning and Multi-Source Integration Pipeline

Objective. Students build a reproducible Python or R data-cleaning and joining pipeline that resolves the data-quality issues identified in EDA and produces a single analysis-ready table, fully documented in a changelog.

⚠ Pending professional review

This lesson, its worked examples and its practice questions has not yet been verified by an industry-certified professional or by a certification or approval body. It is a study draft and may contain errors. Do not use it as instructions for work on a job site or on live equipment. Always follow your instructor, your supervising professional, and current official references and regulations.

VoTechWorks program approval and accreditation are still in progress. Credentials are issued only by the certifying bodies named in each program, after their own exams.

Why a Reproducible Cleaning Pipeline Matters

Real-world city and urbanicity datasets are rarely clean. They arrive from multiple agencies — a county assessor, a transit authority, a census bureau — each with its own formatting conventions, update schedules, and quality standards. Your job as a data analyst is not just to fix the problems you can see; it is to fix them in a way that anyone else on your team can replicate exactly, starting from the same raw files, and arrive at the same result. That reproducibility is the difference between a professional deliverable and a one-time patch.

This lesson walks you through building a multi-source integration pipeline in Python (pandas / pyjanitor) or R (tidyverse). By the end, your raw data sources will be joined into a single analysis-ready table, every transformation will be recorded in a changelog, and three data-quality assertions will confirm the table is safe to analyze.

Understanding the Data-Quality Issues You Are Fixing

Before writing a single line of code, re-read your EDA notes. Common problems in city datasets include:

Document every issue you find before you fix it. That documentation becomes your changelog.

Building the Pipeline Step by Step

Step 1 — Load raw data. Read each source file with an explicit `dtype` argument where possible. Force FIPS codes to string immediately on load to preserve leading zeros: `dtype={'fips': str}`. Never overwrite raw files; always write outputs to a separate `clean/` directory.

Step 2 — Standardize address and identifier fields. Use `str.strip()`, `str.upper()`, and a consistent abbreviation map ("Street" → "ST") before any join. The pyjanitor function `clean_names()` normalizes column names to lowercase snake_case in one call. In R, `janitor::clean_names()` does the same.

Step 3 — Align FIPS codes. After loading, apply `str.zfill(5)` to any five-digit county FIPS column and `str.zfill(11)` to any eleven-digit census-tract column. Check that every value in the join key exists in a known-good reference list (e.g., the Census Bureau's FIPS reference file) before you merge.

Step 4 — Remove duplicates. Call `DataFrame.drop_duplicates(subset=[key_columns])` and record how many rows were removed. Investigate before dropping: if two rows share a key but differ in a value column, that is a conflict requiring a business rule, not a simple drop.

Step 5 — Handle missing values with a documented strategy. Choose one of three approaches for each column and write your reasoning in the changelog:

Step 6 — Join the sources. Use an explicit merge type (`inner`, `left`, `outer`) and state which you chose and why. After the merge, confirm the row count matches your expectation: an inner join on a key with no nulls and no duplicates on either side should produce a predictable number of rows.

Step 7 — Validate with assertions. Write at least three checks using `assert` statements or `pandas .query()`. Examples:

```python

assert clean_df['tract_fips'].isna().sum() == 0, "Null FIPS codes remain"

assert clean_df['year'].between(2015, 2024).all(), "Year out of project scope"

assert (clean_df['pop_density'] >= 0).all(), "Negative population density found"

```

A failed assertion is not a grade penalty — it is an early warning. Fix the root cause, rerun from the top, and confirm all assertions pass before submission.

Writing the Changelog

The changelog is a Markdown file (e.g., `CHANGELOG.md`) stored alongside your notebook. Each entry must include:

1. What changed — the specific column or rows affected.

2. Why — the data-quality reason.

3. Row counts before and after — so a reviewer can verify the math.

A classmate should be able to open your raw data, run your notebook top to bottom, and land on the exact final row count you documented. If they cannot, your pipeline is not reproducible.

Data Safety and Professional Practice

City datasets often contain personally identifiable information (PII) — names, addresses, parcel IDs that can be linked back to individuals. Before you share a notebook or push it to a public repository, confirm that your data-sharing agreement permits it and that any PII columns are masked or aggregated. Treat data governance as seriously as a physical safety rule: a breach cannot be undone. Never commit raw data files containing PII to a public version-control repository.

Connecting to Your Credentials

The skills in this lesson map directly to the Google Data Analytics Certificate Process phase (data cleaning and verification), the CompTIA Data+ domain covering data preparation and quality, and the IBM Data Science Professional Certificate modules on data wrangling. The reproducibility standard you practice here is exactly what a hiring manager expects when they ask you to "show your work."

Key terms9

FIPS codeA standardized numeric identifier assigned by the federal government to states, counties, and census tracts; must be stored as a zero-padded string to preserve leading zeros during joins.
reproducible pipelineA sequence of documented, scripted data-transformation steps that any analyst can rerun on the original raw files to produce an identical output.
changelogA written record of every transformation applied to a dataset, including what changed, the reason for the change, and row counts before and after each step.
imputationThe process of replacing missing values with estimated substitutes (such as column mean, median, or mode) according to a documented, justified strategy.
assertion checkA programmatic statement that raises an error if a specified data-quality condition is not met, used to validate a clean dataset before analysis.
merge typeThe rule governing which rows are kept when two tables are joined; common types are inner (only matching rows), left (all rows from the left table), and outer (all rows from both tables).
deduplicationThe process of identifying and removing rows that are exact or near-exact duplicates, typically using a defined subset of key columns as the uniqueness criterion.
address standardizationConverting address strings to a consistent format (uniform case, standard abbreviations, trimmed whitespace) so that records referring to the same location match during a join.
PII (Personally Identifiable Information)Any data element that could be used alone or in combination to identify a specific individual; must be handled according to applicable data-sharing agreements and masked before public sharing.

Worked examples4

1. A county FIPS column was loaded as an integer. The value 6037 must be a 5-character zero-padded string '06037'. Show the pandas code and confirm the result.
  1. Load the column; after reading the CSV, the column dtype is int64 and the value is 6037.
  2. Convert to string: df['county_fips'] = df['county_fips'].astype(str)
  3. Zero-pad to 5 characters: df['county_fips'] = df['county_fips'].str.zfill(5)
  4. Verify: assert df['county_fips'].str.len().eq(5).all() — this should pass without error.
Answer: The value 6037 becomes the string '06037'. The assertion passes because every value in the column is now exactly 5 characters long.
2. Table A has 1,200 rows. Table B has 950 rows. You perform an inner join on 'tract_fips'. After the join you have 870 rows. How many rows from Table A did not find a match in Table B, and how many rows from Table B did not find a match in Table A?
  1. An inner join keeps only rows where the key exists in both tables.
  2. Rows in Table A with no match in Table B = 1,200 − 870 = 330 rows.
  3. Rows in Table B with no match in Table A = 950 − 870 = 80 rows.
  4. Log both numbers in the changelog as unmatched records and investigate whether the gaps are expected (e.g., rural tracts present in one source but not the other) or signal a FIPS alignment problem.
Answer: 330 rows from Table A had no matching key in Table B; 80 rows from Table B had no matching key in Table A. Both numbers must be documented in the changelog with a justification for why they were excluded.
3. A numeric column 'median_income' has 40 null values out of 870 rows (approximately 4.6 percent). You decide to impute with the column median. Write the two-line pandas code and record the changelog entry.
  1. Calculate the median: median_val = df['median_income'].median() — this ignores nulls by default in pandas.
  2. Fill nulls: df['median_income'] = df['median_income'].fillna(median_val)
  3. Add a flag column so downstream users know which rows were filled: df['income_imputed'] = df['median_income'].isna() — note: compute this flag BEFORE filling.
  4. Changelog entry: 'median_income: 40 null values (4.6 %) imputed with column median (value: [median_val]). Indicator column income_imputed added. Rows before: 870, rows after: 870 (no rows dropped).'
Answer: The column median_income now has zero nulls. Row count is unchanged at 870. The Boolean column income_imputed marks the 40 imputed rows as True. The changelog records the strategy and the median value used.
4. After cleaning, you write three assert statements. One fails: assert (df['year'].between(2015, 2024).all()). The .value_counts() of 'year' shows 12 rows contain the value 9999. What steps do you take before resubmitting?
  1. Identify the root cause: query the raw source file to confirm that 9999 is a sentinel value (a common code for 'unknown' in legacy government datasets), not a real year.
  2. Decide on a business rule: if 9999 means unknown, replace with NaN and impute or drop those rows, depending on whether 'year' is a key analysis field.
  3. Apply the fix: df['year'] = df['year'].replace(9999, pd.NA), then apply your imputation or drop strategy and document both in the changelog with row counts.
  4. Rerun the notebook from the top to ensure no earlier step reintroduces the value.
  5. Confirm the assertion now passes: assert (df['year'].between(2015, 2024).all()) — no error raised.
Answer: The 12 sentinel values are replaced with NaN, treated according to a documented strategy, and the year column now passes the assertion. The changelog records the sentinel value discovery, the rule applied, and the before/after row counts.
In class: Instructor demonstrates a multi-step cleaning pipeline for a two-source city dataset join (address standardization, FIPS code alignment, duplicate removal, missing-value imputation strategy with justification); students fork the notebook and adapt it to their own data. · Students write a changelog Markdown file that records every transformation step (what changed, why, row counts before and after); a classmate must be able to rerun the notebook on the raw data and reproduce the final row count exactly. · Students validate their clean table against three self-defined data-quality rules (e.g., no nulls in key join column, date range within project scope, numeric fields within plausible domain bounds) using assert statements or pandas .query() checks; failed assertions are corrected before submission.
Materials: Python 3 with pandas, numpy, pyjanitor; R with tidyverse (optional); PostgreSQL; Jupyter Notebook or R Markdown; Course 5 data-cleaning reference sheet; Google Data Analytics Certificate — Process phase notes.
Check: Instructor reruns the cleaning notebook from raw data; pass requires zero assert-statement failures, changelog present with entries for every transformation, and final table row count matching the student's documented expected count — all three = pass.

Practice check8

Answer each question, then check it. Nothing is recorded; this is your own practice.

Data preparation and quality (CompTIA Data+)
1. A city dataset joins on a FIPS tract code. After loading, the code '06037190100' appears as the integer 6037190100. Which single pandas operation correctly restores it?
Data wrangling (IBM Data Science Professional)
2. Which merge type should you use when you want to keep ALL rows from your primary city table even if there is no matching record in the supplementary census table?
Process phase / data cleaning documentation (Google Data Analytics Certificate)
3. A student's changelog shows the row count dropped from 1,450 to 870 during deduplication but gives no explanation. What is the most important thing missing from this changelog entry?
Data preparation (CompTIA Data+)
4. You want to flag rows where 'pop_density' was imputed before filling the nulls. In what order should the two operations appear in your code?
Reproducibility and documentation (IBM Data Science Professional)
5. A classmate cannot reproduce your final row count of 812 when they rerun your notebook on the raw data. They get 798. What is the FIRST thing you should check?
Data validation (CompTIA Data+)
6. Which of the following assert statements correctly checks that the key join column 'tract_fips' contains no null values in a pandas DataFrame named clean_df?
Data governance and ethics (Google Data Analytics Certificate)
7. A city dataset contains full mailing addresses. Before sharing the cleaned notebook publicly on GitHub, what is the analyst's most important professional obligation?
Data wrangling / tooling (IBM Data Science Professional)
8. The pyjanitor function clean_names() applied to a pandas DataFrame will:
Record this lesson on my transcript. Sign in with your key and enrollment email to record lessons (enroll free if you have no key). This page stays open to read and practise.