SparkSQL 練習項目 - 出租車利用率分析

SparkSQL 練習項目 - 出租車利用率分析
導讀

本項目是 SparkSQL 階段的練習項目, 主要目的是夯實同學們對於 SparkSQL 的理解和使用

數據集

2013年紐約市出租車乘車記錄

需求

統計出租車利用率, 到某個目的地後, 出租車等待下一個客人的間隔

1. 業務

導讀
  1. 數據集介紹

  2. 業務場景介紹

  3. 和其它業務的關聯

  4. 通過項目能學到什麼

數據集結構
字段 示例 示意

hack_license

BA96DE419E711691B9445D6A6307C170

執照號, 可以唯一標識一輛出租車

pickup_datetime

2013-01-01 15:11:48

上車時間

dropoff_datetime

2013-01-01 15:18:10

下車時間

pickup_longitude

-73.978165

上車點

pickup_latitude

40.757977

上車點

dropoff_longitude

-73.989838

下車點

dropoff_latitude

40.751171

下車點

其中有三個點需要注意

  • hack_license 是出租車執照, 可以唯一標識一輛出租車

  • pickup_datetimedropoff_datetime 分別是上車時間和下車時間, 通過這個時間, 可以獲知行車時間

  • pickup_longitudedropoff_longitude 是經度, 經度所代表的是橫軸, 也就是 X 軸

  • pickup_latitudedropoff_latitude 是緯度, 緯度所代表的是縱軸, 也就是 Y 軸

業務場景

在網約車出現之前, 出行很大一部分要靠出租車和公共交通, 所以經常會見到一些情況, 比如說從東直門打車, 告訴師傅要去昌平, 師傅可能拒載. 這種情況所凸顯的是一個出租車調度的難題, 所以需要先通過數據來看到問題, 後解決問題.

所以要統計出租車利用率, 也就是有乘客乘坐的時間, 和無乘客空跑的時間比例. 這是一個理解出租車的重要指標, 影響利用率的一個因素就是目的地, 比如說, 去昌平, 可能出租車師傅不確定自己是否要空放回來, 而去國貿, 下車幾分鐘內, 一定能有新的顧客上車.

而統計利用率的時候, 需要用到時間數據和空間數據來進行計算, 對於時間計算來說, SparkSQL 提供了很多工具和函數可以使用, 而空間計算仍然是一個比較專業的場景, 需要使用到第三方庫.

我們的需求是, 在上述的數據集中, 根據時間算出等待時間, 根據地點落地到某個區, 算出某個區的平均等待時間, 也就是這個下車地點對於出租車利用率的影響.

技術點和其它技術的關係
  1. 數據清洗

    數據清洗在幾乎所有類型的項目中都會遇到, 處理數據的類型, 處理空值等問題

  2. JSON 解析

    JSON 解析在大部分業務系統的數據分析中都會用到, 如何讀取 JSON 數據, 如何把 JSON 數據變爲可以使用的對象數據

  3. 地理位置信息處理

    地理位置信息的處理是一個比較專業的場景, 在一些租車網站, 或者像滴滴, Uber 之類的出行服務上, 也經常會處理地理位置信息

  4. 探索性數據分析

    從拿到一個數據集, 明確需求以後, 如何逐步瞭解數據集, 如何從數據集中探索對應的內容等, 是一個數據工程師的基本素質

  5. 會話分析

    會話分析用於識別同一個用戶的多個操作之間的關聯, 是分析系統常見的分析模式, 在電商和搜索引擎中非常常見

在這個小節中希望大家掌握的知識
  1. SparkSQL 中對於類型的處理

  2. Scala 中常見的 JSON 解析工具

  3. GeoJson 的使用

2. 流程分析

導讀
  1. 分析的步驟和角度

  2. 流程

分析的視角
  1. 理解數據集

    首先要理解數據集, 要回答自己一些問題

    • 這個數據集是否以行作爲單位, 是否是 DataFrame 可以處理的, 大部分情況下都是

    • 這個數據集每行記錄所代表的實體對象是什麼, 例如: 出租車的載客記錄

    • 表達這個實體對象的最核心字段是什麼, 例如: 上下車地點和時間, 唯一標識一輛車的 License

  2. 理解需求和結果集

    • 小學的時候, 有一次考試考的比較差, 老師在幫我分析的時候, 告訴我, 你下次要讀懂題意, 再去大題, 這樣不會浪費時間, 於是這個信念貫穿了我這些年的工作.

    • 按照我對開發工作的理解, 在一開始的階段進行一個大概的思考和麪向對象的設計, 並不會浪費時間, 即使這些設計可能會佔用一些時間.

    • 對代碼的追求也不會浪費時間, 把代碼寫好, 會減少閱讀成本, 溝通成本.

    • 對測試的追求也不會浪費時間, 因爲在進行迴歸測試的時候, 可以儘可能的減少修改對已有代碼的衝擊.

    所以第一點, 理解需求再動手, 絕對不會浪費時間. 第二點, 在數據分析的任務中, 如何無法理解需求, 可能根本無從動手.

    • 我們的需求是: 出租車在某個地點的平均等待客人時間

    • 簡單來說, 結果集中應該有的列: 地點, 平均等待時間

  3. 反推每一個步驟

    結果集中, 應該有的字段有兩個, 一個是地點, 一個是等待時間

    地點如何獲知? 其實就是乘客的下車點, 但是是一個座標, 如何得到其在哪個區? 等待時間如何獲知? 其實就是上一個乘客下車, 到下一個乘客上車之間的時間, 通過這兩個時間的差值便可獲知

步驟分析
  1. 讀取數據集

    數據集很大, 所以我截取了一小部分, 大概百分之一左右, 如果大家感興趣的話, 可以將完整數據集放在集羣中, 使用集羣來計算 "大數據"

  2. 清洗

    數據集當中的某些列名可能使用起來不方便, 或者數據集當中某些列的值類型可能不對, 或者數據集中有可能存在缺失值, 這些都是要清洗的動機, 和理由

  3. 增加區域列

    由於最終要統計的結果是按照區域作爲單位, 而不是一個具體的目的地點, 所以要在數據集中增加列中放置區域信息

    1. 既然是放置行政區名字, 應該現有行政區以及其邊界的信息

    2. 通過上下車的座標點, 可以判斷是否存在於某個行政區中

    這些判斷座標點是否屬於某個區域, 這些信息, 就是專業的領域了

  4. 按照區域, 統計司機兩次營運記錄之間的時間差

    數據集中存在很多出租車師傅的數據, 所以如何將某個師傅的記錄發往一個分區, 在這個分區上完成會話分析呢? 這也是一個需要理解的點

3. 數據讀取

導讀
  1. 工程搭建

  2. 數據讀取

