Haskell-数据分析秘籍-四-

66 阅读24分钟

Haskell 数据分析秘籍(四)

原文:annas-archive.org/md5/3ff53e35b37e2f50c639bfc6fc052f29

译者:飞龙

协议:CC BY-NC-SA 4.0

第十章:实时数据

本章将涵盖以下示例:

  • 为实时情感分析流式传输 Twitter 数据

  • 读取 IRC 聊天室消息

  • 响应 IRC 消息

  • 向 Web 服务器轮询以获取最新更新

  • 检测实时文件目录变化

  • 通过套接字进行实时通信

  • 通过摄像头流检测面部和眼睛

  • 用于模板匹配的摄像头流

介绍

Introduction

首先收集数据然后再分析它是相当容易的。然而,有些任务可能需要将这两个步骤结合在一起。实时分析接收到的数据是本章的核心内容。我们将讨论如何管理来自 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 凭证并开始编码:

  1. 通过访问 apps.twitter.com 创建一个新的 Twitter 应用。

  2. 从此 Twitter 应用管理页面找到 OAuth 消费者密钥和 OAuth 消费者密钥。分别为 OAUTH_CONSUMER_KEYOAUTH_CONSUMER_SECRET 设置系统环境变量。大多数支持 sh 兼容 shell 的 Unix 系统支持 export 命令:

    $ export OAUTH_CONSUMER_KEY="Your OAuth Consumer Key"
    $ export OAUTH_CONSUMER_SECRET="Your OAuth Consumer Secret"
    
    
  3. 此外,通过相同的 Twitter 应用管理页面找到 OAuth 访问令牌和 OAuth 访问密钥,并相应地设置环境变量:

    $ export OAUTH_ACCESS_TOKEN="Your OAuth Access Token"
    $ export OAUTH_ACCESS_SECRET="Your OAuth Access Secret"
    
    

    小贴士

    我们将密钥、令牌和秘密 PIN 存储在环境变量中,而不是直接将它们硬编码到程序中,因为这些变量与密码一样重要。就像密码永远不应公开可见,我们尽力将这些令牌和密钥保持在源代码之外。

  4. twitter-conduit包的示例目录中下载Common.hs文件,路径为github.com/himura/twitter-conduit/tree/master/sample。研究userstream.hs示例文件。

  5. 首先,我们导入所有相关的库:

    {-# 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 
    
  6. main中,运行我们的实时情感分析器以进行搜索查询:

    main :: IO ()
    
    main = do
      let query = "haskell"
      T.putStrLn $ T.concat [ "Streaming Tweets that match \""
                            , query, "\"..."]
      analyze query
    
  7. 使用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))
    
  8. 一旦我们获得事件触发的输入,就会运行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
    
  9. 如果事件触发的输入不是推文,而是朋友关系事件或其他内容,则不执行任何操作:

    process s = return ()
    
  10. 定义一个辅助函数,通过移除所有@user提及、#hashtagshttp://websites来清理输入:

    clean :: String -> String
    
    clean str = unwords $ filter 
                (\w -> not (or 
                       [ isInfixOf "@" w
                       , isInfixOf "#" w
                       , isInfixOf "http://" w ]))
                (words str)
    
  11. 使用外部 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
    
  12. 从我们的 API 的 JSON 响应中提取情感值:

    polarity :: Value -> Rational
    
    polarity (Object m) = polarity' $ m ! "results"
      where polarity' (Object m) = fromNumber $ m ! "polarity"
            fromNumber (Number n) = n
    polarity _ = -1
    
  13. 运行代码,查看推文在全球任何人公开发布时即刻显示。情感值将是介于 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 库还支持另外两种实时流:statusesFilterByFollowuserstream。前者获取指定用户列表的所有推文,后者获取该账户关注的用户的所有推文。

