如何用 Python 做出杜絕個資外洩風險,又不影響原始數據的數據集?

隱私 程式碼 工程師

【我們為什麼挑選這篇文章】要如何在資料中挑出敏感資訊,但同時又不能影響數據的分析結果呢?其實用 Python 跟 Pandas 就可以輕鬆做到。讓這篇文章手把手教你怎麼不影響數據集的匿名資訊。(責任編輯:陳伯安)

最近,我收到了一個數據集,其中包含有關客戶的敏感信息,這些信息在任何情況下都不應公開。數據集位於我們的一台服務器上,一個相當安全的地方。

但我想將數據複製到我的硬碟上,以便更方便地處理數據,同時又不希望擔心數據不安全。於是,我寫了一個改變數據的小腳本,同時仍然保留了一些關鍵訊息。我將詳細介紹我所採取的所有步驟,並重點介紹一些方便的技巧。

任務:製作一個不具敏感資訊的數據集,但又得反應原始資料的分佈

我們的任務是準備一個數據集,以便以後能用於機器學習(例如分類,回歸,聚類)而且不包含任何敏感信息。最終的數據集不應與原始數據集有太大差異,且應該反映原始數據集的分布。

手把手教學開始

我使用 Jupyter notebook 作為寫程式的環境。首先,讓我們引入所有必須的庫。

import pandas as pd
import numpy as np
import scipy.stats
%matplotlib inline
import matplotlib.pyplot as plt
from sklearn_pandas import DataFrameMapper
from sklearn.preprocessing import LabelEncoder
# get rid of warnings
import warnings
warnings.filterwarnings(
“ignore”)
# get more than one output per Jupyter cell
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity =
“all”
# for functions we implement later
from utils import best_fit_distribution
from utils import plot_result

我假設您已熟悉此處使用的大多數庫。我只想強調三件事。sklearn_pandas 是一個方便的庫,減少了使用兩個包之間的差距。

它提供了一個 DataFrameMapper 類,使得處理 pandas.DataFrame 更容易,因為它可以在更少的程式碼中完成變量的編碼轉換。

我利用 IPython.core.interactiveshell … 更改了 Jupyter Notebook 默認配置,用來顯示多個輸出。這裡有一篇很好的部落格介紹了其他關於 Jupyter 的實用 小技巧

最後,我們將一些程式碼放入一個名為 utils.py 的文件中,我們把這個文件放在 Notebook 程式碼文件旁邊。

df = pd.read_csv(“../data/titanic_train.csv”)

我們的分析採用 Titanic Dataset 的訓練 數據集

df.shape
df.head()

現在我們已經加載了數據,後面將刪除所有可識別個人身份的信息。列 [「PassengerId」,「Name」] 包含此類信息。請注意,[「PassengerId」,「Name」] 對於每一行都是唯一的,因此如果構建機器學習模型,無論如何都需要在後續刪除它們。

同樣對 [「Ticket」,「Cabin」] 列也進行類似的操作,因為這兩列對於每一行幾乎都是唯一的。

出於演示方便,我們不會處理缺失值。我們只是忽略所有包含缺失值的觀察結果。

df.drop(columns=[“PassengerId”, “Name”], inplace=True) # dropped because unique for every row
df.drop(columns=[
“Ticket”, “Cabin”], inplace=True) # dropped because almost unique for every row
df.dropna(inplace=True)

結果看起來像這樣。

df.shape
df.head()

接下來,為了剔除更多信息,並作為後續步驟的預處理,我們將對「Sexed」和「Embarked」進行數值編碼轉換。

「Sex」被編碼為「0,1」,「Embarked」被編碼為「0,1,2」。LabelEncoder() 類為我們完成了大部分工作。

encoders = [([“Sex”], LabelEncoder()), ([“Embarked”], LabelEncoder())]

mapper = DataFrameMapper(encoders, df_out=True)

new_cols = mapper.fit_transform(df.copy())

df = pd.concat([df.drop(columns=[“Sex”, “Embarked”]), new_cols], axis=“columns”)

DataFrameMapper 來自 sklearn_pandas 包,接收元組(tuple)列表作為參數,其中元組的第一項是列名,第二項是轉換器。

我們在這裡使用 LabelEncoder(),但也可以使用其它轉換器(MinMaxScaler(),StandardScaler(),FunctionTransfomer())。

在最後一行中,我們將編碼後的數據與其餘數據連接起來。請注意,您也可以寫 axis = 1,但是 axis =「columns」可讀性更強,我鼓勵大家使用後者。

df.shape
df.head()

df.nunique()

確定概率分布,再用此數據抽樣來做匿名化

