Skip to main content

chromy_core/
estimate.rs

1//! Estimation that matches the design.
2//!
3//! Under minimum replacement a unit may be selected more than once, and
4//! the estimator that fits is the multiplicity (Hansen–Hurwitz form)
5//! estimator: each unit's value divided by its *effective* expected hits —
6//! the ones the selection actually used — and weighted by the hits it
7//! received. For the variance there is no unbiased answer under a
8//! systematic-like design; the successive-difference estimator is the
9//! working convention, computed over the units whose selection was
10//! actually random, in list order, so that it reads the local variation
11//! the ordering left behind.
12
13use crate::error::Error;
14
15/// The totals and their successive-difference variance.
16#[derive(Debug, Clone)]
17pub struct Estimate {
18    /// The estimated total of each study variable, in column order.
19    pub total: Vec<f64>,
20    /// How many sampled units had a randomly decided hit — the sample the
21    /// variance is computed over.
22    pub random_draws: usize,
23    /// The successive-difference variance of each total: zero when no hit
24    /// was random, `NaN` when exactly one was (one observation estimates
25    /// no variance).
26    pub sd_variance: Vec<f64>,
27}
28
29/// The multiplicity estimator with successive-difference variance.
30///
31/// `y` is the matrix of study variables for the *frame*, column-major with
32/// `n_rows` units. The remaining slices describe the sample, one entry per
33/// sampled unit: `units` the 0-based frame indices, `positions` the
34/// 0-based positions in the selection list (the order the variance reads),
35/// `expected_hits` the effective expected hits, `hits` the realised hits.
36pub fn multiplicity_estimate(
37    y: &[f64],
38    n_rows: usize,
39    units: &[usize],
40    positions: &[usize],
41    expected_hits: &[f64],
42    hits: &[u64],
43) -> Result<Estimate, Error> {
44    if n_rows == 0 || y.len() % n_rows != 0 {
45        return Err(Error::Input(format!(
46            "a matrix of {} values cannot have {} rows",
47            y.len(),
48            n_rows
49        )));
50    }
51    let n_cols = y.len() / n_rows;
52    let m = units.len();
53    if positions.len() != m || expected_hits.len() != m || hits.len() != m {
54        return Err(Error::Input(
55            "units, positions, expected hits and hits must have one entry per \
56             sampled unit"
57                .into(),
58        ));
59    }
60    for &v in y {
61        if !v.is_finite() {
62            return Err(Error::Input(format!(
63                "study variables must be finite; found {v}"
64            )));
65        }
66    }
67    for &u in units {
68        if u >= n_rows {
69            return Err(Error::Input(format!(
70                "sampled unit {u} is beyond the {n_rows} rows of the study matrix"
71            )));
72        }
73    }
74    for (s, &e) in expected_hits.iter().enumerate() {
75        if !e.is_finite() || e <= 0.0 {
76            return Err(Error::Input(format!(
77                "expected hits must be positive on the sample; entry {s} is {e}"
78            )));
79        }
80    }
81
82    // The sample in list order: the successive differences are to read
83    // neighbours of the ordered frame, not of whatever order the caller
84    // stored the sample in.
85    let mut by_list: Vec<usize> = (0..m).collect();
86    by_list.sort_by_key(|&s| positions[s]);
87
88    let mut total = vec![0.0_f64; n_cols];
89    for c in 0..n_cols {
90        for &s in &by_list {
91            total[c] += y[c * n_rows + units[s]] / expected_hits[s] * hits[s] as f64;
92        }
93    }
94
95    // The units whose hit was random: realised hits one above the floor of
96    // the expected hits. Fixed hits are certainties and contribute no
97    // variance.
98    let random: Vec<usize> = by_list
99        .iter()
100        .copied()
101        .filter(|&s| hits[s] as i64 - expected_hits[s].floor() as i64 == 1)
102        .collect();
103    let draws = random.len();
104
105    let sd_variance: Vec<f64> = (0..n_cols)
106        .map(|c| {
107            if draws == 0 {
108                0.0
109            } else if draws == 1 {
110                f64::NAN
111            } else {
112                let mut ss = 0.0_f64;
113                for w in random.windows(2) {
114                    let a = y[c * n_rows + units[w[0]]] / expected_hits[w[0]];
115                    let b = y[c * n_rows + units[w[1]]] / expected_hits[w[1]];
116                    ss += (b - a) * (b - a);
117                }
118                draws as f64 / (2.0 * (draws as f64 - 1.0)) * ss
119            }
120        })
121        .collect();
122
123    Ok(Estimate {
124        total,
125        random_draws: draws,
126        sd_variance,
127    })
128}