pikesaku’s blog

個人的な勉強メモです。記載内容について一切の責任は持ちません。

グラフデータベースNeo4j動作検証

やってみる

 
路線図とかをインプットしてみて最短距離検索とかやる場合、どうやるのかな?
と思っていたら、参考URLがあったので、やってみる!
※正直しっかり理解できてません。。。
 

データ登録スクリプト

 
山手線の路線図を作るだけ。

# -*- coding: utf-8 -*-

import urllib.request as urllib
from xml.dom import minidom, Node
from neo4j.v1 import GraphDatabase, basic_auth


def get_station_info(line_cd):
    join_list = []
    url = 'http://www.ekidata.jp/api/n/' + str(line_cd) + '.xml'
    dom = minidom.parse(urllib.urlopen(url))
    all_station = set()
    all_kukan = dict()
    for join_node in dom.getElementsByTagName('station_join'):
        for join_node_child in join_node.childNodes:
            if join_node_child.nodeType == Node.TEXT_NODE:
                continue
            name = join_node_child.nodeName
            value = join_node_child.childNodes.item(0).nodeValue
            if name == 'station_name1':
                station_name1 = value
                all_station.add(value)
            elif name == 'station_name2':
                station_name2 = value
                all_station.add(value)
        all_kukan[station_name1] = station_name2
    return all_station, all_kukan


def connect_srv():
    driver = GraphDatabase.driver("bolt://localhost", auth=basic_auth('neo4j', 'neo4j'))
    return driver


if __name__ == '__main__':
    all_station, all_kukan = get_station_info(11302)
    driver = connect_srv()

    # Delete Data
    session = driver.session()
    q = 'match (a) optional match (a)-[r]-() delete a,r'
    session.run(q)


    # Add Data
    for station in all_station:
        q = 'create (a:' + station + '{name:' + "'" + station + "'" + '})'
        session.run(q)

    # Add Connectivity
    for src,dst in all_kukan.items():
        q = 'match (a:' + src + '{name:' + "'" + src + "'" + '}), '
        q += '(b:' + dst + '{name:' + "'" + dst + "'" + '}) '
        q += 'merge (a)-[c:rosen]->(b)'
        session.run(q)

    session.close()

 
上記を実行すると、こんなグラフが生成される。
f:id:pikesaku:20170717193330p:plain
 
駅の色に意味なし。neo4jが登録順にラベルの色をつけているためと想定される。
駅間の距離も意味なし。
内・外回りのリレーションを作ろうと思ったが、双方向リレーションを設定するのは、非推奨との情報あり。
Neo4jで学ぶGraph DB入門 - omotenashi-mind
Modelling Data in Neo4j: Bidirectional Relationships
参照時は、方向性を無視した探し方で対応するのが良いとのこと。
 

最短距離を検索

 
GUIから以下クエリを実行

池袋→東京

MATCH (from: 池袋 {name:'池袋'}), (to: 東京 {name:'東京'}), path=allShortestPaths((from)-[*]-(to)) return from,to,path

 
f:id:pikesaku:20170717195954p:plain
 

池袋→新宿

MATCH (from: 池袋 {name:'池袋'}), (to: 新宿 {name:'新宿'}), path=allShortestPaths((from)-[*]-(to)) return from,to,path

 
f:id:pikesaku:20170717200128p:plain
 

グラフデータベースNeo4jのクエリ言語Cypherの動作検証

はじめに

 
データ操作はCypherと呼ばれるクエリ言語を利用
Cypherは以下4つのIFより実行可能
①Neo4jウェブインターフェー
②Neo4jシェル
REST API
④Neo4jドライバー
 
Cypher動作検証にNeo4jシェルを利用した。
事前に以下セットアップを実施した。
pikesaku.hatenablog.com

 

データ構成要素

ノード(データの最小単位)

①ラベル
②属性
 

関係性

①タイプ
②方向性
③属性
  

データ構造

 
上記参考URLより引用

