2017 年度 OSS リテラシー 3 : 第 11 回 データ解析と可視化

準備: データの確認

まずは, 自分のラズパイが設置された場所と ID を確認する. データにアクセスする場合は <URL:http://sky.epi.it.matsue-ct.jp/rooms.txt> に書かれた ID (IoT-XX (XX は番号)) を利用することになる.

次に, 10 分平均したデータが CSV ファイルとしてサーバ sky.epi.it.matsue-ct.jp の /iotex/iotex_csv_10min/ 以下に置かれていることを確認する. 演習室の PC から sky.epi.it.matsue-ct.jp に ssh し, PAGER (less や lv) でファイルを開くと, カンマ区切りで 10 分おきにデータが並んでいることがわかる. このファイルにおいて, 999.9 という数字は欠損値を意味する.

$ ls /iotex/data_csv_10min/iot-XX/  (XX は自分の ID)

  2017-03.csv  2017-06.csv  2017-09.csv  2017-12.csv
  2017-04.csv  2017-07.csv  2017-10.csv  2018-01.csv
  2017-05.csv  2017-08.csv  2017-11.csv

$ lv /iotex/data_csv_10min/iot-XX/2018-01.csv  (XX は自分の ID)

  2018/01/01 00:00:00,10.484000000000002,0.0,0.0,53.386923731066666, .... 
  2018/01/01 00:10:00,10.479499999999998,0.0,0.0,53.33955157314166, ....
  2018/01/01 00:20:00,10.470166666666668,0.0,0.0,53.31679504014668, ....
  2018/01/01 00:30:00,10.469166666666666,0.0,0.0,53.27625357968334, ....
  2018/01/01 00:40:00,10.463666666666668,0.0,0.0,53.20861976229166, ....
  2018/01/01 00:50:00,10.466000000000001,0.0,0.0,53.158695008025006, ....
  ...(以下,  略)...

これらの CSV ファイルには以下の 18 個のデータがカンマ区切りで並んでいる.

  • time (時刻)
  • temp (温度, SENSIRION SHT75 (1 台目))
  • temp2 (温度, SENSIRION SHT75 (2 台目))
  • temp3 (温度, SENSIRION SHT75 (3 台目))
  • humi (湿度, SENSIRION SHT75 (1 台目))
  • humi2 (湿度, SENSIRION SHT75 (2 台目))
  • humi3 (湿度, SENSIRION SHT75 (3 台目))
  • dp (露点温度, SENSIRION SHT75 (1 台目))
  • dp2 (露点温度, SENSIRION SHT75 (2 台目))
  • dp3 (露点温度, SENSIRION SHT75 (3 台目))
  • pres (圧力, BMP180)
  • bmptemp (温度, BMP180)
  • dietemp (基板の温度, TMP007)
  • aobjtemp (放射温度 (壁の温度), TMP007)
  • lux (照度, TSL2561)
  • didx1 (不快指数, temp と humi より計算)
  • didx2 (不快指数, temp2 と humi2 より計算)
  • didx3 (不快指数, temp3 と humi3 より計算)

なお, これらの CSV ファイルは学内からは以下の URL でアクセス可能である. <URL:http://sky.epi.it.matsue-ct.jp/iotex_csv_10min/>

Ruby スクリプトのサンプル取得

実習用サンプルスクリプトを GitHub から入手する.

$ cd ~/   (ホームディレクトリに移動)

$ git clone https://github.com/sugiymki/iotex-sensor.git

clone した iotex-sensor の sample 以下にスクリプトが存在する.

$ ls iotex-sensor/sample/

  mkfile_csv-10min_csv-1day.rb   mkgraph-csv_with-1day.rb    mktable-csv_with-1day.rb
  mkgraph-csv_with-10min.rb      mkhist-csv_with-1day.rb

データの解析 & 可視化

10 分おきのデータからグラフを作る.

サンプルスクリプト iotex-sensor/sample/mkgraph-csv_with-10min.rb を利用する. ファイルを見ると, CSV ファイルから読み込んだデータを time_list (時刻), temp_list (気温), humi_list (湿度), didx_list (不快係数) に保管し, それを gnuplot で描画していることがわかる. gnuplot では png にグラフを出力している.

$ cd ~/iotex-sensor/sample

