目前在煙草物流中心的項目規劃及設計中廣泛應用的數據分析工具主要有Excel、R、SQL Server等。其中,Excel對可處理的數據行、列數都有明顯限制。以Excel 2016版為例,最多可處理1048576行、16384列的數據,更適用于小型數據的簡單分析;而商業軟件SQL Server的可伸縮性有限,當多用戶連接時性能會變差,更適用于數據類型相對規整的中小型數據;R在統計方面表現突出,但處理上GB數據時的運行速度較為緩慢,對于數據類型復雜的大型數據來說,其內含包如data.table、smartdata、cleanerR等也不如Python的工具集使用靈活。
Python作為一款免費的開源軟件,在Windows、MacOS、Linux等操作系統中都有其對應的版本,具有代碼簡潔、可讀性高等優勢,其中的pandas工具集提供了大量處理數據的函數和方法,用于數據挖掘和數據分析,同時也提供數據清洗功能。pandas擁有更高的數據交換性,對xlsx、csv、mdf、txt、html等格式數據提供統一的導入模塊,并轉換為DataFrame格式數據;通過參數設置既能導入少量的數據進行預覽,又能只導入需求的列進行分析;具有良好的交互模式,可以一個命令即時返回一個結果,也可以一次性運行所有命令得到最終結果;此外,煙草物流中心的訂單分批現象給處理日期數據增加了難度,pandas則可以自動解析目標列中類似日期格式的數據,為以日期為分組依據的數據聚合運算提供便利。
數據處理為數據分析及方案規劃打下基礎,原始數據中一般會含有大量冗余數據,比如同時存在條煙編號列和條煙名稱列,且為一一對應關系,在導入數據時,只需保留數值型的條煙編號列以減少內存占用;再比如數據中含有多列關于客戶信息的數據、且每一列都無法作為客戶信息識別的唯一標識時,在保證其對應關系的前提下將幾列或所有關于客戶信息的數據列合并成為新的一列,并指定為“客戶編號”,用作唯一標識客戶信息的數據列。因此在數據分析前,一般會先根據需求設定目標列,然后導入對應的原始數據,最后對數據進行清洗和整理。
對煙草物流中心的銷售數據進行處理時,通常把目標列設為:訂單日期、訂單編號、客戶編號、品規及銷售數量等,如果需要區分異型煙,可再多增加一列異型煙標記列。
本文以某煙草物流中心一年的實際銷售數據data2020.csv為例,詳細說明如何使用pandas 0.24.2版來實現數據的導入、基本清洗及整理。
由于pandas是Python的工具集之一,正常情況下,在使用前都需要安裝大量的工具包,安裝步驟也非常復雜,而使用基于Python的Anaconda Navigator軟件作為數據處理工具可以避免上述問題。Anaconda Navigator是一個開源的集成了大量工具包的工具合集,可以一次性安裝所有運行Python時需要的各種工具包,降低了安裝難度。
打開Anaconda Navigator中的Jupyter Notebook,在原始數據data2020.csv所在文件夾中新建一個Jupyter Notebook工程文件,利用import pandas as pd命令導入所需的pandas工具集。
數據預覽是在正式導入數據前,對數據進行的簡單查看,其主要有兩個目的:一是確認數據是否可以使用pandas進行導入;二是查看是否含有不需要導入的冗余數據列,以及是否有需要進行合并等二次處理的數據列。
大部分數據分析軟件不具備數據預覽的功能,只能一次性導入所有數據,一旦報錯就會被迫終止導入。而pandas工具集在這一方面存在天然的優勢,可通過參數設置導入指定行數據來對原始數據進行實驗性導入,幫助我們對數據建立基本認識。
pandas中提供了多種導入數據的函數,并且都可以實現預覽目的,可根據不同數據格式選擇對應的導入函數。本文使用pd.read_csv()函數及其提供的部分參數實現對數據的預導入,參數如下:
nrows:設置導入行數,例如nrows=5,通過查看前五行數據,快速判斷數據是否存在編碼、格式等問題,以及是否需要導入所有列;
skiprows:設置需要忽略的行數或要跳過的行號列表,當樣本數量很大時,可結合函數進行隨機抽取,比如隨機抽取1%的行數。

圖1 編碼格式報錯

圖2 數據預覽

圖3 數據導入結果
在上文新建的Jupyter Notebook文件中,導入data2020.csv的前五行數據預覽并將其命名為“df”,代碼如下:

