use crate::core::error::{Error, Result};
use crate::dataframe::base::DataFrame;
use std::collections::HashMap;
pub trait AstypeErrorsExt {
fn astype_errors(&self, column: &str, dtype: &str, errors: &str) -> Result<DataFrame>;
}
impl AstypeErrorsExt for DataFrame {
fn astype_errors(&self, column: &str, dtype: &str, errors: &str) -> Result<DataFrame> {
super::functions_2_impl_part2::astype_with_errors(self, column, dtype, errors)
}
}
pub trait ValueCountsOptionsExt {
fn value_counts_with_options(
&self,
column: &str,
normalize: bool,
sort: bool,
ascending: bool,
dropna: bool,
) -> Result<Vec<(String, f64)>>;
}
impl ValueCountsOptionsExt for DataFrame {
fn value_counts_with_options(
&self,
column: &str,
normalize: bool,
sort: bool,
ascending: bool,
dropna: bool,
) -> Result<Vec<(String, f64)>> {
if !self.contains_column(column) {
return Err(Error::ColumnNotFound(column.to_string()));
}
let raw_values = self.get_column_string_values(column)?;
let values: Vec<String> = if dropna {
raw_values
.into_iter()
.filter(|v| !v.is_empty() && v != "NaN" && v != "nan")
.collect()
} else {
raw_values
};
let mut counts: HashMap<String, usize> = HashMap::new();
let mut order: Vec<String> = Vec::new();
for v in values {
if !counts.contains_key(&v) {
order.push(v.clone());
}
*counts.entry(v).or_insert(0) += 1;
}
let total: usize = counts.values().sum();
let mut result: Vec<(String, f64)> = order
.into_iter()
.map(|v| {
let count = counts.get(&v).copied().unwrap_or(0);
let reported = if normalize {
if total == 0 {
0.0
} else {
count as f64 / total as f64
}
} else {
count as f64
};
(v, reported)
})
.collect();
if sort {
result.sort_by(|a, b| {
let cmp = a.1.total_cmp(&b.1);
if ascending {
cmp
} else {
cmp.reverse()
}
});
}
Ok(result)
}
}
pub trait BetweenInclusiveExt {
fn between_inclusive(
&self,
column: &str,
lower: f64,
upper: f64,
inclusive: &str,
) -> Result<Vec<bool>>;
}
impl BetweenInclusiveExt for DataFrame {
fn between_inclusive(
&self,
column: &str,
lower: f64,
upper: f64,
inclusive: &str,
) -> Result<Vec<bool>> {
let (lower_ok, upper_ok): (fn(f64, f64) -> bool, fn(f64, f64) -> bool) = match inclusive {
"both" => (|v, b| v >= b, |v, b| v <= b),
"neither" => (|v, b| v > b, |v, b| v < b),
"left" => (|v, b| v >= b, |v, b| v < b),
"right" => (|v, b| v > b, |v, b| v <= b),
other => {
return Err(Error::InvalidValue(format!(
"inclusive must be one of 'both', 'neither', 'left', 'right', got '{}'",
other
)))
}
};
let values = self.get_column_numeric_values(column)?;
Ok(values
.iter()
.map(|&v| !v.is_nan() && lower_ok(v, lower) && upper_ok(v, upper))
.collect())
}
}
pub trait DescribePercentilesExt {
fn describe_percentiles(&self, column: &str, percentiles: &[f64])
-> Result<Vec<(String, f64)>>;
}
impl DescribePercentilesExt for DataFrame {
fn describe_percentiles(
&self,
column: &str,
percentiles: &[f64],
) -> Result<Vec<(String, f64)>> {
for &p in percentiles {
if !(0.0..=1.0).contains(&p) {
return Err(Error::InvalidValue(format!(
"percentiles must be between 0 and 1, got {}",
p
)));
}
}
let raw_values = self.get_column_numeric_values(column)?;
if raw_values.is_empty() {
return Err(Error::Empty("Cannot describe empty column".to_string()));
}
let values: Vec<f64> = raw_values.into_iter().filter(|v| !v.is_nan()).collect();
let count = values.len();
let mut requested: Vec<f64> = percentiles.to_vec();
requested.push(0.5);
requested.sort_by(|a, b| a.total_cmp(b));
requested.dedup();
let percentile_label = |p: f64| -> String {
let pct = p * 100.0;
if (pct - pct.round()).abs() < 1e-9 {
format!("{}%", pct.round() as i64)
} else {
format!("{}%", pct)
}
};
let mut result = Vec::with_capacity(4 + requested.len());
result.push(("count".to_string(), count as f64));
if count == 0 {
result.push(("mean".to_string(), f64::NAN));
result.push(("std".to_string(), f64::NAN));
result.push(("min".to_string(), f64::NAN));
for p in &requested {
result.push((percentile_label(*p), f64::NAN));
}
result.push(("max".to_string(), f64::NAN));
return Ok(result);
}
let n = count as f64;
let mean = values.iter().sum::<f64>() / n;
let std = if count < 2 {
f64::NAN
} else {
let variance: f64 = values.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / (n - 1.0);
variance.sqrt()
};
let mut sorted = values.clone();
sorted.sort_by(|a, b| a.total_cmp(b));
result.push(("mean".to_string(), mean));
result.push(("std".to_string(), std));
result.push(("min".to_string(), sorted[0]));
for p in &requested {
result.push((
percentile_label(*p),
linear_interpolated_percentile(&sorted, *p),
));
}
result.push(("max".to_string(), sorted[count - 1]));
Ok(result)
}
}
fn linear_interpolated_percentile(sorted: &[f64], p: f64) -> f64 {
let n = sorted.len();
if n == 1 {
return sorted[0];
}
let idx = p * (n - 1) as f64;
let lower = idx.floor() as usize;
let upper = idx.ceil() as usize;
if lower == upper {
sorted[lower]
} else {
let weight = idx - lower as f64;
sorted[lower] * (1.0 - weight) + sorted[upper] * weight
}
}