【技术干货】国外大神 手把手教你通过Python机器学习租到低价的公寓

浏览: 2455

如果你曾经找过公寓,你就会明白这个过程可能是多么令人沮丧。它不仅耗费时间,而且即使当你发现一个自己喜欢的公寓,你怎么知道它就是合适的公寓?

你可能在心里设定了目标预算和区域。但是,如果你和我是同一类人,那么你也许愿意做一些权衡。例如,我住在纽约市,那么靠近地铁站这样的便利设施毫无疑问是一个很大的加分项。但是,这点到底值多少钱?我是否应该拿有电梯的住所和靠近火车站的住所进行交换?步行到火车站多少分钟?抵得过走上楼梯吗?租房的时候,有几十个这样的问题需要考虑。那么,如何使用机器学习来帮助我们进行决策呢?

Clipboard Image.png

在 20 世纪 70 年代初,如果你想购买股票,就需要聘请经纪人,他们会收取你将近 1% 的固定佣金;如果你想购买一张机票,你需要联系旅行社代理,他们将赚取大约 7% 的佣金;如果你想出售一间房子,你会联系一个房地产代理,他们赚取 6% 的佣金。在 2016 年,你基本上可以免费地做前两者。而对于最后一项,情况仍然和 20 世纪 70 年代的一样,保持不变。

为什么是这种情况?更重要的是,这些与机器学习有什么关系?现实是,这一切都归结于数据,以及谁能够访问它。

你可能想象着通过API或爬取房地产网站,就能够很容易地访问珍贵的地产房源数据。你错了,如果你打算遵守这些网站的条款和条件的话。房地产数据受到房地产经纪人国家协会(NAR)的严格控制,由他们运行多项房源服务(MLS)。这是一种聚合房源数据的服务,只有经纪人和代理商可以使用它,而且还需要花费巨资。所以,可以想象,他们不太希望任何人都能大量地下载这些数据。

这是不幸的,因为开放这些数据无疑会催生许多有价值的消费者应用程序。对于占家庭预算最大比重的购买决策而言,这点看上去尤其重要。

话虽如此,也不是完全没有希望。虽然依据条款所言,直接从MLS提供商获取数据是被禁止的,但是我们可以利用第三方工具来拉取数据。

现在,我们来看一个有用的工具,它可以帮助我们获取所需的数据。

使用import.io抓取房源数据

有许多优秀的、基于Python的库用于抓取网页,包括requests、Beautiful Soup和Scrapy。为了达到此处的目的,我们将使用免费的替代方案:

