Skip to main content
rule

scikit-learn — Cursor rule: purpose and installation

This Cursor rule guides Python work with scikit-learn, with advice on combining preprocessing and models and keeping test data out of training steps.

Install into Cursor · Manually: put the files in place All methods and platforms ↓
.cursor/rules/scikit-learn.mdc
Open on GitHub

What scikit-learn does

The rule applies to **/*.py files. It recommends splitting data before preprocessing, learning transformation parameters from the training portion, then applying those parameters to both sets. A Pipeline can keep preprocessing and a model together so the same sequence runs for training and prediction.

For custom estimators, the guidance follows the scikit-learn API: keep hyperparameters in __init__, store learned attributes with a trailing underscore, and return self from fit. It also calls for setting random_state on operations that use randomness. Otherwise, their results can vary between runs.

Place the rule at .cursor/rules/scikit-learn.mdc to add it to Cursor. The visible guidance mentions Cython for performance-critical custom algorithms, but does not provide the full set of conventions.

What it helps with

  • chain preprocessing and a model in a Pipeline
  • split data before fitting transformations
  • prevent test data from leaking into preprocessing
  • align a custom estimator with the scikit-learn API
  • make random operations reproducible

Example prompts for the agent

  • Review this scikit-learn Python code for data leakage during preprocessing.
  • Put the feature transformations and model into a Pipeline.
  • Update this custom transformer to follow the scikit-learn API and set random_state where randomness is involved.

How to install scikit-learn

Manually: put the files in place

.cursor/rules/scikit-learn.mdc

Frontmatter: description, globs, alwaysApply

Step by step, with commands for each platform: Cursor rules: .mdc files, four modes and what replaced .cursorrules

Questions about scikit-learn

How do I install scikit-learn in Cursor?

Create the rule file at .cursor/rules/scikit-learn.mdc and put the rule contents there.

What does the scikit-learn rule do?

It guides Python code that uses scikit-learn, covering Pipelines, data splitting, custom estimator conventions, and reproducibility.

How does the rule prevent data leakage?

It says to split the data first and fit preprocessing steps only on the training set. Apply the learned transformations to the test set with transform.

Source text

scikit-learn.mdc · CC0-1.0
---
description: Definitive guidelines for writing robust, maintainable, and performant scikit-learn code, emphasizing consistent preprocessing, API adherence, and data leakage prevention.
globs: **/*.py
---
# scikit-learn Best Practices

This guide outlines our team's definitive best practices for using and extending scikit-learn. Adhering to these rules ensures consistent, reproducible, and production-ready machine learning code.

## 1. Code Organization and Structure

### 1.1. Always Use Pipelines for Preprocessing and Models

**Pipelines are mandatory.** They prevent data leakage, ensure consistent transformations across training and inference, and simplify hyperparameter tuning.

❌ BAD: Inconsistent manual transformations

```python
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_error
import numpy as np

X, y = np.random.rand(100, 5), np.random.rand(100)
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=42)

scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
model = LinearRegression().fit(X_train_scaled, y_train)

# Forgetting to scale X_test leads to incorrect predictions
y_pred = model.predict(X_test)
print(f"MSE (BAD): {mean_squared_error(y_test, y_pred):.2f}")
```

✅ GOOD: Encapsulate all steps in a `Pipeline`

```python
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_error
import numpy as np

X, y = np.random.rand(100, 5), np.random.rand(100)
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=42)

# All steps are chained and applied consistently
model = make_pipeline(StandardScaler(), LinearRegression())
model.fit(X_train, y_train)
y_pred = model.predict(X_test)
print(f"MSE (GOOD): {mean_squared_error(y_test, y_pred):.2f}")
```

### 1.2. Custom Estimators Must Adhere to the scikit-learn API

When creating custom transformers or models, strictly follow the scikit-learn estimator API for seamless integration with pipelines and model selection tools.

-   Inherit from `BaseEstimator` and relevant mixins (`TransformerMixin`, `ClassifierMixin`, `RegressorMixin`).
-   `__init__` stores *only* hyperparameters as keyword arguments with defaults. No logic or data processing here.
-   `fit(self, X, y=None)` learns parameters from data. Store learned attributes with a trailing underscore (e.g., `self.mean_`). Return `self`.
-   `transform(self, X)` (for transformers) or `predict(self, X)`/`predict_proba(self, X)` (for predictors) applies the learned transformation/prediction.
-   Implement `get_params` and `set_params` (usually inherited from `BaseEstimator`).

❌ BAD: Non-compliant custom estimator

```python
class BadCustomScaler:
    def __init__(self, scale_factor):
        # Logic in __init__
        if scale_factor <= 0:
            raise ValueError("Scale factor must be positive")
        self.scale_factor = scale_factor
        self.mean_ = None # Should be learned in fit

    def fit(self, X):
        self.mean_ = X.mean(axis=0)
        return self

    def transform(self, X):
        if self.mean_ is None:
            raise RuntimeError("Fit not called yet.")
        return (X - self.mean_) * self.scale_factor

# This won't work with GridSearchCV or Pipelines reliably
```

