ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

天池二手车价格预测:数据挖掘、特征工程与模型融合实战要点

天池二手车价格预测:数据挖掘、特征工程与模型融合实战要点 简介面向天池二手车价格预测竞赛的完整项目包适合机器学习初学者、竞赛参赛者以及需要毕业设计或期末大作业参考的学生。方案基于LightGBM与XGBoost两种梯度提升树算法从数据读取、缺失值处理、特征构造到模型训练与参数调优均有完整代码与说明并对两个模型的结果进行对比分析帮助读者快速建立一套可复用的预测流程。压缩包共16个文件包含两个IPython Notebook源代码、训练/测试/提交CSV数据、XML项目配置文件、依赖列表及项目说明文档整体约32MB目录按代码、数据、提交结果和文档分块便于按步骤复现与二次开发。已有829人学习下载作为高分赛题方案具有较高参考价值。通过该资源可拿到可直接运行的预测代码、特征工程与调参细节、提交样例适合入门天池竞赛、撰写实验报告或作为课程项目落地参考。1. 天池二手车价格预测赛题到底在考什么入行做数据挖掘这几年我见过太多人把这个赛题当成一个普通的回归任务来打拿到数据集先跑一遍LightGBM调几轮参数交上去然后分数卡在榜单中游再也上不去。实际上天池二手车价格预测真正考的不是模型有多高级而是你对二手车交易场景的理解深度——价格由什么决定、哪个字段是噪音、哪些信息必须靠特征工程才能喂给模型。这个赛题适合正在入门结构化数据竞赛的人也适合想在简历上放一个完整数据挖掘项目的从业者。它最大的价值在于数据量不大、字段有真实业务含义、评分指标固定你能清清楚楚看到每一步处理对分数的影响。高分和低分的差距往往不在最后一轮调参而在前面数据处理和特征构造的阶段就拉开了。所以这篇文章不绕弯子直接按我当时做这个赛题的路径走一遍从赛题数据检查开始到特征工程、模型策略、参数调节最后是那些让很多人白费两周时间的坑。整个方案包含了源码、项目说明和数据集的标准结构你可以直接对照着自己的项目目录逐段改。2. 先把数据摸透几个决定分数上限的字段很多参赛者拿到数据后的第一件事就是训练模型这是最浪费赛题价值的行为。天池二手车价格预测给的数据集有几十个字段但每个字段的缺失比例、类型分布、与价格的关系强度完全不同。先花两三个小时把数据摸清楚胜过盲目调参一周。2.1 拿到数据后先核对字段画像缺失、类型与时间线我一般会在项目根目录建一个data/文件夹放原始数据再建一个notebooks/放探索脚本。第一步不是画图而是先把每个字段的缺失比例、唯一值数量、数据类型一次性打出来。这一步看起来简单做不做差别很大——它能直接告诉你后续特征工程的工作量集中在哪里。import pandas as pd import numpy as np train pd.read_csv(data/train.csv) test pd.read_csv(data/test.csv) def inspect_df(df, name): summary pd.DataFrame({ col: df.columns, dtype: df.dtypes.values, null_pct: (df.isnull().mean() * 100).round(2), nunique: df.nunique().values }) print(f {name}: {df.shape} ) print(summary.to_string(indexFalse)) inspect_df(train, train) inspect_df(test, test)这里注意null_pct我用的是isnull().mean()而不是sum()直接换算成百分比一眼看出哪些字段超过 30% 缺失、要不要直接丢弃。nunique则用来区分连续字段和离散字段——唯一值特别少的数值列后面更适合当类别特征处理。这张表打出来先别急着删字段重点看训练集和测试集的缺失比例是否一致。如果某个字段在训练集缺失 20%在测试集缺失 40%那它不是简单的随机缺失极有可能和真实交易流程有关。这种情况直接填充中位数会引入偏差后面特征工程环节要想别的办法。2.2 价格分布为什么右偏取对数与否的差别二手车价格数据集里的标签price分布呈现典型的右偏形态几万块的车占绝大多数几十万上百万的车只有零星几条。如果不做处理直接作为回归目标模型会把大量容量花在拟合那些价格很高的样本上导致中低价位段的预测误差反而被忽视。而赛题用的评分指标恰恰对这部分更敏感。import matplotlib.pyplot as plt import seaborn as sns fig, axes plt.subplots(1, 2, figsize(12, 4)) sns.histplot(train[price], bins50, axaxes[0], kdeTrue) axes[0].set_title(Price Original) train[price_log] np.log1p(train[price]) sns.histplot(train[price_log], bins50, axaxes[1], kdeTrue) axes[1].set_title(Price Log1p) plt.tight_layout() plt.savefig(notebooks/price_distribution.png)np.log1p就是log(1 x)比直接取对数好在能处理价格为 0 的样本避免出现负无穷。我在这个赛题里始终用log1p变换后的价格作为训练目标提交前再用np.expm1还原成真实价格。这里有一个容易被忽略的点最后提交的预测价格如果直接取指数得到的是中位数意义上的预测和真实价格之间存在系统性偏差。想要修正这个偏差一个常见做法是把预测结果乘上一个调整系数让整体均值与真实分布对齐。这个系数不是调出来的而是用验证集上的预测值和真实值的比值算出来的。后面在验证策略章节我会具体讲。2.3 数据集字段检查表哪些值得做特征哪些直接丢天池这个赛题的字段分成三类强业务字段、匿名特征、纯噪音字段。强业务字段比如name车辆名称、regDate注册日期、model车型编码这些直接决定车辆价格必须重点处理。匿名特征如v_0到v_14这一批字段来源于数据脱敏没有业务含义但很多高分方案证明它们携带了价格信息不能随意丢弃。纯噪音字段代指那些缺失率极高且和价格相关性几乎为零的列。字段类型典型字段处理策略备注强业务字段name,regDate,model,brand清洗后做特征工程车龄、品牌均价、车型热度匿名特征v_0~v_14保留并做缺失填充PCA 或聚类后作为新特征噪音字段缺失 70% 且相关性低直接删除避免引入噪声注意匿名特征这一块不要试图去猜它代表什么物理含义不存在标准答案。我在项目里只把它们当成一组数值向量统计它们的缺失率、均值、方差后续用 PCA 或者聚类生成派生特征实验下来比直接删掉效果好很多。3. 特征工程决定上限把二手车信息转成模型能消化的特征打完字段画像真正的重头戏才开始。特征工程是这类赛题最花功夫的环节我一般会把这部分拆成三块缺失值处理、时间类特征构造、数值字段的交叉组合。每一块在项目源码里都对应独立的脚本文件方便回溯哪些特征有效哪些无效。3.1 数值型字段缺失值填充中位数、众数还是模型预测二手车数据集里的缺失值不是随机出现的。比如power发动机功率缺失的样本往往是低配车或改装车的记录不完整。如果简单用全体中位数填充等于告诉模型这些车有平均水平功率实际上下方分布可能偏高或偏低。from sklearn.impute import SimpleImputer from sklearn.ensemble import RandomForestRegressor # 方案A: 中位数填充适合缺失率低且随机缺失的字段 imputer_med SimpleImputer(strategymedian) train[power_median] imputer_med.fit_transform(train[[power]]) # 方案B: 用其他字段预测缺失值适合缺失率和业务含义相关的字段 known train[train[power].notna()] unknown train[train[power].isna()] feature_cols [kilometer, model, brand, car_age_days] rf RandomForestRegressor(n_estimators200, max_depth6, random_state42, n_jobs-1) rf.fit(known[feature_cols], known[power]) train.loc[unknown.index, power_pred] rf.predict(unknown[feature_cols])方案 A 的strategy参数可以换成mean或most_frequent但对含离群点的字段均值容易被拉偏中位数更稳。方案 B 里我选了max_depth6防止过拟合n_estimators200是这个数据量下性能和速度的平衡点。判断用哪个方案看缺失率和价格的相关性缺失率低于 5% 且和价格没有明显关联的字段直接中位数填充缺失率高于 10% 的先做一次相关性分析再决定。这个赛题数据里power缺失率不低用随机森林预测缺失值的方案最终在验证集上比单纯中位数填充低了 0.002 的误差。3.2 时间类特征构造车龄、上牌月份、季节衰减二手车价格天然和车龄强相关同一款车新车和五年车龄的价格可能差一倍。数据集里的regDate是字符串形式类似19990401tradeTime是交易时间。这两个字段单独作为字符串输入模型没有意义必须转换成数值时间差。from datetime import datetime def parse_date_str(s): s str(s) if s 0 or len(s) 6: return np.nan if len(s) 6: return datetime.strptime(s, %Y%m) return datetime.strptime(s[:8], %Y%m%d) train[reg_date] train[regDate].apply(parse_date_str) train[trade_date] train[tradeTime].apply(parse_date_str) # 车龄按天数计算避免月份精度丢失 train[car_age_days] (train[trade_date] - train[reg_date]).dt.days train[car_age_years] train[car_age_days] / 365.0 # 上牌月份会影响保值率冬季和夏季购车有差异 train[reg_month] train[reg_date].dt.month train[trade_month] train[trade_date].dt.month # 季度特征按二手车市场的淡旺季切分 train[reg_season] train[reg_month].map({1:1,2:1,3:2,4:2,5:2,6:3,7:3,8:3,9:4,10:4,11:4,12:1})这里最容易踩坑的是regDate的解析。原始数据里年份只有 4 位月份只有两位有些样本少了一位数字直接pd.to_datetime会抛异常或者解析成错误日期。我在parse_date_str里先判断字符串长度长度不足的年份直接返回NaN后面用数据集中位数填充。这个处理在项目说明里要写清楚否则别人复现代码时会在数据清洗步骤卡住。车龄特征我同时保留了天数和年份两个版本。天数为连续特征适合树模型的切分年份为离散特征适合做分组统计两个版本在不同的模型里有各自的用途。只保留其中一个在融合模型阶段会少一个可用的输入。3.3 匿名特征向量化PCA 与聚类派生的参数选择v_0到v_14这组匿名特征缺失处理结束后可以进一步压缩或聚类。直接把这 14 列全部丢进模型也行但树模型在处理高维稀疏特征时切分效率不高。我试过两种派生方式效果都不错。from sklearn.decomposition import PCA from sklearn.cluster import KMeans from sklearn.preprocessing import StandardScaler v_cols [fv_{i} for i in range(15)] # 先标准化再 PCA否则量纲差异会让前几个主成分被大数值字段主导 scaler StandardScaler() train_v_scaled scaler.fit_transform(train[v_cols]) pca PCA(n_components6, random_state42) train_pca pca.fit_transform(train_v_scaled) for i in range(6): train[fv_pca_{i}] train_pca[:, i] # 聚类特征用 KMeans 把样本分成 10 组组标签作为类别特征 kmeans KMeans(n_clusters10, random_state42, n_init10) train[v_cluster] kmeans.fit_predict(train_v_scaled)PCA 的n_components取值不是拍脑袋定的我跑到explained_variance_ratio_看累计贡献率取前 6 个主成分能解释 85% 左右的方差再加但收益递减。KMeans 的n_clusters我在 5 到 20 之间都测过10 组在验证集上效果最稳组数太多会过拟合训练集太少则分不出价格差异。注意PCA 和 KMeans 的fit只应该在训练集上做再 transform 测试集。如果把训练和测试数据放在一起做标准化相当于把测试集信息泄漏到了训练阶段虽然单次结果看着好但泛化会变差。3.4 价格相关性强特征交叉品牌、车型、车龄的组合单看brand品牌和model车型各自和价格的相关系数能看出一定规律但还不够。真实的二手车报价逻辑是某一年的某个品牌某个车型市场认可度是多少。所以我把品牌、车型、车龄这三者做成了交叉统计特征用目标编码的方式计算每个组合的均价和样本量。from sklearn.model_selection import KFold # 5折目标编码避免用全量数据计算均值导致的标签泄露 def target_encode(df, col, target, n_folds5): df df.copy() kf KFold(n_splitsn_folds, shuffleTrue, random_state42) encoded np.zeros(len(df)) for tr_idx, te_idx in kf.split(df): tr_mean df.iloc[tr_idx].groupby(col)[target].transform(mean) encoded[te_idx] df.iloc[te_idx][col].map( df.iloc[tr_idx].groupby(col)[target].mean() ).fillna(df[target].mean()).values return encoded train[brand_model_age_target] target_encode( train, train[brand].astype(str) _ train[model].astype(str) _ train[car_age_years].astype(int).astype(str), price_log )这个目标编码的写法重点在 KFold 循环内部每个折只拿训练部分的均值去编码验证部分所以不会把验证集的价格信息带进训练过程。fillna(df[target].mean())处理的是某些组合在训练里没出现过的情况兜底用全局均值。这里的target用的是price_log而不是原始price因为标签经过对数变换后均值更能代表中位数水平。组合键我用下划线拼起来实跑时发现brand和model单独编码效果不如联合但三个字段全联合会让某些组合在验证折里为空。所以我会退一步做brand_model_target和brand_age_target两个二级组合两个特征的稳定性比一个三级特征好。4. 模型策略从单模型到融合的高分路径特征工程做得再漂亮最终还是要落到模型上。我见过太多人在单模型上死磕参数最后验证集分数纹丝不动。天池这个赛题的经验是用两个不同的模型结构各自跑出基础分再在融合阶段把它们的优点叠加往往比单模型调参收益大得多。4.1 基线模型用 LightGBM 跑通最小训练闭环我平时不会一上来就上大融合而是先用一个 LightGBM 构建整个训练、验证、预测的完整流程确保没有数据处理上的错误再在这个闭环上迭代特征或换模型。这是结构化竞赛最稳的起步方式——先跑通再调好。import lightgbm as lgb from sklearn.model_selection import KFold from sklearn.metrics import mean_absolute_error import numpy as np features [c for c in train.columns if c not in [price, price_log, SaleID, reg_date, trade_date, regDate, tradeTime, name]] X train[features].copy() y train[price_log].values # 缺失值统一填充 -1树模型能识别这个哨兵值 X X.fillna(-1) kf KFold(n_splits5, shuffleTrue, random_state42) oof np.zeros(len(X)) pred np.zeros(len(test)) params { objective: regression, metric: mae, learning_rate: 0.05, num_leaves: 31, max_depth: 7, feature_fraction: 0.8, bagging_fraction: 0.8, bagging_freq: 1, lambda_l2: 2.0, verbosity: -1, seed: 42, } for fold, (tr_idx, va_idx) in enumerate(kf.split(X)): dtr lgb.Dataset(X.iloc[tr_idx], y[tr_idx]) dva lgb.Dataset(X.iloc[va_idx], y[va_idx]) model lgb.train(params, dtr, num_boost_round5000, valid_sets[dva], callbacks[lgb.early_stopping(100), lgb.log_evaluation(200)]) oof[va_idx] model.predict(X.iloc[va_idx], num_iterationmodel.best_iteration) pred model.predict(test[features].fillna(-1), num_iterationmodel.best_iteration) / 5 # 验证指标用expm1还原后计算MAE更贴近赛题分数 val_mae mean_absolute_error(np.expm1(y), np.expm1(oof)) print(fValidation MAE: {val_mae:.4f})这段代码有几个参数是刻意的不是随手写的。fillna(-1)是树模型处理缺失值的常见做法LightGBM 原生支持缺失值但显式填充一个哨兵值可以避免测试集和训练集缺失分布不一致带来的偏差。learning_rate0.05配合num_boost_round5000和early_stopping(100)这是为了在验证集上找到最优的迭代轮数我试过直接把学习率调到 0.1分数会差一点点但训练时间缩短一半如果你想快速实验可以用 0.1最终提交还是回到 0.05。log_evaluation(200)不是必需的但能看到验证误差下降的趋势方便判断是欠拟合还是数据有问题。这个版本的 OOFout-of-fold预测一定要保存下来后面所有特征工程、模型融合、阈值调节都基于 OOF 分数做决策。如果把测试集预测结果当作调参依据会严重过拟合测试集最后提交分数和本地验证完全对不上。4.2 训练验证划分K 折为什么要加 Stratified天池这个赛题的数据不是按价格均匀分布的高价位样本占比很低。如果切分时不控制随机种子某些折可能完全没有高价位样本导致那一折的模型学不到价格上限OOF 分数虚高或虚低。解决方式是改用分层 K 折让每一折的价格分布和全量数据尽量一致。from sklearn.model_selection import StratifiedKFold # 先把价格分箱作为分层标签 bins [0, 1, 3, 5, 10, 20, 40, np.inf] labels list(range(len(bins) - 1)) train[price_bin] pd.cut(train[price], binsbins, labelslabels) skf StratifiedKFold(n_splits5, shuffleTrue, random_state42) for fold, (tr_idx, va_idx) in enumerate(skf.split(X, train[price_bin])): print(fFold {fold1}: train {len(tr_idx)}, val {len(va_idx)})pd.cut的作用是把连续价格按自定义区间分箱区间边界的设置要在保证每个箱子里样本数足够至少几百条的前提下尽量让价格分布细节保留。分箱太多会有空折风险分箱太少分层意义不大。StratifiedKFold和普通KFold的差别在split时多传了一个train[price_bin]它保证每一折中各个价格段的比例接近全量数据。这里的random_state42固定后整个赛题周期里不要改。否则每次跑出来的分数不同你就分不清某个特征到底是有效还是随机波动。我后来养成的习惯是数据划分的种子、特征工程的种子、模型的种子全部写死在项目的配置脚本里上线前不碰。4.3 多模型融合加权平均与 Stacking 的收益边界单一 LightGBM 跑出验证分数后我会再补一个 XGBoost 或 CatBoost 模型。两个模型结构不同错误的分布也不同的——融合它们互补的部分验证集分数通常能再降 0.001 到 0.003 的 MAE。这个幅度看着小在榜单上可能能前进几十个名次。import xgboost as xgb xgb_params { objective: reg:squarederror, eval_metric: mae, learning_rate: 0.03, max_depth: 7, subsample: 0.85, colsample_bytree: 0.8, reg_lambda: 2.0, seed: 42, } # 同样用5折训练得到第二个OOF预测 xgb_oof np.zeros(len(X)) xgb_pred np.zeros(len(test)) for fold, (tr_idx, va_idx) in enumerate(skf.split(X, train[price_bin])): dtr xgb.DMatrix(X.iloc[tr_idx], labely[tr_idx]) dva xgb.DMatrix(X.iloc[va_idx], labely[va_idx]) model xgb.train(xgb_params, dtr, num_boost_round5000, evals[(dva, val)], early_stopping_rounds100, verbose_eval0) xgb_oof[va_idx] model.predict(xgb.DMatrix(X.iloc[va_idx]), iteration_range(0, model.best_iteration 1)) xgb_pred model.predict(xgb.DMatrix(test[features].fillna(-1)), iteration_range(0, model.best_iteration 1)) / 5 # 搜索最优融合权重 best_w None best_score 1e9 for w in np.arange(0.1, 1.0, 0.05): blended np.expm1(w * lgb_oof (1 - w) * xgb_oof) score mean_absolute_error(np.expm1(y), blended) if score best_score: best_score score best_w w print(fBest lgb weight: {best_w:.2f}, MAE: {best_score:.4f})这个融合搜索是在对数空间做的也就是对两个模型的 OOF 预测做线性加权再统一取指数还原。我这里用的是w * lgb_oof (1-w) * xgb_oof直接在price_log空间融合比在真实价格空间融合更稳定因为对数空间里误差分布更对称均值更合理。搜索步长 0.05 已经够细再小基本是因为噪声在起作用。best_w通常在 0.5 到 0.7 之间偏向 LightGBM 居多但这不固定取决于特征构造方式和模型超参数。每次改完特征都要重新搜一次权重不要沿用之前的best_w。提示Stacking 在这个赛题上的收益边际很小因为两个基模型的预测相关性较高。加权平均已经能拿到大约 90% 的融合收益剩下 10% 的收益要付出大量调参成本我建议比赛周期短的话做到加权平均就收手。5. 避坑指南这个赛题最容易翻车的 5 个操作5.1 用全量数据计算目标编码验证分数虚高现象目标编码后的特征在验证集上让 MAE 大幅下降但提交后分数反而变差。原因把全量数据的标签均值编码到训练和验证特征里相当于验证集的价格信息在训练阶段就被模型看到了。这在回归任务里特别隐蔽因为不会报错只会让你误以为特征很有效。解决目标编码必须嵌套在 K 折交叉验证内部每一折只用训练部分计算均值再用这个均值编码验证部分。我在 3.4 节给的target_encode函数就是标准写法直接抄用即可。注意这个坑排查起来不容易特征名字要起得自己能看懂比如加_out后缀提醒自己这是无泄漏版本。5.2 regDate 字段解析出错车龄偏差导致价格预测往低偏移现象某些样本的regDate缺位或长度异常解析后得到异常大的车龄模型对这部分车龄过大的样本预测价格偏低。原因regDate是字符串格式存在0、1999、199908这类不规范的取值。直接用pd.to_datetime转出来的日期是错的算出的车龄可能为负数或数十年模型被这些离群样本牵扯过多注意力。解决先写一个解析函数处理长度不足和全零字符串解析失败返回 NaN随后用训练集中位数填充。同时把car_age_days做截断处理超过 15 年的统一设为 15 年——市场上超过这个年限的车基本按报废价处理细分的年份差异对价格影响不大。5.3 测试集缺失值分布和训练集不一致model 字段索引编码错位现象训练时模型表现很好提交时预测结果出现较多 0 值或极端低值。原因model字段是类别编码训练集和测试集覆盖的车型不完全相同某些测试集车型在训练集里没见过。直接用LabelEncoder或factorize编码测试集中的新车型会被编成新数字树模型感知不到这个数字和已知车型的关系。解决先对训练集和测试集的model做并集处理统一编码表并把出现过少的车型映射到一个rare类别。我在特征工程里专门写了这段处理用value_counts()统计频次低于 5 的车型统一归为unknown再进入编码流程。5.4 加权融合时直接在真实价格空间做平均现象融合后的预测经常在中高价位段偏大整体 MAE 反而比单个模型差。原因真实价格分布右偏直接对真实价格做算术平均高价样本的预测误差会被放大中低价位段的主导地位被稀释。解决所有融合都在price_log空间进行最后再统一np.expm1。如果已经保存了真实价格的预测先取对数再融合也可以但我建议从训练一开始就用对数标签这样整个流程都基于对数空间不必在最后额外处理。5.5 过早调参特征还没稳定就开始刷参数现象在 LightGBM 上改learning_rate、num_leaves每次提升 0.001 个 MAE但做完特征工程后发现这些调整完全没有意义。原因特征集合每变化一次当前参数下的最优组合就变了。调参依赖特征集合稳定否则你是在一个移动的靶子上做精确射击耗时且无效。解决先把特征工程迭代到一个相对稳定的版本这个稳定不是指把所有特征都加上而是验证集分数连续两轮特征修改后不再明显下降。这时候再开始系统调参会快很多。天池这个赛题的数据量下特征不变时用 5 折交叉验证调参每次learning_rate0.05跑到早停单次实验 5 分钟左右足够你按max_depth、num_leaves、feature_fraction、reg_lambda的顺序逐项试验。6. 项目源码怎么读才值回票价从复现到改进的一线习惯拿到一个高分源码项目最容易犯的错是直接从头跑一遍看分数完美复现后觉得自己学会了。实际上源码项目最大的价值不在于那个提交分数而在于它整个特征工程留下的痕迹哪些特征得分哪些特征被注释掉哪些参数在不显眼的位置做了微调。我复读这类项目时会专门去看两个地方。第一个是特征筛选的过程。一个完整源码里通常留着多个版本的特征列表只看最终版本只能知道答案看不到特征从粗到细的取舍过程。我会把最终特征列表逐个往回删每删一个跑一次验证集看分数损失多少。损失大的特征才是这个项目真正的核心值得做笔记损失微小的特征说明原作者可能也只是尝试性加上去的。这个反向验证过程做一遍你对数据的理解会深很多。第二个是验证策略的细节。不同赛题的验证策略差异很大有的源码为了追求线下分数和线上一致会在 K 折结构上做文章。看到源码里 OOF 和测试集预测的保存逻辑我会特别关注它有没有做价格还原后的校准——就是前面提到的调整系数。这个细节容易跳过但往往直接影响提交分数。项目说明里如果没写这个系数怎么算出来的我会自己在验证集上推导一遍通常能复现出和线上最接近的数字。我的个人习惯是每跑一个新赛题都会维护一张表特征名、加入时间、验证分数相对上个版本的增量、是否保留。这张表既是排错的依据也是最后写项目说明时的素材。天池这类竞赛项目的源码代码本身通常不难读难的是理解每个操作背后的决策逻辑——把决策逻辑补全比把代码跑通更能决定你下一次独立作战的分数上限。希望这份从数据处理到模型融合的完整路径能帮到正在复现或准备参赛的你。本文还有配套的精品资源点击获取
返回列表