Skip to main content

antecedent_data/
selection.rs

1//! Row selection and column transform helpers shared by estimators / refuters.
2//!
3//! SPDX-License-Identifier: MIT OR Apache-2.0
4
5use std::sync::Arc;
6
7use antecedent_core::{
8    CausalSchemaBuilder, MeasurementSpec, RoleHint, SmallRoleSet, ValueType, VariableId,
9};
10
11use crate::column::{ColumnView, Float64Column, OwnedColumn, ValidityBitmap};
12use crate::dataset::TabularData;
13use crate::error::DataError;
14use crate::storage::OwnedColumnarStorage;
15use crate::table::TableView;
16
17impl TabularData {
18    /// Row mask: analysis mask ∩ validity of every listed column.
19    ///
20    /// # Errors
21    ///
22    /// Unknown variables, or no remaining complete cases.
23    pub fn complete_case_mask(&self, ids: &[VariableId]) -> Result<Vec<bool>, DataError> {
24        let mut keep = Vec::new();
25        self.complete_case_mask_into(ids, &mut keep)?;
26        Ok(keep)
27    }
28
29    /// [`Self::complete_case_mask`] into a caller-owned buffer (cleared and refilled), so
30    /// repeated refute-path calls can reuse one allocation.
31    ///
32    /// # Errors
33    ///
34    /// Unknown variables, or no remaining complete cases.
35    pub fn complete_case_mask_into(
36        &self,
37        ids: &[VariableId],
38        out: &mut Vec<bool>,
39    ) -> Result<(), DataError> {
40        let n = self.row_count();
41        out.clear();
42        out.resize(n, true);
43        let keep = out;
44        if let Some(mask) = self.storage().analysis_mask() {
45            for (i, slot) in keep.iter_mut().enumerate() {
46                *slot = mask.is_valid(i);
47            }
48        }
49        for &id in ids {
50            let validity = self.column(id)?.validity();
51            for (i, slot) in keep.iter_mut().enumerate() {
52                if *slot && !validity.is_valid(i) {
53                    *slot = false;
54                }
55            }
56        }
57        if !keep.iter().any(|k| *k) {
58            return Err(DataError::EmptySelection {
59                context: "complete-case mask after validity/analysis filtering",
60            });
61        }
62        Ok(())
63    }
64
65    /// Extract float64 values for rows where `keep[i]` is true.
66    ///
67    /// # Errors
68    ///
69    /// Unknown / non-float64 column, or keep length mismatch.
70    pub fn float64_masked(&self, id: VariableId, keep: &[bool]) -> Result<Vec<f64>, DataError> {
71        if keep.len() != self.row_count() {
72            return Err(DataError::LengthMismatch {
73                expected: self.row_count(),
74                actual: keep.len(),
75                context: "complete-case keep mask",
76            });
77        }
78        let ColumnView::Float64(c) = self.column(id)? else {
79            return Err(DataError::TypeMismatch { id, expected: "float64" });
80        };
81        let mut out = Vec::with_capacity(keep.iter().filter(|k| **k).count());
82        for (i, &k) in keep.iter().enumerate() {
83            if k {
84                out.push(c.values[i]);
85            }
86        }
87        Ok(out)
88    }
89
90    /// Replace one float64 column; preserve other columns, analysis mask, and weights.
91    ///
92    /// The replacement column is marked all-valid (caller supplies a complete vector).
93    ///
94    /// # Errors
95    ///
96    /// Unknown id, length mismatch, or non-float target.
97    pub fn with_replaced_float(
98        &self,
99        id: VariableId,
100        values: Arc<[f64]>,
101    ) -> Result<Self, DataError> {
102        self.with_replaced_floats(&[(id, values)])
103    }
104
105    /// Replace several float64 columns in **one** storage rebuild; preserve other columns,
106    /// analysis mask, and weights (each copied once, not once per replaced column — this is
107    /// the bulk form bootstrap resampling should use instead of chaining
108    /// [`Self::with_replaced_float`]).
109    ///
110    /// Replacements apply in order against the same column set (ids are expected to be
111    /// distinct; a repeated id keeps the last entry). Each replacement column is marked
112    /// all-valid (callers supply complete vectors).
113    ///
114    /// # Errors
115    ///
116    /// Unknown id, length mismatch, or non-float target.
117    pub fn with_replaced_floats(
118        &self,
119        replacements: &[(VariableId, Arc<[f64]>)],
120    ) -> Result<Self, DataError> {
121        let n = self.row_count();
122        let storage = self.storage();
123        let mut cols: Vec<OwnedColumn> = storage.columns().to_vec();
124        for (id, values) in replacements {
125            let id = *id;
126            if values.len() != n {
127                return Err(DataError::LengthMismatch {
128                    expected: n,
129                    actual: values.len(),
130                    context: "replacement float column",
131                });
132            }
133            let idx = id.as_usize();
134            if idx >= cols.len() {
135                return Err(DataError::UnknownVariable { id });
136            }
137            if !matches!(cols[idx], OwnedColumn::Float64(_)) {
138                return Err(DataError::TypeMismatch { id, expected: "float64" });
139            }
140            cols[idx] = OwnedColumn::Float64(Float64Column::new(
141                id,
142                Arc::clone(values),
143                ValidityBitmap::all_valid(n),
144            )?);
145        }
146        let storage = OwnedColumnarStorage::try_new(
147            storage.schema().clone(),
148            cols,
149            storage.analysis_mask().cloned(),
150            storage.weights().map(Arc::from),
151        )?;
152        Ok(Self::new(storage))
153    }
154
155    /// Restrict analysis to rows where `mask` is valid, intersected (AND) with any existing
156    /// analysis mask; preserves columns, validity, and weights.
157    ///
158    /// # Errors
159    ///
160    /// Mask length mismatch.
161    pub fn with_analysis_mask(&self, mask: ValidityBitmap) -> Result<Self, DataError> {
162        let storage = self.storage();
163        let n = storage.row_count();
164        if mask.len() != n {
165            return Err(DataError::LengthMismatch {
166                expected: n,
167                actual: mask.len(),
168                context: "analysis mask",
169            });
170        }
171        let combined = match storage.analysis_mask() {
172            Some(existing) => {
173                let mut bytes = vec![0u8; n.div_ceil(8)];
174                for i in 0..n {
175                    if existing.is_valid(i) && mask.is_valid(i) {
176                        bytes[i / 8] |= 1 << (i % 8);
177                    }
178                }
179                ValidityBitmap::from_bytes(bytes, n)?
180            }
181            None => mask,
182        };
183        let new_storage = OwnedColumnarStorage::try_new(
184            storage.schema().clone(),
185            storage.columns().to_vec(),
186            Some(combined),
187            storage.weights().map(Arc::from),
188        )?;
189        Ok(Self::new(new_storage))
190    }
191
192    /// Append a continuous float64 covariate; preserve existing columns/mask/weights.
193    ///
194    /// # Errors
195    ///
196    /// Length mismatch or schema construction failure.
197    pub fn with_appended_float(
198        &self,
199        name: &str,
200        values: Arc<[f64]>,
201    ) -> Result<(Self, VariableId), DataError> {
202        let n = self.row_count();
203        if values.len() != n {
204            return Err(DataError::LengthMismatch {
205                expected: n,
206                actual: values.len(),
207                context: "appended float column",
208            });
209        }
210        let storage = self.storage();
211        let mut builder = CausalSchemaBuilder::new();
212        for v in storage.schema().variables() {
213            builder
214                .add_variable(
215                    Arc::clone(&v.name),
216                    v.value_type.clone(),
217                    v.role_hints,
218                    v.unit.clone(),
219                    v.category_domain,
220                    v.measurement.clone(),
221                )
222                .map_err(|e| DataError::Schema(e.to_string()))?;
223        }
224        builder
225            .add_variable(
226                name,
227                ValueType::Continuous,
228                SmallRoleSet::from_hint(RoleHint::Context),
229                None,
230                None,
231                MeasurementSpec::default(),
232            )
233            .map_err(|e| DataError::Schema(e.to_string()))?;
234        let schema = builder.build().map_err(|e| DataError::Schema(e.to_string()))?;
235        let new_id = VariableId::from_raw(u32::try_from(schema.len() - 1).map_err(|_| {
236            DataError::InvalidArgument { message: "schema exceeds VariableId range".into() }
237        })?);
238        let mut cols: Vec<OwnedColumn> = storage.columns().to_vec();
239        cols.push(OwnedColumn::Float64(Float64Column::new(
240            new_id,
241            values,
242            ValidityBitmap::all_valid(n),
243        )?));
244        let storage = OwnedColumnarStorage::try_new(
245            schema,
246            cols,
247            storage.analysis_mask().cloned(),
248            storage.weights().map(Arc::from),
249        )?;
250        Ok((Self::new(storage), new_id))
251    }
252}