✅ GOOD: API-compliant custom estimator

```python
from sklearn.base import BaseEstimator, TransformerMixin
import numpy as np

class GoodCustomScaler(BaseEstimator, TransformerMixin):
    def __init__(self, scale_factor: float = 1.0):
        # Only store hyperparameters, no logic
        self.scale_factor = scale_factor

    def fit(self, X: np.ndarray, y=None):
        # Input validation (optional, but good practice)
        X = self._validate_data(X)
        if self.scale_factor <= 0:
            raise ValueError("Scale factor must be positive")
        self.mean_ = X.mean(axis=0) # Learned attribute with underscore
        return self

    def transform(self, X: np.ndarray) -> np.ndarray:
        X = self._validate_data(X) # Ensure consistency
        if not hasattr(self, "mean_"):
            raise RuntimeError("Estimator not fitted. Call fit() first.")
        return (X - self.mean_) * self.scale_factor

# This estimator integrates perfectly with scikit-learn utilities.
```

## 2. Common Patterns and Anti-patterns

### 2.1. Prevent Data Leakage: Split First, Fit on Train Only

Always split your data into training and test sets *before* any preprocessing. `fit` and `fit_transform` methods must *only* be called on the training data. `transform` can then be called on both training and test data.

❌ BAD: Data leakage by fitting on all data

```python
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import train_test_split
import numpy as np

X, y = np.random.rand(100, 5), np.random.rand(100)
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=42)

scaler = StandardScaler()
# Fitting on all data (X) leaks information from the test set
X_scaled = scaler.fit_transform(X)
X_train_scaled = X_scaled[:len(X_train)]
X_test_scaled = X_scaled[len(X_train):]
```

✅ GOOD: Correct data splitting and transformation

```python
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import train_test_split
import numpy as np

X, y = np.random.rand(100, 5), np.random.rand(100)
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=42)

scaler = StandardScaler()
# Fit only on training data
X_train_scaled = scaler.fit_transform(X_train)
# Transform both train and test using parameters learned from train
X_test_scaled = scaler.transform(X_test)
```

### 2.2. Reproducibility: Always Set `random_state`

For any estimator or utility that involves randomness (e.g., `train_test_split`, `RandomForestClassifier`, `KMeans`), explicitly set the `random_state` parameter for reproducible results.

❌ BAD: Non-reproducible results

```python
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
# Results will change on each run
X_train, X_test, y_train, y_test = train_test_split(X, y)
model = RandomForestClassifier()
```

✅ GOOD: Reproducible results

```python
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier

# Use a fixed integer for random_state
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=42)
model = RandomForestClassifier(random_state=42)
```

## 3. Performance Considerations

### 3.1. Leverage Cython for Performance-Critical Components

When extending scikit-learn with custom, performance-critical algorithms, use Cython and follow scikit-learn's internal conventions.

-   Disable bounds checking and wraparound for production code.
-   Use `sklearn.utils._typedefs` for explicit type declarations.
-   Use `sklearn.utils._openmp_helpers` for OpenMP routines.
-   Prefer memoryviews over `cnp.ndarray` when possible.

```python
# Example of Cython directives and imports (in a .pyx file)
# distutils: language=c
# distutils: extra_compile_args=-fopenmp
# distutils: extra_link_args=-fopenmp
# cython: boundscheck=False
# cython: wraparound=False
# cython: cdivision=True

from cython.parallel import prange
from sklearn.utils._typedefs cimport float64
from sklearn.utils._openmp_helpers cimport _get_num_threads

cdef void my_fast_function(float64[:] data, int n_threads) nogil:
    cdef int i
    with nogil:
        for i in prange(data.shape[0], num_threads=n_threads, schedule='static'):
            data[i] *= 2.0
```

## 4. Common Pitfalls and Gotchas

### 4.1. Avoid Overfitting on Validation Data During Hyperparameter Tuning

Use `GridSearchCV` or `RandomizedSearchCV` *with a pipeline* to ensure that cross-validation folds are correctly handled and preprocessing steps are refitted for each fold, preventing data leakage.

❌ BAD: Tuning hyperparameters outside a pipeline

```python
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import GridSearchCV, train_test_split
import numpy as np

X, y = np.random.rand(100, 5), np.random.randint(0, 2, 100)
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=42)

# Scaling outside of GridSearchCV means the scaler sees all training data
# before cross-validation, leading to optimistic scores.
scaler = StandardScaler().fit(X_train)
X_train_scaled = scaler.transform(X_train)

param_grid = {'C': [0.1, 1.0, 10.0]}
grid_search = GridSearchCV(LogisticRegression(random_state=42), param_grid, cv=3)
grid_search.fit(X_train_scaled, y_train)
# Scores here are optimistically biased
```

