From b0ad09c2deaff4bcb405b387d06898a6a3d825c2 Mon Sep 17 00:00:00 2001 From: Matt McKay Date: Mon, 21 Sep 2026 23:29:19 +1000 Subject: [PATCH 1/3] =?UTF-8?q?dev:=20v0.29.2=20released=20=E2=80=94=20?= =?UTF-8?q?=C2=A74a=20gate=20completed,=20@v0=20=3D=205f74d74;=20round-4?= =?UTF-8?q?=20numpy=20arm=20at=20v0.29.2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Gate 84/84 sync, 28/28 delivery + 28/28 engineVersion 0.29.2 verdicts per lane; scenario 17 on .ml delivered. Round 4 regenerated at @v0: three draws archived, draw 1 sent with one disclosed ml_repair.py comma. Co-Authored-By: Claude Fable 5.1 --- .dev/STATE.md | 17 +- .dev/log/2026-09-21-v0291-gate-scenario17.md | 4 + .../2026-09-21-round4-numpy-v0.29.2/README.md | 27 + .../numpy-v0.29.2-draw1-repaired.md | 1546 ++++++++++++++++ .../numpy-v0.29.2-draw1.md | 1546 ++++++++++++++++ .../numpy-v0.29.2-draw2.md | 1549 ++++++++++++++++ .../numpy-v0.29.2-draw3.md | 1557 +++++++++++++++++ 7 files changed, 6241 insertions(+), 5 deletions(-) create mode 100644 experiments/ml-benchmark/arms/2026-09-21-round4-numpy-v0.29.2/README.md create mode 100644 experiments/ml-benchmark/arms/2026-09-21-round4-numpy-v0.29.2/numpy-v0.29.2-draw1-repaired.md create mode 100644 experiments/ml-benchmark/arms/2026-09-21-round4-numpy-v0.29.2/numpy-v0.29.2-draw1.md create mode 100644 experiments/ml-benchmark/arms/2026-09-21-round4-numpy-v0.29.2/numpy-v0.29.2-draw2.md create mode 100644 experiments/ml-benchmark/arms/2026-09-21-round4-numpy-v0.29.2/numpy-v0.29.2-draw3.md diff --git a/.dev/STATE.md b/.dev/STATE.md index c4b33fe..a88d2ea 100644 --- a/.dev/STATE.md +++ b/.dev/STATE.md @@ -7,11 +7,18 @@ Roadmap detail lives in the work-plan tracker **#257**, not here (PLAN.md predat ## In flight -- **v0.29.2 release cut 2026-09-21** (payload #317 `62504c1`, on top of #315 `cd41558`) — patch: - the scope sentence on the `ml` exercise rule that v0.29.1's failed gate called for, carrying - with it the editor's round-3 answers that v0.29.1 never delivered. §4a gate status is - recorded on the release PR; scenario 17 on the `.ml` lane is the one to read first. After the - floating tags move, round 4 (`numpy`, lecture-python-programming.ml#23) is regenerated at `@v0`. +- **v0.29.2 released 2026-09-21** (release PR #318 `5f74d74`; payload #317 `62504c1`, on top of + #315 `cd41558`; §4a gate **completed**: 84/84 sync runs, 28/28 delivery + 28/28 + `engineVersion: 0.29.2` verdicts per lane, scenario 17 on `.ml` delivered; `v0.29` = `v0` = + `5f74d74`; GitHub release published; `@v0` smoke `engineRef: v0` on all three lanes — tally + on #318) — patch: the scope sentence on the `ml` exercise rule that v0.29.1's failed gate + called for, carrying the editor's round-3 answers that v0.29.1 never delivered. **Round 4 + (`numpy`, lecture-python-programming.ml#23) regenerated at `@v0` the same day**: three draws, + draw 1 sent (bare endings 1 / 37 / 2), one `ml_repair.py` comma applied and disclosed — the + first repaired seed; arm `experiments/ml-benchmark/arms/2026-09-21-round4-numpy-v0.29.2/`. + Harness note: the reset left one PR from the previous gate open on `.ml` (closed by hand). + W1 (#259) still targets v0.30.0. **Next engine-side: the rule-2 arm** (rewrite around the + editor's everyday-speech test, judged held-out before it ships). - **v0.29.1 is tagged but NOT released (2026-09-21)** — its §4a gate came back 83/84: scenario 17 on the `.ml` lane failed twice (the model wrapped a plain `## Exercises` list in `{exercise-start}`; structural parity refused the file). `v0` = `v0.29` = `a6fda54` (v0.29.0) diff --git a/.dev/log/2026-09-21-v0291-gate-scenario17.md b/.dev/log/2026-09-21-v0291-gate-scenario17.md index 39eee5e..15ee849 100644 --- a/.dev/log/2026-09-21-v0291-gate-scenario17.md +++ b/.dev/log/2026-09-21-v0291-gate-scenario17.md @@ -24,3 +24,7 @@ The first six draws per arm read 1/6 against 5/6 and looked like a clean regress **Merged, release cut.** #317 merged as `62504c1` (Copilot: two release-status wording comments — STATE entries reworded, CHANGELOG marks 0.29.1 `[YANKED]` with a note, `cc7c6ab`); release PR for v0.29.2 opened on `release-v0.29.2`. **Next (as written before the merge)**: PR → merge → **v0.29.2** (v0.29.1 stays a tag that was never released; the CHANGELOG says so) → §4a gate on the new tag → floating tags → smoke → release → regenerate round 4 at `@v0`. + +**Addendum — v0.29.2 released.** #318 merged `5f74d74`; tagged `v0.29.2`; §4a gate on the tag: census 9/9 at `@v0.29.2`, 28 source PRs, **84/84 sync success**, 28/28 delivery and **28/28 verdicts at `engineVersion: 0.29.2` per lane**; scenario 17 on `.ml` delivered (test-translation-sync.ml#242). The `.ml` lane briefly read 29 PRs: the reset had left test-translation-sync.ml#192 open from the v0.29.1 gate — closed by hand, and worth a look in the script's close-all step. `v0.29` and `v0` → `5f74d74`; `@v0` smoke (scenario 01) 3/3, `engineRef: v0` on all lanes; GitHub release published (title = tag; notes cover #315 and #317 and say not to pin `v0.29.1`). Tally on #318. + +**Round 4 regenerated at `@v0`.** Three draws of `numpy`, draw 1 chosen by lint (bare endings 1 / 37 / 2), `ml_repair.py` applied — one comma, `Columns-ഉം, rows-ഉം` — and disclosed; force-updated lecture-python-programming.ml#23 (`889d001`), retitled, description rewritten, editor told it is ready. He had not started on the v0.29.0 draft. Arm: `experiments/ml-benchmark/arms/2026-09-21-round4-numpy-v0.29.2/`. diff --git a/experiments/ml-benchmark/arms/2026-09-21-round4-numpy-v0.29.2/README.md b/experiments/ml-benchmark/arms/2026-09-21-round4-numpy-v0.29.2/README.md new file mode 100644 index 0000000..cde85da --- /dev/null +++ b/experiments/ml-benchmark/arms/2026-09-21-round4-numpy-v0.29.2/README.md @@ -0,0 +1,27 @@ +# Arm: round 4 (`numpy`) regenerated — three draws at v0.29.2, chosen by lint, 2026-09-21 + +The round-4 calibration seed for lecture-python-programming.ml was first generated at v0.29.0 +(`../2026-09-18-round4-numpy-v0.29.0/`, draw 3 sent). The editor then answered the round-3 +questions (lecture-python-programming.ml#22) before starting on it, so the seed was regenerated +at the release that carries his answers — the round-3 precedent at v0.28.0. Three draws at +v0.29.2 (`5f74d74` = `@v0`) from `lecture-python-programming@b0b0b56` (`numpy.md` unchanged +since `1706cea`), `init -f numpy.md --localize none -m claude-sonnet-5`. + +| Draw | bare endings before a cell/list | paragraph without punctuation | lowercase-initial | banned | hortative watch | `-ഉം` pair, no comma | *For example* → ഉദാഹരണത്തിന് | `provide ചെയ്യ…` | exercise blocks | headings / code cells | +|---|---|---|---|---|---|---|---|---|---|---| +| 1 (**sent**, after repair) | **1** | 0 | 0 | 1 | 1 | 1 → 0 | 0 | 0 | 8/8 | identical | +| 2 | **37** | 1 | 0 | 2 | 0 | 1 | 6 | 0 | 8/8 | identical | +| 3 | **2** | 8 | 2 | 2 | 2 | 1 | 0 | 0 | 8/8 | identical | +| v0.29.0 draw 3 (first sent) | 3 | 0 | 0 | 3 | 1 | 2 | 0 | 6 | 8/8 | identical | + +`ml_repair.py` was applied to draw 1 and **disclosed on the PR** — the first seed to go to the +editor repaired. It made one change: `Columns-ഉം rows-ഉം` → `Columns-ഉം, rows-ഉം` (line 280). +Every draw here leaves exactly one hyphenated pair without its comma — that pair in draws 1 and +3, `Step 1-ഉം 2-ഉം` in draw 2 — and those are the same two lines the 2026-09-21 answers arm +missed: the prompt rule does not reach them, the script does. Left as +generated and disclosed: line 878 (ends on a closing bracket before a cell), line 898 (*In +fact* → വാസ്തവത്തിൽ, a banned rendering), line 1092 (a genuine future statement the hortative +watch flags). Whether he touches the repaired line is the first field evidence for #260. + +Terminal punctuation remains the draw-dependent class (1 / 37 / 2 here; 37 / 34 / 3 at +v0.29.0): best-of-N by lint is still what keeps it away from the editor. diff --git a/experiments/ml-benchmark/arms/2026-09-21-round4-numpy-v0.29.2/numpy-v0.29.2-draw1-repaired.md b/experiments/ml-benchmark/arms/2026-09-21-round4-numpy-v0.29.2/numpy-v0.29.2-draw1-repaired.md new file mode 100644 index 0000000..3c7a737 --- /dev/null +++ b/experiments/ml-benchmark/arms/2026-09-21-round4-numpy-v0.29.2/numpy-v0.29.2-draw1-repaired.md @@ -0,0 +1,1546 @@ +--- +jupytext: + text_representation: + extension: .md + format_name: myst +kernelspec: + display_name: Python 3 + language: python + name: python3 +translation: + title: NumPy + headings: + Overview: Overview + NumPy Arrays: NumPy Arrays + NumPy Arrays::Basics: Basics + NumPy Arrays::Shape and Dimension: Shape and Dimension + NumPy Arrays::Creating Arrays: Creating Arrays + NumPy Arrays::Array Indexing: Array Indexing + NumPy Arrays::Array Methods: Array Methods + Arithmetic Operations: Arithmetic Operations + Matrix Multiplication: Matrix Multiplication + Broadcasting: Broadcasting + Mutability and Copying Arrays: Mutability and Copying Arrays + Mutability and Copying Arrays::Mutability: Mutability + Mutability and Copying Arrays::Making Copies: Making Copies + Additional Features: Additional Features + Additional Features::Universal Functions: Universal Functions + Additional Features::Comparisons: Comparisons + Additional Features::Sub-packages: Sub-packages + Additional Features::Implicit Multithreading: Implicit Multithreading + Exercises: Exercises +--- + +(np)= +```{raw} jupyter + +``` + +# {index}`NumPy ` + +```{index} single: Python; NumPy +``` + +```{epigraph} +"Let's be clear: the work of science has nothing whatever to do with consensus. Consensus is the business of politics. Science, on the contrary, requires only one investigator who happens to be right, which means that he or she has results that are verifiable by reference to the real world. In science consensus is irrelevant. What is relevant is reproducible results." -- Michael Crichton +``` + +Anaconda-യിൽ ഉള്ളതിന് പുറമേ, ഈ lecture-ന് താഴെ പറയുന്ന libraries ആവശ്യമായിവരുന്നു: + +```{code-cell} ipython3 +:tags: [hide-output] + +!pip install quantecon +``` + +## Overview + +[NumPy](https://en.wikipedia.org/wiki/NumPy) എന്നത്, numerical programming-നുള്ള ഒരു മികച്ച library ആണ്. + +* Academia, finance, industry എന്നിവയിൽ വ്യാപകമായി ഉപയോഗിക്കപ്പെടുന്നു. +* Mature-ഉം, fast-ഉം, stable-ഉം ആണ്, തുടർച്ചയായി development-ലും ആണ്. + +മുൻ lectures-ൽ NumPy ഉൾപ്പെടുന്ന കുറച്ച് code നമ്മൾ already കണ്ടിട്ടുണ്ട്. + +ഈ lecture-ൽ നമ്മൾ ചെയ്യാൻ പോകുന്ന കാര്യങ്ങൾ: + +1. NumPy arrays, ഒപ്പം +1. NumPy നൽകുന്ന അടിസ്ഥാന array processing operations. + +(ഒരു alternative reference-ന്, [the official NumPy documentation](https://numpy.org/doc/stable/reference/) നോക്കുക.) + +താഴെ പറയുന്ന imports നമുക്ക് ഉപയോഗിക്കാം. + +```{code-cell} python3 +import numpy as np +import random +import quantecon as qe +import matplotlib.pyplot as plt +from mpl_toolkits.mplot3d.axes3d import Axes3D +from matplotlib import cm +``` + + + +(numpy_array)= +## NumPy Arrays + +```{index} single: NumPy; Arrays +``` + +NumPy പരിഹരിക്കുന്ന അടിസ്ഥാന പ്രശ്നം, വേഗതയേറിയ array processing ആണ്. + +NumPy define ചെയ്യുന്ന ഏറ്റവും പ്രധാനപ്പെട്ട structure, ഒരു array data type ആണ്, ഔപചാരികമായി ഇതിനെ [numpy.ndarray](https://numpy.org/doc/stable/reference/arrays.ndarray.html) എന്ന് വിളിക്കുന്നു. + +Scientific Python ecosystem-ന്റെ വളരെ വലിയൊരു ഭാഗം NumPy arrays ആണ് പ്രവർത്തിപ്പിക്കുന്നത്. + +### Basics + +Zeros മാത്രം അടങ്ങിയ ഒരു NumPy array create ചെയ്യാൻ നമ്മൾ [np.zeros](https://numpy.org/doc/stable/reference/generated/numpy.zeros.html#numpy.zeros) ഉപയോഗിക്കുന്നു. + +```{code-cell} python3 +a = np.zeros(3) +a +``` + +```{code-cell} python3 +type(a) +``` + +NumPy arrays, native Python lists-നെ കുറച്ചൊക്കെ പോലെയാണ്, പക്ഷേ വ്യത്യാസം എന്തെന്നാൽ: + +* Data *homogeneous ആയിരിക്കണം* (എല്ലാ elements-ഉം ഒരേ type-ലുള്ളതായിരിക്കണം). +* ഈ types, NumPy നൽകുന്ന [data types](https://numpy.org/doc/stable/reference/arrays.dtypes.html) (`dtypes`) ഇൽ ഒന്നായിരിക്കണം. + +ഈ dtypes-ൽ ഏറ്റവും പ്രധാനപ്പെട്ടവ: + +* float64: 64 bit floating-point number +* int64: 64 bit integer +* bool: 8 bit True or False + +Complex numbers, unsigned integers, തുടങ്ങിയവ represent ചെയ്യാൻ ഉള്ള dtypes-ഉം ഉണ്ട്. + +ആധുനിക machines-ൽ, arrays-ന്റെ default dtype `float64` ആണ്. + +```{code-cell} python3 +a = np.zeros(3) +type(a[0]) +``` + +Integers ഉപയോഗിക്കണമെങ്കിൽ താഴെ കാണിച്ചിരിക്കുന്ന പോലെ specify ചെയ്യാം: + +```{code-cell} python3 +a = np.zeros(3, dtype=int) +type(a[0]) +``` + +(numpy_shape_dim)= +### Shape and Dimension + +```{index} single: NumPy; Arrays (Shape and Dimension) +``` + +താഴെ കൊടുത്തിരിക്കുന്ന assignment നോക്കാം: + +```{code-cell} python3 +z = np.zeros(10) +``` + +ഇവിടെ `z` എന്നത് ഒരു **flat** array ആണ് --- row vector-ഉം അല്ല column vector-ഉം അല്ല. + +```{code-cell} python3 +z.shape +``` + +ഇവിടെ shape tuple-ന് ഒരു element മാത്രമേയുള്ളൂ, അതായത് array-യുടെ length (ഒരു element മാത്രമുള്ള tuples ഒരു comma-യിൽ അവസാനിക്കും). + +ഇതിന് ഒരു additional dimension നൽകാൻ, `shape` attribute നമുക്ക് മാറ്റാം: + +```{code-cell} python3 +z.shape = (10, 1) # Convert flat array to column vector (two-dimensional) +z +``` + +```{code-cell} python3 +z = np.zeros(4) # Flat array +z.shape = (2, 2) # Two-dimensional array +z +``` + +അവസാനത്തെ case-ൽ, 2x2 array ഉണ്ടാക്കാൻ, `zeros()` function-ന് ഒരു tuple pass ചെയ്യാം, `z = np.zeros((2, 2))` എന്ന പോലെ. + + + +(creating_arrays)= +### Creating Arrays + +```{index} single: NumPy; Arrays (Creating) +``` + +നമ്മൾ കണ്ടത് പോലെ, `np.zeros` function zeros-ന്റെ ഒരു array create ചെയ്യുന്നു. + +`np.ones` എന്താണ് create ചെയ്യുന്നതെന്ന് നിങ്ങൾക്ക് ഊഹിക്കാൻ കഴിയും. + +ഇതുമായി ബന്ധപ്പെട്ടതാണ് `np.empty`, ഇത് memory-യിൽ arrays create ചെയ്യുന്നു, പിന്നീട് data-കൊണ്ട് നിറയ്ക്കാവുന്നത്: + +```{code-cell} python3 +z = np.empty(3) +z +``` + +ഇവിടെ കാണുന്ന numbers garbage values ആണ്. + +(Python 3 contiguous 64 bit memory pieces allocate ചെയ്യുന്നു, ആ memory slots-ലെ നിലവിലുള്ള contents `float64` values ആയി interpret ചെയ്യപ്പെടുന്നു) + +Evenly spaced numbers-ന്റെ ഒരു grid set up ചെയ്യാൻ `np.linspace` ഉപയോഗിക്കുക: + +```{code-cell} python3 +z = np.linspace(2, 4, 5) # From 2 to 4, with 5 elements +``` + +ഒരു identity matrix create ചെയ്യാൻ `np.identity` അല്ലെങ്കിൽ `np.eye` ഉപയോഗിക്കുക: + +```{code-cell} python3 +z = np.identity(2) +z +``` + +കൂടാതെ, `np.array` ഉപയോഗിച്ച് Python lists, tuples, തുടങ്ങിയവയിൽ നിന്നും NumPy arrays create ചെയ്യാം: + +```{code-cell} python3 +z = np.array([10, 20]) # ndarray from Python list +z +``` + +```{code-cell} python3 +type(z) +``` + +```{code-cell} python3 +z = np.array((10, 20), dtype=float) # Here 'float' is equivalent to 'np.float64' +z +``` + +```{code-cell} python3 +z = np.array([[1, 2], [3, 4]]) # 2D array from a list of lists +z +``` + +`np.asarray` എന്നതും കാണുക, ഇത് similar ആയ ഒരു function ആണ്, പക്ഷേ NumPy array-യിൽ already ഉള്ള data-യുടെ distinct copy ഉണ്ടാക്കുന്നില്ല. + +Numeric data അടങ്ങിയ ഒരു text file-ൽ നിന്നും array data read ചെയ്യാൻ `np.loadtxt` ഉപയോഗിക്കുക --- വിശദാംശങ്ങൾക്ക് [the documentation](https://numpy.org/doc/stable/reference/routines.io.html) കാണുക. + + + +### Array Indexing + +```{index} single: NumPy; Arrays (Indexing) +``` + +ഒരു flat array-ന്, indexing Python sequences-ന്റേത് പോലെ തന്നെയാണ്: + +```{code-cell} python3 +z = np.linspace(1, 2, 5) +z +``` + +```{code-cell} python3 +z[0] +``` + +```{code-cell} python3 +z[0:2] # Two elements, starting at element 0 +``` + +```{code-cell} python3 +z[-1] +``` + +2D arrays-ന് index syntax താഴെ കാണിച്ചിരിക്കുന്ന പോലെയാണ്: + +```{code-cell} python3 +z = np.array([[1, 2], [3, 4]]) +z +``` + +```{code-cell} python3 +z[0, 0] +``` + +```{code-cell} python3 +z[0, 1] +``` + +ഇങ്ങനെ തുടരും. + +Columns-ഉം, rows-ഉം താഴെ കാണിച്ചിരിക്കുന്ന പോലെ extract ചെയ്യാം: + +```{code-cell} python3 +z[0, :] +``` + +```{code-cell} python3 +z[:, 1] +``` + +Integers-ന്റെ NumPy arrays-ഉം elements extract ചെയ്യാൻ ഉപയോഗിക്കാം: + +```{code-cell} python3 +z = np.linspace(2, 4, 5) +z +``` + +```{code-cell} python3 +indices = np.array((0, 2, 3)) +z[indices] +``` + +അവസാനമായി, `dtype bool` ഉള്ള ഒരു array-യും elements extract ചെയ്യാൻ ഉപയോഗിക്കാം: + +```{code-cell} python3 +z +``` + +```{code-cell} python3 +d = np.array([0, 1, 1, 0, 0], dtype=bool) +d +``` + +```{code-cell} python3 +z[d] +``` + +ഇത് എന്തുകൊണ്ട് useful ആണെന്ന് താഴെ നമുക്ക് കാണാം. + +ഒരു ചെറിയ കാര്യം കൂടി: slice notation ഉപയോഗിച്ച് ഒരു array-യിലെ എല്ലാ elements-ഉം ഒരു number-ന് തുല്യമായി set ചെയ്യാം: + +```{code-cell} python3 +z = np.empty(3) +z +``` + +```{code-cell} python3 +z[:] = 42 +z +``` + +### Array Methods + +```{index} single: NumPy; Arrays (Methods) +``` + +Arrays-ന് useful ആയ methods ഉണ്ട്, ഇവയെല്ലാം carefully optimize ചെയ്തിരിക്കുന്നു: + +```{code-cell} python3 +a = np.array((4, 3, 2, 1)) +a +``` + +```{code-cell} python3 +a.sort() # Sorts a in place +a +``` + +```{code-cell} python3 +a.sum() # Sum +``` + +```{code-cell} python3 +a.mean() # Mean +``` + +```{code-cell} python3 +a.max() # Max +``` + +```{code-cell} python3 +a.argmax() # Returns the index of the maximal element +``` + +```{code-cell} python3 +a.cumsum() # Cumulative sum of the elements of a +``` + +```{code-cell} python3 +a.cumprod() # Cumulative product of the elements of a +``` + +```{code-cell} python3 +a.var() # Variance +``` + +```{code-cell} python3 +a.std() # Standard deviation +``` + +```{code-cell} python3 +a.shape = (2, 2) +a.T # Equivalent to a.transpose() +``` + +അറിഞ്ഞിരിക്കേണ്ട മറ്റൊരു method ആണ് `searchsorted()`. + +`z` എന്നത് ഒരു nondecreasing array ആണെങ്കിൽ, `z.searchsorted(a)` എന്നത് `z`-യിലെ `>= a` ആയ ആദ്യത്തെ element-ന്റെ index return ചെയ്യുന്നു: + +```{code-cell} python3 +z = np.linspace(2, 4, 5) +z +``` + +```{code-cell} python3 +z.searchsorted(2.2) +``` + + +## Arithmetic Operations + +```{index} single: NumPy; Arithmetic Operations +``` + +`+`, `-`, `*`, `/`, `**` എന്നീ operators എല്ലാം arrays-ൽ *elementwise* ആയി act ചെയ്യുന്നു: + +```{code-cell} python3 +a = np.array([1, 2, 3, 4]) +b = np.array([5, 6, 7, 8]) +a + b +``` + +```{code-cell} python3 +a * b +``` + +താഴെ കാണിച്ചിരിക്കുന്ന പോലെ ഓരോ element-ഇനും ഒരു scalar നമുക്ക് add ചെയ്യാം: + +```{code-cell} python3 +a + 10 +``` + +Scalar multiplication similar ആണ്: + +```{code-cell} python3 +a * 10 +``` + +Two-dimensional arrays-ഉം അതേ general rules തന്നെ follow ചെയ്യുന്നു: + +```{code-cell} python3 +A = np.ones((2, 2)) +B = np.ones((2, 2)) +A + B +``` + +```{code-cell} python3 +A + 10 +``` + +```{code-cell} python3 +A * B +``` + +(numpy_matrix_multiplication)= +In particular, `A * B` എന്നത് matrix product *അല്ല*, ഇത് ഒരു element-wise product ആണ്. + + +## Matrix Multiplication + +```{index} single: NumPy; Matrix Multiplication +``` + +```{index} single: NumPy; Matrix Multiplication +``` + +Matrix multiplication-ന് നമ്മൾ `@` symbol ഉപയോഗിക്കുന്നു, താഴെ കാണിച്ചിരിക്കുന്ന പോലെ: + +```{code-cell} python3 +A = np.ones((2, 2)) +B = np.ones((2, 2)) +A @ B +``` + +Syntax flat arrays-ലും work ചെയ്യുന്നു --- നിങ്ങൾക്ക് എന്താണ് വേണ്ടതെന്ന് NumPy ഒരു educated guess നടത്തുന്നു: + +```{code-cell} python3 +A @ (0, 1) +``` + +നമ്മൾ post-multiplying ചെയ്യുന്നതിനാൽ, tuple ഒരു column vector ആയി treat ചെയ്യപ്പെടുന്നു. + + + +(broadcasting)= +## Broadcasting + +```{index} single: NumPy; Broadcasting +``` + +(ഈ section, [Jake VanderPlas](https://jakevdp.github.io/PythonDataScienceHandbook/02.05-computation-on-arrays-broadcasting.html) നൽകിയ broadcasting-നെക്കുറിച്ചുള്ള ഒരു മികച്ച discussion extend ചെയ്യുന്നു.) + +```{note} +Broadcasting എന്നത് NumPy-യുടെ വളരെ പ്രധാനപ്പെട്ട ഒരു aspect ആണ്. അതേസമയം, advanced broadcasting താരതമ്യേന complex ആണ്, താഴെ പറയുന്ന ചില details ആദ്യമായി വായിക്കുമ്പോൾ skim ചെയ്ത് പോകാം. +``` + +Element-wise operations-ൽ, arrays-ന് ഒരേ shape ഉണ്ടാകണമെന്നില്ല. + +ഇത് സംഭവിക്കുമ്പോൾ, കഴിയുന്നിടത്തെല്ലാം NumPy automatically arrays-നെ ഒരേ shape-ലേക്ക് expand ചെയ്യും. + +NumPy-യിലെ ഈ useful ആയ (എന്നാൽ ചിലപ്പോൾ confusing ആയ) feature-നെ **broadcasting** എന്ന് വിളിക്കുന്നു. + +Broadcasting-ന്റെ value എന്തെന്നാൽ: + +* `for` loops ഒഴിവാക്കാം, ഇത് numerical code വേഗത്തിൽ run ചെയ്യാൻ സഹായിക്കുന്നു, ഒപ്പം +* arrays-ന്റെ ഈ dimensions memory-യിൽ actually create ചെയ്യാതെ തന്നെ arrays-ൽ operations implement ചെയ്യാൻ broadcasting നമ്മെ അനുവദിക്കുന്നു, arrays വലുതാകുമ്പോൾ ഇത് പ്രധാനമാകാം. + +For example, `a` എന്നത് ഒരു $3 \times 3$ array ആണെന്ന് കരുതുക (`a -> (3, 3)`), അതേസമയം `b` എന്നത് മൂന്ന് elements ഉള്ള ഒരു flat array ആണ് (`b -> (3,)`). + +ഇവയെ ഒരുമിച്ച് add ചെയ്യുമ്പോൾ, NumPy automatically `b -> (3,)` എന്നതിനെ `b -> (3, 3)` ആയി expand ചെയ്യും. + +Element-wise addition-ന്റെ ഫലം ഒരു $3 \times 3$ array ആയിരിക്കും: + +```{code-cell} python3 + +a = np.array( + [[1, 2, 3], + [4, 5, 6], + [7, 8, 9]]) +b = np.array([3, 6, 9]) + +a + b +``` + +ഈ broadcasting operation-ന്റെ ഒരു visual representation താഴെ കാണാം: + +```{code-cell} python3 +--- +tags: [hide-input] +--- +# Adapted and modified based on the code in the book written by Jake VanderPlas (see https://jakevdp.github.io/PythonDataScienceHandbook/06.00-figure-code.html#Broadcasting) +# Originally from astroML: see https://www.astroml.org/book_figures/appendix/fig_broadcast_visual.html + + +def draw_cube(ax, xy, size, depth=0.4, + edges=None, label=None, label_kwargs=None, **kwargs): + """draw and label a cube. edges is a list of numbers between + 1 and 12, specifying which of the 12 cube edges to draw""" + if edges is None: + edges = range(1, 13) + + x, y = xy + + if 1 in edges: + ax.plot([x, x + size], + [y + size, y + size], **kwargs) + if 2 in edges: + ax.plot([x + size, x + size], + [y, y + size], **kwargs) + if 3 in edges: + ax.plot([x, x + size], + [y, y], **kwargs) + if 4 in edges: + ax.plot([x, x], + [y, y + size], **kwargs) + + if 5 in edges: + ax.plot([x, x + depth], + [y + size, y + depth + size], **kwargs) + if 6 in edges: + ax.plot([x + size, x + size + depth], + [y + size, y + depth + size], **kwargs) + if 7 in edges: + ax.plot([x + size, x + size + depth], + [y, y + depth], **kwargs) + if 8 in edges: + ax.plot([x, x + depth], + [y, y + depth], **kwargs) + + if 9 in edges: + ax.plot([x + depth, x + depth + size], + [y + depth + size, y + depth + size], **kwargs) + if 10 in edges: + ax.plot([x + depth + size, x + depth + size], + [y + depth, y + depth + size], **kwargs) + if 11 in edges: + ax.plot([x + depth, x + depth + size], + [y + depth, y + depth], **kwargs) + if 12 in edges: + ax.plot([x + depth, x + depth], + [y + depth, y + depth + size], **kwargs) + + if label: + if label_kwargs is None: + label_kwargs = {} + ax.text(x + 0.5 * size, y + 0.5 * size, label, + ha='center', va='center', **label_kwargs) + +solid = dict(c='black', ls='-', lw=1, + label_kwargs=dict(color='k')) +dotted = dict(c='black', ls='-', lw=0.5, alpha=0.5, + label_kwargs=dict(color='gray')) +depth = 0.3 + +# Draw a figure and axis with no boundary +fig = plt.figure(figsize=(5, 1), facecolor='w') +ax = plt.axes([0, 0, 1, 1], xticks=[], yticks=[], frameon=False) + +# first block +draw_cube(ax, (1, 7.5), 1, depth, [1, 2, 3, 4, 5, 6, 9], '1', **solid) +draw_cube(ax, (2, 7.5), 1, depth, [1, 2, 3, 6, 9], '2', **solid) +draw_cube(ax, (3, 7.5), 1, depth, [1, 2, 3, 6, 7, 9, 10], '3', **solid) + +draw_cube(ax, (1, 6.5), 1, depth, [2, 3, 4], '4', **solid) +draw_cube(ax, (2, 6.5), 1, depth, [2, 3], '5', **solid) +draw_cube(ax, (3, 6.5), 1, depth, [2, 3, 7, 10], '6', **solid) + +draw_cube(ax, (1, 5.5), 1, depth, [2, 3, 4], '7', **solid) +draw_cube(ax, (2, 5.5), 1, depth, [2, 3], '8', **solid) +draw_cube(ax, (3, 5.5), 1, depth, [2, 3, 7, 10], '9', **solid) + +# second block +draw_cube(ax, (6, 7.5), 1, depth, [1, 2, 3, 4, 5, 6, 9], '3', **solid) +draw_cube(ax, (7, 7.5), 1, depth, [1, 2, 3, 6, 9], '6', **solid) +draw_cube(ax, (8, 7.5), 1, depth, [1, 2, 3, 6, 7, 9, 10], '9', **solid) + +draw_cube(ax, (6, 6.5), 1, depth, range(2, 13), '3', **dotted) +draw_cube(ax, (7, 6.5), 1, depth, [2, 3, 6, 7, 9, 10, 11], '6', **dotted) +draw_cube(ax, (8, 6.5), 1, depth, [2, 3, 6, 7, 9, 10, 11], '9', **dotted) + +draw_cube(ax, (6, 5.5), 1, depth, [2, 3, 4, 7, 8, 10, 11, 12], '3', **dotted) +draw_cube(ax, (7, 5.5), 1, depth, [2, 3, 7, 10, 11], '6', **dotted) +draw_cube(ax, (8, 5.5), 1, depth, [2, 3, 7, 10, 11], '9', **dotted) + +# third block +draw_cube(ax, (12, 7.5), 1, depth, [1, 2, 3, 4, 5, 6, 9], '4', **solid) +draw_cube(ax, (13, 7.5), 1, depth, [1, 2, 3, 6, 9], '8', **solid) +draw_cube(ax, (14, 7.5), 1, depth, [1, 2, 3, 6, 7, 9, 10], '12', **solid) + +draw_cube(ax, (12, 6.5), 1, depth, [2, 3, 4], '7', **solid) +draw_cube(ax, (13, 6.5), 1, depth, [2, 3], '11', **solid) +draw_cube(ax, (14, 6.5), 1, depth, [2, 3, 7, 10], '15', **solid) + +draw_cube(ax, (12, 5.5), 1, depth, [2, 3, 4], '10', **solid) +draw_cube(ax, (13, 5.5), 1, depth, [2, 3], '14', **solid) +draw_cube(ax, (14, 5.5), 1, depth, [2, 3, 7, 10], '18', **solid) + +ax.text(5, 7.0, '+', size=12, ha='center', va='center') +ax.text(10.5, 7.0, '=', size=12, ha='center', va='center'); +``` + +`b -> (3, 1)` ആണെങ്കിലോ? + +ഈ case-ൽ, NumPy automatically `b -> (3, 1)` എന്നതിനെ `b -> (3, 3)` ആയി expand ചെയ്യും. + +Element-wise addition-ന്റെ ഫലം അപ്പോൾ ഒരു $3 \times 3$ matrix ആയിരിക്കും: + +```{code-cell} python3 +b.shape = (3, 1) + +a + b +``` + +ഈ broadcasting operation-ന്റെ ഒരു visual representation താഴെ കാണാം: + +```{code-cell} python3 +--- +tags: [hide-input] +--- + +fig = plt.figure(figsize=(5, 1), facecolor='w') +ax = plt.axes([0, 0, 1, 1], xticks=[], yticks=[], frameon=False) + +# first block +draw_cube(ax, (1, 7.5), 1, depth, [1, 2, 3, 4, 5, 6, 9], '1', **solid) +draw_cube(ax, (2, 7.5), 1, depth, [1, 2, 3, 6, 9], '2', **solid) +draw_cube(ax, (3, 7.5), 1, depth, [1, 2, 3, 6, 7, 9, 10], '3', **solid) + +draw_cube(ax, (1, 6.5), 1, depth, [2, 3, 4], '4', **solid) +draw_cube(ax, (2, 6.5), 1, depth, [2, 3], '5', **solid) +draw_cube(ax, (3, 6.5), 1, depth, [2, 3, 7, 10], '6', **solid) + +draw_cube(ax, (1, 5.5), 1, depth, [2, 3, 4], '7', **solid) +draw_cube(ax, (2, 5.5), 1, depth, [2, 3], '8', **solid) +draw_cube(ax, (3, 5.5), 1, depth, [2, 3, 7, 10], '9', **solid) + +# second block +draw_cube(ax, (6, 7.5), 1, depth, [1, 2, 3, 4, 5, 6, 7, 9, 10], '3', **solid) +draw_cube(ax, (7, 7.5), 1, depth, [1, 2, 3, 6, 7, 9, 10], '3', **dotted) +draw_cube(ax, (8, 7.5), 1, depth, [1, 2, 3, 6, 7, 9, 10], '3', **dotted) + +draw_cube(ax, (6, 6.5), 1, depth, [2, 3, 4, 7, 10], '6', **solid) +draw_cube(ax, (7, 6.5), 1, depth, [2, 3, 6, 7, 9, 10, 11], '6', **dotted) +draw_cube(ax, (8, 6.5), 1, depth, [2, 3, 6, 7, 9, 10, 11], '6', **dotted) + +draw_cube(ax, (6, 5.5), 1, depth, [2, 3, 4, 7, 10], '9', **solid) +draw_cube(ax, (7, 5.5), 1, depth, [2, 3, 7, 10, 11], '9', **dotted) +draw_cube(ax, (8, 5.5), 1, depth, [2, 3, 7, 10, 11], '9', **dotted) + +# third block +draw_cube(ax, (12, 7.5), 1, depth, [1, 2, 3, 4, 5, 6, 9], '4', **solid) +draw_cube(ax, (13, 7.5), 1, depth, [1, 2, 3, 6, 9], '5', **solid) +draw_cube(ax, (14, 7.5), 1, depth, [1, 2, 3, 6, 7, 9, 10], '6', **solid) + +draw_cube(ax, (12, 6.5), 1, depth, [2, 3, 4], '10', **solid) +draw_cube(ax, (13, 6.5), 1, depth, [2, 3], '11', **solid) +draw_cube(ax, (14, 6.5), 1, depth, [2, 3, 7, 10], '12', **solid) + +draw_cube(ax, (12, 5.5), 1, depth, [2, 3, 4], '16', **solid) +draw_cube(ax, (13, 5.5), 1, depth, [2, 3], '17', **solid) +draw_cube(ax, (14, 5.5), 1, depth, [2, 3, 7, 10], '18', **solid) + +ax.text(5, 7.0, '+', size=12, ha='center', va='center') +ax.text(10.5, 7.0, '=', size=12, ha='center', va='center'); + + +``` + +ചില cases-ൽ, ഇരു operands-ഉം expand ചെയ്യപ്പെടും. + +`a -> (3,)`, `b -> (3, 1)` എന്നിവയുള്ളപ്പോൾ, `a` എന്നത് `a -> (3, 3)` ആയി expand ചെയ്യപ്പെടും, `b` എന്നത് `b -> (3, 3)` ആയി expand ചെയ്യപ്പെടും. + +ഈ case-ൽ, element-wise addition-ന്റെ ഫലം ഒരു $3 \times 3$ matrix ആയിരിക്കും: + +```{code-cell} python3 +a = np.array([3, 6, 9]) +b = np.array([2, 3, 4]) +b.shape = (3, 1) + +a + b +``` + +ഈ broadcasting operation-ന്റെ ഒരു visual representation താഴെ കാണാം: + +```{code-cell} python3 +--- +tags: [hide-input] +--- + +# Draw a figure and axis with no boundary +fig = plt.figure(figsize=(5, 1), facecolor='w') +ax = plt.axes([0, 0, 1, 1], xticks=[], yticks=[], frameon=False) + +# first block +draw_cube(ax, (1, 7.5), 1, depth, [1, 2, 3, 4, 5, 6, 9], '3', **solid) +draw_cube(ax, (2, 7.5), 1, depth, [1, 2, 3, 6, 9], '6', **solid) +draw_cube(ax, (3, 7.5), 1, depth, [1, 2, 3, 6, 7, 9, 10], '9', **solid) + +draw_cube(ax, (1, 6.5), 1, depth, range(2, 13), '3', **dotted) +draw_cube(ax, (2, 6.5), 1, depth, [2, 3, 6, 7, 9, 10, 11], '6', **dotted) +draw_cube(ax, (3, 6.5), 1, depth, [2, 3, 6, 7, 9, 10, 11], '9', **dotted) + +draw_cube(ax, (1, 5.5), 1, depth, [2, 3, 4, 7, 8, 10, 11, 12], '3', **dotted) +draw_cube(ax, (2, 5.5), 1, depth, [2, 3, 7, 10, 11], '6', **dotted) +draw_cube(ax, (3, 5.5), 1, depth, [2, 3, 7, 10, 11], '9', **dotted) + +# second block +draw_cube(ax, (6, 7.5), 1, depth, [1, 2, 3, 4, 5, 6, 7, 9, 10], '2', **solid) +draw_cube(ax, (7, 7.5), 1, depth, [1, 2, 3, 6, 7, 9, 10], '2', **dotted) +draw_cube(ax, (8, 7.5), 1, depth, [1, 2, 3, 6, 7, 9, 10], '2', **dotted) + +draw_cube(ax, (6, 6.5), 1, depth, [2, 3, 4, 7, 10], '3', **solid) +draw_cube(ax, (7, 6.5), 1, depth, [2, 3, 6, 7, 9, 10, 11], '3', **dotted) +draw_cube(ax, (8, 6.5), 1, depth, [2, 3, 6, 7, 9, 10, 11], '3', **dotted) + +draw_cube(ax, (6, 5.5), 1, depth, [2, 3, 4, 7, 10], '4', **solid) +draw_cube(ax, (7, 5.5), 1, depth, [2, 3, 7, 10, 11], '4', **dotted) +draw_cube(ax, (8, 5.5), 1, depth, [2, 3, 7, 10, 11], '4', **dotted) + +# third block +draw_cube(ax, (12, 7.5), 1, depth, [1, 2, 3, 4, 5, 6, 9], '5', **solid) +draw_cube(ax, (13, 7.5), 1, depth, [1, 2, 3, 6, 9], '8', **solid) +draw_cube(ax, (14, 7.5), 1, depth, [1, 2, 3, 6, 7, 9, 10], '11', **solid) + +draw_cube(ax, (12, 6.5), 1, depth, [2, 3, 4], '6', **solid) +draw_cube(ax, (13, 6.5), 1, depth, [2, 3], '9', **solid) +draw_cube(ax, (14, 6.5), 1, depth, [2, 3, 7, 10], '12', **solid) + +draw_cube(ax, (12, 5.5), 1, depth, [2, 3, 4], '7', **solid) +draw_cube(ax, (13, 5.5), 1, depth, [2, 3], '10', **solid) +draw_cube(ax, (14, 5.5), 1, depth, [2, 3, 7, 10], '13', **solid) + +ax.text(5, 7.0, '+', size=12, ha='center', va='center') +ax.text(10.5, 7.0, '=', size=12, ha='center', va='center'); +``` + +Broadcasting വളരെ useful ആണെങ്കിലും, ചിലപ്പോൾ ഇത് confusing ആയി തോന്നാം. + +For example, `a -> (3, 2)`, `b -> (3,)` എന്നിവ add ചെയ്യാൻ ശ്രമിക്കാം. + +```{code-cell} python3 +--- +tags: [raises-exception] +--- +a = np.array( + [[1, 2], + [4, 5], + [7, 8]]) +b = np.array([3, 6, 9]) + +a + b +``` + +`ValueError`, operands-നെ ഒരുമിച്ച് broadcast ചെയ്യാൻ കഴിഞ്ഞില്ല എന്ന് നമ്മോട് പറയുന്നു. + + +ഈ broadcasting എന്തുകൊണ്ട് execute ചെയ്യാൻ കഴിയില്ല എന്ന് കാണിക്കുന്ന ഒരു visual representation താഴെ കാണാം: + +```{code-cell} python3 +--- +tags: [hide-input] +--- +# Draw a figure and axis with no boundary +fig = plt.figure(figsize=(3, 1.3), facecolor='w') +ax = plt.axes([0, 0, 1, 1], xticks=[], yticks=[], frameon=False) + +# first block +draw_cube(ax, (1, 7.5), 1, depth, [1, 2, 3, 4, 5, 6, 9], '1', **solid) +draw_cube(ax, (2, 7.5), 1, depth, [1, 2, 3, 6, 7, 9, 10], '2', **solid) + +draw_cube(ax, (1, 6.5), 1, depth, [2, 3, 4], '4', **solid) +draw_cube(ax, (2, 6.5), 1, depth, [2, 3, 7, 10], '5', **solid) + +draw_cube(ax, (1, 5.5), 1, depth, [2, 3, 4], '7', **solid) +draw_cube(ax, (2, 5.5), 1, depth, [2, 3, 7, 10], '8', **solid) + +# second block +draw_cube(ax, (6, 7.5), 1, depth, [1, 2, 3, 4, 5, 6, 9], '3', **solid) +draw_cube(ax, (7, 7.5), 1, depth, [1, 2, 3, 6, 9], '6', **solid) +draw_cube(ax, (8, 7.5), 1, depth, [1, 2, 3, 6, 7, 9, 10], '9', **solid) + +draw_cube(ax, (6, 6.5), 1, depth, range(2, 13), '3', **dotted) +draw_cube(ax, (7, 6.5), 1, depth, [2, 3, 6, 7, 9, 10, 11], '6', **dotted) +draw_cube(ax, (8, 6.5), 1, depth, [2, 3, 6, 7, 9, 10, 11], '9', **dotted) + +draw_cube(ax, (6, 5.5), 1, depth, [2, 3, 4, 7, 8, 10, 11, 12], '3', **dotted) +draw_cube(ax, (7, 5.5), 1, depth, [2, 3, 7, 10, 11], '6', **dotted) +draw_cube(ax, (8, 5.5), 1, depth, [2, 3, 7, 10, 11], '9', **dotted) + + +ax.text(4.5, 7.0, '+', size=12, ha='center', va='center') +ax.text(10, 7.0, '=', size=12, ha='center', va='center') +ax.text(11, 7.0, '?', size=16, ha='center', va='center'); +``` + +NumPy-ക്ക് arrays-നെ ഒരേ size-ലേക്ക് expand ചെയ്യാൻ കഴിയില്ല എന്ന് നമുക്ക് കാണാം. + +എന്തുകൊണ്ടെന്നാൽ, `b` എന്നത് `b -> (3,)`-ൽ നിന്നും `b -> (3, 3)`-ലേക്ക് expand ചെയ്യപ്പെടുമ്പോൾ, `b`-നെ `a -> (3, 2)`-മായി match ചെയ്യാൻ NumPy-ക്ക് കഴിയില്ല. + +Higher dimensions-ലേക്ക് നീങ്ങുമ്പോൾ കാര്യങ്ങൾ കൂടുതൽ ബുദ്ധിമുട്ടാകുന്നു. + +നമ്മെ സഹായിക്കാൻ, താഴെ പറയുന്ന rules-ന്റെ list ഉപയോഗിക്കാം: + +* *Step 1:* രണ്ട് arrays-ന്റെ dimensions match ചെയ്യാത്തപ്പോൾ, കുറച്ച് dimensions ഉള്ളതിനെ, existing dimensions-ന്റെ ഇടതുവശത്ത് dimension(s) കൂട്ടിച്ചേർത്ത് NumPy expand ചെയ്യും. + - For example, `a -> (3, 3)`, `b -> (3,)` ആണെങ്കിൽ, broadcasting ഇടതുവശത്ത് ഒരു dimension കൂട്ടിച്ചേർക്കും, അതിനാൽ `b -> (1, 3)` ആകും; + - `a -> (2, 2, 2)`, `b -> (2, 2)` ആണെങ്കിൽ, broadcasting ഇടതുവശത്ത് ഒരു dimension കൂട്ടിച്ചേർക്കും, അതിനാൽ `b -> (1, 2, 2)` ആകും; + - `a -> (3, 2, 2)`, `b -> (2,)` ആണെങ്കിൽ, broadcasting ഇടതുവശത്ത് രണ്ട് dimensions കൂട്ടിച്ചേർക്കും, അതിനാൽ `b -> (1, 1, 2)` ആകും (ഈ process, *Step 1* രണ്ട് പ്രാവശ്യം കടന്നുപോകുന്നത് ആയും കാണാം). + + +* *Step 2:* രണ്ട് arrays-ന് ഒരേ dimension ഉണ്ടെങ്കിലും, shapes വ്യത്യസ്തമാണെങ്കിൽ, shape index 1 ആയ dimensions expand ചെയ്യാൻ NumPy ശ്രമിക്കും. + - For example, `a -> (1, 3)`, `b -> (3, 1)` ആണെങ്കിൽ, broadcasting `a`-യിലും `b`-യിലും shape 1 ഉള്ള dimensions expand ചെയ്യും, അതിനാൽ `a -> (3, 3)`, `b -> (3, 3)` ആകും; + - `a -> (2, 2, 2)`, `b -> (1, 2, 2)` ആണെങ്കിൽ, broadcasting `b`-യുടെ ആദ്യത്തെ dimension expand ചെയ്യും, അതിനാൽ `b -> (2, 2, 2)` ആകും; + - `a -> (3, 2, 2)`, `b -> (1, 1, 2)` ആണെങ്കിൽ, broadcasting `b`-യെ shape 1 ഉള്ള എല്ലാ dimensions-ലും expand ചെയ്യും, അതിനാൽ `b -> (3, 2, 2)` ആകും. + +* *Step 3:* Step 1, 2 എന്നിവയ്ക്ക് ശേഷം, രണ്ട് arrays-ഉം ഇപ്പോഴും match ചെയ്യുന്നില്ലെങ്കിൽ, ഒരു `ValueError` raise ചെയ്യപ്പെടും. For example, `a -> (2, 2, 3)`, `b -> (2, 2)` ആണെന്ന് കരുതുക: + - *Step 1* പ്രകാരം, `b` എന്നത് `b -> (1, 2, 2)` ആയി expand ചെയ്യപ്പെടും; + - *Step 2* പ്രകാരം, `b` എന്നത് `b -> (2, 2, 2)` ആയി expand ചെയ്യപ്പെടും; + - ആദ്യത്തെ രണ്ട് steps-ന് ശേഷവും അവ പരസ്പരം match ചെയ്യുന്നില്ല എന്ന് നമുക്ക് കാണാം. അതിനാൽ, ഒരു `ValueError` raise ചെയ്യപ്പെടും. + + + +## Mutability and Copying Arrays + +NumPy arrays, Python lists-നെ പോലെ mutable data types ആണ്. + +അതായത്, initialization-ന് ശേഷം അവയുടെ contents memory-യിൽ alter ചെയ്യാൻ (mutate ചെയ്യാൻ) കഴിയും. + +ഇത് convenient ആണ്, പക്ഷേ Python-ന്റെ naming, reference model എന്നിവയുമായി combine ചെയ്യുമ്പോൾ, NumPy beginners-ന് mistakes-ലേക്ക് നയിക്കാം. + +ഈ section-ൽ കുറച്ച് key issues നമുക്ക് നോക്കാം. + + +### Mutability + +Mutability-യുടെ examples നമ്മൾ മുകളിൽ already കണ്ടു. + +NumPy array-യുടെ mutation-ന്റെ മറ്റൊരു example താഴെ കാണാം: + +```{code-cell} python3 +a = np.array([42, 44]) +a +``` + +```{code-cell} python3 +a[-1] = 0 # Change last element to 0 +a +``` + +Mutability താഴെ പറയുന്ന behavior-ലേക്ക് നയിക്കുന്നു (ഇത് MATLAB programmers-നെ ഞെട്ടിക്കാം...) + +```{code-cell} python3 +rng = np.random.default_rng() +a = rng.standard_normal(3) +a +``` + +```{code-cell} python3 +b = a +b[0] = 0.0 +a +``` + +സംഭവിച്ചത് എന്തെന്നാൽ, `b`-നെ മാറ്റിയപ്പോൾ നമ്മൾ `a`-യെയും മാറ്റിയിരിക്കുന്നു. + +`b` എന്ന name, `a`-യുമായി bind ചെയ്യപ്പെട്ടിരിക്കുന്നു, ഇത് ആ array-യുടെ മറ്റൊരു reference മാത്രമായി മാറുന്നു (Python assignment model {doc}`later in the course ` കൂടുതൽ വിശദമായി describe ചെയ്യുന്നു). + +അതിനാൽ, ആ array-യിൽ changes നടത്താൻ അതിന് equal rights ഉണ്ട്. + +വാസ്തവത്തിൽ ഇതാണ് ഏറ്റവും sensible ആയ default behavior! + +ഇതിനർത്ഥം, copies ഉണ്ടാക്കുന്നതിന് പകരം, data-യിലേക്കുള്ള pointers മാത്രമാണ് നമ്മൾ pass ചെയ്യുന്നത് എന്നാണ്. + +Copies ഉണ്ടാക്കുന്നത് speed-ന്റെയും memory-യുടെയും കാര്യത്തിൽ expensive ആണ്. + +### Making Copies + +ആവശ്യമുള്ളപ്പോൾ `b`-നെ `a`-യുടെ ഒരു independent copy ആക്കാൻ കഴിയും, തീർച്ചയായും. + +ഇത് `np.copy` ഉപയോഗിച്ച് ചെയ്യാം: + +```{code-cell} python3 +a = rng.standard_normal(3) +a +``` + +```{code-cell} python3 +b = np.copy(a) +b +``` + +ഇപ്പോൾ `b` എന്നത് ഒരു independent copy ആണ് (ഇതിനെ *deep copy* എന്ന് വിളിക്കുന്നു): + +```{code-cell} python3 +b[:] = 1 +b +``` + +```{code-cell} python3 +a +``` + +`b`-യിലെ change, `a`-യെ ബാധിച്ചിട്ടില്ല എന്ന് ശ്രദ്ധിക്കുക. + + + + +## Additional Features + +NumPy-യുടെ മറ്റ് ചില useful features നമുക്ക് നോക്കാം. + + +### Universal Functions + +```{index} single: NumPy; Vectorized Functions +``` + +Arrays-ൽ *element-wise* ആയി act ചെയ്യുന്ന standard functions ആയ `log`, `exp`, `sin`, തുടങ്ങിയവയുടെ versions NumPy നൽകുന്നു: + +```{code-cell} python3 +z = np.array([1, 2, 3]) +np.sin(z) +``` + +താഴെ കാണിച്ചിരിക്കുന്നത് പോലുള്ള explicit element-by-element loops-ന്റെ ആവശ്യം ഇത് ഒഴിവാക്കുന്നു: + +```{code-cell} python3 +n = len(z) +y = np.empty(n) +for i in range(n): + y[i] = np.sin(z[i]) +``` + +Arrays-ൽ element-wise ആയി act ചെയ്യുന്നതിനാൽ, ഈ functions-നെ ചിലപ്പോൾ **vectorized functions** എന്ന് വിളിക്കുന്നു. + +NumPy-speak-ൽ, ഇവയെ **ufuncs**, അല്ലെങ്കിൽ **universal functions** എന്നും വിളിക്കുന്നു. + +മുകളിൽ നമ്മൾ കണ്ടത് പോലെ, സാധാരണ arithmetic operations (`+`, `*`, തുടങ്ങിയവ) element-wise ആയും work ചെയ്യുന്നു, ഇവയെ ufuncs-ഉമായി combine ചെയ്യുമ്പോൾ, വളരെ വലിയൊരു set of fast element-wise functions ലഭിക്കുന്നു. + +```{code-cell} python3 +z +``` + +```{code-cell} python3 +(1 / np.sqrt(2 * np.pi)) * np.exp(- 0.5 * z**2) +``` + +എല്ലാ user-defined functions-ഉം element-wise ആയി act ചെയ്യണമെന്നില്ല. + +For example, താഴെ define ചെയ്തിരിക്കുന്ന `f` എന്ന function-ന് ഒരു NumPy array pass ചെയ്യുന്നത് ഒരു `ValueError`-ന് കാരണമാകുന്നു: + +```{code-cell} python3 +def f(x): + return 1 if x > 0 else 0 +``` + +NumPy function `np.where`, ഒരു vectorized alternative നൽകുന്നു: + +```{code-cell} python3 +x = rng.standard_normal(4) +x +``` + +```{code-cell} python3 +np.where(x > 0, 1, 0) # Insert 1 if x > 0 true, otherwise 0 +``` + +തന്നിരിക്കുന്ന ഒരു function vectorize ചെയ്യാൻ `np.vectorize`-ഉം ഉപയോഗിക്കാം: + +```{code-cell} python3 +f = np.vectorize(f) +f(x) # Passing the same vector x as in the previous example +``` + +എന്നിരുന്നാലും, ഈ approach, കൂടുതൽ carefully crafted ആയ ഒരു vectorized function-ന്റെ speed എപ്പോഴും obtain ചെയ്യില്ല. + +(പിന്നീട് നമ്മൾ കാണും, JAX-ന് `np.vectorize`-ന്റെ ഒരു powerful version ഉണ്ട്, ഇത് പലപ്പോഴും highly efficient code generate ചെയ്യും.) + + +### Comparisons + +```{index} single: NumPy; Comparisons +``` + +സാധാരണയായി, arrays-ലെ comparisons element-wise ആയാണ് ചെയ്യുന്നത്: + +```{code-cell} python3 +z = np.array([2, 3]) +y = np.array([2, 3]) +z == y +``` + +```{code-cell} python3 +y[0] = 5 +z == y +``` + +```{code-cell} python3 +z != y +``` + +`>`, `<`, `>=`, `<=` എന്നിവയ്ക്കും situation similar ആണ്. + +Scalars-നെതിരെയും നമുക്ക് comparisons ചെയ്യാം: + +```{code-cell} python3 +z = np.linspace(0, 10, 5) +z +``` + +```{code-cell} python3 +z > 3 +``` + +*Conditional extraction*-ന് ഇത് പ്രത്യേകിച്ചും useful ആണ്: + +```{code-cell} python3 +b = z > 3 +b +``` + +```{code-cell} python3 +z[b] +``` + +തീർച്ചയായും നമുക്ക് ഇത് ഒറ്റ step-ൽ ചെയ്യാം---അതാണ് പലപ്പോഴും ചെയ്യുന്നത്: + +```{code-cell} python3 +z[z > 3] +``` + +### Sub-packages + +Scientific programming-മായി ബന്ധപ്പെട്ട ചില additional functionality, NumPy അതിന്റെ sub-packages വഴി നൽകുന്നു. + +NumPy-യുടെ [random `Generator`](https://numpy.org/doc/stable/reference/random/generator.html#random-generator) ഉപയോഗിച്ച് random variables generate ചെയ്യുന്നത് നമ്മൾ already കണ്ടു. + +```{code-cell} python3 +z = rng.standard_normal(10000) # Generate standard normals +y = rng.binomial(10, 0.5, size=1000) # 1,000 draws from Bin(10, 0.5) +y.mean() +``` + +സാധാരണയായി ഉപയോഗിക്കുന്ന മറ്റൊരു subpackage ആണ് np.linalg: + +```{code-cell} python3 +A = np.array([[1, 2], [3, 4]]) + +np.linalg.det(A) # Compute the determinant +``` + +```{code-cell} python3 +np.linalg.inv(A) # Compute the inverse +``` + +```{index} single: SciPy +``` + +```{index} single: Python; SciPy +``` + +ഈ functionality-യുടെ ഭൂരിഭാഗവും [SciPy](https://scipy.org/)-യിലും ലഭ്യമാണ്, NumPy-യുടെ മുകളിൽ build ചെയ്തിരിക്കുന്ന modules-ന്റെ ഒരു collection ആണിത്. + +SciPy versions നമ്മൾ {doc}`soon ` കൂടുതൽ വിശദമായി cover ചെയ്യും. + +NumPy-യിൽ ലഭ്യമായതിന്റെ ഒരു comprehensive list-ന് [this documentation](https://numpy.org/doc/stable/reference/routines.html) കാണുക. + + +### Implicit Multithreading + +[Previously](need_for_speed) multithreading വഴിയുള്ള parallelization-ന്റെ concept നമ്മൾ discuss ചെയ്തു. + +NumPy അതിന്റെ compiled code-ന്റെ ഭൂരിഭാഗത്തിലും multithreading implement ചെയ്യാൻ ശ്രമിക്കുന്നു. + +ഇത് action-ൽ കാണാൻ ഒരു example നമുക്ക് നോക്കാം. + +അടുത്ത code piece, randomly generate ചെയ്ത ധാരാളം matrices-ന്റെ eigenvalues compute ചെയ്യുന്നു. + +ഇത് run ചെയ്യാൻ കുറച്ച് seconds എടുക്കും. + +```{code-cell} python3 +n = 20 +m = 1000 +for i in range(n): + X = rng.standard_normal((m, m)) + λ = np.linalg.eigvals(X) +``` + +ഇനി, ഈ code run ചെയ്യുമ്പോൾ നമ്മുടെ machine-ലെ htop system monitor-ന്റെ output നമുക്ക് നോക്കാം: + +```{figure} /_static/lecture_specific/parallelization/htop_parallel_npmat.png +:scale: 80 +``` + +8 CPUs-ൽ 4 എണ്ണം full speed-ൽ run ചെയ്യുന്നത് നമുക്ക് കാണാം. + +NumPy-യുടെ `eigvals` routine tasks-നെ neat ആയി split up ചെയ്ത് വ്യത്യസ്ത threads-ലേക്ക് distribute ചെയ്യുന്നത് കൊണ്ടാണ് ഇത്. + + + + + +## Exercises + + +```{exercise-start} +:label: np_ex1 +``` + +Consider the polynomial expression + +```{math} +:label: np_polynom + +p(x) = a_0 + a_1 x + a_2 x^2 + \cdots a_N x^N = \sum_{n=0}^N a_n x^n +``` + +{ref}`Earlier `, you wrote a simple function `p(x, coeff)` to evaluate {eq}`np_polynom` without considering efficiency. + +Now write a new function that does the same job, but uses NumPy arrays and array operations for its computations, rather than any form of Python loop. + +(Such functionality is already implemented as `np.poly1d`, but for the sake of the exercise don't use this class) + +```{hint} +:class: dropdown +Use `np.cumprod()` +``` +```{exercise-end} +``` + +```{solution-start} np_ex1 +:class: dropdown +``` + +This code does the job + +```{code-cell} python3 +def p(x, coef): + X = np.ones_like(coef) + X[1:] = x + y = np.cumprod(X) # y = [1, x, x**2,...] + return coef @ y +``` + +Let's test it + +```{code-cell} python3 +x = 2 +coef = np.linspace(2, 4, 3) +print(coef) +print(p(x, coef)) +# For comparison +q = np.poly1d(np.flip(coef)) +print(q(x)) +``` + +```{solution-end} +``` + + +```{exercise-start} +:label: np_ex2 +``` + +Let `q` be a NumPy array of length `n` with `q.sum() == 1`. + +Suppose that `q` represents a [probability mass function](https://en.wikipedia.org/wiki/Probability_mass_function). + +We wish to generate a discrete random variable $x$ such that $\mathbb P\{x = i\} = q_i$. + +In other words, `x` takes values in `range(len(q))` and `x = i` with probability `q[i]`. + +The standard (inverse transform) algorithm is as follows: + +* Divide the unit interval $[0, 1]$ into $n$ subintervals $I_0, I_1, \ldots, I_{n-1}$ such that the length of $I_i$ is $q_i$. +* Draw a uniform random variable $U$ on $[0, 1]$ and return the $i$ such that $U \in I_i$. + +The probability of drawing $i$ is the length of $I_i$, which is equal to $q_i$. + +We can implement the algorithm as follows + +```{code-cell} python3 +from random import uniform + +def sample(q): + a = 0.0 + U = uniform(0, 1) + for i in range(len(q)): + if a < U <= a + q[i]: + return i + a = a + q[i] +``` + +If you can't see how this works, try thinking through the flow for a simple example, such as `q = [0.25, 0.75]` +It helps to sketch the intervals on paper. + +Your exercise is to speed it up using NumPy, avoiding explicit loops + +```{hint} +:class: dropdown + +Use `np.searchsorted` and `np.cumsum` + +``` + +If you can, implement the functionality as a class called `DiscreteRV`, where + +* the data for an instance of the class is the vector of probabilities `q` +* the class has a `draw()` method, which returns one draw according to the algorithm described above + +If you can, write the method so that `draw(k)` returns `k` draws from `q`. + +```{exercise-end} +``` + +```{solution-start} np_ex2 +:class: dropdown +``` + +Here's our first pass at a solution: + +```{code-cell} python3 +from numpy import cumsum + +class DiscreteRV: + """ + Generates an array of draws from a discrete random variable with vector of + probabilities given by q. + """ + + def __init__(self, q, seed=None): + """ + The argument q is a NumPy array, or array like, nonnegative and sums + to 1. + + The argument seed sets the seed for the underlying random number + generator; with the default seed=None, draws are not reproducible + across runs. + """ + self.q = q + self.Q = cumsum(q) + self.rng = np.random.default_rng(seed) + + def draw(self, k=1): + """ + Returns k draws from q. For each such draw, the value i is returned + with probability q[i]. + """ + return self.Q.searchsorted(self.rng.uniform(0, 1, size=k)) +``` + +The logic is not obvious, but if you take your time and read it slowly, +you will understand. + +There is a problem here, however. + +Suppose that `q` is altered after an instance of `DiscreteRV` is +created, for example by + +```{code-cell} python3 +q = (0.1, 0.9) +d = DiscreteRV(q) +d.q = (0.5, 0.5) +``` + +The problem is that `Q` does not change accordingly, and `Q` is the +data used in the `draw` method. + +To deal with this, one option is to compute `Q` every time the draw +method is called. + +But this is inefficient relative to computing `Q` once-off. + +A better option is to use descriptors. + +A solution from the [quantecon +library](https://github.com/QuantEcon/QuantEcon.py/tree/main/quantecon) +using descriptors that behaves as we desire can be found +[here](https://github.com/QuantEcon/QuantEcon.py/blob/main/quantecon/discrete_rv.py). + +```{solution-end} +``` + + +```{exercise} +:label: np_ex3 + +Recall our {ref}`earlier discussion ` of the empirical cumulative distribution function. + +Your task is to + +1. Make the `__call__` method more efficient using NumPy. +1. Add a method that plots the ECDF over $[a, b]$, where $a$ and $b$ are method parameters. +``` + +```{solution-start} np_ex3 +:class: dropdown +``` + +An example solution is given below. + +In essence, we've just taken [this code](https://github.com/QuantEcon/QuantEcon.py/blob/main/quantecon/ecdf.py) +from QuantEcon and added in a plot method + +```{code-cell} python3 +""" +Modifies ecdf.py from QuantEcon to add in a plot method + +""" + +class ECDF: + """ + One-dimensional empirical distribution function given a vector of + observations. + + Parameters + ---------- + observations : array_like + An array of observations + + Attributes + ---------- + observations : array_like + An array of observations + + """ + + def __init__(self, observations): + self.observations = np.asarray(observations) + + def __call__(self, x): + """ + Evaluates the ecdf at x + + Parameters + ---------- + x : scalar(float) + The x at which the ecdf is evaluated + + Returns + ------- + scalar(float) + Fraction of the sample less than x + + """ + return np.mean(self.observations <= x) + + def plot(self, ax, a=None, b=None): + """ + Plot the ecdf on the interval [a, b]. + + Parameters + ---------- + a : scalar(float), optional(default=None) + Lower endpoint of the plot interval + b : scalar(float), optional(default=None) + Upper endpoint of the plot interval + + """ + + # === choose reasonable interval if [a, b] not specified === # + if a is None: + a = self.observations.min() - self.observations.std() + if b is None: + b = self.observations.max() + self.observations.std() + + # === generate plot === # + x_vals = np.linspace(a, b, num=100) + f = np.vectorize(self.__call__) + ax.plot(x_vals, f(x_vals)) + plt.show() +``` + +Here's an example of usage + +```{code-cell} python3 +fig, ax = plt.subplots() +rng = np.random.default_rng() +X = rng.standard_normal(1000) +F = ECDF(X) +F.plot(ax) +``` + +```{solution-end} +``` + + +```{exercise-start} +:label: np_ex4 +``` + +Recall that [broadcasting](broadcasting) in NumPy can help us conduct element-wise operations on arrays with different number of dimensions without using `for` loops. + +In this exercise, try to use `for` loops to replicate the result of the following broadcasting operations. + +**Part 1**: Try to replicate this simple example using `for` loops and compare your results with the broadcasting operation below. + +```{code-cell} python3 + +rng = np.random.default_rng(123) +x = rng.standard_normal((4, 4)) +y = rng.standard_normal(4) +A = x / y +``` + +Here is the output + +```{code-cell} python3 +--- +tags: [hide-output] +--- +print(A) +``` + +**Part 2**: Move on to replicate the result of the following broadcasting operation. Meanwhile, compare the speeds of broadcasting and the `for` loop you implement. + +For this part of the exercise you can use the `qe.Timer()` context manager from the `quantecon` library to time the execution. + +Let's make sure this library is installed. + +```{code-cell} python3 +:tags: [hide-output] +!pip install quantecon +``` + +Now we can import the quantecon package. + +```{code-cell} python3 + +rng = np.random.default_rng(123) +x = rng.standard_normal((1000, 100, 100)) +y = rng.standard_normal(100) + +with qe.Timer("Broadcasting operation"): + B = x / y +``` + +Here is the output + +```{code-cell} python3 +--- +tags: [hide-output] +--- +print(B) +``` + +```{exercise-end} +``` + + +```{solution-start} np_ex4 +:class: dropdown +``` + +**Part 1 Solution** + +```{code-cell} python3 +rng = np.random.default_rng(123) +x = rng.standard_normal((4, 4)) +y = rng.standard_normal(4) + +C = np.empty_like(x) +n = len(x) +for i in range(n): + for j in range(n): + C[i, j] = x[i, j] / y[j] +``` + +Compare the results to check your answer + +```{code-cell} python3 +--- +tags: [hide-output] +--- +print(C) +``` + +You can also use `array_equal()` to check your answer + +```{code-cell} python3 +print(np.array_equal(A, C)) +``` + + +**Part 2 Solution** + +```{code-cell} python3 + +rng = np.random.default_rng(123) +x = rng.standard_normal((1000, 100, 100)) +y = rng.standard_normal(100) + +with qe.Timer("For loop operation"): + D = np.empty_like(x) + d1, d2, d3 = x.shape + for i in range(d1): + for j in range(d2): + for k in range(d3): + D[i, j, k] = x[i, j, k] / y[k] +``` + +Note that the `for` loop takes much longer than the broadcasting operation. + +Compare the results to check your answer + +```{code-cell} python3 +--- +tags: [hide-output] +--- +print(D) +``` + +```{code-cell} python3 +print(np.array_equal(B, D)) +``` + +```{solution-end} +``` diff --git a/experiments/ml-benchmark/arms/2026-09-21-round4-numpy-v0.29.2/numpy-v0.29.2-draw1.md b/experiments/ml-benchmark/arms/2026-09-21-round4-numpy-v0.29.2/numpy-v0.29.2-draw1.md new file mode 100644 index 0000000..38b5a72 --- /dev/null +++ b/experiments/ml-benchmark/arms/2026-09-21-round4-numpy-v0.29.2/numpy-v0.29.2-draw1.md @@ -0,0 +1,1546 @@ +--- +jupytext: + text_representation: + extension: .md + format_name: myst +kernelspec: + display_name: Python 3 + language: python + name: python3 +translation: + title: NumPy + headings: + Overview: Overview + NumPy Arrays: NumPy Arrays + NumPy Arrays::Basics: Basics + NumPy Arrays::Shape and Dimension: Shape and Dimension + NumPy Arrays::Creating Arrays: Creating Arrays + NumPy Arrays::Array Indexing: Array Indexing + NumPy Arrays::Array Methods: Array Methods + Arithmetic Operations: Arithmetic Operations + Matrix Multiplication: Matrix Multiplication + Broadcasting: Broadcasting + Mutability and Copying Arrays: Mutability and Copying Arrays + Mutability and Copying Arrays::Mutability: Mutability + Mutability and Copying Arrays::Making Copies: Making Copies + Additional Features: Additional Features + Additional Features::Universal Functions: Universal Functions + Additional Features::Comparisons: Comparisons + Additional Features::Sub-packages: Sub-packages + Additional Features::Implicit Multithreading: Implicit Multithreading + Exercises: Exercises +--- + +(np)= +```{raw} jupyter + +``` + +# {index}`NumPy ` + +```{index} single: Python; NumPy +``` + +```{epigraph} +"Let's be clear: the work of science has nothing whatever to do with consensus. Consensus is the business of politics. Science, on the contrary, requires only one investigator who happens to be right, which means that he or she has results that are verifiable by reference to the real world. In science consensus is irrelevant. What is relevant is reproducible results." -- Michael Crichton +``` + +Anaconda-യിൽ ഉള്ളതിന് പുറമേ, ഈ lecture-ന് താഴെ പറയുന്ന libraries ആവശ്യമായിവരുന്നു: + +```{code-cell} ipython3 +:tags: [hide-output] + +!pip install quantecon +``` + +## Overview + +[NumPy](https://en.wikipedia.org/wiki/NumPy) എന്നത്, numerical programming-നുള്ള ഒരു മികച്ച library ആണ്. + +* Academia, finance, industry എന്നിവയിൽ വ്യാപകമായി ഉപയോഗിക്കപ്പെടുന്നു. +* Mature-ഉം, fast-ഉം, stable-ഉം ആണ്, തുടർച്ചയായി development-ലും ആണ്. + +മുൻ lectures-ൽ NumPy ഉൾപ്പെടുന്ന കുറച്ച് code നമ്മൾ already കണ്ടിട്ടുണ്ട്. + +ഈ lecture-ൽ നമ്മൾ ചെയ്യാൻ പോകുന്ന കാര്യങ്ങൾ: + +1. NumPy arrays, ഒപ്പം +1. NumPy നൽകുന്ന അടിസ്ഥാന array processing operations. + +(ഒരു alternative reference-ന്, [the official NumPy documentation](https://numpy.org/doc/stable/reference/) നോക്കുക.) + +താഴെ പറയുന്ന imports നമുക്ക് ഉപയോഗിക്കാം. + +```{code-cell} python3 +import numpy as np +import random +import quantecon as qe +import matplotlib.pyplot as plt +from mpl_toolkits.mplot3d.axes3d import Axes3D +from matplotlib import cm +``` + + + +(numpy_array)= +## NumPy Arrays + +```{index} single: NumPy; Arrays +``` + +NumPy പരിഹരിക്കുന്ന അടിസ്ഥാന പ്രശ്നം, വേഗതയേറിയ array processing ആണ്. + +NumPy define ചെയ്യുന്ന ഏറ്റവും പ്രധാനപ്പെട്ട structure, ഒരു array data type ആണ്, ഔപചാരികമായി ഇതിനെ [numpy.ndarray](https://numpy.org/doc/stable/reference/arrays.ndarray.html) എന്ന് വിളിക്കുന്നു. + +Scientific Python ecosystem-ന്റെ വളരെ വലിയൊരു ഭാഗം NumPy arrays ആണ് പ്രവർത്തിപ്പിക്കുന്നത്. + +### Basics + +Zeros മാത്രം അടങ്ങിയ ഒരു NumPy array create ചെയ്യാൻ നമ്മൾ [np.zeros](https://numpy.org/doc/stable/reference/generated/numpy.zeros.html#numpy.zeros) ഉപയോഗിക്കുന്നു. + +```{code-cell} python3 +a = np.zeros(3) +a +``` + +```{code-cell} python3 +type(a) +``` + +NumPy arrays, native Python lists-നെ കുറച്ചൊക്കെ പോലെയാണ്, പക്ഷേ വ്യത്യാസം എന്തെന്നാൽ: + +* Data *homogeneous ആയിരിക്കണം* (എല്ലാ elements-ഉം ഒരേ type-ലുള്ളതായിരിക്കണം). +* ഈ types, NumPy നൽകുന്ന [data types](https://numpy.org/doc/stable/reference/arrays.dtypes.html) (`dtypes`) ഇൽ ഒന്നായിരിക്കണം. + +ഈ dtypes-ൽ ഏറ്റവും പ്രധാനപ്പെട്ടവ: + +* float64: 64 bit floating-point number +* int64: 64 bit integer +* bool: 8 bit True or False + +Complex numbers, unsigned integers, തുടങ്ങിയവ represent ചെയ്യാൻ ഉള്ള dtypes-ഉം ഉണ്ട്. + +ആധുനിക machines-ൽ, arrays-ന്റെ default dtype `float64` ആണ്. + +```{code-cell} python3 +a = np.zeros(3) +type(a[0]) +``` + +Integers ഉപയോഗിക്കണമെങ്കിൽ താഴെ കാണിച്ചിരിക്കുന്ന പോലെ specify ചെയ്യാം: + +```{code-cell} python3 +a = np.zeros(3, dtype=int) +type(a[0]) +``` + +(numpy_shape_dim)= +### Shape and Dimension + +```{index} single: NumPy; Arrays (Shape and Dimension) +``` + +താഴെ കൊടുത്തിരിക്കുന്ന assignment നോക്കാം: + +```{code-cell} python3 +z = np.zeros(10) +``` + +ഇവിടെ `z` എന്നത് ഒരു **flat** array ആണ് --- row vector-ഉം അല്ല column vector-ഉം അല്ല. + +```{code-cell} python3 +z.shape +``` + +ഇവിടെ shape tuple-ന് ഒരു element മാത്രമേയുള്ളൂ, അതായത് array-യുടെ length (ഒരു element മാത്രമുള്ള tuples ഒരു comma-യിൽ അവസാനിക്കും). + +ഇതിന് ഒരു additional dimension നൽകാൻ, `shape` attribute നമുക്ക് മാറ്റാം: + +```{code-cell} python3 +z.shape = (10, 1) # Convert flat array to column vector (two-dimensional) +z +``` + +```{code-cell} python3 +z = np.zeros(4) # Flat array +z.shape = (2, 2) # Two-dimensional array +z +``` + +അവസാനത്തെ case-ൽ, 2x2 array ഉണ്ടാക്കാൻ, `zeros()` function-ന് ഒരു tuple pass ചെയ്യാം, `z = np.zeros((2, 2))` എന്ന പോലെ. + + + +(creating_arrays)= +### Creating Arrays + +```{index} single: NumPy; Arrays (Creating) +``` + +നമ്മൾ കണ്ടത് പോലെ, `np.zeros` function zeros-ന്റെ ഒരു array create ചെയ്യുന്നു. + +`np.ones` എന്താണ് create ചെയ്യുന്നതെന്ന് നിങ്ങൾക്ക് ഊഹിക്കാൻ കഴിയും. + +ഇതുമായി ബന്ധപ്പെട്ടതാണ് `np.empty`, ഇത് memory-യിൽ arrays create ചെയ്യുന്നു, പിന്നീട് data-കൊണ്ട് നിറയ്ക്കാവുന്നത്: + +```{code-cell} python3 +z = np.empty(3) +z +``` + +ഇവിടെ കാണുന്ന numbers garbage values ആണ്. + +(Python 3 contiguous 64 bit memory pieces allocate ചെയ്യുന്നു, ആ memory slots-ലെ നിലവിലുള്ള contents `float64` values ആയി interpret ചെയ്യപ്പെടുന്നു) + +Evenly spaced numbers-ന്റെ ഒരു grid set up ചെയ്യാൻ `np.linspace` ഉപയോഗിക്കുക: + +```{code-cell} python3 +z = np.linspace(2, 4, 5) # From 2 to 4, with 5 elements +``` + +ഒരു identity matrix create ചെയ്യാൻ `np.identity` അല്ലെങ്കിൽ `np.eye` ഉപയോഗിക്കുക: + +```{code-cell} python3 +z = np.identity(2) +z +``` + +കൂടാതെ, `np.array` ഉപയോഗിച്ച് Python lists, tuples, തുടങ്ങിയവയിൽ നിന്നും NumPy arrays create ചെയ്യാം: + +```{code-cell} python3 +z = np.array([10, 20]) # ndarray from Python list +z +``` + +```{code-cell} python3 +type(z) +``` + +```{code-cell} python3 +z = np.array((10, 20), dtype=float) # Here 'float' is equivalent to 'np.float64' +z +``` + +```{code-cell} python3 +z = np.array([[1, 2], [3, 4]]) # 2D array from a list of lists +z +``` + +`np.asarray` എന്നതും കാണുക, ഇത് similar ആയ ഒരു function ആണ്, പക്ഷേ NumPy array-യിൽ already ഉള്ള data-യുടെ distinct copy ഉണ്ടാക്കുന്നില്ല. + +Numeric data അടങ്ങിയ ഒരു text file-ൽ നിന്നും array data read ചെയ്യാൻ `np.loadtxt` ഉപയോഗിക്കുക --- വിശദാംശങ്ങൾക്ക് [the documentation](https://numpy.org/doc/stable/reference/routines.io.html) കാണുക. + + + +### Array Indexing + +```{index} single: NumPy; Arrays (Indexing) +``` + +ഒരു flat array-ന്, indexing Python sequences-ന്റേത് പോലെ തന്നെയാണ്: + +```{code-cell} python3 +z = np.linspace(1, 2, 5) +z +``` + +```{code-cell} python3 +z[0] +``` + +```{code-cell} python3 +z[0:2] # Two elements, starting at element 0 +``` + +```{code-cell} python3 +z[-1] +``` + +2D arrays-ന് index syntax താഴെ കാണിച്ചിരിക്കുന്ന പോലെയാണ്: + +```{code-cell} python3 +z = np.array([[1, 2], [3, 4]]) +z +``` + +```{code-cell} python3 +z[0, 0] +``` + +```{code-cell} python3 +z[0, 1] +``` + +ഇങ്ങനെ തുടരും. + +Columns-ഉം rows-ഉം താഴെ കാണിച്ചിരിക്കുന്ന പോലെ extract ചെയ്യാം: + +```{code-cell} python3 +z[0, :] +``` + +```{code-cell} python3 +z[:, 1] +``` + +Integers-ന്റെ NumPy arrays-ഉം elements extract ചെയ്യാൻ ഉപയോഗിക്കാം: + +```{code-cell} python3 +z = np.linspace(2, 4, 5) +z +``` + +```{code-cell} python3 +indices = np.array((0, 2, 3)) +z[indices] +``` + +അവസാനമായി, `dtype bool` ഉള്ള ഒരു array-യും elements extract ചെയ്യാൻ ഉപയോഗിക്കാം: + +```{code-cell} python3 +z +``` + +```{code-cell} python3 +d = np.array([0, 1, 1, 0, 0], dtype=bool) +d +``` + +```{code-cell} python3 +z[d] +``` + +ഇത് എന്തുകൊണ്ട് useful ആണെന്ന് താഴെ നമുക്ക് കാണാം. + +ഒരു ചെറിയ കാര്യം കൂടി: slice notation ഉപയോഗിച്ച് ഒരു array-യിലെ എല്ലാ elements-ഉം ഒരു number-ന് തുല്യമായി set ചെയ്യാം: + +```{code-cell} python3 +z = np.empty(3) +z +``` + +```{code-cell} python3 +z[:] = 42 +z +``` + +### Array Methods + +```{index} single: NumPy; Arrays (Methods) +``` + +Arrays-ന് useful ആയ methods ഉണ്ട്, ഇവയെല്ലാം carefully optimize ചെയ്തിരിക്കുന്നു: + +```{code-cell} python3 +a = np.array((4, 3, 2, 1)) +a +``` + +```{code-cell} python3 +a.sort() # Sorts a in place +a +``` + +```{code-cell} python3 +a.sum() # Sum +``` + +```{code-cell} python3 +a.mean() # Mean +``` + +```{code-cell} python3 +a.max() # Max +``` + +```{code-cell} python3 +a.argmax() # Returns the index of the maximal element +``` + +```{code-cell} python3 +a.cumsum() # Cumulative sum of the elements of a +``` + +```{code-cell} python3 +a.cumprod() # Cumulative product of the elements of a +``` + +```{code-cell} python3 +a.var() # Variance +``` + +```{code-cell} python3 +a.std() # Standard deviation +``` + +```{code-cell} python3 +a.shape = (2, 2) +a.T # Equivalent to a.transpose() +``` + +അറിഞ്ഞിരിക്കേണ്ട മറ്റൊരു method ആണ് `searchsorted()`. + +`z` എന്നത് ഒരു nondecreasing array ആണെങ്കിൽ, `z.searchsorted(a)` എന്നത് `z`-യിലെ `>= a` ആയ ആദ്യത്തെ element-ന്റെ index return ചെയ്യുന്നു: + +```{code-cell} python3 +z = np.linspace(2, 4, 5) +z +``` + +```{code-cell} python3 +z.searchsorted(2.2) +``` + + +## Arithmetic Operations + +```{index} single: NumPy; Arithmetic Operations +``` + +`+`, `-`, `*`, `/`, `**` എന്നീ operators എല്ലാം arrays-ൽ *elementwise* ആയി act ചെയ്യുന്നു: + +```{code-cell} python3 +a = np.array([1, 2, 3, 4]) +b = np.array([5, 6, 7, 8]) +a + b +``` + +```{code-cell} python3 +a * b +``` + +താഴെ കാണിച്ചിരിക്കുന്ന പോലെ ഓരോ element-ഇനും ഒരു scalar നമുക്ക് add ചെയ്യാം: + +```{code-cell} python3 +a + 10 +``` + +Scalar multiplication similar ആണ്: + +```{code-cell} python3 +a * 10 +``` + +Two-dimensional arrays-ഉം അതേ general rules തന്നെ follow ചെയ്യുന്നു: + +```{code-cell} python3 +A = np.ones((2, 2)) +B = np.ones((2, 2)) +A + B +``` + +```{code-cell} python3 +A + 10 +``` + +```{code-cell} python3 +A * B +``` + +(numpy_matrix_multiplication)= +In particular, `A * B` എന്നത് matrix product *അല്ല*, ഇത് ഒരു element-wise product ആണ്. + + +## Matrix Multiplication + +```{index} single: NumPy; Matrix Multiplication +``` + +```{index} single: NumPy; Matrix Multiplication +``` + +Matrix multiplication-ന് നമ്മൾ `@` symbol ഉപയോഗിക്കുന്നു, താഴെ കാണിച്ചിരിക്കുന്ന പോലെ: + +```{code-cell} python3 +A = np.ones((2, 2)) +B = np.ones((2, 2)) +A @ B +``` + +Syntax flat arrays-ലും work ചെയ്യുന്നു --- നിങ്ങൾക്ക് എന്താണ് വേണ്ടതെന്ന് NumPy ഒരു educated guess നടത്തുന്നു: + +```{code-cell} python3 +A @ (0, 1) +``` + +നമ്മൾ post-multiplying ചെയ്യുന്നതിനാൽ, tuple ഒരു column vector ആയി treat ചെയ്യപ്പെടുന്നു. + + + +(broadcasting)= +## Broadcasting + +```{index} single: NumPy; Broadcasting +``` + +(ഈ section, [Jake VanderPlas](https://jakevdp.github.io/PythonDataScienceHandbook/02.05-computation-on-arrays-broadcasting.html) നൽകിയ broadcasting-നെക്കുറിച്ചുള്ള ഒരു മികച്ച discussion extend ചെയ്യുന്നു.) + +```{note} +Broadcasting എന്നത് NumPy-യുടെ വളരെ പ്രധാനപ്പെട്ട ഒരു aspect ആണ്. അതേസമയം, advanced broadcasting താരതമ്യേന complex ആണ്, താഴെ പറയുന്ന ചില details ആദ്യമായി വായിക്കുമ്പോൾ skim ചെയ്ത് പോകാം. +``` + +Element-wise operations-ൽ, arrays-ന് ഒരേ shape ഉണ്ടാകണമെന്നില്ല. + +ഇത് സംഭവിക്കുമ്പോൾ, കഴിയുന്നിടത്തെല്ലാം NumPy automatically arrays-നെ ഒരേ shape-ലേക്ക് expand ചെയ്യും. + +NumPy-യിലെ ഈ useful ആയ (എന്നാൽ ചിലപ്പോൾ confusing ആയ) feature-നെ **broadcasting** എന്ന് വിളിക്കുന്നു. + +Broadcasting-ന്റെ value എന്തെന്നാൽ: + +* `for` loops ഒഴിവാക്കാം, ഇത് numerical code വേഗത്തിൽ run ചെയ്യാൻ സഹായിക്കുന്നു, ഒപ്പം +* arrays-ന്റെ ഈ dimensions memory-യിൽ actually create ചെയ്യാതെ തന്നെ arrays-ൽ operations implement ചെയ്യാൻ broadcasting നമ്മെ അനുവദിക്കുന്നു, arrays വലുതാകുമ്പോൾ ഇത് പ്രധാനമാകാം. + +For example, `a` എന്നത് ഒരു $3 \times 3$ array ആണെന്ന് കരുതുക (`a -> (3, 3)`), അതേസമയം `b` എന്നത് മൂന്ന് elements ഉള്ള ഒരു flat array ആണ് (`b -> (3,)`). + +ഇവയെ ഒരുമിച്ച് add ചെയ്യുമ്പോൾ, NumPy automatically `b -> (3,)` എന്നതിനെ `b -> (3, 3)` ആയി expand ചെയ്യും. + +Element-wise addition-ന്റെ ഫലം ഒരു $3 \times 3$ array ആയിരിക്കും: + +```{code-cell} python3 + +a = np.array( + [[1, 2, 3], + [4, 5, 6], + [7, 8, 9]]) +b = np.array([3, 6, 9]) + +a + b +``` + +ഈ broadcasting operation-ന്റെ ഒരു visual representation താഴെ കാണാം: + +```{code-cell} python3 +--- +tags: [hide-input] +--- +# Adapted and modified based on the code in the book written by Jake VanderPlas (see https://jakevdp.github.io/PythonDataScienceHandbook/06.00-figure-code.html#Broadcasting) +# Originally from astroML: see https://www.astroml.org/book_figures/appendix/fig_broadcast_visual.html + + +def draw_cube(ax, xy, size, depth=0.4, + edges=None, label=None, label_kwargs=None, **kwargs): + """draw and label a cube. edges is a list of numbers between + 1 and 12, specifying which of the 12 cube edges to draw""" + if edges is None: + edges = range(1, 13) + + x, y = xy + + if 1 in edges: + ax.plot([x, x + size], + [y + size, y + size], **kwargs) + if 2 in edges: + ax.plot([x + size, x + size], + [y, y + size], **kwargs) + if 3 in edges: + ax.plot([x, x + size], + [y, y], **kwargs) + if 4 in edges: + ax.plot([x, x], + [y, y + size], **kwargs) + + if 5 in edges: + ax.plot([x, x + depth], + [y + size, y + depth + size], **kwargs) + if 6 in edges: + ax.plot([x + size, x + size + depth], + [y + size, y + depth + size], **kwargs) + if 7 in edges: + ax.plot([x + size, x + size + depth], + [y, y + depth], **kwargs) + if 8 in edges: + ax.plot([x, x + depth], + [y, y + depth], **kwargs) + + if 9 in edges: + ax.plot([x + depth, x + depth + size], + [y + depth + size, y + depth + size], **kwargs) + if 10 in edges: + ax.plot([x + depth + size, x + depth + size], + [y + depth, y + depth + size], **kwargs) + if 11 in edges: + ax.plot([x + depth, x + depth + size], + [y + depth, y + depth], **kwargs) + if 12 in edges: + ax.plot([x + depth, x + depth], + [y + depth, y + depth + size], **kwargs) + + if label: + if label_kwargs is None: + label_kwargs = {} + ax.text(x + 0.5 * size, y + 0.5 * size, label, + ha='center', va='center', **label_kwargs) + +solid = dict(c='black', ls='-', lw=1, + label_kwargs=dict(color='k')) +dotted = dict(c='black', ls='-', lw=0.5, alpha=0.5, + label_kwargs=dict(color='gray')) +depth = 0.3 + +# Draw a figure and axis with no boundary +fig = plt.figure(figsize=(5, 1), facecolor='w') +ax = plt.axes([0, 0, 1, 1], xticks=[], yticks=[], frameon=False) + +# first block +draw_cube(ax, (1, 7.5), 1, depth, [1, 2, 3, 4, 5, 6, 9], '1', **solid) +draw_cube(ax, (2, 7.5), 1, depth, [1, 2, 3, 6, 9], '2', **solid) +draw_cube(ax, (3, 7.5), 1, depth, [1, 2, 3, 6, 7, 9, 10], '3', **solid) + +draw_cube(ax, (1, 6.5), 1, depth, [2, 3, 4], '4', **solid) +draw_cube(ax, (2, 6.5), 1, depth, [2, 3], '5', **solid) +draw_cube(ax, (3, 6.5), 1, depth, [2, 3, 7, 10], '6', **solid) + +draw_cube(ax, (1, 5.5), 1, depth, [2, 3, 4], '7', **solid) +draw_cube(ax, (2, 5.5), 1, depth, [2, 3], '8', **solid) +draw_cube(ax, (3, 5.5), 1, depth, [2, 3, 7, 10], '9', **solid) + +# second block +draw_cube(ax, (6, 7.5), 1, depth, [1, 2, 3, 4, 5, 6, 9], '3', **solid) +draw_cube(ax, (7, 7.5), 1, depth, [1, 2, 3, 6, 9], '6', **solid) +draw_cube(ax, (8, 7.5), 1, depth, [1, 2, 3, 6, 7, 9, 10], '9', **solid) + +draw_cube(ax, (6, 6.5), 1, depth, range(2, 13), '3', **dotted) +draw_cube(ax, (7, 6.5), 1, depth, [2, 3, 6, 7, 9, 10, 11], '6', **dotted) +draw_cube(ax, (8, 6.5), 1, depth, [2, 3, 6, 7, 9, 10, 11], '9', **dotted) + +draw_cube(ax, (6, 5.5), 1, depth, [2, 3, 4, 7, 8, 10, 11, 12], '3', **dotted) +draw_cube(ax, (7, 5.5), 1, depth, [2, 3, 7, 10, 11], '6', **dotted) +draw_cube(ax, (8, 5.5), 1, depth, [2, 3, 7, 10, 11], '9', **dotted) + +# third block +draw_cube(ax, (12, 7.5), 1, depth, [1, 2, 3, 4, 5, 6, 9], '4', **solid) +draw_cube(ax, (13, 7.5), 1, depth, [1, 2, 3, 6, 9], '8', **solid) +draw_cube(ax, (14, 7.5), 1, depth, [1, 2, 3, 6, 7, 9, 10], '12', **solid) + +draw_cube(ax, (12, 6.5), 1, depth, [2, 3, 4], '7', **solid) +draw_cube(ax, (13, 6.5), 1, depth, [2, 3], '11', **solid) +draw_cube(ax, (14, 6.5), 1, depth, [2, 3, 7, 10], '15', **solid) + +draw_cube(ax, (12, 5.5), 1, depth, [2, 3, 4], '10', **solid) +draw_cube(ax, (13, 5.5), 1, depth, [2, 3], '14', **solid) +draw_cube(ax, (14, 5.5), 1, depth, [2, 3, 7, 10], '18', **solid) + +ax.text(5, 7.0, '+', size=12, ha='center', va='center') +ax.text(10.5, 7.0, '=', size=12, ha='center', va='center'); +``` + +`b -> (3, 1)` ആണെങ്കിലോ? + +ഈ case-ൽ, NumPy automatically `b -> (3, 1)` എന്നതിനെ `b -> (3, 3)` ആയി expand ചെയ്യും. + +Element-wise addition-ന്റെ ഫലം അപ്പോൾ ഒരു $3 \times 3$ matrix ആയിരിക്കും: + +```{code-cell} python3 +b.shape = (3, 1) + +a + b +``` + +ഈ broadcasting operation-ന്റെ ഒരു visual representation താഴെ കാണാം: + +```{code-cell} python3 +--- +tags: [hide-input] +--- + +fig = plt.figure(figsize=(5, 1), facecolor='w') +ax = plt.axes([0, 0, 1, 1], xticks=[], yticks=[], frameon=False) + +# first block +draw_cube(ax, (1, 7.5), 1, depth, [1, 2, 3, 4, 5, 6, 9], '1', **solid) +draw_cube(ax, (2, 7.5), 1, depth, [1, 2, 3, 6, 9], '2', **solid) +draw_cube(ax, (3, 7.5), 1, depth, [1, 2, 3, 6, 7, 9, 10], '3', **solid) + +draw_cube(ax, (1, 6.5), 1, depth, [2, 3, 4], '4', **solid) +draw_cube(ax, (2, 6.5), 1, depth, [2, 3], '5', **solid) +draw_cube(ax, (3, 6.5), 1, depth, [2, 3, 7, 10], '6', **solid) + +draw_cube(ax, (1, 5.5), 1, depth, [2, 3, 4], '7', **solid) +draw_cube(ax, (2, 5.5), 1, depth, [2, 3], '8', **solid) +draw_cube(ax, (3, 5.5), 1, depth, [2, 3, 7, 10], '9', **solid) + +# second block +draw_cube(ax, (6, 7.5), 1, depth, [1, 2, 3, 4, 5, 6, 7, 9, 10], '3', **solid) +draw_cube(ax, (7, 7.5), 1, depth, [1, 2, 3, 6, 7, 9, 10], '3', **dotted) +draw_cube(ax, (8, 7.5), 1, depth, [1, 2, 3, 6, 7, 9, 10], '3', **dotted) + +draw_cube(ax, (6, 6.5), 1, depth, [2, 3, 4, 7, 10], '6', **solid) +draw_cube(ax, (7, 6.5), 1, depth, [2, 3, 6, 7, 9, 10, 11], '6', **dotted) +draw_cube(ax, (8, 6.5), 1, depth, [2, 3, 6, 7, 9, 10, 11], '6', **dotted) + +draw_cube(ax, (6, 5.5), 1, depth, [2, 3, 4, 7, 10], '9', **solid) +draw_cube(ax, (7, 5.5), 1, depth, [2, 3, 7, 10, 11], '9', **dotted) +draw_cube(ax, (8, 5.5), 1, depth, [2, 3, 7, 10, 11], '9', **dotted) + +# third block +draw_cube(ax, (12, 7.5), 1, depth, [1, 2, 3, 4, 5, 6, 9], '4', **solid) +draw_cube(ax, (13, 7.5), 1, depth, [1, 2, 3, 6, 9], '5', **solid) +draw_cube(ax, (14, 7.5), 1, depth, [1, 2, 3, 6, 7, 9, 10], '6', **solid) + +draw_cube(ax, (12, 6.5), 1, depth, [2, 3, 4], '10', **solid) +draw_cube(ax, (13, 6.5), 1, depth, [2, 3], '11', **solid) +draw_cube(ax, (14, 6.5), 1, depth, [2, 3, 7, 10], '12', **solid) + +draw_cube(ax, (12, 5.5), 1, depth, [2, 3, 4], '16', **solid) +draw_cube(ax, (13, 5.5), 1, depth, [2, 3], '17', **solid) +draw_cube(ax, (14, 5.5), 1, depth, [2, 3, 7, 10], '18', **solid) + +ax.text(5, 7.0, '+', size=12, ha='center', va='center') +ax.text(10.5, 7.0, '=', size=12, ha='center', va='center'); + + +``` + +ചില cases-ൽ, ഇരു operands-ഉം expand ചെയ്യപ്പെടും. + +`a -> (3,)`, `b -> (3, 1)` എന്നിവയുള്ളപ്പോൾ, `a` എന്നത് `a -> (3, 3)` ആയി expand ചെയ്യപ്പെടും, `b` എന്നത് `b -> (3, 3)` ആയി expand ചെയ്യപ്പെടും. + +ഈ case-ൽ, element-wise addition-ന്റെ ഫലം ഒരു $3 \times 3$ matrix ആയിരിക്കും: + +```{code-cell} python3 +a = np.array([3, 6, 9]) +b = np.array([2, 3, 4]) +b.shape = (3, 1) + +a + b +``` + +ഈ broadcasting operation-ന്റെ ഒരു visual representation താഴെ കാണാം: + +```{code-cell} python3 +--- +tags: [hide-input] +--- + +# Draw a figure and axis with no boundary +fig = plt.figure(figsize=(5, 1), facecolor='w') +ax = plt.axes([0, 0, 1, 1], xticks=[], yticks=[], frameon=False) + +# first block +draw_cube(ax, (1, 7.5), 1, depth, [1, 2, 3, 4, 5, 6, 9], '3', **solid) +draw_cube(ax, (2, 7.5), 1, depth, [1, 2, 3, 6, 9], '6', **solid) +draw_cube(ax, (3, 7.5), 1, depth, [1, 2, 3, 6, 7, 9, 10], '9', **solid) + +draw_cube(ax, (1, 6.5), 1, depth, range(2, 13), '3', **dotted) +draw_cube(ax, (2, 6.5), 1, depth, [2, 3, 6, 7, 9, 10, 11], '6', **dotted) +draw_cube(ax, (3, 6.5), 1, depth, [2, 3, 6, 7, 9, 10, 11], '9', **dotted) + +draw_cube(ax, (1, 5.5), 1, depth, [2, 3, 4, 7, 8, 10, 11, 12], '3', **dotted) +draw_cube(ax, (2, 5.5), 1, depth, [2, 3, 7, 10, 11], '6', **dotted) +draw_cube(ax, (3, 5.5), 1, depth, [2, 3, 7, 10, 11], '9', **dotted) + +# second block +draw_cube(ax, (6, 7.5), 1, depth, [1, 2, 3, 4, 5, 6, 7, 9, 10], '2', **solid) +draw_cube(ax, (7, 7.5), 1, depth, [1, 2, 3, 6, 7, 9, 10], '2', **dotted) +draw_cube(ax, (8, 7.5), 1, depth, [1, 2, 3, 6, 7, 9, 10], '2', **dotted) + +draw_cube(ax, (6, 6.5), 1, depth, [2, 3, 4, 7, 10], '3', **solid) +draw_cube(ax, (7, 6.5), 1, depth, [2, 3, 6, 7, 9, 10, 11], '3', **dotted) +draw_cube(ax, (8, 6.5), 1, depth, [2, 3, 6, 7, 9, 10, 11], '3', **dotted) + +draw_cube(ax, (6, 5.5), 1, depth, [2, 3, 4, 7, 10], '4', **solid) +draw_cube(ax, (7, 5.5), 1, depth, [2, 3, 7, 10, 11], '4', **dotted) +draw_cube(ax, (8, 5.5), 1, depth, [2, 3, 7, 10, 11], '4', **dotted) + +# third block +draw_cube(ax, (12, 7.5), 1, depth, [1, 2, 3, 4, 5, 6, 9], '5', **solid) +draw_cube(ax, (13, 7.5), 1, depth, [1, 2, 3, 6, 9], '8', **solid) +draw_cube(ax, (14, 7.5), 1, depth, [1, 2, 3, 6, 7, 9, 10], '11', **solid) + +draw_cube(ax, (12, 6.5), 1, depth, [2, 3, 4], '6', **solid) +draw_cube(ax, (13, 6.5), 1, depth, [2, 3], '9', **solid) +draw_cube(ax, (14, 6.5), 1, depth, [2, 3, 7, 10], '12', **solid) + +draw_cube(ax, (12, 5.5), 1, depth, [2, 3, 4], '7', **solid) +draw_cube(ax, (13, 5.5), 1, depth, [2, 3], '10', **solid) +draw_cube(ax, (14, 5.5), 1, depth, [2, 3, 7, 10], '13', **solid) + +ax.text(5, 7.0, '+', size=12, ha='center', va='center') +ax.text(10.5, 7.0, '=', size=12, ha='center', va='center'); +``` + +Broadcasting വളരെ useful ആണെങ്കിലും, ചിലപ്പോൾ ഇത് confusing ആയി തോന്നാം. + +For example, `a -> (3, 2)`, `b -> (3,)` എന്നിവ add ചെയ്യാൻ ശ്രമിക്കാം. + +```{code-cell} python3 +--- +tags: [raises-exception] +--- +a = np.array( + [[1, 2], + [4, 5], + [7, 8]]) +b = np.array([3, 6, 9]) + +a + b +``` + +`ValueError`, operands-നെ ഒരുമിച്ച് broadcast ചെയ്യാൻ കഴിഞ്ഞില്ല എന്ന് നമ്മോട് പറയുന്നു. + + +ഈ broadcasting എന്തുകൊണ്ട് execute ചെയ്യാൻ കഴിയില്ല എന്ന് കാണിക്കുന്ന ഒരു visual representation താഴെ കാണാം: + +```{code-cell} python3 +--- +tags: [hide-input] +--- +# Draw a figure and axis with no boundary +fig = plt.figure(figsize=(3, 1.3), facecolor='w') +ax = plt.axes([0, 0, 1, 1], xticks=[], yticks=[], frameon=False) + +# first block +draw_cube(ax, (1, 7.5), 1, depth, [1, 2, 3, 4, 5, 6, 9], '1', **solid) +draw_cube(ax, (2, 7.5), 1, depth, [1, 2, 3, 6, 7, 9, 10], '2', **solid) + +draw_cube(ax, (1, 6.5), 1, depth, [2, 3, 4], '4', **solid) +draw_cube(ax, (2, 6.5), 1, depth, [2, 3, 7, 10], '5', **solid) + +draw_cube(ax, (1, 5.5), 1, depth, [2, 3, 4], '7', **solid) +draw_cube(ax, (2, 5.5), 1, depth, [2, 3, 7, 10], '8', **solid) + +# second block +draw_cube(ax, (6, 7.5), 1, depth, [1, 2, 3, 4, 5, 6, 9], '3', **solid) +draw_cube(ax, (7, 7.5), 1, depth, [1, 2, 3, 6, 9], '6', **solid) +draw_cube(ax, (8, 7.5), 1, depth, [1, 2, 3, 6, 7, 9, 10], '9', **solid) + +draw_cube(ax, (6, 6.5), 1, depth, range(2, 13), '3', **dotted) +draw_cube(ax, (7, 6.5), 1, depth, [2, 3, 6, 7, 9, 10, 11], '6', **dotted) +draw_cube(ax, (8, 6.5), 1, depth, [2, 3, 6, 7, 9, 10, 11], '9', **dotted) + +draw_cube(ax, (6, 5.5), 1, depth, [2, 3, 4, 7, 8, 10, 11, 12], '3', **dotted) +draw_cube(ax, (7, 5.5), 1, depth, [2, 3, 7, 10, 11], '6', **dotted) +draw_cube(ax, (8, 5.5), 1, depth, [2, 3, 7, 10, 11], '9', **dotted) + + +ax.text(4.5, 7.0, '+', size=12, ha='center', va='center') +ax.text(10, 7.0, '=', size=12, ha='center', va='center') +ax.text(11, 7.0, '?', size=16, ha='center', va='center'); +``` + +NumPy-ക്ക് arrays-നെ ഒരേ size-ലേക്ക് expand ചെയ്യാൻ കഴിയില്ല എന്ന് നമുക്ക് കാണാം. + +എന്തുകൊണ്ടെന്നാൽ, `b` എന്നത് `b -> (3,)`-ൽ നിന്നും `b -> (3, 3)`-ലേക്ക് expand ചെയ്യപ്പെടുമ്പോൾ, `b`-നെ `a -> (3, 2)`-മായി match ചെയ്യാൻ NumPy-ക്ക് കഴിയില്ല. + +Higher dimensions-ലേക്ക് നീങ്ങുമ്പോൾ കാര്യങ്ങൾ കൂടുതൽ ബുദ്ധിമുട്ടാകുന്നു. + +നമ്മെ സഹായിക്കാൻ, താഴെ പറയുന്ന rules-ന്റെ list ഉപയോഗിക്കാം: + +* *Step 1:* രണ്ട് arrays-ന്റെ dimensions match ചെയ്യാത്തപ്പോൾ, കുറച്ച് dimensions ഉള്ളതിനെ, existing dimensions-ന്റെ ഇടതുവശത്ത് dimension(s) കൂട്ടിച്ചേർത്ത് NumPy expand ചെയ്യും. + - For example, `a -> (3, 3)`, `b -> (3,)` ആണെങ്കിൽ, broadcasting ഇടതുവശത്ത് ഒരു dimension കൂട്ടിച്ചേർക്കും, അതിനാൽ `b -> (1, 3)` ആകും; + - `a -> (2, 2, 2)`, `b -> (2, 2)` ആണെങ്കിൽ, broadcasting ഇടതുവശത്ത് ഒരു dimension കൂട്ടിച്ചേർക്കും, അതിനാൽ `b -> (1, 2, 2)` ആകും; + - `a -> (3, 2, 2)`, `b -> (2,)` ആണെങ്കിൽ, broadcasting ഇടതുവശത്ത് രണ്ട് dimensions കൂട്ടിച്ചേർക്കും, അതിനാൽ `b -> (1, 1, 2)` ആകും (ഈ process, *Step 1* രണ്ട് പ്രാവശ്യം കടന്നുപോകുന്നത് ആയും കാണാം). + + +* *Step 2:* രണ്ട് arrays-ന് ഒരേ dimension ഉണ്ടെങ്കിലും, shapes വ്യത്യസ്തമാണെങ്കിൽ, shape index 1 ആയ dimensions expand ചെയ്യാൻ NumPy ശ്രമിക്കും. + - For example, `a -> (1, 3)`, `b -> (3, 1)` ആണെങ്കിൽ, broadcasting `a`-യിലും `b`-യിലും shape 1 ഉള്ള dimensions expand ചെയ്യും, അതിനാൽ `a -> (3, 3)`, `b -> (3, 3)` ആകും; + - `a -> (2, 2, 2)`, `b -> (1, 2, 2)` ആണെങ്കിൽ, broadcasting `b`-യുടെ ആദ്യത്തെ dimension expand ചെയ്യും, അതിനാൽ `b -> (2, 2, 2)` ആകും; + - `a -> (3, 2, 2)`, `b -> (1, 1, 2)` ആണെങ്കിൽ, broadcasting `b`-യെ shape 1 ഉള്ള എല്ലാ dimensions-ലും expand ചെയ്യും, അതിനാൽ `b -> (3, 2, 2)` ആകും. + +* *Step 3:* Step 1, 2 എന്നിവയ്ക്ക് ശേഷം, രണ്ട് arrays-ഉം ഇപ്പോഴും match ചെയ്യുന്നില്ലെങ്കിൽ, ഒരു `ValueError` raise ചെയ്യപ്പെടും. For example, `a -> (2, 2, 3)`, `b -> (2, 2)` ആണെന്ന് കരുതുക: + - *Step 1* പ്രകാരം, `b` എന്നത് `b -> (1, 2, 2)` ആയി expand ചെയ്യപ്പെടും; + - *Step 2* പ്രകാരം, `b` എന്നത് `b -> (2, 2, 2)` ആയി expand ചെയ്യപ്പെടും; + - ആദ്യത്തെ രണ്ട് steps-ന് ശേഷവും അവ പരസ്പരം match ചെയ്യുന്നില്ല എന്ന് നമുക്ക് കാണാം. അതിനാൽ, ഒരു `ValueError` raise ചെയ്യപ്പെടും. + + + +## Mutability and Copying Arrays + +NumPy arrays, Python lists-നെ പോലെ mutable data types ആണ്. + +അതായത്, initialization-ന് ശേഷം അവയുടെ contents memory-യിൽ alter ചെയ്യാൻ (mutate ചെയ്യാൻ) കഴിയും. + +ഇത് convenient ആണ്, പക്ഷേ Python-ന്റെ naming, reference model എന്നിവയുമായി combine ചെയ്യുമ്പോൾ, NumPy beginners-ന് mistakes-ലേക്ക് നയിക്കാം. + +ഈ section-ൽ കുറച്ച് key issues നമുക്ക് നോക്കാം. + + +### Mutability + +Mutability-യുടെ examples നമ്മൾ മുകളിൽ already കണ്ടു. + +NumPy array-യുടെ mutation-ന്റെ മറ്റൊരു example താഴെ കാണാം: + +```{code-cell} python3 +a = np.array([42, 44]) +a +``` + +```{code-cell} python3 +a[-1] = 0 # Change last element to 0 +a +``` + +Mutability താഴെ പറയുന്ന behavior-ലേക്ക് നയിക്കുന്നു (ഇത് MATLAB programmers-നെ ഞെട്ടിക്കാം...) + +```{code-cell} python3 +rng = np.random.default_rng() +a = rng.standard_normal(3) +a +``` + +```{code-cell} python3 +b = a +b[0] = 0.0 +a +``` + +സംഭവിച്ചത് എന്തെന്നാൽ, `b`-നെ മാറ്റിയപ്പോൾ നമ്മൾ `a`-യെയും മാറ്റിയിരിക്കുന്നു. + +`b` എന്ന name, `a`-യുമായി bind ചെയ്യപ്പെട്ടിരിക്കുന്നു, ഇത് ആ array-യുടെ മറ്റൊരു reference മാത്രമായി മാറുന്നു (Python assignment model {doc}`later in the course ` കൂടുതൽ വിശദമായി describe ചെയ്യുന്നു). + +അതിനാൽ, ആ array-യിൽ changes നടത്താൻ അതിന് equal rights ഉണ്ട്. + +വാസ്തവത്തിൽ ഇതാണ് ഏറ്റവും sensible ആയ default behavior! + +ഇതിനർത്ഥം, copies ഉണ്ടാക്കുന്നതിന് പകരം, data-യിലേക്കുള്ള pointers മാത്രമാണ് നമ്മൾ pass ചെയ്യുന്നത് എന്നാണ്. + +Copies ഉണ്ടാക്കുന്നത് speed-ന്റെയും memory-യുടെയും കാര്യത്തിൽ expensive ആണ്. + +### Making Copies + +ആവശ്യമുള്ളപ്പോൾ `b`-നെ `a`-യുടെ ഒരു independent copy ആക്കാൻ കഴിയും, തീർച്ചയായും. + +ഇത് `np.copy` ഉപയോഗിച്ച് ചെയ്യാം: + +```{code-cell} python3 +a = rng.standard_normal(3) +a +``` + +```{code-cell} python3 +b = np.copy(a) +b +``` + +ഇപ്പോൾ `b` എന്നത് ഒരു independent copy ആണ് (ഇതിനെ *deep copy* എന്ന് വിളിക്കുന്നു): + +```{code-cell} python3 +b[:] = 1 +b +``` + +```{code-cell} python3 +a +``` + +`b`-യിലെ change, `a`-യെ ബാധിച്ചിട്ടില്ല എന്ന് ശ്രദ്ധിക്കുക. + + + + +## Additional Features + +NumPy-യുടെ മറ്റ് ചില useful features നമുക്ക് നോക്കാം. + + +### Universal Functions + +```{index} single: NumPy; Vectorized Functions +``` + +Arrays-ൽ *element-wise* ആയി act ചെയ്യുന്ന standard functions ആയ `log`, `exp`, `sin`, തുടങ്ങിയവയുടെ versions NumPy നൽകുന്നു: + +```{code-cell} python3 +z = np.array([1, 2, 3]) +np.sin(z) +``` + +താഴെ കാണിച്ചിരിക്കുന്നത് പോലുള്ള explicit element-by-element loops-ന്റെ ആവശ്യം ഇത് ഒഴിവാക്കുന്നു: + +```{code-cell} python3 +n = len(z) +y = np.empty(n) +for i in range(n): + y[i] = np.sin(z[i]) +``` + +Arrays-ൽ element-wise ആയി act ചെയ്യുന്നതിനാൽ, ഈ functions-നെ ചിലപ്പോൾ **vectorized functions** എന്ന് വിളിക്കുന്നു. + +NumPy-speak-ൽ, ഇവയെ **ufuncs**, അല്ലെങ്കിൽ **universal functions** എന്നും വിളിക്കുന്നു. + +മുകളിൽ നമ്മൾ കണ്ടത് പോലെ, സാധാരണ arithmetic operations (`+`, `*`, തുടങ്ങിയവ) element-wise ആയും work ചെയ്യുന്നു, ഇവയെ ufuncs-ഉമായി combine ചെയ്യുമ്പോൾ, വളരെ വലിയൊരു set of fast element-wise functions ലഭിക്കുന്നു. + +```{code-cell} python3 +z +``` + +```{code-cell} python3 +(1 / np.sqrt(2 * np.pi)) * np.exp(- 0.5 * z**2) +``` + +എല്ലാ user-defined functions-ഉം element-wise ആയി act ചെയ്യണമെന്നില്ല. + +For example, താഴെ define ചെയ്തിരിക്കുന്ന `f` എന്ന function-ന് ഒരു NumPy array pass ചെയ്യുന്നത് ഒരു `ValueError`-ന് കാരണമാകുന്നു: + +```{code-cell} python3 +def f(x): + return 1 if x > 0 else 0 +``` + +NumPy function `np.where`, ഒരു vectorized alternative നൽകുന്നു: + +```{code-cell} python3 +x = rng.standard_normal(4) +x +``` + +```{code-cell} python3 +np.where(x > 0, 1, 0) # Insert 1 if x > 0 true, otherwise 0 +``` + +തന്നിരിക്കുന്ന ഒരു function vectorize ചെയ്യാൻ `np.vectorize`-ഉം ഉപയോഗിക്കാം: + +```{code-cell} python3 +f = np.vectorize(f) +f(x) # Passing the same vector x as in the previous example +``` + +എന്നിരുന്നാലും, ഈ approach, കൂടുതൽ carefully crafted ആയ ഒരു vectorized function-ന്റെ speed എപ്പോഴും obtain ചെയ്യില്ല. + +(പിന്നീട് നമ്മൾ കാണും, JAX-ന് `np.vectorize`-ന്റെ ഒരു powerful version ഉണ്ട്, ഇത് പലപ്പോഴും highly efficient code generate ചെയ്യും.) + + +### Comparisons + +```{index} single: NumPy; Comparisons +``` + +സാധാരണയായി, arrays-ലെ comparisons element-wise ആയാണ് ചെയ്യുന്നത്: + +```{code-cell} python3 +z = np.array([2, 3]) +y = np.array([2, 3]) +z == y +``` + +```{code-cell} python3 +y[0] = 5 +z == y +``` + +```{code-cell} python3 +z != y +``` + +`>`, `<`, `>=`, `<=` എന്നിവയ്ക്കും situation similar ആണ്. + +Scalars-നെതിരെയും നമുക്ക് comparisons ചെയ്യാം: + +```{code-cell} python3 +z = np.linspace(0, 10, 5) +z +``` + +```{code-cell} python3 +z > 3 +``` + +*Conditional extraction*-ന് ഇത് പ്രത്യേകിച്ചും useful ആണ്: + +```{code-cell} python3 +b = z > 3 +b +``` + +```{code-cell} python3 +z[b] +``` + +തീർച്ചയായും നമുക്ക് ഇത് ഒറ്റ step-ൽ ചെയ്യാം---അതാണ് പലപ്പോഴും ചെയ്യുന്നത്: + +```{code-cell} python3 +z[z > 3] +``` + +### Sub-packages + +Scientific programming-മായി ബന്ധപ്പെട്ട ചില additional functionality, NumPy അതിന്റെ sub-packages വഴി നൽകുന്നു. + +NumPy-യുടെ [random `Generator`](https://numpy.org/doc/stable/reference/random/generator.html#random-generator) ഉപയോഗിച്ച് random variables generate ചെയ്യുന്നത് നമ്മൾ already കണ്ടു. + +```{code-cell} python3 +z = rng.standard_normal(10000) # Generate standard normals +y = rng.binomial(10, 0.5, size=1000) # 1,000 draws from Bin(10, 0.5) +y.mean() +``` + +സാധാരണയായി ഉപയോഗിക്കുന്ന മറ്റൊരു subpackage ആണ് np.linalg: + +```{code-cell} python3 +A = np.array([[1, 2], [3, 4]]) + +np.linalg.det(A) # Compute the determinant +``` + +```{code-cell} python3 +np.linalg.inv(A) # Compute the inverse +``` + +```{index} single: SciPy +``` + +```{index} single: Python; SciPy +``` + +ഈ functionality-യുടെ ഭൂരിഭാഗവും [SciPy](https://scipy.org/)-യിലും ലഭ്യമാണ്, NumPy-യുടെ മുകളിൽ build ചെയ്തിരിക്കുന്ന modules-ന്റെ ഒരു collection ആണിത്. + +SciPy versions നമ്മൾ {doc}`soon ` കൂടുതൽ വിശദമായി cover ചെയ്യും. + +NumPy-യിൽ ലഭ്യമായതിന്റെ ഒരു comprehensive list-ന് [this documentation](https://numpy.org/doc/stable/reference/routines.html) കാണുക. + + +### Implicit Multithreading + +[Previously](need_for_speed) multithreading വഴിയുള്ള parallelization-ന്റെ concept നമ്മൾ discuss ചെയ്തു. + +NumPy അതിന്റെ compiled code-ന്റെ ഭൂരിഭാഗത്തിലും multithreading implement ചെയ്യാൻ ശ്രമിക്കുന്നു. + +ഇത് action-ൽ കാണാൻ ഒരു example നമുക്ക് നോക്കാം. + +അടുത്ത code piece, randomly generate ചെയ്ത ധാരാളം matrices-ന്റെ eigenvalues compute ചെയ്യുന്നു. + +ഇത് run ചെയ്യാൻ കുറച്ച് seconds എടുക്കും. + +```{code-cell} python3 +n = 20 +m = 1000 +for i in range(n): + X = rng.standard_normal((m, m)) + λ = np.linalg.eigvals(X) +``` + +ഇനി, ഈ code run ചെയ്യുമ്പോൾ നമ്മുടെ machine-ലെ htop system monitor-ന്റെ output നമുക്ക് നോക്കാം: + +```{figure} /_static/lecture_specific/parallelization/htop_parallel_npmat.png +:scale: 80 +``` + +8 CPUs-ൽ 4 എണ്ണം full speed-ൽ run ചെയ്യുന്നത് നമുക്ക് കാണാം. + +NumPy-യുടെ `eigvals` routine tasks-നെ neat ആയി split up ചെയ്ത് വ്യത്യസ്ത threads-ലേക്ക് distribute ചെയ്യുന്നത് കൊണ്ടാണ് ഇത്. + + + + + +## Exercises + + +```{exercise-start} +:label: np_ex1 +``` + +Consider the polynomial expression + +```{math} +:label: np_polynom + +p(x) = a_0 + a_1 x + a_2 x^2 + \cdots a_N x^N = \sum_{n=0}^N a_n x^n +``` + +{ref}`Earlier `, you wrote a simple function `p(x, coeff)` to evaluate {eq}`np_polynom` without considering efficiency. + +Now write a new function that does the same job, but uses NumPy arrays and array operations for its computations, rather than any form of Python loop. + +(Such functionality is already implemented as `np.poly1d`, but for the sake of the exercise don't use this class) + +```{hint} +:class: dropdown +Use `np.cumprod()` +``` +```{exercise-end} +``` + +```{solution-start} np_ex1 +:class: dropdown +``` + +This code does the job + +```{code-cell} python3 +def p(x, coef): + X = np.ones_like(coef) + X[1:] = x + y = np.cumprod(X) # y = [1, x, x**2,...] + return coef @ y +``` + +Let's test it + +```{code-cell} python3 +x = 2 +coef = np.linspace(2, 4, 3) +print(coef) +print(p(x, coef)) +# For comparison +q = np.poly1d(np.flip(coef)) +print(q(x)) +``` + +```{solution-end} +``` + + +```{exercise-start} +:label: np_ex2 +``` + +Let `q` be a NumPy array of length `n` with `q.sum() == 1`. + +Suppose that `q` represents a [probability mass function](https://en.wikipedia.org/wiki/Probability_mass_function). + +We wish to generate a discrete random variable $x$ such that $\mathbb P\{x = i\} = q_i$. + +In other words, `x` takes values in `range(len(q))` and `x = i` with probability `q[i]`. + +The standard (inverse transform) algorithm is as follows: + +* Divide the unit interval $[0, 1]$ into $n$ subintervals $I_0, I_1, \ldots, I_{n-1}$ such that the length of $I_i$ is $q_i$. +* Draw a uniform random variable $U$ on $[0, 1]$ and return the $i$ such that $U \in I_i$. + +The probability of drawing $i$ is the length of $I_i$, which is equal to $q_i$. + +We can implement the algorithm as follows + +```{code-cell} python3 +from random import uniform + +def sample(q): + a = 0.0 + U = uniform(0, 1) + for i in range(len(q)): + if a < U <= a + q[i]: + return i + a = a + q[i] +``` + +If you can't see how this works, try thinking through the flow for a simple example, such as `q = [0.25, 0.75]` +It helps to sketch the intervals on paper. + +Your exercise is to speed it up using NumPy, avoiding explicit loops + +```{hint} +:class: dropdown + +Use `np.searchsorted` and `np.cumsum` + +``` + +If you can, implement the functionality as a class called `DiscreteRV`, where + +* the data for an instance of the class is the vector of probabilities `q` +* the class has a `draw()` method, which returns one draw according to the algorithm described above + +If you can, write the method so that `draw(k)` returns `k` draws from `q`. + +```{exercise-end} +``` + +```{solution-start} np_ex2 +:class: dropdown +``` + +Here's our first pass at a solution: + +```{code-cell} python3 +from numpy import cumsum + +class DiscreteRV: + """ + Generates an array of draws from a discrete random variable with vector of + probabilities given by q. + """ + + def __init__(self, q, seed=None): + """ + The argument q is a NumPy array, or array like, nonnegative and sums + to 1. + + The argument seed sets the seed for the underlying random number + generator; with the default seed=None, draws are not reproducible + across runs. + """ + self.q = q + self.Q = cumsum(q) + self.rng = np.random.default_rng(seed) + + def draw(self, k=1): + """ + Returns k draws from q. For each such draw, the value i is returned + with probability q[i]. + """ + return self.Q.searchsorted(self.rng.uniform(0, 1, size=k)) +``` + +The logic is not obvious, but if you take your time and read it slowly, +you will understand. + +There is a problem here, however. + +Suppose that `q` is altered after an instance of `DiscreteRV` is +created, for example by + +```{code-cell} python3 +q = (0.1, 0.9) +d = DiscreteRV(q) +d.q = (0.5, 0.5) +``` + +The problem is that `Q` does not change accordingly, and `Q` is the +data used in the `draw` method. + +To deal with this, one option is to compute `Q` every time the draw +method is called. + +But this is inefficient relative to computing `Q` once-off. + +A better option is to use descriptors. + +A solution from the [quantecon +library](https://github.com/QuantEcon/QuantEcon.py/tree/main/quantecon) +using descriptors that behaves as we desire can be found +[here](https://github.com/QuantEcon/QuantEcon.py/blob/main/quantecon/discrete_rv.py). + +```{solution-end} +``` + + +```{exercise} +:label: np_ex3 + +Recall our {ref}`earlier discussion ` of the empirical cumulative distribution function. + +Your task is to + +1. Make the `__call__` method more efficient using NumPy. +1. Add a method that plots the ECDF over $[a, b]$, where $a$ and $b$ are method parameters. +``` + +```{solution-start} np_ex3 +:class: dropdown +``` + +An example solution is given below. + +In essence, we've just taken [this code](https://github.com/QuantEcon/QuantEcon.py/blob/main/quantecon/ecdf.py) +from QuantEcon and added in a plot method + +```{code-cell} python3 +""" +Modifies ecdf.py from QuantEcon to add in a plot method + +""" + +class ECDF: + """ + One-dimensional empirical distribution function given a vector of + observations. + + Parameters + ---------- + observations : array_like + An array of observations + + Attributes + ---------- + observations : array_like + An array of observations + + """ + + def __init__(self, observations): + self.observations = np.asarray(observations) + + def __call__(self, x): + """ + Evaluates the ecdf at x + + Parameters + ---------- + x : scalar(float) + The x at which the ecdf is evaluated + + Returns + ------- + scalar(float) + Fraction of the sample less than x + + """ + return np.mean(self.observations <= x) + + def plot(self, ax, a=None, b=None): + """ + Plot the ecdf on the interval [a, b]. + + Parameters + ---------- + a : scalar(float), optional(default=None) + Lower endpoint of the plot interval + b : scalar(float), optional(default=None) + Upper endpoint of the plot interval + + """ + + # === choose reasonable interval if [a, b] not specified === # + if a is None: + a = self.observations.min() - self.observations.std() + if b is None: + b = self.observations.max() + self.observations.std() + + # === generate plot === # + x_vals = np.linspace(a, b, num=100) + f = np.vectorize(self.__call__) + ax.plot(x_vals, f(x_vals)) + plt.show() +``` + +Here's an example of usage + +```{code-cell} python3 +fig, ax = plt.subplots() +rng = np.random.default_rng() +X = rng.standard_normal(1000) +F = ECDF(X) +F.plot(ax) +``` + +```{solution-end} +``` + + +```{exercise-start} +:label: np_ex4 +``` + +Recall that [broadcasting](broadcasting) in NumPy can help us conduct element-wise operations on arrays with different number of dimensions without using `for` loops. + +In this exercise, try to use `for` loops to replicate the result of the following broadcasting operations. + +**Part 1**: Try to replicate this simple example using `for` loops and compare your results with the broadcasting operation below. + +```{code-cell} python3 + +rng = np.random.default_rng(123) +x = rng.standard_normal((4, 4)) +y = rng.standard_normal(4) +A = x / y +``` + +Here is the output + +```{code-cell} python3 +--- +tags: [hide-output] +--- +print(A) +``` + +**Part 2**: Move on to replicate the result of the following broadcasting operation. Meanwhile, compare the speeds of broadcasting and the `for` loop you implement. + +For this part of the exercise you can use the `qe.Timer()` context manager from the `quantecon` library to time the execution. + +Let's make sure this library is installed. + +```{code-cell} python3 +:tags: [hide-output] +!pip install quantecon +``` + +Now we can import the quantecon package. + +```{code-cell} python3 + +rng = np.random.default_rng(123) +x = rng.standard_normal((1000, 100, 100)) +y = rng.standard_normal(100) + +with qe.Timer("Broadcasting operation"): + B = x / y +``` + +Here is the output + +```{code-cell} python3 +--- +tags: [hide-output] +--- +print(B) +``` + +```{exercise-end} +``` + + +```{solution-start} np_ex4 +:class: dropdown +``` + +**Part 1 Solution** + +```{code-cell} python3 +rng = np.random.default_rng(123) +x = rng.standard_normal((4, 4)) +y = rng.standard_normal(4) + +C = np.empty_like(x) +n = len(x) +for i in range(n): + for j in range(n): + C[i, j] = x[i, j] / y[j] +``` + +Compare the results to check your answer + +```{code-cell} python3 +--- +tags: [hide-output] +--- +print(C) +``` + +You can also use `array_equal()` to check your answer + +```{code-cell} python3 +print(np.array_equal(A, C)) +``` + + +**Part 2 Solution** + +```{code-cell} python3 + +rng = np.random.default_rng(123) +x = rng.standard_normal((1000, 100, 100)) +y = rng.standard_normal(100) + +with qe.Timer("For loop operation"): + D = np.empty_like(x) + d1, d2, d3 = x.shape + for i in range(d1): + for j in range(d2): + for k in range(d3): + D[i, j, k] = x[i, j, k] / y[k] +``` + +Note that the `for` loop takes much longer than the broadcasting operation. + +Compare the results to check your answer + +```{code-cell} python3 +--- +tags: [hide-output] +--- +print(D) +``` + +```{code-cell} python3 +print(np.array_equal(B, D)) +``` + +```{solution-end} +``` diff --git a/experiments/ml-benchmark/arms/2026-09-21-round4-numpy-v0.29.2/numpy-v0.29.2-draw2.md b/experiments/ml-benchmark/arms/2026-09-21-round4-numpy-v0.29.2/numpy-v0.29.2-draw2.md new file mode 100644 index 0000000..82c5be9 --- /dev/null +++ b/experiments/ml-benchmark/arms/2026-09-21-round4-numpy-v0.29.2/numpy-v0.29.2-draw2.md @@ -0,0 +1,1549 @@ +--- +jupytext: + text_representation: + extension: .md + format_name: myst +kernelspec: + display_name: Python 3 + language: python + name: python3 +translation: + title: NumPy + headings: + Overview: Overview + NumPy Arrays: NumPy Arrays + NumPy Arrays::Basics: Basics + NumPy Arrays::Shape and Dimension: Shape and Dimension + NumPy Arrays::Creating Arrays: Creating Arrays + NumPy Arrays::Array Indexing: Array Indexing + NumPy Arrays::Array Methods: Array Methods + Arithmetic Operations: Arithmetic Operations + Matrix Multiplication: Matrix Multiplication + Broadcasting: Broadcasting + Mutability and Copying Arrays: Mutability and Copying Arrays + Mutability and Copying Arrays::Mutability: Mutability + Mutability and Copying Arrays::Making Copies: Making Copies + Additional Features: Additional Features + Additional Features::Universal Functions: Universal Functions + Additional Features::Comparisons: Comparisons + Additional Features::Sub-packages: Sub-packages + Additional Features::Implicit Multithreading: Implicit Multithreading + Exercises: Exercises +--- + +(np)= +```{raw} jupyter + +``` + +# {index}`NumPy ` + +```{index} single: Python; NumPy +``` + +```{epigraph} +"Let's be clear: the work of science has nothing whatever to do with consensus. Consensus is the business of politics. Science, on the contrary, requires only one investigator who happens to be right, which means that he or she has results that are verifiable by reference to the real world. In science consensus is irrelevant. What is relevant is reproducible results." -- Michael Crichton +``` + +Anaconda-യിൽ ഉള്ളതിന് പുറമേ, ഈ lecture-ന് താഴെ പറയുന്ന libraries-ഉം ആവശ്യമായിവരുന്നു: + +```{code-cell} ipython3 +:tags: [hide-output] + +!pip install quantecon +``` + +## Overview + +[NumPy](https://en.wikipedia.org/wiki/NumPy) എന്നത്, numerical programming-നായുള്ള ഒരു മികച്ച library ആണ്. + +* Academia, finance, industry എന്നിവയിൽ വ്യാപകമായി ഉപയോഗിക്കപ്പെടുന്നു. +* Mature ആയതും, വേഗതയുള്ളതും, stable ആയതും, തുടർച്ചയായി development-ന് വിധേയമായതും ആണ്. + +മുൻ lectures-ൽ NumPy ഉൾപ്പെടുന്ന കുറച്ച് code നമ്മൾ already കണ്ടിട്ടുണ്ട്. + +ഈ lecture-ൽ, നമുക്ക് + +1. NumPy arrays-ഉം +1. NumPy നൽകുന്ന അടിസ്ഥാന array processing operations-ഉം + +ഇവയെക്കുറിച്ച് കൂടുതൽ systematic ആയ ഒരു ചർച്ച തുടങ്ങാം. + +(മറ്റൊരു reference-ന്, [the official NumPy documentation](https://numpy.org/doc/stable/reference/) നോക്കുക.) + +നമുക്ക് താഴെ പറയുന്ന imports ഉപയോഗിക്കാം. + +```{code-cell} python3 +import numpy as np +import random +import quantecon as qe +import matplotlib.pyplot as plt +from mpl_toolkits.mplot3d.axes3d import Axes3D +from matplotlib import cm +``` + + + +(numpy_array)= +## NumPy Arrays + +```{index} single: NumPy; Arrays +``` + +NumPy പരിഹരിക്കുന്ന അടിസ്ഥാന പ്രശ്നം വേഗതയേറിയ array processing ആണ്. + +NumPy define ചെയ്യുന്ന ഏറ്റവും പ്രധാനപ്പെട്ട structure, ഔപചാരികമായി [numpy.ndarray](https://numpy.org/doc/stable/reference/arrays.ndarray.html) എന്ന് വിളിക്കപ്പെടുന്ന ഒരു array data type ആണ്. + +NumPy arrays, scientific Python ecosystem-ന്റെ വലിയൊരു ഭാഗത്തിന് ശക്തി പകരുന്നു. + +### Basics + +Zeros മാത്രം ഉള്ള ഒരു NumPy array create ചെയ്യാൻ നമ്മൾ [np.zeros](https://numpy.org/doc/stable/reference/generated/numpy.zeros.html#numpy.zeros) ഉപയോഗിക്കുന്നു + +```{code-cell} python3 +a = np.zeros(3) +a +``` + +```{code-cell} python3 +type(a) +``` + +NumPy arrays, native Python lists-നോട് അല്പം സാമ്യമുള്ളവയാണ്, പക്ഷേ ഒരു വ്യത്യാസമുണ്ട് + +* Data *homogeneous ആയിരിക്കണം* (എല്ലാ elements-ഉം ഒരേ type-ൽ ഉള്ളതായിരിക്കണം). +* ഈ types, NumPy നൽകുന്ന [data types](https://numpy.org/doc/stable/reference/arrays.dtypes.html) (`dtypes`) ഇവയിൽ ഒന്നായിരിക്കണം. + +ഈ dtypes-ൽ ഏറ്റവും പ്രധാനപ്പെട്ടവ: + +* float64: 64 bit floating-point number +* int64: 64 bit integer +* bool: 8 bit True or False + +Complex numbers, unsigned integers തുടങ്ങിയവയെ represent ചെയ്യാൻ വേറെയും dtypes ഉണ്ട്. + +ആധുനിക machines-ൽ, arrays-ന്റെ default dtype `float64` ആണ് + +```{code-cell} python3 +a = np.zeros(3) +type(a[0]) +``` + +നമുക്ക് integers ഉപയോഗിക്കണമെങ്കിൽ താഴെ പറയുന്ന രീതിയിൽ specify ചെയ്യാം: + +```{code-cell} python3 +a = np.zeros(3, dtype=int) +type(a[0]) +``` + +(numpy_shape_dim)= +### Shape and Dimension + +```{index} single: NumPy; Arrays (Shape and Dimension) +``` + +താഴെ കൊടുത്തിരിക്കുന്ന assignment നോക്കാം + +```{code-cell} python3 +z = np.zeros(10) +``` + +ഇവിടെ `z` എന്നത് ഒരു **flat** array ആണ് --- row vector-ഉം അല്ല column vector-ഉം അല്ല. + +```{code-cell} python3 +z.shape +``` + +ഇവിടെ shape tuple-ന് ഒരു element മാത്രമേ ഉള്ളൂ, അത് array-യുടെ length ആണ് +(ഒരു element ഉള്ള tuples ഒരു comma-യിൽ അവസാനിക്കും). + +ഇതിന് ഒരു additional dimension നൽകാൻ, നമുക്ക് `shape` attribute മാറ്റാം + +```{code-cell} python3 +z.shape = (10, 1) # Convert flat array to column vector (two-dimensional) +z +``` + +```{code-cell} python3 +z = np.zeros(4) # Flat array +z.shape = (2, 2) # Two-dimensional array +z +``` + +അവസാനത്തെ case-ൽ, 2x2 array undegerbestellen ഉണ്ടാക്കാൻ, നമുക്ക് `zeros()` function-ന് ഒരു tuple pass ചെയ്യാം, `z = np.zeros((2, 2))` എന്നതുപോലെ. + + + +(creating_arrays)= +### Creating Arrays + +```{index} single: NumPy; Arrays (Creating) +``` + +നമ്മൾ കണ്ടതുപോലെ, `np.zeros` function zeros ഉള്ള ഒരു array create ചെയ്യുന്നു. + +`np.ones` എന്താണ് create ചെയ്യുന്നതെന്ന് നിങ്ങൾക്ക് ഊഹിക്കാൻ കഴിയും. + +ഇതുമായി ബന്ധപ്പെട്ടതാണ് `np.empty`, ഇത് memory-യിൽ arrays create ചെയ്യുന്നു, പിന്നീട് data ഉപയോഗിച്ച് populate ചെയ്യാവുന്നവ + +```{code-cell} python3 +z = np.empty(3) +z +``` + +ഇവിടെ കാണുന്ന numbers garbage values ആണ്. + +(Python 3 contiguous 64 bit memory pieces allocate ചെയ്യുന്നു, ആ memory slots-ലെ existing contents `float64` values ആയി interpret ചെയ്യപ്പെടുന്നു) + +Evenly spaced numbers-ന്റെ ഒരു grid set ചെയ്യാൻ `np.linspace` ഉപയോഗിക്കുക + +```{code-cell} python3 +z = np.linspace(2, 4, 5) # From 2 to 4, with 5 elements +``` + +ഒരു identity matrix create ചെയ്യാൻ `np.identity` അല്ലെങ്കിൽ `np.eye` ഉപയോഗിക്കുക + +```{code-cell} python3 +z = np.identity(2) +z +``` + +ഇതിന് പുറമേ, `np.array` ഉപയോഗിച്ച് Python lists, tuples തുടങ്ങിയവയിൽ നിന്നും NumPy arrays create ചെയ്യാൻ കഴിയും + +```{code-cell} python3 +z = np.array([10, 20]) # ndarray from Python list +z +``` + +```{code-cell} python3 +type(z) +``` + +```{code-cell} python3 +z = np.array((10, 20), dtype=float) # Here 'float' is equivalent to 'np.float64' +z +``` + +```{code-cell} python3 +z = np.array([[1, 2], [3, 4]]) # 2D array from a list of lists +z +``` + +സമാനമായ ഒരു function നൽകുന്ന `np.asarray`-യും കാണുക, പക്ഷേ ഇത് ഒരു NumPy array-യിൽ already ഉള്ള data-യുടെ ഒരു പ്രത്യേക copy ഉണ്ടാക്കുന്നില്ല. + +Numeric data ഉള്ള ഒരു text file-ൽ നിന്നും array data read ചെയ്യാൻ `np.loadtxt` ഉപയോഗിക്കുക --- വിശദവിവരങ്ങൾക്ക് [the documentation](https://numpy.org/doc/stable/reference/routines.io.html) കാണുക. + + + +### Array Indexing + +```{index} single: NumPy; Arrays (Indexing) +``` + +ഒരു flat array-ക്ക്, indexing Python sequences-ന്റേതിന് സമാനമാണ്: + +```{code-cell} python3 +z = np.linspace(1, 2, 5) +z +``` + +```{code-cell} python3 +z[0] +``` + +```{code-cell} python3 +z[0:2] # Two elements, starting at element 0 +``` + +```{code-cell} python3 +z[-1] +``` + +2D arrays-ന് index syntax താഴെ പറയുന്നത് പോലെയാണ്: + +```{code-cell} python3 +z = np.array([[1, 2], [3, 4]]) +z +``` + +```{code-cell} python3 +z[0, 0] +``` + +```{code-cell} python3 +z[0, 1] +``` + +ഇങ്ങനെ തുടരാം. + +Columns, rows എന്നിവ താഴെ പറയുന്ന രീതിയിൽ extract ചെയ്യാം + +```{code-cell} python3 +z[0, :] +``` + +```{code-cell} python3 +z[:, 1] +``` + +Elements extract ചെയ്യാൻ integers-ന്റെ NumPy arrays-ഉം ഉപയോഗിക്കാം + +```{code-cell} python3 +z = np.linspace(2, 4, 5) +z +``` + +```{code-cell} python3 +indices = np.array((0, 2, 3)) +z[indices] +``` + +അവസാനമായി, elements extract ചെയ്യാൻ `dtype bool` ഉള്ള ഒരു array ഉപയോഗിക്കാം + +```{code-cell} python3 +z +``` + +```{code-cell} python3 +d = np.array([0, 1, 1, 0, 0], dtype=bool) +d +``` + +```{code-cell} python3 +z[d] +``` + +ഇത് എന്തുകൊണ്ട് useful ആണെന്ന് താഴെ നമുക്ക് കാണാം. + +ഒരു aside: slice notation ഉപയോഗിച്ച് ഒരു array-യുടെ എല്ലാ elements-ഉം ഒരൊറ്റ number-ന് equal ആയി set ചെയ്യാം + +```{code-cell} python3 +z = np.empty(3) +z +``` + +```{code-cell} python3 +z[:] = 42 +z +``` + +### Array Methods + +```{index} single: NumPy; Arrays (Methods) +``` + +Arrays-ന് useful ആയ methods ഉണ്ട്, ഇവയെല്ലാം carefully optimize ചെയ്തിരിക്കുന്നു + +```{code-cell} python3 +a = np.array((4, 3, 2, 1)) +a +``` + +```{code-cell} python3 +a.sort() # Sorts a in place +a +``` + +```{code-cell} python3 +a.sum() # Sum +``` + +```{code-cell} python3 +a.mean() # Mean +``` + +```{code-cell} python3 +a.max() # Max +``` + +```{code-cell} python3 +a.argmax() # Returns the index of the maximal element +``` + +```{code-cell} python3 +a.cumsum() # Cumulative sum of the elements of a +``` + +```{code-cell} python3 +a.cumprod() # Cumulative product of the elements of a +``` + +```{code-cell} python3 +a.var() # Variance +``` + +```{code-cell} python3 +a.std() # Standard deviation +``` + +```{code-cell} python3 +a.shape = (2, 2) +a.T # Equivalent to a.transpose() +``` + +അറിഞ്ഞിരിക്കേണ്ട മറ്റൊരു method ആണ് `searchsorted()`. + +`z` ഒരു nondecreasing array ആണെങ്കിൽ, `z.searchsorted(a)`, `z`-ലെ `>= a` ആയ ആദ്യത്തെ element-ന്റെ index return ചെയ്യുന്നു + +```{code-cell} python3 +z = np.linspace(2, 4, 5) +z +``` + +```{code-cell} python3 +z.searchsorted(2.2) +``` + + +## Arithmetic Operations + +```{index} single: NumPy; Arithmetic Operations +``` + +`+`, `-`, `*`, `/`, `**` എന്നീ operators എല്ലാം arrays-ൽ *elementwise* ആയി act ചെയ്യുന്നു + +```{code-cell} python3 +a = np.array([1, 2, 3, 4]) +b = np.array([5, 6, 7, 8]) +a + b +``` + +```{code-cell} python3 +a * b +``` + +താഴെ പറയുന്ന രീതിയിൽ നമുക്ക് ഓരോ element-ഇനും ഒരു scalar add ചെയ്യാം + +```{code-cell} python3 +a + 10 +``` + +Scalar multiplication സമാനമാണ് + +```{code-cell} python3 +a * 10 +``` + +Two-dimensional arrays-ഉം അതേ പൊതു rules തന്നെയാണ് പിന്തുടരുന്നത് + +```{code-cell} python3 +A = np.ones((2, 2)) +B = np.ones((2, 2)) +A + B +``` + +```{code-cell} python3 +A + 10 +``` + +```{code-cell} python3 +A * B +``` + +(numpy_matrix_multiplication)= +In particular, `A * B` എന്നത് matrix product *അല്ല*, ഇത് ഒരു element-wise product ആണ്. + + +## Matrix Multiplication + +```{index} single: NumPy; Matrix Multiplication +``` + +```{index} single: NumPy; Matrix Multiplication +``` + +Matrix multiplication-ന് നമ്മൾ `@` symbol ഉപയോഗിക്കുന്നു, താഴെ കാണുന്ന രീതിയിൽ: + +```{code-cell} python3 +A = np.ones((2, 2)) +B = np.ones((2, 2)) +A @ B +``` + +Flat arrays-ഉം ഈ syntax-ൽ work ചെയ്യും --- നിങ്ങൾക്ക് എന്താണ് വേണ്ടതെന്ന് NumPy ഒരു educated guess നടത്തുന്നു: + +```{code-cell} python3 +A @ (0, 1) +``` + +നമ്മൾ post-multiply ചെയ്യുന്നതിനാൽ, tuple ഒരു column vector ആയാണ് treat ചെയ്യപ്പെടുന്നത്. + + + +(broadcasting)= +## Broadcasting + +```{index} single: NumPy; Broadcasting +``` + +([Jake VanderPlas](https://jakevdp.github.io/PythonDataScienceHandbook/02.05-computation-on-arrays-broadcasting.html) നൽകിയ broadcasting-നെക്കുറിച്ചുള്ള മികച്ച ഒരു ചർച്ചയാണ് ഈ section extend ചെയ്യുന്നത്.) + +```{note} +Broadcasting എന്നത് NumPy-യുടെ വളരെ പ്രധാനപ്പെട്ട ഒരു വശം ആണ്. അതോടൊപ്പം, advanced broadcasting താരതമ്യേന complex ആണ്, താഴെ കൊടുത്തിരിക്കുന്ന ചില details ആദ്യമായി വായിക്കുമ്പോൾ skim ചെയ്യാം. +``` + +Element-wise operations-ൽ, arrays-ന് ഒരേ shape ഉണ്ടായിരിക്കണമെന്നില്ല. + +ഇത് സംഭവിക്കുമ്പോൾ, കഴിയുന്നിടത്തോളം NumPy automatically arrays-നെ ഒരേ shape-ലേക്ക് expand ചെയ്യും. + +NumPy-യിലെ ഈ useful ആയ (എന്നാൽ ചിലപ്പോൾ confusing ആയ) feature-നെ **broadcasting** എന്ന് വിളിക്കുന്നു. + +Broadcasting-ന്റെ value എന്തെന്നാൽ + +* `for` loops ഒഴിവാക്കാം, ഇത് numerical code വേഗത്തിൽ run ചെയ്യാൻ സഹായിക്കുന്നു, ഒപ്പം +* Arrays-ന്റെ ചില dimensions memory-യിൽ actually create ചെയ്യാതെ തന്നെ, ഈ arrays-ൽ operations implement ചെയ്യാൻ broadcasting നമ്മെ അനുവദിക്കുന്നു, arrays വലുതായിരിക്കുമ്പോൾ ഇത് പ്രധാനമാകാം. + +ഉദാഹരണത്തിന്, `a` ഒരു $3 \times 3$ array ആണെന്ന് കരുതുക (`a -> (3, 3)`), `b` മൂന്ന് elements ഉള്ള ഒരു flat array ആണെന്നും (`b -> (3,)`). + +ഇവയെ ഒരുമിച്ച് add ചെയ്യുമ്പോൾ, NumPy automatically `b -> (3,)`-നെ `b -> (3, 3)` ആയി expand ചെയ്യും. + +Element-wise addition-ന്റെ ഫലം ഒരു $3 \times 3$ array ആയിരിക്കും + +```{code-cell} python3 + +a = np.array( + [[1, 2, 3], + [4, 5, 6], + [7, 8, 9]]) +b = np.array([3, 6, 9]) + +a + b +``` + +ഈ broadcasting operation-ന്റെ ഒരു visual representation താഴെ കാണാം: + +```{code-cell} python3 +--- +tags: [hide-input] +--- +# Adapted and modified based on the code in the book written by Jake VanderPlas (see https://jakevdp.github.io/PythonDataScienceHandbook/06.00-figure-code.html#Broadcasting) +# Originally from astroML: see https://www.astroml.org/book_figures/appendix/fig_broadcast_visual.html + + +def draw_cube(ax, xy, size, depth=0.4, + edges=None, label=None, label_kwargs=None, **kwargs): + """draw and label a cube. edges is a list of numbers between + 1 and 12, specifying which of the 12 cube edges to draw""" + if edges is None: + edges = range(1, 13) + + x, y = xy + + if 1 in edges: + ax.plot([x, x + size], + [y + size, y + size], **kwargs) + if 2 in edges: + ax.plot([x + size, x + size], + [y, y + size], **kwargs) + if 3 in edges: + ax.plot([x, x + size], + [y, y], **kwargs) + if 4 in edges: + ax.plot([x, x], + [y, y + size], **kwargs) + + if 5 in edges: + ax.plot([x, x + depth], + [y + size, y + depth + size], **kwargs) + if 6 in edges: + ax.plot([x + size, x + size + depth], + [y + size, y + depth + size], **kwargs) + if 7 in edges: + ax.plot([x + size, x + size + depth], + [y, y + depth], **kwargs) + if 8 in edges: + ax.plot([x, x + depth], + [y, y + depth], **kwargs) + + if 9 in edges: + ax.plot([x + depth, x + depth + size], + [y + depth + size, y + depth + size], **kwargs) + if 10 in edges: + ax.plot([x + depth + size, x + depth + size], + [y + depth, y + depth + size], **kwargs) + if 11 in edges: + ax.plot([x + depth, x + depth + size], + [y + depth, y + depth], **kwargs) + if 12 in edges: + ax.plot([x + depth, x + depth], + [y + depth, y + depth + size], **kwargs) + + if label: + if label_kwargs is None: + label_kwargs = {} + ax.text(x + 0.5 * size, y + 0.5 * size, label, + ha='center', va='center', **label_kwargs) + +solid = dict(c='black', ls='-', lw=1, + label_kwargs=dict(color='k')) +dotted = dict(c='black', ls='-', lw=0.5, alpha=0.5, + label_kwargs=dict(color='gray')) +depth = 0.3 + +# Draw a figure and axis with no boundary +fig = plt.figure(figsize=(5, 1), facecolor='w') +ax = plt.axes([0, 0, 1, 1], xticks=[], yticks=[], frameon=False) + +# first block +draw_cube(ax, (1, 7.5), 1, depth, [1, 2, 3, 4, 5, 6, 9], '1', **solid) +draw_cube(ax, (2, 7.5), 1, depth, [1, 2, 3, 6, 9], '2', **solid) +draw_cube(ax, (3, 7.5), 1, depth, [1, 2, 3, 6, 7, 9, 10], '3', **solid) + +draw_cube(ax, (1, 6.5), 1, depth, [2, 3, 4], '4', **solid) +draw_cube(ax, (2, 6.5), 1, depth, [2, 3], '5', **solid) +draw_cube(ax, (3, 6.5), 1, depth, [2, 3, 7, 10], '6', **solid) + +draw_cube(ax, (1, 5.5), 1, depth, [2, 3, 4], '7', **solid) +draw_cube(ax, (2, 5.5), 1, depth, [2, 3], '8', **solid) +draw_cube(ax, (3, 5.5), 1, depth, [2, 3, 7, 10], '9', **solid) + +# second block +draw_cube(ax, (6, 7.5), 1, depth, [1, 2, 3, 4, 5, 6, 9], '3', **solid) +draw_cube(ax, (7, 7.5), 1, depth, [1, 2, 3, 6, 9], '6', **solid) +draw_cube(ax, (8, 7.5), 1, depth, [1, 2, 3, 6, 7, 9, 10], '9', **solid) + +draw_cube(ax, (6, 6.5), 1, depth, range(2, 13), '3', **dotted) +draw_cube(ax, (7, 6.5), 1, depth, [2, 3, 6, 7, 9, 10, 11], '6', **dotted) +draw_cube(ax, (8, 6.5), 1, depth, [2, 3, 6, 7, 9, 10, 11], '9', **dotted) + +draw_cube(ax, (6, 5.5), 1, depth, [2, 3, 4, 7, 8, 10, 11, 12], '3', **dotted) +draw_cube(ax, (7, 5.5), 1, depth, [2, 3, 7, 10, 11], '6', **dotted) +draw_cube(ax, (8, 5.5), 1, depth, [2, 3, 7, 10, 11], '9', **dotted) + +# third block +draw_cube(ax, (12, 7.5), 1, depth, [1, 2, 3, 4, 5, 6, 9], '4', **solid) +draw_cube(ax, (13, 7.5), 1, depth, [1, 2, 3, 6, 9], '8', **solid) +draw_cube(ax, (14, 7.5), 1, depth, [1, 2, 3, 6, 7, 9, 10], '12', **solid) + +draw_cube(ax, (12, 6.5), 1, depth, [2, 3, 4], '7', **solid) +draw_cube(ax, (13, 6.5), 1, depth, [2, 3], '11', **solid) +draw_cube(ax, (14, 6.5), 1, depth, [2, 3, 7, 10], '15', **solid) + +draw_cube(ax, (12, 5.5), 1, depth, [2, 3, 4], '10', **solid) +draw_cube(ax, (13, 5.5), 1, depth, [2, 3], '14', **solid) +draw_cube(ax, (14, 5.5), 1, depth, [2, 3, 7, 10], '18', **solid) + +ax.text(5, 7.0, '+', size=12, ha='center', va='center') +ax.text(10.5, 7.0, '=', size=12, ha='center', va='center'); +``` + +`b -> (3, 1)` ആണെങ്കിലോ? + +ഈ case-ൽ, NumPy automatically `b -> (3, 1)`-നെ `b -> (3, 3)` ആയി expand ചെയ്യും. + +Element-wise addition-ന്റെ ഫലം അപ്പോൾ ഒരു $3 \times 3$ matrix ആയിരിക്കും + +```{code-cell} python3 +b.shape = (3, 1) + +a + b +``` + +ഈ broadcasting operation-ന്റെ ഒരു visual representation താഴെ കാണാം: + +```{code-cell} python3 +--- +tags: [hide-input] +--- + +fig = plt.figure(figsize=(5, 1), facecolor='w') +ax = plt.axes([0, 0, 1, 1], xticks=[], yticks=[], frameon=False) + +# first block +draw_cube(ax, (1, 7.5), 1, depth, [1, 2, 3, 4, 5, 6, 9], '1', **solid) +draw_cube(ax, (2, 7.5), 1, depth, [1, 2, 3, 6, 9], '2', **solid) +draw_cube(ax, (3, 7.5), 1, depth, [1, 2, 3, 6, 7, 9, 10], '3', **solid) + +draw_cube(ax, (1, 6.5), 1, depth, [2, 3, 4], '4', **solid) +draw_cube(ax, (2, 6.5), 1, depth, [2, 3], '5', **solid) +draw_cube(ax, (3, 6.5), 1, depth, [2, 3, 7, 10], '6', **solid) + +draw_cube(ax, (1, 5.5), 1, depth, [2, 3, 4], '7', **solid) +draw_cube(ax, (2, 5.5), 1, depth, [2, 3], '8', **solid) +draw_cube(ax, (3, 5.5), 1, depth, [2, 3, 7, 10], '9', **solid) + +# second block +draw_cube(ax, (6, 7.5), 1, depth, [1, 2, 3, 4, 5, 6, 7, 9, 10], '3', **solid) +draw_cube(ax, (7, 7.5), 1, depth, [1, 2, 3, 6, 7, 9, 10], '3', **dotted) +draw_cube(ax, (8, 7.5), 1, depth, [1, 2, 3, 6, 7, 9, 10], '3', **dotted) + +draw_cube(ax, (6, 6.5), 1, depth, [2, 3, 4, 7, 10], '6', **solid) +draw_cube(ax, (7, 6.5), 1, depth, [2, 3, 6, 7, 9, 10, 11], '6', **dotted) +draw_cube(ax, (8, 6.5), 1, depth, [2, 3, 6, 7, 9, 10, 11], '6', **dotted) + +draw_cube(ax, (6, 5.5), 1, depth, [2, 3, 4, 7, 10], '9', **solid) +draw_cube(ax, (7, 5.5), 1, depth, [2, 3, 7, 10, 11], '9', **dotted) +draw_cube(ax, (8, 5.5), 1, depth, [2, 3, 7, 10, 11], '9', **dotted) + +# third block +draw_cube(ax, (12, 7.5), 1, depth, [1, 2, 3, 4, 5, 6, 9], '4', **solid) +draw_cube(ax, (13, 7.5), 1, depth, [1, 2, 3, 6, 9], '5', **solid) +draw_cube(ax, (14, 7.5), 1, depth, [1, 2, 3, 6, 7, 9, 10], '6', **solid) + +draw_cube(ax, (12, 6.5), 1, depth, [2, 3, 4], '10', **solid) +draw_cube(ax, (13, 6.5), 1, depth, [2, 3], '11', **solid) +draw_cube(ax, (14, 6.5), 1, depth, [2, 3, 7, 10], '12', **solid) + +draw_cube(ax, (12, 5.5), 1, depth, [2, 3, 4], '16', **solid) +draw_cube(ax, (13, 5.5), 1, depth, [2, 3], '17', **solid) +draw_cube(ax, (14, 5.5), 1, depth, [2, 3, 7, 10], '18', **solid) + +ax.text(5, 7.0, '+', size=12, ha='center', va='center') +ax.text(10.5, 7.0, '=', size=12, ha='center', va='center'); + + +``` + +ചില cases-ൽ, രണ്ട് operands-ഉം expand ചെയ്യപ്പെടും. + +`a -> (3,)`-ഉം `b -> (3, 1)`-ഉം ഉള്ളപ്പോൾ, `a`, `a -> (3, 3)` ആയി expand ചെയ്യപ്പെടും, `b`, `b -> (3, 3)` ആയും expand ചെയ്യപ്പെടും. + +ഈ case-ൽ, element-wise addition-ന്റെ ഫലം ഒരു $3 \times 3$ matrix ആയിരിക്കും + +```{code-cell} python3 +a = np.array([3, 6, 9]) +b = np.array([2, 3, 4]) +b.shape = (3, 1) + +a + b +``` + +ഈ broadcasting operation-ന്റെ ഒരു visual representation താഴെ കാണാം: + +```{code-cell} python3 +--- +tags: [hide-input] +--- + +# Draw a figure and axis with no boundary +fig = plt.figure(figsize=(5, 1), facecolor='w') +ax = plt.axes([0, 0, 1, 1], xticks=[], yticks=[], frameon=False) + +# first block +draw_cube(ax, (1, 7.5), 1, depth, [1, 2, 3, 4, 5, 6, 9], '3', **solid) +draw_cube(ax, (2, 7.5), 1, depth, [1, 2, 3, 6, 9], '6', **solid) +draw_cube(ax, (3, 7.5), 1, depth, [1, 2, 3, 6, 7, 9, 10], '9', **solid) + +draw_cube(ax, (1, 6.5), 1, depth, range(2, 13), '3', **dotted) +draw_cube(ax, (2, 6.5), 1, depth, [2, 3, 6, 7, 9, 10, 11], '6', **dotted) +draw_cube(ax, (3, 6.5), 1, depth, [2, 3, 6, 7, 9, 10, 11], '9', **dotted) + +draw_cube(ax, (1, 5.5), 1, depth, [2, 3, 4, 7, 8, 10, 11, 12], '3', **dotted) +draw_cube(ax, (2, 5.5), 1, depth, [2, 3, 7, 10, 11], '6', **dotted) +draw_cube(ax, (3, 5.5), 1, depth, [2, 3, 7, 10, 11], '9', **dotted) + +# second block +draw_cube(ax, (6, 7.5), 1, depth, [1, 2, 3, 4, 5, 6, 7, 9, 10], '2', **solid) +draw_cube(ax, (7, 7.5), 1, depth, [1, 2, 3, 6, 7, 9, 10], '2', **dotted) +draw_cube(ax, (8, 7.5), 1, depth, [1, 2, 3, 6, 7, 9, 10], '2', **dotted) + +draw_cube(ax, (6, 6.5), 1, depth, [2, 3, 4, 7, 10], '3', **solid) +draw_cube(ax, (7, 6.5), 1, depth, [2, 3, 6, 7, 9, 10, 11], '3', **dotted) +draw_cube(ax, (8, 6.5), 1, depth, [2, 3, 6, 7, 9, 10, 11], '3', **dotted) + +draw_cube(ax, (6, 5.5), 1, depth, [2, 3, 4, 7, 10], '4', **solid) +draw_cube(ax, (7, 5.5), 1, depth, [2, 3, 7, 10, 11], '4', **dotted) +draw_cube(ax, (8, 5.5), 1, depth, [2, 3, 7, 10, 11], '4', **dotted) + +# third block +draw_cube(ax, (12, 7.5), 1, depth, [1, 2, 3, 4, 5, 6, 9], '5', **solid) +draw_cube(ax, (13, 7.5), 1, depth, [1, 2, 3, 6, 9], '8', **solid) +draw_cube(ax, (14, 7.5), 1, depth, [1, 2, 3, 6, 7, 9, 10], '11', **solid) + +draw_cube(ax, (12, 6.5), 1, depth, [2, 3, 4], '6', **solid) +draw_cube(ax, (13, 6.5), 1, depth, [2, 3], '9', **solid) +draw_cube(ax, (14, 6.5), 1, depth, [2, 3, 7, 10], '12', **solid) + +draw_cube(ax, (12, 5.5), 1, depth, [2, 3, 4], '7', **solid) +draw_cube(ax, (13, 5.5), 1, depth, [2, 3], '10', **solid) +draw_cube(ax, (14, 5.5), 1, depth, [2, 3, 7, 10], '13', **solid) + +ax.text(5, 7.0, '+', size=12, ha='center', va='center') +ax.text(10.5, 7.0, '=', size=12, ha='center', va='center'); +``` + +Broadcasting വളരെ useful ആണെങ്കിലും, ചിലപ്പോൾ confusing ആയി തോന്നാം. + +ഉദാഹരണത്തിന്, `a -> (3, 2)`-ഉം `b -> (3,)`-ഉം add ചെയ്യാൻ ശ്രമിക്കാം. + +```{code-cell} python3 +--- +tags: [raises-exception] +--- +a = np.array( + [[1, 2], + [4, 5], + [7, 8]]) +b = np.array([3, 6, 9]) + +a + b +``` + +Operands-നെ ഒരുമിച്ച് broadcast ചെയ്യാൻ കഴിഞ്ഞില്ല എന്ന് `ValueError` നമ്മോട് പറയുന്നു. + + +ഈ broadcasting എന്തുകൊണ്ട് execute ചെയ്യാൻ കഴിയില്ല എന്ന് കാണിക്കുന്ന ഒരു visual representation താഴെ കാണാം: + +```{code-cell} python3 +--- +tags: [hide-input] +--- +# Draw a figure and axis with no boundary +fig = plt.figure(figsize=(3, 1.3), facecolor='w') +ax = plt.axes([0, 0, 1, 1], xticks=[], yticks=[], frameon=False) + +# first block +draw_cube(ax, (1, 7.5), 1, depth, [1, 2, 3, 4, 5, 6, 9], '1', **solid) +draw_cube(ax, (2, 7.5), 1, depth, [1, 2, 3, 6, 7, 9, 10], '2', **solid) + +draw_cube(ax, (1, 6.5), 1, depth, [2, 3, 4], '4', **solid) +draw_cube(ax, (2, 6.5), 1, depth, [2, 3, 7, 10], '5', **solid) + +draw_cube(ax, (1, 5.5), 1, depth, [2, 3, 4], '7', **solid) +draw_cube(ax, (2, 5.5), 1, depth, [2, 3, 7, 10], '8', **solid) + +# second block +draw_cube(ax, (6, 7.5), 1, depth, [1, 2, 3, 4, 5, 6, 9], '3', **solid) +draw_cube(ax, (7, 7.5), 1, depth, [1, 2, 3, 6, 9], '6', **solid) +draw_cube(ax, (8, 7.5), 1, depth, [1, 2, 3, 6, 7, 9, 10], '9', **solid) + +draw_cube(ax, (6, 6.5), 1, depth, range(2, 13), '3', **dotted) +draw_cube(ax, (7, 6.5), 1, depth, [2, 3, 6, 7, 9, 10, 11], '6', **dotted) +draw_cube(ax, (8, 6.5), 1, depth, [2, 3, 6, 7, 9, 10, 11], '9', **dotted) + +draw_cube(ax, (6, 5.5), 1, depth, [2, 3, 4, 7, 8, 10, 11, 12], '3', **dotted) +draw_cube(ax, (7, 5.5), 1, depth, [2, 3, 7, 10, 11], '6', **dotted) +draw_cube(ax, (8, 5.5), 1, depth, [2, 3, 7, 10, 11], '9', **dotted) + + +ax.text(4.5, 7.0, '+', size=12, ha='center', va='center') +ax.text(10, 7.0, '=', size=12, ha='center', va='center') +ax.text(11, 7.0, '?', size=16, ha='center', va='center'); +``` + +NumPy-ക്ക് arrays-നെ ഒരേ size-ലേക്ക് expand ചെയ്യാൻ കഴിയില്ല എന്ന് നമുക്ക് കാണാം. + +കാരണം, `b`, `b -> (3,)`-ൽ നിന്നും `b -> (3, 3)` ആയി expand ചെയ്യപ്പെടുമ്പോൾ, NumPy-ക്ക് `b`-നെ `a -> (3, 2)`-ഉമായി match ചെയ്യാൻ കഴിയില്ല. + +Higher dimensions-ലേക്ക് നീങ്ങുമ്പോൾ കാര്യങ്ങൾ കൂടുതൽ tricky ആകുന്നു. + +നമ്മെ സഹായിക്കാൻ, താഴെ പറയുന്ന rules-ന്റെ ഒരു list ഉപയോഗിക്കാം: + +* *Step 1:* രണ്ട് arrays-ന്റെയും dimensions match ചെയ്യാത്തപ്പോൾ, NumPy, existing dimensions-ന്റെ ഇടത് വശത്ത് dimension(s) ചേർത്ത്, dimensions കുറവുള്ളതിനെ expand ചെയ്യും. + - ഉദാഹരണത്തിന്, `a -> (3, 3)`-ഉം `b -> (3,)`-ഉം ആണെങ്കിൽ, broadcasting ഇടത് വശത്ത് ഒരു dimension ചേർക്കും, അതിനാൽ `b -> (1, 3)` ആകും; + - `a -> (2, 2, 2)`-ഉം `b -> (2, 2)`-ഉം ആണെങ്കിൽ, broadcasting ഇടത് വശത്ത് ഒരു dimension ചേർക്കും, അതിനാൽ `b -> (1, 2, 2)` ആകും; + - `a -> (3, 2, 2)`-ഉം `b -> (2,)`-ഉം ആണെങ്കിൽ, broadcasting ഇടത് വശത്ത് രണ്ട് dimensions ചേർക്കും, അതിനാൽ `b -> (1, 1, 2)` ആകും (ഈ process-നെ *Step 1* രണ്ട് തവണ കടക്കുന്നത് ആയും കാണാം). + + +* *Step 2:* രണ്ട് arrays-ക്കും ഒരേ dimension ഉണ്ടെങ്കിലും shapes വ്യത്യസ്തമാണെങ്കിൽ, shape index 1 ആയ dimensions-നെ NumPy expand ചെയ്യാൻ ശ്രമിക്കും. + - ഉദാഹരണത്തിന്, `a -> (1, 3)`-ഉം `b -> (3, 1)`-ഉം ആണെങ്കിൽ, broadcasting, `a`-യിലും `b`-യിലും shape 1 ഉള്ള dimensions expand ചെയ്യും, അതിനാൽ `a -> (3, 3)`-ഉം `b -> (3, 3)`-ഉം ആകും; + - `a -> (2, 2, 2)`-ഉം `b -> (1, 2, 2)`-ഉം ആണെങ്കിൽ, broadcasting, `b`-യുടെ ആദ്യത്തെ dimension expand ചെയ്യും, അതിനാൽ `b -> (2, 2, 2)` ആകും; + - `a -> (3, 2, 2)`-ഉം `b -> (1, 1, 2)`-ഉം ആണെങ്കിൽ, broadcasting, shape 1 ഉള്ള എല്ലാ dimensions-ഇലും `b`-യെ expand ചെയ്യും, അതിനാൽ `b -> (3, 2, 2)` ആകും. + +* *Step 3:* Step 1-ഉം 2-ഉം കഴിഞ്ഞിട്ടും, രണ്ട് arrays-ഉം match ചെയ്യുന്നില്ലെങ്കിൽ, ഒരു `ValueError` raise ചെയ്യപ്പെടും. ഉദാഹരണത്തിന്, `a -> (2, 2, 3)`-ഉം `b -> (2, 2)`-ഉം ആണെന്ന് കരുതുക + - *Step 1* പ്രകാരം, `b`, `b -> (1, 2, 2)` ആയി expand ചെയ്യപ്പെടും; + - *Step 2* പ്രകാരം, `b`, `b -> (2, 2, 2)` ആയി expand ചെയ്യപ്പെടും; + - ആദ്യത്തെ രണ്ട് steps-ന് ശേഷവും അവ പരസ്പരം match ചെയ്യുന്നില്ല എന്ന് നമുക്ക് കാണാം. അതിനാൽ, ഒരു `ValueError` raise ചെയ്യപ്പെടും + + + +## Mutability and Copying Arrays + +NumPy arrays, Python lists-നെപ്പോലെ mutable ആയ data types ആണ്. + +അതായത്, initialization-ന് ശേഷം memory-യിൽ അവയുടെ contents alter ചെയ്യാൻ (mutate ചെയ്യാൻ) കഴിയും. + +ഇത് convenient ആണ്, പക്ഷേ Python-ന്റെ naming, reference model എന്നിവയുമായി combine ചെയ്യുമ്പോൾ, NumPy beginners-ന് ഇത് mistakes-ലേക്ക് നയിച്ചേക്കാം. + +ഈ section-ൽ നമുക്ക് ചില പ്രധാന issues review ചെയ്യാം. + + +### Mutability + +Mutability-യുടെ examples നമ്മൾ മുകളിൽ already കണ്ടു. + +NumPy array-യുടെ mutation-ന്റെ മറ്റൊരു example താഴെ കാണാം + +```{code-cell} python3 +a = np.array([42, 44]) +a +``` + +```{code-cell} python3 +a[-1] = 0 # Change last element to 0 +a +``` + +Mutability, താഴെ പറയുന്ന behavior-ലേക്ക് നയിക്കുന്നു (ഇത് MATLAB programmers-ന് shocking ആയേക്കാം...) + +```{code-cell} python3 +rng = np.random.default_rng() +a = rng.standard_normal(3) +a +``` + +```{code-cell} python3 +b = a +b[0] = 0.0 +a +``` + +സംഭവിച്ചത് എന്തെന്നാൽ, `b` മാറ്റുന്നതിലൂടെ നമ്മൾ `a`-യെ മാറ്റിയിരിക്കുന്നു. + +`b` എന്ന name, `a`-യുമായി bind ചെയ്യപ്പെട്ടിരിക്കുന്നു, അത് ആ array-യുടെ മറ്റൊരു reference മാത്രമായി മാറുന്നു (Python assignment model {doc}`കോഴ്സിന്റെ പിന്നീടുള്ള ഭാഗത്ത് ` കൂടുതൽ വിശദമായി describe ചെയ്തിട്ടുണ്ട്). + +അതിനാൽ, ആ array-യിൽ changes വരുത്താൻ ഇതിന് equal rights ഉണ്ട്. + +ഇത് വാസ്തവത്തിൽ ഏറ്റവും sensible ആയ default behavior ആണ്! + +ഇതിനർത്ഥം, copies ഉണ്ടാക്കുന്നതിന് പകരം, data-യിലേക്കുള്ള pointers മാത്രമേ നമ്മൾ pass ചെയ്യുന്നുള്ളൂ എന്നാണ്. + +Copies ഉണ്ടാക്കുന്നത് speed-ന്റെയും memory-യുടെയും കാര്യത്തിൽ expensive ആണ്. + +### Making Copies + +ആവശ്യമുള്ളപ്പോൾ `b`-യെ `a`-യുടെ ഒരു independent copy ആക്കാൻ തീർച്ചയായും കഴിയും. + +ഇത് `np.copy` ഉപയോഗിച്ച് ചെയ്യാം + +```{code-cell} python3 +a = rng.standard_normal(3) +a +``` + +```{code-cell} python3 +b = np.copy(a) +b +``` + +ഇനി `b` ഒരു independent copy ആണ് (*deep copy* എന്ന് വിളിക്കപ്പെടുന്നു) + +```{code-cell} python3 +b[:] = 1 +b +``` + +```{code-cell} python3 +a +``` + +`b`-യിലെ change, `a`-യെ affect ചെയ്തിട്ടില്ല എന്നത് ശ്രദ്ധിക്കുക. + + + + +## Additional Features + +NumPy-യുടെ മറ്റ് ചില useful ആയ features നമുക്ക് നോക്കാം. + + +### Universal Functions + +```{index} single: NumPy; Vectorized Functions +``` + +`log`, `exp`, `sin` തുടങ്ങിയ standard functions-ന്റെ versions, arrays-ൽ *element-wise* ആയി act ചെയ്യുന്നവ NumPy നൽകുന്നു + +```{code-cell} python3 +z = np.array([1, 2, 3]) +np.sin(z) +``` + +ഇത്, താഴെ പറയുന്നത് പോലുള്ള explicit element-by-element loops-ന്റെ ആവശ്യം ഇല്ലാതാക്കുന്നു + +```{code-cell} python3 +n = len(z) +y = np.empty(n) +for i in range(n): + y[i] = np.sin(z[i]) +``` + +Arrays-ൽ element-wise ആയി act ചെയ്യുന്നതിനാൽ, ഈ functions-നെ ചിലപ്പോൾ **vectorized functions** എന്ന് വിളിക്കുന്നു. + +NumPy-speak-ൽ, ഇവയെ **ufuncs**, അല്ലെങ്കിൽ **universal functions** എന്നും വിളിക്കുന്നു. + +മുകളിൽ നമ്മൾ കണ്ടത് പോലെ, സാധാരണ arithmetic operations-ഉം (`+`, `*`, etc.) element-wise ആയി work ചെയ്യുന്നു, ഇവയെ ufuncs-ഉമായി combine ചെയ്യുമ്പോൾ വളരെ വലിയൊരു set of fast element-wise functions ലഭിക്കുന്നു. + +```{code-cell} python3 +z +``` + +```{code-cell} python3 +(1 / np.sqrt(2 * np.pi)) * np.exp(- 0.5 * z**2) +``` + +എല്ലാ user-defined functions-ഉം element-wise ആയി act ചെയ്യില്ല. + +ഉദാഹരണത്തിന്, താഴെ define ചെയ്തിരിക്കുന്ന `f` എന്ന function-ന് ഒരു NumPy array pass ചെയ്യുന്നത് ഒരു `ValueError`-ന് കാരണമാകുന്നു + +```{code-cell} python3 +def f(x): + return 1 if x > 0 else 0 +``` + +NumPy function `np.where`, ഒരു vectorized alternative നൽകുന്നു: + +```{code-cell} python3 +x = rng.standard_normal(4) +x +``` + +```{code-cell} python3 +np.where(x > 0, 1, 0) # Insert 1 if x > 0 true, otherwise 0 +``` + +തന്നിരിക്കുന്ന ഒരു function vectorize ചെയ്യാൻ നിങ്ങൾക്ക് `np.vectorize`-ഉം ഉപയോഗിക്കാം + +```{code-cell} python3 +f = np.vectorize(f) +f(x) # Passing the same vector x as in the previous example +``` + +എന്നിരുന്നാലും, ഈ approach-ന്, കൂടുതൽ carefully crafted ആയ ഒരു vectorized function-ന്റെ speed എപ്പോഴും ലഭിക്കണമെന്നില്ല. + +(പിന്നീട് നമുക്ക് കാണാം, JAX-ന്, `np.vectorize`-ന്റെ ഒരു powerful version ഉണ്ട്, ഇത് highly efficient code generate ചെയ്യാൻ കഴിയുന്നതും, മിക്ക സമയത്തും ചെയ്യുന്നതും ആണ്.) + + +### Comparisons + +```{index} single: NumPy; Comparisons +``` + +സാധാരണയായി, arrays-ലെ comparisons element-wise ആയാണ് ചെയ്യപ്പെടുന്നത് + +```{code-cell} python3 +z = np.array([2, 3]) +y = np.array([2, 3]) +z == y +``` + +```{code-cell} python3 +y[0] = 5 +z == y +``` + +```{code-cell} python3 +z != y +``` + +`>`, `<`, `>=`, `<=` എന്നിവക്കും സ്ഥിതി സമാനമാണ്. + +Scalars-ഇനുമെതിരെയും നമുക്ക് comparisons ചെയ്യാം + +```{code-cell} python3 +z = np.linspace(0, 10, 5) +z +``` + +```{code-cell} python3 +z > 3 +``` + +*Conditional extraction*-ന് ഇത് പ്രത്യേകിച്ചും useful ആണ് + +```{code-cell} python3 +b = z > 3 +b +``` + +```{code-cell} python3 +z[b] +``` + +തീർച്ചയായും നമുക്ക് ഇത് ഒറ്റ step-ൽ perform ചെയ്യാം---മിക്ക സമയത്തും നമ്മൾ അങ്ങനെയാണ് ചെയ്യാറ് + +```{code-cell} python3 +z[z > 3] +``` + +### Sub-packages + +Sub-packages വഴി, scientific programming-മായി ബന്ധപ്പെട്ട ചില additional functionality NumPy നൽകുന്നു. + +NumPy-യുടെ [random `Generator`](https://numpy.org/doc/stable/reference/random/generator.html#random-generator) ഉപയോഗിച്ച് random variables generate ചെയ്യുന്നത് നമ്മൾ already കണ്ടിട്ടുണ്ട്. + +```{code-cell} python3 +z = rng.standard_normal(10000) # Generate standard normals +y = rng.binomial(10, 0.5, size=1000) # 1,000 draws from Bin(10, 0.5) +y.mean() +``` + +സാധാരണയായി ഉപയോഗിക്കപ്പെടുന്ന മറ്റൊരു subpackage ആണ് np.linalg + +```{code-cell} python3 +A = np.array([[1, 2], [3, 4]]) + +np.linalg.det(A) # Compute the determinant +``` + +```{code-cell} python3 +np.linalg.inv(A) # Compute the inverse +``` + +```{index} single: SciPy +``` + +```{index} single: Python; SciPy +``` + +ഈ functionality-യുടെ ഭൂരിഭാഗവും, NumPy-യുടെ മുകളിൽ build ചെയ്തിരിക്കുന്ന modules-ന്റെ ഒരു collection ആയ [SciPy](https://scipy.org/)-യിലും ലഭ്യമാണ്. + +SciPy versions {doc}`ഉടൻതന്നെ ` നമുക്ക് കൂടുതൽ വിശദമായി cover ചെയ്യാം. + +NumPy-യിൽ ലഭ്യമായതിന്റെ ഒരു comprehensive list-ന്, [this documentation](https://numpy.org/doc/stable/reference/routines.html) കാണുക. + + +### Implicit Multithreading + +[മുമ്പ്](need_for_speed) multithreading വഴിയുള്ള parallelization-ന്റെ concept നമ്മൾ discuss ചെയ്തിരുന്നു. + +NumPy, അതിന്റെ compiled code-യുടെ ഭൂരിഭാഗത്തിലും multithreading implement ചെയ്യാൻ ശ്രമിക്കുന്നു. + +ഇത് action-ൽ കാണാൻ നമുക്ക് ഒരു example നോക്കാം. + +താഴെ കൊടുത്തിരിക്കുന്ന code, randomly generate ചെയ്ത ധാരാളം matrices-ന്റെ eigenvalues compute ചെയ്യുന്നു. + +ഇത് run ചെയ്യാൻ കുറച്ച് seconds എടുക്കും. + +```{code-cell} python3 +n = 20 +m = 1000 +for i in range(n): + X = rng.standard_normal((m, m)) + λ = np.linalg.eigvals(X) +``` + +ഇനി, ഈ code run ചെയ്യുമ്പോൾ നമ്മുടെ machine-ലെ htop system monitor-ന്റെ output നമുക്ക് നോക്കാം: + +```{figure} /_static/lecture_specific/parallelization/htop_parallel_npmat.png +:scale: 80 +``` + +8 CPUs-ൽ 4 എണ്ണം full speed-ൽ run ചെയ്യുന്നു എന്ന് നമുക്ക് കാണാം. + +ഇത് സംഭവിക്കുന്നത്, NumPy-യുടെ `eigvals` routine, tasks neat ആയി split ചെയ്ത് വ്യത്യസ്ത threads-ഇലേക്ക് distribute ചെയ്യുന്നത് കൊണ്ടാണ്. + + + + + +## Exercises + + +```{exercise-start} +:label: np_ex1 +``` + +Consider the polynomial expression + +```{math} +:label: np_polynom + +p(x) = a_0 + a_1 x + a_2 x^2 + \cdots a_N x^N = \sum_{n=0}^N a_n x^n +``` + +{ref}`Earlier `, you wrote a simple function `p(x, coeff)` to evaluate {eq}`np_polynom` without considering efficiency. + +Now write a new function that does the same job, but uses NumPy arrays and array operations for its computations, rather than any form of Python loop. + +(Such functionality is already implemented as `np.poly1d`, but for the sake of the exercise don't use this class) + +```{hint} +:class: dropdown +Use `np.cumprod()` +``` +```{exercise-end} +``` + +```{solution-start} np_ex1 +:class: dropdown +``` + +This code does the job + +```{code-cell} python3 +def p(x, coef): + X = np.ones_like(coef) + X[1:] = x + y = np.cumprod(X) # y = [1, x, x**2,...] + return coef @ y +``` + +Let's test it + +```{code-cell} python3 +x = 2 +coef = np.linspace(2, 4, 3) +print(coef) +print(p(x, coef)) +# For comparison +q = np.poly1d(np.flip(coef)) +print(q(x)) +``` + +```{solution-end} +``` + + +```{exercise-start} +:label: np_ex2 +``` + +Let `q` be a NumPy array of length `n` with `q.sum() == 1`. + +Suppose that `q` represents a [probability mass function](https://en.wikipedia.org/wiki/Probability_mass_function). + +We wish to generate a discrete random variable $x$ such that $\mathbb P\{x = i\} = q_i$. + +In other words, `x` takes values in `range(len(q))` and `x = i` with probability `q[i]`. + +The standard (inverse transform) algorithm is as follows: + +* Divide the unit interval $[0, 1]$ into $n$ subintervals $I_0, I_1, \ldots, I_{n-1}$ such that the length of $I_i$ is $q_i$. +* Draw a uniform random variable $U$ on $[0, 1]$ and return the $i$ such that $U \in I_i$. + +The probability of drawing $i$ is the length of $I_i$, which is equal to $q_i$. + +We can implement the algorithm as follows + +```{code-cell} python3 +from random import uniform + +def sample(q): + a = 0.0 + U = uniform(0, 1) + for i in range(len(q)): + if a < U <= a + q[i]: + return i + a = a + q[i] +``` + +If you can't see how this works, try thinking through the flow for a simple example, such as `q = [0.25, 0.75]` +It helps to sketch the intervals on paper. + +Your exercise is to speed it up using NumPy, avoiding explicit loops + +```{hint} +:class: dropdown + +Use `np.searchsorted` and `np.cumsum` + +``` + +If you can, implement the functionality as a class called `DiscreteRV`, where + +* the data for an instance of the class is the vector of probabilities `q` +* the class has a `draw()` method, which returns one draw according to the algorithm described above + +If you can, write the method so that `draw(k)` returns `k` draws from `q`. + +```{exercise-end} +``` + +```{solution-start} np_ex2 +:class: dropdown +``` + +Here's our first pass at a solution: + +```{code-cell} python3 +from numpy import cumsum + +class DiscreteRV: + """ + Generates an array of draws from a discrete random variable with vector of + probabilities given by q. + """ + + def __init__(self, q, seed=None): + """ + The argument q is a NumPy array, or array like, nonnegative and sums + to 1. + + The argument seed sets the seed for the underlying random number + generator; with the default seed=None, draws are not reproducible + across runs. + """ + self.q = q + self.Q = cumsum(q) + self.rng = np.random.default_rng(seed) + + def draw(self, k=1): + """ + Returns k draws from q. For each such draw, the value i is returned + with probability q[i]. + """ + return self.Q.searchsorted(self.rng.uniform(0, 1, size=k)) +``` + +The logic is not obvious, but if you take your time and read it slowly, +you will understand. + +There is a problem here, however. + +Suppose that `q` is altered after an instance of `DiscreteRV` is +created, for example by + +```{code-cell} python3 +q = (0.1, 0.9) +d = DiscreteRV(q) +d.q = (0.5, 0.5) +``` + +The problem is that `Q` does not change accordingly, and `Q` is the +data used in the `draw` method. + +To deal with this, one option is to compute `Q` every time the draw +method is called. + +But this is inefficient relative to computing `Q` once-off. + +A better option is to use descriptors. + +A solution from the [quantecon +library](https://github.com/QuantEcon/QuantEcon.py/tree/main/quantecon) +using descriptors that behaves as we desire can be found +[here](https://github.com/QuantEcon/QuantEcon.py/blob/main/quantecon/discrete_rv.py). + +```{solution-end} +``` + + +```{exercise} +:label: np_ex3 + +Recall our {ref}`earlier discussion ` of the empirical cumulative distribution function. + +Your task is to + +1. Make the `__call__` method more efficient using NumPy. +1. Add a method that plots the ECDF over $[a, b]$, where $a$ and $b$ are method parameters. +``` + +```{solution-start} np_ex3 +:class: dropdown +``` + +An example solution is given below. + +In essence, we've just taken [this code](https://github.com/QuantEcon/QuantEcon.py/blob/main/quantecon/ecdf.py) +from QuantEcon and added in a plot method + +```{code-cell} python3 +""" +Modifies ecdf.py from QuantEcon to add in a plot method + +""" + +class ECDF: + """ + One-dimensional empirical distribution function given a vector of + observations. + + Parameters + ---------- + observations : array_like + An array of observations + + Attributes + ---------- + observations : array_like + An array of observations + + """ + + def __init__(self, observations): + self.observations = np.asarray(observations) + + def __call__(self, x): + """ + Evaluates the ecdf at x + + Parameters + ---------- + x : scalar(float) + The x at which the ecdf is evaluated + + Returns + ------- + scalar(float) + Fraction of the sample less than x + + """ + return np.mean(self.observations <= x) + + def plot(self, ax, a=None, b=None): + """ + Plot the ecdf on the interval [a, b]. + + Parameters + ---------- + a : scalar(float), optional(default=None) + Lower endpoint of the plot interval + b : scalar(float), optional(default=None) + Upper endpoint of the plot interval + + """ + + # === choose reasonable interval if [a, b] not specified === # + if a is None: + a = self.observations.min() - self.observations.std() + if b is None: + b = self.observations.max() + self.observations.std() + + # === generate plot === # + x_vals = np.linspace(a, b, num=100) + f = np.vectorize(self.__call__) + ax.plot(x_vals, f(x_vals)) + plt.show() +``` + +Here's an example of usage + +```{code-cell} python3 +fig, ax = plt.subplots() +rng = np.random.default_rng() +X = rng.standard_normal(1000) +F = ECDF(X) +F.plot(ax) +``` + +```{solution-end} +``` + + +```{exercise-start} +:label: np_ex4 +``` + +Recall that [broadcasting](broadcasting) in NumPy can help us conduct element-wise operations on arrays with different number of dimensions without using `for` loops. + +In this exercise, try to use `for` loops to replicate the result of the following broadcasting operations. + +**Part 1**: Try to replicate this simple example using `for` loops and compare your results with the broadcasting operation below. + +```{code-cell} python3 + +rng = np.random.default_rng(123) +x = rng.standard_normal((4, 4)) +y = rng.standard_normal(4) +A = x / y +``` + +Here is the output + +```{code-cell} python3 +--- +tags: [hide-output] +--- +print(A) +``` + +**Part 2**: Move on to replicate the result of the following broadcasting operation. Meanwhile, compare the speeds of broadcasting and the `for` loop you implement. + +For this part of the exercise you can use the `qe.Timer()` context manager from the `quantecon` library to time the execution. + +Let's make sure this library is installed. + +```{code-cell} python3 +:tags: [hide-output] +!pip install quantecon +``` + +Now we can import the quantecon package. + +```{code-cell} python3 + +rng = np.random.default_rng(123) +x = rng.standard_normal((1000, 100, 100)) +y = rng.standard_normal(100) + +with qe.Timer("Broadcasting operation"): + B = x / y +``` + +Here is the output + +```{code-cell} python3 +--- +tags: [hide-output] +--- +print(B) +``` + +```{exercise-end} +``` + + +```{solution-start} np_ex4 +:class: dropdown +``` + +**Part 1 Solution** + +```{code-cell} python3 +rng = np.random.default_rng(123) +x = rng.standard_normal((4, 4)) +y = rng.standard_normal(4) + +C = np.empty_like(x) +n = len(x) +for i in range(n): + for j in range(n): + C[i, j] = x[i, j] / y[j] +``` + +Compare the results to check your answer + +```{code-cell} python3 +--- +tags: [hide-output] +--- +print(C) +``` + +You can also use `array_equal()` to check your answer + +```{code-cell} python3 +print(np.array_equal(A, C)) +``` + + +**Part 2 Solution** + +```{code-cell} python3 + +rng = np.random.default_rng(123) +x = rng.standard_normal((1000, 100, 100)) +y = rng.standard_normal(100) + +with qe.Timer("For loop operation"): + D = np.empty_like(x) + d1, d2, d3 = x.shape + for i in range(d1): + for j in range(d2): + for k in range(d3): + D[i, j, k] = x[i, j, k] / y[k] +``` + +Note that the `for` loop takes much longer than the broadcasting operation. + +Compare the results to check your answer + +```{code-cell} python3 +--- +tags: [hide-output] +--- +print(D) +``` + +```{code-cell} python3 +print(np.array_equal(B, D)) +``` + +```{solution-end} +``` diff --git a/experiments/ml-benchmark/arms/2026-09-21-round4-numpy-v0.29.2/numpy-v0.29.2-draw3.md b/experiments/ml-benchmark/arms/2026-09-21-round4-numpy-v0.29.2/numpy-v0.29.2-draw3.md new file mode 100644 index 0000000..424efa8 --- /dev/null +++ b/experiments/ml-benchmark/arms/2026-09-21-round4-numpy-v0.29.2/numpy-v0.29.2-draw3.md @@ -0,0 +1,1557 @@ +--- +jupytext: + text_representation: + extension: .md + format_name: myst +kernelspec: + display_name: Python 3 + language: python + name: python3 +translation: + title: NumPy + headings: + Overview: Overview + NumPy Arrays: NumPy Arrays + NumPy Arrays::Basics: Basics + NumPy Arrays::Shape and Dimension: Shape and Dimension + NumPy Arrays::Creating Arrays: Creating Arrays + NumPy Arrays::Array Indexing: Array Indexing + NumPy Arrays::Array Methods: Array Methods + Arithmetic Operations: Arithmetic Operations + Matrix Multiplication: Matrix Multiplication + Broadcasting: Broadcasting + Mutability and Copying Arrays: Mutability and Copying Arrays + Mutability and Copying Arrays::Mutability: Mutability + Mutability and Copying Arrays::Making Copies: Making Copies + Additional Features: Additional Features + Additional Features::Universal Functions: Universal Functions + Additional Features::Comparisons: Comparisons + Additional Features::Sub-packages: Sub-packages + Additional Features::Implicit Multithreading: Implicit Multithreading + Exercises: Exercises +--- + +(np)= +```{raw} jupyter + +``` + +# {index}`NumPy ` + +```{index} single: Python; NumPy +``` + +```{epigraph} +"Let's be clear: the work of science has nothing whatever to do with consensus. Consensus is the business of politics. Science, on the contrary, requires only one investigator who happens to be right, which means that he or she has results that are verifiable by reference to the real world. In science consensus is irrelevant. What is relevant is reproducible results." -- Michael Crichton +``` + +Anaconda-യിലുള്ളവ കൂടാതെ, ഈ lecture-ന് താഴെ പറയുന്ന libraries ആവശ്യമായിവരുന്നു: + +```{code-cell} ipython3 +:tags: [hide-output] + +!pip install quantecon +``` + +## Overview + +[NumPy](https://en.wikipedia.org/wiki/NumPy) എന്നത്, numerical programming-നായുള്ള ഒരു first-rate library ആണ്. + +* Academia, finance, industry എന്നിവയിൽ വ്യാപകമായി ഉപയോഗിക്കപ്പെടുന്നു. +* Mature-ഉം, fast-ഉം, stable-ഉം ആയ, തുടർച്ചയായി development നടക്കുന്ന ഒന്നാണ്. + +മുൻ lectures-ൽ NumPy ഉൾപ്പെടുന്ന ചില code നമ്മൾ already കണ്ടിട്ടുണ്ട്. + +ഈ lecture-ൽ നമ്മൾ ചെയ്യാൻ പോകുന്ന കാര്യങ്ങൾ: + +1. NumPy arrays-ഉം +1. NumPy നൽകുന്ന fundamental array processing operations-ഉം + +ഇവയെക്കുറിച്ച് കൂടുതൽ systematic ആയ ഒരു discussion നമുക്ക് ഇവിടെ തുടങ്ങാം. + +(ഒരു alternative reference-ന്, [the official NumPy documentation](https://numpy.org/doc/stable/reference/) കാണുക.) + +താഴെ കൊടുത്തിരിക്കുന്ന imports നമ്മൾ ഉപയോഗിക്കും. + +```{code-cell} python3 +import numpy as np +import random +import quantecon as qe +import matplotlib.pyplot as plt +from mpl_toolkits.mplot3d.axes3d import Axes3D +from matplotlib import cm +``` + + + +(numpy_array)= +## NumPy Arrays + +```{index} single: NumPy; Arrays +``` + +NumPy പരിഹരിക്കുന്ന പ്രധാന പ്രശ്നം fast array processing ആണ്. + +NumPy define ചെയ്യുന്ന ഏറ്റവും പ്രധാനപ്പെട്ട structure ഒരു array data type ആണ്, ഇതിനെ ഔപചാരികമായി +[numpy.ndarray](https://numpy.org/doc/stable/reference/arrays.ndarray.html) എന്ന് വിളിക്കുന്നു. + +Scientific Python ecosystem-ന്റെ വലിയൊരു ഭാഗവും NumPy arrays ആണ് പ്രവർത്തിപ്പിക്കുന്നത്. + +### Basics + +Zeros മാത്രം അടങ്ങിയ ഒരു NumPy array create ചെയ്യാൻ നമ്മൾ [np.zeros](https://numpy.org/doc/stable/reference/generated/numpy.zeros.html#numpy.zeros) ഉപയോഗിക്കുന്നു. + +```{code-cell} python3 +a = np.zeros(3) +a +``` + +```{code-cell} python3 +type(a) +``` + +NumPy arrays, native Python lists-നോട് ഒരു പരിധി വരെ സാമ്യമുള്ളവയാണ്, പക്ഷേ താഴെ പറയുന്ന വ്യത്യാസങ്ങളുണ്ട്: + +* Data *homogeneous* ആയിരിക്കണം (എല്ലാ elements-ഉം ഒരേ type-ലുള്ളവ ആയിരിക്കണം). +* ഈ types, NumPy നൽകുന്ന [data types](https://numpy.org/doc/stable/reference/arrays.dtypes.html) (`dtypes`) എന്നിവയിൽ ഒന്നായിരിക്കണം. + +ഇവയിൽ ഏറ്റവും പ്രധാനപ്പെട്ട dtypes: + +* float64: 64 bit floating-point number +* int64: 64 bit integer +* bool: 8 bit True or False + +Complex numbers, unsigned integers എന്നിവയെ represent ചെയ്യാൻ വേറെയും dtypes ഉണ്ട്. + +ആധുനിക machines-ൽ, arrays-ന്റെ default dtype `float64` ആണ്. + +```{code-cell} python3 +a = np.zeros(3) +type(a[0]) +``` + +Integers ഉപയോഗിക്കണമെങ്കിൽ, താഴെ പറയുന്ന രീതിയിൽ specify ചെയ്യാം: + +```{code-cell} python3 +a = np.zeros(3, dtype=int) +type(a[0]) +``` + +(numpy_shape_dim)= +### Shape and Dimension + +```{index} single: NumPy; Arrays (Shape and Dimension) +``` + +താഴെ കൊടുത്തിരിക്കുന്ന assignment നോക്കാം. + +```{code-cell} python3 +z = np.zeros(10) +``` + +ഇവിടെ `z` ഒരു **flat** array ആണ് --- row vector-ഉം അല്ല, column vector-ഉം അല്ല. + +```{code-cell} python3 +z.shape +``` + +ഇവിടെ shape tuple-ന് ഒരു element മാത്രമേ ഉള്ളൂ, അത് array-യുടെ length ആണ് +(ഒരു element മാത്രമുള്ള tuples ഒരു comma-യിൽ അവസാനിക്കും). + +ഇതിന് ഒരു additional dimension കൊടുക്കാൻ, നമുക്ക് `shape` attribute മാറ്റാം. + +```{code-cell} python3 +z.shape = (10, 1) # Convert flat array to column vector (two-dimensional) +z +``` + +```{code-cell} python3 +z = np.zeros(4) # Flat array +z.shape = (2, 2) # Two-dimensional array +z +``` + +അവസാനത്തെ case-ൽ, 2x2 array ഉണ്ടാക്കാൻ, `zeros()` function-ന് ഒരു tuple pass ചെയ്യാം, `z = np.zeros((2, 2))` എന്ന രീതിയിൽ. + + + +(creating_arrays)= +### Creating Arrays + +```{index} single: NumPy; Arrays (Creating) +``` + +നമ്മൾ കണ്ടതുപോലെ, `np.zeros` function zeros-ന്റെ ഒരു array create ചെയ്യുന്നു. + +`np.ones` എന്താണ് create ചെയ്യുന്നതെന്ന് നിങ്ങൾക്ക് ഊഹിക്കാൻ കഴിയും. + +ഇതിനോട് ബന്ധപ്പെട്ടതാണ് `np.empty`, ഇത് memory-യിൽ arrays create ചെയ്യുന്നു, ഇവയിൽ പിന്നീട് data populate ചെയ്യാം. + +```{code-cell} python3 +z = np.empty(3) +z +``` + +ഇവിടെ കാണുന്ന numbers garbage values ആണ്. + +(Python 3 contiguous 64 bit memory pieces allocate ചെയ്യുന്നു, ആ memory slots-ലെ നിലവിലുള്ള contents `float64` values ആയി interpret ചെയ്യപ്പെടുന്നു) + +Evenly spaced numbers-ന്റെ ഒരു grid set up ചെയ്യാൻ `np.linspace` ഉപയോഗിക്കുക. + +```{code-cell} python3 +z = np.linspace(2, 4, 5) # From 2 to 4, with 5 elements +``` + +ഒരു identity matrix create ചെയ്യാൻ `np.identity` അല്ലെങ്കിൽ `np.eye` ഉപയോഗിക്കുക. + +```{code-cell} python3 +z = np.identity(2) +z +``` + +ഇതു കൂടാതെ, `np.array` ഉപയോഗിച്ച് Python lists, tuples മുതലായവയിൽ നിന്നും NumPy arrays create ചെയ്യാം. + +```{code-cell} python3 +z = np.array([10, 20]) # ndarray from Python list +z +``` + +```{code-cell} python3 +type(z) +``` + +```{code-cell} python3 +z = np.array((10, 20), dtype=float) # Here 'float' is equivalent to 'np.float64' +z +``` + +```{code-cell} python3 +z = np.array([[1, 2], [3, 4]]) # 2D array from a list of lists +z +``` + +`np.asarray` എന്ന function-ഉം ഇതേ പോലുള്ള ഒരു function നൽകുന്നു, പക്ഷേ ഇത് ഒരു NumPy array-ൽ already ഉള്ള data-യുടെ വേറൊരു copy ഉണ്ടാക്കുന്നില്ല. + +Numeric data അടങ്ങിയ ഒരു text file-ൽ നിന്നും array data read ചെയ്യാൻ `np.loadtxt` ഉപയോഗിക്കുക --- വിശദാംശങ്ങൾക്ക് [the documentation](https://numpy.org/doc/stable/reference/routines.io.html) കാണുക. + + + +### Array Indexing + +```{index} single: NumPy; Arrays (Indexing) +``` + +ഒരു flat array-ന്, indexing Python sequences-ന്റെ അതേ രീതിയിലാണ്: + +```{code-cell} python3 +z = np.linspace(1, 2, 5) +z +``` + +```{code-cell} python3 +z[0] +``` + +```{code-cell} python3 +z[0:2] # Two elements, starting at element 0 +``` + +```{code-cell} python3 +z[-1] +``` + +2D arrays-ന്, index syntax താഴെ കാണാം: + +```{code-cell} python3 +z = np.array([[1, 2], [3, 4]]) +z +``` + +```{code-cell} python3 +z[0, 0] +``` + +```{code-cell} python3 +z[0, 1] +``` + +ഇങ്ങനെ തുടരാം. + +Columns-ഉം rows-ഉം താഴെ പറയുന്ന രീതിയിൽ extract ചെയ്യാം: + +```{code-cell} python3 +z[0, :] +``` + +```{code-cell} python3 +z[:, 1] +``` + +Elements extract ചെയ്യാൻ integers-ന്റെ NumPy arrays-ഉം ഉപയോഗിക്കാം. + +```{code-cell} python3 +z = np.linspace(2, 4, 5) +z +``` + +```{code-cell} python3 +indices = np.array((0, 2, 3)) +z[indices] +``` + +അവസാനമായി, elements extract ചെയ്യാൻ `dtype bool` ഉള്ള ഒരു array ഉപയോഗിക്കാം. + +```{code-cell} python3 +z +``` + +```{code-cell} python3 +d = np.array([0, 1, 1, 0, 0], dtype=bool) +d +``` + +```{code-cell} python3 +z[d] +``` + +ഇത് എന്തുകൊണ്ട് വളരെ useful ആണെന്ന് താഴെ നമുക്ക് കാണാം. + +ഒരു aside: slice notation ഉപയോഗിച്ച് ഒരു array-യിലെ എല്ലാ elements-ഉം ഒരു number-ന് തുല്യമായി set ചെയ്യാം. + +```{code-cell} python3 +z = np.empty(3) +z +``` + +```{code-cell} python3 +z[:] = 42 +z +``` + +### Array Methods + +```{index} single: NumPy; Arrays (Methods) +``` + +Arrays-ന് വളരെ useful ആയ methods ഉണ്ട്, ഇവയെല്ലാം carefully optimize ചെയ്തിരിക്കുന്നു. + +```{code-cell} python3 +a = np.array((4, 3, 2, 1)) +a +``` + +```{code-cell} python3 +a.sort() # Sorts a in place +a +``` + +```{code-cell} python3 +a.sum() # Sum +``` + +```{code-cell} python3 +a.mean() # Mean +``` + +```{code-cell} python3 +a.max() # Max +``` + +```{code-cell} python3 +a.argmax() # Returns the index of the maximal element +``` + +```{code-cell} python3 +a.cumsum() # Cumulative sum of the elements of a +``` + +```{code-cell} python3 +a.cumprod() # Cumulative product of the elements of a +``` + +```{code-cell} python3 +a.var() # Variance +``` + +```{code-cell} python3 +a.std() # Standard deviation +``` + +```{code-cell} python3 +a.shape = (2, 2) +a.T # Equivalent to a.transpose() +``` + +അറിഞ്ഞിരിക്കേണ്ട മറ്റൊരു method `searchsorted()` ആണ്. + +`z` ഒരു nondecreasing array ആണെങ്കിൽ, `z.searchsorted(a)`, `>= a` ആയ `z`-യുടെ +ആദ്യത്തെ element-ന്റെ index return ചെയ്യുന്നു. + +```{code-cell} python3 +z = np.linspace(2, 4, 5) +z +``` + +```{code-cell} python3 +z.searchsorted(2.2) +``` + + +## Arithmetic Operations + +```{index} single: NumPy; Arithmetic Operations +``` + +`+`, `-`, `*`, `/`, `**` എന്നീ operators എല്ലാം arrays-ൽ *elementwise* ആയി act ചെയ്യുന്നു. + +```{code-cell} python3 +a = np.array([1, 2, 3, 4]) +b = np.array([5, 6, 7, 8]) +a + b +``` + +```{code-cell} python3 +a * b +``` + +താഴെ പറയുന്ന രീതിയിൽ ഓരോ element-ഇനും ഒരു scalar നമുക്ക് add ചെയ്യാം: + +```{code-cell} python3 +a + 10 +``` + +Scalar multiplication-ഉം ഇതേ പോലെയാണ്. + +```{code-cell} python3 +a * 10 +``` + +Two-dimensional arrays-ഉം ഇതേ general rules തന്നെയാണ് പിന്തുടരുന്നത്. + +```{code-cell} python3 +A = np.ones((2, 2)) +B = np.ones((2, 2)) +A + B +``` + +```{code-cell} python3 +A + 10 +``` + +```{code-cell} python3 +A * B +``` + +(numpy_matrix_multiplication)= +In particular, `A * B` matrix product അല്ല, ഇത് element-wise product ആണ്. + + +## Matrix Multiplication + +```{index} single: NumPy; Matrix Multiplication +``` + +```{index} single: NumPy; Matrix Multiplication +``` + +Matrix multiplication-ന് നമ്മൾ `@` symbol ഉപയോഗിക്കുന്നു, താഴെ കാണുന്ന പോലെ: + +```{code-cell} python3 +A = np.ones((2, 2)) +B = np.ones((2, 2)) +A @ B +``` + +Flat arrays-ലും ഈ syntax work ചെയ്യുന്നു --- നിങ്ങൾക്ക് എന്താണ് വേണ്ടതെന്ന് +NumPy ഒരു educated guess നടത്തുന്നു: + +```{code-cell} python3 +A @ (0, 1) +``` + +നമ്മൾ post-multiply ചെയ്യുന്നതിനാൽ, tuple ഒരു column vector ആയി treat ചെയ്യപ്പെടുന്നു. + + + +(broadcasting)= +## Broadcasting + +```{index} single: NumPy; Broadcasting +``` + +(ഈ section, [Jake VanderPlas](https://jakevdp.github.io/PythonDataScienceHandbook/02.05-computation-on-arrays-broadcasting.html) നൽകിയ broadcasting-നെക്കുറിച്ചുള്ള ഒരു മികച്ച discussion-നെ extend ചെയ്യുന്നു.) + +```{note} +Broadcasting എന്നത് NumPy-യുടെ വളരെ പ്രധാനപ്പെട്ട ഒരു aspect ആണ്. അതേസമയം, advanced broadcasting താരതമ്യേന complex ആണ്, താഴെയുള്ള ചില details ആദ്യത്തെ വായനയിൽ skim ചെയ്യാവുന്നതാണ്. +``` + +Element-wise operations-ൽ, arrays-ന് ഒരേ shape ഉണ്ടായിരിക്കണമെന്നില്ല. + +ഇത് സംഭവിക്കുമ്പോൾ, സാധ്യമാകുന്നിടത്തെല്ലാം NumPy automatically arrays-നെ ഒരേ shape-ലേക്ക് expand ചെയ്യും. + +NumPy-യിലെ ഈ വളരെ useful ആയ (എന്നാൽ ചിലപ്പോൾ confusing ആയ) feature-നെ **broadcasting** എന്ന് വിളിക്കുന്നു. + +Broadcasting-ന്റെ value എന്തെന്നാൽ: + +* `for` loops ഒഴിവാക്കാം, ഇത് numerical code fast ആയി run ചെയ്യാൻ സഹായിക്കുന്നു, ഒപ്പം +* arrays-ന്റെ ചില dimensions memory-യിൽ actual ആയി create ചെയ്യാതെ തന്നെ, broadcasting arrays-ൽ operations implement ചെയ്യാൻ നമ്മെ അനുവദിക്കുന്നു, arrays വലുതാകുമ്പോൾ ഇത് പ്രധാനമാകാം. + +For example, `a` ഒരു $3 \times 3$ array ആണെന്ന് കരുതുക (`a -> (3, 3)`), `b` മൂന്ന് elements ഉള്ള ഒരു flat array ആണെന്നും കരുതുക (`b -> (3,)`). + +ഇവയെ ഒരുമിച്ച് add ചെയ്യുമ്പോൾ, NumPy automatically `b -> (3,)`-നെ `b -> (3, 3)` ആയി expand ചെയ്യും. + +Element-wise addition-ന്റെ ഫലമായി ഒരു $3 \times 3$ array ലഭിക്കും. + +```{code-cell} python3 + +a = np.array( + [[1, 2, 3], + [4, 5, 6], + [7, 8, 9]]) +b = np.array([3, 6, 9]) + +a + b +``` + +ഈ broadcasting operation-ന്റെ ഒരു visual representation താഴെ കാണാം: + +```{code-cell} python3 +--- +tags: [hide-input] +--- +# Adapted and modified based on the code in the book written by Jake VanderPlas (see https://jakevdp.github.io/PythonDataScienceHandbook/06.00-figure-code.html#Broadcasting) +# Originally from astroML: see https://www.astroml.org/book_figures/appendix/fig_broadcast_visual.html + + +def draw_cube(ax, xy, size, depth=0.4, + edges=None, label=None, label_kwargs=None, **kwargs): + """draw and label a cube. edges is a list of numbers between + 1 and 12, specifying which of the 12 cube edges to draw""" + if edges is None: + edges = range(1, 13) + + x, y = xy + + if 1 in edges: + ax.plot([x, x + size], + [y + size, y + size], **kwargs) + if 2 in edges: + ax.plot([x + size, x + size], + [y, y + size], **kwargs) + if 3 in edges: + ax.plot([x, x + size], + [y, y], **kwargs) + if 4 in edges: + ax.plot([x, x], + [y, y + size], **kwargs) + + if 5 in edges: + ax.plot([x, x + depth], + [y + size, y + depth + size], **kwargs) + if 6 in edges: + ax.plot([x + size, x + size + depth], + [y + size, y + depth + size], **kwargs) + if 7 in edges: + ax.plot([x + size, x + size + depth], + [y, y + depth], **kwargs) + if 8 in edges: + ax.plot([x, x + depth], + [y, y + depth], **kwargs) + + if 9 in edges: + ax.plot([x + depth, x + depth + size], + [y + depth + size, y + depth + size], **kwargs) + if 10 in edges: + ax.plot([x + depth + size, x + depth + size], + [y + depth, y + depth + size], **kwargs) + if 11 in edges: + ax.plot([x + depth, x + depth + size], + [y + depth, y + depth], **kwargs) + if 12 in edges: + ax.plot([x + depth, x + depth], + [y + depth, y + depth + size], **kwargs) + + if label: + if label_kwargs is None: + label_kwargs = {} + ax.text(x + 0.5 * size, y + 0.5 * size, label, + ha='center', va='center', **label_kwargs) + +solid = dict(c='black', ls='-', lw=1, + label_kwargs=dict(color='k')) +dotted = dict(c='black', ls='-', lw=0.5, alpha=0.5, + label_kwargs=dict(color='gray')) +depth = 0.3 + +# Draw a figure and axis with no boundary +fig = plt.figure(figsize=(5, 1), facecolor='w') +ax = plt.axes([0, 0, 1, 1], xticks=[], yticks=[], frameon=False) + +# first block +draw_cube(ax, (1, 7.5), 1, depth, [1, 2, 3, 4, 5, 6, 9], '1', **solid) +draw_cube(ax, (2, 7.5), 1, depth, [1, 2, 3, 6, 9], '2', **solid) +draw_cube(ax, (3, 7.5), 1, depth, [1, 2, 3, 6, 7, 9, 10], '3', **solid) + +draw_cube(ax, (1, 6.5), 1, depth, [2, 3, 4], '4', **solid) +draw_cube(ax, (2, 6.5), 1, depth, [2, 3], '5', **solid) +draw_cube(ax, (3, 6.5), 1, depth, [2, 3, 7, 10], '6', **solid) + +draw_cube(ax, (1, 5.5), 1, depth, [2, 3, 4], '7', **solid) +draw_cube(ax, (2, 5.5), 1, depth, [2, 3], '8', **solid) +draw_cube(ax, (3, 5.5), 1, depth, [2, 3, 7, 10], '9', **solid) + +# second block +draw_cube(ax, (6, 7.5), 1, depth, [1, 2, 3, 4, 5, 6, 9], '3', **solid) +draw_cube(ax, (7, 7.5), 1, depth, [1, 2, 3, 6, 9], '6', **solid) +draw_cube(ax, (8, 7.5), 1, depth, [1, 2, 3, 6, 7, 9, 10], '9', **solid) + +draw_cube(ax, (6, 6.5), 1, depth, range(2, 13), '3', **dotted) +draw_cube(ax, (7, 6.5), 1, depth, [2, 3, 6, 7, 9, 10, 11], '6', **dotted) +draw_cube(ax, (8, 6.5), 1, depth, [2, 3, 6, 7, 9, 10, 11], '9', **dotted) + +draw_cube(ax, (6, 5.5), 1, depth, [2, 3, 4, 7, 8, 10, 11, 12], '3', **dotted) +draw_cube(ax, (7, 5.5), 1, depth, [2, 3, 7, 10, 11], '6', **dotted) +draw_cube(ax, (8, 5.5), 1, depth, [2, 3, 7, 10, 11], '9', **dotted) + +# third block +draw_cube(ax, (12, 7.5), 1, depth, [1, 2, 3, 4, 5, 6, 9], '4', **solid) +draw_cube(ax, (13, 7.5), 1, depth, [1, 2, 3, 6, 9], '8', **solid) +draw_cube(ax, (14, 7.5), 1, depth, [1, 2, 3, 6, 7, 9, 10], '12', **solid) + +draw_cube(ax, (12, 6.5), 1, depth, [2, 3, 4], '7', **solid) +draw_cube(ax, (13, 6.5), 1, depth, [2, 3], '11', **solid) +draw_cube(ax, (14, 6.5), 1, depth, [2, 3, 7, 10], '15', **solid) + +draw_cube(ax, (12, 5.5), 1, depth, [2, 3, 4], '10', **solid) +draw_cube(ax, (13, 5.5), 1, depth, [2, 3], '14', **solid) +draw_cube(ax, (14, 5.5), 1, depth, [2, 3, 7, 10], '18', **solid) + +ax.text(5, 7.0, '+', size=12, ha='center', va='center') +ax.text(10.5, 7.0, '=', size=12, ha='center', va='center'); +``` + +`b -> (3, 1)` ആണെങ്കിലോ? + +ഈ case-ൽ, NumPy automatically `b -> (3, 1)`-നെ `b -> (3, 3)` ആയി expand ചെയ്യും. + +Element-wise addition-ന്റെ ഫലമായി അപ്പോൾ ഒരു $3 \times 3$ matrix ലഭിക്കും. + +```{code-cell} python3 +b.shape = (3, 1) + +a + b +``` + +ഈ broadcasting operation-ന്റെ ഒരു visual representation താഴെ കാണാം: + +```{code-cell} python3 +--- +tags: [hide-input] +--- + +fig = plt.figure(figsize=(5, 1), facecolor='w') +ax = plt.axes([0, 0, 1, 1], xticks=[], yticks=[], frameon=False) + +# first block +draw_cube(ax, (1, 7.5), 1, depth, [1, 2, 3, 4, 5, 6, 9], '1', **solid) +draw_cube(ax, (2, 7.5), 1, depth, [1, 2, 3, 6, 9], '2', **solid) +draw_cube(ax, (3, 7.5), 1, depth, [1, 2, 3, 6, 7, 9, 10], '3', **solid) + +draw_cube(ax, (1, 6.5), 1, depth, [2, 3, 4], '4', **solid) +draw_cube(ax, (2, 6.5), 1, depth, [2, 3], '5', **solid) +draw_cube(ax, (3, 6.5), 1, depth, [2, 3, 7, 10], '6', **solid) + +draw_cube(ax, (1, 5.5), 1, depth, [2, 3, 4], '7', **solid) +draw_cube(ax, (2, 5.5), 1, depth, [2, 3], '8', **solid) +draw_cube(ax, (3, 5.5), 1, depth, [2, 3, 7, 10], '9', **solid) + +# second block +draw_cube(ax, (6, 7.5), 1, depth, [1, 2, 3, 4, 5, 6, 7, 9, 10], '3', **solid) +draw_cube(ax, (7, 7.5), 1, depth, [1, 2, 3, 6, 7, 9, 10], '3', **dotted) +draw_cube(ax, (8, 7.5), 1, depth, [1, 2, 3, 6, 7, 9, 10], '3', **dotted) + +draw_cube(ax, (6, 6.5), 1, depth, [2, 3, 4, 7, 10], '6', **solid) +draw_cube(ax, (7, 6.5), 1, depth, [2, 3, 6, 7, 9, 10, 11], '6', **dotted) +draw_cube(ax, (8, 6.5), 1, depth, [2, 3, 6, 7, 9, 10, 11], '6', **dotted) + +draw_cube(ax, (6, 5.5), 1, depth, [2, 3, 4, 7, 10], '9', **solid) +draw_cube(ax, (7, 5.5), 1, depth, [2, 3, 7, 10, 11], '9', **dotted) +draw_cube(ax, (8, 5.5), 1, depth, [2, 3, 7, 10, 11], '9', **dotted) + +# third block +draw_cube(ax, (12, 7.5), 1, depth, [1, 2, 3, 4, 5, 6, 9], '4', **solid) +draw_cube(ax, (13, 7.5), 1, depth, [1, 2, 3, 6, 9], '5', **solid) +draw_cube(ax, (14, 7.5), 1, depth, [1, 2, 3, 6, 7, 9, 10], '6', **solid) + +draw_cube(ax, (12, 6.5), 1, depth, [2, 3, 4], '10', **solid) +draw_cube(ax, (13, 6.5), 1, depth, [2, 3], '11', **solid) +draw_cube(ax, (14, 6.5), 1, depth, [2, 3, 7, 10], '12', **solid) + +draw_cube(ax, (12, 5.5), 1, depth, [2, 3, 4], '16', **solid) +draw_cube(ax, (13, 5.5), 1, depth, [2, 3], '17', **solid) +draw_cube(ax, (14, 5.5), 1, depth, [2, 3, 7, 10], '18', **solid) + +ax.text(5, 7.0, '+', size=12, ha='center', va='center') +ax.text(10.5, 7.0, '=', size=12, ha='center', va='center'); + + +``` + +ചില cases-ൽ, രണ്ട് operands-ഉം expand ചെയ്യപ്പെടും. + +`a -> (3,)`, `b -> (3, 1)` എന്നിവ ഉള്ളപ്പോൾ, `a`, `a -> (3, 3)` ആയി expand ചെയ്യപ്പെടും, `b`, `b -> (3, 3)` ആയും expand ചെയ്യപ്പെടും. + +ഈ case-ൽ, element-wise addition-ന്റെ ഫലമായി ഒരു $3 \times 3$ matrix ലഭിക്കും. + +```{code-cell} python3 +a = np.array([3, 6, 9]) +b = np.array([2, 3, 4]) +b.shape = (3, 1) + +a + b +``` + +ഈ broadcasting operation-ന്റെ ഒരു visual representation താഴെ കാണാം: + +```{code-cell} python3 +--- +tags: [hide-input] +--- + +# Draw a figure and axis with no boundary +fig = plt.figure(figsize=(5, 1), facecolor='w') +ax = plt.axes([0, 0, 1, 1], xticks=[], yticks=[], frameon=False) + +# first block +draw_cube(ax, (1, 7.5), 1, depth, [1, 2, 3, 4, 5, 6, 9], '3', **solid) +draw_cube(ax, (2, 7.5), 1, depth, [1, 2, 3, 6, 9], '6', **solid) +draw_cube(ax, (3, 7.5), 1, depth, [1, 2, 3, 6, 7, 9, 10], '9', **solid) + +draw_cube(ax, (1, 6.5), 1, depth, range(2, 13), '3', **dotted) +draw_cube(ax, (2, 6.5), 1, depth, [2, 3, 6, 7, 9, 10, 11], '6', **dotted) +draw_cube(ax, (3, 6.5), 1, depth, [2, 3, 6, 7, 9, 10, 11], '9', **dotted) + +draw_cube(ax, (1, 5.5), 1, depth, [2, 3, 4, 7, 8, 10, 11, 12], '3', **dotted) +draw_cube(ax, (2, 5.5), 1, depth, [2, 3, 7, 10, 11], '6', **dotted) +draw_cube(ax, (3, 5.5), 1, depth, [2, 3, 7, 10, 11], '9', **dotted) + +# second block +draw_cube(ax, (6, 7.5), 1, depth, [1, 2, 3, 4, 5, 6, 7, 9, 10], '2', **solid) +draw_cube(ax, (7, 7.5), 1, depth, [1, 2, 3, 6, 7, 9, 10], '2', **dotted) +draw_cube(ax, (8, 7.5), 1, depth, [1, 2, 3, 6, 7, 9, 10], '2', **dotted) + +draw_cube(ax, (6, 6.5), 1, depth, [2, 3, 4, 7, 10], '3', **solid) +draw_cube(ax, (7, 6.5), 1, depth, [2, 3, 6, 7, 9, 10, 11], '3', **dotted) +draw_cube(ax, (8, 6.5), 1, depth, [2, 3, 6, 7, 9, 10, 11], '3', **dotted) + +draw_cube(ax, (6, 5.5), 1, depth, [2, 3, 4, 7, 10], '4', **solid) +draw_cube(ax, (7, 5.5), 1, depth, [2, 3, 7, 10, 11], '4', **dotted) +draw_cube(ax, (8, 5.5), 1, depth, [2, 3, 7, 10, 11], '4', **dotted) + +# third block +draw_cube(ax, (12, 7.5), 1, depth, [1, 2, 3, 4, 5, 6, 9], '5', **solid) +draw_cube(ax, (13, 7.5), 1, depth, [1, 2, 3, 6, 9], '8', **solid) +draw_cube(ax, (14, 7.5), 1, depth, [1, 2, 3, 6, 7, 9, 10], '11', **solid) + +draw_cube(ax, (12, 6.5), 1, depth, [2, 3, 4], '6', **solid) +draw_cube(ax, (13, 6.5), 1, depth, [2, 3], '9', **solid) +draw_cube(ax, (14, 6.5), 1, depth, [2, 3, 7, 10], '12', **solid) + +draw_cube(ax, (12, 5.5), 1, depth, [2, 3, 4], '7', **solid) +draw_cube(ax, (13, 5.5), 1, depth, [2, 3], '10', **solid) +draw_cube(ax, (14, 5.5), 1, depth, [2, 3, 7, 10], '13', **solid) + +ax.text(5, 7.0, '+', size=12, ha='center', va='center') +ax.text(10.5, 7.0, '=', size=12, ha='center', va='center'); +``` + +Broadcasting വളരെ useful ആണെങ്കിലും, ചിലപ്പോൾ ഇത് confusing ആയി തോന്നാം. + +For example, `a -> (3, 2)`, `b -> (3,)` എന്നിവ add ചെയ്യാൻ നമുക്ക് ശ്രമിക്കാം. + +```{code-cell} python3 +--- +tags: [raises-exception] +--- +a = np.array( + [[1, 2], + [4, 5], + [7, 8]]) +b = np.array([3, 6, 9]) + +a + b +``` + +`ValueError`, operands-നെ ഒരുമിച്ച് broadcast ചെയ്യാൻ കഴിഞ്ഞില്ല എന്ന് നമ്മോട് പറയുന്നു. + + +ഈ broadcasting എന്തുകൊണ്ട് execute ചെയ്യാൻ കഴിയില്ല എന്ന് കാണിക്കുന്ന ഒരു visual representation താഴെ കാണാം: + +```{code-cell} python3 +--- +tags: [hide-input] +--- +# Draw a figure and axis with no boundary +fig = plt.figure(figsize=(3, 1.3), facecolor='w') +ax = plt.axes([0, 0, 1, 1], xticks=[], yticks=[], frameon=False) + +# first block +draw_cube(ax, (1, 7.5), 1, depth, [1, 2, 3, 4, 5, 6, 9], '1', **solid) +draw_cube(ax, (2, 7.5), 1, depth, [1, 2, 3, 6, 7, 9, 10], '2', **solid) + +draw_cube(ax, (1, 6.5), 1, depth, [2, 3, 4], '4', **solid) +draw_cube(ax, (2, 6.5), 1, depth, [2, 3, 7, 10], '5', **solid) + +draw_cube(ax, (1, 5.5), 1, depth, [2, 3, 4], '7', **solid) +draw_cube(ax, (2, 5.5), 1, depth, [2, 3, 7, 10], '8', **solid) + +# second block +draw_cube(ax, (6, 7.5), 1, depth, [1, 2, 3, 4, 5, 6, 9], '3', **solid) +draw_cube(ax, (7, 7.5), 1, depth, [1, 2, 3, 6, 9], '6', **solid) +draw_cube(ax, (8, 7.5), 1, depth, [1, 2, 3, 6, 7, 9, 10], '9', **solid) + +draw_cube(ax, (6, 6.5), 1, depth, range(2, 13), '3', **dotted) +draw_cube(ax, (7, 6.5), 1, depth, [2, 3, 6, 7, 9, 10, 11], '6', **dotted) +draw_cube(ax, (8, 6.5), 1, depth, [2, 3, 6, 7, 9, 10, 11], '9', **dotted) + +draw_cube(ax, (6, 5.5), 1, depth, [2, 3, 4, 7, 8, 10, 11, 12], '3', **dotted) +draw_cube(ax, (7, 5.5), 1, depth, [2, 3, 7, 10, 11], '6', **dotted) +draw_cube(ax, (8, 5.5), 1, depth, [2, 3, 7, 10, 11], '9', **dotted) + + +ax.text(4.5, 7.0, '+', size=12, ha='center', va='center') +ax.text(10, 7.0, '=', size=12, ha='center', va='center') +ax.text(11, 7.0, '?', size=16, ha='center', va='center'); +``` + +Arrays-നെ ഒരേ size-ലേക്ക് expand ചെയ്യാൻ NumPy-ക്ക് കഴിയില്ല എന്ന് നമുക്ക് കാണാം. + +കാരണം, `b`, `b -> (3,)`-യിൽ നിന്നും `b -> (3, 3)` ആയി expand ചെയ്യപ്പെടുമ്പോൾ, `b`-യെ `a -> (3, 2)`-യുമായി match ചെയ്യാൻ NumPy-ക്ക് കഴിയില്ല. + +Higher dimensions-ലേക്ക് നീങ്ങുമ്പോൾ കാര്യങ്ങൾ കൂടുതൽ trickier ആകും. + +നമുക്ക് സഹായത്തിനായി, താഴെ കൊടുത്തിരിക്കുന്ന rules-ന്റെ ഒരു list ഉപയോഗിക്കാം: + +* *Step 1:* രണ്ട് arrays-ന്റെയും dimensions match ചെയ്യാത്തപ്പോൾ, NumPy, കുറവ് dimensions ഉള്ളതിനെ, നിലവിലുള്ള dimensions-ന്റെ ഇടതുവശത്ത് dimension(s) ചേർത്ത് expand ചെയ്യും. + - For example, `a -> (3, 3)`, `b -> (3,)` ആണെങ്കിൽ, broadcasting ഇടതുവശത്ത് ഒരു dimension ചേർക്കും, അതിനാൽ `b -> (1, 3)` ആകും; + - `a -> (2, 2, 2)`, `b -> (2, 2)` ആണെങ്കിൽ, broadcasting ഇടതുവശത്ത് ഒരു dimension ചേർക്കും, അതിനാൽ `b -> (1, 2, 2)` ആകും; + - `a -> (3, 2, 2)`, `b -> (2,)` ആണെങ്കിൽ, broadcasting ഇടതുവശത്ത് രണ്ട് dimensions ചേർക്കും, അതിനാൽ `b -> (1, 1, 2)` ആകും (ഇത് *Step 1* രണ്ട് തവണ നടത്തുന്നത് പോലെയും കാണാം). + + +* *Step 2:* രണ്ട് arrays-ക്കും ഒരേ dimension ഉണ്ടെങ്കിലും shapes വ്യത്യസ്തമാണെങ്കിൽ, shape index 1 ആയ dimensions expand ചെയ്യാൻ NumPy ശ്രമിക്കും. + - For example, `a -> (1, 3)`, `b -> (3, 1)` ആണെങ്കിൽ, broadcasting `a`-യിലും `b`-യിലും shape 1 ഉള്ള dimensions expand ചെയ്യും, അതിനാൽ `a -> (3, 3)`, `b -> (3, 3)` ആകും; + - `a -> (2, 2, 2)`, `b -> (1, 2, 2)` ആണെങ്കിൽ, broadcasting `b`-യുടെ ആദ്യത്തെ dimension expand ചെയ്യും, അതിനാൽ `b -> (2, 2, 2)` ആകും; + - `a -> (3, 2, 2)`, `b -> (1, 1, 2)` ആണെങ്കിൽ, broadcasting `b`-യെ shape 1 ഉള്ള എല്ലാ dimensions-ലും expand ചെയ്യും, അതിനാൽ `b -> (3, 2, 2)` ആകും. + +* *Step 3:* Step 1, 2 എന്നിവയ്ക്ക് ശേഷവും, രണ്ട് arrays-ഉം match ചെയ്യുന്നില്ലെങ്കിൽ, ഒരു `ValueError` raise ചെയ്യപ്പെടും. For example, `a -> (2, 2, 3)`, `b -> (2, 2)` ആണെന്ന് കരുതുക. + - *Step 1* പ്രകാരം, `b`, `b -> (1, 2, 2)` ആയി expand ചെയ്യപ്പെടും; + - *Step 2* പ്രകാരം, `b`, `b -> (2, 2, 2)` ആയി expand ചെയ്യപ്പെടും; + - ആദ്യത്തെ രണ്ട് steps-ന് ശേഷവും ഇവ പരസ്പരം match ചെയ്യുന്നില്ല എന്ന് നമുക്ക് കാണാം. അതിനാൽ, ഒരു `ValueError` raise ചെയ്യപ്പെടും. + + + +## Mutability and Copying Arrays + +Python lists പോലെ, NumPy arrays-ഉം mutable data types ആണ്. + +അതായത്, initialization-ന് ശേഷം, memory-യിലെ ഇവയുടെ contents മാറ്റാൻ (mutate ചെയ്യാൻ) കഴിയും. + +ഇത് convenient ആണ്, പക്ഷേ Python-ന്റെ naming, reference model എന്നിവയുമായി combine ചെയ്യുമ്പോൾ, +NumPy beginners-ന് mistakes ഉണ്ടാകാൻ ഇത് കാരണമാകാം. + +ഈ section-ൽ ചില പ്രധാന issues നമുക്ക് നോക്കാം. + + +### Mutability + +Mutability-യുടെ examples നമ്മൾ മുകളിൽ already കണ്ടു. + +ഒരു NumPy array mutate ചെയ്യുന്നതിന്റെ മറ്റൊരു example താഴെ കാണാം: + +```{code-cell} python3 +a = np.array([42, 44]) +a +``` + +```{code-cell} python3 +a[-1] = 0 # Change last element to 0 +a +``` + +Mutability താഴെ പറയുന്ന behavior-ലേക്ക് നയിക്കുന്നു (ഇത് MATLAB programmers-ന് shocking ആയി തോന്നിയേക്കാം...) + +```{code-cell} python3 +rng = np.random.default_rng() +a = rng.standard_normal(3) +a +``` + +```{code-cell} python3 +b = a +b[0] = 0.0 +a +``` + +`b` മാറ്റുന്നതിലൂടെ `a`-യെ നമ്മൾ മാറ്റിയിരിക്കുന്നു എന്നതാണ് ഇവിടെ സംഭവിച്ചത്. + +`b` എന്ന name, `a`-യുമായി bind ചെയ്യപ്പെട്ടിരിക്കുന്നു, ഇത് array-യിലേക്കുള്ള മറ്റൊരു reference ആയി മാറുന്നു +(Python assignment model {doc}`later in the course `-ൽ കൂടുതൽ വിശദമായി describe ചെയ്തിട്ടുണ്ട്). + +അതിനാൽ, ആ array-യിൽ changes വരുത്താൻ ഇതിന് തുല്യ അവകാശമുണ്ട്. + +വാസ്തവത്തിൽ ഇത് ഏറ്റവും sensible ആയ default behavior ആണ്! + +ഇതിനർത്ഥം, copies ഉണ്ടാക്കുന്നതിന് പകരം, data-യിലേക്കുള്ള pointers മാത്രമാണ് നമ്മൾ pass ചെയ്യുന്നത് എന്നാണ്. + +Copies ഉണ്ടാക്കുന്നത് speed-ന്റെയും memory-യുടെയും കാര്യത്തിൽ expensive ആണ്. + +### Making Copies + +ആവശ്യമുള്ളപ്പോൾ `b`-യെ `a`-യുടെ ഒരു independent copy ആക്കുന്നത് തീർച്ചയായും സാധ്യമാണ്. + +ഇത് `np.copy` ഉപയോഗിച്ച് ചെയ്യാം. + +```{code-cell} python3 +a = rng.standard_normal(3) +a +``` + +```{code-cell} python3 +b = np.copy(a) +b +``` + +ഇനി `b` ഒരു independent copy ആണ് (ഇതിനെ *deep copy* എന്ന് വിളിക്കുന്നു) + +```{code-cell} python3 +b[:] = 1 +b +``` + +```{code-cell} python3 +a +``` + +`b`-യിലെ change, `a`-യെ ബാധിച്ചിട്ടില്ല എന്ന് ശ്രദ്ധിക്കുക. + + + + +## Additional Features + +NumPy-യുടെ മറ്റ് ചില useful features നമുക്ക് നോക്കാം. + + +### Universal Functions + +```{index} single: NumPy; Vectorized Functions +``` + +`log`, `exp`, `sin` തുടങ്ങിയ standard functions-ന്റെ versions NumPy നൽകുന്നു, ഇവ arrays-ൽ *element-wise* ആയി act ചെയ്യുന്നു. + +```{code-cell} python3 +z = np.array([1, 2, 3]) +np.sin(z) +``` + +ഇത്, താഴെ കാണുന്ന പോലുള്ള explicit element-by-element loops-ന്റെ ആവശ്യം ഒഴിവാക്കുന്നു: + +```{code-cell} python3 +n = len(z) +y = np.empty(n) +for i in range(n): + y[i] = np.sin(z[i]) +``` + +ഇവ arrays-ൽ element-wise ആയി act ചെയ്യുന്നതിനാൽ, ഈ functions-നെ ചിലപ്പോൾ **vectorized functions** എന്ന് വിളിക്കുന്നു. + +NumPy-speak-ൽ, ഇവയെ **ufuncs**, അഥവാ **universal functions** എന്നും വിളിക്കുന്നു. + +മുകളിൽ നമ്മൾ കണ്ടതുപോലെ, സാധാരണ arithmetic operations-ഉം (`+`, `*` മുതലായവ) +element-wise ആയി work ചെയ്യുന്നു, ഇവയെ ufuncs-ഉമായി combine ചെയ്യുമ്പോൾ വളരെ വലിയൊരു fast element-wise functions-ന്റെ set ലഭിക്കുന്നു. + +```{code-cell} python3 +z +``` + +```{code-cell} python3 +(1 / np.sqrt(2 * np.pi)) * np.exp(- 0.5 * z**2) +``` + +എല്ലാ user-defined functions-ഉം element-wise ആയി act ചെയ്യണമെന്നില്ല. + +For example, താഴെ define ചെയ്തിരിക്കുന്ന `f` എന്ന function-ന് ഒരു NumPy array pass ചെയ്യുന്നത് ഒരു `ValueError`-ന് കാരണമാകുന്നു. + +```{code-cell} python3 +def f(x): + return 1 if x > 0 else 0 +``` + +NumPy function `np.where`, ഒരു vectorized alternative നൽകുന്നു: + +```{code-cell} python3 +x = rng.standard_normal(4) +x +``` + +```{code-cell} python3 +np.where(x > 0, 1, 0) # Insert 1 if x > 0 true, otherwise 0 +``` + +തന്നിരിക്കുന്ന ഒരു function vectorize ചെയ്യാൻ `np.vectorize`-ഉം ഉപയോഗിക്കാം. + +```{code-cell} python3 +f = np.vectorize(f) +f(x) # Passing the same vector x as in the previous example +``` + +എന്നിരുന്നാലും, ഈ approach-ന്, കൂടുതൽ carefully crafted ആയ ഒരു vectorized function-ന്റെ speed എപ്പോഴും ലഭിക്കണമെന്നില്ല. + +(പിന്നീട് നമുക്ക് കാണാം, JAX-ന്, `np.vectorize`-ന്റെ ഒരു powerful version ഉണ്ട്, ഇത് highly efficient ആയ code generate ചെയ്യാൻ കഴിയുന്നതും, മിക്ക സമയത്തും അങ്ങനെ ചെയ്യുന്നതും ആണ്.) + + +### Comparisons + +```{index} single: NumPy; Comparisons +``` + +സാധാരണയായി, arrays-ലെ comparisons element-wise ആയാണ് ചെയ്യപ്പെടുന്നത്. + +```{code-cell} python3 +z = np.array([2, 3]) +y = np.array([2, 3]) +z == y +``` + +```{code-cell} python3 +y[0] = 5 +z == y +``` + +```{code-cell} python3 +z != y +``` + +`>`, `<`, `>=`, `<=` എന്നിവയ്ക്കും situation സമാനമാണ്. + +Scalars-നൊപ്പം comparisons-ഉം നമുക്ക് ചെയ്യാം. + +```{code-cell} python3 +z = np.linspace(0, 10, 5) +z +``` + +```{code-cell} python3 +z > 3 +``` + +*Conditional extraction*-ന് ഇത് പ്രത്യേകിച്ചും വളരെ useful ആണ്. + +```{code-cell} python3 +b = z > 3 +b +``` + +```{code-cell} python3 +z[b] +``` + +തീർച്ചയായും, ഇത് ഒരൊറ്റ step-ൽ നമുക്ക് ചെയ്യാൻ കഴിയും---കൂടാതെ ഇത് നമ്മൾ പലപ്പോഴും ചെയ്യാറുമുണ്ട്. + +```{code-cell} python3 +z[z > 3] +``` + +### Sub-packages + +Sub-packages മുഖേന scientific programming-യുമായി ബന്ധപ്പെട്ട ചില additional functionality NumPy നൽകുന്നു. + +NumPy-യുടെ [random `Generator`](https://numpy.org/doc/stable/reference/random/generator.html#random-generator) ഉപയോഗിച്ച് random variables generate ചെയ്യുന്നത് നമ്മൾ already കണ്ടതാണ്. + +```{code-cell} python3 +z = rng.standard_normal(10000) # Generate standard normals +y = rng.binomial(10, 0.5, size=1000) # 1,000 draws from Bin(10, 0.5) +y.mean() +``` + +സാധാരണയായി ഉപയോഗിക്കുന്ന മറ്റൊരു subpackage np.linalg ആണ്. + +```{code-cell} python3 +A = np.array([[1, 2], [3, 4]]) + +np.linalg.det(A) # Compute the determinant +``` + +```{code-cell} python3 +np.linalg.inv(A) # Compute the inverse +``` + +```{index} single: SciPy +``` + +```{index} single: Python; SciPy +``` + +NumPy-യുടെ മുകളിൽ build ചെയ്തിരിക്കുന്ന modules-ന്റെ ഒരു collection ആയ [SciPy](https://scipy.org/)-ലും ഈ functionality-യുടെ ഭൂരിഭാഗവും available ആണ്. + +SciPy versions {doc}`soon ` നമ്മൾ കൂടുതൽ വിശദമായി cover ചെയ്യും. + +NumPy-യിൽ available ആയവയുടെ ഒരു comprehensive list-ന് [this documentation](https://numpy.org/doc/stable/reference/routines.html) കാണുക. + + +### Implicit Multithreading + +[Previously](need_for_speed) multithreading മുഖേനയുള്ള parallelization-ന്റെ concept നമ്മൾ discuss ചെയ്തിരുന്നു. + +NumPy, അതിന്റെ compiled code-ന്റെ ഭൂരിഭാഗത്തിലും multithreading implement ചെയ്യാൻ ശ്രമിക്കുന്നു. + +ഇത് action-ൽ കാണാൻ ഒരു example നമുക്ക് നോക്കാം. + +താഴെ കൊടുത്തിരിക്കുന്ന code, randomly generate ചെയ്ത ധാരാളം matrices-ന്റെ +eigenvalues compute ചെയ്യുന്നു. + +ഇത് run ആകാൻ കുറച്ച് seconds എടുക്കും. + +```{code-cell} python3 +n = 20 +m = 1000 +for i in range(n): + X = rng.standard_normal((m, m)) + λ = np.linalg.eigvals(X) +``` + +ഇനി, ഈ code run ആയിക്കൊണ്ടിരിക്കുമ്പോൾ നമ്മുടെ machine-ലെ htop system monitor-ന്റെ output നമുക്ക് നോക്കാം: + +```{figure} /_static/lecture_specific/parallelization/htop_parallel_npmat.png +:scale: 80 +``` + +8 CPUs-ൽ 4 എണ്ണം full speed-ൽ run ചെയ്യുന്നത് നമുക്ക് കാണാം. + +ഇതിന് കാരണം, NumPy-യുടെ `eigvals` routine, tasks-നെ neatly split ചെയ്ത് +വ്യത്യസ്ത threads-ലേക്ക് distribute ചെയ്യുന്നു എന്നതാണ്. + + + + + +## Exercises + + +```{exercise-start} +:label: np_ex1 +``` + +Consider the polynomial expression + +```{math} +:label: np_polynom + +p(x) = a_0 + a_1 x + a_2 x^2 + \cdots a_N x^N = \sum_{n=0}^N a_n x^n +``` + +{ref}`Earlier `, you wrote a simple function `p(x, coeff)` to evaluate {eq}`np_polynom` without considering efficiency. + +Now write a new function that does the same job, but uses NumPy arrays and array operations for its computations, rather than any form of Python loop. + +(Such functionality is already implemented as `np.poly1d`, but for the sake of the exercise don't use this class) + +```{hint} +:class: dropdown +Use `np.cumprod()` +``` +```{exercise-end} +``` + +```{solution-start} np_ex1 +:class: dropdown +``` + +This code does the job + +```{code-cell} python3 +def p(x, coef): + X = np.ones_like(coef) + X[1:] = x + y = np.cumprod(X) # y = [1, x, x**2,...] + return coef @ y +``` + +Let's test it + +```{code-cell} python3 +x = 2 +coef = np.linspace(2, 4, 3) +print(coef) +print(p(x, coef)) +# For comparison +q = np.poly1d(np.flip(coef)) +print(q(x)) +``` + +```{solution-end} +``` + + +```{exercise-start} +:label: np_ex2 +``` + +Let `q` be a NumPy array of length `n` with `q.sum() == 1`. + +Suppose that `q` represents a [probability mass function](https://en.wikipedia.org/wiki/Probability_mass_function). + +We wish to generate a discrete random variable $x$ such that $\mathbb P\{x = i\} = q_i$. + +In other words, `x` takes values in `range(len(q))` and `x = i` with probability `q[i]`. + +The standard (inverse transform) algorithm is as follows: + +* Divide the unit interval $[0, 1]$ into $n$ subintervals $I_0, I_1, \ldots, I_{n-1}$ such that the length of $I_i$ is $q_i$. +* Draw a uniform random variable $U$ on $[0, 1]$ and return the $i$ such that $U \in I_i$. + +The probability of drawing $i$ is the length of $I_i$, which is equal to $q_i$. + +We can implement the algorithm as follows + +```{code-cell} python3 +from random import uniform + +def sample(q): + a = 0.0 + U = uniform(0, 1) + for i in range(len(q)): + if a < U <= a + q[i]: + return i + a = a + q[i] +``` + +If you can't see how this works, try thinking through the flow for a simple example, such as `q = [0.25, 0.75]` +It helps to sketch the intervals on paper. + +Your exercise is to speed it up using NumPy, avoiding explicit loops + +```{hint} +:class: dropdown + +Use `np.searchsorted` and `np.cumsum` + +``` + +If you can, implement the functionality as a class called `DiscreteRV`, where + +* the data for an instance of the class is the vector of probabilities `q` +* the class has a `draw()` method, which returns one draw according to the algorithm described above + +If you can, write the method so that `draw(k)` returns `k` draws from `q`. + +```{exercise-end} +``` + +```{solution-start} np_ex2 +:class: dropdown +``` + +Here's our first pass at a solution: + +```{code-cell} python3 +from numpy import cumsum + +class DiscreteRV: + """ + Generates an array of draws from a discrete random variable with vector of + probabilities given by q. + """ + + def __init__(self, q, seed=None): + """ + The argument q is a NumPy array, or array like, nonnegative and sums + to 1. + + The argument seed sets the seed for the underlying random number + generator; with the default seed=None, draws are not reproducible + across runs. + """ + self.q = q + self.Q = cumsum(q) + self.rng = np.random.default_rng(seed) + + def draw(self, k=1): + """ + Returns k draws from q. For each such draw, the value i is returned + with probability q[i]. + """ + return self.Q.searchsorted(self.rng.uniform(0, 1, size=k)) +``` + +The logic is not obvious, but if you take your time and read it slowly, +you will understand. + +There is a problem here, however. + +Suppose that `q` is altered after an instance of `DiscreteRV` is +created, for example by + +```{code-cell} python3 +q = (0.1, 0.9) +d = DiscreteRV(q) +d.q = (0.5, 0.5) +``` + +The problem is that `Q` does not change accordingly, and `Q` is the +data used in the `draw` method. + +To deal with this, one option is to compute `Q` every time the draw +method is called. + +But this is inefficient relative to computing `Q` once-off. + +A better option is to use descriptors. + +A solution from the [quantecon +library](https://github.com/QuantEcon/QuantEcon.py/tree/main/quantecon) +using descriptors that behaves as we desire can be found +[here](https://github.com/QuantEcon/QuantEcon.py/blob/main/quantecon/discrete_rv.py). + +```{solution-end} +``` + + +```{exercise} +:label: np_ex3 + +Recall our {ref}`earlier discussion ` of the empirical cumulative distribution function. + +Your task is to + +1. Make the `__call__` method more efficient using NumPy. +1. Add a method that plots the ECDF over $[a, b]$, where $a$ and $b$ are method parameters. +``` + +```{solution-start} np_ex3 +:class: dropdown +``` + +An example solution is given below. + +In essence, we've just taken [this code](https://github.com/QuantEcon/QuantEcon.py/blob/main/quantecon/ecdf.py) +from QuantEcon and added in a plot method + +```{code-cell} python3 +""" +Modifies ecdf.py from QuantEcon to add in a plot method + +""" + +class ECDF: + """ + One-dimensional empirical distribution function given a vector of + observations. + + Parameters + ---------- + observations : array_like + An array of observations + + Attributes + ---------- + observations : array_like + An array of observations + + """ + + def __init__(self, observations): + self.observations = np.asarray(observations) + + def __call__(self, x): + """ + Evaluates the ecdf at x + + Parameters + ---------- + x : scalar(float) + The x at which the ecdf is evaluated + + Returns + ------- + scalar(float) + Fraction of the sample less than x + + """ + return np.mean(self.observations <= x) + + def plot(self, ax, a=None, b=None): + """ + Plot the ecdf on the interval [a, b]. + + Parameters + ---------- + a : scalar(float), optional(default=None) + Lower endpoint of the plot interval + b : scalar(float), optional(default=None) + Upper endpoint of the plot interval + + """ + + # === choose reasonable interval if [a, b] not specified === # + if a is None: + a = self.observations.min() - self.observations.std() + if b is None: + b = self.observations.max() + self.observations.std() + + # === generate plot === # + x_vals = np.linspace(a, b, num=100) + f = np.vectorize(self.__call__) + ax.plot(x_vals, f(x_vals)) + plt.show() +``` + +Here's an example of usage + +```{code-cell} python3 +fig, ax = plt.subplots() +rng = np.random.default_rng() +X = rng.standard_normal(1000) +F = ECDF(X) +F.plot(ax) +``` + +```{solution-end} +``` + + +```{exercise-start} +:label: np_ex4 +``` + +Recall that [broadcasting](broadcasting) in NumPy can help us conduct element-wise operations on arrays with different number of dimensions without using `for` loops. + +In this exercise, try to use `for` loops to replicate the result of the following broadcasting operations. + +**Part 1**: Try to replicate this simple example using `for` loops and compare your results with the broadcasting operation below. + +```{code-cell} python3 + +rng = np.random.default_rng(123) +x = rng.standard_normal((4, 4)) +y = rng.standard_normal(4) +A = x / y +``` + +Here is the output + +```{code-cell} python3 +--- +tags: [hide-output] +--- +print(A) +``` + +**Part 2**: Move on to replicate the result of the following broadcasting operation. Meanwhile, compare the speeds of broadcasting and the `for` loop you implement. + +For this part of the exercise you can use the `qe.Timer()` context manager from the `quantecon` library to time the execution. + +Let's make sure this library is installed. + +```{code-cell} python3 +:tags: [hide-output] +!pip install quantecon +``` + +Now we can import the quantecon package. + +```{code-cell} python3 + +rng = np.random.default_rng(123) +x = rng.standard_normal((1000, 100, 100)) +y = rng.standard_normal(100) + +with qe.Timer("Broadcasting operation"): + B = x / y +``` + +Here is the output + +```{code-cell} python3 +--- +tags: [hide-output] +--- +print(B) +``` + +```{exercise-end} +``` + + +```{solution-start} np_ex4 +:class: dropdown +``` + +**Part 1 Solution** + +```{code-cell} python3 +rng = np.random.default_rng(123) +x = rng.standard_normal((4, 4)) +y = rng.standard_normal(4) + +C = np.empty_like(x) +n = len(x) +for i in range(n): + for j in range(n): + C[i, j] = x[i, j] / y[j] +``` + +Compare the results to check your answer + +```{code-cell} python3 +--- +tags: [hide-output] +--- +print(C) +``` + +You can also use `array_equal()` to check your answer + +```{code-cell} python3 +print(np.array_equal(A, C)) +``` + + +**Part 2 Solution** + +```{code-cell} python3 + +rng = np.random.default_rng(123) +x = rng.standard_normal((1000, 100, 100)) +y = rng.standard_normal(100) + +with qe.Timer("For loop operation"): + D = np.empty_like(x) + d1, d2, d3 = x.shape + for i in range(d1): + for j in range(d2): + for k in range(d3): + D[i, j, k] = x[i, j, k] / y[k] +``` + +Note that the `for` loop takes much longer than the broadcasting operation. + +Compare the results to check your answer + +```{code-cell} python3 +--- +tags: [hide-output] +--- +print(D) +``` + +```{code-cell} python3 +print(np.array_equal(B, D)) +``` + +```{solution-end} +``` From 21b47c9856c492b4bbe7d5dda81e2cbf0186222d Mon Sep 17 00:00:00 2001 From: Matt McKay Date: Wed, 23 Sep 2026 13:24:52 +1000 Subject: [PATCH 2/3] =?UTF-8?q?dev:=20STATE.md=20=E2=80=94=202026-09-23=20?= =?UTF-8?q?close-out=20(#322=20for=20#321,=20#323=20for=20#320)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5.1 --- .dev/STATE.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.dev/STATE.md b/.dev/STATE.md index a88d2ea..08a53a9 100644 --- a/.dev/STATE.md +++ b/.dev/STATE.md @@ -17,7 +17,11 @@ Roadmap detail lives in the work-plan tracker **#257**, not here (PLAN.md predat draw 1 sent (bare endings 1 / 37 / 2), one `ml_repair.py` comma applied and disclosed — the first repaired seed; arm `experiments/ml-benchmark/arms/2026-09-21-round4-numpy-v0.29.2/`. Harness note: the reset left one PR from the previous gate open on `.ml` (closed by hand). - W1 (#259) still targets v0.30.0. **Next engine-side: the rule-2 arm** (rewrite around the + W1 (#259) still targets v0.30.0. **2026-09-23 close-out**: the reset survivor's cause was + `gh pr list` with no `--limit` — fixed with a post-reset check on **#322** (closes #321); the + gate's one-draw blindness (a ~40% defect passed three gates) is **#320**, decided as a local + N-draw rate check — `tool-test-action-on-github/rate-check.sh`, release step 4b, on **#323** + (v0.29.2 reads 0/12, v0.29.0 4/12). **Next engine-side: the rule-2 arm** (rewrite around the editor's everyday-speech test, judged held-out before it ships). - **v0.29.1 is tagged but NOT released (2026-09-21)** — its §4a gate came back 83/84: scenario 17 on the `.ml` lane failed twice (the model wrapped a plain `## Exercises` list in From 82026eafc13d8018342130affccf8515740c34e0 Mon Sep 17 00:00:00 2001 From: Matt McKay Date: Wed, 23 Sep 2026 13:36:56 +1000 Subject: [PATCH 3/3] arm: note the garbage token in round-4 draw 2 (left as generated; a lint gap, #301) Co-Authored-By: Claude Fable 5.1 --- .../arms/2026-09-21-round4-numpy-v0.29.2/README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/experiments/ml-benchmark/arms/2026-09-21-round4-numpy-v0.29.2/README.md b/experiments/ml-benchmark/arms/2026-09-21-round4-numpy-v0.29.2/README.md index cde85da..8c6cfb1 100644 --- a/experiments/ml-benchmark/arms/2026-09-21-round4-numpy-v0.29.2/README.md +++ b/experiments/ml-benchmark/arms/2026-09-21-round4-numpy-v0.29.2/README.md @@ -25,3 +25,11 @@ watch flags). Whether he touches the repaired line is the first field evidence f Terminal punctuation remains the draw-dependent class (1 / 37 / 2 here; 37 / 34 / 3 at v0.29.0): best-of-N by lint is still what keeps it away from the editor. + +One more observation, raised in review of the PR that archived this arm: draw 2, line 177, +carries a garbage token — `2x2 array undegerbestellen ഉണ്ടാക്കാൻ` — a Latin-script string that +is neither English, code nor a glossary term, emitted by the model and absent from the other +two draws and from the draft sent. It is left exactly as generated: the draws are the record. +It is also a lint gap — nothing in `ml_metrics.py` flags a Latin token that is not English, +code or a pinned term (#301) — and draw 2 was already the one not chosen, on bare endings. +