
# runs.py
# Runs test of randomnes 
# https://www.geeksforgeeks.org/runs-test-of-randomness-in-python/

'''
A run is a series of increasing values or decreasing values. The number of increasing, or decreasing, values is the length of the run.

In our case, the values above the median are treated as positive and values below the median as negative. A run is defined as a series of consecutive positive or negative values.

The standard reference is Knuth, "The Art of Computer Programming", Volume 2, Seminumerical Algorithms. In addition to theoretical discussion, he presents tests including frequency, serial, gap, poker, coupon collector's, permutation, run, maximum-of-t, collision, birthday spacings, and serial correlation. 

'''

import random, math, statistics 


def runsTest(nums): 
  med= statistics.median(nums)
  
  # Checking for start of new run 
  numRuns, n1, n2 = 0, 0, 0
  for i in range(len(nums)): 
    if ((nums[i] >= med) and (nums[i-1] < med)) or \
          ((nums[i] < med) and (nums[i-1] >= med)): 
      numRuns += 1
    
    if nums[i] >= med:  # no. of positive values
      n1 += 1
    else:   # no. of negative values 
      n2 += 1

  runs_exp = ((2*n1*n2)/(n1+n2))+1

  stanDev = math.sqrt((2*n1*n2*(2*n1*n2-n1-n2)) / \
                      (((n1+n2)**2)*(n1+n2-1))) 

  z = abs((numRuns-runs_exp)/stanDev)
  return z 
  

# --------- main ---------

nums = [] 
for i in range(100): 
  nums.append(random.random()) 
  
print(f"Z-statistic = {runsTest(nums):.4f}")