例如,通过将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的新文件中,插入以下代码:

  1. 导入相关的包:

    import Network
    import Control.Monad (forever)
    import System.IO
    import Text.Printf
    
  2. 指定 IRC 服务器的具体信息:

    server = "irc.freenode.org"
    port   = 6667
    chan   = "#haskelldata"
    nick   = "awesome-bot"
    
  3. 连接到服务器并监听聊天室中的所有文本:

    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
    
  4. 定义我们的监听器。对于这个配方,我们将仅将所有事件回显到控制台:

    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,并执行以下操作:

  1. 导入相关的库:

    {-# LANGUAGE OverloadedStrings #-}
    
    import Network.SimpleIRC
    import Data.Maybe
    import qualified Data.ByteString.Char8 as B
    
  2. 创建接收到消息时的事件处理程序。如果消息是“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]
    
  3. 定义要监听的事件:

    events = [(Privmsg onMessage)]
    
  4. 设置 IRC 服务器配置。连接到任意一组频道,并绑定我们的事件:

    freenode = 
      (mkDefaultConfig "irc.freenode.net" "awesome-bot")
      { cChannels = ["#haskelldata"]
      , cEvents   = events
      }
    
  5. 连接到服务器。不要在新线程中运行,而是打印调试信息,按照相应的布尔参数来指定:

    main = connect freenode False True
    
  6. 运行代码,打开 IRC 客户端进行测试:如何操作…

另见

若要在不使用外部库的情况下连接 IRC 服务器,请参见之前的教程,读取 IRC 聊天室消息

轮询 web 服务器以获取最新更新

一些网站的变化非常频繁。例如,Google 新闻和 Reddit 通常在我们刷新页面时,立刻加载最新的帖子。为了随时保持最新数据,最好是频繁地发送 HTTP 请求。

在本教程中,我们每 10 秒轮询一次新的 Reddit 帖子,如下图所示:

轮询 web 服务器以获取最新更新

如何操作…

在一个名为Main.hs的新文件中,执行以下步骤:

  1. 导入相关的库:

    import Network.HTTP
    import Control.Concurrent (threadDelay)
    import qualified Data.Text as T
    
  2. 定义要轮询的 URL:

    url = "http://www.reddit.com/r/pics/new.json"
    
  3. 定义一个函数来获取最新的 HTTP GET 请求数据:

    latest :: IO String
    
    latest = simpleHTTP (getRequest url) >>= getResponseBody
    
  4. 轮询实际上是等待指定时间后递归地执行任务。在这种情况下,我们会等 10 秒钟再请求最新的网页数据:

    poll :: IO a
    
    poll = do
      body <- latest
      print $ doWork body
      threadDelay (10 * 10⁶)
      poll
    
  5. 运行轮询:

    main :: IO a
    main = do
      putStrLn $ "Polling " ++ url ++ " …"
      poll
    
  6. 每次 Web 请求后,分析数据。在本教程中,统计 Imgur 出现的次数:

    doWork str = length $ T.breakOnAll 
                  (T.pack "imgur.com/") (T.pack str)
    

检测实时文件目录变化

在本教程中,我们将实时检测文件是否被创建、修改或删除。类似于流行的文件同步软件 Dropbox,我们每次遇到这样的事件时,都会执行一些有趣的操作。

准备工作

安装fsnotify包:

$ cabal install fsnotify

如何操作…

在一个名为Main.hs的新文件中,执行以下步骤:

  1. 导入相关的库:

    {-# LANGUAGE OverloadedStrings #-}
    import Filesystem.Path.CurrentOS
    import System.FSNotify
    import Filesystem
    import Filesystem.Path (filename)
    
  2. 在当前目录上运行文件监视器:

    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 ()
    
  3. 处理每个文件变化事件。在本教程中,我们仅将操作输出到控制台:

    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"
    
  4. 运行代码并开始修改同一目录中的一些文件。例如,创建一个新文件,编辑它,然后删除它:

    $ runhaskell Main.hs
    
    press return to stop
    FilePath "hello.txt" added
    FilePath "hello.txt" modified
    FilePath "hello.txt" removed
    
    

它是如何工作的…

fsnotify库绑定到特定平台文件系统的事件通知服务。在基于 Unix 的系统中,这通常是inotifydell9.ma.utexas.edu/cgi-bin/man-cgi?inotify)。

通过套接字实时通信

套接字提供了一种方便的实时程序间通信方式。可以把它们想象成一个聊天客户端。

在这个教程中,我们将从一个程序向另一个程序发送消息并获取响应。

如何做…

将以下代码插入到名为Main.hs的新文件中:

  1. 创建服务器代码:

    import Network ( listenOn, withSocketsDo, accept
                   , PortID(..), Socket )
    import System.Environment (getArgs)
    import System.IO ( hSetBuffering, hGetLine, hPutStrLn
                     , BufferMode(..), Handle )
    import Control.Concurrent (forkIO)
    
  2. 创建一个套接字连接以进行监听,并在其上附加我们的处理程序sockHandler

    main :: IO ()
    
    main = withSocketsDo $ do
        let port = PortNumber 9001
        sock <- listenOn port
        putStrLn $ "Listening…"
        sockHandler sock
    
  3. 定义处理每个接收到的消息的处理程序:

    sockHandler :: Socket -> IO ()
    
    sockHandler sock = do
        (h, _, _) <- accept sock
        putStrLn "Connected!"
        hSetBuffering h LineBuffering
        forkIO $ process h
        forkIO $ respond h
        sockHandler sock
    
  4. 定义如何处理客户端发送的消息:

    process :: Handle -> IO ()
    process h = do
        line <- hGetLine h
        print line
        process h
    
  5. 通过用户输入向客户端发送消息:

    respond h = withSocketsDo $ do
      txt <- getLine
      hPutStrLn h txt
      respond h
    
  6. 现在,在一个新文件client.hs中创建客户端代码。首先,导入库:

    import Network (connectTo, withSocketsDo, PortID(..))
    import System.Environment (getArgs)
    import System.IO ( hSetBuffering, hPutStrLn
                     , hGetLine, BufferMode(..) )
    
  7. 将客户端连接到相应的端口,并设置响应者和监听线程:

    main = withSocketsDo $ do
      let port = PortNumber 9001
      h <- connectTo "localhost" port
      putStrLn $ "Connected!"
      hSetBuffering h LineBuffering
      forkIO $ respond h
      forkIO $ process h
      loop
    
  8. 获取用户输入并将其作为消息发送:

    respond h = do
      txt <- getLine
      hPutStrLn h txt
      respond h
    
  9. 监听来自服务器的传入消息:

    process h = do
      line <- hGetLine h
      print line
      process h
    
  10. 先运行服务器,测试代码:

    $ runhaskell Main.hs
    
    
  11. 接下来,在一个单独的终端中运行客户端:

    $ runhaskell client.hs
    
    
  12. 现在,我们可以通过键入并按下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,并按照以下步骤操作:

  1. 导入相关库:

    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)
    
  2. 定义摄像头流的来源。我们将使用内置的摄像头。若要改用视频,可以将camera 0替换为videoFile "./myVideo.mpeg"

    captureDev :: ImageSource
    captureDev = camera 0
    
  3. 缩小流的大小以提高性能:

    resizer :: ImageProcessor
    resizer = resize 320 240 CV.CV_INTER_LINEAR
    
  4. 使用 OpenCV 提供的训练数据集检测图像中的人脸:

    faceDetect :: Processor.IOProcessor PImage [CvRect]
    
    faceDetect = haarDetect  "/usr/share/opencv/haarcascades/haarcascade_frontalface_alt.xml" 1.1 3 CV.cvHaarFlagNone (CvSize 20 20)
    
  5. 使用 OpenCV 提供的训练数据集检测图像中的眼睛:

    eyeDetect :: Processor.IOProcessor PImage [CvRect]
    eyeDetect = haarDetect "/usr/share/opencv/haarcascades/haarcascade_eye.xml" 1.1 3 CV.cvHaarFlagNone (CvSize 20 20)
    
  6. 在人脸和眼睛周围画矩形框:

    faceRects = (id &&& faceDetect) >>> drawRects
    
    eyeRects = (id &&& eyeDetect) >>> drawRects
    
  7. 捕获摄像头流,检测面部和眼睛,绘制矩形,并在两个不同的窗口中显示它们:

    start = captureDev >>> resizer --< (faceRects *** eyeRects) 
                 >>> (window 0 *** window 1)
    
  8. 执行实时摄像头流并在按下某个键后停止:

    main :: IO ()
    main = runTillKeyPressed start
    
  9. 运行代码并查看网络摄像头,以检测面部和眼睛,结果如以下命令后的截图所示:

    $ 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中,从以下步骤开始:

  1. 导入相关库:

    {-#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(..))
    
  2. 加载模板图像并开始对摄像头流进行模板匹配:

    main = do
      Just t <- loadImage "lena.jpg"
      Just c <- captureFromCam 0
      runStream_ . sideEffect (process t) . 
        takeWhileS (\_ -> True) $ streamFromVideo c
    
  3. 对摄像头流的每一帧执行操作。具体来说,使用模板匹配来定位模板并围绕其绘制一个圆圈:

    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 ()
    
  4. 使用以下命令运行代码并显示模板图像。会在找到的图像周围绘制一个黑色圆圈:

    $ 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

如何实现…

  1. 按如下方式导入 Google Chart API 库:

    import Graphics.Google.Chart
    
  2. 从文本文件中获取输入,并将其解析为整数列表:

    main = do
      rawInput <- readFile "input.txt"
      let nums = map (read :: String -> Int) (lines rawInput)
    
  3. 通过适当设置属性,创建一个图表 URL,如以下代码片段所示:

      putStrLn $ chartURL $
        setSize 500 200 $
        setTitle "Example of Plotting a Chart in Haskell" $
        setData (encodeDataSimple [nums]) $
        setLegend ["Stock Price"] $
        newLineChart
    
  4. 运行程序将输出一个 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

如何操作…

  1. 按如下方式导入 Google Chart API 库:

    import Graphics.Google.Chart
    
  2. 从文本文件中收集输入并将其解析为整数列表,如以下代码片段所示:

    main = do
      rawInput <- readFile "input.txt"
      let nums = map (read :: String -> Int) (lines rawInput)
    
  3. 从以下代码中显示的饼图属性中打印出 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
    
  4. 运行程序将输出如下的 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.txtinput2.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

如何操作…

  1. 按如下方式导入 Google Chart API 库:

    import Graphics.Google.Chart
    
  2. 从两个文本文件中获取两个输入值,并将它们解析为两个独立的整数列表,如以下代码片段所示:

    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)
    
  3. 同样设置柱状图并打印出 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
    
  4. 运行程序将输出一个 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

如何实现…

  1. 按照以下方式导入EasyPlot库:

    import Graphics.EasyPlot
    
  2. 定义一个数字列表进行绘图,如下所示:

    main = do
      let values = [4,5,16,15,14,13,13,17]
    
  3. 如以下代码片段所示,在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.csvinput2.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

它是如何工作的…

  1. 导入相关的包,如下所示:

    import Graphics.EasyPlot
    
    import Text.CSV
    
  2. 定义一个辅助函数将 CSV 记录转换为数字元组,如下所示:

    convertRawCSV :: [[String]] -> [(Double, Double)]
    convertRawCSV csv = [ (read x, read y) | [x, y] <- csv ]
    
  3. 读取这两个 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
    
  4. 在同一图表上,使用不同颜色将两个数据集并排绘制。对于许多基于 Linux 的机器,使用 X11 终端来支持 X Window 系统,如下代码所示。如果在 Windows 上运行,则使用 Windows 终端。在 Mac OS X 上,应将 X11 替换为 Aqua

      plot X11 $ [ Data2D [Color Red] [] csv1
      , Data2D [Color Blue] [] csv2 ]
    
  5. 运行程序以显示下方截图中所示的图表:操作方法…

它是如何工作的…

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.csvinput2.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

它是如何工作的…

  1. 导入相关的包,如下所示:

    import Graphics.EasyPlot
    
    import Text.CSV
    
  2. 定义一个辅助函数将 CSV 记录转换为数字元组,如下所示:

    convertRawCSV :: [[String]] -> [(Double, Double, Double)]
    
    convertRawCSV csv = [ (read x, read y, read z) 
                        | [x, y, z] <- csv ]
    
  3. 读取这两个 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
    
  4. 使用 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

如何操作…

  1. 导入相关的库,如下所示:

    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)
    
  2. 使用以下代码行创建一个通过识别形成边的节点对来定义的图形:

    myGraph :: Gr Text Text
    myGraph = mkGraph [ (1, empty)
                      , (2, empty)
                      , (3, empty) ]
              [ (1, 2, empty) 
              , (1, 3, empty) ]
    
  3. 设置图形使用默认参数,如下所示:

    myParams :: GraphvizParams n Text Text () Text
    myParams = nonClusteredParams
    
  4. 如下所示,将图形的 dot 表示打印到终端:

    main :: IO ()
    main = putStr $ unpack $ renderDot $ toDot $ 
           graphToDot myParams myGraph
    
  5. 运行代码以获取图形的 dot 表示,并将其保存到一个单独的文件中,如下所示:

    $ runhaskell Main.hs > graph.dot
    
  6. 对该文件运行 Graphviz 提供的 dot 命令,以渲染出如下的图像:

    $ dot -Tpng graph.dot > graph.png
    
  7. 现在我们可以查看生成的 graph.png 文件,截图如下所示:如何操作…

