青岛房价可视化与价格分析

新房、二手房、租房:从爬虫到建模

逛B站看到一个爬链家的视频,正好课程论文不知道写什么,遂上手试了试,一不做二不休,陆陆续续完成了这篇博客。

爬虫

新房

爬取的内容长这样:

二手房

租房

—下面是新房的—

数据概览

导入基础包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import numpy as py
import pandas as pd
import re
import os
import statistics as stat
import seaborn as sns
import warnings#忽略错误
warnings.filterwarnings('ignore')
#中文注释

%matplotlib inline
from pylab import *
import matplotlib.pyplot as plt
plt.rcParams['font.sans-serif'] = ['SimHei']
#正常显示负号
plt.rcParams['axes.unicode_minus'] = False

导入文件的函数

1
2
3
4
def readdata():
columns_name = ['name', 'type', 'sale', 'rooms', 'areas', 'city', 'per_price', 'total_price']
df = pd.read_csv('../data/lianjia_qdxinfang.csv', names = columns_name)
return df

导入之后长这样:

1
df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 933 entries, 0 to 932
Data columns (total 8 columns):
 #   Column       Non-Null Count  Dtype 
---  ------       --------------  ----- 
 0   name         933 non-null    object
 1   type         933 non-null    object
 2   sale         933 non-null    object
 3   rooms        723 non-null    object
 4   areas        730 non-null    object
 5   city         933 non-null    object
 6   per_price    933 non-null    object
 7   total_price  620 non-null    object
dtypes: object(8)
memory usage: 58.4+ KB

变量的格式都是object

1
df.isnull().sum()
name             0
type             0
sale             0
rooms          210
areas          203
city             0
per_price        0
total_price    313
dtype: int64

删掉rooms这210个数据,但是total_price怎么也删了一些,还剩下185个缺失值的,也就是说total_price不是唯一缺失的列,他的缺失是因为rooms和areas.

初步清洗数据

1
2
3
4
5
6
7
8
9
10
def step1(df):
df.dropna(axis = 0, subset=['areas','rooms', 'total_price'], how = 'any', inplace=True) #加上了total_price
# 对于 total_price 的处理 把 nan 处理为0
df.fillna(0,inplace = True)
# 把 per_price 的 价格待定 处理为0
for i in df.index:
if df['per_price'][i] == '价格待定':
df['per_price'][i] = 0
df.reset_index(drop = True, inplace = True)
return df
1
df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 538 entries, 0 to 537
Data columns (total 8 columns):
 #   Column       Non-Null Count  Dtype 
---  ------       --------------  ----- 
 0   name         538 non-null    object
 1   type         538 non-null    object
 2   sale         538 non-null    object
 3   rooms        538 non-null    object
 4   areas        538 non-null    object
 5   city         538 non-null    object
 6   per_price    538 non-null    object
 7   total_price  538 non-null    object
dtypes: object(8)
memory usage: 33.8+ KB

正则 尝试

第一次自己写正则表达式,先试试提了个啥出来再写函数

total_price处理

1
2
a = re.findall('\d', df['total_price'][1])
''.join(a)
'675'

areas处理

1
2
a = re.findall('\d*',df['areas'][0])
a[0].split('-')
['49', '105']

rooms处理,单纯分成

1
2
3
a = re.findall('\d',df['rooms'][0])
a = [int(i) for i in a]
stat.mean(a)
1.5

切分字符串 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
def step2(df): 
area_min, area_max = [], []
rooms_mean, price, per_price = [], [], []
for i in df.index:
# per_price
per_price.append(int(df['per_price'][i]))
# cnm 先弄 areas
a1 = re.findall('\d*[-]\d*',df['areas'][i]) # tmd有的是范围有的是一个给定值 这里是范围的正则式子
if a1 == []:
list1 = "".join(re.findall('[\d*]',df['areas'][i])) # list1 是要添加的那个值
area_min.append(int(list1))
area_max.append(int(list1))
else:
a1 = a1[0].split('-') #['49-105'] 切分一下 变为 ['49', '105']
list1 = [int(j) for j in a1] # 将列表中的字符串转换为数字

area_min.append(list1[0])
area_max.append(list1[1])
# ok 好起来了 再弄rooms 算平均值
a2 = [int(j) for j in re.findall('\d',df['rooms'][i])] # 这里就缩写了
rooms_mean.append(ceil(stat.mean(a2))) # 不需要起名字 list3 了
# total_price
if df['total_price'][i] == 0: # 我把nan用0全部填充了
price.append(0)
else:
a3 = re.findall('\d', df['total_price'][i])
list3 = ''.join(a3)
price.append(int(list3))
df['area_min'] = area_min
df['area_max'] = area_max
df['rooms_mean'] = rooms_mean
df['total_price'] = price
df['per_price'] = per_price
df.drop(df.columns[[3,4]], axis = 1, inplace = True) # 删掉 areas 和 rooms
return df

数据探索

词云图 词频统计

1
2
3
4
title_cloud = df['name'].tolist()
with open("../sub/cloud_xinfang.txt", 'w', encoding = 'utf-8') as f:
for i in title_cloud:
f.write(i+'\n') #\t也行 不行再换
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
from wordcloud import WordCloud
import codecs
import jieba
#import jieba.analyse as analyse
import imageio
import os
from os import path
import matplotlib.pyplot as plt
from PIL import Image, ImageDraw, ImageFont

