Compare commits

...
10 Commits
16 changed files with 466 additions and 74 deletions

No files matched your search

+1 -1
View File
@@ -9,7 +9,7 @@ insert_final_newline = true
charset = utf-8 charset = utf-8
[*.py] [*.py]
indent_style = tab indent_style = space
indent_size = 4 indent_size = 4
[Makefile] [Makefile]
+1 -1
View File
@@ -7,7 +7,7 @@ save:
pip freeze > requirements.txt pip freeze > requirements.txt
run: run:
python src/main.py python src/main.py $(MODULE)
test: test:
python -m pytest -v python -m pytest -v
+7
View File
@@ -2,3 +2,10 @@ Python by example
================= =================
The intention of this repository is to teach me the basics, along with the best or common practiced, of python development. The intention of this repository is to teach me the basics, along with the best or common practiced, of python development.
Modules must include a "run" function in order to be executed as a dynamic import.
To run
======
`$ make MODULE="module submodule" run`
+2
View File
@@ -1,8 +1,10 @@
iniconfig==2.1.0 iniconfig==2.1.0
mpmath==1.3.0 mpmath==1.3.0
narwhals==2.25.0
numpy==2.3.3 numpy==2.3.3
packaging==25.0 packaging==25.0
pandas==2.3.3 pandas==2.3.3
plotly==6.9.0
pluggy==1.6.0 pluggy==1.6.0
Pygments==2.19.2 Pygments==2.19.2
pytest==8.4.2 pytest==8.4.2
+13 -18
View File
@@ -1,20 +1,15 @@
from sympy import diff, limit, oo, symbols import sys
import unittest
from modules.math import ( if __name__ == "__main__":
test_math_module if sys.argv.__len__() < 2:
) print("[ERROR] Module name must be specified as an argument:")
from modules.probability import ( print(" $ python src/main.py <module_name>")
test_probability_module exit(-1)
)
from modules.statistics import (
test_statistics_module
)
from modules.strings import t_strings
if __name__=="__main__": module_name = sys.argv[1]
# t_strings() module_path = "modules."+module_name
# test_math_module() if sys.argv.__len__() > 2:
# test_probability_module() for submodule_name in sys.argv[2:]:
# test_statistics_module() module_path += "."+submodule_name
# test_exercises_module() module_selected = __import__(module_path, fromlist=[""])
module_selected.run()
+2
View File
@@ -0,0 +1,2 @@
def run():
print("The server will start here")
@@ -1,6 +1,6 @@
## This module represents the first chapter of the book ## This module represents the first chapter of the book
## "Essential Math for Data Science" - Thomas Nield ## "Essential Math for Data Science" - Thomas Nield
## Chaper 1 - Basic Math and Calculus Review ## Chaper 1 - Basic Math and Calculus Review
from cmath import log as complex_log # used for complex numbers from cmath import log as complex_log # used for complex numbers
from math import e, exp, log from math import e, exp, log
@@ -90,7 +90,7 @@ def t_approximate_integral(f, init, end, precission):
def t_calculate_integral(f, init, end, symbol): def t_calculate_integral(f, init, end, symbol):
return integrate(f, (symbol, init, end)) return integrate(f, (symbol, init, end))
def test_math_module(): def run():
print("=== Math module ===") print("=== Math module ===")
t_exponent(2,8) t_exponent(2,8)
print(t_compound_interest(100, 20 / 100, 2, 12)) print(t_compound_interest(100, 20 / 100, 2, 12))
@@ -1,6 +1,6 @@
## This module represents the second chapter of the book ## This module represents the second chapter of the book
## "Essential Math for Data Science" - Thomas Nield ## "Essential Math for Data Science" - Thomas Nield
## Chapter 2 - Probability ## Chapter 2 - Probability
from scipy.stats import binom, beta from scipy.stats import binom, beta
from math import factorial from math import factorial
@@ -31,7 +31,7 @@ class BinomialDistribution:
# For each number calc the probability of that exact number of outcomes (no order) # For each number calc the probability of that exact number of outcomes (no order)
for k in range(n + 1): for k in range(n + 1):
# 1. Simple combinatory with the binomial coeficient (combinations of k elements out of a pool of n without repetition without order) # 1. Simple combinatory with the binomial coeficient (combinations of k elements out of a pool of n without repetition without order)
combinatory = BinomialDistribution.binomial_coeficient(n, k) combinatory = BinomialDistribution.binomial_coeficient(n, k)
# 2. Probability of success, the probability of making it k times # 2. Probability of success, the probability of making it k times
probability_of_success = p ** k probability_of_success = p ** k
# 3. Probability of failure, inverse of the success # 3. Probability of failure, inverse of the success
@@ -44,14 +44,14 @@ class BinomialDistribution:
# > Returns a continuous function, so the final probability of X or better must be calculated using integrals (the area under the curve) # > Returns a continuous function, so the final probability of X or better must be calculated using integrals (the area under the curve)
class BetaDistribution: class BetaDistribution:
@staticmethod @staticmethod
def calc(probability, success_count, failure_count): def calc(probability, success_count, failure_count):
# Only calcs the rpbability to the left # Only calcs the rpbability to the left
return beta.cdf(probability, success_count, failure_count) return beta.cdf(probability, success_count, failure_count)
@staticmethod @staticmethod
def calc_right(probability, success_count, failure_count): def calc_right(probability, success_count, failure_count):
return 1.0 - BetaDistribution.calc(probability, success_count, failure_count) return 1.0 - BetaDistribution.calc(probability, success_count, failure_count)
@staticmethod @staticmethod
def calc_region(init_probability, end_probability, success_count, failure_count): def calc_region(init_probability, end_probability, success_count, failure_count):
return BetaDistribution.calc(end_probability, success_count, failure_count) - BetaDistribution.calc(init_probability, success_count, failure_count) return BetaDistribution.calc(end_probability, success_count, failure_count) - BetaDistribution.calc(init_probability, success_count, failure_count)
@@ -63,7 +63,7 @@ class Exercises:
def one(): def one():
# 30% change of rain and 40% change your umbrella order will arrive. P(R AND U) # 30% change of rain and 40% change your umbrella order will arrive. P(R AND U)
return p_r * p_u return p_r * p_u
@staticmethod @staticmethod
def two(): def two():
# Same Ps as previous. P(!R OR U) # Same Ps as previous. P(!R OR U)
@@ -81,7 +81,7 @@ class Exercises:
p_bail = 0.4 p_bail = 0.4
p_at_least_50_bail = 0.0 p_at_least_50_bail = 0.0
for x in range(50, n + 1): for x in range(50, n + 1):
p_at_least_50_bail += binom.pmf(x, n, p_bail) p_at_least_50_bail += binom.pmf(x, n, p_bail)
return p_at_least_50_bail return p_at_least_50_bail
@staticmethod @staticmethod
@@ -91,7 +91,7 @@ class Exercises:
t = 2 t = 2
return 1.0 - BetaDistribution.calc(0.5, 8, 2) return 1.0 - BetaDistribution.calc(0.5, 8, 2)
@staticmethod @staticmethod
def test(): def test():
print("P(R AND U) = {0}".format(Exercises.one())) print("P(R AND U) = {0}".format(Exercises.one()))
@@ -101,7 +101,7 @@ class Exercises:
print("P(fair) = {0}".format(Exercises.five())) print("P(fair) = {0}".format(Exercises.five()))
def test_probability_module(): def run():
print("=== Probability module ===") print("=== Probability module ===")
print(">> Binomial distribution") print(">> Binomial distribution")
BinomialDistribution.example() BinomialDistribution.example()
+275
View File
@@ -0,0 +1,275 @@
## This module represents the third chapter of the book
## "Essential Math for Data Science" - Thomas Nield
## Chapter 3 - Statistics
from math import sqrt, pi, e, exp
from scipy.stats import norm, t
import random
import plotly.express as px
def my_mean(list):
return sum(list) / len(list)
def weighted_mean(items, weights):
if (len(items) != len(weights)):
return
total = 0
for i in range(len(items)):
total += items[i] * weights[i]
return total / sum(weights)
def weighted_mean_inline(items, weights):
return sum(s * w for s, w in zip(items, weights)) / sum(weights)
# also called 50% quantile
def median(items):
ordered = sorted(items)
length = len(ordered)
pair = length % 2 == 0
mid = int(length / 2) - 1 if pair else int(length/2)
if pair:
return (ordered[mid] + ordered[mid+1]) / 2
else:
return ordered[mid]
def mode(items):
sums = []
def population_variance(value_list, mean):
summatory = 0.0
for value in value_list:
summatory += (value - mean) ** 2
return summatory / len(value_list)
def population_variance_inline(value_list):
return sum((v - (sum(value_list) / len(value_list))) ** 2 for v in value_list) / len(value_list)
def sample_variance(value_list):
mean = sum(value_list) / len(value_list)
return sum((value - mean) ** 2 for value in value_list) / (len(value_list) - 1)
def population_standard_deviation(value_list):
return sqrt(population_variance_inline(value_list))
def sample_standard_deviation(value_list):
return sqrt(sample_variance(value_list))
def standard_deviation(value_list, is_sample):
return sample_standard_deviation(value_list) if is_sample else population_standard_deviation(value_list)
# Normal distribution
# PDF generates the Normal Distribution (symetric arround the mean)
def normal_probability_density_function(x: float, mean: float, standard_deviation: float):
return (1.0 / (2.0 * pi * standard_deviation ** 2) ** 0.5) * exp(-1.0 * ((x - mean) ** 2 / (2.0 * standard_deviation ** 2)))
def normal_cumulative_density_function(x, mean, std_deviation):
return norm.cdf(x, mean, std_deviation)
# Check exected value for a given probability
def inverse_cumulative_density_function(prob, mean, std_dev):
x = norm.ppf(prob, mean, std_dev)
return x
# Z-scores are valuable in order to normalize 2 pieces of data
def z_score(value, data_mean, std_deviation):
return (value - data_mean) / std_deviation
def coeficient_of_variation(std_deviation, mean):
return (std_deviation / mean)
def test_central_limit_theorem(sample_size, sample_count):
x_values = [(sum([random.uniform(0.0,1.0) for i in range(sample_size)]) / sample_size) for _ in range(sample_count)]
y_values = [1 for _ in range(sample_count)]
px.histogram(x=x_values, y=y_values, nbins=20).show()
def generic_critical_z_value(probability):
norm_dist = norm(loc=0.0, scale=1.0)
left_tail_area = (1.0 - probability) / 2.0
upper_area = 1.0 - ((1.0 - probability) / 2.0)
return norm_dist.ppf(left_tail_area), norm_dist.ppf(upper_area)
def margin_of_error(sample_size, standard_deviation, z_value):
return z_value * (standard_deviation / sqrt(sample_size)) # +-, we return the one provided by the z_value (tail or upper)
# How confident we are at a population metric given a sample (the interval we are "probability" sure the value will be)
def confidence_interval(probability, sample_size, standard_deviation, z_value, mean):
critical_z = generic_critical_z_value(probability)
margin_error = margin_of_error(sample_size, standard_deviation, z_value)
return mean + margin_error, mean - margin_error
# T Distribution
# Similar to the normal distribution but made for smaller sample-sizes (30 or less)
# When we get close to the 31 items, both are identical
def get_critical_value_range_t(conficence_percentage: float, sample_size: int):
untrusted_percentage = 1.0 - conficence_percentage
lower = t.ppf(untrusted_percentage / 2, df=sample_size-1)
upper = t.ppf(conficence_percentage + (untrusted_percentage / 2), df=sample_size-1)
return (lower, upper)
def run():
print("=== Statistics module ===")
list = [ 1, 2, 3, 4, 5, 6]
print(">> The mean of {0} is {1}".format(list, my_mean(list)))
weights = [0.2, 0.5, 0.7, 1, 0, 0.9]
print(">> The weighted_mean of {0} is {1} and it is equivalent to {2}".format(list, weighted_mean(list, weights), weighted_mean_inline(list, weights)))
print(">> The median is {0}".format(median(list)))
values = [ 0, 1, 5, 7, 9, 10, 14]
_population_variance = population_variance(values, sum(values) / len(values))
population_variance_calc_inline = population_variance_inline(values);
print("The population variance is", _population_variance, population_variance_calc_inline)
std_dev = standard_deviation(values, False)
print("The standard deviation is", std_dev)
sample = values.copy()
del sample[3]
del sample[1]
print("The sample variance for a population is", sample_variance(sample))
print("The standard deviation for a population is", standard_deviation(sample, True))
print("== Normal distribution ==")
values = [ 0, 1, 5, 7, 9, 10, 14]
mean = sum(values) / len(values)
std_dev = standard_deviation(values, False)
target_x = 1
print(">> The probability_density_function for x = 1 over the example data is {0}".format(normal_probability_density_function(target_x, mean, std_dev)))
print(">> The probability for observing a value smaller than 1 is given by the cumulative density function and it is: {0}".format(normal_cumulative_density_function(target_x, mean, std_dev)))
target_probability = 0.5
expected_value = inverse_cumulative_density_function(target_probability, mean, std_dev);
print(">> For a probability of .5 we expect the value: ", expected_value)
# Population with cold MEAN recovery time of 18 days, with std_dev of 1.5 days.
# Chances of recovery between 15 and 21 days
mean = 18
std_dev = 1.5
init = 15
end = 21
chances = normal_cumulative_density_function(end, mean, std_dev) - normal_cumulative_density_function(init, mean, std_dev)
print("Chances of recovering from a cold between 15 and 21 days: ", chances)
print("Chances of recovering before 15 days or after 21: ", 1.0 - chances)
# since its a normal distribution, the chances are equaly distributed
print("Chances of recovering before 15 days: ", (1.0 - chances) / 2)
# Apply rug (or drug) to 40 people and see a 16 MEAN recovery time. Test if rug improved mean or casuality
## One tailed atest: use inverse cdf in order to find the limit value for a given %.
new_mean = 16
min_target_percentage = 0.05 # this is a standard
min_mean = inverse_cumulative_density_function(min_target_percentage, mean, std_dev)
if (min_mean < new_mean):
print("The rug (drug) did nothing.")
else:
print("The rug (drug) worked.")
## One tailed test with a P value
p_value = normal_cumulative_density_function(new_mean, mean, std_dev)
if (p_value > min_target_percentage):
print("The rug (drug) did nothing.")
else:
print("The rug (drug) worked.")
## Two tailed test (look for both sides of the normal distribution)
## Double the checks, harder to prove (x2) and checks if the rug(drug) makes the recovery time worse.
left_min_target = min_target_percentage / 2
x1 = inverse_cumulative_density_function(left_min_target, mean, std_dev)
x2 = inverse_cumulative_density_function(1.0 - left_min_target, mean, std_dev)
if (new_mean < x1 or new_mean > x2):
print("The rug (drug) worked.", x1, x2)
else:
print("The rug (drug) did nothing.", x1, x2)
## Two tailed test with a P value
p1 = normal_cumulative_density_function(new_mean, mean, std_dev)
right_symetrical_mean = mean + (mean - new_mean)
p2 = 1.0 - normal_cumulative_density_function(right_symetrical_mean, mean, std_dev)
p_value = p1 + p2
if (p_value < min_target_percentage):
print("The rug (drug) worked.", p_value)
else:
print("The rug (drug) did nothing.", p_value)
#### CONCEPT: P-hacking, searching for data (in big data scenarios) that passes the p_value < 0.05 test and claiming for a relation.
print("== Z-scores ==")
print("A house (A) of 150K in a neighborhood of 140K mean and 3K std_dev has a Z-score: {0}".format(z_score(150000, 140000, 3000)))
print("A house (B) of 815K in a neighborhood of 800K mean and 10K std_dev has a Z-score: {0}".format(z_score(815000, 800000, 10000)))
print("The House A is much more expensive because its z-score is higher.")
print("The neighborhood of B has a coeficient of variation: {0}, and the one of A: {1}".format(coeficient_of_variation(3000, 140000), coeficient_of_variation(10000, 800000)))
print("This means that the neighborhood of A has more spread in its prices")
print("== Central Limit Theorem ==")
test_central_limit_theorem(sample_size=1, sample_count=1000)
test_central_limit_theorem(sample_size=31, sample_count=1000)
print("== T Distribution ==")
confidence = 0.95
sample_size = 25
(lower, upper) = get_critical_value_range_t(confidence, sample_size)
print("The confidence interval is: ", lower, upper)
print("== Final Exercises ==")
# 1.
pool_widths = (1.78, 1.75, 1.72, 1.74, 1.77)
pool_width_mean = my_mean(pool_widths)
pool_width_std = standard_deviation(pool_widths, True)
print("1: ", pool_width_mean, pool_width_std)
# 2.
z_mean = 42
z_std_dev = 8
z_prob_init = 20
z_prob_end = 30
z_prob_final = normal_cumulative_density_function(z_prob_end,z_mean,z_std_dev) - normal_cumulative_density_function(z_prob_init, z_mean, z_std_dev)
print("2: ", z_prob_final)
# 3.
filament_value = 1.75
filament_sample_size = 34
filament_mean = 1.715588
filament_std_dev = 0.029252
filament_percentage_conficence = .99
filament_z_value = z_score(filament_value,filament_mean, filament_std_dev)
(filament_confidence_init, filament_conficence_end) = confidence_interval(filament_percentage_conficence, filament_sample_size, filament_std_dev, filament_z_value, filament_mean)
print("3: ", filament_confidence_init, filament_conficence_end)
# 4.
original_sales_average = 10345
original_sales_std_dev = 552
new_sales_average = 11641
min_sales_percentage = 0.05
sales_p1 = 1.0 - normal_cumulative_density_function(new_sales_average, original_sales_average, original_sales_std_dev)
sales_p = sales_p1 * 2 # take advantage of symmetry
if (sales_p < min_sales_percentage):
print("The sales campaing worked", sales_p)
else:
print("The sales campaing did NOT work")
+33
View File
@@ -1,3 +1,7 @@
def run():
print("! This module is ran through tests, do not expect an output.")
def maximum_subarray_sum(input_array: list[int]): def maximum_subarray_sum(input_array: list[int]):
max_sum = input_array[0] max_sum = input_array[0]
subarray = [input_array[0]] subarray = [input_array[0]]
@@ -20,3 +24,32 @@ def find_missing(input_aray: list[int]):
return sum(get_array_range(1, len(input_aray) + 1)) - sum(input_aray) return sum(get_array_range(1, len(input_aray) + 1)) - sum(input_aray)
def trap_rain_water(input_aray: list[int]): def trap_rain_water(input_aray: list[int]):
ceiling = max(input_aray)
# Step 1: create a matrix with the graph
container = []
for wall_index in range(0, len(input_aray)):
wall = []
for coord_index in range(0, ceiling):
if (coord_index < input_aray[wall_index]):
wall.insert(coord_index, "w")
else:
wall.insert(coord_index, "a")
container.insert(wall_index, wall)
total_water = 0
# Step 2: fill the air with water
for wall_x in range(0, len(input_aray)):
for wall_y in range(0, input_aray[wall_x]):
if (container[wall_x][wall_y] == "a"):
continue
# Step 2.1 count distance with next wall
temp_water = 0
for water_x in range(wall_x + 1, len(input_aray)):
if (container[water_x][wall_y] == "a"):
temp_water += 1
else:
total_water += temp_water
break
return total_water
+6
View File
@@ -0,0 +1,6 @@
def run():
print("== Type casting ==")
value = "1234"
value_int = int(value)
print(f"{type(value)} value: {value + str(5)} and {type(value_int)} value: {value_int + 5}")
+16
View File
@@ -0,0 +1,16 @@
import re
def run():
print("== Regular expressions ==")
test_txt = "This is a long test with more test 1234"
matched = re.search("test", test_txt)
print(f"Found span: {matched.span()}, as a group: {matched.group()}")
matched_all = re.findall("^test$", test_txt)
print(f"Found all: {matched_all}")
replaced = re.sub("[(is)*|(it)*]+", "x", test_txt, 7)
print(f"Repalced at: {test_txt} as: {replaced}")
+84
View File
@@ -0,0 +1,84 @@
from string import Formatter
def template_strings():
my_template = "This is a value: {test}"
print(my_template.format(test="another"))
complext_template = "The number of days in a %s is %d"
# % is the string formatting operator
print(complext_template % ("year", 365))
print("Numbers can also be formatted, such as %.2f" % 123.1235)
print("Simple template values can also be indexed {0} {1} but CANT use the formatting operator '%'".format("first", "second"))
print("What can do is specify both a template value name and a format, such as {test:.2f}".format(test=123.12345))
name = "Me"
surname = "Another dude"
print(f"I'm {name} {surname}")
# Lower level access to format flows
test_template = "This is an example with {with_quotes!r} or a number {num:.2f}"
for text, field, format_spec, conversion in Formatter().parse(test_template):
print(f"{text=}, {field=}, {format_spec=}, {conversion=}")
# Allow input in templates
# These are only used to properly manage templates on code, such as library imput for better validation
# they keep track of what is dynamic and whats not
# that makes them useful for db query validation or user input validation
# template_str = t"Hello, {input("Enter your name: ")} 👋 Welcome back!"
# food = "cheese"
# template = t"Tasty {food}!"
# for part in template:
# if isinstance(part, str):
# print("String part:", part)
# else:
# print("Interpolation part:", part.value)
def split_strings():
example_string = "This is a long string with a number 2134"
print(f"> Example string: {example_string}")
first_four = example_string[:4]
print(f"> First four: {first_four}")
last_four = example_string[-4:]
print(f"> Last four: {last_four}")
without_first_four = example_string[4:]
print(f"> Without first four: {without_first_four}")
without_last_four = example_string[:-4]
print(f"> Without last four: {without_last_four}")
with_replaced_l = example_string.replace("l", "p")
print(f"> With replaced l: {with_replaced_l}")
# Must use ascii chars
with_multiple_removed = example_string.translate({97: None, 101: None, 105: None, 111: None, 117: None})
print(f"> Without vocals: {with_multiple_removed}")
a_to_o = str.maketrans("a", "o")
with_o_instead_of_a = example_string.translate(a_to_o)
print(f"> With o instead of a: {with_o_instead_of_a}")
no_a = str.maketrans("", "", "a")
without_a = example_string.translate(no_a)
print(f"> Without a: {without_a}")
split_at_half_first = example_string[:(len(example_string) // 2)]
split_at_half_second = example_string[example_string.__len__() // 2:]
print(f"Split at half: [first] {split_at_half_first} [second] {split_at_half_second}")
removed_the_second = example_string[:2] + example_string[3:]
print(f"Removed the second: {removed_the_second}")
begin_inclusive_not_end = example_string[1:2]
print(f"Inclusive test: {begin_inclusive_not_end}")
def run():
template_strings()
split_strings()
-39
View File
@@ -1,39 +0,0 @@
from math import sqrt, pi, e
def population_variance(difference_list, mean):
summatory = 0.0
for diff in difference_list:
summatory += (diff - mean) ** 2
return summatory / len(difference_list)
def population_variance_inline(difference_list):
return sum((v - (sum(difference_list) / len(difference_list))) ** 2 for v in difference_list) / len(difference_list)
def sample_variance(difference_list):
mean = sum(difference_list) / len(difference_list)
return sum((diff - mean) ** 2 for diff in difference_list) / (len(difference_list) - 1)
def population_standard_deviation(difference_list):
return sqrt(population_variance_inline(difference_list))
def sample_standard_deviation(difference_list):
return sqrt(sample_variance(difference_list))
def standard_deviation(difference_list, is_sample):
return sample_standard_deviation(difference_list) if is_sample else population_standard_deviation(difference_list)
# PDF generates the Normal Distribution (symetric arround the mean)
def probability_density_function(x: float, mean: float, standard_deviation: float):
return (1 / (standard_deviation * sqrt(2 * pi))) * (e ** ((-1/2) * (x - (mean ** 2)) / standard_deviation))
def test_statistics_module():
print("=== Statistics module ===")
differences = [ -6.571, -5.571, -1.571, 0.429, 2.429, 3.429, 7.429 ]
print("The population variance is", population_variance(differences, sum(differences) / len(differences)), population_variance_inline(differences))
print("The standard deviation is", standard_deviation(differences, False))
sample = differences.copy()
del sample[3]
del sample[1]
print("The sample variance for a population is", sample_variance(sample))
print("The standard deviation for a population is", standard_deviation(sample, True))
-3
View File
@@ -1,3 +0,0 @@
def t_strings():
my_template = "This is a value: {test}"
print(my_template.format(test="another"))
+14
View File
@@ -25,3 +25,17 @@ def test_find_missing():
result = find_missing(input) result = find_missing(input)
expected = 4 expected = 4
assert result == expected assert result == expected
def test_rain_watter():
input = [3,0,1,0,4,0,2]
expected = 10
result = trap_rain_water(input)
assert result == expected
input = [3,0,2,0,4]
expected = 7
result = trap_rain_water(input)
assert result == expected
input = [1,2,3,4]
expected = 0
result = trap_rain_water(input)
assert result == expected