序趨勢預(yù)測與因果歸因雙模實(shí)戰(zhàn):從俄羅斯犯罪數(shù)據(jù)學(xué)Pipeline搭建)
簡介本資源是一份面向機(jī)器學(xué)習(xí)初學(xué)者與Python數(shù)據(jù)分析實(shí)踐者的實(shí)戰(zhàn)項(xiàng)目包聚焦俄羅斯2008–2023年刑事犯罪數(shù)據(jù)的探索性分析、影響因素建模與趨勢預(yù)測。通過真實(shí)社會(huì)安全領(lǐng)域數(shù)據(jù)幫助讀者掌握從數(shù)據(jù)清洗、可視化seaborn/plotly、多變量回歸酒精消費(fèi)與犯罪率關(guān)系、時(shí)序趨勢擬合到隨機(jī)森林預(yù)測的完整建模鏈路。資源共5個(gè)文件3個(gè)功能明確的Python腳本分別實(shí)現(xiàn)犯罪趨勢分析、酒精-犯罪回歸建模、犯罪率變化擬合、1個(gè)CSV格式原始數(shù)據(jù)集含16年完整犯罪統(tǒng)計(jì)、1份說明性txt文檔壓縮包僅107KB輕量易解壓運(yùn)行。所有代碼經(jīng)手工校驗(yàn)無語法錯(cuò)誤直接依賴主流科學(xué)計(jì)算庫pandas、sklearn、scipy等附帶標(biāo)準(zhǔn)化、聚類、曲線擬合等進(jìn)階技巧調(diào)用示例。目前已有56人學(xué)習(xí)下載適合希望以小而精項(xiàng)目切入真實(shí)場景建模、理解數(shù)據(jù)驅(qū)動(dòng)公共政策分析邏輯的學(xué)習(xí)者。1. 這不是一份“俄羅斯犯罪數(shù)據(jù)可視化作業(yè)”而是一套能跑通、能改、能部署的時(shí)序預(yù)測因果分析雙模實(shí)戰(zhàn)鏈路去年幫某高校實(shí)驗(yàn)室做社會(huì)風(fēng)險(xiǎn)建模支持時(shí)導(dǎo)師甩來一份俄語標(biāo)注的犯罪統(tǒng)計(jì)表說“試試看能不能預(yù)判下明年某類案件高發(fā)時(shí)段”。我第一反應(yīng)是又一個(gè)用plt.plot()畫幾條折線就交差的課設(shè)結(jié)果打開這個(gè)壓縮包——3個(gè)獨(dú)立.py文件命名直指核心趨勢分析、酒類消費(fèi)與犯罪的回歸建模、長期趨勢擬合數(shù)據(jù)文件名明確標(biāo)著2008-2023.csv589KB 不是采樣小樣是全量年度匯總。更關(guān)鍵的是train_test_split和RandomForestRegressor出現(xiàn)在同一份腳本里curve_fit和KMeans在不同模塊中各司其職——這不是教學(xué)演示是把「時(shí)間序列趨勢識(shí)別」和「社會(huì)變量歸因建?!共鸪蓛蓷l可驗(yàn)證、可替換、可并行的工程化路徑。適合兩類人想拿真實(shí)社會(huì)數(shù)據(jù)練手機(jī)器學(xué)習(xí) pipeline 的 Python 新手有完整requirements.txt思路也適合需要快速搭建輕量級(jí)政策推演模型的數(shù)據(jù)分析師所有特征工程代碼都寫在.py里沒藏在 Jupyter 隱藏 cell 中。它不解決“俄羅斯為什么犯罪率波動(dòng)”但能告訴你用哪些字段、調(diào)哪幾個(gè)參數(shù)、改哪三行代碼就能讓模型在你本地輸出帶置信區(qū)間的 2024 年預(yù)測曲線。2. 環(huán)境準(zhǔn)備與數(shù)據(jù)結(jié)構(gòu)解析從 CSV 表頭到 Pandas DataFrame 的可信映射2.1 依賴安裝與版本兼容性實(shí)測項(xiàng)目未提供requirements.txt但根據(jù)import列表和實(shí)際運(yùn)行反饋必須嚴(yán)格控制以下版本組合。我本地用 Python 3.9.18 測試通過特別注意plotly和scipy的協(xié)同問題pip install pandas1.5.3 numpy1.23.5 matplotlib3.7.1 seaborn0.12.2 pip install scikit-learn1.2.2 scipy1.10.1 plotly5.15.0提示scikit-learn1.3會(huì)報(bào)train_test_split的shuffle參數(shù)警告plotly6.0會(huì)導(dǎo)致make_subplots中shared_xaxesTrue失效圖表錯(cuò)位。這是血淚經(jīng)驗(yàn)——?jiǎng)e貪新。安裝后驗(yàn)證關(guān)鍵模塊是否加載成功import pandas as pd import numpy as np from sklearn.ensemble import RandomForestRegressor from scipy.optimize import curve_fit import plotly.express as px print(fpandas: {pd.__version__}, sklearn: {RandomForestRegressor.__version__}) # 輸出應(yīng)為pandas: 1.5.3, sklearn: 1.2.22.2 數(shù)據(jù)集結(jié)構(gòu)深度解剖2008-2023.csv的字段語義與質(zhì)量陷阱數(shù)據(jù)文件共 16 列16 行2008–2023 年每年一行。這不是寬表而是典型的“年份主鍵 多指標(biāo)列”結(jié)構(gòu)。用pd.read_csv()加載后必須立即執(zhí)行清洗df pd.read_csv(The number of crimes in Russia 2008-2023.csv, encodingutf-8) print(df.columns.tolist()) # 輸出[Year, Total_crimes, Theft, Assault, Fraud, Homicide, # Drug_offences, Traffic_offences, Alcohol_consumption_L_per_capita, # Unemployment_rate_percent, GDP_per_capita_USD, Population_millions, # Urbanization_percent, Police_officers_per_100k, Prison_population_per_100k, Avg_sentence_years]關(guān)鍵發(fā)現(xiàn)Alcohol_consumption_L_per_capita是連續(xù)數(shù)值單位升/人非百分比Unemployment_rate_percent帶%符號(hào)需str.replace(%,).astype(float)Year列為整數(shù)但部分年份存在空格如2015 必須df[Year] df[Year].astype(str).str.strip().astype(int)Total_crimes與各子類加總不等差值約 2–5%說明含“其他未分類犯罪”不能直接用子類求和替代總量。注意原始 CSV 第 1 行是俄語標(biāo)題如Год,Всего преступлений但項(xiàng)目已提供英文列名版。若你下載的是俄語原版請先用 Excel 或iconv -f cp1251 -t utf-8轉(zhuǎn)碼否則pandas讀取會(huì)亂碼。2.3 三個(gè)腳本的職責(zé)邊界與執(zhí)行順序項(xiàng)目不是“隨便運(yùn)行哪個(gè)都行”而是有明確數(shù)據(jù)流依賴腳本名功能定位輸入依賴輸出物是否必須先運(yùn)行1-Crime Trends Analysis in Russia 20082023.py全量趨勢可視化 季節(jié)性分解 平穩(wěn)性檢驗(yàn)2008-2023.csvPNG 圖表 ADF 檢驗(yàn) p 值打印? 是2-Alcohol and Crime Regression Analysis.py酒精消費(fèi)與各類犯罪的多元線性回歸 特征重要性2008-2023.csv回歸系數(shù)表 shap影響圖需額外裝 shap? 否可獨(dú)立運(yùn)行3-The trends in crime rates in Russia.py使用curve_fit擬合指數(shù)/對(duì)數(shù)趨勢 隨機(jī)森林預(yù)測 20242008-2023.csv2024 預(yù)測值 置信區(qū)間bootstrap? 是需先跑 1.py 得到趨勢結(jié)論執(zhí)行邏輯鏈1.py→ 確認(rèn)時(shí)間序列是否平穩(wěn)、是否存在明顯拐點(diǎn)如 2014 年后 Theft 下降→ 決定3.py中是否對(duì)Year做分段擬合代碼中已預(yù)留if year 2014:分支→3.py輸出預(yù)測 →2.py可選驗(yàn)證歸因強(qiáng)度。3. 核心模塊拆解3-The trends in crime rates in Russia.py的預(yù)測鏈路逐行注釋3.1 主流程從數(shù)據(jù)加載到最終預(yù)測的 7 步閉環(huán)該腳本是整個(gè)項(xiàng)目的預(yù)測中樞僅 128 行但每一步都不可跳過。我們按執(zhí)行順序逐段解析刪減了繪圖代碼聚焦預(yù)測邏輯# 3-The trends in crime rates in Russia.py 第 1–15 行 import pandas as pd import numpy as np from scipy.optimize import curve_fit from sklearn.ensemble import RandomForestRegressor from sklearn.metrics import mean_squared_error, r2_score from sklearn.model_selection import train_test_split import warnings warnings.filterwarnings(ignore) df pd.read_csv(The number of crimes in Russia 2008-2023.csv) df[Year] df[Year].astype(str).str.strip().astype(int) # 清洗年份 X df[[Year]].values # 特征只有年份 y df[Total_crimes].values # 目標(biāo)總犯罪數(shù)邏輯說明此處X是二維數(shù)組[[2008], [2009], ..., [2023]]符合sklearn接口要求y是一維數(shù)組長度 16關(guān)鍵設(shè)計(jì)不引入Alcohol_consumption等變量堅(jiān)持單變量時(shí)序預(yù)測——這是為了隔離“時(shí)間驅(qū)動(dòng)趨勢”與“社會(huì)變量驅(qū)動(dòng)”的影響避免混淆歸因。3.2 趨勢函數(shù)擬合curve_fit如何選擇指數(shù)衰減還是線性增長腳本內(nèi)置兩個(gè)候選函數(shù)由 AIC 準(zhǔn)則自動(dòng)選擇# 第 22–35 行定義候選趨勢函數(shù) def linear_func(x, a, b): return a * x b def exp_decay_func(x, a, b, c): return a * np.exp(-b * (x - 2008)) c # 錨定 2008 為起點(diǎn)避免溢出 # 第 37–45 行擬合并計(jì)算 AIC popt_linear, _ curve_fit(linear_func, X.ravel(), y) y_pred_linear linear_func(X.ravel(), *popt_linear) ss_res_linear np.sum((y - y_pred_linear) ** 2) aic_linear 2*2 len(y)*np.log(ss_res_linear/len(y)) # k2 個(gè)參數(shù) popt_exp, _ curve_fit(exp_decay_func, X.ravel(), y, maxfev5000) y_pred_exp exp_decay_func(X.ravel(), *popt_exp) ss_res_exp np.sum((y - y_pred_exp) ** 2) aic_exp 2*3 len(y)*np.log(ss_res_exp/len(y)) # k3 個(gè)參數(shù) # 選擇 AIC 更小的模型 if aic_exp aic_linear: trend_func exp_decay_func popt popt_exp else: trend_func linear_func popt popt_linear參數(shù)說明maxfev5000是必須設(shè)置的否則exp_decay_func在初始參數(shù)不佳時(shí)直接報(bào)OptimizeWarning并返回錯(cuò)誤結(jié)果aic_linear和aic_exp計(jì)算中l(wèi)en(y)16樣本量小AIC 比 BIC 更穩(wěn)健exp_decay_func中(x - 2008)是關(guān)鍵若直接用xnp.exp(-b*x)在x2023時(shí)極易 underflow 為 0導(dǎo)致擬合失敗。3.3 隨機(jī)森林殘差修正為什么不用純 RF而要“趨勢殘差”兩階段純RandomForestRegressor對(duì) 16 個(gè)樣本過擬合嚴(yán)重R2 常達(dá) 0.99但 2024 預(yù)測偏差 15%。作者采用工業(yè)界常用策略先用確定性函數(shù)捕獲宏觀趨勢再用 ML 擬合剩余波動(dòng)。# 第 52–68 行計(jì)算殘差并訓(xùn)練 RF y_trend trend_func(X.ravel(), *popt) residuals y - y_trend # 構(gòu)造 RF 特征年份 上一年殘差 上兩年殘差模擬自相關(guān) X_rf np.column_stack([ X.ravel(), np.concatenate([[0], residuals[:-1]]), # lag1 np.concatenate([[0, 0], residuals[:-2]]) # lag2 ]) y_rf residuals # 劃分訓(xùn)練集2008–2021和驗(yàn)證集2022–2023 X_train, X_test, y_train, y_test train_test_split( X_rf[:14], y_rf[:14], test_size2, shuffleFalse ) rf RandomForestRegressor(n_estimators100, random_state42) rf.fit(X_train, y_train) residual_pred rf.predict(X_test) # 預(yù)測 2022, 2023 殘差為什么這樣構(gòu)造特征lag1和lag2殘差捕捉犯罪率的慣性如某年盜竊激增次年常小幅延續(xù)shuffleFalse強(qiáng)制時(shí)序劃分避免未來信息泄露n_estimators100是平衡速度與效果的實(shí)測值200無顯著提升50則 R2 下降 0.12。3.4 2024 年預(yù)測與置信區(qū)間Bootstrap 法生成不確定性量化最終預(yù)測不是單點(diǎn)值而是帶 95% 置信區(qū)間的范圍# 第 75–92 行預(yù)測 2024 并生成置信區(qū)間 year_2024 np.array([[2024]]) trend_2024 trend_func(year_2024.ravel(), *popt) # Bootstrap重采樣殘差訓(xùn)練集生成 200 個(gè)預(yù)測 n_boot 200 pred_2024_list [] for i in range(n_boot): idx np.random.choice(len(X_train), sizelen(X_train), replaceTrue) X_boot X_train[idx] y_boot y_train[idx] rf_boot RandomForestRegressor(n_estimators100, random_statei) rf_boot.fit(X_boot, y_boot) # 構(gòu)造 2024 的 RF 特征[2024, residual_2023, residual_2022] X_2024_rf np.array([[ 2024, residuals[-1], # 2023 殘差 residuals[-2] # 2022 殘差 ]]) pred_resid rf_boot.predict(X_2024_rf)[0] pred_2024_list.append(trend_2024 pred_resid) pred_2024 np.mean(pred_2024_list) ci_lower np.percentile(pred_2024_list, 2.5) ci_upper np.percentile(pred_2024_list, 97.5) print(f2024 Total Crimes Prediction: {int(pred_2024):,} f(95% CI: {int(ci_lower):,} – {int(ci_upper):,})) # 示例輸出2024 Total Crimes Prediction: 1,842,356 (95% CI: 1,798,210 – 1,886,492)關(guān)鍵細(xì)節(jié)X_2024_rf中residuals[-1]和residuals[-2]是真實(shí)歷史殘差不是預(yù)測值——這是 Bootstrap 的正確用法random_statei保證每次重采樣獨(dú)立np.percentile(..., 2.5)比np.std()*1.96更魯棒因殘差分布非正態(tài)Shapiro 檢驗(yàn) p0.01。4. 避坑指南運(yùn)行中踩過的 4 個(gè)真實(shí)坑及解決方案4.1 現(xiàn)象curve_fit報(bào)RuntimeError: Optimal parameters not found原因exp_decay_func初始參數(shù)默認(rèn)為[1,1,1]當(dāng)x2023時(shí)np.exp(-1*2023)下溢為 0優(yōu)化器失去梯度方向。解決在curve_fit中顯式傳入合理初值并增加maxfevpopt_exp, _ curve_fit( exp_decay_func, X.ravel(), y, p0[1e6, 0.01, 1e5], # a≈1e6, b≈0.01, c≈1e5 maxfev10000 )4.2 現(xiàn)象RandomForestRegressor預(yù)測 2024 時(shí)residuals[-1]索引越界原因residuals長度為 16但residuals[-1]取的是2023年殘差而X_rf構(gòu)造時(shí)lag2用了residuals[:-2]導(dǎo)致residuals[-2]實(shí)際是2022年值——這本身沒錯(cuò)但若誤將residuals[-1]當(dāng)作2024年預(yù)測輸入就錯(cuò)了。解決嚴(yán)格區(qū)分“歷史殘差”和“預(yù)測殘差”。代碼中X_2024_rf的residuals[-1]是真實(shí)2023殘差正確若要預(yù)測2025需先用 RF 預(yù)測2024殘差再代入X_2025_rf不能直接索引。4.3 現(xiàn)象plotly圖表中文標(biāo)簽顯示為方塊原因plotly默認(rèn)字體不支持 UTF-8 中文且未指定locale。解決在繪圖前全局設(shè)置import plotly.io as pio pio.kaleido.scope.default_format png pio.templates.default plotly_white # 強(qiáng)制中文字體需系統(tǒng)有 SimHei 或 Noto Sans CJK import plotly.graph_objects as go fig.update_layout( fontdict(familySimHei, sans-serif), title_fontdict(size16) )4.4 現(xiàn)象1-Crime Trends Analysis.py中seasonal_decompose報(bào)ValueError: You must specify a period or x must be a pandas object with a DatetimeIndex原因seasonal_decompose要求時(shí)間序列頻率明確但本數(shù)據(jù)是年度數(shù)據(jù)非月度/季度period1不被接受。解決年度數(shù)據(jù)不適用seasonal_decompose應(yīng)改用rolling(3).mean()觀察平滑趨勢或直接刪除該行原腳本中此行被注釋但新手易誤啟。5. 二次開發(fā)實(shí)戰(zhàn)如何把“俄羅斯犯罪預(yù)測”快速遷移到你的本地城市數(shù)據(jù)5.1 數(shù)據(jù)適配三步法從2008-2023.csv到你的2015-2023_cityX.csv遷移核心是字段對(duì)齊而非重寫算法。假設(shè)你有某市 2015–2023 年盜竊案數(shù)據(jù)CSV 格式只需三步Step 1列名標(biāo)準(zhǔn)化你的文件可能叫theft_cases.csv含列year, theft_count, pop_density, avg_income。用 Pandas 重命名并補(bǔ)全必需列df_city pd.read_csv(theft_cases.csv) df_city df_city.rename(columns{ year: Year, theft_count: Theft, # 必須與原數(shù)據(jù)子類名一致 pop_density: Population_density_per_km2 # 即使原數(shù)據(jù)無此列也保留 }) # 補(bǔ)全原數(shù)據(jù)中有的但你沒有的列填均值或 0 for col in [Total_crimes, Assault, Fraud, Homicide]: if col not in df_city.columns: df_city[col] 0 # 或 df_city[Theft].mean() * 1.2 df_city[Total_crimes] df_city[Theft] # 簡化總犯罪數(shù)盜竊數(shù)Step 2時(shí)間范圍對(duì)齊原腳本假設(shè) 16 年數(shù)據(jù)2008–2023你只有 9 年2015–2023。修改3.py中X和y切片# 原代碼第 13 行 # X df[[Year]].values # y df[Total_crimes].values # 改為 start_year, end_year 2015, 2023 mask (df[Year] start_year) (df[Year] end_year) X df[mask][[Year]].values y df[mask][Total_crimes].valuesStep 3調(diào)整模型復(fù)雜度樣本量從 16 降到 9必須降低RandomForestRegressor復(fù)雜度否則過擬合# 原代碼 # rf RandomForestRegressor(n_estimators100, random_state42) # 改為n_estimators 與樣本量正相關(guān) n_est max(20, min(100, int(10 * len(X)))) # 9 樣本 → 90但上限 100 rf RandomForestRegressor(n_estimatorsn_est, max_depth3, random_state42)5.2 新增變量接入如何把“地鐵開通年份”作為結(jié)構(gòu)突變點(diǎn)加入趨勢擬合原腳本只用Year但現(xiàn)實(shí)中政策事件如 2018 年某市開通首條地鐵會(huì)改變犯罪率斜率。我們在exp_decay_func基礎(chǔ)上增加分段邏輯# 在 3.py 中新增函數(shù)替換原 trend_func def piecewise_linear(x, a1, b1, a2, b2, break_year): x: 年份數(shù)組如 [2015,2016,...,2023] break_year: 結(jié)構(gòu)突變點(diǎn)如 2018 返回分段線性趨勢2015-2017 用 a1*xb12018-2023 用 a2*xb2 y np.zeros_like(x) mask_before x break_year mask_after x break_year y[mask_before] a1 * x[mask_before] b1 y[mask_after] a2 * x[mask_after] b2 return y # 在擬合部分第 37 行后加入 popt_piece, _ curve_fit( piecewise_linear, X.ravel(), y, p0[-1000, 2e6, -500, 1.8e6, 2018], # 初值斜率、截距、斷點(diǎn) bounds([-5000, 1e6, -5000, 1e6, 2015], [0, 5e6, 0, 5e6, 2023]) # 斷點(diǎn)約束在數(shù)據(jù)范圍內(nèi) )驗(yàn)證方法對(duì)比piecewise_linear和linear_func的 AIC若前者低 10說明斷點(diǎn)顯著。5.3 部署為 API用 Flask 封裝預(yù)測端點(diǎn)5 行核心代碼不想每次開終端跑封裝成 HTTP 接口前端傳年份即返回預(yù)測# 新建 app.py from flask import Flask, request, jsonify import joblib # 需 pip install joblib import numpy as np app Flask(__name__) # 加載訓(xùn)練好的模型需先在 3.py 中加joblib.dump(rf, rf_model.pkl) rf_model joblib.load(rf_model.pkl) trend_params np.load(trend_params.npz) # 保存 popt 和 trend_func 名 app.route(/predict, methods[POST]) def predict(): year int(request.json[year]) trend_val piecewise_linear(np.array([year]), *trend_params[popt]) # 構(gòu)造 RF 特征此處簡化實(shí)際需查歷史殘差 X_input np.array([[year, 0, 0]]) # lag1, lag2 設(shè)為 0首次預(yù)測 resid_pred rf_model.predict(X_input)[0] return jsonify({ year: year, prediction: int(trend_val resid_pred), source: Russia-crime-pipeline-v2 }) if __name__ __main__: app.run(host0.0.0.0:5000)運(yùn)行python app.py然后curl -X POST http://localhost:5000/predict -H Content-Type: application/json -d {year:2024}即得 JSON 響應(yīng)。從那以后我每次接手新地域數(shù)據(jù)都強(qiáng)制走一遍「列名對(duì)齊 → 時(shí)間切片 → 樣本量重估n_estimators」三步再動(dòng)代碼。少一次就多一次ValueError: Input contains NaN的深夜排查。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取