{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Maths, statistics, and random\n",
    "\n",
    "The built-in operators cover arithmetic; the standard library covers the rest. This notebook tours three modules you'll reach for constantly: `math` for mathematical functions, `statistics` for summary statistics, and `random` for generating random numbers. None needs installing — they ship with Python."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## The `math` module\n",
    "\n",
    "`math` is a thin, fast wrapper over your platform's C maths library. It works in `float` (for arbitrary precision use `Decimal`). Here are the groups you'll use most."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Roots, powers, and logs"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "import math\n",
    "\n",
    "print(math.sqrt(16))           # 4.0  (always a float)\n",
    "print(math.isqrt(17))          # 4    integer square root, floored, exact\n",
    "print(math.hypot(3, 4))        # 5.0  sqrt(3**2 + 4**2)\n",
    "print(math.exp(1))             # 2.718281828459045  e**1\n",
    "print(math.log(math.e))        # 1.0  natural log\n",
    "print(math.log(8, 2))          # 3.0  log base 2\n",
    "print(math.log10(1000))        # 3.0"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Rounding helpers\n",
    "\n",
    "Unlike the built-in `round`, these always move in a fixed direction: `floor` down, `ceil` up, `trunc` toward zero. They return an `int`."
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "import math\n",
    "\n",
    "print(math.floor(3.7), math.floor(-3.2))   # 3 -4  (toward negative infinity)\n",
    "print(math.ceil(3.2), math.ceil(-3.7))     # 4 -3  (toward positive infinity)\n",
    "print(math.trunc(3.7), math.trunc(-3.7))   # 3 -3  (toward zero)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Combinatorics and integer maths\n",
    "\n",
    "Exact integer functions for counting problems and number theory — these stay in `int`, so they're exact however large the result."
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "import math\n",
    "\n",
    "print(math.factorial(5))       # 120\n",
    "print(math.comb(5, 2))         # 10   ways to choose 2 of 5 (order ignored)\n",
    "print(math.perm(5, 2))         # 20   ways to arrange 2 of 5 (order matters)\n",
    "print(math.gcd(12, 18))        # 6    greatest common divisor\n",
    "print(math.lcm(4, 6))          # 12   lowest common multiple\n",
    "print(math.prod([1, 2, 3, 4])) # 24   product of an iterable (like sum, but ×)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Trigonometry and constants\n",
    "\n",
    "Trig functions work in **radians**. `math` provides the constants and the `degrees`/`radians` converters."
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "import math\n",
    "\n",
    "print(math.pi, math.e, math.tau)    # 3.141592653589793 2.718281828459045 6.283185307179586\n",
    "print(math.degrees(math.pi))        # 180.0\n",
    "print(math.radians(180))            # 3.141592653589793\n",
    "print(round(math.sin(math.pi / 2), 10))   # 1.0"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Testing special values\n",
    "\n",
    "From the [floating point notebook](https://agilearn.co.uk/guides/numbers-and-maths/learn/02-floating-point): test for `nan` and infinity with these, never with `==`. `isclose` is the safe float comparison."
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "import math\n",
    "\n",
    "print(math.isnan(float('nan')))    # True\n",
    "print(math.isinf(math.inf))        # True\n",
    "print(math.isclose(0.1 + 0.2, 0.3))  # True"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## The `statistics` module\n",
    "\n",
    "For summary statistics without pulling in a third-party library, `statistics` covers the essentials and works with `int`, `float`, `Decimal`, and `Fraction`."
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "import statistics\n",
    "\n",
    "data = [2, 4, 4, 4, 5, 5, 7, 9]\n",
    "print(statistics.mean(data))       # 5\n",
    "print(statistics.median(data))     # 4.5\n",
    "print(statistics.mode(data))       # 4  (most common value)\n",
    "print(statistics.pstdev(data))     # 2.0   population standard deviation\n",
    "print(statistics.stdev(data))      # 2.138...  sample standard deviation"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "`pstdev`/`pvariance` treat the data as the whole population; `stdev`/`variance` treat it as a *sample* (dividing by n−1). Reach for the sample versions when your data is a subset drawn from something larger — the usual case. There's also `fmean` (a faster float mean), `median_low`/`median_high`, and `quantiles`."
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "import statistics\n",
    "\n",
    "data = [2, 4, 4, 4, 5, 5, 7, 9]\n",
    "print(statistics.fmean(data))                  # 5.0 — fast float mean\n",
    "print(statistics.quantiles(data, n=4))         # the three quartile cut points"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## The `random` module\n",
    "\n",
    "`random` generates pseudo-random numbers. \"Pseudo\" matters: the sequence is determined by a starting **seed**, so seeding makes runs reproducible — invaluable for tests and reproducible experiments."
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "import random\n",
    "\n",
    "random.seed(42)                # fix the seed -> same sequence every run\n",
    "print(random.random())         # 0.6394267984578837  float in [0.0, 1.0)\n",
    "print(random.uniform(1, 10))   # 1.2250967970040025  a float in [1, 10]\n",
    "print(random.randint(1, 6))    # an int in [1, 6] inclusive — a dice roll"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Choosing from a sequence\n",
    "\n",
    "`choice` picks one item, `choices` picks several *with* replacement (and optional weights), `sample` picks several *without* replacement, and `shuffle` reorders a list in place."
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "import random\n",
    "random.seed(42)\n",
    "\n",
    "colours = ['red', 'green', 'blue', 'yellow']\n",
    "print(random.choice(colours))               # one item\n",
    "print(random.sample(colours, 2))            # two distinct items (no repeats)\n",
    "print(random.choices(colours, k=3))         # three, repeats allowed\n",
    "print(random.choices(colours, weights=[10, 1, 1, 1], k=5))  # 'red' favoured\n",
    "\n",
    "deck = [1, 2, 3, 4, 5]\n",
    "random.shuffle(deck)                         # shuffles in place\n",
    "print(deck)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### A normal distribution, and a security warning\n",
    "\n",
    "`random.gauss(mu, sigma)` draws from a normal distribution. But the whole `random` module is **not cryptographically secure** — its output is predictable if an attacker learns the state. For passwords, tokens, or anything security-sensitive, use the `secrets` module instead."
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "import random, secrets\n",
    "random.seed(42)\n",
    "\n",
    "print(random.gauss(0, 1))          # a sample from a standard normal\n",
    "\n",
    "# For security-sensitive randomness, NOT random:\n",
    "print(secrets.token_hex(16))       # a secure random token\n",
    "print(secrets.choice(['a', 'b', 'c']))   # a secure pick"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Recap\n",
    "\n",
    "- `math` — roots, logs, `floor`/`ceil`/`trunc`, combinatorics (`comb`, `perm`, `factorial`, `gcd`, `lcm`, `prod`), trig (in radians), constants, and the `isnan`/`isinf`/`isclose` tests.\n",
    "- `statistics` — `mean`, `median`, `mode`, and standard deviation/variance in population (`pstdev`) and sample (`stdev`) forms.\n",
    "- `random` — seed for reproducibility; `random`, `uniform`, `randint`, `choice`, `choices` (with weights), `sample`, `shuffle`, `gauss`.\n",
    "- For security-sensitive randomness, use **`secrets`**, never `random`.\n",
    "\n",
    "That's the toolkit. The [Recipes](https://agilearn.co.uk/guides/numbers-and-maths/recipes) put it to work — rounding correctly, handling money, and formatting numbers for display."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python",
   "version": "3.10"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}