27123125 python

"""
==============================================================
基于随机森林的农户农地经营权抵押贷款信用风险识别
Random Forest Model for Farmland Mortgage Loan Credit Risk
==============================================================
作者:董轩羽 (27123125)
课程:智能风控
参考文献:
  彭艳玲等(2025)系统工程理论与实践
  汪寿阳等(2025)农业经济问题
  张珩等(2018)中国农村经济
==============================================================
"""

import os, logging
import numpy as np
import pandas as pd
from sklearn.ensemble import RandomForestClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split, cross_val_score, StratifiedKFold
from sklearn.metrics import (roc_auc_score, accuracy_score, recall_score,
                             precision_score, f1_score, confusion_matrix, roc_curve)
from sklearn.preprocessing import StandardScaler
import warnings
warnings.filterwarnings('ignore')

# ─── 日志配置 ───────────────────────────────
LOG_FILE = '/kaggle/working/dxy_rf_log_20260619.txt'
os.makedirs(os.path.dirname(LOG_FILE), exist_ok=True)
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s  %(levelname)s  %(message)s',
    datefmt='%Y-%m-%d %H:%M:%S',
    handlers=[logging.FileHandler(LOG_FILE, 'w', 'utf-8'), logging.StreamHandler()]
)
log = logging.getLogger()

log.info("=" * 68)
log.info("  智能风控课程作业  |  随机森林农户信用风险识别模型")
log.info("  作者:董轩羽  学号:27123125")
log.info("=" * 68)

# ═══════════════════════════════════════════════════════════════
# 1. 生成模拟调查数据(宁夏/重庆/四川 三省区农户)
# ═══════════════════════════════════════════════════════════════
log.info("─── 步骤 1:生成模拟调查数据 ──────────────────────────────────")
DATA_FILE = '/kaggle/working/dxy_farmland_credit_data.csv'

SEED = 42
rng = np.random.default_rng(SEED)
N = 840

region        = rng.choice([1, 2, 3], N, p=[0.38, 0.32, 0.30])          # 1=宁夏 2=重庆 3=四川
age           = rng.normal(47, 9, N).clip(24, 74).astype(int)
edu           = rng.choice([6,9,12,15], N, p=[0.18,0.52,0.22,0.08]) + rng.integers(-1,2,N)
edu           = edu.clip(4, 16)
family_size   = rng.integers(2, 8, N)
land_base     = np.where(region==1, 9.5, np.where(region==2, 4.8, 5.9))
land_area     = (rng.exponential(land_base) + 1.0).clip(0.5, 55.0)
agri_income   = (rng.exponential(3.0, N) * (0.4 + 0.6*land_area/land_area.mean())).clip(0.2, 28)
loan_amount   = rng.choice([1,2,3,5,8,10,15,20],N,p=[0.08,0.15,0.22,0.25,0.15,0.09,0.04,0.02]) \
                + rng.uniform(-0.4, 0.4, N)
loan_amount   = loan_amount.clip(0.5, 20)
insured       = rng.binomial(1, 0.47, N)
dist_bank     = rng.exponential(11.5, N).clip(1, 75)
loan_term     = rng.choice([6,12,24,36], N, p=[0.12,0.68,0.15,0.05])
other_debt    = rng.binomial(1, 0.27, N)
non_agri      = rng.exponential(2.8, N).clip(0, 18)

# 违约概率(含非线性项:阈值效应 + 交互项)
def sigmoid(x): return 1 / (1 + np.exp(-x))

Z = (- 0.60*(agri_income/agri_income.std())
     + 0.45*(loan_amount/loan_amount.std())
     - 0.38*insured
     - 0.32*(land_area/land_area.std())
     + 0.20*(family_size/family_size.std())
     - 0.18*(edu/edu.std())
     + 0.15*(dist_bank/dist_bank.std())
     + 0.12*other_debt
     - 0.10*(non_agri/non_agri.std())
     # 非线性:低收入×高贷款 的交互项
     + 0.25*((agri_income < agri_income.mean()*0.6).astype(float)
             * (loan_amount > loan_amount.mean()*1.4).astype(float))
     + rng.normal(0, 0.6, N))

prob = sigmoid(Z)
# 以 top-10% 阈值强制控制违约率为 10%
threshold = np.percentile(prob, 90)
default = (prob >= threshold).astype(int)