工程搭建
  1. 創建 Maven 工程

  2. 導入 Maven 配置

    <?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>
    
    <span class="hljs-tag">&lt;<span class="hljs-name">groupId</span>&gt;</span>cn.itcast<span class="hljs-tag">&lt;/<span class="hljs-name">groupId</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">artifactId</span>&gt;</span>taxi<span class="hljs-tag">&lt;/<span class="hljs-name">artifactId</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">version</span>&gt;</span>0.0.1<span class="hljs-tag">&lt;/<span class="hljs-name">version</span>&gt;</span>
    
    <span class="hljs-tag">&lt;<span class="hljs-name">properties</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">scala.version</span>&gt;</span>2.11.8<span class="hljs-tag">&lt;/<span class="hljs-name">scala.version</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">spark.version</span>&gt;</span>2.2.0<span class="hljs-tag">&lt;/<span class="hljs-name">spark.version</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">hadoop.version</span>&gt;</span>2.7.5<span class="hljs-tag">&lt;/<span class="hljs-name">hadoop.version</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">slf4j.version</span>&gt;</span>1.7.16<span class="hljs-tag">&lt;/<span class="hljs-name">slf4j.version</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">log4j.version</span>&gt;</span>1.2.17<span class="hljs-tag">&lt;/<span class="hljs-name">log4j.version</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">mysql.version</span>&gt;</span>5.1.35<span class="hljs-tag">&lt;/<span class="hljs-name">mysql.version</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">esri.version</span>&gt;</span>2.2.2<span class="hljs-tag">&lt;/<span class="hljs-name">esri.version</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">json4s.version</span>&gt;</span>3.6.6<span class="hljs-tag">&lt;/<span class="hljs-name">json4s.version</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">properties</span>&gt;</span>
    
    <span class="hljs-tag">&lt;<span class="hljs-name">dependencies</span>&gt;</span>
        <span class="hljs-comment">&lt;!-- Scala 庫 --&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">dependency</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">groupId</span>&gt;</span>org.scala-lang<span class="hljs-tag">&lt;/<span class="hljs-name">groupId</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">artifactId</span>&gt;</span>scala-library<span class="hljs-tag">&lt;/<span class="hljs-name">artifactId</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">version</span>&gt;</span>${scala.version}<span class="hljs-tag">&lt;/<span class="hljs-name">version</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">dependency</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">dependency</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">groupId</span>&gt;</span>org.scala-lang.modules<span class="hljs-tag">&lt;/<span class="hljs-name">groupId</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">artifactId</span>&gt;</span>scala-xml_2.11<span class="hljs-tag">&lt;/<span class="hljs-name">artifactId</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">version</span>&gt;</span>1.0.6<span class="hljs-tag">&lt;/<span class="hljs-name">version</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">dependency</span>&gt;</span>
    
        <span class="hljs-comment">&lt;!-- Spark 系列包 --&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">dependency</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">groupId</span>&gt;</span>org.apache.spark<span class="hljs-tag">&lt;/<span class="hljs-name">groupId</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">artifactId</span>&gt;</span>spark-core_2.11<span class="hljs-tag">&lt;/<span class="hljs-name">artifactId</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">version</span>&gt;</span>${spark.version}<span class="hljs-tag">&lt;/<span class="hljs-name">version</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">dependency</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">dependency</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">groupId</span>&gt;</span>org.apache.spark<span class="hljs-tag">&lt;/<span class="hljs-name">groupId</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">artifactId</span>&gt;</span>spark-sql_2.11<span class="hljs-tag">&lt;/<span class="hljs-name">artifactId</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">version</span>&gt;</span>${spark.version}<span class="hljs-tag">&lt;/<span class="hljs-name">version</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">dependency</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">dependency</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">groupId</span>&gt;</span>org.apache.hadoop<span class="hljs-tag">&lt;/<span class="hljs-name">groupId</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">artifactId</span>&gt;</span>hadoop-client<span class="hljs-tag">&lt;/<span class="hljs-name">artifactId</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">version</span>&gt;</span>${hadoop.version}<span class="hljs-tag">&lt;/<span class="hljs-name">version</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">dependency</span>&gt;</span>
    
        <span class="hljs-comment">&lt;!-- 地理位置處理庫 --&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">dependency</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">groupId</span>&gt;</span>com.esri.geometry<span class="hljs-tag">&lt;/<span class="hljs-name">groupId</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">artifactId</span>&gt;</span>esri-geometry-api<span class="hljs-tag">&lt;/<span class="hljs-name">artifactId</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">version</span>&gt;</span>${esri.version}<span class="hljs-tag">&lt;/<span class="hljs-name">version</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">dependency</span>&gt;</span>
    
        <span class="hljs-comment">&lt;!-- JSON 解析庫 --&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">dependency</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">groupId</span>&gt;</span>org.json4s<span class="hljs-tag">&lt;/<span class="hljs-name">groupId</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">artifactId</span>&gt;</span>json4s-native_2.11<span class="hljs-tag">&lt;/<span class="hljs-name">artifactId</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">version</span>&gt;</span>${json4s.version}<span class="hljs-tag">&lt;/<span class="hljs-name">version</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">dependency</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">dependency</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">groupId</span>&gt;</span>org.json4s<span class="hljs-tag">&lt;/<span class="hljs-name">groupId</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">artifactId</span>&gt;</span>json4s-jackson_2.11<span class="hljs-tag">&lt;/<span class="hljs-name">artifactId</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">version</span>&gt;</span>${json4s.version}<span class="hljs-tag">&lt;/<span class="hljs-name">version</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">dependency</span>&gt;</span>
    
        <span class="hljs-comment">&lt;!-- 日誌相關 --&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">dependency</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">groupId</span>&gt;</span>org.slf4j<span class="hljs-tag">&lt;/<span class="hljs-name">groupId</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">artifactId</span>&gt;</span>jcl-over-slf4j<span class="hljs-tag">&lt;/<span class="hljs-name">artifactId</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">version</span>&gt;</span>${slf4j.version}<span class="hljs-tag">&lt;/<span class="hljs-name">version</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">dependency</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">dependency</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">groupId</span>&gt;</span>org.slf4j<span class="hljs-tag">&lt;/<span class="hljs-name">groupId</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">artifactId</span>&gt;</span>slf4j-api<span class="hljs-tag">&lt;/<span class="hljs-name">artifactId</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">version</span>&gt;</span>${slf4j.version}<span class="hljs-tag">&lt;/<span class="hljs-name">version</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">dependency</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">dependency</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">groupId</span>&gt;</span>org.slf4j<span class="hljs-tag">&lt;/<span class="hljs-name">groupId</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">artifactId</span>&gt;</span>slf4j-log4j12<span class="hljs-tag">&lt;/<span class="hljs-name">artifactId</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">version</span>&gt;</span>${slf4j.version}<span class="hljs-tag">&lt;/<span class="hljs-name">version</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">dependency</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">dependency</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">groupId</span>&gt;</span>log4j<span class="hljs-tag">&lt;/<span class="hljs-name">groupId</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">artifactId</span>&gt;</span>log4j<span class="hljs-tag">&lt;/<span class="hljs-name">artifactId</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">version</span>&gt;</span>${log4j.version}<span class="hljs-tag">&lt;/<span class="hljs-name">version</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">dependency</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">dependencies</span>&gt;</span>
    
    <span class="hljs-tag">&lt;<span class="hljs-name">build</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">sourceDirectory</span>&gt;</span>src/main/scala<span class="hljs-tag">&lt;/<span class="hljs-name">sourceDirectory</span>&gt;</span>
    
        <span class="hljs-tag">&lt;<span class="hljs-name">plugins</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">plugin</span>&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">groupId</span>&gt;</span>org.apache.maven.plugins<span class="hljs-tag">&lt;/<span class="hljs-name">groupId</span>&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">artifactId</span>&gt;</span>maven-compiler-plugin<span class="hljs-tag">&lt;/<span class="hljs-name">artifactId</span>&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">version</span>&gt;</span>3.0<span class="hljs-tag">&lt;/<span class="hljs-name">version</span>&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">configuration</span>&gt;</span>
                    <span class="hljs-tag">&lt;<span class="hljs-name">source</span>&gt;</span>1.8<span class="hljs-tag">&lt;/<span class="hljs-name">source</span>&gt;</span>
                    <span class="hljs-tag">&lt;<span class="hljs-name">target</span>&gt;</span>1.8<span class="hljs-tag">&lt;/<span class="hljs-name">target</span>&gt;</span>
                    <span class="hljs-tag">&lt;<span class="hljs-name">encoding</span>&gt;</span>UTF-8<span class="hljs-tag">&lt;/<span class="hljs-name">encoding</span>&gt;</span>
                <span class="hljs-tag">&lt;/<span class="hljs-name">configuration</span>&gt;</span>
            <span class="hljs-tag">&lt;/<span class="hljs-name">plugin</span>&gt;</span>
    
            <span class="hljs-tag">&lt;<span class="hljs-name">plugin</span>&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">groupId</span>&gt;</span>net.alchim31.maven<span class="hljs-tag">&lt;/<span class="hljs-name">groupId</span>&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">artifactId</span>&gt;</span>scala-maven-plugin<span class="hljs-tag">&lt;/<span class="hljs-name">artifactId</span>&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">version</span>&gt;</span>3.2.0<span class="hljs-tag">&lt;/<span class="hljs-name">version</span>&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">executions</span>&gt;</span>
                    <span class="hljs-tag">&lt;<span class="hljs-name">execution</span>&gt;</span>
                        <span class="hljs-tag">&lt;<span class="hljs-name">goals</span>&gt;</span>
                            <span class="hljs-tag">&lt;<span class="hljs-name">goal</span>&gt;</span>compile<span class="hljs-tag">&lt;/<span class="hljs-name">goal</span>&gt;</span>
                            <span class="hljs-tag">&lt;<span class="hljs-name">goal</span>&gt;</span>testCompile<span class="hljs-tag">&lt;/<span class="hljs-name">goal</span>&gt;</span>
                        <span class="hljs-tag">&lt;/<span class="hljs-name">goals</span>&gt;</span>
                        <span class="hljs-tag">&lt;<span class="hljs-name">configuration</span>&gt;</span>
                            <span class="hljs-tag">&lt;<span class="hljs-name">args</span>&gt;</span>
                                <span class="hljs-tag">&lt;<span class="hljs-name">arg</span>&gt;</span>-dependencyfile<span class="hljs-tag">&lt;/<span class="hljs-name">arg</span>&gt;</span>
                                <span class="hljs-tag">&lt;<span class="hljs-name">arg</span>&gt;</span>${project.build.directory}/.scala_dependencies<span class="hljs-tag">&lt;/<span class="hljs-name">arg</span>&gt;</span>
                            <span class="hljs-tag">&lt;/<span class="hljs-name">args</span>&gt;</span>
                        <span class="hljs-tag">&lt;/<span class="hljs-name">configuration</span>&gt;</span>
                    <span class="hljs-tag">&lt;/<span class="hljs-name">execution</span>&gt;</span>
                <span class="hljs-tag">&lt;/<span class="hljs-name">executions</span>&gt;</span>
            <span class="hljs-tag">&lt;/<span class="hljs-name">plugin</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">plugins</span>&gt;</span>
    
    <span class="hljs-tag">&lt;/<span class="hljs-name">build</span>&gt;</span>
    

