机器学习入门 --- 逻辑回归 信用卡欺诈检测实战

机器学习|学习笔记 — 基于信用卡的交易记录数据建立分类模型,预测交易记录是否正常

任务流程

  • 加载数据,观察问题
    查看数据是否有缺失值、异常值或者无法直接使用的值
  • 针对问题给出解决方案
    根据数据用途提前构思,做出方案再进行预处理
  • 数据集切分
    为了对学习结果进行评估
  • 评估办法对比
  • 逻辑回归模型
    使用Sklearn工具包建模
  • 建模结果分析
    参数调整
  • 方案效果对比

观察数据

读入所需数据

data = pd.read_csv("creditcard.csv")
data.head()

原数据展示:

TimeV1V2V3V4V5V6V7V8V9V10V21V22V23V24V25V26V27V28AmountClass
0.0-1.359807-0.0727812.5363471.378155-0.3383210.4623880.2395990.0986980.3637870.090794-0.0183070.277838-0.1104740.0669280.128539-0.1891150.133558-0.021053149.620
0.01.1918570.2661510.1664800.4481540.060018-0.082361-0.0788030.085102-0.255425-0.166974-0.225775-0.6386720.101288-0.3398460.1671700.125895-0.0089830.0147242.690
1.0-1.358354-1.3401631.7732090.379780-0.5031981.8004990.7914610.247676-1.5146540.2076430.2479980.7716790.909412-0.689281-0.327642-0.139097-0.055353-0.059752378.660
1.0-0.966272-0.1852261.792993-0.863291-0.0103091.2472030.2376090.377436-1.387024-0.054952-0.1083000.005274-0.190321-1.1755750.647376-0.2219290.0627230.061458123.500
2.0-1.1582330.8777371.5487180.403034-0.4071930.0959210.592941-0.2705330.8177390.753074-0.0094310.798278-0.1374580.141267-0.2060100.5022920.2194220.21515369.990

查看本次数据的标签分布情况,做柱状图统计:

count_classes = pd.value_counts(data['Class'], sort = True).sort_index()
count_classes.plot(kind = 'bar')
plt.title("Bank card Fraud class histogram")
plt.xlabel("Class")
plt.ylabel("Frequency")
print(count_classes)

在这里插入图片描述
class = 0 有284315条
class = 1 有492条
对于本数据样本,两个类别的数据量相差太大,在学习过程中,对于 class=1 的部分学习的太少,在做预测时进来新的样本,会偏向 class=0 的类别

对于这种正负例偏差过大的数据集,有两种解决方法
方案一 :希望 0 和 1 的数量一样多 — over sample(过采样)
方案二 :希望 0 和 1 的数量一样少 — under sample(下采样)

当然,无论用哪种方案,都会有一种问题,就是打破了实际情况中的平衡,这个问题,在下面会有所说明

数据标准化处理

在原数据中我们看到,Amount 与其他的数据的差距较大,所以在训练时,它相对于其他特征数据,会对训练的结果产生较大的影响,所以需要对其进行标准化操作,减小不同特征数据之间的差距
标准化公式:
y = x i − μ i σ i y= \frac{x_i-\mu_i }{\sigma_i } y=σixiμi

在代码中,就直接使用 sklearn 的 fit_transform 进行标准化操作

from sklearn.preprocessing import StandardScaler
# 对Amount进行数据类型的转换并标准化操作
data['normAmount'] = StandardScaler().fit_transform(data['Amount'].reshape(-1, 1))
data = data.drop(['Time','Amount'],axis=1)
data.head()

至此就完成了数据的标准化处理,并将不需要的数据删除掉

标准化处理过后数据展示:

