1use ndarray::{Array1, Array2, ArrayView1};
18
19use crate::compat::{np_mean, np_sum};
20use crate::error::{DmlError, Result};
21use crate::linalg::spd_inverse;
22use crate::stats::norm_ppf;
23
24const P: usize = 1;
25const Q: usize = 2;
26const NN_MATCHES: usize = 3;
27const C_C: f64 = 2.576;
29
30#[derive(Debug, Clone)]
33pub struct RdEstimate {
34 pub coef: [f64; 3],
35 pub se: [f64; 3],
36 pub ci: [[f64; 2]; 3],
38 pub h: (f64, f64),
40 pub b: (f64, f64),
42 pub n_h: (usize, usize),
44 pub n: (usize, usize),
46}
47
48struct Side {
50 x: Vec<f64>,
51 y: Vec<f64>,
52 t: Option<Vec<f64>>,
53 dups: Vec<usize>,
54 dupsid: Vec<usize>,
55}
56
57impl Side {
58 fn new(x: Vec<f64>, y: Vec<f64>, t: Option<Vec<f64>>) -> Self {
59 let (dups, dupsid) = duplicates(&x);
60 Self {
61 x,
62 y,
63 t,
64 dups,
65 dupsid,
66 }
67 }
68
69 fn n(&self) -> usize {
70 self.x.len()
71 }
72
73 fn select(&self, keep: &[bool]) -> Side {
74 let pick = |v: &[f64]| -> Vec<f64> {
75 v.iter()
76 .zip(keep)
77 .filter(|(_, &k)| k)
78 .map(|(a, _)| *a)
79 .collect()
80 };
81 let pick_n = |v: &[usize]| -> Vec<usize> {
82 v.iter()
83 .zip(keep)
84 .filter(|(_, &k)| k)
85 .map(|(a, _)| *a)
86 .collect()
87 };
88 Side {
89 x: pick(&self.x),
90 y: pick(&self.y),
91 t: self.t.as_ref().map(|t| pick(t)),
92 dups: pick_n(&self.dups),
93 dupsid: pick_n(&self.dupsid),
94 }
95 }
96
97 fn d(&self) -> Array2<f64> {
99 let k = 1 + usize::from(self.t.is_some());
100 let mut d = Array2::zeros((self.n(), k));
101 for i in 0..self.n() {
102 d[[i, 0]] = self.y[i];
103 if let Some(t) = &self.t {
104 d[[i, 1]] = t[i];
105 }
106 }
107 d
108 }
109}
110
111fn duplicates(x: &[f64]) -> (Vec<usize>, Vec<usize>) {
114 let n = x.len();
115 let (mut dups, mut dupsid) = (vec![0; n], vec![0; n]);
116 let mut start = 0;
117 while start < n {
118 let mut end = start + 1;
119 while end < n && x[end] == x[start] {
120 end += 1;
121 }
122 for (k, i) in (start..end).enumerate() {
123 dups[i] = end - start;
124 dupsid[i] = k + 1;
125 }
126 start = end;
127 }
128 (dups, dupsid)
129}
130
131fn kweight(x: f64, c: f64, h: f64) -> f64 {
133 let u = (x - c) / h;
134 ((1.0 - u.abs()) * f64::from(u.abs() <= 1.0)) / h
135}
136
137fn vander(u: &[f64], p: usize) -> Array2<f64> {
139 let mut out = Array2::zeros((u.len(), p + 1));
140 for (i, &v) in u.iter().enumerate() {
141 out[[i, 0]] = 1.0;
142 for j in 1..=p {
143 out[[i, j]] = out[[i, j - 1]] * v;
144 }
145 }
146 out
147}
148
149fn powk(x: f64, k: usize) -> f64 {
151 match k {
152 0 => 1.0,
153 1 => x,
154 2 => x * x,
155 _ => x.powf(k as f64),
156 }
157}
158
159fn scale_rows(m: &Array2<f64>, w: &[f64]) -> Array2<f64> {
160 let mut out = m.clone();
161 for (mut row, &wi) in out.rows_mut().into_iter().zip(w) {
162 row *= wi;
163 }
164 out
165}
166
167fn crossprod(a: &Array2<f64>, b: &Array2<f64>) -> Array2<f64> {
168 a.t().dot(b)
169}
170
171fn n_unique(x: &[f64]) -> usize {
172 let mut v = x.to_vec();
173 v.sort_by(f64::total_cmp);
174 v.dedup();
175 v.len()
176}
177
178fn nn_residuals(s: &Side) -> Array2<f64> {
182 let n = s.n();
183 let k = 1 + usize::from(s.t.is_some());
184 let mut res = Array2::zeros((n, k));
185 let tol_eps = f64::EPSILON.sqrt();
186 let (x, dups, dupsid) = (&s.x, &s.dups, &s.dupsid);
187 for pos in 0..n {
188 let p = pos as i64;
189 let mut rpos = (dups[pos] - dupsid[pos]) as i64;
190 let mut lpos = dupsid[pos] as i64 - 1;
191 let cap = NN_MATCHES.min(n.saturating_sub(1)) as i64;
192 while lpos + rpos < cap {
193 if p - lpos - 1 < 0 {
194 rpos += dups[(p + rpos + 1) as usize] as i64;
195 } else if p + rpos + 1 >= n as i64 {
196 lpos += dups[(p - lpos - 1) as usize] as i64;
197 } else {
198 let dleft = x[pos] - x[(p - lpos - 1) as usize];
199 let dright = x[(p + rpos + 1) as usize] - x[pos];
200 let nn_tol = dleft.max(dright) * tol_eps;
201 if dleft - dright > nn_tol {
202 rpos += dups[(p + rpos + 1) as usize] as i64;
203 } else if dright - dleft > nn_tol {
204 lpos += dups[(p - lpos - 1) as usize] as i64;
205 } else {
206 rpos += dups[(p + rpos + 1) as usize] as i64;
207 lpos += dups[(p - lpos - 1) as usize] as i64;
208 }
209 }
210 }
211 let (lo, hi) = ((p - lpos) as usize, (p + rpos + 1) as usize);
212 let ji = (hi - lo - 1) as f64;
213 let sf = (ji / (ji + 1.0)).powf(0.5);
214 let resid = |v: &[f64]| -> f64 {
215 let mut sum = 0.0;
216 for &vj in &v[lo..hi] {
217 sum += vj;
218 }
219 sum -= v[pos];
220 sf * (v[pos] - sum / ji)
221 };
222 res[[pos, 0]] = resid(&s.y);
223 if let Some(t) = &s.t {
224 res[[pos, 1]] = resid(t);
225 }
226 }
227 res
228}
229
230fn vce(s: &[f64], rx: &Array2<f64>, res: &Array2<f64>) -> Array2<f64> {
232 let r: Vec<f64> = if res.ncols() == 1 {
233 res.column(0).to_vec()
234 } else {
235 res.rows()
236 .into_iter()
237 .map(|row| row.dot(&ArrayView1::from(s)))
238 .collect()
239 };
240 let scaled = scale_rows(rx, &r);
241 crossprod(&scaled, &scaled)
242}
243
244fn std_ddof1(v: &[f64]) -> f64 {
246 let mean = np_sum(v) / v.len() as f64;
247 let sq: Vec<f64> = v.iter().map(|x| (x - mean) * (x - mean)).collect();
248 (np_sum(&sq) / (v.len() - 1) as f64).sqrt()
249}
250
251fn quantile_type2(x: &[f64], prob: f64) -> f64 {
253 let n = x.len();
254 let h = n as f64 * prob;
255 let j = h.floor() as usize;
256 if h == j as f64 {
257 0.5 * (x[j - 1] + x[j])
258 } else {
259 x[j]
260 }
261}
262
263fn split(
265 y: ArrayView1<'_, f64>,
266 x: ArrayView1<'_, f64>,
267 t: Option<ArrayView1<'_, f64>>,
268 c: f64,
269) -> Result<(Side, Side)> {
270 let n = x.len();
271 if y.len() != n || t.is_some_and(|t| t.len() != n) {
272 return Err(DmlError::DimensionMismatch(
273 "rdrobust: y, x and fuzzy need equal lengths".into(),
274 ));
275 }
276 if !x.iter().chain(y.iter()).all(|v| v.is_finite()) {
277 return Err(DmlError::InvalidArgument(
278 "rdrobust: data contain NaN or infinite values".into(),
279 ));
280 }
281 let mut order: Vec<usize> = (0..n).collect();
282 order.sort_by(|&a, &b| x[a].total_cmp(&x[b]));
283 let (x_min, x_max) = (x[order[0]], x[order[n - 1]]);
284 if c <= x_min || c >= x_max {
285 return Err(DmlError::InvalidArgument(
286 "the cutoff must lie within the range of the running variable".into(),
287 ));
288 }
289 let side = |left: bool| -> Side {
290 let rows: Vec<usize> = order
291 .iter()
292 .copied()
293 .filter(|&i| (x[i] < c) == left)
294 .collect();
295 Side::new(
296 rows.iter().map(|&i| x[i]).collect(),
297 rows.iter().map(|&i| y[i]).collect(),
298 t.map(|t| rows.iter().map(|&i| t[i]).collect()),
299 )
300 };
301 Ok((side(true), side(false)))
302}
303
304type BwParts = (f64, f64, f64, f64);
311
312fn bw_vfit(side: &Side, c: f64, o: usize, nu: usize, h_v: f64) -> Option<(f64, f64, Vec<f64>)> {
314 let keep: Vec<bool> = side.x.iter().map(|&x| kweight(x, c, h_v) > 0.0).collect();
315 let e = side.select(&keep);
316 if n_unique(&e.x) < o + 1 {
317 return None;
318 }
319 let ew: Vec<f64> = e.x.iter().map(|&x| kweight(x, c, h_v)).collect();
320 let u: Vec<f64> = e.x.iter().map(|x| x - c).collect();
321 let r = vander(&u, o);
322 let sqrt_w: Vec<f64> = ew.iter().map(|w| w.sqrt()).collect();
323 let rws = scale_rows(&r, &sqrt_w);
324 let inv_g = spd_inverse(&crossprod(&rws, &rws)).ok()?;
325 let rw = scale_rows(&r, &ew);
326 let beta = inv_g.dot(&crossprod(&rw, &e.d()));
327 let fact = (1..=nu).product::<usize>() as f64;
328 let s = if e.t.is_some() {
329 let (tau_y, tau_t) = (fact * beta[[nu, 0]], fact * beta[[nu, 1]]);
330 vec![1.0 / tau_t, -(tau_y / (tau_t * tau_t))]
331 } else {
332 vec![1.0]
333 };
334 let res = nn_residuals(&e);
335 let v_v = inv_g.dot(&vce(&s, &rw, &res)).dot(&inv_g)[[nu, nu]];
336 let pw: Array1<f64> = e.x.iter().map(|x| powk((x - c) / h_v, o + 1)).collect();
337 let v = rw.t().dot(&pw);
338 let ig_v = inv_g.dot(&v);
339 let b_const = powk(h_v, nu) * ig_v[nu];
341 Some((v_v, b_const, s))
342}
343
344#[allow(clippy::too_many_arguments)]
345fn rdrobust_bw(
346 side: &Side,
347 c: f64,
348 o: usize,
349 nu: usize,
350 o_b: usize,
351 h_v: f64,
352 h_b: f64,
353 scale: f64,
354) -> BwParts {
355 let nan = (f64::NAN, f64::NAN, f64::NAN, 1.0 / (2 * o + 3) as f64);
356 let Some((v_v, b_const, s)) = bw_vfit(side, c, o, nu, h_v) else {
357 return nan;
358 };
359 if !v_v.is_finite() || !(h_b.is_finite() && h_b > 0.0) {
360 return nan;
361 }
362 let keep: Vec<bool> = side.x.iter().map(|&x| kweight(x, c, h_b) > 0.0).collect();
363 let e = side.select(&keep);
364 if n_unique(&e.x) < o_b + 1 {
365 return nan;
366 }
367 let ew: Vec<f64> = e.x.iter().map(|&x| kweight(x, c, h_b)).collect();
368 let u: Vec<f64> = e.x.iter().map(|x| x - c).collect();
369 let r = vander(&u, o_b);
370 let sqrt_w: Vec<f64> = ew.iter().map(|w| w.sqrt()).collect();
371 let rws = scale_rows(&r, &sqrt_w);
372 let Ok(inv_g) = spd_inverse(&crossprod(&rws, &rws)) else {
373 return nan;
374 };
375 let rw = scale_rows(&r, &ew);
376 let beta = inv_g.dot(&crossprod(&rw, &e.d()));
377 let mut bw_reg = 0.0;
378 if scale > 0.0 {
379 let res = nn_residuals(&e);
380 let v_b = inv_g.dot(&vce(&s, &rw, &res)).dot(&inv_g)[[o_b, o_b]];
381 bw_reg = 3.0 * b_const * b_const * v_b;
382 }
383 let last = beta.row(o_b);
384 let s_beta: f64 = s.iter().zip(last.iter()).map(|(a, b)| a * b).sum();
385 let b = (2.0 * (o + 1 - nu) as f64).sqrt() * b_const * s_beta;
386 let v = (2 * nu + 1) as f64 * h_v.powf((2 * nu + 1) as f64) * v_v;
387 let r = scale * (2 * (o + 1 - nu)) as f64 * bw_reg;
388 (v, b, r, 1.0 / (2 * o + 3) as f64)
389}
390
391fn select_bandwidth(left: &Side, right: &Side, c: f64) -> Result<(f64, f64)> {
393 let x_all: Vec<f64> = left.x.iter().chain(&right.x).copied().collect();
394 let y_all: Vec<f64> = left.y.iter().chain(&right.y).copied().collect();
395 let x_iq = quantile_type2(&x_all, 0.75) - quantile_type2(&x_all, 0.25);
396 let (x_sd, y_sd) = (std_ddof1(&x_all), std_ddof1(&y_all));
397 let bwp = 1.0_f64.min((x_iq / x_sd) / 1.349);
398 let constant = |t: &Option<Vec<f64>>| t.as_ref().is_some_and(|t| t.iter().all(|&v| v == t[0]));
400 let sharp_bw = constant(&left.t) || constant(&right.t);
401 let standardise = |s: &Side| -> Side {
402 Side::new(
403 s.x.iter().map(|x| x / x_sd).collect(),
404 s.y.iter().map(|y| y / y_sd).collect(),
405 if sharp_bw { None } else { s.t.clone() },
406 )
407 };
408 let (l, r) = (standardise(left), standardise(right));
409 let c = c / x_sd;
410 let (x_min, x_max) = (l.x[0], r.x[r.n() - 1]);
411 let range_l = (c - l.x[0]).abs();
412 let range_r = (c - r.x[r.n() - 1]).abs();
413
414 let mut uniq_l = l.x.clone();
415 uniq_l.dedup();
416 uniq_l.reverse();
417 let mut uniq_r = r.x.clone();
418 uniq_r.dedup();
419 let (m_l, m_r) = (uniq_l.len(), uniq_r.len());
420 let mass = |m: usize, n: usize| 1.0 - m as f64 / n as f64;
421 let bwcheck = (mass(m_l, l.n()) >= 0.2 || mass(m_r, r.n()) >= 0.2).then_some(10usize);
422
423 let mut c_bw = C_C * bwp * ((m_l + m_r) as f64).powf(-1.0 / 5.0);
424 let (bw_max_l, bw_max_r) = ((c - x_min).abs(), (c - x_max).abs());
425 let bw_max = bw_max_l.max(bw_max_r);
426 c_bw = c_bw.min(bw_max);
427 let pad = 1.0 + f64::EPSILON.sqrt();
428 let bw_min = bwcheck.map(|k| {
429 (
430 (uniq_l[k.min(m_l) - 1] - c).abs() * pad,
431 (uniq_r[k.min(m_r) - 1] - c).abs() * pad,
432 )
433 });
434 if let Some((lo_l, lo_r)) = bw_min {
435 c_bw = c_bw.max(lo_l).max(lo_r);
436 }
437
438 let c_d_l = rdrobust_bw(&l, c, Q + 1, Q + 1, Q + 2, c_bw, range_l * pad, 0.0);
439 let c_d_r = rdrobust_bw(&r, c, Q + 1, Q + 1, Q + 2, c_bw, range_r * pad, 0.0);
440 let mut d_bw = ((c_d_l.0 + c_d_r.0) / (c_d_r.1 - c_d_l.1).powi(2)).powf(c_d_l.3);
441 d_bw = d_bw.min(bw_max);
442 if let Some((lo_l, lo_r)) = bw_min {
443 d_bw = d_bw.max(lo_l).max(lo_r);
444 }
445 let c_b_l = rdrobust_bw(&l, c, Q, P + 1, Q + 1, c_bw, d_bw, 1.0);
446 let c_b_r = rdrobust_bw(&r, c, Q, P + 1, Q + 1, c_bw, d_bw, 1.0);
447 let b_bw = ((c_b_l.0 + c_b_r.0) / ((c_b_r.1 - c_b_l.1).powi(2) + (c_b_r.2 + c_b_l.2)))
448 .powf(c_b_l.3)
449 .min(bw_max);
450 let c_h_l = rdrobust_bw(&l, c, P, 0, Q, c_bw, b_bw, 1.0);
451 let c_h_r = rdrobust_bw(&r, c, P, 0, Q, c_bw, b_bw, 1.0);
452 let h_bw = ((c_h_l.0 + c_h_r.0) / ((c_h_r.1 - c_h_l.1).powi(2) + (c_h_r.2 + c_h_l.2)))
453 .powf(c_h_l.3)
454 .min(bw_max);
455 let (h, b) = (x_sd * h_bw, x_sd * b_bw);
456 if !(h.is_finite() && h > 0.0 && b.is_finite() && b > 0.0) {
457 return Err(DmlError::InvalidArgument(
458 "not enough variability in the running variable to compute the bandwidth".into(),
459 ));
460 }
461 Ok((h, b))
462}
463
464pub fn rdbwselect(
468 y: ArrayView1<'_, f64>,
469 x: ArrayView1<'_, f64>,
470 fuzzy: Option<ArrayView1<'_, f64>>,
471 c: f64,
472) -> Result<(f64, f64)> {
473 let (left, right) = split(y, x, fuzzy, c)?;
474 check_support(&left, &right, P)?;
475 select_bandwidth(&left, &right, c)
476}
477
478fn check_support(left: &Side, right: &Side, order: usize) -> Result<()> {
479 if n_unique(&left.x).min(n_unique(&right.x)) < order + 1 {
480 return Err(DmlError::InvalidArgument(format!(
481 "not enough distinct running-variable values on each side of the cutoff for a polynomial of order {order}"
482 )));
483 }
484 Ok(())
485}
486
487struct SideFit {
493 beta_p: Array2<f64>,
494 beta_bc: Array2<f64>,
495 inv_g_p: Array2<f64>,
496 rw_p: Array2<f64>,
497 q_q: Array2<f64>,
498 res: Array2<f64>,
499 n_h: usize,
500}
501
502fn fit_side(side: &Side, c: f64, h: f64, b: f64) -> Result<SideFit> {
503 let w_h: Vec<f64> = side.x.iter().map(|&x| kweight(x, c, h)).collect();
504 let w_b: Vec<f64> = side.x.iter().map(|&x| kweight(x, c, b)).collect();
505 let in_h: Vec<bool> = w_h.iter().map(|&w| w > 0.0).collect();
506 let in_b: Vec<bool> = w_b.iter().map(|&w| w > 0.0).collect();
507 let n_h = in_h.iter().filter(|&&k| k).count();
508 let uniq_in = |keep: &[bool]| {
509 n_unique(
510 &side
511 .x
512 .iter()
513 .zip(keep)
514 .filter(|(_, &k)| k)
515 .map(|(x, _)| *x)
516 .collect::<Vec<_>>(),
517 )
518 };
519 if uniq_in(&in_h) < P + 1 || uniq_in(&in_b) < Q + 1 {
520 return Err(DmlError::InvalidArgument(
521 "not enough distinct running-variable values with positive weight on each side of the cutoff"
522 .into(),
523 ));
524 }
525 let keep = if h > b { &in_h } else { &in_b };
526 let pick = |v: &[f64]| -> Vec<f64> {
527 v.iter()
528 .zip(keep.iter())
529 .filter(|(_, &k)| k)
530 .map(|(a, _)| *a)
531 .collect()
532 };
533 let (ww_h, ww_b) = (pick(&w_h), pick(&w_b));
534 let e = {
536 let s = side.select(keep);
537 Side::new(s.x, s.y, s.t)
538 };
539 let u: Vec<f64> = e.x.iter().map(|x| (x - c) / h).collect();
540 let r_q = vander(&e.x.iter().map(|x| x - c).collect::<Vec<_>>(), Q);
541 let r_p = r_q.slice(ndarray::s![.., ..=P]).to_owned();
542 let rw_p = scale_rows(&r_p, &ww_h);
543 let l: Array1<f64> = rw_p
544 .t()
545 .dot(&Array1::from_iter(u.iter().map(|&v| powk(v, P + 1))));
546 let sq = |w: &[f64]| w.iter().map(|v| v.sqrt()).collect::<Vec<_>>();
547 let g_q = {
548 let m = scale_rows(&r_q, &sq(&ww_b));
549 crossprod(&m, &m)
550 };
551 let inv_g_q = spd_inverse(&g_q)?;
552 let inv_g_p = {
553 let m = scale_rows(&r_p, &sq(&ww_h));
554 spd_inverse(&crossprod(&m, &m))?
555 };
556 let m: Vec<f64> = r_q
557 .dot(&inv_g_q.row(P + 1))
558 .iter()
559 .zip(&ww_b)
560 .map(|(a, w)| a * w)
561 .collect();
562 let hp = powk(h, P + 1);
563 let mut q_q = rw_p.clone();
564 for i in 0..q_q.nrows() {
565 for j in 0..q_q.ncols() {
566 q_q[[i, j]] -= hp * m[i] * l[j];
567 }
568 }
569 let d = e.d();
570 let beta_p = inv_g_p.dot(&crossprod(&rw_p, &d));
571 let beta_bc = inv_g_p.dot(&crossprod(&q_q, &d));
572 let res = nn_residuals(&e);
573 Ok(SideFit {
574 beta_p,
575 beta_bc,
576 inv_g_p,
577 rw_p,
578 q_q,
579 res,
580 n_h,
581 })
582}
583
584pub fn rdrobust(
590 y: ArrayView1<'_, f64>,
591 x: ArrayView1<'_, f64>,
592 fuzzy: Option<ArrayView1<'_, f64>>,
593 c: f64,
594 bandwidths: Option<(f64, f64)>,
595) -> Result<RdEstimate> {
596 let (left, right) = split(y, x, fuzzy, c)?;
597 check_support(&left, &right, Q)?;
598 if let (Some(lt), Some(rt)) = (&left.t, &right.t) {
599 let constant = |t: &[f64]| t.iter().all(|&v| v == t[0]);
600 if constant(lt) && constant(rt) && (np_mean(lt) - np_mean(rt)).abs() < f64::EPSILON.sqrt() {
601 return Err(DmlError::InvalidArgument(
602 "fuzzy RD: the treatment has no variation and no jump at the cutoff".into(),
603 ));
604 }
605 }
606 let n = left.n() + right.n();
607 let (h, b) = match bandwidths {
608 Some((h, b)) => {
609 if !(h.is_finite() && h > 0.0 && b.is_finite() && b > 0.0) {
610 return Err(DmlError::InvalidArgument(
611 "bandwidths must be positive and finite".into(),
612 ));
613 }
614 (h, b)
615 }
616 None if n < 20 => {
617 let h = (c - left.x[0])
618 .abs()
619 .max((right.x[right.n() - 1] - c).abs());
620 (h, h)
621 }
622 None => select_bandwidth(&left, &right, c)?,
623 };
624 let fl = fit_side(&left, c, h, b)?;
625 let fr = fit_side(&right, c, h, b)?;
626
627 let (tau_y_cl, tau_y_bc) = (
628 fr.beta_p[[0, 0]] - fl.beta_p[[0, 0]],
629 fr.beta_bc[[0, 0]] - fl.beta_bc[[0, 0]],
630 );
631 let (tau_cl, tau_bc, s) = if fuzzy.is_some() {
632 let tau_t_cl = fr.beta_p[[0, 1]] - fl.beta_p[[0, 1]];
633 let tau_t_bc = fr.beta_bc[[0, 1]] - fl.beta_bc[[0, 1]];
634 let tau_cl = tau_y_cl / tau_t_cl;
635 let s = vec![1.0 / tau_t_cl, -(tau_y_cl / (tau_t_cl * tau_t_cl))];
636 let tau_bc = tau_cl - (s[0] * (tau_y_cl - tau_y_bc) + s[1] * (tau_t_cl - tau_t_bc));
637 (tau_cl, tau_bc, s)
638 } else {
639 (tau_y_cl, tau_y_bc, vec![1.0])
640 };
641 let variance = |f: &SideFit, robust: bool| -> f64 {
642 let rx = if robust { &f.q_q } else { &f.rw_p };
643 f.inv_g_p.dot(&vce(&s, rx, &f.res)).dot(&f.inv_g_p)[[0, 0]]
644 };
645 let se_cl = (variance(&fl, false) + variance(&fr, false)).sqrt();
646 let se_rb = (variance(&fl, true) + variance(&fr, true)).sqrt();
647 let coef = [tau_cl, tau_bc, tau_bc];
648 let se = [se_cl, se_cl, se_rb];
649 let quant = -norm_ppf(0.025);
650 let ci = std::array::from_fn(|k| [coef[k] - quant * se[k], coef[k] + quant * se[k]]);
651 Ok(RdEstimate {
652 coef,
653 se,
654 ci,
655 h: (h, h),
656 b: (b, b),
657 n_h: (fl.n_h, fr.n_h),
658 n: (left.n(), right.n()),
659 })
660}
661
662#[cfg(test)]
663mod tests {
664 use super::*;
665
666 #[test]
667 fn duplicates_count_runs() {
668 let (d, id) = duplicates(&[1.0, 2.0, 2.0, 2.0, 3.0]);
669 assert_eq!(d, vec![1, 3, 3, 3, 1]);
670 assert_eq!(id, vec![1, 1, 2, 3, 1]);
671 }
672
673 #[test]
674 fn quantile_type2_matches_r() {
675 let x = [1.0, 2.0, 3.0, 4.0];
676 assert_eq!(quantile_type2(&x, 0.25), 1.5);
677 assert_eq!(quantile_type2(&x, 0.75), 3.5);
678 assert_eq!(quantile_type2(&[1.0, 2.0, 3.0], 0.25), 1.0);
679 }
680}