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

準備

データの確認

前回までの内容で, ラズパイからデータが送られてきたデータが grafana で可視化されているはずである. 各自の管理するサーバ上で動いている grafana において, 以下のような画面が表示されているか確認すること.

また, MySQL を直接確認すると, 1 分間隔でデータがテーブルに入力されていることがわかる.

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 order by time;

       ...(略)...

    | 2019-01-15 09:32:00 | 19.68 |
    | 2019-01-15 09:33:00 | 19.14 |
    | 2019-01-15 09:34:00 | 18.84 |
    | 2019-01-15 09:35:00 | 18.62 |
    | 2019-01-15 09:36:00 | 18.49 |
    | 2019-01-15 09:37:00 | 18.35 |
    | 2019-01-15 09:38:00 |  18.3 |
    | 2019-01-15 09:39:00 | 18.95 |
    | 2019-01-15 09:40:00 | 19.85 |
    | 2019-01-15 09:41:00 |  19.8 |
    | 2019-01-15 09:42:00 | 19.85 |

なお, MySQL のテーブルには以下のカラムが存在するが, 実習では温度湿度センサー SHT75 を 1 台ラズパイに接続しているだけなので, hostname, time, temp, humi, dp 以外は NULL になっている.

  • hostname (ホスト名)
  • 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)
  • objtemp (放射温度 (壁の温度), TMP007)
  • lux (照度, TSL2561)
  • winddir (風向)
  • windvel (風速)

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

実習用サンプルスクリプト (3 種類) とデータベース接続情報を書くファイルのサンプルを入手する.

vm$ mkdir ~/bin

vm$ cd ~/bin

vm$ wget http://www.gfd-dennou.org/arch/sugiyama/IoTeX_2018/sample/db_1min-10min.rb

vm$ wget http://www.gfd-dennou.org/arch/sugiyama/IoTeX_2018/sample/db_10min-1hour.rb

vm$ wget http://www.gfd-dennou.org/arch/sugiyama/IoTeX_2018/sample/db_1hour-1day.rb

vm$ wget http://www.gfd-dennou.org/arch/sugiyama/IoTeX_2018/sample/db_info.yml

$ ls ~/bin/

  db_10min-1hour.rb  db_1hour-1day.rb  db_1min-10min.rb  db_info.yml

必要なパッケージのインストール

vm# apt-get update; apt-get install ruby-mysql2

データの解析 & 可視化

本日の作業手順は「仮想サーバ」で行うべき作業である. ラズパイで行うことはない (きちんとデータが送られている限りは).

今回の実習では, ラズパイから送られてきた 1 分平均値から 10 分平均値, 1 時間毎の値の抽出, 1 日の最低・最大・平均値を作成し, それらのデータを grafana で表示できるようにする.

完成予想図は以下の図の通りである.

  • サーバ情報 (ホスト名, IP, SSID),
  • 気温 (1 分平均, 10 分平均, 1 時間おきの値)
  • 気温 (1 日平均の最小・最大・平均). 気象分野では 1 時間おきの値をつかってこれらを計算する.
  • 気温の標準偏差
  • 湿度 (1 分平均, 10 分平均, 1時間おきの値)
  • 露点温度 (1 分平均)
  • 不快指数 (10 分平均)

なお, まだラズパイからうまくデータが送られてきていない, もしくはデータが少なくて困っている場合は, サンプルとして配ったデータに含まれるホスト名が j1511 or j1526 のデータを使うと良い. 以下のように サンプルデータを可視化してみると, j1511 と j1526 が 12/6 ~ 12/11 の期間にデータが入っていることが わかる. grafana に以下のように SQL 文を入力してみよ.

SELECT
  UNIX_TIMESTAMP(time) AS "time",
  temp,
  hostname as metric
FROM iotex2018
WHERE
  $__timeFilter(time)
ORDER BY time

1 分平均値から 10 分平均値を作成する

以下のサンプルスクリプト (db_1min-10min.rb) において, ホスト名や MySQL サーバ接続情報を自分のものに変更せよ. データベース接続情報 (db_info.yml) は絶対に ~/public_html 以下には置かないこと.

