☰
二手房数据分析全流程:从脏数据清洗到交互式大屏可视化
2026/10/3 2:59:16 网站建设 项目流程

简介:本资源是一套基于Python开发的二手房数据分析与可视化系统完整源码,专为高校计算机、数据科学及相关专业学生设计,适用于Python毕业设计、课程设计及期末大作业等实践场景。项目聚焦真实二手房市场数据,涵盖数据清洗、统计分析、多维度可视化及交互式前端展示全流程,具备高完成度与工程规范性,可直接部署运行。压缩包共156个文件,含18个核心Python脚本(实现爬虫、清洗、建模与图表生成)、18个CSV数据集(含原始与清洗后多版本房源数据)、15个HTML+11个JS前端页面(构建可视化看板)、65张PNG图表输出及配置文件、说明文档等,整体大小约40MB。目前已有497人学习下载,资源提供完整目录结构、调试通过的可执行代码、多格式数据样本及界面美观的可视化成果,助学习者深入理解数据分析项目落地的关键环节与技术栈协同逻辑。

1. 这不是又一个“爬完就扔”的二手房脚本:它是一套能跑通从脏数据清洗到交互式大屏的完整分析链路,专治毕业设计答辩前一周还在改图表配色的焦虑

你手头可能已经攒了三四个“二手房爬虫+matplotlib画图”的 GitHub 项目,但真到答辩现场被老师问“你这个均价热力图,缺失值怎么处理的?为什么用中位数而不是均值?你清洗掉的那372条‘单价超10万’的记录,是异常值还是学区房?”——当场哑火。这个源码包不一样:它不只给你一个能点开的.py文件,而是把整个分析闭环拆成了可验证、可复现、可答辩的六个模块:原始 CSV 的编码混乱问题(ANSI/UTF-8 混杂)、字段语义歧义(“建筑面积”字段里混着“㎡”“平米”“平”甚至空格)、价格单位不统一(万元/套 vs 元/㎡)、地理坐标缺失下的区域映射逻辑、基于真实业务规则的异常值拦截(比如单价低于2000元/㎡且面积超200㎡的“凶宅”标记),最后落到一个带筛选控件、支持导出 PNG/PDF 的 PySide6 可视化界面。它不是教你怎么写plt.show(),而是告诉你:当老师指着你的折线图问“这个2023年Q4成交量断崖下跌,是数据漏采还是市场真实拐点?”时,你能在 30 秒内切到data_audit_report.md里调出对应时间段的采集日志和清洗日志截图。适合正在赶 Python 课程设计 deadline、需要一份“能讲清楚每一步为什么这么干”的高分作业的同学,也适合想快速搭建本地数据分析 demo 的初级数据岗面试者。


2. 数据清洗不是删空行:从 5 个原始 CSV 文件的编码战争与字段语义解耦开始

这个项目最硬核的起点,不是代码,而是对ershoufang-origin-ansi.csv和ershoufang-origin-utf8.csv这两组同名不同编码文件的处理策略。它们不是备份,而是真实采集过程中因目标网站响应头缺失导致的编码错乱产物——前者用chardet检测为GB2312,后者为UTF-8,但字段内容完全一致。项目没用暴力open(..., encoding='gbk')硬解,而是通过encoding_detector.py脚本做了三层校验:先用chardet初筛,再用codecs.lookup_error('replace')尝试解码关键字段(如“标题”“地址”),最后比对两个解码结果的中文字符覆盖率。只有当 UTF-8 解码后中文覆盖率 < 95% 且 GBK 解码后 > 98%,才判定为 ANSI 编码。这种判断逻辑直接写进了cleaner/data_loader.py的auto_detect_encoding()方法里。

2.1 字段标准化:把“建筑面积:120平米”“建面:98.5㎡”“面积: 85平”统一成 float 型数值

原始数据里,“面积”字段有至少 7 种表达形式:带单位(㎡/平米/平)、带冒号/顿号/空格、含括号注释(如“120㎡(含公摊)”)、甚至混入文字(“暂无数据”)。项目没用正则硬匹配,而是采用“双阶段提取法”:

