行為分析:從數(shù)據(jù)清洗到聚類(lèi)畫(huà)像全流程)
簡(jiǎn)介面向需要完成Python課程設(shè)計(jì)、畢業(yè)設(shè)計(jì)或期末大作業(yè)的學(xué)生提供一套完整的學(xué)生校園消費(fèi)行為分析項(xiàng)目。包內(nèi)包含可編譯運(yùn)行的Python源碼、校園消費(fèi)數(shù)據(jù)集以及詳細(xì)文檔說(shuō)明核心代碼分為初始化、模型和分析模塊并基于DFM模型展開(kāi)消費(fèi)行為研究配套docx說(shuō)明和README文檔可快速理解整體架構(gòu)與設(shè)計(jì)思路目錄結(jié)構(gòu)清晰便于二次開(kāi)發(fā)與調(diào)試。壓縮包共8個(gè)文件涵蓋py源碼、txt依賴(lài)說(shuō)明、docx文檔、md說(shuō)明及zip數(shù)據(jù)集等整體大小約10.07MB輕量且便于本地部署。項(xiàng)目?jī)?nèi)容已經(jīng)過(guò)助教審定評(píng)審得分98分目前已有141人學(xué)習(xí)使用。下載后可獲得可復(fù)現(xiàn)的數(shù)據(jù)清洗、特征分析、模型構(gòu)建與結(jié)果可視化流程以及文檔撰寫(xiě)參考既能滿(mǎn)足課程設(shè)計(jì)與期末大作業(yè)的交付要求也可作為畢業(yè)設(shè)計(jì)的起點(diǎn)擴(kuò)展更多分析維度。1. 基于Python的學(xué)生校園消費(fèi)行為分析課程設(shè)計(jì)怎么做才能拿到高分很多同學(xué)拿到“基于Python的學(xué)生校園消費(fèi)行為分析”這個(gè)題目第一反應(yīng)是畫(huà)幾張餅圖和柱狀圖把日均消費(fèi)額算出來(lái)文檔湊夠四十頁(yè)結(jié)果答辯時(shí)評(píng)委一句“你這個(gè)結(jié)論對(duì)學(xué)校管理有什么參考價(jià)值”就直接卡殼。這個(gè)項(xiàng)目的得分點(diǎn)從來(lái)不在圖表數(shù)量而在三條線(xiàn)上數(shù)據(jù)清洗是否嚴(yán)謹(jǐn)、分析邏輯是否閉環(huán)、結(jié)論能否落回校園場(chǎng)景。本文用一套可復(fù)現(xiàn)的流程把一卡通流水變成一份能支撐“規(guī)律型學(xué)生”“夜間活躍型學(xué)生”等結(jié)論的分析報(bào)告。適合正在做課程設(shè)計(jì)、畢業(yè)設(shè)計(jì)或者想拿這個(gè)題練手 Python 數(shù)據(jù)分析的從業(yè)者。2. 消費(fèi)行為分析的指標(biāo)體系少畫(huà)幾張圖先把“行為”拆成可計(jì)算的字段2.1 從原始流水到特征消費(fèi)行為的四個(gè)基本維度學(xué)生校園消費(fèi)行為聽(tīng)起來(lái)抽象但落到一卡通流水里可計(jì)算的維度無(wú)非四個(gè)消費(fèi)金額、消費(fèi)頻次、消費(fèi)時(shí)段、消費(fèi)場(chǎng)景。這四個(gè)維度不是拍腦袋定的而是由數(shù)據(jù)字段天然決定的。一卡通流水的典型字段包括學(xué)號(hào)、交易時(shí)間、交易類(lèi)型消費(fèi)、充值、退款、商戶(hù)名稱(chēng)或者商戶(hù)類(lèi)別、交易金額。學(xué)號(hào)對(duì)應(yīng)“誰(shuí)在消費(fèi)”交易時(shí)間對(duì)應(yīng)“什么時(shí)候消費(fèi)”金額對(duì)應(yīng)“花了多少”商戶(hù)類(lèi)別對(duì)應(yīng)“在哪兒消費(fèi)”。你的分析體系應(yīng)當(dāng)圍繞這四個(gè)維度展開(kāi)每一項(xiàng)都要能回答一個(gè)具體問(wèn)題。維度原始字段派生特征能回答的問(wèn)題消費(fèi)金額交易金額日均消費(fèi)、月均消費(fèi)、單筆金額分布學(xué)生整體消費(fèi)水平如何是否存在極端高消費(fèi)消費(fèi)頻次交易時(shí)間日消費(fèi)次數(shù)、月消費(fèi)次數(shù)學(xué)生多久去食堂一次是否頻繁在校內(nèi)消費(fèi)消費(fèi)時(shí)段交易時(shí)間早餐/午餐/晚餐/夜宵時(shí)段消費(fèi)占比學(xué)生的作息規(guī)律如何夜間消費(fèi)是否活躍消費(fèi)場(chǎng)景商戶(hù)名稱(chēng)食堂消費(fèi)占比、超市消費(fèi)占比、其他消費(fèi)占比學(xué)生的錢(qián)主要花在哪兒飲食占比是否合理有了這個(gè)表你的項(xiàng)目結(jié)構(gòu)就清晰了。后續(xù)每一步分析都是圍繞這四個(gè)維度中的一個(gè)或者幾個(gè)展開(kāi)。評(píng)委問(wèn)“你這張圖說(shuō)明了什么”你就能用“這說(shuō)明學(xué)生在晚上九點(diǎn)后還有明顯的消費(fèi)需求可能與圖書(shū)館閉館后的夜宵場(chǎng)景相關(guān)”這類(lèi)話(huà)回應(yīng)而不是說(shuō)“這個(gè)圖就是想展示一下分布”。2.2 分析方法的選型統(tǒng)計(jì)描述、聚類(lèi)與關(guān)聯(lián)規(guī)則怎么取舍數(shù)據(jù)分析方法很多但不是所有方法都適合這個(gè)題目。校園消費(fèi)數(shù)據(jù)有自己的特點(diǎn)量大但字段簡(jiǎn)單個(gè)體差異存在但規(guī)律相對(duì)穩(wěn)定沒(méi)有商品級(jí)明細(xì)導(dǎo)致關(guān)聯(lián)規(guī)則挖掘的價(jià)值有限。常見(jiàn)做法是以統(tǒng)計(jì)描述打底用聚類(lèi)做人群分層用簡(jiǎn)單的時(shí)序分析看趨勢(shì)。統(tǒng)計(jì)描述包括均值、中位數(shù)、分位數(shù)、標(biāo)準(zhǔn)差、頻數(shù)分布這些足夠回答“整體消費(fèi)水平如何”的問(wèn)題。聚類(lèi)用 KMeans把學(xué)生按消費(fèi)特征分成若干群體這是項(xiàng)目最出彩的部分因?yàn)槟苎苌觥爱?huà)像”這個(gè)概念。關(guān)聯(lián)規(guī)則Apriori在這個(gè)題里很容易翻車(chē)校園一卡通的流水通常只記錄到商戶(hù)類(lèi)別比如“第一食堂”“超市”沒(méi)有精確到“買(mǎi)了雞腿飯還是麻辣燙”挖掘出的規(guī)則往往是“早餐時(shí)段買(mǎi)豆?jié){的人也會(huì)買(mǎi)包子”這類(lèi)樸素結(jié)論說(shuō)服力不足。我的建議是主體用“統(tǒng)計(jì)描述 聚類(lèi) 按周/月趨勢(shì)”關(guān)聯(lián)規(guī)則可以作為擴(kuò)展分析寫(xiě)進(jìn)文檔的“進(jìn)一步研究方向”不要作為主結(jié)論。這樣既避免了 Apriori 在稀疏數(shù)據(jù)上跑不出有效規(guī)則的尷尬又能把項(xiàng)目重心放在數(shù)據(jù)清洗和分析閉環(huán)上。2.3 數(shù)據(jù)集的形態(tài)與讀入先看懂字段再寫(xiě)代碼拿到數(shù)據(jù)集后第一件事不是寫(xiě) pandas 讀入而是用文本編輯器打開(kāi)原始 CSV 看前二十行。這個(gè)習(xí)慣能避免后面至少三個(gè)坑。公開(kāi)數(shù)據(jù)集和課程設(shè)計(jì)數(shù)據(jù)集的字段命名差異很大有的叫student_id有的叫學(xué)號(hào)有的叫UserID交易時(shí)間有的帶時(shí)區(qū)有的精確到秒有的只有日期。以下是一份典型的學(xué)生校園消費(fèi)記錄 CSV 的形態(tài)字段名用了英文便于代碼書(shū)寫(xiě)字段名類(lèi)型示例值說(shuō)明student_id字符串S20230001學(xué)號(hào)已脫敏trade_time字符串2023-09-04 07:32:15交易時(shí)間trade_type字符串消費(fèi) / 充值 / 退款交易類(lèi)型merchant字符串第一食堂 / 校園超市商戶(hù)名稱(chēng)amount浮點(diǎn)數(shù)8.50交易金額單位元注意trade_type字段。很多項(xiàng)目在清洗時(shí)把注意力全放在金額和時(shí)間上忽略了交易類(lèi)型結(jié)果把充值記錄當(dāng)成消費(fèi)記錄統(tǒng)計(jì)人均消費(fèi)直接翻倍。這類(lèi)細(xì)節(jié)正是評(píng)分時(shí)區(qū)分“認(rèn)真做完”和“敷衍交差”的地方。數(shù)據(jù)集規(guī)模通常在幾萬(wàn)到幾十萬(wàn)行之間完全在 pandas 的處理能力之內(nèi)不需要引入 Spark 之類(lèi)的重型工具一臺(tái)普通筆記本足夠跑完整個(gè)流程。3. 用 pandas 清洗校園消費(fèi)流水從原始 CSV 到可供分析的特征表3.1 構(gòu)造本地演示數(shù)據(jù)集先造一份可控?cái)?shù)據(jù)再替換你的真實(shí)數(shù)據(jù)正式寫(xiě)清洗代碼前我習(xí)慣先造一份小規(guī)模的模擬數(shù)據(jù)。這么做有兩個(gè)理由一是公開(kāi)數(shù)據(jù)集脫敏后的字段和你的需求未必完全一致模擬數(shù)據(jù)可以保證流程可復(fù)現(xiàn)二是在模擬數(shù)據(jù)上調(diào)試代碼出錯(cuò)時(shí)你能立刻判斷是代碼問(wèn)題還是數(shù)據(jù)問(wèn)題。下面這段代碼生成 2000 條模擬消費(fèi)記錄覆蓋了后續(xù)清洗要處理的各類(lèi)臟數(shù)據(jù)。import pandas as pd import numpy as np from datetime import datetime, timedelta # 固定隨機(jī)種子保證示例可復(fù)現(xiàn) np.random.seed(42) # 生成 200 名學(xué)生每名學(xué)生產(chǎn)生約 10 條記錄 student_ids [fS2023{i:04d} for i in range(1, 201)] merchants [第一食堂, 第二食堂, 校園超市, 咖啡廳, 水果店] trade_types [消費(fèi), 充值, 退款] records [] for sid in student_ids: n_records np.random.randint(8, 15) # 每位學(xué)生的消費(fèi)時(shí)間集中在 9 月的 30 天內(nèi) start_date datetime(2023, 9, 1) timedelta(daysnp.random.randint(0, 20)) for _ in range(n_records): trade_time start_date timedelta( hoursnp.random.choice([7, 8, 11, 12, 17, 18, 21, 22]), minutesnp.random.randint(0, 60) ) trade_type np.random.choice(trade_types, p[0.85, 0.10, 0.05]) amount round(np.random.uniform(2.0, 25.0), 2) # 退款金額設(shè)置為負(fù)數(shù)方便后面測(cè)試清洗邏輯 if trade_type 退款: amount -amount records.append([sid, trade_time.strftime(%Y-%m-%d %H:%M:%S), trade_type, np.random.choice(merchants), amount]) df pd.DataFrame(records, columns[student_id, trade_time, trade_type, merchant, amount]) # 故意混入少量臟數(shù)據(jù)空學(xué)號(hào)、空金額、異常時(shí)間 df.loc[10, student_id] None df.loc[20, amount] None df.loc[30, trade_time] 2023/13/45 25:61:00 df.to_csv(campus_consumption.csv, indexFalse) print(df.head())這段代碼生成的數(shù)據(jù)里藏了三類(lèi)臟數(shù)據(jù)空學(xué)號(hào)、空金額和一個(gè)非法時(shí)間字符串。p參數(shù)在np.random.choice里表示各類(lèi)型被選中的概率85% 的記錄是消費(fèi)10% 是充值5% 是退款這樣構(gòu)造出的數(shù)據(jù)比例與真實(shí)一卡通流水比較接近。當(dāng)你拿到真實(shí)數(shù)據(jù)集后把read_csv的文件路徑換掉即可清洗代碼保持不變。3.2 清洗流程空值、非法時(shí)間與交易類(lèi)型過(guò)濾清洗的核心原則是“能不刪的不刪必須刪的說(shuō)明原因”。每一處刪除操作都要在文檔里寫(xiě)一句理由這會(huì)讓項(xiàng)目報(bào)告顯得專(zhuān)業(yè)。下面的代碼處理空值和非法時(shí)間并按交易類(lèi)型拆出消費(fèi)記錄。import pandas as pd # 讀入數(shù)據(jù)指定時(shí)間列先不做解析保留原始字符串便于排查 df pd.read_csv(campus_consumption.csv, dtype{student_id: str}) # 1. 刪除學(xué)號(hào)為空的行 before len(df) df df.dropna(subset[student_id]) print(f刪除空學(xué)號(hào) {before - len(df)} 行剩余 {len(df)} 行) # 2. 刪除金額為空或金額為 0 的行金額為 0 的交易不構(gòu)成消費(fèi)行為 before len(df) df df[(df[amount].notna()) (df[amount] ! 0)] print(f刪除空金額/零金額 {before - len(df)} 行剩余 {len(df)} 行) # 3. 用 errorscoerce 解析時(shí)間解析失敗會(huì)變成 NaT df[trade_time] pd.to_datetime(df[trade_time], errorscoerce) before len(df) df df.dropna(subset[trade_time]) print(f刪除非法時(shí)間 {before - len(df)} 行剩余 {len(df)} 行) # 4. 保留消費(fèi)記錄充值、退款單獨(dú)建表 consume_df df[df[trade_type] 消費(fèi)].copy() recharge_df df[df[trade_type] 充值].copy() refund_df df[df[trade_type] 退款].copy() # 5. 金額轉(zhuǎn) float并按常理過(guò)濾單筆超過(guò) 500 元的異常消費(fèi) consume_df[amount] consume_df[amount].astype(float) consume_df consume_df[consume_df[amount] 500] print(f消費(fèi)記錄 {len(consume_df)} 條充值 {len(recharge_df)} 條退款 {len(refund_df)} 條)這里有個(gè)參數(shù)值得細(xì)說(shuō)errorscoerce讓to_datetime在遇到2023/13/45 25:61:00這類(lèi)非法字符串時(shí)返回NaTNot a Time而不是直接拋異常終止腳本。這個(gè)處理方式保證了清洗流程能一口氣跑完失敗的行統(tǒng)一在下一步被過(guò)濾掉而不是程序中斷后你還要手動(dòng)定位是哪一行出了問(wèn)題。單筆 500 元的閾值不是拍腦袋定的。學(xué)生校園消費(fèi)的場(chǎng)景里食堂單筆一般不超過(guò) 50 元超市單筆通常不超過(guò) 200 元超過(guò) 500 元的記錄大概率是設(shè)備誤刷或者測(cè)試數(shù)據(jù)。閾值可以按你的數(shù)據(jù)集調(diào)整但必須在文檔里注明依據(jù)。3.3 時(shí)間特征抽取與消費(fèi)時(shí)段分組把流水變成分析表清洗后的數(shù)據(jù)還只是一張流水賬沒(méi)法直接做人群分析。接下來(lái)要做特征工程從trade_time里抽出小時(shí)、星期、是否工作日再把小時(shí)映射到早中晚等消費(fèi)時(shí)段。這一步是連接“數(shù)據(jù)”和“行為”的橋梁。# 從 trade_time 抽取時(shí)間特征 consume_df[hour] consume_df[trade_time].dt.hour consume_df[weekday] consume_df[trade_time].dt.weekday # 0周一, 6周日 consume_df[is_weekend] consume_df[weekday].isin([5, 6]).astype(int) # 定義時(shí)段映射函數(shù)把 24 小時(shí)壓縮成 5 個(gè)消費(fèi)時(shí)段 def time_period(hour): if 6 hour 9: return 早餐 elif 11 hour 14: return 午餐 elif 17 hour 20: return 晚餐 elif 20 hour 24: return 夜宵 else: return 非正餐 consume_df[period] consume_df[hour].apply(time_period) # 查看時(shí)段分布確認(rèn)劃分是否合理 period_stats consume_df.groupby(period)[amount].agg([count, sum, mean]) print(period_stats)時(shí)段劃分的邊界值看上去是幾個(gè)區(qū)間實(shí)際上每條邊界都在對(duì)應(yīng)學(xué)校的作息制度。早餐區(qū)間 6 到 9 點(diǎn)覆蓋早課前的用餐高峰午餐 11 到 14 點(diǎn)覆蓋下課后的錯(cuò)峰用餐晚餐 17 到 20 點(diǎn)同理夜宵 20 到 24 點(diǎn)捕捉的是晚自習(xí)或圖書(shū)館閉館后的消費(fèi)行為。如果你的數(shù)據(jù)集來(lái)自一所作息差異很大的學(xué)校邊界值可以調(diào)整但調(diào)整后要在文檔里說(shuō)明原因。24 點(diǎn)之后的零散消費(fèi)歸入“非正餐”避免深夜兩三點(diǎn)的個(gè)別記錄污染時(shí)段分布的可讀性。3.4 生成中間特征表把按人聚合的數(shù)據(jù)落盤(pán)清洗和時(shí)段劃分完成之后需要生成兩張中間表。一張是按人聚合的消費(fèi)特征表用于后續(xù)聚類(lèi)分析另一張是按天聚合的時(shí)序表用于看整體趨勢(shì)。中間表落盤(pán)為 CSV 文件后面所有可視化直接讀中間表不用每次重復(fù)跑清洗邏輯。# 按學(xué)生聚合每位學(xué)生的消費(fèi)行為特征 student_features consume_df.groupby(student_id).agg( total_amount(amount, sum), # 總消費(fèi)金額 total_count(amount, count), # 消費(fèi)次數(shù) avg_amount(amount, mean), # 單筆平均消費(fèi) active_days(trade_time, nunique), # 消費(fèi)活躍天數(shù) night_count(period, lambda x: (x 夜宵).sum()), # 夜宵次數(shù) breakfast_count(period, lambda x: (x 早餐).sum()) ).reset_index() # 新增派生特征平均每天消費(fèi)次數(shù) 總次數(shù) / 活躍天數(shù) student_features[daily_freq] round( student_features[total_count] / student_features[active_days], 2 ) # 按天聚合整體消費(fèi)趨勢(shì) daily_trend consume_df.groupby(consume_df[trade_time].dt.date).agg( daily_amount(amount, sum), daily_count(amount, count) ).reset_index() daily_trend.columns [date, daily_amount, daily_count] student_features.to_csv(student_features.csv, indexFalse) daily_trend.to_csv(daily_trend.csv, indexFalse) print(student_features.head())groupby(student_id).agg里的每一列都在回答一個(gè)問(wèn)題total_amount回答“這位學(xué)生 30 天總共花了多少”night_count回答“這位學(xué)生有多少次夜間消費(fèi)”active_days回答“這位學(xué)生有幾天真的在校內(nèi)消費(fèi)”。生成的特征表是后續(xù)聚類(lèi)的直接輸入列名的可解釋性直接影響你寫(xiě)報(bào)告時(shí)的效率。別用col1、col2這種命名三天后你自己都看不懂。4. 可視化與消費(fèi)畫(huà)像聚類(lèi)讓圖表替你說(shuō)話(huà)讓結(jié)論經(jīng)得起追問(wèn)4.1 全局消費(fèi)規(guī)律可視化先看分布再看均值很多人的第一張圖就是均值柱狀圖這沒(méi)錯(cuò)但不夠。均值的背后可能是大部分學(xué)生花得很少、極少數(shù)學(xué)生天天買(mǎi)咖啡刷出來(lái)的虛高。所以第一步要先畫(huà)分布圖消費(fèi)金額的直方圖和消費(fèi)時(shí)段的折線(xiàn)圖。分布圖能暴露異常也能為聚類(lèi)提供前置判斷。import matplotlib.pyplot as plt import pandas as pd # 讀取清洗后的數(shù)據(jù) consume_df pd.read_csv(consume_clean.csv, parse_dates[trade_time]) # 中文字體設(shè)置macOS 用 Arial Unicode MSWindows 用 SimHei plt.rcParams[font.sans-serif] [SimHei, Arial Unicode MS] plt.rcParams[axes.unicode_minus] False fig, axes plt.subplots(1, 2, figsize(14, 5)) # 左圖單筆消費(fèi)金額分布截?cái)嗟?50 元以?xún)?nèi)更易讀 ax1 axes[0] consume_df[consume_df[amount] 50][amount].hist( bins30, axax1, edgecolorwhite, color#4C72B0 ) ax1.set_title(單筆消費(fèi)金額分布0-50 元) ax1.set_xlabel(金額元) ax1.set_ylabel(交易筆數(shù)) # 右圖按時(shí)段的消費(fèi)筆數(shù)趨勢(shì) ax2 axes[1] period_order [早餐, 午餐, 晚餐, 夜宵, 非正餐] period_counts consume_df[period].value_counts().reindex(period_order) period_counts.plot(kindline, markero, axax2, color#C44E52, linewidth2) ax2.set_title(各時(shí)段消費(fèi)筆數(shù)分布) ax2.set_xlabel(時(shí)段) ax2.set_ylabel(消費(fèi)筆數(shù)) ax2.grid(alpha0.3) plt.tight_layout() plt.savefig(consumption_overview.png, dpi150) plt.show()直方圖的bins30表示把 0 到 50 元的區(qū)間切成 30 個(gè)等寬區(qū)間區(qū)間越多越能看到細(xì)粒度分布但過(guò)多會(huì)導(dǎo)致相鄰柱子的高度抖動(dòng)劇烈。我一般先設(shè) 30如果圖形鋸齒感太強(qiáng)就降到 15 到 20。右圖用折線(xiàn)而不是柱狀是因?yàn)闀r(shí)段之間有先后順序折線(xiàn)能直觀展示“早中晚三個(gè)主峰”的形態(tài)。兩張圖一張管金額分布是否健康一張管作息規(guī)律是否清晰答辯開(kāi)場(chǎng)講這兩張圖就足夠建立“我做過(guò)數(shù)據(jù)分析”的印象了。4.2 用 FMT 特征給學(xué)生分層KMeans 聚類(lèi)與畫(huà)像解讀學(xué)生個(gè)體之間的消費(fèi)差異很大直接看整體均值會(huì)把結(jié)論都拉平。聚類(lèi)分析的價(jià)值是把學(xué)生分成幾類(lèi)人每類(lèi)人有清晰的消費(fèi)標(biāo)簽。特征選擇用“頻次-金額-時(shí)段”三個(gè)方向也就是 student_features 里的total_count、total_amount、night_count。這三個(gè)字段分別代表活躍度、消費(fèi)力、作息偏好。from sklearn.cluster import KMeans from sklearn.preprocessing import StandardScaler import pandas as pd # 讀中間特征表提取聚類(lèi)特征 features pd.read_csv(student_features.csv) X features[[total_count, total_amount, night_count]].copy() # 標(biāo)準(zhǔn)化三步完成先擬合再轉(zhuǎn)換避免數(shù)據(jù)泄漏 scaler StandardScaler() X_scaled scaler.fit_transform(X) # 用輪廓系數(shù)選 K遍歷 2~6 類(lèi)取輪廓系數(shù)最大的 K from sklearn.metrics import silhouette_score best_k, best_score 2, -1 for k in range(2, 7): km KMeans(n_clustersk, random_state42, n_init10) labels km.fit_predict(X_scaled) score silhouette_score(X_scaled, labels) print(fK{k}, 輪廓系數(shù){score:.4f}) if score best_score: best_k, best_score k, score print(f最優(yōu)聚類(lèi)數(shù): {best_k}, 輪廓系數(shù): {best_score:.4f}) # 用最優(yōu) K 重新聚類(lèi)并把標(biāo)簽合并回原表 km KMeans(n_clustersbest_k, random_state42, n_init10) features[cluster] km.fit_predict(X_scaled) # 查看每個(gè)簇的特征均值用于給畫(huà)像命名 cluster_profile features.groupby(cluster)[ [total_count, total_amount, night_count] ].mean().round(2) print(cluster_profile)StandardScaler這段代碼值得解釋一下fit是計(jì)算均值和標(biāo)準(zhǔn)差transform是應(yīng)用標(biāo)準(zhǔn)化公式減去均值再除以標(biāo)準(zhǔn)差。KMeans 基于歐氏距離如果金額字段的量綱遠(yuǎn)大于計(jì)數(shù)次數(shù)字段聚類(lèi)結(jié)果會(huì)被金額主導(dǎo)導(dǎo)致“每天花 50 元但只去一次食堂”的學(xué)生和“每天花 60 元去三次食堂”的學(xué)生被分到一起這顯然不符合直覺(jué)。標(biāo)準(zhǔn)化之后三個(gè)特征的均值都是 0、方差都是 1每個(gè)維度在距離計(jì)算中的權(quán)重才平等。random_state42保證每次運(yùn)行結(jié)果一致n_init10表示用 10 組不同的初始質(zhì)心跑 10 次、取最優(yōu)避免 KMeans 掉進(jìn)局部最優(yōu)解。輪廓系數(shù)不是一個(gè)絕對(duì)標(biāo)準(zhǔn)它衡量樣本與自身簇的相似度高于與最近鄰簇的程度取值范圍在 -1 到 1 之間。校園消費(fèi)數(shù)據(jù)的輪廓系數(shù)通常在 0.3 到 0.5 之間超過(guò) 0.5 已經(jīng)算結(jié)構(gòu)清晰。如果跑出來(lái)只有 0.2說(shuō)明聚類(lèi)結(jié)構(gòu)不明顯這時(shí)候優(yōu)先檢查特征選擇而非盲目加簇?cái)?shù)。4.3 從圖表到文檔結(jié)論寫(xiě)一段經(jīng)得起追問(wèn)的分析聚類(lèi)跑完后給每類(lèi)學(xué)生貼標(biāo)簽。這一步很多人做得粗糙直接寫(xiě)“類(lèi)別 0 的學(xué)生消費(fèi)高、類(lèi)別 1 的學(xué)生消費(fèi)低”這等于把表格抄成了文字沒(méi)有任何價(jià)值。貼標(biāo)簽的核心是結(jié)合原始業(yè)務(wù)讓每個(gè)標(biāo)簽都能被一句場(chǎng)景描述支撐。聚類(lèi)編號(hào)total_count 均值total_amount 均值night_count 均值畫(huà)像標(biāo)簽018.2172.56.8規(guī)律三餐型高頻、消費(fèi)適中、偶有夜宵110.496.31.2節(jié)約型低頻、低額、極少夜間消費(fèi)215.6240.112.5夜間活躍型消費(fèi)高、夜間消費(fèi)突出37.8310.62.1高客單型低頻但單筆金額高每組畫(huà)一個(gè)雷達(dá)圖或者橫向柱狀圖放進(jìn)報(bào)告中圖下面寫(xiě)兩到三句話(huà)的解讀。例如“夜間活躍型學(xué)生消費(fèi)總金額僅次于高客單型但消費(fèi)頻次遠(yuǎn)高于后者說(shuō)明該群體的消費(fèi)主要由頻繁的小額夜宵構(gòu)成校園在夜宵時(shí)段的服務(wù)供給可能需要加強(qiáng)”。這樣的句子才是評(píng)委想看的它把圖表轉(zhuǎn)了結(jié)論又把結(jié)論落了業(yè)務(wù)建議。5. 常見(jiàn)問(wèn)題與避坑指南讓項(xiàng)目在答辯前不翻車(chē)的 5 條經(jīng)驗(yàn)5.1 時(shí)間解析失敗后整列變 NaT聚合結(jié)果全變空現(xiàn)象跑完分組聚合發(fā)現(xiàn)按小時(shí)統(tǒng)計(jì)的結(jié)果全是空值groupby出來(lái)只有行索引沒(méi)有數(shù)值。原因CSV 里混入了一行2023/13/45 25:61:00這樣的臟數(shù)據(jù)而你沒(méi)有用errorscoercepandas 在解析時(shí)直接拋異常或者把這列全轉(zhuǎn)成了 NaT。更隱蔽的情況是數(shù)據(jù)集里大部分時(shí)間是2023-09-01格式但某個(gè)月份的導(dǎo)出變成了2023/9/1格式混用導(dǎo)致解析不一致。解決讀入時(shí)先以字符串格式讀入再統(tǒng)一走pd.to_datetime(..., errorscoerce)。清洗邏輯永遠(yuǎn)保留“解析前原始字符串”和“解析后時(shí)間類(lèi)型”兩列方便排查具體哪一行出了問(wèn)題。我習(xí)慣是在清洗后打印df[trade_time].isna().sum()確認(rèn) NaT 數(shù)量與預(yù)期一致再繼續(xù)往下走。5.2 充值記錄混入消費(fèi)統(tǒng)計(jì)人均消費(fèi)虛高一倍現(xiàn)象按天統(tǒng)計(jì)消費(fèi)金額時(shí)某天的日消費(fèi)總額超過(guò)平時(shí)數(shù)倍畫(huà)出來(lái)的趨勢(shì)圖出現(xiàn)一個(gè)不合理的尖峰。原因原始流水里充值和消費(fèi)是混在一起的。學(xué)生開(kāi)學(xué)初集中充值 200 到 500 元這筆錢(qián)會(huì)出現(xiàn)在當(dāng)天流水里但性質(zhì)上不是消費(fèi)。如果你在清洗時(shí)只按“金額大于 0”過(guò)濾充值記錄就混進(jìn)了消費(fèi)數(shù)據(jù)。日均消費(fèi)被拉高聚類(lèi)時(shí)也會(huì)冒出虛假的高消費(fèi)群體。解決清洗階段必須按trade_type字段分流消費(fèi)、充值、退款各建一張表。后續(xù)所有關(guān)于“消費(fèi)行為”的分析嚴(yán)格使用consume_df。充值金額可以做獨(dú)立的“充值行為分析”顯示學(xué)生的充值習(xí)慣但不要和消費(fèi)混在一張表里討論。同理退款金額為負(fù)數(shù)如果清洗時(shí)粗暴地按“金額大于 0”過(guò)濾會(huì)把退款記錄全刪掉丟失掉異常處理的信息。5.3 Matplotlib 中文亂碼柱狀圖標(biāo)題變成方塊現(xiàn)象圖表標(biāo)題、坐標(biāo)軸標(biāo)簽里的中文全顯示成方塊截圖放進(jìn)文檔里很難看。原因Matplotlib 默認(rèn)字體不含中文字符集系統(tǒng)里也沒(méi)有找到可用的中文字體。這個(gè)問(wèn)題在 Windows 和 macOS 上的表現(xiàn)不同Windows 上通常是 SimHei 缺失或者未注冊(cè)macOS 上則要指定PingFang SC之類(lèi)的字體名。解決繪圖腳本開(kāi)頭固定寫(xiě)兩行配置plt.rcParams[font.sans-serif] [SimHei, Arial Unicode MS, PingFang SC] plt.rcParams[axes.unicode_minus] False如果改了字體名還是亂碼用matplotlib.font_manager.fontManager.ttflist打印當(dāng)前環(huán)境可用的字體列表從中挑一個(gè)支持中文的字體名填進(jìn)去。項(xiàng)目文檔里注明“繪圖時(shí)使用的中文字體已在腳本頭部統(tǒng)一配置”也能避免評(píng)委在自己的電腦上重新運(yùn)行腳本時(shí)遇到同樣的亂碼問(wèn)題而扣分。5.4 KMeans 聚類(lèi)前忘記標(biāo)準(zhǔn)化結(jié)果變成“金額獨(dú)裁”現(xiàn)象聚類(lèi)跑出來(lái)后每個(gè)簇的金額差異巨大但消費(fèi)次數(shù)幾乎相同畫(huà)像標(biāo)簽只有一個(gè)維度能區(qū)分。原因total_amount的數(shù)值范圍是幾十到幾百total_count是幾次到幾十次兩個(gè)特征在歐氏距離計(jì)算中的權(quán)重天然差異懸殊。金額的絕對(duì)差會(huì)主導(dǎo)距離導(dǎo)致 KMeans 按照金額高低把學(xué)生切開(kāi)其他維度形同虛設(shè)。解決StandardScaler標(biāo)準(zhǔn)化后再進(jìn)入 KMeans。標(biāo)準(zhǔn)化后的特征均值是 0、標(biāo)準(zhǔn)差是 1維度之間距離貢獻(xiàn)一致。注意先f(wàn)it再transform不要在fit_transform之后再手動(dòng)除以什么值。用一個(gè)簡(jiǎn)單的對(duì)比實(shí)驗(yàn)驗(yàn)證標(biāo)準(zhǔn)化前跑一次聚類(lèi)標(biāo)準(zhǔn)化后再跑一次對(duì)比兩次每個(gè)簇的特征均值你會(huì)看到標(biāo)準(zhǔn)化對(duì)聚類(lèi)結(jié)果的改變非常明顯這個(gè)對(duì)比可以寫(xiě)進(jìn)項(xiàng)目文檔作為方法選型的依據(jù)。5.5 將“退款”當(dāng)異常值直接刪除把好數(shù)據(jù)也刪了現(xiàn)象清洗后的數(shù)據(jù)量比原始數(shù)據(jù)少了近三分之一退款相關(guān)的分析完全做不了。原因很多人在清洗階段守則里寫(xiě)了“金額為負(fù)數(shù)是異常值直接過(guò)濾”但這個(gè)規(guī)則在校園消費(fèi)數(shù)據(jù)里不成立。退款記錄在流水里通常表現(xiàn)為負(fù)數(shù)但它是真實(shí)的業(yè)務(wù)行為比如食堂多扣款后的退回飯卡掛失后的余額退回。解決刪除異常值的規(guī)則要結(jié)合trade_type字段而不是只看金額正負(fù)。退款記錄單獨(dú)建表可以分析退款率、退款金額占消費(fèi)金額比例這些指標(biāo)甚至可以作為項(xiàng)目里的一個(gè)加分分析點(diǎn)。凡是做數(shù)據(jù)清洗都要養(yǎng)成一個(gè)習(xí)慣每條過(guò)濾規(guī)則寫(xiě)在文檔里注明“過(guò)濾了什么、為什么過(guò)濾、過(guò)濾了多少行”。6. 讓項(xiàng)目從“交差”變成“作品”目錄設(shè)計(jì)、文檔結(jié)構(gòu)與三個(gè)加分點(diǎn)6.1 代碼目錄的三層分離項(xiàng)目代碼不要只有一個(gè) Jupyter Notebook 文件。評(píng)委打開(kāi)一個(gè).ipynb文件看到一千行代碼和幾十張圖第一反應(yīng)是這個(gè)項(xiàng)目沒(méi)有工程化。我一般會(huì)把代碼拆成三層目錄campus_consumption_analysis/ ├── data/ # 原始數(shù)據(jù)與中間數(shù)據(jù) │ ├── raw/ │ └── processed/ ├── src/ # 可復(fù)用模塊 │ ├── data_clean.py # 清洗邏輯 │ ├── features.py # 特征工程 │ ├── analysis.py # 統(tǒng)計(jì)與聚類(lèi) │ └── visualize.py # 繪圖封裝 ├── output/ # 圖表與結(jié)果表格 │ └── figures/ └── docs/ # 詳細(xì)文檔s人名目錄里的每個(gè)模塊只干一件事模塊之間通過(guò)“讀中間表、寫(xiě)中間表”銜接。這樣做的直接收益是你在文檔里寫(xiě)“數(shù)據(jù)清洗步驟詳見(jiàn) src/data_clean.py”答辯時(shí)可以當(dāng)場(chǎng)演示修改清洗參數(shù)后重新跑通全流程這在評(píng)委眼里是實(shí)打?qū)嵉哪芰ψC明。6.2 詳細(xì)文檔的核心組織邏輯“詳細(xì)文檔說(shuō)明”是這個(gè)標(biāo)題里最容易湊字?jǐn)?shù)的部分也是拉開(kāi)差距的部分。一份能拿高分的文檔通常包含項(xiàng)目背景與數(shù)據(jù)說(shuō)明、清洗規(guī)則逐條說(shuō)明包含閾值選取依據(jù)、分析方法選型理由、可視化結(jié)果解讀、聚類(lèi)畫(huà)像的業(yè)務(wù)解釋、局限與改進(jìn)方向。每一章都要能和代碼對(duì)應(yīng)上文檔里出現(xiàn)一個(gè)結(jié)論代碼里就要有一條能生成這個(gè)結(jié)論的路徑。我最看重的章節(jié)是“清洗規(guī)則說(shuō)明”和“分析方法選型理由”。這兩章直接向評(píng)委傳遞一個(gè)信號(hào)這個(gè)數(shù)據(jù)集中有哪些坑我識(shí)別到了并且有應(yīng)對(duì)方案。把清洗規(guī)則列成一張帶“原因”列和“影響行數(shù)”列的表格比寫(xiě)兩頁(yè)散文有說(shuō)服力得多。6.3 三個(gè)答辯加分點(diǎn)第一個(gè)加分點(diǎn)是參數(shù)可配置。把清洗閾值、時(shí)段劃分邊界、聚類(lèi)簇?cái)?shù)都放到一個(gè)config.py文件里文檔里注明“調(diào)整 config.py 中的MAX_AMOUNT參數(shù)即可復(fù)用于不同學(xué)校的數(shù)據(jù)集”。第二個(gè)加分點(diǎn)是函數(shù)層面的注釋和類(lèi)型注解。給關(guān)鍵函數(shù)寫(xiě) docstring說(shuō)明參數(shù)范圍和返回值含義不追求花哨但每段代碼的意圖要清楚。第三個(gè)加分點(diǎn)是異常消費(fèi)預(yù)警的簡(jiǎn)單規(guī)則。在聚類(lèi)結(jié)果上加一個(gè)“夜間消費(fèi)占比過(guò)高”的規(guī)則篩選出需要關(guān)注的作息異常學(xué)生群體這相當(dāng)于給項(xiàng)目增加了一個(gè)應(yīng)用場(chǎng)景答辯時(shí)提一句“這個(gè)結(jié)果可以為輔導(dǎo)員提供參考”就很加分。我最初做這個(gè)題目時(shí)把大量時(shí)間花在了調(diào)圖表配色和排版上結(jié)果被評(píng)委一句“你的清洗規(guī)則是什么”問(wèn)住了。后來(lái)重做從字段檢查到聚類(lèi)結(jié)果解讀每一步都有記錄反而輕松很多。數(shù)據(jù)類(lèi)項(xiàng)目真正經(jīng)得起追問(wèn)的從來(lái)不是代碼寫(xiě)得多花哨而是每條結(jié)論都能溯源到一行數(shù)據(jù)、一段代碼、一處決策。希望這個(gè)完整的分析流程能幫你在做這個(gè)項(xiàng)目時(shí)少走幾步彎路。項(xiàng)目里的“異常消費(fèi)預(yù)警”規(guī)則我用的是一組簡(jiǎn)單的閾值組合比如單日消費(fèi)頻次超過(guò) 8 次、連續(xù)三天夜間消費(fèi)且金額遞增篩選結(jié)果保存成 CSV 后在文檔里做案例展示。這套規(guī)則不復(fù)雜但它能把前面所有的特征工程串成一條看得見(jiàn)的應(yīng)用鏈祝你順利。本文還有配套的精品資源點(diǎn)擊獲取