講的很直白了。那實際情況下,你如何做到這些呢?讓我們看下“分享經濟”模式典範的Airbnb是如何做的,後續會從頭到尾給出一個例子,使用Python和流行的Scikit-Learn庫,基於Airbnb已公開的舊金山城市的數據。
1.開始
我們基於listing屬性開始listing價格預測。預測價格有幾方面的應用:給客戶提供建議的價格(價格太高或者太低都會顯示提醒);幫助廣告商做廣告;提供數據分析給市場做決策。每個數據集包含以下幾個感興趣的項:
listings.csv.gz:詳細的listing數據,包含每個listing的各種屬性,比如,臥室數目、浴室數目、位置等;
calendar.csv.gz:每個listing的日曆信息;
reviews.csv.gz :listing的瀏覽數據;
neighborhoods and GeoJSON files:同城鄰居的地圖和詳細信息。
本例子提供了詳細的使用Python編程的scikit-learn應用以及如何使用Spark進行交叉驗證和調超參數。我們使用scikit-learn的線性迴歸方法,然後藉助Spark來提高窮舉搜素的結果和速度,這裏面用到GridSearchCV 和GradientBoostingRegressor方法。
2.掃描數據和清洗數據
首先,從MapR-FS文件系統加載listing.csv數據集,創建一個Pandas dataframe(備註:Pandas是Python下一個開源數據分析的庫,它提供的數據結構DataFrame)。數據集大概包含7000條listing,每個listing 有90個不同的列,但不是每個列都有用,這裏只挑選對最終的預測listing價格有用的幾列。
代碼如下:
%matplotlib inline
import pandas as pd
import numpy as np
from sklearn import ensemble
from sklearn import linear_model
from sklearn.grid_search import GridSearchCV
from sklearn import preprocessing
from sklearn.cross_validation import train_test_split
import sklearn.metrics as metrics
import matplotlib.pyplot as plt
from collections import Counter
LISTINGSFILE = '/mapr/tmclust1/user/mapr/pyspark-learn/airbnb/listings.csv'
cols = ['price',
'accommodates',
'bedrooms',
'beds',
'neighbourhood_cleansed',
'room_type',
'cancellation_policy',
'instant_bookable',
'reviews_per_month',
'number_of_reviews',
'availability_30',
'review_scores_rating'
]
# read the file into a dataframe
df = pd.read_csv(LISTINGSFILE, usecols=cols)
neighborhood_cleansed列是房主的鄰居信息。你會看到這些信息分佈不均衡,通過如下的圖看出分佈是個曲線,末尾的數量高,而靠左邊非常少。總體來說,房主的鄰居信息分佈合理。
nb_counts = Counter(df.neighbourhood_cleansed)
tdf = pd.DataFrame.from_dict(nb_counts, orient='index').sort_values(by=0)
tdf.plot(kind='bar')
下面對數據進行按序清洗。
number_reviews'和 reviews_per_month兩列看起來要去掉大量的NaN值(Python中NaN值就是NULL)。我們把reviews_per_month爲NaN值的地方設置爲0,因爲在某些數據分析中這些數據是有意義的。
我們去掉那些明顯異常的數據,比如,臥室數目、牀或者價格爲0的listing記錄,並且刪除那些NaN值的行。最後的結果集有5246條,原始數據集爲7029條。
# first fixup 'reviews_per_month' where there are no reviews
df['reviews_per_month'].fillna(0, inplace=True)
# just drop rows with bad/weird values
# (we could do more here)
df = df[df.bedrooms != 0]
df = df[df.beds != 0]
df = df[df.price != 0]
df = df.dropna(axis=0)
清洗的最後一步,我們把price列的值轉換成float型數據,只保留臥室的數目等於1的數據。擁有一個臥室的數據大概有70%(在大城市,舊金山,這個數字還算正常),這裏對這類數據進行分析。迴歸分析只對單個類型的數據進行分析,迴歸模型很少會和其他特徵進行復雜的交互。爲了對多個類型的數據進行預測,可以選擇對不同的類型數據(比如,分爲擁有2、3、4個臥室)單獨進行建模,或者通過聚類對那些很容易區分開來的數據進行分析。
df = df[df.bedrooms == 1]
# remove the $ from the price and convert to float
df['price'] = df['price'].replace('[\$,)]','', \
regex=True).replace('[(]','-', regex=True).astype(float)
3.類別變量處理
數據集中有幾列包含分類變量。根據可能存在的值有幾種處理方法。
neighborhood_cleansed列是鄰居的名字,string類型。scikit-learn中的迴歸分析只接受數值類型的列。對於這類變量,使用Pandas的get_dummies轉換成虛擬變量,這個處理過程也叫“one hot”編碼,每個listing行都包含一個“1”對應她/他的鄰居。我們用類似的方法處理cancellation_policy和room_type列。
instant_bookable列是個boolean類型的值。
# get feature encoding for categorical variables
n_dummies = pd.get_dummies(df.neighbourhood_cleansed)
rt_dummies = pd.get_dummies(df.room_type)
xcl_dummies = pd.get_dummies(df.cancellation_policy)
# convert boolean column to a single boolean value indicating whether this listing has instant booking available
ib_dummies = pd.get_dummies(df.instant_bookable, prefix="instant")
ib_dummies = ib_dummies.drop('instant_f', axis=1)
# replace the old columns with our new one-hot encoded ones
alldata = pd.concat((df.drop(['neighbourhood_cleansed', \
'room_type', 'cancellation_policy', 'instant_bookable'], axis=1), \
n_dummies.astype(int), rt_dummies.astype(int), \
xcl_dummies.astype(int), ib_dummies.astype(int)), \
axis=1)
allcols = alldata.columns
接下來用Pandas的scatter_matrix函數快速的顯示各個特徵的矩陣,並檢查特徵間的共線性。本列子中共線性不明顯,因爲我們僅僅挑選列一小部分特徵集,而且互相明顯不相關。
scattercols = ['price','accommodates', 'number_of_reviews', 'reviews_per_month', 'beds', 'availability_30', 'review_scores_rating']
axs = pd.scatter_matrix(alldata[scattercols],
figsize=(12, 12), c='red')
scatter_matrix的輸出結果發現並沒有什麼明顯的問題。最相近的特徵應該是beds和accommodates。
4.開始預測
scikit-learn較大的優勢是我們可以在相同的數據集上做不同的線性模型,這可以給我們一些調參的提示。我們開始使用其中的六種:vanilla linear regression, ridge and lasso regressions, ElasticNet, bayesian ridge和 Orthogonal Matching Pursuit。
爲了評估這些模型哪個更好,我們需要一種對其進行打分,這裏採用中位誤差。說到這裏,很可能會出現異常值,因爲我們沒有對數據集進行過濾或者聚合。
rs = 1
ests = [ linear_model.LinearRegression(), linear_model.Ridge(),
linear_model.Lasso(), linear_model.ElasticNet(),
linear_model.BayesianRidge(), linear_model.OrthogonalMatchingPursuit() ]
ests_labels = np.array(['Linear', 'Ridge', 'Lasso', 'ElasticNet', 'BayesRidge', 'OMP'])
errvals = np.array([])
X_train, X_test, y_train, y_test = train_test_split(alldata.drop(['price'], axis=1),
alldata.price, test_size=0.2, random_state=20)
for e in ests:
e.fit(X_train, y_train)
this_err = metrics.median_absolute_error(y_test, e.predict(X_test))
#print "got error %0.2f" % this_err
errvals = np.append(errvals, this_err)
pos = np.arange(errvals.shape[0])
srt = np.argsort(errvals)
plt.figure(figsize=(7,5))
plt.bar(pos, errvals[srt], align='center')
plt.xticks(pos, ests_labels[srt])
plt.xlabel('Estimator')
plt.ylabel('Median Absolute Error')
看下六種評估器得出的結果大體的相同,通過中位誤差預測的結果是30到35美元。最終的結果驚人的相似,主要原因是我們未做任何調參。
接下來我們繼續集成方法來獲取更好的結果。集成方法的優勢在於可以獲得更好的結果,副作用便是超參數的“飄忽不定”,所以得調參。每個參數都會影響我們的模型,必須要求實驗得出正確結構。最常用的方法是網格搜索法(grid search)暴力嘗試所有的超參數,用交叉驗證去找到較好的一個模型。Scikit-learn提供GridSearchCV函數正是爲了這個目的。
使用GridSearchCV需要權衡窮舉搜索和交叉驗證所耗費的CPU和時間。這地方就是爲什麼我們使用Spark進行分佈式搜索,讓我們更快的去組合特徵。
我們第一個嘗試將限制參數的數目爲了更快的得到結果,最後看下是不是超參數會比單個方法要好。
n_est = 300
tuned_parameters = {
"n_estimators": [ n_est ],
"max_depth" : [ 4 ],
"learning_rate": [ 0.01 ],
"min_samples_split" : [ 1 ],
"loss" : [ 'ls', 'lad' ]
}
gbr = ensemble.GradientBoostingRegressor()
clf = GridSearchCV(gbr, cv=3, param_grid=tuned_parameters,
scoring='median_absolute_error')
preds = clf.fit(X_train, y_train)
best = clf.best_estimator_
這次嘗試的中位誤差是23.64美元。已經可以看出用GradientBoostingRegressor比前面那次任何一種方法的結果都要好,沒有做任何調優,中位誤差已經比前面那組裏較好的中位誤差(使用BayesRidge()方法)還要少20%。
讓我們看下每步boosting的誤差,這樣可以幫助我們找到迭代過程遇到的問題。
# plot error for each round of boosting
test_score = np.zeros(n_est, dtype=np.float64)
train_score = best.train_score_
for i, y_pred in enumerate(best.staged_predict(X_test)):
test_score[i] = best.loss_(y_test, y_pred)
plt.figure(figsize=(12, 6))
plt.subplot(1, 2, 1)
plt.plot(np.arange(n_est), train_score, 'darkblue', label='Training Set Error')
plt.plot(np.arange(n_est), test_score, 'red', label='Test Set Error')
plt.legend(loc='upper right')
plt.xlabel('Boosting Iterations')
plt.ylabel('Least Absolute Deviation')
從曲線可以看出,曲線右邊到200-250次迭代到位置仍然可以通過迭代獲得好的結果,所以我們增加迭代次數到500。
接下來使用GridSearchCV進行各種超參數組合,這需要CPU和數小時。使用spark-sklearn 集成可以減少錯誤和時間。
from pyspark import SparkContext, SparkConf
from spark_sklearn import GridSearchCV
conf = SparkConf()
sc = SparkContext(conf=conf)
clf = GridSearchCV(sc, gbr, cv=3, param_grid=tuned_parameters, scoring='median_absolute_error')
至此,我們看下這種spark-sklearn 集成架構的優勢。spark-sklearn 集成提供了跨Spark executor對每個模型進行分佈式交叉驗證;而Spark MLlib只是在集羣間實際的機器學習算法間進行分佈式計算。spark-sklearn 集成主要的優勢是結合了scikit-learn 機器學習豐富的模型集合,這些算法雖然可以在單個機器上並行運算但是不能在集羣間進行運行。
採用這種方法最後優化的中位差結果是21.43美元,並且還縮短了運行時間,如下圖所示。集羣爲4個節點,以Spark YARN client模式提交,每個節點配置如下:
Machine: HP DL380 G6
Memory: 128G
CPU: (2x) Intel X5560
Disk: (6x) 1TB 7200RPM disks
最後讓我們看下特徵的重要性,下面顯示特徵的相對重要性。
feature_importance = clf.best_estimator_.feature_importances_
feature_importance = 100.0 * (feature_importance / feature_importance.max())
sorted_idx = np.argsort(feature_importance)
pos = np.arange(sorted_idx.shape[0]) + .5
pvals = feature_importance[sorted_idx]
pcols = X_train.columns[sorted_idx]
plt.figure(figsize=(8,12))
plt.barh(pos, pvals, align='center')
plt.yticks(pos, pcols)
plt.xlabel('Relative Importance')
plt.title('Variable Importance')
很明顯的是有一些變量比其他變量更重要,最重要的特徵是Entire home/apt。
5.結論
這個列子展示瞭如何使用spark-sklearn進行多變量來預測listing價格,然後進行分佈式交叉驗證和超參數搜索,並給出以下幾點參考:
GradientBoostingRegressor等集成方法比單個方法得出的結果要好;
使用GridSearchCV函數可以測試更多的超參數組合來得到更優的結果;
使用 spark-sklearn能更好節約CPU和時間,減少評估錯誤。