# A Level Statistics Calculator/Helper For A Casio Fx-CG50 Calculator's MicroPython

I have made a program for my calculator's micropython, that can solve various a level statistics questions for me. However due to the limitations of the micropython's standard library, i had to reinvent the wheel on some functions and could not rely on external modules to do the tasks as they don't exist in micropython. I tried to implement everything using mostly pure python. As such, I would like some advice on shortcuts to make my code, more efficient and compact, and if there is an easier way to do a task, it'd be appreciated.

def find_median(lst): # finds the median of a sorted_list
quotient, remainder = divmod(len(lst), 2)
if remainder:
return lst[quotient]
return sum(lst[quotient - 1:quotient + 1]) / 2

def find_mode(listed_data): # finds the mode for listed data
Counter = {value: listed_data.count(value) for value in listed_data}
m = max(Counter.values())
mode = [x for x in set(listed_data) if Counter[x] == m] if m>1 else None
return mode

def interpolation_grouped_data(grouped_data, cumulative_frequencies, position): # responsible for using linear interpolation to find the lower quartile, median, and upper quartile of grouped data
if cumulative_frequencies[0] > position: # if the position of the data required is not in the first interval, then it is between 0 , and the lowest bound in the first interval
mn_cu_freq = 0
mx_cu_freq = cumulative_frequencies[0]
mid_cu_freq = position
interval_index = 0
else:
for index in range(len(cumulative_frequencies) - 1):
if cumulative_frequencies[index+1] > position >= cumulative_frequencies[index]: # if the position is within this interval
mn_cu_freq = cumulative_frequencies[index]
mx_cu_freq = cumulative_frequencies[index + 1]
mid_cu_freq = position
interval_index = index + 1
break
lower_bound, upper_bound = grouped_data[interval_index][0:2]
return interpolation(mn_cu_freq, mid_cu_freq, mx_cu_freq, lower_bound, upper_bound)

def interpolation(mn_cu_freq, mid_cu_freq, mx_cu_freq, lower_bound, upper_bound): # uses interpolation to find the result, cu represents cumulative
result = lower_bound + ( ( (mid_cu_freq - mn_cu_freq)/(mx_cu_freq - mn_cu_freq) ) * (upper_bound - lower_bound) )
return result

def listed_data_stats(listed_data): # for dealing with listed data Ex: 1,2,3,4 or 5,1,4,2,6,7
# sum of data, number of data, mean
sum_x = sum(listed_data)
number_of_data = len(listed_data)
mean = sum_x / number_of_data

# sum of each data squared
sum_x_squared = sum(i**2 for i in listed_data)

# variance, and standard deviation
variance = (sum_x_squared / number_of_data) - mean**2
standard_deviation = round((variance)**0.5, 5)

# data sorted for finding measure of locations
sorted_listed_data = sorted(listed_data)
middle = number_of_data//2

# minimum, and maximum value
minimum = sorted_listed_data[0]
maximum = sorted_listed_data[-1]

# lower quartile, median, upper quartile
LQ_list, Median_list = sorted_listed_data[:middle], sorted_listed_data
UQ_list =  sorted_listed_data[middle:] if number_of_data % 2 == 0 else sorted_listed_data[middle+1:]
lower_quartile = find_median(LQ_list)
median = find_median(Median_list)
upper_quartile = find_median(UQ_list)

# Interquartile Range
interquartile_range = upper_quartile - lower_quartile
Range = sorted_listed_data[-1] - sorted_listed_data[0]

# Outliers
lower_outlier_bound = lower_quartile - (1.5*standard_deviation)
upper_outlier_bound = upper_quartile + (1.5*standard_deviation)

# Skewness
skewness_quantity = (3*(mean-median))/standard_deviation
if skewness_quantity > 0:
skewness = "positive"
elif skewness_quantity < 0:
skewness = "negative"
else:
skewness = "symmetrical"

# mode
mode = find_mode(sorted_listed_data)