def draw_wordcloud():
comment_text = open('../sub/cloud_xinfang.txt', 'rb').read()
cut_text = " ".join(jieba.cut(comment_text))
color_mask = imageio.imread("../pic/dance_girl01.png") # 读取背景图片
cloud = WordCloud(
#设置字体,不指定就会出现乱码
font_path = "../pic/STFANGSO.ttf",
#设置背景色
background_color = 'white',
#词云形状
mask = color_mask,
#允许最大词汇
max_words = 800,
#最大号字体
max_font_size = 40
)
word_cloud = cloud.generate(cut_text) # 产生词云
word_cloud.to_file("../sub/cloud_xinfang.jpg") #保存图片
# 显示词云图片
plt.imshow(word_cloud)
plt.axis('off')
plt.show()
if __name__ == '__main__':
draw_wordcloud()

词云图

热力图 相关分析

1
2
3
4
5
#分析各个特征与房价的相关性,相关性的分析最好使用热力图
corrmat = df.corr()
fig, axs = plt.subplots(figsize=(12,9))
sns.heatmap(corrmat, square=True, cmap='coolwarm')
axs.set_title('相关分析 - 热力图', size=15)

热力图

正态分布图 分布

1
2
3
4
5
6
7
8
9
10
11
12
13
cont_features = ['total_price', 'per_price']
fig, axs = plt.subplots(figsize=(20, 8),nrows = 1, ncols = 2)
plt.subplots_adjust(right=1.5)

for i, feature in enumerate(cont_features):
sns.distplot(df[feature], hist=True, color='#2ecc71', ax=axs[i])
axs[i].set_xlabel('')
axs[i].tick_params(axis='x', labelsize=20)
axs[i].tick_params(axis='y', labelsize=20)
axs[i].legend(loc='upper right', prop={'size': 20})
axs[i].set_title('{}变量的正态分布图'.format(feature), size=20, y=1.05)

plt.show()

正态分布图

箱线图 分布

箱线图是一种用作显示一组数据分散情况资料的统计图,因形状如箱子而得名。它能显示出一组数据的最大值、最小值、中位数、及上下四分位数。
箱形图绘制须使用常用的统计量,能提供有关数据位置和分散情况的关键信息,尤其在比较不同的母体数据时更可表现其差异。
箱形图的绘制主要包含六个数据节点,需要先将数据从大到小进行排列,然后分别计算出它的上边缘,上四分位数,中位数,下四分位数,下边缘,还有一个异常值。

1
2
3
4
5
6
7
f,ax = plt.subplots(figsize=(8,6))
fig = sns.boxplot(x = 'city', y = 'total_price', data = df)
plt.xlabel('')
plt.ylabel('总价(万元)')
plt.title('不同城区的新房总价对比箱线图')
plt.show()
#plt.savefig('../sub/boxplot1.png',width=6,height=4, dpi=900,bbox_inches='tight')

箱线图

特征type 插入

为什么什么type这个列?还不是因为数据偏态分布严重,所以想把房价分成两部分,一种是95%,另一种是那剩下的5%。
95%象征的就是大部分人可以接受的价格。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
def bak(tmp):
g = df.groupby('city').describe()
g = g.total_price
df1 = pd.DataFrame()
df2 = pd.DataFrame()
list1, list2 = [], []

for j in g.index:
q3 = g.loc[j][6]
q1 = g.loc[j][4]
up = q3 + 1.5 *(q3 - q1)
down = q1 - 1.5 *(q3 - q1)
for i in df.index:
if df.city[i] == j:
if (tmp.loc[i,'total_price'] <= up) & (tmp.loc[i,'total_price'] >= down): # 正态分布的
list1.append(i)
tmp1 = tmp.drop(list1) # 扔掉
if (tmp.loc[i,'total_price'] < down) | (tmp.loc[i,'total_price'] > up): # 过大过小的
list2.append(i)
tmp2 = tmp.drop(list2) # 扔掉
tmp1.reset_index(drop = True, inplace = True)
tmp2.reset_index(drop = True, inplace = True) # df1 是异常值, df2是属于正态分布的
tmp1['y'] = 0
tmp2['y'] = 1 # 正态分布
return tmp1, tmp2

数据探索

接回上一章节的数据探索

主程序

1
2
3
4
5
if __name__ == '__main__':
df = readdata()
df = step1(df)
df = step2(df)
df1, df2 = bak(df)

箱线图

重新做箱线图

1
2
3
4
5
6
7
f,ax = plt.subplots(figsize=(8,6))
fig = sns.boxplot(x = 'city', y = 'total_price', data = df2)
plt.xlabel('')
plt.ylabel('总价(万元)')
plt.title('不同城区的新房总价对比箱线图')
plt.show()
#plt.savefig('../sub/boxplot1.png',width=6,height=4, dpi=900,bbox_inches='tight')

小提琴图 分布

是用来展示多组数据的分布状态以及概率密度。这种图表结合了箱形图和密度图的特征,主要用来显示数据的分布形状。跟箱形图类似,但是在密度层面展示更好。在数据量非常大不方便一个一个展示的时候小提琴图特别适用。

1
sns.violinplot(data=df1['total_price'], color='#e74c3c')
<matplotlib.axes._subplots.AxesSubplot at 0x2a80366a128>

频数条形图 统计个数

