1use super::*;
2
3impl StatisticalComputingLibrary {
4 pub fn new() -> Self {
6 Self {
7 data_storage: StatisticalDataStorage::new(),
8 computation_engine: StatisticalComputationEngine::new(),
9 privacy_engine: StatisticalPrivacyEngine::new(),
10 analysis_engine: StatisticalAnalysisEngine::new(),
11 performance_monitor: StatisticalPerformanceMonitor::new(),
12 }
13 }
14
15 pub fn initialize(&mut self) -> Result<(), StatisticalError> {
17 self.data_storage.initialize()?;
19
20 self.computation_engine.initialize()?;
22
23 self.privacy_engine.initialize()?;
25
26 self.analysis_engine.initialize()?;
28
29 Ok(())
30 }
31
32 pub fn create_dataset(
34 &mut self,
35 dataset_id: String,
36 data: Vec<Vec<DataValue>>,
37 column_names: Vec<String>,
38 column_types: Vec<DataType>,
39 privacy_level: PrivacyLevel,
40 ) -> Result<Dataset, StatisticalError> {
41 if data.is_empty() {
43 return Err(StatisticalError::InvalidData(
44 "Dataset cannot be empty".to_string(),
45 ));
46 }
47 if column_names.len() != column_types.len() {
48 return Err(StatisticalError::InvalidData(
49 "Column names and types must match".to_string(),
50 ));
51 }
52 if data.iter().any(|row| row.len() != column_names.len()) {
53 return Err(StatisticalError::InvalidData(
54 "All rows must have same number of columns".to_string(),
55 ));
56 }
57
58 let metadata = DatasetMetadata {
60 dataset_id: dataset_id.clone(),
61 dataset_type: DatasetType::Mixed,
62 dimensions: DatasetDimensions {
63 rows: data.len(),
64 columns: column_names.len(),
65 time_steps: None,
66 features: Some(column_names.len()),
67 },
68 data_types: column_types.clone(),
69 sample_size: data.len(),
70 created_at: std::time::SystemTime::now()
71 .duration_since(std::time::UNIX_EPOCH)
72 .unwrap()
73 .as_secs(),
74 last_updated: 0,
75 access_count: 0,
76 privacy_level,
77 };
78
79 let dataset = Dataset {
81 dataset_id: dataset_id.clone(),
82 metadata,
83 data,
84 column_names,
85 column_types,
86 };
87
88 self.data_storage.store_dataset(dataset.clone())?;
90
91 Ok(dataset)
92 }
93
94 pub fn mean(
96 &mut self,
97 dataset_id: &str,
98 column: &str,
99 privacy_preserved: bool,
100 ) -> Result<StatisticalAnalysisResult<f64>, StatisticalError> {
101 let start_time = std::time::Instant::now();
102
103 let dataset = self.data_storage.get_dataset(dataset_id)?;
105
106 let column_index = dataset
108 .column_names
109 .iter()
110 .position(|name| name == column)
111 .ok_or_else(|| StatisticalError::InvalidColumn(column.to_string()))?;
112
113 if !matches!(
115 dataset.column_types[column_index],
116 DataType::Float32 | DataType::Float64
117 ) {
118 return Err(StatisticalError::InvalidOperation(
119 "Mean can only be computed on numeric columns".to_string(),
120 ));
121 }
122
123 let mut values = Vec::new();
125 for row in &dataset.data {
126 match &row[column_index] {
127 DataValue::Float(value) => values.push(*value),
128 DataValue::Integer(value) => values.push(*value as f64),
129 DataValue::Null => continue,
130 _ => {
131 return Err(StatisticalError::InvalidOperation(
132 "Non-numeric data in column".to_string(),
133 ))
134 }
135 }
136 }
137
138 if values.is_empty() {
139 return Err(StatisticalError::InvalidData(
140 "No valid data in column".to_string(),
141 ));
142 }
143
144 let mean = crate::solvers::statistics::mean(&values)
147 .ok_or_else(|| StatisticalError::InvalidData("No valid data in column".to_string()))?;
148
149 let execution_time = start_time.elapsed().as_millis() as u64;
150
151 let (final_mean, privacy_cost) = if privacy_preserved {
156 let sensitivity = {
157 let analyzer = &mut self
158 .privacy_engine
159 .differential_privacy
160 .sensitivity_analyzer;
161 analyzer.get_sensitivity("mean", &values).unwrap_or(1.0)
162 };
163 let (noisy_mean, cost) = self.privacy_engine.add_laplace_noise(mean, sensitivity)?;
164 (noisy_mean, cost)
165 } else {
166 (mean, 0.0)
167 };
168
169 self.performance_monitor
171 .record_operation("mean", execution_time, 0, privacy_cost);
172
173 Ok(StatisticalAnalysisResult {
174 result: final_mean,
175 execution_time,
176 memory_usage: 0,
177 sample_size: values.len(),
178 confidence_level: 0.95,
179 privacy_preserved,
180 privacy_cost,
181 })
182 }
183
184 pub fn median(
186 &mut self,
187 dataset_id: &str,
188 column: &str,
189 privacy_preserved: bool,
190 ) -> Result<StatisticalAnalysisResult<f64>, StatisticalError> {
191 let start_time = std::time::Instant::now();
192
193 let dataset = self.data_storage.get_dataset(dataset_id)?;
195
196 let column_index = dataset
198 .column_names
199 .iter()
200 .position(|name| name == column)
201 .ok_or_else(|| StatisticalError::InvalidColumn(column.to_string()))?;
202
203 if !matches!(
205 dataset.column_types[column_index],
206 DataType::Float32 | DataType::Float64
207 ) {
208 return Err(StatisticalError::InvalidOperation(
209 "Median can only be computed on numeric columns".to_string(),
210 ));
211 }
212
213 let mut values = Vec::new();
215 for row in &dataset.data {
216 match &row[column_index] {
217 DataValue::Float(value) => values.push(*value),
218 DataValue::Integer(value) => values.push(*value as f64),
219 DataValue::Null => continue,
220 _ => {
221 return Err(StatisticalError::InvalidOperation(
222 "Non-numeric data in column".to_string(),
223 ))
224 }
225 }
226 }
227
228 if values.is_empty() {
229 return Err(StatisticalError::InvalidData(
230 "No valid data in column".to_string(),
231 ));
232 }
233
234 let median = crate::solvers::statistics::median_in_place(&mut values)
237 .ok_or_else(|| StatisticalError::InvalidData("No valid data in column".to_string()))?;
238
239 let execution_time = start_time.elapsed().as_millis() as u64;
240
241 let (final_median, privacy_cost) = if privacy_preserved {
243 let (noisy_median, cost) = self.privacy_engine.add_laplace_noise(median, 1.0)?;
244 (noisy_median, cost)
245 } else {
246 (median, 0.0)
247 };
248
249 self.performance_monitor
251 .record_operation("median", execution_time, 0, privacy_cost);
252
253 Ok(StatisticalAnalysisResult {
254 result: final_median,
255 execution_time,
256 memory_usage: 0,
257 sample_size: values.len(),
258 confidence_level: 0.95,
259 privacy_preserved,
260 privacy_cost,
261 })
262 }
263
264 pub fn variance(
266 &mut self,
267 dataset_id: &str,
268 column: &str,
269 sample: bool,
270 privacy_preserved: bool,
271 ) -> Result<StatisticalAnalysisResult<f64>, StatisticalError> {
272 let start_time = std::time::Instant::now();
273
274 let dataset = self.data_storage.get_dataset(dataset_id)?;
276
277 let column_index = dataset
279 .column_names
280 .iter()
281 .position(|name| name == column)
282 .ok_or_else(|| StatisticalError::InvalidColumn(column.to_string()))?;
283
284 if !matches!(
286 dataset.column_types[column_index],
287 DataType::Float32 | DataType::Float64
288 ) {
289 return Err(StatisticalError::InvalidOperation(
290 "Variance can only be computed on numeric columns".to_string(),
291 ));
292 }
293
294 let mut values = Vec::new();
296 for row in &dataset.data {
297 match &row[column_index] {
298 DataValue::Float(value) => values.push(*value),
299 DataValue::Integer(value) => values.push(*value as f64),
300 DataValue::Null => continue,
301 _ => {
302 return Err(StatisticalError::InvalidOperation(
303 "Non-numeric data in column".to_string(),
304 ))
305 }
306 }
307 }
308
309 if values.is_empty() {
310 return Err(StatisticalError::InvalidData(
311 "No valid data in column".to_string(),
312 ));
313 }
314
315 let variance = crate::solvers::statistics::variance(&values, sample)
318 .ok_or_else(|| StatisticalError::InvalidData("No valid data in column".to_string()))?;
319
320 let execution_time = start_time.elapsed().as_millis() as u64;
321
322 let (final_variance, privacy_cost) = if privacy_preserved {
324 let (noisy_variance, cost) = self.privacy_engine.add_laplace_noise(variance, 1.0)?;
325 (noisy_variance, cost)
326 } else {
327 (variance, 0.0)
328 };
329
330 self.performance_monitor
332 .record_operation("variance", execution_time, 0, privacy_cost);
333
334 Ok(StatisticalAnalysisResult {
335 result: final_variance,
336 execution_time,
337 memory_usage: 0,
338 sample_size: values.len(),
339 confidence_level: 0.95,
340 privacy_preserved,
341 privacy_cost,
342 })
343 }
344
345 pub fn correlation(
347 &mut self,
348 dataset_id: &str,
349 column1: &str,
350 column2: &str,
351 method: CorrelationMethod,
352 privacy_preserved: bool,
353 ) -> Result<StatisticalAnalysisResult<f64>, StatisticalError> {
354 let start_time = std::time::Instant::now();
355
356 let dataset = self.data_storage.get_dataset(dataset_id)?;
358
359 let column1_index = dataset
361 .column_names
362 .iter()
363 .position(|name| name == column1)
364 .ok_or_else(|| StatisticalError::InvalidColumn(column1.to_string()))?;
365
366 let column2_index = dataset
367 .column_names
368 .iter()
369 .position(|name| name == column2)
370 .ok_or_else(|| StatisticalError::InvalidColumn(column2.to_string()))?;
371
372 if !matches!(
374 dataset.column_types[column1_index],
375 DataType::Float32 | DataType::Float64
376 ) {
377 return Err(StatisticalError::InvalidOperation(
378 "Correlation can only be computed on numeric columns".to_string(),
379 ));
380 }
381 if !matches!(
382 dataset.column_types[column2_index],
383 DataType::Float32 | DataType::Float64
384 ) {
385 return Err(StatisticalError::InvalidOperation(
386 "Correlation can only be computed on numeric columns".to_string(),
387 ));
388 }
389
390 let mut x_values = Vec::new();
392 let mut y_values = Vec::new();
393
394 for row in &dataset.data {
395 let x_val = match &row[column1_index] {
396 DataValue::Float(value) => *value,
397 DataValue::Integer(value) => *value as f64,
398 DataValue::Null => continue,
399 _ => {
400 return Err(StatisticalError::InvalidOperation(
401 "Non-numeric data in column".to_string(),
402 ))
403 }
404 };
405
406 let y_val = match &row[column2_index] {
407 DataValue::Float(value) => *value,
408 DataValue::Integer(value) => *value as f64,
409 DataValue::Null => continue,
410 _ => {
411 return Err(StatisticalError::InvalidOperation(
412 "Non-numeric data in column".to_string(),
413 ))
414 }
415 };
416
417 x_values.push(x_val);
418 y_values.push(y_val);
419 }
420
421 if x_values.len() < 2 {
422 return Err(StatisticalError::InvalidData(
423 "Insufficient data for correlation".to_string(),
424 ));
425 }
426
427 let correlation = match method {
429 CorrelationMethod::Pearson => self.pearson_correlation(&x_values, &y_values)?,
430 CorrelationMethod::Spearman => self.spearman_correlation(&x_values, &y_values)?,
431 CorrelationMethod::Kendall => self.kendall_correlation(&x_values, &y_values)?,
432 _ => {
433 return Err(StatisticalError::InvalidOperation(
434 "Correlation method not supported".to_string(),
435 ))
436 }
437 };
438
439 let execution_time = start_time.elapsed().as_millis() as u64;
440
441 let (final_correlation, privacy_cost) = if privacy_preserved {
443 let (noisy_correlation, cost) =
444 self.privacy_engine.add_laplace_noise(correlation, 0.1)?;
445 (noisy_correlation.clamp(-1.0, 1.0), cost)
446 } else {
447 (correlation, 0.0)
448 };
449
450 self.performance_monitor
452 .record_operation("correlation", execution_time, 0, privacy_cost);
453
454 Ok(StatisticalAnalysisResult {
455 result: final_correlation,
456 execution_time,
457 memory_usage: 0,
458 sample_size: x_values.len(),
459 confidence_level: 0.95,
460 privacy_preserved,
461 privacy_cost,
462 })
463 }
464
465 pub fn t_test(
467 &mut self,
468 dataset_id: &str,
469 column: &str,
470 hypothesis_type: HypothesisType,
471 privacy_preserved: bool,
472 ) -> Result<StatisticalAnalysisResult<TTestResult>, StatisticalError> {
473 let start_time = std::time::Instant::now();
474
475 let dataset = self.data_storage.get_dataset(dataset_id)?;
477
478 let column_index = dataset
480 .column_names
481 .iter()
482 .position(|name| name == column)
483 .ok_or_else(|| StatisticalError::InvalidColumn(column.to_string()))?;
484
485 if !matches!(
487 dataset.column_types[column_index],
488 DataType::Float32 | DataType::Float64
489 ) {
490 return Err(StatisticalError::InvalidOperation(
491 "T-test can only be computed on numeric columns".to_string(),
492 ));
493 }
494
495 let mut values = Vec::new();
497 for row in &dataset.data {
498 match &row[column_index] {
499 DataValue::Float(value) => values.push(*value),
500 DataValue::Integer(value) => values.push(*value as f64),
501 DataValue::Null => continue,
502 _ => {
503 return Err(StatisticalError::InvalidOperation(
504 "Non-numeric data in column".to_string(),
505 ))
506 }
507 }
508 }
509
510 if values.len() < 2 {
511 return Err(StatisticalError::InvalidData(
512 "Insufficient data for t-test".to_string(),
513 ));
514 }
515
516 let t_test_result = match hypothesis_type {
518 HypothesisType::OneSample => self.one_sample_t_test(&values, 0.0)?,
519 HypothesisType::TwoSample => {
520 return Err(StatisticalError::InvalidOperation(
521 "Two-sample t-test requires two datasets".to_string(),
522 ))
523 }
524 HypothesisType::Paired => {
525 return Err(StatisticalError::InvalidOperation(
526 "Paired t-test requires paired data".to_string(),
527 ))
528 }
529 HypothesisType::Independent => {
530 return Err(StatisticalError::InvalidOperation(
531 "Independent t-test requires two samples".to_string(),
532 ))
533 }
534 };
535
536 let execution_time = start_time.elapsed().as_millis() as u64;
537
538 let (final_result, privacy_cost) = if privacy_preserved {
540 let (noisy_t_statistic, cost) = self
541 .privacy_engine
542 .add_laplace_noise(t_test_result.t_statistic, 1.0)?;
543 let noisy_result = TTestResult {
544 t_statistic: noisy_t_statistic,
545 p_value: t_test_result.p_value,
546 degrees_of_freedom: t_test_result.degrees_of_freedom,
547 confidence_interval: t_test_result.confidence_interval,
548 };
549 (noisy_result, cost)
550 } else {
551 (t_test_result, 0.0)
552 };
553
554 self.performance_monitor
556 .record_operation("t_test", execution_time, 0, privacy_cost);
557
558 Ok(StatisticalAnalysisResult {
559 result: final_result,
560 execution_time,
561 memory_usage: 0,
562 sample_size: values.len(),
563 confidence_level: 0.95,
564 privacy_preserved,
565 privacy_cost,
566 })
567 }
568
569 pub fn histogram(
571 &mut self,
572 dataset_id: &str,
573 column: &str,
574 bins: usize,
575 privacy_preserved: bool,
576 ) -> Result<StatisticalAnalysisResult<HistogramResult>, StatisticalError> {
577 let start_time = std::time::Instant::now();
578
579 let dataset = self.data_storage.get_dataset(dataset_id)?;
581
582 let column_index = dataset
584 .column_names
585 .iter()
586 .position(|name| name == column)
587 .ok_or_else(|| StatisticalError::InvalidColumn(column.to_string()))?;
588
589 if !matches!(
591 dataset.column_types[column_index],
592 DataType::Float32 | DataType::Float64
593 ) {
594 return Err(StatisticalError::InvalidOperation(
595 "Histogram can only be computed on numeric columns".to_string(),
596 ));
597 }
598
599 let mut values = Vec::new();
601 for row in &dataset.data {
602 match &row[column_index] {
603 DataValue::Float(value) => values.push(*value),
604 DataValue::Integer(value) => values.push(*value as f64),
605 DataValue::Null => continue,
606 _ => {
607 return Err(StatisticalError::InvalidOperation(
608 "Non-numeric data in column".to_string(),
609 ))
610 }
611 }
612 }
613
614 if values.is_empty() {
615 return Err(StatisticalError::InvalidData(
616 "No valid data in column".to_string(),
617 ));
618 }
619
620 let histogram_result = self.compute_histogram(&values, bins)?;
622
623 let execution_time = start_time.elapsed().as_millis() as u64;
624
625 let (final_result, privacy_cost) = if privacy_preserved {
627 let (noisy_counts, cost) = self
628 .privacy_engine
629 .add_histogram_noise(&histogram_result.counts)?;
630 let noisy_result = HistogramResult {
631 bins: histogram_result.bins,
632 counts: noisy_counts,
633 min_value: histogram_result.min_value,
634 max_value: histogram_result.max_value,
635 bin_width: histogram_result.bin_width,
636 };
637 (noisy_result, cost)
638 } else {
639 (histogram_result, 0.0)
640 };
641
642 self.performance_monitor
644 .record_operation("histogram", execution_time, 0, privacy_cost);
645
646 Ok(StatisticalAnalysisResult {
647 result: final_result,
648 execution_time,
649 memory_usage: 0,
650 sample_size: values.len(),
651 confidence_level: 0.95,
652 privacy_preserved,
653 privacy_cost,
654 })
655 }
656
657 fn numeric_column(&self, dataset_id: &str, column: &str) -> Result<Vec<f64>, StatisticalError> {
672 let dataset = self.data_storage.get_dataset(dataset_id)?;
673 let idx = dataset
674 .column_names
675 .iter()
676 .position(|n| n == column)
677 .ok_or_else(|| StatisticalError::InvalidColumn(column.to_string()))?;
678 let mut out = Vec::with_capacity(dataset.data.len());
679 for row in &dataset.data {
680 match &row[idx] {
681 DataValue::Float(v) => out.push(*v),
682 DataValue::Integer(v) => out.push(*v as f64),
683 DataValue::Null => continue,
684 _ => {
685 return Err(StatisticalError::InvalidOperation(
686 "Non-numeric data in column".to_string(),
687 ))
688 }
689 }
690 }
691 if out.is_empty() {
692 return Err(StatisticalError::InvalidData(
693 "No valid data in column".to_string(),
694 ));
695 }
696 Ok(out)
697 }
698
699 fn numeric_matrix(
703 &self,
704 dataset_id: &str,
705 columns: &[&str],
706 ) -> Result<(Vec<f64>, usize, usize), StatisticalError> {
707 let dataset = self.data_storage.get_dataset(dataset_id)?;
708 let p = columns.len();
709 if p == 0 {
710 return Err(StatisticalError::InvalidOperation(
711 "no columns given".to_string(),
712 ));
713 }
714 let mut idxs = Vec::with_capacity(p);
715 for c in columns {
716 idxs.push(
717 dataset
718 .column_names
719 .iter()
720 .position(|n| n == c)
721 .ok_or_else(|| StatisticalError::InvalidColumn((*c).to_string()))?,
722 );
723 }
724 let mut data = Vec::with_capacity(dataset.data.len() * p);
725 let mut n = 0;
726 'rows: for row in &dataset.data {
727 let mut tmp = [0.0f64; 0].to_vec();
728 tmp.reserve(p);
729 for &ix in &idxs {
730 match &row[ix] {
731 DataValue::Float(v) => tmp.push(*v),
732 DataValue::Integer(v) => tmp.push(*v as f64),
733 DataValue::Null => continue 'rows,
734 _ => {
735 return Err(StatisticalError::InvalidOperation(
736 "Non-numeric data in column".to_string(),
737 ))
738 }
739 }
740 }
741 data.extend_from_slice(&tmp);
742 n += 1;
743 }
744 if n == 0 {
745 return Err(StatisticalError::InvalidData(
746 "No complete rows across the requested columns".to_string(),
747 ));
748 }
749 Ok((data, n, p))
750 }
751
752 fn features_and_label(
755 &self,
756 dataset_id: &str,
757 feature_columns: &[&str],
758 label_column: &str,
759 ) -> Result<(Vec<f64>, Vec<f64>, usize, usize), StatisticalError> {
760 let mut cols: Vec<&str> = feature_columns.to_vec();
761 cols.push(label_column);
762 let (mat, n, p_all) = self.numeric_matrix(dataset_id, &cols)?;
763 let p = p_all - 1;
764 let mut x = Vec::with_capacity(n * p);
765 let mut y = Vec::with_capacity(n);
766 for r in 0..n {
767 x.extend_from_slice(&mat[r * p_all..r * p_all + p]);
768 y.push(mat[r * p_all + p]);
769 }
770 Ok((x, y, n, p))
771 }
772
773 fn scalar_result(&self, value: f64, n: usize) -> StatisticalAnalysisResult<f64> {
774 StatisticalAnalysisResult {
775 result: value,
776 execution_time: 0,
777 memory_usage: 0,
778 sample_size: n,
779 confidence_level: 0.95,
780 privacy_preserved: false,
781 privacy_cost: 0.0,
782 }
783 }
784
785 pub fn standard_deviation(
787 &self,
788 dataset_id: &str,
789 column: &str,
790 sample: bool,
791 ) -> Result<StatisticalAnalysisResult<f64>, StatisticalError> {
792 let v = self.numeric_column(dataset_id, column)?;
793 let sd = crate::solvers::statistics::std_dev(&v, sample)
794 .ok_or_else(|| StatisticalError::InvalidData("empty column".to_string()))?;
795 Ok(self.scalar_result(sd, v.len()))
796 }
797
798 pub fn skewness(
800 &self,
801 dataset_id: &str,
802 column: &str,
803 ) -> Result<StatisticalAnalysisResult<f64>, StatisticalError> {
804 let v = self.numeric_column(dataset_id, column)?;
805 let s = crate::solvers::statistics::skewness(&v)
806 .ok_or_else(|| StatisticalError::InvalidData("skewness undefined".to_string()))?;
807 Ok(self.scalar_result(s, v.len()))
808 }
809
810 pub fn kurtosis(
812 &self,
813 dataset_id: &str,
814 column: &str,
815 ) -> Result<StatisticalAnalysisResult<f64>, StatisticalError> {
816 let v = self.numeric_column(dataset_id, column)?;
817 let k = crate::solvers::statistics::kurtosis(&v)
818 .ok_or_else(|| StatisticalError::InvalidData("kurtosis undefined".to_string()))?;
819 Ok(self.scalar_result(k, v.len()))
820 }
821
822 pub fn mode(&self, dataset_id: &str, column: &str) -> Result<ModeResult, StatisticalError> {
824 let mut v = self.numeric_column(dataset_id, column)?;
825 let n = v.len();
826 let (value, count) = crate::solvers::statistics::mode_in_place(&mut v)
827 .ok_or_else(|| StatisticalError::InvalidData("empty column".to_string()))?;
828 Ok(ModeResult {
829 value,
830 count,
831 sample_size: n,
832 })
833 }
834
835 pub fn quantile(
838 &self,
839 dataset_id: &str,
840 column: &str,
841 q: f64,
842 ) -> Result<StatisticalAnalysisResult<f64>, StatisticalError> {
843 let mut v = self.numeric_column(dataset_id, column)?;
844 let n = v.len();
845 let val = crate::solvers::statistics::quantile_in_place(&mut v, q).ok_or_else(|| {
846 StatisticalError::InvalidOperation("quantile requires q in [0,1]".to_string())
847 })?;
848 Ok(self.scalar_result(val, n))
849 }
850
851 pub fn covariance(
853 &self,
854 dataset_id: &str,
855 column_x: &str,
856 column_y: &str,
857 sample: bool,
858 ) -> Result<StatisticalAnalysisResult<f64>, StatisticalError> {
859 let (mat, n, _p) = self.numeric_matrix(dataset_id, &[column_x, column_y])?;
860 let x: Vec<f64> = (0..n).map(|r| mat[r * 2]).collect();
861 let y: Vec<f64> = (0..n).map(|r| mat[r * 2 + 1]).collect();
862 let cov = crate::solvers::statistics::covariance(&x, &y, sample)
863 .ok_or_else(|| StatisticalError::InvalidData("covariance undefined".to_string()))?;
864 Ok(self.scalar_result(cov, n))
865 }
866
867 pub fn linear_regression(
870 &self,
871 dataset_id: &str,
872 column_x: &str,
873 column_y: &str,
874 ) -> Result<crate::solvers::statistics::LinearRegression, StatisticalError> {
875 let (mat, n, _p) = self.numeric_matrix(dataset_id, &[column_x, column_y])?;
876 let x: Vec<f64> = (0..n).map(|r| mat[r * 2]).collect();
877 let y: Vec<f64> = (0..n).map(|r| mat[r * 2 + 1]).collect();
878 crate::solvers::statistics::simple_linear_regression(&x, &y).ok_or_else(|| {
879 StatisticalError::InvalidData(
880 "regression undefined (n<3 or zero variance in x)".to_string(),
881 )
882 })
883 }
884
885 pub fn polynomial_regression(
888 &self,
889 dataset_id: &str,
890 column_x: &str,
891 column_y: &str,
892 degree: usize,
893 ) -> Result<PolynomialFit, StatisticalError> {
894 let (mat, n, _p) = self.numeric_matrix(dataset_id, &[column_x, column_y])?;
895 let x: Vec<f64> = (0..n).map(|r| mat[r * 2]).collect();
896 let y: Vec<f64> = (0..n).map(|r| mat[r * 2 + 1]).collect();
897 let coeffs = crate::solvers::interpolation::least_squares::poly_fit(&x, &y, degree)
898 .map_err(|e| StatisticalError::InvalidOperation(format!("poly_fit: {:?}", e)))?;
899 let y_mean = crate::solvers::statistics::mean(&y).unwrap_or(0.0);
901 let mut ss_res = 0.0;
902 let mut ss_tot = 0.0;
903 for i in 0..n {
904 let yhat = crate::solvers::interpolation::least_squares::poly_eval(&coeffs, x[i]);
905 ss_res += (y[i] - yhat) * (y[i] - yhat);
906 ss_tot += (y[i] - y_mean) * (y[i] - y_mean);
907 }
908 let r_squared = if ss_tot > 0.0 {
909 1.0 - ss_res / ss_tot
910 } else {
911 0.0
912 };
913 Ok(PolynomialFit {
914 degree,
915 coefficients: coeffs,
916 r_squared,
917 n,
918 })
919 }
920
921 pub fn logistic_regression(
924 &self,
925 dataset_id: &str,
926 feature_columns: &[&str],
927 label_column: &str,
928 fit_intercept: bool,
929 ) -> Result<crate::solvers::learning::glm::GlmModel, StatisticalError> {
930 let (x, y, n, p) = self.features_and_label(dataset_id, feature_columns, label_column)?;
931 crate::solvers::learning::glm::fit_logistic(&x, &y, n, p, fit_intercept)
932 .map_err(|e| StatisticalError::InvalidOperation(format!("logistic: {:?}", e)))
933 }
934
935 pub fn anova(
938 &self,
939 dataset_id: &str,
940 group_columns: &[&str],
941 ) -> Result<crate::solvers::statistics::AnovaResult, StatisticalError> {
942 if group_columns.len() < 2 {
943 return Err(StatisticalError::InvalidOperation(
944 "ANOVA needs at least two groups".to_string(),
945 ));
946 }
947 let groups: Vec<Vec<f64>> = group_columns
948 .iter()
949 .map(|c| self.numeric_column(dataset_id, c))
950 .collect::<Result<_, _>>()?;
951 let refs: Vec<&[f64]> = groups.iter().map(|g| g.as_slice()).collect();
952 crate::solvers::statistics::one_way_anova(&refs).ok_or_else(|| {
953 StatisticalError::InvalidData("ANOVA undefined for these groups".to_string())
954 })
955 }
956
957 pub fn chi_square_gof(
960 &self,
961 dataset_id: &str,
962 observed_column: &str,
963 expected_column: Option<&str>,
964 ) -> Result<crate::solvers::statistics::ChiSquareResult, StatisticalError> {
965 let observed = self.numeric_column(dataset_id, observed_column)?;
966 let expected = match expected_column {
967 Some(c) => self.numeric_column(dataset_id, c)?,
968 None => {
969 let total: f64 = observed.iter().sum();
970 let u = total / observed.len() as f64;
971 vec![u; observed.len()]
972 }
973 };
974 crate::solvers::statistics::chi_square_gof(&observed, &expected).ok_or_else(|| {
975 StatisticalError::InvalidOperation("chi-square GoF undefined".to_string())
976 })
977 }
978
979 pub fn chi_square_independence(
982 &self,
983 dataset_id: &str,
984 columns: &[&str],
985 ) -> Result<crate::solvers::statistics::ChiSquareResult, StatisticalError> {
986 let (mat, n, p) = self.numeric_matrix(dataset_id, columns)?;
987 let rows: Vec<&[f64]> = (0..n).map(|r| &mat[r * p..(r + 1) * p]).collect();
988 crate::solvers::statistics::chi_square_independence(&rows).ok_or_else(|| {
989 StatisticalError::InvalidOperation("chi-square independence undefined".to_string())
990 })
991 }
992
993 pub fn autocorrelation(
995 &self,
996 dataset_id: &str,
997 column: &str,
998 lag: usize,
999 ) -> Result<StatisticalAnalysisResult<f64>, StatisticalError> {
1000 let v = self.numeric_column(dataset_id, column)?;
1001 let r = crate::solvers::statistics::autocorrelation(&v, lag).ok_or_else(|| {
1002 StatisticalError::InvalidOperation(
1003 "autocorrelation undefined (lag>=n or constant series)".to_string(),
1004 )
1005 })?;
1006 Ok(self.scalar_result(r, v.len()))
1007 }
1008
1009 pub fn moving_average(
1011 &self,
1012 dataset_id: &str,
1013 column: &str,
1014 window: usize,
1015 ) -> Result<Vec<f64>, StatisticalError> {
1016 let v = self.numeric_column(dataset_id, column)?;
1017 if window == 0 || window > v.len() {
1018 return Err(StatisticalError::InvalidOperation(
1019 "window must be in 1..=n".to_string(),
1020 ));
1021 }
1022 let mut out = vec![0.0; v.len() - window + 1];
1023 crate::solvers::statistics::moving_average_into(&v, window, &mut out).ok_or_else(|| {
1024 StatisticalError::InvalidOperation("moving average failed".to_string())
1025 })?;
1026 Ok(out)
1027 }
1028
1029 pub fn exponential_smoothing(
1031 &self,
1032 dataset_id: &str,
1033 column: &str,
1034 alpha: f64,
1035 ) -> Result<Vec<f64>, StatisticalError> {
1036 let v = self.numeric_column(dataset_id, column)?;
1037 let mut out = vec![0.0; v.len()];
1038 crate::solvers::statistics::exponential_smoothing_into(&v, alpha, &mut out).ok_or_else(
1039 || StatisticalError::InvalidOperation("alpha must be in (0,1]".to_string()),
1040 )?;
1041 Ok(out)
1042 }
1043
1044 pub fn kmeans(
1047 &self,
1048 dataset_id: &str,
1049 feature_columns: &[&str],
1050 k: usize,
1051 max_iter: usize,
1052 seed: u64,
1053 ) -> Result<crate::solvers::learning::clustering::kmeans::KMeansModel, StatisticalError> {
1054 let (x, n, p) = self.numeric_matrix(dataset_id, feature_columns)?;
1055 crate::solvers::learning::clustering::kmeans::fit(&x, n, p, k, max_iter, seed)
1056 .map_err(|e| StatisticalError::InvalidOperation(format!("kmeans: {:?}", e)))
1057 }
1058
1059 pub fn linear_svm(
1063 &self,
1064 dataset_id: &str,
1065 feature_columns: &[&str],
1066 label_column: &str,
1067 c: f64,
1068 ) -> Result<SvmFitResult, StatisticalError> {
1069 let (x, y, n, p) = self.features_and_label(dataset_id, feature_columns, label_column)?;
1070 let labels: Vec<bool> = y.iter().map(|&v| v != 0.0).collect();
1071 let model = crate::solvers::learning::classification::svm::fit(
1072 &x,
1073 &labels,
1074 n,
1075 p,
1076 c,
1077 crate::solvers::learning::classification::svm::Kernel::Linear,
1078 100,
1079 1e-3,
1080 )
1081 .map_err(|e| StatisticalError::InvalidOperation(format!("svm: {:?}", e)))?;
1082 let mut correct = 0usize;
1083 for i in 0..n {
1084 if model.predict_row(&x[i * p..(i + 1) * p]) == labels[i] {
1085 correct += 1;
1086 }
1087 }
1088 Ok(SvmFitResult {
1089 n_support_vectors: model.n_support_vectors(),
1090 train_accuracy: correct as f64 / n as f64,
1091 n,
1092 n_features: p,
1093 })
1094 }
1095
1096 pub fn random_forest(
1100 &self,
1101 dataset_id: &str,
1102 feature_columns: &[&str],
1103 target_column: &str,
1104 n_trees: usize,
1105 classifier: bool,
1106 seed: u64,
1107 ) -> Result<RandomForestFitResult, StatisticalError> {
1108 use crate::solvers::learning::trees::decision_tree::TreeParams;
1109 use crate::solvers::learning::trees::random_forest::RandomForest;
1110 let (x, y, n, p) = self.features_and_label(dataset_id, feature_columns, target_column)?;
1111 let params = TreeParams::default();
1112 let (metric, model_predict): (f64, Vec<f64>) = if classifier {
1113 let labels: Vec<usize> = y.iter().map(|&v| v.round().max(0.0) as usize).collect();
1114 let rf = RandomForest::fit_classifier(&x, &labels, n, p, n_trees, params, seed)
1115 .map_err(|e| StatisticalError::InvalidOperation(format!("forest: {:?}", e)))?;
1116 let mut correct = 0usize;
1117 for i in 0..n {
1118 if rf.predict_class(&x[i * p..(i + 1) * p]) == labels[i] {
1119 correct += 1;
1120 }
1121 }
1122 (correct as f64 / n as f64, vec![])
1123 } else {
1124 let rf = RandomForest::fit_regressor(&x, &y, n, p, n_trees, params, seed)
1125 .map_err(|e| StatisticalError::InvalidOperation(format!("forest: {:?}", e)))?;
1126 let preds: Vec<f64> = (0..n)
1127 .map(|i| rf.predict_row(&x[i * p..(i + 1) * p]))
1128 .collect();
1129 let y_mean = crate::solvers::statistics::mean(&y).unwrap_or(0.0);
1130 let mut ss_res = 0.0;
1131 let mut ss_tot = 0.0;
1132 for i in 0..n {
1133 ss_res += (y[i] - preds[i]) * (y[i] - preds[i]);
1134 ss_tot += (y[i] - y_mean) * (y[i] - y_mean);
1135 }
1136 let r2 = if ss_tot > 0.0 {
1137 1.0 - ss_res / ss_tot
1138 } else {
1139 0.0
1140 };
1141 (r2, preds)
1142 };
1143 let _ = model_predict;
1144 Ok(RandomForestFitResult {
1145 n_trees,
1146 classifier,
1147 train_metric: metric,
1148 n,
1149 n_features: p,
1150 })
1151 }
1152
1153 pub fn get_performance_stats(&self) -> SystemMetrics {
1155 self.performance_monitor.get_system_metrics()
1156 }
1157
1158 pub fn list_datasets(&self) -> Vec<String> {
1160 self.data_storage.list_datasets()
1161 }
1162
1163 pub fn get_dataset_info(&self, dataset_id: &str) -> Option<DatasetMetadata> {
1165 self.data_storage.get_dataset_metadata(dataset_id)
1166 }
1167
1168 fn pearson_correlation(&self, x: &[f64], y: &[f64]) -> Result<f64, StatisticalError> {
1172 crate::solvers::statistics::pearson(x, y).ok_or_else(|| {
1174 StatisticalError::InvalidData("Invalid data for correlation".to_string())
1175 })
1176 }
1177
1178 fn spearman_correlation(&self, x: &[f64], y: &[f64]) -> Result<f64, StatisticalError> {
1180 let x_ranked = self.rank_values(x);
1182 let y_ranked = self.rank_values(y);
1183
1184 self.pearson_correlation(&x_ranked, &y_ranked)
1186 }
1187
1188 fn kendall_correlation(&self, x: &[f64], y: &[f64]) -> Result<f64, StatisticalError> {
1190 crate::solvers::statistics::kendall(x, y).ok_or_else(|| {
1192 StatisticalError::InvalidData("Invalid data for correlation".to_string())
1193 })
1194 }
1195
1196 fn rank_values(&self, values: &[f64]) -> Vec<f64> {
1198 let n = values.len();
1201 let mut idx = vec![0usize; n];
1202 let mut ranks = vec![0.0; n];
1203 let _ = crate::solvers::statistics::rank_into(values, &mut idx, &mut ranks);
1204 ranks
1205 }
1206
1207 fn one_sample_t_test(&self, values: &[f64], mu: f64) -> Result<TTestResult, StatisticalError> {
1209 let n = values.len();
1210 if n < 2 {
1211 return Err(StatisticalError::InvalidData(
1212 "Insufficient data for t-test".to_string(),
1213 ));
1214 }
1215
1216 let t = crate::solvers::statistics::one_sample_t(values, mu).ok_or_else(|| {
1217 StatisticalError::InvalidData("Insufficient data for t-test".to_string())
1218 })?;
1219 Ok(TTestResult {
1220 t_statistic: t.t_statistic,
1221 p_value: t.p_value,
1222 degrees_of_freedom: t.degrees_of_freedom,
1223 confidence_interval: t.confidence_interval,
1224 })
1225 }
1226
1227 fn compute_histogram(
1229 &self,
1230 values: &[f64],
1231 bins: usize,
1232 ) -> Result<HistogramResult, StatisticalError> {
1233 if values.is_empty() {
1234 return Err(StatisticalError::InvalidData(
1235 "No data for histogram".to_string(),
1236 ));
1237 }
1238
1239 let mut counts = vec![0u32; bins];
1242 let range = crate::solvers::statistics::histogram_into(values, &mut counts)
1243 .ok_or_else(|| StatisticalError::InvalidData("No data for histogram".to_string()))?;
1244 Ok(HistogramResult {
1245 bins,
1246 counts,
1247 min_value: range.min,
1248 max_value: range.max,
1249 bin_width: range.bin_width,
1250 })
1251 }
1252}
1253
1254