Case Study 30-1: Priya Investigates South Warehouse Turnover
Background
It was a Wednesday afternoon in mid-February 2024, three weeks after Acme Corp had closed the books on its 2023 fiscal year. Marcus Webb knocked on Priya's office door — an unusual occurrence, since Marcus usually communicated by email.
He was carrying a printout: the January 2024 South region headcount report. The South warehouse had budgeted for 22 employees and currently had 16. The remaining six positions were vacant, creating a backlog in order fulfillment that was starting to affect customer service times.
"Sandra asked me to look into this," Marcus said, setting the printout on Priya's desk. "I think the problem is high turnover — we keep hiring people and they keep leaving. But I don't have numbers. Can you tell me how bad it actually is, and whether there's a pattern we can fix?"
Marcus added: "I need something I can show Sandra and the regional operations director by next Monday. Keep it clean — nothing that would identify individual employees."
Priya had worked with HR data before and knew the rules: aggregate only, suppress small groups, never show individual salaries or performance data in shared reports.
The Data
Marcus arranged read-only access to two datasets from Acme's HRIS:
- acme_employees_2023.csv — current active employees as of December 31, 2023
- acme_separations_2022_2023.csv — separation records for the two most recent calendar years
Priya was not given individual performance ratings, medical leave details, or compensation data beyond salary bands for her own group analysis. These were in the HRIS but outside the scope of this particular analysis.
The Analysis
Step 1: Establish Baseline Metrics
Priya started with the company-wide picture before zooming into South warehouse.
from hr_analytics import (
generate_employee_dataset,
generate_separations_history,
calculate_turnover_rate,
turnover_by_department,
turnover_by_tenure_bucket,
seasonal_turnover,
MIN_GROUP_SIZE_FOR_REPORTING,
)
import pandas as pd
# Load data (using synthetic for this case study)
employee_df = generate_employee_dataset()
separations_df = generate_separations_history(employee_df)
# 2023 separations only
sep_2023 = separations_df[separations_df["year"] == 2023]
# Company-wide turnover rate
company_turnover = calculate_turnover_rate(sep_2023, len(employee_df), "Acme Corp 2023")
print(f"Company-wide turnover rate: {company_turnover['total_turnover_rate']}%")
print(f"Voluntary: {company_turnover['voluntary_turnover_rate']}%")
# Department breakdown
dept_headcount = employee_df.groupby("department").size()
dept_turnover = turnover_by_department(sep_2023, dept_headcount)
print("\nTurnover by department:")
print(dept_turnover[["department", "separations", "turnover_rate"]].to_string(index=False))
Acme's company-wide 2023 turnover: 17.2%. The warehouse department: 26.3%. Already significantly elevated, but the company-wide warehouse number was not the specific story Marcus needed.
Step 2: Isolate the South Region
# South warehouse employees
south_wh_employees = employee_df[
(employee_df["location"] == "South") &
(employee_df["department"] == "Warehouse & Fulfillment")
].copy()
# South warehouse separations in 2023
south_wh_sep = separations_df[
(separations_df["location"] == "South") &
(separations_df["department"] == "Warehouse & Fulfillment") &
(separations_df["year"] == 2023)
].copy()
# All warehouse for comparison
all_wh_employees = employee_df[
employee_df["department"] == "Warehouse & Fulfillment"
]
all_wh_sep = sep_2023[sep_2023["department"] == "Warehouse & Fulfillment"]
# Calculate rates
south_rate = calculate_turnover_rate(
south_wh_sep, len(south_wh_employees), "South Warehouse 2023"
)
all_wh_rate = calculate_turnover_rate(
all_wh_sep, len(all_wh_employees), "All Warehouse 2023"
)
print(f"South Warehouse 2023 turnover: {south_rate['total_turnover_rate']}%")
print(f"All Warehouse 2023 turnover: {all_wh_rate['total_turnover_rate']}%")
print(f"Company average 2023: {company_turnover['total_turnover_rate']}%")
South warehouse: 34.1% annual turnover. All warehouse: 26.3%. Company average: 17.2%.
Priya noted: "South warehouse is running at 2× the company average. That means roughly 1 in 3 warehouse employees in the South is leaving each year."
Step 3: When Are They Leaving?
# Tenure at separation analysis
tenure_analysis = turnover_by_tenure_bucket(south_wh_sep)
print("\nTenure at separation — South Warehouse 2023:")
print(tenure_analysis.to_string(index=False))
# Compare to company-wide separation tenure
company_tenure = turnover_by_tenure_bucket(sep_2023)
print("\nTenure at separation — Company 2023:")
print(company_tenure.to_string(index=False))
| Tenure Bucket | South Warehouse % | Company-Wide % |
|---|---|---|
| 0–6 months | 46% | 29% |
| 6–12 months | 21% | 18% |
| 1–2 years | 18% | 22% |
| 2–5 years | 11% | 20% |
| 5+ years | 4% | 11% |
The concentration in the 0–6 month bucket is striking. Nearly half of South warehouse departures happen in the first six months. By contrast, the company-wide profile is more evenly distributed. South warehouse is losing people very early in their tenure.
Step 4: When During the Year?
# Seasonal pattern for South warehouse
seasonal = seasonal_turnover(south_wh_sep)
print("\nSeasonal turnover pattern — South Warehouse 2023:")
print(seasonal[["month_name", "separations", "seasonal_index"]].to_string(index=False))
| Month | Separations | Seasonal Index |
|---|---|---|
| Jan | 1 | 57 |
| Feb | 1 | 57 |
| Mar | 2 | 114 |
| Apr | 1 | 57 |
| May | 1 | 57 |
| Jun | 2 | 114 |
| Jul | 3 | 171 |
| Aug | 4 | 228 |
| Sep | 3 | 171 |
| Oct | 1 | 57 |
| Nov | 1 | 57 |
| Dec | 1 | 57 |
The pattern is clear: July, August, and September are dramatically elevated — Q3 seasonal index averages 190, meaning nearly double the turnover of other months. This is classic seasonal warehouse attrition: employees hired for the spring/summer period leave as the season winds down.
Step 5: Are These Voluntary Departures?
if "separation_reason" in south_wh_sep.columns:
reason_counts = south_wh_sep["separation_reason"].value_counts()
voluntary_pct = (
south_wh_sep["separation_reason"] == "Voluntary Resignation"
).sum() / len(south_wh_sep) * 100
print(f"\nVoluntary resignation rate: {voluntary_pct:.1f}%")
print(reason_counts)
71.4% voluntary resignations. These employees are actively choosing to leave, not being let go for performance reasons. That matters enormously for the recommended response.
Step 6: Quantifying the Cost
Priya built a simple cost estimate based on industry guidance that warehouse role backfill costs approximately $4,500 per separation (advertising, agency fees, onboarding, lost productivity during ramp-up).
n_south_seps = len(south_wh_sep)
cost_per_sep = 4500
total_2023_cost = n_south_seps * cost_per_sep
industry_expected_seps = int(len(south_wh_employees) * 0.25) # 25% = industry benchmark
industry_expected_cost = industry_expected_seps * cost_per_sep
excess_cost = total_2023_cost - industry_expected_cost
print(f"\nSouth Warehouse 2023 Turnover Cost Estimate:")
print(f" Actual separations: {n_south_seps}")
print(f" Estimated cost per sep: ${cost_per_sep:,}")
print(f" Total 2023 turnover cost: ${total_2023_cost:,}")
print(f" Industry-expected separations: {industry_expected_seps}")
print(f" Excess cost vs. benchmark: ${excess_cost:,}")
print(f" (Note: These are rough estimates for planning, not accounting figures)")
Even at a conservative $4,500 per separation, the excess turnover above benchmark was generating an estimated $22,000–$36,000 in annual avoidable costs — not counting the operational impact of unfilled positions on order fulfillment.
Step 7: The Anonymized Report
Priya prepared a clean, department-level summary with no individual employee data:
print("=" * 60)
print(" ACME CORP — SOUTH WAREHOUSE TURNOVER ANALYSIS")
print(" Prepared by: Priya Okonkwo, Acting Senior Analyst")
print(" Date: February 19, 2024")
print(" Classification: Internal / HR Use Only")
print(" Data: Aggregated department-level, no individual records")
print("=" * 60)
print(f"\n Scope: South Region Warehouse & Fulfillment department")
print(f"\n Key Findings:")
print(f" 1. Annual turnover rate: 34.1% (vs. 26.3% all-warehouse, 17.2% company)")
print(f" 2. 46% of departures happen in first 6 months of tenure")
print(f" 3. Q3 (July-September) shows 2x the monthly average separation rate")
print(f" 4. 71% of departures are voluntary resignations")
print(f" 5. Estimated excess annual turnover cost: $22,000-$36,000")
print(f"\n Interpretation:")
print(f" The data points consistently to an early-tenure retention problem")
print(f" concentrated in Q3. New hires are voluntarily leaving within their")
print(f" first 6 months at nearly twice the company rate. The Q3 concentration")
print(f" suggests a seasonal hiring cohort that does not convert to permanent")
print(f" employees.")
print(f"\n Recommendations (for HR and Operations review):")
print(f" 1. Implement structured 30-60-90 day onboarding for all warehouse hires")
print(f" 2. Introduce a Q3 retention incentive paid at 90 days of service")
print(f" 3. Review May-June hiring to improve pre-summer integration time")
print(f" 4. Conduct brief structured check-in conversations at 30 days")
print(f" (manager-led, not evaluated — focused on role clarity and support)")
print(f"\n Next Steps: HR review by Feb 28 | Budget request by March 15")
print("=" * 60)
The Outcome
Marcus and Sandra reviewed the analysis on a Friday morning. Sandra's first response: "This is exactly what I needed — specific enough to act on, but not pointing fingers at individuals."
Marcus, who had been skeptical of Priya's Python work in her first year, admitted: "I would have guessed the problem was pay. But this shows it's really an onboarding and seasonal issue. That changes what we do about it."
The recommendations were implemented in phases: - A structured 90-day onboarding program launched in April 2024, covering job expectations, equipment training, safety protocols, and explicit check-ins at day 30 and day 60 - A $250 bonus paid at 90 days for new South warehouse hires (approved in March) - Hiring for the summer surge was shifted to April rather than June, giving new hires more time to integrate before the peak summer months
By Q3 2024, South warehouse voluntary turnover in the first 6 months was tracking at 28% (down from 46%), and the annual run-rate for South warehouse turnover was projected at 26% — still elevated but consistent with the all-warehouse average.
What This Case Study Illustrates
Diagnosis before prescription. Marcus thought the problem was pay. The data showed it was onboarding and seasonality. Without a structured diagnosis, the company might have invested in above-band compensation adjustments that addressed the wrong problem.
Multiple lenses on the same data. Priya used three different cuts of the same dataset — overall rate, tenure-at-separation, and seasonal index — and all three pointed to the same explanation. That convergence is what makes the analysis credible.
Privacy-by-default in action. Every output was aggregated. No individual was identified. The recommendations were about process changes (onboarding, timing), not about specific employees. Marcus could share the report broadly because it contained only aggregate data.
Cost quantification creates urgency. The estimate of $22,000–$36,000 in excess annual turnover cost transformed "we have a retention problem" into "we have a retention problem that is costing us real money that could fund the solution." Priya explicitly labeled the estimate as rough and for planning purposes — avoiding the false precision that undermines analytical credibility.