1use std::collections::{BTreeMap, HashSet};
11
12use ndarray::{Array1, ArrayView1};
13
14use crate::crossfit::{Fold, KFold};
15use crate::error::{DmlError, Result};
16use crate::score::LinearScore;
17
18#[derive(Debug, Clone, PartialEq)]
21pub struct ClusterFold {
22 pub fold: Fold,
23 pub test_clusters: Vec<Vec<i64>>,
24}
25
26pub(crate) fn split_clusters(
31 clusters: &[Array1<i64>],
32 n_folds: usize,
33 seed: u64,
34) -> Result<Vec<ClusterFold>> {
35 let n = clusters[0].len();
36 let mut per_var: Vec<(Vec<i64>, Vec<usize>)> = Vec::new(); for (v, c) in clusters.iter().enumerate() {
38 let mut distinct: Vec<i64> = c.to_vec();
39 distinct.sort_unstable();
40 distinct.dedup();
41 let var_seed = seed.wrapping_add((v as u64).wrapping_mul(0xC2B2_AE3D_27D4_EB4F));
42 let folds = KFold::new(n_folds, var_seed)?
43 .split(distinct.len())
44 .map_err(|_| {
45 DmlError::InvalidArgument(format!(
46 "cluster variable {v} has {} clusters, fewer than {n_folds} folds",
47 distinct.len()
48 ))
49 })?;
50 let mut fold_of = vec![0usize; distinct.len()];
51 for (k, f) in folds.iter().enumerate() {
52 for &i in &f.test {
53 fold_of[i] = k;
54 }
55 }
56 per_var.push((distinct, fold_of));
57 }
58 let obs_fold: Vec<Vec<usize>> = clusters
60 .iter()
61 .zip(&per_var)
62 .map(|(c, (distinct, fold_of))| {
63 c.iter()
64 .map(|v| fold_of[distinct.binary_search(v).expect("collected above")])
65 .collect()
66 })
67 .collect();
68 let m = clusters.len();
69 let combos = n_folds.pow(m as u32);
70 let mut out = Vec::with_capacity(combos);
71 for combo in 0..combos {
72 let mut digits = vec![0usize; m];
74 let mut rest = combo;
75 for v in (0..m).rev() {
76 digits[v] = rest % n_folds;
77 rest /= n_folds;
78 }
79 let test: Vec<usize> = (0..n)
80 .filter(|&i| (0..m).all(|v| obs_fold[v][i] == digits[v]))
81 .collect();
82 let train: Vec<usize> = (0..n)
83 .filter(|&i| (0..m).all(|v| obs_fold[v][i] != digits[v]))
84 .collect();
85 let test_clusters = per_var
86 .iter()
87 .zip(&digits)
88 .map(|((distinct, fold_of), &k)| {
89 distinct
90 .iter()
91 .zip(fold_of)
92 .filter(|(_, &f)| f == k)
93 .map(|(&c, _)| c)
94 .collect()
95 })
96 .collect();
97 out.push(ClusterFold {
98 fold: Fold { train, test },
99 test_clusters,
100 });
101 }
102 Ok(out)
103}
104
105pub(crate) fn derive_test_clusters(clusters: &[Array1<i64>], fold: &Fold) -> Vec<Vec<i64>> {
108 clusters
109 .iter()
110 .map(|c| {
111 let mut v: Vec<i64> = fold.test.iter().map(|&i| c[i]).collect();
112 v.sort_unstable();
113 v.dedup();
114 v
115 })
116 .collect()
117}
118
119#[derive(Debug, Clone)]
122pub(crate) struct ClusterInfo {
123 pub clusters: Vec<Array1<i64>>,
124 pub n_folds_per_cluster: usize,
125 pub folds: Vec<Vec<ClusterFold>>,
127 members: Vec<BTreeMap<i64, Vec<usize>>>,
129}
130
131impl ClusterInfo {
132 pub(crate) fn new(clusters: Vec<Array1<i64>>, folds: Vec<Vec<ClusterFold>>) -> Result<Self> {
133 if clusters.is_empty() || clusters.len() > 2 {
134 return Err(DmlError::InvalidArgument(
135 "one or two cluster variables are supported".into(),
136 ));
137 }
138 let m = clusters.len() as u32;
139 let n_folds = folds.first().map_or(0, |f| f.len());
140 let k = (n_folds as f64).powf(1.0 / m as f64).round() as usize;
141 if k.pow(m) != n_folds || folds.iter().any(|f| f.len() != n_folds) {
142 return Err(DmlError::InvalidArgument(
143 "cluster folds must be the product of the per-variable folds".into(),
144 ));
145 }
146 let members = clusters
147 .iter()
148 .map(|c| {
149 let mut map: BTreeMap<i64, Vec<usize>> = BTreeMap::new();
150 for (i, &v) in c.iter().enumerate() {
151 map.entry(v).or_default().push(i);
152 }
153 map
154 })
155 .collect();
156 Ok(Self {
157 clusters,
158 n_folds_per_cluster: k,
159 folds,
160 members,
161 })
162 }
163
164 pub(crate) fn n_ways(&self) -> usize {
165 self.clusters.len()
166 }
167
168 pub(crate) fn n_clusters(&self) -> usize {
171 self.members.iter().map(BTreeMap::len).min().unwrap_or(0)
172 }
173
174 pub(crate) fn solve_theta(&self, score: &LinearScore, rep: usize) -> Result<f64> {
177 let (mut sa, mut sb) = (0.0, 0.0);
178 for cf in &self.folds[rep] {
179 let scale = 1.0
180 / cf.test_clusters
181 .iter()
182 .map(|c| c.len() as f64)
183 .product::<f64>();
184 sa += scale * cf.fold.test.iter().map(|&i| score.psi_a[i]).sum::<f64>();
185 sb += scale * cf.fold.test.iter().map(|&i| score.psi_b[i]).sum::<f64>();
186 }
187 if sa == 0.0 || !sa.is_finite() {
188 return Err(DmlError::InvalidArgument(
189 "weighted mean of psi_a is zero or not finite: theta is not identified".into(),
190 ));
191 }
192 Ok(-sb / sa)
193 }
194
195 pub(crate) fn variance(
198 &self,
199 psi: ArrayView1<'_, f64>,
200 psi_deriv: ArrayView1<'_, f64>,
201 rep: usize,
202 ) -> f64 {
203 let k = self.n_folds_per_cluster as f64;
204 let (mut gamma, mut jac) = (0.0, 0.0);
205 let sum_over = |idx: &[usize]| idx.iter().map(|&i| psi[i]).sum::<f64>();
206 for cf in &self.folds[rep] {
207 let test_deriv: f64 = cf.fold.test.iter().map(|&i| psi_deriv[i]).sum();
208 if self.n_ways() == 1 {
209 let i_k = &cf.test_clusters[0];
210 let c = 1.0 / i_k.len() as f64;
211 for cl in i_k {
212 let s = self.members[0].get(cl).map_or(0.0, |m| sum_over(m));
213 gamma += c * s * s;
214 }
215 jac += test_deriv / i_k.len() as f64;
216 } else {
217 let (i_k, j_l) = (&cf.test_clusters[0], &cf.test_clusters[1]);
218 let (ni, nj) = (i_k.len() as f64, j_l.len() as f64);
219 let c = ni.min(nj) / (ni * nj).powi(2);
220 let in_j: HashSet<i64> = j_l.iter().copied().collect();
221 let in_i: HashSet<i64> = i_k.iter().copied().collect();
222 for cl in i_k {
223 if let Some(m) = self.members[0].get(cl) {
224 let s: f64 = m
225 .iter()
226 .filter(|&&i| in_j.contains(&self.clusters[1][i]))
227 .map(|&i| psi[i])
228 .sum();
229 gamma += c * s * s;
230 }
231 }
232 for cl in j_l {
233 if let Some(m) = self.members[1].get(cl) {
234 let s: f64 = m
235 .iter()
236 .filter(|&&i| in_i.contains(&self.clusters[0][i]))
237 .map(|&i| psi[i])
238 .sum();
239 gamma += c * s * s;
240 }
241 }
242 jac += test_deriv / (ni * nj);
243 }
244 }
245 let norm = k.powi(self.n_ways() as i32);
246 let (gamma, jac) = (gamma / norm, jac / norm);
247 gamma / (self.n_clusters() as f64 * jac * jac)
248 }
249}
250
251#[cfg(test)]
252mod tests {
253 use super::*;
254 use ndarray::array;
255
256 #[test]
257 fn one_way_split_keeps_clusters_together() {
258 let c = Array1::from_shape_fn(40, |i| (i / 4) as i64); let folds = split_clusters(std::slice::from_ref(&c), 3, 7).unwrap();
260 assert_eq!(folds.len(), 3);
261 let mut seen = [false; 40];
262 for cf in &folds {
263 for &i in &cf.fold.test {
264 assert!(!seen[i]);
265 seen[i] = true;
266 assert!(cf.test_clusters[0].contains(&c[i]));
267 }
268 assert!(cf
270 .fold
271 .train
272 .iter()
273 .all(|&i| !cf.test_clusters[0].contains(&c[i])));
274 assert_eq!(cf.fold.train.len() + cf.fold.test.len(), 40);
275 }
276 assert!(seen.iter().all(|&s| s));
277 }
278
279 #[test]
280 fn two_way_split_is_the_fold_product() {
281 let a = Array1::from_shape_fn(36, |i| (i / 6) as i64);
282 let b = Array1::from_shape_fn(36, |i| (i % 6) as i64);
283 let folds = split_clusters(&[a.clone(), b.clone()], 2, 1).unwrap();
284 assert_eq!(folds.len(), 4);
285 let total: usize = folds.iter().map(|cf| cf.fold.test.len()).sum();
286 assert_eq!(total, 36);
287 for cf in &folds {
288 for &i in &cf.fold.train {
289 assert!(
290 !cf.test_clusters[0].contains(&a[i]) && !cf.test_clusters[1].contains(&b[i])
291 );
292 }
293 }
294 }
295
296 #[test]
297 fn one_way_variance_with_singleton_clusters_is_the_iid_variance() {
298 let n = 6;
301 let c = Array1::from_shape_fn(n, |i| i as i64);
302 let folds = split_clusters(std::slice::from_ref(&c), 2, 3).unwrap();
303 let info = ClusterInfo::new(vec![c], vec![folds]).unwrap();
304 let psi = array![1.0, -2.0, 0.5, 0.3, -1.0, 1.2];
305 let deriv = Array1::from_elem(n, -2.0);
306 let iid = psi.dot(&psi) / n as f64 / 4.0 / n as f64;
307 assert!((info.variance(psi.view(), deriv.view(), 0) - iid).abs() < 1e-12);
308 }
309}