Ken WagatsumaSRE at Neo4j

Tutorial: Data Modeling Timeline Events

★★ intermediate

September 30, 2021

本記事では、時系列データをグラフベースでデータモデリングしていく例を見ていきましょう。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.valuemonth.valueday.valuet.amountt.unit
20219110000"JPY"
2021915-5000"JPY"
202193010"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 手法を組み合わせることで、時系列データに対しても柔軟なクエリをかけます。ぜひ他の時系列データに対しても実験してみてはいかがでしょうか。

Recommended Posts

  1. ★★★ advanced
    blog-post-and-urls-relationship
    本ブログでは、全記事のメタデータを HTML ビルド時に解析し、Cypher クエリに変換した上で Neo4j Aura に保存しています。 今回、記事本文に含まれる外部サイトへのリンク数も解析対象に付与しました。 データモデリングの一例としてご紹介します。 課題 ブログの記事には、Wikipedia…
  2. ★★★ advanced
    blog-relevant-tags-internals-datamodeling-sketch
    つい先日、本ブログに、Relevant Tags の機能を実装しました。 例えば、#neo4j タグページの下部に、以下の UI が追加されていることに気づいた方もいらっしゃるかもしれません。 実は、このタグの関連性を計算するにあたって、Neo4j Aura を利用しています。 本ブログでは、Relevant Tags…