# Referral Network & Rewards

I want to be able to distribute a total reward among network, where the distribution diminishes according to the depth of the network but the total sum of distributed rewards equals the initial total reward.

I have attempted to model this with:

$$\S = a * (1 - r^n) / (1 - r)\$$

The below Python snippet presents this series.

def distribute_total_reward(total_reward: float, decay_rate: float, max_levels: int = 10) -> list:
# Solve for the first term (a)
a = total_reward * (1 - decay_rate) / (1 - decay_rate ** max_levels)
rewards = [a * (decay_rate ** i) for i in range(max_levels)]
return rewards

if __name__ == "__main__":
# Total reward to be distributed
tw = 3500

# 50% Decay rate for the distribution
dr = 0.5

# Number of levels in the network
ml = 8

reward_distribution = distribute_total_reward(tw, dr, ml)
for level, reward in enumerate(reward_distribution, start=1):
print(f"Level {level} referrer receives: £${reward:.2f}") print(f"Total Distributed:${sum(reward_distribution):.2f}")


Note: I have arbitrarily set tw, dr, ml.

One of the things I have struggled with is building out the solution to support circular referrals. I think this sort of breaks a referral system designed with hierarchy because anyone can referrer anyone else. I want to accommodate the flexibility of reverse referrals (i.e., downstream referring upstream and vice versa).

I have tried implementing a fixed reward for reverse referrals, separate from the main referral rewards. However, I feel like this is too simplistic as it assumes a fixed number of reverse referrals. Realistically, we need to dynamically identify these reverse referral scenarios and allocate accordingly. Here is the code:

def distribute(
total_reward: int,
decay_rate: float,
max_levels: int = 10,
reverse_referral_reward: int = 5
) -> tuple:
"""
Distributes the total reward among the referral network and handles reverse referrals.
"""
num_reverse_referrals = 2
adjusted_total_reward = total_reward - (num_reverse_referrals * reverse_referral_reward)

a = adjusted_total_reward * (1 - decay_rate) / (1 - decay_rate ** max_levels)
rewards = [a * (decay_rate ** i) for i in range(max_levels)]
reverse_rewards = [reverse_referral_reward for _ in range(num_reverse_referrals)]
return rewards, reverse_rewards

if __name__ == "__main__":

total_reward = 3500           # Total reward to be distributed
decay_rate = 0.5              # Decay rate for the distribution
max_levels = 8                # Number of levels in the referral network
reverse_referral_reward = 5   # Reward for reverse referrals

primary_rewards, reverse_rewards = distribute(
total_reward,
decay_rate,
max_levels,
reverse_referral_reward
)
for level, reward in enumerate(primary_rewards, start=1):
print(f"Level {level} primary referrer receives: ${reward:.2f}") for i, reward in enumerate(reverse_rewards, start=1): print(f"Reverse referral {i} receives:${reward:.2f}")