返回結果,如圖1。
由圖1可知,因為文件自身的編碼與pandas默認采用的“utf-8”解碼方式不相符,從而使“utf-8”編碼解碼器無法解碼該數據,所以返回報錯提示,可通過添加參數encoding來解決。在含有中文編碼的情況下,除常用的“gbk”編解碼器外,還有“gb18030”、“hz”、“big5”等。
修改代碼如下:

運行后,系統不再報錯,使用df.head()函數查看成功導入的前五行數據,返回結果如圖2。
因此,在正式導入數據前,利用參數nrows或skiprows進行數據預導入,可避免因編解碼器錯誤、數據類型復雜等問題帶來的重復操作,以提高數據導入效率。
通過簡單預覽,我們可得出:
(1)該數據沒有列名;
(2)第七列產品名稱列與第六列產品編號列含義相同,因此第七列可當做冗余數據,無需導入,以加快速度、節省內存占用;
(3)前三列分別代表年、月、日,可合并為新的一列并轉換為日期格式。
但完整數據中可能還會存在預覽中無法看出的問題,比如是否存在缺失值、異常值等問題,需要在導入所有行數據后再進行判斷。
以上主要介紹了pd.read_csv()函數中常用于數據預覽的兩個參數,本章中繼續使用此函數中的以下參數對數據進行正式導入,并解決列名及冗余數據問題,減少導入的數據量,加快數據處理速度:
usecols:設置導入指定列,可以是列名或對應的索引值。在此數據中,以目標列為基礎導入,需要導入的列索引值為:usecols=[0,1,2,3,4,5,7,8];
header:指定第幾行作為列名稱,默認為0,若如本文中數據需要另外指定列名,此參數設置為None;
names:指定列名,當header=None時,添加此參數設置列名。
綜上,數據導入代碼如下:

查看返回結果,如圖3。
由圖3結果可知,目標列數據已導入且命名成功,數據預覽中的問題也得到解決。但返回一個第0列及第5列為混合類型的警示:因為在pandas內部以塊的形式處理文件,會降低解析時的內存使用量,但如果在一列中有不止一個數據類型存在時,將被系統判斷為混合類型并返回此警示,可以設置“low_memory=False”,或利用dtype參數指定每列的數據類型,但會占用大量內存和時間,可以等數據全部導入后再進行列數據類型的指定。
通過df.head()函數查看數據前五行發現:數據中的“month”、“day”、“qty”及“customer_code”等四列均被轉換為浮點類型,由于pandas不支持存儲含有缺失值的整型及布爾型數組,當引入含有缺失值的整型數組時,根據pandas對數據的處理規則,該列數據將被轉換為浮點型,因此可以推斷出這四列數據中可能含有缺失值,需要進行單獨驗證。
至此數據導入工作已經完成,除導入本文數據需要的參數外,pd.read_csv()函數中還有許多其他常用參數可以在導入數據時快速設置:
sep:指定分隔符,若其他非逗號分隔符的文件,也可以通過修改該參數導入;
index_col:指定列數據作為行索引,默認值為None;
na_filter:默認為True,是否檢查丟失值(空字符串或空值),在沒有任何缺失值的數據中,傳遞參數na_filter=False可提高讀取大文件的速度;
skip_blank_lines:默認為True,忽略空白行而不是解析為NaN;
error_bad_lines:默認為True,若有含大量字段的行(例如大量逗號)在默認情況下會引發異常,并且不會返回任何DataFrame,若error_bad_lines=False,這些“異常行”將不被導入;
warn_bad_lines:默認為True,如果上一個參數設為False,而warn_bad_lines=True,則將為每個“異常行”輸出警告;

圖4 info()函數返回值

圖5 提取缺失值

圖6 刪除缺失值
parse_dates:指定一列或多列字符串合并解析為日期格式。
數據處理是在數據成功導入后,對數據進行的缺失值及無效值清洗、數據類型轉換、日期格式處理等一系列操作,以達到規整數據的目的,為接下來的數據分析做準備。
利用pandas對數據處理之前,可以先通過info()函數查看數據的詳細信息,例如數據類型、空值及內存占用情況等,info()函數中的常用參數有以下幾個:

圖7 轉換類型及結果查詢
(1)本數據共14023517行,除“year”列以外的其他列都只含有14023515個非空值行,說明除“year”列以外的其他列中都含有2個空值;

圖8 數據描述結果

圖9 提取負值

