基于LendingClub数据的信贷违约预测:从特征工程到模型落地

发布时间:2026/10/11 16:04:20

基于LendingClub数据的信贷违约预测:从特征工程到模型落地 简介基于LendingClub公开信贷数据构建智能违约预测模型的项目资料包面向金融风控入门者、数据建模学习者及信贷机构风控人员解决如何利用历史借贷记录评估借款人信用风险的问题。包内共5个文件包含可运行的Python建模脚本、完整的Jupyter Notebook分析流程、说明文档txt/docx及README压缩包仅47KB轻量便于快速上手。已有59人学习。内容覆盖数据清洗与预处理、特征工程、逻辑回归/随机森林/梯度提升等算法训练以及准确率、精确率、召回率、F1分数等模型评估与调优要点可帮助读者从原始LendingClub数据出发完整走通违约预测建模流程理解信贷风控场景中的特征选择与模型落地思路。附赠文档还梳理了数据隐私、公平性与合规性等实际部署注意事项兼具实战性与行业参考价值。1. LendingClub违约预测项目这套公开信贷数据能做出什么如果你手头刚好有一份LendingClub的信贷历史数据zip包里面躺着几十万条贷款记录每条都记录着借款人当时的信用评分、贷款金额、利率、就业年限和年收入你最想问的问题是哪些人会把钱借走不还。这正是智能违约预测模型要回答的事——基于历史贷款行为训练二分类模型在放款前估算每笔贷款的违约概率再转成风控决策可用的分数或名单。这类项目最适合信贷风控建模从业者和转行数据分析师难度适中却完整覆盖数据清洗、特征工程、模型训练、效果评估到业务化解释全部环节是技术栈里很值得投入的一条主线。2. 从zip数据包到干净数据表字段梳理、标签定义与加载代码2.1 数据包结构和关键字段哪些是强特征哪些是事后变量一份典型的LendingClub数据包解压后是1到2张csv主表每行一笔贷款每列是放款时登记的借款人信息和贷款信息。我先按经验把最常用的字段挑出来这些在后续建模中几乎都能用上字段含义在模型中的角色loan_amnt贷款金额美元数值特征int_rate年化利率风险定价直接体现强特征annual_inc借款人申报年收入数值特征参与比率构造emp_length就业年限数值特征源文件为文本fico_range_low/highFICO信用评分区间取中点作为强特征dti负债收入比强特征revol_util循环贷使用率数值特征表达资金压力delinq_2yrs近2年逾期次数强特征inq_last_6mths近6个月信用查询次数数值特征home_ownership住房状态按揭/租房/自有类别特征loan_status贷款当前状态标签来源不参与特征有一个新手几乎必踩的认知坑loan_status虽然是一个“状态”字段但它只能作为标签不能作为特征。与之类似的还有recoveries催收回款、total_pymnt累计已还金额、last_pymnt_d最后还款日这类字段——它们的取值只有贷款发生之后才存在放进特征矩阵就是典型的未来信息泄漏。建模的第一步先把这类字段封存掉后面第5章还会单独讲它造成的翻车现场。2.2 解压与加载把zip包落到本地再读入Pandas常见做法是先把zip包解压到本地工作目录再用pandas读取不要在代码里反复打开zip对象读csv大文件每次解压都要付出额外的时间。如果数据包本身就是几GB解压一次、后续全走csv是性价比最高的路线。import zipfile import pandas as pd # 1. 先看zip包内部包含哪些文件 with zipfile.ZipFile(lendingclub_data.zip, r) as z: print(z.namelist()) # 常见内容: [loan.csv, rejectStats.csv] z.extractall(./lc_data) # 2. 读取主表 loan pd.read_csv(./lc_data/loan.csv, low_memoryFalse) print(记录数:, loan.shape[0], 字段数:, loan.shape[1])这段代码有两个值得说明的细节。low_memoryFalse是强制pandas在读取时对全部行做统一的类型推断而不是分块推断——默认的分块推断在大csv里经常把全是数字的列误判成object越读到后面越容易出现类型不一致的坑。namelist()先看包内文件列表是检查数据包是否完整的快速手段如果解压时报错提示“could not find eocd”说明zip文件在传输中被截断了不要迷信各种修复工具直接重新下载或让数据提供方重新打包。2.3 标签定义Fully Paid与Charged Off才构成干净的二分类目标loan_status字段的取值远不止两个常见的有Fully Paid已还清、Charged Off核销即确定坏账、Default违约、Current还款中、Late逾期、In Grace Period宽限期。其中只有前两类是已终结状态其余都在进行中拿到手的第一件事就是把进行中的样本剔除。def make_target(status): if status Fully Paid: return 0 if status in (Charged Off, Default): return 1 return None # Current / Late / In Grace Period 等状态剔除 loan[target] loan[loan_status].map(make_target) loan loan[loan[target].notna()].copy() print(loan[target].value_counts(normalizeTrue))为什么不把Late当成违约因为存在两种结局宽限期结束后可能转成Fully Paid也可能变成Charged Off——当下无法定性。Default是后期数据里对Charged Off的替代叫法两个值都要认作正类。value_counts(normalizeTrue)直接显示违约占比如果算出来在10%上下这是P2P信贷数据的常见水平后面的类别不平衡处理就要针对性做了。到这里带标签的干净主表已经有了。下一步进入特征工程。3. 特征工程挖掘信用评分、利率、年收入与就业年限背后的违约信号3.1 文本型字段清洗就业年限和循环贷使用率转数值csv里的字段天然带一些展示格式。emp_length是10 years、1 year、5 years这类字符串revol_util是带百分号的字符串。不清洗干净模型根本没法直接读取这个过程在信贷数据里占掉整个预处理时间的将近一半。def parse_emp_length(s): if pd.isna(s): return None s str(s).strip().lower() if s n/a: return None if in s: return int(s.split()[0]) # 10 years - 10 if in s: return 0 # 1 year - 0 return int(s.split()[0]) # 5 years - 5 loan[emp_length_num] loan[emp_length].map(parse_emp_length) loan[revol_util_rate] ( loan[revol_util].str.replace(%, , regexFalse).astype(float) ) / 100.0parse_emp_length三个分支对应三种边界场景带号的要取加号前的数字带号的直接映射到0正常值只取空格前第一个单词。revol_util去掉百分号后整体除以100是为了和dti保持同一种量纲都是小数比率避免模型里同时出现一个0~100和一个0~1的字段减少逻辑回归在尺度上的负担。3.2 构造比率型特征贷款负担与现金流压力处理完的绝对金额字段不该直接用。年收入20万美元的人贷5000块属于消费行为年收入3万美元的人贷5000块可能就是赌一把同样的金额承载的风险完全不同。所以特征工程第一原则是造比率loan[fico] ( loan[fico_range_low] loan[fico_range_high] ) / 2.0 loan[loan_to_income] loan[loan_amnt] / loan[annual_inc] loan[installment_to_income] loan[installment] / loan[annual_inc]fico取区间上下限的中点这是最稳妥的口径上下限通常只差20分左右取哪边对结果影响都微乎其微。loan_to_income是贷款本金对年收入的倍数超过0.4的借款人风险偏好明显激进installment_to_income是月供对年收入的压力比现金流越紧违约概率越高。注意这里用的是年收入做分母严格说月供应该对月收入但因为同一个常数缩放对所有样本一致树模型的分裂不受影响逻辑回归的系数也只需要在解释时同步换算一般不会出问题。3.3 类别特征编码评级字段的序数化与住房状态的稀有类合并LendingClub自带的grade字段从A到G共7档是平台基于借款人信用、收入、历史行为给的综合评级相当于一个已经抽象好的强特征。它有天然的排序语义做序数编码比one-hot更合适。grade_map {A: 1, B: 2, C: 3, D: 4, E: 5, F: 6, G: 7} loan[grade_num] loan[grade].map(grade_map) loan[home_ownership_clean] loan[home_ownership].replace( {NONE: OTHER, ANY: OTHER} # 稀有类别合并 )grade_num直接保留了评级顺序让线性模型能学到“评级每低一档违约风险升高一截”的单调关系。home_ownership里NONE和ANY样本量太少单独成类会让模型学到少数样本的偶然模式合并进OTHER再用get_dummies展开可以有效压制过拟合。类似的高基数类别字段还有addr_state借款人所在州50个取值不适合直接one-hot除非先用频数编码或目标编码做压缩否则建议直接放弃该字段——它带来的增益通常撑不起维度爆炸的成本。3.4 缺失值策略高缺失字段用缺失标志而不是假装它不缺失信贷数据的缺失大多不是随机发生的。最典型的例子是mths_since_last_delinq距离上次逾期过去的月数一个从未发生过逾期的借款人这个字段天然为空——缺失在这里恰恰代表着干净的信用记录。如果无脑用中位数填充等于把这一批优质客户强行赋予“逾期过但记不清多久”的假身份。字段典型缺失率处理方式理由emp_length_num约3%中位数填充低缺失删样本不划算annual_inc低于1%中位数填充接近无缺失revol_util_rate5%~15%中位数填充 缺失标志缺失通常对应无循环贷账户mths_since_last_delinq20%以上仅生成缺失标志不填充缺失代表无逾期历史交给树模型处理缺失对应代码如下loan[emp_length_num] loan[emp_length_num].fillna( loan[emp_length_num].median() ) loan[annual_inc] loan[annual_inc].fillna( loan[annual_inc].median() ) loan[revol_util_rate] loan[revol_util_rate].fillna( loan[revol_util_rate].median() ) # 缺失标志必须在填充之前生成顺序不能反 loan[revol_util_missing] loan[revol_util].isna().astype(int) loan[delinq_missing] loan[mths_since_last_delinq].isna().astype(int)这里反复强调顺序问题缺失标志必须在填充之前生成fillna之后再查isna就全是0了。这个低级错误在实际项目中出现过不止一次排错时很难一眼看出来。mths_since_last_delinq缺失率超过20%不填充、只保留缺失标志让XGBoost这类原生支持缺失值的模型自己走一条“无逾期历史”的分支比强行填一个假数值更有信息量如果后面切到逻辑回归再根据当时的情况单独决定填充方案。特征组装完成可以把宽表交给模型了feat_cols [ loan_amnt, int_rate, installment_to_income, fico, dti, emp_length_num, annual_inc, loan_to_income, revol_util_rate, revol_util_missing, delinq_2yrs, open_acc, inq_last_6mths, grade_num, ] X pd.get_dummies(loan[feat_cols], columns[home_ownership_clean]) y loan[target]4. 模型选型与训练逻辑回归和XGBoost的落地对比4.1 为什么先跑逻辑回归可解释的基线与系数体检信贷领域的建模天然要面对监管和业务解释的压力一套“为什么拒绝这笔贷款”的答案几乎是从业者的必修课。因此第一步永远先跑逻辑回归不是为了拿它做线上主力而是用它建立基线AUC大致多少、每个特征的系数方向是否合理、哪些特征存在明显的共线性。from sklearn.model_selection import train_test_split from sklearn.pipeline import make_pipeline from sklearn.preprocessing import StandardScaler from sklearn.linear_model import LogisticRegression # 这里先按随机划分演示流程真实信贷场景强制要求按时间切分见5.3 X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42, stratifyy ) lr_pipe make_pipeline( StandardScaler(), LogisticRegression(C1.0, class_weightbalanced, max_iter1000) ) lr_pipe.fit(X_train, y_train) pred_lr lr_pipe.predict_proba(X_test)[:, 1]StandardScaler放在管线里是因为逻辑回归的损失函数对特征尺度敏感loan_amnt动辄上万、dti只有零点几不标准化会让梯度优化过程极其痛苦。C是正则强度的倒数C1.0是sklearn默认值不代表最优后续调参以验证集AUC为准。class_weightbalanced是最关键的一步它按类别比例反向加权让模型不再一味把样本预测为多数类这是在模型层面处理类别不平衡的正道。4.2 XGBoost树模型在信贷数据上的关键参数逻辑回归对特征线性组合的表达力有限信贷违约往往由非线性交互驱动比如“低收入高循环贷使用率近期多次查询”的组合才是危险信号。这时换XGBoost是行业里最常见的选择。import xgboost as xgb xgb_model xgb.XGBClassifier( n_estimators400, max_depth4, learning_rate0.05, subsample0.8, colsample_bytree0.8, scale_pos_weight8.0, # 负样本约8倍于正样本时设为8 eval_metricauc, early_stopping_rounds30, n_jobs-1 ) xgb_model.fit( X_train, y_train, eval_set[(X_test, y_test)], verboseFalse )参数按信贷场景的实际经验逐一说清楚。max_depth4而不是默认的6信用数据的有效信号大多是低阶交互深度过大只会把训练集里的噪声一并记住4层在效果和泛化之间最稳。learning_rate取0.05配合400棵树比默认0.3100棵树更抗过拟合代价是训练时间稍长这里的时间投入完全值得。subsample和colsample_bytree都取0.8双重随机采样让每一棵树看到的都是数据的随机切面集成出来的方差更小。scale_pos_weight是类别不平衡的树模型解法数值取负样本数除以正样本数的比值如果样本统计出来是9:1就填9这里是8.0则代表负样本约为正样本的8倍。early_stopping_rounds30让模型在验证集AUC连续30轮不再提高时提前停下后面那几百棵树不用白跑。4.3 两种模型在测试集上的真实差距有了两个模型的预测概率直接对比核心指标from sklearn.metrics import roc_auc_score, precision_score, recall_score for name, pred in [ (LogisticRegression, pred_lr), (XGBoost, xgb_model.predict_proba(X_test)[:, 1]), ]: auc roc_auc_score(y_test, pred) # 先按默认阈值0.5看真实业务不会用0.5见第5章 pred_label (pred 0.5).astype(int) precision precision_score(y_test, pred_label) recall recall_score(y_test, pred_label) print(f{name}: AUC{auc:.4f}, Precision{precision:.4f}, Recall{recall:.4f})某个典型结果可能是逻辑回归AUC约0.70XGBoost约0.73二者差距不算大但XGBoost在Recall上的优势更明显。别急着只看AUC用默认阈值0.5切出来的Precision大概率很低——这不是模型坏了而是类别不平衡下0.5阈值本身就不合理。真实风控场景里很少存在“50%概率该拒”的天然分界通常的做法是设定一个拒绝率比如只放给预测分数最高的70%人群再去看在这一档拒绝率下拦截了多少坏客户这部分逻辑会在第5章展开。值得补充的是如果这两类模型的AUC差距超过0.05优先怀疑第5章的特征泄漏或多重共线性而不是模型调参不够努力。模型的边际提升经常是玄学特征上的硬伤才是决定天花板的东西。5. 智能违约预测的常见问题与排查五个让模型翻车的现场5.1 违约样本太少默认阈值0.5让模型形同虚设现象XGBoost训练过程一切正常验证集AUC有0.72但按0.5阈值看Precision只有个位数甚至报出0。业务方拿去一用一份客户也拒不掉。原因正样本违约率只有10%左右模型学到的先验就是“绝大多数人不会违约”给所有样本输出的概率都压在0.5以下。解决调整scale_pos_weight或class_weight让模型本身的输出分布更贴合业务先验。另一个更业务化的做法是抛弃固定阈值改用分位数import numpy as np threshold np.quantile(pred_xgb, 0.7) # 拒绝概率最高的30% y_rule (pred_xgb threshold).astype(int)这条规则的含义是无论概率绝对值是多少只拒绝模型认为最危险的30%。它绕开了不平衡对阈值的影响也贴合风控“给定拒绝率下的召回率”这种业务语言。5.2 特征泄漏模型偷看了未来验证集再漂亮也白搭现象验证集AUC到0.93一套流程跑下来无比顺畅上线或者做时间外验证之后AUC直接掉到0.75。原因特征矩阵里混入了贷款发放之后才产生的字段。最常见的是recoveries、total_pymnt、last_pymnt_d、collection_recovery_fee某些数据版本里甚至包含mths_since_last_delinq的“未来版本”——发放日之后的逾期记录也被算了进来。解决严格按“发放日当时可得”筛选特征规则可以做成一次性的字段体检drop_patterns (last_, total_, recoveries, collection, mths_since_last, hardship_) leak_cols [c for c in X.columns if c.startswith(drop_patterns)] X X.drop(columnsleak_cols)这个过滤在真实项目里就是一条血泪经验我见过的许多“线下0.92、线上0.72”的案例最后查出来都是这一类问题。树模型不会提醒你它只会安静地把泄漏字段当作最重要的分裂点。注意凡是字段名带last_、total_、recoveries、collection前缀的先默认它可疑逐个确认是发放前还是发放后产生的信息再决定去留。5.3 随机划分导致的测评乐观偏差现象随机train_test_split跑出AUC 0.88改成按月份时间切分后只有0.80。原因LendingClub数据里同一个借款人可能有多笔贷款随机划分会把同一人的贷款一半放进训练集、一半放进验证集模型等于见过“这个人”再考“这个人”此外宏观经济周期是随时间走的随机划分把不同时间段的宏观风险差异也打散了。解决始终用issue_d做时间切分loan loan.sort_values(issue_d).reset_index(dropTrue) cut_idx int(len(loan) * 0.8) X_train_ts, X_test_ts X.iloc[:cut_idx], X.iloc[cut_idx:] y_train_ts, y_test_ts y.iloc[:cut_idx], y.iloc[cut_idx:]时间切分的原理是模拟真实放款场景用2020年之前的历史训练预测2020年之后发放的贷款。它是信贷模型唯一能被业务接受的验证方式随机划分的结果只能当参考指标不能作为上线依据。5.4 用准确率汇报模型效果最容易交出漂亮但没用的报表现象汇报材料里写着“准确率91%”业务方看了很高兴打开混淆矩阵才发现违约客户几乎全部被预测成了正常客户。原因类别不平衡下全部预测为0就有约90%的准确率准确率这个指标对低频事件完全没有鉴别力。解决换成AUC-ROC做整体区分力评估再用“给定拒绝率下的召回率”做业务评估。风控场景最常用的口径是拒绝30%的客户时能拦下多大比例的违约这比任何单一默认阈值下的Precision都更能指导决策。from sklearn.metrics import roc_auc_score # 拒绝率30%时看违约样本的召回率 thr np.quantile(pred_xgb, 0.7) reach recall_score(y_test, (pred_xgb thr).astype(int)) print(fAUC{auc:.4f}, 拒绝30%时违约召回率{reach:.4f})5.5 缺失值填充造成的假信号现象某字段缺失率高达35%用全局均值填充后它在特征重要性里排进前三业务上看不出任何合理理由。原因填充操作把大量原本缺失的样本强行标到了同一个值上模型学到的其实是“这个字段是否有值”这件事而不是字段本身。缺失率高时纯填充的信息量约等于噪声。解决分情况处理。缺失率低于5%用中位数填充高于20%的字段保留缺失标志并将填充值设为-1或单独分组让模型自行区分“有记录且正常”和“没记录”两条路径。对XGBoost这类原生支持缺失值的树模型最省事的做法是干脆不填充让它默认把缺失分流。6. 进阶落地把违约概率转成可解释信用评分的实操模型输出的0到1概率没法直接给业务用业务方习惯“分数越高越好”的信用分语言。把概率单调映射成600分制属于风控建模最后一步的标准化操作。6.1 概率转评分的标准公式评分卡假设分数与ln(odds)线性相关odds坏概率/好概率。设定三个业务参数就能确定映射线import numpy as np pdo 20 # 分数每提高20分odds翻倍 base_score 600 # 基准分 base_odds 10 # 基准分对应的好:坏10:1 factor pdo / np.log(2) offset base_score - factor * np.log(base_odds) def prob_to_score(p): p np.clip(p, 1e-6, 1 - 1e-6) # 防止极端概率除零 odds p / (1 - p) return offset factor * np.log(odds) score prob_to_score(xgb_prob)600分对应odds10:1也就是违约概率约9%每降20分坏账概率翻一倍。clip防止p0或1时log抛出无穷大。6.2 概率校准与按月复检树模型的概率有系统性偏差直接转分数整体偏高。常见做法是用IsotonicRegression在验证集时间切分的那段上校准from sklearn.isotonic import IsotonicRegression calib IsotonicRegression(out_of_boundsclip) calib.fit(xgb_valid_prob, y_valid) xgb_prob_cal calib.transform(xgb_prob)校准拟合只能用验证集不能用训练集否则把训练误差又搬回线上。校准后AUC基本不变变化的是概率绝对水平而分数切点依赖的正是这个绝对水平。上线后按月拉最近6个月已终结贷款重算AUC和PSI群体稳定性指标PSI超0.2就该考虑重训。我把这个项目做了三遍前两次分别栽在特征泄漏和时间切分上后来把这两项检查固化成脚本就再没翻过车。公开数据、成本低链路里的坑却和真实风控项目几乎一致值得投入也希望这套流程能帮你少走同样一段路——希望帮到你。本文还有配套的精品资源点击获取
延伸阅读