如何工作…

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

如何操作…

  1. 导入相关的函数和库,以自定义 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
    
  2. 按照以下代码片段,首先指定所有节点,然后指定哪些节点对形成边,来定义我们的自定义图形:

    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 "???")]
    
  3. 按照以下方式定义我们自己的自定义图形参数:

    myParams :: GraphvizParams n Text Text () Text
    myParams = nonClusteredParams { 
    
  4. 让图形引擎知道我们希望边缘是有向箭头,方法如下:

        isDirected       = True
    
  5. 设置图形、节点和边缘外观的全局属性如下:

      , globalAttributes = [myGraphAttrs, myNodeAttrs, myEdgeAttrs]
    
  6. 按照我们自己的方式格式化节点如下:

      , fmtNode          = myFN
    
  7. 按照我们自己的方式格式化边缘如下:

      , fmtEdge          = myFE
      }
    
  8. 按照以下代码片段定义自定义内容:

     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]
    
  9. 将图形的 DOT 语言表示打印到终端。

    main :: IO ()
    main = putStr $ unpack $ renderDot $ toDot $ graphToDot myParams myGraph
    
  10. 运行代码以获取图形的dot表示,可以将其保存在单独的文件中,方法如下:

    $ runhaskell Main.hs > graph.dot
    
    
  11. 在此文件上运行 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>

如何操作…

  1. 按照以下方式导入相关的包:

    import qualified Data.Text as T
    import qualified Data.Text.IO as TIO
    import D3JS
    
  2. 使用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
    
  3. 定义要绘制的条形图的值和数量如下:

    main = do
      let nums = [10, 40, 100, 50, 55, 156, 80, 74, 40, 10]
      let numBars = 5
    
  4. 使用reify函数从数据生成 JavaScript D3.js代码。将 JavaScript 写入名为generated.js的文件,如下所示:

      let js = reify $ myChart nums numBars
      TIO.writeFile "generated.js" js
    
  5. index.html文件和generated.js文件并排存在的情况下,我们可以使用支持 JavaScript 的浏览器打开index.html网页,并看到如下所示的图表:如何操作…

它是如何工作的…

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>

如何操作…

  1. 导入相关库,如下所示:

    import D3JS
    import qualified Data.Text as T
    import qualified Data.Text.IO as TIO
    
  2. 定义散点图并输入点列表,如下所示:

    myPlot points = do
      let dim = (300, 300)
      elem <- box (T.pack "#myChart") dim
      scatter (Data2D points) elem
      addFrame (300, 300) (250, 250) elem   
    
  3. 定义要绘制的点列表,如下所示:

    main = do
      let points = [(1,2), (5,10), (139,138), (140,150)]
    
  4. 使用reify函数从数据生成 JavaScript D3.js代码。将 JavaScript 写入名为generated.js的文件,如下所示:

      let js = reify $ myPlot points
      TIO.writeFile "generated.js" js
    
  5. index.htmlgenerated.js文件并排存在的情况下,我们可以使用支持 JavaScript 的浏览器打开index.html网页,并看到如下所示的图表:如何操作…

它是如何工作的…

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

如何操作…

  1. 导入相关库,如下所示:

    {-# LANGUAGE NoMonomorphismRestriction #-}
    import Diagrams.Prelude
    import Diagrams.Backend.SVG.CmdLine (mainWith, B)
    
  2. 从一系列向量中绘制一个连接的路径,如下所示:

    drawPath :: [(Double, Double)] -> Diagram B R2
    drawPath vectors = fromOffsets . map r2 $ vectors
    
  3. 读取一系列方向,将其表示为向量列表,并按如下方式绘制路径:

    main = do
      rawInput <- readFile "input.txt"
      let vs = [ makeVector dir (read dist)
               | [dir, dist] <- map words (lines rawInput)]
      print vs
      mainWith $ drawPath vs
    
  4. 定义一个辅助函数,根据方向及其对应的距离创建一个向量,如下所示:

    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)
    
  5. 编译代码并按如下方式运行:

    $ 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

