The One with ...

授業・研究用のメモなどを公開しています

相対的剥奪のオンライン実験用コード

はじめに

とある原稿を書いているうちに,昔作ったztreeトリートメントをアップデートしたい気持ちがふつふつと高まってきたので,otreeで新たに実験用コードを作成しました.以下忘れた時用のメモです.

otree準備

  • アナコンダの最新版をインストール
  • pip install -U otreeでotreeをインストールする

こちらのサイトがたいへん参考になりました.

akrgt.gitbook.io

作業用ディレクトリの設定

異なるマシンで作業ができるようにdropbox上にフォルダを作ります.

たとえばC:\Users\Dropboxにドロップボックスのローカルアドレスがあれば

otree startproject C:\Users\Dropbox\oTree

という感じでホームディレクトリをつくってあげるとよいでしょう.

はじめてディレクトリをつくるとサンプルコードもdlするかと聞かれるので,これはdlしておきましょう.

今回作ったようなトリートメントはcournotゲームをベースにして作ると簡単でした.

サンプルを探せば自分がやりたい実験に似たコードがあるはずなので,まずはそれをベースに少しずつ修正を加えると作業が楽でしょう (フルスクラッチでいきなり書くのは難しそう)

以降はマシンを変える度にdb.sqlite3を消してから,テスト用のローカルサーバーを otree devserverでたてればOK.

アプリの準備

サンプルコードにcournotゲームのフォルダがあるのでコピーしてリネームしましょう. 相対剥奪なので,relative_deprivationという名前にします.

このフォルダには主に

  • __init__.py
  • Contribute.html
  • Results.html

の三つのファイルがあります.

この中身を適当に修正して実験コードを作成します.一つ上の階層

C:\Users\Dropbox\oTreeには

  • settings.py

というファイルがあります.この中の

SESSION_CONFIGS = [
    dict(
        name='guess_two_thirds',
        display_name="Guess 2/3 of the Average",
        app_sequence=['guess_two_thirds', 'payment_info'],
        num_demo_participants=3,
    ),
    dict(
        name='survey', app_sequence=['survey', 'payment_info'], num_demo_participants=1
    )]

に自分が作成したrelative_deprivation'を登録します.guess_two_thirdssurveyはデフォルトのdemoです.

結果は次のようなものになるでしょう.

SESSION_CONFIGS = [
    dict(
        name='guess_two_thirds',
        display_name="Guess 2/3 of the Average",
        app_sequence=['guess_two_thirds', 'payment_info'],
        num_demo_participants=3,
    ),
    dict(
        name='survey', app_sequence=['survey', 'payment_info'], num_demo_participants=1
    ),
    dict(
        name='relative_deprivation', app_sequence=['relative_deprivation', 'payment_info'], num_demo_participants=5
    )
]

settings.pyを保存したらotree devserverでテスト用サーバを立ち上げます.demoの中に自分が登録した実験(relative_deprivation)があれば成功です.

コードの中身

アプリを構成するコードの中身はこんな感じです

__init__.py

クラスの定義や計算用関数を定義するpythonコードです.

pythonになじみのある人は,なんとなく何をやっているか分かると思います.詳細は後ほど解説します.

from otree.api import *
import random


class C(BaseConstants):
    NAME_IN_URL = 'relative_deprivation'
    PLAYERS_PER_GROUP = 5
    NUM_ROUNDS = 1
    RETURN_RATE = random.randint(3,5)
    NUM_SUCCESS = random.randint(1,5)
    PROFIT = RETURN_RATE*100-100 #COST=100


class Subsession(BaseSubsession):
    pass


class Group(BaseGroup):
    x = models.IntegerField()

class Player(BasePlayer):
    action = models.IntegerField(
        choices=[[0, 'no'],[1,'yes (invest 100)']],
        label="Do you invest? ",initial=(0)
    )
    outcome = models.IntegerField(
        choices=[[5, 'very satisfied'],[4, 'satisfied'],[3, 'neither'],[2, 'dissatisfied'], [1, 'very dissatisfied']],
        label='How do you feel about the result?',
        widget=widgets.RadioSelect,
    )
    profit = models.IntegerField()


