[譯] 數據可視化教程:基于Google Sheets 和 RStudio Shiny 建立實時儀表盤
來自: https://segmentfault.com/a/1190000004426828
概述
對于物聯網應用,收集分布式日志數據到一個中央服務器并做數據可視化是一項十分常見的工作,這通常需要部署和維護自己的服務器、數據庫和可視化界面。我對系統管理任務毫無樂趣,所以我找到了一種方法使用谷歌表作為數據庫和ShinyApps.io作為可視化平臺。上傳數據到Google docs是相對簡單的,但用shiny連接到Google docs卻需要一些技巧,這促使我寫這篇教程向其他人展示如何打造一個類似的系統。
-
在 第一部分(原文) 中,我將解釋如何設置 Google spreadsheet 作為數據庫,然后使用它作為一個基本的儀表板。
-
在 第二部分(原文) ,我將教你如何在R中取回數據,并用ggplot2庫做數據可視化。
-
在 第三部分(原文) ,我將帶您用 Shiny 制作一個簡單的交互式可視化應用,通過 ShinyApps.io 平臺發布在網上。
在本教程中,我們將假裝有一個由幾個溫度和濕度傳感器構成的傳感器網絡。每個傳感器用它所在的位置名稱(如“臥室”或“客廳”)命名,并記錄每小時的溫度。我假設您已經知道了物聯網硬件如何使用HTTP請求上傳數據;因此,教程中我提供了一個Python腳本上傳虛擬的溫度和濕度值。
為什么使用 Google Sheets?
Google Sheets 可以作為一個簡單的服務器來存儲和檢索數據,這只需要寫很少的代碼。我們避免維護我們自己的服務器,另外從容易獲得原始數據中獲益。最重要的是,電子表格自己強大的分析工具,統計,數據透視表、過濾器,和交互式圖表可以嵌入在外部網站。

