數據分析實戰(六):英國電商用戶行爲分析

案例:英國電商用戶行爲數據分析

Part 1. 數據獲取

1.1 數據集簡介
https://archive.ics.uci.edu/ml/datasets/online+retail#
該數據集爲英國在線零售商在2010年12月1日至2011年12月9日間發生的所有網絡交易訂單信息。
1.2 數據集內容
數據集爲xlsx格式,文件大小22.6M。數據共計8個字段,541908條。具體字段如下:

InvoiceNo:發票編號。爲每筆訂單唯一分配的6位整數。若以字母'C'開頭,則表示該訂單被取消。
StockCode:產品代碼。爲每個產品唯一分配的編碼。
Description:產品描述。
Quantity:數量。每筆訂單中各產品分別的數量。
InvoiceDate:發票日期和時間。每筆訂單發生的日期和時間。
UnitPrice:單價。單位產品價格,單位爲英鎊。
CustomerID:客戶編號。爲每個客戶唯一分配的5位整數。
Country:國家。客戶所在國家/地區的名稱。

在這裏插入圖片描述

Part 2. 提出問題

根據數據集提出問題如下:

1.訂單維度:筆單價和連帶率是多少?訂單金額與訂單內商品件數的關係如何?

2.客戶維度:客單價是多少?客戶消費金額與消費件數的關係如何?

3.商品維度:商品的價格定位是高是低?哪種價位的商品賣得好?哪種價位的商品帶來了實際上最多的銷售額?

4.時間維度:各月/各日的銷售情況是什麼走勢?可能受到了什麼影響?

5.區位維度:客戶主要來自哪幾個國家?哪個國家是境外主要市場?哪個國家的客戶平均消費能力最強?

6.客戶行爲:客戶的生命週期、留存情況、購買週期如何?

根據上述問題按如下思路進行分析:
在這裏插入圖片描述

Part 3. 清洗數據

數據清洗部分思路如下:
在這裏插入圖片描述
3.0 導入數據

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
sales_df = pd.read_excel('./Online Retail.xlsx', sheet_name='Online Retail')

再查看一下各列的數據類型:
在這裏插入圖片描述
3.1 列名重命名
觀察到字段InvoiceDate並非只包含了日期信息,同時也涵蓋了具體的時分秒維度,故將其重命名爲InvoiceTime:

# 列名重命名
sales_df.rename(columns={'InvoiceDate': 'InvoiceTime'}, inplace= True)

3.2 刪除重複值
我們規定,若所有字段的值都完全相同,則視爲重複數據,僅保留一條。
刪除重複值,並根據去重前後的行數變化計算重複值數量:

# 刪除重複值
rows_before = sales_df.shape[0]  # 留意pandas對象的shape屬性
sales_df.drop_duplicates(inplace= True)
rows_after = sales_df.shape[0]
print('原行數:', rows_before, '現行數:', rows_after, '刪除行數:', rows_before - rows_after)

在這裏插入圖片描述
由於重複數據的刪除,此時索引值和行數已不相符,進行索引的重設,並刪除原索引:

# 重設索引,並刪除原索引
sales_df.reset_index(drop=True, inplace = True)  # 如果沒有drop的話,會保留原索引

3.3 缺失值處理

# 查看缺失值
sales_df.isnull().sum()

在這裏插入圖片描述
Description是商品的文字描述,不是我們的分析重點,存在1454個空值,不予處理。

CustomerID是客戶的唯一編號,很重要,缺失了135037行,將近總行數的1/5。但並不能直接刪去有缺失值的行,會影響整體銷售情況的分析。也無法採用插值法。姑且用‘0’來填充。雖說客戶ID都是五位數字,但填充前還是先確認下是否真的不存在ID爲‘0’的客戶。

sales_df[sales_df['CustomerID'] == '0']
# 填充缺失的CustomerID
sales_df['CustomerID'].fillna('0', inplace=True)

3.4 一致化處理
3.4.1 時間相關信息的一致化

首先將InvoiceTime轉爲pandas能處理的時間格式datetime:

# 一致化處理 
sales_df['InvoiceTime']=pd.to_datetime(sales_df['InvoiceTime'], errors='coerce')

再新增字段Date存放InvoiceTime中的日期部分:

sales_df['Date'] = pd.to_datetime(sales_df['InvoiceTime'].dt.date, errors='coerce')

新增Month存放月份信息:

sales_df['Month'] = sales_df['InvoiceTime'].dt.month  # 這樣得到month依舊是一個整形數據

由於我們對時間相關字段的操作有可能產生缺失值,再次查看缺失值情況:

sales_df.isnull().sum()

在這裏插入圖片描述
3.4.2 重新規整數字類型
將UnitPrice轉爲浮點型,Quantity和CustomerID轉爲整型:

sales_df['Quantity'] = sales_df['Quantity'].astype('int32')
sales_df['UnitPrice'] = sales_df['UnitPrice'].astype('float')
sales_df['CustomerID'] = sales_df['CustomerID'].astype('int32')
sales_df['InvoiceNo'] = sales_df['InvoiceNo'].astype('str') # 後面檢查C字段訂單的時候,發現這裏需要是字符串類型

增加字段SumPrice用於存放該行數據的總價:

# 計算總價
sales_df['SumPrice'] = sales_df['Quantity'] * sales_df['UnitPrice']

3.5 異常值處理
爲了探究是否存在異常值,我們查看總體的描述性統計情況:

sales_df.describe()

在這裏插入圖片描述
發現Quantity數量、UnitPrice單價、SumPrice總價都存在負值的情況,且絕對值較大。而總價是由數量和單價相乘得到,總價爲負實際上也是數量和單價二者之一爲負值導致的。

故查看數量或單價非正值的數據:

sales_df[(sales_df['Quantity'] <= 0)|(sales_df['UnitPrice'] <= 0)]

在這裏插入圖片描述
初步瀏覽,注意到主要是 ①C字頭的被取消訂單 和 ②單價爲0的免費訂單 導致的異常。
3.5.1 C字頭的取消訂單
取消訂單產生的負值在之後銷售情況的分析中會產生干擾,考慮將sales_df分爲只含成功訂單和只含取消訂單兩部分。
我們需要探究取消訂單是直接在原訂單上進行的修改,還是用來抵消原訂單的新增數據。以上圖第一行爲例來說,即是否存在536379訂單,與C536379訂單對應。(注意這種思考的邏輯)

將sales_df分爲成功訂單和取消訂單兩部分,劃分依據是發票編號InvoiceNo是否含有“C”:

query_c = sales_df['InvoiceNo'].str.contains('C')
# 只含取消訂單
sales_cancel = sales_df.loc[query_c,:].copy()
# 只含成功訂單
sales_success = sales_df.loc[-query_c,:].copy()

爲sales_cancel增加字段SrcInvoiceNo,用於存放去掉“C”的發票編號:

# 增加原訂單號
sales_cancel['SrcInvoiceNo'] = sales_cancel['InvoiceNo'].str.split('C', expand=True)[1]

將sales_cancel和sales_success進行合併:

print('merge之前,sales_cancel的shape爲:{}'.format(sales_cancel.shape))
print('merge之前,sales_success的shape爲:{}'.format(sales_success.shape))

new_data = pd.merge(sales_cancel, sales_success, left_on='SrcInvoiceNo',right_on='InvoiceNo')
print('merge之後,new_data的shape爲:{}'.format(new_data.shape))

在這裏插入圖片描述
可以確認發現取消訂單和成功訂單並無對應關係。
原來的536641行數據中,取消訂單佔了9251行。

3.5.2 單價爲0的免費訂單
推測單價爲0的訂單是促銷活動的贈品,對於訂單量、件單價、連帶率等指標的計算造成干擾,故也單獨分出一張表存放,之後再對免費訂單進行分析:

query_free = sales_success['UnitPrice'] == 0
# 只含免費訂單
sales_cancel = sales_success.loc[query_free,:].copy()
# 只含普通訂單
sales_success = sales_success.loc[-query_free,:]

print(sales_success.describe())

在這裏插入圖片描述
注意到還有一類異常訂單,單價爲負值。

3.5.3 單價爲負的訂單
查詢出此類訂單,並消除:

query_minus = sales_success['UnitPrice'] < 0
sales_success = sales_success.loc[-query_minus, :]

print('至今,sales_success還有數據:{}'.format(sales_success.shape))
print(sales_success.describe())

在這裏插入圖片描述
至此數據清洗告一段落,sales_success還餘524878行數據:!

數據可視化之前的所有code:

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt

# 讀入數據
sales_df = pd.read_excel('./Online Retail.xlsx', sheet_name='Online Retail')

# 列名重命名
sales_df.rename(columns={'InvoiceDate': 'InvoiceTime'}, inplace=True)


# 刪除重複值
rows_before = sales_df.shape[0]
sales_df.drop_duplicates(inplace=True)
rows_after = sales_df.shape[0]

# 填充空值
sales_df['CustomerID'].fillna('0', inplace=True)

# 增添新列(日期和月份)
sales_df['InvoiceTime'] = pd.to_datetime(sales_df['InvoiceTime'], errors='coerce')
sales_df['Date'] = pd.to_datetime(sales_df['InvoiceTime'].dt.date, errors='coerce')
sales_df['Month'] = sales_df['InvoiceTime'].dt.month

# 數據類型轉換
sales_df['Quantity'] = sales_df['Quantity'].astype('int32')
sales_df['UnitPrice'] = sales_df['UnitPrice'].astype('float')
sales_df['CustomerID'] = sales_df['CustomerID'].astype('int32')
sales_df['InvoiceNo'] = sales_df['InvoiceNo'].astype('str')

# 計算總價
sales_df['SumPrice'] = sales_df['Quantity'] * sales_df['UnitPrice']



# 拆分訂單
query_c = sales_df['InvoiceNo'].str.contains('C')

# 只含取消訂單
sales_cancel = sales_df.loc[query_c, :].copy()
# 只含成功訂單
sales_success = sales_df.loc[-query_c, :].copy()



# 增加原訂單號
sales_cancel['SrcInvoiceNo'] = sales_cancel['InvoiceNo'].str.split('C', expand=True)[1]


new_data = pd.merge(sales_cancel, sales_success, left_on='SrcInvoiceNo',right_on='InvoiceNo')

query_free = sales_success['UnitPrice'] == 0
# 只含免費訂單
sales_free = sales_success.loc[query_free, :].copy()
# 只含普通訂單
sales_success = sales_success.loc[-query_free, :]


query_minus = sales_success['UnitPrice'] < 0
sales_success = sales_success.loc[-query_minus, :]

print('至今,sales_success還有數據:{}'.format(sales_success.shape))

Part 4. 分析與可視化

4.1 銷售情況的描述性統計

4.1.1 訂單維度
首先將sales_success按訂單號分組,對Quantity商品數量和SumPrice總價分組求和:

invoice_grouped = sales_success.groupby('InvoiceNo')[['Quantity', 'SumPrice']].sum()

在這裏理解一個東西,字段InvoiceNo是訂單ID,字段StockCode 是產品ID,但是有可能一個訂單會有多種產品,因此會有重複的InvoiceNo,在這裏以536365爲例,打印一下:
在這裏插入圖片描述
通過describe獲得筆單價(每筆訂單的平均交易金額)和連帶率(每筆訂單平均購買的產品件數):

# 筆單價 與 連帶率
# 筆單價 = 總銷售額 / 總筆數
# 連帶率 = 售出商品總數 / 總筆數
invoice_grouped.describe()

在這裏插入圖片描述
統計區間(2010年12月1日-2011年12月9日)內共產生有效訂單19960筆,筆單價爲533.17英鎊,連帶率約爲279件,說明以批發性質的訂單爲主。訂單交易金額和訂單內商品件數,其均值都高於中位數;訂單交易金額的均值甚至高於Q3分位數。說明訂單總體差異大,存在部分購買力極強的客戶。

接下來繪製訂單交易金額的分佈圖:

invoice_grouped['SumPrice'].hist(bins = 100, figsize = (12, 4), color = 'c')
plt.title('SumPrice Distribution of Orders')
plt.ylabel('Frequency')
plt.xlabel('SumPrice')

在這裏插入圖片描述
部分訂單交易金額過大,影響圖表的可讀性,篩去1000英鎊及以上的訂單:

invoice_grouped[invoice_grouped.SumPrice < 1000]['SumPrice'].hist(bins = 100, figsize = (12, 4), color = 'c')
plt.title('SumPrice Distribution of Orders (Below 500)')
plt.ylabel('Frequency')
plt.xlabel('SumPrice')

在這裏插入圖片描述
訂單金額集中在400英鎊內,三個峯值分別爲20英鎊內、100-230英鎊、300-320英鎊。其中300-320英鎊的訂單數量特別多,不知道是否存在某種共性,之後可以進行進一步探究。

對訂單內商品數量的分佈同樣繪製柱形圖,並篩去2000件及以上的訂單:

invoice_grouped[invoice_grouped.Quantity < 2000]['Quantity'].hist(bins = 50, figsize = (12, 4), color = 'c')
plt.title('Quantity Distribution of Orders  (Below 2000)')
plt.ylabel('Frequency')
plt.xlabel('Quantity')

在這裏插入圖片描述
訂單內的商品數量呈現出很典型的長尾分佈,大部分訂單的商品數量在250件內,商品數量越多,訂單數相對越少。

爲了進一步探究訂單交易金額與訂單內商品件數的關係,我們繪製散點圖:

plt.figure(figsize=(14,4))
# plt.subplot用於繪製子圖,121表示分成1*2個圖片區域,佔用第一個。
plt.subplot(121)
plt.scatter(invoice_grouped['Quantity'], invoice_grouped['SumPrice'], color = 'c')
plt.title('SumPrice & Quantity')
plt.ylabel('SumPrice')
plt.xlabel('Quantity')

# 篩去商品件數在20000及以上的訂單
plt.subplot(122)
plt.scatter(invoice_grouped[invoice_grouped.Quantity < 20000]['Quantity'], invoice_grouped[invoice_grouped.Quantity < 20000]['SumPrice'], color = 'c')
plt.title('SumPrice & Quantity (Quantity < 20000)')
plt.ylabel('SumPrice')
plt.xlabel('Quantity')
plt.show()

在這裏插入圖片描述
總體來說訂單交易金額與訂單內商品件數是正相關的,訂單內的商品數越多,訂單金額也相對越高。但在Quantity靠近0的位置也有若干量少高價的訂單,後續可以試探究。

4.1.2 客戶維度
僅對含有CustomerID的客戶進行分析:

sales_customer = sales_success[sales_success['CustomerID'] != 0].copy()

統計各個客戶的訂單數量、消費金額和商品購買數。先按客戶ID和訂單編號分組,對同筆訂單的商品數量和銷售金額求和;再用reset_index重設索引;最後再按客戶ID分組:(這個邏輯無敵)

在這裏插入圖片描述
人均購買筆數爲4筆,中位數爲2筆,25%以上的客戶僅下過一次單,並未留存。每位客戶平均購買了1187件商品,甚至超過了Q3分位數,最多的客戶購買了196915件;客單價爲2049英鎊,平均值同樣超過了Q3分位數,說明客戶的購買力存在較大差距,存在小部分的高消費用戶拉高了人均數值。

進一步觀察客戶消費金額的分佈:

customer_grouped.SumPrice.hist(bins = 50, figsize = (12, 4), color = 'b')
plt.title('SumPrice Distribution of Customers')
plt.ylabel('Frequency')
plt.xlabel('SumPrice')
plt.show()

在這裏插入圖片描述
從直方圖看,大部分用戶的消費能力確實不高,高消費用戶在圖上幾乎看不到。這也確實符合消費行爲的行業規律。

截取消費額5000英鎊以內的客戶:

plt.figure()
customer_grouped[customer_grouped.SumPrice < 5000].SumPrice.hist(bins = 60, figsize = (12, 4), color = 'b')
plt.title('SumPrice Distribution of Customers (Below 5000)')
plt.ylabel('Frequency')
plt.xlabel('SumPrice')
plt.show()

在這裏插入圖片描述
與前面訂單金額的多峯分佈相比,客戶消費金額的分佈呈現單峯長尾形態,金額更爲集中,峯值在83-333英鎊間。

繪製客戶消費金額與消費件數的散點圖:

plt.figure(figsize=(14,4))
plt.subplot(121)
plt.scatter(customer_grouped['Quantity'], customer_grouped['SumPrice'], color = 'b')
plt.title('SumPrice & Quantity')
plt.ylabel('SumPrice')
plt.xlabel('Quantity')

plt.subplot(122)
plt.scatter(customer_grouped[customer_grouped.Quantity < 25000]['Quantity'], customer_grouped[customer_grouped.Quantity < 25000]['SumPrice'], color = 'b')
plt.title('SumPrice & Quantity (Quantity<25000)')
plt.ylabel('SumPrice')
plt.xlabel('Quantity')
plt.show()

在這裏插入圖片描述
客戶羣體比較健康,而且規律性比訂單更強,同時擁有一定數量消費能力強的用戶。總體來說客戶的消費金額與購買的商品數量是正相關的,客戶購買的東西越多,消費金額相對就越高。

4.1.3 商品維度
根據觀察,發現相同的商品在不同的訂單中單價不同,可知商品的單價會發生波動,以商品10002爲例:

# 要確認好使 數值類型 還是 字符串類型
sales_success.loc[sales_success['StockCode'] == 10002,:].UnitPrice.value_counts()

所以接下來求每件商品的平均價格,思路是平均價格=該商品的總銷售額 / 該商品的銷售數量。具體來說,先按商品編號進行分組,對數量和總價分別求和,即得到對應商品的總銷售金額和總銷售量,取商即得到平均價格:

goods_grouped = sales_success.groupby('StockCode')[['Quantity', 'SumPrice']].sum()
goods_grouped['AvgPrice'] = goods_grouped['SumPrice'] / goods_grouped['Quantity']
goods_grouped.head()

在這裏插入圖片描述
查看所有商品AvgPrice的分佈,觀察這家店的價格定位:

goods_grouped.AvgPrice.hist(bins=100)
plt.title('AvgPrice Distribution')
plt.ylabel('Frequency')
plt.xlabel('SumPrice')
plt.show()

在這裏插入圖片描述
發現商品價位基本上全部集中在100英鎊內,出現了極少量的天價商品影響觀測,將其篩去:

plt.figure()
goods_grouped[goods_grouped.AvgPrice < 100].AvgPrice.hist(bins=100,figsize = (12, 4))
plt.title('AvgPrice Distribution (Below 100)')
plt.ylabel('Frequency')
plt.xlabel('SumPrice')
plt.show()

在這裏插入圖片描述
峯值是1-2英鎊,單價10英鎊以上的商品已經很少見,看來該電商的定位主要是價格低的小商品市場。

接下來查看商品單價和商品銷量的散點圖,可以看出哪種價位的商品更受歡迎:

# 商品單價和銷售數量的散點圖
plt.figure(figsize=(14,4))
plt.subplot(121)
plt.scatter(goods_grouped['AvgPrice'], goods_grouped['Quantity'], color = 'r')
plt.title('AvgPrice & Quantity')
plt.ylabel('Quantity')
plt.xlabel('AvgPrice')

plt.subplot(122)
plt.scatter(goods_grouped[goods_grouped.AvgPrice < 50]['AvgPrice'], goods_grouped[goods_grouped.AvgPrice < 50]['Quantity'], color = 'r')
plt.title('AvgPrice & Quantity (AvgPrice < 50)')
plt.ylabel('Quantity')
plt.xlabel('AvgPrice')
plt.show()

在這裏插入圖片描述
從商品的銷量上來看,毫無疑問是低於5英鎊的低價區商品大獲全勝,受到了客戶們的喜愛。

那麼是否價格低廉的商品也帶來了實際上最多的銷售額呢?不妨繪製商品單價和商品總銷售額的散點圖:

# 商品單價和銷售金額的散點圖
plt.figure(figsize=(14,4))
plt.subplot(121)
plt.scatter(goods_grouped['AvgPrice'], goods_grouped['SumPrice'], color = 'r')
plt.title('AvgPrice & SumPrice')
plt.ylabel('SumPrice')
plt.xlabel('AvgPrice')

plt.subplot(122)
plt.scatter(goods_grouped[goods_grouped.AvgPrice < 50]['AvgPrice'], goods_grouped[goods_grouped.AvgPrice < 50]['SumPrice'], color = 'r')
plt.title('AvgPrice & SumPrice (AvgPrice < 50)')
plt.ylabel('SumPrice')
plt.xlabel('AvgPrice')
plt.show()

在這裏插入圖片描述
低價區的商品笑到了最後,不僅在銷售數量上一騎絕塵,也構成了銷售額的主要部分;高價的商品雖然單價高昂,但銷量很低,並沒有帶來太多的銷售額。據此,建議平臺採購部門可以多遴選售價低於10英鎊的產品,來進一步擴充低價區的品類。

4.1.4 時間維度
按訂單號分組,提取出我們需要的信息:

time_grouped = sales_success.groupby('InvoiceNo').agg({'Date': np.min, 'Month': np.min, 'Quantity': np.sum, 'SumPrice': np.sum}).reset_index()

這裏Date和Month取最小值或最大值都可以,因爲都是相同的。(這是同一個訂單,時間當然相同)

以月份爲單位進行折線圖繪製,這裏對Quantity和SumPrice分組求和,代表每月的銷量和銷售額,對InvoiceNo計數,代表每月的訂單數。此處採用雙座標圖,銷量和銷售額爲左軸,參數secondary_y = 'InvoiceNo’表示訂單數爲右軸:

month = time_grouped.groupby('Month').agg({'Quantity': np.sum, 'SumPrice': np.sum, 'InvoiceNo': np.size}).plot(secondary_y = 'InvoiceNo', x_compat=True, figsize = (12, 4))
month.set_ylabel('Quantity & SumPrice')
month.right_ax.set_ylabel('Order quantities')
plt.show()

在這裏插入圖片描述
需要注意此處2011年12月僅統計了前9天,如果全月能基本保持前9天的銷售情況,銷售額會遠超2010年同期。

觀察到三條折線總體上呈現相近的趨勢,除了2011年2月和4月略低外,2010年12月至2011年8月基本維持相近的銷售情況;隨後在9月-11月連續增長,達到高峯。考慮該電商平臺主營禮品,受節日影響可能較大。歐洲重視的萬聖節(11月1日)和聖誕節(12月25日)都在年末,與圖中的趨勢能夠相呼應;同時雖然感恩節(11月第4個週四)是美國節日,但“黑五”的營銷方式對全球都產生了一定影響。

將日期設爲索引,按日繪製折線圖:

plt.figure()
time_grouped = time_grouped.set_index('Date')
day = time_grouped.groupby('Date').agg({'Quantity': np.sum, 'SumPrice': np.sum, 'InvoiceNo': np.size}).plot(secondary_y = 'InvoiceNo', figsize = (15, 5))
day.set_ylabel('Quantity & SumPrice')
day.right_ax.set_ylabel('Order quantities')
plt.show()

在這裏插入圖片描述
可見銷量Quantity和銷售額SumPrice的趨勢是極趨同的,這也和前一節中分析出該電商以低價商品爲主相吻合,商品單價低且價位集中,則銷售額主要隨銷量變化而漲跌。

注意到在最後一天(即2011年12月9日),銷量、銷售額顯著激增,我們放大看看:

# 節取2011101日至2011129日
plt.figure()
day_part = time_grouped['2011-10-01':'2011-12-09'].groupby('Date').agg({'Quantity': np.sum, 'SumPrice': np.sum, 'InvoiceNo': np.size}).plot(
    secondary_y = 'InvoiceNo', figsize = (15, 5))
day_part.set_ylabel('Quantity & SumPrice')
day_part.right_ax.set_ylabel('Order quantities')
plt.show()

