Crossfilterでデータをグループ化したり、フィルターかけたりしてみる

概要

Crossfilterはブラウザ上で大きな多変量データを扱うためのJavaScriptのライブラリです。
素早く、グループ化やフィルタリング、データの集約がおこなえます。
Crossfilter自体には、独自のUIはないので、D3.jsなどの可視化ライブラリと併用してください。

GitHub
https://github.com/square/crossfilter

Crossfilter Tutorial
http://blog.rusty.io/2012/09/17/crossfilter-tutorial/

用語説明

factdimensionmeasure について説明します。

「週にどれだけの注文を処理するか?」 という問題を想像してください。

処理したすべての注文について週ごとにグループ化し、週ごとの注文数を計算することで表すことができるでしょう。

この場合、各注文については「fact」といいます。

週は「dimension」になります。これはあなたがどのようにデータをスライスしたかの方法になります。

注文数は「measure」になります。それは、あなたが計算したい値です。

では今度は、 「週ごと、店員ごとにどれだけの売り上げがあるの?」 という問題を想像してください。

再び「facts」を格納して、今度は2つの「dimension」を使います。週と店員です。そして最後に「measure」は注文毎の価格になります。

FactをCrossfilterに設定する

FactをCrosfilterに設定するには、以下のようにJSONデータを指定する。

var payments = crossfilter([
  {date: "2011-11-14T16:17:54Z", quantity: 2, total: 190, tip: 100, type: "tab"},
  {date: "2011-11-14T16:20:19Z", quantity: 2, total: 190, tip: 100, type: "tab"},
  {date: "2011-11-14T16:28:54Z", quantity: 1, total: 300, tip: 200, type: "visa"},
  {date: "2011-11-14T16:30:43Z", quantity: 2, total: 90, tip: 0, type: "tab"},
  {date: "2011-11-14T16:48:46Z", quantity: 2, total: 90, tip: 0, type: "tab"},
  {date: "2011-11-14T16:53:41Z", quantity: 2, total: 90, tip: 0, type: "tab"},
  {date: "2011-11-14T16:54:06Z", quantity: 1, total: 100, tip: 0, type: "cash"},
  {date: "2011-11-14T16:58:03Z", quantity: 2, total: 90, tip: 0, type: "tab"},
  {date: "2011-11-14T17:07:21Z", quantity: 2, total: 90, tip: 0, type: "tab"},
  {date: "2011-11-14T17:22:59Z", quantity: 2, total: 90, tip: 0, type: "tab"},
  {date: "2011-11-14T17:25:45Z", quantity: 2, total: 200, tip: 0, type: "cash"},
  {date: "2011-11-14T17:29:52Z", quantity: 1, total: 200, tip: 100, type: "visa"}
]);

総計の計算

factから総計を求めます。
まず、すべてのデータを1つのグループにまとめてから、reduceCount()を使用して次のようにします。

console.log("Count: " + payments.groupAll().reduceCount().value()) ;
// Count: 12

もし、特定の属性で計算をしたい場合、例えば、totalの合計を求める場合は、reduceSum()を使用します。

console.log("Sum: " + payments.groupAll().reduceSum(function(fact) { return fact.total; }).value());
//  Sum: 1720

実は、reduceCount(),reduceSum()はreduce()という関数を用いて実装されています。つまり、reduce()を独自に実装することで、独自の計算がおこなえます。

下記の例では、レコード数とtotalの合計を取得しています。

console.log(payments.groupAll().reduce(
  function add(p, v) {
    ++p.count;
    p.total += v.total;
    return p;
  }, 
  function remove(p, v) {
    --p.count;
    p.total -= v.total;
    return p;
  }, 
  function init() {
    return {count: 0, total: 0};;
  }
).value());

//  Object {count: 12, total: 1720}

init()では、集計開始時の初期値を指定します。
add()では、グループにデータが加わった場合の計算、
remove()では、グループからデータが外れた場合の計算を行います。

addやremoveは実際にデータを追加、削除した時だけでなく、filterによってデータが外れた場合も実行されます。

add,removeの引数pは集計値、vはfactのオブジェクトになります。

フィルタをかける

単一のdimensionに対する条件でフィルター

たとえば、typeが"tab"のデータの数とtotalの合計を求める場合を考えます。

この場合は、typeのdimensionを作成後、そのdimensionに対してフィルターをかけます。

var paymentsByType = payments.dimension(function(d) { return d.type; });
paymentsByType.filter('tab');

ここでフィルターを指定することにより、paymentsByTypeオブジェクト上で計算した場合を除き、集計時にフィルターが掛かることが確認できます。たとえば先と同じく、個数とtotalの合計をもとめてみます。

console.log("Count: " + payments.groupAll().reduceCount().value()) ;
console.log("Sum: " + payments.groupAll().reduceSum(function(fact) { return fact.total; }).value());

// Count: 8
// Sum: 920

このように、集計した場合に、「type==tab」のデータについてのみ計算されることが確認できます。

複数のdimensionに対する条件でフィルター

フィルターは同時に複数指定することも可能です。
次の例では「type==tab」という条件に加えて、「tip==0」という条件を指定しましょう。
この場合は、新しくtipsのdimensionを作成後、フィルタをかけることになります。

var paymentsByType = payments.dimension(function(d) { return d.type; });
paymentsByType.filter('tab');
var paymentsByTip = payments.dimension(function(d) { return d.tip; });
paymentsByTip.filter(0);
console.log("Count: " + payments.groupAll().reduceCount().value()) ;
console.log("Sum: " + payments.groupAll().reduceSum(function(fact) { return fact.total; }).value());

//Count: 6
//Sum: 540

このように、dimensionを作成することにより、フィルターを追加が行えます。

しかし、dimensionの作成は高価です。それゆえ、32個までしかdimensionの作成はサポートされていません。
もし、これ以上のdimensionを作成したい場合はdispose関数で作成済みのdimensionを始末してから、作成してください。

