Skip to main content

oxigdal_security/anonymization/
generalization.rs

1//! K-anonymity and L-diversity.
2
3use std::collections::{HashMap, HashSet};
4
5/// K-anonymity checker.
6pub struct KAnonymity {
7    k: usize,
8}
9
10impl KAnonymity {
11    /// Create new K-anonymity checker.
12    pub fn new(k: usize) -> Self {
13        Self { k }
14    }
15
16    /// Check if dataset satisfies k-anonymity.
17    pub fn check(&self, records: &[Vec<String>], quasi_identifiers: &[usize]) -> bool {
18        let mut groups: HashMap<Vec<String>, usize> = HashMap::new();
19
20        for record in records {
21            let qi: Vec<String> = quasi_identifiers
22                .iter()
23                .filter_map(|&i| record.get(i).cloned())
24                .collect();
25            *groups.entry(qi).or_insert(0) += 1;
26        }
27
28        groups.values().all(|&count| count >= self.k)
29    }
30
31    /// Get minimum group size.
32    pub fn min_group_size(&self, records: &[Vec<String>], quasi_identifiers: &[usize]) -> usize {
33        let mut groups: HashMap<Vec<String>, usize> = HashMap::new();
34
35        for record in records {
36            let qi: Vec<String> = quasi_identifiers
37                .iter()
38                .filter_map(|&i| record.get(i).cloned())
39                .collect();
40            *groups.entry(qi).or_insert(0) += 1;
41        }
42
43        groups.values().copied().min().unwrap_or(0)
44    }
45}
46
47/// L-diversity checker.
48pub struct LDiversity {
49    l: usize,
50}
51
52impl LDiversity {
53    /// Create new L-diversity checker.
54    pub fn new(l: usize) -> Self {
55        Self { l }
56    }
57
58    /// Check if dataset satisfies l-diversity.
59    pub fn check(
60        &self,
61        records: &[Vec<String>],
62        quasi_identifiers: &[usize],
63        sensitive_attr: usize,
64    ) -> bool {
65        let mut groups: HashMap<Vec<String>, HashSet<String>> = HashMap::new();
66
67        for record in records {
68            let qi: Vec<String> = quasi_identifiers
69                .iter()
70                .filter_map(|&i| record.get(i).cloned())
71                .collect();
72
73            if let Some(sensitive) = record.get(sensitive_attr) {
74                groups.entry(qi).or_default().insert(sensitive.clone());
75            }
76        }
77
78        groups.values().all(|values| values.len() >= self.l)
79    }
80}
81
82#[cfg(test)]
83mod tests {
84    use super::*;
85
86    #[test]
87    fn test_k_anonymity() {
88        let checker = KAnonymity::new(2);
89
90        let records = vec![
91            vec![
92                "Alice".to_string(),
93                "30".to_string(),
94                "Engineer".to_string(),
95            ],
96            vec!["Bob".to_string(), "30".to_string(), "Doctor".to_string()],
97            vec![
98                "Charlie".to_string(),
99                "40".to_string(),
100                "Teacher".to_string(),
101            ],
102            vec!["David".to_string(), "40".to_string(), "Lawyer".to_string()],
103        ];
104
105        // Age is quasi-identifier (index 1)
106        assert!(checker.check(&records, &[1]));
107    }
108}