為什么使用 R 和 Shiny?
R是一種強大的語言專門為數據分析,結合ggplot2圖形庫,R便可以做專業的數據可視化。一旦你找到了你想要展示什么,shiny 則讓可互動的數據可視化圖表在網上發布。Shiny 由 RStudio 公司開發,他們甚至提供了免費方便的托管可視化服務。
第一部分:如何使用 Google Sheet作為數據庫服務器
在本教程的第一部分中,您將學習如何用 Google Sheets 腳本從硬件設備發起一個 HTTP POST請求來上傳數據和使用一個 HTTP GET請求來獲取數據。在這個過程中,您還將試驗表格的一些內置的分析工具:過濾器,數據透視表和圖表。
您也可以跳到第2部分,學習如何在R中獲取和操作這些數據,或者學習第3部分:如何使用Shiny。
前奏:數據存儲格式
我們將為傳感器上的每個數據點日志存儲起來,包括一個時間戳,一個設備ID,一個變量名(“溫度”,或“濕度”)以及訪問量。這意味著每次讀取的傳感器會產生兩條線:一個溫度和濕度。這種格式是R用戶所說的“長格式”。在下面的示例中,我們以每三秒一次的頻率監控兩個房間:
timestamp | ID | variable | value |
---|---|---|---|
1448227096 | kitchen | temperature | 22.3 |
1448227096 | kitchen | humidity | 45 |
1448227096 | bedroom | temperature | 24.0 |
1448227096 | bedroom | humidity | 46 |
1448227099 | kitchen | temperature | 22.4 |
1448227099 | kitchen | humidity | 45 |
1448227099 | bedroom | temperature | 23.9 |
1448227099 | bedroom | humidity | 45 |
相比“寬”格式,每個變量都有自己的一欄:
timestamp | kitchen temperature | kitchen humidity | bedroom temperature | bedroom humidity |
---|---|---|---|---|
1448227096 | 22.3 | 45 | 24.0 | 46 |
1448227099 | 22.4 | 45 | 23.9 | 45 |
寬格式是更加明顯和更緊湊的格式,但不容易擴展。如果我們添加一個新的傳感器系統,我們需要添加一個列到文件。而長格式,我們只繼續插入新行。長格式也很適合R分析,可以使用數據透視表電子表格轉換成寬格式。
最后,我們將以逗號分隔值(CSV)格式交換數據,因為它從嵌入式設備生成非常容易,要儲存為一個文本文件也很簡單,而且在任何文本編輯器或電子表格應用程序閱讀也非常方便。
準備接收數據的表格
如果你沒有一個 Google Drive 帳戶話可以創建一個。然后:
-
在 Google Drive 創建一個新的 Google Sheet
-
重命名第一張工作表(表中一個標簽文檔),這就是數據將被上傳到這里。
-
創建標題行。每一行的數據,將由unix時間戳(1970年1月1日以來的秒數)、“id”、“變量”和“閱讀量”構成。你可以凍結的行,保持可見滾動時,用“視圖”>“凍結”>“一行”。
-
打開腳本編輯器,在“工具”>“腳本編輯器…”菜單。
現在您已經準備可以開始寫代碼啦!
用谷歌腳本插入一行數據
你現在應該看一下腳本編輯器,這個工具允許您編寫自定義電子表格函數,比如 =SUM(A1:A5)或者是寫簡單的web應用程序。我們的第一步是編寫一個函數,CSV數據表格插入一行。將這段代碼復制到腳本編輯器:
function appendLines(worksheet, csvData) { var ss = SpreadsheetApp.getActiveSpreadsheet(); var sheet = ss.getSheetByName(worksheet);var rows = Utilities.parseCsv(csvData);
for ( var i = 0; i < rows.length; i++ ) { sheet.appendRow(rows[i]); } }
function test() { Logger.log("Appending fake data"); appendLines("Raw", "12345, Monday, kitchen, temperature, 30\n12346, Tuesday, living room, humidity, 50"); }</pre>
我們的 appendLine函數:
-
打開當前電子表格(整個文檔);
-
在文檔中選擇一個表,由第一個參數標識(我們目前只有一個 Raw 表);
-
解析CSV數據作為第二個參數;
-
將每一行的CSV數據,追加到表中。
我們另外寫了一個小測試來測試 appendLines 函數的調用,以確保一切正常。選擇工具欄中的 test 函數并單擊 run 按鈕可以運行這個腳本。如果執行了該腳本,這應該有兩行添加到電子表格。您還可以在腳本編輯器中知道輸出的“視圖”>“日志”來查看日志。
兩行數據將被添加到表中:
然而,我們的代碼還有有一個重要缺陷。
因為稍后當我們需要將數據作為一個web服務時,相關聯的腳本將沒有激活電子表格。我們需要更改代碼,以構建唯一ID和電子表格ID的關系,從其唯一的ID中找到獲取電子表格文檔ID:
然后修改 appendLines 通過ID獲取文檔:
function appendLines(worksheet, csvData) { var ss = SpreadsheetApp.openById("13_BUd7WJlA8Z9B5Vc-5tyf3vyRUYmIx67sDz7ZmyPG4"); var sheet = ss.getSheetByName(worksheet);var rows = Utilities.parseCsv(csvData);
for ( var i = 0; i < rows.length; i++ ) { sheet.appendRow(rows[i]); } }</pre>
再次運 test 函數,它應該添加另一個兩行“Raw”選項卡。
接收POST數據
谷歌腳本接受兩個處理HTTP GET和POST請求的特殊功能:分別是 doGet 和 doPost 。
這些函數接受一個 Event 參數類型,而且必須從 ContentService 或 HtmlService 返回一個特殊的對象。
我們首先做一個函數響應POST請求,通過返回JSON格式的Event對象的內容繼續研究API。將下面的代碼添加到腳本編輯器:
function doPost(e) { var params = JSON.stringify(e); return ContentService.createTextOutput(params); }現在點擊“發布”>“部署為web應用程序…”就可以發布了。給版本一個簡短描述然后執行,并允許匿名訪問,這樣你就可以發布到電子表格不需要身份驗證,接著部署:
![]()
復制URL:
![]()
最后你需要發送HTTP請求到這個新web服務器,然后分享公開的電子表格。返回到電子表格選項卡,打開“文件”>“分享…”。點擊“獲取共享鏈接”,允許任何人編輯鏈接:
![]()
現在,打開你最喜歡的HTTP客戶端(比如 Postman就是一款好用的Chrome擴展)。或者在Terminal中使用CURL,向這個URl發送一個POST請求。如果使用CURL確保添加 -l 參數遵循重定向以及 --data 參數來添加數據:
$ curl --data "hello, world" "{"parameter":{"hello, world":""},"contextPath":"","contentLength":12,"queryString":null,"parameters":{"hello, world":[""]},"postData":{"length":12,"type":"application/x-www-form-urlencoded","contents":"hello, world","name":"postData"}}%</pre>
我的POST請求收到了JSON響應,可以看到 e 的結構參數傳遞給了 doPost 。你將可以看到 “hello, world” POST 請求的數據將會存儲在 e["postData"]["contents"] 中。
我們可以使用URL傳遞參數 doPost 。重復相同的請求,但附加 ?sheet=Raw (確保URL引用):
$ curl -L --data "hello, world" "{"parameter":{"sheet":"Raw","hello, world":""},"contextPath":"","contentLength":12,"queryString":"sheet=Raw","parameters":{"sheet":["Raw"],"hello, world":[""]},"postData":{"length":12,"type":"application/x-www-form-urlencoded","contents":"hello, world","name":"postData"}}%</pre>
URL參數出現在 e["parameter"]["sheet"] 。
既然我們了解事件對象的結構,我們就可以修改 doPost 為:
</div>
在URL中找到一個"sheet"參數
從POST請求中提取CSV數據
附加在CSV數據到指定表的所有行。
function doPost(e) { var contents = e.postData.contents; var sheetName = e.parameter['sheet'];// Append to spreadsheet appendLines(sheetName, contents);
var params = JSON.stringify(e); return ContentService.createTextOutput(params);
}</pre>
發布一個新版本的應用程序(“發布”>“部署為web應用程序…”,“項目版本”設置為“新”,描述它,并點擊“更新”)。現在您可以附加到電子表格行到一個HTTP Post請求!重新運行curl命令,并觀察最新的添加到您的電子表格行:
![]()
清除電子表格:刪除所有行除了標題。
更多關于 doGet 和 doPost 的資料: https://developers.google.com/apps-script/guides/web
更多關于 Event 對象的資料: https://developers.google.com/apps-script/guides/triggers/events
從Python中上傳數據
在實踐中,您可以使用任意語言發起POST請求上傳數據到物聯網平臺。本教程中,我使用一個python腳本創建了一些兩個房間每隔一個小時的隨機濕度和溫度數據并最終上傳電子表格。
本文由于篇幅有,且本節內容并不影響大局,筆者放棄了對本小節其余部分的翻譯,請讀者諒解。
第二部分:在R中獲取數據并通過ggplot制圖
本節,我們將討論:如何從網上下載CSV數據,處理日期字段,并使各種與ggplot2相關的細節。如果你閱讀教程時已經使用R且勾起了使用谷歌表數據的沖動這將是我們的榮幸,如果沒有,我希望本文將激勵你深入學習R!
推薦設置
如果你是一個經驗豐富的R用戶,你已經知道你喜歡開發環境可以使用你自己的IDE。如果不是,我建議用RStudio,它可以用于Linux,Mac和Windows多個平臺上。
RStudio是一個像樣的編輯器(還可以支持Vim模式)并且將Shiny集成其中。 下載地址
此外,我們還需要在R中安裝 ggplot2 包:
install.packages('ggplot2')或者直接在Rstudio中點擊 Install 按鈕,輸入ggplot2后安裝。
從web URL導入一個CSV文件
從網上抓取CSV數據很簡單:只要用 read.csv 帶上 url 參數,它會自動下載正確的數據,使數據幀頭的名字。
創建一個 helpers.R 文件,并編寫下面的代碼:
getRaw <- function () { data <- read.csv( url("https://script.google.com/macros/s/AKfycbxOw-Tl_r0jDV4wcnixdYdUjcNipSzgufiezRKr28Q5OAN50cIP/exec?sheet=Raw"), strip.white = TRUE ) data }我們將在稍后的 Shiny App 中用到 helpers.R 。 getRaw 函數將會返回一個數據庫格式的數據,包括5個變量,而表頭就和Google Sheets里面的電子表格的表頭一樣。
> data <- getRaw() > summary(data) timestamp date origin variable value Min. :1.448e+09 Mon Nov 23 2015 22:44:45 GMT+0100 (CET): 4 bedroom:120 humidity :120 Min. :23.00 1st Qu.:1.448e+09 Mon Nov 23 2015 23:44:45 GMT+0100 (CET): 4 kitchen:120 temperature:120 1st Qu.:23.88 Median :1.448e+09 Thu Nov 26 2015 00:44:45 GMT+0100 (CET): 4 Median :32.55 Mean :1.448e+09 Thu Nov 26 2015 01:44:45 GMT+0100 (CET): 4 Mean :34.34 3rd Qu.:1.448e+09 Thu Nov 26 2015 02:44:45 GMT+0100 (CET): 4 3rd Qu.:44.45 Max. :1.449e+09 Thu Nov 26 2015 03:44:45 GMT+0100 (CET): 4 Max. :49.90 (Other) :216 >轉化 date-time 列
現在讓我們把日期-時間數據轉化為適當的R date對象。最簡單的方法是將UNIX時間戳列轉換為POSIXct對象并取代“日期”列。自從我們生成UTC日期,我們將時區設置為“格林尼治時間”。完成 getRaw 函數:
getRaw <- function () { data <- read.csv( url("https://script.google.com/macros/s/AKfycbxOw-Tl_r0jDV4wcnixdYdUjcNipSzgufiezRKr28Q5OAN50cIP/exec?sheet=Raw"), strip.white = TRUE ) data$date = as.POSIXct(data$timestamp, tz="GMT", origin="1970-01-01") data }萬事俱備,只欠東風!
與 ggplot2 共舞
ggplot2包 提供了機智的 qplot 函數,一行代碼可以繪制種類繁多的圖表,使R成為我最喜歡的工具數據。
首先,我們畫上所有的值。
import(ggplot2) source("helpers.R") data <- getRaw() qplot(date, value, data = data)
qplot(date, value, data = data, colour = origin)我們可以根據傳感器的位置用顏色區分圖上的點:
![]()
我們仍然不能區分溫度和濕度,讓我們在不同的面板使用“facets”區分這些變量。我們將添加“free_y”選項,允許每個獨立y拉伸:
qplot(date, value, data = data, colour = origin) + facet_grid(variable ~ ., scales = "free_y")
![]()
因為有很多噪音,加入點線使圖表更容易閱讀:
qplot(date, value, data = data, colour = origin, geom = "line") + facet_grid(variable ~ ., scales = "free_y")
![]()
如果您添加一個傳感器在一個新的房間,一個新行顏色會自動出現在圖表上。如果你添加一個新的類型的數據(如“權力”功率計),第三個facets就會出現。試一試!
只是修改Python腳本發送數據并重新運行Python腳本,運行 data<-- getRaw() 這行,
并且最新 qplot 命令。
如果x軸上每個點的日期范圍似乎是錯的話,可以嘗試添加 scale_x_datetime :
qplot(date, value, data = data, colour = origin, geom = "line") + scale_x_datetime() + facet_grid(variable ~ ., scales = "free_y")為 Shiny 封裝畫圖函數
為了之后 Shiny 便于調用,我們就把兩個 qplot 封裝成一個函數。
我們的 helpers.R 文件現在看起來像這樣:
library(ggplot2)getRaw <- function () { data <- read.csv( url("
data$date = as.POSIXct(data$timestamp, tz="GMT", origin="1970-01-01") data }
timeseriesPlot <- function(data) { qplot(date, value, data = data, colour = origin, geom = "line") + scale_x_datetime() + facet_grid(variable ~ ., scales = "free_y") }
boxPlot <- function(data) { qplot(origin, value, data = data, geom = "boxplot") + facet_grid(variable ~ ., scales = "free_y") }</pre>
第三部分:與Shiny結合
在這部分, 我們將創建真正的在線儀表盤。Shiny 將 Web 服務器和一個可交互式的Web前端融合在一起,并嵌入 R 的代碼和圖表。我們將使用它制作一個基于 Web 的儀表盤,換句話說就是一個可以實時修改的傳感器數據展示網站。本節,我們將用 Google Sheets 上的數據建立一個簡單的頁面來展示時間序列和箱線圖。
設置 Shiny
我建議你按照 官方教程 最新的安裝說明安裝。如果你有時間,跟隨整個教程的完整概述了解 Shiny 的思維方式。
我們的第一個儀表板:沒有交互性
我們第一次儀表盤只會顯示兩個從 Google Sheets 提取最新數據的圖表。Shiny 應用由兩部分組成: ui.R 和 server.R 。 ui.R 決定了儀表盤的外觀:可視化的內容是什么以及如何排版。在這個文件中我們需要先規定好可視化的對象的名稱和類型,具體的可視化內容則在 server.R 規定。
如下的實例代碼定義了一個容器: shinyUI(fluidPage(....)) 。容器里面包含了一個垂直布局的盒子,將子類垂直堆疊。在布局內部,我們放置三個實體:一個標題欄和兩個畫圖實體。
# ui.RshinyUI(fluidPage( verticalLayout( titlePanel("Sensor data dashboard"), plotOutput("timeseries"), plotOutput("boxplot") )
))</pre>下一步是渲染圖形,在 server.R 中用 plotOutputs 選項控制顯示。
server.R 中的代碼在如下時刻會被調用:
</div>
當服務器啟動時;
每次訪問一個頁面時;
每次一個交互式的部件改變時。
在我們的例子中,當服務器啟動時,我們只希望導入輔助代碼但不執行(數據加載和繪制函數)。每次頁面加載的時候,我們想要獲取新數據并畫出情節。我們沒有小部件,因此,沒有第三類中的代碼。
這是服務器代碼的初稿,下面,就完成了這個任務:
shinyServer(...) 代碼塊之外的代碼只會被執行一次,而代碼塊里面的部分每次訪問頁面都會被執行。
這最后一塊讀取原始數據,
創建了一個時間序列圖并將它放入一個我們在 ui.R 里定義的名為“timeseries”的 plotOuput 中,然后渲染一個箱線圖放入名為“boxplot”的 plotOutput 中。
# server.Rsource("helpers.R")
shinyServer(function(input, output) {
Load data when app is visited
data <- getRaw()
Populate plots
output$timeseries <- renderPlot({ timeseriesPlot(data) })
output$boxplot <- renderPlot({ boxPlot(data) })
})</pre>
像上節提到的 helpers.R 文件一樣,在同一文件夾下創建 ui.R 和 server.R 文件后,RStudio 將自動檢測到這些文件是 Shiny 應用并且會在編輯器上方的工具欄中自動出現一個 “Run App” 的按鈕。
![]()
使用該按鈕來預覽應用程序,它應該彈出一個類似下面的窗口:
![]()
注意,我們的沒有對谷歌文檔自動檢測數據變化的功能,你仍然需要刷新頁面來獲得最新的數據。
添加一個按日期分類的網格布局和過濾器
現在讓我們添加一些日期選擇小部件,限制顯示的日期范圍。我們將首先從一個簡單的擴展布局垂直堆棧流體網格。遇到足夠寬的顯示,這種布局顯示網格。遇到窄的顯示器,它返回一個垂直堆棧,以避免橫向滾動。網格會以一個包含 column 元素的 fluidRow 元素。分配每一列的寬度(單位1?12屏幕的寬度)可以包含另一個小部件(一個輸入,一個圖表,或另一個網格)。新的 ui.R 文件下面會現在顯示嵌套的行和列的布局。
第二個修改是添加輸入小部件,輸入允許用戶通過輸入文本,數量,日期,選擇按鈕…完成可視化交互。你也可以在這里瀏覽完整的 素材庫 。
我們將使用兩種類型的輸入:
dateRangeInput :一個下拉日歷選擇開始和結束日期。
numericInput :一個只接受數字的輸入框,顯示了一個向上和向下箭頭來修改輸入值。
我們使用 dateRangeInput 指定日期范圍的數據繪制,和四個數字輸入指定開始一天的小時和分鐘,小時和分鐘的最后一天。
# ui.RshinyUI(fluidPage( verticalLayout( titlePanel("Sensor data dashboard"), fluidRow( column(3, dateRangeInput("dates", "Date Range", start="2015-11-20"), fluidRow( column(4, h3("From:")), column(4, numericInput("min.hours", "hours:", value=0)), column(4, numericInput("min.minutes", "minutes:", value=0)) ), fluidRow( column(4, h3("To:")), column(4, numericInput("max.hours", "hours:", value=23)), column(4, numericInput("max.minutes", "minutes:", value=59)) ) ), column(9, plotOutput("timeseries")) ), fluidRow( column(3), column(9, plotOutput("boxplot")) ) ) ))</pre>
通過 ShinyServers 回調的 input 參數,服務器端代碼可以獲取用戶從輸入部件的值。當前端的輸入有任何改變時,shiny將自動重新執行服務器上所有的 renderPlot 或者 reactive 的代碼塊來獲取新的輸入值。 reactive 是用來根據用戶輸入轉換數據的,我們將使用一個應用日期范圍過濾器。在下面新代碼示例的 reactive 代碼塊中轉換我們輸入的兩個POSIXct日期對象,然后使用它們作為日期的過濾條件來過濾數據框。
我們將 reactive 代碼塊分配給 data.filt 并且修改 renderPlot 調用繪制 data.filt() 而不是原始 data 。注意語法: data.filt 返回代碼塊的值傳遞給 reactive 。正如上面介紹的那樣,每一次的輸入都會引發 reactive 代碼塊的更新,而其他調用 data.filt() 的代碼塊也會跟著更新一次。在下面的例子中,兩個 renderPlot 代碼塊都會被重新執行以更新數據。
警告:確保包括括號每次你調用 data.filt() !
# server.Rsource("helpers.R")
shinyServer(function(input, output) {
Load data when app is visited
data <- getRaw()
Filter by device ID / time range when options are updated
data.filt <- reactive({ mindate <- as.POSIXct.Date(input$dates[1]) + (input$min.hours 60 + input$min.minutes) 60 maxdate <- as.POSIXct.Date(input$dates[2]) + (input$max.hours 60 + input$max.minutes) 60
subset(data, date > mindate & date < maxdate)
})
Populate plots
output$timeseries <- renderPlot({ timeseriesPlot(data.filt()) })
output$boxplot <- renderPlot({ boxPlot(data.filt()) })
})</pre>
再次運行應用程序。新結果應該類似于下面我的例子:
![]()
發布到Shinyapps.io
ShinyApps.io 為 Shiny 應用提供一個托管服務,而這個擴管服務同時也是 RStudio 的另一個產品。他們的開發者在 RStudio 中集成了這個服務:簡單地在你 Shiny 應用的右上角點擊"發布",跟著指引,創建一個免費的賬號然后上傳你的儀表盤。
![]()
![]()
一旦發布應用程序,您將注意到一個問題是:在儀表板上沒有任何顯示。如果你檢查服務器日志,你會注意到從 R 訪問 Google Sheets 的 HTTPS 請求失敗。下面,我們的教程的最后一節將解釋如何解決。
不幸的是, ShinyApps (在撰寫本文時)不支持https url,從而阻礙了請求 Google Sheets。為了解決這一問題,我們需要通過外部路由請求服務器(代理),可以通過HTTP、HTTPS可以獲取目標頁面,并通過HTTP請求回到R的 read.csv 返回相應的值。這樣做不利于發揮安全的HTTPS的優勢,但由于我們是在做一項公共儀表板數據公開在谷歌,而且大多僅供演示,我不關心它。
保存你的工作,我建立了一個HTTPS-to-HTTP代理服務器。在 server.R 代碼,在 script.google.com/之前添加shinyproxy.appspot.com/。在我的例子中,網址是:
# fragment of helpers.Rdata <- read.csv( url("再次發布儀表板,你成功了!
更多的代理
HTTPS代理是一個駐留在應用程序引擎的簡單代碼。 相關代碼
原文地址 http://douglas-watson.github.io/post/gdocs_0_intro/
本文由用戶 pcuj0331 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!