V1V2V3V4V5V6V7V8V9V10V21V22V23V24V25V26V27V28ClassnormAmount
-1.359807-0.0727812.5363471.378155-0.3383210.4623880.2395990.0986980.3637870.090794-0.0183070.277838-0.1104740.0669280.128539-0.1891150.133558-0.02105300.244964
1.1918570.2661510.1664800.4481540.060018-0.082361-0.0788030.085102-0.255425-0.166974-0.225775-0.6386720.101288-0.3398460.1671700.125895-0.0089830.0147240-0.342475
-1.358354-1.3401631.7732090.379780-0.5031981.8004990.7914610.247676-1.5146540.2076430.2479980.7716790.909412-0.689281-0.327642-0.139097-0.055353-0.05975201.160686
-0.966272-0.1852261.792993-0.863291-0.0103091.2472030.2376090.377436-1.387024-0.054952-0.1083000.005274-0.190321-1.1755750.647376-0.2219290.0627230.06145800.140534
-1.1582330.8777371.5487180.403034-0.4071930.0959210.592941-0.2705330.8177390.753074-0.0094310.798278-0.1374580.141267-0.2060100.5022920.2194220.2151530-0.073403

方案一:下采样数据集制作

数据处理完成后,因为正负例的数量差距过大的原因,这里进行下采样处理,以平衡在训练时正负例的比例

下采样方案:

# 设定特征数据及标签
X = data.ix[:, data.columns != 'Class']
y = data.ix[:, data.columns == 'Class']

# 得到所有异常样本的索引
number_records_fraud = len(data[data.Class == 1])
fraud_indices = np.array(data[data.Class == 1].index)

# 得到所有正常样本的索引
normal_indices = data[data.Class == 0].index

# 在正常样本中随机采样出指定个数的样本,并取其索引 // np.random.choice()在指定索引中选择指定的个数
random_normal_indices = np.random.choice(normal_indices, number_records_fraud, replace = False)
random_normal_indices = np.array(random_normal_indices)

# 有了正常和异常样本后把它们的索引都拿到手
under_sample_indices = np.concatenate([fraud_indices,random_normal_indices])

# 根据索引得到下采样所有样本点
under_sample_data = data.iloc[under_sample_indices,:]

X_undersample = under_sample_data.ix[:, under_sample_data.columns != 'Class']
y_undersample = under_sample_data.ix[:, under_sample_data.columns == 'Class']

# 下采样样本比例
print("正常样本所占整体比例: ", len(under_sample_data[under_sample_data.Class == 0])/len(under_sample_data))
print("异常样本所占整体比例: ", len(under_sample_data[under_sample_data.Class == 1])/len(under_sample_data))
print("下采样策略总体样本数量: ", len(under_sample_data))
正常样本所占整体比例:  0.5
异常样本所占整体比例:  0.5
下采样策略总体样本数量:  984
交叉验证设定

交叉验证:
在一个数据集中,我们训练时需要将其划分为训练集、验证集、测试集。训练集是用来训练模型的, 验证集是用来验证模型的, 测试集是用来评估模型的性能的。但如果在划分数据集时,固定了其中验证集的数据,如果该验证集的数据偏简单或者偏难,都无法很准确的反映出模型的质量,因此需要通过交叉验证来检测模型的质量,即使用多个验证集来验证模型并将结果平均后反映出来。

数据集划分:

from sklearn.model_selection import train_test_split

# 整个数据集进行划分
#设定随机数种子:random_state = 0,为了保证程序每次运行都分割一样的训练集和测试集
X_train, X_test, y_train, y_test = train_test_split(X,y,test_size = 0.3, random_state = 0)

print("原始训练集包含样本数量: ", len(X_train))
print("原始测试集包含样本数量: ", len(X_test))
print("原始样本总数: ", len(X_train)+len(X_test))

# 下采样数据集进行划分
X_train_undersample, X_test_undersample, y_train_undersample, y_test_undersample = train_test_split(X_undersample,
                                                                                                    y_undersample,
                                                                                                    test_size = 0.3,
                                                                                                    random_state = 0)
print("")
print("下采样训练集包含样本数量: ", len(X_train_undersample))
print("下采样测试集包含样本数量: ", len(X_test_undersample))
print("下采样样本总数: ", len(X_train_undersample)+len(X_test_undersample))
原始训练集包含样本数量:  199364
原始测试集包含样本数量:  85443
原始样本总数:  284807

下采样训练集包含样本数量:  688
下采样测试集包含样本数量:  296
下采样样本总数:  984

在此不仅将下采样数据进行了划分,也在原始数据集中进行了划分,目的就是为了能够在测试时,更准确的得到模型的质量,因为在下采样数据集中,class = 0class = 1 = 1:1,而真实情况是class = 0class = 1 = 284315:492,如果用下采样数据集来进行测试,会使得到的模型质量数据偏高,不能正常的反映真实情况。