更多相关文章

2026/10/10 13:42:37

基于CrashRpt与Detours的Windows崩溃捕获深度改造实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/10 13:42:37

Java实现的可审计iOS签名服务系统

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/11 16:04:02

Cminusf编译原理课设全解析:从词法分析到中间代码生成

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/11 15:58:22

跨江桥梁病害检测与资产标定:YOLOv8数据集构建与训练调参实战

简介:这份资源面向计算机视觉研究者、桥梁监测工程师及目标检测学习者,提供跨江桥梁路面病害与道路资产标定的专用数据集,用于训练裂缝、破损、积水等病害及桥墩、拉索、桥面等结构元素的识别模型,弥补通用数据集在桥梁场景下的适…

2026/10/11 0:02:13

Python调用Gemini Structured Outputs实现工单路由门禁

客服工单最怕的不是模型“答错一句话”,而是它给出一段看起来合理的说明,程序却从中猜错优先级。通俗做法是:要求模型只交 JSON(JavaScript Object Notation,轻量数据格式),再让代码验证它。Gem…

2026/10/11 0:02:13

Spring Boot超市进销存系统毕设实战:从需求拆解到答辩通关

最近带的一个学生项目组里,有A同学跑来问我:选什么毕设题目最稳妥,既能让评审老师觉得工作量够,又不会在答辩时被问到语无伦次。我第一反应就是推荐基于Spring Boot的超市仓库管理系统——也就是超市进销存系统。这个题目乍一看平…

