数据分析
数据集结构
其中有几点需要注意
- hack_license 是出租车执照, 可以唯一标识一辆出租车
- pickup_datetime 和 dropoff_datetime 分别是上车时间和下车时间, 通过这个时间, 可以获知行车时间
- pickup_longitude 和 dropoff_longitude 是经度, 经度所代表的是横轴, 也就是 X 轴
- pickup_latitude 和 dropoff_latitude 是纬度, 纬度所代表的是纵轴, 也就是 Y 轴
数据读取
准备工作
配置pom
文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>cn.itcast</groupId>
<artifactId>taxi</artifactId>
<version>0.0.1</version>
<properties>
<scala.version>2.11.8</scala.version>
<spark.version>2.2.0</spark.version>
<hadoop.version>2.7.5</hadoop.version>
<slf4j.version>1.7.16</slf4j.version>
<log4j.version>1.2.17</log4j.version>
<mysql.version>5.1.35</mysql.version>
<esri.version>2.2.2</esri.version>
<json4s.version>3.6.6</json4s.version>
</properties>
<dependencies>
<!-- Scala 库 -->
<dependency>
<groupId>org.scala-lang</groupId>
<artifactId>scala-library</artifactId>
<version>${scala.version}</version>
</dependency>
<dependency>
<groupId>org.scala-lang.modules</groupId>
<artifactId>scala-xml_2.11</artifactId>
<version>1.0.6</version>
</dependency>
<!-- Spark 系列包 -->
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-core_2.11</artifactId>
<version>${spark.version}</version>
</dependency>
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-sql_2.11</artifactId>
<version>${spark.version}</version>
</dependency>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-client</artifactId>
<version>${hadoop.version}</version>
</dependency>
<!-- 地理位置处理库 -->
<dependency>
<groupId>com.esri.geometry</groupId>
<artifactId>esri-geometry-api</artifactId>
<version>${esri.version}</version>
</dependency>
<!-- JSON 解析库 -->
<dependency>
<groupId>org.json4s</groupId>
<artifactId>json4s-native_2.11</artifactId>
<version>${json4s.version}</version>
</dependency>
<dependency>
<groupId>org.json4s</groupId>
<artifactId>json4s-jackson_2.11</artifactId>
<version>${json4s.version}</version>
</dependency>
<!-- 日志相关 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
<version>${slf4j.version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>${slf4j.version}</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>${log4j.version}</version>
</dependency>
</dependencies>
<build>
<sourceDirectory>src/main/scala</sourceDirectory>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.0</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<plugin>
<groupId>net.alchim31.maven</groupId>
<artifactId>scala-maven-plugin</artifactId>
<version>3.2.0</version>
<executions>
<execution>
<goals>
<goal>compile</goal>
<goal>testCompile</goal>
</goals>
<configuration>
<args>
<arg>-dependencyfile</arg>
<arg>${project.build.directory}/.scala_dependencies</arg>
</args>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
创建scala目录,并且将这个目录设置为Source Root
目录结构如下
读取文件
创建SarkSession
与读取文件
//创建SparkSession
val spark = SparkSession.builder()
.master("local[6]")
.appName("taxi")
.getOrCreate()
//导入隐式转换
import spark.implicits._
import org.apache.spark.sql.functions._
//数据读取
val taxiRow = spark.read
.option("header", true)
.csv("dataset/half_trip.csv")
.show()
结果展示
数据清洗
数据类型转换
剪去多余列
- 现在数据集中包含了一些多余的列, 在后续的计算中并不会使用到, 如果让这些列参与计算的话, 会影响整体性能, 浪费集群资源
类型转换
- 可以看到, 现在的数据集中, 所有列类型都是 String, 而在一些统计和运算中, 不能使用 String 来进行, 所以要将这些数据转为对应的类型
我们可以自定义一个样例类将Row
类型数据转换成对象类
/**
* 代表一个行程, 是集合中的一条记录
* @param license 出租车执照号
* @param pickUpTime 上车时间
* @param dropOffTime 下车时间
* @param pickUpX 上车地点的经度
* @param pickUpY 上车地点的纬度
* @param dropOffX 下车地点的经度
* @param dropOffY 下车地点的纬度
*/
case class Trip(
license: String,
pickUpTime: Long,
dropOffTime: Long,
pickUpX: Double,
pickUpY: Double,
dropOffX: Double,
dropOffY: Double
)
我们在逐行转换数据类型时不知道数据是否为空,因此我们应该新建一个类来判断数据是否为空
因为在针对 Row
类型对象进行数据转换时, 需要对一列是否为空进行判断和处理, 在 Scala
中为空的处理进行一些支持和封装, 叫做 Option
, 所以在读取 Row
类型对象的时候, 要返回 Option
对象, 通过一个包装类, 可以轻松做到这件事
class RichRow(row: Row) {
// 为了返回空值,提醒外面进行处理
def getAs[T](field: String): Option[T] = {
//判断是否为空
if (row.isNullAt(row.fieldIndex(field))) {
None
} else {
Some(row.getAs[T](field))
}
}
}
RichRow
类的解释与分析
- 该类的输入时
Row
就是行 getAs
方法输入的参数field
是列名,输出的是每列的数据- 之所将数据包装成
option
类是因为,,此类有个方法getOrElse
,使用该方法,不为空是时输出数据,为空时输出0或者指定的数据
我们需要的数据只有三种数据类型,分别是:String
、Long
、Double
。,字符类型在数据读取时,默认就是,但是后两者类型需要我们自己手动转换
将数据转换成Long
类型
def parseTime(row: RichRow, field: String): Long = {
//表示时间类型的格式
val pattern = "yyyy-MM-dd HH:mm:ss"
val formatter = new SimpleDateFormat(pattern, Locale.ENGLISH)
//执行转换,获取时间戳
val time: Option[String] = row.getAs[String](field)
/**
* 这个map时option中的map
* 他的主要作用就是,如果有值的话就转换成定义的类型
* 值为空就不返回
* 所以它是安全的
*/
val timeOption = time.map(time => formatter.parse(time).getTime)
timeOption.getOrElse(0l)
}
读取Double
类型数据
def parseLocation(row: RichRow, field: String): Double = {
//获取数据
val location: Option[String] = row.getAs[String](field)
//转换数据
val locationOption: Option[Double] = location.map(local => local.toDouble)
locationOption.getOrElse(0.0)
}
此时我们可以将数据一起转换成自定义的类型
def parse(row: Row): Trip = {
val richRow = new RichRow(row)
val license = richRow.getAs[String]("hack_license").orNull
val pickUpTime = parseTime(richRow, "pickup_datetime")
val pickOffTime = parseTime(richRow, "dropoff_datetime")
val pickUpX = parseLocation(richRow, "pickup_longitude")
val pickUpY = parseLocation(richRow, "pickup_latitude")
val pickOffX = parseLocation(richRow, "dropoff_longitude")
val pickOffY = parseLocation(richRow, "dropoff_latitude")
Trip(license, pickUpTime, pickOffTime, pickUpX, pickUpY, pickOffX, pickOffY)
}
解决报错问题
我们上面在对数据进行类型转换时,可能会因为数据的错误而报错,那我们改输入解决?
def safe[P, R](f: P => R): P => Either[R, (P, Exception)] = {
new Function[P, Either[R, (P, Exception)]] with Serializable {
override def apply(param: P): Either[R, (P, Exception)] = {
try {
Left(f(param))
} catch {
case e: Exception => Right((param, e))
}
}
}
}
解释与分析
- 我们为了保证数据处理过程中的安全性,可以在rdd.map的时候调用函数safe
- 但是map输入的是一个函数parse,所以我们应该也是输入函数p
- safe接受的参数也是函数p,但是这个函数p会返回一个函数r
- 所以safe现在是有两种输出的结果,一个是输入的函数变换之后的r
- 一个是输入函数和报错信息
针对异常值进行处理
val taxiParsed: RDD[Either[Trip, (Row, Exception)]] = taxiRow.rdd.map(safe(parse))
//现在result里面全是有问题的row
val result: RDD[Row] = taxiParsed.filter(e => e.isRight)
.map(e => e.right.get._1)
//结果
val taxiGood = taxiParsed.map(either => either.left.get).toDS()
剪除异常数据
val hours = (pickupTime: Long, dropoffTime: Long) => {
val duration = dropoffTime - pickupTime
val hour = TimeUnit.HOURS.convert(duration, TimeUnit.MICROSECONDS)
hour
}
val hoursUDF = udf(hours)
spark.udf.register("hours",hours)
val taxiClean = taxiGood.where("hours(pickUpTime,dropOffTime) between 0 and 3")
.show()
结果展示
完整代码显示
package taxi
import org.apache.spark.rdd.RDD
import org.apache.spark.sql.{Row, SparkSession}
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.concurrent.TimeUnit
object TaxiAnalysisRunner {
def main(args: Array[String]): Unit = {
//创建SparkSession
val spark = SparkSession.builder()
.master("local[6]")
.appName("taxi")
.getOrCreate()
//导入隐式转换
import spark.implicits._
import org.apache.spark.sql.functions._
//数据读取
val taxiRow = spark.read
.option("header", true)
.csv("dataset/half_trip.csv")
//转换操作
val taxiParsed: RDD[Either[Trip, (Row, Exception)]] = taxiRow.rdd.map(safe(parse))
//现在result里面全是有问题的row
// val result: RDD[Row] = taxiParsed.filter(e => e.isRight)
// .map(e => e.right.get._1)
//结果
val taxiGood = taxiParsed.map(either => either.left.get).toDS()
//绘制时长直方图
//统计时长
val hours = (pickupTime: Long, dropoffTime: Long) => {
val duration = dropoffTime - pickupTime
val hour = TimeUnit.HOURS.convert(duration, TimeUnit.MICROSECONDS)
hour
}
val hoursUDF = udf(hours)
spark.udf.register("hours",hours)
val taxiClean = taxiGood.where("hours(pickUpTime,dropOffTime) between 0 and 3")
.show()
}
/**
* 作用就是封装parse方法,捕获异常
*
*/
/**
* 包裹转换逻辑, 并返回 Either
* 我们为了保证数据处理过程中的安全性,可以在rdd.map的时候调用函数safe
* 但是map输入的是一个函数parse,所以我们应该也是输入函数p
* safe接受的参数也是函数p,但是这个函数p会返回一个函数r
* 所以safe现在是有两种输出的结果,一个是输入的函数变换之后的r
* 一个是输入函数和报错信息
*/
def safe[P, R](f: P => R): P => Either[R, (P, Exception)] = {
new Function[P, Either[R, (P, Exception)]] with Serializable {
override def apply(param: P): Either[R, (P, Exception)] = {
try {
Left(f(param))
} catch {
case e: Exception => Right((param, e))
}
}
}
}
/**
* Row -> Trip
*
* @param row
* @return
*/
def parse(row: Row): Trip = {
val richRow = new RichRow(row)
val license = richRow.getAs[String]("hack_license").orNull
val pickUpTime = parseTime(richRow, "pickup_datetime")
val pickOffTime = parseTime(richRow, "dropoff_datetime")
val pickUpX = parseLocation(richRow, "pickup_longitude")
val pickUpY = parseLocation(richRow, "pickup_latitude")
val pickOffX = parseLocation(richRow, "dropoff_longitude")
val pickOffY = parseLocation(richRow, "dropoff_latitude")
Trip(license, pickUpTime, pickOffTime, pickUpX, pickUpY, pickOffX, pickOffY)
}
def parseTime(row: RichRow, field: String): Long = {
//表示时间类型的格式
val pattern = "yyyy-MM-dd HH:mm:ss"
val formatter = new SimpleDateFormat(pattern, Locale.ENGLISH)
//执行转换,获取时间戳
val time: Option[String] = row.getAs[String](field)
/**
* 这个map时option中的map
* 他的主要作用就是,如果有值的话就转换成定义的类型
* 值为空就不返回
* 所以它是安全的
*/
val timeOption = time.map(time => formatter.parse(time).getTime)
timeOption.getOrElse(0l)
}
def parseLocation(row: RichRow, field: String): Double = {
//获取数据
val location: Option[String] = row.getAs[String](field)
//转换数据
val locationOption: Option[Double] = location.map(local => local.toDouble)
locationOption.getOrElse(0.0)
}
}
/**
*
* @param row
*/
class RichRow(row: Row) {
// 为了返回空值,提醒外面进行处理
def getAs[T](field: String): Option[T] = {
//判断是否为空
if (row.isNullAt(row.fieldIndex(field))) {
None
} else {
Some(row.getAs[T](field))
}
}
}
case class Trip(
license: String,
pickUpTime: Long,
dropOffTime: Long,
pickUpX: Double,
pickUpY: Double,
dropOffX: Double,
dropOffY: Double
)