NimでDataFrameを操作
はじめに
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
があるが、現時点datetime
やtime
型は存在しません。作者曰く無くても、そんなに問題ないとの事 - 関数には、
select
,filter
,mutate
,group_by
,summarize
などが存在し、DataFrameを操作可能
動作確認
1. datamancerライブラリのインストール
nimble install datamancer
VSCodeのターミナルからnimble
コマンドを実行すると、datamancerが利用するライブラリも含めて、インストールされます。
2. DataFrameの生成
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
の判定結果です。
つまり、変数名df
とdf2
のデータは同一を意味します。
出力されるDataFrameの中身ですが、ヘッダー部とデータ部に分けて表示されます。
3. datamancerライブラリの基本的な使い方
基本的なdatamancerによる列・行の取得を、下記ソースを実行して説明します。
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. 集計
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. 集約
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. 列の追加と追加列の日付を計算
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