1. 数据分析网首页
  2. 大数据
  3. 数据分析

手把手带你进入TOP20的商超销售预测

摘要:如果说学习数据科学的最佳途径是什么——就是解决实际问题或亲自参与数据科学项目。因为只有当自己动手解决问题时,你才真正开始学习数据科学。

“商超销售预测”这一题目在一个月前一经提出,已有624名数据科学家报名参加,其中77名提交了答案。不管你是在开始时感到无从下手还是过程中遭遇瓶颈,本文都将带大家体验商超销售预测工作的全过程。

希望本文能够帮助越来越多的人走上数据科学之路。手把手带你进入TOP20的商超销售预测

我们的探索将按照如下步骤进行:1.假设生成——对影响因素进行头脑风暴,以便更好地理解问题。

2.数据探索——对类别性和持续性的特征进行观察总结,并推理数据。

3.数据清理——输入数据中的丢失值,并检查异常值。

4.特征工程——修改现有变量,建立新的分析变量。

5.建模——根据数据建立预测模型。

那么,闲话少说,现在言归正传!

1、假设生成

数据分析的关键一步,它包括陈述问题和对积极影响因素做出假设,这是观察数据的前期准备。当然,如果最后能获得数据的话,我们可以列表记下所有可能性分析。问题表述首要步骤就是理解问题表述。大数据科学家们收集了2013年十家不同地区商店的1559件商品的销售数据,每件产品的特性也已一一列明。目的就是建立预测模型,了解这些商店里每个产品的销售情况。

通过运用这一模型,商超销售预测将试图抓取那些对提高销售额起重要作用的产品特性和商店特点。

所以关键就是找出这些特性。我们首先进行可行性分析,然后再提出假设。假设对此问题我进行了如下假设(仅为个人想法,大家或许有更多思路)。既然讨论的是商店和产品,首先要建立不同的数据组。关于商店的假设:

1. 城市类型:位于城区或一线城市的商店的销量应更多,因为这里人群的消费水平更高。

2. 人口密度:人口稠密地区的商店的销量应更多,因为需求更大。

3. 商店规模:规模较大的商店的销量应更多,因为它们以一站式服务的方式运营,人们更喜欢在一个地方买全所有所需产品。

4. 竞争对手:陈设布置类似的商店的销量应更小,因为竞争更加激烈。

5. 营销:设有独特的促销区域的商店的销量应更大,因为促销区通过巧妙定格和广告会更吸引顾客。

6. 位置:位于繁华市场的商店的销量应更大,因为更接近顾客。

7. 顾客行为:保持产品陈列整齐以满足本地顾客需求的商店的销量应更大。

氛围:员工彬彬有礼的商店预计顾客会更多,销量也更多。

关于产品的假设:

1. 品牌:名牌产品销量应更高,因为顾客更加信任。

2. 包装:包装精美的产品可以吸引顾客,销量更好。

3. 用途:相比特殊用途产品,日常用品销量应更高。

4. 展示区:在商店占据较大货架的产品更容易首先吸引消费者,因而更畅销。

5. 店内可见度:商店内产品的摆放会影响销售。放在门口的商品比靠后的商品更吸引顾客。

6. 广告:商店里广告做得好的产品多数情况下销量更高。

7. 促销:有促销和折扣的商品更畅销。

这仅仅是我做的15个假设,大家还可以有更深入的思考,创建自己的假设。请记住,数据可能不足以检测所有假设,但是做这些假设能帮助你更好地理解问题,如果可能的话我们还可以寻找开放源信息。

下面让我们来到数据探索环节,仔细观察数据。

2、数据探索

在此做基本的数据探索并提出关于数据的猜测。我们会试着找出一些异常情况,并在下个环节解决。

第一步是观察数据,并尝试发现根据已有数据做出的假设信息。数据词典和假设的对比显示如下:

手把手带你进入TOP20的商超销售预测

我们可总结发现如下:手把手带你进入TOP20的商超销售预测你一定会发现你所假设的特征,但是数据不能直接反映特征,特征也不是靠数据直观体现。这时,大家可以寻找一些开放源数据来填补漏洞。我们首先加载所需的函数库和数据。可以从页面下载这些数据。

import pandas as pd
import numpy as np#Read files:
train = pd.read_csv(“train.csv”)
test = pd.read_csv(“test.csv”)

