Module 11 · Python for Data Analytics
From question to insight
Run a complete analysis in a notebook: turn a vague question into answerable ones, check the data, analyse, test the obvious explanation, and write findings a manager can act on, with honest caveats.
About 30 minutes
The problem
Kolanut's HR director sends Bisi one line:
"We keep losing people. Why, and what should we do about it?"
That's a real business question, and like most, it isn't one you can answer with a single line of pandas. Analysts who jump straight to code produce a pile of tables. Analysts who are valued produce three sentences the director can act on. This lesson walks through the whole process on Kolanut's HR data, using everything from the course so far.
The concept
1. Turn the question into answerable questions
"Why are we losing people?" becomes:
- How many people have left, and what share of everyone we've employed is that?
- Who leaves: which levels, which departments?
- When do they leave: how long after joining?
- Is it pay? Do leavers earn less than colleagues at the same level who stayed?
Each of these maps to a filter, a groupby or a comparison you already know.
2. Check the data before trusting it
Before analysing, confirm the data means what you think it means. Does every Resigned employee have an exit_date, and does nobody Active have one? Are the dates real dates? Any duplicates? Five minutes of checks protects every number that follows.
3. Analyse: rates, not counts
Use rates to compare groups of different sizes, compare like with like, and keep a note of how many people are behind each figure.
4. Test the obvious explanation
Everyone will assume pay. Check it directly. An analysis that rules out the obvious explanation is often more valuable than one that confirms it.
5. Write the finding, with the caveat
A finding has three parts: what you found (with a number), so what it means, and now what you recommend. Then the honest limit: here, only 11 people have left in seven years. Patterns in 11 people are worth acting on, but not worth over-claiming.
Example
Load, check and set up:
import pandas as pd
hr = "https://academy.cloudtechanalytics.com/datasets/hr/"
employees = pd.read_csv(hr + "employees.csv", parse_dates=["hire_date", "exit_date"])
# Check: Resigned <=> has an exit date
mismatches = ((employees["status"] == "Resigned") != employees["exit_date"].notna()).sum()
print(f"{len(employees)} employees, {employees['employee_id'].duplicated().sum()} duplicate IDs, {mismatches} status/exit mismatches")
employees["left"] = employees["status"] == "Resigned"
as_of = pd.Timestamp("2026-06-30")
employees["tenure_years"] = (employees["exit_date"].fillna(as_of) - employees["hire_date"]).dt.days / 365.25
print(f"{employees['left'].sum()} have left ({employees['left'].mean():.1%} of everyone employed)")80 employees, 0 duplicate IDs, 0 status/exit mismatches
11 have left (13.8% of everyone employed)The data is consistent, so the analysis can proceed. fillna(as_of) gives people who are still here their tenure so far, up to the end of the data.
Who leaves, and when:
by_level = employees.groupby("job_level").agg(staff=("employee_id", "size"), left=("left", "sum"))
by_level["rate"] = (by_level["left"] / by_level["staff"]).round(2)
print(by_level)
print("\nMedian years in the job:")
print(employees.groupby("left")["tenure_years"].median().round(1).rename({True: "leavers", False: "stayers"})) staff left rate
job_level
Junior 31 7 0.23
Manager 10 0 0.00
Mid 24 4 0.17
Senior 15 0 0.00
Median years in the job:
left
stayers 4.2
leavers 1.5
Name: tenure_years, dtype: float64Is it pay? Compare leavers with stayers at the same level:
pay = employees[employees["job_level"].isin(["Junior", "Mid"])].pivot_table(
index="job_level", columns="left", values="monthly_salary", aggfunc="median"
)
pay.columns = ["stayed", "left"]
pay stayed left
job_level
Junior 277500.0 275000.0
Mid 517500.0 582500.0Leavers weren't paid less than people at their level who stayed; at Mid level they were paid slightly more. Pay doesn't explain it.
Walkthrough
- Run the Example's first cell and read the check line before the result line. If the checks had failed, you'd stop and fix the data first.
- Run the level and tenure cell. Write down in plain words what each table says.
- Run the pay comparison. Note how comparing within a level matters: overall, leavers earn less than stayers simply because juniors leave and juniors earn less. That comparison would wrongly "prove" it's about pay.
- Departments:
employees.groupby("department")["left"].agg(["size", "sum", "mean"]).round(2). Customer Service and Operations account for 8 of the 11 leavers. - Look at the leavers themselves:
employees[employees["left"]].sort_values("exit_date")[["department", "job_level", "tenure_years", "exit_date"]]. Three left in 2026 alone: worth flagging as a possible acceleration, though three is a small number. - Add a text cell headed Findings and write your summary (the task below).
- Re-run the notebook from the top (Runtime → Run all) to prove it works start to finish. An analysis that only works when cells are run in a special order isn't finished.
Practice
Practice
What is the resignation rate among Junior staff, as a percentage? One decimal place.
Practice
How many of the 11 leavers left within 18 months of joining (tenure under 1.5 years)?
Task
10 minWrite the Findings for the HR director: three short bullet points (start each line with -), then one line starting Caveat:. Use numbers from your analysis. Say what you found, what it means, and what you'd recommend.
Your work is checked for
- Three bullet points, each starting with -
- Uses numbers from the analysis
- Mentions job level or how long people stay
- Deals with pay (the obvious explanation)
- Makes a recommendation
- Includes an honest caveat line
- Short enough for a busy director (40 to 160 words)
Challenge
Challenge · optional
In the June 2026 attendance.csv, which department has the highest average number of Late records per employee? Merge attendance with employees to get each record's department.
Check your understanding
Answer every question to check.