Price Discrimination in Daily Fantasy Sports Contests (using Python)
By: Lloyd Danzig
Tags: Sports, Sports Analytics, Daily Fantasy Sports, Python, Sports Betting Tips

According to this formulation, we have collected $45 in entry fees but left $30 of upside on the table. It makes sense that $30+$45=$75, which is also the sum of each participants maximum desired wager.

Coding Lesson

If computers are good at one thing, it’s…computation. In order to attempt to perfectly price discriminate, and allow each participant in a DFS contest to wager any amount they’d like, we will borrow inspiration from the way poker games handle side pots. However, this will require a bit more computation than we’d like to track ourselves, so we will use some very basic Python code to speed things up.

If you’re not familiar with basic coding principles, and the fundamentals of data structures like arrays, this may be a bit difficult to follow. However, the interactive nature of the exercise should be instructive.

To allow people to follow along and try some of the code themselves, we will use Google Colaboratory. Anyone can view the code here, but a login will be required to make a copy of the notebook and then run/edit it.

Step 1

First, we will need to import the NumPy library, as we wish to use some of its functions, specifically to generate random integers. We are also going to make an assumption that the first element in the array corresponds to the desired wager of the 1st place participant, the second element of the array corresponds to the desired wager of the 2nd placer participant, and so on.

Instead of the five specific participants mentioned earlier, we are going to use some randomness here. Specifically, we will create an array named customerRiskArray and populate it with random integers. NumPy offers a function random.randint() that will generate random integers according to the parameters that we pass it. In our case, we passed three parameters when we made the call customerRiskArray = np.random.randint(1,20,5)*5

This is instructing the machine to generate values according to the following specifications:

Lower Bound = 1

Upper Bound = 20

Numer of Integers to Generate = 5

Multiplying by 5 after populating the array will result in the range scaling to one with a lower bound of 5 and an upper bound of 100. Also, each number will, necessarily, be a multiple of 5, which provides us with nice round numbers.

We could have also used customerRiskArray = np.random.randint(1,100,5), but the numbers would not have been as round and easy to sanity check mentally.

Of course, to use a script like this in production, we would want to take input from the user and dynamically be able to accommodate contests of any size.

We are going to need to reference this array throughout our computation process, but also will want to manipulate it in ways that serve our end goal. As such, we will create an identical copy called tempFundArray.

Finally, we will print both arrays to ensure that they each contain 5 integers between 5 and 100, inclusive, and are identical to one another.

Step 2

We are now going to get some quick practice using for loops in Python and also re-verifying that our array was properly populated.

Step 3

We are going to need to track the “payments” made between customers, borrowing from an accounting concept known as double entry bookkeeping.

Essentially, we are going to run through every 2-person matchup within our 5-person contest pool, do some computation, and only at the end will funds actually be transferred to winners. The use of payment is meant more in the sense of an unrealized gain or loss, as it is merely taking place temporarily and on paper.

We can use len(array_name) for any array, array_name, in order to return the number of elements it contains. We can then populate our two new arrays with that many 0’s, and print each of them to ensure success.

Step 4

Now comes the fun part, which we will need to further dissect line-by-line.

Step 4a

Anyone with a coding background will likely recall the way in which nested loops are utilized to compare and sort elements within arrays. As arrays in Python start at 0 and we’ve stated that the first element of the array contains the desired wager of the highest ranking participant at the end of the contest, we will set highRank = 0, so that our first comparison begins with the 1st place participant. We will set lowRank = 1, so that this comparison begins against the 2nd place participant. Again, these values only represent the index or location within an array that we seek to compare information to. As such, when we increment and decrement these two values, we are simply moving around within our arrays and comparing different sets of information.

Step 4b

Though a for() loop might have been the standard practice in Java, Python’s version is more equivalent to a forEach() loop, so while() will be more useful. It is also because we are using a while() loop that we declared and set our indexes before and outside of the loop’s declaration.

Our highRank variable is intended to always track information pertaining to the participant, when comparing two in head-to-head fashion, that placed higher in the final standings. In a 5-participant contest, this will mean it is pointing to array elements pertaining to the 1st, 2nd, 3rd, and 4th place participants. It cannot possibly point to the 5th place participant, because there is no lower-ranked entry to compare to.

So, we know that we will want to be able to access the first element of any given array, and now see that we will never have to access beyond the second-to-last element of any given array. We have set highRank = 0, and want to ensure that we cease incrementing it once it points to the penultimate array element.

