數(shù)據(jù)分析與Python聚類(lèi)建模實(shí)戰(zhàn))
簡(jiǎn)介這是一套圍繞學(xué)生校園消費(fèi)行為分析題的Python建模項(xiàng)目專(zhuān)為高校期末大作業(yè)、課程設(shè)計(jì)等場(chǎng)景打造。資源包共6個(gè)文件包含5個(gè)Python腳本和1個(gè)原始數(shù)據(jù)壓縮包腳本按照不同任務(wù)模塊編寫(xiě)如基本數(shù)據(jù)探查、消費(fèi)特征提取、關(guān)聯(lián)分析或預(yù)測(cè)建模等下載解壓后無(wú)需額外修改即可運(yùn)行并可直接作為答辯材料的基礎(chǔ)工程。整個(gè)zip體積僅4.89MB目錄層級(jí)簡(jiǎn)潔便于按任務(wù)順序理解代碼邏輯。目前已有618人學(xué)習(xí)下載是同類(lèi)作業(yè)中熱度較高的參考模板。讀者可獲得一整套可復(fù)用的分析流程框架、數(shù)據(jù)預(yù)處理與建模代碼、任務(wù)拆解思路及結(jié)果展示方式尤其適合希望快速搭建項(xiàng)目骨架、降低排錯(cuò)成本的同學(xué)。1. 校園消費(fèi)行為分析到底在做什么先把期末大作業(yè)的驗(yàn)收點(diǎn)擺出來(lái)晚上十點(diǎn)半的食堂刷卡機(jī)前排著剛下晚課的學(xué)生一張一卡通流水背后是幾十萬(wàn)條可以建模的數(shù)據(jù)。這份期末大作業(yè)最容易踩的誤區(qū)是以為把數(shù)據(jù)丟給 sklearn跑出一個(gè)聚類(lèi)結(jié)果就完事。真實(shí)的驗(yàn)收點(diǎn)有三個(gè)——能不能把原始流水清洗成一張可分析的特征表能不能用合適的建模方法分出有業(yè)務(wù)含義的人群以及能不能在答辯現(xiàn)場(chǎng)十分鐘內(nèi)把「數(shù)據(jù)—特征—模型—結(jié)論」這條鏈路講清楚。這里的 Python 建模落點(diǎn)不是比拼準(zhǔn)確率而是比拼可解釋性適合正在做數(shù)據(jù)分析課設(shè)、數(shù)學(xué)建模入門(mén)、或者想認(rèn)真完成期末大作業(yè)的同學(xué)。2. 數(shù)據(jù)清洗與特征工程把一卡通流水變成能建模的二維表2.1 拿到原始流水后先別急著建模先檢查字段和樣本校園一卡通消費(fèi)流水在不同學(xué)校的導(dǎo)出格式略有差異但核心字段基本穩(wěn)定卡號(hào)或?qū)W號(hào)、交易時(shí)間、交易金額、商戶名稱、商戶類(lèi)型、收支標(biāo)志。最好的習(xí)慣是先用 Excel 或 pandas 打開(kāi)前 100 行肉眼看一遍數(shù)據(jù)長(zhǎng)得什么樣而不是直接開(kāi)始寫(xiě)聚合代碼。曾經(jīng)有人拿到數(shù)據(jù)后直接按卡號(hào)分組求均值結(jié)果發(fā)現(xiàn)「人均消費(fèi)」只有 0.15 元一查才知道有大量刷卡充值記錄混在消費(fèi)記錄里充值金額都是整百把均值拉低了。我一般會(huì)先寫(xiě)一段極簡(jiǎn)的檢查代碼輸出數(shù)據(jù)的行數(shù)、唯一卡數(shù)、時(shí)間范圍和金額分布這幾個(gè)指標(biāo)能快速暴露數(shù)據(jù)集的真實(shí)規(guī)模和分析難度。重點(diǎn)是看三段信息交易時(shí)間是不是統(tǒng)一格式、金額列有沒(méi)有負(fù)數(shù)、同一張卡同一秒同一商戶是否存在重復(fù)記錄。這三類(lèi)問(wèn)題如果不處理后續(xù)所有統(tǒng)計(jì)口徑都會(huì)偏而且偏得悄無(wú)聲息。import pandas as pd df pd.read_csv(card_flow.csv, parse_dates[trade_time]) print(總記錄數(shù):, len(df)) print(唯一卡號(hào)數(shù):, df[card_no].nunique()) print(時(shí)間范圍:, df[trade_time].min(), -, df[trade_time].max()) print(金額描述:) print(df[amount].describe()) print(負(fù)數(shù)金額條數(shù):, (df[amount] 0).sum()) print(重復(fù)記錄條數(shù):, df.duplicated().sum())這段代碼先讀數(shù)據(jù)再把交易時(shí)間解析為 datetime 類(lèi)型。nunique 用于數(shù)出去重后的卡號(hào)數(shù)量duplicated 用來(lái)判斷全字段重復(fù)的行有多少。輸出結(jié)果之后你會(huì)對(duì)本次建模的數(shù)據(jù)形態(tài)有個(gè)整體印象數(shù)據(jù)量是幾萬(wàn)還是幾十萬(wàn)時(shí)間跨度是三個(gè)月還是一整年負(fù)數(shù)是偶爾幾筆還是大面積存在。先看全貌再動(dòng)手避免后續(xù)在錯(cuò)誤假設(shè)上堆代碼。2.2 處理退款負(fù)數(shù)、充值記錄和重復(fù)刷卡常見(jiàn)的坑有兩個(gè)方向。第一個(gè)方向是負(fù)金額食堂窗口操作失誤后的退款、超市退貨、洗衣房余額退回都會(huì)造成負(fù)數(shù)交易。如果分析目標(biāo)是「消費(fèi)行為」我建議把負(fù)數(shù)交易單獨(dú)拎出來(lái)只在退款分析里用不混進(jìn)消費(fèi)頻率和消費(fèi)金額的計(jì)算。第二個(gè)方向是充值記錄一卡通充值本質(zhì)是轉(zhuǎn)賬不是消費(fèi)通常交易類(lèi)型字段里會(huì)有「充值」「消費(fèi)」的標(biāo)記沒(méi)有標(biāo)記的話可以用商戶類(lèi)型或者金額規(guī)律來(lái)過(guò)濾。刪除或標(biāo)記這些記錄后再處理重復(fù)刷卡。有些食堂閘機(jī)數(shù)據(jù)會(huì)同時(shí)寫(xiě)入兩條相同記錄時(shí)間戳精確到秒且金額一樣這種情況直接用 drop_duplicates 刪掉。但要注意如果刷完卡發(fā)現(xiàn)有退款退款記錄和原消費(fèi)記錄成對(duì)出現(xiàn)此時(shí)不能簡(jiǎn)單按全字段去重否則會(huì)把正常的一進(jìn)一出也刪掉。處理時(shí)我通常只對(duì)「卡號(hào) 商戶 時(shí)間 金額」四字段完全相同的行去重保留第一次出現(xiàn)。df df[df[amount] 0].copy() # 先排除退款和充值只看正向消費(fèi) df_clean df.drop_duplicates( subset[card_no, merchant, trade_time, amount], keepfirst ) print(清洗后記錄數(shù):, len(df_clean))這里的邏輯是先假設(shè)金額大于 0 都是消費(fèi)需要你確認(rèn)自己的數(shù)據(jù)里有沒(méi)有負(fù)的消費(fèi)、有沒(méi)有正數(shù)充值。drop_duplicates 的 subset 參數(shù)非常關(guān)鍵它指定了「按哪些列判斷重復(fù)」避免把同一時(shí)間在同一個(gè)商戶買(mǎi)兩筆相同金額商品的正常行為誤刪。keep 參數(shù)用 first 保留第一條在時(shí)間序列場(chǎng)景下等于默認(rèn)選擇先發(fā)生的記錄。2.3 時(shí)間特征拆分星期、小時(shí)、早晚高峰一卡通流水里的交易時(shí)間是最有建模價(jià)值的字段它決定了你能不能在報(bào)告里畫(huà)「一天消費(fèi)曲線」和「一周消費(fèi)熱力圖」。清洗完成后我會(huì)把時(shí)間拆成三個(gè)新列小時(shí)、星期、是否為工作日。小時(shí)用來(lái)判斷早餐、午餐、晚餐、夜宵的高峰分布星期用來(lái)分析周末和上學(xué)日的差異工作日標(biāo)記則幫助后續(xù)分群時(shí)識(shí)別作息規(guī)律。dt df_clean[trade_time] df_clean[hour] dt.dt.hour df_clean[weekday] dt.dt.weekday # 0周一, 6周日 df_clean[is_workday] df_clean[weekday].apply(lambda x: 1 if x 5 else 0)dt.hour 和 dt.weekday 是 pandas 的 datetime 訪問(wèn)器效率高且不容易寫(xiě)錯(cuò)。weekday 范圍是 0 到 60 代表周一6 代表周日。is_workday 是自定義規(guī)則默認(rèn)周一至周五算工作日。如果你的學(xué)校周六還有課或者周五下午就放假這個(gè)標(biāo)記要按自己學(xué)校的校歷調(diào)整不能照抄默認(rèn)值。2.4 構(gòu)建「以人為單位」的消費(fèi)特征表建模時(shí)最常用的主體是「人」也就是每張卡對(duì)應(yīng)一行特征。把流水按卡號(hào)分組統(tǒng)計(jì)消費(fèi)總金額、總次數(shù)、平均金額、消費(fèi)活躍天數(shù)、高頻時(shí)段等字段最終得到一張寬表。這張表就是后續(xù) KMeans、DBSCAN 的輸入。daily df_clean.groupby([card_no]).agg( total_amount(amount, sum), total_count(amount, count), avg_amount(amount, mean), active_days(trade_time, nunique), avg_hour(hour, mean), ) daily[count_per_active_day] daily[total_count] / daily[active_days] print(daily.head())groupby 后面的 agg 是 pandas 推薦的聚合寫(xiě)法每一行指定新列名、原始列和聚合函數(shù)。total_amount 是總消費(fèi)額total_count 是總刷卡次數(shù)active_days 是去重后的活躍天數(shù)用 nunique 統(tǒng)計(jì)。avg_hour 是消費(fèi)時(shí)間的均值它有一個(gè)隱患跨午夜的作息會(huì)被平均到奇怪的位置比如晚上 23 點(diǎn)和凌晨 1 點(diǎn)的平均會(huì)得到 12 點(diǎn)。這種情況我用「圓形均值」處理或者干脆把它換成前三個(gè)固定時(shí)段的消費(fèi)占比。2.5 衍生特征三餐占比、夜間消費(fèi)比例、消費(fèi)熵基礎(chǔ)特征只能描述消費(fèi)多少不能描述消費(fèi)習(xí)慣。期末答辯時(shí)最能打動(dòng)老師的是「這個(gè)簇的人早餐比例明顯高」而不是「這個(gè)簇平均消費(fèi) 12.3 元」。因此我額外構(gòu)建三類(lèi)衍生特征。第一類(lèi)是三餐和夜宵占比用小時(shí)區(qū)間切分。第二類(lèi)是夜間消費(fèi)比例把 23 點(diǎn)到次日 5 點(diǎn)定義為夜宵時(shí)段統(tǒng)計(jì)夜宵金額占總金額的比例。第三類(lèi)是消費(fèi)熵衡量一個(gè)人刷卡金額的分散程度——如果一個(gè)人每頓都是 10 元整熵很低如果忽而 3 元忽而 40 元熵很高說(shuō)明飲食結(jié)構(gòu)波動(dòng)大。def meal_type(hour): if 6 hour 10: return breakfast elif 10 hour 15: return lunch elif 15 hour 20: return dinner elif 20 hour 24 or hour 5: return night else: return other df_clean[meal] df_clean[hour].apply(meal_type) meal_cross pd.crosstab(df_clean[card_no], df_clean[meal], normalizeindex) def entropy(series): p series[series 0] return -(p * np.log(p)).sum()import numpy as np from scipy.stats import entropy as scipy_entropy def cal_entropy_by_group(group): val_counts group[amount].value_counts(normalizeTrue) return scipy_entropy(val_counts)第一個(gè)函數(shù)按小時(shí)區(qū)間把每筆交易歸入早餐、午餐、晚餐或夜宵。pd.crosstab 配合 normalizeindex 得到每張卡在不同餐段的消費(fèi)次數(shù)占比。第二個(gè)函數(shù)計(jì)算金額分布的熵我直接調(diào)用 scipy.stats.entropy它接受概率分布數(shù)組并返回非負(fù)熵值。熵越大代表不同金額的分布越分散越小代表消費(fèi)金額集中在少數(shù)幾個(gè)固定檔位比如總是刷 8 元和 12 元套餐。2.6 標(biāo)準(zhǔn)化為什么用 StandardScaler不用 MinMaxScaler特征表建好后量綱差異非常明顯消費(fèi)總金額可能是幾千早餐占比只有 0.1。如果直接把原始值喂給聚類(lèi)算法金額會(huì)主導(dǎo)距離計(jì)算。常見(jiàn)做法是做標(biāo)準(zhǔn)化。StandardScaler 會(huì)把每個(gè)特征變成均值為 0、方差為 1 的分布適合 KMeans 和 DBSCAN 這種基于距離的模型。MinMaxScaler 會(huì)把數(shù)據(jù)壓縮到 0~1 區(qū)間問(wèn)題在于它很容易被極端值拉偏而校園消費(fèi)里炒飯、夜宵或校外大額支出很容易產(chǎn)生極端值。from sklearn.preprocessing import StandardScaler feature_cols [total_amount, total_count, avg_amount, active_days, count_per_active_day, breakfast_ratio, dinner_ratio, night_ratio, entropy] X daily[feature_cols].fillna(0) scaler StandardScaler() X_scaled scaler.fit_transform(X)fillna(0) 是因?yàn)橛行┛赡軟](méi)有夜宵記錄占比為空。fit_transform 在這段代碼里直接完成了兩步操作fit 計(jì)算均值和標(biāo)準(zhǔn)差transform 應(yīng)用標(biāo)準(zhǔn)化。等到建模階段你只需把 X_scaled 傳入聚類(lèi)算法。注意保留原始特征表 daily后面畫(huà)圖、打標(biāo)簽、寫(xiě)報(bào)告都需要它而不是直接使用標(biāo)準(zhǔn)化后的矩陣。3. 建模方案KMeans、DBSCAN、孤立森林怎么選3.1 先定目標(biāo)再選模型不要先跑代碼很多期末作業(yè)翻車(chē)的原因是拿到數(shù)據(jù)就開(kāi)始找聚類(lèi)代碼跑完卻講不清每一類(lèi)代表什么。建模之前必須回答一個(gè)問(wèn)題這次分析到底要得出什么結(jié)論如果你的題目是「分析學(xué)生消費(fèi)行為」最合理的目標(biāo)是分群畫(huà)像把學(xué)生分成規(guī)律干飯型、高消費(fèi)夜宵型、低消費(fèi)節(jié)儉型等若干類(lèi)。如果你的題目是「識(shí)別異常消費(fèi)」那應(yīng)該用異常檢測(cè)模型而不是聚類(lèi)。如果你的題目是「根據(jù)前兩個(gè)月消費(fèi)預(yù)測(cè)后一個(gè)月」那就進(jìn)入回歸或時(shí)間序列的范疇不是同一套流程。在這份校園消費(fèi)分析作業(yè)里最常見(jiàn)也最容易講清楚的是「聚類(lèi) 畫(huà)像」。KMeans 能分出消費(fèi)水平不同的群體每個(gè)群體的特征中心就是畫(huà)像。DBSCAN 則適合發(fā)現(xiàn)不規(guī)則形狀的群體還有機(jī)會(huì)把「每天只刷一頓飯」的極端群體標(biāo)記為噪聲點(diǎn)。孤立森林不是聚類(lèi)它的用途是識(shí)別偏離正常消費(fèi)模式的人。建議把它們放在同一份作業(yè)里分別回答「共性分群」和「異常個(gè)體」兩個(gè)問(wèn)題報(bào)告結(jié)構(gòu)會(huì)很完整。3.2 聚類(lèi)數(shù)怎么定肘部法則和輪廓系數(shù)KMeans 需要手動(dòng)指定簇?cái)?shù) k這個(gè) k 不能拍腦袋。業(yè)界用的兩個(gè)指標(biāo)分別是 SSE 肘部法則和輪廓系數(shù)。SSE 是簇內(nèi)樣本到中心點(diǎn)的距離平方和k 越大 SSE 越小但下降到某個(gè)點(diǎn)后收益驟減那個(gè)拐點(diǎn)就是肘部。輪廓系數(shù)則衡量每個(gè)樣本和自身簇的相似度同類(lèi)別緊湊、不同類(lèi)別分離時(shí)系數(shù)高范圍是 -1 到 1越接近 1 越好。from sklearn.cluster import KMeans from sklearn.metrics import silhouette_score sse [] sil [] k_range range(2, 9) for k in k_range: km KMeans(n_clustersk, random_state42, n_init10) labels km.fit_predict(X_scaled) sse.append(km.inertia_) sil.append(silhouette_score(X_scaled, labels)) for k, s, si in zip(k_range, sse, sil): print(fk{k}, SSE{s:.1f}, silhouette{si:.4f})KMeans 的計(jì)算受初始中心點(diǎn)影響所以每一次迭代都指定 random_state42保證結(jié)果可復(fù)現(xiàn)。n_init10 表示用 10 組不同的初始中心運(yùn)行算法最后返回 SSE 最小的一組避免隨機(jī)性帶來(lái)翻車(chē)。silhouette_score 在 X_scaled 上計(jì)算取值越大說(shuō)明簇與簇之間分離越好。我的習(xí)慣是優(yōu)先看肘部拐點(diǎn)再看 silhouette 是否超過(guò) 0.25如果超過(guò) 0.5 反而要警惕特征是否過(guò)于簡(jiǎn)單比如只靠金額一列劈成了兩半。3.3 用 KMeans 跑通最小建模流程并打上人群標(biāo)簽確定 k 之后就可以跑最終的聚類(lèi)。以 k4 為例把聚類(lèi)結(jié)果合并回原始特征表就能看到每一類(lèi)人的消費(fèi)規(guī)律。這里還要注意一點(diǎn)KMeans 聚類(lèi)是按距離切分的它不知道食堂、超市、奶茶店的業(yè)務(wù)含義打完標(biāo)簽后必須回看特征均值給每個(gè)簇起一個(gè)人類(lèi)看得懂的名字。k 4 km KMeans(n_clustersk, random_state42, n_init10) cluster_labels km.fit_predict(X_scaled) daily[cluster] cluster_labels cluster_profile daily.groupby(cluster)[feature_cols].mean() print(cluster_profile.round(2))fit_predict 一次性完成訓(xùn)練和預(yù)測(cè)返回每個(gè)樣本的簇編號(hào)。groupby 后取均值得到每類(lèi)人的平均消費(fèi)金額、平均活躍天數(shù)、早餐占比等??吹綌?shù)據(jù)后再命名cluster 0 如果平均金額高、活躍天數(shù)多、夜宵占比高就命名為「活躍夜宵型」cluster 1 如果總金額低、早餐占比高、頻次少就命名為「規(guī)律早餐型」。命名是報(bào)告的點(diǎn)睛之筆不要直接寫(xiě)「群組 0」。3.4 DBSCAN 的取舍噪點(diǎn)本身就是一種結(jié)果KMeans 會(huì)把每個(gè)樣本都分到一個(gè)簇哪怕這個(gè)樣本和其他任何樣本都不像。DBSCAN 則會(huì)把這些樣本直接標(biāo)記為噪點(diǎn)也就是標(biāo)簽 -1。在校園消費(fèi)場(chǎng)景里這很有價(jià)值低活躍、偶發(fā)消費(fèi)、作息不規(guī)律的學(xué)生會(huì)在 DBSCAN 中被暴露出來(lái)。不過(guò) DBSCAN 對(duì)參數(shù)非常敏感兩個(gè)參數(shù) eps 描述鄰域半徑min_samples 描述成為核心對(duì)象所需的最少樣本數(shù)。from sklearn.cluster import DBSCAN db DBSCAN(eps0.8, min_samples10) db_labels db.fit_predict(X_scaled) n_clusters_db len(set(db_labels)) - (1 if -1 in db_labels else 0) print(DBSCAN 發(fā)現(xiàn)簇?cái)?shù):, n_clusters_db) print(噪點(diǎn)數(shù)量:, (db_labels -1).sum()) daily[dbscan_label] db_labelseps 表示如果某個(gè)樣本的鄰域內(nèi)至少有 min_samples 個(gè)樣本它才會(huì)被當(dāng)作核心點(diǎn)。eps 設(shè)太小會(huì)把群體拆得零碎設(shè)太大又把所有人都劃進(jìn)一個(gè)大簇。對(duì)于標(biāo)準(zhǔn)化后的數(shù)據(jù)我一般把 eps 設(shè)在 0.5 到 1.2 之間min_samples 設(shè)在 10 到 20。這里的技巧是先跑一次看噪點(diǎn)比例是否在 5%~15%噪音比例太高說(shuō)明參數(shù)過(guò)緊可以立刻調(diào)整。3.5 用孤立森林找異常消費(fèi)不需要任何標(biāo)簽孤立森林是「無(wú)監(jiān)督異常檢測(cè)」算法它不依賴標(biāo)簽而是隨機(jī)切分特征空間異常點(diǎn)因?yàn)榉植枷∈柚恍枰苌俚那蟹执螖?shù)就能被孤立出來(lái)。校園消費(fèi)場(chǎng)景中它可以識(shí)別刷卡金額異常大、消費(fèi)頻率異常低或夜間消費(fèi)異常多的人。這里要特別強(qiáng)調(diào)異常不等于壞人可能是校外人員借卡、訪客臨時(shí)消費(fèi)也可能是有學(xué)生因病長(zhǎng)時(shí)間不在校。from sklearn.ensemble import IsolationForest iso IsolationForest(contamination0.02, random_state42) anomaly_labels iso.fit_predict(X_scaled) daily[anomaly] anomaly_labels print(daily[anomaly].value_counts())contamination 參數(shù)表示你預(yù)估的數(shù)據(jù)集中異常值比例這里設(shè) 0.02 也就是假設(shè) 2% 的人消費(fèi)模式異常。fit_predict 返回 1 表示正常-1 表示異常注意方向和聚類(lèi)標(biāo)簽正好相反寫(xiě)報(bào)告前容易看反。IsolationForest 的核心優(yōu)點(diǎn)是快幾十萬(wàn)行數(shù)據(jù)也只需要幾秒鐘。缺點(diǎn)是結(jié)果不好解釋它不會(huì)告訴你為什么異常需要回到原始特征表逐個(gè)看異常樣本的數(shù)值。3.6 驗(yàn)證聚類(lèi)不是「隨機(jī)碰運(yùn)氣」聚類(lèi)模型的驗(yàn)證是期末答辯時(shí)最容易被人追問(wèn)的環(huán)節(jié)。老師說(shuō)「你這個(gè)聚類(lèi)結(jié)果會(huì)不會(huì)換一組初始值就完全變了」如果你答不上來(lái)前后就會(huì)陷入被動(dòng)。我通常做兩個(gè)驗(yàn)證第一個(gè)是穩(wěn)定性驗(yàn)證把 random_state 改成其他值多次跑 KMeans比較每個(gè)簇的人數(shù)比例是否保持穩(wěn)定第二個(gè)是樣本驗(yàn)證隨機(jī)抽取 80% 的樣本重新聚類(lèi)再把剩余 20% 預(yù)測(cè)到最近的簇中心觀察整體分布是否一致。from sklearn.model_selection import train_test_split X_train, X_test train_test_split(X_scaled, test_size0.2, random_state7) km_temp KMeans(n_clusters4, random_state0, n_init10).fit(X_train) train_labels km_temp.predict(X_train) test_labels km_temp.predict(X_test) print(pd.Series(train_labels).value_counts(normalizeTrue).sort_index()) print(pd.Series(test_labels).value_counts(normalizeTrue).sort_index())這里用 train_test_split 把樣本切分成 80% 訓(xùn)練、20% 測(cè)試。predict 方法會(huì)把新樣本分配到距離最近的中心。如果兩次的人群占比差距在 3% 以內(nèi)說(shuō)明結(jié)果不是偶然。如果差距很大優(yōu)先檢查特征表是否被少量極端值主導(dǎo)。4. 期末大作業(yè)避坑指南5 條踩過(guò)的數(shù)據(jù)與建???.1 時(shí)間字段解析失敗按天聚合結(jié)果全是 NaN現(xiàn)象用 pd.to_datetime 轉(zhuǎn)換交易時(shí)間后發(fā)現(xiàn)小時(shí)列全部變成 NaN按天聚合后圖像一片空白。 原因原始數(shù)據(jù)里時(shí)間格式不統(tǒng)一有的行是「2025-03-12 12:30:45」有的行是「2025/03/12 12:30」甚至還有「2025年3月12日」。to_datetime 無(wú)法自動(dòng)識(shí)別所有格式解析失敗的行變成了空值。 解決先用 errorscoerce 強(qiáng)制轉(zhuǎn)換標(biāo)記解析失敗的行再檢查失敗樣本的時(shí)間格式寫(xiě)一個(gè)自定義解析函數(shù)補(bǔ)齊。最簡(jiǎn)單的方式是讓 to_datetime 的 format 參數(shù)明確指定格式。不要相信 Excel 導(dǎo)出的時(shí)間是「標(biāo)準(zhǔn)格式」必須有這一層檢查。4.2 退款負(fù)數(shù)拉低均值消費(fèi)金額變成負(fù)的現(xiàn)象早餐平均消費(fèi)是 -3.2 元食堂搶了學(xué)生的錢(qián)。 原因退款記錄被當(dāng)作消費(fèi)納入聚合負(fù)金額抵消了正金額。 解決把金額列拆成正負(fù)兩套邏輯。如果只做消費(fèi)分析直接保留 amount 0需要研究退款時(shí)把負(fù)數(shù)的絕對(duì)值拿出來(lái)單獨(dú)分析。注意充值記錄通常是正數(shù)如果不按交易類(lèi)型區(qū)分也會(huì)混進(jìn)消費(fèi)金額。建議先看交易類(lèi)型字段的值分布再?zèng)Q定過(guò)濾條件。4.3 去重把正常購(gòu)買(mǎi)記錄誤刪了現(xiàn)象同一張卡在同一家店買(mǎi)了兩次相同金額的飲料時(shí)間相差 1 秒結(jié)果被全字段去重刪除。 原因沒(méi)有指定去重列直接用 drop_duplicates() 把「卡號(hào) 商戶 時(shí)間 金額」四列完全相同的記錄當(dāng)成了重復(fù)數(shù)據(jù)。其實(shí)一秒鐘刷兩次卡是合理的人類(lèi)行為比如一次刷卡本人消費(fèi)一次幫同學(xué)帶飯。 解決只對(duì)高頻重復(fù)類(lèi)型做去重判斷標(biāo)準(zhǔn)是「同一秒內(nèi)同一商戶同一金額且同一卡號(hào)」。如果數(shù)據(jù)里有交易流水號(hào)字段優(yōu)先用流水號(hào)去重。沒(méi)有流水號(hào)時(shí)subset 要加上三個(gè)以上維度避免誤刪。4.4 卡號(hào)字符串混進(jìn)特征聚類(lèi)結(jié)果被「身份證號(hào)」支配現(xiàn)象聚類(lèi)出的四個(gè)簇分別對(duì)應(yīng)卡號(hào)開(kāi)頭四個(gè)數(shù)字段業(yè)務(wù)上毫無(wú)意義。 原因把 card_no 或?qū)W號(hào)列當(dāng)成了數(shù)值特征標(biāo)準(zhǔn)化后進(jìn)入聚類(lèi)模型。KMeans 基于歐氏距離卡號(hào)中任何一點(diǎn)數(shù)字差異都會(huì)被當(dāng)成距離導(dǎo)致聚類(lèi)完全偏離消費(fèi)行為。 解決第一步是刪除卡號(hào)、學(xué)號(hào)、姓名等身份標(biāo)識(shí)列第二步檢查特征表里是否有其他高基數(shù)列比如商戶名稱。需要用獨(dú)熱編碼處理商戶偏好字段或者直接不把商戶名稱作為聚類(lèi)輸入改用它構(gòu)造「食堂消費(fèi)占比」這類(lèi)聚合特征。4.5 輪廓系數(shù)很高但人群畫(huà)像完全解釋不通現(xiàn)象k2 時(shí)輪廓系數(shù)高達(dá) 0.6看起來(lái)聚類(lèi)效果很好但打開(kāi)畫(huà)像發(fā)現(xiàn)一群人全是總消費(fèi)金額極高的夜宵大戶另一群人全是幾乎不刷卡的低頻用戶中間人群被強(qiáng)行切成了兩半。 原因特征中存在極端值KMeans 對(duì)這種尖峰分布極其敏感少數(shù)高消費(fèi)樣本把整個(gè)簇中心拉走了。輪廓系數(shù)衡量的是幾何分離程度不判斷業(yè)務(wù)合理性。 解決先對(duì)特征做裁剪比如 95% 分位數(shù)截?cái)鄻O端消費(fèi)金額或者使用分位數(shù)變換替代標(biāo)準(zhǔn)化。更重要的是必須回看每個(gè)簇的人數(shù)比例。如果人數(shù)最少那一類(lèi)只占 2%它更可能是噪聲而不是人群此時(shí)通過(guò) W 檢驗(yàn)調(diào)整 k 或改用 DBSCAN 處理離群點(diǎn)。5. 可視化與結(jié)果報(bào)告讓期末答辯在 10 分鐘內(nèi)有底氣5.1 三種能在一頁(yè) PPT 上講清楚的圖數(shù)據(jù)建模的成果需要用圖形呈現(xiàn)評(píng)卷老師沒(méi)有耐心一行行看 DataFrame。我通常畫(huà)三張圖第一張是所有學(xué)生的分時(shí)段消費(fèi)折線圖展示早中晚高峰第二張是周消費(fèi)熱力圖橫軸星期、縱軸小時(shí)用顏色深淺表示消費(fèi)次數(shù)第三張是每個(gè)聚類(lèi)小組的平均日消費(fèi)金額分布圖用來(lái)突出分群差異。import matplotlib.pyplot as plt import seaborn as sns plt.rcParams[font.sans-serif] [SimHei, PingFang SC, Microsoft YaHei] plt.rcParams[axes.unicode_minus] False fig, axs plt.subplots(1, 3, figsize(16, 4)) hourly_count df_clean.groupby(hour).size() axs[0].plot(hourly_count.index, hourly_count.values, markero) axs[0].set_title(分時(shí)段消費(fèi)次數(shù)) pivot df_clean.pivot_table(indexhour, columnsweekday, valuesamount, aggfunccount).fillna(0) sns.heatmap(pivot, axaxs[1], cmapYlOrRd) axs[1].set_title(每周消費(fèi)規(guī)律熱力圖) daily_group daily.groupby(cluster)[total_count].mean() daily_group.plot(kindbar, axaxs[2], color[#4C72B0, #55A868, #C44E52, #8172B2]) axs[2].set_title(各簇平均消費(fèi)次數(shù)) plt.tight_layout()先設(shè)置中文字體否則圖表里的中文會(huì)變成方塊這是 matplotlib 默認(rèn)字體導(dǎo)致的。分時(shí)段曲線把「早中晚三個(gè)高峰」一句話講完。熱力圖的 x 軸是周一到周日y 軸是 0 到 23 點(diǎn)能直觀看到周末上午消費(fèi)明顯后移。第三張柱狀圖用于展示各簇畫(huà)像差異柱子的高度對(duì)比就能支撐「人群可分」的結(jié)論。5.2 用表格把人群畫(huà)像輸出成報(bào)告素材圖像適合口頭表達(dá)書(shū)面報(bào)告里還需要一張精確的表格。我一般會(huì)把每個(gè)簇的核心特征整理成這種格式簇標(biāo)簽、自定義人群名、人數(shù)占比、平均日消費(fèi)金額、平均日活躍次數(shù)、早餐占比、夜宵占比。這張表可以直接粘貼到期末報(bào)告的模型結(jié)果部分也能在答辯被追問(wèn)時(shí)快速定位到對(duì)應(yīng)數(shù)據(jù)。簇人群標(biāo)簽人數(shù)占比日均消費(fèi)(元)日均次數(shù)早餐占比夜宵占比0規(guī)律早餐型28%9.62.142%5%1活躍夜宵型19%21.33.49%38%2節(jié)制消費(fèi)型36%6.81.231%11%3周末集中型17%13.51.614%20%表格里的值來(lái)自 cluster_profile 的聚合結(jié)果不要手工填數(shù)也不要先把報(bào)告寫(xiě)好了再倒推數(shù)據(jù)。如果發(fā)現(xiàn)某種類(lèi)型的占比太小比如只有 2%說(shuō)明這個(gè)簇可能是異常值簇應(yīng)該回去調(diào)整模型參數(shù)而不是硬搬進(jìn)報(bào)告。5.3 報(bào)告結(jié)構(gòu)抽掉代碼也能自洽期末報(bào)告嚇退同學(xué)的往往不是分析而是不知道按什么順序?qū)?。我建議直接按項(xiàng)目流程組織第一部分寫(xiě)背景與目標(biāo)說(shuō)明為什么用校園消費(fèi)數(shù)據(jù)做建模想解決什么問(wèn)題第二部分寫(xiě)數(shù)據(jù)來(lái)源與預(yù)處理包括字段說(shuō)明和清洗流程第三部分寫(xiě)特征工程解釋你選擇了哪幾個(gè)特征、為什么沒(méi)有選別的第四部分寫(xiě)模型選擇與參數(shù)確定第五部分寫(xiě)分群結(jié)果與人群畫(huà)像最后一段寫(xiě)對(duì)學(xué)生消費(fèi)管理和個(gè)性化服務(wù)的建議。其中第三部分最容易被一筆帶過(guò)。實(shí)際上老師最看重的是特征構(gòu)造思路。你可以寫(xiě)「用 pandas 從原始流水中拆出小時(shí)、星期、餐段三類(lèi)時(shí)間特征」也可以展開(kāi)說(shuō)明「消費(fèi)熵衡量了消費(fèi)習(xí)慣的穩(wěn)定性」后者更能體現(xiàn)建模思考。但如果你的作業(yè)要求里沒(méi)有要求寫(xiě)建議那最后一段不寫(xiě)也沒(méi)問(wèn)題不用為了湊篇幅硬編對(duì)策。5.4 被老師問(wèn)「為什么選 KMeans」時(shí)這樣回答不慌張答這種問(wèn)題講究「性能 業(yè)務(wù)」雙線。先說(shuō)業(yè)務(wù)這次分析的目的是給全校學(xué)生做消費(fèi)人群分層KMeans 的每個(gè)簇可以用中心點(diǎn)描述天然適合輸出畫(huà)像。再說(shuō)性能KMeans 在大樣本下速度快算法復(fù)雜度接近線性咱們學(xué)校這條數(shù)據(jù)集有二十多萬(wàn)條流水跑一遍只需要幾秒鐘。更重要的是它只有 k 一個(gè)超參數(shù)調(diào)參路徑清晰不像 DBSCAN 的 eps 和 min_samples 需要反復(fù)試。如果老師追問(wèn)為什么不用 GMM 或?qū)哟尉垲?lèi)可以說(shuō) GMM 假設(shè)每個(gè)簇服從高斯分布校園消費(fèi)金額分布實(shí)際上偏態(tài)明顯這個(gè)假設(shè)不一定成立層次聚類(lèi)的計(jì)算量隨樣本數(shù)增長(zhǎng)較快面對(duì)幾萬(wàn)張卡的特征表不劃算。回答時(shí)不需要推倒重來(lái)只要把 KMeans 的優(yōu)勢(shì)和你對(duì)數(shù)據(jù)分布的觀察講清楚即可。6. 收尾一步用一個(gè)月做穩(wěn)定性驗(yàn)證讓報(bào)告再上一個(gè)等級(jí)期末作業(yè)交完后真正拉開(kāi)差距的是驗(yàn)證部分。我最后一件事永遠(yuǎn)是「留一個(gè)月數(shù)據(jù)當(dāng)驗(yàn)證集用前幾個(gè)月的參數(shù)復(fù)跑一次」。具體操作是把整個(gè)數(shù)據(jù)集按月份切分比如用 3—5 月數(shù)據(jù)建模再用 6 月數(shù)據(jù)計(jì)算每個(gè)樣本到已得簇中心的距離重新分配簇標(biāo)簽最后對(duì)比兩輪聚類(lèi)人數(shù)比例。如果一個(gè)簇的人數(shù)比例從 28% 跳到 64%說(shuō)明建模只抓住了月份效應(yīng)而不是消費(fèi)習(xí)慣需要回到特征設(shè)計(jì)去加入「學(xué)期前半段 / 后半段」標(biāo)記。另一個(gè)值得做的擴(kuò)展是特征重要性驗(yàn)證。把聚類(lèi)標(biāo)簽作為目標(biāo)變量用隨機(jī)森林訓(xùn)練一個(gè)分類(lèi)器看哪些特征貢獻(xiàn)度最高。如果早餐占比排第一說(shuō)明你的分群主要靠早餐習(xí)慣切分報(bào)告里就要突出描述這個(gè)特征如果消費(fèi)總金額排第一則要承認(rèn)這是個(gè)「消費(fèi)水平分層」模型而非「作息規(guī)律分層」模型。這一步不復(fù)雜卻能讓報(bào)告里所有結(jié)論都有依據(jù)。我當(dāng)年做這份作業(yè)時(shí)最深的教訓(xùn)是急著跑結(jié)果跳過(guò)驗(yàn)證直接寫(xiě)結(jié)論。答辯時(shí)老師對(duì)著投影儀說(shuō)「你把 5 月的數(shù)據(jù)抽出來(lái)跑一遍」結(jié)果簇分布完全變了當(dāng)場(chǎng)冷場(chǎng)。后來(lái)我養(yǎng)成了先切一個(gè)月數(shù)據(jù)做穩(wěn)定性驗(yàn)證的習(xí)慣再小的分析也按這個(gè)流程走。希望幫到你這 10 分鐘會(huì)讓你在講臺(tái)上的語(yǔ)氣跟別人不一樣。本文還有配套的精品資源點(diǎn)擊獲取