天天看點

基于R shinydashboard的道路交通可視化案例

基于R shinydashboard的道路交通可視化案例

作品概述

這個作品剛剛獲得“中國電科杯”城市資料創新大賽的城市交通專項獎,現在作為案例分享出來供同行交流讨論。虛的就不說了,此文隻讨論技術。

先上圖:

基于R shinydashboard的道路交通可視化案例

實時道路交通可視化

基于R shinydashboard的道路交通可視化案例

實時道路擁堵排名

基于R shinydashboard的道路交通可視化案例

曆史路況時間序列圖

基于R shinydashboard的道路交通可視化案例

每日每小時道況熱力圖

基于R shinydashboard的道路交通可視化案例

每小時内道況南丁格爾玫瑰圖

基于R shinydashboard的道路交通可視化案例

每小時内總體路況圓盤圖

基于R shinydashboard的道路交通可視化案例

各道路每日擁堵時長排名

基于R shinydashboard的道路交通可視化案例

實時路況資料下載下傳

這個作品的建構過程與設計工具如下:

原型設計:在建構dashboard之前要先思考要做哪些分析和可視化,這些圖形如何布局,顔色風格如何,互動效果如何做……這些都需要設計原型,這裡用的是Axure;

資料采集:這裡主要采集的是深圳市道路交通委員會網站上的道路交通指數資料。主要是分兩種采集方式:一種是在shinydashboard中内嵌的實時爬蟲,這個用R實作;另一種是用Python爬蟲程式在伺服器上持續采集一個月的資料作為曆史資料進行資料分析。政府網站一般技術含量都很低,沒什麼反爬措施,是以隻要不是爬得太猛把他們伺服器搞垮了,爬起來沒有任何難度。

資料存儲:主要用來存儲曆史資料,本作品以5分鐘為間隔采集一次,一個月下來也有百萬行資料。這裡用到的是MySQL。

資料處理、分析和可視化:這步是核心,下文細說。

CSS美化:原生的shinydashboard是很醜的,好在它支援引入CSS,是以可以通過CSS對顔色、文字大小等進行重新設計,以達到整體風格的統一、美觀。

shiny與shinydashboard的特點

shiny是Rstudio出品的一個可以在R中建構互動式網頁的引擎,shinydashboard則是基于shiny提供的一套快速搭建dashboard的工具。

先談談shiny的優缺點:

能以較低的學習成本實作互動式網頁設計。資料分析師畢竟不是前端工程師,無需去學習HTML、CSS和JavaScript那一大堆東西(當然懂更好,也沒必要完全區分開來,至少騰訊就希望招懂前端的資料分析師或懂資料分析的前端工程師)也能很快建構出一個互動式網頁。

開發原型成本低。一個綜合能力強的資料分析師(比如我),一個人就能快速地實作資料采集、資料處理、資料分析和資料可視化原型設計的全過程,這個時間成本遠遠小于在一支分工明确的團隊上下遊間的磨合和交接。

基本滿足常用的互動式效果。是以作為一個原型也是夠用了,産品經理可以直接提供給開發人員,分析師也可以直接拿來向上司彙報。

基本不支援動态效果。至少到現在shiny連個支援動畫的接口都沒有,更别說前端那些炫酷的動态效果,這些都實作不了。

計算速度慢。一旦資料量大或者資料處理過程複雜或者圖形構造複雜,就會卡得比較久。不過速度慢是R的天生缺陷。

展示内容有限。想在shiny中展示什麼,前提條件是shiny有對應的接口,否則白搭,這就是R中可以生成動畫但卻無法在shiny中展示的原因。

部署困難。R中有另一個做dashboard的包flexdashboard,比shiny簡單而且直接生成一個html,但是shiny由于是Rstudio的商業軟體,是以受到的限制很多。拿部署來說,目前有兩種方式:一種是自己搭建局域伺服器,不過這種方式隻能内部人自己看,無法放在網際網路上公開;另一種方式就是部署到shinyapps.io網站上去,不過這個網站本身奇卡無比,十分影響體驗。

所用工具包

再談談R中所用的包,主要分類兩類:資料處理和可視化。

資料處理包:

rvest用來做實時爬蟲,每次程式一啟動就開始做最新的實時爬蟲。

plyr和dplyr主要做資料篩選、排序、聚合計算等。

stringr用來對字元串分割、轉換等。

data.table用來讀取大量的曆史資料并做一些簡單的處理。

