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