Interactive 2.5D forest visualization for tree-based ML ensembles
Project description
🌲 PrettyForest
Interactive 2.5D forest visualization for tree-based ML ensembles. Explore your model's structure, trace predictions through individual trees, and understand how the ensemble makes decisions.
Features
- 2.5D isometric forest — trees rendered with depth perspective, growth animation, seasonal themes
- All major frameworks — scikit-learn, LightGBM, CatBoost (RandomForest, GradientBoosting, single DecisionTree)
- Prediction tracing — select a data point, see per-tree corrections/votes, get the ensemble's actual prediction
- True label comparison — pass
target=y_testto see if the model got it right - Interactive detail view — double-click a tree to drill into its decision structure with per-node expansion
- Boosted tree awareness — correct display of gradient corrections vs class probabilities, with explanatory notes
- Dark mode — toggle with 🌙 button
- Scales to thousands — pagination, sorting, spotlighting for large ensembles
Installation
pip install prettyforest
# With optional frameworks
pip install prettyforest[all] # includes lightgbm + catboost
Quick Start
from sklearn.ensemble import RandomForestClassifier
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
import polars as pl
from prettyforest import prettygrow
# Train
iris = load_iris()
X_train, X_test, y_train, y_test = train_test_split(
iris.data, iris.target, test_size=0.3, random_state=42
)
model = RandomForestClassifier(n_estimators=20, max_depth=5, random_state=42)
model.fit(X_train, y_train)
# Visualize
X_test_pl = pl.DataFrame({name: X_test[:, i] for i, name in enumerate(iris.feature_names)})
prettygrow(model, data=X_test_pl, target=y_test, output_path="forest.html")
API
from prettyforest import prettygrow
prettygrow(
model, # Trained tree model (sklearn, LightGBM, or CatBoost)
*,
data=None, # Polars DataFrame or NumPy array for prediction tracing
target=None, # True labels/values — shown alongside predictions
output_path=None, # Write HTML to file; if None, returns string or displays in notebook
feature_names=None, # Column names when data is a NumPy array
seed=42, # Random seed for layout
season=None, # Color theme: "summer", "autumn", "winter", or None (metric-based)
)
Returns: Interactive AnyWidget instance when running inside a notebook environment (JupyterLab, Marimo, VS Code, Google Colab); raw HTML string if outside a notebook and no output_path; None if output_path is specified.
Supported Models
| Model | Type | Trees | Leaf values |
|---|---|---|---|
DecisionTreeClassifier/Regressor |
Single tree | 1 | Direct predictions |
RandomForestClassifier/Regressor |
Bagging | N independent | Class proportions / target means |
GradientBoostingClassifier/Regressor |
Boosting | N × classes sequential | Gradient corrections |
LGBMClassifier/Regressor |
Boosting | N × classes sequential | Log-odds / residuals |
CatBoostClassifier/Regressor |
Boosting | N sequential | Ordered gradient corrections |
Interaction Guide
| Action | What it does |
|---|---|
| Hover a tree | Tooltip with tree index, depth, nodes, leaves, purity/magnitude, and metric rank |
| Double-click a tree | Full interactive decision structure with per-node expand |
| Sort by dropdown | Rearrange into grid by depth/nodes/leaves/metric |
| ◀ ▶ | Page through large ensembles (200/page) |
| 🌙 | Toggle dark mode |
| ❓ | Description on how the ensemble works |
| Trace | Show per-tree badges + ensemble prediction + true label |
| Click a truncated node | Expand that subtree 3 more levels |
Seasons & Themes
Switch the visual theme live in the browser or set it via Python:
prettygrow(model, season="spring") # 🌸 Light greens + pink blossoms
prettygrow(model, season="summer") # 🌿 Deep lush greens
prettygrow(model, season="autumn") # 🍂 Warm oranges, reds, golds
prettygrow(model, season="winter") # ❄️ Bare branches, blue-grey
prettygrow(model) # 🌳 Natural (metric-based coloring)
You can also switch seasons on the fly using the dropdown in the header — no need to re-run Python. The canopies, ground, sky, and grass patches all update instantly.
| Season | Canopy colors | Ground | Best for |
|---|---|---|---|
| 🌳 Natural | Green→amber by metric | Soft green | Analysis (purity/variance encoded in color) |
| 🌸 Spring | Light green + pink/purple | Fresh green | Presentations |
| 🌿 Summer | Deep forest greens | Rich green | Dense forests |
| 🍂 Autumn | Orange, red, gold | Warm brown | Warm aesthetics |
| ❄️ Winter | Bare (no canopy) | Blue-grey | Seeing structure clearly |
Prediction Display
When you trace a sample:
- Per-tree badges show on each tree's canopy:
- Random Forest: the class vote (colored by class)
- Boosted models: the raw correction value (green = positive, red = negative)
- Ensemble prediction: the actual
model.predict()output — always correct - True label: shown in green if
targetwas provided
Marimo & Jupyter Notebook Integration
PrettyForest uses native AnyWidget support for bi-directional synchronization and interactive rendering across all notebook environments (JupyterLab, Marimo desktop/cloud, VS Code, and Google Colab).
from sklearn.ensemble import RandomForestClassifier
from sklearn.datasets import load_iris
import polars as pl
from prettyforest import prettygrow
iris = load_iris()
model = RandomForestClassifier(n_estimators=20, max_depth=5, random_state=42)
model.fit(iris.data, iris.target)
X_pl = pl.DataFrame({name: iris.data[:, i] for i, name in enumerate(iris.feature_names)})
# Directly returns and displays an interactive AnyWidget inside Jupyter or Marimo
prettygrow(model, data=X_pl, target=iris.target)
Understanding Boosted vs Bagged Trees
Random Forest (bagged): Each tree trains independently on a random data subset. Leaves contain real class proportions or target means. Ensemble averages/votes.
Gradient Boosting / LightGBM / CatBoost (boosted): Trees train sequentially — each corrects the previous ensemble's errors. Leaf values are small gradient adjustments, not standalone predictions. Ensemble sums corrections.
When you double-click a boosted tree, a warning appears:
⚠️ This is a boosted tree — leaf values are gradient corrections, not final predictions.
The splits and features are fully interpretable — they show which features matter and how the space is partitioned. The leaf values just represent "how much to adjust" rather than "what to predict."
Development
git clone https://github.com/fabioscantamburlo/prettyforest.git
cd prettyforest
uv sync
# Run tests
uv run pytest
# Run experiments (all models)
uv run run_experiments.py --n-samples 1000 --n-trees 50 --max-depth 8
# Iris example (quick, all frameworks)
uv run examples/iris_forest.py
# MNIST example (larger, RF only)
uv run examples/mnist_forest.py
Pre-commit
pre-commit install # set up hooks
pre-commit run --all-files # manual run
Hooks: trailing whitespace, end-of-file, YAML check, large file guard (500KB), ruff lint + format.
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file prettyforest-0.3.0.tar.gz.
File metadata
- Download URL: prettyforest-0.3.0.tar.gz
- Upload date:
- Size: 222.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.8.4
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2ce2c6977cff04f1d9a7cbf1973c8a4e91642670469b64ff81147cfafff99a20
|
|
| MD5 |
322cbc7e7d45f9120a9e47a0f1112ccc
|
|
| BLAKE2b-256 |
eefe2029e21a26400ef5f84d04e73e72dedbcd1089c339a9dae66806277b5320
|
File details
Details for the file prettyforest-0.3.0-py3-none-any.whl.
File metadata
- Download URL: prettyforest-0.3.0-py3-none-any.whl
- Upload date:
- Size: 45.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.8.4
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3bebb36a1a9631a4bb9cffc2c59b4c78709b1c86a6929988c2c0d0cfd3081a25
|
|
| MD5 |
6802b9d013c7f2ed81be0359c622d4cf
|
|
| BLAKE2b-256 |
9d5db18a62b90d7afe1c8bfe5ff752b8e6330cbc8c24fdd45d74e8198ab3e055
|