1use ndarray::{s, Array1, Array2, ArrayView1, ArrayView2, Axis};
20
21use crate::compat::{np_mean, np_sum};
22use crate::error::{DmlError, Result};
23use crate::linalg::{cholesky_factor, spd_inverse, sym_eigen, sym_pinv};
24use crate::stats::norm_ppf;
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
28pub enum Kernel {
29 #[default]
30 Triangular,
31 Epanechnikov,
32 Uniform,
33}
34
35impl Kernel {
36 fn weight(self, x: f64, c: f64, h: f64) -> f64 {
38 let u = (x - c) / h;
39 let inside = f64::from(u.abs() <= 1.0);
40 match self {
41 Kernel::Epanechnikov => (0.75 * (1.0 - u * u) * inside) / h,
42 Kernel::Uniform => (0.5 * inside) / h,
43 Kernel::Triangular => ((1.0 - u.abs()) * inside) / h,
44 }
45 }
46
47 fn c_c(self) -> f64 {
49 match self {
50 Kernel::Epanechnikov => 2.34,
51 Kernel::Uniform => 1.843,
52 Kernel::Triangular => 2.576,
53 }
54 }
55}
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
59pub enum BwSelect {
60 #[default]
62 Mserd,
63 Msetwo,
65 Msesum,
68 Msecomb1,
70 Msecomb2,
72 Cerrd,
74 Certwo,
75 Cersum,
76 Cercomb1,
77 Cercomb2,
78}
79
80impl BwSelect {
81 fn needs_two(self) -> bool {
82 matches!(
83 self,
84 BwSelect::Msetwo | BwSelect::Certwo | BwSelect::Msecomb2 | BwSelect::Cercomb2
85 )
86 }
87
88 fn needs_sum(self) -> bool {
89 matches!(
90 self,
91 BwSelect::Msesum
92 | BwSelect::Cersum
93 | BwSelect::Msecomb1
94 | BwSelect::Msecomb2
95 | BwSelect::Cercomb1
96 | BwSelect::Cercomb2
97 )
98 }
99
100 fn needs_rd(self) -> bool {
101 matches!(
102 self,
103 BwSelect::Mserd
104 | BwSelect::Cerrd
105 | BwSelect::Msecomb1
106 | BwSelect::Msecomb2
107 | BwSelect::Cercomb1
108 | BwSelect::Cercomb2
109 )
110 }
111
112 fn is_cer(self) -> bool {
113 matches!(
114 self,
115 BwSelect::Cerrd
116 | BwSelect::Certwo
117 | BwSelect::Cersum
118 | BwSelect::Cercomb1
119 | BwSelect::Cercomb2
120 )
121 }
122}
123
124#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
130pub enum Vce {
131 #[default]
133 Nn,
134 Hc0,
135 Hc1,
136 Hc2,
137 Hc3,
138 Cr1,
140 Cr2,
142 Cr3,
144}
145
146#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
149pub enum MassPoints {
150 #[default]
154 Adjust,
155 Check,
157 Off,
159}
160
161#[derive(Debug, Clone, PartialEq)]
164pub struct RdOptions {
165 pub p: usize,
167 pub q: Option<usize>,
169 pub kernel: Kernel,
170 pub bwselect: BwSelect,
171 pub vce: Vce,
172 pub nnmatch: usize,
174 pub level: f64,
176 pub masspoints: MassPoints,
177 pub bwcheck: Option<usize>,
180 pub bwrestrict: bool,
182 pub stdvars: bool,
185 pub scaleregul: f64,
187 pub sharpbw: bool,
190 pub rho: Option<f64>,
192 pub scalepar: f64,
194 pub covs_drop: bool,
196}
197
198impl Default for RdOptions {
199 fn default() -> Self {
200 Self {
201 p: 1,
202 q: None,
203 kernel: Kernel::Triangular,
204 bwselect: BwSelect::Mserd,
205 vce: Vce::Nn,
206 nnmatch: 3,
207 level: 0.95,
208 masspoints: MassPoints::Adjust,
209 bwcheck: None,
210 bwrestrict: true,
211 stdvars: true,
212 scaleregul: 1.0,
213 sharpbw: false,
214 rho: None,
215 scalepar: 1.0,
216 covs_drop: true,
217 }
218 }
219}
220
221impl RdOptions {
222 fn q(&self) -> usize {
223 self.q.unwrap_or(self.p + 1)
224 }
225
226 fn validate(&self) -> Result<()> {
227 let q = self.q();
228 let bad = |msg: &str| Err(DmlError::InvalidArgument(format!("rdrobust: {msg}")));
229 if self.p > 20 || q > 20 || q <= self.p {
230 return bad("polynomial orders need p <= 20 and p < q <= 20");
231 }
232 if self.nnmatch == 0 {
233 return bad("nnmatch must be at least 1");
234 }
235 if !(self.level > 0.0 && self.level < 1.0) {
236 return bad("level must be in (0, 1)");
237 }
238 if self.bwcheck == Some(0) {
239 return bad("bwcheck must be a positive integer");
240 }
241 if self.rho.is_some_and(|r| !(r.is_finite() && r > 0.0)) {
242 return bad("rho must be positive");
243 }
244 if !(self.scaleregul.is_finite() && self.scaleregul >= 0.0) || !self.scalepar.is_finite() {
245 return bad("scaleregul and scalepar must be finite (scaleregul >= 0)");
246 }
247 Ok(())
248 }
249}
250
251#[derive(Debug, Clone, Copy)]
254pub struct RdData<'a> {
255 pub y: ArrayView1<'a, f64>,
256 pub x: ArrayView1<'a, f64>,
257 pub cutoff: f64,
258 pub fuzzy: Option<ArrayView1<'a, f64>>,
259 pub covs: Option<ArrayView2<'a, f64>>,
260 pub cluster: Option<&'a [i64]>,
261}
262
263impl<'a> RdData<'a> {
264 pub fn new(y: ArrayView1<'a, f64>, x: ArrayView1<'a, f64>) -> Self {
266 Self {
267 y,
268 x,
269 cutoff: 0.0,
270 fuzzy: None,
271 covs: None,
272 cluster: None,
273 }
274 }
275
276 pub fn cutoff(mut self, c: f64) -> Self {
277 self.cutoff = c;
278 self
279 }
280
281 pub fn fuzzy(mut self, t: ArrayView1<'a, f64>) -> Self {
283 self.fuzzy = Some(t);
284 self
285 }
286
287 pub fn covariates(mut self, z: ArrayView2<'a, f64>) -> Self {
289 self.covs = Some(z);
290 self
291 }
292
293 pub fn clusters(mut self, labels: &'a [i64]) -> Self {
295 self.cluster = Some(labels);
296 self
297 }
298}
299
300#[derive(Debug, Clone, Copy, PartialEq)]
302pub struct RdBandwidths {
303 pub h: (f64, f64),
305 pub b: (f64, f64),
307}
308
309impl RdBandwidths {
310 pub fn common(h: f64, b: f64) -> Self {
312 Self {
313 h: (h, h),
314 b: (b, b),
315 }
316 }
317}
318
319#[derive(Debug, Clone)]
322pub struct RdEstimate {
323 pub coef: [f64; 3],
324 pub se: [f64; 3],
325 pub ci: [[f64; 2]; 3],
327 pub h: (f64, f64),
329 pub b: (f64, f64),
331 pub n_h: (usize, usize),
333 pub n: (usize, usize),
335}
336
337#[derive(Clone)]
343struct Side {
344 x: Vec<f64>,
345 y: Vec<f64>,
346 t: Option<Vec<f64>>,
347 z: Option<Array2<f64>>,
348 cl: Option<Vec<i64>>,
349 dups: Vec<usize>,
350 dupsid: Vec<usize>,
351}
352
353impl Side {
354 fn new(
355 x: Vec<f64>,
356 y: Vec<f64>,
357 t: Option<Vec<f64>>,
358 z: Option<Array2<f64>>,
359 cl: Option<Vec<i64>>,
360 ) -> Self {
361 let (dups, dupsid) = duplicates(&x);
362 Self {
363 x,
364 y,
365 t,
366 z,
367 cl,
368 dups,
369 dupsid,
370 }
371 }
372
373 fn n(&self) -> usize {
374 self.x.len()
375 }
376
377 fn select(&self, keep: &[bool]) -> Side {
379 let rows: Vec<usize> = (0..self.n()).filter(|&i| keep[i]).collect();
380 let pick = |v: &[f64]| -> Vec<f64> { rows.iter().map(|&i| v[i]).collect() };
381 Side::new(
382 pick(&self.x),
383 pick(&self.y),
384 self.t.as_ref().map(|t| pick(t)),
385 self.z.as_ref().map(|z| z.select(Axis(0), &rows)),
386 self.cl
387 .as_ref()
388 .map(|c| rows.iter().map(|&i| c[i]).collect()),
389 )
390 }
391
392 fn d_t(&self) -> usize {
393 usize::from(self.t.is_some())
394 }
395
396 fn d_z(&self) -> usize {
397 self.z.as_ref().map_or(0, |z| z.ncols())
398 }
399
400 fn d(&self) -> Array2<f64> {
402 let (dt, dz) = (self.d_t(), self.d_z());
403 let mut d = Array2::zeros((self.n(), 1 + dt + dz));
404 for i in 0..self.n() {
405 d[[i, 0]] = self.y[i];
406 if let Some(t) = &self.t {
407 d[[i, 1]] = t[i];
408 }
409 if let Some(z) = &self.z {
410 for j in 0..dz {
411 d[[i, 1 + dt + j]] = z[[i, j]];
412 }
413 }
414 }
415 d
416 }
417}
418
419fn duplicates(x: &[f64]) -> (Vec<usize>, Vec<usize>) {
422 let n = x.len();
423 let (mut dups, mut dupsid) = (vec![0; n], vec![0; n]);
424 let mut start = 0;
425 while start < n {
426 let mut end = start + 1;
427 while end < n && x[end] == x[start] {
428 end += 1;
429 }
430 for (k, i) in (start..end).enumerate() {
431 dups[i] = end - start;
432 dupsid[i] = k + 1;
433 }
434 start = end;
435 }
436 (dups, dupsid)
437}
438
439fn vander(u: &[f64], p: usize) -> Array2<f64> {
441 let mut out = Array2::zeros((u.len(), p + 1));
442 for (i, &v) in u.iter().enumerate() {
443 out[[i, 0]] = 1.0;
444 for j in 1..=p {
445 out[[i, j]] = out[[i, j - 1]] * v;
446 }
447 }
448 out
449}
450
451fn powk(x: f64, k: usize) -> f64 {
453 match k {
454 0 => 1.0,
455 1 => x,
456 2 => x * x,
457 _ => x.powf(k as f64),
458 }
459}
460
461fn scale_rows(m: &Array2<f64>, w: &[f64]) -> Array2<f64> {
462 let mut out = m.clone();
463 for (mut row, &wi) in out.rows_mut().into_iter().zip(w) {
464 row *= wi;
465 }
466 out
467}
468
469fn crossprod(a: &Array2<f64>, b: &Array2<f64>) -> Array2<f64> {
470 a.t().dot(b)
471}
472
473fn n_unique(x: &[f64]) -> usize {
474 let mut v = x.to_vec();
475 v.sort_by(f64::total_cmp);
476 v.dedup();
477 v.len()
478}
479
480fn factorial(k: usize) -> f64 {
481 (1..=k).product::<usize>() as f64
482}
483
484fn std_ddof1(v: &[f64]) -> f64 {
486 let mean = np_sum(v) / v.len() as f64;
487 let sq: Vec<f64> = v.iter().map(|x| (x - mean) * (x - mean)).collect();
488 (np_sum(&sq) / (v.len() - 1) as f64).sqrt()
489}
490
491fn var(v: &[f64]) -> f64 {
493 let mean = np_mean(v);
494 let sq: Vec<f64> = v.iter().map(|x| (x - mean) * (x - mean)).collect();
495 np_mean(&sq)
496}
497
498fn quantile_type2(x: &[f64], prob: f64) -> f64 {
500 let n = x.len();
501 let h = n as f64 * prob;
502 let j = h.floor() as usize;
503 if h == j as f64 {
504 0.5 * (x[j - 1] + x[j])
505 } else {
506 x[j]
507 }
508}
509
510fn independent_columns(z: &Array2<f64>) -> Vec<usize> {
514 const TOL: f64 = 1e-5;
515 let k = z.ncols();
516 let (diag, _) = householder_qr(z, false);
517 if diag.iter().all(|d| d.abs() > TOL) {
518 return (0..k).collect();
519 }
520 let (diag, pivot) = householder_qr(z, true);
521 pivot
522 .into_iter()
523 .zip(diag)
524 .filter(|(_, d)| d.abs() > TOL)
525 .map(|(j, _)| j)
526 .collect()
527}
528
529fn householder_qr(a: &Array2<f64>, pivoting: bool) -> (Vec<f64>, Vec<usize>) {
532 let (n, k) = a.dim();
533 let mut m = a.clone();
534 let mut order: Vec<usize> = (0..k).collect();
535 let mut diag = Vec::with_capacity(k.min(n));
536 for j in 0..k.min(n) {
537 if pivoting {
538 let norm =
539 |c: usize, m: &Array2<f64>| (j..n).map(|i| m[[i, c]] * m[[i, c]]).sum::<f64>();
540 let best = (j..k)
541 .max_by(|&a, &b| norm(a, &m).total_cmp(&norm(b, &m)))
542 .expect("columns left");
543 if best != j {
544 for i in 0..n {
545 m.swap([i, j], [i, best]);
546 }
547 order.swap(j, best);
548 }
549 }
550 let alpha = (j..n).map(|i| m[[i, j]] * m[[i, j]]).sum::<f64>().sqrt();
551 let alpha = if m[[j, j]] > 0.0 { -alpha } else { alpha };
552 diag.push(alpha);
553 if alpha == 0.0 {
554 continue;
555 }
556 let mut v: Vec<f64> = (j..n).map(|i| m[[i, j]]).collect();
557 v[0] -= alpha;
558 let vnorm2: f64 = v.iter().map(|x| x * x).sum();
559 if vnorm2 == 0.0 {
560 continue;
561 }
562 for c in j..k {
563 let dot: f64 = (j..n).map(|i| v[i - j] * m[[i, c]]).sum();
564 let f = 2.0 * dot / vnorm2;
565 for i in j..n {
566 m[[i, c]] -= f * v[i - j];
567 }
568 }
569 }
570 (diag, order)
571}
572
573fn prepare(data: &RdData<'_>, opts: &RdOptions) -> Result<(Side, Side)> {
575 let (y, x, c) = (data.y, data.x, data.cutoff);
576 let n = x.len();
577 let lengths_ok = y.len() == n
578 && data.fuzzy.is_none_or_len(n)
579 && data.covs.map_or(true, |z| z.nrows() == n)
580 && data.cluster.map_or(true, |cl| cl.len() == n);
581 if !lengths_ok {
582 return Err(DmlError::DimensionMismatch(
583 "rdrobust: y, x, fuzzy, covs and cluster need the same number of observations".into(),
584 ));
585 }
586 let finite = x.iter().chain(y.iter()).all(|v| v.is_finite())
587 && data.fuzzy.map_or(true, |t| t.iter().all(|v| v.is_finite()))
588 && data.covs.map_or(true, |z| z.iter().all(|v| v.is_finite()));
589 if !finite {
590 return Err(DmlError::InvalidArgument(
591 "rdrobust: data contain NaN or infinite values".into(),
592 ));
593 }
594 if !c.is_finite() {
595 return Err(DmlError::InvalidArgument(
596 "rdrobust: the cutoff must be finite".into(),
597 ));
598 }
599 let covs = match data.covs {
600 Some(z) if opts.covs_drop => {
601 let z = z.to_owned();
602 let keep = independent_columns(&z);
603 Some(z.select(Axis(1), &keep))
604 }
605 Some(z) => Some(z.to_owned()),
606 None => None,
607 };
608 let mut order: Vec<usize> = (0..n).collect();
609 order.sort_by(|&a, &b| x[a].total_cmp(&x[b]));
610 let (x_min, x_max) = (x[order[0]], x[order[n - 1]]);
611 if c <= x_min || c >= x_max {
612 return Err(DmlError::InvalidArgument(
613 "the cutoff must lie within the range of the running variable".into(),
614 ));
615 }
616 let side = |left: bool| -> Side {
617 let rows: Vec<usize> = order
618 .iter()
619 .copied()
620 .filter(|&i| (x[i] < c) == left)
621 .collect();
622 Side::new(
623 rows.iter().map(|&i| x[i]).collect(),
624 rows.iter().map(|&i| y[i]).collect(),
625 data.fuzzy.map(|t| rows.iter().map(|&i| t[i]).collect()),
626 covs.as_ref().map(|z| z.select(Axis(0), &rows)),
627 data.cluster.map(|cl| rows.iter().map(|&i| cl[i]).collect()),
628 )
629 };
630 Ok((side(true), side(false)))
631}
632
633trait LenCheck {
635 fn is_none_or_len(&self, n: usize) -> bool;
636}
637
638impl LenCheck for Option<ArrayView1<'_, f64>> {
639 fn is_none_or_len(&self, n: usize) -> bool {
640 self.map_or(true, |t| t.len() == n)
641 }
642}
643
644fn residuals(
651 e: &Side,
652 vce: Vce,
653 fitted: Option<&Array2<f64>>,
654 hii: Option<&Array1<f64>>,
655 nnmatch: usize,
656 d: usize,
657) -> Array2<f64> {
658 let dmat = e.d();
659 if vce == Vce::Nn {
660 return nn_residuals(e, &dmat, nnmatch);
661 }
662 let n = e.n();
663 let fitted = fitted.expect("plug-in residuals need fitted values");
664 let mut res = &dmat - fitted;
665 for i in 0..n {
666 let w = match vce {
668 Vce::Hc0 | Vce::Cr1 | Vce::Cr2 | Vce::Cr3 => 1.0,
669 Vce::Hc1 => (n as f64 / (n - d) as f64).sqrt(),
670 Vce::Hc2 => (1.0 / (1.0 - hii.expect("leverages")[i]).max(1e-8)).sqrt(),
671 Vce::Hc3 => 1.0 / (1.0 - hii.expect("leverages")[i]).max(1e-8),
672 Vce::Nn => unreachable!(),
673 };
674 res.row_mut(i).mapv_inplace(|r| w * r);
675 }
676 res
677}
678
679fn nn_residuals(s: &Side, dmat: &Array2<f64>, nnmatch: usize) -> Array2<f64> {
683 let n = s.n();
684 let mut res = Array2::zeros(dmat.dim());
685 let tol_eps = f64::EPSILON.sqrt();
686 let (x, dups, dupsid) = (&s.x, &s.dups, &s.dupsid);
687 for pos in 0..n {
688 let p = pos as i64;
689 let mut rpos = (dups[pos] - dupsid[pos]) as i64;
690 let mut lpos = dupsid[pos] as i64 - 1;
691 let cap = nnmatch.min(n.saturating_sub(1)) as i64;
692 while lpos + rpos < cap {
693 if p - lpos - 1 < 0 {
694 rpos += dups[(p + rpos + 1) as usize] as i64;
695 } else if p + rpos + 1 >= n as i64 {
696 lpos += dups[(p - lpos - 1) as usize] as i64;
697 } else {
698 let dleft = x[pos] - x[(p - lpos - 1) as usize];
699 let dright = x[(p + rpos + 1) as usize] - x[pos];
700 let nn_tol = dleft.max(dright) * tol_eps;
701 if dleft - dright > nn_tol {
702 rpos += dups[(p + rpos + 1) as usize] as i64;
703 } else if dright - dleft > nn_tol {
704 lpos += dups[(p - lpos - 1) as usize] as i64;
705 } else {
706 rpos += dups[(p + rpos + 1) as usize] as i64;
707 lpos += dups[(p - lpos - 1) as usize] as i64;
708 }
709 }
710 }
711 let (lo, hi) = ((p - lpos) as usize, (p + rpos + 1) as usize);
712 let ji = (hi - lo - 1) as f64;
713 let sf = (ji / (ji + 1.0)).powf(0.5);
714 for col in 0..dmat.ncols() {
715 let mut sum = 0.0;
716 for j in lo..hi {
717 sum += dmat[[j, col]];
718 }
719 sum -= dmat[[pos, col]];
720 res[[pos, col]] = sf * (dmat[[pos, col]] - sum / ji);
721 }
722 }
723 res
724}
725
726fn cluster_groups(cl: &[i64]) -> Vec<Vec<usize>> {
728 let mut order: Vec<usize> = (0..cl.len()).collect();
729 order.sort_by_key(|&i| cl[i]);
730 let mut groups: Vec<Vec<usize>> = Vec::new();
731 for i in order {
732 match groups.last_mut() {
733 Some(g) if cl[g[0]] == cl[i] => g.push(i),
734 _ => groups.push(vec![i]),
735 }
736 }
737 groups
738}
739
740fn add_outer(outer: &mut Array2<f64>, v: &Array1<f64>) {
742 let k = v.len();
743 for a in 0..k {
744 for b in 0..k {
745 outer[[a, b]] += v[a] * v[b];
746 }
747 }
748}
749
750fn sym_inverse(a: &Array2<f64>) -> Array2<f64> {
752 spd_inverse(a).unwrap_or_else(|_| sym_pinv(a))
753}
754
755type Adjust = Box<dyn Fn(&Array1<f64>) -> Array1<f64>>;
757
758#[derive(Debug, Clone, Copy, PartialEq, Eq)]
760enum Crv {
761 Two,
762 Three,
763}
764
765impl Crv {
766 fn of(vce: Vce) -> Option<Self> {
767 match vce {
768 Vce::Cr2 => Some(Crv::Two),
769 Vce::Cr3 => Some(Crv::Three),
770 _ => None,
771 }
772 }
773}
774
775struct CrvParts<'a> {
777 kind: Crv,
778 inv_g: &'a Array2<f64>,
779 sqrt_rx: &'a Array2<f64>,
781 g: Option<Array2<f64>>,
784}
785
786impl<'a> CrvParts<'a> {
787 fn new(vce: Vce, inv_g: &'a Array2<f64>, sqrt_rx: &'a Array2<f64>, gram: bool) -> Option<Self> {
788 Crv::of(vce).map(|kind| CrvParts {
789 kind,
790 inv_g,
791 sqrt_rx,
792 g: (gram && kind == Crv::Three).then(|| crossprod(sqrt_rx, sqrt_rx)),
793 })
794 }
795}
796
797fn vce_meat(
802 s: &[f64],
803 rx: &Array2<f64>,
804 res: &Array2<f64>,
805 cl: Option<&[i64]>,
806 k_df: usize,
807 crv: Option<&CrvParts<'_>>,
808) -> Array2<f64> {
809 if let (Some(cl), Some(crv)) = (cl, crv) {
810 return crv_meat(s, res, cl, crv);
811 }
812 let k = rx.ncols();
813 let combined: Vec<f64> = res
814 .rows()
815 .into_iter()
816 .map(|row| row.iter().zip(s).map(|(r, si)| r * si).sum())
817 .collect();
818 match cl {
819 None => {
820 let scaled = scale_rows(rx, &combined);
821 crossprod(&scaled, &scaled)
822 }
823 Some(cl) => {
824 let n = cl.len();
825 let groups = cluster_groups(cl);
826 let mut meat = Array2::<f64>::zeros((k, k));
827 for rows in &groups {
828 let mut sv = Array1::<f64>::zeros(k);
829 for &i in rows {
830 sv.scaled_add(combined[i], &rx.row(i));
831 }
832 add_outer(&mut meat, &sv);
833 }
834 let g = groups.len();
835 let w = if g > 1 {
836 ((n - 1) as f64 / (n - k_df) as f64) * (g as f64 / (g - 1) as f64)
837 } else {
838 f64::NAN
839 };
840 meat * w
841 }
842 }
843}
844
845fn crv_meat(s: &[f64], res: &Array2<f64>, cl: &[i64], crv: &CrvParts<'_>) -> Array2<f64> {
851 let k = crv.sqrt_rx.ncols();
852 let mut meat = Array2::<f64>::zeros((k, k));
853 let g_full = match crv.kind {
854 Crv::Three => Some(crv.g.clone().unwrap_or_else(|| sym_inverse(crv.inv_g))),
855 Crv::Two => None,
856 };
857 let c_half = match crv.kind {
859 Crv::Two => cholesky_factor(crv.inv_g).ok().map(|l| l.t().to_owned()),
860 Crv::Three => None,
861 };
862 for rows in cluster_groups(cl) {
863 let srx = crv.sqrt_rx.select(Axis(0), &rows);
864 let sw = srx.column(0).to_owned();
865 let l_g = crossprod(&srx, &srx);
866 let mut sv = Array1::<f64>::zeros(k);
867 let adjust: Adjust = match crv.kind {
869 Crv::Three => {
870 let m_g = sym_inverse(&(g_full.as_ref().expect("crv3") - &l_g));
871 let lm = l_g.dot(&m_g);
872 Box::new(move |u: &Array1<f64>| lm.dot(u))
873 }
874 Crv::Two => match &c_half {
875 Some(c) => {
876 let (sigma2, v) = sym_eigen(&c.dot(&l_g).dot(&c.t()));
877 let coef = sigma2.mapv(|s2| {
878 let s2 = s2.max(0.0);
879 if s2 < 1e-14 {
880 0.0
881 } else {
882 (1.0 / (1.0 - s2).max(1e-8).sqrt() - 1.0) / s2
883 }
884 });
885 let ltc = l_g.dot(&c.t());
886 let c = c.clone();
887 Box::new(move |u: &Array1<f64>| {
888 let vt_ru = v.t().dot(&c.dot(u));
889 ltc.dot(&v.dot(&(&coef * &vt_ru)))
890 })
891 }
892 None => Box::new(move |u: &Array1<f64>| Array1::zeros(u.len())),
893 },
894 };
895 for (l, &sl) in s.iter().enumerate() {
896 let r: Array1<f64> = rows.iter().map(|&i| res[[i, l]]).collect();
897 let u = srx.t().dot(&(&sw * &r));
898 let score = &u + &adjust(&u);
899 sv.scaled_add(sl, &score);
900 }
901 add_outer(&mut meat, &sv);
902 }
903 meat
904}
905
906#[allow(clippy::too_many_arguments)]
910fn crv_meat_qq(
911 q_q: &Array2<f64>,
912 r_q: &Array2<f64>,
913 w_b: &[f64],
914 inv_g_q: &Array2<f64>,
915 g_q: &Array2<f64>,
916 res: &Array2<f64>,
917 cl: &[i64],
918 kind: Crv,
919 s: &[f64],
920) -> Array2<f64> {
921 let k = q_q.ncols();
922 let mut meat = Array2::<f64>::zeros((k, k));
923 for rows in cluster_groups(cl) {
924 let q_g = q_q.select(Axis(0), &rows);
925 let r_g = r_q.select(Axis(0), &rows);
926 let wb: Array1<f64> = rows.iter().map(|&i| w_b[i]).collect();
927 let sr = scale_rows(&r_g, &wb.iter().map(|w| w.sqrt()).collect::<Vec<_>>());
928 let l_g = crossprod(&sr, &sr);
929 let p_g = q_g.t().dot(&r_g);
930 let lambda: Option<Array2<f64>> = match kind {
933 Crv::Two => {
934 let (sigma2, v) = sym_eigen(&l_g);
935 let sigma = sigma2.mapv(|v| v.max(0.0).sqrt());
936 let tol = sigma.iter().fold(0.0_f64, |m, &x| m.max(x)) * 1e-10;
937 let keep: Vec<usize> = (0..sigma.len()).filter(|&j| sigma[j] > tol).collect();
938 if keep.is_empty() {
939 None
940 } else {
941 let v_r = v.select(Axis(1), &keep);
942 let sig: Array1<f64> = keep.iter().map(|&j| sigma[j]).collect();
943 let d = Array2::from_diag(&sig);
944 let t = d.dot(&v_r.t().dot(&inv_g_q.dot(&v_r))).dot(&d);
945 let t = (&t + &t.t()) / 2.0;
946 let (tau, w_g) = sym_eigen(&t);
947 let gamma = tau.mapv(|t| 1.0 / (1.0 - t.clamp(0.0, 1.0 - 1e-10)).sqrt() - 1.0);
948 let a = v_r
949 .dot(&Array2::from_diag(&sig.mapv(|x| 1.0 / x)))
950 .dot(&w_g);
951 Some(a.dot(&Array2::from_diag(&gamma)).dot(&a.t()))
952 }
953 }
954 Crv::Three => {
955 Some(spd_inverse(&(g_q - &l_g)).unwrap_or_else(|_| Array2::zeros(l_g.dim())))
956 }
957 };
958 let mut sv = Array1::<f64>::zeros(k);
959 for (l, &sl) in s.iter().enumerate() {
960 let e: Array1<f64> = rows.iter().map(|&i| res[[i, l]]).collect();
961 let mut score = q_g.t().dot(&e);
962 if let Some(lam) = &lambda {
963 let u_q = r_g.t().dot(&(&wb * &e));
964 score = &score + &p_g.dot(&lam.dot(&u_q));
965 }
966 sv.scaled_add(sl, &score);
967 }
968 add_outer(&mut meat, &sv);
969 }
970 meat
971}
972
973fn leverages(r: &Array2<f64>, inv_g: &Array2<f64>, rw: &Array2<f64>) -> Array1<f64> {
975 (r.dot(inv_g) * rw).sum_axis(Axis(1))
976}
977
978fn covariate_gamma(rw: &Array2<f64>, inv_g: &Array2<f64>, e: &Side, w: &[f64]) -> Array2<f64> {
981 let (dt, dz) = (e.d_t(), e.d_z());
982 let d = e.d();
983 let u = crossprod(rw, &d);
984 let zw = scale_rows(e.z.as_ref().expect("covariates"), w);
985 let zwd = crossprod(&zw, &d);
986 let cols_z = s![.., 1 + dt..1 + dt + dz];
987 let uigu = u.slice(cols_z).t().dot(&inv_g.dot(&u));
988 let zwz = &zwd.slice(cols_z) - &uigu.slice(cols_z);
989 let zwy = &zwd.slice(s![.., ..1 + dt]) - &uigu.slice(s![.., ..1 + dt]);
990 sym_pinv(&zwz).dot(&zwy)
991}
992
993type BwParts = (f64, f64, f64, f64);
1000
1001struct BwCtx<'o> {
1003 opts: &'o RdOptions,
1004 vce: Vce,
1005 c: f64,
1006}
1007
1008impl BwCtx<'_> {
1009 fn pilot_residuals(
1011 &self,
1012 e: &Side,
1013 r: &Array2<f64>,
1014 inv_g: &Array2<f64>,
1015 rw: &Array2<f64>,
1016 beta: &Array2<f64>,
1017 d: usize,
1018 ) -> Array2<f64> {
1019 match self.vce {
1020 Vce::Nn => residuals(e, Vce::Nn, None, None, self.opts.nnmatch, d),
1021 v => {
1022 let fitted = r.dot(beta);
1023 let hii = matches!(v, Vce::Hc2 | Vce::Hc3).then(|| leverages(r, inv_g, rw));
1024 residuals(e, v, Some(&fitted), hii.as_ref(), self.opts.nnmatch, d)
1025 }
1026 }
1027 }
1028
1029 fn vfit(&self, side: &Side, o: usize, nu: usize, h_v: f64) -> Option<(f64, f64, Vec<f64>)> {
1031 let (c, kernel) = (self.c, self.opts.kernel);
1032 let keep: Vec<bool> = side
1033 .x
1034 .iter()
1035 .map(|&x| kernel.weight(x, c, h_v) > 0.0)
1036 .collect();
1037 let e = side.select(&keep);
1038 if n_unique(&e.x) < o + 1 {
1039 return None;
1040 }
1041 let ew: Vec<f64> = e.x.iter().map(|&x| kernel.weight(x, c, h_v)).collect();
1042 let r = vander(&e.x.iter().map(|x| x - c).collect::<Vec<_>>(), o);
1043 let rws = scale_rows(&r, &ew.iter().map(|w| w.sqrt()).collect::<Vec<_>>());
1044 let inv_g = spd_inverse(&crossprod(&rws, &rws)).ok()?;
1045 let rw = scale_rows(&r, &ew);
1046 let beta = inv_g.dot(&crossprod(&rw, &e.d()));
1047 let fact = factorial(nu);
1048 let dt = e.d_t();
1049 let s = match (e.z.is_some(), e.t.is_some()) {
1050 (false, false) => vec![1.0],
1051 (false, true) => {
1052 let (tau_y, tau_t) = (fact * beta[[nu, 0]], fact * beta[[nu, 1]]);
1053 vec![1.0 / tau_t, -(tau_y / (tau_t * tau_t))]
1054 }
1055 (true, has_t) => {
1056 let gamma = covariate_gamma(&rw, &inv_g, &e, &ew);
1057 let dz = e.d_z();
1058 let s_y: Vec<f64> = std::iter::once(1.0)
1059 .chain(gamma.column(0).iter().map(|g| -g))
1060 .collect();
1061 if has_t {
1062 let s_t: Vec<f64> = std::iter::once(1.0)
1063 .chain(gamma.column(1).iter().map(|g| -g))
1064 .collect();
1065 let pick = |col: usize| -> Vec<f64> {
1066 std::iter::once(beta[[nu, col]])
1067 .chain((0..dz).map(|j| beta[[nu, 1 + dt + j]]))
1068 .collect()
1069 };
1070 let dot =
1071 |a: &[f64], b: &[f64]| a.iter().zip(b).map(|(x, y)| x * y).sum::<f64>();
1072 let tau_y = fact * dot(&s_y, &pick(0));
1073 let tau_t = fact * dot(&s_t, &pick(1));
1074 let mut s = vec![1.0 / tau_t, -(tau_y / (tau_t * tau_t))];
1075 s.extend((0..dz).map(|j| {
1076 -(1.0 / tau_t) * gamma[[j, 0]] + (tau_y / (tau_t * tau_t)) * gamma[[j, 1]]
1077 }));
1078 s
1079 } else {
1080 s_y
1081 }
1082 }
1083 };
1084 let res = self.pilot_residuals(&e, &r, &inv_g, &rw, &beta, o + 1);
1085 let crv = CrvParts::new(self.vce, &inv_g, &rws, true);
1086 let meat = vce_meat(&s, &rw, &res, e.cl.as_deref(), rw.ncols(), crv.as_ref());
1087 let v_v = inv_g.dot(&meat).dot(&inv_g)[[nu, nu]];
1088 let pw: Array1<f64> = e.x.iter().map(|x| powk((x - c) / h_v, o + 1)).collect();
1089 let ig_v = inv_g.dot(&rw.t().dot(&pw));
1090 let b_const = powk(h_v, nu) * ig_v[nu];
1092 Some((v_v, b_const, s))
1093 }
1094
1095 #[allow(clippy::too_many_arguments)]
1097 fn bw(
1098 &self,
1099 side: &Side,
1100 o: usize,
1101 nu: usize,
1102 o_b: usize,
1103 h_v: f64,
1104 h_b: f64,
1105 scale: f64,
1106 ) -> BwParts {
1107 let (c, kernel) = (self.c, self.opts.kernel);
1108 let nan = (f64::NAN, f64::NAN, f64::NAN, 1.0 / (2 * o + 3) as f64);
1109 let Some((v_v, b_const, s)) = self.vfit(side, o, nu, h_v) else {
1110 return nan;
1111 };
1112 if !v_v.is_finite() || !(h_b.is_finite() && h_b > 0.0) {
1113 return nan;
1114 }
1115 let keep: Vec<bool> = side
1116 .x
1117 .iter()
1118 .map(|&x| kernel.weight(x, c, h_b) > 0.0)
1119 .collect();
1120 let e = side.select(&keep);
1121 if n_unique(&e.x) < o_b + 1 {
1122 return nan;
1123 }
1124 let ew: Vec<f64> = e.x.iter().map(|&x| kernel.weight(x, c, h_b)).collect();
1125 let r = vander(&e.x.iter().map(|x| x - c).collect::<Vec<_>>(), o_b);
1126 let rws = scale_rows(&r, &ew.iter().map(|w| w.sqrt()).collect::<Vec<_>>());
1127 let Ok(inv_g) = spd_inverse(&crossprod(&rws, &rws)) else {
1128 return nan;
1129 };
1130 let rw = scale_rows(&r, &ew);
1131 let beta = inv_g.dot(&crossprod(&rw, &e.d()));
1132 let mut bw_reg = 0.0;
1133 if scale > 0.0 {
1134 let res = self.pilot_residuals(&e, &r, &inv_g, &rw, &beta, o_b + 1);
1135 let crv = CrvParts::new(self.vce, &inv_g, &rws, true);
1136 let meat = vce_meat(&s, &rw, &res, e.cl.as_deref(), rw.ncols(), crv.as_ref());
1137 let v_b = inv_g.dot(&meat).dot(&inv_g)[[o_b, o_b]];
1138 bw_reg = 3.0 * b_const * b_const * v_b;
1139 }
1140 let last = beta.row(o_b);
1141 let s_beta: f64 = s.iter().zip(last.iter()).map(|(a, b)| a * b).sum();
1142 let b = (2.0 * (o + 1 - nu) as f64).sqrt() * b_const * s_beta;
1143 let v = (2 * nu + 1) as f64 * h_v.powf((2 * nu + 1) as f64) * v_v;
1144 let r = scale * (2 * (o + 1 - nu)) as f64 * bw_reg;
1145 (v, b, r, 1.0 / (2 * o + 3) as f64)
1146 }
1147}
1148
1149fn select_bandwidths(left: &Side, right: &Side, c: f64, opts: &RdOptions) -> Result<RdBandwidths> {
1151 let (p, q) = (opts.p, opts.q());
1152 let x_all: Vec<f64> = left.x.iter().chain(&right.x).copied().collect();
1153 let y_all: Vec<f64> = left.y.iter().chain(&right.y).copied().collect();
1154 let n = x_all.len();
1155 let x_iq = quantile_type2(&x_all, 0.75) - quantile_type2(&x_all, 0.25);
1156 let (x_sd, y_sd, bwp) = if opts.stdvars {
1157 let x_sd = std_ddof1(&x_all);
1158 (x_sd, std_ddof1(&y_all), 1.0_f64.min((x_iq / x_sd) / 1.349))
1159 } else {
1160 (1.0, 1.0, std_ddof1(&x_all).min(x_iq / 1.349))
1161 };
1162 let no_variation = |t: &Option<Vec<f64>>| t.as_ref().is_some_and(|t| var(t) == 0.0);
1164 let sharp_bw = opts.sharpbw || no_variation(&left.t) || no_variation(&right.t);
1165 let standardise = |s: &Side| -> Side {
1166 Side::new(
1167 s.x.iter().map(|x| x / x_sd).collect(),
1168 s.y.iter().map(|y| y / y_sd).collect(),
1169 if sharp_bw { None } else { s.t.clone() },
1170 s.z.clone(),
1171 s.cl.clone(),
1172 )
1173 };
1174 let (l, r) = (standardise(left), standardise(right));
1175 let c = c / x_sd;
1176 let (x_min, x_max) = (l.x[0], r.x[r.n() - 1]);
1177 let range_l = (c - l.x[0]).abs();
1178 let range_r = (c - r.x[r.n() - 1]).abs();
1179
1180 let mut uniq_l = l.x.clone();
1181 uniq_l.dedup();
1182 uniq_l.reverse();
1183 let mut uniq_r = r.x.clone();
1184 uniq_r.dedup();
1185 let inspect = opts.masspoints != MassPoints::Off;
1186 let (m_l, m_r) = if inspect || opts.bwcheck.is_some() {
1187 (uniq_l.len(), uniq_r.len())
1188 } else {
1189 (l.n(), r.n())
1190 };
1191 let mut bwcheck = opts.bwcheck;
1192 if inspect {
1193 let mass = |m: usize, n: usize| 1.0 - m as f64 / n as f64;
1194 let detected = mass(m_l, l.n()) >= 0.2 || mass(m_r, r.n()) >= 0.2;
1195 if detected && bwcheck.is_none() && opts.masspoints == MassPoints::Adjust {
1196 bwcheck = Some(10);
1197 }
1198 }
1199 let m_total = if inspect { m_l + m_r } else { n };
1200
1201 let c_c = opts.kernel.c_c();
1202 let mut c_bw = c_c * bwp * (n as f64).powf(-1.0 / 5.0);
1203 if opts.masspoints == MassPoints::Adjust {
1204 c_bw = c_c * bwp * (m_total as f64).powf(-1.0 / 5.0);
1205 }
1206 let (bw_max_l, bw_max_r) = ((c - x_min).abs(), (c - x_max).abs());
1207 let bw_max = bw_max_l.max(bw_max_r);
1208 if opts.bwrestrict {
1209 c_bw = c_bw.min(bw_max);
1210 }
1211 let pad = 1.0 + f64::EPSILON.sqrt();
1212 let bw_min = bwcheck.map(|k| {
1213 (
1214 (uniq_l[k.min(m_l) - 1] - c).abs() * pad,
1215 (uniq_r[k.min(m_r) - 1] - c).abs() * pad,
1216 )
1217 });
1218 if let Some((lo_l, lo_r)) = bw_min {
1219 c_bw = c_bw.max(lo_l).max(lo_r);
1220 }
1221 let restrict = |v: f64, max: f64| if opts.bwrestrict { v.min(max) } else { v };
1222 let floor = |v: f64| match bw_min {
1223 Some((lo_l, lo_r)) => v.max(lo_l).max(lo_r),
1224 None => v,
1225 };
1226
1227 let ctx = BwCtx {
1228 opts,
1229 vce: effective_vce(opts.vce, left.cl.is_some()),
1230 c,
1231 };
1232 let reg = opts.scaleregul;
1233 let c_d_l = ctx.bw(&l, q + 1, q + 1, q + 2, c_bw, range_l * pad, 0.0);
1234 let c_d_r = ctx.bw(&r, q + 1, q + 1, q + 2, c_bw, range_r * pad, 0.0);
1235
1236 let (mut two, mut sum, mut rd) = (None, None, None);
1237 let sel = opts.bwselect;
1238 if sel.needs_two() {
1239 let side = |s: &Side, cd: BwParts, max: f64, lo: Option<f64>| -> (f64, f64) {
1240 let mut d_bw = restrict((cd.0 / (cd.1 * cd.1)).powf(cd.3), max);
1241 if let Some(lo) = lo {
1242 d_bw = d_bw.max(lo);
1243 }
1244 let cb = ctx.bw(s, q, p + 1, q + 1, c_bw, d_bw, reg);
1245 let b_bw = restrict((cb.0 / (cb.1 * cb.1 + reg * cb.2)).powf(cb.3), max);
1246 let ch = ctx.bw(s, p, 0, q, c_bw, b_bw, reg);
1247 let h_bw = restrict((ch.0 / (ch.1 * ch.1 + reg * ch.2)).powf(ch.3), max);
1248 (x_sd * h_bw, x_sd * b_bw)
1249 };
1250 let (h_l, b_l) = side(&l, c_d_l, bw_max_l, bw_min.map(|m| m.0));
1251 let (h_r, b_r) = side(&r, c_d_r, bw_max_r, bw_min.map(|m| m.1));
1252 two = Some(RdBandwidths {
1253 h: (h_l, h_r),
1254 b: (b_l, b_r),
1255 });
1256 }
1257 let common = |sign: f64| -> (f64, f64) {
1260 let d_bw = floor(restrict(
1261 ((c_d_l.0 + c_d_r.0) / (c_d_r.1 + sign * c_d_l.1).powi(2)).powf(c_d_l.3),
1262 bw_max,
1263 ));
1264 let cb_l = ctx.bw(&l, q, p + 1, q + 1, c_bw, d_bw, reg);
1265 let cb_r = ctx.bw(&r, q, p + 1, q + 1, c_bw, d_bw, reg);
1266 let b_bw = restrict(
1267 ((cb_l.0 + cb_r.0) / ((cb_r.1 + sign * cb_l.1).powi(2) + reg * (cb_r.2 + cb_l.2)))
1268 .powf(cb_l.3),
1269 bw_max,
1270 );
1271 let ch_l = ctx.bw(&l, p, 0, q, c_bw, b_bw, reg);
1272 let ch_r = ctx.bw(&r, p, 0, q, c_bw, b_bw, reg);
1273 let h_bw = restrict(
1274 ((ch_l.0 + ch_r.0) / ((ch_r.1 + sign * ch_l.1).powi(2) + reg * (ch_r.2 + ch_l.2)))
1275 .powf(ch_l.3),
1276 bw_max,
1277 );
1278 (x_sd * h_bw, x_sd * b_bw)
1279 };
1280 if sel.needs_sum() {
1281 sum = Some(common(1.0));
1282 }
1283 if sel.needs_rd() {
1284 rd = Some(common(-1.0));
1285 }
1286 let min_nan = |a: f64, b: f64| {
1288 if a.is_nan() || b.is_nan() {
1289 f64::NAN
1290 } else {
1291 a.min(b)
1292 }
1293 };
1294 let median3 = |a: f64, b: f64, c: f64| {
1295 if a.is_nan() || b.is_nan() || c.is_nan() {
1296 return f64::NAN;
1297 }
1298 let mut v = [a, b, c];
1299 v.sort_by(f64::total_cmp);
1300 v[1]
1301 };
1302 let mse = match sel {
1303 BwSelect::Mserd | BwSelect::Cerrd => {
1304 let (h, b) = rd.expect("computed");
1305 RdBandwidths::common(h, b)
1306 }
1307 BwSelect::Msetwo | BwSelect::Certwo => two.expect("computed"),
1308 BwSelect::Msesum | BwSelect::Cersum => {
1309 let (h, b) = sum.expect("computed");
1310 RdBandwidths::common(h, b)
1311 }
1312 BwSelect::Msecomb1 | BwSelect::Cercomb1 => {
1313 let ((h_d, b_d), (h_s, b_s)) = (rd.expect("computed"), sum.expect("computed"));
1314 RdBandwidths::common(min_nan(h_d, h_s), min_nan(b_d, b_s))
1315 }
1316 BwSelect::Msecomb2 | BwSelect::Cercomb2 => {
1317 let ((h_d, b_d), (h_s, b_s)) = (rd.expect("computed"), sum.expect("computed"));
1318 let t = two.expect("computed");
1319 RdBandwidths {
1320 h: (median3(h_d, h_s, t.h.0), median3(h_d, h_s, t.h.1)),
1321 b: (median3(b_d, b_s, t.b.0), median3(b_d, b_s, t.b.1)),
1322 }
1323 }
1324 };
1325 let bws = if sel.is_cer() {
1326 let base = match (left.cl.as_ref(), right.cl.as_ref()) {
1327 (Some(a), Some(b)) => (n_unique_i64(a) + n_unique_i64(b)) as f64,
1328 _ => n as f64,
1329 };
1330 let pf = p as f64;
1331 let cer_h = base.powf(-(pf / ((3.0 + pf) * (3.0 + 2.0 * pf))));
1332 RdBandwidths {
1333 h: (mse.h.0 * cer_h, mse.h.1 * cer_h),
1334 b: mse.b,
1335 }
1336 } else {
1337 mse
1338 };
1339 let all = [bws.h.0, bws.h.1, bws.b.0, bws.b.1];
1340 if !all.iter().all(|v| v.is_finite() && *v > 0.0) {
1341 return Err(DmlError::InvalidArgument(
1342 "not enough variability in the running variable to compute the bandwidth".into(),
1343 ));
1344 }
1345 Ok(match opts.rho {
1346 Some(rho) => RdBandwidths {
1347 h: bws.h,
1348 b: (bws.h.0 / rho, bws.h.1 / rho),
1349 },
1350 None => bws,
1351 })
1352}
1353
1354fn n_unique_i64(v: &[i64]) -> usize {
1355 let mut v = v.to_vec();
1356 v.sort_unstable();
1357 v.dedup();
1358 v.len()
1359}
1360
1361fn effective_vce(vce: Vce, clustered: bool) -> Vce {
1364 match (vce, clustered) {
1365 (Vce::Nn | Vce::Hc0 | Vce::Hc1 | Vce::Cr1, true) => Vce::Cr1,
1366 (Vce::Hc2 | Vce::Cr2, true) => Vce::Cr2,
1367 (Vce::Hc3 | Vce::Cr3, true) => Vce::Cr3,
1368 (Vce::Cr1, false) => Vce::Hc1,
1369 (Vce::Cr2, false) => Vce::Hc2,
1370 (Vce::Cr3, false) => Vce::Hc3,
1371 (v, false) => v,
1372 }
1373}
1374
1375fn check_support(left: &Side, right: &Side, order: usize) -> Result<()> {
1376 if n_unique(&left.x).min(n_unique(&right.x)) < order + 1 {
1377 return Err(DmlError::InvalidArgument(format!(
1378 "not enough distinct running-variable values on each side of the cutoff for a polynomial of order {order}"
1379 )));
1380 }
1381 Ok(())
1382}
1383
1384pub fn rd_bandwidths(data: &RdData<'_>, opts: &RdOptions) -> Result<RdBandwidths> {
1387 opts.validate()?;
1388 let (left, right) = prepare(data, opts)?;
1389 check_support(&left, &right, opts.p)?;
1390 select_bandwidths(&left, &right, data.cutoff, opts)
1391}
1392
1393pub fn rdbwselect<'a>(
1397 y: ArrayView1<'a, f64>,
1398 x: ArrayView1<'a, f64>,
1399 fuzzy: Option<ArrayView1<'a, f64>>,
1400 c: f64,
1401) -> Result<(f64, f64)> {
1402 let mut data = RdData::new(y, x).cutoff(c);
1403 data.fuzzy = fuzzy;
1404 let bws = rd_bandwidths(&data, &RdOptions::default())?;
1405 Ok((bws.h.0, bws.b.0))
1406}
1407
1408struct SideFit {
1415 beta_p: Array2<f64>,
1416 beta_bc: Array2<f64>,
1417 inv_g_p: Array2<f64>,
1418 inv_g_q: Array2<f64>,
1419 rw_p: Array2<f64>,
1420 rw_q_h: Array2<f64>,
1421 q_q: Array2<f64>,
1422 res_h: Array2<f64>,
1423 res_b: Array2<f64>,
1424 zwd: Option<Array2<f64>>,
1426 u_p: Option<Array2<f64>>,
1427 cl: Option<Vec<i64>>,
1428 n_h: usize,
1429 sqrt_rx_p: Array2<f64>,
1432 sqrt_rx_q_h: Array2<f64>,
1433 r_q: Array2<f64>,
1434 w_b: Vec<f64>,
1435 g_q: Array2<f64>,
1436}
1437
1438fn fit_side(side: &Side, c: f64, h: f64, b: f64, opts: &RdOptions, vce: Vce) -> Result<SideFit> {
1439 let (p, q, kernel) = (opts.p, opts.q(), opts.kernel);
1440 let w_h: Vec<f64> = side.x.iter().map(|&x| kernel.weight(x, c, h)).collect();
1441 let w_b: Vec<f64> = side.x.iter().map(|&x| kernel.weight(x, c, b)).collect();
1442 let in_h: Vec<bool> = w_h.iter().map(|&w| w > 0.0).collect();
1443 let in_b: Vec<bool> = w_b.iter().map(|&w| w > 0.0).collect();
1444 let n_h = in_h.iter().filter(|&&k| k).count();
1445 let uniq_in = |keep: &[bool]| {
1446 n_unique(
1447 &side
1448 .x
1449 .iter()
1450 .zip(keep)
1451 .filter(|(_, &k)| k)
1452 .map(|(x, _)| *x)
1453 .collect::<Vec<_>>(),
1454 )
1455 };
1456 if uniq_in(&in_h) < p + 1 || uniq_in(&in_b) < q + 1 {
1457 return Err(DmlError::InvalidArgument(
1458 "not enough distinct running-variable values with positive weight on each side of the cutoff"
1459 .into(),
1460 ));
1461 }
1462 let keep = if h > b { &in_h } else { &in_b };
1463 let pick = |v: &[f64]| -> Vec<f64> {
1464 v.iter()
1465 .zip(keep.iter())
1466 .filter(|(_, &k)| k)
1467 .map(|(a, _)| *a)
1468 .collect()
1469 };
1470 let (ww_h, ww_b) = (pick(&w_h), pick(&w_b));
1471 let e = side.select(keep);
1473 let u: Vec<f64> = e.x.iter().map(|x| (x - c) / h).collect();
1474 let r_q = vander(&e.x.iter().map(|x| x - c).collect::<Vec<_>>(), q);
1475 let r_p = r_q.slice(s![.., ..=p]).to_owned();
1476 let rw_p = scale_rows(&r_p, &ww_h);
1477 let rw_q_b = scale_rows(&r_q, &ww_b);
1478 let l: Array1<f64> = rw_p
1479 .t()
1480 .dot(&Array1::from_iter(u.iter().map(|&v| powk(v, p + 1))));
1481 let sq = |w: &[f64]| w.iter().map(|v| v.sqrt()).collect::<Vec<_>>();
1482 let g_q = {
1483 let m = scale_rows(&r_q, &sq(&ww_b));
1484 crossprod(&m, &m)
1485 };
1486 let inv_g_q = spd_inverse(&g_q)?;
1487 let inv_g_p = {
1488 let m = scale_rows(&r_p, &sq(&ww_h));
1489 spd_inverse(&crossprod(&m, &m))?
1490 };
1491 let m: Vec<f64> = r_q
1492 .dot(&inv_g_q.row(p + 1))
1493 .iter()
1494 .zip(&ww_b)
1495 .map(|(a, w)| a * w)
1496 .collect();
1497 let hp = powk(h, p + 1);
1498 let mut q_q = rw_p.clone();
1499 for i in 0..q_q.nrows() {
1500 for j in 0..q_q.ncols() {
1501 q_q[[i, j]] -= hp * m[i] * l[j];
1502 }
1503 }
1504 let d = e.d();
1505 let beta_p = inv_g_p.dot(&crossprod(&rw_p, &d));
1506 let beta_q = inv_g_q.dot(&crossprod(&rw_q_b, &d));
1507 let beta_bc = inv_g_p.dot(&crossprod(&q_q, &d));
1508 let (res_h, res_b) = if vce == Vce::Nn {
1509 let res = residuals(&e, Vce::Nn, None, None, opts.nnmatch, p + 1);
1510 (res.clone(), res)
1511 } else {
1512 let (fit_p, fit_q) = (r_p.dot(&beta_p), r_q.dot(&beta_q));
1513 let (hii_p, hii_q) = if matches!(vce, Vce::Hc2 | Vce::Hc3) {
1514 (
1515 Some(leverages(&r_p, &inv_g_p, &rw_p)),
1516 Some(leverages(&r_q, &inv_g_q, &rw_q_b)),
1517 )
1518 } else {
1519 (None, None)
1520 };
1521 (
1522 residuals(&e, vce, Some(&fit_p), hii_p.as_ref(), opts.nnmatch, p + 1),
1523 residuals(&e, vce, Some(&fit_q), hii_q.as_ref(), opts.nnmatch, q + 1),
1524 )
1525 };
1526 let (zwd, u_p) = match &e.z {
1527 Some(z) => (
1528 Some(crossprod(&scale_rows(z, &ww_h), &d)),
1529 Some(crossprod(&rw_p, &d)),
1530 ),
1531 None => (None, None),
1532 };
1533 Ok(SideFit {
1534 sqrt_rx_p: scale_rows(&r_p, &sq(&ww_h)),
1535 sqrt_rx_q_h: scale_rows(&r_q, &sq(&ww_h)),
1536 g_q,
1537 w_b: ww_b,
1538 rw_q_h: scale_rows(&r_q, &ww_h),
1539 beta_p,
1540 beta_bc,
1541 inv_g_p,
1542 inv_g_q,
1543 rw_p,
1544 q_q,
1545 res_h,
1546 res_b,
1547 zwd,
1548 u_p,
1549 cl: e.cl,
1550 n_h,
1551 r_q,
1552 })
1553}
1554
1555pub fn rd_estimate(
1561 data: &RdData<'_>,
1562 opts: &RdOptions,
1563 bandwidths: Option<RdBandwidths>,
1564) -> Result<RdEstimate> {
1565 opts.validate()?;
1566 let q = opts.q();
1567 let c = data.cutoff;
1568 let (left, right) = prepare(data, opts)?;
1569 check_support(&left, &right, q)?;
1570 if let (Some(lt), Some(rt)) = (&left.t, &right.t) {
1571 if var(lt) == 0.0
1572 && var(rt) == 0.0
1573 && (np_mean(lt) - np_mean(rt)).abs() < f64::EPSILON.sqrt()
1574 {
1575 return Err(DmlError::InvalidArgument(
1576 "fuzzy RD: the treatment has no variation and no jump at the cutoff".into(),
1577 ));
1578 }
1579 }
1580 let vce = effective_vce(opts.vce, data.cluster.is_some());
1581 let n = left.n() + right.n();
1582 let bws = match bandwidths {
1583 Some(bws) => {
1584 let all = [bws.h.0, bws.h.1, bws.b.0, bws.b.1];
1585 if !all.iter().all(|v| v.is_finite() && *v > 0.0) {
1586 return Err(DmlError::InvalidArgument(
1587 "bandwidths must be positive and finite".into(),
1588 ));
1589 }
1590 match opts.rho {
1591 Some(rho) => RdBandwidths {
1592 h: bws.h,
1593 b: (bws.h.0 / rho, bws.h.1 / rho),
1594 },
1595 None => bws,
1596 }
1597 }
1598 None if n < 20 => {
1599 let h = (c - left.x[0])
1600 .abs()
1601 .max((right.x[right.n() - 1] - c).abs());
1602 RdBandwidths::common(h, opts.rho.map_or(h, |rho| h / rho))
1603 }
1604 None => select_bandwidths(&left, &right, c, opts)?,
1605 };
1606 let fl = fit_side(&left, c, bws.h.0, bws.b.0, opts, vce)?;
1607 let fr = fit_side(&right, c, bws.h.1, bws.b.1, opts, vce)?;
1608
1609 let fuzzy = data.fuzzy.is_some();
1610 let dt = usize::from(fuzzy);
1611 let dz = left.d_z();
1612 let sp = opts.scalepar;
1613 let beta_p = &fr.beta_p - &fl.beta_p;
1614 let beta_bc = &fr.beta_bc - &fl.beta_bc;
1615 let (tau_cl, tau_bc, s_y) = if dz == 0 {
1616 let (tau_y_cl, tau_y_bc) = (sp * beta_p[[0, 0]], sp * beta_bc[[0, 0]]);
1617 if fuzzy {
1618 let (tau_t_cl, tau_t_bc) = (beta_p[[0, 1]], beta_bc[[0, 1]]);
1619 let tau_cl = tau_y_cl / tau_t_cl;
1620 let s = vec![1.0 / tau_t_cl, -(tau_y_cl / (tau_t_cl * tau_t_cl))];
1621 let tau_bc = tau_cl - (s[0] * (tau_y_cl - tau_y_bc) + s[1] * (tau_t_cl - tau_t_bc));
1622 (tau_cl, tau_bc, s)
1623 } else {
1624 (tau_y_cl, tau_y_bc, vec![1.0])
1625 }
1626 } else {
1627 let cols_z = s![.., 1 + dt..1 + dt + dz];
1630 let side_terms = |f: &SideFit| -> (Array2<f64>, Array2<f64>) {
1631 let (zwd, u) = (f.zwd.as_ref().expect("covs"), f.u_p.as_ref().expect("covs"));
1632 let uigu = u.slice(cols_z).t().dot(&f.inv_g_p.dot(u));
1633 (
1634 &zwd.slice(cols_z) - &uigu.slice(cols_z),
1635 &zwd.slice(s![.., ..1 + dt]) - &uigu.slice(s![.., ..1 + dt]),
1636 )
1637 };
1638 let ((zwz_l, zwy_l), (zwz_r, zwy_r)) = (side_terms(&fl), side_terms(&fr));
1639 let gamma = sym_pinv(&(&zwz_r + &zwz_l)).dot(&(&zwy_r + &zwy_l));
1640 let s_y: Vec<f64> = std::iter::once(1.0)
1641 .chain(gamma.column(0).iter().map(|g| -g))
1642 .collect();
1643 let dot = |a: &[f64], b: &[f64]| a.iter().zip(b).map(|(x, y)| x * y).sum::<f64>();
1644 let row = |beta: &Array2<f64>, col: usize| -> Vec<f64> {
1645 std::iter::once(beta[[0, col]])
1646 .chain((0..dz).map(|j| beta[[0, 1 + dt + j]]))
1647 .collect()
1648 };
1649 if fuzzy {
1650 let s_t: Vec<f64> = std::iter::once(1.0)
1651 .chain(gamma.column(1).iter().map(|g| -g))
1652 .collect();
1653 let tau_y_cl = sp * dot(&s_y, &row(&beta_p, 0));
1654 let tau_t_cl = dot(&s_t, &row(&beta_p, 1));
1655 let tau_y_bc = sp * dot(&s_y, &row(&beta_bc, 0));
1656 let tau_t_bc = dot(&s_t, &row(&beta_bc, 1));
1657 let tau_cl = tau_y_cl / tau_t_cl;
1658 let s2 = [1.0 / tau_t_cl, -(tau_y_cl / (tau_t_cl * tau_t_cl))];
1659 let tau_bc = tau_cl - (s2[0] * (tau_y_cl - tau_y_bc) + s2[1] * (tau_t_cl - tau_t_bc));
1660 let mut s = s2.to_vec();
1661 s.extend((0..dz).map(|j| {
1662 -(1.0 / tau_t_cl) * gamma[[j, 0]]
1663 + (tau_y_cl / (tau_t_cl * tau_t_cl)) * gamma[[j, 1]]
1664 }));
1665 (tau_cl, tau_bc, s)
1666 } else {
1667 let tau_cl = sp * dot(&s_y, &beta_p.row(0).to_vec());
1668 let tau_bc = sp * dot(&s_y, &beta_bc.row(0).to_vec());
1669 (tau_cl, tau_bc, s_y)
1670 }
1671 };
1672
1673 let hb_match = bws.h == bws.b;
1674 let clustered = data.cluster.is_some();
1675 let variance = |f: &SideFit, robust: bool| -> f64 {
1676 let cl = f.cl.as_deref();
1677 if !robust {
1678 let k = f.rw_p.ncols();
1679 let crv = CrvParts::new(vce, &f.inv_g_p, &f.sqrt_rx_p, false);
1681 return f
1682 .inv_g_p
1683 .dot(&vce_meat(&s_y, &f.rw_p, &f.res_h, cl, k, crv.as_ref()))
1684 .dot(&f.inv_g_p)[[0, 0]];
1685 }
1686 if hb_match && clustered {
1687 let k = f.rw_q_h.ncols();
1689 let crv = Crv::of(vce).map(|kind| CrvParts {
1690 kind,
1691 inv_g: &f.inv_g_q,
1692 sqrt_rx: &f.sqrt_rx_q_h,
1693 g: (kind == Crv::Three).then(|| f.g_q.clone()),
1694 });
1695 f.inv_g_q
1696 .dot(&vce_meat(&s_y, &f.rw_q_h, &f.res_b, cl, k, crv.as_ref()))
1697 .dot(&f.inv_g_q)[[0, 0]]
1698 } else if let (Some(cl), Some(kind)) = (cl, Crv::of(vce)) {
1699 let meat = crv_meat_qq(
1700 &f.q_q, &f.r_q, &f.w_b, &f.inv_g_q, &f.g_q, &f.res_b, cl, kind, &s_y,
1701 );
1702 f.inv_g_p.dot(&meat).dot(&f.inv_g_p)[[0, 0]]
1703 } else {
1704 f.inv_g_p
1705 .dot(&vce_meat(&s_y, &f.q_q, &f.res_b, cl, q + 1, None))
1706 .dot(&f.inv_g_p)[[0, 0]]
1707 }
1708 };
1709 let se_cl = (sp * sp * (variance(&fl, false) + variance(&fr, false))).sqrt();
1710 let se_rb = (sp * sp * (variance(&fl, true) + variance(&fr, true))).sqrt();
1711 let coef = [tau_cl, tau_bc, tau_bc];
1712 let se = [se_cl, se_cl, se_rb];
1713 let quant = -norm_ppf((1.0 - opts.level) / 2.0);
1714 let ci = std::array::from_fn(|k| [coef[k] - quant * se[k], coef[k] + quant * se[k]]);
1715 Ok(RdEstimate {
1716 coef,
1717 se,
1718 ci,
1719 h: bws.h,
1720 b: bws.b,
1721 n_h: (fl.n_h, fr.n_h),
1722 n: (left.n(), right.n()),
1723 })
1724}
1725
1726pub fn rdrobust<'a>(
1732 y: ArrayView1<'a, f64>,
1733 x: ArrayView1<'a, f64>,
1734 fuzzy: Option<ArrayView1<'a, f64>>,
1735 c: f64,
1736 bandwidths: Option<(f64, f64)>,
1737) -> Result<RdEstimate> {
1738 let mut data = RdData::new(y, x).cutoff(c);
1739 data.fuzzy = fuzzy;
1740 rd_estimate(
1741 &data,
1742 &RdOptions::default(),
1743 bandwidths.map(|(h, b)| RdBandwidths::common(h, b)),
1744 )
1745}
1746
1747#[cfg(test)]
1748mod tests {
1749 use super::*;
1750 use ndarray::array;
1751
1752 #[test]
1753 fn duplicates_count_runs() {
1754 let (d, id) = duplicates(&[1.0, 2.0, 2.0, 2.0, 3.0]);
1755 assert_eq!(d, vec![1, 3, 3, 3, 1]);
1756 assert_eq!(id, vec![1, 1, 2, 3, 1]);
1757 }
1758
1759 #[test]
1760 fn quantile_type2_matches_r() {
1761 let x = [1.0, 2.0, 3.0, 4.0];
1762 assert_eq!(quantile_type2(&x, 0.25), 1.5);
1763 assert_eq!(quantile_type2(&x, 0.75), 3.5);
1764 assert_eq!(quantile_type2(&[1.0, 2.0, 3.0], 0.25), 1.0);
1765 }
1766
1767 #[test]
1768 fn collinear_covariates_are_dropped() {
1769 let z = array![
1770 [1.0, 2.0, 0.5],
1771 [2.0, 4.0, -1.0],
1772 [3.0, 6.0, 2.0],
1773 [4.0, 8.0, 0.0]
1774 ];
1775 let keep = independent_columns(&z);
1776 assert_eq!(keep.len(), 2);
1777 assert!(keep.contains(&2));
1778 let full = array![[1.0, 0.0], [0.0, 1.0], [1.0, 1.0]];
1779 assert_eq!(independent_columns(&full), vec![0, 1]);
1780 }
1781
1782 #[test]
1783 fn options_are_validated() {
1784 let bad = |o: RdOptions| o.validate().is_err();
1785 assert!(bad(RdOptions {
1786 q: Some(1),
1787 ..RdOptions::default()
1788 }));
1789 assert!(bad(RdOptions {
1790 level: 95.0,
1791 ..RdOptions::default()
1792 }));
1793 assert!(bad(RdOptions {
1794 nnmatch: 0,
1795 ..RdOptions::default()
1796 }));
1797 assert!(bad(RdOptions {
1798 rho: Some(0.0),
1799 ..RdOptions::default()
1800 }));
1801 assert!(RdOptions::default().validate().is_ok());
1802 assert_eq!(effective_vce(Vce::Hc2, true), Vce::Cr2);
1803 assert_eq!(effective_vce(Vce::Nn, true), Vce::Cr1);
1804 assert_eq!(effective_vce(Vce::Cr3, false), Vce::Hc3);
1805 }
1806}