In [1]:
import pandas as pd
import numpy as np
import random as rnd
import scipy.stats as stats
import scipy.optimize as opt
import json as json
import matplotlib as mpl
from math import exp
from matplotlib import pyplot as plt
from IPython.core.pylabtools import figsize
from IPython.display import display
from IPython.core.display import HTML
rnd.seed(2)
In [2]:
%matplotlib inline
figsize(15.5, 8)
pd.set_option("display.precision", 3)
np.set_printoptions(precision=4)
np.set_printoptions(suppress=True)
s = json.load( open("../styles/bmh_matplotlibrc.json") )
mpl.rcParams.update(s)
def css_styling():
styles = open("../styles/custom.css", "r").read()
return HTML(styles)
css_styling()
Out[2]:
In this paper, the agent observes each bus at a given time $t$, and makes a decision regarding the replacement of the engine or its maintenance as a function of the observed state of the engine (i.e. its mileage).
As in the original paper, we discretize the number of states into intervals of length 5000, and define a transition density which governs the evolution of mileage from one time period to another:
$$ p(x_{t+1} | x_{t} , i_{t} , \theta_{3}) = \left\{ \begin{array}{l l} g(x_{t+1} - x_{t}, \theta_{3}) & \quad \text{if } i_t = 0\\ g(x_{t+1} - 0, \theta_{3}) & \quad \text{if } i_t = 1 \end{array} \right.$$Here, the function g is defined by a multinomial distribution on the set {0, 1, 2}. In other words, the probability that a bus' mileage will increase by a value x between now and the next maintenance decision is:
The utility function of the decision maker for a single time period is:
$$ u(x_{t}, i , \theta_{1}) = \left\{ \begin{array}{l l} \qquad \enspace -c(x_t, \theta_1) + \epsilon_t(0)& \quad \text{if } i = 0\\ -RC -c(0, \theta_1) + \epsilon_t(1) & \quad \text{if } i = 1 \end{array} \right. \quad \text{(Errors are I.I.D. standard Gumbel)}$$The agent will chose the decision which maximizes its utility. If the agent is forward looking (i.e. if $\beta \neq 0$), he does not only maximize his utility in the present time period, but also his future utility discounted by $\beta$.
Since $\beta$ cannot be estimated without knowing the utility function, we will later set $\beta$ to its true value.
A functional form has to be imposed on the cost function $c(x_t)$.
In this recreation, we assume that the replacement cost is a constant $\text{RC}$, and allow the maintenance cost $\text{MC}(s, \theta_1)$ to be a linear, exponential or logarithmic function of the state of the bus scaled by a parameter $\theta_1$
The generation of a fake dataset can be done in two different ways:
The advantage of the first method is that this is closer to what is actually done in the paper: indeed, the focal goal of the paper is to recover RC and the cost scaling vector of parameters $\theta_1$.
If the decision of the agent is based on the discretized state (and not on the actual mileage), then this approach is not noisier than the second one.
For this reason, we will adopt this first approach, and assumes that the increase in mileage follows a truncated normal distribution bounded at [0, 15000]:
$$ (M_{t+1} - M_{t}) \sim \mathcal{N}_{Trunc}(6000, 4000) $$The parameters are arbitrarily chosen to ensure that the state transitions probabilities are plausible:
In [3]:
lower, upper = 0, 15000
mu, sigma = 6000, 4000
mileage_dist = stats.truncnorm((lower - mu) / sigma, (upper - mu) / sigma, loc=mu, scale=sigma)
Computing the state transitions probabilities:
In [4]:
p_x0 = mileage_dist.cdf(5000)
p_x1 = mileage_dist.cdf(10000) - p_x0
p_x2 = 1 - p_x1 - p_x0
p = (p_x0, p_x1, p_x2)
Plotting the state transitions probabilities:
In [5]:
x = np.linspace(0,150001,150001)
y = mileage_dist.pdf(x)
plt.plot(x, y)
plt.title("State transitions probabilities", fontsize=25)
plt.xlabel("Mileage")
plt.ylabel("Probabilies")
plt.text(750, 0.00001, "$P(x_t = 0) = {:.2f}$".format(p_x0), fontsize=20, weight="bold")
plt.text(5750, 0.00001, "$P(x_t = 1) = {:.2f}$".format(p_x1), fontsize=20, weight="bold")
plt.text(10500, 0.00001, "$P(x_t = 2) = {:.2f}$".format(p_x2), fontsize=20, weight="bold")
plt.fill_between(x, y, where=x>=0, facecolor='#F6E3CE', interpolate=True)
plt.fill_between(x, y, where=x>=5000, facecolor='#CEF6CE', interpolate=True)
plt.fill_between(x, y, where=x>=10000, facecolor='#CECEF6', interpolate=True)
plt.grid(False)
plt.xlim(0, 15000);
Between each inspection, each bus has:
In [6]:
rc = 20
theta1_1 = 0.5
theta1_2 = 0.01
beta = 0.75
In [7]:
def myopic_costs(S, MF, params, p):
"""
This function computes the myopic expected cost associated with each decision for each state,
and returns an array of state/decision costs.
Takes:
* An integer S, describing the possible states of the bus
* A maintenance cost function MF, which takes a vector of parameters and a `state' argument
* A vector params, to be supplied to the maintenance cost function MF. The first element of
the vector is the replacement cost rc.
* A (3x1) vector p describing the state transitions probabilities
Returns:
* A (Nx2) array containing the maintenance and replacement costs for the N possible states of the bus
"""
rc = params[0]
maint_cost = [MF(s, params[1:]) for s in range(0, S)]
repl_cost = [rc for state in range(0, S)]
return np.vstack((maint_cost, repl_cost)).T
We assume different possible forms for the maintenance cost function:
$$\text{A linear form: } \qquad \text{MF}(s, \theta) = \theta_1 s \\ \text{A quadratic form: } \qquad \text{MF}(s, \theta) = \theta_{1 1} s + \theta_{1 2}s^2 \\ \text{An exponential form: } \qquad \text{MF}(s, \theta) = \theta_{1 1}\exp(\theta_{1 2}s) \\ \text{A logarithmic form: } \qquad \text{MF}(s, \theta) = \theta_{1 1}\log(\theta_{1 2}s)$$
In [8]:
def lin_cost(s, params):
try:
theta1_1, = params
return s*theta1_1
except ValueError:
raise ValueError
print("Wrong number of parameters specified: expected 2, got {}".format(len(params)))
In [9]:
def quad_cost(s, params):
try:
theta1_1, theta1_2 = params
return s*theta1_1 + (s**2)*theta1_2
except ValueError:
raise ValueError
print("Wrong number of parameters specified: expected 2, got {}".format(len(params)))
In [10]:
def exp_cost(s, params):
try:
theta1_1, = params
return np.exp(s*theta1_1)
except ValueError:
raise ValueError
print("Wrong number of parameters specified: expected 1, got {}".format(len(params)))
In [11]:
def log_cost(s, params):
try:
theta1_1, theta1_2 = params
return np.log(theta1_1 + s*theta1_2)
except ValueError:
raise ValueError
print("Wrong number of parameters specified: expected 2, got {}".format(len(params)))
In [12]:
def choice_prob(cost_array):
"""
Returns the probability of each choice, conditional on an array of state/decision costs.
"""
S = cost_array.shape[0]
cost = cost_array - cost_array.min(1).reshape(S, -1)
util = np.exp(-cost)
pchoice = util/(np.sum(util, 1).reshape(S, -1))
return pchoice
In [13]:
def contraction_mapping(S, p, MF, params, beta=0.75, threshold=1e-6, suppr_output=False):
"""
Compute the non-myopic expected value of the agent for each possible decision and each possible
state of the bus.
Iterate until the difference in the previously obtained expected value and the new expected value
is smaller than a constant.
Takes:
* A finite number of states S
* A state-transition probability vector p = [p(0), p(1), p(2), ..., p(k)] of length k < N
* A maintenance cost function MF
* A vector params for the cost function
* A discount factor beta (optional)
* A convergence threshold (optional)
Returns:
* The converged choice probabilities for the forward-looking and myopic agents for each state,
conditional on `params'
"""
achieved = True
# Initialization of the state-transition matrices: describe the state-transition probabilities
# if the maintenance cost is incurred, and regenerate the state to 0 if the replacement cost
# is incurred.
ST_mat = np.zeros((S, S))
p = np.array(p)
for i in range(S):
for j, _p in enumerate(p):
if i + j < S-1:
ST_mat[i+j][i] = _p
elif i + j == S-1:
ST_mat[S-1][i] = p[j:].sum()
else:
pass
R_mat = np.vstack((np.ones((1, S)),np.zeros((S-1, S))))
# Initialization of the expected value (which is also the myopic
# decision cost of the agent). Here, the forward-looking component is initialized at 0.
k = 0
EV = np.zeros((S, 2))
EV_myopic = EV_new = myopic_costs(S, MF, params, p)
# Contraction mapping loop
while abs(EV_new-EV).max() > threshold:
# Store the former expected value
EV = EV_new
# Obtained the probability of maintenance and replacement from the former expected value
pchoice = choice_prob(EV)
# Compute the expected cost for each state: Nx1 vector
ecost = (pchoice*EV).sum(1)
# Compute the two components of forward-looking utility: In case of maintenance,
# utility of future states weighted by transition probabilities. In case of replacement,
# the future utility is the utility of state 0
futil_maint = np.dot(ecost, ST_mat)
futil_repl = np.dot(ecost, R_mat)
futil = np.vstack((futil_maint, futil_repl)).T
# Future utility is discounted by beta, and added to the myopic cost.
EV_new = EV_myopic + beta*futil
k += 1
if k == 1000:
achieved = False
break
if not suppr_output:
if achieved:
print("Convergence achieved in {} iterations".format(k))
else:
print("CM could not converge! Mean difference = {:.6f}".format((EV_new-EV).mean()))
return (choice_prob(EV_new), choice_prob(EV_myopic))
Here, we use the same coefficients as the one described earlier:
Finally, we assume that there are 70 discrete states for the bus' mileage (i.e. no bus has a mileage greater than 345,000 in the dataset).
In [14]:
params_lin = (rc, theta1_1)
p = (p_x0, p_x1, p_x2)
lin_forward, lin_myopic = contraction_mapping(S=70, p=p, MF=lin_cost, params=params_lin, beta = 0.75)
pchoice = lin_forward.T[0]
Plotting the choice probabilities of the Agents:
In [15]:
plt.plot(lin_forward.T[0])
plt.plot(lin_myopic.T[0])
plt.ylabel("Probability of maintenance")
plt.xlabel("State of the bus")
plt.legend(["Forward-Looking", "Myopic"])
plt.xlim(5, 50)
plt.title("Action probabilities of the Agents (Lin. Spec.)");
In [16]:
n_bus = 1000
bus_array = np.hstack((np.linspace(1, n_bus, n_bus).reshape(-1, 1), np.zeros((n_bus, 3))))
The dataset of buses transition from one period to another in the following way:
We define below the transition function and the decision function
In [17]:
def decide(s, pchoice):
"""
Make a decision to maintain or replace the bus, based on a probability of choice p
and a current state s
"""
return np.random.choice([0, 1], p=[pchoice[s], 1-pchoice[s]])
def transition(bus_array, p):
"""
Return the updated bus dataset after one decision of our agent.
Takes:
* bus_array : An array of buses, containing the identifier of the buses, their mileages, and their current
state.
* pchoice : The converged choice probabities of the agent making the decision
Returns:
* The updated dataset of buses, with the new decisions appended at the end of the dataframe.
"""
# Recovering the number of buses, the previous mileage and the previous states of the buses
n_bus = bus_array[:, 0].max()
prev_mileage = bus_array[-n_bus:, 2]
prev_states = bus_array[-n_bus:, 3]
# Generating choices from choice probabilities, conditional on the state of each bus
choices = np.array([decide(x, pchoice) for x in prev_states])
# Generating the new mileage and state
new_mileage = (1-choices)*prev_mileage + mileage_dist.rvs(size=n_bus)
new_states = np.floor(new_mileage/5000)
new_array = np.vstack((bus_array[-n_bus:, 0], np.zeros(n_bus), new_mileage, new_states))
bus_array[-n_bus:,1] = choices
return np.vstack((bus_array, new_array.T))
The complete dataset is generated by making the agent take a serie of decisions. Here, we assume that the agent takes 1000 consecutive decisions. The total number of data points thus generated will be equal to 100,000 (1000 buses and 100 time periods).
In [18]:
n_periods = 100
In [19]:
for i in range(n_periods):
bus_array = transition(bus_array, pchoice)
Finally, the array is stored into a Dataframe, to make plot and data exports more convenient.
In [20]:
lin_dataframe = pd.DataFrame(bus_array, columns=["Identifier", "Choice", "Mileage", "State"])
In [21]:
choice_freq = lin_dataframe.loc[:, ["Choice", "State"]].groupby(["State"]).mean().sort().reset_index()
plt.plot(choice_freq["State"], 1-choice_freq["Choice"])
plt.plot(pchoice, "--")
plt.xlabel("Observed state of the bus")
plt.ylabel("Probability of maintenance")
plt.legend(["Observed behavior", "True choice probabilities"])
plt.title("Behavior of forward-looking agent (Lin. Spec.)")
plt.xlim(5, 25)
plt.ylim(-0.05, 1.05);
In [22]:
lin_dataframe.loc[n_bus+1:,["Choice", "State"]].to_csv("Datasets\Lin_Dataset.csv", index=False)
In [23]:
params_quad = (rc, theta1_1, theta1_2)
p = (p_x0, p_x1, p_x2)
quad_forward, quad_myopic = contraction_mapping(S=70, p=p, MF=quad_cost, params=params_quad, beta = 0.75)
pchoice = quad_forward.T[0]
plt.plot(quad_forward.T[0])
plt.plot(quad_myopic.T[0])
plt.ylabel("Probability of maintenance")
plt.xlabel("State of the bus")
plt.legend(["Forward-Looking", "Myopic"])
plt.xlim(5, 35)
plt.title("Action probabilities of the Agents (Quad. Spec.)");
In [24]:
n_bus = 1000
bus_array = np.hstack((np.linspace(1, n_bus, n_bus).reshape(-1, 1), np.zeros((n_bus, 3))))
for i in range(n_periods):
bus_array = transition(bus_array, pchoice)
quad_dataframe = pd.DataFrame(bus_array, columns=["Identifier", "Choice", "Mileage", "State"])
choice_freq = quad_dataframe.loc[:, ["Choice", "State"]].groupby(["State"]).mean().sort().reset_index()
plt.plot(choice_freq["State"], 1-choice_freq["Choice"])
plt.plot(pchoice, "--")
plt.xlabel("Observed state of the bus")
plt.ylabel("Probability of maintenance")
plt.legend(["Observed behavior", "True choice probabilities"])
plt.title("Behavior of forward-looking agent (Quad. Spec.)")
plt.xlim(5, 20)
plt.ylim(-0.05, 1.05);
quad_dataframe.loc[n_bus+1:,["Choice", "State"]].to_csv("Datasets\Quad_Dataset.csv", index=False)
In [25]:
params_exp = (15, 0.05)
p = (p_x0, p_x1, p_x2)
exp_forward, exp_myopic = contraction_mapping(S=70, p=p, MF=exp_cost, params=params_exp, beta = 0.75)
pchoice = exp_forward.T[0]
plt.plot(exp_forward.T[0])
plt.plot(exp_myopic.T[0])
plt.ylabel("Probability of maintenance")
plt.xlabel("State of the bus")
plt.legend(["Forward-Looking", "Myopic"])
plt.xlim(20, 65)
plt.title("Action probabilities of the Agents (Exp. Spec.)");
In [26]:
n_bus = 1000
bus_array = np.hstack((np.linspace(1, n_bus, n_bus).reshape(-1, 1), np.zeros((n_bus, 3))))
for i in range(n_periods):
bus_array = transition(bus_array, pchoice)
exp_dataframe = pd.DataFrame(bus_array, columns=["Identifier", "Choice", "Mileage", "State"])
choice_freq = exp_dataframe.loc[:, ["Choice", "State"]].groupby(["State"]).mean().sort().reset_index()
plt.plot(choice_freq["State"], 1-choice_freq["Choice"])
plt.plot(pchoice, "--")
plt.xlabel("Observed state of the bus")
plt.ylabel("Probability of maintenance")
plt.legend(["Observed behavior", "True choice probabilities"])
plt.title("Behavior of forward-looking agent (Exp. Spec.)")
plt.xlim(20, 65)
plt.ylim(-0.05, 1.05);
exp_dataframe.loc[n_bus+1:,["Choice", "State"]].to_csv("Datasets\Exp_Dataset.csv", index=False)
In [27]:
params_log = (4, 1, 10)
p = (p_x0, p_x1, p_x2)
log_forward, log_myopic = contraction_mapping(S=70, p=p, MF=log_cost, params=params_log, beta = 0.75)
pchoice = log_forward.T[0]
plt.plot(log_forward.T[0])
plt.plot(log_myopic.T[0])
plt.ylabel("Probability of maintenance")
plt.xlabel("State of the bus")
plt.legend(["Forward-Looking", "Myopic"])
plt.xlim(0, 30)
plt.ylim(0, 1.1)
plt.title("Action probabilities of the Agents (Log. Spec.)");
In [28]:
n_bus = 1000
bus_array = np.hstack((np.linspace(1, n_bus, n_bus).reshape(-1, 1), np.zeros((n_bus, 3))))
for i in range(n_periods):
bus_array = transition(bus_array, pchoice)
log_dataframe = pd.DataFrame(bus_array, columns=["Identifier", "Choice", "Mileage", "State"])
choice_freq = log_dataframe.loc[:, ["Choice", "State"]].groupby(["State"]).mean().sort().reset_index()
plt.plot(choice_freq["State"], 1-choice_freq["Choice"])
plt.plot(pchoice, "--")
plt.xlabel("Observed state of the bus")
plt.ylabel("Probability of maintenance")
plt.legend(["Observed behavior", "True choice probabilities"])
plt.title("Behavior of forward-looking agent (Log. Spec.)")
plt.xlim(0, 30)
plt.ylim(0, 1.1);
log_dataframe.loc[n_bus+1:,["Choice", "State"]].to_csv("Datasets\Log_Dataset.csv", index=False)
In this section, we implement a Dynamic Utility Logit model, which will be our workbench for the estimation of the data. We wrap in this class the functions defined earlier (to avoid global references) and refactor some portions of the code (to make the model more flexible). The maintenance cost function is kept outside of the scope of the class, and must be supplied at the initialization of the object as an argument.
In [29]:
class DynamicLogit(object):
def __init__(self, data, Y, X, p, MF, npars):
"""
A statistics workbench used to evaluate the cost parameters underlying
a bus replacement pattern by a forward-looking agent.
Takes:
* Data: a Pandas dataframe, which contains:
-Y: the name of the column containing the dummy exogenous
variable (here, the choice)
-X: the name of the column containing the endogenous variable
(here, the state of the bus)
* p: The state-transition vector of endogenous variable.
For instance, p = [0, 0.6, 0.4] means that the bus will
transition to the next mileage state with probability 0.6,
and to the second next mileage state with probability 0.4.
* MF: A function passed as an argument, which is the functional
form for the maintenance cost. This function must accept as
a first argument a state s, and as a second argument a vector
of parameters.
* npars: The number of parameters to evalutate (i.e. the number of
parameters of the maintenance cost function, plus 1 for
the replacement cost)
"""
self.endog = data.loc[:, Y].values
self.exog = data.loc[:, X].values
self.N = self.endog.shape[0]
self.S = int(self.exog.max()*2) # Assumes that the true maximum number
# states is twice the maximum observed
# state.
# Check that p is a correct vector of probabilities (i.e. sums to 1)
p = np.array(p)
if p.sum() == 1:
self.p = p
else:
raise ValueError(("The probability of state transitions should add"
" up to 1!"))
# Check that the stated number of parameters correspond to the
# specifications of the maintenance cost function.
try:
MF(1, [0]*(npars-1))
except ValueError:
raise ValueError(("The number of parameters specified does not "
"match the specification of the maintenance cost"
" function!"))
else:
self.MF = MF
self.npars = npars
# To speed up computations and avoid loops when computing the log
# likelihood, we create a few useful matrices here:
S = self.S
# A (SxN) matrix indicating the state of each observation
self.state_mat = np.array([[self.exog[i]==s for i in range(self.N)]
for s in range(self.S)])
# A (SxS) matrix indicating the probability of a bus transitioning
# from a state s to a state s' (used to compute maintenance utility)
self.trans_mat = np.zeros((S, S))
for i in range(S):
for j, _p in enumerate(self.p):
if i + j < S-1:
self.trans_mat[i+j][i] = _p
elif i + j == S-1:
self.trans_mat[S-1][i] = p[j:].sum()
else:
pass
# A second (SxS) matrix which regenerates the bus' state to 0 with
# certainty (used to compute the replacement utility)
self.regen_mat = np.vstack((np.ones((1, S)),np.zeros((S-1, S))))
# A (2xN) matrix indicating with a dummy the decision taken by the agent
# for each time/bus observation (replace or maintain)
self.dec_mat = np.vstack(((1-self.endog), self.endog))
def myopic_costs(self, params):
S = self.S
"""
This function computes the myopic expected cost associated with each
decision for each state.
Takes:
* A vector params, to be supplied to the maintenance cost function
MF. The first element of the vector is the replacement cost rc.
Returns:
* A (Sx2) array containing the maintenance and replacement costs
for the S possible states of the bus
"""
rc = params[0]
thetas = params[1:]
maint_cost = [self.MF(s, thetas) for s in range(0, S)]
repl_cost = [rc for state in range(0, S)]
return np.vstack((maint_cost, repl_cost)).T
def fl_costs(self, params, beta=0.75, threshold=1e-6, suppr_output=False):
"""
Compute the non-myopic expected value of the agent for each possible
decision and each possible state of the bus, conditional on a vector of
parameters and on the maintenance cost function specified at the
initialization of the DynamicUtility model.
Iterates until the difference in the previously obtained expected value
and the new expected value is smaller than a constant.
Takes:
* A vector params for the cost function
* A discount factor beta (optional)
* A convergence threshold (optional)
* A boolean argument to suppress the output (optional)
Returns:
* An (Sx2) array of forward-looking costs associated with each
sate and each decision.
"""
achieved = True
# Initialization of the contraction mapping
k = 0
EV = np.zeros((self.S, 2))
self.EV_myopic = EV_new = self.myopic_costs(params)
# Contraction mapping Loop
while abs(EV_new-EV).max() > threshold:
EV = EV_new
pchoice = self.choice_prob(EV)
ecost = (pchoice*EV).sum(1)
futil_maint = np.dot(ecost, self.trans_mat)
futil_repl = np.dot(ecost, self.regen_mat)
futil = np.vstack((futil_maint, futil_repl)).T
EV_new = self.EV_myopic + beta*futil
k += 1
if k == 1000:
achieved = False
break
# Output:
if not suppr_output:
if achieved:
print("Convergence achieved in {} iterations".format(k))
else:
print("CM could not converge! Mean difference = {:.6f}".format(
(EV_new-EV).mean())
)
self.EV_FL = EV_new
return EV
def choice_prob(self, cost_array):
"""
Returns the probability of each choice for each observed state,
conditional on an array of state/decision costs (generated by the
myopic or forward-looking cost functions)
"""
cost = cost_array - cost_array.min(1).reshape(self.S, -1)
util = np.exp(-cost)
pchoice = util/(np.sum(util, 1).reshape(self.S, -1))
return pchoice
def loglike(self, params):
"""
The log-likelihood of the Dynamic model is estimated in several steps.
1°) The currenter parameters are supplied to the contraction mapping
function
2°) The function returns a matrix of decision probabilities for each
state.
3°) This matrix is used to compute the loglikelihood of the
observations
4°) The log-likelihood are then summed accross individuals, and
returned
"""
util = self.fl_costs(params, suppr_output=True)
pchoice = self.choice_prob(util)
logprob = np.log(np.dot(pchoice.T, self.state_mat))
return -np.sum(self.dec_mat*logprob)
def fit_likelihood(self, x0=None, bounds=None):
"""
Fit the parameters to the data.
"""
if bounds == None:
bounds = [(1e-6, None) for i in range(self.npars)]
if x0 == None:
x0 = [0.1 for i in range(self.npars)]
self.fitted = opt.fmin_l_bfgs_b(self.loglike, x0=x0, approx_grad=True,
bounds=bounds)
def get_parameters(self):
"""
Return the parameters obtained after fitting the likelihood function
to the data.
"""
return self.fitted[0]
def print_parameters(self):
loglike = self.fitted[1]
fit_params = self.get_parameters()
RC, thetas = fit_params[0], fit_params[1:]
logstring = "Log-likelihood = {0:.2f}".format(loglike)
thetas_string = ["theta1_{0} = {1:.4f}".format(i+1, t) \
for i, t in enumerate(thetas)]
thetas_string = ", ".join(thetas_string)
rc_string = "Parameters: RC = {0:.4f}".format(RC)
print(logstring, rc_string + ", " + thetas_string)
In this section, we fit the data generated by the linear cost function and recover the parameters $\text{RC}$ and $\theta$. We do it for different characterizations of the cost function, and thereby illustrate the consequences of a misspecification.
Recall that in the linear model:
$$\text{RC} = 20$$$$\theta_{11} = 0.5$$We start by initializing a DynamicLogit instance, and fit the likelihood function to the data.
In [30]:
lin_to_lin = DynamicLogit(lin_dataframe, "Choice", "State", p, lin_cost, npars=2)
lin_to_lin.fit_likelihood()
The results:
In [31]:
lin_to_lin.print_parameters()
In [32]:
lin_forward_app, lin_myopic_app = contraction_mapping(S=70, p=p,
MF=lin_cost, params=lin_to_lin.get_parameters(),
beta = 0.75)
In [33]:
plt.plot(lin_forward.T[0])
plt.plot(lin_myopic.T[0])
plt.plot(lin_forward_app.T[0])
plt.plot(lin_myopic_app.T[0])
plt.ylabel("Probability of maintenance")
plt.xlabel("State of the bus")
plt.legend(["Forward-Looking (Lin)", "Myopic (Lin)", "Forward-Looking (Lin, Est.)", "Myopic (Lin, Est.)", ])
plt.xlim(0, 70)
plt.title("Action probabilities of the Agents (Linear Cost Specification)");
In [34]:
quad_to_lin = DynamicLogit(lin_dataframe, "Choice", "State", p, quad_cost, npars=3)
quad_to_lin.fit_likelihood()
The results:
In [35]:
quad_to_lin.print_parameters()
The optimization procedures correctly brings $\theta_{12}$ to 0. As a consequence, this specification fits the data perfectly.
In [36]:
quad_forward_app, quad_myopic_app = contraction_mapping(S=70, p=p,
MF=quad_cost, params=quad_to_lin.get_parameters(),
beta = 0.75)
In [37]:
plt.plot(lin_forward.T[0])
plt.plot(lin_myopic.T[0])
plt.plot(quad_forward_app.T[0])
plt.plot(quad_myopic_app.T[0])
plt.ylabel("Probability of maintenance")
plt.xlabel("State of the bus")
plt.legend(["Forward-Looking (Lin)", "Myopic (Lin)", "Forward-Looking (Quad)", "Myopic (Quad)"])
plt.xlim(0, 70)
plt.title("Action probabilities of the Agents (Exponential Cost Specification)");
We initialize a third DynamicLogit instance, this time with our exponential cost specification
In [38]:
exp_to_lin = DynamicLogit(lin_dataframe, "Choice", "State", p, exp_cost, npars=2)
exp_to_lin.fit_likelihood(bounds=[(0, None), (0.1, None)])
The results:
In [39]:
exp_to_lin.print_parameters()
The myopic choice probabilities look different, but the forward-looking ones fit the data quite well.
In [40]:
exp_forward_app, exp_myopic_app = contraction_mapping(S=70, p=p,
MF=exp_cost, params=exp_to_lin.get_parameters(),
beta = 0.75)
In [41]:
plt.plot(lin_forward.T[0])
plt.plot(lin_myopic.T[0])
plt.plot(exp_forward_app.T[0])
plt.plot(exp_myopic_app.T[0])
plt.ylabel("Probability of maintenance")
plt.xlabel("State of the bus")
plt.legend(["Forward-Looking (Lin)", "Myopic (Lin)", "Forward-Looking (Exp)", "Myopic (Exp)"])
plt.xlim(0, 70)
plt.title("Action probabilities of the Agents (Exponential Cost Specification)");
We initialize a fourth DynamicLogit instance, this time with our log cost specification
In [42]:
log_to_lin = DynamicLogit(lin_dataframe, "Choice", "State", p, log_cost, npars=3)
log_to_lin.fit_likelihood(x0=[10, 10, 10])
The results:
In [43]:
log_to_lin.print_parameters()
In [44]:
log_forward_app, log_myopic_app = contraction_mapping(S=70, p=p, MF=log_cost,
params=log_to_lin.get_parameters(), beta = 0.75)
In this case, both the myopic and forward looking choice probabilities are very different from the true ones.
In [45]:
plt.plot(lin_forward.T[0])
plt.plot(lin_myopic.T[0])
plt.plot(log_forward_app.T[0])
plt.plot(log_myopic_app.T[0])
plt.ylabel("Probability of maintenance")
plt.xlabel("State of the bus")
plt.legend(["Forward-Looking (Lin)", "Myopic (Lin)", "Forward-Looking (Log)", "Myopic (Log)"])
plt.xlim(0, 70)
plt.title("Action probabilities of the Agents (Log Cost Specification)");
In this section, we fit the data generated by the quadratic, exponential, and logarithmic cost function and recover the parameters $\text{RC}$ and $\theta_{11}$ and $\theta_{12}$. We do not illustrate the impact of misspecification for those cost functions.
We start by initializing a DynamicLogit instance, and fit the likelihood function to the data.
Recall that in the quadratic model:
$$\text{RC} = 20$$$$\theta_{11} = 0.5$$$$\theta_{12} = 0.01$$
In [46]:
quad_to_quad = DynamicLogit(quad_dataframe, "Choice", "State", p, quad_cost, npars=3)
quad_to_quad.fit_likelihood(x0=[20, 1, 1])
The results:
In [47]:
quad_to_quad.print_parameters()
In [48]:
quad_forward_app, quad_myopic_app = contraction_mapping(S=70, p=p, MF=quad_cost,
params=quad_to_quad.get_parameters(), beta = 0.75)
In [49]:
plt.plot(quad_forward.T[0])
plt.plot(quad_myopic.T[0])
plt.plot(quad_forward_app.T[0])
plt.plot(quad_myopic_app.T[0])
plt.ylabel("Probability of maintenance")
plt.xlabel("State of the bus")
plt.legend(["Forward-Looking (Quad)", "Myopic (Quad)", "Forward-Looking (Quad, Est.)", "Myopic (Quad, Est.)"])
plt.xlim(5, 40)
plt.title("Action probabilities of the Agents (Linear Cost Specification)");
Recall that in the exponential model:
$$\text{RC} = 15$$$$\theta_{11} = 0.05$$
In [50]:
exp_to_exp = DynamicLogit(exp_dataframe, "Choice", "State", p, exp_cost, npars=2)
exp_to_exp.fit_likelihood()
The results:
In [51]:
exp_to_exp.print_parameters()
In [52]:
exp_forward_app, exp_myopic_app = contraction_mapping(S=70, p=p, MF=exp_cost,
params=exp_to_exp.get_parameters(), beta = 0.75)
In [53]:
plt.plot(exp_forward.T[0])
plt.plot(exp_myopic.T[0])
plt.plot(exp_forward_app.T[0])
plt.plot(exp_myopic_app.T[0])
plt.ylabel("Probability of maintenance")
plt.xlabel("State of the bus")
plt.legend(["Forward-Looking (Exp)", "Myopic (Exp)", "Forward-Looking (Exp, Est.)", "Myopic (Exp, Est.)"])
plt.xlim(20, 65)
plt.title("Action probabilities of the Agents (Linear Cost Specification)");
Recall that in the logarithmic model:
$$\text{RC} = 4$$$$\theta_{11} = 1$$$$\theta_{12} = 10$$
In [54]:
log_to_log = DynamicLogit(log_dataframe, "Choice", "State", p, log_cost, npars=3)
log_to_log.fit_likelihood()
The results:
In [55]:
log_to_log.print_parameters()
The parameters look different from the one used to generate the data...
In [56]:
log_forward_app, log_myopic_app = contraction_mapping(S=70, p=p, MF=log_cost,
params=log_to_log.get_parameters(), beta = 0.75)
In [57]:
plt.plot(log_forward.T[0])
plt.plot(log_myopic.T[0])
plt.plot(log_forward_app.T[0])
plt.plot(log_myopic_app.T[0])
plt.ylabel("Probability of maintenance")
plt.xlabel("State of the bus")
plt.legend(["Forward-Looking (Log)", "Myopic (Log)", "Forward-Looking (Log, Est.)", "Myopic (Log, Est.)"])
plt.xlim(0, 65)
plt.title("Action probabilities of the Agents (Linear Cost Specification)");
... but the fit of the data is excellent. This is because in this characterization of the cost function, the parameters recovered are algebrically equivalent to the initial parameters.