app of incanter(2)
『震災ビッグデータ』(NHK出版)なんて本を読んだんじゃなくて眺めてみた。 タイトル通り震災で生まれた膨大なデータを可視化して分析したものだ。過去3回の 放送で公開出来なかったものも混じっているとの事。おいらは、その放送自体も 知らなかったから興味深く拝見しましたよ。
可視化と言うと、近頃では頻繁にやって来た台風や集中豪雨で、どこそこのエリアが降り始めからの 雨量の合計がXXXXmmを超えましたなんてマップにmapしたグラフを見せられる事が多い。 どの地域が凄かったのかが色違いのグラフになってて、非常にインパクトがありぞっと します。
この手法を震災データに適用したら、、、
やっぱりぞっとするデータが並んでいました。けど、時間を経て道路が通行出来るように なったデータを見せられると、日本人の不屈な精神と言うか、この地、災害の国に 生まれ育ったDNAを感じぜずにはいられない。
この道路の通行状態は、ナビに連動した情報を元に構成したとか。一昔では考えられない 情報だな。こういうのがさっと集まってきて、後はどうまとめて、どう見せるかという コンピュータ技術、いや人間の視覚に訴える技術になるんだな。
ツイターで叫ぶ人達の語句を集約して、時系列にまとめたグラフも出てた。時間を 追って、叫ばれる内容が変わっていくのが見てとれて、非常に興味深い。 分析に、マップ・リディースの手法なんですかね。
そう、clojureで言うなら、map reduceって、まんまのものが用意されてるじゃないですか。 時代の寵児ですよ。そして視覚化と言ったらグラフですな。Webの上でやるならD3.jsかな。
とか思っていたら、clojure用の C2: Clojure(Script) data visualization なんてのも有るのね。ちっとも知らんかったぞ。
読書メーター 7冊 / 2012頁 / 11700円
Clojure study
Pythonのまね
前回、ちょいと血圧データの表示アプリをincanterを使って作ろうとして、下調べをした。 既にPythonで作ったやつがあるんで、それを移植してみる。(我ながら進化が無いな)
: (defn cat [x] (let [cs (or (first x) "30") cn (Integer/valueOf cs) ampm (or (first (rest x)) nil)] (tail cn BLD))) (def REGD {:bye sayonara, :yymm cngym, :load myload, :save mysave, :cat cat }) (defn cmdline [] (print (str YYMM "> ")) (flush) (re-seq #"[.\w]+" (read-line))) (defn cmdloop [] (when RUNNING (let [cmd (cmdline) fv (first cmd) args (rest cmd)] (if (re-find #"\d+" fv) (add-data fv args) (if ((keyword fv) REGD) (((keyword fv) REGD) args) (print (keys REGD)))) (cmdloop)))) (defn -main "bld app" [& args] (myload ()) (def RUNNING true) (cmdloop))
自前のreplであるcmdloopを定義して、後は使いそうなコマンドを付け足して行くって方針。 lispとか自作する時、readとprintをまず作り、次はeval。それから関数類を膨らませて行くのが 都合よい。それに習った訳だ。
それで、このアプリが出来た時に、どんな具合に動くのか確認しようと思って、 取り合えず走らせてみると、、、
sakae@uB:~/bld$ time lein run real 0m26.201s user 0m17.344s sys 0m8.384s
起動するまでに、めっぽう時間がかかる。おまけに、データ入力しようとすると
sakae@uB:~/bld$ time lein run 1409> 1204 120 60 55 1409> Exception in thread "main" java.lang.NullPointerException at java.util.regex.Matcher.getTextLength(Matcher.java:1234) at java.util.regex.Matcher.reset(Matcher.java:308) at java.util.regex.Matcher.<init>(Matcher.java:228) at java.util.regex.Pattern.matcher(Pattern.java:1088) at clojure.core$re_matcher.invoke(core.clj:4386) at clojure.core$re_find.invoke(core.clj:4438) at bld.core$cmdloop.invoke(core.clj:68) at bld.core$cmdloop.invoke(core.clj:73) :
Javaがヌルポの牙を剥いてきたじゃないですか。もうイヤになっちゃうな。 26秒もかかって起動して、あてがいぶちのコマンドしか実行出来ないなんて、ショックだなあ。 考えを改めて、R(のコマンドライン)みたいに、replを動かしてしまった方が いいかな。
もくもくとrepl風に
そんな訳で先のコードを大幅に改修したよ。まず手を付けたのは、CSVファイルの入出力と それの更新系。データベースアプリの基本系になるかな。
(def DBF "2013.csv") ; default csv-file (defn myload ([] (def BLD (read-dataset DBF :header true))) ([csvf] (def BLD (read-dataset csvf :header true)))) (defn add-data [ym h t] (let [hs (if (= (count h) 3) (str "0" h) h) hv (Integer/valueOf (str ym hs)) av (cons hv (map #(Integer/valueOf %) t))] (def BLD (conj-rows BLD av)))) (defn mysave ([] (save BLD DBF :header ["ymdh" "hi" "low" "pls"])) ([csvf] (save BLD csvf :header ["ymdh" "hi" "low" "pls"])))
まさに、read-modify-writeです。この場合のmodifyはデータの追加の意味ね。本当の変更は 、このアプリの外でviでも使ってcsvファイルを変更してください。テキストが対象だと、 どうにでもなるので楽で良いぞ。
グローバルにアクセス出来る変数にBLDって名前を使っちゃったけど、これ普通のLisp習慣では、*bld*とか するのが常識。常識にとらわれないclojure上だから、許してねって態度です。
次は、add-dataを呼び出す系ね。
(defn cmdline [ym] (print (str ym "> ")) (flush) (let [line (read-line)] (if (empty? line) (cmdline ym) (re-seq #"[.\w]+" line)))) (defn input "Input data on ym, fin to save data into default csv-file" [ym] (loop [dummy (Integer/valueOf ym)] ; dummy for syntax (let [cmd (cmdline ym) fv (first cmd) args (rest cmd)] (if (re-find #"\d+" fv) (add-data ym fv args)) (if (re-find #"fin" fv) (mysave) (recur ym)))))
cmdline()はinput()の補助関数。両関数とも、再帰を使ってループを実現してる。 cmdlineの方は、一行入力して、それを単語に分解して、リストにして帰す。未入力で ヌルポを避ける為の対策として、再起だーーー。これがLisp伝統の再帰。
inputの方は、clojure様ご推薦の今風な再帰。ひょっとしてYコンビネータを意識してね? この関数の中で、入力終了のfinを検出して、CSVファイルを更新してる。それまでに入力 されたものは、メモリー上にしか無いっていう、危ない設計です。
sakae@uB:~/bld$ lein repl : bld.core=> (input 1408) 1408> 921 123 75 60 1408> 1004 120 73 55 1408> fin nil
こんな風に何年何月の入力をするんだいと、宣言してから入力します。入力値の一番左側は、 日時ですが、3桁でも受けるようにしてます。
incanterやるならLINQしょ
次は、読み込まれたCSVファイルから、必要な部分を抜き出してくるルーチンだな。 incanter/coreの関数を前回眺めたけど、whereだとかjoinだとか、何となくSQLっぽくね?
SQLを関数型言語に適用するって事で、昔マイクロソフトのシンパの人がLINQなんてのを 自慢してたな。そんな訳で、新たに探してみたら、 C# やるなら LINQ を使おう に行き当たりましたよ。
今回は取り合えずwhere系が有ればいいな。
(defn am [ds] ($where ($fn [ymdh] (< (mod ymdh 100) 12)) ds)) (defn pm [ds] ($where ($fn [ymdh] (>= (mod ymdh 100) 12)) ds)) (defn from [start ds] (let [st (* start 10000)] ($where {:ymdh {:$gt st}} ds))) (defn hd ([ds] (head 30 ds)) ([n ds] (head n ds))) (defn tl ([ds] (tl 30 ds)) ([n ds] (tail n ds)))
必要そうなものを数種書いてみました。ymdhは血圧を測定した年月日時を数値(コード)化した ものになってますんで、検索条件もそれに合わせています。
たとえばamは、起床時(午前中)のデータを拾い出します。yudmを100で割って下位2桁を取り出し、 12(時)以前の物が相当って具合。fromは、年月で、それ以降のもの。hdは最初から幾つか。 tlは最後から幾つかです。
前の方でも出てきたけど、同名関数でもアリティー(引数の数)によって、別のbodyを実行する という、他にLispには見られない特徴がclojureには備わっています。これを旨く使うと、 省略時の値ってのを簡単に実行出来ます。
hdみたいに陽に値を指定して底関数を呼び出してもいいですし、tlみたいに、ちょっとズル して再帰しちゃってもOKです。複雑な関数の場合は再帰しちゃった方が、間違いがなくて、かつ 楽が出来ると思いますけどね。
パイプ族
これで、必要なデータをフィルターで選別出来る目処が付いた。後は、これらをどう組み合わせる かだな。そこで、 スレッドマクロって? ですよ。
難しい説明が書いてあるけど、ようするにパイプ族です。
(->> (from 1305 BLD) (am) (hd 6))
これ、(20)13年5月の午前中のデータを6個分だけ選び出して表示してねって意味。古来の Lispでは
(hd 6 (am (from 1305 BLD)))
こんな風に書いていたものです。これで少しはLispの前置法の呪縛から逃れられるかな。
このマクロ、関数が返す計算結果が、次の関数の入力になって、、、って具合に作用する んだけど、別なアプローチを有ります。
使う関数を全部集めておいてそれを合成しとくってものも用意されてます。compってやつね。 そして、合成した関数に引数を作用させる方法。
user=> (def negative-quotient (comp - /)) #'user/negative-quotient user=> (negative-quotient 8 3) -8/3
割り算したのをマイナスにするって関数合成して名前を付ける。そしてそれを実行。
user=> (filter (comp not zero?) [0 1 0 2 0 3 0 4]) (1 2 3 4)
零以外を抽出してねってやつ。合成元となる関数のアリティーが一致してたら、compの出番が 回ってくるでしょう。
パイプの終端装置
こうして、選んだデータを表示出来るようになった。けど、本当に欲しいのは、そのデータを 見える化する事だったりします。
見える化って言うと、すぐグラフって思いつくけど、CUI派のおいらは、要約だな。 要約って言ったら、summuryですよ。incnaterの作者はちゃんと用意してくれていました。
bld.core=> (pprint (rest (summary BLD))) ({:col :hi, :min 93, :max 138, :mean 114.68975069252078, :median 115.0, :is-numeric true} {:col :low, :min 50, : :median 60.0, :is-numeric true})
こんな具合です。けど、残念な事に標準偏差が抜けててRに負けてると思うんよ。それに 結果が縦に伸びてて醜いぞと。だったら、おまえが書けよ。へぃ、ガッテン承知!
(defn ss [ds] (with-data ds (letfn [(d [f k] (format "%4d " (apply f ($ k)))) (f [f k] (format "%6.1f" (f ($ k)))) (fs [s] (apply str (for [k [:hi :low :pls]] (f s k))))] (print (str "size: " (nrow ds) "\n" "min: " (d min :hi) (d min :low) (d min :pls) "\n" "median:" (fs median) "\n" "max: " (d max :hi) (d max :low) (d max :pls) "\n" "mean: " (fs mean) "\n" "sd: " (fs sd) "\n")))))
で、出来栄えは?
bld.core=> (->> (from 1310 BLD) (am) (tl) (ss)) size: 30 min: 104 64 54 median: 120.0 71.0 56.0 max: 131 77 60 mean: 119.6 70.9 56.7 sd: 7.1 2.9 1.7 nil bld.core=> (->> (from 1310 BLD) (pm) (tl) (ss)) size: 30 min: 99 56 57 median: 116.5 64.5 64.0 max: 130 75 74 mean: 114.8 64.4 64.9 sd: 8.5 5.2 3.7 nil
センス無いなんて言わないでね。それより、注目は、関数の中だけで使う内部関数を、letfnで 定義出来る事を発見したおいらを褒めてあげましょう。(もっと褒めるべくは、clojureの 作者さんですがね)
内部関数 fsみたいにforを使うと、同じ事の繰り返すを綺麗に整理出来ます。(って事で、 比較広告を出しておきました)
広告って言えば、最初の方に出てきたやつ、たとえば
(defn mysave ([] (save BLD DBF :header ["ymdh" "hi" "low" "pls"])) ([csvf] (save BLD csvf :header ["ymdh" "hi" "low" "pls"])))
これも悪い例。レールの道を踏み外しているな。ruby Railsの言う事にゃ、同じ事を何度も書くなとね。 (除く、コピペ大好き人間)
再起しましょ、じゃなくて再帰しましょですな。再帰の基底条件をまず書いてしまいます。 この場合なら、引数有りのbodyを書く。続いて、引数無しの条件で、引数相当を省略値で 埋めるって具合。
(defn mysave ([csvf] (save BLD csvf :header ["ymdh" "hi" "low" "pls"])) ([] (mysave BLD DBF)))
これで、ややこしい部分が消えたよ。
それから、forの返り値はリストになるんで、文字列で欲しい場合は、apply str してね ってのは、clojure本にしっかり書いてありましたんで、ちゃんと使いましたよ。
後は、いよいよグラフ化だ。
(defn mybox [ds] (with-data ds (doto (box-plot :hi :legend true) (add-box-plot :low) (add-box-plot :pls) view))) (defn myline [ds] (with-data ds (let [x (range (nrow ds)) p1 (xy-plot x :hi)] (view p1) (add-lines p1 x ($ :low)) (add-lines p1 x ($ :pls)))))
myboxってのは、オイラーが書いた要約関数ssのGUI版ね。mylineは、定番の折れ線グラフ。
去年のデータを折れ線グラフにして眺めていたら、低血圧と言うか拡張時血圧にあきらかに 段がある事が判明。何で?って考えたら、去年、血圧計が壊れて新しいのにしたんだった。
血圧計によって、測定ポイントが違うんだな。
また、こちらは医者への提出用ファイル作成のためのもの。ああ、pngファイルを作って それを印刷って流れね。
(defn mypng [ttl pngfn ds] (with-data ds (letfn [(f [f k] (format "%6.1f" (f ($ k)))) (fs [s] (apply str (for [k [:hi :low :pls]] (f s k))))] (let [x (range (nrow ds)) p1 (xy-plot x :hi :title ttl :x-label "old <- Date -> new" :y-label "Value")] (add-text p1 25 10 (str "Mean: " (fs mean))) (add-text p1 25 5 (str "Sd: " (fs sd))) (add-lines p1 x ($ :low)) (add-lines p1 x ($ :pls)) (save p1 pngfn))))) (defn ampng [] (->> (am BLD) (tl 50) (mypng "at wake-up" "am.png"))) (defn pmpng [] (->> (pm BLD) (tl 50) (mypng "at night" "pm.png")))
グラフ中に平均と偏差を入れてみた。折角、整形してるんだけど、表示に使われている フォントが等幅じゃないものだから、微妙にずれてしまっている。表示座標系は、グラフの 数値座標になってて、文字列の中点を指定するようだ。
試し印字したら、プリンターのインクのとある色が欠乏してて、とんでもない色で印字 されてた。また、プリンター屋を儲けさせないといけないのか。癪に障るなあ。
app of incanter
今までのコードのまとめ
(ns bld.core (:gen-class)) (use 'clojure.repl) (use 'clojure.pprint) (use '(incanter core stats io charts datasets)) (def DBF "2013.csv") ; default csv-file (defn myload ([] (def BLD (read-dataset DBF :header true))) ([csvf] (def BLD (read-dataset csvf :header true)))) (defn add-data [ym h t] (let [hs (if (= (count h) 3) (str "0" h) h) hv (Integer/valueOf (str ym hs)) av (cons hv (map #(Integer/valueOf %) t))] (def BLD (conj-rows BLD av)))) (defn mysave ([] (save BLD DBF :header ["ymdh" "hi" "low" "pls"])) ([csvf] (save BLD csvf :header ["ymdh" "hi" "low" "pls"]))) (defn am [ds] ($where ($fn [ymdh] (< (mod ymdh 100) 12)) ds)) (defn pm [ds] ($where ($fn [ymdh] (>= (mod ymdh 100) 12)) ds)) (defn from [start ds] (let [st (* start 10000)] ($where {:ymdh {:$gt st}} ds))) (defn hd ([ds] (head 30 ds)) ([n ds] (head n ds))) (defn tl ([ds] (tl 30 ds)) ([n ds] (tail n ds))) (defn cmdline [ym] (print (str ym "> ")) (flush) (let [line (read-line)] (if (empty? line) (cmdline ym) (re-seq #"[.\w]+" line)))) (defn input "Input data on ym, fin to save data into default csv-file" [ym] (loop [dummy (Integer/valueOf ym)] ; dummy for syntax (let [cmd (cmdline ym) fv (first cmd) args (rest cmd)] (if (re-find #"\d+" fv) (add-data ym fv args)) (if (re-find #"fin" fv) (mysave) (recur ym))))) (defn mybox [ds] (with-data ds (doto (box-plot :hi :legend true) (add-box-plot :low) (add-box-plot :pls) view))) (defn myline [ds] (with-data ds (let [x (range (nrow ds)) p1 (xy-plot x :hi)] (view p1) (add-lines p1 x ($ :low)) (add-lines p1 x ($ :pls))))) (defn mypng [ttl pngfn ds] (with-data ds (letfn [(f [f k] (format "%6.1f" (f ($ k)))) (fs [s] (apply str (for [k [:hi :low :pls]] (f s k))))] (let [x (range (nrow ds)) p1 (xy-plot x :hi :title ttl :x-label "old <- Date -> new" :y-label "Value")] (add-text p1 25 10 (str "Mean: " (fs mean))) (add-text p1 25 5 (str "Sd: " (fs sd))) (add-lines p1 x ($ :low)) (add-lines p1 x ($ :pls)) (save p1 pngfn))))) (defn ampng [] (->> (am BLD) (tl 50) (mypng "at wake-up" "am.png"))) (defn pmpng [] (->> (pm BLD) (tl 50) (mypng "at night" "pm.png"))) (defn ss [ds] (with-data ds (letfn [(d [f k] (format "%4d " (apply f ($ k)))) (f [f k] (format "%6.1f" (f ($ k)))) (fs [s] (apply str (for [k [:hi :low :pls]] (f s k))))] (print (str "size: " (nrow ds) "\n" "min: " (d min :hi) (d min :low) (d min :pls) "\n" "median:" (fs median) "\n" "max: " (d max :hi) (d max :low) (d max :pls) "\n" "mean: " (fs mean) "\n" "sd: " (fs sd) "\n"))))) (defn myrepl [] (print "myrepl> ") (flush) (println (eval (read))) (myrepl)) ;; for lein uberjar (defn -main "bld app" [& args] (myload) (myrepl)) ;(-main) ;;; for lein repl
単独化
lein replで起動すると、プロンプトが出てくるまで27秒もかかる。これはたまらん。 という事で、leinを使わない化してみる。それには、上記のように自前のrereplを定義 する。そして、
[sakae@manjaro bld]$ lein uberjar Compiling bld.core Compiling bld.core Created /home/sakae/src/bld/target/uberjar/bld-0.1.0-SNAPSHOT.jar Created /home/sakae/src/bld/target/uberjar/bld-0.1.0-SNAPSHOT-standalone.jar
これで44Mという巨大なアプリが出来上がる。それと引き換えに起動時間は5秒に短縮される。 そこまではいいんだけど、、、
[sakae@manjaro uberjar]$ java -jar bld-0.1.0-SNAPSHOT-standalone.jar myrepl> (+ 1 2 3 4) 10 myrepl> (ss BLD) Exception in thread "main" java.lang.RuntimeException: Unable to resolve symbol: ss in this context, compiling:(NO_SOURCE_PATH:2:1) at clojure.lang.Compiler.analyze(Compiler.java:6380) at clojure.lang.Compiler.analyze(Compiler.java:6322) :
myevalの中から外が見えない状態。何故????? その場でソースをコンパイルするようなお節介モードが動いていて、ソースが見当たらんって 事なのかな。
まてまて、単にbld.coreが見えていないんじゃねぇ! だったら、myreplを呼ぶ前に、(use 'bld.core)しとくかな。
[sakae@manjaro uberjar]$ java -jar bld-0.1.0-SNAPSHOT-standalone.jar myrepl> (->> (from 1311 BLD) (am) (hd) (ss)) size: 30 min: 109 65 53 median: 120.0 71.0 56.5 max: 129 77 63 mean: 119.5 71.5 56.8 sd: 5.7 2.8 2.8 nil
今度は旨くいった。やったね。後はながなが入力してるコマンドをまとめておく事だな。 通常は、10日分のデータを入力して、過去50日分ぐらいのデータをグラフにして確認 するぐらいだから。
それから、incanter.coreとかが提供してるのは、そのままじゃ使えない。useすれば使える ようになるはずだけど、それじゃ元の木阿弥になっちゃうな。やってみたけどロードに膨大な 時間がかかる訳でも無いので、まさかの為にuseしとくか。
元凶はleinの便利さの上に無駄時間を起動に費やしていたって事です。