How to use this companion
Open the Module 3 notebook and this companion at the same time. Work through the companion from top to bottom and run the matching notebook cells as you go. The notebook is the authoritative source for the lab sequence, code and required work. This companion explains what each step means, what to notice in the output and why the step matters.
Read aloud: controls and directions
Before you start: choose a reading mode, speed, and voice below. Select Read Aloud from Beginning to hear the full companion, or use Read Aloud from This Section beside any major section to continue from that point. While audio is playing, a floating control bar remains visible at the bottom of the page. Use Pause to pause temporarily, Resume to continue, and Stop to end the current reading.
Study mode reads explanations, results, tables, and notes while skipping exact Python code. Full mode also reads Python code and verbalizes common programming symbols.
Ready to read.
The read-aloud feature uses your browser's speech engine. A screen reader can access the page independently of these controls.
What is in the notebook, and what this companion adds
- Notebook: the exact Python code, the Your Turn tasks, the final practice, the offline assignment and the written reflections.
- Companion: plain-language explanations, the results you should expect (taken from a real run of the notebook), sample solutions for the Your Turn tasks (folded, so you try first), common mistakes and how to fix them.
- If anything ever seems inconsistent: follow the current notebook.
This companion is designed for keyboard and screen-reader access, but it is not a separate version for students who need accessibility features. It is the same learning guide for everyone. You can read it visually, use a screen reader, or use the built-in read-aloud controls while you work in the notebook.
Use either course environment
- GitHub Codespaces (main path): open your own repository in Codespaces, open the Module 3 notebook and use the default Python kernel.
- Local VS Code (optional, for experienced students): clone your repository, use the
baseconda environment, installrequirements.txt, open the repository folder in Visual Studio Code and open the notebook. - In either environment: the notebook and this companion are the same. Keep the repository structure unchanged so the notebook paths and output folders work.
Module 3 Learning Goals
In Week 2 you were a data detective: you profiled a dataset and changed nothing. This week you are a careful data surgeon. You clean the same kind of employee file, employee_data.csv, and you make every change on purpose and write it down. By the end of the lab you should be able to:
- Make a safe working copy of a raw DataFrame with
.copy(). - Fix data types with
pd.to_numeric()andpd.to_datetime(), and explain whaterrors="coerce"does. - Tell full-row duplicates from key-based duplicates, and remove repeated
employee_idvalues. - Compare two ways to handle missing values: dropping rows and filling them with the median (imputation).
- Flag a suspicious value instead of silently changing it.
- Check the result with
info(),shapeandisna().sum(), and keep a cleaning log.
The main idea: cleaning is not guessing. Cleaning is making traceable decisions.
Some string tools in this lab, such as .str.replace(), may be new. That is fine: Module 6 covers text in detail. Today the goal is the overall cleaning procedure.
Before You Start: Repository and Setup
Weeks 3 and 4 use a new repository. Both the Week 3 and the Week 4 notebooks are in it.
- Open the template repository
tianhaiz/ban6003-week-03-04-cleaning-reproducibility-templateon GitHub (the link is in the Canvas lab guide). Make sure it is the week 03-04 template, not another week's. - Select Use this template, then Create a new repository. Use a clear name, for example
ban6003-week-03-04-plus your name. - Keep it public if you can. If you make it private, invite the instructor account
zzz1990771as a collaborator, or the instructor cannot grade it. - Open your new repository in GitHub Codespaces. A new Codespace usually needs 4–5 minutes the first time; reopening it later is much faster.
- Open
notebooks/week3_data_cleaning_lab.ipynband run the cells from the top. If you restart the kernel, run from the top again.
Optional local path: clone your repository with GitHub Desktop, open it in Visual Studio Code, choose the base conda environment as the kernel and install requirements.txt once. If VS Code asks whether you trust the folder, choose Trust; in restricted mode the notebook cannot run.
The repository now has a data folder
Unlike Week 2, the data lives in its own folder. data/raw holds the input file, which you never edit. data/processed is where your cleaned output goes. Keeping the two apart protects the original data.
Parts 1–2: Load and Review the Raw Data
1.1 Load the raw file
from pathlib import Path
import pandas as pd
# Load the protected Week 3 raw employee data.
raw_path = Path("../data/raw/employee_data.csv")
df = pd.read_csv(raw_path)
df.head()
1.2 Rows and columns
# How many rows and columns?
df.shape
1.3 Data types
# What data types did Pandas assign?
df.info()
1.4 Missing values
# Count missing values by column
df.isna().sum()
1.5 Look at all 13 rows
# First look at all rows for this small demo dataset
df
Part 3: Create a Safe Working Copy
3.1 Copy the raw data and start a cleaning log
# Keep the raw df unchanged and clean a copy
raw_df = df.copy()
df_clean = df.copy()
# Start a simple cleaning log
cleaning_log = []
print("Raw data shape:", raw_df.shape)
print("Working copy shape:", df_clean.shape)
Part 4: Correct Numeric Data Types
pd.to_numeric() converts a column to numbers. With errors="coerce", any value that cannot be converted becomes a missing value instead of stopping the code with an error. That is useful: the bad values become visible and countable. It does not decide what the right value is; you still do that.
4.1 employee_id to numbers
# Convert employee_id to a numeric value.
# Invalid values such as "error" become NaN.
df_clean["employee_id"] = pd.to_numeric(df_clean["employee_id"], errors="coerce")
# Use pandas nullable integer type so missing IDs can exist temporarily
df_clean["employee_id"] = df_clean["employee_id"].astype("Int64")
cleaning_log.append("Converted employee_id to numeric using pd.to_numeric(errors='coerce'). Invalid IDs became missing values.")
df_clean[["employee_id"]]
4.2 Look at salary before converting
# Show the original salary values before conversion
df_clean["salary"]
4.3 Remove $ and commas, then convert salary
# Remove dollar signs and commas, then convert to numeric
salary_cleaned = (
df_clean["salary"]
.astype("string")
.str.replace("$", "", regex=False)
.str.replace(",", "", regex=False)
.str.strip()
)
df_clean["salary"] = pd.to_numeric(salary_cleaned, errors="coerce")
cleaning_log.append("Removed dollar signs and commas from salary, then converted salary to numeric. Invalid salary values became missing values.")
df_clean[["salary"]]
4.4 age to numbers
df_clean["age"] = pd.to_numeric(df_clean["age"], errors="coerce")
cleaning_log.append("Converted age to numeric using pd.to_numeric(errors='coerce'). Invalid age values became missing values.")
df_clean[["age"]]
Part 5: Correct Date Data Types
5.1 hire_date to datetime
df_clean["hire_date"] = pd.to_datetime(
df_clean["hire_date"],
errors="coerce",
format="mixed",
)
cleaning_log.append("Converted hire_date to datetime with mixed-format parsing and errors='coerce'. Invalid dates became NaT.")
df_clean[["hire_date"]]
Part 6: Check What Changed
6.1 Data types after conversion
df_clean.info()
6.2 Missing values after conversion
df_clean.isna().sum()
Parts 7–8: Find and Remove Duplicates
A full-row duplicate repeats every value in a row. A key-based duplicate repeats the identifier, here employee_id, even if other values differ. Because one row should be one employee, the key is what matters.
7.1 Full-row duplicates
# Full-row duplicate count
df_clean.duplicated().sum()
7.2 Key-based duplicates
# Key-based duplicate count for employee_id
df_clean["employee_id"].duplicated().sum()
7.3 Show every row with a repeated ID
# Show rows with duplicated employee_id values
# keep=False marks all occurrences of the duplicate ID, not just the later ones.
df_clean[df_clean["employee_id"].duplicated(keep=False)].sort_values("employee_id")
8.1 Remove repeated IDs, keeping the first
before_rows = df_clean.shape[0]
df_clean = df_clean.drop_duplicates(subset=["employee_id"], keep="first")
after_rows = df_clean.shape[0]
rows_removed = before_rows - after_rows
cleaning_log.append(f"Removed {rows_removed} duplicate row(s) based on employee_id, keeping the first occurrence.")
print("Rows before:", before_rows)
print("Rows after:", after_rows)
print("Rows removed:", rows_removed)
Parts 9–10: Inspect Missing Data and Drop Critical Gaps
9.1 Missing values after removing duplicates
df_clean.isna().sum()
9.2 Rows with any missing value
# Show rows with any missing values
missing_rows = df_clean[df_clean.isna().any(axis=1)]
missing_rows
10.1 Drop rows missing a critical field
before_rows = df_clean.shape[0]
df_clean = df_clean.dropna(subset=["employee_id", "hire_date"])
after_rows = df_clean.shape[0]
rows_removed = before_rows - after_rows
cleaning_log.append(f"Dropped {rows_removed} row(s) with missing critical employee_id or hire_date.")
print("Rows before:", before_rows)
print("Rows after:", after_rows)
print("Rows removed:", rows_removed)
Part 11: Impute Missing Numbers with the Median
11.1 Calculate the medians
# Calculate medians before filling missing values
salary_median = df_clean["salary"].median()
age_median = df_clean["age"].median()
print("Salary median:", salary_median)
print("Age median:", age_median)
11.2 Fill the gaps
# Fill missing salary and age values with their medians
df_clean["salary"] = df_clean["salary"].fillna(salary_median)
df_clean["age"] = df_clean["age"].fillna(age_median)
cleaning_log.append(f"Imputed missing salary values with the median salary: {salary_median}.")
cleaning_log.append(f"Imputed missing age values with the median age: {age_median}.")
df_clean[["salary", "age"]].head()
Part 12: Flag Suspicious Values
12.1 Ages above 100
# Show unusually high ages
suspicious_age = df_clean[df_clean["age"] > 100]
suspicious_age
if not suspicious_age.empty:
cleaning_log.append("Flagged age values above 100 for further review. These were not automatically changed in this lab.")
Parts 13–15: Verify, Review the Log and Save
13.1 Data types and counts after cleaning
df_clean.info()
df_clean.isna().sum()
df_clean.shape
df_clean.head()
14.1 Print the cleaning log
for i, item in enumerate(cleaning_log, start=1):
print(f"{i}. {item}")
15.1 Save the cleaned data
processed_dir = Path("../data/processed")
processed_dir.mkdir(parents=True, exist_ok=True)
output_path = processed_dir / "employee_data_cleaned_week3.csv"
df_clean.to_csv(output_path, index=False)
print(f"Cleaned data saved to: {output_path}")
Part 16: Your Turn: Reflection Questions
Complete independently
These five written answers are part of your grade, so the companion does not answer them. Answer in the Your Answers cell, in your own words, and point to the step that supports each answer.
- Which cleaning step created new missing values? Why did that happen?
- Why did we use median imputation for
salaryandageinstead of mean imputation? - Why is it safer to use
drop_duplicates(subset=["employee_id"])instead of onlydrop_duplicates()in this dataset? - What issue did we flag but not automatically fix?
- What is one cleaning decision in this lab that might require more investigation in a real organization?
Where to look: compare the missing counts in steps 1.4 and 6.2; Part 11 and its note; the difference between full-row and key-based duplicates in Part 7; Part 12; and the notes on dropping rows and on keep="first".
Part 17: Offline Assignment: Independent Output Audit
Complete independently
This assignment is graded and intentionally not solved in the companion. Use the exact instructions in the notebook, and do not change the raw CSV file.
- Reload the CSV from
output_pathinto a new DataFrame namedoffline_checkand inspect it. - Complete the four
None # TODOvalues inoffline_validation_summary: duplicate employee IDs, missing critical values (employee_idplushire_date), missing numeric values (salaryplusage), and ages above 100. - Write a 4–6 sentence audit response that refers to at least three of the six results, says whether the file is ready for later analysis, and explains how you would investigate the suspicious age.
What to check: the first two results are already filled in (8 rows and 8 unique IDs). Each TODO must be a single number, not a table: select the column or columns, then count, using the hints in the cell. Your numbers should agree with what you saw in Parts 12 and 13. Do not “fix” the age above 100 to make the summary look clean.
# Offline Assignment
offline_check = pd.read_csv(output_path)
offline_check.head()
# Offline Assignment - Complete the four TODO values.
# Hints:
# - Use .duplicated().sum() on employee_id for duplicate IDs.
# - Select the named columns first, then use .isna().sum().sum() for missing totals.
# - Use a Boolean condition on age, then sum the True values.
offline_validation_summary = pd.Series({
"total_rows": len(offline_check),
"unique_nonmissing_employee_ids": offline_check["employee_id"].dropna().nunique(),
"duplicate_employee_ids": None, # TODO
"missing_critical_values": None, # TODO: employee_id plus hire_date
"missing_numeric_values": None, # TODO: salary plus age
"ages_above_100": None, # TODO
}, name="result")
offline_validation_summary
Optional Practice: Clean a New Mini Dataset
This practice is not graded. It repeats the whole workflow on five new rows, so it is a good way to check that you can do it without the guided code.
practice_raw = pd.DataFrame({
"employee_id": ["201", "202", "202", "bad", "205"],
"salary": ["$52,000", "61,000", "$61,000", "not available", ""],
"hire_date": ["2024-01-15", "02/01/2024", "02/01/2024", "bad date", "2024-03-10"],
"age": ["29", "unknown", "35", "41", "150"],
"department": ["Sales", "Finance", "Finance", "IT", "Operations"],
})
practice_raw
Try first
Work through the seven steps in the notebook, reusing the patterns from the guided lab. Before each step, predict how many rows you will have afterwards.
- Copy
practice_rawintopractice_clean. - Convert
employee_idto a nullable integer, coercing invalid IDs. - Remove
$and commas fromsalary, then convert it. - Convert
ageto numbers andhire_dateto dates, coercing invalid values. - Remove duplicate employee IDs, keeping the first record.
- Drop rows missing
employee_idorhire_date. - Fill missing salary and age values with the median of the remaining records.
Show sample solution for the optional practice
Use this only after you have made your own attempt. Compare the structure and logic, not just the final line of code. This sample solution is written by the companion; it is not part of the notebook.
practice_clean = practice_raw.copy()
practice_clean["employee_id"] = pd.to_numeric(practice_clean["employee_id"], errors="coerce").astype("Int64")
practice_salary = (
practice_clean["salary"]
.astype("string")
.str.replace("$", "", regex=False)
.str.replace(",", "", regex=False)
.str.strip()
)
practice_clean["salary"] = pd.to_numeric(practice_salary, errors="coerce")
practice_clean["age"] = pd.to_numeric(practice_clean["age"], errors="coerce")
practice_clean["hire_date"] = pd.to_datetime(practice_clean["hire_date"], errors="coerce", format="mixed")
practice_clean = practice_clean.drop_duplicates(subset=["employee_id"], keep="first")
practice_clean = practice_clean.dropna(subset=["employee_id", "hire_date"])
practice_clean["salary"] = practice_clean["salary"].fillna(practice_clean["salary"].median())
practice_clean["age"] = practice_clean["age"].fillna(practice_clean["age"].median())
practice_cleanWhat it prints:
employee_id salary hire_date age department
0 201 52000.0 2024-01-15 29.0 Sales
1 202 61000.0 2024-02-01 89.5 Finance
4 205 56500.0 2024-03-10 150.0 OperationsThen run the notebook's self-check cell.
# Run this self-check after you create practice_clean.
if "practice_clean" not in globals():
print("Complete the optional practice first, then rerun this self-check.")
else:
practice_check = pd.Series({
"raw_rows_unchanged": len(practice_raw) == 5,
"clean_rows": len(practice_clean),
"duplicate_employee_ids": practice_clean["employee_id"].duplicated().sum(),
"missing_salary_or_age": practice_clean[["salary", "age"]].isna().sum().sum(),
"ages_above_100": (practice_clean["age"] > 100).sum(),
}, name="result")
display(practice_check)
print("Expected: 3 clean rows, 0 duplicate IDs, 0 missing salary/age values, and 1 age above 100.")
Common Problems
If a cell fails, read the last line of the error message first. Then find it below.
Still stuck? Post the error message and the cell number in the course Q&A discussion. Do not post your full answers.
Save and Submit
- Complete the Part 16 answers and the Part 17 offline audit (summary and written response).
- Restart the kernel and run all cells from top to bottom. Confirm there are no errors.
- Confirm that
data/processed/employee_data_cleaned_week3.csvexists. - Save the notebook, then commit and push:
git add .,git commit -m "Completed Week 3 data cleaning lab",git push. In Codespaces you can also use the Source Control panel; with GitHub Desktop, commit and then push. - Open your repository on GitHub in the browser and check that the notebook and the cleaned CSV are there. The instructor grades the online repository, not your computer.
- Submit your repository link in Canvas. The Week 4 notebook is in the same repository.
Lab Wrap-Up
You used one careful sequence that you will reuse in your project:
.copy(): protect the raw data and clean a copy.pd.to_numeric()/pd.to_datetime()witherrors="coerce": fix the structure first and expose bad values.duplicated()/drop_duplicates(subset=...): decide what must be unique, then remove repeats.dropna(subset=...)andfillna(): drop or fill only with a stated reason.- Flag, do not guess: suspicious values stay until someone with evidence decides.
- Cleaning log and
to_csv(): record every decision and save the result separately from the raw file.
Week 4 uses the same repository and the same data to make this work reproducible: clear names, a notebook that tells a story, a data dictionary and a careful look at sensitive fields.