SearchLibrium 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- old_code/__init__.py +8 -0
- old_code/_choice_model.py +1363 -0
- old_code/_device.py +145 -0
- old_code/akshay_test.py +125 -0
- old_code/boxcox_functions.py +116 -0
- old_code/draws.py +128 -0
- old_code/harmony.py +1261 -0
- old_code/latent_class_constrained.py +434 -0
- old_code/latent_class_mixed_model.py +1566 -0
- old_code/latent_class_model.py +1281 -0
- old_code/latent_main.py +945 -0
- old_code/main.py +1880 -0
- old_code/main_ol.py +127 -0
- old_code/misc.py +303 -0
- old_code/mixed_logit.py +1553 -0
- old_code/multinomial_logit.py +559 -0
- old_code/ordered_logit.py +1641 -0
- old_code/ordered_logit_mixed.py +103 -0
- old_code/ordered_logit_multinomial.py +701 -0
- old_code/r_ordered.py +168 -0
- old_code/rrm.py +521 -0
- old_code/search.py +3485 -0
- old_code/siman.py +1023 -0
- old_code/threshold.py +777 -0
- searchlibrium-0.0.1.dist-info/METADATA +21 -0
- searchlibrium-0.0.1.dist-info/RECORD +28 -0
- searchlibrium-0.0.1.dist-info/WHEEL +5 -0
- searchlibrium-0.0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,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
|
+
# }
|