一般来说,我们会先合并训练数据和测试数据,运行特征工程后再把它们分离。这省去了对训练数据和测试数据重复操作的麻烦。我们可以把它们合并到一个数据框“数据”中,用“源”这一列注明每个观察结果应归为哪一列。

train[‘source’]=’train’
test[‘source’]=’test’
data = pd.concat([train, test],ignore_index=True)
print train.shape, test.shape, data.shape

手把手带你进入TOP20的商超销售预测

这样我们就能看到数据可能有相同的列,而行就相当于测试和训练。挑战之一就是丢失值,我们可以先检查一下哪些列含有丢失值。

data.apply(lambda x: sum(x.isnull()))

手把手带你进入TOP20的商超销售预测

请注意Item_Outlet_Sales是目标变量,而丢失值是测试组的变量,所以不必担心。不过我们要在数据清理环节中把丢失值输入到Item_Weight和Outlet_Size中。
我们来观察一组数字变量的统计:

data.describe()

QQ截图20160320

观察结果如下:1. Item_Visibility最小值为0,没有实际意义,因为当产品在商店销售时,可见度不可能为0.2. Outlet_Establishment_Years变化范围是从1985到2009。数值以这种格式出现可能不太恰当。不过,如果我们能够将它们转化为商店的历史,这些数值对销量应该会产生更积极的作用。3. Item_Weight和Item_Outlet_Sales的数值较低证实了丢失值检查得出的发现。

提到名义变量,我们来观察一下每组中的特殊值。

data.apply(lambda x: len(x.unique()))

手把手带你进入TOP20的商超销售预测

由图可知有1559个产品和10家商店(这些在问题陈述中也提到)。另一个值得关注的是Item_Type有16个特殊值。让我们通过每一个名义变量中的不同类别的频率来进一步探索。显然,这里不涉及ID 和源变量。

#Filter categorical variables
categorical_columns = [x for x in data.dtypes.index if data.dtypes[x]==’object’]
#Exclude ID cols and source:
categorical_columns = [x for x in categorical_columns if x not in [‘Item_Identifier’,’Outlet_Identifier’,’source’]]
#Print frequency of categories
for col in categorical_columns:
print ‘\nFrequency of Categories for varible %s’%colprint data[col].value_counts()

手把手带你进入TOP20的商超销售预测

由上数据得出如下结果:1. Item_Fat_Content:一些‘Low Fat’值被误编码为‘low fat’和‘LF’。此外,一些‘Regular’变成了‘regular’。2. Item_Type:并非所有类别都有实数,合并类别似乎可以得出更好结果。3. Outlet_Type:Supermarket Type2 and Type3可以合并,不过动手前要考虑这个想法是否可取。

3、数据清理这一步骤主要包括录入丢失值和处理异常值。尽管去除异常值在拟合技能中非常重要,基于树的高级算法却对异常值没有影响。所以这一部分留给大家自己尝试。我们在此只关注录入这一关键步骤。注意:我们在此将广泛使用一些Pandas library。如果你对Pandas还不太熟悉,可以参考相关文章,浏览更多Pandas相关文章!录入丢失值我们发现两个有丢失值的变量——Item_Weight和 Outlet_Size。这时按照一个物品的平均重量录入物品重量,步骤如下:

#Determine the average weight per item:
item_avg_weight = data.pivot_table(values=’Item_Weight’, index=’Item_Identifier’)#Get a boolean variable specifying missing Item_Weight values
miss_bool = data[‘Item_Weight’].isnull()#Impute data and check #missing values before and after imputation to confirm
print ‘Orignal #missing: %d’% sum(miss_bool)
data.loc[miss_bool,’Item_Weight’] = data.loc[miss_bool,’Item_Identifier’].apply(lambda x: item_avg_weight[x])
print ‘Final #missing: %d’% sum(data[‘Item_Weight’].isnull())

手把手带你进入TOP20的商超销售预测

以上步骤证实列中目前没有丢失值。我们按照某商店类别录入Outlet_Size。

Import mode function:
from scipy.stats import mode#Determing the mode for each
outlet_size_mode = data.pivot_table(values=’Outlet_Size’, columns=’Outlet_Type’,aggfunc=(lambda x:mode(x).mode[0]) )
print ‘Mode for each Outlet_Type:’
print outlet_size_mode#Get a boolean variable specifying missing Item_Weight values
miss_bool = data[‘Outlet_Size’].isnull()

