Clojure: データフレームまとめ

Clojureの素の文法でやる方法は📝Clojure データ前処理で.

たぶんフレームワークを使うかどうかは扱うデータ量と慣れによる.

🔧cnuernber/dtype-next

ClojureのPrimitiveな算術計算ライブラリ. 📝numpy的な位置づけ.

Clojureの算術計算にはDouble型とDecimal型の扱いで少し面倒なところがあった.

📝Clojure Number

🔧tech.v3.datatype.functional

ベクトル演算をするための関数ライブラリ. 入力はベクトル(🔧tech.ml.datasetのcolumns).

https://cnuernber.github.io/dtype-next/tech.v3.datatype.functional.html

🔧tech.ml.dataset

Clojureのためのデータフレームライブラリ. 🔖Dataframe. 📝pandasみたいな位置づけ.

🔧ptaoussanis/nippy

Clojureオブジェクトをそのまま保存するデータ形式及びライブラリ.

🔧scicloj/tablecloth

🔧cnuernber/dtype-next🔧tech.ml.datasetのラッパーライブラリ.

Sciclojのメンテするライブラリなところは心強い. 💡羽鳥教(dplyr)の影響を受けている.

tablecloth自体の情報は調べてもほぼないのでdtype-nextとtech.ml.datasetの情報も合わせてみたほうがいい. tablecloth APIで帰ってくるオブジェクトの実態はtech.ml.datasetなので, そのAPIも加工に利用できる.

Basics

  • tc/dataset: 新規データセット作成
  • tc/write! 書き出し
  • tc/select-columns: 列の選択.
  • tc/add-columns: 列の追加.
  • tc/update-columns: 列の更新.
    • 変換する関数はtransducerを渡すのがミソ.
    • (tc/update-columns ds :timestamp (partial map str)))

データセット生成

配列から生成

layout指定でas-rowsとas-columnsの2パターンがある.


行ごとのデータ配列をinput.

(-> (map int-array [[1 2] [3 4] [5 6]])
    (into-array)
    (tc/dataset {:layout :as-rows
                 :column-names [:a :b]}))

列(ベクトル, カラム)ごとの配列をinput.

(-> (map int-array [[1 2] [3 4] [5 6]])
    (into-array)
    (tc/dataset {:layout :as-columns}))

データセット操作

一般的なデータフレームはtableclothではDatasetと表現される. dsなどと表記される.

サイズを調べるには?

  • tc/shape: columnsとrowsのサイズ.
    • tc/count-raws
    • tc/count-columns

シリーズを取り出すには?

(DS :hogehoge)のように書く. これは独特の表現.

データセットの性質をみるには?

(tc/info :columns)でカラムの情報をみることができる.

データセットをREPL表示するには?(tc/print-dataset)

(tc/print-dataset ds)をつかう. clojure.pprint/print-tableよりもよい表示.

データセットの型変換

tc/convert-typesをつかう. たとえばAPIから取得した情報が文字列の場合の数値変換の前処理.

(-> DS
    (tc/convert-types {:V1 :float64
                        :V2 :object
                        :V3 [:boolean #(< % 1.0)]
                        :V4 :object})
    (tc/info :columns))

データセットを逆順へ並べ替え

(tc/update-columns ds :all reverse)

データセットからcolumnとrowを指定してデータ取得

get-inを利用する. はじめがcolumn name, 2番目がrow number.

(get-in ds ["wind" 2])

シングルエントリの書き換えはtech.ml.datasetのAPIでは用意されていないので裏技のような方法がドキュメントの下の方に載っている. update-columnsをつかい, if文でindexをマッチさせる.

(def DS (tc/update-columns DS :V2 #(map-indexed (fn [idx v]
                                                   (if (zero? idx) 3 v)) %)))

ファイルI/O

csvファイルを読み書きするには?

;; データ書き出し
(tc/write! DS "DF.csv")
;; データ読み込み, key-fnの指摘でheaderがkeywordになる.
(tc/dataset "DF.csv" {:key-fn keyword})

Column操作

Columnとは配列, ベクトル演算になる.

2つのColumnをベクトル演算のように操作するには 🔧tech.v3.datatype.functional を多用することになる.

Columnsを指定したキーで並べ直すには?