Import.io(http://www.import.io)是一个免费的、基于Web的服务,它会自动抓取网页。这是一个很好的选择,让我们可以避免从头开始创建一个网络爬虫。好在,它为房地产的房源数据提供了一个示例API接口,数据来自Zillow.com。

图 1 的图片来自http://www.import.io/examples。在import.io的搜索框中输入Zillow.com,检索Zillow数据的样例。


他们所提供的数据是有关旧金山的,不过在我们的例子中将使用纽约。为了更换城市,需要使用我们感兴趣的数据所在的网址,来替换演示所提供的网址。

为了实现这点,我们可以打开一个单独的浏览器选项卡,并导航到Zillow.com。在那里执行一个公寓搜索。让我们将公寓搜索限制在曼哈顿地区,价格在 $1500 到 $3000 之间,如图 2 所示。


一旦有结果返回,我们需要从浏览器地址栏中复制Zillow.com站点的URL,并将其粘贴到之前选项卡中import.io的提取框中。

复制图 2 中Zillow.com地址栏中的URL,并将其粘贴到import.io的提取框中,如图 3 所示

图3

单击左上角的提取数据(Extract Data)按钮,你将看到一个结果表,只显示你想要的数据。

现在,我们可以通过单击“下载CSV”(Download CSV)按钮,轻松地下载这些数据。弹出的对话框会问我们需要下载多少页,从结果页可以看出在Zillow的搜索返回了 2640 条结果,我们需要下载 106 页来获得整个数据集。而Import.io仅仅允许我们下载 20 页,现在也只能如此了。

Clipboard Image.png

我们现在有一个包含 500 套公寓的数据集。来看看其中有什么。首先在Jupyter记事本中,使用pandas导入数据。

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

plt.style.use('ggplot') 
%matplotlib inline 

pd.set_option("display.max_columns", 30) 
pd.set_option("display.max_colwidth", 100) 
pd.set_option("display.precision", 3) 

# Use the file location of your Import.io csv 
CSV_PATH = r"/Users/alexcombs/Downloads/magic.csv" 

df = pd.read_csv(CSV_PATH) 
df.columns

上述代码生成图 4 中的输出。

图4

最后一行df.columns为数据提供了列标题的输出。此外,让我们使用df.head().T查看数据的某些样本。在行结束处的.T语法将转置我们的数据框并垂直地显示它,如图 5 所示。


图5

我们已经可以看出数据有一些缺失值(NaN)。需要多个操作来标准化此数据。数据集中的列(或者说是图 5 中转置后的行)表示了每个Zillow房源的单项数据。看起来似乎有两种类型的房源—— 一种类型是单个单元,而另一种类型是多个单元。

这两种类型可以在图 6 中看到。

图6

这两个房源对应于在Zillow.com上所看到的图像,如图 7 所示。

图7

拆分这些的关键是listingtype_value这个列头。我们将数据拆分为单一的单元,Apartment for Rent,以及多个单元,Apartments for Rent:

# multiple units
mu = df[df['listingtype_value'].str.contains ('Apartments For')]

# single units
su = df[df['listingtype_value'].str.contains('Apartment For')]

现在来看看每种房源类型的数量。

len(mu)

上述代码生成以下输出。

161len(su)

上述代码生成以下输出。

339

由于大多数房源属于单一单元的类型,我们现在将从此开始。

接下来,我们需要将数据格式化为标准结构。例如,至少需要为卧室数、浴室数、平方英尺和地址各准备一列。

从之前的观察中可以发现,我们已经有一个清晰的价格列,那就是pricelarge_value_prices。幸运的是,该列中没有缺失值,因此我们不会因为缺少数据而丢失任何的房源。

卧室和浴室的数量以及平方英尺将需要一些解析,因为它们全都挤在单一的列中。让我们解决这个问题。

先来看一下该列。

su['propertyinfo_value']

上述代码生成如图 8 所示的输出。

图8

看上去,数据似乎总是包括卧室和浴室的数量,偶尔也会包含例如年份这样的额外信息。在我们继续解析之前,先来检验一下这个假设。

# 检查没有包含'bd''Studio'的行数 

len(su[~(su['propertyinfo_value'].str.contains('Studio')\ |su['propertyinfo_value'].str.contains('bd'))])

上述代码生成以下输出。

0

现在来看看下面几行代码。

#检查没有包含'ba'的行数 

len(su[~(su['propertyinfo_value'].str.contains('ba'))])

上述代码生成以下输出。

6

看来有几行缺少浴室数量的数据。出现这种情况的原因有多种,我们可以使用一些方法来解决这个问题。一种就是填充或插补这些缺失的数据点。

关于缺失数据的主题,这里我建议投入一些时间来理解这个课题,它是建模过程中一个关键的组成部分。然而,这并非此处讨论的主要目的,所以我们将假设数据的缺失是随机的,即使删除这些没有浴室信息的房源,也不会使得我们的样本产生不恰当的偏向。

# 选择拥有浴室的房源  

no_baths = su[~(su['propertyinfo_value'].str.contains('ba'))]

# 再排除那些缺失了浴室信息的房源

sucln = su[~su.index.isin(no_baths.index)]

现在我们可以继续解析卧室和浴室信息:

# 使用项目符号进行切分
def parse_info(row):            
    if not 'sqft' in row:
     
              br
, ba = row.split('')[:2]
         
              sqft
= np.nan
   
         
else:
       
              br
, ba, sqft = row.split('.')[:3]
 
         
return pd.Series({'Beds': br, 'Baths': ba, 'Sqft': sqft})

attr = sucln['propertyinfo_value'].apply(parse_info)
attr

上述代码生成图 9 的输出。

图9

这里我们做了些什么?我们在propertyinfo_value列上运行了apply函数。然后该操作返回一个数据框,其中每个公寓属性都会成为单独的列。在最终完成之前,还有几个额外的步骤。我们需要在取值中删除字符串(bdbasqft),并且需要将这个新的数据框和原始的数据进行连接。让我们现在就这么做吧。

#在取值中将字符串删除 

attr_cln = attr.applymap(lambda x: x.strip().split(' ')[0]
if
isinstance (x,str) else np.nan)

attr_cln

上述代码生成图 10 的输出。

图10

让我们来看看下面的代码。

sujnd = sucln.join(attr_cln) 

sujnd.T

上述代码生成图 11 的输出。

到了这个时刻,各方面的数据集开始聚集在一起了。我们可以基于卧室的数量、浴室的数量和面积的平方英尺数,来测试关于公寓价值的假设。但是,正如行业专家所说,房地产的区域最为关键。让我们采取和之前相同的属性解析方法,并将其应用到公寓的地址上。

如果可能,我们还将尝试提取楼层的信息。这里我们假设一个模式,其中一个数字后面跟随一个字母,而该数字就表示建筑物的楼层。

# parse out zip, floor 

def parse_addy(r):  
    so_zip
= re.search(', NY(\d+)', r)
 
    so_flr
= re.search('(?:APT|#)\s+(\d+)[A-Z]+,', r)
 
   
if so_zip:
     
         zipc
= so_zip.group(1)
 
   
else:
     
         zipc
= np.nan
   
   
if so_flr:
       
         flr
= so_flr.group(1)
 
   
else:
     
        flr
= np.nan
 
   
return pd.Series({'Zip':zipc, 'Floor': flr})

flrzip = sujnd['routable_link/_text'].apply(parse_addy)

suf = sujnd.join(flrzip)

suf.T

图11

上述代码生成图 12 的输出。

图12

正如你所看到的,当楼层和邮编信息出现的时候,我们能够成功地解析出它们。这使我们从333个房源中获得了320个带有邮政编码信息的房源和164个带有楼层信息的房源。

最终进行一点清理,然后我们即将开始检查这个数据集。

# 我们将数据减少为所感兴趣的那些列 

sudf = suf[['pricelarge_value_prices', 'Beds', 'Baths', 'Sqft', 'Floor', 'Zip']]

# 我们还会清理奇怪的列名,并重置索引
sudf.rename(columns={'pricelarge_value_prices':'Rent'}, inplace=True)

sudf.reset_index(drop=True, inplace=True)

sudf

上述代码生成图 13 的输出。

图13

分析数据

到了这个阶段,数据已经是我们分析时所需要的格式了。让我们从一些总体的统计数据分析开始。

sudf.describe()

上述代码生成图 14 的输出。

图14

这里可以看到租金的统计细分。不要忘记我们从Zillow的原始数据中,只选择了每月价格在 1500 到 3000 美元之间的公寓。在这里无法看到的是卧室和浴室的平均数量,或者楼层的平均数。导致这个现象的问题有两个。第一个问题涉及卧室。我们需要所有的数据都为数值型才能获得统计。可以将工作室公寓认定为一个零卧室的公寓(实际也确实如此),来解决这个问题。

# 我们将出现的'Studio'替换为0 

sudf.loc[:,'Beds'] = sudf['Beds'].map(lambda x: 0 if 'Studio' in x else x)
sudf

上述代码生成图 15 的输出。

图15

这解决了第一个问题,但我们还有另一个问题。任何需要统计数据的列必须是数值类型。正如你在图 16 的截图所见,情况并非如此。

图16

sudf.info()

上述代码生成图 16 的输出。

我们可以通过更改数据类型来解决这个问题,如下面的代码所示。

# 让我们解决列中数据类型的问题 
sudf.loc[:,'Rent'] = sudf['Rent'].astype(int)
sudf.loc[:,'Beds'] = sudf['Beds'].astype(int)

# 存在半间浴室的情况,因此需要浮点型
sudf.loc[:,'Baths'] = sudf['Baths'].astype(float)

# 存在NaNs,需要浮点型,但是首先要将逗号替换掉
sudf.loc[:,'Sqft'] = sudf['Sqft'].str.replace(',','')

sudf.loc[:,'Sqft'] = sudf['Sqft'].astype(float)
sudf.loc[:,'Floor'] = sudf['Floor'].astype(float)

让我们执行下面的这行代码并看看结果如何。

sudf.info()

上述代码生成图 17 的输出。

图17

让我们执行下面的代码行,以便得到最终的统计数据。

sudf.describe()

上述代码生成图 18 的输出。

图18

租金、卧室、浴室和平方英尺的数字都看起来不错,但是Floor楼层这一列似乎有些问题。在纽约,确实有很多非常高的建筑,但我想没有超过 1000 层的。

快速看过数据之后,你会发现APT 1107A给了我们这个结果。很可能,这是一个 11 层的公寓,但是为了安全性以及一致性,我们会放弃这个房源。幸运的是,这是唯一超出 30 楼的房源,所以我们的数据仍然是完好的状态。

# 索引标号318是有问题的房源,这里放弃它 
sudf = sudf.drop([318])

sudf.describe()

上述代码生成图 19 的输出。

图19

我们的数据现在看起来不错,接下来继续分析的步骤。让我们生成数据的透视图,首先通过邮政编码和卧室数量来检视价格的情况。Pandas有一个.pivot_table()函数,使这个操作变得很容易。

sudf.pivot_table('Rent', 'Zip', 'Beds', aggfunc='mean')

上述代码生成图 20 的输出。

图20

此操作可让我们按照邮政编码来查看平均价格。正如你所见,随着房间数量的增加,我们将看到越来越少的房源,NaN值就是很好的证明。为了进一步探究其原因,我们可以基于房源的数量进行透视。

sudf.pivot_table('Rent', 'Zip', 'Beds', aggfunc='count')

上述代码生成图 21 的输出。


图21

从图 21 可以看出,根据邮政编码和卧室数量的维度来分析,我们的数据是稀疏的。这是不幸的,理想情况下,我们应该需要更多的数据。尽管如此,我们仍然可以进行分析。

现在要通过可视化的方式来检视手头的数据。

Clipboard Image.png

由于目前的数据是基于邮政编码的,因此最好的可视化方法是使用热图。如果你不熟悉热图,那么简单地来理解它只是按照色谱来表示数据的可视化。现在,让我们使用名为folium的Python映射库来实现这一点(https://github.com/python-visualization/folium)。

由于缺少包含两到三间卧室的公寓,让我们缩减数据集,聚焦到工作室和一间卧室的房源。

su_lt_two = sudf[sudf['Beds']<2]

现在我们将继续创建可视化。

import folium 

map = folium.Map(location=[40.748817, -73.985428], zoom_start=13)
map.geo_json(geo_path=r'/Users/alexcombs/Downloads/nyc.json', data=su_lt_two,          
              columns
=['Zip', 'Rent'],
         
              key_on
='feature.properties.postalCode',
       
              threshold_scale
=[1700.00, 1900.00, 2100.00, 2300.00, 2500.00,
2750.00],          
              fill_color
='YlOrRd', fill_opacity=0.7, line_opacity=0.2,
           
              legend_name
='Rent (%)',
       
               reset
=True)
map.create_map(path='nyc.html')

上述代码生成图 22 的输出。

这里发生了很多事情,所以让我们一步一步来分析。导入folium后,我们创建了一个.Map()对象。为了使地图居中,还需要传入坐标和缩放级别。我在Google上搜索了帝国大厦的坐标(你需要使用经度的正负符号),并调整缩放,使帝国大厦出现在我想要居中的地方。

下一行代码需要一个称为GeoJSON文件的东西。这是一个表示地理属性的开放格式来。通过搜索NYC GeoJSON文件,我找到了一个,特别是它还包含了邮政编码的映射。一旦传入了GeoJSON文件与邮政编码之后,你还需要传入数据框。

然后你需要引用键列(在这个例子中为Zip)以及你希望用于热图的列。在我们的例子中将使用租金的中位数。其他选项用于确定颜色的调色板、颜色改变的取值以及某些用于调整图例和着色的其他参数。最后一行代码确定了输出文件的名称。

图22

如果你在本地机器上使用这些代码,你可能会在Chrome浏览器中遇到一个问题。阴影部分似乎不正常。Chrome认为其是跨域请求,因此拒绝执行它,而且由于此,你将无法看到热图的叠加部分。Internet Explorer和Safari浏览器应该可以正常显示。

随着热图完成,我们可以感受到哪些地区有更高的或更低的租金。如果你租房的时候关注某个特定的区域,这将很有帮助。不过,让我们继续使用回归建模,进行更为深入的分析。

Clipboard Image.png

让我们开始使用一个和两个卧室的数据集。我们将观察邮政编码和卧室数量对于出租价格的影响。这里将使用两个包:第一个是statsmodels,而第二个包:patsyhttps://patsy.readthedocs.org/en/latest/index.html)和statsmodels搭档使用,使工作更轻松。在运行回归的时候,Patsy让我们可以使用R风格的公式。

让我们现在开始吧。

import patsy 
import statsmodels.api as sm

f = 'Rent ~ Zip + Beds' y, X = patsy.dmatrices(f, su_lt_two, return_type='dataframe')

results = sm.OLS(y, X).fit()
print(results.summary())

上述代码生成图 23 的输出。 

图23

通过这几行代码,我们刚刚运行了第一个机器学习算法。

虽然大多数人不倾向于将线性回归视为机器学习,但它实际上就是机器学习。线性回归是一种监督式的机器学习。在这种情况下,监督只是意味着我们为训练集提供了输出值。

现在,让我们解释这其中发生的事情。在引入包之后,有两行和patsy模块相关的代码。第一行是我们将要使用的公式。在左手边(波浪号之前)是反应或因变量,也就是Rent。在右手边,是独立或预测变量,就是ZipBeds。这个公式表示,我们想知道邮政编码和卧室数量将如何影响出租价格。

然后我们的公式将和包含相应列名的数据框一起,传递给patsy.dmatrices()。然后设置Patsy,让它返回一个数据框,其中X矩阵由预测变量组成,而y向量由响应变量组成。这些将被传递给sm.OLS(),之后调用.fit()来运行我们的模型。最后,打印出模型的结果。

如你所见,输出的结果提供了大量的信息。让我们从最上面的部分开始吧。可以看到模型包括了 262 个观察样本,调整后的R20.283F-statistic1.21e-10,具有统计的显着性。这里显著性是指什么?它意味着我们所创建的模型,仅仅使用卧室数量和邮政编码,就已经能够解释约三分之一的价格差异。这是一个满意的结果吗?为了更好地回答这个问题,让我们来看看输出的中间部分。

中间部分为我们提供了模型中每个自变量的有关信息。从左到右,我们可以看到以下信息:变量、变量在模型中的系数、标准误差、t统计量、t统计量的p值,以及 95% 的置信区间。

这一切告诉我们什么?如果看p值这一列,我们可以确定独立变量从统计的角度来看是否具有意义。在回归模型中具有统计学意义,这意味着一个独立变量和响应变量之间的关系不太可能是偶然发生的。通常,统计学家使用0.05的p值来确定这一点。一个0.05的p值意味着我们看到的结果只有 5% 的可能性是偶然发生的。就这里的输出而言,卧室的数量显然是有意义的。那邮政编码怎么样呢?

首先要注意的是,我们的截距代表了 10001 的邮政编码。建立线性回归模型的时候,是需要截距的。截距就是回归线和y轴交叉的地方。Statsmodels会自动选择一个预测变量作为截距。在这里,它决定使用纽约的切尔西地区(10001)。

就像卧室的数量,截距在统计上是显着的。但是,其他邮政编码又怎么样呢?

在大多数情况下,它们并不显著。不过,让我们来看看显著的几个。邮政编码——100271002910035——都是非常显著的,并且都具有很高的负置信区间。这告诉我们,和切尔西地区一个类似的公寓相比,这些地区往往会有较低的租金价格。

因为切尔西被认为是纽约的一个时尚之地,而另三个街区都在哈林区及其附近——它们当然不会被认为是时尚的地方——模型与我们对真实世界的直觉,是相吻合的。

现在让我们使用这个模型进行一些预测。

预测

假设根据前面的分析,我们对三个特定的邮政编码感兴趣:100021000310009。我们应该如何使用已有的模型,来确定为某个公寓支付多少钱呢?下面来看看吧。

首先,需要理解模型的输入是什么样子,这样我们才知道如何输入一组新的值。让我们来看看X矩阵。

X.head()

上述代码生成图 24 的输出。

图24

我们可以看到,输入是用所谓的虚拟变量进行编码的。由于邮政编码不是数字的,所以为了表示这个特征,系统使用了虚拟编码。如果某个公寓在10003中,那么该列将被编码为1,而所有其他邮政编码都被编码为0。而卧室是数值型的,所以系统将根据实际的数字对其进行编码。现在,让我们创建自己的输入行进行预测。

to_pred_idx = X.iloc [0] .index 
to_pred_zeros = np.zeroslento_pred_idx))
tpdf = pd.DataFrameto_pred_zerosindex = to_pred_idxcolumns = ['value'])