圖10 數據保存圖
(2)數據類型存在問題,例如“year”、“month”、“day”和“qty”列應轉換為int整型,“customer_code”列在預覽導入中可見共12位整數,應轉換為int64整型;
(3)內存占用為3.4GB,其中,最后一列數據為異型煙標識列,代表是否為異型煙,存在大量重復字段且唯一值少,可目錄化為category類型,不但可以繼續顯示原內容為閱讀提供方便,而且可以在占用更少內存的同時兼顧運行速度。
在已經導入的原始數據中可能會存在少量缺失值,這會使某些函數及代碼無法運行或增加其運行時長,例如使用to_datetime()函數將某列或某幾列轉換為日期格式時,當數據中存在缺失值時就會報錯。因此,為使數據分析結果的準確性更高,需要對這些可能存在的缺失值進行優先處理。
首先利用isnull()函數和any()函數提取數據中的缺失值,設置any()函數中的參數axis=1選取含有缺失值的行,觀察其分布情況,判斷缺失值是否對數據分析結果有影響,使用代碼如下:
df[df.isnull().any(axis=1)]
查看返回結果,如圖5。
由圖5可知,除“year”列以外的其他列中的缺失值均位于14023515和14023516兩行,在判定不影響整體分析結果的情況下,將此兩行數據刪除;若缺失值較多或對數據分析結果有影響,則需根據實際情況對缺失值進行填充。
接下來再利用dropna()函數刪除缺失值,相關參數如下:
how:當整行或整列中有至少一個缺失值或全為缺失值時,通過此設置確定刪除方法,默認“any”,刪除存在缺失值的行或列;“all”,刪除全為缺失值的行或列;
axis:確定按行向下判斷還是按列向右判斷,默認axis=0,按行向下判斷;
inplace:默認False,若inplace=True,直接在原始數據上進行;
subset:刪除指定列中含有缺失值的行或指定行中含有缺失值的列。
刪除缺失值并查看結果,代碼如下:

memory_usage:是否顯示DataFrame中所有元素(包括索引)的總內存使用情況,“True”,始終顯示內存情況,“False”不顯示內存情況,“deep”,精確計算內存使用情況;
null_counts:是否顯示非空計數,“True”,始終顯示非空數量,“False”,不顯示非空數量。
使用代碼如下:

查看返回結果,如圖4。
由圖4可知:

返回結果,如圖6。
由圖6可知,再次運行df.info()函數后所有列的行數相同,說明數據中已無缺失值。
在pandas中通常利用astype()函數實現對數據類型的轉換,先返回一個結果作為預覽,并不會改變原始數據,因此在轉換時可先運行代碼查看轉換結果,確認無誤后再通過賦值的方法對原始數據進行更改。
按照上文中提到的分別將“year”、“month”、“day”和“qty”列轉換為int整型,“customer_code”列轉換為int64整型,“special_type”列轉換為category類型,代碼如下:

全部數據列類型轉換成功后,繼續運行代碼df.info()函數查看內存占用情況,返回結果如圖7。
由圖7可知,轉換后的內存占用為1.8GB,比轉換前減少了47%,因此在分析大型數據前盡量把中文字符處理成占用字節更少的數據類型,可有效減小內存占用,提升運行速度。
除上述操作以外,數據中可能還存在異常值,比如本文數據中的“month”列中是否有大于12的數值,比如“day”列是否有大于31的數值,再比如“qty”列中是否有零或負值等,可以使用df.select_dtypes()函數來提取上述可能含有異常值的整形列并判斷其最值情況,代碼如下:

圖11 日期設為索引

圖12 方式一運行

圖13 方式二運行

圖14 數據保存圖

返回結果如圖8。
由圖8可知,只有“qty”列的最小值為-50,不符合實際情況,利用代碼“df[df['qty']<=0]”將“qty”列中所有小于或等于零的數據全部提取出來,返回結果如圖9。
由圖9可知,“qty”列中只存在一個負值,由于負值的數量很少,可直接作為異常值進行刪除,或根據實際分析需求進行處理。
在分析基于時間序列的數據時,經常會碰到日期格式處理和轉換問題,而pandas有著強大的日期數據處理功能,將其設置為整個數據的索引,更方便于利用resample()函數對數據進行頻率轉換和時間序列重采樣等操作。下文將介紹兩種情況下將數據轉換為日期格式的方法:
(1)中小型數據集
運用read_csv()函數中參數parse_dates,在導入數據時直接將一列(如20181018、2018-10-18、2018/10/18等)或多列數據合并解析(以本文數據為例parse_dates={'date':['year','month','day']}),可快速將數據轉換為日期格式。但該方法在解析時間格式時對內存占用較大,數據導入時間較長,且如果待解析列格式不統一或存在空值、異常值時,將不會轉換為日期格式,只能作為普通數值類型返回,因此這個方法常適用于數據量較小的情況。
(2)大型數據集
當數據量很大且日期格式不標準時,可以在導入目標數據后,使用to_datetime()函數將DataFrame中指定的一列或幾列合并后轉換為datetime日期格式,且to_datetime可以解析多種不同的日期表示形式,常用參數如下:
errors:對錯誤的處理方式,默認為“raise”,無效解析將返回異常;“coerce”,無效解析設置為NaT;“ignore”,忽略無效解析返回輸入值;
dayfirst:當待解析的對象為字符串或列表時,指定日期解析順序,默認為False,若為True,優先解析day,例如10/11/12將被解析為2012-11-10;
yearfirst:與dayfirst同理,若為True,優先解析year,例如10/11/12將被解析為2010-11-12;
format:指定日期格式。
在本文數據中處理日期格式數據過程代碼如下:


