Call centres live and die by their metrics. Average wait time. Abandonment rate. Service level. Simulation helps you hit your targets without overstaffing.
The Call Centre Model
Key components:
- Callers - Arrive with varying patience
- Agents - Handle calls
- Queues - Where callers wait
- Skills - Some agents handle specific call types
- Abandonment - Impatient callers hang up
Basic Call Centre
import simpy
import random
class CallCentre:
def __init__(self, env, num_agents):
self.env = env
self.agents = simpy.Resource(env, capacity=num_agents)
self.stats = {
'calls_handled': 0,
'calls_abandoned': 0,
'wait_times': [],
'handle_times': []
}
def handle_call(self, call_id, patience):
arrival = self.env.now
with self.agents.request() as req:
# Wait for agent OR give up
result = yield req | self.env.timeout(patience)
if req in result:
# Connected to agent
wait = self.env.now - arrival
self.stats['wait_times'].append(wait)
# Handle the call
handle_time = random.lognormvariate(2, 0.5) # ~7 mins avg
yield self.env.timeout(handle_time)
self.stats['handle_times'].append(handle_time)
self.stats['calls_handled'] += 1
else:
# Caller abandoned
self.stats['calls_abandoned'] += 1
def call_arrivals(env, call_centre, arrival_rate, mean_patience):
call_id = 0
while True:
yield env.timeout(random.expovariate(arrival_rate))
patience = random.expovariate(1/mean_patience)
env.process(call_centre.handle_call(call_id, patience))
call_id += 1
# Run simulation
env = simpy.Environment()
cc = CallCentre(env, num_agents=10)
env.process(call_arrivals(env, cc, arrival_rate=1/0.5, mean_patience=5))
env.run(until=480) # 8 hours
print(f"Calls handled: {cc.stats['calls_handled']}")
print(f"Calls abandoned: {cc.stats['calls_abandoned']}")
abandonment_rate = cc.stats['calls_abandoned'] / (cc.stats['calls_handled'] + cc.stats['calls_abandoned'])
print(f"Abandonment rate: {abandonment_rate:.1%}")
Multi-Skill Routing
Agents with different skills:
class SkillBasedCallCentre:
def __init__(self, env, agent_config):
self.env = env
self.agents = {}
for skill, count in agent_config.items():
self.agents[skill] = simpy.Resource(env, capacity=count)
self.stats = {'by_skill': {skill: [] for skill in agent_config}}
def handle_call(self, call_id, call_type, patience):
arrival = self.env.now
# Try primary skill first, then overflow
skills_to_try = [call_type, 'general']
for skill in skills_to_try:
if skill not in self.agents:
continue
with self.agents[skill].request() as req:
result = yield req | self.env.timeout(patience)
if req in result:
wait = self.env.now - arrival
handle_time = random.expovariate(1/7)
yield self.env.timeout(handle_time)
self.stats['by_skill'][skill].append({
'call_id': call_id,
'wait': wait,
'handle': handle_time
})
return # Call handled
# Update remaining patience
patience -= (self.env.now - arrival)
if patience <= 0:
break
# All options exhausted - abandoned
print(f"Call {call_id} abandoned")
# Config: 5 sales, 5 support, 3 general (overflow)
config = {'sales': 5, 'support': 5, 'general': 3}
cc = SkillBasedCallCentre(env, config)
VIP Priority Queue
class PriorityCallCentre:
def __init__(self, env, num_agents):
self.env = env
self.agents = simpy.PriorityResource(env, capacity=num_agents)
self.stats = {'vip': [], 'regular': []}
def handle_call(self, call_id, is_vip, patience):
arrival = self.env.now
priority = 1 if is_vip else 5
customer_type = 'vip' if is_vip else 'regular'
with self.agents.request(priority=priority) as req:
result = yield req | self.env.timeout(patience)
if req in result:
wait = self.env.now - arrival
yield self.env.timeout(random.expovariate(1/7))
self.stats[customer_type].append({'wait': wait, 'served': True})
else:
self.stats[customer_type].append({'wait': patience, 'served': False})
Time-Varying Staffing
Match staffing to demand:
class ScheduledCallCentre:
def __init__(self, env, schedule):
"""
schedule = {
0: 5, # Midnight: 5 agents
6: 10, # 6am: 10 agents
9: 20, # 9am: 20 agents (peak)
17: 15, # 5pm: 15 agents
21: 8 # 9pm: 8 agents
}
"""
self.env = env
self.schedule = schedule
self.agents = simpy.Resource(env, capacity=schedule[0])
env.process(self.update_staffing())
def update_staffing(self):
while True:
current_hour = (self.env.now / 60) % 24
# Find applicable staffing level
for hour in sorted(self.schedule.keys(), reverse=True):
if current_hour >= hour:
self.agents._capacity = self.schedule[hour]
break
yield self.env.timeout(60) # Check hourly
Callback System
Instead of waiting, offer callback:
class CallCentreWithCallback:
def __init__(self, env, num_agents, callback_threshold):
self.env = env
self.agents = simpy.Resource(env, capacity=num_agents)
self.callback_threshold = callback_threshold
self.callback_queue = []
def handle_call(self, call_id, patience):
arrival = self.env.now
# If queue is long, offer callback
if len(self.agents.queue) > self.callback_threshold:
if random.random() < 0.7: # 70% accept callback
self.callback_queue.append({
'call_id': call_id,
'requested_at': self.env.now
})
print(f"Call {call_id} scheduled for callback")
return
# Normal queue handling
with self.agents.request() as req:
result = yield req | self.env.timeout(patience)
if req in result:
yield self.env.timeout(random.expovariate(1/7))
def process_callbacks(self):
while True:
yield self.env.timeout(1) # Check regularly
if self.callback_queue and self.agents.count < self.agents.capacity:
callback = self.callback_queue.pop(0)
self.env.process(self.make_callback(callback))
def make_callback(self, callback):
with self.agents.request() as req:
yield req
yield self.env.timeout(random.expovariate(1/7))
print(f"Callback to {callback['call_id']} completed")
Complete Call Centre Simulation
import simpy
import random
import numpy as np
class FullCallCentre:
def __init__(self, env, config):
self.env = env
self.config = config
self.agents = simpy.PriorityResource(env, capacity=config['agents'])
self.stats = {
'calls': [],
'service_levels': [],
'queue_lengths': []
}
def call(self, call_id, call_type, is_vip):
arrival = self.env.now
priority = 1 if is_vip else 5
patience = random.expovariate(1/self.config['mean_patience'])
record = {
'call_id': call_id,
'type': call_type,
'vip': is_vip,
'arrival': arrival
}
with self.agents.request(priority=priority) as req:
result = yield req | self.env.timeout(patience)
if req in result:
record['answered'] = True
record['wait'] = self.env.now - arrival
record['answered_in_target'] = record['wait'] <= self.config['target_wait']
# Handle time varies by type
handle_times = {'sales': 8, 'support': 10, 'billing': 5}
mean_handle = handle_times.get(call_type, 7)
handle = random.expovariate(1/mean_handle)
yield self.env.timeout(handle)
record['handle_time'] = handle
else:
record['answered'] = False
record['wait'] = patience
self.stats['calls'].append(record)
def arrivals(self):
call_id = 0
call_types = ['sales', 'support', 'billing']
type_probs = [0.3, 0.5, 0.2]
while True:
# Time-varying arrival rate
hour = (self.env.now / 60) % 24
if 9 <= hour <= 17:
rate = self.config['peak_rate']
else:
rate = self.config['off_peak_rate']
yield self.env.timeout(random.expovariate(rate))
call_type = random.choices(call_types, weights=type_probs)[0]
is_vip = random.random() < 0.1 # 10% VIP
self.env.process(self.call(call_id, call_type, is_vip))
call_id += 1
def monitor(self, interval=5):
while True:
self.stats['queue_lengths'].append({
'time': self.env.now,
'queue': len(self.agents.queue),
'busy': self.agents.count
})
yield self.env.timeout(interval)
def run(self, duration):
self.env.process(self.arrivals())
self.env.process(self.monitor())
self.env.run(until=duration)
def report(self):
calls = self.stats['calls']
answered = [c for c in calls if c['answered']]
abandoned = [c for c in calls if not c['answered']]
print("\n=== Call Centre Report ===")
print(f"Total calls: {len(calls)}")
print(f"Answered: {len(answered)}")
print(f"Abandoned: {len(abandoned)}")
print(f"Abandonment rate: {len(abandoned)/len(calls):.1%}")
if answered:
waits = [c['wait'] for c in answered]
print(f"\nWait time (answered):")
print(f" Mean: {np.mean(waits):.1f} mins")
print(f" 90th percentile: {np.percentile(waits, 90):.1f} mins")
in_target = [c for c in answered if c['answered_in_target']]
print(f"\nService level (target {self.config['target_wait']} mins):")
print(f" {len(in_target)/len(answered):.1%}")
# Run
random.seed(42)
env = simpy.Environment()
cc = FullCallCentre(env, {
'agents': 15,
'peak_rate': 2, # 2 calls/min at peak
'off_peak_rate': 0.5,
'mean_patience': 5, # 5 min average patience
'target_wait': 0.5 # 30 second target
})
cc.run(duration=480) # 8 hours
cc.report()
Summary
Call centre simulation captures:
- Variable arrival rates and call types
- Agent skills and priorities
- Customer patience and abandonment
- Service level metrics
- Staffing optimisation
Model the chaos. Meet your SLAs.