tpdf

上述代码生成图 25 的输出。

图25

我们刚刚使用了X矩阵的索引,并用零填充数据。现在让我们填入一些实际的值。我们要对一个位于10009区域的、包含一间卧室的公寓进行估价。

tpdf.loc['Intercept'] = 1 
tpdf.loc['Beds'] = 1
tpdf.loc['Zip[T.10009]'] = 1

tpdf

对于线性回归,截距值必须设置为 1 ,模型才能返回正确的统计值。

上述代码生成图 26 的输出。

图26

这里我们可以看到截距和10009邮政编码已经被设置为1了。

在图 27 中,我们可以看到卧室的数量也已经被设置为1了。

图27

我们已经将特征设置为了适当的值,现在使用该模型返回一个预测。

 results.predict(tpdf['value'])

上述代码生成如下输出。

2529.5604669841355

请记住,results是我们保存模型的变量名。这个模型对象有一个.predict()方法,我们使用自己的输入值调用该方法。正如你可以看到的,模型返回了预测的值。

如果我们想要在条件中增加一间卧室怎么办?

来改变一下输入并看看结果。

tpdf['value'] = 0 
tpdf.loc['Intercept'] = 1
tpdf.loc['Beds'] = 2
tpdf.loc['Zip[T.10009]'] = 1

