use rusqlite::Connection;
use crate::error::{DataError, Result};
use crate::ingest::quote_ident;
use crate::store::{ColumnDtype, ColumnSummary, NumericDescribe, TableSummary};
pub fn summarize(conn: &Connection, table: &str) -> Result<TableSummary> {
let quoted = quote_ident(table);
let columns: Vec<(String, String)> = {
let mut stmt = conn.prepare(&format!("PRAGMA table_info({quoted})"))?;
let rows = stmt.query_map([], |row| {
Ok((row.get::<_, String>(1)?, row.get::<_, String>(2)?))
})?;
rows.collect::<rusqlite::Result<Vec<_>>>()?
};
if columns.is_empty() && !table_exists(conn, table)? {
return Err(DataError::NoSuchTable(table.to_string()));
}
let row_count: u64 = conn.query_row(&format!("SELECT COUNT(*) FROM {quoted}"), [], |row| {
row.get::<_, i64>(0).map(|n| n.max(0) as u64)
})?;
let mut summaries = Vec::with_capacity(columns.len());
for (name, decl) in &columns {
let dtype = ColumnDtype::from_decl_type(decl);
let qcol = quote_ident(name);
let non_null: i64 =
conn.query_row(&format!("SELECT COUNT({qcol}) FROM {quoted}"), [], |row| {
row.get(0)
})?;
let distinct_count: i64 = conn.query_row(
&format!("SELECT COUNT(DISTINCT {qcol}) FROM {quoted}"),
[],
|row| row.get(0),
)?;
let null_count = row_count.saturating_sub(non_null.max(0) as u64);
let numeric = match dtype {
ColumnDtype::Integer | ColumnDtype::Real => {
let values = collect_numeric(conn, "ed, &qcol)?;
Some(describe(&values))
}
ColumnDtype::Text => None,
};
summaries.push(ColumnSummary {
name: name.clone(),
dtype,
null_count,
distinct_count: distinct_count.max(0) as u64,
numeric,
});
}
Ok(TableSummary {
table: table.to_string(),
row_count,
columns: summaries,
})
}
fn table_exists(conn: &Connection, table: &str) -> Result<bool> {
let count: i64 = conn.query_row(
"SELECT COUNT(*) FROM sqlite_master WHERE type IN ('table','view') AND name = ?1",
[table],
|row| row.get(0),
)?;
Ok(count > 0)
}
fn collect_numeric(conn: &Connection, quoted_table: &str, quoted_col: &str) -> Result<Vec<f64>> {
let mut stmt = conn.prepare(&format!(
"SELECT {quoted_col} FROM {quoted_table} WHERE {quoted_col} IS NOT NULL ORDER BY {quoted_col}"
))?;
let rows = stmt.query_map([], |row| row.get::<_, f64>(0))?;
Ok(rows.collect::<rusqlite::Result<Vec<_>>>()?)
}
pub fn describe(sorted: &[f64]) -> NumericDescribe {
let count = sorted.len() as u64;
if sorted.is_empty() {
return NumericDescribe {
count: 0,
mean: 0.0,
std: 0.0,
min: 0.0,
q25: 0.0,
q50: 0.0,
q75: 0.0,
max: 0.0,
};
}
let n = sorted.len();
let sum: f64 = sorted.iter().sum();
let mean = sum / n as f64;
let std = if n < 2 {
0.0
} else {
let ss: f64 = sorted.iter().map(|v| (v - mean).powi(2)).sum();
(ss / (n as f64 - 1.0)).sqrt()
};
NumericDescribe {
count,
mean,
std,
min: sorted[0],
q25: quantile_sorted(sorted, 0.25),
q50: quantile_sorted(sorted, 0.50),
q75: quantile_sorted(sorted, 0.75),
max: sorted[n - 1],
}
}
pub fn quantile_sorted(sorted: &[f64], q: f64) -> f64 {
if sorted.is_empty() {
return 0.0;
}
let n = sorted.len();
if n == 1 {
return sorted[0];
}
let pos = (n as f64 - 1.0) * q;
let lower = pos.floor() as usize;
let frac = pos - lower as f64;
if lower + 1 < n {
sorted[lower] + frac * (sorted[lower + 1] - sorted[lower])
} else {
sorted[n - 1]
}
}
#[cfg(test)]
mod tests {
use super::*;
const EPS: f64 = 1e-9;
#[test]
fn describe_matches_pandas_1_2_3_4() {
let d = describe(&[1.0, 2.0, 3.0, 4.0]);
assert_eq!(d.count, 4);
assert!((d.mean - 2.5).abs() < EPS);
assert!((d.std - 1.290_994_448_735_805_6).abs() < EPS);
assert!((d.min - 1.0).abs() < EPS);
assert!((d.q25 - 1.75).abs() < EPS);
assert!((d.q50 - 2.5).abs() < EPS);
assert!((d.q75 - 3.25).abs() < EPS);
assert!((d.max - 4.0).abs() < EPS);
}
#[test]
fn describe_single_value_has_zero_std() {
let d = describe(&[7.0]);
assert_eq!(d.count, 1);
assert!((d.mean - 7.0).abs() < EPS);
assert_eq!(d.std, 0.0);
assert!((d.q25 - 7.0).abs() < EPS);
assert!((d.q50 - 7.0).abs() < EPS);
assert!((d.q75 - 7.0).abs() < EPS);
}
#[test]
fn describe_empty_is_all_zero() {
let d = describe(&[]);
assert_eq!(d.count, 0);
assert_eq!(d.mean, 0.0);
assert_eq!(d.std, 0.0);
assert_eq!(d.max, 0.0);
}
#[test]
fn quantile_interpolates_linearly() {
let v = [10.0, 20.0, 30.0, 40.0, 50.0];
assert!((quantile_sorted(&v, 0.0) - 10.0).abs() < EPS);
assert!((quantile_sorted(&v, 1.0) - 50.0).abs() < EPS);
assert!((quantile_sorted(&v, 0.25) - 20.0).abs() < EPS);
assert!((quantile_sorted(&v, 0.1) - 14.0).abs() < EPS);
}
}