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 n = self.row_count();
25        let mut keep = vec![true; n];
26        if let Some(mask) = self.storage().analysis_mask() {
27            for (i, slot) in keep.iter_mut().enumerate() {
28                *slot = mask.is_valid(i);
29            }
30        }
31        for &id in ids {
32            let validity = self.column(id)?.validity();
33            for (i, slot) in keep.iter_mut().enumerate() {
34                if *slot && !validity.is_valid(i) {
35                    *slot = false;
36                }
37            }
38        }
39        if !keep.iter().any(|k| *k) {
40            return Err(DataError::EmptySelection {
41                context: "complete-case mask after validity/analysis filtering",
42            });
43        }
44        Ok(keep)
45    }
46
47    /// Extract float64 values for rows where `keep[i]` is true.
48    ///
49    /// # Errors
50    ///
51    /// Unknown / non-float64 column, or keep length mismatch.
52    pub fn float64_masked(&self, id: VariableId, keep: &[bool]) -> Result<Vec<f64>, DataError> {
53        if keep.len() != self.row_count() {
54            return Err(DataError::LengthMismatch {
55                expected: self.row_count(),
56                actual: keep.len(),
57                context: "complete-case keep mask",
58            });
59        }
60        let ColumnView::Float64(c) = self.column(id)? else {
61            return Err(DataError::TypeMismatch { id, expected: "float64" });
62        };
63        let mut out = Vec::with_capacity(keep.iter().filter(|k| **k).count());
64        for (i, &k) in keep.iter().enumerate() {
65            if k {
66                out.push(c.values[i]);
67            }
68        }
69        Ok(out)
70    }
71
72    /// Replace one float64 column; preserve other columns, analysis mask, and weights.
73    ///
74    /// The replacement column is marked all-valid (caller supplies a complete vector).
75    ///
76    /// # Errors
77    ///
78    /// Unknown id, length mismatch, or non-float target.
79    pub fn with_replaced_float(
80        &self,
81        id: VariableId,
82        values: Arc<[f64]>,
83    ) -> Result<Self, DataError> {
84        let n = self.row_count();
85        if values.len() != n {
86            return Err(DataError::LengthMismatch {
87                expected: n,
88                actual: values.len(),
89                context: "replacement float column",
90            });
91        }
92        let storage = self.storage();
93        let mut cols: Vec<OwnedColumn> = storage.columns().to_vec();
94        let idx = id.as_usize();
95        if idx >= cols.len() {
96            return Err(DataError::UnknownVariable { id });
97        }
98        if !matches!(cols[idx], OwnedColumn::Float64(_)) {
99            return Err(DataError::TypeMismatch { id, expected: "float64" });
100        }
101        cols[idx] =
102            OwnedColumn::Float64(Float64Column::new(id, values, ValidityBitmap::all_valid(n))?);
103        let storage = OwnedColumnarStorage::try_new(
104            storage.schema().clone(),
105            cols,
106            storage.analysis_mask().cloned(),
107            storage.weights().map(Arc::from),
108        )?;
109        Ok(Self::new(storage))
110    }
111
112    /// Restrict analysis to rows where `mask` is valid, intersected (AND) with any existing
113    /// analysis mask; preserves columns, validity, and weights.
114    ///
115    /// # Errors
116    ///
117    /// Mask length mismatch.
118    pub fn with_analysis_mask(&self, mask: ValidityBitmap) -> Result<Self, DataError> {
119        let storage = self.storage();
120        let n = storage.row_count();
121        if mask.len() != n {
122            return Err(DataError::LengthMismatch {
123                expected: n,
124                actual: mask.len(),
125                context: "analysis mask",
126            });
127        }
128        let combined = match storage.analysis_mask() {
129            Some(existing) => {
130                let mut bytes = vec![0u8; n.div_ceil(8)];
131                for i in 0..n {
132                    if existing.is_valid(i) && mask.is_valid(i) {
133                        bytes[i / 8] |= 1 << (i % 8);
134                    }
135                }
136                ValidityBitmap::from_bytes(bytes, n)?
137            }
138            None => mask,
139        };
140        let new_storage = OwnedColumnarStorage::try_new(
141            storage.schema().clone(),
142            storage.columns().to_vec(),
143            Some(combined),
144            storage.weights().map(Arc::from),
145        )?;
146        Ok(Self::new(new_storage))
147    }
148
149    /// Append a continuous float64 covariate; preserve existing columns/mask/weights.
150    ///
151    /// # Errors
152    ///
153    /// Length mismatch or schema construction failure.
154    pub fn with_appended_float(
155        &self,
156        name: &str,
157        values: Arc<[f64]>,
158    ) -> Result<(Self, VariableId), DataError> {
159        let n = self.row_count();
160        if values.len() != n {
161            return Err(DataError::LengthMismatch {
162                expected: n,
163                actual: values.len(),
164                context: "appended float column",
165            });
166        }
167        let storage = self.storage();
168        let mut builder = CausalSchemaBuilder::new();
169        for v in storage.schema().variables() {
170            builder
171                .add_variable(
172                    Arc::clone(&v.name),
173                    v.value_type.clone(),
174                    v.role_hints,
175                    v.unit.clone(),
176                    v.category_domain,
177                    v.measurement.clone(),
178                )
179                .map_err(|e| DataError::Schema(e.to_string()))?;
180        }
181        builder
182            .add_variable(
183                name,
184                ValueType::Continuous,
185                SmallRoleSet::from_hint(RoleHint::Context),
186                None,
187                None,
188                MeasurementSpec::default(),
189            )
190            .map_err(|e| DataError::Schema(e.to_string()))?;
191        let schema = builder.build().map_err(|e| DataError::Schema(e.to_string()))?;
192        let new_id = VariableId::from_raw(u32::try_from(schema.len() - 1).map_err(|_| {
193            DataError::InvalidArgument { message: "schema exceeds VariableId range".into() }
194        })?);
195        let mut cols: Vec<OwnedColumn> = storage.columns().to_vec();
196        cols.push(OwnedColumn::Float64(Float64Column::new(
197            new_id,
198            values,
199            ValidityBitmap::all_valid(n),
200        )?));
201        let storage = OwnedColumnarStorage::try_new(
202            schema,
203            cols,
204            storage.analysis_mask().cloned(),
205            storage.weights().map(Arc::from),
206        )?;
207        Ok((Self::new(storage), new_id))
208    }
209}