├── .github ├── ISSUE_TEMPLATE │ └── example_request.yaml ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── deploy.yml │ ├── hf_sync.yml │ └── typos.yaml ├── .gitignore ├── .typos.toml ├── Dockerfile ├── Makefile ├── README.md ├── _server ├── README.md └── main.py ├── assets └── marimo-learn.png ├── daft ├── 01_what_makes_daft_special.py └── README.md ├── duckdb ├── 009_loading_json.py ├── 01_getting_started.py ├── DuckDB_Loading_CSVs.py └── README.md ├── functional_programming ├── 05_functors.py ├── 06_applicatives.py ├── CHANGELOG.md └── README.md ├── optimization ├── 01_least_squares.py ├── 02_linear_program.py ├── 03_minimum_fuel_optimal_control.py ├── 04_quadratic_program.py ├── 05_portfolio_optimization.py ├── 06_convex_optimization.py ├── 07_sdp.py └── README.md ├── polars ├── 01_why_polars.py ├── 02_dataframes.py ├── 04_basic_operations.py ├── 05_reactive_plots.py ├── 08_working_with_columns.py ├── 09_data_types.py ├── 10_strings.py ├── 12_aggregations.py ├── 13_window_functions.py ├── 14_user_defined_functions.py └── README.md ├── probability ├── 01_sets.py ├── 02_axioms.py ├── 03_probability_of_or.py ├── 04_conditional_probability.py ├── 05_independence.py ├── 06_probability_of_and.py ├── 07_law_of_total_probability.py ├── 08_bayes_theorem.py ├── 09_random_variables.py ├── 10_probability_mass_function.py ├── 11_expectation.py ├── 12_variance.py ├── 13_bernoulli_distribution.py ├── 14_binomial_distribution.py ├── 15_poisson_distribution.py ├── 16_continuous_distribution.py ├── 17_normal_distribution.py ├── 18_central_limit_theorem.py ├── 19_maximum_likelihood_estimation.py ├── 20_naive_bayes.py ├── 21_logistic_regression.py └── README.md ├── python ├── 001_numbers.py ├── 002_strings.py ├── 003_collections.py ├── 004_conditional_logic.py ├── 005_loops.py ├── 006_dictionaries.py ├── 007_advanced_collections.py ├── 008_functions.py ├── 009_modules.py ├── 010_exceptions.py └── README.md └── scripts ├── build.py ├── preview.py └── templates └── index.html /.github/ISSUE_TEMPLATE/example_request.yaml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://raw.githubusercontent.com/SchemaStore/schemastore/refs/heads/master/src/schemas/json/github-issue-forms.json 2 | name: '🚀 Course or notebook proposal' 3 | description: Request a course or notebook 4 | type: Example 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Let us know what you'd like to see added! 10 | - type: textarea 11 | id: example-description 12 | attributes: 13 | label: Description 14 | description: 'Description of the notebook or course to add. Please make the reason and usecases as detailed as possible. If you intend to submit a PR for this issue, tell us in the description. Thanks!' 15 | placeholder: I would like a notebook or course on ... 16 | validations: 17 | required: true 18 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## 📝 Summary 2 | 3 | 8 | 9 | ## 📋 Checklist 10 | 11 | - [ ] I have included package dependencies in the notebook file [using `--sandbox`](https://docs.marimo.io/guides/package_reproducibility/) 12 | - [ ] If adding a course, include a `README.md` 13 | - [ ] Keep language direct and simple. 14 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to GitHub Pages 2 | 3 | on: 4 | push: 5 | branches: ['main'] 6 | workflow_dispatch: 7 | 8 | concurrency: 9 | group: 'pages' 10 | cancel-in-progress: false 11 | 12 | env: 13 | UV_SYSTEM_PYTHON: 1 14 | 15 | jobs: 16 | build: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v4 20 | 21 | - name: 🚀 Install uv 22 | uses: astral-sh/setup-uv@v4 23 | 24 | - name: 🐍 Set up Python 25 | uses: actions/setup-python@v5 26 | with: 27 | python-version: 3.12 28 | 29 | - name: 📦 Install dependencies 30 | run: | 31 | uv pip install marimo jinja2 32 | 33 | - name: 🛠️ Export notebooks 34 | run: | 35 | python scripts/build.py 36 | 37 | - name: 📤 Upload artifact 38 | uses: actions/upload-pages-artifact@v3 39 | with: 40 | path: _site 41 | 42 | deploy: 43 | needs: build 44 | 45 | permissions: 46 | pages: write 47 | id-token: write 48 | 49 | environment: 50 | name: github-pages 51 | url: ${{ steps.deployment.outputs.page_url }} 52 | runs-on: ubuntu-latest 53 | steps: 54 | - name: 🚀 Deploy to GitHub Pages 55 | id: deployment 56 | uses: actions/deploy-pages@v4 57 | -------------------------------------------------------------------------------- /.github/workflows/hf_sync.yml: -------------------------------------------------------------------------------- 1 | name: Sync to Hugging Face hub 2 | on: 3 | push: 4 | branches: [main] 5 | 6 | # to run this workflow manually from the Actions tab 7 | workflow_dispatch: 8 | 9 | jobs: 10 | sync-to-hub: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | with: 15 | fetch-depth: 0 16 | 17 | - name: Configure Git 18 | run: | 19 | git config --global user.name "GitHub Action" 20 | git config --global user.email "action@github.com" 21 | 22 | - name: Prepend frontmatter to README 23 | run: | 24 | if [ -f README.md ] && ! grep -q "^---" README.md; then 25 | FRONTMATTER="--- 26 | title: marimo learn 27 | emoji: 🧠 28 | colorFrom: blue 29 | colorTo: indigo 30 | sdk: docker 31 | sdk_version: \"latest\" 32 | app_file: app.py 33 | pinned: false 34 | --- 35 | 36 | " 37 | echo "$FRONTMATTER$(cat README.md)" > README.md 38 | git add README.md 39 | git commit -m "Add HF frontmatter to README" || echo "No changes to commit" 40 | fi 41 | 42 | - name: Push to hub 43 | env: 44 | HF_TOKEN: ${{ secrets.HF_TOKEN }} 45 | run: git push -f https://mylessss:$HF_TOKEN@huggingface.co/spaces/marimo-team/marimo-learn main 46 | -------------------------------------------------------------------------------- /.github/workflows/typos.yaml: -------------------------------------------------------------------------------- 1 | name: Typos 2 | 3 | # Run on pull requests 4 | on: 5 | pull_request: {} 6 | 7 | jobs: 8 | typos: 9 | name: Check for typos 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: 🛑 Cancel Previous Runs 13 | uses: styfle/cancel-workflow-action@0.12.1 14 | - uses: actions/checkout@v4 15 | - uses: crate-ci/typos@v1.29.4 16 | with: 17 | config: .typos.toml 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # UV 98 | # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | #uv.lock 102 | 103 | # poetry 104 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 105 | # This is especially recommended for binary packages to ensure reproducibility, and is more 106 | # commonly ignored for libraries. 107 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 108 | #poetry.lock 109 | 110 | # pdm 111 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 112 | #pdm.lock 113 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 114 | # in version control. 115 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 116 | .pdm.toml 117 | .pdm-python 118 | .pdm-build/ 119 | 120 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 121 | __pypackages__/ 122 | 123 | # Celery stuff 124 | celerybeat-schedule 125 | celerybeat.pid 126 | 127 | # SageMath parsed files 128 | *.sage.py 129 | 130 | # Environments 131 | .env 132 | .venv 133 | env/ 134 | venv/ 135 | ENV/ 136 | env.bak/ 137 | venv.bak/ 138 | 139 | # Spyder project settings 140 | .spyderproject 141 | .spyproject 142 | 143 | # Rope project settings 144 | .ropeproject 145 | 146 | # mkdocs documentation 147 | /site 148 | 149 | # mypy 150 | .mypy_cache/ 151 | .dmypy.json 152 | dmypy.json 153 | 154 | # Pyre type checker 155 | .pyre/ 156 | 157 | # pytype static type analyzer 158 | .pytype/ 159 | 160 | # Cython debug symbols 161 | cython_debug/ 162 | 163 | # PyCharm 164 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 165 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 166 | # and can be added to the global gitignore or merged into this file. For a more nuclear 167 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 168 | #.idea/ 169 | 170 | # PyPI configuration file 171 | .pypirc 172 | 173 | # Marimo specific 174 | __marimo__ 175 | 176 | # Generated site content 177 | _site/ 178 | -------------------------------------------------------------------------------- /.typos.toml: -------------------------------------------------------------------------------- 1 | [default] 2 | extend-ignore-re = [ 3 | # LaTeX math expressions 4 | "\\\\\\[.*?\\\\\\]", 5 | "\\\\\\(.*?\\\\\\)", 6 | "\\$\\$.*?\\$\\$", 7 | "\\$.*?\\$", 8 | # LaTeX commands 9 | "\\\\[a-zA-Z]+\\{.*?\\}", 10 | "\\\\[a-zA-Z]+", 11 | # LaTeX subscripts and superscripts 12 | "_\\{.*?\\}", 13 | "\\^\\{.*?\\}" 14 | ] 15 | 16 | # Words to explicitly accept 17 | [default.extend-words] 18 | pn = "pn" 19 | 20 | # You can also exclude specific files or directories if needed 21 | # [files] 22 | # extend-exclude = ["*.tex", "docs/*.md"] -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim 2 | 3 | WORKDIR /app 4 | 5 | # Create a non-root user 6 | RUN useradd -m appuser 7 | 8 | # Copy application files 9 | COPY _server/main.py _server/main.py 10 | COPY polars/ polars/ 11 | COPY duckdb/ duckdb/ 12 | 13 | # Set proper ownership 14 | RUN chown -R appuser:appuser /app 15 | 16 | # Switch to non-root user 17 | USER appuser 18 | 19 | # Create virtual environment and install dependencies 20 | RUN uv venv 21 | RUN uv export --script _server/main.py | uv pip install -r - 22 | 23 | ENV PORT=7860 24 | EXPOSE 7860 25 | 26 | CMD ["uv", "run", "_server/main.py"] 27 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | install: 2 | uv pip install marimo jinja2 markdown 3 | 4 | build: 5 | rm -rf _site 6 | uv run scripts/build.py 7 | 8 | serve: 9 | uv run python -m http.server --directory _site -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 |

6 | A curated collection of educational marimo notebooks || 💬 Discord 7 |

8 | 9 | # 📚 Learn 10 | 11 | This repository contains a curated collection of educational 12 | [marimo](https://github.com/marimo-team/marimo) notebooks. These Python notebooks, 13 | organized into courses, explain fundamental concepts for an expanding set of 14 | topics, including academic topics like computer science, linear algebra, 15 | probability, and machine learning, as well as applied topics such as how to use 16 | popular packages such as polars, pytorch, matplotlib, and more. 17 | 18 | Our goal with `marimo learn` is to create a central repository of high quality 19 | notebooks for educators, students, and practitioners. 20 | 21 | ## Courses 22 | 23 | - 🐍 [Python](python/): Learn the basics of the Python programming language 24 | - ⚖️ [Optimization](optimization/): Learn how to solve optimization problems, with applications to finance and control 25 | 26 | **Coming soon.** 27 | 28 | - 🎲 Probability 29 | - 📏 Linear algebra 30 | - ❄️ Polars 31 | - 🔥 Pytorch 32 | - 🗄️ Duckdb 33 | - 💜 Daft 34 | - 📈 Altair 35 | - 📈 Plotly 36 | - 📈 matplotlib 37 | 38 | 39 | _We're seeking contributions! If you'd like to contribute, please [reach out](https://github.com/marimo-team/learn/issues/new?template=example_request.yaml)._ 40 | 41 | ## Adding notebooks 42 | 43 | We welcome community contributions of notebooks and entire courses (folders of 44 | notebooks on a single topic). 45 | 46 | To get started, please start by [opening an issue](https://github.com/marimo-team/learn/issues/new?template=example_request.yaml) and proposing the notebook 47 | or course you'd like to contribute. Once your proposal is accepted, draft 48 | your notebook and submit it for review as a pull request. We hold learn notebooks to a high 49 | standard, and we may go through a few rounds of reviews before your notebook 50 | is merged. 51 | 52 | Here's a contribution checklist: 53 | 54 | - [ ] Include package dependencies in notebook files [using 55 | `--sandbox`](https://docs.marimo.io/guides/package_reproducibility/) 56 | - [ ] If adding a course, include a `README.md` 57 | - [ ] Keep language direct and simple. 58 | 59 | If you aren't comfortable adding a new notebook or course, you can also request 60 | what you'd like to see by [filing an issue](https://github.com/marimo-team/learn/issues/new?template=example_request.yaml). 61 | 62 | ## Building and Previewing 63 | 64 | The site is built using a Python script that exports marimo notebooks to HTML and generates an index page. 65 | 66 | ```bash 67 | # Build the site 68 | python scripts/build.py --output-dir _site 69 | 70 | # Preview the site (builds first) 71 | python scripts/preview.py 72 | 73 | # Preview without rebuilding 74 | python scripts/preview.py --no-build 75 | ``` 76 | 77 | ## Community 78 | 79 | We're building a community. Come hang out with us! 80 | 81 | - 🌟 [Star us on GitHub](https://github.com/marimo-team/examples) 82 | - 💬 [Chat with us on Discord](https://discord.gg/rT48v2Y9fe) 83 | - 📧 [Subscribe to our Newsletter](https://marimo.io/newsletter) 84 | - ☁️ [Join our Cloud Waitlist](https://marimo.io/cloud) 85 | - ✏️ [Start a GitHub Discussion](https://github.com/marimo-team/marimo/discussions) 86 | - 🦋 [Follow us on Bluesky](https://bsky.app/profile/marimo.io) 87 | - 🐦 [Follow us on Twitter](https://twitter.com/marimo_io) 88 | - 🕴️ [Follow us on LinkedIn](https://www.linkedin.com/company/marimo-io) 89 | 90 | 91 |

92 | 93 |

94 | -------------------------------------------------------------------------------- /_server/README.md: -------------------------------------------------------------------------------- 1 | # marimo learn server 2 | 3 | This folder contains server code for hosting marimo apps. 4 | 5 | ## Running the server 6 | 7 | ```bash 8 | cd _server 9 | uv run --no-project main.py 10 | ``` 11 | 12 | ## Building a Docker image 13 | 14 | From the root directory, run: 15 | 16 | ```bash 17 | docker build -t marimo-learn . 18 | ``` 19 | 20 | ## Running the Docker container 21 | 22 | ```bash 23 | docker run -p 7860:7860 marimo-learn 24 | ``` 25 | -------------------------------------------------------------------------------- /_server/main.py: -------------------------------------------------------------------------------- 1 | # /// script 2 | # requires-python = ">=3.12" 3 | # dependencies = [ 4 | # "fastapi", 5 | # "marimo", 6 | # "starlette", 7 | # "python-dotenv", 8 | # "pydantic", 9 | # "duckdb", 10 | # "altair==5.5.0", 11 | # "beautifulsoup4==4.13.3", 12 | # "httpx==0.28.1", 13 | # "marimo", 14 | # "nest-asyncio==1.6.0", 15 | # "numba==0.61.0", 16 | # "numpy==2.1.3", 17 | # "polars==1.24.0", 18 | # ] 19 | # /// 20 | 21 | import logging 22 | import os 23 | from pathlib import Path 24 | 25 | import marimo 26 | from dotenv import load_dotenv 27 | from fastapi import FastAPI, Request 28 | from fastapi.responses import HTMLResponse 29 | 30 | # Load environment variables 31 | load_dotenv() 32 | 33 | # Set up logging 34 | logging.basicConfig(level=logging.INFO) 35 | logger = logging.getLogger(__name__) 36 | 37 | # Get port from environment variable or use default 38 | PORT = int(os.environ.get("PORT", 7860)) 39 | 40 | root_dir = Path(__file__).parent.parent 41 | 42 | ROOTS = [ 43 | root_dir / "polars", 44 | root_dir / "duckdb", 45 | ] 46 | 47 | 48 | server = marimo.create_asgi_app(include_code=True) 49 | app_names: list[str] = [] 50 | 51 | for root in ROOTS: 52 | for filename in root.iterdir(): 53 | if filename.is_file() and filename.suffix == ".py": 54 | app_path = root.stem + "/" + filename.stem 55 | server = server.with_app(path=f"/{app_path}", root=str(filename)) 56 | app_names.append(app_path) 57 | 58 | # Create a FastAPI app 59 | app = FastAPI() 60 | 61 | logger.info(f"Mounting {len(app_names)} apps") 62 | for app_name in app_names: 63 | logger.info(f" /{app_name}") 64 | 65 | 66 | @app.get("/") 67 | async def home(request: Request): 68 | html_content = """ 69 | 70 | 71 | 72 | marimo learn 73 | 74 | 75 |

Welcome to marimo learn!

76 |

This is a collection of interactive tutorials for learning data science libraries with marimo.

77 |

Check out the GitHub repository for more information.

78 | 79 | 80 | """ 81 | return HTMLResponse(content=html_content) 82 | 83 | 84 | app.mount("/", server.build()) 85 | 86 | # Run the server 87 | if __name__ == "__main__": 88 | import uvicorn 89 | 90 | uvicorn.run(app, host="0.0.0.0", port=PORT, log_level="info") 91 | -------------------------------------------------------------------------------- /assets/marimo-learn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marimo-team/learn/a50306f0bf215ade3aa2f735d72deff2d9ec267f/assets/marimo-learn.png -------------------------------------------------------------------------------- /daft/README.md: -------------------------------------------------------------------------------- 1 | # Learn Daft 2 | 3 | _🚧 This collection is a work in progress. Please help us add notebooks!_ 4 | 5 | This collection of marimo notebooks is designed to teach you the basics of 6 | Daft, a distributed dataframe engine that unifies data engineering, analytics & ML/AI workflows. 7 | 8 | **Help us build this course! ⚒️** 9 | 10 | We're seeking contributors to help us build these notebooks. Every contributor 11 | will be acknowledged as an author in this README and in their contributed 12 | notebooks. Head over to the [tracking 13 | issue](https://github.com/marimo-team/learn/issues/43) to sign up for a planned 14 | notebook or propose your own. 15 | 16 | **Running notebooks.** To run a notebook locally, use 17 | 18 | ```bash 19 | uvx marimo edit 20 | ``` 21 | 22 | You can also open notebooks in our online playground by appending marimo.app/ to a notebook's URL. 23 | 24 | **Thanks to all our notebook authors!** 25 | 26 | * [Péter Gyarmati](https://github.com/peter-gy) 27 | -------------------------------------------------------------------------------- /duckdb/009_loading_json.py: -------------------------------------------------------------------------------- 1 | # /// script 2 | # requires-python = ">=3.11" 3 | # dependencies = [ 4 | # "marimo", 5 | # "duckdb==1.2.1", 6 | # "sqlglot==26.11.1", 7 | # "polars[pyarrow]==1.25.2", 8 | # ] 9 | # /// 10 | 11 | import marimo 12 | 13 | __generated_with = "0.12.8" 14 | app = marimo.App(width="medium") 15 | 16 | 17 | @app.cell(hide_code=True) 18 | def _(mo): 19 | mo.md( 20 | r""" 21 | # Loading JSON 22 | 23 | DuckDB supports reading and writing JSON through the `json` extension that should be present in most distributions and is autoloaded on first-use. If it's not, you can [install and load](https://duckdb.org/docs/stable/data/json/installing_and_loading.html) it manually like any other extension. 24 | 25 | In this tutorial we'll cover 4 different ways we can transfer JSON data in and out of DuckDB: 26 | 27 | - [`FROM`](https://duckdb.org/docs/stable/sql/query_syntax/from.html) statement. 28 | - [`read_json`](https://duckdb.org/docs/stable/data/json/loading_json#the-read_json-function) function. 29 | - [`COPY`](https://duckdb.org/docs/stable/sql/statements/copy#copy--from) statement. 30 | - [`IMPORT DATABASE`](https://duckdb.org/docs/stable/sql/statements/export.html) statement. 31 | """ 32 | ) 33 | return 34 | 35 | 36 | @app.cell(hide_code=True) 37 | def _(mo): 38 | mo.md( 39 | r""" 40 | ## Using `FROM` 41 | 42 | Loading data using `FROM` is simple and straightforward. We use a path or URL to the file we want to load where we'd normally put a table name. When we do this, DuckDB attempts to infer the right way to read the file including the correct format and column types. In most cases this is all we need to load data into DuckDB. 43 | """ 44 | ) 45 | return 46 | 47 | 48 | @app.cell 49 | def _(mo): 50 | _df = mo.sql( 51 | f""" 52 | SELECT * FROM 'https://raw.githubusercontent.com/vega/vega-datasets/refs/heads/main/data/cars.json'; 53 | """ 54 | ) 55 | return 56 | 57 | 58 | @app.cell(hide_code=True) 59 | def _(mo): 60 | mo.md( 61 | r""" 62 | ## Using `read_json` 63 | 64 | For greater control over how the JSON is read, we can directly call the [`read_json`](https://duckdb.org/docs/stable/data/json/loading_json#the-read_json-function) function. It supports a few different arguments — some common ones are: 65 | 66 | - `format='array'` or `format='newline_delimited'` - the former tells DuckDB that the rows should be read from a top-level JSON array while the latter means the rows should be read from JSON objects separated by a newline (JSONL/NDJSON). 67 | - `ignore_errors=true` - skips lines with parse errors when reading newline delimited JSON. 68 | - `columns={columnName: type, ...}` - lets you set types for individual columns manually. 69 | - `dateformat` and `timestampformat` - controls how DuckDB attempts to parse [Date](https://duckdb.org/docs/stable/sql/data_types/date) and [Timestamp](https://duckdb.org/docs/stable/sql/data_types/timestamp) types. Use the format specifiers specified in the [docs](https://duckdb.org/docs/stable/sql/functions/dateformat.html#format-specifiers). 70 | 71 | We could rewrite the previous query more explicitly as: 72 | """ 73 | ) 74 | return 75 | 76 | 77 | @app.cell 78 | def _(mo): 79 | cars_df = mo.sql( 80 | f""" 81 | SELECT * 82 | FROM 83 | read_json( 84 | 'https://raw.githubusercontent.com/vega/vega-datasets/refs/heads/main/data/cars.json', 85 | format = 'array', 86 | columns = {{ 87 | Name:'VARCHAR', 88 | Miles_per_Gallon:'FLOAT', 89 | Cylinders:'FLOAT', 90 | Displacement:'FLOAT', 91 | Horsepower:'FLOAT', 92 | Weight_in_lbs:'FLOAT', 93 | Acceleration:'FLOAT', 94 | Year:'DATE', 95 | Origin:'VARCHAR' 96 | }}, 97 | dateformat = '%Y-%m-%d' 98 | ) 99 | ; 100 | """ 101 | ) 102 | return (cars_df,) 103 | 104 | 105 | @app.cell(hide_code=True) 106 | def _(mo): 107 | mo.md(r"""Other than singular files we can read [multiple files](https://duckdb.org/docs/stable/data/multiple_files/overview.html) at a time by either passing a list of files or a UNIX glob pattern.""") 108 | return 109 | 110 | 111 | @app.cell(hide_code=True) 112 | def _(mo): 113 | mo.md( 114 | r""" 115 | ## Using `COPY` 116 | 117 | `COPY` is for useful both for importing and exporting data in a variety of formats including JSON. For example, we can import data into an existing table from a JSON file. 118 | """ 119 | ) 120 | return 121 | 122 | 123 | @app.cell 124 | def _(mo): 125 | _df = mo.sql( 126 | f""" 127 | CREATE OR REPLACE TABLE cars2 ( 128 | Name VARCHAR, 129 | Miles_per_Gallon VARCHAR, 130 | Cylinders VARCHAR, 131 | Displacement FLOAT, 132 | Horsepower FLOAT, 133 | Weight_in_lbs FLOAT, 134 | Acceleration FLOAT, 135 | Year DATE, 136 | Origin VARCHAR 137 | ); 138 | """ 139 | ) 140 | return (cars2,) 141 | 142 | 143 | @app.cell 144 | def _(cars2, mo): 145 | _df = mo.sql( 146 | f""" 147 | COPY cars2 FROM 'https://raw.githubusercontent.com/vega/vega-datasets/refs/heads/main/data/cars.json' (FORMAT json, ARRAY true, DATEFORMAT '%Y-%m-%d'); 148 | SELECT * FROM cars2; 149 | """ 150 | ) 151 | return 152 | 153 | 154 | @app.cell(hide_code=True) 155 | def _(mo): 156 | mo.md(r"""Similarly, we can write data from a table or select statement to a JSON file. For example, we create a new JSONL file with just the car names and miles per gallon. We first create a temporary directory to avoid cluttering our project directory.""") 157 | return 158 | 159 | 160 | @app.cell 161 | def _(Path): 162 | from tempfile import TemporaryDirectory 163 | 164 | TMP_DIR = TemporaryDirectory() 165 | COPY_PATH = Path(TMP_DIR.name) / "cars_mpg.jsonl" 166 | print(COPY_PATH) 167 | return COPY_PATH, TMP_DIR, TemporaryDirectory 168 | 169 | 170 | @app.cell 171 | def _(COPY_PATH, cars2, mo): 172 | _df = mo.sql( 173 | f""" 174 | COPY ( 175 | SELECT 176 | Name AS car_name, 177 | "Miles_per_Gallon" AS mpg 178 | FROM cars2 179 | WHERE mpg IS NOT null 180 | ) TO '{COPY_PATH}' (FORMAT json); 181 | """ 182 | ) 183 | return 184 | 185 | 186 | @app.cell 187 | def _(COPY_PATH, Path): 188 | Path(COPY_PATH).exists() 189 | return 190 | 191 | 192 | @app.cell(hide_code=True) 193 | def _(mo): 194 | mo.md( 195 | r""" 196 | ## Using `IMPORT DATABASE` 197 | 198 | The last method we can use to load JSON data is using the `IMPORT DATABASE` statement. It works in conjunction with `EXPORT DATABASE` to save and load an entire database to and from a directory. For example let's try and export our default in-memory database. 199 | """ 200 | ) 201 | return 202 | 203 | 204 | @app.cell 205 | def _(Path, TMP_DIR): 206 | EXPORT_PATH = Path(TMP_DIR.name) / "cars_export" 207 | print(EXPORT_PATH) 208 | return (EXPORT_PATH,) 209 | 210 | 211 | @app.cell 212 | def _(EXPORT_PATH, mo): 213 | _df = mo.sql( 214 | f""" 215 | EXPORT DATABASE '{EXPORT_PATH}' (FORMAT json); 216 | """ 217 | ) 218 | return 219 | 220 | 221 | @app.cell 222 | def _(EXPORT_PATH, Path): 223 | list(Path(EXPORT_PATH).iterdir()) 224 | return 225 | 226 | 227 | @app.cell(hide_code=True) 228 | def _(mo): 229 | mo.md(r"""We can then load the database back into DuckDB.""") 230 | return 231 | 232 | 233 | @app.cell 234 | def _(EXPORT_PATH, mo): 235 | _df = mo.sql( 236 | f""" 237 | DROP TABLE IF EXISTS cars2; 238 | IMPORT DATABASE '{EXPORT_PATH}'; 239 | SELECT * FROM cars2; 240 | """ 241 | ) 242 | return 243 | 244 | 245 | @app.cell(hide_code=True) 246 | def _(TMP_DIR): 247 | TMP_DIR.cleanup() 248 | return 249 | 250 | 251 | @app.cell(hide_code=True) 252 | def _(mo): 253 | mo.md( 254 | r""" 255 | ## Further Reading 256 | 257 | - Complete information on the JSON support in DuckDB can be found in their [documentation](https://duckdb.org/docs/stable/data/json/overview.html). 258 | - You can also learn more about using SQL in marimo from the [examples](https://github.com/marimo-team/marimo/tree/main/examples/sql). 259 | """ 260 | ) 261 | return 262 | 263 | 264 | @app.cell 265 | def _(): 266 | import marimo as mo 267 | from pathlib import Path 268 | return Path, mo 269 | 270 | 271 | if __name__ == "__main__": 272 | app.run() 273 | -------------------------------------------------------------------------------- /duckdb/DuckDB_Loading_CSVs.py: -------------------------------------------------------------------------------- 1 | # /// script 2 | # requires-python = ">=3.10" 3 | # dependencies = [ 4 | # "marimo", 5 | # "plotly.express", 6 | # "plotly==6.0.1", 7 | # "duckdb==1.2.1", 8 | # "sqlglot==26.11.1", 9 | # "pyarrow==19.0.1", 10 | # "polars==1.27.1", 11 | # ] 12 | # /// 13 | 14 | import marimo 15 | 16 | __generated_with = "0.12.10" 17 | app = marimo.App(width="medium") 18 | 19 | 20 | @app.cell(hide_code=True) 21 | def _(mo): 22 | mo.md(r"""#Loading CSVs with DuckDB""") 23 | return 24 | 25 | 26 | @app.cell(hide_code=True) 27 | def _(mo): 28 | mo.md( 29 | r""" 30 |

