👽

jsPsychで参加者のipアドレスを取得・保存する

2020/12/11に公開

はじめに

jsPsych で作った心理実験は,それを埋め込んだ html をどこかのサーバーに公開すればオンラインで時間・場所に囚われず実施することができます。とはいえ,放っておいても誰もアクセスしてくれないので,実際にはクラウドソーシングプラットフォームでリンクを共有して参加者を募ったりします。

多くの場合,ある人のデータは1つあれば十分で 2 回目以降のデータはむしろ利用できないため,同じ人は複数回実験に参加できないようにしなければなりません。基本的な対策としては,仕事(実験)の発注時にワーカーが受注できる回数を 1 回に制限するということが挙げられます(利用するプラットフォームによって設定できないかもしれません)。これによって,同一ワーカーは複数回実験に参加できなくなります。

しかし,この対策ではどうしても防げない問題があります。同一人物が複数アカウントを所有し,別個のワーカーとして実験に参加する場合です。これはあまり想定したくないことですし,ただの被害妄想かもしれません。こちらから画面の向こうの個人を特定する方法はないからです。それでもありえなくはありません。おそらくプラットフォーム上でこういった参加を防ぐことはできませんが,得られたデータを参照して同一人物がいる’どうかを判定できれば,そのデータを除外することができます。完璧ではなくても対策を用意しておくに越したことはありません。

そこで活用できるのが,ip アドレスです。ip アドレスはインターネット上の住所と表現されたりします。Wi-Fi・携帯の回線それぞれに異なった ip アドレスが割り当てられます。そのため,もし同一人物が複数アカウントで別人物として参加したとしても,同じ回線からアクセスしていればip アドレスによって同一人物だと判定することができます。

ということで,今回の記事では参加者の ip アドレスを取得・保存する方法を紹介します。保存された ip アドレスを照合することで事後的に分析から除外できます。

とはいえ,ip アドレスから住んでいる市区町村の情報は得られるので,注意して取り扱う必要があります。例えば,研究データの公開が求められる昨今ではありますが,参加者の ip アドレスは公開データに含めないほうがいいと思います。なるべく加工前のロー(寄り)データを公開するために,メインのデータとは別に参加者の ip アドレスを保存するのが良いでしょう。

そんなことよりもまず,そもそも jsPsych でどうやって実験を作成すればいいかわからないという方は, こちらにチュートリアルを作成してありますので,ぜひ参考にしてみてください。

ip アドレスを取得する

さて,前口上が長くなりましたが,ip アドレスを取得するためのコードは以下の4行です[1]

let ip = {};
fetch('https://ipinfo.io/json')
  .then((response) => response.json())
  .then((data) => (ip = data));

これで,変数 ip に ip アドレスやらなんやらが保存されています。どのような情報が保存されるのかを知りたい場合は,実際に https://ipinfo.io/json にアクセスしてみてください。他にも ip アドレスを取得するための無料のサービス(API)は色々あるので,リンク先を参考に好みのものを探してみても良いかもしれません。サービスによって取得できる情報や形式が少し異なります。

ip アドレスを保存する

基本的に,実験のメインのデータを保存するのと同じ方法を使って,別ファイルとして保存すればいいと思います(理由については後述)。jsPsych の公式リファレンスには個別のファイルとして保存する方法と MySQL データベースに保存する方法の 2 種類が紹介されていますが,私は普段,個別のファイルを保存する方法を採用しています。単純に MySQL の導入方法がよくわからなかっただけなのですが,一つの実験で 2 つ以上のファイルにデータを保存するのであれば,個別のファイルとしてデータを保存する方法のほうが楽だと思います。

コード例

コード内に出現するwrite_data.phpは公式リファレンスからコピペしてサーバー上にアップしておいてください。

