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.
@@ -0,0 +1,1566 @@
1
+ """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
2
+ IMPLEMENTATION: LATENT CLASS MIXED MODEL
3
+ """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
4
+ """
5
+ BACKGROUND - LATENT MIXED MODEL
6
+
7
+ A latent mixed model, also known as a latent mixed-effects model or a
8
+ latent variable mixed model, is a statistical model that combines
9
+ elements of both mixed-effects models and latent variable models.
10
+
11
+ Let's break down the components of a latent mixed model:
12
+
13
+ 1. Mixed-effects model: A mixed-effects model is a type of regression model
14
+ that incorporates both fixed effects and random effects. Fixed effects
15
+ represent population-level parameters that are assumed to be constant
16
+ across all individuals or groups, while random effects represent individual
17
+ or group-specific deviations from the population-level parameters.
18
+
19
+ 2. Latent variable model: A latent variable model posits the existence
20
+ of unobserved (latent) variables that underlie the observed data.
21
+ These latent variables are not directly measured but are inferred from
22
+ patterns in the observed data.
23
+
24
+ In a latent mixed model, the key idea is to include latent variables
25
+ as part of the random effects component of the mixed-effects model.
26
+ These latent variables capture unobserved heterogeneity or latent
27
+ traits that influence the outcome variable.
28
+
29
+ Here's how a latent mixed model might be formulated:
30
+
31
+ - Fixed Effects: Similar to traditional mixed-effects models, the fixed
32
+ effects component represents the population-level parameters that are
33
+ assumed to be constant across all individuals or groups.
34
+
35
+ - Random Effects: In addition to the traditional random effects
36
+ (e.g., random intercepts, random slopes), the random effects
37
+ component includes latent variables that capture unobserved
38
+ heterogeneity or latent traits among individuals or groups.
39
+
40
+ - Latent Variables: The latent variables are assumed to influence the outcome
41
+ variable indirectly through their effect on the observed predictors
42
+ or through their interaction with other variables in the model.
43
+ These latent variables can represent underlying traits, attitudes,
44
+ abilities, or other unobserved factors.
45
+
46
+ - Model Estimation: Estimating the parameters of a latent mixed
47
+ model typically involves fitting the model to the observed data
48
+ using statistical methods such as maximum likelihood estimation
49
+ (MLE), Bayesian estimation, or other estimation techniques.
50
+ The goal is to estimate both the fixed effects parameters and
51
+ the random effects parameters, including the parameters associated
52
+ with the latent variables.
53
+
54
+ Latent mixed models are particularly useful when there is interest
55
+ in capturing unobserved heterogeneity or latent traits that may
56
+ influence the outcome variable, while also accounting for the
57
+ hierarchical or clustered structure of the data using random effects.
58
+ These models are commonly used in fields such as psychology,
59
+ sociology, education, and epidemiology to study individual
60
+ differences and group-level effects.
61
+ """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
62
+
63
+
64
+ ''' ---------------------------------------------------------- '''
65
+ ''' LIBRARIES '''
66
+ ''' ---------------------------------------------------------- '''
67
+ import itertools
68
+ import logging
69
+ import time
70
+ import numpy as np
71
+ #import misc
72
+ from scipy.optimize import minimize
73
+
74
+
75
+
76
+ try:
77
+ from . import misc
78
+ from .mixed_logit import MixedLogit
79
+ from .boxcox_functions import truncate_lower, truncate_higher, truncate
80
+ from ._device import device as dev
81
+ except ImportError:
82
+ import misc
83
+ from mixed_logit import MixedLogit
84
+ from boxcox_functions import truncate_lower, truncate_higher, truncate
85
+ from _device import device as dev
86
+
87
+ ''' ---------------------------------------------------------- '''
88
+ ''' CONSTANTS - BOUNDS ON NUMERICAL VALUES '''
89
+ ''' ---------------------------------------------------------- '''
90
+ max_exp_val, min_exp_val = 700, -700
91
+ max_comp_val, min_comp_val = 1e+20, 1e-200 # or use float('inf')
92
+
93
+ ''' ---------------------------------------------------------- '''
94
+ ''' ERROR CHECKING AND LOGGING '''
95
+ ''' ---------------------------------------------------------- '''
96
+ logger = logging.getLogger(__name__)
97
+
98
+ ''' ---------------------------------------------------------- '''
99
+ ''' CLASS FOR ESTIMATION OF LATENT CLASS MODELS '''
100
+ ''' ---------------------------------------------------------- '''
101
+ class LatentClassMixedModel(MixedLogit):
102
+ # {
103
+ """ Docstring """
104
+
105
+ """
106
+ The design of this class is partly based on the LCCM package,
107
+ https://github.com/ferasz/LCCM (El Zarwi, 2017).
108
+
109
+ References
110
+ ----------
111
+ El Zarwi, F. (2017). lccm, a Python package for estimating latent
112
+ class choice models using the Expectation Maximization (EM)
113
+ algorithm to maximize the likelihood function.
114
+ """
115
+
116
+ # ===================
117
+ # CLASS PARAMETERS
118
+ # ===================
119
+ """"
120
+ X: Input data for explanatory variables / long format / array-like / shape (n_samples, n_variables)
121
+ y: Choices / array-like / shape (n_samples,)
122
+ varnames: Names of explanatory variables / list / shape (n_variables,)
123
+ int num_classes: Number of latent classes
124
+ alts: List of alternative names or indexes / long format / array-like / shape (n_samples,)
125
+ isvars: Names of individual-specific variables in varnames / list
126
+ transvars: Names of variables to apply transformation on / list / default=None
127
+ transformation: Transformation to apply to transvars / string / default="boxcox"
128
+ ids: Identifiers for choice situations / long format / array-like / shape (n_samples,)
129
+ weights: Weights for the choice situations / long format / array-like / shape (n_variables,) / default=None
130
+ avail: Availability indicator of alternatives for the choices (1 => available, 0 otherwise)/ array-like / shape (n_samples,)
131
+ base_alt: Base alternative / int, float or str / default=None
132
+ init_coeff: Initial coefficients for estimation/ numpy array / shape (n_variables,) / default=None
133
+ bool fit_intercept: Boolean indicator to include an intercept in the model / default=False
134
+ int maxiter: Maximum number of iterations / default=2000
135
+ dict randvars: Names (keys) and mixing distributions of random variables /
136
+ Distributions: n - normal, ln - lognormal, u - uniform, t - triangular, tn - truncated normal
137
+ params_spec: Array of lists containing names of variables for latent class / array_like / shape (n_variables,)
138
+ member_params_spec: Array of lists containing names of variables for class / array_like / shape (n_variables,)
139
+ panels: Identifiers to create panels in combination with ids / array-like / long format / shape (n_samples,) / default=None
140
+ method: Optimisation method for scipy.optimize.minimize / string / default="bfgs"
141
+ float ftol: Tolerance for scipy.optimize.minimize termination / default=1e-5
142
+ float gtol: Tolerance for scipy.optimize.minimize(method="bfgs") termination - gradient norm / default=1e-5
143
+ bool return_grad: Flag to calculate the gradient in _loglik_and_gradient / default=True
144
+ bool return_hess: Flag to calculate the hessian in _loglik_and_gradient / default=True
145
+ bool scipy_optimisation : Flag to apply optimiser / default=False / When false use own bfgs method.
146
+
147
+ int batch_size: Size of batches of random draws used to avoid overflowing memory during computations/ default=None
148
+ bool shuffle: Flag to shuffle the Halton draws / default=False
149
+ int n_draws: Random draws to approximate the mixing distributions of the random coefficients / default=1000
150
+ bool halton: Boolean flag for Halton draws / default=True
151
+ int drop: # of Halton draws to discard (initially) to minimize correlations between Halton sequences/ default=100
152
+ primes: List of primes for generation of Halton sequences / list
153
+ dict halton_opts: Options for generation of halton draws (shuffle, drop, primes) / default=None
154
+
155
+ """
156
+
157
+ # ===================
158
+ # CLASS FUNCTIONS
159
+ # ===================
160
+
161
+ """
162
+ 1. __init__(self);
163
+ 2. setup(self, X, y, ...);
164
+ 3. fit(self);
165
+ 4. post_process(self, optimization_res, coeff_names, sample_size, hess_inv=None);
166
+ 5. pch <-- compute_probabilities_latent(self, betas, X, y, panel_info, draws, drawstrans, avail);
167
+ 6. H <-- posterior_est_latent_class_probability(self, class_thetas);
168
+ 7. Loglik <-- class_member_func(self, class_thetas, weights, X);
169
+ 8. X_class_idx <-- get_class_X_idx2(self, class_num, coeff_names=None, **kwargs);
170
+ 9. X_class_idx <-- get_class_X_idx(self, class_num, coeff_names=None);
171
+ 10. Kchol <-- get_kchol(self, specs);
172
+ 11. len <-- get_betas_length(self, class_num);
173
+ 12. make_short_df(self, X);
174
+ 13. void set_bw(self, specs);
175
+ 14. rand_idx, randtrans_idx <-- update(self, i, class_idxs, class_fxidxs, class_fxtransidxs, class_rvidxs, class_rvtransidxs);
176
+ 15. expectation_maximisation_algorithm(self, tmp_fn, tmp_betas, args, class_betas=None, class_thetas=None, validation=False, **kwargs);
177
+ 16. result <-- bfgs_optimization(self, betas, X, y, weights, avail, maxiter);
178
+ """
179
+
180
+ ''' ---------------------------------------------------------- '''
181
+ ''' Function '''
182
+ ''' ---------------------------------------------------------- '''
183
+ def __init__(self, **kwargs): # {
184
+ self.verbose= 0
185
+ self.optimise_class = kwargs.get('optimise_class', False)
186
+ self.optimise_membership = kwargs.get('optimise_membership', False)
187
+ self.fixed_solution = kwargs.get('fixed_solution', None)
188
+ self.fixed_thetas = None if self.fixed_solution is None else self.fixed_solution['model'].class_x
189
+ self.save_fitted_params = False # speed-up computation
190
+ self.start_time = time.time()
191
+ self.descr = "LCMM"
192
+ super(LatentClassMixedModel, self).__init__()
193
+ # }
194
+
195
+ ''' ---------------------------------------------------------- '''
196
+ ''' Function. Set up the model '''
197
+ ''' ---------------------------------------------------------- '''
198
+ def setup(self, X, y, varnames=None, alts=None, isvars=None, num_classes=2,
199
+ class_params_spec=None, class_params_spec_is = None, member_params_spec=None,
200
+ transvars=None, transformation=None, ids=None, weights=None, avail=None,
201
+ avail_latent=None, # TODO?: separate param needed?
202
+ randvars=None, panels=None, base_alt=None, intercept_opts=None,
203
+ init_coeff=None, init_class_betas=None, init_class_thetas=None,
204
+ maxiter=2000, correlated_vars=None, n_draws=1000, halton=True,
205
+ batch_size=None, halton_opts=None, ftol=1e-5, ftol_lccmm=1e-4,
206
+ gtol=1e-5, gtol_membership_func=1e-5, return_hess=True, return_grad=True, method="bfgs",
207
+ validation=False, mnl_init=True, mxl_init=True, verbose=False):
208
+ # {
209
+
210
+
211
+ if varnames is not None and member_params_spec is not None:
212
+ varnames = misc.rearrage_varnames(varnames, member_params_spec)
213
+
214
+ #varnames = misc.rearrage_varnames(varnames, member_params_spec)
215
+ self.ftol, self.gtol = ftol, gtol
216
+ self.ftol_lccmm = ftol_lccmm
217
+ self.gtol_membership_func = gtol_membership_func
218
+ self.num_classes = num_classes
219
+ self.panels = panels
220
+ self.init_df, self.init_y = X, y
221
+ self.ids = ids
222
+ self.mnl_init = mnl_init
223
+ self.verbose = verbose
224
+ batch_size = n_draws if batch_size is None else min(n_draws, batch_size)
225
+ self.fit_intercept = misc.initialise_fit_intercept(class_params_spec, intercept_opts)
226
+ self.class_params_spec = misc.initialise_class_params_spec(class_params_spec, isvars, varnames, num_classes)
227
+ self.class_params_spec_is = misc.initialise_class_params_spec(class_params_spec_is, isvars, [], num_classes)
228
+ for i in range(num_classes):
229
+ self.class_params_spec[i] = [j for j in self.class_params_spec[i] if j not in self.class_params_spec_is[i]]
230
+ self.intercept_opts = misc.initialise_opts(intercept_opts, num_classes)
231
+ self.intercept_classes = [('_inter' in class_params_spec[var]) for var in range(len(class_params_spec))]
232
+ self.avail_latent = misc.initialise_avail_latent(avail_latent, num_classes)
233
+ self.membership_as_probability = misc.initialise_membership_as_probability(member_params_spec)
234
+
235
+ args = (self.membership_as_probability, member_params_spec, isvars, varnames, num_classes)
236
+ self.member_params_spec = misc.initialise_member_params_spec(*args)
237
+
238
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
239
+ # Initialise: MXL
240
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
241
+ if mxl_init and init_class_betas is None:
242
+ # {
243
+ init_class_betas = np.array(np.repeat('tmp', num_classes), dtype='object') # Create temp/template array
244
+ for i in range(num_classes):
245
+ # {
246
+ #Only want randvars if its in the class.
247
+ randvars_class = {var: randvars[var] for var in self.class_params_spec[i] if var in randvars}
248
+ mxl = MixedLogit()
249
+ try:
250
+ is_class = self.class_params_spec_is
251
+ except:
252
+ is_class = []
253
+ mxl = misc.setup_logit(i, mxl, X, y, varnames, self.class_params_spec, is_class, avail, alts, transvars, gtol,
254
+ mxl=True, panels=panels, randvars=randvars_class,
255
+ correlated_vars=correlated_vars, n_draws=n_draws, mnl_init=mnl_init)
256
+ init_class_betas = misc.revise_betas(i, mxl, init_class_betas, self.intercept_opts, self.alts)
257
+
258
+ # }
259
+ # }
260
+
261
+ self.init_class_betas = init_class_betas
262
+ self.init_class_thetas = init_class_thetas
263
+ self.validation = validation
264
+ self.ind_pred_prob_classes, self.choice_pred_prob_classes= [], []
265
+
266
+ if self.optimise_class and self.optimise_membership == False and self.fixed_solution is not None:
267
+ minimise_model = self.fixed_expectation_algorithm
268
+ else:
269
+ minimise_model = self.expectation_maximisation_algorithm
270
+ super(LatentClassMixedModel, self).setup(X, y, varnames, alts, isvars,
271
+ transvars, transformation, ids, weights, avail, randvars, panels, base_alt,
272
+ self.fit_intercept, init_coeff, maxiter, correlated_vars,
273
+ n_draws, halton, minimise_model, batch_size,
274
+ halton_opts, ftol, gtol, return_hess, return_grad, method, self.save_fitted_params, mnl_init)
275
+ # }
276
+
277
+ ''' ---------------------------------------------------------- '''
278
+ ''' Function. Fit multinomial and/or conditional logit models '''
279
+ ''' ---------------------------------------------------------- '''
280
+ def fit(self): # {
281
+ super(LatentClassMixedModel, self).fit()
282
+ # }
283
+
284
+ ''' ---------------------------------------------------------- '''
285
+ ''' Function '''
286
+ ''' ---------------------------------------------------------- '''
287
+ def post_process(self, optimization_res, coeff_names, sample_size,
288
+ hess_inv=None):
289
+ # {
290
+ if not self.validation:
291
+ super(LatentClassMixedModel, self).post_process(optimization_res, coeff_names, sample_size)
292
+ # }
293
+
294
+ ''' ---------------------------------------------------------- '''
295
+ ''' Function. Compute the standard logit-based probabilities '''
296
+ ''' Random and fixed coefficients are handled separately '''
297
+ ''' ---------------------------------------------------------- '''
298
+ def compute_probabilities_latent(self, betas, X, y, panel_info, draws, drawstrans, avail):
299
+ # {
300
+ # ________________________________________________________________
301
+ if dev.using_gpu: # {
302
+ X, y = dev.convert_array_gpu(X), dev.convert_array_gpu(y)
303
+ panel_info = dev.convert_array_gpu(panel_info)
304
+ draws = dev.convert_array_gpu(draws)
305
+ drawstrans = dev.convert_array_gpu(drawstrans)
306
+ if avail is not None: avail = dev.convert_array_gpu(avail)
307
+ # }
308
+ # _______________________________________________________________
309
+
310
+ beta_segment_names = ["Bf", "Br_b", "chol", "Br_w", "Bftrans",
311
+ "flmbda", "Brtrans_b", "Brtrans_w", "rlmda"]
312
+
313
+ iterations = [self.Kf, self.Kr, self.Kchol, self.Kbw, self.Kftrans,
314
+ self.Kftrans, self.Krtrans, self.Krtrans, self.Krtrans]
315
+ if sum(iterations) != len(betas):
316
+ print('dirty fix')
317
+ missing_amount = sum(iterations) - len(betas)
318
+ betas = np.append(betas, [0.01] * missing_amount) #
319
+
320
+ var_list = self.split_betas(betas, iterations, beta_segment_names)
321
+ Bf, Br_b, chol, Br_w, Bftrans, flmbda, Brtrans_b, Brtrans_w, rlmda = var_list.values()
322
+
323
+ # ______________________________________________________________________________________
324
+ if dev.using_gpu: # {
325
+ Bf, Br_b = dev.convert_array_gpu(Bf), dev.convert_array_gpu(Br_b)
326
+ chol = dev.convert_array_gpu(chol)
327
+ Br_w, Bftrans = dev.convert_array_gpu(Br_w), dev.convert_array_gpu(Bftrans)
328
+ flmbda = dev.convert_array_gpu(flmbda)
329
+ Brtrans_b, Brtrans_w = dev.convert_array_gpu(Brtrans_b), dev.convert_array_gpu(Brtrans_w)
330
+ rlmda = dev.convert_array_gpu(rlmda)
331
+ # }
332
+ # _________________________________________________________________________________________
333
+
334
+ chol_mat = np.zeros((self.correlationLength, self.correlationLength))
335
+ indices = np.tril_indices(self.correlationLength)
336
+
337
+ # __________________________________________________________
338
+ if dev.using_gpu: chol = dev.convert_array_cpu(chol)
339
+ # __________________________________________________________
340
+
341
+ try:
342
+ chol_mat[indices] = chol
343
+ except Exception as e:
344
+ print('why')
345
+
346
+ Kr_all = self.Kr + self.Krtrans
347
+ chol_mat_temp = np.zeros((self.Kr, self.Kr))
348
+
349
+ # TODO: Structure ... Kr first, Krtrans last, fill in for correlations
350
+ # TODO: could do better
351
+ rv_count, rv_count_all, rv_trans_count, rv_trans_count_all, chol_count = 0, 0, 0, 0, 0
352
+ corr_indices = []
353
+
354
+ # TODO: another bugfix
355
+ # Know beforehand to order rvtrans correctly
356
+ num_corr_rvtrans = 0
357
+ for ii, var in enumerate(self.varnames):
358
+ # {
359
+ if self.rvtransidx[ii] and hasattr(self, 'correlated_vars') \
360
+ and self.correlated_vars \
361
+ and hasattr(self.correlated_vars,'append') and var in self.correlated_vars:
362
+ num_corr_rvtrans += 1
363
+ # }
364
+
365
+ num_rvtrans_total = self.Krtrans + num_corr_rvtrans
366
+
367
+ if self.Kr > 0:
368
+ # {
369
+ for ii, var in enumerate(self.varnames): # TODO: BUGFIX
370
+ # {
371
+ #FIXME I believe this is and not or
372
+ is_correlated = hasattr(self, 'correlated_vars') and self.correlated_vars and (
373
+ hasattr(self.correlated_vars, 'append') and var in self.correlated_vars)
374
+
375
+ if self.rvidx[ii]:
376
+ # {
377
+ rv_val = chol[chol_count] if is_correlated else Br_w[rv_count]
378
+ chol_mat_temp[rv_count_all, rv_count_all] = rv_val
379
+ rv_count_all += 1
380
+
381
+ if is_correlated:
382
+ chol_count += 1
383
+ else:
384
+ rv_count += 1
385
+ # }
386
+
387
+ if self.rvtransidx[ii]:
388
+ # {
389
+ is_correlated = isinstance(self.correlated_vars, bool) and self.correlated_vars
390
+ rv_val = chol[chol_count] if is_correlated else Brtrans_w[rv_trans_count]
391
+ at = rv_trans_count_all - num_rvtrans_total
392
+ chol_mat_temp[at, at] = rv_val
393
+ rv_trans_count_all += 1
394
+
395
+ if is_correlated:
396
+ chol_count += 1
397
+ else:
398
+ rv_trans_count += 1
399
+ # }
400
+
401
+ if hasattr(self, 'correlated_vars') and self.correlated_vars:
402
+ # {
403
+ if hasattr(self.correlated_vars, 'append'):
404
+ # {
405
+ if var in self.correlated_vars:
406
+ # {
407
+ if self.rvidx[ii]:
408
+ corr_indices.append(rv_count_all - 1)
409
+ else:
410
+ corr_indices.append(Kr_all - num_rvtrans_total + rv_trans_count_all - 1) # TODO i think
411
+ # }
412
+ # }
413
+ # }
414
+ # }
415
+ if hasattr(self, 'correlated_vars') and isinstance(self.correlated_vars, bool) and self.correlated_vars:
416
+ corr_pairs = list(itertools.combinations(np.arange(self.Kr), 2)) + [(i, i) for i in range(self.Kr)]
417
+ else:
418
+ corr_pairs = list(itertools.combinations(corr_indices, 2)) + [(idx, idx) for ii, idx in
419
+ enumerate(corr_indices)]
420
+
421
+ reversed_corr_pairs = [tuple(reversed(pair)) for ii, pair in enumerate(corr_pairs)]
422
+ reversed_corr_pairs.sort(key=lambda x: x[0])
423
+
424
+ chol_count = 0
425
+
426
+ for _, corr_pair in enumerate(reversed_corr_pairs):
427
+ # {
428
+ # lower cholesky matrix
429
+ chol_mat_temp[corr_pair] = chol[chol_count]
430
+ chol_count += 1
431
+ # }
432
+ chol_mat = chol_mat_temp
433
+ # }
434
+
435
+
436
+
437
+ V = np.zeros((self.N, self.P, self.J, self.n_draws))
438
+
439
+ # __________________________________________________
440
+ if dev.using_gpu: # {
441
+ V = dev.convert_array_gpu(V)
442
+ chol_mat = dev.convert_array_gpu(chol_mat)
443
+ # }
444
+ # __________________________________________________
445
+
446
+ if self.Kf != 0: # {
447
+ Xf = X[:, :, :, self.fxidx]
448
+ if dev.using_gpu: Xf = dev.convert_array_gpu(Xf)
449
+ XBf = np.einsum('npjk,k -> npj', Xf, Bf, dtype=np.float64)
450
+ V += XBf[:, :, :, None]
451
+ # }
452
+
453
+ if self.Kr != 0: # {
454
+ Br = Br_b[None, :, None] + np.matmul(chol_mat, draws)
455
+ Br = self.apply_distribution(Br, self.rvdist)
456
+ self.Br = Br # save Br to use later
457
+ Xr = X[:, :, :, self.rvidx]
458
+ if dev.using_gpu: Xr = dev.convert_array_gpu(Xr)
459
+ XBr = dev.cust_einsum('npjk,nkr -> npjr', Xr, Br) # (N, P, J, R)
460
+ V += XBr
461
+ # }
462
+
463
+ # Apply transformations for variables with fixed coeffs
464
+ if self.Kftrans != 0:
465
+ # {
466
+ Xftrans = X[:, :, :, self.fxtransidx]
467
+ if dev.using_gpu: Xftrans = dev.convert_array_gpu(Xftrans)
468
+ Xftrans_lmda = self.trans_func(Xftrans, flmbda)
469
+ Xftrans_lmda[np.isneginf(Xftrans_lmda)] = -max_comp_val
470
+ Xftrans_lmda[np.isposinf(Xftrans_lmda)] = max_comp_val
471
+ # Estimating the linear utility specificiation (U = sum XB)
472
+ Xbf_trans = np.einsum('npjk,k -> npj', Xftrans_lmda, Bftrans, dtype=np.float64)
473
+ V += Xbf_trans[:, :, :, None] # Combining utilities
474
+ # }
475
+
476
+ # Apply transformations for variables with random coeffs
477
+ if self.Krtrans != 0:
478
+ # {
479
+ # Create the random coeffs:
480
+ Brtrans = Brtrans_b[None, :, None] + \
481
+ drawstrans[:, 0:self.Krtrans, :] * Brtrans_w[None, :, None]
482
+ Brtrans = self.apply_distribution(Brtrans, self.rvtransdist)
483
+ # Apply transformation:
484
+ Xrtrans = X[:, :, :, self.rvtransidx]
485
+ if dev.using_gpu: Xrtrans = dev.convert_array_gpu(Xrtrans)
486
+ Xrtrans_lmda = self.trans_func(Xrtrans, rlmda)
487
+ Xrtrans_lmda[np.isposinf(Xrtrans_lmda)] = 1e+30
488
+ Xrtrans_lmda[np.isneginf(Xrtrans_lmda)] = -1e+30
489
+ Xbr_trans = np.einsum('npjk, nkr -> npjr', Xrtrans_lmda, Brtrans, dtype=np.float64) # (N, P, J, R)
490
+ V += Xbr_trans # (N, P, J, R) # combining utilities
491
+ # }
492
+
493
+ if avail is not None: # {
494
+ ref = avail[:, :, :, None] if self.panels is not None else avail[:, None, :, None]
495
+ V = V * ref # Accommodate availablity of alts with panels or withut panels
496
+ # }
497
+
498
+ # Thresholds to avoid overflow warnings
499
+ V = truncate(V, -max_exp_val, max_exp_val)
500
+ eV = dev.np.exp(V)
501
+ sum_eV = dev.np.sum(eV, axis=2, keepdims=True)
502
+ sum_eV = truncate_lower(sum_eV, min_comp_val)
503
+ p = np.divide(eV, sum_eV, out=np.zeros_like(eV))
504
+ p = p * panel_info[:, :, None, None] if panel_info is not None else p
505
+ p = y * p
506
+
507
+ # collapse on alts
508
+ pch = np.sum(p, axis=2) # (N, P, R)
509
+
510
+ if hasattr(self, 'panel_info'):
511
+ pch = self.prob_product_across_panels(pch, self.panel_info)
512
+ else:
513
+ pch = np.mean(pch, axis=1) # (N, R)
514
+
515
+ pch = np.mean(pch, axis=1) # (N)
516
+ return pch.flatten()
517
+
518
+ # }
519
+
520
+ ''' ----------------------------------------------------------- '''
521
+ ''' Function: Get prior estimates of latent class probabilities '''
522
+ ''' ----------------------------------------------------------- '''
523
+ def posterior_est_latent_class_probability(self, class_thetas):
524
+ # {
525
+ """
526
+ class_thetas (array-like): Array of latent class vectors
527
+ H: Prior estimates of the class probabilities
528
+ """
529
+ class_thetas_original = class_thetas
530
+ if class_thetas.ndim == 1:
531
+ # {
532
+ new_class_thetas = np.array(np.repeat('tmp', self.num_classes - 1), dtype='object')
533
+ j = 0
534
+ for ii, member_params in enumerate(self.member_params_spec): # {
535
+ num_params = len(member_params)
536
+ tmp = class_thetas[j:j + num_params]
537
+ j += num_params
538
+ new_class_thetas[ii] = tmp
539
+ # }
540
+ class_thetas = new_class_thetas
541
+ # }
542
+
543
+ class_thetas_base = np.zeros(len(class_thetas[0]))
544
+
545
+ # coeff_names_without_intercept = self.global_varnames[(self.J-2):]
546
+ base_X_idx = self.get_member_X_idx(0)
547
+ member_df = np.transpose(self.short_df[:, base_X_idx])
548
+ member_N = member_df.shape[1]
549
+ eZB = np.zeros((self.num_classes, member_N))
550
+
551
+ if '_inter' in self.member_params_spec[0]: # {
552
+ ones = np.ones((1, member_N))
553
+ transposed = np.transpose(self.short_df[:, base_X_idx])
554
+ member_df = np.vstack((ones, transposed))
555
+ # }
556
+
557
+ if self.membership_as_probability: # {
558
+ H = np.tile(np.concatenate([1 - np.sum(class_thetas), class_thetas_original]), (member_N, 1))
559
+ H = np.transpose(H)
560
+ # }
561
+ else: # {
562
+ zB_q = np.dot(class_thetas_base[None, :], member_df)
563
+ eZB[0, :] = np.exp(zB_q)
564
+
565
+ for i in range(0, self.num_classes - 1):
566
+ # {
567
+ class_X_idx = self.get_member_X_idx(i)
568
+ member_df = np.transpose(self.short_df[:, class_X_idx])
569
+
570
+ # add in columns of ones for class-specific const (_inter)
571
+ if '_inter' in self.member_params_spec[i]: # {
572
+ print('off for now'
573
+ )
574
+ '''
575
+ member_df = np.vstack((np.ones((1, member_N)), np.transpose(self.short_df[:, class_X_idx])))
576
+ '''
577
+ # }
578
+
579
+ zB_q = np.dot(class_thetas[i].reshape((1, -1)), member_df)
580
+ zB_q = truncate_higher(zB_q, max_exp_val)
581
+ eZB[i + 1, :] = np.exp(zB_q)
582
+ # }
583
+ H = eZB / np.sum(eZB, axis=0, keepdims=True)
584
+ # }
585
+ self.class_freq = np.mean(H, axis=1) # store to display in summary
586
+ return H
587
+ # }
588
+
589
+ ''' ---------------------------------------------------------- '''
590
+ ''' Function '''
591
+ ''' ---------------------------------------------------------- '''
592
+ def class_member_func(self, class_thetas, weights, X):
593
+ # {
594
+ """Used in Maximisaion step. Used to find latent class vectors that
595
+ minimise the negative loglik where there is no observed dependent
596
+ variable (H replaces y).
597
+
598
+ Args:
599
+ class_thetas (array-like): (number of latent classes) - 1 array of
600
+ latent class vectors
601
+ weights (array-like): weights is prior probability of class by the
602
+ probability of y given the class.
603
+ X (array-like): Input data for explanatory variables in wide format
604
+ Returns:
605
+ ll [np.float64]: Loglik
606
+ """
607
+ H = self.posterior_est_latent_class_probability(class_thetas)
608
+ H = truncate_lower(H, 1e-30) # i.e., H[np.where(H < 1e-30)] = 1e-30
609
+ weight_post = np.multiply(np.log(H), weights)
610
+ ll = -np.sum(weight_post)
611
+ tgr = H - weights
612
+ gr = np.array([])
613
+
614
+ for i in range(1, self.num_classes):
615
+ # {
616
+ member_idx = self.get_member_X_idx(i - 1)
617
+ membership_df = self.short_df[:, member_idx]
618
+
619
+ if '_inter' in self.member_params_spec[i - 1]:
620
+ # {
621
+ membership_df = np.hstack((np.ones((self.short_df.shape[0], 1)), membership_df))
622
+ # }
623
+
624
+ if self.membership_as_probability:
625
+ membership_df = np.ones((self.short_df.shape[0], 1))
626
+
627
+ gr_i = np.dot(np.transpose(membership_df), tgr[i, :])
628
+ gr = np.concatenate((gr, gr_i))
629
+ # }
630
+ penalty = self.reg_penalty*sum(class_thetas)
631
+
632
+ return ll+penalty, gr.flatten()
633
+ # }
634
+
635
+ ''' ---------------------------------------------------------- '''
636
+ ''' Function. Get indices for X dataset for class parameters '''
637
+ ''' ---------------------------------------------------------- '''
638
+ def get_class_X_idx2(self, class_num, coeff_names=None, **kwargs):
639
+ # {
640
+ # below line: return indices of that class params in Xnames
641
+ # pattern matching for isvars
642
+
643
+ tmp_varnames = self.global_varnames.copy() if coeff_names is None else coeff_names.copy()
644
+ for ii, varname in enumerate(tmp_varnames): # {
645
+ if varname.startswith('lambda.'): tmp_varnames[ii] = varname[7:] # Remove lambda
646
+ if varname.startswith('sd.'): tmp_varnames[ii] = varname[3:]
647
+ # }
648
+
649
+ X_class_idx = np.array([], dtype='int32')
650
+ for var in self.class_params_spec[class_num]:
651
+ # {
652
+ for ii, var2 in enumerate(tmp_varnames):
653
+ # {
654
+ if 'inter' in var and 'inter' in var2 and coeff_names is not None: # only want to use summary func
655
+ # {
656
+ if 'class_intercept_alts' in self.intercept_opts:
657
+ # {
658
+ alt_num = int(var2.split('.')[-1])
659
+ if alt_num not in self.intercept_opts['class_intercept_alts'][class_num]:
660
+ continue
661
+ # }
662
+ # }
663
+ if var in var2:
664
+ X_class_idx = np.append(X_class_idx, ii)
665
+ # }
666
+ # }
667
+
668
+ # isvars handled if pass in full coeff names
669
+ X_class_idx = np.unique(X_class_idx)
670
+ X_class_idx = np.sort(X_class_idx)
671
+ X_class_idx_tmp = np.array([], dtype='int')
672
+ counter = 0
673
+
674
+ if coeff_names is not None:
675
+ return X_class_idx
676
+
677
+ for idx_pos in range(len(self.global_varnames)):
678
+ # {
679
+ if idx_pos in self.ispos:
680
+ # {
681
+ # fix bug of not all alts checked intercept
682
+ for i in range(self.J - 1):
683
+ # {
684
+ if idx_pos in X_class_idx:
685
+ # {
686
+ if self.global_varnames[idx_pos] == '_inter' and 'class_intercept_alts' in self.intercept_opts:
687
+ # {
688
+ if i + 2 not in self.intercept_opts['class_intercept_alts'][class_num]:
689
+ # {
690
+ counter += 1
691
+ continue
692
+ # }
693
+ # }
694
+ X_class_idx_tmp = np.append(X_class_idx_tmp, int(counter))
695
+ # }
696
+ counter += 1
697
+ # }
698
+ # }
699
+ else:
700
+ # {
701
+ if idx_pos in X_class_idx:
702
+ X_class_idx_tmp = np.append(X_class_idx_tmp, counter)
703
+ counter += 1
704
+ # }
705
+ # }
706
+
707
+ X_class_idx = X_class_idx_tmp
708
+
709
+ return X_class_idx
710
+ # }
711
+
712
+ ''' ---------------------------------------------------------- '''
713
+ ''' Function. Get indices for X dataset based on which '''
714
+ ''' parameters have been specified for the latent class '''
715
+ ''' ---------------------------------------------------------- '''
716
+ def get_class_X_idx(self, class_num, coeff_names=None):
717
+ # {
718
+ """
719
+ X_class_idx: indices to retrieve relevant
720
+ explanatory params of specified latent class
721
+ """
722
+ # below line: return indices of that class params in Xnames
723
+ # pattern matching for isvars
724
+
725
+ if coeff_names is None:
726
+ coeff_names = self.global_varnames.copy()
727
+ #tring to handle the global var names
728
+ if np.any(self.intercept_classes) == True:
729
+
730
+ #if self.intercept_classes[class_num]:
731
+ # {
732
+ inter_count = sum(1 for name in coeff_names if '_inter' in name)
733
+ num_in =len(self.alts) -1
734
+ if inter_count < num_in:
735
+ new_names = ['_inter' for i in range(num_in)]
736
+ new_names.extend([name for name in coeff_names if 'inter' not in name])
737
+ coeff_names = new_names
738
+ # }
739
+ tmp_varnames = coeff_names.copy()
740
+ for ii, varname in enumerate(tmp_varnames):
741
+ # {
742
+ # remove lambda so can get indices correctly
743
+ if varname.startswith('lambda.'):
744
+ tmp_varnames[ii] = varname[7:]
745
+
746
+ if varname.startswith('sd.'):
747
+ tmp_varnames[ii] = varname[3:]
748
+ # }
749
+
750
+ X_class_idx = np.array([], dtype="int")
751
+
752
+ for var in self.class_params_spec[class_num]:
753
+ # {
754
+ alt_num_counter = 1
755
+ # if 'inter' in var:
756
+ # alt_num_counter = 1
757
+ for ii, var2 in enumerate(tmp_varnames):
758
+ # {
759
+ if 'inter' in var and 'inter' in var2 and coeff_names is not None:
760
+ # {
761
+ if 'class_intercept_alts' in self.intercept_opts:
762
+ # {
763
+ if alt_num_counter not in self.intercept_opts['class_intercept_alts'][class_num]:
764
+ # {
765
+ alt_num_counter += 1
766
+ if alt_num_counter > 2:
767
+ continue # Skip current iteration of loop
768
+ # }
769
+ else:
770
+ alt_num_counter += 1
771
+ # }
772
+ # }
773
+
774
+ if var in var2:
775
+ X_class_idx = np.append(X_class_idx, ii)
776
+ # }
777
+ # }
778
+
779
+ X_class_idx = np.unique(X_class_idx)
780
+ X_class_idx = np.sort(X_class_idx)
781
+
782
+ return X_class_idx
783
+ # }
784
+
785
+ ''' -------------------------------------------------------------- '''
786
+ ''' Function. Get indices for X dataset based on which paramerters '''
787
+ ''' have been specified for the latent class membership '''
788
+ ''' -------------------------------------------------------------- '''
789
+ def get_member_X_idx(self, class_num, coeff_names=None):
790
+ # {
791
+ if coeff_names is None: # {
792
+ cond = ('_inter' in self.global_varnames) and (self.J > 2) # Evaluate boolean condition
793
+ ref = self.global_varnames[(self.J - 2):] if cond else self.global_varnames
794
+ coeff_names = ref.copy() # Make a copy of the reference list
795
+ # }
796
+
797
+ tmp_varnames = coeff_names.copy()
798
+ for ii, varname in enumerate(tmp_varnames):
799
+ # {
800
+ if varname.startswith('lambda.'):
801
+ tmp_varnames[ii] = varname[7:] # Remove lambda so can get indices correctly
802
+ # }
803
+
804
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
805
+ # Indices to retrieve relevant explanatory params of specified latent class
806
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
807
+ #_class_idx = np.array([], dtype='int32')
808
+
809
+
810
+
811
+
812
+ X_class_idx = np.array([], dtype='int32')
813
+ for ii, var in enumerate(self.member_params_spec[class_num]): # { #this causes error
814
+ if '_inter' not in var:
815
+ X_class_idx = np.append(X_class_idx, ii)
816
+ # }
817
+ # }
818
+
819
+ '''
820
+ for var in self.member_params_spec[class_num]: # {
821
+ for ii, var2 in enumerate(tmp_varnames): # {
822
+ if var == var2 and var != '_inter': #TODO changed this to equal
823
+ X_class_idx = np.append(X_class_idx, ii)
824
+ # }
825
+ # }
826
+ '''
827
+ X_class_idx = np.sort(X_class_idx)
828
+
829
+ return X_class_idx
830
+ # }
831
+
832
+ ''' -------------------------------------------------------------- '''
833
+ ''' Function. Permutations of specified params in correlation list '''
834
+ ''' -------------------------------------------------------------- '''
835
+ def get_kchol(self, specs):
836
+ # {
837
+ randvars_specs = [param for param in specs if param in self.randvars]
838
+ Kchol = 0
839
+ if (self.correlated_vars):
840
+ # {
841
+ if (isinstance(self.correlated_vars, list)): # {
842
+ corvars_in_spec = [corvar for corvar in self.correlated_vars if corvar in randvars_specs]
843
+ self.correlationLength = len(corvars_in_spec)
844
+ # }
845
+ else: # {
846
+ self.correlationLength = len(randvars_specs)
847
+ # i.e. correlation = True, Kchol permutations of rand vars
848
+ # }
849
+ Kchol = int(0.5 * self.correlationLength * (self.correlationLength + 1))
850
+ # }
851
+ return Kchol
852
+ # }
853
+
854
+ ''' ---------------------------------------------------------- '''
855
+ ''' Function. Get # betas for specified latent class '''
856
+ ''' ---------------------------------------------------------- '''
857
+ def get_betas_length(self, class_num):
858
+ # {
859
+ class_idx = self.get_class_X_idx(class_num) #FIXME i modified this
860
+ self.set_bw(class_idx)
861
+ class_params_spec = self.class_params_spec[class_num]
862
+ class_asvars = [x for x in class_params_spec if x in self.asvars]
863
+ class_randvars = [x for x in class_params_spec if x in self.randvars]
864
+ class_transvars = [x for x in class_params_spec if x in self.transvars]
865
+
866
+ betas_length = 0
867
+ if 'class_intercept_alts' in self.intercept_opts and '_inter' in class_params_spec:
868
+ # {
869
+ # separate logic for intercept
870
+ # QUERY. UNUSED CODE: class_isvars = [isvar for isvar in self.isvars if isvar != '_inter']
871
+ betas_length += len(self.intercept_opts['class_intercept_alts'][class_num])
872
+ # }
873
+ else: # {
874
+ class_isvars = [x for x in class_params_spec if x in self.isvars]
875
+ betas_length += (len(self.alts) - 1) * (len(class_isvars))
876
+ # }
877
+
878
+ betas_length += len(class_asvars)
879
+ betas_length += len(class_randvars)
880
+
881
+ # copied from choice model logic for Kchol
882
+ betas_length = self.get_kchol(class_params_spec)
883
+ betas_length += len(class_transvars) * 2
884
+ betas_length += sum(self.rvtransidx) # random trans vars
885
+ betas_length += self.Kbw
886
+ return betas_length
887
+ # }
888
+
889
+ ''' ---------------------------------------------------------- '''
890
+ ''' Function. Make a shortened dataframe '''
891
+ ''' Average over alts used in latent class estimation '''
892
+ ''' ---------------------------------------------------------- '''
893
+ def make_short_df(self, X):
894
+ # {
895
+ short_df = np.mean(np.mean(X, axis=2), axis=1) # 2... over alts
896
+
897
+ # Remove intercept columns
898
+ if self.fit_intercept: # {
899
+ short_df = short_df[:, (self.J - 2):]
900
+ short_df[:, 0] = 1
901
+ # }
902
+
903
+ # _________________________________________________
904
+ if dev.using_gpu:
905
+ short_df = dev.convert_array_cpu(short_df)
906
+ # _________________________________________________
907
+
908
+ self.short_df = short_df
909
+ # }
910
+
911
+ ''' ---------------------------------------------------------- '''
912
+ ''' Function '''
913
+ ''' ---------------------------------------------------------- '''
914
+ def set_bw(self, specs):
915
+ # {
916
+ specs = self.global_varnames[specs]
917
+ self.varnames = specs
918
+ randvars_specs = [param for param in specs if param in self.randvars]
919
+ Kr = len(randvars_specs)
920
+ self.Kbw, self.Kr = Kr, Kr # Set self.Kbw and self.Kr as Kr
921
+ self.Kftrans , self.Krtrans = sum(self.fxtransidx), sum(self.rvtransidx)
922
+ self.rvdist = [dist for ii, dist in enumerate(self.global_rvdist) if self.randvars[ii] in randvars_specs]
923
+
924
+ # Set up length of betas required to estimate correlation and/or
925
+ # random variable standard deviations, useful for cholesky matrix
926
+ if (self.correlated_vars):
927
+ # {
928
+ if (isinstance(self.correlated_vars, list)): # {
929
+ corvars_in_spec = [corvar for corvar in self.correlated_vars if corvar in randvars_specs]
930
+ self.correlationLength = len(corvars_in_spec)
931
+ self.Kbw = Kr - self.correlationLength
932
+ # }
933
+ else: # {
934
+ self.correlationLength, self.Kbw = Kr, 0
935
+ # }
936
+ # }
937
+ # }
938
+
939
+ def update_betas(self, betas):
940
+ ''' FIx to make sure the betas hold for latent class'''
941
+ iterations = [self.Kf, self.Kr, self.Kchol, self.Kbw, self.Kftrans,
942
+ self.Kftrans, self.Krtrans, self.Krtrans, self.Krtrans]
943
+ if sum(iterations) != len(betas):
944
+
945
+ missing_amount = sum(iterations) - len(betas)
946
+ betas = np.append(betas, [0.91] * missing_amount)
947
+ return betas
948
+
949
+ ''' ---------------------------------------------------------- '''
950
+ ''' Function '''
951
+ ''' ---------------------------------------------------------- '''
952
+ def update(self, i, class_idxs, class_fxidxs, class_fxtransidxs, class_rvidxs, class_rvtransidxs):
953
+ # {
954
+ #TODO FIX ME 4/11
955
+ self.Kf, self.Kr = sum(class_fxidxs[i]), sum(class_rvidxs[i])
956
+ self.fxidx, self.fxtransidx = class_fxidxs[i], class_fxtransidxs[i]
957
+ self.rvidx, self.rvtransidx= class_rvidxs[i], class_rvtransidxs[i]
958
+
959
+ # todo this need to be back respective to the intercept
960
+ #FIXME
961
+ #if self.intercept_classes[i] is False:
962
+ # class_idxs_sub = np.array([idx - (len(self.alts) - 2) for idx in class_idxs[i]])
963
+ #else:
964
+ class_idxs_sub = class_idxs[i]
965
+
966
+
967
+ self.set_bw(class_idxs_sub) # sets sd. and corr length
968
+ self.Kchol = self.get_kchol(self.global_varnames[class_idxs[i]])
969
+
970
+
971
+ rand_idx = [ii for ii, param in enumerate(self.randvars) if param in self.global_varnames[class_idxs_sub]]
972
+ randtrans_idx = [ii for ii, param in enumerate(self.randtransvars) if param in self.global_varnames[class_idxs_sub]]
973
+ return rand_idx, randtrans_idx
974
+ # }
975
+
976
+ ''' ---------------------------------------------------------- '''
977
+ ''' Function '''
978
+ ''' ---------------------------------------------------------- '''
979
+ def setup_em(self, X, y, class_thetas, class_betas):
980
+ # {
981
+ self.make_short_df(X)
982
+ self.global_rvdist = self.rvdist
983
+ self.global_varnames = self.varnames
984
+ class_idxs, class_fxidxs, class_fxtransidxs, class_rvidx, class_rvtransidxs = self.setup_class()
985
+
986
+ if '_inter' in self.global_varnames:
987
+ # {
988
+ for i in range(self.J - 2): #FIXME 5/11/24 adding _inter
989
+ self.global_varnames = np.concatenate((np.array([f'_inter.{i}'], dtype='<U64'), self.global_varnames))
990
+ # }
991
+
992
+ if X.ndim != 4: # {
993
+ X = X.reshape(self.N, self.P, self.J, -1)
994
+ y = y.reshape(self.N, self.P, self.J, -1)
995
+ # }
996
+
997
+ self.trans_pos = [ii for ii, var in enumerate(self.varnames) if var in self.transvars] # used for get_class_X_idx
998
+
999
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1000
+ # CLASS_THETAS
1001
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1002
+ if self.membership_as_probability:
1003
+ class_thetas = np.array([1 / (self.num_classes) for i in range(0, self.num_classes - 1)])
1004
+
1005
+ if class_thetas is None and self.init_class_thetas is not None:
1006
+ class_thetas = self.init_class_thetas
1007
+
1008
+ if class_thetas is None:
1009
+ # {
1010
+ len_class_thetas = [len(self.get_member_X_idx(i)) for i in range(0, self.num_classes - 1)]
1011
+ for ii, len_class_thetas_ii in enumerate(len_class_thetas): # {
1012
+ if '_inter' in self.member_params_spec[ii]:
1013
+ len_class_thetas[ii] = len_class_thetas[ii] + 1
1014
+ # }
1015
+ class_thetas = np.concatenate([
1016
+ np.zeros(len_class_thetas[i])
1017
+ for i in range(0, self.num_classes - 1)], axis=0)
1018
+ # }
1019
+
1020
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1021
+ # CLASS_BETAS
1022
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1023
+ if class_betas is None:
1024
+ # {
1025
+ class_betas = self.init_class_betas
1026
+ if class_betas is None:
1027
+ class_betas = [-0.1 * np.random.rand(self.get_betas_length(i)) for i in range(self.num_classes)]
1028
+ # }
1029
+
1030
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1031
+ return X, y, class_thetas, class_betas, class_idxs, class_fxidxs, class_fxtransidxs, class_rvidx, class_rvtransidxs
1032
+ # }
1033
+
1034
+ ''' ---------------------------------------------------------- '''
1035
+ ''' Function '''
1036
+ ''' ---------------------------------------------------------- '''
1037
+ def setup_class(self):
1038
+ # {
1039
+ self.global_fxidx, self.global_fxtransidx= self.fxidx, self.fxtransidx
1040
+ self.global_rvidx, self.global_rvtransidx = self.rvidx, self.rvtransidx
1041
+ class_idxs, class_fxidxs, class_fxtransidxs, class_rvidxs, class_rvtransidxs = [], [], [], [], []
1042
+
1043
+ for class_num in range(self.num_classes):
1044
+ # {
1045
+
1046
+ X_class_idx = self.get_class_X_idx(class_num) #FIXME 5 /11/24 change to one if broken
1047
+ class_idxs.append(X_class_idx)
1048
+
1049
+ # deal w/ fix indices
1050
+ class_fx_idx = [fxidx for ii, fxidx in enumerate(self.fxidx) if ii in X_class_idx]
1051
+ class_fxtransidx = [fxtransidx for ii, fxtransidx in enumerate(self.fxtransidx) if ii in X_class_idx]
1052
+
1053
+ # class_fxtransidx = np.repeat(False, len(X_class_idx))
1054
+ class_fxidxs.append(class_fx_idx)
1055
+ class_fxtransidxs.append(class_fxtransidx)
1056
+
1057
+ # deal w/ random indices
1058
+ class_rv_idx = [rvidx for ii, rvidx in enumerate(self.rvidx) if ii in X_class_idx]
1059
+ class_rvtransidx = [rvtransidx for ii, rvtransidx in enumerate(self.rvtransidx) if ii in X_class_idx]
1060
+
1061
+ # class_rvtransidx = np.repeat(False, len(X_class_idx))
1062
+ class_rvidxs.append(class_rv_idx)
1063
+ class_rvtransidxs.append(class_rvtransidx)
1064
+ # }
1065
+ return class_idxs, class_fxidxs, class_fxtransidxs, class_rvidxs, class_rvtransidxs
1066
+ # }
1067
+
1068
+ def fixed_expectation_algorithm(self, tmp_fn, tmp_betas, args, class_thetas = None, class_betas = None, validation=False, **kwargs):
1069
+ # {
1070
+
1071
+ X, y, panel_info, draws, drawstrans, weights, avail, batch_size = args
1072
+ if self.fixed_thetas is None:
1073
+ if self.fixed_solution is not None:
1074
+ self.fixed_thetas = self.fixed_solution['model'].class_x
1075
+
1076
+ if self.fixed_thetas is not None:
1077
+ class_thetas = self.fixed_thetas
1078
+
1079
+ X, y, class_thetas, class_betas, class_idxs, class_fxidxs, class_fxtransidxs, class_rvidxs, class_rvtransidxs \
1080
+ = self.setup_em(X, y, class_thetas, class_betas)
1081
+
1082
+
1083
+ for s in range(0, self.num_classes):
1084
+ # {
1085
+ rand_idx, randtrans_idx = self.update(s, class_idxs, class_fxidxs,
1086
+ class_fxtransidxs, class_rvidxs, class_rvtransidxs)
1087
+ updated_betas = self.update_betas(class_betas[s])
1088
+ class_betas[s] = updated_betas
1089
+ # np.random.uniform(0.23, 0.27, len(betas))
1090
+
1091
+ #class_betas_sd = [np.repeat(0.25, len(betas)) for betas in class_betas]
1092
+
1093
+
1094
+ class_betas_sd = [np.random.uniform(0.23, 0.27, len(betas)) for betas in class_betas]
1095
+ if self.fixed_solution is not None:
1096
+ class_thetas_sd = self.fixed_solution['model'].class_x_stderr
1097
+ else:
1098
+ class_thetas_sd = np.repeat(0.01, class_thetas.size)
1099
+ log_lik_old, log_lik_new = -1E10, -1E10
1100
+ iter, max_iter = 0, 100
1101
+ terminate = False
1102
+
1103
+ while not terminate and iter < max_iter:
1104
+ # {
1105
+
1106
+ self.ind_pred_prob_classes = []
1107
+ self.choice_pred_prob_classes = []
1108
+ args = (0, class_idxs, class_fxidxs, class_fxtransidxs, class_rvidxs, class_rvtransidxs)
1109
+ rand_idx, randtrans_idx = self.update(*args)
1110
+ args = (class_betas[0], X[:, :, :, class_idxs[0]], y, panel_info,
1111
+ draws[:, rand_idx, :], drawstrans[:, randtrans_idx, :], avail)
1112
+
1113
+ p = self.compute_probabilities_latent(*args)
1114
+
1115
+ H = self.posterior_est_latent_class_probability(class_thetas)
1116
+
1117
+ for i in range(1, self.num_classes):
1118
+ # {
1119
+
1120
+ args = (i, class_idxs, class_fxidxs, class_fxtransidxs, class_rvidxs, class_rvtransidxs)
1121
+ rand_idx, randtrans_idx = self.update(*args)
1122
+ args = (class_betas[i], X[:, :, :, class_idxs[i]], y, panel_info, draws[:, rand_idx, :],
1123
+ drawstrans[:, randtrans_idx, :], avail)
1124
+ new_p = self.compute_probabilities_latent(*args)
1125
+ p = np.vstack((p, new_p))
1126
+ # }
1127
+
1128
+ # ______________________________________
1129
+ if dev.using_gpu:
1130
+ p, H = dev.to_cpu(p), dev.to_cpu(H)
1131
+ # _____________________________________
1132
+
1133
+ weights = np.multiply(p, H)
1134
+ weights[weights == 0] = min_comp_val
1135
+ log_lik = np.log(np.sum(weights, axis=0)) # sum over classes
1136
+ log_lik_new = np.sum(log_lik)
1137
+ weights_individual = weights
1138
+ tiled = np.tile(np.sum(weights_individual, axis=0), (self.num_classes, 1))
1139
+ weights_individual = np.divide(weights_individual, tiled) # Compute weights_individual / tiled
1140
+
1141
+ # NOTE: REMOVED CODE. MAY 2024. SEEMS REDUNDANT!
1142
+ # tiled = np.tile(np.sum(weights, axis=0), (self.num_classes, 1))
1143
+ # weights = np.divide(weights, tiled)
1144
+
1145
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1146
+ # SOLVE OPTIMISATION PROBLEM
1147
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1148
+ converged = False
1149
+
1150
+
1151
+ self.pred_prob_all = np.array([])
1152
+ global_transvars = self.transvars.copy()
1153
+
1154
+ self.panel_info = getattr(self, 'panel_info', None) # i.e., Lookup or set as None
1155
+
1156
+ for s in range(0, self.num_classes):
1157
+ # {
1158
+ rand_idx, randtrans_idx = self.update(s, class_idxs, class_fxidxs,
1159
+ class_fxtransidxs, class_rvidxs, class_rvtransidxs)
1160
+ updated_betas = self.update_betas(class_betas[s])
1161
+ class_betas[s] = updated_betas
1162
+ #updates betas is longer than betas, how to copy updated betas into class_betas[s] so that
1163
+
1164
+ jac = True if self.return_grad else False # QUERY: WHY IS THIS REQUIRED? WHY NOT USE self.jac
1165
+ self.total_fun_eval = 0
1166
+
1167
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1168
+ # SOLVE OPTIMISATION PROBLEM
1169
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1170
+ converged = False
1171
+ '''Dont think i need this'''
1172
+ """
1173
+ if self.intercept_classes[s]:
1174
+ X_new = np.insert(X, 0, 1, axis=3)
1175
+ args = (X_new[:, :, :,class_idxs[s]], y, self.panel_info, draws[:, rand_idx, :],
1176
+ drawstrans[:, randtrans_idx, :], weights_individual[s, :], avail, batch_size)
1177
+ else:
1178
+ """
1179
+ args = (X[:, :, :, class_idxs[s]], y, self.panel_info, draws[:, rand_idx, :],
1180
+ drawstrans[:, randtrans_idx, :], weights_individual[s, :], avail, batch_size)
1181
+
1182
+ opt_res = minimize(self.get_loglik_gradient, class_betas[s], jac=jac, args=args, method="BFGS",
1183
+ tol=self.ftol, options={'gtol': self.gtol})
1184
+ """
1185
+ if self.intercept_classes[s]:
1186
+ p = self.compute_probabilities(opt_res['x'], X_new[:, :, :, class_idxs[s]], panel_info,
1187
+ draws[:, rand_idx, :], drawstrans[:, randtrans_idx, :], avail,
1188
+ self.var_list, self.chol_mat)
1189
+
1190
+ # save predicted and observed probabilities to display in summary
1191
+ else:
1192
+ """
1193
+ p = self.compute_probabilities(opt_res['x'], X[:, :, :, class_idxs[s]], panel_info,
1194
+ draws[:, rand_idx, :], drawstrans[:, randtrans_idx, :], avail,
1195
+ self.var_list, self.chol_mat)
1196
+
1197
+ self.choice_pred_prob = np.mean(p, axis=3)
1198
+ self.ind_pred_prob = np.mean(self.choice_pred_prob, axis=1)
1199
+ self.pred_prob = np.mean(self.ind_pred_prob, axis=0)
1200
+ self.prob_full = p
1201
+ self.transvars = global_transvars
1202
+ self.pred_prob_all = np.append(self.pred_prob_all, self.pred_prob)
1203
+ self.ind_pred_prob_classes.append(self.ind_pred_prob)
1204
+ self.choice_pred_prob_classes.append(self.choice_pred_prob)
1205
+
1206
+ if opt_res['success']:
1207
+ # {
1208
+ converged = True
1209
+ class_betas[s] = opt_res['x']
1210
+ prev_class_betas_sd = class_betas_sd
1211
+
1212
+ # Array tmp_calc contains the square roots of the absolute values
1213
+ # of the diagonal elements of the inverse Hessian matrix.
1214
+ tmp_calc = np.sqrt(np.abs(np.diag(opt_res['hess_inv'])))
1215
+
1216
+ # Compute the element-wise minimum between 0.25 * tmp_calc and
1217
+ # prev_class_betas_sd[s], without the need for an explicit loop.
1218
+ class_betas_sd[s] = np.minimum(0.25 * tmp_calc, prev_class_betas_sd[s])
1219
+ # }
1220
+ # }
1221
+
1222
+ self.varnames = self.global_varnames
1223
+
1224
+ terminate = log_lik_new - log_lik_old < self.ftol_lccmm
1225
+
1226
+ # DEBUGGING:
1227
+ # print('class betas: ', class_betas)
1228
+ # print('class_thetas: ', class_thetas)
1229
+ # print(f'Loglik: {log_lik_new:.4f}')
1230
+
1231
+ log_lik_old = log_lik_new
1232
+ iter += 1
1233
+ #class_thetas = class_thetas.reshape((self.num_classes - 1, -1))
1234
+ # }
1235
+
1236
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1237
+ # This code concatenates arrays stored in the class_betas list into a single NumPy array
1238
+ x = np.concatenate(class_betas)
1239
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1240
+
1241
+ stderr = np.concatenate(class_betas_sd)
1242
+ optimisation_result = {'x': x, 'success': converged, 'fun': -log_lik_new, 'nit': iter,
1243
+ 'stderr': stderr, 'is_latent_class': True, 'class_x': class_thetas.flatten(),
1244
+ 'class_x_stderr': class_thetas_sd, 'hess_inv': opt_res['hess_inv']}
1245
+
1246
+ self.fxidx, self.fxtransidx = self.global_fxidx, self.global_fxtransidx
1247
+ self.rvidx, self.rvtransidx = self.global_rvidx, self.global_rvtransidx
1248
+ self.varnames = self.global_varnames
1249
+
1250
+ p_class = np.mean(H, axis=1)
1251
+
1252
+ # --------------------------------------------------------
1253
+ if dev.using_gpu:
1254
+ self.pred_prob_all = dev.to_cpu(self.pred_prob_all)
1255
+ # ------------------------------------------------------
1256
+
1257
+ self.pred_prob = np.zeros(self.J)
1258
+ for i in range(self.num_classes): # {
1259
+ fr = i * self.J
1260
+ to = fr + self.J
1261
+ self.pred_prob += p_class[i] * self.pred_prob_all[fr:to]
1262
+ # }
1263
+ return optimisation_result
1264
+ ''' ---------------------------------------------------------- '''
1265
+ ''' Function '''
1266
+ ''' ---------------------------------------------------------- '''
1267
+
1268
+
1269
+ def expectation_maximisation_algorithm(self, tmp_fn, tmp_betas, args,
1270
+ class_betas=None, class_thetas=None, validation=False, **kwargs):
1271
+ X, y, panel_info, draws, drawstrans, weights, avail, batch_size = args
1272
+ X, y, class_thetas, class_betas, class_idxs, class_fxidxs, class_fxtransidxs, class_rvidxs, class_rvtransidxs \
1273
+ = self.setup_em(X, y, class_thetas, class_betas)
1274
+
1275
+
1276
+ for s in range(0, self.num_classes):
1277
+ # {
1278
+ rand_idx, randtrans_idx = self.update(s, class_idxs, class_fxidxs,
1279
+ class_fxtransidxs, class_rvidxs, class_rvtransidxs)
1280
+ updated_betas = self.update_betas(class_betas[s])
1281
+ class_betas[s] = updated_betas
1282
+
1283
+
1284
+ class_betas_sd = [np.repeat(0.99, len(betas)) for betas in class_betas]
1285
+ class_thetas_sd = np.repeat(0.01, class_thetas.size)
1286
+ log_lik_old, log_lik_new = -1E10, -1E10
1287
+ iter, max_iter = 0, 2000
1288
+ terminate = False
1289
+
1290
+ while not terminate and iter < max_iter:
1291
+ prev_converged= False
1292
+ # {
1293
+ #print("Iteration = ", iter, "(", max_iter, ")")
1294
+ self.ind_pred_prob_classes = []
1295
+ self.choice_pred_prob_classes = []
1296
+ args = (0, class_idxs, class_fxidxs, class_fxtransidxs, class_rvidxs, class_rvtransidxs)
1297
+ rand_idx, randtrans_idx = self.update(*args)
1298
+ args = (class_betas[0], X[:, :, :, class_idxs[0]], y, panel_info,
1299
+ draws[:, rand_idx, :], drawstrans[:, randtrans_idx, :], avail)
1300
+
1301
+ # DEBUG: st = time()
1302
+ p = self.compute_probabilities_latent(*args)
1303
+ # DEBUG: end = time()
1304
+ # DEBUG: print(f"ComputeProbLatent: {end - st:.2f} seconds")
1305
+
1306
+ H = self.posterior_est_latent_class_probability(class_thetas)
1307
+
1308
+ for i in range(1, self.num_classes):
1309
+ # {
1310
+ args = (i, class_idxs, class_fxidxs, class_fxtransidxs, class_rvidxs, class_rvtransidxs)
1311
+ rand_idx, randtrans_idx = self.update(*args)
1312
+ args = (class_betas[i], X[:, :, :, class_idxs[i]], y, panel_info, draws[:, rand_idx, :],
1313
+ drawstrans[:, randtrans_idx, :], avail)
1314
+ new_p = self.compute_probabilities_latent(*args)
1315
+ p = np.vstack((p, new_p))
1316
+ # }
1317
+
1318
+ # ______________________________________
1319
+ if dev.using_gpu:
1320
+ p, H = dev.to_cpu(p), dev.to_cpu(H)
1321
+ # _____________________________________
1322
+
1323
+ weights = np.multiply(p, H)
1324
+ weights[weights == 0] = min_comp_val
1325
+ log_lik = np.log(np.sum(weights, axis=0)) # sum over classes
1326
+ log_lik_new = np.sum(log_lik)
1327
+ weights_individual = weights
1328
+ tiled = np.tile(np.sum(weights_individual, axis=0), (self.num_classes, 1))
1329
+ weights_individual = np.divide(weights_individual, tiled) # Compute weights_individual / tiled
1330
+
1331
+ # NOTE: REMOVED CODE. MAY 2024. SEEMS REDUNDANT!
1332
+ # tiled = np.tile(np.sum(weights, axis=0), (self.num_classes, 1))
1333
+ # weights = np.divide(weights, tiled)
1334
+
1335
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1336
+ # SOLVE OPTIMISATION PROBLEM
1337
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1338
+ converged = True
1339
+ opt_res = minimize(self.class_member_func, class_thetas.flatten(), jac=True,
1340
+ args=(weights_individual, X), method='BFGS', tol=self.ftol,
1341
+ options={'gtol': self.gtol_membership_func})
1342
+ #if opt_res['success'] or opt_res['status']== 2: # {
1343
+ if opt_res['success']:
1344
+ #converged = True
1345
+ class_thetas = opt_res['x']
1346
+ prev_tmp_thetas_sd = class_thetas_sd
1347
+ tmp_thetas_sd = np.sqrt(np.abs(np.diag(opt_res['hess_inv'])))
1348
+
1349
+ for ii, tmp_theta_sd in enumerate(tmp_thetas_sd):
1350
+ # {
1351
+ if prev_tmp_thetas_sd[ii] < 0.25 * tmp_theta_sd and prev_tmp_thetas_sd[ii] != 0.01 \
1352
+ or np.isclose(tmp_thetas_sd[ii], 1.0):
1353
+ tmp_thetas_sd[ii] = prev_tmp_thetas_sd[ii]
1354
+ # }
1355
+ class_thetas_sd = tmp_thetas_sd
1356
+ # }
1357
+ else:
1358
+ converged = False
1359
+
1360
+ self.pred_prob_all = np.array([])
1361
+ global_transvars = self.transvars.copy()
1362
+
1363
+ self.panel_info = getattr(self, 'panel_info', None) # i.e., Lookup or set as None
1364
+
1365
+ for s in range(0, self.num_classes):
1366
+ # {
1367
+ rand_idx, randtrans_idx = self.update(s, class_idxs, class_fxidxs,
1368
+ class_fxtransidxs, class_rvidxs, class_rvtransidxs)
1369
+ jac = True if self.return_grad else False # QUERY: WHY IS THIS REQUIRED? WHY NOT USE self.jac
1370
+ self.total_fun_eval = 0
1371
+
1372
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1373
+ # SOLVE OPTIMISATION PROBLEM
1374
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1375
+ #converged = False
1376
+ '''OLD CODE:
1377
+ if self.intercept_classes[s]:
1378
+ X_new = np.insert(X, 0, 1, axis=3)
1379
+ Xslice = X_new[:, :, :,class_idxs[s]]
1380
+ else:
1381
+ Xslice = X[:, :, :, class_idxs[s]]
1382
+ '''
1383
+ # NEW CODE
1384
+ class_index = class_idxs[s] # Extract the relevant class indices
1385
+
1386
+ # Check if intercept is needed and create the sliced array accordingly
1387
+ Xslice = X[:, :, :, class_index]
1388
+ #TODO flog this off for now. trying to get rid of the interepts
1389
+ '''
1390
+ Xslice = np.insert(X[:, :, :, class_index], 0, 1, axis=3) if self.intercept_classes[s] \
1391
+ else X[:, :, :, class_index]
1392
+ '''
1393
+ # END NEW CODE
1394
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1395
+ args = (Xslice, y, self.panel_info, draws[:, rand_idx, :],
1396
+ drawstrans[:, randtrans_idx, :], weights_individual[s, :], avail, batch_size)
1397
+
1398
+ # DEBUG: st = time()
1399
+ opt_res = minimize(self.get_loglik_gradient, class_betas[s], jac=jac, args=args, method="BFGS",
1400
+ tol=self.ftol, options={'gtol': self.gtol})
1401
+ # DEBUG: end = time()
1402
+ # DEBUG: print(f"Minimize[{s}]: {end - st:.2f} seconds")
1403
+
1404
+ # save predicted and observed probabilities to display in summary
1405
+ # DEBUG: st = time()
1406
+
1407
+ ''' OLD CODE:
1408
+ if self.intercept_classes[s]:
1409
+ Xslice = X_new[:, :, :, class_idxs[s]]
1410
+ else:
1411
+ Xslice = X[:, :, :, class_idxs[s]]
1412
+ '''
1413
+ p = self.compute_probabilities(opt_res['x'], Xslice, panel_info,
1414
+ draws[:, rand_idx, :], drawstrans[:, randtrans_idx, :], avail, self.var_list,
1415
+ self.chol_mat)
1416
+ # DEBUG: end = time()
1417
+ # DEBUG:print(f"ComputeProb{s}: {end-st:.2f} seconds")
1418
+
1419
+ self.choice_pred_prob = np.mean(p, axis=3)
1420
+ self.ind_pred_prob = np.mean(self.choice_pred_prob, axis=1)
1421
+ self.pred_prob = np.mean(self.ind_pred_prob, axis=0)
1422
+ self.prob_full = p
1423
+ self.transvars = global_transvars
1424
+ self.pred_prob_all = np.append(self.pred_prob_all, self.pred_prob)
1425
+ self.ind_pred_prob_classes.append(self.ind_pred_prob)
1426
+ self.choice_pred_prob_classes.append(self.choice_pred_prob)
1427
+
1428
+ #if opt_res['success'] or opt_res['status']== 2:
1429
+ if opt_res['success'] or not prev_converged:
1430
+ prev_class_betas_sd = opt_res['success']
1431
+ # {
1432
+ #converged = True
1433
+ class_betas[s] = opt_res['x']
1434
+ prev_class_betas_sd = class_betas_sd
1435
+
1436
+ # Array tmp_calc contains the square roots of the absolute values
1437
+ # of the diagonal elements of the inverse Hessian matrix.
1438
+ tmp_calc = np.sqrt(np.abs(np.diag(opt_res['hess_inv'])))
1439
+
1440
+ # Compute the element-wise minimum between 0.25 * tmp_calc and
1441
+ # prev_class_betas_sd[s], without the need for an explicit loop.
1442
+ class_betas_sd[s] = np.minimum(0.25 * tmp_calc, prev_class_betas_sd[s])
1443
+ else:
1444
+ converged = False
1445
+ # }
1446
+
1447
+ self.varnames = self.global_varnames
1448
+ terminate = np.abs(log_lik_new - log_lik_old) < self.ftol_lccmm
1449
+ if self.verbose > 1:
1450
+ print(f'Loglik: {log_lik_new:.4f}')
1451
+ # DEBUGGING:
1452
+ # print('class betas: ', class_betas)
1453
+ # print('class_thetas: ', class_thetas)
1454
+ # print(f'Loglik: {log_lik_new:.4f}')
1455
+
1456
+ log_lik_old = log_lik_new
1457
+ iter += 1
1458
+ #FIX ME this falls over because it assumes we have same class sizes
1459
+ # TODO turning off for now, see if this holds up
1460
+ #class_thetas = class_thetas.reshape((self.num_classes - 1, -1))
1461
+ # }
1462
+
1463
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1464
+ # This code concatenates arrays stored in the class_betas list into a single NumPy array
1465
+ x = np.concatenate(class_betas)
1466
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1467
+
1468
+ stderr = np.concatenate(class_betas_sd)
1469
+ optimisation_result = {'x': x, 'success': converged, 'fun': -log_lik_new, 'nit': iter,
1470
+ 'stderr': stderr, 'is_latent_class': True, 'class_x': class_thetas.flatten(),
1471
+ 'class_x_stderr': class_thetas_sd, 'hess_inv': opt_res['hess_inv']}
1472
+
1473
+ self.fxidx, self.fxtransidx = self.global_fxidx, self.global_fxtransidx
1474
+ self.rvidx, self.rvtransidx = self.global_rvidx, self.global_rvtransidx
1475
+ self.varnames = self.global_varnames
1476
+
1477
+ p_class = np.mean(H, axis=1)
1478
+
1479
+ # --------------------------------------------------------
1480
+ if dev.using_gpu:
1481
+ self.pred_prob_all = dev.to_cpu(self.pred_prob_all)
1482
+ # ------------------------------------------------------
1483
+
1484
+ self.pred_prob = np.zeros(self.J)
1485
+ for i in range(self.num_classes):
1486
+ # {
1487
+ fr = i * self.J
1488
+ to = fr + self.J
1489
+ self.pred_prob += p_class[i] * self.pred_prob_all[fr:to]
1490
+ # }
1491
+ return optimisation_result
1492
+ # }
1493
+
1494
+
1495
+ ''' ---------------------------------------------------------- '''
1496
+ ''' Function: Computes the log-likelihood on the validation set'''
1497
+ ''' using the betas fitted using the training set '''
1498
+ ''' ---------------------------------------------------------- '''
1499
+ def validation_loglik(self, validation_X, validation_Y, panel_info=None, avail=None,
1500
+ weights=None, panels=None,
1501
+ betas=None, ids=None, batch_size=None, alts=None): # The inputs on this line are unused?
1502
+ # {
1503
+ N = len(np.unique(panels)) if panels is not None else self.N
1504
+ validation_X, Xnames = self.setup_design_matrix(validation_X)
1505
+
1506
+ if len(np.unique(panels)) != (N / self.J):
1507
+ # {
1508
+ X, y, avail, panel_info = self.balance_panels(validation_X, validation_Y, avail, panels)
1509
+ validation_X = X.reshape((N, self.P, self.J, -1))
1510
+ validation_Y = y.reshape((N, self.P, self.J, -1))
1511
+ # }
1512
+ else: # {
1513
+ validation_X = validation_X.reshape(N, self.P, self.J, -1)
1514
+ validation_Y = validation_Y.reshape(N, self.P, -1)
1515
+ # }
1516
+
1517
+ batch_size = self.n_draws
1518
+ self.N = N # store for use in EM alg
1519
+ # self.ids = ids
1520
+
1521
+ draws, drawstrans = self.generate_draws(N, self.n_draws) # (N,Kr,R)
1522
+
1523
+ class_betas = []
1524
+
1525
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1526
+ counter = 0
1527
+ for _ in self.class_params_spec: # {
1528
+ idx = counter + self.get_betas_length(0)
1529
+ class_betas.append(self.coeff_est[counter:idx])
1530
+ counter = idx
1531
+ # }
1532
+
1533
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1534
+ counter = 0
1535
+ for param_spec in self.member_params_spec: # {
1536
+ idx = counter + len(param_spec)
1537
+ class_betas.append(self.coeff_est[counter:idx])
1538
+ counter = idx
1539
+ # }
1540
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1541
+
1542
+ tmp_fn = None
1543
+ tmp_betas = class_betas
1544
+ args = (validation_X, validation_Y, panel_info, draws, drawstrans, weights, avail, batch_size)
1545
+ res = self.expectation_maximisation_algorithm(tmp_fn, tmp_betas, args, validation=True)
1546
+ return res
1547
+ # }
1548
+
1549
+ ''' ---------------------------------------------------------- '''
1550
+ ''' Function: Override bfgs function '''
1551
+ ''' ---------------------------------------------------------- '''
1552
+ #todo this doesn't actually do anything because it doesnt overide anything, unlike latent class model
1553
+ def bfgs_optimization(self, betas, X, y, weights, avail, maxiter): # {
1554
+ if self.optimise_class == True and self.optimise_membership == False and self.fixed_solution is not None:
1555
+ # {
1556
+ thetas = self.fixed_solution['model'].class_x
1557
+ self.fixed_thetas = thetas
1558
+ # ERROR HERE? INPUT LIST IS ODD?
1559
+ result = self.fixed_expectation_algorithm(X, y, betas, class_thetas = thetas, validation=self.validation)
1560
+ # }
1561
+ else:
1562
+ result = self.expectation_maximisation_algorithm(X, y, avail, validation=self.validation)
1563
+
1564
+ return result
1565
+ # }
1566
+ # }