</project>

  • 創建 Scala 源碼目錄 src/main/scala

    並且設置這個目錄爲 Source Root

    20190602150555
  • 創建文件, 數據讀取
    Step 1: 創建文件

    創建 Spark Application 主類 cn.itcast.taxi.TaxiAnalysisRunner

    package cn.itcast.taxi
    

    object TaxiAnalysisRunner {

    def main(args: Array[String]): Unit = {

    }
    }

    Step 2: 數據讀取
    數據讀取之前要做兩件事
    1. 初始化環境, 導入必備的一些包

    2. 在工程根目錄中創建 dataset 文件夾, 並拷貝數據集進去

    代碼如下
    object TaxiAnalysisRunner {
    

    def main(args: Array[String]): Unit = {
    // 1. 創建 SparkSession
    val spark = SparkSession.builder()
    .master(“local[6]”)
    .appName(“taxi”)
    .getOrCreate()

    // 2. 導入函數和隱式轉換
    import spark.implicits._
    import org.apache.spark.sql.functions._
    
    // 3. 讀取文件
    val taxiRaw = spark.read
      .option("header", value = true)
      .csv("dataset/half_trip.csv")
    
    taxiRaw.show()
    taxiRaw.printSchema()
    

    }
    }

    運行結果如下
    root
     |-- medallion: string (nullable = true)
     |-- hack_license: string (nullable = true)
     |-- vendor_id: string (nullable = true)
     |-- rate_code: string (nullable = true)
     |-- store_and_fwd_flag: string (nullable = true)
     |-- pickup_datetime: string (nullable = true)
     |-- dropoff_datetime: string (nullable = true)
     |-- passenger_count: string (nullable = true)
     |-- trip_time_in_secs: string (nullable = true)
     |-- trip_distance: string (nullable = true)
     |-- pickup_longitude: string (nullable = true)
     |-- pickup_latitude: string (nullable = true)
     |-- dropoff_longitude: string (nullable = true)
     |-- dropoff_latitude: string (nullable = true)
    20190602153339
    下一步
    1. 剪去多餘列

      現在數據集中包含了一些多餘的列, 在後續的計算中並不會使用到, 如果讓這些列參與計算的話, 會影響整體性能, 浪費集羣資源

    2. 類型轉換

      可以看到, 現在的數據集中, 所有列類型都是 String, 而在一些統計和運算中, 不能使用 String 來進行, 所以要將這些數據轉爲對應的類型

    5. 數據清洗

    導讀
    1. Row 對象轉爲 Trip

    2. 處理轉換過程中的報錯

    數據轉換

    通過 DataFrameReader 讀取出來的數據集是 DataFrame, 而 DataFrame 中保存的是 Row 對象, 但是後續我們在進行處理的時候可能要使用到一些有類型的轉換, 也需要每一列數據對應自己的數據類型, 所以, 需要將 Row 所代表的弱類型對象轉爲 Trip 這樣的強類型對象, 而 Trip 對象則是一個樣例類, 用於代表一個出租車的行程

    Step 1: 創建 Trip 樣例類

    Trip 是一個強類型的樣例類, 一個 Trip 對象代表一個出租車行程, 使用 Trip 可以對應數據集中的一條記錄

    object TaxiAnalysisRunner {
    

    def main(args: Array[String]): Unit = {
    // 此處省略 Main 方法中內容
    }

    }

    /**

    • 代表一個行程, 是集合中的一條記錄
    • @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
      )
    Step 2: 將 Row 對象轉爲 Trip 對象, 從而將 DataFrame 轉爲 Dataset[Trip]

    首先應該創建一個新方法來進行這種轉換, 畢竟是一個比較複雜的轉換操作, 不能怠慢

    object TaxiAnalysisRunner {
    

    def main(args: Array[String]): Unit = {
    // … 省略數據讀取

    // 4. 數據轉換和清洗
    val taxiParsed = taxiRaw.rdd.map(parse)
    

    }

    /**
    * 將 Row 對象轉爲 Trip 對象, 從而將 DataFrame 轉爲 Dataset[Trip] 方便後續操作
    * @param row DataFrame 中的 Row 對象
    * @return 代表數據集中一條記錄的 Trip 對象
    */
    def parse(row: Row): Trip = {

    }
    }

    case class Trip(…)

    Step 3: 創建 Row 對象的包裝類型

    因爲在針對 Row 類型對象進行數據轉換時, 需要對一列是否爲空進行判斷和處理, 在 Scala 中爲空的處理進行一些支持和封裝, 叫做 Option, 所以在讀取 Row 類型對象的時候, 要返回 Option 對象, 通過一個包裝類, 可以輕鬆做到這件事

    創建一個類 RichRow 用以包裝 Row 類型對象, 從而實現 getAs 的時候返回 Option 對象

    object TaxiAnalysisRunner {
    

    def main(args: Array[String]): Unit = {
    // …

    // 4. 數據轉換和清洗
    val taxiParsed = taxiRaw.rdd.map(parse)
    

    }

    def parse(row: Row): Trip = {…}

    }

    case class Trip(…)

    class RichRow(row: Row) {

    def getAs[T](field: String): Option[T] = {
    if (row.isNullAt(row.fieldIndex(field)) || StringUtils.isBlank(row.getAsString)) {
    None
    } else {
    Some(row.getAsT)
    }
    }
    }

    Step 4: 轉換

    流程已經存在, 並且也已經爲空值處理做了支持, 現在就可以進行轉換了

    首先根據數據集的情況會發現, 有如下幾種類型的信息需要處理

    • 字符串類型

      執照號就是字符串類型, 對於字符串類型, 只需要判斷空, 不需要處理, 如果是空字符串, 加入數據集的應該是一個 null

    • 時間類型

      上下車時間就是時間類型, 對於時間類型需要做兩個處理

      • 轉爲時間戳, 比較容易處理

      • 如果時間非法或者爲空, 則返回 0L

    • Double 類型

      上下車的位置信息就是 Double 類型, Double 類型的數據在數據集中以 String 的形式存在, 所以需要將 String 類型轉爲 Double 類型

    總結來看, 有兩類數據需要特殊處理, 一類是時間類型, 一類是 Double 類型, 所以需要編寫兩個處理數據的幫助方法, 後在 parse 方法中收集爲 Trip 類型對象

    object TaxiAnalysisRunner {
    

    def main(args: Array[String]): Unit = {
    // …

    // 4. 數據轉換和清洗
    val taxiParsed = taxiRaw.rdd.map(parse)
    

    }

    def parse(row: Row): Trip = {
    // 通過使用轉換方法依次轉換各個字段數據
    val row = new RichRow(row)
    val license = row.getAsString.orNull
    val pickUpTime = parseTime(row, “pickup_datetime”)
    val dropOffTime = parseTime(row, “dropoff_datetime”)
    val pickUpX = parseLocation(row, “pickup_longitude”)
    val pickUpY = parseLocation(row, “pickup_latitude”)
    val dropOffX = parseLocation(row, “dropoff_longitude”)
    val dropOffY = parseLocation(row, “dropoff_latitude”)

    // 創建 Trip 對象返回
    Trip(license, pickUpTime, dropOffTime, pickUpX, pickUpY, dropOffX, dropOffY)
    

    }

    /**
    * 將時間類型數據轉爲時間戳, 方便後續的處理
    * @param row 行數據, 類型爲 RichRow, 以便於處理空值
    * @param field 要處理的時間字段所在的位置
    * @return 返回 Long 型的時間戳
    */
    def parseTime(row: RichRow, field: String): Long = {
    val pattern = “yyyy-MM-dd HH:mm:ss”
    val formatter = new SimpleDateFormat(pattern, Locale.ENGLISH)

    val timeOption = row.getAs[String](field)
    timeOption.map( time =&gt; formatter.parse(time).getTime )
      .getOrElse(0L)
    

    }

    /**
    * 將字符串標識的 Double 數據轉爲 Double 類型對象
    * @param row 行數據, 類型爲 RichRow, 以便於處理空值
    * @param field 要處理的 Double 字段所在的位置
    * @return 返回 Double 型的時間戳
    */
    def parseLocation(row: RichRow, field: String): Double = {
    row.getAsString.map( loc => loc.toDouble ).getOrElse(0.0D)
    }
    }

    case class Trip(…)

    class RichRow(row: Row) {…}

    異常處理

    在進行類型轉換的時候, 是一個非常容易錯誤的點, 需要進行單獨的處理

    Step 1: 思路
    20190603015655

    parse 方法應該做的事情應該有兩件

    • 捕獲異常

      異常一定是要捕獲的, 無論是否要拋給 DataFrame, 都要先捕獲一下, 獲知異常信息

      捕獲要使用 try …​ catch …​ 代碼塊

    • 返回結果

      返回結果應該分爲兩部分來進行說明

      • 正確, 正確則返回數據

      • 錯誤, 則應該返回兩類信息, 一 告知外面哪個數據出了錯, 二 告知錯誤是什麼

    對於這種情況, 可以使用 Scala 中提供的一個類似於其它語言中多返回值的 Either. Either 分爲兩個情況, 一個是 Left, 一個是 Right, 左右兩個結果所代表的意思可有由用戶來指定

    val process = (b: Double) => {       (1)
      val a = 10.0
      a / b
    }
    

    def safe(function: Double => Double, b: Double): Either[Double, (Double, Exception)] = { (2)
    try {
    val result = function(b) (3)
    Left(result)
    } catch {
    case e: Exception => Right(b, e) (4)
    }
    }

    val result = safe(process, 0) (5)

    result match { (6)
    case Left® => println®
    case Right((b, e)) => println(b, e)
    }

    1 一個函數, 接收一個參數, 根據參數進行除法運算
    2 一個方法, 作用是讓 process 函數調用起來更安全, 在其中 catch 錯誤, 報錯後返回足夠的信息 (報錯時的參數和報錯信息)
    3 正常時返回 Left, 放入正確結果
    4 異常時返回 Right, 放入報錯時的參數, 和報錯信息
    5 外部調用
    6 處理調用結果, 如果是 Right 的話, 則可以進行響應的異常處理和彌補

    EitherOption 比較像, 都是返回不同的情況, 但是 EitherRight 可以返回多個值, 而 None 不行

    如果一個 Either 有兩個結果的可能性, 一個是 Left[L], 一個是 Right[R], 則 Either 的範型是 Either[L, R]

    Step 2: 完成代碼邏輯

    加入一個 Safe 方法, 更安全

    object TaxiAnalysisRunner {
    

    def main(args: Array[String]): Unit = {
    // …

    // 4. 數據轉換和清洗
    val taxiParsed = taxiRaw.rdd.map(safe(parse))
    

    }

    /**
    * 包裹轉換邏輯, 並返回 Either
    */
    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))
    }
    }
    }
    }

    def parse(row: Row): Trip = {…}

    def parseTime(row: RichRow, field: String): Long = {…}

    def parseLocation(row: RichRow, field: String): Double = {…}
    }

    case class Trip(…)

    class RichRow(row: Row) {…}

    Step 3: 針對轉換異常進行處理

    對於 Either 來說, 可以獲取 Left 中的數據, 也可以獲取 Right 中的數據, 只不過如果當 Either 是一個 Right 實例時候, 獲取 Left 的值會報錯

    所以, 針對於 Dataset[Either] 可以有如下步驟

    1. 試運行, 觀察是否報錯

    2. 如果報錯, 則打印信息解決報錯

    3. 如果解決不了, 則通過 filter 過濾掉 Right

    4. 如果沒有報錯, 則繼續向下運行

    object TaxiAnalysisRunner {
    

    def main(args: Array[String]): Unit = {

    // 4. 數據轉換和清洗
    val taxiParsed = taxiRaw.rdd.map(safe(parse))
    val taxiGood = taxiParsed.map( either =&gt; either.left.get ).toDS()
    

    }


    }

    很幸運, 在運行上面的代碼時, 沒有報錯, 如果報錯的話, 可以使用如下代碼進行過濾

    object TaxiAnalysisRunner {
    

    def main(args: Array[String]): Unit = {

    // 4. 數據轉換和清洗
    val taxiParsed = taxiRaw.rdd.map(safe(parse))
    val taxiGood = taxiParsed.filter( either =&gt; either.isLeft )
      .map( either =&gt; either.left.get )
      .toDS()
    

    }


    }

    觀察數據集的時間分佈

    觀察數據分佈常用手段是直方圖, 直方圖反應的是數據的 "數量" 分佈

    20190603113500

    通過這個圖可以看到其實就是乘客年齡的分佈, 橫軸是乘客的年齡, 縱軸是乘客年齡的頻數分佈

    因爲我們這個項目中要對出租車利用率進行統計, 所以需要先看一看單次行程的時間分佈情況, 從而去掉一些異常數據, 保證數據是準確的

    繪製直方圖的 "圖" 留在後續的 DMP 項目中再次介紹, 現在先準備好直方圖所需要的數據集, 通過數據集來觀察即可, 直方圖需要的是兩個部分的內容, 一個是數據本身, 另外一個是數據的分佈, 也就是頻數的分佈, 步驟如下

    1. 計算每條數據的時長, 但是單位要有變化, 按照分鐘, 或者小時來作爲時長單位

    2. 統計每個時長的數據量, 例如有 500 個行程是一小時內完成的, 有 300 個行程是 1 - 2 小時內完成

    統計時間分佈直方圖
    使用 UDF 的優點和代價

    UDF 是一個很好用的東西, 特別好用, 對整體的邏輯實現會變得更加簡單可控, 但是有兩個非常明顯的缺點, 所以在使用的時候要注意, 雖然有這兩個缺點, 但是隻在必要的地方使用就沒什麼問題, 對於邏輯的實現依然是有很大幫助的

    1. UDF 中, 對於空值的處理比較麻煩

      例如一個 UDF 接收兩個參數, 是 Scala 中的 Int 類型和 Double 類型, 那麼, 在傳入 UDF 參數的時候, 如果有數據爲 null, 就會出現轉換異常

    2. 使用 UDF 的時候, 優化器可能無法對其進行優化

      UDF 對於 Catalyst 是不透明的, Catalyst 不可獲知 UDF 中的邏輯, 但是普通的 Function 對於 Catalyst 是透明的, Catalyst 可以對其進行優化

    Step 1: 編寫 UDF, 將行程時長由毫秒單位改爲小時單位

    定義 UDF, 在 UDF 中做兩件事

    1. 計算行程時長

    2. 將時長由毫秒轉爲分鐘

    object TaxiAnalysisRunner {
    

    def main(args: Array[String]): Unit = {

    // 5. 過濾行程無效的數據
    val hours = (pickUp: Long, dropOff: Long) =&gt; {
      val duration = dropOff - pickUp
      TimeUnit.HOURS.convert(, TimeUnit.MILLISECONDS)
    }
    val hoursUDF = udf(hours)
    

    }


    }

    Step 2: 統計時長分佈
    1. 第一步應該按照行程時長進行分組

    2. 求得每個分組的個數

    3. 最後按照時長排序並輸出結果

    object TaxiAnalysisRunner {
    

    def main(args: Array[String]): Unit = {

    // 5. 過濾行程無效的數據
    val hours = (pickUp: Long, dropOff: Long) =&gt; {
      val duration = dropOff - pickUp
      TimeUnit.MINUTES.convert(, TimeUnit.MILLISECONDS)
    }
    val hoursUDF = udf(hours)
    
    taxiGood.groupBy(hoursUDF($"pickUpTime", $"dropOffTime").as("duration"))
      .count()
      .sort("duration")
      .show()
    

    }


    }

    會發現, 大部分時長都集中在 1 - 19 分鐘內

    +--------+-----+
    |duration|count|
    +--------+-----+
    |       0|   86|
    |       1|  140|
    |       2|  383|
    |       3|  636|
    |       4|  759|
    |       5|  838|
    |       6|  791|
    |       7|  761|
    |       8|  688|
    |       9|  625|
    |      10|  537|
    |      11|  499|
    |      12|  395|
    |      13|  357|
    |      14|  353|
    |      15|  264|
    |      16|  252|
    |      17|  197|
    |      18|  181|
    |      19|  136|
    +--------+-----+
    Step 3: 註冊函數, 在 SQL 表達式中過濾數據

    大部分時長都集中在 1 - 19 分鐘內, 所以這個範圍外的數據就可以去掉了, 如果同學使用完整的數據集, 會發現還有一些負的時長, 好像是回到未來的場景一樣, 對於這種非法的數據, 也要過濾掉, 並且還要分析原因

    object TaxiAnalysisRunner {
    

    def main(args: Array[String]): Unit = {

    // 5. 過濾行程無效的數據
    val hours = (pickUp: Long, dropOff: Long) =&gt; {
      val duration = dropOff - pickUp
      TimeUnit.MINUTES.convert(, TimeUnit.MILLISECONDS)
    }
    val hoursUDF = udf(hours)
    
    taxiGood.groupBy(hoursUDF($"pickUpTime", $"dropOffTime").as("duration"))
      .count()
      .sort("duration")
      .show()
    
    spark.udf.register("hours", hours)
    val taxiClean = taxiGood.where("hours(pickUpTime, dropOffTime) BETWEEN 0 AND 3")
    taxiClean.show()
    

    }


    }

    6. 行政區信息

    目標和步驟
    目標

    能夠通過 GeoJSON 判斷一個點是否在一個區域內, 能夠使用 JSON4S 解析 JSON 數據

    步驟
    1. 需求介紹

    2. 工具介紹

    3. 解析 JSON

    4. 讀取 Geometry

    總結
    • 整體流程

      1. JSON4S 介紹

      2. ESRI 介紹

      3. 編寫函數實現 經緯度 → Geometry 轉換

    • 後續可以使用函數來進行轉換, 並且求得時間差

    6.1. 需求介紹

    目標和步驟
    目標

    理解表示地理位置常用的 GeoJSON

    步驟
    1. 思路整理

    2. GeoJSON 是什麼

    3. GeoJSON 的使用

    思路整理
    • 需求

      項目的任務是統計出租車在不同行政區的平均等待時間, 所以源數據集和經過計算希望得到的新數據集大致如下

      • 源數據集

        20190812104021
      • 目標數據集

        20190812104113
    • 目標數據集分析

      目標數據集中有三列, borough, avg(seconds), stddev_samp(seconds)

      • borough 表示目的地行政區的名稱

      • avg(seconds)stddev_samp(seconds)seconds 的聚合, seconds 是下車時間和下一次上車時間之間的差值, 代表等待時間

      所以有兩列數據是現在數據集中沒有

      • borough 要根據數據集中的經緯度, 求出其行政區的名字

      • seconds 要根據數據集中上下車時間, 求出差值

    • 步驟

      1. 求出 borough

        1. 讀取行政區位置信息

        2. 搜索每一條數據的下車經緯度所在的行政區

        3. 在數據集中添加行政區列

      2. 求出 seconds

      3. 根據 borough 計算平均等待時間, 是一個聚合操作

    GeoJSON 是什麼
    • 定義

      • GeoJSON 是一種基於 JSON 的開源標準格式, 用來表示地理位置信息

      • 其中定了很多對象, 表示不同的地址位置單位

    • 如何表示地理位置

      類型 例子

      51px SFA Point.svg
      {
          "type": "Point",
          "coordinates": [30, 10]
      }

      線段

      51px SFA LineString.svg
      {
          "type": "Point",
          "coordinates": [30, 10]
      }

      多邊形

      51px SFA LineString.svg
      {
          "type": "Point",
          "coordinates": [30, 10]
      }
      51px SFA Polygon with hole.svg
      {
          "type": "Polygon",
          "coordinates": [
              [[35, 10], [45, 45], [15, 40], [10, 20], [35, 10]],
              [[20, 30], [35, 35], [30, 20], [20, 30]]
          ]
      }
    • 數據集

      • 行政區範圍可以使用 GeoJSON 中的多邊形來表示

      • 課程中爲大家提供了一份表示了紐約的各個行政區範圍的數據集, 叫做 nyc-borough-boundaries-polygon.geojson

        20190603155616

    • 使用步驟

      1. 創建一個類型 Feature, 對應 JSON 文件中的格式

      2. 通過解析 JSON, 創建 Feature 對象

      3. 通過 Feature 對象創建 GeoJSON 表示一個地理位置的 Geometry 對象

      4. 通過 Geometry 對象判斷一個經緯度是否在其範圍內

    總結
    • 思路

      1. 從需求出發, 設計結果集

      2. 推導結果集所欠缺的字段

      3. 補齊欠缺的字段, 生成結果集, 需求完成

    • 後續整體上要做的事情

      • 需求是查看出租車在不同行政區的等待客人的時間

      • 需要補充兩個點, 一是出租車下客點的行政區名稱, 二是等待時間

      • 本章節聚焦於行政區的信息補充

    • 學習步驟

      1. 介紹 JSON 解析的工具

      2. 介紹讀取 GeoJSON 的工具

      3. JSON 解析

      4. 讀取 GeoJSON

    6.2. 工具介紹

    目標和步驟
    目標

    理解 JSON 解析和 Geometry 解析所需要的工具, 後續使用這些工具補充行政區信息

    步驟
    1. JSON4S

    2. ESRI Geometry

    JSON4S 介紹
    • 介紹

      一般在 Java 中, 常使用如下三個工具解析 JSON

      • Gson

        Google 開源的 JSON 解析工具, 比較人性化, 易於使用, 但是性能不如 Jackson, 也不如 Jackson 有積澱

      • Jackson

        Jackson 是功能最完整的 JSON 解析工具, 也是最老牌的 JSON 解析工具, 性能也足夠好, 但是 API 在一開始支持的比較少, 用起來稍微有點繁瑣

      • FastJson

        阿里巴巴的 JSON 開源解析工具, 以快著稱, 但是某些方面用起來稍微有點反直覺

    • 什麼是 JSON 解析

      20190603161629
      • 讀取 JSON 數據的時候, 讀出來的是一個有格式的字符串, 將這個字符串轉換爲對象的過程就叫做解析

      • 可以使用 JSON4S 來解析 JSON, JSON4S 是一個其它解析工具的 Scala 封裝以適應 Scala 的對象轉換

      • JSON4S 支持 Jackson 作爲底層的解析工具

    • Step 1: 導入 Maven 依賴

      <!-- JSON4S -->
      <dependency>
          <groupId>org.json4s</groupId>
          <artifactId>json4s-native_2.11</artifactId>
          <version>${json4s.version}</version>
      </dependency>
      <!-- JSON4S 的 Jackson 集成庫 -->
      <dependency>
          <groupId>org.json4s</groupId>
          <artifactId>json4s-jackson_2.11</artifactId>
          <version>${json4s.version}</version>
      </dependency>
    • Step 2: 解析 JSON

      步驟
      1. 解析 JSON 對象

      2. 序列化 JSON 對象

      3. 使用 Jackson 反序列化 Scala 對象

      4. 使用 Jackson 序列化 Scala 對象

      代碼
      import org.json4s._
      import org.json4s.jackson.JsonMethods._
      import org.json4s.jackson.Serialization.{read, write}
      

    case class Product(name: String, price: Double)

    val product =
    “”"
    |{“name”:“Toy”,“price”:35.35}
    “”".stripMargin

    // 可以解析 JSON 爲對象
    val obj: Product = parse(product).extra[Product]

    // 可以將對象序列化爲 JSON
    val str: String = compact(render(Product(“電視”, 10.5)))

    // 使用序列化 API 之前, 要先導入代表轉換規則的 formats 對象隱式轉換
    implicit val formats = Serialization.formats(NoTypeHints)

    // 可以使用序列化的方式來將 JSON 字符串反序列化爲對象
    val obj1 = readPerson

    // 可以使用序列化的方式將對象序列化爲 JSON 字符串
    val str1 = write(Product(“電視”, 10.5))

    GeoJSON 讀取工具的介紹
    • 介紹

      • 讀取 GeoJSON 的工具有很多, 但是大部分都過於複雜, 有一些只能 Java 中用

      • 有一個較爲簡單, 也沒有使用底層 C 語言開發的解析 GeoJSON 的類庫叫做 ESRI Geometry, Scala 中也可以支持

    • 使用

      ESRI Geometry 的使用比較的簡單, 大致就如下這樣調用即可

      val mg = GeometryEngine.geometryFromGeoJson(jsonStr, 0, Geometry.Type.Unknown) (1)
      val geometry = mg.getGeometry (2)
      

    GeometryEngine.contains(geometry, other, csr) (3)

    1 讀取 JSON 生成 Geometry 對象
    2 重點: 一個 Geometry 對象就表示一個 GeoJSON 支持的對象, 可能是一個點, 也可能是一個多邊形
    3 判斷一個 Geometry 中是否包含另外一個 Geometry
    總結
    • JSON 解析

      • FastJSONGson 直接在 Scala 中使用會出現問題, 因爲 Scala 的對象體系和 Java 略有不同

      • 最爲適合 Scala 的方式是使用 JSON4S 作爲上層 API, Jackson 作爲底層提供 JSON 解析能力, 共同實現 JSON 解析

      • 其使用方式非常簡單, 兩行即可解析

      implicit val formats = Serialization.formats(NoTypeHints)
      val obj = read[Person](product)
    • GeoJSON 的解析

      • 有一個很適合 Scala 的 GeoJSON 解析工具, 叫做 ESRI Geometry, 其可以將 GeoJSON 字符串轉爲 Geometry 對象, 易於使用

      GeometryEngine.geometryFromGeoJson(jsonStr, 0, Geometry.Type.Unknown)
    • 後續工作

      1. 讀取行政區的數據集, 解析 JSON 格式, 將 JSON 格式的字符串轉爲對象

      2. 使用 ESRIGeometryEngine 讀取行政區的 Geometry 對象的 JSON 字符串, 生成 Geometry 對象

      3. 使用上車點和下車點的座標創建 Point 對象 ( Geometry 的子類)

      4. 判斷 Point 是否在行政區的 Geometry 的範圍內 (行政區的 Geometry 其實本質上是子類 Polygon 的對象)

    6.3. 具體實現

    目標和步驟
    目標

    通過 JSON4SESRI 配合解析提供的 GeoJSON 數據集, 獲取紐約的每個行政區的範圍

    步驟
    1. 解析 JSON

    2. 使用 ESRI 生成表示行政區的一組 Geometry 對象

    解析 JSON
    • 步驟

      1. 對照 JSON 中的格式, 創建解析的目標類

      2. 解析 JSON 數據轉爲目標類的對象

      3. 讀取數據集, 執行解析

    • Step 1: 創建目標類

      • GeoJSON

        {
        	"type": "FeatureCollection",
        	"features": [ (1)
            {
              "type": "Feature",
              "id": 0,
              "properties": {
                "boroughCode": 5,
                "borough": "Staten Island",
                "@id": "http:\/\/nyc.pediacities.com\/Resource\/Borough\/Staten_Island"
              },
              "geometry": {
                "type": "Polygon",
                "coordinates": [
                  [
                    [-74.050508064032471, 40.566422034160816],
                    [-74.049983525625748, 40.566395924928273]
                  ]
                ]
              }
            }
          ]
        }
        1 features 是一個數組, 其中每一個 Feature 代表一個行政區
      • 目標類

        case class FeatureCollection(
          features: List[Feature]
        )
        

    case class Feature(
    id: Int,
    properties: Map[String, String],
    geometry: JObject
    )

    case class FeatureProperties(boroughCode: Int, borough: String)

  • Step 2: 將 JSON 字符串解析爲目標類對象

    創建工具類實現功能

    object FeatureExtraction {
    
  • def parseJson(json: String): FeatureCollection = {
    implicit val format: AnyRef with Formats = Serialization.formats(NoTypeHints)
    val featureCollection = readFeatureCollection
    featureCollection
    }
    }

  • Step 3: 讀取數據集, 轉換數據

    val geoJson = Source.fromFile("dataset/nyc-borough-boundaries-polygon.geojson").mkString
    val features = FeatureExtraction.parseJson(geoJson)
  • 解析 GeoJSON
    • 步驟

      1. 轉換 JSONGeometry 對象

    • 表示行政區的 JSON 段在哪

      {
      	"type": "FeatureCollection",
      	"features": [
          {
            "type": "Feature",
            "id": 0,
            "properties": {
              "boroughCode": 5,
              "borough": "Staten Island",
              "@id": "http:\/\/nyc.pediacities.com\/Resource\/Borough\/Staten_Island"
            },
            "geometry": { (1)
              "type": "Polygon",
              "coordinates": [
                [
                  [-74.050508064032471, 40.566422034160816],
                  [-74.049983525625748, 40.566395924928273]
                ]
              ]
            }
          }
        ]
      }
      1 geometry 段即是 Geometry 對象的 JSON 表示
    • 通過 ESRI 解析此段

      case class Feature(
        id: Int,
        properties: Map[String, String],
        geometry: JObject             (1)
      ) {
      

    def getGeometry: Geometry = { (2)
    GeometryEngine.geoJsonToGeometry(compact(render(geometry)), 0, Geometry.Type.Unknown).getGeometry
    }
    }

    1 geometry 對象需要使用 ESRI 解析並生成, 所以此處並沒有使用具體的對象類型, 而是使用 JObject 表示一個 JsonObject, 並沒有具體的解析爲某個對象, 節省資源
    2 JSON 轉爲 Geometry 對象
    在出租車 DataFrame 中增加行政區信息
    • 步驟

      1. Geometry 數據集按照區域大小排序

      2. 廣播 Geometry 信息, 發給每一個 Executor

      3. 創建 UDF, 通過經緯度獲取行政區信息

      4. 統計行政區信息

    • Step 1: 排序 Geometry

      • 動機: 後續需要逐個遍歷 Geometry 對象, 取得每條出租車數據所在的行政區, 大的行政區排在前面效率更好一些

      val areaSortedFeatures = features.features.sortBy(feature => {
          (feature.properties("boroughCode"), - feature.getGeometry.calculateArea2D())
        })
    • Step 2: 發送廣播

      • 動機: Geometry 對象數組相對來說是一個小數據集, 後續需要使用 Spark 來進行計算, 將 Geometry 分發給每一個 Executor 會顯著減少 IO 通量

      val featuresBc = spark.sparkContext.broadcast(areaSortedFeatures)
    • Step 3: 創建 UDF

      • 動機: 創建 UDF, 接收每個出租車數據的下車經緯度, 轉爲行政區信息, 以便後續實現功能

      val boroughLookUp = (x: Double, y: Double) => {
        val features: Option[Feature] = featuresBc.value.find(feature => {
          GeometryEngine.contains(feature.getGeometry, new Point(x, y), SpatialReference.create(4326))
        })
        features.map(feature => {
          feature.properties("borough")
        }).getOrElse("NA")
      }
      

    val boroughUDF = udf(boroughLookUp)

  • Step 4: 測試轉換結果, 統計每個行政區的出租車數據數量

    • 動機: 寫完功能最好先看看, 運行一下

    taxiClean.groupBy(boroughUDF('dropOffX, 'dropOffY))
      .count()
      .show()
  • 總結
    • 具體的實現分爲兩個大步驟

      1. 解析 JSON 生成 Geometry 數據

      2. 通過 Geometry 數據, 取得每一條出租車數據的行政區信息

    • Geometry 數據的生成又有如下步驟

      1. 使用 JSON4S 解析行政區區域信息的數據集

      2. 取得其中每一個行政區信息的 Geometry 區域信息, 轉爲 ESRIGeometry 對象

    • 查詢經緯度信息, 獲取其所在的區域, 有如下步驟

      1. 遍歷 Geometry 數組, 搜索經緯度所表示的 Point 對象在哪個區域內

      2. 返回區域的名稱

        • 使用 UDF 的目的是爲了統計數據集, 後續會通過函數直接完成功能

    7. 會話統計

    目標和步驟
    目標
    • 統計每個行政區的所有行程, 查看每個行政區平均等候客人的時間

    • 掌握會話統計的方式方法

    步驟
    1. 會話統計的概念

    2. 功能實現

    會話統計的概念
    • 需求分析

      • 需求

        統計每個行政區的平均等客時間

      • 需求可以拆分爲如下幾個步驟

        1. 按照行政區分組

        2. 在每一個行政區中, 找到同一個出租車司機的先後兩次訂單, 本質就是再次針對司機的證件號再次分組

        3. 求出這兩次訂單的下車時間和上車時間只差, 便是等待客人的時間

        4. 針對一個行政區, 求得這個時間的平均數

      • 問題: 分組效率太低

        分組的效率相對較低

        • 分組是 Shuffle

        • 兩次分組, 包括後續的計算, 相對比較複雜

      • 解決方案: 分區後在分區中排序

        1. 按照 License 重新分區, 如此一來, 所有相同的司機的數據就會在同一個分區中

        2. 計算分區中連續兩條數據的時間差

          20190813003239
        上述的計算存在一個問題, 一個分組會有多個司機的數據, 如何劃分每個司機的數據邊界? 其實可以先過濾一下, 計算時只保留同一個司機的數據
      • 無論是剛纔的多次分組, 還是後續的分區, 都是要找到每個司機的會話, 通過會話來完成功能, 也叫做會話分析

    功能實現
    • 步驟

      1. 過濾掉沒有經緯度的數據

      2. 按照 License 重新分區並按照 LicensepickUpTime 排序

      3. 求得每個司機的下車和下次上車的時間差

      4. 求得每個行政區得統計數據

    • Step 1: 過濾沒有經緯度的數據

      val taxiDone = taxiClean.where("dropOffX != 0 and dropOffY != 0 and pickUpX != 0 and pickUpY != 0")
    • Step 2: 劃分會話

      val sessions = taxiDone.repartition('license)
        .sortWithinPartitions('license, 'pickUpTime)
    • Step 3: 求得時間差

      1. 處理每個分區, 通過 ScalaAPI 找到相鄰的數據

        sessions.mapPartitions(trips => {
          val viter = trips.sliding(2)
        })
      2. 過濾司機不同的相鄰數據

        sessions.mapPartitions(trips => {
          val viter = trips.sliding(2)
            .filter(_.size == 2)
            .filter(p => p.head.license == p.last.license)
        })
      3. 求得時間差

        def boroughDuration(t1: Trip, t2: Trip): (String, Long) = {
          val borough = boroughLookUp(t1.dropOffX, t1.dropOffY)
          val duration = (t2.pickUpTime - t1.dropOffTime) / 1000
          (borough, duration)
        }
        

    val boroughDurations = sessions.mapPartitions(trips => {
    val viter = trips.sliding(2)
    .filter(_.size == 2)
    .filter(p => p.head.license == p.last.license)
    viter.map(p => boroughDuration(p.head, p.last))
    }).toDF(“borough”, “seconds”)

  • Step 4: 統計數據

    boroughDurations.where("seconds > 0")
      .groupBy("borough")
      .agg(avg("seconds"), stddev("seconds"))
      .show()
  • 總結
    • 其實會話分析的難點就是理解需求

      • 需求是找到每個行政區的待客時間, 就是按照行政區分組

      • 需求是找到待客時間, 就是按照司機進行分組, 並且還要按照時間進行排序, 纔可找到一個司機相鄰的兩條數據

    • 但是分組和統計的效率較低

      • 可以把相同司機的所有形成發往一個分區

      • 然後按照司機的 License 和上車時間綜合排序

      • 這樣就可以找到同一個司機的兩次行程之間的差值

    發表評論
    所有評論
    還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
    相關文章