如何預測粉絲頁發文獲得讚數?

當我們爬完和分析完資料後,可不可以做更進一步的應用呢?
sure! 接下來就要來預測某篇post的按讚程度,假設我們知道post內容,發文類型,發文時間點,
我們能不能預測這篇post能得到多少讚呢? 若要預測得到多少讚的話,這就是一個regression problem
但我們先簡單一點,把它當成一個classification problem,
要如何作到這一點呢?
我們把reaction高於中位數的label成1,低於的label成0,去預測某篇post是0或是1就行了


In [1]:
import pandas as pd
import operator
import datetime
import numpy as np

Data Porcessing

一樣先把資料讀進來


In [2]:
page_id = "appledaily.tw"

path = 'post/'+page_id+'_post.csv'
df = pd.read_csv(path, encoding='utf8')

看一看前五筆


In [3]:
df.head()


Out[3]:
status_id status_message link_name status_type status_link status_published num_reactions num_comments num_shares num_likes num_loves num_wows num_hahas num_sads num_angrys
0 232633627068_10155689734022069 加油!放寬心才能走出來\n \n#金曲男星 #蛋堡 #邱振熙 蛋堡 Soft Lipa 【壹週刊】​金曲男星進精神療養院 曾入圍歌王 link https://www.facebook.com/232633627068/posts/10... 2017-03-14 18:20:02 275 3 0 240 13 14 3 4 1
1 232633627068_10155689752042069 #最新 趕快清查把該抓的抓起來!\n \n相關→ 自殺副局長12年前與晶鑽搭上線 多次提供開... 【晶鑽弊案】北市高官也涉貪 建管處前主秘遭搜索約談 link https://www.facebook.com/232633627068/posts/10... 2017-03-14 17:59:25 157 8 0 141 3 2 7 0 4
2 232633627068_10155689484782069 #慎入 這就跟把雞排放進我嘴裡又不讓我咬一樣呀...... #宅編\n  \n完整 #動新聞... 【大咬片】馴獸師把頭放進鱷魚嘴 被咬得血流滿面 video https://www.facebook.com/232633627068/posts/10... 2017-03-14 17:50:00 269 24 4 210 4 29 24 2 0
3 232633627068_10155689727032069 距離周末前往台中還有...好久 #隨編\n \n#正妹 #紅豆餅妹 #朝聖啦 #蕭卉君 \n... 清新紅豆餅妹藏逆天「胸器」!網友揪朝聖啦 link https://www.facebook.com/232633627068/posts/10... 2017-03-14 17:40:00 2904 109 144 2802 38 44 18 1 1
4 232633627068_10155689539617069 Betty批「這種人根本不配當攝影師,很沒道德」\n  \n【完整 #動新聞】大尺度女模控無... 大尺度女模控無良攝影師 外流露點走光照 video https://www.facebook.com/232633627068/posts/10... 2017-03-14 17:30:00 595 18 7 496 8 21 4 2 64

In [4]:
len(df)


Out[4]:
5234

一共5234筆發文

Check missing

看看個欄位缺失的值


In [5]:
df.apply(lambda x: sum(x.isnull()))


Out[5]:
status_id             0
status_message      150
link_name           147
status_type           0
status_link           0
status_published      0
num_reactions         0
num_comments          0
num_shares            0
num_likes             0
num_loves             0
num_wows              0
num_hahas             0
num_sads              0
num_angrys            0
dtype: int64

把status_message為空值的地方拿掉


In [6]:
df = df[df['status_message'].notnull()].reindex()

再把reactions等於0的去掉


In [7]:
df = df[df['num_reactions']!=0].reindex()

In [8]:
len(df)


Out[8]:
5061

剩下5061篇發文

一樣處理時間,新增星期幾和單日第幾時po文的欄位