df = pd.DataFrame({
    '地区':        region,
    '户主年龄':    age,
    '受教育程度':  edu,
    '家庭人口':    family_size,
    '耕地面积':    land_area.round(2),
    '农业收入':    agri_income.round(2),
    '贷款金额':    loan_amount.round(2),
    '是否参农险':  insured,
    '到银行距离':  dist_bank.round(1),
    '贷款期限':    loan_term,
    '是否有其他负债': other_debt,
    '非农收入':    non_agri.round(2),
    '违约':        default
})
df.to_csv(DATA_FILE, index=False, encoding='utf-8-sig')
log.info(f"  样本数:{len(df)} 条  |  特征数:{df.shape[1]-1} 个")
log.info(f"  样本违约率:{df['违约'].mean()*100:.2f}%(违约 {df['违约'].sum()} 条)")
log.info(f"  地区分布:宁夏={( df['地区']==1).sum()}  重庆={(df['地区']==2).sum()}  四川={(df['地区']==3).sum()}")
log.info(f"  数据文件:./数据/dxy_farmland_credit_data.csv")

# ═══════════════════════════════════════════════════════════════
# 2. 描述性统计
# ═══════════════════════════════════════════════════════════════
log.info("─── 步骤 2:描述性统计 ────────────────────────────────────────")
FEAT = ['农业收入','贷款金额','是否参农险','耕地面积','家庭人口',
        '受教育程度','到银行距离','贷款期限','是否有其他负债','非农收入','户主年龄']

log.info(f"  {'变量':<12}  {'均值':>7}  {'标准差':>7}  {'违约组均值':>10}  {'正常组均值':>10}")
log.info(f"  {'-'*60}")
for c in FEAT:
    mu  = df[c].mean(); sd = df[c].std()
    md  = df[df['违约']==1][c].mean()
    mn  = df[df['违约']==0][c].mean()
    log.info(f"  {c:<12}  {mu:>7.2f}  {sd:>7.2f}  {md:>10.2f}  {mn:>10.2f}")

# ═══════════════════════════════════════════════════════════════
# 3. 数据划分
# ═══════════════════════════════════════════════════════════════
log.info("─── 步骤 3:训练集 / 测试集划分(75% / 25%,分层抽样)──────────")
X, y = df[FEAT], df['违约']
X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.25, random_state=SEED, stratify=y)
log.info(f"  训练集:{len(X_tr)} 条(违约 {y_tr.sum()},{y_tr.mean()*100:.1f}%)")
log.info(f"  测试集:{len(X_te)} 条(违约 {y_te.sum()},{y_te.mean()*100:.1f}%)")

# ═══════════════════════════════════════════════════════════════
# 4. 模型训练
# ═══════════════════════════════════════════════════════════════
log.info("─── 步骤 4:模型训练 ──────────────────────────────────────────")

# ── 随机森林 ──
log.info("  [随机森林] 开始训练 (n_estimators=200, max_depth=10, class_weight='balanced')")
rf = RandomForestClassifier(
    n_estimators=200, max_depth=10, min_samples_leaf=4,
    max_features='sqrt', class_weight='balanced', random_state=SEED, n_jobs=-1
)
rf.fit(X_tr, y_tr)
cv_rf = cross_val_score(rf, X_tr, y_tr, cv=StratifiedKFold(5, shuffle=True, random_state=SEED),
                        scoring='roc_auc')
log.info(f"  [随机森林] 训练完成 | 5折CV AUC:{cv_rf.mean():.4f} ± {cv_rf.std():.4f}")

# ── Logistic 回归 ──
log.info("  [Logistic回归] 开始训练 (C=1.0, class_weight='balanced')")
scaler = StandardScaler()
X_tr_s = scaler.fit_transform(X_tr);  X_te_s = scaler.transform(X_te)
lr = LogisticRegression(C=1.0, class_weight='balanced', max_iter=500, random_state=SEED)
lr.fit(X_tr_s, y_tr)
cv_lr = cross_val_score(lr, X_tr_s, y_tr, cv=StratifiedKFold(5, shuffle=True, random_state=SEED),
                        scoring='roc_auc')
log.info(f"  [Logistic回归] 训练完成 | 5折CV AUC:{cv_lr.mean():.4f} ± {cv_lr.std():.4f}")

# ═══════════════════════════════════════════════════════════════
# 5. 模型评估
# ═══════════════════════════════════════════════════════════════
log.info("─── 步骤 5:测试集模型性能评估 ────────────────────────────────")

def eval_model(model, Xtest, ytest, name, scaler=None):
    Xe = scaler.transform(Xtest) if scaler else Xtest
    yp = model.predict_proba(Xe)[:,1]
    yc = model.predict(Xe)
    auc = roc_auc_score(ytest, yp)
    acc = accuracy_score(ytest, yc)
    rec = recall_score(ytest, yc)
    pre = precision_score(ytest, yc)
    f1  = f1_score(ytest, yc)
    fpr,tpr,_ = roc_curve(ytest, yp)
    ks  = (tpr - fpr).max()
    cm  = confusion_matrix(ytest, yc)
    log.info(f"\n  ── {name} ──────────────────────────")
    log.info(f"  AUC       = {auc:.4f}")
    log.info(f"  准确率     = {acc:.4f}   ({acc*100:.1f}%)")
    log.info(f"  违约召回率 = {rec:.4f}   ({rec*100:.1f}%)")
    log.info(f"  精确率     = {pre:.4f}")
    log.info(f"  F1 分数    = {f1:.4f}")
    log.info(f"  KS 统计量  = {ks:.4f}")
    log.info(f"  混淆矩阵:TN={cm[0,0]}  FP={cm[0,1]}  FN={cm[1,0]}  TP={cm[1,1]}")
    return dict(name=name, AUC=auc, 准确率=acc, 违约召回率=rec, 精确率=pre, F1=f1, KS=ks, prob=yp)

