1use ndarray::{s, Array1, Array2, Axis};
38use rand::seq::index::sample;
39use rand::{Rng, SeedableRng};
40use rand_chacha::ChaCha8Rng;
41use rand_distr::StandardNormal;
42
43use crate::compat::{brent_minimize, np_quantile};
44use crate::data::DmlData;
45use crate::error::{DmlError, Result};
46use crate::linalg::{cholesky_factor, sym_eigen};
47use crate::model::StaticPanel;
48use crate::staggered::{PanelData, RepeatedCrossSections};
49
50#[derive(Debug, Clone)]
58pub struct Simulated {
59 pub x: Array2<f64>,
60 pub y: Array1<f64>,
61 pub d: Array1<f64>,
62 pub z: Option<Array2<f64>>,
63 pub s: Option<Array1<f64>>,
64 pub t: Option<Array1<f64>>,
65 pub clusters: Option<Array2<i64>>,
66}
67
68impl Simulated {
69 fn new(x: Array2<f64>, y: Array1<f64>, d: Array1<f64>) -> Self {
70 Self {
71 x,
72 y,
73 d,
74 z: None,
75 s: None,
76 t: None,
77 clusters: None,
78 }
79 }
80
81 pub fn to_data(&self) -> Result<DmlData> {
84 let mut data = DmlData::new(self.y.clone(), self.d.clone(), self.x.clone())?;
85 if let Some(z) = &self.z {
86 data = data.with_instruments(z.clone())?;
87 }
88 if let Some(s) = &self.s {
89 data = data.with_selection_indicator(s.clone())?;
90 }
91 if let Some(t) = &self.t {
92 data = data.with_time_indicator(t.clone())?;
93 }
94 if let Some(c) = &self.clusters {
95 data = match c.ncols() {
96 1 => data.with_clusters(c.column(0).to_owned())?,
97 _ => data.with_two_way_clusters(c.column(0).to_owned(), c.column(1).to_owned())?,
98 };
99 }
100 Ok(data)
101 }
102}
103
104fn rng(seed: u64) -> ChaCha8Rng {
105 ChaCha8Rng::seed_from_u64(seed)
106}
107
108fn normal(rng: &mut ChaCha8Rng) -> f64 {
109 rng.sample(StandardNormal)
110}
111
112fn normals(rng: &mut ChaCha8Rng, n: usize) -> Array1<f64> {
113 (0..n).map(|_| normal(rng)).collect()
114}
115
116fn uniforms(rng: &mut ChaCha8Rng, n: usize, lo: f64, hi: f64) -> Array1<f64> {
117 (0..n).map(|_| rng.gen_range(lo..hi)).collect()
118}
119
120fn bernoulli(rng: &mut ChaCha8Rng, p: f64) -> f64 {
121 f64::from(rng.gen::<f64>() < p)
122}
123
124fn expit(v: f64) -> f64 {
125 1.0 / (1.0 + (-v).exp())
126}
127
128fn toeplitz(c: f64, p: usize) -> Array2<f64> {
130 Array2::from_shape_fn((p, p), |(i, j)| c.powi(i.abs_diff(j) as i32))
131}
132
133fn mvn(rng: &mut ChaCha8Rng, n: usize, cov: &Array2<f64>) -> Result<Array2<f64>> {
135 let l = cholesky_factor(cov)?;
136 let p = cov.nrows();
137 let e = Array2::from_shape_fn((n, p), |_| normal(rng));
138 Ok(e.dot(&l.t()))
139}
140
141fn correlated_pair(rng: &mut ChaCha8Rng, n: usize, rho: f64) -> (Array1<f64>, Array1<f64>) {
143 let a = normals(rng, n);
144 let b = normals(rng, n);
145 let second = &a * rho + &b * (1.0 - rho * rho).sqrt();
146 (a, second)
147}
148
149fn standardize(z: &Array2<f64>) -> Array2<f64> {
151 let mean = z.mean_axis(Axis(0)).expect("rows");
152 let sd = z.std_axis(Axis(0), 0.0);
153 (z - &mean) / &sd
154}
155
156fn sz_features(x: &Array2<f64>, extra_from: Option<usize>) -> Array2<f64> {
159 let n = x.nrows();
160 let extra = extra_from.map_or(0, |k| x.ncols().saturating_sub(k));
161 let mut z = Array2::zeros((n, 4 + extra));
162 for i in 0..n {
163 let (x0, x1, x2, x3) = (x[[i, 0]], x[[i, 1]], x[[i, 2]], x[[i, 3]]);
164 z[[i, 0]] = (0.5 * x0).exp();
165 z[[i, 1]] = 10.0 + x1 / (1.0 + x0.exp());
166 z[[i, 2]] = (0.6 + x0 * x2 / 25.0).powi(3);
167 z[[i, 3]] = (20.0 + x1 + x3).powi(2);
168 for j in 0..extra {
169 z[[i, 4 + j]] = x[[i, extra_from.expect("extra") + j]];
170 }
171 }
172 standardize(&z)
173}
174
175fn check(cond: bool, msg: &str) -> Result<()> {
176 if cond {
177 Ok(())
178 } else {
179 Err(DmlError::InvalidArgument(msg.into()))
180 }
181}
182
183fn mean(v: &Array1<f64>) -> f64 {
184 v.mean().expect("non-empty")
185}
186
187fn var(v: &Array1<f64>) -> f64 {
188 let m = mean(v);
189 v.mapv(|x| (x - m) * (x - m)).mean().expect("non-empty")
190}
191
192fn corr(a: &Array1<f64>, b: &Array1<f64>) -> f64 {
193 let (ma, mb) = (mean(a), mean(b));
194 let cov = (a - ma).dot(&(b - mb));
195 cov / ((a - ma).dot(&(a - ma)) * (b - mb).dot(&(b - mb))).sqrt()
196}
197
198#[derive(Debug, Clone, PartialEq)]
206pub struct PlrCcddhnr2018 {
207 pub n_obs: usize,
208 pub dim_x: usize,
209 pub alpha: f64,
210 pub a_0: f64,
211 pub a_1: f64,
212 pub s_1: f64,
213 pub b_0: f64,
214 pub b_1: f64,
215 pub s_2: f64,
216}
217
218impl Default for PlrCcddhnr2018 {
219 fn default() -> Self {
220 Self {
221 n_obs: 500,
222 dim_x: 20,
223 alpha: 0.5,
224 a_0: 1.0,
225 a_1: 0.25,
226 s_1: 1.0,
227 b_0: 1.0,
228 b_1: 0.25,
229 s_2: 1.0,
230 }
231 }
232}
233
234impl PlrCcddhnr2018 {
235 pub fn generate(&self, seed: u64) -> Result<Simulated> {
236 check(self.dim_x >= 3, "dim_x must be at least 3")?;
237 let mut rng = rng(seed);
238 let n = self.n_obs;
239 let x = mvn(&mut rng, n, &toeplitz(0.7, self.dim_x))?;
240 let d: Array1<f64> = (0..n)
241 .map(|i| {
242 self.a_0 * x[[i, 0]] + self.a_1 * expit(x[[i, 2]]) + self.s_1 * normal(&mut rng)
243 })
244 .collect();
245 let y: Array1<f64> = (0..n)
246 .map(|i| {
247 self.alpha * d[i]
248 + self.b_0 * expit(x[[i, 0]])
249 + self.b_1 * x[[i, 2]]
250 + self.s_2 * normal(&mut rng)
251 })
252 .collect();
253 Ok(Simulated::new(x, y, d))
254 }
255}
256
257#[derive(Debug, Clone, PartialEq)]
262pub struct PlrTurrell2018 {
263 pub n_obs: usize,
264 pub dim_x: usize,
265 pub theta: f64,
266 pub nu: f64,
267 pub gamma: f64,
268}
269
270impl Default for PlrTurrell2018 {
271 fn default() -> Self {
272 Self {
273 n_obs: 100,
274 dim_x: 20,
275 theta: 0.5,
276 nu: 0.0,
277 gamma: 1.0,
278 }
279 }
280}
281
282fn spd_matrix(rng: &mut ChaCha8Rng, p: usize) -> Array2<f64> {
285 let a = Array2::from_shape_fn((p, p), |_| rng.gen::<f64>());
286 let (_, v) = sym_eigen(&a.t().dot(&a));
287 let mut middle = Array2::<f64>::ones((p, p));
288 for i in 0..p {
289 middle[[i, i]] += rng.gen::<f64>();
290 }
291 let out = v.dot(&middle).dot(&v.t());
292 (&out + &out.t()) / 2.0
294}
295
296impl PlrTurrell2018 {
297 pub fn generate(&self, seed: u64) -> Result<Simulated> {
298 check(self.dim_x >= 1, "dim_x must be at least 1")?;
299 let mut rng = rng(seed);
300 let n = self.n_obs;
301 let sigma = spd_matrix(&mut rng, self.dim_x);
302 let x = mvn(&mut rng, n, &sigma)?;
303 let b: Array1<f64> = (1..=self.dim_x).map(|k| 1.0 / k as f64).collect();
304 let xb = x.dot(&b);
305 let m = |v: f64| {
306 0.5 / std::f64::consts::PI * self.gamma.sinh()
307 / (self.gamma.cosh() - (v - self.nu).cos())
308 };
309 let d: Array1<f64> = xb.iter().map(|&v| m(v) + normal(&mut rng)).collect();
310 let y: Array1<f64> = (0..n)
311 .map(|i| self.theta * d[i] + xb[i].sin().powi(2) + normal(&mut rng))
312 .collect();
313 Ok(Simulated::new(x, y, d))
314 }
315}
316
317#[derive(Debug, Clone, PartialEq)]
322pub struct ConfoundedPlr {
323 pub n_obs: usize,
324 pub theta: f64,
325 pub cf_y: f64,
326 pub cf_d: f64,
327 pub c: f64,
329 pub dim_x: usize,
330}
331
332impl Default for ConfoundedPlr {
333 fn default() -> Self {
334 Self {
335 n_obs: 500,
336 theta: 5.0,
337 cf_y: 0.04,
338 cf_d: 0.04,
339 c: 0.0,
340 dim_x: 4,
341 }
342 }
343}
344
345#[derive(Debug, Clone)]
347pub struct ConfoundedPlrOracle {
348 pub g_long: Array1<f64>,
349 pub g_short: Array1<f64>,
350 pub m_long: Array1<f64>,
351 pub m_short: Array1<f64>,
352 pub theta: f64,
353 pub gamma_a: f64,
354 pub beta_a: f64,
355 pub a: Array1<f64>,
356 pub z: Array2<f64>,
357}
358
359#[derive(Debug, Clone)]
361pub struct ConfoundedPlrSimulated {
362 pub data: Simulated,
363 pub oracle: ConfoundedPlrOracle,
364}
365
366impl ConfoundedPlr {
367 pub fn generate(&self, seed: u64) -> Result<ConfoundedPlrSimulated> {
368 check(self.dim_x >= 4, "dim_x must be at least 4")?;
369 let mut rng = rng(seed);
370 let n = self.n_obs;
371 let x = mvn(&mut rng, n, &toeplitz(self.c, self.dim_x))?;
372 let z = sz_features(&x, Some(4));
373 let (var_eps_y, var_eps_d) = (5.0, 1.0);
374 let eps_y = normals(&mut rng, n) * f64::sqrt(var_eps_y);
375 let eps_d = normals(&mut rng, n) * f64::sqrt(var_eps_d);
376 let a = uniforms(&mut rng, n, -1.0, 1.0);
377 let var_a = 4.0 / 12.0;
378 let m_short: Array1<f64> = (0..n)
379 .map(|i| -z[[i, 0]] + 0.5 * z[[i, 1]] - 0.25 * z[[i, 2]] - 0.1 * z[[i, 3]])
380 .collect();
381 let cf_d = self.cf_d;
382 let mut f_m = |gamma_a: f64| {
383 let rr_long = &eps_d / var_eps_d;
384 let rr_short = (&a * gamma_a + &eps_d) / (gamma_a * gamma_a * var_a + var_eps_d);
385 let (l2, s2) = (
386 rr_long.mapv(|v| v * v).mean().unwrap(),
387 rr_short.mapv(|v| v * v).mean().unwrap(),
388 );
389 let c2 = (l2 - s2) / s2;
390 (c2 / (1.0 + c2) - cf_d).powi(2)
391 };
392 let gamma_a = brent_minimize(&mut f_m, 0.0, 1.0);
393 let m_long = &m_short + &(&a * gamma_a);
394 let d = &m_long + &eps_d;
395 let g_partial: Array1<f64> = (0..n)
396 .map(|i| 210.0 + 27.4 * z[[i, 0]] + 13.7 * (z[[i, 1]] + z[[i, 2]] + z[[i, 3]]))
397 .collect();
398 let var_d = var(&d);
399 let cf_y = self.cf_y;
400 let mut f_g = |beta_a: f64| {
401 let g_diff = (&a - &(&d * (gamma_a * var_a / var_d))) * beta_a;
402 let y_diff = &eps_y + &g_diff;
403 let ratio =
404 g_diff.mapv(|v| v * v).mean().unwrap() / y_diff.mapv(|v| v * v).mean().unwrap();
405 (ratio - cf_y).powi(2)
406 };
407 let beta_a = brent_minimize(&mut f_g, 0.0, 1.0);
408 let g_long = &d * self.theta + &g_partial + &(&a * beta_a);
409 let g_short = &d * (self.theta + gamma_a * beta_a * var_a / var_d) + &g_partial;
410 let y = &g_long + &eps_y;
411 Ok(ConfoundedPlrSimulated {
412 data: Simulated::new(x, y, d),
413 oracle: ConfoundedPlrOracle {
414 g_long,
415 g_short,
416 m_long,
417 m_short,
418 theta: self.theta,
419 gamma_a,
420 beta_a,
421 a,
422 z,
423 },
424 })
425 }
426}
427
428#[derive(Debug, Clone, PartialEq)]
431pub struct PlivChs2015 {
432 pub n_obs: usize,
433 pub alpha: f64,
434 pub dim_x: usize,
435 pub dim_z: usize,
436}
437
438impl Default for PlivChs2015 {
439 fn default() -> Self {
440 Self {
441 n_obs: 500,
442 alpha: 1.0,
443 dim_x: 200,
444 dim_z: 150,
445 }
446 }
447}
448
449impl PlivChs2015 {
450 pub fn generate(&self, seed: u64) -> Result<Simulated> {
451 check(
452 self.dim_z >= 1 && self.dim_x >= self.dim_z,
453 "need 1 <= dim_z <= dim_x",
454 )?;
455 let mut rng = rng(seed);
456 let n = self.n_obs;
457 let (epsilon, u) = correlated_pair(&mut rng, n, 0.6);
458 let x = mvn(&mut rng, n, &toeplitz(0.5, self.dim_x))?;
459 let xi = Array2::from_shape_fn((n, self.dim_z), |_| 0.5 * normal(&mut rng));
460 let beta: Array1<f64> = (1..=self.dim_x).map(|k| 1.0 / (k * k) as f64).collect();
461 let delta: Array1<f64> = (1..=self.dim_z).map(|k| 1.0 / (k * k) as f64).collect();
462 let z = &x.slice(s![.., ..self.dim_z]) + ξ
463 let xb = x.dot(&beta);
464 let d = &xb + &z.dot(&delta) + &u;
465 let y = &d * self.alpha + &xb + ε
466 let mut sim = Simulated::new(x, y, d);
467 sim.z = Some(z);
468 Ok(sim)
469 }
470}
471
472#[derive(Debug, Clone, PartialEq)]
475pub struct PlivMultiwayClusterCkms2021 {
476 pub n: usize,
478 pub m: usize,
480 pub dim_x: usize,
481 pub theta: f64,
482 pub pi_10: f64,
483 pub zeta_0: Vec<f64>,
485 pub pi_20: Vec<f64>,
486 pub xi_0: Vec<f64>,
487 pub omega_x: [f64; 2],
489 pub omega_epsilon: [f64; 2],
490 pub omega_v: [f64; 2],
491 pub omega_vv: [f64; 2],
492 pub s_x: f64,
493 pub s_epsilon_v: f64,
494}
495
496impl Default for PlivMultiwayClusterCkms2021 {
497 fn default() -> Self {
498 Self {
499 n: 25,
500 m: 25,
501 dim_x: 100,
502 theta: 1.0,
503 pi_10: 1.0,
504 zeta_0: Vec::new(),
505 pi_20: Vec::new(),
506 xi_0: Vec::new(),
507 omega_x: [0.25, 0.25],
508 omega_epsilon: [0.25, 0.25],
509 omega_v: [0.25, 0.25],
510 omega_vv: [0.25, 0.25],
511 s_x: 0.25,
512 s_epsilon_v: 0.25,
513 }
514 }
515}
516
517impl PlivMultiwayClusterCkms2021 {
518 pub fn generate(&self, seed: u64) -> Result<Simulated> {
519 let (nn, mm, p) = (self.n, self.m, self.dim_x);
520 check(
521 nn >= 1 && mm >= 1 && p >= 1,
522 "n, m and dim_x must be positive",
523 )?;
524 let default = |v: &Vec<f64>| -> Result<Array1<f64>> {
525 if v.is_empty() {
526 Ok((1..=p).map(|k| 0.5_f64.powi(k as i32)).collect())
527 } else if v.len() == p {
528 Ok(Array1::from(v.clone()))
529 } else {
530 Err(DmlError::InvalidArgument(
531 "coefficient vectors need dim_x entries".into(),
532 ))
533 }
534 };
535 let (zeta_0, pi_20, xi_0) = (
536 default(&self.zeta_0)?,
537 default(&self.pi_20)?,
538 default(&self.xi_0)?,
539 );
540 let mut rng = rng(seed);
541 let rows = nn * mm;
542 let ij = |r: usize| (r / mm, r % mm);
544 let alpha_vv = normals(&mut rng, rows);
545 let alpha_vv_i = normals(&mut rng, nn);
546 let alpha_vv_j = normals(&mut rng, mm);
547 let pair = |rng: &mut ChaCha8Rng, k: usize| correlated_pair(rng, k, self.s_epsilon_v);
548 let (alpha_eps, alpha_v) = pair(&mut rng, rows);
549 let (alpha_eps_i, alpha_v_i) = pair(&mut rng, nn);
550 let (alpha_eps_j, alpha_v_j) = pair(&mut rng, mm);
551 let cov = toeplitz(self.s_x, p);
552 let alpha_x = mvn(&mut rng, rows, &cov)?;
553 let alpha_x_i = mvn(&mut rng, nn, &cov)?;
554 let alpha_x_j = mvn(&mut rng, mm, &cov)?;
555 let mix =
556 |w: [f64; 2], own: f64, a: f64, b: f64| (1.0 - w[0] - w[1]) * own + w[0] * a + w[1] * b;
557 let x = Array2::from_shape_fn((rows, p), |(r, k)| {
558 let (i, j) = ij(r);
559 mix(
560 self.omega_x,
561 alpha_x[[r, k]],
562 alpha_x_i[[i, k]],
563 alpha_x_j[[j, k]],
564 )
565 });
566 let comp =
567 |w: [f64; 2], own: &Array1<f64>, a: &Array1<f64>, b: &Array1<f64>| -> Array1<f64> {
568 (0..rows)
569 .map(|r| {
570 let (i, j) = ij(r);
571 mix(w, own[r], a[i], b[j])
572 })
573 .collect()
574 };
575 let eps = comp(self.omega_epsilon, &alpha_eps, &alpha_eps_i, &alpha_eps_j);
576 let v = comp(self.omega_v, &alpha_v, &alpha_v_i, &alpha_v_j);
577 let vv = comp(self.omega_vv, &alpha_vv, &alpha_vv_i, &alpha_vv_j);
578 let z = x.dot(&xi_0) + &vv;
579 let d = &z * self.pi_10 + &x.dot(&pi_20) + &v;
580 let y = &d * self.theta + &x.dot(&zeta_0) + &eps;
581 let clusters = Array2::from_shape_fn((rows, 2), |(r, c)| {
582 let (i, j) = ij(r);
583 if c == 0 {
584 i as i64
585 } else {
586 j as i64
587 }
588 });
589 let mut sim = Simulated::new(x, y, d);
590 sim.z = Some(z.insert_axis(Axis(1)));
591 sim.clusters = Some(clusters);
592 Ok(sim)
593 }
594}
595
596#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
598pub enum LplrTreatment {
599 #[default]
601 Continuous,
602 Binary,
604 BinaryUnbalanced,
606}
607
608#[derive(Debug, Clone, PartialEq)]
612pub struct LplrLzz2020 {
613 pub n_obs: usize,
614 pub dim_x: usize,
615 pub alpha: f64,
616 pub balanced_r0: bool,
617 pub treatment: LplrTreatment,
618}
619
620impl Default for LplrLzz2020 {
621 fn default() -> Self {
622 Self {
623 n_obs: 500,
624 dim_x: 20,
625 alpha: 0.5,
626 balanced_r0: true,
627 treatment: LplrTreatment::Continuous,
628 }
629 }
630}
631
632impl LplrLzz2020 {
633 pub fn generate(&self, seed: u64) -> Result<(Simulated, Array1<f64>)> {
635 check(self.dim_x >= 13, "dim_x must be at least 13")?;
636 let mut rng = rng(seed);
637 let (n, p) = (self.n_obs, self.dim_x);
638 let sigma = Array2::from_shape_fn((p, p), |(i, j)| if i == j { 1.0 } else { 0.2 });
639 let x = mvn(&mut rng, n, &sigma)?.mapv(|v| v.clamp(-2.0, 2.0));
640 let ind = |v: f64| f64::from(v > 0.0);
641 let (big, step) = if self.balanced_r0 {
642 (1.0, 0.25)
643 } else {
644 (4.0, 1.5)
645 };
646 let r0 = |i: usize| {
647 let r = x.row(i);
648 0.1 * r[0] * r[1] * r[2] + 0.1 * r[3] * r[4] + 0.1 * r[5].powi(3)
649 - 0.5 * r[6].sin().powi(2)
650 + 0.5 * r[7].cos()
651 + big / (1.0 + r[8] * r[8])
652 - 1.0 / (1.0 + r[9].exp())
653 + step * ind(r[10])
654 - 0.25 * ind(r[12])
655 };
656 let a0: Array1<f64> = (0..n)
657 .map(|i| {
658 let r = x.row(i);
659 2.0 / (1.0 + r[0].exp()) - 2.0 / (1.0 + r[1].exp())
660 + r[2].sin()
661 + r[3].cos()
662 + 0.5 * ind(r[4])
663 - 0.5 * ind(r[5])
664 + 0.2 * r[6] * r[7]
665 - 0.2 * r[8] * r[9]
666 })
667 .collect();
668 let d = match self.treatment {
669 LplrTreatment::Continuous => a0,
670 LplrTreatment::Binary => {
671 let m = mean(&a0);
672 a0.mapv(|v| bernoulli(&mut rng, expit(v - m)))
673 }
674 LplrTreatment::BinaryUnbalanced => a0.mapv(|v| bernoulli(&mut rng, expit(v))),
675 };
676 let prob: Array1<f64> = (0..n).map(|i| expit(self.alpha * d[i] + r0(i))).collect();
677 let y = prob.mapv(|q| bernoulli(&mut rng, q));
678 Ok((Simulated::new(x, y, d), prob))
679 }
680}
681
682#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
684pub enum PlprDesign {
685 #[default]
687 Dgp1,
688 Dgp2,
690 Dgp3,
692}
693
694#[derive(Debug, Clone, PartialEq)]
697pub struct PlprCp2025 {
698 pub num_id: usize,
699 pub num_t: usize,
700 pub dim_x: usize,
701 pub theta: f64,
702 pub design: PlprDesign,
703}
704
705impl Default for PlprCp2025 {
706 fn default() -> Self {
707 Self {
708 num_id: 250,
709 num_t: 10,
710 dim_x: 30,
711 theta: 0.5,
712 design: PlprDesign::Dgp1,
713 }
714 }
715}
716
717#[derive(Debug, Clone)]
719pub struct SimulatedPanel {
720 pub id: Array1<i64>,
721 pub t: Array1<f64>,
722 pub y: Array1<f64>,
723 pub d: Array1<f64>,
724 pub x: Array2<f64>,
725}
726
727impl SimulatedPanel {
728 pub fn to_static_panel(&self) -> Result<StaticPanel> {
730 StaticPanel::new(
731 self.y.clone(),
732 self.d.clone(),
733 self.x.clone(),
734 self.id.clone(),
735 self.t.clone(),
736 )
737 }
738}
739
740impl PlprCp2025 {
741 pub fn generate(&self, seed: u64) -> Result<SimulatedPanel> {
742 check(self.dim_x >= 3, "dim_x must be at least 3")?;
743 check(
744 self.num_id >= 1 && self.num_t >= 1,
745 "num_id and num_t must be positive",
746 )?;
747 let mut rng = rng(seed);
748 let (a, b, sigma2_a, sigma2_x) = (0.25, 0.5, 0.95_f64, 5.0_f64);
749 let (ni, nt) = (self.num_id, self.num_t);
750 let rows = ni * nt;
751 let a_i: Vec<f64> = (0..ni)
752 .map(|_| sigma2_a.sqrt() * normal(&mut rng))
753 .collect();
754 let c_i: Vec<f64> = (0..ni).map(|_| normal(&mut rng)).collect();
755 let x = Array2::from_shape_fn((rows, self.dim_x), |_| sigma2_x.sqrt() * normal(&mut rng));
756 let u = normals(&mut rng, rows);
757 let v = normals(&mut rng, rows);
758 let logistic = |v: f64| v.exp() / (1.0 + v.exp());
759 let pos = |v: f64| f64::from(v > 0.0);
760 let (g0, m0): (Vec<f64>, Vec<f64>) = (0..rows)
761 .map(|r| {
762 let (x0, x2) = (x[[r, 0]], x[[r, 2]]);
763 match self.design {
764 PlprDesign::Dgp1 => (a * x0 + x2, a * x0 + x2),
765 PlprDesign::Dgp2 => (logistic(x0) + a * x2.cos(), x0.cos() + a * logistic(x2)),
766 PlprDesign::Dgp3 => (
767 b * (x0 * x2) + a * (x2 * pos(x2)),
768 a * (x0 * pos(x0)) + b * (x0 * x2),
769 ),
770 }
771 })
772 .unzip();
773 let d: Array1<f64> = (0..rows).map(|r| m0[r] + c_i[r / nt] + v[r]).collect();
774 let d_mean = mean(&d);
775 let unit_mean = |f: &dyn Fn(usize) -> f64, i: usize| {
776 (0..nt).map(|k| f(i * nt + k)).sum::<f64>() / nt as f64
777 };
778 let alpha: Vec<f64> = (0..ni)
779 .map(|i| {
780 let d_term = unit_mean(&|r| d[r], i) - d_mean;
781 let x_term = unit_mean(&|r| x[[r, 0]] + x[[r, 2]], i);
782 0.25 * d_term + 0.25 * x_term + a_i[i]
783 })
784 .collect();
785 let y: Array1<f64> = (0..rows)
786 .map(|r| d[r] * self.theta + g0[r] + alpha[r / nt] + u[r])
787 .collect();
788 Ok(SimulatedPanel {
789 id: (0..rows).map(|r| (r / nt + 1) as i64).collect(),
790 t: (0..rows).map(|r| (r % nt + 1) as f64).collect(),
791 y,
792 d,
793 x,
794 })
795 }
796}
797
798#[derive(Debug, Clone, PartialEq)]
806pub struct IrmData {
807 pub n_obs: usize,
808 pub dim_x: usize,
809 pub theta: f64,
810 pub r2_d: f64,
811 pub r2_y: f64,
812}
813
814impl Default for IrmData {
815 fn default() -> Self {
816 Self {
817 n_obs: 500,
818 dim_x: 20,
819 theta: 0.0,
820 r2_d: 0.5,
821 r2_y: 0.5,
822 }
823 }
824}
825
826impl IrmData {
827 pub fn generate(&self, seed: u64) -> Result<Simulated> {
828 check(self.dim_x >= 1, "dim_x must be positive")?;
829 check(
830 self.r2_d > 0.0 && self.r2_d < 1.0 && self.r2_y > 0.0 && self.r2_y < 1.0,
831 "R2_d and R2_y must lie in (0, 1)",
832 )?;
833 let mut rng = rng(seed);
834 let n = self.n_obs;
835 let v = uniforms(&mut rng, n, 0.0, 1.0);
836 let zeta = normals(&mut rng, n);
837 let cov = toeplitz(0.5, self.dim_x);
838 let x = mvn(&mut rng, n, &cov)?;
839 let beta: Array1<f64> = (1..=self.dim_x).map(|k| 1.0 / (k * k) as f64).collect();
840 let b_sigma_b = cov.dot(&beta).dot(&beta);
841 let c_y = (self.r2_y / ((1.0 - self.r2_y) * b_sigma_b)).sqrt();
842 let c_d = (std::f64::consts::PI.powi(2) / 3.0 * self.r2_d
843 / ((1.0 - self.r2_d) * b_sigma_b))
844 .sqrt();
845 let xb = x.dot(&beta);
846 let d: Array1<f64> = (0..n)
847 .map(|i| f64::from(expit(c_d * xb[i]) > v[i]))
848 .collect();
849 let y: Array1<f64> = (0..n)
850 .map(|i| d[i] * self.theta + d[i] * c_y * xb[i] + zeta[i])
851 .collect();
852 Ok(Simulated::new(x, y, d))
853 }
854}
855
856#[derive(Debug, Clone, PartialEq)]
860pub struct ConfoundedIrm {
861 pub n_obs: usize,
862 pub theta: f64,
863 pub gamma_a: f64,
864 pub beta_a: f64,
865 pub linear: bool,
866 pub dim_x: usize,
867 pub trimming_threshold: f64,
868 pub var_eps_y: f64,
869}
870
871impl Default for ConfoundedIrm {
872 fn default() -> Self {
873 Self {
874 n_obs: 500,
875 theta: 0.0,
876 gamma_a: 0.127,
877 beta_a: 0.58,
878 linear: false,
879 dim_x: 5,
880 trimming_threshold: 0.01,
881 var_eps_y: 1.0,
882 }
883 }
884}
885
886#[derive(Debug, Clone)]
888pub struct ConfoundedIrmOracle {
889 pub g_long: Array1<f64>,
890 pub g_short: Array1<f64>,
891 pub m_long: Array1<f64>,
892 pub m_short: Array1<f64>,
893 pub gamma_a: f64,
894 pub beta_a: f64,
895 pub a: Array1<f64>,
896 pub y_0: Array1<f64>,
897 pub y_1: Array1<f64>,
898 pub z: Array2<f64>,
899 pub cf_y: f64,
900 pub cf_d_ate: f64,
901 pub cf_d_atte: f64,
902 pub rho_ate: f64,
903 pub rho_atte: f64,
904}
905
906#[derive(Debug, Clone)]
908pub struct ConfoundedIrmSimulated {
909 pub data: Simulated,
910 pub oracle: ConfoundedIrmOracle,
911}
912
913impl ConfoundedIrm {
914 pub fn generate(&self, seed: u64) -> Result<ConfoundedIrmSimulated> {
915 check(self.dim_x >= 5, "dim_x must be at least 5")?;
916 let mut rng = rng(seed);
917 let n = self.n_obs;
918 let xi = 0.75;
919 let x = mvn(&mut rng, n, &toeplitz(0.0, self.dim_x))?;
920 let mut z = sz_features(&x, None);
922 z = ndarray::concatenate![Axis(1), z, standardize(&x.slice(s![.., 4..5]).to_owned())];
923 let eps_y = normals(&mut rng, n) * self.var_eps_y.sqrt();
924 let a = uniforms(&mut rng, n, -1.0, 1.0);
925 let var_a = 4.0 / 12.0;
926 let features = if self.linear { &x } else { &z };
927 let f_reg = |i: usize| {
928 let w = features.row(i);
929 2.5 + 0.74 * w[0] + 0.25 * w[1] + 0.137 * (w[2] + w[3])
930 };
931 let f_ps = |i: usize| {
932 let w = features.row(i);
933 xi * (-w[0] + 0.1 * w[1] - 0.25 * w[2] - 0.1 * w[3])
934 };
935 let p: Array1<f64> = (0..n).map(|i| expit(f_ps(i))).collect();
936 let mut m_long = &p + &(&a * self.gamma_a);
937 let mut m_short = p;
938 let t = self.trimming_threshold;
939 if m_long.iter().any(|&m| m < t || m > 1.0 - t) {
940 m_long.mapv_inplace(|m| m.clamp(t, 1.0 - t));
941 m_short.mapv_inplace(|m| m.clamp(t, 1.0 - t));
942 }
943 let u = uniforms(&mut rng, n, 0.0, 1.0);
944 let d: Array1<f64> = (0..n).map(|i| f64::from(m_long[i] >= u[i])).collect();
945 let d1x = z.column(4).mapv(|v| v + 1.0);
946 let var_dx = var(&(&d * &d1x));
947 let cov_adx = self.gamma_a * var_a;
948 let g_partial: Array1<f64> = (0..n).map(f_reg).collect();
949 let g_short_d1 = &d1x * (self.theta + self.beta_a * cov_adx / var_dx) + &g_partial;
950 let g_short = &d * &g_short_d1 + &(&d.mapv(|v| 1.0 - v) * &g_partial);
951 let g_long_d0 = &g_partial + &(&a * self.beta_a);
952 let g_long_d1 = &d1x * self.theta + &g_long_d0;
953 let g_long = &d * &g_long_d1 + &(&d.mapv(|v| 1.0 - v) * &g_long_d0);
954 let y_0 = &g_long_d0 + &eps_y;
955 let y_1 = &g_long_d1 + &eps_y;
956 let y = &d * &y_1 + &(&d.mapv(|v| 1.0 - v) * &y_0);
957 let cf_y = (&g_long - &g_short).mapv(|v| v * v).mean().unwrap()
958 / (&y - &g_short).mapv(|v| v * v).mean().unwrap();
959 let mean_d = mean(&d);
960 let rr = |m: &Array1<f64>| -> (Array1<f64>, Array1<f64>) {
961 let ate = (0..n)
962 .map(|i| d[i] / m[i] - (1.0 - d[i]) / (1.0 - m[i]))
963 .collect();
964 let atte = (0..n)
965 .map(|i| d[i] / mean_d - (1.0 - d[i]) / mean_d * (m[i] / (1.0 - m[i])))
966 .collect();
967 (ate, atte)
968 };
969 let ((ate_long, atte_long), (ate_short, atte_short)) = (rr(&m_long), rr(&m_short));
970 let inv_var = |m: &Array1<f64>| m.mapv(|v| 1.0 / (v * (1.0 - v))).mean().unwrap();
971 let odds = |m: &Array1<f64>| m.mapv(|v| v / (1.0 - v)).mean().unwrap();
972 let cf_d_ate = (inv_var(&m_long) - inv_var(&m_short)) / inv_var(&m_long);
973 let cf_d_atte = (odds(&m_long) - odds(&m_short)) / odds(&m_long);
974 let (rho_ate, rho_atte) = if self.beta_a == 0.0 || self.gamma_a == 0.0 {
975 (0.0, 0.0)
976 } else {
977 let g_diff = &g_long - &g_short;
978 (
979 corr(&g_diff, &(&ate_long - &ate_short)),
980 corr(&g_diff, &(&atte_long - &atte_short)),
981 )
982 };
983 Ok(ConfoundedIrmSimulated {
984 data: Simulated::new(x, y, d),
985 oracle: ConfoundedIrmOracle {
986 g_long,
987 g_short,
988 m_long,
989 m_short,
990 gamma_a: self.gamma_a,
991 beta_a: self.beta_a,
992 a,
993 y_0,
994 y_1,
995 z,
996 cf_y,
997 cf_d_ate,
998 cf_d_atte,
999 rho_ate,
1000 rho_atte,
1001 },
1002 })
1003 }
1004}
1005
1006#[derive(Debug, Clone, PartialEq)]
1010pub struct IrmDiscreteTreatments {
1011 pub n_obs: usize,
1012 pub n_levels: usize,
1013 pub linear: bool,
1014 pub xi: f64,
1015 pub c: f64,
1016 pub dim_x: usize,
1017}
1018
1019impl Default for IrmDiscreteTreatments {
1020 fn default() -> Self {
1021 Self {
1022 n_obs: 200,
1023 n_levels: 3,
1024 linear: false,
1025 xi: 0.3,
1026 c: 0.0,
1027 dim_x: 5,
1028 }
1029 }
1030}
1031
1032#[derive(Debug, Clone)]
1034pub struct DiscreteTreatmentOracle {
1035 pub cont_d: Array1<f64>,
1036 pub level_bounds: Array1<f64>,
1037 pub potential_level: Array1<f64>,
1038 pub ite: Array1<f64>,
1039 pub y0: Array1<f64>,
1040}
1041
1042#[derive(Debug, Clone)]
1044pub struct DiscreteTreatmentSimulated {
1045 pub data: Simulated,
1046 pub oracle: DiscreteTreatmentOracle,
1047}
1048
1049impl IrmDiscreteTreatments {
1050 pub fn generate(&self, seed: u64) -> Result<DiscreteTreatmentSimulated> {
1051 check(self.n_levels >= 2, "n_levels must be at least 2")?;
1052 check(self.dim_x >= 4, "dim_x must be at least 4")?;
1053 let mut rng = rng(seed);
1054 let n = self.n_obs;
1055 let x = mvn(&mut rng, n, &toeplitz(self.c, self.dim_x))?;
1056 let z = sz_features(&x, Some(4));
1057 let eps_y = normals(&mut rng, n) * 5.0_f64.sqrt();
1058 let eps_d = normals(&mut rng, n);
1059 let features = if self.linear { &x } else { &z };
1060 let g: Array1<f64> = (0..n)
1061 .map(|i| {
1062 let w = features.row(i);
1063 210.0 + 27.4 * w[0] + 13.7 * (w[1] + w[2] + w[3])
1064 })
1065 .collect();
1066 let cont_d: Array1<f64> = (0..n)
1067 .map(|i| {
1068 let w = features.row(i);
1069 self.xi * (-w[0] + 0.5 * w[1] - 0.25 * w[2] - 0.1 * w[3]) + eps_d[i]
1070 })
1071 .collect();
1072 let k = self.n_levels;
1073 let values = cont_d.to_vec();
1074 let level_bounds: Array1<f64> = (0..=k)
1075 .map(|j| np_quantile(&values, j as f64 / k as f64))
1076 .collect();
1077 let potential_level: Array1<f64> = cont_d.mapv(|c| {
1078 1.0 + level_bounds
1079 .slice(s![1..k])
1080 .iter()
1081 .filter(|&&b| c >= b)
1082 .count() as f64
1083 });
1084 let eta = uniforms(&mut rng, n, 0.0, 1.0);
1085 let d: Array1<f64> = (0..n)
1086 .map(|i| f64::from(eta[i] >= 1.0 / k as f64) * potential_level[i])
1087 .collect();
1088 let ite = cont_d.mapv(|c| 15.0 * expit(c + 1.2 * c.cos()) - 2.0);
1089 let y0 = &g + &eps_y;
1090 let y: Array1<f64> = (0..n)
1091 .map(|i| ite[i] * f64::from(d[i] > 0.0) + y0[i])
1092 .collect();
1093 Ok(DiscreteTreatmentSimulated {
1094 data: Simulated::new(x, y, d),
1095 oracle: DiscreteTreatmentOracle {
1096 cont_d,
1097 level_bounds,
1098 potential_level,
1099 ite,
1100 y0,
1101 },
1102 })
1103 }
1104}
1105
1106#[derive(Debug, Clone, PartialEq)]
1109pub struct IivmData {
1110 pub n_obs: usize,
1111 pub dim_x: usize,
1112 pub theta: f64,
1113 pub alpha_x: f64,
1114}
1115
1116impl Default for IivmData {
1117 fn default() -> Self {
1118 Self {
1119 n_obs: 500,
1120 dim_x: 20,
1121 theta: 1.0,
1122 alpha_x: 0.2,
1123 }
1124 }
1125}
1126
1127impl IivmData {
1128 pub fn generate(&self, seed: u64) -> Result<Simulated> {
1129 check(self.dim_x >= 1, "dim_x must be positive")?;
1130 let mut rng = rng(seed);
1131 let n = self.n_obs;
1132 let (u, v) = correlated_pair(&mut rng, n, 0.3);
1133 let x = mvn(&mut rng, n, &toeplitz(0.5, self.dim_x))?;
1134 let beta: Array1<f64> = (1..=self.dim_x).map(|k| 1.0 / (k * k) as f64).collect();
1135 let z: Array1<f64> = (0..n).map(|_| bernoulli(&mut rng, 0.5)).collect();
1136 let d: Array1<f64> = (0..n)
1137 .map(|i| f64::from(self.alpha_x * z[i] + v[i] > 0.0))
1138 .collect();
1139 let y = &d * self.theta + &x.dot(&beta) + &u;
1140 let mut sim = Simulated::new(x, y, d);
1141 sim.z = Some(z.insert_axis(Axis(1)));
1142 Ok(sim)
1143 }
1144}
1145
1146#[derive(Debug, Clone, PartialEq)]
1151pub struct SsmData {
1152 pub n_obs: usize,
1153 pub dim_x: usize,
1154 pub theta: f64,
1155 pub mar: bool,
1156}
1157
1158impl Default for SsmData {
1159 fn default() -> Self {
1160 Self {
1161 n_obs: 8000,
1162 dim_x: 100,
1163 theta: 1.0,
1164 mar: true,
1165 }
1166 }
1167}
1168
1169impl SsmData {
1170 pub fn generate(&self, seed: u64) -> Result<Simulated> {
1171 check(self.dim_x >= 1, "dim_x must be positive")?;
1172 let mut rng = rng(seed);
1173 let n = self.n_obs;
1174 let (rho, gamma) = if self.mar { (0.0, 0.0) } else { (0.8, 1.0) };
1175 let (e0, e1) = correlated_pair(&mut rng, n, rho);
1176 let x = mvn(&mut rng, n, &toeplitz(0.5, self.dim_x))?;
1177 let beta: Array1<f64> = (1..=self.dim_x).map(|k| 0.4 / (k * k) as f64).collect();
1178 let xb = x.dot(&beta);
1179 let d: Array1<f64> = (0..n)
1180 .map(|i| f64::from(xb[i] + normal(&mut rng) > 0.0))
1181 .collect();
1182 let z = normals(&mut rng, n);
1183 let s: Array1<f64> = (0..n)
1184 .map(|i| f64::from(xb[i] + d[i] + gamma * z[i] + e0[i] > 0.0))
1185 .collect();
1186 let y: Array1<f64> = (0..n)
1187 .map(|i| {
1188 if s[i] == 1.0 {
1189 xb[i] + self.theta * d[i] + e1[i]
1190 } else {
1191 0.0
1192 }
1193 })
1194 .collect();
1195 let mut sim = Simulated::new(x, y, d);
1196 sim.z = Some(z.insert_axis(Axis(1)));
1197 sim.s = Some(s);
1198 Ok(sim)
1199 }
1200}
1201
1202#[derive(Debug, Clone, PartialEq)]
1206pub struct HeterogeneousData {
1207 pub n_obs: usize,
1208 pub p: usize,
1210 pub support_size: usize,
1212 pub n_x: usize,
1214 pub binary_treatment: bool,
1215}
1216
1217impl Default for HeterogeneousData {
1218 fn default() -> Self {
1219 Self {
1220 n_obs: 200,
1221 p: 30,
1222 support_size: 5,
1223 n_x: 1,
1224 binary_treatment: false,
1225 }
1226 }
1227}
1228
1229#[derive(Debug, Clone)]
1231pub struct HeterogeneousSimulated {
1232 pub data: Simulated,
1233 pub effects: Array1<f64>,
1234}
1235
1236impl HeterogeneousData {
1237 pub fn treatment_effect(&self, x: ndarray::ArrayView2<'_, f64>) -> Array1<f64> {
1239 let k = if self.n_x == 2 { 1 } else { 0 };
1240 (0..x.nrows())
1241 .map(|i| (2.0 * x[[i, 0]]).exp() + 3.0 * (4.0 * x[[i, k]]).sin())
1242 .collect()
1243 }
1244
1245 pub fn generate(&self, seed: u64) -> Result<HeterogeneousSimulated> {
1246 check(self.n_x == 1 || self.n_x == 2, "n_x must be 1 or 2")?;
1247 check(
1248 self.support_size <= self.p && self.p >= self.n_x,
1249 "support_size must not exceed p",
1250 )?;
1251 let mut rng = rng(seed);
1252 let n = self.n_obs;
1253 let support: Vec<usize> = sample(&mut rng, self.p, self.support_size).into_vec();
1254 let coefs_y = uniforms(&mut rng, self.support_size, 0.0, 1.0);
1255 let coefs_d = uniforms(&mut rng, self.support_size, 0.0, 0.3);
1256 let epsilon = uniforms(&mut rng, n, -1.0, 1.0);
1257 let eta = uniforms(&mut rng, n, -1.0, 1.0);
1258 let x = Array2::from_shape_fn((n, self.p), |_| rng.gen::<f64>());
1259 let te = self.treatment_effect(x.view());
1260 let xs = x.select(Axis(1), &support);
1261 let index_d = xs.dot(&coefs_d);
1262 let d: Array1<f64> = if self.binary_treatment {
1263 (0..n).map(|i| f64::from(index_d[i] >= eta[i])).collect()
1264 } else {
1265 &index_d + &eta
1266 };
1267 let y = &te * &d + &xs.dot(&coefs_y) + ε
1268 Ok(HeterogeneousSimulated {
1269 data: Simulated::new(x, y, d),
1270 effects: te,
1271 })
1272 }
1273}
1274
1275fn did_features<'a>(
1284 dgp_type: u8,
1285 x: &'a Array2<f64>,
1286 z: &'a Array2<f64>,
1287) -> Result<(Option<&'a Array2<f64>>, &'a Array2<f64>)> {
1288 Ok(match dgp_type {
1289 1 => (Some(z), z),
1290 2 => (Some(x), z),
1291 3 => (Some(z), x),
1292 4 => (Some(x), x),
1293 5 => (None, z),
1294 6 => (None, x),
1295 _ => {
1296 return Err(DmlError::InvalidArgument(
1297 "dgp_type must be between 1 and 6".into(),
1298 ))
1299 }
1300 })
1301}
1302
1303#[derive(Debug, Clone, PartialEq)]
1307pub struct DidSz2020 {
1308 pub n_obs: usize,
1309 pub dgp_type: u8,
1310 pub xi: f64,
1311 pub c: f64,
1312 pub lambda_t: f64,
1314}
1315
1316impl Default for DidSz2020 {
1317 fn default() -> Self {
1318 Self {
1319 n_obs: 500,
1320 dgp_type: 1,
1321 xi: 0.75,
1322 c: 0.0,
1323 lambda_t: 0.5,
1324 }
1325 }
1326}
1327
1328type TwoPeriods = (Array2<f64>, Array1<f64>, Array1<f64>, Array1<f64>);
1330
1331impl DidSz2020 {
1332 fn draw(&self, rng: &mut ChaCha8Rng) -> Result<TwoPeriods> {
1334 let n = self.n_obs;
1335 let x = mvn(rng, n, &toeplitz(self.c, 4))?;
1336 let z = sz_features(&x, None);
1337 let eps_0 = normals(rng, n);
1338 let eps_1a = normals(rng, n);
1339 let eps_1b = normals(rng, n);
1340 let (ps, reg) = did_features(self.dgp_type, &x, &z)?;
1341 let f_reg = |i: usize| {
1342 let w = reg.row(i);
1343 210.0 + 27.4 * w[0] + 13.7 * (w[1] + w[2] + w[3])
1344 };
1345 let p: Array1<f64> = (0..n)
1346 .map(|i| match ps {
1347 None => 0.5,
1348 Some(w) => {
1349 let w = w.row(i);
1350 expit(self.xi * (-w[0] + 0.5 * w[1] - 0.25 * w[2] - 0.1 * w[3]))
1351 }
1352 })
1353 .collect();
1354 let u = uniforms(rng, n, 0.0, 1.0);
1355 let d: Array1<f64> = (0..n).map(|i| f64::from(p[i] >= u[i])).collect();
1356 let fr: Array1<f64> = (0..n).map(f_reg).collect();
1357 let nu: Array1<f64> = (0..n).map(|i| d[i] * fr[i] + normal(rng)).collect();
1358 let y0 = &fr + &nu + &eps_0;
1359 let y1: Array1<f64> = (0..n)
1360 .map(|i| 2.0 * fr[i] + nu[i] + if d[i] == 1.0 { eps_1b[i] } else { eps_1a[i] })
1361 .collect();
1362 Ok((z, d, y0, y1))
1363 }
1364
1365 pub fn generate(&self, seed: u64) -> Result<Simulated> {
1367 let (z, d, y0, y1) = self.draw(&mut rng(seed))?;
1368 Ok(Simulated::new(z, &y1 - &y0, d))
1369 }
1370
1371 pub fn generate_cross_sections(&self, seed: u64) -> Result<Simulated> {
1374 check(
1375 self.lambda_t > 0.0 && self.lambda_t < 1.0,
1376 "lambda_t must lie in (0, 1)",
1377 )?;
1378 let mut rng = rng(seed);
1379 let (z, d, y0, y1) = self.draw(&mut rng)?;
1380 let t: Array1<f64> = (0..self.n_obs)
1381 .map(|_| f64::from(rng.gen::<f64>() <= self.lambda_t))
1382 .collect();
1383 let y = &t * &y1 + &(&t.mapv(|v| 1.0 - v) * &y0);
1384 let mut sim = Simulated::new(z, y, d);
1385 sim.t = Some(t);
1386 Ok(sim)
1387 }
1388}
1389
1390#[derive(Debug, Clone, PartialEq)]
1394pub struct DidCs2021 {
1395 pub n_obs: usize,
1396 pub dgp_type: u8,
1397 pub include_never_treated: bool,
1398 pub c: f64,
1399 pub xi: f64,
1400 pub n_periods: usize,
1401 pub anticipation_periods: usize,
1402 pub n_pre_treat_periods: usize,
1403}
1404
1405impl Default for DidCs2021 {
1406 fn default() -> Self {
1407 Self {
1408 n_obs: 1000,
1409 dgp_type: 1,
1410 include_never_treated: true,
1411 c: 0.0,
1412 xi: 0.75,
1413 n_periods: 5,
1414 anticipation_periods: 0,
1415 n_pre_treat_periods: 2,
1416 }
1417 }
1418}
1419
1420#[derive(Debug, Clone)]
1424pub struct SimulatedStaggered {
1425 pub id: Array1<i64>,
1426 pub t: Array1<f64>,
1427 pub y: Array1<f64>,
1428 pub y0: Array1<f64>,
1429 pub y1: Array1<f64>,
1430 pub cohort: Array1<f64>,
1431 pub x: Array2<f64>,
1432}
1433
1434impl SimulatedStaggered {
1435 fn select(&self, keep: &[bool]) -> Self {
1436 let rows: Vec<usize> = (0..keep.len()).filter(|&r| keep[r]).collect();
1437 let pick = |v: &Array1<f64>| rows.iter().map(|&r| v[r]).collect::<Array1<f64>>();
1438 Self {
1439 id: rows.iter().map(|&r| self.id[r]).collect(),
1440 t: pick(&self.t),
1441 y: pick(&self.y),
1442 y0: pick(&self.y0),
1443 y1: pick(&self.y1),
1444 cohort: pick(&self.cohort),
1445 x: self.x.select(Axis(0), &rows),
1446 }
1447 }
1448
1449 pub fn to_panel(&self) -> Result<PanelData> {
1451 PanelData::from_long(
1452 self.id.as_slice().expect("contiguous"),
1453 self.t.as_slice().expect("contiguous"),
1454 self.y.as_slice().expect("contiguous"),
1455 self.cohort.as_slice().expect("contiguous"),
1456 &self.x,
1457 )
1458 }
1459
1460 pub fn to_cross_sections(&self) -> Result<RepeatedCrossSections> {
1462 RepeatedCrossSections::new(
1463 self.y.clone(),
1464 self.t.clone(),
1465 self.cohort.clone(),
1466 self.x.clone(),
1467 )
1468 }
1469}
1470
1471impl DidCs2021 {
1472 pub fn generate(&self, seed: u64) -> Result<SimulatedStaggered> {
1474 self.panel(&mut rng(seed))
1475 }
1476
1477 pub fn generate_cross_sections(&self, seed: u64, lambda_t: f64) -> Result<SimulatedStaggered> {
1481 check(
1482 lambda_t > 0.0 && lambda_t <= 1.0,
1483 "lambda_t must lie in (0, 1]",
1484 )?;
1485 let mut rng = rng(seed);
1486 let panel = DidCs2021 {
1487 n_obs: (self.n_obs as f64 / lambda_t).ceil() as usize,
1488 ..self.clone()
1489 }
1490 .panel(&mut rng)?;
1491 let keep: Vec<bool> = (0..panel.y.len())
1492 .map(|_| rng.gen::<f64>() < lambda_t)
1493 .collect();
1494 Ok(panel.select(&keep))
1495 }
1496
1497 fn panel(&self, rng: &mut ChaCha8Rng) -> Result<SimulatedStaggered> {
1498 check(
1499 self.n_pre_treat_periods >= 1,
1500 "n_pre_treat_periods must be at least 1",
1501 )?;
1502 let n_periods = self.n_periods + self.anticipation_periods;
1503 check(
1504 n_periods > self.n_pre_treat_periods,
1505 "need more periods than pre-treatment periods",
1506 )?;
1507 let n = self.n_obs;
1508 let x = mvn(rng, n, &toeplitz(self.c, 4))?;
1509 let z = sz_features(&x, None);
1510 let (ps, reg) = did_features(self.dgp_type, &x, &z)?;
1511 let periods: Vec<f64> = (0..n_periods).map(|t| t as f64).collect();
1512 let max_exposure = n_periods - self.n_pre_treat_periods;
1513 let mut cohorts: Vec<f64> = periods[self.n_pre_treat_periods..].to_vec();
1514 if self.include_never_treated {
1515 cohorts.push(f64::INFINITY);
1516 }
1517 let n_groups = cohorts.len();
1518 let group: Vec<usize> = (0..n)
1520 .map(|i| match ps {
1521 None => rng.gen_range(0..n_groups),
1522 Some(w) => {
1523 let coef = [-1.0, 0.5, -0.25, -0.2];
1524 let logits: Vec<f64> = (0..n_groups)
1525 .map(|g| {
1526 let scale = 1.0 - g as f64 / n_groups as f64;
1527 let v: f64 = (0..4).map(|k| w[[i, k]] * coef[k] * scale).sum();
1528 (self.xi * v).clamp(-2.5, 2.5)
1529 })
1530 .collect();
1531 let e: Vec<f64> = logits.iter().map(|l| l.exp()).collect();
1532 let total: f64 = e.iter().sum();
1533 let clipped: Vec<f64> =
1534 e.iter().map(|v| (v / total).clamp(0.05, 0.95)).collect();
1535 let total: f64 = clipped.iter().sum();
1536 let u = rng.gen::<f64>() * total;
1537 let mut acc = 0.0;
1538 let mut pick = n_groups - 1;
1539 for (g, p) in clipped.iter().enumerate() {
1540 acc += p;
1541 if u < acc {
1542 pick = g;
1543 break;
1544 }
1545 }
1546 pick
1547 }
1548 })
1549 .collect();
1550 let eta: Vec<f64> = group.iter().map(|&g| g as f64 + normal(rng)).collect();
1551 let rows = n * n_periods;
1552 let (mut y, mut y0, mut y1) = (
1553 Array1::zeros(rows),
1554 Array1::zeros(rows),
1555 Array1::zeros(rows),
1556 );
1557 let coef_reg = [27.4, 13.7, 13.7, 13.7];
1558 for i in 0..n {
1559 for t in 0..n_periods {
1560 let r = i * n_periods + t;
1561 let scale = (t + 1) as f64 / n_periods as f64;
1562 let f_reg = 210.0
1563 + (0..4)
1564 .map(|k| reg[[i, k]] * coef_reg[k] * scale)
1565 .sum::<f64>();
1566 let exposure = if t < self.n_pre_treat_periods {
1567 0.0
1568 } else {
1569 let e = (t - self.n_pre_treat_periods) as f64 - group[i] as f64 + 1.0;
1570 if group[i] >= max_exposure {
1571 0.0
1572 } else {
1573 e.max(0.0)
1574 }
1575 };
1576 let delta_e = exposure + 0.1 * x[[i, 0]] * exposure;
1577 let (e0, e1) = (normal(rng), normal(rng));
1578 y0[r] = f_reg + t as f64 + eta[i] + e0;
1579 y1[r] = y0[r] + delta_e + (e1 - e0);
1580 y[r] = if exposure > 0.0 { y1[r] } else { y0[r] };
1581 }
1582 }
1583 let panel = SimulatedStaggered {
1584 id: (0..rows).map(|r| (r / n_periods) as i64).collect(),
1585 t: (0..rows).map(|r| (r % n_periods) as f64).collect(),
1586 y,
1587 y0,
1588 y1,
1589 cohort: (0..rows).map(|r| cohorts[group[r / n_periods]]).collect(),
1590 x: Array2::from_shape_fn((rows, 4), |(r, k)| z[[r / n_periods, k]]),
1591 };
1592 if self.anticipation_periods == 0 {
1593 return Ok(panel);
1594 }
1595 let a = self.anticipation_periods;
1598 let last_cohort = periods[n_periods - 1 - a];
1599 let keep: Vec<bool> = (0..rows)
1600 .map(|r| {
1601 panel.t[r] >= periods[a]
1602 && (panel.cohort[r] <= last_cohort || panel.cohort[r].is_infinite())
1603 })
1604 .collect();
1605 let mut kept = panel.select(&keep);
1606 kept.t.mapv_inplace(|t| t - a as f64);
1607 Ok(kept)
1608 }
1609}
1610
1611#[derive(Debug, Clone, PartialEq)]
1619pub struct SimpleRdd {
1620 pub n_obs: usize,
1621 pub p: usize,
1622 pub fuzzy: bool,
1623 pub binary_outcome: bool,
1624 pub cutoff: f64,
1625 pub dim_x: usize,
1626 pub a: f64,
1627 pub tau: f64,
1628}
1629
1630impl Default for SimpleRdd {
1631 fn default() -> Self {
1632 Self {
1633 n_obs: 5000,
1634 p: 4,
1635 fuzzy: true,
1636 binary_outcome: false,
1637 cutoff: 0.0,
1638 dim_x: 3,
1639 a: 0.0,
1640 tau: 1.0,
1641 }
1642 }
1643}
1644
1645#[derive(Debug, Clone)]
1648pub struct SimulatedRdd {
1649 pub score: Array1<f64>,
1650 pub x: Array2<f64>,
1651 pub y: Array1<f64>,
1652 pub d: Array1<f64>,
1653 pub y0: Array1<f64>,
1654 pub y1: Array1<f64>,
1655}
1656
1657impl SimulatedRdd {
1658 pub fn to_data(&self) -> Result<DmlData> {
1661 DmlData::new(self.y.clone(), self.d.clone(), self.x.clone())
1662 }
1663}
1664
1665impl SimpleRdd {
1666 pub fn generate(&self, seed: u64) -> Result<SimulatedRdd> {
1667 check(self.dim_x >= 1, "dim_x must be positive")?;
1668 let mut rng = rng(seed);
1669 let n = self.n_obs;
1670 let score = normals(&mut rng, n);
1671 let x: Array2<f64> = Array2::from_shape_fn((n, self.dim_x), |_| rng.gen_range(-1.0..1.0));
1672 let g_cov: Array1<f64> = (0..n)
1674 .map(|i| {
1675 (0..self.dim_x)
1676 .map(|j| {
1677 (1..=self.p)
1678 .map(|k| k as f64 * x[[i, j]].powi(k as i32))
1679 .sum::<f64>()
1680 })
1681 .sum()
1682 })
1683 .collect();
1684 let g0 = score.mapv(|s| 0.1 * s * s);
1685 let g1: Array1<f64> = (0..n)
1686 .map(|i| {
1687 let s = score[i];
1688 self.tau + 0.1 * s * s - 0.5 * s * s + self.a * x.row(i).sum() * s
1689 })
1690 .collect();
1691 let (y0, y1): (Array1<f64>, Array1<f64>) = if self.binary_outcome {
1692 (
1693 (0..n)
1694 .map(|i| bernoulli(&mut rng, expit(g0[i] + g_cov[i])))
1695 .collect(),
1696 (0..n)
1697 .map(|i| bernoulli(&mut rng, expit(g1[i] + g_cov[i])))
1698 .collect(),
1699 )
1700 } else {
1701 (
1702 (0..n)
1703 .map(|i| g0[i] + g_cov[i] + 0.2 * normal(&mut rng))
1704 .collect(),
1705 (0..n)
1706 .map(|i| g1[i] + g_cov[i] + 0.2 * normal(&mut rng))
1707 .collect(),
1708 )
1709 };
1710 let intended = score.mapv(|s| f64::from(s >= self.cutoff));
1711 let d: Array1<f64> = if self.fuzzy {
1712 (0..n)
1713 .map(|i| {
1714 let (s, z) = (score[i], intended[i]);
1715 let p = (0.3 + 0.4 * z + 0.01 * s * s - 0.02 * s * s * z + 0.2 * g_cov[i])
1716 .clamp(0.0, 1.0);
1717 bernoulli(&mut rng, p)
1718 })
1719 .collect()
1720 } else {
1721 intended
1722 };
1723 let y: Array1<f64> = (0..n)
1724 .map(|i| if d[i] == 1.0 { y1[i] } else { y0[i] })
1725 .collect();
1726 Ok(SimulatedRdd {
1727 score,
1728 x,
1729 y,
1730 d,
1731 y0,
1732 y1,
1733 })
1734 }
1735}
1736
1737#[cfg(test)]
1738mod tests {
1739 use super::*;
1740
1741 #[test]
1742 fn mvn_has_the_requested_covariance() {
1743 let cov = toeplitz(0.7, 3);
1744 let x = mvn(&mut rng(1), 40_000, &cov).unwrap();
1745 let emp = x.t().dot(&x) / x.nrows() as f64;
1746 for ((i, j), &c) in cov.indexed_iter() {
1747 assert!((emp[[i, j]] - c).abs() < 0.03, "cov[{i},{j}]");
1748 }
1749 }
1750
1751 #[test]
1752 fn spd_matrix_is_positive_definite() {
1753 let m = spd_matrix(&mut rng(3), 6);
1754 assert!(cholesky_factor(&m).is_ok());
1755 assert!((&m - &m.t()).iter().all(|v| v.abs() < 1e-12));
1756 }
1757
1758 #[test]
1759 fn generators_are_reproducible_and_validated() {
1760 let a = PlrCcddhnr2018::default().generate(7).unwrap();
1761 let b = PlrCcddhnr2018::default().generate(7).unwrap();
1762 assert_eq!(a.y, b.y);
1763 assert!(PlrCcddhnr2018 {
1764 dim_x: 2,
1765 ..Default::default()
1766 }
1767 .generate(0)
1768 .is_err());
1769 assert!(LplrLzz2020 {
1770 dim_x: 10,
1771 ..Default::default()
1772 }
1773 .generate(0)
1774 .is_err());
1775 assert!(DidSz2020 {
1776 dgp_type: 7,
1777 ..Default::default()
1778 }
1779 .generate(0)
1780 .is_err());
1781 }
1782}