Skip to main content

greeners_glm/
conditional.rs

1use greeners_core::error::GreenersError;
2use greeners_core::linalg::LinalgInverse as _;
3use greeners_core::types::InferenceType;
4use indexmap::IndexMap;
5use ndarray::{Array1, Array2};
6use statrs::distribution::{ContinuousCDF, Normal};
7use std::fmt;
8
9/// Result from Conditional Logit/Poisson models.
10#[derive(Debug)]
11pub struct ConditionalResult {
12    pub model_name: String,
13    /// Coefficients (no intercept — absorbed by group FE).
14    pub params: Array1<f64>,
15    pub std_errors: Array1<f64>,
16    pub z_values: Array1<f64>,
17    pub p_values: Array1<f64>,
18    pub log_likelihood: f64,
19    pub aic: f64,
20    pub bic: f64,
21    pub n_obs: usize,
22    pub n_groups: usize,
23    pub iterations: usize,
24    pub converged: bool,
25    pub inference_type: InferenceType,
26    pub variable_names: Option<Vec<String>>,
27}
28
29impl fmt::Display for ConditionalResult {
30    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
31        writeln!(f, "\n{:=^78}", format!(" {} Results ", self.model_name))?;
32        writeln!(
33            f,
34            "{:<20} {:>15} || {:<20} {:>15.4}",
35            "No. Observations:", self.n_obs, "Log-Likelihood:", self.log_likelihood
36        )?;
37        writeln!(
38            f,
39            "{:<20} {:>15} || {:<20} {:>15.4}",
40            "No. Groups:", self.n_groups, "AIC:", self.aic
41        )?;
42
43        writeln!(f, "\n{:-^78}", "")?;
44        writeln!(
45            f,
46            "{:<12} {:>10} {:>10} {:>8} {:>8}",
47            "", "coef", "std err", "z", "P>|z|"
48        )?;
49        writeln!(f, "{:-^78}", "")?;
50
51        for i in 0..self.params.len() {
52            let name = self
53                .variable_names
54                .as_ref()
55                .and_then(|n| n.get(i).cloned())
56                .unwrap_or_else(|| format!("x{}", i));
57            writeln!(
58                f,
59                "{:<12} {:>10.4} {:>10.4} {:>8.3} {:>8.3}",
60                name, self.params[i], self.std_errors[i], self.z_values[i], self.p_values[i]
61            )?;
62        }
63
64        writeln!(f, "{:=^78}", "")
65    }
66}
67
68impl ConditionalResult {
69    /// Model stats: (AIC, BIC, LogLik).
70    pub fn model_stats(&self) -> (f64, f64, f64) {
71        (self.aic, self.bic, self.log_likelihood)
72    }
73}
74
75/// Conditional Logit (Chamberlain's fixed-effects logit).
76///
77/// Conditions on the sum of y within each group, eliminating the group FE.
78/// Only groups with variation in y contribute to the likelihood.
79pub struct ConditionalLogit;
80
81impl ConditionalLogit {
82    /// Fit conditional logit.
83    /// `groups`: group ID for each observation.
84    pub fn fit(
85        y: &Array1<f64>,
86        x: &Array2<f64>,
87        groups: &[usize],
88    ) -> Result<ConditionalResult, GreenersError> {
89        Self::fit_with_names(y, x, groups, None)
90    }
91
92    pub fn fit_with_names(
93        y: &Array1<f64>,
94        x: &Array2<f64>,
95        groups: &[usize],
96        variable_names: Option<Vec<String>>,
97    ) -> Result<ConditionalResult, GreenersError> {
98        let n = y.len();
99        let k = x.ncols();
100
101        if n != groups.len() {
102            return Err(GreenersError::ShapeMismatch(
103                "y and groups must have same length".into(),
104            ));
105        }
106
107        // Group observations
108        let mut group_map: IndexMap<usize, Vec<usize>> = IndexMap::new();
109        for (i, &g) in groups.iter().enumerate() {
110            group_map.entry(g).or_default().push(i);
111        }
112
113        // Filter groups with variation in y (sum > 0 and sum < group_size)
114        let valid_groups: Vec<Vec<usize>> = group_map
115            .values()
116            .filter(|indices| {
117                let sum: f64 = indices.iter().map(|&i| y[i]).sum();
118                let n_g = indices.len() as f64;
119                sum > 0.5 && sum < n_g - 0.5
120            })
121            .cloned()
122            .collect();
123
124        if valid_groups.is_empty() {
125            return Err(GreenersError::InvalidOperation(
126                "No groups with variation in y".into(),
127            ));
128        }
129
130        let n_groups = valid_groups.len();
131
132        // Newton-Raphson on conditional log-likelihood
133        let mut beta = Array1::<f64>::zeros(k);
134        let max_iter = 100;
135        let tol = 1e-6;
136        let mut converged = false;
137        let mut iter = 0;
138        let mut log_likelihood = 0.0;
139
140        for iteration in 0..max_iter {
141            iter = iteration + 1;
142
143            let mut gradient = Array1::<f64>::zeros(k);
144            let mut hessian = Array2::<f64>::zeros((k, k));
145            log_likelihood = 0.0;
146
147            for group_indices in &valid_groups {
148                let n_g = group_indices.len();
149                let s_g: usize = group_indices.iter().map(|&i| y[i] as usize).sum();
150
151                // For small groups, enumerate all combinations of size s_g
152                // For large groups, approximate via conditional Poisson
153                if n_g <= 20 && s_g <= 10 {
154                    // Exact enumeration
155                    let xb: Vec<f64> = group_indices.iter().map(|&i| x.row(i).dot(&beta)).collect();
156
157                    // Observed: sum of x_i where y_i = 1
158                    let mut x_obs = Array1::<f64>::zeros(k);
159                    for &i in group_indices {
160                        if y[i] > 0.5 {
161                            x_obs = &x_obs + &x.row(i).to_owned();
162                        }
163                    }
164
165                    // Enumerate all combinations of s_g from n_g
166                    let combos = combinations(n_g, s_g);
167                    let mut e_x = Array1::<f64>::zeros(k);
168                    let mut e_xx = Array2::<f64>::zeros((k, k));
169
170                    // First pass: find max for log-sum-exp
171                    let combo_sums: Vec<f64> = combos
172                        .iter()
173                        .map(|combo| combo.iter().map(|&j| xb[j]).sum::<f64>())
174                        .collect();
175
176                    let max_sum = combo_sums.iter().copied().fold(f64::NEG_INFINITY, f64::max);
177
178                    let mut total_w = 0.0;
179                    let mut weighted_x = vec![Array1::<f64>::zeros(k); combos.len()];
180
181                    for (ci, combo) in combos.iter().enumerate() {
182                        let w = (combo_sums[ci] - max_sum).exp();
183                        total_w += w;
184
185                        let mut x_combo = Array1::<f64>::zeros(k);
186                        for &j in combo {
187                            x_combo = &x_combo + &x.row(group_indices[j]).to_owned();
188                        }
189                        weighted_x[ci] = x_combo;
190                    }
191
192                    let log_denom = max_sum + total_w.ln();
193                    let obs_sum: f64 = group_indices
194                        .iter()
195                        .filter(|&&i| y[i] > 0.5)
196                        .map(|&i| x.row(i).dot(&beta))
197                        .sum();
198
199                    log_likelihood += obs_sum - log_denom;
200
201                    //E[X] and E[XX'] under the conditional distribution
202                    for (ci, _combo) in combos.iter().enumerate() {
203                        let w = (combo_sums[ci] - max_sum).exp() / total_w;
204                        let x_c = &weighted_x[ci];
205                        e_x = &e_x + &(x_c * w);
206
207                        for a in 0..k {
208                            for b in 0..k {
209                                e_xx[[a, b]] += w * x_c[a] * x_c[b];
210                            }
211                        }
212                    }
213
214                    // Gradient: x_obs - E[X]
215                    gradient = &gradient + &(&x_obs - &e_x);
216
217                    //Hessian: -(E[XX'] -E[X]*E[X]')
218                    for a in 0..k {
219                        for b in 0..k {
220                            hessian[[a, b]] -= e_xx[[a, b]] - e_x[a] * e_x[b];
221                        }
222                    }
223                } else {
224                    // Large group approximation: use conditional Poisson (Andersen, 1970)
225                    // This is asymptotically equivalent
226                    let xb: Vec<f64> = group_indices.iter().map(|&i| x.row(i).dot(&beta)).collect();
227                    let exp_xb: Vec<f64> = xb.iter().map(|v| v.exp()).collect();
228                    let sum_exp: f64 = exp_xb.iter().sum();
229
230                    for (j_idx, &i) in group_indices.iter().enumerate() {
231                        let p_j = exp_xb[j_idx] / sum_exp;
232                        let diff = y[i] - s_g as f64 * p_j;
233                        for kk in 0..k {
234                            gradient[kk] += diff * x[[i, kk]];
235                        }
236
237                        for kk in 0..k {
238                            for ll in 0..k {
239                                hessian[[kk, ll]] -=
240                                    s_g as f64 * p_j * (1.0 - p_j) * x[[i, kk]] * x[[i, ll]];
241                            }
242                        }
243                    }
244
245                    // Approximate log-likelihood contribution
246                    for (j_idx, &i) in group_indices.iter().enumerate() {
247                        if y[i] > 0.5 {
248                            log_likelihood += (exp_xb[j_idx] / sum_exp).max(1e-15).ln();
249                        }
250                    }
251                }
252            }
253
254            // Newton step
255            let neg_hessian = -&hessian;
256            let inv_neg_hessian = match neg_hessian.inv() {
257                Ok(m) => m,
258                Err(_) => return Err(GreenersError::OptimizationFailed),
259            };
260
261            let change = inv_neg_hessian.dot(&gradient);
262            beta = &beta + &change;
263
264            if change.mapv(|v| v.powi(2)).sum().sqrt() < tol {
265                converged = true;
266                break;
267            }
268        }
269
270        // Standard errors from final Hessian
271        let mut final_hessian = Array2::<f64>::zeros((k, k));
272        for group_indices in &valid_groups {
273            let n_g = group_indices.len();
274            let s_g: usize = group_indices.iter().map(|&i| y[i] as usize).sum();
275
276            if n_g <= 20 && s_g <= 10 {
277                let xb: Vec<f64> = group_indices.iter().map(|&i| x.row(i).dot(&beta)).collect();
278                let combos = combinations(n_g, s_g);
279                let combo_sums: Vec<f64> = combos
280                    .iter()
281                    .map(|combo| combo.iter().map(|&j| xb[j]).sum::<f64>())
282                    .collect();
283                let max_sum = combo_sums.iter().copied().fold(f64::NEG_INFINITY, f64::max);
284
285                let mut total_w = 0.0;
286                let mut weighted_x = vec![Array1::<f64>::zeros(k); combos.len()];
287                for (ci, combo) in combos.iter().enumerate() {
288                    let w = (combo_sums[ci] - max_sum).exp();
289                    total_w += w;
290                    let mut x_combo = Array1::<f64>::zeros(k);
291                    for &j in combo {
292                        x_combo = &x_combo + &x.row(group_indices[j]).to_owned();
293                    }
294                    weighted_x[ci] = x_combo;
295                }
296
297                let mut e_x = Array1::<f64>::zeros(k);
298                let mut e_xx = Array2::<f64>::zeros((k, k));
299                for (ci, _) in combos.iter().enumerate() {
300                    let w = (combo_sums[ci] - max_sum).exp() / total_w;
301                    let x_c = &weighted_x[ci];
302                    e_x = &e_x + &(x_c * w);
303                    for a in 0..k {
304                        for b in 0..k {
305                            e_xx[[a, b]] += w * x_c[a] * x_c[b];
306                        }
307                    }
308                }
309
310                for a in 0..k {
311                    for b in 0..k {
312                        final_hessian[[a, b]] -= e_xx[[a, b]] - e_x[a] * e_x[b];
313                    }
314                }
315            } else {
316                let exp_xb: Vec<f64> = group_indices
317                    .iter()
318                    .map(|&i| x.row(i).dot(&beta).exp())
319                    .collect();
320                let sum_exp: f64 = exp_xb.iter().sum();
321
322                for (j_idx, &i) in group_indices.iter().enumerate() {
323                    let p_j = exp_xb[j_idx] / sum_exp;
324                    for kk in 0..k {
325                        for ll in 0..k {
326                            final_hessian[[kk, ll]] -=
327                                s_g as f64 * p_j * (1.0 - p_j) * x[[i, kk]] * x[[i, ll]];
328                        }
329                    }
330                }
331            }
332        }
333
334        let cov_matrix = (-&final_hessian).inv().unwrap_or(Array2::eye(k) * 1e-4);
335        let std_errors: Array1<f64> = (0..k).map(|i| cov_matrix[[i, i]].max(0.0).sqrt()).collect();
336
337        let normal_dist = Normal::standard();
338        let z_values = &beta / std_errors.mapv(|s| if s > 1e-15 { s } else { 1.0 });
339        let p_values = z_values.mapv(|z| 2.0 * (1.0 - normal_dist.cdf(z.abs())));
340
341        let k_f = k as f64;
342        let aic = -2.0 * log_likelihood + 2.0 * k_f;
343        let bic = -2.0 * log_likelihood + k_f * (n as f64).ln();
344
345        Ok(ConditionalResult {
346            model_name: "Conditional Logit".to_string(),
347            params: beta,
348            std_errors,
349            z_values,
350            p_values,
351            log_likelihood,
352            aic,
353            bic,
354            n_obs: n,
355            n_groups,
356            iterations: iter,
357            converged,
358            inference_type: InferenceType::Normal,
359            variable_names,
360        })
361    }
362}
363
364/// Conditional Poisson (Hausman-Hall-Griliches).
365///
366/// Conditions on the sum of y within each group.
367/// Equivalent to Poisson FE; the conditional likelihood eliminates the FE.
368pub struct ConditionalPoisson;
369
370impl ConditionalPoisson {
371    pub fn fit(
372        y: &Array1<f64>,
373        x: &Array2<f64>,
374        groups: &[usize],
375    ) -> Result<ConditionalResult, GreenersError> {
376        Self::fit_with_names(y, x, groups, None)
377    }
378
379    pub fn fit_with_names(
380        y: &Array1<f64>,
381        x: &Array2<f64>,
382        groups: &[usize],
383        variable_names: Option<Vec<String>>,
384    ) -> Result<ConditionalResult, GreenersError> {
385        let n = y.len();
386        let k = x.ncols();
387
388        if n != groups.len() {
389            return Err(GreenersError::ShapeMismatch(
390                "y and groups must have same length".into(),
391            ));
392        }
393
394        let mut group_map: IndexMap<usize, Vec<usize>> = IndexMap::new();
395        for (i, &g) in groups.iter().enumerate() {
396            group_map.entry(g).or_default().push(i);
397        }
398
399        //Filter groups with positive total count
400        let valid_groups: Vec<Vec<usize>> = group_map
401            .values()
402            .filter(|indices| {
403                let sum: f64 = indices.iter().map(|&i| y[i]).sum();
404                sum > 0.5
405            })
406            .cloned()
407            .collect();
408
409        if valid_groups.is_empty() {
410            return Err(GreenersError::InvalidOperation(
411                "No groups with positive counts".into(),
412            ));
413        }
414
415        let n_groups = valid_groups.len();
416
417        // Newton-Raphson on conditional Poisson log-likelihood
418        // L_g = Π (exp(x_it β) / Σ_s exp(x_is β))^{y_it}
419        let mut beta = Array1::<f64>::zeros(k);
420        let max_iter = 100;
421        let tol = 1e-6;
422        let mut converged = false;
423        let mut iter = 0;
424        let mut log_likelihood = 0.0;
425
426        for iteration in 0..max_iter {
427            iter = iteration + 1;
428
429            let mut gradient = Array1::<f64>::zeros(k);
430            let mut hessian = Array2::<f64>::zeros((k, k));
431            log_likelihood = 0.0;
432
433            for group_indices in &valid_groups {
434                let s_g: f64 = group_indices.iter().map(|&i| y[i]).sum();
435
436                let exp_xb: Vec<f64> = group_indices
437                    .iter()
438                    .map(|&i| x.row(i).dot(&beta).exp())
439                    .collect();
440                let sum_exp: f64 = exp_xb.iter().sum();
441
442                // Log-likelihood contribution
443                for (j_idx, &i) in group_indices.iter().enumerate() {
444                    if y[i] > 0.0 {
445                        log_likelihood += y[i] * (exp_xb[j_idx] / sum_exp).max(1e-15).ln();
446                    }
447                }
448
449                // Gradient and Hessian
450                let mut e_x = Array1::<f64>::zeros(k);
451                for (j_idx, &i) in group_indices.iter().enumerate() {
452                    let p_j = exp_xb[j_idx] / sum_exp;
453                    for kk in 0..k {
454                        e_x[kk] += p_j * x[[i, kk]];
455                    }
456                }
457
458                // Gradient: Σ y_it (x_it - E[x])
459                for &i in group_indices {
460                    for kk in 0..k {
461                        gradient[kk] += y[i] * (x[[i, kk]] - e_x[kk]);
462                    }
463                }
464
465                // Hessian: -s_g * (E[xx'] - E[x]E[x]')
466                let mut e_xx = Array2::<f64>::zeros((k, k));
467                for (j_idx, &i) in group_indices.iter().enumerate() {
468                    let p_j = exp_xb[j_idx] / sum_exp;
469                    for a in 0..k {
470                        for b in 0..k {
471                            e_xx[[a, b]] += p_j * x[[i, a]] * x[[i, b]];
472                        }
473                    }
474                }
475
476                for a in 0..k {
477                    for b in 0..k {
478                        hessian[[a, b]] -= s_g * (e_xx[[a, b]] - e_x[a] * e_x[b]);
479                    }
480                }
481            }
482
483            let neg_hessian = -&hessian;
484            let inv_neg_hessian = match neg_hessian.inv() {
485                Ok(m) => m,
486                Err(_) => return Err(GreenersError::OptimizationFailed),
487            };
488
489            let change = inv_neg_hessian.dot(&gradient);
490            beta = &beta + &change;
491
492            if change.mapv(|v| v.powi(2)).sum().sqrt() < tol {
493                converged = true;
494                break;
495            }
496        }
497
498        // Final covariance
499        let mut final_hessian = Array2::<f64>::zeros((k, k));
500        for group_indices in &valid_groups {
501            let s_g: f64 = group_indices.iter().map(|&i| y[i]).sum();
502            let exp_xb: Vec<f64> = group_indices
503                .iter()
504                .map(|&i| x.row(i).dot(&beta).exp())
505                .collect();
506            let sum_exp: f64 = exp_xb.iter().sum();
507
508            let mut e_x = Array1::<f64>::zeros(k);
509            let mut e_xx = Array2::<f64>::zeros((k, k));
510            for (j_idx, &i) in group_indices.iter().enumerate() {
511                let p_j = exp_xb[j_idx] / sum_exp;
512                for kk in 0..k {
513                    e_x[kk] += p_j * x[[i, kk]];
514                }
515                for a in 0..k {
516                    for b in 0..k {
517                        e_xx[[a, b]] += p_j * x[[i, a]] * x[[i, b]];
518                    }
519                }
520            }
521
522            for a in 0..k {
523                for b in 0..k {
524                    final_hessian[[a, b]] -= s_g * (e_xx[[a, b]] - e_x[a] * e_x[b]);
525                }
526            }
527        }
528
529        let cov_matrix = (-&final_hessian).inv().unwrap_or(Array2::eye(k) * 1e-4);
530        let std_errors: Array1<f64> = (0..k).map(|i| cov_matrix[[i, i]].max(0.0).sqrt()).collect();
531
532        let normal_dist = Normal::standard();
533        let z_values = &beta / std_errors.mapv(|s| if s > 1e-15 { s } else { 1.0 });
534        let p_values = z_values.mapv(|z| 2.0 * (1.0 - normal_dist.cdf(z.abs())));
535
536        let k_f = k as f64;
537        let aic = -2.0 * log_likelihood + 2.0 * k_f;
538        let bic = -2.0 * log_likelihood + k_f * (n as f64).ln();
539
540        Ok(ConditionalResult {
541            model_name: "Conditional Poisson".to_string(),
542            params: beta,
543            std_errors,
544            z_values,
545            p_values,
546            log_likelihood,
547            aic,
548            bic,
549            n_obs: n,
550            n_groups,
551            iterations: iter,
552            converged,
553            inference_type: InferenceType::Normal,
554            variable_names,
555        })
556    }
557}
558
559/// Conditional Multinomial Logit (McFadden's choice model).
560///
561/// Softmax likelihood within each choice set (group).
562/// Each group has `n_alts` alternatives; y indicates the chosen one.
563pub struct ConditionalMNLogit;
564
565impl ConditionalMNLogit {
566    /// Fit conditional multinomial logit.
567    ///
568    /// - `y`: chosen alternative index (0-based) for each choice occasion
569    /// - `x`: stacked design matrix (n_occasions * n_alts) x k
570    /// - `groups`: group/choice-set ID for each row of x
571    /// - `n_alts`: number of alternatives per choice set
572    pub fn fit(
573        y: &Array1<f64>,
574        x: &Array2<f64>,
575        groups: &[usize],
576        _n_alts: usize,
577    ) -> Result<ConditionalResult, GreenersError> {
578        Self::fit_with_names(y, x, groups, _n_alts, None)
579    }
580
581    pub fn fit_with_names(
582        y: &Array1<f64>,
583        x: &Array2<f64>,
584        groups: &[usize],
585        _n_alts: usize,
586        variable_names: Option<Vec<String>>,
587    ) -> Result<ConditionalResult, GreenersError> {
588        let n_rows = x.nrows();
589        let k = x.ncols();
590
591        if n_rows != groups.len() {
592            return Err(GreenersError::ShapeMismatch(
593                "x rows and groups must have same length".into(),
594            ));
595        }
596
597        // Build choice sets
598        let mut group_map: IndexMap<usize, Vec<usize>> = IndexMap::new();
599        for (i, &g) in groups.iter().enumerate() {
600            group_map.entry(g).or_default().push(i);
601        }
602
603        let mut choice_sets: Vec<Vec<usize>> = group_map.values().cloned().collect();
604        choice_sets.sort_by_key(|v| v[0]);
605
606        let n_occasions = y.len();
607        if choice_sets.len() != n_occasions {
608            return Err(GreenersError::ShapeMismatch(
609                "Number of groups must equal length of y".into(),
610            ));
611        }
612
613        // Newton-Raphson
614        let mut beta = Array1::<f64>::zeros(k);
615        let max_iter = 100;
616        let tol = 1e-6;
617        let mut converged = false;
618        let mut iter = 0;
619        let mut log_likelihood = 0.0;
620
621        for iteration in 0..max_iter {
622            iter = iteration + 1;
623            let mut gradient = Array1::<f64>::zeros(k);
624            let mut hessian = Array2::<f64>::zeros((k, k));
625            log_likelihood = 0.0;
626
627            for (occ, indices) in choice_sets.iter().enumerate() {
628                let chosen = y[occ] as usize;
629
630                // Compute exp(x_j' beta) for each alternative
631                let xb: Vec<f64> = indices.iter().map(|&i| x.row(i).dot(&beta)).collect();
632                let max_xb = xb.iter().copied().fold(f64::NEG_INFINITY, f64::max);
633                let exp_xb: Vec<f64> = xb.iter().map(|&v| (v - max_xb).exp()).collect();
634                let sum_exp: f64 = exp_xb.iter().sum();
635
636                // Log-likelihood: xb[chosen] - log(sum_exp) - max_xb + max_xb
637                if chosen < indices.len() {
638                    log_likelihood += xb[chosen] - max_xb - sum_exp.ln();
639                }
640
641                // Gradient and Hessian
642                let probs: Vec<f64> = exp_xb.iter().map(|&e| e / sum_exp).collect();
643
644                // E[x] = sum p_j x_j
645                let mut e_x = Array1::<f64>::zeros(k);
646                for (j, &idx) in indices.iter().enumerate() {
647                    let xj = x.row(idx);
648                    for kk in 0..k {
649                        e_x[kk] += probs[j] * xj[kk];
650                    }
651                }
652
653                // gradient += x_chosen - E[x]
654                if chosen < indices.len() {
655                    let x_chosen = x.row(indices[chosen]);
656                    for kk in 0..k {
657                        gradient[kk] += x_chosen[kk] - e_x[kk];
658                    }
659                }
660
661                //Hessian -= E[xx'] - E[x]E[x]'
662                for (j, &idx) in indices.iter().enumerate() {
663                    let xj = x.row(idx);
664                    for a in 0..k {
665                        for b in 0..k {
666                            hessian[[a, b]] -= probs[j] * xj[a] * xj[b];
667                        }
668                    }
669                }
670                for a in 0..k {
671                    for b in 0..k {
672                        hessian[[a, b]] += e_x[a] * e_x[b];
673                    }
674                }
675            }
676
677            let neg_hessian = -&hessian;
678            let inv_neg_hessian = match neg_hessian.inv() {
679                Ok(m) => m,
680                Err(_) => return Err(GreenersError::OptimizationFailed),
681            };
682
683            let change = inv_neg_hessian.dot(&gradient);
684            beta = &beta + &change;
685
686            if change.mapv(|v| v.powi(2)).sum().sqrt() < tol {
687                converged = true;
688                break;
689            }
690        }
691
692        // Standard errors
693        let mut final_hessian = Array2::<f64>::zeros((k, k));
694        for indices in &choice_sets {
695            let xb: Vec<f64> = indices.iter().map(|&i| x.row(i).dot(&beta)).collect();
696            let max_xb = xb.iter().copied().fold(f64::NEG_INFINITY, f64::max);
697            let exp_xb: Vec<f64> = xb.iter().map(|&v| (v - max_xb).exp()).collect();
698            let sum_exp: f64 = exp_xb.iter().sum();
699            let probs: Vec<f64> = exp_xb.iter().map(|&e| e / sum_exp).collect();
700
701            let mut e_x = Array1::<f64>::zeros(k);
702            for (j, &idx) in indices.iter().enumerate() {
703                let xj = x.row(idx);
704                for kk in 0..k {
705                    e_x[kk] += probs[j] * xj[kk];
706                }
707            }
708
709            for (j, &idx) in indices.iter().enumerate() {
710                let xj = x.row(idx);
711                for a in 0..k {
712                    for b in 0..k {
713                        final_hessian[[a, b]] -= probs[j] * xj[a] * xj[b];
714                    }
715                }
716            }
717            for a in 0..k {
718                for b in 0..k {
719                    final_hessian[[a, b]] += e_x[a] * e_x[b];
720                }
721            }
722        }
723
724        let cov_matrix = (-&final_hessian).inv().unwrap_or(Array2::eye(k) * 1e-4);
725        let std_errors: Array1<f64> = (0..k).map(|i| cov_matrix[[i, i]].max(0.0).sqrt()).collect();
726
727        let normal_dist = Normal::standard();
728        let z_values = &beta / std_errors.mapv(|s| if s > 1e-15 { s } else { 1.0 });
729        let p_values = z_values.mapv(|z| 2.0 * (1.0 - normal_dist.cdf(z.abs())));
730
731        let k_f = k as f64;
732        let n = n_rows;
733        let aic = -2.0 * log_likelihood + 2.0 * k_f;
734        let bic = -2.0 * log_likelihood + k_f * (n as f64).ln();
735
736        Ok(ConditionalResult {
737            model_name: "Conditional MNLogit".to_string(),
738            params: beta,
739            std_errors,
740            z_values,
741            p_values,
742            log_likelihood,
743            aic,
744            bic,
745            n_obs: n_rows,
746            n_groups: choice_sets.len(),
747            iterations: iter,
748            converged,
749            inference_type: greeners_core::types::InferenceType::Normal,
750            variable_names,
751        })
752    }
753}
754
755/// Generate all combinations of `r` elements from `0..n`.
756fn combinations(n: usize, r: usize) -> Vec<Vec<usize>> {
757    if r == 0 {
758        return vec![vec![]];
759    }
760    if r > n {
761        return vec![];
762    }
763
764    let mut result = Vec::new();
765    let mut combo = vec![0usize; r];
766    // Initialize
767    for (i, item) in combo.iter_mut().enumerate().take(r) {
768        *item = i;
769    }
770
771    loop {
772        result.push(combo.clone());
773
774        // Find rightmost element that can be incremented
775        let mut i = r;
776        loop {
777            if i == 0 {
778                return result;
779            }
780            i -= 1;
781            if combo[i] < n - r + i {
782                break;
783            }
784            if i == 0 {
785                return result;
786            }
787        }
788
789        combo[i] += 1;
790        for j in (i + 1)..r {
791            combo[j] = combo[j - 1] + 1;
792        }
793    }
794}