<!DOCTYPE html>
<html>
  <head>
    <script src="../jspsych.js"></script>
    <script src="../plugins/jspsych-html-keyboard-response.js"></script>
    <script src="../plugins/jspsych-survey-text.js"></script>
    <link rel="stylesheet" href="../css/jspsych.css" />
  </head>
  <body></body>
  <script>
    // ipアドレスの取得
    let ip = {};
    fetch('https://ipinfo.io/json')
      .then((response) => response.json())
      .then((data) => (ip = data));

    // 参加者情報の取得
    let subjID = '';
    var ask_subjID = {
      type: 'survey-text',
      questions: [{ prompt: 'ID', columns: 3, required: true, name: 'subjID' }],
      data: { task: 'par_info' },
      on_finish: function (data) {
        subjID = JSON.parse(data.responses).subjID;
      },
    };

    // 適当なトライアル(examples/jspsych-html-keyboard-response.htmlから拝借)
    var trial_1 = {
      type: 'html-keyboard-response',
      stimulus: '<p style="color: red; font-size: 48px; font-weight: bold;">GREEN</p>',
      choices: ['y', 'n'],
      prompt: '<p>Does the color match the word? (Y or N)</p>',
    };

    // データをサーバーに保存するための関数(公式リファレンスからコピペ)
    function saveData(name, data) {
      var xhr = new XMLHttpRequest();
      xhr.open('POST', 'write_data.php'); // 'write_data.php' is the path to the php file described above.
      xhr.setRequestHeader('Content-Type', 'application/json');
      xhr.send(JSON.stringify({ filename: name, filedata: data }));
    }

    // ipアドレスの情報はjson形式で取得されるので,それをcsvに変換する関数(jspsych.jsからコピペ)
    function JSON2CSV(objArray) {
      var array = typeof objArray != 'object' ? JSON.parse(objArray) : objArray;
      var line = '';
      var result = '';
      var columns = [];

      var i = 0;
      for (var j = 0; j < array.length; j++) {
        for (var key in array[j]) {
          var keyString = key + '';
          keyString = '"' + keyString.replace(/"/g, '""') + '",';
          if (!columns.includes(key)) {
            columns[i] = key;
            line += keyString;
            i++;
          }
        }
      }

      line = line.slice(0, -1);
      result += line + '\r\n';

      for (var i = 0; i < array.length; i++) {
        var line = '';
        for (var j = 0; j < columns.length; j++) {
          var value = typeof array[i][columns[j]] === 'undefined' ? '' : array[i][columns[j]];
          var valueString = value + '';
          line += '"' + valueString.replace(/"/g, '""') + '",';
        }

        line = line.slice(0, -1);
        result += line + '\r\n';
      }

      return result;
    }

    jsPsych.init({
      timeline: [ask_subjID, trial_1],
      on_finish: function () {
        ip.subjID = subjID;
        saveData(`${subjID}_ip`, JSON2CSV([ip]));
        saveData(`${subjID}_experiment_data`, jsPsych.data.get().csv());
      },
      default_iti: 250,
    });
  </script>
</html>

JSON2CSV関数はせっかく jsPsych ライブラリ内に書いてあるんだから,jsPsych.data.JSON2CSV()って直接アクセスできたらいいんですけどね。

あと,saveData()の 1 つ目の引数で使っている${subjID}は,バッククォート`と合わせて使うことで,変数の値を代入した文字列を作成することができるという便利なやつです。

別ファイルで保存したほうが良いと思う理由
メインのデータとは別ファイルで保存したほうがいい理由は,昨今公開することが望まれる実験データにはデータ収集段階から ip アドレスを含めないほうが良いと個人的に思っているからです。ipinfo.io にアクセスすると分かる通り,ip アドレスから住んでいる地域が分かります[2]。「ズバリこの家」ということは把握できないですしその点ではインターネット利用者としても安心なのですが,念の為,公開するデータにはいれないほうが良いと思っています。もちろん,公開時に個人の特定に繋がりそうなデータを省いたデータセットを用意して公開するというのも一つの手段とは思います。ただ,公開データは(おそらく)なるべく得られた状態のローデータであるほうがよいと思われるので,できる限り,公開すべきではないデータは最初から含めないようにしておくのがいいと思います[3]

注意点

完璧に同一個人を特定することはできない

「はじめに」でも太字にしたりしてそれとなく示していましたが,ip アドレスで完璧に同一個人かどうかを判定することはできません。1 回目は家の Wi-Fi で,2 回目は携帯の回線で参加すれば,異なる ip アドレスで参加することができます。また,同じ Wi-Fi・携帯であっても,少し工夫すれば ip アドレスを変えることができます。時間が経ったら自動的に変わるということもあるようです。同一人物が複数アカウントから参加するのだとしても,願わくば,同じ ip アドレスで参加してもらいたいところですね。

アクセス回数の制限

ip アドレスを取得するなどのサービスにはアクセス回数に制限があります。ipinfo.io の場合は 月あたり 50000 回だそうです(公式サイト)。「50000 回とかアクセスしないでしょ」と思うかもしれませんが,作成した実験の細かい調整を繰り返していると上限を超えてしまうかもしれません。ip アドレスを取得しないと動作しない実験ではない限りは,一通り実験が完成してから,ip アドレスを取得するコードを足すのが良いでしょう。

非同期処理

今回紹介しているfetchの処理は非同期で行われます。基本的にjavascriptの処理は書いてある順に実行され,前の処理が終了するまで次の処理は実行されません。しかし,処理が非同期で実行された場合,その終了を待たずに次の処理が実行されます。そのため,ip アドレスを取得するコードの直後に保存のコードを書いたとしてもうまくいかない可能性が高いです。ip アドレスが取得できてないまま保存の処理が始まってしまって、結局何も保存されないからです。

良くない例
jsPsych.init({
  timeline: [ask_subjID, trial_1],
  on_finish: function () {
    let ip = {};
    fetch('https://ipinfo.io/json')
      .then((response) => response.json())
      .then((data) => (ip = data));
    ip.subjID = subjID;

    // fetchの一連の処理が終わる前に以下の処理が実行される
    saveData('subject_ip', JSON2CSV([ip]));
    saveData('experiment_data', jsPsych.data.get().csv());
  },
  default_iti: 250,
});

実は,saveData()内の処理も非同期で実行されています。何人かの同僚からデータが上手く保存されなかった例をきいたことがありますが,jsPsych.initon_finishで非同期のsaveData()を実行するしていることが原因なのかもしれません。色々対策方法はありますが,私は実験終了時ではなく,一番最後の「参加いただきありがとうございました」と表示するページで保存するようにしています。

おわりに

今回は ip アドレスを取得・保存する方法を紹介しました。個人的に気をつけたほうが良いと思う点が色々あったので,説明が余分に多くなってしまった気がします。そもそも同一人物の複数アカウントから参加などはないと信じるのが良いのかもしれません。

不定期にはなりますが,今後も引き続き心理学実験・研究法に関する Tips を共有していきます。いいね・サポートをいただけると大変励みになりますので,ぜひそちらもよろしくお願いします。

脚注
  1. 本当はエラー処理もあったほうが良いと思うのですが,テスト時に自分でエラーを発生させるほう法がわからずエラー処理のテストができなったので,今回は紹介していません。そのうち分かったら記事を修正するかもしれません。 ↩︎

  2. 家の回線だとそうなのですが,携帯回線とかポケット Wi-Fi だと契約している会社の基地局がありそうな場所が取得されたります。 ↩︎

  3. 神経質になりすぎて実験を実施できないというのが一番良くないので,できる範囲で実践すればと思います。取得時点で ip アドレスをメインのデータに保存していたが,公開データセットから省いたというのであれば,その旨を論文に記載しておけば大丈夫だと思います。 ↩︎

Discussion