🔰

NimでDataFrameを操作

2023/02/15に公開

はじめに

Nim言語を始めたばかりの超初心者です。
Nimで今流行り(?)のDataFrameを使ったプログラムができないかと思ってやってみました。
使用するライブラリはdatamancerを入れています。
ちなみに、私は今はエンジニアではないのでプログラム記述が汚かったりしても、ご容赦願います。

環境

私のPC環境はWindowsであるため、Nightly build版で動作させています。

  • OS: Windows11
  • Nim 1.9.1 (v2.0 RC版 2023/01/29)
  • datamancer 0.3.11

datamancerの特徴

  • カラムの型は、int, float, string, boolがあるが、現時点datetimetime型は存在しません。作者曰く無くても、そんなに問題ないとの事
  • 関数には、select, filter, mutate, group_by, summarizeなどが存在し、DataFrameを操作可能

動作確認

1. datamancerライブラリのインストール

nimble install datamancer

VSCodeのターミナルからnimbleコマンドを実行すると、datamancerが利用するライブラリも含めて、インストールされます。

2. DataFrameの生成

sample01.nim
import datamancer

# DataFrameの値判定
proc df_check(a, b: DataFrame): bool =
  for clm in a.getKeys :  # 列のインデックスを配列で得る
    if a[clm].toTensor(int) != b[clm].toTensor(int):  # 列毎のすべての値をチェック
      return false
  return true

let exp = """
x,y,z
1,2,3
4,5,6
7,8,9
"""
let df = parseCsvString(exp)

let df2 = toDf({ "x" : @[1, 4, 7], "y" : @[2, 5, 8], "z": @[3, 6, 9] })

echo df_check(df, df2)
echo df

let df3 = readCsv("test.csv")   # CSVファイルを読み込みDataFrameを生成

変数expにcsv形式の文字列を設定し、parseCsvStringで読み込む事でDataFrameが生成されます、最初の文字はインデックス名となります。
また、シーケンスやリストから生成する場合は、toDf関数を、CSVファイルを直接読み込む場合は、readCsv関数を使用します。
今回はreadCsv関数による読み込み処理は、割愛します。
関数df_checkを作成し、DataFrame内の値が同一かのチェックを行ってます

下記のようにターミナル上から実行

nim r .\sample01.nim

出力結果

true
DataFrame with 3 columns and 3 rows:
     Idx            x            y            z
  dtype:          int          int          int
       0            1            2            3
       1            4            5            6
       2            7            8            9

最初のtrueは、関数df_checkの判定結果です。
つまり、変数名dfdf2のデータは同一を意味します。
出力されるDataFrameの中身ですが、ヘッダー部とデータ部に分けて表示されます。

3. datamancerライブラリの基本的な使い方

基本的なdatamancerによる列・行の取得を、下記ソースを実行して説明します。

sample02.nim
import datamancer

let data = """
   name     height     age
   John     178.9      13
   ben      182.0      11
   alice    162.5      15
   mark     201.5      29
   zion     198.0      20
"""
var df = parseCsvString(data, sep = ' ')
# 値の抽出
echo "df[height,]=" & $df["height", float].sorted # height列だけを取得しsortする
echo "df[,1]=" & $df.row(1)                       # 2行目を取得
echo "df[name, 2]=" & $df["name", 2, string]      # 3行目のname列を求める

# 値の修正と追加
df["age"][1] = 10                             # age列の2行目だけを変更
let df2 = toDf({ "name": ["terry", "rock"],
                 "height": @[175.2, 205.1],
                 "age": @[18, 42]})
var expDf = bind_rows([df, df2])
expDf["test"] = 5                             # test列を追加、行の値はすべて5
echo expDf

下記のようにターミナル上から実行

nim r .\sample02.nim

出力結果

df[height,]=Tensor[system.float] of shape "[5]" on backend "Cpu"
    162.5    178.9      182      198    201.5
df[,1]={name: ben, height: 182, age: 11}
df[name, 2]=alice
DataFrame with 4 columns and 7 rows:
     Idx         name       height          age         test
  dtype:       string        float          int     constant
       0         John        178.9           13            5
       1          ben          182           10            5
       2        alice        162.5           15            5
       3         mark        201.5           29            5
       4         zion          198           20            5
       5        terry        175.2           18            5
       6         rock        205.1           42            5

最初の出力結果は、height列をソートして出力
次に、2行目の値だけを抽出
最後の抽出は、name列の3行目の値を抽出
最後のDataFrameの出力は、順に説明すると、age列の2番目benの年齢を10に変更し、
bind_rows関数で2つのDataFrameを連結し、最後に、test列を追加しています。

4. 集計

sample03.nim
import datamancer

