2018 年度 OSS リテラシー 3 : 第 13 回 データ解析と可視化 (2)

補足 (1) : screen

効率的な作業のために screen を活用すると良い.

vm$ sudo apt-get update

vm$ sudo apt-get install screen

screen の使い方は授業中に実演する. Google で "Linux screen" で使い方が山と出てくる.

補足 (2) : /etc/hosts と zabbix の設定

zabbix でラズパイの情報がうまく表示されないという問い合わせがあった. 調べてみると, 仮想マシンの /etc/hosts の設定に問題があった.

/etc/hosts 書かれているかローカルホスト (仮想マシン) の名前とラズパイ のホスト名が同じになっていた. そのため, ラズパイにアクセスするつもりが 仮想マシンにアクセスしてしまうという状況が生じていた.

例えば, /etc/hosts が,

vm$ cat /etc/hosts

  127.0.0.1       localhost
  127.0.1.1       j1500.epi.it.matsue-ct.jp       j1500       <<< ココ

  # The following lines are desirable for IPv6 capable hosts
  ::1     localhost ip6-localhost ip6-loopback
  ff02::1 ip6-allnodes
  ff02::2 ip6-allrouters
  10.52.2.36    j1500.epi.it.matsue-ct.jp   j1500             <<< ココ

となっている場合, j1500 と仮想マシンとラズパイに同じ名前が割り当てられているため, j1500 に ping するとラズパイ (10.52.2.36) ではなく仮想マシン (127.0.1.1) が返事をする.

vm$ ping j1500

  PING j1500.epi.it.matsue-ct.jp (127.0.1.1) 56(84) bytes of data.
  64 bytes from j1500.epi.it.matsue-ct.jp (127.0.1.1): icmp_seq=1 ttl=64 time=0.040 ms
  64 bytes from j1500.epi.it.matsue-ct.jp (127.0.1.1): icmp_seq=2 ttl=64 time=0.038 ms

この状態を解消するためには, 仮想マシンのホスト名を変更するか, ラズパイに別名をつけるかである.

今までの設定で, apache のログが /var/log/apache2/access.log から /var/log/apache2/other_vhosts_access.log に変更されているケースが散見される. それを確認してみる.

vm$ grep monitoring /var/log/apache2/access.log 

を行って表示が出れば良いが, 出ない場合はログファイルの変更の可能性を考える. /var/log/apache2/other_vhosts_access.log に対して

vm$ grep monitoring /var/log/apache2/other_vhosts_access.log

を実行して直近のアクセスログが表示された場合は, /var/log/apache2/other_vhosts_access.log にログが置き換わったと判断する.

ラズパイに別名をつける場合は, /etc/hosts を編集しているスクリプト (root の crontab に設定したもの) を以下のように修正する (name の部分を適宜編集する. 学生番号は使わない). なお, sed の正規表現を若干変えているので注意 (other_vhosts_access.log の場合に, ログが "ホスト名:80 IPアドレス - - 接続先情報" となっていることがあるので).

[access.log の場合]

vm$ sudo vi /root/mkhosts.sh

  #!/bin/bash
  ip=`grep monitoring.php /var/log/apache2/access.log | tail -n 1 | sed -r 's/^.*\s*(10\.[0-9]+\.[0-9]+\.[0-9]+)\s-\s-.*$/\1/'`

  ...(中略)...

  # IP を追加
  echo "$ip    name.epi.it.matsue-ct.jp   name" >> $HOST 


[other_vhosts_access.log の場合]

vm$ sudo vi /root/mkhosts.sh

  #!/bin/bash
  ip=`grep monitoring.php /var/log/apache2/other_vhosts_access.log | tail -n 1 | sed -r 's/^.*\s*(10\.[0-9]+\.[0-9]+\.[0-9]+)\s-\s-.*$/\1/'`

  ...(中略)...

  # IP を追加
  echo "$ip    name.epi.it.matsue-ct.jp   name" >> $HOST 

/root/mkhosts.sh を実行すると /etc/hosts が以下のように変更されることがわかる.

vm$ sudo sh /root/mkhosts.sh 

