Danfo.js – PandasライクなJavaScriptのDataFrameライブラリ-

2021.08.06

Introduction

例えば機械学習のトレーニングデータを用意するとき、
csvからデータを読み込んだり列や行を追加・削除・変更したりグルーピングしたりなどなど、
データの加工がほぼ必須です。
そういったときにデータ解析用ライブラリとしてPythonのPandasなどがよく使われます。

とても使いやすいライブラリなのですが、
Pythonに不慣れな私としてはJavascriptでなんとかしたいなと思っていました。
そこで探してみたところ、みつけたのがDanfo.jsです。

Environment

  • OS : MacOS 10.15.7
  • Node : v14.16.1

Danfo.js?

Danfo.jsとは、PythonのデータフレームライブラリであるPandasにインスパイアされた、
構造化データの操作や処理を実施するためのJavascriptライブラリです。
(おそらく)Pandasを使える人ならすぐに馴染めると思われます。

csvやjsonファイル、TensorflowのオブジェクトからDataFrameを構築したり、
それらのDataFrameに対して変更や結合、データのグルーピングなどいろいろな操作も提供しています。

Setup

npmでインストールします。

%  npm install danfojs-node

(2021/8月時点)
danfojsはtensorflow.jsに依存しているようで、M1 Macでは動きませんでした。
また、tensor 2dをdanfojsにそのまま渡すことができるみたいでしたが、
自分の環境ではエラーだった(多分バージョンとかそのへんの問題)のでそこは未確認です。

Use Danfojs

ではつかってみましょう。
まずは配列データからDataFrameを作成して表示してみます。
(printでコンソール表示)

//danfojs-main.js
const dfd = require("danfojs-node");

const arr_data = [[1, 'hello', 3.2],
            [5, 'danfojs',7.5],[3, 'javascipt',10.0]];

df = new dfd.DataFrame(arr_data)
df.print()
% node danfojs-main.js
╔═══╤═══════════════════╤═══════════════════╤═══════════════════╗
║   │ 0                 │ 1                 │ 2                 ║
╟───┼───────────────────┼───────────────────┼───────────────────╢
║ 0 │ 1                 │ hello             │ 3.2               ║
╟───┼───────────────────┼───────────────────┼───────────────────╢
║ 1 │ 5                 │ danfojs           │ 7.5               ║
╟───┼───────────────────┼───────────────────┼───────────────────╢
║ 2 │ 3                 │ javascipt         │ 10                ║
╚═══╧═══════════════════╧═══════════════════╧═══════════════════╝

ctypesで型情報がわかる。

df.ctypes.print()
╔═══╤══════════════════════╗
║   │ 0                    ║
╟───┼──────────────────────╢
║ 0 │ int32                ║
╟───┼──────────────────────╢
║ 1 │ string               ║
╟───┼──────────────────────╢
║ 2 │ float32              ║
╚═══╧══════════════════════╝

headで頭から、tailで後ろから任意の行数を取得。

df.head(1).print()
df.tail(1).print()
╔═══╤═══════════════════╤═══════════════════╤═══════════════════╗
║   │ 0                 │ 1                 │ 2                 ║
╟───┼───────────────────┼───────────────────┼───────────────────╢
║ 0 │ 1                 │ hello             │ 3.2               ║
╚═══╧═══════════════════╧═══════════════════╧═══════════════════╝

╔═══╤═══════════════════╤═══════════════════╤═══════════════════╗
║   │ 0                 │ 1                 │ 2                 ║
╟───┼───────────────────┼───────────────────┼───────────────────╢
║ 2 │ 3                 │ javascipt         │ 10                ║
╚═══╧═══════════════════╧═══════════════════╧═══════════════════╝

column指定も可能。
describeでサマリ情報取得。  

const arr_data = [[1, 'hello', 3.2],
            [5, 'danfojs',7.5],[3, 'javascipt',10.0]];

df = new dfd.DataFrame(arr_data,{columns: ["id", "name", "number"]});

console.log(df.index);
console.log(df.columns);
df.describe().print()
[ 0, 1, 2 ]
[ 'id', 'u_name', 'number' ]
╔══════════╤═══════════════════╤═══════════════════╗
║          │ id                │ number            ║
╟──────────┼───────────────────┼───────────────────╢
║ count    │ 3                 │ 3                 ║
╟──────────┼───────────────────┼───────────────────╢
║ mean     │ 3                 │ 6.9               ║
╟──────────┼───────────────────┼───────────────────╢
║ std      │ 2                 │ 3.439477          ║
╟──────────┼───────────────────┼───────────────────╢
║ min      │ 1                 │ 3.2               ║
╟──────────┼───────────────────┼───────────────────╢
║ median   │ 3                 │ 7.5               ║
╟──────────┼───────────────────┼───────────────────╢
║ max      │ 5                 │ 10                ║
╟──────────┼───────────────────┼───────────────────╢
║ variance │ 4                 │ 11.83             ║
╚══════════╧═══════════════════╧═══════════════════╝