var ds=[]
for (var i = 0; i < 33; ++i) {
  var d = payments.dimension(function(d) { return d.type; });
  d.dispose(); // これをはずすとエラーになる。
}

フィルターの解除方法

指定したフィルターを解除するには、filterAll()を使用します。

var paymentsByType = payments.dimension(function(d) { return d.type; });
paymentsByType.filter('tab');
console.log("Count: " + payments.groupAll().reduceCount().value()) ;
console.log("Sum: " + payments.groupAll().reduceSum(function(fact) { return fact.total; }).value());
// Count: 8
// Sum:920

paymentsByType.filterAll()
console.log("Count: " + payments.groupAll().reduceCount().value()) ;
console.log("Sum: " + payments.groupAll().reduceSum(function(fact) { return fact.total; }).value());
// Count: 12
// Sum:1720

この例ではfilterAll()後に、先に指定したフィルターの条件が外れて集計されていることが確認できます。

フィルター条件の種類

先の例では一致するか否かの条件でのみフィルターをかけていました。
crossfilterは以下のように、範囲を指定して条件を指定できます。

var paymentsByTip = payments.dimension(function(d) { return d.tip; });
paymentsByTip.filter([100,200]);
console.log("Count: " + payments.groupAll().reduceCount().value()) ;
console.log("Sum: " + payments.groupAll().reduceSum(function(fact) { return fact.total; }).value());

//Count: 3
//Sum: 580

この場合はtipが100以上で200より小さいデータのみ抽出しています。

また、このフィルター条件については、関数で指定することができます。

var paymentsByTip = payments.dimension(function(d) { return d.tip; });
paymentsByTip.filterFunction( function(d) {
  return (d >= 100 && d < 200);
});
console.log("Count: " + payments.groupAll().reduceCount().value()) ;
console.log("Sum: " + payments.groupAll().reduceSum(function(fact) { return fact.total; }).value());

グループ化

グループ化を行うには、グループ化をする属性に対するdimensionを作成し、それに対してグループ化を行います。

var paymentsByType = payments.dimension(function(d) { return d.type; });
var gp = paymentsByType.group();
console.log('グループごとの数');
var countMeasure = gp.reduceCount();
var ret = countMeasure.all()
for (var i = 0; i < ret.length ;++i) {
  console.log(ret[i]);
}
// グループごとの数
// Object {key: "cash", value: 2}
// Object {key: "tab", value: 8}
// Object {key: "visa", value: 2}

dimension.group()にて、特定のdimensionに対するグループ化を行いgroupオブジェクトを作成します。

var gp = paymentsByType.group();

次に、作成したグループに対して、集計を行いmesureを求めます。

var countMeasure = gp.reduceCount();

mesureには集計結果が格納されていますが、これを取得するには、all()を使用します。これにより、配列として全てのデータが取得できます。

var ret = countMeasure.all()
for (var i = 0; i < ret.length ;++i) {
  console.log(ret[i]);
}

all()の代わりにtop()を用いることで指定の件数を大きいもの順から取得することができます。

var ret = countMeasure.top(gp.size())
for (var i = 0; i < ret.length ;++i) {
  console.log(ret[i]);
}
// Object {key: "tab", value: 8}
// Object {key: "visa", value: 2}
// Object {key: "cash", value: 2}

複数の属性によるグループ化

複数の属性を指定してグループ化することも可能です。
以下の例ではtype, quantity毎の合計を取得します。

var dim = payments.dimension(function(d) { return [d.type, d.quantity]; });
var m = dim.group().reduceSum(
  function(d) { return d.total; }
);
var ret = m.all();
for (var i = 0; i < ret.length ;++i) {
  console.log('key:', ret[i]['key'],'value:', ret[i]['value']);
}
key: ["cash", 1] value: 100
key: ["cash", 2] value: 200
key: ["tab", 2] value: 920
key: ["visa", 1] value: 500

フィルタの落とし穴

フィルターを書けたdimensionオブジェクトで計算を行うとフィルターが掛からない状態になります。

var paymentsByType = payments.dimension(function(d) { return d.type; });
paymentsByType.filter('tab');
var ret = paymentsByType.group().reduceCount().all();
for (var i = 0; i < ret.length ;++i) {
  console.log(ret[i]);
}
// Object {key: "cash", value: 2}
// Object {key: "tab", value: 8}
// Object {key: "visa", value: 2}

「type==tab」でフィルターを掛けているのに、cash,visaに数値が計上されています。これはフィルターを掛けたオブジェクト上で計算を行った場合にフィルターが解除されるためです。

もし、期待した動作をしたい場合は、別のdimensionオブジェクトで計算する必要があります。

var paymentsByType = payments.dimension(function(d) { return d.type; });
paymentsByType.filter('tab');
var paymentsByType2 = payments.dimension(function(d) { return d.type; });
var ret = paymentsByType2.group().reduceCount().all();
for (var i = 0; i < ret.length ;++i) {
  console.log(ret[i]);
}
// Object {key: "cash", value: 0}
// Object {key: "tab", value: 8}
// Object {key: "visa", value: 0}

作成済みのcrossfilterにfactを追加、削除をする

crossfilterにデータを追加するには、addメソッドを使用します

console.log("Count: " + payments.size()) ;
payments.add(
   [
     {date: "2011-11-15T17:29:52Z", quantity: 1, total: 200, tip: 100, type: "visa"},
     {date: "2011-11-15T17:29:52Z", quantity: 1, total: 200, tip: 150, type: "cash"}
   ]
);
console.log("Count: " + payments.size()) ;
//Count: 12
//Count: 14

削除する場合はフィルターで削除対象のデータを選択後、remove()を実行します。

console.log("Count: " + payments.size()) ;

var paymentsByType = payments.dimension(function(d) { return d.type; });
paymentsByType.filter('tab');
payments.remove();
console.log("Count: " + payments.size()) ;
//Count: 14
//Count: 6

国土数値情報の行政区域をQGISで編集した後にブラウザに表示してみる

目的

国土数値情報の行政区域の東京都と神奈川県の情報を編集した後にブラウザに表示してみます。

