简介:面向计算机专业毕业设计、期末大作业以及数据分析实战训练的一套Python二手房数据分析项目资料,内容覆盖源代码、详细解析文档和答辩讲解PPT。整套资料围绕二手房市场数据展开,完整演示数据读取、清洗、转换、挖掘与可视化的流程,并应用NumPy、Pandas、Matplotlib等常用数据分析库,适合已有Python基础并希望提升项目经验的学习者参考使用。压缩包共一百一十四个文件,其中包括十八个Python脚本文件作为核心功能实现,十七个CSV数据文件存放原始与清洗后的数据,十五个HTML网页文件便于展示分析结果,同时配备讲解PPT、PNG图片等辅助材料,整体大小约二十九点三一兆,目录结构清晰。项目曾获九十八分评价,并得到导师指导与认可,解析文档针对每个处理环节作出详细说明,可帮助理解算法思路、排查常见问题;所有源码已在本地编译调试并确认可运行。目前已有四十二人浏览学习,适合用作课程设计或毕业设计的完整范本。
1. 一份二手房分析项目,先绕开两个最常见的坑
拿到这份项目资料时,我先注意到的是那一串 CSV 文件名:既有ershoufang-origin-utf8.csv,又有ershoufang-origin-ansi.csv,后面还跟着_old和clean-v1.1两个版本。这种命名方式很真实,说明数据集的清洗过程是分阶段完成的,而不是一次成型。对于做 Python 数据分析的人来说,这正是业务场景的常态——原始数据永远是脏的、乱的、编码不统一的,真正的分析工作有一半以上耗在整理上,而不是跑模型。
这个项目的价值不在于算法多复杂,而在于它把一条完整的数据分析链路走通了:从原始 CSV 读取、编码识别、数据清洗、字段类型转换,到 Pandas 聚合和 Matplotlib 可视化,最后形成可以直接答辩的 PPT 素材。以分析室内面积、朝向和总价的关系为例,这个项目既覆盖期末大作业要求的完整流程,又提供了可复现的代码思路。如果你手头也有一批不规整的数据表,这篇内容能帮你直接替换字段名复用整套逻辑。我在拆解时把重心放在两个最影响结果质量的环节——编码处理和重复值清洗,这两处做好了,后续的统计图和分析结论才站得住脚。
2. 读取原始 CSV:编码识别与 Pandas 的 encoding 参数
2.1 UTF-8 与 ANSI 混存,文件读取的第一个分岔口
数据文件里有ershoufang-origin-utf8.csv和ershoufang-origin-ansi.csv两份,内容相同但编码不同。这种情况在实际项目中非常常见:有人用 Windows 上的 Excel 保存数据,默认落盘就是 ANSI(在中文 Windows 下通常对应 GBK);有人从网页端导出,拿到的是 UTF-8。如果 Pandas 的read_csv不加 encoding 参数直接读,会遇到UnicodeDecodeError,代码在执行到读取这一步就会中断,后面的清洗和分析全部没法跑。
处理这类问题之前,先搞清楚一个判断路径。GBK 是双字节编码,UTF-8 是变长编码,二者的字节序列有区别。推荐先用二进制方式打开文件,观察前几个字节是否带 BOM(\xef\xbb\xbf),或者让 Python 用chardet库自动检测,这是快速确定编码的常见做法。在这个项目里,文件命名已经帮我们标注了编码,所以读取逻辑可以很清晰:
import pandas as pd def load_house_data(path: str, encoding: str = "utf-8") -> pd.DataFrame: """ 读取二手房原始 CSV,返回 DataFrame。 路径里带 ansi 的文件用 gbk 编码,带 utf8 的用 utf-8。 """ if "ansi" in path.lower(): encoding = "gbk" df = pd.read_csv(path, encoding=encoding) print(f"loaded {path}, shape={df.shape}") return df # 读取两份不同编码的原始数据 df_utf8 = load_house_data("ershoufang-origin-utf8.csv") df_ansi = load_house_data("ershoufang-origin-ansi.csv")代码里的判断逻辑很简单:文件名中出现ansi字符串时,把编码切换成gbk,否则使用默认的utf-8。shape打印出来可以第一时间确认数据的行数和列数是否与预期一致。这里有一个值得注意的细节:read_csv的encoding参数只影响文本解码方式,不影响后续的数值计算,但编码搞错,所有字段都可能变成乱码,类似于把人名、小区名和朝向全部打上错误标签,分析出来的区域均价自然没有参考价值。
2.2 读取之后先看 dtypes 和缺失值分布
编码问题解决之后,不要急着做清洗。先运行下面几行命令,搞清楚数据表的长相:
# 查看各列数据类型和缺失情况 print(df_utf8.dtypes) print(df_utf8.isnull().sum()) # 只显示前 5 行,检查列名是否正常 print(df_utf8.head())这个步骤的意义在于确认列名的规范性。有些 CSV 文件的首行会带空字符或者隐藏的\ufeffBOM,导致第一列列名变成\ufeff标题,后续按列名取数时会报KeyError。如果发现列名异常,可以用df.columns = [col.strip().replace('\ufeff', '') for col in df.columns]批量修正。另外,isnull().sum()会给出每一列缺失值的数量,这个数字决定了清洗策略——缺失占比低于 5% 的列可以直接删行,高于 30% 的列要考虑字段本身是否有保留价值。
这个环节通常还会暴露一个隐蔽问题:数值列被读成了 object 类型。原因是原始数据里混入了空字符串或者单位符号,比如面积列里出现"78.5平"这种带单位的写法,Pandas 就会整列降级为字符串。dtypes 的输出能让你一眼发现这种异常。
| 列类型 | 常见误读原因 | 处理方式 |
|---|---|---|
| object | 混入单位、空格、逗号 | 用pd.to_numeric(..., errors='coerce')强制转换 |
| float64 | 正常数值 | 直接使用,无需额外处理 |
| int64 | 楼层、室数 | 检查是否有 NaN,有的话用中位数填充 |
pd.to_numeric的errors='coerce'参数是这里的关键,当某个字段无法转换为数字时,它会将该位置置为 NaN,而不是让整个转换过程抛异常。这种方式最稳妥,比逐个单元格去replace要高效得多。
2.3 用 sample 代替 head 检查数据分布
head()只能看到前 5 行,如果原始数据是按区域排序的,前 5 行只能反映一个区域的房源情况,无法代表全局。这个项目里有ershoufang_old.csv这个文件,说明数据可能是分多次收集后合并的,顺序上可能带着批次特征。我一般会用df.sample(10, random_state=42)随机抽 10 行来检查,这样能更大概率看到不同区域、不同价位的记录。random_state固定随机种子,保证每次抽查结果一致,方便对照清洗前后的变化。
3. 用 Pandas 做实质性清洗:去重、缺失值填充与字段规整
3.1 重复数据不是简单的 drop_duplicates
这个项目提供了ershoufang-clean-utf8-v1.1.csv和ershoufang-clean-ansi-v1.1.csv两个清洗后的版本,说明清洗过程经过了迭代。v1.1 相对于原始文件,核心变化之一肯定是去重。但直接调用drop_duplicates()有一个隐患:它会把所有列完全一致的记录视为重复,而实际业务里,两条房源记录的发布时间不同、带看次数不同,但房屋本身是同一套,并不算真正意义上的重复。
3.1.1 按关键字段去重
对于二手房数据,判断同一套房源,最靠谱的字段组合是"小区名称 + 户型 + 面积 + 朝向"。总价和单价在一段时间内可能调整,但房屋本身的属性不会变。所以去重逻辑应该这样写:
# 按核心属性判断重复,保留第一次出现的记录 df_dedup = df_utf8.drop_duplicates( subset=["小区", "户型", "面积", "朝向"], keep="first" ).copy() print(f"去重前: {len(df_utf8)} 行, 去重后: {len(df_dedup)} 行")subset参数定义了判断重复的列集合,keep='first'表示保留首次出现的记录。之所以加.copy(),是因为 Pandas 的链式操作容易触发视图警告,后续如果对df_dedup做修改,可能会影响原 DataFrame。使用copy()可以把数据复制一份,避免副作用。
3.1.2 重复率过高时检查字段取值
如果去重后行数减少了 20% 以上,复查逻辑是否过于宽松。有可能"朝向"字段的取值不统一,比如同一套房,一条记录写"南北",另一条写"南 北",语义相同但字符串不同,导致漏去重。处理方式是先把字段内容规范化:先用str.strip()去掉首尾空格,再str.replace(" ", "")去掉内部空格,最后在去重。
# 清洗字符串字段中的杂散空格 for col in ["朝向", "户型"]: df_utf8[col] = df_utf8[col].str.strip().str.replace(" ", "") # 重新去重 df_dedup = df_utf8.drop_duplicates(subset=["小区", "户型", "面积", "朝向"], keep="first")这里的str.strip()去除首尾空白,str.replace(" ", "")去除字符串内部的空格,避免"南 北"和"南北"被当成两个不同值。这些字符串操作往往不为人注意,却直接决定了去重是否彻底。
3.2 缺失值处理:能填的不用 dropna 删行
原始数据集里,总价、单价、面积、楼层这几个关键字段缺失比例通常较低,直接dropna()删掉整行影响不大。但有些字段比如"装修情况"或者"建筑年代",可能缺失率高达 40% 以上,把这些行全删掉会损失大量样本,导致后续分组统计时某些区域没有数据。这时应该分列处理:
# 按列处理缺失值 df_clean = df_dedup.copy() # 数值列:缺失值用该列中位数填充 numeric_cols = ["总价", "单价", "面积"] for col in numeric_cols: if col in df_clean.columns: median_val = df_clean[col].median() df_clean[col] = df_clean[col].fillna(median_val) # 类别列:缺失值填充为"未知" category_cols = ["朝向", "装修", "建筑年代"] for col in category_cols: if col in df_clean.columns: df_clean[col] = df_clean[col].fillna("未知")fillna(median_val)用的是该列的中位数,不是平均数。原因是房价、面积这类数据有长尾分布,个别豪宅会把平均值拉高,中位数更能代表普通房源的典型水平。类别列填充"未知"而不是删行,是为了保留该房源的其余属性,比如朝向未知,但面积、价格都真实存在,分组分析时仍然有用。
3.3 单位统一:把"平米""万元"等字符剥离干净
在真实的二手房 CSV 里,"面积"列经常长这样:"78.5平"、"89㎡"、"120.3平米";"总价"列可能是"560万"或"560万元"。这些带单位的字符串无法参与数值计算,必须先清洗。
import re def clean_area(value): """提取字符串中的纯数字部分,单位不一致时统一按平方米处理""" if isinstance(value, str): match = re.search(r"\d+\.?\d*", value) if match: return float(match.group()) return value def clean_price(value): """万元为单位,需要去掉'万'字并保留数值""" if isinstance(value, str): match = re.search(r"\d+\.?\d*", value) if match: return float(match.group()) return value df_clean["面积"] = df_clean["面积"].apply(clean_area) df_clean["总价"] = df_clean["总价"].apply(clean_price)正则表达式\d+\.?\d*匹配整数或带小数的数字,返回第一个匹配到的数字串。这里用apply对整列逐行执行,效率比 for 循环高很多。清洗完成后,再用df_clean["面积"] = pd.to_numeric(df_clean["面积"], errors="coerce")做一次强校验,确保所有值都是 float,之前提到的低效重复就能避免掉。
关于这个项目的案例流程,用一张表可以概括不同数据问题的处理优先度:
| 数据问题 | 处理优先级 | 参考方法与参数 |
|---|---|---|
| 编码错误 | 第一优先,不解决即全乱 | encoding='gbk'或utf-8 |
| 重复记录 | 第二优先,影响所有统计指标 | drop_duplicates(subset=关键列, keep='first') |
| 数值列含单位 | 第三优先,影响计算 | apply + 正则提取数字 |
| 缺失值 | 第四优先,影响样本量 | fillna(state='median'),类别列填"未知" |
这种处理顺序不是绝对的,但按这个次序做,每一步的输入都是可控的,排查问题时会省很多时间。
4. 用 Matplotlib 做数据可视化:让图表回答业务问题
4.1 先从直方图看分布形态
数据分析项目里,可视化不是把数据画出来就完事,而是用图表回答一个具体问题。这个项目最典型的分析问题是:总价集中在哪个区间?单价分布是正态还是偏态?这类问题用直方图最直接:
import matplotlib.pyplot as plt plt.rcParams["font.sans-serif"] = ["SimHei"] # 解决中文标签乱码 plt.rcParams["axes.unicode_minus"] = False # 解决负号显示异常 fig, ax = plt.subplots(figsize=(10, 6)) ax.hist(df_clean["总价"], bins=30, color="#6699CC", alpha=0.75, edgecolor="black") ax.set_xlabel("总价(万元)") ax.set_ylabel("房源数量") ax.set_title("二手房总价分布直方图") plt.tight_layout() plt.savefig("price_distribution.png", dpi=150) plt.show()bins=30决定直方图的分组数量,组数太少会掩盖分布细节,太多会显得杂乱。对于几千条数据,30 是个合理起步值,视情况可以调整到 50。alpha=0.75控制柱子的透明度,多图层叠加时会比较有用。保存图片时用dpi=150,在答辩 PPT 上不会模糊。
4.2 用散点图看单价与面积的关系
直方图只能展示单变量的分布,如果要解释"大面积房源是否单价更低"这类问题,散点图是更好的选择:
fig, ax = plt.subplots(figsize=(10, 6)) ax.scatter( df_clean["面积"], df_clean["单价"], s=12, alpha=0.4, c="#CC6666" ) ax.set_xlabel("面积(平方米)") ax.set_ylabel("单价(万元/平方米)") ax.set_title("面积与单价散点图") plt.tight_layout() plt.savefig("area_vs_price.png", dpi=150) plt.show()散点图的s参数控制点的大小,这里调成 12 是因为数据量偏大,点太大会严重重叠。alpha=0.4是为了在半透明效果下显露点密集区域,密集程度本身就是信息——它告诉你大多数房源的面积和单价集中在哪个区域。
4.3 用柱状图对比各区域均价
这一步是多数据表合并的典型场景——原始数据里有latlng.csv文件,它提供了经纬度信息,用途之一就是把房源按所在区进行分类,然后计算每个区的平均单价。这里用到的聚合操作是groupby:
# 假设 df_clean 里有区域字段,计算每个区域的平均单价 region_price = df_clean.groupby("区域")["单价"].mean().sort_values(ascending=False) # 绘制水平柱状图,避免区域名称过长遮挡 fig, ax = plt.subplots(figsize=(10, 8)) region_price.plot(kind="barh", ax=ax, color="#6699CC") ax.set_xlabel("平均单价(万元/平方米)") ax.set_ylabel("区域") ax.set_title("各区域二手房平均单价对比") plt.tight_layout() plt.savefig("region_price_bar.png", dpi=150) plt.show()groupby("区域")["单价"].mean()是数据分析里出现频率最高的聚合形式,按"区域"分组,对"单价"求均值,然后sort_values排序,使得图表呈现从上到下递减的视觉效果。水平柱状图之所以用barh,是因为区域名称一般是 2 到 4 个字,水平放置时不会被旋转的字号压缩,页面显示更清晰。
在展示或答辩 PPT 里,通常建议把这三张图并列呈现——直方图讲分布、散点图讲关系、柱状图讲对比。一张图回答一个问题,比把所有内容塞进一张图更容易让听者跟上思路。这里用柱状图排序后的结果也可以顺带用一个表格输出,方便直接粘贴到文档里:
| 区域 | 平均单价(万元/平方米) | 房源数量 |
|---|---|---|
| 区域A | 5.42 | 312 |
| 区域B | 4.18 | 458 |
| 区域C | 3.76 | 291 |
这个表格如果在清洗完成后打印,可以作为答辩 PPT 中展示数据概览的底稿。图案和表格一起呈现,内容的可信度会更高,这也是项目拿到高分的原因之一——不是图花哨,而是图表之间逻辑自洽。
4.4 中文字体问题在 Linux 系统下的处理
这段要特别提一下,因为不少人在 Windows 本地写代码没问题,一到 Ubuntu 服务器或 WSL 环境跑plt.show(),图表里的中文全部变成方块。原因是 Linux 系统默认没有 SimHei 字体。常见的做法有两种,任选其一:
方式一,代码里指定系统中文字体路径:
import matplotlib.pyplot as plt from matplotlib.font_manager import FontProperties # 指定已安装的中文字体文件(以 Noto Sans CJK 为例) font_path = "/usr/share/fonts/truetype/noto/NotoSansCJK-Regular.ttc" font_prop = FontProperties(fname=font_path) plt.rcParams["font.family"] = font_prop.get_name()方式二,在线下载字体文件到本地目录再加载。注意不要全局设置plt.rcParams["font.sans-serif"]为不存在的字体名,那样设置完之后问题照旧。用fc-list :lang=zh命令可以在 Linux 终端里查看可用的中文字体列表,确认后再在 Python 里配置,能节省很多排查时间。
5. 进阶技巧:用 loc 与 iloc 让数据切片更干净
5.1 为什么不用裸的中括号条件筛选
很多人在做分析时习惯这样写:
# 不推荐的写法:直接链式赋值会触发警告 df_clean[df_clean["区域"] == "区域A"]["单价"] = df_clean["单价"] * 0.95这里的问题在于,df_clean[...]返回的是一个视图还是副本取决于 Pandas 内部逻辑,链式赋值时修改可能不会生效,或者弹出SettingWithCopyWarning。在答辩 PPT 里看到这个警告很影响观感,而且如果数据量大,视图和副本的内存行为也难以预测。推荐的做法是先用loc精确选择行列:
# 推荐写法:loc 同时指定行条件和目标列 mask = df_clean["区域"] == "区域A" df_clean.loc[mask, "单价"] = df_clean.loc[mask, "单价"] * 0.95loc[mask, "单价"]中,第一个参数mask是布尔索引,选择满足条件的行,第二个参数"单价"是要操作的列。行和列同时定位,修改的就是原 DataFrame 的真实数据,不会产生视图语义。
5.2 整表精确定位用 iloc 配合 shape
iloc按整数位置取值,在需要浏览数据表的某个局部区域时很实用。比如把清洗后的前 100 条记录导出到一个单独文件,作为旧版本ershoufang_old.csv的对照样本:
# 提取前 100 条记录,用作数据核对 sample_df = df_clean.iloc[0:100, :] sample_df.to_csv("check_sample.csv", index=False, encoding="utf-8-sig")iloc[0:100, :]的0:100表示第 0 到第 99 行,冒号表示全部列。写入时用encoding="utf-8-sig"而不是"utf-8",是为了让生成的 CSV 在 Windows Excel 中直接双击打开时不出现中文乱码,这对答辩前检查数据非常实用。这个小细节切实解决了实际教学中高频出现的问题——很多时候不是数据算错了,而是 Excel 打开 CSV 时编码不识别。
另外,在拼接新旧数据时,pd.concat的ignore_index=True也是一个值得记住的参数。两份清洗过的数据纵向堆叠,如果不设ignore_index=True,合并后 DataFrame 的索引会重复,导致后续loc按索引取值时匹配到多行。设了之后,行索引会从前到后重新排列成 0、1、2…,这一行参数往往能避免很多debug时间。
整个项目看下来,编码识别、关键字段去重、数值列的强制类型转换、按区域聚合、utf-8-sig的导出保存,这些操作不大,但都踩在真实业务数据的痛点上。把这套流程跑通一次,提取出自己数据集里对应的字段,再配合直方图、散点图和柱状图来验证结论,一份看得过去的数据分析报告就有雏形了。
本文还有配套的精品资源,点击获取