I remember when I first learnt about DuckDB, it was a gamechanger — I used to load the data I wanted to work on to a database software like MS SQL Server, and then build a bridge to an IDE with the language I wanted to use like Python, or R; it was quite the hassle. DuckDB changed my whole world — now I could just import the data file into the IDE, or notebook, make a duckdb connection, and there we go! But then, I realized I didn't even need the step of first importing the file using python. I could just query the csv file directly using SQL through a DuckDB connection.

31 | 32 | ##Introduction 33 |

I found this dataset on the evolution of AI research by discipline from OECD, and it piqued my interest. I feel like publications in natural language processing drastically jumped in the mid 2010s, and I'm excited to find out if that's the case.

34 | 35 |

In this notebook, we'll:

36 | 41 | """ 42 | ) 43 | return 44 | 45 | 46 | @app.cell(hide_code=True) 47 | def _(mo): 48 | mo.md(r"""##Load the CSV""") 49 | return 50 | 51 | 52 | @app.cell 53 | def _(mo): 54 | _df = mo.sql( 55 | f""" 56 | /* Another way to load the CSV could be 57 | SELECT * 58 | FROM read_csv('https://github.com/Mustjaab/Loading_CSVs_in_DuckDB/blob/main/AI_Research_Data.csv') 59 | */ 60 | SELECT * 61 | FROM "https://raw.githubusercontent.com/Mustjaab/Loading_CSVs_in_DuckDB/refs/heads/main/AI_Research_Data.csv" 62 | LIMIT 5; 63 | """ 64 | ) 65 | return 66 | 67 | 68 | @app.cell(hide_code=True) 69 | def _(mo): 70 | mo.md(r"""##Create Another Table""") 71 | return 72 | 73 | 74 | @app.cell 75 | def _(mo): 76 | Discipline_Analysis = mo.sql( 77 | f""" 78 | -- Build a table based on the CSV where it just contains the specified columns 79 | CREATE TABLE Domain_Analysis AS 80 | SELECT Year, Concept, publications FROM "https://raw.githubusercontent.com/Mustjaab/Loading_CSVs_in_DuckDB/refs/heads/main/AI_Research_Data.csv" 81 | """ 82 | ) 83 | return Discipline_Analysis, Domain_Analysis 84 | 85 | 86 | @app.cell 87 | def _(Domain_Analysis, mo): 88 | Analysis = mo.sql( 89 | f""" 90 | SELECT * 91 | FROM Domain_Analysis 92 | GROUP BY Concept, Year, publications 93 | ORDER BY Year 94 | """ 95 | ) 96 | return (Analysis,) 97 | 98 | 99 | @app.cell 100 | def _(Domain_Analysis, mo): 101 | _df = mo.sql( 102 | f""" 103 | SELECT 104 | AVG(CASE WHEN Year < 2020 THEN publications END) AS avg_pre_2020, 105 | AVG(CASE WHEN Year >= 2020 THEN publications END) AS avg_2020_onward 106 | FROM Domain_Analysis 107 | WHERE Concept = 'Natural language processing'; 108 | """ 109 | ) 110 | return 111 | 112 | 113 | @app.cell 114 | def _(Domain_Analysis, mo): 115 | NLP_Analysis = mo.sql( 116 | f""" 117 | SELECT 118 | publications, 119 | CASE 120 | WHEN Year < 2020 THEN 'Pre-2020' 121 | ELSE '2020-onward' 122 | END AS period 123 | FROM Domain_Analysis 124 | WHERE Year >= 2000 125 | AND Concept = 'Natural language processing'; 126 | """, 127 | output=False 128 | ) 129 | return (NLP_Analysis,) 130 | 131 | 132 | @app.cell 133 | def _(NLP_Analysis, px): 134 | px.box(NLP_Analysis, x='period', y='publications', color='period') 135 | return 136 | 137 | 138 | @app.cell(hide_code=True) 139 | def _(mo): 140 | mo.md(r"""

We can see there's a significant increase in NLP publications 2020 and onwards which definitely makes sense provided the rapid emergence of commercial large language models, and AI assistants.

""") 141 | 142 | @app.cell(hide_code=True) 143 | def _(mo): 144 | mo.md( 145 | r""" 146 | ##Conclusion 147 |

In this notebook, we learned how to:

148 | 153 | """ 154 | ) 155 | return 156 | 157 | 158 | @app.cell 159 | def _(): 160 | import pyarrow 161 | import polars 162 | return polars, pyarrow 163 | 164 | 165 | @app.cell 166 | def _(): 167 | import marimo as mo 168 | import plotly.express as px 169 | return mo, px 170 | 171 | 172 | if __name__ == "__main__": 173 | app.run() 174 | -------------------------------------------------------------------------------- /duckdb/README.md: -------------------------------------------------------------------------------- 1 | # Learn DuckDB 2 | 3 | _🚧 This collection is a work in progress. Please help us add notebooks!_ 4 | 5 | This collection of marimo notebooks is designed to teach you the basics of 6 | DuckDB, a fast in-memory OLAP engine that can interoperate with Dataframes. 7 | These notebooks also show how marimo gives DuckDB superpowers. 8 | 9 | **Help us build this course! ⚒️** 10 | 11 | We're seeking contributors to help us build these notebooks. Every contributor 12 | will be acknowledged as an author in this README and in their contributed 13 | notebooks. Head over to the [tracking 14 | issue](https://github.com/marimo-team/learn/issues/48) to sign up for a planned 15 | notebook or propose your own. 16 | 17 | **Running notebooks.** To run a notebook locally, use 18 | 19 | ```bash 20 | uvx marimo edit 21 | ``` 22 | 23 | You can also open notebooks in our online playground by appending marimo.app/ to a notebook's URL. 24 | 25 | 26 | **Authors.** 27 | 28 | Thanks to all our notebook authors! 29 | 30 | * [Mustjaab](https://github.com/Mustjaab) 31 | * [julius383](https://github.com/julius383) 32 | -------------------------------------------------------------------------------- /functional_programming/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog of the functional-programming course 2 | 3 | ## 2025-04-16 4 | 5 | **applicatives.py** 6 | 7 | - replace `return NotImplementedError` with `raise NotImplementedError` 8 | 9 | - add `Either` applicative 10 | - Add `Alternative` 11 | 12 | ## 2025-04-11 13 | 14 | **functors.py** 15 | 16 | - add `Bifunctor` section 17 | 18 | - replace `return NotImplementedError` with `raise NotImplementedError` 19 | 20 | ## 2025-04-08 21 | 22 | **functors.py** 23 | 24 | - restructure the notebook 25 | - replace `f` in the function signatures with `g` to indicate regular functions and 26 | distinguish from functors 27 | - move `Maybe` funtor to section `More Functor instances` 28 | 29 | - add `Either` functor 30 | 31 | - add `unzip` utility function for functors 32 | 33 | ## 2025-04-07 34 | 35 | **applicatives.py** 36 | 37 | - the `apply` method of `Maybe` _Applicative_ should return `None` when `fg` or `fa` is 38 | `None` 39 | 40 | - add `sequenceL` as a classmethod for `Applicative` and add examples for `Wrapper`, 41 | `Maybe`, `List` 42 | - add description for utility functions of `Applicative` 43 | 44 | - refine the implementation of `IO` _Applicative_ 45 | - reimplement `get_chars` with `IO.sequenceL` 46 | 47 | - add an example to show that `ListMonoidal` is equivalent to `List` _Applicative_ 48 | 49 | ## 2025-04-06 50 | 51 | **applicatives.py** 52 | 53 | - remove `sequenceL` from `Applicative` because it should be a classmethod but can't be 54 | generically implemented 55 | 56 | ## 2025-04-02 57 | 58 | **functors.py** 59 | 60 | - Migrate to `python3.13` 61 | 62 | - Replace all occurrences of 63 | 64 | ```python 65 | class Functor(Generic[A]) 66 | ``` 67 | 68 | with 69 | 70 | ```python 71 | class Functor[A] 72 | ``` 73 | 74 | for conciseness 75 | 76 | - Use `fa` in function signatures instead of `a` when `fa` is a _Functor_ 77 | 78 | **applicatives.py** 79 | 80 | - `0.1.0` version of notebook `06_applicatives.py` 81 | 82 | ## 2025-03-16 83 | 84 | **functors.py** 85 | 86 | - Use uppercased letters for `Generic` types, e.g. `A = TypeVar("A")` 87 | - Refactor the `Functor` class, changing `fmap` and utility methods to `classmethod` 88 | 89 | For example: 90 | 91 | ```python 92 | @dataclass 93 | class Wrapper(Functor, Generic[A]): 94 | value: A 95 | 96 | @classmethod 97 | def fmap(cls, f: Callable[[A], B], a: "Wrapper[A]") -> "Wrapper[B]": 98 | return Wrapper(f(a.value)) 99 | 100 | >>> Wrapper.fmap(lambda x: x + 1, wrapper) 101 | Wrapper(value=2) 102 | ``` 103 | 104 | - Move the `check_functor_law` method from `Functor` class to a standard function 105 | 106 | - Rename `ListWrapper` to `List` for simplicity 107 | - Remove the `Just` class 108 | 109 | - Rewrite proofs 110 | 111 | ## 2025-03-13 112 | 113 | **functors.py** 114 | 115 | - `0.1.0` version of notebook `05_functors` 116 | 117 | Thank [Akshay](https://github.com/akshayka) and [Haleshot](https://github.com/Haleshot) 118 | for reviewing 119 | 120 | ## 2025-03-11 121 | 122 | **functors.py** 123 | 124 | - Demo version of notebook `05_functors.py` 125 | -------------------------------------------------------------------------------- /functional_programming/README.md: -------------------------------------------------------------------------------- 1 | # Learn Functional Programming 2 | 3 | _🚧 This collection is a [work in progress](https://github.com/marimo-team/learn/issues/51)._ 4 | 5 | This series of marimo notebooks introduces the powerful paradigm of functional 6 | programming through Python. Taking inspiration from Haskell and Category 7 | Theory, we'll build a strong foundation in FP concepts that can transform how 8 | you approach software development. 9 | 10 | ## What You'll Learn 11 | 12 | **Using only Python's standard library**, we'll construct functional 13 | programming concepts from first principles. 14 | 15 | Topics include: 16 | 17 | + Currying and higher-order functions 18 | + Functors, Applicatives, and Monads 19 | + Category theory fundamentals 20 | 21 | ## Running Notebooks 22 | 23 | ### Locally 24 | 25 | To run a notebook locally, use 26 | 27 | ```bash 28 | uvx marimo edit 29 | ``` 30 | 31 | For example, run the `Functor` tutorial with 32 | 33 | ```bash 34 | uvx marimo edit https://github.com/marimo-team/learn/blob/main/functional_programming/05_functors.py 35 | ``` 36 | 37 | ### On Our Online Playground 38 | 39 | You can also open notebooks in our online playground by appending `marimo.app/` to a notebook's URL like: 40 | 41 | https://marimo.app/https://github.com/marimo-team/learn/blob/main/functional_programming/05_functors.py 42 | 43 | ### On Our Landing Page 44 | 45 | Open the notebooks in our landing page page [here](https://marimo-team.github.io/learn/functional_programming/05_functors.html) 46 | 47 | ## Collaboration 48 | 49 | If you're interested in collaborating or have questions, please reach out to me 50 | on Discord (@eugene.hs). 51 | 52 | ## Description of notebooks 53 | 54 | Check [here](https://github.com/marimo-team/learn/issues/51) for current series 55 | structure. 56 | 57 | | Notebook | Title | Key Concepts | Prerequisites | 58 | |----------|-------|--------------|---------------| 59 | | [05. Functors](https://github.com/marimo-team/learn/blob/main/functional_programming/05_functors.py) | Category Theory and Functors | Category Theory, Functor, fmap, Bifunctor | Basic Python, Functions | 60 | | [06. Applicatives](https://github.com/marimo-team/learn/blob/main/functional_programming/06_applicatives.py) | Applicative programming with effects | Applicative Functor, pure, apply, Effectful programming, Alternative | Functors | 61 | 62 | **Authors.** 63 | 64 | Thanks to all our notebook authors! 65 | 66 | - [métaboulie](https://github.com/metaboulie) 67 | 68 | **Reviewers.** 69 | 70 | Thanks to all our notebook reviews! 71 | 72 | - [Haleshot](https://github.com/Haleshot) 73 | -------------------------------------------------------------------------------- /optimization/01_least_squares.py: -------------------------------------------------------------------------------- 1 | # /// script 2 | # requires-python = ">=3.11" 3 | # dependencies = [ 4 | # "cvxpy==1.6.0", 5 | # "marimo", 6 | # "numpy==2.2.2", 7 | # ] 8 | # /// 9 | 10 | import marimo 11 | 12 | __generated_with = "0.11.0" 13 | app = marimo.App() 14 | 15 | 16 | @app.cell 17 | def _(): 18 | import marimo as mo 19 | return (mo,) 20 | 21 | 22 | @app.cell(hide_code=True) 23 | def _(mo): 24 | mo.md( 25 | r""" 26 | # Least squares 27 | 28 | In a least-squares problem, we have measurements $A \in \mathcal{R}^{m \times 29 | n}$ (i.e., $m$ rows and $n$ columns) and $b \in \mathcal{R}^m$. We seek a vector 30 | $x \in \mathcal{R}^{n}$ such that $Ax$ is close to $b$. The matrices $A$ and $b$ are problem data or constants, and $x$ is the variable we are solving for. 31 | 32 | Closeness is defined as the sum of the squared differences: 33 | 34 | \[ \sum_{i=1}^m (a_i^Tx - b_i)^2, \] 35 | 36 | also known as the $\ell_2$-norm squared, $\|Ax - b\|_2^2$. 37 | 38 | For example, we might have a dataset of $m$ users, each represented by $n$ features. Each row $a_i^T$ of $A$ is the feature vector for user $i$, while the corresponding entry $b_i$ of $b$ is the measurement we want to predict from $a_i^T$, such as ad spending. The prediction for user $i$ is given by $a_i^Tx$. 39 | 40 | We find the optimal value of $x$ by solving the optimization problem 41 | 42 | \[ 43 | \begin{array}{ll} 44 | \text{minimize} & \|Ax - b\|_2^2. 45 | \end{array} 46 | \] 47 | 48 | Let $x^\star$ denote the optimal $x$. The quantity $r = Ax^\star - b$ is known as the residual. If $\|r\|_2 = 0$, we have a perfect fit. 49 | """ 50 | ) 51 | return 52 | 53 | 54 | @app.cell(hide_code=True) 55 | def _(mo): 56 | mo.md( 57 | r""" 58 | ## Example 59 | 60 | In this example, we use the Python library [CVXPY](https://github.com/cvxpy/cvxpy) to construct and solve a least-squares problems. 61 | """ 62 | ) 63 | return 64 | 65 | 66 | @app.cell 67 | def _(): 68 | import cvxpy as cp 69 | import numpy as np 70 | return cp, np 71 | 72 | 73 | @app.cell 74 | def _(): 75 | m = 20 76 | n = 15 77 | return m, n 78 | 79 | 80 | @app.cell 81 | def _(m, n, np): 82 | np.random.seed(0) 83 | A = np.random.randn(m, n) 84 | b = np.random.randn(m) 85 | return A, b 86 | 87 | 88 | @app.cell 89 | def _(A, b, cp, n): 90 | x = cp.Variable(n) 91 | objective = cp.sum_squares(A @ x - b) 92 | problem = cp.Problem(cp.Minimize(objective)) 93 | optimal_value = problem.solve() 94 | return objective, optimal_value, problem, x 95 | 96 | 97 | @app.cell 98 | def _(A, b, cp, mo, optimal_value, x): 99 | mo.md( 100 | f""" 101 | - The optimal value is **{optimal_value:.04f}**. 102 | - The optimal value of $x$ is {mo.as_html(list(x.value))} 103 | - The norm of the residual is **{cp.norm(A @ x - b, p=2).value:0.4f}** 104 | """ 105 | ) 106 | return 107 | 108 | 109 | @app.cell(hide_code=True) 110 | def _(mo): 111 | mo.md( 112 | r""" 113 | ## Further reading 114 | 115 | For a primer on least squares, with many real-world examples, check out the free book 116 | [Vectors, Matrices, and Least Squares](https://web.stanford.edu/~boyd/vmls/), which is used for undergraduate linear algebra education at Stanford. 117 | """ 118 | ) 119 | return 120 | 121 | 122 | if __name__ == "__main__": 123 | app.run() 124 | -------------------------------------------------------------------------------- /optimization/02_linear_program.py: -------------------------------------------------------------------------------- 1 | # /// script 2 | # requires-python = ">=3.13" 3 | # dependencies = [ 4 | # "cvxpy==1.6.0", 5 | # "marimo", 6 | # "matplotlib==3.10.0", 7 | # "numpy==2.2.2", 8 | # "wigglystuff==0.1.9", 9 | # ] 10 | # /// 11 | 12 | import marimo 13 | 14 | __generated_with = "0.11.0" 15 | app = marimo.App() 16 | 17 | 18 | @app.cell 19 | def _(): 20 | import marimo as mo 21 | return (mo,) 22 | 23 | 24 | @app.cell(hide_code=True) 25 | def _(mo): 26 | mo.md( 27 | r""" 28 | # Linear program 29 | 30 | A linear program is an optimization problem with a linear objective and affine 31 | inequality constraints. A common standard form is the following: 32 | 33 | \[ 34 | \begin{array}{ll} 35 | \text{minimize} & c^Tx \\ 36 | \text{subject to} & Ax \leq b. 37 | \end{array} 38 | \] 39 | 40 | Here $A \in \mathcal{R}^{m \times n}$, $b \in \mathcal{R}^m$, and $c \in \mathcal{R}^n$ are problem data and $x \in \mathcal{R}^{n}$ is the optimization variable. The inequality constraint $Ax \leq b$ is elementwise. 41 | 42 | For example, we might have $n$ different products, each constructed out of $m$ components. Each entry $A_{ij}$ is the amount of component $i$ required to build one unit of product $j$. Each entry $b_i$ is the total amount of component $i$ available. We lose $c_j$ for each unit of product $j$ ($c_j < 0$ indicates profit). Our goal then is to choose how many units of each product $j$ to make, $x_j$, in order to minimize loss without exceeding our budget for any component. 43 | 44 | In addition to a solution $x^\star$, we obtain a dual solution $\lambda^\star$. A positive entry $\lambda^\star_i$ indicates that the constraint $a_i^Tx \leq b_i$ holds with equality for $x^\star$ and suggests that changing $b_i$ would change the optimal value. 45 | 46 | **Why linear programming?** Linear programming is a way to achieve an optimal outcome, such as maximum utility or lowest cost, subject to a linear objective function and affine constraints. Developed in the 20th century, linear programming is widely used today to solve problems in resource allocation, scheduling, transportation, and more. The discovery of polynomial-time algorithms to solve linear programs was of tremendous worldwide importance and entered the public discourse, even making the front page of the New York Times. 47 | 48 | In the late 20th and early 21st century, researchers generalized linear programming to a much wider class of problems called convex optimization problems. Nearly all convex optimization problems can be solved efficiently and reliably, and even more difficult problems are readily solved by a sequence of convex optimization problems. Today, convex optimization is used to fit machine learning models, land rockets in real-time at SpaceX, plan trajectories for self-driving cars at Waymo, execute many billions of dollars of financial trades a day, and much more. 49 | 50 | This marimo learn course uses CVXPY, a modeling language for convex optimization problems developed originally at Stanford, to construct and solve convex programs. 51 | """ 52 | ) 53 | return 54 | 55 | 56 | @app.cell(hide_code=True) 57 | def _(mo): 58 | mo.md( 59 | f""" 60 | {mo.image("https://www.debugmind.com/wp-content/uploads/2020/01/spacex-1.jpg")} 61 | _SpaceX solves convex optimization problems onboard to land its rockets, using CVXGEN, a code generator for quadratic programming developed at Stephen Boyd’s Stanford lab. Photo by SpaceX, licensed CC BY-NC 2.0._ 62 | """ 63 | ) 64 | return 65 | 66 | 67 | @app.cell(hide_code=True) 68 | def _(mo): 69 | mo.md( 70 | r""" 71 | ## Example 72 | 73 | Here we use CVXPY to construct and solve a linear program. 74 | """ 75 | ) 76 | return 77 | 78 | 79 | @app.cell 80 | def _(): 81 | import cvxpy as cp 82 | import numpy as np 83 | return cp, np 84 | 85 | 86 | @app.cell(hide_code=True) 87 | def _(np): 88 | A = np.array( 89 | [ 90 | [0.76103773, 0.12167502], 91 | [0.44386323, 0.33367433], 92 | [1.49407907, -0.20515826], 93 | [0.3130677, -0.85409574], 94 | [-2.55298982, 0.6536186], 95 | [0.8644362, -0.74216502], 96 | [2.26975462, -1.45436567], 97 | [0.04575852, -0.18718385], 98 | [1.53277921, 1.46935877], 99 | [0.15494743, 0.37816252], 100 | ] 101 | ) 102 | 103 | b = np.array( 104 | [ 105 | 2.05062369, 106 | 0.94934659, 107 | 0.89559424, 108 | 1.04389978, 109 | 2.45035643, 110 | -0.95479445, 111 | -0.83801349, 112 | -0.26562529, 113 | 2.35763652, 114 | 0.98286942, 115 | ] 116 | ) 117 | return A, b 118 | 119 | 120 | @app.cell(hide_code=True) 121 | def _(mo): 122 | mo.md(r"""We've randomly generated problem data $A$ and $B$. The vector for $c$ is shown below. Try playing with the value of $c$ by dragging the components, and see how the level curves change in the visualization below.""") 123 | return 124 | 125 | 126 | @app.cell 127 | def _(mo, np): 128 | from wigglystuff import Matrix 129 | 130 | c_widget = mo.ui.anywidget(Matrix(matrix=np.array([[0.1, -0.2]]), step=0.01)) 131 | c_widget 132 | return Matrix, c_widget 133 | 134 | 135 | @app.cell 136 | def _(c_widget, np): 137 | c = np.array(c_widget.value["matrix"][0]) 138 | return (c,) 139 | 140 | 141 | @app.cell 142 | def _(A, b, c, cp): 143 | x = cp.Variable(A.shape[1]) 144 | prob = cp.Problem(cp.Minimize(c.T @ x), [A @ x <= b]) 145 | _ = prob.solve() 146 | x_star = x.value 147 | return prob, x, x_star 148 | 149 | 150 | @app.cell(hide_code=True) 151 | def _(mo): 152 | mo.md(r"""Below, we plot the feasible region of the problem — the intersection of the inequalities — and the level curves of the objective function. The optimal value $x^\star$ is the point farthest in the feasible region in the direction $-c$.""") 153 | return 154 | 155 | 156 | @app.cell(hide_code=True) 157 | def _(A, b, c, make_plot, x_star): 158 | make_plot(A, b, c, x_star) 159 | return 160 | 161 | 162 | @app.cell(hide_code=True) 163 | def _(np): 164 | import matplotlib.pyplot as plt 165 | 166 | 167 | def make_plot(A, b, c, x_star): 168 | # Define a grid over a region that covers the feasible set. 169 | # You might need to adjust these limits. 170 | x_vals = np.linspace(-1, 1, 400) 171 | y_vals = np.linspace(1, 3, 400) 172 | X, Y = np.meshgrid(x_vals, y_vals) 173 | 174 | # Flatten the grid points into an (N,2) array. 175 | points = np.vstack([X.ravel(), Y.ravel()]).T 176 | 177 | # For each point, check if it satisfies all the constraints: A @ x <= b. 178 | # A dot product: shape of A @ x.T will be (m, N). We add a little tolerance. 179 | feasible = np.all(np.dot(A, points.T) <= (b[:, None] + 1e-8), axis=0) 180 | feasible = feasible.reshape(X.shape) 181 | 182 | # Create the figure with a white background. 183 | fig = plt.figure(figsize=(8, 6), facecolor="white") 184 | ax = fig.add_subplot(111) 185 | ax.set_facecolor("white") 186 | 187 | # Plot the feasible region. 188 | # Since "feasible" is a boolean array (False=0, True=1), we set contour levels so that 189 | # the region with value 1 (feasible) gets filled. 190 | ax.contourf( 191 | X, 192 | Y, 193 | feasible.astype(float), 194 | levels=[-0.5, 0.5, 1.5], 195 | colors=["white", "gray"], 196 | alpha=0.5, 197 | ) 198 | 199 | # Plot level curves of the objective function c^T x. 200 | # Compute c^T x over the grid: 201 | Z = c[0] * X + c[1] * Y 202 | # Choose several levels for the iso-cost lines. 203 | levels = np.linspace(np.min(Z), np.max(Z), 20) 204 | contours = ax.contour( 205 | X, Y, Z, levels=levels, colors="gray", linestyles="--", linewidths=1 206 | ) 207 | 208 | # Draw the vector -c as an arrow starting at x_star. 209 | norm_c = np.linalg.norm(c) 210 | if norm_c > 0: 211 | head_width = norm_c * 0.1 212 | head_length = norm_c * 0.1 213 | # The arrow starts at x_star and points in the -c direction. 214 | ax.arrow( 215 | x_star[0], 216 | x_star[1], 217 | -c[0], 218 | -c[1], 219 | head_width=head_width, 220 | head_length=head_length, 221 | fc="black", 222 | ec="black", 223 | length_includes_head=True, 224 | ) 225 | # Label the arrow near its tip. 226 | ax.text( 227 | x_star[0] - c[0] * 1.05, 228 | x_star[1] - c[1] * 1.05, 229 | r"$-c$", 230 | color="black", 231 | fontsize=12, 232 | ) 233 | 234 | # Optionally, mark and label the point x_star. 235 | ax.plot(x_star[0], x_star[1], "ko", markersize=5) 236 | ax.text( 237 | x_star[0], 238 | x_star[1], 239 | r"$\mathbf{x}^\star$", 240 | color="black", 241 | fontsize=12, 242 | verticalalignment="bottom", 243 | horizontalalignment="right", 244 | ) 245 | # Label the axes and set title. 246 | ax.set_xlabel("$x_1$") 247 | ax.set_ylabel("$x_2$") 248 | ax.set_title("Feasible Region and Level Curves of $c^Tx$") 249 | ax.set_xlim(np.min(x_vals), np.max(x_vals)) 250 | ax.set_ylim(np.min(y_vals), np.max(y_vals)) 251 | return ax 252 | return make_plot, plt 253 | 254 | 255 | @app.cell(hide_code=True) 256 | def _(mo, prob, x): 257 | mo.md( 258 | f""" 259 | The optimal value is {prob.value:.04f}. 260 | 261 | A solution $x$ is {mo.as_html(list(x.value))} 262 | A dual solution is is {mo.as_html(list(prob.constraints[0].dual_value))} 263 | """ 264 | ) 265 | return 266 | 267 | 268 | if __name__ == "__main__": 269 | app.run() 270 | -------------------------------------------------------------------------------- /optimization/03_minimum_fuel_optimal_control.py: -------------------------------------------------------------------------------- 1 | import marimo 2 | 3 | __generated_with = "0.11.0" 4 | app = marimo.App() 5 | 6 | 7 | @app.cell 8 | def _(): 9 | import marimo as mo 10 | return (mo,) 11 | 12 | 13 | @app.cell(hide_code=True) 14 | def _(mo): 15 | mo.md( 16 | r""" 17 | # Minimal fuel optimal control 18 | 19 | This notebook includes an application of linear programming to controlling a 20 | physical system, adapted from [Convex 21 | Optimization](https://web.stanford.edu/~boyd/cvxbook/) by Boyd and Vandenberghe. 22 | 23 | We consider a linear dynamical system with state $x(t) \in \mathbf{R}^n$, for $t = 0, \ldots, T$. At each time step $t = 0, \ldots, T - 1$, an actuator or input signal $u(t)$ is applied, affecting the state. The dynamics 24 | of the system is given by the linear recurrence 25 | 26 | \[ 27 | x(t + 1) = Ax(t) + bu(t), \quad t = 0, \ldots, T - 1, 28 | \] 29 | 30 | where $A \in \mathbf{R}^{n \times n}$ and $b \in \mathbf{R}^n$ are given and encode how the system evolves. The initial state $x(0)$ is also given. 31 | 32 | The _minimum fuel optimal control problem_ is to choose the inputs $u(0), \ldots, u(T - 1)$ so as to achieve 33 | a given desired state $x_\text{des} = x(T)$ while minimizing the total fuel consumed 34 | 35 | \[ 36 | F = \sum_{t=0}^{T - 1} f(u(t)). 37 | \] 38 | 39 | The function $f : \mathbf{R} \to \mathbf{R}$ tells us how much fuel is consumed as a function of the input, and is given by 40 | 41 | \[ 42 | f(a) = \begin{cases} 43 | |a| & |a| \leq 1 \\ 44 | 2|a| - 1 & |a| > 1. 45 | \end{cases} 46 | \] 47 | 48 | This means the fuel use is proportional to the magnitude of the signal between $-1$ and $1$, but for larger signals the marginal fuel efficiency is half. 49 | 50 | **This notebook.** In this notebook we use CVXPY to formulate the minimum fuel optimal control problem as a linear program. The notebook lets you play with the initial and target states, letting you see how they affect the planned trajectory of inputs $u$. 51 | 52 | First, we create the **problem data**. 53 | """ 54 | ) 55 | return 56 | 57 | 58 | @app.cell 59 | def _(): 60 | import numpy as np 61 | return (np,) 62 | 63 | 64 | @app.cell 65 | def _(): 66 | n, T = 3, 30 67 | return T, n 68 | 69 | 70 | @app.cell 71 | def _(np): 72 | A = np.array([[-1, 0.4, 0.8], [1, 0, 0], [0, 1, 0]]) 73 | b = np.array([[1, 0, 0.3]]).T 74 | return A, b 75 | 76 | 77 | @app.cell(hide_code=True) 78 | def _(mo, n, np): 79 | import wigglystuff 80 | 81 | x0_widget = mo.ui.anywidget(wigglystuff.Matrix(np.zeros((1, n)))) 82 | xdes_widget = mo.ui.anywidget(wigglystuff.Matrix(np.array([[7, 2, -6]]))) 83 | 84 | _a = mo.md( 85 | rf""" 86 | 87 | Choose a value for $x_0$ ... 88 | 89 | {x0_widget} 90 | """ 91 | ) 92 | 93 | _b = mo.md( 94 | rf""" 95 | ... and for $x_\text{{des}}$ 96 | 97 | {xdes_widget} 98 | """ 99 | ) 100 | 101 | mo.hstack([_a, _b], justify="space-around") 102 | return wigglystuff, x0_widget, xdes_widget 103 | 104 | 105 | @app.cell 106 | def _(x0_widget, xdes_widget): 107 | x0 = x0_widget.matrix 108 | xdes = xdes_widget.matrix 109 | return x0, xdes 110 | 111 | 112 | @app.cell(hide_code=True) 113 | def _(mo): 114 | mo.md(r"""**Next, we specify the problem as a linear program using CVXPY.** This problem is linear because the objective and constraints are affine. (In fact, the objective is piecewise affine, but CVXPY rewrites it to be affine for you.)""") 115 | return 116 | 117 | 118 | @app.cell 119 | def _(): 120 | import cvxpy as cp 121 | return (cp,) 122 | 123 | 124 | @app.cell 125 | def _(A, T, b, cp, mo, n, x0, xdes): 126 | X, u = cp.Variable(shape=(n, T + 1)), cp.Variable(shape=(1, T)) 127 | 128 | objective = cp.sum(cp.maximum(cp.abs(u), 2 * cp.abs(u) - 1)) 129 | constraints = [ 130 | X[:, 1:] == A @ X[:, :-1] + b @ u, 131 | X[:, 0] == x0, 132 | X[:, -1] == xdes, 133 | ] 134 | 135 | fuel_used = cp.Problem(cp.Minimize(objective), constraints).solve() 136 | mo.md(f"Achieved a fuel usage of {fuel_used:.02f}. 🚀") 137 | return X, constraints, fuel_used, objective, u 138 | 139 | 140 | @app.cell(hide_code=True) 141 | def _(mo): 142 | mo.md( 143 | """ 144 | Finally, we plot the chosen inputs over time. 145 | 146 | **🌊 Try it!** Change the initial and desired states; how do fuel usage and controls change? Can you explain what you see? You can also try experimenting with the value of $T$. 147 | """ 148 | ) 149 | return 150 | 151 | 152 | @app.cell 153 | def _(plot_solution, u): 154 | plot_solution(u) 155 | return 156 | 157 | 158 | @app.cell 159 | def _(T, cp, np): 160 | def plot_solution(u: cp.Variable): 161 | import matplotlib.pyplot as plt 162 | 163 | plt.step(np.arange(T), u.T.value) 164 | plt.axis("tight") 165 | plt.xlabel("$t$") 166 | plt.ylabel("$u$") 167 | return plt.gca() 168 | return (plot_solution,) 169 | 170 | 171 | if __name__ == "__main__": 172 | app.run() 173 | -------------------------------------------------------------------------------- /optimization/04_quadratic_program.py: -------------------------------------------------------------------------------- 1 | # /// script 2 | # requires-python = ">=3.13" 3 | # dependencies = [ 4 | # "cvxpy==1.6.0", 5 | # "marimo", 6 | # "matplotlib==3.10.0", 7 | # "numpy==2.2.2", 8 | # "wigglystuff==0.1.9", 9 | # ] 10 | # /// 11 | 12 | import marimo 13 | 14 | __generated_with = "0.11.0" 15 | app = marimo.App() 16 | 17 | 18 | @app.cell 19 | def _(): 20 | import marimo as mo 21 | return (mo,) 22 | 23 | 24 | @app.cell(hide_code=True) 25 | def _(mo): 26 | mo.md( 27 | r""" 28 | # Quadratic program 29 | 30 | A quadratic program is an optimization problem with a quadratic objective and 31 | affine equality and inequality constraints. A common standard form is the 32 | following: 33 | 34 | \[ 35 | \begin{array}{ll} 36 | \text{minimize} & (1/2)x^TPx + q^Tx\\ 37 | \text{subject to} & Gx \leq h \\ 38 | & Ax = b. 39 | \end{array} 40 | \] 41 | 42 | Here $P \in \mathcal{S}^{n}_+$, $q \in \mathcal{R}^n$, $G \in \mathcal{R}^{m \times n}$, $h \in \mathcal{R}^m$, $A \in \mathcal{R}^{p \times n}$, and $b \in \mathcal{R}^p$ are problem data and $x \in \mathcal{R}^{n}$ is the optimization variable. The inequality constraint $Gx \leq h$ is elementwise. 43 | 44 | **Why quadratic programming?** Quadratic programs are convex optimization problems that generalize both least-squares and linear programming.They can be solved efficiently and reliably, even in real-time. 45 | 46 | **An example from finance.** A simple example of a quadratic program arises in finance. Suppose we have $n$ different stocks, an estimate $r \in \mathcal{R}^n$ of the expected return on each stock, and an estimate $\Sigma \in \mathcal{S}^{n}_+$ of the covariance of the returns. Then we solve the optimization problem 47 | 48 | \[ 49 | \begin{array}{ll} 50 | \text{minimize} & (1/2)x^T\Sigma x - r^Tx\\ 51 | \text{subject to} & x \geq 0 \\ 52 | & \mathbf{1}^Tx = 1, 53 | \end{array} 54 | \] 55 | 56 | to find a nonnegative portfolio allocation $x \in \mathcal{R}^n_+$ that optimally balances expected return and variance of return. 57 | 58 | When we solve a quadratic program, in addition to a solution $x^\star$, we obtain a dual solution $\lambda^\star$ corresponding to the inequality constraints. A positive entry $\lambda^\star_i$ indicates that the constraint $g_i^Tx \leq h_i$ holds with equality for $x^\star$ and suggests that changing $h_i$ would change the optimal value. 59 | """ 60 | ) 61 | return 62 | 63 | 64 | @app.cell(hide_code=True) 65 | def _(mo): 66 | mo.md( 67 | r""" 68 | ## Example 69 | 70 | In this example, we use CVXPY to construct and solve a quadratic program. 71 | """ 72 | ) 73 | return 74 | 75 | 76 | @app.cell 77 | def _(): 78 | import cvxpy as cp 79 | import numpy as np 80 | return cp, np 81 | 82 | 83 | @app.cell(hide_code=True) 84 | def _(mo): 85 | mo.md("""First we generate synthetic data. In this problem, we don't include equality constraints, only inequality.""") 86 | return 87 | 88 | 89 | @app.cell 90 | def _(np): 91 | m = 4 92 | n = 2 93 | 94 | np.random.seed(1) 95 | q = np.random.randn(n) 96 | G = np.random.randn(m, n) 97 | h = G @ np.random.randn(n) 98 | return G, h, m, n, q 99 | 100 | 101 | @app.cell(hide_code=True) 102 | def _(mo, np): 103 | import wigglystuff 104 | 105 | P_widget = mo.ui.anywidget( 106 | wigglystuff.Matrix(np.array([[4.0, -1.4], [-1.4, 4]]), step=0.1) 107 | ) 108 | 109 | mo.md( 110 | f""" 111 | The quadratic form $P$ is equal to the symmetrized version of this 112 | matrix: 113 | 114 | {P_widget.center()} 115 | """ 116 | ) 117 | return P_widget, wigglystuff 118 | 119 | 120 | @app.cell 121 | def _(P_widget, np): 122 | P = 0.5 * (np.array(P_widget.matrix) + np.array(P_widget.matrix).T) 123 | return (P,) 124 | 125 | 126 | @app.cell(hide_code=True) 127 | def _(mo): 128 | mo.md(r"""Next, we specify the problem. Notice that we use the `quad_form` function from CVXPY to create the quadratic form $x^TPx$.""") 129 | return 130 | 131 | 132 | @app.cell 133 | def _(G, P, cp, h, n, q): 134 | x = cp.Variable(n) 135 | 136 | problem = cp.Problem( 137 | cp.Minimize((1 / 2) * cp.quad_form(x, P) + q.T @ x), 138 | [G @ x <= h], 139 | ) 140 | _ = problem.solve() 141 | return problem, x 142 | 143 | 144 | @app.cell(hide_code=True) 145 | def _(mo, problem, x): 146 | mo.md( 147 | f""" 148 | The optimal value is {problem.value:.04f}. 149 | 150 | A solution $x$ is {mo.as_html(list(x.value))} 151 | A dual solution is is {mo.as_html(list(problem.constraints[0].dual_value))} 152 | """ 153 | ) 154 | return 155 | 156 | 157 | @app.cell 158 | def _(G, P, h, plot_contours, q, x): 159 | plot_contours(P, G, h, q, x.value) 160 | return 161 | 162 | 163 | @app.cell(hide_code=True) 164 | def _(mo): 165 | mo.md( 166 | r""" 167 | In this plot, the gray shaded region is the feasible region (points satisfying the inequality), and the ellipses are level curves of the quadratic form. 168 | 169 | **🌊 Try it!** Try changing the entries of $P$ above with your mouse. How do the 170 | level curves and the optimal value of $x$ change? Can you explain what you see? 171 | """ 172 | ) 173 | return 174 | 175 | 176 | @app.cell(hide_code=True) 177 | def _(P, mo): 178 | mo.md( 179 | rf""" 180 | The above contour lines were generated with 181 | 182 | \[ 183 | P= \begin{{bmatrix}} 184 | {P[0, 0]:.01f} & {P[0, 1]:.01f} \\ 185 | {P[1, 0]:.01f} & {P[1, 1]:.01f} \\ 186 | \end{{bmatrix}} 187 | \] 188 | """ 189 | ) 190 | return 191 | 192 | 193 | @app.cell(hide_code=True) 194 | def _(np): 195 | def plot_contours(P, G, h, q, x_star): 196 | import matplotlib.pyplot as plt 197 | 198 | # Create a grid of x and y values. 199 | x = np.linspace(-5, 5, 400) 200 | y = np.linspace(-5, 5, 400) 201 | X, Y = np.meshgrid(x, y) 202 | 203 | # Compute the quadratic form Q(x, y) = a*x^2 + 2*b*x*y + c*y^2. 204 | # Here, a = P[0,0], b = P[0,1] (and P[1,0]), c = P[1,1] 205 | Z = ( 206 | 0.5 * (P[0, 0] * X**2 + 2 * P[0, 1] * X * Y + P[1, 1] * Y**2) 207 | + q[0] * X 208 | + q[1] * Y 209 | ) 210 | 211 | # --- Evaluate the constraints on the grid --- 212 | # We stack X and Y to get a list of (x,y) points. 213 | points = np.vstack([X.ravel(), Y.ravel()]).T 214 | 215 | # Start with all points feasible 216 | feasible = np.ones(points.shape[0], dtype=bool) 217 | 218 | # Apply the inequality constraints Gx <= h. 219 | # Each row of G and corresponding h defines a condition. 220 | for i in range(G.shape[0]): 221 | # For a given point x, the condition is: G[i,0]*x + G[i,1]*y <= h[i] 222 | feasible &= points.dot(G[i]) <= h[i] + 1e-8 # small tolerance 223 | # Reshape the boolean mask back to grid shape. 224 | feasible_grid = feasible.reshape(X.shape) 225 | 226 | # --- Plot the feasible region and contour lines--- 227 | plt.figure(figsize=(8, 6)) 228 | 229 | # Use contourf to fill the region where feasible_grid is True. 230 | # We define two levels, so that points that are True (feasible) get one 231 | # color. 232 | plt.contourf( 233 | X, 234 | Y, 235 | feasible_grid, 236 | levels=[-0.5, 0.5, 1.5], 237 | colors=["white", "gray"], 238 | alpha=0.5, 239 | ) 240 | 241 | contours = plt.contour(X, Y, Z, levels=10, cmap="viridis") 242 | plt.clabel(contours, inline=True, fontsize=8) 243 | plt.title("Feasible region and level curves") 244 | plt.xlabel("$x_1$") 245 | plt.ylabel("$y_2$") 246 | # plt.colorbar(contours, label='Q(x, y)') 247 | 248 | ax = plt.gca() 249 | # Optionally, mark and label the point x_star. 250 | ax.plot(x_star[0], x_star[1], "ko", markersize=5) 251 | ax.text( 252 | x_star[0], 253 | x_star[1], 254 | r"$\mathbf{x}^\star$", 255 | color="black", 256 | fontsize=12, 257 | verticalalignment="bottom", 258 | horizontalalignment="right", 259 | ) 260 | return plt.gca() 261 | return (plot_contours,) 262 | 263 | 264 | if __name__ == "__main__": 265 | app.run() 266 | -------------------------------------------------------------------------------- /optimization/05_portfolio_optimization.py: -------------------------------------------------------------------------------- 1 | # /// script 2 | # requires-python = ">=3.13" 3 | # dependencies = [ 4 | # "cvxpy==1.6.0", 5 | # "marimo", 6 | # "matplotlib==3.10.0", 7 | # "numpy==2.2.2", 8 | # "scipy==1.15.1", 9 | # "wigglystuff==0.1.9", 10 | # ] 11 | # /// 12 | 13 | import marimo 14 | 15 | __generated_with = "0.11.2" 16 | app = marimo.App() 17 | 18 | 19 | @app.cell 20 | def _(): 21 | import marimo as mo 22 | return (mo,) 23 | 24 | 25 | @app.cell(hide_code=True) 26 | def _(mo): 27 | mo.md(r"""# Portfolio optimization""") 28 | return 29 | 30 | 31 | @app.cell(hide_code=True) 32 | def _(mo): 33 | mo.md( 34 | r""" 35 | In this example we show how to use CVXPY to design a financial portfolio; this is called _portfolio optimization_. 36 | 37 | In portfolio optimization we have some amount of money to invest in any of $n$ different assets. 38 | We choose what fraction $w_i$ of our money to invest in each asset $i$, $i=1, \ldots, n$. The goal is to maximize return of the portfolio while minimizing risk. 39 | """ 40 | ) 41 | return 42 | 43 | 44 | @app.cell(hide_code=True) 45 | def _(mo): 46 | mo.md( 47 | r""" 48 | ## Asset returns and risk 49 | 50 | We will only model investments held for one period. The initial prices are $p_i > 0$. The end of period prices are $p_i^+ >0$. The asset (fractional) returns are $r_i = (p_i^+-p_i)/p_i$. The portfolio (fractional) return is $R = r^Tw$. 51 | 52 | A common model is that $r$ is a random variable with mean ${\bf E}r = \mu$ and covariance ${\bf E{(r-\mu)(r-\mu)^T}} = \Sigma$. 53 | It follows that $R$ is a random variable with ${\bf E}R = \mu^T w$ and ${\bf var}(R) = w^T\Sigma w$. In real-world applications, $\mu$ and $\Sigma$ are estimated from data and models, and $w$ is chosen using a library like CVXPY. 54 | 55 | ${\bf E}R$ is the (mean) *return* of the portfolio. ${\bf var}(R)$ is the *risk* of the portfolio. Portfolio optimization has two competing objectives: high return and low risk. 56 | """ 57 | ) 58 | return 59 | 60 | 61 | @app.cell(hide_code=True) 62 | def _(mo): 63 | mo.md( 64 | r""" 65 | ## Classical (Markowitz) portfolio optimization 66 | 67 | Classical (Markowitz) portfolio optimization solves the optimization problem 68 | """ 69 | ) 70 | return 71 | 72 | 73 | @app.cell(hide_code=True) 74 | def _(mo): 75 | mo.md( 76 | r""" 77 | $$ 78 | \begin{array}{ll} \text{maximize} & \mu^T w - \gamma w^T\Sigma w\\ 79 | \text{subject to} & {\bf 1}^T w = 1, w \geq 0, 80 | \end{array} 81 | $$ 82 | """ 83 | ) 84 | return 85 | 86 | 87 | @app.cell(hide_code=True) 88 | def _(mo): 89 | mo.md( 90 | r""" 91 | where $w \in {\bf R}^n$ is the optimization variable and $\gamma >0$ is a constant called the *risk aversion parameter*. The constraint $\mathbf{1}^Tw = 1$ says the portfolio weight vector must sum to 1, and $w \geq 0$ says that we can't invest a negative amount into any asset. 92 | 93 | The objective $\mu^Tw - \gamma w^T\Sigma w$ is the *risk-adjusted return*. Varying $\gamma$ gives the optimal *risk-return trade-off*. 94 | We can get the same risk-return trade-off by fixing return and minimizing risk. 95 | """ 96 | ) 97 | return 98 | 99 | 100 | @app.cell(hide_code=True) 101 | def _(mo): 102 | mo.md( 103 | r""" 104 | ## Example 105 | 106 | In the following code we compute and plot the optimal risk-return trade-off for $10$ assets. First we generate random problem data $\mu$ and $\Sigma$. 107 | """ 108 | ) 109 | return 110 | 111 | 112 | @app.cell 113 | def _(): 114 | import numpy as np 115 | return (np,) 116 | 117 | 118 | @app.cell(hide_code=True) 119 | def _(mo, np): 120 | import wigglystuff 121 | 122 | mu_widget = mo.ui.anywidget( 123 | wigglystuff.Matrix( 124 | np.array( 125 | [ 126 | [1.6], 127 | [0.6], 128 | [0.5], 129 | [1.1], 130 | [0.9], 131 | [2.3], 132 | [1.7], 133 | [0.7], 134 | [0.9], 135 | [0.3], 136 | ] 137 | ) 138 | ) 139 | ) 140 | 141 | 142 | mo.md( 143 | rf""" 144 | The value of $\mu$ is 145 | 146 | {mu_widget.center()} 147 | 148 | _Try changing the entries of $\mu$ and see how the plots below change._ 149 | """ 150 | ) 151 | return mu_widget, wigglystuff 152 | 153 | 154 | @app.cell 155 | def _(mu_widget, np): 156 | np.random.seed(1) 157 | n = 10 158 | mu = np.array(mu_widget.matrix) 159 | Sigma = np.random.randn(n, n) 160 | Sigma = Sigma.T.dot(Sigma) 161 | return Sigma, mu, n 162 | 163 | 164 | @app.cell(hide_code=True) 165 | def _(mo): 166 | mo.md("""Next, we solve the problem for 100 different values of $\gamma$""") 167 | return 168 | 169 | 170 | @app.cell 171 | def _(Sigma, mu, n): 172 | import cvxpy as cp 173 | 174 | w = cp.Variable(n) 175 | gamma = cp.Parameter(nonneg=True) 176 | ret = mu.T @ w 177 | risk = cp.quad_form(w, Sigma) 178 | prob = cp.Problem(cp.Maximize(ret - gamma * risk), [cp.sum(w) == 1, w >= 0]) 179 | return cp, gamma, prob, ret, risk, w 180 | 181 | 182 | @app.cell 183 | def _(cp, gamma, np, prob, ret, risk): 184 | _SAMPLES = 100 185 | risk_data = np.zeros(_SAMPLES) 186 | ret_data = np.zeros(_SAMPLES) 187 | gamma_vals = np.logspace(-2, 3, num=_SAMPLES) 188 | for _i in range(_SAMPLES): 189 | gamma.value = gamma_vals[_i] 190 | prob.solve() 191 | risk_data[_i] = cp.sqrt(risk).value 192 | ret_data[_i] = ret.value 193 | return gamma_vals, ret_data, risk_data 194 | 195 | 196 | @app.cell(hide_code=True) 197 | def _(mo): 198 | mo.md("""Plotted below are the risk return tradeoffs for two values of $\gamma$ (blue squares), and the risk return tradeoffs for investing fully in each asset (red circles)""") 199 | return 200 | 201 | 202 | @app.cell(hide_code=True) 203 | def _(Sigma, cp, gamma_vals, mu, n, ret_data, risk_data): 204 | import matplotlib.pyplot as plt 205 | 206 | markers_on = [29, 40] 207 | fig = plt.figure() 208 | ax = fig.add_subplot(111) 209 | plt.plot(risk_data, ret_data, "g-") 210 | for marker in markers_on: 211 | plt.plot(risk_data[marker], ret_data[marker], "bs") 212 | ax.annotate( 213 | "$\\gamma = %.2f$" % gamma_vals[marker], 214 | xy=(risk_data[marker] + 0.08, ret_data[marker] - 0.03), 215 | ) 216 | for _i in range(n): 217 | plt.plot(cp.sqrt(Sigma[_i, _i]).value, mu[_i], "ro") 218 | plt.xlabel("Standard deviation") 219 | plt.ylabel("Return") 220 | plt.show() 221 | return ax, fig, marker, markers_on, plt 222 | 223 | 224 | @app.cell(hide_code=True) 225 | def _(mo): 226 | mo.md( 227 | r""" 228 | We plot below the return distributions for the two risk aversion values marked on the trade-off curve. 229 | Notice that the probability of a loss is near 0 for the low risk value and far above 0 for the high risk value. 230 | """ 231 | ) 232 | return 233 | 234 | 235 | @app.cell(hide_code=True) 236 | def _(gamma, gamma_vals, markers_on, np, plt, prob, ret, risk): 237 | import scipy.stats as spstats 238 | 239 | plt.figure() 240 | for midx, _idx in enumerate(markers_on): 241 | gamma.value = gamma_vals[_idx] 242 | prob.solve() 243 | x = np.linspace(-2, 5, 1000) 244 | plt.plot( 245 | x, 246 | spstats.norm.pdf(x, ret.value, risk.value), 247 | label="$\\gamma = %.2f$" % gamma.value, 248 | ) 249 | plt.xlabel("Return") 250 | plt.ylabel("Density") 251 | plt.legend(loc="upper right") 252 | plt.show() 253 | return midx, spstats, x 254 | 255 | 256 | if __name__ == "__main__": 257 | app.run() 258 | -------------------------------------------------------------------------------- /optimization/06_convex_optimization.py: -------------------------------------------------------------------------------- 1 | # /// script 2 | # requires-python = ">=3.13" 3 | # dependencies = [ 4 | # "cvxpy==1.6.0", 5 | # "marimo", 6 | # "numpy==2.2.2", 7 | # ] 8 | # /// 9 | 10 | import marimo 11 | 12 | __generated_with = "0.11.2" 13 | app = marimo.App() 14 | 15 | 16 | @app.cell 17 | def _(): 18 | import marimo as mo 19 | return (mo,) 20 | 21 | 22 | @app.cell(hide_code=True) 23 | def _(mo): 24 | mo.md( 25 | r""" 26 | # Convex optimization 27 | 28 | In the previous tutorials, we learned about least squares, linear programming, 29 | and quadratic programming, and saw applications of each. We also learned that these problem 30 | classes can be solved efficiently and reliably using CVXPY. That's because these problem classes are a special 31 | case of a more general class of tractable problems, called **convex optimization problems.** 32 | 33 | A convex optimization problem is an optimization problem that minimizes a convex 34 | function, subject to affine equality constraints and convex inequality 35 | constraints ($f_i(x)\leq 0$, where $f_i$ is a convex function). 36 | 37 | **CVXPY.** CVXPY lets you specify and solve any convex optimization problem, 38 | abstracting away the more specific problem classes. You start with CVXPY's **atomic functions**, like `cp.exp`, `cp.log`, and `cp.square`, and compose them to build more complex convex functions. As long as the functions are composed in the right way — as long as they are "DCP-compliant" — your resulting problem will be convex and solvable by CVXPY. 39 | """ 40 | ) 41 | return 42 | 43 | 44 | @app.cell(hide_code=True) 45 | def _(mo): 46 | mo.md( 47 | r""" 48 | **🛑 Stop!** Before proceeding, read the CVXPY docs to learn about atomic functions and the DCP ruleset: 49 | 50 | https://www.cvxpy.org/tutorial/index.html 51 | """ 52 | ) 53 | return 54 | 55 | 56 | @app.cell(hide_code=True) 57 | def _(mo): 58 | mo.md(r"""**Is my problem DCP-compliant?** Below is a sample CVXPY problem. It is DCP-compliant. Try typing in other problems and seeing if they are DCP-compliant. If you know your problem is convex, there exists a way to express it in a DCP-compliant way.""") 59 | return 60 | 61 | 62 | @app.cell 63 | def _(mo): 64 | import cvxpy as cp 65 | import numpy as np 66 | 67 | x = cp.Variable(3) 68 | P_sqrt = np.random.randn(3, 3) 69 | 70 | objective = cp.log(np.random.randn(3) @ x) - cp.sum_squares(P_sqrt @ x) 71 | constraints = [x >= 0, cp.sum(x) == 1] 72 | problem = cp.Problem(cp.Maximize(objective), constraints) 73 | mo.md(f"Is my problem DCP? `{problem.is_dcp()}`") 74 | return P_sqrt, constraints, cp, np, objective, problem, x 75 | 76 | 77 | @app.cell 78 | def _(problem): 79 | problem.solve() 80 | return 81 | 82 | 83 | @app.cell 84 | def _(x): 85 | x.value 86 | return 87 | 88 | 89 | if __name__ == "__main__": 90 | app.run() 91 | -------------------------------------------------------------------------------- /optimization/07_sdp.py: -------------------------------------------------------------------------------- 1 | # /// script 2 | # requires-python = ">=3.13" 3 | # dependencies = [ 4 | # "cvxpy==1.6.0", 5 | # "marimo", 6 | # "numpy==2.2.2", 7 | # "wigglystuff==0.1.9", 8 | # ] 9 | # /// 10 | 11 | import marimo 12 | 13 | __generated_with = "0.11.2" 14 | app = marimo.App() 15 | 16 | 17 | @app.cell 18 | def _(): 19 | import marimo as mo 20 | return (mo,) 21 | 22 | 23 | @app.cell(hide_code=True) 24 | def _(mo): 25 | mo.md(r"""# Semidefinite program""") 26 | return 27 | 28 | 29 | @app.cell(hide_code=True) 30 | def _(mo): 31 | mo.md( 32 | r""" 33 | _This notebook introduces an advanced topic._ A semidefinite program (SDP) is an optimization problem of the form 34 | 35 | \[ 36 | \begin{array}{ll} 37 | \text{minimize} & \mathbf{tr}(CX) \\ 38 | \text{subject to} & \mathbf{tr}(A_iX) = b_i, \quad i=1,\ldots,p \\ 39 | & X \succeq 0, 40 | \end{array} 41 | \] 42 | 43 | where $\mathbf{tr}$ is the trace function, $X \in \mathcal{S}^{n}$ is the optimization variable and $C, A_1, \ldots, A_p \in \mathcal{S}^{n}$, and $b_1, \ldots, b_p \in \mathcal{R}$ are problem data, and $X \succeq 0$ is a matrix inequality. Here $\mathcal{S}^{n}$ denotes the set of $n$-by-$n$ symmetric matrices. 44 | 45 | **Example.** An example of an SDP is to complete a covariance matrix $\tilde \Sigma \in \mathcal{S}^{n}_+$ with missing entries $M \subset \{1,\ldots,n\} \times \{1,\ldots,n\}$: 46 | 47 | \[ 48 | \begin{array}{ll} 49 | \text{minimize} & 0 \\ 50 | \text{subject to} & \Sigma_{ij} = \tilde \Sigma_{ij}, \quad (i,j) \notin M \\ 51 | & \Sigma \succeq 0, 52 | \end{array} 53 | \] 54 | """ 55 | ) 56 | return 57 | 58 | 59 | @app.cell(hide_code=True) 60 | def _(mo): 61 | mo.md( 62 | r""" 63 | ## Example 64 | 65 | In the following code, we show how to specify and solve an SDP with CVXPY. 66 | """ 67 | ) 68 | return 69 | 70 | 71 | @app.cell 72 | def _(): 73 | import cvxpy as cp 74 | import numpy as np 75 | return cp, np 76 | 77 | 78 | @app.cell 79 | def _(np): 80 | # Generate a random SDP. 81 | n = 3 82 | p = 3 83 | np.random.seed(1) 84 | C = np.random.randn(n, n) 85 | A = [] 86 | b = [] 87 | for i in range(p): 88 | A.append(np.random.randn(n, n)) 89 | b.append(np.random.randn()) 90 | return A, C, b, i, n, p 91 | 92 | 93 | @app.cell 94 | def _(A, C, b, cp, n, p): 95 | # Create a symmetric matrix variable. 96 | X = cp.Variable((n, n), symmetric=True) 97 | 98 | # The operator >> denotes matrix inequality, with X >> 0 constraining X 99 | # to be positive semidefinite 100 | constraints = [X >> 0] 101 | constraints += [cp.trace(A[i] @ X) == b[i] for i in range(p)] 102 | prob = cp.Problem(cp.Minimize(cp.trace(C @ X)), constraints) 103 | _ = prob.solve() 104 | return X, constraints, prob 105 | 106 | 107 | @app.cell 108 | def _(X, mo, prob, wigglystuff): 109 | mo.md( 110 | f""" 111 | The optimal value is {prob.value:0.4f}. 112 | 113 | A solution for $X$ is (rounded to the nearest decimal) is: 114 | 115 | {mo.ui.anywidget(wigglystuff.Matrix(X.value)).center()} 116 | """ 117 | ) 118 | return 119 | 120 | 121 | @app.cell 122 | def _(): 123 | import wigglystuff 124 | return (wigglystuff,) 125 | 126 | 127 | if __name__ == "__main__": 128 | app.run() 129 | -------------------------------------------------------------------------------- /optimization/README.md: -------------------------------------------------------------------------------- 1 | # Learn optimization 2 | 3 | This collection of marimo notebooks teaches you the basics of convex 4 | optimization. 5 | 6 | After working through these notebooks, you'll understand how to create 7 | and solve optimization problems using the Python library 8 | [CVXPY](https://github.com/cvxpy/cvxpy), as well as how to apply what you've 9 | learned to real-world problems such as portfolio allocation in finance, 10 | control of vehicles, and more. 11 | 12 | ![SpaceX](https://www.debugmind.com/wp-content/uploads/2020/01/spacex-1.jpg) 13 | 14 | _SpaceX solves convex optimization problems onboard to land its rockets, using CVXGEN, a code generator for quadratic programming developed at Stephen Boyd’s Stanford lab. Photo by SpaceX, licensed CC BY-NC 2.0._ 15 | 16 | **Running notebooks.** To run a notebook locally, use 17 | 18 | ```bash 19 | uvx marimo edit 20 | ``` 21 | 22 | For example, run the least-squares tutorial with 23 | 24 | ```bash 25 | uvx marimo edit https://github.com/marimo-team/learn/blob/main/optimization/01_least_squares.py 26 | ``` 27 | 28 | You can also open notebooks in our online playground by appending `marimo.app/` 29 | to a notebook's URL: [marimo.app/github.com/marimo-team/learn/blob/main/optimization/01_least_squares.py](https://marimo.app/https://github.com/marimo-team/learn/blob/main/optimization/01_least_squares.py). 30 | 31 | **Thanks to all our notebook authors!** 32 | 33 | * [Akshay Agrawal](https://github.com/akshayka) 34 | -------------------------------------------------------------------------------- /polars/09_data_types.py: -------------------------------------------------------------------------------- 1 | # /// script 2 | # requires-python = ">=3.11" 3 | # dependencies = [ 4 | # "polars==1.18.0", 5 | # "marimo", 6 | # ] 7 | # /// 8 | 9 | import marimo 10 | 11 | __generated_with = "0.12.0" 12 | app = marimo.App(width="medium") 13 | 14 | 15 | @app.cell(hide_code=True) 16 | def _(mo): 17 | mo.md( 18 | r""" 19 | # Data Types 20 | 21 | Author: [Deb Debnath](https://github.com/debajyotid2) 22 | 23 | **Note**: The following tutorial has been adapted from the Polars [documentation](https://docs.pola.rs/user-guide/concepts/data-types-and-structures/). 24 | """ 25 | ) 26 | return 27 | 28 | 29 | @app.cell(hide_code=True) 30 | def _(mo): 31 | mo.md( 32 | r""" 33 | Polars supports a variety of data types that fall broadly under the following categories: 34 | 35 | - Numeric data types: integers and floating point numbers. 36 | - Nested data types: lists, structs, and arrays. 37 | - Temporal: dates, datetimes, times, and time deltas. 38 | - Miscellaneous: strings, binary data, Booleans, categoricals, enums, and objects. 39 | 40 | All types support missing values represented by `null` which is different from `NaN` used in floating point data types. The numeric datatypes in Polars loosely follow the type system of the Rust language, since its core functionalities are built in Rust. 41 | 42 | [Here](https://docs.pola.rs/api/python/stable/reference/datatypes.html) is a full list of all data types Polars supports. 43 | """ 44 | ) 45 | return 46 | 47 | 48 | @app.cell(hide_code=True) 49 | def _(mo): 50 | mo.md( 51 | r""" 52 | ## Series 53 | 54 | A series is a 1-dimensional data structure that can hold only one data type. 55 | """ 56 | ) 57 | return 58 | 59 | 60 | @app.cell 61 | def _(pl): 62 | s = pl.Series("emojis", ["😀", "🤣", "🥶", "💀", "🤖"]) 63 | s 64 | return (s,) 65 | 66 | 67 | @app.cell(hide_code=True) 68 | def _(mo): 69 | mo.md(r"""Unless specified, Polars infers the datatype from the supplied values.""") 70 | return 71 | 72 | 73 | @app.cell 74 | def _(pl): 75 | s1 = pl.Series("friends", ["Евгений", "अभिषेक", "秀良", "Federico", "Bob"]) 76 | s2 = pl.Series("uints", [0x00, 0x01, 0x10, 0x11], dtype=pl.UInt8) 77 | s1.dtype, s2.dtype 78 | return s1, s2 79 | 80 | 81 | @app.cell(hide_code=True) 82 | def _(mo): 83 | mo.md( 84 | r""" 85 | ## Dataframe 86 | 87 | A dataframe is a 2-dimensional data structure that contains uniquely named series and can hold multiple data types. Dataframes are more commonly used for data manipulation using the functionality of Polars. 88 | 89 | The snippet below shows how to create a dataframe from a dictionary of lists: 90 | """ 91 | ) 92 | return 93 | 94 | 95 | @app.cell 96 | def _(pl): 97 | data = pl.DataFrame( 98 | { 99 | "Product ID": ["L51172", "M22586", "L51639", "L50250", "M20109"], 100 | "Type": ["L", "M", "L", "L", "M"], 101 | "Air temperature": [302.3, 300.8, 302.6, 300, 303.4], # (K) 102 | "Machine Failure": [False, True, False, False, True] 103 | } 104 | ) 105 | data 106 | return (data,) 107 | 108 | 109 | @app.cell(hide_code=True) 110 | def _(mo): 111 | mo.md( 112 | r""" 113 | ### Inspecting a dataframe 114 | 115 | Polars has various functions to explore the data in a dataframe. We will use the dataframe `data` defined above in our examples. Alongside we can also see a view of the dataframe rendered by `marimo` as the cells are executed. 116 | 117 | ///note 118 | We can also use `marimo`'s built in data-inspection elements/features such as [`mo.ui.dataframe`](https://docs.marimo.io/api/inputs/dataframe/#marimo.ui.dataframe) & [`mo.ui.data_explorer`](https://docs.marimo.io/api/inputs/data_explorer/). For more check out our Polars tutorials at [`marimo learn`](https://marimo-team.github.io/learn/)! 119 | """ 120 | ) 121 | return 122 | 123 | 124 | @app.cell(hide_code=True) 125 | def _(mo): 126 | mo.md( 127 | """ 128 | #### Head 129 | 130 | The function `head` shows the first rows of a dataframe. Unless specified, it shows the first 5 rows. 131 | """ 132 | ) 133 | return 134 | 135 | 136 | @app.cell 137 | def _(data): 138 | data.head(3) 139 | return 140 | 141 | 142 | @app.cell(hide_code=True) 143 | def _(mo): 144 | mo.md( 145 | r""" 146 | #### Glimpse 147 | 148 | The function `glimpse` is an alternative to `head` to view the first few columns, but displays each line of the output corresponding to a single column. That way, it makes inspecting wider dataframes easier. 149 | """ 150 | ) 151 | return 152 | 153 | 154 | @app.cell 155 | def _(data): 156 | print(data.glimpse(return_as_string=True)) 157 | return 158 | 159 | 160 | @app.cell(hide_code=True) 161 | def _(mo): 162 | mo.md( 163 | r""" 164 | #### Tail 165 | 166 | The `tail` function, just like its name suggests, shows the last rows of a dataframe. Unless the number of rows is specified, it will show the last 5 rows. 167 | """ 168 | ) 169 | return 170 | 171 | 172 | @app.cell 173 | def _(data): 174 | data.tail(3) 175 | return 176 | 177 | 178 | @app.cell(hide_code=True) 179 | def _(mo): 180 | mo.md( 181 | r""" 182 | #### Sample 183 | 184 | `sample` can be used to show a specified number of randomly selected rows from the dataframe. Unless the number of rows is specified, it will show a single row. `sample` does not preserve order of the rows. 185 | """ 186 | ) 187 | return 188 | 189 | 190 | @app.cell 191 | def _(data): 192 | import random 193 | 194 | random.seed(42) # For reproducibility. 195 | 196 | data.sample(3) 197 | return (random,) 198 | 199 | 200 | @app.cell(hide_code=True) 201 | def _(mo): 202 | mo.md( 203 | r""" 204 | #### Describe 205 | 206 | The function `describe` describes the summary statistics for all columns of a dataframe. 207 | """ 208 | ) 209 | return 210 | 211 | 212 | @app.cell 213 | def _(data): 214 | data.describe() 215 | return 216 | 217 | 218 | @app.cell(hide_code=True) 219 | def _(mo): 220 | mo.md( 221 | r""" 222 | ## Schema 223 | 224 | A schema is a mapping showing the datatype corresponding to every column of a dataframe. The schema of a dataframe can be viewed using the attribute `schema`. 225 | """ 226 | ) 227 | return 228 | 229 | 230 | @app.cell 231 | def _(data): 232 | data.schema 233 | return 234 | 235 | 236 | @app.cell(hide_code=True) 237 | def _(mo): 238 | mo.md(r"""Since a schema is a mapping, it can be specified in the form of a Python dictionary. Then this dictionary can be used to specify the schema of a dataframe on definition. If not specified or the entry is `None`, Polars infers the datatype from the contents of the column. Note that if the schema is not specified, it will be inferred automatically by default.""") 239 | return 240 | 241 | 242 | @app.cell 243 | def _(pl): 244 | pl.DataFrame( 245 | { 246 | "Product ID": ["L51172", "M22586", "L51639", "L50250", "M20109"], 247 | "Type": ["L", "M", "L", "L", "M"], 248 | "Air temperature": [302.3, 300.8, 302.6, 300, 303.4], # (K) 249 | "Machine Failure": [False, True, False, False, True] 250 | }, 251 | schema={"Product ID": pl.String, "Type": pl.String, "Air temperature": None, "Machine Failure": None}, 252 | ) 253 | return 254 | 255 | 256 | @app.cell(hide_code=True) 257 | def _(mo): 258 | mo.md(r"""Sometimes the automatically inferred schema is enough for some columns, but we might wish to override the inference of only some columns. We can specify the schema for those columns using `schema_overrides`.""") 259 | return 260 | 261 | 262 | @app.cell 263 | def _(pl): 264 | pl.DataFrame( 265 | { 266 | "Product ID": ["L51172", "M22586", "L51639", "L50250", "M20109"], 267 | "Type": ["L", "M", "L", "L", "M"], 268 | "Air temperature": [302.3, 300.8, 302.6, 300, 303.4], # (K) 269 | "Machine Failure": [False, True, False, False, True] 270 | }, 271 | schema_overrides={"Air temperature": pl.Float32}, 272 | ) 273 | return 274 | 275 | 276 | @app.cell(hide_code=True) 277 | def _(mo): 278 | mo.md( 279 | r""" 280 | ### References 281 | 282 | 1. Polars documentation ([link](https://docs.pola.rs/api/python/stable/reference/datatypes.html)) 283 | """ 284 | ) 285 | return 286 | 287 | 288 | @app.cell(hide_code=True) 289 | def _(): 290 | import marimo as mo 291 | import polars as pl 292 | return mo, pl 293 | 294 | 295 | if __name__ == "__main__": 296 | app.run() 297 | -------------------------------------------------------------------------------- /polars/12_aggregations.py: -------------------------------------------------------------------------------- 1 | # /// script 2 | # requires-python = ">=3.13" 3 | # dependencies = [ 4 | # "marimo", 5 | # "polars==1.23.0", 6 | # ] 7 | # /// 8 | 9 | import marimo 10 | 11 | __generated_with = "0.12.9" 12 | app = marimo.App(width="medium") 13 | 14 | 15 | @app.cell 16 | def _(): 17 | import marimo as mo 18 | return (mo,) 19 | 20 | 21 | @app.cell(hide_code=True) 22 | def _(mo): 23 | mo.md( 24 | r""" 25 | # Aggregations 26 | _By [Joram Mutenge](https://www.udemy.com/user/joram-mutenge/)._ 27 | 28 | In this notebook, you'll learn how to perform different types of aggregations in Polars, including grouping by categories and time. We'll analyze sales data from a clothing store, focusing on three product categories: hats, socks, and sweaters. 29 | """ 30 | ) 31 | return 32 | 33 | 34 | @app.cell 35 | def _(): 36 | import polars as pl 37 | 38 | df = (pl.read_csv('https://raw.githubusercontent.com/jorammutenge/learn-rust/refs/heads/main/sample_sales.csv', try_parse_dates=True) 39 | .rename(lambda col: col.replace(' ','_').lower()) 40 | ) 41 | df 42 | return df, pl 43 | 44 | 45 | @app.cell(hide_code=True) 46 | def _(mo): 47 | mo.md( 48 | r""" 49 | ## Grouping by category 50 | ### With single category 51 | Let's find out how many of each product category we sold. 52 | """ 53 | ) 54 | return 55 | 56 | 57 | @app.cell 58 | def _(df, pl): 59 | (df 60 | .group_by('category') 61 | .agg(pl.sum('quantity')) 62 | ) 63 | return 64 | 65 | 66 | @app.cell(hide_code=True) 67 | def _(mo): 68 | mo.md( 69 | r""" 70 | It looks like we sold more sweaters. Maybe this was a winter season. 71 | 72 | Let's add another aggregate to see how much was spent on the total units for each product. 73 | """ 74 | ) 75 | return 76 | 77 | 78 | @app.cell 79 | def _(df, pl): 80 | (df 81 | .group_by('category') 82 | .agg(pl.sum('quantity'), 83 | pl.sum('ext_price')) 84 | ) 85 | return 86 | 87 | 88 | @app.cell(hide_code=True) 89 | def _(mo): 90 | mo.md(r"""We could also write aggregate code for the two columns as a single line.""") 91 | return 92 | 93 | 94 | @app.cell 95 | def _(df, pl): 96 | (df 97 | .group_by('category') 98 | .agg(pl.sum('quantity','ext_price')) 99 | ) 100 | return 101 | 102 | 103 | @app.cell(hide_code=True) 104 | def _(mo): 105 | mo.md(r"""Actually, the way we've been writing the aggregate lines is syntactic sugar. Here's a longer way of doing it as shown in the [Polars documentation](https://docs.pola.rs/api/python/stable/reference/dataframe/api/polars.dataframe.group_by.GroupBy.agg.html).""") 106 | return 107 | 108 | 109 | @app.cell 110 | def _(df, pl): 111 | (df 112 | .group_by('category') 113 | .agg(pl.col('quantity').sum(), 114 | pl.col('ext_price').sum()) 115 | ) 116 | return 117 | 118 | 119 | @app.cell(hide_code=True) 120 | def _(mo): 121 | mo.md( 122 | r""" 123 | ### With multiple categories 124 | We can also group by multiple categories. Let's find out how many items we sold in each product category for each SKU. This more detailed aggregation will produce more rows than the previous DataFrame. 125 | """ 126 | ) 127 | return 128 | 129 | 130 | @app.cell 131 | def _(df, pl): 132 | (df 133 | .group_by('category','sku') 134 | .agg(pl.sum('quantity')) 135 | ) 136 | return 137 | 138 | 139 | @app.cell(hide_code=True) 140 | def _(mo): 141 | mo.md( 142 | r""" 143 | Aggregations when grouping data are not limited to sums. You can also use functions like [`max`, `min`, `median`, `first`, and `last`](https://docs.pola.rs/user-guide/expressions/aggregation/#basic-aggregations). 144 | 145 | Let's find the largest sale quantity for each product category. 146 | """ 147 | ) 148 | return 149 | 150 | 151 | @app.cell 152 | def _(df, pl): 153 | (df 154 | .group_by('category') 155 | .agg(pl.max('quantity')) 156 | ) 157 | return 158 | 159 | 160 | @app.cell(hide_code=True) 161 | def _(mo): 162 | mo.md( 163 | r""" 164 | Let's make the aggregation more interesting. We'll identify the first customer to purchase each item, along with the quantity they bought and the amount they spent. 165 | 166 | **Note:** To make this work, we'll have to sort the date from earliest to latest. 167 | """ 168 | ) 169 | return 170 | 171 | 172 | @app.cell 173 | def _(df, pl): 174 | (df 175 | .sort('date') 176 | .group_by('category') 177 | .agg(pl.first('account_name','quantity','ext_price')) 178 | ) 179 | return 180 | 181 | 182 | @app.cell(hide_code=True) 183 | def _(mo): 184 | mo.md( 185 | r""" 186 | ## Grouping by time 187 | Since `datetime` is a special data type in Polars, we can perform various group-by aggregations on it. 188 | 189 | Our dataset spans a two-year period. Let's calculate the total dollar sales for each year. We'll do it the naive way first so you can appreciate grouping with time. 190 | """ 191 | ) 192 | return 193 | 194 | 195 | @app.cell 196 | def _(df, pl): 197 | (df 198 | .with_columns(year=pl.col('date').dt.year()) 199 | .group_by('year') 200 | .agg(pl.sum('ext_price').round(2)) 201 | ) 202 | return 203 | 204 | 205 | @app.cell(hide_code=True) 206 | def _(mo): 207 | mo.md( 208 | r""" 209 | We had more sales in 2014. 210 | 211 | Now let's perform the above operation by grouping with time. This requires sorting the dataframe first. 212 | """ 213 | ) 214 | return 215 | 216 | 217 | @app.cell 218 | def _(df, pl): 219 | (df 220 | .sort('date') 221 | .group_by_dynamic('date', every='1y') 222 | .agg(pl.sum('ext_price')) 223 | ) 224 | return 225 | 226 | 227 | @app.cell(hide_code=True) 228 | def _(mo): 229 | mo.md( 230 | r""" 231 | The beauty of grouping with time is that it allows us to resample the data by selecting whatever time interval we want. 232 | 233 | Let's find out what the quarterly sales were for 2014 234 | """ 235 | ) 236 | return 237 | 238 | 239 | @app.cell 240 | def _(df, pl): 241 | (df 242 | .filter(pl.col('date').dt.year() == 2014) 243 | .sort('date') 244 | .group_by_dynamic('date', every='1q') 245 | .agg(pl.sum('ext_price')) 246 | ) 247 | return 248 | 249 | 250 | @app.cell(hide_code=True) 251 | def _(mo): 252 | mo.md( 253 | r""" 254 | Here's an interesting question we can answer that takes advantage of grouping by time. 255 | 256 | Let's find the hour of the day where we had the most sales in dollars. 257 | """ 258 | ) 259 | return 260 | 261 | 262 | @app.cell 263 | def _(df, pl): 264 | (df 265 | .sort('date') 266 | .group_by_dynamic('date', every='1h') 267 | .agg(pl.max('ext_price')) 268 | .filter(pl.col('ext_price') == pl.col('ext_price').max()) 269 | ) 270 | return 271 | 272 | 273 | @app.cell(hide_code=True) 274 | def _(mo): 275 | mo.md(r"""Just for fun, let's find the median number of items sold in each SKU and the total dollar amount in each SKU every six days.""") 276 | return 277 | 278 | 279 | @app.cell 280 | def _(df, pl): 281 | (df 282 | .sort('date') 283 | .group_by_dynamic('date', every='6d') 284 | .agg(pl.first('sku'), 285 | pl.median('quantity'), 286 | pl.sum('ext_price')) 287 | ) 288 | return 289 | 290 | 291 | @app.cell(hide_code=True) 292 | def _(mo): 293 | mo.md(r"""Let's rename the columns to clearly indicate the type of aggregation performed. This will help us identify the aggregation method used on a column without needing to check the code.""") 294 | return 295 | 296 | 297 | @app.cell 298 | def _(df, pl): 299 | (df 300 | .sort('date') 301 | .group_by_dynamic('date', every='6d') 302 | .agg(pl.first('sku'), 303 | pl.median('quantity').alias('median_qty'), 304 | pl.sum('ext_price').alias('total_dollars')) 305 | ) 306 | return 307 | 308 | 309 | @app.cell(hide_code=True) 310 | def _(mo): 311 | mo.md( 312 | r""" 313 | ## Grouping with over 314 | 315 | Sometimes, we may want to perform an aggregation but also keep all the columns and rows of the dataframe. 316 | 317 | Let's assign a value to indicate the number of times each customer visited and bought something. 318 | """ 319 | ) 320 | return 321 | 322 | 323 | @app.cell 324 | def _(df, pl): 325 | (df 326 | .with_columns(buy_freq=pl.col('account_name').len().over('account_name')) 327 | ) 328 | return 329 | 330 | 331 | @app.cell(hide_code=True) 332 | def _(mo): 333 | mo.md(r"""Finally, let's determine which customers visited the store the most and bought something.""") 334 | return 335 | 336 | 337 | @app.cell 338 | def _(df, pl): 339 | (df 340 | .with_columns(buy_freq=pl.col('account_name').len().over('account_name')) 341 | .filter(pl.col('buy_freq') == pl.col('buy_freq').max()) 342 | .select('account_name','buy_freq') 343 | .unique() 344 | ) 345 | return 346 | 347 | 348 | @app.cell(hide_code=True) 349 | def _(mo): 350 | mo.md(r"""There's more you can do with aggregations in Polars such as [sorting with aggregations](https://docs.pola.rs/user-guide/expressions/aggregation/#sorting). We hope that in this notebook, we've armed you with the tools to get started.""") 351 | return 352 | 353 | 354 | if __name__ == "__main__": 355 | app.run() 356 | -------------------------------------------------------------------------------- /polars/README.md: -------------------------------------------------------------------------------- 1 | # Learn Polars 2 | 3 | _🚧 This collection is a work in progress. Please help us add notebooks!_ 4 | 5 | This collection of marimo notebooks is designed to teach you the basics of data wrangling using a Python library called Polars. 6 | 7 | **Help us build this course! ⚒️** 8 | 9 | We're seeking contributors to help us build these notebooks. Every contributor will be acknowledged as an author in this README and in their contributed notebooks. Head over to the [tracking issue](https://github.com/marimo-team/learn/issues/40) to sign up for a planned notebook or propose your own. 10 | 11 | **Running notebooks.** To run a notebook locally, use 12 | 13 | ```bash 14 | uvx marimo edit 15 | ``` 16 | 17 | You can also open notebooks in our online playground by appending marimo.app/ to a notebook's URL: 18 | 19 | [https://marimo.app/github.com/marimo-team/learn/blob/main/polars/01_why_polars.py](https://marimo.app/github.com/marimo-team/learn/blob/main/polars/01_why_polars.py). 20 | 21 | **Thanks to all our notebook authors!** 22 | 23 | * [Koushik Khan](https://github.com/koushikkhan) 24 | * [Péter Gyarmati](https://github.com/peter-gy) 25 | * [Joram Mutenge](https://github.com/jorammutenge) 26 | * [etrotta](https://github.com/etrotta) 27 | * [Debajyoti Das](https://github.com/debajyotid2) 28 | -------------------------------------------------------------------------------- /probability/01_sets.py: -------------------------------------------------------------------------------- 1 | # /// script 2 | # requires-python = ">=3.10" 3 | # dependencies = [ 4 | # "marimo", 5 | # ] 6 | # /// 7 | 8 | import marimo 9 | 10 | __generated_with = "0.11.0" 11 | app = marimo.App() 12 | 13 | 14 | @app.cell(hide_code=True) 15 | def _(mo): 16 | mo.md( 17 | r""" 18 | # Sets 19 | 20 | Probability is the study of "events", assigning numerical values to how likely 21 | events are to occur. For example, probability lets us quantify how likely it is for it to rain or shine on a given day. 22 | 23 | 24 | Typically we reason about _sets_ of events. In mathematics, 25 | a set is a collection of elements, with no element included more than once. 26 | Elements can be any kind of object. 27 | 28 | For example: 29 | 30 | - ☀️ Weather events: $\{\text{Rain}, \text{Overcast}, \text{Clear}\}$ 31 | - 🎲 Die rolls: $\{1, 2, 3, 4, 5, 6\}$ 32 | - 🪙 Pairs of coin flips = $\{ \text{(Heads, Heads)}, \text{(Heads, Tails)}, \text{(Tails, Tails)} \text{(Tails, Heads)}\}$ 33 | 34 | Sets are the building blocks of probability, and will arise frequently in our study. 35 | """ 36 | ) 37 | return 38 | 39 | 40 | @app.cell(hide_code=True) 41 | def _(mo): 42 | mo.md(r"""## Set operations""") 43 | return 44 | 45 | 46 | @app.cell(hide_code=True) 47 | def _(mo): 48 | mo.md(r"""In Python, sets are made with the `set` function:""") 49 | return 50 | 51 | 52 | @app.cell 53 | def _(): 54 | A = set([2, 3, 5, 7]) 55 | A 56 | return (A,) 57 | 58 | 59 | @app.cell 60 | def _(): 61 | B = set([0, 1, 2, 3, 5, 8]) 62 | B 63 | return (B,) 64 | 65 | 66 | @app.cell(hide_code=True) 67 | def _(mo): 68 | mo.md( 69 | r""" 70 | Below we explain common operations on sets. 71 | 72 | _**Try it!** Try modifying the definitions of `A` and `B` above, and see how the results change below._ 73 | 74 | The **union** $A \cup B$ of sets $A$ and $B$ is the set of elements in $A$, $B$, or both. 75 | """ 76 | ) 77 | return 78 | 79 | 80 | @app.cell 81 | def _(A, B): 82 | A | B 83 | return 84 | 85 | 86 | @app.cell(hide_code=True) 87 | def _(mo): 88 | mo.md(r"""The **intersection** $A \cap B$ is the set of elements in both $A$ and $B$""") 89 | return 90 | 91 | 92 | @app.cell 93 | def _(A, B): 94 | A & B 95 | return 96 | 97 | 98 | @app.cell(hide_code=True) 99 | def _(mo): 100 | mo.md(r"""The **difference** $A \setminus B$ is the set of elements in $A$ that are not in $B$.""") 101 | return 102 | 103 | 104 | @app.cell 105 | def _(A, B): 106 | A - B 107 | return 108 | 109 | 110 | @app.cell(hide_code=True) 111 | def _(mo): 112 | mo.md( 113 | """ 114 | ### 🎬 An interactive example 115 | 116 | Here's a simple example that classifies TV shows into sets by genre, and uses these sets to recommend shows to a user based on their preferences. 117 | """ 118 | ) 119 | return 120 | 121 | 122 | @app.cell(hide_code=True) 123 | def _(mo): 124 | viewer_type = mo.ui.radio( 125 | options={ 126 | "I like action and drama!": "New Viewer", 127 | "I only like action shows": "Action Fan", 128 | "I only like dramas": "Drama Fan", 129 | }, 130 | value="I like action and drama!", 131 | label="Which genre do you prefer?", 132 | ) 133 | return (viewer_type,) 134 | 135 | 136 | @app.cell(hide_code=True) 137 | def _(viewer_type): 138 | viewer_type 139 | return 140 | 141 | 142 | @app.cell 143 | def _(): 144 | action_shows = {"Stranger Things", "The Witcher", "Money Heist"} 145 | drama_shows = {"The Crown", "Money Heist", "Bridgerton"} 146 | return action_shows, drama_shows 147 | 148 | 149 | @app.cell 150 | def _(action_shows, drama_shows): 151 | recommendations = { 152 | "New Viewer": action_shows | drama_shows, # Union for new viewers 153 | "Action Fan": action_shows - drama_shows, # Unique action shows 154 | "Drama Fan": drama_shows - action_shows, # Unique drama shows 155 | } 156 | return (recommendations,) 157 | 158 | 159 | @app.cell(hide_code=True) 160 | def _(mo, recommendations, viewer_type): 161 | result = recommendations[viewer_type.value] 162 | 163 | explanation = { 164 | "New Viewer": "You get everything to explore!", 165 | "Action Fan": "Pure action, no drama!", 166 | "Drama Fan": "Drama-focused selections!", 167 | } 168 | 169 | mo.md(f""" 170 | **🎬 Recommended shows.** Based on your preference for **{viewer_type.value}**, 171 | we recommend: 172 | 173 | {", ".join(result)} 174 | 175 | **Why these shows?** 176 | {explanation[viewer_type.value]} 177 | """) 178 | return explanation, result 179 | 180 | 181 | @app.cell(hide_code=True) 182 | def _(mo): 183 | mo.md(""" 184 | ### Exercise 185 | 186 | Given these sets: 187 | 188 | - A = {🎮, 📱, 💻} 189 | 190 | - B = {📱, 💻, 🖨️} 191 | 192 | - C = {💻, 🖨️, ⌨️} 193 | 194 | Can you: 195 | 196 | 1. Find all elements that are in A or B 197 | 198 | 2. Find elements common to all three sets 199 | 200 | 3. Find elements in A that aren't in C 201 | 202 |
203 | 204 | Check your answers! 205 | 206 | 1. A ∪ B = {🎮, 📱, 💻, 🖨️}
207 | 2. A ∩ B ∩ C = {💻}
208 | 3. A - C = {🎮, 📱} 209 | 210 |
211 | """) 212 | return 213 | 214 | 215 | @app.cell(hide_code=True) 216 | def _(mo): 217 | mo.md( 218 | r""" 219 | ## 🧮 Set properties 220 | 221 | Here are some important properties of the set operations: 222 | 223 | 1. **Commutative**: $A \cup B = B \cup A$ 224 | 2. **Associative**: $(A \cup B) \cup C = A \cup (B \cup C)$ 225 | 3. **Distributive**: $A \cup (B \cap C) = (A \cup B) \cap (A \cup C)$ 226 | """ 227 | ) 228 | return 229 | 230 | 231 | @app.cell(hide_code=True) 232 | def _(mo): 233 | mo.md( 234 | r""" 235 | ## Set builder notation 236 | 237 | To compactly describe the elements in a set, we can use **set builder notation**, which specifies conditions that must be true for elements to be in the set. 238 | 239 | For example, here is how to specify the set of positive numbers less than 10: 240 | 241 | \[ 242 | \{x \mid 0 < x < 10 \} 243 | \] 244 | 245 | The predicate to the right of the vertical bar $\mid$ specifies conditions that must be true for an element to be in the set; the expression to the left of $\mid$ specifies the value being included. 246 | 247 | In Python, set builder notation is called a "set comprehension." 248 | """ 249 | ) 250 | return 251 | 252 | 253 | @app.cell 254 | def _(): 255 | def predicate(x): 256 | return x > 0 and x < 10 257 | return (predicate,) 258 | 259 | 260 | @app.cell 261 | def _(predicate): 262 | set(x for x in range(100) if predicate(x)) 263 | return 264 | 265 | 266 | @app.cell(hide_code=True) 267 | def _(mo): 268 | mo.md("""**Try it!** Try modifying the `predicate` function above and see how the set changes.""") 269 | return 270 | 271 | 272 | @app.cell(hide_code=True) 273 | def _(mo): 274 | mo.md(""" 275 | ## Summary 276 | 277 | You've learned: 278 | 279 | - Basic set operations 280 | - Set properties 281 | - Real-world applications 282 | 283 | In the next lesson, we'll define probability from the ground up, using sets. 284 | 285 | Remember: In probability, every event is a set, and every set can be an event! 286 | """) 287 | return 288 | 289 | 290 | @app.cell 291 | def _(): 292 | import marimo as mo 293 | return (mo,) 294 | 295 | 296 | if __name__ == "__main__": 297 | app.run() 298 | -------------------------------------------------------------------------------- /probability/02_axioms.py: -------------------------------------------------------------------------------- 1 | # /// script 2 | # requires-python = ">=3.11" 3 | # dependencies = [ 4 | # "marimo", 5 | # "matplotlib==3.10.0", 6 | # "numpy==2.2.2", 7 | # ] 8 | # /// 9 | 10 | import marimo 11 | 12 | __generated_with = "0.11.2" 13 | app = marimo.App(width="medium") 14 | 15 | 16 | @app.cell 17 | def _(): 18 | import marimo as mo 19 | return (mo,) 20 | 21 | 22 | @app.cell(hide_code=True) 23 | def _(mo): 24 | mo.md( 25 | r""" 26 | # Axioms of Probability 27 | 28 | Probability theory is built on three fundamental axioms, known as the [Kolmogorov axioms](https://en.wikipedia.org/wiki/Probability_axioms). These axioms form 29 | the mathematical foundation for all of probability theory[1](https://chrispiech.github.io/probabilityForComputerScientists/en/part1/probability). 30 | 31 | Let's explore each axiom and understand why they make intuitive sense: 32 | """ 33 | ) 34 | return 35 | 36 | 37 | @app.cell(hide_code=True) 38 | def _(mo): 39 | mo.md( 40 | r""" 41 | ## The Three Axioms 42 | 43 | | Axiom | Mathematical Form | Meaning | 44 | |-------|------------------|----------| 45 | | **Axiom 1** | $0 \leq P(E) \leq 1$ | All probabilities are between 0 and 1 | 46 | | **Axiom 2** | $P(S) = 1$ | The probability of the sample space is 1 | 47 | | **Axiom 3** | $P(E \cup F) = P(E) + P(F)$ | For mutually exclusive events, probabilities add | 48 | 49 | where the set $S$ is the sample space (all possible outcomes), and $E$ and $F$ are sets that represent events. The notation $P(E)$ denotes the probability of $E$, which you can interpret as the chance that something happens. $P(E) = 0$ means that the event cannot happen, while $P(E) = 1$ means the event will happen no matter what; $P(E) = 0.5$ means that $E$ has a 50% chance of happening. 50 | 51 | For an example, when rolling a fair six-sided die once, the sample space $S$ is the set of die faces ${1, 2, 3, 4, 5, 6}$, and there are many possible events; we'll see some examples below. 52 | """ 53 | ) 54 | return 55 | 56 | 57 | @app.cell(hide_code=True) 58 | def _(mo): 59 | mo.md( 60 | r""" 61 | ## Understanding Through Examples 62 | 63 | Let's explore these axioms using a simple experiment: rolling a fair six-sided die. 64 | We'll use this to demonstrate why each axiom makes intuitive sense. 65 | """ 66 | ) 67 | return 68 | 69 | 70 | @app.cell 71 | def _(event): 72 | event 73 | return 74 | 75 | 76 | @app.cell(hide_code=True) 77 | def _(mo): 78 | # Create an interactive widget to explore different events 79 | 80 | event = mo.ui.dropdown( 81 | 82 | options=[ 83 | 84 | "Rolling an even number (2,4,6)", 85 | 86 | "Rolling an odd number (1,3,5)", 87 | 88 | "Rolling a prime number (2,3,5)", 89 | 90 | "Rolling less than 4 (1,2,3)", 91 | 92 | "Any possible roll (1,2,3,4,5,6)", 93 | 94 | ], 95 | 96 | value="Rolling an even number (2,4,6)", 97 | 98 | label="Select an event" 99 | 100 | ) 101 | return (event,) 102 | 103 | 104 | @app.cell(hide_code=True) 105 | def _(event, mo, np, plt): 106 | # Define the probabilities for each event 107 | event_map = { 108 | "Rolling an even number (2,4,6)": [2, 4, 6], 109 | "Rolling an odd number (1,3,5)": [1, 3, 5], 110 | "Rolling a prime number (2,3,5)": [2, 3, 5], 111 | "Rolling less than 4 (1,2,3)": [1, 2, 3], 112 | "Any possible roll (1,2,3,4,5,6)": [1, 2, 3, 4, 5, 6], 113 | } 114 | 115 | # Get outcomes directly from the event value 116 | outcomes = event_map[event.value] 117 | prob = len(outcomes) / 6 118 | 119 | # Visualize the probability 120 | dice = np.arange(1, 7) 121 | colors = ['#1f77b4' if d in outcomes else '#d3d3d3' for d in dice] 122 | 123 | fig, ax = plt.subplots(figsize=(8, 2)) 124 | ax.bar(dice, np.ones_like(dice), color=colors) 125 | ax.set_xticks(dice) 126 | ax.set_yticks([]) 127 | ax.set_title(f"P(Event) = {prob:.2f}") 128 | 129 | # Add explanation 130 | explanation = mo.md(f""" 131 | **Event**: {event.value} 132 | 133 | **Probability**: {prob:.2f} 134 | 135 | **Favorable outcomes**: {outcomes} 136 | 137 | This example demonstrates: 138 | 139 | - Axiom 1: The probability is between 0 and 1 140 | 141 | - Axiom 2: For the sample space, P(S) = 1 142 | 143 | - Axiom 3: The probability is the sum of individual outcome probabilities 144 | """) 145 | 146 | mo.hstack([plt.gcf(), explanation]) 147 | return ax, colors, dice, event_map, explanation, fig, outcomes, prob 148 | 149 | 150 | @app.cell(hide_code=True) 151 | def _(mo): 152 | mo.md( 153 | r""" 154 | ## Why These Axioms Matter 155 | 156 | These axioms are more than just rules - they provide the foundation for all of probability theory: 157 | 158 | 1. **Non-negativity** (Axiom 1) makes intuitive sense: you can't have a negative number of occurrences 159 | in any experiment. 160 | 161 | 2. **Normalization** (Axiom 2) ensures that something must happen - the total probability must be 1. 162 | 163 | 3. **Additivity** (Axiom 3) lets us build complex probabilities from simple ones, but only for events 164 | that can't happen together (mutually exclusive events). 165 | 166 | From these simple rules, we can derive all the powerful tools of probability theory that are used in 167 | statistics, machine learning, and other fields. 168 | """ 169 | ) 170 | return 171 | 172 | 173 | @app.cell(hide_code=True) 174 | def _(mo): 175 | mo.md( 176 | r""" 177 | ## 🤔 Test Your Understanding 178 | 179 | Consider rolling two dice. Which of these statements follow from the axioms? 180 | 181 |
182 | 1. P(sum is 13) = 0 183 | 184 | ✅ Correct! This follows from Axiom 1. Since no combination of dice can sum to 13, 185 | the probability must be non-negative but can be 0. 186 |
187 | 188 |
189 | 2. P(sum is 7) + P(sum is not 7) = 1 190 | 191 | ✅ Correct! This follows from Axioms 2 and 3. These events are mutually exclusive and cover 192 | the entire sample space. 193 |
194 | 195 |
196 | 3. P(first die is 6 or second die is 6) = P(first die is 6) + P(second die is 6) 197 | 198 | ❌ Incorrect! This doesn't follow from Axiom 3 because the events are not mutually exclusive - 199 | you could roll (6,6). 200 |
201 | """ 202 | ) 203 | return 204 | 205 | 206 | @app.cell 207 | def _(): 208 | import numpy as np 209 | import matplotlib.pyplot as plt 210 | return np, plt 211 | 212 | 213 | if __name__ == "__main__": 214 | app.run() 215 | -------------------------------------------------------------------------------- /probability/03_probability_of_or.py: -------------------------------------------------------------------------------- 1 | # /// script 2 | # requires-python = ">=3.10" 3 | # dependencies = [ 4 | # "marimo", 5 | # "matplotlib", 6 | # "matplotlib-venn" 7 | # ] 8 | # /// 9 | 10 | import marimo 11 | 12 | __generated_with = "0.11.2" 13 | app = marimo.App(width="medium") 14 | 15 | 16 | @app.cell 17 | def _(): 18 | import marimo as mo 19 | return (mo,) 20 | 21 | 22 | @app.cell 23 | def _(): 24 | import matplotlib.pyplot as plt 25 | from matplotlib_venn import venn2 26 | import numpy as np 27 | return np, plt, venn2 28 | 29 | 30 | @app.cell(hide_code=True) 31 | def _(mo): 32 | mo.md( 33 | r""" 34 | # Probability of Or 35 | 36 | When calculating the probability of either one event _or_ another occurring, we need to be careful about how we combine probabilities. The method depends on whether the events can happen together[1](https://chrispiech.github.io/probabilityForComputerScientists/en/part1/prob_or/). 37 | 38 | Let's explore how to calculate $P(E \cup F)$, i.e. $P(E \text{ or } F)$, in different scenarios. 39 | """ 40 | ) 41 | return 42 | 43 | 44 | @app.cell(hide_code=True) 45 | def _(mo): 46 | mo.md( 47 | r""" 48 | ## Mutually Exclusive Events 49 | 50 | Two events $E$ and $F$ are **mutually exclusive** if they cannot occur simultaneously. 51 | In set notation, this means: 52 | 53 | $E \cap F = \emptyset$ 54 | 55 | For example: 56 | 57 | - Rolling an even number (2,4,6) vs rolling an odd number (1,3,5) 58 | - Drawing a heart vs drawing a spade from a deck 59 | - Passing vs failing a test 60 | 61 | Here's a Python function to check if two sets of outcomes are mutually exclusive: 62 | """ 63 | ) 64 | return 65 | 66 | 67 | @app.cell 68 | def _(): 69 | def are_mutually_exclusive(event1, event2): 70 | return len(event1.intersection(event2)) == 0 71 | 72 | # Example with dice rolls 73 | even_numbers = {2, 4, 6} 74 | odd_numbers = {1, 3, 5} 75 | prime_numbers = {2, 3, 5, 7} 76 | return are_mutually_exclusive, even_numbers, odd_numbers, prime_numbers 77 | 78 | 79 | @app.cell 80 | def _(are_mutually_exclusive, even_numbers, odd_numbers): 81 | are_mutually_exclusive(even_numbers, odd_numbers) 82 | return 83 | 84 | 85 | @app.cell 86 | def _(are_mutually_exclusive, even_numbers, prime_numbers): 87 | are_mutually_exclusive(even_numbers, prime_numbers) 88 | return 89 | 90 | 91 | @app.cell(hide_code=True) 92 | def _(mo): 93 | mo.md( 94 | r""" 95 | ## Or with Mutually Exclusive Events 96 | 97 | For mutually exclusive events, the probability of either event occurring is simply the sum of their individual probabilities: 98 | 99 | $P(E \cup F) = P(E) + P(F)$ 100 | 101 | This extends to multiple events. For $n$ mutually exclusive events $E_1, E_2, \ldots, E_n$: 102 | 103 | $P(E_1 \cup E_2 \cup \cdots \cup E_n) = \sum_{i=1}^n P(E_i)$ 104 | 105 | Let's implement this calculation: 106 | """ 107 | ) 108 | return 109 | 110 | 111 | @app.cell 112 | def _(): 113 | def prob_union_mutually_exclusive(probabilities): 114 | return sum(probabilities) 115 | 116 | # Example: Rolling a die 117 | # P(even) = P(2) + P(4) + P(6) 118 | p_even_mutually_exclusive = prob_union_mutually_exclusive([1/6, 1/6, 1/6]) 119 | print(f"P(rolling an even number) = {p_even_mutually_exclusive}") 120 | 121 | # P(prime) = P(2) + P(3) + P(5) 122 | p_prime_mutually_exclusive = prob_union_mutually_exclusive([1/6, 1/6, 1/6]) 123 | print(f"P(rolling a prime number) = {p_prime_mutually_exclusive}") 124 | return ( 125 | p_even_mutually_exclusive, 126 | p_prime_mutually_exclusive, 127 | prob_union_mutually_exclusive, 128 | ) 129 | 130 | 131 | @app.cell(hide_code=True) 132 | def _(mo): 133 | mo.md( 134 | r""" 135 | ## Or with Non-Mutually Exclusive Events 136 | 137 | When events can occur together, we need to use the **inclusion-exclusion principle**: 138 | 139 | $P(E \cup F) = P(E) + P(F) - P(E \cap F)$ 140 | 141 | Why subtract $P(E \cap F)$? Because when we add $P(E)$ and $P(F)$, we count the overlap twice! 142 | 143 | For example, consider calculating $P(\text{prime or even})$ when rolling a die: 144 | 145 | - Prime numbers: {2, 3, 5} 146 | - Even numbers: {2, 4, 6} 147 | - The number 2 is counted twice unless we subtract its probability 148 | 149 | Here's how to implement this calculation: 150 | """ 151 | ) 152 | return 153 | 154 | 155 | @app.cell 156 | def _(): 157 | def prob_union_general(p_a, p_b, p_intersection): 158 | """Calculate probability of union for any two events""" 159 | return p_a + p_b - p_intersection 160 | 161 | # Example: Rolling a die 162 | # P(prime or even) 163 | p_prime_general = 3/6 # P(prime) = P(2,3,5) 164 | p_even_general = 3/6 # P(even) = P(2,4,6) 165 | p_intersection = 1/6 # P(intersection) = P(2) 166 | 167 | result = prob_union_general(p_prime_general, p_even_general, p_intersection) 168 | print(f"P(prime or even) = {p_prime_general} + {p_even_general} - {p_intersection} = {result}") 169 | return ( 170 | p_even_general, 171 | p_intersection, 172 | p_prime_general, 173 | prob_union_general, 174 | result, 175 | ) 176 | 177 | 178 | @app.cell(hide_code=True) 179 | def _(mo): 180 | mo.md( 181 | r""" 182 | ### Extension to Three Events 183 | 184 | For three events, the inclusion-exclusion principle becomes: 185 | 186 | $P(E_1 \cup E_2 \cup E_3) = P(E_1) + P(E_2) + P(E_3)$ 187 | $- P(E_1 \cap E_2) - P(E_1 \cap E_3) - P(E_2 \cap E_3)$ 188 | $+ P(E_1 \cap E_2 \cap E_3)$ 189 | 190 | The pattern is: 191 | 192 | 1. Add individual probabilities 193 | 2. Subtract probabilities of pairs 194 | 3. Add probability of triple intersection 195 | """ 196 | ) 197 | return 198 | 199 | 200 | @app.cell(hide_code=True) 201 | def _(mo): 202 | mo.md(r"""### Interactive example:""") 203 | return 204 | 205 | 206 | @app.cell 207 | def _(event_type): 208 | event_type 209 | return 210 | 211 | 212 | @app.cell(hide_code=True) 213 | def _(mo): 214 | # Create a dropdown to select the type of events to visualize 215 | event_type = mo.ui.dropdown( 216 | options=[ 217 | "Mutually Exclusive Events (Rolling Odd vs Even)", 218 | "Non-Mutually Exclusive Events (Prime vs Even)", 219 | "Three Events (Less than 3, Even, Prime)" 220 | ], 221 | value="Mutually Exclusive Events (Rolling Odd vs Even)", 222 | label="Select Event Type" 223 | ) 224 | return (event_type,) 225 | 226 | 227 | @app.cell(hide_code=True) 228 | def _(event_type, mo, plt, venn2): 229 | # Define the events and their probabilities 230 | events_data = { 231 | "Mutually Exclusive Events (Rolling Odd vs Even)": { 232 | "sets": (round(3/6, 2), round(3/6, 2), 0), # (odd, even, intersection) 233 | "labels": ("Odd\n{1,3,5}", "Even\n{2,4,6}"), 234 | "title": "Mutually Exclusive Events: Odd vs Even Numbers", 235 | "explanation": r""" 236 | ### Mutually Exclusive Events 237 | 238 | $P(\text{Odd}) = \frac{3}{6} = 0.5$ 239 | 240 | $P(\text{Even}) = \frac{3}{6} = 0.5$ 241 | 242 | $P(\text{Odd} \cap \text{Even}) = 0$ 243 | 244 | $P(\text{Odd} \cup \text{Even}) = P(\text{Odd}) + P(\text{Even}) = 1$ 245 | 246 | These events are mutually exclusive because a number cannot be both odd and even. 247 | """ 248 | }, 249 | "Non-Mutually Exclusive Events (Prime vs Even)": { 250 | "sets": (round(2/6, 2), round(2/6, 2), round(1/6, 2)), # (prime-only, even-only, intersection) 251 | "labels": ("Prime\n{3,5}", "Even\n{4,6}"), 252 | "title": "Non-Mutually Exclusive: Prime vs Even Numbers", 253 | "explanation": r""" 254 | ### Non-Mutually Exclusive Events 255 | 256 | $P(\text{Prime}) = \frac{3}{6} = 0.5$ (2,3,5) 257 | 258 | $P(\text{Even}) = \frac{3}{6} = 0.5$ (2,4,6) 259 | 260 | $P(\text{Prime} \cap \text{Even}) = \frac{1}{6}$ (2) 261 | 262 | $P(\text{Prime} \cup \text{Even}) = \frac{3}{6} + \frac{3}{6} - \frac{1}{6} = \frac{5}{6}$ 263 | 264 | These events overlap because 2 is both prime and even. 265 | """ 266 | }, 267 | "Three Events (Less than 3, Even, Prime)": { 268 | "sets": (round(1/6, 2), round(2/6, 2), round(1/6, 2)), # (less than 3, even, intersection) 269 | "labels": ("<3\n{1,2}", "Even\n{2,4,6}"), 270 | "title": "Complex Example: Numbers < 3 and Even Numbers", 271 | "explanation": r""" 272 | ### Complex Event Interaction 273 | 274 | $P(x < 3) = \frac{2}{6}$ (1,2) 275 | 276 | $P(\text{Even}) = \frac{3}{6}$ (2,4,6) 277 | 278 | $P(x < 3 \cap \text{Even}) = \frac{1}{6}$ (2) 279 | 280 | $P(x < 3 \cup \text{Even}) = \frac{2}{6} + \frac{3}{6} - \frac{1}{6} = \frac{4}{6}$ 281 | 282 | The number 2 belongs to both sets, requiring the inclusion-exclusion principle. 283 | """ 284 | } 285 | } 286 | 287 | # Get data for selected event type 288 | data = events_data[event_type.value] 289 | 290 | # Create visualization 291 | plt.figure(figsize=(10, 5)) 292 | v = venn2(subsets=data["sets"], 293 | set_labels=data["labels"]) 294 | plt.title(data["title"]) 295 | 296 | # Display explanation alongside visualization 297 | mo.hstack([ 298 | plt.gcf(), 299 | mo.md(data["explanation"]) 300 | ]) 301 | return data, events_data, v 302 | 303 | 304 | @app.cell(hide_code=True) 305 | def _(mo): 306 | mo.md( 307 | r""" 308 | ## 🤔 Test Your Understanding 309 | 310 | Consider rolling a six-sided die. Which of these statements are true? 311 | 312 |
313 | 1. P(even or less than 3) = P(even) + P(less than 3) 314 | 315 | ❌ Incorrect! These events are not mutually exclusive (2 is both even and less than 3). 316 | We need to use the inclusion-exclusion principle. 317 |
318 | 319 |
320 | 2. P(even or greater than 4) = 4/6 321 | 322 | ✅ Correct! {2,4,6} ∪ {5,6} = {2,4,5,6}, so probability is 4/6. 323 |
324 | 325 |
326 | 3. P(prime or odd) = 5/6 327 | 328 | ✅ Correct! {2,3,5} ∪ {1,3,5} = {1,2,3,5}, so probability is 5/6. 329 |
330 | """ 331 | ) 332 | return 333 | 334 | 335 | @app.cell(hide_code=True) 336 | def _(mo): 337 | mo.md( 338 | """ 339 | ## Summary 340 | 341 | You've learned: 342 | 343 | - How to identify mutually exclusive events 344 | - The addition rule for mutually exclusive events 345 | - The inclusion-exclusion principle for overlapping events 346 | - How to extend these concepts to multiple events 347 | 348 | In the next lesson, we'll explore **conditional probability** - how the probability 349 | of one event changes when we know another event has occurred. 350 | """ 351 | ) 352 | return 353 | 354 | 355 | if __name__ == "__main__": 356 | app.run() 357 | -------------------------------------------------------------------------------- /probability/06_probability_of_and.py: -------------------------------------------------------------------------------- 1 | # /// script 2 | # requires-python = ">=3.10" 3 | # dependencies = [ 4 | # "marimo", 5 | # "matplotlib", 6 | # "matplotlib-venn" 7 | # ] 8 | # /// 9 | 10 | import marimo 11 | 12 | __generated_with = "0.11.4" 13 | app = marimo.App(width="medium") 14 | 15 | 16 | @app.cell 17 | def _(): 18 | import marimo as mo 19 | return (mo,) 20 | 21 | 22 | @app.cell 23 | def _(): 24 | import matplotlib.pyplot as plt 25 | from matplotlib_venn import venn2 26 | return plt, venn2 27 | 28 | 29 | @app.cell(hide_code=True) 30 | def _(mo): 31 | mo.md( 32 | r""" 33 | # Probability of And 34 | _This notebook is a computational companion to the book ["Probability for Computer Scientists"](https://chrispiech.github.io/probabilityForComputerScientists/en/part1/prob_and/), by Stanford professor Chris Piech._ 35 | 36 | When calculating the probability of both events occurring together, we need to consider whether the events are independent or dependent. 37 | Let's explore how to calculate $P(E \cap F)$, i.e. $P(E \text{ and } F)$, in different scenarios. 38 | """ 39 | ) 40 | return 41 | 42 | 43 | @app.cell(hide_code=True) 44 | def _(mo): 45 | mo.md( 46 | r""" 47 | ## And with Independent Events 48 | 49 | Two events $E$ and $F$ are **independent** if knowing one event occurred doesn't affect the probability of the other. 50 | For independent events: 51 | 52 | $P(E \text{ and } F) = P(E) \cdot P(F)$ 53 | 54 | For example: 55 | 56 | - Rolling a 6 on one die and getting heads on a coin flip 57 | - Drawing a heart from a deck, replacing it, and drawing another heart 58 | - Getting a computer error on Monday vs. Tuesday 59 | 60 | Here's a Python function to calculate probability for independent events: 61 | """ 62 | ) 63 | return 64 | 65 | 66 | @app.cell 67 | def _(): 68 | def calc_independent_prob(p_e, p_f): 69 | return p_e * p_f 70 | 71 | # Example 1: Rolling a die and flipping a coin 72 | p_six = 1/6 # P(rolling a 6) 73 | p_heads = 1/2 # P(getting heads) 74 | p_both = calc_independent_prob(p_six, p_heads) 75 | print(f"Example 1: P(rolling 6 AND getting heads) = {p_six:.3f} × {p_heads:.3f} = {p_both:.3f}") 76 | return calc_independent_prob, p_both, p_heads, p_six 77 | 78 | 79 | @app.cell 80 | def _(calc_independent_prob): 81 | # Example 2: Two independent system components failing 82 | p_cpu_fail = 0.05 # P(CPU failure) 83 | p_disk_fail = 0.03 # P(disk failure) 84 | p_both_fail = calc_independent_prob(p_cpu_fail, p_disk_fail) 85 | print(f"Example 2: P(both CPU and disk failing) = {p_cpu_fail:.3f} × {p_disk_fail:.3f} = {p_both_fail:.3f}") 86 | return p_both_fail, p_cpu_fail, p_disk_fail 87 | 88 | 89 | @app.cell(hide_code=True) 90 | def _(mo): 91 | mo.md( 92 | r""" 93 | ## And with Dependent Events 94 | 95 | For dependent events, we use the **chain rule**: 96 | 97 | $P(E \text{ and } F) = P(E) \cdot P(F|E)$ 98 | 99 | where $P(F|E)$ is the probability of $F$ occurring given that $E$ has occurred. 100 | 101 | For example: 102 | 103 | - Drawing two hearts without replacement 104 | - Getting two consecutive heads in poker 105 | - System failures in connected components 106 | 107 | Let's implement this calculation: 108 | """ 109 | ) 110 | return 111 | 112 | 113 | @app.cell 114 | def _(): 115 | def calc_dependent_prob(p_e, p_f_given_e): 116 | return p_e * p_f_given_e 117 | 118 | # Example 1: Drawing two hearts without replacement 119 | p_first_heart = 13/52 # P(first heart) 120 | p_second_heart = 12/51 # P(second heart | first heart) 121 | p_both_hearts = calc_dependent_prob(p_first_heart, p_second_heart) 122 | print(f"Example 1: P(two hearts) = {p_first_heart:.3f} × {p_second_heart:.3f} = {p_both_hearts:.3f}") 123 | return calc_dependent_prob, p_both_hearts, p_first_heart, p_second_heart 124 | 125 | 126 | @app.cell 127 | def _(calc_dependent_prob): 128 | # Example 2: Drawing two aces without replacement 129 | p_first_ace = 4/52 # P(first ace) 130 | p_second_ace = 3/51 # P(second ace | first ace) 131 | p_both_aces = calc_dependent_prob(p_first_ace, p_second_ace) 132 | print(f"Example 2: P(two aces) = {p_first_ace:.3f} × {p_second_ace:.3f} = {p_both_aces:.3f}") 133 | return p_both_aces, p_first_ace, p_second_ace 134 | 135 | 136 | @app.cell(hide_code=True) 137 | def _(mo): 138 | mo.md( 139 | r""" 140 | ## Multiple Events 141 | 142 | For multiple independent events: 143 | 144 | $P(E_1 \text{ and } E_2 \text{ and } \cdots \text{ and } E_n) = \prod_{i=1}^n P(E_i)$ 145 | 146 | For dependent events: 147 | 148 | $P(E_1 \text{ and } E_2 \text{ and } \cdots \text{ and } E_n) = P(E_1) \cdot P(E_2|E_1) \cdot P(E_3|E_1,E_2) \cdots P(E_n|E_1,\ldots,E_{n-1})$ 149 | 150 | Let's visualize these probabilities: 151 | """ 152 | ) 153 | return 154 | 155 | 156 | @app.cell(hide_code=True) 157 | def _(mo): 158 | mo.md(r"""### Interactive example""") 159 | return 160 | 161 | 162 | @app.cell 163 | def _(event_type): 164 | event_type 165 | return 166 | 167 | 168 | @app.cell(hide_code=True) 169 | def _(mo): 170 | event_type = mo.ui.dropdown( 171 | options=[ 172 | "Independent AND (Die and Coin)", 173 | "Dependent AND (Sequential Cards)", 174 | "Multiple AND (System Components)" 175 | ], 176 | value="Independent AND (Die and Coin)", 177 | label="Select AND Probability Scenario" 178 | ) 179 | return (event_type,) 180 | 181 | 182 | @app.cell(hide_code=True) 183 | def _(event_type, mo, plt, venn2): 184 | # Define the events and their probabilities 185 | events_data = { 186 | "Independent AND (Die and Coin)": { 187 | "sets": (0.33, 0.17, 0.08), # (die, coin, intersection) 188 | "labels": ("Die\nP(6)=1/6", "Coin\nP(H)=1/2"), 189 | "title": "Independent Events: Rolling a 6 AND Getting Heads", 190 | "explanation": r""" 191 | ### Independent Events: Die Roll and Coin Flip 192 | 193 | $P(\text{Rolling 6}) = \frac{1}{6} \approx 0.17$ 194 | 195 | $P(\text{Getting Heads}) = \frac{1}{2} = 0.5$ 196 | 197 | $P(\text{6 and Heads}) = \frac{1}{6} \times \frac{1}{2} = \frac{1}{12} \approx 0.08$ 198 | 199 | These events are independent because the outcome of the die roll 200 | doesn't affect the coin flip, and vice versa. 201 | """, 202 | }, 203 | "Dependent AND (Sequential Cards)": { 204 | "sets": ( 205 | 0.25, 206 | 0.24, 207 | 0.06, 208 | ), # (first heart, second heart, intersection) 209 | "labels": ("First\nP(H₁)=13/52", "Second\nP(H₂|H₁)=12/51"), 210 | "title": "Dependent Events: Drawing Two Hearts", 211 | "explanation": r""" 212 | ### Dependent Events: Drawing Hearts 213 | 214 | $P(\text{First Heart}) = \frac{13}{52} = 0.25$ 215 | 216 | $P(\text{Second Heart}|\text{First Heart}) = \frac{12}{51} \approx 0.24$ 217 | 218 | $P(\text{Both Hearts}) = \frac{13}{52} \times \frac{12}{51} \approx 0.06$ 219 | 220 | These events are dependent because drawing the first heart 221 | changes the probability of drawing the second heart. 222 | """, 223 | }, 224 | "Multiple AND (System Components)": { 225 | "sets": (0.05, 0.03, 0.0015), # (CPU fail, disk fail, intersection) 226 | "labels": ("CPU\nP(C)=0.05", "Disk\nP(D)=0.03"), 227 | "title": "Independent System Failures", 228 | "explanation": r""" 229 | ### System Component Failures 230 | 231 | $P(\text{CPU Failure}) = 0.05$ 232 | 233 | $P(\text{Disk Failure}) = 0.03$ 234 | 235 | $P(\text{Both Fail}) = 0.05 \times 0.03 = 0.0015$ 236 | 237 | Component failures are typically independent in **well-designed systems**, 238 | meaning one component's failure doesn't affect the other's probability of failing. 239 | """, 240 | }, 241 | } 242 | 243 | # Get data for selected event type 244 | data = events_data[event_type.value] 245 | 246 | # Create visualization 247 | plt.figure(figsize=(10, 5)) 248 | v = venn2(subsets=data["sets"], set_labels=data["labels"]) 249 | plt.title(data["title"]) 250 | 251 | # Display explanation alongside visualization 252 | mo.hstack([plt.gcf(), mo.md(data["explanation"])]) 253 | return data, events_data, v 254 | 255 | 256 | @app.cell(hide_code=True) 257 | def _(mo): 258 | mo.md( 259 | r""" 260 | ## 🤔 Test Your Understanding 261 | 262 | Which of these statements about AND probability are true? 263 | 264 |
265 | 1. The probability of getting two sixes in a row with a fair die is 1/36 266 | 267 | ✅ True! Since die rolls are independent events: 268 | P(two sixes) = P(first six) × P(second six) = 1/6 × 1/6 = 1/36 269 |
270 | 271 |
272 | 2. When drawing cards without replacement, P(two kings) = 4/52 × 4/52 273 | 274 | ❌ False! This is a dependent event. The correct calculation is: 275 | P(two kings) = P(first king) × P(second king | first king) = 4/52 × 3/51 276 |
277 | 278 |
279 | 3. If P(A) = 0.3 and P(B) = 0.4, then P(A and B) must be 0.12 280 | 281 | ❌ False! P(A and B) = 0.12 only if A and B are independent events. 282 | If they're dependent, we need P(B|A) to calculate P(A and B). 283 |
284 | 285 |
286 | 4. The probability of rolling a six AND getting tails is (1/6 × 1/2) 287 | 288 | ✅ True! These are independent events, so we multiply their individual probabilities: 289 | P(six and tails) = P(six) × P(tails) = 1/6 × 1/2 = 1/12 290 |
291 | """ 292 | ) 293 | return 294 | 295 | 296 | @app.cell(hide_code=True) 297 | def _(mo): 298 | mo.md( 299 | """ 300 | ## Summary 301 | 302 | You've learned: 303 | 304 | - How to identify independent vs dependent events 305 | - The multiplication rule for independent events 306 | - The chain rule for dependent events 307 | - How to extend these concepts to multiple events 308 | 309 | In the next lesson, we'll explore **law of total probability** in more detail, building on our understanding of various topics. 310 | """ 311 | ) 312 | return 313 | 314 | 315 | if __name__ == "__main__": 316 | app.run() 317 | -------------------------------------------------------------------------------- /probability/README.md: -------------------------------------------------------------------------------- 1 | # Learn probability 2 | 3 | This collection of marimo notebooks teaches the fundamentals of probability, 4 | with an emphasis on computation with Python. 5 | 6 | Much of the structure and many explanations here are adapted from Chris Piech's [Probability for Computer Scientists](https://chrispiech.github.io/probabilityForComputerScientists/en/index.html) course reader. 7 | 8 | 9 | **Running notebooks.** To run a notebook locally, use 10 | 11 | ```bash 12 | uvx marimo edit 13 | ``` 14 | 15 | For example, run the numbers tutorial with 16 | 17 | ```bash 18 | uvx marimo edit https://github.com/marimo-team/learn/blob/main/probability/01_sets.py 19 | ``` 20 | 21 | You can also open notebooks in our online playground by appending `marimo.app/` 22 | to a notebook's URL: [marimo.app/https://github.com/marimo-team/learn/blob/main/probability/01_sets.py](https://marimo.app/https://github.com/marimo-team/learn/blob/main/probability/01_sets.py). 23 | 24 | **Thanks to all our notebook authors!** 25 | 26 | * [Srihari Thyagarajan](https://github.com/Haleshot) 27 | -------------------------------------------------------------------------------- /python/001_numbers.py: -------------------------------------------------------------------------------- 1 | # /// script 2 | # requires-python = ">=3.10" 3 | # dependencies = [ 4 | # "marimo", 5 | # ] 6 | # /// 7 | 8 | import marimo 9 | 10 | __generated_with = "0.10.19" 11 | app = marimo.App() 12 | 13 | 14 | @app.cell(hide_code=True) 15 | def _(mo): 16 | mo.md( 17 | """ 18 | # 🔢 Numbers 19 | 20 | This tutorial provides a brief overview of working with numbers. 21 | 22 | ## Number Types 23 | 24 | Python has several types of numbers: 25 | 26 | ```python 27 | integer = 42 # whole numbers (int) 28 | decimal = 3.14 # floating-point numbers (float) 29 | complex_num = 2 + 3j # complex numbers 30 | ``` 31 | 32 | Below is an example number we'll use to explore operations. 33 | """ 34 | ) 35 | return 36 | 37 | 38 | @app.cell 39 | def _(): 40 | number = 42 41 | return (number,) 42 | 43 | 44 | @app.cell(hide_code=True) 45 | def _(mo): 46 | mo.md( 47 | """ 48 | ## Basic mathematical operations 49 | 50 | Python supports all standard mathematical operations. 51 | 52 | Try changing the value of `number` above and watch how the results change. 53 | """ 54 | ) 55 | return 56 | 57 | 58 | @app.cell 59 | def _(number): 60 | number + 10 # Addition 61 | return 62 | 63 | 64 | @app.cell 65 | def _(number): 66 | number - 5 # Subtraction 67 | return 68 | 69 | 70 | @app.cell 71 | def _(number): 72 | number * 3 # Multiplication 73 | return 74 | 75 | 76 | @app.cell 77 | def _(number): 78 | number / 2 # Division (always returns float) 79 | return 80 | 81 | 82 | @app.cell(hide_code=True) 83 | def _(mo): 84 | mo.md("""Python also has special division operators and power operations.""") 85 | return 86 | 87 | 88 | @app.cell 89 | def _(number): 90 | number // 5 # Floor division (rounds down) 91 | return 92 | 93 | 94 | @app.cell 95 | def _(number): 96 | number % 5 # Modulus (remainder) 97 | return 98 | 99 | 100 | @app.cell 101 | def _(number): 102 | number**2 # Exponentiation 103 | return 104 | 105 | 106 | @app.cell(hide_code=True) 107 | def _(mo): 108 | mo.md( 109 | """ 110 | ## Type conversion 111 | 112 | You can convert between different number types. Try changing these values! 113 | """ 114 | ) 115 | return 116 | 117 | 118 | @app.cell 119 | def _(): 120 | decimal_number = 3.14 121 | return (decimal_number,) 122 | 123 | 124 | @app.cell 125 | def _(decimal_number): 126 | int(decimal_number) # Convert to integer (truncates decimal part) 127 | return 128 | 129 | 130 | @app.cell 131 | def _(number): 132 | float(number) # Convert to "float" or decimal 133 | return 134 | 135 | 136 | @app.cell(hide_code=True) 137 | def _(mo): 138 | mo.md( 139 | """ 140 | ## Built-in math functions 141 | Python provides many useful built-in functions for working with numbers: 142 | """ 143 | ) 144 | return 145 | 146 | 147 | @app.cell 148 | def _(number): 149 | abs(-number) # Absolute value 150 | return 151 | 152 | 153 | @app.cell 154 | def _(): 155 | round(3.14159, 2) # Round to 2 decimal places 156 | return 157 | 158 | 159 | @app.cell 160 | def _(): 161 | max(1, 5, 3, 7, 2) # Find maximum value 162 | return 163 | 164 | 165 | @app.cell 166 | def _(): 167 | min(1, 5, 3, 7, 2) # Find minimum value 168 | return 169 | 170 | 171 | @app.cell(hide_code=True) 172 | def _(mo): 173 | mo.md( 174 | """ 175 | ## Advanced operations 176 | 177 | For more complex mathematical operations, use Python's [math module](https://docs.python.org/3/library/math.html). 178 | """ 179 | ) 180 | return 181 | 182 | 183 | @app.cell 184 | def _(): 185 | import math 186 | return (math,) 187 | 188 | 189 | @app.cell 190 | def _(math): 191 | math.sqrt(16) 192 | return 193 | 194 | 195 | @app.cell 196 | def _(math): 197 | math.sin(math.pi/2) 198 | return 199 | 200 | 201 | @app.cell 202 | def _(math): 203 | math.cos(0) 204 | return 205 | 206 | 207 | @app.cell 208 | def _(math): 209 | math.pi, math.e 210 | return 211 | 212 | 213 | @app.cell 214 | def _(math): 215 | math.log10(100) 216 | return 217 | 218 | 219 | @app.cell 220 | def _(math): 221 | math.log(math.e) 222 | return 223 | 224 | 225 | @app.cell(hide_code=True) 226 | def _(mo): 227 | mo.md(""" 228 | ## Next steps: 229 | 230 | - Practice different mathematical operations 231 | - Experiment with type conversions 232 | - Try out the math module functions 233 | 234 | Keep calculating! 🧮✨ 235 | """) 236 | return 237 | 238 | 239 | @app.cell 240 | def _(): 241 | import marimo as mo 242 | return (mo,) 243 | 244 | 245 | if __name__ == "__main__": 246 | app.run() 247 | -------------------------------------------------------------------------------- /python/002_strings.py: -------------------------------------------------------------------------------- 1 | # /// script 2 | # requires-python = ">=3.10" 3 | # dependencies = [ 4 | # "marimo", 5 | # ] 6 | # /// 7 | 8 | import marimo 9 | 10 | __generated_with = "0.10.19" 11 | app = marimo.App(width="medium") 12 | 13 | 14 | @app.cell(hide_code=True) 15 | def _(mo): 16 | mo.md( 17 | """ 18 | # 🎭 Strings 19 | 20 | This notebook introduces **strings**, which are containers for text. 21 | 22 | ## Creating strings 23 | Create strings by wrapping text in quotes: 24 | 25 | ```python 26 | # Use double quotes 27 | greeting = "Hello, Python!" 28 | 29 | # or single quotes 30 | name = 'Alice' 31 | 32 | # or triple quotes 33 | multiline_string = \""" 34 | Dear, Alice, 35 | Nice to meet you. 36 | Sincerely, 37 | Bob. 38 | \""" 39 | ``` 40 | 41 | Below is an example string. 42 | """ 43 | ) 44 | return 45 | 46 | 47 | @app.cell 48 | def _(): 49 | text = "Python is amazing!" 50 | text 51 | return (text,) 52 | 53 | 54 | @app.cell(hide_code=True) 55 | def _(mo): 56 | mo.md( 57 | """ 58 | ## Essential string operations 59 | 60 | Here are some methods for working with strings. 61 | 62 | Tip: Try changing the value of `text` above, and watch how the 63 | computed values below change. 64 | """ 65 | ) 66 | return 67 | 68 | 69 | @app.cell 70 | def _(text): 71 | # the `len` method returns the number of characters in the string. 72 | len(text) 73 | return 74 | 75 | 76 | @app.cell 77 | def _(text): 78 | text.upper() 79 | return 80 | 81 | 82 | @app.cell 83 | def _(text): 84 | text.lower() 85 | return 86 | 87 | 88 | @app.cell 89 | def _(text): 90 | text.title() 91 | return 92 | 93 | 94 | @app.cell(hide_code=True) 95 | def _(mo): 96 | mo.md("""Use string methods and the `in` operator to find things in strings.""") 97 | return 98 | 99 | 100 | @app.cell 101 | def _(text): 102 | # Returns the index of "is" in the string 103 | text.find("is") 104 | return 105 | 106 | 107 | @app.cell 108 | def _(text): 109 | "Python" in text 110 | return 111 | 112 | 113 | @app.cell 114 | def _(text): 115 | "Javascript" in text 116 | return 117 | 118 | 119 | @app.cell(hide_code=True) 120 | def _(mo): 121 | mo.md( 122 | """ 123 | ## Inserting values in strings 124 | 125 | Modern Python uses f-strings to insert values into strings. For example, 126 | check out how the next cell greets you (and notice the `f''''`)! 127 | 128 | **Try it!** Enter your name in `my_name` below, then run the cell. 129 | """ 130 | ) 131 | return 132 | 133 | 134 | @app.cell 135 | def _(): 136 | my_name = "" 137 | return (my_name,) 138 | 139 | 140 | @app.cell 141 | def _(my_name): 142 | f"Hello, {my_name}!" 143 | return 144 | 145 | 146 | @app.cell(hide_code=True) 147 | def _(mo): 148 | mo.md( 149 | """ 150 | ## Working with parts of strings 151 | You can access any part of a string using its position (index): 152 | """ 153 | ) 154 | return 155 | 156 | 157 | @app.cell 158 | def _(text): 159 | first_letter = text[0] 160 | first_letter 161 | return (first_letter,) 162 | 163 | 164 | @app.cell 165 | def _(text): 166 | last_letter = text[-1] 167 | last_letter 168 | return (last_letter,) 169 | 170 | 171 | @app.cell 172 | def _(text): 173 | first_three = text[0:3] 174 | first_three 175 | return (first_three,) 176 | 177 | 178 | @app.cell 179 | def _(text): 180 | last_two = text[-2:] 181 | last_two 182 | return (last_two,) 183 | 184 | 185 | @app.cell(hide_code=True) 186 | def _(mo): 187 | mo.md( 188 | """ 189 | ## Other helpful string methods 190 | 191 | Finally, here are some other helpful string methods. Feel free to try them out on your own strings by modifying the value of `sentence` below. 192 | """ 193 | ) 194 | return 195 | 196 | 197 | @app.cell 198 | def _(): 199 | sentence = " python is fun " 200 | sentence 201 | return (sentence,) 202 | 203 | 204 | @app.cell 205 | def _(sentence): 206 | # Remove extra spaces 207 | sentence.strip() 208 | return 209 | 210 | 211 | @app.cell 212 | def _(sentence): 213 | # Split into a list of words 214 | sentence.split() 215 | return 216 | 217 | 218 | @app.cell 219 | def _(sentence): 220 | sentence.replace("fun", "awesome") 221 | return 222 | 223 | 224 | @app.cell 225 | def _(): 226 | "123".isdigit(), "abc".isdigit() 227 | return 228 | 229 | 230 | @app.cell 231 | def _(): 232 | "123".isalpha(), "abc".isalpha() 233 | return 234 | 235 | 236 | @app.cell 237 | def _(): 238 | "Python3".isalnum() 239 | return 240 | 241 | 242 | @app.cell(hide_code=True) 243 | def _(mo): 244 | mo.md( 245 | r""" 246 | ## Next steps 247 | 248 | For a full primer on strings, check out the [official documentation](https://docs.python.org/3/library/string.html). 249 | """ 250 | ) 251 | return 252 | 253 | 254 | @app.cell 255 | def _(): 256 | import marimo as mo 257 | return (mo,) 258 | 259 | 260 | if __name__ == "__main__": 261 | app.run() 262 | -------------------------------------------------------------------------------- /python/003_collections.py: -------------------------------------------------------------------------------- 1 | # /// script 2 | # requires-python = ">=3.10" 3 | # dependencies = [ 4 | # "marimo", 5 | # ] 6 | # /// 7 | 8 | import marimo 9 | 10 | __generated_with = "0.10.19" 11 | app = marimo.App(width="medium") 12 | 13 | 14 | @app.cell(hide_code=True) 15 | def _(mo): 16 | mo.md( 17 | """ 18 | # 📦 Collections 19 | 20 | A "collection" is a type of variable that holds multiple values. 21 | 22 | ## Lists 23 | Lists are ordered, mutable sequences. Create them using square brackets: 24 | 25 | ```python 26 | fruits = ["apple", "banana", "orange"] 27 | numbers = [1, 2, 3, 4, 5] 28 | mixed = [1, "hello", 3.14, True] 29 | ``` 30 | 31 | Below is an example list we'll use to explore operations. 32 | """ 33 | ) 34 | return 35 | 36 | 37 | @app.cell 38 | def _(): 39 | sample_list = [1, 2, 3, 4, 5] 40 | return (sample_list,) 41 | 42 | 43 | @app.cell(hide_code=True) 44 | def _(mo): 45 | mo.md( 46 | """ 47 | ## List operations 48 | 49 | Here are common operations you can perform on lists. 50 | 51 | Try changing the values in `sample_list` above and watch the results change. 52 | """ 53 | ) 54 | return 55 | 56 | 57 | @app.cell 58 | def _(sample_list): 59 | len(sample_list) # List length 60 | return 61 | 62 | 63 | @app.cell 64 | def _(sample_list): 65 | extended_list = sample_list + [6] # Concatenate two lists 66 | extended_list 67 | return (extended_list,) 68 | 69 | 70 | @app.cell 71 | def _(extended_list): 72 | extended_list[0] # Access first element 73 | return 74 | 75 | 76 | @app.cell 77 | def _(extended_list): 78 | extended_list[-1] # Access last element 79 | return 80 | 81 | 82 | @app.cell(hide_code=True) 83 | def _(mo): 84 | mo.md( 85 | """ 86 | ## Tuples 87 | 88 | Tuples are immutable sequences. They're like lists that can't be changed after creation: 89 | """ 90 | ) 91 | return 92 | 93 | 94 | @app.cell 95 | def _(): 96 | coordinates = (10, 20) 97 | return (coordinates,) 98 | 99 | 100 | @app.cell 101 | def _(coordinates): 102 | x, y = coordinates # Tuple unpacking 103 | x 104 | return x, y 105 | 106 | 107 | @app.cell(hide_code=True) 108 | def _(mo): 109 | mo.md("""#### Tuple concatenation""") 110 | return 111 | 112 | 113 | @app.cell 114 | def _(): 115 | tuple1 = (1, 2, 3) 116 | tuple2 = (4, 5, 6) 117 | 118 | tuple3 = tuple1 + tuple2 119 | tuple3 120 | return tuple1, tuple2, tuple3 121 | 122 | 123 | @app.cell(hide_code=True) 124 | def _(mo): 125 | mo.md( 126 | """ 127 | ## Dictionaries 128 | 129 | Dictionaries store key-value pairs. They're perfect for mapping relationships: 130 | """ 131 | ) 132 | return 133 | 134 | 135 | @app.cell 136 | def _(): 137 | person = {"name": "John Doe", "age": 25, "city": "New York"} 138 | return (person,) 139 | 140 | 141 | @app.cell 142 | def _(person): 143 | person["name"] # Access value by key 144 | return 145 | 146 | 147 | @app.cell 148 | def _(person): 149 | person.keys() # Get all keys 150 | return 151 | 152 | 153 | @app.cell 154 | def _(person): 155 | person.values() # Get all values 156 | return 157 | 158 | 159 | @app.cell(hide_code=True) 160 | def _(mo): 161 | mo.md( 162 | """ 163 | ## Sets 164 | 165 | Sets are unordered collections of unique elements: 166 | """ 167 | ) 168 | return 169 | 170 | 171 | @app.cell 172 | def _(): 173 | numbers_set = {1, 2, 3, 3, 2, 1} # Duplicates are removed 174 | return (numbers_set,) 175 | 176 | 177 | @app.cell 178 | def _(numbers_set): 179 | numbers_set | {4} # Add a new element 180 | return 181 | 182 | 183 | @app.cell 184 | def _(): 185 | {1, 2, 3} & {3, 4, 5} # Find common elements 186 | return 187 | 188 | 189 | @app.cell(hide_code=True) 190 | def _(mo): 191 | mo.md( 192 | """ 193 | ## Collection methods and operations 194 | 195 | Here are some common operations across collections: 196 | 197 | ```python 198 | # Lists 199 | my_list = [1, 2, 3] 200 | my_list.insert(0, 0) # Insert at position 201 | my_list.remove(2) # Remove first occurrence 202 | my_list.sort() # Sort in place 203 | sorted_list = sorted(my_list) # Return new sorted list 204 | 205 | # Dictionaries 206 | my_dict = {"a": 1} 207 | my_dict.update({"b": 2}) # Add new key-value pairs 208 | my_dict.get("c", "Not found") # Safe access with default 209 | 210 | # Sets 211 | set_a = {1, 2, 3} 212 | set_b = {3, 4, 5} 213 | set_a.union(set_b) # Combine sets 214 | set_a.difference(set_b) # Elements in A but not in B 215 | ``` 216 | """ 217 | ) 218 | return 219 | 220 | 221 | @app.cell(hide_code=True) 222 | def _(mo): 223 | mo.md( 224 | r""" 225 | ## Documentation 226 | 227 | See the official [Python tutorial on data structures](https://docs.python.org/3/tutorial/datastructures.html) for more in-depth information. 228 | """ 229 | ) 230 | return 231 | 232 | 233 | @app.cell 234 | def _(): 235 | import marimo as mo 236 | return (mo,) 237 | 238 | 239 | if __name__ == "__main__": 240 | app.run() 241 | -------------------------------------------------------------------------------- /python/004_conditional_logic.py: -------------------------------------------------------------------------------- 1 | # /// script 2 | # requires-python = ">=3.10" 3 | # dependencies = [ 4 | # "marimo", 5 | # ] 6 | # /// 7 | 8 | import marimo 9 | 10 | __generated_with = "0.10.19" 11 | app = marimo.App() 12 | 13 | 14 | @app.cell(hide_code=True) 15 | def _(mo): 16 | mo.md( 17 | """ 18 | # 🔄 Conditional logic 19 | 20 | This tutorial teaches you how to how to make **decisions** in your code, using 21 | Python's conditional statements. 22 | 23 | ## If Statements 24 | The foundation of decision-making in Python: 25 | ```python 26 | if condition: 27 | # code to run if condition is True 28 | elif another_condition: 29 | # code to run if another_condition is True 30 | else: 31 | # code to run if no conditions are True 32 | ``` 33 | Let's explore with some examples: 34 | """ 35 | ) 36 | return 37 | 38 | 39 | @app.cell(hide_code=True) 40 | def _(mo): 41 | mo.md("""**Try it!** Try changing the value of `42` below, and see how the output changes.""") 42 | return 43 | 44 | 45 | @app.cell 46 | def _(): 47 | number = 42 48 | return (number,) 49 | 50 | 51 | @app.cell(hide_code=True) 52 | def _(mo): 53 | mo.md( 54 | r""" 55 | Compare numbers using operators like 56 | 57 | - `>` 58 | - `>=` 59 | - `<` 60 | - `<=` 61 | - `==` (note the two equal signs!) 62 | """ 63 | ) 64 | return 65 | 66 | 67 | @app.cell 68 | def _(mo, number): 69 | if number > 42: 70 | result = "Greater than 42" 71 | elif number == 42: 72 | result = "Equal to 42!" 73 | else: 74 | result = "Less than 42" 75 | mo.md(result) 76 | return (result,) 77 | 78 | 79 | @app.cell(hide_code=True) 80 | def _(mo): 81 | mo.md( 82 | r""" 83 | ### Interactive decision making 84 | **Try it!** Try changing the conditions below and see how the results change: 85 | """ 86 | ) 87 | return 88 | 89 | 90 | @app.cell(hide_code=True) 91 | def _(mo, threshold, value): 92 | mo.hstack([value, threshold], justify="start") 93 | return 94 | 95 | 96 | @app.cell(hide_code=True) 97 | def _(mo): 98 | value = mo.ui.number(value=25, start=0, stop=100, label="Enter a number") 99 | threshold = mo.ui.slider(value=50, start=0, stop=100, label="Set threshold") 100 | return threshold, value 101 | 102 | 103 | @app.cell(hide_code=True) 104 | def _(mo, threshold, value): 105 | if value.value > threshold.value: 106 | decision = f"{value.value} is greater than {threshold.value}" 107 | elif value.value == threshold.value: 108 | decision = f"{value.value} is equal to {threshold.value}" 109 | else: 110 | decision = f"{value.value} is less than {threshold.value}" 111 | 112 | mo.hstack( 113 | [ 114 | mo.md(f"**Decision**: {decision}"), 115 | mo.md( 116 | f"**Threshold cleared?**: {'✅' if value.value >= threshold.value else '❌'}" 117 | ), 118 | ], 119 | justify="space-around", 120 | ) 121 | return (decision,) 122 | 123 | 124 | @app.cell(hide_code=True) 125 | def _(mo): 126 | mo.md( 127 | r""" 128 | ## Boolean operations 129 | Python uses boolean operators to combine conditions: 130 | 131 | - `and`: Both conditions must be True 132 | 133 | - `or`: At least one condition must be True 134 | 135 | - `not`: Inverts the condition 136 | """ 137 | ) 138 | return 139 | 140 | 141 | @app.cell(hide_code=True) 142 | def _(mo): 143 | _text = mo.md(""" 144 | - Try different combinations of age and ID status 145 | - Notice how both conditions must be True to allow voting 146 | - Experiment with edge cases (exactly 18, no ID, etc.) 147 | """) 148 | mo.accordion({"💡 Experiment Tips": _text}) 149 | return 150 | 151 | 152 | @app.cell(hide_code=True) 153 | def _(age, has_id, mo): 154 | mo.hstack([age, has_id], justify="start") 155 | return 156 | 157 | 158 | @app.cell(hide_code=True) 159 | def _(mo): 160 | age = mo.ui.number(value=18, start=0, stop=120, label="Age") 161 | has_id = mo.ui.switch(value=True, label="Has ID") 162 | return age, has_id 163 | 164 | 165 | @app.cell(hide_code=True) 166 | def _(age, has_id, mo): 167 | can_vote = age.value >= 18 and has_id.value 168 | 169 | explanation = f""" 170 | ### Voting eligibility check 171 | 172 | Current Status: 173 | 174 | - Age: {age.value} years old 175 | 176 | - Has ID: {"Yes" if has_id.value else "No"} 177 | 178 | - Can Vote: {"Yes ✅" if can_vote else "No ❌"} 179 | 180 | Reason: { 181 | "Both age and ID requirements met" 182 | if can_vote 183 | else "Missing " + ("required age" if age.value < 18 else "valid ID") 184 | } 185 | """ 186 | 187 | mo.md(explanation) 188 | return can_vote, explanation 189 | 190 | 191 | @app.cell(hide_code=True) 192 | def _(mo): 193 | mo.md(r"""**Try it!** Write Python code that computes whether an individual can vote.""") 194 | return 195 | 196 | 197 | @app.cell 198 | def _(): 199 | my_age = 18 200 | return (my_age,) 201 | 202 | 203 | @app.cell 204 | def _(): 205 | has_an_id = False 206 | return (has_an_id,) 207 | 208 | 209 | @app.cell(hide_code=True) 210 | def _(mo): 211 | mo.md( 212 | """ 213 | ## Complex conditions 214 | Combine multiple conditions for more sophisticated logic: 215 | ```python 216 | # Multiple conditions 217 | if (age >= 18 and has_id) or has_special_permission: 218 | print("Access granted") 219 | 220 | # Nested conditions 221 | if age >= 18: 222 | if has_id: 223 | print("Full access") 224 | else: 225 | print("Limited access") 226 | ``` 227 | """ 228 | ) 229 | return 230 | 231 | 232 | @app.cell(hide_code=True) 233 | def _(humidity, mo, temp, wind): 234 | mo.hstack([temp, humidity, wind]) 235 | return 236 | 237 | 238 | @app.cell(hide_code=True) 239 | def _(mo): 240 | temp = mo.ui.number(value=25, start=-20, stop=50, label="Temperature (°C)") 241 | humidity = mo.ui.slider(value=60, start=0, stop=100, label="Humidity (%)") 242 | wind = mo.ui.number(value=10, start=0, stop=100, label="Wind Speed (km/h)") 243 | return humidity, temp, wind 244 | 245 | 246 | @app.cell(hide_code=True) 247 | def _(humidity, mo, temp, wind): 248 | def get_weather_advice(): 249 | conditions = [] 250 | 251 | if temp.value > 30: 252 | conditions.append("🌡️ High temperature") 253 | elif temp.value < 10: 254 | conditions.append("❄️ Cold temperature") 255 | 256 | if humidity.value > 80: 257 | conditions.append("💧 High humidity") 258 | elif humidity.value < 30: 259 | conditions.append("🏜️ Low humidity") 260 | 261 | if wind.value > 30: 262 | conditions.append("💨 Strong winds") 263 | 264 | return conditions 265 | 266 | 267 | conditions = get_weather_advice() 268 | 269 | message = f""" 270 | ### Weather analysis 271 | 272 | Current Conditions: 273 | 274 | - Temperature: {temp.value}°C 275 | 276 | - Humidity: {humidity.value}% 277 | 278 | - Wind Speed: {wind.value} km/h 279 | 280 | Alerts: {", ".join(conditions) if conditions else "No special alerts"} 281 | """ 282 | 283 | mo.md(message) 284 | return conditions, get_weather_advice, message 285 | 286 | 287 | @app.cell(hide_code=True) 288 | def _(mo): 289 | mo.md(""" 290 | ## Next steps 291 | 292 | - Practice combining multiple conditions 293 | - Explore nested if statements 294 | - Try creating your own complex decision trees 295 | 296 | Keep coding! 🎯✨ 297 | """) 298 | return 299 | 300 | 301 | @app.cell 302 | def _(): 303 | import marimo as mo 304 | return (mo,) 305 | 306 | 307 | if __name__ == "__main__": 308 | app.run() 309 | -------------------------------------------------------------------------------- /python/005_loops.py: -------------------------------------------------------------------------------- 1 | # /// script 2 | # requires-python = ">=3.10" 3 | # dependencies = [ 4 | # "marimo", 5 | # ] 6 | # /// 7 | 8 | import marimo 9 | 10 | __generated_with = "0.10.19" 11 | app = marimo.App() 12 | 13 | 14 | @app.cell(hide_code=True) 15 | def _(mo): 16 | mo.md( 17 | """ 18 | # 🔄 Loops 19 | 20 | Let's learn how Python helps us repeat tasks efficiently with loops. 21 | 22 | A "loop" is a way to execute a block of code multiple times. Python has two 23 | main types of loops: 24 | 25 | ```python 26 | # For loop: when you know how many times to repeat 27 | for i in range(5): 28 | print(i) 29 | 30 | # While loop: when you don't know how many repetitions 31 | while condition: 32 | do_something() 33 | ``` 34 | 35 | Let's start with a simple list to explore loops. Feel free to modify this list and see how the subsequent outputs change. 36 | """ 37 | ) 38 | return 39 | 40 | 41 | @app.cell 42 | def _(): 43 | sample_fruits = ["apple", "banana", "orange", "grape"] 44 | return (sample_fruits,) 45 | 46 | 47 | @app.cell(hide_code=True) 48 | def _(mo): 49 | mo.md( 50 | """ 51 | ## The for loop 52 | 53 | The for loop is perfect for iterating over sequences. 54 | Try changing the `sample_fruits` list above and see how the output changes. 55 | """ 56 | ) 57 | return 58 | 59 | 60 | @app.cell 61 | def _(sample_fruits): 62 | for _fruit in sample_fruits: 63 | print(f"I like {_fruit}s!") 64 | return 65 | 66 | 67 | @app.cell(hide_code=True) 68 | def _(mo): 69 | mo.md( 70 | """ 71 | ### Getting the position of an item 72 | 73 | When you need both the item and its position, use `enumerate()`: 74 | """ 75 | ) 76 | return 77 | 78 | 79 | @app.cell 80 | def _(sample_fruits): 81 | for _idx, _fruit in enumerate(sample_fruits): 82 | print(f"{_idx + 1}. {_fruit}") 83 | return 84 | 85 | 86 | @app.cell(hide_code=True) 87 | def _(mo): 88 | mo.md( 89 | """ 90 | ### Iterating over a range of numbers 91 | 92 | `range()` is a powerful function for generating sequences of numbers: 93 | """ 94 | ) 95 | return 96 | 97 | 98 | @app.cell 99 | def _(): 100 | print("range(5):", list(range(5))) 101 | print("range(2, 5):", list(range(2, 5))) 102 | print("range(0, 10, 2):", list(range(0, 10, 2))) 103 | return 104 | 105 | 106 | @app.cell 107 | def _(): 108 | for _i in range(5): 109 | print(_i) 110 | return 111 | 112 | 113 | @app.cell(hide_code=True) 114 | def _(mo): 115 | mo.md( 116 | """ 117 | ## The `while` loop 118 | 119 | While loops continue as long as a condition is `True`. 120 | """ 121 | ) 122 | return 123 | 124 | 125 | @app.cell 126 | def _(): 127 | _count = 0 128 | while _count < 5: 129 | print(f"The count is {_count}") 130 | _count += 1 131 | return 132 | 133 | 134 | @app.cell(hide_code=True) 135 | def _(mo): 136 | mo.md( 137 | """ 138 | ## Controlling loop execution 139 | 140 | Python provides several ways to control loop execution: 141 | 142 | - `break`: exit the loop immediately 143 | 144 | - `continue`: skip to the next iteration 145 | 146 | These can be used with both `for` and `while` loops. 147 | """ 148 | ) 149 | return 150 | 151 | 152 | @app.cell 153 | def _(): 154 | for _i in range(1, 6): 155 | if _i == 4: 156 | print("Breaking out of the loop.") 157 | break 158 | print(_i) 159 | return 160 | 161 | 162 | @app.cell 163 | def _(): 164 | for _i in range(1, 6): 165 | if _i == 3: 166 | continue 167 | print(_i) 168 | return 169 | 170 | 171 | @app.cell(hide_code=True) 172 | def _(mo): 173 | mo.md( 174 | """ 175 | ## Practical loop patterns 176 | 177 | Here are some common patterns you'll use with loops: 178 | 179 | ```python 180 | # Pattern 1: Accumulator 181 | value = 0 182 | for num in [1, 2, 3, 4, 5]: 183 | value += num 184 | 185 | # Pattern 2: Search 186 | found = False 187 | for item in items: 188 | if condition: 189 | found = True 190 | break 191 | 192 | # Pattern 3: Filter 193 | filtered = [] 194 | for item in items: 195 | if condition: 196 | filtered.append(item) 197 | ``` 198 | """ 199 | ) 200 | return 201 | 202 | 203 | @app.cell(hide_code=True) 204 | def _(mo): 205 | mo.md( 206 | r""" 207 | ## Next steps 208 | 209 | Check out the official [Python docs on loops and control flow](https://docs.python.org/3/tutorial/controlflow.html). 210 | """ 211 | ) 212 | return 213 | 214 | 215 | @app.cell 216 | def _(): 217 | import marimo as mo 218 | return (mo,) 219 | 220 | 221 | if __name__ == "__main__": 222 | app.run() 223 | -------------------------------------------------------------------------------- /python/006_dictionaries.py: -------------------------------------------------------------------------------- 1 | # /// script 2 | # requires-python = ">=3.10" 3 | # dependencies = [ 4 | # "marimo", 5 | # ] 6 | # /// 7 | 8 | import marimo 9 | 10 | __generated_with = "0.10.19" 11 | app = marimo.App() 12 | 13 | 14 | @app.cell(hide_code=True) 15 | def _(mo): 16 | mo.md( 17 | """ 18 | # 📚 Dictionaries 19 | 20 | Dictionaries are collections of key-value pairs, with each key associated with a value. The keys are unique, meaning they show up only once. 21 | 22 | ## Creating dictionaries 23 | Here are a few ways to create dictionaries: 24 | 25 | ```python 26 | simple_dict = {"name": "Alice", "age": 25} 27 | empty_dict = dict() 28 | from_pairs = dict([("a", 1), ("b", 2)]) 29 | ``` 30 | 31 | Below is a sample dictionary we'll use to explore operations. 32 | """ 33 | ) 34 | return 35 | 36 | 37 | @app.cell 38 | def _(): 39 | sample_dict = { 40 | "name": "Python", 41 | "type": "programming language", 42 | "year": 1991, 43 | "creator": "Guido van Rossum", 44 | "is_awesome": True, 45 | } 46 | return (sample_dict,) 47 | 48 | 49 | @app.cell(hide_code=True) 50 | def _(mo): 51 | mo.md( 52 | """ 53 | ## Operations 54 | 55 | Let's explore how to work with dictionaries. 56 | 57 | **Try it!** Try modifying the `sample_dict` above and watch how the results change! 58 | """ 59 | ) 60 | return 61 | 62 | 63 | @app.cell(hide_code=True) 64 | def _(mo): 65 | mo.md( 66 | r""" 67 | ### Accessing values by key 68 | 69 | Access values by key using square brackets, like below 70 | """ 71 | ) 72 | return 73 | 74 | 75 | @app.cell 76 | def _(sample_dict): 77 | sample_dict['name'], sample_dict['year'] 78 | return 79 | 80 | 81 | @app.cell(hide_code=True) 82 | def _(mo): 83 | mo.md(r"""If you're not sure if a dictionary has a given key, use `get()`:""") 84 | return 85 | 86 | 87 | @app.cell 88 | def _(sample_dict): 89 | sample_dict.get("version", "Not specified"), sample_dict.get("type", "Unknown") 90 | return 91 | 92 | 93 | @app.cell(hide_code=True) 94 | def _(mo): 95 | mo.md( 96 | """ 97 | ## Enumerating dictionary contents 98 | 99 | Python dictionaries come with helpful methods to enumerate keys, values, and pairs. 100 | """ 101 | ) 102 | return 103 | 104 | 105 | @app.cell 106 | def _(sample_dict): 107 | print(list(sample_dict.keys())) 108 | return 109 | 110 | 111 | @app.cell 112 | def _(sample_dict): 113 | print(list(sample_dict.values())) 114 | return 115 | 116 | 117 | @app.cell 118 | def _(sample_dict): 119 | print(list(sample_dict.items())) 120 | return 121 | 122 | 123 | @app.cell 124 | def _(): 125 | def demonstrate_modification(): 126 | _dict = {"a": 1, "b": 2} 127 | print("Original:", _dict) 128 | 129 | # Adding/updating 130 | _dict.update({"c": 3, "b": 22}) 131 | print("After update:", _dict) 132 | 133 | # Removing 134 | _removed = _dict.pop("b") 135 | print(f"Removed {_removed}, Now:", _dict) 136 | 137 | 138 | demonstrate_modification() 139 | return (demonstrate_modification,) 140 | 141 | 142 | @app.cell(hide_code=True) 143 | def _(mo): 144 | mo.md( 145 | """ 146 | ## Dictionary comprehension 147 | 148 | Create dictionaries efficiently with dictionary comprehensions: 149 | """ 150 | ) 151 | return 152 | 153 | 154 | @app.cell 155 | def _(): 156 | print({x: x**2 for x in range(5)}) 157 | return 158 | 159 | 160 | @app.cell 161 | def _(): 162 | print({x: x**2 for x in range(5) if x % 2 == 0}) 163 | return 164 | 165 | 166 | @app.cell(hide_code=True) 167 | def _(mo): 168 | mo.md( 169 | """ 170 | ## Nested dictionaries 171 | 172 | Dictionaries can contain other dictionaries, creating complex data structures: 173 | """ 174 | ) 175 | return 176 | 177 | 178 | @app.cell 179 | def _(): 180 | nested_data = { 181 | "users": { 182 | "alice": { 183 | "age": 25, 184 | "email": "alice@example.com", 185 | "interests": ["python", "data science"], 186 | }, 187 | "bob": { 188 | "age": 30, 189 | "email": "bob@example.com", 190 | "interests": ["web dev", "gaming"], 191 | }, 192 | } 193 | } 194 | return (nested_data,) 195 | 196 | 197 | @app.cell 198 | def _(mo, nested_data): 199 | mo.md(f"Alice's age: {nested_data['users']['alice']['age']}") 200 | return 201 | 202 | 203 | @app.cell 204 | def _(mo, nested_data): 205 | mo.md(f"Bob's interests: {nested_data['users']['bob']['interests']}") 206 | return 207 | 208 | 209 | @app.cell(hide_code=True) 210 | def _(mo): 211 | mo.md( 212 | """ 213 | ## Common dictionary patterns 214 | 215 | Here are some useful patterns when working with dictionaries: 216 | 217 | ```python 218 | # Pattern 1: Counting items 219 | counter = {} 220 | for item in items: 221 | counter[item] = counter.get(item, 0) + 1 222 | 223 | # Pattern 2: Grouping data 224 | groups = {} 225 | for item in _items: 226 | key = get_group_key(item) 227 | groups.setdefault(key, []).append(item) 228 | 229 | # Pattern 3: Caching/Memoization 230 | cache = {} 231 | def expensive_function(arg): 232 | if arg not in cache: 233 | cache[arg] = compute_result(arg) 234 | return cache[arg] 235 | ``` 236 | """ 237 | ) 238 | return 239 | 240 | 241 | @app.cell 242 | def _(): 243 | import marimo as mo 244 | return (mo,) 245 | 246 | 247 | if __name__ == "__main__": 248 | app.run() 249 | -------------------------------------------------------------------------------- /python/007_advanced_collections.py: -------------------------------------------------------------------------------- 1 | # /// script 2 | # requires-python = ">=3.10" 3 | # dependencies = [ 4 | # "marimo", 5 | # ] 6 | # /// 7 | 8 | import marimo 9 | 10 | __generated_with = "0.10.19" 11 | app = marimo.App() 12 | 13 | 14 | @app.cell(hide_code=True) 15 | def _(mo): 16 | mo.md( 17 | """ 18 | # 🔄 Advanced collections 19 | 20 | This tutorials hows advanced patterns for working with collections. 21 | 22 | ## Lists of dictionaries 23 | 24 | A common pattern in data handling is working with lists of dictionaries: 25 | this is helpful for representing structured data like records or entries. 26 | """ 27 | ) 28 | return 29 | 30 | 31 | @app.cell 32 | def _(): 33 | # Sample data: List of user records 34 | users_data = [ 35 | {"id": 1, "name": "Alice", "skills": ["Python", "SQL"]}, 36 | {"id": 2, "name": "Bob", "skills": ["JavaScript", "HTML"]}, 37 | {"id": 3, "name": "Charlie", "skills": ["Python", "Java"]} 38 | ] 39 | return (users_data,) 40 | 41 | 42 | @app.cell(hide_code=True) 43 | def _(mo): 44 | mo.md( 45 | """ 46 | Let's explore common operations on structured data. 47 | 48 | **Try it!** Try modifying the `users_data` above and see how the results 49 | change! 50 | """ 51 | ) 52 | return 53 | 54 | 55 | @app.cell 56 | def _(users_data): 57 | # Finding users with specific skills 58 | python_users = [ 59 | user["name"] for user in users_data if "Python" in user["skills"] 60 | ] 61 | print("Python developers:", python_users) 62 | return (python_users,) 63 | 64 | 65 | @app.cell(hide_code=True) 66 | def _(mo): 67 | mo.md( 68 | """ 69 | ## Nested data structures 70 | 71 | Python collections can be nested in various ways to represent complex data: 72 | """ 73 | ) 74 | return 75 | 76 | 77 | @app.cell 78 | def _(): 79 | # Complex nested structure 80 | project_data = { 81 | "web_app": { 82 | "frontend": ["HTML", "CSS", "React"], 83 | "backend": { 84 | "languages": ["Python", "Node.js"], 85 | "databases": ["MongoDB", "PostgreSQL"] 86 | } 87 | }, 88 | "mobile_app": { 89 | "platforms": ["iOS", "Android"], 90 | "technologies": { 91 | "iOS": ["Swift", "SwiftUI"], 92 | "Android": ["Kotlin", "Jetpack Compose"] 93 | } 94 | } 95 | } 96 | return (project_data,) 97 | 98 | 99 | @app.cell 100 | def _(project_data): 101 | # Nested data accessing 102 | backend_langs = project_data["web_app"]["backend"]["languages"] 103 | print("Backend languages:", backend_langs) 104 | 105 | ios_tech = project_data["mobile_app"]["technologies"]["iOS"] 106 | print("iOS technologies:", ios_tech) 107 | return backend_langs, ios_tech 108 | 109 | 110 | @app.cell(hide_code=True) 111 | def _(mo): 112 | mo.md( 113 | """ 114 | ### Example: data transformation 115 | 116 | Let's explore how to transform and reshape collection data: 117 | """ 118 | ) 119 | return 120 | 121 | 122 | @app.cell 123 | def _(): 124 | # Data-sample for transformation 125 | sales_data = [ 126 | {"date": "2024-01", "product": "A", "units": 100}, 127 | {"date": "2024-01", "product": "B", "units": 150}, 128 | {"date": "2024-02", "product": "A", "units": 120}, 129 | {"date": "2024-02", "product": "B", "units": 130} 130 | ] 131 | return (sales_data,) 132 | 133 | 134 | @app.cell 135 | def _(sales_data): 136 | # Transform to product-based structure 137 | product_sales = {} 138 | for sale in sales_data: 139 | if sale["product"] not in product_sales: 140 | product_sales[sale["product"]] = [] 141 | product_sales[sale["product"]].append({ 142 | "date": sale["date"], 143 | "units": sale["units"] 144 | }) 145 | 146 | print("Sales by product:", product_sales) 147 | return product_sales, sale 148 | 149 | 150 | @app.cell(hide_code=True) 151 | def _(mo): 152 | mo.md( 153 | """ 154 | ## More collection utilities 155 | 156 | Python's `collections` module provides specialized container datatypes: 157 | 158 | ```python 159 | from collections import defaultdict, Counter, deque 160 | 161 | # defaultdict - dictionary with default factory 162 | word_count = defaultdict(int) 163 | for word in words: 164 | word_count[word] += 1 165 | 166 | # Counter - count hashable objects 167 | colors = Counter(['red', 'blue', 'red', 'green', 'blue', 'blue']) 168 | print(colors.most_common(2)) # Top 2 most common colors 169 | 170 | # deque - double-ended queue 171 | history = deque(maxlen=10) # Only keeps last 10 items 172 | history.append(item) 173 | ``` 174 | """ 175 | ) 176 | return 177 | 178 | 179 | @app.cell 180 | def _(): 181 | from collections import Counter 182 | 183 | # Example using Counter 184 | programming_languages = [ 185 | "Python", "JavaScript", "Python", "Java", 186 | "Python", "JavaScript", "C++", "Java" 187 | ] 188 | 189 | language_count = Counter(programming_languages) 190 | print("Language frequency:", dict(language_count)) 191 | print("Most common language:", language_count.most_common(1)) 192 | return Counter, language_count, programming_languages 193 | 194 | 195 | @app.cell(hide_code=True) 196 | def _(mo): 197 | mo.md( 198 | """ 199 | ## Next steps 200 | 201 | For a reference on the `collections` module, see [the official Python 202 | docs](https://docs.python.org/3/library/collections.html). 203 | """ 204 | ) 205 | return 206 | 207 | 208 | @app.cell 209 | def _(): 210 | import marimo as mo 211 | return (mo,) 212 | 213 | 214 | if __name__ == "__main__": 215 | app.run() 216 | -------------------------------------------------------------------------------- /python/008_functions.py: -------------------------------------------------------------------------------- 1 | # /// script 2 | # requires-python = ">=3.10" 3 | # dependencies = [ 4 | # "marimo", 5 | # ] 6 | # /// 7 | 8 | import marimo 9 | 10 | __generated_with = "0.10.19" 11 | app = marimo.App() 12 | 13 | 14 | @app.cell(hide_code=True) 15 | def _(mo): 16 | mo.md( 17 | """ 18 | # 🧩 Functions 19 | 20 | This tutorial is about an important topic: **functions.** 21 | 22 | A function is a reusable block of code, similar in spirit to a mathematical function. Each function has a **name**, and accepts some number of **arguments**. These arguments are used in the function "body" (its block of code), and each function can **return** values. 23 | 24 | **Example.** Below is an example function. 25 | """ 26 | ) 27 | return 28 | 29 | 30 | @app.cell 31 | def _(): 32 | def greet(your_name): 33 | return f"Hello, {your_name}!" 34 | return (greet,) 35 | 36 | 37 | @app.cell(hide_code=True) 38 | def _(mo): 39 | mo.md(r"""The keyword `def` starts the function definition. The function's **name** is `greet`. It accepts one **argument** called `your_name`. It then creates a string and **returns** it.""") 40 | return 41 | 42 | 43 | @app.cell(hide_code=True) 44 | def _(mo): 45 | mo.md( 46 | """ 47 | In the next cell, we **call** the function with a value and assign its return value to a variable. 48 | 49 | **Try it!** Try changing the input to the function. 50 | """ 51 | ) 52 | return 53 | 54 | 55 | @app.cell 56 | def _(greet): 57 | greeting = greet(your_name="") 58 | greeting 59 | return (greeting,) 60 | 61 | 62 | @app.cell(hide_code=True) 63 | def _(mo): 64 | mo.md( 65 | """ 66 | **Why use functions?** Functions help you: 67 | 68 | - Break down complex problems 69 | - Create reusable code blocks 70 | - Improve code readability 71 | """ 72 | ) 73 | return 74 | 75 | 76 | @app.cell(hide_code=True) 77 | def _(mo): 78 | mo.md( 79 | """ 80 | ## Default parameters 81 | Make your functions more flexible by providing default values. 82 | """ 83 | ) 84 | return 85 | 86 | 87 | @app.cell 88 | def _(): 89 | def create_profile(name, age=18): 90 | return f"{name} is {age} years old" 91 | return (create_profile,) 92 | 93 | 94 | @app.cell 95 | def _(create_profile): 96 | # Example usage 97 | example_name = "Alex" 98 | example_profile = create_profile(example_name) 99 | example_profile 100 | return example_name, example_profile 101 | 102 | 103 | @app.cell(hide_code=True) 104 | def _(mo): 105 | mo.md("""You can also create functions that reference variables outside the function body. This is called 'closing over' variables""") 106 | return 107 | 108 | 109 | @app.cell 110 | def _(): 111 | base_multiplier = 2 112 | 113 | def multiplier(x): 114 | """ 115 | Create a function that multiplies input by a base value. 116 | 117 | This demonstrates how functions can 'close over' 118 | values from their surrounding scope. 119 | """ 120 | return x * base_multiplier 121 | return base_multiplier, multiplier 122 | 123 | 124 | @app.cell 125 | def _(multiplier): 126 | print([multiplier(num) for num in [1, 2, 3]]) 127 | return 128 | 129 | 130 | @app.cell(hide_code=True) 131 | def _(mo): 132 | mo.md( 133 | """ 134 | ## Returning multiple values 135 | 136 | Functions can return multiple values: just separate the values to return by 137 | commas. Check out the next cell for an example. 138 | """ 139 | ) 140 | return 141 | 142 | 143 | @app.cell 144 | def _(): 145 | def weather_analysis(temp): 146 | """ 147 | Analyze weather based on temperature. 148 | 149 | Args: 150 | temp (float): Temperature in Celsius 151 | 152 | Returns: 153 | tuple: Weather status, recommendation, warning level 154 | """ 155 | if temp <= 0: 156 | return "Freezing", "Wear heavy coat", "High" 157 | elif 0 < temp <= 15: 158 | return "Cold", "Layer up", "Medium" 159 | elif 15 < temp <= 25: 160 | return "Mild", "Comfortable clothing", "Low" 161 | else: 162 | return "Hot", "Stay hydrated", "High" 163 | return (weather_analysis,) 164 | 165 | 166 | @app.cell 167 | def _(): 168 | temperature = 25 169 | return (temperature,) 170 | 171 | 172 | @app.cell 173 | def _(temperature, weather_analysis): 174 | status, recommendation, warning_level = weather_analysis(temperature) 175 | status, recommendation, warning_level 176 | return recommendation, status, warning_level 177 | 178 | 179 | @app.cell 180 | def _(): 181 | import marimo as mo 182 | return (mo,) 183 | 184 | 185 | if __name__ == "__main__": 186 | app.run() 187 | -------------------------------------------------------------------------------- /python/009_modules.py: -------------------------------------------------------------------------------- 1 | # /// script 2 | # requires-python = ">=3.10" 3 | # dependencies = [ 4 | # "marimo", 5 | # ] 6 | # /// 7 | 8 | import marimo 9 | 10 | __generated_with = "0.10.19" 11 | app = marimo.App() 12 | 13 | 14 | @app.cell(hide_code=True) 15 | def _(mo): 16 | mo.md( 17 | """ 18 | # 🧩 Using modules 19 | 20 | A `module` in Python is a Python file that defines functions and variables. Modules can be `imported` into other Python files, letting you reuse their 21 | functions and variables. 22 | 23 | We have already seen some modules in previous tutorials, including the `math` 24 | module. Python comes with many other modules built-in. 25 | """ 26 | ) 27 | return 28 | 29 | 30 | @app.cell(hide_code=True) 31 | def _(mo): 32 | mo.md( 33 | """ 34 | ## The Python standard library 35 | 36 | Python's "standard library" provides many modules, for many kinds of tasks. 37 | 38 | ```python 39 | # String manipulation 40 | import string 41 | 42 | # Operating system interactions 43 | import os 44 | 45 | # Date and time handling 46 | import datetime 47 | 48 | # Mathematical operations 49 | import math 50 | ``` 51 | 52 | See the [Python standard library documentation](https://docs.python.org/3/library/) for a full reference 53 | """ 54 | ) 55 | return 56 | 57 | 58 | @app.cell(hide_code=True) 59 | def _(mo): 60 | mo.md("""### Example""") 61 | return 62 | 63 | 64 | @app.cell 65 | def _(): 66 | import string 67 | import os 68 | import datetime 69 | import math 70 | 71 | # Example of using imported modules 72 | def demonstrate_standard_library_usage(): 73 | # String module: get all punctuation 74 | punctuation_example = string.punctuation 75 | 76 | # OS module: get current working directory 77 | current_dir = os.getcwd() 78 | 79 | # Datetime module: get current date 80 | today = datetime.date.today() 81 | 82 | # Math module: calculate square root 83 | sqrt_example = math.sqrt(16) 84 | 85 | return { 86 | "Punctuation": punctuation_example, 87 | "Current Directory": current_dir, 88 | "Today's Date": today, 89 | "Square Root Example": sqrt_example 90 | } 91 | 92 | # Run the demonstration 93 | module_usage_examples = demonstrate_standard_library_usage() 94 | module_usage_examples 95 | return ( 96 | datetime, 97 | demonstrate_standard_library_usage, 98 | math, 99 | module_usage_examples, 100 | os, 101 | string, 102 | ) 103 | 104 | 105 | @app.cell(hide_code=True) 106 | def _(mo): 107 | mo.md( 108 | """ 109 | ## Import syntax 110 | 111 | You can import entire modules, and access their functions and variables using dot notation (`math.sqrt`). Or you can import specific members: 112 | 113 | ```python 114 | # Import entire module 115 | import math 116 | 117 | # Import specific functions 118 | from math import sqrt, pow 119 | 120 | # Import with alias 121 | import math as m 122 | ``` 123 | """ 124 | ) 125 | return 126 | 127 | 128 | @app.cell 129 | def _(): 130 | def demonstrate_import_strategies(): 131 | """ 132 | Demonstrate different import strategies using the math module 133 | """ 134 | # Strategy 1: Import entire module 135 | import math 136 | entire_module_result = math.sqrt(25) 137 | 138 | # Strategy 2: Import specific functions 139 | from math import pow, sqrt 140 | specific_import_result = pow(2, 3) 141 | 142 | # Strategy 3: Import with alias 143 | import math as m 144 | alias_result = m.sqrt(16) 145 | 146 | return { 147 | "Entire Module Import": entire_module_result, 148 | "Specific Function Import": specific_import_result, 149 | "Alias Import": alias_result 150 | } 151 | 152 | # Run the import strategy demonstration 153 | import_strategy_examples = demonstrate_import_strategies() 154 | import_strategy_examples 155 | return demonstrate_import_strategies, import_strategy_examples 156 | 157 | 158 | @app.cell(hide_code=True) 159 | def _(mo): 160 | mo.md( 161 | """ 162 | ## Third-party packages 163 | 164 | In addition to Python's standard library, there are hundreds of thousands of 165 | modules available for free on the Python Package index. 166 | 167 | These are distributed as Python "packages", and include packages for 168 | manipulating arrays of numbers, creating web applications, and more. `marimo` 169 | itself is a third-party package! 170 | 171 | For installing packages on your machine, we recommend using the [`uv` package manager](https://docs.astral.sh/uv/). 172 | """ 173 | ) 174 | return 175 | 176 | 177 | @app.cell 178 | def _(): 179 | import marimo as mo 180 | return (mo,) 181 | 182 | 183 | if __name__ == "__main__": 184 | app.run() 185 | -------------------------------------------------------------------------------- /python/010_exceptions.py: -------------------------------------------------------------------------------- 1 | # /// script 2 | # requires-python = ">=3.10" 3 | # dependencies = [ 4 | # "marimo", 5 | # ] 6 | # /// 7 | 8 | import marimo 9 | 10 | __generated_with = "0.10.19" 11 | app = marimo.App() 12 | 13 | 14 | @app.cell(hide_code=True) 15 | def _(mo): 16 | mo.md( 17 | """ 18 | # 🛡️ Handling errors 19 | 20 | Sometimes things go wrong in programs. When that happens, Python raises `exceptions` to tell you what went amiss. For example, maybe you divided by 0: 21 | """ 22 | ) 23 | return 24 | 25 | 26 | @app.cell 27 | def _(): 28 | 1 / 0 29 | return 30 | 31 | 32 | @app.cell(hide_code=True) 33 | def _(mo): 34 | mo.md( 35 | """ 36 | That's a lot of red! The outputs above are Python telling you that 37 | something went wrong — in this case, we tried dividing a number by 0. 38 | 39 | Python provides tools to catch and handle exceptions: the `try/except` 40 | block. This is demonstrated in the next couple cells. 41 | """ 42 | ) 43 | return 44 | 45 | 46 | @app.cell 47 | def _(): 48 | # Try changing the value of divisor below, and see how the output changes. 49 | divisor = 0 50 | return (divisor,) 51 | 52 | 53 | @app.cell 54 | def _(divisor): 55 | try: 56 | print(1 / divisor) 57 | except ZeroDivisionError as e: 58 | print("Something went wrong!", e) 59 | return 60 | 61 | 62 | @app.cell(hide_code=True) 63 | def _(mo): 64 | mo.md( 65 | """ 66 | Python has many types of Exceptions besides `ZeroDivisionError`. If you 67 | don't know what kind of exception you're handling, catch the generic 68 | `Exception` type: 69 | 70 | ```python 71 | try: 72 | ... 73 | except Exception: 74 | ... 75 | ``` 76 | """ 77 | ) 78 | return 79 | 80 | 81 | @app.cell(hide_code=True) 82 | def _(error_types): 83 | error_types 84 | return 85 | 86 | 87 | @app.cell(hide_code=True) 88 | def _(mo): 89 | # Choose error type 90 | error_types = mo.ui.dropdown( 91 | value="ZeroDivisionError", 92 | options=[ 93 | "ZeroDivisionError", 94 | "TypeError", 95 | "ValueError", 96 | "IndexError", 97 | "KeyError" 98 | ], 99 | label="Learn about ..." 100 | ) 101 | return (error_types,) 102 | 103 | 104 | @app.cell(hide_code=True) 105 | def _(error_types, mo): 106 | # Error explanation 107 | error_explanations = { 108 | "ZeroDivisionError": """ 109 | ### 🚫 ZeroDivisionError 110 | - Occurs when you try to divide by zero 111 | - Mathematical impossibility 112 | - Example: 113 | ```python 114 | x = 10 / 0 # Triggers ZeroDivisionError 115 | ``` 116 | """, 117 | "TypeError": """ 118 | ### 🔀 TypeError 119 | - Happens when an operation is applied to an inappropriate type 120 | - Mixing incompatible types 121 | - Example: 122 | ```python 123 | "2" + 3 # Can't add string and integer 124 | ``` 125 | """, 126 | "ValueError": """ 127 | ### 📊 ValueError 128 | - Raised when a function receives an argument of correct type 129 | but inappropriate value 130 | - Example: 131 | ```python 132 | int("hello") # Can't convert non-numeric string to int 133 | ``` 134 | """, 135 | "IndexError": """ 136 | ### 📑 IndexError 137 | - Occurs when trying to access a list index that doesn't exist 138 | - Going beyond list boundaries 139 | - Example: 140 | ```python 141 | my_list = [1, 2, 3] 142 | print(my_list[5]) # Only has indices 0, 1, 2 143 | ``` 144 | """, 145 | "KeyError": """ 146 | ### 🗝️ KeyError 147 | - Raised when trying to access a dictionary key that doesn't exist 148 | - Example: 149 | ```python 150 | my_dict = {"a": 1, "b": 2} 151 | print(my_dict["c"]) # "c" key doesn't exist 152 | ``` 153 | """ 154 | } 155 | 156 | mo.md(error_explanations.get(error_types.value, "Select an error type")) 157 | return (error_explanations,) 158 | 159 | 160 | @app.cell(hide_code=True) 161 | def _(mo): 162 | mo.md( 163 | """ 164 | ## Handling multiple exception types 165 | 166 | Catch and handle different types of errors specifically: 167 | 168 | ```python 169 | def complex_function(x, y): 170 | try: 171 | # Potential errors: TypeError, ZeroDivisionError 172 | result = x / y 173 | return int(result) 174 | except TypeError: 175 | return "Type mismatch!" 176 | except ZeroDivisionError: 177 | return "No division by zero!" 178 | except ValueError: 179 | return "Conversion error!" 180 | finally: 181 | # The `finally` block always runs, regardless if there 182 | # was an error or not 183 | ... 184 | 185 | ``` 186 | """ 187 | ) 188 | return 189 | 190 | 191 | @app.cell(hide_code=True) 192 | def _(finally_input): 193 | finally_input 194 | return 195 | 196 | 197 | @app.cell(hide_code=True) 198 | def _(mo): 199 | # Finally Block Demonstration 200 | finally_input = mo.ui.switch( 201 | label="Throw an error?", 202 | value=True 203 | ) 204 | return (finally_input,) 205 | 206 | 207 | @app.cell 208 | def _(finally_input, mo): 209 | def simulate_resource_management(): 210 | try: 211 | # Simulating a resource-intensive operation 212 | if not finally_input.value: 213 | return "🟢 Resource processing successful" 214 | else: 215 | raise Exception("Simulated failure") 216 | except Exception as e: 217 | return f"🔴 Error: {e}" 218 | finally: 219 | return "📦 Resource cleanup completed" 220 | 221 | 222 | _result = simulate_resource_management() 223 | 224 | mo.md(f""" 225 | ### Example: the finally clause 226 | 227 | **Scenario**: {"Normal operation" if not finally_input.value else "An exception was raised"} 228 | 229 | **Result**: {_result} 230 | 231 | Notice how the `finally` block always runs, ensuring cleanup! 232 | """) 233 | return (simulate_resource_management,) 234 | 235 | 236 | @app.cell 237 | def _(): 238 | import marimo as mo 239 | return (mo,) 240 | 241 | 242 | if __name__ == "__main__": 243 | app.run() 244 | -------------------------------------------------------------------------------- /python/README.md: -------------------------------------------------------------------------------- 1 | # Learn Python 2 | 3 | This collection of marimo notebooks is designed to teach you the basics 4 | of the Python programming language. 5 | 6 | **Running notebooks.** To run a notebook locally, use 7 | 8 | ```bash 9 | uvx marimo edit 10 | ``` 11 | 12 | For example, run the numbers tutorial with 13 | 14 | ```bash 15 | uvx marimo edit https://github.com/marimo-team/learn/blob/main/python/001_numbers.py 16 | ``` 17 | 18 | You can also open notebooks in our online playground by appending `marimo.app/` 19 | to a notebook's URL: [marimo.app/https://github.com/marimo-team/learn/blob/main/python/001_numbers.py](https://marimo.app/https://github.com/marimo-team/learn/blob/main/python/001_numbers.py). 20 | 21 | **Thanks to all our notebook authors!** 22 | 23 | * [Srihari Thyagarajan](https://github.com/Haleshot) 24 | -------------------------------------------------------------------------------- /scripts/build.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | import subprocess 5 | import argparse 6 | import json 7 | import datetime 8 | import markdown 9 | from datetime import date 10 | from pathlib import Path 11 | from typing import Dict, List, Any, Optional, Tuple 12 | 13 | from jinja2 import Environment, FileSystemLoader 14 | 15 | 16 | def export_html_wasm(notebook_path: str, output_dir: str, as_app: bool = False) -> bool: 17 | """Export a single marimo notebook to HTML format. 18 | 19 | Args: 20 | notebook_path: Path to the notebook to export 21 | output_dir: Directory to write the output HTML files 22 | as_app: If True, export as app instead of notebook 23 | 24 | Returns: 25 | bool: True if export succeeded, False otherwise 26 | """ 27 | # Create directory for the output 28 | os.makedirs(output_dir, exist_ok=True) 29 | 30 | # Determine the output path (preserving directory structure) 31 | rel_path = os.path.basename(os.path.dirname(notebook_path)) 32 | if rel_path != os.path.dirname(notebook_path): 33 | # Create subdirectory if needed 34 | os.makedirs(os.path.join(output_dir, rel_path), exist_ok=True) 35 | 36 | # Determine output filename (same as input but with .html extension) 37 | output_filename = os.path.basename(notebook_path).replace(".py", ".html") 38 | output_path = os.path.join(output_dir, rel_path, output_filename) 39 | 40 | # Run marimo export command 41 | mode = "--mode app" if as_app else "--mode edit" 42 | cmd = f"marimo export html-wasm {mode} {notebook_path} -o {output_path} --sandbox" 43 | print(f"Exporting {notebook_path} to {rel_path}/{output_filename} as {'app' if as_app else 'notebook'}") 44 | print(f"Running command: {cmd}") 45 | 46 | try: 47 | result = subprocess.run(cmd, shell=True, check=True, capture_output=True, text=True) 48 | print(f"Successfully exported {notebook_path} to {output_path}") 49 | return True 50 | except subprocess.CalledProcessError as e: 51 | print(f"Error exporting {notebook_path}: {e}") 52 | print(f"Command output: {e.output}") 53 | return False 54 | 55 | 56 | def get_course_metadata(course_dir: Path) -> Dict[str, Any]: 57 | """Extract metadata from a course directory. 58 | 59 | Reads the README.md file to extract title and description. 60 | 61 | Args: 62 | course_dir: Path to the course directory 63 | 64 | Returns: 65 | Dict: Dictionary containing course metadata (title, description) 66 | """ 67 | readme_path = course_dir / "README.md" 68 | title = course_dir.name.replace("_", " ").title() 69 | description = "" 70 | description_html = "" 71 | 72 | if readme_path.exists(): 73 | with open(readme_path, "r", encoding="utf-8") as f: 74 | content = f.read() 75 | 76 | # Try to extract title from first heading 77 | title_match = content.split("\n")[0] 78 | if title_match.startswith("# "): 79 | title = title_match[2:].strip() 80 | 81 | # Extract description from content after first heading 82 | desc_content = "\n".join(content.split("\n")[1:]).strip() 83 | if desc_content: 84 | # Take first paragraph as description, preserve markdown formatting 85 | description = desc_content.split("\n\n")[0].strip() 86 | # Convert markdown to HTML 87 | description_html = markdown.markdown(description) 88 | 89 | return { 90 | "title": title, 91 | "description": description, 92 | "description_html": description_html 93 | } 94 | 95 | 96 | def organize_notebooks_by_course(all_notebooks: List[str]) -> Dict[str, Dict[str, Any]]: 97 | """Organize notebooks by course. 98 | 99 | Args: 100 | all_notebooks: List of paths to notebooks 101 | 102 | Returns: 103 | Dict: A dictionary where keys are course directories and values are 104 | metadata about the course and its notebooks 105 | """ 106 | courses = {} 107 | 108 | for notebook_path in sorted(all_notebooks): 109 | # Parse the path to determine course 110 | # The first directory in the path is the course 111 | path_parts = Path(notebook_path).parts 112 | 113 | if len(path_parts) < 2: 114 | print(f"Skipping notebook with invalid path: {notebook_path}") 115 | continue 116 | 117 | course_id = path_parts[0] 118 | 119 | # If this is a new course, initialize it 120 | if course_id not in courses: 121 | course_metadata = get_course_metadata(Path(course_id)) 122 | 123 | courses[course_id] = { 124 | "id": course_id, 125 | "title": course_metadata["title"], 126 | "description": course_metadata["description"], 127 | "description_html": course_metadata["description_html"], 128 | "notebooks": [] 129 | } 130 | 131 | # Extract the notebook number and name from the filename 132 | filename = Path(notebook_path).name 133 | basename = filename.replace(".py", "") 134 | 135 | # Extract notebook metadata 136 | notebook_title = basename.replace("_", " ").title() 137 | 138 | # Try to extract a sequence number from the start of the filename 139 | # Match patterns like: 01_xxx, 1_xxx, etc. 140 | import re 141 | number_match = re.match(r'^(\d+)(?:[_-]|$)', basename) 142 | notebook_number = number_match.group(1) if number_match else None 143 | 144 | # If we found a number, remove it from the title 145 | if number_match: 146 | notebook_title = re.sub(r'^\d+\s*[_-]?\s*', '', notebook_title) 147 | 148 | # Calculate the HTML output path (for linking) 149 | html_path = f"{course_id}/{filename.replace('.py', '.html')}" 150 | 151 | # Add the notebook to the course 152 | courses[course_id]["notebooks"].append({ 153 | "path": notebook_path, 154 | "html_path": html_path, 155 | "title": notebook_title, 156 | "display_name": notebook_title, 157 | "original_number": notebook_number 158 | }) 159 | 160 | # Sort notebooks by number if available, otherwise by title 161 | for course_id, course_data in courses.items(): 162 | # Sort the notebooks list by number and title 163 | course_data["notebooks"] = sorted( 164 | course_data["notebooks"], 165 | key=lambda x: ( 166 | int(x["original_number"]) if x["original_number"] is not None else float('inf'), 167 | x["title"] 168 | ) 169 | ) 170 | 171 | return courses 172 | 173 | 174 | def generate_clean_tailwind_landing_page(courses: Dict[str, Dict[str, Any]], output_dir: str) -> None: 175 | """Generate a clean tailwindcss landing page with green accents. 176 | 177 | This generates a modern, minimal landing page for marimo notebooks using tailwindcss. 178 | The page is designed with clean aesthetics and green color accents using Jinja2 templates. 179 | 180 | Args: 181 | courses: Dictionary of courses metadata 182 | output_dir: Directory to write the output index.html file 183 | """ 184 | print("Generating clean tailwindcss landing page") 185 | 186 | index_path = os.path.join(output_dir, "index.html") 187 | os.makedirs(output_dir, exist_ok=True) 188 | 189 | # Load Jinja2 template 190 | current_dir = Path(__file__).parent 191 | templates_dir = current_dir / "templates" 192 | env = Environment(loader=FileSystemLoader(templates_dir)) 193 | template = env.get_template('index.html') 194 | 195 | try: 196 | with open(index_path, "w", encoding="utf-8") as f: 197 | # Render the template with the provided data 198 | rendered_html = template.render( 199 | courses=courses, 200 | current_year=datetime.date.today().year 201 | ) 202 | f.write(rendered_html) 203 | 204 | print(f"Successfully generated clean tailwindcss landing page at {index_path}") 205 | 206 | except IOError as e: 207 | print(f"Error generating clean tailwindcss landing page: {e}") 208 | 209 | 210 | def main() -> None: 211 | parser = argparse.ArgumentParser(description="Build marimo notebooks") 212 | parser.add_argument( 213 | "--output-dir", default="_site", help="Output directory for built files" 214 | ) 215 | parser.add_argument( 216 | "--course-dirs", nargs="+", default=None, 217 | help="Specific course directories to build (default: all directories with .py files)" 218 | ) 219 | args = parser.parse_args() 220 | 221 | # Find all course directories (directories containing .py files) 222 | all_notebooks: List[str] = [] 223 | 224 | # Directories to exclude from course detection 225 | excluded_dirs = ["scripts", "env", "__pycache__", ".git", ".github", "assets"] 226 | 227 | if args.course_dirs: 228 | course_dirs = args.course_dirs 229 | else: 230 | # Automatically detect course directories (any directory with .py files) 231 | course_dirs = [] 232 | for item in os.listdir("."): 233 | if (os.path.isdir(item) and 234 | not item.startswith(".") and 235 | not item.startswith("_") and 236 | item not in excluded_dirs): 237 | # Check if directory contains .py files 238 | if list(Path(item).glob("*.py")): 239 | course_dirs.append(item) 240 | 241 | print(f"Found course directories: {', '.join(course_dirs)}") 242 | 243 | for directory in course_dirs: 244 | dir_path = Path(directory) 245 | if not dir_path.exists(): 246 | print(f"Warning: Directory not found: {dir_path}") 247 | continue 248 | 249 | notebooks = [str(path) for path in dir_path.rglob("*.py") 250 | if not path.name.startswith("_") and "/__pycache__/" not in str(path)] 251 | all_notebooks.extend(notebooks) 252 | 253 | if not all_notebooks: 254 | print("No notebooks found!") 255 | return 256 | 257 | # Export notebooks sequentially 258 | successful_notebooks = [] 259 | for nb in all_notebooks: 260 | # Determine if notebook should be exported as app or notebook 261 | # For now, export all as notebooks 262 | if export_html_wasm(nb, args.output_dir, as_app=False): 263 | successful_notebooks.append(nb) 264 | 265 | # Organize notebooks by course (only include successfully exported notebooks) 266 | courses = organize_notebooks_by_course(successful_notebooks) 267 | 268 | # Generate landing page using Tailwind CSS 269 | generate_clean_tailwind_landing_page(courses, args.output_dir) 270 | 271 | # Save course data as JSON for potential use by other tools 272 | courses_json_path = os.path.join(args.output_dir, "courses.json") 273 | with open(courses_json_path, "w", encoding="utf-8") as f: 274 | json.dump(courses, f, indent=2) 275 | 276 | print(f"Build complete! Site generated in {args.output_dir}") 277 | print(f"Successfully exported {len(successful_notebooks)} out of {len(all_notebooks)} notebooks") 278 | 279 | 280 | if __name__ == "__main__": 281 | main() 282 | -------------------------------------------------------------------------------- /scripts/preview.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | import subprocess 5 | import argparse 6 | import webbrowser 7 | import time 8 | import sys 9 | from pathlib import Path 10 | 11 | def main(): 12 | parser = argparse.ArgumentParser(description="Build and preview marimo notebooks site") 13 | parser.add_argument( 14 | "--port", default=8000, type=int, help="Port to run the server on" 15 | ) 16 | parser.add_argument( 17 | "--no-build", action="store_true", help="Skip building the site (just serve existing files)" 18 | ) 19 | parser.add_argument( 20 | "--output-dir", default="_site", help="Output directory for built files" 21 | ) 22 | args = parser.parse_args() 23 | 24 | # Store the current directory 25 | original_dir = os.getcwd() 26 | 27 | try: 28 | # Build the site if not skipped 29 | if not args.no_build: 30 | print("Building site...") 31 | build_script = Path("scripts/build.py") 32 | if not build_script.exists(): 33 | print(f"Error: Build script not found at {build_script}") 34 | return 1 35 | 36 | result = subprocess.run( 37 | [sys.executable, str(build_script), "--output-dir", args.output_dir], 38 | check=False 39 | ) 40 | if result.returncode != 0: 41 | print("Warning: Build process completed with errors.") 42 | 43 | # Check if the output directory exists 44 | output_dir = Path(args.output_dir) 45 | if not output_dir.exists(): 46 | print(f"Error: Output directory '{args.output_dir}' does not exist.") 47 | return 1 48 | 49 | # Change to the output directory 50 | os.chdir(args.output_dir) 51 | 52 | # Open the browser 53 | url = f"http://localhost:{args.port}" 54 | print(f"Opening {url} in your browser...") 55 | webbrowser.open(url) 56 | 57 | # Start the server 58 | print(f"Starting server on port {args.port}...") 59 | print("Press Ctrl+C to stop the server") 60 | 61 | # Use the appropriate Python executable 62 | subprocess.run([sys.executable, "-m", "http.server", str(args.port)]) 63 | 64 | return 0 65 | except KeyboardInterrupt: 66 | print("\nServer stopped.") 67 | return 0 68 | except Exception as e: 69 | print(f"Error: {e}") 70 | return 1 71 | finally: 72 | # Always return to the original directory 73 | os.chdir(original_dir) 74 | 75 | if __name__ == "__main__": 76 | sys.exit(main()) 77 | -------------------------------------------------------------------------------- /scripts/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Marimo Learn - Interactive Python Notebooks 7 | 8 | 9 | 23 | 24 | 25 | 26 |
27 |
28 |
29 |
30 |

