本記事では、時系列データをグラフベースでデータモデリングしていく例を見ていきましょう。Neo4j Aura 及び Cypher を利用します。
時系列データのモデリング手法については、『Graph Databases』2nd Edition の "Chapter 4: Building a Graph Database Application" も参考にしています。
みなさんは、家庭のお金の流れをどのように管理していますか?
給与受け取りから貯蓄用にと複数の銀行口座、用途別のクレジットカードに資産運用のための証券口座。趣味半分出始めた仮想通貨の口座にデビットカード。更には家族の分の口座も管理するとなってくると、お金の流れを管理するのは途端に難しい重労働となってしまいますよね。
そこで、各金融口座と自動で連携してお金の流れを管理する家計簿アプリや貯金支援サービスが巷に存在しているわけですが、お金の流れというのはプライベートな情報でもあるので、自分で管理してみたいと思った方もいるかもしれません。
家庭におけるお金の流れをグラフネットワークでデータモデリングしてみると、どのような形になるのでしょうか。
まずは、お金の流れを管理したいグループごとに、Timeline
ノードを作成してみましょう。今回は、銀行口座・証券口座・クレジットカードなどの用途別にこの Timeline
ノードを作成することとします。
以下の例では、Foo Bank
銀行口座向けのタイムラインを作成します。
CREATE (t:Timeline {id: 1, name: 'Foo Bank'})
RETURN t;
続いて、Timeline
ノードに取引履歴を紐づけてみましょう。
ここで、「2021/09/01 に 10,000 円の給与振込が」があった、という事実をデータセットとして表現してみます。具体的には、年・月・日それぞれを表現するノードを作成し、取引の内容自体を表現する Transaction
ノードと紐付ける、という形でデータモデリングしていきます。
『Graph Databases』2nd Edition の "Chapter 4: Building a Graph Database Application" にも紹介されている Timetree 手法です。
MATCH (timeline:Timeline {name: 'Foo Bank'})
CREATE (transaction:Transaction {text: 'Salary', unit: 'JPY', amount: 10000})
MERGE (timeline)-[:YEAR]->(year:Year {value: 2021})
MERGE (year)-[:MONTH]->(month:Month {value: 9})
MERGE (month)-[:DAY]->(day:Day {value: 1})
MERGE (day)<-[:RECORDED_ON]-(transaction)
MERGE
クエリは、MATCH
と CREATE
の合わせ技のようなクエリです。すでにそのノードが存在する場合はそのノードを返却し、まだ存在しない場合は新規にノードを作成します。
他にも、現金引き落とし、利子の入金、副業先からの給与振込など、いくつかの取引履歴を追加してみましょう。
例:「2021/09/15 に 5,000 円の現金引き落とし」
MATCH (timeline:Timeline {name: 'Foo Bank'})
CREATE (transaction:Transaction {text: 'Withdraw', unit: 'JPY', amount: -5000})
MERGE (timeline)-[:YEAR]->(year:Year {value: 2021})
MERGE (year)-[:MONTH]->(month:Month {value: 9})
MERGE (month)-[:DAY]->(day:Day {value: 15})
MERGE (day)<-[:RECORDED_ON]-(transaction)
例:「2021/09/30 に 10 円の利子振込」
MATCH (timeline:Timeline {name: 'Foo Bank'})
CREATE (transaction:Transaction {text: 'Interest', unit: 'JPY', amount: 10})
MERGE (timeline)-[:YEAR]->(year:Year {value: 2021})
MERGE (year)-[:MONTH]->(month:Month {value: 9})
MERGE (month)-[:DAY]->(day:Day {value: 30})
MERGE (day)<-[:RECORDED_ON]-(transaction)
例:「2021/10/1 に 10,000 円の給与振込」
MATCH (timeline:Timeline {name: 'Foo Bank'})
CREATE (transaction:Transaction {text: 'Salary', unit: 'JPY', amount: 10000})
MERGE (timeline)-[:YEAR]->(year:Year {value: 2021})
MERGE (year)-[:MONTH]->(month:Month {value: 10})
MERGE (month)-[:DAY]->(day:Day {value: 1})
MERGE (day)<-[:RECORDED_ON]-(transaction)
例:「2021/10/12 に 8,000 円の現金引き落とし」
MATCH (timeline:Timeline {name: 'Foo Bank'})
CREATE (transaction:Transaction {text: 'Withdraw', unit: 'JPY', amount: -8000})
MERGE (timeline)-[:YEAR]->(year:Year {value: 2021})
MERGE (year)-[:MONTH]->(month:Month {value: 10})
MERGE (month)-[:DAY]->(day:Day {value: 12})
MERGE (day)<-[:RECORDED_ON]-(transaction)
この時点で、グラフネットワークは以下の形になっていることでしょう。
次に、作成した取引履歴から任意の取引を問い合わせてみましょう。
例えば、「2020/09 に発生した全ての取引」は、以下のクエリで問い合わせることができます。
MATCH (timeline:Timeline {name: 'Foo Bank'})
MATCH (timeline)-[:YEAR]->(year:Year)-[:MONTH]->(month:Month)-[:DAY]->(day:Day)<-[:RECORDED_ON]-(t:Transaction)
WHERE year.value = 2021 AND month.value = 9
RETURN year.value, month.value, day.value, t.amount, t.unit
ORDER BY day.value
結果は以下のテーブルになります。
year.value | month.value | day.value | t.amount | t.unit |
---|---|---|---|---|
2021 | 9 | 1 | 10000 | "JPY" |
2021 | 9 | 15 | -5000 | "JPY" |
2021 | 9 | 30 | 10 | "JPY" |
ここで、取引履歴同士の関連性について見ていきましょう。
現状のネットワークモデルですと、最初に発生した取引から、次に発生した取引への関連性がありません。これだと、例えば「次の取引を問い合わせたい」といったページネーションのような要件であったり、「全ての取引を問い合わせて残高を計算したい」といった要件に対して対応できません。
そこで、取引履歴同士の関連を、:NEXT
リレーションと :PREV
リレーションを使って、前後の関連性を持たせてみてはどうでしょうか。
『Graph Databases』2nd Edition の "Chapter 4: Building a Graph Database Application" にも紹介されている Linked lists 手法です。
:NEXT/:PREV
を追加する Cypher クエリ一覧MATCH (t1:Transaction)-[:RECORDED_ON]->(d:Day)<-[:DAY]-(m:Month)<-[:MONTH]-(y:Year)<-[:YEAR]-(timeline:Timeline)
WHERE timeline.name = 'Foo Bank' AND y.value = 2021 AND m.value = 9 AND d.value = 1
WITH t1
MATCH (t2:Transaction)-[:RECORDED_ON]->(d:Day)<-[:DAY]-(m:Month)<-[:MONTH]-(y:Year)<-[:YEAR]-(timeline:Timeline)
WHERE timeline.name = 'Foo Bank' AND y.value = 2021 AND m.value = 9 AND d.value = 15
CREATE (t1)-[:NEXT]->(t2), (t2)-[:PREV]->(t1)
MATCH (t1:Transaction)-[:RECORDED_ON]->(d:Day)<-[:DAY]-(m:Month)<-[:MONTH]-(y:Year)<-[:YEAR]-(timeline:Timeline)
WHERE timeline.name = 'Foo Bank' AND y.value = 2021 AND m.value = 9 AND d.value = 15
WITH t1
MATCH (t2:Transaction)-[:RECORDED_ON]->(d:Day)<-[:DAY]-(m:Month)<-[:MONTH]-(y:Year)<-[:YEAR]-(timeline:Timeline)
WHERE timeline.name = 'Foo Bank' AND y.value = 2021 AND m.value = 9 AND d.value = 30
CREATE (t1)-[:NEXT]->(t2), (t2)-[:PREV]->(t1)
MATCH (t1:Transaction)-[:RECORDED_ON]->(d:Day)<-[:DAY]-(m:Month)<-[:MONTH]-(y:Year)<-[:YEAR]-(timeline:Timeline)
WHERE timeline.name = 'Foo Bank' AND y.value = 2021 AND m.value = 9 AND d.value = 30
WITH t1
MATCH (t2:Transaction)-[:RECORDED_ON]->(d:Day)<-[:DAY]-(m:Month)<-[:MONTH]-(y:Year)<-[:YEAR]-(timeline:Timeline)
WHERE timeline.name = 'Foo Bank' AND y.value = 2021 AND m.value = 10 AND d.value = 1
CREATE (t1)-[:NEXT]->(t2), (t2)-[:PREV]->(t1)
MATCH (t1:Transaction)-[:RECORDED_ON]->(d:Day)<-[:DAY]-(m:Month)<-[:MONTH]-(y:Year)<-[:YEAR]-(timeline:Timeline)
WHERE timeline.name = 'Foo Bank' AND y.value = 2021 AND m.value = 10 AND d.value = 1
WITH t1
MATCH (t2:Transaction)-[:RECORDED_ON]->(d:Day)<-[:DAY]-(m:Month)<-[:MONTH]-(y:Year)<-[:YEAR]-(timeline:Timeline)
WHERE timeline.name = 'Foo Bank' AND y.value = 2021 AND m.value = 10 AND d.value = 12
CREATE (t1)-[:NEXT]->(t2), (t2)-[:PREV]->(t1)
:FIRST/:LAST
リレーションまた、取引履歴の一番最初と一番最後の取引に対してもポインターがあると便利です。今回は、Timeline
ノードから、:FIRST
/:LAST
リレーションを作成してみましょう。
最初の取引への :FIRST
リレーション:
MATCH (transaction:Transaction)-[:RECORDED_ON]->(d:Day)<-[:DAY]-(m:Month)<-[:MONTH]-(y:Year)<-[:YEAR]-(timeline:Timeline)
WHERE timeline.name = 'Foo Bank' AND y.value = 2021 AND m.value = 9 AND d.value = 1
CREATE (timeline)-[:FIRST]->(transaction)
最後の取引への :LAST
リレーション:
MATCH (transaction:Transaction)-[:RECORDED_ON]->(d:Day)<-[:DAY]-(m:Month)<-[:MONTH]-(y:Year)<-[:YEAR]-(timeline:Timeline)
WHERE timeline.name = 'Foo Bank' AND y.value = 2021 AND m.value = 10 AND d.value = 12
CREATE (timeline)-[:LAST]->(transaction)
:NEXT/:PREV
を利用した問い合わせ例現段階で、Transaction
同士のリレーションは、以下のようになっていることでしょう。
例えば、Foo Bank
タイムラインの全ての取引履歴について、取引を確定させる marked=true
フラグを一律でつけたいという場合、以下の Cypher クエリで実現できます。
MATCH (timeline:Timeline)-[:FIRST]->(start:Transaction),
p=(start)-[:NEXT*]->(finish:Transaction)
WHERE timeline.name = 'Foo Bank'
FOREACH (n IN nodes(p) | SET n.marked = true)
RETURN p
また、例えば「最初から 3 件の取引履歴を取得」するクエリは、以下の Cypher クエリで実現できます。
MATCH (timeline:Timeline)-[:FIRST]->(start:Transaction),
p=(start)-[:NEXT*..3]->(trans:Transaction)-[:RECORDED_ON]->(d:Day)<-[:DAY]-(m:Month)<-[:MONTH]-(y:Year)
WHERE timeline.name = 'Foo Bank'
RETURN y.value, m.value, d.value, trans.text, trans.amount, trans.unit
以上、家庭のお金の流れを管理するための時系列データのネットワークモデルを例に、時系列データのグラフベースでのデータモデリングについて紹介しました。
本記事で紹介した通り、Timetree 手法と Linked lists 手法を組み合わせることで、時系列データに対しても柔軟なクエリをかけます。ぜひ他の時系列データに対しても実験してみてはいかがでしょうか。