return [round(x, 5) if isinstance(x, float) else x for x in (sorted_listed_data, minimum,
maximum, sum_x, sum_x_squared, number_of_data, mean, mode, lower_quartile, median,
upper_quartile, interquartile_range, Range, variance, standard_deviation,
lower_outlier_bound, upper_outlier_bound, skewness, skewness_quantity)]

def continuous_grouped_data_stats(grouped_data): # for dealing with grouped data ex: [[lower bound, upper bound, frequency], [...], [...]] etc. in [[0, 10, 16], [10, 15, 18], [15, 20, 50]] in the first list, 0 and 10 represents the interval 0 -> 10, and 16 is the frequency of numbers in this range
midpoints = []
cumulative_frequencies = []
sum_x = 0
sum_x_squared = 0
number_of_data = 0
if grouped_data[1][0] != grouped_data[0][1]: # if there are gaps in data
gap = (grouped_data[1][0] - grouped_data[0][1])/2
for data in grouped_data:
if data[0] != 0:
data[0] -= gap
data[1] += gap

count = 0
for data in grouped_data:
start_bound = data[0]
end_bound = data[1]
frequency = data[2]
midpoints.append((start_bound + end_bound)/2) # acquires a list of midpoints for the each interval/tuple
current_midpoint = midpoints[count]
number_of_data += frequency # acquires the number of data/ total frequency of all intervals
sum_x += (current_midpoint * frequency) # gets the sum of all midpoints x frequency
sum_x_squared += (current_midpoint**2 * frequency) # gets the sum of all midpoints^2 x frequency
if count == 0: # if it is the first loop, then add the first value of cumulative frequency to the list
cumulative_frequencies.append(frequency)
else: # if it is not, then get the value of the previous cumulative frequency and add to it the frequency of the current data, and append it
cumulative_frequencies.append(cumulative_frequencies[count-1] + frequency)
count += 1

# mean
mean = sum_x / number_of_data

# variance, and standard deviation
variance = (sum_x_squared / number_of_data) - mean**2
standard_deviation = (variance)**0.5

# lower quartile, median, and upper quartile, interquartile range, Range, and outlier
lower_quartile = interpolation_grouped_data(grouped_data, cumulative_frequencies, 0.25 * number_of_data) # performs interpolation to acquire it
median = interpolation_grouped_data(grouped_data, cumulative_frequencies, 0.5 * number_of_data)
upper_quartile = interpolation_grouped_data(grouped_data, cumulative_frequencies, 0.75 * number_of_data)
interquartile_range = upper_quartile - lower_quartile
Range = grouped_data[-1][1] - grouped_data[0][0]
lower_outlier_bound = lower_quartile - (1.5*standard_deviation)
upper_outlier_bound = upper_quartile + (1.5*standard_deviation)

# Skewness
skewness_quantity = (3*(mean-median))/standard_deviation
if skewness_quantity > 0:
skewness = "positive"
elif skewness_quantity < 0:
skewness = "negative"
else:
skewness = "symmetrical"

return [round(x, 5) if isinstance(x, float) else x for x in (sum_x, sum_x_squared, number_of_data, midpoints, cumulative_frequencies,
mean, lower_quartile, median, upper_quartile, interquartile_range,
Range, variance, standard_deviation, lower_outlier_bound,
upper_outlier_bound, skewness, skewness_quantity)]

def discrete_grouped_data_stats(grouped_data):
cumulative_frequencies = []
sum_data = 0
sum_data_squared = 0

sum_x = 0
sum_x_squared = 0
sum_y_squared = 0
number_of_data = 0

count = 0
for data in grouped_data:
value, frequency = data
number_of_data += frequency
sum_data += (value * frequency)
sum_data_squared += (value**2 * frequency)
sum_x += value
sum_x_squared += value**2
sum_y_squared += frequency**2

