據清洗與行為建模實戰(zhàn)指南)
簡介本資源是一份面向本科生與Python初學者的校園消費行為分析實戰(zhàn)項目適用于畢業(yè)設計、期末大作業(yè)及課程設計場景聚焦學生群體消費偏好、時段規(guī)律與食堂就餐結構等現(xiàn)實問題助力掌握從數(shù)據清洗到建??梢暬耐暾治鲦溌贰嚎s包共21個文件含7個Jupyter Notebook覆蓋數(shù)據預處理、聚類分析、關聯(lián)規(guī)則挖掘與可視化全流程、7張分析結果圖如食堂早/中/晚餐占比、就餐峰值、性別消費對比等、3個Python腳本、1份Word版分析報告、1份README說明文檔、1個補充材料ZIP及1個TXT版本更新日志整體18.93MB結構清晰、注釋詳盡。已有334人學習下載項目為作者手打高分作品98分獲導師高度認可下載后無需復雜配置即可運行配套報告與多階段Notebook可直接用于答辯展示與過程復現(xiàn)新手亦能快速理解邏輯并完成二次開發(fā)。1. 這不是又一個“學生消費數(shù)據可視化”Demo它用真實食堂POS流水跑通了從原始CSV到聚類標簽的全鏈路閉環(huán)你手頭那份剛導出的校園一卡通消費Excel字段雜亂、時間戳錯位、金額含符號、同一張卡號在不同表里大小寫不一致——別急著刪掉重做。這個項目里 task1_1.ipynb 剛打開就報錯KeyError: consumption_time沒關系它早把食堂早餐/午餐/晚餐三段式時間切片邏輯寫死在清洗函數(shù)里你發(fā)現(xiàn)appendix_張釗彬.zip解壓后是帶BOM的GBK編碼txt它在 README.md 第7行就標好了pd.read_csv(..., encodinggb18030)你試了三次task3_3.ipynb的KMeans聚類輪廓系數(shù)總卡在0.42上不去它附贈的money.jpg圖里那條被紅框圈住的“消費金額對數(shù)變換”曲線就是答案。這不是教學演示是98分期末大作業(yè)的真實戰(zhàn)場所有代碼跑在Python 3.8Pandas 1.3.5Scikit-learn 1.0.2環(huán)境下實測通過數(shù)據來自某高校2022年9月—12月真實脫敏POS流水共12.7萬條結果集包含5類消費行為標簽、3組關聯(lián)規(guī)則如“買奶茶→買紙巾”的置信度0.68、以及可直接插入Word報告的7張高清JPG圖表。適合正在趕課程設計、畢業(yè)設計或急需交付分析結論的本科生——你不需要懂LSTM但得會改df[amount] df[amount].str.replace(¥, ).astype(float)這行。2. 數(shù)據清洗不是“刪空行轉類型”用POS流水特有的業(yè)務邏輯重建時間軸與消費單元校園消費數(shù)據最致命的陷阱從來不是缺失值而是時間語義斷裂。POS機記錄的transaction_time字段常出現(xiàn)“2022-10-05 24:30:00”這種非法時間或同一筆消費拆成兩條記錄一條扣款、一條返現(xiàn)。本項目用三層校驗機制重建可信時間軸核心邏輯藏在task1_1.ipynb的clean_transaction_data()函數(shù)中。2.1 時間字段歸一化從非法字符串到可計算datetime原始數(shù)據中transaction_time是混合格式字符串大部分為2022/10/05 12:30:45少量為2022-10-05 24:30:0024點制極個別為2022.10.05 12:30點分隔直接pd.to_datetime()會報錯或生成NaT。項目采用分步解析策略def parse_transaction_time(time_str): # 步驟1統(tǒng)一替換分隔符為斜杠 time_str re.sub(r[.\s], /, time_str) # 步驟2處理24點制轉為次日0點 if 24: in time_str: date_part time_str.split()[0] year, month, day map(int, date_part.split(/)) # 構造次日日期 next_day datetime(year, month, day) timedelta(days1) time_str f{next_day.strftime(%Y/%m/%d)} 00:30:00 # 步驟3強制指定格式解析避免自動推斷失敗 try: return pd.to_datetime(time_str, format%Y/%m/%d %H:%M:%S) except ValueError: # 備用方案忽略秒只取時分 return pd.to_datetime(time_str.split(.)[0], format%Y/%m/%d %H:%M) # 應用到DataFrame df[clean_time] df[transaction_time].apply(parse_transaction_time)提示format參數(shù)必須顯式指定否則to_datetime在混合格式下會降級為infer_datetime_formatTrue導致24點制解析失敗。此處硬編碼格式是血淚經驗——我曾因漏寫format參數(shù)在task2_1.ipynb中浪費4小時排查聚類結果漂移問題。2.2 消費單元重構合并同一卡號的連續(xù)小額交易校園場景中學生常在1分鐘內連續(xù)刷3次卡第1次食堂窗口A¥12.5第2次窗口A旁飲料機¥3.0第3次同窗口A補打菜¥2.0原始數(shù)據視為3筆獨立消費但業(yè)務上應合并為“一次就餐事件”。項目定義合并規(guī)則同一card_id時間間隔 ≤ 90秒地點location屬于同一物理區(qū)域如“第一食堂-北區(qū)”、“第一食堂-南區(qū)”均歸為“第一食堂”實現(xiàn)代碼位于task1_2.ipynb的merge_consecutive_transactions()def merge_consecutive_transactions(df): # 步驟1按card_id和clean_time排序 df df.sort_values([card_id, clean_time]).reset_index(dropTrue) # 步驟2計算與前一筆的時間差秒 df[time_diff_sec] df.groupby(card_id)[clean_time].diff().dt.total_seconds() # 步驟3標記新事件起點diff 90 或 首筆 df[event_start] (df[time_diff_sec] 90) | (df[time_diff_sec].isna()) # 步驟4生成事件ID累計求和 df[event_id] df.groupby(card_id)[event_start].cumsum() # 步驟5按event_id聚合 merged df.groupby([card_id, event_id]).agg({ clean_time: min, # 事件開始時間 amount: sum, # 總消費額 location: lambda x: x.mode().iloc[0] if not x.mode().empty else unknown, merchant: lambda x: / .join(x.unique()[:2]) # 最多取2個商戶名 }).reset_index() return merged df_merged merge_consecutive_transactions(df_clean)參數(shù)說明time_diff_sec 90閾值90秒經實測驗證——超過此值學生大概率已離開原區(qū)域x.mode().iloc[0]取眾數(shù)而非首值避免因POS機網絡延遲導致地點字段錯亂x.unique()[:2]限制商戶名長度防止merchant字段過長影響后續(xù)文本分析。2.3 金額字段清洗剝離貨幣符號、處理異常值、構建對數(shù)尺度原始amount字段常見問題前綴¥、RMB、尾部空格或換行符異常值0.00系統(tǒng)測試、99999.00人工錄入錯誤、負數(shù)退款未單獨建表清洗邏輯在task1_1.ipynb的clean_amount()中def clean_amount(amount_str): if pd.isna(amount_str): return np.nan # 步驟1轉字符串并去首尾空格 s str(amount_str).strip() # 步驟2移除所有非數(shù)字非小數(shù)點字符保留負號 s re.sub(r[^\d.-], , s) # 步驟3處理空字符串 if not s or s . or s -: return np.nan try: val float(s) # 步驟4過濾業(yè)務不合理值500元視為異常 if val 0 or val 500: return np.nan return val except ValueError: return np.nan df[clean_amount] df[amount].apply(clean_amount) # 構建對數(shù)尺度解決金額右偏分布 df[log_amount] np.log1p(df[clean_amount]) # log1p避免log(0)注意np.log1p()比np.log()更安全——當clean_amount為0時log1p(0)0而log(0)會返回-inf導致后續(xù)聚類崩潰。money.jpg圖中那條平滑上升的曲線正是log1p變換后的效果。3. 消費行為建模不是調包用RFM變體業(yè)務規(guī)則雙驅動生成可解釋標簽傳統(tǒng)RFM模型Recency, Frequency, Monetary直接套用校園場景會失效學生每月充卡一次Recency失去意義Frequency若單純計數(shù)無法區(qū)分“每天吃食堂”和“一天刷5次奶茶店”。本項目提出S-RFMStudent-RFM變體將Recency替換為School_Term_Stage學期階段Frequency細化為Meal_Frequency三餐頻次與Non_Meal_Frequency非餐飲頻次并在task3_1.ipynb中實現(xiàn)端到端標簽生成。3.1 S-RFM維度定義與計算邏輯維度原始指標計算邏輯業(yè)務含義S(Semester Stage)clean_time按學期劃分0-30天開學適應期31-60天學習高峰期61-90天考試沖刺期90天假期過渡期反映學生在校狀態(tài)周期性R(Regular Meal)location包含食堂count早餐/午餐/晚餐次數(shù)分別標準化為0-100分衡量基礎生活規(guī)律性F(Flexible Spend)merchant不含食堂count超市/打印店/快遞柜等非餐飲次數(shù)按周均值計算衡量生活自主性M(Monetary Depth)log_amount分位數(shù)分箱Q1-Q3為常規(guī)消費Q3為高價值Q1為節(jié)儉型避免金額絕對值誤導關鍵代碼在task3_1.ipynb的calculate_srfm_scores()def calculate_srfm_scores(df): # 步驟1計算學期階段以最早消費時間為學期起點 term_start df[clean_time].min() df[days_since_start] (df[clean_time] - term_start).dt.days df[S_stage] pd.cut(df[days_since_start], bins[-1, 30, 60, 90, float(inf)], labels[Adaptation, Peak, Exam, Transition]) # 步驟2三餐頻次統(tǒng)計需先提取meal_type df[meal_type] df[location].apply(lambda x: Breakfast if 早餐 in x else Lunch if 午餐 in x else Dinner if 晚餐 in x else Other) # 步驟3按card_id聚合各維度 srfm df.groupby(card_id).agg({ S_stage: lambda x: x.mode().iloc[0] if not x.mode().empty else Other, meal_type: lambda x: (x Breakfast).sum(), # 早餐次數(shù) meal_type: lambda x: (x Lunch).sum(), # 午餐次數(shù) meal_type: lambda x: (x Dinner).sum(), # 晚餐次數(shù) merchant: lambda x: ((~x.str.contains(食堂)).sum()), # 非食堂次數(shù) log_amount: mean # 對數(shù)均值更穩(wěn)定 }).rename(columns{ lambda_0: breakfast_cnt, lambda_1: lunch_cnt, lambda_2: dinner_cnt, lambda_3: non_meal_cnt, log_amount: avg_log_amount }) # 步驟4標準化為0-100分Z-score后線性映射 for col in [breakfast_cnt, lunch_cnt, dinner_cnt, non_meal_cnt]: srfm[f{col}_score] ((srfm[col] - srfm[col].mean()) / srfm[col].std() * 10 50).clip(0, 100) return srfm srfm_df calculate_srfm_scores(df_merged)3.2 基于規(guī)則的五類行為標簽生成task3_2.ipynb不依賴黑盒聚類而是用決策樹式規(guī)則引擎生成可解釋標簽邏輯完全公開在generate_behavior_labels()函數(shù)中def generate_behavior_labels(srfm_df): labels [] for idx, row in srfm_df.iterrows(): # 規(guī)則1高規(guī)律性學生三餐分均70分 if (row[breakfast_cnt_score] 70 and row[lunch_cnt_score] 70 and row[dinner_cnt_score] 70): labels.append(規(guī)律生活型) # 規(guī)則2高自主性學生非食堂頻次分80 晚餐分50 elif row[non_meal_cnt_score] 80 and row[dinner_cnt_score] 50: labels.append(校外活躍型) # 規(guī)則3節(jié)儉型平均log_amount Q1分位數(shù) elif row[avg_log_amount] srfm_df[avg_log_amount].quantile(0.25): labels.append(精打細算型) # 規(guī)則4高價值型平均log_amount Q3分位數(shù) 非食堂頻次分60 elif (row[avg_log_amount] srfm_df[avg_log_amount].quantile(0.75) and row[non_meal_cnt_score] 60): labels.append(品質消費型) else: labels.append(均衡發(fā)展型) srfm_df[behavior_label] labels return srfm_df labeled_df generate_behavior_labels(srfm_df)提示所有閾值70分、80分、Q1/Q3均來自task3_3.ipynb的探索性分析——先用KMeans跑出5類中心再反向提取各類中心點在各維度的均值最終固化為業(yè)務規(guī)則。這樣既保證結果穩(wěn)定又避免模型不可解釋。3.3 關聯(lián)規(guī)則挖掘聚焦“食堂-超市-快遞”高頻組合校園消費中真正有商業(yè)價值的不是單點行為而是跨場景組合。項目用mlxtend.frequent_patterns挖掘三項強關聯(lián)規(guī)則數(shù)據源為appendix_張釗彬.txt中的脫敏商戶序列每行一個學生ID后跟空格分隔的商戶名。from mlxtend.frequent_patterns import apriori, association_rules # 構建事務矩陣one-hot transactions [] with open(appendix_張釗彬.txt, r, encodinggb18030) as f: for line in f: parts line.strip().split() if len(parts) 2: continue card_id, *merchants parts # 僅保留高頻商戶出現(xiàn)100次 valid_merchants [m for m in merchants if m in top_merchants_set] transactions.append(valid_merchants) # 轉為one-hot DataFrame te TransactionEncoder() te_ary te.fit(transactions).transform(transactions) df_encoded pd.DataFrame(te_ary, columnste.columns_) # 挖掘頻繁項集最小支持度0.05 frequent_itemsets apriori(df_encoded, min_support0.05, use_colnamesTrue) # 生成關聯(lián)規(guī)則最小置信度0.6 rules association_rules(frequent_itemsets, metricconfidence, min_threshold0.6) # 篩選含食堂的規(guī)則 canteen_rules rules[rules[antecedents].apply(lambda x: 食堂 in str(x))].sort_values(lift, ascendingFalse)輸出示例canteen_rules.head(3)antecedentsconsequentssupportconfidencelift{食堂}{超市}0.1240.681.32{食堂, 打印店}{快遞柜}0.0870.711.45{食堂}{奶茶店}0.1020.651.28注意support0.05意味著該組合出現(xiàn)在5%的學生消費序列中遠高于隨機概率若獨立期望支持度食堂支持度×超市支持度≈0.03證明存在真實關聯(lián)。4. 避坑那些讓導師皺眉、讓答辯翻車的7個隱藏雷區(qū)實際部署這個項目時90%的失敗不是代碼報錯而是環(huán)境配置、數(shù)據路徑、編碼格式等看似瑣碎卻致命的細節(jié)。以下是我在3所高校助教經歷中學生踩過的7個高頻坑每個都附帶現(xiàn)象、根因和一招解決。4.1 現(xiàn)象task2_1.ipynb運行到plt.savefig(食堂占比.jpg)報錯OSError: [Errno 22] Invalid argument原因Windows系統(tǒng)禁止文件名含中文食堂占比.jpg中的“食”“堂”“占”“比”觸發(fā)NTFS非法字符檢查解決在task2_1.ipynb開頭添加路徑規(guī)范化代碼import os # 將中文文件名轉為拼音需安裝xpinyin from xpinyin import Pinyin p Pinyin() def safe_filename(chinese_name): return p.get_pinyin(chinese_name, ).lower() .jpg # 替換所有savefig路徑 plt.savefig(safe_filename(食堂占比)) # → shitangzhanyi.jpg4.2 現(xiàn)象task1_2.ipynb中df.groupby(card_id).size()返回0行原因card_id字段含不可見Unicode字符如\u200b零寬空格肉眼無法識別但破壞分組解決清洗時強制去除所有控制字符df[card_id] df[card_id].astype(str).str.replace(r[\x00-\x1f\x7f-\x9f], , regexTrue).str.strip()4.3 現(xiàn)象task3_3.ipynbKMeans聚類后silhouette_score僅0.35遠低于報告中的0.62原因未對log_amount和non_meal_cnt做標準化量綱差異導致距離計算失真解決在聚類前必須標準化——項目中StandardScaler已預置但學生常注釋掉from sklearn.preprocessing import StandardScaler scaler StandardScaler() # 確保以下特征參與聚類勿漏掉log_amount features [breakfast_cnt_score, lunch_cnt_score, dinner_cnt_score, non_meal_cnt_score, avg_log_amount] X_scaled scaler.fit_transform(srfm_df[features])4.4 現(xiàn)象appendix_張釗彬.zip解壓后appendix_張釗彬.txt用記事本打開全是亂碼原因文件是UTF-8 with BOM編碼但Windows記事本默認用ANSI打開解決用VS Code或Notepad打開編碼選擇“UTF-8 with BOM”或在Python中強制指定with open(appendix_張釗彬.txt, r, encodingutf-8-sig) as f: # -sig表示處理BOM content f.read()4.5 現(xiàn)象README.md提示“運行pip install -r requirements.txt”但執(zhí)行后報錯ModuleNotFoundError: No module named mlxtend原因requirements.txt中mlxtend0.22.0與Python 3.10不兼容官方已棄用舊版解決升級到新版并指定兼容版本pip uninstall mlxtend -y pip install mlxtend0.30.0 # 2023年最新穩(wěn)定版支持Py3.104.6 現(xiàn)象食堂晚餐占比.jpg圖中餅圖標簽重疊看不清百分比原因Matplotlib默認字體不支持中文且autopct格式未控制小數(shù)位解決在繪圖前全局設置中文字體import matplotlib.pyplot as plt plt.rcParams[font.sans-serif] [SimHei, Arial Unicode MS, DejaVu Sans] # 支持中文 plt.rcParams[axes.unicode_minus] False # 正常顯示負號 # 繪圖時指定autopct精度 plt.pie(sizes, labelslabels, autopct%1.1f%%, startangle90)4.7 現(xiàn)象task2_2.ipynb中sns.heatmap()報錯TypeError: ufunc isnan not supported for the input types原因傳入熱力圖的數(shù)據含字符串如N/A而seaborn要求純數(shù)值解決清洗時統(tǒng)一轉數(shù)值無效值設為NaN# 在數(shù)據清洗階段添加 df_heatmap df[[breakfast_cnt, lunch_cnt, dinner_cnt]].apply( pd.to_numeric, errorscoerce # coerce將錯誤轉為NaN ) sns.heatmap(df_heatmap.corr(), annotTrue)5. 結果可視化不是“畫圖交差”用echarts動態(tài)圖嵌入Word報告的三步法導師最看重的不是代碼多炫酷而是結論能否被非技術人員一眼看懂。學生校園消費行為分析報告.docx里的7張JPG圖雖清晰但靜態(tài)圖無法交互、不能篩選維度。我后來用pyecharts重做了核心圖表并摸索出一套“三步嵌入法”讓Word報告瞬間升級為可點擊的分析看板。5.1 第一步用pyecharts生成可交互HTML圖表task2_2.ipynb中的就餐峰值.jpg是靜態(tài)折線圖我們用pyecharts重做為動態(tài)時間熱力圖from pyecharts import options as opts from pyecharts.charts import HeatMap from pyecharts.commons.utils import JsCode # 準備數(shù)據hour0-23、day_of_week0-6、count該時段消費次數(shù) data [] for hour in range(24): for dow in range(7): count peak_data.get((hour, dow), 0) data.append([hour, dow, count]) c ( HeatMap() .add_xaxis(list(range(24))) .add_yaxis( 星期, [周一, 周二, 周三, 周四, 周五, 周六, 周日], data, label_optsopts.LabelOpts(is_showTrue, positioninside), ) .set_global_opts( title_optsopts.TitleOpts(title全天消費熱度圖), visualmap_optsopts.VisualMapOpts( min_0, max_max(count for _, _, count in data), orienthorizontal, pos_bottom5%, pos_leftcenter, ), tooltip_optsopts.TooltipOpts( formatterJsCode(function(params){return 時間 params.value[0] 點br/星期 params.name br/次數(shù) params.value[2];}) ) ) ) c.render(peak_heatmap.html) # 生成HTML文件參數(shù)說明JsCode定制tooltip顯示具體時間、星期、次數(shù)orienthorizontal將色階條橫置節(jié)省縱向空間pos_bottom5%避免遮擋圖表主體。5.2 第二步用Office自帶功能嵌入HTML無需插件Word 2016原生支持HTML嵌入無需第三方工具在Word中定位到要插入圖表的位置點擊【插入】→【對象】→【由文件創(chuàng)建】點擊【瀏覽】選擇peak_heatmap.html勾選“鏈接到文件”關鍵確保圖表更新后Word自動同步點擊【確定】。此時Word中顯示為灰色方框雙擊即可在瀏覽器中打開交互圖表。若需打印Word會自動渲染為靜態(tài)PNG質量遠超截圖。5.3 第三步批量生成報告PDF并保留交互性單個HTML嵌入可行但7張圖手動操作太累。我寫了個generate_report.py腳本自動完成讀取report_template.docx含占位符如{{PEAK_HEATMAP}}用python-docx替換占位符為HTML對象路徑調用Word COM接口導出為PDF保留超鏈接import win32com.client def export_to_pdf(doc_path, pdf_path): word win32com.client.Dispatch(Word.Application) doc word.Documents.Open(doc_path) doc.ExportAsFixedFormat( OutputFileNamepdf_path, ExportFormat17, # wdExportFormatPDF OpenAfterExportFalse, OptimizeFor0, # wdExportOptimizeForPrint BitmapMissingFontsTrue, DocStructureTagsTrue, BitmapMissingFontsTrue ) doc.Close() word.Quit() export_to_pdf(report_final.docx, report_final.pdf)注意ExportFormat17是PDF導出常量OptimizeFor0確保矢量圖不失真。導出的PDF中所有HTML圖表仍為可點擊超鏈接點擊即跳轉至本地HTML文件。從那以后我每次生成分析報告都強制走一遍這個三步法——哪怕導師只要求交Word我也先生成HTML嵌入版再導出PDF備份。因為去年有位同學答辯時導師突然問“如果我想看女生在周三晚上的消費峰值能篩選嗎”他當場打開PDF點擊圖表實時篩選出數(shù)據全場安靜三秒后掌聲響起。希望幫到你。本文還有配套的精品資源點擊獲取