map012.png

Demo
http://needtec.sakura.ne.jp/dc_example/simpleMap.html

データの入手

国土数値情報ダウンロードサービス から 東京都と神奈川県の情報を取得します。
http://nlftp.mlit.go.jp/ksj/gml/datalist/KsjTmplt-N03.html

以下のフォルダが取得できます。

N03-140401_13_GML : 東京都
N03-140401_14_GML : 神奈川県

このフォルダ中のN03-14_13_140401.shpとN03-14_14_140401.shpを使用します。

QGISで地図情報を編集する。

QGISのインストール

QGISを下記からダウンロードしてインストールします。

http://qgis.org/ja/site/

Windows,Mac,Linuxなどの複数のOSで動作します。

shpファイルを開く

1.QGIS Desktopを開きます
map001.png

2.画面が立ち上がるまでゆっくり待ちます(結構遅い)

map002.png

3.「レイヤ」→「レイヤの追加」→「ベクタレイヤの追加」を選択します
map003.png

4.神奈川県の情報であるN03-14_14_140401.shpを選択します。エンコーディングは「System」とします。
map012.png

5.神奈川県の地図が表示されます
map012.png

属性テーブルの文字を確認する

国土数値情報の行政区域には県や市の名前が属性として設定されています。
この値が正しく表示されているか確認します。

1.メニューのアイコンから属性テーブルを開きます
map006.png

2.Windowsの場合、以下のように文字化けしています。
map011.png

東京都と神奈川県を合わせる

東京都と神奈川県を同じレイヤに表示する。
1.N03-14_13_140401.shpを同様の手順で表示します。

map012.png

2.メニューから「領域またはシングルクリックによる地物選択」を選択します。
map012.png

3.東京都の必要な個所を選択します。この際、マウスホイールにより地図の縮小率を変更できます。

map012.png

選択した範囲は以下のように色が変更されます。
map012.png

4.「CTRL+C」または「編集」→「地物のコピー」で指定範囲をコピーします。
map012.png

5.レイヤにて「N03-14_14_140401」を選択して、メニューから「編集モード」に切り替えます。
map012.png

編集中のレイヤには鉛筆のアイコンが表示され、地図上の地物情報の色が変更されます。
map012.png

6.CTRL+Vまたは「編集」→「地物の貼り付け」で編集中のレイヤに、コピーした地物情報を張り付けることができます。

map012.png

7.レイヤから東京のレイヤを削除しても、張り付けた地物は残ることが確認できます。
map012.png

map012.png

ジオメトリを簡素化する

国土数値情報のジオメトリ情報は細かいため、データ量が多くなります。
そこで、簡素化することで、データを削減します。

1.「ベクタ」→「ジオメトリーツール」→「ジオメトリを簡素化する」を選択します。
map012.png

2.簡素化の許容範囲(ここでは0.001)と新規ファイルを入力してOKを押します。

map012.png

3.変換がおわると新しいレイヤが作成されます。
map012.png

GeoJSONへの変換

JavaScriptで使用できるようにGeoJSONに変換します。

1.作成されたレイヤで右クリックして「名前を付けて保存」を選択します。

2.形式と保存先を指定してOKを押します。
map012.png

これにより次のようなJSONファイルが作成されます

{
"type": "FeatureCollection",
"crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:EPSG::4612" } },

"features": [
{ "type": "Feature", "properties": { "N03_001": "神奈川県", "N03_002": null, "N03_003": "相模原市", "N03_004": "緑区", "N03_007": "14151" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 139.161601, 35.667907 ], [ 139.164979, 35.665279 ], [ 139.168358, 35.658556 ], [ 139.166952, 35.651066 ], [ 139.174948, 35.649889 ], [ 139.1790840000001, 35.645572 ], [ 139.189822, 35.646534 ], [ 139.194041, 35.651824 ], [ 139.198483, 35.653018 ], [ 139.202106, 35.653007 ], [ 139.213663, 35.647782 ], [ 139.215751, 35.645457 ], [ 139.214428, 35.641683 ], [ 139.215369, 35.637076 ], [ 139.221431, 35.629466 ], [ 139.219638, 35.625895 ], [ 139.225903, 35.623477 ], } },
// 略

http://needtec.sakura.ne.jp/dc_example/tokyo_kanagawa.geojson

作成したGeoJSONをブラウザで表示する。

GeoJSONをブラウザに表示するにはD3.jsを使用します。

以下からD3.jsをダウンロードしてください。
http://ja.d3js.node.ws/

作成したGeoJSONを表示するコードは以下のようになります。

// 参考
// http://shimz.me/blog/d3-js/2351
// http://shimz.me/blog/d3-js/2526
var width = 800;
var height = 600;
var vbox_x = 0;
var vbox_y = 0;
var vbox_default_width = vbox_width = 500;
var vbox_default_height = vbox_height = 500;

var projection = d3.geo.mercator()
   .center([139.700, 35.4500])
   .scale(30000)
   .translate([width / 2, height / 2]);

//geoJSONのデータをパスに変換する関数を作成
var path = d3.geo.path().projection(projection); 

//ステージとなるsvgを追加
var svg = d3.select("body").append("svg")
    .attr("width", width)
    .attr("height", height)
    .attr("viewBox", "" + vbox_x + " " + vbox_y + " " + vbox_width + " " + vbox_height); //viewBox属性を付加

//geoJSONファイルの読み込み
d3.json("tokyo_kanagawa.geojson", function(json) {
  return svg.append("svg:g")
            .attr("class", "tracts")
            .selectAll("path")
            .data(json.features)
            .enter()
            .append("svg:path")
            .attr("d", path)
            .attr("fill", "#ccc")
            .attr("stroke", "#000");
});

// ドラッグによる移動
var drag = d3.behavior.drag().on("drag", function(d) {
  vbox_x -= d3.event.dx;
  vbox_y -= d3.event.dy;
  return svg.attr("translate", "" + vbox_x + " " + vbox_y);
});
svg.call(drag);

