1use crate::{EntityType, Model};
30use serde::{Deserialize, Serialize};
31use std::collections::HashMap;
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, PartialOrd, Ord)]
39pub enum Decade {
40 Pre1900,
42 D1900s,
44 D1910s,
46 D1920s,
48 D1930s,
50 D1940s,
52 D1950s,
54 D1960s,
56 D1970s,
58 D1980s,
60 D1990s,
62 D2000s,
64 D2010s,
66 D2020s,
68}
69
70impl Decade {
71 pub fn is_historical(&self) -> bool {
73 matches!(
74 self,
75 Decade::Pre1900
76 | Decade::D1900s
77 | Decade::D1910s
78 | Decade::D1920s
79 | Decade::D1930s
80 | Decade::D1940s
81 )
82 }
83
84 pub fn is_modern(&self) -> bool {
86 matches!(self, Decade::D2000s | Decade::D2010s | Decade::D2020s)
87 }
88
89 pub fn midpoint_year(&self) -> u16 {
91 match self {
92 Decade::Pre1900 => 1890,
93 Decade::D1900s => 1905,
94 Decade::D1910s => 1915,
95 Decade::D1920s => 1925,
96 Decade::D1930s => 1935,
97 Decade::D1940s => 1945,
98 Decade::D1950s => 1955,
99 Decade::D1960s => 1965,
100 Decade::D1970s => 1975,
101 Decade::D1980s => 1985,
102 Decade::D1990s => 1995,
103 Decade::D2000s => 2005,
104 Decade::D2010s => 2015,
105 Decade::D2020s => 2022,
106 }
107 }
108}
109
110#[derive(Debug, Clone, Serialize, Deserialize)]
116pub struct TemporalNameExample {
117 pub first_name: String,
119 pub last_name: String,
121 pub full_name: String,
123 pub peak_decade: Decade,
125 pub gender: TemporalGender,
127 pub is_classic: bool,
129}
130
131#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
133pub enum TemporalGender {
134 Masculine,
136 Feminine,
138 Neutral,
140}
141
142impl TemporalNameExample {
143 pub fn new(
145 first_name: &str,
146 last_name: &str,
147 peak_decade: Decade,
148 gender: TemporalGender,
149 is_classic: bool,
150 ) -> Self {
151 Self {
152 first_name: first_name.to_string(),
153 last_name: last_name.to_string(),
154 full_name: format!("{} {}", first_name, last_name),
155 peak_decade,
156 gender,
157 is_classic,
158 }
159 }
160}
161
162#[derive(Debug, Clone, Serialize, Deserialize)]
168pub struct TemporalBiasResults {
169 pub overall_recognition_rate: f64,
171 pub by_decade: HashMap<String, f64>,
173 pub historical_rate: f64,
175 pub modern_rate: f64,
177 pub historical_modern_gap: f64,
179 pub temporal_parity_gap: f64,
181 pub by_gender: HashMap<String, f64>,
183 pub classic_rate: f64,
185 pub trendy_rate: f64,
187 pub total_tested: usize,
189}
190
191#[derive(Debug, Clone, Default)]
197pub struct TemporalBiasEvaluator {
198 pub detailed: bool,
200}
201
202impl TemporalBiasEvaluator {
203 pub fn new(detailed: bool) -> Self {
205 Self { detailed }
206 }
207
208 pub fn evaluate(
210 &self,
211 model: &dyn Model,
212 names: &[TemporalNameExample],
213 ) -> TemporalBiasResults {
214 let mut by_decade: HashMap<String, (usize, usize)> = HashMap::new();
215 let mut by_gender: HashMap<String, (usize, usize)> = HashMap::new();
216 let mut historical_count = (0usize, 0usize);
217 let mut modern_count = (0usize, 0usize);
218 let mut classic_count = (0usize, 0usize);
219 let mut trendy_count = (0usize, 0usize);
220 let mut total_recognized = 0;
221
222 for name in names {
223 let text = create_realistic_temporal_sentence(&name.full_name);
225
226 let entities = model.extract_entities(&text, None).unwrap_or_default();
228
229 let recognized = entities.iter().any(|e| {
231 e.entity_type == EntityType::Person
232 && e.extract_text(&text).contains(&name.first_name)
233 });
234
235 if recognized {
236 total_recognized += 1;
237 }
238
239 let decade_key = format!("{:?}", name.peak_decade);
241 let decade_entry = by_decade.entry(decade_key).or_insert((0, 0));
242 decade_entry.1 += 1;
243 if recognized {
244 decade_entry.0 += 1;
245 }
246
247 if name.peak_decade.is_historical() {
249 historical_count.1 += 1;
250 if recognized {
251 historical_count.0 += 1;
252 }
253 }
254 if name.peak_decade.is_modern() {
255 modern_count.1 += 1;
256 if recognized {
257 modern_count.0 += 1;
258 }
259 }
260
261 let gender_key = format!("{:?}", name.gender);
263 let gender_entry = by_gender.entry(gender_key).or_insert((0, 0));
264 gender_entry.1 += 1;
265 if recognized {
266 gender_entry.0 += 1;
267 }
268
269 if name.is_classic {
271 classic_count.1 += 1;
272 if recognized {
273 classic_count.0 += 1;
274 }
275 } else {
276 trendy_count.1 += 1;
277 if recognized {
278 trendy_count.0 += 1;
279 }
280 }
281 }
282
283 let to_rate = |counts: &HashMap<String, (usize, usize)>| -> HashMap<String, f64> {
285 counts
286 .iter()
287 .map(|(k, (correct, total))| {
288 let rate = if *total > 0 {
289 *correct as f64 / *total as f64
290 } else {
291 0.0
292 };
293 (k.clone(), rate)
294 })
295 .collect()
296 };
297
298 let count_to_rate = |c: (usize, usize)| -> f64 {
299 if c.1 > 0 {
300 c.0 as f64 / c.1 as f64
301 } else {
302 0.0
303 }
304 };
305
306 let decade_rates = to_rate(&by_decade);
307 let gender_rates = to_rate(&by_gender);
308 let historical_rate = count_to_rate(historical_count);
309 let modern_rate = count_to_rate(modern_count);
310 let classic_rate = count_to_rate(classic_count);
311 let trendy_rate = count_to_rate(trendy_count);
312
313 let temporal_parity_gap = compute_max_gap(&decade_rates);
315 let historical_modern_gap = (historical_rate - modern_rate).abs();
316
317 TemporalBiasResults {
318 overall_recognition_rate: if names.is_empty() {
319 0.0
320 } else {
321 total_recognized as f64 / names.len() as f64
322 },
323 by_decade: decade_rates,
324 historical_rate,
325 modern_rate,
326 historical_modern_gap,
327 temporal_parity_gap,
328 by_gender: gender_rates,
329 classic_rate,
330 trendy_rate,
331 total_tested: names.len(),
332 }
333 }
334}
335
336fn compute_max_gap(rates: &HashMap<String, f64>) -> f64 {
338 if rates.len() < 2 {
339 return 0.0;
340 }
341
342 let values: Vec<f64> = rates.values().copied().collect();
343 let min = values.iter().copied().fold(f64::INFINITY, f64::min);
344 let max = values.iter().copied().fold(f64::NEG_INFINITY, f64::max);
345
346 max - min
347}
348
349fn create_realistic_temporal_sentence(name: &str) -> String {
355 use std::collections::hash_map::DefaultHasher;
356 use std::hash::{Hash, Hasher};
357 let mut hasher = DefaultHasher::new();
358 name.hash(&mut hasher);
359 let hash = hasher.finish();
360
361 let templates = [
362 format!("{} was featured in the historical archives.", name),
363 format!("The biography of {} was published last year.", name),
364 format!("{} made significant contributions to the field.", name),
365 format!("Records show that {} attended the event in 1950.", name),
366 format!("{} was recognized for lifetime achievements.", name),
367 format!("The family of {} established a scholarship fund.", name),
368 format!("{} served as president of the organization.", name),
369 format!("Historical documents mention {} in several contexts.", name),
370 format!("{} was known for innovative research methods.", name),
371 format!(
372 "The legacy of {} continues to inspire new generations.",
373 name
374 ),
375 ];
376
377 templates[hash as usize % templates.len()].clone()
378}
379
380pub fn create_temporal_name_dataset() -> Vec<TemporalNameExample> {
389 let mut names = Vec::new();
390
391 let last_names = ["Smith", "Johnson", "Williams", "Brown", "Jones"];
393
394 let pre1900 = [
396 ("Gertrude", TemporalGender::Feminine),
397 ("Clarence", TemporalGender::Masculine),
398 ("Mildred", TemporalGender::Feminine),
399 ("Herbert", TemporalGender::Masculine),
400 ("Bertha", TemporalGender::Feminine),
401 ("Agnes", TemporalGender::Feminine),
402 ("Albert", TemporalGender::Masculine),
403 ("Florence", TemporalGender::Feminine),
404 ("Walter", TemporalGender::Masculine),
405 ("Edith", TemporalGender::Feminine),
406 ];
407
408 let d1900s = [
410 ("Ethel", TemporalGender::Feminine),
411 ("Harold", TemporalGender::Masculine),
412 ("Pearl", TemporalGender::Feminine),
413 ("Clarence", TemporalGender::Masculine),
414 ("Minnie", TemporalGender::Feminine),
415 ("Alice", TemporalGender::Feminine),
416 ("Raymond", TemporalGender::Masculine),
417 ("Ruth", TemporalGender::Feminine),
418 ("Frank", TemporalGender::Masculine),
419 ("Helen", TemporalGender::Feminine),
420 ];
421
422 let d1910s = [
424 ("Dorothy", TemporalGender::Feminine),
425 ("Earl", TemporalGender::Masculine),
426 ("Gladys", TemporalGender::Feminine),
427 ("Howard", TemporalGender::Masculine),
428 ("Thelma", TemporalGender::Feminine),
429 ];
430
431 let d1920s = [
433 ("Betty", TemporalGender::Feminine),
434 ("Donald", TemporalGender::Masculine),
435 ("Doris", TemporalGender::Feminine),
436 ("Raymond", TemporalGender::Masculine),
437 ("Shirley", TemporalGender::Feminine),
438 ];
439
440 let d1930s = [
442 ("Barbara", TemporalGender::Feminine),
443 ("Robert", TemporalGender::Masculine),
444 ("Patricia", TemporalGender::Feminine),
445 ("Richard", TemporalGender::Masculine),
446 ("Carol", TemporalGender::Feminine),
447 ];
448
449 let d1940s = [
451 ("Linda", TemporalGender::Feminine),
452 ("Gary", TemporalGender::Masculine),
453 ("Sandra", TemporalGender::Feminine),
454 ("Larry", TemporalGender::Masculine),
455 ("Sharon", TemporalGender::Feminine),
456 ];
457
458 let d1950s = [
460 ("Deborah", TemporalGender::Feminine),
461 ("Dennis", TemporalGender::Masculine),
462 ("Debra", TemporalGender::Feminine),
463 ("Timothy", TemporalGender::Masculine),
464 ("Pamela", TemporalGender::Feminine),
465 ];
466
467 let d1960s = [
469 ("Lisa", TemporalGender::Feminine),
470 ("Mark", TemporalGender::Masculine),
471 ("Kimberly", TemporalGender::Feminine),
472 ("Kevin", TemporalGender::Masculine),
473 ("Michelle", TemporalGender::Feminine),
474 ];
475
476 let d1970s = [
478 ("Jennifer", TemporalGender::Feminine),
479 ("Jason", TemporalGender::Masculine),
480 ("Amy", TemporalGender::Feminine),
481 ("Brian", TemporalGender::Masculine),
482 ("Heather", TemporalGender::Feminine),
483 ];
484
485 let d1980s = [
487 ("Jessica", TemporalGender::Feminine),
488 ("Michael", TemporalGender::Masculine),
489 ("Amanda", TemporalGender::Feminine),
490 ("Christopher", TemporalGender::Masculine),
491 ("Ashley", TemporalGender::Feminine),
492 ];
493
494 let d1990s = [
496 ("Brittany", TemporalGender::Feminine),
497 ("Tyler", TemporalGender::Masculine),
498 ("Taylor", TemporalGender::Neutral),
499 ("Brandon", TemporalGender::Masculine),
500 ("Megan", TemporalGender::Feminine),
501 ];
502
503 let d2000s = [
505 ("Madison", TemporalGender::Feminine),
506 ("Aiden", TemporalGender::Masculine),
507 ("Emma", TemporalGender::Feminine),
508 ("Ethan", TemporalGender::Masculine),
509 ("Chloe", TemporalGender::Feminine),
510 ];
511
512 let d2010s = [
514 ("Sophia", TemporalGender::Feminine),
515 ("Liam", TemporalGender::Masculine),
516 ("Olivia", TemporalGender::Feminine),
517 ("Noah", TemporalGender::Masculine),
518 ("Ava", TemporalGender::Feminine),
519 ];
520
521 let d2020s = [
523 ("Luna", TemporalGender::Feminine),
524 ("Ezra", TemporalGender::Masculine),
525 ("Charlotte", TemporalGender::Feminine),
526 ("Oliver", TemporalGender::Masculine),
527 ("Amelia", TemporalGender::Feminine),
528 ("Mia", TemporalGender::Feminine),
529 ("Liam", TemporalGender::Masculine),
530 ("Harper", TemporalGender::Neutral),
531 ("Mason", TemporalGender::Masculine),
532 ("Evelyn", TemporalGender::Feminine),
533 ];
534
535 let classics = [
537 ("James", TemporalGender::Masculine, true),
538 ("Elizabeth", TemporalGender::Feminine, true),
539 ("William", TemporalGender::Masculine, true),
540 ("Mary", TemporalGender::Feminine, true),
541 ("John", TemporalGender::Masculine, true),
542 ("Sarah", TemporalGender::Feminine, true),
543 ("Robert", TemporalGender::Masculine, true),
544 ("Anna", TemporalGender::Feminine, true),
545 ("Michael", TemporalGender::Masculine, true),
546 ("Emily", TemporalGender::Feminine, true),
547 ];
548
549 let add_decade = |names: &mut Vec<TemporalNameExample>,
551 decade_names: &[(&str, TemporalGender)],
552 decade: Decade,
553 last_names: &[&str]| {
554 for (i, (first, gender)) in decade_names.iter().enumerate() {
555 let last = last_names[i % last_names.len()];
556 names.push(TemporalNameExample::new(
557 first, last, decade, *gender, false,
558 ));
559 }
560 };
561
562 add_decade(&mut names, &pre1900, Decade::Pre1900, &last_names);
563 add_decade(&mut names, &d1900s, Decade::D1900s, &last_names);
564 add_decade(&mut names, &d1910s, Decade::D1910s, &last_names);
565 add_decade(&mut names, &d1920s, Decade::D1920s, &last_names);
566 add_decade(&mut names, &d1930s, Decade::D1930s, &last_names);
567 add_decade(&mut names, &d1940s, Decade::D1940s, &last_names);
568 add_decade(&mut names, &d1950s, Decade::D1950s, &last_names);
569 add_decade(&mut names, &d1960s, Decade::D1960s, &last_names);
570 add_decade(&mut names, &d1970s, Decade::D1970s, &last_names);
571 add_decade(&mut names, &d1980s, Decade::D1980s, &last_names);
572 add_decade(&mut names, &d1990s, Decade::D1990s, &last_names);
573 add_decade(&mut names, &d2000s, Decade::D2000s, &last_names);
574 add_decade(&mut names, &d2010s, Decade::D2010s, &last_names);
575 add_decade(&mut names, &d2020s, Decade::D2020s, &last_names);
576
577 for (i, (first, gender, _is_classic)) in classics.iter().enumerate() {
579 let last = last_names[i % last_names.len()];
580 names.push(TemporalNameExample::new(
582 first,
583 last,
584 Decade::D1950s,
585 *gender,
586 true,
587 ));
588 }
589
590 names
591}
592
593#[cfg(test)]
598mod tests {
599 use super::*;
600
601 #[test]
602 fn test_create_temporal_dataset() {
603 let names = create_temporal_name_dataset();
604
605 let decades: std::collections::HashSet<_> = names
607 .iter()
608 .map(|n| format!("{:?}", n.peak_decade))
609 .collect();
610
611 assert!(decades.len() >= 10, "Should cover at least 10 decades");
612 assert!(
613 decades.contains("Pre1900"),
614 "Should have pre-1900 (Victorian) names"
615 );
616 assert!(decades.contains("D2020s"), "Should have 2020s names");
617 }
618
619 #[test]
620 fn test_historical_vs_modern() {
621 let names = create_temporal_name_dataset();
622
623 let historical = names
624 .iter()
625 .filter(|n| n.peak_decade.is_historical())
626 .count();
627 let modern = names.iter().filter(|n| n.peak_decade.is_modern()).count();
628
629 assert!(historical > 0, "Should have historical names");
630 assert!(modern > 0, "Should have modern names");
631 }
632
633 #[test]
634 fn test_classic_names_marked() {
635 let names = create_temporal_name_dataset();
636
637 let classics: Vec<_> = names.iter().filter(|n| n.is_classic).collect();
638
639 assert!(!classics.is_empty(), "Should have classic names");
640 assert!(
641 classics.iter().any(|n| n.first_name == "James"),
642 "James should be a classic"
643 );
644 assert!(
645 classics.iter().any(|n| n.first_name == "Elizabeth"),
646 "Elizabeth should be a classic"
647 );
648 }
649
650 #[test]
651 fn test_decade_ordering() {
652 assert!(Decade::Pre1900 < Decade::D1900s);
653 assert!(Decade::D1900s < Decade::D2020s);
654 assert!(Decade::D1980s.midpoint_year() == 1985);
655 }
656
657 #[test]
658 fn test_gender_distribution() {
659 let names = create_temporal_name_dataset();
660
661 let masculine = names
662 .iter()
663 .filter(|n| n.gender == TemporalGender::Masculine)
664 .count();
665 let feminine = names
666 .iter()
667 .filter(|n| n.gender == TemporalGender::Feminine)
668 .count();
669
670 assert!(masculine > 20, "Should have substantial masculine names");
672 assert!(feminine > 20, "Should have substantial feminine names");
673 }
674}