このプログラム中においては SQL の AVG 関数で平均値を計算している.

#!/usr/bin/env ruby
# coding: utf-8
#
# 表題: DB から取り出した 1 分毎のデータから 10 分平均データを作る.
# 

require 'yaml'
require 'mysql2'
require 'fileutils'
require 'active_support/time'

###
### 変数宣言
###

# ホスト名
myhost = "j52"                   # <<< 変更

# データベースのテーブル名
mytable_from = "iotex2018"
mytable_to   = "iotex2018_10min"

# データベースへの接続情報の置き場.
# ~/public_html 以下には置かないこと.
conf = "/home/hogehoge/conf/db_info.yml"   # <<< 変更

# 設定ファイルの読み込み
mydb = YAML.load_file( conf )

# データベースへの接続
client = Mysql2::Client.new(
  :host     => "#{mydb["SERV"]}",
  :username => "#{mydb["USER"]}",
  :password => "#{mydb["PASS"]}",
  :database => "#{mydb["DBNM"]}"
)

###
### 新たなテーブルの作成
###

# テーブルの存在の確認
flag = false
sql  = "SHOW TABLES"
client.query(sql).each do |item|
  if item["Tables_in_#{mydb["DBNM"]}"] == mytable_to
     flag = true
  end
end
# テーブルが存在しない場合は新規作成. 不快指数 didx をカラムに追加. 
unless (flag)
  sql  = "CREATE TABLE #{mytable_to} like #{mytable_from}"
  client.query(sql)
  sql  = "ALTER  TABLE #{mytable_to} ADD didx double"
  client.query(sql)
end

###
### 時刻の設定
###

# 平均を開始する時間 (デフォルト値)
time_from = DateTime.new( 2018, 11, 1, 0, 0, 0, "JST")

# テーブルに既に値が入っている場合は平均開始時刻 time_from の値を更新する. 
sql = "SELECT time FROM #{mytable_to} 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 )
  end
end

# 平均を終了する時間
time_end = DateTime.now

###
### 前 10 分平均値の計算とテーブルへの代入
###

# 時刻の初期化
time0 = time_from
time1 = time_from + 10.minutes 

