小さなエンドウ豆

まだまだいろいろ勉強中

【Rails】Javascript で使う変数等を環境変数にまとめる

Rails プロジェクトの環境変数を JS で使用する

Rails アプリケーションで外出したくない環境変数.env に書き込んで dotenv-rails を使って呼び出すのが一般的らしいので設定してみました。

ただ、今回は mapbox という Javascript のライブラリの API token を .env に定義したいので、Rails環境変数を JS側でも使用できるように gon という Gem を使って参照する方法をまとめていきます。

Gem のインストール

まずRails プロジェクト配下の Gemfile に下記を追記する。

 gem 'gon'
 gem 'dotenv-rails'

※バージョンは今回指定していません...
それから $ bundle install でそれぞれインストールします。

.env の設定

次に .env ファイルを作ります。
例えば、下記のように環境変数を定義します。

MAPBOX_ACCESS_KEY={{自分のAPI token}}

Rails環境変数を扱う

Rails で定義した環境変数( MAPBOX_ACCESS_KEY )をコントローラで使用する際は下記のように書きます。

def show
  p ENV['MAPBOX_ACCESS_KEY']
end

※確認のためにprint してます
ENV[key] って感じで呼び出すことができます。便利!

Rails 環境変数を JS ファイルで使用

先程説明した gon という Gem を使用して環境変数を JS に渡します。
まずコントローラ側で下記のように書きます。

def show
  gon.mapbox_access_key = ENV['MAPBOX_ACCESS_KEY']
end

これでバックエンド側での設定は終わりです。
次に JS ファイル側に下記のように書いて使用します。

mapboxgl.accessToken=gon.mapbox_access_key;

gon.{{変数名}} で参照できるのすごく便利です。

最後に

.env は公開したくないので .gitignore などに追加しておきましょう。

参考 qiita.com

Kaggle に初挑戦

Kaggle とは

簡単に言うにまとめると

  1. データ持ってる人がデータ投稿する
  2. 投稿者がコンペを開く
  3. 世界中のデータサイエンティストたちが分析を行う
  4. 分析結果を Kaggle に提出してスコアが算出される
  5. 算出されたスコアをもとに競う

細かくいうとこんな感じです。
僕も存在は知っていたのですが、なかなか手が出せずにいました。
今日は Kaggle の中でも有名な タイタニックの生存予測」 のコンペに提出するまでの流れを説明したいと思います。

Kaggle 始め方

Kaggle

まずはアクセスしてアカウントを作ります。
FacebookGoogle のアカウントでサインインできるのでそれらを使うと便利かもです。

コンペへの参加の仕方

アカウントを作るとよくわからない偉い人たちのフィードが現れます。
とりあえず、これらは無視して、ページ上部の Competitions とクリックします。 f:id:h-piiice16:20170820173325p:plain

これですね。
これをクリックすると、いろいろなコンペが表示されます。
スクロールするとタイタニックのコンペもあるので、これをクリックします。

f:id:h-piiice16:20170820173600p:plain

すると、コンペの詳細ページが表示されます。
ここで予測に必要なデータをダウンロードします。
f:id:h-piiice16:20170820173828p:plain

ここで、以下の2つのファイルをダウンロードします。

(名前からして説明はいらないかもですけど、一応訓練データと予測用のデータです。)

生存予測

今回は提出するまでを目標としているので予測に関しては凝りませんでした。
使うモデルは Random Forest です。
予測を行うにあたり便利なのが Jupyter notebook でした。
これに関しては以下の記事を参考にして Anaconda をインストールしていただくと環境が整うと思います。

h-piiice16.hatenablog.com

train.csv の中身は数値データだけでなく文字列で与えられるデータもあります。
ランダムフォレストに学習させるデータは数値データでなくてはならないので train.csv の中からは以下のカラムを抜き出して使用しました。

  • Pclass … 乗客のクラス(1が高いらしい)
  • Sex … 性別
  • Age … 年齢
  • SibSp … 兄弟と配偶者の数
  • Parch … 親と子供の数
  • Fare … 料金

前処理として以下の2つを行いました。参考

# male ... 0, female ... 1
data = pd.read_csv('train.csv').replace('male', 0).replace('female', 1)