$ lv mkgraph-csv_with-10min.rb 

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

  ###
  ### デバイス毎の設定
  ###

  # デバイス名
  myid = "iot-50"

  # 公開ディレクトリ
  pubdir = "/home/sugiyama/public_html/graph-csv_with-10min" 

  ###
  ### 初期化
  ###

  # データ置き場
  srcdir = "/iotex/data_csv_10min/#{myid}/"

  # 公開ディレクトリの作成
  FileUtils.rm_rf(   pubdir ) if    FileTest.exists?( pubdir )
  FileUtils.mkdir_p( pubdir ) until FileTest.exists?( pubdir )

  # 欠損値
  miss = 999.9

  ###
  ### データの取得とグラフの作成
  ### 

  # 7, 30, 90, 120, 360 日の幅で描画
  [7,30,90,120,240,360].each do |range|
     p "#{range} days"

    # 描画範囲
    time_from = DateTime.now - range

    # ハッシュと配列の初期化
    time_list = Array.new #時刻
    temp_list = Array.new #温度
    humi_list = Array.new #湿度
    didx_list = Array.new #不快係数

    # csv ファイルから指定された時刻を読み込み. 配列化
    Dir.glob("#{srcdir}/*csv").sort.each do |csvfile|
      CSV.foreach( csvfile ) do |item|

        # 時刻. DateTime オブジェクト化.
        time = DateTime.parse( "#{item[0]} JST" )

        # 指定された時刻より後のデータを取得.
        if time >= time_from
          time_list.push( time )          # 時刻        
          temp_list.push( item[1].to_f )  # 温度
          humi_list.push( item[4].to_f )  # 湿度
          didx_list.push( item[15].to_f ) # 不快係数
        end
      end
    end
    p "plot from #{time_list[0]} to #{time_list[-1]}"

    # 温度グラフ作成.
    Numo.gnuplot do
      set ylabel:   "temperature (C)"
      set xlabel:   "time"
      set xdata:    "time"
      set timefmt_x:"%Y-%m-%dT%H:%M:%S+00:00"
      set format_x: "%m/%d %H:%M" 
      set xtics:    "rotate by -60"
      set terminal: "png"
      set output:   "#{pubdir}/#{myid}_temp_#{range}days.png"
      set :datafile, :missing, "#{miss}" # 欠損値
      set :nokey # 凡例なし
      plot time_list, temp_list, using:'1:($2)', with:"linespoints", lc_rgb:"green", lw:3
    end

    # 湿度グラフ作成 (各自で書くこと).

    # 不快指数グラフ作成 (各自で書くこと).
  end

Ruby の書き方を忘れた場合は以下を参考にせよ.

vi でサンプルスクリプト内の myid と pubdir を自分のものに変更する.

$ vi mkgraph-csv_with-10min.rb 

  # デバイス名
  myid = "iot-01"    <= 自分の ID に変更. 

  # 公開ディレクトリ
  pubdir = "/home/sugiyama/public_html/graph-csv_with-10min"   <= sugiyama を学生番号に変更

デバイス名と公開ディレクトリを変更したら, 以下のようにスクリプトを実行する.

$ ruby mkgraph-csv_with-10min.rb 

実行後にブラウザで <URL:http://sky.epi.it.matsue-ct.jp/~jxxxx> (jxxxx は自分の学生番号に変更) にアクセスすると, graph-csv_with-10min 以下に作成したグラフが存在することがわかる.

上記を終えたら mkgraph-csv_with-10min.rb を編集して湿度と不快係数のグラフも作成できるようにせよ. 湿度のグラフについては, 以下のように temp_list の代わりに humi_list を用いれば良い. 同様に, temp_list の代わりに didx_list を使えば不快指数のグラフも書けるはずである. ylabel や output も変更する必要があることを忘れないように.

Numo.gnuplot do
  set ylabel:   "humidity (%)"
  set xlabel:   "time"
  set xdata:    "time"
  set timefmt_x:"%Y-%m-%dT%H:%M:%S+00:00"
  set format_x: "%m/%d %H:%M"
  set xtics:    "rotate by -60"
  set terminal: "png"
  set output:   "#{pubdir}/#{myid}_humi_#{range}days.png"
  set :datafile, :missing, "#{miss}" # 欠損値
  set :nokey # 凡例なし
  plot time_list, humi_list, using:'1:($2)', with:"linespoints", lc_rgb:"blue", lw:3
end

修正後に再度スクリプトを実行する.

$ ruby mkgraph-csv_with-10min.rb 

実行後にブラウザで <URL:http://sky.epi.it.matsue-ct.jp/~jxxxx> (jxxxx は自分の学生番号に変更) にアクセスしてグラフが存在することを確かめる.

1 日単位での統計量の計算

