prospicio_models/
encode.rs1use std::collections::BTreeMap;
13
14use prospicio_core::{Error, Result};
15
16use crate::resample::Split;
17
18#[derive(Debug, Clone, PartialEq)]
21pub struct TargetEncoding {
22 pub levels: Vec<String>,
24 pub ratios: Vec<f64>,
26 pub k: f64,
28}
29
30impl TargetEncoding {
31 pub fn encode(&self, values: &[String]) -> Vec<f64> {
34 values
35 .iter()
36 .map(|v| match self.levels.binary_search(v) {
37 Ok(i) => self.ratios[i],
38 Err(_) => 1.0,
39 })
40 .collect()
41 }
42}
43
44pub fn target_encode(
73 values: &[String],
74 y: &[f64],
75 expected: &[f64],
76 exposure: Option<&[f64]>,
77 splits: &[Split],
78 k: f64,
79) -> Result<(Vec<f64>, TargetEncoding)> {
80 let n = values.len();
81 for (len, name) in [(y.len(), "y"), (expected.len(), "expected")] {
82 if len != n {
83 return Err(Error::InvalidParameter {
84 name,
85 value: len as f64,
86 reason: "must have one value per row",
87 });
88 }
89 }
90 if let Some(e) = exposure {
91 if e.len() != n {
92 return Err(Error::InvalidParameter {
93 name: "exposure",
94 value: e.len() as f64,
95 reason: "must have one value per row",
96 });
97 }
98 }
99 if !(k.is_finite() && k >= 0.0) {
100 return Err(Error::InvalidParameter {
101 name: "k",
102 value: k,
103 reason: "must be finite and non-negative",
104 });
105 }
106 let mut seen = vec![false; n];
107 for s in splits {
108 for &i in &s.test {
109 if i >= n || seen[i] {
110 return Err(Error::InvalidParameter {
111 name: "splits",
112 value: i as f64,
113 reason: "every row must be in exactly one test set",
114 });
115 }
116 seen[i] = true;
117 }
118 }
119 if let Some(i) = seen.iter().position(|s| !s) {
120 return Err(Error::InvalidParameter {
121 name: "splits",
122 value: i as f64,
123 reason: "every row must be in exactly one test set",
124 });
125 }
126 let e = |i: usize| exposure.map_or(1.0, |e| e[i]);
127 let stats = |rows: &mut dyn Iterator<Item = usize>| {
128 let mut by: BTreeMap<&str, (f64, f64, f64)> = BTreeMap::new();
129 for i in rows {
130 let s = by.entry(values[i].as_str()).or_default();
131 s.0 += y[i];
132 s.1 += expected[i];
133 s.2 += e(i);
134 }
135 by
136 };
137 let ratio = |(actual, expected, exposure): (f64, f64, f64)| {
138 let z = if exposure + k > 0.0 {
139 exposure / (exposure + k)
140 } else {
141 0.0
142 };
143 if expected == 0.0 {
144 1.0
145 } else {
146 z * actual / expected + (1.0 - z)
147 }
148 };
149 let mut rows = vec![1.0; n];
150 for s in splits {
151 let by = stats(&mut s.train.iter().copied());
152 for &i in &s.test {
153 rows[i] = by.get(values[i].as_str()).copied().map_or(1.0, ratio);
154 }
155 }
156 let by = stats(&mut (0..n));
157 let encoding = TargetEncoding {
158 levels: by.keys().map(|l| l.to_string()).collect(),
159 ratios: by.values().copied().map(ratio).collect(),
160 k,
161 };
162 Ok((rows, encoding))
163}
164
165#[cfg(test)]
166mod tests {
167 use super::*;
168 use crate::resample::k_fold;
169
170 #[test]
171 fn a_row_never_sees_its_own_response() {
172 let values: Vec<String> = (0..60).map(|i| format!("l{}", i % 7)).collect();
173 let y: Vec<f64> = (0..60).map(|i| ((i * 13) % 5) as f64).collect();
174 let expected = vec![2.0; 60];
175 let splits = k_fold(60, 5, 3).unwrap();
176 let (rows, all) = target_encode(&values, &y, &expected, None, &splits, 4.0).unwrap();
177 let mut y2 = y.clone();
179 y2[10] += 100.0;
180 let (rows2, _) = target_encode(&values, &y2, &expected, None, &splits, 4.0).unwrap();
181 assert_eq!(rows[10], rows2[10]);
182 assert!(rows2.iter().zip(&rows).any(|(a, b)| a != b));
183 let (_, plain) = target_encode(&values, &y, &expected, None, &splits, 0.0).unwrap();
185 let a: f64 = (0..60).filter(|i| i % 7 == 0).map(|i| y[i]).sum();
186 let n = (0..60).filter(|i| i % 7 == 0).count() as f64;
187 assert!((plain.encode(&["l0".into()])[0] - a / (2.0 * n)).abs() < 1e-12);
188 assert_eq!(all.encode(&["new".into()]), [1.0]);
189 assert!(target_encode(&values, &y, &expected, None, &splits[1..], 4.0).is_err());
190 }
191}