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


初書:2020/11/09
php:7.4.10

前置き

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

目標の出力

前置きでも分かりづらいので、画像で(色がmarkdownだと表示できなかった)

ちなみにこれは正規分布乱数を出力している。
こんな感じで、出力の偏りを色で簡単に見れるというもの。

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

コード

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

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

/**
 * ターミナル上で、一次元分布を可視化します。(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
bash:tip_colors_and_formatting - FLOZz' MISC

つまり、出力時に\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
配列のキーを昇順に並び替える。
元々作成していた配列が、値0のキーを作成しておき、それに1ずつ足していく、という方法ではなく、その値が出来たらキーを作成して1ずつ足す、という方法であったため、キーが数値順に並んでいなかった。そのため、ここで一度並び替えを行っている。

$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
PHP: array_key_last - Manual

配列の先頭のキーもしくは最後のキーを取得する。
ちなみにこの関数はphp7.3で追加されたので、php7.2以前は使えない1
今回は、一つ前でキーソートを行っているので、実質キーの最小値と最大値を取得している。


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

使ってみる

今回は正規乱数を作成したかったので、以下のサイトを参考に、分布を表示してみる
平均値から正規分布乱数を生成する方法(PHP) | colori

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

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

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

まとめ

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


  1. 一応7.2以前でも、独自で実現する方法はあるようなので、7.2以下で使う場合は置き換えれば使用できる。