rf_r = eval_model(rf, X_te, y_te, "随机森林 (Random Forest)")
lr_r = eval_model(lr, X_te, y_te, "Logistic 回归", scaler=scaler)

# ═══════════════════════════════════════════════════════════════
# 6. 特征重要性
# ═══════════════════════════════════════════════════════════════
log.info("\n─── 步骤 6:特征重要性分析 (Gini 不纯度降低量) ───────────────")
imp = pd.Series(rf.feature_importances_, index=FEAT).sort_values(ascending=False)
log.info(f"  {'排名':>4}  {'特征':<14}  {'重要性':>8}")
log.info(f"  {'-'*34}")
for r,(f,v) in enumerate(imp.items(),1):
    log.info(f"  {r:>4}  {f:<14}  {v:>8.4f}")

# ═══════════════════════════════════════════════════════════════
# 7. Credit Risk+ 期望损失测量
# ═══════════════════════════════════════════════════════════════
log.info("\n─── 步骤 7:Credit Risk+ 期望损失测量 ──────────────────────────")
pd_arr  = rf.predict_proba(X_te)[:,1]
LGD     = 0.65
ead_arr = X_te['贷款金额'].values
el_arr  = pd_arr * LGD * ead_arr
total_ead = ead_arr.sum()
total_el  = el_arr.sum()
el_rate   = total_el / total_ead * 100
shock_el  = pd_arr * (LGD * 1.35) * ead_arr
shock_inc = (shock_el.sum() - total_el) / total_el * 100

log.info(f"  总风险敞口(EAD)      : {total_ead:.2f} 万元")
log.info(f"  违约损失率(LGD)假设  : {LGD:.0%}(农地流转变现难,参照行业均值)")
log.info(f"  组合期望损失(EL)     : {total_el:.2f} 万元")
log.info(f"  期望损失率(EL/EAD)   : {el_rate:.2f}%")
log.info(f"  [压力测试] 极端气候冲击(LGD×1.35={LGD*1.35:.0%}):期望损失增幅 +{shock_inc:.1f}%")

# ═══════════════════════════════════════════════════════════════
# 8. 汇总
# ═══════════════════════════════════════════════════════════════
log.info("\n─── 步骤 8:结果汇总 ──────────────────────────────────────────")
log.info("\n  表2  模型性能对比(测试集)")
log.info(f"  {'指标':<12}  {'随机森林':>10}  {'Logistic':>10}  {'差值':>8}  {'提升幅度':>9}")
log.info(f"  {'-'*56}")
for k in ['AUC','准确率','违约召回率','F1','KS']:
    rv, lv = rf_r[k], lr_r[k]
    log.info(f"  {k:<12}  {rv:>10.4f}  {lv:>10.4f}  {rv-lv:>+8.4f}  {(rv-lv)/lv*100:>+8.1f}%")

log.info("\n  表3  特征重要性前7位")
log.info(f"  {'排名':>4}  {'特征变量':<14}  {'重要性得分':>10}  {'经济含义'}")
log.info(f"  {'-'*60}")
meanings = {'农业收入':'还款能力核心指标','贷款金额':'资金需求与偿债压力',
            '耕地面积':'抵押物价值基础','是否参农险':'风险保障程度',
            '家庭人口':'家庭负担水平','受教育程度':'风险认知与管理能力',
            '到银行距离':'金融服务可及性','非农收入':'收入多元化程度',
            '户主年龄':'经验与风险偏好','贷款期限':'还款期限结构','是否有其他负债':'债务负担'}
for r,(f,v) in enumerate(imp.head(7).items(),1):
    log.info(f"  {r:>4}  {f:<14}  {v:>10.4f}  {meanings.get(f,'—')}")

log.info("\n  Credit Risk+ 损失测量汇总")
log.info(f"  基准情景 EL      = {total_el:.2f} 万元  (EL率 {el_rate:.2f}%)")
log.info(f"  极端情景 EL      = {shock_el.sum():.2f} 万元  (增幅 +{shock_inc:.1f}%)")

log.info("\n" + "=" * 68)
log.info("  模型运行完成")
log.info("=" * 68)

print(f"\n✓ 全部步骤运行完成")
print(f"  运行日志:./运行日志/dxy_rf_log_20260619.txt")
print(f"  模拟数据:./数据/dxy_farmland_credit_data.csv")