JavaScript
GoogleAppsScript

JavaScriptの配列の最頻値を求めるコピペ

概要

JavaScriptって数値計算用の機能が貧弱ですよね。
「じゃあPython使う?」ってなっちゃっても良いんですけど、
漢にはどうしてもJSで頑張らなくちゃいけないときがあります。GoogleAppsScriptみたいに。
(淑女の方にもそんな時あると思います)

特に配列の要素の最頻値を返すなんて超ありがちな処理をわざわざフルスクラッチで書かなくちゃいけなくなった時なんて殺意すら感じます。

なので今回はArrayクラスに勝手に最頻値を返すメソッドを追加する技について書きたいと思います。
ネイティブ型の空間をぶち壊したくない型は素直に関数として定義して下さい。

環境

2018年1月現在普通のブラウザ
Node.js v8.9.4
2018年1月現在のGoogleAppsScript
などなど

実装

配列に入りうる中身によって2パターン作りました。

機能要件

  • 要素にArrayや連想配列のようなobjが入ることが無い
  • 返り値は最頻の値。
  • 同数の場合はindexが若いものを返す。
  • GoogleAppsScriptのようなES6が使えない環境においても動作可能にする。

使い方

単純

howtouse.js
Array.prototype.mode = ....中略

var samples = ["りんご","ばなな","りんご","みかん"]
samples.mode()
// "りんご"

一番堅実な実装(おすすめ)

  • nullundefinedに対応。
    • 配列の要素数が0の時、nullを明示的に返さない。
  • 型違い(undefined"undefined"のような紛らわしい違いも含む)を別のものとして扱う
  • 普通は気にしなくていいが非常に大きな配列を食わせるとパフォーマンスに影響する可能性あり。
mode.js
Array.prototype.mode = function () {
    if (this.length === 0){
        //配列の個数が0だとエラーを返す。
        throw new Error("配列の長さが0のため最頻値が計算できません");
        //nullを返しても困らない時(配列の中にnullが無い時)はnullを返すように実装しても良い。
        //return null
    }
    //回数を記録する連想配列
    var counter = {}
    //本来の値を入れた辞書
    var nativeValues = {}

    //最頻値とその出現回数を挿入する変数
    var maxCounter = 0;
    var maxValue = null;

    for (var i = 0; i < this.length; i++) {
        //counterに存在しなければ作る。keyは型を区別する
        if (!counter[this[i] + "_" + typeof this[i]]) {
            counter[this[i] + "_" + typeof this[i]] = 0;
        }
        counter[this[i] + "_" + typeof this[i]]++;
        nativeValues[this[i] + "_" + typeof this[i]] = this[i];

    }
    for (var j = 0; j < Object.keys(counter).length; j++) {
        key = Object.keys(counter)[j];
        if (counter[key] > maxCounter) {
            maxCounter = counter[key];
            maxValue = nativeValues[key]
        }
    }
    return maxValue

}

細かいことを気にしない雑な実装

  • 型違いの要素に非対応
  • 極めて特殊な要素("2+3"とかevalして値が変わるもの)が来た時にバグる。
  • 非常に細かいパフォーマンスの差を気にしない限り↑を使うのが堅実
mode_zatsu.js
Array.prototype.mode = function() {
 var counter = {}
 var maxValue = null
 var maxCounter = 0;
 for (var i=0;i < this.length;i++){
   if (!counter[this[i]]){
     //存在しなかれば作る
     counter[this[i]] = 0
   }
   counter[this[i]]++;

   for (var j=0;j<Object.keys(counter).length;j++){
     key = Object.keys(counter)[j];
     if (counter[key] > maxCounter){
       maxValue = key
       maxCounter = counter[key]
     }
   }

 }

 return eval(maxValue)
 // 以下のようにすることで全て文字列で返すさらに雑な実装を実現可能
 //return maxValue 
}

今後の課題と感想

  • 2次元配列やobjの入る配列のような多様な配列に対応
    ES6が動く環境ならSymbol型が使えるので簡単に作れそう。ただ、筆者はGoogleAppsScriptで動かしたかったために、機能の限定とクソレガシーな書き方を余儀なくされた。悲しい。
  • ES6が使えないと想像以上にしょうもないところでコケる。
    GiigleAppsScriptの残念すぎる実行環境が悪い。おこ。