นี่เป็นหนึ่งในการจำลองที่ให้คำแนะนำและสนุกสนานที่สุด:คุณสร้างตัวแทนอิสระในคอมพิวเตอร์ให้พวกเขามีปฏิสัมพันธ์โต้ตอบติดตามสิ่งที่พวกเขาทำและศึกษาสิ่งที่เกิดขึ้น มันเป็นวิธีที่ยอดเยี่ยมในการเรียนรู้เกี่ยวกับระบบที่ซับซ้อนโดยเฉพาะอย่างยิ่ง (แต่ไม่ จำกัด เพียง) ระบบที่ไม่สามารถเข้าใจได้ด้วยการวิเคราะห์ทางคณิตศาสตร์อย่างหมดจด
วิธีที่ดีที่สุดในการสร้างแบบจำลองดังกล่าวคือด้วยการออกแบบจากบนลงล่าง
ในระดับสูงสุดรหัสควรมีลักษณะเหมือน
initialize(...)
while (process(get.next.event())) {}
(นี่และตัวอย่างที่ตามมาทั้งหมดเป็นรหัสที่ปฏิบัติการ R
ได้ไม่ใช่แค่รหัสหลอก) การวนซ้ำเป็นการจำลองเหตุการณ์ที่ขับเคลื่อนด้วย : get.next.event()
ค้นหา "เหตุการณ์" ที่น่าสนใจและส่งคำอธิบายของมันไปยังprocess
ซึ่งทำอะไรกับมัน (รวมถึงการบันทึกใด ๆ ข้อมูลเกี่ยวกับมัน) มันกลับมาตราบTRUE
ใดที่สิ่งต่าง ๆ ทำงานได้ดี ในการระบุข้อผิดพลาดหรือจุดสิ้นสุดของการจำลองมันจะส่งกลับFALSE
, สิ้นสุดการวนซ้ำ
หากเรานึกภาพการใช้คิวนี้เช่นคนที่รอใบขับขี่ในนิวยอร์กซิตี้หรือขอใบขับขี่หรือตั๋วรถไฟเกือบทุกที่เรานึกถึงตัวแทนสองประเภท: ลูกค้าและ "ผู้ช่วย" (หรือเซิร์ฟเวอร์) . ลูกค้าประกาศตัวเองโดยแสดง; ผู้ช่วยประกาศความพร้อมของพวกเขาโดยการเปิดไฟหรือลงนามหรือเปิดหน้าต่าง นี่เป็นเหตุการณ์สองประเภทที่ต้องดำเนินการ
สภาพแวดล้อมในอุดมคติสำหรับการจำลองนั้นเป็นสภาพแวดล้อมจริงที่วัตถุไม่แน่นอนซึ่งสามารถเปลี่ยนสถานะเพื่อตอบสนองต่อสิ่งต่าง ๆ รอบตัวได้อย่างอิสระ R
มันแย่มากสำหรับเรื่องนี้ (แม้แต่ Fortran ก็ยังดีกว่า!) อย่างไรก็ตามเรายังสามารถใช้งานได้หากเราใช้ความระมัดระวัง เคล็ดลับคือการรักษาข้อมูลทั้งหมดในชุดโครงสร้างข้อมูลทั่วไปที่สามารถเข้าถึงได้ (และแก้ไข) โดยขั้นตอนการโต้ตอบที่แยกกันจำนวนมาก ฉันจะรับรองระเบียบการใช้ชื่อตัวแปรใน CAPS ทั้งหมดสำหรับข้อมูลดังกล่าว
ระดับถัดไปprocess
ของการออกแบบจากบนลงล่างเป็นรหัส มันตอบสนองต่อ descriptor เหตุการณ์เดียวe
:
process <- function(e) {
if (is.null(e)) return(FALSE)
if (e$type == "Customer") {
i <- find.assistant(e$time)
if (is.null(i)) put.on.hold(e$x, e$time) else serve(i, e$x, e$time)
} else {
release.hold(e$time)
}
return(TRUE)
}
มันจะต้องตอบสนองต่อเหตุการณ์ที่เป็นโมฆะเมื่อget.next.event
ไม่มีเหตุการณ์ที่ต้องรายงาน มิฉะนั้นจะprocess
ใช้ "กฎเกณฑ์ทางธุรกิจ" ของระบบ มันเขียนตัวเองจากคำอธิบายในคำถาม วิธีการทำงานควรมีความคิดเห็นเล็ก ๆ น้อย ๆ ยกเว้นเพื่อชี้ให้เห็นว่าในที่สุดเราจะต้องโค้ดรูทีนย่อยput.on.hold
และrelease.hold
(การนำคิวการถือครองลูกค้า) และserve
(การนำการโต้ตอบระหว่างผู้ช่วยลูกค้าไปใช้)
"กิจกรรม" คืออะไร มันจะต้องมีข้อมูลเกี่ยวกับผู้ที่จะทำหน้าที่, สิ่งที่ชนิดของการกระทำพวกเขาจะพาและเมื่อมันเกิดขึ้น รหัสของฉันจึงใช้รายการที่มีข้อมูลสามประเภทนี้ อย่างไรก็ตามget.next.event
จำเป็นต้องตรวจสอบเวลาเท่านั้น มันมีหน้าที่รับผิดชอบในการรักษาลำดับเหตุการณ์ที่เกิดขึ้นเท่านั้น
เหตุการณ์ใด ๆ ที่สามารถใส่ลงในคิวเมื่อได้รับและ
เหตุการณ์ที่เร็วที่สุดในคิวสามารถดึงและส่งผ่านไปยังผู้โทรได้อย่างง่ายดาย
การดำเนินงานที่ดีที่สุดนี้คิวลำดับความสำคัญจะเป็นกองR
แต่ที่มากเกินไปในจุกจิก ทำตามคำแนะนำในการเขียนโปรแกรมศิลปะของนอร์แมน Matloff (ผู้เสนอการจำลองคิวที่มีความยืดหยุ่นมากขึ้น แต่ จำกัด คิว) ฉันได้ใช้กรอบข้อมูลเพื่อจัดกิจกรรมและค้นหาเวลาขั้นต่ำในบันทึกของมัน
get.next.event <- function() {
if (length(EVENTS$time) <= 0) new.customer() # Wait for a customer$
if (length(EVENTS$time) <= 0) return(NULL) # Nothing's going on!$
if (min(EVENTS$time) > next.customer.time()) new.customer()# See text
i <- which.min(EVENTS$time)
e <- EVENTS[i, ]; EVENTS <<- EVENTS[-i, ]
return (e)
}
มีหลายวิธีที่สามารถเข้ารหัสได้ รุ่นสุดท้ายที่แสดงที่นี่สะท้อนให้เห็นถึงทางเลือกที่ฉันทำในการเขียนรหัสว่าprocess
ปฏิกิริยาตอบสนองต่อเหตุการณ์ "ผู้ช่วย" และวิธีการnew.customer
ทำงาน: get.next.event
เพียงนำลูกค้าออกจากคิวการพักไว้จากนั้นจึงนั่งรออีกเหตุการณ์หนึ่ง บางครั้งมันจำเป็นต้องมองหาลูกค้าใหม่ในสองวิธี: อันดับแรกเพื่อดูว่ามีใครรออยู่ที่หน้าประตู (ตามเดิม) และที่สองหรือไม่ว่ามีลูกค้าเข้ามาบ้างหรือไม่เมื่อเราไม่ได้มองหา
ชัดเจนnew.customer
และnext.customer.time
เป็นกิจวัตรที่สำคัญดังนั้นเรามาดูแลพวกเขาต่อไป
new.customer <- function() {
if (CUSTOMER.COUNT < dim(CUSTOMERS)[2]) {
CUSTOMER.COUNT <<- CUSTOMER.COUNT + 1
insert.event(CUSTOMER.COUNT, "Customer",
CUSTOMERS["Arrived", CUSTOMER.COUNT])
}
return(CUSTOMER.COUNT)
}
next.customer.time <- function() {
if (CUSTOMER.COUNT < dim(CUSTOMERS)[2]) {
x <- CUSTOMERS["Arrived", CUSTOMER.COUNT]
} else {x <- Inf}
return(x) # Time when the next customer will arrive
}
CUSTOMERS
คืออาร์เรย์ 2 มิติที่มีข้อมูลสำหรับลูกค้าแต่ละรายในคอลัมน์ มันมีสี่แถว (ทำหน้าที่เป็นสาขา) ที่อธิบายลูกค้าและบันทึกประสบการณ์ของพวกเขาในระหว่างการจำลอง : "ถึงแล้ว", "เสิร์ฟ", "ระยะเวลา" และ "ผู้ช่วย" (ตัวระบุตัวเลขที่เป็นบวกของผู้ช่วยถ้ามี และ-1
สำหรับสัญญาณไม่ว่าง) ในการจำลองที่มีความยืดหยุ่นสูงคอลัมน์เหล่านี้จะถูกสร้างขึ้นแบบไดนามิก แต่เนื่องจากวิธีR
การทำงานมันสะดวกที่จะสร้างลูกค้าทั้งหมดในตอนแรกในเมทริกซ์ขนาดใหญ่เพียงครั้งเดียวโดยมีเวลามาถึงแล้วโดยการสุ่ม next.customer.time
สามารถดูที่คอลัมน์ถัดไปของเมทริกซ์นี้เพื่อดูว่าใครกำลังมา ตัวแปรทั่วโลกCUSTOMER.COUNT
หมายถึงลูกค้าคนสุดท้ายที่จะมาถึง ลูกค้าได้รับการจัดการอย่างง่ายดายผ่านตัวชี้นี้ซึ่งจะทำให้ลูกค้าใหม่ได้รับและมองไปไกลกว่านั้นโดยไม่ต้องมองหาลูกค้ารายต่อไป
serve
ใช้กฎทางธุรกิจในการจำลอง
serve <- function(i, x, time.now) {
#
# Serve customer `x` with assistant `i`.
#
a <- ASSISTANTS[i, ]
r <- rexp(1, a$rate) # Simulate the duration of service
r <- round(r, 2) # (Make simple numbers)
ASSISTANTS[i, ]$available <<- time.now + r # Update availability
#
# Log this successful service event for later analysis.
#
CUSTOMERS["Assistant", x] <<- i
CUSTOMERS["Served", x] <<- time.now
CUSTOMERS["Duration", x] <<- r
#
# Queue the moment the assistant becomes free, so they can check for
# any customers on hold.
#
insert.event(i, "Assistant", time.now + r)
if (VERBOSE) cat(time.now, ": Assistant", i, "is now serving customer",
x, "until", time.now + r, "\n")
return (TRUE)
}
ตรงไปตรงมานี้ ASSISTANTS
คือ dataframe ที่มีสองฟิลด์: capabilities
(ระบุอัตราการให้บริการ) และavailable
ซึ่งจะตั้งค่าสถานะในครั้งถัดไปที่ผู้ช่วยจะว่าง ลูกค้าจะได้รับบริการโดยการสร้างระยะเวลาการให้บริการแบบสุ่มตามความสามารถของผู้ช่วยอัปเดตเวลาที่ผู้ช่วยคนต่อไปพร้อมใช้งานและบันทึกช่วงเวลาบริการในCUSTOMERS
โครงสร้างข้อมูล การVERBOSE
ตั้งค่าสถานะมีประโยชน์สำหรับการทดสอบและการดีบัก: เมื่อเป็นจริงจะปล่อยกระแสประโยคภาษาอังกฤษที่อธิบายจุดการประมวลผลที่สำคัญ
วิธีการกำหนดผู้ช่วยให้กับลูกค้ามีความสำคัญและน่าสนใจ เราสามารถจินตนาการได้ว่ามีหลายขั้นตอน: การมอบหมายโดยการสุ่มโดยการสั่งซื้อคงที่หรือตามผู้ที่ได้รับเวลาฟรี (หรือสั้นที่สุด) ที่ยาวที่สุด หลายสิ่งเหล่านี้แสดงอยู่ในรหัสที่แสดงความคิดเห็น:
find.assistant <- function(time.now) {
j <- which(ASSISTANTS$available <= time.now)
#if (length(j) > 0) {
# i <- j[ceiling(runif(1) * length(j))]
#} else i <- NULL # Random selection
#if (length(j) > 0) i <- j[1] else i <- NULL # Pick first assistant
#if (length(j) > 0) i <- j[length(j)] else i <- NULL # Pick last assistant
if (length(j) > 0) {
i <- j[which.min(ASSISTANTS[j, ]$available)]
} else i <- NULL # Pick most-rested assistant
return (i)
}
ส่วนที่เหลือของการจำลองเป็นเพียงการออกกำลังกายตามปกติในการโน้มน้าวใจR
ให้ใช้โครงสร้างข้อมูลมาตรฐานส่วนใหญ่จะเป็นบัฟเฟอร์แบบวงกลมสำหรับคิวที่รอสาย เพราะคุณไม่ต้องการที่จะทำงานอาละวาดกับ Globals sim
ผมใส่สิ่งเหล่านี้เป็นขั้นตอนเดียว ข้อโต้แย้งอธิบายถึงปัญหา: จำนวนลูกค้าที่จะจำลอง ( n.events
), อัตราการมาถึงของลูกค้า, ความสามารถของผู้ช่วยและขนาดของคิวการพัก (ซึ่งสามารถกำหนดให้เป็นศูนย์เพื่อกำจัดคิวทั้งหมด)
r <- sim(n.events=250, arrival.rate=60/45, capabilities=1:5/10, hold.queue.size=10)
ส่งคืนรายการโครงสร้างข้อมูลที่เก็บรักษาไว้ในระหว่างการจำลอง หนึ่งในสิ่งที่น่าสนใจที่สุดคือCUSTOMERS
อาร์เรย์ R
ทำให้ค่อนข้างง่ายในการพล็อตข้อมูลสำคัญในอาเรย์นี้ในวิธีที่น่าสนใจ นี่คือผลลัพธ์หนึ่งรายการที่แสดงลูกค้ารายสุดท้ายในการจำลองลูกค้าอีกราย25050250
ประสบการณ์ของลูกค้าแต่ละคนถูกพล็อตเป็นเส้นเวลาในแนวนอนโดยมีสัญลักษณ์วงกลมในเวลาที่มาถึงเส้นทึบสีดำสำหรับรอสายและเส้นสีในช่วงระยะเวลาของการโต้ตอบกับผู้ช่วย (สีและชนิดของเส้น แยกความแตกต่างระหว่างผู้ช่วย) ใต้พล็อตลูกค้านี้เป็นหนึ่งในการแสดงประสบการณ์ของผู้ช่วยทำเครื่องหมายเวลาที่พวกเขาและไม่ได้มีส่วนร่วมกับลูกค้า จุดสิ้นสุดของแต่ละช่วงเวลาของกิจกรรมถูกคั่นด้วยแถบแนวตั้ง
เมื่อรันด้วยverbose=TRUE
เอาต์พุตข้อความของการจำลองจะมีลักษณะดังนี้:
...
160.71 : Customer 211 put on hold at position 1
161.88 : Customer 212 put on hold at position 2
161.91 : Assistant 3 is now serving customer 213 until 163.24
161.91 : Customer 211 put on hold at position 2
162.68 : Assistant 4 is now serving customer 212 until 164.79
162.71 : Assistant 5 is now serving customer 211 until 162.9
163.51 : Assistant 5 is now serving customer 214 until 164.05
...
(ตัวเลขที่ด้านซ้ายเป็นครั้งแต่ละข้อความถูกปล่อยออกมา.) คุณสามารถจับคู่คำอธิบายเหล่านี้บางส่วนของลูกค้าที่พล็อตครั้งนอนอยู่ระหว่างและ165165160165
เราสามารถศึกษาประสบการณ์การพักของลูกค้าโดยการวางแผนระยะเวลาการพักโดยใช้ตัวระบุลูกค้าโดยใช้สัญลักษณ์พิเศษ (สีแดง) เพื่อแสดงให้ลูกค้าได้รับสัญญาณไม่ว่าง
(แผนการทั้งหมดนี้จะไม่สร้างแดชบอร์ดตามเวลาจริงที่ยอดเยี่ยมสำหรับใครก็ตามที่จัดการคิวบริการนี้!)
sim
มันเป็นที่น่าสนใจเพื่อเปรียบเทียบแปลงและสถิติที่คุณได้รับเมื่อแตกต่างกันพารามิเตอร์ที่ส่งผ่านไปยัง จะเกิดอะไรขึ้นเมื่อลูกค้ามาถึงเร็วเกินกว่าที่จะประมวลผล จะเกิดอะไรขึ้นเมื่อคิวการพักถูกทำให้เล็กลงหรือถูกกำจัด? การเปลี่ยนแปลงเมื่อผู้ช่วยจะถูกเลือกในลักษณะที่แตกต่างกันอย่างไร ตัวเลขและความสามารถของผู้ช่วยมีอิทธิพลต่อประสบการณ์ของลูกค้าอย่างไร อะไรคือจุดสำคัญที่ลูกค้าบางคนเริ่มหันเหความสนใจหรือเริ่มถูกพักไว้เป็นเวลานาน?
โดยปกติสำหรับคำถามที่ศึกษาด้วยตนเองอย่างชัดเจนเช่นนี้เราจะหยุดที่นี่และปล่อยให้รายละเอียดที่เหลือเป็นแบบฝึกหัด อย่างไรก็ตามฉันไม่ต้องการผิดหวังผู้อ่านที่อาจได้รับสิ่งนี้มาไกลและมีความสนใจที่จะลองใช้ด้วยตัวเอง (และอาจแก้ไขและสร้างมันเพื่อวัตถุประสงค์อื่น ๆ ) ดังนั้นสิ่งที่แนบมาด้านล่างนี้คือรหัสการทำงานที่สมบูรณ์
TEX$
sim <- function(n.events, verbose=FALSE, ...) {
#
# Simulate service for `n.events` customers.
#
# Variables global to this simulation (but local to the function):
#
VERBOSE <- verbose # When TRUE, issues informative message
ASSISTANTS <- list() # List of assistant data structures
CUSTOMERS <- numeric(0) # Array of customers that arrived
CUSTOMER.COUNT <- 0 # Number of customers processed
EVENTS <- list() # Dynamic event queue
HOLD <- list() # Customer on-hold queue
#............................................................................#
#
# Start.
#
initialize <- function(arrival.rate, capabilities, hold.queue.size) {
#
# Create common data structures.
#
ASSISTANTS <<- data.frame(rate=capabilities, # Service rate
available=0 # Next available time
)
CUSTOMERS <<- matrix(NA, nrow=4, ncol=n.events,
dimnames=list(c("Arrived", # Time arrived
"Served", # Time served
"Duration", # Duration of service
"Assistant" # Assistant id
)))
EVENTS <<- data.frame(x=integer(0), # Assistant or customer id
type=character(0), # Assistant or customer
time=numeric(0) # Start of event
)
HOLD <<- list(first=1, # Index of first in queue
last=1, # Next available slot
customers=rep(NA, hold.queue.size+1))
#
# Generate all customer arrival times in advance.
#
CUSTOMERS["Arrived", ] <<- cumsum(round(rexp(n.events, arrival.rate), 2))
CUSTOMER.COUNT <<- 0
if (VERBOSE) cat("Started.\n")
return(TRUE)
}
#............................................................................#
#
# Dispatching.
#
# Argument `e` represents an event, consisting of an assistant/customer
# identifier `x`, an event type `type`, and its time of occurrence `time`.
#
# Depending on the event, a customer is either served or an attempt is made
# to put them on hold.
#
# Returns TRUE until no more events occur.
#
process <- function(e) {
if (is.null(e)) return(FALSE)
if (e$type == "Customer") {
i <- find.assistant(e$time)
if (is.null(i)) put.on.hold(e$x, e$time) else serve(i, e$x, e$time)
} else {
release.hold(e$time)
}
return(TRUE)
}#$
#............................................................................#
#
# Event queuing.
#
get.next.event <- function() {
if (length(EVENTS$time) <= 0) new.customer()
if (length(EVENTS$time) <= 0) return(NULL)
if (min(EVENTS$time) > next.customer.time()) new.customer()
i <- which.min(EVENTS$time)
e <- EVENTS[i, ]; EVENTS <<- EVENTS[-i, ]
return (e)
}
insert.event <- function(x, type, time.occurs) {
EVENTS <<- rbind(EVENTS, data.frame(x=x, type=type, time=time.occurs))
return (NULL)
}
#
# Customer arrivals (called by `get.next.event`).
#
# Updates the customers pointer `CUSTOMER.COUNT` and returns the customer
# it newly points to.
#
new.customer <- function() {
if (CUSTOMER.COUNT < dim(CUSTOMERS)[2]) {
CUSTOMER.COUNT <<- CUSTOMER.COUNT + 1
insert.event(CUSTOMER.COUNT, "Customer",
CUSTOMERS["Arrived", CUSTOMER.COUNT])
}
return(CUSTOMER.COUNT)
}
next.customer.time <- function() {
if (CUSTOMER.COUNT < dim(CUSTOMERS)[2]) {
x <- CUSTOMERS["Arrived", CUSTOMER.COUNT]
} else {x <- Inf}
return(x) # Time when the next customer will arrive
}
#............................................................................#
#
# Service.
#
find.assistant <- function(time.now) {
#
# Select among available assistants.
#
j <- which(ASSISTANTS$available <= time.now)
#if (length(j) > 0) {
# i <- j[ceiling(runif(1) * length(j))]
#} else i <- NULL # Random selection
#if (length(j) > 0) i <- j[1] else i <- NULL # Pick first assistant
#if (length(j) > 0) i <- j[length(j)] else i <- NULL # Pick last assistant
if (length(j) > 0) {
i <- j[which.min(ASSISTANTS[j, ]$available)]
} else i <- NULL # Pick most-rested assistant
return (i)
}#$
serve <- function(i, x, time.now) {
#
# Serve customer `x` with assistant `i`.
#
a <- ASSISTANTS[i, ]
r <- rexp(1, a$rate) # Simulate the duration of service
r <- round(r, 2) # (Make simple numbers)
ASSISTANTS[i, ]$available <<- time.now + r # Update availability
#
# Log this successful service event for later analysis.
#
CUSTOMERS["Assistant", x] <<- i
CUSTOMERS["Served", x] <<- time.now
CUSTOMERS["Duration", x] <<- r
#
# Queue the moment the assistant becomes free, so they can check for
# any customers on hold.
#
insert.event(i, "Assistant", time.now + r)
if (VERBOSE) cat(time.now, ": Assistant", i, "is now serving customer",
x, "until", time.now + r, "\n")
return (TRUE)
}
#............................................................................#
#
# The on-hold queue.
#
# This is a cicular buffer implemented by an array and two pointers,
# one to its head and the other to the next available slot.
#
put.on.hold <- function(x, time.now) {
#
# Try to put customer `x` on hold.
#
if (length(HOLD$customers) < 1 ||
(HOLD$first - HOLD$last %% length(HOLD$customers) == 1)) {
# Hold queue is full, alas. Log this occurrence for later analysis.
CUSTOMERS["Assistant", x] <<- -1 # Busy signal
if (VERBOSE) cat(time.now, ": Customer", x, "got a busy signal.\n")
return(FALSE)
}
#
# Add the customer to the hold queue.
#
HOLD$customers[HOLD$last] <<- x
HOLD$last <<- HOLD$last %% length(HOLD$customers) + 1
if (VERBOSE) cat(time.now, ": Customer", x, "put on hold at position",
(HOLD$last - HOLD$first - 1) %% length(HOLD$customers) + 1, "\n")
return (TRUE)
}
release.hold <- function(time.now) {
#
# Pick up the next customer from the hold queue and place them into
# the event queue.
#
if (HOLD$first != HOLD$last) {
x <- HOLD$customers[HOLD$first] # Take the first customer
HOLD$customers[HOLD$first] <<- NA # Update the hold queue
HOLD$first <<- HOLD$first %% length(HOLD$customers) + 1
insert.event(x, "Customer", time.now)
}
}$
#............................................................................#
#
# Summaries.
#
# The CUSTOMERS array contains full information about the customer experiences:
# when they arrived, when they were served, how long the service took, and
# which assistant served them.
#
summarize <- function() return (list(c=CUSTOMERS, a=ASSISTANTS, e=EVENTS,
h=HOLD))
#............................................................................#
#
# The main event loop.
#
initialize(...)
while (process(get.next.event())) {}
#
# Return the results.
#
return (summarize())
}
#------------------------------------------------------------------------------#
#
# Specify and run a simulation.
#
set.seed(17)
n.skip <- 200 # Number of initial events to skip in subsequent summaries
system.time({
r <- sim(n.events=50+n.skip, verbose=TRUE,
arrival.rate=60/45, capabilities=1:5/10, hold.queue.size=10)
})
#------------------------------------------------------------------------------#
#
# Post processing.
#
# Skip the initial phase before equilibrium.
#
results <- r$c
ids <- (n.skip+1):(dim(results)[2])
arrived <- results["Arrived", ]
served <- results["Served", ]
duration <- results["Duration", ]
assistant <- results["Assistant", ]
assistant[is.na(assistant)] <- 0 # Was on hold forever
ended <- served + duration
#
# A detailed plot of customer experiences.
#
n.events <- length(ids)
n.assistants <- max(assistant, na.rm=TRUE)
colors <- rainbow(n.assistants + 2)
assistant.color <- colors[assistant + 2]
x.max <- max(results["Served", ids] + results["Duration", ids], na.rm=TRUE)
x.min <- max(min(results["Arrived", ids], na.rm=TRUE) - 2, 0)
#
# Lay out the graphics.
#
layout(matrix(c(1,1,2,2), 2, 2, byrow=TRUE), heights=c(2,1))
#
# Set up the customers plot.
#
plot(c(x.min, x.max), range(ids), type="n",
xlab="Time", ylab="Customer Id", main="Customers")
#
# Place points at customer arrival times.
#
points(arrived[ids], ids, pch=21, bg=assistant.color[ids], col="#00000070")
#
# Show wait times on hold.
#
invisible(sapply(ids, function(i) {
if (!is.na(served[i])) lines(x=c(arrived[i], served[i]), y=c(i,i))
}))
#
# More clearly show customers getting a busy signal.
#
ids.not.served <- ids[is.na(served[ids])]
ids.served <- ids[!is.na(served[ids])]
points(arrived[ids.not.served], ids.not.served, pch=4, cex=1.2)
#
# Show times of service, colored by assistant id.
#
invisible(sapply(ids.served, function(i) {
lines(x=c(served[i], ended[i]), y=c(i,i), col=assistant.color[i], lty=assistant[i])
}))
#
# Plot the histories of the assistants.
#
plot(c(x.min, x.max), c(1, n.assistants)+c(-1,1)/2, type="n", bty="n",
xlab="", ylab="Assistant Id", main="Assistants")
abline(h=1:n.assistants, col="#808080", lwd=1)
invisible(sapply(1:(dim(results)[2]), function(i) {
a <- assistant[i]
if (a > 0) {
lines(x=c(served[i], ended[i]), y=c(a, a), lwd=3, col=colors[a+2])
points(x=c(served[i], ended[i]), y=c(a, a), pch="|", col=colors[a+2])
}
}))
#
# Plot the customer waiting statistics.
#
par(mfrow=c(1,1))
i <- is.na(served)
plot(served - arrived, xlab="Customer Id", ylab="Minutes",
main="Service Wait Durations")
lines(served - arrived, col="Gray")
points(which(i), rep(0, sum(i)), pch=16, col="Red")
#
# Summary statistics.
#
mean(!is.na(served)) # Proportion of customers served
table(assistant)