sort可能。

df.sort_values({by: "number", ascending:false,inplace: true});
df.print();
╔═══╤═══════════════════╤═══════════════════╤═══════════════════╗
║   │ id                │ u_name            │ number            ║
╟───┼───────────────────┼───────────────────┼───────────────────╢
║ 2 │ 3                 │ javascipt         │ 10                ║
╟───┼───────────────────┼───────────────────┼───────────────────╢
║ 1 │ 5                 │ danfojs           │ 7.5               ║
╟───┼───────────────────┼───────────────────┼───────────────────╢
║ 0 │ 1                 │ hello             │ 3.2               ║
╚═══╧═══════════════════╧═══════════════════╧═══════════════════╝

pandasでも使えるlocとかilocとか。
slice指定もできます。

console.log("loc")
df.loc({rows: [0,1]}).print()
console.log("iloc")
df.iloc({rows: [1,2]}).print()
console.log("slice")
df.iloc({rows: ["0:3"]}).print()
loc
╔═══╤═══════════════════╤═══════════════════╤═══════════════════╗
║   │ id                │ u_name            │ number            ║
╟───┼───────────────────┼───────────────────┼───────────────────╢
║ 0 │ 1                 │ hello             │ 3.2               ║
╟───┼───────────────────┼───────────────────┼───────────────────╢
║ 1 │ 5                 │ danfojs           │ 7.5               ║
╚═══╧═══════════════════╧═══════════════════╧═══════════════════╝

iloc
╔═══╤═══════════════════╤═══════════════════╤═══════════════════╗
║   │ id                │ u_name            │ number            ║
╟───┼───────────────────┼───────────────────┼───────────────────╢
║ 1 │ 5                 │ danfojs           │ 7.5               ║
╟───┼───────────────────┼───────────────────┼───────────────────╢
║ 2 │ 3                 │ javascipt         │ 10                ║
╚═══╧═══════════════════╧═══════════════════╧═══════════════════╝

slice
╔═══╤═══════════════════╤═══════════════════╤═══════════════════╗
║   │ id                │ u_name            │ number            ║
╟───┼───────────────────┼───────────────────┼───────────────────╢
║ 0 │ 1                 │ hello             │ 3.2               ║
╟───┼───────────────────┼───────────────────┼───────────────────╢
║ 1 │ 5                 │ danfojs           │ 7.5               ║
╟───┼───────────────────┼───────────────────┼───────────────────╢
║ 2 │ 3                 │ javascipt         │ 10                ║
╚═══╧═══════════════════╧═══════════════════╧═══════════════════╝

queryを使うとSQLのwhereみたいなかんじで行を抽出できます。

console.log("query-1")
df.query({ "column": "number", "is": ">", "to": 5 }).print();

console.log("query-2")
query_df = df.query({ column: "u_name", is: "==", to: "danfojs"}).print();
query-1
╔═══╤═══════════════════╤═══════════════════╤═══════════════════╗
║   │ id                │ u_name            │ number            ║
╟───┼───────────────────┼───────────────────┼───────────────────╢
║ 1 │ 5                 │ danfojs           │ 7.5               ║
╟───┼───────────────────┼───────────────────┼───────────────────╢
║ 2 │ 3                 │ javascipt         │ 10                ║
╚═══╧═══════════════════╧═══════════════════╧═══════════════════╝

query-2
╔═══╤═══════════════════╤═══════════════════╤═══════════════════╗
║   │ id                │ u_name            │ number            ║
╟───┼───────────────────┼───────────────────┼───────────────────╢
║ 1 │ 5                 │ danfojs           │ 7.5               ║
╚═══╧═══════════════════╧═══════════════════╧═══════════════════╝

dropnaを使えば不正データを除去できる。

const json_data = [
            { A:1,B:'hello',C:NaN},
            { A:5, B:'danfojs',C:7.5 },
            { A:3, B:'javascipt',C:10.0 },
            { A:NaN,B:null,C:3.3}];
df = new dfd.DataFrame(json_data);
df.isna().print();
df.dropna({axis: 0}).print();
╔═══╤═══════════════════╤═══════════════════╤═══════════════════╗
║   │ A                 │ B                 │ C                 ║
╟───┼───────────────────┼───────────────────┼───────────────────╢
║ 0 │ false             │ false             │ true              ║
╟───┼───────────────────┼───────────────────┼───────────────────╢
║ 1 │ false             │ false             │ false             ║
╟───┼───────────────────┼───────────────────┼───────────────────╢
║ 2 │ false             │ false             │ false             ║
╟───┼───────────────────┼───────────────────┼───────────────────╢
║ 3 │ true              │ true              │ false             ║
╚═══╧═══════════════════╧═══════════════════╧═══════════════════╝

╔═══╤═══════════════════╤═══════════════════╤═══════════════════╗
║   │ A                 │ B                 │ C                 ║
╟───┼───────────────────┼───────────────────┼───────────────────╢
║ 0 │ 5                 │ danfojs           │ 7.5               ║
╟───┼───────────────────┼───────────────────┼───────────────────╢
║ 1 │ 3                 │ javascipt         │ 10                ║
╚═══╧═══════════════════╧═══════════════════╧═══════════════════╝

applyをつかって各値に関数を適用できます。

const data = [[1, 2, 3], [4, 5, 6], [20, 30, 40], [39, 89, 78]]
const cols = ["A", "B", "C"]
const df = new dfd.DataFrame(data, { columns: cols })

function square(x) {
    return x ** 2
}

df.apply({callable: square }).print();
╔═══╤═══════════════════╤═══════════════════╤═══════════════════╗
║   │ A                 │ B                 │ C                 ║
╟───┼───────────────────┼───────────────────┼───────────────────╢
║ 0 │ 1                 │ 4                 │ 9                 ║
╟───┼───────────────────┼───────────────────┼───────────────────╢
║ 1 │ 16                │ 25                │ 36                ║
╟───┼───────────────────┼───────────────────┼───────────────────╢
║ 2 │ 400               │ 900               │ 1600              ║
╟───┼───────────────────┼───────────────────┼───────────────────╢
║ 3 │ 1521              │ 7921              │ 6084              ║
╚═══╧═══════════════════╧═══════════════════╧═══════════════════╝

concatでDataFrame同士を結合できます。

let data_1 = [['U1', 'taro'], ['U2', 'hanako'],['U3','takeshi']];
let data_2 = [['tokyo', 20],['saitama',30], ['okinawa',40]];

let colum1 = ['id', 'name'];
let colum2 = ['address', 'age'];

let df_1 = new dfd.DataFrame(data_1, { columns: colum1 });
let df_2 = new dfd.DataFrame(data_2, { columns: colum2 });

dfd.concat({ df_list: [df_1, df_2], axis: 1 }).print();
╔═══╤═══════════════════╤═══════════════════╤═══════════════════╤═══════════════════╗
║   │ id                │ name              │ address           │ age               ║
╟───┼───────────────────┼───────────────────┼───────────────────┼───────────────────╢
║ 0 │ U1                │ taro              │ tokyo             │ 20                ║
╟───┼───────────────────┼───────────────────┼───────────────────┼───────────────────╢
║ 1 │ U2                │ hanako            │ saitama           │ 30                ║
╟───┼───────────────────┼───────────────────┼───────────────────┼───────────────────╢
║ 2 │ U3                │ takeshi           │ okinawa           │ 40                ║
╚═══╧═══════════════════╧═══════════════════╧═══════════════════╧═══════════════════╝

Groupingもいろいろとできます。

let data_g ={'A': ['foo', 'bar', 'foo', 'bar',
                'foo', 'bar', 'foo', 'foo'],
           'B': ['one', 'one', 'two', 'three',
                'two', 'two', 'one', 'three'],
           'C': [1,3,2,4,5,2,6,7],
           'D': [3,2,4,1,5,6,7,8]
        }

let df_g = new dfd.DataFrame(data_g);
let grp = df_g.groupby(["A"]);

grp.get_groups(["foo"]).print();
grp.col(["C"]).sum().print();

let grp_2 = df_g.groupby(["A","B"])
grp_2.col(["C"]).sum().print()
╔═══╤═══════════════════╤═══════════════════╤═══════════════════╤═══════════════════╗
║   │ A                 │ B                 │ C                 │ D                 ║
╟───┼───────────────────┼───────────────────┼───────────────────┼───────────────────╢
║ 0 │ foo               │ one               │ 1                 │ 3                 ║
╟───┼───────────────────┼───────────────────┼───────────────────┼───────────────────╢
║ 1 │ foo               │ two               │ 2                 │ 4                 ║
╟───┼───────────────────┼───────────────────┼───────────────────┼───────────────────╢
║ 2 │ foo               │ two               │ 5                 │ 5                 ║
╟───┼───────────────────┼───────────────────┼───────────────────┼───────────────────╢
║ 3 │ foo               │ one               │ 6                 │ 7                 ║
╟───┼───────────────────┼───────────────────┼───────────────────┼───────────────────╢
║ 4 │ foo               │ three             │ 7                 │ 8                 ║
╚═══╧═══════════════════╧═══════════════════╧═══════════════════╧═══════════════════╝

