YouPlot - 標準入力からターミナルにグラフを描出するツール

## はじめに

こんにちは。昨年のアドベントカレンダーでは、GR.rbというRubyでグラフを描出するツールを紹介いたしました。

* [Rubyでグラフを描画するツール GR.rb の紹介](https://qiita.com/kojix2/items/456259f40d5563005cd1)

今年は[YouPlot](https://github.com/kojix2/youplot)というツールを作ったので紹介します。

Github: https://github.com/kojix2/youplot


### YouPlotは、ターミナルにグラフを描出するソフトです

　ターミナルの画面にアスキーアートでグラフを表示したいと思ったことはありませんか？YouPlotはそのためのツールです。グラフを描出するエンジンの部分は、mrknさんの作った[UnicodePlot](https://github.com/red-data-tools/unicode_plot.rb)を利用しています。**エンジンの部分を作る記事ではありません。**

　この記事では、前半でYouPlotの使い方を簡単に紹介し、後半でコマンドラインツールをRubyで作る上で感じたことを書きます。

## Uplotのつかい方

### インストール

```
gem install youplot
```

#### コマンド名はuplotです！[^1]

`youplot` でも動きます。

```
uplot
```

### ヒストグラム

```sh
echo -e "from numpy import random;" \
        "n = random.randn(10000);"  \
        "print('\\\n'.join(str(i) for i in n))" \
| python \
| uplot hist --nbins 20
```

### 

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144608/92943971-b420-2cb5-e31b-2e9f15b7c9d8.png)

### 散布図

```sh
curl -s https://raw.githubusercontent.com/uiuc-cse/data-fa14/gh-pages/data/iris.csv \
| cut -f1-4 -d, \
| uplot scatter -H -d, -t IRIS
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144608/7733cd19-de34-1686-abc6-ddd55ef39884.png)

```sh
curl -s https://raw.githubusercontent.com/uiuc-cse/data-fa14/gh-pages/data/iris.csv \
| cut -f1-4 -d, \
| uplot density -H -d, -t IRIS
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144608/d224b43b-d332-794c-f06b-2057366a229c.png)


### 折れ線グラフ

```sh
curl -s https://www.mhlw.go.jp/content/pcr_positive_daily.csv \
| cut -f2 -d, \
| uplot line -w 50 -h 15 -t 'PCR positive tests' --xlabel Date --ylabel number
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144608/6bf05898-4ecd-2767-554d-2f392b89a5dc.png)

### 箱ひげ図

```sh
curl -s https://raw.githubusercontent.com/uiuc-cse/data-fa14/gh-pages/data/iris.csv \
| cut -f1-4 -d, \
| uplot box -H -d, -t IRIS
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144608/81cc38eb-e8a5-262c-393d-463585b47afc.png)

## 棒グラフ