import re import pandas as pd def extract_area_value(text: str) -> float: """ 从混合文本中提取面积数值,单位自动转换为平方米 支持:'120㎡', '98.5平米', '85平', '约72.3', '100-120'(取均值) """ if not isinstance(text, str) or not text.strip(): return 0.0 # 阶段一:剥离所有非数字、小数点、连字符、中文单位的字符 cleaned = re.sub(r'[^\d.\-—~\u4e00-\u9fa5]', ' ', text) # 阶段二:按中文单位做归一化("平"/"平米"/"㎡" → 平方米;"亩" → *666.67) unit_multipliers = { '平': 1.0, '平米': 1.0, '㎡': 1.0, '亩': 666.67, '公顷': 10000.0 } # 提取数字部分(支持范围如"90-100") numbers = re.findall(r'(\d+\.?\d*)[-—~](\d+\.?\d*)|(\d+\.?\d*)', cleaned) if not numbers: return 0.0 # 处理范围值(取平均)和单值 area_values = [] for match in numbers: if match[0]: # 匹配到范围 start, end = float(match[0]), float(match[1]) area_values.append((start + end) / 2) elif match[2]: # 匹配到单值 area_values.append(float(match[2])) # 取第一个有效值(避免“总价120万,面积85平”被误提两次) return area_values[0] if area_values else 0.0 # 应用到 DataFrame df['area_clean'] = df['area_raw'].apply(extract_area_value)

提示:这段代码的关键在re.sub(r'[^\d.\-—~\u4e00-\u9fa5]', ' ', text)——它保留中文单位字符供后续识别,而不是粗暴删除。很多同学用re.sub(r'[^0-9.]', '', text)会把“120㎡”变成“120”,丢失单位信息,导致“120亩”也被当成120㎡,误差超600倍。

2.2 价格字段解耦:分离“总价”与“单价”,并建立双向校验关系

原始 CSV 中,“价格”字段存在三种形态:“320万/套”“42500元/㎡”“总价320万,单价42500元/㎡”。项目用price_parser.py实现了结构化解析:

def parse_price_field(text: str) -> dict: """ 返回字典:{'total': float, 'unit': float, 'unit_type': str, 'raw': str} unit_type: 'per_m2', 'per_unit', 'unknown' """ if not isinstance(text, str): return {'total': 0.0, 'unit': 0.0, 'unit_type': 'unknown', 'raw': str(text)} # 规则1:匹配“X万/套”或“X万元/套” total_match = re.search(r'([\d.]+)[\s]*[万]?[元]?[/\s]*(?:套|单位)', text) if total_match: total_val = float(total_match.group(1)) if '万' in text: total_val *= 10000 return {'total': total_val, 'unit': 0.0, 'unit_type': 'per_unit', 'raw': text} # 规则2:匹配“X元/㎡”或“X元每平米” unit_match = re.search(r'([\d.]+)[\s]*[元]?[/\s]*(?:㎡|平米|平)', text) if unit_match: unit_val = float(unit_match.group(1)) return {'total': 0.0, 'unit': unit_val, 'unit_type': 'per_m2', 'raw': text} # 规则3:同时存在总价和单价(如“总价320万,单价42500元/㎡”) both_match = re.search(r'总价([\d.]+)[\s]*[万]?[元]?,?\s*单价([\d.]+)[\s]*[元]?[/\s]*(?:㎡|平米)', text) if both_match: total_val = float(both_match.group(1)) unit_val = float(both_match.group(2)) if '万' in text.split('总价')[1]: total_val *= 10000 return {'total': total_val, 'unit': unit_val, 'unit_type': 'both', 'raw': text} return {'total': 0.0, 'unit': 0.0, 'unit_type': 'unknown', 'raw': text} # 批量解析 price_df = df['price_raw'].apply(parse_price_field) df['price_total'] = [p['total'] for p in price_df] df['price_unit'] = [p['unit'] for p in price_df] df['price_type'] = [p['unit_type'] for p in price_df]