2026/10/11 0:02:13

Flutter StatefulWidget 生命周期核心解析

很多刚开始接触 Flutter 的朋友,在看完一堆“Hello World”和基础组件之后,大概率都会撞上同一堵墙:StatefulWidget 里那堆 initState、build、dispose 方法,到底什么时候被调用?为什么顺序是那样?在里面到…

2026/10/11 0:02:13

Python调用Gemini Structured Outputs实现工单路由门禁

客服工单最怕的不是模型“答错一句话”,而是它给出一段看起来合理的说明,程序却从中猜错优先级。通俗做法是:要求模型只交 JSON(JavaScript Object Notation,轻量数据格式),再让代码验证它。Gem…

2026/10/11 0:02:13

Spring Boot超市进销存系统毕设实战:从需求拆解到答辩通关

最近带的一个学生项目组里,有A同学跑来问我:选什么毕设题目最稳妥,既能让评审老师觉得工作量够,又不会在答辩时被问到语无伦次。我第一反应就是推荐基于Spring Boot的超市仓库管理系统——也就是超市进销存系统。这个题目乍一看平…

2026/10/11 0:02:13

Flutter StatefulWidget 生命周期核心解析

很多刚开始接触 Flutter 的朋友,在看完一堆“Hello World”和基础组件之后,大概率都会撞上同一堵墙:StatefulWidget 里那堆 initState、build、dispose 方法,到底什么时候被调用?为什么顺序是那样?在里面到…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