LoginSignup
2
0

More than 3 years have passed since last update.

q/kdb+の処理をJuliaで書く

Last updated at Posted at 2020-09-11

列指向インメモリDB、kdb+の埋め込み言語であるqでは、シンプルな表現で様々な処理を書くことができます
最近Juliaを利用する中で、qの標準関数で出来るのにJuliaで出来ない!ということが多かったので自分用にまとめます。
Juliaで壁にぶち当たる度に増えていくかもしれません

使用環境

Julia: 1.5.1
DataFrames: 0.21.7

kdb+: 32bit 3.6

標準関数

文字列の結合(,)

文字列を結合します

q
q)"abc","def"
"abcdef"

Juliaの場合は*演算子になります

julia
julia> "abc"*"def"
"abcdef"

なお、kdbの結合は厳密にはリストやAtomに対する結合ですので、
数値リストの結合も同じ演算子で可能です

q
q)(1 2 3),(4 5 6)
1 2 3 4 5 6

連番:til

0からスタートする連番を生成します

q
q)til 10
0 1 2 3 4 5 6 7 8 9

Juliaではrange, collectを使うのが近いでしょうか

julia
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
q)count each ("ab"; "cde"; "fghi")
2 3 4

JuliaではBroadcast(.)を使います

julia
julia> length.(["ab", "cde", "fghi"])
3-element Array{Int64,1}:
 2
 3
 4

each-left(\:), each-right(/:), each-both(')

2つの引数を持つ関数を修飾し、左、右、その両方のリスト内要素それぞれに関数を適用します

q
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
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
q)(1 2 3)+'4 5 6
5 7 9
julia
julia> [1, 2, 3].+[4, 5, 6]
3-element Array{Int64,1}:
 5
 7
 9

2020/09/12 追記
文字列結合であればよりシンプルな記述をコメントいただきました。
ありがとうございます。

julia
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
q)raze (1 2 3; 4 5 6)
1 2 3 4 5 6

Juliaの場合vecが使えます。
Juliaの場合は配列の配列と行列は異なる型になりますが、vecが期待通りに動くのは行列に対してです。

julia
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
julia> collect(vec([[1 2 3];[4 5 6]]'))
6-element Array{Int64,1}:
 1
 2
 3
 4
 5
 6

累積計算(scan)

関数を修飾することで、2つの引数を取る任意の関数を累積計算させることができます

q
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
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がありますので
わざわざscanaccumulateを使う必要はありません

欠損値を直前の値で埋める(fills)

リスト内に欠損値があった場合に手前の(欠損値ではない)値で補完します。

q
q)fills 0N 1 9 3 0N 0N 7 8 0N 4
0N 1 9 3 3 3 7 8 8 4

Juliaですと関数を作ってあげるしかないでしょうか

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
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
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
q)/10分毎の単位で切り捨て
q)0D00:10:00 xbar 2020.09.01D10:15:20.000000000
2020.09.01D10:10:00.000000000
julia
julia> using Dates
julia> round(DateTime(2020, 9, 1, 10, 15, 20), Dates.Minute(10), RoundDown)
2020-09-01T10:10:00

kdb+は時系列DBということもあり、日付・時間リテラルもサポートされている分書きやすいですね。

構文

関数作成

q
q)f: {[a; b] a + b}
q)f[5;6]
11
julia
julia> function f(a, b)
           return a + b
       end
f (generic function with 1 method)
julia> f(5, 6)
11

関数のプロジェクション

関数の引数の一部のみを固定して新たな関数を作成する機能です
※先ほどの関数fを流用します

q
q)g: f[5;]
q)g[6]
11
julia
julia> g(b) = f(5, b)
g (generic function with 1 method)
julia> g(6)
11

2020/09/17 追記
コメントで別の方法を教えていただきました。
ありがとうございます

julia
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
q){[a; b] a + b}[5; 6]
11
julia
julia> ((a, b) -> a + b)(5, 6)
11

Table・DataFrame関連

kdb+はインメモリデータベースということもあって、PythonやJuliaで言うDataFrameの概念がTableとして標準で用意されています。
sqlライクに問い合わせが可能なq-sqlも標準の構文として存在しているため高い汎用性があります。

テーブル作成

q
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
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
q)tab.a
`A`B`C`D`E
q)exec a from tab
`A`B`C`D`E

Juliaも同様にドットで列にアクセス出来ます
DataFrameに問い合わせを行うためのパッケージはいくつかあるようですが、
ここではDataFramesMetaのwithマクロを併せて記載します。

julia
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
q)select a, b from tab
a b
---
A 1
B 2
C 3
D 4
E 5

Juliaでは例えば以下のように取得できます

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
q)select from tab where a = `C
a b
---
C 3

先ほどと同じく、DataFrame単体でやる場合、DataFramesMetaでそれぞれ以下の通りです

julia
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
q)tab[where tab.a = `C]
a b
---
C 3

q)@[tab; where tab.a = `C]
a b
---
C 3

また、更に取得する列を絞る場合は以下のようになります

q
q)select b from tab where a = `C
b
-
3
julia
julia> @select(@where(df, :a .== Symbol("C")), :b)
1×1 DataFrame
 Row  b     
      Int64 
├─────┼───────┤
 1    3     

ただ、Juliaの場合は@linqを使って以下のように書くほうが可読性の面では良さそうです

julia
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

2
0
8

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0