How to use this companion
Open the Module 4 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 4 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 4 Learning Goals
This lab is light on new code. It is about professional habits: a notebook someone else can rerun, names a reader understands, documentation of what each column means, and careful decisions about who may see which data. By the end of the lab you should be able to:
- Use reusable, relative file paths with
pathlib.Path. - Write readable Python with descriptive
snake_casenames. - Organize a notebook so its purpose, inputs, checks, decisions and outputs are visible.
- Draft, improve and save a data dictionary.
- Tell apart direct identifiers, confidential identifiers, quasi-identifiers and sensitive business fields.
- Create and verify a review copy for one specific audience, and explain responsible AI and tool use.
The main idea: someone else should be able to rerun your work, understand it, and know how the data may be shared.
Before You Start: Repository and Setup
Module 4 uses the same repository you created for Module 3 from the template tianhaiz/ban6003-week-03-04-cleaning-reproducibility-template. Do not create a new one, and do not work in the instructor template.
- Open your own Week 3–4 repository in GitHub Codespaces (main path). If you create a new Codespace, the first setup takes about 4–5 minutes.
- Open
notebooks/week4_github_repro_governance_lab.ipynb. Select the kernel if VS Code asks. - Run the cells from the top. If you restart the kernel, run from the top again.
- Keep the repository public, or invite the instructor account
zzz1990771as a collaborator.
Optional local path: open the repository with GitHub Desktop (Open in Visual Studio Code), trust the folder when VS Code asks, and choose the base conda environment as the kernel.
Part 1: Imports and Reusable Paths
A reproducible notebook shows clearly where its data comes from and where its outputs go. Import packages first, at the top, because every later cell uses them. Then define the paths once, near the top.
1.1 Imports, paths and an input check
from pathlib import Path
import pandas as pd
# Project-style paths
RAW_DATA_PATH = Path("../data/raw/employee_data.csv")
PROCESSED_DATA_DIR = Path("../data/processed")
PROCESSED_DATA_DIR.mkdir(parents=True, exist_ok=True)
if not RAW_DATA_PATH.is_file():
raise FileNotFoundError(f"Required raw input was not found: {RAW_DATA_PATH}")
print("Raw data path:", RAW_DATA_PATH)
print("Processed data folder:", PROCESSED_DATA_DIR)
Part 2: Load the Data with a Descriptive Name
employees_raw = pd.read_csv(RAW_DATA_PATH)
employees_raw.head()
| employee_id | first_name | last_name | hire_date | department | salary | age | full_time |
|---|---|---|---|---|---|---|---|
| 101 | Ava | Nguyen | 2021-03-15 | Finance | $95,000 | 29 | Yes |
| 102 | Ben | Carter | 2020/07/01 | Sales | 78000 | 35 | Y |
| 103 | Carla | Diaz | not a date | HR | 62000 | thirty | No |
| 104 | David | Evans | 2019-11-30 | IT | $88,500 | 41 | Yes |
| 105 | Ella | Ford | NaN | Marketing | NaN | 27 | No |
This is the same raw employee file as in Module 3, so the problems will look familiar: a mixed date format, not a date, thirty as an age, $ and commas in salaries, and Yes/Y in full_time.
Check the column types
employees_raw.info()
Part 3: Clean Code Style in Practice
Readable code is not complicated code. Four habits help: snake_case names, descriptive names, important paths and settings near the top, and comments that explain why rather than repeat what the code says.
3.1 Profile values with clear names
row_count, column_count = employees_raw.shape
salary_missing_count = employees_raw["salary"].isna().sum()
duplicate_employee_id_count = employees_raw["employee_id"].duplicated().sum()
print(f"Rows: {row_count}")
print(f"Columns: {column_count}")
print(f"Missing salary values: {salary_missing_count}")
print(f"Duplicate employee_id values: {duplicate_employee_id_count}")
Your Turn: Improve the Names
Try first
The notebook's original cell works, but x and y say nothing. Copy it into the TODO cell and rename both variables. What does each line compute, and what name says that?
- Rewrite
x = employees_raw["age"].isna().sum()andy = employees_raw["department"].value_counts()with descriptive names. - Change the names in the
print()lines too, or Python will not find them.
# Original version
x = employees_raw["age"].isna().sum()
y = employees_raw["department"].value_counts()
print(x)
print(y)
Show sample solution for Your Turn: improve names
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.
age_missing_count = employees_raw["age"].isna().sum()
department_value_count = employees_raw["department"].value_counts()
print(age_missing_count)
print(department_value_count)What it prints:
1
department
Finance 3
Sales 2
HR 2
IT 2
Marketing 2
sales 1
Name: count, dtype: int64The fastest way is to copy the working cell and change only the names. Copying and adapting working code is good practice; just replace every occurrence.
Part 4: Notebook Organization
A professional notebook tells a story, not just code and results. A useful structure is: purpose → inputs → data checks → decisions or observations → outputs. Use it for the whole notebook and also for each small task: before a code block, say what you are checking; after it, say what you found.
4.1 Count missing values in every column
employees_raw.isna().sum()
Complete independently: your observation
Write 3–4 sentences in the notebook's Your observation cell. This companion does not write it for you.
What to check: you report at least two results with numbers (for example a total, or the column with the most missing values), and you name one limitation of isna().sum(). Hint for the limitation: look again at the first five rows. Is not a date counted as missing? What about invalid in a salary?
Part 5: Build a Practical Data Dictionary
A data dictionary is the most common kind of metadata (data about data). It tells another person what each column means and how it must be handled. This one has six columns: column_name, current_dtype, business_description, sensitivity_level, cleaning_or_governance_notes and sharing_guidance. The last one turns a classification into an action: who may use the field, or what must happen before it is shared.
5.1 Create the starter dictionary
data_dictionary = pd.DataFrame({
"column_name": employees_raw.columns,
"current_dtype": [str(dtype) for dtype in employees_raw.dtypes],
"business_description": [
"Internal employee identifier",
"Employee first name",
"Employee last name",
"Employee hire date",
"Employee department",
"Employee salary",
"Employee age",
"Whether the employee works full time"
],
"sensitivity_level": [
"Confidential internal identifier",
"Direct identifier / PII",
"Direct identifier / PII",
"Quasi-identifier",
"Business attribute",
"Sensitive financial/business information",
"Quasi-identifier",
"Employment attribute"
],
"cleaning_or_governance_notes": [
"Should be unique; validate non-numeric and duplicate values",
"Not needed for most analytical summaries",
"Not needed for most analytical summaries",
"Parse and validate dates before analysis",
"Clarify valid categories and business definitions",
"Convert to numeric and investigate missing or invalid values",
"Convert to numeric and investigate impossible values",
"Clarify meaning and standardize valid values"
],
"sharing_guidance": [
"Restrict to authorized users who need record-level traceability",
"Remove unless the approved purpose requires a name",
"Remove unless the approved purpose requires a name",
"Generalize or remove for broad sharing",
"Review disclosure risk when combined with other fields",
"Restrict access; aggregate for broad audiences",
"Group into ranges or remove for broad sharing",
"Use only when relevant to the approved purpose"
]
})
data_dictionary
Your Turn: Improve the Data Dictionary
Try first
The starter entries are deliberately brief. Edit at least two entries so another analyst would know exactly what the field means or how to handle it. Use what you learned about this file in Module 3.
- Choose two entries in
business_description,cleaning_or_governance_notesorsharing_guidance. - Use
data_dictionary.loc[row condition, column name] = new text, as in the example in the TODO cell. - Run the save cell afterwards, so the saved CSV contains your edits.
Show sample solution for Your Turn: improve the data dictionary
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.
data_dictionary.loc[
data_dictionary["column_name"] == "salary",
"cleaning_or_governance_notes",
] = (
"Stored as text: remove $ and commas, then convert to numeric. "
"One value is missing and one says 'invalid'; confirm both with the HR system owner."
)
data_dictionary.loc[
data_dictionary["column_name"] == "full_time",
"business_description",
] = "Whether the employee works full time; recorded as Yes/Y (full time) or No/N (not full time)"
data_dictionary.loc[
data_dictionary["column_name"].isin(["salary", "full_time"]),
["column_name", "business_description", "cleaning_or_governance_notes"],
]These are examples; choose your own entries and wording. Good edits are specific: they name the actual problem, the action, and who confirms it.
5.2 Save the dictionary
dictionary_output_path = PROCESSED_DATA_DIR / "employee_data_dictionary.csv"
data_dictionary.to_csv(dictionary_output_path, index=False)
print("Saved data dictionary to:", dictionary_output_path)
Part 6: Identify PII and Sensitive Fields
PII (personally identifiable information) is information that identifies a person. Some columns identify a person directly. Others become identifying when combined. Some do not identify anyone but still carry business or ethical risk.
6.1 Sensitivity summary
sensitivity_summary = data_dictionary[["column_name", "sensitivity_level"]]
sensitivity_summary
| column_name | sensitivity_level |
|---|---|
| employee_id | Confidential internal identifier |
| first_name | Direct identifier / PII |
| last_name | Direct identifier / PII |
| hire_date | Quasi-identifier |
| department | Business attribute |
| salary | Sensitive financial/business information |
| age | Quasi-identifier |
| full_time | Employment attribute |
Complete independently: privacy and governance reflection
Answer the four questions in the notebook in 4–6 sentences: direct identifiers; fields that stay sensitive or identifying after names are removed; what you would remove, generalize or aggregate first for a broad management audience; and why audience and purpose are part of the release decision.
What to check: you name specific columns from the table above, you explain at least one combination risk, and you connect your answer to a specific audience rather than saying “it is safe” or “it is unsafe” in general.
Part 7: Create a Limited Internal Review Copy
Scenario: an authorized HR data-quality reviewer needs row-level records and employee IDs to trace source-system problems, but does not need names. You create a copy without first_name and last_name. This file is not anonymous: it still has a confidential ID, exact dates, age and salary, so it stays with the approved internal audience.
7.1 Drop the name columns and verify
direct_name_columns = ["first_name", "last_name"]
employees_review_copy = employees_raw.drop(columns=direct_name_columns).copy()
# Verify that names were removed without changing the number of records.
assert len(employees_review_copy) == len(employees_raw)
assert set(direct_name_columns).isdisjoint(employees_review_copy.columns)
assert set(direct_name_columns).issubset(employees_raw.columns)
employees_review_copy.head()
| employee_id | hire_date | department | salary | age | full_time |
|---|---|---|---|---|---|
| 101 | 2021-03-15 | Finance | $95,000 | 29 | Yes |
| 102 | 2020/07/01 | Sales | 78000 | 35 | Y |
| 103 | not a date | HR | 62000 | thirty | No |
| 104 | 2019-11-30 | IT | $88,500 | 41 | Yes |
| 105 | NaN | Marketing | NaN | 27 | No |
7.2 Save the review copy
review_output_path = PROCESSED_DATA_DIR / "employee_review_copy_no_names.csv"
employees_review_copy.to_csv(review_output_path, index=False)
print("Saved restricted internal review copy to:", review_output_path)
7.3 Verify the saved file, not only the DataFrame
saved_review_copy = pd.read_csv(review_output_path)
guided_release_checks = pd.Series({
"source_rows": len(employees_raw),
"saved_review_rows": len(saved_review_copy),
"rows_match": len(saved_review_copy) == len(employees_raw),
"first_name_present": "first_name" in saved_review_copy.columns,
"last_name_present": "last_name" in saved_review_copy.columns,
"employee_id_present": "employee_id" in saved_review_copy.columns,
"salary_present": "salary" in saved_review_copy.columns,
})
guided_release_checks
Part 8: Responsible AI and Tool-Use Checkpoint
Complete independently
An AI assistant asks you to upload the employee CSV. In 3–4 sentences in the notebook, explain why a real HR file should not go to an unapproved external tool, what you would provide instead, and who remains accountable for the final decision.
What to check: your answer names at least one specific sensitive field from this file, describes a safer input (for example column names, data types and made-up example rows), and names who is accountable.
Part 9: GitHub Workflow Checkpoint
Save the notebook first. Then review what will be committed with git status, and use a commit message that says what the work did.
git status
git add .
git commit -m "Complete Week 4 documentation and governance lab"
git pushRun these in the terminal (Terminal → New Terminal), not in a notebook cell. In Codespaces you can also use the Source Control panel; with the local path, use GitHub Desktop: commit, then Push origin.
Part 10: Offline Assignment: Release-Readiness Audit
Complete independently
This assignment is graded and intentionally not solved in the companion. Scenario: a teammate wants to email employee_review_copy_no_names.csv to a broad group of managers because the names were removed.
- Reload
review_output_pathasoffline_review(the starter cell does this). - Replace each
None # TODOinrelease_readiness_summarywith a True/False check. - Display the summary.
- Write the 5–6 sentence release decision.
What to check: before your edits, the summary already shows source_rows 13, review_rows 13, rows_match True and direct_name_columns_present False; the four TODO columns show None. Follow the hint in the cell: use the same "column_name" in offline_review.columns pattern as step 7.3. All eight values should be True or False when you finish. Your decision approves or rejects broad email, cites at least three results from the summary, names at least two remaining disclosure risks and recommends one safer audience, control or more aggregated output.
# Offline Assignment
# Hint: use `"column_name" in offline_review.columns` for each remaining-field check.
# The direct-name check is already completed as an example.
offline_review = pd.read_csv(review_output_path)
release_readiness_summary = pd.DataFrame([{
"source_rows": len(employees_raw),
"review_rows": len(offline_review),
"rows_match": len(employees_raw) == len(offline_review),
"direct_name_columns_present": bool(set(direct_name_columns) & set(offline_review.columns)),
"employee_id_present": None, # TODO
"salary_present": None, # TODO
"exact_age_present": None, # TODO
"exact_hire_date_present": None, # TODO
}])
release_readiness_summary
Optional Practice: A Purpose-Limited Copy (Not Graded)
This practice does not count toward your grade. Scenario: an approved internal manager needs only the recorded department and full_time categories. Keep only those two columns, save, reload and check.
Try first
Reuse the pattern from Part 7. Decide which columns are approved, which are restricted, and which checks prove the saved file is right.
- Create
manager_category_copywith onlydepartmentandfull_time; save it asoptional_manager_category_copy.csvinPROCESSED_DATA_DIR. - Reload it as
saved_manager_category_copy. - Create
optional_release_checkswith the row-count, approved-columns and restricted-columns checks, then run the self-check cell.
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.
approved_columns = ["department", "full_time"]
restricted_columns = {"employee_id", "first_name", "last_name", "hire_date", "salary", "age"}
manager_category_copy = employees_raw[approved_columns].copy()
manager_copy_path = PROCESSED_DATA_DIR / "optional_manager_category_copy.csv"
manager_category_copy.to_csv(manager_copy_path, index=False)
saved_manager_category_copy = pd.read_csv(manager_copy_path)
optional_release_checks = pd.Series({
"rows_match_source": len(saved_manager_category_copy) == len(employees_raw),
"only_approved_columns": set(saved_manager_category_copy.columns) == set(approved_columns),
"restricted_columns_present": sorted(restricted_columns & set(saved_manager_category_copy.columns)),
})
optional_release_checksWhat it prints:
rows_match_source True
only_approved_columns True
restricted_columns_present []
dtype: object
# Run this self-check after you create saved_manager_category_copy and optional_release_checks.
required_practice_objects = {
"saved_manager_category_copy",
"optional_release_checks",
}
if not required_practice_objects.issubset(globals()):
print("Complete the optional practice first, then rerun this self-check.")
else:
expected_columns = {"department", "full_time"}
restricted_columns = {"employee_id", "first_name", "last_name", "hire_date", "salary", "age"}
print("Rows match source:", len(saved_manager_category_copy) == len(employees_raw))
print("Only approved columns remain:", set(saved_manager_category_copy.columns) == expected_columns)
print("Restricted columns present:", sorted(restricted_columns & set(saved_manager_category_copy.columns)))
display(optional_release_checks)
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 name rewrite, the observation, the dictionary edits, the privacy reflection, the AI response, the offline summary and the release decision.
- Restart the kernel and run all cells from top to bottom. Confirm there are no errors.
- Confirm that both files exist in
data/processed:employee_data_dictionary.csvandemployee_review_copy_no_names.csv. - Save the notebook, check
git status, then commit and push. - Open your repository on GitHub to confirm everything arrived, and submit the repository link in Canvas.
Lab Wrap-Up
This lab added professional habits to the cleaning work of Module 3:
Path, relative paths,mkdir(exist_ok=True)andis_file(): a notebook that runs on any computer.- Descriptive
snake_casenames and a purpose → inputs → checks → decisions → outputs structure: work others can read. - A saved data dictionary: metadata that tells people what each field means and how to share it.
drop(...).copy(),assertand reloading the saved file: verified, audience-specific outputs.- Responsible tool use: data ownership and policy first, and you remain accountable.
Removing names alone does not make a file anonymous. Who receives the data, and for what purpose, is part of every release decision. Module 5 starts transforming data with query(), sort_values() and assign(); keep using the naming and path habits from this lab.