1use serde::{Deserialize, Serialize};
34use std::collections::{HashMap, HashSet};
35
36#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct QualityReport {
43 pub reliability: ReliabilityMetrics,
45 pub difficulty: DifficultyMetrics,
47 pub validity: ValidityMetrics,
49 pub overall_grade: String,
51 pub recommendations: Vec<String>,
53}
54
55#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct ReliabilityMetrics {
58 pub redundancy: f64,
60 pub duplicate_count: usize,
62 pub leakage_ratio: f64,
64 pub leaked_count: usize,
66}
67
68#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct DifficultyMetrics {
71 pub unseen_entity_ratio: f64,
73 pub unseen_entity_count: usize,
75 pub entity_ambiguity: f64,
77 pub ambiguous_examples: Vec<(String, Vec<String>)>,
79 pub entity_density: f64,
81}
82
83#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct ValidityMetrics {
86 pub entity_imbalance: f64,
88 pub type_distribution: HashMap<String, usize>,
90 pub entity_null_rate: f64,
92 pub avg_entities_per_sample: f64,
94}
95
96#[derive(Debug, Clone, Default)]
102pub struct DatasetQualityAnalyzer {
103 pub min_samples: usize,
105}
106
107impl DatasetQualityAnalyzer {
108 pub fn new(min_samples: usize) -> Self {
110 Self { min_samples }
111 }
112
113 pub fn analyze<S: AsRef<str>, T: AsRef<str>>(
119 &self,
120 train_data: &[(S, Vec<(T, T)>)],
121 test_data: &[(S, Vec<(T, T)>)],
122 ) -> QualityReport {
123 let reliability = self.compute_reliability(train_data, test_data);
124 let difficulty = self.compute_difficulty(train_data, test_data);
125 let validity = self.compute_validity(train_data);
126
127 let (grade, recommendations) =
128 self.compute_grade_and_recommendations(&reliability, &difficulty, &validity);
129
130 QualityReport {
131 reliability,
132 difficulty,
133 validity,
134 overall_grade: grade,
135 recommendations,
136 }
137 }
138
139 fn compute_reliability<S: AsRef<str>, T: AsRef<str>>(
140 &self,
141 train_data: &[(S, Vec<(T, T)>)],
142 test_data: &[(S, Vec<(T, T)>)],
143 ) -> ReliabilityMetrics {
144 let mut seen_texts = HashSet::new();
146 let mut duplicate_count = 0;
147
148 for (text, _) in train_data {
149 let normalized = text.as_ref().to_lowercase();
150 if !seen_texts.insert(normalized) {
151 duplicate_count += 1;
152 }
153 }
154
155 let redundancy = if train_data.is_empty() {
156 0.0
157 } else {
158 duplicate_count as f64 / train_data.len() as f64
159 };
160
161 let train_texts: HashSet<String> = train_data
163 .iter()
164 .map(|(t, _)| t.as_ref().to_lowercase())
165 .collect();
166
167 let mut leaked_count = 0;
168 for (text, _) in test_data {
169 if train_texts.contains(&text.as_ref().to_lowercase()) {
170 leaked_count += 1;
171 }
172 }
173
174 let leakage_ratio = if test_data.is_empty() {
175 0.0
176 } else {
177 leaked_count as f64 / test_data.len() as f64
178 };
179
180 ReliabilityMetrics {
181 redundancy,
182 duplicate_count,
183 leakage_ratio,
184 leaked_count,
185 }
186 }
187
188 fn compute_difficulty<S: AsRef<str>, T: AsRef<str>>(
189 &self,
190 train_data: &[(S, Vec<(T, T)>)],
191 test_data: &[(S, Vec<(T, T)>)],
192 ) -> DifficultyMetrics {
193 let train_entities: HashSet<String> = train_data
195 .iter()
196 .flat_map(|(_, entities)| entities.iter().map(|(e, _)| e.as_ref().to_lowercase()))
197 .collect();
198
199 let mut unseen_count = 0;
201 let mut total_test_entities = 0;
202
203 for (_, entities) in test_data {
204 for (entity, _) in entities {
205 total_test_entities += 1;
206 if !train_entities.contains(&entity.as_ref().to_lowercase()) {
207 unseen_count += 1;
208 }
209 }
210 }
211
212 let unseen_entity_ratio = if total_test_entities == 0 {
213 0.0
214 } else {
215 unseen_count as f64 / total_test_entities as f64
216 };
217
218 let mut entity_labels: HashMap<String, HashSet<String>> = HashMap::new();
220
221 for (_, entities) in train_data.iter().chain(test_data.iter()) {
222 for (entity, label) in entities {
223 entity_labels
224 .entry(entity.as_ref().to_lowercase())
225 .or_default()
226 .insert(label.as_ref().to_string());
227 }
228 }
229
230 let ambiguous: Vec<_> = entity_labels
231 .iter()
232 .filter(|(_, labels)| labels.len() > 1)
233 .map(|(entity, labels)| (entity.clone(), labels.iter().cloned().collect()))
234 .collect();
235
236 let entity_ambiguity = if entity_labels.is_empty() {
237 0.0
238 } else {
239 ambiguous.len() as f64 / entity_labels.len() as f64
240 };
241
242 let total_tokens: usize = train_data
244 .iter()
245 .map(|(t, _)| t.as_ref().split_whitespace().count())
246 .sum();
247
248 let total_entities: usize = train_data.iter().map(|(_, e)| e.len()).sum();
249
250 let entity_density = if total_tokens == 0 {
251 0.0
252 } else {
253 (total_entities as f64 / total_tokens as f64) * 100.0
254 };
255
256 DifficultyMetrics {
257 unseen_entity_ratio,
258 unseen_entity_count: unseen_count,
259 entity_ambiguity,
260 ambiguous_examples: ambiguous.into_iter().take(5).collect(),
261 entity_density,
262 }
263 }
264
265 fn compute_validity<S: AsRef<str>, T: AsRef<str>>(
266 &self,
267 train_data: &[(S, Vec<(T, T)>)],
268 ) -> ValidityMetrics {
269 let mut type_counts: HashMap<String, usize> = HashMap::new();
271
272 for (_, entities) in train_data {
273 for (_, label) in entities {
274 *type_counts.entry(label.as_ref().to_string()).or_insert(0) += 1;
275 }
276 }
277
278 let (max_count, min_count) = if type_counts.is_empty() {
279 (0, 0)
280 } else {
281 let counts: Vec<_> = type_counts.values().copied().collect();
282 (
283 *counts.iter().max().unwrap_or(&0),
284 *counts.iter().min().unwrap_or(&0),
285 )
286 };
287
288 let entity_imbalance = if min_count == 0 {
289 f64::INFINITY
290 } else {
291 max_count as f64 / min_count as f64
292 };
293
294 let total_tokens: usize = train_data
296 .iter()
297 .map(|(t, _)| t.as_ref().split_whitespace().count())
298 .sum();
299
300 let entity_tokens: usize = train_data
302 .iter()
303 .flat_map(|(_, entities)| {
304 entities
305 .iter()
306 .map(|(e, _)| e.as_ref().split_whitespace().count())
307 })
308 .sum();
309
310 let entity_null_rate = if total_tokens == 0 {
311 1.0
312 } else {
313 1.0 - (entity_tokens as f64 / total_tokens as f64)
314 };
315
316 let total_entities: usize = train_data.iter().map(|(_, e)| e.len()).sum();
317 let avg_entities_per_sample = if train_data.is_empty() {
318 0.0
319 } else {
320 total_entities as f64 / train_data.len() as f64
321 };
322
323 ValidityMetrics {
324 entity_imbalance,
325 type_distribution: type_counts,
326 entity_null_rate,
327 avg_entities_per_sample,
328 }
329 }
330
331 fn compute_grade_and_recommendations(
332 &self,
333 reliability: &ReliabilityMetrics,
334 difficulty: &DifficultyMetrics,
335 validity: &ValidityMetrics,
336 ) -> (String, Vec<String>) {
337 let mut issues = Vec::new();
338 let mut score = 100;
339
340 if reliability.redundancy > 0.1 {
342 issues.push(format!(
343 "High redundancy ({:.1}%): Remove duplicates from training data",
344 reliability.redundancy * 100.0
345 ));
346 score -= 15;
347 }
348 if reliability.leakage_ratio > 0.01 {
349 issues.push(format!(
350 "Data leakage detected ({:.1}%): {} test samples appear in training",
351 reliability.leakage_ratio * 100.0,
352 reliability.leaked_count
353 ));
354 score -= 25;
355 }
356
357 if difficulty.unseen_entity_ratio > 0.5 {
359 issues.push(format!(
360 "High unseen entity ratio ({:.1}%): Test set may be too different from training",
361 difficulty.unseen_entity_ratio * 100.0
362 ));
363 score -= 10;
364 }
365 if difficulty.entity_ambiguity > 0.1 {
366 issues.push(format!(
367 "Entity ambiguity ({:.1}%): Some entities have multiple labels - review guidelines",
368 difficulty.entity_ambiguity * 100.0
369 ));
370 score -= 10;
371 }
372
373 if validity.entity_imbalance > 10.0 {
375 issues.push(format!(
376 "Severe class imbalance ({:.1}x): Consider oversampling rare entity types",
377 validity.entity_imbalance
378 ));
379 score -= 15;
380 }
381 if validity.entity_null_rate > 0.95 {
382 issues.push(format!(
383 "Very sparse entities ({:.1}% null): May need more annotated data",
384 validity.entity_null_rate * 100.0
385 ));
386 score -= 10;
387 }
388
389 let grade = match score {
390 90..=100 => "A (Excellent)",
391 80..=89 => "B (Good)",
392 70..=79 => "C (Acceptable)",
393 60..=69 => "D (Needs Improvement)",
394 _ => "F (Critical Issues)",
395 };
396
397 (grade.to_string(), issues)
398 }
399}
400
401pub fn check_leakage<S: AsRef<str>>(train_texts: &[S], test_texts: &[S]) -> (usize, f64) {
407 let train_set: HashSet<String> = train_texts
408 .iter()
409 .map(|t| t.as_ref().to_lowercase())
410 .collect();
411
412 let leaked = test_texts
413 .iter()
414 .filter(|t| train_set.contains(&t.as_ref().to_lowercase()))
415 .count();
416
417 let ratio = if test_texts.is_empty() {
418 0.0
419 } else {
420 leaked as f64 / test_texts.len() as f64
421 };
422
423 (leaked, ratio)
424}
425
426pub fn entity_imbalance_ratio<S: AsRef<str>>(entity_types: &[S]) -> f64 {
428 let mut counts: HashMap<&str, usize> = HashMap::new();
429 for t in entity_types {
430 *counts.entry(t.as_ref()).or_insert(0) += 1;
431 }
432
433 if counts.is_empty() {
434 return 1.0;
435 }
436
437 let max = *counts.values().max().unwrap_or(&0);
438 let min = *counts.values().min().unwrap_or(&0);
439
440 if min == 0 {
441 f64::INFINITY
442 } else {
443 max as f64 / min as f64
444 }
445}
446
447#[cfg(test)]
452mod tests {
453 use super::*;
454
455 #[test]
456 fn test_redundancy_detection() {
457 let train: Vec<(&str, Vec<(&str, &str)>)> = vec![
458 ("John works at Google.", vec![("John", "PER")]),
459 ("John works at Google.", vec![("John", "PER")]), ("Jane joined Microsoft.", vec![("Jane", "PER")]),
461 ];
462 let test: Vec<(&str, Vec<(&str, &str)>)> = vec![];
463
464 let analyzer = DatasetQualityAnalyzer::default();
465 let report = analyzer.analyze(&train, &test);
466
467 assert_eq!(report.reliability.duplicate_count, 1);
468 assert!(report.reliability.redundancy > 0.0);
469 }
470
471 #[test]
472 fn test_leakage_detection() {
473 let train: Vec<(&str, Vec<(&str, &str)>)> =
474 vec![("John works at Google.", vec![("John", "PER")])];
475 let test: Vec<(&str, Vec<(&str, &str)>)> = vec![
476 ("John works at Google.", vec![("John", "PER")]), ("Jane joined Microsoft.", vec![("Jane", "PER")]),
478 ];
479
480 let analyzer = DatasetQualityAnalyzer::default();
481 let report = analyzer.analyze(&train, &test);
482
483 assert_eq!(report.reliability.leaked_count, 1);
484 assert!((report.reliability.leakage_ratio - 0.5).abs() < 0.01);
485 }
486
487 #[test]
488 fn test_unseen_entity_ratio() {
489 let train: Vec<(&str, Vec<(&str, &str)>)> = vec![(
490 "John works at Google.",
491 vec![("John", "PER"), ("Google", "ORG")],
492 )];
493 let test: Vec<(&str, Vec<(&str, &str)>)> = vec![(
494 "Jane joined Microsoft.",
495 vec![("Jane", "PER"), ("Microsoft", "ORG")],
496 )];
497
498 let analyzer = DatasetQualityAnalyzer::default();
499 let report = analyzer.analyze(&train, &test);
500
501 assert_eq!(report.difficulty.unseen_entity_count, 2);
503 assert!((report.difficulty.unseen_entity_ratio - 1.0).abs() < 0.01);
504 }
505
506 #[test]
507 fn test_entity_ambiguity() {
508 let train: Vec<(&str, Vec<(&str, &str)>)> = vec![
509 ("Washington is a state.", vec![("Washington", "LOC")]),
510 ("Washington was president.", vec![("Washington", "PER")]), ];
512 let test: Vec<(&str, Vec<(&str, &str)>)> = vec![];
513
514 let analyzer = DatasetQualityAnalyzer::default();
515 let report = analyzer.analyze(&train, &test);
516
517 assert!(report.difficulty.entity_ambiguity > 0.0);
518 assert!(!report.difficulty.ambiguous_examples.is_empty());
519 }
520
521 #[test]
522 fn test_entity_imbalance() {
523 let train: Vec<(&str, Vec<(&str, &str)>)> = vec![
524 ("Text 1", vec![("e1", "PER"), ("e2", "PER"), ("e3", "PER")]),
525 ("Text 2", vec![("e4", "ORG")]), ];
527 let test: Vec<(&str, Vec<(&str, &str)>)> = vec![];
528
529 let analyzer = DatasetQualityAnalyzer::default();
530 let report = analyzer.analyze(&train, &test);
531
532 assert!((report.validity.entity_imbalance - 3.0).abs() < 0.01);
533 }
534
535 #[test]
536 fn test_quick_leakage_check() {
537 let train = vec!["text a", "text b", "text c"];
538 let test = vec!["text a", "text d"]; let (count, ratio) = check_leakage(&train, &test);
541 assert_eq!(count, 1);
542 assert!((ratio - 0.5).abs() < 0.01);
543 }
544}