2016年8月25日木曜日

Haskellでさくっとデータ処理をする

Haskellの強みの一つは、関数合成やパターンマッチを使って複雑なロジックをバグを抑えながら書けることだと思います。

さくっとデータ処理をやりたい時に使えそうなところをまとめました。

まずはシンプルに

ghciとdoctest

対話型のghciを使うと試しながらできるのでオススメです。

そこである程度固まってきたら、hsファイルにまとめたくなると思います。

doctestは、簡単な実行環境としても使えます。 Haskellの単体テスト最前線

Listを使う

列ごとにListとして取り込みます。

行は、Listをzipしてtupleを作ると柔軟に使えます。

Record構文は、さくっとやりたい時には、手間がかかります。

>>> id = ["a","b","c"]
>>> num = [1,2,3]
>>> data = zip id num
>>> data
[("a", 1), ("b", 2), ("c", 3)]

モナドについて

IOは出力に必要ですが、それ以外は最低限必要なものだけを使います。

実践的にモナドを使う方針と具体的な使い方が参考になります。

Haskellで競技プログラミング IO編

Maybeは、欠損値がある場合に使います。

型宣言しない

型宣言をつけると格好がつきますが、さくっとやる場合は、基本つけなくてもいいと思います。

少し実践的に

Vector

パフォーマンスを考えたらぜひ使いたいライブラリです。

List操作は楽ですが、パフォーマンスがネックになる場合があります。

その時に使えます。

また、データ処理系のライブラリでは、デフォルトで使われています。

https://github.com/haskell/vector

csvを使う

cassavaがシンプルに使えて便利です。

https://github.com/hvr/cassava

サクッと使うならば、ヘッダなしがオススメ。 レコードごとに処理するサンプルコードです。

{-# LANGUAGE ScopedTypeVariables #-}

import qualified Data.ByteString.Lazy as BL
import Data.Csv
import qualified Data.Vector as V

main :: IO ()
main = do
    csvData <- BL.readFile "salaries.csv"
    case decode NoHeader csvData of
        Left err -> putStrLn err
        Right v -> V.forM_ v $ \ (name, salary :: Int) ->
            putStrLn $ name ++ " earns " ++ show salary ++ " dollars"

ここで、decode NoHeader csvDataは、Vectorを返すので、forM_以外のVectorの関数にも慣れておくといいです。

joinを作ってみた

Haskellはロジック記述力が高いので、シンプルにいろいろと書けます。

SQLのjoinに相当する操作(結合条件を指定して、二つの表をつなげる)を探しましたが、意外とありませんでした。

実際にデータを処理するときは、キー結合したいときは結構多いため、書いてみました。

キーを条件に要素をつなげてますが、結合条件を関数で渡してもよかったと思います。 inner joinは、zip, filter, intersectなどでそのまま書けます。

import Data.List
import Data.Maybe as M

-- | Join 
--
-- Examples:
--
-- >>> fullJoin [1,2] [10,20] [2,3] [200, 300]
-- [(1,Just 10,Nothing),(2,Just 20,Just 200),(3,Nothing,Just 300)]
--
-- >>> leftJoin [1,2] [10,20] [2,3] [200, 300]
-- [(1,Just 10,Nothing),(2,Just 20,Just 200)]
--
-- >>> rightJoin [1,2] [10,20] [2,3] [200, 300]
-- [(2,Just 20,Just 200),(3,Nothing,Just 300)]
-- 
-- >>> innerJoin [1,2] [10,20] [2,3] [200, 300]
-- [(2,Just 20,Just 200)]
--
-- >>> data Row = Row { col1 :: Integer , col2 :: Integer } deriving (Eq, Show)
-- >>> type Table = [Row]
-- >>> let table = [Row {col1 = 10, col2 = 11}, Row{col1 = 20, col2 = 21}]
-- >>> fullJoin [1,2] table [2,3] [200,300]
-- [(1,Just (Row {col1 = 10, col2 = 11}),Nothing),(2,Just (Row {col1 = 20, col2 = 21}),Just 200),(3,Nothing,Just 300)]
--
-- >>> let { f i (Just x) Nothing = (i, col1 x, col2 x, 0); f i (Just x) (Just y) = (i, col1 x, col2 x, y); f i Nothing (Just y) = (i, 0, 0, y); f i Nothing Nothing = (i, 0, 0, 0); }
-- >>> fullJoinWith f [1,2] table [2,3] [200,300]
-- [(1,10,11,0),(2,20,21,200),(3,0,0,300)]

fullJoinWith f ks1 vs1 ks2 vs2 = map (\(i, x, y) -> f i x y) $ fullJoin ks1 vs1 ks2 vs2

leftJoin :: Ord k => [k] -> [a] -> [k] -> [b] -> [(k, M.Maybe a, M.Maybe b)]
leftJoin ks1 vs1 ks2 vs2 = _join ks1 ks1 vs1 ks2 vs2

rightJoin :: Ord k => [k] -> [a] -> [k] -> [b] -> [(k, M.Maybe a, M.Maybe b)]
rightJoin ks1 vs1 ks2 vs2 = map (\(x, y, z) -> (x, z, y)) $ _join ks2 ks2 vs2 ks1 vs1

fullJoin :: Ord k => [k] -> [a] -> [k] -> [b] -> [(k, M.Maybe a, M.Maybe b)]
fullJoin ks1 vs1 ks2 vs2 = _join (union ks1 ks2) ks1 vs1 ks2 vs2

innerJoin :: Ord k => [k] -> [a] -> [k] -> [b] -> [(k, M.Maybe a, M.Maybe b)]
innerJoin ks1 vs1 ks2 vs2 = _join (intersect ks1 ks2) ks1 vs1 ks2 vs2

_join :: Ord k => [k] -> [k] -> [a] -> [k] -> [b] -> [(k, M.Maybe a, M.Maybe b)]
_join ids ks1 vs1 ks2 vs2 = zipWith(\(x, y) (_, y2) -> (x, y, y2)) zs1 zs2
  where
    zs1 = mkZs ks1 vs1 
    zs2 = mkZs ks2 vs2
    mkZs ks vs = sortByFst . filterByFst . (++) (zipWith(\k v -> (k, Just v)) ks vs) $ zip (ids \\ ks) (repeat Nothing) 
    sortByFst = sortBy (\(x1, _) (x2, _) -> compare x1 x2)
    filterByFst = filter (flip elem ids . fst) 

0 件のコメント:

コメントを投稿