use crate::traits::{Error, Fit, Result, Transform};
use polars::prelude::*;
#[derive(Clone, Debug, PartialEq)]
pub enum ColumnType {
Numeric,
Categorical,
HighCardinality,
}
pub struct AutoTypeDetector {
fitted: bool,
cat_threshold: usize,
hash_buckets: usize,
column_types: Option<Vec<(String, ColumnType)>>,
}
impl AutoTypeDetector {
pub fn new() -> Self {
Self {
fitted: false,
cat_threshold: 20,
hash_buckets: 100,
column_types: None,
}
}
pub fn cat_threshold(mut self, value: usize) -> Self {
self.cat_threshold = value;
self
}
pub fn hash_buckets(mut self, value: usize) -> Self {
self.hash_buckets = value;
self
}
pub fn column_types(&self) -> Option<&[(String, ColumnType)]> {
self.column_types.as_deref()
}
}
impl Default for AutoTypeDetector {
fn default() -> Self {
Self::new()
}
}
impl Fit<DataFrame, DataFrame> for AutoTypeDetector {
type Output = ();
fn fit(&mut self, x: DataFrame, _y: DataFrame) -> Result<()> {
let mut types = Vec::new();
for col in x.columns() {
let name = col.name().to_string();
let dtype = col.dtype();
let detected = match dtype {
dt if dt == &DataType::Float64
|| dt == &DataType::Int64
|| dt == &DataType::Int32 =>
{
ColumnType::Numeric
}
dt if dt == &DataType::String => {
let ca = col.as_materialized_series().str().map_err(|_| {
Error::Computation(format!("could not read string column '{}'", name))
})?;
let n_unique = ca
.iter()
.flatten()
.collect::<std::collections::HashSet<_>>()
.len();
if n_unique < self.cat_threshold {
ColumnType::Categorical
} else {
ColumnType::HighCardinality
}
}
_ => ColumnType::Numeric,
};
types.push((name, detected));
}
self.column_types = Some(types);
self.fitted = true;
Ok(())
}
}
impl Transform<DataFrame> for AutoTypeDetector {
type Output = DataFrame;
fn transform(&self, x: DataFrame) -> Result<DataFrame> {
if !self.fitted {
return Err(Error::NotFitted("AutoTypeDetector".into()));
}
let types = self.column_types.as_ref().unwrap();
let mut parts: Vec<DataFrame> = Vec::new();
let mut numeric_cols: Vec<Column> = Vec::new();
for (name, ctype) in types {
match ctype {
ColumnType::Numeric => {
if let Ok(col) = x.column(name.as_str()) {
numeric_cols.push(col.clone());
}
}
ColumnType::Categorical => {
let subset = x
.clone()
.select([name.as_str()])
.map_err(|e| Error::Computation(e.to_string()))?;
use crate::preprocessing::encoder::OneHotEncoder;
let mut enc = OneHotEncoder::new();
enc.fit(subset.clone(), subset.clone()).map_err(|e| {
Error::Computation(format!("AutoType: one-hot failed on '{}': {}", name, e))
})?;
let encoded = enc.transform(subset).map_err(|e| {
Error::Computation(format!(
"AutoType: one-hot transform failed on '{}': {}",
name, e
))
})?;
if !encoded.columns().is_empty() {
parts.push(encoded);
}
}
ColumnType::HighCardinality => {
let subset = x
.clone()
.select([name.as_str()])
.map_err(|e| Error::Computation(e.to_string()))?;
use crate::preprocessing::feature_hasher::FeatureHasher;
let mut fh = FeatureHasher::new(&[name.as_str()], self.hash_buckets);
fh.fit(subset.clone(), subset.clone()).map_err(|e| {
Error::Computation(format!("AutoType: hashing failed on '{}': {}", name, e))
})?;
let hashed = fh.transform(subset).map_err(|e| {
Error::Computation(format!(
"AutoType: hashing transform failed on '{}': {}",
name, e
))
})?;
if !hashed.columns().is_empty() {
parts.push(hashed);
}
}
}
}
if !numeric_cols.is_empty() {
let h = x.height();
parts.push(
DataFrame::new(h, numeric_cols).map_err(|e| Error::Computation(e.to_string()))?,
);
}
if parts.is_empty() {
return Err(Error::Computation(
"AutoTypeDetector produced no output columns.".into(),
));
}
let mut result = parts.remove(0);
for other in &parts {
let cols = other.columns().to_vec();
result = result
.hstack(&cols)
.map_err(|e| Error::Computation(e.to_string()))?;
}
Ok(result)
}
}