1 日ごとに統計量を計算するためのスクリプトを作成する. サンプル内の mkfile_csv-10min_csv-1day.rb を修正するとよい.

vi でサンプルスクリプト内の myid と pubdir を自分のものに変更する.

$ vi mkfile_csv-10min_csv-1day.rb

  # デバイス名
  myid = "iot-01"    <= 自分の ID に変更. 

  # 公開ディレクトリ
  pubdir = "/home/sugiyama/public_html/data_csv_1day"   <= sugiyama を学生番号に変更

次に, ファイルの穴埋め部分をプログラミングする. 例えば Array オブジェクトから NArray オブジェクトへの変換部分は以下のように書けばよい.

# NArray オブジェクトへ変換. 解析が容易になる. 
vars_list_narray = Array.new
num.times do |i|      
  vars_list_narray[i] = NArray.to_na( vars_list[i] )
end

時間平均の部分では, 初期値として欠損値を与えた配列を用意している. 欠損値が含まれていない場合には, NArray のメソッド を使って統計量を得る. 平均値については既にプログラミングされているので, それを参考にして最小・最大・標準偏差・中央値についてプログラミングせよ.

# 時刻をずらしながら 1 日の統計量を作成する. 
while (time_list[idx0] + 1 < time_list[-1]) do 

  # 配列初期化
  time0  = time_list[idx0]
  mean   = Array.new( num, miss )  # 欠損値
  min    = Array.new( num, miss )  # 欠損値
  max    = Array.new( num, miss )  # 欠損値
  stddev = Array.new( num, miss )  # 欠損値
  median = Array.new( num, miss )  # 欠損値

  puts "#{time0} : #{time_list[idx0+1]}..#{time_list[idx1]}"

  # 1 つでも欠損値が含まれていたら日平均は欠損値扱いに.
  # 欠損値が含まれていない場合は idx2 は nil になる. 
  idx2 = ( vars_list_narray[0][idx0+1..idx1] ).to_a.index( miss )    
  unless ( idx2 )
    num.times do |i|
      mean[i]  = vars_list_narray[i][idx0+1..idx1].mean(0)
      min[i]   = # ... 自分で書く ...  
      max[i]   = # ... 自分で書く ...  
     stddev[i] = # ... 自分で書く ...  
     median[i] = # ... 自分で書く ...  
   end
 end      

  # ファイルの書き出し (平均値)
  csv = open("#{pubdir}/#{myid}_mean.csv", "a")
  csv.puts "#{time0.strftime("%Y/%m/%d")},#{mean.join(',')},\n"
  csv.close
  # 最小・最大・標準偏差・中央値のファイル出力
  # ... 自分で書く ...

  # 添字の更新
  idx0 = idx1 
  idx1 = idx0 + count  # 24時間分進める
end

上記を終えた後, 以下のようにスクリプトを実行する.

$ ruby mkfile_csv-10min_csv-1day.rb 

実行後にブラウザで <URL:http://sky.epi.it.matsue-ct.jp/~jxxxx> (jxxxx は自分の学生番号に変更) にアクセスすると, data_csv_1day 以下に作成した CSV ファイルが存在することがわかる.

ファイルを less や lv で開き, データが含まれていることを確認せよ.

1 日おきのデータからグラフを作る.

サンプルスクリプト iotex-sensor/sample/mkgraph-csv_with-1day.rb を利用する. CSV ファイルから温度に対する平均値, 最小値, 最大値, 中央値, 標準偏差を読み込み, グラフに表示している.

vi でサンプルスクリプト内の myid, srcdir, pubdir を自分のものに変更する.

$ vi mkgraph-csv_with-1day.rb 

  # デバイス名
  myid = "iot-01"    <= 自分の ID に変更. 

  # 公開ディレクトリ
  pubdir = "/home/sugiyama/public_html/graph-csv_with-1day" <= sugiyama を学生番号に変更
  # データ置き場
  srcdir = "/home/sugiyama/public_html/data_csv_1day" <= sugiyama を学生番号に変更

スクリプト内の「平均値・最小値・最大値の比較のグラフ」の部分の プログラムを参考に, 「平均値と中央値の比較のグラフ」をプログラミングせよ. output でファイル名を変えること, 各折れ線の凡例 (title) を適切にすることを忘れずに.