1
2
3
4
5
6
7
8
9
10
fig, axs = plt.subplots(figsize=(20, 8))

sns.countplot(x='city', data = df[df['sale'] == '在售'])
plt.xlabel('城区', size=15, labelpad=20)
plt.ylabel('房屋个数累计', size=15, labelpad=20)
plt.tick_params(axis='x', labelsize=15)
plt.tick_params(axis='y', labelsize=15)
plt.legend(loc='upper right', prop={'size': 15})
plt.title('各个城区在售的房屋数量柱状图')
plt.show()
No handles with labels found to put in legend.

提取sale状态为在售的房子,对各个城区进行计数。

1
2
3
4
5
6
7
8
9
fig, axs = plt.subplots(figsize=(20, 8))

sns.countplot(x='rooms_mean', hue='city', data=df)
plt.xlabel('室厅数', size=15, labelpad=20)
plt.ylabel('房屋个数累计', size=15, labelpad=20)
plt.tick_params(axis='x', labelsize=15)
plt.tick_params(axis='y', labelsize=15)
plt.legend(loc='upper right', prop={'size': 15})
plt.show()

各个城市的室厅数,一般都是几室几厅的说,这个由于原数据的问题,只能取一个新特征。

1
2
3
4
5
6
7
8
fig, axs = plt.subplots(figsize=(20, 8))
sns.countplot(x='city', hue='sale', data = df)
plt.xlabel('城区', size=15, labelpad=20)
plt.ylabel('房屋个数累计', size=15, labelpad=20)
plt.tick_params(axis='x', labelsize=15)
plt.tick_params(axis='y', labelsize=15)
plt.legend(loc='upper right', prop={'size': 15})
plt.show()

各个城区的房子销售情况,取值是在售、售罄、未开盘。

1
2
3
4
5
6
7
8
9
fig, axs = plt.subplots(figsize=(20, 8))
sns.countplot(x='city', hue='type', data = df)
plt.xlabel('城区', size=15, labelpad=20)
plt.ylabel('房屋个数累计', size=15, labelpad=20)
plt.tick_params(axis='x', labelsize=15)
plt.tick_params(axis='y', labelsize=15)
plt.legend(loc='upper right', prop={'size': 15})
plt.title('在售房种类', size = 15)
plt.show()

各个城区挂卖的房子种类

—下面是二手房—

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 1
import numpy as py
import pandas as pd
import re
import os
import statistics as stat
import seaborn as sns
import warnings#忽略错误
warnings.filterwarnings('ignore')

%matplotlib inline
from pylab import *
import matplotlib.pyplot as plt
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False

1
2
3
4
5
6
import xgboost as xgb
from xgboost.sklearn import XGBClassifier
from sklearn.metrics import roc_curve, auc
from sklearn.model_selection import StratifiedKFold
from sklearn.preprocessing import OneHotEncoder, LabelEncoder, StandardScaler
from sklearn.preprocessing import OneHotEncoder, LabelEncoder, StandardScaler

导入文件

导入数据 并合并所有的数据文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 2
upath = 'C:/.../data/ershoufang/' # 举例
list1 = os.listdir(upath)
for filename, i in zip(list1, range(len(list1))):
columns_name = ['title', 'address', 'houseInfo', 'followInfo', 'tags', 'totalPrice', 'unitPrice']
#name1 = re.findall('^[a-z]*[_][a-z]*[_][a-z]*', filename[i])

tmp = pd.read_csv(upath + filename, names = columns_name) # 读取文件后的临时变量
tmp['city'] = re.findall('[a-z]*',filename)[4] # shinan
tmp.drop('followInfo', axis = 1, inplace = True)
if i == 0:
df = tmp
else:
df = pd.concat([df,tmp] ,axis = 0) #ignore_index=True
df.reset_index(drop = True, inplace = True)
df.head()

title address houseInfo tags totalPrice unitPrice city
0 美立方套三精装修满五唯一 户型方正 南北通透 天泰城美立方 ,十梅庵 3室1厅 | 89.84平米 | 南 北 | 精装 | 中楼层(共11层) | 2011年建... VR房源,房本满五年,随时看房 142.0 单价15806元/平米 chengyang
1 白沙河公园套二洋房,户型方正,刚需好房 崂山水岸绿洲 ,夏庄 2室2厅 | 87.99平米 | 南 | 精装 | 中楼层(共6层) | 板楼 VR房源,房本满五年 105.0 单价11934元/平米 chengyang
2 精装小套三,次顶楼,看河看景,全天采光 卓越蔚蓝群岛 ,女姑 3室2厅 | 85.65平米 | 南 | 精装 | 高楼层(共11层) | 2015年建 | 板楼 VR房源,房本满五年,随时看房 124.0 单价14478元/平米 chengyang
3 急售,精装修,好楼层,价格可议,随时可看房。 中铁华胥美邦 ,流亭 3室2厅 | 88.87平米 | 南 北 | 精装 | 中楼层(共13层) | 2014年建... VR房源,房本满两年,随时看房 135.0 单价15191元/平米 chengyang
4 天泰城美立方套三双卫自住装修保持好 天泰城美立方 ,十梅庵 3室2厅 | 123.93平米 | 南 北 | 精装 | 中楼层(共9层) | 2012年建... VR房源,房本满两年,随时看房 186.0 单价15009元/平米 chengyang
1
df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 23169 entries, 0 to 23168
Data columns (total 7 columns):
 #   Column      Non-Null Count  Dtype  
