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

大資料文摘發表於2018-09-28

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

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

任務

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

動手開始吧!

我使用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)高,而在生成的資料集中,這個資訊丟失了,其它變數之間可能存在的關係也會丟失。

我希望你發現這篇文章有用,可以在文末留言區討論。

相關文章