Repository: liberapay/liberapay.com

Issue:
Tweaking the take throttling algorithm
 We're trying to use liberapay for our catalyst community in Lille, that's why we got 100€ (https://liberapay.com/collectifcatalyst/) by week for our local community. We need people to take this money faster than starting with 1€ (and 2€ / 4 / 8 next week) as people are giving a lot of time now. Is that possible ? Could we get multiple way to distribute money in a team depending on how the project go (for example for a faster project, starting with 1€ is difficult).



Discussion:
For the record, the description of the current system from [/about/teams](https://en.liberapay.com/about/teams#throttling):

> Nominal takes are throttled, they can't be raised higher than a maximum computed based on the nominal takes at the time of the previous payday. This regulation is meant to reduce the risk of abuse by ill-intentioned team members.
> 
> The exact limit is the maximum of these three values: the number 1, two times the member's take in the previous payday, 70% of the median take in the previous payday, or if that number is zero, the current income divided by the number of members.

Related issue: #81.

Hello Changaco, I read your message and I wanted to complete Simon's question. I'm also a member of Catalyst community. As Simon said we have 100 euros per week to split between the persons who contribute to a specific project. The thing is that we want to share this money between us in a defined period. Indeed it's 1,5 month work so we'd like to be able to split and use this money during this period. How can we do that? Could we adapt the system for cases like that? Thank you and see you soon in Lille.

I have an idea on how to improve the take throttling algorithm, but I'm not sure it would work for your case, and more importantly I don't want to work on it right now. Thus I think the best course of action here is for you to tell me how much each member of your team would like to take this week, and I'll enter those amounts directly into the database, bypassing the throttling mechanism.

(Note to self: the idea is to bring the amount of leftover income into the equation.)

(But the problem with the leftover amount is that it depends on income so it can change.)

@SimonSarazin @MarionRousseaux @pgno If you want me to change your takes for this week's payday then I need an answer this morning.

In the absence of a reply I've opted to boost the two existing takes to €16, which means that other members will be able to take up to ~~€11~~ €6.96 after payday.

For the record, how I did it:

```
% rhc ssh
[…]
> cd $OPENSHIFT_REPO_DIR && PYTHONPATH=. python -i liberapay/main.py
>>> changaco = Participant.from_username('Changaco')
>>> pgno = Participant.from_username('pgno')
>>> simons = Participant.from_username('simons')
>>> team = Participant.from_username('collectifcatalyst')
>>> team.set_take_for(pgno, '16.00', changaco, check_max=False); team.set_take_for(simons, '16.00', changaco, check_max=False)
Decimal('16.00')
Decimal('16.00')
```

I've upped @MarionRousseaux's take to 16 by request of @SimonSarazin in IRC.

The proposal of Gratipay's founder for their new 2.0 take-what-you-want system: https://github.com/gratipay/gratipay.com/issues/3994#issuecomment-217340995. It doesn't have the throttling issue that the Catalyst team ran into on Liberapay, as their proposed system is fully proportional, but the downside is that actual take amounts would not be stable, they'd go up and down as income changes.

> the downside is that actual take amounts would not be stable, they'd go up and down as income changes.

Don't they already go up and down as income changes?

Sorry, seeing [this](https://liberapay.com/about/teams) now:

> Nominal takes also act as maximums: the actual takes are never higher than the nominal ones, even if additional income is available.

It would be nice to have the leftover money shared equally between every member of the team that takes more then 0€. 

That way, people taking nothing won't get anything (what they want), but people taking will get some more.

Distributing the leftover every week would lead to a decrease in income for established members when new people join the team and start taking money. That's contrary to our goal of a stable income, plus I think it could create resentment and deter teams from adding new members.

However, I think @whit537's idea of a "bonus" (https://github.com/gratipay/gratipay.com/issues/3994#issuecomment-217419201) is interesting. It could be monthly or quarterly, and maybe limited to a percentage of the money the member has taken from the team during that time, like 10%.

Another possibility is to use the leftover to fund one-time team takes (#81).


Resolving patch:
diff --git a/liberapay/models/_mixin_team.py b/liberapay/models/_mixin_team.py
--- a/liberapay/models/_mixin_team.py
+++ b/liberapay/models/_mixin_team.py
@@ -75,7 +75,6 @@ def get_takes_last_week(self):
           ORDER BY member, mtime DESC
 
         """, (self.id,)) if t.amount}
-        takes['_relative_min'] = median(takes.values() or (0,)) ** Decimal('0.7')
         return takes
 
     def get_take_for(self, member):
@@ -87,13 +86,20 @@ def get_take_for(self, member):
         )
 
     def compute_max_this_week(self, member_id, last_week):
-        """2x the member's take last week, or a minimum based on last week's
-        median take, or current income divided by the number of members if takes
-        were zero last week, or 1.
+        """2x the member's take last week, or the member's take last week + a
+        proportional share of the leftover, or a minimum based on last week's
+        median take, or 1.
         """
+        sum_last_week = sum(last_week.values())
+        initial_leftover = self.receiving - sum_last_week
+        nonzero_last_week = [a for a in last_week.values() if a]
+        member_last_week = last_week.get(member_id, 0)
+        leftover_share = member_last_week / (sum_last_week or Decimal('inf'))
+        leftover_share = max(leftover_share, D_UNIT / self.nmembers)
         return max(
-            last_week.get(member_id, 0) * 2,
-            last_week['_relative_min'] or self.receiving / self.nmembers,
+            member_last_week * 2,
+            member_last_week + initial_leftover * leftover_share,
+            median(nonzero_last_week or (0,)),
             D_UNIT
         )
 


Test patch:
diff --git a/tests/py/test_take.py b/tests/py/test_take.py
--- a/tests/py/test_take.py
+++ b/tests/py/test_take.py
@@ -24,10 +24,11 @@ def take_last_week(self, team, member, amount, actual_amount=None):
         team.set_take_for(member, amount, team, check_max=False)
         self.db.run("INSERT INTO paydays DEFAULT VALUES")
         actual_amount = amount if actual_amount is None else actual_amount
-        self.db.run("""
-            INSERT INTO transfers (tipper, tippee, amount, context, status, team)
-            VALUES (%(tipper)s, %(tippee)s, %(amount)s, 'take', 'succeeded', %(team)s)
-        """, dict(tipper=self.warbucks.id, tippee=member.id, amount=actual_amount, team=team.id))
+        if D(actual_amount) > 0:
+            self.db.run("""
+                INSERT INTO transfers (tipper, tippee, amount, context, status, team)
+                VALUES (%(tipper)s, %(tippee)s, %(amount)s, 'take', 'succeeded', %(team)s)
+            """, dict(tipper=self.warbucks.id, tippee=member.id, amount=actual_amount, team=team.id))
         self.db.run("UPDATE paydays SET ts_end=now() WHERE ts_end < ts_start")
 
     def test_random_schmoe_is_not_member_of_team(self):
@@ -41,12 +42,6 @@ def test_team_member_is_team_member(self):
         self.take_last_week(team, alice, '40.00')
         assert alice.member_of(team)
 
-    def test_cant_grow_tip_a_lot(self):
-        team = self.make_team()
-        alice = self.make_participant('alice')
-        self.take_last_week(team, alice, '40.00')
-        assert team.set_take_for(alice, D('100.00'), alice) == 80
-
     def test_take_can_double(self):
         team = self.make_team()
         alice = self.make_participant('alice')
@@ -56,6 +51,7 @@ def test_take_can_double(self):
 
     def test_take_can_double_but_not_a_penny_more(self):
         team = self.make_team()
+        self.warbucks.set_tip_to(team, '20')
         alice = self.make_participant('alice')
         self.take_last_week(team, alice, '40.00')
         actual = team.set_take_for(alice, D('80.01'), alice)
@@ -63,6 +59,7 @@ def test_take_can_double_but_not_a_penny_more(self):
 
     def test_increase_is_based_on_nominal_take_last_week(self):
         team = self.make_team()
+        self.warbucks.set_tip_to(team, '15.03')
         alice = self.make_participant('alice')
         self.take_last_week(team, alice, '20.00', actual_amount='15.03')
         team.set_take_for(alice, D('35.00'), team, check_max=False)
@@ -70,11 +67,26 @@ def test_increase_is_based_on_nominal_take_last_week(self):
 
     def test_if_last_week_is_less_than_one_can_increase_to_one(self):
         team = self.make_team()
+        self.warbucks.set_tip_to(team, '0.50')
         alice = self.make_participant('alice')
         self.take_last_week(team, alice, '0.01')
         actual = team.set_take_for(alice, D('42.00'), team)
         assert actual == 1
 
+    def test_can_take_one_share_when_takes_were_all_zero_last_week(self):
+        team = self.make_team()
+        alice = self.make_participant('alice')
+        self.take_last_week(team, alice, '0.00')
+        actual = team.set_take_for(alice, D('200.00'), team)
+        assert actual == 100
+
+    def test_can_take_leftover(self):
+        team = self.make_team()
+        alice = self.make_participant('alice')
+        self.take_last_week(team, alice, '0.01')
+        actual = team.set_take_for(alice, D('200.00'), team)
+        assert actual == 100
+
     def test_get_members(self):
         team = self.make_team()
         alice = self.make_participant('alice')