python資料分析:流量資料化運營(下)——基於自動K值得KMeans廣告效果聚類分析
案例背景
某企業由於投放的廣告渠道比較多,需要對其做廣告效果分析以實現有針對性的廣告效果測量和優化工作。跟以應用為目的的案例不同的是,由於本案例是一個分析型案例,該過程的輸出其實是不固定的,因此需要跟業務運營方具體溝通需求。
以下是在開展研究之前的基本預設條件:
- 廣告渠道的範疇是什麼?具體包括哪些渠道?——所有站外標記的廣告類渠道(以ad_開頭)。
- 資料集時間選擇哪個時間段?——最近90天的資料。
- 資料集選擇哪些維度和指標?——渠道代號、日均UV、平均註冊率、平均搜尋量、訪問深度、平均停留時間、訂單轉化率、投放總時間、素材型別、廣告型別、合作方式、廣告尺寸、廣告賣點。
- 專題分析要解決什麼問題?——將廣告分類並找出其重點特徵,為接下來的業務討論和資料分析提供支援。
案例資料
以下是本資料集的13個欄位的詳細說明:
- 渠道代號:業務方統一命名規劃的唯一渠道標誌。
- 日均UV:每天的平均獨立訪客,從一個渠道中帶來的一個訪客即使一天中到達多次都統計為1次。
- 平均註冊率:日均註冊的使用者數量/平均每天的訪問量。
- 平均搜尋量:平均每個訪問的搜尋次數。
- 訪問深度:總頁面瀏覽量/平均每天的訪問量。
- 平均停留時間:總停留時間/平均每天的訪問量。
- 訂單轉化率:總訂單數量/平均每天的訪問量。
- 投放總時間:每個廣告媒介在站外投放的天數。
- 素材型別:廣告素材型別,包括jpg、gif、swf、sp。
- 廣告型別:廣告投放型別,包括banner、tips、橫幅、通欄、暫停以及不確定(不知道到底是何種形式)。
- 合作方式:廣告合作方式,包括roi、cpc、cpm和cpd。
- 廣告尺寸:每個廣告投放的尺寸大小,包括14040、308388、450300、60090、480360、960126、900120、390270。
- 廣告賣點:廣告素材上主要的賣點訴求資訊,包括打折、滿減、滿贈、秒殺、直降、滿返。
案例實現
import numpy as np
import pandas as pd
from sklearn.feature_extraction import DictVectorizer
from sklearn.preprocessing import MinMaxScaler
from sklearn.cluster import KMeans
from sklearn import metrics # 匯入sklearn效果評估模組
import matplotlib.pyplot as plt
%matplotlib inline
# 載入資料,空格為間隔
df = pd.read_csv('https://raw.githubusercontent.com/ffzs/dataset/master/ad_performance.txt', delimiter='\t')
# 資料概觀
df.tail(3).T
# 資料型別
df.dtypes
'''
渠道代號 object
日均UV float64
平均註冊率 float64
平均搜尋量 float64
訪問深度 float64
平均停留時間 float64
訂單轉化率 float64
投放總時間 float64
素材型別 object
廣告型別 object
合作方式 object
廣告尺寸 object
廣告賣點 object
dtype: object
'''
# 缺失值情況
df.isna().sum()
'''
渠道代號 0
日均UV 0
平均註冊率 0
平均搜尋量 0
訪問深度 0
平均停留時間 2
訂單轉化率 0
投放總時間 0
素材型別 0
廣告型別 0
合作方式 0
廣告尺寸 0
廣告賣點 0
dtype: int64
'''
# 原始資料基本描述
df.describe().round(3)
如下的描述性統計結果中,反映了3個資訊點:
- 日均UV的資料標準差很大,說明了不同渠道間的特徵差異非常明顯。
- 平均停留時間的有效資料(非空資料)只有887,比其他資料少2條,這也印證了上述缺失值統計結果。
- 平均註冊率、平均搜尋量、訂單轉化率的多個統計量(例如最小值、25%分位數等)都為0,看似資料不太正常。
# 計算特徵相關性
df.corr()
通過相關性結果分析,12個特徵中平均停留時間和訪問深度的相關係數為0.72,這兩個指標具有較高的相關性,但特徵也不是非常明顯;其他特徵之間的相關性關係都不突出。
# 使用平均值替換缺失值
df = df.fillna(df['平均停留時間'].mean())
# 字串分類轉整數分類
conver_cols = ['素材型別', '廣告型別', '合作方式', '廣告尺寸', '廣告賣點']
convert_matrix = df[conver_cols] # 獲得要轉換的陣列
lines = df.shape[0] # 獲得總記錄數
dict_list = [] # 總空列表,用於存放字串與對應索引組成的字典
unique_list = [] # 總唯一值列表,用於儲存每個列的唯一值列表
for col_name in conver_cols: # 迴圈讀取每個列名
cols_unqiue_value = df[col_name].unique().tolist() # 獲取列的唯一值列表
unique_list.append(cols_unqiue_value) # 將唯一值列表追加到總列表
for line_index in range(lines): # 讀取每行索引
each_record = convert_matrix.iloc[line_index] # 獲得每行資料,是一個Series
for each_index, each_data in enumerate(each_record): # 讀取Series每行對應的索引值
list_value = unique_list[each_index] # 讀取該行索引對應到總唯一值列表列索引下的資料(其實是相當於原來的列做了轉置成了行,目的是查詢唯一值在列表中的位置)
each_record[each_index] = list_value.index(each_data) # 獲得每個值對應到總唯一值列表中的索引
each_dict = dict(zip(conver_cols, each_record)) # 將每個值和對應的索引組合字典
dict_list.append(each_dict) # 將字典追加到總列表
model_dvtransform = DictVectorizer(sparse=False, dtype=np.int64) # 建立轉換模型物件
data_dictvec = model_dvtransform.fit_transform(dict_list) # 應用分類轉換訓練
# 獲取特徵轉換成矩陣
scale_matrix = df.iloc[:, 1:8]
# 建立MinMaxScaler模型物件
minmax_scaler = MinMaxScaler()
# 標準化資料
data_scaled = minmax_scaler.fit_transform(scale_matrix)
# 矩陣合併成目標資料
X = np.hstack((data_scaled, data_dictvec))
# 用來儲存每個k下模型的平均輪廓係數
score_list =[]
# 初始化的平均輪廓係數閾值
silhouette_int = -1
# k取分別取2-9,評估模型情況
for n_clusters in range(2, 10):
# 建立聚類模型物件
kmeans = KMeans(n_clusters=n_clusters, random_state=0)
# 訓練聚類模型
labels = kmeans.fit_predict(X)
# 獲取每個K的平均輪廓係數
silhouette_tmp = metrics.silhouette_score(X, labels)
# 如果平均輪廓係數更高,儲存模型
if silhouette_tmp > silhouette_int:
best_k = n_clusters # 記錄最好K
best_kmeans = kmeans # 記錄最好模型
silhouette_int = silhouette_tmp # 記錄最好平均輪廓
cluster_labels = labels #記錄最好的labels
# 記錄每次的K和輪廓值
score_list.append([n_clusters, silhouette_tmp])
print ('{:*^60}'.format('K value and silhouette summary:'))
print (np.array(score_list)) # 列印輸出所有K下的詳細得分
print ('Best K is:{} with average silhouette of {}'.format(best_k, silhouette_int.round(4)))
結果如下:
**************K value and silhouette summary:***************
[[2. 0.46692821]
[3. 0.54904646]
[4. 0.56968547]
[5. 0.48186604]
[6. 0.45477667]
[7. 0.48204261]
[8. 0.50447223]
[9. 0.52697493]]
Best K is:4 with average silhouette of 0.5697
對於平均輪廓係數而言,其值域分散式[-1, 1]。因此silhouette_int的初始值可以設定為-1或比-1更小的值。
上述結果顯示了不同K下的平均輪廓得分。就經驗看,如果平均輪廓得分值小於0,意味著聚類效果不佳;如果值大約0且小於0.5,那麼說明聚類效果一般;如果值大於0.5,則說明聚類效果比較好。本案例在K=4時,得分為0.5697,說明效果較好。
# 將最優情況的標籤整合到原始資料上
df['clusters'] = cluster_labels
# 每個聚類的樣本量
cluster_count = pd.DataFrame(df.clusters.value_counts()).rename(columns={'clusters': 'counts'})
# 獲取樣本佔比
cluster_count['percentage'] = (cluster_count.counts/cluster_count.counts.sum()).round(2)
cluster_count
### 視覺化 ###
# 設定顏色
colors='yellowgreen','gold','lightskyblue','lightcoral'
# 設定分離
explode=0.1,0,0,0
# 設定畫幅
plt.figure(figsize=(5, 5))
# 作圖
plt.pie(cluster_count.counts,explode=explode,labels=cluster_count.index,colors=colors,autopct='%1.1f%%',shadow=True,startangle=50)
plt.savefig('x.png')
# 空列表,用於儲存最終合併後的所有特徵資訊
cluster_features = []
for line in range(best_k): # 讀取每個類索引
label_data = df[df['clusters'] == line] # 獲得特定類的資料
part1_data = label_data.iloc[:, 1:8] # 獲得數值型資料特徵
part1_desc = part1_data.describe().round(3) # 得到數值型特徵的描述性統計資訊
merge_data1 = part1_desc.iloc[1, :] # 得到數值型特徵的均值
part2_data = label_data.iloc[:, 8:-1] # 獲得字串型資料特徵
part2_desc = part2_data.describe(include='all') # 獲得字串型資料特徵的描述性統計資訊
merge_data2 = part2_desc.iloc[2, :] # 獲得字串型資料特徵的最頻繁值
merge_line = pd.concat((merge_data1, merge_data2), axis=0) # 將數值型和字串型典型特徵沿行合併
cluster_features.append(merge_line) # 將每個類別下的資料特徵追加到列表
# 將列表轉化為矩陣
cluster_pd = pd.DataFrame(cluster_features)
# 將資訊合併
all_cluster_set = cluster_count.join(cluster_pd).sort_index().T
all_cluster_set
# 獲取要顯示的資料
num_sets = cluster_pd.iloc[:,:6].astype('float64')
# 獲得標準化後的資料
num_sets_max_min = minmax_scaler.fit_transform(num_sets)
#####繪製雷達圖#####
fig = plt.figure(figsize=(8, 8)) # 建立畫布
ax = fig.add_subplot(111, polar=True) # 增加子網格,注意polar引數
labels = np.array(merge_data1.index[:-1]) # 設定要展示的資料標籤
cor_list = ['r', 'g', 'b', 'y'] # 定義不同類別的顏色
angles = np.linspace(0, 2 * np.pi, len(labels), endpoint=False) # 計算各個區間的角度
angles = np.concatenate((angles, [angles[0]])) # 建立相同首尾欄位以便於閉合
for i in range(len(num_sets)): # 迴圈每個類別
data_tmp = num_sets_max_min[i, :] # 獲得對應類資料
data = np.concatenate((data_tmp, [data_tmp[0]])) # 建立相同首尾欄位以便於閉合
ax.plot(angles, data, 'o-', c=cor_list[i], label=i) # 畫線
ax.set_thetagrids(angles * 180 / np.pi, labels, fontproperties="SimHei") # 設定極座標軸
ax.set_title("各聚類類別顯著特徵對比", fontproperties="SimHei") # 設定標題放置
ax.set_rlim(-0.2, 1.2) # 設定座標軸尺度範圍
plt.legend(loc=0) # 設定圖例位置
plt.savefig('xx.png')
plt.show() # 展示影象
使用fig.add_subplot(111, polar=True)
為fig物件增加一個子網格,其中引數polar=True用來設定該子網格物件顯示極座標系。
np.array(merge_data1.index[:-1])
設定要展示的資料標籤,標籤從merge_data1的索引中獲取。
定義不同類別的顏色列表cor_list,分別代表紅色、綠色、藍色、黃色。
通過np.linspace(0, 2*np.pi, len(labels), endpoint=False)
計算各個區間的角度,由於要在雷達圖中顯示多個類別,這裡需要按照類別將整個“圓”按照類別數平均劃分。其中各個引數如下:
- 0:建立間隔區間的起始。
- 2*np.pi:建立間隔區間的末尾。
- len(labels):間隔長度。
- endpoint:值設定False代表間隔的最後一個值不是間隔區間的末尾。
最後使用angles = np.concatenate((angles, [angles[0]]))
建立相同首尾欄位以便於閉合。預設情況下,每個類別的多個特徵在雷達圖中首尾應該是相連的,這樣才能形成閉合效果,因此這裡將第一個只追加到列表末尾。
案例總結
初步分析:
聚類1(索引值為0):各方面的特徵都不明顯,換句話說就是效果比較平庸,沒有明顯的優勢或短板。但這些“中庸”的廣告媒體卻構成了整個廣告的主體。
聚類2(索引值為1):這類廣告媒體在訪問深度、平均停留時間、訂單轉化率以及平均搜尋量等流量質量的特徵上的表現較好,除了註冊轉化率較低外,該類渠道各方面比較均衡。更重要的是該類媒體的數量佔據了33%的數量,因此是一類規模較大且綜合效果較好的媒體。
聚類3(索引值為2):這類廣告媒體跟聚類2非常類似,並且相對聚類2的典型特徵表現更好,但綜合其只佔3%的媒體數量,屬於少量的“精英”類渠道。
聚類4(索引值為3):這類渠道跟其他幾類渠道有個明顯的特徵區隔,其日均UV和平均註冊率非常突出,證明這是一類“引流”+“拉新”的渠道;而其他的流量質量方面的表現卻比較差。
深入分析:
聚類1的廣告渠道各方面表現均比較一般,因此需要業務部門重點考慮其投放的實際價值。
聚類2的廣告渠道的短板是日均UV和平均註冊率,因此該類媒體無法為企業帶來大量的流量以及新使用者。這類廣告的特質適合使用者轉化,尤其是有關訂單轉化的提升。
聚類3的廣告渠道跟效果拔群,但是由於例項比較少,不排除是因為個例導致效果擴大,可以合理增加此類渠道投放。
聚類4的廣告渠道更佳符合廣告本身“廣而告之”的基礎訴求,因此適合在大規模的廣告宣傳和引流時使用,尤其對於新使用者的註冊轉化上的效果非常明顯,也適合“拉新”使用。
案例引申
本案例中通過平均輪廓係數的方法得到的最佳K值不一定在業務上具有明顯的解讀和應用價值。如果最佳K值的解讀無效怎麼辦?有兩種思路:
- 擴大K值範圍,例如將K的範圍調整為[2,12],然後再次運算看更大範圍內得到的K值是否更加有效並且能符合業務解讀和應用需求。
- 得到平均輪廓係數“次要好”(而不是最好)的K值,再對其結果做分析。
對於不同類別的典型特徵的對比,除了使用雷達圖直觀地顯示外,還可以使用多個柱形圖的形式,將每個類別對應特徵的值做柱形圖統計,這樣也是一個非常直觀的對比方法。
參考:
《python資料分析與資料化運營》 宋天龍