data-privacy 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data_privacy-0.1.0/LICENSE +21 -0
- data_privacy-0.1.0/PKG-INFO +16 -0
- data_privacy-0.1.0/README.md +4 -0
- data_privacy-0.1.0/data_privacy/__init__.py +7 -0
- data_privacy-0.1.0/data_privacy/anonymity.py +19 -0
- data_privacy-0.1.0/data_privacy/channel.py +79 -0
- data_privacy-0.1.0/data_privacy/dataset.py +33 -0
- data_privacy-0.1.0/data_privacy/measure.py +101 -0
- data_privacy-0.1.0/data_privacy/mechanism.py +119 -0
- data_privacy-0.1.0/data_privacy/strategy.py +21 -0
- data_privacy-0.1.0/data_privacy.egg-info/PKG-INFO +16 -0
- data_privacy-0.1.0/data_privacy.egg-info/SOURCES.txt +15 -0
- data_privacy-0.1.0/data_privacy.egg-info/dependency_links.txt +1 -0
- data_privacy-0.1.0/data_privacy.egg-info/requires.txt +3 -0
- data_privacy-0.1.0/data_privacy.egg-info/top_level.txt +1 -0
- data_privacy-0.1.0/pyproject.toml +19 -0
- data_privacy-0.1.0/setup.cfg +4 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Topete
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: data-privacy
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Utilities for ...
|
|
5
|
+
Requires-Python: >=3.9
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Dist: numpy
|
|
9
|
+
Requires-Dist: pandas
|
|
10
|
+
Requires-Dist: scipy
|
|
11
|
+
Dynamic: license-file
|
|
12
|
+
|
|
13
|
+
# A First Course in Data Privacy
|
|
14
|
+
|
|
15
|
+
This is the codebase supporting the textbook on privacy for undergraduates.
|
|
16
|
+
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
|
|
3
|
+
def max_kanonymity(D):
|
|
4
|
+
'''Outputs the maximum value of k for which D represents a k-anonymous dataset. D is
|
|
5
|
+
a pandas dataframe created using the make_channel function.'''
|
|
6
|
+
return D.astype(bool).sum(axis=0).min()
|
|
7
|
+
|
|
8
|
+
def is_kanonymous(D, k):
|
|
9
|
+
'''Outputs true if the dataset D satisfies k-anonymity. D is a pandas dataframe
|
|
10
|
+
created using the make_channel function.'''
|
|
11
|
+
max_k = max_kanonymity(D)
|
|
12
|
+
return max_k >= k
|
|
13
|
+
|
|
14
|
+
def vulnerable_identifiers(D, k):
|
|
15
|
+
'''Outputs the column names for which k-anonymity does not hold.'''
|
|
16
|
+
# k_values contains the k-anonymity value for each column
|
|
17
|
+
k_values = D.astype(bool).sum(axis=0)
|
|
18
|
+
# return the column names and k-anonymity values for which k_values are < k.
|
|
19
|
+
return k_values[k_values < k]
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
import numpy as np
|
|
3
|
+
import math
|
|
4
|
+
from itertools import product
|
|
5
|
+
import pyqif as pyq
|
|
6
|
+
import privacybook.strategy as strategy
|
|
7
|
+
|
|
8
|
+
def compose(C, D):
|
|
9
|
+
'''Constructs the perturbation channel corresponding to the composition of
|
|
10
|
+
the channels C and D. Assumes that the columns of C match the rows of D.'''
|
|
11
|
+
C_values = C.to_numpy()
|
|
12
|
+
D_values = D.to_numpy()
|
|
13
|
+
composition = np.matmul(C_values, D_values)
|
|
14
|
+
return pd.DataFrame(data=composition, index=C.index, columns=D.columns)
|
|
15
|
+
|
|
16
|
+
def kroneckerN(C, n):
|
|
17
|
+
'''Computes the n-fold Kronecker product of channel C with itself.'''
|
|
18
|
+
# Use underlying pyqif library to kronecker the channel
|
|
19
|
+
Ch = C.to_numpy()
|
|
20
|
+
ChN = pyq.compose.kroneckerN(Ch, n)
|
|
21
|
+
|
|
22
|
+
# Compute cartesian product of labels
|
|
23
|
+
states = list(product(C.index, repeat=n))
|
|
24
|
+
idx = pd.MultiIndex.from_tuples(
|
|
25
|
+
states,
|
|
26
|
+
names=[f"X{i+1}" for i in range(n)]
|
|
27
|
+
)
|
|
28
|
+
# Put channel back together with labels
|
|
29
|
+
Kn = pd.DataFrame(ChN, index=idx, columns=idx)
|
|
30
|
+
return Kn
|
|
31
|
+
|
|
32
|
+
def inputs_at_risk(C):
|
|
33
|
+
'''
|
|
34
|
+
Returns the row indices of the channel C for which the secrets can be identified completely.
|
|
35
|
+
A secret is fully identified if there exists a column for which this secret is the only non-zero
|
|
36
|
+
value in the column.
|
|
37
|
+
'''
|
|
38
|
+
# Assume C is a channel with secrets as row labels
|
|
39
|
+
# We first count the number of non-zero values in each column
|
|
40
|
+
counts = C.astype(bool).sum(axis=0)
|
|
41
|
+
# Now we find the secrets that correspond to single non-zero values in a column
|
|
42
|
+
inputs = counts.index[counts == 1]
|
|
43
|
+
return inputs
|
|
44
|
+
|
|
45
|
+
def inputs_at_high_risk(C, p):
|
|
46
|
+
'''Returns a list of row indices (secrets) for which the adversary can guess the secret
|
|
47
|
+
with probability ≥ p under a uniform prior.
|
|
48
|
+
'''
|
|
49
|
+
# Compute the posterior matrix under uniform prior
|
|
50
|
+
u = pyq.probab.uniform(C.shape[0])
|
|
51
|
+
P = pyq.channel.posteriors(u, C.to_numpy())
|
|
52
|
+
P_df = pd.DataFrame(data=P, index=C.index, columns=C.columns)
|
|
53
|
+
maxes = P_df.max()
|
|
54
|
+
return maxes.index[maxes > p]
|
|
55
|
+
|
|
56
|
+
def inputs_at_linkage_risk(C, attrs):
|
|
57
|
+
'''Returns the secrets which can be perfectly identified by the QIDs in attrs.
|
|
58
|
+
'''
|
|
59
|
+
# Assume that C is a channel returned by make_channel.
|
|
60
|
+
# Assume also that attrs is a subset of the QIDs.
|
|
61
|
+
|
|
62
|
+
# Group the secrets by the attrs
|
|
63
|
+
groups = C.T.groupby(attrs).sum()
|
|
64
|
+
|
|
65
|
+
# Count how many QIDs are unique to a secret
|
|
66
|
+
summary = groups.astype(bool).T.sum()
|
|
67
|
+
|
|
68
|
+
vulnerable_attrs = summary.index[summary == 1]
|
|
69
|
+
vulnerable_secrets = C[vulnerable_attrs].astype(bool).sum(axis=1)
|
|
70
|
+
return vulnerable_secrets.index[vulnerable_secrets == 1]
|
|
71
|
+
|
|
72
|
+
def inputs_at_inference_risk(C, D):
|
|
73
|
+
'''Returns the list of of secrets that are at risk from being inferred
|
|
74
|
+
through linking C and D.
|
|
75
|
+
'''
|
|
76
|
+
# Assume that C and D are channels created by make_channel.
|
|
77
|
+
# Assume that the outputs of C correspond with the inputs of D.
|
|
78
|
+
CD = strategy.linkage(C, D)
|
|
79
|
+
return inputs_at_risk(CD)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
|
|
3
|
+
#
|
|
4
|
+
# make_channel
|
|
5
|
+
#
|
|
6
|
+
# Inputs:
|
|
7
|
+
# - inputTable: dataframe representing a dataset
|
|
8
|
+
# - secretsList: array of column names representing secrets
|
|
9
|
+
# - qidsList: array of column names representing QIDs
|
|
10
|
+
#
|
|
11
|
+
# Returns:
|
|
12
|
+
# - dataframe representing a channel: rows are labelled with
|
|
13
|
+
# secrets and columns are labelled with QIDs
|
|
14
|
+
#
|
|
15
|
+
|
|
16
|
+
def make_channel(inputTable, secretsList, qidsList):
|
|
17
|
+
'''Create a channel from a tabular datasource given as a pandas frame.
|
|
18
|
+
The row and column labels are also provided as secretsList and qidsList
|
|
19
|
+
respectively.'''
|
|
20
|
+
|
|
21
|
+
# Group by QIDs + secrets → counts
|
|
22
|
+
counts = inputTable.groupby(qidsList + secretsList).size()
|
|
23
|
+
|
|
24
|
+
# Reshape into QID (columns) × Secret (rows)
|
|
25
|
+
channel = counts.unstack(level=qidsList, fill_value=0)
|
|
26
|
+
|
|
27
|
+
# Convert counts into probabilities
|
|
28
|
+
row_sums = channel.sum(axis=1)
|
|
29
|
+
prob_channel = channel.div(row_sums, axis="index")
|
|
30
|
+
|
|
31
|
+
return prob_channel
|
|
32
|
+
|
|
33
|
+
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
from scipy.spatial.distance import pdist, squareform
|
|
2
|
+
import pyqif as pyq
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
def quasi_identification_risk(D):
|
|
6
|
+
# Pre: D is a dataframe
|
|
7
|
+
c = D.to_numpy() # Convert DataFrame to NumPy array
|
|
8
|
+
u = pyq.probab.uniform(c.shape[0])
|
|
9
|
+
|
|
10
|
+
# Return average risk across all individuals
|
|
11
|
+
return pyq.measure.vg.posterior_bayes(u, c)
|
|
12
|
+
|
|
13
|
+
#
|
|
14
|
+
# This function computes the bayes capacity of the channel, which is the
|
|
15
|
+
# sum of the column maxima.
|
|
16
|
+
#
|
|
17
|
+
def reidentification_risk(D):
|
|
18
|
+
'''Computes the Bayes' capacity for the channel DBN, which corresponds to the
|
|
19
|
+
leakage due to the channel for an adversary who wants to perform a reidentification
|
|
20
|
+
attack (guessing the secret exactly in 1 try). Ranges from 1 (no leak) to #columns in
|
|
21
|
+
channel (full leakage)'''
|
|
22
|
+
return quasi_identification_risk(D)
|
|
23
|
+
#ch = D.to_numpy()
|
|
24
|
+
#return pyq.measure.bayes_capacity(ch)
|
|
25
|
+
|
|
26
|
+
#
|
|
27
|
+
# Computes the posterior vulnerability of the attribute with respect to
|
|
28
|
+
# a uniform prior. This gives the probability of a successful property
|
|
29
|
+
# inference against a given sensitive attribute for an adversary with no
|
|
30
|
+
# prior knowledge.
|
|
31
|
+
#
|
|
32
|
+
def property_inference_risk(D, attr):
|
|
33
|
+
'''Computes the risk of inferring a sensitive attribute in the dataset D.
|
|
34
|
+
This assumes an adversary who has a uniform prior over all secrets.'''
|
|
35
|
+
attr_i = D.index.names.index(attr)
|
|
36
|
+
# Combine all rows with the same sensitive attribute
|
|
37
|
+
# by computing their mean (equivalent to uniform prior)
|
|
38
|
+
D_by_attr = D.groupby(level=[attr_i]).mean()
|
|
39
|
+
u = pyq.probab.uniform(D_by_attr.shape[0])
|
|
40
|
+
return pyq.measure.vg.posterior_bayes(u, D_by_attr.to_numpy())
|
|
41
|
+
|
|
42
|
+
#
|
|
43
|
+
# Computes the risk of guessing the secret given knowledge of the attributes
|
|
44
|
+
# in the attrs array, under a uniform prior.
|
|
45
|
+
#
|
|
46
|
+
def linkability_risk(C, attrs):
|
|
47
|
+
'''Outputs the risk of reconstructing the full secret in channel C,
|
|
48
|
+
relative to linking via attributeList (quasi-identifiers).
|
|
49
|
+
A row is considered at risk if its projection on the attributeList is unique.
|
|
50
|
+
'''
|
|
51
|
+
# For this we assume C is a channel produced by make_channel.
|
|
52
|
+
# We assume attrs is a subset of the QIDs in C.
|
|
53
|
+
|
|
54
|
+
# Group the secrets by the attrs
|
|
55
|
+
groups = C.T.groupby(attrs).sum()
|
|
56
|
+
|
|
57
|
+
# Count how many QIDs are unique to a secret
|
|
58
|
+
summary = groups.astype(bool).T.sum()
|
|
59
|
+
|
|
60
|
+
# Identify the vulnerable secrets as ones which contain a single non-zero value
|
|
61
|
+
# Note that these are actually the QIDs since we have transposed the channel
|
|
62
|
+
vulnerable_attrs = summary.index[summary == 1]
|
|
63
|
+
|
|
64
|
+
# Add up all the probabilities for the vulnerable inputs.
|
|
65
|
+
prob = groups.loc[vulnerable_attrs, ].max(axis=1).sum()
|
|
66
|
+
|
|
67
|
+
# Now have to divide probability by size of secret space, equivalent to uniform prior
|
|
68
|
+
num_secrets = C.index.size
|
|
69
|
+
return prob / num_secrets
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def __dist_arr(C, values):
|
|
73
|
+
val_arr = np.array([ [i] for i in values ])
|
|
74
|
+
dists = pdist(val_arr)
|
|
75
|
+
# create the loss function which gives the abs distance between values of attr
|
|
76
|
+
dists_arr = squareform(dists)
|
|
77
|
+
return dists_arr
|
|
78
|
+
|
|
79
|
+
def average_minimum_error(C, attr):
|
|
80
|
+
'''Computes the expected distance of the true value from the output value for
|
|
81
|
+
the given attribute in the channel D.'''
|
|
82
|
+
# C is a pandas dataframe representing a channel which takes secrets X to observations Y.
|
|
83
|
+
# We assume that the given attribute is numeric so we can compute a mean error
|
|
84
|
+
# between the guessed value of the attribute and the true value.
|
|
85
|
+
values = C.index.get_level_values(attr).to_numpy()
|
|
86
|
+
dists_arr = __dist_arr(C, values)
|
|
87
|
+
avg_uncertainty = pyq.measure.ul.posterior(pyq.probab.uniform(len(values)), C.to_numpy(), dists_arr)
|
|
88
|
+
return avg_uncertainty
|
|
89
|
+
|
|
90
|
+
def likely_interval(D, attr, c):
|
|
91
|
+
'''Computes the probability that an adversary cannot infer the value of attr
|
|
92
|
+
to within bound c of its true value.'''
|
|
93
|
+
# The implementation of this uses the loss function from average_minimum_error
|
|
94
|
+
# and converts the distances to bits depending on c.
|
|
95
|
+
values = D.index.get_level_values(attr).to_numpy()
|
|
96
|
+
dists_arr = __dist_arr(D, values)
|
|
97
|
+
# convert distance matrix to bit matrix
|
|
98
|
+
bits_arr = np.array( [ [ int(i<c) for i in row] for row in dists_arr])
|
|
99
|
+
avg_uncertainty = pyq.measure.ul.posterior(pyq.probab.uniform(len(values)), D.to_numpy(), bits_arr)
|
|
100
|
+
return avg_uncertainty
|
|
101
|
+
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
import numpy as np
|
|
3
|
+
import pyqif as pyq
|
|
4
|
+
import math
|
|
5
|
+
|
|
6
|
+
def bounded_noise(D, attr, noise):
|
|
7
|
+
'''Creates a customised bounded noise channel using the given noise
|
|
8
|
+
distribution. For example, noise which returns the same value with prob.
|
|
9
|
+
1/2, and returns the value +- 1 with prob. 1/4 (each) would look like:
|
|
10
|
+
noise=pd.Series([ 1/4, 1/2, 1/4], index=[-1, 0, 1]).'''
|
|
11
|
+
try:
|
|
12
|
+
attr_i = D.columns.names.index(attr)
|
|
13
|
+
noise_labels = noise.index
|
|
14
|
+
df = pd.DataFrame()
|
|
15
|
+
for col_i in range(len(D.columns)):
|
|
16
|
+
v = D.columns[col_i][attr_i] # the value of the attr to add noise to
|
|
17
|
+
if int(v) != v:
|
|
18
|
+
raise Exception("The given attribute is not numeric")
|
|
19
|
+
# Create a new channel with labels as noisy attrs, values
|
|
20
|
+
# are the probabilities.
|
|
21
|
+
def get_noisy_qid(noise_i):
|
|
22
|
+
qid = list(D.columns[col_i])
|
|
23
|
+
qid[attr_i] += noise_labels[noise_i]
|
|
24
|
+
return qid
|
|
25
|
+
tuples = list([ get_noisy_qid(idx) for idx in range(len(noise_labels))])
|
|
26
|
+
result = pd.MultiIndex.from_tuples(tuples, names=D.columns.names)
|
|
27
|
+
multirow = pd.Series(noise.to_numpy(), index=result, name=D.columns[col_i])
|
|
28
|
+
df = pd.concat([df, multirow.to_frame().T])
|
|
29
|
+
df = df.fillna(0)
|
|
30
|
+
# Fix up the names for the rows (secrets)
|
|
31
|
+
df.index = df.index.set_names(D.columns.names)
|
|
32
|
+
return df
|
|
33
|
+
except ValueError:
|
|
34
|
+
print("Error: no such attribute name: ", attr)
|
|
35
|
+
except Exception as inst:
|
|
36
|
+
print(inst)
|
|
37
|
+
return None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def uniform_bounded_noise(D, attr, bound):
|
|
41
|
+
'''Computes a bounded noise channel for perturbing the attribute in a given channel D
|
|
42
|
+
using the given bound. For example, if bound=1 and attr="age", then each age in
|
|
43
|
+
the dataset D should be perturbed by either adding 1, returning unchanged or subtracting 1,
|
|
44
|
+
each with probability 1/3 (uniform).'''
|
|
45
|
+
noise_prob = 1.0 / (2 * bound + 1) # uniform prob over values
|
|
46
|
+
noise = pd.Series(noise_prob, index=list(range(-bound, bound+1)))
|
|
47
|
+
return bounded_noise(D, attr, noise)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def random_response(D, attr, K, eps):
|
|
51
|
+
'''Given the dataset channel D and an attribute attr, computes the random
|
|
52
|
+
response channel applied to attr with range K and epsilon eps. Note that attr
|
|
53
|
+
(and K) can be categorical (string valued) or numeric.'''
|
|
54
|
+
# RR channel has entries q = 1 / (len(K) - 1 + e **eps)
|
|
55
|
+
# and single entry p = e**eps/(len(K) - 1 + e ** eps)
|
|
56
|
+
q = 1 / (len(K) -1 + (math.e ** eps))
|
|
57
|
+
p = q * (math.e ** eps)
|
|
58
|
+
try:
|
|
59
|
+
attr_i = D.columns.names.index(attr)
|
|
60
|
+
df = pd.DataFrame()
|
|
61
|
+
for col_i in range(len(D.columns)):
|
|
62
|
+
v = D.columns[col_i][attr_i] # the value of the attr to add noise to
|
|
63
|
+
#if int(v) != v:
|
|
64
|
+
# raise Exception("The given attribute is not numeric")
|
|
65
|
+
qid = list(D.columns[col_i]) # the value of the QID for this column
|
|
66
|
+
# We want to replace the value at index attr_i with the range of K
|
|
67
|
+
tuples = [ qid[:attr_i] + [noise_i] + qid[attr_i+1:] for noise_i in K]
|
|
68
|
+
result = pd.MultiIndex.from_tuples(tuples, names=D.columns.names)
|
|
69
|
+
# Random response noise is almost all the same
|
|
70
|
+
noise = [q] * len(K)
|
|
71
|
+
# We need the index of v in K
|
|
72
|
+
if not v in K:
|
|
73
|
+
raise Exception("The attribute " + str(v) + " lies outside the range " + str(K))
|
|
74
|
+
noise[K.index(v)] = p
|
|
75
|
+
multirow = pd.Series(noise, index=result, name=D.columns[col_i])
|
|
76
|
+
df = pd.concat([df, multirow.to_frame().T])
|
|
77
|
+
df = df.fillna(0)
|
|
78
|
+
# Fix up the names for the rows (secrets)
|
|
79
|
+
df.index = df.index.set_names(D.columns.names)
|
|
80
|
+
return df
|
|
81
|
+
except ValueError:
|
|
82
|
+
print("Error: no such attribute name: ", attr)
|
|
83
|
+
except Exception as inst:
|
|
84
|
+
print(inst)
|
|
85
|
+
return None
|
|
86
|
+
|
|
87
|
+
def geometric(D, attr, K, eps):
|
|
88
|
+
'''Given the dataset channel D and a numeric attribute attr, computes the geometric
|
|
89
|
+
channel for adding noise to attr with range K and epsilon eps.'''
|
|
90
|
+
# We start by just generating geometric noise as a numpy array
|
|
91
|
+
G_channel = pyq.dp.ranged_geometric(K, eps)
|
|
92
|
+
try:
|
|
93
|
+
attr_i = D.columns.names.index(attr)
|
|
94
|
+
df = pd.DataFrame()
|
|
95
|
+
for col_i in range(len(D.columns)):
|
|
96
|
+
v = D.columns[col_i][attr_i] # the value of the attr to add noise to
|
|
97
|
+
if int(v) != v:
|
|
98
|
+
raise Exception("The given attribute is not numeric")
|
|
99
|
+
qid = list(D.columns[col_i]) # the value of the QID for this column
|
|
100
|
+
# We want to replace the value at index attr_i with the range of K
|
|
101
|
+
tuples = [ qid[:attr_i] + [noise_i] + qid[attr_i+1:] for noise_i in K]
|
|
102
|
+
result = pd.MultiIndex.from_tuples(tuples, names=D.columns.names)
|
|
103
|
+
# Get the noise from the correct row of the geometric mechanism
|
|
104
|
+
# We need the index of v in K
|
|
105
|
+
if not v in K:
|
|
106
|
+
raise Exception("The attribute " + str(v) + " lies outside the range " + str(K))
|
|
107
|
+
noise = G_channel[K.index(v)]
|
|
108
|
+
multirow = pd.Series(noise, index=result, name=D.columns[col_i])
|
|
109
|
+
df = pd.concat([df, multirow.to_frame().T])
|
|
110
|
+
df = df.fillna(0)
|
|
111
|
+
# Fix up the names for the rows (secrets)
|
|
112
|
+
df.index = df.index.set_names(D.columns.names)
|
|
113
|
+
return df
|
|
114
|
+
except ValueError:
|
|
115
|
+
print("Error: no such attribute name: ", attr)
|
|
116
|
+
except Exception as inst:
|
|
117
|
+
print(inst)
|
|
118
|
+
return None
|
|
119
|
+
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
import numpy as np
|
|
3
|
+
import pyqif as pyq
|
|
4
|
+
|
|
5
|
+
def intersection(C, D):
|
|
6
|
+
'''Constructs the intersection attack channel for an adversary who
|
|
7
|
+
has access to 2 channels C, D on the same inputs. Corresponds to a
|
|
8
|
+
parallel composition of channels.'''
|
|
9
|
+
#Pre: Channels C, D must have the same number of rows
|
|
10
|
+
CD = pyq.compose.parallel(C.to_numpy(), D.to_numpy())
|
|
11
|
+
# Recompute the columns
|
|
12
|
+
cols = np.array(np.meshgrid(C.columns.names, D.columns.names)).T.reshape(-1, 2)
|
|
13
|
+
return pd.DataFrame(data=CD, index=D.index, columns=cols)
|
|
14
|
+
|
|
15
|
+
def linkage(C, D):
|
|
16
|
+
'''Computes the linkage attack matrix which utilises the
|
|
17
|
+
correlation between observations on C and inputs on D.
|
|
18
|
+
'''
|
|
19
|
+
# Assume that C and D are channels created using make_channel
|
|
20
|
+
return C.dot(D)
|
|
21
|
+
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: data-privacy
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Utilities for ...
|
|
5
|
+
Requires-Python: >=3.9
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Dist: numpy
|
|
9
|
+
Requires-Dist: pandas
|
|
10
|
+
Requires-Dist: scipy
|
|
11
|
+
Dynamic: license-file
|
|
12
|
+
|
|
13
|
+
# A First Course in Data Privacy
|
|
14
|
+
|
|
15
|
+
This is the codebase supporting the textbook on privacy for undergraduates.
|
|
16
|
+
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
data_privacy/__init__.py
|
|
5
|
+
data_privacy/anonymity.py
|
|
6
|
+
data_privacy/channel.py
|
|
7
|
+
data_privacy/dataset.py
|
|
8
|
+
data_privacy/measure.py
|
|
9
|
+
data_privacy/mechanism.py
|
|
10
|
+
data_privacy/strategy.py
|
|
11
|
+
data_privacy.egg-info/PKG-INFO
|
|
12
|
+
data_privacy.egg-info/SOURCES.txt
|
|
13
|
+
data_privacy.egg-info/dependency_links.txt
|
|
14
|
+
data_privacy.egg-info/requires.txt
|
|
15
|
+
data_privacy.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
data_privacy
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "data-privacy"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Utilities for ..."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"numpy",
|
|
13
|
+
"pandas",
|
|
14
|
+
"scipy"
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[tool.setuptools.packages.find]
|
|
18
|
+
where = ["."]
|
|
19
|
+
include = ["data_privacy*"]
|