Greg Wilson commited on
Commit
8fe65d4
·
1 Parent(s): 8eccedb

fix: finalizing notebooks for relaunch

Browse files
.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 the [vega-datasets](https://github.com/vega/vega-datasets) repository. Some of these datasets are directly available as Pandas data frames:
80
  """)
81
  return
82
 
83
 
84
  @app.cell
85
  def _():
86
- from vega_datasets import data # import vega_datasets
87
- cars = data.cars() # load cars data as a Pandas data frame
88
- cars.head() # display the first five rows
89
  return cars, data
90
 
91
 
92
  @app.cell(hide_code=True)
93
  def _(mo):
94
  mo.md(r"""
95
- Datasets in the vega-datasets collection can also be accessed via URLs:
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 from the [vega-datasets](https://github.com/vega/vega-datasets) collection into a Pandas data frame.
66
  """)
67
  return
68
 
69
 
70
  @app.cell
71
  def _():
72
- from vega_datasets import data as vega_data
73
- data = vega_data.gapminder()
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 vega_datasets package, and then read the data into a Pandas data frame so that we can inspect its contents.
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 precendence 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
  """)
 
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 vega_datasets import data
 
 
55
 
56
  return alt, data
57
 
@@ -131,7 +132,8 @@ def _(data):
131
 
132
  @app.cell
133
  def _(data):
134
- world_topo = data.world_110m()
 
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 faeture 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:
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 retreive 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.
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) and [Vega Datasets](https://github.com/altair-viz/vega_datasets) packages. If you are running this notebook on [Colab](https://colab.research.google.com), Altair and Vega Datasets 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.
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 vega_datasets
66
  ```
67
 
68
  Or if you use [Conda](https://conda.io)
69
 
70
  ```bash
71
- conda install -c conda-forge altair vega_datasets
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 vega_datasets !pip install altair vega_datasets
82
  return
83
 
84
 
85
  @app.cell
86
  def _():
87
  import altair as alt
88
- from vega_datasets import data
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 vega_datasets
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 vaid field.
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
- # "cvxpy-base",
 
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
- # "cvxpy-base",
 
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
- # "cvxpy-base",
 
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
- # "cvxpy-base",
 
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
- # "cvxpy-base",
 
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
- - what *is* a notebook?
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
- ## Ways to Teach With marimo
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
- **Works For:** Any audience
96
 
97
- **Format:** Assignments and labs
98
 
99
- **Pro:** Focus attention on a specific concept (e.g., filtering data)
100
 
101
- **Con:** “Just get AI to do it”; required work can be too easy or too hard
102
 
103
- ### Tweak and twiddle
104
 
105
- **Description:** Learner starts with complete working notebook, is asked to alter parameters to achieve some goal
106
 
107
- **Use For:** Compare and contrast; acquiring domain knowledge
108
 
109
- **Works For:** Learners without programming experience (but requires some domain knowledge)
110
 
111
- **Format:** Fixed-time workshop exercise; pair programming
112
 
113
- **Pro:** Helps learners overcome code anxiety
114
 
115
- **Con:** “Where do I start?” and going down rabbit holes
116
 
117
- ### Notebook as app
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
- ### Top-down delivery
132
 
133
- **Description:** Give learners just enough control to get to a motivating result quickly (“day one”)
134
 
135
- **Use For:** Follow-along lectures
136
 
137
- **Works For:** Any audience (but most engaging for people with low programming skills)
138
 
139
- **Format:** Tutorials and workshops (synchronous)
140
 
141
- **Pro:** Student engagement
142
 
143
- **Con:** Hard to get the right level of detail for a mixed-ability audience
 
144
 
145
- ### Coding as translation
 
146
 
147
- **Description:** Convert prose to code (or vice versa)
 
148
 
149
- **Use For:** Connect concepts to implementation (and implementation to concepts)
 
150
 
151
- **Works For:** Learners who understand theory but struggle with coding (or vice versa)
152
 
153
- **Format:** Notebook with scaffolding text and possibly some (scaffolded) code
 
 
154
 
155
- **Pro:** Low barrier to entry for learners with limited programming knowledge
156
 
157
- **Con:** Hard to get the level right for mixed-ability audience
158
 
159
- ### Symbolic math
160
 
161
- **Description:** Use SymPy for symbolic math in notebook
162
 
163
- **Use For:** Extension of previous exercise: convert math to code or code to math
164
 
165
- **Works For:** STEM students interested in theory
166
 
167
- **Format:** Any
168
 
169
- **Pro:** Introduce another real-world tool
170
 
171
- **Con:** Math in SymPy is yet another thing to learn
172
 
173
- ### Numerical methods / simulation
174
 
175
- **Description:** Use calculation or simulation instead of formulaic analysis
176
 
177
- **Use For:** Make concepts tangible before introducing mathematical abstraction
178
 
179
- **Works For:** Learners with some programming skill
180
 
181
- **Format:** Any
182
 
183
- **Pro:** Going from specific to general is often more engaging and approachable
184
 
185
- **Con:** Requires programming skill; can be hard to debug
186
 
187
  ### Learn an API
188
 
189
- **Description:** Introduce a key API example by example
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
- **Use For:** Engagement
206
 
207
- **Works For:** Learners with specific domain interest (e.g., sports analytics)
208
 
209
- **Format:** Common first half, learners explore on their own for second half; learners create presentations to share with others
210
 
211
- **Pro:** Improves self-efficacy; leverages engagement with personal interests
212
 
213
- **Con:** Can’t find data, data is too messy, learners’ interest don’t overlap
214
 
215
- ### Test-driven learning
216
-
217
- **Description:** Instructor provides notebook full of tests; learners must write code to make those tests pass (e.g., handle messy data)
218
-
219
- **Use For:** Think in terms of a spec
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
- **Use For:** Learning critical thinking
248
 
249
- **Works For:** Learners with enough programming experience to be able to debug systematically
 
 
250
 
251
- **Format:** Works well as homework exercise
 
 
252
 
253
- **Pro:** Helps learners appreciate how hard it is to write robust code; improves their debugging skills
254
 
255
- **Con:** Learners can break code in repetitive ways (e.g., provide several inputs that trigger the same flaw)
256
 
257
- ## Acknowledgments
258
 
259
- Much of this is inspired by or taken from
260
- [*Teaching and Learning with Jupyter*](https://jupyter4edu.github.io/jupyter-edu-book/).
261
 
262
  ## Appendix: Learner Personas
263
 
264
- ### Anya Academic
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
- **Goals**
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
- **Complications**
279
 
280
- 1. Is concerned about tool setup and maintenance overheads. Doesn't have time to completely rewrite courses, so will only move over if there's an incremental migration path that allows her to back out if thing don't appear to be working.
281
 
282
- 2. Anya's department has two overworked IT staff, and nothing at her university is allowed to go beyond the pilot phase if it doesn't integrate with the LMS somehow.
283
 
284
  ### Ellis Engineer
285
 
286
- **Background:** Senior undergraduate in mechanical engineering who just returned to school from their third and final co-op placement. They are very excited about drones.
287
 
288
- **Relevant Experience:** Used Jupyter notebooks with Colab in their second semester. They are comfortable with NumPy and Altair and has bumped into Pandas, but has done as many classes with MATLAB and AutoCAD as with Python.
289
 
290
- **Goals**
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
- **Background:** Undergraduate business student; decided not to minor in CS because "AI is going to eat all those jobs". Nang chooses courses, tools, and interests based primarily on what the web tells him potential future employers are going to look for. He routinely uses ChatGPT for help with homework.
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
- **Complications**
313
 
314
- Nang is taking five courses and volunteering with two campus clubs (one for the sake of his CV, and one because of his passion for basketball), so he is chronically over-committed.
315
 
316
  ## Appendix: KaTeX vs. MathJax
317
 
318
- marimo uses [KaTeX](https://katex.org/) for rendering math (faster, slightly narrower coverage, silent errors) rather than [MathJax](https://www.mathjax.org/).
319
 
320
- ### Use raw strings
321
 
322
  LaTeX lives in Python strings in marimo, so use `r"..."` to preserve backslashes:
323
 
324
  ```python
325
- mo.md(r"$\\frac{1}{2}$") #
326
- mo.md("$\\frac{1}{2}$") # \\f is a form-feed character
327
  ```
328
 
329
- ### MathJax KaTeX
330
 
331
  | Category | MathJax | KaTeX |
332
  | --- | --- | --- |
333
- | Text | `\\mbox`, `\\bbox` | `\\text{}` |
334
- | Text style | `\\textsc`, `\\textsl` | `\\text{}` |
335
- | Environments | `\\begin{eqnarray}` | `\\begin{align}` |
336
- | | `\\begin{multline}` | `\\begin{gather}` |
337
- | References | `\\label`, `\\eqref`, `\\ref` | `\\tag{}` for manual numbering |
338
- | Arrays | `\\cline`, `\\multicolumn`, `\\hfill`, `\\vline` | |
339
- | Macros | `\\DeclareMathOperator` | `\\operatorname{}` inline |
340
- | | `\\newenvironment` | |
341
- | Spacing | `\\mspace`, `\\setlength`, `\\strut`, `\\rotatebox` | |
342
- | Conditionals | `\\if`, `\\else`, `\\fi`, `\\ifx` | |
343
 
344
- These *do* work in KaTeX (despite outdated claims): `\\newcommand`, `\\def`, `\\hbox`, `\\hskip`, `\\cal`, `\\pmb`, `\\begin{equation}`, `\\begin{split}`, `\\operatorname*`.
345
 
346
- ### Shared macros across cells
347
 
348
- `\\newcommand` works inline. For cross-cell reuse, use `mo.latex(filename="macros.tex")` in the same cell as `import marimo`.
349
 
350
- ### Migration checklist
351
 
352
- 1. Find-replace `\\mbox{` `\\text{`
353
- 2. Use raw strings (`r"..."`)
354
- 3. Replace `\\begin{eqnarray}` `\\begin{align}`
355
- 4. Replace `\\DeclareMathOperator` `\\operatorname{}`
356
- 5. Remove `\\label`/`\\eqref` use `\\tag{}` if needed
357
- 6. Visually verify KaTeX fails silently
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
- sinks = [
497
- lz.sink_csv(folder / "data_1.csv", lazy=True),
498
- lz2.sink_csv(folder / "data_2.csv", lazy=True),
499
- lz3.sink_csv(folder / "data_3.csv", lazy=True),
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
- # If you want to write to a file, use `lz.sink_format(lazy=True)` followed by `...collect_async()` or `pl.collect_all_async(...)`
524
- _ = await lz.sink_csv(folder / "data_from_async.csv", lazy=True).collect_async()
525
- _ = await pl.collect_all_async(sinks)
 
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
- df = (
66
- lz
67
- # Filter data we consider relevant (somewhat arbitrary in this example)
68
- .filter(pl.col("explicit") == False)
69
- .drop("Unnamed: 0", "track_id", "explicit")
70
- .with_columns(
71
- # Perform whichever transformations you want (again somewhat arbitrary in this example)
72
- # Convert the duration from milliseconds to seconds (int)
73
- pl.col("duration_ms").floordiv(1_000).alias("duration_seconds"),
74
- # Convert the popularity from an integer 0 ~ 100 to a percentage 0 ~ 1.0
75
- pl.col("popularity").truediv(100),
 
 
 
 
76
  )
77
- # lastly, download (if needed) and collect into memory
78
- .collect()
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.3.13",
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
- dirty_stations = raw_stations.select(
748
- pl.col("id_estacao").alias("station"),
749
- pl.col("estacao").alias("name"),
750
- pl.col("latitude").alias("lat"),
751
- pl.col("longitude").alias("lon"),
752
- pl.col("cota").alias("altitude"),
753
- pl.col("situacao").alias("situation"),
754
- pl.col("endereco").alias("address"),
755
- pl.col("data_inicio_operacao").alias("operation_start_date"),
756
- pl.col("data_fim_operacao").alias("operation_end_date"),
757
- ).collect()
 
 
 
758
  return (dirty_stations,)
759
 
760
 
761
  @app.cell
762
- def _(pl, raw_weather):
763
- dirty_weather_naive = raw_weather.select(
764
- pl.col("id_estacao").alias("station"),
765
- pl.col("acumulado_chuva_15_min").alias("accumulated_rain_15_minutes"),
766
- pl.concat_str("data_particao", pl.lit("T"), "horario").str.to_datetime(time_zone=None).alias("datetime"),
767
- ).collect()
 
 
 
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
- log_data_erroneous.collect()
 
 
 
295
  return
296
 
297
 
@@ -307,12 +310,15 @@ def _(mo):
307
 
308
  @app.cell
309
  def _(log_data, pl):
310
- (
311
- log_data.pivot(index="time", on="request_code",
312
- values="status", aggregate_function="len")
313
- .filter(pl.col("POST").is_null())
314
- .collect()
315
- )
 
 
 
316
  return
317
 
318
 
@@ -438,7 +444,10 @@ def _(mo):
438
 
439
  @app.cell
440
  def _(a_query):
441
- a_query.collect(engine="streaming")
 
 
 
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. The data being sunk can also be partitioned into multiple files if needed, after specifying a suitable partitioning strategy, as shown below.
495
  """)
496
  return
497
 
498
 
499
  @app.cell
500
- def _(a_query, pl):
501
- (
502
- a_query
503
- .sink_parquet(
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
- _q1 = a_query.sink_parquet("log_data_filtered.parquet", lazy=True)
524
- _q2 = a_query.sink_ipc("log_data_filtered.ipc", lazy=True)
525
- pl.collect_all([_q1, _q2])
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, max_capacity=k), Queue(env, max_capacity=k)]
244
  else:
245
- lanes = [Queue(env, max_capacity=k)]
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, max_capacity=buffer_capacity)
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()