// ズーム処理
zoom = d3.behavior.zoom().on("zoom", function(d) {
  var befere_vbox_width, before_vbox_height, d_x, d_y;
  befere_vbox_width = vbox_width;
  before_vbox_height = vbox_height;
  vbox_width = vbox_default_width * d3.event.scale;
  vbox_height = vbox_default_height * d3.event.scale;
  d_x = (befere_vbox_width - vbox_width) / 2;
  d_y = (before_vbox_height - vbox_height) / 2;
  vbox_x += d_x;
  vbox_y += d_y;
  return svg.attr("viewBox", "" + vbox_x + " " + vbox_y + " " + vbox_width + " " + vbox_height);  //svgタグのviewBox属性を更新
});
svg.call(zoom); 

Demo
http://needtec.sakura.ne.jp/dc_example/simpleMap.html

参考

【D3.js】鶴舞う形の群馬県をSVGで描いてみる
http://shimz.me/blog/d3-js/2351

【D3.js】 viewBox属性を使ったPan/Zoon
http://shimz.me/blog/d3-js/2526

地図とかの空間情報をSQLiteに格納するSpatiaLiteを使用してみる

概要

SpatiaLiteは地図などの空間情報を格納できるSQLiteの拡張です。
ShpファイルやGeoJsonで記述されたGeometry情報をデータベースに格納し利用できます。

http://www.gaia-gis.it/gaia-sins/index.html

配布ファイルの説明

SpatiaLite

コマンドラインからデータベースの操作を行えます。

バイナリの配布
Windowsの場合は以下から任意のプラットフォームのバイナリが取得できます。
http://www.gaia-gis.it/gaia-sins/index.html

mod_spatialite.dll/mod_spatialite.so

SQLiteの拡張モジュールです。
SQLite上で、load_extensionを利用してDLLまたはsoファイルを読み込むことでSpatiaLiteの機能が使用できるようになります。

バイナリの配布
Windowsの場合は以下から任意のプラットフォームのバイナリが取得できます。
http://www.gaia-gis.it/gaia-sins/index.html

ソースコードの配布
以下からlibspatialiteのソースコードをダウンロードしてビルドすることで作成できます。
https://www.gaia-gis.it/fossil/libspatialite/index

spatialite_gui

GUIでspatialiteの機能が使用できます。
spatialite2.png

POLYGONなどのGeometry型の列については、画像としてその内容を確認できます。
spatialite.png

バイナリの配布
Windowsの場合は以下から任意のプラットフォームのバイナリが取得できます。
http://www.gaia-gis.it/gaia-sins/index.html

libspatialiteのビルド

もし、バイナリでmod_spatialiteを取得できない場合は、libspatialiteをビルドする必要があります。

libspatialite-4.2.0のビルドには下記のライブラリが必要です。
proj-4.8.0
proj.4は地図作成投影ライブラリです。
http://trac.osgeo.org/proj/

geos-3.4.2
Geometry EnginはJavaTopologySuiteのC++への移植で、Geometryを処理するための空間述語と機能のAPIです。
http://trac.osgeo.org/geos/

以上をインストールしたあとに、libsatialiteのソースがあるフォルダにて下記のコマンドを実行してください。

./configure --disable-freexl
make
make install

--disable-freexlでExcelとの連携機能を無効にしてます。
これを有効にした場合はfreexlが必要になります。
https://www.gaia-gis.it/fossil/freexl/index

FreeBSDでエラーが出る場合

FreeBSDで下記のようなエラーがでる場合があります

/usr/bin/ld: cannot find -ldl

FreeBSDではdlopen は標準ライブラリなので-ldlは不要です。
src/Makefileで-ldlを検索して削除したのちにmakeをし直しましょう。

参考:
http://sourceforge.net/p/idjc/discussion/458834/thread/246f0841/

mod_spatialiteをSQLiteから使用する。

先に述べたように、load_extensionでspatialiteが利用できますが、いくつか注意点があります。

・SQLiteのバージョンは3.7.17以降である必要があります。

・依存するDLLはすべて、SQLiteから認識できる場所に配置される必要があります。関連のDLLのあるフォルダを環境変数PATHで指定してください。

・SQLiteとmod_spatialiteのプロセスは同じビットでビルドしなければいけません。
 SQLiteが32bitで動作している場合はmod_spatialiteは32bitでビルドする必要がり、SQLiteが64bitで動作している場合はmod_spatialiteは64bitでビルドする必要があります。
 もし、Windowsで64bitバイナリのSQLiteが必要な場合は下記を記事通りに行えば作成できます。
http://qiita.com/akaneko3/items/0e99c3c1366dfbad006f

・load_extensionを利用してDLLまたはsoファイルを読み込むことで、以降spatialiteの機能が利用できるようになります。

SELECT load_extension('/usr/local/lib/mod_spatialite.so');

・SQLite3からデータベースを作成した場合、SpatiaLiteが管理するメタデータが作成されていません。このメタデータを作成するために下記のSQLを実行します。

SELECT InitSpatialMetaData();

チュートリアル

ここでは、実際に国土数値情報の行政区域をspatialiteに格納して利用してみます。

国土数値情報で全国の行政区域を取得する。

まず下記のページから全国の行政区域をダウンロードします。
http://nlftp.mlit.go.jp/ksj/gml/datalist/KsjTmplt-N03.html

空のSQLiteファイルを作成する。

spatialite_guiを起動して空のSQLiteのデータベースファイルを作成します。

[Creating a New (empty) SQLite DB]アイコンを押して、ファイル名を入力します。
spatialite001.png

正常に作成されると、作成されたデータベースへのパスが表示されます。
spatialite002.png

ここまでは普通のデータベースと変わりありません。

shpファイルをインポート

国土数値情報のshpファイルをデータベースに取り込みます。

[Load Shape file]アイコンを押して、国土数値情報の「N03-14_140401.shp」を選択します。

spatialite003.png

この時、エンコードを聞かれるので「CP932」を選択してください。
spatialite004.png

正常に完了すると「N03-14_140401」というテーブルが作成されます。
spatialite005.png

