ggplot_rs/scale/
transform.rs1use crate::data::Value;
2
3#[derive(Clone, Debug)]
5pub enum ScaleTransform {
6 Identity,
7 Log10,
8 Log2,
9 Ln,
10 Sqrt,
11 Reverse,
12 Logit,
14 Probit,
16 PseudoLog,
18 Reciprocal,
20 Exp,
22 BoxCox(f64),
24}
25
26impl ScaleTransform {
27 pub fn apply(&self, value: f64) -> f64 {
29 match self {
30 ScaleTransform::Identity => value,
31 ScaleTransform::Log10 => {
32 if value > 0.0 {
33 value.log10()
34 } else {
35 f64::NEG_INFINITY
36 }
37 }
38 ScaleTransform::Log2 => {
39 if value > 0.0 {
40 value.log2()
41 } else {
42 f64::NEG_INFINITY
43 }
44 }
45 ScaleTransform::Ln => {
46 if value > 0.0 {
47 value.ln()
48 } else {
49 f64::NEG_INFINITY
50 }
51 }
52 ScaleTransform::Sqrt => {
53 if value >= 0.0 {
54 value.sqrt()
55 } else {
56 f64::NAN
57 }
58 }
59 ScaleTransform::Reverse => -value,
60 ScaleTransform::Logit => {
61 if value <= 0.0 {
62 f64::NEG_INFINITY
63 } else if value >= 1.0 {
64 f64::INFINITY
65 } else {
66 (value / (1.0 - value)).ln()
67 }
68 }
69 ScaleTransform::Probit => qnorm(value),
70 ScaleTransform::PseudoLog => (value / 2.0).asinh(),
71 ScaleTransform::Reciprocal => {
72 if value != 0.0 {
73 1.0 / value
74 } else {
75 f64::NAN
76 }
77 }
78 ScaleTransform::Exp => value.exp(),
79 ScaleTransform::BoxCox(lambda) => {
80 if value <= 0.0 {
81 f64::NAN
82 } else if lambda.abs() < 1e-9 {
83 value.ln()
84 } else {
85 (value.powf(*lambda) - 1.0) / lambda
86 }
87 }
88 }
89 }
90
91 pub fn inverse(&self, value: f64) -> f64 {
93 match self {
94 ScaleTransform::Identity => value,
95 ScaleTransform::Log10 => 10f64.powf(value),
96 ScaleTransform::Log2 => 2f64.powf(value),
97 ScaleTransform::Ln => value.exp(),
98 ScaleTransform::Sqrt => value * value,
99 ScaleTransform::Reverse => -value,
100 ScaleTransform::Logit => 1.0 / (1.0 + (-value).exp()),
101 ScaleTransform::Probit => pnorm(value),
102 ScaleTransform::PseudoLog => 2.0 * value.sinh(),
103 ScaleTransform::Reciprocal => {
104 if value != 0.0 {
105 1.0 / value
106 } else {
107 f64::NAN
108 }
109 }
110 ScaleTransform::Exp => value.ln(),
111 ScaleTransform::BoxCox(lambda) => {
112 if lambda.abs() < 1e-9 {
113 value.exp()
114 } else {
115 (value * lambda + 1.0).powf(1.0 / lambda)
116 }
117 }
118 }
119 }
120
121 pub fn transform_value(&self, value: &Value) -> Value {
123 match value.as_f64() {
124 Some(f) => {
125 let t = self.apply(f);
126 if t.is_finite() || (f.is_infinite() && t.is_infinite()) {
128 Value::Float(t)
129 } else {
130 Value::Na
131 }
132 }
133 None => value.clone(),
134 }
135 }
136
137 pub fn is_identity(&self) -> bool {
138 matches!(self, ScaleTransform::Identity)
139 }
140}
141
142fn pnorm(x: f64) -> f64 {
144 0.5 * (1.0 + erf(x / std::f64::consts::SQRT_2))
145}
146
147fn erf(x: f64) -> f64 {
148 let sign = if x < 0.0 { -1.0 } else { 1.0 };
149 let x = x.abs();
150 let t = 1.0 / (1.0 + 0.327_591_1 * x);
151 let y = 1.0
152 - (((((1.061_405_429 * t - 1.453_152_027) * t) + 1.421_413_741) * t - 0.284_496_736) * t
153 + 0.254_829_592)
154 * t
155 * (-x * x).exp();
156 sign * y
157}
158
159fn qnorm(p: f64) -> f64 {
161 if p <= 0.0 {
162 return f64::NEG_INFINITY;
163 }
164 if p >= 1.0 {
165 return f64::INFINITY;
166 }
167 if p < 0.5 {
168 -rational_approx((-2.0 * p.ln()).sqrt())
169 } else if p > 0.5 {
170 rational_approx((-2.0 * (1.0 - p).ln()).sqrt())
171 } else {
172 0.0
173 }
174}
175
176fn rational_approx(t: f64) -> f64 {
177 let c0 = 2.515_517;
178 let c1 = 0.802_853;
179 let c2 = 0.010_328;
180 let d1 = 1.432_788;
181 let d2 = 0.189_269;
182 let d3 = 0.001_308;
183 t - (c0 + c1 * t + c2 * t * t) / (1.0 + d1 * t + d2 * t * t + d3 * t * t * t)
184}
185
186#[cfg(test)]
187mod tests {
188 use super::ScaleTransform::*;
189
190 fn roundtrip(t: super::ScaleTransform, v: f64, tol: f64) {
191 let back = t.inverse(t.apply(v));
192 assert!(
193 (back - v).abs() < tol,
194 "{t:?}: {v} -> {} -> {back}",
195 t.apply(v)
196 );
197 }
198
199 #[test]
200 fn transforms_roundtrip() {
201 roundtrip(Logit, 0.3, 1e-9);
202 roundtrip(Probit, 0.3, 1e-2); roundtrip(PseudoLog, -4.0, 1e-9);
204 roundtrip(PseudoLog, 0.0, 1e-9);
205 roundtrip(Reciprocal, 2.5, 1e-9);
206 roundtrip(Exp, 1.7, 1e-9);
207 roundtrip(BoxCox(0.5), 4.0, 1e-9);
208 roundtrip(BoxCox(0.0), 4.0, 1e-9); }
210
211 #[test]
212 fn transforms_domain_edges() {
213 assert_eq!(Logit.apply(0.0), f64::NEG_INFINITY);
214 assert_eq!(Logit.apply(1.0), f64::INFINITY);
215 assert!(Reciprocal.apply(0.0).is_nan());
216 assert!(BoxCox(0.5).apply(-1.0).is_nan());
217 assert_eq!(Probit.apply(0.5), 0.0);
218 assert!((BoxCox(0.0).apply(std::f64::consts::E) - 1.0).abs() < 1e-9);
220 }
221}