R-ohjelmointi.org

Tilastotieteellistä ohjelmointia R-kielellä

Jonotusjärjestelmän tapahtumakohtainen simulointi simmer-paketilla



Yksinkertainen jonotusjärjestelmä on esimerkiksi kaupan kassajono. Monimutkaisempia jonotusjärjestelmiä ovat vaikkapa sairaalan poliklinikan moniammatillisen tiimin toiminta tai postin kuljetusketju. Tällaisten jonotusjärjestelmien toimintaa voidaan tutkia esimerkiksi simuloimalla.

Simulaatiomenetelmät

Simulointimenetelmät voidaan jakaa kahteen ryhmään, tapahtumakohtaisiin ja jatkuvatoimisiin. Tapahtumakohtaisessa simuloinnissa mallin tilan muutoksen aiheuttaa jokin tapahtuma. Jatkuvatoimisissa simulaatioissa mallin tilaa seurataan jatkuvasti esimerkiksi ajan funktiona.

Simulaatiomenetelmät voidaan jakaa myös stokastisiin ja deterministisiin menetelmiin. Stokastisissa menetelmissä mallin lopputulos voi erota mallin ajokertojen välillä, vaikka malli ajettaisiinkin samoilla lähtöarvoilla. Deterministisiin simulaatiomenetelmiin ei siälly tällaista epävarmuutta. Simmer-paketin tapahtumakohtainen simulointimenetelmä on stokastinen.

Kassajonon simulointi simmer-paketilla

Simmer-paketti löytyy CRAN:sta. Paketin tekninen toimintamalli on rakennettu magrittr-paketin mahdollistaman komentojen putkittamisen päälle.

Jos ajatellaan kassajonoa yksinkertaisena putkena, jonka päähän saapuu asiakkaita ostoksineen. Kassavirkailija ”piippaa” ostokset kassajärjestelmään, ja laskuttaa asiakasta. Oletetaan, että kassavirkailijalta kestää keskimäärin 2 minuuttia hoitaa asiakkaan kassatapahtuma. Koska palvelutapahtuman kesto on kuitenkin aavistuksen ennakoimaton, joten voimme mallintaa sitä vaikkapa normaalijakaumalla, jonka keskiarvo on 2 ja keskihajonta 1.

Simmer-paketille kassavirkailijan palvelutapahtuma kuvattaisiin seuraavasti:

t0 <- create_trajectory("kassajono") %>%
  seize("kassa", 1) %>%
  timeout(function() abs(rnorm(1, mean=2, sd=1))) %>%
  release("kassa", 1)

Kassavirkailija varataan (seize), hän suorittaa palvelutapahtuman (timeout), ja lopuksi kassavirkailija vapautuu (release). R-komentojen välissä olevat ’%>%’ ovat magrittr-paketin putkituksia, jotka ohjaavat edellisen komennon tuloksen seuraavan komennon syötteeksi.

Kun kassan toiminta on simuloitu, pitää vielä simuloida asiakkaiden saapuminen. Oletetaan vaikkapa, että kassalle saapuu uusia asiakas aina kahden minuutin välein. Tällöin voitaisiin ajaa 100 simulaatiota seuraavasti:

envs <- lapply(1:100, function(i) {
  simmer("KauppaSimu") %>%
  add_resource("kassa", 1) %>%
  add_generator("saapuminen", t0, function() rnorm(1, mean=2, sd=0)) %>%
  run(85)
})

Simulaatiossa kaupassa on vain yksi kassavirkailija (add_resource), ja asiakkaita saapuu siis kahden minuutin välein (add_generator). Lopuksi simulaatiota ajetaan 85 sykliä (run).

Funktion toimintaa voidaan havainnollistaa parhaiten muutamilla kaavioilla. Seuraavat komennot tuottavat kuvan, joka kuvaa kassavirkailijan varattuna oloaikaa, läpimenoaikaa, palvelutapahtuma-aikaa ja jonotusaikaa:

library(gridExtra)
X11()
p1<-plot_resource_utilization(envs, c("kassa"))
p2<-plot_evolution_arrival_times(envs, type = "flow_time")
p3<-plot_evolution_arrival_times(envs, type = "waiting_time")
p4<-plot_evolution_arrival_times(envs, type = "activity_time")
grid.arrange(p1, p2, p3, p4, ncol=2)

Komennot tuottavat seuraavan kuvan:
simmer_kuva1

Toimenpiteiden vaikutusten simulointi

Kun jonotusmalli on saatu määriteltyä, sen avulla voidaan tutkia erilaisten kassan toimintaan kohdistuvien toimenpiteiden vaikutusta vaikkapa asiakkaiden jonotus- tai palveluaikoihin. Testataanpa seuraavassa miten jonotus-ja palvelutapahtumien yhteenlaskettu aika (läpimenoaika) muuttuu, jos palvelutapahtuman hajonta pienenee tai jos kauppaan lisätään toinen kassavirkalija.