注意:parse_price_field()的返回值unit_type是后续清洗的关键开关。当unit_type == 'per_m2'且area_clean > 0时,系统会反向计算price_total = price_unit * area_clean并与原始总价比对,偏差 >15% 则标记为price_consistency_flag = False,进入人工复核队列。这比单纯删掉“异常值”更符合答辩场景——你能指着表格说:“老师,这12条记录我标红了,因为单价×面积≠总价,可能是录入错误,我建议剔除。”

2.3 地理信息补全:没有经纬度?用行政区划树+模糊匹配生成伪坐标

ershoufang-clean-utf8-v1.1.csv里有近 40% 的记录缺失lng/lat字段。项目没用百度/高德 API(避免密钥泄露和调用限制),而是构建了三级行政区划树(省→市→区),配合fuzzywuzzy做地址模糊匹配:

from fuzzywuzzy import fuzz import json # 加载预置行政区划树(省市区三级,含常用别名) with open('data/region_tree.json', 'r', encoding='utf-8') as f: region_tree = json.load(f) def match_region_by_address(address: str) -> dict: """ 输入地址字符串,返回匹配的省市区代码及置信度 示例:输入"北京市朝阳区建国路8号" → {'province': '北京', 'city': '北京', 'district': '朝阳区', 'score': 92} """ if not address or not isinstance(address, str): return {'province': '', 'city': '', 'district': '', 'score': 0} best_match = {'score': 0} # 逐级匹配:先匹配区,再匹配市,最后省 for province in region_tree: for city in province.get('cities', []): for district in city.get('districts', []): # 计算地址与区名的相似度(考虑别名) names_to_check = [district['name']] + district.get('aliases', []) for name in names_to_check: score = fuzz.partial_ratio(address, name) if score > best_match['score']: best_match = { 'province': province['name'], 'city': city['name'], 'district': district['name'], 'score': score } return best_match # 应用匹配 df['region_match'] = df['address'].apply(match_region_by_address) df['province'] = df['region_match'].apply(lambda x: x['province']) df['city'] = df['region_match'].apply(lambda x: x['city']) df['district'] = df['region_match'].apply(lambda x: x['district']) df['match_score'] = df['region_match'].apply(lambda x: x['score'])

提示:fuzz.partial_ratio()比fuzz.ratio()更适合地址匹配——它不要求字符串完全重合,能匹配“朝阳区建国路8号”和“朝阳区”这种包含关系。而region_tree.json是手动整理的,包含“海淀”“海淀区”“北京市海淀区”等别名,避免因用户输入简写导致匹配失败。


3. 分析逻辑不是堆函数:用业务规则驱动统计口径,拒绝“平均数陷阱”

很多同学的毕业设计图表里,房价均价直接df['price_unit'].mean(),结果被老师一句“北京五环外均价 12 万,海淀中关村 28 万,你这个全市均价 15 万,权重怎么算的?”问懵。这个项目把统计口径拆成了可配置、可验证、可答辩的三层:基础层(原始字段)、业务层(加权计算)、展示层(聚合图表)。核心逻辑封装在analyzer/business_rules.py中。

3.1 加权均价计算:按区域成交量占比动态赋权,而非简单算术平均

def calculate_weighted_avg_price(df: pd.DataFrame, weight_col: str = 'area_clean', price_col: str = 'price_unit') -> float: """ 计算加权均价:权重 = area_clean(面积越大,该房源对市场均价影响越大) 避免小户型低价拉低整体均价,或豪宅高价扭曲感知 """ # 过滤无效值 valid_mask = (df[weight_col] > 0) & (df[price_col] > 0) & (df[price_col] < 100000) df_valid = df[valid_mask].copy() if len(df_valid) == 0: return 0.0 # 权重归一化 weights = df_valid[weight_col] / df_valid[weight_col].sum() # 加权计算 weighted_avg = (df_valid[price_col] * weights).sum() return round(weighted_avg, 2) # 全市加权均价 city_avg = calculate_weighted_avg_price(df) # 分区加权均价(按 district 分组) district_avg = df.groupby('district').apply( lambda x: calculate_weighted_avg_price(x) ).reset_index(name='weighted_avg_price')