# 平均値, 最小値, 最大値の比較のグラフ       
Numo.gnuplot do
  set ylabel:   "temperature (C)"
  set xlabel:   "time"
  set xdata:    "time"
  set timefmt_x:"%Y-%m-%dT%H:%M:%S+09:00"
  set format_x: "%Y/%m/%d"
  set xtics:    "rotate by -60"
  set terminal: "png"
  set output:   "#{pubdir}/#{myid}_temp1_#{range}day.png"
  set key: "box"
  set :datafile, :missing, "999.9"

  plot [time_list, temp_list["mean"], using:'1:($2)', with:"linespoints", lc_rgb:"green", lw:3, title:"mean"],
       [time_list, temp_list["min"],  using:'1:($2)', with:"linespoints", lc_rgb:"blue",  lw:3, title:"min "],
       [time_list, temp_list["max"],  using:'1:($2)', with:"linespoints", lc_rgb:"red",   lw:3, title:"max "]
end   

# 平均値と中央値の比較のグラフ  
# ... 自分で書く ...   

上記を終えた後, 以下のようにスクリプトを実行する.

$ ruby mkgraph-csv_with-1day.rb 

実行後にブラウザで <URL:http://sky.epi.it.matsue-ct.jp/~jxxxx> (jxxxx は自分の学生番号に変更) にアクセスすると, graph-csv_with-1day 以下に作成した CSV ファイルが存在することがわかる.

ヒストグラム

サンプルスクリプト iotex-sensor/sample/mkhist-csv_with-1day.rb を利用する. ファイルを見ると, CSV ファイルから 1 日分の温度データ (1 時間ごと) を読み込み, それをヒストグラムにしている.

vi でサンプルスクリプト内の date, myid, pubdir を自分のものに変更する.

$ vi mkhist-csv_with-1day.rb 

  # 描画対象の日時. 区切りは "-" にしておくこと. 
  date = "2017-12-21"     (適当に与える)

  # デバイス名
  myid = "iot-01"    <= 自分の ID に変更. 

  # 公開ディレクトリ
  pubdir = "/home/sugiyama/public_html/histgram-csv_with-1day"    <= sugiyama を学生番号に変更

次に, グラフ作成部分をプログラミングする. 今までの numo/gnuplot の書き方を 参考にして欲しい.

###
### 1 日の頻度分布. グラフ化. 
###

bins = [0,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,40]
hist = Array.new( bins.size, 0 )

temp_list.size.times do |i|
  (bins.size-1).times do |j|
    if bins[j] <= temp_list[i] && bins[j+1] > temp_list[i]
      hist[j] += 1
      break
    end
  end
end

# ヒストグラム. 自分で書く. 
# ヒストグラムの場合は plot ..., with:"boxes", ....  というように,    
# with:"boxes" をオプションで与える.

修正を終えたら以下のようにスクリプトを実行する.

$ ruby mkhist-csv_with-1day.rb 

実行後にブラウザで <URL:http://sky.epi.it.matsue-ct.jp/~jxxxx> (jxxxx は自分の学生番号に変更) にアクセスすると, histgram-csv_with-1day 以下に作成したグラフが存在することがわかる.

表 (HTML) の作成

データを HTML の table 形式で表示するには, iotex-sensor/sample/mktable-csv_with-1day.rb を利用する. まずは myid, pubdir, srcdir を設定する.

$ vi mktable-csv_with-1day.rb 

  # デバイス名
  myid = "iot-01"  <= 自分の ID に変更.

  # 公開ディレクトリ
  pubdir = "/home/sugiyama/public_html/table-csv_with-1day/" <= sugiyama を学生番号に変更

  # データ置き場
  srcdir = "/home/sugiyama/public_html/data_csv_1day" <= sugiyama を学生番号に変更

例えば以下のように実行する.

$ ruby mktable-csv_with-1day.rb 

実行後にブラウザで <URL:http://sky.epi.it.matsue-ct.jp/~jxxxx> (jxxxx は自分の学生番号に変更) にアクセスすると, table-csv_with-1day 以下に作成したグラフが存在することがわかる.

課題

作成した図の一部を提出せよ.

  • 10 分おきのデータから作成したグラフ
    • 温度, 湿度, 不快指数の 3 種類のグラフをそれぞれ 1 枚づつ.
  • 1 日おきのデータから作成したグラフ
    • 平均値・最小値・最大値の比較, 平均値・中央値の比較, 平均値 + 標準偏差, の 3 種類のグラフをそれぞれ 1 枚づつ.
  • ヒストグラム 1 枚

