ngboost_rs/dist/
laplace.rs

1use crate::dist::{Distribution, DistributionMethods, RegressionDistn};
2use crate::scores::{CRPScore, LogScore, Scorable};
3use ndarray::{array, Array1, Array2, Array3};
4use rand::prelude::*;
5
6/// The Laplace distribution.
7#[derive(Debug, Clone)]
8pub struct Laplace {
9    /// The location parameter (mean/median).
10    pub loc: Array1<f64>,
11    /// The scale parameter.
12    pub scale: Array1<f64>,
13    /// The parameters of the distribution, stored as a 2D array.
14    _params: Array2<f64>,
15}
16
17impl Distribution for Laplace {
18    fn from_params(params: &Array2<f64>) -> Self {
19        let loc = params.column(0).to_owned();
20        let scale = params.column(1).mapv(f64::exp);
21        Laplace {
22            loc,
23            scale,
24            _params: params.clone(),
25        }
26    }
27
28    fn fit(y: &Array1<f64>) -> Array1<f64> {
29        // For Laplace, the MLE for loc is the median and scale is mean absolute deviation
30        let n = y.len();
31        if n == 0 {
32            return array![0.0, 0.0];
33        }
34
35        // Compute median
36        let mut sorted: Vec<f64> = y.to_vec();
37        sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
38        let median = if n % 2 == 0 {
39            (sorted[n / 2 - 1] + sorted[n / 2]) / 2.0
40        } else {
41            sorted[n / 2]
42        };
43
44        // Scale is mean absolute deviation from median
45        let mad: f64 = y.iter().map(|&x| (x - median).abs()).sum::<f64>() / n as f64;
46
47        array![median, mad.max(1e-6).ln()]
48    }
49
50    fn n_params(&self) -> usize {
51        2
52    }
53
54    fn predict(&self) -> Array1<f64> {
55        // Mean of Laplace is loc
56        self.loc.clone()
57    }
58
59    fn params(&self) -> &Array2<f64> {
60        &self._params
61    }
62}
63
64impl RegressionDistn for Laplace {}
65
66impl DistributionMethods for Laplace {
67    fn mean(&self) -> Array1<f64> {
68        self.loc.clone()
69    }
70
71    fn variance(&self) -> Array1<f64> {
72        // Variance of Laplace is 2 * scale^2
73        2.0 * &self.scale * &self.scale
74    }
75
76    fn std(&self) -> Array1<f64> {
77        self.variance().mapv(f64::sqrt)
78    }
79
80    fn pdf(&self, y: &Array1<f64>) -> Array1<f64> {
81        // pdf = 1/(2*scale) * exp(-|y - loc| / scale)
82        let mut result = Array1::zeros(y.len());
83        for i in 0..y.len() {
84            let abs_diff = (y[i] - self.loc[i]).abs();
85            result[i] = (-abs_diff / self.scale[i]).exp() / (2.0 * self.scale[i]);
86        }
87        result
88    }
89
90    fn logpdf(&self, y: &Array1<f64>) -> Array1<f64> {
91        // log(pdf) = -|y - loc| / scale - log(2 * scale)
92        let mut result = Array1::zeros(y.len());
93        for i in 0..y.len() {
94            let abs_diff = (y[i] - self.loc[i]).abs();
95            result[i] = -abs_diff / self.scale[i] - (2.0 * self.scale[i]).ln();
96        }
97        result
98    }
99
100    fn cdf(&self, y: &Array1<f64>) -> Array1<f64> {
101        // CDF for Laplace:
102        // if y < loc: 0.5 * exp((y - loc) / scale)
103        // if y >= loc: 1 - 0.5 * exp(-(y - loc) / scale)
104        let mut result = Array1::zeros(y.len());
105        for i in 0..y.len() {
106            let diff = y[i] - self.loc[i];
107            if diff < 0.0 {
108                result[i] = 0.5 * (diff / self.scale[i]).exp();
109            } else {
110                result[i] = 1.0 - 0.5 * (-diff / self.scale[i]).exp();
111            }
112        }
113        result
114    }
115
116    fn ppf(&self, q: &Array1<f64>) -> Array1<f64> {
117        // Inverse CDF (quantile function) for Laplace:
118        // if q < 0.5: loc + scale * ln(2*q)
119        // if q >= 0.5: loc - scale * ln(2*(1-q))
120        let mut result = Array1::zeros(q.len());
121        for i in 0..q.len() {
122            let q_clamped = q[i].clamp(1e-15, 1.0 - 1e-15);
123            if q_clamped < 0.5 {
124                result[i] = self.loc[i] + self.scale[i] * (2.0 * q_clamped).ln();
125            } else {
126                result[i] = self.loc[i] - self.scale[i] * (2.0 * (1.0 - q_clamped)).ln();
127            }
128        }
129        result
130    }
131
132    fn sample(&self, n_samples: usize) -> Array2<f64> {
133        let n_obs = self.loc.len();
134        let mut samples = Array2::zeros((n_samples, n_obs));
135        let mut rng = rand::rng();
136
137        for i in 0..n_obs {
138            for s in 0..n_samples {
139                // Use inverse CDF method
140                let u: f64 = rng.random();
141                if u < 0.5 {
142                    samples[[s, i]] = self.loc[i] + self.scale[i] * (2.0 * u).ln();
143                } else {
144                    samples[[s, i]] = self.loc[i] - self.scale[i] * (2.0 * (1.0 - u)).ln();
145                }
146            }
147        }
148        samples
149    }
150
151    fn median(&self) -> Array1<f64> {
152        // Median of Laplace is loc
153        self.loc.clone()
154    }
155
156    fn mode(&self) -> Array1<f64> {
157        // Mode of Laplace is loc
158        self.loc.clone()
159    }
160}
161
162impl Scorable<LogScore> for Laplace {
163    fn score(&self, y: &Array1<f64>) -> Array1<f64> {
164        // -logpdf(y) = |y - loc| / scale + log(2 * scale)
165        let mut scores = Array1::zeros(y.len());
166        for i in 0..y.len() {
167            let abs_diff = (y[i] - self.loc[i]).abs();
168            scores[i] = abs_diff / self.scale[i] + (2.0 * self.scale[i]).ln();
169        }
170        scores
171    }
172
173    fn d_score(&self, y: &Array1<f64>) -> Array2<f64> {
174        let n_obs = y.len();
175        let mut d_params = Array2::zeros((n_obs, 2));
176
177        for i in 0..n_obs {
178            let diff = self.loc[i] - y[i];
179            // d/d(loc) = sign(loc - y) / scale
180            d_params[[i, 0]] = diff.signum() / self.scale[i];
181            // d/d(log(scale)) = 1 - |loc - y| / scale
182            d_params[[i, 1]] = 1.0 - diff.abs() / self.scale[i];
183        }
184
185        d_params
186    }
187
188    fn metric(&self) -> Array3<f64> {
189        // Fisher Information Matrix for Laplace
190        let n_obs = self.loc.len();
191        let mut fi = Array3::zeros((n_obs, 2, 2));
192
193        for i in 0..n_obs {
194            let scale_sq = self.scale[i] * self.scale[i];
195            fi[[i, 0, 0]] = 1.0 / scale_sq;
196            fi[[i, 1, 1]] = 1.0;
197        }
198
199        fi
200    }
201}
202
203impl Scorable<CRPScore> for Laplace {
204    fn score(&self, y: &Array1<f64>) -> Array1<f64> {
205        // CRPS for Laplace distribution:
206        // CRPS(F, y) = |y - loc| + scale * exp(-|y - loc|/scale) - 0.75 * scale
207        let mut scores = Array1::zeros(y.len());
208        for i in 0..y.len() {
209            let abs_diff = (y[i] - self.loc[i]).abs();
210            let exp_term = (-abs_diff / self.scale[i]).exp();
211            scores[i] = abs_diff + self.scale[i] * exp_term - 0.75 * self.scale[i];
212        }
213        scores
214    }
215
216    fn d_score(&self, y: &Array1<f64>) -> Array2<f64> {
217        let n_obs = y.len();
218        let mut d_params = Array2::zeros((n_obs, 2));
219
220        for i in 0..n_obs {
221            let diff = self.loc[i] - y[i];
222            let abs_diff = diff.abs();
223            let exp_term = (-abs_diff / self.scale[i]).exp();
224
225            // d/d(loc)
226            d_params[[i, 0]] = diff.signum() * (1.0 - exp_term);
227
228            // d/d(log(scale)) - multiply by scale due to chain rule for log(scale)
229            d_params[[i, 1]] = exp_term * (self.scale[i] + abs_diff) - 0.75 * self.scale[i];
230        }
231
232        d_params
233    }
234
235    fn metric(&self) -> Array3<f64> {
236        // CRPS metric for Laplace
237        let n_obs = self.loc.len();
238        let mut fi = Array3::zeros((n_obs, 2, 2));
239
240        for i in 0..n_obs {
241            fi[[i, 0, 0]] = 0.5 / self.scale[i];
242            fi[[i, 1, 1]] = 0.25 * self.scale[i];
243        }
244
245        fi
246    }
247}
248
249#[cfg(test)]
250mod tests {
251    use super::*;
252    use approx::assert_relative_eq;
253
254    #[test]
255    fn test_laplace_distribution_methods() {
256        // params: [loc, log(scale)]
257        // First obs: loc=0, scale=1 (log(scale)=0)
258        // Second obs: loc=5, scale=2 (log(scale)=ln(2))
259        let params = Array2::from_shape_vec((2, 2), vec![0.0, 0.0, 5.0, 2.0_f64.ln()]).unwrap();
260        let dist = Laplace::from_params(&params);
261
262        // Test mean
263        let mean = dist.mean();
264        assert_relative_eq!(mean[0], 0.0, epsilon = 1e-10);
265        assert_relative_eq!(mean[1], 5.0, epsilon = 1e-10);
266
267        // Test variance: 2 * scale^2
268        let var = dist.variance();
269        assert_relative_eq!(var[0], 2.0, epsilon = 1e-10); // 2 * 1^2 = 2
270        assert_relative_eq!(var[1], 8.0, epsilon = 1e-10); // 2 * 2^2 = 8
271
272        // Test median = loc
273        let median = dist.median();
274        assert_relative_eq!(median[0], 0.0, epsilon = 1e-10);
275        assert_relative_eq!(median[1], 5.0, epsilon = 1e-10);
276
277        // Test mode = loc
278        let mode = dist.mode();
279        assert_relative_eq!(mode[0], 0.0, epsilon = 1e-10);
280        assert_relative_eq!(mode[1], 5.0, epsilon = 1e-10);
281    }
282
283    #[test]
284    fn test_laplace_cdf_ppf() {
285        // Create distribution with 3 observations to test ppf inverse
286        let params = Array2::from_shape_vec((3, 2), vec![0.0, 0.0, 0.0, 0.0, 0.0, 0.0]).unwrap();
287        let dist = Laplace::from_params(&params);
288
289        // CDF at loc should be 0.5
290        let y = Array1::from_vec(vec![0.0, 0.0, 0.0]);
291        let cdf = dist.cdf(&y);
292        assert_relative_eq!(cdf[0], 0.5, epsilon = 1e-10);
293
294        // PPF at 0.5 should be loc
295        let q = Array1::from_vec(vec![0.5, 0.5, 0.5]);
296        let ppf = dist.ppf(&q);
297        assert_relative_eq!(ppf[0], 0.0, epsilon = 1e-10);
298
299        // PPF inverse test
300        let q = Array1::from_vec(vec![0.25, 0.5, 0.75]);
301        let ppf = dist.ppf(&q);
302        let cdf_of_ppf = dist.cdf(&ppf);
303        assert_relative_eq!(cdf_of_ppf[0], 0.25, epsilon = 1e-10);
304        assert_relative_eq!(cdf_of_ppf[1], 0.5, epsilon = 1e-10);
305        assert_relative_eq!(cdf_of_ppf[2], 0.75, epsilon = 1e-10);
306    }
307
308    #[test]
309    fn test_laplace_pdf() {
310        let params = Array2::from_shape_vec((1, 2), vec![0.0, 0.0]).unwrap();
311        let dist = Laplace::from_params(&params);
312
313        // PDF at loc should be 1/(2*scale) = 0.5 for scale=1
314        let y = Array1::from_vec(vec![0.0]);
315        let pdf = dist.pdf(&y);
316        assert_relative_eq!(pdf[0], 0.5, epsilon = 1e-10);
317
318        // logpdf should match
319        let logpdf = dist.logpdf(&y);
320        assert_relative_eq!(logpdf[0], 0.5_f64.ln(), epsilon = 1e-10);
321    }
322
323    #[test]
324    fn test_laplace_sample() {
325        let params = Array2::from_shape_vec((1, 2), vec![5.0, 1.0_f64.ln()]).unwrap();
326        let dist = Laplace::from_params(&params);
327
328        let samples = dist.sample(1000);
329        assert_eq!(samples.shape(), &[1000, 1]);
330
331        // Check that sample mean is close to loc = 5
332        let sample_mean: f64 = samples.column(0).mean().unwrap();
333        assert!((sample_mean - 5.0).abs() < 0.3);
334
335        // Check that sample median is close to loc = 5
336        let mut sample_vec: Vec<f64> = samples.column(0).to_vec();
337        sample_vec.sort_by(|a, b| a.partial_cmp(b).unwrap());
338        let sample_median = sample_vec[500];
339        assert!((sample_median - 5.0).abs() < 0.3);
340    }
341
342    #[test]
343    fn test_laplace_fit() {
344        let y = Array1::from_vec(vec![1.0, 2.0, 3.0, 4.0, 5.0]);
345        let params = Laplace::fit(&y);
346        assert_eq!(params.len(), 2);
347        // Median should be 3.0
348        assert_relative_eq!(params[0], 3.0, epsilon = 1e-10);
349    }
350
351    #[test]
352    fn test_laplace_logscore() {
353        let params = Array2::from_shape_vec((1, 2), vec![0.0, 0.0]).unwrap();
354        let dist = Laplace::from_params(&params);
355
356        let y = Array1::from_vec(vec![0.0]);
357        let score = Scorable::<LogScore>::score(&dist, &y);
358
359        // Score at loc should be log(2*scale) = log(2) for scale=1
360        assert_relative_eq!(score[0], 2.0_f64.ln(), epsilon = 1e-10);
361    }
362
363    #[test]
364    fn test_laplace_crps() {
365        let params = Array2::from_shape_vec((1, 2), vec![0.0, 0.0]).unwrap();
366        let dist = Laplace::from_params(&params);
367
368        let y = Array1::from_vec(vec![0.0]);
369        let score = Scorable::<CRPScore>::score(&dist, &y);
370
371        // CRPS at loc = |0| + scale * exp(0) - 0.75*scale = 0 + 1 - 0.75 = 0.25 for scale=1
372        assert_relative_eq!(score[0], 0.25, epsilon = 1e-10);
373    }
374
375    #[test]
376    fn test_laplace_crps_d_score() {
377        let params = Array2::from_shape_vec((1, 2), vec![0.0, 0.0]).unwrap();
378        let dist = Laplace::from_params(&params);
379
380        let y = Array1::from_vec(vec![1.0]);
381        let d_score = Scorable::<CRPScore>::d_score(&dist, &y);
382
383        // Gradients should be finite
384        assert!(d_score[[0, 0]].is_finite());
385        assert!(d_score[[0, 1]].is_finite());
386    }
387
388    #[test]
389    fn test_laplace_interval() {
390        let params = Array2::from_shape_vec((1, 2), vec![0.0, 0.0]).unwrap();
391        let dist = Laplace::from_params(&params);
392
393        let (lower, upper) = dist.interval(0.1);
394        assert!(lower[0] < 0.0);
395        assert!(upper[0] > 0.0);
396        // Should be symmetric around loc
397        assert_relative_eq!(lower[0], -upper[0], epsilon = 1e-10);
398    }
399
400    #[test]
401    fn test_laplace_survival_function() {
402        let params = Array2::from_shape_vec((1, 2), vec![0.0, 0.0]).unwrap();
403        let dist = Laplace::from_params(&params);
404
405        // SF at loc should be 0.5
406        let y = Array1::from_vec(vec![0.0]);
407        let sf = dist.sf(&y);
408        assert_relative_eq!(sf[0], 0.5, epsilon = 1e-10);
409
410        // SF + CDF should equal 1
411        let cdf = dist.cdf(&y);
412        assert_relative_eq!(sf[0] + cdf[0], 1.0, epsilon = 1e-10);
413    }
414}