因此,在训练时会做如下设定:
训练使用 X_train_undersample y_train_undersample
测试使用 X_test y_test

模型评估方法与召回率设定

召回率:模型在任务目标当中做的效果如何

R e c a l l = T P / ( T P + F N ) Recall = TP/(TP+FN) Recall=TP/(TP+FN)

TP:True Positives 正类判断为正类
FP:False Positives 负类判断为正类

在本文中,我们关注的是诈骗的信息,因此将诈骗数据作为正类,非诈骗数据作为负类
TP:正确的将诈骗数据识别出来
FP:错误的将非诈骗数据识别为诈骗数据

正则化惩罚项

正则化惩罚项只对权重参数进行惩罚

惩罚函数公式: R ( W ) = ∑ ( W ) 2 R(W)=\sum (W)^2 R(W)=(W)2

训练时的损失函数不仅有数据的损失,还会加上对模型的正则化惩罚损失,公式如下:
L O S S = d a t a _ l o s s + λ R ( W ) LOSS = data\_loss + \lambda R(W) LOSS=data_loss+λR(W)

训练逻辑回归模型

这里,我们将使用不同的正则化惩罚力度参数进行比较

from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import KFold, cross_val_score
from sklearn.metrics import confusion_matrix,recall_score,classification_report 

def printing_Kfold_scores(x_train_data,y_train_data):
    #数据分成五等分
    fold = KFold(len(y_train_data),5,shuffle=False) 

    # 定义不同力度的正则化惩罚力度
    c_param_range = [0.01,0.1,1,10,100]
    #展示结果用的表格
    results_table = pd.DataFrame(index = range(len(c_param_range),2), columns = ['C_parameter','Mean recall score'])
    results_table['C_parameter'] = c_param_range
    # k-fold 表示K折的交叉验证,这里会得到两个索引集合:训练集 = indices[0],验证集 = indices[1]
    j = 0
    #循环遍历不同的参数
    for c_param in c_param_range:
        print('-------------------------------------------')
        print('正则化惩罚力度: ', c_param)
        print('-------------------------------------------')
        print('')

        recall_accs = []
        # 一步步分解执行交叉验证
        for iteration, indices in enumerate(fold,start=1):
            '''
            LogisticRegression()
            C:float, default=1.0
            Inverse of regularization strength; must be a positive float. 
            Like in support vector machines, smaller values specify stronger regularization.
            
            通俗地讲:正则化强度是你传入的数的倒数,较小的值指定更强的正则化
            '''
            # 指定算法模型,并且给定参数
            lr = LogisticRegression(C = c_param, penalty = 'l1')

            # 训练模型,注意索引不要给错了,训练的时候一定传入的是训练集,所以 X 和 y 的索引都是 0
            lr.fit(x_train_data.iloc[indices[0],:],y_train_data.iloc[indices[0],:].values.ravel())

            # 建立好模型后,预测模型结果,这里用的就是验证集,索引为 1
            y_pred_undersample = lr.predict(x_train_data.iloc[indices[1],:].values)

            # 有了预测结果之后就可以进行评估了,这里 recall_score 需要传入预测值和真实值
            recall_acc = recall_score(y_train_data.iloc[indices[1],:].values,y_pred_undersample)
            #保存每一步的结果,等待后续操作中求平均
            recall_accs.append(recall_acc)
            print('Iteration ', iteration,': 召回率 = ', recall_acc)

        # 当执行完所有的交叉验证之后,计算平均结果
        results_table.loc[j,'Mean recall score'] = np.mean(recall_accs)
        j += 1
        print('')
        print('平均召回率 ', np.mean(recall_accs))
        print('')

    best_c = results_table.loc[results_table['Mean recall score'].astype("float32").idxmax()]['C_parameter']
    
    # Finally, we can check which C parameter is the best amongst the chosen.
    print('*********************************************************************************')
    print('效果最好的模型所选的参数 = ', best_c)
    print('*********************************************************************************')
    
    return best_c
best_c = printing_Kfold_scores(X_train_undersample,y_train_undersample)

输出结果如下:

-------------------------------------------
正则化惩罚力度:  0.01
-------------------------------------------

Iteration  1 : 召回率 =  0.9315068493150684
Iteration  2 : 召回率 =  0.9178082191780822
Iteration  3 : 召回率 =  1.0
Iteration  4 : 召回率 =  0.9594594594594594
Iteration  5 : 召回率 =  0.9696969696969697

平均召回率  0.955694299529916

-------------------------------------------
正则化惩罚力度:  0.1
-------------------------------------------

Iteration  1 : 召回率 =  0.8356164383561644
Iteration  2 : 召回率 =  0.863013698630137
Iteration  3 : 召回率 =  0.9491525423728814
Iteration  4 : 召回率 =  0.9324324324324325
Iteration  5 : 召回率 =  0.8939393939393939

平均召回率  0.8948309011462019

-------------------------------------------
正则化惩罚力度:  1
-------------------------------------------

Iteration  1 : 召回率 =  0.8493150684931506
Iteration  2 : 召回率 =  0.8767123287671232
Iteration  3 : 召回率 =  0.9661016949152542
Iteration  4 : 召回率 =  0.9594594594594594
Iteration  5 : 召回率 =  0.9090909090909091

平均召回率  0.9121358921451794

-------------------------------------------
正则化惩罚力度:  10
-------------------------------------------

Iteration  1 : 召回率 =  0.863013698630137
Iteration  2 : 召回率 =  0.8767123287671232
Iteration  3 : 召回率 =  0.9661016949152542
Iteration  4 : 召回率 =  0.9459459459459459
Iteration  5 : 召回率 =  0.9242424242424242

平均召回率  0.9152032185001768

-------------------------------------------
正则化惩罚力度:  100
-------------------------------------------

Iteration  1 : 召回率 =  0.863013698630137
Iteration  2 : 召回率 =  0.8767123287671232
Iteration  3 : 召回率 =  0.9830508474576272
Iteration  4 : 召回率 =  0.9594594594594594
Iteration  5 : 召回率 =  0.9242424242424242

平均召回率  0.921295751711354

*********************************************************************************
效果最好的模型所选的参数 =  0.01
*********************************************************************************

在此输出结果中可以看到:

  1. 不同的验证集之间召回率有十几个百分点之差
  2. 正则化惩罚力度参数的不同,会有较大的差别,对于 [0.01,0.1,1,10,100]这五个数据而言,验证结果有六个百分点之差

所以对于训练过程中,正则化惩罚力度参数的设定是很重要的

本段代码的解释基本都在注释中讲清楚了,如有问题可前往 Sklearn 官方API文档 查询API官方解释

到目前为止,我们还没有用到测试集来测试,接下来就使用测试集进行模型评估

混淆矩阵评估分析

对于画图类的函数,就直接使用画图模板,使用的时候拿来调用即可

def plot_confusion_matrix(cm, classes,
                          title='Confusion matrix',
                          cmap=plt.cm.Blues):
    """
    classes: 分类的类别数
    
    绘制混淆矩阵
    """
    plt.imshow(cm, interpolation='nearest', cmap=cmap)
    plt.title(title)
    plt.colorbar()
    tick_marks = np.arange(len(classes))
    plt.xticks(tick_marks, classes, rotation=0)
    plt.yticks(tick_marks, classes)

    thresh = cm.max() / 2.
    for i, j in itertools.product(range(cm.shape[0]), range(cm.shape[1])):
        plt.text(j, i, cm[i, j],
                 horizontalalignment="center",
                 color="white" if cm[i, j] > thresh else "black")

    plt.tight_layout()
    plt.ylabel('True label')
    plt.xlabel('Predicted label')

在这里计算混淆矩阵的值并调用 plot_confusion_matrix() 函数进行绘图查看预测结果:

import itertools
lr = LogisticRegression(C = best_c, penalty = 'l2')
lr.fit(X_train_undersample,y_train_undersample.values.ravel())
y_pred_undersample = lr.predict(X_test_undersample.values)

# 计算混淆矩阵值
cnf_matrix = confusion_matrix(y_test_undersample,y_pred_undersample)
np.set_printoptions(precision=2)

print("召回率: ", cnf_matrix[1,1]/(cnf_matrix[1,0]+cnf_matrix[1,1]))