#Impute data and check #missing values before and after imputation to confirm
print ‘\nOrignal #missing: %d’% sum(miss_bool)
data.loc[miss_bool,’Outlet_Size’] = data.loc[miss_bool,’Outlet_Type’].apply(lambda x: outlet_size_mode[x])
print sum(data[‘Outlet_Size’].isnull())

手把手带你进入TOP20的商超销售预测

这表明数据中没有丢失值。现在我们来看特征工程。4、特征工程在数据探索阶段,我们发现数据之间也有细微不同,所以一定要解决问题。根据现阶段的变量做好分析时,我们也会建立一些新变量。步骤一:考虑合并店铺类型(Outlet_Type)探索过程中,我们决定考虑合并Supermarket Type2 和Type3变量。这个想法怎么样?快速检查的方法是通过商店类型分析平均销售量,如果销售很相近,那么合并数据也不会有很大意义。data.pivot_table(values=’Item_Outlet_Sales’,index=’Outlet_Type’)手把手带你进入TOP20的商超销售预测上图表明了两者之间的巨大区别,我们按原貌表现出来。请注意这只是其中一种方法,面对不同的情景可以采用其他的分析方法,针对其他特征也可以采用同样的方法。步骤二:修改Item_Visibility我们注意到最小值是0, 没有实际意义,把它当做丢失信息,按照产品的平均可见性进行输入。

#Determine average visibility of a product
visibility_avg = data.pivot_table(values=’Item_Visibility’, index=’Item_Identifier’)#Impute 0 values with mean visibility of that product:
miss_bool = (data[‘Item_Visibility’] == 0)print ‘Number of 0 values initially: %d’%sum(miss_bool)
data.loc[miss_bool,’Item_Visibility’] = data.loc[miss_bool,’Item_Identifier’].apply(lambda x: visibility_avg[x])
print ‘Number of 0 values after modification: %d’%sum(data[‘Item_Visibility’] == 0)

手把手带你进入TOP20的商超销售预测

这样就能看出任何一个值都不为0。
在步骤一中,我们假设有更高可见度的产品更畅销。但是对产品进行比较的同时,我们也要看到具体某家商店产品的可见度和所有商店同类产品的平均可见度。这使我们知道该产品在某商店与其他商店相比的重视程度。我们可以用上文的‘visibility_avg’变量做到。

#Determine another variable with means ratio
data[‘Item_Visibility_MeanRatio’] = data.apply(lambda x: x[‘Item_Visibility’]/visibility_avg[x[‘Item_Identifier’]], axis=1)
print data[‘Item_Visibility_MeanRatio’].describe()

手把手带你进入TOP20的商超销售预测

这样就成功创造出了新变量。再次,这也仅仅是如何创造新特征的一例。强烈建议大家多多尝试,因为优秀的特征可以大大提高模型运行效果,而且无一例外地表现为优秀模型和普通模型的区别。步骤三:建立范围广的品类刚才我们看到Item_Type变量有16个类别,这在分析中可能会有用,所以合并是个不错的想法。其中一个方法就是手动将类别分配给每一个变量。不过这有一个问题:如果你观察Item_Identifier,也就是每个物品各自的ID,开头一般都是FD、DR 或者NC。如果观察这些类别,它们主要就是食品、饮品和非消耗品。所以我用Item_Identifier变量创造新的一列。

#Get the first two characters of ID:
data[‘Item_Type_Combined’] = data[‘Item_Identifier’].apply(lambda x: x[0:2])
#Rename them to more intuitive categories:
data[‘Item_Type_Combined’] = data[‘Item_Type_Combined’].map({‘FD’:’Food’,
‘NC’:’Non-Consumable’,
‘DR’:’Drinks’})
data[‘Item_Type_Combined’].value_counts()

手把手带你进入TOP20的商超销售预测

另一个方法就是根据销售量合并类别。平均销售量高的类别可以合并到一起。这个大家可以自己尝试。步骤四:决定商店运营年限我们想新创造一个描述商店的运营年限的列。操作如下:

#Years:
data[‘Outlet_Years’] = 2013 – data[‘Outlet_Establishment_Year’]
data[‘Outlet_Years’].describe()

手把手带你进入TOP20的商超销售预测