上述程式碼我設了每列的唯一值的取值個數。我們假設具有少於 20 個取值個數的是名義變量或分類變量,具有大於等於 20 個取值個數的都是連續變量。

我們將名義/分類變量放在一個列表中,將其它變量放在另一個列表中。

categorical = []
continuous = []
for c in
list(df):
col = df[c]
nunique = col.nunique()
if nunique <
20:
categorical.append(c)
else:
continuous.append(c)

for c in list(df):迭代所有列。對於 list(df),我們也可以寫成 df.columns.tolist()。我還是喜歡 list(df)。

以下是本文的核心思想:對於每個分類變量,我們將計算其每項取值出現的頻率,然後為每個取值創建具有相同頻率的離散概率分布。

對於每個連續變量,我們將從預定義的分布列表中確定最佳連續分布。我們怎麼做呢?一旦確定了所有概率分布(離散和連續),我們就可以從這些分布中進行採樣以創建新的數據集。

處理分類變量

這是一個簡單的例子,只用三行程式碼。

for c in categorical:
counts = df[c].value_counts()
np.random.choice(
list(counts.index), p=(counts/len(df)).values, size=5)

首先,我們確定變量中每個唯一值出現的頻率。然後我們使用這個經驗概率函數並將其傳遞給 np.random.choice() 以創建一個具有相同概率函數的新隨機變量。

處理連續變量

幸運的是,StackOverflow 上有一個類似問題的討論。主要解決方案如下,對於每個連續變量做如下處理:

1. 使用預定義數量的區間來創建直方圖

2. 嘗試一系列連續函數,讓每個函數都去擬合該直方圖,擬合過程中會產生函數的參數。

3. 找到具有最小誤差(最小殘差平方和)的函數,該函數與該直方圖將被我們用來模擬連續變量分布。

該解決方案的作者將所有內容整齊地分為兩個函數。我創建了第三個函數並將所有內容放在一個名為 utils.py 的文件中,後面將在 Jupyter Notebook 中使用 utils.py 中定義的函數。

best_distributions = []
for c in continuous:
data = df[c]
best_fit_name, best_fit_params = best_fit_distribution(data,
50)
best_distributions.append((best_fit_name, best_fit_params))
# Result
best_distributions = [
(
‘fisk’, (11.744665309421649, -66.15529969956657, 94.73575225186589)),
(
‘halfcauchy’, (-5.537941926133496e-09, 17.86796415175786))]

Age 的最佳分布是 fisk,Fare 的最佳分布是 halfcauchy,讓我們來看看結果。

plot_result(df, continuous, best_distributions

還不錯哦。

將程式碼整合到函數裡

def generate_like_df(df, categorical_cols, continuous_cols, best_distributions, n, seed=0):
np.random.
seed(seed)
d = {}
for c in categorical_cols:
counts = df[c].value_counts()
d[c] = np.random.choice(
list(counts.index), p=(counts/len(df)).values, size=n)
for c, bd in zip(continuous_cols, best_distributions):
dist = getattr(scipy.stats, bd[
0])
d[c] = dist.rvs(size=n, *bd[
1])
return pd.DataFrame(d, columns=categorical_cols+continuous_cols)

現在我們有了一個函數,可以用它來創建 100 個新的觀測值。

gendf = generate_like_df(df, categorical, continuous, best_distributions, n=100)
gendf.shape
gendf.head()

作為後置處理步驟,還可以對連續變量進行取捨。我選擇不這樣做。我所做的是刪除了所有列名,因為這也可能洩漏有關數據集的一些信息,簡單地用 0,1,2…替換它們。

gendf.columns = list(range(gendf.shape[1]))

最後,大功告成。

gendf.to_csv(“output.csv”, index_label=“id”)

這種方法的一個缺點是變量之間的所有交互都丟失了。例如,假設在原始數據集中,女性(Sex= 1)存活的機會(Survived= 1)比男性(Sex= 0)高,而在生成的數據集中,這個信息丟失了,其它變量之間可能存在的關係也會丟失。

文中所有程式碼:

https://github.com/r0f1/dev_to_posts/tree/master/fake_data

(本文經合作夥伴 大數據文摘 授權轉載,並同意 TechOrange 編寫導讀與修訂標題,原文標題為 〈手把手:如何方便地使用 Python 和 Pandas 来匿名信息 〉,首圖來源:Pxhere, CC Licensed。)

延伸閱讀

程式語言手刀必存:Python 的數據類該如何理解?
為什麼 Python 這麼慢?比其他程式語言慢十倍的病根就在「全局解釋器鎖」
美國工程師花一週寫 Python,用一支機械手臂毀了「威利在哪裡?」這個遊戲


點關鍵字看更多相關文章: