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) + + +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 usesaxis=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) + + +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) + + +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" +]