use rill_ml::feature_hasher::FeatureHasher;
use rill_ml::loss::BinaryLogLoss;
use rill_ml::metrics::{F1Score, LogLoss};
use rill_ml::models::{
FtrlClassifier, FtrlConfig, GaussianNaiveBayes, LogisticRegression, LogisticRegressionConfig,
NewFeaturePolicy,
};
use rill_ml::optim::{Optimizer, SgdConfig};
use rill_ml::{Metric, OnlineBinaryClassifier, SparseClassifier};
struct ClickEvent {
features: Vec<(&'static str, f64)>,
clicked: bool,
}
fn generate_data() -> Vec<ClickEvent> {
vec![
ClickEvent {
features: vec![
("user=alice", 1.0),
("device=mobile", 1.0),
("hour=morning", 1.0),
],
clicked: true,
},
ClickEvent {
features: vec![
("user=alice", 1.0),
("device=desktop", 1.0),
("hour=evening", 1.0),
],
clicked: true,
},
ClickEvent {
features: vec![
("user=bob", 1.0),
("device=mobile", 1.0),
("hour=afternoon", 1.0),
],
clicked: true,
},
ClickEvent {
features: vec![
("user=bob", 1.0),
("device=desktop", 1.0),
("hour=morning", 1.0),
],
clicked: false,
},
ClickEvent {
features: vec![
("user=charlie", 1.0),
("device=mobile", 1.0),
("hour=evening", 1.0),
],
clicked: false,
},
ClickEvent {
features: vec![
("user=charlie", 1.0),
("device=desktop", 1.0),
("hour=afternoon", 1.0),
],
clicked: false,
},
ClickEvent {
features: vec![
("user=alice", 1.0),
("device=mobile", 1.0),
("hour=afternoon", 1.0),
],
clicked: true,
},
ClickEvent {
features: vec![
("user=bob", 1.0),
("device=mobile", 1.0),
("hour=evening", 1.0),
],
clicked: true,
},
ClickEvent {
features: vec![
("user=charlie", 1.0),
("device=mobile", 1.0),
("hour=morning", 1.0),
],
clicked: false,
},
ClickEvent {
features: vec![
("user=alice", 1.0),
("device=desktop", 1.0),
("hour=afternoon", 1.0),
],
clicked: true,
},
ClickEvent {
features: vec![
("user=bob", 1.0),
("device=desktop", 1.0),
("hour=evening", 1.0),
],
clicked: false,
},
ClickEvent {
features: vec![
("user=charlie", 1.0),
("device=desktop", 1.0),
("hour=morning", 1.0),
],
clicked: false,
},
]
}
fn main() {
let data = generate_data();
let hasher = FeatureHasher::new(64, 42).unwrap();
let mut ftrl_config = FtrlConfig::default();
ftrl_config.alpha = 0.5;
ftrl_config.beta = 1.0;
ftrl_config.l1 = 2.0;
ftrl_config.l2 = 0.5;
ftrl_config.max_features = None;
ftrl_config.new_feature_policy = NewFeaturePolicy::default();
let mut ftrl = FtrlClassifier::new(ftrl_config).unwrap();
let d = 64;
let mut sgd = SgdConfig::default();
sgd.learning_rate = 0.1;
sgd.l2 = 0.01;
let mut lr_config = LogisticRegressionConfig::default();
lr_config.optimizer = Optimizer::sgd(d, sgd).unwrap();
lr_config.loss = BinaryLogLoss::default();
let log_reg = LogisticRegression::new(d, lr_config).unwrap();
let mut log_reg = log_reg;
let mut nb = GaussianNaiveBayes::new(d, Default::default()).unwrap();
let mut ftrl_f1 = F1Score::default();
let mut logreg_f1 = F1Score::default();
let mut nb_f1 = F1Score::default();
let mut ftrl_logloss = LogLoss::default();
let mut logreg_logloss = LogLoss::default();
let mut nb_logloss = LogLoss::default();
for event in &data {
let sparse = hasher.hash_strings(&event.features).unwrap();
let dense = hasher.transform(&sparse).unwrap();
let ftrl_proba = ftrl.predict_proba(&sparse).unwrap();
let ftrl_pred = ftrl_proba >= 0.5;
ftrl_f1.update(event.clicked, ftrl_pred).unwrap();
ftrl_logloss.update(event.clicked, ftrl_proba).unwrap();
let logreg_proba = log_reg.predict_proba(&dense).unwrap();
let logreg_pred = logreg_proba >= 0.5;
logreg_f1.update(event.clicked, logreg_pred).unwrap();
logreg_logloss.update(event.clicked, logreg_proba).unwrap();
let nb_proba = nb.predict_proba(&dense).unwrap();
let nb_pred = nb_proba >= 0.5;
nb_f1.update(event.clicked, nb_pred).unwrap();
nb_logloss.update(event.clicked, nb_proba).unwrap();
ftrl.learn(&sparse, event.clicked).unwrap();
log_reg.learn(&dense, event.clicked).unwrap();
nb.learn(&dense, event.clicked).unwrap();
}
println!("=== Click Prediction Results ({} samples) ===", data.len());
println!();
println!("{:<25} {:>10} {:>10}", "Model", "F1", "LogLoss");
println!("{:-<47}", "");
let f1_val = ftrl_f1.value().unwrap_or(0.0);
let ll_val = ftrl_logloss.value().unwrap_or(0.0);
println!("{:<25} {:>10.4} {:>10.4}", "FTRL (sparse)", f1_val, ll_val);
let f1_val = logreg_f1.value().unwrap_or(0.0);
let ll_val = logreg_logloss.value().unwrap_or(0.0);
println!(
"{:<25} {:>10.4} {:>10.4}",
"Logistic (hashed)", f1_val, ll_val
);
let f1_val = nb_f1.value().unwrap_or(0.0);
let ll_val = nb_logloss.value().unwrap_or(0.0);
println!(
"{:<25} {:>10.4} {:>10.4}",
"GaussianNB (hashed)", f1_val, ll_val
);
let weights = ftrl.weights();
let total_features = ftrl.feature_count();
let nonzero = weights.len();
println!();
println!("=== FTRL Sparsity ===");
println!("Total features seen: {}", total_features);
println!("Non-zero weights: {}", nonzero);
if total_features > 0 {
let sparsity = 1.0 - nonzero as f64 / total_features as f64;
println!("Sparsity ratio: {:.1}%", sparsity * 100.0);
}
println!();
println!("=== Non-zero FTRL Weights ===");
for (id, w) in &weights {
println!(" Feature {:<6}: {:>+.6}", id, w);
}
}