In [9]:
df['datetime'] = df['status_published'].apply(lambda x: datetime.datetime.strptime(x,'%Y-%m-%d %H:%M:%S'))
df['weekday'] = df['datetime'].apply(lambda x: x.weekday_name)
df['hour'] = df['datetime'].apply(lambda x:  x.hour)

Feature Engineering

status_type,weekday必須經過One-Hot Coding的處理,因為
假設有一個feature是 地區:["Europe","US","Asia"],我們先把它數字化[2,1,0]
但轉成數字後就存在1>0,2>0這種關係,但是US不會大於Asia,所以還必須經過別種處理方式
要把一維的地區feature轉成三維的地區feature,變成是否就可以用[1,0]表示


In [10]:
pd.Series(data = ["EuropeS","Asia","Europe","US","Asia"], name = 'location')


Out[10]:
0    EuropeS
1       Asia
2     Europe
3         US
4       Asia
Name: location, dtype: object

In [11]:
# Numerical Coding
from sklearn.preprocessing import LabelEncoder

le = LabelEncoder()
le.fit_transform(pd.Series(data = ["Europe","Asia","Europe","US","Asia"], name = 'location'))


Out[11]:
array([1, 0, 1, 2, 0])

In [12]:
# One-Hot Coding
pd.get_dummies(le.fit_transform(pd.Series(data = ["Europe","Asia","Europe","US","Asia"], name = 'location')))


Out[12]:
0 1 2
0 0 1 0
1 1 0 0
2 0 1 0
3 0 0 1
4 1 0 0

處理 feature (status_type,weekday)

所以要把status_type,weekday經過 Numerical Coding 和 One-Hot Coding


In [13]:
var_to_encode = ['status_type','weekday']
for col in var_to_encode:
    df[col] = le.fit_transform(df[col])
df = pd.get_dummies(df, columns=var_to_encode+['hour'])

處理 label

把reaction大於中位數(1243)label成1,其餘label成0


In [14]:
y = df['num_reactions']

In [15]:
y.describe()


Out[15]:
count      5061.000000
mean       3151.130409
std        8860.404063
min           1.000000
25%         558.000000
50%        1243.000000
75%        3049.000000
max      327177.000000
Name: num_reactions, dtype: float64

In [16]:
y = df['num_reactions'].apply(lambda x: 1 if x > 1243 else 0)

處理 feature (status_message)

接下來要建立詞向量,什麼是詞向量
舉例 總共有三篇文章
第一篇 我想吃蘋果 提取關鍵字後 我,吃,蘋果
第二篇 我想吃香蕉 提取關鍵字後 我,吃,香蕉
第三篇 我想吃火鍋 提取關鍵字後 我,吃,火鍋
詞向量就是

蘋果 香蕉 火鍋
1 1 1 0 0
1 1 0 1 0
1 1 0 0 1

所以接下來就建立發文內容的詞向量


In [17]:
# 載入繁體詞庫
import jieba.analyse

# 安裝jieba套件的時候,就有繁體詞庫
jieba.set_dictionary('/home/wy/anaconda3/envs/python3/lib/python3.6/site-packages/jieba/extra_dict/dict.txt.big')

def jieba_extract(message_list):
    word_count = {}
    for message in message_list:
        seg_list = jieba.analyse.extract_tags(message, topK=120)
        
        for seg in seg_list:
            if not seg in word_count:
                word_count[seg] = 1
            else:
                word_count[seg] += 1

    sorted_word_count = sorted(word_count.items(), key=operator.itemgetter(1))
    sorted_word_count.reverse()
    return sorted_word_count

In [18]:
sorted_word_count = jieba_extract(list(df['status_message']))
word = [one[0] for one in sorted_word_count]


Building prefix dict from /home/wy/anaconda3/envs/python3/lib/python3.6/site-packages/jieba/extra_dict/dict.txt.big ...
Loading model from cache /tmp/jieba.ue56a5cc1e0fa654d6fb1c49f7279c0f2.cache
Loading model cost 1.258 seconds.
Prefix dict has been built succesfully.