tpdf

上述代码生成了图 28 的输出。

图28

我们可以看到卧室数量已经被更新为2

现在,我们将再次运行预测。

results.predict(tpdf['value'])

上述代码生成以下输出。

2738.035104645339

看起来,额外增加的卧室每个月将花费我们大约 200 美元。如果我们选择 10002 地区呢?让我们在代码中实现这个。

tpdf['value'] = 0 
tpdf.loc['Intercept'] = 1
tpdf.loc['Beds'] = 2
tpdf.loc['Zip[T.10002]'] = 1

results.predict(tpdf['value'])

上述代码生成以下输出。

2651.1763504369078

根据我们的模型,如果选择10002而不是10009地区,我们可以在两卧室的公寓上少花一些钱。

扩展模型

到了目前这个阶段,我们只检视了邮政编码、卧室和出租价格之间的关系。虽然这个模型有一定的解释能力,但是,我们的数据集太小,使用的特征也太少,无法充分地观测房地产估值这个复杂的市场。

然而,幸运的是,我们即将向该模型添加更多的数据和特征,而且可以使用完全相同的框架来扩展我们的分析。

未来可扩展的探索包括利用Foursquare或Yelp API所提供的餐馆和酒吧数据,或者是Walk Score这类供应商所提供的可步行性和交通便利性指标。

要扩展这个模型有很多的方法,我建议你在一个方向上持续努力,例如探索各种指标。随着每天更多的数据被发布,你的模型会不断地改善。



推荐 3
本文由 datakong 创作,采用 知识共享署名-相同方式共享 3.0 中国大陆许可协议 进行许可。
转载、引用前需联系作者,并署名作者且注明文章出处。
本站文章版权归原作者及原出处所有 。内容为作者个人观点, 并不代表本站赞同其观点和对其真实性负责。本站是一个个人学习交流的平台,并不用于任何商业目的,如果有任何问题,请及时联系我们,我们将根据著作权人的要求,立即更正或者删除有关内容。本站拥有对此声明的最终解释权。

0 个评论

要回复文章请先登录注册