1use std::collections::BTreeSet;
11
12use prospicio_core::{Error, Result};
13
14#[derive(Debug, Clone, PartialEq)]
16pub enum Column {
17 Numeric(Vec<f64>),
18 Categorical(Vec<String>),
19}
20
21impl Column {
22 fn len(&self) -> usize {
23 match self {
24 Self::Numeric(v) => v.len(),
25 Self::Categorical(v) => v.len(),
26 }
27 }
28}
29
30#[derive(Debug, Clone, PartialEq)]
43pub struct Frame {
44 columns: Vec<(String, Column)>,
45 n_rows: usize,
46}
47
48impl Frame {
49 pub fn new(columns: Vec<(String, Column)>) -> Result<Self> {
51 let n_rows = columns.first().map_or(0, |(_, c)| c.len());
52 for (i, (name, c)) in columns.iter().enumerate() {
53 if c.len() != n_rows {
54 return Err(invalid(
55 "columns",
56 c.len() as f64,
57 "must all have the same length",
58 ));
59 }
60 if columns[..i].iter().any(|(other, _)| other == name) {
61 return Err(invalid("columns", i as f64, "repeat a column name"));
62 }
63 }
64 Ok(Self { columns, n_rows })
65 }
66
67 pub fn n_rows(&self) -> usize {
69 self.n_rows
70 }
71
72 pub fn column(&self, name: &str) -> Option<&Column> {
74 self.columns.iter().find(|(n, _)| n == name).map(|(_, c)| c)
75 }
76
77 fn require(&self, name: &str) -> Result<&Column> {
78 self.column(name)
79 .ok_or_else(|| Error::Data(format!("no column named {name:?}")))
80 }
81}
82
83#[derive(Debug, Clone, PartialEq)]
85pub enum Term {
86 Intercept,
88 Numeric(String),
90 Factor {
94 name: String,
95 reference: Option<String>,
96 },
97 Interaction { a: String, b: String },
101 Poly { name: String, degree: usize },
104 Hinges { name: String, knots: Vec<f64> },
106 Steps { name: String, knots: Vec<f64> },
108 Bins {
112 name: String,
113 cuts: Vec<f64>,
114 reference: Option<usize>,
115 },
116 Log { name: String, base: f64 },
119 Ordinal { name: String, levels: Vec<String> },
123 OrthogonalPoly { name: String, degree: usize },
127 NaturalSpline {
133 name: String,
134 knots: Vec<f64>,
135 boundary: Option<(f64, f64)>,
136 },
137}
138
139#[derive(Debug, Clone, Default, PartialEq)]
155pub struct Terms {
156 terms: Vec<Term>,
157}
158
159impl Terms {
160 pub fn new() -> Self {
162 Self::default()
163 }
164
165 pub fn intercept(mut self) -> Self {
167 self.terms.push(Term::Intercept);
168 self
169 }
170
171 pub fn numeric(mut self, name: &str) -> Self {
173 self.terms.push(Term::Numeric(name.into()));
174 self
175 }
176
177 pub fn factor(mut self, name: &str) -> Self {
179 self.terms.push(Term::Factor {
180 name: name.into(),
181 reference: None,
182 });
183 self
184 }
185
186 pub fn factor_with_reference(mut self, name: &str, reference: &str) -> Self {
188 self.terms.push(Term::Factor {
189 name: name.into(),
190 reference: Some(reference.into()),
191 });
192 self
193 }
194
195 pub fn interaction(self, a: &str, b: &str) -> Self {
197 self.term(Term::Interaction {
198 a: a.into(),
199 b: b.into(),
200 })
201 }
202
203 pub fn poly(self, name: &str, degree: usize) -> Self {
205 self.term(Term::Poly {
206 name: name.into(),
207 degree,
208 })
209 }
210
211 pub fn hinges(self, name: &str, knots: &[f64]) -> Self {
213 self.term(Term::Hinges {
214 name: name.into(),
215 knots: knots.to_vec(),
216 })
217 }
218
219 pub fn steps(self, name: &str, knots: &[f64]) -> Self {
221 self.term(Term::Steps {
222 name: name.into(),
223 knots: knots.to_vec(),
224 })
225 }
226
227 pub fn bins(self, name: &str, cuts: &[f64]) -> Self {
230 self.term(Term::Bins {
231 name: name.into(),
232 cuts: cuts.to_vec(),
233 reference: None,
234 })
235 }
236
237 pub fn log(self, name: &str, base: f64) -> Self {
239 self.term(Term::Log {
240 name: name.into(),
241 base,
242 })
243 }
244
245 pub fn ordinal(self, name: &str, levels: &[&str]) -> Self {
248 self.term(Term::Ordinal {
249 name: name.into(),
250 levels: levels.iter().map(|l| (*l).to_string()).collect(),
251 })
252 }
253
254 pub fn orthogonal_poly(self, name: &str, degree: usize) -> Self {
256 self.term(Term::OrthogonalPoly {
257 name: name.into(),
258 degree,
259 })
260 }
261
262 pub fn natural_spline(self, name: &str, knots: &[f64]) -> Self {
265 self.term(Term::NaturalSpline {
266 name: name.into(),
267 knots: knots.to_vec(),
268 boundary: None,
269 })
270 }
271
272 pub fn term(mut self, term: Term) -> Self {
274 self.terms.push(term);
275 self
276 }
277
278 pub fn fit(&self, frame: &Frame) -> Result<Coding> {
281 let mut coded = Vec::with_capacity(self.terms.len());
282 for term in &self.terms {
283 coded.push(match term {
284 Term::Intercept => Coded::Intercept,
285 Term::Numeric(name) => {
286 numeric(frame, name)?;
287 Coded::Numeric(name.clone())
288 }
289 Term::Factor { name, reference } => {
290 let (reference, levels) = factor_levels(frame, name, reference.as_deref())?;
291 Coded::Factor {
292 name: name.clone(),
293 reference,
294 levels,
295 }
296 }
297 Term::Interaction { a, b } => Coded::Interaction {
298 a: side(frame, a)?,
299 b: side(frame, b)?,
300 },
301 Term::Poly { name, degree } => {
302 numeric(frame, name)?;
303 if *degree == 0 {
304 return Err(invalid("degree", 0.0, "must be at least 1"));
305 }
306 Coded::Poly {
307 name: name.clone(),
308 degree: *degree,
309 }
310 }
311 Term::Hinges { name, knots } | Term::Steps { name, knots } => {
312 numeric(frame, name)?;
313 increasing(knots, "knots")?;
314 if matches!(term, Term::Hinges { .. }) {
315 Coded::Hinges {
316 name: name.clone(),
317 knots: knots.clone(),
318 }
319 } else {
320 Coded::Steps {
321 name: name.clone(),
322 knots: knots.clone(),
323 }
324 }
325 }
326 Term::Bins {
327 name,
328 cuts,
329 reference,
330 } => {
331 numeric(frame, name)?;
332 increasing(cuts, "cuts")?;
333 let reference = reference.unwrap_or(0);
334 if reference > cuts.len() {
335 return Err(invalid(
336 "reference",
337 reference as f64,
338 "must index one of the bins",
339 ));
340 }
341 Coded::Bins {
342 name: name.clone(),
343 cuts: cuts.clone(),
344 reference,
345 }
346 }
347 Term::Log { name, base } => {
348 numeric(frame, name)?;
349 if !(base.is_finite() && *base > 0.0) {
350 return Err(invalid("base", *base, "must be positive"));
351 }
352 Coded::Log {
353 name: name.clone(),
354 base: *base,
355 }
356 }
357 Term::OrthogonalPoly { name, degree } => {
358 let x = numeric(frame, name)?;
359 orthogonal_poly(name, x, *degree)?
360 }
361 Term::NaturalSpline {
362 name,
363 knots,
364 boundary,
365 } => {
366 let x = numeric(frame, name)?;
367 let (lo, hi) = match boundary {
368 Some(b) => *b,
369 None => x
370 .iter()
371 .fold((f64::INFINITY, f64::NEG_INFINITY), |(lo, hi), &v| {
372 (lo.min(v), hi.max(v))
373 }),
374 };
375 let mut all = vec![lo];
376 all.extend(knots);
377 all.push(hi);
378 increasing(&all, "knots")?;
379 if knots.is_empty() {
380 return Err(invalid(
381 "knots",
382 0.0,
383 "a natural spline needs an interior knot",
384 ));
385 }
386 Coded::NaturalSpline {
387 name: name.clone(),
388 knots: all,
389 }
390 }
391 Term::Ordinal { name, levels } => {
392 categorical(frame, name)?;
393 if levels.len() < 2 {
394 return Err(invalid(
395 "levels",
396 levels.len() as f64,
397 "an ordinal needs at least two levels",
398 ));
399 }
400 if levels
401 .iter()
402 .enumerate()
403 .any(|(i, l)| levels[..i].contains(l))
404 {
405 return Err(Error::Data(format!("{name:?} repeats a level")));
406 }
407 Coded::Ordinal {
408 name: name.clone(),
409 levels: levels.clone(),
410 }
411 }
412 });
413 }
414 Ok(Coding { coded })
415 }
416}
417
418fn numeric<'a>(frame: &'a Frame, name: &str) -> Result<&'a [f64]> {
419 match frame.require(name)? {
420 Column::Numeric(v) => Ok(v),
421 Column::Categorical(_) => Err(Error::Data(format!("{name:?} is categorical, not numeric"))),
422 }
423}
424
425fn categorical<'a>(frame: &'a Frame, name: &str) -> Result<&'a [String]> {
426 match frame.require(name)? {
427 Column::Categorical(v) => Ok(v),
428 Column::Numeric(_) => Err(Error::Data(format!("{name:?} is numeric, not categorical"))),
429 }
430}
431
432fn factor_levels(
434 frame: &Frame,
435 name: &str,
436 reference: Option<&str>,
437) -> Result<(String, Vec<String>)> {
438 let values = categorical(frame, name)?;
439 let levels: BTreeSet<&String> = values.iter().collect();
440 let reference = match reference {
441 Some(r) if levels.iter().any(|l| l.as_str() == r) => r.to_string(),
442 Some(r) => {
443 return Err(Error::Data(format!(
444 "reference level {r:?} does not occur in {name:?}"
445 )));
446 }
447 None => levels
448 .first()
449 .map(|s| (*s).clone())
450 .ok_or_else(|| Error::Data(format!("{name:?} is empty")))?,
451 };
452 let others = levels
453 .into_iter()
454 .filter(|l| **l != reference)
455 .cloned()
456 .collect();
457 Ok((reference, others))
458}
459
460fn side(frame: &Frame, name: &str) -> Result<Side> {
461 Ok(match frame.require(name)? {
462 Column::Numeric(_) => Side::Numeric(name.to_string()),
463 Column::Categorical(_) => {
464 let (reference, levels) = factor_levels(frame, name, None)?;
465 Side::Factor {
466 name: name.to_string(),
467 reference,
468 levels,
469 }
470 }
471 })
472}
473
474fn increasing(v: &[f64], name: &'static str) -> Result<()> {
475 if v.is_empty() {
476 return Err(invalid(name, 0.0, "must not be empty"));
477 }
478 if let Some(&bad) = v.iter().find(|x| !x.is_finite()) {
479 return Err(invalid(name, bad, "must be finite"));
480 }
481 if v.windows(2).any(|w| w[0] >= w[1]) {
482 return Err(invalid(name, v.len() as f64, "must be strictly increasing"));
483 }
484 Ok(())
485}
486
487#[derive(Debug, Clone, PartialEq)]
489enum Side {
490 Numeric(String),
491 Factor {
492 name: String,
493 reference: String,
494 levels: Vec<String>,
495 },
496}
497
498impl Side {
499 fn names(&self) -> Vec<String> {
500 match self {
501 Self::Numeric(n) => vec![n.clone()],
502 Self::Factor { name, levels, .. } => {
503 levels.iter().map(|l| format!("{name}[{l}]")).collect()
504 }
505 }
506 }
507
508 fn columns(&self, frame: &Frame) -> Result<Vec<Vec<f64>>> {
509 match self {
510 Self::Numeric(n) => Ok(vec![numeric(frame, n)?.to_vec()]),
511 Self::Factor {
512 name,
513 reference,
514 levels,
515 } => indicators(categorical(frame, name)?, name, reference, levels),
516 }
517 }
518}
519
520fn indicators(
522 values: &[String],
523 name: &str,
524 reference: &str,
525 levels: &[String],
526) -> Result<Vec<Vec<f64>>> {
527 let mut columns = vec![vec![0.0; values.len()]; levels.len()];
528 for (i, v) in values.iter().enumerate() {
529 if v == reference {
530 continue;
531 }
532 let k = levels.binary_search(v).map_err(|_| {
533 Error::Data(format!("level {v:?} of {name:?} was not seen in training"))
534 })?;
535 columns[k][i] = 1.0;
536 }
537 Ok(columns)
538}
539
540fn orthogonal_poly(name: &str, x: &[f64], degree: usize) -> Result<Coded> {
542 let distinct: BTreeSet<u64> = x.iter().map(|v| v.to_bits()).collect();
543 if degree == 0 || degree >= distinct.len() {
544 return Err(invalid(
545 "degree",
546 degree as f64,
547 "must be at least 1 and less than the number of distinct values",
548 ));
549 }
550 let n = x.len();
551 let (mut alpha, mut norm2) = (Vec::with_capacity(degree), vec![1.0, n as f64]);
552 let (mut prev, mut cur) = (vec![0.0; n], vec![1.0; n]);
553 for k in 0..degree {
554 let a = x.iter().zip(&cur).map(|(x, p)| x * p * p).sum::<f64>() / norm2[k + 1];
555 let ratio = norm2[k + 1] / norm2[k];
556 let next: Vec<f64> = (0..n)
557 .map(|i| (x[i] - a) * cur[i] - if k == 0 { 0.0 } else { ratio * prev[i] })
558 .collect();
559 alpha.push(a);
560 norm2.push(next.iter().map(|p| p * p).sum());
561 prev = std::mem::replace(&mut cur, next);
562 }
563 Ok(Coded::OrthogonalPoly {
564 name: name.to_string(),
565 alpha,
566 norm2,
567 })
568}
569
570fn poly_columns(x: &[f64], alpha: &[f64], norm2: &[f64]) -> Vec<Vec<f64>> {
571 let n = x.len();
572 let (mut prev, mut cur) = (vec![0.0; n], vec![1.0; n]);
573 let mut out = Vec::with_capacity(alpha.len());
574 for (k, a) in alpha.iter().enumerate() {
575 let ratio = norm2[k + 1] / norm2[k];
576 let next: Vec<f64> = (0..n)
577 .map(|i| (x[i] - a) * cur[i] - if k == 0 { 0.0 } else { ratio * prev[i] })
578 .collect();
579 let scale = norm2[k + 2].sqrt();
580 out.push(next.iter().map(|p| p / scale).collect());
581 prev = std::mem::replace(&mut cur, next);
582 }
583 out
584}
585
586fn spline_columns(x: &[f64], knots: &[f64]) -> Vec<Vec<f64>> {
591 let k = knots.len();
592 let last = knots[k - 1];
593 let scale = (last - knots[0]).powi(2);
594 let cube = |v: f64| v.max(0.0).powi(3);
595 let d = |j: usize, v: f64| (cube(v - knots[j]) - cube(v - last)) / (last - knots[j]);
596 let mut out = vec![x.to_vec()];
597 out.extend((0..k - 2).map(|j| x.iter().map(|&v| (d(j, v) - d(k - 2, v)) / scale).collect()));
598 out
599}
600
601fn bin_label(cuts: &[f64], k: usize) -> String {
603 let lo = if k == 0 {
604 "-Inf".to_string()
605 } else {
606 cuts[k - 1].to_string()
607 };
608 if k == cuts.len() {
609 format!("({lo},Inf)")
610 } else {
611 format!("({lo},{}]", cuts[k])
612 }
613}
614
615#[derive(Debug, Clone, PartialEq)]
616enum Coded {
617 Intercept,
618 Numeric(String),
619 Factor {
620 name: String,
621 reference: String,
622 levels: Vec<String>,
623 },
624 Interaction {
625 a: Side,
626 b: Side,
627 },
628 Poly {
629 name: String,
630 degree: usize,
631 },
632 Hinges {
633 name: String,
634 knots: Vec<f64>,
635 },
636 Steps {
637 name: String,
638 knots: Vec<f64>,
639 },
640 Bins {
641 name: String,
642 cuts: Vec<f64>,
643 reference: usize,
644 },
645 Log {
646 name: String,
647 base: f64,
648 },
649 Ordinal {
650 name: String,
651 levels: Vec<String>,
652 },
653 OrthogonalPoly {
657 name: String,
658 alpha: Vec<f64>,
659 norm2: Vec<f64>,
660 },
661 NaturalSpline {
663 name: String,
664 knots: Vec<f64>,
665 },
666}
667
668#[derive(Debug, Clone, PartialEq)]
671pub struct Coding {
672 coded: Vec<Coded>,
673}
674
675impl Coding {
676 pub fn names(&self) -> Vec<String> {
679 let mut names = Vec::new();
680 for c in &self.coded {
681 match c {
682 Coded::Intercept => names.push("(Intercept)".into()),
683 Coded::Numeric(n) => names.push(n.clone()),
684 Coded::Factor { name, levels, .. } => {
685 names.extend(levels.iter().map(|l| format!("{name}[{l}]")));
686 }
687 Coded::Interaction { a, b } => {
688 let (na, nb) = (a.names(), b.names());
689 for y in &nb {
690 names.extend(na.iter().map(|x| format!("{x}:{y}")));
691 }
692 }
693 Coded::Poly { name, degree } => {
694 names.push(name.clone());
695 names.extend((2..=*degree).map(|k| format!("{name}^{k}")));
696 }
697 Coded::Hinges { name, knots } => {
698 names.extend(knots.iter().map(|k| format!("hinge({name},{k})")));
699 }
700 Coded::Steps { name, knots } => {
701 names.extend(knots.iter().map(|k| format!("step({name},{k})")));
702 }
703 Coded::Bins {
704 name,
705 cuts,
706 reference,
707 } => names.extend(
708 (0..=cuts.len())
709 .filter(|k| k != reference)
710 .map(|k| format!("{name}[{}]", bin_label(cuts, k))),
711 ),
712 Coded::Log { name, base } => names.push(format!("log({name}/{base})")),
713 Coded::Ordinal { name, levels } => {
714 names.extend(levels[1..].iter().map(|l| format!("{name}[>={l}]")));
715 }
716 Coded::OrthogonalPoly { name, alpha, .. } => {
717 names.extend((1..=alpha.len()).map(|k| format!("poly({name}){k}")));
718 }
719 Coded::NaturalSpline { name, knots } => {
720 names.extend((1..knots.len()).map(|k| format!("spline({name}){k}")));
721 }
722 }
723 }
724 names
725 }
726
727 pub fn references(&self) -> Vec<(String, String)> {
729 self.coded
730 .iter()
731 .filter_map(|c| match c {
732 Coded::Factor {
733 name, reference, ..
734 } => Some((name.clone(), reference.clone())),
735 _ => None,
736 })
737 .collect()
738 }
739
740 pub fn design(&self, frame: &Frame) -> Result<Design> {
743 let n = frame.n_rows();
744 let mut columns: Vec<Vec<f64>> = Vec::new();
745 for c in &self.coded {
746 match c {
747 Coded::Intercept => columns.push(vec![1.0; n]),
748 Coded::Numeric(name) => columns.push(numeric(frame, name)?.to_vec()),
749 Coded::Factor {
750 name,
751 reference,
752 levels,
753 } => columns.extend(indicators(
754 categorical(frame, name)?,
755 name,
756 reference,
757 levels,
758 )?),
759 Coded::Interaction { a, b } => {
760 let (ca, cb) = (a.columns(frame)?, b.columns(frame)?);
761 for y in &cb {
762 for x in &ca {
763 columns.push(x.iter().zip(y).map(|(x, y)| x * y).collect());
764 }
765 }
766 }
767 Coded::Poly { name, degree } => {
768 let x = numeric(frame, name)?;
769 columns.extend(
770 (1..=*degree).map(|k| x.iter().map(|v| v.powi(k as i32)).collect()),
771 );
772 }
773 Coded::Hinges { name, knots } => {
774 let x = numeric(frame, name)?;
775 columns.extend(
776 knots
777 .iter()
778 .map(|k| x.iter().map(|v| (v - k).max(0.0)).collect()),
779 );
780 }
781 Coded::Steps { name, knots } => {
782 let x = numeric(frame, name)?;
783 columns.extend(
784 knots
785 .iter()
786 .map(|k| x.iter().map(|v| if v >= k { 1.0 } else { 0.0 }).collect()),
787 );
788 }
789 Coded::Bins {
790 name,
791 cuts,
792 reference,
793 } => {
794 let x = numeric(frame, name)?;
795 let start = columns.len();
796 columns.extend((0..cuts.len()).map(|_| vec![0.0; n]));
797 for (i, v) in x.iter().enumerate() {
798 if v.is_nan() {
799 return Err(Error::Data(format!("{name:?} holds NaN")));
800 }
801 let k = cuts.partition_point(|c| c < v);
803 if k != *reference {
804 let col = if k < *reference { k } else { k - 1 };
805 columns[start + col][i] = 1.0;
806 }
807 }
808 }
809 Coded::Log { name, base } => {
810 let x = numeric(frame, name)?;
811 if let Some(&bad) = x.iter().find(|v| v.is_nan() || **v <= 0.0) {
812 return Err(Error::Data(format!(
813 "log({name}/{base}) needs positive values, got {bad}"
814 )));
815 }
816 columns.push(x.iter().map(|v| (v / base).ln()).collect());
817 }
818 Coded::OrthogonalPoly { alpha, norm2, name } => {
819 columns.extend(poly_columns(numeric(frame, name)?, alpha, norm2));
820 }
821 Coded::NaturalSpline { name, knots } => {
822 columns.extend(spline_columns(numeric(frame, name)?, knots));
823 }
824 Coded::Ordinal { name, levels } => {
825 let values = categorical(frame, name)?;
826 let start = columns.len();
827 columns.extend((1..levels.len()).map(|_| vec![0.0; n]));
828 for (i, v) in values.iter().enumerate() {
829 let rank = levels.iter().position(|l| l == v).ok_or_else(|| {
830 Error::Data(format!("level {v:?} of {name:?} is not one of its levels"))
831 })?;
832 for col in columns[start..start + rank].iter_mut() {
833 col[i] = 1.0;
834 }
835 }
836 }
837 }
838 }
839 Design::new(self.names(), columns)
840 }
841}
842
843#[derive(Debug, Clone, PartialEq)]
850pub struct Design {
851 names: Vec<String>,
852 columns: Vec<Vec<f64>>,
853 n_rows: usize,
854 offset: Vec<f64>,
855 weights: Vec<f64>,
856}
857
858impl Design {
859 pub fn new(names: Vec<String>, columns: Vec<Vec<f64>>) -> Result<Self> {
862 if names.len() != columns.len() {
863 return Err(invalid(
864 "names",
865 names.len() as f64,
866 "must name every column",
867 ));
868 }
869 let n_rows = columns.first().map_or(0, Vec::len);
870 if columns.iter().any(|c| c.len() != n_rows) {
871 return Err(invalid(
872 "columns",
873 n_rows as f64,
874 "must all have the same length",
875 ));
876 }
877 if let Some(bad) = columns.iter().flatten().find(|x| !x.is_finite()) {
878 return Err(invalid("columns", *bad, "must be finite"));
879 }
880 Ok(Self {
881 names,
882 columns,
883 n_rows,
884 offset: vec![0.0; n_rows],
885 weights: vec![1.0; n_rows],
886 })
887 }
888
889 pub fn with_offset(mut self, offset: Vec<f64>) -> Result<Self> {
891 if offset.len() != self.n_rows {
892 return Err(invalid(
893 "offset",
894 offset.len() as f64,
895 "needs one value per row",
896 ));
897 }
898 if let Some(bad) = offset.iter().find(|x| !x.is_finite()) {
899 return Err(invalid("offset", *bad, "must be finite"));
900 }
901 self.offset = offset;
902 Ok(self)
903 }
904
905 pub fn with_weights(mut self, weights: Vec<f64>) -> Result<Self> {
907 if weights.len() != self.n_rows {
908 return Err(invalid(
909 "weights",
910 weights.len() as f64,
911 "needs one value per row",
912 ));
913 }
914 if let Some(bad) = weights.iter().find(|x| !(x.is_finite() && **x > 0.0)) {
915 return Err(invalid("weights", *bad, "must be finite and positive"));
916 }
917 self.weights = weights;
918 Ok(self)
919 }
920
921 pub fn names(&self) -> &[String] {
923 &self.names
924 }
925
926 pub fn n_rows(&self) -> usize {
928 self.n_rows
929 }
930
931 pub fn n_cols(&self) -> usize {
933 self.columns.len()
934 }
935
936 pub fn column(&self, j: usize) -> &[f64] {
938 &self.columns[j]
939 }
940
941 pub fn offset(&self) -> &[f64] {
943 &self.offset
944 }
945
946 pub fn weights(&self) -> &[f64] {
948 &self.weights
949 }
950
951 pub fn linear_predictor(&self, beta: &[f64]) -> Vec<f64> {
953 let mut eta = self.offset.clone();
954 for (col, b) in self.columns.iter().zip(beta) {
955 for (e, x) in eta.iter_mut().zip(col) {
956 *e += b * x;
957 }
958 }
959 eta
960 }
961
962 pub fn select(&self, rows: &[usize]) -> Self {
964 let pick = |v: &[f64]| rows.iter().map(|&i| v[i]).collect::<Vec<_>>();
965 Self {
966 names: self.names.clone(),
967 columns: self.columns.iter().map(|c| pick(c)).collect(),
968 n_rows: rows.len(),
969 offset: pick(&self.offset),
970 weights: pick(&self.weights),
971 }
972 }
973}
974
975fn invalid(name: &'static str, value: f64, reason: &'static str) -> Error {
976 Error::InvalidParameter {
977 name,
978 value,
979 reason,
980 }
981}
982
983#[cfg(test)]
984mod tests {
985 use super::*;
986
987 fn frame(regions: &[&str]) -> Frame {
988 Frame::new(vec![
989 (
990 "x".into(),
991 Column::Numeric((0..regions.len()).map(|i| i as f64).collect()),
992 ),
993 (
994 "region".into(),
995 Column::Categorical(regions.iter().map(|s| s.to_string()).collect()),
996 ),
997 ])
998 .unwrap()
999 }
1000
1001 #[test]
1002 fn coding_replays_on_new_data() {
1003 let train = frame(&["S", "N", "W", "N"]);
1004 let coding = Terms::new()
1005 .intercept()
1006 .factor_with_reference("region", "S")
1007 .numeric("x")
1008 .fit(&train)
1009 .unwrap();
1010 assert_eq!(
1011 coding.names(),
1012 ["(Intercept)", "region[N]", "region[W]", "x"]
1013 );
1014 let d = coding.design(&frame(&["W", "S"])).unwrap();
1016 assert_eq!(d.column(1), [0.0, 0.0]);
1017 assert_eq!(d.column(2), [1.0, 0.0]);
1018 assert!(coding.design(&frame(&["E"])).is_err());
1019 }
1020
1021 #[test]
1022 fn rejects_bad_frames_and_designs() {
1023 assert!(
1024 Frame::new(vec![
1025 ("a".into(), Column::Numeric(vec![1.0])),
1026 ("a".into(), Column::Numeric(vec![2.0])),
1027 ])
1028 .is_err()
1029 );
1030 assert!(Terms::new().numeric("region").fit(&frame(&["N"])).is_err());
1031 let d = Design::new(vec!["a".into()], vec![vec![1.0, 2.0]]).unwrap();
1032 assert!(d.clone().with_weights(vec![1.0, 0.0]).is_err());
1033 assert!(d.with_offset(vec![1.0]).is_err());
1034 }
1035
1036 #[test]
1037 fn linear_predictor_and_select() {
1038 let d = Design::new(
1039 vec!["a".into(), "b".into()],
1040 vec![vec![1.0, 1.0, 1.0], vec![0.0, 1.0, 2.0]],
1041 )
1042 .unwrap()
1043 .with_offset(vec![0.5, 0.0, 0.0])
1044 .unwrap();
1045 assert_eq!(d.linear_predictor(&[1.0, 2.0]), [1.5, 3.0, 5.0]);
1046 let s = d.select(&[2, 0]);
1047 assert_eq!(s.column(1), [2.0, 0.0]);
1048 assert_eq!(s.offset(), [0.0, 0.5]);
1049 }
1050
1051 fn rating_frame() -> Frame {
1052 Frame::new(vec![
1053 ("age".into(), Column::Numeric(vec![20.0, 30.0, 45.0, 60.0])),
1054 (
1055 "region".into(),
1056 Column::Categorical(["N", "S", "W", "S"].iter().map(|s| s.to_string()).collect()),
1057 ),
1058 (
1059 "size".into(),
1060 Column::Categorical(
1061 ["small", "large", "medium", "small"]
1062 .iter()
1063 .map(|s| s.to_string())
1064 .collect(),
1065 ),
1066 ),
1067 ])
1068 .unwrap()
1069 }
1070
1071 #[test]
1072 fn interactions_multiply_the_coded_columns() {
1073 let f = rating_frame();
1074 let c = Terms::new()
1075 .interaction("region", "age")
1076 .interaction("region", "size")
1077 .interaction("age", "age")
1078 .fit(&f)
1079 .unwrap();
1080 assert_eq!(
1081 c.names(),
1082 [
1083 "region[S]:age",
1084 "region[W]:age",
1085 "region[S]:size[medium]",
1087 "region[W]:size[medium]",
1088 "region[S]:size[small]",
1089 "region[W]:size[small]",
1090 "age:age",
1091 ]
1092 );
1093 let d = c.design(&f).unwrap();
1094 assert_eq!(d.column(0), [0.0, 30.0, 0.0, 60.0]);
1095 assert_eq!(d.column(3), [0.0, 0.0, 1.0, 0.0]);
1096 assert_eq!(d.column(4), [0.0, 0.0, 0.0, 1.0]);
1097 assert_eq!(d.column(6), [400.0, 900.0, 2025.0, 3600.0]);
1098 }
1099
1100 #[test]
1101 fn polynomials_hinges_steps_and_logs() {
1102 let f = rating_frame();
1103 let c = Terms::new()
1104 .poly("age", 3)
1105 .hinges("age", &[25.0, 50.0])
1106 .steps("age", &[30.0])
1107 .log("age", 40.0)
1108 .fit(&f)
1109 .unwrap();
1110 assert_eq!(
1111 c.names(),
1112 [
1113 "age",
1114 "age^2",
1115 "age^3",
1116 "hinge(age,25)",
1117 "hinge(age,50)",
1118 "step(age,30)",
1119 "log(age/40)"
1120 ]
1121 );
1122 let d = c.design(&f).unwrap();
1123 assert_eq!(d.column(2), [8000.0, 27000.0, 91125.0, 216000.0]);
1124 assert_eq!(d.column(3), [0.0, 5.0, 20.0, 35.0]);
1125 assert_eq!(d.column(4), [0.0, 0.0, 0.0, 10.0]);
1126 assert_eq!(d.column(5), [0.0, 1.0, 1.0, 1.0]);
1128 assert!((d.column(6)[2] - (45.0f64 / 40.0).ln()).abs() < 1e-15);
1129 assert!(Terms::new().hinges("age", &[50.0, 25.0]).fit(&f).is_err());
1130 assert!(Terms::new().poly("age", 0).fit(&f).is_err());
1131 assert!(Terms::new().log("age", 0.0).fit(&f).is_err());
1132 }
1133
1134 #[test]
1135 fn bins_code_a_numeric_column_as_a_factor() {
1136 let f = rating_frame();
1137 let c = Terms::new().bins("age", &[30.0, 50.0]).fit(&f).unwrap();
1138 assert_eq!(c.names(), ["age[(30,50]]", "age[(50,Inf)]"]);
1140 let d = c.design(&f).unwrap();
1141 assert_eq!(d.column(0), [0.0, 0.0, 1.0, 0.0]);
1142 assert_eq!(d.column(1), [0.0, 0.0, 0.0, 1.0]);
1143 let middle = Terms::new()
1144 .term(Term::Bins {
1145 name: "age".into(),
1146 cuts: vec![30.0, 50.0],
1147 reference: Some(1),
1148 })
1149 .fit(&f)
1150 .unwrap();
1151 assert_eq!(middle.names(), ["age[(-Inf,30]]", "age[(50,Inf)]"]);
1152 assert_eq!(middle.design(&f).unwrap().column(0), [1.0, 1.0, 0.0, 0.0]);
1153 let new = Frame::new(vec![("age".into(), Column::Numeric(vec![-5.0, 99.0]))]).unwrap();
1155 assert_eq!(c.design(&new).unwrap().column(1), [0.0, 1.0]);
1156 }
1157
1158 #[test]
1159 fn ordinals_are_cumulative_steps() {
1160 let f = rating_frame();
1161 let c = Terms::new()
1162 .ordinal("size", &["small", "medium", "large"])
1163 .fit(&f)
1164 .unwrap();
1165 assert_eq!(c.names(), ["size[>=medium]", "size[>=large]"]);
1166 let d = c.design(&f).unwrap();
1167 assert_eq!(d.column(0), [0.0, 1.0, 1.0, 0.0]);
1168 assert_eq!(d.column(1), [0.0, 1.0, 0.0, 0.0]);
1169 let unknown = Frame::new(vec![(
1170 "size".into(),
1171 Column::Categorical(vec!["huge".into()]),
1172 )])
1173 .unwrap();
1174 assert!(c.design(&unknown).is_err());
1175 assert!(
1176 Terms::new()
1177 .ordinal("size", &["small", "small"])
1178 .fit(&f)
1179 .is_err()
1180 );
1181 }
1182}