简介:本资源是一份面向高校Python课程学习者与毕业设计学生的实战型大作业方案,聚焦上海链家出租房数据的采集、分析与可视化全流程。项目涵盖合规爬虫实现、Pandas数据清洗、Matplotlib/Seaborn图表绘制及KMeans聚类等核心技能,适合作为期末大作业或毕设基础框架,难度适中且经助教审定,评审分达95分以上。压缩包共20个文件,含1个主爬虫脚本(.py)、1个Jupyter分析笔记本(.ipynb)、1份完整Word报告(.docx)、1份答辩PPT(.pptx)、1个原始CSV数据集及13张高质量可视化图表(.png),另有README说明与文本配置文件,总大小20.91MB,结构清晰、开箱即用。目前已有102人学习下载,提供从数据获取到结论呈现的完整闭环,包含热力图、散点图、词云、户型与装修分布图等多维度分析成果,可直接运行复现,显著降低初学者项目落地门槛。
1. 项目缘起与核心价值
最近在辅导学生做数据分析相关的大作业时,发现一个高频选题:基于网络公开数据的房产市场分析。其中,链家作为国内头部房产信息平台,其出租房数据因其结构化程度高、信息维度丰富,成为了许多同学入门爬虫、数据分析与可视化的“练手神器”。尤其是以上海这样的一线城市为例,数据量大、市场动态活跃,分析结果也更具现实意义。这个项目看似简单,但要想做出一个能拿高分的“大作业”,远不止写几行爬虫代码那么简单。它考验的是从数据获取、清洗、分析到最终呈现的完整数据科学流程能力,以及将技术栈(Python, Jupyter, Requests等)融会贯通的实战水平。
很多人拿到这个题目,第一反应就是去网上找一段爬虫代码,运行后导出个Excel,再用Matplotlib画几个柱状图就交差了。结果往往是爬取的数据字段不全、清洗逻辑粗糙、分析维度单一、可视化图表简陋,最终报告缺乏深度,自然难以获得高分。一个优秀的大作业,应该能清晰展示你解决问题的系统性思维:你如何设计稳健的爬虫策略以应对网站反爬?如何从海量字段中挖掘出有价值的分析维度?如何选择恰当的可视化图表来讲述数据背后的故事?以及,如何将这一切整合成一份逻辑严谨、图文并茂的文档报告?
本文将围绕“链家上海出租房”这一具体场景,拆解一个高分大作业应有的全貌。我会分享从零开始构建这个项目的完整思路、关键技术细节、以及那些在教程里不会写的“踩坑”经验。无论你是正在筹备大作业的学生,还是希望通过一个完整项目来巩固Python数据技能的自学者,这篇文章都将提供一份可直接参考复现的“实战地图”。
2. 项目架构设计与技术选型考量
在动手写第一行代码之前,合理的项目架构是成功的基石。一个松散、混乱的代码结构,会为后续的数据处理、分析和报告撰写带来无穷无尽的麻烦。对于这个项目,我推荐采用模块化、管道化的设计思想,将整个流程清晰地划分为几个独立的阶段。
2.1 核心模块划分
一个结构清晰的项目通常包含以下目录和模块:
lianjia_rent_analysis/ ├── spiders/ # 爬虫相关模块 │ ├── __init__.py │ ├── crawler.py # 主爬虫逻辑 │ └── utils.py # 请求头、代理、解析函数等工具 ├── data/ # 数据存储 │ ├── raw/ # 原始爬取数据(JSON/CSV) │ └── processed/ # 清洗后的数据 ├── analysis/ # 数据分析模块 │ ├── __init__.py │ ├── cleaner.py # 数据清洗与预处理 │ └── analyzer.py # 核心分析逻辑 ├── visualization/ # 可视化模块 │ ├── __init__.py │ └── plotter.py # 图表生成函数 ├── notebooks/ # Jupyter Notebook 分析过程 │ └── main_analysis.ipynb ├── report/ # 最终报告与图表输出 │ ├── figures/ # 保存的图片 │ └── final_report.md 或 .pdf ├── config.py # 配置文件(数据库连接、API密钥等) ├── requirements.txt # 项目依赖 └── README.md # 项目说明为什么这样设计?这种结构遵循了数据科学项目的典型生命周期(ETL:抽取、转换、加载)。spiders负责数据抽取(E),analysis/cleaner.py负责数据转换(T),而analysis/analyzer.py和visualization/则负责数据的加载与分析(L)。模块化使得代码复用性高,例如,清洗逻辑独立后,无论是从爬虫还是从本地文件加载数据,都可以调用同一套清洗函数。notebooks/目录的存在至关重要,它允许你以交互式、探索性的方式进行数据分析,并将思考过程(包括失败的尝试)记录下来,这本身就是一份宝贵的文档,能向评审者展示你的分析思路。
2.2 技术栈深度解析
爬虫层:
Requests+BeautifulSoup4/lxml- 选型理由:链家出租房列表页和详情页是静态HTML,结构相对规整。
Requests库简单高效,足以应对基本请求。BeautifulSoup4语法友好,适合初学者快速上手解析。如果追求极致的解析速度,lxml是更优选择,但语法稍复杂。对于这个项目,BeautifulSoup4的易用性优势更大。 - 关键考量:必须处理反爬机制。链家有针对高频请求的封禁策略。这意味着你不能用一个死循环疯狂请求。需要加入:
- 随机延迟:在请求间插入
time.sleep(random.uniform(1, 3)),模拟人类浏览间隔。 - 轮换User-Agent:准备一个列表,每次请求随机选取一个合法的浏览器UA。
- 使用Session:
requests.Session()可以保持cookies,在某些场景下更稳定。 - 谨慎使用IP代理:对于学生作业,通常不推荐涉及付费代理。更可行的方案是控制爬取速度,并准备好断点续爬逻辑(记录已爬取的页面URL),因为IP被封是大概率事件,需要能从断点恢复。
- 随机延迟:在请求间插入
- 选型理由:链家出租房列表页和详情页是静态HTML,结构相对规整。
数据分析层:
Pandas+NumPy- 选型理由:
Pandas是Python数据分析的事实标准。其DataFrame结构非常适合处理表格型数据,链家每条房源信息正好对应一行。内置的聚合、分组、合并、透视表功能,能轻松实现“各区租金对比”、“户型分布”等核心分析。NumPy提供底层数值计算支持。 - 实操心得:爬取的数据字段可能很多,但并非所有都有用。初期应尽可能全地抓取(如标题、区域、板块、小区、面积、户型、朝向、楼层、装修、月租、单价、发布时间、经纪人等),在清洗阶段再决定取舍。
Pandas的apply函数和向量化操作是数据清洗的利器。
- 选型理由:
可视化层:
Matplotlib+Seaborn+Plotly(可选)Matplotlib:基础绘图库,高度定制化,但默认样式较丑。常用于绘制需要精细控制的图表。Seaborn:基于Matplotlib,默认样式美观,且高级接口(如displot,catplot,heatmap)能一键生成复杂的统计图表,非常适合快速探索数据分布与关系(如租金分布直方图、区与户型的租金箱线图)。Plotly:生成交互式图表,可以嵌入网页。如果你的报告是HTML格式,使用Plotly能让读者互动探索。但对于静态PDF报告,Matplotlib/Seaborn更稳定。- 建议:在Jupyter Notebook中探索时多用Seaborn和Plotly;在生成最终报告图表时,用Seaborn设定好主题,或使用Matplotlib进行细节调整后保存为高清矢量图(
.svg或.pdf),保证印刷质量。
环境与文档:
Jupyter Notebook+AnacondaJupyter Notebook:是此项目的“灵魂”。它将代码、可视化结果、Markdown文字叙述完美结合。你的分析过程、试错、中间结论都应该记录在Notebook中。最终,一个干净、连贯的Notebook本身就是一份优秀的数据分析报告初稿。Anaconda:强烈推荐使用Anaconda管理Python环境。它可以一键创建独立的项目环境,避免包版本冲突。通过conda create -n lianjia python=3.8创建环境,再pip install -r requirements.txt安装依赖,能保证项目在任何机器上可复现。
3. 稳健爬虫策略的构建与核心实现
爬虫是这个项目的数据源头,其稳定性和数据质量直接决定了后续所有分析的上限。一个鲁棒的爬虫需要兼顾效率、礼貌性和抗风险能力。
3.1 爬取策略设计:列表页与详情页的配合
链家出租房的数据获取通常需要两级爬取:
- 遍历列表页:获取所有房源的基本信息和详情页链接。例如,上海浦东新区的出租房可能有上百页,需要循环遍历每一页。
- 抓取详情页:访问每个房源的详情页,获取更全面的信息,如户型图、具体描述、配套设施等。
实现要点:
- URL规律分析:观察链家列表页URL,通常形如
https://sh.lianjia.com/zufang/pudong/pg2/,其中pudong是区域,pg2是页码。你需要先获取上海所有区域的编码(如pudong, minhang等),然后为每个区域构造分页URL循环。 - 解析列表页:列表页通常包含房源ID、标题、区域、租金、户型、面积等核心字段,以及最重要的——详情页的相对链接。用BeautifulSoup定位到每个房源卡片所在的HTML元素,逐一提取。
- 解析详情页:详情页信息更丰富,但结构也可能更复杂。需要仔细分析HTML,找到关键信息的标签。有时信息可能藏在JavaScript变量或后续接口请求中,这就需要更高级的技巧(如分析XHR请求),但对于基础作业,抓取静态HTML中的主要信息通常足够。
3.2 反爬应对与健壮性代码
这是爬虫部分最容易失分,也最能体现功力的地方。
import requests import time import random from bs4 import BeautifulSoup import pandas as pd import logging # 配置日志,方便调试和记录错误 logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s: %(message)s') class LianjiaRentSpider: def __init__(self): self.session = requests.Session() self.headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36...', 'Accept-Language': 'zh-CN,zh;q=0.9', } self.session.headers.update(self.headers) self.data_list = [] # 存储爬取的数据 self.visited_urls = set() # 记录已访问URL,避免重复 # 可准备一个User-Agent列表进行轮换 self.user_agents = [...] def get_random_delay(self): """返回一个随机延迟时间,用于请求间隔""" return random.uniform(1.5, 4) # 间隔1.5到4秒,比较安全 def fetch_page(self, url, max_retries=3): """获取页面内容,包含重试机制""" for attempt in range(max_retries): try: # 每次请求前随机延迟 time.sleep(self.get_random_delay()) # 可在此轮换User-Agent # self.session.headers['User-Agent'] = random.choice(self.user_agents) response = self.session.get(url, timeout=10) response.raise_for_status() # 检查HTTP状态码 # 简单检查页面是否被反爬(例如包含验证关键字) if "安全验证" in response.text: logging.warning(f"页面可能触发了反爬:{url}") # 这里可以触发更长的等待或更换策略 time.sleep(30) continue return response.text except requests.exceptions.RequestException as e: logging.error(f"请求失败 ({attempt+1}/{max_retries}): {url}, 错误: {e}") time.sleep(5 * (attempt + 1)) # 退避策略 logging.error(f"URL {url} 重试{max_retries}次后仍失败") return None def parse_list_page(self, html, district): """解析列表页,提取房源基本信息及详情链接""" soup = BeautifulSoup(html, 'html.parser') house_items = soup.find_all('div', class_='content__list--item') # 根据实际HTML结构调整 for item in house_items: try: title_elem = item.find('p', class_='content__list--item--title') title = title_elem.text.strip() if title_elem else 'N/A' # 提取详情页链接 link_elem = item.find('a', class_='content__list--item--aside') detail_path = link_elem['href'] if link_elem else None if detail_path and not detail_path.startswith('http'): detail_url = 'https://sh.lianjia.com' + detail_path else: detail_url = detail_path # 提取其他信息:价格、户型、面积等(需根据实际HTML结构调整选择器) price = item.find('span', class_='content__list--item-price').text.strip() # ... 更多字段解析 # 将详情URL加入待爬队列,或直接开始解析详情页 if detail_url and detail_url not in self.visited_urls: detail_info = self.parse_detail_page(detail_url) if detail_info: # 合并列表页和详情页信息 house_data = { 'district': district, 'title': title, 'price': price, 'detail_url': detail_url, **detail_info # 合并详情页信息 } self.data_list.append(house_data) self.visited_urls.add(detail_url) logging.info(f"成功爬取房源: {title}") except Exception as e: logging.error(f"解析房源条目时出错: {e}", exc_info=True) continue # 跳过当前出错条目,继续下一个 def parse_detail_page(self, url): """解析详情页,提取更丰富的信息""" html = self.fetch_page(url) if not html: return None soup = BeautifulSoup(html, 'html.parser') detail_info = {} try: # 示例:提取户型、面积、朝向、楼层等 # 需要你实际查看链家详情页的HTML结构来定位 info_elements = soup.find_all('li', class_='fl oneline') # 假设的类名 for elem in info_elements: text = elem.text.strip() if '户型' in text: detail_info['layout'] = text.split(':')[-1] elif '面积' in text: detail_info['area'] = text.split(':')[-1] # ... 类似处理其他字段 # 提取小区名称 community_elem = soup.find('a', class_='info') if community_elem: detail_info['community'] = community_elem.text.strip() # 提取地理位置(可能需从JS或特定标签解析) # ... return detail_info except Exception as e: logging.error(f"解析详情页 {url} 时出错: {e}") return None def run(self, start_urls): """主运行方法""" for district, list_url_template in start_urls.items(): page = 1 while True: list_url = list_url_template.format(page=page) logging.info(f"正在爬取列表页: {list_url}") html = self.fetch_page(list_url) if not html: break # 检查是否已到最后一页(例如,解析页面发现没有房源条目) soup = BeautifulSoup(html, 'html.parser') house_items = soup.find_all('div', class_='content__list--item') if not house_items: logging.info(f"区域 {district} 第 {page} 页无数据,可能已到末尾") break self.parse_list_page(html, district) page += 1 # 每爬完一页,可以考虑将数据临时保存,防止程序意外中断数据丢失 if page % 5 == 0: self.save_to_csv(f'backup_data_page_{page}.csv') # 最终保存所有数据 self.save_to_csv('lianjia_rent_shanghai_full.csv') def save_to_csv(self, filename): """将数据保存到CSV文件""" if self.data_list: df = pd.DataFrame(self.data_list) df.to_csv(filename, index=False, encoding='utf-8-sig') logging.info(f"数据已保存至 {filename}, 共 {len(df)} 条记录") # 使用示例 if __name__ == '__main__': # 定义起始URL(需要根据链家实际URL结构填写) districts = { 'pudong': 'https://sh.lianjia.com/zufang/pudong/pg{page}/', 'minhang': 'https://sh.lianjia.com/zufang/minhang/pg{page}/', # ... 添加其他区域 } spider = LianjiaRentSpider() spider.run(districts)关键注意事项:
- 选择器稳定性:链家前端的CSS类名可能会变动。你写的选择器(如
content__list--item)今天有效,明天可能就失效了。因此,爬虫代码需要有一定的容错性,并且要准备在失效时快速调整。 - 数据存储策略:不要等到所有数据爬完才保存。应该每爬取一定数量(比如每50条)或每隔一段时间就将数据追加保存到文件(如CSV)或数据库中。这样即使程序中途崩溃或IP被封,也已保存了部分成果。
- 尊重
robots.txt:在爬取前,检查https://sh.lianjia.com/robots.txt。虽然对于教育项目通常较宽松,但了解网站的爬虫协议是良好的实践。 - 道德与法律边界:明确你的爬虫仅用于个人学习、研究目的,且控制请求频率,避免对目标网站服务器造成负担。切勿将爬取数据用于商业用途或大规模公开传播。
4. 从原始数据到分析就绪:数据清洗实战
爬取到的原始数据通常是“脏”的,包含缺失值、异常值、不一致的格式,无法直接用于分析。数据清洗是数据分析中最耗时但最关键的一步,直接决定了分析结果的可靠性。
4.1 典型数据问题与处理策略
将爬取的数据加载到Pandas DataFrame后,你会面临以下常见问题:
字段提取与拆分:原始“标题”或“户型”字段可能包含冗余信息。例如,标题“【急租】浦东世纪公园 2室1厅 精装修”,我们需要从中提取出关键信息。户型“2室1厅1卫”需要拆分成“室”、“厅”、“卫”三个独立的数值型字段。
import pandas as pd import numpy as np # 假设df是加载的DataFrame # 拆分户型 def split_layout(layout_str): if pd.isna(layout_str): return np.nan, np.nan, np.nan try: # 使用正则表达式提取数字 import re rooms = re.search(r'(\d+)室', layout_str) halls = re.search(r'(\d+)厅', layout_str) baths = re.search(r'(\d+)卫', layout_str) r = int(rooms.group(1)) if rooms else 0 h = int(halls.group(1)) if halls else 0 b = int(baths.group(1)) if baths else 0 return r, h, b except: return np.nan, np.nan, np.nan df[['rooms', 'halls', 'bathrooms']] = df['layout'].apply( lambda x: pd.Series(split_layout(x)) )数值型数据清洗:“面积”字段可能带有单位“㎡”,“租金”字段可能带有“元/月”。需要去除单位并转换为数值类型。同时,要警惕异常值,比如面积10㎡或1000㎡的出租房,租金为0或100万元/月的异常记录。
# 清洗面积和租金 df['area'] = df['area'].str.replace('㎡', '').str.strip().astype(float) df['price'] = df['price'].str.replace('元/月', '').str.replace(',', '').astype(float) # 处理异常值:使用分位数或业务逻辑过滤 # 例如,假设面积在15-200平米,租金在1000-50000元为合理范围 df_clean = df[(df['area'] >= 15) & (df['area'] <= 200) & (df['price'] >= 1000) & (df['price'] <= 50000)].copy() # 计算单价(元/平米/月),这是一个非常重要的衍生指标 df_clean['unit_price'] = df_clean['price'] / df_clean['area']分类数据标准化:“区域”、“板块”等信息可能存在别名或前后空格不一致。例如,“浦东新区”和“浦东”可能混用。“装修”情况可能有“精装修”、“简装修”、“毛坯”等多种表述,需要统一。
# 区域名称标准化 district_mapping = {'浦东': '浦东新区', '浦西': '...'} # 根据实际情况补充 df_clean['district'] = df_clean['district'].replace(district_mapping).str.strip() # 装修情况归类 def categorize_renovation(text): if pd.isna(text): return '其他' if '精装' in text: return '精装修' elif '简装' in text or '普装' in text: return '简装修' elif '毛坯' in text: return '毛坯' else: return '其他' df_clean['renovation_cat'] = df_clean['renovation'].apply(categorize_renovation)处理缺失值:对于关键字段(如面积、租金)的缺失,通常选择删除该行记录。对于非关键字段(如朝向、楼层),可以用众数、中位数填充,或单独设为“未知”类别。
# 删除关键字段缺失的行 df_clean = df_clean.dropna(subset=['area', 'price', 'district']) # 填充楼层信息(如果缺失不多,可以用上下楼层的平均值或众数,这里用‘未知’填充) df_clean['floor'] = df_clean['floor'].fillna('未知')
4.2 数据探索与质量检查
在正式分析前,进行探索性数据分析(EDA)至关重要。这能帮你理解数据分布,发现潜在问题。
import seaborn as sns import matplotlib.pyplot as plt # 1. 查看数据概览 print(df_clean.info()) print(df_clean.describe()) # 2. 检查数值型变量的分布(发现偏态、异常值) fig, axes = plt.subplots(2, 2, figsize=(12, 8)) sns.histplot(df_clean['price'], kde=True, ax=axes[0, 0]) axes[0, 0].set_title('租金分布') sns.histplot(df_clean['area'], kde=True, ax=axes[0, 1]) axes[0, 1].set_title('面积分布') sns.histplot(df_clean['unit_price'], kde=True, ax=axes[1, 0]) axes[1, 0].set_title('单价分布') sns.boxplot(x='district', y='price', data=df_clean, ax=axes[1, 1]) axes[1, 1].set_title('各区租金箱线图') axes[1, 1].tick_params(axis='x', rotation=45) plt.tight_layout() plt.show() # 3. 检查分类变量的分布 print(df_clean['district'].value_counts()) print(df_clean['renovation_cat'].value_counts())通过以上清洗和探索,你得到的是一个干净、规整的数据集,可以放心地进行深入分析了。
5. 多维数据分析与洞察挖掘
有了干净的数据,就可以从不同维度切入,回答一些关于上海租房市场的有趣问题。分析不应只是简单的统计,而应试图揭示数据背后的模式和故事。
5.1 空间维度分析:租金的地理格局
各区租金对比:计算每个行政区的平均租金、租金中位数和租金分布。箱线图非常适合展示各区的租金范围和离散程度。
district_price_stats = df_clean.groupby('district')['price'].agg(['mean', 'median', 'std', 'count']).round(2) district_price_stats = district_price_stats.sort_values('mean', ascending=False) print(district_price_stats) plt.figure(figsize=(14, 6)) sns.boxplot(x='district', y='price', data=df_clean, order=district_price_stats.index) plt.title('上海各行政区出租房租金分布对比') plt.xlabel('行政区') plt.ylabel('月租金 (元)') plt.xticks(rotation=45) plt.tight_layout() plt.savefig('./report/figures/district_price_boxplot.png', dpi=300) plt.show()分析点:静安、黄浦、徐汇等核心城区的租金中位数和上限显著高于外围区域。箱线图还能看出浦东新区虽然平均租金高,但内部差异(箱体长度)可能也很大,说明区域内部分化明显。
租金单价热力图:单纯看总租金可能受面积影响大。计算“每平米月租金”(单价)并按板块聚合,用热力图或分级统计地图展示,能更真实反映地段价值。
# 假设数据中有‘plate’(板块)字段 plate_unit_price = df_clean.groupby(['district', 'plate'])['unit_price'].mean().reset_index() # 这里可以导出plate_unit_price,用专业GIS软件(如QGIS)或在线地图库(如Pyecharts)绘制热力图 # 以下用Seaborn绘制一个简化的透视热图示例(如果板块数量不多) pivot_table = plate_unit_price.pivot(index='district', columns='plate', values='unit_price') plt.figure(figsize=(12, 8)) sns.heatmap(pivot_table, cmap='YlOrRd', annot=True, fmt='.1f', linewidths=.5) plt.title('各行政区-板块租金单价热力图 (元/㎡/月)') plt.tight_layout() plt.savefig('./report/figures/unit_price_heatmap.png', dpi=300) plt.show()
5.2 房屋属性维度分析:什么因素最影响租金?
面积与租金的关系:绘制散点图并添加趋势线,观察是否是线性关系。通常面积越大,总租金越高,但单价可能会下降。
plt.figure(figsize=(10, 6)) sns.scatterplot(x='area', y='price', data=df_clean, alpha=0.5, hue='district', legend=False) sns.regplot(x='area', y='price', data=df_clean, scatter=False, color='red', line_kws={"linewidth":2}) plt.title('房屋面积与月租金关系散点图') plt.xlabel('面积 (㎡)') plt.ylabel('月租金 (元)') plt.tight_layout() plt.savefig('./report/figures/area_vs_price_scatter.png', dpi=300) plt.show() # 分区域看面积与单价的关系 g = sns.lmplot(x='area', y='unit_price', data=df_clean, hue='district', height=6, aspect=1.5, ci=None, scatter_kws={'alpha':0.3}) g.set_axis_labels('面积 (㎡)', '租金单价 (元/㎡/月)') g.fig.suptitle('各行政区面积与租金单价关系', y=1.02) plt.tight_layout() plt.savefig('./report/figures/area_vs_unitprice_by_district.png', dpi=300) plt.show()户型对租金的影响:分析不同卧室数量(如1室、2室、3室)的租金分布。可以使用小提琴图或分组箱线图。
# 过滤出主流户型 df_main_layout = df_clean[df_clean['rooms'].isin([1, 2, 3])] plt.figure(figsize=(10, 6)) sns.boxplot(x='rooms', y='unit_price', data=df_main_layout) plt.title('不同卧室数量的租金单价对比') plt.xlabel('卧室数量 (室)') plt.ylabel('租金单价 (元/㎡/月)') plt.tight_layout() plt.savefig('./report/figures/layout_vs_unitprice_boxplot.png', dpi=300) plt.show()装修情况与楼层分析:精装修的房子是否比简装修有显著的溢价?高楼层、中楼层、低楼层的租金是否有差异?可以使用分组统计和可视化进行验证。
5.3 高级分析思路(加分项)
租金预测模型(简单线性回归):将面积、房间数、区域(转换为虚拟变量)、装修情况等作为特征,租金作为目标变量,建立一个简单的多元线性回归模型。这不仅能预测租金,还能量化各因素对租金的影响程度(系数)。
from sklearn.linear_model import LinearRegression from sklearn.model_selection import train_test_split from sklearn.metrics import mean_absolute_error, r2_score from sklearn.preprocessing import OneHotEncoder from sklearn.compose import ColumnTransformer # 准备特征和目标变量 features = df_clean[['area', 'rooms', 'halls', 'bathrooms', 'district', 'renovation_cat']].copy() target = df_clean['price'] # 对分类变量进行独热编码 categorical_cols = ['district', 'renovation_cat'] numerical_cols = ['area', 'rooms', 'halls', 'bathrooms'] preprocessor = ColumnTransformer( transformers=[ ('num', 'passthrough', numerical_cols), ('cat', OneHotEncoder(drop='first', sparse_output=False), categorical_cols) ]) X = preprocessor.fit_transform(features) y = target.values # 划分训练集和测试集 X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42) # 训练模型 model = LinearRegression() model.fit(X_train, y_train) # 预测与评估 y_pred = model.predict(X_test) mae = mean_absolute_error(y_test, y_pred) r2 = r2_score(y_test, y_pred) print(f'线性回归模型评估:') print(f'平均绝对误差 (MAE): {mae:.2f} 元') print(f'决定系数 (R²): {r2:.4f}') # 可以查看特征重要性(对于线性模型,系数的绝对值大小可近似代表重要性) # 注意:需要将编码后的特征名映射回去,这里略去文本分析(房源标题):对房源标题进行简单的词频分析或情感分析,看看哪些词汇(如“近地铁”、“拎包入住”、“房东直租”)出现频率高,它们是否与更高的租金或更快的出租速度相关?(这需要更复杂的NLP技术,但可以作为拓展方向)。
6. 成果可视化与报告撰写
数据分析的最终目的是为了有效传达信息。一份高分报告,其可视化图表和叙述逻辑必须清晰、专业、有说服力。
6.1 可视化图表选择与美化原则
- 一图一议:每张图都应该有一个明确的结论或想要展示的模式。在图表下方或报告正文中,用一两句话点明从这张图中能看出什么。
- 图表类型匹配:
- 比较:条形图(分类数据)、折线图(时间序列)。
- 分布:直方图、箱线图、小提琴图。
- 关系:散点图、热力图。
- 构成:饼图(慎用,除非类别很少)、堆叠条形图。
- 美化技巧:
- 使用Seaborn主题:
sns.set_theme(style="whitegrid")可以让图表立刻变得美观。 - 颜色:使用色盲友好的调色板,如
sns.color_palette("husl")或sns.color_palette("viridis")。在同一系列图表中保持颜色含义一致。 - 标签与标题:确保坐标轴标签清晰(包括单位),标题简明扼要。字体大小要适合阅读。
- 去除冗余:去掉不必要的背景网格、边框(除非有助于阅读),简化图例。
- 使用Seaborn主题:
# 示例:绘制一个精美的各区平均租金条形图 plt.figure(figsize=(12, 6)) sns.set_theme(style="whitegrid") # 按平均租金排序 order = district_price_stats.sort_values('mean', ascending=False).index ax = sns.barplot(x=district_price_stats.loc[order, 'mean'].index, y=district_price_stats.loc[order, 'mean'].values, palette="viridis") plt.title('上海各行政区出租房平均月租金对比', fontsize=16, fontweight='bold') plt.xlabel('行政区', fontsize=12) plt.ylabel('平均月租金 (元)', fontsize=12) plt.xticks(rotation=45, ha='right') # 在柱子上方添加数值标签 for i, v in enumerate(district_price_stats.loc[order, 'mean'].values): ax.text(i, v + 50, f'{v:.0f}', ha='center', va='bottom', fontsize=10) plt.tight_layout() plt.savefig('./report/figures/avg_rent_by_district_bar.png', dpi=300, bbox_inches='tight') plt.show()6.2 大作业报告结构与内容组织
一份完整的报告文档(如Markdown或PDF)应包含以下部分:
- 封面与摘要:项目标题、姓名、学号、日期。摘要部分用200-300字概括项目目标、方法、主要发现和结论。
- 引言/背景:简述上海租房市场的背景,说明本项目的目的和意义。
- 数据与方法:
- 数据来源:说明数据来自链家网,并注明爬取时间、样本量(如“共爬取2023年X月上海出租房源信息约X万条”)。
- 技术栈:列出使用的关键库(Requests, Pandas, Seaborn等)及其版本。
- 方法概述:简要描述爬虫策略、数据清洗流程、分析方法和可视化工具。
- 数据分析与结果:这是报告的核心。按照逻辑顺序呈现你的分析,例如:
- 4.1 数据概览与清洗结果
- 4.2 上海租房市场整体情况(租金、面积分布)
- 4.3 租金的空间分布特征(各区、各板块对比)
- 4.4 房屋属性对租金的影响分析(面积、户型、装修等)
- 4.5 (可选)简单租金预测模型构建与评估
- 每一小节都应有文字描述、关键统计表格和对应的可视化图表。文字要解读图表,指出关键发现,而不是简单重复图表内容。
- 结论与建议:总结核心发现,例如“上海租房市场租金呈现明显的中心-外围梯度格局”、“面积和区位是影响租金的最主要因素”、“精装修相比简装修有约X%的溢价”等。可以基于分析,给租房者或投资者提出一两条简要的建议。
- 附录:
- 完整的、可运行的Jupyter Notebook链接或关键代码片段。
- 数据清洗和处理的主要步骤代码。
- 遇到的主要问题及解决方案(如反爬处理、异常数据清洗)。
- 参考文献:列出参考的网站、技术文档、相关论文等。
最后,将代码、数据、Notebook、图表和报告文档打包,确保在另一台电脑上能完整复现你的分析过程。可复现性是衡量一个数据科学项目质量的金标准。一个结构清晰、分析深入、可视化专业、文档完备的项目,无疑是一份能获得高分的优秀大作业。
本文还有配套的精品资源,点击获取