///|
/// Columnar table schema for analysis pipelines.
pub struct TableSchema {
names : Array[String]
types : Array[String]
required : Array[Bool]
version : String
}
///|
/// Numeric table profile.
pub struct TableProfile {
rows : Int
columns : Int
complete_rows : Int
missing_cells : Int
duplicate_rows : Int
schema_fingerprint : UInt64
data_fingerprint : UInt64
passes : Bool
}
///|
/// Group-level aggregate with count, mean and dispersion.
pub struct GroupAggregate {
key : Int
count : Int
total : Double
mean : Double
variance : Double
minimum : Double
maximum : Double
}
///|
/// Result of a keyed inner or left join.
pub struct TableJoinResult {
left_indices : Array[Int]
right_indices : Array[Int]
matched : Int
unmatched_left : Int
unmatched_right : Int
passes : Bool
}
///|
/// A deterministic rolling summary for one key and time index.
pub struct TableWindowSummary {
key : Int
time : Int
count : Int
mean : Double
standard_deviation : Double
minimum : Double
maximum : Double
}
///|
fn table_unique_int(values : Array[Int]) -> Array[Int] {
let result : Array[Int] = Array::new()
for value in values {
if !result.contains(value) {
result.push(value)
}
}
result
}
///|
fn table_min(values : Array[Double]) -> Double {
if values.length() == 0 {
return 0.0
}
let mut result = values[0]
for value in values[1:] {
if value < result {
result = value
}
}
result
}
///|
fn table_max(values : Array[Double]) -> Double {
if values.length() == 0 {
return 0.0
}
let mut result = values[0]
for value in values[1:] {
if value > result {
result = value
}
}
result
}
///|
/// Creates a schema with stable defaults.
pub fn table_schema(
names : Array[String],
types? : Array[String] = [],
required? : Array[Bool] = [],
version? : String = "1",
) -> TableSchema {
let actual_types : Array[String] = Array::new(capacity=names.length())
let actual_required : Array[Bool] = Array::new(capacity=names.length())
for i in 0.. UInt64 {
let rows : Array[Array[Double]] = Array::new()
for i in 0.. Array[Array[Double]] {
let result : Array[Array[Double]] = Array::new(capacity=table.length())
for row in table {
let selected = Array::new(capacity=indexes.length())
for index in indexes {
if index >= 0 && index < row.length() {
selected.push(row[index])
}
}
result.push(selected)
}
result
}
///|
/// Filters rows using an explicit mask.
pub fn table_filter_rows(
table : Array[Array[Double]],
mask : Array[Bool],
) -> Array[Array[Double]] {
let result : Array[Array[Double]] = Array::new()
for i in 0.. Array[Array[Double]] {
let result : Array[Array[Double]] = Array::new()
for row in table {
if column >= 0 && column < row.length() {
let passes = if keep_above {
row[column] >= threshold
} else {
row[column] <= threshold
}
if passes {
result.push(row)
}
}
}
result
}
///|
/// Adds a numeric column, truncating to the existing row count when needed.
pub fn table_add_column(
table : Array[Array[Double]],
column : Array[Double],
) -> Array[Array[Double]] {
let result : Array[Array[Double]] = Array::new(capacity=table.length())
for i in 0.. Array[Array[Double]] {
let result = table.copy()
for i in 0..= 0 &&
column_index < result[i].length() &&
i < values.length() {
result[i][column_index] = values[i]
}
}
result
}
///|
/// Profiles a numeric table and its schema.
pub fn profile_table(
table : Array[Array[Double]],
schema : TableSchema,
) -> TableProfile {
let missing = assess_matrix(table).missing_cells
let complete = complete_row_count(table)
let duplicates = duplicate_row_flags(table)
.filter(fn(value) { value })
.length()
let columns = if table.length() == 0 { 0 } else { table[0].length() }
let passes = table.length() == complete &&
duplicates == 0 &&
schema.names.length() == columns
{
rows: table.length(),
columns,
complete_rows: complete,
missing_cells: missing,
duplicate_rows: duplicates,
schema_fingerprint: table_schema_fingerprint(schema),
data_fingerprint: matrix_checksum(table),
passes,
}
}
///|
/// Counts rows without non-finite cells.
pub fn complete_row_count(table : Array[Array[Double]]) -> Int {
let mut result = 0
for row in table {
let mut complete = true
for value in row {
if !is_finite(value) {
complete = false
}
}
if complete {
result += 1
}
}
result
}
///|
/// Aggregates a value by integer group key.
pub fn group_aggregate(
keys : Array[Int],
values : Array[Double],
) -> Array[GroupAggregate] {
let n = keys.length().min(values.length())
let groups = table_unique_int(keys[:n].to_owned())
let result : Array[GroupAggregate] = Array::new(capacity=groups.length())
for key in groups {
let selected = Array::new()
for i in 0.. Array[Array[Double]] {
let n = keys.length().min(values.length()).min(weights.length())
let groups = table_unique_int(keys[:n].to_owned())
let result : Array[Array[Double]] = Array::new(capacity=groups.length())
for key in groups {
let mut numerator = 0.0
let mut denominator = 0.0
for i in 0.. Array[Array[Double]] {
let n = keys.length().min(treatment.length()).min(outcomes.length())
let groups = table_unique_int(keys[:n].to_owned())
let result : Array[Array[Double]] = Array::new(capacity=groups.length())
for key in groups {
let treated = Array::new()
let control = Array::new()
for i in 0.. TableJoinResult {
let left_indices : Array[Int] = Array::new()
let right_indices : Array[Int] = Array::new()
let matched_right : Array[Int] = Array::new()
for i in 0..= 0 {
left_indices.push(i)
right_indices.push(match_index)
matched_right.push(match_index)
}
}
{
left_indices,
right_indices,
matched: left_indices.length(),
unmatched_left: left_keys.length() - left_indices.length(),
unmatched_right: right_keys.length() - matched_right.length(),
passes: left_indices.length() > 0 ||
(left_keys.length() == 0 && right_keys.length() == 0),
}
}
///|
/// Performs a left keyed join and marks absent right rows with -1.
pub fn table_left_join(
left_keys : Array[Int],
right_keys : Array[Int],
) -> TableJoinResult {
let left_indices : Array[Int] = Array::new()
let right_indices : Array[Int] = Array::new()
let used : Array[Int] = Array::new()
for i in 0..= 0 {
used.push(match_index)
}
}
{
left_indices,
right_indices,
matched: used.length(),
unmatched_left: left_keys.length() - used.length(),
unmatched_right: right_keys.length() - used.length(),
passes: true,
}
}
///|
/// Calculates a lag within each integer group after assuming row order is time order.
pub fn group_lag(
keys : Array[Int],
values : Array[Double],
lag : Int,
fill? : Double = 0.0,
) -> Array[Double] {
let result = Array::make(keys.length().min(values.length()), fill)
if lag <= 0 {
return values[:result.length()].to_owned()
}
for i in lag.. Array[Double] {
let n = keys.length().min(values.length())
let result = Array::make(n, 0.0)
let width = if window > 0 { window } else { 1 }
for i in 0.. width { i + 1 - width } else { 0 }
let end = i + 1
for j in start.. Array[Double] {
let n = keys.length().min(values.length())
let result = Array::make(n, 0.0)
let width = if window > 0 { window } else { 1 }
for i in 0.. width { i + 1 - width } else { 0 }
let end = i + 1
for j in start.. Array[TableWindowSummary] {
let n = keys.length().min(times.length()).min(values.length())
let result : Array[TableWindowSummary] = Array::new(capacity=n)
let width = if window > 0 { window } else { 1 }
for i in 0..= times[i] - width + 1 {
selected.push(values[j])
}
}
result.push({
key: keys[i],
time: times[i],
count: selected.length(),
mean: mean_or(selected, 0.0),
standard_deviation: std_dev(selected),
minimum: table_min(selected),
maximum: table_max(selected),
})
}
result
}