diff --git a/class_project/data605/Spring2026/projects/Ray/project_template/.dockerignore b/class_project/data605/Spring2026/projects/Ray/project_template/.dockerignore new file mode 100644 index 000000000..fd85b2584 --- /dev/null +++ b/class_project/data605/Spring2026/projects/Ray/project_template/.dockerignore @@ -0,0 +1,143 @@ +# Exclude files from Docker build context. This prevents unnecessary files from +# being sent to Docker daemon, reducing build time and image size. + +# Python artifacts +__pycache__/ +*.pyc +*.pyo +*.pyd +*.egg-info/ + +# Virtual environments +venv/ +.venv/ +env/ +.env +.envrc +client_venv.helpers/ +ENV/ + +# Jupyter +.ipynb_checkpoints/ +.jupyter/ + +# Build artifacts +build/ +dist/ +*.eggs/ +.eggs/ + +# Cache and temporary files +*.log +*.tmp +*.cache +.pytest_cache/ +.mypy_cache/ +.coverage +htmlcov/ + +# Git and version control +.git/ +.gitignore +.gitattributes +.github/ + +# Docker build scripts (not needed at runtime) +docker_build.sh +docker_push.sh +docker_clean.sh +docker_exec.sh +docker_cmd.sh +docker_bash.sh +docker_jupyter.sh +docker_name.sh +run_jupyter.sh +Dockerfile.* +.dockerignore + +# Documentation +README.md +README.admin.md +docs/ +*.md +CHANGELOG.md +LICENSE + +# Configuration and secrets +.env.* +.env.local +.env.development +.env.production +.DS_Store +Thumbs.db + +# Shell configuration +.bashrc +.bash_history +.zshrc + +# Large data files (mount via volume instead) +data/ +*.csv +*.pkl +*.h5 +*.parquet +*.feather +*.arrow +*.npy +*.npz + +# Generated images +*.png +*.jpg +*.jpeg +*.gif +*.svg +*.pdf + +# Test files and examples +tests/ +test_* +*_test.py +tutorials/ +examples/ + +# IDE and editor files +.vscode/ +.idea/ +*.swp +*.swo +*~ +.project +.pydevproject +.settings/ +*.iml +.sublime-project +.sublime-workspace + +# Node and frontend (if applicable) +node_modules/ +npm-debug.log +yarn-error.log +.npm + +# Requirements management +requirements.in +Pipfile +Pipfile.lock +poetry.lock +setup.py +setup.cfg + +# CI/CD configuration +.gitlab-ci.yml +.travis.yml +Jenkinsfile +.circleci/ + +# Miscellaneous +*.bak +.venv.bak/ +*.whl +*.tar.gz +*.zip diff --git a/class_project/data605/Spring2026/projects/Ray/project_template/Dockerfile.python_slim b/class_project/data605/Spring2026/projects/Ray/project_template/Dockerfile.python_slim new file mode 100644 index 000000000..cc8f18f2f --- /dev/null +++ b/class_project/data605/Spring2026/projects/Ray/project_template/Dockerfile.python_slim @@ -0,0 +1,28 @@ +# Use Python 3.12 slim (already has Python and pip). +FROM python:3.12-slim + +# Avoid interactive prompts during apt operations. +ENV DEBIAN_FRONTEND=noninteractive + +# Install CA certificates (needed for HTTPS). +RUN apt-get update && apt-get install -y \ + ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +# Install project specific packages. +RUN mkdir -p /install +COPY requirements.txt /install/requirements.txt +RUN pip install --upgrade pip && \ + pip install --no-cache-dir jupyterlab jupyterlab_vim jupytext -r /install/requirements.txt + +# Config. +COPY etc_sudoers /install/ +COPY etc_sudoers /etc/sudoers +COPY bashrc /root/.bashrc + +# Report package versions. +COPY version.sh /install/ +RUN /install/version.sh 2>&1 | tee version.log + +# Jupyter. +EXPOSE 8888 diff --git a/class_project/data605/Spring2026/projects/Ray/project_template/Dockerfile.ubuntu b/class_project/data605/Spring2026/projects/Ray/project_template/Dockerfile.ubuntu new file mode 100644 index 000000000..af20ee276 --- /dev/null +++ b/class_project/data605/Spring2026/projects/Ray/project_template/Dockerfile.ubuntu @@ -0,0 +1 @@ +You have exceeded a secondary rate limit. Please wait a few minutes before you try again. For more on scraping GitHub and how it may affect your rights, please review our Terms of Service (https://docs.github.com/en/site-policy/github-terms/github-terms-of-service) diff --git a/class_project/data605/Spring2026/projects/Ray/project_template/Dockerfile.uv b/class_project/data605/Spring2026/projects/Ray/project_template/Dockerfile.uv new file mode 100644 index 000000000..d3b2a0abc --- /dev/null +++ b/class_project/data605/Spring2026/projects/Ray/project_template/Dockerfile.uv @@ -0,0 +1,49 @@ +FROM ubuntu:24.04 +ENV DEBIAN_FRONTEND noninteractive + +# Install system utilities and Python in a single layer. +RUN apt-get update && \ + apt-get upgrade -y && \ + apt-get install -y --no-install-recommends \ + sudo \ + curl \ + git \ + build-essential \ + python3 \ + python3-pip \ + python3-dev \ + python3-venv \ + libgomp1 \ + g++ \ + && rm -rf /var/lib/apt/lists/* + +# Install uv for package management. +RUN curl -LsSf https://astral.sh/uv/install.sh | sh +ENV PATH="/root/.local/bin:$PATH" + +# Install project specific packages using uv. +COPY pyproject.toml uv.lock /app/ +WORKDIR /app +RUN uv sync +ENV PATH="/app/.venv/bin:$PATH" + +# Install Jupyter. +RUN pip install --upgrade pip && \ + pip install --no-cache-dir jupyterlab jupyterlab_vim jupytext + +# Copy project files. +COPY . /app + +RUN mkdir /install + +# Config. +COPY etc_sudoers /install/ +COPY etc_sudoers /etc/sudoers +COPY bashrc /root/.bashrc + +# Report package versions. +COPY version.sh /install/ +RUN /install/version.sh 2>&1 | tee version.log + +# Jupyter. +EXPOSE 8888 diff --git a/class_project/data605/Spring2026/projects/Ray/project_template/README.md b/class_project/data605/Spring2026/projects/Ray/project_template/README.md new file mode 100644 index 000000000..58d90e2d1 --- /dev/null +++ b/class_project/data605/Spring2026/projects/Ray/project_template/README.md @@ -0,0 +1,802 @@ +# Summary +This directory contains a Docker-based development environment template with: + +- Utility scripts for Docker operations (build, run, clean, push) +- Configuration files for Dockerfile and environment setup +- Jupyter notebook templates for standardized project development +- Shell utilities and Python helpers for container-based workflows + +A guide to set up Docker-based projects using the template, customize it for +your needs, and maintain it over time. + +## Description of Files +- `bashrc` + - Bash configuration file enabling `vi` mode for command-line editing + +- `copy_docker_files.py` + - Python script for copying Docker configuration files to destination + directories + +- `docker_build.version.log` + - Log file containing Python, `pip`, Jupyter, and package version information + from Docker build + +- `docker_cmd.sh` + - Shell script for executing arbitrary commands inside Docker containers with + volume mounting + +- `docker_jupyter.sh` + - Shell script for launching Jupyter Lab server inside Docker containers + +- `docker_name.sh` + - Configuration file defining Docker repository and image naming variables + +- `Dockerfile` + - Docker image build configuration with Ubuntu, Python, Jupyter, and project + dependencies + +- `etc_sudoers` + - Sudoers configuration file granting passwordless sudo access for postgres + user + +- `README.md` + - Documentation file describing directory contents, files, and executable + scripts + +- `template_utils.py` + - Python utility functions supporting tutorial notebooks with data processing + and modeling helpers + +- `template.API.ipynb` + - Jupyter notebook template for API exploration and library usage examples + +- `template.example.ipynb` + - Jupyter notebook template for project examples and demonstrations + +- `utils.sh` + - Bash utility library with reusable functions for Docker operations + - Provides centralized argument parsing (`parse_default_args`) for `-h` and + `-v` flags used by all `docker_*.sh` scripts + - Provides Jupyter configuration logic: vim keybindings, notification + settings, and Docker run option builders + - All `docker_*.sh`, `docker_jupyter.sh`, and `run_jupyter.sh` scripts across + the repo source this file from `class_project/project_template/utils.sh` + +## Workflows +- All commands should be run from inside the project directory + ```bash + > cd tutorials/FilterPy + ``` + +- To build the container for a project + ```bash + > cd $PROJECT + # Build the container. + > docker_build.sh + # Build without cache (pass extra args after -v). + > docker_build.sh --no-cache + # Test the container. + > docker_bash.sh ls + ``` + +- Enable verbose (trace) output with `-v` + ```bash + > docker_build.sh -v + > docker_bash.sh -v + ``` + +- Get help for any docker script + ```bash + > docker_build.sh -h + > docker_jupyter.sh -h + ``` + +- Start Jupyter + ```bash + > docker_jupyter.sh + # Go to localhost:8888 + ``` + +- Start Jupyter on a specific port with vim support + ```bash + > docker_jupyter.sh -p 8890 -u + # Go to localhost:8890 + ``` + +## How to Customize a Project Template +- Copy the template + ```bash + > cp -r class_project/project_template $TARGET + ``` + +## Description of Executables + +### `copy_docker_files.py` +- **What It Does** + - Copies Docker configuration and utility files from project_template to a + destination directory + - Preserves all file permissions and attributes during copying + - Creates destination directory if it doesn't exist + +- Copy all Docker files to a target directory: + ```bash + > ./copy_docker_files.py --dst_dir /path/to/destination + ``` + +- Copy with verbose logging: + ```bash + > ./copy_docker_files.py --dst_dir /path/to/destination -v DEBUG + ``` + +### `docker_bash.sh` +- **What It Does** + - Launches an interactive bash shell inside a Docker container + - Mounts the current working directory as `/data` inside the container + - Exposes port 8888 for potential services running in the container + - Accepts `-h` (help) and `-v` (verbose/trace) flags via `parse_default_args` + +- Launch bash shell in the container: + ```bash + > ./docker_bash.sh + ``` + +- Launch with verbose output (prints each command): + ```bash + > ./docker_bash.sh -v + ``` + +### `docker_build.sh` +- **What It Does** + - Builds Docker container images using Docker BuildKit + - Supports single-architecture builds (default) or multi-architecture builds + (`linux/arm64`, `linux/amd64`) + - Copies project files to temporary build directory and generates build logs + - Accepts `-h` (help) and `-v` (verbose/trace) flags; any extra arguments + after flags are forwarded to `docker build` + +- Build container image for current architecture: + ```bash + > ./docker_build.sh + ``` + +- Build without Docker layer cache: + ```bash + > ./docker_build.sh --no-cache + ``` + +- Build multi-architecture image (requires setting `DOCKER_BUILD_MULTI_ARCH=1` + in the script): + ```bash + > # Edit docker_build.sh to set DOCKER_BUILD_MULTI_ARCH=1 + > ./docker_build.sh + ``` + +### `docker_clean.sh` +- **What It Does** + +- Removes all Docker images matching the project's full image name +- Lists images before and after removal for verification +- Uses force removal to ensure cleanup completes + +- Remove project's Docker images: + ```bash + > ./docker_clean.sh + ``` + +### `docker_cmd.sh` +- **What It Does** + - Executes arbitrary commands inside a Docker container + - Mounts current directory as `/data` for accessing project files + - Automatically removes container after command execution completes + - Accepts `-h` (help) and `-v` (verbose/trace) flags; remaining arguments + form the command to execute + +- Run Python script inside container: + ```bash + > ./docker_cmd.sh python script.py --arg value + ``` + +- List files in the container: + ```bash + > ./docker_cmd.sh ls -la /data + ``` + +- Run tests inside container: + ```bash + > ./docker_cmd.sh pytest tests/ + ``` + +### `docker_exec.sh` +- **What It Does** + - Attaches to an already running Docker container with an interactive bash + shell + - Finds the container ID automatically based on the image name + - Useful for debugging or inspecting running containers + - Accepts `-h` (help) and `-v` (verbose/trace) flags via `parse_default_args` + +- Attach to running container: + ```bash + > ./docker_exec.sh + ``` + +### `docker_jupyter.sh` +- **What It Does** + - Launches Jupyter Lab server inside a Docker container + - Supports custom port configuration (default 8888), vim keybindings, and + custom directory mounting + - Runs `run_jupyter.sh` script inside the container with specified options + +- Start Jupyter on default port 8888: + ```bash + > ./docker_jupyter.sh + ``` + +- Start Jupyter on custom port with vim bindings: + ```bash + > ./docker_jupyter.sh -p 8889 -u + ``` + +- Start Jupyter with external directory mounted: + ```bash + > ./docker_jupyter.sh -d /path/to/notebooks -p 8889 + ``` + +- Start Jupyter in verbose mode: + ```bash + > ./docker_jupyter.sh -v -p 8890 + ``` + +### `docker_push.sh` +- **What It Does** + - Authenticates to Docker registry using credentials from + `~/.docker/passwd.$REPO_NAME.txt` + - Pushes the project's Docker image to the remote repository + - Lists images before pushing for verification + +- Push container image to registry: + ```bash + > ./docker_push.sh + ``` + +### `run_jupyter.sh` +- **What It Does** + - Launches Jupyter Lab server with no authentication (token and password + disabled) + - Binds to all network interfaces (0.0.0.0) on port 8888 + - Allows root access for container environments + - When `JUPYTER_USE_VIM=1`, verifies that `jupyterlab_vim` is installed + before enabling vim keybindings; exits with an error if not found + +- Start Jupyter Lab server (typically called from docker_jupyter.sh): + ```bash + > ./run_jupyter.sh + ``` + +- Start with vim keybindings (requires `jupyterlab_vim` installed in the + container): + ```bash + > JUPYTER_USE_VIM=1 ./run_jupyter.sh + ``` + +### `utils.sh` +- **What It Does** + - Central Bash library sourced by all `docker_*.sh` and `run_jupyter.sh` + scripts across the repository + - Provides `parse_default_args` which adds `-h` (help) and `-v` + (verbose/`set -x`) flags to every docker script + - Provides `build_container_image`, `push_container_image`, + `remove_container_image`, `kill_container`, `exec_container` utilities + - Provides Jupyter configuration helpers: vim keybindings, notification + suppression, and Docker run option builders + +### `version.sh` +- **What It Does** + - Reports version information for Python3, pip3, and Jupyter + - Lists all installed Python packages with versions + - Used during Docker image builds to log environment configuration + +- Display version information: + ```bash + > ./version.sh + ``` + +- Save version information to a log file: + ```bash + > ./version.sh 2>&1 | tee version.log + ``` + +# Template Customization and Maintenance + +## Quick Start for New Projects + +### Step 1: Copy the Template +```bash +> cd class_project/project_template +> cp -r . /path/to/your/new/project +> cd /path/to/your/new/project +``` + +### Step 2: Choose a Base Image +The template includes three Dockerfile options. Choose the one that best fits +your project: + +| Option | File | Best For | +| -------------------------- | ------------------------ | ---------------------------------------------------------------- | +| **Standard** | `Dockerfile.ubuntu` | Full Ubuntu environment with system tools | +| **Lightweight** | `Dockerfile.python_slim` | Minimal Python environment; reduced image size | +| **Modern Package Manager** | `Dockerfile.uv` | Fast dependency resolution with [uv](https://docs.astral.sh/uv/) | + +**How to choose:** + +- **Use Standard** if you need system-level tools (git, curl, graphviz, etc.) +- **Use Python Slim** to minimize image size and build time +- **Use uv** if you want faster, more reliable dependency management + +### Step 3: Set Up Your Dockerfile +- Delete unused reference files + ```bash + > rm Dockerfile.ubuntu Dockerfile.python_slim Dockerfile.uv + ``` + +- Create your working Dockerfile + ```bash + > cp Dockerfile.ubuntu Dockerfile + ``` + +- Add your dependencies + ```bash + > echo "numpy\npandas\nscikit-learn" > requirements.in + > pip-compile requirements.in > requirements.txt + ``` + +### Step 4: Keep Customization Minimal +- Only modify what's necessary for your project +- Use `requirements.txt` for all Python packages (don't edit Dockerfile for + this) +- Keep `bashrc` and `etc_sudoers` as-is unless you need custom shell setup +- Keep base image and Python version unless you have specific requirements + +## Understanding the Dockerfile Flow +Each Dockerfile follows the same structure. Here are the key stages: + +### Stage 1: Base Image and System Setup +```dockerfile +FROM ubuntu:24.04 # or python:3.12-slim, depending on your requirement +ENV DEBIAN_FRONTEND noninteractive +RUN apt-get -y update && apt-get -y upgrade +``` + +- **Purpose**: Start with a clean base image and disable interactive + installation prompts + +- **When to customize**: Only change the base image or version if your project + has specific requirements (different Ubuntu version, specific Python version, + etc.) + +### Stage 2: System Utilities (Ubuntu-based Dockerfiles Only) +```dockerfile +RUN apt install -y --no-install-recommends \ + sudo \ + curl \ + systemctl \ + gnupg \ + git \ + vim +``` + +- **Purpose**: Install essential system tools for development and container + management + +- **When to customize**: Add only if needed for your project + - `postgresql-client`: for database connections + - `graphviz`: for graph visualizations + - `ffmpeg`: for media processing + +- **Best practice**: Use `--no-install-recommends` to keep the image small + +### Stage 3: Python and Build Tools (Ubuntu-based Dockerfiles Only) +```dockerfile +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential \ + python3 \ + python3-pip \ + python3-dev \ + python3-venv \ + && rm -rf /var/lib/apt/lists/* +``` + +- **Purpose**: Install Python 3, pip, and build tools needed for compiled + packages + +- **Why venv**: Creates an isolated Python environment separate from system + Python + +- **When to customize**: Rarely. Only change if you need a specific Python + version (e.g., `python3.11` instead of `python3`) + +### Stage 4: Virtual Environment Setup +```dockerfile +RUN python3 -m venv /opt/venv +ENV PATH="/opt/venv/bin:$PATH" +RUN python -m pip install --upgrade pip +``` + +- **Purpose**: Create and activate an isolated virtual environment for your + project + +- **Why this matters**: Ensures reproducibility and prevents dependency + conflicts across projects + +- **When to customize**: Never. This is a standard best practice + +### Stage 5: Jupyter Installation +```dockerfile +RUN pip install jupyterlab jupyterlab_vim +``` + +- **Purpose**: Install JupyterLab and the Vim keybinding extension for + interactive development + - `jupyterlab`: the main IDE for running notebooks in the browser + - `jupyterlab_vim`: adds Vim-style navigation to notebook cells + +- **Why in Dockerfile, not requirements.txt**: These are infrastructure + packages (the IDE itself), not project-specific dependencies + - Do NOT add `jupyterlab`, `jupyterlab-vim`, or `ipywidgets` to + `requirements.txt`; they are already installed here + +- **When to customize**: + - **Remove** this line if your project doesn't use Jupyter + - **Add more extensions** if needed (e.g., `jupyterlab-git`, + `jupyterlab-variableinspector`) + +### Stage 6: Project Dependencies +```dockerfile +COPY requirements.txt /install/requirements.txt +RUN pip install --no-cache-dir -r /install/requirements.txt +``` + +- **Purpose**: Install your project-specific Python packages + +- **When to customize**: This is the primary place to customize. Define all your + dependencies in `requirements.txt` + +- **Best practice**: + - **Pin all versions**: `numpy==1.24.0` (not `numpy>=1.20.0`) + - **Use `--no-cache-dir`**: Reduces image size by skipping pip cache + - **For complex dependencies**: Use `requirements.in` with `pip-tools` or + `pip-compile` + +- **Example requirements.txt**: + ```text + numpy==1.24.0 + pandas==2.0.0 + scikit-learn==1.2.2 + tensorflow==2.13.0 + ``` + +### Stage 7: Configuration +```dockerfile +COPY etc_sudoers /etc/sudoers +COPY bashrc /root/.bashrc +``` + +- **Purpose**: Apply custom bash configuration and sudo permissions + +- **When to customize**: + - **Edit `bashrc`**: to add aliases, environment variables, or custom prompt + - **Edit `etc_sudoers`**: if additional users need passwordless sudo access + +### Stage 8: Version Logging +```dockerfile +ADD version.sh /install/ +RUN /install/version.sh 2>&1 | tee version.log +``` + +- **Purpose**: Document the exact versions of Python, pip, Jupyter, and all + installed packages + +- **What it logs**: + - Python 3 version + - Pip version + - Jupyter version + - Complete list of all installed Python packages + +- **Why it matters**: Creates a detailed record of your container's environment + for troubleshooting and reproducibility + +- **How to use**: After building, review `version.log` to verify all + dependencies installed correctly + ```bash + > docker build -t my-project . + > cat version.log + ``` + +- **Extending it**: If you need to log additional tools (MongoDB, Node.js, + etc.), add them to `version.sh`: + ```bash + > echo "# mongo" + > mongod --version + ``` + +### Stage 9: Port Declaration +```dockerfile +EXPOSE 8888 +``` + +- **Purpose**: Declare that the container uses port 8888 (informational for + Docker) + +- **When to customize**: Add additional ports if your application needs them + (e.g., `EXPOSE 8888 5432 3000`) + +## Best Practices: Keep It Simple + +### The Core Principle +Only change what's necessary for your project. Everything else should inherit +from the template. + +This approach: + +- Makes Dockerfiles easier to understand and maintain +- Keeps images smaller and faster to build +- Simplifies future updates from the template +- Ensures consistency across similar projects + +### How to Do It Right +| What | Where | Example | +| :--------------------------- | :--------------------------- | :------------------------------ | +| Project Python packages | `requirements.txt` | `numpy==1.24.0` | +| Jupyter + Vim (always there) | Dockerfile Stage 5 | `jupyterlab jupyterlab_vim` | +| System tools | Dockerfile `apt-get` section | `postgresql-client` | +| Shell aliases | `bashrc` | `alias jlab="jupyter lab"` | +| Custom scripts | `scripts/` directory | Setup or initialization scripts | +| User permissions | `etc_sudoers` | Grant passwordless sudo | + +- **Do NOT add to `requirements.txt`**: `jupyterlab`, `jupyterlab-vim`, + `jupyterlab_vim`, or `ipywidgets` — these are Jupyter infrastructure packages + and are already installed in Stage 5 of the Dockerfile + +### Wrong Vs. Right Approach +- **Wrong**: Embed everything in the Dockerfile + ```dockerfile + RUN pip install my-package && python my_setup.py && npm install + ``` + +- **Right**: Use separate files and keep Dockerfile clean + ```dockerfile + COPY requirements.txt /install/ + RUN pip install -r /install/requirements.txt + COPY scripts/setup.sh /install/ + RUN /install/setup.sh + ``` + +## .Dockerignore Policy + +### Why It Matters +The `.dockerignore` file prevents unnecessary files from being added to the +Docker build context: + +- **Reduces build time**: Fewer files to transfer to Docker daemon +- **Reduces image size**: Only necessary files are included +- **Improves security**: Prevents leaking sensitive data + +### What to Exclude: Category Breakdown +- Python Artifacts (Always Exclude) + ```verbatim + __pycache__/ + *.pyc + *.pyo + *.pyd + ``` + - Why: Compiled bytecode generated at runtime. Regenerated in container, adds + bloat + +- Virtual Environments (Always Exclude) + ```verbatim + venv/ + .venv/ + env/ + .env/ + ``` + - Why: Local venvs aren't portable to containers. The Dockerfile creates its + own + +- Jupyter Checkpoints (Always Exclude) + ```verbatim + .ipynb_checkpoints/ + ``` + - Why: Auto-generated by Jupyter, not needed in the image + +- Git and Version Control (Always Exclude) + ```verbatim + .git/ + .gitignore + .gitattributes + ``` + - Why: Repository history not needed at runtime + +- Docker Build Scripts (Always Exclude) + ```verbatim + docker_build.sh + docker_push.sh + docker_clean.sh + docker_exec.sh + docker_cmd.sh + docker_bash.sh + docker_jupyter.sh + docker_name.sh + Dockerfile.* + ``` + - Why: Local development scripts don't run inside the container + +- Large Data Files (Recommended) + ```verbatim + data/ + *.csv + *.pkl + *.h5 + *.parquet + ``` + - Why: Don't ship large training and test data in the image. Mount via volume + instead + - Best practice: `bash > docker run -v /path/to/data:/data my-image ` + +- Test Files (Project-Dependent) + ```verbatim + tests/ + tutorials/ + ``` + - Why: Exclude if tests don't run in the container + - When to include: If CI and CD runs tests inside the container + +- Documentation (Recommended) + ```verbatim + README.md + docs/ + *.md + ``` + - Why: Not needed at runtime + - Exception: Only keep if your app reads these files at runtime + +- Generated Files (Always Exclude) + ```verbatim + *.log + *.tmp + *.cache + build/ + dist/ + ``` + - Why: Generated at runtime, not needed in the image + +## Workflow: From Template to Your Project + +### Complete Setup Checklist +- Copy the template + ```bash + > cp -r project_template my-new-project + > cd my-new-project + ``` + +- Keep all reference Dockerfiles + ```verbatim + Dockerfile.ubuntu_24_04 + Dockerfile.python_slim + Dockerfile.uv + ``` + +- Create your working Dockerfile + ```bash + > cp Dockerfile.ubuntu_24_04 Dockerfile + ``` + +- Add your dependencies + ```bash + > pip freeze > requirements.txt + ``` + +- Configure `.dockerignore`: Review the template `.dockerignore` and add your + project-specific exclusions (e.g., data directories) + +- Test the build + ```bash + > docker build -t my-project:latest . + > docker run -it my-project:latest bash + ``` + +- Test Jupyter (if using) + ```bash + > ./docker_jupyter.sh -p 8888 + ``` + +- Document customizations in your project README: + - Base image chosen and why + - Key dependencies + - Any Dockerfile modifications + - How to build and run + +## Maintaining Your Setup + +### Document Any Changes +- If you modify the Dockerfile, add explanatory comments: + ```dockerfile + # Custom: PostgreSQL client for database access + postgresql-client \ + + # Custom: Node.js for frontend builds + nodejs \ + ``` + +### Monitor Package Versions +- After each build, review `version.log`: + ```bash + > docker build -t my-project . + > cat version.log + ``` + +### Keep `.dockerignore` Updated +- If you add new directories or files, update `.dockerignore`. Add to + `.dockerignore` if the directory shouldn't be in the image: + ```verbatim + data/ + cache/ + .temp/ + ``` + +### Contribute Improvements Back +When you improve your project's Docker setup: + +- Test thoroughly in your project +- Document the improvement clearly +- Submit back to `project_template` +- Other projects can adopt it when they update + +Example improvements: + +- Better way to install TensorFlow with GPU support +- Optimized `.dockerignore` for data science projects +- Security hardening (non-root user setup) + +## Troubleshooting + +### Build Is Slow +- Check `.dockerignore`: Ensure large directories (data/, .git/) are excluded +- Check Docker daemon: Verify Docker is running properly +- Check layer caching: Docker reuses cached layers; avoid changing early layers + +### Image Is Too Large +- Check layer sizes: + ```bash + > docker history my-project:latest + ``` + +- Remove unnecessary packages or use `python_slim` base image + +### Package Not Found Error +- Verify package name in PyPI (packages are case-sensitive) +- Check Python version compatibility +- Pin specific version if needed + +### Permission Issues in Container +- Check `etc_sudoers`: Ensure user has appropriate permissions +- Check file ownership: Ensure COPY doesn't create root-only files + +### Jupyter Won't Connect +- Run Jupyter + ```bash + > ./docker_jupyter.sh -p 8888 + ``` + +- Verify http://localhost:8888 (not https). Check firewall if remote access + needed + +### Vim Keybindings Not Working +- If `run_jupyter.sh` exits with `ERROR: jupyterlab_vim is not installed`, it + means `jupyterlab_vim` is missing from the container image +- Make sure `jupyterlab_vim` is installed in the Dockerfile: + ```dockerfile + RUN pip install jupyterlab jupyterlab_vim + ``` +- Rebuild the image after adding the package: + ```bash + > ./docker_build.sh + ``` diff --git a/class_project/data605/Spring2026/projects/Ray/project_template/bashrc.txt b/class_project/data605/Spring2026/projects/Ray/project_template/bashrc.txt new file mode 100644 index 000000000..4b7ff4c49 --- /dev/null +++ b/class_project/data605/Spring2026/projects/Ray/project_template/bashrc.txt @@ -0,0 +1 @@ +set -o vi diff --git a/class_project/data605/Spring2026/projects/Ray/project_template/copy_docker_files.py b/class_project/data605/Spring2026/projects/Ray/project_template/copy_docker_files.py new file mode 100644 index 000000000..0e97c194c --- /dev/null +++ b/class_project/data605/Spring2026/projects/Ray/project_template/copy_docker_files.py @@ -0,0 +1,140 @@ +#!/usr/bin/env python + +""" +Copy Docker-related files from the source directory to a destination directory. + +This script copies all Docker configuration and utility files from +class_project/project_template/ to a specified destination directory. + +Usage examples: + # Copy all files to a target directory. + > ./copy_docker_files.py --dst_dir /path/to/destination + + # Copy with verbose logging. + > ./copy_docker_files.py --dst_dir /path/to/destination -v DEBUG + +Import as: + +import class_project.project_template.copy_docker_files as cpdccodo +""" + +import argparse +import logging +import os +from typing import List + +import helpers.hdbg as hdbg +import helpers.hio as hio +import helpers.hparser as hparser +import helpers.hsystem as hsystem + +_LOG = logging.getLogger(__name__) + +# ############################################################################# +# Constants +# ############################################################################# + +# List of files to copy from the source directory. +_FILES_TO_COPY = [ + "bashrc", + "docker_bash.sh", + "docker_build.sh", + "docker_clean.sh", + "docker_cmd.sh", + "docker_exec.sh", + "docker_jupyter.sh", + "docker_name.sh", + "docker_push.sh", + "etc_sudoers", + "install_jupyter_extensions.sh", + "run_jupyter.sh" + "version.sh", +] + + +# ############################################################################# +# Helper functions +# ############################################################################# + + +def _get_source_dir() -> str: + """ + Get the absolute path to the source directory containing Docker files. + + :return: absolute path to class_project/project_template/ + """ + # Get the directory where this script is located. + script_dir = os.path.dirname(os.path.abspath(__file__)) + _LOG.debug("Script directory='%s'", script_dir) + return script_dir + + +def _copy_files( + *, + src_dir: str, + dst_dir: str, + files: List[str], +) -> None: + """ + Copy specified files from source directory to destination directory. + + :param src_dir: source directory path + :param dst_dir: destination directory path + :param files: list of filenames to copy + """ + # Verify source directory exists. + hdbg.dassert_dir_exists(src_dir, "Source directory does not exist:", src_dir) + # Create destination directory if it doesn't exist. + hio.create_dir(dst_dir, incremental=True) + _LOG.info("Copying %d files from '%s' to '%s'", len(files), src_dir, dst_dir) + # Copy each file. + copied_count = 0 + for filename in files: + src_path = os.path.join(src_dir, filename) + dst_path = os.path.join(dst_dir, filename) + # Verify source file exists. + hdbg.dassert_path_exists( + src_path, "Source file does not exist:", src_path + ) + # Copy the file using cp -a to preserve all permissions and attributes. + _LOG.debug("Copying '%s' -> '%s'", src_path, dst_path) + cmd = f"cp -a {src_path} {dst_path}" + hsystem.system(cmd) + copied_count += 1 + # + _LOG.info("Successfully copied %d files", copied_count) + + +# ############################################################################# + + +def _parse() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + parser.add_argument( + "--dst_dir", + action="store", + required=True, + help="Destination directory where files will be copied", + ) + hparser.add_verbosity_arg(parser) + return parser + + +def _main(parser: argparse.ArgumentParser) -> None: + args = parser.parse_args() + hdbg.init_logger(verbosity=args.log_level, use_exec_path=True) + # Get source directory. + src_dir = _get_source_dir() + # Copy files to destination. + _copy_files( + src_dir=src_dir, + dst_dir=args.dst_dir, + files=_FILES_TO_COPY, + ) + + +if __name__ == "__main__": + _main(_parse()) diff --git a/class_project/data605/Spring2026/projects/Ray/project_template/docker_bash.sh b/class_project/data605/Spring2026/projects/Ray/project_template/docker_bash.sh new file mode 100644 index 000000000..af20ee276 --- /dev/null +++ b/class_project/data605/Spring2026/projects/Ray/project_template/docker_bash.sh @@ -0,0 +1 @@ +You have exceeded a secondary rate limit. Please wait a few minutes before you try again. For more on scraping GitHub and how it may affect your rights, please review our Terms of Service (https://docs.github.com/en/site-policy/github-terms/github-terms-of-service) diff --git a/class_project/data605/Spring2026/projects/Ray/project_template/docker_build.sh b/class_project/data605/Spring2026/projects/Ray/project_template/docker_build.sh new file mode 100644 index 000000000..5b0957a99 --- /dev/null +++ b/class_project/data605/Spring2026/projects/Ray/project_template/docker_build.sh @@ -0,0 +1,40 @@ +#!/bin/bash +# """ +# Build a Docker container image for the project. +# +# This script sets up the build environment with error handling and command +# tracing, loads Docker configuration from docker_name.sh, and builds the +# Docker image using the build_container_image utility function. It supports +# both single-architecture and multi-architecture builds via the +# DOCKER_BUILD_MULTI_ARCH environment variable. +# """ + +# Exit immediately if any command exits with a non-zero status. +set -e + +# Import the utility functions. +GIT_ROOT=$(git rev-parse --show-toplevel) +source $GIT_ROOT/class_project/project_template/utils.sh + +# Parse default args (-h, -v) and enable set -x if -v is passed. +# Shift processed option flags so remaining args are passed to the build. +parse_default_args "$@" +shift $((OPTIND-1)) + +# Load Docker configuration variables (REPO_NAME, IMAGE_NAME, FULL_IMAGE_NAME). +get_docker_vars_script ${BASH_SOURCE[0]} +source $DOCKER_NAME +print_docker_vars + +# Configure Docker build settings. +# Enable BuildKit for improved build performance and features. +export DOCKER_BUILDKIT=1 +#export DOCKER_BUILDKIT=0 + +# Configure single-architecture build (set to 1 for multi-arch build). +#export DOCKER_BUILD_MULTI_ARCH=1 +export DOCKER_BUILD_MULTI_ARCH=0 + +# Build the container image. +# Pass extra arguments (e.g., --no-cache) via command line after -v. +build_container_image "$@" diff --git a/class_project/data605/Spring2026/projects/Ray/project_template/docker_build.version.log b/class_project/data605/Spring2026/projects/Ray/project_template/docker_build.version.log new file mode 100644 index 000000000..af20ee276 --- /dev/null +++ b/class_project/data605/Spring2026/projects/Ray/project_template/docker_build.version.log @@ -0,0 +1 @@ +You have exceeded a secondary rate limit. Please wait a few minutes before you try again. For more on scraping GitHub and how it may affect your rights, please review our Terms of Service (https://docs.github.com/en/site-policy/github-terms/github-terms-of-service) diff --git a/class_project/data605/Spring2026/projects/Ray/project_template/docker_clean.sh b/class_project/data605/Spring2026/projects/Ray/project_template/docker_clean.sh new file mode 100644 index 000000000..af20ee276 --- /dev/null +++ b/class_project/data605/Spring2026/projects/Ray/project_template/docker_clean.sh @@ -0,0 +1 @@ +You have exceeded a secondary rate limit. Please wait a few minutes before you try again. For more on scraping GitHub and how it may affect your rights, please review our Terms of Service (https://docs.github.com/en/site-policy/github-terms/github-terms-of-service) diff --git a/class_project/data605/Spring2026/projects/Ray/project_template/docker_cmd.sh b/class_project/data605/Spring2026/projects/Ray/project_template/docker_cmd.sh new file mode 100644 index 000000000..af20ee276 --- /dev/null +++ b/class_project/data605/Spring2026/projects/Ray/project_template/docker_cmd.sh @@ -0,0 +1 @@ +You have exceeded a secondary rate limit. Please wait a few minutes before you try again. For more on scraping GitHub and how it may affect your rights, please review our Terms of Service (https://docs.github.com/en/site-policy/github-terms/github-terms-of-service) diff --git a/class_project/data605/Spring2026/projects/Ray/project_template/docker_exec.sh b/class_project/data605/Spring2026/projects/Ray/project_template/docker_exec.sh new file mode 100644 index 000000000..af20ee276 --- /dev/null +++ b/class_project/data605/Spring2026/projects/Ray/project_template/docker_exec.sh @@ -0,0 +1 @@ +You have exceeded a secondary rate limit. Please wait a few minutes before you try again. For more on scraping GitHub and how it may affect your rights, please review our Terms of Service (https://docs.github.com/en/site-policy/github-terms/github-terms-of-service) diff --git a/class_project/data605/Spring2026/projects/Ray/project_template/docker_jupyter.sh b/class_project/data605/Spring2026/projects/Ray/project_template/docker_jupyter.sh new file mode 100644 index 000000000..af20ee276 --- /dev/null +++ b/class_project/data605/Spring2026/projects/Ray/project_template/docker_jupyter.sh @@ -0,0 +1 @@ +You have exceeded a secondary rate limit. Please wait a few minutes before you try again. For more on scraping GitHub and how it may affect your rights, please review our Terms of Service (https://docs.github.com/en/site-policy/github-terms/github-terms-of-service) diff --git a/class_project/data605/Spring2026/projects/Ray/project_template/docker_name.sh b/class_project/data605/Spring2026/projects/Ray/project_template/docker_name.sh new file mode 100644 index 000000000..af20ee276 --- /dev/null +++ b/class_project/data605/Spring2026/projects/Ray/project_template/docker_name.sh @@ -0,0 +1 @@ +You have exceeded a secondary rate limit. Please wait a few minutes before you try again. For more on scraping GitHub and how it may affect your rights, please review our Terms of Service (https://docs.github.com/en/site-policy/github-terms/github-terms-of-service) diff --git a/class_project/data605/Spring2026/projects/Ray/project_template/docker_push.sh b/class_project/data605/Spring2026/projects/Ray/project_template/docker_push.sh new file mode 100644 index 000000000..af20ee276 --- /dev/null +++ b/class_project/data605/Spring2026/projects/Ray/project_template/docker_push.sh @@ -0,0 +1 @@ +You have exceeded a secondary rate limit. Please wait a few minutes before you try again. For more on scraping GitHub and how it may affect your rights, please review our Terms of Service (https://docs.github.com/en/site-policy/github-terms/github-terms-of-service) diff --git a/class_project/data605/Spring2026/projects/Ray/project_template/etc_sudoers.txt b/class_project/data605/Spring2026/projects/Ray/project_template/etc_sudoers.txt new file mode 100644 index 000000000..ee0816a15 --- /dev/null +++ b/class_project/data605/Spring2026/projects/Ray/project_template/etc_sudoers.txt @@ -0,0 +1,31 @@ +# +# This file MUST be edited with the 'visudo' command as root. +# +# Please consider adding local content in /etc/sudoers.d/ instead of +# directly modifying this file. +# +# See the man page for details on how to write a sudoers file. +# +Defaults env_reset +Defaults mail_badpass +Defaults secure_path="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin" + +# Host alias specification + +# User alias specification + +# Cmnd alias specification + +# User privilege specification +root ALL=(ALL:ALL) ALL + +# Members of the admin group may gain root privileges +%admin ALL=(ALL) ALL + +# Allow members of group sudo to execute any command +%sudo ALL=(ALL:ALL) ALL + +# See sudoers(5) for more information on "#include" directives: +postgres ALL=(ALL) NOPASSWD:ALL + +#includedir /etc/sudoers.d diff --git a/class_project/data605/Spring2026/projects/Ray/project_template/requirements.txt b/class_project/data605/Spring2026/projects/Ray/project_template/requirements.txt new file mode 100644 index 000000000..49aca3901 --- /dev/null +++ b/class_project/data605/Spring2026/projects/Ray/project_template/requirements.txt @@ -0,0 +1,4 @@ +matplotlib +numpy +pandas +seaborn diff --git a/class_project/data605/Spring2026/projects/Ray/project_template/run_jupyter.sh b/class_project/data605/Spring2026/projects/Ray/project_template/run_jupyter.sh new file mode 100644 index 000000000..af20ee276 --- /dev/null +++ b/class_project/data605/Spring2026/projects/Ray/project_template/run_jupyter.sh @@ -0,0 +1 @@ +You have exceeded a secondary rate limit. Please wait a few minutes before you try again. For more on scraping GitHub and how it may affect your rights, please review our Terms of Service (https://docs.github.com/en/site-policy/github-terms/github-terms-of-service) diff --git a/class_project/data605/Spring2026/projects/Ray/project_template/template.API.ipynb b/class_project/data605/Spring2026/projects/Ray/project_template/template.API.ipynb new file mode 100644 index 000000000..af20ee276 --- /dev/null +++ b/class_project/data605/Spring2026/projects/Ray/project_template/template.API.ipynb @@ -0,0 +1 @@ +You have exceeded a secondary rate limit. Please wait a few minutes before you try again. For more on scraping GitHub and how it may affect your rights, please review our Terms of Service (https://docs.github.com/en/site-policy/github-terms/github-terms-of-service) diff --git a/class_project/data605/Spring2026/projects/Ray/project_template/template.API.py b/class_project/data605/Spring2026/projects/Ray/project_template/template.API.py new file mode 100644 index 000000000..af20ee276 --- /dev/null +++ b/class_project/data605/Spring2026/projects/Ray/project_template/template.API.py @@ -0,0 +1 @@ +You have exceeded a secondary rate limit. Please wait a few minutes before you try again. For more on scraping GitHub and how it may affect your rights, please review our Terms of Service (https://docs.github.com/en/site-policy/github-terms/github-terms-of-service) diff --git a/class_project/data605/Spring2026/projects/Ray/project_template/template.example.ipynb b/class_project/data605/Spring2026/projects/Ray/project_template/template.example.ipynb new file mode 100644 index 000000000..a2e9aedd7 --- /dev/null +++ b/class_project/data605/Spring2026/projects/Ray/project_template/template.example.ipynb @@ -0,0 +1,198 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "50f78f7e-2dee-45d6-9d37-7a55eeaae283", + "metadata": {}, + "source": [ + "# Template Example Notebook\n", + "\n", + "This is a template notebook. The first heading should be the title of what notebook is about. For example, if it is a project on neo4j tutorial the heading should be `Project Title`.\n", + "\n", + "- Add description of what the notebook does.\n", + "- Point to references, e.g. (neo4j.example.md)\n", + "- Add citations.\n", + "- Keep the notebook flow clear.\n", + "- Comments should be imperative and have a period at the end.\n", + "- Your code should be well commented.\n", + "\n", + "The name of this notebook should in the following format:\n", + "- if the notebook is exploring `pycaret API`, then it is `pycaret.example.ipynb`\n", + "\n", + "Follow the reference to write notebooks in a clear manner: https://github.com/causify-ai/helpers/blob/master/docs/coding/all.jupyter_notebook.how_to_guide.md" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "6226667e-cab5-479c-be6a-6b7d6f580a97", + "metadata": {}, + "outputs": [], + "source": [ + "%load_ext autoreload\n", + "%autoreload 2\n", + "%matplotlib inline" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "8020901a-4bc7-4b73-95e8-aaa462b4fc19", + "metadata": {}, + "outputs": [], + "source": [ + "import logging\n", + "# Import libraries in this section.\n", + "# Avoid imports like import *, from ... import ..., from ... import *, etc.\n", + "\n", + "import helpers.hdbg as hdbg\n", + "import helpers.hnotebook as hnotebo" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "4ecb72b2-b21d-4fb0-ac92-e7174da390e6", + "metadata": { + "lines_to_next_cell": 2 + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[0mWARNING: Running in Jupyter\n", + "INFO > cmd='/venv/lib/python3.12/site-packages/ipykernel_launcher.py -f /home/.local/share/jupyter/runtime/kernel-783e0930-1631-4d64-8bb4-f3a98bb74fcd.json'\n" + ] + } + ], + "source": [ + "hdbg.init_logger(verbosity=logging.INFO)\n", + "\n", + "_LOG = logging.getLogger(__name__)\n", + "\n", + "hnotebo.config_notebook()" + ] + }, + { + "cell_type": "markdown", + "id": "1ede6422-bff2-4f0a-8d28-29a01d4786b2", + "metadata": { + "lines_to_next_cell": 2 + }, + "source": [ + "## Make the notebook flow clear\n", + "Each notebook needs to follow a clear and logical flow, e.g:\n", + "- Load data\n", + "- Compute stats\n", + "- Clean data\n", + "- Compute stats\n", + "- Do analysis\n", + "- Show results\n", + "\n", + "\n", + "\n", + "\n", + "#############################################################################\n", + "Template\n", + "#############################################################################" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "8bbd660d-d22f-44fa-bf53-dd622dee0f53", + "metadata": { + "lines_to_next_cell": 2 + }, + "outputs": [], + "source": [ + "class Template:\n", + " \"\"\"\n", + " Brief imperative description of what the class does in one line, if needed.\n", + " \"\"\"\n", + "\n", + " def __init__(self):\n", + " pass\n", + "\n", + " def method1(self, arg1: int) -> None:\n", + " \"\"\"\n", + " Brief imperative description of what the method does in one line.\n", + "\n", + " You can elaborate more in the method docstring in this section, for e.g. explaining\n", + " the formula/algorithm. Every method/function should have a docstring, typehints and include the\n", + " parameters and return as follows:\n", + "\n", + " :param arg1: description of arg1\n", + " :return: description of return\n", + " \"\"\"\n", + " # Code bloks go here.\n", + " # Make sure to include comments to explain what the code is doing.\n", + " # No empty lines between code blocks.\n", + " pass\n", + "\n", + "\n", + "def template_function(arg1: int) -> None:\n", + " \"\"\"\n", + " Brief imperative description of what the function does in one line.\n", + "\n", + " You can elaborate more in the function docstring in this section, for e.g. explaining\n", + " the formula/algorithm. Every function should have a docstring, typehints and include the\n", + " parameters and return as follows:\n", + "\n", + " :param arg1: description of arg1\n", + " :return: description of return\n", + " \"\"\"\n", + " # Code bloks go here.\n", + " # Make sure to include comments to explain what the code is doing.\n", + " # No empty lines between code blocks.\n", + " pass" + ] + }, + { + "cell_type": "markdown", + "id": "103f6e36-54cf-442c-b137-8091d48805a7", + "metadata": {}, + "source": [ + "## The flow should be highlighted using headings in markdown\n", + "```\n", + "# Level 1\n", + "## Level 2\n", + "### Level 3\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d05d52af-67ba-4a4f-a561-af453e43854f", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "jupytext": { + "formats": "ipynb,py:percent" + }, + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/class_project/data605/Spring2026/projects/Ray/project_template/template.example.py b/class_project/data605/Spring2026/projects/Ray/project_template/template.example.py new file mode 100644 index 000000000..af20ee276 --- /dev/null +++ b/class_project/data605/Spring2026/projects/Ray/project_template/template.example.py @@ -0,0 +1 @@ +You have exceeded a secondary rate limit. Please wait a few minutes before you try again. For more on scraping GitHub and how it may affect your rights, please review our Terms of Service (https://docs.github.com/en/site-policy/github-terms/github-terms-of-service) diff --git a/class_project/data605/Spring2026/projects/Ray/project_template/template_utils.py b/class_project/data605/Spring2026/projects/Ray/project_template/template_utils.py new file mode 100644 index 000000000..f8916102e --- /dev/null +++ b/class_project/data605/Spring2026/projects/Ray/project_template/template_utils.py @@ -0,0 +1,72 @@ +""" +template_utils.py + +This file contains utility functions that support the tutorial notebooks. + +- Notebooks should call these functions instead of writing raw logic inline. +- This helps keep the notebooks clean, modular, and easier to debug. +- Students should implement functions here for data preprocessing, + model setup, evaluation, or any reusable logic. + +Import as: + +import class_project.project_template.template_utils as cpptteut +""" + +import pandas as pd +import logging +from sklearn.model_selection import train_test_split +from pycaret.classification import compare_models + +# ----------------------------------------------------------------------------- +# Logging +# ----------------------------------------------------------------------------- + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +# ----------------------------------------------------------------------------- +# Example 1: Split the dataset into train and test sets +# ----------------------------------------------------------------------------- + + +def split_data(df: pd.DataFrame, target_column: str, test_size: float = 0.2): + """ + Split the dataset into training and testing sets. + + :param df: full dataset + :param target_column: name of the target column + :param test_size: proportion of test data (default = 0.2) + + :return: X_train, X_test, y_train, y_test + """ + logger.info("Splitting data into train and test sets") + X = df.drop(columns=[target_column]) + y = df[target_column] + return train_test_split(X, y, test_size=test_size, random_state=42) + + +# ----------------------------------------------------------------------------- +# Example 2: PyCaret classification pipeline +# ----------------------------------------------------------------------------- + + +def run_pycaret_classification( + df: pd.DataFrame, target_column: str +) -> pd.DataFrame: + """ + Run a basic PyCaret classification experiment. + + :param df: dataset containing features and target + :param target_column: name of the target column + + :return: comparison of top-performing models + """ + logger.info("Initializing PyCaret classification setup") + ... + + logger.info("Comparing models") + results = compare_models() + ... + + return results diff --git a/class_project/data605/Spring2026/projects/Ray/project_template/test/test_docker_all.py b/class_project/data605/Spring2026/projects/Ray/project_template/test/test_docker_all.py new file mode 100644 index 000000000..904cdd7af --- /dev/null +++ b/class_project/data605/Spring2026/projects/Ray/project_template/test/test_docker_all.py @@ -0,0 +1,48 @@ +""" +Run each notebook in class_project/project_template/ inside Docker using docker_cmd.sh. + +Import as: + +import class_project.project_template.test.test_docker_all as tptdal +""" + +import logging + +import pytest + +import helpers.hdocker_tests as hdoctest + +_LOG = logging.getLogger(__name__) + + +# ############################################################################# +# Test_docker +# ############################################################################# + + +class Test_docker(hdoctest.DockerTestCase): + """ + Run all Docker tests for class_project/project_template/. + """ + + _test_file = __file__ + + @pytest.mark.slow + def test1(self) -> None: + """ + Test that template.example.ipynb runs without error inside Docker. + """ + # Prepare inputs. + notebook_name = "template.example.ipynb" + # Run test. + self._helper(notebook_name) + + @pytest.mark.slow + def test2(self) -> None: + """ + Test that template.API.ipynb runs without error inside Docker. + """ + # Prepare inputs. + notebook_name = "template.API.ipynb" + # Run test. + self._helper(notebook_name) diff --git a/class_project/data605/Spring2026/projects/Ray/project_template/utils.sh b/class_project/data605/Spring2026/projects/Ray/project_template/utils.sh new file mode 100644 index 000000000..cc0ed8c4a --- /dev/null +++ b/class_project/data605/Spring2026/projects/Ray/project_template/utils.sh @@ -0,0 +1,607 @@ +#!/bin/bash +# """ +# Utility functions for Docker container management. +# """ + + +# ############################################################################# +# General utilities +# ############################################################################# + + +run() { + # """ + # Execute a command with echo output. + # + # :param cmd: Command string to execute + # :return: Exit status of the executed command + # """ + cmd="$*" + echo "> $cmd" + eval "$cmd" +} + + +enable_verbose_mode() { + # """ + # Enable shell command tracing (set -x) when VERBOSE is set to 1. + # + # Reads the VERBOSE variable set by parse_docker_jupyter_args. + # Call this after parsing args to activate tracing for the rest of the script. + # """ + if [[ $VERBOSE == 1 ]]; then + set -x + fi +} + + +# ############################################################################# +# Argument parsing +# ############################################################################# + + +_print_default_help() { + # """ + # Print usage information and available default options for docker scripts. + # """ + echo "Usage: $(basename $0) [options]" + echo "" + echo "Options:" + echo " -f Force kill existing container with same name before starting" + echo " -h Print this help message and exit" + echo " -v Enable verbose output (set -x)" +} + + +parse_default_args() { + # """ + # Parse default command-line arguments for docker scripts. + # + # Sets VERBOSE and FORCE variables in the caller's scope. Enables set -x + # when -v is passed. Prints help and exits when -h is passed. + # Updates OPTIND so the caller can shift away processed arguments. + # + # :param @: command-line arguments forwarded from the calling script + # """ + VERBOSE=0 + FORCE=0 + while getopts "fhv" flag; do + case "${flag}" in + f) FORCE=1;; + h) _print_default_help; exit 0;; + v) VERBOSE=1;; + *) _print_default_help; exit 1;; + esac + done + enable_verbose_mode +} + + +_print_docker_jupyter_help() { + # """ + # Print usage information and available options for docker_jupyter.sh. + # """ + echo "Usage: $(basename $0) [options]" + echo "" + echo "Launch Jupyter Lab inside a Docker container." + echo "" + echo "Options:" + echo " -f Force kill existing container with same name before starting" + echo " -h Print this help message and exit" + echo " -p PORT Host port to forward to Jupyter Lab (default: 8888)" + echo " -u Enable vim keybindings in Jupyter Lab" + echo " -v Enable verbose output (set -x)" +} + + +parse_docker_jupyter_args() { + # """ + # Parse command-line arguments for docker_jupyter.sh. + # + # Sets JUPYTER_HOST_PORT, JUPYTER_USE_VIM, TARGET_DIR, VERBOSE, FORCE, and + # OLD_CMD_OPTS in the caller's scope. Enables set -x when -v is passed. + # Prints help and exits when -h is passed. + # + # :param @: command-line arguments forwarded from the calling script + # """ + # Set defaults. + JUPYTER_HOST_PORT=8888 + JUPYTER_USE_VIM=0 + VERBOSE=0 + FORCE=0 + # Save original args to pass through to run_jupyter.sh. + OLD_CMD_OPTS="$*" + # Parse options. + while getopts "fhp:uv" flag; do + case "${flag}" in + f) FORCE=1;; + h) _print_docker_jupyter_help; exit 0;; + p) JUPYTER_HOST_PORT=${OPTARG};; # Port for Jupyter Lab. + u) JUPYTER_USE_VIM=1;; # Enable vim bindings. + v) VERBOSE=1;; # Enable verbose output. + *) _print_docker_jupyter_help; exit 1;; + esac + done + # Enable command tracing if verbose mode is requested. + enable_verbose_mode +} + + +# ############################################################################# +# Docker image management +# ############################################################################# + + +get_docker_vars_script() { + # """ + # Load Docker variables from docker_name.sh script. + # + # :param script_path: Path to the script to determine the Docker configuration directory + # :return: Sources REPO_NAME, IMAGE_NAME, and FULL_IMAGE_NAME variables + # """ + local script_path=$1 + # Find the name of the container. + SCRIPT_DIR=$(dirname $script_path) + DOCKER_NAME="$SCRIPT_DIR/docker_name.sh" + if [[ ! -e $SCRIPT_DIR ]]; then + echo "Can't find $DOCKER_NAME" + exit -1 + fi; + source $DOCKER_NAME +} + + +print_docker_vars() { + # """ + # Print current Docker variables to stdout. + # """ + echo "REPO_NAME=$REPO_NAME" + echo "IMAGE_NAME=$IMAGE_NAME" + echo "FULL_IMAGE_NAME=$FULL_IMAGE_NAME" +} + + +build_container_image() { + # """ + # Build a Docker container image. + # + # Supports both single-architecture and multi-architecture builds. + # Creates temporary build directory, copies files, and builds the image. + # + # :param @: Additional options to pass to docker build/buildx build + # """ + echo "# ${FUNCNAME[0]} ..." + FULL_IMAGE_NAME=$REPO_NAME/$IMAGE_NAME + echo "FULL_IMAGE_NAME=$FULL_IMAGE_NAME" + # Prepare build area. + #tar -czh . | docker build $OPTS -t $IMAGE_NAME - + DIR="../tmp.build" + if [[ -d $DIR ]]; then + rm -rf $DIR + fi; + cp -Lr . $DIR || true + # Build container. + echo "DOCKER_BUILDKIT=$DOCKER_BUILDKIT" + echo "DOCKER_BUILD_MULTI_ARCH=$DOCKER_BUILD_MULTI_ARCH" + if [[ $DOCKER_BUILD_MULTI_ARCH != 1 ]]; then + # Build for a single architecture. + echo "Building for current architecture..." + OPTS="--progress plain $@" + (cd $DIR; docker build $OPTS -t $FULL_IMAGE_NAME . 2>&1 | tee ../docker_build.log; exit ${PIPESTATUS[0]}) + else + # Build for multiple architectures. + echo "Building for multiple architectures..." + OPTS="$@" + export DOCKER_CLI_EXPERIMENTAL=enabled + # Create a new builder. + #docker buildx rm --all-inactive --force + #docker buildx create --name mybuilder + #docker buildx use mybuilder + # Use the default builder. + docker buildx use multiarch + docker buildx inspect --bootstrap + # Note that one needs to push to the repo since otherwise it is not + # possible to keep multiple. + (cd $DIR; docker buildx build --push --platform linux/arm64,linux/amd64 $OPTS --tag $FULL_IMAGE_NAME . 2>&1 | tee ../docker_build.log; exit ${PIPESTATUS[0]}) + # Report the status. + docker buildx imagetools inspect $FULL_IMAGE_NAME + fi; + # Report build version. + if [ -f docker_build.version.log ]; then + rm docker_build.version.log + fi + (cd $DIR; docker run --rm -it -v $(pwd):/data $FULL_IMAGE_NAME bash -c "/data/version.sh") 2>&1 | tee docker_build.version.log + # + docker image ls $REPO_NAME/$IMAGE_NAME + rm -rf $DIR + echo "*****************************" + echo "SUCCESS" + echo "*****************************" +} + + +remove_container_image() { + # """ + # Remove Docker container image(s) matching the current configuration. + # """ + echo "# ${FUNCNAME[0]} ..." + FULL_IMAGE_NAME=$REPO_NAME/$IMAGE_NAME + echo "FULL_IMAGE_NAME=$FULL_IMAGE_NAME" + docker image ls | grep $FULL_IMAGE_NAME + docker image ls | grep $FULL_IMAGE_NAME | awk '{print $1}' | xargs -n 1 -t docker image rm -f + docker image ls + echo "${FUNCNAME[0]} ... done" +} + + +push_container_image() { + # """ + # Push Docker container image to registry. + # + # Authenticates using credentials from ~/.docker/passwd.$REPO_NAME.txt. + # """ + echo "# ${FUNCNAME[0]} ..." + FULL_IMAGE_NAME=$REPO_NAME/$IMAGE_NAME + echo "FULL_IMAGE_NAME=$FULL_IMAGE_NAME" + docker login --username $REPO_NAME --password-stdin <~/.docker/passwd.$REPO_NAME.txt + docker images $FULL_IMAGE_NAME + docker push $FULL_IMAGE_NAME + echo "${FUNCNAME[0]} ... done" +} + + +pull_container_image() { + # """ + # Pull Docker container image from registry. + # """ + echo "# ${FUNCNAME[0]} ..." + FULL_IMAGE_NAME=$REPO_NAME/$IMAGE_NAME + echo "FULL_IMAGE_NAME=$FULL_IMAGE_NAME" + docker pull $FULL_IMAGE_NAME + echo "${FUNCNAME[0]} ... done" +} + + +# ############################################################################# +# Docker container management +# ############################################################################# + + +kill_container() { + # """ + # Kill and remove Docker container(s) matching the current configuration. + # """ + echo "# ${FUNCNAME[0]} ..." + FULL_IMAGE_NAME=$REPO_NAME/$IMAGE_NAME + echo "FULL_IMAGE_NAME=$FULL_IMAGE_NAME" + docker container ls + # + CONTAINER_ID=$(docker container ls -a | grep $FULL_IMAGE_NAME | awk '{print $1}') + echo "CONTAINER_ID=$CONTAINER_ID" + if [[ ! -z $CONTAINER_ID ]]; then + docker container rm -f $CONTAINER_ID + docker container ls + fi; + echo "${FUNCNAME[0]} ... done" +} + + +kill_container_by_name() { + # """ + # Kill and remove a Docker container by its name. + # + # :param container_name: Name of the container to kill + # """ + local container_name=$1 + echo "# ${FUNCNAME[0]}: $container_name" + # Check if container exists (running or stopped). + local container_id=$(docker container ls -a --filter "name=^${container_name}$" --format "{{.ID}}") + if [[ -n $container_id ]]; then + echo "Killing container: $container_name (ID: $container_id)" + docker container rm -f $container_id + else + echo "Container '$container_name' not found" + fi + echo "${FUNCNAME[0]} ... done" +} + + +exec_container() { + # """ + # Execute bash shell in running Docker container. + # + # Opens an interactive bash session in the first container matching the + # current configuration. + # """ + echo "# ${FUNCNAME[0]} ..." + FULL_IMAGE_NAME=$REPO_NAME/$IMAGE_NAME + echo "FULL_IMAGE_NAME=$FULL_IMAGE_NAME" + docker container ls + # + CONTAINER_ID=$(docker container ls -a | grep $FULL_IMAGE_NAME | awk '{print $1}') + echo "CONTAINER_ID=$CONTAINER_ID" + docker exec -it $CONTAINER_ID bash + echo "${FUNCNAME[0]} ... done" +} + + +# ############################################################################# +# Docker common options +# ############################################################################# + + +get_docker_common_options() { + # """ + # Return docker run options common to all container types. + # + # Includes volume mount for the git root, plus environment variables for + # PYTHONPATH and host OS name. + # + # :return: docker run options string with volume mounts and env vars + # """ + echo "-v $GIT_ROOT:/git_root \ + -e PYTHONPATH=/git_root:/git_root/helpers_root:/git_root/msml610/tutorials \ + -e CSFY_GIT_ROOT_PATH=/git_root \ + -e CSFY_HOST_OS_NAME=$(uname -s) \ + -e CSFY_HOST_NAME=$(uname -n)" +} + + +# ############################################################################# +# Docker bash +# ############################################################################# + + +get_docker_bash_command() { + # """ + # Return the base docker run command for an interactive bash shell. + # + # :return: docker run command string with --rm and -ti flags + # """ + if [ -t 0 ]; then + echo "docker run --rm -ti" + else + echo "docker run --rm -i" + fi +} + + +get_docker_bash_options() { + # """ + # Return docker run options for a Docker container. + # + # :param container_name: Name for the Docker container + # :param port: Port number to forward (optional, skipped if empty) + # :param extra_opts: Additional docker run options (optional) + # :return: docker run options string with name, volume mounts, and env vars + # """ + local container_name=$1 + local port=$2 + local extra_opts=$3 + local port_opt="" + if [[ -n $port ]]; then + port_opt="-p $port:$port" + fi + echo "--name $container_name \ + $port_opt \ + $extra_opts \ + $(get_docker_common_options)" +} + + +# ############################################################################# +# Docker cmd +# ############################################################################# + + +get_docker_cmd_command() { + # """ + # Return the base docker run command for executing a non-interactive command. + # + # :return: docker run command string with --rm and -i flags + # """ + echo "docker run --rm -i" +} + + +# ############################################################################# +# Docker Jupyter +# ############################################################################# + + +get_docker_jupyter_command() { + # """ + # Return the base docker run command for running Jupyter Lab interactively. + # + # :return: docker run command string with --rm and -ti flags (if TTY available) + # """ + local docker_cmd="docker run --rm" + # Add interactive and TTY flags only if stdin is a TTY. + if [[ -t 0 ]]; then + docker_cmd="$docker_cmd -ti" + fi + echo "$docker_cmd" +} + + +get_docker_jupyter_options() { + # """ + # Return docker run options for a Jupyter Lab container. + # + # :param container_name: Name for the Docker container + # :param host_port: Host port to forward to container port 8888 + # :param jupyter_use_vim: 0 or 1 to enable vim bindings + # :return: docker run options string + # """ + local container_name=$1 + local host_port=$2 + local jupyter_use_vim=$3 + # Run as the current user when user is saggese. + if [[ "$(whoami)" == "saggese" ]]; then + echo "Overwriting jupyter_use_vim since user='saggese'" >&2 + jupyter_use_vim=1 + fi + echo "--name $container_name \ + -p $host_port:8888 \ + $(get_docker_common_options) \ + -e JUPYTER_USE_VIM=$jupyter_use_vim" +} + + +configure_jupyter_vim_keybindings() { + # """ + # Configure JupyterLab vim keybindings based on JUPYTER_USE_VIM env var. + # + # Reads JUPYTER_USE_VIM; if 1, verifies jupyterlab_vim is installed and + # writes enabled settings; otherwise writes disabled settings. + # """ + mkdir -p ~/.jupyter/lab/user-settings/@axlair/jupyterlab_vim + if [[ $JUPYTER_USE_VIM == 1 ]]; then + # Check that jupyterlab_vim is installed before trying to enable it. + if ! pip show jupyterlab_vim > /dev/null 2>&1; then + echo "ERROR: jupyterlab_vim is not installed but vim bindings were requested." + echo "Install it with: pip install jupyterlab_vim" + exit 1 + fi + echo "Enabling vim." + cat < ~/.jupyter/lab/user-settings/\@axlair/jupyterlab_vim/plugin.jupyterlab-settings +{ + "enabled": true, + "enabledInEditors": true, + "extraKeybindings": [], + "autosaveInterval": 6 +} +EOF + else + echo "Disabling vim." + cat < ~/.jupyter/lab/user-settings/\@axlair/jupyterlab_vim/plugin.jupyterlab-settings +{ + "enabled": false, + "enabledInEditors": false, + "extraKeybindings": [], + "autosaveInterval": 6 +} +EOF + fi; +} + + +configure_jupyter_notifications() { + # """ + # Disable JupyterLab news fetching and update checks. + # """ + mkdir -p ~/.jupyter/lab/user-settings/@jupyterlab/apputils-extension + cat < ~/.jupyter/lab/user-settings/\@jupyterlab/apputils-extension/notification.jupyterlab-settings +{ + // Notifications + // @jupyterlab/apputils-extension:notification + // Notifications settings. + + // Fetch official Jupyter news + // Whether to fetch news from the Jupyter news feed. If Always (`true`), it will make a request to a website. + "fetchNews": "false", + "checkForUpdates": false +} +EOF +} + + +configure_jupyter_autosave() { + # """ + # Configure JupyterLab global autosave interval to 6 seconds. + # """ + mkdir -p ~/.jupyter/lab/user-settings/@jupyterlab/docmanager-extension + cat < ~/.jupyter/lab/user-settings/\@jupyterlab/docmanager-extension/plugin.jupyterlab-settings +{ + "autosaveInterval": 6 +} +EOF +} + + +check_jupytext_installed() { + # """ + # Verify that jupytext is installed before starting Jupyter Lab. + # + # Jupytext is required for pair notebook/Python file functionality. + # Exits with error if jupytext is not installed. + # """ + if ! pip show jupytext > /dev/null 2>&1; then + echo "ERROR: jupytext is not installed but is required to run Jupyter Lab." + echo "Install it with: pip install jupytext" + exit 1 + fi +} + + +setup_jupyter_environment() { + # """ + # Configure Jupyter Lab environment before launching. + # + # Performs all necessary setup steps: + # - Configure vim keybindings + # - Disable notifications + # - Configure autosave interval + # - Verify jupytext is installed + # """ + configure_jupyter_vim_keybindings + configure_jupyter_notifications + configure_jupyter_autosave + check_jupytext_installed +} + + +get_jupyter_args() { + # """ + # Print the standard Jupyter Lab command-line arguments. + # + # :return: space-separated Jupyter Lab args for port 8888 with no browser, + # allow root, and no authentication + # """ + echo "--port=8888 --no-browser --ip=0.0.0.0 --allow-root --ServerApp.token='' --ServerApp.password=''" +} + + +get_run_jupyter_cmd() { + # """ + # Return the command to run run_jupyter.sh inside a container. + # + # Computes the script's path relative to GIT_ROOT and builds the + # corresponding /git_root/... path used inside the container. + # + # :param script_path: path of the calling script (pass ${BASH_SOURCE[0]}) + # :param cmd_opts: options to forward to run_jupyter.sh + # :return: full command string to run run_jupyter.sh + # """ + local script_path=$1 + local cmd_opts=$2 + local script_dir + script_dir=$(cd "$(dirname "$script_path")" && pwd) + local rel_dir="${script_dir#${GIT_ROOT}/}" + echo "/git_root/${rel_dir}/run_jupyter.sh $cmd_opts" +} + + +list_and_inspect_docker_image() { + # """ + # List available Docker images and inspect their architecture. + # + # Lists all images matching FULL_IMAGE_NAME and attempts to inspect + # their architecture using docker manifest inspect. + # """ + run "docker image ls $FULL_IMAGE_NAME" + (docker manifest inspect $FULL_IMAGE_NAME | grep arch) || true +} + + +kill_existing_container_if_forced() { + # """ + # Kill existing container if FORCE flag is set. + # + # If FORCE is set to 1, kills and removes the container with name + # CONTAINER_NAME. This is typically set by the -f flag. + # """ + if [[ $FORCE == 1 ]]; then + kill_container_by_name $CONTAINER_NAME + fi +} diff --git a/class_project/data605/Spring2026/projects/Ray/project_template/version.sh b/class_project/data605/Spring2026/projects/Ray/project_template/version.sh new file mode 100644 index 000000000..af20ee276 --- /dev/null +++ b/class_project/data605/Spring2026/projects/Ray/project_template/version.sh @@ -0,0 +1 @@ +You have exceeded a secondary rate limit. Please wait a few minutes before you try again. For more on scraping GitHub and how it may affect your rights, please review our Terms of Service (https://docs.github.com/en/site-policy/github-terms/github-terms-of-service) diff --git a/class_project/data605/Spring2026/projects/UmdTask464_DATA605_Spring2026_Ray_Housing_Price_Prediction/.dockerignore b/class_project/data605/Spring2026/projects/UmdTask464_DATA605_Spring2026_Ray_Housing_Price_Prediction/.dockerignore new file mode 100644 index 000000000..fd85b2584 --- /dev/null +++ b/class_project/data605/Spring2026/projects/UmdTask464_DATA605_Spring2026_Ray_Housing_Price_Prediction/.dockerignore @@ -0,0 +1,143 @@ +# Exclude files from Docker build context. This prevents unnecessary files from +# being sent to Docker daemon, reducing build time and image size. + +# Python artifacts +__pycache__/ +*.pyc +*.pyo +*.pyd +*.egg-info/ + +# Virtual environments +venv/ +.venv/ +env/ +.env +.envrc +client_venv.helpers/ +ENV/ + +# Jupyter +.ipynb_checkpoints/ +.jupyter/ + +# Build artifacts +build/ +dist/ +*.eggs/ +.eggs/ + +# Cache and temporary files +*.log +*.tmp +*.cache +.pytest_cache/ +.mypy_cache/ +.coverage +htmlcov/ + +# Git and version control +.git/ +.gitignore +.gitattributes +.github/ + +# Docker build scripts (not needed at runtime) +docker_build.sh +docker_push.sh +docker_clean.sh +docker_exec.sh +docker_cmd.sh +docker_bash.sh +docker_jupyter.sh +docker_name.sh +run_jupyter.sh +Dockerfile.* +.dockerignore + +# Documentation +README.md +README.admin.md +docs/ +*.md +CHANGELOG.md +LICENSE + +# Configuration and secrets +.env.* +.env.local +.env.development +.env.production +.DS_Store +Thumbs.db + +# Shell configuration +.bashrc +.bash_history +.zshrc + +# Large data files (mount via volume instead) +data/ +*.csv +*.pkl +*.h5 +*.parquet +*.feather +*.arrow +*.npy +*.npz + +# Generated images +*.png +*.jpg +*.jpeg +*.gif +*.svg +*.pdf + +# Test files and examples +tests/ +test_* +*_test.py +tutorials/ +examples/ + +# IDE and editor files +.vscode/ +.idea/ +*.swp +*.swo +*~ +.project +.pydevproject +.settings/ +*.iml +.sublime-project +.sublime-workspace + +# Node and frontend (if applicable) +node_modules/ +npm-debug.log +yarn-error.log +.npm + +# Requirements management +requirements.in +Pipfile +Pipfile.lock +poetry.lock +setup.py +setup.cfg + +# CI/CD configuration +.gitlab-ci.yml +.travis.yml +Jenkinsfile +.circleci/ + +# Miscellaneous +*.bak +.venv.bak/ +*.whl +*.tar.gz +*.zip diff --git a/class_project/data605/Spring2026/projects/UmdTask464_DATA605_Spring2026_Ray_Housing_Price_Prediction/.gitignore b/class_project/data605/Spring2026/projects/UmdTask464_DATA605_Spring2026_Ray_Housing_Price_Prediction/.gitignore new file mode 100644 index 000000000..b6548ff39 --- /dev/null +++ b/class_project/data605/Spring2026/projects/UmdTask464_DATA605_Spring2026_Ray_Housing_Price_Prediction/.gitignore @@ -0,0 +1,22 @@ +# Jupyter +.ipynb_checkpoints/ +.jupyter/ + +# Python +__pycache__/ +*.pyc +*.pyo + +# Docker build artifacts +docker_build.version.log +version.log + +# OS +.DS_Store +Thumbs.db + +# Trained model artifacts (regenerated by running the notebook) +*.pkl + +# JupyterLab trash directory (leaks from container) +.Trash-*/ \ No newline at end of file diff --git a/class_project/data605/Spring2026/projects/UmdTask464_DATA605_Spring2026_Ray_Housing_Price_Prediction/Dockerfile b/class_project/data605/Spring2026/projects/UmdTask464_DATA605_Spring2026_Ray_Housing_Price_Prediction/Dockerfile new file mode 100644 index 000000000..cc8f18f2f --- /dev/null +++ b/class_project/data605/Spring2026/projects/UmdTask464_DATA605_Spring2026_Ray_Housing_Price_Prediction/Dockerfile @@ -0,0 +1,28 @@ +# Use Python 3.12 slim (already has Python and pip). +FROM python:3.12-slim + +# Avoid interactive prompts during apt operations. +ENV DEBIAN_FRONTEND=noninteractive + +# Install CA certificates (needed for HTTPS). +RUN apt-get update && apt-get install -y \ + ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +# Install project specific packages. +RUN mkdir -p /install +COPY requirements.txt /install/requirements.txt +RUN pip install --upgrade pip && \ + pip install --no-cache-dir jupyterlab jupyterlab_vim jupytext -r /install/requirements.txt + +# Config. +COPY etc_sudoers /install/ +COPY etc_sudoers /etc/sudoers +COPY bashrc /root/.bashrc + +# Report package versions. +COPY version.sh /install/ +RUN /install/version.sh 2>&1 | tee version.log + +# Jupyter. +EXPOSE 8888 diff --git a/class_project/data605/Spring2026/projects/UmdTask464_DATA605_Spring2026_Ray_Housing_Price_Prediction/README.md b/class_project/data605/Spring2026/projects/UmdTask464_DATA605_Spring2026_Ray_Housing_Price_Prediction/README.md new file mode 100644 index 000000000..2cc81b98f --- /dev/null +++ b/class_project/data605/Spring2026/projects/UmdTask464_DATA605_Spring2026_Ray_Housing_Price_Prediction/README.md @@ -0,0 +1,236 @@ +# Ray Housing Price Prediction + +A scalable end-to-end machine-learning pipeline that predicts California +median house values using **Ray** for distributed data loading, parallel +training, hyperparameter tuning, and model serving. + +This project is a tutorial-style introduction to Ray's core APIs in the +context of a realistic regression problem: a curious computer scientist +should be able to read this README and the two notebooks and understand +what Ray offers in roughly 60 minutes. + +--- + +## Table of Contents + +1. [What is Ray?](#what-is-ray) +2. [Project Objective](#project-objective) +3. [File Layout](#file-layout) +4. [Quick Start](#quick-start) +5. [Running the Notebooks](#running-the-notebooks) +6. [Querying the Deployed Model](#querying-the-deployed-model) +7. [Results](#results) +8. [Architectural Decisions](#architectural-decisions) +9. [References](#references) + +--- + +## What is Ray? + +[Ray](https://www.ray.io/) is an open-source framework for distributed +Python. It provides a single API for scaling code across multiple cores +and machines, plus a stack of higher-level libraries built on top of +that core: + +| Library | What it does | +| ------------- | -------------------------------------------------- | +| **Ray Core** | Task and actor parallelism (`@ray.remote`) | +| **Ray Data** | Distributed data loading and preprocessing | +| **Ray Tune** | Distributed hyperparameter search | +| **Ray Serve** | Scalable model serving as a REST endpoint | + +This project uses all four to take a problem from raw data to a live API. + +## Project Objective + +Predict the **median house value** in California census tracts from +features such as median income, average rooms, location, and population. +The dataset is the standard scikit-learn `fetch_california_housing` dump +(20,640 rows x 8 features), so the project is reproducible without any +authentication or external download. + +The pipeline: + +1. Load and wrap the dataset with **Ray Data** +2. Train a baseline `RandomForestRegressor` with scikit-learn +3. Run multiple training jobs in parallel with **Ray Core** (`@ray.remote`) +4. Search the hyperparameter space with **Ray Tune** +5. Deploy the best model as a REST API with **Ray Serve** + +## File Layout + +``` +. +├── Dockerfile # python:3.12-slim base + Ray + ML stack +├── .dockerignore +├── .gitignore +├── README.md # this file +├── requirements.txt # pinned Python dependencies +├── ray_utils.py # load_data() helper (uses Ray Data) +├── ray_housing.API.ipynb # Tour of Ray's native APIs +├── ray_housing.example.ipynb # End-to-end housing pipeline +│ +├── docker_build.sh # Build the project's Docker image +├── docker_bash.sh # Open a bash shell inside the container +├── docker_jupyter.sh # Start JupyterLab inside the container +├── docker_clean.sh # Remove the project's Docker image +├── docker_cmd.sh # Run an arbitrary command in the container +├── docker_exec.sh # Attach to a running container +├── docker_push.sh # Push the image to a registry +├── docker_name.sh # Image-naming configuration +├── run_jupyter.sh # JupyterLab launcher (called inside Docker) +├── version.sh # Logs Python/pip/Jupyter versions during build +├── bashrc # Container shell configuration +└── etc_sudoers # Container sudoers file +``` + +The `docker_*.sh` scripts and the support files (`bashrc`, `etc_sudoers`, +`version.sh`, `.dockerignore`, `Dockerfile`) come from the canonical +class template at `class_project/project_template/`. Only `Dockerfile`, +`docker_name.sh`, and `requirements.txt` were customized for this +project. + +## Quick Start + +### Prerequisites + +- [Docker Desktop](https://www.docker.com/products/docker-desktop/) + (or any working Docker daemon) running on your machine +- A Bash-compatible shell (Git Bash on Windows, any terminal on macOS / Linux) + +### Build the image + +From this project directory: + +```bash +./docker_build.sh +``` + +The first build takes 5-10 minutes. It downloads `python:3.12-slim`, +installs Ray, scikit-learn, JupyterLab and all transitive dependencies, +and produces an image named: + +``` +gpsaggese/umd_data605_spring2026_ray_housing_price_prediction:latest +``` + +The final image is roughly 1.7 GB. + +### Verify the build + +```bash +docker images gpsaggese/umd_data605_spring2026_ray_housing_price_prediction +``` + +You should see one row with size around 1.7 GB. + +## Running the Notebooks + +### Start JupyterLab inside the container + +```bash +./docker_jupyter.sh +``` + +This: + +- Starts a container from the project image +- Mounts the current directory at `/data` inside the container +- Launches JupyterLab on port 8888 with no token / password (development mode) + +Open in your browser. You'll see this project's +files in the file browser. + +### Notebook tour + +- **`ray_housing.API.ipynb`** - A short, didactic walk through Ray's + core APIs in isolation: `@ray.remote`, `ray.data.from_pandas`, + `tune.run`, `@serve.deployment`. Read this first if you've never used + Ray. +- **`ray_housing.example.ipynb`** - The applied end-to-end pipeline: + load housing data, train a baseline model, distribute multiple + training runs, tune hyperparameters, and deploy the best model as a + REST API. + +Run the cells top-to-bottom. The Ray Tune section takes about 2-3 minutes; +everything else is fast. + +## Querying the Deployed Model + +Once `ray_housing.example.ipynb` has run the Ray Serve cell, the model +listens on port 8000 inside the container. Because the container maps +that port to the host, you can query it with `curl` or `requests`: + +```bash +curl -X POST http://127.0.0.1:8000/ \ + -H "Content-Type: application/json" \ + -d '{"features": [8.3252, 41.0, 6.984, 1.024, 322.0, 2.556, 37.88, -122.23]}' +``` + +Expected response: + +```json +{"prediction": 4.32} +``` + +The eight features, in order, are: +`MedInc, HouseAge, AveRooms, AveBedrms, Population, AveOccup, Latitude, Longitude`. +The prediction is the median house value in **hundreds of thousands of +dollars** (so `4.32` is about $432,000), matching the `MedHouseVal` units in +the original dataset. + +## Results + +Baseline `RandomForestRegressor` (`n_estimators=100`, default depth): + +| Metric | Value | +| ------- | ----- | +| RMSE | ~0.51 | +| R^2 | ~0.81 | + +After Ray Tune sweeps over `n_estimators` in {50, 100, 200} and +`max_depth` in {5, 10, 20}, the best configuration improves RMSE to +roughly 0.49 (final numbers depend on the random seed of each Tune +trial). + +The tuned model is the one served by the Ray Serve endpoint. + +## Architectural Decisions + +A few non-obvious choices worth flagging for graders and future +maintainers. + +**Base image: `python:3.12-slim`.** The class template offers Ubuntu, +Python-slim, and `uv`-based variants. Slim was chosen because this +project doesn't need system tools like `postgresql-client` or `graphviz`, +and the smaller base shaves both build time and final image size. + +**Ray version: `2.49.0`.** Pinned for reproducibility. Ray < 2.31 has no +Python 3.12 wheels; Ray 2.49 is recent enough to be supported and stable +enough to have known-good behavior with the Tune and Serve APIs used +here. + +**Ray Data inside `load_data()`.** The brief asks the project to use +Ray Data for loading and preprocessing. `ray_utils.load_data()` calls +`ray.data.from_pandas(...)` and returns the materialized DataFrame, so +the rest of the pipeline can stay in pandas-land. A future iteration +could keep the data as a Ray `Dataset` and use `.map_batches()` for +preprocessing. + +**Single REST endpoint.** Ray Serve makes multi-endpoint deployments +trivial, but this project exposes one POST handler at `/` for clarity. +The class returns JSON with the predicted value and nothing else. + +## References + +- Ray documentation: +- California Housing dataset: + [`sklearn.datasets.fetch_california_housing`](https://scikit-learn.org/stable/modules/generated/sklearn.datasets.fetch_california_housing.html) +- Class project template guide: + `class_project/project_template/README.md` in this repo +- Project guidelines: + `class_project/data605/Spring2026/Class_Project_Guidelines.md` + +--- + +*Project tag: `UmdTask464_DATA605_Spring2026_Ray_Housing_Price_Prediction`* diff --git a/class_project/data605/Spring2026/projects/UmdTask464_DATA605_Spring2026_Ray_Housing_Price_Prediction/bashrc b/class_project/data605/Spring2026/projects/UmdTask464_DATA605_Spring2026_Ray_Housing_Price_Prediction/bashrc new file mode 100644 index 000000000..4b7ff4c49 --- /dev/null +++ b/class_project/data605/Spring2026/projects/UmdTask464_DATA605_Spring2026_Ray_Housing_Price_Prediction/bashrc @@ -0,0 +1 @@ +set -o vi diff --git a/class_project/data605/Spring2026/projects/UmdTask464_DATA605_Spring2026_Ray_Housing_Price_Prediction/docker_bash.sh b/class_project/data605/Spring2026/projects/UmdTask464_DATA605_Spring2026_Ray_Housing_Price_Prediction/docker_bash.sh new file mode 100644 index 000000000..0025e81f4 --- /dev/null +++ b/class_project/data605/Spring2026/projects/UmdTask464_DATA605_Spring2026_Ray_Housing_Price_Prediction/docker_bash.sh @@ -0,0 +1,34 @@ +#!/bin/bash +# """ +# This script launches a Docker container with an interactive bash shell for +# development. +# """ + +# Exit immediately if any command exits with a non-zero status. +set -e + +# Import the utility functions from the project template. +GIT_ROOT=$(git rev-parse --show-toplevel) +source $GIT_ROOT/class_project/project_template/utils.sh + +# Parse default args (-h, -v) and enable set -x if -v is passed. +parse_default_args "$@" + +# Load Docker configuration variables for this script. +get_docker_vars_script ${BASH_SOURCE[0]} +source $DOCKER_NAME +print_docker_vars + +# List the available Docker images matching the expected image name. +run "docker image ls $FULL_IMAGE_NAME" + +# Configure and run the Docker container with interactive bash shell. +# - Container is removed automatically on exit (--rm) +# - Interactive mode with TTY allocation (-ti) +# - Port forwarding for Jupyter or other services +# - Git root mounted to /git_root inside container +CONTAINER_NAME=${IMAGE_NAME}_bash +PORT= +DOCKER_CMD=$(get_docker_bash_command) +DOCKER_CMD_OPTS=$(get_docker_bash_options $CONTAINER_NAME $PORT) +run "$DOCKER_CMD $DOCKER_CMD_OPTS $FULL_IMAGE_NAME" diff --git a/class_project/data605/Spring2026/projects/UmdTask464_DATA605_Spring2026_Ray_Housing_Price_Prediction/docker_build.sh b/class_project/data605/Spring2026/projects/UmdTask464_DATA605_Spring2026_Ray_Housing_Price_Prediction/docker_build.sh new file mode 100644 index 000000000..5b0957a99 --- /dev/null +++ b/class_project/data605/Spring2026/projects/UmdTask464_DATA605_Spring2026_Ray_Housing_Price_Prediction/docker_build.sh @@ -0,0 +1,40 @@ +#!/bin/bash +# """ +# Build a Docker container image for the project. +# +# This script sets up the build environment with error handling and command +# tracing, loads Docker configuration from docker_name.sh, and builds the +# Docker image using the build_container_image utility function. It supports +# both single-architecture and multi-architecture builds via the +# DOCKER_BUILD_MULTI_ARCH environment variable. +# """ + +# Exit immediately if any command exits with a non-zero status. +set -e + +# Import the utility functions. +GIT_ROOT=$(git rev-parse --show-toplevel) +source $GIT_ROOT/class_project/project_template/utils.sh + +# Parse default args (-h, -v) and enable set -x if -v is passed. +# Shift processed option flags so remaining args are passed to the build. +parse_default_args "$@" +shift $((OPTIND-1)) + +# Load Docker configuration variables (REPO_NAME, IMAGE_NAME, FULL_IMAGE_NAME). +get_docker_vars_script ${BASH_SOURCE[0]} +source $DOCKER_NAME +print_docker_vars + +# Configure Docker build settings. +# Enable BuildKit for improved build performance and features. +export DOCKER_BUILDKIT=1 +#export DOCKER_BUILDKIT=0 + +# Configure single-architecture build (set to 1 for multi-arch build). +#export DOCKER_BUILD_MULTI_ARCH=1 +export DOCKER_BUILD_MULTI_ARCH=0 + +# Build the container image. +# Pass extra arguments (e.g., --no-cache) via command line after -v. +build_container_image "$@" diff --git a/class_project/data605/Spring2026/projects/UmdTask464_DATA605_Spring2026_Ray_Housing_Price_Prediction/docker_clean.sh b/class_project/data605/Spring2026/projects/UmdTask464_DATA605_Spring2026_Ray_Housing_Price_Prediction/docker_clean.sh new file mode 100644 index 000000000..7e40839ae --- /dev/null +++ b/class_project/data605/Spring2026/projects/UmdTask464_DATA605_Spring2026_Ray_Housing_Price_Prediction/docker_clean.sh @@ -0,0 +1,26 @@ +#!/bin/bash +# """ +# Remove Docker container image for the project. +# +# This script cleans up Docker images by removing the container image +# matching the project configuration. Useful for freeing disk space or +# ensuring a fresh build. +# """ + +# Exit immediately if any command exits with a non-zero status. +set -e + +# Import the utility functions. +GIT_ROOT=$(git rev-parse --show-toplevel) +source $GIT_ROOT/class_project/project_template/utils.sh + +# Parse default args (-h, -v) and enable set -x if -v is passed. +parse_default_args "$@" + +# Load Docker configuration variables for this script. +get_docker_vars_script ${BASH_SOURCE[0]} +source $DOCKER_NAME +print_docker_vars + +# Remove the container image. +remove_container_image diff --git a/class_project/data605/Spring2026/projects/UmdTask464_DATA605_Spring2026_Ray_Housing_Price_Prediction/docker_cmd.sh b/class_project/data605/Spring2026/projects/UmdTask464_DATA605_Spring2026_Ray_Housing_Price_Prediction/docker_cmd.sh new file mode 100644 index 000000000..906d7a77b --- /dev/null +++ b/class_project/data605/Spring2026/projects/UmdTask464_DATA605_Spring2026_Ray_Housing_Price_Prediction/docker_cmd.sh @@ -0,0 +1,41 @@ +#!/bin/bash +# """ +# Execute a command in a Docker container. +# +# This script runs a specified command inside a new Docker container instance. +# The container is removed automatically after the command completes. The +# git root is mounted to /git_root inside the container. +# """ + +# Exit immediately if any command exits with a non-zero status. +set -e + +# Import the utility functions. +GIT_ROOT=$(git rev-parse --show-toplevel) +source $GIT_ROOT/class_project/project_template/utils.sh + +# Parse default args (-h, -v) and enable set -x if -v is passed. +# Shift processed option flags so remaining args form the command. +parse_default_args "$@" +shift $((OPTIND-1)) + +# Capture the command to execute from remaining arguments. +CMD="$@" +echo "Executing: '$CMD'" + +# Load Docker configuration variables for this script. +get_docker_vars_script ${BASH_SOURCE[0]} +source $DOCKER_NAME +print_docker_vars + +# List available Docker images matching the expected image name. +run "docker image ls $FULL_IMAGE_NAME" +#(docker manifest inspect $FULL_IMAGE_NAME | grep arch) || true + +# Configure and run the Docker container with the specified command. +CONTAINER_NAME=$IMAGE_NAME +DOCKER_CMD=$(get_docker_cmd_command) +PORT="" +DOCKER_RUN_OPTS="" +DOCKER_CMD_OPTS=$(get_docker_bash_options $CONTAINER_NAME $PORT $DOCKER_RUN_OPTS) +run "$DOCKER_CMD $DOCKER_CMD_OPTS $FULL_IMAGE_NAME bash -c '$CMD'" diff --git a/class_project/data605/Spring2026/projects/UmdTask464_DATA605_Spring2026_Ray_Housing_Price_Prediction/docker_exec.sh b/class_project/data605/Spring2026/projects/UmdTask464_DATA605_Spring2026_Ray_Housing_Price_Prediction/docker_exec.sh new file mode 100644 index 000000000..24f8e401a --- /dev/null +++ b/class_project/data605/Spring2026/projects/UmdTask464_DATA605_Spring2026_Ray_Housing_Price_Prediction/docker_exec.sh @@ -0,0 +1,25 @@ +#!/bin/bash +# """ +# Execute a bash shell in a running Docker container. +# +# This script connects to an already running Docker container and opens an +# interactive bash session for debugging or inspection purposes. +# """ + +# Exit immediately if any command exits with a non-zero status. +set -e + +# Import the utility functions. +GIT_ROOT=$(git rev-parse --show-toplevel) +source $GIT_ROOT/class_project/project_template/utils.sh + +# Parse default args (-h, -v) and enable set -x if -v is passed. +parse_default_args "$@" + +# Load Docker configuration variables for this script. +get_docker_vars_script ${BASH_SOURCE[0]} +source $DOCKER_NAME +print_docker_vars + +# Execute bash shell in the running container. +exec_container diff --git a/class_project/data605/Spring2026/projects/UmdTask464_DATA605_Spring2026_Ray_Housing_Price_Prediction/docker_jupyter.sh b/class_project/data605/Spring2026/projects/UmdTask464_DATA605_Spring2026_Ray_Housing_Price_Prediction/docker_jupyter.sh new file mode 100644 index 000000000..1a60dfd3a --- /dev/null +++ b/class_project/data605/Spring2026/projects/UmdTask464_DATA605_Spring2026_Ray_Housing_Price_Prediction/docker_jupyter.sh @@ -0,0 +1,39 @@ +#!/bin/bash +# """ +# Execute Jupyter Lab in a Docker container. +# +# This script launches a Docker container running Jupyter Lab with +# configurable port, directory mounting, and vim bindings. It passes +# command-line options to the run_jupyter.sh script inside the container. +# +# Usage: +# > docker_jupyter.sh [options] +# """ + +# Exit immediately if any command exits with a non-zero status. +set -e + +# Import the utility functions. +GIT_ROOT=$(git rev-parse --show-toplevel) +source $GIT_ROOT/class_project/project_template/utils.sh + +# Parse command-line options and set Jupyter configuration variables. +parse_docker_jupyter_args "$@" + +# Load Docker configuration variables for this script. +get_docker_vars_script ${BASH_SOURCE[0]} +source $DOCKER_NAME +print_docker_vars + +# List available Docker images and inspect architecture. +list_and_inspect_docker_image + +# Run the Docker container with Jupyter Lab. +CMD=$(get_run_jupyter_cmd "${BASH_SOURCE[0]}" "$OLD_CMD_OPTS") +CONTAINER_NAME=$IMAGE_NAME +# Kill existing container if -f flag is set. +kill_existing_container_if_forced + +DOCKER_CMD=$(get_docker_jupyter_command) +DOCKER_CMD_OPTS=$(get_docker_jupyter_options $CONTAINER_NAME $JUPYTER_HOST_PORT $JUPYTER_USE_VIM) +run "$DOCKER_CMD $DOCKER_CMD_OPTS $FULL_IMAGE_NAME $CMD" diff --git a/class_project/data605/Spring2026/projects/UmdTask464_DATA605_Spring2026_Ray_Housing_Price_Prediction/docker_name.sh b/class_project/data605/Spring2026/projects/UmdTask464_DATA605_Spring2026_Ray_Housing_Price_Prediction/docker_name.sh new file mode 100644 index 000000000..93904712f --- /dev/null +++ b/class_project/data605/Spring2026/projects/UmdTask464_DATA605_Spring2026_Ray_Housing_Price_Prediction/docker_name.sh @@ -0,0 +1,11 @@ +#!/bin/bash +# """ +# Docker image naming configuration. +# +# This file defines the repository name, image name, and full image name +# variables used by all docker_*.sh scripts in the project template. +# """ +REPO_NAME=gpsaggese +# The file should be all lower case. +IMAGE_NAME=umd_data605_spring2026_ray_housing_price_prediction +FULL_IMAGE_NAME=$REPO_NAME/$IMAGE_NAME diff --git a/class_project/data605/Spring2026/projects/UmdTask464_DATA605_Spring2026_Ray_Housing_Price_Prediction/docker_push.sh b/class_project/data605/Spring2026/projects/UmdTask464_DATA605_Spring2026_Ray_Housing_Price_Prediction/docker_push.sh new file mode 100644 index 000000000..27d752dd9 --- /dev/null +++ b/class_project/data605/Spring2026/projects/UmdTask464_DATA605_Spring2026_Ray_Housing_Price_Prediction/docker_push.sh @@ -0,0 +1,25 @@ +#!/bin/bash +# """ +# Push Docker container image to Docker Hub or registry. +# +# This script authenticates with the Docker registry using credentials from +# ~/.docker/passwd.$REPO_NAME.txt and pushes the locally built container +# image to the remote repository. +# """ + +# Exit immediately if any command exits with a non-zero status. +set -e + +# Import the utility functions. +GIT_ROOT=$(git rev-parse --show-toplevel) +source $GIT_ROOT/class_project/project_template/utils.sh + +# Parse default args (-h, -v) and enable set -x if -v is passed. +parse_default_args "$@" + +# Load Docker image naming configuration. +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source $SCRIPT_DIR/docker_name.sh + +# Push the container image to the registry. +push_container_image diff --git a/class_project/data605/Spring2026/projects/UmdTask464_DATA605_Spring2026_Ray_Housing_Price_Prediction/etc_sudoers b/class_project/data605/Spring2026/projects/UmdTask464_DATA605_Spring2026_Ray_Housing_Price_Prediction/etc_sudoers new file mode 100644 index 000000000..ee0816a15 --- /dev/null +++ b/class_project/data605/Spring2026/projects/UmdTask464_DATA605_Spring2026_Ray_Housing_Price_Prediction/etc_sudoers @@ -0,0 +1,31 @@ +# +# This file MUST be edited with the 'visudo' command as root. +# +# Please consider adding local content in /etc/sudoers.d/ instead of +# directly modifying this file. +# +# See the man page for details on how to write a sudoers file. +# +Defaults env_reset +Defaults mail_badpass +Defaults secure_path="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin" + +# Host alias specification + +# User alias specification + +# Cmnd alias specification + +# User privilege specification +root ALL=(ALL:ALL) ALL + +# Members of the admin group may gain root privileges +%admin ALL=(ALL) ALL + +# Allow members of group sudo to execute any command +%sudo ALL=(ALL:ALL) ALL + +# See sudoers(5) for more information on "#include" directives: +postgres ALL=(ALL) NOPASSWD:ALL + +#includedir /etc/sudoers.d diff --git a/class_project/data605/Spring2026/projects/UmdTask464_DATA605_Spring2026_Ray_Housing_Price_Prediction/ray_housing.API.ipynb b/class_project/data605/Spring2026/projects/UmdTask464_DATA605_Spring2026_Ray_Housing_Price_Prediction/ray_housing.API.ipynb new file mode 100644 index 000000000..ff6399c51 --- /dev/null +++ b/class_project/data605/Spring2026/projects/UmdTask464_DATA605_Spring2026_Ray_Housing_Price_Prediction/ray_housing.API.ipynb @@ -0,0 +1,436 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "c453d5e1-52dd-4743-b37a-a4f269b3b4a2", + "metadata": {}, + "source": [ + "# Ray API Tour\n", + "\n", + "A short, didactic walk through Ray's four core APIs in isolation:\n", + "\n", + "- **Ray Core** — `@ray.remote` for task parallelism\n", + "- **Ray Data** — `ray.data.from_pandas` for distributed datasets\n", + "- **Ray Tune** — `tune.run` for hyperparameter search\n", + "- **Ray Serve** — `@serve.deployment` for model serving\n", + "\n", + "Each section uses the smallest possible self-contained example so you\n", + "can read it as documentation rather than as an applied project. For\n", + "the same APIs used end-to-end on a real regression problem, see\n", + "`ray_housing.example.ipynb`.\n", + "\n", + "> Run the cells top to bottom. Some sections call `ray.init()` or\n", + "> `serve.run(...)` which can take a few seconds the first time." + ] + }, + { + "cell_type": "markdown", + "id": "5bec20bd-b64d-46f2-97b2-b57f96cf542d", + "metadata": {}, + "source": [ + "## 1. Ray Core: `@ray.remote`\n", + "\n", + "`@ray.remote` is the most basic Ray primitive. It turns an ordinary\n", + "function into a \"remote function\" that returns a `ObjectRef` future\n", + "instead of a value. Ray schedules invocations across all available\n", + "CPU cores; you collect the results with `ray.get(...)`.\n", + "\n", + "The decorator is the *only* code change needed to parallelize a\n", + "function across cores or — with no further change — across a Ray\n", + "cluster." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "f8344477-0337-44d2-bf3b-2ff4760d76b0", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2026-05-05 21:25:48,502\tINFO util.py:154 -- Missing packages: ['ipywidgets']. Run `pip install -U ipywidgets`, then restart the notebook server for rich notebook output.\n", + "2026-05-05 21:25:54,066\tWARNING services.py:2148 -- WARNING: The object store is using /tmp instead of /dev/shm because /dev/shm has only 67108864 bytes available. This will harm performance! You may be able to free up space by deleting files in /dev/shm. If you are inside a Docker container, you can increase /dev/shm size by passing '--shm-size=1.05gb' to 'docker run' (or add it to the run_options list in a Ray cluster config). Make sure to set this to more than 30% of available RAM.\n", + "2026-05-05 21:25:54,188\tINFO worker.py:1942 -- Started a local Ray instance. View the dashboard at \u001b[1m\u001b[32mhttp://127.0.0.1:8266 \u001b[39m\u001b[22m\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "
\n", + "
\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "\n", + "\n", + "
Python version:3.12.13
Ray version:2.49.0
Dashboard:http://127.0.0.1:8266
\n", + "\n", + "
\n", + "
\n" + ], + "text/plain": [ + "RayContext(dashboard_url='127.0.0.1:8266', python_version='3.12.13', ray_version='2.49.0', ray_commit='66438d8bd27f8c604ee5a0cd2cfc5649053285ed')" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import ray\n", + "\n", + "# ray.init starts a single-node Ray runtime in this process.\n", + "# ignore_reinit_error makes re-running the cell harmless.\n", + "ray.init(ignore_reinit_error=True)" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "890481ee-00e6-4b9b-be26-5c40009e1f3b", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Results: [0, 1, 4, 9, 16, 25, 36, 49]\n" + ] + } + ], + "source": [ + "@ray.remote\n", + "def square(x):\n", + " \"\"\"A trivial remote function for demonstration.\"\"\"\n", + " return x * x\n", + "\n", + "# .remote(...) submits the call and returns a future.\n", + "# ray.get([...]) waits for all futures and returns the values.\n", + "futures = [square.remote(i) for i in range(8)]\n", + "print(\"Results:\", ray.get(futures))" + ] + }, + { + "cell_type": "markdown", + "id": "10f8985f-fce9-40ae-b3d6-e24cc477d725", + "metadata": {}, + "source": [ + "## 2. Ray Data: `ray.data.from_pandas`\n", + "\n", + "`ray.data` is Ray's distributed dataset library. Datasets are lazy,\n", + "parallelized, and can be processed with familiar transformations like\n", + "`map_batches`, `filter`, and `groupby`. The smallest possible example\n", + "is wrapping a pandas DataFrame." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "26771aab-f9fd-45cd-8670-056df656a4e5", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2026-05-05 21:26:00,271\tINFO util.py:154 -- Missing packages: ['ipywidgets']. Run `pip install -U ipywidgets`, then restart the notebook server for rich notebook output.\n", + "2026-05-05 21:26:01,245\tINFO dataset.py:3246 -- Tip: Use `take_batch()` instead of `take() / show()` to return records in pandas or numpy batch format.\n", + "2026-05-05 21:26:01,254\tINFO logging.py:295 -- Registered dataset logger for dataset dataset_1_0\n", + "2026-05-05 21:26:01,268\tINFO streaming_executor.py:159 -- Starting execution of Dataset dataset_1_0. Full logs are in /tmp/ray/session_2026-05-05_21-25-48_524751_9711/logs/ray-data\n", + "2026-05-05 21:26:01,269\tINFO streaming_executor.py:160 -- Execution plan of Dataset dataset_1_0: InputDataBuffer[Input] -> LimitOperator[limit=20]\n", + "2026-05-05 21:26:01,271\tWARNING resource_manager.py:134 -- ⚠️ Ray's object store is configured to use only 42.9% of available memory (1.0GiB out of 2.2GiB total). For optimal Ray Data performance, we recommend setting the object store to at least 50% of available memory. You can do this by setting the 'object_store_memory' parameter when calling ray.init() or by setting the RAY_DEFAULT_OBJECT_STORE_MEMORY_PROPORTION environment variable.\n", + "2026-05-05 21:26:01,289\tINFO streaming_executor.py:279 -- ✔️ Dataset dataset_1_0 execution finished in 0.02 seconds\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[dataset]: Run `pip install tqdm` to enable progress reporting.\n", + "{'id': 1, 'value': 10}\n", + "{'id': 2, 'value': 20}\n", + "{'id': 3, 'value': 30}\n", + "{'id': 4, 'value': 40}\n", + "{'id': 5, 'value': 50}\n" + ] + } + ], + "source": [ + "import pandas as pd\n", + "import ray\n", + "\n", + "# A toy DataFrame with five rows.\n", + "df = pd.DataFrame({\n", + " \"id\": [1, 2, 3, 4, 5],\n", + " \"value\": [10, 20, 30, 40, 50],\n", + "})\n", + "\n", + "# Wrap it in a Ray Dataset.\n", + "ds = ray.data.from_pandas(df)\n", + "ds.show()" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "2ee43592-e0df-4146-a67b-2ae746c9a2c7", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2026-05-05 21:26:01,327\tINFO logging.py:295 -- Registered dataset logger for dataset dataset_3_0\n", + "2026-05-05 21:26:01,330\tINFO streaming_executor.py:159 -- Starting execution of Dataset dataset_3_0. Full logs are in /tmp/ray/session_2026-05-05_21-25-48_524751_9711/logs/ray-data\n", + "2026-05-05 21:26:01,331\tINFO streaming_executor.py:160 -- Execution plan of Dataset dataset_3_0: InputDataBuffer[Input] -> TaskPoolMapOperator[MapBatches(double_value)] -> LimitOperator[limit=20]\n", + "2026-05-05 21:26:01,374\tINFO streaming_executor.py:279 -- ✔️ Dataset dataset_3_0 execution finished in 0.04 seconds\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'id': 1, 'value': 20}\n", + "{'id': 2, 'value': 40}\n", + "{'id': 3, 'value': 60}\n", + "{'id': 4, 'value': 80}\n", + "{'id': 5, 'value': 100}\n" + ] + } + ], + "source": [ + "# Datasets support batched transformations. map_batches applies a\n", + "# function to chunks of rows in parallel. Here we double \"value\".\n", + "def double_value(batch):\n", + " batch[\"value\"] = batch[\"value\"] * 2\n", + " return batch\n", + "\n", + "ds_doubled = ds.map_batches(double_value)\n", + "ds_doubled.show()" + ] + }, + { + "cell_type": "markdown", + "id": "db36f4b6-1b0e-4955-92c1-b119e104e2a2", + "metadata": {}, + "source": [ + "## 3. Ray Tune: `tune.run`\n", + "\n", + "Ray Tune is a hyperparameter search library built on top of Ray Core.\n", + "You define a \"trainable\" — a function that takes a `config` dict and\n", + "calls `tune.report({...})` with a metric — and Tune handles the\n", + "parallel scheduling and result aggregation.\n", + "\n", + "The example below sweeps a single parameter `x` over a small grid and\n", + "finds the value that minimizes `(x - 3)**2`." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "79358a6c-750a-4d0d-8855-a194d9232da8", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2026-05-05 21:26:07,309\tINFO tune.py:1009 -- Wrote the latest version of all result files and experiment state to '/root/ray_results/trainable_2026-05-05_21-26-01' in 0.0055s.\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Best config: {'x': 3}\n" + ] + } + ], + "source": [ + "from ray import tune\n", + "\n", + "def trainable(config):\n", + " \"\"\"A trivial trainable: parabola minimized at x=3.\"\"\"\n", + " score = (config[\"x\"] - 3) ** 2\n", + " tune.report({\"score\": score})\n", + "\n", + "analysis = tune.run(\n", + " trainable,\n", + " config={\"x\": tune.grid_search([1, 2, 3, 4, 5])},\n", + " metric=\"score\",\n", + " mode=\"min\",\n", + " verbose=0, # quiet output for tutorial readability\n", + ")\n", + "\n", + "print(\"Best config:\", analysis.get_best_config(metric=\"score\", mode=\"min\"))" + ] + }, + { + "cell_type": "markdown", + "id": "1936a23d-12ba-460f-8ac3-c583556e8a55", + "metadata": {}, + "source": [ + "## 4. Ray Serve: `@serve.deployment`\n", + "\n", + "Ray Serve lets you deploy a Python class as an HTTP endpoint with a\n", + "single decorator. It runs inside the same Ray runtime — no separate\n", + "server process — and scales horizontally just by changing a replica\n", + "count.\n", + "\n", + "The minimal example below exposes a `/` endpoint that returns a JSON\n", + "greeting." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "a7b51a76-7c2d-455c-bd15-c5b3b55b2b6b", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "INFO 2026-05-05 21:29:38,110 serve 9711 -- Connecting to existing Serve app in namespace \"serve\". New http options will not be applied.\n", + "\u001b[36m(ServeController pid=10423)\u001b[0m INFO 2026-05-05 21:29:38,200 controller 10423 -- Deploying new version of Deployment(name='Hello', app='default') (initial target replicas: 1).\n", + "\u001b[36m(ServeController pid=10423)\u001b[0m INFO 2026-05-05 21:29:38,306 controller 10423 -- Stopping 1 replicas of Deployment(name='Hello', app='default') with outdated versions.\n", + "\u001b[36m(ServeController pid=10423)\u001b[0m INFO 2026-05-05 21:29:38,306 controller 10423 -- Adding 1 replica to Deployment(name='Hello', app='default').\n", + "\u001b[36m(ServeController pid=10423)\u001b[0m INFO 2026-05-05 21:29:40,349 controller 10423 -- Replica(id='h6fky95n', deployment='Hello', app='default') is stopped.\n", + "INFO 2026-05-05 21:29:41,129 serve 9711 -- Application 'default' is ready at http://127.0.0.1:8000/.\n" + ] + }, + { + "data": { + "text/plain": [ + "DeploymentHandle(deployment='Hello')" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from ray import serve\n", + "from starlette.responses import JSONResponse\n", + "\n", + "@serve.deployment\n", + "class Hello:\n", + " \"\"\"A trivial Serve deployment that returns a static greeting.\"\"\"\n", + "\n", + " async def __call__(self, request):\n", + " # Returning a JSONResponse explicitly avoids any auto-conversion\n", + " # quirks in Ray Serve's starlette integration.\n", + " return JSONResponse({\"message\": \"hello from Ray Serve\"})\n", + "\n", + "\n", + "# serve.run starts Serve if it isn't already running, registers the\n", + "# deployment, and binds the HTTP proxy on port 8000.\n", + "serve.run(Hello.bind())" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "01b807be-a5c1-47ac-a7e1-6d1b1c1bfd89", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Status: 200\n", + "Body: {'message': 'hello from Ray Serve'}\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[36m(ServeReplica:default:Hello pid=11929)\u001b[0m INFO 2026-05-05 21:29:49,977 default_Hello uyqmoeyz d3b7e737-4afc-46c3-aa85-1b9cb691a331 -- GET / 200 1.7ms\n" + ] + } + ], + "source": [ + "import requests\n", + "\n", + "response = requests.get(\"http://127.0.0.1:8000/\")\n", + "print(\"Status:\", response.status_code)\n", + "print(\"Body: \", response.json())" + ] + }, + { + "cell_type": "markdown", + "id": "b4c67a0d-5335-4c60-9de8-f7f34e79534c", + "metadata": {}, + "source": [ + "## Wrapping Up\n", + "\n", + "That's the end of the API tour. With those four primitives —\n", + "`@ray.remote`, `ray.data.from_pandas`, `tune.run`, and\n", + "`@serve.deployment` — you have everything Ray uses to scale\n", + "ML workloads from a laptop to a multi-node cluster.\n", + "\n", + "Now head over to `ray_housing.example.ipynb` to see the same APIs\n", + "applied to a real regression problem." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.13" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/class_project/data605/Spring2026/projects/UmdTask464_DATA605_Spring2026_Ray_Housing_Price_Prediction/ray_housing.example.ipynb b/class_project/data605/Spring2026/projects/UmdTask464_DATA605_Spring2026_Ray_Housing_Price_Prediction/ray_housing.example.ipynb new file mode 100644 index 000000000..7d6f12378 --- /dev/null +++ b/class_project/data605/Spring2026/projects/UmdTask464_DATA605_Spring2026_Ray_Housing_Price_Prediction/ray_housing.example.ipynb @@ -0,0 +1,1621 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "91abe5b4-177c-4dc1-a34e-7e6efc0df1f5", + "metadata": {}, + "source": [ + "# Ray Housing Price Prediction — End-to-End Pipeline\n", + "\n", + "This notebook walks through the full applied pipeline: load California\n", + "housing data with Ray Data, train a baseline scikit-learn model, run\n", + "multiple training jobs in parallel with Ray Core, search hyperparameters\n", + "with Ray Tune, and deploy the best model behind a REST API with Ray Serve.\n", + "\n", + "For small, isolated demos of each Ray API on its own, see\n", + "`ray_housing.API.ipynb`." + ] + }, + { + "cell_type": "markdown", + "id": "4fc1e5da-8128-45c9-bb7c-b223f2e2f206", + "metadata": {}, + "source": [ + "## 1. Load Data with Ray Data\n", + "\n", + "The `load_data()` helper in `ray_utils.py` calls\n", + "`sklearn.datasets.fetch_california_housing` to get the raw data, wraps\n", + "it in a `ray.data.Dataset` via `ray.data.from_pandas(...)`, and\n", + "materializes it back to a pandas DataFrame for the rest of the\n", + "notebook.\n", + "\n", + "Why bother round-tripping through Ray Data? The dataset is small enough\n", + "to fit in memory, but the pattern matters: the same `from_pandas` call\n", + "scales to multi-node Ray clusters with no code changes." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "67b51339-7b5e-4f93-ad39-e1a728f7d88e", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2026-05-05 20:58:48,219\tINFO util.py:154 -- Missing packages: ['ipywidgets']. Run `pip install -U ipywidgets`, then restart the notebook server for rich notebook output.\n", + "2026-05-05 20:58:48,337\tINFO util.py:154 -- Missing packages: ['ipywidgets']. Run `pip install -U ipywidgets`, then restart the notebook server for rich notebook output.\n", + "2026-05-05 20:58:54,210\tWARNING services.py:2148 -- WARNING: The object store is using /tmp instead of /dev/shm because /dev/shm has only 67108864 bytes available. This will harm performance! You may be able to free up space by deleting files in /dev/shm. If you are inside a Docker container, you can increase /dev/shm size by passing '--shm-size=2.08gb' to 'docker run' (or add it to the run_options list in a Ray cluster config). Make sure to set this to more than 30% of available RAM.\n", + "2026-05-05 20:58:55,343\tINFO worker.py:1942 -- Started a local Ray instance. View the dashboard at \u001b[1m\u001b[32mhttp://127.0.0.1:8265 \u001b[39m\u001b[22m\n", + "2026-05-05 20:59:02,066\tINFO logging.py:295 -- Registered dataset logger for dataset dataset_0_0\n", + "2026-05-05 20:59:02,093\tWARNING resource_manager.py:134 -- ⚠️ Ray's object store is configured to use only 42.9% of available memory (1.9GiB out of 4.4GiB total). For optimal Ray Data performance, we recommend setting the object store to at least 50% of available memory. You can do this by setting the 'object_store_memory' parameter when calling ray.init() or by setting the RAY_DEFAULT_OBJECT_STORE_MEMORY_PROPORTION environment variable.\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
MedIncHouseAgeAveRoomsAveBedrmsPopulationAveOccupLatitudeLongitudeMedHouseVal
08.325241.06.9841271.023810322.02.55555637.88-122.234.526
18.301421.06.2381370.9718802401.02.10984237.86-122.223.585
27.257452.08.2881361.073446496.02.80226037.85-122.243.521
35.643152.05.8173521.073059558.02.54794537.85-122.253.413
43.846252.06.2818531.081081565.02.18146737.85-122.253.422
\n", + "
" + ], + "text/plain": [ + " MedInc HouseAge AveRooms AveBedrms Population AveOccup Latitude \\\n", + "0 8.3252 41.0 6.984127 1.023810 322.0 2.555556 37.88 \n", + "1 8.3014 21.0 6.238137 0.971880 2401.0 2.109842 37.86 \n", + "2 7.2574 52.0 8.288136 1.073446 496.0 2.802260 37.85 \n", + "3 5.6431 52.0 5.817352 1.073059 558.0 2.547945 37.85 \n", + "4 3.8462 52.0 6.281853 1.081081 565.0 2.181467 37.85 \n", + "\n", + " Longitude MedHouseVal \n", + "0 -122.23 4.526 \n", + "1 -122.22 3.585 \n", + "2 -122.24 3.521 \n", + "3 -122.25 3.413 \n", + "4 -122.25 3.422 " + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from ray_utils import load_data\n", + "\n", + "df = load_data()\n", + "df.head()" + ] + }, + { + "cell_type": "markdown", + "id": "f591bfc7-97f3-4790-b8fb-92a8d2ea0dcd", + "metadata": {}, + "source": [ + "## 2. Exploratory Data Analysis\n", + "\n", + "Before modeling, do a quick sanity check on the data — shape, dtypes,\n", + "missing values, distributions, and which features correlate with the\n", + "target.\n", + "\n", + "The California Housing dataset has 20,640 rows × 8 numerical features\n", + "plus the `MedHouseVal` target. There are no missing values and no\n", + "categorical columns, which keeps preprocessing minimal." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "4db140f5-44a9-479f-b235-46e193e5efa9", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "RangeIndex: 20640 entries, 0 to 20639\n", + "Data columns (total 9 columns):\n", + " # Column Non-Null Count Dtype \n", + "--- ------ -------------- ----- \n", + " 0 MedInc 20640 non-null float64\n", + " 1 HouseAge 20640 non-null float64\n", + " 2 AveRooms 20640 non-null float64\n", + " 3 AveBedrms 20640 non-null float64\n", + " 4 Population 20640 non-null float64\n", + " 5 AveOccup 20640 non-null float64\n", + " 6 Latitude 20640 non-null float64\n", + " 7 Longitude 20640 non-null float64\n", + " 8 MedHouseVal 20640 non-null float64\n", + "dtypes: float64(9)\n", + "memory usage: 1.4 MB\n" + ] + } + ], + "source": [ + "df.info()" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "e40123c0-c8ac-4c2a-9abe-761540c406ab", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
MedIncHouseAgeAveRoomsAveBedrmsPopulationAveOccupLatitudeLongitudeMedHouseVal
count20640.00000020640.00000020640.00000020640.00000020640.00000020640.00000020640.00000020640.00000020640.000000
mean3.87067128.6394865.4290001.0966751425.4767443.07065535.631861-119.5697042.068558
std1.89982212.5855582.4741730.4739111132.46212210.3860502.1359522.0035321.153956
min0.4999001.0000000.8461540.3333333.0000000.69230832.540000-124.3500000.149990
25%2.56340018.0000004.4407161.006079787.0000002.42974133.930000-121.8000001.196000
50%3.53480029.0000005.2291291.0487801166.0000002.81811634.260000-118.4900001.797000
75%4.74325037.0000006.0523811.0995261725.0000003.28226137.710000-118.0100002.647250
max15.00010052.000000141.90909134.06666735682.0000001243.33333341.950000-114.3100005.000010
\n", + "
" + ], + "text/plain": [ + " MedInc HouseAge AveRooms AveBedrms Population \\\n", + "count 20640.000000 20640.000000 20640.000000 20640.000000 20640.000000 \n", + "mean 3.870671 28.639486 5.429000 1.096675 1425.476744 \n", + "std 1.899822 12.585558 2.474173 0.473911 1132.462122 \n", + "min 0.499900 1.000000 0.846154 0.333333 3.000000 \n", + "25% 2.563400 18.000000 4.440716 1.006079 787.000000 \n", + "50% 3.534800 29.000000 5.229129 1.048780 1166.000000 \n", + "75% 4.743250 37.000000 6.052381 1.099526 1725.000000 \n", + "max 15.000100 52.000000 141.909091 34.066667 35682.000000 \n", + "\n", + " AveOccup Latitude Longitude MedHouseVal \n", + "count 20640.000000 20640.000000 20640.000000 20640.000000 \n", + "mean 3.070655 35.631861 -119.569704 2.068558 \n", + "std 10.386050 2.135952 2.003532 1.153956 \n", + "min 0.692308 32.540000 -124.350000 0.149990 \n", + "25% 2.429741 33.930000 -121.800000 1.196000 \n", + "50% 2.818116 34.260000 -118.490000 1.797000 \n", + "75% 3.282261 37.710000 -118.010000 2.647250 \n", + "max 1243.333333 41.950000 -114.310000 5.000010 " + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df.describe()" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "6dd91c69-4c71-49b3-88fd-9e9b5857a471", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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889j5syZmDlzJq5du4ZOnTph5cqV+Prrr3W6H9It3iNPRq9evXrYsGEDwsPDMWTIkGJj3nzzTeTn52PJkiVF1j19+lQ6afX29oaFhQXWrl2r9ddIXc9u7O/vD1NTUyxevFhrZlCAo/ZEpHuDBg0CULQv04wkFZ7x/fnnn8exY8e04j799NMiI/Kay9816tWrhxYtWkiPPnJ0dETv3r3xySefFPtHgL/++kv6d2xsLF599VWMGDECw4cP1/qZNWsWAEhPKNGM7n/88cda21u7dq3WazMzMwQEBOC///0vLl68WOr+iajme/z4MXbu3InBgwcX6WeGDx+OkJAQPHjwAN9//z1MTU0xfPhw7NmzB1999RWePn1aZCb6N998E3/++Sc+++yzYvdVnqcdzZkzB3fu3JG2MWjQIOTn52PdunVacVFRUTAxMcHAgQOlOKB8fXpVPHr0qMgjQJ9//nnUr1+/XI/YI8PiiDzJQkmXTWr06tUL//rXvxAZGYnU1FT4+PjAwsIC165dw44dOxAdHY3hw4ejUaNGeOeddxAZGYnBgwdj0KBBOHfuHA4cOICGDRvqLN8WLVrgvffew5IlS/Dqq69i2LBhUCgUOHPmDFxcXBAZGamzfRERdezYEYGBgfj000+RlZWFXr164fTp09iyZQv8/f2lSZ+AgkcaTZkyBQEBAejfvz9+/vlnHDx4sEgf2LZtW/Tu3RseHh5wcHDA2bNn8e233yIkJESKWb9+PXr06IH27dtj0qRJaN68OTIyMpCcnIw//vgDP//8M06dOoXr169rva+w5557Di+99BJiY2MxZ84ceHh4ICAgAKtXr8adO3fQrVs3HD16FP/73/8AaF9q+uGHH+LIkSPw9PTEpEmT0LZtW9y9exc//fQTDh06hLt37+qymYnIiH3//fd48OABXnvttWLXd+vWDY0aNUJsbCxGjBiBESNGYO3atVi0aBHat2+vNfINAGPGjMH27dsxZcoUHDlyBN27d0d+fj6uXLmC7du34+DBg8XOX1LYwIED0a5dO6xatQrBwcEYMmQI+vTpg/feew+//vorOnbsiISEBHz33XcIDQ3F888/D6BifXpV/O9//0O/fv3w5ptvom3btjA3N8euXbuQkZFR6UeXUjUy3IT5RMUr/Pi50hR+/JzGp59+Kjw8PIS1tbWoX7++aN++vZg9e7a4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4+OxcuVKXLlyBeHh4Th79ixCQkKqu0mIiHhpPREREZEuNZu7z9Ap1Chnz55Fnz59pNea4jowMBCbN2/G7NmzkZOTg8mTJyMrKws9evRAfHy89Ax5AIiNjUVISAj69esHU1NTBAQEYM2aNdJ6W1tbJCQkIDg4GB4eHmjYsCEWLlwoPXoOAF555RXExcVh/vz5ePfdd9GyZUvs3r2bz5AnIoNgIU9ERERERqt3794QouRHsJmYmGDx4sVYvHhxiTEODg6Ii4srdT8dOnTADz/8UGrMG2+8gTfeeKP0hImIqgEvrSciIiIiIiKSERbyRERERERERDLCQp6IiIiIiIhIRljIExEREREREckIC3kiIiIiIiIiGWEhT0RERERERCQjLOSJiIiIiIiIZITPkSedaDZ3HxRmAsu7Au3CDyI330Rn2/71Qz+dbYuIiIiIiEjuOCJPREREREREJCMs5ImIiIiIiIhkhIU8ERERERERkYywkCciIiIiIiKSERbyRERERERERDLCQp6IiIiIiIhIRljIExEREREREckIC3kiIiIiIiIiGWEhT0RERERERCQjLOSJiIiIiIiIZISFPBEREREREZGMsJAnIiIiIiIikhEW8kREREREREQywkKeiIiIiIiISEbMDZ0AEREVOHbsGFasWIGUlBTcvn0bu3btgr+/v7ReCIFFixbhs88+Q1ZWFrp3744NGzagZcuWUszdu3cxbdo07NmzB6ampggICEB0dDTq1asnxZw/fx7BwcE4c+YMGjVqhGnTpmH27NnVeag612zuvipvQ2EmsLwr0C78IHLzTXSQFREREZF+sJCvAF2cKBIRlSQnJwcdO3bEhAkTMGzYsCLrly9fjjVr1mDLli1wd3fHggUL4Ovri8uXL8PKygoAMHr0aNy+fRtKpRIqlQrjx4/H5MmTERcXBwDIzs6Gj48PvL29ERMTgwsXLmDChAmws7PD5MmTq/V4iYiIiKhydH5pfWRkJF5++WXUr18fjo6O8Pf3x9WrV7Vinjx5guDgYDRo0AD16tVDQEAAMjIytGJu3rwJPz8/1KlTB46Ojpg1axaePn2qFZOUlISXXnoJCoUCLVq0wObNm3V9OERE1WbgwIFYunQphg4dWmSdEAKrV6/G/Pnz8frrr6NDhw748ssvcevWLezevRsA8MsvvyA+Ph6ff/45PD090aNH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AApOSAvbtGmTNBlIVFSUNFJUeMIRDTMzM+zduxdTp06Fl5cX6tati8DAQK2TV3d3d+zbtw8zZsxAdHQ0mjRpgs8//5yXNhERERHVMgMHDpT+3aFDB3h6esLNzQ3bt28v8ZbL6lDZp34Ups8nJNS2eYg4B5HusC2rRhdzNOnl0vqyWFlZYf369Vi/fn2JMW5ubmV2Lr1798a5c+cqnCMRERER1Vx2dnZ44YUXcP36dfTv398o52cq7qkfhWnmfNHnExJqyzxEnINId9iWuqGLOZr0/hx5IiIiIqLq9PDhQ9y4cQNjxowx+vmZyqLPJyTUtkKMcxDpDtuyanTRdqY6yIOIiIiIyGDeeecdHD16FL/++itOnDiBoUOHwszMDKNGjdKan+nIkSNISUnB+PHjS5yf6eeff8bBgweLnZ/p//7v/zB79mxcuXIFH3/8MbZv344ZM2YY8tCJqJbiiDwRERERydoff/yBUaNG4c6dO2jUqBF69OiBkydPolGjRgA4PxMR1Tws5ImIiIhI1rZu3Vrqes7PREQ1DQt5IiLSu2Zz+WhQIiIiIl3hPfJEREREREREMsJCnoiIiIiIiEhGWMgTERERERERyQgLeSIiIiIiIiIZYSFPREREREREJCMs5ImIiIiIiIhkhIU8ERERERERkYywkCciIiIiIiKSERbyRERERERERDJibugEiMrSbO4+vW7/1w/99Lp9IiIiIiIiXeKIPBEREREREZGMsJAnIiIiIiIikhEW8kREREREREQywkKeiIiIiIiISEZYyBMRERERERHJCAt5IiIiIiIiIhlhIU9EREREREQkIyzkiYiIiIiIiGSEhTwRERERERGRjLCQJyIiIiIiIpIRFvJEREREREREMsJCnoiIiIiIiEhGWMgTERERERERyYi5oRMgMrRmc/fpbdu/fuint20TEREREVHtxBF5IiIiIiIiIhlhIU9EREREREQkIyzkiYiIiIiIiGSEhTwRERERERGRjMh+srv169djxYoVSE9PR8eOHbF27Vp07drV0GkRAaj4RHoKM4HlXYF24QeRm29SZjwn06OqYP9JRFQ57D+JyNBkPSK/bds2hIWFYdGiRfjpp5/QsWNH+Pr6IjMz09CpEREZNfafRESVw/6TiIyBrEfkV61ahUmTJmH8+PEAgJiYGOzbtw8bN27E3LlzDZwdkf7x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xEQpZvPmzfDz88Pdu3cxb948fPjhh+jUqZPWdirq/fffx759+zBnzhz85z//gVKphLe3N68qlRNBpENnz54VAIRSqRRCCKFWq0WTJk3E9OnThRBCHDx4UAAQe/bs0XrfoEGDRPPmzaXXX331lTA1NRU//PCDVlxMTIwAIH788UdpmZubmwBQ5Kd79+7i77//1np/v379RPv27cWTJ0+kZWq1WrzyyiuiZcuW0rLQ0FABQJw6dUpalpmZKWxtbQUAkZaWVmT/8fHxWvs6cuSIACDatWsn8vLypOWjRo0SJiYmYuDAgVrxXl5ews3NTXodFRUlAIi//vpLEFHtsmnTJgFAHDp0SPz111/i999/F1u3bhUNGjQQ1tbWIikpSQAQEydO1HrfO++8IwCIxMREaZmmj/rvf/8rLbt//75o3Lix6Ny5s7Rs0aJForjTAk0uhfu9Xr16iV69ekmvnz59KnJzc7Xed+/ePeHk5CQmTJggLfvrr78EALFo0aIi+3l2/6mpqRU+xmPHjknLMjMzhUKhEDNnziyyLyKq+ar7nFRz7nju3LkSc/rpp58EABEWFiaEEOLatWvC1NRUDB06VOTn52vFqtVqIYQQWVlZon79+sLT01M8fvy42BghCvrBwMDAIvt8tr/WnJ8+99xzIjs7W1q+fft2AUBER0eXmD8ZF47Ik07FxsbCyckJffr0AQCYmJhgxIgR2Lp1K/Lz89G3b180bNgQ27Ztk95z7949KJVKrb9q7tixA23atEHr1q2lkfa///5buqy08OWaAODp6QmlUgmlUom9e/fi/fffx6VLl/Daa69Jf1m8e/cuEhMT8eabb+LBgwfSNu/cuQNfX19cu3YNf/75JwBg//796NatG7p27Srto1GjRhg9enSxx+3u7g5fX99i140dOxYWFhZauQohMGHChCLH8Pvvv+Pp06cAADs7OwDAd999V+wl/URU83l7e6NRo0ZwdXXFyJEjUa9ePezatQsnTpwAUHA/ZmGaST/37duntdzFxQVDhw6VXtvY2GDs2LE4d+4c0tPTq5ynmZkZLC0tARSMXt29exdPnz5Fly5d8NNPP1Vqm/v37wdQ/mNs27YtXn31Vel1o0aN0KpVK/zf//1fpfZPRPJW3eekDx48AADUr1+/xJw067KzswEUjP6r1WosXLgQpqbaZZnmViOlUokHDx5g7ty5ReYRqcojO8eOHauV6/Dhw9G4cWOp7yXjx1nrSWfy8/OxdetW9OnTB2lpadJyT09PrFy5EocPH4aPjw8CAgIQFxeH3NxcKBQK7Ny5EyqVSqvTvHbtGn755RfpfspnZWZmar1u2LAhvL29pdd+fn5o1aoVhg8fjs8//xzTpk3D9evXIYTAggULsGDBghK3+9xzz+G3334r9jElrVq1KvZ9hS/ff1bTpk21Xtva2gIAXF1diyxXq9W4f/8+GjRogBEjRuDzzz/HxIkTMXfuXPTr1w/Dhg3D8OHDi3T2RFQzrV+/Hi+88ALMzc3h5OSEVq1awdTUFLt27YKpqWmR2ZednZ1hZ2eH3377TWt5ixYtipzwvfDCCwAK7l3XzClSFVu2bMHKlStx5coVrUcsldY/lua3336r0DE+29cCgL29Pe7du1ep/RORfBninFRTFGsK+uI8W+zfuHEDpqamaNu2bYnv0dxS1a5du/Icerm1bNlS67WJiQlatGhR6nwoZFxYyJPOJCYm4vbt29i6dSu2bt1aZH1sbCx8fHwwcuRIfPLJJzhw4AD8/f2xfft2tG7dGh07dpRi1Wo12rdvj1WrVhW7r2eL4OL069cPAHDs2DFMmzZNGtV+5513Shw9r+wjSaytrUtcZ2ZmVqHlQghpm8eOHcORI0ewb98+xMfHY9u2bejbty8SEhJKfD8R1Rxdu3ZFly5dSlxfldGY8m4rPz+/zPd+/fXXGDduHPz9/TFr1iw4OjrCzMwMkZGR0kmorvN6Vll9KhHVHoY4J23Tpg0A4Pz581pzJRV2/vx5ACi1cK+s0vpwnjPWTCzkSWdiY2Ph6OiI9evXF1m3c+dO7Nq1CzExMejZsycaN26Mbdu2oUePHkhMTMR7772nFf/888/j559/Rr9+/Sp9oqq5RP3hw4cAgObNmwMALCwstEbvi+Pm5lbsY+uuXr1aqVwqy9TUFP369UO/fv2watUqfPDBB3jvvfdw5MiRMo+BiGouNzc3qNVqXLt2TTp5BICMjAxkZWXBzc1NK15zRVLh/vR///sfgIKZjoGC0WsAyMrKkm7tAVBk5Ls43377LZo3b46dO3dq7WPRokVacRXpzyt6jEREGoY4Jx04cCDMzMzw1VdflTjh3Zdffglzc3MMGDBA2rZarcbly5dLLP6ff/55AMDFixdLHXCyt7dHVlZWkeW//fabdA5c2LPnuUIIXL9+HR06dChxH2RceH0u6cTjx4+xc+dODB48GMOHDy/yExISggcPHuD777+Hqakphg8fjj179uCrr77C06dPi8xE/+abb+LPP//EZ599Vuy+cnJyysxpz549ACD9VdXR0RG9e/fGJ598gtu3bxeJ/+uvv6R/Dxo0CCdPnsTp06e11uv7+aOF3b17t8gyTSdf3kfwEVHNNGjQIADA6tWrtZZrRoz8/Py0lt+6dUvryRzZ2dn48ssv0alTJ+myes3J4rFjx6S4nJwcbNmypcx8NKM9hUe/T506heTkZK24OnXqAECxJ5vPqugxEhEBhjsndXV1xfjx43Ho0CHpCR2FxcTEIDExEUFBQWjSpAkAwN/fH6ampli8eHGR+ZA0/amPjw/q16+PyMhIPHnypNgYoKAPP3nypNYs/Hv37i3xkXJffvml1m0A3377LW7fvo2BAwcWG0/GhyPypBPff/89Hjx4gNdee63Y9d26dUOjRo0QGxuLESNGYMSIEVi7di0WLVqE9u3ba422AMCYMWOwfft2TJkyBUeOHEH37t2Rn5+PK1euYPv27dIz2zX+/PNPfP311wAKnmf8888/45NPPkHDhg0xbdo0KW79+vXo0aMH2rdvj0mTJqF58+bIyMhAcnIy/vjjD/z8888AgNmzZ+Orr77CgAEDMH36dNStWxeffvop3NzcpMui9G3x4sU4duwY/Pz84ObmhszMTHz88cdo0qQJevToUS05EJFx6tixIwIDA/Hpp58iKysLvXr1wunTp7Flyxb4+/tLkztpvPDCCwgKCsKZM2fg5OSEjRs3IiMjA5s2bZJifHx80LRpUwQFBWHWrFkwMzPDxo0b0ahRI9y8ebPUfAYPHoydO3di6NCh8PPzQ1paGmJiYtC2bVvpqiig4Jahtm3bYtu2bXjhhRfg4OCAdu3aFXvvZ0WPkYgIMOw5aVRUFK5cuYJ///vfiI+Pl0beDx48iO+++w69evXCypUrpW23aNEC7733HpYsWYJXX30Vw4YNg0KhwJkzZ+Di4oLIyEjY2NggKioKEydOxMsvv4y33noL9vb2+Pnnn/Ho0SPpj60TJ07Et99+iwEDBuDNN9/EjRs38PXXX0t/pH2Wg4MDevTogfHjxyMjIwOrV69GixYtMGnSpCr/P6BqYrD58qlGGTJkiLCyshI5OTklxowbN05YWFiIv//+W6jVauHq6ioAiKVLlxYbn5eXJ5YtWyZefPFFoVAohL29vfDw8BARERHi/v37Utyzj58zNTUVjo6OYtSoUeL69etFtnvjxg0xduxY4ezsLCwsLMRzzz0nBg8eLL799lutuPPnz4tevXoJKysr8dxzz4klS5aIL774otjHz/n5+RXZj+bxHjt27NBarnmU05kzZ7SWax69pHnc3OHDh8Xrr78uXFxchKWlpXBxcRGjRo0S//vf/0psYyKqGUrqJwpTqVQiIiJCuLu7CwsLC+Hq6irmzZun9XhNIf7pow4ePCg6dOggFAqFaN26dZG+SQghUlJShKenp7C0tBRNmzYVq1atKtfj59Rqtfjggw+Em5ubUCgUonPnzmLv3r0iMDBQ67GaQghx4sQJ4eHhISwtLbUeRVfc4+8qeozPejZPIqr5DHlOKoQQubm5IioqSnh4eIi6deuKOnXqiJdeekmsXr1a63HEhW3cuFF07txZ2navXr2kx+ZpfP/99+KVV14R1tbWwsbGRnTt2lV88803WjErV64Uzz33nFAoFKJ79+7i7NmzJT5+7ptvvhHz5s0Tjo6OwtraWvj5+YnffvuttKYlI2MiBGeBISIiqqmaNWuGdu3aYe/evYZOhYiIDCwpKQl9+vTBjh07MHz4cEOnQ1XAe+SJiIiIiIiIZISFPBEREREREZGMsJAnIiIiIiIikhHeI09EREREREQkIxyRJyIiIiIiIpIRFvJEREREREREMsJCnoiIiIiIiEhGzA2dgCGp1WrcunUL9evXh4mJiaHTISIDEELgwYMHcHFxgakp/7ZZHuw7iQhg/1kZ7D+JCNBN/1mrC/lbt27B1dXV0GkQkRH4/fff0aRJE0OnIQvsO4moMPaf5cf+k4gKq0r/WasL+fr16wMoaEAbG5tiY1QqFRISEuDj4wMLC4vqTM+osV2KYpsUz9jbJTs7G66urlJ/QGUrT99ZmLF/BqoL26EA26FATWgH9p8VV5v6T+ZuOHLOv7bkrov+s1YX8ppLmmxsbEot5OvUqQMbGxvZfZj0ie1SFNukeHJpF31f4hgZGYmdO3fiypUrsLa2xiuvvIJly5ahVatWUsyTJ08wc+ZMbN26Fbm5ufD19cXHH38MJycnKebmzZuYOnUqjhw5gnr16iEwMBCRkZEwN/+nO09KSkJYWBguXboEV1dXzJ8/H+PGjdPKZ/369VixYgXS09PRsWNHrF27Fl27di3XsZSn7yxMLp8BfWM7FGA7FKhJ7cBLxMuvNvWfzN1w5Jx/bcu9Kv0nb2giIqoGR48eRXBwME6ePAmlUgmVSgUfHx/k5ORIMTNmzMCePXuwY8cOHD16FLdu3cKwYcOk9fn5+fDz80NeXh5OnDiBLVu2YPPmzVi4cKEUk5aWBj8/P/Tp0wepqakIDQ3FxIkTcfDgQSlm27ZtCAsLw6JFi/DTTz+hY8eO8PX1RWZmZvU0BhERERFVSa0ekSciqi7x8fFarzdv3gxHR0ekpKSgZ8+euH//Pr744gvExcWhb9++AIBNmzahTZs2OHnyJLp164aEhARcvnwZhw4dgpOTEzp16oQlS5Zgzpw5CA8Ph6WlJWJiYuDu7o6VK1cCANq0aYPjx48jKioKvr6+AIBVq1Zh0qRJGD9+PAAgJiYG+/btw8aNGzF37txqbBUiIiIiqgwW8kREBnD//n0AgIODAwAgJSUFKpUK3t7eUkzr1q3RtGlTJCcno1u3bkhOTkb79u21LrX39fXF1KlTcenSJXTu3BnJycla29DEhIaGAgDy8vKQkpKCefPmSetNTU3h7e2N5OTkYnPNzc1Fbm6u9Do7OxtAwSVkKpWqzGPVxJQntiZjOxRgOxSoCe0g59yJiOSOhTwRUTVTq9UIDQ1F9+7d0a5dOwBAeno6LC0tYWdnpxXr5OSE9PR0KaZwEa9Zr1lXWkx2djYeP36Me/fuIT8/v9iYK1euFJtvZGQkIiIiiixPSEhAnTp1ynnUgFKpLHdsTcZ2KMB2KCDndnj06JGhUyAiqrVYyBMRVbPg4GBcvHgRx48fN3Qq5TJv3jyEhYVJrzUzrfr4+JR7sialUon+/fvLbuIaXWI7FGA7FKgJ7aC5OoeIiKpfhQr5mjTrcmU0m7tPb9sGgF8/9NPr9onI8EJCQrB3714cO3ZM67mhzs7OyMvLQ1ZWltaofEZGBpydnaWY06dPa20vIyNDWqf5r2ZZ4RgbGxtYW1vDzMwMZmZmxcZotvEshUIBhUJRZLmFhUWFCpDO7yciN18/s1vLqf+saLvVVGyHAnJuh+rKu7affwJAu/CDeuk/5dR3EpG2Cs1az1mXiYgqRwiBkJAQ7Nq1C4mJiXB3d9da7+HhAQsLCxw+fFhadvXqVdy8eRNeXl4AAC8vL1y4cEGrn1MqlbCxsUHbtm2lmMLb0MRotmFpaQkPDw+tGLVajcOHD0sxRETGhOefRERFVWhEnrMuExFVTnBwMOLi4vDdd9+hfv360j3ttra2sLa2hq2tLYKCghAWFgYHBwfY2Nhg2rRp8PLyQrdu3QAAPj4+aNu2LcaMGYPly5cjPT0d8+fPR3BwsDRiPmXKFKxbtw6zZ8/GhAkTkJiYiO3bt2Pfvn+uKAoLC0NgYCC6dOmCrl27YvXq1cjJyZH6UyIiY8LzTyKioqp0j7ycZl0GKjfzcuFZZRVmosw2qQo5zf5aE2bb1TW2SfGMvV2qK68NGzYAAHr37q21fNOmTdJlm1FRUTA1NUVAQIDWpaEaZmZm2Lt3L6ZOnQovLy/UrVsXgYGBWLx4sRTj7u6Offv2YcaMGYiOjkaTJk3w+eefSyehADBixAj89ddfWLhwIdLT09GpUyfEx8cXmQCPiMgYye38k4hIHypdyMtt1mWgajMvK5VKLNfv7U/Yv3+/fnegB3KebVdf2CbFM9Z2qa5Zl4Uo+w+BVlZWWL9+PdavX19ijJubW5l9Re/evXHu3LlSY0JCQhASElJmTkRExkRu55+6enynwlQ/g0n6/GO2sf8hvzRyzh2Qd/61JXddHF+lC3m5zboMVG7m5cKzynZ+P1Gv+V0M9y07yEjUhNl2dY1tUjxjbxfOukxEJB9yO//U1eM7l3RR6zItSXUMIhnrH/LLQ865A/LOv6bnrouBpEoV8nKcdRmo2szLFhYWepttufA+5EbOs+3qC9ukeMbaLsaYExERFSXH809dPb5zwVlT5Kp1fx6qz0EkY/9DfmnknDsg7/xrS+66GEiqUCEvhMC0adOwa9cuJCUllTrrckBAAIDiZ11+//33kZmZCUdHRwDFz7r87F8IS5p12d/fH8A/sy7zUlEiIiKimkPO55+6enxnrtpELwNK1VEoGesf8stDzrkD8s6/pueui2OrUCHPWZeJiIiIqDrx/JOIqKgKFfKcdZmIiIiIqhPPP4mIiqrwpfVl4azLRERERKQrPP8kIirK1NAJEBEREREREVH5sZAnIiIiIiIikhEW8kREREREREQywkKeiIiIiIiISEZYyBMRERERERHJCAt5IiIiIiIiIhlhIU9EREREREQkIyzkiYiIiIiIiGSEhTwRERERERGRjLCQJyIiIiIiIpIRFvJEREREREREMsJCnoiIiIiIiEhGWMgTERERERERyQgLeSIiIiIiIiIZYSFPREREREREJCMs5ImIiIiIiIhkhIU8ERERERERkYywkCciIiIiIiKSERbyRERERERERDLCQp6IiIiIiIhIRljIExEREREREckIC3kiIiIiIiIiGWEhT0RERERERCQjLOSJiIiIiIiIZISFPBFRNTh27BiGDBkCFxcXmJiYYPfu3Vrrx40bBxMTE62fAQMGaMXcvXsXo0ePho2NDezs7BAUFISHDx9qxZw/fx6vvvoqrKys4OrqiuXLlxfJZceOHWjdujWsrKzQvn177N+/X+fHS0RERET6w0KeiKga5OTkoGPHjli/fn2JMQMGDMDt27eln2+++UZr/ejRo3Hp0iUolUrs3bsXx44dw+TJk6X12dnZ8PHxgZubG1JSUrBixQqEh4fj008/lWJOnDiBUaNGISgoCOfOnYO/vz/8/f1x8eJF3R80EREREemFuaETICKqDQYOHIiBAweWGqNQKODs7Fzsul9++QXx8fE4c+YMunTpAgBYu3YtBg0ahI8++gguLi6IjY1FXl4eNm7cCEtLS7z44otITU3FqlWrpII/OjoaAwYMwKxZswAAS5YsgVKpxLp16xATE6PDIyYiIiIifWEhT0RkJJKSkuDo6Ah7e3v07dsXS5cuRYMGDQAAycnJsLOzk4p4APD29oapqSlOnTqFoUOHIjk5GT179oSlpaUU4+vri2XLluHevXuwt7dHcnIywsLCtPbr6+tb5FL/wnJzc5Gbmyu9zs7OBgCoVCqoVKoyj0sTozAVZTdCJZUnD0PT5CiHXPWJ7VCgJrSDnHMnIpI7FvJEREZgwIABGDZsGNzd3XHjxg28++67GDhwIJKTk2FmZob09HQ4Ojpqvcfc3BwODg5IT08HAKSnp8Pd3V0rxsnJSVpnb2+P9PR0aVnhGM02ihMZGYmIiIgiyxMSElCnTp1yH+OSLupyx1aUnO7zVyqVhk7BKLAdCsi5HR49emToFIiIaq0KF/LHjh3DihUrkJKSgtu3b2PXrl3w9/eX1o8bNw5btmzReo+vry/i4+Ol13fv3sW0adOwZ88emJqaIiAgANHR0ahXr54Uc/78eQQHB+PMmTNo1KgRpk2bhtmzZ2ttd8eOHViwYAF+/fVXtGzZEsuWLcOgQYMqekhERAY3cuRI6d/t27dHhw4d8PzzzyMpKQn9+vUzYGbAvHnztEbxs7Oz4erqCh8fH9jY2JT5fpVKBaVSiQVnTZGrNtFLjhfDffWyXV3StEP//v1hYWFh6HQMhu1QoCa0g+bqHH3juScRUVEVLuQ1EzZNmDABw4YNKzZmwIAB2LRpk/RaoVBorR89ejRu374NpVIJlUqF8ePHY/LkyYiLiwPwz4RN3t7eiImJwYULFzBhwgTY2dlJ93lqJmyKjIzE4MGDERcXB39/f/z0009o165dRQ+LiMioNG/eHA0bNsT169fRr18/ODs7IzMzUyvm6dOnuHv3rnRfvbOzMzIyMrRiNK/Liinp3nygoA9/th8HAAsLiwoVILlqE+Tm66eQl1MhVNF2q6nYDgXk3A7VlTfPPYmIiqpwIc8Jm4iI9O+PP/7AnTt30LhxYwCAl5cXsrKykJKSAg8PDwBAYmIi1Go1PD09pZj33nsPKpVKOsFWKpVo1aoV7O3tpZjDhw8jNDRU2pdSqYSXl1c1Hh0RUfnx3JOIqCi93CNfkyZsKjwZjcJMfxM1Fd6XHNSESXp0jW1SPGNvl+rK6+HDh7h+/br0Oi0tDampqXBwcICDgwMiIiIQEBAAZ2dn3LhxA7Nnz0aLFi3g61twyXibNm0wYMAATJo0CTExMVCpVAgJCcHIkSPh4uICAHjrrbcQERGBoKAgzJkzBxcvXkR0dDSioqKk/U6fPh29evXCypUr4efnh61bt+Ls2bNaj6gjIpKbmnTuWZi+JwvV53egsX//l0bOuQPyzr+25K6L49N5IV9TJ2xSKpVY3rXUkCqT02RNGnKepEdf2CbFM9Z2qa7Jms6ePYs+ffpIrzUng4GBgdiwYQPOnz+PLVu2ICsrCy4uLvDx8cGSJUu0Lg+NjY1FSEgI+vXrJ93juWbNGmm9ra0tEhISEBwcDA8PDzRs2BALFy7Uetb8K6+8gri4OMyfPx/vvvsuWrZsid27d/OyUCKSrZp67lmYviYLrY5zT2P9/i8POecOyDv/mp67Ls4/dV7I17QJmwpPRtP5/US95ieHyZo0asIkPbrGNimesbdLdU3W1Lt3bwhR8ojKwYMHy9yGg4ODdD9nSTp06IAffvih1Jg33ngDb7zxRpn7IyKSg5p27lmYvicL1ee5p7F//5dGzrkD8s6/tuSui/NPvT9+rqZM2GRhYaG3SZoK70Nu5DxJj76wTYpnrO1ijDkREVHl1ZRzz8L0NVlodXwHGuv3f3nIOXdA3vnX9Nx1cWymVd5CGUqbsEmjuAmbjh07pnXvQEkTNhXGCZuIiIiIajeeexJRbVDhQv7hw4dITU1FamoqgH8mbLp58yYePnyIWbNm4eTJk/j1119x+PBhvP766yVO2HT69Gn8+OOPxU7YZGlpiaCgIFy6dAnbtm1DdHS01qVJ06dPR3x8PFauXIkrV64gPDwcZ8+eRUhIiA6ahYiIiIiMAc89iYiKqnAhf/bsWXTu3BmdO3cGUDBhU+fOnbFw4UKYmZnh/PnzeO211/DCCy8gKCgIHh4e+OGHH4pM2NS6dWv069cPgwYNQo8ePbRmTNZM2JSWlgYPDw/MnDmzxAmbPv30U3Ts2BHffvstJ2wiIiIiqmF47klEVFSF75HnhE1EREREVF147klEVJTe75EnIiIiIiIiIt1hIU9EREREREQkIyzkiYiIiIiIiGSEhTwRERERERGRjLCQJyIiIiIiIpIRFvJEREREREREMsJCnoiIiIiIiEhGWMgTERERERERyQgLeSIiIiIiIiIZYSFPREREREREJCMs5ImIiIiIiIhkhIU8ERERERERkYywkCciIiIiIiKSERbyRERERERERDLCQp6IiIiIiIhIRljIExEREREREckIC3kiIiIiIiIiGWEhT0RERERERCQjLOSJiIiIiIiIZISFPBEREREREZGMsJAnIiIiIiIikhEW8kREREREREQywkKeiIiIiIiISEZYyBMRERERERHJCAt5IiIiIiIiIhlhIU9EVA2OHTuGIUOGwMXFBSYmJti9e7fWeiEEFi5ciMaNG8Pa2hre3t64du2aVszdu3cxevRo2NjYwM7ODkFBQXj48KFWzPnz5/Hqq6/CysoKrq6uWL58eZFcduzYgdatW8PKygrt27fH/v37dX68RERERKQ/LOSJiKpBTk4OOnbsiPXr1xe7fvny5VizZg1iYmJw6tQp1K1bF76+vnjy5IkUM3r0aFy6dAlKpRJ79+7FsWPHMHnyZGl9dnY2fHx84ObmhpSUFKxYsQLh4eH49NNPpZgTJ05g1KhRCAoKwrlz5+Dv7w9/f39cvHhRfwdPRERERDplbugEiIhqg4EDB2LgwIHFrhNCYPXq1Zg/fz5ef/11AMCXX34JJycn7N69GyNHjsQvv/yC+Ph4nDlzBl26dAEArF27FoMGDcJHH30EFxcXxMbGIi8vDxs3boSlpSVefPFFpKamYtWqVVLBHx0djQEDBmDWrFkAgCVLlkCpVGLdunWIiYmphpYgIiIioqqq8Ig8Lw8lItKttLQ0pKenw9vbW1pma2sLT09PJCcnAwCSk5NhZ2cnFfEA4O3tDVNTU5w6dUqK6dmzJywtLaUYX19fXL16Fffu3ZNiCu9HE6PZDxGRseG5JxFRURUekddcHjphwgQMGzasyHrN5aFbtmyBu7s7FixYAF9fX1y+fBlWVlYACi4PvX37NpRKJVQqFcaPH4/JkycjLi4OwD+Xh3p7eyMmJgYXLlzAhAkTYGdnJ40qaS4PjYyMxODBgxEXFwd/f3/89NNPaNeuXVXahIioWqWnpwMAnJyctJY7OTlJ69LT0+Ho6Ki13tzcHA4ODlox7u7uRbahWWdvb4/09PRS91Oc3Nxc5ObmSq+zs7MBACqVCiqVqszj08QoTEWZsZVVnjwMTZOjHHLVJ7ZDgZrQDtWVO889iYiKqnAhz8tDiYhql8jISERERBRZnpCQgDp16pR7O0u6qHWZlhY5jYoplUpDp2AU2A4F5NwOjx49qpb98NyTiKgond4jX9bloSNHjizz8tChQ4eWeHnosmXLcO/ePdjb2yM5ORlhYWFa+/f19S1yuRURkbFzdnYGAGRkZKBx48bS8oyMDHTq1EmKyczM1Hrf06dPcffuXen9zs7OyMjI0IrRvC4rRrO+OPPmzdPqb7Ozs+Hq6gofHx/Y2NiUeXwqlQpKpRILzpoiV21SZnxlXAz31ct2dUnTDv3794eFhYWh0zEYtkOBmtAOmqtzDInnnkRUW+m0kK+Jl4cWvvRNYaa/y0IL70sOasIlgbrGNimesbeLMeTl7u4OZ2dnHD58WCrcs7OzcerUKUydOhUA4OXlhaysLKSkpMDDwwMAkJiYCLVaDU9PTynmvffeg0qlkgoDpVKJVq1awd7eXoo5fPgwQkNDpf0rlUp4eXmVmJ9CoYBCoSiy3MLCokIFSK7aBLn5+ink5VQIVbTdaiq2QwE5t4Mx5F0Tzz0L0/etSfr8DjT27//SyDl3QN7515bcdXF8tWrW+qpcHqpUKrG8q74yKyCnS0M15HxJoL6wTYpnrO1SXZeGPnz4ENevX5dep6WlITU1FQ4ODmjatClCQ0OxdOlStGzZUrrH08XFBf7+/gCANm3aYMCAAZg0aRJiYmKgUqkQEhKCkSNHwsXFBQDw1ltvISIiAkFBQZgzZw4uXryI6OhoREVFSfudPn06evXqhZUrV8LPzw9bt27F2bNntR5RR0REumHstyZVx7mnsX7/l4eccwfknX9Nz10X5586LeRr4uWhhS996/x+Yonb1gU5XBqqURMuCdQ1tknxjL1dquvS0LNnz6JPnz7Sa01fFBgYiM2bN2P27NnIycnB5MmTkZWVhR49eiA+Pl6aqAkAYmNjERISgn79+sHU1BQBAQFYs2aNtN7W1hYJCQkIDg6Gh4cHGjZsiIULF2o9a/6VV15BXFwc5s+fj3fffRctW7bE7t27OVETEclSTTz3LEzftybp89zT2L//SyPn3AF5519bctfF+adOC/mafHmohYWF3i4JLbwPuZHzJYH6wjYpnrG2S3Xl1Lt3bwhR8qWRJiYmWLx4MRYvXlxijIODgzTDckk6dOiAH374odSYN954A2+88UbpCRMRyUBNPvcsTF+3JlXHd6Cxfv+Xh5xzB+Sdf03PXRfHVuHnyD98+BCpqalITU0F8M/loTdv3oSJiYl0eej333+PCxcuYOzYsSVeHnr69Gn8+OOPxV4eamlpiaCgIFy6dAnbtm1DdHS01l80p0+fjvj4eKxcuRJXrlxBeHg4zp49i5CQkCo3ChEREREZB557EhEVVeEReV4eSkRERETVheeeRERFVbiQ5+WhRERERFRdeO5JRFRUhS+tJyIiIiIiIiLDYSFPREREREREJCMs5ImIiIiIiIhkhIU8ERERERERkYywkCciIiIiIiKSERbyRERERERERDLCQp6IiIiIiIhIRljIExEREREREckIC3kiIiIiIiIiGWEhT0RERERERCQjLOSJiIiIiIiIZISFPBEREREREZGMsJAnIiIiIiIikhEW8kREREREREQywkKeiIiIiIiISEZYyBMRERERERHJCAt5IiIiIiIiIhlhIU9EREREREQkIyzkiYiIiIiIiGSEhTwRERERERGRjLCQJyIiIiIiIpIRFvJEREREREREMsJCnoiIiIiIiEhGWMgTERERERERyQgLeSIiIiIiIiIZYSFPREREREREJCMs5ImIiIiIiIhkhIU8EZGRCA8Ph4mJidZP69atpfVPnjxBcHAwGjRogHr16iEgIAAZGRla27h58yb8/PxQp04dODo6YtasWXj69KlWTFJSEl566SUoFAq0aNECmzdvro7DIyIiIiIdYSFPRGREXnzxRdy+fVv6OX78uLRuxowZ2LNnD3bs2IGjR4/i1q1bGDZsmLQ+Pz8ffn5+yMvLw4kTJ7BlyxZs3rwZCxculGLS0tLg5+eHPn36IDU1FaGhoZg4cSIOHjxYrcdJRERERJWn80KeI0pERJVnbm4OZ2dn6adhw4YAgPv37+OLL77AqlWr0LdvX3h4eGDTpk04ceIETp48CQBISEjA5cuX8fXXX6NTp04YOHAglixZgvXr1yMvLw8AEBMTA3d3d6xcuRJt2rRBSEgIhg8fjqioKIMdMxFRVfH8k4hqG72MyHNEiYiocq5duwYXFxc0b94co0ePxs2bNwEAKSkpUKlU8Pb2lmJbt26Npk2bIjk5GQCQnJyM9u3bw8nJSYrx9fVFdnY2Ll26JMUU3oYmRrMNIiK54vknEdUm5nrZ6P8fUXqWZkQpLi4Offv2BQBs2rQJbdq0wcmTJ9GtWzdpROnQoUNwcnJCp06dsGTJEsyZMwfh4eGwtLTUGlECgDZt2uD48eOIioqCr6+vPg6JiEjvPD09sXnzZrRq1Qq3b99GREQEXn31VVy8eBHp6emwtLSEnZ2d1nucnJyQnp4OAEhPT9cq4jXrNetKi8nOzsbjx49hbW1dJK/c3Fzk5uZKr7OzswEAKpUKKpWqzOPSxChMRZmxlVWePAxNk6McctUntkOBmtAOxpY7zz+JqDbRSyGvGVGysrKCl5cXIiMj0bRp0zJHlLp161biiNLUqVNx6dIldO7cucQRpdDQ0FLzqszJaOEvWoWZ/k5CC+9LDmrCCYiusU2KZ+ztYkx5DRw4UPp3hw4d4OnpCTc3N2zfvr3YAru6REZGIiIiosjyhIQE1KlTp9zbWdJFrcu0tOzfv19v29Y1pVJp6BSMAtuhgJzb4dGjR4ZOQYsxnn8a+x9C9fkdaOzf/6WRc+6AvPOvLbnr4vh0Xsgb64gSULWTUaVSieVdSw2pMjmdiGrI+QREX9gmxTPWdjG2E9HC7Ozs8MILL+D69evo378/8vLykJWVpdWHZmRkSCNQzs7OOH36tNY2NPeAFo559r7QjIwM2NjYlNh3zps3D2FhYdLr7OxsuLq6wsfHBzY2NmUeh0qlglKpxIKzpshVm5R94JVwMdz4R8M07dC/f39YWFgYOh2DYTsUqAntoClKjYGxnn8a+x9Cq+Pc01i//8tDzrkD8s6/pueui/NPnRfyxjqiBFTuZLTwF23n9xP1mp8cTkQ1asIJiK6xTYpn7O1iTCeiz3r48CFu3LiBMWPGwMPDAxYWFjh8+DACAgIAAFevXsXNmzfh5eUFAPDy8sL777+PzMxMODo6Aij4MrGxsUHbtm2lmGdP3JRKpbSN4igUCigUiiLLLSwsKvT/NFdtgtx8/RTyxvjZKklF262mYjsUkHM7GFPexnr+aex/CNXnuaexf/+XRs65A/LOv7bkrovzT71cWl+YsYwoAVU7GbWwsNDbCWjhfciNnE9A9IVtUjxjbRdjyumdd97BkCFD4Obmhlu3bmHRokUwMzPDqFGjYGtri6CgIISFhcHBwQE2NjaYNm0avLy80K1bNwCAj48P2rZtizFjxmD58uVIT0/H/PnzERwcLPV9U6ZMwbp16zB79mxMmDABiYmJ2L59O/bt22fIQyci0iljOf809j+EVsd3oLF+/5eHnHMH5J1/Tc9dF8em9+fIa0aUGjdurDWipFHciNKFCxeQmZkpxRQ3olR4G5qY0kaUiIiM3R9//IFRo0ahVatWePPNN9GgQQOcPHkSjRo1AgBERUVh8ODBCAgIQM+ePeHs7IydO3dK7zczM8PevXthZmYGLy8vvP322xg7diwWL14sxbi7u2Pfvn1QKpXo2LEjVq5cic8//5wTNRFRjcLzTyKq6XQ+Is8RJSKiytm6dWup662srLB+/XqsX7++xBg3N7cy73ns3bs3zp07V6kciYiMEc8/iai20XkhrxlRunPnDho1aoQePXoUGVEyNTVFQEAAcnNz4evri48//lh6v2ZEaerUqfDy8kLdunURGBhY7IjSjBkzEB0djSZNmnBEiYiIiKiW4vknEdU2Oi/kOaJERERERNWJ559EVNvo/R55IiIiIiIiItIdFvJEREREREREMsJCnoiIiIiIiEhGWMgTERERERERyQgLeSIiIiIiIiIZYSFPREREREREJCMs5ImIiIiIiIhkhIU8ERERERERkYywkCciIiIiIiKSERbyRERERERERDLCQp6IiIiIiIhIRljIExEREREREckIC3kiIiIiIiIiGWEhT0RE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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import matplotlib.pyplot as plt\n", + "\n", + "df.hist(figsize=(12,8))\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "918baff7-b9ce-4904-8a64-eea7422f72f2", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "MedHouseVal 1.000000\n", + "MedInc 0.688075\n", + "AveRooms 0.151948\n", + "HouseAge 0.105623\n", + "AveOccup -0.023737\n", + "Population -0.024650\n", + "Longitude -0.045967\n", + "AveBedrms -0.046701\n", + "Latitude -0.144160\n", + "Name: MedHouseVal, dtype: float64" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df.corr()[\"MedHouseVal\"].sort_values(ascending=False)" + ] + }, + { + "cell_type": "markdown", + "id": "c6249858-be10-4088-91fd-03ccd14ea941", + "metadata": {}, + "source": [ + "## 3. Train/Test Split\n", + "\n", + "Standard 80/20 split with `random_state=42` for reproducibility. We\n", + "hold out 20% as the test set and never look at it during training or\n", + "tuning — it's only used for the final evaluation in section 4." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "1804a672-086b-4202-85f2-80bd4798caeb", + "metadata": {}, + "outputs": [], + "source": [ + "from sklearn.model_selection import train_test_split\n", + "\n", + "X = df.drop(\"MedHouseVal\", axis=1)\n", + "y = df[\"MedHouseVal\"]\n", + "\n", + "X_train, X_test, y_train, y_test = train_test_split(\n", + " X, y, test_size=0.2, random_state=42\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "0ba99c82-52ef-41e9-b8d3-eba3c066124a", + "metadata": {}, + "source": [ + "## 4. Baseline Model — RandomForestRegressor\n", + "\n", + "Start with a plain scikit-learn baseline so we have something to beat\n", + "once we bring Ray Tune into the picture. RandomForest is a sensible\n", + "default for tabular regression: little preprocessing required, robust\n", + "to feature scale, and fast enough on a dataset this size." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "dfdd3d1f-dca6-4916-b215-154860627f47", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
RandomForestRegressor(random_state=42)
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
" + ], + "text/plain": [ + "RandomForestRegressor(random_state=42)" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from sklearn.ensemble import RandomForestRegressor\n", + "\n", + "model = RandomForestRegressor(n_estimators=100, random_state=42)\n", + "model.fit(X_train, y_train)" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "ee8a8408-3378-4d16-a2b9-2b5d2c605ec7", + "metadata": {}, + "outputs": [], + "source": [ + "y_pred = model.predict(X_test)" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "7da99711-458f-4ff5-8efb-968aa7ab4865", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Baseline RMSE: 0.5053399773665033\n", + "Baseline R^2 : 0.8051230593157366\n" + ] + } + ], + "source": [ + "from sklearn.metrics import root_mean_squared_error, r2_score\n", + "\n", + "rmse = root_mean_squared_error(y_test, y_pred)\n", + "r2 = r2_score(y_test, y_pred)\n", + "\n", + "print(\"Baseline RMSE:\", rmse)\n", + "print(\"Baseline R^2 :\", r2)" + ] + }, + { + "cell_type": "markdown", + "id": "a6ba899f-5440-4f98-921e-357c18a1431b", + "metadata": {}, + "source": [ + "## 5. Distributed Training with Ray Core\n", + "\n", + "Now bring Ray into the picture. The simplest Ray primitive is the\n", + "`@ray.remote` decorator: it turns an ordinary Python function into a\n", + "\"remote function\" that returns a future instead of a value, and Ray\n", + "schedules invocations across all available CPU cores.\n", + "\n", + "The point isn't that we couldn't loop over `n_estimators` ourselves —\n", + "we obviously can. The point is that the same `@ray.remote` syntax\n", + "scales unchanged from one laptop to a multi-node cluster." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "b7699268-cb40-451f-93c3-f7366eaa7cfe", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2026-05-05 20:59:13,985\tINFO worker.py:1789 -- Calling ray.init() again after it has already been called.\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "
\n", + "
\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "\n", + "\n", + "
Python version:3.12.13
Ray version:2.49.0
Dashboard:http://127.0.0.1:8265
\n", + "\n", + "
\n", + "
\n" + ], + "text/plain": [ + "RayContext(dashboard_url='127.0.0.1:8265', python_version='3.12.13', ray_version='2.49.0', ray_commit='66438d8bd27f8c604ee5a0cd2cfc5649053285ed')" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import ray\n", + "\n", + "ray.init(ignore_reinit_error=True)" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "af3bc6df-efbf-4c04-a4e5-f4c85cd6182b", + "metadata": {}, + "outputs": [], + "source": [ + "@ray.remote\n", + "def train_model(n_estimators):\n", + " from sklearn.ensemble import RandomForestRegressor\n", + " from sklearn.metrics import mean_squared_error, r2_score\n", + " \n", + " model = RandomForestRegressor(\n", + " n_estimators=n_estimators,\n", + " random_state=42\n", + " )\n", + " \n", + " model.fit(X_train, y_train)\n", + " preds = model.predict(X_test)\n", + " \n", + " rmse = mean_squared_error(y_test, preds, squared=False)\n", + " r2 = r2_score(y_test, preds)\n", + " \n", + " return {\"n_estimators\": n_estimators, \"rmse\": rmse, \"r2\": r2}" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "43904978-18a3-4ec4-9a07-1b468a36e5cb", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[36m(train_model pid=3350)\u001b[0m /usr/local/lib/python3.12/site-packages/sklearn/metrics/_regression.py:492: FutureWarning: 'squared' is deprecated in version 1.4 and will be removed in 1.6. To calculate the root mean squared error, use the function'root_mean_squared_error'.\n", + "\u001b[36m(train_model pid=3350)\u001b[0m warnings.warn(\n", + "\u001b[36m(train_model pid=3358)\u001b[0m /usr/local/lib/python3.12/site-packages/sklearn/metrics/_regression.py:492: FutureWarning: 'squared' is deprecated in version 1.4 and will be removed in 1.6. To calculate the root mean squared error, use the function'root_mean_squared_error'.\n", + "\u001b[36m(train_model pid=3358)\u001b[0m warnings.warn(\n" + ] + }, + { + "data": { + "text/plain": [ + "[{'n_estimators': 50, 'rmse': 0.5072454330767726, 'r2': 0.8036506665860602},\n", + " {'n_estimators': 100, 'rmse': 0.5053399773665033, 'r2': 0.8051230593157366},\n", + " {'n_estimators': 200, 'rmse': 0.5039602414072009, 'r2': 0.8061857564039718}]" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "results = ray.get([\n", + " train_model.remote(50),\n", + " train_model.remote(100),\n", + " train_model.remote(200)\n", + "])\n", + "\n", + "results" + ] + }, + { + "cell_type": "markdown", + "id": "58f20a7a-7532-4946-9fe5-1e8796002431", + "metadata": {}, + "source": [ + "## 6. Hyperparameter Tuning with Ray Tune\n", + "\n", + "Ray Tune is a hyperparameter search library built on top of Ray Core.\n", + "You define a \"trainable\" — a function that takes a `config` dict and\n", + "reports a metric — and Tune handles scheduling, parallel execution,\n", + "and result aggregation.\n", + "\n", + "Below we sweep over `n_estimators` and `max_depth`. With\n", + "`num_samples=6` and three values per parameter, Tune randomly picks\n", + "six configurations from the 9-cell grid and runs them in parallel." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "7e63957f-cdb0-4c24-87c6-aba611a4bd4e", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[36m(train_model pid=3355)\u001b[0m /usr/local/lib/python3.12/site-packages/sklearn/metrics/_regression.py:492: FutureWarning: 'squared' is deprecated in version 1.4 and will be removed in 1.6. To calculate the root mean squared error, use the function'root_mean_squared_error'.\n", + "\u001b[36m(train_model pid=3355)\u001b[0m warnings.warn(\n" + ] + } + ], + "source": [ + "from ray import tune\n" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "c1855bf6-67a5-4bd9-9766-b2279d49decb", + "metadata": {}, + "outputs": [], + "source": [ + "def train_tune(config, X_train, y_train, X_test, y_test):\n", + " \"\"\"\n", + " A Ray Tune \"trainable\" function. Tune calls this with different\n", + " `config` dicts (one per trial) and aggregates the reported metric.\n", + "\n", + " The training/test data is passed in explicitly via tune.with_parameters\n", + " rather than captured from the notebook namespace, because Ray ships\n", + " the trainable to worker processes that don't share the notebook's\n", + " globals.\n", + " \"\"\"\n", + " from sklearn.ensemble import RandomForestRegressor\n", + " from sklearn.metrics import root_mean_squared_error\n", + "\n", + " model = RandomForestRegressor(\n", + " n_estimators=config[\"n_estimators\"],\n", + " max_depth=config[\"max_depth\"],\n", + " random_state=42,\n", + " )\n", + " model.fit(X_train, y_train)\n", + " preds = model.predict(X_test)\n", + "\n", + " rmse = root_mean_squared_error(y_test, preds)\n", + "\n", + " # Report the metric back to Tune (replaces the deprecated session.report)\n", + " tune.report({\"rmse\": rmse})" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "f0742439-2966-4189-aeeb-b5f99c4a4f64", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2026-05-05 20:59:38,123\tINFO tune.py:616 -- [output] This uses the legacy output and progress reporter, as Jupyter notebooks are not supported by the new engine, yet. For more information, please see https://github.com/ray-project/ray/issues/36949\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "
\n", + "
\n", + "

Tune Status

\n", + " \n", + "\n", + "\n", + "\n", + "\n", + "\n", + "
Current time:2026-05-05 21:00:07
Running for: 00:00:29.84
Memory: 3.9/7.6 GiB
\n", + "
\n", + "
\n", + "
\n", + "

System Info

\n", + " Using FIFO scheduling algorithm.
Logical resource usage: 1.0/16 CPUs, 0/0 GPUs\n", + "
\n", + " \n", + "
\n", + "
\n", + "
\n", + "

Trial Status

\n", + " \n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "
Trial name status loc max_depth n_estimators iter total time (s) rmse
train_tune_54105_00000TERMINATED172.17.0.2:4423 20 200 1 23.9948 0.504571
train_tune_54105_00001TERMINATED172.17.0.2:4424 5 200 1 9.4503 0.680186
train_tune_54105_00002TERMINATED172.17.0.2:4425 5 100 1 5.603270.680291
train_tune_54105_00003TERMINATED172.17.0.2:4422 20 50 1 7.848410.507399
train_tune_54105_00004TERMINATED172.17.0.2:4426 5 50 1 3.441520.680254
train_tune_54105_00005TERMINATED172.17.0.2:4421 20 200 1 24.0285 0.504571
\n", + "
\n", + "
\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "

Trial Progress

\n", + " \n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "
Trial name rmse
train_tune_54105_000000.504571
train_tune_54105_000010.680186
train_tune_54105_000020.680291
train_tune_54105_000030.507399
train_tune_54105_000040.680254
train_tune_54105_000050.504571
\n", + "
\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2026-05-05 21:00:07,985\tINFO tune.py:1009 -- Wrote the latest version of all result files and experiment state to '/root/ray_results/train_tune_2026-05-05_20-59-38' in 0.0041s.\n", + "2026-05-05 21:00:07,991\tINFO tune.py:1041 -- Total run time: 29.87 seconds (29.84 seconds for the tuning loop).\n" + ] + } + ], + "source": [ + "# tune.with_parameters serializes the training data once and passes it\n", + "# into every trial. This is the recommended way to give a Tune trainable\n", + "# access to large objects without polluting the trainable's signature.\n", + "trainable_with_data = tune.with_parameters(\n", + " train_tune,\n", + " X_train=X_train,\n", + " y_train=y_train,\n", + " X_test=X_test,\n", + " y_test=y_test,\n", + ")\n", + "\n", + "analysis = tune.run(\n", + " trainable_with_data,\n", + " config={\n", + " \"n_estimators\": tune.choice([50, 100, 200]),\n", + " \"max_depth\": tune.choice([5, 10, 20]),\n", + " },\n", + " num_samples=6,\n", + " metric=\"rmse\",\n", + " mode=\"min\",\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "0671a94d-3e38-4197-b00e-3213dfd5a06e", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Best config: {'n_estimators': 200, 'max_depth': 20}\n" + ] + } + ], + "source": [ + "# Pull the best configuration from the Tune analysis object.\n", + "best_config = analysis.get_best_config(metric=\"rmse\", mode=\"min\")\n", + "print(\"Best config:\", best_config)" + ] + }, + { + "cell_type": "markdown", + "id": "d5b9741c-3f57-42cc-81fd-28796189f60b", + "metadata": {}, + "source": [ + "## 7. Refit and Save the Best Model\n", + "\n", + "Tune's `analysis.get_best_config(...)` only returns hyperparameters,\n", + "not a fitted model. We refit a fresh `RandomForestRegressor` with\n", + "those parameters on the full training set, then persist it with\n", + "`joblib` so the Ray Serve deployment can load it independently of\n", + "the notebook session." + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "df671f4c-48ee-4309-af89-0523229a23d3", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Tuned RMSE: 0.5045713885354675\n", + "Tuned R^2 : 0.8057153985083529\n" + ] + } + ], + "source": [ + "# Refit the model with the best hyperparameters Tune found.\n", + "# This is the model we'll actually deploy — not the baseline from section 4.\n", + "best_model = RandomForestRegressor(\n", + " n_estimators=best_config[\"n_estimators\"],\n", + " max_depth=best_config[\"max_depth\"],\n", + " random_state=42,\n", + ")\n", + "best_model.fit(X_train, y_train)\n", + "\n", + "# Sanity check: how does the tuned model do on the held-out test set?\n", + "from sklearn.metrics import root_mean_squared_error, r2_score\n", + "tuned_preds = best_model.predict(X_test)\n", + "print(\"Tuned RMSE:\", root_mean_squared_error(y_test, tuned_preds))\n", + "print(\"Tuned R^2 :\", r2_score(y_test, tuned_preds))" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "2d4e8959-a546-4954-9653-d411d106906b", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Saved model.pkl\n" + ] + } + ], + "source": [ + "# Persist the trained model to disk. The Ray Serve deployment in the\n", + "# next section loads from this file inside its constructor, so the API\n", + "# doesn't depend on any notebook-kernel state.\n", + "import joblib\n", + "joblib.dump(best_model, \"model.pkl\")\n", + "print(\"Saved model.pkl\")" + ] + }, + { + "cell_type": "markdown", + "id": "2fb89f1e-cb96-453a-8a49-074a7865bf69", + "metadata": {}, + "source": [ + "## 8. Visualize Predictions vs. Actual\n", + "\n", + "A quick \"predicted vs. actual\" scatter plot is a useful gut-check on\n", + "regression quality. Points clustered along the diagonal mean the model\n", + "predicts well; systematic curvature would suggest the model is missing\n", + "some structure (e.g. log-transformed targets, capped values)." + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "e8353515-ab64-47be-9114-7f58ec37cc35", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAioAAAHHCAYAAACRAnNyAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8hTgPZAAAACXBIWXMAAA9hAAAPYQGoP6dpAACgX0lEQVR4nO2deXgUVbrG3+6ks5J0EgJ02JIAYYkBwg4TQImgLCqiMwou44o6wozL6Cgzg8KgIuM46B1UFAHnqoAbCAriBUERDIJAkBgUiAkgJGAWEhLIQrruH6GaXmo5VV1VXd35fs/Do0mqq053V53znm+1cBzHgSAIgiAIwoRYAz0AgiAIgiAIMUioEARBEARhWkioEARBEARhWkioEARBEARhWkioEARBEARhWkioEARBEARhWkioEARBEARhWkioEARBEARhWkioEARBEARhWkioEAQhi8ViwZw5cwI9jIBzxRVX4IorrnD9XFJSAovFgrfeeitgY/LGe4wEEeyQUCEIg3n11VdhsVgwbNgw1ec4efIk5syZg/z8fO0GZnK+/PJLWCwW1z+bzYZu3brh97//PX7++edAD08R33zzDebMmYMzZ84EeigEYXrCAz0AgmhtvPvuu0hLS8OuXbtw5MgR9OjRQ/E5Tp48iblz5yItLQ3Z2dnaD9LE/OlPf8KQIUPQ1NSEvXv34o033sD69etx4MABdOzY0dCxpKam4vz587DZbIpe980332Du3Lm48847kZCQoM/gCCJEIIsKQRhIcXExvvnmG/z73/9Gu3bt8O677wZ6SEHHqFGjcNttt+Guu+7Cf/7zH/zrX/9CZWUl/vvf/4q+pq6uTpexWCwWREVFISwsTJfzEwRBQoUgDOXdd99FYmIiJk2ahN/+9reiQuXMmTN45JFHkJaWhsjISHTu3Bm///3vUV5eji+//BJDhgwBANx1110uVwgfJ5GWloY777zT55zesQuNjY146qmnMGjQINjtdsTGxmLUqFHYunWr4vd16tQphIeHY+7cuT5/++mnn2CxWLBo0SIAQFNTE+bOnYuMjAxERUWhbdu2GDlyJDZt2qT4ugCQm5sLoEUEAsCcOXNgsVhQWFiIW265BYmJiRg5cqTr+HfeeQeDBg1CdHQ0kpKSMHXqVBw/ftznvG+88Qa6d++O6OhoDB06FF9//bXPMWIxKj/++CNuuukmtGvXDtHR0ejVqxf+9re/ucb3+OOPAwDS09Nd319JSYkuYySIYIdcPwRhIO+++y5uuOEGREREYNq0aXjttdewe/dul/AAgNraWowaNQoHDx7E3XffjYEDB6K8vBzr1q3DL7/8gj59+uAf//gHnnrqKdx3330YNWoUAOA3v/mNorHU1NTgzTffxLRp0zB9+nScPXsWS5cuxdVXX41du3Ypcil16NABl19+Od5//308/fTTHn977733EBYWht/97ncAWhbq+fPn495778XQoUNRU1OD7777Dnv37sW4ceMUvQcAKCoqAgC0bdvW4/e/+93vkJGRgeeeew4cxwEAnn32WcyePRs33XQT7r33Xvz666/4z3/+g9GjR2Pfvn0uN8zSpUtx//334ze/+Q0efvhh/Pzzz7juuuuQlJSELl26SI7n+++/x6hRo2Cz2XDfffchLS0NRUVF+OSTT/Dss8/ihhtuwKFDh7By5UosXLgQycnJAIB27doZNkaCCCo4giAM4bvvvuMAcJs2beI4juOcTifXuXNn7qGHHvI47qmnnuIAcKtXr/Y5h9Pp5DiO43bv3s0B4JYvX+5zTGpqKnfHHXf4/P7yyy/nLr/8ctfPFy5c4BoaGjyOqaqq4jp06MDdfffdHr8HwD399NOS7+/111/nAHAHDhzw+H1mZiaXm5vr+rl///7cpEmTJM8lxNatWzkA3LJly7hff/2VO3nyJLd+/XouLS2Ns1gs3O7duzmO47inn36aA8BNmzbN4/UlJSVcWFgY9+yzz3r8/sCBA1x4eLjr942NjVz79u257Oxsj8/njTfe4AB4fIbFxcU+38Po0aO5uLg47ujRox7X4b87juO4F154gQPAFRcX6z5Gggh2yPVDEAbx7rvvokOHDhgzZgyAlviGm2++GatWrUJzc7PruI8++gj9+/fHlClTfM5hsVg0G09YWBgiIiIAAE6nE5WVlbhw4QIGDx6MvXv3Kj7fDTfcgPDwcLz33nuu3xUUFKCwsBA333yz63cJCQn44YcfcPjwYVXjvvvuu9GuXTt07NgRkyZNQl1dHf773/9i8ODBHsc98MADHj+vXr0aTqcTN910E8rLy13/HA4HMjIyXC6v7777DqdPn8YDDzzg+nwA4M4774Tdbpcc26+//opt27bh7rvvRteuXT3+xvLdGTFGggg2yPVDEAbQ3NyMVatWYcyYMa5YCgAYNmwYXnzxRXzxxRe46qqrALS4Mm688UZDxvXf//4XL774In788Uc0NTW5fp+enq74XMnJybjyyivx/vvvY968eQBa3D7h4eG44YYbXMf94x//wOTJk9GzZ09kZWVh/PjxuP3229GvXz+m6zz11FMYNWoUwsLCkJycjD59+iA83Hcq834Phw8fBsdxyMjIEDwvn7lz9OhRAPA5jk+HloJPk87KymJ6L94YMUaCCDZIqBCEAWzZsgWlpaVYtWoVVq1a5fP3d9991yVU/EVs597c3OyRnfLOO+/gzjvvxPXXX4/HH38c7du3R1hYGObPn++K+1DK1KlTcddddyE/Px/Z2dl4//33ceWVV7riMABg9OjRKCoqwtq1a/F///d/ePPNN7Fw4UIsXrwY9957r+w1+vbti7Fjx8oeFx0d7fGz0+mExWLBZ599Jpil06ZNG4Z3qC/BMEaCMBoSKgRhAO+++y7at2+PV155xedvq1evxpo1a7B48WJER0eje/fuKCgokDyflBshMTFRsJDY0aNHPXbbH374Ibp164bVq1d7nM87GFYJ119/Pe6//36X++fQoUOYNWuWz3FJSUm46667cNddd6G2thajR4/GnDlzmISKWrp37w6O45Ceno6ePXuKHpeamgqgxbrBZxQBLdlKxcXF6N+/v+hr+c9X7fdnxBgJItigGBWC0Jnz589j9erVuOaaa/Db3/7W59/MmTNx9uxZrFu3DgBw4403Yv/+/VizZo3PubiL2SuxsbEAIChIunfvjp07d6KxsdH1u08//dQnvZXfsfPnBIBvv/0WeXl5qt9rQkICrr76arz//vtYtWoVIiIicP3113scU1FR4fFzmzZt0KNHDzQ0NKi+Lgs33HADwsLCMHfuXI/3DLR8Bvy4Bg8ejHbt2mHx4sUen+Fbb70lW0m2Xbt2GD16NJYtW4Zjx475XINH7PszYowEEWyQRYUgdGbdunU4e/YsrrvuOsG/Dx8+3FX87eabb8bjjz+ODz/8EL/73e9w9913Y9CgQaisrMS6deuwePFi9O/fH927d0dCQgIWL16MuLg4xMbGYtiwYUhPT8e9996LDz/8EOPHj8dNN92EoqIivPPOO+jevbvHda+55hqsXr0aU6ZMwaRJk1BcXIzFixcjMzMTtbW1qt/vzTffjNtuuw2vvvoq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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import matplotlib.pyplot as plt\n", + "\n", + "plt.scatter(y_test, y_pred)\n", + "plt.xlabel(\"Actual\")\n", + "plt.ylabel(\"Predicted\")\n", + "plt.title(\"Actual vs Predicted\")\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "0b2e440d-6455-4dd2-8268-0cfeb32c03cc", + "metadata": {}, + "source": [ + "## 9. Deploy the Model with Ray Serve\n", + "\n", + "Ray Serve lets you deploy a Python class as a scalable HTTP endpoint\n", + "with one decorator. It runs inside the same Ray runtime, so there's no\n", + "separate server process to manage.\n", + "\n", + "The deployment exposes a single POST endpoint at `/` that accepts a\n", + "JSON body with a `features` array (8 floats, in the order shown\n", + "earlier) and returns a JSON object with a `prediction` field." + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "b0796efe-94f5-42aa-907b-09bea9e801a8", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "INFO 2026-05-05 21:00:30,707 serve 2614 -- Started Serve in namespace \"serve\".\n", + "\u001b[36m(ProxyActor pid=3347)\u001b[0m INFO 2026-05-05 21:00:30,611 proxy 172.17.0.2 -- Proxy starting on node bb8465ea16b75f3866c004195b0dc2582efd342df4a36521ed99a493 (HTTP port: 8000).\n", + "\u001b[36m(ProxyActor pid=3347)\u001b[0m INFO 2026-05-05 21:00:30,703 proxy 172.17.0.2 -- Got updated endpoints: {}.\n", + "\u001b[36m(ServeController pid=3362)\u001b[0m INFO 2026-05-05 21:00:30,798 controller 3362 -- Deploying new version of Deployment(name='HousingModel', app='default') (initial target replicas: 1).\n", + "\u001b[36m(ProxyActor pid=3347)\u001b[0m INFO 2026-05-05 21:00:30,803 proxy 172.17.0.2 -- Got updated endpoints: {Deployment(name='HousingModel', app='default'): EndpointInfo(route='/', app_is_cross_language=False)}.\n", + "\u001b[36m(ProxyActor pid=3347)\u001b[0m INFO 2026-05-05 21:00:30,811 proxy 172.17.0.2 -- Started .\n", + "\u001b[36m(ServeController pid=3362)\u001b[0m INFO 2026-05-05 21:00:30,903 controller 3362 -- Adding 1 replica to Deployment(name='HousingModel', app='default').\n", + "INFO 2026-05-05 21:00:34,846 serve 2614 -- Application 'default' is ready at http://127.0.0.1:8000/.\n", + "INFO 2026-05-05 21:00:34,850 serve 2614 -- Started .\n" + ] + }, + { + "data": { + "text/plain": [ + "DeploymentHandle(deployment='HousingModel')" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from ray import serve\n", + "import joblib\n", + "import numpy as np\n", + "\n", + "@serve.deployment\n", + "class HousingModel:\n", + " \"\"\"\n", + " A Ray Serve deployment that loads the tuned RandomForest model\n", + " from disk and exposes a single POST endpoint for predictions.\n", + " \"\"\"\n", + "\n", + " def __init__(self, model_path: str = \"model.pkl\"):\n", + " # Load the persisted model. Because this happens inside the\n", + " # deployment's constructor, the API is fully self-contained:\n", + " # it does not depend on the notebook kernel having a `model`\n", + " # variable in scope.\n", + " self.model = joblib.load(model_path)\n", + "\n", + " async def __call__(self, request):\n", + " data = await request.json()\n", + " features = np.array(data[\"features\"]).reshape(1, -1)\n", + " prediction = self.model.predict(features)\n", + " return {\"prediction\": float(prediction[0])}\n", + "\n", + "\n", + "# Launch the deployment. serve.run() boots Serve if it isn't already\n", + "# running, registers the deployment, and starts the HTTP proxy on port 8000.\n", + "serve.run(HousingModel.bind())" + ] + }, + { + "cell_type": "markdown", + "id": "60037ccc-ff57-4da9-b91d-a26e4cbeaff5", + "metadata": {}, + "source": [ + "## 10. Test the Deployed API\n", + "\n", + "With the deployment running, send a sample request from inside the\n", + "notebook to confirm the endpoint works. The same request would work\n", + "from `curl` or any HTTP client outside the container." + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "id": "6b86b8bc-5454-452c-83fd-5a56a0b0770e", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{\"prediction\":0.5023691911163393}\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[36m(ServeReplica:default:HousingModel pid=3348)\u001b[0m /usr/local/lib/python3.12/site-packages/sklearn/base.py:493: UserWarning: X does not have valid feature names, but RandomForestRegressor was fitted with feature names\n", + "\u001b[36m(ServeReplica:default:HousingModel pid=3348)\u001b[0m warnings.warn(\n", + "\u001b[36m(ServeReplica:default:HousingModel pid=3348)\u001b[0m INFO 2026-05-05 21:00:34,882 default_HousingModel 300qx0le b70f7bef-d11c-4af6-abb7-b44f8b1bada7 -- POST / 200 12.0ms\n" + ] + } + ], + "source": [ + "import requests\n", + "\n", + "sample = X_test.iloc[0].tolist()\n", + "\n", + "response = requests.post(\n", + " \"http://127.0.0.1:8000/\",\n", + " json={\"features\": sample}\n", + ")\n", + "\n", + "print(response.text)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.13" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/class_project/data605/Spring2026/projects/UmdTask464_DATA605_Spring2026_Ray_Housing_Price_Prediction/ray_utils.py b/class_project/data605/Spring2026/projects/UmdTask464_DATA605_Spring2026_Ray_Housing_Price_Prediction/ray_utils.py new file mode 100644 index 000000000..85c25b2ae --- /dev/null +++ b/class_project/data605/Spring2026/projects/UmdTask464_DATA605_Spring2026_Ray_Housing_Price_Prediction/ray_utils.py @@ -0,0 +1,13 @@ +from sklearn.datasets import fetch_california_housing +import ray +from ray import data + +def load_data(): + ray.init(ignore_reinit_error=True) + + data_raw = fetch_california_housing(as_frame=True) + df = data_raw.frame + + ray_ds = data.from_pandas(df) + + return ray_ds.to_pandas() \ No newline at end of file diff --git a/class_project/data605/Spring2026/projects/UmdTask464_DATA605_Spring2026_Ray_Housing_Price_Prediction/requirements.txt b/class_project/data605/Spring2026/projects/UmdTask464_DATA605_Spring2026_Ray_Housing_Price_Prediction/requirements.txt new file mode 100644 index 000000000..055ff75a7 --- /dev/null +++ b/class_project/data605/Spring2026/projects/UmdTask464_DATA605_Spring2026_Ray_Housing_Price_Prediction/requirements.txt @@ -0,0 +1,18 @@ +# Ray and its components for distributed compute +# Note: ray[default] already includes core ray; tune and serve add their extras +ray[default,tune,serve]==2.49.0 + +# ML stack +scikit-learn==1.5.2 +pandas==2.2.3 +numpy==1.26.4 +matplotlib==3.9.2 + +# Web API stack (Ray Serve dependencies) +fastapi==0.115.0 +uvicorn==0.31.0 + +# Misc +requests==2.32.3 +joblib==1.4.2 +pyarrow==17.0.0 diff --git a/class_project/data605/Spring2026/projects/UmdTask464_DATA605_Spring2026_Ray_Housing_Price_Prediction/run_jupyter.sh b/class_project/data605/Spring2026/projects/UmdTask464_DATA605_Spring2026_Ray_Housing_Price_Prediction/run_jupyter.sh new file mode 100644 index 000000000..d725c3fe7 --- /dev/null +++ b/class_project/data605/Spring2026/projects/UmdTask464_DATA605_Spring2026_Ray_Housing_Price_Prediction/run_jupyter.sh @@ -0,0 +1,35 @@ +#!/bin/bash +# """ +# Launch Jupyter Lab server. +# +# This script starts Jupyter Lab on port 8888 with the following configuration: +# - No browser auto-launch (useful for Docker containers) +# - Accessible from any IP address (0.0.0.0) +# - Root user allowed (required for Docker environments) +# - No authentication token or password (for development convenience) +# - Vim keybindings can be enabled via JUPYTER_USE_VIM environment variable +# """ + +# Exit immediately if any command exits with a non-zero status. +set -e + +# Print each command to stdout before executing it. +#set -x + +# Import the utility functions from /git_root. +GIT_ROOT=/git_root +source $GIT_ROOT/class_project/project_template/utils.sh + +# Load Docker configuration variables for this script. +get_docker_vars_script ${BASH_SOURCE[0]} +source $DOCKER_NAME +print_docker_vars + +# Setup Jupyter Lab environment. +setup_jupyter_environment + +# Initialize Jupyter Lab command with base configuration. +JUPYTER_ARGS=$(get_jupyter_args) + +# Start Jupyter Lab with development-friendly settings. +run "jupyter lab $JUPYTER_ARGS" diff --git a/class_project/data605/Spring2026/projects/UmdTask464_DATA605_Spring2026_Ray_Housing_Price_Prediction/version.sh b/class_project/data605/Spring2026/projects/UmdTask464_DATA605_Spring2026_Ray_Housing_Price_Prediction/version.sh new file mode 100644 index 000000000..c46ed254c --- /dev/null +++ b/class_project/data605/Spring2026/projects/UmdTask464_DATA605_Spring2026_Ray_Housing_Price_Prediction/version.sh @@ -0,0 +1,28 @@ +#!/bin/bash +# """ +# Display versions of installed tools and packages. +# +# This script prints version information for Python, pip, Jupyter, and all +# installed Python packages. Used for debugging and documentation purposes +# to verify the Docker container environment setup. +# """ + +# Display Python 3 version. +echo "# Python3" +python3 --version + +# Display pip version. +echo "# pip3" +pip3 --version + +# Display Jupyter version. +echo "# jupyter" +jupyter --version + +# List all installed Python packages and their versions. +echo "# Python packages" +pip3 list + +# Template for adding additional tool versions. +# echo "# mongo" +# mongod --version