注意:weight_col='area_clean'是业务强相关选择。二手房市场中,大面积房源交易频次虽低,但单笔金额高、对资金面影响大,其价格更能反映区域真实价值锚点。而price_col='price_unit'保证了单位统一(元/㎡),避免“总价”因面积差异失去可比性。

3.2 成交周期分析:从“挂牌到成交天数”推导市场冷热,而非仅看成交量

原始数据中没有“成交日期”,但有“挂牌日期”和“是否已售”状态。项目用analyzer/market_heat.py构建了“挂牌存活期”模型:

from datetime import datetime, timedelta def calculate_listing_duration(df: pd.DataFrame) -> pd.Series: """ 计算挂牌存活期(天):当前日期 - 挂牌日期 对于已售房源,用实际成交日期(若存在);否则用挂牌日期+30天(行业经验值) """ today = datetime.now() def get_duration(row): if pd.isna(row['list_date']): return 0 try: list_dt = datetime.strptime(str(row['list_date']), '%Y-%m-%d') except ValueError: return 0 if row['status'] == '已售' and pd.notna(row['sold_date']): try: sold_dt = datetime.strptime(str(row['sold_date']), '%Y-%m-%d') return (sold_dt - list_dt).days except ValueError: pass # 未售或无成交日期,按行业经验值估算 return min((today - list_dt).days, 180) # 最长算180天,避免僵尸房源干扰 return df.apply(get_duration, axis=1) df['listing_duration'] = calculate_listing_duration(df)

提示:min((today - list_dt).days, 180)是关键约束。北京某小区有套房子挂了 5 年没卖,如果直接计入,会把“平均挂牌时长”拉到 200+ 天,严重失真。180 天是链家、贝壳等平台公认的“有效挂牌期”阈值,超过即视为失效房源,应从活跃市场分析中剔除。

3.3 异常值拦截:用 IQR + 业务规则双校验,不盲目删除

def flag_outliers_iqr_business(df: pd.DataFrame, col: str, iqr_multiplier: float = 1.5, business_rules: dict = None) -> pd.Series: """ 双重异常值标记: - IQR 法:Q1 - 1.5*IQR, Q3 + 1.5*IQR - 业务规则:如 price_unit < 2000 & area_clean > 200 → 凶宅嫌疑 """ if business_rules is None: business_rules = {} # IQR 标记 Q1 = df[col].quantile(0.25) Q3 = df[col].quantile(0.75) IQR = Q3 - Q1 lower_bound = Q1 - iqr_multiplier * IQR upper_bound = Q3 + iqr_multiplier * IQR iqr_flags = (df[col] < lower_bound) | (df[col] > upper_bound) # 业务规则标记(示例) business_flags = pd.Series([False] * len(df)) if col == 'price_unit': # 单价过低且面积过大:可能为事故房、产权瑕疵 business_flags |= (df['price_unit'] < 2000) & (df['area_clean'] > 200) # 单价过高且楼层过低:可能为底商、违建 business_flags |= (df['price_unit'] > 150000) & (df['floor'] <= 2) return iqr_flags | business_flags # 应用 df['price_unit_outlier'] = flag_outliers_iqr_business( df, 'price_unit', business_rules={'low_price_large_area': True, 'high_price_low_floor': True} )

注意:business_rules参数是答辩亮点。你可以告诉老师:“我定义了两条业务规则:第一,单价低于2000元/㎡且面积超200㎡的,大概率是事故房或无法正常交易的产权房;第二,单价超15万/㎡且楼层≤2的,多为底层商铺或存在违建风险,这两类我都标为 outlier,后续分析中排除。”——这比说“我用了IQR方法”有力得多。


4. 可视化不是换主题:PySide6 + Matplotlib 的交互式大屏,支持实时筛选与导出