f:id:pikesaku:20170717140119p:plain

 
参考URL筆者の考察の箇条書き
・ノードとノード間の関係性を明確に分離して管理
・ノードはノードストア・関係者は関係性ストアで管理
・同じラベルを持ったノード群がRDMSのテーブル、ノードの属性はレコードのような役割をする。
・関係性はタイプという情報を持つ。タイプは属性情報を保持し、多様性のある連結を表現。
 

データ操作

 
neo4j-shellで記述

全ノード参照

 

neo4j-sh (?)$ match (a) return a;
+---+
| a |
+---+
+---+
0 row
9 ms
neo4j-sh (?)$ 

  
上記で全ノードの情報確認が可能
上記のaは識別子。クエリ実行時のみ利用される識別子。aでなくてもOK
 

neo4j-sh (?)$ match (hoge) return hoge;
+------+
| hoge |
+------+
+------+
0 row
18 ms
neo4j-sh (?)$ 

 
returnや他操作(後述するcreate or merge or delete等)を指定しないとエラーになる。
 

neo4j-sh (?)$ match (a);
1 ms

WARNING: Query cannot conclude with MATCH (must be RETURN or an update clause) (line 1, column 1 (offset: 0))
"match (a)"
 ^
neo4j-sh (?)$ 

 

ノード登録

 

ラベルも属性もないノードを登録

neo4j-sh (?)$ create (hoge);
+-------------------+
| No data returned. |
+-------------------+
Nodes created: 1
5 ms
neo4j-sh (?)$ match (a) return a;
+-----------+
| a         |
+-----------+
| Node[4]{} |
+-----------+
1 row
6 ms
neo4j-sh (?)$ 

 
Node[4]の4は内部でノードを特定するためのID番号が使われていると想定。
上記が[4]になった理由は、上記実行前に登録したデータがあったため。
 

登録後に状態を表示させるには、returnを付与

neo4j-sh (?)$ create (hoge2) return hoge2;;
+-----------+
| hoge2     |
+-----------+
| Node[0]{} |
+-----------+
1 row
Nodes created: 1
42 ms
neo4j-sh (?)$ 

 

ラベル有・属性なしで登録

識別子:ラベルで記述。
※くどいが識別子はクエリ実行時のみ利用される情報。なんでもOK。自分はこの情報を知らず、結構はまった。。。。。
以下の場合、

neo4j-sh (?)$ create (test:hoge) return test;
+-----------+
| test      |
+-----------+
| Node[2]{} |
+-----------+
1 row
Nodes created: 1
Labels added: 1
20 ms
neo4j-sh (?)$ 

 

ラベル有・属性ありノードを登録

neo4j-sh (?)$ create (test:hoge{zoku1:"test", zoku2:1000, zoku3:True}) return test;
+---------------------------------------------+
| test                                        |
+---------------------------------------------+
| Node[3]{zoku1:"test",zoku2:1000,zoku3:true} |
+---------------------------------------------+
1 row
Nodes created: 1
Properties set: 3
Labels added: 1
37 ms
neo4j-sh (?)$ 

 

全ノード削除

neo4j-sh (?)$ match (a) delete a;
+-------------------+
| No data returned. |
+-------------------+
Nodes deleted: 5
21 ms
neo4j-sh (?)$ 

 

関係性のあるノードの登録

neo4j-sh (?)$ create (a:hoge1{zoku:"test"}), (b:hoge2{zoku:"test"}), (a)-[kankeiseitype:huga]->(b) return a,b,kankeiseitype;
+-------------------------------------------------------------+
| a                    | b                    | kankeiseitype |
+-------------------------------------------------------------+
| Node[8]{zoku:"test"} | Node[9]{zoku:"test"} | :huga[1]{}    |
+-------------------------------------------------------------+
[f:id:pikesaku:20170717155751p:plain]1 row
Nodes created: 2
Relationships created: 1
Properties set: 2
Labels added: 2
18 ms
neo4j-sh (?)$ 

 
上記では、ラベルhoge1を持つノードとラベルhoge2を持つノードを関係性タイプhugaで関係性をもたせている。
GUIでは以下の様に表示される。

