This article walks you through 30 exercises covering Python’s built-in math and statistics modules. Topics include core mathematical functions like sqrt, factorial, ceil, floor, gcd, and log; trigonometry and floating-point precision; and statistical measures such as mean, median, mode, standard deviation, variance, quartiles, correlation, geometric mean, harmonic mean, and the NormalDist class
Each coding challenge includes a Practice Problem, Hint, Solution code, and detailed Explanation, ensuring you don’t just copy code, but genuinely practice and understand how and why it works.
- All solutions have been fully tested on Python 3.
- Use our Online Code Editor to solve these exercises in real time.
- Also, Solve Python Exercises: 29 topic-wise exercises with over 800+ coding questions
Let us know if you have any alternative solutions. It will help other developers.
+ Table of Contents (30 Exercises)
Table of contents
- Exercise 1: Square Root and Power
- Exercise 2: Area of a Circle
- Exercise 3: Factorial Computation
- Exercise 4: Ceiling and Floor
- Exercise 5: Greatest Common Divisor
- Exercise 6: Hypotenuse of a Right Triangle
- Exercise 7: Natural and Base-10 Logarithms
- Exercise 8: Degrees to Radians, Sine and Cosine
- Exercise 9: Combinations and Permutations
- Exercise 10: Floating-Point Precision with math.isclose()
- Exercise 11: nth Fibonacci Using the Golden Ratio
- Exercise 12: Verifying exp() and log() Are Inverses
- Exercise 13: Degrees to DMS Converter
- Exercise 14: Perfect Square Checker Using math.isqrt()
- Exercise 15: Product of All Primes Below 30
- Exercise 16: Mean, Median, and Mode of Exam Scores
- Exercise 17: Sample vs Population Standard Deviation
- Exercise 18: Variance of Temperatures
- Exercise 19: median_low() and median_high()
- Exercise 20: Finding All Modes with multimode()
- Exercise 21: Quartiles with statistics.quantiles()
- Exercise 22: Harmonic Mean of Internet Speeds
- Exercise 23: Geometric Mean for Compound Growth Rate
- Exercise 24: Covariance Between Study Hours and Test Scores
- Exercise 25: Pearson Correlation Coefficient
- Exercise 26: Probability with NormalDist
- Exercise 27: Distribution Overlap Between Two Models
- Exercise 28: Grading Curve with NormalDist.from_samples()
- Exercise 29: Linear Regression to Predict Scores
- Exercise 30: Z-Score Outlier Detector Combining NormalDist and quantiles()
Exercise 1: Square Root and Power
Problem Statement: Write a Python program that uses math.sqrt() to find the square root of 144 and math.pow() to compute 2 raised to the power of 10, then prints both results.
Purpose: Square roots and powers appear constantly in geometry, physics simulations, financial modelling, and algorithm complexity analysis. This exercise introduces the two most fundamental functions in the math module and highlights the difference between math.pow() and Python’s built-in ** operator in terms of return type and precision.
Given Input: number = 144 and base, exponent = 2, 10
Expected Output: Square root of 144: 12.0 and 2 ^ 10 = 1024.0
▼ Hint
- Import the
mathmodule and callmath.sqrt(144)to get the square root as a float. - Call
math.pow(2, 10)to compute the power. Note that it always returns afloat, unlike the**operator which returns anintwhen both operands are integers. - Try comparing
math.pow(2, 10)with2 ** 10and print both types usingtype()to see the difference.
▼ Solution & Explanation
Explanation:
math.sqrt(number): Returns the square root of the given number as afloat. It is defined only for non-negative values – passing a negative number raises aValueError. For complex-number square roots, usecmath.sqrt()from thecmathmodule instead.math.pow(base, exponent): Always returns afloatregardless of the input types, making it consistent for floating-point pipelines. In contrast, the**operator returns anintwhen both operands are integers (2 ** 10gives1024, not1024.0), which can cause unexpected type mismatches downstream.- When to use which: Use
math.pow()when working in a floating-point context or interfacing with C-level math functions. Use**for integer exponentiation, large integer arithmetic, or when you need exact integer results without floating-point rounding.
Exercise 2: Area of a Circle
Problem Statement: Write a Python program that calculates the area of a circle with radius 7 using math.pi, and also computes the circumference, printing both results rounded to two decimal places.
Purpose: Circle geometry is foundational in graphics, physics engines, signal processing, and engineering calculations. Using math.pi instead of a hardcoded approximation like 3.14159 gives the highest precision available in floating-point arithmetic and makes the code’s intent immediately clear to any reader.
Given Input: radius = 7
Expected Output: Area: 153.94 and Circumference: 43.98
▼ Hint
- The formula for area is
math.pi * radius ** 2and for circumference is2 * math.pi * radius. - Use
round(value, 2)or the:.2fformat specifier to print results to two decimal places. - Print
math.pidirectly to see its full precision value stored by Python.
▼ Solution & Explanation
Explanation:
math.pi: A constant defined in themathmodule with the value3.141592653589793, which is the most precise representation of pi available in a 64-bit floating-point number. Using it instead of a manually typed approximation eliminates rounding errors introduced at the source of the calculation.math.pi * radius ** 2: Python’s operator precedence evaluatesradius ** 2first (exponentiation before multiplication), so no parentheses are needed here. The result is the area formula A = πr² applied directly.:.2fformat specifier: Formats the float to exactly two decimal places in the output string. This is the standard way to display measurements and monetary values cleanly without altering the underlying precision of the variable itself.
Exercise 3: Factorial Computation
Problem Statement: Write a Python program that uses math.factorial() to compute 10! and verifies that the result equals 3628800.
Purpose: Factorials are central to combinatorics, probability theory, permutation and combination calculations, and Taylor series expansions. Using math.factorial() is significantly faster and safer than writing a manual loop or recursive function, especially for large values where Python’s arbitrary-precision integers shine.
Given Input: n = 10
Expected Output: 10! = 3628800 and Verification passed: True
▼ Hint
- Call
math.factorial(n)and store the result, then compare it with3628800using==. math.factorial()accepts only non-negative integers – passing a float or a negative number raises aValueError.- Try computing factorials for a range of values (0 through 10) in a loop to see how rapidly the numbers grow.
▼ Solution & Explanation
Explanation:
math.factorial(n): Returns the exact integer result of n! (n × (n-1) × … × 1). It returns anint, not a float, which means it benefits from Python’s arbitrary-precision integer arithmetic and produces exact results for any non-negative integer, no matter how large.result == expected: Becausemath.factorial()returns an exact integer, the equality check is reliable with no floating-point rounding concern. This makes it safe to use in unit tests and verification logic where exact results are required.- Factorial growth: The growth table reveals how explosively factorials increase.
0!and1!are both1by definition, and the values roughly multiply by their index at each step –10!is already over 3.6 million. This rapid growth is why factorials appear in complexity analysis of brute-force combinatorial algorithms.
Exercise 4: Ceiling and Floor
Problem Statement: Write a Python program that uses math.ceil() and math.floor() to find the ceiling and floor of 4.7, then demonstrates the same functions on negative numbers to reveal their behaviour around zero.
Purpose: Ceiling and floor rounding are essential in pagination logic (how many pages to display), resource allocation (how many containers to provision), pricing (always rounding up to the nearest cent), and grid snapping in graphics. Understanding how they behave with negative numbers prevents a common off-by-one class of bugs.
Given Input: value = 4.7 and negative_value = -4.7
Expected Output: ceil(4.7) = 5, floor(4.7) = 4, ceil(-4.7) = -4, floor(-4.7) = -5
▼ Hint
math.ceil(x)returns the smallest integer greater than or equal tox. Think of it as always rounding up toward positive infinity.math.floor(x)returns the largest integer less than or equal tox. Think of it as always rounding down toward negative infinity.- Both functions return an
intin Python 3, unlike in Python 2 where they returned afloat. Verify this withtype(math.ceil(4.7)).
▼ Solution & Explanation
Explanation:
math.ceil(4.7)returns 5: Ceiling always moves toward positive infinity, so 4.7 rounds up to 5. For-4.7, moving toward positive infinity means rounding up to-4, not-5– a result that surprises many developers who expect it to mirror the positive-number behaviour symmetrically.math.floor(4.7)returns 4: Floor always moves toward negative infinity, so 4.7 rounds down to 4. For-4.7, moving toward negative infinity means rounding down to-5. This is distinct from truncation:int(-4.7)truncates toward zero and gives-4, whereasmath.floor(-4.7)gives-5.- Pagination example:
math.ceil(total_items / items_per_page)is the canonical way to calculate how many pages are needed. For 31 items at 10 per page, plain integer division gives3(missing the last page), whilemath.ceil(31 / 10)correctly gives4.
Exercise 5: Greatest Common Divisor
Problem Statement: Write a Python program that uses math.gcd() to find the greatest common divisor of 48 and 180, then uses it to reduce the corresponding fraction to its simplest form.
Purpose: The GCD is a foundational operation in number theory with practical applications in fraction simplification, cryptography (RSA key generation), scheduling (finding common cycle lengths), and screen resolution scaling. This exercise shows both the direct use of math.gcd() and a concrete real-world application of its result.
Given Input: a = 48 and b = 180
Expected Output: GCD(48, 180) = 12 and 48/180 simplified = 4/15
▼ Hint
- Call
math.gcd(a, b)to get the greatest common divisor as a positive integer. - Divide both the numerator and denominator by the GCD to reduce the fraction:
a // gcdandb // gcd. - In Python 3.9 and later,
math.gcd()accepts more than two arguments, so you can find the GCD of a list of numbers in a single call.
▼ Solution & Explanation
Explanation:
math.gcd(a, b): Returns the largest positive integer that divides bothaandbwithout a remainder. Forgcd(48, 180), the result is12because 12 is the largest number that divides both evenly (48 / 12 = 4 and 180 / 12 = 15).- Fraction simplification: Dividing both the numerator and denominator by their GCD reduces a fraction to its lowest terms. Using integer floor division
//ensures the result is anint, not a float, which is appropriate for fraction representation. - LCM relationship: The Least Common Multiple can be derived from the GCD using the identity
lcm(a, b) = |a × b| / gcd(a, b). Python 3.9 introducedmath.lcm()as a direct function. Both LCM and GCD together solve a wide range of scheduling and synchronisation problems.
Exercise 6: Hypotenuse of a Right Triangle
Problem Statement: Write a Python program that computes the hypotenuse of a right triangle with legs 3 and 4 using math.hypot(), then verifies the result against the manually computed Pythagorean formula.
Purpose: Euclidean distance – the straight-line distance between two points – is the hypotenuse formula in disguise. It is used constantly in game development, mapping applications, robotics path planning, machine learning distance metrics, and physics simulations. math.hypot() is numerically safer than the manual formula because it avoids overflow and underflow for very large or very small values.
Given Input: a = 3 and b = 4
Expected Output: Hypotenuse: 5.0 and Manual formula result: 5.0
▼ Hint
- Call
math.hypot(a, b)which computessqrt(a² + b²)internally but with better numerical stability than writing it manually. - Compute the manual version with
math.sqrt(a**2 + b**2)and compare both results to confirm they match. - In Python 3.8 and later,
math.hypot()accepts more than two arguments, allowing you to compute the Euclidean distance in 3D or higher dimensions directly.
▼ Solution & Explanation
Explanation:
math.hypot(a, b): Computessqrt(a² + b²)using an algorithm that scales the inputs internally to avoid intermediate overflow (when values are very large) or underflow (when values are very small). The manual formulamath.sqrt(a**2 + b**2)can overflow toinffor inputs near the float maximum, whilemath.hypot()handles these edge cases gracefully.math.isclose(): Used to compare two floating-point numbers for near-equality rather than exact equality. Direct==comparison of floats can fail due to tiny rounding differences.math.isclose(a, b)checks that the difference is within a relative tolerance of1e-9by default, which is appropriate for most scientific comparisons.- Multi-dimensional use: From Python 3.8,
math.hypot(*coords)accepts any number of arguments, computing the n-dimensional Euclidean norm directly. This makes it a single-function solution for distance calculations in 2D, 3D, and beyond without needing to write a manual sum-of-squares loop.
Exercise 7: Natural and Base-10 Logarithms
Problem Statement: Write a Python program that uses math.log() and math.log10() to compute the natural logarithm (base e) and the base-10 logarithm of 1000, then verifies each result by reversing it with the corresponding exponential function.
Purpose: Logarithms are used in decibel calculations, pH measurements, earthquake magnitude scales, information entropy, algorithmic complexity (binary search is O(log n)), and machine learning loss functions. Understanding both the natural log and base-10 log, and knowing how to verify them by reversal, builds the intuition needed to apply them correctly across these domains.
Given Input: value = 1000
Expected Output: ln(1000) = 6.9078, log10(1000) = 3.0, and verification that reversing each result recovers the original value.
▼ Hint
- Use
math.log(value)for the natural log (base e) andmath.log10(value)for the base-10 log. - Pass a second argument to
math.log()to compute a log in any base:math.log(value, base). - Verify the natural log by computing
math.e ** ln_resultand verify the base-10 log by computing10 ** log10_result– both should return a value close to the original 1000.
▼ Solution & Explanation
Explanation:
math.log(value): With a single argument, computes the natural logarithm (base e, wheremath.e ≈ 2.71828). With a second argument it computes the logarithm in that base:math.log(1000, 10)gives the same result asmath.log10(1000), though the dedicated functions are more numerically accurate for common bases.math.log10(value): A dedicated base-10 logarithm function that is more numerically stable thanmath.log(value, 10)for values very close to powers of 10. Forvalue = 1000, it returns exactly3.0because10³ = 1000is an exact power of 10.math.log2(): A dedicated base-2 logarithm introduced in Python 3.3, more precise thanmath.log(x, 2)for values that are exact powers of 2. The ceiling oflog2(n)gives the number of bits needed to represent the integern, which is a practical bit-width calculation used in data encoding and compression.
Exercise 8: Degrees to Radians, Sine and Cosine
Problem Statement: Write a Python program that converts 270 degrees to radians using math.radians(), then computes and prints its sine and cosine values.
Purpose: Python’s trigonometric functions operate in radians, not degrees, so converting between the two is a required first step in any geometry, physics, or signal processing task. This exercise also builds intuition for the unit circle – knowing that sin(270°) = -1 and cos(270°) = 0 provides a mental anchor for verifying trigonometric results.
Given Input: degrees = 270
Expected Output: 270° in radians: 4.7124, sin(270°) = -1.0, cos(270°) = 0.0
▼ Hint
- Use
math.radians(degrees)to convert degrees to radians. The formula isradians = degrees × π / 180. - Pass the radian value to
math.sin()andmath.cos()to get the trigonometric results. - Use
round(value, 10)ormath.isclose(result, expected)when verifying results, since floating-point arithmetic may produce values like-1.0000000000000002instead of exactly-1.0. - Use
math.degrees(radians)to convert back and confirm the round-trip gives the original angle.
▼ Solution & Explanation
Explanation:
math.radians(degrees): Converts an angle from degrees to radians using the formularadians = degrees × π / 180. All of Python’s trigonometric functions (math.sin(),math.cos(),math.tan()) require their input in radians, so this conversion step is mandatory when working with degree-based angle values.round(sin_val, 10): Due to floating-point representation,math.sin(math.radians(270))may return a value like-1.0exactly or something very close such as-1.0000000000000002. Rounding to 10 decimal places collapses these near-zero residuals to clean values for display, without affecting the underlying precision used in calculations.math.degrees(radians): The inverse ofmath.radians(), converting back from radians to degrees. The round-trip conversion confirms that no information is lost:math.degrees(math.radians(270))returns exactly270.0.
Exercise 9: Combinations and Permutations
Problem Statement: Write a Python program that uses math.comb() and math.perm() to calculate the number of combinations and permutations for n=10 items taken k=3 at a time, then prints both results with a clear explanation of what each value means.
Purpose: Combinations and permutations are the building blocks of probability theory, statistics, and discrete mathematics. They appear in lottery odds, card game probabilities, password strength calculations, and test case generation. math.comb() and math.perm() were introduced in Python 3.8 as direct, readable replacements for the manual factorial-based formulas.
Given Input: n = 10, k = 3
Expected Output: C(10, 3) = 120 and P(10, 3) = 720
▼ Hint
math.comb(n, k)computes C(n, k) = n! / (k! × (n-k)!), the number of ways to choose k items from n where order does not matter.math.perm(n, k)computes P(n, k) = n! / (n-k)!, the number of ways to arrange k items chosen from n where order matters.- Verify both results manually using
math.factorial()and the corresponding formulas to confirm the functions are working as expected.
▼ Solution & Explanation
Explanation:
math.comb(n, k): Returns the number of ways to choosekitems fromnwithout regard to order, computed asn! / (k! × (n-k)!). It returns an exactintand is more readable and efficient than computing the formula manually usingmath.factorial(). Added in Python 3.8.math.perm(n, k): Returns the number of ways to arrangekitems chosen fromnwhere order matters, computed asn! / (n-k)!. Because each combination of k items can be arranged ink!ways,P(n, k)always equalsC(n, k) × k!– confirmed by the ratio printed in the output.- Combinations vs permutations: The key distinction is whether order matters. Selecting a 3-person committee from 10 people is a combination problem (Alice-Bob-Charlie is the same committee as Charlie-Bob-Alice). Selecting a President, Vice-President, and Secretary is a permutation problem (the same three people in different roles produce different outcomes).
Exercise 10: Floating-Point Precision with math.isclose()
Problem Statement: Write a Python program that uses math.isclose() to compare 0.1 + 0.2 with 0.3, demonstrates why a direct == comparison fails, and explains the floating-point precision issue behind it.
Purpose: Floating-point precision errors are among the most common sources of bugs in scientific computing, financial software, and unit tests. Understanding why 0.1 + 0.2 != 0.3 in binary floating-point arithmetic, and knowing how to compare floats correctly using math.isclose(), is an essential skill for writing reliable numerical code.
Given Input: a = 0.1 + 0.2 and b = 0.3
Expected Output: 0.1 + 0.2 == 0.3 : False and math.isclose(0.1 + 0.2, 0.3): True
▼ Hint
- Print
0.1 + 0.2with high precision usingf"{0.1 + 0.2:.20f}"to reveal the hidden floating-point error digit. - Call
math.isclose(a, b)which checks that the relative difference between two values is within a default tolerance of1e-9. - Try adjusting the
rel_tolandabs_tolkeyword arguments ofmath.isclose()to understand how tolerance thresholds affect the comparison.
▼ Solution & Explanation
Explanation:
- Why
0.1 + 0.2 != 0.3: Decimal fractions like 0.1, 0.2, and 0.3 cannot be represented exactly in binary floating-point (IEEE 754). The computer stores the closest representable binary approximation, so0.1 + 0.2produces0.30000000000000004rather than exactly0.3. This is not a Python bug – it is a fundamental property of binary floating-point arithmetic shared by virtually all programming languages. math.isclose(a, b, rel_tol=1e-9): ReturnsTrueif the values are close enough within the specified tolerances. The relative tolerancerel_tolis a fraction of the larger value (default1e-9), and the absolute toleranceabs_tolis a fixed threshold (default0.0). The check passes ifabs(a-b) <= max(rel_tol * max(abs(a), abs(b)), abs_tol).- When to use
abs_tolvsrel_tol: Userel_tolfor general-purpose comparisons where the magnitude of values varies. Useabs_tolwhen comparing values near zero, since relative tolerance becomes meaningless when one value is zero or very close to it. For robust comparisons, specify both:math.isclose(a, b, rel_tol=1e-9, abs_tol=1e-12).
Exercise 11: nth Fibonacci Using the Golden Ratio
Problem Statement: Write a Python function that computes the nth Fibonacci number using Binet’s formula, which involves math.sqrt() and the golden ratio, then verifies the results against the traditional iterative approach for the first 15 terms.
Purpose: Binet’s formula is a striking example of how a purely integer sequence (Fibonacci numbers) can be computed using irrational numbers (the golden ratio and square roots). It also demonstrates the practical limits of floating-point arithmetic – the formula works perfectly for small n but accumulates rounding error for large n, making it an excellent case study in numerical precision trade-offs.
Given Input: Compute Fibonacci numbers for n from 0 to 14.
Expected Output: A table comparing the golden ratio formula result and the iterative result for each n, confirming they match for small values.
▼ Hint
- The golden ratio is
phi = (1 + math.sqrt(5)) / 2. Binet’s formula isF(n) = round((phi**n - (-1/phi)**n) / math.sqrt(5)). - Use
round()to convert the float result back to the nearest integer, since the formula introduces floating-point imprecision. - Write a simple iterative Fibonacci function using a loop as the reference implementation to compare against.
▼ Solution & Explanation
Explanation:
phi = (1 + math.sqrt(5)) / 2: The golden ratio, approximately1.6180339887. It is the positive root of the equationx² = x + 1and has the remarkable property that the ratio of consecutive Fibonacci numbers converges to phi as n increases. Usingmath.sqrt(5)gives the highest floating-point precision available for this constant.- Binet’s formula:
F(n) = (phi^n - (-1/phi)^n) / sqrt(5). The formula produces an exact integer result in theory, but in practice the floating-point arithmetic introduces tiny errors. Theround()call corrects these sub-integer residuals and returns the exact Fibonacci integer for values up to roughly n=70, beyond which float64 precision is insufficient. - Iterative reference: The iterative function uses tuple assignment
a, b = b, a + bto advance the sequence in-place without a temporary variable. It produces exact integer results for any n and serves as the ground truth to validate the golden ratio formula. For very large n (beyond ~70), the iterative version remains exact while the golden ratio formula diverges.
Exercise 12: Verifying exp() and log() Are Inverses
Problem Statement: Write a Python program that uses math.exp() and math.log() to verify that exp(log(x)) == x and log(exp(x)) == x for several test values, using math.isclose() for the comparison.
Purpose: Understanding that exp and log are inverse functions is foundational for solving exponential equations, working with growth and decay models, and understanding how neural network activation functions relate to their loss functions. Verifying mathematical identities in code also reinforces the habit of testing assumptions rather than trusting them implicitly.
Given Input: Test values [1, 2, 10, 0.5, 100, math.e]
Expected Output: A table showing exp(log(x)) and log(exp(x)) for each test value, with a verification flag confirming both round-trips recover the original.
▼ Hint
math.exp(x)computes e raised to the power x.math.log(x)computes the natural logarithm (base e) of x.- Use
math.isclose(math.exp(math.log(x)), x)rather than==to account for floating-point rounding in the round-trip. - Note that
math.log(x)is only defined forx > 0– passing zero or a negative value raises aValueError.
▼ Solution & Explanation
Explanation:
math.exp(x): Computes e^x where e is Euler’s number (math.e ≈ 2.71828). It is more numerically precise than writingmath.e ** xfor small values of x near zero, where the built-in function uses a specially optimised algorithm to minimise rounding error.- Inverse relationship: By definition,
expandlogare mutual inverses:exp(log(x)) = xfor allx > 0, andlog(exp(x)) = xfor all real x. In practice, floating-point arithmetic introduces tiny errors in both directions, which is whymath.isclose()is used instead of==for the verification. math.log(math.e)equals exactly 1.0: Becausemath.eis the base of the natural logarithm,log(e) = 1by definition. Python’s floating-point implementation returns exactly1.0for this input, making it a clean anchor point for understanding the exp-log relationship.
Exercise 13: Degrees to DMS Converter
Problem Statement: Write a Python function that converts a decimal degree value into degrees, minutes, and seconds (DMS) format using math.floor() and math.modf(), then formats the output as a standard geographic coordinate string.
Purpose: DMS notation is the standard format for geographic coordinates in GPS devices, aviation, cartography, and astronomy. Converting between decimal degrees and DMS is a practical exercise in decomposing a float into its integer and fractional parts, which is exactly what math.modf() is designed for.
Given Input: decimal_degrees = 45.8833 (representing a latitude or longitude)
Expected Output: 45.8833° = 45° 52' 59.88"
▼ Hint
math.modf(x)returns a tuple of(fractional_part, integer_part)– note the order is fractional first, integer second.- Multiply the fractional degree part by 60 to get total minutes, then apply
math.modf()again to split that into whole minutes and remaining fractional minutes. - Multiply the remaining fractional minutes by 60 to get the seconds. Use
math.floor()to extract whole minutes cleanly.
▼ Solution & Explanation
Explanation:
math.modf(x): Returns a two-element tuple(fractional_part, integer_part)where both have the same sign asx. Note the counter-intuitive order: fractional comes first, integer second. For example,math.modf(45.8833)returns(0.8833000000000024, 45.0). It is the cleanest way to split a float into its integer and decimal components without string parsing.- Two-stage decomposition: The conversion applies
math.modf()twice. The first call extracts the whole degrees and the fractional degree remainder. Multiplying the remainder by 60 converts fractional degrees to minutes, and a secondmath.modf()call splits that into whole minutes and the remaining fraction. Multiplying the second remainder by 60 gives seconds. - Sign handling: Geographic coordinates can be negative (west longitudes, south latitudes). The function strips the sign before decomposition and restores it to the degrees component only. Minutes and seconds are always non-negative in standard DMS notation – the direction is carried by the sign of the degrees or an explicit N/S/E/W suffix.
Exercise 14: Perfect Square Checker Using math.isqrt()
Problem Statement: Write a Python function that uses math.isqrt() to check whether a given positive integer is a perfect square, then tests it against a range of values and prints which ones qualify.
Purpose: Perfect square detection appears in number theory puzzles, competitive programming, cryptographic primality tests, and grid-layout algorithms (e.g. determining whether n items can be arranged in a square grid). Using math.isqrt() instead of math.sqrt() avoids floating-point rounding errors that can cause int(math.sqrt(n))**2 == n to return incorrect results for large perfect squares.
Given Input: Test integers from 1 to 30, then verify specific large values.
Expected Output: A list of perfect squares in the range 1-30 and a verification result for large numbers like 999999999999999999.
▼ Hint
math.isqrt(n)returns the integer square root of n – the largest integer whose square does not exceed n. It is exact for all non-negative integers with no floating-point rounding.- A number
nis a perfect square ifmath.isqrt(n) ** 2 == n. This check is reliable even for very large integers wheremath.sqrt()would lose precision. - Show why using
int(math.sqrt(n))can fail for large numbers by testing a value like999999999999999999with both approaches.
▼ Solution & Explanation
Explanation:
math.isqrt(n): Returns the exact integer square root of n – the largest integer r such that r² does not exceed n. It was introduced in Python 3.8 and operates entirely in integer arithmetic, so it is precise for arbitrarily large integers without any floating-point rounding. For example,math.isqrt(26)returns5because 5² = 25 ≤ 26 but 6² = 36 > 26.root * root == n: The perfect square check squares the integer root and compares it to the original value. If they match, n is a perfect square. This integer-only comparison is always exact, unlikeint(math.sqrt(n))**2 == nwhich can fail for large n becausemath.sqrt()uses 64-bit float arithmetic and loses precision beyond roughly 15-17 significant digits.- Float precision failure: For large perfect squares like
999999999²,math.sqrt()may return a float that is slightly off from the true root (e.g.999999999.0000005), causingint(math.sqrt(n))**2to compute the wrong integer and fail the equality check.math.isqrt()has no such limitation and gives the correct result for any non-negative integer, however large.
Exercise 15: Product of All Primes Below 30
Problem Statement: Write a Python program that identifies all prime numbers below 30, then uses math.prod() to compute their product (the primorial), and prints both the list of primes and the final result.
Purpose: math.prod() was introduced in Python 3.8 as a clean, readable counterpart to the built-in sum() function, replacing verbose functools.reduce() or manual loop patterns for multiplicative aggregation. This exercise pairs it with a prime sieve to demonstrate how functional tools compose naturally with number theory operations used in cryptography, hashing, and combinatorics.
Given Input: All prime numbers below 30.
Expected Output: Primes below 30: [2, 3, 5, 7, 11, 13, 17, 19, 23, 29] and Primorial (product): 6469693230
▼ Hint
- Write a helper function
is_prime(n)that returnsTrueifnhas no divisors other than 1 and itself. Check divisibility up toint(math.sqrt(n)) + 1for efficiency. - Use a list comprehension to build the list of primes below 30:
[n for n in range(2, 30) if is_prime(n)]. - Pass the primes list directly to
math.prod()to get the product. Compare the result with a manual loop using*=to confirm they match.
▼ Solution & Explanation
Explanation:
math.prod(iterable): Computes the product of all elements in the iterable, starting from an implicit initial value of 1. It mirrors the behaviour ofsum()for addition. Callingmath.prod([])on an empty iterable returns1(the multiplicative identity), just assum([])returns0(the additive identity).int(math.sqrt(n)) + 1in primality check: A composite number n must have at least one factor less than or equal to its square root. Checking divisors only up tosqrt(n)reduces the number of iterations from O(n) to O(sqrt(n)), which is a significant speedup for larger values. The+1ensures the boundary is inclusive after integer truncation.- Primorial: The product of the first k prime numbers is called a primorial (written p#). Primorials appear in proofs related to the infinitude of primes, in wheel factorisation sieves, and in generating candidate primes efficiently. The primorial of 29 is 6,469,693,230 – a number that grows astronomically fast as more primes are included.
Exercise 16: Mean, Median, and Mode of Exam Scores
Problem Statement: Write a Python program that takes a list of exam scores and uses the statistics module to compute the mean, median, and mode, then prints a formatted summary.
Purpose: Mean, median, and mode are the three measures of central tendency that appear in every branch of data analysis. Using Python’s built-in statistics module produces accurate results without external dependencies, making it the right choice for lightweight scripts, educational tools, and situations where NumPy or pandas would be overkill.
Given Input: scores = [72, 85, 90, 88, 76, 95, 85, 60, 72, 85, 91, 78]
Expected Output: Mean: 81.42, Median: 85.0, Mode: 85
▼ Hint
- Import
mean,median, andmodefrom thestatisticsmodule. statistics.mean(data)returns the arithmetic average.statistics.median(data)returns the middle value.statistics.mode(data)returns the single most common value – it raisesStatisticsErrorif there is no unique mode (usemultimode()to handle ties, covered in Exercise 20).- Use
sorted(scores)to print the data in order alongside the computed statistics so the results are easy to verify visually.
▼ Solution & Explanation
Explanation:
statistics.mean(scores): Computes the arithmetic mean as the sum of all values divided by the count. Thestatisticsmodule uses exact rational arithmetic for integer inputs, avoiding the floating-point rounding that can occur withsum(scores) / len(scores)for large datasets.statistics.median(scores): Sorts the data internally and returns the middle value for odd-length datasets, or the mean of the two middle values for even-length datasets. Because this input has 12 values, the median is the average of the 6th and 7th sorted values:(85 + 85) / 2 = 85.0.statistics.mode(scores): Returns the single most frequently occurring value. For this dataset,85appears three times and is the clear mode. In Python 3.8 and later,mode()no longer raisesStatisticsErroron a tie – it returns the first mode encountered. Usemultimode()from Exercise 20 when you need all modes.
Exercise 17: Sample vs Population Standard Deviation
Problem Statement: Write a Python program that computes both statistics.stdev() (sample standard deviation) and statistics.pstdev() (population standard deviation) on the same dataset, prints both values, and explains when each should be used.
Purpose: Choosing between sample and population standard deviation is one of the most common statistical mistakes made by programmers. Using the wrong one leads to either underestimating or overestimating the spread of data, which has real consequences in machine learning feature scaling, quality control, A/B testing, and scientific reporting.
Given Input: temperatures = [22.1, 24.5, 19.8, 23.3, 25.0, 21.7, 20.4, 26.1, 23.8, 22.9]
Expected Output: Sample stdev: 1.99 and Population pstdev: 1.89
▼ Hint
- Use
statistics.stdev(data)for sample standard deviation (divides byn-1, Bessel’s correction). - Use
statistics.pstdev(data)for population standard deviation (divides byn). - Also compute
statistics.variance(data)andstatistics.pvariance(data)and verify they are the squares of their respective standard deviations.
▼ Solution & Explanation
Explanation:
statistics.stdev()divides by n-1: The subtraction of 1 from the denominator is called Bessel’s correction. It compensates for the fact that a sample mean is not the true population mean – the sample tends to underestimate the spread. Dividing byn-1produces an unbiased estimator of the population standard deviation. Use this whenever your data is a subset drawn from a larger group you want to make inferences about.statistics.pstdev()divides by n: No correction is applied because there is nothing to estimate – the data represents the entire population. Use this when you have measured every member of the group and simply want to describe how spread out those exact values are, with no inference intended.- Variance is stdev squared: Standard deviation and variance always satisfy
stdev² = varianceandpstdev² = pvariance. Variance is expressed in squared units (e.g. °C²), which can be hard to interpret directly. Taking the square root to get standard deviation restores the original unit (°C), making it more intuitive to compare against the mean.
Exercise 18: Variance of Temperatures
Problem Statement: Write a Python program that computes the variance of a list of daily temperature readings using statistics.variance(), then uses the result to identify which readings fall more than one standard deviation from the mean.
Purpose: Variance quantifies how spread out a dataset is around its mean. It is a foundational metric in quality control, financial risk modelling, weather analysis, and machine learning (where high variance in a model signals overfitting). Extending the exercise to flag outliers bridges the gap between computing a statistic and applying it to a real decision.
Given Input: temperatures = [18.5, 21.0, 19.3, 35.2, 20.1, 17.8, 22.4, 16.9, 21.7, 34.8, 20.5, 19.0]
Expected Output: Variance, standard deviation, and a list of readings flagged as outliers (more than one standard deviation from the mean).
▼ Hint
- Use
statistics.variance(data)for sample variance andstatistics.mean(data)for the mean. - Compute the standard deviation as
math.sqrt(variance)or usestatistics.stdev(data)directly. - A value is an outlier if
abs(value - mean) > stdev. Use a list comprehension to collect all such values.
▼ Solution & Explanation
Explanation:
statistics.variance(temperatures): Returns the sample variance, computed as the average of the squared differences from the mean, with Bessel’s correction (dividing by n-1). The result is in squared units – for temperatures in °C, the variance is in °C². Taking the square root viamath.sqrt()orstatistics.stdev()converts back to the original unit.- Outlier detection with standard deviation: The rule of thumb that values more than one standard deviation from the mean are noteworthy is a simplified form of the empirical rule (68-95-99.7 rule), which applies to normally distributed data. For a normal distribution, roughly 68% of values fall within one standard deviation of the mean, so values outside that range are in the less common tail.
- Z-score: The formula
z = (value - mean) / stdevstandardises each data point to show how many standard deviations it sits above or below the mean. A z-score of +2.0 means the value is two standard deviations above the mean. Z-scores are the foundation of standard normal distribution tables, hypothesis testing, and feature normalisation in machine learning.
Exercise 19: median_low() and median_high()
Problem Statement: Write a Python program that applies statistics.median_low() and statistics.median_high() to an even-length dataset, compares their results with the standard statistics.median(), and explains when each variant is the appropriate choice.
Purpose: When a dataset has an even number of values, the standard median is the average of the two middle elements – which may not itself be a value in the dataset. In contexts such as reporting a real measurement, selecting a representative record from a database, or choosing a salary benchmark from actual payroll data, median_low() and median_high() ensure the result is always a genuine data point.
Given Input: response_times = [120, 135, 98, 150, 112, 143, 107, 160, 125, 138] (10 values – even length)
Expected Output: median: 131.5, median_low: 125, median_high: 138
▼ Hint
- All three functions –
median(),median_low(), andmedian_high()– take the same data list as their argument. - For an even-length dataset,
median_low()returns the lower of the two middle values, andmedian_high()returns the upper of the two middle values. Both return an actual element from the dataset. - Print the sorted dataset first so the two middle elements are clearly visible before computing the statistics.
▼ Solution & Explanation
Explanation:
statistics.median_low(data): Sorts the data and returns the lower of the two central values for even-length datasets. For odd-length datasets, it returns the single middle value – identical tomedian(). Because it always returns an element that exists in the original dataset, it is appropriate when the median must be a real, measurable data point (e.g. an actual server response time, not a calculated average of two times).statistics.median_high(data): Returns the upper of the two central values for even-length datasets, and the single middle value for odd-length datasets. Use it when a conservative upper estimate of the centre is needed – for example, when selecting a salary benchmark and you want to err on the higher side of the midpoint.- Relationship between the three: For even-length data,
median()is always the arithmetic mean ofmedian_low()andmedian_high(). For odd-length data, all three functions return the same value. This meansmedian_low()andmedian_high()are only meaningfully different frommedian()when the dataset has an even number of elements.
Exercise 20: Finding All Modes with multimode()
Problem Statement: Write a Python program that creates a dataset containing multiple repeated values and uses statistics.multimode() to find all modes, then demonstrates how the result differs from statistics.mode() when there are ties.
Purpose: Real-world datasets are frequently multimodal – think of survey responses clustered at two popular options, or sales data peaking in two different regions. statistics.multimode() was introduced in Python 3.8 specifically to handle tie cases that mode() either misses or handles ambiguously, making it essential for accurate frequency analysis.
Given Input: survey_responses = [3, 5, 2, 4, 5, 3, 1, 5, 2, 3, 4, 2, 5, 3, 1, 4, 2, 3]
Expected Output: multimode: [3, 2, 5] (or the modes in order of first appearance) and a frequency count confirming the tie.
▼ Hint
- Call
statistics.multimode(data)to get a list of all values that appear with the highest frequency. It always returns a list, even when there is only one mode. - Use
Counterfrom thecollectionsmodule to build a frequency table and verify that the modes returned bymultimode()all share the same count. - Compare the output of
statistics.mode(data)andstatistics.multimode(data)on the same dataset to highlight the difference in behaviour when a tie exists.
▼ Solution & Explanation
Explanation:
statistics.multimode(data): Returns a list of all values that occur with the maximum frequency. If there is a single mode it returns a list with one element. If multiple values are tied for the highest frequency, all of them are returned in the order they first appear in the input. It never raises an error, making it safe to use on any dataset without knowing the mode structure in advance.statistics.mode(data)on tied data: In Python 3.8 and later,mode()returns the first value it encounters with the highest frequency rather than raising aStatisticsError(the old Python 3.7 behaviour). This means it silently returns only one of several equally valid modes, which can mislead analysis if the tie is not known about – makingmultimode()the safer default choice.- Multimodal distributions: A dataset with two modes is bimodal; with three or more it is multimodal. Multimodal distributions often indicate that the data comes from two or more distinct subpopulations (e.g. survey respondents from different age groups preferring different options). Detecting multiple modes is therefore an important signal for further segmentation or investigation.
Exercise 21: Quartiles with statistics.quantiles()
Problem Statement: Write a Python program that generates a dataset of 20 integers, uses statistics.quantiles() to compute Q1, Q2, and Q3, then uses these values to calculate the interquartile range (IQR) and identify any outliers using the IQR fence method.
Purpose: Quartiles and the IQR are among the most robust tools in descriptive statistics. Unlike standard deviation, they are not sensitive to extreme outliers and are the basis of the box-and-whisker plot. The IQR fence method (Q1 – 1.5×IQR, Q3 + 1.5×IQR) is the standard algorithm used by statistical software to flag outliers in exploratory data analysis.
Given Input: data = [4, 7, 13, 2, 19, 25, 8, 14, 3, 31, 11, 6, 17, 22, 9, 5, 28, 16, 10, 100] (20 integers including one obvious outlier)
Expected Output: Q1, Q2, Q3, IQR, fence boundaries, and the list of values flagged as outliers by the IQR method.
▼ Hint
- Call
statistics.quantiles(data, n=4)to divide the data into 4 equal parts. This returns a list of 3 cut points:[Q1, Q2, Q3]. - Compute the IQR as
Q3 - Q1, then set the lower fence asQ1 - 1.5 * IQRand the upper fence asQ3 + 1.5 * IQR. - Use
method="inclusive"ormethod="exclusive"as a keyword argument toquantiles()to control how the boundaries are calculated, and note the small difference in results.
▼ Solution & Explanation
Explanation:
statistics.quantiles(data, n=4): Divides the sorted dataset intonequal-probability intervals and returns a list ofn-1cut points. Withn=4it returns the three quartile boundaries[Q1, Q2, Q3]. Settingn=100would return 99 percentile cut points. The function was introduced in Python 3.8.- IQR fence method: Any value below
Q1 - 1.5 × IQRor aboveQ3 + 1.5 × IQRis flagged as a potential outlier. The factor 1.5 was chosen by John Tukey to capture roughly 99.3% of normally distributed data within the fences. The value 100 in the dataset sits far above the upper fence and is correctly identified as an outlier. method="inclusive"vsmethod="exclusive": The two methods differ in how they handle the boundary data points when computing quartile positions."exclusive"(the default) excludes the minimum and maximum values from the interpolation, which is common in software like R and Excel."inclusive"includes them, matching the convention used in some textbooks. For large datasets the difference is negligible, but for small datasets it can produce visibly different Q1 and Q3 values.
Exercise 22: Harmonic Mean of Internet Speeds
Problem Statement: Write a Python program that computes the harmonic mean of a list of internet speeds using statistics.harmonic_mean(), then compares it to the arithmetic mean to show why the harmonic mean is the correct average for rate-based measurements.
Purpose: The harmonic mean is the correct average to use when combining rates, speeds, or ratios measured over equal distances or time slots. Using the arithmetic mean for speeds systematically overestimates the true average, which leads to incorrect capacity planning, network benchmarking, and performance reporting. This exercise builds the intuition to recognise when each type of mean is appropriate.
Given Input: speeds = [10, 20, 40, 80] (internet speeds in Mbps)
Expected Output: Harmonic mean: 22.86 Mbps and Arithmetic mean: 37.50 Mbps
▼ Hint
- Use
statistics.harmonic_mean(data)to compute the harmonic mean. The formula isn / (1/x1 + 1/x2 + ... + 1/xn). - Compare with
statistics.mean(data)to show the arithmetic mean and highlight the gap between the two. - To build intuition, simulate equal-time segments: if you spend 1 hour downloading at each speed, calculate the total data transferred and divide by total time to verify the harmonic mean is the correct result.
▼ Solution & Explanation
Explanation:
statistics.harmonic_mean(data): Computesn / (1/x1 + 1/x2 + ... + 1/xn), which is the reciprocal of the arithmetic mean of the reciprocals. It was added in Python 3.6 and raisesStatisticsErrorif any value is zero or negative, since rates must be positive. For a single-element list it returns that element unchanged.- Why harmonic mean for rates: When the same amount of work (distance, time, or quantity) is performed at different rates, the harmonic mean gives the correct overall rate. Spending one hour at 10 Mbps, 20 Mbps, 40 Mbps, and 80 Mbps transfers 150 Mb in 4 hours, giving a true average of 37.5 Mbps – which matches the arithmetic mean here only because the scenario happens to be equal-time. For equal-distance or equal-work scenarios the harmonic mean is the right choice.
- Arithmetic vs harmonic gap: The arithmetic mean always overestimates the true average rate when values vary, because slow segments drag down actual throughput more than fast segments boost it. The larger the variance in the rates, the more the arithmetic mean overestimates, and the bigger the practical error in capacity planning or benchmarking.
Exercise 23: Geometric Mean for Compound Growth Rate
Problem Statement: Write a Python program that uses statistics.geometric_mean() to calculate the average compound annual growth rate (CAGR) from five years of annual returns, then verifies the result by compounding the geometric mean over the same period.
Purpose: The geometric mean is the correct average for quantities that multiply over time – investment returns, population growth rates, inflation rates, and biological reproduction factors. Using the arithmetic mean for such data overestimates the true long-term growth and leads to misleading projections. This is one of the most practically important distinctions in financial and scientific data analysis.
Given Input: Annual growth factors for 5 years: growth_factors = [1.08, 0.95, 1.12, 1.06, 0.98] (representing +8%, -5%, +12%, +6%, -2%)
Expected Output: Geometric mean (CAGR): 1.0369 meaning approximately 3.69% average annual growth.
▼ Hint
- Pass the list of growth factors (not percentage returns) to
statistics.geometric_mean(). The formula is the nth root of the product of all values. - Verify the result by computing
geometric_mean ** nand comparing it tomath.prod(growth_factors)– both should give the total cumulative growth. - Show the final portfolio value by multiplying an initial investment of 1000 by the total growth factor, then by compounding at the geometric mean rate for the same period.
▼ Solution & Explanation
Explanation:
statistics.geometric_mean(data): Computes the nth root of the product of all values, equivalent toexp(mean(log(x) for x in data)). It was added in Python 3.8 and requires all values to be strictly positive – passing zero or a negative value raises aStatisticsError. For growth factors this is always satisfied since a -100% return (total loss) would be represented as 0.0, which is excluded by definition.- CAGR verification: The geometric mean raised to the power of n equals the total cumulative product of all growth factors. This is the defining property of the geometric mean for multiplicative sequences: it is the single constant rate that, compounded for n periods, produces the same end result as the actual sequence of varying rates.
- Arithmetic mean overestimates for multiplicative growth: The arithmetic mean of the growth factors is 1.038, implying 3.8% annual growth. Compounded over 5 years this gives a higher final value than actually occurred. The geometric mean (3.69%) correctly captures the drag that loss years impose on compounding, which the arithmetic mean ignores.
Exercise 24: Covariance Between Study Hours and Test Scores
Problem Statement: Write a Python program that uses statistics.covariance() to measure the linear relationship between hours studied and test scores, then interprets the sign and magnitude of the result.
Purpose: Covariance is the foundational measure of how two variables move together. It is the building block of correlation, regression, and portfolio theory in finance. Understanding its sign (direction of relationship) and its scale-dependence (why raw covariance cannot be compared across different datasets) motivates the need for the normalised correlation coefficient introduced in the next exercise.
Given Input: hours = [2, 4, 6, 8, 5, 7, 3, 9, 1, 6] and scores = [55, 70, 80, 92, 74, 85, 60, 95, 45, 78]
Expected Output: A positive covariance value confirming that more study hours are associated with higher scores.
▼ Hint
- Call
statistics.covariance(x, y)with two equal-length lists. It computes the sample covariance using n-1 in the denominator. - A positive result means the variables tend to increase together. A negative result means one increases as the other decreases. Zero means no linear relationship.
- Verify the result manually: compute
sum((xi - mean_x) * (yi - mean_y) for xi, yi in zip(x, y)) / (n - 1)and confirm it matchesstatistics.covariance(x, y).
▼ Solution & Explanation
Explanation:
statistics.covariance(x, y): Computes the sample covariance between two equal-length sequences using the formulasum((xi - mean_x) * (yi - mean_y)) / (n - 1). It was added in Python 3.10 along withstatistics.correlation(). Both lists must have the same length, otherwise aStatisticsErroris raised.- Sign interpretation: A positive covariance means the variables tend to move in the same direction – when hours is above its mean, score tends to be above its mean too. A negative covariance means they move in opposite directions. Zero covariance means there is no linear relationship, though a non-linear relationship could still exist.
- Scale-dependence limitation: The raw covariance value (e.g. 47.6) is hard to compare across datasets because it is measured in the product of the two units (hours × score points). Doubling all the score values would double the covariance without any change in the underlying relationship. This scale-dependence is resolved by dividing by both standard deviations to produce the dimensionless Pearson correlation coefficient shown in the next exercise.
Exercise 25: Pearson Correlation Coefficient
Problem Statement: Write a Python program that uses statistics.correlation() to compute the Pearson correlation coefficient between hours studied and test scores, interprets the strength and direction of the relationship, and verifies that it equals the covariance divided by the product of the two standard deviations.
Purpose: The Pearson correlation coefficient is the most widely used measure of linear association in science, engineering, finance, and machine learning. Unlike covariance, its value is always between -1 and +1, making it directly comparable across datasets regardless of units. Understanding it deeply is a prerequisite for linear regression, feature selection, and multicollinearity analysis.
Given Input: hours = [2, 4, 6, 8, 5, 7, 3, 9, 1, 6] and scores = [55, 70, 80, 92, 74, 85, 60, 95, 45, 78]
Expected Output: Pearson r: 0.9934 indicating a very strong positive linear relationship.
▼ Hint
- Call
statistics.correlation(x, y)to get the Pearson r value. It always returns a float between -1.0 and +1.0. - Verify using the relationship:
r = covariance(x, y) / (stdev(x) * stdev(y)). - Print an interpretation guide: r close to +1 is strong positive, close to -1 is strong negative, close to 0 is weak or no linear relationship.
▼ Solution & Explanation
Explanation:
statistics.correlation(x, y): Returns the Pearson correlation coefficient, a dimensionless value in the range [-1, +1]. It was added in Python 3.10 alongsidecovariance(). A value of +1 means perfect positive linear relationship, -1 means perfect negative, and 0 means no linear relationship. For this dataset, r ≈ 0.993 indicates that hours studied is an almost perfect linear predictor of test score.- Relationship to covariance: The Pearson r is the covariance normalised by the product of both standard deviations:
r = cov(x, y) / (stdev(x) * stdev(y)). This division removes the unit-dependence of raw covariance and constrains the result to [-1, +1], making it universally comparable across different datasets and variable types. - R-squared (coefficient of determination): Squaring the Pearson r gives R², which represents the proportion of variance in one variable that is explained by the other. An R² of 0.987 means that 98.7% of the variation in test scores is accounted for by variation in study hours, leaving only 1.3% unexplained by the linear relationship.
Exercise 26: Probability with NormalDist
Problem Statement: Write a Python program that creates a statistics.NormalDist object with mean 70 and standard deviation 10, then calculates the probability of a student scoring above 85, below 60, and between 65 and 80.
Purpose: NormalDist makes working with the normal (Gaussian) distribution straightforward without requiring external libraries like SciPy. It is used in grading, quality control (Six Sigma), A/B testing, risk modelling, and any domain where measurements cluster around a mean. This exercise introduces the CDF (cumulative distribution function), which answers the question “what fraction of values fall below this threshold?”
Given Input: mu = 70, sigma = 10
Expected Output: Probability of scoring above 85, below 60, and between 65 and 80, each expressed as a percentage.
▼ Hint
- Create the distribution with
dist = statistics.NormalDist(mu=70, sigma=10). - Use
dist.cdf(x)to get the probability that a value is less than or equal to x. For “above x”, compute1 - dist.cdf(x). - For a range, compute
dist.cdf(upper) - dist.cdf(lower)to get the probability of falling between the two bounds.
▼ Solution & Explanation
Explanation:
statistics.NormalDist(mu, sigma): Creates a normal distribution object with the given mean and standard deviation. Added in Python 3.8, it provides.cdf(),.pdf(),.inv_cdf(),.overlap(), and.zscore()methods, covering most common normal distribution tasks without needing SciPy.dist.cdf(x): The cumulative distribution function returns the probability that a randomly drawn value from the distribution is less than or equal to x. It always returns a value between 0 and 1. For symmetric distributions like the normal,cdf(mean)is exactly 0.5.- Empirical rule verification: The output confirms the 68-95-99.7 rule: approximately 68.27% of values fall within one standard deviation of the mean, 95.45% within two, and 99.73% within three. This rule is widely used in quality control and statistical process monitoring to set acceptance thresholds.
Exercise 27: Distribution Overlap Between Two Models
Problem Statement: Write a Python program that uses NormalDist.overlap() to compute how much two normal distributions representing the error rates of two machine learning models overlap, then interprets what the overlap coefficient means for model comparison.
Purpose: Overlap between distributions is a practical measure of how distinguishable two groups are – used in medical diagnostic accuracy (sensitivity vs specificity), A/B test analysis (are the two variants really different?), and model evaluation (do two models perform differently or are their results statistically indistinguishable?). NormalDist.overlap() computes this in one call without numerical integration.
Given Input: Model A error rate: NormalDist(mu=0.15, sigma=0.03). Model B error rate: NormalDist(mu=0.22, sigma=0.04).
Expected Output: An overlap coefficient between 0 and 1, with 1 meaning identical distributions and 0 meaning completely separate distributions.
▼ Hint
- Create two
NormalDistobjects and calldist_a.overlap(dist_b)to get the overlap coefficient. - The overlap coefficient is symmetric:
dist_a.overlap(dist_b) == dist_b.overlap(dist_a). - Test the extremes: create two identical distributions and verify the overlap is 1.0, then create two distributions with means far apart and verify the overlap approaches 0.
▼ Solution & Explanation
Explanation:
dist_a.overlap(dist_b): Returns the overlap coefficient, defined as the area under the minimum of the two probability density functions. A value of 1.0 means the distributions are identical. A value of 0.0 means they have no area in common (impossible for two normal distributions unless their means are infinitely far apart). The computation is exact for normal distributions and does not require numerical integration.- Practical interpretation for model comparison: An overlap of around 0.50 (50%) means that knowing a value came from one distribution gives you only modest confidence about which model produced it. A low overlap (say, below 0.20) means the two models are performing sufficiently differently that the distinction is practically meaningful – one is clearly better than the other in terms of error rate.
- Symmetry property: The overlap coefficient is symmetric by definition – swapping the two distributions does not change the shared area. This is verified in the output and is a useful sanity check when debugging code that computes pairwise overlaps for multiple distributions.
Exercise 28: Grading Curve with NormalDist.from_samples()
Problem Statement: Write a Python program that uses NormalDist.from_samples() to fit a normal distribution to raw exam scores, then rescales all scores so that the distribution mean becomes 75, preserving the relative spread of the original grades.
Purpose: Grade curving is a direct application of distribution shifting – adjusting all scores by a constant so the class mean hits a target without changing the relative ranking or spread. NormalDist.from_samples() provides a clean interface for fitting a distribution to observed data, and the resulting object’s parameters drive the rescaling logic.
Given Input: raw_scores = [52, 61, 47, 73, 68, 55, 80, 44, 66, 71, 58, 49, 76, 63, 57]
Expected Output: Original mean, target mean of 75, the shift applied, and the full list of curved scores.
▼ Hint
- Use
statistics.NormalDist.from_samples(data)to fit a normal distribution to the raw scores. It returns aNormalDistobject whose.meanand.stdevmatch the sample. - Compute the shift as
target_mean - dist.meanand add it to every score to curve the grades. - Verify that
statistics.mean(curved_scores)equals the target mean after applying the shift.
▼ Solution & Explanation
Explanation:
NormalDist.from_samples(data): A class method that fits a normal distribution to a list of observed values by computing their sample mean and standard deviation, then returns aNormalDistobject with those parameters. It requires at least two data points and raises aStatisticsErrorfor a single-element list. It is the bridge between raw data and the full set ofNormalDistmethods.- Additive shift preserves spread: Adding a constant to every score shifts the entire distribution horizontally without changing its shape or width. The mean increases by exactly the shift amount, but the standard deviation – which measures spread relative to the mean – remains identical. This means relative performance (rank and distance from classmates) is fully preserved after curving.
- Effect on failure rate: Curving upward shifts the distribution away from the failing threshold, reducing the probability of scores below 60. The output shows this concretely by computing the CDF at 60 before and after the curve, demonstrating how a purely statistical operation translates into a meaningful educational outcome.
Exercise 29: Linear Regression to Predict Scores
Problem Statement: Write a Python program that uses statistics.linear_regression() to fit a line to hours-studied and test-score data, then uses the resulting slope and intercept to predict the score for a student who studies 8 hours.
Purpose: Linear regression is the most widely used predictive modelling technique in statistics and data science. statistics.linear_regression() was added in Python 3.10 as a lightweight built-in implementation for simple (single-variable) regression, removing the need for NumPy or scikit-learn for straightforward prediction tasks. Understanding slope and intercept lays the groundwork for all more advanced regression methods.
Given Input: hours = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] and scores = [50, 55, 60, 65, 70, 75, 80, 82, 88, 95]
Expected Output: Slope, intercept, and predicted score for 8 hours of study.
▼ Hint
- Call
statistics.linear_regression(x, y)which returns a named tuple with.slopeand.interceptattributes. - Predict a value using the line equation:
predicted = slope * x + intercept. - Compute the residuals (actual minus predicted) for each data point and print them to show how well the line fits the data.
▼ Solution & Explanation
Explanation:
statistics.linear_regression(x, y): Returns aLinearRegressionnamed tuple with.slopeand.interceptattributes. It fits the line that minimises the sum of squared vertical distances from the data points to the line (ordinary least squares). Added in Python 3.10, it supports an optionalproportional=Trueargument to force the line through the origin.- Slope interpretation: The slope tells you the expected change in the dependent variable (score) for each one-unit increase in the independent variable (hours). A slope of approximately 4.6 means each additional hour of study is associated with roughly 4.6 more points on the test, on average across the dataset.
- Residuals: The residual for each data point is the difference between the actual value and the value predicted by the line. Small residuals indicate a good fit. Large or systematically patterned residuals (e.g. all positive for high x values) signal that a linear model may not be the best choice and a curved relationship might fit better.
Exercise 30: Z-Score Outlier Detector Combining NormalDist and quantiles()
Problem Statement: Write a Python function that combines statistics.NormalDist and statistics.quantiles() to build a dual-method outlier detector: one based on z-scores from the fitted normal distribution, and one based on the IQR fence method, then compares which values each method flags.
Purpose: No single outlier detection method is universally best. The z-score method assumes normality and is sensitive to extreme values distorting the mean and standard deviation. The IQR method is non-parametric and robust to skew. Combining both in a single function and comparing their outputs builds the critical thinking needed to choose the right tool for a given dataset in real data cleaning pipelines.
Given Input: data = [14, 18, 11, 13, 6, 8, 2, 74, 12, 9, 17, 15, 10, 13, 16, 8, 11, -5, 14, 12]
Expected Output: Outliers flagged by each method printed separately, followed by a comparison showing agreements and disagreements.
▼ Hint
- For the z-score method, fit a
NormalDist.from_samples(data)and usedist.zscore(x)to compute each value’s z-score. Flag values whereabs(z) > threshold(commonly 2.5 or 3). - For the IQR method, use
statistics.quantiles(data, n=4)to get Q1 and Q3, compute IQR, and flag values outside[Q1 - 1.5×IQR, Q3 + 1.5×IQR]. - Use set operations (
&for intersection,|for union,-for difference) to compare which values each method flags exclusively vs which both agree on.
▼ Solution & Explanation
Explanation:
dist.zscore(x): Computes(x - mean) / stdevfor the fitted distribution. A z-score of +2.5 means the value is 2.5 standard deviations above the mean. Because the mean and standard deviation are computed from the full dataset including outliers, extreme values inflate both parameters and can mask themselves – a known weakness of the z-score method called masking or swamping.- IQR method robustness: The IQR uses only the middle 50% of the data (Q1 to Q3) to set its fences, so a single extreme outlier cannot shift the boundaries the way it shifts the mean and standard deviation. This makes the IQR method resistant to the masking problem and more reliable for skewed or heavy-tailed distributions.
- Set operations for comparison: Using Python sets and the
&(intersection),|(union), and-(difference) operators cleanly separates values flagged by both methods, only by z-score, and only by IQR. Values flagged by both are high-confidence outliers. Values flagged by only one method warrant closer inspection to determine whether the data point is genuinely anomalous or whether the method’s assumptions are mismatched to the data.

Leave a Reply