Pyspark:特征处理(ml.feature包的使用)

写在最前

  本篇主要介绍Pyspark.ml.feature中各个类的作用及使用方法,但不会详细到所有类都一一介绍。在正式介绍之前,有以下几点需要说明:

  • 为行文方便,本文依照各个类的作用将其分为以下几种:特征变换、特征选择、特征降维、自然语言处理及向量操作。
  • ml.feature包中有些类配备了与其同名的Model类,比如Imputer和ImputerModel类。若有同名Model类,则在使用时需要先进行fit()操作转化为对应的同名Model类对象,再经过transform()方法得到转化处理后的数据;若没有同名Model类,则可以直接使用transform()方法得到转化后的数据。
  • 下文使用了自定义函数vector_format()来控制vector向量中数值的小数位数。之所以这样操作,只是为了能清晰展示转化后的数据,方便校验结果。在实际的项目中,并不需要这样做。
  • 下文提到的特征与字段同义。

1 特征变换

  ml.feature包中与特征处理相关的类主要包括以下几种,具体如下表:

名称作用
Binarizer连续字段二值化。当字段值超过指定阈值时,其值转化为1,否则为0。
Bucketizer根据自定义的分箱区间对特征进行分箱。
QuantileDiscretizer按百分位数对字段进行分箱。
Imputer、ImputerModel对字段中的缺失值进行填充。
MaxAbsScaler、MaxAbsScalerModel将字段映射到指定范围 [ m i n , m a x ] [min,max] [min,max]之间。
MinMaxScaler、MinMaxScalerModel最大最小归一化方法。将字段映射到 [ 0 , 1 ] [0,1] [0,1]之间。
StandardScaler、StandardScalerModel对特征进行标准化转换。
RobustScaler、RobustScalerModel删除中值并根据分位数范围缩放数据。
Normalizer使用 p p p范数对向量(按行)进行规范化。
VectorIndexer、VectorIndexerModel将分类特征中的分类要素重新映射编码。
StringIndexer、StringIndexerModel将字符串型的分类特征重新映射编码。
IndexToStringStringIndexer的逆操作。
DCT对字段进行一维离散余弦变换。
ElementwiseProduct将特征与指定的权重相乘。
RFormula、RFormulaModel使用R语言中的公式对字段进行转换。
SQLTransformer使用SQL语句对字段进行转换。
PolynomialExpansion对特征进行多项式扩展。
OneHotEncoder、OneHotEncoderModel对特征进行one-hot编码。 具体地,采用的是哑变量编码。
Interaction实现特征叉积向量。
1.1 离散化处理

  ml.feature包中对连续变量进行离散化处理的类主要有:Binarizer、Bucketizer和QuantileDiscretizer。其具体用法如下:

from pyspark.sql import SparkSession
from pyspark.ml.feature import *
import os
from pyspark.ml.linalg import Vectors
from pyspark.sql import functions as func
from pyspark.sql.types import *

os.environ['SPARK_HOME'] ='/Users/sherry/documents/spark/spark-3.2.1-bin-hadoop3.2'
spark=SparkSession.builder.appName('ml').getOrCreate()

vector_format=func.udf(lambda x:[round(item,2) for item in x.toArray().tolist()],
                       ArrayType(FloatType()))
                       
data=[[1.2,0.6],[0.5,0.5],
      [0.6,float("nan")],[0.4,0.2]]
df=spark.createDataFrame(data,['A','B'])
#对变量进行二值化
binarizer=Binarizer(threshold=0.5,inputCol='A',outputCol='A_bin')
df_bin=binarizer.transform(df)
df_bin.show()

#对变量进行分箱
bucket=Bucketizer(splits=[0,0.3,0.6,0.9,float('inf')],inputCol='B',
                  outputCol='B_buck',handleInvalid='keep')
df_buck=bucket.transform(df)
df_buck.show()

#同时对A\B两个特征进行百分数分箱
qd=QuantileDiscretizer(numBucketsArray=[3,4],inputCols=['A','B'],
                       outputCols=['A_qd',"B_qd"],
                       relativeError=0.01,
                       handleInvalid='keep')
qd_model=qd.fit(df)
#qd_model.getSplitsArray() 可以查看具体的分箱区间
df_qd=qd_model.transform(df)
df_qd.show()