╔═══╤═══════════════════╤═══════════════════╗
║   │ A                 │ C_sum             ║
╟───┼───────────────────┼───────────────────╢
║ 0 │ foo               │ 21                ║
╟───┼───────────────────┼───────────────────╢
║ 1 │ bar               │ 9                 ║
╚═══╧═══════════════════╧═══════════════════╝

╔═══╤═══════════════════╤═══════════════════╤═══════════════════╗
║   │ A                 │ B                 │ C_sum             ║
╟───┼───────────────────┼───────────────────┼───────────────────╢
║ 0 │ foo               │ one               │ 7                 ║
╟───┼───────────────────┼───────────────────┼───────────────────╢
║ 1 │ foo               │ two               │ 7                 ║
╟───┼───────────────────┼───────────────────┼───────────────────╢
║ 2 │ foo               │ three             │ 7                 ║
╟───┼───────────────────┼───────────────────┼───────────────────╢
║ 3 │ bar               │ one               │ 3                 ║
╟───┼───────────────────┼───────────────────┼───────────────────╢
║ 4 │ bar               │ two               │ 2                 ║
╟───┼───────────────────┼───────────────────┼───────────────────╢
║ 5 │ bar               │ three             │ 4                 ║
╚═══╧═══════════════════╧═══════════════════╧═══════════════════╝

csvファイルを読み込むこともかんたんにできます。
ためしにこの記事でつかったペンギンデータ(penguins_size.csv)を
読んでみます。

const dfd = require("danfojs-node")


async function exec() {
    let df = await dfd.read("path/your/penguins_size.csv");
    console.log(df.shape);
    let df_schema = df
      .astype({column: "culmen_length_mm", dtype: "float32"})
      .astype({column: "flipper_length_mm", dtype: "float32"})
      .astype({column: "body_mass_g", dtype: "int32"})
      .astype({column: "culmen_depth_mm", dtype: "float32"});

    //replace NA to null
    let df_rep = df_schema.replace({ "replace": "NA", "with": null, "in": ["sex"] });
    let df_new2 = df_rep.dropna({axis: 0})
    df_new2.ctypes.print()
    df_new2.print();
}

exec();
[ 344, 7 ]
╔═══════════════════╤══════════════════════╗
║                   │ 0                    ║
╟───────────────────┼──────────────────────╢
║ species           │ string               ║
╟───────────────────┼──────────────────────╢
║ island            │ string               ║
╟───────────────────┼──────────────────────╢
║ culmen_length_mm  │ float32              ║
╟───────────────────┼──────────────────────╢
║ culmen_depth_mm   │ float32              ║
╟───────────────────┼──────────────────────╢
║ flipper_length_mm │ int32                ║
╟───────────────────┼──────────────────────╢
║ body_mass_g       │ int32                ║
╟───────────────────┼──────────────────────╢
║ sex               │ string               ║
╚═══════════════════╧══════════════════════╝

╔════╤═══════════════════╤═══════════════════╤═══════════════════╤═══════════════════╤═══════════════════╤═══════════════════╤═══════════════════╗
║    │ species           │ island            │ culmen_length_mm  │ culmen_depth_mm   │ flipper_lengt...  │ body_mass_g       │ sex               ║
╟────┼───────────────────┼───────────────────┼───────────────────┼───────────────────┼───────────────────┼───────────────────┼───────────────────╢
║ 0  │ Adelie            │ Torgersen         │ 39.1              │ 18.7              │ 181               │ 3750              │ MALE              ║
╟────┼───────────────────┼───────────────────┼───────────────────┼───────────────────┼───────────────────┼───────────────────┼───────────────────╢
║ 1  │ Adelie            │ Torgersen         │ 39.5              │ 17.4              │ 186               │ 3800              │ FEMALE            ║
╟────┼───────────────────┼───────────────────┼───────────────────┼───────────────────┼───────────────────┼───────────────────┼───────────────────╢
・・・・・・・・・

Summary

今回はJavascript用DataFrameライブラリ、Danfo.jsを使ってみました。
DataFrameのファイル出力(csvやjson)もかんたんにできますし、
データ整形が楽になりそうです。

References