1use ndarray::{s, Array1, Array2, ArrayView1, ArrayView2, Axis};
19
20use crate::compat::{np_mean, np_sum};
21use crate::error::{DmlError, Result};
22use crate::linalg::{spd_inverse, sym_pinv};
23use crate::stats::norm_ppf;
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
27pub enum Kernel {
28 #[default]
29 Triangular,
30 Epanechnikov,
31 Uniform,
32}
33
34impl Kernel {
35 fn weight(self, x: f64, c: f64, h: f64) -> f64 {
37 let u = (x - c) / h;
38 let inside = f64::from(u.abs() <= 1.0);
39 match self {
40 Kernel::Epanechnikov => (0.75 * (1.0 - u * u) * inside) / h,
41 Kernel::Uniform => (0.5 * inside) / h,
42 Kernel::Triangular => ((1.0 - u.abs()) * inside) / h,
43 }
44 }
45
46 fn c_c(self) -> f64 {
48 match self {
49 Kernel::Epanechnikov => 2.34,
50 Kernel::Uniform => 1.843,
51 Kernel::Triangular => 2.576,
52 }
53 }
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
58pub enum BwSelect {
59 #[default]
61 Mserd,
62 Msetwo,
64 Msesum,
67 Msecomb1,
69 Msecomb2,
71 Cerrd,
73 Certwo,
74 Cersum,
75 Cercomb1,
76 Cercomb2,
77}
78
79impl BwSelect {
80 fn needs_two(self) -> bool {
81 matches!(
82 self,
83 BwSelect::Msetwo | BwSelect::Certwo | BwSelect::Msecomb2 | BwSelect::Cercomb2
84 )
85 }
86
87 fn needs_sum(self) -> bool {
88 matches!(
89 self,
90 BwSelect::Msesum
91 | BwSelect::Cersum
92 | BwSelect::Msecomb1
93 | BwSelect::Msecomb2
94 | BwSelect::Cercomb1
95 | BwSelect::Cercomb2
96 )
97 }
98
99 fn needs_rd(self) -> bool {
100 matches!(
101 self,
102 BwSelect::Mserd
103 | BwSelect::Cerrd
104 | BwSelect::Msecomb1
105 | BwSelect::Msecomb2
106 | BwSelect::Cercomb1
107 | BwSelect::Cercomb2
108 )
109 }
110
111 fn is_cer(self) -> bool {
112 matches!(
113 self,
114 BwSelect::Cerrd
115 | BwSelect::Certwo
116 | BwSelect::Cersum
117 | BwSelect::Cercomb1
118 | BwSelect::Cercomb2
119 )
120 }
121}
122
123#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
128pub enum Vce {
129 #[default]
131 Nn,
132 Hc0,
133 Hc1,
134 Hc2,
135 Hc3,
136}
137
138#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
141pub enum MassPoints {
142 #[default]
146 Adjust,
147 Check,
149 Off,
151}
152
153#[derive(Debug, Clone, PartialEq)]
156pub struct RdOptions {
157 pub p: usize,
159 pub q: Option<usize>,
161 pub kernel: Kernel,
162 pub bwselect: BwSelect,
163 pub vce: Vce,
164 pub nnmatch: usize,
166 pub level: f64,
168 pub masspoints: MassPoints,
169 pub bwcheck: Option<usize>,
172 pub bwrestrict: bool,
174 pub stdvars: bool,
177 pub scaleregul: f64,
179 pub sharpbw: bool,
182 pub rho: Option<f64>,
184 pub scalepar: f64,
186 pub covs_drop: bool,
188}
189
190impl Default for RdOptions {
191 fn default() -> Self {
192 Self {
193 p: 1,
194 q: None,
195 kernel: Kernel::Triangular,
196 bwselect: BwSelect::Mserd,
197 vce: Vce::Nn,
198 nnmatch: 3,
199 level: 0.95,
200 masspoints: MassPoints::Adjust,
201 bwcheck: None,
202 bwrestrict: true,
203 stdvars: true,
204 scaleregul: 1.0,
205 sharpbw: false,
206 rho: None,
207 scalepar: 1.0,
208 covs_drop: true,
209 }
210 }
211}
212
213impl RdOptions {
214 fn q(&self) -> usize {
215 self.q.unwrap_or(self.p + 1)
216 }
217
218 fn validate(&self) -> Result<()> {
219 let q = self.q();
220 let bad = |msg: &str| Err(DmlError::InvalidArgument(format!("rdrobust: {msg}")));
221 if self.p > 20 || q > 20 || q <= self.p {
222 return bad("polynomial orders need p <= 20 and p < q <= 20");
223 }
224 if self.nnmatch == 0 {
225 return bad("nnmatch must be at least 1");
226 }
227 if !(self.level > 0.0 && self.level < 1.0) {
228 return bad("level must be in (0, 1)");
229 }
230 if self.bwcheck == Some(0) {
231 return bad("bwcheck must be a positive integer");
232 }
233 if self.rho.is_some_and(|r| !(r.is_finite() && r > 0.0)) {
234 return bad("rho must be positive");
235 }
236 if !(self.scaleregul.is_finite() && self.scaleregul >= 0.0) || !self.scalepar.is_finite() {
237 return bad("scaleregul and scalepar must be finite (scaleregul >= 0)");
238 }
239 Ok(())
240 }
241}
242
243#[derive(Debug, Clone, Copy)]
246pub struct RdData<'a> {
247 pub y: ArrayView1<'a, f64>,
248 pub x: ArrayView1<'a, f64>,
249 pub cutoff: f64,
250 pub fuzzy: Option<ArrayView1<'a, f64>>,
251 pub covs: Option<ArrayView2<'a, f64>>,
252 pub cluster: Option<&'a [i64]>,
253}
254
255impl<'a> RdData<'a> {
256 pub fn new(y: ArrayView1<'a, f64>, x: ArrayView1<'a, f64>) -> Self {
258 Self {
259 y,
260 x,
261 cutoff: 0.0,
262 fuzzy: None,
263 covs: None,
264 cluster: None,
265 }
266 }
267
268 pub fn cutoff(mut self, c: f64) -> Self {
269 self.cutoff = c;
270 self
271 }
272
273 pub fn fuzzy(mut self, t: ArrayView1<'a, f64>) -> Self {
275 self.fuzzy = Some(t);
276 self
277 }
278
279 pub fn covariates(mut self, z: ArrayView2<'a, f64>) -> Self {
281 self.covs = Some(z);
282 self
283 }
284
285 pub fn clusters(mut self, labels: &'a [i64]) -> Self {
287 self.cluster = Some(labels);
288 self
289 }
290}
291
292#[derive(Debug, Clone, Copy, PartialEq)]
294pub struct RdBandwidths {
295 pub h: (f64, f64),
297 pub b: (f64, f64),
299}
300
301impl RdBandwidths {
302 pub fn common(h: f64, b: f64) -> Self {
304 Self {
305 h: (h, h),
306 b: (b, b),
307 }
308 }
309}
310
311#[derive(Debug, Clone)]
314pub struct RdEstimate {
315 pub coef: [f64; 3],
316 pub se: [f64; 3],
317 pub ci: [[f64; 2]; 3],
319 pub h: (f64, f64),
321 pub b: (f64, f64),
323 pub n_h: (usize, usize),
325 pub n: (usize, usize),
327}
328
329#[derive(Clone)]
335struct Side {
336 x: Vec<f64>,
337 y: Vec<f64>,
338 t: Option<Vec<f64>>,
339 z: Option<Array2<f64>>,
340 cl: Option<Vec<i64>>,
341 dups: Vec<usize>,
342 dupsid: Vec<usize>,
343}
344
345impl Side {
346 fn new(
347 x: Vec<f64>,
348 y: Vec<f64>,
349 t: Option<Vec<f64>>,
350 z: Option<Array2<f64>>,
351 cl: Option<Vec<i64>>,
352 ) -> Self {
353 let (dups, dupsid) = duplicates(&x);
354 Self {
355 x,
356 y,
357 t,
358 z,
359 cl,
360 dups,
361 dupsid,
362 }
363 }
364
365 fn n(&self) -> usize {
366 self.x.len()
367 }
368
369 fn select(&self, keep: &[bool]) -> Side {
371 let rows: Vec<usize> = (0..self.n()).filter(|&i| keep[i]).collect();
372 let pick = |v: &[f64]| -> Vec<f64> { rows.iter().map(|&i| v[i]).collect() };
373 Side::new(
374 pick(&self.x),
375 pick(&self.y),
376 self.t.as_ref().map(|t| pick(t)),
377 self.z.as_ref().map(|z| z.select(Axis(0), &rows)),
378 self.cl
379 .as_ref()
380 .map(|c| rows.iter().map(|&i| c[i]).collect()),
381 )
382 }
383
384 fn d_t(&self) -> usize {
385 usize::from(self.t.is_some())
386 }
387
388 fn d_z(&self) -> usize {
389 self.z.as_ref().map_or(0, |z| z.ncols())
390 }
391
392 fn d(&self) -> Array2<f64> {
394 let (dt, dz) = (self.d_t(), self.d_z());
395 let mut d = Array2::zeros((self.n(), 1 + dt + dz));
396 for i in 0..self.n() {
397 d[[i, 0]] = self.y[i];
398 if let Some(t) = &self.t {
399 d[[i, 1]] = t[i];
400 }
401 if let Some(z) = &self.z {
402 for j in 0..dz {
403 d[[i, 1 + dt + j]] = z[[i, j]];
404 }
405 }
406 }
407 d
408 }
409}
410
411fn duplicates(x: &[f64]) -> (Vec<usize>, Vec<usize>) {
414 let n = x.len();
415 let (mut dups, mut dupsid) = (vec![0; n], vec![0; n]);
416 let mut start = 0;
417 while start < n {
418 let mut end = start + 1;
419 while end < n && x[end] == x[start] {
420 end += 1;
421 }
422 for (k, i) in (start..end).enumerate() {
423 dups[i] = end - start;
424 dupsid[i] = k + 1;
425 }
426 start = end;
427 }
428 (dups, dupsid)
429}
430
431fn vander(u: &[f64], p: usize) -> Array2<f64> {
433 let mut out = Array2::zeros((u.len(), p + 1));
434 for (i, &v) in u.iter().enumerate() {
435 out[[i, 0]] = 1.0;
436 for j in 1..=p {
437 out[[i, j]] = out[[i, j - 1]] * v;
438 }
439 }
440 out
441}
442
443fn powk(x: f64, k: usize) -> f64 {
445 match k {
446 0 => 1.0,
447 1 => x,
448 2 => x * x,
449 _ => x.powf(k as f64),
450 }
451}
452
453fn scale_rows(m: &Array2<f64>, w: &[f64]) -> Array2<f64> {
454 let mut out = m.clone();
455 for (mut row, &wi) in out.rows_mut().into_iter().zip(w) {
456 row *= wi;
457 }
458 out
459}
460
461fn crossprod(a: &Array2<f64>, b: &Array2<f64>) -> Array2<f64> {
462 a.t().dot(b)
463}
464
465fn n_unique(x: &[f64]) -> usize {
466 let mut v = x.to_vec();
467 v.sort_by(f64::total_cmp);
468 v.dedup();
469 v.len()
470}
471
472fn factorial(k: usize) -> f64 {
473 (1..=k).product::<usize>() as f64
474}
475
476fn std_ddof1(v: &[f64]) -> f64 {
478 let mean = np_sum(v) / v.len() as f64;
479 let sq: Vec<f64> = v.iter().map(|x| (x - mean) * (x - mean)).collect();
480 (np_sum(&sq) / (v.len() - 1) as f64).sqrt()
481}
482
483fn var(v: &[f64]) -> f64 {
485 let mean = np_mean(v);
486 let sq: Vec<f64> = v.iter().map(|x| (x - mean) * (x - mean)).collect();
487 np_mean(&sq)
488}
489
490fn quantile_type2(x: &[f64], prob: f64) -> f64 {
492 let n = x.len();
493 let h = n as f64 * prob;
494 let j = h.floor() as usize;
495 if h == j as f64 {
496 0.5 * (x[j - 1] + x[j])
497 } else {
498 x[j]
499 }
500}
501
502fn independent_columns(z: &Array2<f64>) -> Vec<usize> {
506 const TOL: f64 = 1e-5;
507 let k = z.ncols();
508 let (diag, _) = householder_qr(z, false);
509 if diag.iter().all(|d| d.abs() > TOL) {
510 return (0..k).collect();
511 }
512 let (diag, pivot) = householder_qr(z, true);
513 pivot
514 .into_iter()
515 .zip(diag)
516 .filter(|(_, d)| d.abs() > TOL)
517 .map(|(j, _)| j)
518 .collect()
519}
520
521fn householder_qr(a: &Array2<f64>, pivoting: bool) -> (Vec<f64>, Vec<usize>) {
524 let (n, k) = a.dim();
525 let mut m = a.clone();
526 let mut order: Vec<usize> = (0..k).collect();
527 let mut diag = Vec::with_capacity(k.min(n));
528 for j in 0..k.min(n) {
529 if pivoting {
530 let norm =
531 |c: usize, m: &Array2<f64>| (j..n).map(|i| m[[i, c]] * m[[i, c]]).sum::<f64>();
532 let best = (j..k)
533 .max_by(|&a, &b| norm(a, &m).total_cmp(&norm(b, &m)))
534 .expect("columns left");
535 if best != j {
536 for i in 0..n {
537 m.swap([i, j], [i, best]);
538 }
539 order.swap(j, best);
540 }
541 }
542 let alpha = (j..n).map(|i| m[[i, j]] * m[[i, j]]).sum::<f64>().sqrt();
543 let alpha = if m[[j, j]] > 0.0 { -alpha } else { alpha };
544 diag.push(alpha);
545 if alpha == 0.0 {
546 continue;
547 }
548 let mut v: Vec<f64> = (j..n).map(|i| m[[i, j]]).collect();
549 v[0] -= alpha;
550 let vnorm2: f64 = v.iter().map(|x| x * x).sum();
551 if vnorm2 == 0.0 {
552 continue;
553 }
554 for c in j..k {
555 let dot: f64 = (j..n).map(|i| v[i - j] * m[[i, c]]).sum();
556 let f = 2.0 * dot / vnorm2;
557 for i in j..n {
558 m[[i, c]] -= f * v[i - j];
559 }
560 }
561 }
562 (diag, order)
563}
564
565fn prepare(data: &RdData<'_>, opts: &RdOptions) -> Result<(Side, Side)> {
567 let (y, x, c) = (data.y, data.x, data.cutoff);
568 let n = x.len();
569 let lengths_ok = y.len() == n
570 && data.fuzzy.is_none_or_len(n)
571 && data.covs.map_or(true, |z| z.nrows() == n)
572 && data.cluster.map_or(true, |cl| cl.len() == n);
573 if !lengths_ok {
574 return Err(DmlError::DimensionMismatch(
575 "rdrobust: y, x, fuzzy, covs and cluster need the same number of observations".into(),
576 ));
577 }
578 let finite = x.iter().chain(y.iter()).all(|v| v.is_finite())
579 && data.fuzzy.map_or(true, |t| t.iter().all(|v| v.is_finite()))
580 && data.covs.map_or(true, |z| z.iter().all(|v| v.is_finite()));
581 if !finite {
582 return Err(DmlError::InvalidArgument(
583 "rdrobust: data contain NaN or infinite values".into(),
584 ));
585 }
586 if !c.is_finite() {
587 return Err(DmlError::InvalidArgument(
588 "rdrobust: the cutoff must be finite".into(),
589 ));
590 }
591 let covs = match data.covs {
592 Some(z) if opts.covs_drop => {
593 let z = z.to_owned();
594 let keep = independent_columns(&z);
595 Some(z.select(Axis(1), &keep))
596 }
597 Some(z) => Some(z.to_owned()),
598 None => None,
599 };
600 let mut order: Vec<usize> = (0..n).collect();
601 order.sort_by(|&a, &b| x[a].total_cmp(&x[b]));
602 let (x_min, x_max) = (x[order[0]], x[order[n - 1]]);
603 if c <= x_min || c >= x_max {
604 return Err(DmlError::InvalidArgument(
605 "the cutoff must lie within the range of the running variable".into(),
606 ));
607 }
608 let side = |left: bool| -> Side {
609 let rows: Vec<usize> = order
610 .iter()
611 .copied()
612 .filter(|&i| (x[i] < c) == left)
613 .collect();
614 Side::new(
615 rows.iter().map(|&i| x[i]).collect(),
616 rows.iter().map(|&i| y[i]).collect(),
617 data.fuzzy.map(|t| rows.iter().map(|&i| t[i]).collect()),
618 covs.as_ref().map(|z| z.select(Axis(0), &rows)),
619 data.cluster.map(|cl| rows.iter().map(|&i| cl[i]).collect()),
620 )
621 };
622 Ok((side(true), side(false)))
623}
624
625trait LenCheck {
627 fn is_none_or_len(&self, n: usize) -> bool;
628}
629
630impl LenCheck for Option<ArrayView1<'_, f64>> {
631 fn is_none_or_len(&self, n: usize) -> bool {
632 self.map_or(true, |t| t.len() == n)
633 }
634}
635
636fn residuals(
643 e: &Side,
644 vce: Vce,
645 fitted: Option<&Array2<f64>>,
646 hii: Option<&Array1<f64>>,
647 nnmatch: usize,
648 d: usize,
649) -> Array2<f64> {
650 let dmat = e.d();
651 if vce == Vce::Nn {
652 return nn_residuals(e, &dmat, nnmatch);
653 }
654 let n = e.n();
655 let clustered = e.cl.is_some();
656 let fitted = fitted.expect("plug-in residuals need fitted values");
657 let mut res = &dmat - fitted;
658 for i in 0..n {
659 let w = match vce {
660 Vce::Hc0 => 1.0,
661 Vce::Hc1 if clustered => 1.0,
662 Vce::Hc1 => (n as f64 / (n - d) as f64).sqrt(),
663 Vce::Hc2 => (1.0 / (1.0 - hii.expect("leverages")[i]).max(1e-8)).sqrt(),
664 Vce::Hc3 => 1.0 / (1.0 - hii.expect("leverages")[i]).max(1e-8),
665 Vce::Nn => unreachable!(),
666 };
667 res.row_mut(i).mapv_inplace(|r| w * r);
668 }
669 res
670}
671
672fn nn_residuals(s: &Side, dmat: &Array2<f64>, nnmatch: usize) -> Array2<f64> {
676 let n = s.n();
677 let mut res = Array2::zeros(dmat.dim());
678 let tol_eps = f64::EPSILON.sqrt();
679 let (x, dups, dupsid) = (&s.x, &s.dups, &s.dupsid);
680 for pos in 0..n {
681 let p = pos as i64;
682 let mut rpos = (dups[pos] - dupsid[pos]) as i64;
683 let mut lpos = dupsid[pos] as i64 - 1;
684 let cap = nnmatch.min(n.saturating_sub(1)) as i64;
685 while lpos + rpos < cap {
686 if p - lpos - 1 < 0 {
687 rpos += dups[(p + rpos + 1) as usize] as i64;
688 } else if p + rpos + 1 >= n as i64 {
689 lpos += dups[(p - lpos - 1) as usize] as i64;
690 } else {
691 let dleft = x[pos] - x[(p - lpos - 1) as usize];
692 let dright = x[(p + rpos + 1) as usize] - x[pos];
693 let nn_tol = dleft.max(dright) * tol_eps;
694 if dleft - dright > nn_tol {
695 rpos += dups[(p + rpos + 1) as usize] as i64;
696 } else if dright - dleft > nn_tol {
697 lpos += dups[(p - lpos - 1) as usize] as i64;
698 } else {
699 rpos += dups[(p + rpos + 1) as usize] as i64;
700 lpos += dups[(p - lpos - 1) as usize] as i64;
701 }
702 }
703 }
704 let (lo, hi) = ((p - lpos) as usize, (p + rpos + 1) as usize);
705 let ji = (hi - lo - 1) as f64;
706 let sf = (ji / (ji + 1.0)).powf(0.5);
707 for col in 0..dmat.ncols() {
708 let mut sum = 0.0;
709 for j in lo..hi {
710 sum += dmat[[j, col]];
711 }
712 sum -= dmat[[pos, col]];
713 res[[pos, col]] = sf * (dmat[[pos, col]] - sum / ji);
714 }
715 }
716 res
717}
718
719fn vce_meat(
723 s: &[f64],
724 rx: &Array2<f64>,
725 res: &Array2<f64>,
726 cl: Option<&[i64]>,
727 k_df: usize,
728) -> Array2<f64> {
729 let k = rx.ncols();
730 let combined: Vec<f64> = res
731 .rows()
732 .into_iter()
733 .map(|row| row.iter().zip(s).map(|(r, si)| r * si).sum())
734 .collect();
735 match cl {
736 None => {
737 let scaled = scale_rows(rx, &combined);
738 crossprod(&scaled, &scaled)
739 }
740 Some(cl) => {
741 let n = cl.len();
742 let mut order: Vec<usize> = (0..n).collect();
743 order.sort_by_key(|&i| cl[i]);
744 let mut meat = Array2::<f64>::zeros((k, k));
745 let mut g = 0usize;
746 let mut start = 0;
747 while start < n {
748 let mut end = start + 1;
749 while end < n && cl[order[end]] == cl[order[start]] {
750 end += 1;
751 }
752 let mut sv = Array1::<f64>::zeros(k);
753 for &i in &order[start..end] {
754 sv.scaled_add(combined[i], &rx.row(i));
755 }
756 for a in 0..k {
757 for b in 0..k {
758 meat[[a, b]] += sv[a] * sv[b];
759 }
760 }
761 g += 1;
762 start = end;
763 }
764 let w = if g > 1 {
765 ((n - 1) as f64 / (n - k_df) as f64) * (g as f64 / (g - 1) as f64)
766 } else {
767 f64::NAN
768 };
769 meat * w
770 }
771 }
772}
773
774fn leverages(r: &Array2<f64>, inv_g: &Array2<f64>, rw: &Array2<f64>) -> Array1<f64> {
776 (r.dot(inv_g) * rw).sum_axis(Axis(1))
777}
778
779fn covariate_gamma(rw: &Array2<f64>, inv_g: &Array2<f64>, e: &Side, w: &[f64]) -> Array2<f64> {
782 let (dt, dz) = (e.d_t(), e.d_z());
783 let d = e.d();
784 let u = crossprod(rw, &d);
785 let zw = scale_rows(e.z.as_ref().expect("covariates"), w);
786 let zwd = crossprod(&zw, &d);
787 let cols_z = s![.., 1 + dt..1 + dt + dz];
788 let uigu = u.slice(cols_z).t().dot(&inv_g.dot(&u));
789 let zwz = &zwd.slice(cols_z) - &uigu.slice(cols_z);
790 let zwy = &zwd.slice(s![.., ..1 + dt]) - &uigu.slice(s![.., ..1 + dt]);
791 sym_pinv(&zwz).dot(&zwy)
792}
793
794type BwParts = (f64, f64, f64, f64);
801
802struct BwCtx<'o> {
804 opts: &'o RdOptions,
805 vce: Vce,
806 c: f64,
807}
808
809impl BwCtx<'_> {
810 fn pilot_residuals(
812 &self,
813 e: &Side,
814 r: &Array2<f64>,
815 inv_g: &Array2<f64>,
816 rw: &Array2<f64>,
817 beta: &Array2<f64>,
818 d: usize,
819 ) -> Array2<f64> {
820 match self.vce {
821 Vce::Nn => residuals(e, Vce::Nn, None, None, self.opts.nnmatch, d),
822 v => {
823 let fitted = r.dot(beta);
824 let hii = matches!(v, Vce::Hc2 | Vce::Hc3).then(|| leverages(r, inv_g, rw));
825 residuals(e, v, Some(&fitted), hii.as_ref(), self.opts.nnmatch, d)
826 }
827 }
828 }
829
830 fn vfit(&self, side: &Side, o: usize, nu: usize, h_v: f64) -> Option<(f64, f64, Vec<f64>)> {
832 let (c, kernel) = (self.c, self.opts.kernel);
833 let keep: Vec<bool> = side
834 .x
835 .iter()
836 .map(|&x| kernel.weight(x, c, h_v) > 0.0)
837 .collect();
838 let e = side.select(&keep);
839 if n_unique(&e.x) < o + 1 {
840 return None;
841 }
842 let ew: Vec<f64> = e.x.iter().map(|&x| kernel.weight(x, c, h_v)).collect();
843 let r = vander(&e.x.iter().map(|x| x - c).collect::<Vec<_>>(), o);
844 let rws = scale_rows(&r, &ew.iter().map(|w| w.sqrt()).collect::<Vec<_>>());
845 let inv_g = spd_inverse(&crossprod(&rws, &rws)).ok()?;
846 let rw = scale_rows(&r, &ew);
847 let beta = inv_g.dot(&crossprod(&rw, &e.d()));
848 let fact = factorial(nu);
849 let dt = e.d_t();
850 let s = match (e.z.is_some(), e.t.is_some()) {
851 (false, false) => vec![1.0],
852 (false, true) => {
853 let (tau_y, tau_t) = (fact * beta[[nu, 0]], fact * beta[[nu, 1]]);
854 vec![1.0 / tau_t, -(tau_y / (tau_t * tau_t))]
855 }
856 (true, has_t) => {
857 let gamma = covariate_gamma(&rw, &inv_g, &e, &ew);
858 let dz = e.d_z();
859 let s_y: Vec<f64> = std::iter::once(1.0)
860 .chain(gamma.column(0).iter().map(|g| -g))
861 .collect();
862 if has_t {
863 let s_t: Vec<f64> = std::iter::once(1.0)
864 .chain(gamma.column(1).iter().map(|g| -g))
865 .collect();
866 let pick = |col: usize| -> Vec<f64> {
867 std::iter::once(beta[[nu, col]])
868 .chain((0..dz).map(|j| beta[[nu, 1 + dt + j]]))
869 .collect()
870 };
871 let dot =
872 |a: &[f64], b: &[f64]| a.iter().zip(b).map(|(x, y)| x * y).sum::<f64>();
873 let tau_y = fact * dot(&s_y, &pick(0));
874 let tau_t = fact * dot(&s_t, &pick(1));
875 let mut s = vec![1.0 / tau_t, -(tau_y / (tau_t * tau_t))];
876 s.extend((0..dz).map(|j| {
877 -(1.0 / tau_t) * gamma[[j, 0]] + (tau_y / (tau_t * tau_t)) * gamma[[j, 1]]
878 }));
879 s
880 } else {
881 s_y
882 }
883 }
884 };
885 let res = self.pilot_residuals(&e, &r, &inv_g, &rw, &beta, o + 1);
886 let meat = vce_meat(&s, &rw, &res, e.cl.as_deref(), rw.ncols());
887 let v_v = inv_g.dot(&meat).dot(&inv_g)[[nu, nu]];
888 let pw: Array1<f64> = e.x.iter().map(|x| powk((x - c) / h_v, o + 1)).collect();
889 let ig_v = inv_g.dot(&rw.t().dot(&pw));
890 let b_const = powk(h_v, nu) * ig_v[nu];
892 Some((v_v, b_const, s))
893 }
894
895 #[allow(clippy::too_many_arguments)]
897 fn bw(
898 &self,
899 side: &Side,
900 o: usize,
901 nu: usize,
902 o_b: usize,
903 h_v: f64,
904 h_b: f64,
905 scale: f64,
906 ) -> BwParts {
907 let (c, kernel) = (self.c, self.opts.kernel);
908 let nan = (f64::NAN, f64::NAN, f64::NAN, 1.0 / (2 * o + 3) as f64);
909 let Some((v_v, b_const, s)) = self.vfit(side, o, nu, h_v) else {
910 return nan;
911 };
912 if !v_v.is_finite() || !(h_b.is_finite() && h_b > 0.0) {
913 return nan;
914 }
915 let keep: Vec<bool> = side
916 .x
917 .iter()
918 .map(|&x| kernel.weight(x, c, h_b) > 0.0)
919 .collect();
920 let e = side.select(&keep);
921 if n_unique(&e.x) < o_b + 1 {
922 return nan;
923 }
924 let ew: Vec<f64> = e.x.iter().map(|&x| kernel.weight(x, c, h_b)).collect();
925 let r = vander(&e.x.iter().map(|x| x - c).collect::<Vec<_>>(), o_b);
926 let rws = scale_rows(&r, &ew.iter().map(|w| w.sqrt()).collect::<Vec<_>>());
927 let Ok(inv_g) = spd_inverse(&crossprod(&rws, &rws)) else {
928 return nan;
929 };
930 let rw = scale_rows(&r, &ew);
931 let beta = inv_g.dot(&crossprod(&rw, &e.d()));
932 let mut bw_reg = 0.0;
933 if scale > 0.0 {
934 let res = self.pilot_residuals(&e, &r, &inv_g, &rw, &beta, o_b + 1);
935 let meat = vce_meat(&s, &rw, &res, e.cl.as_deref(), rw.ncols());
936 let v_b = inv_g.dot(&meat).dot(&inv_g)[[o_b, o_b]];
937 bw_reg = 3.0 * b_const * b_const * v_b;
938 }
939 let last = beta.row(o_b);
940 let s_beta: f64 = s.iter().zip(last.iter()).map(|(a, b)| a * b).sum();
941 let b = (2.0 * (o + 1 - nu) as f64).sqrt() * b_const * s_beta;
942 let v = (2 * nu + 1) as f64 * h_v.powf((2 * nu + 1) as f64) * v_v;
943 let r = scale * (2 * (o + 1 - nu)) as f64 * bw_reg;
944 (v, b, r, 1.0 / (2 * o + 3) as f64)
945 }
946}
947
948fn select_bandwidths(left: &Side, right: &Side, c: f64, opts: &RdOptions) -> Result<RdBandwidths> {
950 let (p, q) = (opts.p, opts.q());
951 let x_all: Vec<f64> = left.x.iter().chain(&right.x).copied().collect();
952 let y_all: Vec<f64> = left.y.iter().chain(&right.y).copied().collect();
953 let n = x_all.len();
954 let x_iq = quantile_type2(&x_all, 0.75) - quantile_type2(&x_all, 0.25);
955 let (x_sd, y_sd, bwp) = if opts.stdvars {
956 let x_sd = std_ddof1(&x_all);
957 (x_sd, std_ddof1(&y_all), 1.0_f64.min((x_iq / x_sd) / 1.349))
958 } else {
959 (1.0, 1.0, std_ddof1(&x_all).min(x_iq / 1.349))
960 };
961 let no_variation = |t: &Option<Vec<f64>>| t.as_ref().is_some_and(|t| var(t) == 0.0);
963 let sharp_bw = opts.sharpbw || no_variation(&left.t) || no_variation(&right.t);
964 let standardise = |s: &Side| -> Side {
965 Side::new(
966 s.x.iter().map(|x| x / x_sd).collect(),
967 s.y.iter().map(|y| y / y_sd).collect(),
968 if sharp_bw { None } else { s.t.clone() },
969 s.z.clone(),
970 s.cl.clone(),
971 )
972 };
973 let (l, r) = (standardise(left), standardise(right));
974 let c = c / x_sd;
975 let (x_min, x_max) = (l.x[0], r.x[r.n() - 1]);
976 let range_l = (c - l.x[0]).abs();
977 let range_r = (c - r.x[r.n() - 1]).abs();
978
979 let mut uniq_l = l.x.clone();
980 uniq_l.dedup();
981 uniq_l.reverse();
982 let mut uniq_r = r.x.clone();
983 uniq_r.dedup();
984 let inspect = opts.masspoints != MassPoints::Off;
985 let (m_l, m_r) = if inspect || opts.bwcheck.is_some() {
986 (uniq_l.len(), uniq_r.len())
987 } else {
988 (l.n(), r.n())
989 };
990 let mut bwcheck = opts.bwcheck;
991 if inspect {
992 let mass = |m: usize, n: usize| 1.0 - m as f64 / n as f64;
993 let detected = mass(m_l, l.n()) >= 0.2 || mass(m_r, r.n()) >= 0.2;
994 if detected && bwcheck.is_none() && opts.masspoints == MassPoints::Adjust {
995 bwcheck = Some(10);
996 }
997 }
998 let m_total = if inspect { m_l + m_r } else { n };
999
1000 let c_c = opts.kernel.c_c();
1001 let mut c_bw = c_c * bwp * (n as f64).powf(-1.0 / 5.0);
1002 if opts.masspoints == MassPoints::Adjust {
1003 c_bw = c_c * bwp * (m_total as f64).powf(-1.0 / 5.0);
1004 }
1005 let (bw_max_l, bw_max_r) = ((c - x_min).abs(), (c - x_max).abs());
1006 let bw_max = bw_max_l.max(bw_max_r);
1007 if opts.bwrestrict {
1008 c_bw = c_bw.min(bw_max);
1009 }
1010 let pad = 1.0 + f64::EPSILON.sqrt();
1011 let bw_min = bwcheck.map(|k| {
1012 (
1013 (uniq_l[k.min(m_l) - 1] - c).abs() * pad,
1014 (uniq_r[k.min(m_r) - 1] - c).abs() * pad,
1015 )
1016 });
1017 if let Some((lo_l, lo_r)) = bw_min {
1018 c_bw = c_bw.max(lo_l).max(lo_r);
1019 }
1020 let restrict = |v: f64, max: f64| if opts.bwrestrict { v.min(max) } else { v };
1021 let floor = |v: f64| match bw_min {
1022 Some((lo_l, lo_r)) => v.max(lo_l).max(lo_r),
1023 None => v,
1024 };
1025
1026 let ctx = BwCtx {
1027 opts,
1028 vce: effective_vce(opts.vce, left.cl.is_some())?,
1029 c,
1030 };
1031 let reg = opts.scaleregul;
1032 let c_d_l = ctx.bw(&l, q + 1, q + 1, q + 2, c_bw, range_l * pad, 0.0);
1033 let c_d_r = ctx.bw(&r, q + 1, q + 1, q + 2, c_bw, range_r * pad, 0.0);
1034
1035 let (mut two, mut sum, mut rd) = (None, None, None);
1036 let sel = opts.bwselect;
1037 if sel.needs_two() {
1038 let side = |s: &Side, cd: BwParts, max: f64, lo: Option<f64>| -> (f64, f64) {
1039 let mut d_bw = restrict((cd.0 / (cd.1 * cd.1)).powf(cd.3), max);
1040 if let Some(lo) = lo {
1041 d_bw = d_bw.max(lo);
1042 }
1043 let cb = ctx.bw(s, q, p + 1, q + 1, c_bw, d_bw, reg);
1044 let b_bw = restrict((cb.0 / (cb.1 * cb.1 + reg * cb.2)).powf(cb.3), max);
1045 let ch = ctx.bw(s, p, 0, q, c_bw, b_bw, reg);
1046 let h_bw = restrict((ch.0 / (ch.1 * ch.1 + reg * ch.2)).powf(ch.3), max);
1047 (x_sd * h_bw, x_sd * b_bw)
1048 };
1049 let (h_l, b_l) = side(&l, c_d_l, bw_max_l, bw_min.map(|m| m.0));
1050 let (h_r, b_r) = side(&r, c_d_r, bw_max_r, bw_min.map(|m| m.1));
1051 two = Some(RdBandwidths {
1052 h: (h_l, h_r),
1053 b: (b_l, b_r),
1054 });
1055 }
1056 let common = |sign: f64| -> (f64, f64) {
1059 let d_bw = floor(restrict(
1060 ((c_d_l.0 + c_d_r.0) / (c_d_r.1 + sign * c_d_l.1).powi(2)).powf(c_d_l.3),
1061 bw_max,
1062 ));
1063 let cb_l = ctx.bw(&l, q, p + 1, q + 1, c_bw, d_bw, reg);
1064 let cb_r = ctx.bw(&r, q, p + 1, q + 1, c_bw, d_bw, reg);
1065 let b_bw = restrict(
1066 ((cb_l.0 + cb_r.0) / ((cb_r.1 + sign * cb_l.1).powi(2) + reg * (cb_r.2 + cb_l.2)))
1067 .powf(cb_l.3),
1068 bw_max,
1069 );
1070 let ch_l = ctx.bw(&l, p, 0, q, c_bw, b_bw, reg);
1071 let ch_r = ctx.bw(&r, p, 0, q, c_bw, b_bw, reg);
1072 let h_bw = restrict(
1073 ((ch_l.0 + ch_r.0) / ((ch_r.1 + sign * ch_l.1).powi(2) + reg * (ch_r.2 + ch_l.2)))
1074 .powf(ch_l.3),
1075 bw_max,
1076 );
1077 (x_sd * h_bw, x_sd * b_bw)
1078 };
1079 if sel.needs_sum() {
1080 sum = Some(common(1.0));
1081 }
1082 if sel.needs_rd() {
1083 rd = Some(common(-1.0));
1084 }
1085 let min_nan = |a: f64, b: f64| {
1087 if a.is_nan() || b.is_nan() {
1088 f64::NAN
1089 } else {
1090 a.min(b)
1091 }
1092 };
1093 let median3 = |a: f64, b: f64, c: f64| {
1094 if a.is_nan() || b.is_nan() || c.is_nan() {
1095 return f64::NAN;
1096 }
1097 let mut v = [a, b, c];
1098 v.sort_by(f64::total_cmp);
1099 v[1]
1100 };
1101 let mse = match sel {
1102 BwSelect::Mserd | BwSelect::Cerrd => {
1103 let (h, b) = rd.expect("computed");
1104 RdBandwidths::common(h, b)
1105 }
1106 BwSelect::Msetwo | BwSelect::Certwo => two.expect("computed"),
1107 BwSelect::Msesum | BwSelect::Cersum => {
1108 let (h, b) = sum.expect("computed");
1109 RdBandwidths::common(h, b)
1110 }
1111 BwSelect::Msecomb1 | BwSelect::Cercomb1 => {
1112 let ((h_d, b_d), (h_s, b_s)) = (rd.expect("computed"), sum.expect("computed"));
1113 RdBandwidths::common(min_nan(h_d, h_s), min_nan(b_d, b_s))
1114 }
1115 BwSelect::Msecomb2 | BwSelect::Cercomb2 => {
1116 let ((h_d, b_d), (h_s, b_s)) = (rd.expect("computed"), sum.expect("computed"));
1117 let t = two.expect("computed");
1118 RdBandwidths {
1119 h: (median3(h_d, h_s, t.h.0), median3(h_d, h_s, t.h.1)),
1120 b: (median3(b_d, b_s, t.b.0), median3(b_d, b_s, t.b.1)),
1121 }
1122 }
1123 };
1124 let bws = if sel.is_cer() {
1125 let base = match (left.cl.as_ref(), right.cl.as_ref()) {
1126 (Some(a), Some(b)) => (n_unique_i64(a) + n_unique_i64(b)) as f64,
1127 _ => n as f64,
1128 };
1129 let pf = p as f64;
1130 let cer_h = base.powf(-(pf / ((3.0 + pf) * (3.0 + 2.0 * pf))));
1131 RdBandwidths {
1132 h: (mse.h.0 * cer_h, mse.h.1 * cer_h),
1133 b: mse.b,
1134 }
1135 } else {
1136 mse
1137 };
1138 let all = [bws.h.0, bws.h.1, bws.b.0, bws.b.1];
1139 if !all.iter().all(|v| v.is_finite() && *v > 0.0) {
1140 return Err(DmlError::InvalidArgument(
1141 "not enough variability in the running variable to compute the bandwidth".into(),
1142 ));
1143 }
1144 Ok(match opts.rho {
1145 Some(rho) => RdBandwidths {
1146 h: bws.h,
1147 b: (bws.h.0 / rho, bws.h.1 / rho),
1148 },
1149 None => bws,
1150 })
1151}
1152
1153fn n_unique_i64(v: &[i64]) -> usize {
1154 let mut v = v.to_vec();
1155 v.sort_unstable();
1156 v.dedup();
1157 v.len()
1158}
1159
1160fn effective_vce(vce: Vce, clustered: bool) -> Result<Vce> {
1163 match (vce, clustered) {
1164 (Vce::Nn | Vce::Hc0 | Vce::Hc1, true) => Ok(Vce::Hc1),
1165 (Vce::Hc2 | Vce::Hc3, true) => Err(DmlError::InvalidArgument(
1166 "rdrobust: the leverage-adjusted cluster-robust variances (cr2, cr3) are not available; use Vce::Hc1 (cr1) with clusters".into(),
1167 )),
1168 (v, false) => Ok(v),
1169 }
1170}
1171
1172fn check_support(left: &Side, right: &Side, order: usize) -> Result<()> {
1173 if n_unique(&left.x).min(n_unique(&right.x)) < order + 1 {
1174 return Err(DmlError::InvalidArgument(format!(
1175 "not enough distinct running-variable values on each side of the cutoff for a polynomial of order {order}"
1176 )));
1177 }
1178 Ok(())
1179}
1180
1181pub fn rd_bandwidths(data: &RdData<'_>, opts: &RdOptions) -> Result<RdBandwidths> {
1184 opts.validate()?;
1185 let (left, right) = prepare(data, opts)?;
1186 check_support(&left, &right, opts.p)?;
1187 select_bandwidths(&left, &right, data.cutoff, opts)
1188}
1189
1190pub fn rdbwselect<'a>(
1194 y: ArrayView1<'a, f64>,
1195 x: ArrayView1<'a, f64>,
1196 fuzzy: Option<ArrayView1<'a, f64>>,
1197 c: f64,
1198) -> Result<(f64, f64)> {
1199 let mut data = RdData::new(y, x).cutoff(c);
1200 data.fuzzy = fuzzy;
1201 let bws = rd_bandwidths(&data, &RdOptions::default())?;
1202 Ok((bws.h.0, bws.b.0))
1203}
1204
1205struct SideFit {
1212 beta_p: Array2<f64>,
1213 beta_bc: Array2<f64>,
1214 inv_g_p: Array2<f64>,
1215 inv_g_q: Array2<f64>,
1216 rw_p: Array2<f64>,
1217 rw_q_h: Array2<f64>,
1218 q_q: Array2<f64>,
1219 res_h: Array2<f64>,
1220 res_b: Array2<f64>,
1221 zwd: Option<Array2<f64>>,
1223 u_p: Option<Array2<f64>>,
1224 cl: Option<Vec<i64>>,
1225 n_h: usize,
1226}
1227
1228fn fit_side(side: &Side, c: f64, h: f64, b: f64, opts: &RdOptions, vce: Vce) -> Result<SideFit> {
1229 let (p, q, kernel) = (opts.p, opts.q(), opts.kernel);
1230 let w_h: Vec<f64> = side.x.iter().map(|&x| kernel.weight(x, c, h)).collect();
1231 let w_b: Vec<f64> = side.x.iter().map(|&x| kernel.weight(x, c, b)).collect();
1232 let in_h: Vec<bool> = w_h.iter().map(|&w| w > 0.0).collect();
1233 let in_b: Vec<bool> = w_b.iter().map(|&w| w > 0.0).collect();
1234 let n_h = in_h.iter().filter(|&&k| k).count();
1235 let uniq_in = |keep: &[bool]| {
1236 n_unique(
1237 &side
1238 .x
1239 .iter()
1240 .zip(keep)
1241 .filter(|(_, &k)| k)
1242 .map(|(x, _)| *x)
1243 .collect::<Vec<_>>(),
1244 )
1245 };
1246 if uniq_in(&in_h) < p + 1 || uniq_in(&in_b) < q + 1 {
1247 return Err(DmlError::InvalidArgument(
1248 "not enough distinct running-variable values with positive weight on each side of the cutoff"
1249 .into(),
1250 ));
1251 }
1252 let keep = if h > b { &in_h } else { &in_b };
1253 let pick = |v: &[f64]| -> Vec<f64> {
1254 v.iter()
1255 .zip(keep.iter())
1256 .filter(|(_, &k)| k)
1257 .map(|(a, _)| *a)
1258 .collect()
1259 };
1260 let (ww_h, ww_b) = (pick(&w_h), pick(&w_b));
1261 let e = side.select(keep);
1263 let u: Vec<f64> = e.x.iter().map(|x| (x - c) / h).collect();
1264 let r_q = vander(&e.x.iter().map(|x| x - c).collect::<Vec<_>>(), q);
1265 let r_p = r_q.slice(s![.., ..=p]).to_owned();
1266 let rw_p = scale_rows(&r_p, &ww_h);
1267 let rw_q_b = scale_rows(&r_q, &ww_b);
1268 let l: Array1<f64> = rw_p
1269 .t()
1270 .dot(&Array1::from_iter(u.iter().map(|&v| powk(v, p + 1))));
1271 let sq = |w: &[f64]| w.iter().map(|v| v.sqrt()).collect::<Vec<_>>();
1272 let inv_g_q = {
1273 let m = scale_rows(&r_q, &sq(&ww_b));
1274 spd_inverse(&crossprod(&m, &m))?
1275 };
1276 let inv_g_p = {
1277 let m = scale_rows(&r_p, &sq(&ww_h));
1278 spd_inverse(&crossprod(&m, &m))?
1279 };
1280 let m: Vec<f64> = r_q
1281 .dot(&inv_g_q.row(p + 1))
1282 .iter()
1283 .zip(&ww_b)
1284 .map(|(a, w)| a * w)
1285 .collect();
1286 let hp = powk(h, p + 1);
1287 let mut q_q = rw_p.clone();
1288 for i in 0..q_q.nrows() {
1289 for j in 0..q_q.ncols() {
1290 q_q[[i, j]] -= hp * m[i] * l[j];
1291 }
1292 }
1293 let d = e.d();
1294 let beta_p = inv_g_p.dot(&crossprod(&rw_p, &d));
1295 let beta_q = inv_g_q.dot(&crossprod(&rw_q_b, &d));
1296 let beta_bc = inv_g_p.dot(&crossprod(&q_q, &d));
1297 let (res_h, res_b) = if vce == Vce::Nn {
1298 let res = residuals(&e, Vce::Nn, None, None, opts.nnmatch, p + 1);
1299 (res.clone(), res)
1300 } else {
1301 let (fit_p, fit_q) = (r_p.dot(&beta_p), r_q.dot(&beta_q));
1302 let (hii_p, hii_q) = if matches!(vce, Vce::Hc2 | Vce::Hc3) {
1303 (
1304 Some(leverages(&r_p, &inv_g_p, &rw_p)),
1305 Some(leverages(&r_q, &inv_g_q, &rw_q_b)),
1306 )
1307 } else {
1308 (None, None)
1309 };
1310 (
1311 residuals(&e, vce, Some(&fit_p), hii_p.as_ref(), opts.nnmatch, p + 1),
1312 residuals(&e, vce, Some(&fit_q), hii_q.as_ref(), opts.nnmatch, q + 1),
1313 )
1314 };
1315 let (zwd, u_p) = match &e.z {
1316 Some(z) => (
1317 Some(crossprod(&scale_rows(z, &ww_h), &d)),
1318 Some(crossprod(&rw_p, &d)),
1319 ),
1320 None => (None, None),
1321 };
1322 Ok(SideFit {
1323 rw_q_h: scale_rows(&r_q, &ww_h),
1324 beta_p,
1325 beta_bc,
1326 inv_g_p,
1327 inv_g_q,
1328 rw_p,
1329 q_q,
1330 res_h,
1331 res_b,
1332 zwd,
1333 u_p,
1334 cl: e.cl,
1335 n_h,
1336 })
1337}
1338
1339pub fn rd_estimate(
1345 data: &RdData<'_>,
1346 opts: &RdOptions,
1347 bandwidths: Option<RdBandwidths>,
1348) -> Result<RdEstimate> {
1349 opts.validate()?;
1350 let q = opts.q();
1351 let c = data.cutoff;
1352 let (left, right) = prepare(data, opts)?;
1353 check_support(&left, &right, q)?;
1354 if let (Some(lt), Some(rt)) = (&left.t, &right.t) {
1355 if var(lt) == 0.0
1356 && var(rt) == 0.0
1357 && (np_mean(lt) - np_mean(rt)).abs() < f64::EPSILON.sqrt()
1358 {
1359 return Err(DmlError::InvalidArgument(
1360 "fuzzy RD: the treatment has no variation and no jump at the cutoff".into(),
1361 ));
1362 }
1363 }
1364 let vce = effective_vce(opts.vce, data.cluster.is_some())?;
1365 let n = left.n() + right.n();
1366 let bws = match bandwidths {
1367 Some(bws) => {
1368 let all = [bws.h.0, bws.h.1, bws.b.0, bws.b.1];
1369 if !all.iter().all(|v| v.is_finite() && *v > 0.0) {
1370 return Err(DmlError::InvalidArgument(
1371 "bandwidths must be positive and finite".into(),
1372 ));
1373 }
1374 match opts.rho {
1375 Some(rho) => RdBandwidths {
1376 h: bws.h,
1377 b: (bws.h.0 / rho, bws.h.1 / rho),
1378 },
1379 None => bws,
1380 }
1381 }
1382 None if n < 20 => {
1383 let h = (c - left.x[0])
1384 .abs()
1385 .max((right.x[right.n() - 1] - c).abs());
1386 RdBandwidths::common(h, opts.rho.map_or(h, |rho| h / rho))
1387 }
1388 None => select_bandwidths(&left, &right, c, opts)?,
1389 };
1390 let fl = fit_side(&left, c, bws.h.0, bws.b.0, opts, vce)?;
1391 let fr = fit_side(&right, c, bws.h.1, bws.b.1, opts, vce)?;
1392
1393 let fuzzy = data.fuzzy.is_some();
1394 let dt = usize::from(fuzzy);
1395 let dz = left.d_z();
1396 let sp = opts.scalepar;
1397 let beta_p = &fr.beta_p - &fl.beta_p;
1398 let beta_bc = &fr.beta_bc - &fl.beta_bc;
1399 let (tau_cl, tau_bc, s_y) = if dz == 0 {
1400 let (tau_y_cl, tau_y_bc) = (sp * beta_p[[0, 0]], sp * beta_bc[[0, 0]]);
1401 if fuzzy {
1402 let (tau_t_cl, tau_t_bc) = (beta_p[[0, 1]], beta_bc[[0, 1]]);
1403 let tau_cl = tau_y_cl / tau_t_cl;
1404 let s = vec![1.0 / tau_t_cl, -(tau_y_cl / (tau_t_cl * tau_t_cl))];
1405 let tau_bc = tau_cl - (s[0] * (tau_y_cl - tau_y_bc) + s[1] * (tau_t_cl - tau_t_bc));
1406 (tau_cl, tau_bc, s)
1407 } else {
1408 (tau_y_cl, tau_y_bc, vec![1.0])
1409 }
1410 } else {
1411 let cols_z = s![.., 1 + dt..1 + dt + dz];
1414 let side_terms = |f: &SideFit| -> (Array2<f64>, Array2<f64>) {
1415 let (zwd, u) = (f.zwd.as_ref().expect("covs"), f.u_p.as_ref().expect("covs"));
1416 let uigu = u.slice(cols_z).t().dot(&f.inv_g_p.dot(u));
1417 (
1418 &zwd.slice(cols_z) - &uigu.slice(cols_z),
1419 &zwd.slice(s![.., ..1 + dt]) - &uigu.slice(s![.., ..1 + dt]),
1420 )
1421 };
1422 let ((zwz_l, zwy_l), (zwz_r, zwy_r)) = (side_terms(&fl), side_terms(&fr));
1423 let gamma = sym_pinv(&(&zwz_r + &zwz_l)).dot(&(&zwy_r + &zwy_l));
1424 let s_y: Vec<f64> = std::iter::once(1.0)
1425 .chain(gamma.column(0).iter().map(|g| -g))
1426 .collect();
1427 let dot = |a: &[f64], b: &[f64]| a.iter().zip(b).map(|(x, y)| x * y).sum::<f64>();
1428 let row = |beta: &Array2<f64>, col: usize| -> Vec<f64> {
1429 std::iter::once(beta[[0, col]])
1430 .chain((0..dz).map(|j| beta[[0, 1 + dt + j]]))
1431 .collect()
1432 };
1433 if fuzzy {
1434 let s_t: Vec<f64> = std::iter::once(1.0)
1435 .chain(gamma.column(1).iter().map(|g| -g))
1436 .collect();
1437 let tau_y_cl = sp * dot(&s_y, &row(&beta_p, 0));
1438 let tau_t_cl = dot(&s_t, &row(&beta_p, 1));
1439 let tau_y_bc = sp * dot(&s_y, &row(&beta_bc, 0));
1440 let tau_t_bc = dot(&s_t, &row(&beta_bc, 1));
1441 let tau_cl = tau_y_cl / tau_t_cl;
1442 let s2 = [1.0 / tau_t_cl, -(tau_y_cl / (tau_t_cl * tau_t_cl))];
1443 let tau_bc = tau_cl - (s2[0] * (tau_y_cl - tau_y_bc) + s2[1] * (tau_t_cl - tau_t_bc));
1444 let mut s = s2.to_vec();
1445 s.extend((0..dz).map(|j| {
1446 -(1.0 / tau_t_cl) * gamma[[j, 0]]
1447 + (tau_y_cl / (tau_t_cl * tau_t_cl)) * gamma[[j, 1]]
1448 }));
1449 (tau_cl, tau_bc, s)
1450 } else {
1451 let tau_cl = sp * dot(&s_y, &beta_p.row(0).to_vec());
1452 let tau_bc = sp * dot(&s_y, &beta_bc.row(0).to_vec());
1453 (tau_cl, tau_bc, s_y)
1454 }
1455 };
1456
1457 let hb_match = bws.h == bws.b;
1458 let clustered = data.cluster.is_some();
1459 let variance = |f: &SideFit, robust: bool| -> f64 {
1460 let cl = f.cl.as_deref();
1461 if !robust {
1462 let k = f.rw_p.ncols();
1463 return f
1464 .inv_g_p
1465 .dot(&vce_meat(&s_y, &f.rw_p, &f.res_h, cl, k))
1466 .dot(&f.inv_g_p)[[0, 0]];
1467 }
1468 if hb_match && clustered {
1469 let k = f.rw_q_h.ncols();
1471 f.inv_g_q
1472 .dot(&vce_meat(&s_y, &f.rw_q_h, &f.res_b, cl, k))
1473 .dot(&f.inv_g_q)[[0, 0]]
1474 } else {
1475 f.inv_g_p
1476 .dot(&vce_meat(&s_y, &f.q_q, &f.res_b, cl, q + 1))
1477 .dot(&f.inv_g_p)[[0, 0]]
1478 }
1479 };
1480 let se_cl = (sp * sp * (variance(&fl, false) + variance(&fr, false))).sqrt();
1481 let se_rb = (sp * sp * (variance(&fl, true) + variance(&fr, true))).sqrt();
1482 let coef = [tau_cl, tau_bc, tau_bc];
1483 let se = [se_cl, se_cl, se_rb];
1484 let quant = -norm_ppf((1.0 - opts.level) / 2.0);
1485 let ci = std::array::from_fn(|k| [coef[k] - quant * se[k], coef[k] + quant * se[k]]);
1486 Ok(RdEstimate {
1487 coef,
1488 se,
1489 ci,
1490 h: bws.h,
1491 b: bws.b,
1492 n_h: (fl.n_h, fr.n_h),
1493 n: (left.n(), right.n()),
1494 })
1495}
1496
1497pub fn rdrobust<'a>(
1503 y: ArrayView1<'a, f64>,
1504 x: ArrayView1<'a, f64>,
1505 fuzzy: Option<ArrayView1<'a, f64>>,
1506 c: f64,
1507 bandwidths: Option<(f64, f64)>,
1508) -> Result<RdEstimate> {
1509 let mut data = RdData::new(y, x).cutoff(c);
1510 data.fuzzy = fuzzy;
1511 rd_estimate(
1512 &data,
1513 &RdOptions::default(),
1514 bandwidths.map(|(h, b)| RdBandwidths::common(h, b)),
1515 )
1516}
1517
1518#[cfg(test)]
1519mod tests {
1520 use super::*;
1521 use ndarray::array;
1522
1523 #[test]
1524 fn duplicates_count_runs() {
1525 let (d, id) = duplicates(&[1.0, 2.0, 2.0, 2.0, 3.0]);
1526 assert_eq!(d, vec![1, 3, 3, 3, 1]);
1527 assert_eq!(id, vec![1, 1, 2, 3, 1]);
1528 }
1529
1530 #[test]
1531 fn quantile_type2_matches_r() {
1532 let x = [1.0, 2.0, 3.0, 4.0];
1533 assert_eq!(quantile_type2(&x, 0.25), 1.5);
1534 assert_eq!(quantile_type2(&x, 0.75), 3.5);
1535 assert_eq!(quantile_type2(&[1.0, 2.0, 3.0], 0.25), 1.0);
1536 }
1537
1538 #[test]
1539 fn collinear_covariates_are_dropped() {
1540 let z = array![
1541 [1.0, 2.0, 0.5],
1542 [2.0, 4.0, -1.0],
1543 [3.0, 6.0, 2.0],
1544 [4.0, 8.0, 0.0]
1545 ];
1546 let keep = independent_columns(&z);
1547 assert_eq!(keep.len(), 2);
1548 assert!(keep.contains(&2));
1549 let full = array![[1.0, 0.0], [0.0, 1.0], [1.0, 1.0]];
1550 assert_eq!(independent_columns(&full), vec![0, 1]);
1551 }
1552
1553 #[test]
1554 fn options_are_validated() {
1555 let bad = |o: RdOptions| o.validate().is_err();
1556 assert!(bad(RdOptions {
1557 q: Some(1),
1558 ..RdOptions::default()
1559 }));
1560 assert!(bad(RdOptions {
1561 level: 95.0,
1562 ..RdOptions::default()
1563 }));
1564 assert!(bad(RdOptions {
1565 nnmatch: 0,
1566 ..RdOptions::default()
1567 }));
1568 assert!(bad(RdOptions {
1569 rho: Some(0.0),
1570 ..RdOptions::default()
1571 }));
1572 assert!(RdOptions::default().validate().is_ok());
1573 assert!(effective_vce(Vce::Hc2, true).is_err());
1574 assert_eq!(effective_vce(Vce::Nn, true).unwrap(), Vce::Hc1);
1575 }
1576}