# 绘制图像
class_names = [0,1]
plt.figure()
plot_confusion_matrix(cnf_matrix
                      , classes=class_names
                      , title='Confusion matrix')
plt.show()

在这里插入图片描述
虽然现在的召回率 = 0.9387755102040817 ,看上去已经是一个比较不错的结果了,但是我们以上所有的评估都是基于下采样这个大环境下进行的,但是真实环境中正负样本的比例并不是 1:1

接下来,我们在真实环境下,再进行一次混淆矩阵的计算绘图:

lr = LogisticRegression(C = best_c, penalty = 'l2')
lr.fit(X_train_undersample,y_train_undersample.values.ravel())
y_pred = lr.predict(X_test.values)

# 计算混淆矩阵值
cnf_matrix = confusion_matrix(y_test,y_pred)
np.set_printoptions(precision=2)

print("召回率: ", cnf_matrix[1,1]/(cnf_matrix[1,0]+cnf_matrix[1,1]))

# 绘制图像
class_names = [0,1]
plt.figure()
plot_confusion_matrix(cnf_matrix
                      , classes=class_names
                      , title='Confusion matrix')
plt.show()

在这里插入图片描述
在这个结果中,召回率 = 0.9183673469387755 ,还算可以。
虽然诈骗数据被误判的数量是12个,与上一结果相差不大,但是对非诈骗数据的预测却误判了 9349 条为诈骗数据,所以这里就显现出了因下采样导致的误杀的增多

查看阈值对结果产生的影响
#用之前最好的参数来进行建模
lr = LogisticRegression(C = 0.01, penalty = 'l1')
#训练模型,同样用下采样模型
lr.fit(X_train_undersample,y_train_undersample.values.ravel())
#得到预测结果的概率值
y_pred_undersample_proba = lr.predict_proba(X_test_undersample.values)
#指定不同的阈值
thresholds = [0.1,0.2,0.3,0.4,0.5,0.6,0.7,0.8,0.9]

plt.figure(figsize=(10,10))

j = 1
# 用混淆矩阵进行展示
for i in thresholds:
    y_test_predictions_high_recall = y_pred_undersample_proba[:,1] > i
    
    plt.subplot(3,3,j)
    j += 1
    
    # 计算混淆矩阵值
    cnf_matrix = confusion_matrix(y_test_undersample,y_test_predictions_high_recall)
    np.set_printoptions(precision=2)

    print("给定的阈值为: {} 时,测试集召回率: ".format(i), cnf_matrix[1,1]/(cnf_matrix[1,0]+cnf_matrix[1,1]))

    # 绘制图像
    class_names = [0,1]
    plot_confusion_matrix(cnf_matrix
                          , classes=class_names
                          , title='Threshold >= %s'%i) 

输出结果如下:
在这里插入图片描述
随着阈值的增大,召回率一直降低,是因为随着阈值增大,判断标准逐渐提高,对于一些概率小的诈骗数据,会分类到非诈骗数据中去。

对于输出的混淆矩阵的内容可以看出

  1. 阈值在 0.1,0.2,0.3 时,召回率为1,但是它们几乎没有判断能力
  2. 阈值在 0.5,0.6 时,虽然有漏检,但是误杀的数量越来越少
  3. 随着阈值越来越大,漏检的数量就会越来越多

因此,阈值在 0.5,0.6 附近是一个比较不错的结果

方案二:SMOTE过采样方案

SMOTE算法:

  1. 只对少数样本操作
  2. 对于少数类中每一个样本x,以欧氏距离为标准计算它到少数类样本集中所有样本的距离,得到其k近邻
  3. 对各个样本间距离进行排序
  4. 从其k近邻中随机选择若干个样本

公式:
x ′ = x + r a n d ( 0 , 1 ) ∗ Δ x {x}'= x + rand(0,1)*\Delta x x=x+rand(0,1)Δx

credit_cards=pd.read_csv('creditcard.csv')
# 拿到数据名称
columns=credit_cards.columns
# 在特征中去掉标签
features_columns=columns.delete(len(columns)-1)
# 拿到所有特征数据
features=credit_cards[features_columns]
# 拿到所有标签
labels=credit_cards['Class']
# 对数据集进行切分
features_train, features_test, labels_train, labels_test = train_test_split(features, 
                                                                            labels, 
                                                                            test_size=0.2, 
                                                                            random_state=0)