# FUNCTIONS
def set_profit(group: Group):#グループオブジェクトを引数にした関数
    players = group.get_players()#playerを取得
    group.x = sum([p.action for p in players])#投資者数の総数xを計算
    xids=[]#投資者のplayeridを格納するリスト
    for p in players:
        if p.action==1:
            xids.append(p.id_in_group)#投資者のplayeridを全てリストに追加
    #投資者数が多い場合の抽選
    if C.NUM_SUCCESS < group.x:
        win=random.sample(xids,C.NUM_SUCCESS)#NUM_SUCCESSだけxidsから勝者を選択
    for p in players:
        if p.action == 0:
            p.profit=0 # 非投資者の利得を計算
        else:
            if C.NUM_SUCCESS >= group.x:
                p.profit= 100*(C.RETURN_RATE*p.action-p.action)
            else:
                if p.id_in_group in win:
                    p.profit= 100*(C.RETURN_RATE*p.action-p.action)
                else:
                    p.profit= -100

# PAGES
class Contribute(Page):
    form_model = 'player'
    form_fields = ['action']


class ResultsWaitPage(WaitPage):
    after_all_players_arrive = set_profit


class Results(Page):
    form_model = 'player'
    form_fields = ['outcome']


page_sequence = [Contribute, ResultsWaitPage, Results]

Contribute.html

つぎは入力画面のhtmlです.

htmlタグ直書き世代の自分にとっては懐かしいコードです(わしらの若い頃は,これを手打ちして「ホームページ」をつくっておったのじゃよ).

ちなみに{{C.PROFIT}}のように変数名C.PROFITを囲むと,pythonで定義した変数をhtml上に表示させることができます.

{{ extends "global/Page.html" }}
{{ block title }}Contribute{{ endblock }}
{{ block content }}

    <p>
        You can choose "invest" or "not invest".
    </p>

    <p>
        {{C.NUM_SUCCESS}} players among those who invest will get a profit.
    </p>

 <p>
     You will get 
 <ul>
     <strong>{{C.PROFIT}}, when you win.</strong>
 </ul>
 <ul>
     <strong>- 100 , when you lose.</strong>
 </ul>
 <ul>
     <strong>0 , when you do not invest.</strong>
 </ul>


    {{ formfields }}

    {{ next_button }}

{{ endblock }}

Results.html

最後は結果表示と結果に対する満足度を聞くフォームを表示するhtmlです.

{{ block title }}Results{{ endblock }}
{{ block content }}

    <p>
       Your profit is {{ player.profit }}.
    </p>

    <p>
        Please answer the following questions.
    </p>

    {{ formfields }}


    {{ next_button }}

{{ endblock }}

実験の概要

相対的剥奪実験の具体例は以下の通りです.

  • 被験者は100円投資or投資しないを選択できます
  • 投資をえらんだ人は成功すれば300円もらえますが,失敗すると何ももらえません.
  • 成功人数は2名です.投資した人の数が2名を超えると,成功者の2名以外は100円失います

セッションによって利益率Rと成功者数nが変化します.一般的には

  • 被験者は100円投資or投資しないを選択できます
  • 投資をえらんだ人は成功すれば100*R円もらえますが,失敗すると何ももらえません.
  • 成功人数はn名です.投資した人の数がn名を超えると,成功者のn名以外は100円失います

というル-ルです

実験の流れ

直感的には実験は次のような順番で進行します.

  1. 実験用URLにアクセスしたプレイヤーにContribute.htmlが表示され,プレイヤーは行動(投資するか否か)を選択します
  2. 全プレイヤーが選択を終えると __init__.pyで定義した関数(set_profit)がプレイヤーの利得を計算します
  3. Result.htmlが表示され,各プレイヤーが受け取った利得がその中に表示されます.
  4. Result.htmlの中に満足度を入力するフォームが表示されます.プレイヤーが満足度を選択すると実験は終了です
  5. 結果が自動でデータベースに記録されます.

コードの概要

otreeでは基本変数と関数の定義を全て,__init__.py内に記述します.htmlはプレイヤーからの入力の受け取りや,計算結果の表示を担当します.