shpファイルから作成されたテーブルの確認

ここではshpファイルから作成されたテーブルの確認を行います。
作成されたテーブルの列を確認するにはテーブルを選択して、右クリックを押して「Show columns」を選択します。

spatialite006.png

spatialite007.png

N03_001~N03_004は県、区、市、N03_007にはコードが格納されているTEXT型の列です。
Geometryは行政区域の形がBlobとして格納されていてPOLYGONというspatiaLiteが指定した型になっています。

このテーブルの作成に実際しようされたSQL文はテーブルを右クリックして[Show CREATE statement]で確認できます。

CREATE TABLE "N03-14_140401" (
"PK_UID" INTEGER PRIMARY KEY AUTOINCREMENT,
"N03_001" TEXT,
"N03_002" TEXT,
"N03_003" TEXT,
"N03_004" TEXT,
"N03_007" TEXT, 
"Geometry" POLYGON)

では実際にテーブルの内容を見てみます。
そのために、SQL文を実行します。

SELECT * FROM "N03-14_140401" LIMIT 10;

spatialite008.png

実行すると、以下のような情報が取得できます。

spatialite009.png

PK_UID~N03_007はテキストでデータが表現されています。
しかし、GeometryはBLOBデータなので人が見て内容が確認できません。

この内容を確認する方法は2通りあります。
1つはBLOB exploreを使用する方法です。
Geometryの列の任意のセルを選択して右クリックを押し「BLOB explore」を選択します。

spatialite010.png

BLOB exploreではバイナリ、画像、SVG、GeoJSONなどの様々な形式でデータを表現することができます。
spatialite011.png

BLOB exploreを使用しない方法としては、SQLでGeometryの列を任意の形式で表示する方法があります。

以下の例ではテキスト形式でGeometry列を表示しています。

-- データが長いので最初の100文字まで表示
SELECT substr(ASTEXT(Geometry), 1, 100) FROM "N03-14_140401" LIMIT 10;

spatialite012.png

ASTEXTでPOLYGON情報を文字として表示できました。これは任意の形式で出力することが可能です。たとえば、ASGEOJSONとした場合は、GeoJSONとして出力されます。

SELECT substr(ASGEOJSON(Geometry), 1, 100) FROM "N03-14_140401" LIMIT 10;

spatialite013.png

ASTEXTやASGEOJSONのSpatiaLiteの提供する関数は下記の「Spatial SQL functions reference guide」に記載されています。

http://www.gaia-gis.it/gaia-sins/spatialite-sql-4.2.0.html

自分で座標情報を持つテーブルを作成してみる

先の例ではshpファイルからテーブルを作りましたが、今回は、座標情報を持つテーブルを自分で作ってみます。

以下のテーブルでは東京タワーなどの地物情報の座標を持つテーブルを作成します。

まず、地物情報を含まない列のみを作成します。

CREATE TABLE "places" (
"PK_UID" INTEGER PRIMARY KEY AUTOINCREMENT,
"name" TEXT)

SQLを実行した後に、refreshを行うとテーブルが追加されます。

spatialite014.png

次に、地物情報を表す列をAddGeometryColumn()を用いて作成します。

Select AddGeometryColumn ('places', 'Geometry', 0, 'POINT', 'XY')

SQLを実行した後に、refreshを行うと列が追加されて、テーブルに地球のアイコンがつきます。

sptialite100.png

テーブルを作成できたら、次はデータを入力します。
以下の例では都庁の座標をテーブルに格納しています。

INSERT INTO places (name, Geometry) VALUES(
  '都庁',
  GeomFromText('POINT(139.692101 35.689634 )')
)

GeomFromTextは文字列をGeometryに変更する関数です。今回はテキスト形式から変換しましたが、GeomFromGeoJSON、GeomFromKmlなどを利用してGeoJSONやKmlから作成することができます。

なお、以下のように一気に作ると、一見それっぽく動作しますが、メタデータが関連付けられていないので、RTreeインデックスを作るときなどに正常に動作しません。

CREATE TABLE "places" (
"PK_UID" INTEGER PRIMARY KEY AUTOINCREMENT,
"name" TEXT,
"Geometry" POINT)

座標情報に対する検索

ここでは座標情報について検索します。
placesテーブルで指定した座標が、どの行政区画に含まれているか検索してみます。

SELECT
  name, "N03-14_140401".*
FROM
 "N03-14_140401"
INNER JOIN places ON
 Contains("N03-14_140401".Geometry, places.Geometry)

Contains関数は第一引数の範囲に第二引数の領域が含まれているか調べます。
この結果は次のようになります。

spatialite015.png

都庁は新宿区にあるので期待どおりの結果といえるでしょう。
先の例ではテーブルに格納されているデータで検索しましたが、直接WHERE区に指定することも可能です。

SELECT
  *
FROM
 "N03-14_140401"
WHERE
  Contains("N03-14_140401".Geometry,GeomFromText('POINT(139.692101 35.689634 )'))

MBRと大まかな検索

先ほどの検索は各座標について正確に検索していましたが、大まかな位置を早く取得したい場合もあります。

この場合は、Minimum Bounding Rectangle - MBRを使用します。
MBRはGEOMETRYのおおまかな大きさを表すものです。

以下のような複雑な図形を赤い枠線の図形とみなします。

spatialite016.png

これにより、不正確ではあるが、早い検索が行えます。
GEOMETRYからMBRを取得するには Envelope()を用います。

SELECT
   ASTEXT(Envelope(Geometry))
FROM
 "N03-14_140401"
LIMIT 10

この結果は以下のように単純な図形になります。
spatialite017.png

では、先ほどと同じ都庁を含む座標をMBRを利用して検索してみましょう。

SELECT
  *
FROM
 "N03-14_140401"
WHERE
  MBRContains("N03-14_140401".Geometry,GeomFromText('POINT(139.692101 35.689634 )'))

検索速度が向上したかわりに以下のように、不要なデータも抽出されてしまっています。

spatialite018.png

RTreeインデックスを利用した検索

