rill_ml/preprocessing/
ordinal.rs1use crate::error::RillError;
8
9#[derive(Debug, Clone, Default)]
15#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
16pub struct OrdinalEncoder {
17 categories: Vec<String>,
18 samples_seen: u64,
19}
20
21impl OrdinalEncoder {
22 pub fn new() -> Self {
24 Self {
25 categories: Vec::new(),
26 samples_seen: 0,
27 }
28 }
29
30 pub fn categories(&self) -> &[String] {
32 &self.categories
33 }
34
35 pub fn category_index(&self, category: &str) -> Option<usize> {
37 self.categories
38 .binary_search_by(|c| c.as_str().cmp(category))
39 .ok()
40 }
41
42 pub fn fit_one(&mut self, category: &str) {
44 match self
45 .categories
46 .binary_search_by(|c| c.as_str().cmp(category))
47 {
48 Ok(_) => {}
49 Err(idx) => self.categories.insert(idx, category.to_string()),
50 }
51 }
52
53 pub fn transform_strs(&self, features: &[&str]) -> Result<Vec<f64>, RillError> {
64 if features.is_empty() {
65 return Err(RillError::EmptyFeatures);
66 }
67 if self.categories.is_empty() {
68 return Ok(Vec::new());
69 }
70 features
71 .iter()
72 .map(|&feat| {
73 self.category_index(feat)
74 .map(|idx| idx as f64)
75 .ok_or_else(|| RillError::UnknownCategory(feat.to_string()))
76 })
77 .collect()
78 }
79
80 pub fn update_strs(&mut self, features: &[&str]) -> Result<(), RillError> {
85 if features.is_empty() {
86 return Err(RillError::EmptyFeatures);
87 }
88 for &feat in features {
89 self.fit_one(feat);
90 }
91 self.samples_seen += 1;
92 Ok(())
93 }
94
95 pub fn samples_seen(&self) -> u64 {
97 self.samples_seen
98 }
99
100 pub fn reset(&mut self) {
102 self.categories.clear();
103 self.samples_seen = 0;
104 }
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110
111 #[test]
112 fn known_category_returns_correct_index() {
113 let mut enc = OrdinalEncoder::new();
114 enc.update_strs(&["b", "a", "c"]).unwrap();
115 let out = enc.transform_strs(&["a"]).unwrap();
117 assert_eq!(out, vec![0.0]);
118 let out = enc.transform_strs(&["b"]).unwrap();
119 assert_eq!(out, vec![1.0]);
120 let out = enc.transform_strs(&["c"]).unwrap();
121 assert_eq!(out, vec![2.0]);
122 }
123
124 #[test]
125 fn unknown_category_rejected() {
126 let mut enc = OrdinalEncoder::new();
127 enc.update_strs(&["a", "b"]).unwrap();
128 assert!(matches!(
129 enc.transform_strs(&["z"]),
130 Err(RillError::UnknownCategory(_))
131 ));
132 }
133
134 #[test]
135 fn new_category_added_on_update() {
136 let mut enc = OrdinalEncoder::new();
137 enc.update_strs(&["b"]).unwrap();
138 assert_eq!(enc.categories(), &["b"]);
139 enc.update_strs(&["a"]).unwrap();
140 assert_eq!(enc.categories(), &["a", "b"]);
141 }
142
143 #[test]
144 fn multiple_features_return_one_index_each() {
145 let mut enc = OrdinalEncoder::new();
146 enc.update_strs(&["a", "b", "c"]).unwrap();
147 let out = enc.transform_strs(&["c", "a"]).unwrap();
148 assert_eq!(out, vec![2.0, 0.0]);
149 }
150
151 #[test]
152 fn reset_clears_state() {
153 let mut enc = OrdinalEncoder::new();
154 enc.update_strs(&["a", "b"]).unwrap();
155 enc.reset();
156 assert!(enc.categories().is_empty());
157 assert_eq!(enc.samples_seen(), 0);
158 }
159
160 #[test]
161 fn empty_input_rejected() {
162 let mut enc = OrdinalEncoder::new();
163 assert!(matches!(
164 enc.update_strs(&[]),
165 Err(RillError::EmptyFeatures)
166 ));
167 assert!(matches!(
168 enc.transform_strs(&[]),
169 Err(RillError::EmptyFeatures)
170 ));
171 }
172
173 #[test]
174 #[cfg(feature = "serde")]
175 fn serde_roundtrip() {
176 let mut enc = OrdinalEncoder::new();
177 enc.update_strs(&["b", "a", "c"]).unwrap();
178 let json = serde_json::to_string(&enc).unwrap();
179 let restored: OrdinalEncoder = serde_json::from_str(&json).unwrap();
180 assert_eq!(restored.categories(), enc.categories());
181 assert_eq!(restored.samples_seen(), enc.samples_seen());
182 }
183}