```
echo -ne "A:200\nB:300\n" | uplot bar -d: -s 🍎
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144608/bcbc87f3-ef17-d849-d2e3-005de1fa3925.png)

## そのほかの機能

* Kernel.gets が ARGF も取ってきてくれるので、`uplot lines -H -d, hoge.csv` みたいにファイルを指定することもできます。
* `--pass` または `-O` オプションをつけると、受け取った標準入力をそのまま標準出力に流し込むことができます。（グラフは標準エラー出力に描出されます）これは、`hoge | uplot | fuga` のようにコマンドをパイプラインのチェーンの間に挟んで使用する場合に便利です。このあたりのオプションの仕様は固まっていないのでまた変更になる可能性があります。
* `uplot color` で指定可能な色一覧が表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144608/dc8457db-08b6-1cec-9ec8-e6c033d1589b.png)
* `uplot count` で要素の数を数えて棒グラフにすることができます。

より詳しい使い方は `uplot --help` をご覧ください。`uplot bar --help` などとするとより詳しいオプションが表示されます。
[UnicodePlot](https://github.com/red-data-tools/unicode_plot.rb)の大半のオプションは指定できるようになっているはずです。[^2]

## YouPlotを作って感じたこと

### バックグラウンド

#### Rubyではコマンドラインツールが簡単に作れてしまう…

　Rubyはコマンドラインツールを作るのが得意です。Macで広く使われているパッケージマネージャーの `homebrew` はRubyで書かれています。

　YouPlotは`UnicodePlot`に、`optparse`を使ってコマンドラインインターフェースをつけたツールです。言い換えると、グラフ描画エンジンUnicodePlotに、データをパイプから読み込む仕組みや、オプションで操作を切り替えられるスイッチを付けて、一つのツールにまとめたものです。技術的に難しいことは何もやっていません。グラフの描出はすべてUnicodePlotにお任せしているのです。でも、便利です。

　Rubyでお気に入りのツールをラップすれば、簡単にコマンドラインツールを作成することができます。

#### でもYouPlotと同じようなツールは多くない…

　YouPlotを作る前に、私は同じようなツールがないかGoogle検索やGithubで探しました。デファクト・スタンダードになっているような便利なツールがすぐに見つかると思ったのです。けれども、私が探した範囲では、デファクト・スタンダードと呼べるようなツールはありませんでした。（強いて言うならば`gnuplot`がそれに近いかも知れません）それならば、ということで自分で作ることにしました。

　なぜYouPlotのようなツールはあまり多くないのでしょうか？

　その理由はいくつか考えられます。一つは、Unixのコマンドラインツールで作業をしている人の絶対数がそれほど多くないことです。

　もう一つの理由は、アスキーアートでグラフを表示するためのライブラリの不足です。自分で描画エンジンを作るのは結構大変です。YouPlotのようなツールを思いついても、作ることができなかった人もたくさんいると思います。

　例えば、GoやRustのような言語では、コマンドラインツールが盛んに開発されています。しかし、必ずしもこれらの言語でターミナル上にアスキーアートでグラフを描出するライブラリが特別に豊富というわけではなさそうです。だからそれなりに手間がかかる気がします。

#### アスキーアートでグラフを描出するライブラリ UnicodePlot

　幸いにしてRubyには、ターミナル上にアスキーアートでグラフを表示する便利なライブラリがありました。YouPlotが使っているグラフ描画エンジンは`UnicodePlot`です。これは、Rubyのコミッタで、Julia言語が好きなmrknさんが、JuliaのUnicodePlotをRuby向けに実装し直したものです。

* https://github.com/red-data-tools/unicode_plot.rb

　Julia言語の[UnicodePlot](https://github.com/Evizero/UnicodePlots.jl)は、ターミナル上にグラフを描出する有名なライブラリです。しかし、Julia言語は、REPLやJupyterを通して利用する場合が多く、コマンドラインツールの作成はそれほど活発ではありません。

　こんな感じで、いろいろな偶然が重なって、YouPlotもちょっとだけユニークなツールになっているのです。

#### ツールはみんなユニーク

　ツールは、二度と重ならない条件のもとで生まれます。作者、目的、環境、マシン、ユーザー、仲間、同僚、家族、時代…　そういったものが交差した座標はどれもユニークです。だから、どのツールも必ずユニークになります。

　YouPlotは、もともと生命情報のタブ区切りテキストをすばやく観察したい目的で作られました。なのでリアルタイムに時系列データを表示する機能がありません。一方やはりRubyで実装されている[termplot](https://github.com/Martin-Nyaga/termplot)というツールは、YouPlotより少しあとに開発されましたが、リアルタイムでグラフを描く機能に特化しています。このように作者の目的によってツールは少しずつ違ったものになります。

　さらに、全てのツールは作者に求められて生み出されています。だから、あなたが作っているツールも、おそらくユニーク、かつ誰かに求められているはずです。私が言いたいことは一つです。みんな、Rubyでツールを作ったら公開しよう。

…

…（さて、今日は十分ポエムを書いたので、もう帰っていいですか）


### OptionParserを使ってコマンドラインオプションをつける

コマンドラインツールを作るのにはRuby標準ライブラリの[optparse](https://docs.ruby-lang.org/ja/latest/library/optparse.html)を使いました。optparseは下記のような構文でコマンドラインオプションを用意してくれます。

```ruby
require 'optparse'
opt = OptionParser.new

