Python數據分析之pandas初體驗
寫這篇筆記的念頭起於在工作、學習Python數據分析時,一些需要用到的功能在網路上找不到有效直觀的解決方法,找到的主題相關的鏈接,大多數都是以R或者MATLAB為基礎的。於是想要為豐富Python數據分析資料盡點綿力,方便其他像我這樣有需要但又找不到現有答案的人。
本文主要陳述3個問題:
- 按列內容篩選數據
- DataFrame數據合併
- DataFrame行和列的轉換
此外還涉及到一丟丟的作圖
0. 環境需求及源數據
0.1 環境需求
本文使用Python科學計算環境WinPython中的Jupyter Notebook,以前名為Ipython Notebook,這東西已經包含了Python解釋器,所以完全可以把它當普通的Python環境來使用。同時它還自帶了很多科學計算、數據挖掘和數據分析、數據處理相關的包。此外,除了可以寫代碼(支持語法高亮)外,它還支持Markdown寫作。所以當前這篇文章也是在這Notebook裡面完成的。
0.2 源數據
為了方便有需求的朋友進行體驗測試,我把本文中用到的源數據文件打包放在了網路中,可點此下載。數據保存於Excel文件中,共三份文件,每個文件以其事件發生月份命名,各包含一個含有待處理數據的工作表,各包含約1000行數據。為方便後面進行陳述,我們可以先看一下數據的結構:
這是一份產品銷售數據,其中A列是客戶名稱,B列是產品代號,C列是銷售地區,D、E兩列是無用的干擾數據,F列是銷量。
本文以每月每個客戶共賣出多少產品(銷量)及銷量最大的前5個客戶分別是誰為中心開展數據處理工作
1. 數據規整
數據規整,也就是把源數據格式化為我們想要的、方便Python處理的格式。在本例中,有3個地方需要處理:
- 去除無用信息。由上圖中可以看出,數據表中前面5行是一些說明信息,對我們而言是沒有實際用處的,並且不是結構化的
- 適當的填充。而A列是已經在Excel中進行了單元格合併的,這樣的數據並不適合Python進行處理,我們要對它進行拆分並且進行相應的填充
- 增加日期信息。我們要按月份進行統計,但數據中並沒有日期信息
首先先導入所需要的包並切換到工作目錄下:
In [1]:
import pandas as pdnimport numpy as npnimport matplotlibnimport osnnos.chdir(r"E:TEMPFY1314")n
先獲取文件對象
In [2]:
files = [x for x in os.listdir() if x.endswith(xlsx)]n
查看files列表是否包含了所有待處理文件
In [3]:
filesn
Out [3]:
[2013-12.xlsx, 2014-01.xlsx, 2014-02.xlsx, ~$FY1415_comb.xlsx]n
如上,在我工作中,每次使用這樣的方法來獲取一個目錄下的所有excel文件時,如果該目錄下曾經有其它文件,而後來被刪除掉,則在讀取時,它依然會被讀取到,可能是還留存在內存中沒有被釋放。這裡我們要先把最後一個去掉:
In [4]:
files.pop()n
Out [4]:
~$FY1415_comb.xlsxn
然後,我們要把三個文件中的有效內容取出併合併到同一個DataFrame中,以方便處理。原理是分別讀取3個文件中的有效數據到pandas的DataFrame中,再利用pandas的concat()函數進行合併。
In [5]:
# 用於保存3個DataFrame的字典n# 其鍵是文件名稱(不包含後綴),值是對應的數據nall_dict = {}nfor each_file in files:n # 獲取月份——即文件名作為字典的鍵n key_month = each_file[:-5]n # 分別讀取每個EXCEL文件n df = pd.read_excel(each_file, sheetname="Sheet1", header=5) n # 因為我們只需要查看客戶和銷量數據,所以其它列可以不要n # 此處取出Customer 和 Shipments兩列數據放到待操作的DataFrame中 n all_dict[key_month] = df.ix[:,[Customer, Shipments]]n # 添加日期列,命名為 Datan all_dict[key_month][Date] = key_monthn # 填充Customern filled_customer = all_dict[key_month][Customer].fillna(method=ffill)n # 用填充後的Customer列代替舊列n all_dict[key_month][New_customer] = filled_customern all_dict[key_month].drop(Customer, axis=1, inplace=True) nn# 把已放到字典中的3個DataFrame取出並放到列表中,方便後面進行合併 ndf_list = [all_dict[x] for x in all_dict.keys()]n# 合併3個DataFramencomb_df = pd.concat(df_list) n
在read_excel()中指定header=5可以略過前面的5行,將第6行作為DataFrame的頭(header,其實DataFrame是沒有「頭」的說法的,這裡是「列名稱」的意思)。這樣,去除無用信息這一工作就完成了。
接下來是使用DataFrame對象的ix方法進行切片,這裡使用了切片而不是直接取列(df[[Customer, Shipments]])的原因切片比取列更快,因為取列其實是複製,返回的是DataFrmae的視圖。
填充Customer列使用了pandas中Series和DataFrame的空值填充方法fillna(),指定填充方法為ffill,即向前填充。到這裡,進行適當的填充這一工作也完成了。
此時,我們先查看一下合併後數據結構是怎麼樣的:
In [6]:
# 用DateFrame.head()方法可以查看DataFrame的前幾行信息,默認為前5行(不包含表頭)ncomb_df.head()n
Out[6]:
再看一下各列數據的類型
In [7]:
comb_df.info()n
Out [7]:
<class pandas.core.frame.DataFrame>nInt64Index: 3245 entries, 0 to 1099nData columns (total 3 columns):nShipments 3207 non-null float64nDate 3245 non-null objectnNew_customer 3245 non-null objectndtypes: float64(1), object(2)nmemory usage: 101.4+ KBn
可以看到,我們添加進去的「日期」列,其實還不是日期,而是object類型。但這裡並不影響我們進行分析,如果有需要,可以將它轉換成真正的時間序列。具體可參考pandas文檔。此時,我們的數據規整就結束了,接下來,可以開始進行處理了
2. 數據透視表
用過Excel處理數據的人應該都知道它有個強大的工具叫數據透視表,利用它可以方便快捷地按行和列來觀察數據,其中的數據可以是求和或頻率統計等等的結果。Pandas也有類似的功能,而且更加強大,個性化程度更高。
這裡我們利用前面已經規整好的數據製作數據透視表,並最終得到銷量Top 5客戶及其總銷量。這裡有兩種方法
- 直接求得每個客戶每個月的銷售額,然後新增一列,其值為3個月銷售額的和,再以總和列進行排序
- 先求每個客戶在這三個月里的總銷售額並排序得到Top 5客戶,取出它們的值。再製作一份每個客戶每月銷售額的透視表,篩選出前面已得到的Top 5客戶的行
下面分別進行演示
方法1:
In [8]:
pt1 = comb_df.pivot_table(index=[New_customer], columns=[Date], aggfunc=sum, fill_value=0)n
這裡用New_customer列作為索引,Date為列,匯總功能為求和sum,並使用0對空值進行填充。得到的透視表其實也還是一個DataFrame,可以先看一下它的數據結構和屬性信息
In [9]:
pt1.head()n
Out[9]:
In [10]:
pt1.info()n
Out [10]:
<class pandas.core.frame.DataFrame>nIndex: 25 entries, YA to WUnData columns (total 3 columns):n(Shipments, 2013-12) 25 non-null objectn(Shipments, 2014-01) 25 non-null objectn(Shipments, 2014-02) 25 non-null objectndtypes: object(3)nmemory usage: 800.0+ bytesn
可以看到,它的列名(鍵)是一個個元組,所以我們在對它的列進行操作時要傳入的就是元組。接下來我們新增一列,放於放置三個月銷量的總和
In [11]:
pt1[Total] = pd.to_numeric(pt1[(Shipments, 2013-12)]) + pd.to_numeric(pt1[(Shipments, 2014-02)]) + pd.to_numeric(pt1[(Shipments, 2014-01)])n
由前面的屬性信息可以看到,pt1的每一列是一個非空對象,所以如果我們直接拿這三列進行相加,是會出現TypeError的,所以要先用pandas的to_numeric()方法將其轉換成數值,再進行求和運算。
11月30日更新:
對於求各列的和,今天學到了種更加方便的方式,不需要逐個指定列,代碼如下:
pt1[Total] = pt1.apply(lambda x: x.sum(), axis=1)n
注意必須要指定axis=1以實現對列應用第1個參數中指定的函數,默認是axis=0也就是對行應用函數。
再來看一下現在的透視表是怎樣的
In [13]:
pt1.head()n
Out[13]:
現在已經得到了匯總信息,我們需要以它進行排序,DataFrame對象有一個sort_values()方法用於對值進行排序,默認是升序排列,可以傳入一個ascending=False參數以使用降序排列
In [13]:
pt1.sort_values(Total, ascending=False, inplace=True)n
此時,我們只需要取pt1的前5行,即可得到Top 5客戶及其每月銷量數據
In [14]:
Top5 = pt1.iloc[:5]nTop5n
Out[14]:
注意這裡的切片方法iloc[]和前面的ix[]不同,具體可查看pandas文檔
方法2
先得到3個月的總銷售額,再取出銷量總額前5的客戶名稱
In [15]:
pt2 = comb_df.pivot_table(index=[New_customer], aggfunc=sum, fill_value=0)npt2.head()n
Out[15]:
In [16]:
pt2.sort_values(Shipments, ascending=False, inplace=True)ntop5_cs = pt2.index[:5]ntop5_cs = list(top5_cs)ntop5_csn
Out[16]:
[MA, AU, WA, CA, ME]n
這裡已經得到前5客戶的名字並轉為列表格式(方便後面進行篩選),然後再製作按月排列的透視表
In [17]:
pt3 = comb_df.pivot_table(index=[New_customer], columns=[Date], aggfunc=sum, fill_value=0)n
目前pt3是以客戶名稱為索引,所以我們只需取出索引值等於列表top5_cs中的幾行即可
In [18]:
Top_5 = pt3[pt3.index.isin(top5_cs)]nTop_5n
Out[18]:
使用pandas數據對象的`isin()`方法可以很方便地根據列值進行篩選。除了像上面的對索引列進行篩選外,也可以對各數據列進行篩選。如下面的代碼可以篩選出`(Shipments, 2013-12)`列中值為0的行
In [19]:
# 以下代碼也等價於:pt3[pt3[(Shipments, 2013-12)] == 0],適用於篩選少量數據的時候npt3[pt3[(Shipments, 2013-12)].isin([0])]n
Out[19]:
但這裡應當提出的是,如果在這裡直接用pt3[pt3[(Shipments, 2013-12)].isin([176.567, 62.4604])]來試圖篩選(Shipments, 2013-12)列中值為176.567和62.4604的行是會出錯的,因為這裡得到的數值是具有一定的誤差的(不過不用擔心,這裡的誤差出現在小數點後面的好多位那裡,通常不會影響到計算結果)。如果直接用pt.ix[AU, (Shipments, 2013-12)]取AU行和(Shipments, 2013-12)列的值就會發現它並不完全等同於176.567。
3. 作圖
得到Top 5客戶及其每月銷售總量數據後,我們就可以製作折線圖。本例中,由於我在準備數據的時候是隨機性選擇的,所以導致了數據中沒有一個客戶是連續三個月都有銷量的,也就是數據出現了斷層,這樣的數據就不適用於折線圖了(可以用散點圖)。好在pandas具有很好的數據處理功能,可以幫我們處理這個問題。可以利用pandas的填充功能對Top 5客戶每月銷量數據進行填充。
這裡為了方便起見,我使用往後填充的方法對空值進行填充,對於(Shipmetns, 2014-02)列則只能使用向前填充的方法。但由於我們前面在做透視表時,使用了0填充空值,這時候就不能直接使用fillna()進行填充了,因為這時候Top5裡面並沒有NA值,所以我們要先把0替換成NA,再使用fillna()填充。
在這裡我使用了第2種方法得到的Top_5進行作圖,因為它沒有Total列,更加方便
In [20]:
Top_5.replace(0, np.nan, inplace=True)n
這時再來看看Top_5,它的0已經被替換為NaN了
In [21]:
Top_5n
Out[21]:
再進行填充
In [22]:
Top_5 = Top_5.fillna(method=bfill)nTop_5 = Top_5.fillna(method=ffill)n
In [23]:
Top_5n
Out[23]:
DataFrame有個plot()方法可以直接利用DataFrame中的數據進行繪圖,默認以索引列為橫坐標,縱坐標則會根據數值範圍自行選擇。但如果我們在這時直接用Top_5.plot()進行繪圖,會發現結果並不是我們想要的
In [24]:
%matplotlib inlinenTop_5.plot()n
Out[24]:
<matplotlib.axes._subplots.AxesSubplot at 0x242e7c99160>n
%matplotlib inline 這一行是為了繪圖需要而添加的,如果不寫,則運行後不會在下面顯示圖片
DataFrame.plot()函數是以列進行繪圖的,而我目前為止也還沒發現能設置按行進行繪圖。所以我們必須要把DataFrame對象的行和列進行置換,然後再進行繪圖
In [25]:
Top_5_stack = Top_5.stack()n
轉換後的結果是這樣子的:
In [26]:
Top_5_stackn
Out[26]:
明顯依然不是我們想要的,但已經有點像了,這時候我們只需要把New_customer列轉換到列標上去就行了,使用stack的逆方法unstack並指定列名(即key)即可
In [27]:
Top_5_stack = Top_5_stack.unstack(New_customer)nTop_5_stackn
======================================================================
2017.3.20更新。今天在操作數據的時候,發現原來Pandas本身有提供類似的方法了,不需要通過stack再unstack,應該是我在寫這篇文章的時候給忘了。在這裡可以這麼完成:
Top5.Tn
使用這個T方法即可得到上面的Top_5_stack
======================================================================
Out[27]:
然後就可以做圖了
In [28]:
Top_5_stack.plot(figsize=(10,8))n
Out[28]:
<matplotlib.axes._subplots.AxesSubplot at 0x242e7eac2b0>n
plot()中的figsize()接收一個元組參數,指定圖的寬和高。由於數值差別有點大,所以這圖不太好看。plot()還提供了一個sublots參數,用於設置是把所有列都放在一個折線圖中還是分開繪圖。我們來試試
In [29]:
Top_5_stack.plot(subplots=True, figsize=(8,12), sharex=False, sharey=False, title="Shipments Line Chart")n
Out[29]:
array([<matplotlib.axes._subplots.AxesSubplot object at 0x00000242E7F85E80>,n <matplotlib.axes._subplots.AxesSubplot object at 0x00000242EA6FD898>,n <matplotlib.axes._subplots.AxesSubplot object at 0x00000242EA505CF8>,n <matplotlib.axes._subplots.AxesSubplot object at 0x00000242EA547080>,n <matplotlib.axes._subplots.AxesSubplot object at 0x00000242EA590C18>], dtype=object)n
sharex和sharey參數用於設置是否使用相同的x坐標和y坐標,默認是True,如果使用默認,則會是這樣子的
In [29]:
Top_5_stack.plot(subplots=True, figsize=(8,12))n
Out[29]:
array([<matplotlib.axes._subplots.AxesSubplot object at 0x00000242EA00FA20>,n <matplotlib.axes._subplots.AxesSubplot object at 0x00000242EA19DCC0>,n <matplotlib.axes._subplots.AxesSubplot object at 0x00000242EA1EA5C0>,n <matplotlib.axes._subplots.AxesSubplot object at 0x00000242EA226160>,n <matplotlib.axes._subplots.AxesSubplot object at 0x00000242EA271278>], dtype=object)n
本文到此結束
關於pandas的其它用法可查找pandas手冊。而關於作圖這裡,我在工作中作圖時還遇到了一些問題尚未找到解決方法
未解決的問題
- 無法在折線圖上顯示數值
- 無法操作圖例位置
- x軸上顯示的坐標被省略了一部分
最後一點在本例中沒有出現,但是如果時間跨度變得稍大一點時,則會出現這樣的情況。我在工作中處理的時間大概為16個月,在作圖時就只顯示了8個,如1、3、5、7、9。希望後面可以解決,或者有懂的同行還望告知一聲
=========================================
16年12月30日更新:
今晚學到一種方法,可以解決第上面第3個問題,但是使用這種方法需要在作圖時使用plt.plot()的方法,然後再進行設置。原理是先獲取當前圖例的坐標軸對象,再進行設置
# 獲取坐標軸對象nax = plt.gca()nnax.plot(傳入數據) nn# 如果是時間序列畫圖,還應該使用plot_date()方法n# 還可以使用ax.autofmt_xdate()方法使橫坐標數值自適應。水平顯示還是垂直、傾斜nn# ax對象的locator_paramsx()方法可用於設置x軸的區間數量n# 下面這行代碼就把x軸劃分為5個區間。即類似於0, 2, 4, 6, 8, 10這樣的序列值n# 把x改為y,則可對y軸進行設置nax.locator_params(x, nbins=5)n
此時可以針對ax對象進行設置坐標軸。這裡不演示了,有需要的朋友可以自己去看官方文檔。
此外,使用xlim()方法可以設置橫坐標的起始值和終值,ylim()則用於設置縱軸。這個是可以在使用DataFrame對象的plot()方法時直接傳入的:
df.plot(xlim(0, 20), ylim(0, 100))n
至於第2個問題,圖例位置。如果使用plt.plot()的方式進行作圖,也是有一定自由度可以去設置的。即使用plt.legend()方法:
plt.legend(loc=0)n
loc參數的0,表示自適應。1為右上角,2為左上角,3為左下角, 4為右下角。還有居中靠左、居中靠右等,需要查看官方文檔了
2017年9月7日更新
為了避免X軸數據被簡化,還可以在plot()中指定參數x_compat=True,問題迎刃而解。
推薦閱讀:
※用R替換數據
※數據分析師之必備Excel使用技巧1-6
※在前端MVC越來越成熟Ajax大量運用的今天,傳統的MVC等數據處理完畢再顯示的方式有何優勢?
※實驗數據中是否可以捨去少數顯著不合理的部分?判據是怎樣的?
※5萬多行數據用excel做地圖經常卡死,除了換電腦,還有什麼好的方法解決?