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,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
+ # }