Verified Commit e79721b4 authored by Max R. P. Grossmann's avatar Max R. P. Grossmann
Browse files

Initial commit

parents
Copyright (c) 2021 Max R. P. Grossmann
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
# AnonPay
AnonPay helps to enhance privacy in online experiments. **For the first time in experimental history, AnonPay made it possible to safely collect payment details within the experiment.**
Included in this repository are slides of a recent talk on AnonPay as well as example experiments for oTree and z-Tree. In all, these materials should be sufficient to use these tools. If you have further questions, please contact us: <cler-team@uni-koeln.de>
# Don't change anything in this file.
from .. import models
import otree.api
class Page(otree.api.Page):
subsession: models.Subsession
group: models.Group
player: models.Player
class WaitPage(otree.api.WaitPage):
subsession: models.Subsession
group: models.Group
player: models.Player
class Bot(otree.api.Bot):
subsession: models.Subsession
group: models.Group
player: models.Player
import math, random, sys
# These are the other AnonPay algorithms (ROD, NUN, SPA).
# They must be invoked manually from within your app.
# The remainder of this directory is the oTree equivalent of CPY+TDY.
def count(l, x):
return sum([1 for el in l if el == x])
def p_print2(paym):
for pi in paym:
print(pi, end = ", ")
print()
def hadUniqPaym(paym):
return any([count(paym, pi) == 1 for pi in paym])
def P(paym, attr, q):
a = sum([1 for (i, el) in enumerate(paym) if el == q and attr[i] == 1])/count(attr, 1)
b = sum([1 for (i, el) in enumerate(paym) if el == q and attr[i] == 0])/count(attr, 0)
c = count(attr, 1)/len(paym)
return (a * c)/(a * c + b * (1 - c))
def infoBayes(paym, attr):
return {pi: P(paym, attr, pi) for pi in [_pi for (_pi, _x) in zip(paym, attr) if _x == 1]}
def ROD(paym, chi):
for (i, pi) in enumerate(paym):
paym[i] = math.ceil(pi/chi)*chi
return paym
def NUN(paym):
if len(paym) > 3:
s = 1
while True:
if s == 1:
rank = [sum([1 for pi2 in paym if pi2 >= pi]) for pi in paym]
if count(rank, s) > 0:
q = [paym[i] for (i, r) in enumerate(rank) if r == s][0]
if count(paym, q) == 1:
who1 = max([pi for pi in paym if pi < q], default = -math.inf)
who2 = min([pi for pi in paym if pi > q], default = +math.inf)
if count(paym, who1) > 0 and count(paym, who1)*(q-who1) <= who2-q:
for (i, pi) in enumerate(paym):
if pi == who1:
paym[i] = q
s = 0
break
else:
for (i, pi) in enumerate(paym):
if pi == q:
paym[i] = who2
s = s + 1
if s > len(paym):
break
return paym
else:
print("ok - few subjects, payments all equalized", file = sys.stderr)
paym = [max(paym) for _ in paym]
return paym
def SPA(paym, attr, eta):
if eta > 0 and eta < 1 and len(paym) > 3 and count(attr, 1) > 0 and count(attr, 0) > 0:
s = 1
hadUniq = hadUniqPaym(paym)
while True:
if s == 1:
rank = [sum([1 for pi2 in paym if pi2 >= pi]) for pi in paym]
if count(rank, s) > 0:
q = [paym[i] for (i, r) in enumerate(rank) if r == s][0]
if count(attr, 1)/len(paym) > eta:
print("ERROR: too many attribute bearers, infeasible", file = sys.stderr)
break
else:
while P(paym, attr, q) > eta:
if any([pi < q for (i, pi) in enumerate(paym) if attr[i] == 0]) or any([pi > q for (i, pi) in enumerate(paym) if attr[i] == 0]):
who1 = max([pi for (i, pi) in enumerate(paym) if pi < q and attr[i] == 0], default = -math.inf)
who2 = min([pi for (i, pi) in enumerate(paym) if pi > q and attr[i] == 0], default = +math.inf)
c = sum([1 for (i, pi) in enumerate(paym) if pi == who1 and attr[i] == 0])
if c > 0 and c*(q-who1) <= count(paym, q)*(who2-q):
for (i, pi) in enumerate(paym):
if pi == who1 and attr[i] == 0:
paym[i] = q
break
s = 0
break
else:
for (i, pi) in enumerate(paym):
if pi == q:
paym[i] = who2
s = 0
break
else:
print(f"ERROR: no valid candidates", file = sys.stderr) # should be impossible
break
if s == len(paym) and not hadUniq:
if any([count(paym, pi) == 1 for pi in paym]):
paym = NUN(paym)
s = 0
s = s + 1
if s > len(paym):
break
return paym
elif eta <= 0 or eta >= 1:
print("(disabled)", file = sys.stderr)
return paym
elif len(paym) <= 3:
paym = [max(paym) for _ in paym]
print("err - few subjects, payments all equalized", file = sys.stderr)
return paym
else:
print("ok - inactive", file = sys.stderr)
return paym
from otree.api import (
models,
widgets,
BaseConstants,
BaseSubsession,
BaseGroup,
BasePlayer,
Currency as c,
currency_range,
)
author = 'AnonPay'
doc = """
AnonPay (CPY/TDY oTree equivalent, the other algorithms are in anonpay.py)
"""
class Constants(BaseConstants):
name_in_url = 'AnonPay'
players_per_group = None
num_rounds = 1
class Subsession(BaseSubsession):
pass
class Group(BaseGroup):
pass
class Player(BasePlayer):
error = models.StringField(initial = '')
email = models.StringField(initial = '')
emergency_code = models.StringField(initial = '')
feedback = models.LongStringField(blank = True)
from otree.api import Currency as c, currency_range
from ._builtin import Page, WaitPage
from .models import Constants
import random, os
try:
import fcntl
except ModuleNotFoundError:
pass
def formatNicely(amount):
return "{:.2f}".format(amount)
# return "{:.2f}".format(amount).replace('.', ',') # for EUR
class PageWithAmount(Page):
def vars_for_template(self):
vft = {'nicePayment': formatNicely(self.participant.vars['payment']), 'emailEntered': self.player.email}
self.player.email = '[REDACTED]'
return vft
class Details(PageWithAmount):
form_fields = ['email']
form_model = 'player'
timeout_seconds = 5*60
def before_next_page(self):
self.player.emergency_code = ''.join(random.choices(list('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'), k = 8))
try:
pfile = os.path.join(os.path.dirname(os.path.abspath(__file__)), "payments", self.session.code + ".csv")
with open(pfile, "a+") as fp:
try:
fcntl.flock(fp, fcntl.LOCK_EX)
except NameError:
pass
fp.seek(0)
lines = fp.read().splitlines()
lines.append(f'"{self.player.email}";"{self.participant.vars["payment"]}"')
random.shuffle(lines)
fp.seek(0)
fp.truncate()
fp.write('\n'.join(lines))
try:
fcntl.flock(fp, fcntl.LOCK_UN)
except NameError:
pass
except:
# This should not happen, but if the payment file is not writable, we will write all data
# to the first subject's error variable.
self.group.get_players()[0].error = self.group.get_players()[0].error + "email: " + self.player.email + ", payment: " + str(self.participant.vars["payment"]) + " | "
class EndFeedback(PageWithAmount):
form_model = 'player'
form_fields = ['feedback']
page_sequence = [Details, EndFeedback]
{% extends "global/Page.html" %}
{% load otree static %}
{% block title %}
Payment form
{% endblock %}
{% block content %}
<p>Welcome to the payment form of the Example Lab.</p>
<p>Using this payment form ensures that your behavioral data cannot be linked to your payment data.</p>
<p>Your payment is <b>${{ nicePayment }}</b>. To perform the payment, please enter your payment details.</p>
{% formfield player.email label="Your payment details" %}
<script type="text/javascript">
document.getElementsByName("email")[0].value = "";
</script>
<p>By submitting this form, you agree to the <a href="https://example.com" target="_new">privacy policy of the Example Lab</a>. In addition, you agree that your payment data may be processed for the purposes of the payment. Your behavioral data will not be linked to your payment data.</p>
{% next_button %}
{% endblock %}
{% extends "global/Page.html" %}
{% load otree static %}
{% block title %}
Thank you
{% endblock %}
{% block content %}
<p>The following data has been saved:</p>
<div class="table-responsive">
<table class="table table-striped">
<tr>
<th>Payment details</th><td>{{ emailEntered }}</td>
</tr>
<tr>
<th>Payment amount</th><td>{{ nicePayment }} €</td>
</tr>
</table>
</div>
<p><i>If you spot an error in these data, you must <b>now</b> contact the lab using the code <tt>{{ player.emergency_code }}</tt>: <a href="mailto:lab@example.com">lab@example.com</a>.</i></p>
<p>If you have no further instructions, the experiment is now completed. You can now close the browser.</p>
<p>Payment will be made within three working days. Please do not contact the laboratory until this deadline has passed. Quote the code <tt>{{ player.emergency_code }}</tt> in all queries. Without this code it is not possible to determine your payment amount.</p>
<p>Using this payment form ensures that your behavioral data cannot be linked to your payment data. Thank you for your understanding.</p>
<p><b>If you like, you can give us feedback on today's experiment.</b> Please make sure not to enter any personal data in this field.</p>
{% formfield player.feedback label="" %}
{% next_button %}
{% endblock %}
from otree.api import Currency as c, currency_range
from . import pages
from ._builtin import Bot
from .models import Constants
class PlayerBot(Bot):
def play_round(self):
yield pages.Details, {'email': str(self.player.id)}
yield pages.EndFeedback
# Don't change anything in this file.
from .. import models
import otree.api
class Page(otree.api.Page):
subsession: models.Subsession
group: models.Group
player: models.Player
class WaitPage(otree.api.WaitPage):
subsession: models.Subsession
group: models.Group
player: models.Player
class Bot(otree.api.Bot):
subsession: models.Subsession
group: models.Group
player: models.Player
from otree.api import (
models,
widgets,
BaseConstants,
BaseSubsession,
BaseGroup,
BasePlayer,
Currency as c,
currency_range,
)
import AnonPay.anonpay as anonpay
import random
author = 'Max R. P. Grossmann'
doc = """
A game where everyone gets what they want
"""
class Constants(BaseConstants):
name_in_url = 'Game'
players_per_group = None
num_rounds = 1
class Subsession(BaseSubsession):
def creating_session(self):
for p in self.get_players():
p.local_payoff = round(max(3, random.gauss(10, 4)), 1) # example payoffs
class Group(BaseGroup):
def set_and_adjust_payments(self):
# This runs exactly once: When all players have arrived at the WaitPage.
# See pages.py, line 19.
paym = [float(p.local_payoff) for p in self.get_players()] # must be float
attr = [int(p.has_attr) for p in self.get_players()] # must be 0/1
# This is where the magic happens:
paym = anonpay.ROD(paym, 0.1) # the second argument here is chi
paym = anonpay.NUN(paym)
paym = anonpay.SPA(paym, attr, 0.4) # the third argument is eta
# After the algorithms, the adjusted payments are written back:
for (p, pi_tilde) in zip(self.get_players(), paym):
p.participant.vars['payment'] = pi_tilde
# Note how oTree's own payoff variable is not used.
# You may use it, but it is ignored by the AnonPay app.
# Only participant.vars['payment'] matters to AnonPay.
class Player(BasePlayer):
local_payoff = models.FloatField()
has_attr = models.BooleanField(initial = False)
from otree.api import Currency as c, currency_range
from ._builtin import Page, WaitPage
from .models import Constants
from AnonPay.pages import formatNicely
class MyPage(Page):
form_model = 'player'
form_fields = ['has_attr']
timeout_seconds = 2*60
def vars_for_template(self):
return {'nicePayment': formatNicely(self.player.local_payoff)}
class ResultsWaitPage(WaitPage):
after_all_players_arrive = 'set_and_adjust_payments'
class Results(Page):
def vars_for_template(self):
return {'nicePayment': formatNicely(self.participant.vars['payment'])}
timeout_seconds = 60
page_sequence = [MyPage, ResultsWaitPage, Results]
{% extends "global/Page.html" %}
{% load otree static %}
{% block title %}
Data entry
{% endblock %}
{% block content %}
<p>Your (intended) payoff is <b>${{ nicePayment }}</b>.</p>
{% formfield player.has_attr label="Has attribute?" %}
{% next_button %}
{% endblock %}
{% extends "global/Page.html" %}
{% load otree static %}
{% block title %}
Result
{% endblock %}
{% block content %}
<p>Thanks for playing.</p>
<p>Your payment is <b>${{ nicePayment }}</b>.</p>
<p>This might be higher than the amount you expected because you might be paid a "privacy-enhancing payment": A bonus payment to enhance the privacy of subjects in this experiment.</p>
<p>If you want to find out how this works, please contact the lab.</p>
<p>To go to the payment form, please click “Next”.</p>
{% next_button %}
{% endblock %}
from otree.api import Currency as c, currency_range
from . import pages
from ._builtin import Bot
from .models import Constants
class PlayerBot(Bot):
def play_round(self):
pass
dict(
name = 'AnonPay_example',
num_demo_participants = 1,
app_sequence = ['Game', 'AnonPay'],
),
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment