データ分析のためのプログラミング言語といえば R や Python が有名ですが、実はJavaScriptでもデータ分析ができるんです!
今回はJavaScriptの統計処理ライブラリ「jStat」を利用して、データを集計し、そのデータを元に分析を行うアプリをMonacaで作ってみたいと思います。

データ分析とは

そもそも、データ分析はどういった場面で役立つのでしょうか。ひとつの例をもとに説明したいと思います。


例として、店頭で本を購入してくれた人にはおまけをプレゼントするキャンペーンを実施したとします。

おまけには、以下の3種類を用意しました。どれかひとつだけ好きなものをもらえる、という制限つきです。

  1. もなか
  2. チョコモナカ
  3. レトルトカレー

どのおまけを選んだ人が多いのかを集計したところ、以下のような結果が得られました。

実施日程 もなか チョコモナカ レトルトカレー
一日目 40 50 60
二日目 39 51 55
三日目 52 46 57
四日目 45 43 66
五日目 48 62 68
合計 224 252 306

レトルトカレーが一番人気だったので、「レトルトカレーをプレゼントするのがもっとも効果的である」と思うのではないでしょうか。
しかし、この情報だけでは「たまたま今回はレトルトカレーを選んだ人がちょっと多かっただけなんじゃないの?プレゼントなんてどれを配ろうと大差ないんじゃないの?」と考える人もいるかもしれません。
そこで「いや、たまたまとかじゃなくて、マジで全然違ったんだって!」という主張を裏付けるために役立つのがデータ分析です。
なお、「たまたまとかじゃなくてマジで違いがある」ことを専門的な言い方で「統計的に有意な差がある」といいます。

このケースにおける分析手法としては、3種類全体の母平均に差があるかどうかを調べる「分散分析(ANOVA:analysis of variance)」と、
もなかとチョコモナカ、もなかとレトルトカレー、といったようにそれぞれの組み合わせにおける差を比較する「テューキーの多重比較検定」がよく用いられます。

今回作るアプリ

起動時は以下の状態になっています。

ラベルをタップすると、名称を変更できます。

「+」ボタン、「-」ボタンで集計数をカウントします。

「保存」ボタンで1日分の集計データをローカルストレージに保存します。

「合計の確認」ボタンで各グループ毎の合計数を表示します。

「分析結果の確認」ボタンで分析を実行します。

プロジェクトの構成

以下のライブラリを利用しています。

  • Onsen UI
  • jQuery
  • jStat

それぞれのライブラリはMonacaの「JS/CSSコンポーネントの追加と削除」画面から追加してください。

jStatはインストール時に選択可能なファイル名がたくさん表示されますが、components/jstat/dist/jstat.min.js にチェックを入れればOKです。

集計機能のソースコード

HTML(index.html)

<!DOCTYPE HTML>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
  <meta http-equiv="Content-Security-Policy" content="default-src * data: gap: https://ssl.gstatic.com; style-src * 'unsafe-inline'; script-src * 'unsafe-inline' 'unsafe-eval'">
  <script src="components/loader.js"></script>
  <script src="lib/onsenui/js/onsenui.min.js"></script>

  <link rel="stylesheet" href="components/loader.css">
  <link rel="stylesheet" href="lib/onsenui/css/onsenui.css">
  <link rel="stylesheet" href="lib/onsenui/css/onsen-css-components.css">
  <link rel="stylesheet" href="css/style.css">

  <script src="js/app.js"></script>
</head>
<body>
  <ons-page style="text-align:center;">
    <ons-toolbar>
      <div class="center">集計アプリ</div>
    </ons-toolbar>
    <ons-row id="dataLabels">
      <ons-col><ons-button modifier="quiet">ラベル1</ons-button></ons-col>
      <ons-col><ons-button modifier="quiet">ラベル2</ons-button></ons-col>
      <ons-col><ons-button modifier="quiet">ラベル3</ons-button></ons-col>
    </ons-row>
    <ons-row id="countLabels" style="font-size:24px;font-weight:bold;margin-bottom:16px">
      <ons-col><span>0</span></ons-col>
      <ons-col><span>0</span></ons-col>
      <ons-col><span>0</span></ons-col>
    </ons-row>
    <ons-row id="plusButtons" style="margin-bottom:20px">
      <ons-col><ons-button style="width:60px"><ons-icon icon="plus"></ons-icon></ons-button></ons-col>
      <ons-col><ons-button style="width:60px"><ons-icon icon="plus"></ons-icon></ons-button></ons-col>
      <ons-col><ons-button style="width:60px"><ons-icon icon="plus"></ons-icon></ons-button></ons-col>
    </ons-row>
    <ons-row id="minusButtons" style="margin-bottom:20px">
      <ons-col><ons-button style="width:60px"><ons-icon icon="minus"></ons-icon></ons-button></ons-col>
      <ons-col><ons-button style="width:60px"><ons-icon icon="minus"></ons-icon></ons-button></ons-col>
      <ons-col><ons-button style="width:60px"><ons-icon icon="minus"></ons-icon></ons-button></ons-col>
    </ons-row>
    <ons-row style="padding:10px 22px">
      <ons-button id="saveButton" modifier="large">保存する</ons-button>
    </ons-row>
    <ons-row style="padding:10px 22px">
      <ons-button id="resetButton" modifier="large">リセット</ons-button>
    </ons-row>
    <ons-row style="padding:10px 22px">
      <ons-button id="sumButton" modifier="large">合計の確認</ons-button>
    </ons-row>
    <ons-row style="padding:10px 22px">
      <ons-button id="analyzeButton" modifier="large">分析結果の確認</ons-button>
    </ons-row>
  </ons-page>
</body>
</html>

JavaScript(js/app.js)

// カウント数
let count = [0,0,0];

ons.ready(function() {

  // ラベルタップ時の処理
  $('#dataLabels ons-button').on('click', (event) => {
    // 新しいラベルを入力させる
    let value = prompt('新しいラベルを入力してください');
    $(event.target).text(value);
  });

  // +ボタンタップ時の処理
  $('#plusButtons ons-button').each((index, elm) => {
    $(elm).on('click', (event) => {
      // カウントに1加算
      count[index]++;
      $('#countLabels span').eq(index).text(count[index]);
    });
  });

  // -ボタンタップ時の処理
  $('#minusButtons ons-button').each((index, elm) => {
    $(elm).on('click', (event) => {
      // カウントから1減算
      count[index]--;
      $('#countLabels span').eq(index).text(count[index]);
    });
  });

  // リセットボタンタップ時の処理
  $('#resetButton').on('click', () => {
    // カウントをすべて0にする
    count = [0,0,0];
    $('#countLabels span').each((index, elm) => {
      $(elm).text(count[index]);
    })
  });

  // 保存ボタンタップ時の処理
  $('#saveButton').on('click', () => {
    // ローカルストレージに保存
    Util.saveItem();
  });

  // 合計の確認ボタンタップ時の処理
  $('#sumButton').on('click', () => {
    // 保存済みの全データを取得
    let items = Util.getItems();
    // 値の部分のみをArray型で抽出
    let values = Object.values(items);
    // 2次元配列の列方向合計を得る
    let sumValues = jStat(values).sum();

    let message = '';
    sumValues.forEach((value, index) => {
      message += $('#dataLabels ons-button').eq(index).text();
      message += ':';
      message += value;
      message += '<br>';
    });
    ons.notification.alert({message:message, title:'合計'});
  });

  // 分析結果の確認ボタンタップ時の処理(後述)


});

// ローカルストレージ操作関連
class Util {
  // ローカルストレージのデータを取得
  static getItems() {
    let localData = localStorage.getItem('count_data');
    if(localData !== null) {
      // ローカルストレージにデータがあれば、オブジェクト型に復元して返す
      return JSON.parse(localData);
    } else {
      // なければ、空のオブジェクトを返す
      return {};
    }
  }
  // 今日のデータを保存
  static saveItem() {
    // 今日の日付をyyyy-MM-dd形式にする
    let date = new Date();
    let today = `${date.getFullYear()}-${date.getMonth()+1}-${date.getDate()}`;
    // ローカルストレージのデータを取得
    let items = this.getItems();
    if(today in items) {
      // 既に今日の分のデータが保存済みの場合
      let result = confirm('本日分のデータを上書きしますか?')
      if (!result) return;
    }
    // 今日のデータをオブジェクトにセット
    items[today] = count;

    // ローカルストレージに保存
    localStorage.setItem('count_data', JSON.stringify(items));
    alert('保存しました');
  }
};

ポイントは、ローカルストレージへの保存形式です。以下のように、日付ごとに集計データを保存しています。

{
  "2018-02-01" : [40,50,60],
  "2018-02-02" : [39,51,55],
  "2018-02-03" : [52,46,57],
  "2018-02-04" : [45,43,66],
  "2018-02-05" : [48,62,68]
}

分析機能のソースコード

「分析結果の確認」ボタン押下時に、分散分析とテューキーの多重比較検定を行います。

  // 分析結果の確認ボタンタップ時の処理
  $('#analyzeButton').on('click', () => {
    // 保存済みの全データを取得
    let items = Util.getItems();
    // 値の部分のみをArray型で抽出
    let values = Object.values(items);
    // 2次元配列の行と列を入れ替える
    let groupData = jStat.transpose(values);

    // 分散分析
    let anovaPvalue = jStat.anovaftest.apply(this, groupData);
    let message = '';
    // p値が5%未満なら統計的に有意な差があると判定
    if (anovaPvalue < 0.05) {
      message += '3グループ間の平均値に差があります<br>';
    } else {
      message += '3グループ間の平均値に差はありません<br>';
    }

    // テューキーの多重比較検定
    let tukeyPvalue = jStat.tukeyhsd(groupData);
    tukeyPvalue.forEach((value) => {
      let factor1 = value[0][0];
      let factor2 = value[0][1];
      let p = value[1];
      // p値が5%未満なら統計的に有意な差があると判定
      if(p < 0.05) {
        message += $('#dataLabels ons-button').eq(factor1).text() + 'と';
        message += $('#dataLabels ons-button').eq(factor2).text() + '間に差があります<br>';
      }  
    });
    ons.notification.alert({message:message, title:'分析結果'});
  });

分散分析(ANOVA:analysis of variance)

jStatによる分散分析は、jStat.anovaftest() を使って行います。

jStat.anovaftest(array1, array2, … )

引数にはもなかやチョコモナカといった各グループ毎の集計データを配列で渡します。しかし、ローカルストレージには日付毎の集計データが保存されています。
そこで、jStat.transpose() を利用して配列の行列を入れ替え、グループ毎の集計データに変換しています。jStatにはこのようなユーティリティメソッドも豊富に用意されています。

ローカルストレージに保存されているデータの変換手順

{
  "2018-02-01" : [40,50,60],
  "2018-02-02" : [39,51,55],
  "2018-02-03" : [52,46,57],
  "2018-02-04" : [45,43,66],
  "2018-02-05" : [48,62,68]
}

↓ Object.values() によって値の部分のみをArray型で抽出

[
  [40,50,60],
  [39,51,55],
  [52,46,57],
  [45,43,66],
  [48,62,68]
]

↓ jStat.transpose() によって二次元配列の行と列を入れ替える

[
  [40,39,52,45,48],
  [50,51,46,43,62],
  [60,55,57,66,68]
]

また、jStat.anovaftest() は複数の配列を可変長引数として受け取ります。
そのため、Objectオブジェクトの apply() メソッドで二次元配列を可変長形式に変換したうえで引数を渡しています。
本来、 apply() はあるオブジェクトが持つメソッドを別のオブジェクトが持っているかのように振る舞わせるための命令で、実行時に二次元配列形式で渡した引数が可変長形式に変換されるという特性があります。
今回はこの特性を利用し、同一オブジェクト間(jStatオブジェクト)で引数の形式を変換したうえで anovaftest() メソッドを実行しています。

二次元配列を可変長引数に変換してメソッドを実行する

jStat.anovaftest.apply(this, [[40,39,52,45,48], [50,51,46,43,62],
[60,55,57,66,68]]
);

jStat.anovaftest( [40,39,52,45,48], [50,51,46,43,62], [60,55,57,66,68] );

jStat.anovaftest() の戻り値はp値(有意確率)です。一般的に、p値が1~5%未満であれば統計的に有意であると判定するケースが多いようです。
今回は5%を基準として採用しました。

テューキーの多重比較検定

jStatによるテューキーの多重比較検定は、jStat.tukeyhsd() を使います。

jStat.tukeyhsd([ array1, array2, … ])

jStat.anovaftest() とは異なり、二次元配列形式で引数を渡せます。

戻り値は以下の形式になります。それぞれの組み合わせにおけるp値が返却されます。

[
  [ [0,1], 0.35340848974638084 ], --> もなかとチョコモナカ
  [ [0,2], 0.003193013906594544 ], --> もなかとカレー
  [ [1,2], 0.04147516365222781 ] --> チョコモナカとカレー
]

有意水準を5%とした場合、もなかとチョコモナカ間には統計的に有意な差はなく、もなかとカレー間、チョコモナカとカレー間には統計的に有意な差があるという結果になりました。
このことから、レトルトカレーは他のプレゼントよりも効果的であるということがわかります。


jStatは今回紹介した分析手法の他にも、T検定やZ検定などの様々な分析機能を提供しています。ドキュメント によると、回帰分析も今後提供される予定だそうです。
このように、JavaScriptの統計処理ライブラリを利用することで手軽に分析機能を実装することができます。既存のアプリに組み込むのも容易なので、ぜひ試してみてください。

今回のソースコードは GitHub で公開しています。開発の参考になれば幸いです。


ところで・・・モナカレーキャンペーンにはもう応募されましたか?
"アプリ開発者のための旨辛豆カレー"と銘打ったモナカレー、「スパイスが効いていてやみつきになる!」と各所で評判です。
クイズにお答え頂いた方の中から抽選で300名様にプレゼントしています。皆様のご応募をお待ちしております!