Simulaation koodi kaavioineen näyttää seuraavalta:

t0 <- create_trajectory("kassajono") %>%
  seize("kassa", 1) %>%
  timeout(function() abs(rnorm(1, mean=2, sd=1))) %>%
  release("kassa", 1)
envs <- lapply(1:100, function(i) {
  simmer("KauppaSimu") %>%
  add_resource("kassa", 1) %>%
  add_generator("saapuminen", t0, function() rnorm(1, mean=2, sd=0)) %>%
  run(85)
})
monitor_data1 <- envs %>% get_mon_arrivals() %>% dplyr::mutate(flow_time = end_time - start_time, waiting_time = flow_time - activity_time)
 
t0 <- create_trajectory("kassajono") %>%
  seize("kassa", 1) %>%
  timeout(function() abs(rnorm(1, mean=2, sd=0.5))) %>%
  release("kassa", 1)
envs <- lapply(1:100, function(i) {
  simmer("KauppaSimu") %>%
  add_resource("kassa", 1) %>%
  add_generator("saapuminen", t0, function() rnorm(1, mean=2, sd=0)) %>%
  run(85)
})
monitor_data2 <- envs %>% get_mon_arrivals() %>% dplyr::mutate(flow_time = end_time - start_time, waiting_time = flow_time - activity_time)
 
t0 <- create_trajectory("kassajono") %>%
  seize("kassa", 1) %>%
  timeout(function() abs(rnorm(1, mean=2, sd=1))) %>%
  release("kassa", 1)
envs <- lapply(1:100, function(i) {
  simmer("KauppaSimu") %>%
  add_resource("kassa", 2) %>%
  add_generator("saapuminen", t0, function() rnorm(1, mean=2, sd=0)) %>%
  run(85)
})
monitor_data3 <- envs %>% get_mon_arrivals() %>% dplyr::mutate(flow_time = end_time - start_time, waiting_time = flow_time - activity_time)
 
t0 <- create_trajectory("kassajono") %>%
  seize("kassa", 1) %>%
  timeout(function() abs(rnorm(1, mean=2, sd=0.5))) %>%
  release("kassa", 1)
envs <- lapply(1:100, function(i) {
  simmer("KauppaSimu") %>%
  add_resource("kassa", 2) %>%
  add_generator("saapuminen", t0, function() rnorm(1, mean=2, sd=0)) %>%
  run(85)
})
monitor_data4 <- envs %>% get_mon_arrivals() %>% dplyr::mutate(flow_time = end_time - start_time, waiting_time = flow_time - activity_time)
 
par(mfrow=c(2,2))
hist(monitor_data1$flow_time, main="palvelun mean=2.0, sd=1.0; kassoja=1", br=200, xlim=c(0,20), ylim=c(0, 150), col="black", las=1, xlab="Läpimenoaika")
abline(v=median(monitor_data1$flow_time), col="red", lwd=4)
hist(monitor_data2$flow_time, main="palvelun mean=2.0, sd=0.5; kassoja=1", br=100, xlim=c(0,20), ylim=c(0, 150), col="black", las=1, xlab="Läpimenoaika")
abline(v=median(monitor_data2$flow_time), col="red", lwd=4)
hist(monitor_data3$flow_time, main="palvelun mean=2.0, sd=1.0; kassoja=2", br=200, xlim=c(0,20), ylim=c(0, 150), col="black", las=1, xlab="Läpimenoaika")
abline(v=median(monitor_data3$flow_time), col="red", lwd=4)
hist(monitor_data4$flow_time, main="palvelun mean=2.0, sd=0.5; kassoja=2", br=100, xlim=c(0,20), ylim=c(0, 150), col="black", las=1, xlab="Läpimenoaika")
abline(v=median(monitor_data4$flow_time), col="red", lwd=4)

Tuloksista piirretyt läpimenoaikojen histogrammin näyttävät seuraavilta:

simmer_kuva2

Kuvista on helppo havaita, että esimerkiksi yhden kassan tilanteessa palveluajan hajonnan pienentäminen johtaa läpimenoaikojen lyhentymiseen. Samaan johtaa myös toisen kassan lisääminen kauppaan.

Mites tästä eteenpäin?

Vastaavalla tavalla voitaisiin simuloida myös monimutkaisempia, jopa haarautuvia jonotusjärjestelmiä. Simmer-paketin vignetit ja manuaali antavat hyviä esimerkkejä menetelmän soveltamisesta monimutkaisempiinkin tilanteisiin. Paketin hyödyntäminen esimerkiksi Lean Six Sigma -projektien yhteydessä vaikkapa pöytätestauksen rinnalla ja tavanomaisen arvovirta-analyysin apuna voisi antaa lisätietoa toimenpiteiden vaikuttavuudesta (lisätietoa kokemuksista Ruotsista).


Vastaa

Sähköpostiosoitettasi ei julkaista. Pakolliset kentät on merkitty *