
简介这是一份面向计算机相关专业学生与项目实战学习者的Python数据分析资源对应课程设计与期末大作业场景以二手房数据为分析对象帮助读者在真实数据集上练习数据采集、清洗、统计分析与可视化全流程。压缩包共190个文件约48.21MB包含18个py源码文件、18个csv数据文件、15个html页面、65个png图表及docx分析报告、pptx展示文档等源码、数据与报告配套齐全下载后可直接运行调试。资源中提供了原始数据与清洗后多版本数据便于对比不同编码与清洗策略的效果报告则完整呈现分析思路与结论解读适合作为报告撰写与结果表达的参考。目前已有65人学习下载适合需要快速上手数据分析项目、积累实操经验并完成课程作业的学习者参考使用。1. 从一份二手房 CSV 到能交付的分析报告这个项目到底在做什么很多人第一次拿到二手房数据第一反应是打开 Excel 拉个均价透视表然后发现同一个小区里 60 平和 140 平的单价差了快一倍均价这个指标瞬间失去意义。基于 Python 的二手房数据分析项目要解决的正是这个问题把散落在 CSV 里的挂牌记录变成一套能解释价格结构、能定位异常房源、能输出可视化报告的可复现流程。它适合两类人——一类是刚学完 pandas 想找个真实数据集练手的入门者另一类是手里有几十个小区挂牌数据、想快速看出哪些房源定价离谱的从业者。整条链路不复杂清洗字段、构造单价与房龄、做分组聚合、画分布与相关性图、最后落成一份 HTML 或 Markdown 报告。真正花时间的不是建模而是把「满五唯一」「精装」「近地铁」这些非结构化描述变成能参与计算的列。下面按我实际跑过一遍的顺序拆开讲。2. 数据清洗与字段构造把挂牌文本变成能算的列2.1 先看清原始字段长什么样二手房数据来源五花八门但字段结构高度相似。常见的一份原始 CSV 大概长这样小区、户型、面积、总价、单价、朝向、楼层、装修、建成年代、标题、标签。麻烦在于后四个字段——楼层可能是「中楼层/共18层」装修可能是「精装」「简装」「毛坯」标题里塞满了「满五唯一」「近地铁」「业主急售」这类卖点建成年代有时是「2008年」有时是「2008」。我一般先做一次字段体检把每列的非空率、唯一值数量、前几个样本打出来避免后面清洗时误伤。import pandas as pd df pd.read_csv(ershoufang.csv, encodingutf-8-sig) # 字段体检非空率、唯一值数、样本 report pd.DataFrame({ 非空率: df.notna().mean().round(3), 唯一值数: df.nunique(), 样本: [df[c].dropna().iloc[0] if df[c].notna().any() else None for c in df.columns] }) print(report)这段代码的作用是快速判断哪些列需要清洗。encodingutf-8-sig是为了处理带 BOM 的 CSV否则第一列列名会多出一个不可见字符后面按列名取值直接报 KeyError这个坑我踩过不止一次。notna().mean()给出非空率低于 0.6 的列基本要考虑是否保留nunique()帮你识别哪些是分类列、哪些是连续列。2.2 面积、总价、单价三列的类型转换原始数据里这三列经常带单位比如「89.5㎡」「320万」「35754元/平」。直接astype(float)会全部变成 NaN。稳妥做法是用正则抽出数字。import re def to_float(x): if pd.isna(x): return None m re.search(r(\d\.?\d*), str(x)) return float(m.group(1)) if m else None for col in [面积, 总价, 单价]: df[col] df[col].apply(to_float) # 用面积和总价反推单价校验原始单价是否可信 df[单价_校验] (df[总价] * 10000 / df[面积]).round(0) df[单价偏差] (df[单价_校验] - df[单价]).abs() / df[单价] print(df[单价偏差].describe())逻辑说明to_float用正则匹配第一个数字兼容「89.5㎡」「320万」这类写法。反推单价这一步很关键——如果原始单价和「总价÷面积」偏差超过 5%说明这条记录要么面积错了要么总价单位不是万。参数上总价 * 10000是把「万」换算成「元」如果你的数据总价单位本来就是元这个系数要去掉。我一般把偏差大于 0.1 的记录单独存一份人工核对不直接删因为有些是复式或赠送面积导致的合理偏差。2.3 从标题和标签里抽特征「满五唯一」「近地铁」「学区」这些词对价格影响很大但它们在原始数据里只是标题里的一段文字。用str.contains批量打标是最省事的做法。keywords { 满五唯一: 满五唯一|满5唯一, 近地铁: 近地铁|地铁口|地铁站, 学区: 学区|学位, 精装: 精装|豪装, 急售: 急售|诚心卖 } for name, pattern in keywords.items(): df[name] df[标题].fillna().str.contains(pattern, regexTrue).astype(int) print(df[list(keywords.keys())].sum())这里每个关键词生成一个 0/1 列后续可以直接进分组聚合或相关性分析。regexTrue允许用|写多个同义词。注意fillna()不能省否则 NaN 会让str.contains返回 NaN 而不是 Falseastype(int)就会报错。参数上关键词表要根据你实际数据的标题用词调整我建议先把标题里出现频率最高的 50 个词打出来再定。2.4 房龄与楼层这两个隐形变量建成年代要转成房龄楼层要从文本转成有序分类。这两列处理不好后面所有分组都会失真。import datetime current_year datetime.datetime.now().year df[建成年代_数字] df[建成年代].apply(to_float) df[房龄] current_year - df[建成年代_数字] def floor_level(x): if pd.isna(x): return 未知 if 低 in str(x): return 低楼层 if 中 in str(x): return 中楼层 if 高 in str(x): return 高楼层 return 未知 df[楼层等级] df[楼层].apply(floor_level) df[房龄] df[房龄].clip(lower0, upper60) # 过滤明显错误clip这一步是后悔药有些数据建成年代填成 1900 或者 2099房龄算出来是负数或 100 多直接参与分析会把均值拉飞。上下限设 0 和 60 是根据常见住宅寿命经验定的你可以按自己城市情况调。楼层转成有序分类后做分组时用pd.Categorical指定顺序否则「高楼层」会排在「低楼层」前面。3. 分组聚合与价格结构分析哪些因素真的在推高单价3.1 按小区和户型做基准对比清洗完之后第一件有价值的事是按小区算单价中位数而不是均值。二手房单价分布右偏严重几套豪宅就能把小区均价拉高一大截中位数更稳。# 只保留样本量足够的小区避免单套样本误导 community_stat df.groupby(小区).agg( 套数(单价, count), 单价中位数(单价, median), 单价均值(单价, mean), 面积中位数(面积, median) ).query(套数 5).sort_values(单价中位数, ascendingFalse) print(community_stat.head(20))query(套数 5)是过滤掉只有一两套挂牌的小区这类样本统计意义太弱。agg里同时算中位数和均值两者差距大的小区往往内部房源差异极大值得单独看。参数上套数阈值 5 是个经验值数据量大可以提到 10数据量小降到 3。3.2 用透视表看「户型 × 房龄」的单价矩阵单看一个维度容易漏掉交互效应。比如小户型单价高但可能只是因为小户型集中在市中心老小区。用透视表把两个维度交叉起来看。df[房龄段] pd.cut(df[房龄], bins[0, 5, 10, 20, 30, 60], labels[5年内, 5-10年, 10-20年, 20-30年, 30年以上]) pivot df.pivot_table( values单价, index房龄段, columns户型, aggfuncmedian ) print(pivot.round(0))pd.cut把连续房龄切成有序区间pivot_table默认会丢掉 NaN如果某个「户型 × 房龄段」组合没有样本格子里就是空的这本身也是信息——说明该组合在市场上很稀缺。aggfuncmedian同样是为了抗离群值。这个矩阵能直接看出是「老破小」单价高还是「次新房大户型」单价高结论往往和直觉相反。3.3 特征与单价的相关性排序前面打的「满五唯一」「近地铁」这些 0/1 标签可以用点二列相关看它们和单价的关系。注意这只是相关不是因果但排序能帮你快速定位哪些特征值得在报告里重点讲。features [满五唯一, 近地铁, 学区, 精装, 急售, 房龄] corr df[features [单价]].corr()[单价].drop(单价).sort_values(ascendingFalse) print(corr.round(3))corr()默认算皮尔逊相关对 0/1 变量和连续变量的组合就是点二列相关。结果里正数表示该特征存在时单价偏高负数表示偏低。我一般会把相关系数绝对值小于 0.05 的特征从报告里去掉避免读者被噪声干扰。参数上如果你的单价分布特别偏可以先对单价取对数再算相关结果更稳。3.4 定位异常房源谁在乱定价分析的另一半价值是找出定价明显偏离小区基准的房源。用小区中位数做基准算每套房的偏离度。df[小区单价中位数] df.groupby(小区)[单价].transform(median) df[偏离度] (df[单价] - df[小区单价中位数]) / df[小区单价中位数] outliers df[df[偏离度].abs() 0.3][ [小区, 户型, 面积, 总价, 单价, 偏离度, 标题] ].sort_values(偏离度) print(outliers.head(30))transform(median)把小区中位数广播回每一行这样每套房都能和本小区基准比。偏离度阈值 0.3 是我常用的超过 30% 就值得人工看一眼。结果里偏离度为负的是明显低于小区均价的可能是真捡漏也可能是数据错误为正的可能是挂牌价虚高。这一步的输出直接就是报告里最有看点的部分。4. 可视化与报告输出让结论能被人一眼看懂4.1 单价分布直方图与小区对比条形图分布图用 matplotlib 或 seaborn 都行关键是把中位数线画上去否则读者不知道分布中心在哪。import matplotlib.pyplot as plt import seaborn as sns plt.rcParams[font.sans-serif] [SimHei] # 中文显示 plt.rcParams[axes.unicode_minus] False fig, axes plt.subplots(1, 2, figsize(14, 5)) sns.histplot(df[单价].dropna(), bins50, axaxes[0], kdeTrue) axes[0].axvline(df[单价].median(), colorred, linestyle--, label中位数) axes[0].set_title(单价分布) axes[0].legend() top10 community_stat.head(10) axes[1].barh(top10.index, top10[单价中位数]) axes[1].set_title(小区单价中位数 Top10) axes[1].invert_yaxis() plt.tight_layout() plt.savefig(price_overview.png, dpi150)font.sans-serif设成 SimHei 是为了中文不乱码Linux 环境下如果没有这个字体换成WenQuanYi Micro Hei或直接指定字体文件路径。axvline画中位数参考线invert_yaxis让条形图从高到低排列。dpi150是报告插图常用的清晰度再高文件会很大。4.2 用 pandas 直接生成 HTML 报告骨架不想引入额外报告框架的话DataFrame.to_html()加上一点字符串拼接就能出一份能直接打开的 HTML。html_parts [] html_parts.append(h1二手房数据分析报告/h1) html_parts.append(fp样本量{len(df)} 条覆盖小区{df[小区].nunique()} 个/p) html_parts.append(h2小区单价排名/h2) html_parts.append(community_stat.head(20).to_html(float_format%.0f)) html_parts.append(h2异常定价房源/h2) html_parts.append(outliers.head(20).to_html(float_format%.2f)) with open(report.html, w, encodingutf-8) as f: f.write(\n.join(html_parts))to_html的float_format控制小数位避免出现一长串浮点数。encodingutf-8保证中文正常。这个骨架的好处是不依赖任何报告库坏处是样式朴素想好看可以套一个简单的 CSS 字符串。图片用img srcprice_overview.png插进去即可。4.3 报告里必须写清的三个数字一份能被人信任的报告开头就要交代清楚数据边界。我固定放三个数字样本总量、覆盖小区数、数据时间范围。样本量决定结论的置信度小区数决定覆盖面时间范围决定结论的时效性——二手房市场三个月就能变一轮去年的数据今年只能当参考。这三个数字放在报告第一段比任何花哨图表都重要。5. 避坑与排查跑这个项目最容易翻车的五个地方5.1 中文列名读取后变成乱码或带 BOM现象df[小区]报 KeyError打印列名发现第一个是\ufeff小区。原因是 CSV 带 BOM用utf-8读取时 BOM 被当成列名的一部分。解决读取时用encodingutf-8-sig或者读完后df.columns df.columns.str.replace(\ufeff, )。这个坑在 Windows 上用 Excel 另存的 CSV 里几乎必现。5.2 单价单位不统一导致均值失真现象算出来的平均单价是 3.5 万但明显有几条记录单价是 350 万。原因是部分数据单价单位是「元/平」部分是「万元/平」。解决先看df[单价].describe()的最大值如果最大值比中位数大两个数量级基本就是单位混了。统一除以 10000 或乘以 10000处理前先抽样确认。5.3 分组聚合时 NaN 被静默丢弃现象按小区分组后总套数比原始数据少了几百条。原因是小区列有 NaNgroupby默认丢弃。解决分组前先df[小区] df[小区].fillna(未知小区)或者用dropnaFalse参数保留。我一般倾向填「未知小区」因为丢掉的数据里可能有其他字段的有效信息。5.4 相关性分析把分类编码当连续值现象把「楼层等级」直接astype(category).cat.codes后算相关得出「楼层越高单价越高」的结论但实际数据里高楼层和低楼层单价差不多。原因是编码顺序是任意的算相关没有意义。解决分类变量要么做分组对比要么做 one-hot 编码后再分析不要直接当连续值算皮尔逊相关。5.5 报告图片中文显示成方框现象matplotlib 图里所有中文变成豆腐块。原因是默认字体不含中文。解决plt.rcParams[font.sans-serif] [SimHei]同时plt.rcParams[axes.unicode_minus] False解决负号显示问题。如果系统没装 SimHei用matplotlib.font_manager查一下可用中文字体再指定。6. 把分析固化成可复用的脚本我的三个习惯跑通一次不难难的是下周换一批数据还能三分钟出报告。我自己的做法是把整条链路拆成三个函数load_and_clean(path)负责读取和清洗analyze(df)负责分组聚合和异常检测render(df, stats, outliers, out_path)负责出图和写 HTML。主程序只有三行调用。这样换数据时只改路径逻辑一行不动。第二个习惯是给每个清洗步骤留一个中间产物。比如清洗后的数据存一份clean.csv异常房源存一份outliers.csv。报告出问题时能快速定位是哪一步的锅而不是从头再跑一遍。这个习惯在数据源不稳定的时候特别值钱。第三个习惯是把所有阈值集中写在脚本开头的一个字典里比如CONFIG {min_community_count: 5, outlier_threshold: 0.3, age_bins: [0, 5, 10, 20, 30, 60]}。这样调参不用在代码里到处找魔法数字也方便不同城市用不同参数。下面是一个最小可复用的骨架CONFIG { min_community_count: 5, outlier_threshold: 0.3, age_bins: [0, 5, 10, 20, 30, 60], age_labels: [5年内, 5-10年, 10-20年, 20-30年, 30年以上] } def load_and_clean(path): df pd.read_csv(path, encodingutf-8-sig) # 类型转换、特征抽取、房龄计算 return df def analyze(df, config): stat df.groupby(小区).agg(...).query(f套数 {config[min_community_count]}) df[偏离度] ... outliers df[df[偏离度].abs() config[outlier_threshold]] return stat, outliers def render(df, stat, outliers, out_path): # 出图 写 HTML pass if __name__ __main__: df load_and_clean(ershoufang.csv) stat, outliers analyze(df, CONFIG) render(df, stat, outliers, report.html)这个骨架的价值在于它把「一次性分析」变成了「可重复流程」。你下次拿到新数据只需要确认列名一致跑一遍就有报告。如果列名变了改load_and_clean里的映射就行分析和渲染完全不用动。我见过太多人每次分析都重写一遍代码最后连自己上次怎么算的都忘了。把流程固化下来才是这个项目真正能持续产出价值的地方。希望帮到你。本文还有配套的精品资源点击获取