１次元配列の分布をターミナルで可視化して簡単にデバッグする

初書：2020/11/09
php：7.4.10

# 前置き
タイトルで何言ってるか微妙に伝えづらかったので、説明。
何をしたいのかというと、例えば乱数生成器を作成したとする。その関数がどれくらい偏っているかを簡単に調べたい。
ただグラフを書こうとするとターミナルで実行しても表示できない(知識がないだけかもしれないが)ので、簡
単に可視化する方法がないか考え、棒と色で可視化させようとしたもの。

# 目標の出力
前置きでも分かりづらいので、画像で（色がmarkdownだと表示できなかった）
![スクリーンショット 2020-11-10 13.23.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/636785/f09b6d67-e445-a250-54ab-7e5b705065ce.png)
ちなみにこれは正規分布乱数を出力している。
こんな感じで、出力の偏りを色で簡単に見れるというもの。

ちなみにターミナル用に作成しているので、いわばデバッグ用である。


# コード
先にコードを置いておく。今回はgithubも作成してみたので、ダウンロードして速攻使えるようにしてある。
[distribution_show/distribution_show.php at main · yuu-1st/distribution_show](https://github.com/yuu-1st/distribution_show/blob/main/distribution_show.php)
（初めてgithubを使ったので変な使い方してるかも）

で、改めてコード。少し長い。

```php
/**
 * ターミナル上で、一次元分布を可視化します。(need PHP 7.3.0)
 *
 * @param array $arr 分布配列。キーは全て整数値である必要があります。
 * @param integer $interval 出力間隔。値は切り捨てで圧縮されます。
 * @param int $maxwidth 出力時の横幅。指定文字数を超えると改行されます。
 * @return void
 */
function distribution_show(array $arr, int $interval = 1, int $maxwidth = PHP_INT_MAX) : void
{
    ksort($arr); 
    $min = array_key_first($arr);
    $max = array_key_last($arr);
    $value_max = 0;
    $array = [];
    for ($i = $min; $i <= $max; $i++) {
        $key = (int)floor(($i - $min) / $interval);
        $array[$key] ??= 0;
        $array[$key] += isset($arr[$i]) ? $arr[$i] : 0;
        $value_max = $array[$key] > $value_max ? $array[$key] : $value_max;
    }
    $console = "";
    $co = 0;
    $coplus = 0;
    for ($i = 0; $i < count($array); $i++) {
        if ($array[$i] > 0) {
            $color = (int)floor((255 - 232) * ($array[$i] * 1.0 / $value_max) + 232);
            printf("\e[38;5;%dm%s\e[m", $color, "|");
        } else {
            printf(" ");
        }
        if ($coplus % 10 == 0) {
            $key = $min + $i * $interval;
            $console .= "↑" . $key;
            $coplus += mb_strlen($key) + 1;
        } elseif ($co == $coplus) {
            $console .= " ";
            $coplus++;
        }
        $co++;
        if ($co % $maxwidth === 0) {
            printf("\n%s\n", $console);
            $console = "";
        }
    }
    printf("\n%s\n", $console);
}
```

# ターミナルで色を出力する

今回のメインとなるところ。グラフで表示するには最低でも二次元平面が必要になるので、一次元で表示するには色しかない。
調べてみると、ターミナルでも色の出力は可能な様子。

参考サイト：
[ターミナルのechoやprintfに256色で色をつける 完全版 - vorfee's Tech Blog](https://vorfee.hatenablog.jp/entry/2015/03/17/173635)
[bash:tip_colors_and_formatting - FLOZz' MISC](https://misc.flogisoft.com/bash/tip_colors_and_formatting)

つまり、出力時に`\e[38;5;255m文字列なんとか\e[m`を含め、255を指定の色コードに変えれば色を変更することが可能。
ただ残念なことにRGBでの出力は出来ず、256色しかないので、0~255を1ずつ変えて〜は出来ない。
後者の参考サイトに色の一覧が載っているので、有難く拝見させてもらうと、どうやら232-256の間では白黒でグラデーションができるらしい。白黒で十分なので、この領域を使用することにする。
（なお、コード内の指定値は232~255になっているのだが、これは256を選択すると色が変になったので、念のために除外した）

ちなみにphpの場合はechoでもprintfでも上記の出力を行うと色が変化する。今回はターミナルでの出力なので、それらしいprintfを採用した。

# 配列を操作する

正直これが出来たらあとは作業になる。配列をごちゃごちゃしてprintfで出力するだけなのだから。

詳しい内容はコードを読んでもらうとして、配列操作に使った関数をいくつかメモ。

## ksort
[PHP: ksort - Manual](https://www.php.net/manual/ja/function.ksort.php)
配列のキーを昇順に並び替える。
元々作成していた配列が、値0のキーを作成しておき、それに1ずつ足していく、という方法ではなく、その値が出来たらキーを作成して1ずつ足す、という方法であったため、キーが数値順に並んでいなかった。そのため、ここで一度並び替えを行っている。

```php
$array = ["25" => 13, "19" => 43, "8" => 24];
ksort($array);
var_dump($array); // array(3) {[8]=>int(24) [19]=>int(43) [25]=>int(13)}
```

## array_key_first / array_key_last
[PHP: array_key_first - Manual](https://www.php.net/manual/ja/function.array-key-first.php)
[PHP: array_key_last - Manual](https://www.php.net/manual/ja/function.array-key-last.php)

配列の先頭のキーもしくは最後のキーを取得する。
ちなみにこの関数はphp7.3で追加されたので、**php7.2以前は使えない**[^1]
[^1]: 一応7.2以前でも、独自で実現する方法はあるようなので、7.2以下で使う場合は置き換えれば使用できる。

今回は、一つ前でキーソートを行っているので、実質キーの最小値と最大値を取得している。

---
とまぁとりあえず前処理を行い、あとは一つ目のforループで配列の再生成(間隔圧縮のため)と値の最大値を取得し、二つ目のforループで出力を行っていく。


# 使ってみる

今回は正規乱数を作成したかったので、以下のサイトを参考に、分布を表示してみる
[平均値から正規分布乱数を生成する方法（PHP） | colori](https://colo-ri.jp/develop/2017/10/gausian-rand-from-average.html)

```php
$arr = [];
for ($i = 0; $i < 100000; $i++) {
    $a = (int)round(normal(500, 170)); // 参考サイトにあるnormalと同じ
    $arr[$a] ??= 0;
    $arr[$a]++;
}
distribution_show($arr, 5, 150);
```

normal関数で乱数を取得し、整数値化。その値のキーをインクリメントすることで、カウントしていく。
そのあと、先ほど作成した関数を呼び出し、第一引数に配列、第二引数には一つの棒がどの範囲を示すか、第三引数は改行する文字数。

![スクリーンショット 2020-11-10 13.18.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/636785/da37cc61-1e65-4b77-1260-961ebda21799.png)

今回は基準を500にしているので、500を中心に色が濃い事が分かる。

一応第二引数の詳しい説明をしておくと、今回の画像だと左端が-168を指しているので、この棒一つで-168~-164の5数値がまとめているという事。1ずつだとかなり横長になったので追加した。

# まとめ
せっかく作ったけど、実際にこの関数が役に立つことはほとんどないので、誰か使ってあげてください。