1use crate::{EntityType, Model};
30use serde::{Deserialize, Serialize};
31use std::collections::HashMap;
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
39pub enum LengthBucket {
40 VeryShort,
42 Short,
44 Medium,
46 Long,
48 VeryLong,
50}
51
52impl LengthBucket {
53 pub fn from_char_length(len: usize) -> Self {
55 match len {
56 0..=5 => LengthBucket::VeryShort,
57 6..=15 => LengthBucket::Short,
58 16..=30 => LengthBucket::Medium,
59 31..=50 => LengthBucket::Long,
60 _ => LengthBucket::VeryLong,
61 }
62 }
63
64 pub fn from_word_count(words: usize) -> Self {
66 match words {
67 0..=1 => LengthBucket::VeryShort,
68 2 => LengthBucket::Short,
69 3..=4 => LengthBucket::Medium,
70 5..=7 => LengthBucket::Long,
71 _ => LengthBucket::VeryLong,
72 }
73 }
74}
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
78pub enum WordCountBucket {
79 SingleWord,
81 TwoWords,
83 ThreeWords,
85 FourPlusWords,
87}
88
89impl WordCountBucket {
90 pub fn from_count(count: usize) -> Self {
92 match count {
93 0..=1 => WordCountBucket::SingleWord,
94 2 => WordCountBucket::TwoWords,
95 3 => WordCountBucket::ThreeWords,
96 _ => WordCountBucket::FourPlusWords,
97 }
98 }
99}
100
101#[derive(Debug, Clone, Serialize, Deserialize)]
107pub struct LengthTestExample {
108 pub entity_text: String,
110 pub sentence: String,
112 pub entity_type: EntityType,
114 pub char_length: usize,
116 pub word_count: usize,
118 pub char_bucket: LengthBucket,
120 pub word_bucket: WordCountBucket,
122}
123
124impl LengthTestExample {
125 pub fn new(entity: &str, entity_type: EntityType) -> Self {
127 let sentence = format!("The entity {} was mentioned.", entity);
128 let char_length = entity.chars().count();
129 let word_count = entity.split_whitespace().count();
130
131 Self {
132 entity_text: entity.to_string(),
133 sentence,
134 entity_type,
135 char_length,
136 word_count,
137 char_bucket: LengthBucket::from_char_length(char_length),
138 word_bucket: WordCountBucket::from_count(word_count),
139 }
140 }
141
142 pub fn with_sentence(entity: &str, sentence: &str, entity_type: EntityType) -> Self {
144 let char_length = entity.chars().count();
145 let word_count = entity.split_whitespace().count();
146
147 Self {
148 entity_text: entity.to_string(),
149 sentence: sentence.to_string(),
150 entity_type,
151 char_length,
152 word_count,
153 char_bucket: LengthBucket::from_char_length(char_length),
154 word_bucket: WordCountBucket::from_count(word_count),
155 }
156 }
157}
158
159#[derive(Debug, Clone, Serialize, Deserialize)]
165pub struct LengthBiasResults {
166 pub overall_recognition_rate: f64,
168 pub by_char_bucket: HashMap<String, f64>,
170 pub by_word_bucket: HashMap<String, f64>,
172 pub by_entity_type: HashMap<String, f64>,
174 pub char_length_parity_gap: f64,
176 pub word_count_parity_gap: f64,
178 pub short_vs_long_gap: f64,
180 pub avg_recognized_char_length: f64,
182 pub avg_missed_char_length: f64,
184 pub total_tested: usize,
186}
187
188#[derive(Debug, Clone, Default)]
194pub struct EntityLengthEvaluator {
195 pub detailed: bool,
197}
198
199impl EntityLengthEvaluator {
200 pub fn new(detailed: bool) -> Self {
202 Self { detailed }
203 }
204
205 pub fn evaluate(&self, model: &dyn Model, examples: &[LengthTestExample]) -> LengthBiasResults {
207 let mut by_char_bucket: HashMap<String, (usize, usize)> = HashMap::new();
208 let mut by_word_bucket: HashMap<String, (usize, usize)> = HashMap::new();
209 let mut by_entity_type: HashMap<String, (usize, usize)> = HashMap::new();
210 let mut total_recognized = 0;
211 let mut recognized_char_lengths: Vec<usize> = Vec::new();
212 let mut missed_char_lengths: Vec<usize> = Vec::new();
213
214 for example in examples {
215 let entities = model
217 .extract_entities(&example.sentence, None)
218 .unwrap_or_default();
219
220 let recognized = entities.iter().any(|e| {
222 e.entity_type == example.entity_type
223 && example
224 .sentence
225 .get(
226 anno::offset::TextSpan::from_chars(
227 &example.sentence,
228 e.start(),
229 e.end(),
230 )
231 .byte_range(),
232 )
233 .map(|s| s.contains(&example.entity_text))
234 .unwrap_or(false)
235 });
236
237 if recognized {
238 total_recognized += 1;
239 recognized_char_lengths.push(example.char_length);
240 } else {
241 missed_char_lengths.push(example.char_length);
242 }
243
244 let char_key = format!("{:?}", example.char_bucket);
246 let char_entry = by_char_bucket.entry(char_key).or_insert((0, 0));
247 char_entry.1 += 1;
248 if recognized {
249 char_entry.0 += 1;
250 }
251
252 let word_key = format!("{:?}", example.word_bucket);
254 let word_entry = by_word_bucket.entry(word_key).or_insert((0, 0));
255 word_entry.1 += 1;
256 if recognized {
257 word_entry.0 += 1;
258 }
259
260 let type_key = format!("{:?}", example.entity_type);
262 let type_entry = by_entity_type.entry(type_key).or_insert((0, 0));
263 type_entry.1 += 1;
264 if recognized {
265 type_entry.0 += 1;
266 }
267 }
268
269 let to_rate = |counts: &HashMap<String, (usize, usize)>| -> HashMap<String, f64> {
271 counts
272 .iter()
273 .map(|(k, (correct, total))| {
274 let rate = if *total > 0 {
275 *correct as f64 / *total as f64
276 } else {
277 0.0
278 };
279 (k.clone(), rate)
280 })
281 .collect()
282 };
283
284 let char_rates = to_rate(&by_char_bucket);
285 let word_rates = to_rate(&by_word_bucket);
286 let type_rates = to_rate(&by_entity_type);
287
288 let char_length_parity_gap = compute_max_gap(&char_rates);
290 let word_count_parity_gap = compute_max_gap(&word_rates);
291
292 let short_rate = word_rates
294 .iter()
295 .filter(|(k, _)| k.contains("SingleWord") || k.contains("TwoWords"))
296 .map(|(_, v)| *v)
297 .sum::<f64>()
298 / 2.0;
299 let long_rate = word_rates
300 .get("FourPlusWords")
301 .copied()
302 .unwrap_or(short_rate);
303 let short_vs_long_gap = (short_rate - long_rate).abs();
304
305 let avg_recognized = if recognized_char_lengths.is_empty() {
307 0.0
308 } else {
309 recognized_char_lengths.iter().sum::<usize>() as f64
310 / recognized_char_lengths.len() as f64
311 };
312 let avg_missed = if missed_char_lengths.is_empty() {
313 0.0
314 } else {
315 missed_char_lengths.iter().sum::<usize>() as f64 / missed_char_lengths.len() as f64
316 };
317
318 LengthBiasResults {
319 overall_recognition_rate: if examples.is_empty() {
320 0.0
321 } else {
322 total_recognized as f64 / examples.len() as f64
323 },
324 by_char_bucket: char_rates,
325 by_word_bucket: word_rates,
326 by_entity_type: type_rates,
327 char_length_parity_gap,
328 word_count_parity_gap,
329 short_vs_long_gap,
330 avg_recognized_char_length: avg_recognized,
331 avg_missed_char_length: avg_missed,
332 total_tested: examples.len(),
333 }
334 }
335}
336
337fn compute_max_gap(rates: &HashMap<String, f64>) -> f64 {
339 if rates.len() < 2 {
340 return 0.0;
341 }
342
343 let values: Vec<f64> = rates.values().copied().collect();
344 let min = values.iter().copied().fold(f64::INFINITY, f64::min);
345 let max = values.iter().copied().fold(f64::NEG_INFINITY, f64::max);
346
347 max - min
348}
349
350pub fn create_length_varied_dataset() -> Vec<LengthTestExample> {
356 vec![
357 LengthTestExample::with_sentence(
360 "JFK",
361 "JFK gave a famous speech in Berlin.",
362 EntityType::Person,
363 ),
364 LengthTestExample::with_sentence(
365 "FDR",
366 "FDR led the country through World War II.",
367 EntityType::Person,
368 ),
369 LengthTestExample::with_sentence(
371 "John Smith",
372 "John Smith attended the meeting.",
373 EntityType::Person,
374 ),
375 LengthTestExample::with_sentence(
376 "Mary Johnson",
377 "Mary Johnson won the award.",
378 EntityType::Person,
379 ),
380 LengthTestExample::with_sentence(
382 "Dr. Martin Luther King",
383 "Dr. Martin Luther King delivered a powerful speech.",
384 EntityType::Person,
385 ),
386 LengthTestExample::with_sentence(
387 "William Jefferson Clinton",
388 "William Jefferson Clinton served as president.",
389 EntityType::Person,
390 ),
391 LengthTestExample::with_sentence(
393 "His Royal Highness Prince William",
394 "His Royal Highness Prince William visited the hospital.",
395 EntityType::Person,
396 ),
397 LengthTestExample::with_sentence(
400 "IBM",
401 "IBM announced new products.",
402 EntityType::Organization,
403 ),
404 LengthTestExample::with_sentence(
405 "MIT",
406 "MIT published research findings.",
407 EntityType::Organization,
408 ),
409 LengthTestExample::with_sentence(
410 "NASA",
411 "NASA launched a new satellite.",
412 EntityType::Organization,
413 ),
414 LengthTestExample::with_sentence(
416 "Google Inc",
417 "Google Inc acquired the startup.",
418 EntityType::Organization,
419 ),
420 LengthTestExample::with_sentence(
421 "Apple Computer",
422 "Apple Computer revolutionized mobile phones.",
423 EntityType::Organization,
424 ),
425 LengthTestExample::with_sentence(
427 "University of California",
428 "University of California released the study.",
429 EntityType::Organization,
430 ),
431 LengthTestExample::with_sentence(
432 "World Health Organization",
433 "World Health Organization issued guidelines.",
434 EntityType::Organization,
435 ),
436 LengthTestExample::with_sentence(
438 "Massachusetts Institute of Technology",
439 "Massachusetts Institute of Technology won the competition.",
440 EntityType::Organization,
441 ),
442 LengthTestExample::with_sentence(
443 "International Business Machines Corporation",
444 "International Business Machines Corporation reported earnings.",
445 EntityType::Organization,
446 ),
447 LengthTestExample::with_sentence(
449 "United States Department of Health and Human Services",
450 "United States Department of Health and Human Services announced the policy.",
451 EntityType::Organization,
452 ),
453 LengthTestExample::with_sentence(
454 "European Organization for Nuclear Research",
455 "European Organization for Nuclear Research discovered the particle.",
456 EntityType::Organization,
457 ),
458 LengthTestExample::with_sentence(
461 "NYC",
462 "NYC is known for its skyline.",
463 EntityType::Location,
464 ),
465 LengthTestExample::with_sentence("LA", "LA has beautiful weather.", EntityType::Location),
466 LengthTestExample::with_sentence(
468 "New York",
469 "New York is a bustling city.",
470 EntityType::Location,
471 ),
472 LengthTestExample::with_sentence(
473 "London",
474 "London has many museums.",
475 EntityType::Location,
476 ),
477 LengthTestExample::with_sentence(
479 "San Francisco Bay Area",
480 "San Francisco Bay Area is a tech hub.",
481 EntityType::Location,
482 ),
483 LengthTestExample::with_sentence(
484 "United Arab Emirates",
485 "United Arab Emirates hosted the conference.",
486 EntityType::Location,
487 ),
488 LengthTestExample::with_sentence(
490 "Democratic Republic of the Congo",
491 "Democratic Republic of the Congo has vast resources.",
492 EntityType::Location,
493 ),
494 LengthTestExample::with_sentence(
495 "Saint Vincent and the Grenadines",
496 "Saint Vincent and the Grenadines is in the Caribbean.",
497 EntityType::Location,
498 ),
499 LengthTestExample::with_sentence(
501 "Llanfairpwllgwyngyllgogerychwyrndrobwllllantysiliogogogoch",
502 "Llanfairpwllgwyngyllgogerychwyrndrobwllllantysiliogogogoch is a town in Wales.",
503 EntityType::Location,
504 ),
505 LengthTestExample::with_sentence(
507 "Dr. Jane Smith",
508 "Dr. Jane Smith diagnosed the patient.",
509 EntityType::Person,
510 ),
511 LengthTestExample::with_sentence(
512 "Prof. John Doe",
513 "Prof. John Doe published the research.",
514 EntityType::Person,
515 ),
516 LengthTestExample::with_sentence(
517 "Mary-Jane Watson",
518 "Mary-Jane Watson attended the event.",
519 EntityType::Person,
520 ),
521 LengthTestExample::with_sentence(
522 "José María García",
523 "José María García spoke at the conference.",
524 EntityType::Person,
525 ),
526 LengthTestExample::with_sentence(
527 "Robert Williams Jr.",
528 "Robert Williams Jr. inherited the business.",
529 EntityType::Person,
530 ),
531 LengthTestExample::with_sentence(
532 "Elizabeth Taylor III",
533 "Elizabeth Taylor III was the third generation.",
534 EntityType::Person,
535 ),
536 LengthTestExample::with_sentence(
537 "Jean-Pierre Dubois",
538 "Jean-Pierre Dubois visited from France.",
539 EntityType::Person,
540 ),
541 LengthTestExample::with_sentence(
542 "Mary Ann Johnson",
543 "Mary Ann Johnson was the keynote speaker.",
544 EntityType::Person,
545 ),
546 LengthTestExample::with_sentence(
548 "AT&T",
549 "AT&T announced the merger.",
550 EntityType::Organization,
551 ),
552 LengthTestExample::with_sentence(
553 "3M",
554 "3M developed new materials.",
555 EntityType::Organization,
556 ),
557 LengthTestExample::with_sentence(
558 "JPMorgan Chase",
559 "JPMorgan Chase reported earnings.",
560 EntityType::Organization,
561 ),
562 LengthTestExample::with_sentence(
563 "Bank of America",
564 "Bank of America opened new branches.",
565 EntityType::Organization,
566 ),
567 LengthTestExample::with_sentence(
568 "General Electric Company",
569 "General Electric Company restructured operations.",
570 EntityType::Organization,
571 ),
572 LengthTestExample::with_sentence(
573 "The Coca-Cola Company",
574 "The Coca-Cola Company launched a new product.",
575 EntityType::Organization,
576 ),
577 LengthTestExample::with_sentence(
578 "Procter & Gamble",
579 "Procter & Gamble acquired the brand.",
580 EntityType::Organization,
581 ),
582 LengthTestExample::with_sentence(
583 "Johnson & Johnson",
584 "Johnson & Johnson developed the vaccine.",
585 EntityType::Organization,
586 ),
587 LengthTestExample::with_sentence("UK", "UK announced new policies.", EntityType::Location),
589 LengthTestExample::with_sentence("USA", "USA hosted the summit.", EntityType::Location),
590 LengthTestExample::with_sentence(
591 "Los Angeles",
592 "Los Angeles hosted the Olympics.",
593 EntityType::Location,
594 ),
595 LengthTestExample::with_sentence(
596 "San Diego",
597 "San Diego is a coastal city.",
598 EntityType::Location,
599 ),
600 LengthTestExample::with_sentence(
601 "New York City",
602 "New York City never sleeps.",
603 EntityType::Location,
604 ),
605 LengthTestExample::with_sentence(
606 "Greater London Area",
607 "Greater London Area has millions of residents.",
608 EntityType::Location,
609 ),
610 LengthTestExample::with_sentence(
611 "Republic of South Africa",
612 "Republic of South Africa celebrated independence.",
613 EntityType::Location,
614 ),
615 LengthTestExample::with_sentence(
616 "Federative Republic of Brazil",
617 "Federative Republic of Brazil hosted the World Cup.",
618 EntityType::Location,
619 ),
620 LengthTestExample::with_sentence(
622 "2024",
623 "The year 2024 was significant.",
624 EntityType::Date,
625 ),
626 LengthTestExample::with_sentence(
627 "January 15, 2024",
628 "The meeting was scheduled for January 15, 2024.",
629 EntityType::Date,
630 ),
631 LengthTestExample::with_sentence(
632 "Q1 2024",
633 "Q1 2024 showed strong growth.",
634 EntityType::Date,
635 ),
636 LengthTestExample::with_sentence("$5", "The item cost $5.", EntityType::Money),
638 LengthTestExample::with_sentence(
639 "$1,234.56",
640 "The total was $1,234.56.",
641 EntityType::Money,
642 ),
643 LengthTestExample::with_sentence(
644 "€1,000,000",
645 "The investment was €1,000,000.",
646 EntityType::Money,
647 ),
648 ]
649}
650
651#[cfg(test)]
656mod tests {
657 use super::*;
658
659 #[test]
660 fn test_length_bucket_classification() {
661 assert_eq!(LengthBucket::from_char_length(3), LengthBucket::VeryShort);
662 assert_eq!(LengthBucket::from_char_length(10), LengthBucket::Short);
663 assert_eq!(LengthBucket::from_char_length(25), LengthBucket::Medium);
664 assert_eq!(LengthBucket::from_char_length(40), LengthBucket::Long);
665 assert_eq!(LengthBucket::from_char_length(60), LengthBucket::VeryLong);
666 }
667
668 #[test]
669 fn test_word_count_bucket() {
670 assert_eq!(WordCountBucket::from_count(1), WordCountBucket::SingleWord);
671 assert_eq!(WordCountBucket::from_count(2), WordCountBucket::TwoWords);
672 assert_eq!(WordCountBucket::from_count(3), WordCountBucket::ThreeWords);
673 assert_eq!(
674 WordCountBucket::from_count(5),
675 WordCountBucket::FourPlusWords
676 );
677 }
678
679 #[test]
680 fn test_create_length_dataset() {
681 let examples = create_length_varied_dataset();
682
683 let char_buckets: std::collections::HashSet<_> = examples
685 .iter()
686 .map(|e| format!("{:?}", e.char_bucket))
687 .collect();
688
689 assert!(
690 char_buckets.contains("VeryShort"),
691 "Should have very short entities"
692 );
693 assert!(char_buckets.contains("Short"), "Should have short entities");
694 assert!(
695 char_buckets.contains("Medium"),
696 "Should have medium entities"
697 );
698 assert!(char_buckets.contains("Long"), "Should have long entities");
699 }
700
701 #[test]
702 fn test_entity_type_coverage() {
703 let examples = create_length_varied_dataset();
704
705 let types: std::collections::HashSet<_> = examples
706 .iter()
707 .map(|e| format!("{:?}", e.entity_type))
708 .collect();
709
710 assert!(types.contains("Person"), "Should have PERSON entities");
711 assert!(
712 types.contains("Organization"),
713 "Should have ORGANIZATION entities"
714 );
715 assert!(types.contains("Location"), "Should have LOCATION entities");
716 }
717
718 #[test]
719 fn test_example_construction() {
720 let example = LengthTestExample::new("John Smith", EntityType::Person);
721
722 assert_eq!(example.entity_text, "John Smith");
723 assert_eq!(example.char_length, 10);
724 assert_eq!(example.word_count, 2);
725 assert_eq!(example.char_bucket, LengthBucket::Short);
726 assert_eq!(example.word_bucket, WordCountBucket::TwoWords);
727 }
728}