程序員,如何在買房時不被宰?
作者 | 胡蘿蔔
責編 | 胡巍巍
身為程序員,如何自製一個二手房估價模型,以最實惠的價格購得房子?本篇文章講的就是這件事!不過本文側重於完整的實現過程和思路,而不是代碼部分。
完成後會用自己訓練的模型來實戰預測下Q房網的成交數據,並且對比下房產大數據平台的「房價網」的估價器。
所用到的Python庫:sickit-learn、Numpy、pandas、Matplotlib、beautifulsoup。
機器學習到底是幹什麼的?
假設你有一位鄰居老張,東北人,平時經常去市場買菜,其中買大白菜最多。
由於老張這個人特別會過日子,不希望被攤位隨便要價,他就準備自己研究下白菜的價格,買菜都帶一個賬本記賬。買了5次以後記錄下來白菜的價格是這樣的:
根據賬本他推算了下白菜的價格,大約是5元一斤。如果買4斤白菜的話,正常價格應該是20元左右,這個特別簡單。
數學上描述這個關係是用一個線性方程y = ax,其中x代表白菜的斤數,y代表白菜的價格。a在這個案例裡面等於5(每斤白菜的單價),而y = 5x就是白菜價格的模型。
現在老張覺得這個估價方法挺管用,由於價格猜測得準確也沒有攤位敢胡亂開價。太平過了一段時間後,有一天他又去買菜,這次攤主卻告訴他4斤白菜要28塊了。老張很疑惑,問攤主原因。
攤主說,哦,因為這批白菜比較新鮮,剛採摘下來不到24小時,所以賣的比較貴。而且,由於家裡要買一塊新菜地,為了增加點收入,以後白菜也都要按照新鮮程度來賣了…...
這樣一來,原來那個模型就沒用了。於是老張又記錄了一段時間的賬本,與之前不同的是他這次還記錄了白菜的新鮮程度。這個賬本是這樣的:
這麼一看還是能隱約知道白菜的價格與斤數有關,但是似乎關係不那麼明顯了。現在老張怎麼來估算白菜的價格?
其實白菜的價格分布還是有規律的。假設把老張的賬本投射到一個三維空間里是這樣的:
可以看到幾乎所有的點在三維空間中都處於同一個平面範圍上。根據高中數學我們會知道三維空間平面的表達式是z = ax + by + c。
這裡的a不再代表白菜的單價了,僅僅是白菜重量的係數。新的賬本的表達式是z = 7x - 3.5y + 3.5。
這個白菜價格估算方法用的就是機器學習中最基礎也最經典的一種演算法,叫做Linear Regression線性回歸。
老張的賬本在機器學習中叫做訓練數據集,重量、新鮮程度等描述白菜屬性的數值或者分類叫做特徵。
而求解模型中a,b,c參數具體數值,使得它對所有預測結果與真實值之間綜合誤差最小的過程就叫做模型的擬合。
這看起來很OK,可是你一定會說現實生活中的問題根本不是這樣的啊!事實上老張可能會去多個攤位買菜,每次去哪個攤位都是隨機的。
又或者白菜有大有小,白菜大小也會影響價格。白菜也可能在冬天更便宜,夏天更貴等等…...
說的沒錯,實際生活中我們往往需要多個特徵來描述問題,對應一個多維以上的空間。
數據分布也並非總是線性的,可能是一個曲面或者高維超曲面,數據也可能並不會正好都在某個曲面上等等。
這樣問題就來了,大多數的人對高維想像都很難,怎麼去解一個高維空間的問題哪?
這個時候計算機就發揮作用了。因為在機器「眼」里世界是數學抽象的,它不需要理解或者想像高維空間,只需要將低維空間的運算規則推廣到高維空間即能處理一系列求解。
進一步,計算機尋找最佳參數的方式叫做梯度下降法(理解需要一點入門級別的微積分。)
這種方法很容易讓我聯想起曾經看過的一系列與時間循環有關的科幻電影。
電影講述主角被困於某段時間線中,每次死亡以後又都會回到時間線的最初。
每次重生到再死亡的過程中也都會多獲得關於事件一點的信息,最後所有拼湊起來的信息還原了事件的真相。
如果把電影中的每次重生到死亡的過程看成一次「迭代」,那麼計算機尋找最佳參數的梯度下降法就是迭代,每次迭代都向最優方向前進一點點,當迭代非常多的次數後最終就能非常逼近最優參數。
這跟現實生活中人的學習方法很接近,所以機器學習叫Machine Learning而不叫Machine Fitting,或者Machine Predicting什麼的…...
區別只是計算機的迭代時間可能1萬次只用了幾秒鐘,而人類,因為現實中不會真的有時間循環讓你去重複經歷同一件事,可能在重大問題上迭代個幾次大概一輩子就過去了…...
所以,概括地說機器學習做的事情就是輸入訓練數據集,給定一種建模方式,計算機自動尋找最佳擬合參數使模型可以描述數據集中輸入和輸出的對應關係。並用這個模型來預測新輸入數據的過程。
準備工作
既然已經知道機器學習是什麼了,我們就要著手開始製作自己的模型了。參考上方關係圖,我們需要準備點什麼哪?
首先,我們需要一個開源庫,不用自己寫一大堆晦澀艱深的數學公式去指導機器計算,只要傳參數就可以傻瓜式操作了。
Python有一個機器學習的庫Scikit-learn就很好用。為了熟悉庫,需要看下使用文檔。
我們還需要一台能計算的電腦,我就用了公司配發的低配行政筆記本電腦 。
解決特定領域問題的時候,該領域的專業知識會幫助你。比如,你要通過人臉表情照片去識別笑容,就需要了解一點圖形學,知道計算機「看」照片是一個像素矩陣,每個像素點的灰度值是一個數字等等。
最後,就是最重要的數據源了。選擇數據的質量和規模是直接影響模型表現的最重要因素。更多的時候可能我們想的到要解決哪些問題,卻根本不知道從哪兒去找數據源…...
這裡我們選擇做一個上海二手房的成交估價模型,因為相對數據更好採集。
採集還是通過爬蟲來實現,對象則是最受廣大爬蟲玩家歡迎的房產網站「鏈家網」。
動手採集前我們需要先看下鏈家「二手房成交」板塊房產詳情頁,分析下大致哪些特徵可能對判斷成交價格有用。
區域,板塊、小區名稱、成交價格、成交日期這幾項是必須採集的。掛牌價格、成交天數、帶看、關注、瀏覽量這幾項假設想進一步分析成交時間的話會有用,可采可不採。
戶型、樓層、面積、朝向、梯戶比這幾項是直覺與價格有關的因素,所以採下來。
小區信息中建築年份、物業費、總樓棟數和總戶數這四個特徵我們也認為與成交價格有關,所以採集下來。
這裡你可能會問為啥沒有採集「小區均價」哪?估算房價最直接的不應該是小區均價嗎?
其實是因為鏈家網站推算小區均價的邏輯,這裡的小區均價計算的是「掛牌價格」的均價。
留意下案例中這套房子,成交均價是46259元/平,並且成交時間就是離現在很近的9.30日。
而小區的平均掛牌均價是57507元/平。直覺告訴我們房產雖然具有投資屬性但並不可能在20天內有這樣大規模的波動,既然我們研究的問題是成交,那麼就以成交價格為準。
最後要採集的就是配套了。作為一個實驗案例我這裡並沒有採集醫院,學校等信息。而是著重採集了小區經緯度和周邊1.5公里直線距離內的地鐵站個數,地鐵線路條數。
整個爬蟲的代碼是比較簡單的,類似爬取「鏈家網」的博文CSDN上可以找到很多,用到的庫就是beautifulsoup,這邊就不贅述了。
貼一下調試爬取地鐵配套部分的代碼吧,這裡需要調用下百度地圖的API來定位到小區經緯度,並且用POI來查找周邊地鐵站個數和地鐵線路數,返回json格式再解析出來。
# 輸入上海任意小區名,列印出周邊1.5公里直接距離內的地鐵站個數,名稱,地鐵線路和步行距離
name_estate = input("輸入小區名字: ")
#中文轉碼utf-8
name_estate_quoted = quote("上海"+name_estate)
#調用百度地圖API獲得小區經緯度
find_location ="http://api.map.baidu.com/geocoder/v2/?address="+name_estate_quoted+"&output=json&ak=你申請的ak"
page = urlopen(find_location).read()
content = json.loads(page,encoding="utf-8")
o_lat = content["result"]["location"]["lat"]
o_lng = content["result"]["location"]["lng"]
#用獲取的小區經緯度作為參數,調用百度地圖的POI查找周邊地鐵站。radius傳範圍大小(米),output返回格式
find_metro = r"http://api.map.baidu.com/place/v2/search?query=%E5%9C%B0%E9%93%81&location="+str(o_lat)+","+str(o_lng)+"&radius=1500&output=json&ak=你申請的ak"
page = urlopen(find_metro).read()
results = json.loads(page,encoding ="utf-8")["results"]
counts = len(results)
print("【"+name_estate+"】周邊1.5公里範圍內共有"+str(counts)+"個地鐵站,分別是:")
forresult in results:
d_lat = result["location"]["lat"]
d_lng = result["location"]["lng"]
#調取百度地圖算路,mode傳出行方式(步行,駕車等), origin傳出發地經緯度,destination傳目的地經緯度
calcu ="http://api.map.baidu.com/direction/v1?mode=walking&origin="+str(o_lat)+","+str(o_lng)+"&destination="+str(d_lat)+","+str(d_lng)+r"&origin_region=%E4%B8%8A%E6%B5%B7&destination_region=%E4%B8%8A%E6%B5%B7&output=json&ak=你申請的ak"
obj = urlopen(calcu).read()
content = json.loads(obj,encoding ="utf-8")
res = content["result"]["routes"][]
distance = res["distance"]
duration = res["duration"]
print(result["name"]+" "+result["address"]+" 步行距離約"+str(int(distance))+"米"+" 耗時約"+str(int(duration/60))+"分鐘")
另外一個採集前需要考慮的問題是,我們是否有必要控制下數據時效性?
假設我們打算只估計近期二手房成交價格,那麼因為價格的波動,太久遠的數據反而可能讓模型產生偏差。
所以我們圈定了一個時間範圍為7月至今。最後,採集完成後就得到了大約7901組數據。
這樣準備工作就完成了。
對數據的清洗和預處理
到這裡為止上面那堆數據還不能直接拿來訓練模型,我們還需要對其進行清洗和預處理。
▌處理虛擬變數
第一個問題是機器無法處理像類似「兩梯三戶」這種文字特徵,或者說這種表述方式無法給予機器有效信息。
一種處理方法是我們將這個特徵做成「虛擬變數」或這叫One-Hot編碼,其實就是一個01矩陣。
打個比方來說,在梯戶比這個特徵上假設可能出現的結果有「一梯兩戶」,「一梯四戶」,「兩梯三戶」,「兩梯四戶」這4種可能性,一個「一梯四戶」房產就表示為下面這種形式
這個編碼可以用pandas的get_dummies方法來實現,非常方便。假設你不想逐一設定列名的話,使用get_dummies之前唯一要小心的點在於要確認所有數值型的數據類型不是object類型,否則get_dummies是會把數值類型特徵也虛擬變數化的。
因為虛擬變數會大大增大特徵維度,造成計算量上升。而梯戶比的實際含義是數值,也可以直接處理成兩列,一列代表梯數,一列代表戶數。顯然「梯戶比」這個特徵這裡處理成數值更好。
最終我們直接去除了「小區信息」,沒有把它作為輸入變數。原因一是假設對小區進行虛擬變數變換的話會大大增加數據維度從而對計算性能提出更高的要求。
二是我們目前的數據量沒有足夠大到覆蓋上海所有小區,假設預測新數據的小區並沒有出現訓練數據集里則會造成特徵不一致的問題,代碼會直接報錯。
▌填充缺失值
其次,網路採集的數據都可能會存在大量缺失值。比如下面這種「暫無信息」。
在我們這個訓練數據集里,有缺失值的數據有703條,幾乎佔了總數據量的9%。如果我們不想損失掉這些數據就不能粗暴的將它們刪除,而是要設定一定的方式對缺失值填空。
這裡我們可以用numpy的isnull方法來查找下哪些列有缺失值,發現是「成交時間」、「朝向」、「電梯」、「建設年份」和「物業費」這5列。
#處理「暫無信息」或者「暫無數據」
for_training = for_training.apply(lambda x:x.replace("暫無信息",np.nan).replace("暫無數據",np.nan)
for_training.isnull().any()
其中成交天數我們最終不打算把它作為輸入特徵,可以隨便給它一個值後不用管它。朝向我們統一給它填充「南」,有無電梯我們按照2000年前
建設年份按照同板塊樓盤建設年份的平均數來估。物業費則按照同建設年份物業費的平均數來估。
▌查找異常值
上面這些都完成了以後還需要觀察下現有數據。
想像下在老張買菜的案例裡面,如果他記錄賬本的那段時間正好碰到白菜大減價,那麼輸入大量減價後的價格特徵,模型一定會產生偏斜。
在二手房的問題上像下面這種成交價格低的不可思議的(相對上海房價來說),或者掛牌價格和成交價格相差巨大的,就可以判定為典型異常值。
這裡我們用統計學的分箱圖來排除異常值,我們計算下成交均價的log變換後做下分箱:
分箱圖的看法是這樣的,中間紅線代表「中位數」,箱體的上下邊緣分別是「上四分位」和「下四分位」。上下四分位間的距離叫做「四分位距」。而上下超過1.5倍四分位距的數值都被判斷為異常值。這裡大約要刪除53組數據。
刪除後可以看到二手房成交均價的分布1.598~12.612萬之間,較為符合我們對上海房價的邏輯常識認知了。
完成這步後,最後得到了一個7833x245的數據集。去除不作為輸入的信息,基本上可以知道我們輸入數據的維度在240左右。
訓練模型、調參和可視化
我們來為模型選擇一種演算法,這裡預測二手房成交價格是個回歸問題,我們選擇RandomForestRegression隨機森林回歸。
與一開始老張買菜的案例不同,二手房問題的複雜度高的多。線性模型我在這裡也調試了下,表現最好的情況是L1正則化以後的Lasso可以達到0.84分(滿分為1,表示100%的數據可用模型解釋),這個分數不算太低。
但樹集成類演算法在這個問題上可以表現更好。關於隨機森林的原理有興趣的可以自行百度,簡單來說可以理解為N棵隨機的決策樹通過分叉後覆蓋所有數據,然後再取平均。
因為scikit-learn是個傻瓜式工具包,我們只需要為演算法調節一些參數。分別是隨機樹的棵樹(n_estimators)和樹的最大深度(max_depth)。在scikit-learn裡面最佳參數的查找也是可以用網格搜索grid_search查找的。
#讀取清洗好的數據集
data = pd.read_csv(r"你的目錄shhouse_dummies.csv",header =,encoding ="gbk")
#打亂數據集
data = data.reindex(np.random.permutation(data.index))
#設計成交價格為預測目標
target = data["deal_price"]
#刪除不作為輸入特徵的列
data.drop("deal_price",axis =1,inplace =True)
data.drop("post_price",axis =1,inplace =True)
data.drop("deal_days",axis =1,inplace =True)
data.drop("price_per_area",axis =1,inplace =True)
data.drop("community",axis =1,inplace =True)
#分割數據(註:正規做法是這裡是要將數據集分割為訓練集和測試集的,由於我們下面會啟動五折交叉驗證,為了節省數據集就不再分割了)
#X_train,X_test,y_train,y_test = train_test_split(data,target,random_state = 1)
X_train = data
y_train = target
#調用scikit-learn的網格搜索,傳入參數選擇範圍,並且制定隨機森林回歸演算法,cv = 5表示5折交叉驗證
param_grid = {"n_estimators":[5,10,50,100,200,500],"max_depth":[5,10,50,100,200,500]}
grid_search = GridSearchCV(RandomForestRegressor(),param_grid,cv =5)
#讓模型對訓練集和結果進行擬合
grid_search.fit(X_train,y_train)
print(np.around(grid_search.best_score_,2))
我們嘗試兩個參數在[5,10,50,100,200,500]中各種排列組合的可能性,並對訓練集進行5折交叉驗證(平均分成五分,每次各用不同的四份來訓練,用剩下的一份來測試)來選出最優參數。
完了以後運行代碼就是等待了。根據機器的計算性能需要等待不同的時間,我的行政筆本等待的時間約為20-30分鐘左右。
結束後可以看到最終我們獲得了一個約0.90分的模型,即約90%的數據可以用模型來解釋,這高於了線性模型約6個百分點。該模型最佳的參數選擇是500棵樹,50層深度。
我們還可以將不同參數的組合結果用Matplotlib的imshow可視化一下,代碼如下:
#畫最優參數的熱力圖選擇
fig = plt.figure(figsize = (16,9))
ax = fig.add_subplot(1,1,1,facecolor ="whitesmoke",alpha =0.2)
ax.imshow(df,cmap ="summer")
ax.set_xlim(-0.5,5.5)
ax.set_ylim(-0.5,5.5)
ax.set_xticklabels([,5,10,50,100,200,500],fontsize =18)
ax.set_yticklabels([,5,10,50,100,200,500],fontsize =18)
ax.set_xlabel("n_estimators",fontsize =18)
ax.set_ylabel("max_depth",fontsize =18)
foriinrange(,6,1):
forjinrange(,6,1):
ax.text(i,j,str(np.around(df.iloc[j,i],3)),fontsize=15,verticalalignment="center",horizontalalignment="center",color ="black")
▌得到如下結果:
這就完了?你可能會說這樣一點都不直觀啊!我該怎麼去解釋這個完成的模型是什麼樣的哪?
還好,scikit-learn的樹演算法還提供了一個叫特徵權重的屬性。我們可以把這個屬性調出來可視化一下,看下從機器的「眼睛」如何解讀影響房價的這些特徵因素。代碼是這樣的:
#特徵重要前十位性可視化
features = X_train.columns
importance = grid_search.best_estimator_.feature_importances_
fi = pd.Series(importance,index = features)
fi = fi.sort_values(ascending = False)
ten = fi[:10]
fig = plt.figure(figsize = (16,9))
ax = fig.add_subplot(1,1,1,facecolor ="whitesmoke",alpha = 0.2)
ax.grid(color ="grey",linestyle=":",alpha = 0.8,axis ="y")
ax.barh(ten.index,ten.values,color ="dodgerblue")
ax.set_xticklabels([0.0,0.1,0.2,0.3,0.4,0.5,0.6],fontsize = 22)
ax.set_yticklabels(ten.index,fontsize = 22)
ax.set_xlabel("importance",fontsize = 22)
▌結果如下:
上圖展示了機器評價重要程度前10位的特徵,所有重要程度的和為1。
可以看出排在第1位的是「面積」,的確符合常識,面積是與總價關聯性最強的因素,影響權重在0.6左右。
第2位有點出人意料,機器選中的是「物業費」。一種可能性是物業費的高低反應了小區的檔次,小區的檔次是影響不同樓盤單價差異的重要因素。
第3,4位分別是「地鐵站數量」和「地鐵線路數」,這個比較符合常識,交通方便理論上房價就會提升。
第5,6位是經緯度,也就是小區在上海的實際位置,反應的是區位。
後7,8,9,10位都是與小區有關的特徵,有「總戶數(單元數)」、「建設年份」、「總層數(高度)」和「總戶數」。
排除建設年份這點,樓盤設計師們應該會很高興,因為機器認為一個樓盤的規劃設計參數的確影響了房價。
模型預測實戰VS「房價網」估價器
堅持看到這裡的你一定希望看下模型的應用效果吧?這肯定比給出一個0.90分的評分更直觀,我們就來試一下。
由於我們的訓練數據集來自鏈家,測試的時候就不能再用鏈家數據來測試,某則模型會給你一個100%準確的預測結果。我們需要一個全新的數據集。
這裡我們選擇了也可以查到成交數據的「Q房網」。選取Q房第一頁8月底至今的成交數據20條。
為了讓測試更有意思一點,我們特別讓模型對比了下「房產大數據平台」的「房價網」的估價器。
下面就是最終結果。
自製模型大多數情況誤差都小於12%,總體要好於「房價網」的估價器,其中高於15%的誤差總共出現了3次,在預測區域高均價房產上的表現較差。而「房價網」估價器高於15%的誤差出現了6次,總體誤差範圍更大。
查找誤差原因和改進模型
為了進一步改進,嘗試找下自製模型較大誤差部分產生的原因。
以第7號數據「大華鉑金華府」,誤差27.26%為例,我們懷疑可能原因是數據分布造成的。為了驗證用Matplotlib來看下該樓盤所在的寶山區-大場板塊訓練數據集的成交均價分布。
整個寶山區的價格分布集中於3-5萬之間,「大華鉑金華府」在整個寶山區屬於偏右側尾部均價偏高的樓盤,成交均價約為62222元/平方米。
而它所在大場板塊,訓練數據集中最高成交均價也僅為4.897萬。果然與我們猜測的情況是一致的。這裡也可以看出我們開頭提到的模型的性能表現主要取決於訓練集的數據規模和質量。
優化這個模型的一種方式(可能)是放寬時間維度以換取更大的訓練數據集體量,或者多渠道獲取數據集,以保證訓練集含有一定數量的高成交均價樣本。
作者:胡蘿蔔,CSDN博客專家。普通的地產從業人員,職業是市場BD。自學Python和數據分析,希望讓技術接地氣,解決實際工作和日常生活中的問題。
微信改版了,
想快速看到CSDN的熱乎文章,
趕快把CSDN公眾號設為星標吧,
打開公眾號,點擊「設為星標」就可以啦!
※演算法和編程面試題精選 TOP50!
※用 Python 分析《斗破蒼穹》,分析其究竟是爛片無疑還是滄海遺珠?
TAG:CSDN |