返回結果,如圖10和圖11。
為更直觀的感受上述兩種方式在運行速度上的差別,新建一個Jupyter Notebook文件并導入pandas,借助%time函數來分別計算兩種方式的代碼運行時間。本文用于實現的電腦基本配置為:Windows 7 64位操作系統、Intel Xeon CPU E5-1650 V3處理器、16G內存、128G固態硬盤。
第一種方式的代碼如下:

運行結果,如圖12。
第二種方式的代碼如下:

運行結果,如圖13。
由圖12可知,第一種方式先用上文中提到的parse_dates參數合并日期列,再導入完整數據的所用時長為27分15秒;由圖13可知,第二種方式先直接導入數據,然后再分步處理日期得到與第一種方式的相同結果,需要注意的是在上文中已知數據存在空值行,為避免to_datetime()函數報錯,在合并日期數據前,先對空值行進行刪除,所有代碼的運行時間加和約為45秒。綜上所述,對于含有需要合并的日期列且數據量大的數據來說,采用第一種方式雖然可以一次性解決日期數據問題,但邏輯不如第二種清晰,且讀取csv文件的時間成倍增加,大約是第二種方式的36倍,此時優先選擇第二種方式更能節省整體的處理時間。
pandas工具集不但可以導入多種格式的數據,而且能根據數據處理情況將數據保存成多種格式。
若數據未處理完,可保存為hdf格式來快速暫存數據處理的中間結果。在將hdf格式數據保存到硬盤的過程中,會保留數據在內存中的排列順序,以及列數據的格式類型,數據的保存和讀取速度都十分迅速。
若數據已經完成處理,可保存為txt、hdf、csv等格式,其中csv格式數據可以被大多數電子表格和數據庫系統支持,具有較高的通用性,因此將本文中處理好的數據也保存為csv格式數據。
pandas中一般使用df.to_csv()函數將已處理好的數據保存成csv格式,常用參數如下:
sep:指定分隔符,csv文件默認為逗號,若其他非逗號分隔符的文件,也可以通過修改該參數保存;
header:是否保存列名,默認為True,當值設為零時,不保存列名;
index:是否保存索引,默認為True,當值設為False時,不保存索引;
index_label:設置索引對應的列名,默認為None;
encoding:設置編碼方式,默認編碼方式為utf-8。
保存本文整理好的df數據,并將其命名為“data2020-1”,代碼如下:
df.to_csv('data2020-1.csv')
數據保存結果,如圖14。
由圖14可知,處理后的數據不但文件大小得到改善,而且具有更高的交換性及準確性,可以直接用其他數據分析軟件或仿真軟件進行數據分析,也可以繼續用pandas分析,節省導入時間的同時提高效率。
本文通過使用pandas對煙草物流中心的實際銷售數據進行導入、清洗、整理工作,將類型不規則、存在缺失值的數據規整為數據類型固定且只含有目標列的數據,為數據分析、挖掘及可視化奠定基礎。從導入和處理過程中可以看出,基于Python編程語言的pandas工具集的代碼可讀性更高,可以快速簡便地實現少量行數據或需求列數據的導入,導入速度也優于其他數據分析工具,其中內置的函數和方法可以更加高效的對大型數據集進行處理。但由于pandas處理數據時是把數據存儲于計算機的內存中,如果數據量太大,比如數據量高達幾百GB時,考慮內存限制也不適合用pandas工具集進行導入。煙草物流中心規劃項目的特殊性使不同項目的原始數據間存在一定的相似性,因此引用的函數和方法可以對不同項目的原始數據進行重復使用,也可以利用其編程語言的靈活性,針對特殊問題編寫個性化的函數使用,提高工作效率,具有良好的通用性和實用性。