1use std::borrow::Cow;
2
3use ndarray::{concatenate, Array1, Array2, ArrayView1, ArrayView2, Axis};
4
5use crate::error::{DmlError, Result};
6
7#[derive(Debug, Clone)]
18pub struct DmlData {
19 y: Array1<f64>,
20 d: Array2<f64>,
21 x: Array2<f64>,
22 z: Option<Array1<f64>>,
23 t: Option<Array1<f64>>,
24 treatment_names: Vec<String>,
25}
26
27fn all_finite<'a>(mut values: impl Iterator<Item = &'a f64>) -> bool {
28 values.all(|v| v.is_finite())
29}
30
31impl DmlData {
32 pub fn new(y: Array1<f64>, d: Array1<f64>, x: Array2<f64>) -> Result<Self> {
34 let n = d.len();
35 let d = d.into_shape_with_order((n, 1)).expect("1-D to (n, 1)");
36 Self::with_treatments(y, d, x)
37 }
38
39 pub fn with_treatments(y: Array1<f64>, d: Array2<f64>, x: Array2<f64>) -> Result<Self> {
41 let n = y.len();
42 if n < 2 {
43 return Err(DmlError::InvalidArgument(
44 "need at least 2 observations".into(),
45 ));
46 }
47 if d.nrows() != n || x.nrows() != n {
48 return Err(DmlError::DimensionMismatch(format!(
49 "y has {n} rows, d has {}, x has {}",
50 d.nrows(),
51 x.nrows()
52 )));
53 }
54 if d.ncols() == 0 {
55 return Err(DmlError::InvalidArgument(
56 "need at least one treatment".into(),
57 ));
58 }
59 if !all_finite(y.iter()) || !all_finite(d.iter()) || !all_finite(x.iter()) {
60 return Err(DmlError::InvalidArgument(
61 "data contains NaN or infinite values".into(),
62 ));
63 }
64 let treatment_names = if d.ncols() == 1 {
65 vec!["d".to_string()]
66 } else {
67 (0..d.ncols()).map(|j| format!("d{j}")).collect()
68 };
69 Ok(Self {
70 y,
71 d,
72 x,
73 z: None,
74 t: None,
75 treatment_names,
76 })
77 }
78
79 pub fn with_instrument(mut self, z: Array1<f64>) -> Result<Self> {
81 if z.len() != self.n_obs() {
82 return Err(DmlError::DimensionMismatch(format!(
83 "z has {} rows, expected {}",
84 z.len(),
85 self.n_obs()
86 )));
87 }
88 if !all_finite(z.iter()) {
89 return Err(DmlError::InvalidArgument(
90 "instrument contains NaN or infinite values".into(),
91 ));
92 }
93 self.z = Some(z);
94 Ok(self)
95 }
96
97 pub fn with_time_indicator(mut self, t: Array1<f64>) -> Result<Self> {
100 if t.len() != self.n_obs() {
101 return Err(DmlError::DimensionMismatch(format!(
102 "t has {} rows, expected {}",
103 t.len(),
104 self.n_obs()
105 )));
106 }
107 if !is_binary(t.view()) {
108 return Err(DmlError::InvalidArgument(
109 "time indicator must contain only 0 and 1".into(),
110 ));
111 }
112 self.t = Some(t);
113 Ok(self)
114 }
115
116 pub fn without_covariates(&self, drop: &[usize]) -> Result<Self> {
119 let p = self.n_features();
120 if drop.is_empty() || drop.iter().any(|&c| c >= p) {
121 return Err(DmlError::InvalidArgument(format!(
122 "benchmarking columns must be a non-empty subset of the {p} covariates"
123 )));
124 }
125 let keep: Vec<usize> = (0..p).filter(|c| !drop.contains(c)).collect();
126 Ok(Self {
127 x: self.x.select(Axis(1), &keep),
128 ..self.clone()
129 })
130 }
131
132 pub fn with_treatment_names<S: Into<String>>(
134 mut self,
135 names: impl IntoIterator<Item = S>,
136 ) -> Result<Self> {
137 let names: Vec<String> = names.into_iter().map(Into::into).collect();
138 if names.len() != self.n_treat() {
139 return Err(DmlError::DimensionMismatch(format!(
140 "{} names for {} treatments",
141 names.len(),
142 self.n_treat()
143 )));
144 }
145 self.treatment_names = names;
146 Ok(self)
147 }
148
149 pub fn n_obs(&self) -> usize {
150 self.y.len()
151 }
152
153 pub fn n_features(&self) -> usize {
154 self.x.ncols()
155 }
156
157 pub fn n_treat(&self) -> usize {
158 self.d.ncols()
159 }
160
161 pub fn y(&self) -> ArrayView1<'_, f64> {
162 self.y.view()
163 }
164
165 pub fn d(&self) -> ArrayView2<'_, f64> {
167 self.d.view()
168 }
169
170 pub fn treatment(&self, j: usize) -> ArrayView1<'_, f64> {
172 self.d.column(j)
173 }
174
175 pub fn x(&self) -> ArrayView2<'_, f64> {
176 self.x.view()
177 }
178
179 pub fn z(&self) -> Option<ArrayView1<'_, f64>> {
180 self.z.as_ref().map(|z| z.view())
181 }
182
183 pub fn time(&self) -> Option<ArrayView1<'_, f64>> {
185 self.t.as_ref().map(|t| t.view())
186 }
187
188 pub fn treatment_names(&self) -> &[String] {
189 &self.treatment_names
190 }
191
192 pub fn covariates_for(&self, j: usize) -> Cow<'_, Array2<f64>> {
197 if self.n_treat() == 1 {
198 return Cow::Borrowed(&self.x);
199 }
200 let others: Vec<usize> = (0..self.n_treat()).filter(|&k| k != j).collect();
201 let d_other = self.d.select(Axis(1), &others);
202 Cow::Owned(concatenate![Axis(1), self.x, d_other])
203 }
204
205 pub fn is_binary_treatment(&self, j: usize) -> bool {
207 is_binary(self.d.column(j))
208 }
209}
210
211pub(crate) fn is_binary(v: ArrayView1<'_, f64>) -> bool {
213 v.iter().all(|&x| x == 0.0 || x == 1.0)
214}
215
216pub(crate) fn gather(v: ArrayView1<'_, f64>, idx: &[usize]) -> Array1<f64> {
218 idx.iter().map(|&i| v[i]).collect()
219}
220
221#[cfg(test)]
222mod tests {
223 use super::*;
224 use ndarray::array;
225
226 #[test]
227 fn rejects_mismatched_lengths() {
228 let err = DmlData::new(array![1.0, 2.0], array![0.0], array![[1.0], [2.0]]).unwrap_err();
229 assert!(matches!(err, DmlError::DimensionMismatch(_)));
230 }
231
232 #[test]
233 fn rejects_nan() {
234 let err = DmlData::new(
235 array![1.0, f64::NAN],
236 array![0.0, 1.0],
237 array![[1.0], [2.0]],
238 )
239 .unwrap_err();
240 assert!(matches!(err, DmlError::InvalidArgument(_)));
241 }
242
243 #[test]
244 fn detects_binary_treatment() {
245 let data = DmlData::new(array![1.0, 2.0], array![0.0, 1.0], array![[1.0], [2.0]]).unwrap();
246 assert!(data.is_binary_treatment(0));
247 }
248
249 #[test]
250 fn other_treatments_become_covariates() {
251 let d = array![[1.0, 10.0, 100.0], [2.0, 20.0, 200.0]];
252 let data = DmlData::with_treatments(array![0.0, 1.0], d, array![[5.0], [6.0]]).unwrap();
253 assert_eq!(data.treatment_names(), ["d0", "d1", "d2"]);
254 assert_eq!(
255 *data.covariates_for(1),
256 array![[5.0, 1.0, 100.0], [6.0, 2.0, 200.0]]
257 );
258 assert!(matches!(data.covariates_for(0), Cow::Owned(_)));
259 }
260
261 #[test]
262 fn time_indicator_must_be_binary() {
263 let data = DmlData::new(array![1.0, 2.0], array![0.0, 1.0], array![[1.0], [2.0]]).unwrap();
264 assert!(data.clone().with_time_indicator(array![0.0, 2.0]).is_err());
265 assert!(data.clone().with_time_indicator(array![1.0]).is_err());
266 assert!(data
267 .with_time_indicator(array![0.0, 1.0])
268 .unwrap()
269 .time()
270 .is_some());
271 }
272
273 #[test]
274 fn instrument_length_checked() {
275 let data = DmlData::new(array![1.0, 2.0], array![0.0, 1.0], array![[1.0], [2.0]]).unwrap();
276 assert!(data.clone().with_instrument(array![1.0]).is_err());
277 assert!(data
278 .with_instrument(array![1.0, 0.0])
279 .unwrap()
280 .z()
281 .is_some());
282 }
283}