原文:
annas-archive.org/md5/3ff53e35b37e2f50c639bfc6fc052f29
译者:飞龙
第十章:实时数据
本章将涵盖以下示例:
-
为实时情感分析流式传输 Twitter 数据
-
读取 IRC 聊天室消息
-
响应 IRC 消息
-
向 Web 服务器轮询以获取最新更新
-
检测实时文件目录变化
-
通过套接字进行实时通信
-
通过摄像头流检测面部和眼睛
-
用于模板匹配的摄像头流
介绍
首先收集数据然后再分析它是相当容易的。然而,有些任务可能需要将这两个步骤结合在一起。实时分析接收到的数据是本章的核心内容。我们将讨论如何管理来自 Twitter 推文、Internet Relay Chat(IRC)、Web 服务器、文件变更通知、套接字和网络摄像头的实时数据输入。
前三个示例将重点处理来自 Twitter 的实时数据。这些主题将包括用户发布的内容以及与关键词相关的帖子。
接下来,我们将使用两个独立的库与 IRC 服务器进行交互。第一个示例将展示如何加入一个 IRC 聊天室并开始监听消息,接下来的示例将展示我们如何在 IRC 服务器上监听直接消息。
如果不支持实时数据,常见的解决方法是频繁查询该数据。这一过程称为 轮询,我们将在某个示例中学习如何快速轮询 Web 服务器。
我们还将在文件目录中检测到文件被修改、删除或创建的变化。可以想象在 Haskell 中实现 Dropbox、OneDrive 或 Google Drive。
最后,我们将创建一个简单的服务器-客户端交互,使用套接字并操作实时网络摄像头流。
为实时情感分析流式传输 Twitter 数据
Twitter 上充满了每秒钟涌现的内容。开始调查实时数据的一个好方法是检查推文。
本示例将展示如何编写代码来响应与特定搜索查询相关的推文。我们使用外部 Web 端点来确定情感是积极、消极还是中立。
准备工作
安装 twitter-conduit
包:
$ cabal install twitter-conduit
为了解析 JSON,我们使用 yocto
:
$ cabal install yocto
如何操作…
按照以下步骤设置 Twitter 凭证并开始编码:
-
通过访问
apps.twitter.com
创建一个新的 Twitter 应用。 -
从此 Twitter 应用管理页面找到 OAuth 消费者密钥和 OAuth 消费者密钥。分别为
OAUTH_CONSUMER_KEY
和OAUTH_CONSUMER_SECRET
设置系统环境变量。大多数支持 sh 兼容 shell 的 Unix 系统支持export
命令:$ export OAUTH_CONSUMER_KEY="Your OAuth Consumer Key" $ export OAUTH_CONSUMER_SECRET="Your OAuth Consumer Secret"
-
此外,通过相同的 Twitter 应用管理页面找到 OAuth 访问令牌和 OAuth 访问密钥,并相应地设置环境变量:
$ export OAUTH_ACCESS_TOKEN="Your OAuth Access Token" $ export OAUTH_ACCESS_SECRET="Your OAuth Access Secret"
小贴士
我们将密钥、令牌和秘密 PIN 存储在环境变量中,而不是直接将它们硬编码到程序中,因为这些变量与密码一样重要。就像密码永远不应公开可见,我们尽力将这些令牌和密钥保持在源代码之外。
-
从
twitter-conduit
包的示例目录中下载Common.hs
文件,路径为github.com/himura/twitter-conduit/tree/master/sample
。研究userstream.hs
示例文件。 -
首先,我们导入所有相关的库:
{-# LANGUAGE OverloadedStrings #-} import qualified Data.Conduit as C import qualified Data.Conduit.List as CL import qualified Data.Text.IO as T import qualified Data.Text as T import Control.Monad.IO.Class (liftIO) import Network.HTTP (getResponseBody, getRequest, simpleHTTP, urlEncode) import Text.JSON.Yocto import Web.Twitter.Conduit (stream, statusesFilterByTrack) import Common import Control.Lens ((^!), (^.), act) import Data.Map ((!)) import Data.List (isInfixOf, or) import Web.Twitter.Types
-
在
main
中,运行我们的实时情感分析器以进行搜索查询:main :: IO () main = do let query = "haskell" T.putStrLn $ T.concat [ "Streaming Tweets that match \"" , query, "\"..."] analyze query
-
使用
Common
模块提供的runTwitterFromEnv'
函数,通过我们的 Twitter API 凭证连接到 Twitter 的实时流。我们将使用一些非常规的语法,如$$+-
或^!
。请不要被它们吓到,它们主要用于简洁表达。每当触发事件时,例如新的推文或新的关注,我们将调用我们的process
函数进行处理:analyze :: T.Text -> IO () analyze query = runTwitterFromEnv' $ do src <- stream $ statusesFilterByTrack query src C.$$+- CL.mapM_ (^! act (liftIO . process))
-
一旦我们获得事件触发的输入,就会运行
process
以获取输出,例如发现文本的情感。在本示例中,我们将情感输出附加到逗号分隔文件中:process :: StreamingAPI -> IO () process (SStatus s) = do let theUser = userScreenName $ statusUser s let theTweet = statusText s T.putStrLn $ T.concat [theUser, ": ", theTweet] val <- sentiment $ T.unpack theTweet let record = (T.unpack theUser) ++ "," ++ (show.fromRational) val ++ "\n" appendFile "output.csv" record print val
-
如果事件触发的输入不是推文,而是朋友关系事件或其他内容,则不执行任何操作:
process s = return ()
-
定义一个辅助函数,通过移除所有
@user
提及、#hashtags
或http://websites
来清理输入:clean :: String -> String clean str = unwords $ filter (\w -> not (or [ isInfixOf "@" w , isInfixOf "#" w , isInfixOf "http://" w ])) (words str)
-
使用外部 API 对文本内容进行情感分析。在本示例中,我们使用 Sentiment140 API,因为它简单易用。更多信息请参考
help.sentiment140.com/api
。为了防止被限制访问,也请提供appid
参数,并附上电子邮件地址或获取商业许可证:sentiment :: String -> IO Rational sentiment str = do let baseURL = "http://www.sentiment140.com/api/classify?text=" resp <- simpleHTTP $ getRequest $ baseURL ++ (urlEncode.clean) str body <- getResponseBody resp let p = polarity (decode body) / 4.0 return p
-
从我们的 API 的 JSON 响应中提取情感值:
polarity :: Value -> Rational polarity (Object m) = polarity' $ m ! "results" where polarity' (Object m) = fromNumber $ m ! "polarity" fromNumber (Number n) = n polarity _ = -1
-
运行代码,查看推文在全球任何人公开发布时即刻显示。情感值将是介于 0 和 1 之间的有理数,其中 0 表示负面情感,1 表示正面情感:
$ runhaskell Main.hs Streaming Tweets that match "x-men"…
查看以下输出:
我们还可以从output.csv
文件中批量分析数据。以下是情感分析的可视化表现:
它是如何工作的…
Twitter-conduit 包使用了来自原始包的 conduit 设计模式,原始包位于hackage.haskell.org/package/conduit
。conduit 文档中指出:
Conduit 是解决流数据问题的方案,允许在恒定内存中进行数据流的生产、转换和消费。它是惰性 I/O 的替代方案,保证了确定性的资源处理,并且与枚举器/迭代器和管道处于相同的通用解决方案空间中。
为了与 Twitter 的应用程序编程接口(API)进行交互,必须获得访问令牌和应用程序密钥。我们将这些值存储在环境变量中,并让 Haskell 代码从中检索。
Common.hs
文件负责处理单调的认证代码,应该保持不变。
反应每个 Twitter 事件的函数是process
。我们可以修改process
以满足我们特定的需求。更具体地说,我们可以修改情感分析函数,以使用不同的sentiment
分析服务。
还有更多内容…
我们的代码监听任何与我们查询匹配的推文。这个 Twitter-conduit 库还支持另外两种实时流:statusesFilterByFollow
和userstream
。前者获取指定用户列表的所有推文,后者获取该账户关注的用户的所有推文。
例如,通过将statusesFilterByTrack
查询替换为一些 Twitter 用户的 UID 来修改我们的代码:
analyze:: IO ()
analyze = runTwitterFromEnv' $ do
src <- statusesFilterByFollow [ 103285804, 450331119
, 64895420]
src C.$$+- CL.mapM_ (^! act (liftIO . process))
此外,为了仅获取我们关注的用户的推文,我们可以通过将statusesFilterByTrack
查询替换为userstream
来修改我们的代码:
analyze :: IO ()
analyze = runTwitterFromEnv' $ do
src <- stream userstream
src C.$$+- CL.mapM_ (^! act (liftIO . process))
通过github.com/himura/twitter-conduit/tree/master/sample
可以找到更多示例。
阅读 IRC 聊天室消息
Internet Relay Chat(IRC)是最古老且最活跃的群聊服务之一。Haskell 社区在 Freenode IRC 服务器(irc.freenode.org
)的#haskell
频道中拥有非常友好的存在。
在这个配方中,我们将构建一个 IRC 机器人,加入一个聊天室并监听文本对话。我们的程序将模拟一个 IRC 客户端,并连接到现有的 IRC 服务器之一。这个配方完全不需要外部库。
做好准备
确保启用互联网连接。
要测试 IRC 机器人,最好安装一个 IRC 客户端。例如,顶级的 IRC 客户端之一是Hexchat,可以从hexchat.github.io
下载。对于基于终端的 IRC 客户端,Irssi是最受欢迎的:www.irssi.org
。
在 Haskell wiki 上查看自己动手制作 IRC 机器人文章:www.haskell.org/haskellwiki/Roll_your_own_IRC_bot
。这个配方的代码大多基于 wiki 上的内容。
如何做…
在一个名为Main.hs
的新文件中,插入以下代码:
-
导入相关的包:
import Network import Control.Monad (forever) import System.IO import Text.Printf
-
指定 IRC 服务器的具体信息:
server = "irc.freenode.org" port = 6667 chan = "#haskelldata" nick = "awesome-bot"
-
连接到服务器并监听聊天室中的所有文本:
main = do h <- connectTo server (PortNumber (fromIntegral port)) hSetBuffering h NoBuffering write h "NICK" nick write h "USER" (nick++" 0 * :tutorial bot") write h "JOIN" chan listen h write :: Handle -> String -> String -> IO () write h s t = do hPrintf h "%s %s\r\n" s t printf "> %s %s\n" s t
-
定义我们的监听器。对于这个配方,我们将仅将所有事件回显到控制台:
listen :: Handle -> IO () listen h = forever $ do s <- hGetLine h putStrLn s
另见
要了解另一种与 IRC 交互的方式,请查看下一个配方,回应 IRC 消息。
回应 IRC 消息
另一种与 IRC 交互的方式是使用Network.SimpleIRC
包。此包封装了许多底层网络操作,并提供了有用的 IRC 接口。
在本教程中,我们将回应频道中的消息。如果有用户输入触发词,在本案例中为“host?”,我们将回复该用户其主机地址。
准备工作
安装Network.SimpleIRC
包:
$ cabal install simpleirc
要测试 IRC 机器人,安装 IRC 客户端会很有帮助。一个不错的 IRC 客户端是 Hexchat,可以从hexchat.github.io
下载。对于基于终端的 IRC 客户端,Irssi 是最好的之一:www.irssi.org
。
如何操作…
创建一个新的文件,我们称之为Main.hs
,并执行以下操作:
-
导入相关的库:
{-# LANGUAGE OverloadedStrings #-} import Network.SimpleIRC import Data.Maybe import qualified Data.ByteString.Char8 as B
-
创建接收到消息时的事件处理程序。如果消息是“host?”,则回复用户其主机信息:
onMessage :: EventFunc onMessage s m = do case msg of "host?" -> sendMsg s chan $ botMsg otherwise -> return () where chan = fromJust $ mChan m msg = mMsg m host = case mHost m of Just h -> h Nothing -> "unknown" nick = case mNick m of Just n -> n Nothing -> "unknown user" botMsg = B.concat [ "Hi ", nick, " , your host is ", host]
-
定义要监听的事件:
events = [(Privmsg onMessage)]
-
设置 IRC 服务器配置。连接到任意一组频道,并绑定我们的事件:
freenode = (mkDefaultConfig "irc.freenode.net" "awesome-bot") { cChannels = ["#haskelldata"] , cEvents = events }
-
连接到服务器。不要在新线程中运行,而是打印调试信息,按照相应的布尔参数来指定:
main = connect freenode False True
-
运行代码,打开 IRC 客户端进行测试:https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/hskl-da-cb/img/6331OS_10_03.jpg
另见
若要在不使用外部库的情况下连接 IRC 服务器,请参见之前的教程,读取 IRC 聊天室消息。
轮询 web 服务器以获取最新更新
一些网站的变化非常频繁。例如,Google 新闻和 Reddit 通常在我们刷新页面时,立刻加载最新的帖子。为了随时保持最新数据,最好是频繁地发送 HTTP 请求。
在本教程中,我们每 10 秒轮询一次新的 Reddit 帖子,如下图所示:
如何操作…
在一个名为Main.hs
的新文件中,执行以下步骤:
-
导入相关的库:
import Network.HTTP import Control.Concurrent (threadDelay) import qualified Data.Text as T
-
定义要轮询的 URL:
url = "http://www.reddit.com/r/pics/new.json"
-
定义一个函数来获取最新的 HTTP GET 请求数据:
latest :: IO String latest = simpleHTTP (getRequest url) >>= getResponseBody
-
轮询实际上是等待指定时间后递归地执行任务。在这种情况下,我们会等 10 秒钟再请求最新的网页数据:
poll :: IO a poll = do body <- latest print $ doWork body threadDelay (10 * 10⁶) poll
-
运行轮询:
main :: IO a main = do putStrLn $ "Polling " ++ url ++ " …" poll
-
每次 Web 请求后,分析数据。在本教程中,统计 Imgur 出现的次数:
doWork str = length $ T.breakOnAll (T.pack "imgur.com/") (T.pack str)
检测实时文件目录变化
在本教程中,我们将实时检测文件是否被创建、修改或删除。类似于流行的文件同步软件 Dropbox,我们每次遇到这样的事件时,都会执行一些有趣的操作。
准备工作
安装fsnotify
包:
$ cabal install fsnotify
如何操作…
在一个名为Main.hs
的新文件中,执行以下步骤:
-
导入相关的库:
{-# LANGUAGE OverloadedStrings #-} import Filesystem.Path.CurrentOS import System.FSNotify import Filesystem import Filesystem.Path (filename)
-
在当前目录上运行文件监视器:
main :: IO () main = do wd <- getWorkingDirectory print wd man <- startManager watchTree man wd (const True) doWork putStrLn "press return to stop" getLine putStrLn "watching stopped, press return to exit" stopManager man getLine return ()
-
处理每个文件变化事件。在本教程中,我们仅将操作输出到控制台:
doWork :: Event -> IO () doWork (Added filepath time) = putStrLn $ (show $ filename filepath) ++ " added" doWork (Modified filepath time) = putStrLn $ (show $ filename filepath) ++ " modified" doWork (Removed filepath time) = putStrLn $ (show $ filename filepath) ++ " removed"
-
运行代码并开始修改同一目录中的一些文件。例如,创建一个新文件,编辑它,然后删除它:
$ runhaskell Main.hs press return to stop FilePath "hello.txt" added FilePath "hello.txt" modified FilePath "hello.txt" removed
它是如何工作的…
fsnotify
库绑定到特定平台文件系统的事件通知服务。在基于 Unix 的系统中,这通常是inotify
(dell9.ma.utexas.edu/cgi-bin/man-cgi?inotify
)。
通过套接字实时通信
套接字提供了一种方便的实时程序间通信方式。可以把它们想象成一个聊天客户端。
在这个教程中,我们将从一个程序向另一个程序发送消息并获取响应。
如何做…
将以下代码插入到名为Main.hs
的新文件中:
-
创建服务器代码:
import Network ( listenOn, withSocketsDo, accept , PortID(..), Socket ) import System.Environment (getArgs) import System.IO ( hSetBuffering, hGetLine, hPutStrLn , BufferMode(..), Handle ) import Control.Concurrent (forkIO)
-
创建一个套接字连接以进行监听,并在其上附加我们的处理程序
sockHandler
:main :: IO () main = withSocketsDo $ do let port = PortNumber 9001 sock <- listenOn port putStrLn $ "Listening…" sockHandler sock
-
定义处理每个接收到的消息的处理程序:
sockHandler :: Socket -> IO () sockHandler sock = do (h, _, _) <- accept sock putStrLn "Connected!" hSetBuffering h LineBuffering forkIO $ process h forkIO $ respond h sockHandler sock
-
定义如何处理客户端发送的消息:
process :: Handle -> IO () process h = do line <- hGetLine h print line process h
-
通过用户输入向客户端发送消息:
respond h = withSocketsDo $ do txt <- getLine hPutStrLn h txt respond h
-
现在,在一个新文件
client.hs
中创建客户端代码。首先,导入库:import Network (connectTo, withSocketsDo, PortID(..)) import System.Environment (getArgs) import System.IO ( hSetBuffering, hPutStrLn , hGetLine, BufferMode(..) )
-
将客户端连接到相应的端口,并设置响应者和监听线程:
main = withSocketsDo $ do let port = PortNumber 9001 h <- connectTo "localhost" port putStrLn $ "Connected!" hSetBuffering h LineBuffering forkIO $ respond h forkIO $ process h loop
-
获取用户输入并将其作为消息发送:
respond h = do txt <- getLine hPutStrLn h txt respond h
-
监听来自服务器的传入消息:
process h = do line <- hGetLine h print line process h
-
先运行服务器,测试代码:
$ runhaskell Main.hs
-
接下来,在一个单独的终端中运行客户端:
$ runhaskell client.hs
-
现在,我们可以通过键入并按下Enter键在两者之间发送消息:
Hello? "yup, I can hear you!"
它是如何工作的…
hGetLine
函数会阻塞代码执行,这意味着代码执行在此处暂停,直到接收到消息为止。这允许我们等待消息并进行实时反应。
我们首先在计算机上指定一个端口,这只是一个尚未被其他程序占用的数字。服务器设置套接字,客户端连接到它,而无需进行设置。两者之间传递的消息是实时发生的。
以下图示演示了服务器-客户端模型的可视化:
通过摄像头流检测人脸和眼睛
摄像头是另一个实时数据的来源。随着帧的进出,我们可以使用 OpenCV 库进行强大的分析。
在这个教程中,我们通过实时摄像头流进行人脸检测。
准备工作
安装 OpenCV、SDL 和 FTGL 库以进行图像处理和计算机视觉:
sudo apt-get install libopencv-dev libsdl1.2-dev ftgl-dev
使用 cabal 安装 OpenCV 库:
cabal install cv-combinators
如何做…
创建一个新的源文件Main.hs
,并按照以下步骤操作:
-
导入相关库:
import AI.CV.ImageProcessors import qualified AI.CV.OpenCV.CV as CV import qualified Control.Processor as Processor import Control.Processor ((--<)) import AI.CV.OpenCV.Types (PImage) import AI.CV.OpenCV.CxCore (CvRect(..), CvSize(..)) import Prelude hiding (id) import Control.Arrow ((&&&), (***)) import Control.Category ((>>>), id)
-
定义摄像头流的来源。我们将使用内置的摄像头。若要改用视频,可以将
camera 0
替换为videoFile "./myVideo.mpeg"
:captureDev :: ImageSource captureDev = camera 0
-
缩小流的大小以提高性能:
resizer :: ImageProcessor resizer = resize 320 240 CV.CV_INTER_LINEAR
-
使用 OpenCV 提供的训练数据集检测图像中的人脸:
faceDetect :: Processor.IOProcessor PImage [CvRect] faceDetect = haarDetect "/usr/share/opencv/haarcascades/haarcascade_frontalface_alt.xml" 1.1 3 CV.cvHaarFlagNone (CvSize 20 20)
-
使用 OpenCV 提供的训练数据集检测图像中的眼睛:
eyeDetect :: Processor.IOProcessor PImage [CvRect] eyeDetect = haarDetect "/usr/share/opencv/haarcascades/haarcascade_eye.xml" 1.1 3 CV.cvHaarFlagNone (CvSize 20 20)
-
在人脸和眼睛周围画矩形框:
faceRects = (id &&& faceDetect) >>> drawRects eyeRects = (id &&& eyeDetect) >>> drawRects
-
捕获摄像头流,检测面部和眼睛,绘制矩形,并在两个不同的窗口中显示它们:
start = captureDev >>> resizer --< (faceRects *** eyeRects) >>> (window 0 *** window 1)
-
执行实时摄像头流并在按下某个键后停止:
main :: IO () main = runTillKeyPressed start
-
运行代码并查看网络摄像头,以检测面部和眼睛,结果如以下命令后的截图所示:
$ runhaskell Main.hs
它是如何工作的…
为了检测面部、眼睛或其他物体,我们使用haarDetect
函数,它执行从许多正面和负面测试案例中训练出来的分类器。这些测试案例由 OpenCV 提供,通常位于 Unix 系统中的/usr/share/opencv/haarcascades/
目录下。
cv-combinator 库提供了 OpenCV 底层操作的便捷抽象。为了运行任何有用的代码,我们必须定义一个源、一个过程和一个最终目标(也称为sink)。在我们的案例中,源是机器内置的摄像头。我们首先将图像调整为更易处理的大小(resizer
),然后将流分成两个并行流(--<
),在一个流中绘制面部框,在另一个流中绘制眼睛框,最后将这两个流输出到两个独立的窗口。有关 cv-combinators 包的更多文档,请参见hackage.haskell.org/package/cv-combinators
。
摄像头流的模板匹配
模板匹配是一种机器学习技术,用于寻找与给定模板图像匹配的图像区域。我们将把模板匹配应用于实时视频流的每一帧,以定位图像。
准备工作
安装 OpenCV 和 c2hs 工具包:
$ sudo apt-get install c2hs libopencv-dev
从 cabal 安装 CV 库。确保根据安装的 OpenCV 版本包含–fopencv24
或–fopencv23
参数:
$ cabal install CV -fopencv24
同时,创建一个小的模板图像。在这个实例中,我们使用的是 Lena 的图像,这个图像通常用于许多图像处理实验。我们将此图像文件命名为lena.png
:
如何操作…
在一个新的文件Main.hs
中,从以下步骤开始:
-
导入相关库:
{-#LANGUAGE ScopedTypeVariables#-} module Main where import CV.Image (loadImage, rgbToGray, getSize) import CV.Video (captureFromCam, streamFromVideo) import Utils.Stream (runStream_, takeWhileS, sideEffect) import CV.HighGUI (showImage, waitKey) import CV.TemplateMatching ( simpleTemplateMatch , MatchType(..) ) import CV.ImageOp ((<#)) import CV.Drawing (circleOp, ShapeStyle(..))
-
加载模板图像并开始对摄像头流进行模板匹配:
main = do Just t <- loadImage "lena.jpg" Just c <- captureFromCam 0 runStream_ . sideEffect (process t) . takeWhileS (\_ -> True) $ streamFromVideo c
-
对摄像头流的每一帧执行操作。具体来说,使用模板匹配来定位模板并围绕其绘制一个圆圈:
process t img = do let gray = rgbToGray img let ((mx, my), _) = simpleTemplateMatch CCOEFF_NORMED gray t let circleSize = (fst (getSize t)) `div` 2 let circleCenter = (mx + circleSize, my + circleSize) showImage "test" (img <# circleOp (0,0,0) circleCenter circleSize (Stroked 3)) waitKey 100 return ()
-
使用以下命令运行代码并显示模板图像。会在找到的图像周围绘制一个黑色圆圈:
$ runhaskell Main.hs
还有更多内容……
更多 OpenCV 示例可以在github.com/aleator/CV/tree/master/examples
找到。
第十一章 数据可视化
在本章中,我们将介绍以下可视化技术:
-
使用 Google 的 Chart API 绘制折线图
-
使用 Google 的 Chart API 绘制饼图
-
使用 Google 的 Chart API 绘制条形图
-
使用 gnuplot 显示折线图
-
显示二维点的散点图
-
与三维空间中的点进行交互
-
可视化图形网络
-
自定义图形网络图的外观
-
使用 D3.js 在 JavaScript 中渲染条形图
-
使用 D3.js 在 JavaScript 中渲染散点图
-
从向量列表中绘制路径图
引言
可视化在数据分析的所有步骤中都非常重要。无论我们是刚开始接触数据,还是已经完成了分析,通过图形辅助工具直观地理解数据总是非常有用的。幸运的是,Haskell 提供了许多库来帮助实现这一目标。
在本章中,我们将介绍使用各种 API 绘制折线图、饼图、条形图和散点图的技巧。除了常见的数据可视化,我们还将学习如何绘制网络图。此外,在最后一个技巧中,我们将通过在空白画布上绘制向量来描述导航方向。
使用 Google 的 Chart API 绘制折线图
我们将使用方便的 Google Chart API (developers.google.com/chart
) 来渲染折线图。该 API 会生成指向图表 PNG 图像的 URL。这个轻量级的 URL 比实际的图像更易于处理。
我们的数据将来自一个文本文件,其中包含按行分隔的数字列表。代码将生成一个 URL 来展示这些数据。
准备工作
按如下方式安装 GoogleChart
包:
$ cabal install hs-gchart
创建一个名为 input.txt
的文件,并按如下方式逐行插入数字:
$ cat input.txt
2
5
3
7
4
1
19
18
17
14
15
16
如何实现…
-
按如下方式导入 Google Chart API 库:
import Graphics.Google.Chart
-
从文本文件中获取输入,并将其解析为整数列表:
main = do rawInput <- readFile "input.txt" let nums = map (read :: String -> Int) (lines rawInput)
-
通过适当设置属性,创建一个图表 URL,如以下代码片段所示:
putStrLn $ chartURL $ setSize 500 200 $ setTitle "Example of Plotting a Chart in Haskell" $ setData (encodeDataSimple [nums]) $ setLegend ["Stock Price"] $ newLineChart
-
运行程序将输出一个 Google Chart URL,如下所示:
$ runhaskell Main.hs http://chart.apis.google.com/chart?chs=500x200&chtt=Example+of+Plotting+a+Chart+in+Haskell&chd=s:CFDHEBTSROPQ&chdl=Stock+Price&cht=lc
确保网络连接正常,并导航到该 URL 查看图表,如下图所示:
工作原理…
Google 会将所有图表数据编码到 URL 中。我们的图表越复杂,Google 图表 URL 就越长。在这个技巧中,我们使用 encodeDataSimple
函数,它创建了一个相对较短的 URL,但只接受 0 到 61 之间的整数(包括 0 和 61)。
还有更多内容…
为了可视化一个更详细的图表,允许数据具有小数位数,我们可以使用 encodeDataText :: RealFrac a => [[a]] -> ChartData
函数。这将允许 0 到 100 之间的十进制数(包含 0 和 100)。
为了在图表中表示更大的整数范围,我们应使用 encodeDataExtended
函数,它支持 0 到 4095 之间的整数(包括 0 和 4095)。
关于 Google Charts Haskell 包的更多信息,请访问 hackage.haskell.org/package/hs-gchart
。
另请参见
此配方需要连接互联网以查看图表。如果我们希望在本地执行所有操作,请参考 使用 gnuplot 显示折线图 配方。其他 Google API 配方包括 使用 Google 的 Chart API 绘制饼图 和 使用 Google 的 Chart API 绘制条形图。
使用 Google 的 Chart API 绘制饼图
Google Chart API 提供了一个外观非常优雅的饼图界面。通过正确地输入数据和标签,我们可以生成设计精良的饼图,如本配方所示。
准备工作
按如下方式安装 GoogleChart 包:
$ cabal install hs-gchart
创建一个名为 input.txt
的文件,每行插入数字,格式如下:
$ cat input.txt
2
5
3
7
4
1
19
18
17
14
15
16
如何操作…
-
按如下方式导入 Google Chart API 库:
import Graphics.Google.Chart
-
从文本文件中收集输入并将其解析为整数列表,如以下代码片段所示:
main = do rawInput <- readFile "input.txt" let nums = map (read :: String -> Int) (lines rawInput)
-
从以下代码中显示的饼图属性中打印出 Google Chart URL:
putStrLn $ chartURL $ setSize 500 400 $ setTitle "Example of Plotting a Pie Chart in Haskell" $ setData (encodeDataSimple [nums]) $ setLabels (lines rawInput) $ newPieChart Pie2D
-
运行程序将输出如下的 Google Chart URL:
$ runhaskell Main.hs http://chart.apis.google.com/chart?chs=500x400&chtt=Example+of+Plotting+a+Pie+Chart+in+Haskell&chd=s:CFDHEBTSROPQ&chl=2|5|3|7|4|1|19|18|17|14|15|16&cht=p
确保有网络连接,并访问该网址以查看下图所示的图表:
如何运作…
Google 将所有图表数据编码在 URL 中。图表越复杂,Google Chart URL 越长。在此配方中,我们使用 encodeDataSimple
函数,它创建一个相对较短的 URL,但仅接受 0 到 61(包括 0 和 61)之间的整数。饼图的图例由 setLabels :: [String] -> PieChart -> PieChart
函数按照与数据相同的顺序指定。
还有更多…
为了可视化一个包含小数的更详细的图表,我们可以使用 encodeDataText :: RealFrac a => [[a]] -> ChartData
函数。该函数支持 0 到 100(包括 0 和 100)之间的小数。
为了在图表中表示更大的整数范围,我们应使用 encodeDataExtended
函数,该函数支持 0 到 4095(包括 0 和 4095)之间的整数。
关于 Google Charts Haskell 包的更多信息,请访问 hackage.haskell.org/package/hs-gchart
。
另请参见
-
使用 Google 的 Chart API 绘制折线图
-
使用 Google 的 Chart API 绘制条形图
使用 Google 的 Chart API 绘制条形图
Google Chart API 也很好地支持条形图。在本配方中,我们将生成包含两组输入数据的条形图,以展示该 API 的实用性。
准备工作
按如下方式安装 GoogleChart
包:
$ cabal install hs-gchart
创建两个文件,名为 input1.txt
和 input2.txt
,每行插入数字,格式如下:
$ cat input1.txt
2
5
3
7
4
1
19
18
17
14
15
16
$ cat input2.txt
4
2
6
7
8
2
18
17
16
17
15
14
如何操作…
-
按如下方式导入 Google Chart API 库:
import Graphics.Google.Chart
-
从两个文本文件中获取两个输入值,并将它们解析为两个独立的整数列表,如以下代码片段所示:
main = do rawInput1 <- readFile "input1.txt" rawInput2 <- readFile "input2.txt" let nums1 = map (read :: String -> Int) (lines rawInput1) let nums2 = map (read :: String -> Int) (lines rawInput2)
-
同样设置柱状图并打印出 Google Chart URL,如下所示:
putStrLn $ chartURL $ setSize 500 400 $ setTitle "Example of Plotting a Bar Chart in Haskell" $ setDataColors ["00ff00", "ff0000"] $ setLegend ["A", "B"] $ setData (encodeDataSimple [nums1, nums2]) $ newBarChart Horizontal Grouped
-
运行程序将输出一个 Google Chart URL,如下所示:
$ runhaskell Main.hs http://chart.apis.google.com/chart?chs=500x400&chtt=Example+of+Plotting+a+Bar+Chart+in+Haskell&chco=00ff00,ff0000&chdl=A|B&chd=s:CFDHEBTSROPQ,ECGHICSRQRPO&cht=bhg
确保存在互联网连接并导航到该 URL 以查看以下图表:
如何实现…
Google 将所有图表数据编码在 URL 中。图表越复杂,Google Chart URL 就越长。在本教程中,我们使用encodeDataSimple
函数,它创建了一个相对较短的 URL,但仅接受 0 到 61 之间的整数。
还有更多…
若要可视化更详细的图表并允许数据具有小数位,我们可以改用 encodeDataText :: RealFrac a => [[a]] -> ChartData
函数。该函数允许介于 0 和 100 之间的小数。
若要在图表中表示更大的整数范围,我们应使用 encodeDataExtended
函数,该函数支持介于 0 和 4095 之间的整数。
关于 Google Charts Haskell 包的更多信息,请访问 hackage.haskell.org/package/hs-gchart
。
另见
若要使用其他 Google Chart 工具,请参考使用 Google Chart API 绘制饼图和使用 Google Chart API 绘制折线图的教程。
使用 gnuplot 显示折线图
绘制图表通常不需要互联网连接。因此,在本教程中,我们将展示如何在本地绘制折线图。
准备工作
本教程使用的库通过 gnuplot 渲染图表。我们应首先安装 gnuplot。
在基于 Debian 的系统(如 Ubuntu)上,我们可以使用 apt-get
安装,如下所示:
$ sudo apt-get install gnuplot-x11
gnuplot 的官方下载地址是其官方网站 www.gnuplot.info
。
安装 gnuplot 后,使用 cabal 安装 EasyPlot
Haskell 库,如下所示:
$ cabal install easyplot
如何实现…
-
按照以下方式导入
EasyPlot
库:import Graphics.EasyPlot
-
定义一个数字列表进行绘图,如下所示:
main = do let values = [4,5,16,15,14,13,13,17]
-
如以下代码片段所示,在
X11
窗口上绘制图表。X11
X Window 系统终端被许多基于 Linux 的机器使用。如果在 Windows 上运行,我们应使用Windows
终端。在 Mac OS X 上,我们应将X11
替换为Aqua
:plot X11 $ Data2D [ Title "Line Graph" , Style Linespoints , Color Blue] [] (zip [1..] values)
运行代码将生成一个 plot1.dat
数据文件,并从选定的终端显示可视化图表,如下图所示:
如何实现…
EasyPlot
库将所有用户指定的代码转换为 gnuplot 可理解的语言,用于绘制数据图表。
另见
若要使用 Google Chart API 而不是 easy plot,请参考使用 Google Chart API 绘制折线图的教程。
显示二维点的散点图
本教程介绍了一种快速简单的方法,可以将 2D 点列表可视化为图像中的散点。
准备工作
本食谱中使用的库通过 gnuplot 来渲染图表。我们应先安装 gnuplot。
在基于 Debian 的系统(如 Ubuntu)上,我们可以使用 apt-get
安装,方法如下:
$ sudo apt-get install gnuplot-x11
下载 gnuplot 的官方网站是 www.gnuplot.info
。
在设置好 gnuplot 后,使用 cabal 安装 easyplot
Haskell 库,如下所示:
$ cabal install easyplot
同样,安装一个辅助的 CSV 包,如下所示:
$ cabal install csv
同样,创建两个逗号分隔的文件 input1.csv
和 input2.csv
,这两个文件表示两组独立的点,如下所示:
$ cat input1.csv
1,2
3,2
2,3
2,2
3,1
2,2
2,1
$ cat input2.csv
7,4
8,4
6,4
7,5
7,3
6,4
7,6
它是如何工作的…
-
导入相关的包,如下所示:
import Graphics.EasyPlot import Text.CSV
-
定义一个辅助函数将 CSV 记录转换为数字元组,如下所示:
convertRawCSV :: [[String]] -> [(Double, Double)] convertRawCSV csv = [ (read x, read y) | [x, y] <- csv ]
-
读取这两个 CSV 文件,如下所示:
main = do csv1Raw <- parseCSVFromFile "input1.csv" csv2Raw <- parseCSVFromFile "input2.csv" let csv1 = case csv1Raw of Left err -> [] Right csv -> convertRawCSV csv let csv2 = case csv2Raw of Left err -> [] Right csv -> convertRawCSV csv
-
在同一图表上,使用不同颜色将两个数据集并排绘制。对于许多基于 Linux 的机器,使用
X11
终端来支持 X Window 系统,如下代码所示。如果在 Windows 上运行,则使用Windows
终端。在 Mac OS X 上,应将X11
替换为Aqua
:plot X11 $ [ Data2D [Color Red] [] csv1 , Data2D [Color Blue] [] csv2 ]
-
运行程序以显示下方截图中所示的图表:https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/hskl-da-cb/img/6331OS_11_05.jpg
它是如何工作的…
EasyPlot
库将所有用户指定的代码转换为 gnuplot 可理解的语言来绘制数据。plot 函数的最后一个参数可以接受多个数据集的列表来绘制图形。
另见
要可视化 3D 点,请参阅 与三维空间中的点交互 这一食谱。
与三维空间中的点交互
在可视化 3D 空间中的点时,交互式地旋转、缩放和平移表示非常有用。本食谱演示了如何在 3D 中绘制数据并实时交互。
准备工作
本食谱中使用的库通过 gnuplot 来渲染图表。我们应先安装 gnuplot。
在基于 Debian 的系统(如 Ubuntu)上,我们可以使用 apt-get
安装,方法如下:
$ sudo apt-get install gnuplot-x11
下载 gnuplot 的官方网站是 www.gnuplot.info
。
在设置好 gnuplot 后,使用 Cabal 安装 easyplot
Haskell 库,如下所示:
$ cabal install easyplot
同样,安装一个辅助的 CSV 包,如下所示:
$ cabal install csv
同样,创建两个逗号分隔的文件 input1.csv
和 input2.csv
,这两个文件表示两组独立的点,如下所示:
$ cat input1.csv
1,1,1
1,2,1
0,1,1
1,1,0
2,1,0
2,1,1
1,0,1
$ cat input2.csv
4,3,2
3,3,2
3,2,3
4,4,3
5,4,2
4,2,3
3,4,3
它是如何工作的…
-
导入相关的包,如下所示:
import Graphics.EasyPlot import Text.CSV
-
定义一个辅助函数将 CSV 记录转换为数字元组,如下所示:
convertRawCSV :: [[String]] -> [(Double, Double, Double)] convertRawCSV csv = [ (read x, read y, read z) | [x, y, z] <- csv ]
-
读取这两个 CSV 文件,如下所示:
main = do csv1Raw <- parseCSVFromFile "input1.csv" csv2Raw <- parseCSVFromFile "input2.csv" let csv1 = case csv1Raw of Left err -> [] Right csv -> convertRawCSV csv let csv2 = case csv2Raw of Left err -> [] Right csv -> convertRawCSV csv
-
使用
plot'
函数绘制数据,该函数会保持 gnuplot 运行以启用Interactive
选项。对于许多基于 Linux 的机器,使用X11
终端来支持 X Window 系统,如下代码所示。如果在 Windows 上运行,则使用Windows
终端。在 Mac OS X 上,应将X11
替换为Aqua
:plot' [Interactive] X11 $ [ Data3D [Color Red] [] csv1 , Data3D [Color Blue] [] csv2]
它是如何工作的…
EasyPlot
库将所有用户指定的代码转换成 gnuplot 能理解的语言,以绘制数据图表。最后一个参数 plot
可以接受一个数据集列表进行绘图。通过使用 plot'
函数,我们可以让 gnuplot 持续运行,这样我们可以通过旋转、缩放和平移三维图像与图形进行交互。
另请参阅
要可视化二维点,请参考 显示二维点的散点图 示例。
可视化图形网络
边和节点的图形化网络可能很难调试或理解,因此可视化可以极大地帮助我们。在本教程中,我们将把一个图形数据结构转换成节点和边的图像。
准备工作
要使用 Graphviz 图形可视化库,我们首先需要在机器上安装它。Graphviz 的官方网站包含了下载和安装说明(www.graphviz.org
)。在基于 Debian 的操作系统上,可以通过以下方式使用 apt-get
安装 Graphviz:
$ sudo apt-get install graphviz-dev graphviz
接下来,我们需要通过 Cabal 安装 Graphviz 的 Haskell 绑定,具体方法如下:
$ cabal install graphviz
如何操作…
-
导入相关的库,如下所示:
import Data.Text.Lazy (Text, empty, unpack) import Data.Graph.Inductive (Gr, mkGraph) import Data.GraphViz (GraphvizParams, nonClusteredParams, graphToDot) import Data.GraphViz.Printing (toDot, renderDot)
-
使用以下代码行创建一个通过识别形成边的节点对来定义的图形:
myGraph :: Gr Text Text myGraph = mkGraph [ (1, empty) , (2, empty) , (3, empty) ] [ (1, 2, empty) , (1, 3, empty) ]
-
设置图形使用默认参数,如下所示:
myParams :: GraphvizParams n Text Text () Text myParams = nonClusteredParams
-
如下所示,将图形的 dot 表示打印到终端:
main :: IO () main = putStr $ unpack $ renderDot $ toDot $ graphToDot myParams myGraph
-
运行代码以获取图形的 dot 表示,并将其保存到一个单独的文件中,如下所示:
$ runhaskell Main.hs > graph.dot
-
对该文件运行 Graphviz 提供的
dot
命令,以渲染出如下的图像:$ dot -Tpng graph.dot > graph.png
-
现在我们可以查看生成的
graph.png
文件,截图如下所示:https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/hskl-da-cb/img/6331OS_11_07.jpg
如何工作…
graphToDot
函数将图形转换为 DOT 语言,以描述图形。这是图形的文本序列化形式,可以被 Graphviz 的 dot
命令读取并转换成可视化图像。
更多内容…
在本教程中,我们使用了 dot
命令。Graphviz 网站还描述了其他可以将 DOT 语言文本转换成可视化图像的命令:
dot - "层级"或分层绘制有向图。如果边具有方向性,这是默认的工具。
neato - "弹簧模型"布局。如果图形不太大(约 100 个节点)且你对图形没有其他了解,这是默认的工具。Neato 尝试最小化一个全局能量函数,这等同于统计多维尺度化。
fdp - "弹簧模型"布局,类似于 neato,但通过减少力来完成布局,而不是使用能量。
sfdp - fdp 的多尺度版本,用于大图的布局。
twopi - 径向布局,基于 Graham Wills 97。节点根据与给定根节点的距离,放置在同心圆上。
circo - 圆形布局,参考 Six 和 Tollis 99,Kauffman 和 Wiese 02。这适用于某些包含多个循环结构的图,如某些电信网络。
另见
要进一步更改图形的外观和感觉,请参考 自定义图形网络图的外观 这一食谱。
自定义图形网络图的外观
为了更好地呈现数据,我们将介绍如何定制图形网络图的设计。
准备工作
要使用 Graphviz 图形可视化库,我们首先需要在机器上安装它。Graphviz 的官方网站包含了下载和安装说明,网址为 www.graphviz.org
。在基于 Debian 的操作系统上,可以使用apt-get
命令来安装 Graphviz,方法如下:
$ sudo apt-get install graphviz-dev graphviz
接下来,我们需要从 Cabal 安装 Graphviz Haskell 绑定,方法如下:
$ cabal install graphviz
如何操作…
-
导入相关的函数和库,以自定义 Graphviz 图形,方法如下:
import Data.Text.Lazy (Text, pack, unpack) import Data.Graph.Inductive (Gr, mkGraph) import Data.GraphViz ( GraphvizParams(..), GlobalAttributes( GraphAttrs, NodeAttrs, EdgeAttrs ), X11Color(Blue, Orange, White), nonClusteredParams, globalAttributes, fmtNode, fmtEdge, graphToDot ) import Data.GraphViz.Printing (toDot, renderDot) import Data.GraphViz.Attributes.Complete
-
按照以下代码片段,首先指定所有节点,然后指定哪些节点对形成边,来定义我们的自定义图形:
myGraph :: Gr Text Text myGraph = mkGraph [ (1, pack "Haskell") , (2, pack "Data Analysis") , (3, pack "Haskell Data Analysis") , (4, pack "Profit!")] [ (1, 3, pack "learn") , (2, 3, pack "learn") , (3, 4, pack "???")]
-
按照以下方式定义我们自己的自定义图形参数:
myParams :: GraphvizParams n Text Text () Text myParams = nonClusteredParams {
-
让图形引擎知道我们希望边缘是有向箭头,方法如下:
isDirected = True
-
设置图形、节点和边缘外观的全局属性如下:
, globalAttributes = [myGraphAttrs, myNodeAttrs, myEdgeAttrs]
-
按照我们自己的方式格式化节点如下:
, fmtNode = myFN
-
按照我们自己的方式格式化边缘如下:
, fmtEdge = myFE }
-
按照以下代码片段定义自定义内容:
where myGraphAttrs = GraphAttrs [ RankDir FromLeft , BgColor [toWColor Blue] ] myNodeAttrs = NodeAttrs [ Shape BoxShape , FillColor [toWColor Orange] , Style [SItem Filled []] ] myEdgeAttrs = EdgeAttrs [ Weight (Int 10) , Color [toWColor White] , FontColor (toColor White) ] myFN (n,l) = [(Label . StrLabel) l] myFE (f,t,l) = [(Label . StrLabel) l]
-
将图形的 DOT 语言表示打印到终端。
main :: IO () main = putStr $ unpack $ renderDot $ toDot $ graphToDot myParams myGraph
-
运行代码以获取图形的
dot
表示,可以将其保存在单独的文件中,方法如下:$ runhaskell Main.hs > graph.dot
-
在此文件上运行 Graphviz 提供的
dot
命令,以渲染图像,方法如下:$ dot -Tpng graph.dot > graph.png
我们现在可以查看生成的graph.png
文件,如下所示的截图:
工作原理…
graphToDot
函数将图形转换为 DOT 语言,以描述图形。这是一种图形的文本序列化格式,可以被 Graphviz 的 dot
命令读取,并转换为可视化图像。
还有更多……
图形、节点和边缘的所有可能自定义选项都可以在 Data.GraphViz.Attributes.Complete
包文档中找到,网址为 hackage.haskell.org/package/graphviz-2999.12.0.4/docs/Data-GraphViz-Attributes-Complete.html
。
使用 D3.js 在 JavaScript 中渲染条形图
我们将使用名为D3.js
的便携式 JavaScript 库来绘制条形图。这使得我们能够轻松地创建一个包含图表的网页,该图表来自 Haskell 代码。
准备工作
设置过程中需要连接互联网。
按照以下方式安装 d3js
Haskell 库:
$ cabal install d3js
创建一个网站模板,用于承载生成的 JavaScript 代码,方法如下:
$ cat index.html
JavaScript 代码如下:
<html>
<head>
<title>Chart</title>
</head>
<body>
<div id='myChart'></div>
<script charset='utf-8' src='http://d3js.org/d3.v3.min.js'></script>
<script charset='utf-8' src='generated.js'></script>
</body>
</html>
如何操作…
-
按照以下方式导入相关的包:
import qualified Data.Text as T import qualified Data.Text.IO as TIO import D3JS
-
使用
bars
函数创建条形图。输入指定的值和要绘制的条形数量,如以下代码片段所示:myChart nums numBars = do let dim = (300, 300) elem <- box (T.pack "#myChart") dim bars numBars 300 (Data1D nums) elem addFrame (300, 300) (250, 250) elem
-
定义要绘制的条形图的值和数量如下:
main = do let nums = [10, 40, 100, 50, 55, 156, 80, 74, 40, 10] let numBars = 5
-
使用
reify
函数从数据生成 JavaScriptD3.js
代码。将 JavaScript 写入名为generated.js
的文件,如下所示:let js = reify $ myChart nums numBars TIO.writeFile "generated.js" js
-
在
index.html
文件和generated.js
文件并排存在的情况下,我们可以使用支持 JavaScript 的浏览器打开index.html
网页,并看到如下所示的图表:https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/hskl-da-cb/img/6331OS_11_09.jpg
它是如何工作的…
D3.js
库是一个用于创建优雅可视化和图表的 JavaScript 库。我们使用浏览器运行 JavaScript 代码,它也充当我们的图表渲染引擎。
另请参阅
另一个D3.js
的用法,请参阅使用 D3.js 在 JavaScript 中渲染散点图食谱。
使用 D3.js 在 JavaScript 中渲染散点图
我们将使用名为D3.js
的便携式 JavaScript 库来绘制散点图。这样我们就可以轻松地创建一个包含图表的网页,该图表来自 Haskell 代码。
准备工作
进行此设置需要互联网连接。
如下所示安装d3js
Haskell 库:
$ cabal install d3js
创建一个网站模板来承载生成的 JavaScript 代码,如下所示:
$ cat index.html
JavaScript 代码如下所示:
<html>
<head>
<title>Chart</title>
</head>
<body>
<div id='myChart'></div>
<script charset='utf-8' src='http://d3js.org/d3.v3.min.js'></script>
<script charset='utf-8' src='generated.js'></script>
</body>
</html>
如何操作…
-
导入相关库,如下所示:
import D3JS import qualified Data.Text as T import qualified Data.Text.IO as TIO
-
定义散点图并输入点列表,如下所示:
myPlot points = do let dim = (300, 300) elem <- box (T.pack "#myChart") dim scatter (Data2D points) elem addFrame (300, 300) (250, 250) elem
-
定义要绘制的点列表,如下所示:
main = do let points = [(1,2), (5,10), (139,138), (140,150)]
-
使用
reify
函数从数据生成 JavaScriptD3.js
代码。将 JavaScript 写入名为generated.js
的文件,如下所示:let js = reify $ myPlot points TIO.writeFile "generated.js" js
-
在
index.html
和generated.js
文件并排存在的情况下,我们可以使用支持 JavaScript 的浏览器打开index.html
网页,并看到如下所示的图表:https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/hskl-da-cb/img/6331OS_11_10.jpg
它是如何工作的…
graphToDot
函数将图表转换为 DOT 语言来描述图表。这是图表的文本序列化格式,可以通过 Graphviz 的dot
命令读取并转换为可视化图像。
另请参阅
另一个D3.js
的用法,请参阅使用 D3.js 在 JavaScript 中渲染条形图食谱。
从向量列表绘制路径
在这个食谱中,我们将使用diagrams
包来从驾驶路线中绘制路径。我们将所有可能的旅行方向分类为八个基本方向,并附上相应的距离。我们使用下图中 Google Maps 提供的方向,并从文本文件中重建这些方向:
准备工作
如下所示安装diagrams
库:
$ cabal install diagrams
创建一个名为input.txt
的文本文件,其中包含八个基本方向之一,后面跟着距离,每一步用新的一行分隔:
$ cat input.txt
N 0.2
W 0.1
S 0.6
W 0.05
S 0.3
SW 0.1
SW 0.2
SW 0.3
S 0.3
如何操作…
-
导入相关库,如下所示:
{-# LANGUAGE NoMonomorphismRestriction #-} import Diagrams.Prelude import Diagrams.Backend.SVG.CmdLine (mainWith, B)
-
从一系列向量中绘制一个连接的路径,如下所示:
drawPath :: [(Double, Double)] -> Diagram B R2 drawPath vectors = fromOffsets . map r2 $ vectors
-
读取一系列方向,将其表示为向量列表,并按如下方式绘制路径:
main = do rawInput <- readFile "input.txt" let vs = [ makeVector dir (read dist) | [dir, dist] <- map words (lines rawInput)] print vs mainWith $ drawPath vs
-
定义一个辅助函数,根据方向及其对应的距离创建一个向量,如下所示:
makeVector :: String -> Double -> (Double, Double) makeVector "N" dist = (0, dist) makeVector "NE" dist = (dist / sqrt 2, dist / sqrt 2) makeVector "E" dist = (dist, 0) makeVector "SE" dist = (dist / sqrt 2, -dist / sqrt 2) makeVector "S" dist = (0, -dist) makeVector "SW" dist = (-dist / sqrt 2, -dist / sqrt 2) makeVector "W" dist = (-dist, 0) makeVector "NW" dist = (-dist / sqrt 2, dist / sqrt 2) makeVector _ _ = (0, 0)
-
编译代码并按如下方式运行:
$ ghc --make Main.hs $ ./Main –o output.svg –w 400
它是如何工作的…
mainWith
函数接收一个 Diagram
类型,并在终端中调用时生成相应的图像文件。我们通过 drawPath
函数获得 Diagram
,该函数通过偏移量将向量连接在一起。
第十二章 导出与展示
本章将涵盖如何导出结果并通过以下食谱优雅地展示它们:
-
将数据导出到 CSV 文件
-
将数据导出为 JSON
-
使用 SQLite 存储数据
-
将数据保存到 MongoDB 数据库
-
在 HTML 网页中展示结果
-
创建 LaTeX 表格以展示结果
-
使用文本模板个性化消息
-
将矩阵值导出到文件
介绍
在数据收集、清洗、表示和分析后,数据分析的最后一步是将数据导出并以可用格式展示。 本章中的食谱将展示如何将数据结构保存到磁盘,以供其他程序后续使用。此外,我们还将展示如何使用 Haskell 优雅地展示数据。
将数据导出到 CSV 文件
有时,使用像 LibreOffice、Microsoft Office Excel 或 Apple Numbers 这样的电子表格程序查看数据更为便捷。导出和导入简单电子表格表格的标准方式是通过逗号分隔值(CSV)。
在这个食谱中,我们将使用cassava
包轻松地从数据结构编码一个 CSV 文件。
准备工作
使用以下命令从 cabal 安装 Cassava CSV 包:
$ cabal install cassava
如何实现……
-
使用以下代码导入相关包:
import Data.Csv import qualified Data.ByteString.Lazy as BSL
-
定义将作为 CSV 导出的数据关联列表。在这个食谱中,我们将字母和数字配对,如以下代码所示:
myData :: [(Char, Int)] myData = zip ['A'..'Z'] [1..]
-
运行
encode
函数将数据结构转换为懒加载的 ByteString CSV 表示,如以下代码所示:main = BSL.writeFile "letters.csv" $ encode myData
它是如何工作的……
CSV 文件只是记录的列表。Cassava 库中的encode
函数接受实现了ToRecord
类型类的项列表。
在这个食谱中,我们可以看到像('A', 1)
这样的大小为 2 的元组是encode
函数的有效参数。默认情况下,支持大小为 2 到 7 的元组以及任意大小的列表。元组或列表的每个元素必须实现ToField
类型类,大多数内置的原始数据类型默认支持该类。有关该包的更多细节,请访问hackage.haskell.org/package/cassava
。
还有更多……
为了方便地将数据类型转换为 CSV,我们可以实现ToRecord
类型类。
例如,Cassava 文档展示了以下将Person
数据类型转换为 CSV 记录的例子:
data Person = Person { name :: Text, age :: Int }
instance ToRecord Person where
toRecord (Person name age) = record [
toField name, toField age]
另请参见
如果是 JSON 格式,请参考以下导出数据为 JSON食谱。
导出数据为 JSON
存储可能不遵循严格模式的数据的便捷方式是通过 JSON。为此,我们将使用一个名为Yocto的简便 JSON 库。它牺牲了性能以提高可读性并减小体积。
在这个食谱中,我们将导出一个点的列表为 JSON 格式。
准备工作
使用以下命令从 cabal 安装 Yocto JSON 编码器和解码器:
$ cabal install yocto
如何实现……
从创建一个新的文件开始,我们称其为Main.hs
,并执行以下步骤:
-
如下所示导入相关数据结构:
import Text.JSON.Yocto import qualified Data.Map as M
-
如下所示定义一个二维点的数据结构:
data Point = Point Rational Rational
-
将
Point
数据类型转换为 JSON 对象,如下方代码所示:pointObject (Point x y) = Object $ M.fromList [ ("x", Number x) , ("y", Number y)]
-
创建点并构建一个 JSON 数组:
main = do let points = [ Point 1 1 , Point 3 5 , Point (-3) 2] let pointsArray = Array $ map pointObject points
-
将 JSON 数组写入文件,如下方代码所示:
writeFile "points.json" $ encode pointsArray
-
运行代码时,我们会发现生成了
points.json
文件,如下方代码所示:$ runhaskell Main.hs $ cat points.json [{"x":1,"y":1}, {"x":3,"y":5}, {"x":-3,"y":2}]
还有更多内容…
若需更高效的 JSON 编码器,请参考 Aeson 包,位于hackage.haskell.org/package/aeson
。
参见
要将数据导出为 CSV,请参考前面标题为导出数据到 CSV 文件的食谱。
使用 SQLite 存储数据
SQLite 是最流行的数据库之一,用于紧凑地存储结构化数据。我们将使用 Haskell 的 SQL 绑定来存储字符串列表。
准备工作
我们必须首先在系统上安装 SQLite3 数据库。在基于 Debian 的系统上,我们可以通过以下命令进行安装:
$ sudo apt-get install sqlite3
使用以下命令从 cabal 安装 SQLite 包:
$ cabal install sqlite-simple
创建一个名为test.db
的初始数据库,并设置其模式。在本食谱中,我们只会存储整数和字符串,如下所示:
$ sqlite3 test.db "CREATE TABLE test (id INTEGER PRIMARY KEY, str text);"
如何实现…
-
导入相关库,如下方代码所示:
{-# LANGUAGE OverloadedStrings #-} import Control.Applicative import Database.SQLite.Simple import Database.SQLite.Simple.FromRow
-
为
TestField
(我们将要存储的数据类型)创建一个FromRow
类型类的实现,如下方代码所示:data TestField = TestField Int String deriving (Show) instance FromRow TestField where fromRow = TestField <$> field <*> field
-
创建一个辅助函数,用于仅为调试目的从数据库中检索所有数据,如下方代码所示:
getDB :: Connection -> IO [TestField] getDB conn = query_ conn "SELECT * from test"
-
创建一个辅助函数,将字符串插入数据库,如下方代码所示:
insertToDB :: Connection -> String -> IO () insertToDB conn item = execute conn "INSERT INTO test (str) VALUES (?)" (Only item)
-
如下所示连接到数据库:
main :: IO () main = withConnection "test.db" dbActions
-
设置我们希望插入的字符串数据,如下方代码所示:
dbActions :: Connection -> IO () dbActions conn = do let dataItems = ["A", "B", "C"]
-
将每个元素插入数据库,如下方代码所示:
mapM_ (insertToDB conn) dataItems
-
使用以下代码打印数据库内容:
r <- getDB conn mapM_ print r
-
我们可以通过调用以下命令验证数据库中是否包含新插入的数据:
$ sqlite3 test.db "SELECT * FROM test" 1|A 2|C 3|D
参见
若使用另一种类型的数据库,请参考以下食谱保存数据到 MongoDB 数据库。
保存数据到 MongoDB 数据库
MongoDB 可以非常自然地使用 JSON 语法存储非结构化数据。在本例中,我们将把一组人员数据存储到 MongoDB 中。
准备工作
我们必须首先在机器上安装 MongoDB。安装文件可以从www.mongodb.org
下载。
我们需要使用以下命令为数据库创建一个目录:
$ mkdir ~/db
最后,使用以下命令在该目录下启动 MongoDB 守护进程:
$ mongod –dbpath ~/db
使用以下命令从 cabal 安装 MongoDB 包:
$ cabal install mongoDB
如何实现…
创建一个名为Main.hs
的新文件,并执行以下步骤:
-
按照如下方式导入库:
{-# LANGUAGE OverloadedStrings, ExtendedDefaultRules #-} import Database.MongoDB import Control.Monad.Trans (liftIO)
-
如下所示定义一个表示人物姓名的数据类型:
data Person = Person { first :: String , last :: String }
-
设置我们希望存储的几个数据项,如下所示:
myData :: [Person] myData = [ Person "Mercury" "Merci" , Person "Sylvester" "Smith"]
-
连接到 MongoDB 实例并存储所有数据,如下所示:
main = do pipe <- runIOE $ connect (host "127.0.0.1") e <- access pipe master "test" (store myData) close pipe print e
-
按照以下方式将
Person
数据类型转换为适当的 MongoDB 类型:store vals = insertMany "people" mongoList where mongoList = map (\(Person f l) -> ["first" =: f, "last" =: l]) vals
-
我们必须确保 MongoDB 守护进程正在运行。如果没有,我们可以使用以下命令创建一个监听我们选择目录的进程:
$ mongod --dbpath ~/db
-
运行代码后,我们可以通过以下命令检查操作是否成功,方法是访问 MongoDB:
$ runhaskell Main.hs $ mongo > db.people.find() { "_id" : ObjectId("536d2b13f8712126e6000000"), "first" : "Mercury", "last" : "Merci" } { "_id" : ObjectId("536d2b13f8712126e6000001"), "first" : "Sylvester", "last" : "Smith" }
另见
对于 SQL 的使用,请参考之前的使用 SQLite 存储数据的做法。
在 HTML 网页中展示结果
在线共享数据是触及广泛受众的最快方式之一。然而,直接将数据输入到 HTML 中可能会耗费大量时间。本做法将使用 Blaze Haskell 库生成一个网页,来展示数据结果。更多文档和教程,请访问项目网页jaspervdj.be/blaze/
。
准备工作
从 cabal 使用以下命令安装 Blaze 包:
$ cabal install blaze-html
如何操作…
在一个名为Main.hs
的新文件中,执行以下步骤:
-
按照以下方式导入所有必要的库:
{-# LANGUAGE OverloadedStrings #-} import Control.Monad (forM_) import Text.Blaze.Html5 import qualified Text.Blaze.Html5 as H import Text.Blaze.Html.Renderer.Utf8 (renderHtml) import qualified Data.ByteString.Lazy as BSL
-
按照以下代码片段将字符串列表转换为 HTML 无序列表:
dataInList :: Html -> [String] -> Html dataInList label items = docTypeHtml $ do H.head $ do H.title "Generating HTML from data" body $ do p label ul $ mapM_ (li . toHtml) items
-
创建一个字符串列表,并按以下方式将其渲染为 HTML 网页:
main = do let movies = [ "2001: A Space Odyssey" , "Watchmen" , "GoldenEye" ] let html = renderHtml $ dataInList "list of movies" movies BSL.writeFile "index.html" $ html
-
运行代码以生成 HTML 文件,并使用浏览器打开,如下所示:
$ runhaskell Main.hs
输出结果如下:
另见
要将数据呈现为 LaTeX 文档并最终生成 PDF,请参考以下创建一个 LaTeX 表格来展示结果的做法。
创建一个 LaTeX 表格来展示结果
本做法将通过编程方式创建一个 LaTeX 表格,以便于文档的创建。我们可以从 LaTeX 代码生成 PDF 并随意分享。
准备工作
从 cabal 安装HaTeX
,Haskell LaTeX 库:
$ cabal install LaTeX
如何操作…
创建一个名为Main.hs
的文件,并按照以下步骤进行:
-
按照以下方式导入库:
{-# LANGUAGE OverloadedStrings #-} import Text.LaTeX import Text.LaTeX.Base.Class import Text.LaTeX.Base.Syntax import qualified Data.Map as M
-
按照以下规格保存一个 LaTeX 文件:
main :: IO () main = execLaTeXT myDoc >>= renderFile "output.tex"
-
按照以下方式定义文档,文档被分为前言和正文:
myDoc :: Monad m => LaTeXT_ m myDoc = do thePreamble document theBody
-
前言部分包含作者数据、标题、格式选项等内容,如下代码所示:
thePreamble :: Monad m => LaTeXT_ m thePreamble = do documentclass [] article author "Dr. Databender" title "Data Analyst"
-
按照以下方式定义我们希望转换为 LaTeX 表格的数据列表:
myData :: [(Int,Int)] myData = [ (1, 50) , (2, 100) , (3, 150)]
-
按照以下方式定义正文:
theBody :: Monad m => LaTeXT_ m theBody = do
-
设置标题和章节,并按照以下代码片段构建表格:
maketitle section "Fancy Data Table" bigskip center $ underline $ textbf "Table of Points" center $ tabular Nothing [RightColumn, VerticalLine, LeftColumn] $ do textbf "Time" & textbf "Cost" lnbk hline mapM_ (\(t, c) -> do texy t & texy c; lnbk) myData
-
运行以下命令后,我们可以获取 PDF 并查看:
$ runhaskell Main.hs $ pdflatex output.tex
输出结果如下:
另见
要构建一个网页,请参考前面的做法,标题为在 HTML 网页中展示结果。
使用文本模板个性化消息
有时我们有一个包含大量用户名和相关数据的列表,并且希望单独向每个人发送消息。本做法将创建一个文本模板,该模板将从数据中填充。
准备工作
使用 cabal 安装template
库:
$ cabal install template
如何操作…
在一个名为Main.hs
的新文件中执行以下步骤:
-
按如下方式导入相关库:
{-# LANGUAGE OverloadedStrings #-} import qualified Data.ByteString.Lazy as S import qualified Data.Text as T import qualified Data.Text.IO as TIO import qualified Data.Text.Lazy.Encoding as E import qualified Data.ByteString as BS import Data.Text.Lazy (toStrict) import Data.Text.Template
-
定义我们处理的数据如下:
myData = [ [ ("name", "Databender"), ("title", "Dr.") ], [ ("name", "Paragon"), ("title", "Master") ], [ ("name", "Marisa"), ("title", "Madam") ] ]
-
定义数据模板如下:
myTemplate = template "Hello $title $name!"
-
创建一个辅助函数,将数据项转换为模板,如下所示:
context :: [(T.Text, T.Text)] -> Context context assocs x = maybe err id . lookup x $ assocs where err = error $ "Could not find key: " ++ T.unpack x
-
将每个数据项与模板匹配,并将所有内容打印到文本文件中,如下代码片段所示:
main :: IO () main = do let res = map (\d -> toStrict ( render myTemplate (context d) )) myData TIO.writeFile "messages.txt" $ T.unlines res
-
运行代码以查看生成的文件:
$ runhaskell Main.hs $ cat messages.txt Hello Dr. Databender! Hello Master Paragon! Hello Madam Marisa!
将矩阵值导出到文件
在数据分析和机器学习中,矩阵是一种常见的数据结构,经常需要导入和导出到程序中。在这个方案中,我们将使用 Repa I/O 库导出一个示例矩阵。
准备工作
使用 cabal 安装repa-io
库,如下所示:
$ cabal install repa-io
如何做……
创建一个新文件,我们命名为Main.hs
,并插入接下来步骤中解释的代码:
-
按如下方式导入相关库:
import Data.Array.Repa.IO.Matrix import Data.Array.Repa
-
定义一个 4 x 3 的矩阵,如下所示:
x :: Array U DIM2 Int x = fromListUnboxed (Z :. (4::Int) :. (3::Int)) [ 1, 2, 9, 10 , 4, 3, 8, 11 , 5, 6, 7, 12 ]
-
将矩阵写入文件,如下所示:
main = writeMatrixToTextFile "output.dat" x
工作原理……
矩阵简单地表示为其元素的列表,按行优先顺序排列。文件的前两行定义了数据类型和维度。
还有更多内容……
要从此文件中读取矩阵,我们可以使用readMatrixFromTextFile
函数来检索二维矩阵。更多关于此包的文档可以在hackage.haskell.org/package/repa-io
找到。