1use scirs2_core::ndarray::{Array1, Array2, ArrayView2, Axis};
7use sklears_core::{
8 error::Result as SklResult,
9 prelude::{Fit, SklearsError, Transform},
10 types::Float,
11};
12use std::collections::HashMap;
13
14pub mod advanced_sentiment;
16pub mod embeddings;
17pub mod preprocessing;
18pub mod trait_impls;
19
20#[derive(Debug, Clone)]
25pub struct CountVectorizer {
26 max_features: Option<usize>,
27 min_df: Option<usize>,
28 max_df: Option<f64>,
29 ngram_range: (usize, usize),
30 binary: bool,
31 lowercase: bool,
32 stop_words: Option<Vec<String>>,
33 vocabulary: Option<HashMap<String, usize>>,
34 feature_names: Vec<String>,
35 document_frequencies: HashMap<String, usize>,
36}
37
38impl CountVectorizer {
39 pub fn new() -> Self {
41 Self {
42 max_features: None,
43 min_df: Some(1),
44 max_df: Some(1.0),
45 ngram_range: (1, 1),
46 binary: false,
47 lowercase: true,
48 stop_words: None,
49 vocabulary: None,
50 feature_names: Vec::new(),
51 document_frequencies: HashMap::new(),
52 }
53 }
54
55 pub fn max_features(mut self, max_features: Option<usize>) -> Self {
57 self.max_features = max_features;
58 self
59 }
60
61 pub fn min_df(mut self, min_df: usize) -> Self {
63 self.min_df = Some(min_df);
64 self
65 }
66
67 pub fn max_df(mut self, max_df: f64) -> Self {
69 self.max_df = Some(max_df);
70 self
71 }
72
73 pub fn ngram_range(mut self, ngram_range: (usize, usize)) -> Self {
75 self.ngram_range = ngram_range;
76 self
77 }
78
79 pub fn binary(mut self, binary: bool) -> Self {
81 self.binary = binary;
82 self
83 }
84
85 pub fn lowercase(mut self, lowercase: bool) -> Self {
87 self.lowercase = lowercase;
88 self
89 }
90
91 pub fn stop_words(mut self, stop_words: Option<Vec<String>>) -> Self {
93 self.stop_words = stop_words;
94 self
95 }
96
97 pub fn fit_instance_method(&mut self, documents: &[String]) -> SklResult<()> {
99 if documents.is_empty() {
100 return Err(SklearsError::InvalidInput(
101 "Empty document collection".to_string(),
102 ));
103 }
104
105 let mut term_doc_freq = HashMap::new();
107 let total_docs = documents.len();
108
109 for document in documents {
110 let tokens = self.tokenize(document);
111 let ngrams = self.extract_ngrams(&tokens);
112 let unique_ngrams: std::collections::HashSet<_> = ngrams.into_iter().collect();
113
114 for ngram in unique_ngrams {
115 *term_doc_freq.entry(ngram).or_insert(0) += 1;
116 }
117 }
118
119 let mut filtered_terms = Vec::new();
121 let min_df = self.min_df.unwrap_or(1);
122 let max_df_count = if let Some(max_df) = self.max_df {
123 if max_df <= 1.0 {
124 (total_docs as f64 * max_df).ceil() as usize
125 } else {
126 max_df as usize
127 }
128 } else {
129 total_docs
130 };
131
132 for (term, doc_freq) in &term_doc_freq {
133 if *doc_freq >= min_df && *doc_freq <= max_df_count {
134 if let Some(stop_words) = &self.stop_words {
135 if !stop_words.contains(term) {
136 filtered_terms.push(term.clone());
137 }
138 } else {
139 filtered_terms.push(term.clone());
140 }
141 }
142 }
143
144 filtered_terms.sort();
146
147 if let Some(max_features) = self.max_features {
149 if filtered_terms.len() > max_features {
150 filtered_terms.sort_by_key(|term| std::cmp::Reverse(term_doc_freq[term]));
152 filtered_terms.truncate(max_features);
153 filtered_terms.sort(); }
155 }
156
157 let mut vocabulary = HashMap::new();
159 for (idx, term) in filtered_terms.iter().enumerate() {
160 vocabulary.insert(term.clone(), idx);
161 }
162
163 self.vocabulary = Some(vocabulary);
164 self.feature_names = filtered_terms;
165 self.document_frequencies = term_doc_freq;
166
167 Ok(())
168 }
169
170 pub fn transform(&self, documents: &[String]) -> SklResult<Array2<Float>> {
172 let vocabulary = self
173 .vocabulary
174 .as_ref()
175 .ok_or_else(|| SklearsError::InvalidInput("Vectorizer not fitted".to_string()))?;
176
177 let n_docs = documents.len();
178 let n_features = vocabulary.len();
179 let mut feature_matrix = Array2::zeros((n_docs, n_features));
180
181 for (doc_idx, document) in documents.iter().enumerate() {
182 let tokens = self.tokenize(document);
183 let ngrams = self.extract_ngrams(&tokens);
184
185 let mut term_counts = HashMap::new();
187 for ngram in ngrams {
188 if vocabulary.contains_key(&ngram) {
189 *term_counts.entry(ngram).or_insert(0) += 1;
190 }
191 }
192
193 for (term, count) in term_counts {
195 if let Some(&feature_idx) = vocabulary.get(&term) {
196 let value = if self.binary { 1.0 } else { count as Float };
197 feature_matrix[(doc_idx, feature_idx)] = value;
198 }
199 }
200 }
201
202 Ok(feature_matrix)
203 }
204
205 pub fn fit_transform(&mut self, documents: &[String]) -> SklResult<Array2<Float>> {
207 self.fit_instance_method(documents)?;
208 self.transform(documents)
209 }
210
211 pub fn get_feature_names(&self) -> &[String] {
213 &self.feature_names
214 }
215
216 pub fn get_vocabulary(&self) -> Option<&HashMap<String, usize>> {
218 self.vocabulary.as_ref()
219 }
220
221 pub(crate) fn tokenize(&self, text: &str) -> Vec<String> {
222 let text = if self.lowercase {
223 text.to_lowercase()
224 } else {
225 text.to_string()
226 };
227
228 text.split_whitespace()
230 .map(|token| {
231 token
233 .trim_matches(|c: char| c.is_ascii_punctuation())
234 .to_string()
235 })
236 .filter(|token| !token.is_empty())
237 .collect()
238 }
239
240 pub(crate) fn extract_ngrams(&self, tokens: &[String]) -> Vec<String> {
241 let mut ngrams = Vec::new();
242 let (min_n, max_n) = self.ngram_range;
243
244 for n in min_n..=max_n {
245 if n <= tokens.len() {
246 for i in 0..=tokens.len() - n {
247 let ngram = tokens[i..i + n].join(" ");
248 ngrams.push(ngram);
249 }
250 }
251 }
252
253 ngrams
254 }
255}
256
257impl Default for CountVectorizer {
258 fn default() -> Self {
259 Self::new()
260 }
261}
262
263#[derive(Debug, Clone)]
265pub struct FittedCountVectorizer {
266 inner: CountVectorizer,
267}
268
269impl FittedCountVectorizer {
270 pub fn transform(&self, documents: &[String]) -> SklResult<Array2<Float>> {
272 self.inner.transform(documents)
273 }
274
275 pub fn get_feature_names(&self) -> &[String] {
277 self.inner.get_feature_names()
278 }
279
280 pub fn get_vocabulary(&self) -> Option<&HashMap<String, usize>> {
282 self.inner.get_vocabulary()
283 }
284}
285
286impl Fit<Vec<String>, ()> for CountVectorizer {
287 type Fitted = FittedCountVectorizer;
288
289 fn fit(mut self, x: &Vec<String>, _y: &()) -> SklResult<Self::Fitted> {
290 self.fit_instance_method(x)?;
291 Ok(FittedCountVectorizer { inner: self })
292 }
293}
294
295impl Transform<Vec<String>, Array2<Float>> for FittedCountVectorizer {
296 fn transform(&self, x: &Vec<String>) -> SklResult<Array2<Float>> {
297 self.transform(x)
298 }
299}
300
301#[derive(Debug, Clone)]
306pub struct TfidfVectorizer {
307 max_features: Option<usize>,
308 min_df: Option<usize>,
309 max_df: Option<f64>,
310 ngram_range: (usize, usize),
311 use_idf: bool,
312 sublinear_tf: bool,
313 smooth_idf: bool,
314 norm: Option<String>,
315 lowercase: bool,
316 stop_words: Option<Vec<String>>,
317 count_vectorizer: CountVectorizer,
318 idf_weights: Vec<Float>,
319}
320
321impl TfidfVectorizer {
322 pub fn new() -> Self {
324 Self {
325 max_features: None,
326 min_df: Some(1),
327 max_df: Some(1.0),
328 ngram_range: (1, 1),
329 use_idf: true,
330 sublinear_tf: false,
331 smooth_idf: true,
332 norm: Some("l2".to_string()),
333 lowercase: true,
334 stop_words: None,
335 count_vectorizer: CountVectorizer::new(),
336 idf_weights: Vec::new(),
337 }
338 }
339
340 pub fn max_features(mut self, max_features: Option<usize>) -> Self {
342 self.max_features = max_features;
343 self.count_vectorizer = self.count_vectorizer.max_features(max_features);
344 self
345 }
346
347 pub fn min_df(mut self, min_df: usize) -> Self {
349 self.min_df = Some(min_df);
350 self.count_vectorizer = self.count_vectorizer.min_df(min_df);
351 self
352 }
353
354 pub fn max_df(mut self, max_df: f64) -> Self {
356 self.max_df = Some(max_df);
357 self.count_vectorizer = self.count_vectorizer.max_df(max_df);
358 self
359 }
360
361 pub fn ngram_range(mut self, ngram_range: (usize, usize)) -> Self {
363 self.ngram_range = ngram_range;
364 self.count_vectorizer = self.count_vectorizer.ngram_range(ngram_range);
365 self
366 }
367
368 pub fn use_idf(mut self, use_idf: bool) -> Self {
370 self.use_idf = use_idf;
371 self
372 }
373
374 pub fn sublinear_tf(mut self, sublinear_tf: bool) -> Self {
376 self.sublinear_tf = sublinear_tf;
377 self
378 }
379
380 pub fn smooth_idf(mut self, smooth_idf: bool) -> Self {
382 self.smooth_idf = smooth_idf;
383 self
384 }
385
386 pub fn norm(mut self, norm: Option<String>) -> Self {
388 self.norm = norm;
389 self
390 }
391
392 pub fn lowercase(mut self, lowercase: bool) -> Self {
394 self.lowercase = lowercase;
395 self.count_vectorizer = self.count_vectorizer.lowercase(lowercase);
396 self
397 }
398
399 pub fn stop_words(mut self, stop_words: Option<Vec<String>>) -> Self {
401 self.stop_words = stop_words.clone();
402 self.count_vectorizer = self.count_vectorizer.stop_words(stop_words);
403 self
404 }
405
406 pub fn fit_instance_method(&mut self, documents: &[String]) -> SklResult<()> {
408 if documents.is_empty() {
409 return Err(SklearsError::InvalidInput(
410 "Empty document collection".to_string(),
411 ));
412 }
413
414 self.count_vectorizer.fit_instance_method(documents)?;
416
417 if self.use_idf {
418 self.calculate_idf_weights(documents)?;
420 }
421
422 Ok(())
423 }
424
425 pub fn transform(&self, documents: &[String]) -> SklResult<Array2<Float>> {
427 let mut tf_matrix = self.count_vectorizer.transform(documents)?;
429
430 if self.sublinear_tf {
432 tf_matrix.mapv_inplace(|x| if x > 0.0 { 1.0 + x.ln() } else { 0.0 });
433 }
434
435 if self.use_idf && !self.idf_weights.is_empty() {
437 for mut row in tf_matrix.axis_iter_mut(Axis(0)) {
438 for (i, &idf_weight) in self.idf_weights.iter().enumerate() {
439 row[i] *= idf_weight;
440 }
441 }
442 }
443
444 if let Some(norm_type) = &self.norm {
446 self.normalize_rows(&mut tf_matrix, norm_type)?;
447 }
448
449 Ok(tf_matrix)
450 }
451
452 pub fn fit_transform(&mut self, documents: &[String]) -> SklResult<Array2<Float>> {
454 self.fit_instance_method(documents)?;
455 self.transform(documents)
456 }
457
458 pub fn get_feature_names(&self) -> &[String] {
460 self.count_vectorizer.get_feature_names()
461 }
462
463 pub fn get_vocabulary(&self) -> Option<&HashMap<String, usize>> {
465 self.count_vectorizer.get_vocabulary()
466 }
467
468 pub fn get_idf_weights(&self) -> &[Float] {
470 &self.idf_weights
471 }
472
473 fn calculate_idf_weights(&mut self, documents: &[String]) -> SklResult<()> {
474 let vocabulary = self
475 .count_vectorizer
476 .get_vocabulary()
477 .ok_or_else(|| SklearsError::InvalidInput("Count vectorizer not fitted".to_string()))?;
478
479 let n_docs = documents.len() as Float;
480 let mut idf_weights = vec![0.0; vocabulary.len()];
481
482 let mut doc_frequencies = vec![0; vocabulary.len()];
484
485 for document in documents {
486 let tokens = self.count_vectorizer.tokenize(document);
487 let ngrams = self.count_vectorizer.extract_ngrams(&tokens);
488 let unique_ngrams: std::collections::HashSet<_> = ngrams.into_iter().collect();
489
490 for ngram in unique_ngrams {
491 if let Some(&feature_idx) = vocabulary.get(&ngram) {
492 doc_frequencies[feature_idx] += 1;
493 }
494 }
495 }
496
497 for (i, &doc_freq) in doc_frequencies.iter().enumerate() {
499 let df = doc_freq as Float;
500 let idf = if self.smooth_idf {
501 (n_docs / (1.0 + df)).ln() + 1.0
502 } else {
503 (n_docs / df).ln() + 1.0
504 };
505 idf_weights[i] = idf;
506 }
507
508 self.idf_weights = idf_weights;
509 Ok(())
510 }
511
512 fn normalize_rows(&self, matrix: &mut Array2<Float>, norm_type: &str) -> SklResult<()> {
513 for mut row in matrix.axis_iter_mut(Axis(0)) {
514 let norm = match norm_type {
515 "l1" => row.iter().map(|x| x.abs()).sum::<Float>(),
516 "l2" => row.iter().map(|x| x * x).sum::<Float>().sqrt(),
517 _ => {
518 return Err(SklearsError::InvalidInput(format!(
519 "Unknown norm type: {}",
520 norm_type
521 )))
522 }
523 };
524
525 if norm > 0.0 {
526 for x in row.iter_mut() {
527 *x /= norm;
528 }
529 }
530 }
531
532 Ok(())
533 }
534}
535
536impl Default for TfidfVectorizer {
537 fn default() -> Self {
538 Self::new()
539 }
540}
541
542#[derive(Debug, Clone)]
544pub struct FittedTfidfVectorizer {
545 inner: TfidfVectorizer,
546}
547
548impl FittedTfidfVectorizer {
549 pub fn transform(&self, documents: &[String]) -> SklResult<Array2<Float>> {
551 self.inner.transform(documents)
552 }
553
554 pub fn get_feature_names(&self) -> &[String] {
556 self.inner.get_feature_names()
557 }
558
559 pub fn get_vocabulary(&self) -> Option<&HashMap<String, usize>> {
561 self.inner.get_vocabulary()
562 }
563
564 pub fn get_idf_weights(&self) -> &[Float] {
566 self.inner.get_idf_weights()
567 }
568}
569
570impl Fit<Vec<String>, ()> for TfidfVectorizer {
571 type Fitted = FittedTfidfVectorizer;
572
573 fn fit(mut self, x: &Vec<String>, _y: &()) -> SklResult<Self::Fitted> {
574 self.fit_instance_method(x)?;
575 Ok(FittedTfidfVectorizer { inner: self })
576 }
577}
578
579impl Transform<Vec<String>, Array2<Float>> for FittedTfidfVectorizer {
580 fn transform(&self, x: &Vec<String>) -> SklResult<Array2<Float>> {
581 self.inner.transform(x)
582 }
583}
584
585#[derive(Debug, Clone)]
586pub struct HashingVectorizer {
587 n_features: usize,
588 #[allow(dead_code)] ngram_range: (usize, usize),
590}
591
592impl HashingVectorizer {
593 pub fn new() -> Self {
594 Self {
595 n_features: 1024,
596 ngram_range: (1, 1),
597 }
598 }
599
600 pub fn n_features(mut self, n_features: usize) -> Self {
601 self.n_features = n_features;
602 self
603 }
604}
605
606impl Default for HashingVectorizer {
607 fn default() -> Self {
608 Self::new()
609 }
610}
611
612#[derive(Debug, Clone)]
617pub struct SentimentAnalyzer {
618 positive_words: std::collections::HashSet<String>,
619 negative_words: std::collections::HashSet<String>,
620 neutral_threshold: f64,
621 case_sensitive: bool,
622}
623
624impl SentimentAnalyzer {
625 pub fn new() -> Self {
627 let mut analyzer = Self {
628 positive_words: std::collections::HashSet::new(),
629 negative_words: std::collections::HashSet::new(),
630 neutral_threshold: 0.1,
631 case_sensitive: false,
632 };
633
634 for word in &[
636 "good",
637 "great",
638 "excellent",
639 "amazing",
640 "wonderful",
641 "fantastic",
642 "awesome",
643 "love",
644 "like",
645 "enjoy",
646 "happy",
647 "pleased",
648 "satisfied",
649 "perfect",
650 "best",
651 "brilliant",
652 "outstanding",
653 "superb",
654 "marvelous",
655 "incredible",
656 ] {
657 analyzer.positive_words.insert(word.to_string());
658 }
659
660 for word in &[
662 "bad",
663 "terrible",
664 "awful",
665 "horrible",
666 "disgusting",
667 "hate",
668 "dislike",
669 "disappointed",
670 "frustrated",
671 "angry",
672 "sad",
673 "worst",
674 "pathetic",
675 "useless",
676 "annoying",
677 "boring",
678 "stupid",
679 "ridiculous",
680 "poor",
681 ] {
682 analyzer.negative_words.insert(word.to_string());
683 }
684
685 analyzer
686 }
687
688 pub fn neutral_threshold(mut self, threshold: f64) -> Self {
690 self.neutral_threshold = threshold;
691 self
692 }
693
694 pub fn case_sensitive(mut self, case_sensitive: bool) -> Self {
696 self.case_sensitive = case_sensitive;
697 self
698 }
699
700 pub fn add_positive_words(mut self, words: Vec<String>) -> Self {
702 for word in words {
703 self.positive_words.insert(word);
704 }
705 self
706 }
707
708 pub fn add_negative_words(mut self, words: Vec<String>) -> Self {
710 for word in words {
711 self.negative_words.insert(word);
712 }
713 self
714 }
715
716 pub fn analyze_sentiment(&self, text: &str) -> SentimentResult {
718 let text = if self.case_sensitive {
719 text.to_string()
720 } else {
721 text.to_lowercase()
722 };
723
724 let words: Vec<&str> = text.split_whitespace().collect();
725 let mut positive_count = 0;
726 let mut negative_count = 0;
727
728 for word in &words {
729 let clean_word = word.trim_matches(|c: char| c.is_ascii_punctuation());
731
732 if self.positive_words.contains(clean_word) {
733 positive_count += 1;
734 } else if self.negative_words.contains(clean_word) {
735 negative_count += 1;
736 }
737 }
738
739 let total_sentiment_words = positive_count + negative_count;
740 let score = if total_sentiment_words == 0 {
741 0.0
742 } else {
743 (positive_count as f64 - negative_count as f64) / total_sentiment_words as f64
744 };
745
746 let polarity = if score.abs() <= self.neutral_threshold {
747 SentimentPolarity::Neutral
748 } else if score > 0.0 {
749 SentimentPolarity::Positive
750 } else {
751 SentimentPolarity::Negative
752 };
753
754 SentimentResult {
756 polarity,
757 score,
758 positive_words: positive_count,
759 negative_words: negative_count,
760 total_words: words.len(),
761 }
762 }
763
764 pub fn extract_features(&self, documents: &[String]) -> SklResult<Array2<Float>> {
766 if documents.is_empty() {
767 return Err(SklearsError::InvalidInput(
768 "Empty document collection".to_string(),
769 ));
770 }
771
772 let mut features = Array2::zeros((documents.len(), 5));
773
774 for (i, document) in documents.iter().enumerate() {
775 let sentiment = self.analyze_sentiment(document);
776
777 features[(i, 0)] = sentiment.score;
779 features[(i, 1)] =
780 sentiment.positive_words as f64 / sentiment.total_words.max(1) as f64;
781 features[(i, 2)] =
782 sentiment.negative_words as f64 / sentiment.total_words.max(1) as f64;
783 features[(i, 3)] = (sentiment.positive_words + sentiment.negative_words) as f64
784 / sentiment.total_words.max(1) as f64;
785 features[(i, 4)] = match sentiment.polarity {
786 SentimentPolarity::Negative => -1.0,
787 SentimentPolarity::Neutral => 0.0,
788 SentimentPolarity::Positive => 1.0,
789 };
790 }
791
792 Ok(features)
793 }
794}
795
796impl Default for SentimentAnalyzer {
797 fn default() -> Self {
798 Self::new()
799 }
800}
801
802#[derive(Debug, Clone)]
804pub struct SentimentResult {
805 pub polarity: SentimentPolarity,
807 pub score: f64,
809 pub positive_words: usize,
811 pub negative_words: usize,
813 pub total_words: usize,
815}
816
817#[derive(Debug, Clone, PartialEq)]
819pub enum SentimentPolarity {
820 Positive,
822 Negative,
824 Neutral,
826}
827
828#[derive(Debug, Clone)]
833pub struct StreamingTextProcessor {
834 chunk_size: usize,
835 overlap_size: usize,
836 min_chunk_words: usize,
837}
838
839impl StreamingTextProcessor {
840 pub fn new() -> Self {
842 Self {
843 chunk_size: 10000, overlap_size: 1000,
845 min_chunk_words: 50,
846 }
847 }
848
849 pub fn chunk_size(mut self, chunk_size: usize) -> Self {
851 self.chunk_size = chunk_size;
852 self
853 }
854
855 pub fn overlap_size(mut self, overlap_size: usize) -> Self {
857 self.overlap_size = overlap_size;
858 self
859 }
860
861 pub fn min_chunk_words(mut self, min_words: usize) -> Self {
863 self.min_chunk_words = min_words;
864 self
865 }
866
867 pub fn stream_process_with_count_vectorizer(
869 &self,
870 large_text: &str,
871 vectorizer: &mut CountVectorizer,
872 ) -> SklResult<Array2<Float>> {
873 let chunks = self.create_chunks(large_text);
874
875 if chunks.is_empty() {
876 return Err(SklearsError::InvalidInput(
877 "No valid chunks created".to_string(),
878 ));
879 }
880
881 let sample_size = (chunks.len() / 4).max(1);
883 let sample_chunks: Vec<String> = chunks
884 .iter()
885 .step_by(chunks.len() / sample_size)
886 .take(sample_size)
887 .cloned()
888 .collect();
889
890 vectorizer.fit_instance_method(&sample_chunks)?;
891
892 let chunk_features = vectorizer.transform(&chunks)?;
894
895 let (n_chunks, n_features) = chunk_features.dim();
897 let mut aggregated = Array2::zeros((1, n_features));
898
899 for j in 0..n_features {
900 let mut sum = 0.0;
901 for i in 0..n_chunks {
902 sum += chunk_features[(i, j)];
903 }
904 aggregated[(0, j)] = sum / n_chunks as f64;
905 }
906
907 Ok(aggregated)
908 }
909
910 pub fn stream_process_with_tfidf(
912 &self,
913 large_text: &str,
914 vectorizer: &mut TfidfVectorizer,
915 ) -> SklResult<Array2<Float>> {
916 let chunks = self.create_chunks(large_text);
917
918 if chunks.is_empty() {
919 return Err(SklearsError::InvalidInput(
920 "No valid chunks created".to_string(),
921 ));
922 }
923
924 vectorizer.fit_instance_method(&chunks)?;
926 let chunk_features = vectorizer.transform(&chunks)?;
927
928 let (n_chunks, n_features) = chunk_features.dim();
930 let mut aggregated = Array2::zeros((1, n_features));
931 let chunk_weights: Vec<f64> = chunks
932 .iter()
933 .map(|chunk| chunk.split_whitespace().count() as f64)
934 .collect();
935 let total_weight: f64 = chunk_weights.iter().sum();
936
937 for j in 0..n_features {
938 let mut weighted_sum = 0.0;
939 for i in 0..n_chunks {
940 weighted_sum += chunk_features[(i, j)] * chunk_weights[i];
941 }
942 aggregated[(0, j)] = weighted_sum / total_weight;
943 }
944
945 Ok(aggregated)
946 }
947
948 pub fn extract_streaming_stats(&self, large_text: &str) -> SklResult<Array1<Float>> {
950 let chunks = self.create_chunks(large_text);
951
952 if chunks.is_empty() {
953 return Err(SklearsError::InvalidInput(
954 "No valid chunks created".to_string(),
955 ));
956 }
957
958 let mut total_chars = 0;
959 let mut total_words = 0;
960 let mut total_sentences = 0;
961 let mut word_lengths = Vec::new();
962
963 for chunk in &chunks {
964 total_chars += chunk.len();
965 let words: Vec<&str> = chunk.split_whitespace().collect();
966 total_words += words.len();
967
968 total_sentences += chunk.matches('.').count()
970 + chunk.matches('!').count()
971 + chunk.matches('?').count();
972
973 for word in words {
975 let clean_word = word.trim_matches(|c: char| c.is_ascii_punctuation());
976 if !clean_word.is_empty() {
977 word_lengths.push(clean_word.len() as f64);
978 }
979 }
980 }
981
982 let avg_word_length = if !word_lengths.is_empty() {
984 word_lengths.iter().sum::<f64>() / word_lengths.len() as f64
985 } else {
986 0.0
987 };
988
989 let avg_words_per_sentence = if total_sentences > 0 {
990 total_words as f64 / total_sentences as f64
991 } else {
992 0.0
993 };
994
995 let features = vec![
996 total_chars as f64,
997 total_words as f64,
998 total_sentences as f64,
999 avg_word_length,
1000 avg_words_per_sentence,
1001 total_chars as f64 / total_words.max(1) as f64, ];
1003
1004 Ok(Array1::from_vec(features))
1005 }
1006
1007 fn create_chunks(&self, text: &str) -> Vec<String> {
1008 let mut chunks = Vec::new();
1009 let mut start = 0;
1010
1011 while start < text.len() {
1012 let end = (start + self.chunk_size).min(text.len());
1013 let chunk = &text[start..end];
1014
1015 let final_chunk = if end < text.len() {
1017 if let Some(last_space) = chunk.rfind(' ') {
1018 &chunk[..last_space]
1019 } else {
1020 chunk
1021 }
1022 } else {
1023 chunk
1024 };
1025
1026 if final_chunk.split_whitespace().count() >= self.min_chunk_words {
1028 chunks.push(final_chunk.to_string());
1029 }
1030
1031 start += final_chunk.len().saturating_sub(self.overlap_size);
1032 if start >= text.len() {
1033 break;
1034 }
1035 }
1036
1037 chunks
1038 }
1039}
1040
1041impl Default for StreamingTextProcessor {
1042 fn default() -> Self {
1043 Self::new()
1044 }
1045}
1046
1047#[derive(Debug, Clone)]
1049pub struct LSA {
1050 n_components: usize,
1051}
1052
1053impl LSA {
1054 pub fn new() -> Self {
1055 Self { n_components: 100 }
1056 }
1057
1058 pub fn n_components(mut self, n_components: usize) -> Self {
1059 self.n_components = n_components;
1060 self
1061 }
1062
1063 pub fn algorithm(self, _algorithm: &str) -> Self {
1064 self
1066 }
1067
1068 #[allow(non_snake_case)]
1069 pub fn fit(&self, _X: &ArrayView2<f64>) -> SklResult<LSATrained> {
1070 Ok(LSATrained {
1072 components: Array2::zeros((self.n_components, 10)),
1073 })
1074 }
1075}
1076
1077#[derive(Debug, Clone)]
1078pub struct LSATrained {
1079 #[allow(dead_code)] components: Array2<f64>,
1081}
1082
1083impl Default for LSA {
1084 fn default() -> Self {
1085 Self::new()
1086 }
1087}
1088
1089#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1095pub enum EmotionType {
1096 Joy,
1098 Sadness,
1100 Anger,
1102 Fear,
1104 Surprise,
1106 Disgust,
1108 Neutral,
1110}
1111
1112impl EmotionType {
1113 pub fn all_emotions() -> Vec<EmotionType> {
1115 vec![
1116 EmotionType::Joy,
1117 EmotionType::Sadness,
1118 EmotionType::Anger,
1119 EmotionType::Fear,
1120 EmotionType::Surprise,
1121 EmotionType::Disgust,
1122 ]
1123 }
1124
1125 pub fn as_str(&self) -> &'static str {
1127 match self {
1128 EmotionType::Joy => "joy",
1129 EmotionType::Sadness => "sadness",
1130 EmotionType::Anger => "anger",
1131 EmotionType::Fear => "fear",
1132 EmotionType::Surprise => "surprise",
1133 EmotionType::Disgust => "disgust",
1134 EmotionType::Neutral => "neutral",
1135 }
1136 }
1137}
1138
1139#[derive(Debug, Clone)]
1141pub struct EmotionResult {
1142 pub primary_emotion: EmotionType,
1144 pub confidence: f64,
1146 pub emotion_scores: std::collections::HashMap<EmotionType, f64>,
1148 pub total_emotion_words: usize,
1150 pub total_words: usize,
1152}
1153
1154impl EmotionResult {
1155 pub fn intensity(&self) -> f64 {
1157 if self.total_words == 0 {
1158 0.0
1159 } else {
1160 self.total_emotion_words as f64 / self.total_words as f64
1161 }
1162 }
1163
1164 pub fn secondary_emotion(&self) -> Option<(EmotionType, f64)> {
1166 let mut sorted: Vec<_> = self
1167 .emotion_scores
1168 .iter()
1169 .filter(|(e, _)| **e != self.primary_emotion)
1170 .map(|(e, s)| (*e, *s))
1171 .collect();
1172 sorted.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
1173 sorted.first().copied()
1174 }
1175}
1176
1177#[derive(Debug, Clone)]
1192pub struct EmotionDetector {
1193 emotion_lexicons: std::collections::HashMap<EmotionType, std::collections::HashSet<String>>,
1194 case_sensitive: bool,
1195 min_confidence: f64,
1196 intensity_weight: f64,
1197}
1198
1199impl EmotionDetector {
1200 pub fn new() -> Self {
1202 let mut detector = Self {
1203 emotion_lexicons: std::collections::HashMap::new(),
1204 case_sensitive: false,
1205 min_confidence: 0.1,
1206 intensity_weight: 0.5,
1207 };
1208
1209 detector.initialize_lexicons();
1211 detector
1212 }
1213
1214 fn initialize_lexicons(&mut self) {
1216 let joy_words = vec![
1218 "happy",
1219 "joy",
1220 "joyful",
1221 "cheerful",
1222 "delighted",
1223 "pleased",
1224 "glad",
1225 "excited",
1226 "thrilled",
1227 "ecstatic",
1228 "elated",
1229 "wonderful",
1230 "amazing",
1231 "fantastic",
1232 "great",
1233 "excellent",
1234 "love",
1235 "loving",
1236 "loved",
1237 "enjoy",
1238 "fun",
1239 "amusing",
1240 "hilarious",
1241 "laugh",
1242 "laughter",
1243 "smile",
1244 "smiling",
1245 "blessed",
1246 "grateful",
1247 "thankful",
1248 "celebration",
1249 "celebrate",
1250 "victory",
1251 ];
1252 self.add_emotion_words(EmotionType::Joy, joy_words);
1253
1254 let sadness_words = vec![
1256 "sad",
1257 "unhappy",
1258 "depressed",
1259 "miserable",
1260 "sorrowful",
1261 "gloomy",
1262 "melancholy",
1263 "dejected",
1264 "downcast",
1265 "heartbroken",
1266 "crying",
1267 "cry",
1268 "tears",
1269 "weeping",
1270 "grief",
1271 "grieving",
1272 "mourning",
1273 "loss",
1274 "lost",
1275 "lonely",
1276 "loneliness",
1277 "alone",
1278 "isolated",
1279 "hopeless",
1280 "despair",
1281 "disappointed",
1282 "regret",
1283 "sorry",
1284 "hurt",
1285 "pain",
1286 "painful",
1287 ];
1288 self.add_emotion_words(EmotionType::Sadness, sadness_words);
1289
1290 let anger_words = vec![
1292 "angry",
1293 "mad",
1294 "furious",
1295 "enraged",
1296 "outraged",
1297 "livid",
1298 "irate",
1299 "annoyed",
1300 "irritated",
1301 "frustrated",
1302 "rage",
1303 "fury",
1304 "wrath",
1305 "hatred",
1306 "hate",
1307 "hating",
1308 "hostile",
1309 "aggravated",
1310 "infuriated",
1311 "resentful",
1312 "bitter",
1313 "indignant",
1314 "offensive",
1315 "insulting",
1316 "provoked",
1317 "incensed",
1318 "exasperated",
1319 "disgusted",
1320 "contempt",
1321 ];
1322 self.add_emotion_words(EmotionType::Anger, anger_words);
1323
1324 let fear_words = vec![
1326 "afraid",
1327 "scared",
1328 "fearful",
1329 "frightened",
1330 "terrified",
1331 "horrified",
1332 "panic",
1333 "panicked",
1334 "anxious",
1335 "anxiety",
1336 "worried",
1337 "nervous",
1338 "uneasy",
1339 "alarmed",
1340 "threatened",
1341 "intimidated",
1342 "dread",
1343 "dreading",
1344 "apprehensive",
1345 "tense",
1346 "stressed",
1347 "stress",
1348 "concern",
1349 "concerned",
1350 "phobia",
1351 "terror",
1352 "horror",
1353 "nightmare",
1354 "shock",
1355 "shocked",
1356 ];
1357 self.add_emotion_words(EmotionType::Fear, fear_words);
1358
1359 let surprise_words = vec![
1361 "surprised",
1362 "amazed",
1363 "astonished",
1364 "astounded",
1365 "shocked",
1366 "stunned",
1367 "startled",
1368 "unexpected",
1369 "sudden",
1370 "wow",
1371 "incredible",
1372 "unbelievable",
1373 "remarkable",
1374 "extraordinary",
1375 "spectacular",
1376 "breathtaking",
1377 "dumbfounded",
1378 "flabbergasted",
1379 "bewildered",
1380 "confused",
1381 "puzzled",
1382 "wonder",
1383 "wondering",
1384 "curious",
1385 "odd",
1386 "strange",
1387 "unusual",
1388 ];
1389 self.add_emotion_words(EmotionType::Surprise, surprise_words);
1390
1391 let disgust_words = vec![
1393 "disgusted",
1394 "disgusting",
1395 "revolting",
1396 "repulsive",
1397 "repugnant",
1398 "nauseating",
1399 "sickening",
1400 "gross",
1401 "yuck",
1402 "nasty",
1403 "foul",
1404 "vile",
1405 "loathsome",
1406 "offensive",
1407 "appalling",
1408 "abhorrent",
1409 "detestable",
1410 "horrible",
1411 "terrible",
1412 "awful",
1413 "unpleasant",
1414 "distasteful",
1415 "repellent",
1416 "obnoxious",
1417 "hideous",
1418 "ugly",
1419 "grotesque",
1420 ];
1421 self.add_emotion_words(EmotionType::Disgust, disgust_words);
1422 }
1423
1424 fn add_emotion_words(&mut self, emotion: EmotionType, words: Vec<&str>) {
1426 let word_set = self.emotion_lexicons.entry(emotion).or_default();
1427
1428 for word in words {
1429 word_set.insert(word.to_string());
1430 }
1431 }
1432
1433 pub fn case_sensitive(mut self, case_sensitive: bool) -> Self {
1435 self.case_sensitive = case_sensitive;
1436 self
1437 }
1438
1439 pub fn min_confidence(mut self, min_confidence: f64) -> Self {
1441 self.min_confidence = min_confidence.clamp(0.0, 1.0);
1442 self
1443 }
1444
1445 pub fn intensity_weight(mut self, weight: f64) -> Self {
1447 self.intensity_weight = weight.clamp(0.0, 1.0);
1448 self
1449 }
1450
1451 pub fn add_custom_emotion_words(mut self, emotion: EmotionType, words: Vec<String>) -> Self {
1453 let word_set = self.emotion_lexicons.entry(emotion).or_default();
1454
1455 for word in words {
1456 word_set.insert(word);
1457 }
1458 self
1459 }
1460
1461 pub fn detect_emotion(&self, text: &str) -> EmotionResult {
1463 let text = if self.case_sensitive {
1464 text.to_string()
1465 } else {
1466 text.to_lowercase()
1467 };
1468
1469 let words: Vec<&str> = text.split_whitespace().collect();
1470 let total_words = words.len();
1471
1472 let mut emotion_counts: std::collections::HashMap<EmotionType, usize> =
1474 std::collections::HashMap::new();
1475 let mut total_emotion_words = 0;
1476
1477 for word in &words {
1478 let clean_word = word.trim_matches(|c: char| c.is_ascii_punctuation());
1480
1481 for (emotion, lexicon) in &self.emotion_lexicons {
1483 if lexicon.contains(clean_word) {
1484 *emotion_counts.entry(*emotion).or_insert(0) += 1;
1485 total_emotion_words += 1;
1486 }
1487 }
1488 }
1489
1490 let mut emotion_scores: std::collections::HashMap<EmotionType, f64> =
1492 std::collections::HashMap::new();
1493
1494 for emotion in EmotionType::all_emotions() {
1495 let count = *emotion_counts.get(&emotion).unwrap_or(&0) as f64;
1496 let intensity = if total_words > 0 {
1497 count / total_words as f64
1498 } else {
1499 0.0
1500 };
1501
1502 let score = if total_emotion_words > 0 {
1504 (1.0 - self.intensity_weight) * (count / total_emotion_words as f64)
1505 + self.intensity_weight * intensity
1506 } else {
1507 0.0
1508 };
1509
1510 emotion_scores.insert(emotion, score);
1511 }
1512
1513 let (primary_emotion, confidence) = if total_emotion_words == 0 {
1515 (EmotionType::Neutral, 0.0)
1516 } else {
1517 let mut sorted: Vec<_> = emotion_scores.iter().map(|(e, s)| (*e, *s)).collect();
1518 sorted.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
1519
1520 let (emotion, score) = sorted[0];
1521 if score >= self.min_confidence {
1522 (emotion, score)
1523 } else {
1524 (EmotionType::Neutral, score)
1525 }
1526 };
1527
1528 EmotionResult {
1529 primary_emotion,
1530 confidence,
1531 emotion_scores,
1532 total_emotion_words,
1533 total_words,
1534 }
1535 }
1536
1537 pub fn extract_features(&self, documents: &[String]) -> SklResult<Array2<Float>> {
1545 if documents.is_empty() {
1546 return Err(SklearsError::InvalidInput(
1547 "Empty document collection".to_string(),
1548 ));
1549 }
1550
1551 let mut features = Array2::zeros((documents.len(), 14));
1555
1556 for (i, document) in documents.iter().enumerate() {
1557 let result = self.detect_emotion(document);
1558
1559 features[(i, 0)] = *result.emotion_scores.get(&EmotionType::Joy).unwrap_or(&0.0);
1561 features[(i, 1)] = *result
1562 .emotion_scores
1563 .get(&EmotionType::Sadness)
1564 .unwrap_or(&0.0);
1565 features[(i, 2)] = *result
1566 .emotion_scores
1567 .get(&EmotionType::Anger)
1568 .unwrap_or(&0.0);
1569 features[(i, 3)] = *result
1570 .emotion_scores
1571 .get(&EmotionType::Fear)
1572 .unwrap_or(&0.0);
1573 features[(i, 4)] = *result
1574 .emotion_scores
1575 .get(&EmotionType::Surprise)
1576 .unwrap_or(&0.0);
1577 features[(i, 5)] = *result
1578 .emotion_scores
1579 .get(&EmotionType::Disgust)
1580 .unwrap_or(&0.0);
1581
1582 let primary_idx = match result.primary_emotion {
1584 EmotionType::Joy => 6,
1585 EmotionType::Sadness => 7,
1586 EmotionType::Anger => 8,
1587 EmotionType::Fear => 9,
1588 EmotionType::Surprise => 10,
1589 EmotionType::Disgust => 11,
1590 EmotionType::Neutral => continue, };
1592 features[(i, primary_idx)] = 1.0;
1593
1594 features[(i, 12)] = result.confidence;
1596 features[(i, 13)] = result.intensity();
1597 }
1598
1599 Ok(features)
1600 }
1601
1602 pub fn analyze_distribution(
1604 &self,
1605 documents: &[String],
1606 ) -> std::collections::HashMap<EmotionType, usize> {
1607 let mut distribution: std::collections::HashMap<EmotionType, usize> =
1608 std::collections::HashMap::new();
1609
1610 for document in documents {
1611 let result = self.detect_emotion(document);
1612 *distribution.entry(result.primary_emotion).or_insert(0) += 1;
1613 }
1614
1615 distribution
1616 }
1617}
1618
1619impl Default for EmotionDetector {
1620 fn default() -> Self {
1621 Self::new()
1622 }
1623}
1624
1625#[derive(Debug, Clone, PartialEq)]
1631pub struct AspectSentiment {
1632 pub aspect: String,
1634 pub sentiment: SentimentPolarity,
1636 pub score: f64,
1638 pub confidence: f64,
1640 pub opinion_words: Vec<String>,
1642}
1643
1644#[derive(Debug, Clone)]
1659pub struct AspectBasedSentimentAnalyzer {
1660 aspects: std::collections::HashSet<String>,
1662 sentiment_analyzer: SentimentAnalyzer,
1664 context_window: usize,
1666 min_confidence: f64,
1668 case_sensitive: bool,
1670 auto_extract_aspects: bool,
1672}
1673
1674impl AspectBasedSentimentAnalyzer {
1675 pub fn new() -> Self {
1677 Self {
1678 aspects: std::collections::HashSet::new(),
1679 sentiment_analyzer: SentimentAnalyzer::new(),
1680 context_window: 3,
1681 min_confidence: 0.1,
1682 case_sensitive: false,
1683 auto_extract_aspects: true,
1684 }
1685 }
1686
1687 pub fn add_aspects(mut self, aspects: Vec<String>) -> Self {
1689 for aspect in aspects {
1690 self.aspects.insert(if self.case_sensitive {
1691 aspect
1692 } else {
1693 aspect.to_lowercase()
1694 });
1695 }
1696 self
1697 }
1698
1699 pub fn context_window(mut self, window: usize) -> Self {
1701 self.context_window = window;
1702 self
1703 }
1704
1705 pub fn min_confidence(mut self, confidence: f64) -> Self {
1707 self.min_confidence = confidence.clamp(0.0, 1.0);
1708 self
1709 }
1710
1711 pub fn case_sensitive(mut self, case_sensitive: bool) -> Self {
1713 self.case_sensitive = case_sensitive;
1714 self
1715 }
1716
1717 pub fn auto_extract_aspects(mut self, auto_extract: bool) -> Self {
1719 self.auto_extract_aspects = auto_extract;
1720 self
1721 }
1722
1723 pub fn sentiment_analyzer(mut self, analyzer: SentimentAnalyzer) -> Self {
1725 self.sentiment_analyzer = analyzer;
1726 self
1727 }
1728
1729 pub fn analyze(&self, text: &str) -> Vec<AspectSentiment> {
1731 let normalized_text = if self.case_sensitive {
1732 text.to_string()
1733 } else {
1734 text.to_lowercase()
1735 };
1736
1737 let words: Vec<&str> = normalized_text.split_whitespace().collect();
1738 let mut aspect_sentiments = Vec::new();
1739
1740 let mut aspect_positions: Vec<(usize, String)> = Vec::new();
1742
1743 if !self.aspects.is_empty() {
1745 for (i, word) in words.iter().enumerate() {
1746 let clean_word = word.trim_matches(|c: char| c.is_ascii_punctuation());
1747
1748 if self.aspects.contains(clean_word) {
1750 aspect_positions.push((i, clean_word.to_string()));
1751 }
1752
1753 for window_size in 2..=3 {
1755 if i + window_size <= words.len() {
1756 let multi_word = words[i..i + window_size]
1757 .iter()
1758 .map(|w| w.trim_matches(|c: char| c.is_ascii_punctuation()))
1759 .collect::<Vec<_>>()
1760 .join(" ");
1761
1762 if self.aspects.contains(&multi_word) {
1763 aspect_positions.push((i, multi_word));
1764 }
1765 }
1766 }
1767 }
1768 }
1769
1770 if self.auto_extract_aspects && self.aspects.is_empty() {
1772 aspect_positions.extend(self.extract_aspects_auto(&words));
1773 }
1774
1775 for (position, aspect) in aspect_positions {
1777 let (context_words, opinion_words) = self.get_context(position, &words);
1778
1779 let context_text = context_words.join(" ");
1781 let sentiment_result = self.sentiment_analyzer.analyze_sentiment(&context_text);
1782
1783 let confidence = if context_words.is_empty() {
1785 0.0
1786 } else {
1787 (sentiment_result.positive_words + sentiment_result.negative_words) as f64
1788 / context_words.len() as f64
1789 };
1790
1791 if confidence >= self.min_confidence {
1792 aspect_sentiments.push(AspectSentiment {
1793 aspect,
1794 sentiment: sentiment_result.polarity,
1795 score: sentiment_result.score,
1796 confidence,
1797 opinion_words,
1798 });
1799 }
1800 }
1801
1802 aspect_sentiments
1803 }
1804
1805 fn get_context(&self, position: usize, words: &[&str]) -> (Vec<String>, Vec<String>) {
1807 let start = position.saturating_sub(self.context_window);
1808 let end = (position + self.context_window + 1).min(words.len());
1809
1810 let context_words: Vec<String> = words[start..end]
1811 .iter()
1812 .map(|w| {
1813 w.trim_matches(|c: char| c.is_ascii_punctuation())
1814 .to_string()
1815 })
1816 .collect();
1817
1818 let opinion_words: Vec<String> = context_words
1820 .iter()
1821 .filter(|w| {
1822 self.sentiment_analyzer.positive_words.contains(w.as_str())
1823 || self.sentiment_analyzer.negative_words.contains(w.as_str())
1824 })
1825 .cloned()
1826 .collect();
1827
1828 (context_words, opinion_words)
1829 }
1830
1831 fn extract_aspects_auto(&self, words: &[&str]) -> Vec<(usize, String)> {
1833 let mut aspects = Vec::new();
1834
1835 let aspect_indicators = [
1837 "food",
1838 "service",
1839 "price",
1840 "quality",
1841 "staff",
1842 "location",
1843 "room",
1844 "product",
1845 "delivery",
1846 "support",
1847 "performance",
1848 "design",
1849 "battery",
1850 "screen",
1851 "camera",
1852 "sound",
1853 "comfort",
1854 "size",
1855 "color",
1856 "taste",
1857 "menu",
1858 "atmosphere",
1859 "experience",
1860 "value",
1861 "selection",
1862 ];
1863
1864 for (i, word) in words.iter().enumerate() {
1865 let clean_word = word.trim_matches(|c: char| c.is_ascii_punctuation());
1866
1867 if aspect_indicators.contains(&clean_word) {
1869 aspects.push((i, clean_word.to_string()));
1870 }
1871 }
1872
1873 aspects
1874 }
1875
1876 pub fn extract_features(&self, documents: &[String]) -> SklResult<Array2<Float>> {
1882 if documents.is_empty() {
1883 return Err(SklearsError::InvalidInput(
1884 "Empty document collection".to_string(),
1885 ));
1886 }
1887
1888 let mut features = Array2::zeros((documents.len(), 6));
1889
1890 for (i, document) in documents.iter().enumerate() {
1891 let aspect_sentiments = self.analyze(document);
1892
1893 if aspect_sentiments.is_empty() {
1894 continue;
1895 }
1896
1897 let avg_score = aspect_sentiments.iter().map(|a| a.score).sum::<f64>()
1899 / aspect_sentiments.len() as f64;
1900
1901 let positive_count = aspect_sentiments
1902 .iter()
1903 .filter(|a| a.sentiment == SentimentPolarity::Positive)
1904 .count() as f64;
1905
1906 let negative_count = aspect_sentiments
1907 .iter()
1908 .filter(|a| a.sentiment == SentimentPolarity::Negative)
1909 .count() as f64;
1910
1911 let neutral_count = aspect_sentiments
1912 .iter()
1913 .filter(|a| a.sentiment == SentimentPolarity::Neutral)
1914 .count() as f64;
1915
1916 let unique_aspects: std::collections::HashSet<_> =
1918 aspect_sentiments.iter().map(|a| &a.aspect).collect();
1919 let diversity = unique_aspects.len() as f64 / aspect_sentiments.len() as f64;
1920
1921 let avg_confidence = aspect_sentiments.iter().map(|a| a.confidence).sum::<f64>()
1922 / aspect_sentiments.len() as f64;
1923
1924 features[(i, 0)] = avg_score;
1925 features[(i, 1)] = positive_count;
1926 features[(i, 2)] = negative_count;
1927 features[(i, 3)] = neutral_count;
1928 features[(i, 4)] = diversity;
1929 features[(i, 5)] = avg_confidence;
1930 }
1931
1932 Ok(features)
1933 }
1934
1935 pub fn aggregate_aspects(
1937 &self,
1938 documents: &[String],
1939 ) -> std::collections::HashMap<String, Vec<AspectSentiment>> {
1940 let mut aggregated: std::collections::HashMap<String, Vec<AspectSentiment>> =
1941 std::collections::HashMap::new();
1942
1943 for document in documents {
1944 let sentiments = self.analyze(document);
1945 for sentiment in sentiments {
1946 aggregated
1947 .entry(sentiment.aspect.clone())
1948 .or_default()
1949 .push(sentiment);
1950 }
1951 }
1952
1953 aggregated
1954 }
1955
1956 pub fn aspect_summary(&self, documents: &[String]) -> Vec<(String, f64, usize)> {
1958 let aggregated = self.aggregate_aspects(documents);
1959
1960 let mut summary: Vec<(String, f64, usize)> = aggregated
1961 .iter()
1962 .map(|(aspect, sentiments)| {
1963 let avg_score =
1964 sentiments.iter().map(|s| s.score).sum::<f64>() / sentiments.len() as f64;
1965 let count = sentiments.len();
1966 (aspect.clone(), avg_score, count)
1967 })
1968 .collect();
1969
1970 summary.sort_by_key(|b| std::cmp::Reverse(b.2));
1972 summary
1973 }
1974}
1975
1976impl Default for AspectBasedSentimentAnalyzer {
1977 fn default() -> Self {
1978 Self::new()
1979 }
1980}