reshape2用來對資料框做變形處理。

可視化包:

shiny和shinydashboard用來搭建基本的網頁結構和内容。

ggplot2這個神器應該無人不知無人不曉,用來做基本的可視化。

ggiraph這也是個神奇的包,能把ggplot2的圖形轉化成互動式圖形,與ggplot2堪稱絕配。

dygraphs用來畫互動式的時間序列折線圖。

DT用來呈現互動式表格。

建構shinydashboard

一個shiny程式基本包含兩部分:ui.R和server.R。其中ui.R主要用來設計限定網頁結構,比如每一行是什麼圖形或内容,尺寸大小如何設定,文字怎麼插入,控件的位置和編号等等——基本上可以概括為:一切關于外在結構而不涉及内在計算的都在ui.R中設計。反之,server.R就主要用來做資料處理、計算和可視化,并把結果映射至ui.R中,是以它才是核心,代碼也長得多。

本作品的ui.R核心代碼如下:

dashboardPage( 

  dashboardHeader(title = "深圳道路的資料畫像",titleWidth = 220), 

  dashboardSidebar( 

    sidebarMenu( 

    menuItem(iconv("實時路況展示",to="UTF-8"), tabName = "realtime_traffic", icon = icon("road")), 

    radioButtons(inputId = "choose_direction", label = "請選擇一個方向:",selected=1,choiceNames=c("1(東->西 或 北->南)","2(西->東 或 南->北)"),choiceValues=1:2), 

    radioButtons(inputId = "rank_class", label = "請選擇道路排名類别:",selected="擁堵排名",choices=c("擁堵排名","通暢排名")), 

    numericInput(inputId = "rank_num",label = "請輸入道路交通排名數量:",min = 5,max = 100,step = 1,value=20), 

    menuItem(iconv("道路畫像分析",to="UTF-8"), icon = icon("area-chart"), tabName = "statistics"), 

    selectInput(inputId = "choose_road",label = "請選擇一條道路:",choices = all_roads), 

    radioButtons(inputId = "choose_direction2", label = "請選擇一個方向:",selected=1,choiceNames=c("1(東->西 或 北->南)","2(西->東 或 南->北)"),choiceValues=1:2), 

    menuItem(iconv("曆史路況回顧",to="UTF-8"), icon = icon("calendar"), tabName = "history"), 

    dateInput(inputId = "choose_date", label = "請選擇4月的一天:",value = "2017-04-01",min = "2017-04-01",max = "2017-04-30"), 

    radioButtons(inputId = "choose_direction3", label = "請選擇一個方向:",selected="東->西",choices=c("東->西","西->東","北->南","南->北")), 

    radioButtons(inputId = "rank_class2", label = "請選擇道路排名類别:",selected="擁堵排名",choices=c("擁堵排名","通暢排名")), 

    numericInput(inputId = "rank_num2",label = "請輸入道路交通排名數量:",min = 5,max = 100,step = 1,value=20), 

    menuItem(iconv("實時資料下載下傳",to="UTF-8"),tabName = "data_download",icon = icon("database")) 

 ),width = 220), 

  dashboardBody( 

    tags$head( 

      tags$link(rel = "stylesheet", type = "text/css", href = "custom.css")), 

    tabItems( 

      tabItem("realtime_traffic", 

              fluidRow( 

                box(ggiraphOutput("map"),width = 12,solidHeader = T,collapsible = T) 

              ), 

                box(ggiraphOutput("rank1"),width = 6,solidHeader = T,collapsible = T), 

                box(ggiraphOutput("rank2"),width = 6,solidHeader = T,collapsible = T) 

                box(ggiraphOutput("rank3"),width = 6,solidHeader = T,collapsible = T), 

                box(ggiraphOutput("rank4"),width = 6,solidHeader = T,collapsible = T) 

              ) 

      ), 

      tabItem("statistics", 

                box(dygraphOutput("ts_history"),width = 12,solidHeader = T,collapsible = T) 

                ), 

                box(ggiraphOutput("heat"),width = 12,solidHeader = T,collapsible = T) 

                box(ggiraphOutput("polar_weekdays"),width = 6,solidHeader = T,collapsible = T), 

                box(ggiraphOutput("polar_holidays"),width = 6,solidHeader = T,collapsible = T) 

      tabItem("history", 

              # fluidRow( 

              #   box(img(src="https://ss1.bdstatic.com/70cFvXSh_Q1YnxGkpoWK1HF6hhy/it/u=3245526806,2208748886&fm=21&gp=0.jpg"),width = 12,solidHeader = T) 

              # )), 

                box(ggiraphOutput("history_bars"),width = 12,solidHeader = T,collapsible = T) 

                box(ggiraphOutput("day_rank"),width = 12,solidHeader = T,collapsible = T) 

              )), 

      tabItem("data_download", 

              box( 

                dataTableOutput("rawdata"),width = 12 

                p("資料來源:",strong(a("深圳市交通運輸委員會",href="http://sztocc.sztb.gov.cn/roadcongmore.aspx"))) 

              downloadButton("downloadCsv", "下載下傳實時資料") 

      ) 

      )) 

