unit_root/tools/dickeyfuller.rs
1// Copyright (c) 2022. Sebastien Soudan
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http:www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use nalgebra::{RealField, Scalar};
16use num_traits::Float;
17
18use crate::distrib::Regression;
19use crate::prelude::nalgebra::DVector;
20use crate::prelude::tools::Report;
21use crate::regression::ols;
22use crate::tools::prepare;
23use crate::Error;
24
25/// Returns the t-statistic of the Dickey-Fuller test
26/// and the size of the sample.
27///
28/// The null hypothesis is that the series is non-stationary.
29///
30/// # Details
31///
32/// Critical values for can obtained from
33/// `unit_root::prelude::distrib::dickeyfuller::get_critical_value`.
34///
35/// - If $t_{stat} < \mathrm{t_{\mathrm{crit}}(\alpha)}$ then reject $H_0$ at
36/// $alpha$ significance level - and thus conclude that the series is stationary.
37/// - If $t_{stat} > \mathrm{t_{\mathrm{crit}}(\alpha)}$ then fail to reject $H_0$ at
38/// $alpha$ significance level - and thus conclude we cannot reject the hypothesis that
39/// the series is not stationary.
40///
41/// # Examples:
42///
43/// ```rust
44/// use unit_root::prelude::distrib::{AlphaLevel, Regression};
45/// use unit_root::prelude::nalgebra::DVector;
46/// use unit_root::prelude::*;
47///
48/// let y = DVector::from_row_slice(&[
49/// -0.89642362,
50/// 0.3222552,
51/// -1.96581989,
52/// -1.10012936,
53/// -1.3682928,
54/// 1.17239875,
55/// 2.19561259,
56/// 2.54295031,
57/// 2.05530587,
58/// 1.13212955,
59/// -0.42968979,
60/// ]);
61///
62/// let regression = Regression::Constant;
63///
64/// let report = tools::dickeyfuller_test(&y, regression).unwrap();
65///
66/// let critical_value =
67/// distrib::dickeyfuller::get_critical_value(regression, report.size, AlphaLevel::OnePercent)
68/// .unwrap();
69/// assert_eq!(report.size, 10);
70///
71/// let t_stat = report.test_statistic;
72/// println!("t-statistic: {}", t_stat);
73/// assert!((t_stat - -1.472691f64).abs() < 1e-6);
74/// assert!(t_stat > critical_value);
75/// ```
76pub fn dickeyfuller_test<F: Float + Scalar + RealField>(
77 series: &DVector<F>,
78 regression: Regression,
79) -> Result<Report<F>, Error> {
80 let (delta_y, y_t_1, size) = prepare(series, 0, regression)?;
81
82 let (_betas, t_stats) = ols(&delta_y, &y_t_1)?;
83
84 Ok(Report {
85 test_statistic: t_stats[0],
86 size,
87 })
88}
89
90/// Comparison with statsmodels.tsa.stattools.adfuller use the following code - see
91/// [`tools::adf_test::test`] for the definition of the function:
92/// ```python
93/// adf_test(y, maxlag=0, regression='n')
94/// adf_test(y, maxlag=0, regression='c')
95/// adf_test(y, maxlag=0, regression='ct')
96/// ```
97/// Note: maxlag is set to 0.
98#[cfg(test)]
99mod tests {
100 use approx::assert_relative_eq;
101 use rand::prelude::*;
102 use rand_chacha::ChaCha8Rng;
103
104 use super::*;
105 use crate::distrib::dickeyfuller::constant_no_trend_critical_value;
106 use crate::distrib::AlphaLevel;
107 use crate::utils::gen_ar_1;
108
109 const Y: [f64; 11] = [
110 -1.06714348,
111 -1.14700339,
112 0.79204106,
113 -0.05845247,
114 -0.67476754,
115 -0.10396661,
116 1.82059282,
117 -0.51169443,
118 2.07712365,
119 1.85668086,
120 2.56363688,
121 ];
122
123 #[test]
124 fn test_t_statistics_n() {
125 let y = DVector::from_row_slice(&Y[..]);
126
127 let report = dickeyfuller_test(&y, Regression::NoConstantNoTrend).unwrap();
128 assert_eq!(report.size, 10);
129 assert_relative_eq!(report.test_statistic, -1.5140129055f64, epsilon = 1e-9);
130 // Results of Dickey-Fuller Test:
131 // Test Statistic -1.5140129055
132 // p-value 0.121977783883
133 // #Lags Used 0
134 // Number of Observations Used 10
135 // Critical Value (1%) -2.82559
136 // Critical Value (5%) -1.970287
137 // Critical Value (10%) -1.592036
138 }
139
140 #[test]
141 fn test_t_statistics_c() {
142 let y = DVector::from_row_slice(&Y[..]);
143
144 let report = dickeyfuller_test(&y, Regression::Constant).unwrap();
145 assert_eq!(report.size, 10);
146 assert_relative_eq!(report.test_statistic, -1.83288396527f64, epsilon = 1e-9);
147 // Results of Dickey-Fuller Test:
148 // Test Statistic -1.83288396527
149 // p-value 0.364262207135
150 // #Lags Used 0
151 // Number of Observations Used 10
152 // Critical Value (1%) -4.331573
153 // Critical Value (5%) -3.23295
154 // Critical Value (10%) -2.7487
155 }
156
157 #[test]
158 fn test_t_statistics_ct() {
159 let y = DVector::from_row_slice(&Y[..]);
160
161 let report = dickeyfuller_test(&y, Regression::ConstantAndTrend).unwrap();
162 assert_eq!(report.size, 10);
163 assert_relative_eq!(report.test_statistic, -4.20337098854f64, epsilon = 1e-9);
164 // Results of Dickey-Fuller Test:
165 // Test Statistic -4.20337098854
166 // p-value 0.00442477220907
167 // #Lags Used 0
168 // Number of Observations Used 10
169 // Critical Value (1%) -5.282515
170 // Critical Value (5%) -3.985264
171 // Critical Value (10%) -3.44724
172 }
173
174 #[test]
175 fn test_dickeyfuller_no_unit_root_f32() {
176 let n = 100;
177
178 let mut rng = ChaCha8Rng::seed_from_u64(42);
179
180 let delta: f32 = 0.5;
181 let y = gen_ar_1(&mut rng, n, 0.0, delta, 1.0);
182
183 let report = dickeyfuller_test(&y, Regression::Constant).unwrap();
184
185 let critical_value =
186 match constant_no_trend_critical_value(report.size, AlphaLevel::OnePercent) {
187 Ok(v) => v,
188 Err(_) => f32::MIN,
189 };
190
191 let t_stat = report.test_statistic;
192 assert!(t_stat < critical_value);
193 }
194
195 #[test]
196 fn test_dickeyfuller_with_unit_root_f32() {
197 let n = 100;
198
199 let mut rng = ChaCha8Rng::seed_from_u64(42);
200
201 let delta: f32 = 1.0;
202 let y = gen_ar_1(&mut rng, n, 0.0, delta, 1.0);
203
204 let report = dickeyfuller_test(&y, Regression::Constant).unwrap();
205
206 let critical_value =
207 match constant_no_trend_critical_value(report.size, AlphaLevel::OnePercent) {
208 Ok(v) => v,
209 Err(_) => f32::MAX,
210 };
211
212 let t_stat = report.test_statistic;
213 assert!(t_stat > critical_value);
214 }
215
216 #[test]
217 fn test_dickeyfuller_no_unit_root_f64() {
218 let n = 100;
219
220 let mut rng = ChaCha8Rng::seed_from_u64(42);
221
222 let delta: f64 = 0.5;
223 let y = gen_ar_1(&mut rng, n, 0.0, delta, 1.0);
224
225 let report = dickeyfuller_test(&y, Regression::Constant).unwrap();
226
227 let critical_value =
228 match constant_no_trend_critical_value(report.size, AlphaLevel::OnePercent) {
229 Ok(v) => v,
230 Err(_) => f64::MIN,
231 };
232
233 let t_stat = report.test_statistic;
234 assert!(t_stat < critical_value);
235 }
236
237 #[test]
238 fn test_dickeyfuller_with_unit_root_f64() {
239 let n = 100;
240
241 let mut rng = ChaCha8Rng::seed_from_u64(42);
242
243 let delta: f64 = 1.0;
244 let y = gen_ar_1(&mut rng, n, 0.0, delta, 1.0);
245
246 let report = dickeyfuller_test(&y, Regression::Constant).unwrap();
247
248 let critical_value =
249 match constant_no_trend_critical_value(report.size, AlphaLevel::OnePercent) {
250 Ok(v) => v,
251 Err(_) => f64::MAX,
252 };
253
254 let t_stat = report.test_statistic;
255 assert!(t_stat > critical_value);
256 }
257
258 #[test]
259 fn no_enough_data() {
260 let y = DVector::from_row_slice(&[1.0]);
261 let report = dickeyfuller_test(&y, Regression::Constant);
262 assert!(report.is_err());
263 }
264
265 #[test]
266 fn test_constant_no_trend_test() {
267 let y = vec![1_f32, 3., 6., 10., 15., 21., 28., 36., 45., 55.];
268
269 let y = DVector::from(y);
270
271 let report = dickeyfuller_test(&y, Regression::Constant).unwrap();
272
273 assert_eq!(report.size, 9);
274 }
275}