Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions examples/numpy/README.md
Original file line number Diff line number Diff line change
@@ -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.
66 changes: 66 additions & 0 deletions examples/numpy/array_basics/code.py
Original file line number Diff line number Diff line change
@@ -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: <code>{readings_f.shape}</code>, "
f"dtype: <code>{readings_f.dtype}</code>, "
f"size: <code>{readings_f.size}</code>"
)
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: <strong>{readings_c.min():.2f} &deg;C</strong> &middot; "
f"max: <strong>{readings_c.max():.2f} &deg;C</strong> &middot; "
f"mean: <strong>{readings_c.mean():.2f} &deg;C</strong> &middot; "
f"std: <strong>{readings_c.std():.2f}</strong>"
)

# Boolean indexing: select array elements with a boolean mask.
heading("Boolean indexing")
warm = readings_c[readings_c > 20]
note(
f"Hours above 20&deg;C: <strong>{warm.size}</strong>. "
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)
1 change: 1 addition & 0 deletions examples/numpy/array_basics/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
packages = ["numpy", "matplotlib"]
46 changes: 46 additions & 0 deletions examples/numpy/array_basics/setup.py
Original file line number Diff line number Diff line change
@@ -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"<h{level}>{text}</h{level}>"), append=True)


def note(text):
display(HTML(f"<p>{text}</p>"), append=True)


import numpy as np
import matplotlib.pyplot as plt

rng = np.random.default_rng(7)
65 changes: 65 additions & 0 deletions examples/numpy/broadcasting_and_reshaping/code.py
Original file line number Diff line number Diff line change
@@ -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 <code>axis=1</code> (collapse columns). "
"Per-exam mean uses <code>axis=0</code> (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)
1 change: 1 addition & 0 deletions examples/numpy/broadcasting_and_reshaping/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
packages = ["numpy", "matplotlib"]
25 changes: 25 additions & 0 deletions examples/numpy/broadcasting_and_reshaping/setup.py
Original file line number Diff line number Diff line change
@@ -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"<h{level}>{text}</h{level}>"), append=True)


def note(text):
display(HTML(f"<p>{text}</p>"), append=True)


import numpy as np
import matplotlib.pyplot as plt

rng = np.random.default_rng(7)
71 changes: 71 additions & 0 deletions examples/numpy/linear_algebra_and_random/code.py
Original file line number Diff line number Diff line change
@@ -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: <code>{noise.mean():+.3f}</code>, "
f"std: <code>{noise.std():.3f}</code>"
)

# ---------------------------------------------------------------------
# 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: <code>{design.shape}</code>")

# 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: <strong>{fit_slope:.3f}</strong> "
f"(true {true_slope}) &middot; "
f"intercept: <strong>{fit_intercept:.3f}</strong> "
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&sup2;: <strong>{r_squared:.4f}</strong>")

# ---------------------------------------------------------------------
# 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)
1 change: 1 addition & 0 deletions examples/numpy/linear_algebra_and_random/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
packages = ["numpy", "matplotlib"]
25 changes: 25 additions & 0 deletions examples/numpy/linear_algebra_and_random/setup.py
Original file line number Diff line number Diff line change
@@ -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"<h{level}>{text}</h{level}>"), append=True)


def note(text):
display(HTML(f"<p>{text}</p>"), append=True)


import numpy as np
import matplotlib.pyplot as plt

rng = np.random.default_rng(7)
5 changes: 5 additions & 0 deletions examples/numpy/order.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[
"array_basics",
"broadcasting_and_reshaping",
"linear_algebra_and_random"
]