在這裏插入圖片描述
2011年12月的前8天基本延續了11月下旬的銷售趨勢,但在12月9日訂單量大幅下降時,卻創造了樣本區間內銷量和銷售額的歷史新高。說明存在某筆或某幾筆購買量極大的訂單,從而使得銷售額大幅上升。

將當日的銷售詳單拉取出來:

sales_success[sales_success.Date == '2011-12-09'].sort_values(by='SumPrice', ascending=False).head()

在這裏插入圖片描述
破案了,有一個英國的客戶,一口氣購買了8萬餘件的紙工藝品,貢獻了168469.60英鎊的銷售額。建議對大客戶配備固定客服,好及時獲知對方的需求與意見,並增加客戶的認同感。不過反過來說,12月之後的20余天應該是無法保持這麼迅猛的銷售勢頭了。

4.1.5 區位維度
首先提取出一張客戶ID及其國家的關係表:

sales_country = sales_success.drop_duplicates(subset=['CustomerID', 'Country'])[['CustomerID', 'Country']]

按客戶分組,計算消費總額:

country_grouped = sales_success.groupby('CustomerID')[['SumPrice']].sum().reset_index()

將上述兩張表合併:

country_grouped = pd.merge(country_grouped, sales_country, left_on='CustomerID', right_on='CustomerID')

按國家再次分組,計算出各國客戶的消費總額和客戶總數:

country_grouped = country_grouped.groupby('Country').agg({'SumPrice': np.sum, 'CustomerID': np.size})

新增AvgAmount字段,用於存放該國家客戶的人均消費金額:

country_grouped['AvgAmount'] = country_grouped['SumPrice'] / country_grouped['CustomerID']

對消費總額降序排列:

country_grouped.sort_values(by='SumPrice',ascending=False).head(20)  

在這裏插入圖片描述
可知絕大部分客戶仍來自英國本土,主要境外收入來源也多爲英國周邊國家,基本上符合以英國爲圓心向外輻射的情況。這種現象可能和運輸成本及語言等有關,也可能是影響力隨距離而衰減,可以嘗試增加境外的宣傳投放,提高知名度;同時建議網站做好多國語言的適配,也可以在網站上對於境外物流費用計算及手續辦理等事項給出更易懂的說明。

4.2 客戶消費行爲分析

4.2.1 客戶的生命週期
該數據集的統計對象爲2010年12月1日至2011年12月9日的全部訂單,我們這節研究的客戶是指所有成功的普通訂單中有CustomerID的客戶,不包含未記錄CustomerID的客戶,下同。

其實樣本中的客戶有許多未進行完整的生命週期 ,我們並不知道在統計時段前他們是否購買過,也不知道在統計時段後他們中的哪些會繼續購買。所以這裏計算的生命週期是有侷限性的,真實的客戶平均生命週期必然會更長。

首先篩選出CustomerID不爲空的客戶(NaN之前被我們填充爲0):

sales_customer = sales_success[sales_success['CustomerID'] != 0].copy()

查看用戶的初次與末次(最近)消費時間:

# 客戶的初次消費時間
mindate = sales_customer.groupby('CustomerID')[['Date']].min()
# 客戶的末次消費時間
maxdate = sales_customer.groupby('CustomerID')[['Date']].max()

查看用戶的初次/末次消費集中在哪些日期:

mindate.Date.value_counts().head(10)
maxdate.Date.value_counts().head(10)

在這裏插入圖片描述
發現初次消費的高頻日期爲統計時段的初期,末次消費的高頻日期爲統計時段的末期。說明有大量用戶的生命週期被低估,實際上還要向前向後延伸。

末次消費日期減去初次消費日期得到統計時段內的生命週期,展示前5行:

(maxdate - mindate).head()

在這裏插入圖片描述
0 days表示該客戶只在某一天內消費過,未能留存。

再看一下客戶生命週期的總體情況:

life_time = maxdate - mindate
life_time.describe()

在這裏插入圖片描述
共有4338個有CustomerID的客戶,其平均生命週期爲130天,中位數則是93天,說明有部分生命週期很長的忠實客戶拉高了均值;而最小值和Q1分位數都爲0天,說明存在25%以上的客戶僅消費了一次,生命週期的分佈呈兩極分化的狀態。

接下來,繪製柱形圖觀察客戶生命週期的實際分佈。

這裏的時間差是timedelta類型,無法繪製柱形圖,將其先轉化爲數值:

life_time['life_time'] = life_time['Date'].dt.days  # .dt.days提取出來就是數值了!

繪製20個分組的柱形圖:

life_time['life_time'].hist(bins=20, color='c')
plt.title('Life Time Distribution')
plt.ylabel('Customer number')
plt.xlabel('Life time (days)')
plt.show()