if count != 0: # if it is not the first loop, then get the value of the previous cumulative frequency and add to it the frequency of the current data, and append it
cumulative_frequencies.append(cumulative_frequencies[count-1] + frequency)
else: # if it is the first loop, then add the first value of cumulative frequency to the list
cumulative_frequencies.append(frequency)
count += 1

# mean
mean = sum_data / number_of_data

# variance, and standard deviation
variance = (sum_data_squared / number_of_data) - mean**2
standard_deviation = variance**0.5

# data sorted for finding measure of locations
sorted_listed_data = []
if all((isinstance(freq[1], int) for freq in grouped_data)):
for value, frequency in grouped_data:
sorted_listed_data.extend([float(value)] * frequency)
sorted_listed_data.sort()
else:
sorted_listed_data = None

if sorted_listed_data: # standard discrete data

# lower quartile, median, upper quartile
middle = number_of_data//2
LQ_list = sorted_listed_data[:middle]
UQ_list =  sorted_listed_data[middle:] if number_of_data % 2 == 0 else sorted_listed_data[middle+1:]
lower_quartile = find_median(LQ_list)
median = find_median(sorted_listed_data)
upper_quartile = find_median(UQ_list)

# Interquartile Range
interquartile_range = upper_quartile - lower_quartile
Range = sorted_listed_data[-1] - sorted_listed_data[0]

# Outliers
lower_outlier_bound = lower_quartile - (1.5*standard_deviation)
upper_outlier_bound = upper_quartile + (1.5*standard_deviation)

# Skewness
skewness_quantity = (3*(mean-median))/standard_deviation
if skewness_quantity > 0:
skewness = "positive"
elif skewness_quantity < 0:
skewness = "negative"
else:
skewness = "symmetrical"

else:  # Path towards regression line related data
cumulative_frequencies = None

# Sxx, Syy, Sxy, Regression Line equation (y = a + bx)
sum_y = number_of_data
sum_xy = sum_data
Sxx = sum_x_squared - ( (sum_x**2)/ count )
Syy = sum_y_squared - ( (sum_y**2)/ count )
Sxy = sum_xy - ((sum_x * sum_y)/ count  )
mean_x = sum_x/count
mean_y = sum_y/count
b = Sxy/Sxx
a = mean_y - b*(mean_x)
regression_line_equation = ['y = {} + {}x'.format(round(a, 5), round(b, 5))]
if not cumulative_frequencies: # if it is regression related, then no Nones
lower_quartile = upper_quartile = interquartile_range = lower_outlier_bound = upper_outlier_bound = None
sum_data = sum_data_squared = number_of_data = mean = skewness = skewness_quantity = median = Range = None

# Product Moment Coefficient
product_momentum_correlation_coefficient = Sxy/(Sxx * Syy)**0.5

return [round(x, 5) if isinstance(x, float) else x for x in (sum_data, sum_data_squared, number_of_data, cumulative_frequencies,
mean, lower_quartile, median, upper_quartile,
interquartile_range, Range, variance, standard_deviation,
lower_outlier_bound, upper_outlier_bound, skewness,
skewness_quantity, count, sum_x, sum_x_squared, sum_y, sum_y_squared, sum_xy, mean_x,
mean_y, Sxx, Syy, Sxy, b, a, regression_line_equation,
product_momentum_correlation_coefficient)]

def check_type(x):
if isinstance(x, float): # if type is list, do not convert to int
return str(int(x)) if x % 1 == 0 else str(x)
elif isinstance(x, list):
if isinstance(x[0], float):
return str([int(x[i]) if x[i] % 1 == 0 else x[i] for i in range(len(x))])
return str(x)

def print_stats(results_names, results):
print("", *(results_names[i] + " = " + check_type(results[i]) for i in range(len(results_names))), sep='\n')

def linear_interpolation(): # a
variables = [None] * 5 # values to be inputted for interpolation
variables_names = ["mn_cu_freq", "mid_cu_freq", "mx_cu_freq", "lower_bound", "upper_bound"]
for index in range(5):
variables[index] = float(input("{}: ".format(variables_names[index])))
print("x = ", interpolation(*variables))

