この記事では、Python製の実験プラットフォーム「oTree」を用いて、社会学と社会心理学の文脈でよく使われる「一般交換ゲーム(Generalized Exchange Game)」の実験プログラムを作成する方法について解説します。
内容がやや専門的かつ長くなるため、数回に分けて投稿する予定です。
※本記事は、筆者がoTreeの学習過程で作成した実験プログラムをもとにまとめたものです。まだ応用経験は浅く、コードには未熟な点や改善の余地が多々あるかと思いますが、「ひとまず動く形」にたどり着いた記録として、同様に学ぶ方の参考になれば幸いです。
今回の内容(第1回)
- oTreeとは何か
- 一般交換ゲームとは何か
- 一般交換と限定交換
- 一般交換の分類
- 実験プログラムの基本構造
- 実験の基本デザイン
C(BaseConstants)Subsession(BaseSubsession)Group(BaseGroup)Player(BasePlayer)- 実験上必要な他の補助メソッド
- 引用文献
oTreeとは何か
oTreeはPythonで開発された、Webブラウザを利用して社会科学実験を行うためのライブラリです。
他の実験プログラム、例えばjsPsych, Psychopyなどと比べて、oTreeは複数人プレイヤーが同時にプレイーするインタラクションのあるゲーム実験を容易に実施できる(後藤, 2024)。
oTreeの詳しい説明に関して、後藤先生の「oTreeではじめる社会科学実験入門 Pythonのインストールから実験の実施まで」をご参照ください。本記事の説明もこの本を参考にしています。
一般交換ゲームとは何か
一般交換と限定交換
皆さんは「囚人のジレンマ」をご存知でしょうか?
囚人のジレンマでは、2人のプレイヤーがペアを組み、互いの選択によって得られる報酬が決まります。このように、特定の相手との間で直接的なやり取りを行う状況は「限定交換」と呼ばれます。
これに対して「一般交換(generalized exchange)」は、少なくとも3人以上の参加者が必要です。一般交換では、資源を提供した相手から直接見返りを得るのではなく、他の第三者から資源を受け取る構造になっています。言い換えれば、直接的な返報が存在しない交換形式です(清成, 2002)。
たとえば、雪道で立ち往生している車を助ける行為を考えてみてください。その場で助けた相手から何かを得ようとしているわけではなく、「自分が将来困ったときに、見知らぬ誰かが助けてくれるかもしれない」という期待が行動を動機づけています。
一般交換が成立するための基盤としては、過去の行動に基づいた他者の評判を参照して意思決定が行われることが前提とされている(小野田・高橋, 2016)。
例えば、ある人(Aさん)が別の人(Bさん)に協力すべきかどうかを判断する際、Aさん自身はBさんと直接的なやり取りを持っていない場合でも、これまでBさんが他者に対してどの程度協力的であったかという情報(評判) に基づいて、協力するか否かを決定する。
一般交換の分類
一般交換システム(Generalized Exchange System)は、さらに以下の二つに分類されます(Simpson et al., 2018):
連鎖型一般交換(Chain Generalized Exchange System)
純粋型一般交換(Pure Generalized Exchange System)
連鎖型一般交換では、交換のネットワーク構造が固定されており、各ラウンドでは、同じ順序で資源がやり取りされます。例えば、「A→B、B→C、C→A」というように、あらかじめ定められた構造に従ってゲームが繰り返されます。
一方、純粋型一般交換では、ネットワークの構造は固定されておらず、各ラウンドで参加者は自由に資源の提供先を選択できます。たとえば、1回目は「A→B、B→C、C→A」という構造でも、2回目には「A→C、C→B、B→A」という異なる構造が形成される可能性もあります。
実験プログラムの基本構造
実験の基本デザイン
実験デザインはWhitham(2018)を参考にした。
-
参加者は、いくつかの3人グループに分けられます。
-
各グループでは、毎ラウンドごとにランダムに2名が選ばれ、そのうち1名が「提供者(sender)」、もう1名が「受取者(receiver)」の役割を担当します。残りの1名はそのラウンドでは役割を持たず、ゲームには参加しません。
このように、参加者はラウンドごとに異なる役割を担う可能性があり、あるラウンドでは提供者、別のラウンドでは受取者になることもあります。 -
各ラウンドでは、提供者に10ポイントが与えられます。提供者は「提供する」か「提供しない」かを選択します:
- 「提供する」を選んだ場合、6ポイントが2倍(12ポイント)となって受取者に渡され、残りの4ポイントは提供者の報酬(payoff)となります。
- 「提供しない」を選んだ場合、提供者は10ポイントすべてを自身の報酬とし、受取者にはポイントが渡されません。
C(BaseConstants)
このクラスでは、実験の基本的な定数を定義しています。
from otree.api import *
import random
doc = """
Your app description
"""
class C(BaseConstants):
NAME_IN_URL = 'generalized_exchange_realtime'
#各グループの人数
PLAYERS_PER_GROUP = 3
#ゲームの反復回数
NUM_ROUNDS = 5
#固定される提供ポイント数
FIXED_AMOUNT = cu(6)
#毎ラウンドsenderに割り振られるポイント数
ENDOWMENT = cu(10)
#FIXED_AMOUNT*2はreceiverがもらったポイント数
MULTIPLIER = 2
各グループは3人(PLAYER_PER_GROUP)で、全5ラウンド(NUM_ROUNDS)実施。
senderには毎ラウンド10ポイント(ENDOWMENT)が配布され、6ポイント(FIXED_AMOUNT)を提供すれば2倍(MULTIPLIER)でreceiverに渡る。
他のクラスまたは各ページのHTMLテンプレートでは、C.ENDOWMENT のように C 経由で定数を呼び出せます。
Subsession(BaseSubsession)
このクラスでは、第2ラウンド以降のプレイヤー役割(sender / receiver / inactive)のランダム割り当てを行います。第1ラウンドの割り当て処理は creating_session() で別途定義します。
class Subsession(BaseSubsession):
def before_next_round(self):
#senderとreceiverをランダムに選出する
for group in self.get_groups():
players = group.get_players()
selected_pair = random.sample(players, 2)
sender = selected_pair[0]
receiver = selected_pair[1]
sender.roles = "sender"
receiver.roles = "receiver"
sender.partner_id = receiver.id_in_group
receiver.partner_id = sender.id_in_group
#残りの参加者をinactiveとして定義する
for p in players:
if p not in selected_pair:
p.roles = "inactive"
p.partner_id = 0 # 安全のため初期化
この before_next_round()メソッドは、第2ラウンドでは実行されず、第2ラウンド以降の開始前に oTree によって自動的に実行されます。
get_players() を使って各グループのプレイヤーを取得し、その中からランダムに2名を選出して、sender / receiver の役割を割り当てます。選ばれなかった1名は inactive に設定され、やり取りに関与しません。
Group(BaseGroup)
このクラスでは、特に追加の属性や設定は行いません。ただし、Group クラスを引数として受け取り、各プレイヤーの報酬や行動履歴を処理する補助的なメソッドを別に定義しています。
class Group(BaseGroup):
pass
Player(BasePlayer)
Playerクラスで、各プレイヤーに関する変数を定義します。
class Player(BasePlayer):
#役割を記録する(sender, receiver,inactive)
roles = models.StringField() # "sender", "receiver", "inactive"
#相手のIDを記録する(senderにとって相手はreceiver,receiverにとって相手はsender)
partner_id = models.IntegerField()
# 送金するかどうか(貢献するか)
contribute = models.BooleanField(
label="このプレイヤーにポイントを送りますか?",
choices = [
(True, 'はい'),
(False, 'いいえ')
],
widget = widgets.RadioSelect,
blank = False
)
#累積報酬を記録する
cumulative_payoff = models.CurrencyField(initial=0)
#各参加者の参加回数を記録する(sender or receiverとして選ばれた際に参加回数+1)
participated_rounds_tracker = models.IntegerField(initial=0)
#各参加者がsenderとして選ばれた際に、「提供する」を選んだ回数を記録する
sender_contribute_times = models.IntegerField(initial=0)
実験上必要な他の補助メソッド
利得計算用メソッドset_payoffs
プレイヤーの利得計算、参加回数および提供回数を計算するためのメソッドを定義します。
def set_payoffs(group: Group):
players = group.get_players()
sender = next(p for p in players if p.roles == 'sender')
receiver = next(p for p in players if p.roles == 'receiver')
#senderが提供または提供しない場合の利得計算関数
if sender.contribute:
sender.payoff = C.ENDOWMENT - C.FIXED_AMOUNT
receiver.payoff = C.FIXED_AMOUNT * C.MULTIPLIER
else:
sender.payoff = C.ENDOWMENT
receiver.payoff = 0
#送信者(sender)がこれまでに何回「提供(貢献)」したかをカウントしています。
if 'contribute_times' not in sender.participant.vars:
sender.participant.vars['contribute_times'] = 0
if sender.contribute:
sender.participant.vars['contribute_times'] += 1
#各プレイヤーの参加回数を計算する
for p in [sender, receiver]:
if 'participated_rounds' not in p.participant.vars:
p.participant.vars['participated_rounds'] = 1
else:
p.participant.vars['participated_rounds'] += 1
#senderとreceiverの累積報酬を計算する
for p in [sender, receiver]:
if 'cumulative_payoff' not in p.participant.vars:
p.participant.vars['cumulative_payoff'] = p.payoff
else:
p.participant.vars['cumulative_payoff'] += p.payoff
p.cumulative_payoff = p.participant.vars['cumulative_payoff']
set_payoffs メソッドでは、participant.vars という 辞書型の一時保存領域を利用して、各参加者の 参加回数、提供回数、および 累積報酬 を計算・更新しています。
participant.vars は oTree が提供する特殊な変数で、ラウンドやアプリをまたいで値を保持できるため、途中経過の記録に便利です。
ただし、participant.vars はデータベースのフィールドではなく、エクスポート時のCSVには直接出力されません。
分析用にデータを残したい場合は、計算結果を Player モデルや Group モデルのフィールドにも保存しておく必要があります。
例えば、participant.vars['cumulative_payoff'] の値を同時に player.cumulative_payoff フィールドにコピーしておけば、実験終了後に出力ファイルから直接集計・分析が可能になります。
第1ラウンドの役割配分メソッドcreating_session
続いて、第1ラウンド開始前にプレイヤーの役割をランダムに配分するメソッドを定義します。
この処理は creating_session メソッドとして記述し、セッション生成時に1度だけ実行されます。
def creating_session(subsession:Subsession):
for group in subsession.get_groups():
players = group.get_players()
selected_pair = random.sample(players, 2)
sender = selected_pair[0]
receiver = selected_pair[1]
sender.roles = "sender"
receiver.roles = "receiver"
sender.partner_id = receiver.id_in_group
receiver.partner_id = sender.id_in_group
for p in players:
if p not in selected_pair:
p.roles = "inactive"
p.partner_id = 0 # 安全のため初期化
この creating_session メソッドの処理内容は、Subsession クラスの before_next_round メソッドとほぼ同じです。
ただし、creating_session は実験の第1ラウンド開始前に1度だけ実行され、初期の役割配分を行います。
それ以降のラウンドにおけるプレイヤーの役割再配分は、before_next_round メソッドに任せています。
今回の記録・説明は以上です。
次回の記事では、実験画面を構築するための HTMLテンプレートの書き方 について解説します。
引用文献
清成透子. (2002). 一般交換システムに対する期待と内集団ひいき 閉ざされた互酬性の期待に関する実験研究. 心理学研究, 73(1), 1-9.
小野田竜一, & 高橋伸幸. (2016). 2 つの集団で構成される社会で一般交換を維持させる利他行動の特徴. 心理学研究, 87(3), 240-250.
後藤, 晶. (2024). oTreeではじめる社会科学実験入門――Pythonのインストールから実験の実施まで. コロナ社.
Whitham, M. M. (2018). Paying it forward and getting it back: The benefits of shared social identity in generalized exchange. Sociological Perspectives, 61(1), 81-98.
Simpson, B., Harrell, A., Melamed, D., Heiserman, N., & Negraia, D. V. (2018). The roots of reciprocity: Gratitude and reputation in generalized exchange systems. American Sociological Review, 83(1), 88-110.