SearchLibrium 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- old_code/__init__.py +8 -0
- old_code/_choice_model.py +1363 -0
- old_code/_device.py +145 -0
- old_code/akshay_test.py +125 -0
- old_code/boxcox_functions.py +116 -0
- old_code/draws.py +128 -0
- old_code/harmony.py +1261 -0
- old_code/latent_class_constrained.py +434 -0
- old_code/latent_class_mixed_model.py +1566 -0
- old_code/latent_class_model.py +1281 -0
- old_code/latent_main.py +945 -0
- old_code/main.py +1880 -0
- old_code/main_ol.py +127 -0
- old_code/misc.py +303 -0
- old_code/mixed_logit.py +1553 -0
- old_code/multinomial_logit.py +559 -0
- old_code/ordered_logit.py +1641 -0
- old_code/ordered_logit_mixed.py +103 -0
- old_code/ordered_logit_multinomial.py +701 -0
- old_code/r_ordered.py +168 -0
- old_code/rrm.py +521 -0
- old_code/search.py +3485 -0
- old_code/siman.py +1023 -0
- old_code/threshold.py +777 -0
- searchlibrium-0.0.1.dist-info/METADATA +21 -0
- searchlibrium-0.0.1.dist-info/RECORD +28 -0
- searchlibrium-0.0.1.dist-info/WHEEL +5 -0
- searchlibrium-0.0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1281 @@
|
|
|
1
|
+
"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
|
|
2
|
+
IMPLEMENTATION: LATENT CLASS MODEL
|
|
3
|
+
"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
|
|
4
|
+
|
|
5
|
+
"""
|
|
6
|
+
BACKGROUND - LATENT CLASS MODEL
|
|
7
|
+
|
|
8
|
+
A latent class model is a statistical model used for analyzing categorical or
|
|
9
|
+
ordinal data, particularly when the observed data can be best explained by
|
|
10
|
+
assuming the existence of unobserved (latent) classes or groups within
|
|
11
|
+
the population. These models are widely used in various fields including
|
|
12
|
+
psychology, sociology, marketing, and epidemiology.
|
|
13
|
+
|
|
14
|
+
In a latent class model, each individual in the population is assumed to belong
|
|
15
|
+
to one of several unobserved classes or groups. The observed data are then
|
|
16
|
+
assumed to arise from the interaction between the latent class membership
|
|
17
|
+
and a set of observed variables.
|
|
18
|
+
|
|
19
|
+
Here are the key components of a latent class model:
|
|
20
|
+
|
|
21
|
+
Latent Classes: The model assumes the existence of unobserved (latent) classes or
|
|
22
|
+
groups within the population. These classes are not directly observable but are
|
|
23
|
+
inferred from patterns in the observed data. Each individual is assumed to
|
|
24
|
+
belong to one of these latent classes.
|
|
25
|
+
|
|
26
|
+
Observed Variables: The observed variables are the variables that are measured
|
|
27
|
+
directly from each individual in the dataset. These variables are used to
|
|
28
|
+
characterize the individuals and are assumed to be related to the latent class membership.
|
|
29
|
+
|
|
30
|
+
Model Parameters: The parameters of the latent class model include:
|
|
31
|
+
|
|
32
|
+
Class Membership Probabilities: The probabilities of belonging to each latent class.
|
|
33
|
+
|
|
34
|
+
Class-Specific Parameters: Parameters that describe the relationship between the
|
|
35
|
+
observed variables and the latent class membership for each class. These
|
|
36
|
+
parameters may include item response probabilities, regression coefficients, means,
|
|
37
|
+
variances, etc.
|
|
38
|
+
|
|
39
|
+
Model Estimation: Estimating the parameters of a latent class model involves
|
|
40
|
+
fitting the model to the observed data using statistical methods such as maximum
|
|
41
|
+
likelihood estimation (MLE), Bayesian estimation, or expectation-maximization
|
|
42
|
+
(EM) algorithm. The goal is to find the parameter values that maximize the
|
|
43
|
+
likelihood of observing the observed data given the latent class structure.
|
|
44
|
+
|
|
45
|
+
Model Interpretation: Once the model has been estimated, the parameters can be
|
|
46
|
+
interpreted to understand the characteristics of each latent class and the
|
|
47
|
+
relationships between the observed variables and the latent class membership.
|
|
48
|
+
|
|
49
|
+
Latent class models are particularly useful for uncovering hidden structures
|
|
50
|
+
or patterns in categorical or ordinal data and for identifying subgroups
|
|
51
|
+
within the population that may have distinct characteristics or behaviors.
|
|
52
|
+
They can be applied to various types of data, including survey responses,
|
|
53
|
+
diagnostic criteria, market segmentation, and more.
|
|
54
|
+
"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
''' ---------------------------------------------------------- '''
|
|
58
|
+
''' UNUSED LIBRARIES '''
|
|
59
|
+
''' ---------------------------------------------------------- '''
|
|
60
|
+
#import logging
|
|
61
|
+
|
|
62
|
+
''' ---------------------------------------------------------- '''
|
|
63
|
+
''' LIBRARIES '''
|
|
64
|
+
''' ---------------------------------------------------------- '''
|
|
65
|
+
try:
|
|
66
|
+
from . import misc
|
|
67
|
+
from ._device import device as dev
|
|
68
|
+
from .multinomial_logit import MultinomialLogit
|
|
69
|
+
from .boxcox_functions import truncate_higher, truncate_lower
|
|
70
|
+
except ImportError:
|
|
71
|
+
#print('Relative Import')
|
|
72
|
+
import misc
|
|
73
|
+
from _device import device as dev
|
|
74
|
+
from multinomial_logit import MultinomialLogit
|
|
75
|
+
from boxcox_functions import truncate_higher, truncate_lower
|
|
76
|
+
|
|
77
|
+
import time
|
|
78
|
+
import numpy as np
|
|
79
|
+
from scipy.optimize import minimize
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
''' ---------------------------------------------------------- '''
|
|
83
|
+
''' CONSTANTS - BOUNDS ON NUMERICAL VALUES '''
|
|
84
|
+
''' ---------------------------------------------------------- '''
|
|
85
|
+
max_exp_val, min_exp_val = 700, -700
|
|
86
|
+
max_comp_val, min_comp_val= 1e+300, 1e-300 # or float('inf')
|
|
87
|
+
|
|
88
|
+
''' ---------------------------------------------------------- '''
|
|
89
|
+
''' ERROR CHECKING AND LOGGING '''
|
|
90
|
+
''' ---------------------------------------------------------- '''
|
|
91
|
+
#logger = logging.getLogger(__name__)
|
|
92
|
+
|
|
93
|
+
''' ---------------------------------------------------------- '''
|
|
94
|
+
''' CLASS FOR ESTIMATION OF LATENT CLASS MODELS '''
|
|
95
|
+
''' ---------------------------------------------------------- '''
|
|
96
|
+
class LatentClassModel(MultinomialLogit):
|
|
97
|
+
# {
|
|
98
|
+
""" Docstring """
|
|
99
|
+
|
|
100
|
+
# ===================
|
|
101
|
+
# CLASS PARAMETERS
|
|
102
|
+
# ===================
|
|
103
|
+
|
|
104
|
+
"""
|
|
105
|
+
X: Input data for explanatory variables / long format / array-like
|
|
106
|
+
/ shape (n_samples, n_variables)
|
|
107
|
+
y: Choices / array-like / shape (n_samples,)
|
|
108
|
+
varnames: Names of explanatory variables / list / shape (n_variables,)
|
|
109
|
+
int num_classes: Number of latent classes
|
|
110
|
+
alts: List of alternative names or indexes / long format / array-like / shape (n_samples,)
|
|
111
|
+
isvars: Names of individual-specific variables in varnames / list
|
|
112
|
+
transvars: Names of variables to apply transformation on / list / default=None
|
|
113
|
+
transformation: Transformation to apply to transvars / string / default="boxcox"
|
|
114
|
+
ids: Identifiers for choice situations / long format / array-like / shape (n_samples,)
|
|
115
|
+
weights: Weights for the choice situations / long format / array-like
|
|
116
|
+
/ shape (n_variables,) / default=None
|
|
117
|
+
panels: Identifiers to create panels in combination with ids / array-like / long format
|
|
118
|
+
/ shape (n_samples,) / default=None
|
|
119
|
+
avail: Availability indicator of alternatives for the choices (1 => available, 0 otherwise)
|
|
120
|
+
/ array-like / shape (n_samples,)
|
|
121
|
+
base_alt: Base alternative / int, float or str / default=None
|
|
122
|
+
init_coeff: Initial coefficients for estimation/ numpy array / shape (n_variables,) / default=None
|
|
123
|
+
bool fit_intercept: Boolean indicator to include an intercept in the model / default=False
|
|
124
|
+
int maxiter: Maximum number of iterations / default=2000
|
|
125
|
+
float ftol: Termination tolerance for scipy.optimize.minimize / default=1e-5
|
|
126
|
+
float gtol: Termination tolerance for scipy.optimize.minimize(method="bfgs") / default=1e-5
|
|
127
|
+
bool return_grad: Flag to calculate the gradient in _loglik_and_gradient / default=True
|
|
128
|
+
bool return_hess: Flag to calculate the hessian in _loglik_and_gradient / default=True
|
|
129
|
+
method: Optimisation method for scipy.optimize.minimize / string / default="bfgs"
|
|
130
|
+
bool scipy_optimisation : Flag to apply optimiser / default=False / When false use own bfgs method.
|
|
131
|
+
|
|
132
|
+
class_params_spec: Array of lists containing names of variables for latent class / array-like / shape (n_samples,)
|
|
133
|
+
class_params_spec_is = array like except for isvars
|
|
134
|
+
member_params_spec: Array of lists containing names of variables for class membership / array-like / shape (n_samples,)
|
|
135
|
+
|
|
136
|
+
dict intercept_opts: Options for intercept. Allows specific alts for various intercepts / default=None
|
|
137
|
+
init_class_betas: Coefficients specified initially for each class / numpy array / shape (n_classes,) / default=None
|
|
138
|
+
init_class_thetas: Coefficients specified initially - membership function/ numpy array / shape (n_classes-1,)
|
|
139
|
+
/ default=None
|
|
140
|
+
gtol_membership_func: Same as gtol, but for the membership function/ int, float / default=1e-5
|
|
141
|
+
|
|
142
|
+
Assumption: "varnames must match the number and order of columns in X
|
|
143
|
+
"""
|
|
144
|
+
|
|
145
|
+
# ===================
|
|
146
|
+
# CLASS FUNCTIONS
|
|
147
|
+
# ===================
|
|
148
|
+
|
|
149
|
+
"""
|
|
150
|
+
1. __init__(self);
|
|
151
|
+
2. setup(self, X, y, ...);
|
|
152
|
+
3. fit(self);
|
|
153
|
+
4. X_class_idx <-- get_member_X_idx(self, class_num, coeff_names=None);
|
|
154
|
+
5. post_process(self, result, coeff_names, sample_size, hess_inv=None);
|
|
155
|
+
6. pch <-- compute_probabilities_latent(self, betas, X, y, avail);
|
|
156
|
+
7. pch <-- prob_product_across_panels(self, pch, panel_info);
|
|
157
|
+
8. X, y, panel <-- balance_panels(self, X, y, panels);
|
|
158
|
+
9. H <-- posterior_est_latent_class_probability(self, class_thetas);
|
|
159
|
+
10. loglik <-- class_member_func(self, class_thetas, weights, X);
|
|
160
|
+
11. X_class_idx <-- get_class_X_idx(self, class_num, coeff_names=None, **kwargs);
|
|
161
|
+
12. len <-- get_betas_length(self, class_num);
|
|
162
|
+
13. void update(self, i, class_fxidxs, class_fxtransidxs);
|
|
163
|
+
14. p <-- get_p(self, i, X, y, class_betas, class_idxs, class_fxidxs, class_fxtransidxs);
|
|
164
|
+
15. short_df <-- get_short_df(self, X);
|
|
165
|
+
16. result <-- expectation_maximisation_algorithm(self, X, y, avail=None, weights=None,
|
|
166
|
+
class_betas=None, class_thetas=None, validation=False);
|
|
167
|
+
17. loglik <-- get_validation_loglik(self, validation_X, validation_Y, avail=None,
|
|
168
|
+
avail_latent=None, weights=None, betas=None, panels=None);
|
|
169
|
+
18. result <-- bfgs_optimization(self, betas, X, y, weights, avail, maxiter);
|
|
170
|
+
"""
|
|
171
|
+
|
|
172
|
+
''' ---------------------------------------------------------- '''
|
|
173
|
+
''' Function . Constructor '''
|
|
174
|
+
''' ---------------------------------------------------------- '''
|
|
175
|
+
def __init__(self, **kwargs): # {
|
|
176
|
+
self.verbose = 0
|
|
177
|
+
self.optimise_class = kwargs.get('optimise_class', False)
|
|
178
|
+
self.optimise_membership = kwargs.get('optimise_membership', False)
|
|
179
|
+
self.fixed_solution = kwargs.get('fixed_solution', None)
|
|
180
|
+
self.start_time = time.time()
|
|
181
|
+
super(LatentClassModel, self).__init__()
|
|
182
|
+
self.descr = "LCM"
|
|
183
|
+
# }
|
|
184
|
+
|
|
185
|
+
''' ---------------------------------------------------------- '''
|
|
186
|
+
''' Function. Set up the model '''
|
|
187
|
+
''' ---------------------------------------------------------- '''
|
|
188
|
+
def setup(self, X, y, varnames=None, alts=None, isvars=None, num_classes=2,
|
|
189
|
+
class_params_spec=None, class_params_spec_is = None, member_params_spec=None,
|
|
190
|
+
ids=None, weights=None, avail=None, avail_latent=None,
|
|
191
|
+
transvars=None, transformation=None, base_alt=None, intercept_opts=None,
|
|
192
|
+
init_coeff=None, init_class_betas=None, init_class_thetas=None,
|
|
193
|
+
maxiter=2000, ftol=1e-5, ftol_lccm=1e-4,
|
|
194
|
+
gtol=1e-5, gtol_membership_func=1e-5, return_grad=True, return_hess=True,
|
|
195
|
+
panels=None, method="bfgs", scipy_optimisation=False,
|
|
196
|
+
validation=False, mnl_init=True, LCC_CLASS =None, verbose=0):
|
|
197
|
+
# {
|
|
198
|
+
|
|
199
|
+
if varnames is not None and member_params_spec is not None:
|
|
200
|
+
varnames = misc.rearrage_varnames(varnames, member_params_spec)
|
|
201
|
+
self.verbose = verbose
|
|
202
|
+
self.ftol, self.gtol, self.ftol_lccm = ftol, gtol, ftol_lccm
|
|
203
|
+
self.gtol_membership_func = gtol_membership_func
|
|
204
|
+
self.num_classes = num_classes
|
|
205
|
+
self.panels = panels
|
|
206
|
+
self.init_df, self.init_y = X, y
|
|
207
|
+
self.ids = ids
|
|
208
|
+
self.pred_prob, self.pred_prob_all= None, None
|
|
209
|
+
self.ind_pred_prob_classes, self.choice_pred_prob_classes = [], []
|
|
210
|
+
self.fit_intercept = misc.initialise_fit_intercept(class_params_spec)
|
|
211
|
+
self.intercept_classes = [('_inter' in class_params_spec[var]) for var in range(len(class_params_spec))]
|
|
212
|
+
#self.intercept_classes = [True if 'inter' in class_params_spec[var] for var in len(class_params_spec) else False]
|
|
213
|
+
#check the below setup
|
|
214
|
+
print('check the below setup')
|
|
215
|
+
self.class_params_spec = misc.initialise_class_params_spec(class_params_spec, varnames, varnames, num_classes)
|
|
216
|
+
self.class_params_spec_is = misc.initialise_class_params_spec(class_params_spec_is, isvars, [], num_classes)
|
|
217
|
+
for i in range(num_classes):
|
|
218
|
+
self.class_params_spec[i] = [j for j in self.class_params_spec[i] if j not in self.class_params_spec_is[i]]
|
|
219
|
+
#self.class_asvars = misc.initialise_class_params_spec(class_params_spec, isvars, varnames, num_classes)
|
|
220
|
+
self.intercept_opts = misc.initialise_opts(intercept_opts, num_classes)
|
|
221
|
+
self.avail_latent = misc.initialise_avail_latent(avail_latent, num_classes)
|
|
222
|
+
self.membership_as_probability = misc.initialise_membership_as_probability(member_params_spec)
|
|
223
|
+
self.member_params_spec = misc.initialise_member_params_spec(self.membership_as_probability,
|
|
224
|
+
member_params_spec, isvars, varnames, num_classes)
|
|
225
|
+
|
|
226
|
+
if LCC_CLASS is not None:
|
|
227
|
+
self.LCC_CLASS = LCC_CLASS
|
|
228
|
+
|
|
229
|
+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
230
|
+
# Initialise: MNL
|
|
231
|
+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
232
|
+
if mnl_init and init_class_betas is None:
|
|
233
|
+
# {
|
|
234
|
+
init_class_betas = np.array(np.repeat('tmp', num_classes), dtype='object') # Create temp/template array
|
|
235
|
+
init_class_names = np.array(np.repeat('tmp', num_classes), dtype='object') # Create temp/template array
|
|
236
|
+
self.fit_intercept = np.any(self.intercept_classes) == True
|
|
237
|
+
for i in range(num_classes):
|
|
238
|
+
# {
|
|
239
|
+
|
|
240
|
+
mnl = MultinomialLogit()
|
|
241
|
+
mnl = misc.setup_logit(i, mnl, X, y, varnames, self.class_params_spec, self.class_params_spec_is, avail,alts, transvars, gtol)
|
|
242
|
+
init_class_names[i] = mnl.coeff_names
|
|
243
|
+
init_class_betas = misc.revise_betas(i, mnl, init_class_betas, self.intercept_opts, alts)
|
|
244
|
+
# }
|
|
245
|
+
# }
|
|
246
|
+
else:
|
|
247
|
+
init_class_names = np.array(np.repeat('tmp', num_classes), dtype='object') # Create temp/template array
|
|
248
|
+
for c in range(num_classes):
|
|
249
|
+
#init_class_names[c] = [name for name in self.class_params_spec[c] joined with name for name class_params_spec_is[c] ]
|
|
250
|
+
init_class_names[c] = ', '.join( [name for name in self.class_params_spec[c]] + [name for name in class_params_spec_is[c]])
|
|
251
|
+
|
|
252
|
+
self.init_class_betas = init_class_betas
|
|
253
|
+
self.init_class_thetas = init_class_thetas
|
|
254
|
+
self.validation = validation
|
|
255
|
+
#to do, this falls over with we have different intercepts ie True/False
|
|
256
|
+
self.latent_class_names = init_class_names
|
|
257
|
+
super(LatentClassModel, self).setup(X, y, varnames, alts, isvars,
|
|
258
|
+
transvars, transformation, ids, weights, avail, base_alt,
|
|
259
|
+
self.fit_intercept, init_coeff, maxiter, ftol,
|
|
260
|
+
gtol, return_grad, return_hess, method, scipy_optimisation)
|
|
261
|
+
# }
|
|
262
|
+
|
|
263
|
+
''' ---------------------------------------------------------- '''
|
|
264
|
+
''' Function. Fit multinomial and/or conditional logit models '''
|
|
265
|
+
''' ---------------------------------------------------------- '''
|
|
266
|
+
def fit(self):
|
|
267
|
+
# {
|
|
268
|
+
super(LatentClassModel, self).fit()
|
|
269
|
+
# }
|
|
270
|
+
|
|
271
|
+
''' -------------------------------------------------------------- '''
|
|
272
|
+
''' Function. Get indices for X dataset '''
|
|
273
|
+
''' -------------------------------------------------------------- '''
|
|
274
|
+
def get_member_X_idx(self, class_num, coeff_names=None):
|
|
275
|
+
# {
|
|
276
|
+
#TODO make sure this works
|
|
277
|
+
tmp_varnames = self.varnames.copy() if coeff_names is None else coeff_names.copy()
|
|
278
|
+
#if '_inter' in self.class_params_spec[class_num]:
|
|
279
|
+
|
|
280
|
+
# if '_inter' not in tmp_varnames and '_intercept.2' not in tmp_varnames: # add the intercept
|
|
281
|
+
# tmp_varnames = np.append(['_inter'], tmp_varnames)
|
|
282
|
+
for ii, varname in enumerate(tmp_varnames): # {
|
|
283
|
+
if varname.startswith('lambda.'):
|
|
284
|
+
tmp_varnames[ii] = varname[7:] # Remove lambda so can get indices correctly
|
|
285
|
+
# }
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
for ii, varname in enumerate(tmp_varnames):
|
|
289
|
+
# {
|
|
290
|
+
if varname.startswith('lambda.'):
|
|
291
|
+
tmp_varnames[ii] = varname[7:] # Remove lambda so can get indices correctly
|
|
292
|
+
# }
|
|
293
|
+
|
|
294
|
+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
295
|
+
# Indices to retrieve relevant explanatory params of specified latent class
|
|
296
|
+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
297
|
+
X_class_idx = np.array([], dtype='int32')
|
|
298
|
+
for ii, var in enumerate(self.member_params_spec[class_num]): # { #this causes error
|
|
299
|
+
if '_inter' not in var:
|
|
300
|
+
X_class_idx = np.append(X_class_idx, ii)
|
|
301
|
+
# }
|
|
302
|
+
# }
|
|
303
|
+
'''
|
|
304
|
+
for var in self.member_params_spec[class_num]: # { #this causes error
|
|
305
|
+
for ii, var2 in enumerate(tmp_varnames): # {
|
|
306
|
+
if var in var2 and var != '_inter':
|
|
307
|
+
X_class_idx = np.append(X_class_idx, ii)
|
|
308
|
+
# }
|
|
309
|
+
# }
|
|
310
|
+
'''
|
|
311
|
+
X_class_idx = np.sort(X_class_idx)
|
|
312
|
+
|
|
313
|
+
return X_class_idx
|
|
314
|
+
# }
|
|
315
|
+
|
|
316
|
+
''' ---------------------------------------------------------- '''
|
|
317
|
+
''' Function '''
|
|
318
|
+
''' ---------------------------------------------------------- '''
|
|
319
|
+
def post_process(self, result, coeff_names, sample_size, hess_inv=None):
|
|
320
|
+
# {
|
|
321
|
+
if self.validation:
|
|
322
|
+
return
|
|
323
|
+
# else:
|
|
324
|
+
super(LatentClassModel, self).post_process(result, coeff_names, sample_size)
|
|
325
|
+
# }
|
|
326
|
+
|
|
327
|
+
''' ---------------------------------------------------------- '''
|
|
328
|
+
''' Function '''
|
|
329
|
+
''' ---------------------------------------------------------- '''
|
|
330
|
+
def compute_probabilities_latent(self, betas, X, y, avail):
|
|
331
|
+
# {
|
|
332
|
+
p = self.compute_probabilities(betas, X, avail)
|
|
333
|
+
p = y*p # Compute p[i][j] = p[i][j] * y[i][j]
|
|
334
|
+
pch = np.sum(p, axis=1) # Sum values across the second dimension - compute row sums
|
|
335
|
+
pch = truncate_lower(pch, min_comp_val) # Truncate values if they are too small
|
|
336
|
+
pch = np.log(pch)
|
|
337
|
+
|
|
338
|
+
# collapse on alts
|
|
339
|
+
if hasattr(self, 'panel_info'):
|
|
340
|
+
# {
|
|
341
|
+
counter = 0
|
|
342
|
+
p_test = np.zeros(self.panel_info.shape[0])
|
|
343
|
+
for i, row in enumerate(self.panel_info):
|
|
344
|
+
# {
|
|
345
|
+
row_sum = int(np.sum(row))
|
|
346
|
+
# pch_new[counter:counter+row_sum] = np.mean(pch[counter:counter+row_sum])
|
|
347
|
+
p_test[i] = np.sum(pch[counter:counter+row_sum])
|
|
348
|
+
counter += row_sum
|
|
349
|
+
# }
|
|
350
|
+
# pch = pch_new
|
|
351
|
+
pch = p_test
|
|
352
|
+
# }
|
|
353
|
+
|
|
354
|
+
pch = np.exp(pch)
|
|
355
|
+
return pch
|
|
356
|
+
# }
|
|
357
|
+
|
|
358
|
+
''' ---------------------------------------------------------- '''
|
|
359
|
+
''' Function '''
|
|
360
|
+
''' ---------------------------------------------------------- '''
|
|
361
|
+
def prob_product_across_panels(self, pch, panel_info):
|
|
362
|
+
# {
|
|
363
|
+
if not np.all(panel_info): # {
|
|
364
|
+
idx = panel_info == .0
|
|
365
|
+
pch[:, :][idx] = 1 # Multiply by one when unbalanced
|
|
366
|
+
# }
|
|
367
|
+
pch = pch.prod(axis=1, dtype=np.float64) # (N,R)
|
|
368
|
+
pch = truncate_lower(pch, min_comp_val) # i.e., pch[pch < min_comp_val] = min_comp_val
|
|
369
|
+
return pch # (N,R)
|
|
370
|
+
# }
|
|
371
|
+
|
|
372
|
+
''' -------------------------------------------------------------------------- '''
|
|
373
|
+
''' Function. Balance panels if necessary and produce a new version of X and y '''
|
|
374
|
+
''' panel_info keeps track of the panels that needed balancing and is returned '''
|
|
375
|
+
''' -------------------------------------------------------------------------- '''
|
|
376
|
+
# copied from mixed_logit class
|
|
377
|
+
def balance_panels(self, X, y, panels):
|
|
378
|
+
# {
|
|
379
|
+
_, J, K = X.shape
|
|
380
|
+
_, p_obs = np.unique(panels, return_counts=True)
|
|
381
|
+
p_obs = (p_obs/J).astype(int)
|
|
382
|
+
N = len(p_obs) # This is the new N after accounting for panels
|
|
383
|
+
P = np.max(p_obs) # panels length for all records
|
|
384
|
+
NP = N * P
|
|
385
|
+
if not np.all(p_obs[0] == p_obs): # Balancing needed
|
|
386
|
+
# {
|
|
387
|
+
y = y.reshape(X.shape[0], J, 1)
|
|
388
|
+
Xbal, ybal = np.zeros((NP, J, K)), np.zeros((NP, J, 1))
|
|
389
|
+
panel_info = np.zeros((N, P))
|
|
390
|
+
cum_p = 0 # Cumulative sum of n_obs at every iteration
|
|
391
|
+
for n, p in enumerate(p_obs): # {
|
|
392
|
+
# Copy data from original to balanced version
|
|
393
|
+
nP = n * P
|
|
394
|
+
Xbal[nP:nP + p, :, :] = X[cum_p:cum_p + p, :, :]
|
|
395
|
+
ybal[nP:nP + p, :, :] = y[cum_p:cum_p + p, :, :]
|
|
396
|
+
panel_info[n, :p] = np.ones(p)
|
|
397
|
+
cum_p += p
|
|
398
|
+
# }
|
|
399
|
+
# }
|
|
400
|
+
else: # No balancing needed
|
|
401
|
+
# {
|
|
402
|
+
Xbal, ybal = X, y
|
|
403
|
+
panel_info = np.ones((N, P))
|
|
404
|
+
# }
|
|
405
|
+
|
|
406
|
+
return Xbal, ybal, panel_info
|
|
407
|
+
# }
|
|
408
|
+
|
|
409
|
+
''' ----------------------------------------------------------------------- '''
|
|
410
|
+
''' Function. Compute the prior estimates of the latent class probabilities '''
|
|
411
|
+
''' ----------------------------------------------------------------------- '''
|
|
412
|
+
def posterior_est_latent_class_probability(self, class_thetas):
|
|
413
|
+
# {
|
|
414
|
+
# class_thetas: array of latent class vectors
|
|
415
|
+
# H: Prior estimates of the class
|
|
416
|
+
|
|
417
|
+
class_thetas_original = class_thetas
|
|
418
|
+
if class_thetas.ndim == 1:
|
|
419
|
+
# {
|
|
420
|
+
temp = np.repeat('tmp', self.num_classes-1)
|
|
421
|
+
new_class_thetas = np.array(temp, dtype='object')
|
|
422
|
+
j = 0
|
|
423
|
+
for ii, member_params in enumerate(self.member_params_spec): # {
|
|
424
|
+
num_params = len(member_params)
|
|
425
|
+
tmp = class_thetas[j:j+num_params]
|
|
426
|
+
j += num_params
|
|
427
|
+
new_class_thetas[ii] = tmp
|
|
428
|
+
# }
|
|
429
|
+
class_thetas = new_class_thetas
|
|
430
|
+
# }
|
|
431
|
+
|
|
432
|
+
class_thetas_base = np.zeros(len(class_thetas[0]))
|
|
433
|
+
base_X_idx = self.get_member_X_idx(0)
|
|
434
|
+
|
|
435
|
+
member_df = np.transpose(self.short_df[:, base_X_idx])
|
|
436
|
+
dim = member_df.shape[1]
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
if '_inter' in self.member_params_spec[0]: # {
|
|
440
|
+
#print('off for now')
|
|
441
|
+
|
|
442
|
+
ones = np.ones((1, dim)) # Create 1 x dim array
|
|
443
|
+
member_df = np.vstack((ones, member_df))
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
if self.membership_as_probability: # {
|
|
448
|
+
H = np.tile(np.concatenate([1 - np.sum(class_thetas), class_thetas_original]), (dim, 1))
|
|
449
|
+
H = np.transpose(H)
|
|
450
|
+
# }
|
|
451
|
+
else:
|
|
452
|
+
# {
|
|
453
|
+
zB_q = np.dot(class_thetas_base[None, :], member_df)
|
|
454
|
+
eZB = np.zeros((self.num_classes, dim))
|
|
455
|
+
eZB[0, :] = np.exp(zB_q)
|
|
456
|
+
|
|
457
|
+
for i in range(0, self.num_classes-1):
|
|
458
|
+
# {
|
|
459
|
+
class_X_idx = self.get_member_X_idx(i)
|
|
460
|
+
member_df = np.transpose(self.short_df[:, class_X_idx])
|
|
461
|
+
|
|
462
|
+
# add in columns of ones for class-specific const (_inter)
|
|
463
|
+
if '_inter' in self.member_params_spec[i]:
|
|
464
|
+
# {
|
|
465
|
+
#print('off for now')
|
|
466
|
+
|
|
467
|
+
ones = np.ones((1, dim)) # Create 1 x dim array
|
|
468
|
+
transpose = np.transpose(self.short_df[:, class_X_idx])
|
|
469
|
+
member_df = np.vstack((ones, transpose))
|
|
470
|
+
|
|
471
|
+
# }
|
|
472
|
+
|
|
473
|
+
zB_q = np.dot(class_thetas[i].reshape((1, -1)), member_df)
|
|
474
|
+
zB_q = truncate_higher(zB_q, max_exp_val)
|
|
475
|
+
eZB[i+1, :] = np.exp(zB_q)
|
|
476
|
+
# }
|
|
477
|
+
|
|
478
|
+
H = eZB/np.sum(eZB, axis=0, keepdims=True)
|
|
479
|
+
# }
|
|
480
|
+
|
|
481
|
+
# Add attribute to class. Variable does not exist till now.
|
|
482
|
+
# class_freq information is displayed in function 'ChoiceModel::summarise'
|
|
483
|
+
self.class_freq = np.mean(H, axis=1) # Compute: class_freq[i] = average(H[i,:])
|
|
484
|
+
return H
|
|
485
|
+
# }
|
|
486
|
+
|
|
487
|
+
''' ----------------------------------------------------------------- '''
|
|
488
|
+
''' Function. Find latent class params that minimise negative loglik '''
|
|
489
|
+
''' ----------------------------------------------------------------- '''
|
|
490
|
+
def class_member_func(self, class_thetas, weights, X):
|
|
491
|
+
# {
|
|
492
|
+
""" Function used in maximisation step. Used to find latent class vectors that
|
|
493
|
+
minimise the negative loglik where there is no observed dependent variable (H replaces y)."""
|
|
494
|
+
|
|
495
|
+
# class_thetas: (number of latent classes) - 1 array of latent class vectors
|
|
496
|
+
# weights: Prior probability of class by the probability of y given the class.
|
|
497
|
+
# X: Input data for explanatory variables / wide format
|
|
498
|
+
|
|
499
|
+
H = self.posterior_est_latent_class_probability(class_thetas)
|
|
500
|
+
self.H = H # save individual-level probabilities for class membership
|
|
501
|
+
|
|
502
|
+
H = truncate_lower(H, 1e-30) # i.e., H[np.where(H < 1e-30)] = 1e-30
|
|
503
|
+
weight_post = np.multiply(np.log(H), weights)
|
|
504
|
+
ll = -np.sum(weight_post) # Compute loglik
|
|
505
|
+
tgr = H - weights
|
|
506
|
+
gr = np.array([])
|
|
507
|
+
|
|
508
|
+
for i in range(1, self.num_classes):
|
|
509
|
+
# {
|
|
510
|
+
member_idx = self.get_member_X_idx(i-1)
|
|
511
|
+
membership_df = self.short_df[:, member_idx]
|
|
512
|
+
|
|
513
|
+
# add in columns of ones for class-specific const (_inter)
|
|
514
|
+
if '_inter' in self.member_params_spec[i-1]:
|
|
515
|
+
|
|
516
|
+
membership_df = np.hstack((np.ones((self.short_df.shape[0], 1)), membership_df))
|
|
517
|
+
if self.membership_as_probability:
|
|
518
|
+
membership_df = np.ones((self.short_df.shape[0], 1))
|
|
519
|
+
|
|
520
|
+
gr_i = np.dot(np.transpose(membership_df), tgr[i, :])
|
|
521
|
+
gr = np.concatenate((gr, gr_i))
|
|
522
|
+
# }
|
|
523
|
+
return ll, gr.flatten() # Return loglik
|
|
524
|
+
# }
|
|
525
|
+
|
|
526
|
+
''' ---------------------------------------------------------- '''
|
|
527
|
+
''' Function. Get indices for X dataset for class parameters '''
|
|
528
|
+
''' ---------------------------------------------------------- '''
|
|
529
|
+
def get_class_X_idx(self, class_num, coeff_names=None, **kwargs):
|
|
530
|
+
# {
|
|
531
|
+
"""
|
|
532
|
+
X_class_idx [np.ndarray]: indices to retrieve relevant explanatory params
|
|
533
|
+
of specified latent class
|
|
534
|
+
"""
|
|
535
|
+
|
|
536
|
+
tmp_varnames = self.varnames.copy() if coeff_names is None else coeff_names.copy()
|
|
537
|
+
if '_inter' in self.class_params_spec[class_num]:
|
|
538
|
+
if '_inter' not in tmp_varnames and '_intercept.2' not in tmp_varnames: #add the intercept
|
|
539
|
+
tmp_varnames = np.append(['_inter'], tmp_varnames)
|
|
540
|
+
for ii, varname in enumerate(tmp_varnames): # {
|
|
541
|
+
if varname.startswith('lambda.'):
|
|
542
|
+
tmp_varnames[ii] = varname[7:] # Remove lambda so can get indices correctly
|
|
543
|
+
# }
|
|
544
|
+
|
|
545
|
+
X_class_idx = np.array([], dtype='int32')
|
|
546
|
+
|
|
547
|
+
# Iterate through variables in self.class_params_spec[class_num] and
|
|
548
|
+
# compare them with variables in tmp_varnames. Based on certain conditions, modify the X_class_idx array.
|
|
549
|
+
|
|
550
|
+
params_is = np.array(self.class_params_spec_is[class_num])
|
|
551
|
+
params_spec = np.array(self.class_params_spec[class_num])
|
|
552
|
+
combined_params = np.concatenate([params_is, params_spec])
|
|
553
|
+
|
|
554
|
+
for var in (combined_params):
|
|
555
|
+
# {
|
|
556
|
+
for ii, var2 in enumerate(tmp_varnames):
|
|
557
|
+
# {
|
|
558
|
+
if 'inter' in var and coeff_names is not None and 'inter' in var2:
|
|
559
|
+
# {
|
|
560
|
+
if 'class_intercept_alts' in self.intercept_opts:
|
|
561
|
+
# {
|
|
562
|
+
# Split string var2 by delimiter '.'. The result is a list of substrings
|
|
563
|
+
# The [-1] index is used to access the last element of the resulting list.
|
|
564
|
+
alt_num = int(var2.split('.')[-1])
|
|
565
|
+
if alt_num not in self.intercept_opts['class_intercept_alts'][class_num]:
|
|
566
|
+
continue # i.e., skip current iteration of the loop
|
|
567
|
+
# }
|
|
568
|
+
# }
|
|
569
|
+
if var == '_inter':
|
|
570
|
+
if var in var2: #FIXME added 4/11/24
|
|
571
|
+
X_class_idx = np.append(X_class_idx, ii)
|
|
572
|
+
else:
|
|
573
|
+
if var == var2:
|
|
574
|
+
X_class_idx = np.append(X_class_idx, ii)
|
|
575
|
+
# }
|
|
576
|
+
# }
|
|
577
|
+
|
|
578
|
+
# isvars handled if pass in full coeff names
|
|
579
|
+
X_class_idx = np.unique(X_class_idx)
|
|
580
|
+
X_class_idx = np.sort(X_class_idx)
|
|
581
|
+
X_class_idx_tmp = np.array([], dtype='int')
|
|
582
|
+
counter = 0
|
|
583
|
+
|
|
584
|
+
# TODO? better approach than replicating Xname creation?
|
|
585
|
+
if coeff_names is not None:
|
|
586
|
+
return X_class_idx
|
|
587
|
+
|
|
588
|
+
for idx_pos, _ in enumerate(tmp_varnames):
|
|
589
|
+
# {
|
|
590
|
+
if idx_pos in self.ispos:
|
|
591
|
+
# {
|
|
592
|
+
# fix bug of not all alts checked intercept
|
|
593
|
+
for i in range(self.J - 1):
|
|
594
|
+
# {
|
|
595
|
+
if idx_pos not in X_class_idx:
|
|
596
|
+
continue # Skip iteration of loop at this point
|
|
597
|
+
|
|
598
|
+
if self.varnames[idx_pos] == '_inter' and 'class_intercept_alts' in self.intercept_opts:
|
|
599
|
+
# {
|
|
600
|
+
if i+2 not in self.intercept_opts['class_intercept_alts'][class_num]:
|
|
601
|
+
# {
|
|
602
|
+
counter += 1
|
|
603
|
+
continue # Skip iteration of loop at this point
|
|
604
|
+
# }
|
|
605
|
+
# }
|
|
606
|
+
X_class_idx_tmp = np.append(X_class_idx_tmp, int(counter))
|
|
607
|
+
counter += 1
|
|
608
|
+
# }
|
|
609
|
+
# }
|
|
610
|
+
else: # {
|
|
611
|
+
if idx_pos in X_class_idx:
|
|
612
|
+
X_class_idx_tmp = np.append(X_class_idx_tmp, counter)
|
|
613
|
+
counter += 1
|
|
614
|
+
# }
|
|
615
|
+
# }
|
|
616
|
+
|
|
617
|
+
X_class_idx = X_class_idx_tmp
|
|
618
|
+
|
|
619
|
+
return X_class_idx
|
|
620
|
+
# }
|
|
621
|
+
|
|
622
|
+
def get_class_X_idx_alternative(self, class_num, coeff_names=None, **kwargs):
|
|
623
|
+
"""
|
|
624
|
+
X_class_idx: indices to retrieve relevant
|
|
625
|
+
explanatory params of specified latent class
|
|
626
|
+
"""
|
|
627
|
+
# below line: return indices of that class params in Xnames
|
|
628
|
+
# pattern matching for isvars
|
|
629
|
+
|
|
630
|
+
if coeff_names is None:
|
|
631
|
+
coeff_names = self.global_varnames.copy()
|
|
632
|
+
#tring to handle the global var names
|
|
633
|
+
if np.any(self.intercept_classes) == True:
|
|
634
|
+
|
|
635
|
+
#if self.intercept_classes[class_num]:
|
|
636
|
+
# {
|
|
637
|
+
inter_count = sum(1 for name in coeff_names if '_inter' in name)
|
|
638
|
+
num_in =len(self.alts) -1
|
|
639
|
+
if inter_count < num_in:
|
|
640
|
+
new_names = ['_inter' for i in range(num_in)]
|
|
641
|
+
new_names.extend([name for name in coeff_names if 'inter' not in name])
|
|
642
|
+
coeff_names = new_names
|
|
643
|
+
# }
|
|
644
|
+
tmp_varnames = coeff_names.copy()
|
|
645
|
+
for ii, varname in enumerate(tmp_varnames):
|
|
646
|
+
# {
|
|
647
|
+
# remove lambda so can get indices correctly
|
|
648
|
+
if varname.startswith('lambda.'):
|
|
649
|
+
tmp_varnames[ii] = varname[7:]
|
|
650
|
+
|
|
651
|
+
if varname.startswith('sd.'):
|
|
652
|
+
tmp_varnames[ii] = varname[3:]
|
|
653
|
+
# }
|
|
654
|
+
|
|
655
|
+
X_class_idx = np.array([], dtype="int")
|
|
656
|
+
|
|
657
|
+
for var in self.class_params_spec_is[class_num] + self.class_params_spec[class_num]:
|
|
658
|
+
# {
|
|
659
|
+
alt_num_counter = 1
|
|
660
|
+
# if 'inter' in var:
|
|
661
|
+
# alt_num_counter = 1
|
|
662
|
+
for ii, var2 in enumerate(tmp_varnames):
|
|
663
|
+
# {
|
|
664
|
+
if 'inter' in var and 'inter' in var2 and coeff_names is not None:
|
|
665
|
+
# {
|
|
666
|
+
if 'class_intercept_alts' in self.intercept_opts:
|
|
667
|
+
# {
|
|
668
|
+
if alt_num_counter not in self.intercept_opts['class_intercept_alts'][class_num]:
|
|
669
|
+
# {
|
|
670
|
+
alt_num_counter += 1
|
|
671
|
+
if alt_num_counter > 2:
|
|
672
|
+
continue # Skip current iteration of loop
|
|
673
|
+
# }
|
|
674
|
+
else:
|
|
675
|
+
alt_num_counter += 1
|
|
676
|
+
# }
|
|
677
|
+
# }
|
|
678
|
+
|
|
679
|
+
if var in var2:
|
|
680
|
+
X_class_idx = np.append(ii,X_class_idx)
|
|
681
|
+
# }
|
|
682
|
+
# }
|
|
683
|
+
|
|
684
|
+
X_class_idx = np.unique(X_class_idx)
|
|
685
|
+
X_class_idx = np.sort(X_class_idx)
|
|
686
|
+
|
|
687
|
+
return X_class_idx
|
|
688
|
+
|
|
689
|
+
|
|
690
|
+
''' ---------------------------------------------------------- '''
|
|
691
|
+
''' Function. Get betas length (parameter vectors) for the '''
|
|
692
|
+
''' specified latent class '''
|
|
693
|
+
''' ---------------------------------------------------------- '''
|
|
694
|
+
def get_betas_length(self, class_num):
|
|
695
|
+
# {
|
|
696
|
+
class_params_spec = self.class_params_spec[class_num]
|
|
697
|
+
|
|
698
|
+
betas_length = 0 # The number of betas for latent class
|
|
699
|
+
if 'class_intercept_alts' in self.intercept_opts and '_inter' in class_params_spec: # {
|
|
700
|
+
# separate logic for intercept
|
|
701
|
+
# class_isvars = [isvar for isvar in self.isvars if isvar != '_inter']
|
|
702
|
+
betas_length += len(self.intercept_opts['class_intercept_alts'][class_num])
|
|
703
|
+
# }
|
|
704
|
+
else: # {
|
|
705
|
+
class_isvars = [x for x in class_params_spec if x in self.isvars]
|
|
706
|
+
betas_length += (len(self.alts)-1)*(len(class_isvars))
|
|
707
|
+
# }
|
|
708
|
+
|
|
709
|
+
class_asvars = [x for x in class_params_spec if x in self.asvars]
|
|
710
|
+
class_transvars = [x for x in class_params_spec if x in self.transvars]
|
|
711
|
+
|
|
712
|
+
betas_length += len(class_asvars)
|
|
713
|
+
betas_length += len(class_transvars)*2
|
|
714
|
+
|
|
715
|
+
return betas_length
|
|
716
|
+
# }
|
|
717
|
+
|
|
718
|
+
''' ----------------------------------------------------------- '''
|
|
719
|
+
''' Function. '''
|
|
720
|
+
''' ----------------------------------------------------------- '''
|
|
721
|
+
def update(self, i, class_fxidxs, class_fxtransidxs):
|
|
722
|
+
# {
|
|
723
|
+
#FIXME THIS FALLS OVER
|
|
724
|
+
self.fxidx = class_fxidxs[i]
|
|
725
|
+
self.Kf = sum(class_fxidxs[i]) # Sum the booleans
|
|
726
|
+
self.fxtransidx = class_fxtransidxs[i]
|
|
727
|
+
self.Kftrans = sum(class_fxtransidxs[i]) # Sum the booleans
|
|
728
|
+
params_is = np.array(self.class_params_spec_is[i])
|
|
729
|
+
params_spec = np.array(self.class_params_spec[i])
|
|
730
|
+
combined_params = np.concatenate([params_is, params_spec])
|
|
731
|
+
self.varnames = np.array(list(set(combined_params)))
|
|
732
|
+
self.isvars = self.class_params_spec_is[i]
|
|
733
|
+
|
|
734
|
+
# }
|
|
735
|
+
|
|
736
|
+
''' ----------------------------------------------------------- '''
|
|
737
|
+
''' Function. '''
|
|
738
|
+
''' ----------------------------------------------------------- '''
|
|
739
|
+
def get_p(self, i, X, y, class_betas, class_idxs, class_fxidxs, class_fxtransidxs):
|
|
740
|
+
# {
|
|
741
|
+
self.update(i, class_fxidxs, class_fxtransidxs)
|
|
742
|
+
p = self.compute_probabilities_latent(class_betas[i], X[:, :, class_idxs[i]], y, self.avail_latent[i])
|
|
743
|
+
return p
|
|
744
|
+
# }
|
|
745
|
+
|
|
746
|
+
''' ----------------------------------------------------------- '''
|
|
747
|
+
''' Function '''
|
|
748
|
+
''' ----------------------------------------------------------- '''
|
|
749
|
+
def get_short_df(self, X):
|
|
750
|
+
# {
|
|
751
|
+
short_df = np.mean(X, axis=1) # Compute: short_df[i] = average(X[i,:])
|
|
752
|
+
if hasattr(self, 'panel_info') and self.panel_info is not None:
|
|
753
|
+
# {
|
|
754
|
+
counter = 0
|
|
755
|
+
new_short_df = np.zeros((self.panel_info.shape[0], short_df.shape[1]))
|
|
756
|
+
for ii, row in enumerate(self.panel_info): # {
|
|
757
|
+
row_sum = int(np.sum(row))
|
|
758
|
+
new_short_df[ii, :] = np.mean(short_df[counter:counter + row_sum, :], axis=0)
|
|
759
|
+
counter += row_sum
|
|
760
|
+
# }
|
|
761
|
+
short_df = new_short_df
|
|
762
|
+
# }
|
|
763
|
+
|
|
764
|
+
# Remove intercept columns
|
|
765
|
+
if self.fit_intercept: # {
|
|
766
|
+
short_df = short_df[:, (self.J - 2):]
|
|
767
|
+
short_df[:, 0] = 1
|
|
768
|
+
# }
|
|
769
|
+
return short_df
|
|
770
|
+
# }
|
|
771
|
+
|
|
772
|
+
''' ----------------------------------------------------------- '''
|
|
773
|
+
''' Function. '''
|
|
774
|
+
''' ----------------------------------------------------------- '''
|
|
775
|
+
def call_validate(self, X, y, H, log_lik_new, class_betas, class_thetas,
|
|
776
|
+
class_idxs, class_fxidxs, class_fxtransidxs):
|
|
777
|
+
# {
|
|
778
|
+
self.loglik = log_lik_new
|
|
779
|
+
num_params = 0
|
|
780
|
+
num_params += sum([len(betas) for betas in class_betas])
|
|
781
|
+
num_params += len(class_thetas)
|
|
782
|
+
self.aic = -2 * log_lik_new + 2 * num_params
|
|
783
|
+
self.bic = -2 * log_lik_new + num_params * np.log(self.sample_size)
|
|
784
|
+
global_transvars = self.transvars.copy()
|
|
785
|
+
self.pred_prob_all = np.array([])
|
|
786
|
+
|
|
787
|
+
for i in range(self.num_classes):
|
|
788
|
+
# {
|
|
789
|
+
# Remove transvars which are not included in class params
|
|
790
|
+
self.transvars = [transvar for transvar in global_transvars if transvar in self.class_params_spec[i]]
|
|
791
|
+
|
|
792
|
+
p = self.get_p(i,X,y,class_betas,class_idxs,class_fxidxs,class_fxtransidxs)
|
|
793
|
+
|
|
794
|
+
self.ind_pred_prob = p
|
|
795
|
+
self.choice_pred_prob = p
|
|
796
|
+
self.pred_prob = np.mean(p, axis=0) # Compute: pred_prob[j] = average(H[:,j])
|
|
797
|
+
self.obs_prob = np.mean(y, axis=0) # Compute: obs_prob[j] = average(H[:,j])
|
|
798
|
+
self.pred_prob_all = np.append(self.pred_prob_all, self.pred_prob)
|
|
799
|
+
# }
|
|
800
|
+
|
|
801
|
+
p_class = np.mean(H, axis=1) # Compute: p_class[i] = average(H[i,:])
|
|
802
|
+
|
|
803
|
+
# ---------------------------------------------------------------------
|
|
804
|
+
if dev.using_gpu:
|
|
805
|
+
self.pred_prob_all = dev.convert_array_cpu(self.pred_prob_all)
|
|
806
|
+
# ---------------------------------------------------------------------
|
|
807
|
+
|
|
808
|
+
pred_prob_tmp = np.zeros(self.J)
|
|
809
|
+
for i in range(self.num_classes):
|
|
810
|
+
# {
|
|
811
|
+
low = i*self.J
|
|
812
|
+
high = low + self.J
|
|
813
|
+
pred_prob_tmp += p_class[i] * self.pred_prob_all[low:high]
|
|
814
|
+
# }
|
|
815
|
+
self.pred_prob = pred_prob_tmp
|
|
816
|
+
return log_lik_new
|
|
817
|
+
# }
|
|
818
|
+
|
|
819
|
+
''' ---------------------------------------------------------- '''
|
|
820
|
+
''' Function '''
|
|
821
|
+
''' ---------------------------------------------------------- '''
|
|
822
|
+
def setup_em(self, X, class_thetas, class_betas):
|
|
823
|
+
# {
|
|
824
|
+
if class_betas is None: # {
|
|
825
|
+
if self.init_class_betas is not None:
|
|
826
|
+
class_betas = self.init_class_betas
|
|
827
|
+
else:
|
|
828
|
+
class_betas = [-0.1 * np.random.rand(self.get_betas_length(i)) for i in range(self.num_classes)]
|
|
829
|
+
# }
|
|
830
|
+
|
|
831
|
+
if self.membership_as_probability:
|
|
832
|
+
class_thetas = np.array([1 / (self.num_classes) for i in range(0, self.num_classes - 1)])
|
|
833
|
+
|
|
834
|
+
if class_thetas is None:
|
|
835
|
+
# {
|
|
836
|
+
if self.init_class_thetas is not None:
|
|
837
|
+
class_thetas = self.init_class_thetas
|
|
838
|
+
else:
|
|
839
|
+
# {
|
|
840
|
+
# class membership probability
|
|
841
|
+
len_class_thetas = [len(self.get_member_X_idx(i)) for i in range(0, self.num_classes - 1)]
|
|
842
|
+
for ii, len_class_thetas_ii in enumerate(len_class_thetas):
|
|
843
|
+
# {
|
|
844
|
+
if '_inter' in self.member_params_spec[ii]:
|
|
845
|
+
len_class_thetas[ii] = len_class_thetas[ii] + 1
|
|
846
|
+
# }
|
|
847
|
+
|
|
848
|
+
class_thetas = np.concatenate([np.zeros(len_class_thetas[i])
|
|
849
|
+
for i in range(0, self.num_classes - 1)], axis=0)
|
|
850
|
+
# }
|
|
851
|
+
# }
|
|
852
|
+
|
|
853
|
+
self.trans_pos = [ii for ii, var in enumerate(self.varnames) if var in self.transvars]
|
|
854
|
+
|
|
855
|
+
# Note: trans_pos is used for _get_class_X_idx
|
|
856
|
+
|
|
857
|
+
self.short_df = self.get_short_df(X)
|
|
858
|
+
self.global_varnames = self.varnames
|
|
859
|
+
self.global_fxidx, self.global_fxtransidx = self.fxidx, self.fxtransidx
|
|
860
|
+
|
|
861
|
+
if self.panels is not None: self.N = X.shape[0]
|
|
862
|
+
|
|
863
|
+
class_idxs, class_fxidxs, class_fxtransidxs = [], [], []
|
|
864
|
+
|
|
865
|
+
for class_num in range(self.num_classes):
|
|
866
|
+
# {
|
|
867
|
+
X_class_idx = self.get_class_X_idx(class_num)
|
|
868
|
+
#X_class_idx = self.get_class_X_idx_alternative(class_num, coeff_names=self.global_varnames)
|
|
869
|
+
|
|
870
|
+
class_idxs.append(X_class_idx)
|
|
871
|
+
class_fx_idx = [fxidx for ii, fxidx in enumerate(self.fxidx) if ii in X_class_idx]
|
|
872
|
+
class_fxidxs.append(class_fx_idx)
|
|
873
|
+
class_fxtransidx = [not fxidx for fxidx in class_fx_idx]
|
|
874
|
+
class_fxtransidxs.append(class_fxtransidx)
|
|
875
|
+
# }
|
|
876
|
+
|
|
877
|
+
return class_thetas, class_betas, class_idxs, class_fxidxs, class_fxtransidxs
|
|
878
|
+
# }
|
|
879
|
+
|
|
880
|
+
''' ---------------------------------------------------------- '''
|
|
881
|
+
''' Function '''
|
|
882
|
+
''' ---------------------------------------------------------- '''
|
|
883
|
+
def reset_fx(self):
|
|
884
|
+
# {
|
|
885
|
+
self.fxidx = self.global_fxidx
|
|
886
|
+
self.fxtransidx = self.global_fxtransidx
|
|
887
|
+
self.Kf = sum(self.global_fxidx)
|
|
888
|
+
self.Kftrans = sum(self.global_fxidx)
|
|
889
|
+
self.varnames = self.global_varnames
|
|
890
|
+
# }
|
|
891
|
+
|
|
892
|
+
''' ----------------------------------------------------------- '''
|
|
893
|
+
''' Function. Expectation-maximisation algorithm [APPALLING CODE] '''
|
|
894
|
+
''' ERROR: 'log_lik_new' may be referenced before initialisation'''
|
|
895
|
+
''' ERROR: 'H' may be referenced before initialisation '''
|
|
896
|
+
''' ----------------------------------------------------------- '''
|
|
897
|
+
def fixed_expectation_algorithm(self, X, y, class_thetas, avail=None, weights=None,
|
|
898
|
+
class_betas=None, validation=False ):
|
|
899
|
+
"""
|
|
900
|
+
Fix the EM algorithm for the theta coefficients
|
|
901
|
+
"""
|
|
902
|
+
|
|
903
|
+
#FIX ME, always add an intercpt
|
|
904
|
+
|
|
905
|
+
#X = #3 dimensional x, add a list of ones to the first column in [:, :, 0]
|
|
906
|
+
#X = np.insert(X, 0, 1, axis=2)
|
|
907
|
+
#TODO, I think this might throw out the fxidx for when we have intercepts
|
|
908
|
+
class_thetas, class_betas, class_idxs, class_fxidxs, class_fxtransidxs = self.setup_em(X, class_thetas, class_betas)
|
|
909
|
+
|
|
910
|
+
|
|
911
|
+
class_betas_sd = [np.repeat(.99, len(betas)) for betas in class_betas]
|
|
912
|
+
class_thetas_sd = np.repeat(.01, class_thetas.size)
|
|
913
|
+
if self.fixed_solution is not None:
|
|
914
|
+
class_thetas_sd = self.fixed_solution['model'].class_x_stderr
|
|
915
|
+
log_lik_old, log_lik_new = -1E10, -1E10
|
|
916
|
+
iter, max_iter = 0, 1000
|
|
917
|
+
terminate, converged = False, False
|
|
918
|
+
self.H = None
|
|
919
|
+
while not terminate and iter < max_iter:
|
|
920
|
+
# {
|
|
921
|
+
prev_converged = False
|
|
922
|
+
self.ind_pred_prob_classes = []
|
|
923
|
+
self.choice_pred_prob_classes = []
|
|
924
|
+
p = self.get_p(0, X, y, class_betas, class_idxs, class_fxidxs, class_fxtransidxs)
|
|
925
|
+
self.varnames = self.global_varnames # Reset varnames
|
|
926
|
+
self.H = self.posterior_est_latent_class_probability(class_thetas)
|
|
927
|
+
for i in range(1, self.num_classes): # {
|
|
928
|
+
new_p = self.get_p(i, X, y, class_betas, class_idxs, class_fxidxs, class_fxtransidxs)
|
|
929
|
+
p = np.vstack((p, new_p))
|
|
930
|
+
# }
|
|
931
|
+
|
|
932
|
+
self.varnames = self.global_varnames # Reset varnames
|
|
933
|
+
|
|
934
|
+
weights = np.multiply(p, self.H) # Compute weights = p * H
|
|
935
|
+
weights = truncate_lower(weights, min_comp_val) # i.e., weights[weights == 0] = min_comp_val
|
|
936
|
+
log_lik = np.log(np.sum(weights, axis=0)) # Sum over classes
|
|
937
|
+
log_lik_new = np.sum(log_lik)
|
|
938
|
+
|
|
939
|
+
weights_individual = weights # Make a copy of weights
|
|
940
|
+
tiled = np.tile(np.sum(weights_individual, axis=0), (self.num_classes, 1))
|
|
941
|
+
weights_individual = np.divide(weights_individual, tiled) # Compute weights_individual / tiled
|
|
942
|
+
|
|
943
|
+
if hasattr(self, 'panel_info'):
|
|
944
|
+
# {
|
|
945
|
+
weights_new = np.zeros((self.num_classes, self.N))
|
|
946
|
+
log_ind_divide = np.zeros(self.N)
|
|
947
|
+
counter = 0
|
|
948
|
+
for ii, row in enumerate(self.panel_info):
|
|
949
|
+
# {
|
|
950
|
+
row_sum = int(np.sum(row))
|
|
951
|
+
for class_i in range(self.num_classes):
|
|
952
|
+
weights_new[class_i, counter:counter + row_sum] = np.repeat(weights[class_i, ii], row_sum)
|
|
953
|
+
log_ind_divide[counter:counter + row_sum] = 1 / row_sum
|
|
954
|
+
counter += row_sum
|
|
955
|
+
# }
|
|
956
|
+
weights = weights_new
|
|
957
|
+
# }
|
|
958
|
+
tiled = np.tile(np.sum(weights, axis=0), (self.num_classes, 1))
|
|
959
|
+
weights = np.divide(weights, tiled)
|
|
960
|
+
|
|
961
|
+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
962
|
+
# SOLVE OPTIMISATION PROBLEM
|
|
963
|
+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
964
|
+
converged = False
|
|
965
|
+
|
|
966
|
+
#TODO make sure I get the right thetas
|
|
967
|
+
self.pred_prob_all = np.array([])
|
|
968
|
+
self.global_transvars = self.transvars.copy()
|
|
969
|
+
|
|
970
|
+
for s in range(0, self.num_classes):
|
|
971
|
+
# {
|
|
972
|
+
self.update(s, class_fxidxs, class_fxtransidxs)
|
|
973
|
+
|
|
974
|
+
# Remove transvars which are not included in class params
|
|
975
|
+
self.transvars = [transvar for transvar in self.global_transvars if transvar in self.class_params_spec[s]]
|
|
976
|
+
|
|
977
|
+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
978
|
+
# SOLVE OPTIMISATION PROBLEM
|
|
979
|
+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
980
|
+
converged = False
|
|
981
|
+
args = (X[:, :, class_idxs[s]], y, weights[s, :].reshape(-1, 1), self.avail_latent[s])
|
|
982
|
+
|
|
983
|
+
|
|
984
|
+
#TODO THIS IS WHAT I MAINLY WHAT I WANT TO MANIPULATE
|
|
985
|
+
result = minimize(self.get_loglik_and_gradient, class_betas[s], jac=self.jac,
|
|
986
|
+
args=args, method="BFGS", tol=self.ftol, options={'gtol': self.gtol})
|
|
987
|
+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
988
|
+
# save predicted and observed probabilities to display in summary
|
|
989
|
+
self.varnames, self.transvars = self.global_varnames, self.global_transvars
|
|
990
|
+
p = self.compute_probabilities(result['x'], X[:, :, class_idxs[s]], avail)
|
|
991
|
+
self.ind_pred_prob, self.choice_pred_prob = p, p
|
|
992
|
+
self.pred_prob = np.mean(p, axis=0) # Compute: pred_prob[j] = average(p[:,j])
|
|
993
|
+
self.pred_prob_all = np.append(self.pred_prob_all, self.pred_prob)
|
|
994
|
+
self.ind_pred_prob_classes.append(self.ind_pred_prob)
|
|
995
|
+
self.choice_pred_prob_classes.append(self.choice_pred_prob)
|
|
996
|
+
|
|
997
|
+
if result['success'] or not prev_converged:
|
|
998
|
+
# {
|
|
999
|
+
converged = True
|
|
1000
|
+
prev_converged = result['success']
|
|
1001
|
+
class_betas[s] = result['x']
|
|
1002
|
+
prev_class_betas_sd = class_betas_sd
|
|
1003
|
+
tmp_calc = np.sqrt(abs(np.diag(result['hess_inv'])))
|
|
1004
|
+
for ii, tmp_beta_sd in enumerate(tmp_calc): # {
|
|
1005
|
+
if prev_class_betas_sd[s][ii] < 0.25 * tmp_beta_sd:
|
|
1006
|
+
tmp_calc[ii] = prev_class_betas_sd[s][ii]
|
|
1007
|
+
# }
|
|
1008
|
+
class_betas_sd[s] = tmp_calc
|
|
1009
|
+
# }
|
|
1010
|
+
# }
|
|
1011
|
+
|
|
1012
|
+
terminate = abs(log_lik_new - log_lik_old) < self.ftol_lccm
|
|
1013
|
+
log_lik_old = log_lik_new
|
|
1014
|
+
iter += 1
|
|
1015
|
+
|
|
1016
|
+
# }
|
|
1017
|
+
|
|
1018
|
+
# This code flattens the list class_betas by converting its elements to numpy arrays
|
|
1019
|
+
# and concatenating them into a single numpy array x.
|
|
1020
|
+
x = np.concatenate([np.array(betas) for betas in class_betas])
|
|
1021
|
+
|
|
1022
|
+
stderr = np.concatenate(class_betas_sd)
|
|
1023
|
+
|
|
1024
|
+
# Create result dictionary
|
|
1025
|
+
result = {'x': x, 'success': converged, 'fun': -log_lik_new, 'nit': iter,
|
|
1026
|
+
'stderr': stderr, 'is_latent_class': True, 'class_x': class_thetas.flatten(),
|
|
1027
|
+
'class_x_stderr': class_thetas_sd}
|
|
1028
|
+
|
|
1029
|
+
self.reset_fx()
|
|
1030
|
+
|
|
1031
|
+
p_class = np.mean(self.H, axis=1) # Compute: p_class[i] = average(H[i,:])
|
|
1032
|
+
pred_prob_tmp = np.zeros(self.J)
|
|
1033
|
+
|
|
1034
|
+
for i in range(self.num_classes): # {
|
|
1035
|
+
left = i * self.J
|
|
1036
|
+
right = left + self.J
|
|
1037
|
+
pred_prob_tmp += p_class[i] * self.pred_prob_all[left:right] # TO DO: DESCRIBE WHAT THIS IS DOING
|
|
1038
|
+
# }
|
|
1039
|
+
self.pred_prob = pred_prob_tmp
|
|
1040
|
+
return result
|
|
1041
|
+
# }
|
|
1042
|
+
def expectation_maximisation_algorithm(self, X, y, avail=None, weights=None,
|
|
1043
|
+
class_betas=None, class_thetas=None, validation=False):
|
|
1044
|
+
# {
|
|
1045
|
+
"""
|
|
1046
|
+
Run the EM algorithm by iterating between computing the
|
|
1047
|
+
posterior class probabilities and re-estimating the model parameters
|
|
1048
|
+
in each class by using a probability weighted loglik function
|
|
1049
|
+
|
|
1050
|
+
weights (array-like): weights is prior probability of class by the probability of y given the class.
|
|
1051
|
+
avail (array-like): Availability of alternatives for the choice situations. One when available or zero otherwise.
|
|
1052
|
+
|
|
1053
|
+
Comment (*): in scipy.optimse if "initial guess" is close to optimal
|
|
1054
|
+
then solution it will not build up a guess at the Hessian inverse
|
|
1055
|
+
# this if statement is intended to prevent this
|
|
1056
|
+
# Ad-hoc prevention
|
|
1057
|
+
"""
|
|
1058
|
+
class_thetas, class_betas, class_idxs, class_fxidxs, class_fxtransidxs = self.setup_em(X, class_thetas, class_betas)
|
|
1059
|
+
#getting best class_thetas
|
|
1060
|
+
if hasattr(self, "LCC_CLASS") and self.LCC_CLASS is not None:
|
|
1061
|
+
class_thetas = self.LCC_CLASS.get_thetas(self.member_params_spec, class_thetas)
|
|
1062
|
+
class_betas = self.LCC_CLASS.get_betas(self.class_params_spec_is, self.class_params_spec, class_betas)
|
|
1063
|
+
class_betas_sd = [np.repeat(.99, len(betas)) for betas in class_betas]
|
|
1064
|
+
|
|
1065
|
+
class_thetas_sd = np.repeat(.99, class_thetas.size)
|
|
1066
|
+
log_lik_old, log_lik_new = -1E10, -1E10
|
|
1067
|
+
iter, max_iter = 0, 2000 #TODO add this as an argument
|
|
1068
|
+
terminate, converged = False, False
|
|
1069
|
+
self.H = None
|
|
1070
|
+
while not terminate and iter < max_iter:
|
|
1071
|
+
# {
|
|
1072
|
+
prev_converged = False
|
|
1073
|
+
self.ind_pred_prob_classes = []
|
|
1074
|
+
self.choice_pred_prob_classes = []
|
|
1075
|
+
p = self.get_p(0, X, y, class_betas, class_idxs, class_fxidxs, class_fxtransidxs)
|
|
1076
|
+
self.varnames = self.global_varnames # Reset varnames
|
|
1077
|
+
self.H = self.posterior_est_latent_class_probability(class_thetas)
|
|
1078
|
+
for i in range(1, self.num_classes): # {
|
|
1079
|
+
new_p = self.get_p(i, X, y, class_betas, class_idxs, class_fxidxs, class_fxtransidxs)
|
|
1080
|
+
p = np.vstack((p, new_p))
|
|
1081
|
+
# }
|
|
1082
|
+
|
|
1083
|
+
self.varnames = self.global_varnames # Reset varnames
|
|
1084
|
+
|
|
1085
|
+
weights = np.multiply(p, self.H) # Compute weights = p * H
|
|
1086
|
+
weights = truncate_lower(weights, min_comp_val) # i.e., weights[weights == 0] = min_comp_val
|
|
1087
|
+
log_lik = np.log(np.sum(weights, axis=0)) # Sum over classes
|
|
1088
|
+
log_lik_new = np.sum(log_lik)
|
|
1089
|
+
|
|
1090
|
+
weights_individual = weights # Make a copy of weights
|
|
1091
|
+
tiled = np.tile(np.sum(weights_individual, axis=0), (self.num_classes, 1))
|
|
1092
|
+
weights_individual = np.divide(weights_individual, tiled) # Compute weights_individual / tiled
|
|
1093
|
+
|
|
1094
|
+
if hasattr(self, 'panel_info'):
|
|
1095
|
+
# {
|
|
1096
|
+
weights_new = np.zeros((self.num_classes, self.N))
|
|
1097
|
+
log_ind_divide = np.zeros(self.N)
|
|
1098
|
+
counter = 0
|
|
1099
|
+
for ii, row in enumerate(self.panel_info):
|
|
1100
|
+
# {
|
|
1101
|
+
row_sum = int(np.sum(row))
|
|
1102
|
+
for class_i in range(self.num_classes):
|
|
1103
|
+
weights_new[class_i, counter:counter+row_sum] = np.repeat(weights[class_i, ii], row_sum)
|
|
1104
|
+
log_ind_divide[counter:counter+row_sum] = 1/row_sum
|
|
1105
|
+
counter += row_sum
|
|
1106
|
+
# }
|
|
1107
|
+
weights = weights_new
|
|
1108
|
+
# }
|
|
1109
|
+
tiled = np.tile(np.sum(weights, axis=0), (self.num_classes, 1))
|
|
1110
|
+
weights = np.divide(weights, tiled)
|
|
1111
|
+
|
|
1112
|
+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
1113
|
+
# SOLVE OPTIMISATION PROBLEM
|
|
1114
|
+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
1115
|
+
#converged = False
|
|
1116
|
+
optimsation_convergences = True
|
|
1117
|
+
result = minimize(self.class_member_func, class_thetas, args=(weights_individual, X),
|
|
1118
|
+
jac=True, method='BFGS', tol=self.ftol, options={'gtol': self.gtol_membership_func})
|
|
1119
|
+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
1120
|
+
|
|
1121
|
+
if result['success']: # See commment (*)
|
|
1122
|
+
# {
|
|
1123
|
+
#converged = True
|
|
1124
|
+
class_thetas = result['x']
|
|
1125
|
+
prev_tmp_thetas_sd = class_thetas_sd
|
|
1126
|
+
tmp_thetas_sd = np.sqrt(abs(np.diag(result['hess_inv'])))
|
|
1127
|
+
for ii, tmp_theta_sd in enumerate(tmp_thetas_sd):
|
|
1128
|
+
# {
|
|
1129
|
+
if prev_tmp_thetas_sd[ii] < 0.25 * tmp_theta_sd and prev_tmp_thetas_sd[ii] != 0.01:
|
|
1130
|
+
tmp_thetas_sd[ii] = prev_tmp_thetas_sd[ii]
|
|
1131
|
+
if np.isclose(tmp_thetas_sd[ii], 1.0):
|
|
1132
|
+
tmp_thetas_sd[ii] = prev_tmp_thetas_sd[ii]
|
|
1133
|
+
# }
|
|
1134
|
+
class_thetas_sd = tmp_thetas_sd
|
|
1135
|
+
else:
|
|
1136
|
+
optimsation_convergences = False
|
|
1137
|
+
|
|
1138
|
+
self.pred_prob_all = np.array([])
|
|
1139
|
+
self.global_transvars = self.transvars.copy()
|
|
1140
|
+
|
|
1141
|
+
for s in range(0, self.num_classes):
|
|
1142
|
+
# {
|
|
1143
|
+
|
|
1144
|
+
self.update(s, class_fxidxs, class_fxtransidxs)
|
|
1145
|
+
|
|
1146
|
+
# Remove transvars which are not included in class params
|
|
1147
|
+
self.transvars = [transvar for transvar in self.global_transvars if transvar in self.class_params_spec[s]]
|
|
1148
|
+
|
|
1149
|
+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
1150
|
+
# SOLVE OPTIMISATION PROBLEM
|
|
1151
|
+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
1152
|
+
#converged = False
|
|
1153
|
+
#FIXME
|
|
1154
|
+
'''FIXME: flog this off for now I don't think i need this.
|
|
1155
|
+
if self.intercept_classes[s]:
|
|
1156
|
+
X_new = np.insert(X, 0, 1, axis=2)
|
|
1157
|
+
args = (X_new[:, :, class_idxs[s]], y, weights[s, :].reshape(-1, 1), self.avail_latent[s])
|
|
1158
|
+
else:
|
|
1159
|
+
'''
|
|
1160
|
+
args = (X[:, :, class_idxs[s]], y, weights[s, :].reshape(-1, 1), self.avail_latent[s])
|
|
1161
|
+
self.return_grad = False
|
|
1162
|
+
self.return_hess =False
|
|
1163
|
+
self.jac = False
|
|
1164
|
+
result = minimize(self.get_loglik_and_gradient, class_betas[s], jac = self.jac,
|
|
1165
|
+
args=args, method="BFGS", tol=self.ftol, options= {'gtol': self.gtol})
|
|
1166
|
+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
1167
|
+
# save predicted and observed probabilities to display in summary
|
|
1168
|
+
self.varnames, self.transvars = self.global_varnames, self.global_transvars
|
|
1169
|
+
p = self.compute_probabilities(result['x'], X[:, :, class_idxs[s]], avail)
|
|
1170
|
+
self.ind_pred_prob, self.choice_pred_prob = p, p
|
|
1171
|
+
self.pred_prob = np.mean(p, axis=0) # Compute: pred_prob[j] = average(p[:,j])
|
|
1172
|
+
self.pred_prob_all = np.append(self.pred_prob_all, self.pred_prob)
|
|
1173
|
+
self.ind_pred_prob_classes.append(self.ind_pred_prob)
|
|
1174
|
+
self.choice_pred_prob_classes.append(self.choice_pred_prob)
|
|
1175
|
+
|
|
1176
|
+
if result['success'] or not prev_converged:
|
|
1177
|
+
# {
|
|
1178
|
+
#converged = True
|
|
1179
|
+
prev_converged = result['success']
|
|
1180
|
+
class_betas[s] = result['x']
|
|
1181
|
+
prev_class_betas_sd = class_betas_sd
|
|
1182
|
+
tmp_calc = np.sqrt(abs(np.diag(result['hess_inv'])))
|
|
1183
|
+
for ii, tmp_beta_sd in enumerate(tmp_calc): # {
|
|
1184
|
+
if prev_class_betas_sd[s][ii] < 0.25 * tmp_beta_sd:
|
|
1185
|
+
tmp_calc[ii] = prev_class_betas_sd[s][ii]
|
|
1186
|
+
# }
|
|
1187
|
+
class_betas_sd[s] = tmp_calc
|
|
1188
|
+
else:
|
|
1189
|
+
optimsation_convergences = False
|
|
1190
|
+
|
|
1191
|
+
# }
|
|
1192
|
+
#NOTE absolute value removed as this could cause backwards and forwards repeitition
|
|
1193
|
+
terminate = np.abs(log_lik_new - log_lik_old) < self.ftol_lccm
|
|
1194
|
+
if self.verbose:
|
|
1195
|
+
print(f'Loglik: {log_lik_new:.4f}')
|
|
1196
|
+
|
|
1197
|
+
log_lik_old = log_lik_new
|
|
1198
|
+
iter += 1
|
|
1199
|
+
|
|
1200
|
+
# }
|
|
1201
|
+
|
|
1202
|
+
# This code flattens the list class_betas by converting its elements to numpy arrays
|
|
1203
|
+
# and concatenating them into a single numpy array x.
|
|
1204
|
+
x = np.concatenate([np.array(betas) for betas in class_betas])
|
|
1205
|
+
|
|
1206
|
+
stderr = np.concatenate(class_betas_sd)
|
|
1207
|
+
|
|
1208
|
+
# Create result dictionary
|
|
1209
|
+
result = {'x': x, 'success': optimsation_convergences, 'fun': -log_lik_new, 'nit': iter,
|
|
1210
|
+
'stderr': stderr, 'is_latent_class': True, 'class_x': class_thetas.flatten(),
|
|
1211
|
+
'class_x_stderr': class_thetas_sd}
|
|
1212
|
+
|
|
1213
|
+
self.reset_fx()
|
|
1214
|
+
|
|
1215
|
+
p_class = np.mean(self.H, axis=1) # Compute: p_class[i] = average(H[i,:])
|
|
1216
|
+
pred_prob_tmp = np.zeros(self.J)
|
|
1217
|
+
|
|
1218
|
+
for i in range(self.num_classes): # {
|
|
1219
|
+
left = i * self.J
|
|
1220
|
+
right = left + self.J
|
|
1221
|
+
pred_prob_tmp += p_class[i] * self.pred_prob_all[left:right] # TO DO: DESCRIBE WHAT THIS IS DOING
|
|
1222
|
+
# }
|
|
1223
|
+
self.pred_prob = pred_prob_tmp
|
|
1224
|
+
return result
|
|
1225
|
+
# }
|
|
1226
|
+
|
|
1227
|
+
''' ---------------------------------------------------------- '''
|
|
1228
|
+
''' Function. Compute the log-likelihood on the validation set '''
|
|
1229
|
+
''' QUERY: IS THIS EVER USED? '''
|
|
1230
|
+
''' ---------------------------------------------------------- '''
|
|
1231
|
+
def get_validation_loglik(self, validation_X, validation_Y, avail=None,
|
|
1232
|
+
avail_latent=None, weights=None, betas=None, panels=None):
|
|
1233
|
+
# {
|
|
1234
|
+
validation_X, _ = self.setup_design_matrix(validation_X)
|
|
1235
|
+
N = validation_X.shape[0]
|
|
1236
|
+
validation_Y = validation_Y.reshape(N, -1)
|
|
1237
|
+
|
|
1238
|
+
if panels is not None: # {
|
|
1239
|
+
ind_N = len(np.unique(panels))
|
|
1240
|
+
self.N = ind_N
|
|
1241
|
+
_, _, panel_info = self.balance_panels(validation_X, validation_Y, panels)
|
|
1242
|
+
self.panel_info = panel_info
|
|
1243
|
+
# }
|
|
1244
|
+
|
|
1245
|
+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
1246
|
+
# REMOVED CODE. WHY REPEAT THIS. ALREADY DONE IN SETUP FUNCTION?
|
|
1247
|
+
'''if avail_latent is None:
|
|
1248
|
+
self.avail_latent = np.repeat('None', self.num_classes)
|
|
1249
|
+
else:
|
|
1250
|
+
self.avail_latent = avail_latent'''
|
|
1251
|
+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
1252
|
+
|
|
1253
|
+
self.panels = panels
|
|
1254
|
+
class_betas = []
|
|
1255
|
+
counter = 0
|
|
1256
|
+
for ii, param_spec in enumerate(self.class_params_spec): # {
|
|
1257
|
+
idx = counter + self.get_betas_length(ii)
|
|
1258
|
+
class_betas.append(self.coeff_est[counter:idx])
|
|
1259
|
+
counter = idx
|
|
1260
|
+
# }
|
|
1261
|
+
|
|
1262
|
+
loglik = self.expectation_maximisation_algorithm(validation_X, validation_Y,
|
|
1263
|
+
avail=avail, weights=weights, class_betas=class_betas,
|
|
1264
|
+
class_thetas=self.class_x, validation=True)
|
|
1265
|
+
return loglik
|
|
1266
|
+
# }
|
|
1267
|
+
|
|
1268
|
+
''' ---------------------------------------------------------------- '''
|
|
1269
|
+
''' Function. Override bfgs function in multinomial logit to use EM '''
|
|
1270
|
+
''' ---------------------------------------------------------------- '''
|
|
1271
|
+
def bfgs_optimization(self, betas, X, y, weights, avail, maxiter, ftol, gtol, jac): # {
|
|
1272
|
+
if self.optimise_class == True and self.optimise_membership == False and self.fixed_solution is not None:
|
|
1273
|
+
thetas = self.fixed_solution['model'].class_x
|
|
1274
|
+
self.fixed_thetas = thetas
|
|
1275
|
+
result = self.fixed_expectation_algorithm(X, y, class_thetas = thetas, validation=self.validation)
|
|
1276
|
+
else:
|
|
1277
|
+
result = self.expectation_maximisation_algorithm(X, y, avail, validation=self.validation)
|
|
1278
|
+
self.converged = result.get('success')
|
|
1279
|
+
return result
|
|
1280
|
+
# }
|
|
1281
|
+
# }
|