def listed_data_statistics(): # b
listed_data = []
value = input("Enter Values: ")
while value != 'x':
value = float(value)
listed_data.append(value)
value = input("Enter Values: ")
results = listed_data_stats(listed_data) # for concatonation
results_names = ('Sorted_Data', 'Minimum', 'Maximum', 'Sum_x', 'Sum_x^2', 'n', 'Mean', 'Mode', 'Lower Quartile',
'Median', 'Upper Quartile', 'IQR', 'Range', 'Variance', 'Standard Deviation',
'Lower Outlier', 'Upper Outlier', 'Skewness', 'Skewness Value')
print_stats(results_names, results)

def continuous_grouped_data_statistics(): # c
grouped_data = []
while True:
start_boundary = input("Start Bound: ")
if start_boundary == "x": # enter x when no more data available
break
end_boundary = input("End Bound: ")
frequency = input("Frequency: ")
grouped_data.append([float(start_boundary), float(end_boundary), int(frequency)]) # each row in the grouped data is a list
results = continuous_grouped_data_stats(grouped_data)
results_names = ('Sum_x', 'Sum_x^2', 'n', 'Midpoints', 'Cum. Freq', 'Mean', 'Lower Quartile',
'Median', 'Upper Quartile', 'IQR', 'Range', 'Variance', 'Standard Deviation',
'Lower Outlier', 'Upper Outlier', 'Skewness', 'Skewness Value')
print_stats(results_names, results)

def discrete_grouped_data_statistics(): # d
grouped_data = []
while True:
value = input("Value: ")
if value == "x":
break
frequency = input("Frequency: ")
grouped_data.append([float(value), (int(frequency) if float(frequency) % 1 == 0 else float(frequency))])
results = discrete_grouped_data_stats(grouped_data)
results_names = ('Sum', 'Sum^2', 'n', 'Cum. Freq', 'Mean', 'Lower Quartile',
'Median', 'Upper Quartile', 'IQR', 'Range', 'Variance', 'Standard Deviation',
'Lower Outlier', 'Upper Outlier', 'Skewness', 'Skewness Value', 'Sample_n', 'Sum_x', 'Sum_x^2', 'Sum_y',
'Sum_y^2', 'Sum_xy', 'Mean_x', 'Mean_y', 'Sxx', 'Syy', 'Sxy', 'b', 'a', 'Reg. Eq', 'Prod. Momen. Coeff')
print_stats(results_names, results)

def coded_data_discrete_output(grouped_data, prompt_index):
prompts = ["-- With Coding --", '-- Without Coding --']
print(prompts[prompt_index])
results = discrete_grouped_data_stats(grouped_data)
results_names = ('Sum', 'Sum^2', 'n', 'Cum. Freq', 'Mean', 'Lower Quartile',
'Median', 'Upper Quartile', 'IQR', 'Range', 'Variance', 'Standard Deviation',
'Lower Outlier', 'Upper Outlier', 'Skewness', 'Skewness Value', 'Sample_n', 'Sum_x', 'Sum_x^2', 'Sum_y',
'Sum_y^2', 'Sum_xy', 'Mean_x', 'Mean_y', 'Sxx', 'Syy', 'Sxy', 'b', 'a', 'Reg. Eq', 'Prod. Momen. Coeff')
print_stats(results_names, results)

def histogram_calculator(): # e
names = ["Freq. 1 : ", "ClassWidth 1 : ", "Freq. 2 : ", "ClassWidth 2 : ", "Height 1 : ", "Width 1 : "]
Frequency_1, Class_Width_1, Frequency_2, Class_Width_2, Height_1, Width_1 = [float(input(prompt)) for prompt in names]