这个项目的可视化模块(gui/main_window.py)没用 Streamlit 或 Dash——前者部署麻烦,后者答辩时容易因网络波动崩掉。它用 PySide6 搭建原生桌面应用,主界面是QTabWidget,包含“全局概览”“区域对比”“价格分布”“成交趋势”四个 Tab,每个 Tab 内嵌FigureCanvasQTAgg,图表支持缩放、拖拽、鼠标悬停显示详情。最关键的是,所有图表都绑定同一个筛选器控件(QComboBox区域选择、QDateEdit时间范围、QSlider价格区间),实现“改一个,全联动”。

4.1 全局概览 Tab:用mplfinance绘制带成交量的 K 线图,替代传统折线图

import mplfinance as mpf import pandas as pd def plot_price_trend_kline(df: pd.DataFrame, ax=None): """ 绘制价格趋势 K 线图:x轴为时间,y轴为价格,柱状图为成交量 数据需按日期聚合:date, open, high, low, close, volume """ # 按月聚合(示例) df_monthly = df.copy() df_monthly['date'] = pd.to_datetime(df_monthly['list_date']) df_monthly = df_monthly.set_index('date').resample('M').agg({ 'price_unit': ['min', 'max', 'first', 'last'], 'id': 'count' # 作为成交量 }).round(2) # 重命名列以匹配 mplfinance 要求 df_monthly.columns = ['low', 'high', 'open', 'close', 'volume'] # 绘制 mpf.plot(df_monthly, type='candle', style='yahoo', ylabel='单价 (元/㎡)', volume=True, mav=(7, 30), # 7日/30日均线 figratio=(12, 6), ax=ax) # 在 GUI 中调用 fig, ax = plt.subplots(figsize=(12, 6)) plot_price_trend_kline(df_filtered, ax=ax) canvas = FigureCanvasQTAgg(fig)

提示:mplfinance的 K 线图比折线图更能体现市场情绪。开盘价(first)、收盘价(last)、最高价(max)、最低价(min)构成一根 K 线,配合成交量柱,能直观看出“放量上涨”“缩量下跌”等信号。答辩时老师问“你怎么判断市场是回暖还是回调?”,你可以指着 K 线说:“看2023年10月这根阳线,成交量是前月2.3倍,且收盘价突破30日均线,这是典型启动信号。”

4.2 区域对比 Tab:用seaborn.catplot实现箱线图+散点叠加,暴露离群点

import seaborn as sns import matplotlib.pyplot as plt def plot_district_comparison(df: pd.DataFrame, ax=None): """ 绘制各区域单价箱线图,叠加散点显示具体房源 """ # 过滤掉空区域 df_valid = df[df['district'].notna() & (df['district'] != '')] # 绘制箱线图(按 district 排序,按均价降序) districts_ordered = df_valid.groupby('district')['price_unit'].median().sort_values(ascending=False).index sns.boxplot(data=df_valid, x='price_unit', y='district', order=districts_ordered, width=0.5, ax=ax) # 叠加散点(半透明,避免遮挡) sns.stripplot(data=df_valid, x='price_unit', y='district', order=districts_ordered, alpha=0.4, size=3, color='black', ax=ax) ax.set_xlabel('单价 (元/㎡)') ax.set_title('各行政区二手房单价分布(箱线图+散点)') # 在 GUI 中调用 fig, ax = plt.subplots(figsize=(10, 8)) plot_district_comparison(df_filtered, ax=ax) canvas = FigureCanvasQTAgg(fig)

注意:stripplot叠加在boxplot上,是答辩利器。老师问“朝阳区为什么箱子这么宽?”,你直接指出散点图里那些远离箱子的点:“这些是望京、酒仙桥的科技公司员工购房,单价超8万,拉高了上四分位数;而东坝、豆各庄的刚需盘单价3-4万,压低了下四分位数,所以箱子宽——说明朝阳区市场分化严重。”

4.3 交互筛选与导出:一键 PNG/PDF,附带数据水印

