NimでDataFrameを操作
はじめに
datamancerは、多次元配列を操作するarraymancerをコアとして作られた100%Nimで書かれたDataFrameライブラリです。
R言語のDataFrameライブラリであるdplyr
を元にしています。
※この操作説明は、datamancerの「SciNim Getting Started」を元に作成しています。
環境
私のPC環境はWindowsで動作させています。
- OS: Windows11
- Nim 2.0.8
- datamancer 0.4.6
datamancerの特徴
- カラムの型は、
int
,float
,string
,bool
,value
があるが、現時点datetime
やtime
型は存在しません。作者曰く無くても、そんなに問題ないとの事 - メソッドには、DataFrameの値を直接扱う物(
row
,add
,getKeys
,head
,tail
など)と、DataFrameを条件抽出するの物(select
,filter
,mutate
,group_by
,summarize
など)が存在し、操作可能です。
datamancerライブラリのインストール
nimble install datamancer
ターミナルからnimble
コマンドを実行すると、datamancerが利用するライブラリも含めて、インストールされます。
動作確認
1. DataFrameの生成
DataFrameを生成する方法は4つあります。
- newDataFrameで空のDataFrameを作成
- toDf関数としてDataFrameを作成
- CSVファイルからDataFrameを生成
- CSV形式の文字列からDataFrameを生成
1-1. newDataFrame関数による生成
import datamancer
var df = newDataFrame() # 空のDataFrameを生成し、カラムを追加
for i in 0 ..< 3:
df["x" & $i] = @[1 + i, 2 + i, i + 3] # 列(カラム)毎の追加
1-2. toDf関数による生成
import datamancer
# カラム列を抽出
let s1 = [1, 2, 3] # array型
let s2 = @["hello", "foo", "bar"] # seq型
let s3 = @[1.5, 2.5, 3.5].toTensor # tensor型
let df = toDf({"clm01": s1, "clm02": s2, "clm03": s3}) # カラムを挿入
echo df # toDf(s1, s2, s3)でも良いが、その場合、変数名s1がカラム名になる
# 直接値を入れても良い
let df2 = toDf({"x" : @[1, 2, 3], "y" : @[4.0, 5.0, 6.0], "z" : @["a", "b", "c"]})
echo df2
1-3. CSVファイルから生成
import datamancer
let df = readCsv("test.csv") # ヘッダー行を指定したCSVファイルを読む
echo df
1-4. CSV形式の文字列から生成
import datamancer
let exp = """
x,y,z
1,2,3
4,5,6
7,8,9
"""
let df = parseCsvString(exp, sep = ',') # 第二引数のsepは`,`なら指定なくても問題ない
echo df
2. 列(カラム)の追加・削除・カラム名変更
2-1. 列(カラム)の追加
import datamancer
let exp = """
x,y,z
1,2,3
4,5,6
7,8,9
"""
var df = parseCsvString(exp)
let a = @["japan","america","china"]
# カラム名をseqデータを追加 (行数が合わないとエラーになる)
df["country"] = a
echo df
DataFrame with 4 columns and 3 rows:
Idx x y z country
dtype: int int int string
0 1 2 3 japan
1 4 5 6 america
2 7 8 9 china
2-2. 列(カラム)の削除
# 2.1のプログラムに追記する
# 列名(カラム名)を指定して、drop関数で削除
df.drop("country")
echo df
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
2-3. 列(カラム)のカラム名変更
# 2.2のプログラムに追記する
# renameは、マクロf{}を使用して右の旧名称から新名称に変更し、コピーする形になります。
var df2 = df.rename(f{"xxx" <- "x"})
echo df2
DataFrame with 3 columns and 3 rows:
Idx y z xxx
dtype: int int int
0 2 3 1
1 5 6 4
2 8 9 7
※追記になるので、カラム列は最後尾に移動される
3. 行の追加
3-1. 行の追加
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 = ' ')
# 1行追加 (追加するカラム属性は、同一でないとエラーになる)
df.add("tyson", 178, 58)
echo df
DataFrame with 3 columns and 6 rows:
Idx name height age
dtype: string float int
0 John 178.9 13
1 ben 182 11
2 alice 162.5 15
3 mark 201.5 29
4 zion 198 20
5 tyson 178 58
3-2. 同一型のDataFrame結合
# 3-1.のプログラムに追記する
# 結合する複数行のDataFrameを用意する
let df2 = toDf({ "name": ["terry", "rock"],
"height": @[175.2, 205.1],
"age": @[18, 42]})
# bind_rows関数で結合を行う
var expDf = bind_rows([df, df2])
echo expDf
DataFrame with 3 columns and 8 rows:
Idx name height age
dtype: string float int
0 John 178.9 13
1 ben 182 11
2 alice 162.5 15
3 mark 201.5 29
4 zion 198 20
5 tyson 178 58
6 terry 175.2 18
7 rock 205.1 42
※df2の値が最後尾2行に追加される
4. 値の抽出と変更
4-1. 値の抽出 (指定列・行の抽出)
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 = ' ')
# 指定行・列から値を抽出
# ※注意 [カラム名][行数]は登録時のみ可能、値の抽出は[カラム名, 行数]
var value_name1: string = df["name", 3, string]
var value_height1: float = df["height", 3, float]
var value_age1: int = df["age", 3, int]
echo value_name1, ", ", $value_height1, ", ", $value_age1
# または 上下どちらの記載でも、同じ値を求める事が出来る
var value_name2: string = df["name", 3].toStr()
var value_height2: float = df["height", 3].toFloat()
var value_age2: int = df["age", 3].toInt()
echo value_name2, ", ", $value_height2, ", ", $value_age2
# 行指定後に列指定しても、同じ値を求める事が出来る
var value_name3: string = df.row(3)["name"].toStr()
var value_height3: float = df.row(3)["height"].toFloat()
var value_age3: int = df.row(3)["age"].toInt()
echo value_name3, ", ", $value_height3, ", ", $value_age3
mark, 201.5, 29
mark, 201.5, 29
mark, 201.5, 29
4-2. 列(カラム)の抽出
# 4-1.のプログラムに追記する
# height列だけを取得しsortする (この時の型はTensor[float]型になる)
var height_clm = df["height", float].sorted
echo "--- height sorted ---"
for i in height_clm:
echo i
--- height sorted ---
162.5
178.9
182.0
198.0
201.5
4-3. 行の抽出
# 4-2.のプログラムに追記する
# row関数で指定行を抽出 (1行目を取得)
var value_row = df.row(0)
# これは、前説の4-1. 値の抽出 (指定列・行の抽出)と同じです
var value_name4: string = value_row["name"].toStr()
var value_height4: float = value_row["height"].toFloat()
var value_age4: int = value_row["age"].toInt()
echo value_name4, ", ", $value_height4, ", ", $value_age4
John, 178.9, 13
4-4. 指定行・列の変更
# 4-3.のプログラムに追記する
# 値の修正と追加
df["age"][1] = 10 # age列の2行目だけを変更
echo df
DataFrame with 3 columns and 5 rows:
Idx name height age
dtype: string float int
0 John 178.9 13
1 ben 182 10 <= ここ変更される
2 alice 162.5 15
3 mark 201.5 29
4 zion 198 20
4-5. 指定列に全て同一の固定値を設定
# 4-4.のプログラムに追記する
# 値の修正と追加
df["age"] = 100 # test列を追加、行の値はすべて100
echo df
DataFrame with 3 columns and 5 rows:
Idx name height age
dtype: string float constant
0 John 178.9 100
1 ben 182 100
2 alice 162.5 100
3 mark 201.5 100
4 zion 198 100
※ageカラム全ての値が100になる
f{}
の説明
5. 式マクロ
f{}
マクロの使い方
5-1. f{}
マクロはカラムの名称変更、追加、算術などに使われます。
使う関数は、6.1 関数一覧に記載された関数群で利用可能
5-2. カラム名の表示
# f{}マクロ内でのカラム表示は、"clm_name"・`clm_name`・c"clm_name"・col("clm_name")の4通りで表現されます。
# 式を現す「~ << <- ->」の代入する方の左カラム名は「"clm_name"」が無難
# 逆に計算式を行う右カラム名は、「col("clm_name")」が無難
# ※min maxの計算の時は、「col("clm_name")」でないと何故かエラーになる
# 式マクロだけを使った結果表示
import datamancer
let f = f{`height` ~ (c"hwy" + c"cty")} # 式マクロを変数に代入
echo f.colName, " : ", f.name # 出力結果 height : (~ height ((+ hwy cty)))
# colNameからカラム名が抽出できる、nameは式情報を表示(ポーランド記法で表示される)
let f2 = f{1 + 2} # 数値だけの算術の場合
echo f2.val # 出力結果 3が表示される
<-
、<<
、~
、->
式記号の使い方
5-3. 式マクロ内では式記号を使って、カラムに代入される。
-
<-
はカラム名を変更する場合に使われます。- 例:f{"new_colum" <- "old_colum">}
-
<<
は集計カラムを追加する場合に使われます。- 別カラム追加時に集計結果表示時に使用される
- summarize関数で使用される
-
~
は算術カラムを追加する場合に使われます。- 別カラム追加時に算術結果表示時に使用される
- mutate関数や、transmute関数で使用される
-
->
はカラム属性を変更する場合に使われます。- 属性の変更時に使用される
- f{string -> float: "yFloat" ~ parseFloat(df["y"][idx])}) stringをfloatにカラム属性変更などで利用される
6. DataFrameの集計・集約・計算関数の使い方
6-1. 関数一覧
関数名 | 内容 | 使用される式記号 |
---|---|---|
select | 列の抽出 | <- |
rename | 列名の変更 2-3.に内容を記載 | <- |
filter | 指定列の条件で検索 | == != < > in notin |
mutate | 列の追加と計算を行う | ~ |
transmute | 浮動小数点演算を行う | ~ |
arrange | 指定列で昇順ソート | |
summarize | 集計 | << |
group_by | 指定列内の値で分類 6-6.参照 | |
inner_join | 集約 | |
setDiff | DataFrame同士の異なる値を抽出 | |
count | 列内の同一件数を抽出 | |
bind_rows | 行の追加 3-2.に内容を記載 | |
gather | 列名(カラム名)毎に、値を行表示 | |
spread | 指定列によって分類される | |
unique | 同一値の集約を行う |
6-2. select関数(項目の抽出)
mport 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 = ' ')
# selectはDataFrameから項目を抽出
# fマクロは1項目だけなので、カンマで繋げる またカラム名の変更も可能
var df2 = df.select(f{"name"}, f{"rename_age" <- "age"})
echo df2
# var df3 = df.select(@["name", "age"])
# 上記例のように、seqやarrayも可能
var df3 = df.select("height", "age")
echo df3
DataFrame with 2 columns and 5 rows:
Idx name rename_age
dtype: string int
0 John 13
1 ben 11
2 alice 15
3 mark 29
4 zion 20
DataFrame with 2 columns and 5 rows:
Idx height age
dtype: float int
0 178.9 13
1 182 11
2 162.5 15
3 201.5 29
4 198 20
6-3. filter関数(指定カラムの条件検索)
import datamancer
import std/times
# 今日までの日数
proc to_days(d: string): int =
var dd = d.parse("yyyy/MM/dd")
var i = (now() - dd).inDays
return (i.int)
let data = """
x y z d
1 10 "5" 2024/03/28
2 11 "6" 2023/04/02
3 12 "7" 2022/07/22
"""
var df = parseCsvString(data, sep = ' ')
let expDf = df
.filter(f{c"x" > 1}) # .filter(f{c"x" > 1 and c"y" > 11}) でも結果は同じ
.filter(f{c"y" > 11})
echo expDf
# to_days関数で現在日付からの日数を算出し、700日より低いカラムを抽出
let expDf2 = df
.filter(f{to_days(c"d") < 700})
echo expDf2
DataFrame with 4 columns and 1 rows:
Idx x y z d
dtype: int int int string
0 3 12 7 2022/07/22
DataFrame with 4 columns and 2 rows:
Idx x y z d
dtype: int int int string
0 1 10 5 2024/03/28
1 2 11 6 2023/04/02
6-4. mutate関数(列の追加と算術計算)
mutate
関数は、行毎の算術を行う関数で、一番良く使われる関数である。
以下のプログラムでは、日付の加算と、値の加算を例に記載してみた。
import datamancer
import std/times
# 10日加算
proc after_date(d: string): string =
return (d.parse("yyyy/MM/dd") + 10.days).format("yyyy/MM/dd")
let data = """
x y z d
1 10 "5" 2022/03/28
2 11 "6" 2022/04/02
3 12 "7" 2022/07/22
"""
var df = parseCsvString(data, sep = ' ')
# mutate算術計算関数で行の値を計算し、列を追加
# また、after_date関数で日付を10日加算した結果を出力
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
DataFrame with 7 columns and 3 rows:
Idx x y z d x+y x*y d+10day
dtype: int int int 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
6-5. transmute関数(浮動小数点演算を行う)
import datamancer
import std/[sequtils, strutils]
# transmuteは、浮動小数点演算処理に使用される
let x = toSeq(0 ..< 5) # x = @[0, 1, 2, 3, 4]
let y = x.mapIt(sin(it.float)) # sequtilsのmapIt関数はarray/seq属性の値(it=x)を変換する
let y2 = x.mapIt(pow(sin(it.float), 2.0))
let df = toDf(x, y)
let df_tran1 = df.transmute(f{"x"}, f{"y2" ~ c"y" * c"y"})
let df2 = toDf(x, y2) # 上記と同じ結果になる
echo df_tran1
let df_tran2 = df.transmute(f{"x"}, f{"yFloat" ~ c"y" / 3}) # yカラムを3で割ってるだけ
echo df_tran2
DataFrame with 2 columns and 5 rows:
Idx x y2
dtype: int float
0 0 0
1 1 0.7081
2 2 0.8268
3 3 0.01991
4 4 0.5728
DataFrame with 2 columns and 5 rows:
Idx x yFloat
dtype: int float
0 0 0
1 1 0.2805
2 2 0.3031
3 3 0.04704
4 4 -0.2523
6-6. summarize関数(集計)
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:合計
# ※f{float: "new_clmname"}で、new_clmname列が作成され、列の属性はfloat指定を意味する
var df_sum = df.summarize(f{float: "mean_height" << mean(c"height")},
f{float: "sum_age" << sum(c"age")})
echo df_sum
# class毎のmean:平均 sum:合計 max:最大値 min:最小値
# ※max/minを使用する時は、max(col("height"))でないとエラーになる
# 通常、列指定は"new_clmname",c"new_clmname",`new_clmname`で問題ない
var df_sum2 = df.group_by("class")
.summarize(f{float: "mean_height" << mean(col("height"))},
f{float: "max_height" << max(col("height"))},
f{float: "min_height" << min(col("height"))},
f{float: "sum_age" << sum(`height`)})
echo df_sum2
summarize
関数は、mutate
関数のように複数回の呼び出しが出来ない。
その代わり、関数内で複数のカラムを追加する事は可能。
DataFrame with 2 columns and 1 rows:
Idx mean_height sum_age
dtype: float float
0 183 199
DataFrame with 5 columns and 3 rows:
Idx class mean_height max_height min_height sum_age
dtype: string float float float float
0 A 172.2 178.9 162.5 516.6
1 B 195.3 205.1 179.4 586
2 C 181.6 198 164.8 544.8
6-7. arrange関数(指定列で昇順ソート)
let df = toDf({ "x" : @[4, 2, 7, 4], "y" : @[2.3, 7.1, 3.3, 1.0],
"z" : @["b", "c", "d", "a"]})
echo df.arrange("x") # x列を昇順にソート
echo df.arrange("x", order = SortOrder.Descending) # x列を降順にソート
echo df.arrange(["x", "z"]) # x,y列でソート
DataFrame with 3 columns and 4 rows:
Idx x y z
dtype: int float string
0 2 7.1 c
1 4 2.3 b
2 4 1 a
3 7 3.3 d
DataFrame with 3 columns and 4 rows:
Idx x y z
dtype: int float string
0 7 3.3 d
1 4 2.3 b
2 4 1 a
3 2 7.1 c
DataFrame with 3 columns and 4 rows:
Idx x y z
dtype: int float string
0 2 7.1 c
1 4 1 a
2 4 2.3 b
3 7 3.3 d
6-8. inner_join関数(集約)
import datamancer
# 二つの異なるDataFrameから集約を行う
let data = """
Ident Ids
A 1
B 2
C 3
D 4
E 5
F 6
"""
var df1 = parseCsvString(data, sep = ' ')
let data2 = """
Ident Words
A suggest
B result
D from
F to
"""
var df2 = parseCsvString(data2, sep = ' ')
echo df1.innerJoin(df2, by = "Ident") # Indentで集約
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-9. setDiff関数(DataFrame同士の異なる値を抽出)
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)
let df2 = df[4 .. 6] # 4行目から8行目までをコピー
# dfとdf2を比較し、dfにあって、df2ない値を表示する
let res = setDiff(df, df2)
echo res # 重複しない部分を表示 (1-4,7-8行目が表示される)
DataFrame with 1 columns and 6 rows:
Idx name height age class
dtype: string
0 John 178.9 13 A
1 ben 182.0 11 C
2 alice 162.5 15 A
3 mark 201.5 29 B
4 pate 179.4 17 B
5 sims 164.8 34 C
6-10. count関数(列内の同一件数を抽出)
import datamancer
let data = """
name height age class
John 178.9 13 B
ben 182.0 11 A
alice 162.5 15 C
mark 201.5 11 A
zion 198.0 20 C
"""
var df = parseCsvString(data, sep = ' ')
let df_class = df.count("class")
echo df_class.len
echo df_class
let df_age_class = df.count(["age", "class"])
echo df_age_class.len
echo df_age_class
3 <- df_class.len
DataFrame with 2 columns and 3 rows:
Idx class n
dtype: string int
0 A 2
1 B 1
2 C 2
4 <- df_age_class.len
DataFrame with 3 columns and 4 rows:
Idx age class n
dtype: int string int
0 11 A 2
1 13 B 1
2 15 C 1
3 20 C 1
count
関数で指定した行名(カラム名)とその合計値しか返さない
また、複数の行名を指定する場合は、array型で渡す。その場合、指定した行名と合計値が返される。
6-11. gather関数(列名毎に、値を行表示)
import datamancer
let data = """
A B C country
1 3 5 america
8 4 7 japan
6 9 0 china
"""
# 列名(カラム名)毎に、値を行表示
let df = parseCsvString(data, sep = ' ')
let dfRes = df.gather(df.getKeys(), # 全ての行名(カラム名)をseq表示
key = "Class", # 行名を"Class"に設定し、全ての行名を表示
value = "Num") # 行名を"Num"に設定し、全ての値を表示する
echo dfRes
echo df.getKeys()
※カラム名を限定した場合(["A","B"]だけを指定した場合)は、4カラムで表示される
Class Num C countryの4カラムが表示される
DataFrame with 2 columns and 12 rows:
Idx Class Num
dtype: string object
0 A 1
1 A 8
2 A 6
3 B 3
4 B 4
5 B 9
6 C 5
7 C 7
8 C 0
9 country america
10 country japan
11 country china
@["A", "B", "C", "country"] <- df.getKeys()の結果
Class行名にdf内の全ての行名が表示され、Num行名には、全ての値が表示される。
6-12. spread関数(指定列によって分類される)
gather
の逆操作。長い形式のデータ フレームから広い形式のデータ フレームへの変換。
名前は spread
ですが、APIはdplyr
の新しい pivot_wider
にさらに近づけています。
import datamancer
# 3列以上は必須
let data = """
Class Num v
A 78 1
A 41 8
A 120 0
B 31 3
B 89 4
B 68 0
C 25 5
C 92 7
C 12 2
"""
let df = parseCsvString(data, sep = ' ')
# Class毎にカラムが分裂し、対応するNum列として値にはいる vカラムは先頭列になり、昇順にソートされる
# v=0にはAとBに値が入る
let df_sp = df.spread(namesFrom = "Class", valuesFrom = "Num")
echo df_sp
echo "clm size=", $df_sp.ncols # 4列 v A B C
echo "row size=", $df_sp.len
echo df_sp["A", int]
echo df_sp["B", int]
echo df_sp["C", int]
DataFrame with 4 columns and 8 rows:
Idx v A B C
dtype: int int int int
0 0 120 68 0
1 1 78 0 0
2 2 0 0 12
3 3 0 31 0
4 4 0 89 0
5 5 0 0 25
6 7 0 0 92
7 8 41 0 0
clm size=4
row size=8
Tensor[system.int] of shape "[8]" on backend "Cpu"
120 78 0 0 0 0 0 41
Tensor[system.int] of shape "[8]" on backend "Cpu"
68 0 0 31 89 0 0 0
Tensor[system.int] of shape "[8]" on backend "Cpu"
0 0 12 0 0 25 92 0
6-13. unique関数(同一値の集約を行う)
import datamancer
let df = toDf({ "x" : @[1, 2, 2, 2, 4],
"y" : @[5.0, 6.0, 7.0, 8.0, 9.0],
"z" : @["a", "b", "b", "d", "e"]})
echo df.unique() # 全ての値を出力
echo df.unique("x") # x列の値で集約する
echo df.unique(["x", "z"]) # x,y列の値で集約を行う
DataFrame with 3 columns and 5 rows:
Idx x y z
dtype: int float string
0 1 5 a
1 2 6 b
2 2 7 b
3 2 8 d
4 4 9 e
DataFrame with 3 columns and 3 rows:
Idx x y z
dtype: int float string
0 1 5 a
1 2 6 b
2 4 9 e
DataFrame with 3 columns and 4 rows:
Idx x y z
dtype: int float string
0 1 5 a
1 2 6 b
2 2 8 d
3 4 9 e
おわりに
Nim言語も知らない頃から、DataFrameライブラリの記事を書いてしまった事もあり、流石に内容が分かりにくかったので、今回は、少しは読みやすく書き直したつもりです。
Nim言語でも、基本的なDataFrameの利用は可能です。
Python+pandasやRust+polarsのような高機能な部分は足りていませんが、業務で利用するには、事足りるんじゃないかなとは思います。何よりpandasより早いし、polarsよりわかりやすい!
Discussion