手把手:如何方便地使用Python和Pandas來匿名信息


手把手:如何方便地使用Python和Pandas來匿名信息

大數據文摘出品

編譯:毅航、胡笳、Aileen

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

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

任務


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

動手開始吧!


我使用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是一個方便的庫,減少了使用兩個包之間的差距。

sklearn_pandas:

https://github.com/scikit-learn-contrib/sklearn-pandas

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

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

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

Jupyter的實用小技巧:

https://www.dataquest.io/blog/jupyter-notebook-tips-tricks-shortcuts/

df = pd.read_csv("../data/titanic_train.csv")


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

數據集鏈接:

https://www.kaggle.com/c/titanic

df.shape
df.head()


手把手:如何方便地使用Python和Pandas來匿名信息


現在我們已經加載了數據,後面將刪除所有可識別個人身份的信息。列[“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()


手把手:如何方便地使用Python和Pandas來匿名信息


接下來,為了剔除更多信息,並作為後續步驟的預處理,我們將對“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()


手把手:如何方便地使用Python和Pandas來匿名信息


df.nunique()


手把手:如何方便地使用Python和Pandas來匿名信息


通過從同一分佈中抽樣來匿名化

上述代碼我打印了每列的唯一值的取值個數。我們假設具有少於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)



手把手:如何方便地使用Python和Pandas來匿名信息



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

處理連續變量

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

  • 使用預定義數量的區間來創建直方圖
  • 嘗試一系列連續函數,讓每個函數都去擬合該直方圖,擬合過程中會產生函數的參數。
  • 找到具有最小誤差(最小殘差平方和)的函數,該函數與該直方圖將被我們用來模擬連續變量分佈。


該解決方案的作者將所有內容整齊地分為兩個函數。我創建了第三個函數並將所有內容放在一個名為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


手把手:如何方便地使用Python和Pandas來匿名信息


手把手:如何方便地使用Python和Pandas來匿名信息


還不錯哦。

把代碼整合到一個函數中

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()


手把手:如何方便地使用Python和Pandas來匿名信息


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

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


手把手:如何方便地使用Python和Pandas來匿名信息


最後,大功告成。

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

相關報道:

https://dev.to/r0f1/a-simple-way-to-anonymize-data-with-python-and-pandas-79g


分享到:


相關文章: