【k-匿名(k-Anonymity)代码实现】差分隐私代码实现系列(三)

写在前面的话

书上学来终觉浅,绝知此事要躬行

回顾

数据:

1、显式标识符(ID,能够唯一地确定一条用户记录)。
2、准标识符(QI,能够以较高的概率结合一定的外部信息确定一条用户记录):单列并不能定位个人,但是多列信息可用来潜在的识别某个人。
3、敏感属性(需要保护的信息)。
4、非敏感属性(一般可以直接发布的信息)。

隐私:用户敏感数据与个体身份之间的对应关系。

个人标识泄露。当数据使用人员通过任何方式确认数据表中某条数据属于某个人时,称为个人标识泄露。个人标识泄露最为严重,因为一旦发生个人标识泄露,数据使用人员就可以得到具体个人的敏感信息。

属性泄露,当数据使用人员根据其访问的数据表了解到某个人新的属性信息时,称为属性泄露。个人标识泄露肯定会导致属性泄露,但属性泄露也有可能单独发生。

成员关系泄露。当数据使用人员可以确认某个人的数据存在于数据表中时,称为成员关系泄露。成员关系泄露相对风险较小,个人标识泄露与属性泄露肯定意味着成员关系泄露,但成员关系泄露也有可能单独发生。

方法:删除标识符的方式发布数据。

缺点:攻击者可以通过链接攻击和差分攻击获取个体的隐私数据。

链接攻击:攻击者通过对发布的数据和其他渠道获取的外部数据进行链接操作,以推理出隐私数据,从而造成隐私泄露,相当于一种个人信息维度的扩充。最简单的例子就是数据库里两张表通过主键关联,得到更多的信息。

差分攻击:攻击者通过对发布的数据进行查询操作,通过查询结果做差从而推理出隐私数据,造成隐私泄露。最简单的例子就是先查n个人的信息,再查n-1个人的信息,再做差。

k k k-匿名( k k k-Anonymity)

长篇大论之前先抛一个最直白的理解, k k k-Anonymity就是处理数据记录让大家看起来一样。再进一步解释就是,通过发布精度较低的数据,使得每条记录至少与数据表中其他k-1 条记录具有完全相同的准标识符属性值,从而减少链接攻击所导致的隐私泄露

长篇大论来了~

为了攻击者可以通过链接攻击和差分攻击获取个体的隐私数据,通常需要对准标识列进行脱敏处理,如数据泛化等。

数据泛化是将准标识列的数据替换为语义一致但更通用的数据,经过泛化后,有多条纪录的准标识列属性值相同,所有准标识列属性值相同的行的集合被称为相等集。 k k k-Anonymity要求对于任意一行纪录,其所属的相等集内纪录数量不小于k,即至少有k-1条纪录半标识列属性值与该条纪录相同。

k k k-Anonymity[2]是一个正式的隐私定义。 k k k-Anonymity的定义旨在正式化我们的对隐私保护的看法,即一条辅助信息不应该缩小个人"太多"的可能记录集。换句话说, k k k-Anonymity旨在确保每个人都可以"融入人群"

非正式地说,如果数据集中的每个个体都是大小至少为 k k k的组的成员,则说数据集对于特定 k k k是" k k k-Anonymity",使得组的每个成员都与组的所有其他成员共享相同的准标识符(所有数据集列的选定子集)。因此,每个组中的个人"融入"他们的组 - 可以将个人缩小到特定组中的成员身份,但不能确定哪个组成员是目标。

定义:

Definition
Formally, we say that a dataset D D D satisfies k k k-Anonymity for a value of k k k if:

  • For each row r 1 ∈ D r_1 \in D r1D, there exist at least k − 1 k-1 k1 other rows r 2 … r k ∈ D r_2 \dots r_k \in D r2rkD such that Π q i ( D ) r 1 = Π q i ( D ) r 2 , … , Π q i ( D ) r 1 = Π q i ( D ) r k \Pi_{qi(D)} r_1 = \Pi_{qi(D)} r_2, \dots, \Pi_{qi(D)} r_1 = \Pi_{qi(D)} r_k Πqi(D)r1=Πqi(D)r2,,Πqi(D)r1=Πqi(D)rk

where q i ( D ) qi(D) qi(D) is the quasi-identifiers of D D D, and Π q i ( D ) r \Pi_{qi(D)} r Πqi(D)r represents the columns of r r r containing quasi-identifiers (i.e. the projection of the quasi-identifiers).

形式上,我们说数据集 D D D满足 k k k k k k-Anonymity的话,则:

对于每行 r 1 ∈ D r_1 \in D r1D,至少存在 k − 1 k-1 k1其他行 r 2 … r k ∈ D r_2 \dots r_k \in D r2rkD,使得 Π q i ( D ) r 1 = Π q i ( D ) r 2 , … , Π q i ( D ) r 1 = Π q i ( D ) r k \Pi_{qi(D)} r_1 = \Pi_{qi(D)} r_2, \dots, \Pi_{qi(D)} r_1 = \Pi_{qi(D)} r_k Πqi(D)r1=Πqi(D)r2,,Πqi(D)r1=Πqi(D)rk

其中 q i ( D ) qi(D) qi(D) D D D的准标识符, Π q i ( D ) r \Pi_{qi(D)} r Πqi(D)r表示包含准标识符的 r r r 列(即准标识符的投影)。

k-匿名通过概括(对数据进行更加概括、抽象的描述)和隐匿(不发布某些数据项)技术。

检查 k k k-匿名(Checking for k k k-Anonymity)

我们将从一个小数据集开始,以便通过查看数据可以立即看到它是否满足 k k k-Anonymity。

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
plt.style.use('seaborn-whitegrid')
raw_data = {
    'first_name': ['Jason', 'Molly', 'Tina', 'Jake', 'Amy'], 
    'last_name': ['Miller', 'Jacobson', 'Ali', 'Milner', 'Cooze'], 
    'age': [42, 52, 36, 24, 73], 
    'preTestScore': [4, 24, 31, 2, 3],
    'postTestScore': [25, 94, 57, 62, 70]}
#df = pd.DataFrame(raw_data, columns = ['first_name', 'last_name', 'age', 'preTestScore', 'postTestScore'])
df = pd.DataFrame(raw_data, columns = ['age', 'preTestScore', 'postTestScore'])

在这里插入图片描述

此数据集包含年龄加上两个测试分数,显然它不满足当 k > 1 k>1 k>1 k k k-Anonymity的定义,但是满足 k = 1 k=1 k=1 k k k-Anonymity的定义。具体来说就是每行都可以形成自己的大小为 1 的组,每行都不同。

用看的太麻烦,写个函数吧~

def isKAnonymized(df, k):
    for index, row in df.iterrows():
        query = ' & '.join([f'{col} == {row[col]}' for col in df.columns])
        rows = df.query(query)
        if rows < k:
            return False
    return True

循环访问行,对于每一行,我们查询数据记录以查看有多少行与准标识符的值匹配。

如果任何组中的行数小于 k k k,则数据帧不满足该值 k k k-Anonymity,我们返回 False。

请注意,在这个简单的定义中,我们认为所有列都包含准标识符;要将检查限制为所有列的子集,我们需要将表达式替换为其他内容。

看看效果~

当k=1的时候是满足条件的:

isKAnonymized(df, 1)

当k=2的时候是不满足条件的:

isKAnonymized(df, 2)

在这里插入图片描述

生成满足 k k k-匿名的数据(Generalizing Data to Satisfy k k k-Anonymity)

修改数据集以使其满足所需 k k k-匿名性的过程通常通过泛化数据来完成

修改值以使其不那么具体,因此更有可能与数据集中其他个体的值匹配。

例如,精确到年份的年龄可以通过舍入到最接近的 10 年来泛化,或者邮政编码可能将其最右边的数字替换为零。

对于数值,这很容易实现。我们将使用数据帧的方法,并传入一个名为字典的字典,该字典指定每列要用零替换的数字数。

这使我们能够灵活地试验不同列的不同泛化级别。

还是老样子,写个函数吧~
df.apply(lambda表达式)

def generalize(df, depths):
    return df.apply(lambda x: x.apply(lambda y: int(int(y/(10**depths[x.name]))*(10**depths[x.name]))))

这个函数里面的y表示表中实际数据,比如42。depths[x.name]表示当前属性下设定的映射值,下面在depths中将各个属性设为1,也就是对42/10再*10得到40。整个函数就是对所有数字向下取到10的倍数。

