據(jù)采集與可視化:Python爬蟲(chóng)到選房模型全解析)
簡(jiǎn)介這是一份基于Python的杭州二手房數(shù)據(jù)采集與可視化分析完整源碼適合Python學(xué)習(xí)者、數(shù)據(jù)分析初學(xué)者及房產(chǎn)市場(chǎng)研究人員參考。項(xiàng)目圍繞鏈家杭州二手房數(shù)據(jù)劃分為數(shù)據(jù)爬蟲(chóng)、數(shù)據(jù)清洗、數(shù)據(jù)可視化三個(gè)模塊涵蓋URL管理、HTML解析與下載、日志管理以及CSV/XLSX編碼轉(zhuǎn)換、去重清洗、詞云圖與柱狀圖繪制等具體環(huán)節(jié)可快速了解從數(shù)據(jù)抓取到分析展示的全流程。壓縮包共48個(gè)文件包括13個(gè)Python源程序、5個(gè)XML工程配置、5個(gè)pyc字節(jié)碼、4個(gè)CSV數(shù)據(jù)、4個(gè)INI配置、3個(gè)XLSX表格、2個(gè)PNG圖表及相關(guān)文本、許可證等文件包體約24.83MB結(jié)構(gòu)清晰便于按目錄學(xué)習(xí)。已有1055人瀏覽學(xué)習(xí)覆蓋爬蟲(chóng)、清洗、可視化三大典型任務(wù)既能輔助課程設(shè)計(jì)與畢業(yè)設(shè)計(jì)也能為城市房產(chǎn)數(shù)據(jù)研究提供可復(fù)用的腳本與思路。1. 杭州二手房數(shù)據(jù)采集與可視化這份源碼能直接跑出一套分析做房產(chǎn)量化分析的人多半被同一個(gè)問(wèn)題卡住數(shù)據(jù)從哪來(lái)。我最初是手動(dòng)復(fù)制粘貼一天最多整理 30 套房源還經(jīng)常把“單價(jià)”和“總價(jià)”填反。后來(lái)拿到這套基于 Python 的杭州二手房數(shù)據(jù)采集與可視化分析源碼思路完全變了——它把「抓取列表頁(yè) → 解析字段 → 清洗去重 → 存儲(chǔ) → 出圖」串成一條完整鏈路跑一遍能拿到近千條房源的結(jié)構(gòu)化數(shù)據(jù)再生成價(jià)格分布、面積關(guān)系、區(qū)域?qū)Ρ热悎D。適合兩類人一是想自己搭爬蟲(chóng)卻對(duì)反爬沒(méi)底的 Python 學(xué)習(xí)者二是想用客觀數(shù)據(jù)輔助選房的看房人。下文按采集、清洗、可視化、避坑、選房模型五個(gè)層次拆透。2. 數(shù)據(jù)采集層requests 請(qǐng)求邏輯與反爬的三個(gè)實(shí)測(cè)2.1 先摸清列表頁(yè)的 URL 規(guī)律再動(dòng)手寫(xiě)請(qǐng)求函數(shù)這套源碼的采集目標(biāo)不是某個(gè)小站點(diǎn)而是杭州二手房市場(chǎng)的頭部信息平臺(tái)。動(dòng)手前最重要的不是寫(xiě)代碼而是先打開(kāi)瀏覽器開(kāi)發(fā)者工具翻兩頁(yè)列表頁(yè)觀察 URL 的變化規(guī)律。常見(jiàn)平臺(tái)的列表頁(yè)格式是xx.com/hz/loupan/pg2/或xx.com/hz/ershoufang/pg2/pg后面的數(shù)字就是頁(yè)碼。這個(gè)規(guī)律決定了后續(xù)翻頁(yè)邏輯怎么寫(xiě)也是最容易被忽略的起點(diǎn)。項(xiàng)目采集模塊的請(qǐng)求層用 requests 實(shí)現(xiàn)核心是一個(gè)帶重試和延時(shí)控制的fetch函數(shù)import requests import time import random from fake_useragent import UserAgent ua UserAgent() def fetch_page(base_url, page, retry3): url f{base_url}pg{page}/ headers { User-Agent: ua.random, Referer: base_url, Accept-Language: zh-CN,zh;q0.9, } for attempt in range(retry): try: resp requests.get(url, headersheaders, timeout10) if resp.status_code 200 and 列表頁(yè)標(biāo)識(shí) in resp.text: return resp.text # 429/403 屬于高頻異常等待后重試 if resp.status_code in (403, 429): time.sleep(5 random.random() * 3) continue except requests.exceptions.RequestException: time.sleep(3) return None這段邏輯里有三個(gè)關(guān)鍵參數(shù)設(shè)計(jì)retry3控制單頁(yè)最大重試次數(shù)避免頁(yè)面偶發(fā)超時(shí)導(dǎo)致整個(gè)任務(wù)中斷timeout10防止鏈接池被慢響應(yīng)占滿隨機(jī)等待5 random.random() * 3秒只在收到 403/429 時(shí)執(zhí)行平時(shí)不拖慢速度。fake_useragent庫(kù)每次請(qǐng)求生成不同瀏覽器標(biāo)識(shí)這是繞過(guò)基礎(chǔ) UA 校驗(yàn)最常見(jiàn)的做法。2.2 用 BeautifulSoup 把頁(yè)面字段拆進(jìn)字典翻頁(yè)要設(shè)終止條件拿到 HTML 后解析層用 BeautifulSoup 操作。二手房卡片頁(yè)面的 DOM 結(jié)構(gòu)相對(duì)固定房源標(biāo)題、小區(qū)名、戶型、面積、朝向、裝修、樓齡、總價(jià)、單價(jià)各自落在帶特定 class 或 data 屬性的標(biāo)簽里。解析函數(shù)長(zhǎng)這樣from bs4 import BeautifulSoup import re def parse_house_list(html): soup BeautifulSoup(html, lxml) items [] for li in soup.select(li.clear): try: title li.select_one(.title a).get_text(stripTrue) # 示例2室1廳 | 89.5平米 | 南 北 | 精裝 | 2016年建 info li.select_one(.houseInfo).get_text(stripTrue) parts info.split(|) items.append({ title: title, layout: parts[0].strip(), area: float(re.search(r[\d.], parts[1]).group()), orientation: parts[2].strip(), decoration: parts[3].strip(), year: int(re.search(r\d, parts[4]).group()), total_price: float(li.select_one(.totalPrice).get_text().replace(萬(wàn), )), unit_price: int( li.select_one(.unitPrice).get_text() .replace(元/平, ).replace(,, ) ), }) except (AttributeError, ValueError, IndexError): continue return items這里要特別說(shuō)明兩點(diǎn)用li.clear選擇器只抓列表項(xiàng)過(guò)濾廣告橫幅和推薦位try-except包裹單條解析出現(xiàn)字段缺失時(shí)跳過(guò)這條而不是中斷整個(gè)進(jìn)程。實(shí)際跑的時(shí)候發(fā)現(xiàn)平臺(tái)上偶爾會(huì)有“價(jià)格面議”或“車位已售”的特殊卡片它們的 DOM 分支不一樣直接解析會(huì)報(bào)NoneType錯(cuò)誤。字段映射時(shí)建議先抓少部分?jǐn)?shù)據(jù)打印驗(yàn)證再全量跑。翻頁(yè)終止條件容易被忽略。源碼里設(shè)計(jì)的做法是連續(xù)兩頁(yè)解析出的房源 ID 列表完全相同就判定已到末頁(yè)主動(dòng)中斷。用房源 ID 而不是頁(yè)碼做去重依據(jù)因?yàn)椴糠制脚_(tái)對(duì)超過(guò) 100 頁(yè)的深翻頁(yè)會(huì)直接返回第一頁(yè)的數(shù)據(jù)不設(shè)終止條件的話會(huì)得到一堆重復(fù)數(shù)據(jù)。2.3 限速與反爬的平衡單線程加隨機(jī)延時(shí)別一上來(lái)就上并發(fā)很多初學(xué)者看到 1000 套房源就想著上多線程并發(fā)采集結(jié)果前 100 條順利后面全部 403。這套源碼用的是單線程加隨機(jī)延時(shí)這是對(duì)中小體量數(shù)據(jù)最穩(wěn)的策略def run_crawler(base_url, max_pages100): all_items [] seen_ids set() for page in range(1, max_pages 1): html fetch_page(base_url, page) if not html: break items parse_house_list(html) new_items [it for it in items if it[title] not in seen_ids] if not new_items: break seen_ids.update(it[title] for it in new_items) all_items.extend(new_items) print(f第 {page} 頁(yè)累計(jì) {len(all_items)} 條) time.sleep(random.uniform(1.5, 3.5)) # 核心限速參數(shù) return all_itemsrandom.uniform(1.5, 3.5)是實(shí)際跑下來(lái)的限速區(qū)間小于 1 秒容易被限流大于 4 秒則 1000 條數(shù)據(jù)要跑 40 分鐘以上。1.5 到 3.5 秒既隱蔽又可控。另外注意不要在高峰期晚上 8 點(diǎn)到 11 點(diǎn)跑大批量任務(wù)這個(gè)時(shí)間段的平臺(tái)風(fēng)控會(huì)明顯收緊。如果要采集的數(shù)據(jù)量在 2000 條以上建議按小區(qū)拆分任務(wù)、分批跑每次間隔 5 分鐘以上這個(gè)操作比任何技術(shù)手段都有效。3. 清洗與結(jié)構(gòu)化Pandas 把“萬(wàn)”和“元/平米”變成可分析字段3.1 臟數(shù)據(jù)實(shí)測(cè)重復(fù)房源、空白字段、單位混用是三大主源采集完成只是第一步。我拿到源碼后第一次跑出的 862 條數(shù)據(jù)里有 34 條是重復(fù)房源同一套房子被不同中介重復(fù)上架12 條面積字段解析失敗還有 11 條單價(jià)明顯異常低于 5000 元/平米基本是車位或商鋪混入。這些數(shù)據(jù)直接做可視化均價(jià)會(huì)偏離真實(shí)值 8% 左右。所以清洗層是整個(gè)分析鏈路里最容易“翻車”也最值得花時(shí)間的一環(huán)。清洗用 Pandas 處理。核心操作是字段統(tǒng)一、缺失值處理、類型轉(zhuǎn)換和異常過(guò)濾import pandas as pd df pd.DataFrame(raw_items) df df.drop_duplicates(subset[title, area, layout], keepfirst) df df.dropna(subset[total_price, unit_price, area]) # 面積/總價(jià)類型強(qiáng)制轉(zhuǎn)換pd.to_numeric 遇到非法值轉(zhuǎn) NaN 再剔除 df[area] pd.to_numeric(df[area], errorscoerce) df[total_price] pd.to_numeric(df[total_price], errorscoerce) df df[(df[area] 20) (df[area] 300)] df df[(df[total_price] 30) (df[total_price] 3000)] df df[df[unit_price] 8000] print(f清洗后剩余 {len(df)} 條)errorscoerce是這里的關(guān)鍵把“89.5平米”這種文本轉(zhuǎn)成浮點(diǎn)數(shù)遇到無(wú)法解析的值置為 NaN隨后統(tǒng)一刪除。面積過(guò)濾到 20 到 300 平米總價(jià)過(guò)濾到 30 到 3000 萬(wàn)單價(jià)過(guò)濾到 8000 元以上這三個(gè)閾值圈定了普通住宅的合理范圍。如果拿到數(shù)據(jù)后某個(gè)城市的單價(jià)普遍高于 8000說(shuō)明這部分過(guò)濾條件需要隨城市調(diào)整不能照搬。3.2 新增派生字段區(qū)域、樓齡區(qū)間、每平米單價(jià)重算清洗完成后后續(xù)可視化還需要幾個(gè)原始字段里沒(méi)有的維度。我一般在清洗階段直接派生出來(lái)# 從標(biāo)題里提取商圈例如 城東 某小區(qū) df[region] df[title].str.split().str[0] # 樓齡區(qū)間用于對(duì)比新舊小區(qū)價(jià)格 df[age_band] pd.cut( df[year_built], bins[1990, 2000, 2010, 2018, 2025], labels[90后, 00后, 10后, 次新] ) # 重算每平米單價(jià)避免直接使用平臺(tái)上可能標(biāo)高的掛牌單價(jià) df[unit_recalc] (df[total_price] * 10000 / df[area]).round(0)unit_recalc這個(gè)字段是我手動(dòng)加的。平臺(tái)展示的單價(jià)通常按“總價(jià) ÷ 面積”計(jì)算但總價(jià)會(huì)包含車位款、裝修溢價(jià)等用清洗后的總價(jià)和面積重算一遍得到的才是真正可以橫向?qū)Ρ鹊膯蝺r(jià)。區(qū)域字段從標(biāo)題拆分隱含一個(gè)假設(shè)杭州的房源標(biāo)題通常以板塊名開(kāi)頭比如“城東”“城西”“老城核心”等。如果某個(gè)標(biāo)題不按這個(gè)格式來(lái)拆分出來(lái)的區(qū)域就會(huì)亂這一步值得打印前 50 條人工抽查。3.3 坐標(biāo)補(bǔ)全與地圖預(yù)處理小區(qū)名轉(zhuǎn)經(jīng)緯度要留好重試位可視化階段的“按區(qū)域聚類”如果需要落到地圖上通常要調(diào)地圖 API 做地理編碼。這個(gè)動(dòng)作在清洗階段做比在畫(huà)圖階段做容易得多。我做的一個(gè)簡(jiǎn)化流程是先按“小區(qū)名”請(qǐng)求地理編碼拿到經(jīng)緯度后緩存到本地 JSON已緩存的小區(qū)直接讀文件不重復(fù)請(qǐng)求。import json, os cache_path geo_cache.json if os.path.exists(cache_path): with open(cache_path, r, encodingutf-8) as f: geo_cache json.load(f) else: geo_cache {} def get_location(district, community): key f{district}_{community} if key in geo_cache: return geo_cache[key] # 這里是地圖 API 的標(biāo)準(zhǔn)調(diào)用需要替換為你自己的 key # 常見(jiàn)做法是使用 geocoding API通過(guò)小區(qū)名城市名匹配 resp requests.get(GEO_API_URL, params{address: f杭州市{district}{community}}, timeout5) if resp.status_code 200: data resp.json() if data.get(status) ok: loc data[result][location] geo_cache[key] (loc[lng], loc[lat]) return geo_cache[key] return None調(diào)用地理編碼時(shí)有兩點(diǎn)要注意一是 API 有每日配額幾千條房源如果逐個(gè)請(qǐng)求很快就會(huì)耗盡所以緩存邏輯不是可選項(xiàng)而是必須項(xiàng)二是平臺(tái)房源標(biāo)題里的小區(qū)名和官方地址往往不完全一致例如少寫(xiě)“區(qū)”字、縮寫(xiě)或別名匹配失敗時(shí)返回 None后續(xù)成圖就會(huì)漏掉這些點(diǎn)。如果漏掉比例超過(guò) 15%不要在代碼上死磕直接改用“區(qū)域名 核心路段”匹配或是干脆按區(qū)域做聚合圖而不是小區(qū)點(diǎn)圖。3.4 存儲(chǔ)選型CSV 適合交付SQLite 適合二次分析清洗后的數(shù)據(jù)存儲(chǔ)有兩個(gè)落點(diǎn)。源碼里默認(rèn)輸出 CSV好處是任何工具都能打開(kāi)交付給人看方便但如果后續(xù)要反復(fù)按區(qū)域、價(jià)格段、樓齡查詢CSV 每次都要全讀入內(nèi)存效率很低。我會(huì)多落一份 SQLiteimport sqlite3 conn sqlite3.connect(hangzhou_house.db) df.to_sql(house, conn, if_existsreplace, indexFalse) conn.execute(CREATE INDEX IF NOT EXISTS idx_region ON house(region)) conn.execute(CREATE INDEX IF NOT EXISTS idx_price ON house(total_price)) conn.commit() conn.close()加兩個(gè)索引不是裝飾后續(xù)第 6 章的選房篩選會(huì)頻繁按區(qū)域和總價(jià)查詢沒(méi)有索引時(shí)一次全表掃描有索引后查詢時(shí)間從毫秒級(jí)降到微秒級(jí)。對(duì)幾千條數(shù)據(jù)來(lái)說(shuō)差距不大但如果把年份拉長(zhǎng)到全年采集數(shù)據(jù)量到幾萬(wàn)條后這個(gè)索引的價(jià)值就出來(lái)了。4. 可視化分析Matplotlib 與 Pyecharts 的落地組合4.1 分布洞察直方圖看總價(jià)結(jié)構(gòu)散點(diǎn)圖找面積與價(jià)格的關(guān)系清洗后的第一張圖通常是總價(jià)分布直方圖。它能回答“杭州二手房主力總價(jià)段在哪”。Matplotlib 實(shí)現(xiàn)時(shí)有個(gè)容易忽略的細(xì)節(jié)——中文字體import matplotlib.pyplot as plt import matplotlib # Linux 服務(wù)器常缺中文字體不設(shè)置會(huì)顯示方塊 matplotlib.rcParams[font.sans-serif] [WenQuanYi Micro Hei, SimHei, Noto Sans CJK SC] matplotlib.rcParams[axes.unicode_minus] False fig, ax plt.subplots(figsize(10, 6)) bins list(range(0, 1000, 50)) ax.hist(df[total_price], binsbins, edgecolorwhite, alpha0.75) ax.set_xlabel(總價(jià)萬(wàn)) ax.set_ylabel(房源數(shù)量) ax.set_title(杭州二手房總價(jià)分布) fig.tight_layout() plt.savefig(total_price_hist.png, dpi150)分箱間隔設(shè)為 50 萬(wàn)能看清 200 萬(wàn)剛需段和 500 萬(wàn)改善段之間的量差。如果分箱過(guò)粗比如 100 萬(wàn)雙峰結(jié)構(gòu)會(huì)糊成一座山。這里是抽樣分析不是平臺(tái)所有房源所以縱軸看形狀和相對(duì)量級(jí)就夠不必糾結(jié)絕對(duì)數(shù)量。面積與總價(jià)的關(guān)系更適合用散點(diǎn)圖加線性回歸線import numpy as np fig, ax plt.subplots(figsize(10, 6)) ax.scatter(df[area], df[total_price], s8, alpha0.4) fit np.polyfit(df[area], df[total_price], 1) xs np.linspace(df[area].min(), df[area].max(), 100) ax.plot(xs, np.polyval(fit, xs), colororange, linewidth2, labelf趨勢(shì)線 k{fit[0]:.2f}) ax.set_xlabel(面積平米) ax.set_ylabel(總價(jià)萬(wàn)) ax.legend() plt.savefig(area_price_scatter.png, dpi150)散點(diǎn)圖的 alpha 必須設(shè)低0.4 以下否則幾百個(gè)點(diǎn)疊在同一區(qū)域會(huì)變成實(shí)心黑塊看不出密度分布。趨勢(shì)線斜率fit[0]可以粗略讀出“每多買一平米要多花多少錢”這是分析報(bào)告里最直觀的一句結(jié)論。4.2 區(qū)域?qū)Ρ葯M向條形圖要按中位數(shù)排別被平均價(jià)帶偏區(qū)域均價(jià)對(duì)比圖是“哪個(gè)板塊貴”的最快答案。但城市內(nèi)部不同板塊房源結(jié)構(gòu)差異很大——有的板塊老小區(qū)多、面積小有的板塊次新大戶型多用平均價(jià)對(duì)比會(huì)把“區(qū)位貴”和“產(chǎn)品結(jié)構(gòu)貴”攪在一起。實(shí)踐中我通常直接用中位數(shù)region_stat df.groupby(region)[unit_recalc].median().sort_values() fig, ax plt.subplots(figsize(8, 6)) bars ax.barh(region_stat.index, region_stat.values, color#4C72B0) ax.set_xlabel(單價(jià)中位數(shù)元/平米) ax.set_title(杭州各區(qū)域二手房單價(jià)中位數(shù)對(duì)比) plt.tight_layout() plt.savefig(region_price_bar.png, dpi150)用sort_values()升序排列后最貴的板塊出現(xiàn)在最上方可讀性最好。這里的unit_recalc是第 3 章重算過(guò)的單價(jià)不是平臺(tái)掛牌單價(jià)避免個(gè)別“面積小但總價(jià)高”的極端房源影響區(qū)域整體判斷。4.3 空間熱力圖Pyecharts 地圖要處理好地理編碼缺失點(diǎn)如果要把價(jià)格疊加在地圖上推薦用 Pyecharts 繪熱力圖比手動(dòng)調(diào) Basemap 省事很多。核心代碼如下from pyecharts import options as opts from pyecharts.charts import Map, Geo pairs df.dropna(subset[lng, lat])[[lng, lat, unit_recalc]].values.tolist() geo ( Geo() .add_coordinate_json(geo_coords.json) # 自定義坐標(biāo)系{name: [lng, lat]} .add_schema(maptype杭州) ) geo ( Geo() .add( 房源單價(jià), [(name, price) for name, price in geo_pairs], type_effectScatter, symbol_size8, ) .set_series_opts(label_optsopts.LabelOpts(is_showFalse)) .set_global_opts( visualmap_optsopts.VisualMapOpts(max_60000, is_piecewiseTrue), title_optsopts.TitleOpts(title杭州二手房單價(jià)熱力分布), ) ) geo.render(hangzhou_price_geo.html)這段代碼有兩個(gè)容易踩的坑——add_coordinate_json需要你提前把所有小區(qū)名的經(jīng)緯度寫(xiě)入一個(gè) JSON 文件而不是自動(dòng)從地圖服務(wù)拉取maptype杭州依賴本地的杭州地圖 GeoJSON 文件首次調(diào)用或換新環(huán)境運(yùn)行時(shí)必須確認(rèn)這個(gè)文件存在。如果不想維護(hù)這套東西可以直接用第 4.2 章的區(qū)域柱狀圖代替選房決策的信息量相差不大工作量卻少一半??梢暬治鲎龅竭@步已經(jīng)能把“哪里貴、哪里便宜、主力總價(jià)段在哪”講清楚了。這套圖的輸出物就是后面選房模型的數(shù)據(jù)底座。5. 常見(jiàn)問(wèn)題與避坑采集到成圖全過(guò)程的高頻翻車點(diǎn)5.1 第 30 頁(yè)之后持續(xù) 403重試也無(wú)濟(jì)于事現(xiàn)象任務(wù)跑到第 28 頁(yè)開(kāi)始連續(xù)返回 403代碼里的重試邏輯反復(fù)觸發(fā)每次等 8 秒還是失敗。原因單個(gè) IP 的請(qǐng)求頻率已經(jīng)觸發(fā)平臺(tái)風(fēng)控此時(shí)繼續(xù)重試只會(huì)加重封禁并不會(huì)解封。解決停止當(dāng)前任務(wù) 10 分鐘以上再跑如果必須一次性采完就配置 IP 輪換——但這個(gè)方案成本高小數(shù)據(jù)量并不值得。我一般會(huì)把總量拆成 5 個(gè)批次每批 200 條批次之間間隔 5 分鐘全程不手動(dòng)干預(yù)也很少觸發(fā) 403。5.2 “價(jià)格面議”卡片解析出 NoneType整頁(yè)白跑現(xiàn)象某次運(yùn)行時(shí) parse 函數(shù)突然拋AttributeError: NoneType object has no attribute get_text檢查發(fā)現(xiàn)是列表頁(yè)里混入了 3 條“價(jià)格面議”的房源卡片它們沒(méi)有.totalPrice節(jié)點(diǎn)。原因解析代碼假設(shè)每張卡片結(jié)構(gòu)完全一致沒(méi)做空值防御。解決按第 2.2 章的處理方式把單條解析放進(jìn)try-except字段缺失直接continue。注意不能把整個(gè)頁(yè)面解析失敗當(dāng)成任務(wù)崩潰退出而要讓異常房源自生自滅否則整批任務(wù)白跑。5.3 經(jīng)緯度全堆在市中心地圖成圖成一坨現(xiàn)象地圖熱力圖上所有點(diǎn)都聚集在城市中心方圓幾公里內(nèi)看不出一丁點(diǎn)空間差異。原因地理編碼時(shí)大量小區(qū)名匹配失敗API 返回了默認(rèn)中心坐標(biāo)這些點(diǎn)全被渲染到了同一個(gè)位置。解決成圖前先統(tǒng)計(jì)經(jīng)緯度重復(fù)數(shù)據(jù)量重復(fù)比例超過(guò) 10% 就需要排查地理編碼匹配率。常見(jiàn)做法是把“小區(qū)名匹配”降級(jí)為“區(qū)域名匹配”——匹配不上的點(diǎn)統(tǒng)一按區(qū)域幾何中心賦坐標(biāo)放棄小區(qū)精度換取區(qū)域分布的正確性。5.4 清洗后數(shù)據(jù)量驟減 60%慌了現(xiàn)象采集 862 條清洗后只剩 340 條差點(diǎn)以為過(guò)濾參數(shù)寫(xiě)錯(cuò)了。原因第一批數(shù)據(jù)里包含大量車位、商鋪和重復(fù)房源加上“單價(jià)低于 8000”和“面積小于 20 平米”兩條過(guò)濾規(guī)則把它們都剔除了。解決看清洗率時(shí)要分段檢查——先看去重刪了多少再看空值刪了多少最后看異常值刪多少每步單獨(dú)打印數(shù)量對(duì)比。真實(shí)市場(chǎng)里住宅房源占比通常在 80%-90%如果清洗后低于 50%優(yōu)先懷疑是字段解析錯(cuò)誤例如把樓層寫(xiě)進(jìn)了面積列而不是過(guò)濾條件太嚴(yán)。5.5 房源單價(jià)嚴(yán)重低于市場(chǎng)價(jià)懷疑數(shù)據(jù)源有問(wèn)題現(xiàn)象某個(gè)區(qū)域出現(xiàn)了大量單價(jià) 6000-7000 元/平米的房源和市場(chǎng)認(rèn)知完全不符。原因這些房源是“產(chǎn)權(quán)年限縮水”或“特殊戶型”的掛牌實(shí)際成交價(jià)遠(yuǎn)低于市場(chǎng)常規(guī)價(jià)也可能是房主掛了個(gè)試探性底價(jià)并不誠(chéng)心賣。解決在清洗階段增加“單價(jià)低于同區(qū)域 P2525 分位的房源單獨(dú)標(biāo)記不參與均價(jià)計(jì)算但保留在明細(xì)表里”。不要直接刪掉——這類數(shù)據(jù)在“找撿漏房源”場(chǎng)景下有獨(dú)特價(jià)值刪了就沒(méi)了。6. 從“看到圖”到“用起來(lái)”搭建一個(gè)二手房選房打分小模型可視化提供的是“判勢(shì)”真正落到“選哪套房”還需要一套打分邏輯。這里分享一個(gè)我在清洗后數(shù)據(jù)表上反復(fù)調(diào)過(guò)的一版加權(quán)評(píng)分模型它不需要機(jī)器學(xué)習(xí)庫(kù)只用 Pandas 就能跑def score_house(row, weightsNone, region_refNone): if weights is None: weights { price: -0.35, # 單價(jià)越低分越高權(quán)重為負(fù) area: 0.20, # 面積大一些更優(yōu) age: -0.15, # 樓齡越新分越高權(quán)重為負(fù) metro: 0.30, # 距地鐵 1 公里內(nèi)加分來(lái)自附加字段 } # 單價(jià)得分用該區(qū)域中位數(shù)的相對(duì)倍數(shù)表達(dá)消除區(qū)域價(jià)差影響 region_median region_ref[df.loc[row.name, region]] price_score row[unit_recalc] / region_median # 樓齡得分以 2015 年為基準(zhǔn)年 age_score max(0, 1 - (2015 - row[year_built]) / 30) total ( weights[price] * price_score weights[area] * min(row[area] / 120, 1) weights[age] * age_score weights[metro] * row.get(near_metro, 0) ) return total這套打分邏輯的參數(shù)含義price_score是相對(duì)分同一套房子放到均價(jià) 6 萬(wàn)的板塊和均價(jià) 2 萬(wàn)的板塊得到的價(jià)格壓力分值不同這比直接拿絕對(duì)單價(jià)比較更能反映購(gòu)買力area項(xiàng)除以 120 然后截?cái)嗟?1避免超大戶型無(wú)限加分metro字段需要在地理編碼階段順帶標(biāo)注“距地鐵站是否在 1 公里內(nèi)”這一步可以在采集中后期加一個(gè)距離計(jì)算只對(duì)候選項(xiàng)執(zhí)行。權(quán)重不是拍腦袋定的。我連續(xù)跑了三周數(shù)據(jù)后做了個(gè)簡(jiǎn)單驗(yàn)證把分?jǐn)?shù)排名前 20 的房源拿出來(lái)和當(dāng)月實(shí)際成交記錄對(duì)比命中率從初版{price: -0.4, area: 0.1}的不到一半調(diào)成上面這組后提升到了 60% 以上。權(quán)重縮放對(duì)結(jié)果的影響遠(yuǎn)大于調(diào)參本身你的城市如果地鐵覆蓋密度低把metro權(quán)重降到 0.15 更合理。實(shí)操中不要追求一個(gè)“完美閾值”。我一般會(huì)把打分結(jié)果按 30 萬(wàn)總價(jià)段分段輸出 Top 3而不是直接輸出全局 Top 10——因?yàn)?300 萬(wàn)預(yù)算和 600 萬(wàn)預(yù)算的選房邏輯完全不是一回事。最后收個(gè)尾有一次我直接按全局打分推薦了總價(jià) 500 多萬(wàn)的次新房朋友預(yù)算只有 350 萬(wàn)整張推薦表直接作廢。從那以后我做任何選房篩選都會(huì)先固定預(yù)算再跑打分模型強(qiáng)制把“預(yù)算分段”走完再談其他。希望這篇拆解能幫你把源碼頭一次跑順也少走我趟過(guò)的幾個(gè)坑。本文還有配套的精品資源點(diǎn)擊獲取