通常、DBを扱う場合、インデックスを利用することで検索速度が向上します。
残念なことに、文字や数値に付与できるインデックスをGeometryに付与することはできません。
しかしながら、SpatiaLiteはSQLiteのRTreeを利用して、RTreeインデックスをサポートしています。
RTreeインデックスのアルゴリズムについては下記を参考にしてください。

*Wonderful RTree Spatial Index**
http://www.gaia-gis.it/gaia-sins/spatialite-cookbook/html/rtree.html

RTreeの概要としては、GeometryをMBRをつかって四角形(rectangle)にし、その四角形の交わりに応じてツリーを作り、検索しやすくしています。
rectangleを使ったツリーを作成しているのでRTree indexと言われます。

RTreeインデックスの作成と利用

では、RTreeインデックスを実際使用してみます。
RTreeインデックスを作成するにはCreateSpatialIndex関数を使用します。第一引数にテーブル名、第二引数に列名を指定します。

SELECT CreateSpatialIndex("N03-14_140401", "Geometry") ;

正常に作成できた場合は、1が返り、refresh後にSpatialIndexに新しいテーブルが表示されることが確認できます。
spatialite019.png

spatialite020.png

RTreeインデックスを作成することにより、以下の4つのテーブルが作成されます。

・idx_N03-14_140401_Geometry
・idx_N03-14_140401_Geometry_node
・idx_N03-14_140401_Geometry_parent
・idx_N03-14_140401_Geometry_rowid

idx_N03-14_140401_Geometry以外は内部で使用するテーブルになっており、ユーザーが直接操作することを期待していません。

idx_N03-14_140401_GeometryはVIRTUAL TABLEになっており、実際には内部テーブルを利用して結果を返します。

では、idx_N03-14_140401_Geometryの内容を確認してみます。

SELECT * FROM "idx_N03-14_140401_Geometry" LIMIT 10;

このように各レコードのMRBが格納されています。
spatialite021.png

ユーザはこのテーブルを利用することで、大量のデータの中からデータをフィルターをし、結果を求めることができます。

SELECT 
  * 
FROM "N03-14_140401"
WHERE ROWID IN(
  SELECT
    pkid
  FROM
   "idx_N03-14_140401_Geometry"
  WHERE
    MBRContains(
      BuildMBR(xmin,ymin,xmax,ymax),
      GeomFromText('POINT(139.692101 35.689634 )')
  )
) AND Contains(Geometry, 
  GeomFromText('POINT(139.692101 35.689634 )')
)

通常の検索より早く正確なデータが取得できたかと思います。これはデータ数が多くなれば、なるほどその差が大きく現れます。

このようにRTreeインデックスは通常のインデックスと違い、実テーブルにSQLを実行しただけでは適用されずに、作成されたVIRTUAL TABLEを利用してサブクエリ―やJOINでデータをフィルタリングする役割になります。

別テーブルでインデックスを管理していることで、実テーブルが更新された時に不一致がでるかもしれないという疑問をもつでしょう。
しかし、CreateSpatialIndexを実行した時にトリガーが作成されているので、ユーザは一切手を加えずにインデックスとテーブルの同期がとれます。

このトリガーを確認するには次のようなSQLを実行します。

select name  from sqlite_master where type = 'trigger' and tbl_name='N03-14_140401';

今回は次のようなトリガーが自動で作成されていることが確認できます。

・ggi_N03-14_140401_Geometry
・ggu_N03-14_140401_Geometry
・gii_N03-14_140401_Geometry
・giu_N03-14_140401_Geometry
・gid_N03-14_140401_Geometry

RTreeインデックスの削除

作成したRTreeインデックスを削除するには、DisableSpatialIndexを使用します。

SELECT DisableSpatialIndex("N03-14_140401", "Geometry") ;

refreshを実行するとSpatial Indexが削除されていることが確認できます。
spatialite022.png

pythonからの利用

pythonからSpatiaLiteを使用する場合、SQLiteが適切に利用できる状況なら簡単に利用できます。

Pythonが使っているSqlite3のバージョンは次の手順で調べることができます。

import sqlite3
print (sqlite3.sqlite_version_info)

もし、バージョンを満たさない場合は、SQLITEを更新してください。
Windowsの場合は以下のフォルダのDLLを書き換えるといいでしょう。

C:\Python27\DLLs\sqlite3.dll

以下はPythonでのコードになります。

# !/usr/bin/python
# -*- coding: utf-8 -*-
import sqlite3
import os

# mod_spatialiteのあるフォルダをPATHに加える
os.environ["PATH"] = os.environ["PATH"] + ';C:\\tool\\spatialite\\mod_spatialite-4.2.0-win-x86'
cnn = sqlite3.connect('database/gyouseikuiki.sqlite')

# mod_spatialiteの読み込み
cnn.enable_load_extension(True)
cnn.execute("SELECT load_extension('./mod_spatialite-4.2.0-win-x86/mod_spatialite.dll');")
sql = """
SELECT
  N03_001,
  N03_002,
  N03_003,
  N03_004,
  N03_007, 
  AsGeoJson(Geometry)
FROM
 "N03-14_140401"
WHERE
  MBRContains("N03-14_140401".Geometry,GeomFromText('POINT(139.692101 35.689634 )'))
"""

ret = cnn.execute(sql)
for r in ret:
  print('----------------------------')
  print(r[0].encode('utf_8') )
  print(r[1].encode('utf_8') )
  print(r[2].encode('utf_8') )
  print(r[3].encode('utf_8') )
  print(r[4].encode('utf_8') )
  print(r[5].encode('utf_8') )

環境変数をmod_spatialiteのあるパスに通し、enable_load_extensionで拡張DLLの使用を許可してから、mod_spatialiteをロードします。

あとは、通常通りSQLが実行できます。

enable_load_extensionでエラーになる場合

enable_load_extensionを使用した時に以下のようなエラーが発生する場合があります。

"'sqlite3.Connection' object has no attribute 'enable_load_extension'"