---  ------      --------------  -----  
 0   title       23169 non-null  object 
 1   address     23169 non-null  object 
 2   houseInfo   23169 non-null  object 
 3   tags        22440 non-null  object 
 4   totalPrice  23169 non-null  float64
 5   unitPrice   23169 non-null  object 
 6   city        23169 non-null  object 
dtypes: float64(1), object(6)
memory usage: 1.2+ MB

切分字符串 提取有用内容

提取含信息的变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
# 3
def floor_count(a):
#中楼层(共11层)
# 第一个汉字
hanzi = re.findall('^[\u4e00-\u9fa5]', a)[0]
#print(hanzi)
# 数字
shuzi = re.findall('\d+', a)[0]
shuzi = int(shuzi)
#print(shuzi)
if hanzi == '低':
return int((shuzi/3)/2)
if hanzi == '中':
return int((shuzi/3)*2)
if hanzi == '高':
return int(shuzi-((shuzi/3)/2))

def city_change(b):
# chengyang
# 市南市北李沧崂山黄岛城阳胶州即墨平度莱西
if b == 'shinan':
return '市南'
if b == 'shibei':
return '市北'
if b == 'licang':
return '李沧'
if b == 'laoshan':
return '崂山'
if b == 'huangdao':
return '黄岛'
if b == 'chengyang':
return '城阳'
if b == 'jiaozhou':
return '胶州'
if b == 'jimo':
return '即墨'
if b == 'pingdu':
return '平度'
if b == 'laixi':
return '莱西'
def count_age(c):
if c == ' ':
return 2021
else:
return (2021-int(c or 0))
def step2(df):
c=1
shi, ting, area, category, age, floor = [], [], [], [], [], []
price,city2 = [], []

for i in range(len(df)):
# 提取 厅室
list1 = df.loc[i,'houseInfo']
#print(list1)
list1 = list1.split(' | ') # ['3室1厅', '89.84平米', '南 北', '精装', '中楼层(共11层)', '2011年建', '板楼']
shi.append(int(re.findall('\d*', list1[0])[0])) # '3室1厅' # ['3', '', '1', '', ''] # 3
ting.append(int(re.findall('\d*', list1[0])[2])) # 1

# 提取 面积
area.append(float(re.findall('\d+.\d+|\d+', list1[1])[0]))

# 精装 毛坯 其他
category.append(list1[3])

# 楼层
f = list1[4] # '中楼层(共11层)'

if len(f) > 5 : # 一般是8 9, 有的只有层数 我看到的
floor.append(floor_count(f))
else: # 3 层
floor.append(int(re.findall('\d+', f)[0])) # 3

# 房龄
age.append(count_age(re.findall('\d*', list1[5])[0])) # 有的没有这个 所以不用+ 匹配0次和无数次


# unitprice 单价
price.append(int(re.findall('\d+', df['unitPrice'][i])[0]))

# city
city2.append(city_change(df['city'][i])) # chengyang

df['shi'] = shi
df['ting'] = ting
df['area'] = area
df['category'] = category
df['age'] = age
df['floor'] = floor
df['per_price'] = price
df['region'] = city2

# 删掉之前的变量 删掉houseinfo tags unitprice;address我也不知道关联啊,删了吧
df.drop(df.columns[[1, 2, 3, 5, 6]], axis = 1, inplace = True)

# 删掉age是2020的行, 删掉floor缺失的行 floor怎么会缺失呢 他应该早给我报错啊..
df = df[-df.age.isin([2021])]
df = df[-df.age.isin([-78])]
#后面发现floor有缺失 我怀疑是类型的事 直接删了吧不做深究
df = df.dropna()
return df


1
2
3
if __name__ == '__main__':
df = step2(df)
df.head()

删除负值

负值这个是后面画图发现的,我现在想想应该是我数据清洗考虑的不全面,这些东西可能做一个describe()就看出来了。

1
2
3
4
# df[df.totalPrice < 0]
df[df.per_price < 0]
# df.drop(index=df[df.age < 0].index, inplace=True)
# df
1
df.head()

title totalPrice shi ting area category age floor per_price region
0 美立方套三精装修满五唯一 户型方正 南北通透 142.0 3 1 89.84 精装 10 7.0 15806 城阳
2 精装小套三,次顶楼,看河看景,全天采光 124.0 3 2 85.65 精装 6 9.0 14478 城阳
3 急售,精装修,好楼层,价格可议,随时可看房。 135.0 3 2 88.87 精装 7 8.0 15191 城阳
4 天泰城美立方套三双卫自住装修保持好 186.0 3 2 123.93 精装 9 6.0 15009 城阳
5 天泰奥园,平层套三双卫,南北通透,全明户型,公摊低 180.0 3 2 128.59 精装 14 1.0 13998 城阳

title 是户主用来描述自家房子的叙述

我们要做的工作是 把他的文字提取出来,保存为txt,然后做词云图进行展示

1
print(type(df['title'].tolist()))
1
2
3
4
title_cloud = df['title'].tolist()
with open("../sub/title_cloud.txt", 'w', encoding = 'utf-8') as f:
for i in title_cloud:
f.write(i+'\n') #\t也行 不行再换
1
#!pip install jieba -i https://pypi.tuna.tsinghua.edu.cn/simple
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
from wordcloud import WordCloud
import codecs
import jieba
#import jieba.analyse as analyse
import imageio
import os
from os import path
import matplotlib.pyplot as plt
from PIL import Image, ImageDraw, ImageFont

