天天看點

功能式Python中的探索性資料分析

這裡有一些技巧來處理日志檔案提取。假設我們正在檢視一些Enterprise Splunk提取。我們可以用Splunk來探索資料。或者我們可以得到一個簡單的提取并在Python中擺弄這些資料。

在Python中運作不同的實驗似乎比試圖在Splunk中進行這種探索性的操作更有效。主要是因為我們可以無所限制地對資料做任何事。我們可以在一個地方建立非常複雜的統計模型。

理論上,我們可以在Splunk中做很多的探索。它有各種報告和分析功能。

但是...

使用Splunk需要假設我們知道我們正在尋找什麼。在很多情況下,我們不知道我們在尋找什麼:我們正在探索。可能會有一些迹象表明,一些RESTful API處理速度很慢,但還不止于此。我們如何繼續?

第一步是擷取CSV格式的原始資料。怎麼辦?

讀取原始資料

我們将首先用一些附加函數來包裝一個CSV.DictReader對象。

面向對象的純粹主義者會反對這個政策。 “為什麼不擴充DictReader?”他們問。我沒有一個很好的答案。我傾向于函數式程式設計群組件的正交性。對于一個純粹的面向對象的方法,我們不得不使用更複雜的混合來實作這一點。

我們處理日志的一般架構是這樣的。

這使我們可以讀取CSV格式的Splunk提取物。我們可以疊代閱讀器中的行。這是訣竅#1。這不是非常棘手,但我喜歡它。

我們可以 - 在一定程度上 - 以有用的格式報告原始資料。如果我們想粉飾一下輸出,我們可以改變格式字元串。那就可能是“{主機:30s} {回複時間:8s} {來源:s}”或類似的東西。

過濾

常見的情況是我們提取了太多,但其實隻需要看一個子集。我們可以更改Splunk過濾器,但是,在完成我們的探索之前,過量使用過濾器令人讨厭。在Python中過濾要容易得多。一旦我們了解到需要什麼,就可以在Splunk中完成。

我們已經加入了一個生成器表達式來過濾源行,能夠處理一個有意義的子集。

投影

在某些情況下,我們會添加額外的源資料列,這些列我們并不想使用。是以将通過對每一行進行投影來消除這些資料。

原則上,Splunk從不産生空列。但是,RESTful API日志可能會導緻資料集中包含大量列标題,這些列标題是基于請求URI一部分的代理鍵。這些列将包含來自使用該代理鍵的一個請求的一行資料。對于其他行,在這一列中沒有任何用處。是以要删除這些空列。

我們也可以用一個生成器表達式來做到這一點,但是它會變得有點長。生成器函數更容易閱讀.

我們已經從原始閱讀器中的一部分項目建構了一個新的行字典。我們可以使用它來包裝我們的過濾器的輸出。

這将減少在for語句内部可見的未使用的列。

符号更改

row['source']符号會變得比較笨重。使用types.SimpleNamespace比用字典 更好。這使得我們可以使用row.source。

這是一個很酷的技巧來創造更有用的東西。

rdr_ns= (types.SimpleNamespace(**row) forrowinreader)

我們可以将其折疊成這樣的步驟序列。

請注意我們對format_map()方法的小改動。從SimpleNamespace的屬性中,我們添加了vars()函數來提取字典 。

我們可以用其他函數把它寫成一個函數來保留句法對稱性。

的确,我們可以把它寫成一個像函數一樣使用的lambda結構

雖然ns_reader()函數和ns_reader()lambda的使用方式相同,但為lambda編寫文檔字元串和doctest單元測試稍微困難一些。出于這個原因,應該避免使用lambda結構。

我們可以使用map(lambda row:types.SimpleNamespace(** row),reader)。有些人喜歡這個發生器表達式。

我們可以用一個适當的for語句和一個内部的yield語句,但是從一個小的東西裡寫大的語句似乎沒有什麼好處。

我們有很多選擇,因為Python提供了如此多的函數式程式設計功能。雖然我們不會經常把Python視作一種功能性語言。但我們有多種方法來處理簡單的映射。

映射:轉換和派生資料

我們經常會有一個非常明顯的資料轉換清單。此外,我們将有一個衍生的資料項目越來越多的清單。衍生項目将是動态的,并基于我們正在測試的不同假設。每當我們有一個實驗或問題,我們可能會改變派生的資料。

這些步驟中的每一個:過濾,投影,轉換和派生都是map-reduce管道的“map”部分的階段。我們可以建立一些較小的函數,并将其應用于map()。因為我們正在更新一個有狀态的對象,是以我們不能使用一般的map()函數。如果我們想實作一個更純粹的函數式程式設計風格,我們将使用一個不可變的namedtuple而不是一個可變的SimpleNamespace。

在我們探索的過程中,我們将調整這個轉換函數的主體。也許我們将從一些最小的轉換和派生開始。我們将用一些“這些是正确的?”的問題來繼續探索。當我們發現不工作時,我們會從中取出一些。

我們的整體處理過程如下所示:

請注意語句主體的變化。convert()函數産生我們确定的值。我們已經在for循環中添加了一些額外的變量,我們不能100%确定。在更新convert()函數之前,我們會看看它們是否有用(甚至是正确的)。

減量

在減量方面,我們可以采取稍微不同的加工方式。我們需要重構我們之前的例子,并把它變成一個生成器函數。

接着用一個yield代替了print()。

這是重構的另一部分。

理想情況下,我們所有的程式設計都是這樣的。我們使用生成器函數來生成資料。資料的最終顯示保持完全分離。這使我們可以更自由地重構和改變處理。

現在我們可以做一些事情,例如将行收集到Counter()對象中,或者可能計算一些統計資訊。我們可以使用defaultdict(list)按服務對行進行分組。

我們決定在這裡建立具體的清單對象。我們可以使用itertools按服務分組響應時間。它看起來像是正确的函數式程式設計,但是這種實施在Pythonic函數式程式設計形式中指出了一些限制。要麼我們必須對資料進行排序(建立清單對象),要麼在分組資料時建立清單。為了做好幾個不同的統計,通過建立具體的清單來分組資料通常更容易。

我們現在正在做兩件事情,而不是簡單地列印行對象。

建立一些局部變量,如svc和m。我們可以很容易地添加變化或其他措施。

使用沒有參數的vars()函數,它會從局部變量中建立一個字典。

這個使用vars()而沒有參數的行為就像locals()一樣是一個友善的技巧。它允許我們簡單地建立我們想要的任何局部變量,并将它們包含在格式化輸出中。我們可以侵入我們認為可能相關的各種統計方法中。

既然我們的基本處理循環是針對converted_log(“somefile.csv”)中的行,我們可以通過一個小小的,易于修改的腳本探索很多處理選擇。我們可以探索一些假設來确定為什麼某些RESTful API處理速度慢,而其他處理速度則很快。