步骤五:修改Item_Fat_Content类别我们发现表现Item_Fat_Content变量类别中出现错误和差异,可以纠正为:

#Change categories of low fat:
print ‘Original Categories:’
print data[‘Item_Fat_Content’].value_counts()print ‘\nModified Categories:’
data[‘Item_Fat_Content’] = data[‘Item_Fat_Content’].replace({‘LF’:’Low Fat’,
‘reg’:’Regular’,
‘low fat’:’Low Fat’})
print data[‘Item_Fat_Content’].value_counts()

手把手带你进入TOP20的商超销售预测

现在看起来合理一些。不过请注意,步骤四中我们也看到了非消耗品,而 fat-content不能很好地表明差异。因此可以为这种观察结果创建一个单独的类。

#Mark non-consumables as separate category in low_fat:
data.loc[data[‘Item_Type_Combined’]==”Non-Consumable”,’Item_Fat_Content’] = “Non-Edible”
data[‘Item_Fat_Content’].value_counts()

手把手带你进入TOP20的商超销售预测

步骤六:类别变量的数值编码和独热编码既然scikit-learn只接受数值编码,我就将所有类型的名义变量都转化为数值变量。此外,我还想把Outlet_Identifier作为变量。所以我创建了一个和Outlet_Identifier相同的新的变量“‘Outlet’ 并进行编码。Outlet_Identifier要保持其原貌,因为提交文件还需要它。
我们首先使用sklearn预处理模块中的LabelEncoder把所有类别变量编码为数值变量。

#Import library:
from sklearn.preprocessing import LabelEncoder
le = LabelEncoder()
#New variable for outlet
data[‘Outlet’] = le.fit_transform(data[‘Outlet_Identifier’])
var_mod = [‘Item_Fat_Content’,’Outlet_Location_Type’,’Outlet_Size’,’Item_Type_Combined’,’Outlet_Type’,’Outlet’]
le = LabelEncoder()
for i in var_mod:
data[i] = le.fit_transform(data[i])

一个独热码指的是创建虚拟变量,每个类别变量需要一个。例如:Item_Fat_Content有三个类别——‘Low Fat’, ‘Regular’ and ‘Non-Edible’.。独热码会消除该变量并产生三个新变量,它们每一个都有二进制数——0 (如果该类别不在)和1(如果该类别存在)。可以通过Pandas的“‘get_dummies’功能做到。

#One Hot Coding:
data = pd.get_dummies(data, columns=[‘Item_Fat_Content’,’Outlet_Location_Type’,’Outlet_Size’,’Outlet_Type’,
‘Item_Type_Combined’,’Outlet’])

Lets look at the datatypes of columns now:

data.dtypes

手把手带你进入TOP20的商超销售预测

手把手带你进入TOP20的商超销售预测

我们看到所有变量都是实型变量,每个类别都有一个新变量。现在让我们观察一下由Item_Fat_Content形成的3列:

data[[‘Item_Fat_Content_0′,’Item_Fat_Content_1′,’Item_Fat_Content_2’]].head(10)

手把手带你进入TOP20的商超销售预测

大家可以发现每行都只有一列对应原始变量的类别。步骤七:数据输出最后一步就是将数据转化为训练数据组和测试数据组。将这两种数据输出为修改数据组也是个很好的想法,这样就可以重复用到多种场合。通过以下编码可以做到:

#Drop the columns which have been converted to different types:
data.drop([‘Item_Type’,’Outlet_Establishment_Year’],axis=1,inplace=True)#Divide into test and train:
train = data.loc[data[‘source’]==”train”]
test = data.loc[data[‘source’]==”test”]#Drop unnecessary columns:
test.drop([‘Item_Outlet_Sales’,’source’],axis=1,inplace=True)
train.drop([‘source’],axis=1,inplace=True)

#Export files as modified versions:
train.to_csv(“train_modified.csv”,index=False)
test.to_csv(“test_modified.csv”,index=False)

好了,本环节到此结束。如果你希望特征工程的所有编码以iPython notebook形式出现的话,可以参考GitHub repository!5、建立模型现在我们准备好了所有数据,可以开始建立预测模型了。我将带大家了解六种模型,包括:线性拟合,决策树和随机森林,这些可以帮你杀入TOP20。
首先建立基准模型。它不需要预测模型,就像一种基于信息的猜测。例如:本例中我们将销售预测为总体平均销售。过程如下:

#Mean based:
mean_sales = train[‘Item_Outlet_Sales’].mean()#Define a dataframe with IDs for submission:
base1 = test[[‘Item_Identifier’,’Outlet_Identifier’]]
base1[‘Item_Outlet_Sales’] = mean_sales#Export submission file
base1.to_csv(“alg0.csv”,index=False)

公共领导榜排名:1773你是不是嫌它太过小儿科?如果你看一看现在的LB, 会发现有4名选手在后面。所以基准模型有助于你建立标准。如果你的预测算法如下,肯定是出现了严重错误,最终需要核对数据。如果你参加了AV datahacks或其他短程黑客马拉松,你会发现在5-10分钟左右就有人提交数据了。这不特别,其实就是基准解决方案,并不是高深莫测的科学。取总体平均数是最简单的方法。你还可以尝试:
1. 产品平均销售2. 特定类型商店里产品的平均销售这些应该可以提供更好的解决方案。

既然要建立很多模型,我们就不再重复编码,而是定义一个通用函数,把算法和数据作为输入,然后建立模型,运行交叉验证,最后生成提交。如果不喜欢函数,也可以选择其他更为复杂的方法。不过我现在越来越倾向于使用函数(事实上我有时使用过度)。函数如下:

#Define target and ID columns:
target = ‘Item_Outlet_Sales’
IDcol = [‘Item_Identifier’,’Outlet_Identifier’]
from sklearn import cross_validation, metrics
def modelfit(alg, dtrain, dtest, predictors, target, IDcol, filename):
#Fit the algorithm on the data
alg.fit(dtrain[predictors], dtrain[target])#Predict training set:
dtrain_predictions = alg.predict(dtrain[predictors])#Perform cross-validation:
cv_score = cross_validation.cross_val_score(alg, dtrain[predictors], dtrain[target], cv=20, scoring=’mean_squared_error’)
cv_score = np.sqrt(np.abs(cv_score))

#Print model report:
print “\nModel Report”
print “RMSE : %.4g” % np.sqrt(metrics.mean_squared_error(dtrain[target].values, dtrain_predictions))
print “CV Score : Mean – %.4g | Std – %.4g | Min – %.4g | Max – %.4g” % (np.mean(cv_score),np.std(cv_score),np.min(cv_score),np.max(cv_score))

#Predict on testing data:
dtest[target] = alg.predict(dtest[predictors])

#Export submission file:
IDcol.append(target)
submission = pd.DataFrame({ x: dtest[x] for x in IDcol})
submission.to_csv(filename, index=False)

我添加了标注。如果难以理解编码,欢迎在评论中留言。线性拟合模型我们现在建立第一个线性——拟合模型。

from sklearn.linear_model import LinearRegression, Ridge, Lasso
predictors = [x for x in train.columns if x not in [target]+IDcol]
# print predictors
alg1 = LinearRegression(normalize=True)
modelfit(alg1, train, test, predictors, target, IDcol, ‘alg1.csv’)
coef1 = pd.Series(alg1.coef_, predictors).sort_values()
coef1.plot(kind=’bar’, title=’Model Coefficients’)

公共领导榜排名:1202
可以看出这比基准模型排名靠前。不过,如果你注意到系数的话,会发现表示过度拟合的程度很高。为了实现这个目标,可以使用岭回归模型。想要了解更多关于Ridge & Lasso拟合技巧,请在公众号中回复“销售预测”即可下载!岭回归模型

predictors = [x for x in train.columns if x not in [target]+IDcol]
alg2 = Ridge(alpha=0.05,normalize=True)
modelfit(alg2, train, test, predictors, target, IDcol, ‘alg2.csv’)
coef2 = pd.Series(alg2.coef_, predictors).sort_values()
coef2.plot(kind=’bar’, title=’Model Coefficients’)

手把手带你进入TOP20的商超销售预测

公共领导榜:1203

尽管回归系数看起来更佳,但是排名几乎一样。可以微调模型参数使成绩更好,但我认为不会有显著进步。使用交叉验证发现也是收效甚微,所以不能指望有更出色的表现了。

决策树模型我们来尝试下决策树模型,看看结果是否会有所改善。

