#----------------------------------------------------------------------------------
# Module containing methods to perform analytics on an array of numbers
# Methods are: Python has built-in functions:   len(), sum(), min(), max()
#              Python statistics has functions: mean(), median(), mode(), stdev() 
#              normalize(): takes a 1dim array, and eliminates bad/no data  
#              slice():     takes a 2dim array, and returns a row or col slice  
#              transpose(): takes a 2dim array, and transpose it x/y becomes y/x  
#----------------------------------------------------------------------------------
import math                                 #for math.pow() and sqrt()
import statistics                           #for mean(), median(), stdDev()
import sys                                  #for sys.float_info.min float

#--------------------------------------------------------------------------------
# normalize: Returns a new clean array that has all the bad/no data removed 
#--------------------------------------------------------------------------------
def normalize(array):
    array2 = []                                         #create new clean array

    for col in array:                                   #loop through all columns                  
        try:                                        
            num = float(col)                            #convert string to number
            array2.append(num)                          #add to new clean array
        except:
            pass                                        #do nothing (no op)   

    return array2                              

#--------------------------------------------------------------------------------
# Analytics functions
#--------------------------------------------------------------------------------
def count(array):   return len(array)                                                  
#def sum(array):    return sum(array)                                                   
#def min(array):    return min(array)                                                   
#def max(array):    return max(array)
def range(array):   return max(array) - min(array)                                                   

def avg(array):     return statistics.mean(array) 
def median(array):  return statistics.median(array) 
def medianL(array): return statistics.median_low(array) 
def medianH(array): return statistics.median_high(array) 
#def mode(array):   return statistics.mode(array)       #only a single mode, better function below 
def stdDev(array):  return statistics.pstdev(array)     #full population std dev 
def stdDevS(array): return statistics.stdev(array)      #sample standard deviation 

#--------------------------------------------------------------------------------
# median: Returns the median value of an array
#--------------------------------------------------------------------------------
'''
def median(array):
    array2 = sorted(array)                              #sort the array                   
        
    count = len(array2)                                 #count the array

    mid1 = round(count/2)                               #possibly round .5 up
    mid2 = mid1-1

    if (count%2 == 1):                                  #if count is odd
        median = array2[mid1]                           #median= mid point                                                               
    else:                                               #else, if even
        median = (array2[mid1] + array2[mid2]) / 2      #median= average of 2 mid points                                                               

    return median                              
'''

#--------------------------------------------------------------------------------
# mode: Returns the mode value(s) of an array   (algorithm 1)
#--------------------------------------------------------------------------------
def mode(array):

    prevCol   = sys.float_info.min
    prevCount = 0
    maxOccur  = 2
    modes     = []

    array2 = sorted(array)                              #sort the array                   

    for col in array2 :
        if col == prevCol :
            prevCount += 1
        else:
            if prevCount > maxOccur :
                maxOccur = prevCount                    #save the max occurences that far
                modes = []                              #clean up the modes array
            if prevCount >= maxOccur :
                modes.append(prevCol)                   #add it to the modes array
            prevCount = 1
            prevCol  = col
             
    if prevCount > maxOccur :                           #test/process the last record                   
        maxOccur = prevCount                            #save the max occurences thus far 
        modes = []                                      #clean up the modes array
    if prevCount >= maxOccur :
        modes.append(prevCol)                           #add it to the modes array

    return modes                              

#--------------------------------------------------------------------------------
# mode2: Returns the mode value(s) of an array   (algorithm 2)
#--------------------------------------------------------------------------------
def mode2(array):

    maxOccur = 2                                       #set minimum number of occurrence
    modes    = []                                      #create an empty list
    dict     = {}                                      #create a dictionary

    for elem in array:                                 #iterate thru elements
        try:                                           #try:
            dict[elem] += 1                            #add 1 to the element's value
        except:                                        #if unsuccessful, element must not be in dictionary 
            dict[elem]  = 1                            #add the element to dictionary with value 1

    listOfTuples = sorted(dict.items(), key=lambda elem: elem[1], reverse=True)     #sort by highest value 

    for tuple in listOfTuples:                         #loop through the sorted list of tuples
        (elem, occur) = tuple                          #each tuple has element and value
        if occur < maxOccur:                           #if occurrence < maxOccurence
            break                                      #we are done, no need to continue
        modes.append(elem)                             #add the element to the list of modes
        maxOccur = occur                               #save the number of occurence as maxOccurence 
    
    return modes   

#--------------------------------------------------------------------------------
# stdDev: Returns the standard deviation of an array
#         It is a measure of the amount of variation of a set of data values
#         Low stdDev means the values are close to the average (or are tight) 
#         1. take average of array
#         2. take the difference (delta) of each element to the average
#         3. take the square of that delta
#         4. add all those square of deltas
#         5. divide the square of deltas by count of elements
#         6. take the square root of item 5.  
#--------------------------------------------------------------------------------
'''
def stdDev(array):
    counter = count(array)                          #call count method
    total   = sum(array)                            #call built-in sum method
    average = total/counter

    sqDelta = 0;                                    #square of deltas      
    for col in array:                               #loop through all columns                  
        sqDelta += math.pow(col-average,2)          #add to square of delta

    std_dev = math.sqrt(sqDelta/counter)            #square root of average(square of deltas)
    return std_dev                               
'''

#--------------------------------------------------------------------------------
# toString: Returns the array as well as all the analytic computations 
#--------------------------------------------------------------------------------
def toString(array):
    data  = "Data points: "   + str( array )
    data += "\nCount......: " + str( count(array)  ) 
    data += "\nSum........: " + str( sum(array)    ) 
    data += "\nAverage....: " + str( avg(array)    ) 
    data += "\nMedian.....: " + str( median(array) ) 
    data += "\nMode.......: " + str( mode(array)   ) 
    data += "\nMinimum....: " + str( min(array)    ) 
    data += "\nMaximum....: " + str( max(array)    ) 
    data += "\nRange......: " + str( range(array)  ) 
    data += "\nStd.Dev....: " + str( stdDev(array) ) 
    return data                              

#--------------------------------------------------------------------------------
# slice: Takes  a 2 dimensional array, slice type (row/col/all), and index
#        Return a single dimention array for that row or column or all cells
#--------------------------------------------------------------------------------
def slice(array2dim, type, idx):
    size  = 0
    array = []

    if (type =="row"):                                  # ROW slice
        array = array2dim[idx]                              #take a row slice

    if (type =="col"):                                  # COL slice
        obj    = zip(*array2dim)                            #transpose 2dim array, returns an object
        array2 = list(obj)                                  #turn the object into an array
        array  = array2[idx]                                #take a slice 

    if (type =="all"):                                  # ALL slice (turn a 2dim array to 1 dim)
        for row in array2dim:                               #loop through all rows                     
            for col in row:                                 #loop through all columns
                array.append(col)                           #add to new 1dim array

    return array

#--------------------------------------------------------------------------------
# transpose: Transpose a 2 dimensional array, x/y dimension become y/x dimension
#            Return a 2 dimensional transposed array
#--------------------------------------------------------------------------------
def transpose(array2dim):

    obj    = zip(*array2dim)                         #transpose 2dim array, returns an object   
    array2 = list(obj)                               #turn the object to an array

    return array2

#--------------------------------------------------------------------------------