最近,我收到了一個資料集,其中包含有關客戶的敏感資訊,這些資訊在任何情況下都不應公開。資料集位於我們的一臺伺服器上,一個相當安全的地方。
但我想將資料複製到我的本地磁碟上,以便更方便地處理資料,同時又不希望擔心資料不安全。於是,我寫了一個改變資料的小指令碼,同時仍然保留了一些關鍵資訊。我將詳細介紹我所採取的所有步驟,並重點介紹一些方便的技巧。
任務
我們的任務是準備一個資料集,以便以後能用於機器學習(例如分類,迴歸,聚類)而且不包含任何敏感資訊。最終的資料集不應與原始資料集有太大差異,且應該反映原始資料集的分佈。
動手開始吧!
我使用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()
現在我們已經載入了資料,後面將刪除所有可識別個人身份的資訊。列[“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上有一個類似問題的討論。主要解決方案如下,對於每個連續變數做如下處理:
使用預定義數量的區間來建立直方圖
嘗試一系列連續函式,讓每個函式都去擬合該直方圖,擬合過程中會產生函式的引數。
找到具有最小誤差(最小殘差平方和)的函式,該函式與該直方圖將被我們用來模擬連續變數分佈。
該解決方案的作者將所有內容整齊地分為兩個函式。我建立了第三個函式並將所有內容放在一個名為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)高,而在生成的資料集中,這個資訊丟失了,其它變數之間可能存在的關係也會丟失。
我希望你發現這篇文章有用,可以在文末留言區討論。