時間が余れば..., 以下の発展課題をいくつか行うこと. 成果物を適宜提出すること.

  • 図をもとに, 教室の温度の特徴を議論する.
  • 温度以外の変数についてもグラフを作成する.
  • mktable-csv_with-1day.rb が出力する HTML のヘッダを正しく記述する. CSS を用いる.
    • タイトル,キーワード,HTMLバージョンなど,J3実験(WWW入門)で学習したとおりにする。なお,HTMLバージョン:4.01,文字コード:UTF-8とする
    • CSSは外部ファイルに記述し読み込む

参考

松江高専 環境モニタリングシステム

気象観測統計

Ruby

NArray

gnuplot

補遺:SD カードへの書き込みの抑制

注) この節は教員の方で実施済み. 参考までに.

ラズパイの SD カードに常時頻繁に書き込みを行うと, SD カードが壊れることがある. 2016 年度の IoT 演習では, 10 秒おきにデータをファイル出力したところ, 稼働開始から数ヶ月で SD カードが壊れる事態が頻発した. また, ラズパイをセンサーとして運用していると電源ケーブルを抜き差しして ラズパイを再起動したくなることがあるが, SD カードの読み書き中に電源を切ると SD カードが壊れることがある.

そこで SD カードへの書き込みしないようにする. overlayfs を用いることで, Readonly な下の層 (この場合は SD カード) と, Writable な上の層 (この場合はメモリー) を重ね合わせ 1 つのファイルシステムに見せることができる. データの書き出しは全てメモリ上に行われるので, 例え電源が抜き差ししても SD カードが壊れることはない.

実装の仕方は色々あるが, 今回は導入やメンテナンスが簡単な root-ro (Read-only Root-FS with overlayfs for Raspian) を用いる. 以下の作業は root-ro の README に基づいている.

パッケージのインストール, root-ro の取得

root-ro は GitHub から入手する. それ以外は raspbian のパッケージを用いる.

$ sudo -s 

# apt-get update

  ...(略)...

# apt-get install rsync git gawk busybox bindfs

  ...(略)...

# git clone https://github.com/josepsanzcamp/root-ro.git

Swap 領域の無効化

まずは Swap 領域を無効にして SD カードへの退避書き込みを無くす. Swap はメモリ不足の時にディスクをメモリ代わりに使うものである. 今回はデータを全てメモリ上に置くので, Swap を使う意味がない.

まずはメモリと Swap の状態を確認する. Swap が 100 MB あることがわかる.

# free
               total       used       free     shared    buffers     cached
  Mem:        947732     745712     202020      49428     167380     332228
  -/+ buffers/cache:     246104     701628
  Swap:       102396          0     102396

Swap 領域の無効化を行う.

# dphys-swapfile swapoff

# dphys-swapfile uninstall

# update-rc.d dphys-swapfile disable

# systemctl disable dphys-swapfile

  dphys-swapfile.service is not a native service, redirecting to systemd-sysv-install.
  Executing: /lib/systemd/systemd-sysv-install disable dphys-swapfile

Swap 領域が無効化されたことを確認する. 以下のように Swap の total が 0 になっていれば良い.

# free
                total        used        free      shared  buff/cache   available
  Mem:         949580      113572      499352       16012      336656      765948
  Swap:             0           0           0

root-ro の設定

GitHub から入手した root-ro のファイル群をシステム領域にコピーし, initramfs を作成し, 設定ファイル (/boot/config.txt) に修正を加える.

# cd root-ro

# rsync -va root-ro/etc/initramfs-tools/* /etc/initramfs-tools/

  sending incremental file list
  modules
  hooks/
  hooks/root-ro
  scripts/
  scripts/init-bottom/
  scripts/init-bottom/root-ro

  sent 10,494 bytes  received 93 bytes  21,174.00 bytes/sec
  total size is 10,130  speedup is 0.96

# mkinitramfs -o /boot/initrd.gz
# echo initramfs initrd.gz >> /boot/config.txt

再起動

# reboot

再起動後に数分待てば自分のホームディレクトリ内に data_now, data_csv といったディレクトリが できるはずである. 存在することを確認したら, 再びラズパイを再起動せよ.

$ ls -l data_now

  drwxr-xr-x 4 sugiyama sugiyama 4096 11月 18 15:02 data_now

# reboot

再起動した直後には data_now, data_csv といったファイルが存在しないことを確認せよ. このように新たなデータは全てメモリ上に保管されるので, 再起動するとそれらのデータは 全て失われる.

$ ls -l data_now

  ls -l data_now
  ls: 'data_now' にアクセスできません: そのようなファイルやディレクトリはありません