f:id:pikesaku:20170717155832p:plain
 

関係性のあるノードの削除

ノード削除ではエラーになる。事前に関係性を削除する必要あり。

neo4j-sh (?)$ match (a) delete a;
+-------------------+
| No data returned. |
+-------------------+
Nodes deleted: 2
4 ms
ConstraintViolationException: Cannot delete node<8>, because it still has relationships. To delete this node, you must first delete its relationships.
neo4j-sh (?)$ match (a) optional match (a)-[r]-() delete a,r;
+-------------------+
| No data returned. |
+-------------------+
Nodes deleted: 2
Relationships deleted: 1
17 ms
neo4j-sh (?)$ 

 
optional matchで関係性も削除すればOK
 

まとめ

 
他mergeなどいろいろな操作が可能だが、参考URLをみればなんとかできそう。
個人的な動作把握のためのメモとしては、これまでとする。

グラフデータベースNeo4jのセットアップ

環境構築

 
環境: Mac Sierra
 
Javaインストー

brew cask install java

 
brewでNeo4jをインストー

brew install neo4j

 

やってみる

 
①shellからCypher実行するために設定ファイル変更
stackoverflow.com
 

$ diff /usr/local/Cellar/neo4j/3.2.2/libexec/conf/neo4j.conf /usr/local/Cellar/neo4j/3.2.2/libexec/conf/neo4j.conf_org 
233c233
< dbms.shell.enabled=true
---
> #dbms.shell.enabled=true
235c235
< dbms.shell.host=127.0.0.1
---
> #dbms.shell.host=127.0.0.1
237c237
< dbms.shell.port=1337
---
> #dbms.shell.port=1337

 
上記変更しないと、neo4j-shell実行時に
Connection refused
エラーになる。
 
②neo4j起動

neo4j start

 
※停止はneo4j stop
 
③Webアクセスしneo4jユーザーでログインしパスワード変更
http://localhost:7474/browser/
 
④neo4j-shellログイン確認

$ neo4j-shell

データ構造としてのグラフとは?

グラフとは?

 
ノードとエッジで表現するデータ型
ノードは頂点
エッジはノード間の連結関係を示す
グラフ理論を適用できる
mathtrain.jp

 

グラフの数学的表現方法

以下の2つがあり。
 

①隣接行列

 
以下URLより引用
隣接行列 — WTOPIA v1.0 documentation
 

行列という名の通り, グラフを 2 次元配列で表現する. 配列のインデックスがノードの番号に対応する. 例えば, この 2 次元配列を M とすると, M[i][j] がノード i と ノード j の関係を表す

f:id:pikesaku:20170716223645p:plain

 
連結の向き・重みを考慮した場合の表現もある。
 

②隣接リスト

 
以下URLより引用
mathwords.net
 

f:id:pikesaku:20170716224306p:plain

 

グラフ表現の例

 
tech-blog.abeja.asia
グラフ表現
 
画像や文章の表現も可能。
画像表現に適しているが、グラフ構造を処理するには、計算量が莫大になる問題があり。
 
※Convolution=畳み込み(難しい。。。)
qiita.com

pythonのcymruwhoisモジュールでIPアドレスからwhois情報取得

使い方が簡単

#! /usr/bin/env python3
# -*- coding: utf-8 -*-


from cymruwhois import Client

# 一個だけデータを取得する場合
c = Client()
r = c.lookup('8.8.8.8')
print('ANS:    ' + r.asn)
print('IP :    ' + r.ip)
print('PREFIX: ' + r.prefix)
print('CC:     ' + r.cc)
print('OWNER:  ' + r.owner)

# 複数データを取得する場合