# データの抜き出し
train_data = data[['Pclass', 'Sex', 'Age', 'SibSp', 'Parch', 'Fare]]

# Age の欠損値を中央値で埋める
train_data["Age"].fillna(train_data.Age.median(), inplace=True)

# Fare の欠損値を中央値で埋める
train_data["Fare"].fillna(train_data.Fare.median(), inplace=True)

性別のデータは男性が male、女性が female で与えられているためそれを数値に変換します。
欠損値(NaN)に関しては中央値で埋めます。
これを test.csv にも行います。
そして scikit-learn の Random Forest を使ってモデルを作り、結果を出します。
ランダムフォレスト使ってタイタニックの生存予測をする例が以下になります。

# lable_data は正解データ
# label_data = data['Survived'] で抽出
# モデルの作成
from sklearn.ensemble import RandomForestClassifier
model = RandomForestClassifier()
model.fit(train_data, label_data)

# test_data は test.csv を加工したもの
# 予測
output = model.predict(test_data)

# 提出用のファイルへの書き出し
# test_origin は test.csv を読み込んだもの
test_origin['Survived'] = output
submit_data = test_origin[['PassengerId', 'Survived']]
submit_data.to_csv('output.csv', index=False)

これで output.csv に予測結果が出力される。
PassengerId は予測用のデータに付与されている乗客を表すIDのことです。
提出用のファイルの形式はこの PassengerId と 生存結果を表す Survived を並べたものです。

提出

先程のコンペの詳細ページへ戻り、Submit Prediction を押します。 このページで output.csv をアップロードし提出完了です!
このあとスコアが計算され、順位も出ます。(僕は6000番くらいでした…)

まとめ

以上が提出までの手順となります。
タイタニック以外にもコンペがたくさんあるのでなにかしらにチャレンジしたと思います。
今回提出に当たって便利だと思ったものを以下にまとめます。

  • Chrome ? の翻訳
    Kaggle のページで右クリックでできる
    ページが英語なので結構役に立つw
  • Jupyter notebook
    書いたコードを残せるので便利!
    グラフなどもかけるので分析が捗りそう
  • pandas
    python のライブラリで csv の読み込みや書き出し、欠損値の埋め合わせやデータの抽出などが簡単にできる

これで Kaggle では怖いものなしだと思われます(たぶん)

Railsでdb:seedを用いたデータの用意

db:seed とは

Rails アプリに予め必要なデータをデータベースに一括で投入することができる機能。今回はアプリ内でenumっぽく使いたいデータをどんどん投入していきます。

手順

まずはモデルとmigrateファイルを作り、db:migrateを実行します。

$ rails generate model area name:string
$ rails db:migrate # railsは5系です

これでAreaテーブルが作成されます。(ここらへん曖昧)

で、Rails プロジェクトの db/seeds.rb と以下のように編集します。

Area.create({name: '北海道'})
Area.create({name: '東北'})
Area.create({name: '関東'})
Area.create({name: '甲信越'})
Area.create({name: '東海・北陸'})
Area.create({name: '近畿'})
Area.create({name: '中国'})
Area.create({name: '四国'})
Area.create({name: '九州'})

保存して以下のコマンドを実行します。

$ rails db:seed

すると Area テーブルに9行のレコードが生成されます。 試しに rails console で見てみます。

$ rails console

irb> Area.all

   (2.9ms)  SET NAMES utf8,  @@SESSION.sql_mode = CONCAT(CONCAT(@@sql_mode, ',STRICT_ALL_TABLES'), ',NO_AUTO_VALUE_ON_ZERO'),  @@SESSION.sql_auto_is_null = 0, @@SESSION.wait_timeout = 2147483
  Area Load (1.5ms)  SELECT  `areas`.* FROM `areas` LIMIT 11
=> #<ActiveRecord::Relation [#<Area id: 1, name: "北海道", created_at: "2017-08-11 04:16:13", updated_at: "2017-08-11 04:16:13">, #<Area id: 2, name: "東北", created_at: "2017-08-11 04:16:13", updated_at: "2017-08-11 04:16:13">, #<Area id: 3, name: "関東", created_at: "2017-08-11 04:16:13", updated_at: "2017-08-11 04:16:13">, #<Area id: 4, name: "甲信越", created_at: "2017-08-11 04:16:13", updated_at: "2017-08-11 04:16:13">, #<Area id: 5, name: "東海・北陸", created_at: "2017-08-11 04:16:13", updated_at: "2017-08-11 04:16:13">, #<Area id: 6, name: "近畿", created_at: "2017-08-11 04:16:13", updated_at: "2017-08-11 04:16:13">, #<Area id: 7, name: "中国", created_at: "2017-08-11 04:16:13", updated_at: "2017-08-11 04:16:13">, #<Area id: 8, name: "四国", created_at: "2017-08-11 04:16:13", updated_at: "2017-08-11 04:16:13">, #<Area id: 9, name: "九州", created_at: "2017-08-11 04:16:13", updated_at: "2017-08-11 04:16:13">]>

まあなんかいけてるっぽいw

db/seeds.rb に以下のように書くとCSVからデータを投入することもできる

require "csv"

CSV.foreach('db/company.csv') do |row|
  Company.create({area_id: row[0]}, {name: row[1]})
end

めっちゃ便利!

leaflet.jsの地図に駅をプロットする

leaflet.jsの地図に駅のレイヤーをオーバーレイする

前回作ったtopojsonをleaflet.jsを使って描画した日本地図に載せていきます。
方法としてはD3.jsでjsonを読み込んでレイヤーを作り、日本地図にオーバーレイする。
では始めます。 (鉄道データ引用元

複数のjsonファイルを読み込む

今回駅情報を記述したstation.jsonと線路情報を記述したrail.jsonを同時に読み込む必要があったため以下のように記述した。

d3.json("rail_road.json", function(railjson){
    d3.json("Station.json", function(stationjson) {
        railDraw(railjson, stationjson);
    });
});

この方法だとjsonファイルが何個もあるときどんどん深くなっていくのでおすすめできないかも… shpファイルをQGISなどを使って一つにまとめる方法や前のブログでやった変換時に複数のgeojsonファイルを指定して、一つのtopojsonに吐く方法などあるらしいのでそちらを使った方が良いかもしれない

leafletの地図にオーバーレイ

leaflet.jsで生成した地図にオーバーレイでレイヤー(svg)を追加する方法

var svg = d3.select(map.getPanes().overlayPane)
    .append("svg")
    .attr('class', 'leaflet-zoom-hide')
    .attr({width:960,height:500});

線路情報の描画

線路情報はjsonファイルの中を覗くとわかるが、緯度経度のリストがいくつも並んでいる形式になっている。以下例

"features": [
{ "type": "Feature", "properties": { "N02_001": "23", "N02_002": "5", "N02_003": "沖縄都市モノレール線", "N02_004": "沖縄都市モノレール" }, "geometry": { "type": "LineString", "coordinates": [ [ 127.67948, 26.21454 ], [ 127.6797, 26.21474 ], [ 127.67975, 26.2148 ], [ 127.68217, 26.21728 ], [ 127.68357, 26.21862 ], [ 127.68394, 26.21891 ], [ 127.68419, 26.21905 ] ] } },
{ "type": "Feature", "properties": { "N02_001": "12", "N02_002": "5", "N02_003": "いわて銀河鉄道線", "N02_004": "アイジーアールいわて銀河鉄道" }, "geometry": { "type": "LineString", "coordinates": [ [ 141.29139, 40.3374 ], [ 141.29176, 40.33723 ], [ 141.29243, 40.33692 ], [ 141.29323, 40.33654 ], [ 141.29379, 40.33624 ], [ 141.29411, 40.33608 ], [ 141.2949, 40.33563 ], [ 141.29624, 40.33477 ], [ 141.29813, 40.33354 ], [ 141.29862, 40.33317 ] ] } },
{ "type": "Feature", "properties": { "N02_001": "12", "N02_002": "5", "N02_003": "いわて銀河鉄道線", "N02_004": "アイジーアールいわて銀河鉄道" }, "geometry": { "type": "LineString", "coordinates": [ [ 141.27554, 40.23936 ], [ 141.27567, 40.23884 ], [ 141.27587, 40.23827 ], [ 141.27622, 40.23756 ], [ 141.27656, 40.23694 ], [ 141.27722, 40.23579 ], [ 141.27789, 40.23462 ], [ 141.27856, 40.23344 ], [ 141.27922, 40.23236 ], [ 141.27983, 40.23153 ], [ 141.28006, 40.23123 ], [ 141.28011, 40.23118 ], [ 141.28034, 40.23094 ], [ 141.28092, 40.23042 ], [ 141.28207, 40.22963 ], [ 141.28328, 40.22883 ], [ 141.28345, 40.22874 ] ] } },
{ "type": "Feature", "properties": { "N02_001": "12", "N02_002": "5", "N02_003": "いわて銀河鉄道線", "N02_004": "アイジーアールいわて銀河鉄道" }, "geometry": { "type": "LineString", "coordinates": [ [ 141.28659, 40.26092 ], [ 141.28538, 40.25874 ] ] } },
...

このgeometryの部分がそうである。これをsvgのpath要素で結んでいくイメージ

以下にD3でsvg要素にpathを追加していく例を記述します。

var g1 = svg.append("g");
    
//緯度経度->パスジェネレーター関数作成
var transform = d3.geo.transform({point: projectPoint});
var path = d3.geo.path().projection(transform);

 featureElement = g1.selectAll("path")
    .data(geo_rail_json.features)
    .enter()
    .append("path")
    .attr({
        "stroke": "red",
        "fill": "green",
        "fill-opacity": 0.4,
    });

function projectPoint(x, y) {
    var point = map.latLngToLayerPoint(new L.LatLng(y, x));
    this.stream.point(point.x, point.y);
}

上記のソースはネットでググったら同じようなものが色々出てきてそれを引用しましたが、いまいち何やっているかわからぬ。。 勉強しなければいけませんね

駅情報のプロット

駅情報を記述したjsonファイルであるstation.jsonはこんな感じ

"features": [
{ "type": "Feature", "properties": { "N02_001": "11", "N02_002": "2", "N02_003": "指宿枕崎線", "N02_004": "九州旅客鉄道", "N02_005": "二月田" }, "geometry": { "type": "LineString", "coordinates": [ [ 130.63035, 31.25405 ], [ 130.62985, 31.25459 ] ] } },
{ "type": "Feature", "properties": { "N02_001": "23", "N02_002": "5", "N02_003": "沖縄都市モノレール線", "N02_004": "沖縄都市モノレール", "N02_005": "古島" }, "geometry": { "type": "LineString", "coordinates": [ [ 127.70279, 26.23035 ], [ 127.70309, 26.23093 ] ] } },
{ "type": "Feature", "properties": { "N02_001": "24", "N02_002": "5", "N02_003": "東京臨海新交通臨海線", "N02_004": "ゆりかもめ", "N02_005": "お台場海浜公園" }, "geometry": { "type": "LineString", "coordinates": [ [ 139.77818, 35.62961 ], [ 139.77888, 35.63 ] ] } },
...

なぜか緯度経度の組みが2つずつあるような形式。おそらく南東と北西の点を取ってきているイメージ。四角形にすると右斜め下と左斜め上の点の緯度経度(わかりにくい)

これらを考慮しプロットするソースが以下の通りである

var g2 = svg.append("g");
 

geo_station_json.features.forEach(function(d){
    d.LatLng = new L.LatLng(d.geometry.coordinates[0][1], d.geometry.coordinates[0][0]);
});
 

stationElement = g2.selectAll("image")
    .data(geo_station_json.features)
    .enter()
    .append("image")
    .attr({"xlink:href":"./station.svg",
        "width":15,
        "height":10,
       });

今回はプロットする点を画像にしました。いつも通りだとsvgにcircle要素を追加していくのですが、image要素を追加していくことで画像がプロットされます。  

完成図

こちらが今回作った駅と線路をleaflet.jsに追加した地図です。 f:id:h-piiice16:20170611103900p:plain

気持ち悪い…拡大すると f:id:h-piiice16:20170611104103p:plain

なんかずれてる 直すのはまた今度にします。

今回作ったもの GitHub - hiracky16/RailRoad

topojsonへの変換と鉄道データの描画

geojson から topojson への変換

geojsonからtopojsonへの変換はググったらたくさん出てきてどれもnodejsのパッケージのtopojsonを使ったものが多かった。今回もそれを使います。

$ npm i -g topojson

$ geo2topo -q 1e6 railroad=N02-15_RailroadSection.json > N02-15_RailroadSection.topojson

-q 1e6というオプションが変換時に大切なものらしく ほぼ常時つけた方がよいらしい

railroadとはtopojsonでのオブジェクト名になるらしい。

D3.jsによるtopojsonの描画

変換したtopojsonファイルを描画していきます。
使うライブラリはみんな大好きD3です。

<!DOCTYPE html>
<html>
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
  <meta charset="utf-8" />
  <title></title>
  <style>
  </style>
</head>
<body>
  <h1>Rail Road Test</h1>
  <script src="//d3js.org/d3.v3.min.js" charset="utf-8"></script>
  <script src="//d3js.org/topojson.v0.min.js" charset="utf-8"></script>
  <script src="test.js"></script>
</body>

次にtest.jsについて

var width = 800;
var height = 800;

var svg = d3.select("body")
  .append("svg")
  .attr("width", width)
  .attr("height", height);

d3.json("rail_road.topojson", function(error, topo){

  var data = topojson.object(topo, topo.objects.railroad)
  var projection = d3.geo.mercator()
    .center(d3.geo.centroid(data))
    .scale(1200)
    .translate([width/2, height/2])

  var path = d3.geo.path().projection(projection);

  svg.selectAll("path")
    .data(data.geometries)
    .enter()
    .append("path")
    .attr("d", path);
});

dataの受け渡しの際にさっき設定したオブジェクト名"railroad"を指定して行っています。

上記のhtmlをブラウザで表示すると

f:id:h-piiice16:20170527225844p:plain

とまあわかりにくい

しかし描画できてるっぽいので今日はここまで!

次はこのsvg要素をオーバーレイとして、leaflet.jsなどを使い地図上に鉄道データをのせていきたいとおもいます。

国土数値情報の鉄道データ(shp)をgeojsonへ変換

手順

  1. データ のダウンロード
  2. QGISのインストー
  3. .shp -> .json の変換

それぞれ説明します。

データのダウンロード

データはこちらです。
このページの下の方からダウンロードしてきます。
データが年度で分かれているのですが、27年をダウンロードしました。

QGISのインストー

Macの場合、ここから
今回はおそらく最新のQGISのバージョン2.18.7-1 をダウンロードしました。
DLが済むとzipファイルを展開して、GDALとmatplotlib、QGISインストーラを起動します。

開発元が未確認のため開けません についての解決法

単にクリックしてインストールしようとしたところ「開発元が未確認のため開けません」というメッセージが出てインストールできませんでした。
解決策として、右クリックで「このアプリケーションで開く」を押すと開くの選択肢ができるので、このやり方でインストールを行いました。

インストールが終わると以下の1行をPATHに追加します。

/Library/Frameworks/GDAL.framework/Programs

.shp -> .json の変換

この変換は以下のコマンドで行えます。

ogr2ogr -f GeoJSON N02-15_RailroadSection.json N02-15_RailroadSection.shp

React-Dropzoneが便利

ドラックアンドドロップでファイルをアップロードしたい

最近?のWEBサービスではよくファイルをアップロードする際ドラックアンドドロップで行う場合が多い。
今回はそれを自分のサービスの中にも使いたいと思い実装してみました。

Reactにはそんな願いを叶える便利なコンポーネントとしてreact-dropzoneというものがある。
今回はこれを使って手っ取り早く実装することにしました。

React-Dropzone

実装したサービスの画面はこれです。 f:id:h-piiice16:20170423212401p:plain

ここにファイルをドラックアンドドロップで持っていくと、デベロッパーツールのコンソールにファイル名が表示されるサンプルを作りました。

実装のためにまずはreact-dropzoneをnpm経由でインストールします

npm install --save-dev react-dropzone

そして以下のようにコンポーネントを作成します。

import Dropzone from 'react-dropzone';
import React, {Component} from 'react';
import {render} from 'react-dom';

export default class UploadFile extends Component {

  handleOnDrop(files) {
    files.map(file => console.log(file.name))
  }

  render() {
    return( 
      <Dropzone
        onDrop={this.handleOnDrop}
        accept="image/gif,image/jpeg,image/png,image/jpg" >
          <div>
            Drag and Drop files Here!
            <p>format: gif/png/jpeg/jpg</p>
          </div>
      </Dropzone>
    );
  }
}

にはいろいろなアトリビュートが設定できるらしく、デフォで必要そうなonDrop(アップロード時の振る舞い)にコールバック関数としてhandleDrop()を、accept(受け入れるファイルの形式)に画像ファイルの形式を記述しました。

次回はアップロードされたファイルをサーバサイドにpostして保存するところまで行きたいと思います。