Capacity Planning
Overview
Strategic capacity planning for construction organizations. Forecast resource requirements based on project pipeline, identify capacity constraints, optimize staffing levels, and support go/no-go decisions on new project pursuits.
Capacity Planning Framework
┌─────────────────────────────────────────────────────────────────┐
│ CAPACITY PLANNING │
├─────────────────────────────────────────────────────────────────┤
│ │
│ DEMAND FORECAST CAPACITY ANALYSIS DECISIONS │
│ ─────────────── ───────────────── ───────── │
│ │
│ Current Projects → Available: Pursue new │
│ • Project A (Active) 👷 PM: 5 project? │
│ • Project B (Active) 👷 Supers: 12 ──────── │
│ • Project C (Starting) 📐 Engineers: 8 ✅ Capacity │
│ ⚠️ Stretch │
│ Pipeline: → Required: ❌ Decline │
│ • Bid D (60% win) 👷 PM: 7 │
│ • Bid E (40% win) 👷 Supers: 15 │
│ • Opportunity F 📐 Engineers: 10 │
│ │
│ GAP ANALYSIS: ACTIONS: │
│ • PM: -2 (deficit) • Hire 2 PMs │
│ • Supers: -3 (deficit) • Promote from within │
│ • Engineers: -2 (deficit) • Partner with firm │
│ │
└─────────────────────────────────────────────────────────────────┘
Technical Implementation
from dataclasses import dataclass, field
from typing import List, Dict, Optional, Tuple
from datetime import datetime, timedelta
from enum import Enum
import statistics
class ResourceRole(Enum):
PROJECT_MANAGER = "project_manager"
SUPERINTENDENT = "superintendent"
PROJECT_ENGINEER = "project_engineer"
ESTIMATOR = "estimator"
SCHEDULER = "scheduler"
SAFETY_MANAGER = "safety_manager"
QC_MANAGER = "qc_manager"
ADMIN = "admin"
class ProjectPhase(Enum):
PURSUIT = "pursuit"
PRECONSTRUCTION = "preconstruction"
CONSTRUCTION = "construction"
CLOSEOUT = "closeout"
class OpportunityStatus(Enum):
IDENTIFIED = "identified"
PURSUING = "pursuing"
BID_SUBMITTED = "bid_submitted"
NEGOTIATING = "negotiating"
WON = "won"
LOST = "lost"
@dataclass
class StaffMember:
id: str
name: str
role: ResourceRole
capacity: float = 1.0 # FTE
current_assignment: str = ""
availability_date: datetime = None
skills: List[str] = field(default_factory=list)
max_project_value: float = 0 # Max project size they can handle
@dataclass
class ProjectDemand:
project_id: str
project_name: str
value: float
phase: ProjectPhase
start_date: datetime
end_date: datetime
probability: float = 1.0 # 1.0 for active, <1 for pipeline
resource_needs: Dict[ResourceRole, float] = field(default_factory=dict)
@dataclass
class CapacityGap:
role: ResourceRole
period_start: datetime
period_end: datetime
demand: float
capacity: float
gap: float
severity: str
@dataclass
class CapacityForecast:
forecast_date: datetime
horizon_months: int
total_demand_fte: float
total_capacity_fte: float
utilization_pct: float
gaps: List[CapacityGap]
recommendations: List[str]
class CapacityPlanner:
"""Plan organizational capacity for construction projects."""
# Typical staffing ratios by project value
STAFFING_RATIOS = {
ResourceRole.PROJECT_MANAGER: 20000000, # 1 PM per $20M
ResourceRole.SUPERINTENDENT: 10000000, # 1 Super per $10M
ResourceRole.PROJECT_ENGINEER: 15000000, # 1 PE per $15M
ResourceRole.ESTIMATOR: 50000000, # 1 Estimator per $50M (pursuit)
ResourceRole.SCHEDULER: 30000000, # 1 Scheduler per $30M
ResourceRole.SAFETY_MANAGER: 25000000, # 1 Safety per $25M
}
# Phase factors (multiply by role ratio)
PHASE_FACTORS = {
ProjectPhase.PURSUIT: {"estimator": 1.5, "pm": 0.3},
ProjectPhase.PRECONSTRUCTION: {"pm": 0.7, "pe": 0.5, "scheduler": 0.5},
ProjectPhase.CONSTRUCTION: {"pm": 1.0, "super": 1.0, "pe": 1.0, "safety": 1.0},
ProjectPhase.CLOSEOUT: {"pm": 0.5, "pe": 0.3, "admin": 1.0}
}
def __init__(self, organization_name: str):
self.organization_name = organization_name
self.staff: Dict[str, StaffMember] = {}
self.projects: Dict[str, ProjectDemand] = {}
self.pipeline: Dict[str, ProjectDemand] = {}
def add_staff(self, id: str, name: str, role: ResourceRole,
capacity: float = 1.0, current_assignment: str = "",
availability_date: datetime = None,
max_project_value: float = 0) -> StaffMember:
"""Add staff member to capacity pool."""
member = StaffMember(
id=id,
name=name,
role=role,
capacity=capacity,
current_assignment=current_assignment,
availability_date=availability_date or datetime.now(),
max_project_value=max_project_value
)
self.staff[id] = member
return member
def add_active_project(self, id: str, name: str, value: float,
phase: ProjectPhase, start_date: datetime,
end_date: datetime) -> ProjectDemand:
"""Add active project to demand forecast."""
# Calculate resource needs based on value and phase
needs = self._calculate_resource_needs(value, phase)
project = ProjectDemand(
project_id=id,
project_name=name,
value=value,
phase=phase,
start_date=start_date,
end_date=end_date,
probability=1.0,
resource_needs=needs
)
self.projects[id] = project
return project
def add_pipeline_opportunity(self, id: str, name: str, value: float,
win_probability: float,
expected_start: datetime,
duration_months: int) -> ProjectDemand:
"""Add pipeline opportunity to demand forecast."""
needs = self._calculate_resource_needs(value, ProjectPhase.CONSTRUCTION)
opportunity = ProjectDemand(
project_id=id,
project_name=name,
value=value,
phase=ProjectPhase.PURSUIT,
start_date=expected_start,
end_date=expected_start + timedelta(days=duration_months * 30),
probability=win_probability,
resource_needs=needs
)
self.pipeline[id] = opportunity
return opportunity
def _calculate_resource_needs(self, value: float,
phase: ProjectPhase) -> Dict[ResourceRole, float]:
"""Calculate resource needs based on project value and phase."""
needs = {}
for role, ratio in self.STAFFING_RATIOS.items():
base_need = value / ratio
# Apply phase factor
phase_key = role.value.split('_')[0][:3]
factor = 1.0
if phase in self.PHASE_FACTORS:
factor = self.PHASE_FACTORS[phase].get(phase_key, 1.0)
needs[role] = base_need * factor
return needs
def get_current_capacity(self) -> Dict[ResourceRole, float]:
"""Get current capacity by role."""
capacity = {role: 0.0 for role in ResourceRole}
for member in self.staff.values