One might think we can just use the length of the array, len(customerRiskArray), to accomplish this. If the array has 5 elements, its length will be 5. So, as long as highRank is pointing to some element before the 5th, it will never point to the final element.

However, because the array index starts at 0, we must subtract 1, and have our loop run while(highRank < len(customerRiskArray) - 1).

Step 4c

Before we start moving our pointer to the array elements relating to the top performers, let’s iterate through our inner loop, which will move the pointer to the elements relating to the lower performers.

We use similar logic to set our loop conditions. Now, when this code runs on the first loop, it will be comparing information about the 1st place participant against the 2nd place participant, as well as the 1st place participant against all others, before resetting and starting comparisons wherein the 2nd place participant is the higher-ranking performer.

Step 4d

We now go into one of our two conditional computation instruction sets, depending on where we are in the process. We can see the comparison being conducted is if(tempFundArray[highRank] >= tempFundArray[lowRank]):

We recall that tempFundArray is simply a copy of customerRiskArray, which holds the list of desired wagers of each participant. Thus, on the first loop, this condition will be true if the value in tempFundArray located at index 0 is greater than the value located at index 1.

In other words, if will be true if the 1st-place participant desired to wager more than the 2nd-place participant. We can generalize this to describe the comparison that will be conducted repeatedly. However, tempFundArray will be altered through the course of our code running, so the English description will not hold perfectly.

We see that three operations are to be conducted if the condition holds:

customerCollectionArray[highRank] += tempFundArray[lowRank] customerPaymentArray[lowRank] += tempFundArray[lowRank] tempFundArray[lowRank] = 0

Which can be translated as follows:

The array which tracks collections, at the position referencing the higher-ranking participant, will be incremented by the amount remaining in the location of the temporary array that applies to the participant with the lower rank. The array which tracks payments, at the position referencing the lower-ranking participant, will be incremented by the amount remaining in the location of the temporary array that applies to the participant with the lower rank. The temporary array, at the location that applies to the participant with the lower rank, will be set to 0.

Step 4e

If the conditional fails, that means that the participant with the lower ranking actually has a greater value remaining in tempFundArray than that of the higher-ranking participant.

When this is the case, we see the following three operations take place:

customerCollectionArray[highRank] += tempFundArray[highRank] customerPaymentArray[lowRank] += tempFundArray[highRank] tempFundArray[lowRank] -= tempFundArray[highRank]

Which can be translated as follows:

The array which tracks collections, at the position referencing the higher-ranking participant, will be incremented by the amount remaining in the location of the temporary array that applies to the participant with the higher rank. The array which tracks payments, at the position referencing the lower-ranking participant, will be incremented by the amount remaining in the location of the temporary array that applies to the participant with the higher rank. The temporary array, at the location that applies to the participant with the lower rank, will be decremented by the amount contained at the location that applies to the participant with the higher rank.

Step 4f

We use a larger screenshot just to recall where we are in this portion of the notebook.

On the first run of this code, we will get to this point once we compared the 1st place array element to the 2nd place array element. We know want to compare the 1st place element to that of 3rd-place, then 4th, and then 5th. To this end, we increment lowRank and then go back to test the inner while() loop condition.

That condition required that lowRank <= len(customerRiskArray) - 1. We know that lowRank was initialized to equal 1 and has now been incremented to 2. We also know that the array is 5 elements long. Thus, we are testing that 2 ≤(5–1). This will evaluate to true, and the inner loop will run again.

Once this condition fails, the loop will exit, and reach our final set of incrementers.

Step 4g

Once we’ve done these comparisons using the 1st-place participant, we will also need to conduct them using the 2nd-place participant as the one with the higher rank. Additionally, we need to reset lowRank so that it is looking one element to the right of wherever highRank is now pointing.

Step 5

As the final step, we will use two for() loops to iterate through the collections and payment arrays, so that we can print out the payment instructions our code is providing, and then conduct a sanity check.

Optimizations & Refactoring

Readers who found the above lesson to be rudimentary may also have noticed that this code is far from being optimal. Rather, there are a number of approaches to refactoring that would offer benefits. Certainly, there are Python functions and libraries that would help us clean this code up and make it more elegant. However, it will suffice for the purposes of this exercise.

“Wait, I’m confused.”

Those who found everything discussed to be perfectly intuitive can skip this section, but for those who don’t exactly understand what happened here, let’s step into our code and go through the entirety of the runtime experience, line-by-line.

Code Review

When we first start to manipulate our inputs, we have two identical, but not immutable copies of the array containing each customer’s desired risk amount.