use std::fmt::Display;
use ndarray::Array1;
#[cfg(feature = "serde")]
use serde::{Serialize, Deserialize};
use crate::error::{DigiFiError, ErrorTitle};
use crate::utilities::LARGE_TEXT_BREAK;
use crate::statistics::{
ProbabilityDistribution, se_lr_coefficient,
continuous_distributions::StudentsTDistribution,
stat_tests::ConfidenceLevel,
};
#[derive(Clone, Copy, Debug)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct TTestResult {
pub reject_h0: bool,
pub t_score: f64,
pub dof: f64,
pub p_value: f64,
pub p_cl: f64,
}
impl Display for TTestResult {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let result: String = LARGE_TEXT_BREAK.to_owned()
+ "t-Test Result\n"
+ LARGE_TEXT_BREAK
+ &format!("Degrees of Freedom: {}\n", self.dof)
+ &format!("Reject Null Hypothesis: {}\n", self.reject_h0)
+ &format!("t-Test t-statistic: {}\n", self.t_score)
+ &format!("t-Test p-value: {}\n", self.p_value)
+ &format!("Confidence Level: {}\n", self.p_cl)
+ LARGE_TEXT_BREAK;
write!(f, "{}", result)
}
}
#[derive(Clone, Copy, Debug, Default)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum TTestTwoSampleCase {
#[default]
EqualVariance,
SimilarVariance,
UnequalVariance,
}
impl TTestTwoSampleCase {
fn unbiased_std_ratio(sample_1: &Array1<f64>, sample_2: &Array1<f64>) -> f64 {
sample_1.std(1.0) / sample_2.std(1.0)
}
pub fn select_case(sample_1: &Array1<f64>, sample_2: &Array1<f64>) -> Self {
if sample_1.len() == sample_2.len() {
Self::EqualVariance
} else {
let unbiased_std_ratio: f64 = Self::unbiased_std_ratio(sample_1, sample_2);
if 0.5 <= unbiased_std_ratio && unbiased_std_ratio <= 2.0 {
Self::SimilarVariance
} else {
Self::UnequalVariance
}
}
}
fn validate(&self, sample_1: &Array1<f64>, sample_2: &Array1<f64>) -> Result<(), DigiFiError> {
if sample_1.len() < 1 || sample_2.len() < 1 {
return Err(DigiFiError::ParameterConstraint {
title: Self::error_title(),
constraint: "Both samples must contain at least one data point.".to_owned(),
});
}
let unbiased_std_ratio: f64 = Self::unbiased_std_ratio(sample_1, sample_2);
match self {
Self::EqualVariance => {
if sample_1.len() != sample_2.len() {
return Err(DigiFiError::UnmatchingLength { array_1: "sample_1".to_owned(), array_2: "sample_2".to_owned(), })
}
},
Self::SimilarVariance => {
if unbiased_std_ratio < 0.5 || 2.0 < unbiased_std_ratio {
return Err(DigiFiError::ParameterConstraint {
title: Self::error_title(),
constraint: "The unbiased estimators of variance of `sample_1` and `sample_2` have to satisfy the unequality `0.5 < s^{2}_{1} / s^{2}_{2} < 2` to use `SimilarVariance` two sample t-test case.".to_owned(),
})
}
},
Self::UnequalVariance => {
if 0.5 <= unbiased_std_ratio && unbiased_std_ratio <= 2.0 {
return Err(DigiFiError::ParameterConstraint {
title: Self::error_title(),
constraint: "The unbiased estimators of variance of `sample_1` and `sample_2` have to satisfy either the unequality `s_{1} > 2s_{2}` or `2s_{1} < s_{2}` to use `UnequalVariance` two sample t-test case.".to_owned(),
})
}
},
}
Ok(())
}
}
impl ErrorTitle for TTestTwoSampleCase {
fn error_title() -> String {
String::from("T-test (Two Sample) Case")
}
}
impl Display for TTestTwoSampleCase {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::EqualVariance => write!(f, "Equal Variance"),
Self::SimilarVariance => write!(f, "Similar Variance"),
Self::UnequalVariance => write!(f, "Unequal Variance"),
}
}
}
pub fn t_test_two_sample(sample_1: &Array1<f64>, sample_2: &Array1<f64>, cl: Option<ConfidenceLevel>, case: Option<TTestTwoSampleCase>) -> Result<TTestResult, DigiFiError> {
let error_title: String = String::from("T-test (Two Sample)");
let case: TTestTwoSampleCase = match case {
Some(v) => v,
None => TTestTwoSampleCase::select_case(sample_1, sample_2),
};
case.validate(sample_1, sample_2)?;
let sample_1_len: f64 = sample_1.len() as f64;
let sample_2_len: f64 = sample_2.len() as f64;
let mean_1: f64 = sample_1.mean().ok_or(DigiFiError::MeanCalculation { title: error_title.clone(), series: "series_1".to_owned(), })?;
let mean_2: f64 = sample_2.mean().ok_or(DigiFiError::MeanCalculation { title: error_title, series: "series_2".to_owned(), })?;
let unbiased_var_1: f64 = sample_1.var(1.0);
let unbiased_var_2: f64 = sample_2.var(1.0);
let (t_score, dof) = match case {
TTestTwoSampleCase::EqualVariance => {
let dof: f64 = sample_1_len;
let pooled_std: f64 = ((unbiased_var_1 + unbiased_var_2) / 2.0).sqrt();
let t_score: f64 = (mean_1 - mean_2) / (pooled_std * (2.0/dof).sqrt());
(t_score, dof)
},
TTestTwoSampleCase::SimilarVariance => {
let dof: f64 = sample_1_len + sample_2_len - 2.0;
let pooled_std: f64 = (((sample_1_len - 1.0) * unbiased_var_1 + (sample_2_len - 1.0) * unbiased_var_2) / dof).sqrt();
let t_score: f64 = (mean_1 - mean_2) / (pooled_std * (1.0/sample_1_len + 1.0/sample_2_len).sqrt());
(t_score, dof)
},
TTestTwoSampleCase::UnequalVariance => {
let scaled_unbiased_var_1: f64 = unbiased_var_1 / sample_1_len;
let scaled_unbiased_var_2: f64 = unbiased_var_2 / sample_2_len;
let dof: f64 = (scaled_unbiased_var_1 + scaled_unbiased_var_2).powi(2) / (scaled_unbiased_var_1.powi(2)/(sample_1_len - 1.0) + scaled_unbiased_var_2.powi(2)/(sample_2_len - 1.0));
let pooled_std: f64 = (scaled_unbiased_var_1 + scaled_unbiased_var_2).sqrt();
let t_score: f64 = (mean_1 - mean_2) / pooled_std;
(t_score, dof)
},
};
let p_value: f64 = 1.0 - StudentsTDistribution::build(dof)?.cdf(t_score)?;
let p_cl: f64 = 1.0 - cl.unwrap_or_default().get_p();
let reject_h0: bool = if p_cl < p_value { true } else { false };
Ok(TTestResult { reject_h0, t_score, dof, p_value, p_cl })
}
pub fn t_test_lr(beta: f64, beta_0: Option<f64>, y: &Array1<f64>, y_prediction: &Array1<f64>, x: &Array1<f64>, ddof: usize, cl: Option<ConfidenceLevel>) -> Result<TTestResult, DigiFiError> {
let beta_0: f64 = match beta_0 { Some(v) => v, None => 0.0, };
let se_beta: f64 = se_lr_coefficient(y, y_prediction, x, ddof)?;
let t_score: f64 = (beta - beta_0) / se_beta;
let dof: f64 = y.len()
.checked_sub(ddof)
.ok_or(DigiFiError::Other {
title: "T-test (Linear Regression)".to_owned(),
details: "There are fewer data points in `y` array than `ddof`.".to_owned(),
})? as f64;
let p_value: f64 = 1.0 - StudentsTDistribution::build(dof)?.cdf(t_score)?;
let p_cl: f64 = cl.unwrap_or_default().get_p();
let reject_h0: bool = if p_value < p_cl { true } else { false };
Ok(TTestResult { reject_h0, t_score, dof, p_value, p_cl })
}
#[cfg(test)]
mod tests {
#[cfg(all(test, feature = "sample_data"))]
#[test]
fn unit_test_t_test_two_sample() -> () {
use ndarray::Array1;
use crate::utilities::sample_data::SampleData;
use crate::statistics::stat_tests::{ConfidenceLevel, TTestResult, TTestTwoSampleCase, t_test_two_sample};
let sample: SampleData = SampleData::Portfolio;
let (_, mut sample_data) = sample.load_sample_data();
let jpm: Array1<f64> = sample_data.remove("JPM").unwrap();
let gs: Array1<f64> = sample_data.remove("GS").unwrap();
let test_result: TTestResult = t_test_two_sample(
&jpm, &gs, Some(ConfidenceLevel::Five), Some(TTestTwoSampleCase::EqualVariance)
).unwrap();
assert_eq!(test_result.reject_h0, true);
}
#[cfg(all(test, feature = "sample_data"))]
#[test]
fn unit_test_t_test_lr() -> () {
use ndarray::Array1;
use crate::utilities::sample_data::SampleData;
use crate::corporate_finance::capm::{CAPMParams, CAPMSolutionType, CAPMType, CAPM};
use crate::statistics::stat_tests::{ConfidenceLevel, TTestResult, t_test_lr};
let sample: SampleData = SampleData::CAPM;
let (_, mut sample_data) = sample.load_sample_data();
let capm_type: CAPMType = CAPMType::FiveFactorFamaFrench {
smb: sample_data.remove("SMB").unwrap(), hml: sample_data.remove("HML").unwrap(),
rmw: sample_data.remove("RMW").unwrap(), cma: sample_data.remove("CMA").unwrap(),
};
let market_return: Array1<f64> = sample_data.remove("Market").unwrap();
let rf: Array1<f64> = sample_data.remove("RF").unwrap();
let solution_type: CAPMSolutionType = CAPMSolutionType::LinearRegression;
let capm: CAPM = CAPM::build(market_return.clone(), rf.clone(), capm_type, solution_type).unwrap();
let y: Array1<f64> = sample_data.remove("Stock Returns").unwrap();
let params: CAPMParams = capm.get_parameters(&y).unwrap();
let x: Array1<f64> = market_return - rf;
let y_prediction: Array1<f64> = capm.predict_asset_return(
params.alpha.unwrap(), params.beta, params.beta_s.unwrap_or(0.0), params.beta_h.unwrap_or(0.0),
params.beta_r.unwrap_or(0.0), params.beta_c.unwrap_or(0.0)
).unwrap();
let ddof: usize = y.len() - 6;
let test_result: TTestResult = t_test_lr(params.beta, None, &y, &y_prediction, &x, ddof, Some(ConfidenceLevel::Five)).unwrap();
assert_eq!(test_result.reject_h0, true);
let test_result: TTestResult = t_test_lr(params.beta, Some(1.0), &y, &y_prediction, &x, ddof, Some(ConfidenceLevel::Five)).unwrap();
assert_eq!(test_result.reject_h0, false);
}
}