1use crate::categorical_kind::CategoricalTransformerKind;
2use crate::encoder::OneHotEncoder;
3use crate::error::{DatarustError, Result};
4use crate::matrix::{Matrix, StrMatrix};
5use crate::target_kind::TargetTransformerKind;
6use crate::traits::{default_input_names, CategoricalTransformer, FeatureNames, TargetTransformer};
7use crate::transformer_kind::TransformerKind;
8use crate::Transformer;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
12#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
13pub enum Remainder {
14 #[default]
16 Drop,
17 Passthrough,
22}
23
24#[derive(Debug, Clone)]
26#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
27pub enum ColumnSpec {
28 Numeric {
30 name: String,
32 columns: Vec<usize>,
34 transformer: TransformerKind,
36 },
37 Categorical {
39 name: String,
41 columns: Vec<usize>,
43 encoder: CategoricalTransformerKind,
45 },
46 Target {
49 name: String,
51 columns: Vec<usize>,
53 encoder: TargetTransformerKind,
55 },
56}
57
58#[derive(Debug, Clone)]
60pub struct Table {
61 pub numeric: Matrix,
63 pub categorical: StrMatrix,
65}
66
67impl Table {
68 pub fn new(numeric: Matrix, categorical: StrMatrix) -> Result<Self> {
70 if numeric.nrows() != categorical.nrows() {
71 return Err(DatarustError::ShapeMismatch {
72 expected: format!("{} rows", numeric.nrows()),
73 actual: format!("{} rows", categorical.nrows()),
74 });
75 }
76 Ok(Self {
77 numeric,
78 categorical,
79 })
80 }
81
82 pub fn from_numeric(numeric: Matrix) -> Self {
85 let cat = StrMatrix {
86 data: (0..numeric.nrows()).map(|_| vec![]).collect(),
87 };
88 Table {
89 numeric,
90 categorical: cat,
91 }
92 }
93
94 pub fn nrows(&self) -> usize {
96 self.numeric.nrows()
97 }
98}
99
100#[derive(Debug, Clone)]
107#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
108pub struct ColumnTransformer {
109 specs: Vec<ColumnSpec>,
110 remainder: Remainder,
111 #[cfg_attr(feature = "serde", serde(default))]
113 consumed_numeric: Vec<usize>,
114 #[cfg_attr(feature = "serde", serde(default))]
116 total_numeric_cols: usize,
117 #[cfg_attr(feature = "serde", serde(default))]
119 consumed_categorical: Vec<usize>,
120 #[cfg_attr(feature = "serde", serde(default))]
122 total_categorical_cols: usize,
123 #[cfg_attr(feature = "serde", serde(default))]
126 max_col_index: usize,
127 #[cfg_attr(feature = "serde", serde(default))]
129 remainder_cat_encoders: Vec<CategoricalTransformerKind>,
130 #[cfg_attr(feature = "serde", serde(default))]
132 remainder_cat_cols: Vec<usize>,
133 #[cfg_attr(feature = "serde", serde(default))]
134 fitted: bool,
135}
136
137impl ColumnTransformer {
138 pub fn new() -> Self {
140 Self {
141 specs: vec![],
142 remainder: Remainder::Drop,
143 consumed_numeric: vec![],
144 total_numeric_cols: 0,
145 consumed_categorical: vec![],
146 total_categorical_cols: 0,
147 max_col_index: 0,
148 remainder_cat_encoders: vec![],
149 remainder_cat_cols: vec![],
150 fitted: false,
151 }
152 }
153
154 pub fn remainder(mut self, r: Remainder) -> Self {
156 self.remainder = r;
157 self
158 }
159
160 pub fn add_numeric<S>(
162 mut self,
163 name: S,
164 columns: Vec<usize>,
165 transformer: TransformerKind,
166 ) -> Self
167 where
168 S: Into<String>,
169 {
170 self.specs.push(ColumnSpec::Numeric {
171 name: name.into(),
172 columns,
173 transformer,
174 });
175 self
176 }
177
178 pub fn add_categorical<S>(
181 mut self,
182 name: S,
183 columns: Vec<usize>,
184 encoder: CategoricalTransformerKind,
185 ) -> Self
186 where
187 S: Into<String>,
188 {
189 self.specs.push(ColumnSpec::Categorical {
190 name: name.into(),
191 columns,
192 encoder,
193 });
194 self
195 }
196
197 pub fn add_target<S>(
201 mut self,
202 name: S,
203 columns: Vec<usize>,
204 encoder: TargetTransformerKind,
205 ) -> Self
206 where
207 S: Into<String>,
208 {
209 self.specs.push(ColumnSpec::Target {
210 name: name.into(),
211 columns,
212 encoder,
213 });
214 self
215 }
216
217 fn validate_columns(table: &Table, cols: &[usize], categorical: bool) -> Result<()> {
218 let max = if categorical {
219 table.categorical.ncols()
220 } else {
221 table.numeric.ncols()
222 };
223 for &c in cols {
224 if c >= max {
225 return Err(DatarustError::InvalidInput(format!(
226 "column index {} out of range (max {})",
227 c, max
228 )));
229 }
230 }
231 Ok(())
232 }
233
234 fn extract_numeric_cols(table: &Table, cols: &[usize]) -> Result<Matrix> {
235 Self::validate_columns(table, cols, false)?;
236 let mut data = Vec::with_capacity(table.nrows());
237 for i in 0..table.nrows() {
238 let row: Vec<f64> = cols.iter().map(|&c| table.numeric.get(i, c)).collect();
239 data.push(row);
240 }
241 Matrix::new(data)
242 }
243
244 fn extract_categorical_cols(table: &Table, cols: &[usize]) -> Result<StrMatrix> {
245 Self::validate_columns(table, cols, true)?;
246 let mut data = Vec::with_capacity(table.nrows());
247 for i in 0..table.nrows() {
248 let row: Vec<String> = cols
249 .iter()
250 .map(|&c| table.categorical.get(i, c).to_string())
251 .collect();
252 data.push(row);
253 }
254 StrMatrix::new(data)
255 }
256
257 pub fn fit(&mut self, table: &Table) -> Result<()> {
263 if self
264 .specs
265 .iter()
266 .any(|s| matches!(s, ColumnSpec::Target { .. }))
267 {
268 return Err(DatarustError::InvalidInput(
269 "ColumnTransformer contains Target specs; use fit_with_target() instead".into(),
270 ));
271 }
272 self.fit_inner(table)
273 }
274
275 fn fit_inner(&mut self, table: &Table) -> Result<()> {
277 if self.specs.is_empty() {
278 return Err(DatarustError::InvalidInput("no column specs".into()));
279 }
280 let mut consumed_num_set = std::collections::HashSet::new();
281 let mut consumed_cat_set = std::collections::HashSet::new();
282 let mut consumed_num = Vec::new();
283 let mut consumed_cat = Vec::new();
284 for spec in self.specs.iter_mut() {
285 match spec {
286 ColumnSpec::Numeric {
287 name,
288 columns,
289 transformer,
290 } => {
291 let mut seen = std::collections::HashSet::new();
293 for &c in columns.iter() {
294 if !seen.insert(c) {
295 return Err(DatarustError::InvalidInput(format!(
296 "duplicate column index {} in numeric spec '{}'",
297 c, name
298 )));
299 }
300 }
301 for &c in columns.iter() {
303 if consumed_num_set.contains(&c) {
304 return Err(DatarustError::InvalidInput(format!(
305 "column index {} is already consumed by another numeric spec",
306 c
307 )));
308 }
309 }
310 let sub = Self::extract_numeric_cols(table, columns)?;
311 transformer.fit(&sub)?;
312 for &c in columns.iter() {
313 consumed_num_set.insert(c);
314 }
315 consumed_num.extend_from_slice(columns);
316 }
317 ColumnSpec::Categorical {
318 name,
319 columns,
320 encoder,
321 } => {
322 let mut seen = std::collections::HashSet::new();
324 for &c in columns.iter() {
325 if !seen.insert(c) {
326 return Err(DatarustError::InvalidInput(format!(
327 "duplicate column index {} in categorical spec '{}'",
328 c, name
329 )));
330 }
331 }
332 for &c in columns.iter() {
334 if consumed_cat_set.contains(&c) {
335 return Err(DatarustError::InvalidInput(format!(
336 "column index {} is already consumed by another categorical spec",
337 c
338 )));
339 }
340 }
341 let sub = Self::extract_categorical_cols(table, columns)?;
342 encoder.fit(&sub)?;
343 for &c in columns.iter() {
344 consumed_cat_set.insert(c);
345 }
346 consumed_cat.extend_from_slice(columns);
347 }
348 ColumnSpec::Target { .. } => {}
349 }
350 }
351 consumed_num.sort_unstable();
352 consumed_num.dedup();
353 consumed_cat.sort_unstable();
354 consumed_cat.dedup();
355 self.consumed_numeric = consumed_num;
356 self.total_numeric_cols = table.numeric.ncols();
357 self.consumed_categorical = consumed_cat;
358 self.total_categorical_cols = table.categorical.ncols();
359 self.max_col_index = self.total_numeric_cols.max(self.total_categorical_cols);
360
361 self.remainder_cat_encoders.clear();
363 self.remainder_cat_cols.clear();
364 if matches!(self.remainder, Remainder::Passthrough) {
365 let unused_cat: Vec<usize> = (0..table.categorical.ncols())
366 .filter(|c| self.consumed_categorical.binary_search(c).is_err())
367 .collect();
368 for &c in &unused_cat {
369 let mut enc = OneHotEncoder::new();
370 let sub = Self::extract_categorical_cols(table, &[c])?;
371 enc.fit(&sub)?;
372 self.remainder_cat_encoders
373 .push(CategoricalTransformerKind::OneHotEncoder(enc));
374 }
375 self.remainder_cat_cols = unused_cat;
376 }
377
378 self.fitted = true;
379 Ok(())
380 }
381
382 pub fn fit_transform_with_target(&mut self, table: &Table, y: &[f64]) -> Result<Matrix> {
388 self.fit_with_target(table, y)?;
389 self.transform(table)
390 }
391
392 pub fn fit_with_target(&mut self, table: &Table, y: &[f64]) -> Result<()> {
398 self.fit_inner(table)?;
399 for spec in self.specs.iter_mut() {
400 if let ColumnSpec::Target {
401 columns, encoder, ..
402 } = spec
403 {
404 let sub = Self::extract_categorical_cols(table, columns)?;
405 encoder.fit(&sub, y)?;
406 }
407 }
408 Ok(())
409 }
410
411 pub fn transform(&self, table: &Table) -> Result<Matrix> {
413 if !self.fitted {
414 return Err(DatarustError::NotFitted("ColumnTransformer".into()));
415 }
416 let mut blocks: Vec<Vec<Vec<f64>>> = Vec::new();
417 let mut block_cols: Vec<usize> = Vec::new();
418 for spec in &self.specs {
419 match spec {
420 ColumnSpec::Numeric {
421 columns,
422 transformer,
423 ..
424 } => {
425 let sub = Self::extract_numeric_cols(table, columns)?;
426 let t = transformer.transform(&sub)?;
427 block_cols.push(t.ncols());
428 blocks.push(t.into_rows());
429 }
430 ColumnSpec::Categorical {
431 columns, encoder, ..
432 } => {
433 let sub = Self::extract_categorical_cols(table, columns)?;
434 let t = encoder.transform(&sub)?;
435 block_cols.push(t.ncols());
436 blocks.push(t.into_rows());
437 }
438 ColumnSpec::Target {
439 columns, encoder, ..
440 } => {
441 let sub = Self::extract_categorical_cols(table, columns)?;
442 let t = encoder.transform(&sub)?;
443 block_cols.push(t.ncols());
444 blocks.push(t.into_rows());
445 }
446 }
447 }
448 if matches!(self.remainder, Remainder::Passthrough) {
450 let nrows = table.nrows();
451 let remainder_cols: Vec<usize> = (0..table.numeric.ncols())
452 .filter(|c| self.consumed_numeric.binary_search(c).is_err())
453 .collect();
454 if !remainder_cols.is_empty() {
455 let mut rem = vec![vec![0.0; remainder_cols.len()]; nrows];
456 for (i, rem_row) in rem.iter_mut().enumerate() {
457 let num_row = table.numeric.row(i);
458 for (k, &c) in remainder_cols.iter().enumerate() {
459 rem_row[k] = num_row[c];
460 }
461 }
462 block_cols.push(remainder_cols.len());
463 blocks.push(rem);
464 }
465 for (enc, &c) in self
467 .remainder_cat_encoders
468 .iter()
469 .zip(self.remainder_cat_cols.iter())
470 {
471 let sub = Self::extract_categorical_cols(table, &[c])?;
472 let t = enc.transform(&sub)?;
473 block_cols.push(t.ncols());
474 blocks.push(t.into_rows());
475 }
476 }
477 let nrows = table.nrows();
479 let total_cols: usize = block_cols.iter().sum();
480 if total_cols == 0 {
481 return Err(DatarustError::InvalidInput(
482 "transform produced zero columns".into(),
483 ));
484 }
485 let mut out = vec![vec![0.0; total_cols]; nrows];
486 for i in 0..nrows {
487 let mut offset = 0;
488 for (block, &cols) in blocks.iter().zip(block_cols.iter()) {
489 for k in 0..cols {
490 out[i][offset + k] = block[i][k];
491 }
492 offset += cols;
493 }
494 }
495 Matrix::new(out)
496 }
497
498 pub fn fit_transform(&mut self, table: &Table) -> Result<Matrix> {
500 self.fit(table)?;
501 self.transform(table)
502 }
503
504 pub fn transform_to_table(&self, table: &Table) -> Result<crate::compose::Output> {
511 if !self.fitted {
512 return Err(DatarustError::NotFitted("ColumnTransformer".into()));
513 }
514 let mut num_blocks: Vec<Vec<Vec<f64>>> = Vec::new();
515 let mut num_block_cols: Vec<usize> = Vec::new();
516 let mut cat_blocks: Vec<Vec<Vec<String>>> = Vec::new();
517 let mut cat_block_cols: Vec<usize> = Vec::new();
518 let nrows = table.nrows();
519
520 for spec in &self.specs {
521 match spec {
522 ColumnSpec::Numeric {
523 columns,
524 transformer,
525 ..
526 } => {
527 let sub = Self::extract_numeric_cols(table, columns)?;
528 let t = transformer.transform(&sub)?;
529 num_block_cols.push(t.ncols());
530 num_blocks.push(t.into_rows());
531 }
532 ColumnSpec::Categorical {
533 columns, encoder, ..
534 } => {
535 let sub = Self::extract_categorical_cols(table, columns)?;
536 let t = encoder.transform(&sub)?;
537 num_block_cols.push(t.ncols());
538 num_blocks.push(t.into_rows());
539 }
540 ColumnSpec::Target {
541 columns, encoder, ..
542 } => {
543 let sub = Self::extract_categorical_cols(table, columns)?;
544 let t = encoder.transform(&sub)?;
545 num_block_cols.push(t.ncols());
546 num_blocks.push(t.into_rows());
547 }
548 }
549 }
550
551 if matches!(self.remainder, Remainder::Passthrough) {
553 let remainder_cols: Vec<usize> = (0..table.numeric.ncols())
555 .filter(|c| self.consumed_numeric.binary_search(c).is_err())
556 .collect();
557 if !remainder_cols.is_empty() {
558 let mut rem = vec![vec![0.0; remainder_cols.len()]; nrows];
559 for (i, rem_row) in rem.iter_mut().enumerate() {
560 let num_row = table.numeric.row(i);
561 for (k, &c) in remainder_cols.iter().enumerate() {
562 rem_row[k] = num_row[c];
563 }
564 }
565 num_block_cols.push(remainder_cols.len());
566 num_blocks.push(rem);
567 }
568
569 for &c in &self.remainder_cat_cols {
571 let mut col_data: Vec<String> = Vec::with_capacity(nrows);
572 for i in 0..nrows {
573 col_data.push(table.categorical.get(i, c).to_string());
574 }
575 cat_block_cols.push(1);
576 cat_blocks.push(col_data.into_iter().map(|s| vec![s]).collect());
577 }
578 }
579
580 let total_num_cols: usize = num_block_cols.iter().sum();
582 let total_cat_cols: usize = cat_block_cols.iter().sum();
583
584 if total_num_cols == 0 && total_cat_cols == 0 {
585 return Err(DatarustError::InvalidInput(
586 "transform_to_table produced zero columns".into(),
587 ));
588 }
589
590 let numeric = if total_num_cols > 0 {
592 let mut out = vec![vec![0.0; total_num_cols]; nrows];
593 for i in 0..nrows {
594 let mut offset = 0;
595 for (block, &cols) in num_blocks.iter().zip(num_block_cols.iter()) {
596 for k in 0..cols {
597 out[i][offset + k] = block[i][k];
598 }
599 offset += cols;
600 }
601 }
602 Matrix::new(out)?
603 } else {
604 Matrix::zeros(nrows, 1)?
608 };
609
610 let categorical = if total_cat_cols > 0 {
612 let mut out = vec![vec![String::new(); total_cat_cols]; nrows];
613 for i in 0..nrows {
614 let mut offset = 0;
615 for (block, &cols) in cat_blocks.iter().zip(cat_block_cols.iter()) {
616 for k in 0..cols {
617 out[i][offset + k] = block[i][k].clone();
618 }
619 offset += cols;
620 }
621 }
622 StrMatrix::new(out)?
623 } else {
624 StrMatrix {
625 data: vec![vec![]; nrows],
626 }
627 };
628
629 crate::compose::Output::new(numeric, categorical)
630 }
631
632 pub fn fit_transform_to_table(&mut self, table: &Table) -> Result<crate::compose::Output> {
635 self.fit(table)?;
636 self.transform_to_table(table)
637 }
638}
639
640impl Default for ColumnTransformer {
641 fn default() -> Self {
642 Self::new()
643 }
644}
645
646impl FeatureNames for ColumnTransformer {
647 fn feature_names_out(&self, input_features: Option<&[String]>) -> Vec<String> {
648 let names: Vec<String> = match input_features {
649 Some(fs) => fs.to_vec(),
650 None => default_input_names(self.max_col_index),
651 };
652 let mut out: Vec<String> = Vec::new();
653 for spec in &self.specs {
654 match spec {
655 ColumnSpec::Numeric { columns, .. } => {
656 for &c in columns {
657 out.push(names.get(c).cloned().unwrap_or_else(|| format!("x{}", c)));
658 }
659 }
660 ColumnSpec::Categorical {
661 columns, encoder, ..
662 } => {
663 let sub: Vec<String> = columns
664 .iter()
665 .map(|&c| names.get(c).cloned().unwrap_or_else(|| format!("cat{}", c)))
666 .collect();
667 out.extend(encoder.feature_names_out(Some(&sub)));
668 }
669 ColumnSpec::Target {
670 columns, encoder, ..
671 } => {
672 let sub: Vec<String> = columns
673 .iter()
674 .map(|&c| names.get(c).cloned().unwrap_or_else(|| format!("cat{}", c)))
675 .collect();
676 out.extend(encoder.feature_names_out(Some(&sub)));
677 }
678 }
679 }
680 if matches!(self.remainder, Remainder::Passthrough) {
681 let limit = names.len().min(self.total_numeric_cols);
684 for (c, name) in names[..limit].iter().enumerate() {
685 if self.consumed_numeric.binary_search(&c).is_err() {
686 out.push(name.clone());
687 }
688 }
689 for (enc, &c) in self
690 .remainder_cat_encoders
691 .iter()
692 .zip(self.remainder_cat_cols.iter())
693 {
694 let col_name = names.get(c).cloned().unwrap_or_else(|| format!("cat{}", c));
695 let sub = vec![col_name];
696 out.extend(enc.feature_names_out(Some(&sub)));
697 }
698 }
699 out
700 }
701}
702
703#[cfg(test)]
704mod tests {
705 use super::*;
706 use crate::encoder::TargetEncoder;
707 use crate::scaler::{MinMaxScaler, StandardScaler};
708
709 fn sample_table() -> Table {
710 let numeric = Matrix::new(vec![
712 vec![10.0, 1000.0],
713 vec![20.0, 2000.0],
714 vec![30.0, 3000.0],
715 vec![40.0, 4000.0],
716 ])
717 .unwrap();
718 let categorical = StrMatrix::from_strings(vec![
719 vec!["Istanbul"],
720 vec!["Ankara"],
721 vec!["Izmir"],
722 vec!["Istanbul"],
723 ])
724 .unwrap();
725 Table::new(numeric, categorical).unwrap()
726 }
727
728 #[test]
729 fn numeric_scale_plus_onehot() {
730 let table = sample_table();
731 let mut ct = ColumnTransformer::new()
732 .add_numeric(
733 "num",
734 vec![0, 1],
735 TransformerKind::StandardScaler(StandardScaler::new()),
736 )
737 .add_categorical(
738 "city",
739 vec![0],
740 CategoricalTransformerKind::OneHotEncoder(OneHotEncoder::new()),
741 );
742 let out = ct.fit_transform(&table).unwrap();
743 assert_eq!(out.ncols(), 5);
744 assert_eq!(
745 [out.get(0, 2), out.get(0, 3), out.get(0, 4)],
746 [0.0, 1.0, 0.0]
747 );
748 assert_eq!(
749 [out.get(1, 2), out.get(1, 3), out.get(1, 4)],
750 [1.0, 0.0, 0.0]
751 );
752 assert_eq!(
753 [out.get(2, 2), out.get(2, 3), out.get(2, 4)],
754 [0.0, 0.0, 1.0]
755 );
756 }
757
758 #[test]
759 fn remainder_drop_default() {
760 let table = sample_table();
761 let mut ct = ColumnTransformer::new().add_numeric(
762 "num0",
763 vec![0],
764 TransformerKind::StandardScaler(StandardScaler::new()),
765 );
766 let out = ct.fit_transform(&table).unwrap();
767 assert_eq!(out.ncols(), 1);
768 }
769
770 #[test]
771 fn remainder_passthrough_appends_unused_numeric() {
772 let table = Table::from_numeric(
774 Matrix::new(vec![
775 vec![10.0, 1000.0],
776 vec![20.0, 2000.0],
777 vec![30.0, 3000.0],
778 vec![40.0, 4000.0],
779 ])
780 .unwrap(),
781 );
782 let mut ct = ColumnTransformer::new()
783 .remainder(Remainder::Passthrough)
784 .add_numeric(
785 "num0",
786 vec![0],
787 TransformerKind::StandardScaler(StandardScaler::new()),
788 );
789 let out = ct.fit_transform(&table).unwrap();
790 assert_eq!(out.ncols(), 2);
791 for i in 0..4 {
792 assert!((out.get(i, 1) - (1000.0 * (i as f64 + 1.0))).abs() < 1e-9);
793 }
794 }
795
796 #[test]
797 fn remainder_passthrough_includes_categorical() {
798 let table = sample_table();
800 let mut ct = ColumnTransformer::new()
801 .remainder(Remainder::Passthrough)
802 .add_numeric(
803 "num0",
804 vec![0],
805 TransformerKind::StandardScaler(StandardScaler::new()),
806 );
807 let out = ct.fit_transform(&table).unwrap();
808 assert_eq!(out.ncols(), 5);
810 }
811
812 #[test]
813 fn output_column_order_specs_then_remainder() {
814 let table = sample_table();
815 let mut ct = ColumnTransformer::new()
816 .remainder(Remainder::Passthrough)
817 .add_categorical(
818 "city",
819 vec![0],
820 CategoricalTransformerKind::OneHotEncoder(OneHotEncoder::new()),
821 )
822 .add_numeric(
823 "num0",
824 vec![0],
825 TransformerKind::StandardScaler(StandardScaler::new()),
826 );
827 let out = ct.fit_transform(&table).unwrap();
828 assert_eq!(out.ncols(), 5);
829 }
830
831 #[test]
832 fn transform_before_fit_errors() {
833 let table = sample_table();
834 let ct = ColumnTransformer::new().add_numeric(
835 "n",
836 vec![0],
837 TransformerKind::StandardScaler(StandardScaler::new()),
838 );
839 assert!(matches!(
840 ct.transform(&table),
841 Err(DatarustError::NotFitted(_))
842 ));
843 }
844
845 #[test]
846 fn empty_specs_errors() {
847 let table = sample_table();
848 let mut ct = ColumnTransformer::new();
849 assert!(matches!(
850 ct.fit(&table),
851 Err(DatarustError::InvalidInput(_))
852 ));
853 }
854
855 #[test]
856 fn bad_column_index_errors() {
857 let table = sample_table();
858 let mut ct = ColumnTransformer::new().add_numeric(
859 "n",
860 vec![5],
861 TransformerKind::StandardScaler(StandardScaler::new()),
862 );
863 assert!(ct.fit(&table).is_err());
864 }
865
866 #[test]
867 fn categorical_on_numeric_only_table_errors() {
868 let table = Table::from_numeric(Matrix::new(vec![vec![1.0], vec![2.0]]).unwrap());
869 let mut ct = ColumnTransformer::new().add_categorical(
870 "c",
871 vec![0],
872 CategoricalTransformerKind::OneHotEncoder(OneHotEncoder::new()),
873 );
874 assert!(ct.fit(&table).is_err());
875 }
876
877 #[test]
878 fn transform_new_data_consistent_shape() {
879 let table = sample_table();
880 let mut ct = ColumnTransformer::new()
881 .add_numeric(
882 "num",
883 vec![0, 1],
884 TransformerKind::StandardScaler(StandardScaler::new()),
885 )
886 .add_categorical(
887 "city",
888 vec![0],
889 CategoricalTransformerKind::OneHotEncoder(OneHotEncoder::new()),
890 );
891 ct.fit(&table).unwrap();
892 let new = sample_table();
893 let out = ct.transform(&new).unwrap();
894 assert_eq!(out.ncols(), 5);
895 assert_eq!(out.nrows(), 4);
896 }
897
898 #[test]
899 fn row_count_mismatch_in_table_rejected() {
900 let numeric = Matrix::new(vec![vec![1.0], vec![2.0]]).unwrap();
901 let categorical = StrMatrix::from_column(["a"]).unwrap();
902 assert!(Table::new(numeric, categorical).is_err());
903 }
904
905 #[test]
906 fn feature_names_default() {
907 let table = sample_table();
908 let mut ct = ColumnTransformer::new()
909 .add_numeric(
910 "num",
911 vec![0, 1],
912 TransformerKind::StandardScaler(StandardScaler::new()),
913 )
914 .add_categorical(
915 "city",
916 vec![0],
917 CategoricalTransformerKind::OneHotEncoder(OneHotEncoder::new()),
918 );
919 ct.fit(&table).unwrap();
920 let names = ct.feature_names_out(None);
921 assert_eq!(
922 names,
923 vec!["x0", "x1", "x0_Ankara", "x0_Istanbul", "x0_Izmir",]
924 );
925 }
926
927 #[test]
928 fn feature_names_with_input() {
929 let table = sample_table();
930 let mut ct = ColumnTransformer::new()
931 .add_numeric(
932 "num0",
933 vec![0],
934 TransformerKind::StandardScaler(StandardScaler::new()),
935 )
936 .add_categorical(
937 "city",
938 vec![0],
939 CategoricalTransformerKind::OneHotEncoder(OneHotEncoder::new()),
940 );
941 ct.fit(&table).unwrap();
942 let names = ct.feature_names_out(Some(&["age".into(), "salary".into()]));
943 assert_eq!(
944 names,
945 vec!["age", "age_Ankara", "age_Istanbul", "age_Izmir",]
946 );
947 }
948
949 #[test]
950 fn feature_names_remainder_passthrough() {
951 let table = sample_table();
952 let mut ct = ColumnTransformer::new()
953 .remainder(Remainder::Passthrough)
954 .add_numeric(
955 "num0",
956 vec![0],
957 TransformerKind::StandardScaler(StandardScaler::new()),
958 );
959 ct.fit(&table).unwrap();
960 let names = ct.feature_names_out(Some(&["age".into(), "salary".into()]));
961 assert_eq!(
963 names,
964 vec!["age", "salary", "age_Ankara", "age_Istanbul", "age_Izmir",]
965 );
966 }
967
968 #[test]
969 fn feature_names_remainder_passthrough_numeric_only() {
970 let table = Table::from_numeric(
971 Matrix::new(vec![vec![1.0, 2.0, 3.0], vec![4.0, 5.0, 6.0]]).unwrap(),
972 );
973 let mut ct = ColumnTransformer::new()
974 .remainder(Remainder::Passthrough)
975 .add_numeric(
976 "num0",
977 vec![0],
978 TransformerKind::StandardScaler(StandardScaler::new()),
979 );
980 ct.fit(&table).unwrap();
981 let names = ct.feature_names_out(Some(&["a".into(), "b".into(), "c".into()]));
982 assert_eq!(names, vec!["a", "b", "c"]);
983 }
984
985 #[test]
986 fn feature_names_default_categorical_outnumbers_numeric() {
987 let numeric = Matrix::new(vec![vec![10.0], vec![20.0]]).unwrap();
989 let categorical =
990 StrMatrix::from_strings(vec![vec!["a", "x", "low"], vec!["b", "y", "high"]]).unwrap();
991 let table = Table::new(numeric, categorical).unwrap();
992 let mut ct = ColumnTransformer::new()
993 .add_numeric(
994 "num",
995 vec![0],
996 TransformerKind::StandardScaler(StandardScaler::new()),
997 )
998 .add_categorical(
999 "cat",
1000 vec![0],
1001 CategoricalTransformerKind::OneHotEncoder(OneHotEncoder::new()),
1002 );
1003 ct.fit(&table).unwrap();
1004 let names = ct.feature_names_out(None);
1005 assert_eq!(names, vec!["x0", "x0_a", "x0_b"]);
1007 }
1008
1009 #[test]
1010 fn feature_names_default_numeric_outnumbers_categorical() {
1011 let numeric = Matrix::new(vec![vec![1.0, 2.0, 3.0], vec![4.0, 5.0, 6.0]]).unwrap();
1013 let categorical = StrMatrix::from_strings(vec![vec!["red"], vec!["blue"]]).unwrap();
1014 let table = Table::new(numeric, categorical).unwrap();
1015 let mut ct = ColumnTransformer::new()
1016 .add_numeric(
1017 "num",
1018 vec![0, 1],
1019 TransformerKind::StandardScaler(StandardScaler::new()),
1020 )
1021 .add_categorical(
1022 "cat",
1023 vec![0],
1024 CategoricalTransformerKind::OneHotEncoder(OneHotEncoder::new()),
1025 );
1026 ct.fit(&table).unwrap();
1027 let names = ct.feature_names_out(None);
1028 assert_eq!(names, vec!["x0", "x1", "x0_blue", "x0_red"]);
1030 }
1031
1032 #[test]
1033 fn target_encoder_with_target_values() {
1034 let numeric = Matrix::new(vec![vec![1.0], vec![2.0], vec![3.0]]).unwrap();
1035 let categorical = StrMatrix::from_column(["Istanbul", "Ankara", "Istanbul"]).unwrap();
1036 let table = Table::new(numeric, categorical).unwrap();
1037 let y = vec![1.0, 0.0, 1.0];
1038
1039 let mut ct = ColumnTransformer::new()
1040 .add_numeric(
1041 "num",
1042 vec![0],
1043 TransformerKind::StandardScaler(StandardScaler::new()),
1044 )
1045 .add_target(
1046 "city_target",
1047 vec![0],
1048 TargetTransformerKind::TargetEncoder(TargetEncoder::new(0.0).unwrap()),
1049 );
1050 let out = ct.fit_transform_with_target(&table, &y).unwrap();
1051 assert_eq!(out.ncols(), 2);
1053 assert!((out.get(0, 1) - 1.0).abs() < 1e-9);
1055 assert!((out.get(1, 1) - 0.0).abs() < 1e-9);
1056 assert!((out.get(2, 1) - 1.0).abs() < 1e-9);
1057 }
1058
1059 #[test]
1060 fn target_encoder_requires_fit_with_target() {
1061 let table = sample_table();
1062 let mut ct = ColumnTransformer::new().add_target(
1063 "t",
1064 vec![0],
1065 TargetTransformerKind::TargetEncoder(TargetEncoder::new(0.0).unwrap()),
1066 );
1067 assert!(matches!(
1069 ct.fit(&table),
1070 Err(DatarustError::InvalidInput(_))
1071 ));
1072 ct.fit_with_target(&table, &[1.0, 0.0, 1.0, 0.0]).unwrap();
1074 assert!(ct.transform(&table).is_ok());
1075 }
1076
1077 #[test]
1078 fn transform_to_table_preserves_cat_remainder_as_strings() {
1079 let table = sample_table();
1080 let mut ct = ColumnTransformer::new()
1081 .remainder(Remainder::Passthrough)
1082 .add_numeric(
1083 "num0",
1084 vec![0],
1085 TransformerKind::StandardScaler(StandardScaler::new()),
1086 );
1087 let out = ct.fit_transform_to_table(&table).unwrap();
1088 assert_eq!(out.numeric.ncols(), 2);
1090 assert_eq!(out.categorical.ncols(), 1);
1092 for i in 0..4 {
1094 assert_eq!(out.categorical.get(i, 0), table.categorical.get(i, 0));
1095 }
1096 }
1097
1098 #[test]
1099 fn transform_to_table_onehot_numeric_block() {
1100 let table = sample_table();
1101 let mut ct = ColumnTransformer::new()
1102 .add_categorical(
1103 "city",
1104 vec![0],
1105 CategoricalTransformerKind::OneHotEncoder(OneHotEncoder::new()),
1106 )
1107 .add_numeric(
1108 "num",
1109 vec![0],
1110 TransformerKind::StandardScaler(StandardScaler::new()),
1111 );
1112 let out = ct.fit_transform_to_table(&table).unwrap();
1113 assert_eq!(out.numeric.ncols(), 4);
1115 assert_eq!(out.categorical.ncols(), 0);
1116 }
1117
1118 #[test]
1119 fn transform_to_table_requires_fitted() {
1120 let table = sample_table();
1121 let ct = ColumnTransformer::new().add_numeric(
1122 "n",
1123 vec![0],
1124 TransformerKind::StandardScaler(StandardScaler::new()),
1125 );
1126 assert!(matches!(
1127 ct.transform_to_table(&table),
1128 Err(DatarustError::NotFitted(_))
1129 ));
1130 }
1131
1132 #[test]
1133 fn transform_to_table_remainder_only_categorical() {
1134 let table = sample_table();
1135 let mut ct = ColumnTransformer::new()
1136 .remainder(Remainder::Passthrough)
1137 .add_numeric(
1138 "n",
1139 vec![0],
1140 TransformerKind::StandardScaler(StandardScaler::new()),
1141 );
1142 ct.fit(&table).unwrap();
1143 let out = ct.transform_to_table(&table).unwrap();
1144 assert_eq!(out.numeric.ncols(), 2);
1146 assert_eq!(out.categorical.ncols(), 1);
1148 for i in 0..4 {
1149 assert_eq!(out.categorical.get(i, 0), table.categorical.get(i, 0));
1150 }
1151 }
1152
1153 #[test]
1154 fn duplicate_column_across_specs_errors() {
1155 let table = sample_table();
1156 let mut ct = ColumnTransformer::new()
1157 .add_numeric(
1158 "num",
1159 vec![0, 1],
1160 TransformerKind::StandardScaler(StandardScaler::new()),
1161 )
1162 .add_numeric(
1163 "num2",
1164 vec![0], TransformerKind::MinMaxScaler(MinMaxScaler::new()),
1166 );
1167 assert!(matches!(
1168 ct.fit(&table),
1169 Err(DatarustError::InvalidInput(msg))
1170 if msg.contains("already consumed")
1171 ));
1172 }
1173
1174 #[test]
1175 fn duplicate_column_within_spec_errors() {
1176 let table = sample_table();
1177 let mut ct = ColumnTransformer::new().add_numeric(
1178 "num",
1179 vec![0, 0], TransformerKind::StandardScaler(StandardScaler::new()),
1181 );
1182 assert!(matches!(
1183 ct.fit(&table),
1184 Err(DatarustError::InvalidInput(msg))
1185 if msg.contains("duplicate column")
1186 ));
1187 }
1188}