vm$ cat /etc/hosts

  127.0.0.1       localhost
  127.0.1.1       j1500.epi.it.matsue-ct.jp       j1500     <<< ココ

  # The following lines are desirable for IPv6 capable hosts
  ::1     localhost ip6-localhost ip6-loopback
  ff02::1 ip6-allnodes
  ff02::2 ip6-allrouters
  10.52.2.36    name.epi.it.matsue-ct.jp   name             <<< ココ

新たにつけた別名 (ここでは name) に ping をうつと, ラズパイ (127.0/16 でない) から返事が来ることがわかる.

vm$ ping name

  PING name.epi.it.matsue-ct.jp (10.52.2.36) 56(84) bytes of data.
  64 bytes from name.epi.it.matsue-ct.jp (10.52.2.36): icmp_seq=1 ttl=62 time=0.552 ms
  64 bytes from name.epi.it.matsue-ct.jp (10.52.2.36): icmp_seq=2 ttl=62 time=0.660 ms

さらに zabbix でホストの設定を変更する. ホスト名の部分を新たに付けた別名に変える.

データ処理で注意すること : 欠損値

何らかの理由でセンサー (ラズパイ) がフリーズすると, その時間帯のデータは取得できない = データが欠損する, ということが生じる. grafana や gnuplot に限らないが, 欠損があることを教えてあげないと 描画ツールは以下のように欠損部分をつないだ不自然な線グラフを作ってしまう. 本来はデータが無いはずなのに変なデータが存在しているように見えてしまい, これはデータ処理を行う上で大変まずい.

データに欠損値がある場合には, そこに NULL や取り得ない数字 (999 など)を入れておく. そして, 描画ツールに欠損値はどのように表されているか教える必要がある.

前回の課題の様子を見ても, データ欠損があるにもかかわらず不自然につながった折れ線グラフが多く作られている.

データの作成

前回のスクリプトを修正する. 10 分平均の作成スクリプトにおいて, 10 分間のデータが 5 点以下の場合には欠損値を入れる処理を加える.

vm$ vi ~/bin/db_1min-10min.rb

  ...(略)...

  # SQL の出力を用いて不快指数を計算し, 新たなテーブルに入れる. 
  client.query(sql).each do |item|

    # 10 分のうち, 5 点以上のデータがあるなら平均操作を行う
    if item["count"] > 5      

      ...(略)...

    # 欠損値の場合. ホスト名 (hostname) と時刻 (time) 以外のカラムは自動的に NULL になる.
    else          
      sql = "INSERT INTO #{mytable_to} (hostname,time) 
             VALUES ('#{myhost}', '#{time1}' )"
      client.query(sql)  
    
    end
  end

また, 1 日平均のスクリプトにおいても同様の処理を加える. また前回のスクリプト例では平均値につける日付が 1 日ずれていたので, その部分も修正している.