Freq_Dens_1 = Frequency_1/Class_Width_1
Freq_Dens_2 = Frequency_2/Class_Width_2
Width_2 = (Class_Width_2*Width_1)/Class_Width_1
Height_2 = (Freq_Dens_2*Height_1)/Freq_Dens_1
print("", "Other Width = " + str(Width_2), "Other Height = " + str(Height_2), sep="\n")

def code_data(): # f
# codes x and y data
x_lst = []
y_lst = []
count = 2
x = input("X1: ")
y = input("Y1: ")
while x != 'x' and y != 'x':
x_lst.append(x)
y_lst.append(y)
x = input("X{}: ".format(count))
y = input("Y{}: ".format(count))
count += 1

x_lst = list(map(float, x_lst))
y_lst = list(map(float, y_lst))
original_data = list(zip(x_lst, y_lst))

choices = {'+': lambda n1, n2: n1+n2,
'-': lambda n1, n2: n1-n2,
'*': lambda n1, n2: n1*n2,
'/': lambda n1, n2: n1/n2}

prompts = ["Enter Operation: ", "Enter Value: "]

x_operations = []
y_operations = []
count = 0
print("\nCoding X values - - - -")
# coding x
coding = input(prompts[0])
while coding != 'x':
count += 1
x_operations.append(coding)
coding = input(prompts[count%2])

count = 0
print("\nCoding Y values - - - -")
# coding y
coding = input(prompts[0])
while coding != 'x':
count += 1
y_operations.append(coding)
coding = input(prompts[count%2])

# coding elements in x and y lsts
for i in range(0, len(x_operations), 2):
number = float(x_operations[i+1])
for j in range(0, len(x_lst)):
x_lst[j] = choices[x_operations[i]](x_lst[j], number)
x_lst[j] = int(x_lst[j]) if x_lst[j] % 1 == 0 else float(x_lst[j])

for i in range(0, len(y_operations), 2):
number = float(y_operations[i+1])
for j in range(0, len(y_lst)):
y_lst[j] = choices[y_operations[i]](y_lst[j], number)
y_lst[j] = int(y_lst[j]) if y_lst[j] % 1 == 0 else float(y_lst[j])

coded_data = list(zip(x_lst, y_lst))
print("Coded X: {}".format(x_lst))
print("Coded Y: {}\n".format(y_lst))

d = {'x': coded_data_discrete_output}
c = input("Stats?: x=yes: ")
choice = d.get(c, lambda a, b: None)(coded_data, 0)
if c == 'x':
print("\n")
coded_data_discrete_output(original_data, 1)

def normal_distribution():
"""
Acquires a, given x [and y], for a standard Normal Distribution of mean 0, and standard deviation 1
1) P(Z < x) = a
2) P(Z > x) = a
3) P(x < Z < y) = a
4) P(Z < a) = x
5) P(Z > a) = x
6) P(-a < x < a) = x
"""
from math import sqrt, exp
mean = 0
standard_dev = 1
percentage_points = {0.5000: 0.0000, 0.4000: 0.2533, 0.3000: 0.5244, 0.2000: 0.8416, 0.1000: 1.2816, 0.0500: 1.6440, 0.0250: 1.9600, 0.0100: 2.3263, 0.0050: 2.5758, 0.0010: 3.0902, 0.0005: 3.2905}

def erf(x):
"""
python implementation of math.erf() as it is not available in micropython
"""
# save the sign of x
sign = 1 if x >= 0 else -1
x = abs(x)

# constants
a1 =  0.254829592
a2 = -0.284496736
a3 =  1.421413741
a4 = -1.453152027
a5 =  1.061405429
p  =  0.3275911

# A&S formula 7.1.26
t = 1.0/(1.0 + p*x)
y = 1.0 - (((((a5*t + a4)*t) + a3)*t + a2)*t + a1)*t*exp(-x*x)
return sign*y # erf(-x) = -erf(x)

def get_z_less_than(x=None, digits=4):
"""
P(Z < x) = a
"""
if x is None:
x = float(input("Enter x: "))