# 悪い例
# 同じCIDR内だが、都度クエリが発生
# 以下URLにも注意書きがあり。loop内で使うと性能悪い
# http://pythonhosted.org/cymruwhois/api.html
for i in range(256):
    ip = '8.8.8.' + str(i + 1)
    r = c.lookup(ip)
    print(r.asn)


# 良い例
# 同じCIDR内なので、通信は最初の一回だけ。
ips = list()
for i in range(254):
    ips.append('8.8.8.' + str(i + 1))

for r in c.lookupmany(ips):
    print(r.asn)


信頼性もありそう

・CentOS7のwgetコマンドは引数に指定されたIPを管理するレジストラを探すためにDNS(逆引き&TXTレコード)を使っている。
 その問い合わせ先がcymruのサーバであった。ここでIPアドレスを管理するレジストラ情報を得て、whoisプロトコルで情報を得ている。
 →CentOSwgetに組み込まれているサービスに関与している組織。

・cymruとは?
 
www.team-cymru.org

 Googleや名だたるITサービスプロバイダーがパートナー

備考

whoisはレスポンスの規則がないとの情報あり。RDAPなるものを知り、これがいいと思ったがCentOS7環境でRDAP対応のipwhoisが上手く動かず、困っていたところで見つけたモジュール。
Macではipwhois正常に動く。。。使いやすメリットでcymruwhoisを使おう。cymruwhoisはRDAP未対応?(デフォルト動作ではwhoisプロトコルを利用)

jubatus anomalyのnum_rules動作検証(jubaanomalyにデータを投入するツール)

説明

jubaanomalyにデータを学習させたり、外れ値を計算させるツール。

指定可能なオプションは以下の通り。

オプション 意味
-t num_rulesのタイプ(num or str or log)を指定。必須指定オプション
-c データ学習前に既存データをクリアする。省略可能。デフォルトは無効
-s 学習データをファイルに保存する。省略可能。デフォルトは無効
-d 学習済みデータのID一覧を出力する。省略可能。デフォルトは無効
-o データを学習せず外れ値の計算だけやる。省略可能。デフォルトは無効

-dは-c,-s,-o,-tと排他
-sは-oと排他

anom_test.py

# -*- coding: utf-8 -*-

import signal
import argparse
import numpy as np
import matplotlib.pyplot as plt

from jubatus.anomaly import client
from jubatus.common import Datum

parser = argparse.ArgumentParser(description='This is jubaanomaly test tool')
parser.add_argument('name',                                help='data name')
parser.add_argument('-t', '--type',      dest='type',      help='type num_rules')
parser.add_argument('-c', '--clear',     dest='clear',     help='clear data',     action="store_true", default=False)
parser.add_argument('-s', '--save',      dest='save',      help='save or not',    action="store_true", default=False)
parser.add_argument('-d', '--display',   dest='display',   help='save or not',    action="store_true", default=False)
parser.add_argument('-o', '--only_calc', dest='only_calc', help='only calc',      action="store_true", default=False)

args = parser.parse_args()


def display_result(rdata, idata):
    dpoint = 5
    idx = 0
    result = dict()
    inf = float("inf")
    for i in idata:
        print(i, rdata[idx])
        if rdata[idx] != inf:
            result[i] = round(rdata[idx], dpoint)
        idx += 1

    plt.plot(list(result.keys()), list(result.values()), 'o')
    plt.title(u'Outliers')
    plt.show()


def connect_srv():
    try:
        anom = client.Anomaly("127.0.0.1", 9199, args.name)
        if args.clear:
            anom.clear()
    except:
        err_fin('failed to connect to server')
    return anom


def study_data(anom):
    idata = list()
    rdata = list()
    while True:
        try:
            line = float(input().strip())
            idata.append(line)
        except EOFError:
            break

        datum = Datum()
        if args.type == 'num':
            d_type = 'd1'
        elif args.type == 'str':
            d_type = 'd2'
        elif args.type == 'log':
            d_type = 'd3'
        datum.add_number(d_type, line)
        if args.only_calc:
            ret = anom.calc_score(datum)