In [19]:
def get_word_vector(message):
    v = np.zeros(len(word))
    v_word = jieba.analyse.extract_tags(message, topK=120)
    for w in v_word:
        v[words.index(w)] = 1
    return v

In [20]:
sorted_word_count = jieba_extract(list(df['status_message']))
words = [one[0] for one in sorted_word_count]

word_vector = [get_word_vector(one) for one in df['status_message']]
word_vector = np.array(word_vector)

In [21]:
word_vector.shape
word_vector


Out[21]:
array([[ 0.,  0.,  0., ...,  1.,  1.,  1.],
       [ 1.,  1.,  1., ...,  0.,  0.,  0.],
       [ 1.,  1.,  1., ...,  0.,  0.,  0.],
       ..., 
       [ 0.,  0.,  0., ...,  0.,  0.,  0.],
       [ 1.,  1.,  1., ...,  0.,  0.,  0.],
       [ 0.,  0.,  0., ...,  0.,  0.,  0.]])

代表有5061篇發文和27518個詞


In [22]:
df.columns


Out[22]:
Index(['status_id', 'status_message', 'link_name', 'status_link',
       'status_published', 'num_reactions', 'num_comments', 'num_shares',
       'num_likes', 'num_loves', 'num_wows', 'num_hahas', 'num_sads',
       'num_angrys', 'datetime', 'status_type_0', 'status_type_1',
       'status_type_2', 'status_type_3', 'weekday_0', 'weekday_1', 'weekday_2',
       'weekday_3', 'weekday_4', 'weekday_5', 'weekday_6', 'hour_0', 'hour_1',
       'hour_2', 'hour_3', 'hour_4', 'hour_5', 'hour_6', 'hour_7', 'hour_8',
       'hour_9', 'hour_10', 'hour_11', 'hour_12', 'hour_13', 'hour_14',
       'hour_15', 'hour_16', 'hour_17', 'hour_18', 'hour_19', 'hour_20',
       'hour_21', 'hour_22', 'hour_23'],
      dtype='object')

看一下資料的欄位,我們只能使用status_message,status_type,weekday,hour
因為 num_reactions 和剩下的欄位都有關係(分享數多那按讚也會多 blabla)所以不能拿進來用
status_type,weekday,hour 已經經過One-Hot Coding處理,才會有這麼多欄位


In [23]:
features = df[['status_type_0', 'status_type_1',
       'status_type_2', 'status_type_3', 'weekday_0', 'weekday_1', 'weekday_2',
       'weekday_3', 'weekday_4', 'weekday_5', 'weekday_6', 'hour_0', 'hour_1',
       'hour_2', 'hour_3', 'hour_4', 'hour_5', 'hour_6', 'hour_7', 'hour_8',
       'hour_9', 'hour_10', 'hour_11', 'hour_12', 'hour_13', 'hour_14',
       'hour_15', 'hour_16', 'hour_17', 'hour_18', 'hour_19', 'hour_20',
       'hour_21', 'hour_22', 'hour_23']].as_matrix()
features


Out[23]:
array([[1, 0, 0, ..., 0, 0, 0],
       [1, 0, 0, ..., 0, 0, 0],
       [0, 0, 0, ..., 0, 0, 0],
       ..., 
       [1, 0, 0, ..., 0, 0, 0],
       [1, 0, 0, ..., 0, 0, 0],
       [1, 0, 0, ..., 0, 0, 0]], dtype=uint8)

把詞向量的feature和其他feature結合在一起


In [24]:
X = np.concatenate((features, word_vector), axis=1)

In [25]:
X


Out[25]:
array([[ 1.,  0.,  0., ...,  1.,  1.,  1.],
       [ 1.,  0.,  0., ...,  0.,  0.,  0.],
       [ 0.,  0.,  0., ...,  0.,  0.,  0.],
       ..., 
       [ 1.,  0.,  0., ...,  0.,  0.,  0.],
       [ 1.,  0.,  0., ...,  0.,  0.,  0.],
       [ 1.,  0.,  0., ...,  0.,  0.,  0.]])