#skip:删除
qd=QuantileDiscretizer(numBuckets=4,inputCol='B',
                       outputCol="B_qd_skip",
                       relativeError=0.01,
                       handleInvalid='skip')
qd_model_skip=qd.fit(df)
df_qd_skip=qd_model_skip.transform(df)
df_qd_skip.show()

其结果如下:
在这里插入图片描述
这里Bucketizer()和QuantileDiscretizer()类中都有一个参数handleInvalid。这个参数主要用于指定包括空值在内的非法输入的处理方式,其取值主要有以下几种:

  • keep: 把非法输入分配到特定的箱中。上述代码中QuantileDiscretizer()中将空值分配到了标号为4的分箱中,合法数据分配到标号0-3的分箱中。
  • error: 不处理,抛出异常;
  • skip: 过滤。有缺失值的记录会被剔除。如上图中的df_qd_skip。
1.2 归一化处理

  ml.feature包中常用归一化的类主要有:MaxAbsScaler、MinMaxScaler、StandardScaler、RobustScaler和Normalizer。其归一化公式如下:

  • MaxAbsScaler: x ′ = x m a x ( ∣ x ) ∣ x^{'}=\frac{x}{max(|x)|} x=max(x)x
  • MinMaxScaler: x ′ = x − m i n ( x ) m a x ( x ) − m i n ( x ) ∗ ( m a x − m i n ) + m i n x^{'}=\frac{x-min(x)}{max(x)-min(x)}*(max-min)+min x=max(x)min(x)xmin(x)(maxmin)+min,其中 m i n 、 m a x min、max minmax由参数指定
  • StandardScaler: x ′ = x − μ σ x^{'}=\frac{x-\mu}{\sigma} x=σxμ,其中 μ , σ \mu,\sigma μ,σ为变量 x x x的均值及方差
  • RobustScaler:: x ′ = x r a n g e ( x ) x^{'}=\frac{x}{range(x)} x=range(x)x,其中 r a n g e ( x ) range(x) range(x)由变量 x x x相应的分位数决定。默认情况下,将变量中的的中位数删除之后, r a n g e ( x ) range(x) range(x)为IQR(75%分位数-25%分位数)。
  • Normalizer:使用 p p p范数对向量(按行)进行归一化。

具体用法举例如下:

data=[(Vectors.dense([1.2,0.6]),),
      (Vectors.dense([0.5,0.5]),),
      (Vectors.dense([0.6,0.9]),),
      (Vectors.dense([0.4,0.2]),)]
df=spark.createDataFrame(data,['A'])
#最大最小归一化
mm_scaler=MinMaxScaler(inputCol='A',outputCol='A_mm')
mm_model=mm_scaler.fit(df)
df_mm=mm_model.transform(df)
df_mm.select('A',vector_format('A_mm').alias('A_mm')).show()
#RobustScaler
rb_scaler=RobustScaler(inputCol="A",outputCol='A_rb',lower=0.6,upper=0.8)
rb_model=rb_scaler.fit(df)
df_rb=rb_model.transform(df)
df_rb.select('A',vector_format('A_rb').alias('A_rb')).show()
#p范数归一化
np_scaler=Normalizer(p=1,inputCol="A",outputCol="A_np")
df_np=np_scaler.transform(df)
df_np.select('A',vector_format('A_np').alias('A_np')).show()

其结果如下:
在这里插入图片描述
Tips:这里要注意,以上这些类只能处理Vectors型的数据。以下大部分类都是如此,不再一一说明。如果输入的数据类型不对,会产生如下类似错误IllegalArgumentException: requirement failed: Column A must be of type class org.apache.spark.ml.linalg.VectorUDT: struct<type:tinyint,size:int,indices:array,values:array> but was actually class org.apache.spark.sql.types.DoubleType$:double.

1.3 分类变量映射

  ml.feature于分类变量映射有关的类主要有:VectorIndexer、StringIndexer和IndexToString类。VectorIndexer和StringIndexer的类的作用基本相同,都可以对分类型特征中的类型元素重新编号。但VectorIndexer能接收的数据类型为vector型,而StringIndexer可以直接处理字符串型的特征。VectorIndexer()的具体用法如下:

df=spark.createDataFrame([(Vectors.dense([1.0,-1]),),
                          (Vectors.dense([0.5,-1]),),
                          (Vectors.dense([3,2]),)],['A'])
vecidx=VectorIndexer(maxCategories=2,inputCol='A',outputCol='A_indexed')
vecidx_model=vecidx.fit(df)
#categoryMaps可以查看每个特征的具体编号映射结果。
print(vecidx_model.categoryMaps)
df_vecidx=vecidx_model.transform(df)
df_vecidx.show()

vecidx1=VectorIndexer(maxCategories=4,inputCol='A',outputCol='A_indexed')
vecidx_model1=vecidx1.fit(df)
print(vecidx_model1.categoryMaps)
df_vecidx1=vecidx_model1.transform(df)
df_vecidx1.show()

其结果如下
在这里插入图片描述
Tips:如果特征中的不同值的个数超过了指定的maxCategories,那么会将该类当作连续型特征,不会对其进行变换。
  StringIndexer和IndexToString类的使用方法如下:

df=spark.createDataFrame([['a','b'],
                          ['b','c'],
                          ['b','d']],['A','B'])

str2idx=StringIndexer(inputCols=['A','B'],outputCols=['A_index','B_index'],
                      stringOrderType='alphabetDesc')
str2idx_model=str2idx.fit(df)
df_str2idx=str2idx_model.transform(df)
df_str2idx.show()

idx2str=IndexToString(inputCol='A_index',outputCol='A_new')
df_idx2str=idx2str.transform(df_str2idx)
df_idx2str.show()

其结果如下:
在这里插入图片描述
StringIndexer可以通过参数stringOrderType设置字符串排序方式,支持的取值有:frequencyDesc, frequencyAsc, alphabetDesc, alphabetAsc。

1.4 特征衍生

  ml.feature包中特征衍生相关的类有:PolynomialExpansion(多项式衍生)、OneHotEncoder(one-hot编码)。其具体用法如下:

data=[(Vectors.dense([1.2,0.6]),),
      (Vectors.dense([0.5,0.5]),),
      (Vectors.dense([0.6,0.9]),),
      (Vectors.dense([0.4,0.2]),)]
df=spark.createDataFrame(data,['A'])
#多项式扩展
poly=PolynomialExpansion(degree=2,inputCol='A',outputCol='A_poly')
df_poly=poly.transform(df)
df_poly.select('A',vector_format('A_poly').alias('A_poly')).show(truncate=False)

#onehot
df=spark.createDataFrame([[1],[2],[0]],['A'])
onehot=OneHotEncoder(inputCol='A',outputCol='A_onehot')
oh_model=onehot.fit(df)
df_oh=oh_model.transform(df)
df_oh.select('A',vector_format('A_onehot').alias('A_onehot')).show()

其结果如下:
在这里插入图片描述
关于OneHotEncoder类有以下几点需要说明:

  • OneHotEncoder()中inputCol或inputCols中指定的字段必须是整数,OneHotEncoder会依据这些数值生成转化后的向量,该转后的向量在该数值对应的维度上位1,其余位置为0。
  • OneHotEncoder()实现的的并不是标准的One-Hot编码,而是哑变量编码。

结合以上对OneHotEncoder的转化结果进行说明:字段中的最大值即为转后的向量的维度,注意不是字段中不同值的总数;字段中的最大值转后的向量为零向量。

1.5 其他方法

  除了以上介绍的类之外,ml.feature包中还有其他方法。比如:DCT、ElementwiseProduct、RFormula和SQLTransformer。其具体用法如下:

data=[[1.2,0.6],
     [0.5,0.5],
     [0.6,0.9],
     [0.4,0.2]]
df=spark.createDataFrame(data,['A','B'])
# SQL
sql=SQLTransformer(statement="select *,A*5 as A1,B-1 AS B1,A*B AS AB FROM __THIS__")
df_sql=sql.trnsform(df)
df_sql.select('A','B','A1',
              func.round('B1',2).alias('B1'),
              func.round('AB',2).alias('AB')).show()
              
#Interaction叉积
inter=Interaction(inputCols=['A','B'],outputCol='AB_inter')
df_inter=inter.transform(df)
df_inter.select('A','B',vector_format('AB_inter').alias('AB_inter')).show()

#DCT
data=[(Vectors.dense([1.2,0.6]),),
      (Vectors.dense([0.5,0.5]),),
      (Vectors.dense([0.6,0.9]),),
      (Vectors.dense([0.4,0.2]),)]
df=spark.createDataFrame(data,['A'])
dct=DCT(inputCol='A',outputCol='A_dct')
df_dct=dct.transform(df)
df_dct.select('A',vector_format('A_dct').alias('A_dct')).show()

#elementwise
element=ElementwiseProduct(inputCol='A',outputCol='A_element',
                         scalingVec=[0.4,0.2])
df_element=element.transform(df)
df_element.select('A',vector_format('A_element').alias('A_element')).show()

其结果如下:
在这里插入图片描述
关于以上各个类,需要说明以下几点:

  • SQLTransformer中支持的SQL语法如下:SELECT … FROM THIS where THIS (__THIS代表当前的数据表)。
  • DCT:假设有 N N N点的序列信号 { x ( n ) } ; n = 0 , 1 , … , N − 1 \{x(n)\};n=0,1,\dots,N-1 {x(n)};n=0,1,,N1,其中 x ( n ) x(n) x(n)是这个 N N N点的序列信号在第 n n n点的信号幅值,那么这个 N N N点的序列信号的一位离散N点余弦变换的定义为: y ( k ) = 2 N c ( k ) ∑ n = 0 N − 1 x ( n ) c o s ( 2 n + 1 ) k π 2 N y(k)=\sqrt\frac{2}{N}c(k)\sum_{n=0}^{N-1}x(n)cos\frac{(2n+1)k\pi}{2N} y(k)=N2 c(k)n=0N1x(n)cos2N(2n+1)其中 c ( k ) = { 1 2 k = 0 1 k ≠ 0 c(k)=\begin{cases}\sqrt\frac{1}{2}&k=0\\1&k\neq0\end{cases} c(k)={21 1k=0k=0
    DCT()类中可以通过参数inverse来指定计算方向,当inverse为False(默认)时,DCT会按行(在向量上)进行变换;当为True时,DCT会按列(在特征上)进行变换。
  • Interaction: 两个特征相乘。
  • ElementElementwiseProduct:将特征中的每个值与指定的维度相乘。

2 特征选择

  ml.feature包中与特征选择相关的类主要包括以下几种,具体如下表:

名称作用
ChiSqSelector、ChiSqSelectorModel使用卡方检验对变量进行筛选
UnivariateFeatureSelector、UnivariateFeatureSelectorModel使用单变量统计检验对变量进行筛选。目前支持的检测方法有:chi-squared, ANOVA F-test and F-value
VarianceThresholdSelector、VarianceThresholdSelectorModel使用方差对变量进行筛选

其具体用法如下:

df = spark.createDataFrame([(Vectors.dense([0.0, 0.0, 18.0, 1.0,14.0]), 1.0),
                            (Vectors.dense([0.0, 1.0, 12.0, 0.0,2.0]), 0.0),
                            (Vectors.dense([1.0, 0.0, 15.0, 0.1,4.0]), 0.0)],
                           ["features", "label"])

CS_model = ChiSqSelector(numTopFeatures=3,
                         featuresCol='features',
                         labelCol="label",
                         outputCol="selectedFeatures")
df_cs=CS_model.fit(df).transform(df)
df_cs.show(truncate=False)

Uni_model=UnivariateFeatureSelector(featuresCol='features',
                                    labelCol='label',
                                    selectionMode='fpr',
                                    outputCol='selectedFeatures')
Uni_model.setFeatureType('continuous')
Uni_model.setLabelType('categorical')
Uni_model.setSelectionThreshold(0.3)
df_uni=Uni_model.fit(df).transform(df)
df_uni.show(truncate=False)

Var_model=VarianceThresholdSelector(featuresCol='features',
                                    outputCol='selectedFeatures',
                                    varianceThreshold=3)
df_var=Var_model.fit(df).transform(df)
df_var.show(truncate=False)

其结果如下:
在这里插入图片描述使用UnivariateFeatureSelector时需要注意以下几点:

  • 需要使用setFeatureType()和setLabelType()方法指定特征及标签字段的类型。主要类型有:categorical、continuous。
  • UnivariateFeatureSelector根据指定的特征和标签字段的类型,指定不同的单变量检测方法。
  • UnivariateFeatureSelector使用selectionMode参数指定不同的特征选择模式,具体支持的模式有:numTopFeatures, percentile, fpr, fdr, fwe。并通过setSelectionThreshold()方法指定特征选择的阈值。注意,不同的特征选择模式其对应的阈值的数值类型不同。

3 特征降维

  ml.feature包中与特征降维相关的类主要包括以下几种,具体如下表:

名称作用
BucketedRandomProjectionLSH、BucketedRandomProjectionLSHModel使用欧式距离下的局部敏感哈希算法对数据进行降维
MinHashLSH、MinHashLSHModel使用minhash算法对特征进行降维
PCA、PCAModel对特征进行PCA降维

  关于使用主成分分析方法(PCA)和局部敏感哈希算法(LSH)进行降维的原理不再赘述,可以参考其他两篇博客:

这里仅列举局部敏感哈希的使用方法,具体如下:

data = [(0, Vectors.dense([-0.9, -5.7,4.6,0]),),
        (1, Vectors.dense([-0.5, 3.8,-9,0.4 ]),),
        (2, Vectors.dense([2.3, -1.2,0.3,-0.5 ]),),
        (3, Vectors.dense([0.9, 1.3,0.5,4.3]),)]
df = spark.createDataFrame(data, ["id", "features"])
##欧式距离下的LSH
brp = BucketedRandomProjectionLSH(inputCol="features",
                                  outputCol="brp_hash",
                                  numHashTables=2,
                                  bucketLength=3,
                                  seed=123)
brp_model=brp.fit(df)
df_brp=brp_model.transform(df)
df_brp.show()

#Jaccard距离下的LSH
data = [(0, Vectors.sparse(6, [0, 1, 2], [1.0, 1.0, 1.0]),),
        (1, Vectors.sparse(6, [2, 3, 4], [1.0, 1.0, 1.0]),),
        (2, Vectors.sparse(6, [0, 2, 4], [1.0, 1.0, 1.0]),)]
df = spark.createDataFrame(data, ["id", "features"])
minhash=MinHashLSH(inputCol="features",
               outputCol="min_hash",
               numHashTables=2)
minhash_model=minhash.fit(df)
df_minhash=minhash_model.transform(df)
df_minhash.show(truncate=False)

其结果如下:
在这里插入图片描述
使用局部敏感哈希方法进行降维的时候,这里要注意以下几点:

  • 使用局部敏感哈希算法进行降维会将实数向量转化为整数向量;
  • setNumHashTables方法可以控制降维后向量的维度;
  • MinHashLSH类中数据集中所有的非0值都当作1处理;
  • 这里MinHashLSH类使用的哈希函数族与上述网页资料中给出的不同,所以该方法得到的值非常大,其使用的函数族为: h ( x ) = ( x ⋅ a + b ) / w h(x)=(x\cdot a+b)/w h(x)=(xa+b)/w

4 自然处理相关

  ml.feature包中与自然语言处理相关的类主要包括以下几种,如下表:

名称作用
Tokenizer分词器,将文档全都转换为小写字母并按照空格分割文本。
RegexTokenizer使用正则表达式定义分隔符来切割文本。
StopWordsRemover移除停用词。
HashingTF统计文档中各个单词的词频TF。具体用法参考另一篇博客:HashingTF用法
IDF、IDFModel将各个文档中单词的TF向量转化为TF-IDF向量。
CountVectorizer、CountVectorizerModel统计文档中单词出现的次数。
NGram文本里面的内容按照字节进行大小为N的滑动窗口操作,形成了长度是N的字节片段序列。
Word2Vec、Word2VecModelWord2Vec模型。
4.1 分词/移除停用词

  ml.feature包中与分词等相关的类包括:Tokenizer、RegexTokenizer和StopWordsRemover。其具体用法如下:

df=spark.createDataFrame([['a b c d e'],
                          ['abc 123 edv'],
                          ['你好 世界']],['document'])

token=Tokenizer(inputCol='document',outputCol='words')
df_token=token.transform(df)
df_token.show()

regex=RegexTokenizer(pattern=r'\s+',inputCol='document',
                     outputCol='word_regx',minTokenLength=2)
#minTokenLength指定合法单词的最小长度
df_regex=regex.transform(df)
df_regex.show()

stop=StopWordsRemover(inputCol='words',outputCol='new_words',
                      stopWords=['a','abc'])
df_token_stop=stop.transform(df_token)
df_token_stop.show()

其结果如下:
在这里插入图片描述

4.2 TF/IDF计算

  ml.feature包中计算TF和IDF指标相关的包有:HashingTF、CountVectorizer和IDF等。关于这三个类需要说明一下几点:

  • HashingTF和CountVectorizer都可以计算单词的词频,这两者的区别在于单词和TF向量的列索引的对应关系。HashingTF类中单词对应的列索引是通过哈希函数和模函数产生的,且其TF向量的维度是由numFeatures决定的。而CountVectorizer中单词与列索引直接一一对应,且文档中所有不同单词的总数即为TF向量的维度。
  • IDF类中IDF的计算公式为: i d f = l n m + 1 d ( t ) + 1 idf=ln\frac{m+1}{d(t)+1} idf=lnd(t)+1m+1, m m m为文档总数, d ( t ) d(t) d(t)为单词 t t t出现的文档总数。若单词 t t t在文档中没有出现,在其 i d f = 0 , t f − i d f = 0 idf=0,tf-idf=0 idf=0,tfidf=0

其用法举例如下:

df=spark.createDataFrame([(['a','d','d','b'],),
                          (['a','b','b','c','c'],),
                          (['d','d','d'],)],['words'])

cv=CountVectorizer(inputCol='words',outputCol='tf')
tf_model=cv.fit(df)
#TF向量的对应的单词
tf_index=tf_model.vocabulary
print("TF向量索引对应的单词:",tf_index)
data_tf=tf_model.transform(df)
data_tf.show(truncate=False)
idf=IDF(inputCol='tf',outputCol='tf-idf')
data_tfidf=idf.fit(data_tf).transform(data_tf)
data_tfidf.select('words',vector_format('tf-idf').alias('tf-idf')).show(truncate=False)

其结果如下:
在这里插入图片描述

4.3 词向量

  ml.featuer包中与词向量相关的的包有:NGram和Word2Vec等。NGram的基本思想是将文本里面的内容按照字节进行大小为N的滑动窗口操作,形成了长度是N的字节片段序列。

df=spark.createDataFrame([(['a','d','d','b'],),
                          (['a','b','b','c','c'],),
                          (['d','d','d'],)],['words'])
#产生n-gram
ng=NGram(n=3,inputCol='words',outputCol='w_ng')
df_ng=ng.transform(df)
df_ng.show(truncate=False)
w2c=Word2Vec(vectorSize=5,inputCol='words',outputCol='w2c')
w2c_model=w2c.fit(df)
df_w2c=w2c_model.transform(df)
df_w2c.select('words',vector_format('w2c').alias('w2c')).show(truncate=False)

其结果如下:
在这里插入图片描述

5 向量操作

  ml.feature包中与向量操作相关类主要包括以下几种,具体如下表:

名称作用
VectorAssembler将多个特征组合成向量。
VectorSizeHint将大小信息添加到列的元数据中。
VectorSlicer获取原始向量的子向量。

其具体用法如下:

df=spark.createDataFrame([(Vectors.dense([1,2,3]),Vectors.dense([1.2,3.2])),
                          (Vectors.dense([2,3,4]),Vectors.dense([1.3,2.1])),
                          (Vectors.dense([3,2,3]),Vectors.dense([2.2,3.4]))],
                         ['A','B'])

vectsize=VectorSizeHint(inputCol='A',size=3)
df_vect=vectsize.transform(df) #还没有找到方法查看效果,后续补充

vectAssemble=VectorAssembler(inputCols=['A','B'],outputCol='A_B')
df_vectAssemble=vectAssemble.transform(df)
df_vectAssemble.show(truncate=False)

vectSlice=VectorSlicer(inputCol='A',indices=[0,2],outputCol='A_slice')
df_vectSlice=vectSlice.transform(df)
df_vectSlice.show()

其结果如下:
在这里插入图片描述

参考资料

  1. https://spark.apache.org/docs/latest/api/python/reference/pyspark.ml.html
  • 1
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值