vm$ vi ~/bin/db_1hour-1day.rb

   ...(略)...

   # テーブルに既に値が入っている場合は平均開始時刻 time_from の値を更新する. 
  sql = "SELECT time FROM #{mytable_to['min']} WHERE hostname LIKE '#{myhost}' 
         ORDER BY time DESC LIMIT 1"
  client.query(sql).each do |item|
    if item["time"].present?
      time_from = Time.parse( item["time"].to_s ) + 1.days   #<<< 1.day を加える. 
    end
  end

  ...(略)...

  ####
  #### 最小値
  ####

  ...(略)...

  # SQL の出力を用いて不快指数を計算し, 新たなテーブルに入れる. 
  client.query(sql).each do |item|

    p item["count"]
    # 24 時間分のデータが揃っているか確認
    if item["count"] == 24    

      ...(略)...

      # 新たなテーブルへ入力
      sql = "INSERT INTO #{mytable_to["min"]} (hostname,time,temp,humi,didx) 
          VALUES ('#{myhost}', '#{time0}', #{temp}, #{humi}, #{didx})"     #<<< time1 ではなく time0 に. 
      client.query(sql)

    # 欠損値の場合. ホスト名 (hostname) と時刻 (time) 以外のカラムは自動的に NULL になる.
    else
      sql = "INSERT INTO #{mytable_to["min"]} (hostname,time)
            VALUES ('#{myhost}', '#{time0}')"
      client.query(sql)

    end
  end

  ...(略)...

  #最大値・平均値・標準偏差についても同様に time1 => time0 と欠損値の扱いを追加する.

修正したスクリプトを実行する前にデータベースのテーブルを一度削除する.

vm$ mysql -u hogehoge -p

  Enter password:    (適宜入力)

  ...(略)...

  MariaDB [(none)]> use iotex

    Reading table information for completion of table and column names
    You can turn off this feature to get a quicker startup with -A

    Database changed

  MariaDB [iotex]> drop table iotex2018_1day_stddev;
  Query OK, 0 rows affected (0.02 sec)

  MariaDB [iotex]> drop table iotex2018_1day_min;
  Query OK, 0 rows affected (0.02 sec)

  MariaDB [iotex]> drop table iotex2018_1day_max;
  Query OK, 0 rows affected (0.02 sec)

  MariaDB [iotex]> drop table iotex2018_1day_avg;
  Query OK, 0 rows affected (0.02 sec)

  MariaDB [iotex]> drop table iotex2018_1hour;
  Query OK, 0 rows affected (0.02 sec)

  MariaDB [iotex]> drop table iotex2018_10min;
  Query OK, 0 rows affected (0.02 sec)

10 分平均値を作り直し, 確認する. データベースへのアクセス回数が増えたため, スクリプトの実行に時間がかかるようになる. データベースへのアクセスは基本遅いので, なるべく SQL を実行する回数を減らすのがデータベースを利用するスクリプトを高速化するコツである.

vm$ ruby bin/db_1min-10min.rb

  "2018-11-01T00:00:00+09:00 ... 2018-11-01T00:10:00+09:00"
  "2018-11-01T00:10:00+09:00 ... 2018-11-01T00:20:00+09:00"
  ...(以下略)...

データを確認し, 欠損値 (NULL) が入っていることを確認する. 以下では 2019-01-10 12:50:00 から 2019-01-10 14:20 のデータを確認しているが, その部分は自分のラズパイで欠損値になっていた時刻に変更すること.

vm$ mysql -u hogehoge -p

  Enter password:    (適宜入力)

  ...(略)...

  MariaDB [(none)]> use iotex

    Reading table information for completion of table and column names
    You can turn off this feature to get a quicker startup with -A

    Database changed

  MariaDB [iotex]> select time,temp from iotex2018_10min where time >= '2019-01-10 12:50:00' and time <= '2019-01-10 14:20 ';

     | 2019-01-10 12:50:00 |             18.246 |
     | 2019-01-10 13:00:00 | 17.666249999999998 |
     | 2019-01-10 13:10:00 |             17.861 |
     | 2019-01-10 13:20:00 | 18.189999999999998 |
     | 2019-01-10 13:30:00 | 18.398571428571426 |
     | 2019-01-10 13:40:00 |               NULL |
     | 2019-01-10 13:50:00 |               NULL |
     | 2019-01-10 14:00:00 |               NULL |
     | 2019-01-10 14:10:00 |               NULL |
     | 2019-01-10 14:20:00 |               NULL |

同様にして, 1 時間値と 1 日平均値を作る.

vm$ ruby ~/bin/db_10min-1hour.rb

vm$ ruby ~/bin/db_1hour-1day.rb

grafana

上記のようにデータベースを作り直すと, 以下の図のように欠損値の部分は折れ線でつながれなくなる.

これは, grafana はデフォルトで NULL は欠損値としているからである. グラフの編集 (edit) ページを開き, "Display" タグを開くと, NULL を欠損値として扱っていることがわかる. 試しに Null value を変えると図の見てくれが変化する.

gnuplot

前回と同様に grafana から欠損値のある時間帯を選んで csv を作成し, gnuplot で表示してみる.

前回のスクリプトを改良したものを以下に示すが, 例えば「変更点 (1)」に あるようにデータ読み込みの部分で "null" なら欠損値を表す数字 999 に置き換え, 「変更点 (2)」にあるようにプロット部分で 999 が欠損値だと指定すれば良い. なお, 作図部分に using:'1:($2)' と書いているが, このカッコとドルマーク $ で囲む書き方は欠損値処理をする場合の書き方である.

欠損値を直したのは 10 分平均値であるので, データの取得の部分で "data_list.push( row[2].to_f )" としている (配列添字が 2).

#!/usr/bin/env ruby
# coding: utf-8
#

require 'csv'
require 'date'
require 'fileutils'
require 'numo/gnuplot'

# 初期化
csvfile = "temp.csv"
output  = "/home/hogehoge/public_html/output.png"
time_list = []
data_list = []

# csv ファイルの読み込み
CSV.foreach( csvfile, {:encoding => "UTF-8", :col_sep => ";" }) do |row|
  next if row[0] == "Time"  # 1 行目無視
  p row
  time_list.push( row[0] )

  ### 変更点 (1)
  ### 
  unless row[1] == "null"
    data_list.push( row[2].to_f )  # 10 分平均値は 3 番目のカラム (添字番号 2) に入っている.
  else
    data_list.push( 999 )
  end
end

# 作図
Numo.gnuplot do
  debug_on
  set title:    "温度"
  set ylabel:   "temperature (C)"
  set xlabel:   "time"
  set xdata:    "time"
  set timefmt:  "%Y-%m-%dT%H:%M:%S+09:00"
  set format_x: "%m/%d %H:%M"
  set xtics:    "rotate by -60"
  set terminal: "png"
  set output:   "#{output}"

  ### 変更点 (2)
  ### 
  set :datafile, :missing, "999"     

  plot time_list, data_list, using:'1:($2)', with:"linespoints", lc_rgb:"red", lw:2, title:"ほげ"
end

上記のスクリプトを実行すると, 元々は,

だったものが, 以下のように欠損値処理された形で表示されるはずである.

注意事項

スクリプトを実行した時に以下のようなメッセージが表示された場合は, csv ファイルの改行コードを変更する必要がある. Windows と Unix では改行コードの表し方が違う.

vm$ ruby gnuplot.rb

  /usr/lib/ruby/2.3.0/csv.rb:1883:in `block (2 levels) in shift': Illegal quoting in line 1. (CSV::MalformedCSVError)

nkf コマンドを使うと簡単に変換することができる.

vm$ nkf -Lu grafana_data_export.csv > temp.csv

データ処理に関連して: HTML の作成とメールの送信

IoT と称されるシステムでは, データ処理の結果を元にアラートをメールで送ったり, Web にアナウンスを出すことがよく行われる. 実際にデータ処理を行う前に, ruby スクリプトからメールを送信したり, Web を作成する方法について簡単に紹介する.

HTML の作成

ここではヒアドキュメントを用いて簡単に HTML ファイルを作ることにする (他にも, 埋め込み Ruby など, いくらでも方法はあるが).

ヒアドキュメントでは << を使う. << の後ろにヒアドキュメントの始まりと終わりを示す文字列の識別子を書く. EOS(End Of String)とEOL(End Of Line)が使われている例が多いが, 統一されていれば何を使っても問題ない. また, ruby の命令を埋め込む場合は #{...} のように書けば良い (サンプルでは現在時刻を示す Time.now を使っている).

以下のようなサンプルを作成し実行してみよ. 但し, 出力ファイルのパスは適宜変更すること (注意: << と "EOS" の間には空白を入れないこと).

#!/usr/bin/env ruby
# coding: utf-8

html = "/home/hogehoge/public_html/sample.html"  # 出力ファイル名                     

# HTML データ   
body = <<"EOS"                                                                        
<html>                                                                                
  <head>                                                                              
   <meta charset="utf-8" />                                                           
  </head>                                                                             
  <body>                                                                              
    Hello World! <br>                                                                 
    ファイル作成時の時刻は #{Time.now} です.                                          
  </body>                                                                             
</html>                                                                               
EOS                                                                                   

# ファイルへの書き出し
output = File.open( html, "w" )
output.puts( body )
output.close

サンプルによって test.html というファイルが作成されるので, Web ブラウザで表示してみよ.

メールの送信

Mail ライブラリを利用する. 以下のサンプルのようなスクリプトを作成して送信せよ (送信元と送信先は正しく変更すること. 送信先は自分の携帯にしてみよ (学校の学生用メールアドレスには制限がかかっているので, うまく動かないことあり)). メール本文は前述のヒアドキュメントを使うと良い.

まずは Mail ライブラリをインストールする.

# gem install mail

次に以下のようなスクリプトを作成してテストする.

#!/usr/bin/env ruby
# coding: utf-8
require 'mail'

options = {
  :address  => "red.matsue-ct.jp",
  :port   => 25,
  :authentication => 'plain',
  :enable_starttls_auto => true  
}

mail = Mail.new do
  from     'hogehoge@matsue-ct.jp'    # 送信元 (@matsue-ct.jp にしておく)
  to       'herohero@matsue-ct.jp'    # 送信先
  subject  "mail test"
#  body      "this is test mail."
end

# 本文が長い場合は以下のようにヒアドキュメントを使う方が簡単. 
mail.body = <<"EOS"
このメールは、
#{Time.now} に送信されました。
EOS 

# 送信
mail.delivery_method :smtp, options
mail.deliver

最終課題:データ解析プログラムの作成

今回はラズパイから送られてきたデータを解析して, 何らかの動作を行うプログラムを作成する. データを眺めて, 「これが言えそうだ」というのを自分なりに考えてプログラムを作成してほしい. 特に思いつかない場合は, 以下の例を参考にしてプログラムを作成してほしい. 今までサンプルとして提示したプログラムを活用すれば作成できると思う.

冷暖房基準を満たしているかのチェック

文科省の基準で学校の教室の「望ましい温度」が定められている.

  • 17 <= 気温 <= 28

また, 松江高専の基準では, 夏季にクーラーを入れる条件は以下の 2 つの条件のうち 1 つが満たされた場合である.

  • 気温 > 28 度
  • 不快指数 > 78

そこで, DB から気温 (temp) と不快指数 (didx) を取り出し, 冷暖房をつける必要があるか否かを判断するプログラムをする. プログラムにおいては, 冷暖房を ON する必要のある温度・不快指数と判断された場合には, 以下の動作をするようにする.

  • 自分に警告メールを送る.
  • Web ページにアラートを出す.

動作テストするには, 自分のラズパイのセンサーにドライヤーの風を送ってみたり, 窓を開けて外気を取り込むと良いだろう.

教室を使っているかのチェック

得られた温度のデータを見てみると, 平日は以下の特徴がある.

  • 朝, 暖房が ON されたタイミングで気温が急激に高くなる.
  • 日中は暖房のために気温が高い.
  • 1 日の平均気温の標準偏差が大きい.

一方, 休日は以下の特徴がある.

  • 暖房が入らないので気温が低い.
  • 1 日の平均気温の標準偏差が小さい.

これらの特徴を使うと, 例えば以下がわかりそうである.

  • 標準偏差 < 閾値 (例えば 1) => 教室は使われなかった.
  • 温度の時間変化 (dT/dt) > 閾値 => 学生が教室に入って暖房を付けた時刻
  • 昼から夜にかけて温度変化が小さい => 暖房が点きっぱなし?

これらのうち 1 つを選択し, その判断をプログラム中で行い, 以下の該当項目を行うようにする.

  • 教室が使われたかどうかの結果を毎日メールで送信 & Web ページに表示.
  • 暖房がついたらその時刻をメールで送信 & Web ページに表示.
  • 暖房が点きっぱなしなら, 警告メールを自分に送る & Web ページにアラートを出す.

特に最後の例は検証は難しいので今回の課題としては適当でないかもしれないが, 興味を持った人は是非考えてみてほしい.

課題の提出

最終課題に関する簡単なレポートを作成し, 電子ファイルで提出せよ.

レポートには以下の内容を入れること.

  • はじめに: 作成したプログラムの目的
  • 手法: プログラム中でのデータ処理の概要.
    • 使ったデータの説明 (使ったテーブルやカラム, それを使った理由)
    • データを使って何を判断したか. 判断の基準.
  • 結果: 判断結果を使って行ったことを示すもの.
    • 送られたメールのコピー
    • Web ページのスナップショット
  • まとめ
    • 何を行って何が実現できたかのサマリ.
  • 作成したプログラムの添付