params = {}

opt.on('-a') {|v| params[:a] = v }
opt.on('-b') {|v| params[:b] = v }

opt.parse!(ARGV)
```

[公式リファレンス](https://docs.ruby-lang.org/ja/latest/library/optparse.html)がまとまっているのでしっかりと目を通します。

optparseの応用は、sonotsさんの下記の記事がとても参考になりました。

* [Ruby の OptionParser チートシート](https://qiita.com/sonots/items/1b44ed3a770ef790a63d)

（今回YouPlotでは利用していませんが、すこし複雑なツールを作りたい時は[Piotr Murach](https://github.com/piotrmurach)さんが開発している[TTYツールキット](https://ttytoolkit.org/)が便利です。たとえば、ターミナルにボックスやツリーを表示したり、コマンドを実行して結果を表示する機能などがあります。）

### オブジェクト指向のRubyでデータ分析用のツールを作るときに気になること

　この項は、主観が強く入っています。私はプログラミングも数学もよくわかりません。コンピュータやプログラミング、数学に造形の深い方らみると間違っている記述もあるかもしれません。一つの意見として聞いてください。

　Rubyはデータ処理用の言語としてはそれほど広く使われていません。その理由はいろいろ言われています。ライブラリが足りない、とか。しかし、私の個人的な意見としては、オブジェクト指向とデータ分析相性があまりよくないからだと思います。普及しているデータ処理用の言語、例えばR, Juliaは、どちらもオブジェクト指向らしくはありません。（このような話題では、どうしてもPythonに注目が集まる傾向がありますが、Pythonよりも、RやJuliaとの比較に意味があると思います）

#### 「データ」と「手続き」

 オブジェクト指向はむずかしく、私は理解できまている自信はありません。しかし、言いたいことを自分なりに表現するために続けますね。オブジェクト指向とは「データ」と「手続き」を一つのまとまりとして扱うことだと理解しています。しかし、「データ」と「手続き」を一つのカタマリとして扱うこと、これは、データを分析する際には不便な面があると思います。

　私たちはデータを分析するとき、「データ」に、さまざまな「手続き」を適応して、うまくいく手続きを探そうとします。あたらしい「手続き」を開発しようとしている人がいます。「データ」にどの「手続き」を適応すると良い結果が出るかを試して、それを仕事にしている人もいます。結果だけほしくて、「手続き」には興味がない人もいます。いずれにせよ、この目的のためには「データ」はデータだけ、「手続き」は手続きだけで管理して、互いに依存しないようにバラバラにしておいた方が安全できれいに見えるでしょう。

　一方で、Rubyでは、全てはオブジェクトで構成されており、原則として「手続き」はメソッドとしてすべて作者の意図通りにオブジェクトに所属しています。（単体の手続きは`lambda`や`proc`,`block`など存在はします）ここでは、オブジェクトにどの「手続き」を適応すればよいのかは、オブジェクト自身が知っています。私たちはオブジェクトがどんなメソッドを持っているのか、オブジェクト自身に問い合わせることができ、自分で「手続き」を管理する必要はありません。そう考えるとRubyでは「データ」と「手続き」の関係性がよく整備された環境で、成果物そのものに注力できる良い環境が整っているとも言えるかもしれません。しかし、「データ」と「手続き」の関係性自体を探索的に調べているときには、この環境はベストではないかもしれません。

　Rubyのオブジェクトは、インスタンス変数を持っています。なので、メソッドを呼び出すとしばしばオブジェクトの状態が変わってしまいます。これもデータを分析する上では望ましくない性質です。

　たとえば、Jupyter Notebookでセルを一つづつ実行していいくことを考えます。セルを実行する順番を変えたり、あとからもう一回実行すると結果が変わってしまうようなワークフローは、データ分析の観点からはあまり望ましくないと思います。そしてJupyter Notebookを使っているような場合には、そのような試行錯誤が頻繁に発生します。このようなケースでは内部の状態が変化してしまうオブジェクトとメソッドは活躍しにくくなります。

　このように、オブジェクト指向の持つ「データ」と「手続き」をまとめて管理する傾向は、データ分析ではしばしば不利になるケースがあると私は考えます。

### モジュール関数(module function)

　そうはいっても、Rubyでも、どうしても「手続き」そのものを扱いたいこともあります。例えば、数学の関数がそれに当たります。RubyではMathモジュールというものが存在し、これらはモジュール関数(module function)として作成されています。モジュール関数は、特異メソッドでありかつ、プライベートメソッドでもあるような関数のことです。

```ruby
Math.sin(x)   # 特異メソッドなので呼べる