otreeでは実験用に基本的なクラスである

  • class C(BaseConstants): グローバル変数を定義するクラス
  • class Subsession(BaseSubsession): セッションを定義するクラス
  • class Group(BaseGroup): グループ属性を定義するクラス
  • class Player(BasePlayer): プレイヤー属性を定義するクラス

などがあらかじめ定義されています.

それぞれ簡単に内容を確認します.

class C(BaseConstants):
    NAME_IN_URL = 'relative_deprivation' #実験の名称です
    PLAYERS_PER_GROUP = 5 #被験者数です
    NUM_ROUNDS = 1 #ラウンド数です
    RETURN_RATE = random.randint(3,5) #利益率です
    NUM_SUCCESS = random.randint(1,5) #成功者数です
    PROFIT = RETURN_RATE*100-100 #COST=100 #成功時の利得です

以上がグローバル変数です.ラウンドごとに水準を変化させる場合は, 利益率などをsessionクラスで定義します.

class Group(BaseGroup):
    x = models.IntegerField()

グループの属性です.被験者集団全体で決まる値をここで属性として定義します. 属性xは投資者数を記録します.この値は被験者の行動によって変化するので,グローバル変数としては定義できないことに注意します.

class Player(BasePlayer):
    action = models.IntegerField(
        choices=[[0, 'no'],[1,'yes (invest 100)']],
        label="Do you invest? ",initial=(0)
    )
    outcome = models.IntegerField(
        choices=[[5, 'very satisfied'],[4, 'satisfied'],[3, 'neither'],[2, 'dissatisfied'], [1, 'very dissatisfied']],
        label='How do you feel about the result?',
        widget=widgets.RadioSelect,
    )
    profit = models.IntegerField()

プレイヤーのクラスです.ここでは

  • action #行動
  • outcome #結果への評価(満足度)
  • profit #受け取る利得

の3属性を定義します.idのような基本属性はもともと定義されているので,呼び出すだけで使えます.

def set_profit(group: Group):#グループオブジェクトを引数にした関数
    players = group.get_players()#playerを取得
    group.x = sum([p.action for p in players])#投資者数の総数xを計算
    xids=[]#投資者のplayeridを格納するリスト
    for p in players:
        if p.action==1:
            xids.append(p.id_in_group)#投資者のplayeridを全てリストに追加
    #投資者数が多い場合の抽選
    if C.NUM_SUCCESS < group.x:
        win=random.sample(xids,C.NUM_SUCCESS)#NUM_SUCCESSだけxidsから勝者を選択
    for p in players:
        if p.action == 0:
            p.profit=0 # 非投資者の利得を計算
        else:
            if C.NUM_SUCCESS > group.x:
                p.profit= 100*(C.RETURN_RATE*p.action-p.action)
            else:
                if p.id_in_group in win:
                    p.profit= 100*(C.RETURN_RATE*p.action-p.action)
                else:
                    p.profit= -100

利得計算関数です.本実験の肝の部分です.投資者数xnで設定した成功者数を下回る場合は,投資者全員が成功し, n<xの場合はランダムに成功者が決まるようにrandom.sample(xids,C.NUM_SUCCESS)で計算しています. そのため,投資選択者のidリストをxids.append(p.id_in_group)で作ります.このときotreeのプレイヤーのデフォルト属性であるid_in_group を利用します.

このようにotreeにはクラス毎によく利用する属性が準備されています.

# PAGES
class Contribute(Page):
    form_model = 'player'
    form_fields = ['action']


class ResultsWaitPage(WaitPage):
    after_all_players_arrive = set_profit


class Results(Page):
    form_model = 'player'
    form_fields = ['outcome']


page_sequence = [Contribute, ResultsWaitPage, Results]

htmlのクラスです.フォームからの入力などを定義します. 入力の受け取りにはプレイヤークラスで定義した属性をform_fieldsで指定します. 回答選択肢などは,プレイヤークラスの方で定義しています.




まとめ

otreeで相対的剥奪オンライン実験用コードを作成しました.

ポイントは

  • クールノーゲームのようにベースとなる実験を見つけ,それを上書き修正する
  • クラス毎に定義すべき属性を理解する

です.ztreeに比べるとかなりコードが書きやすくなった印象です. pythonの基本とhtmlのタグを知っていれば,簡単な実験なら2~3時間くらいで書けると思います.