✅ GOOD: Tuning hyperparameters with a pipeline

```python
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import GridSearchCV, train_test_split
import numpy as np

X, y = np.random.rand(100, 5), np.random.randint(0, 2, 100)
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=42)

# Define the pipeline including preprocessing and the model
pipeline = Pipeline([
    ('scaler', StandardScaler()),
    ('logreg', LogisticRegression(random_state=42))
])

param_grid = {'logreg__C': [0.1, 1.0, 10.0]} # Note the 'estimator__param' syntax
grid_search = GridSearchCV(pipeline, param_grid, cv=3)
grid_search.fit(X_train, y_train) # Fit the entire pipeline on X_train
# Scores are realistic as preprocessing is done correctly within each fold
```

## 5. Type Hints

### 5.1. Use Type Hints Extensively

All function signatures, class attributes, and complex variable assignments must include type hints. This improves code readability, enables static analysis, and reduces bugs.

❌ BAD: Untyped function

```python
def calculate_metric(y_true, y_pred):
    # What are y_true and y_pred? Lists, arrays, Series?
    return (y_true == y_pred).mean()
```

✅ GOOD: Clearly typed function

```python
import numpy as np
from typing import Union

def calculate_metric(y_true: Union[np.ndarray, list], y_pred: Union[np.ndarray, list]) -> float:
    """Calculates accuracy for binary classification."""
    y_true_arr = np.asarray(y_true)
    y_pred_arr = np.asarray(y_pred)
    return (y_true_arr == y_pred_arr).mean()
```

## 6. Virtual Environments

### 6.1. Isolate Project Dependencies with Virtual Environments

Always use a dedicated virtual environment (`venv`, `conda`, `poetry`, etc.) for each project. This prevents dependency conflicts and ensures consistent environments.

❌ BAD: Global `pip install`

```bash
pip install scikit-learn pandas numpy # Pollutes global environment
```

✅ GOOD: Project-specific virtual environment

```bash
# Using venv
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt

# Using conda
conda create -n my_ml_env python=3.9 scikit-learn pandas numpy
conda activate my_ml_env
```

## 7. Packaging

### 7.1. Use `scikit-learn-contrib` Template for Shareable Estimators

If you develop a custom estimator intended for public use or sharing across projects, leverage the `scikit-learn-contrib template`. It provides a robust structure, testing, and CI/CD setup, ensuring your estimator is fully compliant and easily consumable.

❌ BAD: Ad-hoc custom estimator distribution

```python
# Just a .py file with a custom estimator, no proper packaging
# Difficult for others to install, test, or integrate
```

✅ GOOD: Structured project using `scikit-learn-contrib`

```bash
# Follow the template to create a new project:
# https://github.com/scikit-learn-contrib/project-template
# This provides setup.py, tests, documentation structure, etc.
```

## 8. Testing Approaches

### 8.1. Implement Comprehensive Unit and Integration Tests

For custom estimators, write unit tests to verify individual methods and use `sklearn.utils.estimator_checks.check_estimator` to ensure API compliance. For pipelines, write integration tests that cover the entire workflow.

❌ BAD: No tests or only manual verification

```python
# No tests for MyCustomEstimator, relying on manual runs
# to confirm it works.
```

✅ GOOD: Automated testing with `check_estimator`

```python
import pytest
from sklearn.utils.estimator_checks import check_estimator
from sklearn.base import BaseEstimator, TransformerMixin
import numpy as np

class MyCustomTransformer(BaseEstimator, TransformerMixin):
    def __init__(self, offset: float = 0.0):
        self.offset = offset
    def fit(self, X, y=None):
        self._validate_data(X)
        return self
    def transform(self, X):
        self._validate_data(X, reset=False)
        return X + self.offset

# Test API compliance
@pytest.mark.parametrize(
    "estimator", [MyCustomTransformer()]
)
def test_all_estimators(estimator):
    return check_estimator(estimator)

# Add specific unit tests for logic
def test_my_custom_transformer_offset():
    X = np.array([[1, 2], [3, 4]])
    transformer = MyCustomTransformer(offset=10)
    transformer.fit(X)
    transformed_X = transformer.transform(X)
    assert np.array_equal(transformed_X, np.array([[11, 12], [13, 14]]))
```

© sanjeed5 · CC0-1.0 · sanjeed5/awesome-cursor-rules-mdc

Similar IDE rules

All IDE rules for AI and ML →

From the same set240

Project sponsors

Companies that keep this analytics open
Gold sponsor

Impressions, clicks and code copies — open statistics. We keep the numbers public for the same reason NeBlask itself is public. Become a sponsor