现在,我们可以概括我们的示例数据帧。

首先,我们将尝试将每列概括为一个"级别" , 即舍入到最接近的10。

depths = {
    'age': 1,
    'preTestScore': 1,
    'postTestScore': 1
}
df2 = generalize(df, depths)
df2

在这里插入图片描述
请注意,即使在泛化之后,我们的示例数据仍然不满足当 k > 1 k>1 k>1 k k k-Anonymity的定义。

在这里插入图片描述
我们可以尝试泛化更多,但随后我们最终会删除所有数据!各个属性的深度又1变成2,等于是42/100直接为0了。

depths = {
    'age': 2,
    'preTestScore': 2,
    'postTestScore': 2
}
generalize(df, depths)

在这里插入图片描述
这样的例子也说明了 k k k组有意义的数据帧对于 k k k-Anonymity,需要从数据中删除大量信息。

数据越多泛化越好?(Does More Data Improve Generalization?)

我们的示例数据集太小,无法正常工作。由于数据集中只有 5 个个体,因此很难构建由 2 个或更多共享相同属性的个体组成的组。这个问题的解决方案是更多的数据:在具有更多个体的数据集中,通常需要较少的泛化来满足 k k k组有意义的数据帧对于 k k k-Anonymity。

让我们尝试一下我们检查的用于去识别化的相同人口普查数据。此数据集包含超过 32,000 行,因此实现 k k k-Anonymity应该更容易。

我们将每个人的邮政编码,年龄和教育成就视为准标识符。我们将只投影这些列,并尝试实现 k = 2 k=2 k=2 k k k-Anonymity。因为数据已为 k = 1 k=1 k=1 k k k-Anonymity。

请注意,我们只从数据集中获取前 100 行进行此检查 。

老样子,读数据~

adult_data = pd.read_csv("adult_with_pii.csv")
adult_data.head()

在这里插入图片描述
当前情况 k = 2 k=2 k=2 k k k-Anonymity不满足, k = 1 k=1 k=1 k k k-Anonymity已经满足。

df = adult_data[['Age', 'Education-Num']]
df.columns = ['age', 'edu']
isKAnonymized(df.head(100), 1)
isKAnonymized(df.head(100), 2)

在这里插入图片描述
还是老方法,调用generalize对数据进行处理。

# outliers are a real problem!
depths = {
    'age': 1,
    'edu': 1
}
df2 = generalize(df.head(1000), depths)
isKAnonymized(df2, 2)

在这里插入图片描述
结果仍然不满足 k = 2 k=2 k=2 k k k-Anonymity!

事实上,我们可以对所有 32,000 行执行此泛化,但仍然无法满足 k = 2 k=2 k=2 k k k-Anonymity。

因此添加更多数据并不一定像我们预期的那样有帮助。

原因是数据集包含异常值,即与其他人群截然不同的个体。这些人不容易融入任何群体,即使在泛化之后也是如此。

即使只考虑年龄,我们也可以看到添加更多数据不太可能有所帮助,因为非常低和非常高的年龄在数据集中的代表性很差。

df2['age'].hist();

在这里插入图片描述
在这种情况下,实现 k k k-Anonymity的最佳泛化是非常具有挑战性的。

对于年龄在20-40岁之间的代表性良好的个体来说,将每一行泛化得更多是过分的,并且会损害效用。

然而,对于年龄范围上端和下端的个体,显然需要更多的泛化。

这是在实践中经常发生的那种挑战,很难自动解决。

事实上, k k k-Anonymity的最佳泛化已被证明是NP-hard。

异常值使得实现 k k k-Anonymity非常具有挑战性,即使对于大型数据集也是如此。 k k k的最优泛化 k k k-Anonymity是NP-hard。

删除异常值(Removing Outliers)

那我们就束手无策了吗?

这个问题的一个解决方案是简单地将数据集中每个人的年龄裁剪为位于特定范围内,从而完全消除异常值。

这也会损害效用,因为它用假的年龄取代了真实的年龄,但它可能比进一步泛化每一行要好。

我们可以使用Numpy的方法来执行此裁剪。我们将年龄裁剪为60岁或以下,而将教育水平单独保留(通过将其裁剪为非常大的值)。
Python pandas.DataFrame.clip函数方法的使用