#            print(ret)
        else:
            ret = anom.add(datum)
            #if (ret.score != float('Inf')) and (ret.score != 1.0):
            print(ret)
        rdata.append(ret)

    if args.save:
        anom.save(args.name)
    return rdata, idata


def do_exit(sig, stack):
    print('You pressed Ctrl+C.')
    print('Stop running the job.')
    exit(0)


def display_data(anom):
    try:
        data = anom.get_all_rows()
    except:
        err_fin(args.name + ' failed to get data')
    if not data:
        print(args.name + ' has no data')
    else:
        for i in sorted(data, key=float):
            print(i)


def err_fin(mes):
   print('Error: ' + mes)
   exit(1)


def chk_args():
    # -dと-c,-s,-n,-oは排他
    type_rules = ['num', 'str', 'log']
    if args.display and (args.clear or args.save or args.type or args.only_calc):
        err_fin('can not use -d with -c,-s,-o,-t')
    if args.save and args.only_calc:
        err_fin('can not use -s with -o')
    if not args.display and not args.type in type_rules:
        err_fin('invlid rule_type. ' + 'should be ' + ', '.join(type_rules))


if __name__ == '__main__':
    signal.signal(signal.SIGINT, do_exit)
    chk_args()
    anom = connect_srv()
    if args.display:
        display_data(anom)
        exit()
    else:
        rdata, idata = study_data(anom)
        if args.only_calc:
            display_result(rdata, idata)

使い方例

データを初期化してから学習させる(num_rulesのtypeはstr、学習データのファイル保存なし)

10個のデータを学習

$ seq 1 10 | python ./anom_test.py -c -t str test
id_with_score{id: 220, score: inf}
id_with_score{id: 221, score: 1.0}
id_with_score{id: 222, score: 0.9926380515098572}
id_with_score{id: 223, score: 1.0}
id_with_score{id: 224, score: 1.0065279006958008}
id_with_score{id: 225, score: 1.010113000869751}
id_with_score{id: 226, score: 0.9977192282676697}
id_with_score{id: 227, score: 0.993184506893158}
id_with_score{id: 228, score: 0.9930071830749512}
id_with_score{id: 229, score: 0.9959399700164795}

学習済みデータの確認

$ python ./anom_test.py -d test
220
221
222
223
224
225
226
227
228
229
$

外れ値計算 & 分布図出力

$ seq 1 10 | python ./anom_test.py -o -t str test
1.0 0.9999997615814209
2.0 0.9999999403953552
3.0 1.0
4.0 0.9999997615814209
5.0 0.9999998211860657
6.0 1.0
7.0 1.0
8.0 0.9999999403953552
9.0 1.0
10.0 1.0

f:id:pikesaku:20170709223633p:plain

jubatus anomalyのnum_rules動作検証(テストデータ分布状況出力ツール)

説明

データを受けてヒストグラムとパイチャートを出力
一次元データのみ対応。

dist_disp.py

# -*- coding: utf-8 -*-

import matplotlib.pyplot as plt


def make_pie_data(data):
    pie_data = dict()
    for i in data:
        pie_data[i] = data.count(i)
    return pie_data


def display_dist(data):
    # ヒストグラム生成
    plt.subplot(2, 1, 1)
    plt.hist(data, bins=50)
    plt.title(u'Input data histgram')

    # パイチャート生成
    pie_data = make_pie_data(data)
    plt.subplot(2, 1, 2)
    plt.pie(list(pie_data.values()), labels=list(pie_data.keys()))
    plt.title(u'Input data pie chart')

    # タイトルの被りを防ぐ
    plt.tight_layout()
    plt.show()


def get_data():
    data = list()
    while True:
        try:
            line = float(input().strip())
            data.append(line)
        except EOFError:
            break
    return data


if __name__ == '__main__':
    data = get_data()
    if data:
        display_dist(data)

使い方例

$ (seq 1 10;echo 1; echo 3) | python ./disp_dist.py

f:id:pikesaku:20170709214433p:plain