include Math
sin(x)        # プライベートメソッドでもあるので呼べる

Object.sin(x) # 呼べない
```

　Mathモジュールは、クラスではなくてモジュールなので、インスタンス変数を持ちません。だから、何回実行しても内部の状態が変わって結果が変わることはありません。

```ruby
# 馬の耳に念仏モジュール
module Horse
  module_function
  def hear(sound)
    "..."
  end
end

# Horseはモジュールなので中味は変化しない

Horse.hear("念仏")
Horse.hear("東風") # 馬耳東風
```

　なんだ、Rubyでも関数が作れるんだ、よかった。という話になるでしょうか…。

　長々と書きました。YouPlotのような小さなツールを作るためには、こんなことをくどくど考える必要はないかもしれません。けれども、ライブラリを作る前に、一つのオブジェクトをモジュールにしようか、クラスにしようか、Mixinをどうしようかと悩んだりすることは、パッケージを作った方はきっと経験があると思います。

　YouPlotでも上記のようなことをあれこれ考えて、部分的にモジュール関数(module function)を使っています。モジュール関数を作るのは簡単で、`module_function` と書くだけです。

### 構造体(struct)

YouPlotでは、パラメータの保持に構造体を使っています。

```ruby
Qiitan = Struct.new(:name, :age)

qiitan = Qiitan.new("キータン", 6)
qiitan.age # 6
```

　StructはHashと比較して、デフォルトでkeyが固定されています。そのため何かのタイミングで新しくキーが追加されてしまうことを防ぐことができて少し安心です。また、値を取り出す記述がメソッドのように見えるのですこしすっきりします。

　モジュール関数や構造体を使うRubyのコードは、どちらかというとあまりRubyらしくない記述法だと思います。けれども、Juliaや他の言語のデータ解析関連のライブラリーはオブジェクトとメソッドという組み合わせよりは、構造体と関数という発想で作られている場合があるのではないかと思います。そういったライブラリをRubyに移植することを考えると、module function や struct　のようなものを適材適所で使っていけば、頭を切り替えなくてもすむというメリットがあります。とくにデータ分析のツールを作る上では、活躍する場面があるのではないかと思います。

（一方で、「データ」と、それに適応する「手続き」が一度確立された分野では、Rubyのオブジェクト指向の強みがいかんなく発揮されていくのではないかと思います。）

## YouPlotの課題とこれから

#### リアルタイムにグラフを表示したい

　YouPlotはデータをリアルタイムに表示できません。いずれそういったことができるようになるといいなと思っています。

#### テストとエラーメッセージの整備

 勉強して少しずつテストを追加したり、エラーメッセージを整備していきたいと思っています。

#### 前処理・統計処理など

　YouPlotのようなツールに簡単な前処理や統計の機能をつけようとするのは自然だと思います。しかし、Rubyのスクリプトでありますのでスピードがでないと思っています。`datamash` などの専用のコマンドと組み合わせる方がUnixらしくて現実的かも知れません。
　もしも、YouPlotと同様のツールをGolangやRustで開発したらとても便利なツールになるかも知れません。興味のある方は挑戦してみてください。


### おわりに

みなさんもRubyのツールを作っていたらぜひ公開してください。

この記事は以上です。

[^1]: （本当はgem名もuplotにしたかったのですが、[uPlot](https://github.com/leeoniya/uPlot)という有名なJavaScriptのツールがあって、そちらに関するライブラリにネームスペースを取られてしまいました。ググラビリティ改善の目的もありYouPlotとしました。）
[^2]: オプションやサブコマンドは今後も変更する可能性があります。