机器学习助您一臂之力。 第一部分

您是否曾经寻找过公寓? 您想增加一些机器学习并使过程更有趣吗?


叶卡捷琳堡的公寓

今天,我们将考虑应用机器学习来找到最佳单位。


引言


首先,我想澄清这一刻,并解释“最佳公寓”的含义。 这是一个具有一系列不同特征的单位,例如“区域”,“区域”,“阳台数量”等。 对于这些功能,我们希望有一个特定的价格。 看起来像一个需要几个参数并返回数字的函数。 也许还有一个黑匣子可以提供一些魔力。


但是……有一个很大的“但是”,有时您可能会遇到由于地理位置等一系列原因而被高估的公寓。 此外,城市中心还有城镇以外的地区,都享有较高的声誉。 或者...有时人们想出售他们的公寓,因为他们搬到了地球的另一个地方。 换句话说,有许多因素会影响价格。 听起来很熟悉吗?


退一步


在继续之前,请允许我进行一点抒情离题。
我住在叶卡捷琳堡(欧洲和亚洲之间的城市,这是2018年举办过世界足球锦标赛的城市之一)五年。


我爱上了这些水泥丛林。 我讨厌那个城市的冬季和公共交通。 这是一个发展中的城市,每个月都有成千上万的公寓要出售。


是的,这是一个人满为患,污染严重的城市。 同时-这是分析房地产市场的好地方。 我从互联网上收到了很多关于公寓的广告。 我将进一步使用该信息。


另外,我尝试在叶卡捷琳堡地图上形象化显示不同的报价。 是的,这是来自habracut的引人注目的照片,是在Kepler.gl上制作的


图片


2019年7月在叶卡捷琳堡出售了超过2000套一居室公寓。 它们的价格有所不同,从不到一百万卢布到近一千四百万卢布。


这些点是指其地理位置。 地图上点的颜色表示价格,蓝色附近的价格越低,红色附近的价格越高。 您可以将其视为冷色和暖色的类比,暖色越大价格就越高。
拜托,记住那一刻,颜色越红,值越高。 相同的想法适用于蓝色,但价格最低。


现在,您对图片有了大致的了解,分析的时间到了。


目标


我住在叶卡捷琳堡时想要什么? 我想找一个足够好的公寓,或者如果我们谈论ML,我想建立一个模型,该模型会给我有关购买的建议。


一方面,如果某个公寓的定价过高,则该模型应建议通过显示该公寓的预期价格来等待价格下降。
另一方面-根据市场状况,如果价格足够好-也许我应该考虑该报价。


当然,没有什么理想的,我准备接受计算中的错误。 通常,对于此类任务,请使用预测的平均误差,而我准备将误差降低到10%。 例如,如果您有2-3百万俄罗斯卢布,则可以忽略200-300,000的错误,您可以负担得起。 在我看来。


准备


正如我之前提到的,有很多公寓,让我们仔细看看。
将熊猫作为pd导入


df = pd.read_csv('flats.csv') df.shape 

图片


2310个单位住了一个月,我们可以从中提取一些有用的东西。 一般数据概述呢?


 df.describe() 

图片
没有什么特别的东西-经度,纬度,单位价格(标签为“ cost ”)等。 是的,那一刻我使用“ 成本 ”而不是“ 价格 ”,希望不会引起误解,请一视同仁。


清洁用品


每个记录都有相同的含义吗? 其中有些是像小隔间这样的有代表性的公寓,您可以在那儿工作,但不想住在那儿。 他们是狭窄的小房间,不是真正的公寓。 让我们删除它们。


 df = df[df.total_area >= 20] 

单位价格的预测来自经济学和相关领域中最古老的问题。 与“ ML”一词无关,人们试图根据平方米/英尺来猜测价格。
因此,我们查看这些列/标签,并尝试获取它们的分布。


 numerical_fields = ['total_area','cost'] for col in numerical_fields: mask = ~np.isnan(df[col]) sns.distplot(df[col][mask], color="r",label=col) plot.show() 

图片


好吧...没有什么特别的,看起来像是正态分布。 也许我们需要更进一步?


 sns.pairplot(df[numerical_fields]) 

图片


糟糕...出了点问题。 清除这些字段中的异常值,然后尝试再次分析我们的数据。


 #Remove outliers df = df[abs(df.total_area - df.total_area.mean()) <= (3 * df.total_area.std())] df = df[abs(df.cost - df.cost.mean()) <= (3 * df.cost.std())] #Redraw our data sns.pairplot(df[numerical_fields]) 

图片


离群值已消失,现在看起来更好。


转型


指向建造年份的标签“ year”应该转换为更有意义的东西。 让它成为建筑物的年龄,换句话说,就是一栋特定的房子是多么古老。


 df['age'] = 2019 -df['year'] 

让我们看一下结果。


 df.head() 

图片


