👻

Tablecloth のDataset を Vega-Lite で可視化する時の Datetimeのあつかいについて

2025/03/10に公開

Clojureで Vega-Lite を使って可視化した時に、色々つまづいたのでここにメモしておきます。

Vega-Liteの基本構造

Vega-Liteで可視化するには、JSON形式で「データの見た目をどうするか?」を記述して渡します。主な構成要素は以下の通りです。

  1. data : どのデータを使うか
  2. mark : グラフの種類(line, bar, など)
  3. encoding:データをグラフの要素にどう対応させるか。(x軸、y軸、色など)。

たとえば、Line Chart with Point Markers | Vega-Lite の JSON を例に説明すると、

{
  "$schema": "https://vega.github.io/schema/vega-lite/v5.json",  // 仕様のバージョン
  "description": "Stock prices of 5 Tech Companies over Time.",  // グラフの説明
  "data": {"url": "data/stocks.csv"},  // データソース(CSVファイル)
  "mark": {
    "type": "line",  // 折れ線グラフ
    "point": true    // 折れ線に点を追加
  },
  "encoding": {      // データと見た目の対応
    "x": {"timeUnit": "year", "field": "date"},  // x軸:日付から年だけ抽出
    "y": {"aggregate": "mean", "field": "price", "type": "quantitative"},  // y軸:価格の平均
    "color": {"field": "symbol", "type": "nominal"}  // 色:企業ごと
  }
}

という具合になります。

