Module 4 Lab Companion

BAN 6003: Data Management and Analytics Integration

GitHub, Reproducibility, Governance, and Ethics

Experimental pilot. Use the Ask about this lab panel (on the right, or the Ask a question button on small screens) to ask about the section you are reading, or open Common questions for saved answers. Answers explain the companion in plain language and may add general background. Each answer is labelled: instructor-approved answers were approved by your instructor; AI-generated answers are new and not yet reviewed. The notebook is still the authoritative source. Graded work (Part 4: Notebook Organization; Part 6: Identify PII and Sensitive Fields; Part 8: Responsible AI and Tool-Use Checkpoint; Part 10: Offline Assignment: Release-Readiness Audit) is not answered for you. Questions about grades, deadlines or extensions go to your instructor.

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.

Adjust text-to-speech speed from one half to two times normal speed.

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 contentCode, official prompts, required tasks and the lab sequence come from the notebook. Each code block shows its notebook cell number.
Companion-added instructor noteThese clearly labeled notes are explanations, cautions or study advice added by this companion, many of them from the in-person lab session. They are not extra required notebook steps unless the note says so.

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

↑ Back to Lab Contents

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:

The main idea: someone else should be able to rerun your work, understand it, and know how the data may be shared.

↑ Back to Lab Contents

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.

  1. 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.
  2. Open notebooks/week4_github_repro_governance_lab.ipynb. Select the kernel if VS Code asks.
  3. Run the cells from the top. If you restart the kernel, run from the top again.
  4. Keep the repository public, or invite the instructor account zzz1990771 as 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.

↑ Back to Lab Contents

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)

↑ Back to Lab Contents

Part 2: Load the Data with a Descriptive Name

employees_raw = pd.read_csv(RAW_DATA_PATH)
employees_raw.head()
First five rows of employees_raw
employee_idfirst_namelast_namehire_datedepartmentsalaryagefull_time
101AvaNguyen2021-03-15Finance$95,00029Yes
102BenCarter2020/07/01Sales7800035Y
103CarlaDiaznot a dateHR62000thirtyNo
104DavidEvans2019-11-30IT$88,50041Yes
105EllaFordNaNMarketingNaN27No

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()

↑ Back to Lab Contents

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}")

↑ Back to Lab Contents

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?

  1. Rewrite x = employees_raw["age"].isna().sum() and y = employees_raw["department"].value_counts() with descriptive names.
  2. 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: int64

The 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.

↑ Back to Lab Contents

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?

↑ Back to Lab Contents

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

↑ Back to Lab Contents

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.

  1. Choose two entries in business_description, cleaning_or_governance_notes or sharing_guidance.
  2. Use data_dictionary.loc[row condition, column name] = new text, as in the example in the TODO cell.
  3. 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)

↑ Back to Lab Contents

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
Sensitivity level of each column (starter dictionary)
column_namesensitivity_level
employee_idConfidential internal identifier
first_nameDirect identifier / PII
last_nameDirect identifier / PII
hire_dateQuasi-identifier
departmentBusiness attribute
salarySensitive financial/business information
ageQuasi-identifier
full_timeEmployment 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.

↑ Back to Lab Contents

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()
First five rows of employees_review_copy
employee_idhire_datedepartmentsalaryagefull_time
1012021-03-15Finance$95,00029Yes
1022020/07/01Sales7800035Y
103not a dateHR62000thirtyNo
1042019-11-30IT$88,50041Yes
105NaNMarketingNaN27No

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

↑ Back to Lab Contents

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.

↑ Back to Lab Contents

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 push

Run 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.

↑ Back to Lab Contents

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.

  1. Reload review_output_path as offline_review (the starter cell does this).
  2. Replace each None # TODO in release_readiness_summary with a True/False check.
  3. Display the summary.
  4. 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

↑ Back to Lab Contents

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.

  1. Create manager_category_copy with only department and full_time; save it as optional_manager_category_copy.csv in PROCESSED_DATA_DIR.
  2. Reload it as saved_manager_category_copy.
  3. Create optional_release_checks with 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_checks

What 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)

↑ Back to Lab Contents

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.

↑ Back to Lab Contents

Save and Submit

  1. Complete the name rewrite, the observation, the dictionary edits, the privacy reflection, the AI response, the offline summary and the release decision.
  2. Restart the kernel and run all cells from top to bottom. Confirm there are no errors.
  3. Confirm that both files exist in data/processed: employee_data_dictionary.csv and employee_review_copy_no_names.csv.
  4. Save the notebook, check git status, then commit and push.
  5. Open your repository on GitHub to confirm everything arrived, and submit the repository link in Canvas.

↑ Back to Lab Contents

Lab Wrap-Up

This lab added professional habits to the cleaning work of Module 3:

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.