Spaces:
Runtime error
Runtime error
Greg Wilson commited on
Commit ·
8fe65d4
1
Parent(s): 8eccedb
fix: finalizing notebooks for relaunch
Browse files- .gitignore +2 -1
- altair/01_introduction.py +5 -6
- altair/02_marks_encoding.py +3 -4
- altair/03_data_transformation.py +1 -1
- altair/05_view_composition.py +1 -1
- altair/07_cartographic.py +7 -5
- altair/08_debugging.py +7 -8
- altair/altair_introduction.py.lock +0 -0
- optimization/01_least_squares.py +2 -1
- optimization/04_quadratic_program.py +2 -1
- optimization/05_portfolio_optimization.py +2 -1
- optimization/06_convex_optimization.py +2 -1
- optimization/07_sdp.py +2 -1
- pages/educators.md +109 -263
- polars/03_loading_data.py +9 -9
- polars/05_reactive_plots.py +18 -15
- polars/07_querying_with_sql.py +1 -1
- polars/11_missing_data.py +24 -18
- polars/16_lazy_execution.py +25 -21
- queueing/07_late_merge.py +2 -2
- queueing/11_tandem_queue.py +1 -1
- tools/01_widgets.py +471 -0
- tools/02_turtles.py +63 -0
- tools/index.md +6 -0
- tools/wiggly.py +201 -0
.gitignore
CHANGED
|
@@ -183,4 +183,5 @@ _site/
|
|
| 183 |
tmp/
|
| 184 |
example.db
|
| 185 |
example.db.wal
|
| 186 |
-
log_data_filtered*
|
|
|
|
|
|
| 183 |
tmp/
|
| 184 |
example.db
|
| 185 |
example.db.wal
|
| 186 |
+
log_data_filtered/*
|
| 187 |
+
log_data_filtered.*
|
altair/01_introduction.py
CHANGED
|
@@ -4,7 +4,6 @@
|
|
| 4 |
# "altair==6.0.0",
|
| 5 |
# "marimo",
|
| 6 |
# "pandas==3.0.1",
|
| 7 |
-
# "vega_datasets==0.9.0",
|
| 8 |
# ]
|
| 9 |
# ///
|
| 10 |
|
|
@@ -76,23 +75,23 @@ def _(mo):
|
|
| 76 |
|
| 77 |
When using Altair, datasets are commonly provided as data frames. Alternatively, Altair can also accept a URL to load a network-accessible dataset. As we will see, the named columns of the data frame are an essential piece of plotting with Altair.
|
| 78 |
|
| 79 |
-
We will often use datasets from
|
| 80 |
""")
|
| 81 |
return
|
| 82 |
|
| 83 |
|
| 84 |
@app.cell
|
| 85 |
def _():
|
| 86 |
-
from
|
| 87 |
-
cars = data.cars()
|
| 88 |
-
cars.head()
|
| 89 |
return cars, data
|
| 90 |
|
| 91 |
|
| 92 |
@app.cell(hide_code=True)
|
| 93 |
def _(mo):
|
| 94 |
mo.md(r"""
|
| 95 |
-
|
| 96 |
""")
|
| 97 |
return
|
| 98 |
|
|
|
|
| 4 |
# "altair==6.0.0",
|
| 5 |
# "marimo",
|
| 6 |
# "pandas==3.0.1",
|
|
|
|
| 7 |
# ]
|
| 8 |
# ///
|
| 9 |
|
|
|
|
| 75 |
|
| 76 |
When using Altair, datasets are commonly provided as data frames. Alternatively, Altair can also accept a URL to load a network-accessible dataset. As we will see, the named columns of the data frame are an essential piece of plotting with Altair.
|
| 77 |
|
| 78 |
+
We will often use datasets from Altair's `datasets` sub-package. Some of these datasets are directly available as Pandas data frames:
|
| 79 |
""")
|
| 80 |
return
|
| 81 |
|
| 82 |
|
| 83 |
@app.cell
|
| 84 |
def _():
|
| 85 |
+
from altair.datasets import data # import vega_datasets
|
| 86 |
+
cars = data.cars() # load cars data as a Pandas data frame
|
| 87 |
+
cars.head() # display the first five rows
|
| 88 |
return cars, data
|
| 89 |
|
| 90 |
|
| 91 |
@app.cell(hide_code=True)
|
| 92 |
def _(mo):
|
| 93 |
mo.md(r"""
|
| 94 |
+
Altair's datasets can also be accessed via URLs:
|
| 95 |
""")
|
| 96 |
return
|
| 97 |
|
altair/02_marks_encoding.py
CHANGED
|
@@ -4,7 +4,6 @@
|
|
| 4 |
# "altair==6.0.0",
|
| 5 |
# "marimo",
|
| 6 |
# "pandas==3.0.1",
|
| 7 |
-
# "vega_datasets==0.9.0",
|
| 8 |
# ]
|
| 9 |
# ///
|
| 10 |
|
|
@@ -62,15 +61,15 @@ def _(mo):
|
|
| 62 |
mo.md(r"""
|
| 63 |
We will be visualizing global health and population data for a number of countries, over the time period of 1955 to 2005. The data was collected by the [Gapminder Foundation](https://www.gapminder.org/) and shared in [Hans Rosling's popular TED talk](https://www.youtube.com/watch?v=hVimVzgtD6w). If you haven't seen the talk, we encourage you to watch it first!
|
| 64 |
|
| 65 |
-
Let's first load the dataset
|
| 66 |
""")
|
| 67 |
return
|
| 68 |
|
| 69 |
|
| 70 |
@app.cell
|
| 71 |
def _():
|
| 72 |
-
from
|
| 73 |
-
data =
|
| 74 |
return (data,)
|
| 75 |
|
| 76 |
|
|
|
|
| 4 |
# "altair==6.0.0",
|
| 5 |
# "marimo",
|
| 6 |
# "pandas==3.0.1",
|
|
|
|
| 7 |
# ]
|
| 8 |
# ///
|
| 9 |
|
|
|
|
| 61 |
mo.md(r"""
|
| 62 |
We will be visualizing global health and population data for a number of countries, over the time period of 1955 to 2005. The data was collected by the [Gapminder Foundation](https://www.gapminder.org/) and shared in [Hans Rosling's popular TED talk](https://www.youtube.com/watch?v=hVimVzgtD6w). If you haven't seen the talk, we encourage you to watch it first!
|
| 63 |
|
| 64 |
+
Let's first load the dataset into a Pandas data frame.
|
| 65 |
""")
|
| 66 |
return
|
| 67 |
|
| 68 |
|
| 69 |
@app.cell
|
| 70 |
def _():
|
| 71 |
+
from altair.datasets import data as altair_data
|
| 72 |
+
data = altair_data.gapminder()
|
| 73 |
return (data,)
|
| 74 |
|
| 75 |
|
altair/03_data_transformation.py
CHANGED
|
@@ -55,7 +55,7 @@ def _(mo):
|
|
| 55 |
mo.md(r"""
|
| 56 |
We will be working with a table of data about motion pictures, taken from the [vega-datasets](https://vega.github.io/vega-datasets/) collection. The data includes variables such as the film name, director, genre, release date, ratings, and gross revenues. However, _be careful when working with this data_: the films are from unevenly sampled years, using data combined from multiple sources. If you dig in you will find issues with missing values and even some subtle errors! Nevertheless, the data should prove interesting to explore...
|
| 57 |
|
| 58 |
-
Let's retrieve the URL for the JSON data file from the
|
| 59 |
""")
|
| 60 |
return
|
| 61 |
|
|
|
|
| 55 |
mo.md(r"""
|
| 56 |
We will be working with a table of data about motion pictures, taken from the [vega-datasets](https://vega.github.io/vega-datasets/) collection. The data includes variables such as the film name, director, genre, release date, ratings, and gross revenues. However, _be careful when working with this data_: the films are from unevenly sampled years, using data combined from multiple sources. If you dig in you will find issues with missing values and even some subtle errors! Nevertheless, the data should prove interesting to explore...
|
| 57 |
|
| 58 |
+
Let's retrieve the URL for the JSON data file from the datasets package, and then read the data into a Pandas data frame so that we can inspect its contents.
|
| 59 |
""")
|
| 60 |
return
|
| 61 |
|
altair/05_view_composition.py
CHANGED
|
@@ -542,7 +542,7 @@ def _(mo):
|
|
| 542 |
|
| 543 |
Finally, note that horizontal and vertical concatenation can be combined. _What happens if you write something like `(temp | precip) & wind`?_
|
| 544 |
|
| 545 |
-
_Aside_: Note the importance of those parentheses... what happens if you remove them? Keep in mind that these overloaded operators are still subject to [Python's operator
|
| 546 |
|
| 547 |
As we will revisit later, concatenation operators let you combine any and all charts into a multi-view dashboard!
|
| 548 |
""")
|
|
|
|
| 542 |
|
| 543 |
Finally, note that horizontal and vertical concatenation can be combined. _What happens if you write something like `(temp | precip) & wind`?_
|
| 544 |
|
| 545 |
+
_Aside_: Note the importance of those parentheses... what happens if you remove them? Keep in mind that these overloaded operators are still subject to [Python's operator precedence rules](https://docs.python.org/3/reference/expressions.html#operator-precedence), and so vertical concatenation with `&` will take precedence over horizontal concatenation with `|`!
|
| 546 |
|
| 547 |
As we will revisit later, concatenation operators let you combine any and all charts into a multi-view dashboard!
|
| 548 |
""")
|
altair/07_cartographic.py
CHANGED
|
@@ -4,7 +4,6 @@
|
|
| 4 |
# "altair==6.0.0",
|
| 5 |
# "marimo",
|
| 6 |
# "pandas==3.0.1",
|
| 7 |
-
# "vega_datasets==0.9.0",
|
| 8 |
# ]
|
| 9 |
# ///
|
| 10 |
|
|
@@ -51,7 +50,9 @@ def _(mo):
|
|
| 51 |
def _():
|
| 52 |
import pandas as pd
|
| 53 |
import altair as alt
|
| 54 |
-
from
|
|
|
|
|
|
|
| 55 |
|
| 56 |
return alt, data
|
| 57 |
|
|
@@ -131,7 +132,8 @@ def _(data):
|
|
| 131 |
|
| 132 |
@app.cell
|
| 133 |
def _(data):
|
| 134 |
-
|
|
|
|
| 135 |
return (world_topo,)
|
| 136 |
|
| 137 |
|
|
@@ -160,7 +162,7 @@ def _(mo):
|
|
| 160 |
|
| 161 |
In the data above, the `objects` property indicates the named elements we can extract from the data: geometries for all `countries`, or a single polygon representing all `land` on Earth. Either of these can be unpacked to GeoJSON data we can then visualize.
|
| 162 |
|
| 163 |
-
As TopoJSON is a specialized format, we need to instruct Altair to parse the TopoJSON format, indicating which named
|
| 164 |
|
| 165 |
~~~ js
|
| 166 |
alt.topo_feature(world, 'countries')
|
|
@@ -563,7 +565,7 @@ def _(mo):
|
|
| 563 |
mo.md(r"""
|
| 564 |
_Which U.S. airports have the highest number of outgoing routes?_
|
| 565 |
|
| 566 |
-
Now that we can see the airports, which may wish to interact with them to better understand the structure of the air traffic network. We can add a `rule` mark layer to represent paths from `origin` airports to `destination` airports, which requires two `lookup` transforms to
|
| 567 |
|
| 568 |
_Starting from the static map above, can you build an interactive version? Feel free to skip the code below to engage with the interactive map first, and think through how you might build it on your own!_
|
| 569 |
""")
|
|
|
|
| 4 |
# "altair==6.0.0",
|
| 5 |
# "marimo",
|
| 6 |
# "pandas==3.0.1",
|
|
|
|
| 7 |
# ]
|
| 8 |
# ///
|
| 9 |
|
|
|
|
| 50 |
def _():
|
| 51 |
import pandas as pd
|
| 52 |
import altair as alt
|
| 53 |
+
from altair.datasets import data
|
| 54 |
+
import json
|
| 55 |
+
import urllib.request
|
| 56 |
|
| 57 |
return alt, data
|
| 58 |
|
|
|
|
| 132 |
|
| 133 |
@app.cell
|
| 134 |
def _(data):
|
| 135 |
+
with urllib.request.urlopen(world) as response:
|
| 136 |
+
world_topo = json.load(response)
|
| 137 |
return (world_topo,)
|
| 138 |
|
| 139 |
|
|
|
|
| 162 |
|
| 163 |
In the data above, the `objects` property indicates the named elements we can extract from the data: geometries for all `countries`, or a single polygon representing all `land` on Earth. Either of these can be unpacked to GeoJSON data we can then visualize.
|
| 164 |
|
| 165 |
+
As TopoJSON is a specialized format, we need to instruct Altair to parse the TopoJSON format, indicating which named feature object we wish to extract from the topology. The following code indicates that we want to extract GeoJSON features from the `world` dataset for the `countries` object:
|
| 166 |
|
| 167 |
~~~ js
|
| 168 |
alt.topo_feature(world, 'countries')
|
|
|
|
| 565 |
mo.md(r"""
|
| 566 |
_Which U.S. airports have the highest number of outgoing routes?_
|
| 567 |
|
| 568 |
+
Now that we can see the airports, which may wish to interact with them to better understand the structure of the air traffic network. We can add a `rule` mark layer to represent paths from `origin` airports to `destination` airports, which requires two `lookup` transforms to retrieve coordinates for each end point. In addition, we can use a `single` selection to filter these routes, such that only the routes originating at the currently selected airport are shown.
|
| 569 |
|
| 570 |
_Starting from the static map above, can you build an interactive version? Feel free to skip the code below to engage with the interactive map first, and think through how you might build it on your own!_
|
| 571 |
""")
|
altair/08_debugging.py
CHANGED
|
@@ -4,7 +4,6 @@
|
|
| 4 |
# "altair==6.0.0",
|
| 5 |
# "marimo",
|
| 6 |
# "pandas==3.0.1",
|
| 7 |
-
# "vega_datasets==0.9.0",
|
| 8 |
# ]
|
| 9 |
# ///
|
| 10 |
|
|
@@ -57,18 +56,18 @@ def _(mo):
|
|
| 57 |
mo.md(r"""
|
| 58 |
These instructions follow [the Altair documentation](https://altair-viz.github.io/getting_started/installation.html) but focus on some specifics for this series of notebooks.
|
| 59 |
|
| 60 |
-
In every notebook, we will import the [Altair](https://github.com/altair-viz/altair)
|
| 61 |
|
| 62 |
If you are running in Jupyter Lab or Jupyter Notebooks, you have to install the necessary packages by running the following command in your terminal.
|
| 63 |
|
| 64 |
```bash
|
| 65 |
-
pip install altair
|
| 66 |
```
|
| 67 |
|
| 68 |
Or if you use [Conda](https://conda.io)
|
| 69 |
|
| 70 |
```bash
|
| 71 |
-
conda install -c conda-forge altair
|
| 72 |
```
|
| 73 |
|
| 74 |
You can run command line commands from a code cell by prefixing it with `!`. For example, to install Altair and Vega Datasets with [Pip](https://pip.pypa.io/), you can run the following cell.
|
|
@@ -78,14 +77,14 @@ def _(mo):
|
|
| 78 |
|
| 79 |
@app.cell
|
| 80 |
def _():
|
| 81 |
-
# packages added via marimo's package management: altair
|
| 82 |
return
|
| 83 |
|
| 84 |
|
| 85 |
@app.cell
|
| 86 |
def _():
|
| 87 |
import altair as alt
|
| 88 |
-
from
|
| 89 |
|
| 90 |
return alt, data
|
| 91 |
|
|
@@ -134,7 +133,7 @@ def _(mo):
|
|
| 134 |
If you are not running the latest version, you can update it with `pip`. You can update Altair and Vega Datasets by running this command in your terminal.
|
| 135 |
|
| 136 |
```
|
| 137 |
-
pip install -U altair
|
| 138 |
```
|
| 139 |
""")
|
| 140 |
return
|
|
@@ -247,7 +246,7 @@ def _(alt):
|
|
| 247 |
@app.cell(hide_code=True)
|
| 248 |
def _(mo):
|
| 249 |
mo.md(r"""
|
| 250 |
-
Check the spelling of your files and print the data source to confirm that the data and fields exist. For instance, here you see that `color` is not a
|
| 251 |
""")
|
| 252 |
return
|
| 253 |
|
|
|
|
| 4 |
# "altair==6.0.0",
|
| 5 |
# "marimo",
|
| 6 |
# "pandas==3.0.1",
|
|
|
|
| 7 |
# ]
|
| 8 |
# ///
|
| 9 |
|
|
|
|
| 56 |
mo.md(r"""
|
| 57 |
These instructions follow [the Altair documentation](https://altair-viz.github.io/getting_started/installation.html) but focus on some specifics for this series of notebooks.
|
| 58 |
|
| 59 |
+
In every notebook, we will import the [Altair](https://github.com/altair-viz/altair) package. If you are running this notebook on [Colab](https://colab.research.google.com), Altair should be preinstalled and ready to go. The notebooks in this series are designed for Colab but should also work in Jupyter Lab or the Jupyter Notebook (the notebook requires a bit more setup [described below](#Special-Setup-for-the-Jupyter-Notebook)) but additional packages are required.
|
| 60 |
|
| 61 |
If you are running in Jupyter Lab or Jupyter Notebooks, you have to install the necessary packages by running the following command in your terminal.
|
| 62 |
|
| 63 |
```bash
|
| 64 |
+
pip install altair
|
| 65 |
```
|
| 66 |
|
| 67 |
Or if you use [Conda](https://conda.io)
|
| 68 |
|
| 69 |
```bash
|
| 70 |
+
conda install -c conda-forge altair
|
| 71 |
```
|
| 72 |
|
| 73 |
You can run command line commands from a code cell by prefixing it with `!`. For example, to install Altair and Vega Datasets with [Pip](https://pip.pypa.io/), you can run the following cell.
|
|
|
|
| 77 |
|
| 78 |
@app.cell
|
| 79 |
def _():
|
| 80 |
+
# packages added via marimo's package management: altair !pip install altair
|
| 81 |
return
|
| 82 |
|
| 83 |
|
| 84 |
@app.cell
|
| 85 |
def _():
|
| 86 |
import altair as alt
|
| 87 |
+
from altair.datasets import data
|
| 88 |
|
| 89 |
return alt, data
|
| 90 |
|
|
|
|
| 133 |
If you are not running the latest version, you can update it with `pip`. You can update Altair and Vega Datasets by running this command in your terminal.
|
| 134 |
|
| 135 |
```
|
| 136 |
+
pip install -U altair
|
| 137 |
```
|
| 138 |
""")
|
| 139 |
return
|
|
|
|
| 246 |
@app.cell(hide_code=True)
|
| 247 |
def _(mo):
|
| 248 |
mo.md(r"""
|
| 249 |
+
Check the spelling of your files and print the data source to confirm that the data and fields exist. For instance, here you see that `color` is not a valid field.
|
| 250 |
""")
|
| 251 |
return
|
| 252 |
|
altair/altair_introduction.py.lock
DELETED
|
The diff for this file is too large to render.
See raw diff
|
|
|
optimization/01_least_squares.py
CHANGED
|
@@ -1,7 +1,8 @@
|
|
| 1 |
# /// script
|
| 2 |
# requires-python = ">=3.11"
|
| 3 |
# dependencies = [
|
| 4 |
-
# "
|
|
|
|
| 5 |
# "marimo",
|
| 6 |
# "numpy==2.4.3",
|
| 7 |
# ]
|
|
|
|
| 1 |
# /// script
|
| 2 |
# requires-python = ">=3.11"
|
| 3 |
# dependencies = [
|
| 4 |
+
# "clarabel>=0.11.1",
|
| 5 |
+
# "cvxpy-base>=1.8.2",
|
| 6 |
# "marimo",
|
| 7 |
# "numpy==2.4.3",
|
| 8 |
# ]
|
optimization/04_quadratic_program.py
CHANGED
|
@@ -1,7 +1,8 @@
|
|
| 1 |
# /// script
|
| 2 |
# requires-python = ">=3.13"
|
| 3 |
# dependencies = [
|
| 4 |
-
# "
|
|
|
|
| 5 |
# "marimo",
|
| 6 |
# "matplotlib==3.10.8",
|
| 7 |
# "numpy==2.4.3",
|
|
|
|
| 1 |
# /// script
|
| 2 |
# requires-python = ">=3.13"
|
| 3 |
# dependencies = [
|
| 4 |
+
# "clarabel>=0.11.1",
|
| 5 |
+
# "cvxpy-base>=1.8.2",
|
| 6 |
# "marimo",
|
| 7 |
# "matplotlib==3.10.8",
|
| 8 |
# "numpy==2.4.3",
|
optimization/05_portfolio_optimization.py
CHANGED
|
@@ -1,7 +1,8 @@
|
|
| 1 |
# /// script
|
| 2 |
# requires-python = ">=3.13"
|
| 3 |
# dependencies = [
|
| 4 |
-
# "
|
|
|
|
| 5 |
# "marimo",
|
| 6 |
# "matplotlib==3.10.8",
|
| 7 |
# "numpy==2.4.3",
|
|
|
|
| 1 |
# /// script
|
| 2 |
# requires-python = ">=3.13"
|
| 3 |
# dependencies = [
|
| 4 |
+
# "clarabel>=0.11.1",
|
| 5 |
+
# "cvxpy-base>=1.8.2",
|
| 6 |
# "marimo",
|
| 7 |
# "matplotlib==3.10.8",
|
| 8 |
# "numpy==2.4.3",
|
optimization/06_convex_optimization.py
CHANGED
|
@@ -1,7 +1,8 @@
|
|
| 1 |
# /// script
|
| 2 |
# requires-python = ">=3.13"
|
| 3 |
# dependencies = [
|
| 4 |
-
# "
|
|
|
|
| 5 |
# "marimo",
|
| 6 |
# "numpy==2.4.3",
|
| 7 |
# ]
|
|
|
|
| 1 |
# /// script
|
| 2 |
# requires-python = ">=3.13"
|
| 3 |
# dependencies = [
|
| 4 |
+
# "clarabel>=0.11.1",
|
| 5 |
+
# "cvxpy-base>=1.8.2",
|
| 6 |
# "marimo",
|
| 7 |
# "numpy==2.4.3",
|
| 8 |
# ]
|
optimization/07_sdp.py
CHANGED
|
@@ -1,7 +1,8 @@
|
|
| 1 |
# /// script
|
| 2 |
# requires-python = ">=3.13"
|
| 3 |
# dependencies = [
|
| 4 |
-
# "
|
|
|
|
| 5 |
# "marimo",
|
| 6 |
# "numpy==2.4.3",
|
| 7 |
# "wigglystuff==0.2.37",
|
|
|
|
| 1 |
# /// script
|
| 2 |
# requires-python = ">=3.13"
|
| 3 |
# dependencies = [
|
| 4 |
+
# "clarabel>=0.11.1",
|
| 5 |
+
# "cvxpy>=1.8.2",
|
| 6 |
# "marimo",
|
| 7 |
# "numpy==2.4.3",
|
| 8 |
# "wigglystuff==0.2.37",
|
pages/educators.md
CHANGED
|
@@ -1,362 +1,208 @@
|
|
| 1 |
-
|
| 2 |
-
title: marimo for Educators
|
| 3 |
-
---
|
| 4 |
|
| 5 |
## Introduction
|
| 6 |
|
| 7 |
-
|
| 8 |
-
- *literate programming* mixes prose and software in a single "runnable paper"
|
| 9 |
-
- each *cell* is prose or software
|
| 10 |
-
- prose typically written in Markdown
|
| 11 |
-
- software written in whatever programming languages the notebook supports
|
| 12 |
-
- software's output displayed in the notebook as well
|
| 13 |
-
- why notebooks for everyday work?
|
| 14 |
-
- easier to understand (think about the way textbooks present material)
|
| 15 |
-
- improves reproducibility
|
| 16 |
-
- [GVW: if we emphasize embedded AI] keep track of what you asked for as well as what you did
|
| 17 |
-
- why notebooks for learning?
|
| 18 |
-
- more engaging than static material: learners are active users of material, not passive consumers, can experiment with settings, alter code, etc.
|
| 19 |
-
- no installation required: notebooks can be hosted so learners don't have to struggle with the hard bits first (i.e., focus on learning rather than on the tool)
|
| 20 |
-
- reproducibility helps collaboration as well [GVW: but we don’t support concurrent editing a la Google Docs, which some people will regard as table stakes]
|
| 21 |
-
- less intimidating than jumping straight into scripting
|
| 22 |
-
- introduces a real-world tool
|
| 23 |
-
- [if we emphasize embedded AI] a natural way to bring LLMs into the classroom
|
| 24 |
-
- why notebooks for teaching?
|
| 25 |
-
- all of the above…
|
| 26 |
-
- create interactive lecture material in a single place
|
| 27 |
-
- why the marimo notebook?
|
| 28 |
-
- open source
|
| 29 |
-
- more than Notebook but not as intimidating as VS Code
|
| 30 |
-
- reactivity allows for (encourages) dynamic, interactive elements
|
| 31 |
-
- marimo is both a notebook and a library of UI elements
|
| 32 |
-
- and [AnyWidget](https://anywidget.dev/) makes it relatively easy to extend [GVW: point at [faw](https://github.com/gvwilson/faw)]
|
| 33 |
-
- doesn't allow out-of-order execution of cells, which reduces “worked for me” complaints
|
| 34 |
-
- plays nicely with other Python tools (because a notebook is a Python file)
|
| 35 |
-
- plays nicely with version control (same reason)
|
| 36 |
-
- helps instructors keep their prose and examples in sync
|
| 37 |
-
- configurable interaction with AI tools
|
| 38 |
-
- [if we emphasize embedded AI] natural way to teaching prompting and review
|
| 39 |
-
- why *not* marimo?
|
| 40 |
-
- not yet as widely known as Jupyter (i.e., your IT department may not already support it)
|
| 41 |
-
- not yet integrated with auto-grading tools ([faw](https://github.com/gvwilson/faw) is a start, but we're waiting to see what you want)
|
| 42 |
-
- doesn't yet support multi-notebook books
|
| 43 |
-
- some quirks that might not make it the right tool for a CS-101 course (see below)
|
| 44 |
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
- high level
|
| 48 |
-
- follow along with lesson (code already present)
|
| 49 |
-
- workbooks for assignments ("fill in these cells")
|
| 50 |
-
- notebooks as apps (play with data rather than write code)
|
| 51 |
-
- notebooks as lab reports (models real-world use)
|
| 52 |
-
- micro
|
| 53 |
-
- scroll through a pre-executed notebook
|
| 54 |
-
- step through a notebook by executing the cells in order
|
| 55 |
-
- fill out details or values into a mostly complete notebook
|
| 56 |
-
- tweak or fill in a notebook with some content
|
| 57 |
-
- add content to a completely blank notebook
|
| 58 |
-
- ask learners what to add *or* what's going to happen
|
| 59 |
-
- ask AI to do something and then explore/correct/improve its output
|
| 60 |
-
|
| 61 |
-
## Things to Watch Out For
|
| 62 |
-
|
| 63 |
-
- Variable names
|
| 64 |
-
- Underscored variable names are different from common usage, and require some understanding of scope
|
| 65 |
-
- Solution is functions-early teaching methodology, which has a sound pedagogical basis
|
| 66 |
-
- Image files
|
| 67 |
-
- For security reasons, marimo requires local image files to be in a folder called `public` below the directory the notebook is run from, and to be accessed in Markdown as `[alt text](/public/image.ext)`
|
| 68 |
-
- Which means it’s important to launch the notebook from the right place
|
| 69 |
-
- Can get around this using `mo.image` but that can’t be embedded in Markdown
|
| 70 |
-
- [Using pytest in marimo](https://docs.marimo.io/guides/testing/pytest/#testing-in-notebook) is straightforward as long as the cell *only* contains tests
|
| 71 |
-
- marimo uses [KaTeX](https://katex.org/) rather than [MathJax](https://www.mathjax.org/) for rendering math - see the appendix to this document for notes
|
| 72 |
-
|
| 73 |
-
## Pedagogical Patterns
|
| 74 |
-
|
| 75 |
-
### Shift-Enter
|
| 76 |
-
|
| 77 |
-
**Description:** Learner starts with complete notebook, re-executes cells; (possibly) fills in prose cells with analysis/description
|
| 78 |
-
|
| 79 |
-
**Use For:** Introduce new topics; check understanding (e.g., warmup exercise)
|
| 80 |
-
|
| 81 |
-
**Works For:** Any audience
|
| 82 |
-
|
| 83 |
-
**Format:** Synchronous
|
| 84 |
-
|
| 85 |
-
**Pro:** Gives learners a complete working example
|
| 86 |
-
|
| 87 |
-
**Con:** Low engagement
|
| 88 |
-
|
| 89 |
-
### Fill in the blanks
|
| 90 |
-
|
| 91 |
-
**Description:** Some code cells filled in, learner must complete
|
| 92 |
-
|
| 93 |
-
**Use For:** Reducing cognitive load
|
| 94 |
|
| 95 |
-
|
| 96 |
|
| 97 |
-
|
| 98 |
|
| 99 |
-
|
| 100 |
|
| 101 |
-
|
| 102 |
|
| 103 |
-
|
| 104 |
|
| 105 |
-
**
|
| 106 |
|
| 107 |
-
|
| 108 |
|
| 109 |
-
|
| 110 |
|
| 111 |
-
|
| 112 |
|
| 113 |
-
|
| 114 |
|
| 115 |
-
|
| 116 |
|
| 117 |
-
##
|
| 118 |
-
|
| 119 |
-
**Description:** Use notebook as interactive dashboard (note: usually keep prose in a separate document to make the dashboard look like an app)
|
| 120 |
-
|
| 121 |
-
**Use For:** Exploring datasets
|
| 122 |
-
|
| 123 |
-
**Works For:** Non-programmers
|
| 124 |
-
|
| 125 |
-
**Format:** Use instead of slides (but must know where you’re going); have learners suggest alternatives to explore; data analysis after (physical) lab experiment
|
| 126 |
-
|
| 127 |
-
**Pro:** Less effort to build than custom UI
|
| 128 |
-
|
| 129 |
-
**Con:** Requires testing; does not develop programming skills
|
| 130 |
|
| 131 |
-
|
| 132 |
|
| 133 |
-
|
| 134 |
|
| 135 |
-
|
| 136 |
|
| 137 |
-
|
| 138 |
|
| 139 |
-
|
| 140 |
|
| 141 |
-
|
| 142 |
|
| 143 |
-
|
|
|
|
| 144 |
|
| 145 |
-
|
|
|
|
| 146 |
|
| 147 |
-
|
|
|
|
| 148 |
|
| 149 |
-
|
|
|
|
| 150 |
|
| 151 |
-
|
| 152 |
|
| 153 |
-
|
|
|
|
|
|
|
| 154 |
|
| 155 |
-
|
| 156 |
|
| 157 |
-
|
| 158 |
|
| 159 |
-
###
|
| 160 |
|
| 161 |
-
|
| 162 |
|
| 163 |
-
|
| 164 |
|
| 165 |
-
|
| 166 |
|
| 167 |
-
|
| 168 |
|
| 169 |
-
|
| 170 |
|
| 171 |
-
|
| 172 |
|
| 173 |
-
|
| 174 |
|
| 175 |
-
|
| 176 |
|
| 177 |
-
|
| 178 |
|
| 179 |
-
|
| 180 |
|
| 181 |
-
|
| 182 |
|
| 183 |
-
|
| 184 |
|
| 185 |
-
|
| 186 |
|
| 187 |
### Learn an API
|
| 188 |
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
**Use For:** Put focus on tools to be used in other places / lessons
|
| 192 |
-
|
| 193 |
-
**Works For:** Learners with some programming skill (and patience)
|
| 194 |
-
|
| 195 |
-
**Format:** Examples in order of increasing complexity or decreasing frequency of use
|
| 196 |
-
|
| 197 |
-
**Pro:** Guide learning in a sensible order (which AI sometimes struggles with)
|
| 198 |
-
|
| 199 |
-
**Con:** “Can’t see the forest for the trees”; learners may prefer just asking AI as needed
|
| 200 |
-
|
| 201 |
-
### Choose your data
|
| 202 |
-
|
| 203 |
-
**Description:** Replace the dataset used in a notebook with another one (which may require some modifications to code)
|
| 204 |
|
| 205 |
-
|
| 206 |
|
| 207 |
-
|
| 208 |
|
| 209 |
-
|
| 210 |
|
| 211 |
-
|
| 212 |
|
| 213 |
-
|
| 214 |
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
**Works For:** Learners who want firm goalposts
|
| 222 |
-
|
| 223 |
-
**Format:** Notebook full of test cases with empty cells (and function stubs) for code; works well for homework exercises
|
| 224 |
-
|
| 225 |
-
**Pro:** Helps learners stay focused on well-defined task
|
| 226 |
-
|
| 227 |
-
**Con:** Very easy to have AI generate the code without understanding it
|
| 228 |
-
|
| 229 |
-
### Bug hunt
|
| 230 |
-
|
| 231 |
-
**Description:** Give learners a notebook with one or more bugs (which can include misleading prose)
|
| 232 |
-
|
| 233 |
-
**Use For:** Developing critical reading skills (especially important for learners using AI)
|
| 234 |
-
|
| 235 |
-
**Works For:** Learners with enough programming experience to be able to debug systematically
|
| 236 |
-
|
| 237 |
-
**Format:** Works well as homework exercise
|
| 238 |
-
|
| 239 |
-
**Pro:** Some learners enjoy playing detective; extremely useful skill to learn
|
| 240 |
-
|
| 241 |
-
**Con:** Hard to calibrate bug difficulty to learner level; hard for learners to know when they’re done
|
| 242 |
-
|
| 243 |
-
### Adversarial programming
|
| 244 |
-
|
| 245 |
-
**Description:** Given a notebook full of code, write tests that break it (reverse of bug hunt)
|
| 246 |
|
| 247 |
-
|
| 248 |
|
| 249 |
-
|
|
|
|
|
|
|
| 250 |
|
| 251 |
-
|
|
|
|
|
|
|
| 252 |
|
| 253 |
-
|
| 254 |
|
| 255 |
-
|
| 256 |
|
| 257 |
-
##
|
| 258 |
|
| 259 |
-
|
| 260 |
-
[*Teaching and Learning with Jupyter*](https://jupyter4edu.github.io/jupyter-edu-book/).
|
| 261 |
|
| 262 |
## Appendix: Learner Personas
|
| 263 |
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
**Background:** Biology professor at mid-sized state university; teaches undergrad microbiology and biostatistics classes, both of which emphasize data management and visualization.
|
| 267 |
-
|
| 268 |
-
**Relevant Experience:** Used R for 15 years, switched to Python three years ago, mostly self-taught. Frequently remixes teaching material she finds online, particularly examples.
|
| 269 |
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
1. Wants to equip her students with modern skills, especially AI-related, both because she thinks they’re important and to increase student engagement.
|
| 273 |
-
|
| 274 |
-
2. Wants more recognition at her university for her teaching work, which she believes is more likely to come from publishable innovation than from high student evaluations.
|
| 275 |
-
|
| 276 |
-
3. Would like to get student engagement back to pre-COVID levels; she feels that today’s cohorts don’t know each other as well and aren’t as excited about material because of the shift to online education.
|
| 277 |
|
| 278 |
-
|
| 279 |
|
| 280 |
-
|
| 281 |
|
| 282 |
-
|
| 283 |
|
| 284 |
### Ellis Engineer
|
| 285 |
|
| 286 |
-
|
| 287 |
|
| 288 |
-
|
| 289 |
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
1. Ellis wants to create an impressive senior project to secure themself a place in a good graduate program (which they think is essential to doing interesting work with drones). They have seen custom widgets in notebooks, and are willing to invest some time to learn how to build one with AI support.
|
| 293 |
-
|
| 294 |
-
2. They also want to explore small-craft aerodynamics, particularly feedback stability problems, out of personal interest and as a way to become part of the “serious” drone community.
|
| 295 |
-
|
| 296 |
-
**Complications**
|
| 297 |
-
|
| 298 |
-
Having spent several months convinced that Lisp was the language of the future, Ellis is leery of investing too much in new technologies just because they’re cool.
|
| 299 |
|
| 300 |
### Nang Newbie
|
| 301 |
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
**Relevant Experience:** Used Scratch in middle school and did one CS class in high school that covered HTML and a bit of Python. He just finished an intro stats class that used Pandas, which to his surprise he enjoyed enough to sign up for the sequel.
|
| 305 |
-
|
| 306 |
-
**Goals**
|
| 307 |
-
|
| 308 |
-
1. Nang wants to be able to do homework assignments more quickly and with less effort (hence his interest in ChatGPT).
|
| 309 |
-
|
| 310 |
-
2. He wants to learn how to explore and analyze sports statistics for fun (he's a keen basketball fan), and to share what he finds with like-minded fans through online forums.
|
| 311 |
|
| 312 |
-
|
| 313 |
|
| 314 |
-
Nang is taking five courses and volunteering with two campus clubs
|
| 315 |
|
| 316 |
## Appendix: KaTeX vs. MathJax
|
| 317 |
|
| 318 |
-
marimo uses [KaTeX](https://katex.org/) for rendering math
|
| 319 |
|
| 320 |
-
### Use
|
| 321 |
|
| 322 |
LaTeX lives in Python strings in marimo, so use `r"..."` to preserve backslashes:
|
| 323 |
|
| 324 |
```python
|
| 325 |
-
mo.md(r"$\
|
| 326 |
-
mo.md("$\
|
| 327 |
```
|
| 328 |
|
| 329 |
-
### MathJax
|
| 330 |
|
| 331 |
| Category | MathJax | KaTeX |
|
| 332 |
| --- | --- | --- |
|
| 333 |
-
| Text | `\
|
| 334 |
-
| Text style | `\
|
| 335 |
-
| Environments | `\
|
| 336 |
-
| | `\
|
| 337 |
-
| References | `\
|
| 338 |
-
| Arrays | `\
|
| 339 |
-
| Macros | `\
|
| 340 |
-
| | `\
|
| 341 |
-
| Spacing | `\
|
| 342 |
-
| Conditionals | `\
|
| 343 |
|
| 344 |
-
|
| 345 |
|
| 346 |
-
### Shared
|
| 347 |
|
| 348 |
-
`\
|
| 349 |
|
| 350 |
-
### Migration
|
| 351 |
|
| 352 |
-
1. Find
|
| 353 |
-
2.
|
| 354 |
-
3. Replace `\
|
| 355 |
-
4. Replace `\
|
| 356 |
-
5. Remove `\
|
| 357 |
-
6.
|
| 358 |
|
| 359 |
### References
|
| 360 |
|
| 361 |
- [KaTeX Support Table](https://katex.org/docs/support_table) — definitive command lookup
|
| 362 |
-
- [KaTeX Unsupported Features](https://github.com/KaTeX/KaTeX/wiki/Things-that-KaTeX-does-not-(yet)-support)
|
|
|
|
| 1 |
+
marimo for Educators
|
|
|
|
|
|
|
| 2 |
|
| 3 |
## Introduction
|
| 4 |
|
| 5 |
+
A computational notebook is a form of *literate programming* that mixes prose and software in a single runnable document. Each *cell* contains either prose written in Markdown or code written in a supported programming language, and the output of any code cell is displayed directly in the notebook alongside the source.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
|
| 7 |
+
Notebooks make everyday work easier to understand because they present explanation and evidence together, just like people do in conversation. This format also improves reproducibility, since the code that produced a result lives beside the result itself.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
|
| 9 |
+
For learning, notebooks offer several advantages over static material. Learners become active participants rather than passive readers: they can experiment with settings, alter code, and see immediate results. Because notebooks can be hosted, learners do not need to install anything before they start, which means the first session can focus on the subject itself rather than on tooling. The format is less intimidating than jumping straight into a script editor, and it introduces learners to a real-world tool used in research and industry.
|
| 10 |
|
| 11 |
+
For teaching, notebooks serve all those purposes while also letting instructors keep interactive lecture material in one place.
|
| 12 |
|
| 13 |
+
### Why marimo?
|
| 14 |
|
| 15 |
+
marimo is open source and occupies a useful middle ground: richer than a plain text editor but less overwhelming than a full IDE such as VS Code. Its reactivity encourages dynamic, interactive elements, because marimo is both a notebook environment and a library of UI components. The [AnyWidget](https://anywidget.dev/) protocol makes it relatively straightforward to extend marimo with custom widgets.
|
| 16 |
|
| 17 |
+
marimo does not allow out-of-order cell execution, which eliminates a common class of "it worked on my machine" complaints. And since a marimo notebook is a valid Python file, it integrates naturally with other Python tools and with version control. Instructors benefit from having prose and code examples in the same file, which helps keep explanations in sync with the code they describe. marimo also offers configurable integration with AI tools.
|
| 18 |
|
| 19 |
+
### Why *Not* marimo?
|
| 20 |
|
| 21 |
+
marimo is not yet as widely known as Jupyter, so your institution's IT department may not already support it. Auto-grading integration is not yet available in a mature form (but we're working on it), and multi-chapter books are not yet supported. Some quirks in how marimo handles scope may make it a less natural fit for an introductory CS course; see "Things to Watch Out For" below.
|
| 22 |
|
| 23 |
+
## molab
|
| 24 |
|
| 25 |
+
molab is marimo's free cloud-hosted notebook service, available at [molab.marimo.io](https://molab.marimo.io/notebooks). It is the easiest way for students to get started because it requires no local installation: marimo is accessible as a self-contained web application, comparable in experience to Google Colab. Notebooks created on molab are public but not discoverable by default, and can be shared with others by URL. Students can download their notebooks as `.py`, `.ipynb`, or PDF files, which makes submission to grading systems such as Gradescope straightforward.
|
| 26 |
|
| 27 |
+
molab can also preview notebooks hosted on GitHub. The service provides a stable URL for a notebook that stays current as the notebook changes, so students always see the latest version. From the preview page, students can fork the notebook into their own workspace.
|
| 28 |
|
| 29 |
+
Existing Jupyter notebooks can be converted to marimo notebooks automatically with `uvx marimo convert my_notebook.ipynb -o my_notebook.py`.
|
| 30 |
|
| 31 |
+
## Ways to Teach With marimo
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
|
| 33 |
+
At a high level, there are (at least) four ways to teach with marimo notebooks:
|
| 34 |
|
| 35 |
+
1. Learners can follow along with the lesson when the code already present in the notebook.
|
| 36 |
|
| 37 |
+
1. Notebooks can be used as assignments (i.e., "fill in these cells").
|
| 38 |
|
| 39 |
+
1. Notebooks be used as apps so that learners can explore data rather than, or as well as, writing code.
|
| 40 |
|
| 41 |
+
1. Learners can create notebooks from scratch as lab reports, which most closely models real-world use.
|
| 42 |
|
| 43 |
+
## Things to Watch Out For
|
| 44 |
|
| 45 |
+
Variable names
|
| 46 |
+
: Underscore-prefixed variable names in marimo have a meaning different from their common usage in Python and require some understanding of lexical scope. The recommended remedy is a functions-early teaching methodology, which has a sound pedagogical basis and prepares learners for idiomatic Python.
|
| 47 |
|
| 48 |
+
Image files
|
| 49 |
+
: For security reasons, marimo requires local image files to be placed in a folder named `public` directly below the directory from which the notebook is launched, and to be referenced in Markdown as `/public/image.ext`. This means it matters where the notebook is started. The `mo.image` function works around this restriction but cannot be embedded inside a Markdown string.
|
| 50 |
|
| 51 |
+
Testing
|
| 52 |
+
: [Using pytest in marimo](https://docs.marimo.io/guides/testing/pytest/#testing-in-notebook) is straightforward, provided that each test cell contains only tests and nothing else.
|
| 53 |
|
| 54 |
+
Math rendering
|
| 55 |
+
: marimo uses [KaTeX](https://katex.org/) rather than [MathJax](https://www.mathjax.org/) for rendering mathematics. The two systems are largely compatible but differ in some commands and environments. See the appendix for details.
|
| 56 |
|
| 57 |
+
## Pedagogical Patterns
|
| 58 |
|
| 59 |
+
Much of this section is inspired by or taken from
|
| 60 |
+
[*Teaching and Learning with Jupyter*](https://jupyter4edu.github.io/jupyter-edu-book/).
|
| 61 |
+
We are grateful to its authors for making their work available under an open license.
|
| 62 |
|
| 63 |
+
### Shift-Enter
|
| 64 |
|
| 65 |
+
Learners start with a complete notebook and re-execute the cells in order, optionally filling in prose cells with analysis or description. This pattern is well suited to introducing new topics or checking understanding through warmup exercises, and works with any audience in a synchronous setting. It gives learners a working example immediately, though engagement tends to be low because learners are not yet making decisions.
|
| 66 |
|
| 67 |
+
### Fill in the Blanks
|
| 68 |
|
| 69 |
+
Some code cells are provided complete; learners must complete the rest. This reduces cognitive load by directing attention to a single concept, such as filtering a dataset, and works for any audience in assignments and lab sessions. The risk is that learners delegate the task to an AI tool, and the difficulty of the blanks can be hard to calibrate for a mixed-ability group.
|
| 70 |
|
| 71 |
+
### Tweak and Twiddle
|
| 72 |
|
| 73 |
+
Learners start with a complete, working notebook and are asked to alter parameters in order to achieve a specified goal. This pattern supports compare-and-contrast exercises and the acquisition of domain knowledge. It is particularly effective for learners who have domain knowledge but little programming experience, in fixed-time workshop exercises or pair programming sessions. It helps learners overcome anxiety about code. The main difficulties are that learners may not know where to start, or may spend time following unproductive tangents.
|
| 74 |
|
| 75 |
+
### Notebook as App
|
| 76 |
|
| 77 |
+
The notebook is presented as an interactive dashboard, with prose kept in a separate document so the interface looks like a standalone application. This pattern is designed for non-programmers exploring datasets. It can replace slides in a lecture if the instructor knows the material well enough to navigate live, or it can be used after a physical lab experiment for data analysis. It requires less effort to build than a custom UI, but it demands thorough testing and does not develop learners' programming skills.
|
| 78 |
|
| 79 |
+
### Top-Down Delivery
|
| 80 |
|
| 81 |
+
Learners are given just enough control to reach a motivating result quickly. The goal is engagement on the first day of a course or workshop. This pattern works for any audience but is most effective with learners who have limited programming experience, in tutorials and synchronous workshops. Student engagement is the main advantage; the main challenge is finding the right level of detail for a group with mixed abilities.
|
| 82 |
|
| 83 |
+
### Coding as Translation
|
| 84 |
|
| 85 |
+
Learners convert prose to code, or code to prose. The purpose is to connect concepts to implementations and implementations to concepts. It is well suited to learners who understand theory but struggle with coding, or the reverse. A notebook with scaffolding text and possibly some pre-written code works well as the format. The barrier to entry is low for learners with limited programming background; the challenge, again, is calibrating difficulty for a mixed-ability group.
|
| 86 |
|
| 87 |
+
### Symbolic Math
|
| 88 |
|
| 89 |
+
Learners use SymPy to do symbolic mathematics inside the notebook, extending the coding-as-translation pattern to include converting mathematical expressions to code or code to mathematical expressions. This works well for STEM students interested in theory and fits any format. It introduces another real-world tool, but SymPy's syntax is yet another thing to learn on top of the mathematics itself.
|
| 90 |
|
| 91 |
+
### Numerical Methods and Simulation
|
| 92 |
|
| 93 |
+
Learners use calculation or simulation rather than closed-form analysis to make a concept tangible before the mathematical abstraction is introduced. This requires some programming skill and fits any format. Going from specific to general is often more engaging and approachable than the reverse, but debugging numerical code can be difficult.
|
| 94 |
|
| 95 |
### Learn an API
|
| 96 |
|
| 97 |
+
A key library or API is introduced example by example, in order of increasing complexity or decreasing frequency of use. The purpose is to direct learner attention toward tools they will use in other parts of the course. This works for learners with some programming skill and patience. It guides learning in a sensible order, which AI tools sometimes struggle to provide on their own. The risk is that learners lose sight of the larger goal, or prefer to ask an AI for help as needed rather than building systematic knowledge.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 98 |
|
| 99 |
+
### Choose Your Data
|
| 100 |
|
| 101 |
+
Learners replace the dataset in a provided notebook with one of their own choosing, possibly making some modifications to the code. The goal is engagement through personal relevance, and it works well for learners with a specific domain interest such as sports analytics. A common structure is a shared first half followed by independent exploration, sometimes leading to presentations. It improves self-efficacy, but learners may struggle to find suitable data, encounter data that is too messy to work with, or have interests that do not overlap enough for a shared debrief.
|
| 102 |
|
| 103 |
+
### Test-Driven Learning
|
| 104 |
|
| 105 |
+
The instructor provides a notebook full of tests; learners must write code that makes those tests pass. This teaches learners to think in terms of a specification. It works for learners who want firm goalposts and fits homework exercises well. The task is well-defined and easy to stay focused on, but it is very easy for learners to have an AI generate the code without understanding it.
|
| 106 |
|
| 107 |
+
A useful pattern is to place a stub function in one cell and pytest tests in a separate cell. Because of marimo's reactive execution, every time the learner edits their implementation the tests rerun automatically, giving immediate feedback on correctness without giving away the answer. For example, the stub cell might contain:
|
| 108 |
|
| 109 |
+
```python
|
| 110 |
+
def add(x, y):
|
| 111 |
+
"""Return the sum of x and y."""
|
| 112 |
+
# your code here
|
| 113 |
+
pass
|
| 114 |
+
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
|
| 116 |
+
and the test cell:
|
| 117 |
|
| 118 |
+
```python
|
| 119 |
+
def test_add_integers():
|
| 120 |
+
assert add(5, 6) == 11
|
| 121 |
|
| 122 |
+
def test_add_floats():
|
| 123 |
+
assert isinstance(add(4, 2.1), float)
|
| 124 |
+
```
|
| 125 |
|
| 126 |
+
### Bug Hunt
|
| 127 |
|
| 128 |
+
Learners are given a notebook with one or more bugs, which may include misleading prose. The purpose is to develop critical reading skills, which is especially important for learners who regularly use AI tools. It requires enough programming experience to debug systematically and works well as a homework exercise. Some learners find the detective aspect genuinely engaging, and the skill is extremely valuable. The main challenges are calibrating bug difficulty and helping learners know when they are done.
|
| 129 |
|
| 130 |
+
### Adversarial Programming
|
| 131 |
|
| 132 |
+
Given a notebook full of code, learners write tests designed to break it. This is the reverse of the bug hunt. The purpose is to develop critical thinking, and it requires the same level of programming experience. It works well as a homework exercise. It helps learners appreciate the difficulty of writing robust code and sharpens their debugging skills, but learners sometimes find repetitive ways to break the code rather than probing for distinct failure modes.
|
|
|
|
| 133 |
|
| 134 |
## Appendix: Learner Personas
|
| 135 |
|
| 136 |
+
The three profiles below outlines who we're trying to help and how.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 137 |
|
| 138 |
+
### Anya Academic
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 139 |
|
| 140 |
+
Anya is a biology professor at a mid-sized state university who teaches undergraduate microbiology and biostatistics, both of which emphasize data management and visualization. She used R for fifteen years before switching to Python three years ago, largely through self-teaching, and she routinely remixes teaching material she finds online.
|
| 141 |
|
| 142 |
+
She wants to equip her students with modern skills, including AI-related tools, both because she sees them as important and because she hopes they will increase student engagement. She is also looking for more recognition at her university for her teaching work, which she believes is more likely to come from publishable innovation than from high student evaluations. She would like to restore the level of student engagement she saw before the pandemic, which she attributes in part to the shift toward online education reducing social cohesion in her cohorts.
|
| 143 |
|
| 144 |
+
Her main concern is tool setup and maintenance overhead. She does not have time to rewrite courses wholesale, so she will only migrate to a new tool if there is an incremental path that allows her to back out if things are not working. Her department has only two IT staff, and nothing at her university can move beyond a pilot without integrating with the institution's learning management system.
|
| 145 |
|
| 146 |
### Ellis Engineer
|
| 147 |
|
| 148 |
+
Ellis is a senior undergraduate in mechanical engineering who has just returned from their third co-op placement and is very interested in drones. They used Jupyter notebooks with Google Colab in their second semester and are comfortable with NumPy and Altair, though they have done roughly as many courses in MATLAB and AutoCAD as in Python.
|
| 149 |
|
| 150 |
+
Ellis wants to build an impressive senior project to strengthen their graduate school application. They have seen custom widgets in notebooks and are willing to invest time in learning to build one with AI support. They also want to explore small-craft aerodynamics, particularly feedback stability problems, both as a personal interest and as a way to become known in the drone community.
|
| 151 |
|
| 152 |
+
Having spent a period convinced that Lisp was the language of the future, Ellis is now cautious about investing heavily in new technologies purely because they seem exciting.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 153 |
|
| 154 |
### Nang Newbie
|
| 155 |
|
| 156 |
+
Nang is an undergraduate business student who decided against minoring in computer science because he believes AI will take most programming jobs. He chooses courses, tools, and interests largely based on what he reads about future employer demand, and he routinely uses ChatGPT for help with homework. He used Scratch in middle school and covered HTML and basic Python in a high school CS course. He just finished an introductory statistics course that used Pandas, which he enjoyed enough to sign up for the follow-on course.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 157 |
|
| 158 |
+
He wants to complete assignments more quickly and with less effort. He also wants to learn how to explore and analyze sports statistics for fun, as a keen basketball fan, and to share findings with other fans through online forums.
|
| 159 |
|
| 160 |
+
Nang is taking five courses and volunteering with two campus clubs, one for his CV and one out of genuine passion for basketball, so he is chronically over-committed.
|
| 161 |
|
| 162 |
## Appendix: KaTeX vs. MathJax
|
| 163 |
|
| 164 |
+
marimo uses [KaTeX](https://katex.org/) for rendering math rather than [MathJax](https://www.mathjax.org/). KaTeX is faster and has slightly narrower coverage, and it fails silently when it encounters an unsupported command.
|
| 165 |
|
| 166 |
+
### Use Raw Strings
|
| 167 |
|
| 168 |
LaTeX lives in Python strings in marimo, so use `r"..."` to preserve backslashes:
|
| 169 |
|
| 170 |
```python
|
| 171 |
+
mo.md(r"$\frac{1}{2}$") # correct
|
| 172 |
+
mo.md("$\frac{1}{2}$") # wrong: \f is a form-feed character
|
| 173 |
```
|
| 174 |
|
| 175 |
+
### MathJax to KaTeX
|
| 176 |
|
| 177 |
| Category | MathJax | KaTeX |
|
| 178 |
| --- | --- | --- |
|
| 179 |
+
| Text | `\mbox`, `\bbox` | `\text{}` |
|
| 180 |
+
| Text style | `\textsc`, `\textsl` | `\text{}` |
|
| 181 |
+
| Environments | `\begin{eqnarray}` | `\begin{align}` |
|
| 182 |
+
| | `\begin{multline}` | `\begin{gather}` |
|
| 183 |
+
| References | `\label`, `\eqref`, `\ref` | `\tag{}` for manual numbering |
|
| 184 |
+
| Arrays | `\cline`, `\multicolumn`, `\hfill`, `\vline` | not supported |
|
| 185 |
+
| Macros | `\DeclareMathOperator` | `\operatorname{}` inline |
|
| 186 |
+
| | `\newenvironment` | not supported |
|
| 187 |
+
| Spacing | `\mspace`, `\setlength`, `\strut`, `\rotatebox` | not supported |
|
| 188 |
+
| Conditionals | `\if`, `\else`, `\fi`, `\ifx` | not supported |
|
| 189 |
|
| 190 |
+
The following commands do work in KaTeX despite claims to the contrary in some older references: `\newcommand`, `\def`, `\hbox`, `\hskip`, `\cal`, `\pmb`, `\begin{equation}`, `\begin{split}`, `\operatorname*`.
|
| 191 |
|
| 192 |
+
### Shared Macros Across Cells
|
| 193 |
|
| 194 |
+
`\newcommand` works inline within a single cell. For macros that need to be available across multiple cells, use `mo.latex(filename="macros.tex")` in the same cell as your `import marimo` statement.
|
| 195 |
|
| 196 |
+
### Migration Checklist
|
| 197 |
|
| 198 |
+
1. Find and replace `\mbox{` with `\text{`.
|
| 199 |
+
2. Wrap all LaTeX strings in raw string literals (`r"..."`).
|
| 200 |
+
3. Replace `\begin{eqnarray}` with `\begin{align}`.
|
| 201 |
+
4. Replace `\DeclareMathOperator` with `\operatorname{}`.
|
| 202 |
+
5. Remove `\label` and `\eqref`; use `\tag{}` where manual numbering is needed.
|
| 203 |
+
6. Verify the output visually, since KaTeX fails silently.
|
| 204 |
|
| 205 |
### References
|
| 206 |
|
| 207 |
- [KaTeX Support Table](https://katex.org/docs/support_table) — definitive command lookup
|
| 208 |
+
- [KaTeX Unsupported Features](https://github.com/KaTeX/KaTeX/wiki/Things-that-KaTeX-does-not-(yet)-support)
|
polars/03_loading_data.py
CHANGED
|
@@ -493,12 +493,11 @@ def _(folder, lz, pl):
|
|
| 493 |
_df, _df2, _df3 = pl.collect_all([lz, lz2, lz3])
|
| 494 |
|
| 495 |
# Sinking multiple LazyFrames into different destinations
|
| 496 |
-
|
| 497 |
-
|
| 498 |
-
|
| 499 |
-
|
| 500 |
-
]
|
| 501 |
-
_ = pl.collect_all(sinks)
|
| 502 |
return (sinks,)
|
| 503 |
|
| 504 |
|
|
@@ -520,9 +519,10 @@ async def _(lz):
|
|
| 520 |
|
| 521 |
@app.cell
|
| 522 |
async def _(folder, lz, pl, sinks):
|
| 523 |
-
#
|
| 524 |
-
|
| 525 |
-
|
|
|
|
| 526 |
return
|
| 527 |
|
| 528 |
|
|
|
|
| 493 |
_df, _df2, _df3 = pl.collect_all([lz, lz2, lz3])
|
| 494 |
|
| 495 |
# Sinking multiple LazyFrames into different destinations
|
| 496 |
+
# (polars 1.23+ removed lazy=True from sink methods; each sink now executes immediately)
|
| 497 |
+
lz.sink_csv(folder / "data_1.csv")
|
| 498 |
+
lz2.sink_csv(folder / "data_2.csv")
|
| 499 |
+
lz3.sink_csv(folder / "data_3.csv")
|
| 500 |
+
sinks = []
|
|
|
|
| 501 |
return (sinks,)
|
| 502 |
|
| 503 |
|
|
|
|
| 519 |
|
| 520 |
@app.cell
|
| 521 |
async def _(folder, lz, pl, sinks):
|
| 522 |
+
# polars 1.23+ removed lazy=True from sink methods; sinks now execute immediately
|
| 523 |
+
lz.sink_csv(folder / "data_from_async.csv")
|
| 524 |
+
if sinks:
|
| 525 |
+
_ = await pl.collect_all_async(sinks)
|
| 526 |
return
|
| 527 |
|
| 528 |
|
polars/05_reactive_plots.py
CHANGED
|
@@ -61,22 +61,25 @@ def _(mo):
|
|
| 61 |
|
| 62 |
|
| 63 |
@app.cell
|
| 64 |
-
def _(lz, pl):
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
)
|
| 77 |
-
|
| 78 |
-
.
|
| 79 |
-
)
|
| 80 |
df
|
| 81 |
return (df,)
|
| 82 |
|
|
|
|
| 61 |
|
| 62 |
|
| 63 |
@app.cell
|
| 64 |
+
def _(lz, mo, pl):
|
| 65 |
+
try:
|
| 66 |
+
df = (
|
| 67 |
+
lz
|
| 68 |
+
# Filter data we consider relevant (somewhat arbitrary in this example)
|
| 69 |
+
.filter(pl.col("explicit") == False)
|
| 70 |
+
.drop("Unnamed: 0", "track_id", "explicit")
|
| 71 |
+
.with_columns(
|
| 72 |
+
# Perform whichever transformations you want (again somewhat arbitrary in this example)
|
| 73 |
+
# Convert the duration from milliseconds to seconds (int)
|
| 74 |
+
pl.col("duration_ms").floordiv(1_000).alias("duration_seconds"),
|
| 75 |
+
# Convert the popularity from an integer 0 ~ 100 to a percentage 0 ~ 1.0
|
| 76 |
+
pl.col("popularity").truediv(100),
|
| 77 |
+
)
|
| 78 |
+
# lastly, download (if needed) and collect into memory
|
| 79 |
+
.collect()
|
| 80 |
)
|
| 81 |
+
except Exception as e:
|
| 82 |
+
mo.stop(True, mo.md(f"**Failed to load data from HuggingFace:** {e}"))
|
|
|
|
| 83 |
df
|
| 84 |
return (df,)
|
| 85 |
|
polars/07_querying_with_sql.py
CHANGED
|
@@ -2,7 +2,7 @@
|
|
| 2 |
# requires-python = ">=3.12"
|
| 3 |
# dependencies = [
|
| 4 |
# "duckdb==1.4.4",
|
| 5 |
-
# "kagglehub==0.
|
| 6 |
# "marimo",
|
| 7 |
# "polars==1.24.0",
|
| 8 |
# "pyarrow==22.0.0",
|
|
|
|
| 2 |
# requires-python = ">=3.12"
|
| 3 |
# dependencies = [
|
| 4 |
# "duckdb==1.4.4",
|
| 5 |
+
# "kagglehub==1.0.0",
|
| 6 |
# "marimo",
|
| 7 |
# "polars==1.24.0",
|
| 8 |
# "pyarrow==22.0.0",
|
polars/11_missing_data.py
CHANGED
|
@@ -743,28 +743,34 @@ def _(pl):
|
|
| 743 |
|
| 744 |
|
| 745 |
@app.cell
|
| 746 |
-
def _(pl, raw_stations):
|
| 747 |
-
|
| 748 |
-
|
| 749 |
-
|
| 750 |
-
|
| 751 |
-
|
| 752 |
-
|
| 753 |
-
|
| 754 |
-
|
| 755 |
-
|
| 756 |
-
|
| 757 |
-
|
|
|
|
|
|
|
|
|
|
| 758 |
return (dirty_stations,)
|
| 759 |
|
| 760 |
|
| 761 |
@app.cell
|
| 762 |
-
def _(pl, raw_weather):
|
| 763 |
-
|
| 764 |
-
|
| 765 |
-
|
| 766 |
-
|
| 767 |
-
|
|
|
|
|
|
|
|
|
|
| 768 |
return (dirty_weather_naive,)
|
| 769 |
|
| 770 |
|
|
|
|
| 743 |
|
| 744 |
|
| 745 |
@app.cell
|
| 746 |
+
def _(mo, pl, raw_stations):
|
| 747 |
+
try:
|
| 748 |
+
dirty_stations = raw_stations.select(
|
| 749 |
+
pl.col("id_estacao").alias("station"),
|
| 750 |
+
pl.col("estacao").alias("name"),
|
| 751 |
+
pl.col("latitude").alias("lat"),
|
| 752 |
+
pl.col("longitude").alias("lon"),
|
| 753 |
+
pl.col("cota").alias("altitude"),
|
| 754 |
+
pl.col("situacao").alias("situation"),
|
| 755 |
+
pl.col("endereco").alias("address"),
|
| 756 |
+
pl.col("data_inicio_operacao").alias("operation_start_date"),
|
| 757 |
+
pl.col("data_fim_operacao").alias("operation_end_date"),
|
| 758 |
+
).collect()
|
| 759 |
+
except Exception as e:
|
| 760 |
+
mo.stop(True, mo.md(f"**Failed to load stations data from HuggingFace:** {e}"))
|
| 761 |
return (dirty_stations,)
|
| 762 |
|
| 763 |
|
| 764 |
@app.cell
|
| 765 |
+
def _(mo, pl, raw_weather):
|
| 766 |
+
try:
|
| 767 |
+
dirty_weather_naive = raw_weather.select(
|
| 768 |
+
pl.col("id_estacao").alias("station"),
|
| 769 |
+
pl.col("acumulado_chuva_15_min").alias("accumulated_rain_15_minutes"),
|
| 770 |
+
pl.concat_str("data_particao", pl.lit("T"), "horario").str.to_datetime(time_zone=None).alias("datetime"),
|
| 771 |
+
).collect()
|
| 772 |
+
except Exception as e:
|
| 773 |
+
mo.stop(True, mo.md(f"**Failed to load weather data from HuggingFace:** {e}"))
|
| 774 |
return (dirty_weather_naive,)
|
| 775 |
|
| 776 |
|
polars/16_lazy_execution.py
CHANGED
|
@@ -291,7 +291,10 @@ def _(log_data, pl):
|
|
| 291 |
|
| 292 |
@app.cell
|
| 293 |
def _(log_data_erroneous):
|
| 294 |
-
|
|
|
|
|
|
|
|
|
|
| 295 |
return
|
| 296 |
|
| 297 |
|
|
@@ -307,12 +310,15 @@ def _(mo):
|
|
| 307 |
|
| 308 |
@app.cell
|
| 309 |
def _(log_data, pl):
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
|
|
|
|
|
|
|
|
|
| 316 |
return
|
| 317 |
|
| 318 |
|
|
@@ -438,7 +444,10 @@ def _(mo):
|
|
| 438 |
|
| 439 |
@app.cell
|
| 440 |
def _(a_query):
|
| 441 |
-
|
|
|
|
|
|
|
|
|
|
| 442 |
return
|
| 443 |
|
| 444 |
|
|
@@ -491,21 +500,16 @@ def _(mo):
|
|
| 491 |
@app.cell(hide_code=True)
|
| 492 |
def _(mo):
|
| 493 |
mo.md(r"""
|
| 494 |
-
The results of a query from a lazyframe can be saved in streaming mode using `sink_*` (e.g. `sink_parquet`) functions. Sinks support saving data to disk or cloud, and are especially helpful with large datasets.
|
| 495 |
""")
|
| 496 |
return
|
| 497 |
|
| 498 |
|
| 499 |
@app.cell
|
| 500 |
-
def _(a_query
|
| 501 |
-
(
|
| 502 |
-
|
| 503 |
-
|
| 504 |
-
pl.PartitionMaxSize(
|
| 505 |
-
"log_data_filtered_{part}.parquet",
|
| 506 |
-
max_size=1_000
|
| 507 |
-
)
|
| 508 |
-
)
|
| 509 |
)
|
| 510 |
return
|
| 511 |
|
|
@@ -520,9 +524,9 @@ def _(mo):
|
|
| 520 |
|
| 521 |
@app.cell
|
| 522 |
def _(a_query, pl):
|
| 523 |
-
|
| 524 |
-
|
| 525 |
-
|
| 526 |
return
|
| 527 |
|
| 528 |
|
|
|
|
| 291 |
|
| 292 |
@app.cell
|
| 293 |
def _(log_data_erroneous):
|
| 294 |
+
try:
|
| 295 |
+
log_data_erroneous.collect()
|
| 296 |
+
except Exception as e:
|
| 297 |
+
print(f"{type(e).__name__}: {e}")
|
| 298 |
return
|
| 299 |
|
| 300 |
|
|
|
|
| 310 |
|
| 311 |
@app.cell
|
| 312 |
def _(log_data, pl):
|
| 313 |
+
try:
|
| 314 |
+
(
|
| 315 |
+
log_data.pivot(index="time", on="request_code",
|
| 316 |
+
values="status", aggregate_function="len")
|
| 317 |
+
.filter(pl.col("POST").is_null())
|
| 318 |
+
.collect()
|
| 319 |
+
)
|
| 320 |
+
except Exception as e:
|
| 321 |
+
print(f"{type(e).__name__}: {e}")
|
| 322 |
return
|
| 323 |
|
| 324 |
|
|
|
|
| 444 |
|
| 445 |
@app.cell
|
| 446 |
def _(a_query):
|
| 447 |
+
try:
|
| 448 |
+
a_query.collect(engine="streaming")
|
| 449 |
+
except Exception as e:
|
| 450 |
+
print(f"{type(e).__name__}: {e}")
|
| 451 |
return
|
| 452 |
|
| 453 |
|
|
|
|
| 500 |
@app.cell(hide_code=True)
|
| 501 |
def _(mo):
|
| 502 |
mo.md(r"""
|
| 503 |
+
The results of a query from a lazyframe can be saved in streaming mode using `sink_*` (e.g. `sink_parquet`) functions. Sinks support saving data to disk or cloud, and are especially helpful with large datasets. To write output partitioned into multiple files by a column key, collect the LazyFrame first and use `DataFrame.write_parquet` with the `partition_by` parameter, as shown below.
|
| 504 |
""")
|
| 505 |
return
|
| 506 |
|
| 507 |
|
| 508 |
@app.cell
|
| 509 |
+
def _(a_query):
|
| 510 |
+
a_query.collect().write_parquet(
|
| 511 |
+
"log_data_filtered/",
|
| 512 |
+
partition_by="request_code",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 513 |
)
|
| 514 |
return
|
| 515 |
|
|
|
|
| 524 |
|
| 525 |
@app.cell
|
| 526 |
def _(a_query, pl):
|
| 527 |
+
# polars 1.23+ removed lazy=True from sink methods; each sink executes immediately
|
| 528 |
+
a_query.sink_parquet("log_data_filtered.parquet")
|
| 529 |
+
a_query.sink_ipc("log_data_filtered.ipc")
|
| 530 |
return
|
| 531 |
|
| 532 |
|
queueing/07_late_merge.py
CHANGED
|
@@ -240,9 +240,9 @@ def _(ArrivalStream, Environment, MergeServer, Queue, SEED, SIM_TIME, random):
|
|
| 240 |
blocked = []
|
| 241 |
|
| 242 |
if zipper:
|
| 243 |
-
lanes = [Queue(env,
|
| 244 |
else:
|
| 245 |
-
lanes = [Queue(env,
|
| 246 |
ArrivalStream(env, lanes, sojourn_times, blocked, zipper)
|
| 247 |
MergeServer(env, lanes, zipper)
|
| 248 |
env.run(until=SIM_TIME)
|
|
|
|
| 240 |
blocked = []
|
| 241 |
|
| 242 |
if zipper:
|
| 243 |
+
lanes = [Queue(env, capacity=k), Queue(env, capacity=k)]
|
| 244 |
else:
|
| 245 |
+
lanes = [Queue(env, capacity=k)]
|
| 246 |
ArrivalStream(env, lanes, sojourn_times, blocked, zipper)
|
| 247 |
MergeServer(env, lanes, zipper)
|
| 248 |
env.run(until=SIM_TIME)
|
queueing/11_tandem_queue.py
CHANGED
|
@@ -189,7 +189,7 @@ def _(Environment, Queue, SIM_TIME, Source, Stage1, Stage2):
|
|
| 189 |
def simulate(buffer_capacity):
|
| 190 |
env = Environment()
|
| 191 |
input_q = Queue(env)
|
| 192 |
-
middle_q = Queue(env,
|
| 193 |
s2_idle = []
|
| 194 |
completions = []
|
| 195 |
Source(env, input_q)
|
|
|
|
| 189 |
def simulate(buffer_capacity):
|
| 190 |
env = Environment()
|
| 191 |
input_q = Queue(env)
|
| 192 |
+
middle_q = Queue(env, capacity=buffer_capacity)
|
| 193 |
s2_idle = []
|
| 194 |
completions = []
|
| 195 |
Source(env, input_q)
|
tools/01_widgets.py
ADDED
|
@@ -0,0 +1,471 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# /// script
|
| 2 |
+
# requires-python = "==3.12"
|
| 3 |
+
# dependencies = [
|
| 4 |
+
# "anywidget",
|
| 5 |
+
# "marimo",
|
| 6 |
+
# "marimo-learn",
|
| 7 |
+
# ]
|
| 8 |
+
# ///
|
| 9 |
+
|
| 10 |
+
"""
|
| 11 |
+
Example Marimo Notebook: Education Widgets Demo
|
| 12 |
+
|
| 13 |
+
This notebook demonstrates all formative assessment widgets.
|
| 14 |
+
Run with: marimo edit demo.py
|
| 15 |
+
"""
|
| 16 |
+
|
| 17 |
+
import marimo
|
| 18 |
+
|
| 19 |
+
__generated_with = "0.20.4"
|
| 20 |
+
app = marimo.App()
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
@app.cell
|
| 24 |
+
def _():
|
| 25 |
+
import marimo as mo
|
| 26 |
+
from marimo_learn import (
|
| 27 |
+
Color,
|
| 28 |
+
ConceptMapWidget,
|
| 29 |
+
FlashcardWidget,
|
| 30 |
+
LabelingWidget,
|
| 31 |
+
MatchingWidget,
|
| 32 |
+
MultipleChoiceWidget,
|
| 33 |
+
NumericEntryWidget,
|
| 34 |
+
OrderingWidget,
|
| 35 |
+
PredictThenCheckWidget,
|
| 36 |
+
World,
|
| 37 |
+
)
|
| 38 |
+
|
| 39 |
+
return (
|
| 40 |
+
Color,
|
| 41 |
+
ConceptMapWidget,
|
| 42 |
+
FlashcardWidget,
|
| 43 |
+
LabelingWidget,
|
| 44 |
+
MatchingWidget,
|
| 45 |
+
MultipleChoiceWidget,
|
| 46 |
+
NumericEntryWidget,
|
| 47 |
+
OrderingWidget,
|
| 48 |
+
PredictThenCheckWidget,
|
| 49 |
+
World,
|
| 50 |
+
)
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
@app.cell
|
| 54 |
+
def _(mo):
|
| 55 |
+
mo.md("""
|
| 56 |
+
# Educational Widgets Demo
|
| 57 |
+
|
| 58 |
+
This notebook demonstrates widgets in marimo_learn.
|
| 59 |
+
""")
|
| 60 |
+
return
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
# Spiral
|
| 64 |
+
@app.cell
|
| 65 |
+
def _(Color, World, mo):
|
| 66 |
+
_world = World()
|
| 67 |
+
|
| 68 |
+
async def _spiral(world, turtle):
|
| 69 |
+
colors = list(Color)
|
| 70 |
+
for i in range(70):
|
| 71 |
+
if i % 10 == 0:
|
| 72 |
+
turtle.set_color(colors[(i // 10) % len(colors)])
|
| 73 |
+
await turtle.forward(i * 2.8)
|
| 74 |
+
turtle.right(91)
|
| 75 |
+
|
| 76 |
+
_world.set_coroutine(_spiral)
|
| 77 |
+
mo.ui.anywidget(_world)
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
@app.cell
|
| 81 |
+
def _(mo):
|
| 82 |
+
mo.md("""
|
| 83 |
+
## Concept Map
|
| 84 |
+
""")
|
| 85 |
+
return
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
@app.cell
|
| 89 |
+
def _(ConceptMapWidget, mo):
|
| 90 |
+
concept_map = mo.ui.anywidget(
|
| 91 |
+
ConceptMapWidget(
|
| 92 |
+
question="Map the relationships between these Python concepts:",
|
| 93 |
+
concepts=["function", "parameter", "argument", "return value", "call site"],
|
| 94 |
+
terms=["defines", "accepts", "supplies", "produces", "invokes"],
|
| 95 |
+
correct_edges=[
|
| 96 |
+
{"from": "function", "to": "parameter", "label": "accepts"},
|
| 97 |
+
{"from": "function", "to": "return value", "label": "produces"},
|
| 98 |
+
{"from": "call site", "to": "argument", "label": "supplies"},
|
| 99 |
+
{"from": "argument", "to": "parameter", "label": "defines"},
|
| 100 |
+
{"from": "call site", "to": "function", "label": "invokes"},
|
| 101 |
+
],
|
| 102 |
+
)
|
| 103 |
+
)
|
| 104 |
+
concept_map
|
| 105 |
+
return (concept_map,)
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
@app.cell
|
| 109 |
+
def _(concept_map, mo):
|
| 110 |
+
def concept_map_msg(widget):
|
| 111 |
+
val = widget.value.get("value") or {}
|
| 112 |
+
score = val.get("score")
|
| 113 |
+
total = val.get("total", 5)
|
| 114 |
+
if score is None:
|
| 115 |
+
return "Draw connections between concepts to see your score."
|
| 116 |
+
msg = f"**{score}/{total}** correct connection{'s' if total != 1 else ''}"
|
| 117 |
+
if val.get("correct"):
|
| 118 |
+
msg += " — complete!"
|
| 119 |
+
return msg
|
| 120 |
+
|
| 121 |
+
mo.md(concept_map_msg(concept_map))
|
| 122 |
+
return
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
@app.cell
|
| 126 |
+
def _(mo):
|
| 127 |
+
mo.md("""
|
| 128 |
+
## Flashcard Deck
|
| 129 |
+
""")
|
| 130 |
+
return
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
@app.cell
|
| 134 |
+
def _(FlashcardWidget, mo):
|
| 135 |
+
flashcard_deck = mo.ui.anywidget(
|
| 136 |
+
FlashcardWidget(
|
| 137 |
+
question="Python Concepts — rate yourself on each card:",
|
| 138 |
+
cards=[
|
| 139 |
+
{
|
| 140 |
+
"front": "What does a list comprehension look like?",
|
| 141 |
+
"back": "[expr for item in iterable if condition] — e.g., [x**2 for x in range(10) if x % 2 == 0]",
|
| 142 |
+
},
|
| 143 |
+
{
|
| 144 |
+
"front": "What is the difference between a list and a tuple?",
|
| 145 |
+
"back": "Lists are mutable (can be changed after creation); tuples are immutable (cannot be changed).",
|
| 146 |
+
},
|
| 147 |
+
{
|
| 148 |
+
"front": "What does the `*args` parameter do in a function definition?",
|
| 149 |
+
"back": "It collects any number of positional arguments into a tuple named `args`.",
|
| 150 |
+
},
|
| 151 |
+
{
|
| 152 |
+
"front": "What is a Python generator?",
|
| 153 |
+
"back": "A function that uses `yield` to produce values one at a time, pausing between each, without building the full sequence in memory.",
|
| 154 |
+
},
|
| 155 |
+
{
|
| 156 |
+
"front": "What does `if __name__ == '__main__':` do?",
|
| 157 |
+
"back": "It runs the indented code only when the file is executed directly, not when it is imported as a module.",
|
| 158 |
+
},
|
| 159 |
+
{
|
| 160 |
+
"front": "What is the difference between `is` and `==` in Python?",
|
| 161 |
+
"back": "`==` tests value equality; `is` tests identity (whether two names refer to the exact same object in memory).",
|
| 162 |
+
},
|
| 163 |
+
],
|
| 164 |
+
shuffle=True,
|
| 165 |
+
)
|
| 166 |
+
)
|
| 167 |
+
flashcard_deck
|
| 168 |
+
return (flashcard_deck,)
|
| 169 |
+
|
| 170 |
+
|
| 171 |
+
@app.cell
|
| 172 |
+
def _(flashcard_deck, mo):
|
| 173 |
+
def flashcard_progress(widget):
|
| 174 |
+
val = widget.value.get("value") or {}
|
| 175 |
+
results = val.get("results", {})
|
| 176 |
+
counts = {"got_it": 0, "almost": 0, "no": 0}
|
| 177 |
+
for r in results.values():
|
| 178 |
+
counts[r["rating"]] = counts.get(r["rating"], 0) + 1
|
| 179 |
+
return len(results), counts, val.get("complete", False)
|
| 180 |
+
|
| 181 |
+
_rated, _counts, _complete = flashcard_progress(flashcard_deck)
|
| 182 |
+
mo.md(f"""
|
| 183 |
+
**Progress:** {_rated} card(s) rated —
|
| 184 |
+
✓ Got it: {_counts["got_it"]}
|
| 185 |
+
~ Almost: {_counts["almost"]}
|
| 186 |
+
✗ No: {_counts["no"]}
|
| 187 |
+
{" 🎉 Deck complete!" if _complete else ""}
|
| 188 |
+
""")
|
| 189 |
+
return
|
| 190 |
+
|
| 191 |
+
|
| 192 |
+
@app.cell
|
| 193 |
+
def _(mo):
|
| 194 |
+
mo.md("""
|
| 195 |
+
## Labeling Question
|
| 196 |
+
""")
|
| 197 |
+
return
|
| 198 |
+
|
| 199 |
+
|
| 200 |
+
@app.cell
|
| 201 |
+
def _(LabelingWidget, mo):
|
| 202 |
+
labeling_question = mo.ui.anywidget(
|
| 203 |
+
LabelingWidget(
|
| 204 |
+
question="Label the parts of this Python code:",
|
| 205 |
+
labels=[
|
| 206 |
+
"Variable declaration",
|
| 207 |
+
"Function call",
|
| 208 |
+
"String literal",
|
| 209 |
+
"Arithmetic operation",
|
| 210 |
+
],
|
| 211 |
+
text_lines=[
|
| 212 |
+
"name = 'Alice'",
|
| 213 |
+
"age = 25",
|
| 214 |
+
"result = age + 5",
|
| 215 |
+
"print(name)",
|
| 216 |
+
],
|
| 217 |
+
correct_labels={
|
| 218 |
+
0: [0, 2], # Line 0: labels 0 and 2
|
| 219 |
+
1: [0], # Line 1: label 0
|
| 220 |
+
2: [0, 3], # Line 2: labels 0 and 3
|
| 221 |
+
3: [1], # Line 3: label 1
|
| 222 |
+
},
|
| 223 |
+
)
|
| 224 |
+
)
|
| 225 |
+
labeling_question
|
| 226 |
+
return (labeling_question,)
|
| 227 |
+
|
| 228 |
+
|
| 229 |
+
@app.cell
|
| 230 |
+
def _(labeling_question, mo):
|
| 231 |
+
_val = labeling_question.value.get("value") or {}
|
| 232 |
+
_total = _val.get("total", 0)
|
| 233 |
+
_msg = f"Score: {_val['score']}/{_total}" if _total > 0 else "Not submitted yet"
|
| 234 |
+
mo.md(f"**{_msg}**")
|
| 235 |
+
return
|
| 236 |
+
|
| 237 |
+
|
| 238 |
+
@app.cell
|
| 239 |
+
def _(mo):
|
| 240 |
+
mo.md("""
|
| 241 |
+
## Matching Question
|
| 242 |
+
""")
|
| 243 |
+
return
|
| 244 |
+
|
| 245 |
+
|
| 246 |
+
@app.cell
|
| 247 |
+
def _(MatchingWidget, mo):
|
| 248 |
+
matching_question = mo.ui.anywidget(
|
| 249 |
+
MatchingWidget(
|
| 250 |
+
question="Match the programming languages to their primary paradigms:",
|
| 251 |
+
left=["Python", "Haskell", "C", "SQL"],
|
| 252 |
+
right=["Functional", "Procedural", "Multi-paradigm", "Declarative"],
|
| 253 |
+
correct_matches={0: 2, 1: 0, 2: 1, 3: 3},
|
| 254 |
+
)
|
| 255 |
+
)
|
| 256 |
+
matching_question
|
| 257 |
+
return (matching_question,)
|
| 258 |
+
|
| 259 |
+
|
| 260 |
+
@app.cell
|
| 261 |
+
def _(matching_question, mo):
|
| 262 |
+
_val = matching_question.value.get("value") or {}
|
| 263 |
+
_msg = f"Score: {_val['score']}/{_val['total']}" if _val else "Not answered yet"
|
| 264 |
+
mo.md(f"**{_msg}**")
|
| 265 |
+
return
|
| 266 |
+
|
| 267 |
+
|
| 268 |
+
@app.cell
|
| 269 |
+
def _(mo):
|
| 270 |
+
mo.md("""
|
| 271 |
+
## Multiple Choice Question
|
| 272 |
+
""")
|
| 273 |
+
return
|
| 274 |
+
|
| 275 |
+
|
| 276 |
+
@app.cell
|
| 277 |
+
def _(MultipleChoiceWidget, mo):
|
| 278 |
+
multiple_choice_question = mo.ui.anywidget(
|
| 279 |
+
MultipleChoiceWidget(
|
| 280 |
+
question="What is the capital of France?",
|
| 281 |
+
options=["London", "Berlin", "Paris", "Madrid"],
|
| 282 |
+
correct_answer=2,
|
| 283 |
+
explanation="Paris has been the capital of France since the 12th century.",
|
| 284 |
+
)
|
| 285 |
+
)
|
| 286 |
+
multiple_choice_question
|
| 287 |
+
return (multiple_choice_question,)
|
| 288 |
+
|
| 289 |
+
|
| 290 |
+
@app.cell
|
| 291 |
+
def _(multiple_choice_question, mo):
|
| 292 |
+
_val = multiple_choice_question.value.get("value") or {}
|
| 293 |
+
if _val.get("answered"):
|
| 294 |
+
_msg = "Score: 1/1" if _val.get("correct") else "Score: 0/1"
|
| 295 |
+
else:
|
| 296 |
+
_msg = "Not answered yet"
|
| 297 |
+
mo.md(f"**{_msg}**")
|
| 298 |
+
return
|
| 299 |
+
|
| 300 |
+
|
| 301 |
+
@app.cell
|
| 302 |
+
def _(mo):
|
| 303 |
+
mo.md("""
|
| 304 |
+
## Ordering Question
|
| 305 |
+
""")
|
| 306 |
+
return
|
| 307 |
+
|
| 308 |
+
|
| 309 |
+
@app.cell
|
| 310 |
+
def _(OrderingWidget, mo):
|
| 311 |
+
ordering_question = mo.ui.anywidget(
|
| 312 |
+
OrderingWidget(
|
| 313 |
+
question="Arrange these steps of the scientific method in the correct order:",
|
| 314 |
+
items=[
|
| 315 |
+
"Ask a question",
|
| 316 |
+
"Do background research",
|
| 317 |
+
"Construct a hypothesis",
|
| 318 |
+
"Test with an experiment",
|
| 319 |
+
"Analyze data",
|
| 320 |
+
"Draw conclusions",
|
| 321 |
+
],
|
| 322 |
+
shuffle=True,
|
| 323 |
+
)
|
| 324 |
+
)
|
| 325 |
+
ordering_question
|
| 326 |
+
return (ordering_question,)
|
| 327 |
+
|
| 328 |
+
|
| 329 |
+
@app.cell
|
| 330 |
+
def _(mo, ordering_question):
|
| 331 |
+
_val = ordering_question.value.get("value") or {}
|
| 332 |
+
if _val.get("correct") is not None and _val.get("order"):
|
| 333 |
+
_msg = "Score: 1/1" if _val.get("correct") else "Score: 0/1"
|
| 334 |
+
else:
|
| 335 |
+
_msg = "Not answered yet"
|
| 336 |
+
mo.md(f"**{_msg}**")
|
| 337 |
+
return
|
| 338 |
+
|
| 339 |
+
|
| 340 |
+
@app.cell
|
| 341 |
+
def _(mo):
|
| 342 |
+
mo.md("""
|
| 343 |
+
## Numeric Entry Question
|
| 344 |
+
""")
|
| 345 |
+
return
|
| 346 |
+
|
| 347 |
+
|
| 348 |
+
@app.cell
|
| 349 |
+
def _(NumericEntryWidget, mo):
|
| 350 |
+
numeric_entry_question = mo.ui.anywidget(
|
| 351 |
+
NumericEntryWidget(
|
| 352 |
+
question="How many bits are in one byte?",
|
| 353 |
+
correct_answer=8,
|
| 354 |
+
tolerance=0.5,
|
| 355 |
+
explanation="A byte consists of exactly 8 bits.",
|
| 356 |
+
)
|
| 357 |
+
)
|
| 358 |
+
numeric_entry_question
|
| 359 |
+
return (numeric_entry_question,)
|
| 360 |
+
|
| 361 |
+
|
| 362 |
+
@app.cell
|
| 363 |
+
def _(mo, numeric_entry_question):
|
| 364 |
+
_val = numeric_entry_question.value.get("value") or {}
|
| 365 |
+
if _val.get("answered"):
|
| 366 |
+
_msg = "Score: 1/1" if _val.get("ok") else "Score: 0/1"
|
| 367 |
+
else:
|
| 368 |
+
_msg = "Not answered yet"
|
| 369 |
+
mo.md(f"**{_msg}**")
|
| 370 |
+
return
|
| 371 |
+
|
| 372 |
+
|
| 373 |
+
@app.cell
|
| 374 |
+
def _(mo):
|
| 375 |
+
mo.md("""
|
| 376 |
+
## Predict-Then-Check Question
|
| 377 |
+
""")
|
| 378 |
+
return
|
| 379 |
+
|
| 380 |
+
|
| 381 |
+
@app.cell
|
| 382 |
+
def _(PredictThenCheckWidget, mo):
|
| 383 |
+
predict_then_check_question = mo.ui.anywidget(
|
| 384 |
+
PredictThenCheckWidget(
|
| 385 |
+
question="What does this Python code print?",
|
| 386 |
+
code='words = ["one", "two", "three"]\nprint(len(words))',
|
| 387 |
+
output="3",
|
| 388 |
+
options=["2", "3", "['one', 'two', 'three']", "None"],
|
| 389 |
+
correct_answer=1,
|
| 390 |
+
explanations=[
|
| 391 |
+
"Wrong: len() counts all items; the list has three elements.",
|
| 392 |
+
"Correct: len() returns the number of items, which is 3.",
|
| 393 |
+
"Wrong: len() returns an integer count, not the list itself.",
|
| 394 |
+
"Wrong: len() always returns an integer, never None.",
|
| 395 |
+
],
|
| 396 |
+
)
|
| 397 |
+
)
|
| 398 |
+
predict_then_check_question
|
| 399 |
+
return (predict_then_check_question,)
|
| 400 |
+
|
| 401 |
+
|
| 402 |
+
@app.cell
|
| 403 |
+
def _(mo, predict_then_check_question):
|
| 404 |
+
_val = predict_then_check_question.value.get("value") or {}
|
| 405 |
+
if _val.get("answered"):
|
| 406 |
+
_msg = "Score: 1/1" if _val.get("correct") else "Score: 0/1"
|
| 407 |
+
else:
|
| 408 |
+
_msg = "Not answered yet"
|
| 409 |
+
mo.md(f"**{_msg}**")
|
| 410 |
+
return
|
| 411 |
+
|
| 412 |
+
|
| 413 |
+
@app.cell
|
| 414 |
+
def _(mo):
|
| 415 |
+
mo.md("""
|
| 416 |
+
---
|
| 417 |
+
|
| 418 |
+
## Quiz Results Summary
|
| 419 |
+
|
| 420 |
+
You can access the values from all widgets to create a summary or scoring system.
|
| 421 |
+
""")
|
| 422 |
+
return
|
| 423 |
+
|
| 424 |
+
|
| 425 |
+
@app.cell
|
| 426 |
+
def _(
|
| 427 |
+
concept_map,
|
| 428 |
+
flashcard_deck,
|
| 429 |
+
matching_question,
|
| 430 |
+
multiple_choice_question,
|
| 431 |
+
mo,
|
| 432 |
+
numeric_entry_question,
|
| 433 |
+
ordering_question,
|
| 434 |
+
predict_then_check_question,
|
| 435 |
+
):
|
| 436 |
+
def widget_val(widget):
|
| 437 |
+
return widget.value.get("value") or {}
|
| 438 |
+
|
| 439 |
+
def calculate_score():
|
| 440 |
+
score = 0
|
| 441 |
+
total = 0
|
| 442 |
+
for val, answered_key, correct_key in [
|
| 443 |
+
(widget_val(concept_map), "score", "correct"),
|
| 444 |
+
(widget_val(matching_question), "score", "correct"),
|
| 445 |
+
(widget_val(multiple_choice_question), "answered", "correct"),
|
| 446 |
+
(widget_val(ordering_question), "order", "correct"),
|
| 447 |
+
(widget_val(numeric_entry_question), "answered", "ok"),
|
| 448 |
+
(widget_val(predict_then_check_question), "answered", "correct"),
|
| 449 |
+
]:
|
| 450 |
+
if val.get(answered_key) is not None and val.get(answered_key) is not False:
|
| 451 |
+
total += 1
|
| 452 |
+
if val.get(correct_key):
|
| 453 |
+
score += 1
|
| 454 |
+
fc = widget_val(flashcard_deck)
|
| 455 |
+
if fc.get("results"):
|
| 456 |
+
total += 1
|
| 457 |
+
if fc.get("complete"):
|
| 458 |
+
score += 1
|
| 459 |
+
return score, total
|
| 460 |
+
|
| 461 |
+
score, total = calculate_score()
|
| 462 |
+
mo.md(f"""
|
| 463 |
+
### Current Score: {score}/{total}
|
| 464 |
+
|
| 465 |
+
{"🎉 Perfect score!" if score == total and total > 0 else "Keep going!" if total > 0 else "Answer the questions above to see your score."}
|
| 466 |
+
""")
|
| 467 |
+
return
|
| 468 |
+
|
| 469 |
+
|
| 470 |
+
if __name__ == "__main__":
|
| 471 |
+
app.run()
|
tools/02_turtles.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# /// script
|
| 2 |
+
# requires-python = "==3.12"
|
| 3 |
+
# dependencies = [
|
| 4 |
+
# "anywidget",
|
| 5 |
+
# "marimo",
|
| 6 |
+
# "marimo-learn",
|
| 7 |
+
# ]
|
| 8 |
+
# ///
|
| 9 |
+
|
| 10 |
+
"""
|
| 11 |
+
Example Marimo Notebook: Turtle Graphics
|
| 12 |
+
"""
|
| 13 |
+
|
| 14 |
+
import marimo
|
| 15 |
+
|
| 16 |
+
__generated_with = "0.20.4"
|
| 17 |
+
app = marimo.App()
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
@app.cell
|
| 21 |
+
def _():
|
| 22 |
+
import marimo as mo
|
| 23 |
+
from marimo_learn import (
|
| 24 |
+
Color,
|
| 25 |
+
World,
|
| 26 |
+
)
|
| 27 |
+
|
| 28 |
+
return (
|
| 29 |
+
Color,
|
| 30 |
+
World,
|
| 31 |
+
)
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
@app.cell
|
| 35 |
+
def _(mo):
|
| 36 |
+
mo.md("""
|
| 37 |
+
# Turtle Graphics Demo
|
| 38 |
+
|
| 39 |
+
This notebook demonstrates turtle graphics in marimo_learn.
|
| 40 |
+
See [the documentation](https://github.com/gvwilson/marimo_learn) for details.
|
| 41 |
+
""")
|
| 42 |
+
return
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
# Spiral
|
| 46 |
+
@app.cell
|
| 47 |
+
def _(Color, World, mo):
|
| 48 |
+
_world = World()
|
| 49 |
+
|
| 50 |
+
async def _spiral(world, turtle):
|
| 51 |
+
colors = list(Color)
|
| 52 |
+
for i in range(70):
|
| 53 |
+
if i % 10 == 0:
|
| 54 |
+
turtle.set_color(colors[(i // 10) % len(colors)])
|
| 55 |
+
await turtle.forward(i * 2.8)
|
| 56 |
+
turtle.right(91)
|
| 57 |
+
|
| 58 |
+
_world.set_coroutine(_spiral)
|
| 59 |
+
mo.ui.anywidget(_world)
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
if __name__ == "__main__":
|
| 63 |
+
app.run()
|
tools/index.md
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Tools
|
| 3 |
+
description: >
|
| 4 |
+
These notebooks show how to integrate educational widgets
|
| 5 |
+
and other tools into your notebooks.
|
| 6 |
+
---
|
tools/wiggly.py
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# /// script
|
| 2 |
+
# requires-python = ">=3.14"
|
| 3 |
+
# dependencies = [
|
| 4 |
+
# "altair",
|
| 5 |
+
# "marimo",
|
| 6 |
+
# "matplotlib",
|
| 7 |
+
# "numpy",
|
| 8 |
+
# "pandas",
|
| 9 |
+
# "wigglystuff==0.3.1",
|
| 10 |
+
# ]
|
| 11 |
+
# ///
|
| 12 |
+
import marimo
|
| 13 |
+
|
| 14 |
+
__generated_with = "0.20.4"
|
| 15 |
+
app = marimo.App(width="full")
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
@app.cell(hide_code=True)
|
| 19 |
+
def _(mo):
|
| 20 |
+
mo.md(r"""
|
| 21 |
+
# wigglystuff Widgets
|
| 22 |
+
|
| 23 |
+
This notebook demonstrates four widgets from the [wigglystuff](https://github.com/koaning/wigglystuff) package:
|
| 24 |
+
`Slider2D`, `Matrix`, `HoverZoom`, and `TextCompare`.
|
| 25 |
+
""")
|
| 26 |
+
return
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
@app.cell
|
| 30 |
+
def _():
|
| 31 |
+
import marimo as mo
|
| 32 |
+
import numpy as np
|
| 33 |
+
import pandas as pd
|
| 34 |
+
import altair as alt
|
| 35 |
+
import matplotlib
|
| 36 |
+
matplotlib.use("Agg")
|
| 37 |
+
import matplotlib.pyplot as plt
|
| 38 |
+
from wigglystuff import Slider2D, Matrix, HoverZoom, TextCompare
|
| 39 |
+
return HoverZoom, Matrix, Slider2D, TextCompare, alt, mo, np, pd, plt
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
# ---------------------------------------------------------------------------
|
| 43 |
+
# Slider2D
|
| 44 |
+
# ---------------------------------------------------------------------------
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
@app.cell(hide_code=True)
|
| 48 |
+
def _(mo):
|
| 49 |
+
mo.md(r"""
|
| 50 |
+
## Slider2D
|
| 51 |
+
|
| 52 |
+
A two-dimensional slider that lets you pick an (x, y) point inside a bounded region.
|
| 53 |
+
""")
|
| 54 |
+
return
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
@app.cell
|
| 58 |
+
def _(Slider2D, mo):
|
| 59 |
+
slider2d = mo.ui.anywidget(
|
| 60 |
+
Slider2D(
|
| 61 |
+
width=320,
|
| 62 |
+
height=320,
|
| 63 |
+
x_bounds=(-2.0, 2.0),
|
| 64 |
+
y_bounds=(-1.0, 1.5),
|
| 65 |
+
)
|
| 66 |
+
)
|
| 67 |
+
slider2d
|
| 68 |
+
return (slider2d,)
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
@app.cell
|
| 72 |
+
def _(mo, slider2d):
|
| 73 |
+
mo.callout(
|
| 74 |
+
f"x = {slider2d.x:.3f}, y = {slider2d.y:.3f}; bounds {slider2d.x_bounds} / {slider2d.y_bounds}"
|
| 75 |
+
)
|
| 76 |
+
return
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
# ---------------------------------------------------------------------------
|
| 80 |
+
# Matrix
|
| 81 |
+
# ---------------------------------------------------------------------------
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
@app.cell(hide_code=True)
|
| 85 |
+
def _(mo):
|
| 86 |
+
mo.md(r"""
|
| 87 |
+
## Matrix
|
| 88 |
+
|
| 89 |
+
An editable matrix widget. The demo below applies a 3x2 transformation matrix to
|
| 90 |
+
1000 random RGB colours and plots the result in 2D — a manual PCA explorer.
|
| 91 |
+
""")
|
| 92 |
+
return
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
@app.cell
|
| 96 |
+
def _(Matrix, mo, np, pd):
|
| 97 |
+
pca_mat = mo.ui.anywidget(Matrix(np.random.normal(0, 1, size=(3, 2)), step=0.1))
|
| 98 |
+
rgb_mat = np.random.randint(0, 255, size=(1000, 3))
|
| 99 |
+
color = ["#{0:02x}{1:02x}{2:02x}".format(r, g, b) for r, g, b in rgb_mat]
|
| 100 |
+
rgb_df = pd.DataFrame(
|
| 101 |
+
{"r": rgb_mat[:, 0], "g": rgb_mat[:, 1], "b": rgb_mat[:, 2], "color": color}
|
| 102 |
+
)
|
| 103 |
+
return color, pca_mat, rgb_df, rgb_mat
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
@app.cell
|
| 107 |
+
def _(alt, color, mo, pca_mat, pd, rgb_mat):
|
| 108 |
+
X_tfm = rgb_mat @ pca_mat.matrix
|
| 109 |
+
df_pca = pd.DataFrame({"x": X_tfm[:, 0], "y": X_tfm[:, 1], "c": color})
|
| 110 |
+
pca_chart = (
|
| 111 |
+
alt.Chart(df_pca)
|
| 112 |
+
.mark_point()
|
| 113 |
+
.encode(x="x", y="y", color=alt.Color("c:N", scale=None))
|
| 114 |
+
.properties(width=400, height=400)
|
| 115 |
+
)
|
| 116 |
+
mo.hstack([pca_mat, pca_chart])
|
| 117 |
+
return
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
# ---------------------------------------------------------------------------
|
| 121 |
+
# HoverZoom
|
| 122 |
+
# ---------------------------------------------------------------------------
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
@app.cell(hide_code=True)
|
| 126 |
+
def _(mo):
|
| 127 |
+
mo.md(r"""
|
| 128 |
+
## HoverZoom
|
| 129 |
+
|
| 130 |
+
A magnifying-glass overlay for any image or matplotlib figure.
|
| 131 |
+
Hover over the chart to read labels on densely packed points.
|
| 132 |
+
""")
|
| 133 |
+
return
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
@app.cell
|
| 137 |
+
def _(np, plt):
|
| 138 |
+
rng = np.random.default_rng(42)
|
| 139 |
+
N_POINTS = 200
|
| 140 |
+
hz_x = rng.normal(0, 1, N_POINTS)
|
| 141 |
+
hz_y = rng.normal(0, 1, N_POINTS)
|
| 142 |
+
labels = [f"p{i}" for i in range(N_POINTS)]
|
| 143 |
+
|
| 144 |
+
fig, ax = plt.subplots(figsize=(8, 6), dpi=200)
|
| 145 |
+
ax.scatter(hz_x, hz_y, s=12, alpha=0.6)
|
| 146 |
+
for xi, yi, label in zip(hz_x, hz_y, labels):
|
| 147 |
+
ax.annotate(label, (xi, yi), fontsize=4, alpha=0.7, ha="center", va="bottom")
|
| 148 |
+
ax.set_title(f"{N_POINTS} labeled points — hover to read the labels")
|
| 149 |
+
fig.tight_layout()
|
| 150 |
+
return (fig,)
|
| 151 |
+
|
| 152 |
+
|
| 153 |
+
@app.cell
|
| 154 |
+
def _(HoverZoom, fig, mo):
|
| 155 |
+
chart_widget = mo.ui.anywidget(HoverZoom(fig, zoom_factor=4.0, width=500))
|
| 156 |
+
chart_widget
|
| 157 |
+
return
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
# ---------------------------------------------------------------------------
|
| 161 |
+
# TextCompare
|
| 162 |
+
# ---------------------------------------------------------------------------
|
| 163 |
+
|
| 164 |
+
|
| 165 |
+
@app.cell(hide_code=True)
|
| 166 |
+
def _(mo):
|
| 167 |
+
mo.md(r"""
|
| 168 |
+
## TextCompare
|
| 169 |
+
|
| 170 |
+
A side-by-side text comparison widget that highlights matching passages between two texts.
|
| 171 |
+
Useful for plagiarism detection, finding shared passages, or comparing document versions.
|
| 172 |
+
Hover over a highlighted match in one panel to see the corresponding match in the other.
|
| 173 |
+
""")
|
| 174 |
+
return
|
| 175 |
+
|
| 176 |
+
|
| 177 |
+
@app.cell
|
| 178 |
+
def _(TextCompare, mo):
|
| 179 |
+
text_a = """The quick brown fox jumps over the lazy dog.
|
| 180 |
+
This is a unique sentence in text A.
|
| 181 |
+
Both texts share this common passage here.
|
| 182 |
+
Another unique line for the first text."""
|
| 183 |
+
|
| 184 |
+
text_b = """A quick brown fox leaps over a lazy dog.
|
| 185 |
+
This is different content in text B.
|
| 186 |
+
Both texts share this common passage here.
|
| 187 |
+
Some other unique content for text B."""
|
| 188 |
+
|
| 189 |
+
compare = mo.ui.anywidget(TextCompare(text_a=text_a, text_b=text_b, min_match_words=3))
|
| 190 |
+
compare
|
| 191 |
+
return compare, text_a, text_b
|
| 192 |
+
|
| 193 |
+
|
| 194 |
+
@app.cell
|
| 195 |
+
def _(compare, mo):
|
| 196 |
+
mo.md(f"**Found {len(compare.matches)} matching passages**")
|
| 197 |
+
return
|
| 198 |
+
|
| 199 |
+
|
| 200 |
+
if __name__ == "__main__":
|
| 201 |
+
app.run()
|