有各种各样的数据,分类的,Nan值,文本描述和一些地理信息(经度和纬度)。 让我们抛开最后一个,因为在那个阶段它们是无用的。 我们稍后会再与他们联系。


 df.drop(columns=["lon","lat","description"],inplace=True) 

分类数据


通常,对于分类数据,人们使用不同类型的编码或诸如CatBoost之类的东西,它们提供了与数字变量一起使用它们的机会。
但是,我们可以使用更合理,更直观的方法吗? 现在是时候让我们的数据更容易理解而不丢失其含义了。


地区


嗯,有二十多个可能的地区,我们可以在模型中添加20多个其他变量吗? 当然可以,但是……应该吗? 我们是人,我们可以比较事物,不是吗?
首先-并非每个区都等同。 在城市中心,一平方米的价格更高,离市区更远-价格下降。 听起来合乎逻辑吗? 我们可以使用吗?
是的,我们绝对可以匹配具有特定系数的任何地区,而其他地区则是较便宜的公寓。


匹配城市并使用另一个Web服务地图(ArcGIS Online)后,其视图已更改,并且具有相似的视图
图片


我使用了与Flat可视化相同的想法。 最“有名望”和“最昂贵”的地区以红色和最少的颜色-蓝色。 色温,您还记得吗?
另外,我们应该对数据框进行一些操作。


 district_map = {'alpha': 2, 'beta': 4, ... 'delta':3, ... 'epsilon': 1} df.district = df.district.str.lower() df.replace({"district": district_map}, inplace=True) 

相同的方法将用于描述公寓的内部质量。 有时它需要一些修理,有时平坦则可以居住。 在其他情况下,您应该花更多的钱使它看起来更好(更换水龙头,粉刷墙壁)。 也可能有使用系数。


 repair = {'A': 1, 'B': 0.6, 'C': 0.7, 'D': 0.8} df.repair.fillna('D', inplace=True) df.replace({"repair": repair}, inplace=True) 

顺便说一下,关于墙壁。 当然,它也会影响公寓的价格。 现代材料比旧材料好,砖比混凝土好。 木质墙是一个颇具争议的时刻,也许它是乡村的好选择,但对城市生活却不是那么好。


我们使用与以前相同的方法,并对不知道的行提出建议。 是的,有时人们不会提供有关其公寓的所有信息。 此外,根据历史,我们可以尝试猜测墙壁的材质。 在特定的时间段内(例如赫鲁晓夫的领导时期)-我们了解典型的建筑材料。


 walls_map = {'brick': 1.0, ... 'concrete': 0.8, 'block': 0.8, ... 'monolith': 0.9, 'wood': 0.4} mask = df[df['walls'].isna()][df.year >= 2010].index df.loc[mask, 'walls'] = 'monolith' mask = df[df['walls'].isna()][df.year >= 2000].index df.loc[mask, 'walls'] = 'concrete' mask = df[df['walls'].isna()][df.year >= 1990].index df.loc[mask, 'walls'] = 'block' mask = df[df['walls'].isna()].index df.loc[mask, 'walls'] = 'block' df.replace({"walls": walls_map}, inplace=True) df.drop(columns=['year'],inplace=True) 

此外,还有有关阳台的信息。 以我的拙见-阳台是一件非常有用的东西,所以我忍不住考虑。
不幸的是,有一些空值。 如果广告的作者检查了有关该广告的信息,我们将获得更现实的信息。
好吧,如果没有信息,它将意味着“没有阳台”。


 df.balcony.fillna(0,inplace=True) 

在那之后,我们删除带有有关建造年份信息的列(我们有一个很好的替代方法)。 另外,我们删除包含建筑物类型信息的列,因为它具有很多NaN值,而且我没有找到任何机会来填补这些空白。 然后,我们使用NaN删除所有行。


 df.drop(columns=['type_house'],inplace=True) df = df.astype(np.float64) df.dropna(inplace=True) 

检查中


所以...我们使用了一种非标准的方法,将分类值替换为其数值表示形式。 现在,我们完成了数据转换。
删除了一部分数据,但总的来说,它是一个很好的数据集。 让我们看一下自变量之间的相关性。


 def show_correlation(df): sns.set(style="whitegrid") corr = df.corr() * 100 # Select upper triangle of correlation matrix mask = np.zeros_like(corr, dtype=np.bool) mask[np.triu_indices_from(mask)] = True # Set up the matplotlib figure f, ax = plt.subplots(figsize=(15, 11)) # Generate a custom diverging colormap cmap = sns.diverging_palette(220, 10) # Draw the heatmap with the mask and correct aspect ratio sns.heatmap(corr, mask=mask, cmap=cmap, center=0, linewidths=1, cbar_kws={"shrink": .7}, annot=True, fmt=".2f") plot.show() # df[columns] = scale(df[columns]) return df df1 = show_correlation(df.drop(columns=['cost'])) 

