diff --git a/examples/numpy/README.md b/examples/numpy/README.md new file mode 100644 index 0000000..259f8e8 --- /dev/null +++ b/examples/numpy/README.md @@ -0,0 +1,18 @@ +# numpy Examples + +Each sub-directory contains a self-contained example. The order in +which the examples are to appear is specified in `order.json` (an +array of directory names in the expected order). + +In each example directory you'll find: + +* `config.toml` - must conform to the specification outlined here: + https://docs.pyscript.net/latest/user-guide/configuration/ This is + parsed and ultimately turned into a JSON representation as part of + the package's API object. +* `setup.py` - Python code for contextual and environmental setup, + NOT SEEN BY THE END USER, but is run before the `code.py` code is + evaluated. Allows us to create useful (IPython) shims, avoid + repeating boilerplate and whatnot. +* `code.py` - the actual code added to the editor which forms the + practical example of using the package. diff --git a/examples/numpy/array_basics/code.py b/examples/numpy/array_basics/code.py new file mode 100644 index 0000000..5a8461e --- /dev/null +++ b/examples/numpy/array_basics/code.py @@ -0,0 +1,66 @@ +""" +A first look at NumPy: arrays, vectorized math, and summary stats. + +NumPy's core idea is the ndarray: a fixed-size, typed, N-dimensional +array that lets you express numeric computation as whole-array +expressions instead of Python loops. + +Docs: https://numpy.org/doc/stable/ +""" +import numpy as np +import matplotlib.pyplot as plt +from IPython.core.display import display, HTML + + +# A small story: hourly temperature readings from a weather station, +# in degrees Fahrenheit. We want to clean them up and summarize. +heading("From Python list to NumPy array") + +readings_f = np.array( + [58.2, 60.1, 63.7, 67.4, 71.0, 74.6, 76.2, 75.1, 71.9, 66.8], + dtype=np.float64, +) + +note( + f"Shape: {readings_f.shape}, " + f"dtype: {readings_f.dtype}, " + f"size: {readings_f.size}" +) +display(readings_f, append=True) + +# Vectorized arithmetic: convert F to C with a single expression that +# applies element-wise. No Python-level loop is involved. +heading("Vectorized math: Fahrenheit to Celsius") +readings_c = (readings_f - 32) * 5 / 9 +display(readings_c.round(2), append=True) + +# Summary methods live as both functions (np.mean) and methods (.mean()). +heading("Summary statistics") +note( + f"min: {readings_c.min():.2f} °C · " + f"max: {readings_c.max():.2f} °C · " + f"mean: {readings_c.mean():.2f} °C · " + f"std: {readings_c.std():.2f}" +) + +# Boolean indexing: select array elements with a boolean mask. +heading("Boolean indexing") +warm = readings_c[readings_c > 20] +note( + f"Hours above 20°C: {warm.size}. " + f"Their values:" +) +display(warm.round(2), append=True) + +# A tiny plot to put it all together. +fig, ax = plt.subplots(figsize=(8, 3.5)) +hours = np.arange(readings_c.size) +ax.plot(hours, readings_c, marker="o", color="crimson") +ax.axhline(readings_c.mean(), color="gray", linestyle="--", + label=f"mean = {readings_c.mean():.1f} \u00b0C") +ax.set_xlabel("Hour") +ax.set_ylabel("Temperature (\u00b0C)") +ax.set_title("Hourly temperature") +ax.legend() +fig.tight_layout() +display(fig, append=True) diff --git a/examples/numpy/array_basics/config.toml b/examples/numpy/array_basics/config.toml new file mode 100644 index 0000000..ec4f4aa --- /dev/null +++ b/examples/numpy/array_basics/config.toml @@ -0,0 +1 @@ +packages = ["numpy", "matplotlib"] diff --git a/examples/numpy/array_basics/setup.py b/examples/numpy/array_basics/setup.py new file mode 100644 index 0000000..e452fea --- /dev/null +++ b/examples/numpy/array_basics/setup.py @@ -0,0 +1,46 @@ +""" +Shim IPython's display API onto PyScript so example code written in a +Jupyter/IPython idiom runs unmodified in the browser. +""" + +import sys +import types +import js +from pyscript import window, HTML, display as _display + +js.alert = window.alert + + +def display(*args, **kwargs): + return _display( + *args, **kwargs, target=__pyscript_display_target__, + ) + + +ipython = types.ModuleType("IPython") +core = types.ModuleType("IPython.core") +core_display = types.ModuleType("IPython.core.display") +core_display.display = display +core_display.HTML = HTML +ipython.core = core +core.display = core_display +ipython.get_ipython = lambda: None +ipython.display = core_display +sys.modules["IPython"] = ipython +sys.modules["IPython.core"] = core +sys.modules["IPython.core.display"] = core_display +sys.modules["IPython.display"] = core_display + + +def heading(text, level=2): + display(HTML(f"{text}"), append=True) + + +def note(text): + display(HTML(f"

