【第6回】 2022年6月16日
oTree の組み込み関数の使い方
組み込み関数の使い方については,公式ドキュメントで関数名を検索してください.
- たとえば,
is_displayedで検索.
- たとえば,
oTree Hub の Example code で(ブラウザの検索機能を使って)検索して使用例を見るのも有用です.
逐次手番ゲーム(信頼ゲーム)
otree startprojectコマンド実行で手に入るサンプルゲームの「trust」アプリ.
意思決定データを Group に記録する
信頼ゲームでは役割(先手と後手)で行う意思決定が異なり,group でユニークな値である.
- 先手は後手に預けるポイント数
sent_amountを決定する. - 後手は先手に返すポイント数
sent_back_amountを決定する.
- 先手は後手に預けるポイント数
player のフィールドとして定義するより,group のフィールドとして定義した方が良い.
- テンプレートで
sent_amountを展開するとき...- group のフィールドであれば
{{ group.sent_amount }}. - player のフィールドであれば
vars_for_template()でplayer.group.get_player_by_id(1).sent_amountを何らかの変数として渡す必要がある.- テンプレートで直接
{{ group.get_player_by_id(1).sent_amount }}としたいところだが,エラーとなる. - 先手のみに表示する部分であれば
{{ player.sent_amount }}でよいが,後手には使えない.
- テンプレートで直接
- group のフィールドであれば
- テンプレートで
あえて信頼ゲームの意思決定データを Player に保存するにはどうすれば良いか,と質問を受けました.
たとえば
sent_amountとsent_back_amountをPlayerクラスで定義する場合,__init__.pyはたとえば以下のように書く.__init__.pyfrom otree.api import * class C(BaseConstants): NAME_IN_URL = 'trust' PLAYERS_PER_GROUP = 2 NUM_ROUNDS = 1 INSTRUCTIONS_TEMPLATE = 'trust/instructions.html' ENDOWMENT = cu(100) MULTIPLIER = 3 class Subsession(BaseSubsession): pass class Group(BaseGroup): pass class Player(BasePlayer): sent_amount = models.CurrencyField( min=0, max=C.ENDOWMENT, doc="""Amount sent by P1""", label="Please enter an amount from 0 to 100:" ) sent_back_amount = models.CurrencyField( min=cu(0), doc="""Amount sent back by P2""" ) def sent_back_amount_max(player: Player): """ 引数が group ではなく player であることに注意! """ group: Group = player.group ## ← 「: Group」 は消しても良い return group.get_player_by_id(1).sent_amount * C.MULTIPLIER def set_payoffs(group: Group): p1: Player = group.get_player_by_id(1) ## ← 「: Player」 は消しても良い p2: Player = group.get_player_by_id(2) ## ← 「: Player」 は消しても良い p1.payoff = C.ENDOWMENT - p1.sent_amount + p2.sent_back_amount p2.payoff = p1.sent_amount * C.MULTIPLIER - p2.sent_back_amount class Introduction(Page): pass class Send(Page): form_model = 'player' form_fields = ['sent_amount'] @staticmethod def is_displayed(player: Player): return player.id_in_group == 1 class SendBackWaitPage(WaitPage): pass class SendBack(Page): form_model = 'player' form_fields = ['sent_back_amount'] @staticmethod def is_displayed(player: Player): return player.id_in_group == 2 @staticmethod def vars_for_template(player: Player): group: Group = player.group p1: Player = group.get_player_by_id(1) tmp_sent_amount = p1.sent_amount return dict( tmp_sent_amount = tmp_sent_amount, ## ← {{ group.sent_amount }} を {{ tmp_sent_amount }} に置換せよ tripled_amount = tmp_sent_amount * C.MULTIPLIER ) class ResultsWaitPage(WaitPage): after_all_players_arrive = set_payoffs class Results(Page): @staticmethod def vars_for_template(player: Player): group: Group = player.group p1: Player = group.get_player_by_id(1) p2: Player = group.get_player_by_id(2) ## ↑ group を定義せず,いちいち ## p1 = player.group.get_player_by_id(1) ## p2 = player.group.get_player_by_id(2) ## とすると,パフォーマンスが低下する. ## https://otree.readthedocs.io/en/latest/misc/tips_and_tricks.html#improving-code-performance tmp_sent_amount = p1.sent_amount tmp_sent_back_amount = p2.sent_back_amount return dict( tmp_sent_amount = tmp_sent_amount, ## ← {{ group.sent_amount }} を {{ tmp_sent_amount }} に置換せよ tmp_sent_back_amount = tmp_sent_back_amount, ## ← {{ group.sent_back_amount }} を {{ tmp_sent_back_amount }} に置換せよ tripled_amount = tmp_sent_amount * C.MULTIPLIER ) page_sequence = [ Introduction, Send, SendBackWaitPage, SendBack, ResultsWaitPage, Results, ]テンプレートでは,自分以外の特定のプレイヤーのフィールドを直接展開することができないため,
vars_for_template()を使ってテンプレートに変数を渡す必要がある.この手間がデメリットといえばデメリット.sent_amountとsent_back_amountをPlayerクラスで定義する場合,出力されるCSVファイルの1行(あるプレイヤーのデータ)には,先手プレイヤーの場合にsent_amount列は数値が入っているが,sent_asent_back_amountmount列は空欄(None)となっている.後手プレイヤーの場合にはsent_asent_back_amountmount列は数値が入っているが,sent_amount列は空欄となっている.- 実験実施だけに注目すれば(変数を呼び出すのが少々面倒くさいだけで)問題ないが,実験データの分析のことを考慮すると,1行に当該プレイヤーの意思決定と相手プレイヤーの意思決定の両方が入っている方が便利かも.
Playerクラスにフィールドを定義して入力フォームを実装し, oTree サーバー側で(たとえばbefore_next_page()を使って)Groupクラスのフィールドに "転記" する方法も一案.__init__.pyfrom otree.api import * class C(BaseConstants): NAME_IN_URL = 'trust' PLAYERS_PER_GROUP = 2 NUM_ROUNDS = 1 INSTRUCTIONS_TEMPLATE = 'trust/instructions.html' ENDOWMENT = cu(100) MULTIPLIER = 3 class Subsession(BaseSubsession): pass class Group(BaseGroup): sent_amount = models.CurrencyField() sent_back_amount = models.CurrencyField() class Player(BasePlayer): p_sent_amount = models.CurrencyField( min=0, max=C.ENDOWMENT, doc="""Amount sent by P1""", label="Please enter an amount from 0 to 100:" ) p_sent_back_amount = models.CurrencyField( min=cu(0), doc="""Amount sent back by P2""" ) def p_sent_back_amount_max(player: Player): """ 引数が group ではなく player であることに注意! """ group: Group = player.group ## ← 「: Group」 は消しても良い return group.sent_amount * C.MULTIPLIER def set_payoffs(group: Group): p1: Player = group.get_player_by_id(1) ## ← 「: Player」 は消しても良い p2: Player = group.get_player_by_id(2) ## ← 「: Player」 は消しても良い p1.payoff = C.ENDOWMENT - group.sent_amount + group.sent_back_amount p2.payoff = group.sent_amount * C.MULTIPLIER - group.sent_back_amount class Introduction(Page): pass class Send(Page): form_model = 'player' form_fields = ['p_sent_amount'] @staticmethod def is_displayed(player: Player): return player.id_in_group == 1 @staticmethod def before_next_page(player: Player, timeout_happened): group: Group = player.group if player.id_in_group == 1: """ is_displayed() によって id_in_group != 1 なるプレイヤーはスキップされ, 後手プレイヤーにはこの before_next_page() は実行されない. したがって,このif文は必ず True で通るはずである. (必ず True になるのだったら,if文を噛まさなくてもいいかも.) """ group.sent_amount = player.p_sent_amount class SendBackWaitPage(WaitPage): pass class SendBack(Page): form_model = 'player' form_fields = ['p_sent_back_amount'] @staticmethod def is_displayed(player: Player): return player.id_in_group == 2 @staticmethod def vars_for_template(player: Player): group: Group = player.group return dict( tripled_amount = group.sent_amount * C.MULTIPLIER ) @staticmethod def before_next_page(player: Player, timeout_happened): group: Group = player.group if player.id_in_group == 2: """ is_displayed() によって id_in_group != 2 なるプレイヤーはスキップされ, 先手プレイヤーにはこの before_next_page() は実行されない. """ group.sent_back_amount = player.p_sent_back_amount class ResultsWaitPage(WaitPage): after_all_players_arrive = set_payoffs class Results(Page): @staticmethod def vars_for_template(player: Player): group: Group = player.group return dict( tripled_amount = group.sent_amount * C.MULTIPLIER ) page_sequence = [ Introduction, Send, SendBackWaitPage, SendBack, ResultsWaitPage, Results, ]
ページ表示をスキップする
ページクラスの組み込みメソッド
is_displayed()を定義すれば,返り値で渡す真偽値でページを表示するか否かを設定できる.ラウンド数(
player.round_number)やプレイヤーの役割(player.id_in_group)で条件分岐させることが多い.- 複数ラウンドを設定していて,アプリ内にインストラクションページが含まれるとき,インストラクションは最初の1回だけ表示するには,
is_displayed()でplayer.round_number == 1を返せばよい.
- 複数ラウンドを設定していて,アプリ内にインストラクションページが含まれるとき,インストラクションは最初の1回だけ表示するには,
https://otree.readthedocs.io/en/latest/pages.html#is-displayed
ページクラスの組み込みメソッド
app_after_this_page()で返り値をアプリ名とすれば,返り値のアプリまでスキップされる.ページクラスの組み込みメソッド
get_timeout_seconds()で返り値を0とすれば,0秒で自動的にページが遷移するため,ページをスキップさせる手段として使えなくもない.ただし一瞬はページが表示されることに注意.
逐次手番ゲームでの is_displayed() の使い方
信頼ゲームでは役割(先手と後手)で行う意思決定が異なる.それぞれを別のページとして実装する.
- 先手が後手に預けるポイント数を入力するページとして
Send. - 後手が,先手から受け取ったポイント数のうち先手に返すポイント数を入力するページとして
SendBack. - ゲームの結果を表示するページとして
Results.
- 先手が後手に預けるポイント数を入力するページとして
is_displayed()を使って,Sendページを先手だけに,SendBackページを後手だけに表示する.is_displayed()の引数はplayerオブジェクト.返り値がTrueのとき,その player に表示される.- group 内の役割は
player.id_in_groupの値で定義する(定数Cクラスで*_ROLEを定義して役割のラベルを設定しても良い).player.id_in_group == 1なる player を先手,player.id_in_group == 2なる player を後手とする.Sendクラスにおいて,is_displayed()を定義し,返り値をplayer.id_in_group == 1(の真偽値)とする.SendBackクラスにおいて,is_displayed()を定義し,返り値をplayer.id_in_group == 2(の真偽値)とする.
ページ順を
page_sequence = [Send, SendBack, Results]と設定しているとき...- 先手が
Sendページで意思決定している間,後手にはSendページが表示されず,次のSendBackページが表示されてしまう.SendBackページにおいてSendページにおける先手の意思決定の結果を表示させるような実装をしている場合,意図しない挙動となるかエラーが出る. - 後手が
SendBackページで意思決定している間,先手にはResultsページが表示されてしまう.後手の意思決定が終わっていなければ,当然利得の計算もまだ行われていないため,Resultsページで利得を表示することはできない.
- 先手が
一方のプレイヤーが意思決定している間,他方のプレイヤーは待機ページで待機する必要がある.
- 先手が
Sendページで意思決定している間,後手にはSendBackWaitPageなる待機ページを表示する.先手の意思決定が終わったタイミングで特に行うべき処理は無いため,SendBackWaitPageクラスは定義するだけで中身はpassとだけ書いておけば良い. - 後手が
SendBackページで意思決定している間,先手にはResultsWaitPageなる待機ページを表示する.後手の意思決定が終わったタイミングで利得を計算するため,after_all_players_arriveを定義する.
- 先手が
フィールド名_max() で入力フォーム検証の条件を動的に設定する
信頼ゲームで後手が行う意思決定(先手に返すポイント数:
sent_back_amount)の上限は,先手が預けたポイント数(を何倍かしたもの).先手の意思決定によって,後手の意思決定の上限が変動する.
モジュールレベルで
sent_back_amount_max()を定義して,返り値をsent_back_amountの最大値とする.
有限回繰り返しゲーム(マッチングペニー)
otree startprojectコマンド実行で手に入るサンプルゲームの「matching_pennies」アプリ.ソースコード https://github.com/oTree-org/oTree/tree/lite/matching_pennies
デモページ https://otree-demo.herokuapp.com/demo/matching_pennies
creating_session() を使ったプレイヤーのシャッフル
モジュールレベルで組み込み関数
creating_session()を定義すると,subsession の最初のページが表示されるタイミングで group の編成を定義できる.一つのアプリを何回も繰り返す設定にしている場合(
NUM_ROUNDSが 1よりも大きい場合),繰り返す度に(つまり subsession ごとに) group 編成を変更することができる.subsession の途中で group 編成を変更することはできない.
- ただし, subsession の途中でも, group 内での役割(
id_in_group)は group のset_players()メソッドで変更できる.
- ただし, subsession の途中でも, group 内での役割(
creating_session()はセッションを作成するタイミングで実行されるので,creating_session()では意思決定に応じて group 編成を変更することはできない.- (待機ページクラスで
group_by_arrival_time = Trueとした上で)group_by_arrival_time_method()を使えば,そこで柔軟に group 編成を定義できる.
- (待機ページクラスで
group の編成は subsession のメソッド
set_group_matrix()で設定できる.引数に2次元配列で記述した新しい group 編成を渡す.- z-Tree とは異なり absolute stranger マッチングは実装されていないので,自分で実装する.
https://otree.readthedocs.io/en/latest/multiplayer/groups.html#group-matching
(Python 標準機能としての)リストの操作は以下を参照.
https://docs.python.org/3.9/library/stdtypes.html#mutable-sequence-types- 作例中で使っている
.reverse()メソッドはリスト自体を破壊的に変更(逆転)する..reverse()自体の返り値はNoneであることに注意. (雑談) R と Python の挙動の違いに注意!
R ではデータをいじると,勝手にコピーしたものを変えてくれる.
a <- c(0:4) print(a) # [1] 0 1 2 3 4 b <- a b[3] <- 999 print(b) # [1] 0 1 999 3 4 print(a) # [1] 0 1 2 3 4一方で,Pythonはコピーせず,もとのデータも変えてくれる.
a = list(range(5)) print(a) # [0, 1, 2, 3, 4] b = a b[2] = 999 print(b) # [0, 1, 999, 3, 4] print(a) # [0, 1, 999, 3, 4]
- 作例中で使っている
【Tips】 関数内でインポートしてよいか?
oTree の公式ドキュメントでは,パッケージやモジュールをインポートするとき,関数定義の中で
importしていることが多い.PEP (Python Enhancement Proposal) では以下のように説明されている.
Imports are always put at the top of the file, just after any module comments and docstrings, and before module globals and constants.
- https://peps.python.org/pep-0008/#imports
- 原則は
.pyファイルの冒頭でimportしなければならない.
ただし,
otree startprojectで入るサンプルゲームでの実装から分かるように,関数定義の中でimportしても(ただし関数の中でのみ)モジュールの関数が使える.名前が衝突してしまう場合には,やむを得ず関数定義の中で
importして衝突を回避する.冒頭で
importすると,具体的にどの箇所でモジュールの関数を使っているのかがわかりにくくなってしまう.だから,モジュールの関数を使いたい場所の直前でimportしたい.- そのような動機の場合,自分でモジュールを作り(つまり,関数定義を別の
.pyファイルに記述し),その中でimportすれば良い.
- そのような動機の場合,自分でモジュールを作り(つまり,関数定義を別の
意思決定画面で時間制限を設定してみる
ページクラスで変数
timeout_secondsに整数を定義すると,そのtimeout_seconds秒の時間制限を設定できる.動的に時間制限を設定するには,ページクラスの組み込みメソッド
get_timeout_seconds()を定義し,返り値を残り秒数とする.デフォルトでは,画面に「このページでの残り時間 mm:ss」と表示される.
タイムアウトするとページのフォームが自動送信される.
- どんな値が自動送信されても,エラーが表示されることなく次のページへ遷移する.
- 入力フォームに数値などが途中まで入力してあって放置されている状態のとき,タイムアウトの時点で入力されていた値が送信される.
- 値として適切である場合のみ(たとえば
models.FloatFieldとしてある入力フォームに文字列ではなく数値が入力されている場合など)ちゃんと記録される. - 入力フォームが空の場合,あるいは値として不適切な場合,記録されるデータは,(
initialが設定されていなければ)BooleanFieldならFalse,IntegerFieldやFloatFieldなら0,StringFieldなら"". - 自動送信されて記録された値を変更したい場合は,ページクラスの組み込みメソッド
before_next_page()の中でtimeout_happened == Trueなる player に対し処理を行う. otree prodserverでサーバーを起動している場合のみ,クライアントがブラウザーを閉じているときにタイムアウトが発生すると,クライアントの代わりに,サーバーが自分宛てにフォームを送信する.
「Advance slowest user(s)」ボタンで次のページへ遷移させた場合も,当該ページでタイムアウトしたのと同じ処理が行われる.
- ただし,サーバーが自分宛てにフォームを送信するので,クライアントで入力フォームに何か入力されていても,その値を取得することはできない.
【Tips】 「 @staticmethod 」は何か?
関数デコレータで,すぐ下で定義している関数をスタティックメソッドに変換するもの.
純粋な Python の機能の説明...
- 用語:
- 「メソッド」: クラスの中で定義した関数.
- 「インスタンス(オブジェクト)」: (操作的な定義)
Kurasuという名前のクラスが定義してあるとき,Kurasu()はインスタンスオブジェクト.- oTree でよく出てくる
playerやgroupはインスタンスオブジェクトが代入されたもの.つまりplayer = Player(),group = Group().
- oTree でよく出てくる
たとえば,以下のようなクラスが定義してあるとする.
class Kurasu: mytext = "こんにちは" def testfunc1(self, nanika): print(self.mytext) print(nanika)関数
testfunc1()をKurasuクラスのインスタンス(Kurasu())のメソッドとして呼び出すとき,第1引数に自身のインスタンスオブジェクト(self = Kurasu())を受け取った状態の関数になっており(「部分適用」と呼ばれる),第2引数のnanikaだけ渡せば良い.- たとえば,
とすると,「こんにちは」と「あいうえお」が出力される.kurasu = Kurasu() kurasu.testfunc1("あいうえお") # こんにちは # あいうえお testfunc1()の引数に,自分ではselfとして何のオブジェクトも渡しておらず,nanikaだけしか渡していないのに,ちゃんと動いていることに注目されたい.
- たとえば,
関数
testfunc1()をクラスから直接呼び出すとき,testfunc1()の定義通り,ちゃんと第1引数にselfとして自身のインスタンスオブジェクト,第2引数にnanikaを渡さなければならない.- たとえば,先ほどと同様,引数
nanikaだけしか渡さない場合,とエラーが出る.Kurasu.testfunc1("あいうえお") # TypeError: testfunc1() missing 1 required positional argument: 'nanika'nanikaに渡したつもりの文字列がselfに取られ,もう一つの引数が足らない状態である. - ちゃんと引数としてインスタンスオブジェクトと
nanikaの2つを渡してやると上手くいく.kurasu = Kurasu() Kurasu.testfunc1(kurasu, "あいうえお") # こんにちは # あいうえお
- たとえば,先ほどと同様,引数
@staticmethodがついた関数testfunc2()でKurasuクラスのインスタンス(Kurasu())のメソッドとして呼び出すとき,関数の定義の通り,引数はnanikaだけで渡せば良い.class Kurasu: mytext = "こんにちは" @staticmethod def testfunc2(nanika): print(nanika) kurasu = Kurasu() kurasu.testfunc2("かきくけこ") # かきくけことすると,「かきくけこ」が出力される.
- なお,関数の定義で
selfを引数に取っていないので,testfunc2()の中ではselfを使えない(self.mytextで値を受け取れない). 関数
testfunc2()の定義の直前に書いてある@staticmethodを消した上で,testfunc2()をインスタンスメソッドとして呼び出すと,エラーが出る.class Kurasu: def testfunc2(nanika): print(nanika) kurasu = Kurasu() kurasu.testfunc2("かきくけこ") # TypeError: testfunc2() takes 1 positional argument but 2 were given- 曰く「
testfunc2()は引数を1つ(nanika)しか取らない関数のはずなのに,2個渡されましたけど,どういうこと?」と. - この場合,
testfunc2()がインスタンスメソッドとして呼び出されているので,第1引数として暗黙のうちにインスタンスオブジェクトKurasu()が渡されている. - しかし,
testfunc2()を定義するときにnanikaしか受け取らない,ということにしていたので,エラーが出た. 関数
testfunc2()をクラスから直接呼び出すとき,@staticmethodがついているか否かによらず,(Kurasu.testfunc1()の場合と同様に)testfunc2()の定義で記述した引数をすべて渡さなければならない.@staticmethodがついている場合.class Kurasu: @staticmethod def testfunc2(nanika): print(nanika) Kurasu.testfunc2("かきくけこ") # かきくけこ@staticmethodがついていない場合.class Kurasu: def testfunc2(nanika): print(nanika) Kurasu.testfunc2("かきくけこ") # かきくけこ
- 関数
testfunc2()をクラスから直接呼び出すとき,@staticmethodがついているか否かによらず,インスタンスメソッドとして呼び出したとき(kurasu.testfunc2()のとき)と同じ挙動(引数nanikaだけ渡せば良い)となる.
- 用語:
クラスの中で定義する関数で,自身のインスタンスオブジェクト(
self)を第1引数として受け取りたくない場合,@staticmethodをつけなければならない.- oTree で,たとえばページクラスの中で
is_displayed()を定義するとき,ページクラス自身のインスタンスオブジェクトは不要であり,別クラスであるPlayerの インスタンスオブジェクトplayerを引数に取る( oTree 本体はis_displayed()なる関数がplayerのみを引数に取る,ということを前提としてコードが書かれている). - oTree 5 で組み込み関数(メソッド)はもはや
selfを引数にとる必要がなくなったため,@staticmethodをつけるべきである.
- oTree で,たとえばページクラスの中で
ところが,実のところ oTree は,ページクラスで組み込みのメソッドを定義する際に
@staticmethodをつけなくても,エラーは出ず普通に動いてしまう.なぜ?oTree 本体は,たとえば
MyPageクラスで定義したis_displayed()を呼び出すとき,getattr(MyPage, "is_displayed")(Player())として呼び出している.これは,
MyPage.is_displayed(Player())と等価である.
- つまり, oTree の本体は
is_displayed()をインスタンスのメソッドとしてではなく,クラスから直接呼び出していて,引数はplayer = Player()のみを渡している. - ページクラスの中で
@staticmethodをつけて定義した関数は,クラスから直接呼び出されても,あるいは仮にインスタンスのメソッドとして呼び出されたとしても,playerオブジェクトだけを受け取って想定した挙動をしてくれる. - ページクラスの中で
@staticmethodをつけずに定義した関数をクラスから直接呼び出すとき,通常は第1引数に当該クラスのインスタンスオブジェクト(self = MyPage())を渡さないといけないが, oTree 本体は有無を言わさずplayer = Player()を渡す.引数にselfを受け取って処理を行うような関数の定義をしていればエラーとなるが,公式ドキュメントの指示通り (selfではなく)playerオブジェクトを受け取るように定義していれば,想定した挙動をしてくれる.
つまり,関数を呼び出すときに
@staticmethodの有無で挙動が変わるような呼び出し方をしていないため,@staticmethodをつけなくても,エラーが出ずに動く.では,
@staticmethodはつけなくても良いか?- 公式ドキュメントではつけなくても良いと言っている.
https://otree.readthedocs.io/en/latest/install-nostudio.html#about-staticmethod-etc - Python 学習者は,クラスで
selfを第1引数で受け取らない関数には@staticmethodをつけるクセをつけたほうが良い. - 動けば良い,ではトラブルシューティングで苦労する.
- 公式ドキュメントではつけなくても良いと言っている.
oTree 3 では,ページクラス自身のインスタンスオブジェクト(
self)を受け取り,そこからplayerオブジェクトを取り出していた.- oTree 5 へのアップデートで,組み込みの関数において実験データにアクセスする際にいちいち
selfから取り出す必要がなくなったことは(利点かどうかは別として)大きな特徴である.oTree の著者も, v5 のスタイルを「 no-self format 」と呼んでいる.
- oTree 5 へのアップデートで,組み込みの関数において実験データにアクセスする際にいちいち
なお,oTree 3 のような
selfを引数に取るスタイルでの関数定義は oTree 5 でも可能といえば可能だが,スタイルを完全に oTree 3 のスタイルにしなければならない.重要な点は__init__.pyにデータモデルのクラスとページクラスの両方を定義するのではなく,models.pyとpages.pyに分離して定義しなければならないところである.実のところ,「 no-self format 」か否かの判定は__init__.pyにimportの文字列が含まれているかどうかで,含まれていれば「 no-self format 」として処理をする.@staticmethodがついているかどうか,とか,関数の引数がselfかplayerか,とかはまったく関係ない.引数にselfと書いてあっても,「 no-self format 」の場合は容赦なくplayerオブジェクトを渡してくる.
無限回繰り返しゲーム(繰り返しPD)
ただし, oTree のバージョンが古い.バージョン5の書き方に翻訳したものはこちら:
https://github.com/yshimod/otree_repeated_prisoner
確率的に繰り返しを終了するとき NUM_ROUNDS は最大数を設定する
oTree はサーバーを立ち上げる際にデータベースの列を作成している.
一度データベースの枠を多めに作っておいてから,使わずに空欄のままにしておく分にはどうとでもなる.
NUM_ROUNDS = 100としておくと,サーバーを立ち上げたときデータベースにラウンド数分の列を生成する.- subsession の途中で引き出した乱数の値によって条件分岐し,終了ラウンド以降を
is_displayed()やapp_after_this_page()を使ってページをスキップしてしまえば,確率的にラウンドの繰り返しを終わらせることができる.- subsession の途中でも REST を使って外部から
session.varsに値を渡すことができるので,ラボでサイコロを振って確率的に繰り返しの終了を決めることもできる.
https://otree.readthedocs.io/en/latest/misc/rest_api.html#session-vars-endpoint
- subsession の途中でも REST を使って外部から
- 途中でラウンド数を増やすことはできないため,
NUM_ROUNDSで設定するラウンド数は大きくしないといけない.しかし,NUM_ROUNDSを増やすほどデータベースの列数は増え,処理に時間がかかりパフォーマンスは悪化する(?).
サーバーを立ち上げる際に(定数
Cクラスにおいて)乱数を引き出して,確率的に決定されるラウンド数を予め決定してしまう,というのも手.最大数が定義できない場合や,データベースの列数が増えることによるパフォーマンス低下を避ける場合,ライブページと ExtraModel を使って実装するのが良い.
- https://www.otreehub.com/projects/otree-more-demos/ の「supergames_indefinite」アプリ.
- この作例のポイントは...
- oTree サーバーを立ち上げた時点で乱数を引き,幾何分布から一つ一つのスーパーゲームの長さ(ステージゲームの回数)を決めて,定数としている点.
Cクラスの中で乱数を引いた場合,サーバーを再起動しない限り,いくらセッションを作り直しても同じスーパーゲームの長さとなる.- スーパーゲームの長さは定数で定義されているため, group によってスーパーゲームの長さを変えたい場合には違う実装を考えなければならない.
- Heroku を使う場合,自動的に再起動されるので(乱数のシードを設定していない限り)意図しないタイミングでスーパーゲームの長さが変わってしまうことに注意.
- スーパーゲームの回数は最大数を定数で定義して,指定時間を超えた以降のスーパーゲームは表示させない点.
- 指定時間が来る前に設定した最大回数が終わってしまった場合,追加ラウンドを行うことはできない(急いでサーバーを立ち上げ直した後セッションをもう一度始める,で良いならそれでいいが).
- この作例では,指定時間を超えた場合にのみ表示する最終ページ(
End)を用意し,そのページに「次へ」ボタンを置かないことにより,そこで終了(したことに)している.- あくまでラウンド繰り返しの途中で「次へ」進めなくしているだけなので,うっかり「Advance slowest user(s)」ボタンを押してしまうと,続きの新たなラウンドが始まってしまう.
- 次のアプリに進める必要がある場合(たとえば繰り返しゲーム課題の後に質問紙調査がある場合),ちゃんと
is_displayed()やapp_after_this_page()を使ってページをスキップさせる必要がある.- 指定時間を超えた場合にのみ表示する最終ページで
app_after_this_page()を定義すれば,以降の余分なラウンドを一気にスキップできる.
- 指定時間を超えた場合にのみ表示する最終ページで
- oTree サーバーを立ち上げた時点で乱数を引き,幾何分布から一つ一つのスーパーゲームの長さ(ステージゲームの回数)を決めて,定数としている点.