///|
/// A named view over one numeric dataset column.
pub struct CausalColumnView {
name : String
index : Int
values : Array[Double]
finite_count : Int
missing_count : Int
mean : Double
standard_deviation : Double
minimum : Double
maximum : Double
}
///|
/// A row-level view used by scoring and review workflows.
pub struct CausalRowView {
index : Int
covariates : Array[Double]
treatment : Bool
outcome : Double
finite : Bool
score : Double
}
///|
/// Deterministic train/validation split indices.
pub struct CausalDataSplit {
train : Array[Int]
validation : Array[Int]
train_fraction : Double
seed : UInt64
passes : Bool
}
///|
/// Cross-fitting fold assignment and size diagnostics.
pub struct CausalFoldAssignment {
folds : Array[Int]
sizes : Array[Int]
requested_folds : Int
passes : Bool
}
///|
/// Treatment-stratified group summary.
pub struct CausalStratumSummary {
stratum : Int
count : Int
treated : Int
control : Int
treatment_rate : Double
outcome_mean : Double
}
///|
/// Dataset view diagnostics used by audit and model preparation.
pub struct CausalDataViewSummary {
rows : Int
columns : Int
finite_rows : Int
treated : Int
control : Int
missing_cells : Int
constant_columns : Int
outcome_mean : Double
outcome_standard_deviation : Double
passes : Bool
}
///|
/// Returns a sorted copy without mutating caller-owned values.
pub fn causal_view_sorted(values : Array[Double]) -> Array[Double] {
let result = values.copy()
for i in 1.. 0 && result[cursor - 1] > value {
result[cursor] = result[cursor - 1]
cursor -= 1
}
result[cursor] = value
}
result
}
///|
/// Creates a column view from a matrix with a stable fallback for short rows.
pub fn causal_column_view(
matrix : Array[Array[Double]],
index : Int,
name? : String = "",
fill? : Double = 0.0,
) -> CausalColumnView {
let values : Array[Double] = Array::new(capacity=matrix.length())
for row in matrix {
values.push(
if index >= 0 && index < row.length() {
row[index]
} else {
fill
},
)
}
let finite = causal_finite_values(values)
{
name: if name == "" {
"x\{index}"
} else {
name
},
index,
values,
finite_count: finite.length(),
missing_count: values.length() - finite.length(),
mean: mean_or(finite, 0.0),
standard_deviation: if finite.length() > 1 {
std_dev(finite)
} else {
0.0
},
minimum: causal_minimum(finite),
maximum: causal_maximum(finite),
}
}
///|
/// Creates all column views in matrix order.
pub fn causal_column_views(
matrix : Array[Array[Double]],
names? : Array[String] = [],
) -> Array[CausalColumnView] {
let width = if matrix.length() == 0 {
names.length()
} else {
let mut maximum = 0
for row in matrix {
if row.length() > maximum {
maximum = row.length()
}
}
maximum.max(names.length())
}
let result : Array[CausalColumnView] = Array::new(capacity=width)
for index in 0.. CausalRowView {
let valid = index >= 0 && index < dataset.n()
let covariates = if valid { dataset.covariates[index].copy() } else { [] }
let treatment = if valid { dataset.treatment[index] } else { false }
let outcome = if valid { dataset.outcome[index] } else { 0.0 }
let finite = valid && is_finite(outcome) && finite_array(covariates)
{
index,
covariates,
treatment,
outcome,
finite,
score: if is_finite(score) {
score
} else {
0.0
},
}
}
///|
/// Returns all row views in dataset order.
pub fn causal_row_views(dataset : CausalDataset) -> Array[CausalRowView] {
let result : Array[CausalRowView] = Array::new(capacity=dataset.n())
for index in 0.. Array[Int] {
let order : Array[Int] = Array::new(capacity=values.length())
for index in 0.. 0 {
let previous = order[cursor - 1]
let previous_value = if is_finite(values[previous]) {
values[previous]
} else {
1.0e300
}
if previous_value <= value {
break
}
order[cursor] = previous
cursor -= 1
}
order[cursor] = index
}
order
}
///|
/// Splits indices into train and validation partitions reproducibly.
pub fn causal_train_validation_split(
rows : Int,
train_fraction? : Double = 0.8,
seed? : UInt64 = 20260819,
) -> CausalDataSplit {
let n = rows.max(0)
let fraction = clamp(train_fraction, 0.0, 1.0)
let target = (fraction * n.to_double()).to_int().min(n)
let rng = RandomState::new(seed)
let order : Array[Int] = Array::new(capacity=n)
for index in 0.. 1 {
let offset = (rng.uniform() * remaining.to_double())
.to_int()
.min(remaining - 1)
let swap = i + offset
let temporary = order[i]
order[i] = order[swap]
order[swap] = temporary
}
}
let train : Array[Int] = Array::new(capacity=target)
let validation : Array[Int] = Array::new(capacity=n - target)
for i in 0.. CausalDataSplit {
let fraction = clamp(train_fraction, 0.0, 1.0)
let treated : Array[Int] = Array::new()
let control : Array[Int] = Array::new()
for index in 0.. CausalFoldAssignment {
let n = rows.max(0)
let folds_count = requested_folds.max(1).min(n.max(1))
let split = causal_train_validation_split(n, train_fraction=1.0, seed~)
let labels = Array::make(n, 0)
let sizes = Array::make(folds_count, 0)
for i in 0.. CausalDataSplit {
let fold = held_out_fold.max(0).min(assignment.requested_folds - 1)
let train : Array[Int] = Array::new()
let validation : Array[Int] = Array::new()
for index in 0.. Array[CausalDataSplit] {
let assignment = causal_fold_assignment(rows, requested_folds, seed~)
let result : Array[CausalDataSplit] = Array::new(
capacity=assignment.requested_folds,
)
for fold in 0.. Array[Array[Int]] {
let n = rows.max(0)
let count = replicates.max(0)
let rng = RandomState::new(seed)
let result : Array[Array[Int]] = Array::new(capacity=count)
for _ in 0.. 0 {
sample.push(index)
}
}
result.push(sample)
}
result
}
///|
/// Resamples a causal dataset using one bootstrap index vector.
pub fn causal_bootstrap_dataset(
dataset : CausalDataset,
indices : Array[Int],
) -> CausalDataset {
dataset.select(indices)
}
///|
/// Returns the finite row mask for a rectangular dataset.
pub fn causal_finite_row_mask(dataset : CausalDataset) -> Array[Bool] {
let result : Array[Bool] = Array::new(capacity=dataset.n())
for index in 0.. Array[Int] {
let result : Array[Int] = Array::new()
for index in 0.. CausalDataset {
dataset.select(causal_mask_indices(causal_finite_row_mask(dataset)))
}
///|
/// Assigns rows to integer strata using a finite score and fixed width.
pub fn causal_score_strata(
scores : Array[Double],
strata : Int,
minimum? : Double = 0.0,
maximum? : Double = 1.0,
) -> Array[Int] {
let count = strata.max(1)
let lower = minimum.min(maximum)
let upper = maximum.max(minimum)
let width = (upper - lower).max(1.0e-12)
scores.map(fn(score) {
if !is_finite(score) {
0
} else {
((score - lower) / width * count.to_double())
.to_int()
.max(0)
.min(count - 1)
}
})
}
///|
/// Summarizes treatment and outcome within integer strata.
pub fn causal_stratum_summaries(
strata : Array[Int],
treatment : Array[Bool],
outcome : Array[Double],
stratum_count : Int,
) -> Array[CausalStratumSummary] {
let count = stratum_count.max(1)
let sizes = Array::make(count, 0)
let treated = Array::make(count, 0)
let outcome_sum = Array::make(count, 0.0)
let n = strata.length().min(treatment.length()).min(outcome.length())
for i in 0.. CausalDataset {
let n = dataset.n().min(scores.length())
let order = causal_order_indices(scores[:n].to_owned())
dataset.select(order)
}
///|
/// Selects rows by a deterministic score quantile interval.
pub fn causal_quantile_slice(
dataset : CausalDataset,
scores : Array[Double],
lower_probability : Double,
upper_probability : Double,
) -> CausalDataset {
let finite = causal_finite_values(scores)
if finite.length() == 0 {
return CausalDataset::new([], [], [])
}
let lower = quantile(finite, clamp(lower_probability, 0.0, 1.0))
let upper = quantile(finite, clamp(upper_probability, 0.0, 1.0))
causal_dataset_filter_score(dataset, scores, lower, upper)
}
///|
/// Builds a complete dataset view summary.
pub fn causal_data_view_summary(
dataset : CausalDataset,
) -> CausalDataViewSummary {
let mask = causal_finite_row_mask(dataset)
let finite_rows = mask.fold(init=0, fn(total, value) {
if value {
total + 1
} else {
total
}
})
let missing_cells = causal_matrix_missing(dataset.covariates)
let constant_columns = causal_constant_columns(dataset.covariates)
{
rows: dataset.n(),
columns: dataset.p(),
finite_rows,
treated: dataset.treated_count(),
control: dataset.control_count(),
missing_cells,
constant_columns,
outcome_mean: mean_or(causal_finite_values(dataset.outcome), 0.0),
outcome_standard_deviation: std_dev(causal_finite_values(dataset.outcome)),
passes: dataset.is_valid() &&
finite_rows == dataset.n() &&
dataset.treated_count() > 0 &&
dataset.control_count() > 0,
}
}
///|
/// Returns a compact data-view summary vector.
pub fn causal_data_view_summary_vector(
summary : CausalDataViewSummary,
) -> Array[Double] {
[
summary.rows.to_double(),
summary.columns.to_double(),
summary.finite_rows.to_double(),
summary.treated.to_double(),
summary.control.to_double(),
summary.missing_cells.to_double(),
summary.constant_columns.to_double(),
summary.outcome_mean,
summary.outcome_standard_deviation,
if summary.passes {
1.0
} else {
0.0
},
]
}
///|
/// Computes a stable fingerprint for split indices.
pub fn causal_split_fingerprint(split : CausalDataSplit) -> UInt64 {
let rows : Array[Array[Double]] = Array::new()
rows.push([split.train_fraction, split.seed.to_double()])
for index in split.train {
rows.push([1.0, index.to_double()])
}
for index in split.validation {
rows.push([0.0, index.to_double()])
}
matrix_checksum(rows)
}
///|
/// Computes a stable fingerprint for fold assignments.
pub fn causal_fold_fingerprint(assignment : CausalFoldAssignment) -> UInt64 {
let rows : Array[Array[Double]] = Array::new()
for index in 0..