def draw_wordcloud():
comment_text = open('../sub/title_cloud.txt', 'rb').read()
cut_text = " ".join(jieba.cut(comment_text))
color_mask = imageio.imread("../pic/dance_girl01.png") # 读取背景图片
cloud = WordCloud(
#设置字体,不指定就会出现乱码
font_path = "../pic/STFANGSO.ttf",
#设置背景色
background_color = 'white',
#词云形状
mask = color_mask,
#允许最大词汇
max_words = 800,
#最大号字体
max_font_size = 40
)
word_cloud = cloud.generate(cut_text) # 产生词云
word_cloud.to_file("../sub/pic1.jpg") #保存图片
# 显示词云图片
plt.imshow(word_cloud)
plt.axis('off')
plt.show()
if __name__ == '__main__':
draw_wordcloud()

热力图 相关分析

1
2
3
4
corrmat = df.corr()
fig, axs = plt.subplots(figsize=(12,9))
sns.heatmap(corrmat, square=True, cmap='coolwarm')
axs.set_title('相关分析 - 热力图', size=15)
Text(0.5, 1.0, '相关分析 - 热力图')

可以看到主要是面积和单价哈

totalprice 单位是万元,也就是房屋的总价

作图,与region的折线图,也就是探索房价和城区的关系,最后是要做回归的话,也就是房价是因变量,其他的是自变量。

1
2
3
sns.distplot(df['totalPrice'],bins = 50)
print("skewness:%f"%df['totalPrice'].skew())
print("kurtosis:%f"%df['totalPrice'].kurtosis())
skewness:4.204764
kurtosis:35.250381

正偏态,有峰值,多处于0-500w之间

箱形图(Box-plot)

又称为盒须图、盒式图或箱线图
是一种用作显示一组数据分散情况资料的统计图,因形状如箱子而得名。它能显示出一组数据的最大值、最小值、中位数、及上下四分位数。
箱形图绘制须使用常用的统计量,能提供有关数据位置和分散情况的关键信息,尤其在比较不同的母体数据时更可表现其差异。
箱形图的绘制主要包含六个数据节点,需要先将数据从大到小进行排列,然后分别计算出它的上边缘,上四分位数,中位数,下四分位数,下边缘,还有一个异常值。

1
2
3
4
5
6
7
f,ax = plt.subplots(figsize=(8,6))
fig = sns.boxplot(x = 'region', y = 'totalPrice', data = df)
plt.xlabel('')
plt.ylabel('总价(万元)')
plt.title('不同城区的二手房总价对比箱线图')
plt.show()
#plt.savefig('../sub/boxplot1.png',width=6,height=4, dpi=900,bbox_inches='tight')

异常值,也不能说是异常值,就是房屋的价格跨度还是太大了,所以接下来我想根据 总价 切分属于正态分布的房价和“异常值”

即 大于箱线图 的上边缘 和 小于 箱线图的下边缘

重新标记数据集 type

我用重新读入数据的方法,并新建一列type,取值为0和1,0是“异常值”,1是正态分布的值;这样就将某个城区的房屋总价分成正常值和异常值(过高过低,主要是过高的房子)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
def backspace(tmp):
# print(tmp.describe())
q3 = tmp.describe().iloc[6,0]
q1 = tmp.describe().iloc[4,0]
up = q3 + 1.5 *(q3 - q1)
down = q1 - 1.5 *(q3 - q1)

tmp2 = tmp.drop(tmp[(tmp['totalPrice'] < down) | (tmp['totalPrice'] > up)].index) # tmp2过大过小的
tmp.drop(tmp[(tmp['totalPrice'] <= up) & (tmp['totalPrice'] >= down)].index, inplace = True) # tmp1正态分布的
return tmp, tmp2
def cuttdown(upath):
list1 = os.listdir(upath)
for filename, i in zip(list1, range(len(list1))): #range(len(list1))
columns_name = ['title', 'address', 'houseInfo', 'followInfo', 'tags', 'totalPrice', 'unitPrice']

tmp = pd.read_csv(upath + filename, names = columns_name) # 读取文件后的临时变量
tmp['city'] = re.findall('[a-z]*',filename)[4] # shinan
tmp.drop('followInfo', axis = 1, inplace = True)

# print(tmp.info())
tmp1, tmp2 = backspace(tmp)
# print(tmp1.info())
# print(tmp2.info())
if i == 0:
df1 = tmp1
df2 = tmp2
else:
df1 = pd.concat([df1,tmp1] ,axis = 0) #ignore_index=True
df2 = pd.concat([df2,tmp2] ,axis = 0)

df1.reset_index(drop = True, inplace = True)
df2.reset_index(drop = True, inplace = True) # df1 是异常值, df2是属于正态分布的
df1['type'] = 0 # 异常值
df2['type'] = 1
return df1, df2
if __name__ == '__main__':
path = 'C:/Users/ASUS/Desktop/学位课/应用回归分析/大作业/data/ershoufang/'
df1, df2 = cuttdown(path)
df1 = step2(df1)
df2 = step2(df2)
df = pd.concat([df1,df2] ,axis = 0) # 19545个样本数 与 第一次读取一样 不同点在于 加了 type 区分 总价的高低水平
1
df.head()

title totalPrice type shi ting area category age floor per_price region
0 香溪庭院 边户 精装修 一楼带老人房 低总价 拎包入住 370.0 0 5 2 198.33 简装 12 4.0 18656 城阳
1 实际287平下叠别墅 带院子 带车位 310.0 0 5 2 178.33 毛坯 7 0.0 17384 城阳
2 高新区 世茂意墅湾 312万住联排 生活配套齐全就这一套 312.0 0 3 2 180.44 毛坯 6 3.0 17292 城阳
3 三层精装别墅出售,独门独院,环境优美,安静不临街 450.0 0 4 3 167.85 精装 8 3.0 26810 城阳
5 世茂玲珑台联排别墅 户型方正 地下室80平 带车位 400.0 0 4 3 201.30 其他 7 3.0 19871 城阳

然后再看箱线图,果然好很多了

1
2
3
4
5
6
7
f,ax = plt.subplots(figsize=(8,6))
fig = sns.boxplot(x = 'region', y = 'totalPrice', data = df2)
plt.xlabel('')
plt.ylabel('总价(万元)')
plt.title('不同城区的二手房总价对比箱线图')
plt.show()
#plt.savefig('../sub/boxplot1.png',width=6,height=4, dpi=900,bbox_inches='tight')

小提琴图 (Violin Plot)

是用来展示多组数据的分布状态以及概率密度。这种图表结合了箱形图和密度图的特征,主要用来显示数据的分布形状。跟箱形图类似,但是在密度层面展示更好。在数据量非常大不方便一个一个展示的时候小提琴图特别适用。

本质上还是结合了上面的两个图。

下为df2,正常值

1
sns.violinplot(data=df2['totalPrice'], color='#e74c3c')
<matplotlib.axes._subplots.AxesSubplot at 0x148a880e160>

我看有的人图是数据大的在左边 降序排列了

shi ting 分别对应的是室厅

1
df.shi.unique()
array([5., 3., 4., 6., 1., 8., 7., 9., 2., 0.])
1
df.ting.unique()
array([2., 3., 4., 1., 7., 5., 0.])
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
fig, axs = plt.subplots(figsize=(20, 8))
plt.subplot(121)
sns.countplot(x='shi', hue='type', data=df)
plt.xlabel('室', size=15, labelpad=20)
plt.ylabel('房屋个数累计', size=15, labelpad=20)
plt.tick_params(axis='x', labelsize=15)
plt.tick_params(axis='y', labelsize=15)
plt.legend(['异常值', '正常值'], loc='upper right', prop={'size': 15})


plt.subplot(122)
sns.countplot(x='ting', hue='type', data=df)
plt.xlabel('厅', size=15, labelpad=20)
plt.ylabel('房屋个数累计', size=15, labelpad=20)
plt.tick_params(axis='x', labelsize=15)
plt.tick_params(axis='y', labelsize=15)
plt.legend(['异常值', '正常值'], loc='upper right', prop={'size': 15})

plt.show()

area 前面有户型,这里是面积

1
2
sns.violinplot(data=df2['area'], color='#e74c3c')
plt.xlabel('面积')
Text(0.5, 0, '面积')

1
2
3
4
5
6
7
f,ax = plt.subplots(figsize=(8,6))
fig = sns.boxplot(x = 'region', y = 'area', data = df2)
plt.xlabel('')
plt.ylabel('平方米')
plt.title('不同城区的二手房面积箱线图')
plt.show()
#plt.savefig('../sub/boxplot1.png',width=6,height=4, dpi=900,bbox_inches='tight')

category 这边主要是有 精装 别墅等

1
df.category.unique()
array(['精装', '毛坯', '其他', '简装'], dtype=object)
1
df_all_decks['即墨']['简装'][0]
370

这个叫堆砌柱状图哈

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
def get_pclass_dist(df):

# 创建一个字典,为每个城区上的每个房子类型计数
city_counts = {'城阳': {}, '黄岛': {}, '胶州': {}, '即墨': {}, '莱西': {}, '崂山': {}, '李沧': {}, '市北': {}, '市南': {}}
citys = df.columns.levels[0]
# print(citys)
for city in citys:
for category in ['其他','毛坯','简装','精装']:
try:
count = df[city][category][0]
city_counts[city][category] = count
except KeyError:
city_counts[city][category] = 0

df_citys = pd.DataFrame(city_counts)
# print(df_citys)
city_percentages = {}

for col in df_citys.columns:
city_percentages[col] = [(count / df_citys[col].sum()) * 100 for count in df_citys[col]]
return city_counts, city_percentages

def display_pclass_dist(percentages):

df_percentages = pd.DataFrame(percentages).transpose()
city_names = ('城阳', '黄岛', '胶州', '即墨', '莱西', '崂山', '李沧', '市北', '市南')
bar_count = np.arange(len(city_names))
bar_width = 0.85

c1 = df_percentages[0]
c2 = df_percentages[1]
c3 = df_percentages[2]
c4 = df_percentages[3]

plt.figure(figsize=(20, 10))
plt.bar(bar_count, c1, color='#aba3d5', edgecolor='white', width=bar_width, label='其他')
plt.bar(bar_count, c2, bottom=c1, color='#b5ffb9', edgecolor='white', width=bar_width, label='毛坯')
plt.bar(bar_count, c3, bottom=c1 + c2, color='#a3acff', edgecolor='white', width=bar_width, label='简装')
plt.bar(bar_count, c4, bottom=c1 + c2 + c3, color='#f9bc86', edgecolor='white', width=bar_width, label='精装')