res = 0.5 * (1 + erf((x - mean) / sqrt(2 * standard_dev ** 2)))
return round(res, digits)

def get_z_greater_than(x=None):
"""
P(Z > x) = a
"""
if x is None:
x = float(input("Enter x: "))

return round(1 - get_z_less_than(x), 4)

def get_z_in_range(lower_bound=None, upper_bound=None):
"""
P(lower_bound < Z < upper_bound) =
"""
if lower_bound is None and upper_bound is None:
lower_bound = float(input("Enter lower_bound: "))
upper_bound = float(input("Enter upper_bound: "))

return round(get_z_less_than(upper_bound) - get_z_less_than(lower_bound), 4)

def get_z_less_than_a_equal(x=None, digits=4, round_=2):
"""
P(Z < a) = x
"""
if x is None:
x = float(input("Enter x: "))

if x <= 0.0 or x >= 1.0:
raise ValueError("x must be >0.0 and <1.0")
min_res, max_res = -10, 10
while max_res - min_res > 10 ** -(digits * 2):
mid = (max_res + min_res) / 2
if get_z_less_than(mid, digits*2) < x:
min_res = mid
else:
max_res = mid
return round((max_res + min_res) / 2, round_)

def get_z_greater_than_a_equal(x=None):
"""
P(Z > a) = x
"""
if x is None:
x = float(input("Enter x: "))

if x in percentage_points:
return percentage_points[x]
else:
return get_z_less_than_a_equal(1-x)

def get_z_in_range_a_b_equal(x=None):
"""
P(-a < Z < a) = x
acquires a
"""
if x is None:
x = float(input("Enter x: "))

return get_z_less_than_a_equal(0.5 + x/2, 4, 4)

norm_choices = {'1': get_z_less_than,
'2': get_z_greater_than,
'3': get_z_in_range,
'4': get_z_less_than_a_equal,
'5': get_z_greater_than_a_equal,
'6': get_z_in_range_a_b_equal}

option = input("1: P(Z < x) = a\n2: P(Z > x) = a\n3: P(-x < Z < x) = a\n4: P(Z < a) = x\n5: P(Z > a) = x\n6: P(-a < Z < a) = x\n: ")

# if not a valid option, then do nothing and naturally exit
print(norm_choices.get(option, lambda: None)())
again = input("Try again? 1 = Yes\n: ")
if again == '1':
normal_distribution()

def statistics(): # checks for what you want
choices = {'1': linear_interpolation,
'2': listed_data_statistics,
'3': continuous_grouped_data_statistics,
'4': discrete_grouped_data_statistics,
'5': histogram_calculator,
'6': code_data,
'7': normal_distribution}
choice = input("1: Interpolation\n2: Listed Data\n3: Continuous Data\n4: Discrete Data\n5: Histogram\n6: Code Data\n7: Norm_Dist : ")
choices.get(choice, lambda: None)()

statistics()


As a disclaimer, I'm not familiar with either micropython, nor the calculator hardware that it will run on. I can only give advice on the Python code itself in isolation.

def find_median(lst): # finds the median of a sorted_list
quotient, remainder = divmod(len(lst), 2)
if remainder:
return lst[quotient]
return sum(lst[quotient - 1:quotient + 1]) / 2


There is a pretty big oversight in this code. It does not check if the sequence is sorted, nor does it sort it. You'll need to do one of the two.

Quotient and remainder, while accurate, are not particularly communicative names. Why do you why the quotient and remainder? You could try something like half_len and has_odd_len.

Since you know there will be exactly two values to sum up, I'd say stick with the simple lst[quotient - 1] + lst[quotient]

If I were to be picky

• find_median could be simply median
• # finds the median of a sorted_list seems like it is a docstring without the conventional triple quotes.
• lst is not a great name. The statistics module tends to go with data, which I think is a better choice.

def median(data): """Get the median of a sorted list""" if not is_sorted(data): raise ValueError("The data must be sorted")