#基于SMOTE算法来进行样本生成,这样的正负例样本数量就是一致的了
oversampler=SMOTE(random_state=0)
os_features,os_labels=oversampler.fit_sample(features_train,labels_train)
# 建模训练
os_features = pd.DataFrame(os_features)
os_labels = pd.DataFrame(os_labels)
best_c = printing_Kfold_scores(os_features,os_labels)

输出结果:

-------------------------------------------
正则化惩罚力度:  0.01
-------------------------------------------
Iteration  0 : 召回率 =  0.8838709677419355
Iteration  1 : 召回率 =  0.875
Iteration  2 : 召回率 =  0.9636162443288702
Iteration  3 : 召回率 =  0.952616480364032
Iteration  4 : 召回率 =  0.9549246545982129

平均召回率  0.9260056694066101

-------------------------------------------
正则化惩罚力度:  0.1
-------------------------------------------
Iteration  0 : 召回率 =  0.8903225806451613
Iteration  1 : 召回率 =  0.875
Iteration  2 : 召回率 =  0.9441850171517097
Iteration  3 : 召回率 =  0.9548367241511964
Iteration  4 : 召回率 =  0.9554962025038195

平均召回率  0.9239681048903773

-------------------------------------------
正则化惩罚力度:  1
-------------------------------------------
Iteration  0 : 召回率 =  0.8838709677419355
Iteration  1 : 召回率 =  0.875
Iteration  2 : 召回率 =  0.9441850171517097
Iteration  3 : 召回率 =  0.9553423242215408
Iteration  4 : 召回率 =  0.9556830547037294

平均召回率  0.922816272763783

-------------------------------------------
正则化惩罚力度:  10
-------------------------------------------
Iteration  0 : 召回率 =  0.8903225806451613
Iteration  1 : 召回率 =  0.875
Iteration  2 : 召回率 =  0.9442071483899525
Iteration  3 : 召回率 =  0.9526934195051714
Iteration  4 : 召回率 =  0.9552434024686473

平均召回率  0.9234933102017866

-------------------------------------------
正则化惩罚力度:  100
-------------------------------------------
Iteration  0 : 召回率 =  0.8903225806451613
Iteration  1 : 召回率 =  0.875
Iteration  2 : 召回率 =  0.9442071483899525
Iteration  3 : 召回率 =  0.9548696980688276
Iteration  4 : 召回率 =  0.9552434024686473

平均召回率  0.9239285659145178

*********************************************************************************
效果最好的模型所选的参数 =  0.01
*********************************************************************************
lr = LogisticRegression(C = best_c, penalty = 'l2')
lr.fit(os_features,os_labels.values.ravel())
y_pred = lr.predict(features_test.values)

# Compute confusion matrix
cnf_matrix = confusion_matrix(labels_test,y_pred)
np.set_printoptions(precision=2)

print("召回率: ", cnf_matrix[1,1]/(cnf_matrix[1,0]+cnf_matrix[1,1]))

# Plot non-normalized confusion matrix
class_names = [0,1]
plt.figure()
plot_confusion_matrix(cnf_matrix
                      , classes=class_names
                      , title='Confusion matrix')
plt.show()

在这里插入图片描述
使用测试集评估模型,召回率的值为 0.8910891089108911,相比于下采样方法有所下降,其中一方面原因也是因为数据是我们自己造的,与真实数据多少有点差距,结果也就差了一些。但错杀的数量大大降低了,模型的性能也是得到了一些提升

总结:

  1. 对数据进行观察,看看数据有什么问题,针对这些问题来选择解决方案
  2. 在这里,使用了两种方案:下采样、过采样,分别训练并进行对比试验
  3. 在建模之前,对数据要进行各种预处理,比如数据预处理、确实只补充等问题
  4. 先选好评估方法,在进行建模
  5. 选择合适的算法
  6. 调节模型参数,模型的调参也是非常重要的,通过本次实验我们也能看出不同的参数对结果产生较大的影响
  7. 得到的结果要与实际结果相结合
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值