{text}

"), append=True) + + +import numpy as np +import matplotlib.pyplot as plt + +rng = np.random.default_rng(7) diff --git a/examples/numpy/broadcasting_and_reshaping/code.py b/examples/numpy/broadcasting_and_reshaping/code.py new file mode 100644 index 0000000..46bdb50 --- /dev/null +++ b/examples/numpy/broadcasting_and_reshaping/code.py @@ -0,0 +1,65 @@ +# --------------------------------------------------------------------- +# Broadcasting: combining arrays of different (but compatible) shapes. +# --------------------------------------------------------------------- + +heading("Broadcasting: per-student exam scores") +note( + "Five students sit four exams. We'll center each exam around its " + "mean, then convert to letter grades, all without writing a loop." +) + +students = ["Ada", "Brij", "Cleo", "Dax", "Esi"] +exams = ["Algebra", "Biology", "Chemistry", "Drama"] + +# A 5x4 matrix: rows are students, columns are exams. +scores = rng.integers(50, 100, size=(len(students), len(exams))) +display(scores, append=True) + +# Per-exam mean has shape (4,). Subtracting it from a (5, 4) array +# broadcasts the row across all five students. +exam_means = scores.mean(axis=0) +note(f"Per-exam means (shape {exam_means.shape}):") +display(exam_means.round(2), append=True) + +centered = scores - exam_means +note("Scores centered on each exam's mean (shape stays 5\u00d74):") +display(centered.round(2), append=True) + +# Grade thresholds via np.where, applied element-wise to the whole array. +heading("Element-wise classification with np.where") +grades = np.where(scores >= 85, "A", + np.where(scores >= 70, "B", + np.where(scores >= 60, "C", "D"))) +display(grades, append=True) + +# --------------------------------------------------------------------- +# Reshape and aggregate along axes. +# --------------------------------------------------------------------- + +heading("Reshape and axis-wise reductions") +note( + "Per-student mean uses axis=1 (collapse columns). " + "Per-exam mean uses axis=0 (collapse rows)." +) + +per_student = scores.mean(axis=1).round(2) +per_exam = scores.mean(axis=0).round(2) + +note("Per-student means:") +display(dict(zip(students, per_student.tolist())), append=True) +note("Per-exam means:") +display(dict(zip(exams, per_exam.tolist())), append=True) + +# A heat-map style visualization of the centered scores. +fig, ax = plt.subplots(figsize=(7, 3.5)) +im = ax.imshow(centered, cmap="RdBu_r", vmin=-25, vmax=25, aspect="auto") +ax.set_xticks(range(len(exams)), exams) +ax.set_yticks(range(len(students)), students) +ax.set_title("Score relative to exam mean") +for i in range(centered.shape[0]): + for j in range(centered.shape[1]): + ax.text(j, i, f"{centered[i, j]:+.0f}", + ha="center", va="center", color="black", fontsize=9) +fig.colorbar(im, ax=ax, label="\u0394 from mean") +fig.tight_layout() +display(fig, append=True) diff --git a/examples/numpy/broadcasting_and_reshaping/config.toml b/examples/numpy/broadcasting_and_reshaping/config.toml new file mode 100644 index 0000000..ec4f4aa --- /dev/null +++ b/examples/numpy/broadcasting_and_reshaping/config.toml @@ -0,0 +1 @@ +packages = ["numpy", "matplotlib"] diff --git a/examples/numpy/broadcasting_and_reshaping/setup.py b/examples/numpy/broadcasting_and_reshaping/setup.py new file mode 100644 index 0000000..01e6d18 --- /dev/null +++ b/examples/numpy/broadcasting_and_reshaping/setup.py @@ -0,0 +1,25 @@ +"""Setup for the broadcasting example: same names as cell 1, no shim.""" +import js +from pyscript import window, HTML, display as _display + +js.alert = window.alert + + +def display(*args, **kwargs): + return _display( + *args, **kwargs, target=__pyscript_display_target__, + ) + + +def heading(text, level=2): + display(HTML(f"{text}"), append=True) + + +def note(text): + display(HTML(f"

{text}

"), append=True) + + +import numpy as np +import matplotlib.pyplot as plt + +rng = np.random.default_rng(7) diff --git a/examples/numpy/linear_algebra_and_random/code.py b/examples/numpy/linear_algebra_and_random/code.py new file mode 100644 index 0000000..764eef1 --- /dev/null +++ b/examples/numpy/linear_algebra_and_random/code.py @@ -0,0 +1,71 @@ +# --------------------------------------------------------------------- +# Random sampling and a tiny linear regression by least squares. +# --------------------------------------------------------------------- + +heading("Random sampling: simulating noisy measurements") +note( + "We'll pretend we measured how much a spring stretches under " + "different loads. The true relationship is linear, but our " + "measurements are noisy." +) + +# np.random.default_rng is the modern way to get a Generator. It +# exposes normal, uniform, integers, choice, and many more. +true_slope = 2.4 # cm of stretch per kg of load +true_intercept = 1.1 # cm at zero load + +loads_kg = np.linspace(0, 10, 25) +noise = rng.normal(loc=0.0, scale=0.8, size=loads_kg.size) +stretch_cm = true_slope * loads_kg + true_intercept + noise + +note( + f"Mean noise: {noise.mean():+.3f}, " + f"std: {noise.std():.3f}" +) + +# --------------------------------------------------------------------- +# Solve for slope and intercept with np.linalg.lstsq. +# --------------------------------------------------------------------- + +heading("Least-squares fit via np.linalg.lstsq") + +# Build the design matrix [load, 1]; each row is one observation. +design = np.column_stack([loads_kg, np.ones_like(loads_kg)]) +note(f"Design matrix shape: {design.shape}") + +# lstsq returns (solution, residuals, rank, singular_values). +solution, *_ = np.linalg.lstsq(design, stretch_cm, rcond=None) +fit_slope, fit_intercept = solution + +note( + f"Recovered slope: {fit_slope:.3f} " + f"(true {true_slope}) · " + f"intercept: {fit_intercept:.3f} " + f"(true {true_intercept})" +) + +# Coefficient of determination, R^2. +predictions = design @ solution +residuals = stretch_cm - predictions +ss_res = np.sum(residuals ** 2) +ss_tot = np.sum((stretch_cm - stretch_cm.mean()) ** 2) +r_squared = 1 - ss_res / ss_tot +note(f"R²: {r_squared:.4f}") + +# --------------------------------------------------------------------- +# Plot the data, the true line, and the fitted line. +# --------------------------------------------------------------------- + +fig, ax = plt.subplots(figsize=(8, 4)) +ax.scatter(loads_kg, stretch_cm, color="steelblue", + label="measurements", zorder=3) +ax.plot(loads_kg, true_slope * loads_kg + true_intercept, + color="green", linestyle="--", label="true line") +ax.plot(loads_kg, predictions, color="crimson", linewidth=2, + label=f"fit: y = {fit_slope:.2f}x + {fit_intercept:.2f}") +ax.set_xlabel("Load (kg)") +ax.set_ylabel("Stretch (cm)") +ax.set_title("Spring stretch vs. load") +ax.legend() +fig.tight_layout() +display(fig, append=True) diff --git a/examples/numpy/linear_algebra_and_random/config.toml b/examples/numpy/linear_algebra_and_random/config.toml new file mode 100644 index 0000000..ec4f4aa --- /dev/null +++ b/examples/numpy/linear_algebra_and_random/config.toml @@ -0,0 +1 @@ +packages = ["numpy", "matplotlib"] diff --git a/examples/numpy/linear_algebra_and_random/setup.py b/examples/numpy/linear_algebra_and_random/setup.py new file mode 100644 index 0000000..7bcd7c4 --- /dev/null +++ b/examples/numpy/linear_algebra_and_random/setup.py @@ -0,0 +1,25 @@ +"""Setup for the linear algebra example: same names as cell 1, no shim.""" +import js +from pyscript import window, HTML, display as _display + +js.alert = window.alert + + +def display(*args, **kwargs): + return _display( + *args, **kwargs, target=__pyscript_display_target__, + ) + + +def heading(text, level=2): + display(HTML(f"{text}"), append=True) + + +def note(text): + display(HTML(f"

{text}

"), append=True) + + +import numpy as np +import matplotlib.pyplot as plt + +rng = np.random.default_rng(7) diff --git a/examples/numpy/order.json b/examples/numpy/order.json new file mode 100644 index 0000000..340a267 --- /dev/null +++ b/examples/numpy/order.json @@ -0,0 +1,5 @@ +[ + "array_basics", + "broadcasting_and_reshaping", + "linear_algebra_and_random" +]