列指向インメモリDB、kdb+の埋め込み言語であるqでは、シンプルな表現で様々な処理を書くことができます
最近Juliaを利用する中で、qの標準関数で出来るのにJuliaで出来ない!ということが多かったので自分用にまとめます。
Juliaで壁にぶち当たる度に増えていくかもしれません
#使用環境
Julia: 1.5.1
DataFrames: 0.21.7
kdb+: 32bit 3.6
#標準関数
##文字列の結合(,)
文字列を結合します
q)"abc","def"
"abcdef"
Juliaの場合は*演算子になります
julia> "abc"*"def"
"abcdef"
なお、kdbの結合は厳密にはリストやAtomに対する結合ですので、
数値リストの結合も同じ演算子で可能です
q)(1 2 3),(4 5 6)
1 2 3 4 5 6
##連番:til
0からスタートする連番を生成します
q)til 10
0 1 2 3 4 5 6 7 8 9
Juliaではrange, collectを使うのが近いでしょうか
julia> collect(0:9)
10-element Array{Int64,1}:
0
1
2
3
4
5
6
7
8
9
累計:sums
リストの累計を算出します
q)sums 1 + til 10
1 3 6 10 15 21 28 36 45 55
Juliaではcumsumが使えます
julia> cumsum(1:10)
10-element Array{Int64,1}:
1
3
6
10
15
21
28
36
45
55
each
関数を修飾し、リスト内の全ての値に対して処理を実行します
q)count each ("ab"; "cde"; "fghi")
2 3 4
JuliaではBroadcast(.)を使います
julia> length.(["ab", "cde", "fghi"])
3-element Array{Int64,1}:
2
3
4
each-left(\:), each-right(/:), each-both(')
2つの引数を持つ関数を修飾し、左、右、その両方のリスト内要素それぞれに関数を適用します
q)/each-left
q)"12345",\:"ABC"
"1ABC"
"2ABC"
"3ABC"
"4ABC"
"5ABC"
q)/each-right
q)"12345",/:"ABC"
"12345A"
"12345B"
"12345C"
q)/each-both
q)"123",'"ABC"
"1A"
"2B"
"3C"
q)/each-left,right
q)"12345",/:\:"ABC"
"1A" "1B" "1C"
"2A" "2B" "2C"
"3A" "3B" "3C"
"4A" "4B" "4C"
"5A" "5B" "5C"
標準では見つからなかったので、
色々考えましたがJuliaだとmapやforを使って以下のように書くしかないでしょうか
julia> #each-left
julia> map(x -> *(x, "ABC"), collect("12345"))
5-element Array{String,1}:
"1ABC"
"2ABC"
"3ABC"
"4ABC"
"5ABC"
julia> #each-right
julia> map(x -> *("12345", x), collect("ABC"))
3-element Array{String,1}:
"12345A"
"12345B"
"12345C"
julia> #each-both
julia> map((x,y)->*(x, y), "123", "ABC")
3-element Array{String,1}:
"1A"
"2B"
"3C"
julia> #each-left,right
julia> [x*y for x in "12345" for y in "ABC"]
15-element Array{String,1}:
"1A"
"1B"
"1C"
"2A"
"2B"
"2C"
"3A"
"3B"
"3C"
"4A"
"4B"
"4C"
"5A"
"5B"
"5C"
上記は文字列結合を例にしましたが、Juliaの文字列は文字型の配列という側面もありながらも、型としてはStringになるため、少し特殊な例と言えるかもしれません。
そもそも修飾する関数が左辺、右辺にどのような型を取れるかによって、各イテレーションにおける挙動もそれに応じて変わりますので、結局はそこに依存することにはなります。
例えば、each-bothのような処理は、数値演算であればJuliaでもBroadcast(.)だけで記述できます
q)(1 2 3)+'4 5 6
5 7 9
julia> [1, 2, 3].+[4, 5, 6]
3-element Array{Int64,1}:
5
7
9
2020/09/12 追記
文字列結合であればよりシンプルな記述をコメントいただきました。
ありがとうございます。
julia> collect("12345") .* "ABC"
5-element Array{String,1}:
"1ABC"
"2ABC"
"3ABC"
"4ABC"
"5ABC"
julia> "12345" .* collect("ABC")
3-element Array{String,1}:
"12345A"
"12345B"
"12345C"
julia> collect("123") .* collect("ABC")
3-element Array{String,1}:
"1A"
"2B"
"3C"
julia> collect("12345") .* reshape(collect("ABC"), 1, :)
5×3 Array{String,2}:
"1A" "1B" "1C"
"2A" "2B" "2C"
"3A" "3B" "3C"
"4A" "4B" "4C"
"5A" "5B" "5C"
リストの次元を減らす(raze)
2次元の配列を1次元配列に直列化します
q)raze (1 2 3; 4 5 6)
1 2 3 4 5 6
Juliaの場合vecが使えます。
Juliaの場合は配列の配列と行列は異なる型になりますが、vecが期待通りに動くのは行列に対してです。
julia> vec([[1 2 3];[4 5 6]]')
6-element reshape(::LinearAlgebra.Adjoint{Int64,Array{Int64,2}}, 6) with eltype Int64:
1
2
3
4
5
6
Juliaの行列はcolumn-major orderとのことですので、普通にvecをしてしまうと
1 4 2 5 3 6
という順番になってしまいます。(行列の左上から下に読んでいく)
そのため、転置(')をしてからvec関数に通しています。
また、厳密にArray型にするのであれば、更にcollect関数を通します
julia> collect(vec([[1 2 3];[4 5 6]]'))
6-element Array{Int64,1}:
1
2
3
4
5
6
##累積計算(scan)
関数を修飾することで、2つの引数を取る任意の関数を累積計算させることができます
q)(+) scan 1 2 3 4 5 6 7 8 9 10
1 3 6 10 15 21 28 36 45 55
Juliaではaccumulateを使います
julia> accumulate(+, [1,2,3,4,5,6,7,8,9,10])
10-element Array{Int64,1}:
1
3
6
10
15
21
28
36
45
55
ちなみに今回の例は足し算ですが、足し算であれば既に述べた通り
qであればsums
が、Juliaであればcumsum
がありますので
わざわざscan
やaccumulate
を使う必要はありません
##欠損値を直前の値で埋める(fills)
リスト内に欠損値があった場合に手前の(欠損値ではない)値で補完します。
q)fills 0N 1 9 3 0N 0N 7 8 0N 4
0N 1 9 3 3 3 7 8 8 4
Juliaですと関数を作ってあげるしかないでしょうか
julia> function fills!(arr)
for i in 2:length(arr)
if ismissing(arr[i])
arr[i] = arr[i-1]
end
end
end
fills! (generic function with 1 method)
julia> arr = [missing, 1, 9, 3, missing, missing, 7, 8, missing, 4]
10-element Array{Union{Missing, Int64},1}:
missing
1
9
3
missing
missing
7
8
missing
4
julia> fills!(arr)
julia> arr
10-element Array{Union{Missing, Int64},1}:
missing
1
9
3
3
3
7
8
8
4
#xbar
x xbar y
とした時、yに最も近いxの倍数に切り捨てます。
q)4 xbar 1 2 3 4 5 6 7 8 9 10
0 0 0 4 4 4 4 8 8 8
そのものは見つからなかったのでJuliaでやるとすれば関数を新たに作成することになりそうです。さらに、Floatに対して適用する場合はもう少し工夫が要りそうです。
julia> function xbar(x, y)
return y ÷ x * x
end
xbar (generic function with 1 method)
julia> xbar.(4, [1,2,3,4,5,6,7,8,9,10])
10-element Array{Int64,1}:
0
0
0
4
4
4
4
8
8
8
kdb+でxbarを使うときは時間に対して使うことも多いと思いますが、
そちらはjuliaもDatesで実装されているroundが問題なく使えそうです
q)/10分毎の単位で切り捨て
q)0D00:10:00 xbar 2020.09.01D10:15:20.000000000
2020.09.01D10:10:00.000000000
julia> using Dates
julia> round(DateTime(2020, 9, 1, 10, 15, 20), Dates.Minute(10), RoundDown)
2020-09-01T10:10:00
kdb+は時系列DBということもあり、日付・時間リテラルもサポートされている分書きやすいですね。
#構文
##関数作成
q)f: {[a; b] a + b}
q)f[5;6]
11
julia> function f(a, b)
return a + b
end
f (generic function with 1 method)
julia> f(5, 6)
11
##関数のプロジェクション
関数の引数の一部のみを固定して新たな関数を作成する機能です
※先ほどの関数f
を流用します
q)g: f[5;]
q)g[6]
11
julia> g(b) = f(5, b)
g (generic function with 1 method)
julia> g(6)
11
2020/09/17 追記
コメントで別の方法を教えていただきました。
ありがとうございます
julia> f(x,y)=x/y
f (generic function with 1 method)
julia> g=Base.Fix1(f,2)
(::Base.Fix1{typeof(f),Int64}) (generic function with 1 method)
julia> g(1) # => 2/1
2.0
julia> h=Base.Fix2(f,2)
(::Base.Fix1{typeof(f),Int64}) (generic function with 1 method)
julia> h(1) # => 1/2
0.5
##無名関数
q){[a; b] a + b}[5; 6]
11
julia> ((a, b) -> a + b)(5, 6)
11
#Table・DataFrame関連
kdb+はインメモリデータベースということもあって、PythonやJuliaで言うDataFrameの概念がTableとして標準で用意されています。
sqlライクに問い合わせが可能なq-sqlも標準の構文として存在しているため高い汎用性があります。
##テーブル作成
q)tab: ([] a: `A`B`C`D`E; b:1 2 3 4 5)
q)tab
a b
---
A 1
B 2
C 3
D 4
E 5
julia> using DataFrames
julia> df = DataFrame(a = [:A, :B, :C, :D, :E], b = [1, 2, 3, 4, 5])
5×2 DataFrame
│ Row │ a │ b │
│ │ Symbol │ Int64 │
├─────┼────────┼───────┤
│ 1 │ A │ 1 │
│ 2 │ B │ 2 │
│ 3 │ C │ 3 │
│ 4 │ D │ 4 │
│ 5 │ E │ 5 │
##1列抽出する
テーブル・データフレームから1列抽出します。
qの場合列を指定するほかに、q-sqlのexecを使うことでも抽出できます
※先ほど作成したテーブル、データフレームを流用しています
q)tab.a
`A`B`C`D`E
q)exec a from tab
`A`B`C`D`E
Juliaも同様にドットで列にアクセス出来ます
DataFrameに問い合わせを行うためのパッケージはいくつかあるようですが、
ここではDataFramesMetaのwithマクロを併せて記載します。
julia> df.a
5-element Array{Symbol,1}:
:A
:B
:C
:D
:E
julia> using DataFramesMeta
julia> @with(df, :a)
5-element Array{Symbol,1}:
:A
:B
:C
:D
:E
##抽出(select)
###条件無しで取得
qでは、q-sqlによりほぼsqlと同じように問い合わせが出来ます
q)select a, b from tab
a b
---
A 1
B 2
C 3
D 4
E 5
Juliaでは例えば以下のように取得できます
julia> #DataFrame標準
julia> df[[:a, :b]]
5×2 DataFrame
│ Row │ a │ b │
│ │ Symbol │ Int64 │
├─────┼────────┼───────┤
│ 1 │ A │ 1 │
│ 2 │ B │ 2 │
│ 3 │ C │ 3 │
│ 4 │ D │ 4 │
│ 5 │ E │ 5 │
julia> #DataFramesMeta
julia> @select(df, :a, :b)
5×2 DataFrame
│ Row │ a │ b │
│ │ Symbol │ Int64 │
├─────┼────────┼───────┤
│ 1 │ A │ 1 │
│ 2 │ B │ 2 │
│ 3 │ C │ 3 │
│ 4 │ D │ 4 │
│ 5 │ E │ 5 │
###where句
qについてはこれもほぼSQLと同様です
(全ての列を取得するときにアスタリスクを書かないことくらいです)
q)select from tab where a = `C
a b
---
C 3
先ほどと同じく、DataFrame単体でやる場合、DataFramesMetaでそれぞれ以下の通りです
julia> #DataFrame
julia> df[df.a .== Symbol("C"), :]
1×2 DataFrame
│ Row │ a │ b │
│ │ Symbol │ Int64 │
├─────┼────────┼───────┤
│ 1 │ C │ 3 │
julia> #DataFramesMeta
julia> @where(df, :a .== Symbol("C"))
1×2 DataFrame
│ Row │ a │ b │
│ │ Symbol │ Int64 │
├─────┼────────┼───────┤
│ 1 │ C │ 3 │
qの場合以下のような方法もあります。
q)tab[where tab.a = `C]
a b
---
C 3
q)@[tab; where tab.a = `C]
a b
---
C 3
また、更に取得する列を絞る場合は以下のようになります
q)select b from tab where a = `C
b
-
3
julia> @select(@where(df, :a .== Symbol("C")), :b)
1×1 DataFrame
│ Row │ b │
│ │ Int64 │
├─────┼───────┤
│ 1 │ 3 │
ただ、Juliaの場合は@linqを使って以下のように書くほうが可読性の面では良さそうです
julia> @linq df |>
where(:a .== Symbol("C")) |>
select(:b)
1×1 DataFrame
│ Row │ b │
│ │ Int64 │
├─────┼───────┤
│ 1 │ 3 │
#参考
##Julia
Juliaで最低限やっていくための配列操作まとめ
Dates
Github - JuliaData/DataFramesMeta.jl
##kdb+/q
Map Iterators