plt.xlabel('Deck', size=15, labelpad=20)
plt.ylabel('房屋类型的百分比', size=15, labelpad=20)
plt.xticks(bar_count, city_names)
plt.tick_params(axis='x', labelsize=15)
plt.tick_params(axis='y', labelsize=15)

plt.legend(loc='upper left', bbox_to_anchor=(1, 1), prop={'size': 15})
plt.title('城区中的房型分布', size=18, y=1.05)

plt.show()

df_all = df
df_all_decks = df_all.groupby(['region', 'category']).count().drop(columns=['title', 'shi', 'ting', 'area', 'age',
'floor', 'per_price']).rename(columns={'totalPrice': 'Count'}).transpose()

all_deck_count, all_deck_per = get_pclass_dist(df_all_decks)
display_pclass_dist(all_deck_per)

变量分箱

age 房屋的年龄

下面是条形图哈

1
df.age.unique()
array([12,  7,  6,  8, 10,  9, 11, 13,  4, 17,  5,  3, 14, 15, 16, 18, 20,
       19, 25, 21,  1, 22, 23, 24, 27,  2, 26, 28, 32, 33, 41, 34, 51, 31,
       38, 42, 35, 39, 29, 30, 36, 37, 43, 40, 44, 46, 72, 57, 45, 71, 76,
       56, 68, 49, 47, 64], dtype=int64)
1
df['age'] = pd.qcut(df['age'], 9)
1
2
3
4
5
6
7
8
9
10
11
12
fig, axs = plt.subplots(figsize=(22, 9))
sns.countplot(x='age', hue='type', data = df)

plt.xlabel('房龄', size=15, labelpad=20)
plt.ylabel('房屋计数', size=15, labelpad=20)
plt.tick_params(axis='x', labelsize=10)
plt.tick_params(axis='y', labelsize=15)

plt.legend(['异常值', '正常值'], loc='upper right', prop={'size': 15})
plt.title('正常房价在房龄中的分布', size=15, y=1.05)

plt.show()

1
df.age.unique()
[(10.0, 13.0], (5.0, 7.0], (-0.001, 5.0], (7.0, 10.0], (13.0, 17.0], (17.0, 22.0], (22.0, 75.0]]
Categories (7, interval[float64]): [(-0.001, 5.0] < (5.0, 7.0] < (7.0, 10.0] < (10.0, 13.0] < (13.0, 17.0] < (17.0, 22.0] < (22.0, 75.0]]

floor 所在的层数

1
df.floor.unique()
array([ 4.,  0.,  3.,  2., 10.,  1.,  9., 12., 17.,  5.,  7., 19., 14.,
       16.,  6., 20., 11., 15.,  8., 21., 25., 18., 23., 26., 27., 13.,
       32., 24., 35., 22., 30., 36., 29., 37., 39., 49., 28., 45., 44.,
       34., 33., 31.])
1
df['floor'] = pd.qcut(df['floor'], 9,duplicates = 'drop')
1
2
3
4
5
6
7
8
9
10
11
12
fig, axs = plt.subplots(figsize=(22, 9))
sns.countplot(x='floor', hue='type', data = df)

plt.xlabel('楼层', size=15, labelpad=20)
plt.ylabel('房屋计数', size=15, labelpad=20)
plt.tick_params(axis='x', labelsize=10)
plt.tick_params(axis='y', labelsize=15)

plt.legend(['异常值', '正常值'], loc='upper right', prop={'size': 15})
plt.title('正常房价在楼层中的分布', size=15, y=1.05)

plt.show()

per_price 均价

正态分布图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
cont_features = ['totalPrice', 'per_price']
norm = df['type'] == 1

fig, axs = plt.subplots(figsize=(20, 8),nrows = 1, ncols = 2)
plt.subplots_adjust(right=1.5)

for i, feature in enumerate(cont_features):

sns.distplot(df[~norm][feature], label='异常值', hist=True, color='#e74c3c', ax=axs[i])
sns.distplot(df[norm][feature], label='正常值', hist=True, color='#2ecc71', ax=axs[i])

axs[i].set_xlabel('')
axs[i].tick_params(axis='x', labelsize=20)
axs[i].tick_params(axis='y', labelsize=20)

axs[i].legend(loc='upper right', prop={'size': 20})
axs[i].set_title('{}变量的正态分布图'.format(feature), size=20, y=1.05)

# plt.subplot(212)
# sns.distplot(df[cont_features], hist=True)
# axs[0].legend(loc='upper right', prop={'size': 20})


plt.show()

region 青岛的哪个区

1
df.region.unique()
array(['城阳', '黄岛', '胶州', '即墨', '莱西', '崂山', '李沧', '市北', '市南'], dtype=object)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
plt.figure(figsize=(16, 8))
# sns.countplot(x='region', hue='category', data=df1)

plt.subplot(221)
sns.countplot(x='region', hue='category', data=df1)

plt.subplot(222)
sns.countplot(x='region', hue='category', data=df2)

plt.subplot(212)
sns.countplot(x='region', hue='category', data=df)

plt.show()

从总体来看,精装房还是最多的,毛坯房占比最少。