如何实现……

  1. 使用以下代码导入相关包:

    import Data.Csv
    import qualified Data.ByteString.Lazy as BSL
    
  2. 定义将作为 CSV 导出的数据关联列表。在这个食谱中,我们将字母和数字配对,如以下代码所示:

    myData :: [(Char, Int)]
    myData = zip ['A'..'Z'] [1..]
    
  3. 运行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,并执行以下步骤:

  1. 如下所示导入相关数据结构:

    import Text.JSON.Yocto
    import qualified Data.Map as M
    
  2. 如下所示定义一个二维点的数据结构:

    data Point = Point Rational Rational
    
  3. Point数据类型转换为 JSON 对象,如下方代码所示:

    pointObject (Point x y) = 
      Object $ M.fromList [ ("x", Number x)
                          , ("y", Number y)]
    
  4. 创建点并构建一个 JSON 数组:

    main = do
      let points = [ Point 1 1
                   , Point 3 5
                   , Point (-3) 2]
      let pointsArray = Array $ map pointObject points
    
  5. 将 JSON 数组写入文件,如下方代码所示:

      writeFile "points.json" $ encode pointsArray
    
  6. 运行代码时,我们会发现生成了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);"

如何实现…

  1. 导入相关库,如下方代码所示:

    {-# LANGUAGE OverloadedStrings #-}
    import Control.Applicative
    import Database.SQLite.Simple
    import Database.SQLite.Simple.FromRow
    
  2. TestField(我们将要存储的数据类型)创建一个FromRow类型类的实现,如下方代码所示:

    data TestField = TestField Int String deriving (Show)
    instance FromRow TestField where
      fromRow = TestField <$> field <*> field
    
  3. 创建一个辅助函数,用于仅为调试目的从数据库中检索所有数据,如下方代码所示:

    getDB :: Connection -> IO [TestField]
    
    getDB conn = query_ conn "SELECT * from test"
    
  4. 创建一个辅助函数,将字符串插入数据库,如下方代码所示:

    insertToDB :: Connection -> String -> IO ()  
    insertToDB conn item = 
      execute conn 
      "INSERT INTO test (str) VALUES (?)" 
      (Only item)
    
  5. 如下所示连接到数据库:

    main :: IO ()
    
    main = withConnection "test.db" dbActions
    
  6. 设置我们希望插入的字符串数据,如下方代码所示:

    dbActions :: Connection -> IO ()
    
    dbActions conn = do
      let dataItems = ["A", "B", "C"]
    
  7. 将每个元素插入数据库,如下方代码所示:

      mapM_ (insertToDB conn) dataItems
    
  8. 使用以下代码打印数据库内容:

      r <- getDB conn
      mapM_ print r 
    
  9. 我们可以通过调用以下命令验证数据库中是否包含新插入的数据:

    $ 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的新文件,并执行以下步骤:

  1. 按照如下方式导入库:

    {-# LANGUAGE OverloadedStrings, ExtendedDefaultRules #-}
    import Database.MongoDB
    import Control.Monad.Trans (liftIO)
    
  2. 如下所示定义一个表示人物姓名的数据类型:

    data Person = Person { first :: String 
                         , last :: String }
    
  3. 设置我们希望存储的几个数据项,如下所示:

    myData :: [Person]
    myData = [ Person "Mercury" "Merci"
             , Person "Sylvester" "Smith"]
    
  4. 连接到 MongoDB 实例并存储所有数据,如下所示:

    main = do
        pipe <- runIOE $ connect (host "127.0.0.1")
        e <- access pipe master "test" (store myData)
        close pipe
        print e
    
  5. 按照以下方式将Person数据类型转换为适当的 MongoDB 类型:

    store vals = insertMany "people" mongoList 
      where mongoList = map 
                        (\(Person f l) -> 
                          ["first" =: f, "last" =: l]) 
                        vals
    
  6. 我们必须确保 MongoDB 守护进程正在运行。如果没有,我们可以使用以下命令创建一个监听我们选择目录的进程:

    $ mongod --dbpath ~/db
    
    
  7. 运行代码后,我们可以通过以下命令检查操作是否成功,方法是访问 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的新文件中,执行以下步骤:

  1. 按照以下方式导入所有必要的库:

    {-# 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
    
  2. 按照以下代码片段将字符串列表转换为 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
    
  3. 创建一个字符串列表,并按以下方式将其渲染为 HTML 网页:

    main = do    
      let movies = [ "2001: A Space Odyssey"
                   , "Watchmen"
                   , "GoldenEye" ]
      let html = renderHtml $ dataInList "list of movies" movies
      BSL.writeFile "index.html" $ html
    
  4. 运行代码以生成 HTML 文件,并使用浏览器打开,如下所示:

    $ runhaskell Main.hs
    
    

    输出结果如下:

    How to do it…

另见

要将数据呈现为 LaTeX 文档并最终生成 PDF,请参考以下创建一个 LaTeX 表格来展示结果的做法。

创建一个 LaTeX 表格来展示结果

本做法将通过编程方式创建一个 LaTeX 表格,以便于文档的创建。我们可以从 LaTeX 代码生成 PDF 并随意分享。

准备工作

从 cabal 安装HaTeX,Haskell LaTeX 库:

$ cabal install LaTeX

如何操作…

创建一个名为Main.hs的文件,并按照以下步骤进行:

  1. 按照以下方式导入库:

    {-# LANGUAGE OverloadedStrings #-}
    import Text.LaTeX
    import Text.LaTeX.Base.Class
    import Text.LaTeX.Base.Syntax
    import qualified Data.Map as M
    
  2. 按照以下规格保存一个 LaTeX 文件:

    main :: IO ()
    main = execLaTeXT myDoc >>= renderFile "output.tex"
    
  3. 按照以下方式定义文档,文档被分为前言和正文:

    myDoc :: Monad m => LaTeXT_ m
    
    myDoc = do
      thePreamble
      document theBody
    
  4. 前言部分包含作者数据、标题、格式选项等内容,如下代码所示:

    thePreamble :: Monad m => LaTeXT_ m
    
    thePreamble = do
      documentclass [] article
      author "Dr. Databender"
      title "Data Analyst"
    
  5. 按照以下方式定义我们希望转换为 LaTeX 表格的数据列表:

    myData :: [(Int,Int)]
    
    myData = [ (1, 50)
             , (2, 100)
             , (3, 150)]
    
  6. 按照以下方式定义正文:

    theBody :: Monad m => LaTeXT_ m
    
    theBody = do
    
  7. 设置标题和章节,并按照以下代码片段构建表格:

      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 
    
  8. 运行以下命令后,我们可以获取 PDF 并查看:

    $ runhaskell Main.hs
    $ pdflatex output.tex
    
    

    输出结果如下:

    How to do it…

另见

要构建一个网页,请参考前面的做法,标题为在 HTML 网页中展示结果

使用文本模板个性化消息

有时我们有一个包含大量用户名和相关数据的列表,并且希望单独向每个人发送消息。本做法将创建一个文本模板,该模板将从数据中填充。

准备工作

使用 cabal 安装template库:

$ cabal install template

如何操作…

在一个名为Main.hs的新文件中执行以下步骤:

  1. 按如下方式导入相关库:

    {-# 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
    
  2. 定义我们处理的数据如下:

    myData = [ [ ("name", "Databender"), ("title", "Dr.") ],
               [ ("name", "Paragon"), ("title", "Master") ],
               [ ("name", "Marisa"), ("title", "Madam") ] ]
    
  3. 定义数据模板如下:

    myTemplate = template "Hello $title $name!"
    
  4. 创建一个辅助函数,将数据项转换为模板,如下所示:

    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
    
  5. 将每个数据项与模板匹配,并将所有内容打印到文本文件中,如下代码片段所示:

    main :: IO ()
    main = do
      let res = map (\d -> toStrict ( 
                      render myTemplate (context d) )) myData
      TIO.writeFile "messages.txt" $ T.unlines res
    
  6. 运行代码以查看生成的文件:

    $ 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,并插入接下来步骤中解释的代码:

  1. 按如下方式导入相关库:

    import Data.Array.Repa.IO.Matrix
    import Data.Array.Repa
    
  2. 定义一个 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 ]
    
  3. 将矩阵写入文件,如下所示:

    main = writeMatrixToTextFile "output.dat" x
    

工作原理……

矩阵简单地表示为其元素的列表,按行优先顺序排列。文件的前两行定义了数据类型和维度。

还有更多内容……

要从此文件中读取矩阵,我们可以使用readMatrixFromTextFile函数来检索二维矩阵。更多关于此包的文档可以在hackage.haskell.org/package/repa-io找到。