這些代碼基本是在模闆的基礎上改,menuItem()是左側sidebar裡的一個子頁面,tabName是這個頁面的名稱,它來指定後來的各個布局元素存放在哪個頁面裡。radioButtons是單選框,numericInput是數字輸入框,dateInput是日期選擇框,這些控件都有唯一一個id來辨別,以input$id來取它們的值傳輸至server.R中。

在dashboardBody中,每個tabItem代表一個子頁面,這跟前面的tabName一一對應;裡面的fluidRow表示一行,box是基本的容器,可以放圖形或其他輸出内容(也可以不加box,不過這樣沒法設定寬度),每個box的寬度最大為12,同一行的多個box可以各自設定總和為12的寬度(這一點比flexdashboard自由)。

注意到代碼裡的box的内容基本都有OutPut字尾(如ggiraphOutput、dataTableOutput、dygraphOutput等),這是那些包提供的shiny接口函數——換句話說如果一個包裡有這種類似于以Output的shiny接口函數,它産出的内容才能放在shiny中,否則不行(這一點限制比不上flexdashboard)。

shinydashboard預設的風格很醜,好在它支援CSS,tag$head(tag$link())就可以引進CSS,不過這個CSS必須放在項目路徑下的www檔案夾中。CSS正常地寫就好,如果遇到失效的,可能是優先級的問題,就多加點字首。鑒于CSS是前端領域的知識,本文不多說。

加載包與讀取資料都可以放在shinyServer()外面執行。關于資料,要分為兩種:一種是固定不變的,這種按照正常的資料指派方法即可;另一種是随着使用者的互動而動态改變的,這種要加個reactive()函數,比如:

traffic_choosen = reactive(traffic[traffic$direction_id == input$choose_direction, 1: 7]) 

roads_map < - reactive({ 

    roads_map < - join(roads, traffic_choosen(), type="inner") 

    roads_map 

}) 

這裡的traffic_choosen和roads_map都是根據使用者在名為direction_id的控件中的選擇值篩選的子集,是以它是動态可變的,都加了一個reactive();不過要注意加了reactive()後,traffic_choosen和roads_map就不是一個變量,而是一個函數,如果要調用這個動态變量值,就必須要加括号。

後面就是給ui.R中的架構填充内容,一般都是以output$id來指定。

output$ts_history <- renderDygraph({ 

  df1 <- (traffic_history %>% filter(road == input$choose_road & direction_id==1))[,c("time","index")] 

  df2 <- (traffic_history %>% filter(road == input$choose_road & direction_id==2))[,c("time","index")] 

  df <- full_join(df1,df2,by="time") 

  df <- df[!duplicated(df$time),] 

  rownames(df) <- df$time 

  df$time <- NULL 

  colnames(df) <- c("方向1","方向2") 

  dygraph(df,main = paste(input$choose_road,"4月内曆史交通指數",sep="")) %>%  

    dyOptions(colors=c("orange","steelblue"),axisLineColor = "#FEFEFE",drawGrid = F) %>% 

    dyLegend(show="onmouseover",labelsSeparateLines = T) %>% 

    dyRangeSelector() %>%  

     dyUnzoom() 

比如這段代碼就指定了一個id為ts_history的用dygraph包畫的時間序列互動圖,而在ui.R中要與其呼應:

box(dygraphOutput("ts_history"), width=12, solidHeader=T, collapsible=T) 

box(dygraphOutput(“ts_history”), width=12, solidHeader=T, collapsible=T) 

注意到畫圖語句外層都有一個render為字首的函數(renderDygraph、renderggiraph、renderDataTable),同樣這也是shiny接口的标志,必須以這個函數轉換之後才能在shiny中傳輸。

server.R中也可以捕獲使用者的在圖形中的互動行為,聯想到ggiraph互動圖形中總有一個參數是data_id,之前以為填什麼不重要,但是在這裡起作用了——使用者在某處點選一下,便捕獲到該處的data_id值。

selected_road <- reactive({ 

if( is.null(input$map_selected)){ 

  NA 

} else input$map_selected 

這段代碼的含義就是:如果使用者沒有選中某條道路,select_road就是NA;反之就是使用者選中的那條道路。

通過響應捕捉,可以進一步強化互動性。

主要圖形解析

下面開始對前面的幾張圖的設計思路與處理方法做個簡單的介紹。

這個圖最為複雜,它首先以深圳地圖為底圖,然後再疊加路徑圖。這個路徑圖是100+條道路的經緯度坐标畫成的。而這些道路的經緯度坐标擷取難度就比較大:首先在百度地圖上查詢道路起點和終點的坐标,然後調用百度地圖API,輸入起點和終點的坐标,查詢駕車路線,會傳回一串散點的坐标,在ggplot2中把這些散點連接配接成path就可以把道路可視化出來;不過這些點的坐标有可能不準,是以還需要人工校對。這些路徑的顔色映射的是rvest采集的該道路實時的交通指數——由于交委網站上在任一時刻并不總是會有全量的資料,是以本圖中隻展示有資料的道路的交通狀況。從這張圖中可以一眼看出整個深圳市總體的道路狀況,一眼可看出哪條道路最堵。

實時道路排名

這個比較簡單,根據使用者選擇的排名種類和排名資料對實時資料進行篩選、排序,以條形圖的方式展現即可。

這個時間序列圖把一條道路一個月内每5分鐘的交通指數都可視化出來了。這個圖形很神奇,橫軸縱軸都可以縮放選擇,下方有縮略圖。

這個熱力圖放在大屏上就像一座牆,倒還是挺壯觀的。這是一個30*24的熱力圖,每一塊映射每小時内的平均交通指數,用以呈現周期性規律。

這裡分為工作日和節假日來呈現某條道路24小時内的總體交通狀況,其中每一片花瓣映射着各種交通狀況的比例。

這張圖以每天為機關,彙總全部條道路的交通指數,用以研究每日全市總體的交通周期性規律。

原本以5min為間隔采集的資料都是離散的,無法計算連續時長。這裡借鑒了微積分的思想,以每一個5min内的最終狀态作為5min内的持續狀态,于是多個5min累加來代表一天中的擁堵時長。

這個就是展示一下實時資料并提供下載下傳,如果你不會爬蟲,可以在這上面來下載下傳資料。

Shiny中的坑

無論是ui.R還是server.R,編碼都強制是UTF-8,是以在輸入中文之前最好先将檔案存成UTF-8,以免編碼不對隻能重新讀入。同樣,讀入的資料如果包含中文,最後讀進來也要使其編碼為UTF-8,不然顯示會有亂碼。

彈出的R本地伺服器不夠清晰,甚至在有的機器上圖形一直無法加載——是以檢查的時候最好還是在浏覽器中檢查。

關于下載下傳的控件,不要在彈出的R視窗中執行,總是報錯;在浏覽器中試一下就沒問題。

按照shinyapps.io的流程,部署過程可以不出錯,甚至它還在計算時間流量;不過基于shinydashboard做的東西,打開網頁一看就會報錯“沒有名為shinydashboard的程楫包”,是以也許用純粹的shiny來做的話可能能釋出成功,但由于架構就是基于shinydashboard的,不能再推翻重來——是以就沒辦法部署。

是以shiny終究隻能産出一個原型,它離真正企業級的資料可視化系統差距還是不小。是以,如果是專門做資料可視化的,前端是必須要前進的方向。

【編輯推薦】

<a href="http://bigdata.51cto.com/art/201707/544682.htm" target="_blank">資料可視化的7個好處</a>

<a href="http://bigdata.51cto.com/art/201707/544979.htm" target="_blank">Python Crawler – 網信貸黑名單資料爬取</a>

<a href="http://bigdata.51cto.com/art/201707/545016.htm" target="_blank">超實用的資料可視化零基礎教程之實戰案例篇</a>

<a href="http://bigdata.51cto.com/art/201707/545317.htm" target="_blank">Teradata天睿公司收購StackIQ 增強Teradata Everywhere和IntelliCloud部署能力</a>

本文作者:真依然很拉風

來源:51CTO

繼續閱讀