half_len, has_odd_len = divmod(len(data), 2)
if has_odd_len:
return data[half_len]
return (data[half_len - 1] + data[half_len]) / 2


def find_mode(listed_data): # finds the mode for listed data
Counter = {value: listed_data.count(value) for value in listed_data}
m = max(Counter.values())
mode = [x for x in set(listed_data) if Counter[x] == m] if m>1 else None
return mode


You have an implicit O(n2) time complexity in this function (with n being the length of the list). listed_data.count(value) takes up to O(n) time as it needs to check every element. This counting is done O(n) times. You can fix this by implementing your own mini collections.Counter with a dict.

Making a set out of listed_data is unnecessary, the keys in the Counter dict are already the set you want. I would change the list comprehension to use the dict as it has all the information you need.

If were are re-implementing Python's statistics, this seems more like multimode than mode, since it may return multiple elements.

In a list with just one element, this unexpectedly returns None. I think you need a few tests to see if everything is actually working as expected. I've left the behaviour alone in the sample code below.

Again being picky, don't have any variables start with uppercase. That is usually an indicator that this is the name of a class.

def mode(data):
"""Find the mode(s) of the data.
A mode is any value which occurs the most number of times.
"""
counter = dict()
for value in data:
if value not in counter:
counter[value] = 0
counter[value] += 1

m = max(counter.values())
if m <= 1:
return None

return [x for x, occurance in counter.items() if occurance == m]


def listed_data_stats(listed_data): # for dealing with listed data Ex: 1,2,3,4 or 5,1,4,2,6,7
# sum of data, number of data, mean
sum_x = sum(listed_data)
number_of_data = len(listed_data)
mean = sum_x / number_of_data

# sum of each data squared
sum_x_squared = sum(i**2 for i in listed_data)

# variance, and standard deviation
variance = (sum_x_squared / number_of_data) - mean**2
standard_deviation = round((variance)**0.5, 5)

# data sorted for finding measure of locations
sorted_listed_data = sorted(listed_data)
middle = number_of_data//2

# minimum, and maximum value
minimum = sorted_listed_data[0]
maximum = sorted_listed_data[-1]


You can make a small improvement to the functionality of this code by computing the sorted list of data first. This will allow you to compute the stats for one time iterators (you can only iterate over them once, a.k.a. one call to len, sum, etc).

Comments like # sum of data, number of data, mean don't really add much to the code. I can see you've computed the sum of the data, the size of it, and it's mean, but I still don't know why you want these. If the comment is purely descriptive of the code, it probably isn't worth keeping.

return [round(x, 5) if isinstance(x, float) else x for x in (sorted_listed_data, minimum,
maximum, sum_x, sum_x_squared, number_of_data, mean, mode, lower_quartile, median,
upper_quartile, interquartile_range, Range, variance, standard_deviation,
lower_outlier_bound, upper_outlier_bound, skewness, skewness_quantity)]


This is a lot of data to return as a tuple. Without a good comment in the docstring, it will be rather cumbersome for the user of this function to figure out which position in the list corresponds to which statistic. This is problematic as this is the only place in the whole function that will give them this information, and it is not easy to use. Consider making a class with attributes, a dictionary with easy to use key value pairs (e.g. {"skewness": skewness}), or splitting this up into multiple functions and letting the user decide which statistics they want.

Some other things to consider are

• Which the functions will react poorly to being fed an empty list of data? Or a very long list of data? It is worth writing down these tests and running them after each code change.
• Try running the code through pylint, flake8, pep8, or another linter. It will point out a fair number of small problems with styling, especially with weird spacing. Don't take the results too seriously, they are useful to get code into shape for when other people will look at the code.
• There are a few places with hardcoded precision values that might be nicer as positional parameters, or a global constant, so that they can be changed later.
• +1 oh i should've considered using a dictionary instead of a tuple to represent this
– user227128
Jul 22, 2020 at 17:25