from sklearn.tree import DecisionTreeRegressor
predictors = [x for x in train.columns if x not in [target]+IDcol]
alg3 = DecisionTreeRegressor(max_depth=15, min_samples_leaf=100)
modelfit(alg3, train, test, predictors, target, IDcol, ‘alg3.csv’)
coef3 = pd.Series(alg3.feature_importances_, predictors).sort_values(ascending=False)
coef3.plot(kind=’bar’, title=’Feature Importances’)

手把手带你进入TOP20的商超销售预测

公共领导榜排名:1162

可以看出RMSE 是1058, CV 是1091,这告诉我们模型有些过度拟合。我们尝试一下只用4个首要变量建立决策树,max_depth 为8,min_samples_leaf 为150.

predictors = [‘Item_MRP’,’Outlet_Type_0′,’Outlet_5′,’Outlet_Years’]
alg4 = DecisionTreeRegressor(max_depth=8, min_samples_leaf=150)
modelfit(alg4, train, test, predictors, target, IDcol, ‘alg4.csv’)
coef4 = pd.Series(alg4.feature_importances_, predictors).sort_values(ascending=False)
coef4.plot(kind=’bar’, title=’Feature Importances’)

手把手带你进入TOP20的商超销售预测

公共领导榜排名:1157

也可以使用其他参数进一步微调,这一步留给大家尝试。

随机森林模型我们也尝试下随机森林,看看是否有改善。

from sklearn.ensemble import RandomForestRegressor
predictors = [x for x in train.columns if x not in [target]+IDcol]
alg5 = RandomForestRegressor(n_estimators=200,max_depth=5, min_samples_leaf=100,n_jobs=4)
modelfit(alg5, train, test, predictors, target, IDcol, ‘alg5.csv’)
coef5 = pd.Series(alg5.feature_importances_, predictors).sort_values(ascending=False)
coef5.plot(kind=’bar’, title=’Feature Importances’)

手把手带你进入TOP20的商超销售预测

公共领导排名:1154

你也许觉得改善不太明显,然而,随着模型越来越好,进哪怕微小的提高都变得难上加难。我们尝试一下max_depth 为 6 和tree为400的算法进行另一种随机森林。增加决策树数量使模型更加有效,然而计算成本也在提高。

predictors = [x for x in train.columns if x not in [target]+IDcol]
alg6 = RandomForestRegressor(n_estimators=400,max_depth=6, min_samples_leaf=100,n_jobs=4)
modelfit(alg6, train, test, predictors, target, IDcol, ‘alg6.csv’)
coef6 = pd.Series(alg6.feature_importances_, predictors).sort_values(ascending=False)
coef6.plot(kind=’bar’, title=’Feature Importances’)

手把手带你进入TOP20的商超销售预测

公共领导榜排名:1152

这又是一次增长性变化,它会送你进入TOP10甚至TOP5。进一步微调参数提高准确性。目前的模型把你送入TOP20已经绰绰有余。我曾试过基本的GBM,微调后名次到了TOP10。大家可以尝试用GBM和XGBoost集成方法这样更佳的算法进一步提高成绩。

本环节到此结束。如果你想要iPython notebook 形式的建模编码,请从GitHub repository下载。

尾注:本文带大家走遍了解决数据科学问题的全过程。首先,我们没有观察的情况下根据数据做出假设。其次,我们进行数据探索,发现数据里的细微偏差需要纠正。接着,我们进行数据清理和特征工程,输入丢失值,解决异常情况,建立新特征,还通过独热码使数据更加适应模型。最后,我们进行拟合,决策树和随机森林模型,并大致了解了如何微调能得出更好结果。我相信本文每位读者都能在商超销售预测中取得很好的成绩。对初学者而言,你的得分至少应为1150,对于已经遥遥领先的选手来说,可以使用这里的一些特征工程小技巧来进一步提高。祝大家一切顺利!你认为本文有用吗?你还可以做出其他更有趣的假设吗?你还创造了其他什么特征?你能够通过GBM 和 XGBoost获得很高分数吗?欢迎在下面的评论中留言与我们分享你的经历,或在讨论端口与我们互动。

【文件下载】链接: 销售预测 密码: v4ma

本文为专栏文章,来自:融智未来,内容观点不代表本站立场,如若转载请联系专栏作者,本文链接:https://www.afenxi.com/10722.html 。

发表评论

登录后才能评论

联系我们

如有建议:>>给我留言

QR code