Is it Random Enough?
Many plugins show one of many, whether it be quotes, jokes, totes, or photes [sic]. The freshness of the data depends on randomly selecting one of the possibilities, either from an API or from a set of data stored in a JSON array or object. Thus, early on we added a help document to inform developers how to get a random number with Liquid, the markup language for our plugins.
Hypothesis
When a playlist item is on a regular interval, it creates a pattern in a randomization based on server time.
TL;DR
We were wrong, both methods are sufficiently random across a sample of 1,000 data points. Human bias towards patterns was to blame.
After creating a plugin with ~120 questions in a static array, the plugin was set to randomly select one question using the nanoseconds value of the current time, with Liquid's date filter:
{% assign random_number = "now" | date: "%N" | modulo: 120 %}After a week of watching the questions come up, there were some questions that seemed to never appear and some that appeared much more frequently than others. Obviously it could have just been luck based on when I was looking, but to be sure there wasn't a flaw in the system, a test was created.
Storing the Number
Using an old trick called a tracking pixel, much like email and websites have previously used to register that a page or email was loaded, a small script was setup that would accomplish 3 tasks:
- Return a transparent 1x1 pixel image
- Capture the random value passed in a query string parameter
- Store the value, timestamp, and IP address in a CSV file
By adding this script link to the test plugin's Shared code as an <img> tag, we could capture and display the random number generated not only on the screen of the test TRMNL OG, but also transmit that value to the script on an external site.
Running the Test... Twice
Setting the test plugin to run every 5 minutes as the only item on the playlist on a single TRMNL OG, meant that within 4 days we would have 1,000+ data points, enough to do a cursory analysis of the randomization.
Since I didn't want to write a bunch of Python code myself, AI was used to spit out a few more charts:
(This last one is a favorite because it leans into human pattern recognition. It's a statistical "Where's Waldo" for finding a problem.)
Diagnostic | Result | Interpretation |
|---|---|---|
Mean | 50.572 | Very close to the uniform expectation of 50.5; p = 0.937 |
Median | 50.5 | Exactly centered between the two middle values |
Sample standard deviation | 29.161 | Close to the uniform expectation of 28.866 |
Ten value bands | 81–118 per band | No significant deviation; p = 0.573 |
Twenty value bands | — | No significant deviation; p = 0.470 |
Last digits 0–9 | 88–110 per digit | No significant deviation; p = 0.864 |
Even versus odd | 520 versus 480 | No significant deviation; p = 0.217 |
Values 51–100 versus 1–50 | 500 versus 500 | Perfect balance in this sample |
Meanwhile...
Ryan, TRMNL's founder, took a customer focused look at the problem and thought, "This method sucks." A short while later, random_number was born as a Liquid filter, built on Ruby's own randomization.
This took a chunk of code:
{%liquid
assign rand_index = "now" | date: "%N" | modulo: my_collection.size
assign rand_item = my_collection[rand_index]
%}and turned it into this:
{%liquid
assign rand_index = "" | random_number: my_collection.size
assign rand_item = my_collection[rand_index]
%}You're thinking, that's not that different. Except... this is what it looks like when calling an API and wanted to just get a random_number in a range:
https://some-api.com/jokes/{{ "" | random_number: 1, 1000 }}Back to Testing
With a new method in hand, it was time to update the test plugin and repeat the test. Other than a bug fix discovered because of this test, not much to report other that the results.
Narrator: They were very normal and boring.
Diagnostic | New dataset, first 1,000 | Interpretation |
|---|---|---|
Mean | 49.051 | Somewhat below 50.5, but not significant; p = 0.112 |
Median | 49.5 | Near the center |
Sample standard deviation | 28.831 | Very close to the uniform expectation of 28.866 |
Values 1–50 versus 51–100 | 506 versus 494 | No significant imbalance; p = 0.728 |
Even versus odd | 493 versus 507 | No significant imbalance; p = 0.681 |
Ten value bands | 81–115 per band | Compatible with uniformity; p = 0.241 |
Twenty value bands | — | Compatible with uniformity; p = 0.539 |
Last digits | 75–111 per digit | Compatible with uniformity; p = 0.163 |
Distinct values | 100 of 100 | All possible values appeared |
Conclusion
We had an assumption, we tested that assumption, and in the end it improved the quality of life for our plugin developers. It's not a groundbreaking result, but hopefully an interesting detour.
Also, we learned that Mario was wrong, which was valuable, but not very reassuring.