tc/select-coulumnsをつかう.

(-> ds
    (tc/select-columns [:timestamp :side :price :size :pnl]))

tc/reorder-columnsもある. 先頭にきてほしいものだけ指定. たとえばIDやtimestampだけとりあえず先頭にきてあとは気にしない場合につかう.

Columns名をrenameするには?

tc/rename-columnsをつかう.

新規のColumnを追加するには?

tc/add-columnsをつかう. 渡す関数はDataset(行列)を受け取ってColumn(列)を返す関数.

pandasだとdf[‘foo’] = seriesみたいな処理に相当.

(def pnl
  (-> pnl-raw
      (tc/add-columns {:timestamp
                       #(map time/unixtime->table (% :created-time))})))

既存のColumnに関数を適用してupdateするには?

tc/update-columnsをつかう. 渡す関数はColumn(列)を受け取ってColumn(列)を返す関数.

(tc/update-columns ds {:timestamp (partial map time/unixtime->datetime)})

既存のcolumnsを元に新しくcolumnを追加するには?

tc/map-rowsをつかう.

(-> ds
    (tc/map-rows (fn [{:keys [size price side]}]
                   {:pnl (let [d (if (= side :sell) 1 -1)]
                           (* size price d))})))

tc/add-columnsでもいける.


おそらくrow-mapに相当. ✨CSVとはマップのリストに過ぎないの延長で, DataframeとはList of Mapsに過ぎない.

https://techascent.github.io/tech.ml.dataset/tech.v3.dataset.html#var-row-map

欠損値の置換

tc/replace-missingをつかう. なお, 変換後にclerkでの表示がエラーするときはconvert-typesをつかう.

tableclothで変換した値がclerkでエラーする場合はconvert-typesで型変換

行操作(Rows)

行を条件でselect(filter/remove)するには?

tc/select-rowsをつかう.

compは右から関数適用する. 言い換えると右辺でカラムを指定して左辺で条件を指定する.

(tc/select-rows DS (comp #(< % 1) :V3))

tc/drop-rowsで条件に合わないものを削除することもできる.

結合

ここでは列のマージをmerge, 行のマージをconcatとする.

時系列データを同一日時を残して結合するには?

tc/unionをつかう.

(defn merge [a b]
  (-> a
      (tc/union b)
      (tc/reorder-columns :datetime)))

Advanced

やりたいことができなくて困ったらtech.ml.dataset/next-dtypeのドキュメントを読む.

行計算/列計算/逐次計算の比較

データフレームでの計算はこの3つに分解できると思う.

  • 列計算: Column単位でベクトル演算, 🔧tech.v3.datatype.functionalを多用する.
    • tc/add-columns
    • tc/update-columns
  • 行計算: Row単位, 行ごとに計算.
    • tc/map-rows
  • 逐次計算: 必要な列同士を取り出してfor文を回すイメージ.

列計算, 行計算, 逐次計算の順に計算量が増えるが難易度は下がる. ベクトル演算は慣れが必要. for文を回すのはわかりやすい.

累積和を計算するには?

clojure.core/reductionsという便利関数があるのでこれをつかって計算する.

(def cumsum
  (->> (ds :pnl)
       (reductions +)
       (map double)
       (into [])))
(tc/add-column ds2 :cumsum cumsum)

ref. Clojure: 累積和(cumulative sequences)

vega-lite表示用にデータフレームを変換するには?(List of Maps)

📝Vega-Liteに渡すdataはMapのListである必要がある. (tc/rows ds :as-maps)をつかう.

(def data
  (-> pnl
      (tc/select-columns [:timestamp :cum-pnl])
      (tc/rows :as-maps)))

✅tableclothで変換した値がclerkでエラーする場合はconvert-typesで型変換

こんなやつ. tc/convert-typesで変換のあとに型を揃えるとエラーがでなくなった.

Unhandled java.lang.ClassCastException
class clojure.lang.Reduced cannot be cast to class clojure.lang.ITransientCollection (clojure.lang.Reduced and clojure.lang.ITransientCollection are in unnamed module of loader 'app')

tableclothの問題ではなく, 🔧cnuernber/dtype-nextの問題.

🔧nextjournal/clerk

up: 📝Clojure データサイエンス