Home Ciw Hack to Get Resampling Routing Matrices
Post
Cancel

Ciw Hack to Get Resampling Routing Matrices

The Ciw library allows for a stochastic matrix representing probabilities of transitioning from one service to another. Here is an example from the documentation.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import ciw

N = ciw.create_network(
    arrival_distributions=[ciw.dists.Exponential(rate=0.3),
                           ciw.dists.Exponential(rate=0.2),
                           None],
    service_distributions=[ciw.dists.Exponential(rate=1.0),
                           ciw.dists.Exponential(rate=0.4),
                           ciw.dists.Exponential(rate=0.5)],
    routing=[[0.0, 0.3, 0.7],
             [0.0, 0.0, 1.0],
             [0.0, 0.0, 0.0]],
    number_of_servers=[1, 2, 2]
)

This example will only use a fixed stochastic matrix, however. If the probabilities were estimated from data or there is a priori uncertainty in the entries of the stochastic matrix then we might want to account for that.

Strictly-speaking, the Ciw library provides process-based routing customization that is extremely flexible. See this guide for how to use their process-based routing. But as a weird flex I would like to share a little hack to incorporate uncertainty into the stochastic matrices themselves.

We can make a subclass from list and provide methods such that it samples new values whenever it is accessed!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
import numpy as np

class RandomDirichletList(list):
    """
    List that contains and resamples from a Dirichlet distribution.

    This class represents a list-like structure that contains and
    resamples from a Dirichlet distribution. It is designed for
    working with probabilistic values that sum to 1, such as probability
    distributions. The Dirichlet distribution is used to generate
    such values.

    Attributes:
        alphas (list or array-like): The alpha parameters of the
            Dirichlet distribution. These parameters control the
            shape of the distribution.

    Methods:
        __init__(self, alphas):
            Initializes the RandomDirichletList with the provided alpha
            parameters. It ensures that the alpha parameters are positive
            and populates the list with Dirichlet-distributed values. The
            probabilities are adjusted to ensure they sum to 1.

        _renormalize_probs(self):
            Private method to renormalize the probabilities. It ensures
            that the probabilities sum to 1 according to the built-in
            sum function. This is important for working with certain
            libraries that require normalized probabilities.

        sample(self):
            Sample new values from the Dirichlet distribution and ensure
            they sum to 1. This method is used to initialize or reinitialize
            the probabilities in the list.

        __getitem__(self, index):
            Resamples from the Dirichlet distribution and returns the
            value at the specified index. This method updates the list
            with newly sampled values and then returns the requested
            item.

        __min__(self):
            Returns the minimum value in the list of probabilities.
            This method provides functionality similar to the built-in
            min() function for RandomDirichletList.

        __max__(self):
            Returns the maximum value in the list of probabilities.
            This method provides functionality similar to the built-in
            max() function for RandomDirichletList.

        __len__(self):
            Returns the length of the list, which corresponds to the
            number of elements in the probability distribution.

        __repr__(self):
            Returns a string representation of the RandomDirichletList
            object, displaying its current list of probabilities.
    """

    def __init__(self, alphas):

        if np.any(alphas) <= 0:
            raise ValueError("Alpha parameters must be positive")
        else:
            self.alphas = alphas

        self.sample()
        super().__init__(self.probs)

    def _renormalize_probs(self):
        """Sum renormalize probabilities.

        NumPy provides probabilities that are close to
        summing to one, but to work with Ciw they need
        to sum 1 according to the built-in sum function.
        """
        self.probs /= sum(self.probs)

    def sample(self):

        # Sample new values to get started
        self.probs = np.random.dirichlet(self.alphas)
        self._renormalize_probs()

        # If needed, resample until normalization passes
        while sum(self.probs) != 1.0:
            self.probs = np.random.dirichlet(self.alphas)
            self._renormalize_probs()

    def __getitem__(self, index):
        """Resample and then get item."""
        self.sample()
        return self.probs[index]

    def __min__(self):
        return min(self.probs)

    def __max__(self):
        return max(self.probs)

    def __len__(self):
        return len(self.probs)

    def __repr__(self):
        return f"RDL{self.probs}"

Here is an example of indexing the same element of the instance and yet sampling different values! This is achieved by resampling from a Dirichlet distribution every time __getitem__ is called.

1
2
3
4
>>> np.random.seed(seed=2018)
>>> rdl = RandomDirichletList([1.1, 2, 3])
>>> print([rdl[0] for i in range(10)])
[0.29042591447408683, 0.023680603962985814, 0.11617557745526234, 0.1422083243333615, 0.1914130984353422, 0.17498591805601912, 0.22902976104654874, 0.28693524142371135, 0.40673998074008844, 0.0788396847445936]

The RandomDirichletList has implementations of __min__, __max__, and __len__ to help reassure Ciw that it has the right properties to be a stochastic matrix.

However, sometimes np.random.dirichlet produces numbers that are merely ‘close’ to summing to unity. This can sometimes result in Ciw raising a ValueError. To tackle this I implemented sample and such that _renormalize_probs combined with rejection sampling should eventually produce a valid stochastic matrix for Ciw.

Here is an example of creating a list of instances of RandomDirichletList which Ciw accepts and uses in the simulation.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import ciw
import numpy
import pandas as pd

np.random.seed(seed=2018)

# Assume RandomDirichletList is in namespace

sm = [
    RandomDirichletList([1, 2, 3]),
    RandomDirichletList([3, 4, 5]),
    RandomDirichletList([6, 7, 8]),
]

N = ciw.create_network(
    arrival_distributions=[
        ciw.dists.Exponential(rate=0.3),
        ciw.dists.Exponential(rate=0.2),
        None,
    ],
    service_distributions=[
        ciw.dists.Exponential(rate=1.0),
        ciw.dists.Exponential(rate=0.4),
        ciw.dists.Exponential(rate=0.5),
    ],
    routing=sm,
    number_of_servers=[1, 2, 2],
)

sim = ciw.Simulation(N)
sim.simulate_until_max_time(10)

pd.DataFrame(
    sim.get_all_records()
    ).to_markdown('out.md')

Here is the output!

 id_numbercustomer_classoriginal_customer_classnodearrival_datewaiting_timeservice_start_dateservice_timeservice_end_datetime_blockedexit_datedestinationqueue_size_at_arrivalqueue_size_at_departureserver_idrecord_type
05CustomerCustomer16.5845806.584580.1665736.7511606.751161001service
15CustomerCustomer16.7511606.751160.7764177.5275707.527571001service
25CustomerCustomer17.5275707.527570.2916037.8191807.819182001service
36CustomerCustomer17.8602307.860230.1193577.9795907.979592001service
43CustomerCustomer13.8567403.856740.6344594.491204.49122001service
53CustomerCustomer24.49123.573648.064840.00205018.0668908.066892241service
67CustomerCustomer18.1292508.129250.5020568.6313108.631311001service
77CustomerCustomer18.6313108.631310.4094129.0407209.040722001service
84CustomerCustomer25.82772.239198.066890.18718.2539908.253993341service
94CustomerCustomer38.2539908.253991.197389.4513809.451382112service
101CustomerCustomer20.016603800.01660382.447582.4641902.464192001service
111CustomerCustomer22.4641902.464195.600658.0648408.064843051service

Have fun!

This post is licensed under CC BY 4.0 by the author.

Response To Dustin Fife On Rank-Based Nonparametric Statistics

Notes On Beautiful Python Refactoring Video