个性化广告推荐系统实战系列(三):CTR预估的数据准备(这篇走起来步履维艰)

本文详细记录了在大数据环境下,使用Spark进行广告推荐系统的数据预处理过程,包括原始数据清洗、特征工程、缺失值处理等。作者通过实例演示了如何使用Spark SQL、OneHot编码、随机森林模型填充缺失值,以及在处理过程中遇到的numpy版本问题及其解决方案。此外,还探讨了特征选择的原则和方法,如针对高缺失率的分类特征,采用随机森林预测填充缺失值。文章最后总结了处理缺失值的经验和注意事项。
摘要由CSDN通过智能技术生成

1. 写在前面

这几天打算整理一个模拟真实情景进行广告推荐的一个小Demon, 这个项目使用的阿里巴巴提供的一个淘宝广告点击率预估的数据集, 采用lambda架构,实现一个离线和在线相结合的实时推荐系统,对非搜索类型的广告进行点击率预测和推荐(没有搜索词,没有广告的内容特征信息)。这个感觉挺接近于工业上的那种推荐系统了,通过这个推荐系统,希望能从工程的角度了解推荐系统的流程,也顺便学习一下大数据的相关技术,这次会涉及到大数据平台上的数据处理, 离线处理业务和在线处理业务, 涉及到的技术包括大数据的各种技术,包括Hadoop,Spark(Spark SQL, Spqrk ML, Spark-Streaming), Redis,Hive,HBase,Kafka和Flume等, 机器学习的相关技术(数据预处理,模型的离线训练和在线更新等。所以这几天的时间借机会走一遍这个流程,这里也详细记录一下,方便以后回看和回练, 这次的课程是跟着B站上的一个课程走的, 讲的挺详细的,就是没有课件和资料,得需要自己搞,并且在实战这次的推荐系统之前,最好是有一整套的大数据环境(我已经搭建好了), 然后就可以来玩这个系统了哈哈, 现在开始 😉

上一篇文章已经召回了候选的商品类别和品牌,并保存到了Redis中,把数据规模一下子缩减了下来, 接下来我们就是要增加更多的信息, 训练一个排序模型, 对于候选广告进行预测的任务。而这篇文章是为排序模型做数据的准备工作,这次的数据集有4个, 在召回部分中已经用了用户行为数据,召回了候选商品类别数据, 排序部分我们会使用另外的三个数据集,这次会学习一波基于spark的数据预处理操作,比如数据的独热编码, 缺失值填充等。主要包括以下内容:

  • raw_sample.csv数据的分析和预处理
  • ad_feature.csv数据的分析和预处理
  • user_profile.csv数据的分析和预处理

Ok, let’s go!

开始之前, 开启三台虚拟机, Hadoop, spark集群和jupyter notebook,并完成pyspark的配置工作,然后开干。

import os
os.environ['JAVA_HOME'] = '/opt/bigdata/java/jdk1.8'
os.environ['SPARK_HOME'] = '/opt/bigdata/spark/spark2.2'
os.environ['PYSPARK_PYTHON'] = '/opt/bigdata/anaconda3/envs/bigdata_env/bin/python3.7'
os.environ['PYSPARK_DRIVER_PYTHON'] = '/opt/bigdata/anaconda3/envs/bigdata_env/bin/python3.7'

# 配置spark driver和pyspark运行时,所使用的python解释器路径
import sys   # sys.path是python的搜索模块的路径集,是一个list
sys.path.append('/opt/bigdata/spark/spark2.2/python')
sys.path.append('/opt/bigdata/spark/spark2.2/python/lib/py4j-0.10.4-src.zip')
sys.path.append('/opt/bigdata/anaconda3/envs/bigdata_env/bin/python3.7')

# spark 配置信息
from pyspark import SparkConf
from pyspark.sql import SparkSession

SPARK_APP_NAME = "ALSRecommend"
SPARK_URL = "spark://192.168.56.101:7077"   

conf = SparkConf()    # 创建spark config对象
config = (
    ("spark.app.name", SPARK_APP_NAME),    # 设置启动的spark的app名称,没有提供,将随机产生一个名称
    ("spark.executor.memory", "2g"),    # 设置该app启动时占用的内存用量,默认1g
    ("spark.master", SPARK_URL),    # spark master的地址
    ("spark.executor.cores", "2"),    # 设置spark executor使用的CPU核心数
    # 以下三项配置,可以控制执行器数量
#     ("spark.dynamicAllocation.enabled", True),
#     ("spark.dynamicAllocation.initialExecutors", 1),    # 1个执行器
#     ("spark.shuffle.service.enabled", True)
#     ('spark.sql.pivotMaxValues', '99999'),  # 当需要pivot DF,且值很多时,需要修改,默认是10000
)

# 查看更详细配置及说明:https://spark.apache.org/docs/latest/configuration.html
conf.setAll(config)

# 利用config对象,创建spark session
spark = SparkSession.builder.config(conf=conf).getOrCreate()

导入numpy包

import numpy as np

其他包后面具体用到的时候再导入。

2. raw_sample的数据分析和处理

2.1 数据分析

首先, 从hdfs上加载数据集:

# 从hdfs加载csv文件为DataFrame
df = spark.read.csv("hdfs://master:9000/user/icss/RecommendSystem/dataset/raw_sample.csv", header=True)

看了一下情况:
在这里插入图片描述
有两个点我们要知道,第一个就是show()函数类似于pandas的head()函数, 只不过这里默认显示前20, 第二个点是默认情况下加载的数据字段格式是string。简单查看一下数据的情况:

print("样本数据集总条目数:", df.count())   # 223017
print("用户user总数:", df.groupBy("user").count().count())    # 10000
print("广告id adgroup_id总数:", df.groupBy("adgroup_id").count().count())    # 96469
print("广告展示位pid情况:", df.groupBy("pid").count().collect())    # [Row(pid='430548_1007', count=140110), Row(pid='430539_1007', count=82907)]
print("广告点击数据情况clk:", df.groupBy("clk").count().collect())    # [Row(clk='0', count=211396), Row(clk='1', count=11621)]

这里重点看到广告位有两种取值情况, 广告点击情况的个数极度不平衡, 由于之前做了采样,这里只有10000个用户的点击记录情况。

下面先用两个函数把数据字段的格式改一下,然后讨论一下特征的选择与处理问题

# 下面更改表的结构, 字段类型和字段名称
# 这里用到了dataframe.withColumn和withColumnRenamed
from pyspark.sql.types import StructType, StructField, StringType, IntegerType, LongType

raw_sample_df = df.\
    withColumn("user", df.user.cast(IntegerType())).withColumnRenamed("user", "userId").\
    withColumn("time_stamp", df.time_stamp.cast(LongType())).withColumnRenamed("time_stamp", "timestamp").\
    withColumn("adgroup_id", df.adgroup_id.cast(IntegerType())).withColumnRenamed("adgroup_id", "adgroupId").\
    withColumn("pid", df.pid.cast(StringType())).\
    withColumn("nonclk", df.nonclk.cast(IntegerType())).\
    withColumn("clk", df.clk.cast(IntegerType()))

上面的这个操作要会, 可以对列重命名和修改字段的数据类型, 类似于pandas里面的astype函数和rename函数。改完之后我们看一下数据的格式:

# 名字和数据类型都改了
raw_sample_df.printSchema()

root
 |-- userId: integer (nullable = true)
 |-- timestamp: long (nullable = true)
 |-- adgroupId: integer (nullable = true)
 |-- pid: string (nullable = true)
 |-- nonclk: integer (nullable = true)
 |-- clk: integer (nullable = true)

2.2 特征的选择和处理

特征选择就是选择那些靠谱的Feature,去掉冗余的Feature。对于搜索广告,Query关键词和广告的匹配程度很重要;但对于展示广告,广告本身的历史表现,往往是最重要的Feature。据经验,该数据集中,只有广告展示位pid对比较重要,且数据不同数据之间的占比约为6:4,因此pid可以作为一个关键特征. nonclk和clk在这里是作为目标值,不做为特征。

对于pid的处理,我们需要进行OneHot编码处理,热独编码是一种经典编码,是使用N位状态寄存器(如0和1)来对N个状态进行编码,每个状态都由他独立的寄存器位,并且在任意时候,其中只有一位有效。

假设有三组特征,分别表示年龄,城市,设备;

  • [“男”, “女”] -> [0,1]
  • [“北京”, “上海”, “广州”] -> [0,1,2]
  • [“苹果”, “小米”, “华为”, “微软”] -> [0,1,2,3]


传统变化: 对每一组特征,使用枚举类型,从0开始;

  • ["男“,”上海“,”小米“]=[ 0,1,1]
  • ["女“,”北京“,”苹果“] =[1,0,0]


传统变化后的数据不是连续的,而是随机分配的,不容易应用在分类器中,而经过热独编码,数据会变成稀疏的,方便分类器处理:

  • ["男“,”上海“,”小米“]=[ 1,0,0,1,0,0,1,0,0]
  • ["女“,”北京“,”苹果“] =[0,1,1,0,0,1,0,0,0] 。


这样做保留了特征的多样性,但是也要注意如果数据过于稀疏(样本较少、维度过高),其效果反而会变差。

下面的重点是spark下如何使用独热编码,首先,注意独热编码只能对字符串类型的列数据进行处理,需要用到下面的三个函数:

  • StringIndexer:对指定字符串列数据进行特征处理,如将性别数据“男”、“女”转化为0和1
  • OneHotEncoder:对特征列数据,进行热编码,通常需结合StringIndexer一起使用
  • Pipeline:让数据按顺序依次被处理,将前一次的处理结果作为下一次的输入

先导入包,然后特征处理:

from pyspark.ml.feature import OneHotEncoder
from pyspark.ml.feature import StringIndexer
from pyspark.ml import Pipeline

"""特征处理"""
# pid 资源位。该特征属于分类特征,只有两类取值,因此考虑进行热编码处理即可,分为是否在资源位1、是否在资源位2 两个特征


# StringIndexer对指定字符串列进行特征处理
stringindexer = StringIndexer(inputCol='pid', outputCol='pid_feature')

# 对处理出来的特征处理列进行,独热编码
encoder = OneHotEncoder(dropLast=False, inputCol='pid_feature', outputCol='pid_value')

# 利用一个管道对每个数据进行独热编码处理
pipeline = Pipeline(stages=[stringindexer, encoder])
pipeline_model = pipeline.fit(raw_sample_df)
new_df = pipeline_model.transform(raw_sample_df)

这里又学习到了一波新的操作,就是Pipeline进行数据的预处理操作, 得到的结果如下:
在这里插入图片描述
返回字段pid_value是一个稀疏向量类型数据 pyspark.ml.linalg.SparseVector,这个的效果是这样:

在这里插入图片描述
第1个元素表示取值个个数,第二个元素表示取值位置,第三个数表示取的值是多少。

2.3 划分数据集

第一篇文章中已经分析过, 这个raw_sample是骨架数据, 我们最后的排序模型是用这个数据,所以我们在这里可以划分一波数据集,由于我们这里并不对数据归一化标准化那样的处理, 且只需要后面拼接广告和用户的特征,所以这里先划分开也没有问题。

这里采用的是时间划分的方式

# 查看最大时间
new_df.sort('timestamp', ascending=False).show()   # 最大我这边是1494691172

# 本样本数据集共计8天数据
# 前七天为训练数据、最后一天为测试数据

from datetime import datetime
datetime.fromtimestamp(1494691172)
print("该时间之前的数据为训练样本,该时间以后的数据为测试样本:", datetime.fromtimestamp(1494691172-24*60*60))

该时间之前的数据为训练样本,该时间以后的数据为测试样本: 2017-05-12 23:59:32

所以下面这样划分, filter操作要学习一下, 类似于pandas的选择操作:

"""划分数据集"""
train_sample = raw_sample_df.filter(raw_sample_df.timestamp <= (1494691172-24*60*60))
print("训练样本个数: ")
print(train_sample.count())    # 195283

# 测试样本
test_sample = raw_sample_df.filter(raw_sample_df.timestamp>(1494691172-24*60*60))
print("测试样本个数:")
print(test_sample.count())  # 27734
 
# 注意:还需要加入广告基本特征和用户基本特征才能做程一份完整的样本数据集

这样, raw_sample数据处理完毕。

3. ad_feature数据的分析和预处理

这个数据是广告的特征数据, 首先依然是先导入:

# 从HDFS中加载广告基本信息数据,返回spark dafaframe对象
df = spark.read.csv("hdfs://master:9000/user/icss/RecommendSystem/dataset/ad_feature.csv", header=True)

# 注意:由于本数据集中存在NULL字样的数据,无法直接设置schema,只能先将NULL类型的数据处理掉,然后进行类型转换
from pyspark.sql.types import StructType, StructField, IntegerType, FloatType

# 替换掉NULL字符串
df = df.replace("NULL", "-1")

# 更改df表结构:更改列类型和列名称
ad_feature_df = df.\
    withColumn("adgroup_id", df.adgroup_id.cast(IntegerType())).withColumnRenamed("adgroup_id", "adgroupId").\
    withColumn("cate_id", df.cate_id.cast(IntegerType())).withColumnRenamed("cate_id", "cateId").\
    withColumn("campaign_id", df.campaign_id.cast(IntegerType())).withColumnRenamed("campaign_id", "campaignId").\
    withColumn("customer", df.customer.cast(IntegerType())).withColumnRenamed("customer", "customerId").\
    withColumn("brand", df.brand.cast(IntegerType())).withColumnRenamed("brand", "brandId").\
    withColumn("price", df.price.cast(FloatType()))

这个需要再强调一下, 由于本数据集中存在NULL字样的数据,无法直接设置schema,只能先将NULL类型的数据处理掉,然后进行类型转换。 还需要知道NULL并不是说这个地方是空哈!

看一眼数据:

在这里插入图片描述
这里我们需要了解每个字段的名称,选择特征的时候要用到,所以把第一篇里面的字段弄过来:

  • adgroup_id:脱敏过的广告ID;
  • cate_id:脱敏过的商品类目ID;
  • campaign_id:脱敏过的广告计划ID;
  • customer_id: 脱敏过的广告主ID;
  • brand_id:脱敏过的品牌ID;
  • price: 宝贝的价格

看一下各个字段唯一值的情况:

# 查看各项数据的特征
print("总广告条数: ", df.count())  # 846811
print("cateId数值个数: ", ad_feature_df.groupBy("cateId").count().count()) #  6769
print("campaignId数值个数:", ad_feature_df.groupBy("campaignId").count().count()) # 423436
print("customerId数值个数: ", ad_feature_df.groupBy("customerId").count().count())# 255875
print("brandId数值个数: ", ad_feature_df.groupBy("brandId").count().count()) # 99815

# 价格分析
print("价格高于1w的条目个数:", ad_feature_df.select("price").filter("price>10000").count())
print("价格低于1的条目个数", ad_feature_df.select("price").filter("price<1").count())

价格高于1w的条目个数: 6527
价格低于1的条目个数 5762

这里可以发现, 商品类目,广告计划ID,广告主和品牌虽然是分类特征, 但是取值个数太大, 如果去做热独编码处理,会导致数据过于稀疏 且当前我们缺少对这些特征更加具体的信息(如商品类目具体信息、品牌具体信息等),从而无法对这些特征的数据做聚类、降维处理 因此这里不选取它们作为特征。 我们只选取price作为特征数据,因为价格本身是一个统计类型连续数值型数据,且能很好的体现广告的价值属性特征,通常也不需要做其他处理(离散化、归一化、标准化等),所以这里直接将当做特征数据来使用。

广告信息数据集分析处理完毕,下面是user_profile数据集,这个有些麻烦。

4. user_profile数据集的分析和处理

4.1 数据分析

依然是需要先导入:

# 从HDFS加载用户基本信息数据
df = spark.read.csv("hdfs://master:9000/user/icss/RecommendSystem/dataset/user_profile.csv", header=True)
df.show()
# 发现pvalue_level和new_user_class_level存在空值:(注意此处的null表示空值,而如果是NULL,则往往表示是一个字符串)
# 因此直接利用schema就可以加载进该数据,无需替换null值

看一下数据会发现一些问题:

在这里插入图片描述
会发现这里面出现了一些空值null,注意这里不是NULL了,对于这种情况,pyspark会识别为None,这个也就是我们说的缺失值,我们后面需要进行缺失值的处理。 下面用schema重新导入,修改字段类型:

# 构建表结构schema对象
schema = StructType([
    StructField("userId", IntegerType()),
    StructField("cms_segid", IntegerType()),
    StructField("cms_group_id", IntegerType()),
    StructField("final_gender_code", IntegerType()),
    StructField("age_level", IntegerType()),
    StructField("pvalue_level", IntegerType()),
    StructField("shopping_level", IntegerType()),
    StructField("occupation", IntegerType()),
    StructField("new_user_class_level", IntegerType())
])
# 利用schema从hdfs加载
user_profile_df = spark.read.csv("hdfs://master:9000/user/icss/RecommendSystem/dataset/user_profile.csv", header=True, schema=schema)
user_profile_df.printSchema()
user_profile_df.show()

看一下数据情况:

在这里插入图片描述
这里会发现有两个字段特征存在缺失的情况, 下面具体来看一下子:

"""显示每一列的特征情况"""
print("分类特征值个数情况: ")
print("cms_segid: ", user_profile_df.groupBy("cms_segid").count().count())
print("cms_group_id: ", user_profile_df.groupBy("cms_group_id").count().count())
print("final_gender_code: ", user_profile_df.groupBy("final_gender_code").count().count())
print("age_level: ", user_profile_df.groupBy("age_level").count().count())
print("shopping_level: ", user_profile_df.groupBy("shopping_level").count().count())
print("occupation: ", user_profile_df.groupBy("occupation").count().count())

## 结果
分类特征值个数情况: 
cms_segid:  97
cms_group_id:  13
final_gender_code:  2
age_level:  7
shopping_level:  3
occupation:  2

缺失特征的情况:

user_profile_df.groupBy("pvalue_level").count().show()
user_profile_df.groupBy("new_user_class_level").count().show()

上面这个功能类似于pandas里面的value_counts()函数,统计每个取值的个数。
在这里插入图片描述
看一下缺失情况严不严重:

t_count = user_profile_df.count()
pl_na_count = t_count - user_profile_df.dropna(subset=["pvalue_level"]).count()
print("pvalue_level的空值情况:", pl_na_count, "空值占比:%0.2f%%"%(pl_na_count/t_count*100))
nul_na_count = t_count - user_profile_df.dropna(subset=["new_user_class_level"]).count()
print("new_user_class_level的空值情况:", nul_na_count, "空值占比:%0.2f%%"%(nul_na_count/t_count*100))

## 结果
pvalue_level的空值情况: 575917 空值占比:54.24%
new_user_class_level的空值情况: 344920 空值占比:32.49%

看了一下这俩字段的缺失还是很严重的。 把第一篇里面各个字段的含义拿过来:

在这里插入图片描述
下面是缺失值处理。

4.2 缺失值处理

一般情况下:

  • 缺失率低于10%:可直接进行相应的填充,如默认值、均值、算法拟合等等;
  • 高于10%:往往会考虑舍弃该特征
  • 特征处理,如1维转多维

特征处理方案:

  • 填充方案:结合用户的其他特征值,利用随机森林算法进行预测;但产生了大量人为构建的数据,一定程度上增加了数据的噪音
  • 把变量映射到高维空间:如pvalue_level的1维数据,转换成是否1、是否2、是否3、是否缺失的4维数据;这样保证了所有原始数据不变,同时能提高精确度,但这样会导致数据变得比较稀疏,如果样本量很小,反而会导致样本效果较差,因此也不能滥用

根据经验,我们的广告推荐其实和用户的消费水平、用户所在城市等级都有比较大的关联,因此在这里pvalue_levelnew_user_class_level都是比较重要的特征,我们不考虑舍弃, 所以我们这里考虑用随机森林对缺失值进行填充。

这里首先是pvalue_level字段, 我们的思路是这样, 把这个字段当做label, 其他特征当做特征,把label存在缺失的部分样本当做测试集,没有缺失的数据当做训练集,训练集训练随机森林,然后用测试集预测,就可以把测试集的缺失值预测出来。

所以第一步就是准备训练集, 首先剔除掉该字段缺失的数据, 然后其他不缺失的特征当做特征, 这里要准备成随机森林模型要求的模式, 用到了LabeledPoint函数。

LabeledPoint是与标签/响应相关联的密集或稀疏的局部矢量。在MLlib中,LabeledPoint用于监督学习算法。我们使用double来存储标签,因此我们可以在回归和分类中使用LabeledPoint。对于二进制分类,标签应为0(负)或1(正)。对于多类分类,标签应该是从零开始的类索引:0, 1, 2, 标记点表示为 LabeledPoint。 有关API的更多详细信息,请参阅LabeledPointPython文档。 可以理解成一种格式的包装。

下面先看两个LabeledPoint使用例子:

from pyspark.mllib.linalg import SparseVector
from pyspark.mllib.regression import LabeledPoint
# Create a labeled point with a positive label and a dense feature vector.
pos = LabeledPoint(1.0, [1.0, 0.0, 3.0])
# Create a labeled point with a negative label and a sparse feature vector.
neg = LabeledPoint(0.0, SparseVector(3, [0, 2], [1.0, 3.0]))

第一个位置的值是label, 第二个位置的值是特征, 类似于numpy的表示。下面看具体使用了:

from pyspark.mllib.regression import LabeledPoint

# 剔除掉缺失值数据, 将余下的数据作为训练集
#  user_profile_df.dropna(subset=["pvalue_level"]): 将pvalue_level中的空值所在行数据剔除后的数据,作为训练样本
train_data = user_profile_df.dropna(subset=['pvalue_level']).rdd.map(
    lambda r: LabeledPoint(r.pvalue_level-1, [r.cms_segid, r.cms_group_id, r.final_gender_code, r.age_level, r.shopping_level, r.occupation])
)

# 注意随机森林输入数据时,由于label的分类数是从0开始的,但pvalue_level的目前只分别是1,2,3,所以需要对应分别-1来作为目标值
# 自然那么最终得出预测值后,需要对应+1才能还原回来

# 我们使用cms_segid, cms_group_id, final_gender_code, age_level, shopping_level, occupation作为特征值,pvalue_level作为目标值

下面建立随机森林模型并训练

from pyspark.mllib.tree import RandomForest
# 训练分类模型
# 参数1 训练的数据
# 参数2 目标值的分类个数 0,1,2
# 参数3 特征中是否包含分类的特征 {2:2,3:7} {2:2} 表示 在特征中 第二个特征是分类的: 有两个分类
# 参数4 随机森林中 树的棵数

model = RandomForest.trainClassifier(train_data, 3, {}, 5)

哈哈, 这里运行,竟然又出现了那个似曾相识的报错,终于出来了,上次没来得及截图, 我们看看和导入numpy时显示错误的不同之处:

在这里插入图片描述
这里报错找不到numpy,开始的时候不是导入了numpy了吗? 具体原因这里不在过多描述了,可以参考上一篇文章里面的redis报错,我说过,一样的原理。 这里直接上解决方法, 把numpy.zip放到某个目录,然后用spark.sparkContext.addPyFile添加进来即可。 上代码了直接:

# 我在当前目录下面建了一个numpy目录名,必须是这个名字
# 然后把anaconda环境中的numpy包移动到了这个目录下,然后进行了压缩操作
!mkdir numpy
!cp /opt/bigdata/anaconda3/envs/bigdata_env/lib/python3.7/site-packages/numpy numpy

# 这里是用代码进行的压缩,也可以zip命令压缩好
import shutil
dir_name = "numpy"
output_filename = "./numpy"
shutil.make_archive(output_filename, 'zip', dir_name)

# 把numpy.zip移动到spark的lib目录下  master命令行; mv操作
!mv numpy.zip /opt/bigdata/spark/spark2.2/python/lib/

上面的代码不可重复执行,弄完一次就注释掉就行了。下面就是在jupyter中加入这个代码,进行导入即可

# 这样就添加了numpy到spark中去, 后面的这个路径只要写redis.zip所在路径即可
spark.sparkContext.addPyFile("/opt/bigdata/spark/spark2.2/python/lib/numpy.zip")

结果这里报了一个No module named 'numpy.core._multiarray_umath, 本来兴高采烈,结果这个错误一报就发现有事情发生了。这个错误百度,说是numpy的版本过低的原因,升级一下numpy的版本就好。但是我这里的numpy是最高的1.19.5, 还怎么升级。 然后我想了一下,难道是过高了? 要降低一下,结果在这里换了几个numpy的版本都没有作用。 结果就卡住了。这个问题一直没有解决, 先停一波了。

过了三天,把手头的紧急事情搞定, 再来看这个错误, 发现还是没有头绪, 网上压根就没有这么玩的,或者可能有这么玩的,人家的都没报这样的奇葩错误。 所以逼得我看随机森林的源码, 以为是随机森林的锅,结果也不是, 不过看到这个源码,看到mllib随机森林的使用, 于是想先用给出的示例代码简单调试这个错误,毕竟直接拿数据集这个读数据啥的太慢,代码如下:
在这里插入图片描述
这里也就是说其实这些模型的代码我们是能看到源码的,就在spark/python/pyspark/mllib下面。人家都给出了每个模型的使用例子和函数, 所以看源码的好处是能快速的掌握基本的使用,感觉比文档还好。于是我有写了这么段代码,想用这个调上面的错误, 结果又报新的错误了。

Python3.7出现RuntimeError: generator raised StopIteration异常,这个原因是python3.7和spark之间的一个不兼容bug, 需要修改pyspark下面的rdd.py里面的源代码,然后重新打包成pyspark.zip文件。具体参考这篇博客, 这里用gedit的时候还出现了空格和tab键导致python代码对不齐出错问题, 又调了gedit的首选项, 然后进行的修改,调首选项的时候发现root用户直接调还调不了,就切换到icss用户,用su命令调过来的,真实醉了。这个搞定之后, 依然是上面那个奇葩的报错。就知道了确实是numpy存在的问题了。 但是还是没有找到错误之处。 于是想直接把bigdata_env卸掉,重新来一遍吧。

这里就conda remove -n bigdata_env --all, 把新建立的虚拟环境局移除掉, 然后新建立虚拟环境conda env -n bigdata_env python=3.8, 这里学乖了,上面虽然把python3.7的兼容搞定了,但是想看看是不是python的版本也有问题, 于是换了个高级版本, 结果建立环境有又报错

InvalidArchiveError(‘Error with archive
/opt/anaconda3/pkgs/libgcc-ng-9.1.0-hdf63c60_0l8stfchr/info-libgcc-ng-9.1.0-hdf63c60_0.tar.zst.
You probably need to delete and re-download or re-create this file.
Message from libarchive was:\n\nCould not unlink’)

这个错误也是服了, 根目找不到那个文件,也删不掉, 于是乎我直接把pkgs目录删除掉。 才得以建立成功。 结果安装numpy的时候,发现anaconda本身的源太慢了,根本走不动,这里我是用conda install安装的,没敢用pip,因为有的说上面那个奇葩报错是这里的问题。也就没用之前那个pip后面加上清华源加速。但是实在太慢,搜了搜,这里又添加了两个清华源搜索镜像:

conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/free/
conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/main/
conda config --set show_channel_urls yes

这里又出问题, 提示源的错误 CondaHTTPError: HTTP 000 CONNECTION FAILED for url <https://nanomirrors.tuna.tsinghua.edu.cn/anaconda/cloud/linux-64/rpodata.json> Elapsed,这里又搞这个, 打开镜像添加的文件, vi /root/.condarc, 然后把里面的-default去掉,结果安上了。安装完numpy之后,其实挺麻烦的,就是得需要把这个新的bigdata_env文件夹复制到slave01和slave02的相同位置下。 因为后面jupyter跑pyspark的时候,指定的python解释器就是这个环境里面的。 得保证每个woker节点都能在相同位置找到相同的python解释器。 于是乎scp命令把目录又发了过去。 这样还没完, 还得把新的numpy重新打包成zip, 加入到spark/python/lib目录, 这里知道了之前zip命令压缩不行的原因了, 忘了加-r了,于是zip -r numpy.zip numpy,进行压缩,然后mv移动到lib目录,这样完事了? 还没有,因为新建立的环境之后,jupyter notebook还要重新配置呢, 于是又重新安装jupyter notebook, 配置,然后才完事。打开,用那个简单的随机森林hh跑了一下,结果,呵呵。

报错TypeError:an integer is required(got type bytes)spark2.2不支持python3.8, 需要降级,输入命令conda install python=3.6, 这时候报了个根目录空间不足的错误no space left on device, 我天,这个我是真服了, 涉及到空间不足的问题, 这个需要扩展空间了,一开始分配的16G的硬盘空间玩完了, 所以这里装环境和包或者卸载都出现了问题, 所以这里给了个教训如果宿主主机设备条件允许的话, 给虚拟机分配硬盘空间尽量大, 否则后面一旦空间不足了,就是自找麻烦,我的完全一点空余都没有了df -h 之后,全是剩余0,安装的大数据环境占了大部分(13个G), 加上一些数据,垃圾文件等, 16个G根本不够用。 后来我重启都显示不出界面来了, 即使显示出界面安装的增强功能也没法用了。 所以我趁着偶然进去了一次,赶紧把bigdata_env环境删除掉, anaconda3的pkgs目录(安装的包)删除掉, 腾了点地方之后, 至少保证了系统正常运行着, 我好扩展空间。唉, 这一倒腾,反而是白忙活了一圈。 此时没有退路了,只能硬着头皮往前了。

4.2.1 出现意外:需要先扩展根目录空间

所以下面的首要任务是扩展根目录的大小, 还好我这是虚拟机, 如果不扩展的话,后面开机都是个问题了, 所以这里先学了一波如何扩展根目录的大小,首先找到虚拟机的硬盘, 然后再Windows中的命令行执行命令:

VBoxManage modifyhd master-disk1.vdi --resize 60000   # 硬盘名需要改成自己的

这里前提, VirtualBOX的安装路径加入到环境变量,否则找不到VBoxManage命令。 我这里又重新分配大小为60个G, 这样就不担心不够了。 所以接下来就是看看如何把这40多个G的新空间合并到根目录下面去,这个还是挺复杂的。不是说这里扩展完了就完事了,这里弄完了,实际上会发现虚拟机上的还是没变,只是多出了40多个G的不能用的空间,叫做LVM。 开启虚拟机, 然后输入命令fdisk -l 就可以查看:

在这里插入图片描述
这里会看到,虽然磁盘大小变成了60多个G, 但是我们扩充的磁盘并没有被分区,不能直接被虚拟机用。所以接下来我们应该给这部分新增加的磁盘分区,扩展挂载到根目录的逻辑卷(LV)。 这里又学了一招哈哈。

首先制作物理卷PV,并扩充卷组VG。输入命令pvdisplay, 看看逻辑卷不足的原因。然后创建新的分区, 命令: fdisk /dev/sda。这里看个图吧, 不是我的系统了,但是操作基本一样, 我的重启了发现。

在这里插入图片描述
这里和他不一样的就是最后这个数我这里也是默认的, 一开始有点懵,没算过到多少个扇区是多大。 所以我直接默认了,wq保存退出。这样发现出来的sda3是40多个G,相当于一下子就把分配的全用上了,这里其实可以先小点,等再不够了再分的。 不过我硬盘空间足,这里就没考虑这么多了。 这样, 再fdisk -l查看,就发现多出了一个新分区。

在这里插入图片描述
这里发现虽然有了这个,但是还是不能用。下面重启电脑reboot now, 然后进行sd3的格式化。命令:mkfs.ext4 /dev/sda3, 这个要注意在这时候搞才行, 我看有些博客是先上来就整这个,还没创建好分区呢那时候,我这边反正是报sd3找不到Could not stat /dev/sd3 --- No such file or directory。还让重启或者partprobe, 我这里都不好使,先分区然后格式化好使了,可能是系统啥的不一样吧。

在这里插入图片描述
接下来创建逻辑分区sd3: pvcreate /dev/sda3

在这里插入图片描述
接下来扩展VG和resize lv, 输入命令:vgextend centos /dev/sda3.

在这里插入图片描述
输入命令lvextend -l +10903 /dev/mapper/centos-root,然后把它扩展到根目录, 先查一下:cat /etc/fstab | grep centos-root, 显示xfs。所以合适的扩展命令就是:xfs_growfs /dev/mapper/centos-root 并不是 resize2fs。 这时候,我们再df -h,就发现根目录空间扩大了

在这里插入图片描述

用了一下午+一晚上的时间调numpy的bug,没想到bug没调好,倒是学习了一波扩展根目录的空间。

4.2.2 继续解决numpy的bug

这里重新安装bigdata_env和numpy, 重复上面的那个步骤, 此时python换成了3.6, numpy换成1.16.2, 看好多都是这么版本,希望幸运一些。 结果还是不行, 这里怎么尝试怎么出问题,几乎快自闭了,闭目养神了一个小时,心态真的有点崩, 这个bug从周四开始遇到, 周五搞,周六搞,停了三天,周三又开始,有这个bug在,没法静下心干别的事情,于是乎,周四又搭上一天, 把网上所有方法试了一个遍,结果还是崩, 环境也都乱了套。 一乱之下, 推倒重来。因为这个bug实在弄不清楚哪里的原因了,源码也看了,玄学了已经,心已乱,不如重新开始

于是删除掉anaconda,开始重新搞,这里又出了个插曲,就是寻思着把看的b站上那个老师的miniconda3环境拿过来直接用,因为之前一直以为Linux是没有捆绑性质的,一般把整个目录拷过来就完事了,结果想的太天真,费了半天劲(重下VMworksataion pro, 下载人家的虚拟机20个G,改ip,主机访问虚拟机(桥接模式最快),打包下载上传等), 结果放到我的环境里面不能用,有些配置报错找不到,又花了点时间解决这个问题,最后也无果。 所以这里得到的教训,anaconda这个东西不能盲目的只拷贝,最好还是拿安装包自己安装,所以把这些杂乱的东西统统删除,重新安装anaconda3, 配置环境变量。

好了,开始说不一样的地方了,毕竟花了这么多时间踩雷还是采出一些感觉的,虽然上面按个顽固的bug我不知道是哪里回事, 但是我的感觉是我的环境太乱,都调乱套了。所以这次我小心翼翼,步步为营。重新装完anaconda3, 我配置环境变量,把这里注释掉:
在这里插入图片描述
这里的注释这个很关键, 如果不注释, 程序里面用的python或者numpy都往这里来找,这里面的python,numpy有点多,多则乱,想到了奥姆剃须刀原则。然后重新创建bigdata_env环境,这个名称,python版本没变,还是用的python3.7,虽然看那个视频里面老师用的3.6,但是我之前换过3.6,给我的感觉心理有些慌,于是还是用的3.7, 不要用3.8,上面说过,和spark2.2不兼容

建立完环境之后,激活,然后安装numpy,注意此时用的

conda install numpy

这里没加版本号,没用pip,因为网上有的说那个奇葩报错出自pip,我也感觉出自这里,虽然后面也尝试过这个思路,但是由于源各种原因总是间断,没有一次性安装成功过,且版本查的都乱套。 所以这里没加清华源,没加版本号, 采用的anaconda默认安装,版本是1.19.2,这次运气稍差,mkl第一次没装上报的condaHTTPERROR, 于是执行相同的命令, 只安装这个包第二次,成功。

接下来, 把bigdata_env这个大环境, 放到slave01和slave02相同的目录下面/opt/bigdata/anaconda3/envs/,用的scp命令。之所以这么做,是因为后面跑pyspark程序的时候,我用的python解释器是这里面的python3.7,各个节点要保证都能有这个python解释器才可以。

然后安装jupyter notebook,这里也是用的conda安装的,变小心了,慎用pip了。 以后安装包先用conda,当conda找不到的时候再用pip

全部搞定之后,重新开启Hadoop集群,spark集群。然后打开DataPrepare.ipynb,从头小心翼翼。导入numpy,配置spark环境, 注意这里注释掉的这两行,也非常关键,和之前导入不一样的地方:

import os
os.environ['JAVA_HOME'] = '/opt/bigdata/java/jdk1.8'
os.environ['SPARK_HOME'] = '/opt/bigdata/spark/spark2.2'
os.environ['PYSPARK_PYTHON'] = '/opt/bigdata/anaconda3/envs/bigdata_env/bin/python3.7'
os.environ['PYSPARK_DRIVER_PYTHON'] = '/opt/bigdata/anaconda3/envs/bigdata_env/bin/python3.7'

# 配置spark driver和pyspark运行时,所使用的python解释器路径
import sys   # sys.path是python的搜索模块的路径集,是一个list
#sys.path.append('/opt/bigdata/spark/spark2.2/python')
sys.path.append('/opt/bigdata/spark/spark2.2/python/lib/py4j-0.10.4-src.zip')
#sys.path.append('/opt/bigdata/spark/spark2.2/python/lib/numpy.zip')
sys.path.append('/opt/bigdata/anaconda3/envs/bigdata_env/bin/python3.7')

# spark 配置信息
from pyspark import SparkConf
from pyspark.sql import SparkSession

SPARK_APP_NAME = "DataPrepare"
SPARK_URL = "spark://192.168.56.101:7077"   

conf = SparkConf()    # 创建spark config对象
config = (
    ("spark.app.name", SPARK_APP_NAME),    # 设置启动的spark的app名称,没有提供,将随机产生一个名称
    ("spark.executor.memory", "2g"),    # 设置该app启动时占用的内存用量,默认1g
    ("spark.master", SPARK_URL),    # spark master的地址
    ("spark.executor.cores", "2"),    # 设置spark executor使用的CPU核心数
    # 以下三项配置,可以控制执行器数量
#     ("spark.dynamicAllocation.enabled", True),
#     ("spark.dynamicAllocation.initialExecutors", 1),    # 1个执行器
#     ("spark.shuffle.service.enabled", True)
#     ('spark.sql.pivotMaxValues', '99999'),  # 当需要pivot DF,且值很多时,需要修改,默认是10000
)

# 查看更详细配置及说明:https://spark.apache.org/docs/latest/configuration.html
conf.setAll(config)

# 利用config对象,创建spark session
spark = SparkSession.builder.config(conf=conf).getOrCreate()

这里不用spark的python路径,不用numpy.zip包了,这里从根上发现,就不是找不到numpy的问题,和上一篇redis上不一样。上面这个就是配置pyspark的标配了, 上一篇的代码也按照这个走了一下,没有任何问题。 然后再去执行随机森林的代码,奇迹出现了,成功了。
在这里插入图片描述
这里必须得激动一下子,哈哈。这个numpy的错误折磨了我将近一周,差点怀疑人生。到这里终于解决了这个错误。下面总结一下所悟:

  1. 这个问题一开始进坑了,报numpy的包找不到,于是立即联想前面的redis,直接压缩,添加路径,思路没错,但不知不觉入坑, 一入坑门深似海,最后还是重新推倒,然后重来, 以后遇到问题的时候,如果感觉越到后面方向把握不住的时候,需要先停,然后静下心去想想,是不是哪里出现了问题,当情景太乱,没法静心的时候, 重新开始反而是不错的选择。
  2. 这个问题的原因,虽然网上没有答案,但是我猜是环境太乱造成的,比如python环境系统本身有,anaconda的base有,新建的虚拟环境有,spark里面的python等, 而numpy, 这里不该指定numpy.zip的位置,这个我看过源码,确实从numpy.zip里面看,是没有那个numpy.core._multiarray_umath函数的。但是这个从anaconda环境里面导入就能导入进去。所以我猜应该还得结合着大环境,不能单独的压缩出numpy来用,这个和redis可能不一样,numpy是需要很多c++依赖包的。所以numpy这里有问题,然后就是python各个版本也太乱了。 后面我重新搞的时候,只用了bigdata_env的解释器和numpy,没有刻意做别的东西,反而没有遇到找不到numpy的错误,当然,感觉这个和注释掉总的环境变量里面的pythonpath也有关系 ,能局部整,就别全局定义这种东西。
  3. 这种技术上的问题能谷歌不要去百度,这次是体会到了, 果真这种涉及技术方面的还是老外比较专业,且谷歌搜索要强很多。百度上小问题还行,遇到这种问题乱七八糟,且遇到问题的深度不够或者不喜欢整理和分享。
  4. anaconda环境安装最好是用安装包走,不要试图迁移或者直接复制人家配置好的,这样反而是浪费时间。
  5. 从anaconda安装包的时候,能用conda install就不用pip,感觉上面那个也和pip安装numpy可能出现anaconda不和谐有关,毕竟再涉及到spark的时候,真有可能出问题。
  6. conda源能用,就别加清华源,这个是我的感觉,总感觉有些东西会不太一样。
  7. 关于虚拟机的配置,外存分配上不要吝啬, 尤其是真正拿虚拟机顶半边天的时候,否则后面真的报存储空间不足会非常难受且麻烦。对了,这里还要记录一个du -sh *命令,用来查看当前目录下所有目录所占的空间大小,后面的*也可以替换成具体某个目录, 实在是让空间不足的问题整怕了, 还是经常查着点好。大文件可能非常重要也可能都是垃圾,走两个极端。
  8. 不要害怕出错, 这时候出错是个好事情,能学到很多经验和知识,虽然有些不一定有用,但是心理承受能力确实在变强。现在感觉Linux也非常有感觉了,其次毕竟是虚拟机, 出错最差的结果无非就是全部删除掉重新开始。
  9. 一定要勤于整理这些过程, 让这些问题得以沉淀,不要过于相信记忆,这些东西小细节太多,尤其是大数据这块,再出现同样的问题,根本不能指望之前的记忆搞定,如果再各种百度,反而浪费时间,走过的坑就让他有价值些。 整理成博客是个不错的选择,既能让自己随时可查,也能帮助到别人避免到一些坑,帮别人节省些时间,这非常有价值。
  10. 版本不兼容的问题要记好, python3.8与spark2.x, 还有就是redis的3.x和spark2.x, 还有python3.7与spark之间也有一个bug,但是可通过改rdd的源码搞定。

好了, 花费了四五天搞定这个错误, 也把这些天的坑的感想总结完毕, 下面回归正题,继续走项目了。

4.2.3 回归正题:随机森林法填充缺失

上面我们已经可以训练出随机森林模型来了, 下面就筛选出pvalue_level里面的缺失值条目。

# 筛选出缺失值条目
pl_na_df = user_profile_df.na.fill(-1).where("pvalue_level=-1")

下面看眼情况,顺便回忆下情景:

在这里插入图片描述
这里我们用训练好的随机森林进行预测,逻辑是先转成rdd, 然后取出其他列作为特征,预测pvalue_level,

def row(r):
    return r.cms_segid, r.cms_group_id, r.final_gender_code, r.age_level, r.shopping_level, r.occupation

# 转换为普通的rdd类型
rdd = pl_na_df.rdd.map(row)
# 预测全部的p_value_level值
predicts = model.predict(rdd)

# 查看前20条
print(predicts.take(20))
print("预测值总数", predicts.count())

# 这里注意predict参数,如果是预测多个,那么参数必须是直接有列表构成的rdd参数,而不能是dataframe.rdd类型
# 因此这里经过map函数处理,将每一行数据转换为普通的列表数据

## 结果:
[1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0]
预测值总数 575917

这里由于数据量比较小,可以直接转成pandas的DataFrame处理。把预测值还原,别忘了+1操作。

# 这里数据量比较小,直接转换为pandas dataframe来处理,因为方便,但注意如果数据量较大不推荐,因为这样会把全部数据加载到内存中
temp = predicts.map(lambda x: int(x)).collect()
pdf = pl_na_df.toPandas()

# 在pandas df的基础上直接替换掉列数据
pdf["pvalue_level"] = np.array(temp) + 1  # 注意+1 还原预测值

这里的toPandas()函数,直接转成了pandas的DataFrame了,这样就能用我们熟悉的pandas的操作了

在这里插入图片描述
对了,上面那个代码需要安装pandas包, 这里学乖了,直接conda install pandas了,版本1.2.0。下面是与非缺失数据拼接, 完成pvalue_level缺失值预测,也就是把上面这个合并到原来的数据集中。

new_user_profile_df = user_profile_df.dropna(subset=["pvalue_level"]).unionAll(spark.createDataFrame(pdf, schema=schema))

new_user_profile_df.show(5)
# 注意:unionAll的使用,两个df的表结构必须完全一样

这个结果看一下, 此时是spark的DataFrame,和pandas的还是有些区别的。
在这里插入图片描述
同样的方式,这里我们对new_user_class_level的缺失进行预测, 这里把上面所有代码放到一块了。

# 选出new_user_class_level全部的
train_data2 = user_profile_df.dropna(subset=["new_user_class_level"]).rdd.map(
    lambda r:LabeledPoint(r.new_user_class_level - 1, [r.cms_segid, r.cms_group_id, r.final_gender_code, r.age_level, r.shopping_level, r.occupation])
)

# 重新建立随机森林模型,并完成训练
model2 = RandomForest.trainClassifier(train_data2, 4, {}, 5)

# 预测缺失
nul_na_df = user_profile_df.na.fill(-1).where("new_user_class_level=-1")
def row(r):
    return r.cms_segid, r.cms_group_id, r.final_gender_code, r.age_level, r.shopping_level, r.occupation

rdd2 = nul_na_df.rdd.map(row)
predicts2 = model2.predict(rdd2)

temp1 = predicts.map(lambda x: int(x)).collect()
pdf1 = pl_na_df.toPandas()

# 在pandas df的基础上直接替换掉列数据
pdf1["new_user_class_level"] = np.array(temp1) + 1  # 注意+1 还原预测值

new_user_profile_df1 = user_profile_df.dropna(subset=["new_user_class_level"]).unionAll(spark.createDataFrame(pdf1, schema=schema))

填充完了,想办法把这两个spark的DataFrame合并成一个最终的DataFrame。视频里面这块并没有写,因为发现由于这两个字段的缺失过多,所以预测出来的值已经大大失真,但如果缺失率在10%以下,这种方法是比较有效的一种。 这里没有提供合并的方式, 这里找到了一篇关于pyspark DataFrame操作总结的博客,查了一下,把两个进行了合并join函数。

new_user_profile_df = new_user_profile_df.drop('new_user_class_level')
new_user_profile = new_user_profile_df.join(new_user_profile_df1[['userId', 'new_user_class_level']], 'userId', 'left_outer')

于是就得到了填充完缺失的数据集了:
在这里插入图片描述
缺失值处理结束,下面是进行OneHot数据处理,低伪维转高维。

4.3 低维转高维:One-Hot编码,把缺失当做一种特征单独对待

上面总结里面发现了带有缺失的两个字段缺失数据太多了, 50%以上的缺失,这种情况下用随机森林会使得预测出来的值非常的不准, 在10%的缺失下可以用, 但是缺失太大,这里不妨换一种思路处理这些缺失。

我们接下来采用将变量映射到高维空间的方法来处理数据,即将缺失项也当做一个单独的特征来对待,保证数据的原始性。由于该思想正好和热独编码实现方法一样,因此这里直接使用热独编码方式处理数据。

还原原来的数据,即我们用user_profile_df, 然后直接把缺失值填充成-1.然后对这两个缺失的特征直接one-hot编码。

# 使用热独编码转换pvalue_level的一维数据为多维,其中缺失值单独作为一个特征值
# 需要先将缺失值全部替换为数值,与原有特征一起处理
user_profile_df = user_profile_df.na.fill(-1)

one-hot时候,需要将特征处理成字符串类型才行。

from pyspark.sql.types import StringType
# 热独编码时,必须先将待处理字段转为字符串类型才可处理
user_profile_df = user_profile_df.withColumn("pvalue_level", user_profile_df.pvalue_level.cast(StringType()))\
    .withColumn("new_user_class_level", user_profile_df.new_user_class_level.cast(StringType()))
user_profile_df.printSchema()

看下结果:
在这里插入图片描述
下面进行独热编码, 这个过程上面已经用过了:

stringindexer = StringIndexer(inputCol='pvalue_level', outputCol='pl_onehot_feature')
encoder = OneHotEncoder(dropLast=False, inputCol='pl_onehot_feature', outputCol='pl_onehot_value')
pipeline = Pipeline(stages=[stringindexer, encoder])
pipeline_fit = pipeline.fit(user_profile_df)
user_profile_df2 = pipeline_fit.transform(user_profile_df)
# pl_onehot_value列的值为稀疏向量,存储热独编码的结果

看下结果:

在这里插入图片描述
这里面的StringIndexer有种labelEncoder的感觉,方便后面的onehot编码, 这里的对应关系注意一下, 存储在redis的时候,为了节约存储空间,一般是直接存储单列特征, 但是得通过这个对应关系,知道onehot后的情况。

对于new_user_class_level,也是同样的操作过程,直接上代码:

stringindexer = StringIndexer(inputCol='new_user_class_level', outputCol='nucl_onehot_feature')
encoder = OneHotEncoder(dropLast=False, inputCol='nucl_onehot_feature', outputCol='nucl_onehot_value')
pipeline = Pipeline(stages=[stringindexer, encoder])
pipeline_fit = pipeline.fit(user_profile_df2)
user_profile_df3 = pipeline_fit.transform(user_profile_df2)
user_profile_df3.show()

这里看下这个新的df:
在这里插入图片描述

上面把处理过的数据进行保存,排序的时候需要用到。

# 下面把上面处理的这几个文件保存到本地, 我们进行下一步
new_df.toPandas().to_csv("new_raw_sample.csv", header=True, index=False)
ad_feature_df.toPandas().to_csv("new_ad_feature.csv", header=True, index=False)
user_profile_df3.toPandas().to_csv("new_user_profile.csv", header=True, index=False)

本来想保存到hdfs上面, 但是出了几个问题,于是就直接保存到本地了。这里之所以转成Pandas,是因为用Spark DataFrame的write.csv会出现不支持的存储格式,就是那个one-hot的向量格式。这里不支持报错。

5. 总结

这里简单的总结一下, 这篇文章主要围绕着数据和处理来写的,学习了分布式情况下,用pyspark进行的数据预处理, spark SQL操作起来和pandas的DataFrame有些一样,主要是三个数据集的预处理工作,重点是user_profile的缺失处理, 这里总结一下经验:

  • 连续特征
    • 缺失比例比较严重,可以考虑舍弃
    • 可以考虑利用平均值,中位数,分位数填充
    • 算法预测(样本中其他特征作为特征, 有缺失的特征作为label)
  • 分类特征
    • 缺失比例验证,可以考虑舍弃
    • 把缺失作为单独的分类,如果之前的数据只有两个分类,把缺失考虑进来变成3个分类
    • 算法预测
  • 利用算法预测缺失值
    • 其它特征和要预测的特征之间是否有联系
    • 样本数据是否足够
    • 利用算法预测缺失值会引入噪声

然后就是主要利用了随机森林预测了一波缺失, 用的pyspark MLlib,步骤回忆:

  • 基于RDD的
  • 监督学习的样本数据要创建成LabeledPoint对象,MLlib通过LabeledPoint来训练模型
  • pos = LabeledPoint(目标, [特征list])
    • 目标值是分类情况 分类值从0开始连续增加
    • 特征double类型

这篇走起来相对来说比较艰难,大部分时间都在调bug, 不过最后调好了还是, 下一个jupyter, 基于这三个保存的新的处理后的数据集, 训练逻辑回归模型。这个就是先用pandas读入,然后转成spark 的DataFrame就OK了。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值