在這裏插入圖片描述
橫座標代表生命週期的天數區間,縱座標爲區間內的客戶數。
許多客戶僅消費過一次,沒有留存下來,需要更加重視客戶初次購買的體驗感,可以考慮通過網站內服務評價、客服電詢等方式獲知新客對於購買流程中不滿意之處,針對性地加以改進;同時應該對新客採取吸引其二次購買的手段,如發放有時限的優惠券等。有趣的是在350天左右出現一個次高峯,不妨將生命週期爲0天的客戶排除掉再看看分佈:

# 將分組增多至100,並拉寬圖表的尺寸
plt.figure()
life_time[life_time['life_time'] > 0].life_time.hist(bins = 100, figsize = (12, 6), color = 'c')
plt.title('Life Time Distribution without One-time Deal Hunters')
plt.ylabel('Customer number')
plt.xlabel('Life time (days)')
plt.show()

在這裏插入圖片描述
這個數據漂亮得有些驚人。生命週期在0-75天的客戶數略高於75-170天,可以考慮加強前70天內對客戶的引導。約1/4的客戶集中在170天-330天,屬於較高質量客戶的生命週期;而在330天以後,則是數量可觀的死忠客戶,擁有極高的用戶粘性。考慮到這些客戶中有許多未進行完整的生命週期 ,實際的客戶平均生命週期會更長。

# 消費兩次及以上的用戶平均生命週期
life_time[life_time['life_time'] > 0].life_time.mean()
# out:203.32867383512544

消費兩次及以上的用戶平均生命週期是203天,遠高於總體均值103天。從策略看,用戶首次消費後應該花更多的精力引導其進行多次消費,能有效提高生命週期。

4.2.2 客戶的留存情況
客戶的生命週期實際上是首次和末次消費的時間差,故無法對客戶各月的消費情況獲得直觀的感受。因此接下來我們對客戶的留存情況展開探究。這裏需要說明的是,同樣由於樣本統計區間的緣故,我們無法判斷2010年12月1日至2011年12月9日內的首次消費是否是該客戶的歷史首次消費。

先將用戶首次消費日期合併進sales_customer中,suffixes參數是對重名的字段自定義後綴:

customer_retention = pd.merge(sales_customer, mindate, left_on='CustomerID', right_index=True, how='inner', suffixes=('', 'Min'))

新增字段DateDiff,用於存放本次消費日期與首次消費日期的時間差,並轉爲數值:

customer_retention['DateDiff'] = (customer_retention.Date - customer_retention.DateMin).dt.days

對時間差分段,我這裏將3天、7天、30天、60天、90天、180天作爲區間端點,並新增字段DateDiffBin來存放:

date_bins = [0, 3, 7, 30, 60, 90, 180]
customer_retention['DateDiffBin'] = pd.cut(customer_retention.DateDiff, bins = date_bins)
customer_retention['DateDiffBin'].value_counts()

在這裏插入圖片描述
DateDiffBin代表客戶該筆訂單的消費時間距其首次消費屬於哪個時間段。因爲計算的是留存,如果客戶僅消費了一次(當日多次消費也視作一次),我們認爲該客戶是流失了的,這裏DateDiff=0,並不會被劃分入(0, 3]天的開閉區間內。

接下來用pivot_table作數據透視表,這裏index相當於數據透視表的行,columns相當於列,values表示聚合對象,aggfunc表示聚合方法。對SumPrice求和,獲得的結果是客戶首次消費後,在後續各時間段內的消費總金額:

retention_pivot = customer_retention.pivot_table(index = ['CustomerID'], columns = ['DateDiffBin'], values = ['SumPrice'], aggfunc= np.sum)
print(retention_pivot)

在這裏插入圖片描述
NaN表示該客戶在該區間內未進行過消費,聚合值全爲NaN的客戶會被過濾,即透視表中全爲消費2次及以上的留存客戶。

將數據轉換成是否,1代表在該時間段內有後續消費,0代表沒有:

retention_pivot_trans = retention_pivot.fillna(0).applymap(lambda x:1 if x > 0 else 0)
retention_pivot_trans.head()

在這裏插入圖片描述
統計留存客戶首次消費後各時間段內的購買率,這裏對各列中的1求和,再除以計數值:

(retention_pivot_trans.sum()/ retention_pivot_trans.count())

在這裏插入圖片描述
爲了方便直觀感受,繪製爲柱形圖:

(retention_pivot_trans.sum()/ retention_pivot_trans.count()).plot.bar()
plt.show()

在這裏插入圖片描述
在這些老客戶中,只有3.2%在第一次消費的次日至3天內有過消費,6.6%的客戶在4-7天有過消費。分別有40.5%和37.4%的客戶在首次消費後的第二個月內和第三個月內有過購買行爲。將時間範圍繼續放寬,有高達67%的客戶在90天至半年內消費過。說明該電商網站的客戶羣體,其採購並非高頻行爲,但留存下來的老客戶忠誠度卻極高。結合前文,僅有首次購買行爲的客戶佔總客戶的37.5%,如能提高這部分羣體的留存率,將會帶來很高的收益。