(実際のデータはこちら: https://github.com/vega/vega/blob/main/docs/data/stocks.csv)

encoding がクセを感じる部分なのでもう少し説明します。

  • x軸
    • "field": "date" : データの date 列をデータとして使う
    • "timeUnit": "year" : 日時データから年だけ取り出してX軸に使う。
      • vega-lite は時間のパースと変換を自動で行う。大体のフォーマットには対応できるが2025年3月3日といった特殊な例の場合はフォーマットを明示的に指定する。例:
        "data": {"values": [{"date": "2025年3月3日"}],
                 "format": {"parse": {"date": "%Y年%m月%d日"}}}
        
  • y軸
    • "field": "price": データの price 列をデータとして使う
    • "aggregate": "mean" : price の平均を自動計算してy軸にプロット
    • "type": "quantitative" : データは数値ですよ、と教える。

Vega-Lite の注意点

Vega-Liteは、"data": {"values": [...]} で渡されるデータが基本的にJSON形式(文字列や数値などプリミティブ型)を想定しています。

new Date() のようなオブジェクトが直接渡されても、JSON.stringify される過程で文字列(例: "2000-01-01T00:00:00Z")に変換されます。
(これを知らなかったので下に続く tablecloth/dataset につまづきました 😮‍💨)

Tablecloth の dataset について

Tableclothは、Clojureでデータ操作を簡単に扱うことができるライブラリで、tech.ml.dataset の上に構築されています。

CSVを直接 dataset に変換してくれるなど便利な機能が満載なのですが、完璧ではありません。

たとえば Vega-Lite のExampleで使った以下のファイルを

https://github.com/vega/vega/blob/main/docs/data/stocks.csv#L1-L5

tc/dataset で dataset に変換した時、日付データはパースしてくれません。

(-> (tc/dataset "https://raw.githubusercontent.com/vega/vega/refs/heads/main/docs/data/stocks.csv" {:key-fn keyword})
    (tc/info :columns))
https://raw.githubusercontent.com/vega/vega/refs/heads/main/docs/data/stocks.csv :column info [3 4]:

| :categorical? | :name   | :datatype | :n-elems |
| ------------- | ------- | --------- | -------: |
| true          | :symbol | :string   |      560 |
| true          | :date   | :string   |      560 |
|               | :price  | :float64  |      560 |

仕方ないので日付データを yyyy-mm-dd に変換したものを作って

https://gist.github.com/shinseitaro/543b7c398400ced1ed87b8edd685c170

tc/dataset で dataset に変換するとパースしてくれます。

(-> (tc/dataset "https://gist.githubusercontent.com/shinseitaro/543b7c398400ced1ed87b8edd685c170/raw/73ee2bd7e93cda41a80ddfc55f053f26dceb7c36/stock-2.csv" {:key-fn keyword})
    (tc/info :columns))

| :categorical? | :name   | :datatype          | :n-elems |
| ------------- | ------- | ------------------ | -------: |
| true          | :symbol | :string            |      560 |
|               | :date   | :packed-local-date |      560 |
|               | :price  | :float64           |      560 |

ただし、変換してくれるがゆえに、tablecloth + Vega-Lite を使って可視化する時に逆に困ることになりました🫠

date の対処方法

MMM d yyyy のフォーマットのファイルを dataset で使う

この場合は Vega-Lite の例に従ってかけばOKです。ただし、:data にわたすデータはマップに変換する必要があります。(tc/rows DS :as-maps) を使えばかんたんにできます。

(ns core
  (:require [tablecloth.api :as tc]
            [scicloj.kindly.v4.kind :as kind]))

(def data 
 (-> (tc/dataset "https://raw.githubusercontent.com/vega/vega/refs/heads/main/docs/data/stocks.csv" 
       {:key-fn keyword})
     (tc/rows :as-maps)))

(-> {:data {:values data}
      :mark {:type "line"
             :point true}
      :encoding {:x {:field "date" :timeUnit "year"}
                 :y {:field "price" :aggregate "mean" :type "quantitative"}
                 :color {:field "symbol" :type "nominal"}}}
    kind/vega-lite)

date型に変換されたデータを使う

tc/dataset が変換してくれたデータを使って上記と同じことをしたい場合 date 列をプリミティブなデータに変えて渡す必要があります。しかも、今回の場合、株価を年ごと group-by して平均を出す必要もあります。

ちょっと頭がクラクラしますが、コードをみたら更にクラクラすると思います。

(def data 
 (-> (tc/dataset "https://gist.githubusercontent.com/shinseitaro/543b7c398400ced1ed87b8edd685c170/raw/73ee2bd7e93cda41a80ddfc55f053f26dceb7c36/stock-2.csv" 
       {:key-fn keyword})
     (tc/group-by (juxt :symbol #(dt/long-temporal-field :years (% :date))))
     (tc/aggregate #(dfn/mean (% :price)))
     (tc/rename-columns {:$group-name-0 :symbol
                         :$group-name-1 :year})
     (tc/rows :as-maps)))

(-> {:data {:values data}
      :mark {:type "line"
             :point true}
      :encoding {:x {:field "year"}
                 :y {:field "summary"  :aggregate "mean"}
                 :color {:field "symbol" :type "nominal"}}}
    kind/vega-lite)

この juxt の使い方!!もちろん私が考えた訳ではありません!!!ここにありました。

https://scicloj.github.io/tablecloth/#stocks

data は

[{:symbol "MSFT", :year 2000, "summary" 29.673333333333332} 
 {:symbol "MSFT", :year 2001, "summary" 25.3475}...]

このような型になりますので、:y:field "summary" を渡しています。

date型に変換されたデータでも同様にできた!

(2025/03/17更新)

すみません。↑のようにへんにこねくり回さなくても、普通に出来ました。

(def data (-> (tc/dataset "https://gist.githubusercontent.com/shinseitaro/543b7c398400ced1ed87b8edd685c170/raw/73ee2bd7e93cda41a80ddfc55f053f26dceb7c36/stock-2.csv"
               {:key-fn       keyword 
                :dataset-name "stock"})
              (tc/rows :as-maps)))

(-> {:data     {:values data}
     :mark     {:type  "line"
                :point true}
     :encoding {:x     {:field    "date"
                        :timeUnit "year"}
                :y     {:field     "price"
                        :aggregate "mean"
                        :type      "quantitative"}
                :color {:field "symbol"
                        :type  "nominal"}}}
    kind/vega-lite)

まとめ

TableclothのDatetime型がVega-Liteにそのまま渡ると描画されない問題は、JSONの文字列化が鍵でした。すべては Vega-Liteが扱うのは基本的にJSON形式(文字列) ということを知らなかった起きたつまづきでした。今回は grok に助けてもらって解決できて良かったです。ありがとうAIさん。

(2025/03/17 更新 : いや、今更新したようにdata型を渡してもできることがわかりました。すみません。一週間ウソついてました。)

可視化は pandas を使っているころからそんなに得意ではなかったのですが、仕事上絶対必要なのでがんばります。


ま、つまづいたおかげで頭おかしい(褒めてる) juxt の使い方を発見できて良かったです。

Discussion