處理後的feature 維度和數量


In [26]:
X.shape


Out[26]:
(5061, 27553)

處理後的label 數量


In [27]:
y.shape


Out[27]:
(5061,)

接下剌我們要使用的model是logistic regression
logistic regression : 是機器學習分類算法的其中一,其算法作為一個二分類算法,主要解決的是線性可分的問題

方程式 : $$ h(x)=\frac{1}{1+exp(-w^Tx)} $$

假設我們可以知道一個病人的性別,年齡,身高,體重,血壓...為了要計算他得到癌症的機率,可以將這幾個特徵做加權求和
x代表每個特徵,w代表每個特徵相對應的權重,s為加權求和的值
$$ \color{purple}{s}=\sum_{i={0}}^d{w_i}{x_i}=w^Tx $$

但是算出來的s範圍是負無限大到正無限大,我們會希望得到一個機率估計的值0到1,所以要把s經過sigmoid函數${\theta}$處理
$$ {\theta}({s})=\frac{e^{s}}{1+e^{s}}=\frac{1}{1+e^{-s}} $$

綜合以上由於我們只要可以知道x和w,就可以進行預測了!
x是我們已經知道的,但是要如何得到w呢?

我們先需要也要有一個可以衡量這組w好不好的函數稱為cost function
這邊所使用的誤差函數為 cross entropy error (直接給方程式,就不附推導過程)
y為train data中的label,1就是得癌症,0就是沒得
$$ err({w},x,y)=ln(1+exp(-y{w}x)) $$

所以cost function為 $$ E({w})=\frac{1}{N}\sum_{n=1}^{N}ln(1+exp(-y_n{w}^Tx_n)) $$

有了cost function就可以用梯度下降法(Gradient Descent)去尋找適合的w,找到w後就可以進行預測了!

predict

套入scikit的LogisticRegression,並用cross validation去算平均準確率

cross validation :
假設cv=5 就是把整組資料分成五份,其中一份當作testing data,剩下四份當作training data,然後輪流做五次
讓每一份都會輪到當成testing data,最後把五次的結果平均,算出平均準確率


In [28]:
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import cross_val_score

In [29]:
clf = LogisticRegression()
clf.fit(X, y)


Out[29]:
LogisticRegression(C=1.0, class_weight=None, dual=False, fit_intercept=True,
          intercept_scaling=1, max_iter=100, multi_class='ovr', n_jobs=1,
          penalty='l2', random_state=None, solver='liblinear', tol=0.0001,
          verbose=0, warm_start=False)

In [30]:
score = cross_val_score(clf, X, y, cv=5)
print (score.mean())


0.503655833844

算完之後你會發現,準確率只有50趴,那這樣跟我隨便猜有什麼不一樣,
那先來調整一下參數看準確率會不會提高好了,因此調整一下C


In [31]:
clf = LogisticRegression(C=0.001)
clf.fit(X, y)


Out[31]:
LogisticRegression(C=0.001, class_weight=None, dual=False, fit_intercept=True,
          intercept_scaling=1, max_iter=100, multi_class='ovr', n_jobs=1,
          penalty='l2', random_state=None, solver='liblinear', tol=0.0001,
          verbose=0, warm_start=False)

In [32]:
score = cross_val_score(clf, X, y, cv=5)
print (score.mean())


0.641988926563

準確率大幅上升,原因是因為 參數C在Scikit-learn裡LogisticRegression定義為正規化參數(regularization parameter)λ的倒數,
而正規化(regularization)是用來防止高度適合(overfitting)
也就是說C變得很小,那λ就會變很大,正規化的強度就會變強防止overfitting

結語

本文只使用較為簡單的linear model Logistic Regression,若用更為複雜的model準確率有可能會在提高
另一個提高準確率的方法,就是把feature engineering 再做好一點!