# clipping away outliers
depths = {
    'age': 1,
    'edu': 1
}
dfp = df.clip(upper=np.array([60, 10000000000000]), axis='columns')
df2 = generalize(dfp.head(500), depths)
isKAnonymized(df2, 7)

在这里插入图片描述
此时处理过的数据集就满足 k = 7 k=7 k=7 k k k-Anonymity。我们的泛化水平是适当的,但是异常值仍旧阻止我们实现 k k k-Anonymity。

总结

1、 k k k-Anonymity是数据的一个属性,它确保每个个体都与至少一组 k k k个体"融合"。

2、 k k k-Anonymity甚至在计算上也很昂贵:朴素算法是 O ( n 2 ) O(n^2) On2,更快的算法占用相当大的空间。

3、 k k k-Anonymity可以通过泛化数据集来修改数据集来实现,这样特定值变得更加常见,组更容易形成。

4、最优泛化极其困难,异常值可能使其更具挑战性。自动解决这个问题是NP困难的。

  • 13
    点赞
  • 90
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 8
    评论
以下是一个简单的k匿名算法的C++实现,其中k的值为3: ```c++ #include <iostream> #include <string> #include <fstream> #include <vector> #include <algorithm> #include <map> using namespace std; // 定义一个结构体存储数据 struct Record { string name; int age; string gender; string occupation; }; // 定义一个函数读取数据 vector<Record> readData(string filename) { vector<Record> records; ifstream infile(filename); string name, gender, occupation; int age; while (infile >> name >> age >> gender >> occupation) { Record record = {name, age, gender, occupation}; records.push_back(record); } infile.close(); return records; } // 定义一个函数对数据进行k匿名处理 void k_anonymity(vector<Record>& records, int k) { int n = records.size(); // 定义一个map存储每种属性的出现次数 map<string, int> name_count, age_count, gender_count, occupation_count; // 统计每种属性的出现次数 for (int i = 0; i < n; i++) { name_count[records[i].name]++; age_count[to_string(records[i].age)]++; gender_count[records[i].gender]++; occupation_count[records[i].occupation]++; } // 对每条记录进行k匿名处理 for (int i = 0; i < n; i++) { // 找到与当前记录相同的所有记录 vector<Record> group; for (int j = 0; j < n; j++) { if (records[i].name == records[j].name && to_string(records[i].age) == to_string(records[j].age) && records[i].gender == records[j].gender && records[i].occupation == records[j].occupation) { group.push_back(records[j]); } } // 如果当前组的大小小于k,则将所有记录的年龄设为0 if (group.size() < k) { for (int j = 0; j < group.size(); j++) { group[j].age = 0; } } // 如果当前组的大小大于等于k,则将所有记录的年龄设为当前组中年龄的众数 else { int max_count = 0; string max_age; for (auto& it : age_count) { if (it.second > max_count) { max_count = it.second; max_age = it.first; } } for (int j = 0; j < group.size(); j++) { group[j].age = stoi(max_age); } } } } // 定义一个函数输出匿名处理后的数据 void printData(vector<Record>& records) { int n = records.size(); for (int i = 0; i < n; i++) { cout << records[i].name << " " << records[i].age << " " << records[i].gender << " " << records[i].occupation << endl; } } int main() { vector<Record> records = readData("data.txt"); k_anonymity(records, 3); printData(records); return 0; } ``` 在上述代码中,我们首先定义了一个结构体`Record`来存储数据,然后定义了一个函数`readData`来读取数据,接着定义了一个函数`k_anonymity`来对数据进行k匿名处理,最后定义了一个函数`printData`来输出匿名处理后的数据。在`k_anonymity`函数中,我们首先使用map来统计每种属性的出现次数,然后对每条记录进行k匿名处理,具体的处理方式如下: - 如果当前组的大小小于k,则将所有记录的年龄设为0。 - 如果当前组的大小大于等于k,则将所有记录的年龄设为当前组中年龄的众数。 在本实现中,我们只对年龄进行了匿名处理,而对其他属性没有进行处理。当然,我们也可以对其他属性进行类似的处理,具体的实现方式类似。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

粥粥粥少女的拧发条鸟

你的鼓励是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值