4.2.3 客戶的購買週期
接下來計算客戶的購買週期,思路是對其相鄰兩次消費日期相減,這裏我們通過之前得到的DateDiff來運算。

對customer_retention去除客戶編號和消費日期都相同的重複數據(即相同訂單的不同商品),參數keep='first’表示保留重複值中的第一條;由於要考慮到相鄰的問題,再對日期進行升序排序:

sales_cycle = customer_retention.drop_duplicates(subset=['CustomerID', 'Date'], keep='first')
sales_cycle.sort_values(by = 'Date',ascending = True)  

定義函數diff,用於計算相鄰兩次消費的時間差:

def diff(group):
    d = group.DateDiff - group.DateDiff.shift()
    return d

shift()是往上偏移一個位置,shift(-1)是往下偏移一個位置,求客戶本次消費與上次消費的時間間隔,用當前值減去shift()即可。若爲NaN,則僅消費過一次。

先按客戶編碼分組,在應用diff函數:

last_diff = sales_cycle.groupby('CustomerID').apply(diff)

得到結果如下:

last_diff.head(10)

在這裏插入圖片描述
第一列爲客戶編碼,第二列是索引值,第三列表示本次消費同上次消費的時間差。

保險起見,可以提取客戶編碼12347的訂單詳情進行覈對,確保我們的函數沒有寫錯:

sales_cycle[customer_retention['CustomerID'] == 12347]

在這裏插入圖片描述
與last_diff的日期間隔一致,函數使用正確。經過計算正好是50天。

畫出按訂單統計的購買週期柱狀圖,查看其分佈:

last_diff.hist(bins = 70, figsize = (12, 6), color = 'c')

在這裏插入圖片描述
典型的長尾分佈,大部分購買行爲的消費間隔比較短。但這是所有訂單的購買週期分佈,並不是對客戶個體爲統計單位的購買週期分佈。故對客戶編號進行分組:

last_diff_customer = last_diff.groupby('CustomerID').mean()
last_diff_customer.hist(bins = 70, figsize = (12, 6), color = 'c')

在這裏插入圖片描述
一個右偏分佈,峯值在15-70天,說明大部分留存客戶的購買週期集中於此。建議可以每隔30天左右對客戶進行些優惠活動的信息推送,比較符合大部分老客戶的購買週期。

Part 5. 小結

1.訂單維度:

有效訂單共19960筆,筆單價爲533.17英鎊,連帶率約爲279件。訂單以批發性質爲主,訂單間差異較大,存在部分購買力極強的客戶。總體來說訂單交易金額與訂單內商品件數正相關。

2.客戶維度:

客單價爲2049英鎊,客戶的購買力存在較大差距,擁有一定數量消費能力強的客戶。客戶羣體比較健康,其消費金額與購買商品數量正相關,而且規律性比訂單更強。

3.商品維度:

商品的單價會發生波動,集中於1-2英鎊,定位主要是低價的小商品市場。低於5英鎊的商品最受客戶喜愛,同時也構成了銷售額的主要部分。高價的商品雖然單價不菲,但銷量很低,並沒有帶來太多的銷售額。建議平臺採購部門可以多遴選售價低於10英鎊的產品,來進一步擴充低價區的品類。

4.時間維度:

訂單數、銷量、銷售額總體上呈現相近的趨勢, 在2011年9月-11月連續增長,達到高峯。考慮到主營商品爲禮品,猜測受節日影響可能較大,如萬聖節(11月1日)和聖誕節(12月25日),也可能受到“黑五”(11月第4個週四)影響。商品單價低且價位集中,銷售額主要隨銷量變化而漲跌。

5.區位維度:

絕大部分客戶來自英國本土,主要境外收入也多來自周邊國家,基本上符合以英國爲圓心向外輻射的情況。可能和運輸成本及語言等有關,也可能是影響力隨距離而衰減。可以考慮增加境外的宣傳投放,提高知名度;同時建議網站做好多國語言的適配。

6.生命週期:

平均生命週期爲130天,生命週期的分佈呈兩極分化的狀態。消費兩次及以上的客戶平均生命週期是203天,遠高於總體均值103天。建議更加重視客戶初次消費的體驗感,可以考慮通過網站內服務評價、客服電詢等方式獲知新客對於購買流程中不滿意之處,針對性地加以改進;並且花更多的精力引導其進行再次消費,如發放有時限的優惠券等。

7.留存情況:

客戶羣體的採購並非高頻行爲,但留存下來的老客戶忠誠度極高。而僅有首次購買行爲的客戶佔總客戶的37.5%,如能提高這部分羣體的留存率,將會帶來很高的收益。

8.購買週期:

大部分留存客戶的購買週期集中在15-70天,建議可以每隔30天左右對客戶進行些優惠活動的信息推送。

發佈了120 篇原創文章 · 獲贊 9 · 訪問量 4239
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章