引言
隨著城市化進程的加速,人口流動日益頻繁,租房市場作為城市生活的重要組成部分,其價格波動對居民生活質量和城市經濟發展具有顯著影響。成都市,作為中國西部地區的經濟、文化、交通和科技中心,近年來吸引了大量人才和企業,租房需求持續增長。然而,租房價格的不確定性給租戶和房東帶來了諸多不便。為了更好地理解租房市場的動態,預測租房價格成為一項重要的研究課題。
研究方法
本文將采用多種數據分析方法,包括統計分析、機器學習等,以確保模型的準確性和可靠性。
研究目標
本文旨在通過構建基于成都市各區(市)縣的租房價格預測模型,分析影響租房價格的關鍵因素。
數據信息
數據源自某租房平臺網站,數據為公開數據,通過整合形成表格,本數據需要進行清洗,沒有國外數據好用。
過程……
讀入數據:
import matplotlib.pylab as plt
import re
import numpy as np
import seaborn as sns
import pandas as pd
plt.rcParams['font.sans-serif'] = ['SimHei'] #解決中文顯示
plt.rcParams['axes.unicode_minus'] = False #解決符號無法顯示
import warnings
warnings.filterwarnings('ignore')
# 導入plotly庫,這是一個基于Web的交互式圖表庫,允許創建豐富的、交互式的數據可視化圖表。
import plotly as py
# 導入cufflinks庫,這是一個用于Pandas DataFrame的Plotly繪圖接口,可以讓Pandas DataFrame直接通過cufflinks的API繪制Plotly圖表。
import plotly.express as px
import plotly.graph_objects as go df = pd.read_csv('成都.csv')
df.tail()
原始數據長下面這樣,顯示后5行:
我們先看下數據信息、重復值、缺失值情況:
有4列存在缺失值情況,181行數據重復。
# 刪除缺失值
df.dropna(inplace=True)
# 刪除重復值
df.drop_duplicates(inplace=True)
df.shape
刪完剩余2786行, 8列。
數據清洗:
# 定義一個函數,用于從字符串中提取括號前的中文部分
def extract_chinese_before_brackets(s): # 檢查傳入的參數s是否為字符串類型 if isinstance(s, str): # 使用正則表達式搜索字符串s,查找從開頭到第一個全角左括號(之前的所有內容 match = re.search(r'^(.*?)(', s) # 如果找到了匹配項 if match: # 返回匹配到的內容(即括號前的所有字符) return match.group(1) else: # 如果沒有找到匹配項(即沒有括號或括號前沒有內容),返回空字符串 return '' else: # 如果傳入的參數s不是字符串類型,直接返回空字符串 return '' # 應用上面定義的函數到DataFrame的'樓層'列
# 使用apply方法,將函數應用于'樓層'列的每一個元素
# 并將函數的返回值(即每個元素括號前的中文部分)存儲在新的列'樓層類型'中
df['樓層類型'] = df['樓層'].apply(extract_chinese_before_brackets) df.tail()
上面定義一個函數,用于提取“樓層”列最左邊的中文文字,存儲于新建列“樓層類型”列并顯示后5行,詳見下圖:
# 使用str.extract方法從'樓層'列的每個字符串中提取括號內的數字(\d+)
# 這里的正則表達式'((\d+)層)'用于匹配全角左括號'('后跟著一個或多個數字'\d+',然后是全角右括號和'層'字
# 提取的數字(即括號內的內容)將被存儲在新的列'樓房層數'中
df['樓房層數'] = df['樓層'].str.extract(r'((\d+)層)')
# 使用drop方法刪除原始的'樓層'列
# 參數columns='樓層'指定了要刪除的列名
# inplace=True表示在原地修改DataFrame,而不是返回一個新的DataFrame
df.drop(columns='樓層', inplace=True)
# 使用head方法查看修改后的DataFrame的前幾行(默認是前5行)
# 這有助于驗證'樓房層數'列是否已正確添加,并且'樓層'列是否已被刪除
df.head()
再利用正則表達式,提取出括號內的數字,也就是樓層并存儲于新列,顯示前5行數據,詳見下圖:
# 提取的結果將被分配到新的列'臥室間數'、'客廳間數'和'衛生間數'
df[['臥室間數', '客廳間數', '衛生間數']] = df['戶型'].str.extract(r'(\d+)室(\d+)廳(\d+)衛', expand=True)
df.drop(columns='戶型', inplace=True)
df.head()
新建三列’臥室間數’, ‘客廳間數’, '衛生間數’用于存儲戶型內的如“3室1廳2衛 ”便于后續的機器學習,還是利用正則表達式提取,詳見下圖:
df[['區(市)縣', '地域', '樓盤名稱']] = df['位置'].str.split('-', expand=True, n=2)
df.drop(columns='位置', inplace=True)
df.head()
新建’區(市)縣’, ‘地域’, '樓盤名稱’三列,用于存儲“位置”列如“青羊-外光華-凱德風尚 ”用于后續的數據分析,詳見下圖:
現在我們來看看“朝向”列有哪些不重復的唯一值:
可以看到國內的數據很亂,現在要做的是有“東”或“西”字眼的我們統一把他替換為“東西”,有“南”或“北”字眼的我們統一把他替換為“南北”。繼續寫個函數來處理數據。
# 定義轉換函數
def transform_direction(direction): if pd.isna(direction): # 檢查是否為 NaN return np.nan # 或者你可以選擇返回其他默認值,如 '未知' if isinstance(direction, str): # 確保是字符串類型 if '東' in direction or '西' in direction: return '東西' elif '南' in direction or '北' in direction: return '南北' else: return direction # 返回原始字符串(這里可能包含'未知'等) else: return np.nan # 如果不是字符串類型,返回 NaN(或其他默認值) # 將 '朝向' 列轉換為字符串類型,并處理 NaN 值
df['朝向'] = df['朝向'].astype(str)
# 應用轉換函數到 Series 的每個元素
df['朝向'] = df['朝向'].apply(transform_direction)
df.tail()
處理完成,顯示后5行,詳見下圖:
再看下其他列有沒有問題:
沒啥問題,現在將數字類型的列轉為相對應的類型:
df = df.astype({'金額': 'int','樓房層數': 'int','臥室間數': 'int','客廳間數': 'int','衛生間數': 'int'}) # 指定字段轉指定類型
df.info()
搞定,進入下一階段內容。
探索性分析(EDA)
# 使用Plotly繪制箱型圖
fig = go.Figure(data=[go.Box( y=df['金額'], # 這里y軸對應你的租金數據 name='房屋租金' # 圖例名稱
)]) # 自定義圖表標題和軸標簽
fig.update_layout(title='房屋租金——箱型圖', yaxis_title='租金', xaxis_title='') # 箱型圖通常不需要x軸標題,除非你有多個箱型圖并列 # 顯示圖表
fig.show()
從上圖可以看出,大部分房屋月租金都比較集中,成都市最高的租金為45000元/月,最小的為500元/月,中位數為2500元/月,還有四分位距、決策邊界等內容。
# 對DataFrame按'區(市)縣'進行分組,并獲取每個組的'房屋租金'數據
grouped = df.groupby('區(市)縣')['金額'] # 創建一個空的圖表對象
fig = go.Figure() # 遍歷每個組,為每個組添加一個箱型圖軌跡
for name, group in grouped: fig.add_trace(go.Box( y=group, name=name, boxpoints='all', # 顯示所有點 jitter=0.3, # 點的抖動量,以防重疊 pointpos=-1.8 # 點的位置 )) # 自定義圖表布局
fig.update_layout( title='按區(市)縣分組的房屋租金——箱型圖', yaxis_title='租金', xaxis_title='', xaxis=dict( type='category', # 設置x軸為類別軸 categoryorder='array', # 自定義類別順序 categoryarray=list(grouped.groups.keys()) # 將dict_keys轉換為列表 )
) # 顯示圖表
fig.show()
分區縣繪制的箱型圖,我們重點關注一下“三遺”之城都江堰,最高的房租為6000元/月。
from scipy.stats import gaussian_kde # 假設df是你的DataFrame,且包含'房屋面積'列 # 提取房屋面積數據
x = df['房子大小'].values # 計算KDE
kde = gaussian_kde(x)
# 生成KDE曲線的x軸數據點(這里假設你想要覆蓋的范圍是x的最小值到最大值,步長為0.1)
x_kde = np.linspace(x.min(), x.max(), 300)
# 計算KDE曲線在這些點上的y值
y_kde = kde(x_kde) # 繪制直方圖
fig = go.Figure()
fig.add_trace(go.Histogram( x=x, histnorm='probability density', # 使得直方圖的面積等于1,與KDE曲線可比較 name='直方圖'
)) # 繪制KDE曲線
fig.add_trace(go.Scatter( x=x_kde, y=y_kde, mode='lines', name='KDE曲線'
)) # 自定義圖表布局
fig.update_layout( title='房屋面積分布(直方圖+KDE)', xaxis_title='房屋面積', yaxis_title='概率密度'
) # 顯示圖表
fig.show()
要解讀這種圖表,需要觀察直方圖中條形的高度和KDE曲線的形狀。如果直方圖的條形較高,表示在相應的面積區間內房屋較多;KDE曲線的峰值則表示最可能的房屋面積。通過比較直方圖和KDE曲線,可以分析房屋面積的分布模式,例如是否存在一個或多個常見的面積區間,或者面積分布是否均勻。
# 創建一個散點圖
fig = go.Figure(data=go.Scatter( x=df['房子大小'], y=df['金額'], mode='markers', # 設置為散點圖 marker=dict( size=10, # 散點大小 color='blue',# 散點顏色 opacity=0.8 # 散點透明度 )
)) # 自定義圖表布局
fig.update_layout( title='房屋面積與租金——散點圖', xaxis_title='房屋面積', yaxis_title='房屋租金'
) # 顯示圖表
fig.show()
散點圖主要是解釋相關性的圖表,不懂的可以點擊鏈接轉到我的博客詳細查看。藍色框框內左邊的數字是面積,右邊是房租。
上圖實際就是一個坐標軸,分x和y軸,每個點代表每個數值,單個數據叫標量,一組數據叫向量,坐標軸內可以視作向量空間。
# 計算每個出租方式的計數
value_counts = df['類型'].value_counts() # 創建餅圖數據
labels = value_counts.index.tolist() # 獲取標簽(出租方式)
values = value_counts.values.tolist() # 獲取計數 # 繪制餅圖
fig = go.Figure(data=[go.Pie(labels=labels, values=values, hole=0.3, textinfo='label+percent', insidetextorientation='radial')]) # 自定義圖表布局
fig.update_layout( title='出租方式分布——餅圖', font_size=12, # 你可以根據需要調整字體大小 legend_title_text='出租方式' # 如果你需要圖例(對于餅圖通常不需要,但這里作為示例)
) # 注意:餅圖通常不需要圖例,因為每個切片都直接標記了標簽和百分比
# 如果你確實需要圖例(盡管這在餅圖中不常見),你可能需要以一種不同的方式來實現它 # 顯示圖表
fig.show()
從上圖可以看出,整租占比達到80.7%。
# 創建箱形圖數據
fig = go.Figure() # 遍歷'出租方式'的唯一值,為每個類別繪制一個箱形圖
for category in df['類型'].unique(): subset = df[df['類型'] == category] fig.add_trace(go.Box( y=subset['金額'], name=category, marker=dict( color='rgb(0, 0, 0)', # 箱體的顏色 outliercolor='rgba(219, 64, 82, 0.6)', # 異常值的顏色 line=dict( color='rgb(0,0,0)', width=1.5 ) ), boxmean='sd' # 'sd'表示使用標準差標記平均值,你也可以設置為'mean'來直接使用平均值 )) # 自定義圖表布局
fig.update_layout( title='按出租方式分組的房屋租金——箱形圖', yaxis_title='房屋租金', xaxis_title='出租方式', boxmode='group' # 箱形圖的顯示模式,'group'表示每個類別的箱形圖并排顯示
) # 顯示圖表
fig.show()
從上圖可以看出,整租的租金浮動較大,而合租的租金區間很小。合租的租金價格在2100元/月以下區間,僅有1個房屋租金是2100元/月,其余都往下在走。
sns.barplot(data=df,x='朝向',y='金額')
plt.show()
南北朝向的房子明顯租金更高。
# 計算每個樓層的計數
floor_counts = df['樓層類型'].value_counts() # 創建餅圖數據
fig = go.Figure(data=[go.Pie( labels=floor_counts.index, # 標簽是樓層的唯一值 values=floor_counts.values, # 值是每個樓層的計數 hole=0.3, # 設置餅圖的中心空洞大小 textinfo='label+percent', # 在餅圖上顯示標簽和百分比 insidetextorientation='radial' # 文本方向為徑向
)]) # 更新圖表的布局
fig.update_layout( title='樓層分布——餅圖', # 設置圖表的標題 annotations=[dict(text=str(round(value, 2))+'%', x=0.5, y=0.5, xref='paper', yref='paper', showarrow=False, font=dict(size=10), textangle=0 if i == 0 else i*360/len(floor_counts)) for i, value in enumerate(floor_counts.values)], # 注釋文本的位置需要手動調整,這里使用了一種簡單的方法,但可能需要根據實際情況調整
) # 顯示圖表
fig.show()
上圖展示了成都市樓層分布情況。
sns.barplot(data=df,x='樓層類型',y='金額')
plt.show()
低樓層租金略微高于中樓層,中樓層租金略微高于高樓層。
# 相關性分析
numeric_columns_simplified = df.select_dtypes(include='number')
sns.heatmap(numeric_columns_simplified.corr(),vmax=1,annot=True,linewidths=0.5,cbar=False,cmap='YlGnBu',annot_kws={'fontsize':12})
plt.xticks(fontsize=10)
plt.yticks(fontsize=10)
plt.title('房屋特征皮爾遜相關系數矩陣——熱力圖',fontsize=17)
plt.show()
跟房租最相關的是他自己,1也就是100%相關,第二相關的是房子大小68%,需要注意的是樓房層數是負相關,也就是樓層越高房屋租金越低。
from wordcloud import WordCloud
# 將'樓層信息'列中的所有文本合并成一個長字符串
text = ' '.join(df['樓盤名稱'].astype(str)) font_path = '/Library/Fonts/Arial Unicode.ttf' # macOS上的示例路徑 # 創建詞云對象,并指定中文字體
wordcloud = WordCloud(width=800, height=400, background_color='black', font_path=font_path).generate(text) # 使用matplotlib顯示詞云
plt.figure(figsize=(10, 5))
plt.imshow(wordcloud, interpolation='bilinear')
plt.axis('off') # 不顯示坐標軸
plt.show()
根據樓盤名稱繪制的詞云圖,可以看出,“保利天悅”出現的頻率很高。
機器學習
特征工程:
# 特征篩選
new_df = df[['金額', '區(市)縣', '類型', '朝向', '樓層類型', '房子大小', '樓房層數', '臥室間數', '客廳間數', '衛生間數']]
new_df = new_df.astype('str')
new_df
選取以下內容作為特征:
很多中文,我們需要把他轉為數字,變成計算機熟悉的分類數據:
from sklearn.preprocessing import LabelEncoder # 定義一個函數,用于對pandas Series進行標簽編碼
def label_encode(series): # 創建一個LabelEncoder實例 le = LabelEncoder() # 使用LabelEncoder的fit_transform方法對Series進行擬合和轉換 # 這將Series中的類別(通常是字符串)轉換為從0到n_classes-1的整數 transformed_series = le.fit_transform(series) # 返回轉換后的Series return transformed_series # 假設new_df是一個已經存在的DataFrame
# 使用apply函數將label_encode函數應用到DataFrame的每一列上
# axis=0表示函數沿著列(垂直方向)應用,即每一列被視為一個Series進行處理
# 注意:這里直接使用apply可能會導致一些問題,特別是如果DataFrame包含非對象類型的列
# 因為LabelEncoder只能用于對象類型(如字符串)的列
# 但為了注釋的目的,我們假設所有列都是對象類型
new_df = new_df.apply(label_encode, axis=0)
new_df
上圖已經全部為了數字。
劃分訓練集和測試集:
from sklearn.model_selection import train_test_split
# 準備數據
X = new_df.drop('金額',axis=1)
y = new_df['金額']
# 劃分數據集
X_train,X_test,y_train,y_test = train_test_split(X,y,test_size=0.2,random_state=42)
print('訓練集大小:',X_train.shape[0])
print('測試集大小:',X_test.shape[0])
定義模型評估函數:
from sklearn.metrics import r2_score, mean_absolute_error, mean_squared_error
# 導入sklearn庫中用于評估模型性能的三個指標:R^2分數、平均絕對誤差(MAE)、均方誤差(MSE) # 定義一個訓練模型并輸出模型的評估指標的函數
def train_model(ml_model): # 打印傳入的模型類型或實例 print("模型名稱: ", ml_model) # 使用訓練數據擬合模型。這里假設X_train和y_train是全局變量或已在此函數外部定義 model = ml_model.fit(X_train, y_train) # 在訓練數據上評估模型的性能(這一步主要用于調試或查看過擬合情況,通常不用訓練集評估模型最終性能) print("模型預測準確率: ", model.score(X_train, y_train)) # 使用訓練好的模型對測試集進行預測 predictions = model.predict(X_test) # 計算并打印R^2分數,評估模型的預測性能 r2score = r2_score(y_test, predictions) print("R方: ", r2score) # 計算并打印平均絕對誤差(MAE),衡量預測值與真實值之間差異的平均絕對值 print('平均絕對誤差(MAE):', mean_absolute_error(y_test,predictions)) # 計算并打印均方誤差(MSE),衡量預測值與真實值之間差異的平方的平均值 print('均方誤差(MSE):', mean_squared_error(y_test,predictions)) # 計算并打印均方根誤差(RMSE),是MSE的平方根,與預測值的尺度相同,便于理解 print('均方根誤差(RMSE):', np.sqrt(mean_squared_error(y_test,predictions))) # 真實值和預測值的差值(繪制殘差分布圖)sns.distplot(y_test - predictions)
構建線性回歸模型:
# 導入LinearRegression類
from sklearn.linear_model import LinearRegression
# 創建LinearRegression的實例
lg = LinearRegression()
# 調用train_model函數,傳入LinearRegression實例作為參數
train_model(lg)
構建決策樹模型:
# 構建決策樹回歸
from sklearn.tree import DecisionTreeRegressor
# 初始化DecisionTreeRegressor并設置防止過擬合的參數
tree = DecisionTreeRegressor()
train_model(tree)
構建xgboost模型:
# 構建xgboost回歸模型
from xgboost import XGBRegressor
xgb = XGBRegressor(learning_rate=1.4, n_estimators=1000)
train_model(xgb)
計算各特征重要程度:
# 首先,定義特征標簽列表,這里假設X_train是一個DataFrame,其列名即為特征名
feat_labels = X_train.columns[0:] # 獲取X_train的所有列名作為特征標簽
# 注意:這里假設xgb是一個已經訓練好的XGBoost模型實例,該模型具有feature_importances_屬性
# feature_importances_屬性包含了模型中每個特征的重要性評分
# importances = xgb.feature_importances_
importances = tree.feature_importances_
# 使用argsort函數對重要性評分進行排序,并通過[::-1]反轉數組,以得到重要性從高到低的索引
# 這樣,indices數組中的第一個索引對應最重要的特征
indices = np.argsort(importances)[::-1] # 初始化兩個列表,用于存儲特征和它們對應的重要性評分
index_list = []
value_list = [] # 遍歷排序后的索引和特征數量(X_train.shape[1]),將特征和重要性評分添加到列表中
# 注意:range(X_train.shape[1])實際上生成的是從0到特征數-1的索引,但在這里我們將其與indices結合使用
# 并在打印時添加了1,以便特征編號從1開始顯示(如果偏好從0開始,則可以去掉f+1)
for f, j in zip(range(X_train.shape[1]), indices): index_list.append(feat_labels[j]) # 將特征名添加到index_list value_list.append(importances[j]) # 將重要性評分添加到value_list print(f + 1, feat_labels[j], importances[j]) # 打印特征編號、名稱和重要性評分 # 使用matplotlib的pyplot模塊創建一個圖形
plt.figure(figsize=(10,6)) # 設置圖形的大小為10x6英寸 # 繪制水平條形圖,展示特征的重要性評分
# 注意:這里使用了[::-1]來反轉列表,因為matplotlib默認是從下到上繪制條形圖
# 但我們想要從最重要的特征開始,即從圖形頂部開始繪制
plt.barh(index_list[::-1], value_list[::-1]) # 設置y軸刻度標簽的字體大小
plt.yticks(fontsize=12) # 設置圖形的標題和字體大小
plt.title('各特征重要程度排序', fontsize=14) # 顯示圖形
plt.show()
從下圖可以看出,影響房屋租金特征排序最高的是房子大小,接著是樓房層數,詳見下圖:
# 模型預測
y_pred = tree.predict(X_test)
result_df = pd.DataFrame()
result_df['真實值'] = y_test
result_df['預測值'] = y_pred
result_df.sample(2)
隨機選擇兩個真實值和預測值對比,對了1個,感覺模型有些過擬合了,想要再準點,還需要對模型調超參數,由于比較復雜,今天就不展示了。
還有一個知識點需要給大家講,下面其實是一個錯誤示范,之前中文轉數字對數據進行了標準化,但在預測時未對y_pred進行相應的逆變換,預測值看起來比原始數據(房屋租金)小了很多,所以下面的數值是標準化后的數值,并不是原始數據。
數據可視化(真實值和預測值對比):
# 創建一個新的圖形,并設置其大小為10x6英寸
plt.figure(figsize=(10,6))
# 繪制預測值。這里只取前200個預測值(y_pred[:200])和對應的索引(range(len(y_test))[:200])
# 使用藍色('b')線條表示預測值,并設置圖例標簽為'predict'
plt.plot(range(len(y_test))[:200], y_pred[:200], 'b', label='預測值')
# 繪制測試集的真實值。同樣只取前200個真實值(y_test[:200])和對應的索引
# 使用紅色('r')線條表示真實值,并設置圖例標簽為'test'
plt.plot(range(len(y_test))[:200], y_test[:200], 'r', label='真實值')
# 添加圖例,并設置其位置在圖形的右上角,字體大小為15
plt.legend(loc='upper right', fontsize=15)
# 設置x軸的標簽為'the number of house',并通過fontdict字典設置其字體權重和大小
plt.xlabel('the number of house', fontdict={'weight': 'normal', 'size': 15})
# 設置y軸的標簽為'value of Price',同樣通過fontdict字典設置其字體權重和大小
plt.ylabel('value of Price', fontdict={'weight': 'normal', 'size': 15})
# 顯示圖形
plt.show()
本文數據集可以點擊本文上方下載,創作不易,點贊、評論、收藏是我創作的動力!