Tutorial: Data Modeling Timeline Events

本記事では、時系列データをグラフベースでデータモデリングしていく例を見ていきましょう。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 クエリは、MATCHCREATE の合わせ技のようなクエリです。すでにそのノードが存在する場合はそのノードを返却し、まだ存在しない場合は新規にノードを作成します。

他にも、現金引き落とし、利子の入金、副業先からの給与振込など、いくつかの取引履歴を追加してみましょう。

例:「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)

この時点で、グラフネットワークは以下の形になっていることでしょう。

First Iteration

取引履歴の問い合わせ

次に、作成した取引履歴から任意の取引を問い合わせてみましょう。

例えば、「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 同士のリレーションは、以下のようになっていることでしょう。

Second Iteration

例えば、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 手法を組み合わせることで、時系列データに対しても柔軟なクエリをかけます。ぜひ他の時系列データに対しても実験してみてはいかがでしょうか。

September 30, 2021