Spot workload imbalances across your team
The problem
One team member is consistently working evenings while another finishes on time. Complex cases are all going to the same person. Someone's carrying three projects while others have one. You don't spot these imbalances until someone burns out or quits. By then the damage is done: lost expertise, team morale hit, service disruption. You need to see workload distribution patterns before they become crises.
The solution
Analyse workload data to identify systemic imbalances: who's carrying disproportionate complexity, who's consistently overloaded, where capacity doesn't match demand. The analysis spots patterns: 'Team A consistently handles 40% more cases than Team B', 'Complex cases concentrated on 2 staff members', 'Evening/weekend work pattern indicates understaffing'. This isn't individual monitoring - it's organisational health checking. You can rebalance, redistribute, or resource properly.
What you get
Workload distribution analysis showing: cases per staff member, complexity distribution, time patterns (working hours, out-of-hours activity), workload trends over time. 'Imbalance identified: Youth team member carrying 35 active cases (team average: 22). Complex case concentration: 70% handled by 2 of 6 staff. Capacity gap: Team consistently working 15% over contracted hours.' Evidence to support hiring, restructuring, or workload redistribution.
Before you start
- Workload data: cases/clients per staff member, case complexity indicators, hours worked (if tracked)
- At least 6-12 months of data to identify patterns
- Staff consent and transparency - this is about organisational fairness, not individual surveillance
- Commitment to act on findings (rebalancing, hiring, support)
- A Google account for Colab
- Basic Python skills or willingness to adapt example code
- Check your data protection policy - processing staff activity data may require a Data Protection Impact Assessment (DPIA)
When to use this
- You suspect workload is unevenly distributed but lack evidence
- Team members are burning out and you need to understand why
- You're trying to make the case for hiring and need to demonstrate capacity gaps
- Complex cases seem concentrated on specific staff members
When not to use this
- Your team is tiny (under 5 people) - you can see workload distribution directly
- You don't have workload data (no case management system, no tracking)
- You're planning to use this for performance management - this is about organisational fairness, not individual assessment
- You won't act on findings - analysing workload without addressing imbalances demotivates staff
Steps
- 1
Be transparent with staff
Critical first step: tell your team what you're doing and why. This is about identifying unfair workload distribution to fix it, not monitoring individuals. Share findings with staff. Get their input on what data matters (they know what creates pressure). Transparency builds trust - hidden analysis creates anxiety.
- 2
Gather workload data
Export from case management systems: cases per staff member, case complexity scores (if you track them), case duration, client contact hours, administrative load. Also: hours worked (from timesheets if available), out-of-hours activity, leave taken. You're looking for: quantity, complexity, and time patterns.
- 3
Define workload indicators
What creates workload pressure in your context? Number of active cases? Complexity scores? Hours of client contact? Mix of case types? Emergency interventions? Identify 3-5 key indicators that genuinely reflect work intensity, not just what's easy to count.
- 4
Analyse distribution patterns
Compare across team: who has above/below average caseload? Is complexity evenly distributed or concentrated? Are some roles carrying disproportionate admin? The analysis shows whether workload is fair or systematically imbalanced. Look for patterns, not individual outliers (someone on leave skews data temporarily).
- 5
Identify capacity mismatches
Calculate: total demand (cases, hours needed) vs total capacity (staff hours available). If demand exceeds capacity consistently, you're understaffed - workload imbalance might be a staffing problem, not distribution. If capacity exists but work is concentrated, it's redistribution needed.
- 6
Spot concerning trends
Look at patterns over time: is workload growing faster than capacity? Are imbalances getting worse or better? Is out-of-hours work increasing (sign of unsustainability)? Are complex cases increasingly concentrated (knowledge concentration risk)? Trends show whether your current approach is working.
- 7
Check for systemic factors
Why do imbalances exist? Are certain roles inherently overloaded by design? Do some staff get allocated complex cases because they're experienced (concentrating risk)? Is geography a factor (one area has more demand)? Understanding why helps you fix root causes, not just symptoms.
- 8
Develop action plans
Based on findings: hire more staff if understaffed, redistribute cases if imbalanced, restructure teams if roles are unsustainable, provide support for high-complexity workloads, address skill gaps that concentrate work. Share analysis with team and ask: does this match your experience? What would help?
- 9
Monitor and adjust(optional)
Run analysis quarterly. Are interventions working? Is workload becoming more balanced? Are capacity issues improving? This isn't set-and-forget - workload patterns change as demand shifts, staff turnover happens, programmes grow. Regular monitoring catches issues early.
Example code
Analyse team workload distribution
This identifies workload imbalances across a team. Your CSV should include columns: staff_member, date, active_cases, case_complexity_avg, hours_worked. The scipy library (used for trend analysis) is included in Colab by default.
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
# Load workload data
# Columns: staff_member, date, active_cases, case_complexity_avg, hours_worked, etc.
workload = pd.read_csv('team_workload.csv')
workload['date'] = pd.to_datetime(workload['date'])
print(f"Loaded {len(workload)} workload records")
print(f"Team members: {workload['staff_member'].nunique()}")
print(f"Date range: {workload['date'].min()} to {workload['date'].max()}")
# Get most recent month for current snapshot
latest_month = workload['date'].max()
current_workload = workload[workload['date'] == latest_month]
print(f"\nAnalysing workload as of {latest_month.strftime('%B %Y')}")
print("="*60)
# 1. Case distribution analysis
case_stats = current_workload.groupby('staff_member').agg({
'active_cases': 'sum',
'case_complexity_avg': 'mean',
'hours_worked': 'sum'
}).round(1)
team_avg_cases = case_stats['active_cases'].mean()
team_avg_complexity = case_stats['case_complexity_avg'].mean()
team_avg_hours = case_stats['hours_worked'].mean()
print("\nCurrent Workload Distribution:")
print(f"{'Staff Member':<20} {'Active Cases':<15} {'Avg Complexity':<18} {'Hours':<10}")
print("-"*60)
for staff, row in case_stats.iterrows():
cases_indicator = "⚠️ " if row['active_cases'] > team_avg_cases * 1.2 else " "
complexity_indicator = "⚠️ " if row['case_complexity_avg'] > team_avg_complexity * 1.15 else " "
hours_indicator = "⚠️ " if row['hours_worked'] > team_avg_hours * 1.1 else " "
print(f"{staff:<20} {cases_indicator}{row['active_cases']:<13.0f} "
f"{complexity_indicator}{row['case_complexity_avg']:<16.1f} "
f"{hours_indicator}{row['hours_worked']:<8.0f}")
print(f"\n{'Team Average:':<20} {team_avg_cases:<13.1f} {team_avg_complexity:<16.1f} {team_avg_hours:<8.1f}")
# 2. Identify imbalances
print("\n" + "="*60)
print("Workload Imbalances Identified:")
print("="*60)
# Cases imbalance
case_std = case_stats['active_cases'].std()
case_cv = (case_std / team_avg_cases) * 100 # Coefficient of variation
if case_cv > 25: # More than 25% variation
print(f"\n⚠️ High Case Distribution Inequality")
print(f" Variation: {case_cv:.1f}% (above 25% threshold)")
highest_cases = case_stats['active_cases'].max()
lowest_cases = case_stats['active_cases'].min()
print(f" Range: {lowest_cases:.0f} to {highest_cases:.0f} cases")
print(f" Ratio: Highest has {highest_cases/lowest_cases:.1f}x more cases than lowest")
# Complexity concentration
high_complexity_staff = case_stats[case_stats['case_complexity_avg'] > team_avg_complexity * 1.15]
if len(high_complexity_staff) > 0 and len(high_complexity_staff) < len(case_stats) * 0.5:
print(f"\n⚠️ Complex Case Concentration")
print(f" {len(high_complexity_staff)} of {len(case_stats)} staff handling disproportionate complexity")
print(f" Staff with high complexity:")
for staff in high_complexity_staff.index:
complexity = high_complexity_staff.loc[staff, 'case_complexity_avg']
print(f" {staff}: {complexity:.1f} (team avg: {team_avg_complexity:.1f})")
# Hours worked imbalance
hours_over_40 = current_workload[current_workload['hours_worked'] > 40]
if len(hours_over_40) > 0:
print(f"\n⚠️ Excess Hours Pattern")
print(f" {len(hours_over_40)} staff members working over 40 hours")
for _, row in hours_over_40.iterrows():
print(f" {row['staff_member']}: {row['hours_worked']:.0f} hours")
# 3. Capacity analysis
total_cases = current_workload['active_cases'].sum()
total_capacity_hours = len(current_workload) * 37.5 # Assuming 37.5hr contracts
total_worked_hours = current_workload['hours_worked'].sum()
capacity_utilisation = (total_worked_hours / total_capacity_hours) * 100
print(f"\n" + "="*60)
print("Team Capacity Analysis:")
print("="*60)
print(f"Total active cases: {total_cases:.0f}")
print(f"Team size: {len(current_workload)} staff")
print(f"Contracted capacity: {total_capacity_hours:.0f} hours")
print(f"Actual hours worked: {total_worked_hours:.0f} hours")
print(f"Capacity utilisation: {capacity_utilisation:.1f}%")
if capacity_utilisation > 105:
print(f"\n⚠️ Team Operating Over Capacity")
print(f" Working {capacity_utilisation - 100:.1f}% above contracted hours")
print(f" Indicates understaffing or unsustainable workload")
elif capacity_utilisation < 85:
print(f"\n✅ Team Has Spare Capacity")
print(f" Could accommodate {100 - capacity_utilisation:.1f}% more work")
else:
print(f"\n✅ Capacity Utilisation Healthy")
# 4. Trend analysis (last 6 months)
recent_data = workload[workload['date'] >= (latest_month - pd.DateOffset(months=6))]
trend_by_month = recent_data.groupby('date').agg({
'active_cases': 'sum',
'hours_worked': 'sum'
})
# Calculate trend
from scipy import stats
months_numeric = np.arange(len(trend_by_month))
cases_slope, _, _, _, _ = stats.linregress(months_numeric, trend_by_month['active_cases'])
hours_slope, _, _, _, _ = stats.linregress(months_numeric, trend_by_month['hours_worked'])
print(f"\n" + "="*60)
print("6-Month Trends:")
print("="*60)
if cases_slope > 2:
print(f"⚠️ Cases Increasing: +{cases_slope:.1f} per month")
elif cases_slope < -2:
print(f"✅ Cases Decreasing: {cases_slope:.1f} per month")
else:
print(f"→ Cases Stable: {cases_slope:+.1f} per month")
if hours_slope > 5:
print(f"⚠️ Hours Increasing: +{hours_slope:.1f} per month")
print(f" Suggests growing workload pressure")
elif hours_slope < -5:
print(f"✅ Hours Decreasing: {hours_slope:.1f} per month")
else:
print(f"→ Hours Stable: {hours_slope:+.1f} per month")
# 5. Recommendations
print(f"\n" + "="*60)
print("Recommendations:")
print("="*60)
if capacity_utilisation > 105:
additional_staff_needed = ((total_worked_hours - total_capacity_hours) / 37.5)
print(f"\n1. Consider hiring {additional_staff_needed:.1f} additional FTE")
print(f" Current team working unsustainable hours")
if case_cv > 25:
print(f"\n2. Redistribute cases to balance workload")
print(f" Current distribution shows significant inequality")
if len(high_complexity_staff) > 0 and len(high_complexity_staff) < len(case_stats) * 0.5:
print(f"\n3. Spread complex cases more evenly across team")
print(f" Current concentration creates burnout risk and knowledge silos")
if cases_slope > 2:
print(f"\n4. Monitor growing demand")
print(f" Cases increasing {cases_slope:.1f} per month - plan capacity accordingly")
print(f"\n5. Review findings with team")
print(f" Ask: Does this match your experience? What would help?")
# Save analysis
case_stats.to_csv('workload_analysis_current.csv')
trend_by_month.to_csv('workload_trends.csv')
# Create visualisation
fig, axes = plt.subplots(2, 1, figsize=(12, 10))
# Chart 1: Current case distribution
ax1 = axes[0]
case_stats['active_cases'].plot(kind='bar', ax=ax1, color='skyblue')
ax1.axhline(team_avg_cases, color='red', linestyle='--', label='Team Average')
ax1.set_title('Active Cases per Staff Member')
ax1.set_xlabel('Staff Member')
ax1.set_ylabel('Active Cases')
ax1.legend()
ax1.grid(True, alpha=0.3, axis='y')
# Chart 2: Trend over time
ax2 = axes[1]
ax2.plot(trend_by_month.index, trend_by_month['active_cases'], marker='o', label='Total Cases')
ax2.set_title('Team Workload Trend (Last 6 Months)')
ax2.set_xlabel('Month')
ax2.set_ylabel('Total Active Cases')
ax2.legend()
ax2.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('workload_analysis.png', dpi=150)
print(f"\nAnalysis complete!")
print(f"Results saved to:")
print(f" - workload_analysis_current.csv")
print(f" - workload_trends.csv")
print(f" - workload_analysis.png")Tools
Resources
At a glance
- Time to implement
- days
- Setup cost
- free
- Ongoing cost
- free
- Cost trend
- stable
- Organisation size
- medium, large
- Target audience
- operations-manager, ceo-trustees, data-analyst
All tools are free. All processing runs locally. This is about using existing data (case management systems, timesheets) to spot patterns. Initial setup takes a day. Run quarterly to monitor trends. Prevents burnout and turnover which cost far more than analysis time (recruitment: £5,000-15,000 per role).