def export_chart_to_file(canvas, filename: str, format: str = 'png'): """ 导出当前图表为文件,自动添加数据水印 """ fig = canvas.figure # 添加水印 fig.text(0.5, 0.5, f'Data Source: ershoufang-clean-utf8-v1.1.csv\nExport Time: {datetime.now().strftime("%Y-%m-%d %H:%M")}', fontsize=12, alpha=0.3, ha='center', va='center', rotation=30) if format == 'pdf': fig.savefig(filename, bbox_inches='tight', dpi=300, format='pdf') else: fig.savefig(filename, bbox_inches='tight', dpi=150, format=format) # GUI 中绑定按钮 self.export_png_btn.clicked.connect( lambda: export_chart_to_file(self.current_canvas, 'price_trend.png', 'png') ) self.export_pdf_btn.clicked.connect( lambda: export_chart_to_file(self.current_canvas, 'price_trend.pdf', 'pdf') )

提示:fig.text(..., alpha=0.3)添加的半透明水印,既表明数据来源(避免答辩时被质疑“你这数据哪来的?”),又不影响图表阅读。PDF 导出用dpi=300,确保打印清晰;PNG 用dpi=150,兼顾清晰度与文件大小。


5. 避坑:五个血泪经验总结,专治运行报错、图表空白、答辩翻车

这个项目在调试阶段踩过的坑,比代码行数还多。以下是答辩前必须检查的五个致命点,每一条都对应真实翻车现场:

5.1 现象:程序启动后界面空白,控制台无报错

原因:PySide6版本与matplotlib后端冲突。项目默认使用Qt5Agg,但新版本 PySide6 要求QtAgg。requirements.txt中指定PySide6==6.5.3和matplotlib==3.7.1,若你升级到PySide6==6.7.0,FigureCanvasQTAgg初始化会静默失败。
解决:严格按requirements.txt安装:pip install -r requirements.txt。若已升级,先卸载pip uninstall PySide6 matplotlib,再重装。

5.2 现象:清洗后的 CSV 中,“单价”字段全是 0.0

原因:price_parser.py中正则表达式r'([\d.]+)[\s]*[元]?[/\s]*(?:㎡|平米|平)'无法匹配“42500元/平方米”中的“平方米”(多了一个“米”字)。原始数据存在“㎡”“平米”“平方米”“平”四种写法,但代码只覆盖了前三种。
解决:打开price_parser.py,将正则中的(?:㎡|平米|平)改为(?:㎡|平米|平方米|平),并重新运行cleaner/run_all_cleaners.py。

5.3 现象:地图热力图显示为一片灰色,无颜色渐变

原因:geopandas加载的行政区划 GeoJSON 文件路径错误。项目中data/beijing_districts.geojson是相对路径,若你在gui/目录下运行main.py,Python 当前工作目录是gui/,而data/在上一级,导致gpd.read_file('data/beijing_districts.geojson')报错,程序降级为纯色填充。
解决:在analyzer/heatmap_generator.py中,将路径改为绝对路径:

import os geojson_path = os.path.join(os.path.dirname(__file__), '..', 'data', 'beijing_districts.geojson') gdf = gpd.read_file(geojson_path)

5.4 现象:筛选“海淀区”后,图表数据量骤减 90%,但原始 CSV 中海淀区记录充足

原因:region_match的模糊匹配分数阈值设为 85,而部分海淀房源地址写为“北京市海定区”,fuzzywuzzy匹配“海定区”与“海淀区”得分仅 72,被过滤。match_score字段在清洗后被设为>=85才保留,导致误杀。
解决:打开cleaner/data_cleaner.py,找到df = df[df['match_score'] >= 85],将85改为70,并重新运行清洗脚本。答辩时可说明:“我将匹配阈值设为 70,因为实际地址存在大量错别字(如‘海定’‘朝杨’),过严会损失有效样本。”

5.5 现象:导出 PDF 后,中文标题显示为方框(□□□)

原因:matplotlib默认字体不支持中文。虽然代码中设置了plt.rcParams['font.sans-serif'] = ['SimHei', 'Arial Unicode MS'],但若系统未安装SimHei(微软雅黑),且Arial Unicode MS在 macOS/Linux 上不可用,就会回退到无中文支持字体。
解决:在gui/main_window.py开头添加字体强制加载:

import matplotlib.font_manager as fm # 指定中文字体路径(Windows) zh_font = fm.FontProperties(fname='C:/Windows/Fonts/msyh.ttc') # 微软雅黑 plt.rcParams['font.sans-serif'] = ['Microsoft YaHei'] plt.rcParams['axes.unicode_minus'] = False # 正常显示负号

macOS 用户替换为/System/Library/Fonts/PingFang.ttc,Linux 用户安装fonts-wqy-zenhei并用/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc。


6. 答辩加分技巧:用data_audit_report.md自动生成清洗日志,让老师看到你的思考过程

答辩时最怕被问“你这个数据怎么来的?清洗逻辑是什么?”,然后手忙脚乱翻代码。这个项目在report/目录下内置了generate_audit_report.py,它会在每次运行清洗脚本后,自动生成一份 Markdown 格式的审计报告,包含原始数据概况、清洗步骤耗时、各字段缺失率、异常值分布直方图、以及关键决策依据。这才是高分作业的隐藏王牌。

6.1 报告结构:从数据快照到决策溯源,全程可追溯

data_audit_report.md不是静态文档,而是由pandas_profiling(轻量版)+ 自定义统计脚本动态生成。核心结构如下:

模块内容答辩话术
数据快照原始 CSV 行数、列数、编码检测结果、内存占用“老师,原始数据共 20,142 条,其中 12,893 条为 UTF-8 编码,7,249 条为 GBK,我们用双编码校验确保无乱码。”
清洗步骤每步耗时(ms)、处理行数、输出文件名“字段标准化耗时 842ms,处理全部 20,142 条;价格解耦耗时 1,205ms,因需正则匹配 7 种格式。”
质量看板各字段缺失率表格、price_unit_outlier标记数、match_score < 70记录数“地址匹配成功率 92.3%,未匹配的 1,542 条中,87% 是‘XX村’‘XX庄’等非标准地名,我们人工补充了 327 条。”
决策依据关键参数截图(如 IQR multiplier=1.5 的行业依据)、业务规则原文“IQR 系数 1.5 采用 Tukey 箱线图标准;‘单价<2000且面积>200’规则,源自链家研究院《2023 事故房交易白皮书》第 4.2 节。”

6.2 自动生成:三行命令,产出带图表的 PDF 报告

# 1. 运行清洗(自动触发报告生成) python cleaner/run_all_cleaners.py # 2. 报告生成脚本会自动执行,输出 report/data_audit_report.md # 3. 转 PDF(需安装 pandoc 和 wkhtmltopdf) pandoc report/data_audit_report.md -o report/data_audit_report.pdf \ --pdf-engine=wkhtmltopdf \ --css report/style.css \ --variable papersize:a4

注意:style.css里预置了学术风排版:标题居中、表格边框、代码块灰色背景、图表居中。PDF 导出后,首页是带学校 Logo 和项目名称的封面页,第二页是目录,第三页开始是审计内容。答辩时,你可以直接打开 PDF,翻到“质量看板”页,指着表格说:“老师,您看,清洗后price_unit缺失率从 12.7% 降到 0.3%,area_clean缺失率从 8.2% 降到 0.1%,这就是清洗的价值。”

6.3 答辩实战:把报告变成你的“思考外化”工具

我带过三届毕设学生,发现一个规律:老师不关心你写了多少行代码,只关心你有没有“数据思维”。而data_audit_report.md就是把思维具象化的载体。比如,当老师问“你为什么用中位数而不是均值计算区域均价?”,你不用背公式,直接翻到报告的“统计口径”章节,那里有一张对比图:

  • 左图:海淀区单价均值 = 98,243 元/㎡(受 3 套 28 万/㎡ 豪宅拉高)
  • 右图:海淀区单价中位数 = 72,500 元/㎡(覆盖 65% 的真实成交区间)
    下面一行小字:“选用中位数,因二手房价格呈右偏分布,均值易受极端值干扰,中位数更能代表典型购房者支付能力。”

从那以后我每次做数据分析项目,都强制走一遍generate_audit_report.py,哪怕只是本地测试。它逼着我把每一步操作的理由写下来,而不是凭感觉敲代码。这份报告不是给机器看的,是给我自己写的“后悔药”——半年后

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询