use super::*;
#[test]
fn apriori_finds_frequent_singletons() -> Result<(), AssociationError> {
let t = vec![
vec![true, true, false],
vec![true, true, true],
vec![true, true, false],
vec![false, false, false],
];
let frequent = apriori(&t, 0.5)?;
let singles: Vec<&FrequentItemset> = frequent.iter().filter(|s| s.items().len() == 1).collect();
assert_eq!(singles.len(), 2, "two frequent singletons, got {singles:?}");
let s0 = singles
.iter()
.find(|s| s.items() == [0])
.map_or(f64::NAN, |s| s.support());
assert!((s0 - 0.75).abs() < 1e-12, "item 0 support was {s0}");
Ok(())
}
#[test]
fn apriori_finds_frequent_pairs_and_triples() -> Result<(), AssociationError> {
let t = vec![
vec![true, true, true],
vec![true, true, true],
vec![true, true, false],
vec![false, false, false],
];
let frequent = apriori(&t, 0.5)?;
let pair01 = frequent
.iter()
.find(|s| s.items() == [0, 1])
.map_or(f64::NAN, FrequentItemset::support);
assert!(
(pair01 - 0.75).abs() < 1e-12,
"pair {{0,1}} support was {pair01}"
);
let triple = frequent
.iter()
.find(|s| s.items() == [0, 1, 2])
.map_or(f64::NAN, FrequentItemset::support);
assert!((triple - 0.5).abs() < 1e-12, "triple support was {triple}");
Ok(())
}
#[test]
fn rule_metrics_match_hand_values() -> Result<(), String> {
let t = vec![
vec![true, true],
vec![true, true],
vec![true, false],
vec![false, true],
];
let frequent = apriori(&t, 0.25).map_err(|e| e.to_string())?;
let rules =
association_rules(&frequent, RuleMetric::Confidence, 0.0).map_err(|e| e.to_string())?;
let r = rules
.iter()
.find(|r| r.antecedent() == [0] && r.consequent() == [1])
.ok_or_else(|| "rule {0}=>{1} present".to_owned())?;
assert!(
(r.confidence() - 2.0 / 3.0).abs() < 1e-12,
"conf was {}",
r.confidence()
);
assert!(
(r.lift() - 8.0 / 9.0).abs() < 1e-12,
"lift was {}",
r.lift()
);
assert!(
(r.leverage() - (-0.0625)).abs() < 1e-12,
"leverage was {}",
r.leverage()
);
assert!(
(r.conviction() - 0.75).abs() < 1e-12,
"conviction was {}",
r.conviction()
);
assert!(
(r.support() - 0.5).abs() < 1e-12,
"support was {}",
r.support()
);
Ok(())
}
#[test]
fn perfect_confidence_gives_infinite_conviction() -> Result<(), String> {
let t = vec![vec![true, true], vec![true, true], vec![false, true]];
let frequent = apriori(&t, 0.25).map_err(|e| e.to_string())?;
let rules =
association_rules(&frequent, RuleMetric::Confidence, 0.0).map_err(|e| e.to_string())?;
let r = rules
.iter()
.find(|r| r.antecedent() == [0] && r.consequent() == [1])
.ok_or_else(|| "rule {0}=>{1} present".to_owned())?;
assert!(
(r.confidence() - 1.0).abs() < 1e-12,
"conf was {}",
r.confidence()
);
assert!(
r.conviction().is_infinite(),
"conviction was {}",
r.conviction()
);
Ok(())
}
#[test]
fn metric_threshold_filters_rules() -> Result<(), AssociationError> {
let t = vec![
vec![true, true],
vec![true, true],
vec![true, false],
vec![false, true],
];
let frequent = apriori(&t, 0.25)?;
let strict = association_rules(&frequent, RuleMetric::Confidence, 0.9)?;
assert!(
!strict
.iter()
.any(|r| r.antecedent() == [0] && r.consequent() == [1]),
"0.9 floor must drop the 2/3-confidence rule, got {strict:?}"
);
let loose = association_rules(&frequent, RuleMetric::Confidence, 0.5)?;
assert!(
loose
.iter()
.any(|r| r.antecedent() == [0] && r.consequent() == [1]),
"0.5 floor must keep the 2/3-confidence rule"
);
Ok(())
}
#[test]
fn triple_generates_all_partitions() -> Result<(), AssociationError> {
let t = vec![vec![true, true, true], vec![true, true, true]];
let frequent = apriori(&t, 0.5)?;
let rules = association_rules(&frequent, RuleMetric::Support, 0.0)?;
let from_triple: Vec<&AssociationRule> = rules
.iter()
.filter(|r| r.antecedent().len() + r.consequent().len() == 3)
.collect();
assert_eq!(
from_triple.len(),
6,
"6 partitions of a triple, got {}",
from_triple.len()
);
assert!(
from_triple.iter().any(|r| r.antecedent().len() == 2),
"a pair-antecedent rule must be present"
);
Ok(())
}
#[test]
fn mining_is_deterministic() -> Result<(), AssociationError> {
let t = vec![
vec![true, true, false],
vec![true, false, true],
vec![true, true, true],
vec![false, true, true],
];
let a = apriori(&t, 0.25)?;
let b = apriori(&t, 0.25)?;
assert_eq!(a, b, "frequent itemsets must be deterministic");
let ra = association_rules(&a, RuleMetric::Lift, 0.0)?;
let rb = association_rules(&b, RuleMetric::Lift, 0.0)?;
assert_eq!(ra, rb, "rules must be deterministic");
Ok(())
}
#[test]
fn empty_matrix_is_rejected() {
let t: Vec<Vec<bool>> = Vec::new();
assert_eq!(apriori(&t, 0.5), Err(AssociationError::EmptyInput));
}
#[test]
fn no_items_is_rejected() {
let t = vec![Vec::new(), Vec::new()];
assert_eq!(apriori(&t, 0.5), Err(AssociationError::NoItems));
}
#[test]
fn ragged_rows_are_rejected() {
let t = vec![vec![true, false], vec![true]];
assert_eq!(apriori(&t, 0.5), Err(AssociationError::RaggedRows));
}
#[test]
fn invalid_support_is_rejected() {
let t = vec![vec![true], vec![false]];
assert_eq!(apriori(&t, 0.0), Err(AssociationError::InvalidSupport));
assert_eq!(apriori(&t, 1.5), Err(AssociationError::InvalidSupport));
assert_eq!(apriori(&t, f64::NAN), Err(AssociationError::InvalidSupport));
}
#[test]
fn invalid_threshold_is_rejected() -> Result<(), AssociationError> {
let t = vec![vec![true, true], vec![true, true]];
let frequent = apriori(&t, 0.5)?;
assert_eq!(
association_rules(&frequent, RuleMetric::Confidence, f64::INFINITY),
Err(AssociationError::InvalidThreshold)
);
Ok(())
}