Interactive Python Learning with marimo

31 |

Explore our collection of interactive notebooks for Python, data science, and machine learning.

32 | 36 |
37 |
38 |
39 | Marimo Logo 40 |
41 |
42 |
43 |
44 |
45 | 46 | 47 |
48 |
49 |

Why Learn with Marimo?

50 |
51 |
52 |
53 | 54 | 55 | 56 |
57 |

Interactive Learning

58 |

Learn by doing with interactive notebooks that run directly in your browser.

59 |
60 |
61 |
62 | 63 | 64 | 65 |
66 |

Practical Examples

67 |

Real-world examples and applications to reinforce your understanding.

68 |
69 |
70 |
71 | 72 | 73 | 74 |
75 |

Comprehensive Curriculum

76 |

From Python basics to advanced machine learning concepts.

77 |
78 |
79 |
80 |
81 | 82 | 83 |
84 |
85 |

Explore Our Courses

86 |
87 | {% for course_id, course in courses.items() %} 88 | {% set notebooks = course.get('notebooks', []) %} 89 | {% set notebook_count = notebooks|length %} 90 | 91 | {% if notebook_count > 0 %} 92 | {% set title = course.get('title', course_id|replace('_', ' ')|title) %} 93 | 94 |
95 |
96 |
97 |

{{ title }}

98 |

99 | {% if course.get('description_html') %} 100 | {{ course.get('description_html')|safe }} 101 | {% endif %} 102 |

103 |
104 | {{ notebook_count }} notebooks: 105 |
    106 | {% for notebook in notebooks %} 107 | {% set notebook_title = notebook.get('title', notebook.get('path', '').split('/')[-1].replace('.py', '').replace('_', ' ').title()) %} 108 |
  1. 109 | 110 | {{ notebook_title }} 111 | 112 |
  2. 113 | {% endfor %} 114 |
115 |
116 |
117 |
118 | {% endif %} 119 | {% endfor %} 120 |
121 |
122 |
123 | 124 | 125 |
126 |
127 |

Want to Contribute?

128 |

Help us improve these learning materials by contributing to the GitHub repository. We welcome new content, bug fixes, and improvements!

129 | 130 | 131 | 132 | 133 | Contribute on GitHub 134 | 135 |
136 |
137 | 138 | 139 | 160 | 161 | 162 | 173 | 174 | 175 | --------------------------------------------------------------------------------