原因として、MaxOSやDebianなどのいくつかのOSにプリインストールされているPythonはコンパイル時に、拡張ライブラリの読み込み機能を無効にしているためです。

https://docs.python.org/2/library/sqlite3.html#f1

これに対応するにはPythonをインストールしなおすしかありません。
インストールする際、Pythonのソースコードのフォルダにある、setup.pyを手で修正する必要があります。

SQLITE_OMIT_LOAD_EXTENSIONを検索して、その行をコメントアウトして、./configure, make, make install を実行します。

もし、make install でエラーが発生する場合は、make install時にエラーがでるなら「make -i altinstall」をためしてみてください。

http://python.g.hatena.ne.jp/nelnal_programing/20101026/1288084718

Peeweeを利用する方法

ORMであるPeeweeライブラリでspatialiteを使用する方法については下記を参考にしてください。

http://qiita.com/mima_ita/items/9d4e1d0afac1865acdbb#spatialitesql%E3%81%B8%E3%81%AE%E6%8E%A5%E7%B6%9A%E6%96%B9%E6%B3%95

phpからの利用

Windows+PHPの場合、簡単に使うことができません。
詳細は下記を参照してください。

PHPで地図とかの空間情報をDBに格納しようとしてSpatialiteを使うと苦労する
http://qiita.com/mima_ita/items/1a90de89f0a194ba843e

その他Tip

測地系の変換をSpatialiteを使って行う

Transform関数を使うと測地系の変換が行えます。
以下の例ではJGD2000/平面直角座標系6を世界測地系に変換しています。

select AsText(Transform(GeomFromText('POINT(-4408.916645 -108767.765479)', 2448), 4326))

参考

SpatiaLite 4.2.0 SQL functions reference list
http://www.gaia-gis.it/gaia-sins/spatialite-sql-4.2.0.html

The SpatiaLite Cookbook
http://www.gaia-gis.it/gaia-sins/spatialite-cookbook/index.html

A quick tutorial to SpatiaLite - a Spatial extension for SQLite(古いが有用)
http://www.gaia-gis.it/gaia-sins/spatialite-tutorial-2.3.1.html

多次元データを複数のグラフや地図に連携して表示するdc.jsを使ってみる

概要

dc.jsは多次元データを複数のグラフや地図に連携して表示することが可能なJavaScriptのライブラリです。

dc.js - Dimensional Charting Javascript Library
http://dc-js.github.io/dc.js/

たとえば次のようなグラフを描画したとします。

dc001.png

dc.jsを使えば、グラフ上の項目をクリックすることで任意の項目のフィルタリングが行えます。
下記の例では「艦種」が「戦艦」かつ「速度」が「低」の艦を抽出しています。

dc002.png

特定の項目をフィルタリングすることにより、全てのグラフが連動して変化していることが確認できます。これによりに、様々な角度でデータを表現することが可能になります。

このサンプルは以下にあります。
http://needtec.sakura.ne.jp/dc_example/exampl001.html

依存ライブラリ

dc.jsは以下のライブラリに依存しています。
・D3.js
・crossfilter.js

D3.js

データに基づいてドキュメントを操作するための JavaScript ライブラリです。D3 はHTML や SVG、 CSSを使ってデータを様々な形で表現することができます。

Data-Driven Documents
http://d3js.org/

日本語ドキュメント
http://ja.d3js.node.ws/

crossfilter.js

Crossfilterはブラウザ上で大きな多変量データを扱うためのJavaScriptのライブラリです。
素早く、グループ化やフィルタリング、データの集約がおこなえます。

github
https://github.com/square/crossfilter

Crossfilterでデータをグループ化したり、フィルターかけたりしてみる
http://qiita.com/mima_ita/items/ba194a2d086c1f67aa7d

サンプルとチュートリアル

ここではdc.jsのサンプルと簡単なチュートリアルを記述します。
使用しているdc.jsは開発版です。

PieChartによる円グラフの例

PieChartは以下のように円でデータの割合を表すグラフです。

dc003.png

このサンプルコードは以下のようになります。

<!DOCTYPE html>
<html lang="en">
<head>
    <title>dc.js - Bar Chart Example</title>
    <meta charset="UTF-8">
    <link rel="stylesheet" type="text/css" href="js/dc.js/dc.css"/>
</head>
<body>

<div id="chart_name"></div>
<div  style="clear:both;"></div>
<div id="chart_type"><p>艦種</p></div>

<script type="text/javascript" src="js/d3/d3.js"></script>
<script type="text/javascript" src="js/crossfilter/crossfilter.js"></script>
<script type="text/javascript" src="js/dc.js/dc.js"></script>
<script type="text/javascript">

