ccboard_core/analytics/
forecasting.rs1use super::trends::TrendsData;
7
8#[derive(Debug, Clone)]
10pub struct ForecastData {
11 pub next_30_days_tokens: u64,
13 pub next_30_days_cost: f64,
15 pub monthly_cost_estimate: f64,
17 pub confidence: f64,
19 pub trend_direction: TrendDirection,
21 pub unavailable_reason: Option<String>,
23}
24
25#[derive(Debug, Clone)]
27pub enum TrendDirection {
28 Up(f64),
30 Down(f64),
32 Stable,
34}
35
36impl ForecastData {
37 pub fn unavailable(reason: &str) -> Self {
39 Self {
40 next_30_days_tokens: 0,
41 next_30_days_cost: 0.0,
42 monthly_cost_estimate: 0.0,
43 confidence: 0.0,
44 trend_direction: TrendDirection::Stable,
45 unavailable_reason: Some(reason.to_string()),
46 }
47 }
48}
49
50pub fn forecast_usage(trends: &TrendsData) -> ForecastData {
63 if trends.dates.len() < 7 {
64 return ForecastData::unavailable("Insufficient data (<7 days)");
65 }
66
67 let points: Vec<_> = trends
69 .daily_tokens
70 .iter()
71 .enumerate()
72 .map(|(i, &tokens)| (i as f64, tokens as f64))
73 .collect();
74
75 let (slope, intercept, r_squared) = linear_regression(&points);
77
78 let confidence = r_squared.clamp(0.0, 1.0);
81
82 let last_x = points.len() as f64;
84 let next_30_x = last_x + 30.0;
85 let next_30_days_tokens = (slope * next_30_x + intercept).max(0.0) as u64;
86
87 let total_cost: f64 = trends.daily_cost.iter().sum();
89 let total_tokens: u64 = trends.daily_tokens.iter().sum();
90 let cost_per_token = if total_tokens > 0 {
91 total_cost / total_tokens as f64
92 } else {
93 0.01 / 1000.0 };
95 let next_30_days_cost = next_30_days_tokens as f64 * cost_per_token;
96
97 let days_in_period = trends.dates.len() as f64;
99 let monthly_cost_estimate = (total_cost / days_in_period) * 30.0;
100
101 let trend_direction = if slope.abs() < 0.01 * intercept.abs() {
103 TrendDirection::Stable
104 } else if slope > 0.0 {
105 let increase_pct = (slope * 30.0 / intercept.abs() * 100.0).abs();
106 TrendDirection::Up(increase_pct)
107 } else {
108 let decrease_pct = (slope * 30.0 / intercept.abs() * 100.0).abs();
109 TrendDirection::Down(decrease_pct)
110 };
111
112 ForecastData {
113 next_30_days_tokens,
114 next_30_days_cost,
115 monthly_cost_estimate,
116 confidence,
117 trend_direction,
118 unavailable_reason: None,
119 }
120}
121
122fn linear_regression(points: &[(f64, f64)]) -> (f64, f64, f64) {
129 let n = points.len() as f64;
130 let sum_x: f64 = points.iter().map(|p| p.0).sum();
131 let sum_y: f64 = points.iter().map(|p| p.1).sum();
132 let sum_xx: f64 = points.iter().map(|p| p.0 * p.0).sum();
133 let sum_xy: f64 = points.iter().map(|p| p.0 * p.1).sum();
134
135 let slope = (n * sum_xy - sum_x * sum_y) / (n * sum_xx - sum_x * sum_x);
137 let intercept = (sum_y - slope * sum_x) / n;
138
139 let mean_y = sum_y / n;
141 let ss_tot: f64 = points.iter().map(|p| (p.1 - mean_y).powi(2)).sum();
142 let ss_res: f64 = points
143 .iter()
144 .map(|p| {
145 let predicted = slope * p.0 + intercept;
146 (p.1 - predicted).powi(2)
147 })
148 .sum();
149
150 let r_squared = if ss_tot > 0.0 {
151 1.0 - (ss_res / ss_tot)
152 } else {
153 0.0
154 };
155
156 (slope, intercept, r_squared)
157}