print(f"Total Distributed Reward: ${sum(primary_rewards) + sum(reverse_rewards):.2f}")  • Can you add descriptions for all of the variables in your expression at the top? Mar 15 at 16:13 • I have applied this to the new edit. – Bob Mar 16 at 1:48 • I have rolled back Rev 5 → 1. Please see What should I do when someone answers my question?. Mar 16 at 4:16 ## 2 Answers # clear identifiers distribute_total_reward() looks great. All the identifiers are very helpful. An URL citation of that formula which introduces the variable "a" wouldn't hurt. # post-condition The essential aspect of this routine is the computed rewards shall sum to the specified input parameter. We should minimally spell this out with an English sentence in a docstring. The supplied test code nicely computes a sum(), but it is not self-evaluating so a human must eyeball the result and verify it's sensible. Ideally the function would end with an assert of equality, but due to FP ULP rounding errors we expect a small epsilon error, so the test would be for relative_error() less than e.g. 1 ppb. An easy assert would be to construct a unit test to check the post-condition. For some parameters, such as 1024 total reward and .5 decay rate, the FP result will yield exact equality. # def main() Nice __main__ guard. There is starting to be enough code here that it may be worth burying it within def main():. By the time you introduce reverse_rewards we're definitely ready for that. I confess I'm not sure about the choice of tw rather than a tr name. nit: We switch between £$ and $ currency prefixes. # distribute() It's unclear why the currency figures of total_reward and reverse_referral_reward would be of type int. Please understand that a float annotation subsumes int, even though integers can have unlimited magnitude and there's no inheritance relationship between them. We have a docstring, and it is informative. But it's a little wishy washy. If I'm tasked with writing a unit test that verifies correct behavior, verifies the implementation conforms to a spec, then consulting just the docstring, alas, won't suffice. The verbs "distributes" and "handles" are OK but they aren't accompanied by any specifics. It seems like we want a pair of post-conditions here, constraining what happens with each of the two reward categories. # DRY  adjusted_total_reward = total_reward - (num_reverse_referrals * reverse_referral_reward) ... reverse_rewards = [reverse_referral_reward for _ in range(num_reverse_referrals)]  These are kind of saying the same thing. Consider first computing  reverse_rewards = [reverse_referral_reward] * num_reverse_referrals  and then you can conveniently assign total_reward - sum(reverse_rewards). # design of Public API  return rewards, reverse_rewards  This kind of looks like we're returning parallel vectors a, b as seen in many Fortran APIs, where a[i] describes one aspect of entity i and b[i] another aspect of that same entity. But of course they have different lengths. Consider including 0 values in the reverse_rewards. Consider returning a single vector of namedtuples that contain values for both reward categories. Which brings us to the input parameters. You will need a way to describe who referred, in which direction, and how effectively. This could be a general graph, but I am skeptical that you have a business use case for that yet. Better to start out with a restricted graph such as a tree. Maybe pass in a vector of (forward, reverse) referral magnitudes, and of course when all the reverse values are zero we should produce same result as that first algorithm produces. # algorithm The closed-form solution you provide is very nice. Here is another way, a little messier, to think about that initial algorithm. Assign total_reward to the initial entry, with the rest zero. Use a for i in range(max_levels): loop to make several passes over the vector, distributing a small fraction of remaining reward each time. The last iteration makes the one and only assignment to the final level. We always {add, subtract} same small value, preserving a constant total. Now consider a graph that includes reverse referrals. Again we make several passes over the graph starting from its root, distributing a fraction as we go. But when following an upward edge we now can distribute upward, as well. The current approach of “divide level I reward evenly among that level’s participants” will need to move toward considering each participant individually. It's possible that you wish to view this as "a single (downward) origin node plus N (upward) reverse nodes", and so you want to run N + 1 instances of the loop. Applying @J_H suggestions, I am adding this answer as reference for any future readers. def distribute(total_reward: float, decay_rate: float, max_levels: int) -> list: """ Distributes a total reward across a referral network based on a geometric decay rate. This function calculates the initial reward amount for the first level and then applies a decay rate for subsequent levels, ensuring that the sum of all rewards is as close as possible to the total_reward. The geometric series formula used is: S = a(1-r^n)/(1-r), where S is the total sum of rewards, a is the first term, r is the common ratio (decay rate), and n is the number of terms (levels). Post-condition: The sum of the computed rewards is approximately equal to the specified total_reward, allowing for floating point rounding errors. :param total_reward: The total reward amount to be distributed (float). :param decay_rate: The decay rate for the distribution of rewards (float). :param max_levels: The number of levels in the referral network (int). :return: A list of reward amounts for each level (list of floats). """ # Calculate the first term of the geometric series first_term = (total_reward * (1 - decay_rate)) / (1 - decay_rate ** max_levels) # Generate the list of rewards for each level rewards = [first_term * (decay_rate ** i) for i in range(max_levels)] # The sum of rewards should be close to the total_reward assert abs(sum(rewards) - total_reward) / total_reward < 1e-9, ( "The sum of distributed rewards does not match the total reward." ) return rewards def main(total_reward: float, decay_rate: float, max_levels: int = 10): reward_distribution = distribute(total_reward, decay_rate, max_levels) for level, reward in enumerate(reward_distribution, start=1): print(f"Level {level} primary referrer receives:${reward:.2f}")

if __name__ == "__main__":
main(total_reward=3500, decay_rate=0.5)