Skip to main content

qualia_core_db/specialized_libs/statistical_computing/
library.rs

1use super::*;
2
3impl StatisticalComputingLibrary {
4    /// Create new statistical computing library
5    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    /// Initialize the library
16    pub fn initialize(&mut self) -> Result<(), StatisticalError> {
17        // Initialize storage
18        self.data_storage.initialize()?;
19
20        // Initialize computation engine
21        self.computation_engine.initialize()?;
22
23        // Initialize privacy engine
24        self.privacy_engine.initialize()?;
25
26        // Initialize analysis engine
27        self.analysis_engine.initialize()?;
28
29        Ok(())
30    }
31
32    /// Create a new dataset
33    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        // Validate input
42        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        // Create metadata
59        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        // Create dataset
80        let dataset = Dataset {
81            dataset_id: dataset_id.clone(),
82            metadata,
83            data,
84            column_names,
85            column_types,
86        };
87
88        // Store dataset
89        self.data_storage.store_dataset(dataset.clone())?;
90
91        Ok(dataset)
92    }
93
94    /// Compute mean of a column
95    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        // Get dataset
104        let dataset = self.data_storage.get_dataset(dataset_id)?;
105
106        // Find column index
107        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        // Validate column type
114        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        // Extract column data
124        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        // Compute mean via the engine's canonical statistics solver (Modality-First:
145        // no inline re-implementation). `values` is the caller-owned slice.
146        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        // Apply privacy if requested. Sensitivity is calibrated via the
152        // differential-privacy sensitivity analyzer (mean sensitivity = 1/n)
153        // instead of the previous hardcoded 1.0, so noise scales with the
154        // actual query sensitivity.
155        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        // Update performance metrics
170        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    /// Compute median of a column
185    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        // Get dataset
194        let dataset = self.data_storage.get_dataset(dataset_id)?;
195
196        // Find column index
197        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        // Validate column type
204        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        // Extract column data
214        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        // Compute median via the engine's canonical statistics solver (sorts the
235        // caller-owned buffer in place; no inline re-implementation).
236        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        // Apply privacy if requested
242        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        // Update performance metrics
250        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    /// Compute variance of a column
265    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        // Get dataset
275        let dataset = self.data_storage.get_dataset(dataset_id)?;
276
277        // Find column index
278        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        // Validate column type
285        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        // Extract column data
295        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        // Compute variance via the engine's canonical statistics solver
316        // (Modality-First: no inline re-implementation).
317        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        // Apply privacy if requested
323        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        // Update performance metrics
331        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    /// Compute correlation between two columns
346    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        // Get dataset
357        let dataset = self.data_storage.get_dataset(dataset_id)?;
358
359        // Find column indices
360        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        // Validate column types
373        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        // Extract column data
391        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        // Compute correlation based on method
428        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        // Apply privacy if requested
442        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        // Update performance metrics
451        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    /// Perform t-test
466    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        // Get dataset
476        let dataset = self.data_storage.get_dataset(dataset_id)?;
477
478        // Find column index
479        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        // Validate column type
486        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        // Extract column data
496        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        // Compute t-test based on hypothesis type
517        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        // Apply privacy if requested
539        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        // Update performance metrics
555        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    /// Generate histogram
570    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        // Get dataset
580        let dataset = self.data_storage.get_dataset(dataset_id)?;
581
582        // Find column index
583        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        // Validate column type
590        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        // Extract column data
600        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        // Compute histogram
621        let histogram_result = self.compute_histogram(&values, bins)?;
622
623        let execution_time = start_time.elapsed().as_millis() as u64;
624
625        // Apply privacy if requested
626        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        // Update performance metrics
643        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    // ========================================================================
658    // Wired capability methods.
659    //
660    // Each declared `StatisticalOperation` is a genuine, MCP-reachable
661    // computation. Every method below marshals the caller's dataset into a
662    // slice and delegates to the canonical numeric kernels in
663    // `crate::solvers` (Modality-First Composition — no inline re-derivation);
664    // the descriptive/correlation/regression/hypothesis kernels live in
665    // `solvers::statistics`, the learning models in `solvers::learning`, and
666    // polynomial least-squares in `solvers::interpolation`.
667    // ========================================================================
668
669    /// Extract one numeric column as an owned `Vec<f64>` (Integer widened,
670    /// Null rows skipped). Errors on unknown column, non-numeric cell, or empty.
671    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    /// Extract several numeric columns row-aligned into a row-major `n × p`
700    /// matrix, skipping any row where one of the requested cells is Null (so
701    /// every returned row is complete). Returns `(data, n, p)`.
702    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    /// Split feature columns + a trailing label column into `(x, y, n, p)`,
753    /// row-aligned (rows with a Null in any of them are dropped together).
754    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    /// Standard deviation (`sample = true` → Bessel-corrected).
786    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    /// Sample skewness (Fisher-Pearson).
799    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    /// Excess kurtosis.
811    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    /// Modal value + its frequency.
823    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    /// The `q`-quantile (`q ∈ [0,1]`) via linear interpolation between order
836    /// statistics. `percentile` is the same with `p ∈ [0,100]`.
837    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    /// Covariance between two columns (`sample = true` → divide by n-1).
852    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    /// Ordinary-least-squares simple linear regression `y ~ x` with full
868    /// inferential statistics (slope/intercept, R², standard errors, t, p).
869    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    /// Polynomial regression `y ~ poly(x, degree)` by least squares (normal
886    /// equations). Returns ascending-power coefficients and in-sample R².
887    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        // In-sample R² from the fitted coefficients.
900        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    /// Binary logistic regression by IRLS (`label` in {0,1}), with Wald
922    /// standard errors, z-statistics and p-values on the coefficients.
923    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    /// One-way ANOVA across the named columns (each column is a group). Groups
936    /// may have unequal lengths; Null cells are dropped per column.
937    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    /// Chi-square goodness-of-fit test: observed counts vs. expected counts.
958    /// If `expected_column` is `None`, a uniform expectation is used.
959    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    /// Chi-square test of independence over a contingency table whose columns
980    /// are the named columns (each row of the table is one dataset row).
981    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    /// Autocorrelation of a column at the given lag (biased estimator).
994    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    /// Simple moving average of a column with the given window.
1010    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    /// Single exponential smoothing of a column with factor `alpha ∈ (0,1]`.
1030    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    /// K-means clustering over the named feature columns. Returns the fitted
1045    /// model (centroids, per-point labels, inertia, convergence).
1046    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    /// Soft-margin linear SVM over the named feature columns with a boolean
1060    /// `label` column (non-zero = positive class). Returns a fit summary with
1061    /// support-vector count and in-sample accuracy.
1062    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    /// Random-forest fit over the named feature columns and a target column.
1097    /// `classifier = true` fits a classification forest (integer labels) and
1098    /// reports in-sample accuracy; otherwise a regression forest reporting R².
1099    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    /// Get performance statistics
1154    pub fn get_performance_stats(&self) -> SystemMetrics {
1155        self.performance_monitor.get_system_metrics()
1156    }
1157
1158    /// List all datasets
1159    pub fn list_datasets(&self) -> Vec<String> {
1160        self.data_storage.list_datasets()
1161    }
1162
1163    /// Get dataset information
1164    pub fn get_dataset_info(&self, dataset_id: &str) -> Option<DatasetMetadata> {
1165        self.data_storage.get_dataset_metadata(dataset_id)
1166    }
1167
1168    // Internal methods
1169
1170    /// Compute Pearson correlation
1171    fn pearson_correlation(&self, x: &[f64], y: &[f64]) -> Result<f64, StatisticalError> {
1172        // Modality-First: the math lives in the engine's statistics solver.
1173        crate::solvers::statistics::pearson(x, y).ok_or_else(|| {
1174            StatisticalError::InvalidData("Invalid data for correlation".to_string())
1175        })
1176    }
1177
1178    /// Compute Spearman correlation
1179    fn spearman_correlation(&self, x: &[f64], y: &[f64]) -> Result<f64, StatisticalError> {
1180        // Convert to ranks
1181        let x_ranked = self.rank_values(x);
1182        let y_ranked = self.rank_values(y);
1183
1184        // Compute Pearson correlation on ranks
1185        self.pearson_correlation(&x_ranked, &y_ranked)
1186    }
1187
1188    /// Compute Kendall correlation
1189    fn kendall_correlation(&self, x: &[f64], y: &[f64]) -> Result<f64, StatisticalError> {
1190        // Modality-First: the math lives in the engine's statistics solver.
1191        crate::solvers::statistics::kendall(x, y).ok_or_else(|| {
1192            StatisticalError::InvalidData("Invalid data for correlation".to_string())
1193        })
1194    }
1195
1196    /// Rank values
1197    fn rank_values(&self, values: &[f64]) -> Vec<f64> {
1198        // Modality-First: ranking lives in the engine. The wrapper owns the
1199        // scratch/output buffers (heap is fine at this composition boundary).
1200        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    /// One sample t-test
1208    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    /// Compute histogram
1228    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        // Modality-First: binning lives in the engine; the wrapper owns the
1240        // counts buffer and builds the domain result.
1241        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// Supporting implementations