let data = """
   name     height     age     class
   John     178.9      13      A
   ben      182.0      11      C
   alice    162.5      15      A
   mark     201.5      29      B
   zion     198.0      20      C
   terry    175.2      18      A
   rock     205.1      42      B
   pate     179.4      17      B
   sims     164.8      34      C
"""
var df = parseCsvString(data, sep = ' ')

# mean:平均 sum:合計
echo df.summarize(f{float: "mean_height" << mean(c"height")},
                  f{float: "sum_age" << sum(c"age")})
# class毎のmean:平均 sum:合計
echo df.group_by("class")
       .summarize(f{float: "mean_height" << mean(c"height")},
                 f{float: "sum_age" << sum(c"age")})

#ageが20以上の行列を抽出
echo df.filter(f{c"age" >= 20})

#class毎にageが20以上の最大height値を抽出
echo df.filter(f{c"age" >= 20})
      .group_by("class")
      .summarize(f{float: "max_height" << max(c"height")})

summarize関数は、集計用に使う事が出来ます。また、filter関数で条件にあった集計が可能になります。

下記のようにターミナル上から実行

nim r .\sample03.nim

出力結果

DataFrame with 2 columns and 1 rows:
     Idx    mean_height        sum_age
  dtype:          float          float
       0            183            199

DataFrame with 3 columns and 3 rows:
     Idx          class    mean_height        sum_age
  dtype:         string          float          float
       0              A          172.2             46
       1              B          195.3             88
       2              C          181.6             65

DataFrame with 4 columns and 4 rows:
     Idx         name       height          age        class
  dtype:       string        float          int       string
       0         mark        201.5           29            B
       1         zion          198           20            C
       2         rock        205.1           42            B
       3         sims        164.8           34            C

DataFrame with 2 columns and 2 rows:
     Idx         class    max_height
  dtype:        string         float
       0             B         205.1
       1             C           198

5. 集約

sample04.nim
import datamancer

# 集約
let df1 = toDf({ "Ident" : @["A", "B", "C", "D", "E", "F"],
                 "Ids"   : @[1, 2, 3, 4, 5, 6]})
let df2 = toDf({ "Ident" : @["A", "B", "D", "F"],
                 "Words" : @["suggest", "result", "from", "to"] })
echo df1.innerJoin(df2, by = "Ident")   # Indentで集約

innerJoin関数を使い、Indent列に一致する行を抽出。ただしouterJoin関数が何故かない。また、pandasにあるmergeに似たgather関数はある事はあるんですが、何か思ったのと違う気がするので、今回は記載しませんでした。

下記のようにターミナル上から実行

nim r .\sample04.nim

出力結果

DataFrame with 3 columns and 4 rows:
     Idx        Ident        Words          Ids
  dtype:       object       string          int
       0            A      suggest            1
       1            B       result            2
       2            D         from            4
       3            F           to            6

innerJoin関数を利用する事で、Ident列がそれぞれ同じ値が(A,B,D,F)の列を抽出。

6. 列の追加と追加列の日付を計算

sample05.nim
import datamancer
import std/[times, strutils]

# 10日加算
proc after_date(d: string): string =
  return (d.parse("yyyy/MM/dd") + 10.days).format("yyyy/MM/dd")

# DataFrameを生成
let df = toDf({ "x" : @[1, 2, 3],
                "y" : @[10, 11, 12],
                "z": ["5","6","7"],
                "d": ["2022/03/28", "2022/04/02", "2022/07/22"] })

# mutateで行の値を計算し、列を追加
let expDf = df.mutate(f{"x+y" ~ `x` + `y`})
              .mutate(f{"x*y" ~ `x` * `y`})
              .mutate(f{"d+10day" ~ after_date(c"d")})  # `d` == c"d"
echo expDf

mutate関数は列の追加と計算を行う事が出来ます。また、datamancerの属性にはdetatimeやtimeがないので、自前でafter_date関数内で計算を行う事も出来ます。
ちなみにカラム名については、c"d"d同じ意味を指します。

下記のようにターミナル上から実行

nim r .\sample05.nim

出力結果

DataFrame with 7 columns and 3 rows:
     Idx            x            y            z            d          x+y          x*y      d+10day
  dtype:          int          int       string       string        float        float       string
       0            1           10          "5"   2022/03/28           11           10   2022/04/07
       1            2           11          "6"   2022/04/02           13           22   2022/04/12
       2            3           12          "7"   2022/07/22           15           36   2022/08/01

列名x+y, x*y, d+10dayは、プログラム処理後に追加された列となり、それぞれ行内の値に加算して処理結果を出力しています。

おわりに

Nim言語でも、基本的なDataFrameの利用は可能です。
Python+pandasやRust+polarsのような高機能な部分は足りていませんが、業務で利用するには、事足りるんじゃないかなと思います。何よりpandasより早いし、polarsよりわかりやすい!

Discussion