1use crate::eval::synthetic::AnnotatedExample;
33use serde::{Deserialize, Serialize};
34use std::collections::{HashMap, HashSet};
35
36#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct DatasetStats {
43 pub num_examples: usize,
45 pub num_entities: usize,
47 pub type_distribution: HashMap<String, f64>,
49 pub avg_entities_per_example: f64,
51 pub vocab_size: usize,
53 pub entity_length_stats: LengthStats,
55 pub unique_entity_texts: usize,
57 pub entity_diversity: f64,
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct LengthStats {
64 pub mean: f64,
66 pub median: f64,
68 pub std_dev: f64,
70 pub min: usize,
72 pub max: usize,
74}
75
76#[derive(Debug, Clone, Serialize, Deserialize)]
78pub struct DatasetComparison {
79 pub stats_a: DatasetStats,
81 pub stats_b: DatasetStats,
83 pub type_divergence: f64,
85 pub vocab_overlap: f64,
87 pub entity_text_overlap: f64,
89 pub types_only_in_a: Vec<String>,
91 pub types_only_in_b: Vec<String>,
93 pub estimated_domain_gap: f64,
95 pub recommendations: Vec<String>,
97}
98
99pub fn compute_stats(examples: &[AnnotatedExample]) -> DatasetStats {
105 if examples.is_empty() {
106 return DatasetStats {
107 num_examples: 0,
108 num_entities: 0,
109 type_distribution: HashMap::new(),
110 avg_entities_per_example: 0.0,
111 vocab_size: 0,
112 entity_length_stats: LengthStats {
113 mean: 0.0,
114 median: 0.0,
115 std_dev: 0.0,
116 min: 0,
117 max: 0,
118 },
119 unique_entity_texts: 0,
120 entity_diversity: 1.0,
121 };
122 }
123
124 let mut type_counts: HashMap<String, usize> = HashMap::new();
125 let mut vocab: HashSet<String> = HashSet::new();
126 let mut entity_texts: HashSet<String> = HashSet::new();
127 let mut entity_lengths: Vec<usize> = Vec::new();
128 let mut total_entities = 0;
129
130 for example in examples {
131 for token in example.text.split_whitespace() {
133 vocab.insert(token.to_lowercase());
134 }
135
136 for entity in &example.entities {
138 total_entities += 1;
139 *type_counts
140 .entry(entity.entity_type.to_string())
141 .or_insert(0) += 1;
142 entity_texts.insert(entity.text.to_lowercase());
143
144 let token_count = entity.text.split_whitespace().count().max(1);
146 entity_lengths.push(token_count);
147 }
148 }
149
150 let type_distribution: HashMap<String, f64> = type_counts
152 .iter()
153 .map(|(t, c)| (t.clone(), *c as f64 / total_entities.max(1) as f64))
154 .collect();
155
156 let entity_length_stats = if entity_lengths.is_empty() {
158 LengthStats {
159 mean: 0.0,
160 median: 0.0,
161 std_dev: 0.0,
162 min: 0,
163 max: 0,
164 }
165 } else {
166 let mut sorted = entity_lengths.clone();
167 sorted.sort_unstable();
168
169 let mean = entity_lengths.iter().sum::<usize>() as f64 / entity_lengths.len() as f64;
170 let median = sorted[sorted.len() / 2] as f64;
171 let variance = entity_lengths
172 .iter()
173 .map(|&l| (l as f64 - mean).powi(2))
174 .sum::<f64>()
175 / entity_lengths.len() as f64;
176 let std_dev = variance.sqrt();
177
178 LengthStats {
179 mean,
180 median,
181 std_dev,
182 min: *sorted.first().unwrap_or(&0),
183 max: *sorted.last().unwrap_or(&0),
184 }
185 };
186
187 DatasetStats {
188 num_examples: examples.len(),
189 num_entities: total_entities,
190 type_distribution,
191 avg_entities_per_example: total_entities as f64 / examples.len() as f64,
192 vocab_size: vocab.len(),
193 entity_length_stats,
194 unique_entity_texts: entity_texts.len(),
195 entity_diversity: entity_texts.len() as f64 / total_entities.max(1) as f64,
196 }
197}
198
199pub fn compare_datasets(a: &[AnnotatedExample], b: &[AnnotatedExample]) -> DatasetComparison {
201 let stats_a = compute_stats(a);
202 let stats_b = compute_stats(b);
203
204 let vocab_a: HashSet<String> = a
206 .iter()
207 .flat_map(|e| e.text.split_whitespace().map(|t| t.to_lowercase()))
208 .collect();
209 let vocab_b: HashSet<String> = b
210 .iter()
211 .flat_map(|e| e.text.split_whitespace().map(|t| t.to_lowercase()))
212 .collect();
213
214 let entities_a: HashSet<String> = a
216 .iter()
217 .flat_map(|e| e.entities.iter().map(|ent| ent.text.to_lowercase()))
218 .collect();
219 let entities_b: HashSet<String> = b
220 .iter()
221 .flat_map(|e| e.entities.iter().map(|ent| ent.text.to_lowercase()))
222 .collect();
223
224 let vocab_intersection = vocab_a.intersection(&vocab_b).count();
226 let vocab_union = vocab_a.union(&vocab_b).count();
227 let vocab_overlap = if vocab_union == 0 {
228 1.0
229 } else {
230 vocab_intersection as f64 / vocab_union as f64
231 };
232
233 let entity_intersection = entities_a.intersection(&entities_b).count();
235 let entity_union = entities_a.union(&entities_b).count();
236 let entity_text_overlap = if entity_union == 0 {
237 1.0
238 } else {
239 entity_intersection as f64 / entity_union as f64
240 };
241
242 let type_divergence =
244 jensen_shannon_divergence(&stats_a.type_distribution, &stats_b.type_distribution);
245
246 let types_a: HashSet<&String> = stats_a.type_distribution.keys().collect();
248 let types_b: HashSet<&String> = stats_b.type_distribution.keys().collect();
249 let types_only_in_a: Vec<String> = types_a.difference(&types_b).map(|s| (*s).clone()).collect();
250 let types_only_in_b: Vec<String> = types_b.difference(&types_a).map(|s| (*s).clone()).collect();
251
252 let estimated_domain_gap =
254 0.4 * type_divergence + 0.3 * (1.0 - vocab_overlap) + 0.3 * (1.0 - entity_text_overlap);
255
256 let recommendations = generate_recommendations(
258 type_divergence,
259 vocab_overlap,
260 entity_text_overlap,
261 &types_only_in_a,
262 &types_only_in_b,
263 );
264
265 DatasetComparison {
266 stats_a,
267 stats_b,
268 type_divergence,
269 vocab_overlap,
270 entity_text_overlap,
271 types_only_in_a,
272 types_only_in_b,
273 estimated_domain_gap,
274 recommendations,
275 }
276}
277
278fn jensen_shannon_divergence(p: &HashMap<String, f64>, q: &HashMap<String, f64>) -> f64 {
279 let all_keys: HashSet<&String> = p.keys().chain(q.keys()).collect();
281
282 if all_keys.is_empty() {
283 return 0.0;
284 }
285
286 let mut m: HashMap<&String, f64> = HashMap::new();
288 for k in &all_keys {
289 let p_val = p.get(*k).copied().unwrap_or(0.0);
290 let q_val = q.get(*k).copied().unwrap_or(0.0);
291 m.insert(*k, (p_val + q_val) / 2.0);
292 }
293
294 let kl_p_m: f64 = all_keys
296 .iter()
297 .map(|k| {
298 let p_val = p.get(*k).copied().unwrap_or(0.0);
299 let m_val = m.get(k).copied().unwrap_or(1e-10);
300 if p_val > 0.0 {
301 p_val * (p_val / m_val).ln()
302 } else {
303 0.0
304 }
305 })
306 .sum();
307
308 let kl_q_m: f64 = all_keys
309 .iter()
310 .map(|k| {
311 let q_val = q.get(*k).copied().unwrap_or(0.0);
312 let m_val = m.get(k).copied().unwrap_or(1e-10);
313 if q_val > 0.0 {
314 q_val * (q_val / m_val).ln()
315 } else {
316 0.0
317 }
318 })
319 .sum();
320
321 ((kl_p_m + kl_q_m) / 2.0) / 2.0_f64.ln()
324}
325
326fn generate_recommendations(
327 type_div: f64,
328 vocab_overlap: f64,
329 entity_overlap: f64,
330 types_only_a: &[String],
331 types_only_b: &[String],
332) -> Vec<String> {
333 let mut recs = Vec::new();
334
335 if type_div > 0.5 {
336 recs.push("High type distribution divergence - consider domain adaptation".into());
337 } else if type_div > 0.2 {
338 recs.push("Moderate type divergence - transfer learning may require fine-tuning".into());
339 }
340
341 if vocab_overlap < 0.3 {
342 recs.push("Low vocabulary overlap - domains use different terminology".into());
343 }
344
345 if entity_overlap < 0.1 {
346 recs.push("Very few shared entities - gazetteer transfer unlikely to help".into());
347 }
348
349 if !types_only_a.is_empty() {
350 recs.push(format!(
351 "Types in source only: {:?} - target may not need these",
352 types_only_a
353 ));
354 }
355
356 if !types_only_b.is_empty() {
357 recs.push(format!(
358 "Types in target only: {:?} - source cannot help with these",
359 types_only_b
360 ));
361 }
362
363 if recs.is_empty() {
364 recs.push("Datasets appear compatible for transfer learning".into());
365 }
366
367 recs
368}
369
370pub fn estimate_difficulty(stats: &DatasetStats) -> DifficultyEstimate {
376 let mut factors = Vec::new();
377 let mut score: f64 = 0.0;
378
379 let num_types = stats.type_distribution.len();
381 if num_types > 10 {
382 factors.push("Many entity types (>10)".into());
383 score += 0.2;
384 } else if num_types > 5 {
385 factors.push("Moderate entity types (5-10)".into());
386 score += 0.1;
387 }
388
389 if stats.entity_length_stats.mean > 3.0 {
391 factors.push("Long average entity length (>3 tokens)".into());
392 score += 0.2;
393 }
394
395 if stats.entity_length_stats.std_dev > 2.0 {
397 factors.push("High entity length variance".into());
398 score += 0.1;
399 }
400
401 if stats.entity_diversity > 0.9 {
403 factors.push("High entity diversity (few repeated entities)".into());
404 score += 0.2;
405 } else if stats.entity_diversity < 0.3 {
406 factors.push("Low entity diversity (model can memorize)".into());
407 score -= 0.1;
408 }
409
410 if stats.avg_entities_per_example < 1.0 {
412 factors.push("Few entities per example (<1 avg)".into());
413 score += 0.1;
414 }
415
416 let difficulty = match score {
417 s if s < 0.2 => EstimatedDifficulty::Easy,
418 s if s < 0.4 => EstimatedDifficulty::Medium,
419 s if s < 0.6 => EstimatedDifficulty::Hard,
420 _ => EstimatedDifficulty::VeryHard,
421 };
422
423 DifficultyEstimate {
424 difficulty,
425 score: score.clamp(0.0, 1.0),
426 factors,
427 }
428}
429
430#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
435pub enum EstimatedDifficulty {
436 Easy,
438 Medium,
440 Hard,
442 VeryHard,
444}
445
446#[derive(Debug, Clone, Serialize, Deserialize)]
448pub struct DifficultyEstimate {
449 pub difficulty: EstimatedDifficulty,
451 pub score: f64,
453 pub factors: Vec<String>,
455}
456
457#[cfg(feature = "discourse")]
467#[derive(Debug, Clone, Serialize, Deserialize)]
468pub struct DiscourseStats {
469 pub abstract_anaphor_count: usize,
471 pub event_trigger_count: usize,
473 pub shell_noun_count: usize,
475 pub avg_sentence_length: f64,
477 pub multi_sentence_examples: usize,
479 pub discourse_complexity: f64,
481}
482
483#[cfg(feature = "discourse")]
485pub fn compute_discourse_stats(examples: &[AnnotatedExample]) -> DiscourseStats {
486 use crate::discourse::{classify_shell_noun, DiscourseScope, EventExtractor};
487
488 if examples.is_empty() {
489 return DiscourseStats {
490 abstract_anaphor_count: 0,
491 event_trigger_count: 0,
492 shell_noun_count: 0,
493 avg_sentence_length: 0.0,
494 multi_sentence_examples: 0,
495 discourse_complexity: 0.0,
496 };
497 }
498
499 let extractor = EventExtractor::default();
500 let mut abstract_anaphor_count = 0;
501 let mut event_trigger_count = 0;
502 let mut shell_noun_count = 0;
503 let mut total_sentences = 0;
504 let mut multi_sentence_examples = 0;
505
506 let anaphor_patterns = [
508 "this ", "that ", "these ", "those ", "this.", "that.", "this,", "that,", " it ", " it.",
509 " it,",
510 ];
511
512 for example in examples {
513 let text_lower = example.text.to_lowercase();
514
515 for pattern in &anaphor_patterns {
517 abstract_anaphor_count += text_lower.matches(pattern).count();
518 }
519
520 let events = extractor.extract(&example.text);
522 event_trigger_count += events.len();
523
524 for word in example.text.split_whitespace() {
526 let word_clean = word.trim_matches(|c: char| !c.is_alphabetic());
527 if classify_shell_noun(word_clean).is_some() {
528 shell_noun_count += 1;
529 }
530 }
531
532 let scope = DiscourseScope::analyze(&example.text);
534 let num_sentences = scope.sentence_count().max(1);
535 total_sentences += num_sentences;
536
537 if num_sentences > 1 {
538 multi_sentence_examples += 1;
539 }
540 }
541
542 let avg_sentence_length = examples
543 .iter()
544 .map(|e| e.text.split_whitespace().count())
545 .sum::<usize>() as f64
546 / total_sentences.max(1) as f64;
547
548 let complexity = ((abstract_anaphor_count as f64 / examples.len() as f64).min(1.0) * 0.3
550 + (event_trigger_count as f64 / examples.len() as f64).min(1.0) * 0.3
551 + (shell_noun_count as f64 / examples.len() as f64 / 2.0).min(1.0) * 0.2
552 + (multi_sentence_examples as f64 / examples.len() as f64) * 0.2)
553 .clamp(0.0, 1.0);
554
555 DiscourseStats {
556 abstract_anaphor_count,
557 event_trigger_count,
558 shell_noun_count,
559 avg_sentence_length,
560 multi_sentence_examples,
561 discourse_complexity: complexity,
562 }
563}
564
565#[cfg(feature = "discourse")]
567#[derive(Debug, Clone, Serialize, Deserialize)]
568pub struct ExtendedDatasetComparison {
569 pub basic: DatasetComparison,
571 pub discourse_a: DiscourseStats,
573 pub discourse_b: DiscourseStats,
575 pub discourse_gap: f64,
577 pub discourse_recommendations: Vec<String>,
579}
580
581#[cfg(feature = "discourse")]
583pub fn compare_datasets_extended(
584 a: &[AnnotatedExample],
585 b: &[AnnotatedExample],
586) -> ExtendedDatasetComparison {
587 let basic = compare_datasets(a, b);
588 let discourse_a = compute_discourse_stats(a);
589 let discourse_b = compute_discourse_stats(b);
590
591 let discourse_gap = (discourse_a.discourse_complexity - discourse_b.discourse_complexity).abs();
592
593 let mut discourse_recommendations = Vec::new();
594
595 if discourse_gap > 0.3 {
596 discourse_recommendations.push(
597 "Significant discourse complexity difference - models may struggle with transfer"
598 .into(),
599 );
600 }
601
602 if discourse_a.event_trigger_count > 0 && discourse_b.event_trigger_count == 0 {
603 discourse_recommendations.push(
604 "Source has event triggers but target doesn't - event extraction may not transfer"
605 .into(),
606 );
607 }
608
609 if discourse_a.abstract_anaphor_count > discourse_b.abstract_anaphor_count * 2 {
610 discourse_recommendations
611 .push("Source has more abstract anaphora - coreference may not generalize".into());
612 }
613
614 if discourse_a.multi_sentence_examples > 0 && discourse_b.multi_sentence_examples == 0 {
615 discourse_recommendations
616 .push("Target is single-sentence only - cross-sentence phenomena won't appear".into());
617 }
618
619 if discourse_recommendations.is_empty() {
620 discourse_recommendations
621 .push("Discourse characteristics are similar between datasets".into());
622 }
623
624 ExtendedDatasetComparison {
625 basic,
626 discourse_a,
627 discourse_b,
628 discourse_gap,
629 discourse_recommendations,
630 }
631}
632
633#[cfg(test)]
638mod tests {
639 use super::*;
640
641 fn make_example(text: &str, entities: Vec<(&str, &str)>) -> AnnotatedExample {
642 use crate::eval::datasets::GoldEntity;
643 use crate::eval::synthetic::{Difficulty, Domain};
644 use anno::{EntityCategory, EntityType};
645
646 let mut gold_entities = Vec::new();
647
648 for (entity_text, entity_type_str) in entities {
649 if let Some(start) = text.find(entity_text) {
650 let entity_type = match entity_type_str {
651 "PER" => EntityType::Person,
652 "ORG" => EntityType::Organization,
653 "LOC" => EntityType::Location,
654 _ => EntityType::custom(entity_type_str, EntityCategory::Misc),
655 };
656 gold_entities.push(GoldEntity::new(entity_text, entity_type, start));
657 }
658 }
659
660 AnnotatedExample {
661 text: text.to_string(),
662 entities: gold_entities,
663 domain: Domain::News,
664 difficulty: Difficulty::Easy,
665 }
666 }
667
668 #[test]
669 fn test_compute_stats_empty() {
670 let stats = compute_stats(&[]);
671 assert_eq!(stats.num_examples, 0);
672 assert_eq!(stats.num_entities, 0);
673 }
674
675 #[test]
676 fn test_compute_stats_basic() {
677 let examples = vec![
678 make_example(
679 "John works at Google.",
680 vec![("John", "PER"), ("Google", "ORG")],
681 ),
682 make_example(
683 "Paris is in France.",
684 vec![("Paris", "LOC"), ("France", "LOC")],
685 ),
686 ];
687
688 let stats = compute_stats(&examples);
689
690 assert_eq!(stats.num_examples, 2);
691 assert_eq!(stats.num_entities, 4);
692 assert_eq!(stats.avg_entities_per_example, 2.0);
693 assert!(stats.type_distribution.contains_key("PER"));
694 assert!(stats.type_distribution.contains_key("ORG"));
695 assert!(stats.type_distribution.contains_key("LOC"));
696 }
697
698 #[test]
699 fn test_compare_identical_datasets() {
700 let examples = vec![make_example(
701 "John works at Google.",
702 vec![("John", "PER"), ("Google", "ORG")],
703 )];
704
705 let comparison = compare_datasets(&examples, &examples);
706
707 assert!(comparison.type_divergence < 0.01);
708 assert!((comparison.vocab_overlap - 1.0).abs() < 0.01);
709 assert!((comparison.entity_text_overlap - 1.0).abs() < 0.01);
710 }
711
712 #[test]
713 fn test_compare_different_datasets() {
714 let a = vec![make_example("John works at Google.", vec![("John", "PER")])];
715 let b = vec![make_example("Paris is beautiful.", vec![("Paris", "LOC")])];
716
717 let comparison = compare_datasets(&a, &b);
718
719 assert!(comparison.type_divergence > 0.5);
721 assert!(comparison.vocab_overlap < 0.5);
722 assert!((comparison.entity_text_overlap - 0.0).abs() < 0.01);
723 }
724
725 #[test]
726 fn test_jensen_shannon_identical() {
727 let mut p = HashMap::new();
728 p.insert("A".into(), 0.5);
729 p.insert("B".into(), 0.5);
730
731 let js = jensen_shannon_divergence(&p, &p);
732 assert!(js < 0.01);
733 }
734
735 #[test]
736 fn test_jensen_shannon_disjoint() {
737 let mut p = HashMap::new();
738 p.insert("A".into(), 1.0);
739
740 let mut q = HashMap::new();
741 q.insert("B".into(), 1.0);
742
743 let js = jensen_shannon_divergence(&p, &q);
744 assert!(js > 0.9);
745 }
746
747 #[test]
748 fn test_difficulty_estimation() {
749 let easy_examples = vec![
750 make_example("John works here.", vec![("John", "PER")]),
751 make_example("John went home.", vec![("John", "PER")]),
752 ];
753
754 let hard_examples = vec![make_example(
755 "International Business Machines Corporation announced.",
756 vec![("International Business Machines Corporation", "ORG")],
757 )];
758
759 let easy_stats = compute_stats(&easy_examples);
760 let hard_stats = compute_stats(&hard_examples);
761
762 let easy_diff = estimate_difficulty(&easy_stats);
763 let hard_diff = estimate_difficulty(&hard_stats);
764
765 assert!(hard_diff.score >= easy_diff.score);
766 }
767
768 #[test]
773 #[cfg(feature = "discourse")]
774 fn test_discourse_stats_empty() {
775 let stats = compute_discourse_stats(&[]);
776 assert_eq!(stats.abstract_anaphor_count, 0);
777 assert_eq!(stats.event_trigger_count, 0);
778 assert_eq!(stats.shell_noun_count, 0);
779 }
780
781 #[test]
782 #[cfg(feature = "discourse")]
783 fn test_discourse_stats_with_anaphors() {
784 let examples = vec![
785 make_example(
786 "Russia invaded Ukraine. This caused inflation.",
787 vec![("Russia", "LOC")],
788 ),
789 make_example(
790 "The merger was announced. That surprised investors.",
791 vec![],
792 ),
793 ];
794
795 let stats = compute_discourse_stats(&examples);
796
797 assert!(
799 stats.abstract_anaphor_count >= 2,
800 "Should detect abstract anaphors"
801 );
802 assert!(
804 stats.event_trigger_count >= 2,
805 "Should detect event triggers"
806 );
807 assert_eq!(stats.multi_sentence_examples, 2);
809 }
810
811 #[test]
812 #[cfg(feature = "discourse")]
813 fn test_discourse_stats_with_shell_nouns() {
814 let examples = vec![
815 make_example("This problem is serious.", vec![]),
816 make_example("The fact is clear.", vec![]),
817 make_example("The situation is complex.", vec![]),
818 ];
819
820 let stats = compute_discourse_stats(&examples);
821
822 assert!(stats.shell_noun_count >= 3, "Should detect shell nouns");
824 }
825
826 #[test]
827 #[cfg(feature = "discourse")]
828 fn test_extended_comparison() {
829 let simple = vec![make_example(
830 "John works at Google.",
831 vec![("John", "PER"), ("Google", "ORG")],
832 )];
833
834 let complex = vec![
835 make_example(
836 "Russia invaded Ukraine in 2022. This caused a global energy crisis. The situation remains tense.",
837 vec![("Russia", "LOC"), ("Ukraine", "LOC")]
838 ),
839 ];
840
841 let comparison = compare_datasets_extended(&simple, &complex);
842
843 assert!(
845 comparison.discourse_b.discourse_complexity
846 > comparison.discourse_a.discourse_complexity,
847 "Complex dataset should have higher discourse complexity"
848 );
849
850 assert!(comparison.discourse_gap > 0.0);
852 }
853
854 #[test]
855 #[cfg(feature = "discourse")]
856 fn test_discourse_complexity_bounds() {
857 let examples = vec![
858 make_example(
859 "This problem happened. That event occurred. This situation developed. The fact emerged.",
860 vec![]
861 ),
862 ];
863
864 let stats = compute_discourse_stats(&examples);
865
866 assert!(stats.discourse_complexity >= 0.0);
868 assert!(stats.discourse_complexity <= 1.0);
869 }
870}