# ループを回しながら 10 分平均をとる. 
while ( time1 < time_end ) do
  p "#{time0} ... #{time1}"

  # 温度湿度の平均値と, 温度が 10 分間に何回記録されているか調べる. 
  sql = "SELECT count(temp) as count, AVG(temp) as temp, AVG(humi) as humi
         FROM #{mytable_from} 
         WHERE time > '#{time0}' AND time <= '#{time1}' 
         AND hostname LIKE '#{myhost}' ORDER BY time"

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

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

      # 平均値を変数に代入
      temp = item["temp"]
      humi = item["humi"]

      # 不快指数の計算
      didx = 0.81 * temp + 0.01 * humi * (0.99 * temp - 14.3) + 46.3

      # 平均値と不快指数を新たなテーブルへ入力
      sql = "INSERT INTO #{mytable_to} (hostname,time,temp,humi,didx) 
             VALUES ('#{myhost}', '#{time1}', #{temp}, #{humi}, #{didx})"
      client.query(sql)      
    end
  end

  time0 = time1
  time1 = time1 + 10.minutes
end

exit

データベースの接続情報が書かれたファイル (db_info.yml) は適切に修正する.

vm$ vi db_info.yml

実際に動かしてみる. 標準出力に平均をとるのに用いた時間帯が表示される.

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

  "2019-01-15 22:00:00 +0900 ... 2019-01-15 22:10:00 +0900"
  "2019-01-15 22:10:00 +0900 ... 2019-01-15 22:20:00 +0900"
  "2019-01-15 22:20:00 +0900 ... 2019-01-15 22:30:00 +0900"
  "2019-01-15 22:30:00 +0900 ... 2019-01-15 22:40:00 +0900"
  "2019-01-15 22:40:00 +0900 ... 2019-01-15 22:50:00 +0900"
  "2019-01-15 22:50:00 +0900 ... 2019-01-15 23:00:00 +0900"
  "2019-01-15 23:00:00 +0900 ... 2019-01-15 23:10:00 +0900"
  "2019-01-15 23:10:00 +0900 ... 2019-01-15 23:20:00 +0900"
  "2019-01-15 23:20:00 +0900 ... 2019-01-15 23:30:00 +0900"

MySQL に接続して, データがテーブル iotex2018_10min に保管されているか確かめること.

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 order by time;

       ...(略)...

    | 2019-01-15 09:30:00 | 19.68 |
    | 2019-01-15 09:40:00 | 19.14 |

crontab を設定して定期に実行させるようにする. ラズパイから送られてくる時間が毎分 0 秒とはならないので, 30 秒ほど sleep で調整している.

vm$ crontab -e

  MAILTO=""
  PATH=/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin
  */10 * * * * sleep 30; ruby bin/db_1min-10min.rb

1 時間おきの値を抽出する.

10 分間隔のデータから毎時 0 分のデータを SQL の命令を用いて取り出す. 気象分野ではこのように取り出した 1 時間おきの値を, 1 時間ごとの代表値とみなす. 次に行う 1 日の平均・最小・最大・標準偏差は, 1 時間おき (00 分) のデータを使って計算する.

以下のようなサンプルスクリプト (db_10min-1hour.rb) において, ホスト名や MySQL サーバ接続情報を自分のものに更新せよ. データベース接続情報 (db_info.yml) は絶対に ~/public_html 以下には置かないこと.

#!/usr/bin/env ruby
# coding: utf-8
#
# 表題: データ解析スクリプト. DB から 1 時間おきの値を抽出する.
# 

require 'yaml'
require 'mysql2'
require 'fileutils'
require 'active_support/time'

###
### 変数宣言
###

# ホスト名
myhost = "j52"

# データベースのテーブル名
mytable_from = "iotex2018_10min"
mytable_to   = "iotex2018_1hour"

# データベースへの接続情報の置き場.
# ~/public_html 以下には置かないこと.
conf = "/home/hogehoge/conf/db_info.yml"

# 設定ファイルの読み込み
mydb = YAML.load_file( conf )

# データベースへの接続
client = Mysql2::Client.new(
  :host     => "#{mydb["SERV"]}",
  :username => "#{mydb["USER"]}",
  :password => "#{mydb["PASS"]}",
  :database => "#{mydb["DBNM"]}"
)

###
### 新たなテーブルの作成
###

# テーブルの存在の確認
flag = false
sql  = "SHOW TABLES"
client.query(sql).each do |item|
  if item["Tables_in_#{mydb["DBNM"]}"] == mytable_to
    flag = true
  end
end
# テーブルが存在しない場合は新規作成. 
unless (flag)
  sql  = "CREATE TABLE #{mytable_to} like #{mytable_from}"
  client.query(sql)
end

###
### 時刻の設定
###

# 平均を開始する時間 (デフォルト値)
time_from = DateTime.new( 2018, 11, 1, 0, 0, 0, "JST")

# テーブルに既に値が入っている場合は平均開始時刻 time_from の値を更新する. 
sql = "SELECT time FROM #{mytable_to} 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 )
  end
end

###
### 毎正時の値を抽出
###

sql = "INSERT INTO #{mytable_to} 
       SELECT * FROM #{mytable_from} 
       WHERE TIME(time) LIKE '%00:00' AND time > '#{time_from}' "
p sql
client.query(sql)

実際に動かしてみる.

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

MySQL に接続して, データがテーブル iotex2018_1hour に保管されているか確かめること.

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_1hour order by time;

       ...(略)...

    | 2019-01-15 09:30:00 | 19.68 |
    | 2019-01-15 09:40:00 | 19.14 |

crontab を設定して定期に実行させるようにする. スクリプトを実行する時刻は毎時 10 分としている.

vm$ crontab -e

  MAILTO=""
  PATH=/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin
  */10 * * * *  sleep 30; ruby bin/db_1min-10min.rb
  10   * * * *  ruby bin/db_10min-1hour.rb

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

1 日ごとに統計量を計算する. 以下のサンプルスクリプト (db_1hour-1day.rb) を完成させよ. 平均・最大・最小・標準偏差には, SQL の AVG, MAX, MIN, STDDEV 関数を用いることができる. 1 日の平均・最小・最大・標準偏差は 1 時間おき (00 分) のデータを使って計算するため, 1 日 24 点分のデータが有るかないかをチェックし, 有る場合のみ計算を行なっている.

-> db_1hour-1day.rb を完成させないと本日の課題ができあがらないので注意.

#!/usr/bin/env ruby
# coding: utf-8
#
# 表題: データ解析スクリプト. 1 日平均値を作る
# 

require 'yaml'
require 'mysql2'
require 'fileutils'
require 'active_support/time'

###
### 変数宣言
###

# ホスト名
myhost = "j52"

# データベースのテーブル名
mytable_from = "iotex2018_1hour"
mytable_to   = {
  "min"  => "iotex2018_1day_min",
  "max"  => "iotex2018_1day_max",
  "avg"  => "iotex2018_1day_avg",
  "stddev"  => "iotex2018_1day_stddev"
}

# データベースへの接続情報の置き場.
# ~/public_html 以下には置かないこと.
conf = "/home/hogehoge/conf/db_info.yml"

# 設定ファイルの読み込み
mydb = YAML.load_file( conf )

# データベースへの接続
client = Mysql2::Client.new(
  :host     => "#{mydb["SERV"]}",
  :username => "#{mydb["USER"]}",
  :password => "#{mydb["PASS"]}",
  :database => "#{mydb["DBNM"]}"
)

###
### 新たなテーブルの作成
###

# テーブルの存在の確認
mytable_to.each_key do |key|
  flag = false
  sql  = "SHOW TABLES"
  client.query(sql).each do |item|
    if item["Tables_in_#{mydb["DBNM"]}"] == mytable_to[ key ]
      flag = true
    end
  end

  # テーブルが存在しない場合は新規作成. 
  unless (flag)
    sql  = "CREATE TABLE #{mytable_to[key]} like #{mytable_from}"
    client.query(sql)
  end
end

###
### 時刻の設定
###

# 平均を開始する時間 (デフォルト値)
time_from = DateTime.new( 2018, 11, 1, 0, 0, 0, "JST")

# テーブルに既に値が入っている場合は平均開始時刻 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 )
  end
end

# 平均を終了する時間
time_end = DateTime.now


###
### 前 1 日のデータから最大・最小・平均・標準偏差を計算
###

# 時刻の初期化
time0 = time_from
time1 = time_from + 1.days

# ループを回しながら最大・最小・平均・標準偏差をとる. 
while ( time1 < time_end ) do
  p "#{time0} ... #{time1}"

  ####
  #### 最小値
  ####
  sql = "SELECT count(temp) as count, 
         MIN(temp) as temp, MIN(humi) as humi, MIN(didx) as didx
         FROM #{mytable_from} 
         WHERE time > '#{time0}' AND time <= '#{time1}' 
         AND hostname LIKE '#{myhost}' ORDER BY time"

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

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

      # 変数に代入
      temp = item["temp"]
      humi = item["humi"]
      didx = item["didx"]

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

    end
  end

  ####
  #### 最大値
  ####

     #### ここに自分でスクリプトを書く ####


  ####
  #### 平均値
  ####

     #### ここに自分でスクリプトを書く ####

  ####
  #### 標準偏差
  ####

     #### ここに自分でスクリプトを書く ####


  time0 = time1
  time1 = time1 + 1.days
end

スクリプトが完成したら実際に動かしてみる.

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

MySQL に接続して, データがテーブル iotex2018_1day_min, iotex2018_1day_max, iotex2018_1day_avg, iotex2018_1day_stddev に保管されているか確かめること.

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_1day_min order by time;

       ...(略)...

    | 2019-01-15 09:30:00 | 19.68 |
    | 2019-01-15 09:40:00 | 19.14 |

  その他の iotex2018_1day_max, iotex2018_1day_avg, iotex2018_1day_stddev に
  ついても必ず確認すること. 

さらに, crontab に設定し, 定期的に実行できるようにする.

vm$ crontab -e

  MAILTO=""
  PATH=/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin
  */10 * * * * sleep 30; ruby bin/db_1min-10min.rb
  10   * * * * ruby bin/db_10min-1hour.rb
  20   0 * * * ruby bin/db_1hour-1day.rb 

grafana での可視化

作成した統計量を可視化する. -> 本日の課題

これまでに作成した grafana のダッシュボードを改良し, 以下のデータをダッシュボード上で表示せよ.

  • サーバ情報 (ホスト名, IP, SSID),
  • 気温 (1 分平均, 10 分平均, 1 時間おきの値)
  • 気温 (1 日平均の最小・最大・平均). 気象分野では 1 時間おきの値をつかってこれらを計算する.
  • 気温の標準偏差
  • 湿度 (1 分平均, 10 分平均, 1時間おきの値)
  • 露点温度 (1 分平均)
  • 不快指数 (10 分平均)

気温の 1 分平均値, 10 分平均値・1 時間おきの値を重ね書きする場合は, 以下のようにSQL を書くと良い. ホスト名は適宜書き換えること.

SELECT
  UNIX_TIMESTAMP(time) as time_sec,
  temp as "1min"
FROM iotex2018
WHERE
  $__timeFilter(time) AND
  hostname = 'j52'
ORDER BY time

SELECT
  UNIX_TIMESTAMP(time) as time_sec,
  temp as "10min"
FROM iotex2018_10min
WHERE
  $__timeFilter(time) AND
  hostname = 'j52'
ORDER BY time

SELECT
  UNIX_TIMESTAMP(time) as time_sec,
  temp as "1hour"
FROM iotex2018_1hour
WHERE
  $__timeFilter(time) AND
  hostname = 'j52'
ORDER BY time 

gnuplot を用いた可視化

gnuplot は伝統的かつ標準的な UNIX 系グラフ作成ツールであり, 覚えておいて損はない. 将来的に grafana は消えることはあっても gnuplot が消えることはほぼ無いであろう. 今回は gnuplot の ruby ラッパー (numo/gnuplot) を使って図を作ることにする.

grafana でグラフのタイトル部分を右クリックすると, データを csv に出力することができる (下図). この機能を用いて温度などの csv ファイルから gnuplot を用いてグラフを作成する. なお, CSV に変換する際は "Mode : Series as column" を選ぶこと. また Windows PC で "CSV export" を行うとファイルが Windows 上に保存される. FileZilla や scp で自分の仮想マシンにコピーするか (sky に一旦保存して, さらに sky から仮想サーバに転送する必要がある), Windows 上でファイルを開いてそれを仮想マシンのファイルにコピペする, などしないといけない.

gnuplot のインストール

$ sudo apt-get update 

  ... (略) ...

$ sudo apt-get install gnuplot-x11 fonts-noto-cjk

  ... (略) ...

$ sudo gem install numo-gnuplot

  Fetching: numo-gnuplot-0.2.4.gem (100%)
  Successfully installed numo-gnuplot-0.2.4
  Parsing documentation for numo-gnuplot-0.2.4
  Installing ri documentation for numo-gnuplot-0.2.4
  Done installing documentation for numo-gnuplot after 2 seconds
  1 gem installed

~/public_html において以下のようなスクリプトを作成・実行すると, ブラウザ上 (http://10.176.0.XXX/~hogehoge/, IP とユーザ名は適宜修正すること) から png ファイルを確認することができる.

#!/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] )
  data_list.push( row[1].to_f )
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}"

  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

課題

  • grafana のダッシュボードのスクリーンショットを提出せよ.
    • 作成した全てのグラフが含まれるようにスナップショットをとること. スナップショットが複数枚にわたっても構わない.
  • gnuplot で温度・湿度・不快指数の 3 種類のグラフを作成し, その png ファイルを提出せよ.

参考

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

気象観測統計

Ruby

gnuplot