var cf = crossfilter([
  {name:"長門", type:"戦艦", speed:"低", range:"長", endurance: 80, fire: 82},
  {name:"陸奥", type:"戦艦", speed:"低", range:"長", endurance: 80, fire: 82},
  {name:"伊勢", type:"戦艦", speed:"低", range:"長", endurance: 74, fire: 74},
  {name:"日向", type:"戦艦", speed:"低", range:"長", endurance: 74, fire: 74},
  {name:"雪風", type:"駆逐艦", speed:"高", range:"短", endurance: 16,fire: 10},
  {name:"赤城", type:"正規空母", speed:"高", range:"短", endurance: 69, fire: 0},
  {name:"加賀", type:"正規空母", speed:"高", range:"短", endurance: 71, fire: 0},
  {name:"蒼龍", type:"正規空母", speed:"高", range:"短", endurance: 50, fire: 0},
  {name:"飛龍", type:"正規空母", speed:"高", range:"短", endurance: 50, fire: 0},
  {name:"島風", type:"駆逐艦", speed:"高", range:"短", endurance: 19,fire: 12},
  {name:"吹雪", type:"駆逐艦", speed:"高", range:"短", endurance: 15,fire: 10},
  {name:"白雪", type:"駆逐艦", speed:"高", range:"短", endurance: 15,fire: 10},
  {name:"初雪", type:"駆逐艦", speed:"高", range:"短", endurance: 15,fire: 10},
  {name:"深雪", type:"駆逐艦", speed:"高", range:"短", endurance: 15,fire: 10},
  {name:"叢雲", type:"駆逐艦", speed:"高", range:"短", endurance: 15,fire: 10},
  {name:"磯波", type:"駆逐艦", speed:"高", range:"短", endurance: 15,fire: 10},
  {name:"綾波", type:"駆逐艦", speed:"高", range:"短", endurance: 15,fire: 10},
  {name:"敷波", type:"駆逐艦", speed:"高", range:"短", endurance: 15,fire: 10},
  {name:"大井", type:"軽巡洋艦", speed:"高", range:"中", endurance: 25, fire: 14},
  {name:"北上", type:"軽巡洋艦", speed:"高", range:"中", endurance: 25, fire: 14},
  {name:"金剛", type:"戦艦", speed:"高", range:"長", endurance: 63, fire: 63},
  {name:"比叡", type:"戦艦", speed:"高", range:"長", endurance: 63, fire: 63},
  {name:"榛名", type:"戦艦", speed:"高", range:"長", endurance: 63, fire: 63},
  {name:"霧島", type:"戦艦", speed:"高", range:"長", endurance: 63, fire: 63},
  {name:"鳳翔", type:"軽空母", speed:"低", range:"短", endurance: 30, fire: 0},
  {name:"扶桑", type:"戦艦", speed:"低", range:"長", endurance:67, fire: 74},
  {name:"山城", type:"戦艦", speed:"低", range:"長", endurance:67, fire: 74},
  {name:"天龍", type:"軽巡洋艦", speed:"高", range:"中", endurance:23, fire: 11},
  {name:"龍田", type:"軽巡洋艦", speed:"高", range:"中", endurance:23, fire: 11},
  {name:"龍驤", type:"軽空母", speed:"高", range:"短", endurance:31, fire: 0}
]);

var dimType = cf.dimension(function(d) {
  return d.type;
});

// 艦種
var gpType = dimType.group().reduceCount();
var chartType = dc.pieChart('#chart_type'); 
chartType
  .width(300)
  .height(220)
  .cx(160)
  .innerRadius(35)
  .slicesCap(3)    // 上位3種のみ表示し、後はその他とする
  .dimension(dimType)
  .group(gpType)
  .ordering(function(t){
    return -t.value;
  })
  .legend(dc.legend())
chartType.render();

</script>

</body>
</html>

デモ
http://needtec.sakura.ne.jp/dc_example/exampl002.html

API
https://github.com/dc-js/dc.js/blob/master/web/docs/api-latest.md#pie-chart

まずcrossfilterのオブジェクトを作成し、艦種によるdimensionを作成します。

var cf = crossfilter([
  {name:"長門", type:"戦艦", speed:"低", range:"長", endurance: 80, fire: 82},
  // 略
]);

var dimType = cf.dimension(function(d) {
  return d.type;
});

次のコードによって、艦種毎にグループ化を行い、艦種毎の数を集計する関数を指定します。

var gpType = dimType.group().reduceCount();

ここまでは、crossfiterが提供する処理です。
このcrossfilterを実際にグラフに描画するコードは次の通りです。

var chartType = dc.pieChart('#chart_type'); 
chartType
  .width(300)
  .height(220)
  .cx(160)
  .innerRadius(35)
  .slicesCap(3)    // 上位3種のみ表示し、後はその他とする
  .dimension(dimType)
  .group(gpType)
  .ordering(function(t){
    return -t.value;
  })
  .legend(dc.legend())
chartType.render();

まずdc.pieChart()で描画先のdivを指定してオブジェクトを作成し、次にpieChartの設定をおこなってからrender()関数で描画を行います。

主なプロパティの説明は次の通りです。

プロパティ 説明
width,heigth 描画領域の大きさを指定します
cx,cy 円の中心座標を指定します
innerRadius 内部の円の半径を指定します。
dc005.png
dimension グラフに関連付けるcrossfilterのdimentionを指定します
group グラフに関連付けるcrossfilterのgroupを指定します
ordering 要素の並び順を指定する関数を指定します
slicesCap グラフに表示する項目の上限を指定します。この上限を超えた場合、Othersに分類されます。
legend 次のようなサンプルを表示します。
dc004.png
ここで指定できるlegendオブジェクトについては下記を参考にしてください.
https://github.com/dc-js/dc.js/blob/master/web/docs/api-latest.md#legend

グラフ中のラベルの変更方法

labelプロパティに描画用の関数を指定します。
この関数は、ラベルを描画時に、引数にデータを指定して呼び出されます。

chartType.label(function(d) {
  console.log('label', d);
  return d.key + ';' + d.value;
});

dc006.png

なお、引数には以下のようなデータが渡されます。

label Object {key: "戦艦", value: 10}
label Object {key: "駆逐艦", value: 10}
label Object {key: "軽巡洋艦", value: 4}
label Object {others: Array[2], key: "Others", value: 6}

グラフ中の項目をマウスオーバーした際のツールチップの変更

マウスオーバーでツールチップが表示されますが、そのツールチップを次のようなコードで変更することが可能です。

chartType.title(function(d) {
  console.log('title', d);
  return '★' + d.key + ';' + d.value;
});

dc007.png

色の変更方法

下記のような実装で、グラフの色を変更できます。

chartType.colors(function(keyName) {
  console.log('color', keyName);
  var pallet = {
    '戦艦' : '#ff0000',    // 6桁のHEX
    '駆逐艦' : '#00f',     // 3桁で表示
    '軽巡洋艦' : 'Green'  //文字で表示
  };
  var r = pallet[keyName];
  if (!r) r = '#000';
  return r;
});

dc008.png

その他、色に関する詳細は下記を参考にしてください。
https://github.com/dc-js/dc.js/blob/master/web/docs/api-latest.md#color-mixin

RowChartによる棒グラフの例

RowChartは以下のようにデータを表現します。

dc009.png





    dc.js - Bar Chart Example
    
    



艦種