结合前面的分析,个人感觉房价高的城区,例如崂山区和李沧区,精装房的比例非常大,所以这个变量可能会很影响房屋的总价。

高价房屋和正常价格的房屋,都是精装房较多,由于爬到的信息无经纬度或者街道数据,所以无法考虑地段,这应该是非常遗憾的一点。

特征重编码

1
2
3
4
5
6
X_train = StandardScaler().fit_transform(df.drop(columns='type'))
y_train = df['type'].values

print('X_train shape: {}'.format(X_train.shape))
print('y_train shape: {}'.format(y_train.shape))

X_train shape: (18528, 9)
y_train shape: (18528,)
1
2
3
# df1.drop(columns='title', inplace=True)
# df2.drop(columns='title', inplace=True)
df.drop(columns='title', inplace=True)
1
2
3
non_numeric_features = ['category', 'region']

df_feature = pd.get_dummies(df, columns = non_numeric_features )
1
df_feature.info()
<class 'pandas.core.frame.DataFrame'>
Int64Index: 19544 entries, 0 to 21830
Data columns (total 21 columns):
 #   Column       Non-Null Count  Dtype  
---  ------       --------------  -----  
 0   totalPrice   19544 non-null  float64
 1   type         19544 non-null  int64  
 2   shi          19544 non-null  int64  
 3   ting         19544 non-null  int64  
 4   area         19544 non-null  float64
 5   age          19544 non-null  int64  
 6   floor        19544 non-null  float64
 7   per_price    19544 non-null  int64  
 8   category_其他  19544 non-null  uint8  
 9   category_毛坯  19544 non-null  uint8  
 10  category_简装  19544 non-null  uint8  
 11  category_精装  19544 non-null  uint8  
 12  region_即墨    19544 non-null  uint8  
 13  region_城阳    19544 non-null  uint8  
 14  region_崂山    19544 non-null  uint8  
 15  region_市北    19544 non-null  uint8  
 16  region_市南    19544 non-null  uint8  
 17  region_李沧    19544 non-null  uint8  
 18  region_胶州    19544 non-null  uint8  
 19  region_莱西    19544 non-null  uint8  
 20  region_黄岛    19544 non-null  uint8  
dtypes: float64(3), int64(5), uint8(13)
memory usage: 1.6 MB

训练模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
####读取数据
import numpy as np
import pandas as pd
from numpy import sort
import matplotlib.pyplot as plt
from xgboost import XGBClassifier
from xgboost import plot_importance
from sklearn import metrics
from sklearn.metrics import accuracy_score
from sklearn.metrics import classification_report
from sklearn.metrics import precision_recall_curve
from sklearn.feature_selection import SelectFromModel

import warnings
warnings.filterwarnings('ignore')



####确定训练集和测试集


x_train, x_test, y_train, y_test = train_test_split(df_feature.drop(columns='type'), df_feature['type'], test_size = 0.2, random_state = 2021)
###############模型 : 全特征 + xgb
model = XGBClassifier(learning_rate=0.1,
n_estimators=1000, # 树的个数--1000棵树建立xgboost
max_depth=6, # 树的深度
min_child_weight = 1, # 叶子节点最小权重
gamma=0., # 惩罚项中叶子结点个数前的参数
subsample=0.8, # 随机选择80%样本建立决策树
colsample_btree=0.8, # 随机选择80%特征建立决策树
objective='binary:logistic', # 指定损失函数
scale_pos_weight=1, # 解决样本个数不平衡的问题
random_state=27 # 随机数
)

model.fit(x_train,
y_train,
eval_set = [(x_test,y_test)],
eval_metric = "error",
early_stopping_rounds = 10, verbose = 500)
# 画图
fig,ax = plt.subplots(figsize=(15,15))
plot_importance(model,
height=0.5,
ax=ax)
plt.show()
pred = model.predict(x_test)



print('F1-score:%.4f' % metrics.f1_score(y_test,pred))
print('AUC:%.4f' % metrics.roc_auc_score(y_test,pred))
print('ACC:%.4f' % metrics.accuracy_score(y_test,pred))
print('Recall:%.4f' % metrics.recall_score(y_test,pred))
print('Precesion:%.4f' % metrics.precision_score(y_test,pred))
#metrics.confusion_matrix(test_y,pred)
precision,recall,thresholds = precision_recall_curve(y_train,model.predict(x_train))
print(classification_report(y_test,pred,target_names=['up','down']))
[0]    validation_0-error:0.017907
Will train until validation_0-error hasn't improved in 10 rounds.
Stopping. Best iteration:
[50]    validation_0-error:0.001791

F1-score:0.9991
AUC:0.9868
ACC:0.9982
Recall:0.9995
Precesion:0.9987
              precision    recall  f1-score   support

          up       0.99      0.97      0.98       194
        down       1.00      1.00      1.00      3715

    accuracy                           1.00      3909
   macro avg       0.99      0.99      0.99      3909
weighted avg       1.00      1.00      1.00      3909

重编码 注

1
2
df['category']  = LabelEncoder().fit_transform(df['category']) 
df['region'] = LabelEncoder().fit_transform(df['region'])

1
2
map1 = {'精装':1,'简装':2,'毛坯':3,'其他':4}
df['category'] = df['category'].map(map1)

—再来一点租房的—

我直接放图

我又可以了xdm

final finish