图片


嗯...这变得非常有趣。
正相关
阳台总面积 。 为什么不呢 如果我们的公寓很大,将会有一个阳台。
负相关
总面积年龄 。 较新的公寓,居住面积更大。 听起来合乎逻辑,新的比旧的更宽敞。
年龄阳台 。 越平的阳台越少。 好像是通过另一个变量的关联。 也许这是一个三角形的Age-Balcony-Area,其中一个变量对另一个变量具有隐式影响。 搁置一段时间。
年龄-地区。 较老的公寓将被安置在享有较高声誉的地区。 可能与中心附近的较高价格有关吗?


此外,我们可以看到与因变量的相关性


 plt.figure(figsize=(6,6)) corr = df.corr()*100.0 sns.heatmap(corr[['cost']], cmap= sns.diverging_palette(220, 10), center=0, linewidths=1, cbar_kws={"shrink": .7}, annot=True, fmt=".2f") 

图片


我们开始...


单位面积与价格之间有很强的相关性。 如果您想拥有更大的居住空间,将需要更多的钱。
年龄/成本 ”和“ 地区/成本 ”对之间存在负相关。 新房中的公寓比旧房便宜。 在农村,公寓便宜。
无论如何,这似乎很清楚而且可以理解,所以我决定使用它。


型号


对于通常与预测单位价格有关的任务,请使用线性回归。 根据前一阶段的显着相关性,我们也可以尝试使用它。 它是适合许多任务的主力。
准备我们的数据以备后用


 from sklearn.model_selection import train_test_split y = df.cost X = df.drop(columns=['cost']) X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42) 

此外,我们创建了一些简单的函数来预测和评估结果。 让我们先尝试预测价格!


 def predict(X, y_test, model): y = model.predict(X) score = round((r2_score(y_test, y) * 100), 2) print(f'Score on {model.__class__.__name__} is {score}') return score def train_model(X, y, regressor): model = regressor.fit(X, y) return model 

 from sklearn.linear_model import LinearRegression regressor = LinearRegression() model = train_model(X_train, y_train, regressor) predict(X_test, y_test, model) 

图片


好吧……准确度为76.67%。 是不是很大? 根据我的观点,这还不错。 此外,这是一个很好的起点。 当然,这不是理想的,并且有改进的潜力。


同时-我们试图仅预测数据的一部分。 将相同的策略应用于其他数据该怎么办? 是的,需要进行交叉验证的时间。


 def do_cross_validation(X, y, model): from sklearn.model_selection import KFold, cross_val_score regressor_name = model.__class__.__name__ fold = KFold(n_splits=10, shuffle=True, random_state=0) scores_on_this_split = cross_val_score(estimator=model, X=X, y=y, cv=fold, scoring='r2') scores_on_this_split = np.round(scores_on_this_split * 100, 2) mean_accuracy = scores_on_this_split.mean() print(f'Crossvaladaion accuracy on {model.__class__.__name__} is {mean_accuracy}') return mean_accuracy do_cross_validation(X, y, model) 

图片


交叉验证的结果现在我们得到另一个结果。 73小于76。但是,直到我们拥有一个更好的候选人之前,它也是一个不错的候选人。 同样,这意味着线性回归在我们的数据集上非常稳定。


现在是最后一步的时候了。


我们将研究线性回归的最佳特征。
与更复杂的模型相反,该模型家族具有更好的理解能力。 只有一些带有系数的数字,您可以将您的数字放在等式中,进行一些简单的数学运算并得出结果。


让我们尝试解释我们的模型


 def estimate_model(model): sns.set(style="white", context="talk") f, ax = plot.subplots(1, 1, figsize=(10, 10), sharex=True) sns.barplot(x=model.coef_, y=X.columns, palette="vlag", ax=ax) for i, v in enumerate(model.coef_.astype(int)): ax.text(v + 3, i + .25, str(v), color='black') ax.set_title(f"Coefficients") estimate_model(regressor) 

我们模型的系数


图片看起来很合逻辑。 阳台/墙壁/面积/维修可为固定价格做出积极贡献。
距离越远, 负贡献越大。 也适用于年龄。 单位越大,价格越低。


因此,这是一次迷人的旅程。
我们从头开始,基于人类的观点(数字代替虚拟变量),检查的变量及其相互关系,使用非典型的方法进行数据转换。 之后,我们建立了简单的模型,并使用交叉验证对其进行了测试。 就像蛋糕上的樱桃一样-看一下模型的内部结构,这使我们对自己的方式充满信心。


但是! 这不是旅程的终点​​,只是休息。 将来,我们将尝试更改模型,也许(只是也许)它会提高预测的准确性。


感谢您的阅读!


第二部分在那里

PS源数据和Ipython笔记本位于此处

Source: https://habr.com/ru/post/zh-CN468053/


All Articles