一、项目技术框架与环境准备
本次实践的核心目标是完成 "数据获取 - 数据预处理 - 聚类分析 - 可视化展示" 的全流程闭环,技术选型围绕 Python 生态的成熟工具展开,兼顾开发效率和实战效果。
1.1 核心技术工具
- 数据爬取:Requests(网络请求)+ BeautifulSoup4(页面解析),轻量高效,适合静态页面数据抓取,满足链家二手房列表页的解析需求;
- 数据预处理:Pandas(数据清洗、结构化)+ Numpy(数值计算),处理缺失值、异常值,将爬取的非结构化数据转换为结构化 DataFrame;
- 聚类分析:Scikit-learn(机器学习库),使用 K-Means 算法实现房源的无监督聚类,挖掘数据内在规律;
- 数据可视化:Matplotlib(基础可视化)+ Pyecharts(交互式可视化),分别实现静态统计图表和交互式地理、维度分析图表。
1.2 开发环境配置
二、链家二手房数据爬取实现
本次爬取目标为链家北京二手房列表页数据(可根据需求替换城市域名),爬取字段包括房源标题、户型、建筑面积、朝向、装修情况、挂牌价格、单价、所属区域,共 8 个核心字段,爬取后存储为 CSV 文件,便于后续处理。
2.1 爬取核心思路
- 分析链家二手房页面结构:列表页为静态 HTML,房源信息存储在指定 class 的标签中,分页通过 URL 参数
<font style="color:rgb(0, 0, 0);background-color:rgba(0, 0, 0, 0);">pg</font>控制; - 构造请求头:添加 User-Agent、Cookie(可选),模拟浏览器访问,避免被反爬;
- 循环请求分页数据:解析每个页面的房源信息,提取目标字段,处理字段格式(如去除单位、转换数值类型);
- 数据持久化:将解析后的结构化数据保存为 CSV 文件,方便后续数据预处理。
2.2 完整爬取代码实现
python
运行
plain
import requests
from bs4 import BeautifulSoup
import pandas as pd
import time
import random
# 配置请求头,模拟浏览器访问
HEADERS = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
"Accept-Language": "zh-CN,zh;q=0.9",
"Connection": "keep-alive"
}
# 爬取单页数据
def crawl_single_page(page_num, city="bj"):
"""
爬取链家指定城市二手房单页数据
:param page_num: 页码
:param city: 城市域名,bj=北京、sh=上海、gz=广州,默认北京
:return: 单页房源数据列表
"""
base_url = f"https://{city}.lianjia.com/ershoufang/pg{page_num}/"
try:
# 发送GET请求,添加随机延迟避免反爬
time.sleep(random.uniform(1, 3))
response = requests.get(base_url, headers=HEADERS, timeout=10)
response.raise_for_status() # 抛出HTTP请求异常
soup = BeautifulSoup(response.text, "html.parser")
# 定位房源列表标签
house_items = soup.find_all("div", class_="info clear")
page_data = []
for item in house_items:
house_dict = {}
# 提取房源标题
title = item.find("a", class_="noresultRecommend img LOGCLICKDATA").get_text(strip=True)
# 提取户型、面积、朝向、装修等基础信息
house_info = item.find("div", class_="address").get_text(strip=True).split("|")
# 提取挂牌价格和单价
price_total = item.find("div", class_="priceInfo").find("div", class_="totalPrice").get_text(strip=True)
price_unit = item.find("div", class_="priceInfo").find("div", class_="unitPrice").get_text(strip=True)
# 提取所属区域
area = item.find("div", class_="flood").find("div", class_="positionInfo").get_text(strip=True).split("-")[0]
# 字段清洗与赋值,处理数据缺失情况
house_dict["标题"] = title if title else None
house_dict["户型"] = house_info[0].strip() if len(house_info)>=1 else None
house_dict["建筑面积"] = house_info[1].strip() if len(house_info)>=2 else None
house_dict["朝向"] = house_info[2].strip() if len(house_info)>=3 else None
house_dict["装修情况"] = house_info[3].strip() if len(house_info)>=4 else None
house_dict["挂牌价格(万)"] = price_total.replace("万", "").strip() if price_total else None
house_dict["单价(元/平)"] = price_unit.replace("单价", "").replace("元/平", "").strip() if price_unit else None
house_dict["所属区域"] = area if area else None
page_data.append(house_dict)
print(f"第{page_num}页爬取完成,共{len(page_data)}条房源数据")
return page_data
except Exception as e:
print(f"第{page_num}页爬取出错:{str(e)}")
return []
# 批量爬取多页数据
def crawl_lianjia(total_pages, city="bj"):
"""
批量爬取链家多页二手房数据
:param total_pages: 总爬取页码
:param city: 城市域名
:return: 所有房源数据的DataFrame
"""
all_data = []
for page in range(1, total_pages+1):
page_data = crawl_single_page(page, city)
all_data.extend(page_data)
# 转换为DataFrame
df = pd.DataFrame(all_data)
# 保存为CSV文件,去除索引
df.to_csv("链家二手房数据.csv", index=False, encoding="utf-8-sig")
print(f"全部数据爬取完成,共{len(df)}条房源,已保存为链家二手房数据.csv")
return df
# 主函数执行,爬取北京前5页数据(可根据需求调整)
if __name__ == "__main__":
df = crawl_lianjia(total_pages=5, city="bj")
# 打印前5条数据预览
print(df.head())
2.3 爬取注意事项
- 反爬机制规避:添加随机请求延迟(1-3 秒)、使用真实浏览器 User-Agent,避免短时间内高频请求;爬取量较大时可使用代理 IP 池;
- 数据完整性:部分房源可能存在字段缺失,代码中已做缺失值处理,后续数据预处理阶段需进一步清洗;
- 平台规则遵守:本次爬取仅用于技术研究,请勿将数据用于商业用途,爬取频率需控制在合理范围,避免给平台服务器造成压力。
三、数据预处理:为聚类分析做准备
爬取的原始数据存在格式不统一、缺失值、异常值 等问题,无法直接用于聚类分析。聚类分析作为数值型机器学习算法,要求输入数据为标准化的数值类型,因此需通过 Pandas 完成数据清洗和特征工程,核心步骤包括缺失值处理、字段格式转换、异常值剔除、特征标准化。
3.1 数据预处理核心思路
- 读取 CSV 原始数据,查看数据基本信息(字段类型、缺失值占比、数据范围);
- 缺失值处理:采用 "删除法" 剔除缺失值较多的行(缺失值占比 < 5% 时),保证数据完整性;
- 格式转换:将 "建筑面积(平)""挂牌价格(万)""单价(元 / 平)" 转换为浮点型数值,去除非数值字符;
- 异常值剔除:通过四分位数法(IQR)剔除单价、面积的极端值(如单价过高的豪宅、面积过小的公寓),避免影响聚类效果;
- 特征选择:聚类分析选取建筑面积、挂牌价格、单价三个核心数值特征,作为模型输入;
- 特征标准化:使用 Z-Score 标准化将特征值转换为均值为 0、方差为 1 的标准分布,消除量纲影响(K-Means 算法对量纲敏感)。
3.2 数据预处理代码实现
python
运行
plain
import pandas as pd
import numpy as np
from sklearn.preprocessing import StandardScaler
# 数据预处理主函数
def preprocess_data(file_path):
"""
链家二手房数据预处理,为聚类分析做准备
:param file_path: 原始数据CSV文件路径
:return: 标准化后的特征矩阵、原始清洗后数据、特征标准化器
"""
# 1. 读取原始数据
df = pd.read_csv(file_path, encoding="utf-8-sig")
print("原始数据基本信息:")
print(df.info())
print("原始数据描述性统计:")
print(df[["挂牌价格(万)", "单价(元/平)", "建筑面积"]].describe())
# 2. 字段格式转换:转换为浮点型,处理转换失败的行
df["挂牌价格(万)"] = pd.to_numeric(df["挂牌价格(万)"], errors="coerce")
df["单价(元/平)"] = pd.to_numeric(df["单价(元/平)"], errors="coerce")
df["建筑面积"] = pd.to_numeric(df["建筑面积"].str.replace("平", ""), errors="coerce")
# 3. 缺失值处理:删除任意特征缺失的行
df = df.dropna(subset=["挂牌价格(万)", "单价(元/平)", "建筑面积"])
print(f"缺失值处理后,剩余数据量:{len(df)}")
# 4. 异常值剔除:四分位数法(IQR)处理单价和建筑面积
def remove_outlier(data, col):
q1 = data[col].quantile(0.25)
q3 = data[col].quantile(0.75)
iqr = q3 - q1
lower = q1 - 1.5 * iqr
upper = q3 + 1.5 * iqr
return data[(data[col] >= lower) & (data[col] <= upper)]
df = remove_outlier(df, "单价(元/平)")
df = remove_outlier(df, "建筑面积")
print(f"异常值剔除后,剩余数据量:{len(df)}")
# 5. 特征选择:选取3个核心数值特征
features = df[["建筑面积", "挂牌价格(万)", "单价(元/平)"]]
# 6. 特征标准化:Z-Score标准化
scaler = StandardScaler()
features_scaled = scaler.fit_transform(features)
print("数据预处理完成,标准化后特征矩阵形状:", features_scaled.shape)
return features_scaled, df, scaler
# 执行预处理
if __name__ == "__main__":
features_scaled, df_clean, scaler = preprocess_data("链家二手房数据.csv")
# 保存清洗后的数据
df_clean.to_csv("链家二手房清洗后数据.csv", index=False, encoding="utf-8-sig")
预处理完成后,将得到标准化的特征矩阵 (用于聚类模型训练)和清洗后的结构化数据(用于后续可视化),同时保存标准化器,便于后续对新数据进行相同的标准化处理。
四、基于 K-Means 的二手房数据聚类分析
聚类分析是无监督学习的核心应用之一,旨在将相似的数据点归为同一簇,将差异较大的数据点归为不同簇。本次实践选用 K-Means 算法,该算法简单高效、适合大规模数值型数据,能够快速将二手房房源按建筑面积、挂牌价格、单价三个维度进行聚类,挖掘房源的内在分类规律(如刚需小户型、改善型大户型、高端豪宅等)。
4.1 K-Means 聚类核心思路
- 确定最优聚类数 K:K-Means 算法需要预先指定聚类数,本次通过肘部法则(Elbow Method)计算不同 K 值对应的平方和误差(SSE),选取 SSE 下降趋势骤缓的点作为最优 K;
- 训练 K-Means 模型:使用最优 K 值对标准化后的特征矩阵进行模型训练,得到每个房源的聚类标签;
- 聚类结果融合:将聚类标签添加到清洗后的原始数据中,便于后续分析各簇的特征;
- 聚类结果分析:计算各簇的特征均值(建筑面积、价格、单价),总结每类房源的核心特征。
4.2 聚类分析代码实现
python
运行
plain
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from sklearn.cluster import KMeans
from sklearn.preprocessing import StandardScaler
import warnings
warnings.filterwarnings("ignore")
# 加载预处理后的数据
features_scaled, df_clean, scaler = preprocess_data("链家二手房数据.csv")
# 1. 肘部法则确定最优聚类数K
def select_optimal_k(features, max_k=10):
"""
肘部法则选取最优K值
:param features: 标准化后的特征矩阵
:param max_k: 最大尝试K值
:return: 最优K值
"""
sse = []
for k in range(1, max_k+1):
kmeans = KMeans(n_clusters=k, random_state=42, n_init=10)
kmeans.fit(features)
sse.append(kmeans.inertia_) # 平方和误差(SSE)
# 绘制肘部法则图
plt.figure(figsize=(10, 6))
plt.plot(range(1, max_k+1), sse, marker="o", linestyle="-", color="#FF5722")
plt.xlabel("聚类数K", fontsize=12)
plt.ylabel("平方和误差(SSE)", fontsize=12)
plt.title("肘部法则确定最优K值", fontsize=14, fontweight="bold")
plt.grid(alpha=0.3)
plt.savefig("肘部法则图.png", dpi=300, bbox_inches="tight")
plt.show()
return sse
# 计算不同K值的SSE
sse = select_optimal_k(features_scaled, max_k=8)
# 2. 训练K-Means模型(根据肘部法则,本次选取K=3)
k = 3
kmeans = KMeans(n_clusters=k, random_state=42, n_init=10)
cluster_labels = kmeans.fit_predict(features_scaled)
# 3. 融合聚类标签到清洗后的数据中
df_clean["聚类标签"] = cluster_labels
# 4. 分析各簇的特征均值
cluster_analysis = df_clean.groupby("聚类标签")[["建筑面积", "挂牌价格(万)", "单价(元/平)"]].mean().round(2)
print("各聚类簇核心特征均值:")
print(cluster_analysis)
# 保存带聚类标签的数据
df_clean.to_csv("链家二手房聚类后数据.csv", index=False, encoding="utf-8-sig")
print("聚类分析完成,带标签数据已保存为链家二手房聚类后数据.csv")
4.3 聚类结果解读
以本次爬取的北京前 5 页二手房数据为例,肘部法则显示 K=3 时 SSE 下降趋势骤缓,聚类结果将房源分为 3 类,典型特征解读如下(实际数据会因爬取范围不同略有差异):
- 簇 0(刚需型):建筑面积约 60-80㎡,挂牌价格约 300-400 万,单价约 5-6 万 / 平,主要为一居、两居小户型,分布在通州、昌平、房山等远郊区域,适合刚需购房者;
- 簇 1(改善型):建筑面积约 100-120㎡,挂牌价格约 600-800 万,单价约 6-7 万 / 平,主要为三居、四居改善型户型,分布在朝阳、海淀、丰台等近郊区域,兼顾居住品质和交通便利性;
- 簇 2(高端型):建筑面积约 150㎡以上,挂牌价格约 1500 万以上,单价约 8-10 万 / 平,主要为大平层、别墅等高端房源,分布在西城、东城、海淀核心学区或朝阳 CBD 区域,面向高端改善和投资客群。
五、二手房数据可视化展示
数据可视化是将分析结果直观呈现的核心环节,本次实践采用Matplotlib 实现静态统计可视化,Pyecharts 实现交互式可视化,分别从维度统计、聚类分布、地理分布三个角度展示数据规律,让分析结果更易理解。
5.1 静态可视化:Matplotlib 实现维度统计与聚类分布
通过 Matplotlib 绘制各区域房源数量分布、各簇价格箱线图、建筑面积与价格散点图,直观展示房源的区域分布和聚类特征,代码实现如下:
python
运行
plain
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
plt.rcParams["font.sans-serif"] = ["SimHei"] # 解决中文显示问题
plt.rcParams["axes.unicode_minus"] = False # 解决负号显示问题
# 加载聚类后的数据
df = pd.read_csv("链家二手房聚类后数据.csv", encoding="utf-8-sig")
# 1. 各所属区域房源数量TOP10柱状图
plt.figure(figsize=(12, 6))
area_count = df["所属区域"].value_counts().head(10)
sns.barplot(x=area_count.index, y=area_count.values, palette="viridis")
plt.xlabel("所属区域", fontsize=12)
plt.ylabel("房源数量", fontsize=12)
plt.title("链家二手房各区域房源数量TOP10", fontsize=14, fontweight="bold")
plt.xticks(rotation=45)
plt.tight_layout()
plt.savefig("各区域房源数量TOP10.png", dpi=300)
# 2. 各聚类簇挂牌价格箱线图
plt.figure(figsize=(10, 6))
sns.boxplot(x="聚类标签", y="挂牌价格(万)", data=df, palette="Set2")
plt.xlabel("聚类标签", fontsize=12)
plt.ylabel("挂牌价格(万)", fontsize=12)
plt.title("各聚类簇二手房挂牌价格分布", fontsize=14, fontweight="bold")
plt.savefig("各簇价格箱线图.png", dpi=300)
# 3. 建筑面积与挂牌价格散点图(按聚类标签着色)
plt.figure(figsize=(10, 6))
sns.scatterplot(x="建筑面积", y="挂牌价格(万)", hue="聚类标签", data=df, palette="tab10", s=50)
plt.xlabel("建筑面积(平)", fontsize=12)
plt.ylabel("挂牌价格(万)", fontsize=12)
plt.title("建筑面积与挂牌价格散点图(按聚类着色)", fontsize=14, fontweight="bold")
plt.legend(title="聚类标签")
plt.grid(alpha=0.3)
plt.savefig("建筑面积-价格散点图.png", dpi=300)
plt.show()
5.2 交互式可视化:Pyecharts 实现地理与维度分析
Pyecharts 支持生成交互式 HTML 图表,本次实现北京各区域房源数量地图、各聚类簇特征雷达图、单价与面积热力图,支持鼠标悬停查看详细数据,代码实现如下:
python
运行
plain
import pandas as pd
from pyecharts import options as opts
from pyecharts.charts import Bar, Scatter, Radar, HeatMap, Map
from pyecharts.globals import ThemeType
import numpy as np
# 加载聚类后的数据
df = pd.read_csv("链家二手房聚类后数据.csv", encoding="utf-8-sig")
# 简化区域名称(适配Pyecharts地图)
df["区域简化"] = df["所属区域"].str.replace("区", "").str.replace("市", "").str.replace("县", "")
# 1. 北京各区域房源数量交互式地图
area_count = df["区域简化"].value_counts()
map_chart = (
Map(init_opts=opts.InitOpts(theme=ThemeType.LIGHT, width="1200px", height="600px"))
.add("房源数量", [list(z) for z in zip(area_count.index, area_count.values)], "北京")
.set_global_opts(
title_opts=opts.TitleOpts(title="北京各区域二手房房源数量分布", font_size=16),
visualmap_opts=opts.VisualMapOpts(max_=area_count.max(), is_piecewise=True),
)
)
map_chart.render("北京二手房区域分布地图.html")
# 2. 各聚类簇核心特征雷达图
cluster_mean = df.groupby("聚类标签")[["建筑面积", "挂牌价格(万)", "单价(元/平)"]].mean().round(2)
# 雷达图指标标准化(0-100)
def normalize(x):
return (x - x.min()) / (x.max() - x.min()) * 100
cluster_norm = cluster_mean.apply(normalize, axis=0)
# 构造雷达图数据
radar_data = []
for i in range(3):
radar_data.append(cluster_norm.iloc[i].tolist())
schema = [
opts.RadarIndicatorItem(name="建筑面积", max_=100),
opts.RadarIndicatorItem(name="挂牌价格", max_=100),
opts.RadarIndicatorItem(name="单价", max_=100),
]
radar_chart = (
Radar(init_opts=opts.InitOpts(theme=ThemeType.LIGHT, width="800px", height="600px"))
.add_schema(schema=schema, shape="polygon")
.add("簇0(刚需型)", [radar_data[0]], color="#FF5722")
.add("簇1(改善型)", [radar_data[1]], color="#1E90FF")
.add("簇2(高端型)", [radar_data[2]], color="#32CD32")
.set_global_opts(title_opts=opts.TitleOpts(title="各聚类簇核心特征雷达图", font_size=16))
)
radar_chart.render("各簇特征雷达图.html")
# 3. 单价与建筑面积热力图
# 数据分箱
df["面积区间"] = pd.cut(df["建筑面积"], bins=[0, 60, 90, 120, 150, 200, 300], labels=["0-60", "60-90", "90-120", "120-150", "150-200", "200+"])
df["单价区间"] = pd.cut(df["单价(元/平)"], bins=[0, 4, 6, 8, 10, 15], labels=["0-4万", "4-6万", "6-8万", "8-10万", "10万+"])
heat_data = df.groupby(["面积区间", "单价区间"]).size().reset_index(name="数量")
# 构造热力图数据格式
heat_map_data = []
for _, row in heat_data.iterrows():
heat_map_data.append([row["面积区间"], row["单价区间"], row["数量"]])
heat_chart = (
HeatMap(init_opts=opts.InitOpts(theme=ThemeType.LIGHT, width="1000px", height="600px"))
.add_xaxis([x for x in df["面积区间"].cat.categories])
.add_yaxis("房源数量", [y for y in df["单价区间"].cat.categories], heat_map_data)
.set_global_opts(
title_opts=opts.TitleOpts(title="二手房建筑面积与单价热力图", font_size=16),
visualmap_opts=opts.VisualMapOpts(min_=0, max_=heat_data["数量"].max()),
xaxis_opts=opts.AxisOpts(name="建筑面积区间(平)"),
yaxis_opts=opts.AxisOpts(name="单价区间(元/平)"),
)
)
heat_chart.render("面积-单价热力图.html")
print("交互式可视化完成,已生成3个HTML图表文件")
5.3 可视化结果价值
- 区域分布:可快速识别北京二手房房源的核心分布区域(如朝阳、海淀、丰台),反映区域房产市场的活跃度;
- 聚类特征:雷达图直观展示三类房源的特征差异,刚需型房源面积小、价格低,高端型房源面积大、单价高,改善型房源处于中间区间;
- 维度关联:热力图清晰呈现 "面积越大,单价越高" 的关联规律,为购房决策提供数据支撑(如刚需购房者可重点关注 60-90㎡、4-6 万 / 平的房源)。
六、项目总结与拓展方向
本次实践完成了链家二手房数据从爬取、预处理到聚类分析、可视化展示的全流程实现,通过 Python 生态工具构建了一套完整的房产数据分析方案,最终成功将北京二手房房源分为刚需型、改善型、高端型三类,并通过多维度可视化呈现了数据规律。本次实践的核心价值在于为房产行业研究、购房决策、数据分析学习提供了可落地的技术模板,代码具有良好的可扩展性,可根据需求快速适配其他城市、其他房产平台的数据分析。
6.1 项目核心亮点
- 全流程闭环:覆盖数据获取、清洗、分析、可视化全环节,每个步骤均有可运行的代码实现,新手可快速上手;
- 实用性强:爬取字段、分析维度均为房产研究的核心指标,聚类结果和可视化图表具有实际参考价值;
- 技术通用性:使用的爬虫、数据处理、机器学习、可视化技术均为 Python 数据分析的通用技术,可迁移到电商、金融、教育等其他领域的数据分析项目。
6.2 拓展方向
- 爬取优化:引入 Selenium 处理动态加载页面,爬取更多字段(如房龄、楼层、配套设施、成交周期);添加代理 IP 池和请求重试机制,提升爬取效率和稳定性,重点推荐亿牛云隧道转发代理IP;
- 特征工程:加入更多特征(如房龄、学区、地铁距离),通过特征筛选(如方差分析、互信息)提升聚类效果;引入地理特征(经纬度),实现空间聚类分析;
- 算法优化:尝试使用层次聚类、DBSCAN 等其他聚类算法,对比不同算法的聚类效果;引入有监督学习,构建房价预测模型(如线性回归、XGBoost);
- 可视化升级:结合 Tableau、Power BI 实现更专业的可视化仪表盘;将分析结果部署为 Web 应用(如 Flask/Django),支持实时数据爬取和分析。