Skip to content

Classification API

Classify pixels

obia.classification.classify_pixels(image, points, *, bands=None, label_column='class', method='rf', mask=None, invalid_samples='raise', test_size=0.2, compute_reports=False, scale_features='auto', split_random_state=42, stratify=True, split=None, batch_size=65536, return_probabilities=False, **kwargs)

Learn from labelled point pixels and classify the raster directly.

Parameters:

Name Type Description Default
image Image

Loaded raster. Source values are used without normalization.

required
points GeoDataFrame

Labelled Point GeoDataFrame; see sample_pixels for sampling rules.

required
bands Sequence[int] | None

Ordered zero-based band indices; None selects all loaded bands.

None
label_column str

Point attribute containing string or numeric class labels.

'class'
method str | BaseEstimator

Any CLASSIFICATION_METHODS name or compatible classifier/pipeline.

'rf'
mask ndarray | None

Boolean inclusion mask applied to both sampling and prediction.

None
invalid_samples Literal['raise', 'drop']

"raise" or "drop" for outside or invalid point pixels.

'raise'
test_size float | int | None

Random sample holdout fraction/count; None fits all samples.

0.2
compute_reports bool

Calculate holdout metrics; requires test samples.

False
scale_features bool | Literal['auto']

"auto", True, or False, as in object classification. Preprocessing is fitted only on training samples.

'auto'
split_random_state int | None

Random holdout seed, independent of model parameters.

42
stratify bool

Preserve class coverage in the default random holdout.

True
split ObjectSplit | None

Reusable split created from sample_pixels with the same settings. Overrides test_size, split_random_state, and stratify. Spatial, grouped, explicit, and cross-validation partitions are supported.

None
batch_size int

Maximum number of pixels processed per prediction batch.

65536
return_probabilities bool

Retain the full probability array when available.

False
**kwargs Any

Estimator constructor parameters or pipeline parameter overrides.

{}

Returns:

Name Type Description
ClassifiedRaster ClassifiedRaster

Class codes, original labels, fitted model, sampling diagnostics, optional holdout reports, and raster georeferencing. Invalid pixels have code 0. Prediction batches limit temporary feature memory; the source and output rasters remain in memory.

Source code in obia/classification/pixels.py
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
def classify_pixels(
    image: Image, points: gpd.GeoDataFrame, *, bands: Sequence[int] | None = None,
    label_column: str = "class", method: str | BaseEstimator = "rf",
    mask: np.ndarray | None = None, invalid_samples: Literal["raise", "drop"] = "raise",
    test_size: float | int | None = 0.2, compute_reports: bool = False,
    scale_features: bool | Literal["auto"] = "auto", split_random_state: int | None = 42,
    stratify: bool = True, split: ObjectSplit | None = None,
    batch_size: int = 65536, return_probabilities: bool = False, **kwargs: Any,
) -> ClassifiedRaster:
    """Learn from labelled point pixels and classify the raster directly.

    Args:
        image: Loaded raster. Source values are used without normalization.
        points: Labelled Point GeoDataFrame; see sample_pixels for sampling rules.
        bands: Ordered zero-based band indices; None selects all loaded bands.
        label_column: Point attribute containing string or numeric class labels.
        method: Any CLASSIFICATION_METHODS name or compatible classifier/pipeline.
        mask: Boolean inclusion mask applied to both sampling and prediction.
        invalid_samples: "raise" or "drop" for outside or invalid point pixels.
        test_size: Random sample holdout fraction/count; None fits all samples.
        compute_reports: Calculate holdout metrics; requires test samples.
        scale_features: "auto", True, or False, as in object classification.
            Preprocessing is fitted only on training samples.
        split_random_state: Random holdout seed, independent of model parameters.
        stratify: Preserve class coverage in the default random holdout.
        split: Reusable split created from sample_pixels with the same settings.
            Overrides test_size, split_random_state, and stratify. Spatial,
            grouped, explicit, and cross-validation partitions are supported.
        batch_size: Maximum number of pixels processed per prediction batch.
        return_probabilities: Retain the full probability array when available.
        **kwargs: Estimator constructor parameters or pipeline parameter overrides.

    Returns:
        ClassifiedRaster: Class codes, original labels, fitted model, sampling
            diagnostics, optional holdout reports, and raster georeferencing.
            Invalid pixels have code 0. Prediction batches limit temporary
            feature memory; the source and output rasters remain in memory.
    """
    _prediction_options(batch_size, return_probabilities)
    samples = sample_pixels(image, points, bands=bands, label_column=label_column,
                            mask=mask, invalid_samples=invalid_samples)
    selected = samples.attrs["obia_sampling"]["bands"]
    columns = [f"b{band}" for band in selected]
    y = _labels(samples)
    if split is None:
        split = split_objects(samples, test_size=test_size, random_state=split_random_state, stratify=stratify)
    elif not isinstance(split, ObjectSplit):
        raise TypeError("split must be an ObjectSplit created from sampled pixels.")
    split.validate_for(samples)
    if compute_reports and not len(split.test_indices):
        raise ValueError("compute_reports=True requires a holdout test subset.")
    if set(columns).intersection(split.feature_exclusions):
        raise ValueError("Pixel band columns cannot also be grouping columns.")
    model, params = _classifier_pipeline(method, scale_features, **kwargs)
    features = samples.loc[:, columns]
    model.fit(features.iloc[split.train_indices], y[split.train_indices])
    result = _predict_raster(image, model, selected, params, mask, batch_size, return_probabilities)
    result.samples, result.split = samples, split
    if compute_reports:
        observed = y[split.test_indices]
        predicted = model.predict(features.iloc[split.test_indices])
        result.confusion_matrix = confusion_matrix(observed, predicted, labels=result.classes)
        result.report = classification_report(observed, predicted, labels=result.classes, zero_division=0)
    return result

Sample labelled pixels

obia.classification.sample_pixels(image, points, *, bands=None, label_column='class', mask=None, invalid_samples='raise')

Read the pixel containing each labelled point, without interpolation.

Parameters:

Name Type Description Default
image Image

Loaded raster with a CRS and affine transform.

required
points GeoDataFrame

GeoDataFrame of finite, nonempty Point geometries with a CRS. Points are reprojected to the raster CRS when necessary.

required
bands Sequence[int] | None

Ordered, zero-based indices into the loaded image; None uses all.

None
label_column str

Column containing string or numeric class labels.

'class'
mask ndarray | None

Boolean (height, width) inclusion mask, in addition to source masks.

None
invalid_samples Literal['raise', 'drop']

Raise for points outside the raster or on excluded, NoData, or nonfinite pixels. "drop" explicitly discards these points.

'raise'

Returns:

Type Description
GeoDataFrame

gpd.GeoDataFrame: One row per sampled pixel, with bN band values, feature_class, pixel_row, and pixel_col. Geometry is the pixel centre in the raster CRS. Other point attributes and indices are retained; generated columns replace existing columns of those names. Repeated points in one pixel retain the first row if their labels agree; conflicting labels raise an error. Sampling counts are recorded in attrs['obia_sampling']. Inputs are unchanged.

Source code in obia/classification/pixels.py
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
def sample_pixels(
    image: Image, points: gpd.GeoDataFrame, *, bands: Sequence[int] | None = None,
    label_column: str = "class", mask: np.ndarray | None = None,
    invalid_samples: Literal["raise", "drop"] = "raise",
) -> gpd.GeoDataFrame:
    """Read the pixel containing each labelled point, without interpolation.

    Args:
        image: Loaded raster with a CRS and affine transform.
        points: GeoDataFrame of finite, nonempty Point geometries with a CRS.
            Points are reprojected to the raster CRS when necessary.
        bands: Ordered, zero-based indices into the loaded image; None uses all.
        label_column: Column containing string or numeric class labels.
        mask: Boolean (height, width) inclusion mask, in addition to source masks.
        invalid_samples: Raise for points outside the raster or on excluded,
            NoData, or nonfinite pixels. "drop" explicitly discards these points.

    Returns:
        gpd.GeoDataFrame: One row per sampled pixel, with ``bN`` band values,
            ``feature_class``, ``pixel_row``, and ``pixel_col``. Geometry is the
            pixel centre in the raster CRS. Other point attributes and indices
            are retained; generated columns replace existing columns of those
            names. Repeated points in one pixel retain the first row if their
            labels agree; conflicting labels raise an error. Sampling counts
            are recorded in ``attrs['obia_sampling']``. Inputs are unchanged.
    """
    if invalid_samples not in ("raise", "drop"):
        raise ValueError("invalid_samples must be 'raise' or 'drop'.")
    data, selected, valid = _raster_inputs(image, bands, mask)
    if not isinstance(points, gpd.GeoDataFrame) or not points.columns.is_unique:
        raise TypeError("points must be a GeoDataFrame with unique column names.")
    if points.crs is None:
        raise ValueError("Sample points must have a CRS.")
    if label_column not in points or not len(points):
        raise ValueError(f"points must contain labelled rows and a {label_column!r} column.")
    geometry = points.geometry
    if (geometry.isna().any() or geometry.is_empty.any()
            or not geometry.geom_type.eq("Point").all() or not geometry.is_valid.all()):
        raise ValueError("Sample geometries must be finite, nonempty Points.")
    reserved = {"feature_class", "pixel_row", "pixel_col", *(f"b{b}" for b in selected)}
    if geometry.name in reserved or geometry.name == label_column:
        raise ValueError("The geometry column must be distinct from label and sample feature columns.")
    sampled = points.to_crs(image.crs).copy()
    if not np.isfinite(sampled.geometry.bounds.to_numpy()).all():
        raise ValueError("Sample geometries must have finite coordinates in the raster CRS.")
    rows, cols = rowcol(image.transform, sampled.geometry.x, sampled.geometry.y, op=np.floor)
    rows, cols = np.asarray(rows), np.asarray(cols)
    inside = (rows >= 0) & (rows < data.shape[0]) & (cols >= 0) & (cols < data.shape[1])
    positions = np.flatnonzero(inside)
    inside[positions] &= valid[rows[positions].astype(int), cols[positions].astype(int)]
    n_invalid = int((~inside).sum())
    if n_invalid and invalid_samples == "raise":
        raise ValueError(
            f"{n_invalid} sample points fall outside the raster or on invalid pixels. "
            "Review them, or set invalid_samples='drop'."
        )
    sampled = sampled.iloc[np.flatnonzero(inside)].copy()
    if sampled.empty:
        raise ValueError("No valid labelled pixels remain.")
    rows, cols = rows[inside].astype(int), cols[inside].astype(int)
    sampled["feature_class"] = sampled[label_column].to_numpy(copy=True)
    _labels(sampled)
    sampled["pixel_row"], sampled["pixel_col"] = rows, cols
    for band in selected:
        sampled[f"b{band}"] = data[rows, cols, band]
    xs, ys = xy(image.transform, rows, cols)
    sampled = sampled.set_geometry(gpd.points_from_xy(xs, ys, crs=image.crs))
    counts = sampled.groupby(["pixel_row", "pixel_col"], sort=False).feature_class.nunique()
    if (counts > 1).any():
        raise ValueError("Conflicting class labels select the same raster pixel; resolve those labels first.")
    duplicates = sampled.duplicated(["pixel_row", "pixel_col"])
    sampled = sampled.loc[~duplicates].copy()
    sampled.attrs["obia_sampling"] = {
        "bands": selected, "label_column": label_column, "input_points": len(points),
        "invalid_points": n_invalid, "duplicate_points": int(duplicates.sum()),
        "sampled_pixels": len(sampled),
    }
    return sampled

Raster results

obia.classification.ClassifiedRaster dataclass

Pixel predictions, georeferencing, and the fitted classifier.

Attributes:

Name Type Description
classified ndarray

uint32 array (height, width). Codes 1..K follow classes; zero is NoData. class_mapping recovers original string/numeric labels.

crs Any

Raster coordinate reference system.

transform Affine

Raster affine transform.

model BaseEstimator

Fitted pipeline accepting columns named bN in band order.

bands tuple[int, ...]

Ordered zero-based indices used for learning and prediction.

classes ndarray

Original class labels, in code and probability-column order.

params dict

Estimator parameters.

prediction_margin ndarray

Top-two probability gap, or NaN for invalid pixels, one-class models, or estimators without probabilities.

probabilities ndarray | None

Optional float32 (height, width, classes) array; invalid pixels are NaN. None when not requested or unavailable.

samples GeoDataFrame | None

Sampled training/holdout table, or None on prediction-only results.

split ObjectSplit | None

Sample partition used for fitting, or None on prediction-only results.

confusion_matrix ndarray | None

Holdout matrix in class order, when requested.

report str | None

Holdout classification report, when requested.

Source code in obia/classification/pixels.py
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
@dataclass
class ClassifiedRaster:
    """Pixel predictions, georeferencing, and the fitted classifier.

    Attributes:
        classified: uint32 array (height, width). Codes 1..K follow ``classes``;
            zero is NoData. ``class_mapping`` recovers original string/numeric labels.
        crs: Raster coordinate reference system.
        transform: Raster affine transform.
        model: Fitted pipeline accepting columns named ``bN`` in band order.
        bands: Ordered zero-based indices used for learning and prediction.
        classes: Original class labels, in code and probability-column order.
        params: Estimator parameters.
        prediction_margin: Top-two probability gap, or NaN for invalid pixels,
            one-class models, or estimators without probabilities.
        probabilities: Optional float32 (height, width, classes) array; invalid
            pixels are NaN. None when not requested or unavailable.
        samples: Sampled training/holdout table, or None on prediction-only results.
        split: Sample partition used for fitting, or None on prediction-only results.
        confusion_matrix: Holdout matrix in class order, when requested.
        report: Holdout classification report, when requested.
    """

    classified: np.ndarray
    crs: Any
    transform: Affine
    model: BaseEstimator
    bands: tuple[int, ...]
    classes: np.ndarray
    params: dict
    prediction_margin: np.ndarray
    probabilities: np.ndarray | None = None
    samples: gpd.GeoDataFrame | None = None
    split: ObjectSplit | None = None
    confusion_matrix: np.ndarray | None = None
    report: str | None = None

    @property
    def class_mapping(self) -> dict:
        """Map positive raster codes to original class labels; 0 is NoData."""
        return dict(enumerate(self.classes.tolist(), start=1))

    @property
    def feature_columns(self) -> tuple[str, ...]:
        """Column order expected by the fitted model."""
        return tuple(f"b{band}" for band in self.bands)

    @property
    def valid_mask(self) -> np.ndarray:
        """Pixels with predictions."""
        return self.classified != 0

    def predict(
        self, image: Image, *, mask: np.ndarray | None = None,
        batch_size: int = 65536, return_probabilities: bool = False,
    ) -> ClassifiedRaster:
        """Predict another raster with the same band positions, without refitting.

        Args:
            image: Raster with matching band meanings, order, and value units.
                Its size, transform, and CRS may differ from the training raster.
            mask: Optional boolean (height, width) inclusion mask.
            batch_size: Maximum number of pixels processed per prediction batch.
            return_probabilities: Retain a full probability array when supported.

        Returns:
            ClassifiedRaster: Predictions on the new grid. Training samples,
                split, and holdout reports are not copied into this result.
        """
        return _predict_raster(image, self.model, self.bands, self.params,
                               mask, batch_size, return_probabilities)

    def write_geotiff(self, output_path: str) -> None:
        """Save uint32 class codes with CRS, transform, NoData=0, and label mapping.

        Args:
            output_path: Destination GeoTIFF. An existing file is replaced.

        The ``class_mapping`` dataset tag contains a JSON mapping of raster
        codes to original labels. The ``band_indices`` tag records model inputs.
        """
        with rasterio.open(
            output_path, "w", driver="GTiff", height=self.classified.shape[0],
            width=self.classified.shape[1], count=1, dtype="uint32", nodata=0,
            crs=self.crs, transform=self.transform, compress="deflate",
        ) as dst:
            dst.write(self.classified, 1)
            dst.set_band_description(1, "class_code")
            dst.update_tags(class_mapping=json.dumps(self.class_mapping),
                            band_indices=json.dumps(self.bands))

class_mapping property

Map positive raster codes to original class labels; 0 is NoData.

feature_columns property

Column order expected by the fitted model.

valid_mask property

Pixels with predictions.

predict(image, *, mask=None, batch_size=65536, return_probabilities=False)

Predict another raster with the same band positions, without refitting.

Parameters:

Name Type Description Default
image Image

Raster with matching band meanings, order, and value units. Its size, transform, and CRS may differ from the training raster.

required
mask ndarray | None

Optional boolean (height, width) inclusion mask.

None
batch_size int

Maximum number of pixels processed per prediction batch.

65536
return_probabilities bool

Retain a full probability array when supported.

False

Returns:

Name Type Description
ClassifiedRaster ClassifiedRaster

Predictions on the new grid. Training samples, split, and holdout reports are not copied into this result.

Source code in obia/classification/pixels.py
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
def predict(
    self, image: Image, *, mask: np.ndarray | None = None,
    batch_size: int = 65536, return_probabilities: bool = False,
) -> ClassifiedRaster:
    """Predict another raster with the same band positions, without refitting.

    Args:
        image: Raster with matching band meanings, order, and value units.
            Its size, transform, and CRS may differ from the training raster.
        mask: Optional boolean (height, width) inclusion mask.
        batch_size: Maximum number of pixels processed per prediction batch.
        return_probabilities: Retain a full probability array when supported.

    Returns:
        ClassifiedRaster: Predictions on the new grid. Training samples,
            split, and holdout reports are not copied into this result.
    """
    return _predict_raster(image, self.model, self.bands, self.params,
                           mask, batch_size, return_probabilities)

write_geotiff(output_path)

Save uint32 class codes with CRS, transform, NoData=0, and label mapping.

Parameters:

Name Type Description Default
output_path str

Destination GeoTIFF. An existing file is replaced.

required

The class_mapping dataset tag contains a JSON mapping of raster codes to original labels. The band_indices tag records model inputs.

Source code in obia/classification/pixels.py
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
def write_geotiff(self, output_path: str) -> None:
    """Save uint32 class codes with CRS, transform, NoData=0, and label mapping.

    Args:
        output_path: Destination GeoTIFF. An existing file is replaced.

    The ``class_mapping`` dataset tag contains a JSON mapping of raster
    codes to original labels. The ``band_indices`` tag records model inputs.
    """
    with rasterio.open(
        output_path, "w", driver="GTiff", height=self.classified.shape[0],
        width=self.classified.shape[1], count=1, dtype="uint32", nodata=0,
        crs=self.crs, transform=self.transform, compress="deflate",
    ) as dst:
        dst.write(self.classified, 1)
        dst.set_band_description(1, "class_code")
        dst.update_tags(class_mapping=json.dumps(self.class_mapping),
                        band_indices=json.dumps(self.bands))

See pixel classification for sampling rules, class codes, raster exports, and prediction on another image.

Classify Objects

obia.classification.classify

Train object classifiers and retain geospatial attributes in their predictions.

ClassifiedImage

Classification output and its fitted, reusable preprocessing/model pipeline.

Attributes:

Name Type Description
classified

Copy of the input segments with predictions and probability margins.

model

Fitted scikit-learn pipeline; accepts the selected feature columns.

feature_columns

Ordered feature names used for fitting and prediction.

classes

Class labels in probability-column and confusion-matrix order.

probabilities

Class probabilities indexed like the segments, or None.

margin_kind

"probability" when probabilities exist, otherwise None.

confusion_matrix

Held-out confusion matrix, or None.

report

Held-out classification report, or None.

params

Classifier parameters, excluding OBIA preprocessing options.

shap_values

SHAP values for training features, or None.

split

ObjectSplit with positional training/test/excluded assignments and class counts.

Source code in obia/classification/classify.py
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
class ClassifiedImage:
    """Classification output and its fitted, reusable preprocessing/model pipeline.

    Attributes:
        classified: Copy of the input segments with predictions and probability margins.
        model: Fitted scikit-learn pipeline; accepts the selected feature columns.
        feature_columns: Ordered feature names used for fitting and prediction.
        classes: Class labels in probability-column and confusion-matrix order.
        probabilities: Class probabilities indexed like the segments, or None.
        margin_kind: ``"probability"`` when probabilities exist, otherwise None.
        confusion_matrix: Held-out confusion matrix, or None.
        report: Held-out classification report, or None.
        params: Classifier parameters, excluding OBIA preprocessing options.
        shap_values: SHAP values for training features, or None.
        split: ObjectSplit with positional training/test/excluded assignments and class counts.
    """

    def __init__(self, classified, confusion_matrix, report, shap_values, transform, crs,
                 params, *, model=None, feature_columns=(), classes=None, probabilities=None, split=None):
        self.classified = classified
        self.confusion_matrix = confusion_matrix
        self.report = report
        self.shap_values = shap_values
        self.transform = transform
        self.crs = crs
        self.params = params
        self.model = model
        self.feature_columns = tuple(feature_columns)
        self.classes = classes
        self.probabilities = probabilities
        self.margin_kind = "probability" if probabilities is not None else None
        self.split = split

    def write_geotiff(self, output_path):
        """Write raster-valued classified data; polygon outputs must be rasterized first."""
        _write_geotiff(self.classified, output_path, self.crs, self.transform)

write_geotiff(output_path)

Write raster-valued classified data; polygon outputs must be rasterized first.

Source code in obia/classification/classify.py
60
61
62
def write_geotiff(self, output_path):
    """Write raster-valued classified data; polygon outputs must be rasterized first."""
    _write_geotiff(self.classified, output_path, self.crs, self.transform)

classify(segments, training_classes, acceptable_classes_gdf=None, method='rf', test_size=0.2, compute_reports=False, compute_shap=False, sample_shap=False, *, feature_columns=None, scale_features='auto', split_random_state=42, stratify=True, split=None, **kwargs)

Fit a classifier on labelled objects and predict classes for segment polygons.

Parameters:

Name Type Description Default
segments GeoDataFrame

Segment GeoDataFrame. The input is not modified.

required
training_classes DataFrame

Table with numeric features and a feature_class target. Labels may be strings or numeric class codes; missing labels are rejected.

required
acceptable_classes_gdf GeoDataFrame | None

Optional polygons with lists of allowed class labels. Requires probabilities; overlapping polygons intersect their allowed sets.

None
method str | BaseEstimator

Name in CLASSIFICATION_METHODS or a scikit-learn-compatible classifier/pipeline instance, which is cloned before fitting.

'rf'
test_size float | int | None

Random holdout fraction or count; None trains on all labelled objects. Used only when split is None.

0.2
compute_reports bool

Report held-out performance; requires a test split.

False
compute_shap bool

Calculate SHAP explanations (requires the explain extra).

False
sample_shap bool

Limit SHAP background and explained training rows to 500.

False
feature_columns Sequence[str] | None

Ordered feature names. Default selects numeric training columns, excluding IDs, labels, and previous prediction columns.

None
scale_features bool | Literal['auto']

"auto" scales linear, SVM, neighbor, neural-network, discriminant, and Gaussian-process estimators. True always scales; False disables scaling. Custom pipelines and composite estimators own preprocessing. The scaler is fitted only on training rows and reused everywhere.

'auto'
split_random_state int | None

Seed for the holdout split, independent of estimator parameters.

42
stratify bool

Preserve class proportions in the holdout split (default True).

True
split ObjectSplit | None

Optional ObjectSplit from split_objects/cross_validation_splits. Reuses exactly that partition, including buffer exclusions, instead of test_size, split_random_state, and stratify. Named group columns are excluded from predictors. Reports require a nonempty test subset.

None
**kwargs Any

Estimator parameters, including nested parameters for supplied pipelines.

{}

Returns:

Type Description
ClassifiedImage

Object table, fitted model, feature order, probabilities, optional holdout

ClassifiedImage

reports, and optional SHAP values. Probability margins are NaN for

ClassifiedImage

estimators without probabilities or fewer than two allowed classes.

Source code in obia/classification/classify.py
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
def classify(segments: gpd.GeoDataFrame, training_classes: pd.DataFrame,
             acceptable_classes_gdf: gpd.GeoDataFrame | None = None,
             method: str | BaseEstimator = "rf", test_size: float | int | None = 0.2,
             compute_reports: bool = False, compute_shap: bool = False,
             sample_shap: bool = False, *, feature_columns: Sequence[str] | None = None,
             scale_features: bool | Literal["auto"] = "auto", split_random_state: int | None = 42,
             stratify: bool = True, split: ObjectSplit | None = None, **kwargs: Any) -> ClassifiedImage:
    """Fit a classifier on labelled objects and predict classes for segment polygons.

    Args:
        segments: Segment GeoDataFrame. The input is not modified.
        training_classes: Table with numeric features and a ``feature_class`` target.
            Labels may be strings or numeric class codes; missing labels are rejected.
        acceptable_classes_gdf: Optional polygons with lists of allowed class labels.
            Requires probabilities; overlapping polygons intersect their allowed sets.
        method: Name in ``CLASSIFICATION_METHODS`` or a scikit-learn-compatible
            classifier/pipeline instance, which is cloned before fitting.
        test_size: Random holdout fraction or count; None trains on all labelled objects.
            Used only when split is None.
        compute_reports: Report held-out performance; requires a test split.
        compute_shap: Calculate SHAP explanations (requires the ``explain`` extra).
        sample_shap: Limit SHAP background and explained training rows to 500.
        feature_columns: Ordered feature names. Default selects numeric training columns,
            excluding IDs, labels, and previous prediction columns.
        scale_features: ``"auto"`` scales linear, SVM, neighbor, neural-network,
            discriminant, and Gaussian-process estimators. True always scales; False
            disables scaling. Custom pipelines and composite estimators own preprocessing.
            The scaler is fitted only on training rows and reused everywhere.
        split_random_state: Seed for the holdout split, independent of estimator parameters.
        stratify: Preserve class proportions in the holdout split (default True).
        split: Optional ObjectSplit from split_objects/cross_validation_splits.
            Reuses exactly that partition, including buffer exclusions, instead of
            test_size, split_random_state, and stratify. Named group columns are
            excluded from predictors. Reports require a nonempty test subset.
        **kwargs: Estimator parameters, including nested parameters for supplied pipelines.

    Returns:
        Object table, fitted model, feature order, probabilities, optional holdout
        reports, and optional SHAP values. Probability margins are NaN for
        estimators without probabilities or fewer than two allowed classes.
    """
    if not isinstance(segments, gpd.GeoDataFrame):
        raise TypeError("segments must be a GeoDataFrame.")
    if not training_classes.columns.is_unique or not segments.columns.is_unique:
        raise ValueError("Training and segment column names must be unique.")
    y = _labels(training_classes)
    if split is None:
        split = split_objects(training_classes, test_size=test_size,
                              random_state=split_random_state, stratify=stratify)
    elif not isinstance(split, ObjectSplit):
        raise TypeError("split must be an ObjectSplit created by split_objects or cross_validation_splits.")
    split.validate_for(training_classes)
    if compute_reports and not len(split.test_indices):
        raise ValueError("compute_reports=True requires a holdout test subset.")
    columns, x, x_pred = _features(training_classes, segments, feature_columns, split.feature_exclusions)
    restrictions = _constraints(segments, acceptable_classes_gdf)
    model, params = _classifier_pipeline(method, scale_features, **kwargs)
    x_train, y_train = x.iloc[split.train_indices], y[split.train_indices]
    model.fit(x_train, y_train)
    predictions, margins, probabilities = _predict(model, x_pred, segments, restrictions)
    classes = np.asarray(model.classes_)
    cm = report = None
    if compute_reports:
        x_test, y_test = x.iloc[split.test_indices], y[split.test_indices]
        predicted_test = model.predict(x_test)
        cm = confusion_matrix(y_test, predicted_test, labels=classes)
        report = classification_report(y_test, predicted_test, labels=classes, zero_division=0)
    shap_values = _explain(model, x_train, sample_shap) if compute_shap else None
    classified = segments.copy()
    label_dtype = pd.Series(classes).convert_dtypes().dtype
    classified["predicted_class"] = pd.array(predictions.tolist(), dtype=label_dtype)
    classified["prediction_margin"] = margins
    return ClassifiedImage(
        classified, cm, report, shap_values, None, segments.crs, params,
        model=model, feature_columns=columns, classes=classes, probabilities=probabilities, split=split,
    )

Results

obia.classification.ClassifiedImage

Classification output and its fitted, reusable preprocessing/model pipeline.

Attributes:

Name Type Description
classified

Copy of the input segments with predictions and probability margins.

model

Fitted scikit-learn pipeline; accepts the selected feature columns.

feature_columns

Ordered feature names used for fitting and prediction.

classes

Class labels in probability-column and confusion-matrix order.

probabilities

Class probabilities indexed like the segments, or None.

margin_kind

"probability" when probabilities exist, otherwise None.

confusion_matrix

Held-out confusion matrix, or None.

report

Held-out classification report, or None.

params

Classifier parameters, excluding OBIA preprocessing options.

shap_values

SHAP values for training features, or None.

split

ObjectSplit with positional training/test/excluded assignments and class counts.

Source code in obia/classification/classify.py
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
class ClassifiedImage:
    """Classification output and its fitted, reusable preprocessing/model pipeline.

    Attributes:
        classified: Copy of the input segments with predictions and probability margins.
        model: Fitted scikit-learn pipeline; accepts the selected feature columns.
        feature_columns: Ordered feature names used for fitting and prediction.
        classes: Class labels in probability-column and confusion-matrix order.
        probabilities: Class probabilities indexed like the segments, or None.
        margin_kind: ``"probability"`` when probabilities exist, otherwise None.
        confusion_matrix: Held-out confusion matrix, or None.
        report: Held-out classification report, or None.
        params: Classifier parameters, excluding OBIA preprocessing options.
        shap_values: SHAP values for training features, or None.
        split: ObjectSplit with positional training/test/excluded assignments and class counts.
    """

    def __init__(self, classified, confusion_matrix, report, shap_values, transform, crs,
                 params, *, model=None, feature_columns=(), classes=None, probabilities=None, split=None):
        self.classified = classified
        self.confusion_matrix = confusion_matrix
        self.report = report
        self.shap_values = shap_values
        self.transform = transform
        self.crs = crs
        self.params = params
        self.model = model
        self.feature_columns = tuple(feature_columns)
        self.classes = classes
        self.probabilities = probabilities
        self.margin_kind = "probability" if probabilities is not None else None
        self.split = split

    def write_geotiff(self, output_path):
        """Write raster-valued classified data; polygon outputs must be rasterized first."""
        _write_geotiff(self.classified, output_path, self.crs, self.transform)

write_geotiff(output_path)

Write raster-valued classified data; polygon outputs must be rasterized first.

Source code in obia/classification/classify.py
60
61
62
def write_geotiff(self, output_path):
    """Write raster-valued classified data; polygon outputs must be rasterized first."""
    _write_geotiff(self.classified, output_path, self.crs, self.transform)

Holdout Splits

obia.classification.split_objects(data, *, method='random', test_size=0.2, random_state=42, stratify=True, groups=None, block_size=None, block_origin=(0.0, 0.0), buffer_distance=0.0, train_indices=None, test_indices=None, n_candidates=128)

Create a random, grouped, spatial, or explicitly assigned object holdout.

Parameters:

Name Type Description Default
data DataFrame

Labelled DataFrame/GeoDataFrame with feature_class. Spatial methods/buffers require valid geometry and a projected CRS.

required
method Literal['random', 'grouped', 'spatial', 'explicit']

random, grouped, spatial, or explicit.

'random'
test_size float | int | None

Fraction or integer count of rows for random splits, or of groups/occupied blocks for grouped/spatial splits. None fits all rows. Ignored for explicit partitions, whose indices determine their size.

0.2
random_state int | None

Seed for reproducible holdout selection.

42
stratify bool

Random splits use class stratification. Grouped/spatial splits search candidate group partitions for approximate class balance with all classes in both subsets. Explicit splits retain supplied subsets and check test-class coverage when True. Training always needs all classes.

True
groups str | Sequence[Hashable] | Series | ndarray | None

Group column name or row-ordered labels; required for grouped splits and optional for enforcing group separation in explicit splits. A named group column is excluded from classifier features.

None
block_size float | None

Positive square-grid width in projected CRS units, required for spatial splits. Each object's representative point selects its block, and the entire object stays in that block.

None
block_origin tuple[float, float]

Fixed grid origin (x, y), default (0, 0).

(0.0, 0.0)
buffer_distance float

Exclude training geometries at distance <= this value from any test geometry. Zero disables buffering. If groups/blocks are used, exclude the entire affected training group/block.

0.0
train_indices ArrayLike | None

Explicit zero-based row positions or a boolean mask.

None
test_indices ArrayLike | None

Explicit zero-based row positions or a boolean mask. Rows in neither subset are marked excluded.

None
n_candidates int

Number of random group partitions considered for stratified grouped/spatial holdouts. The search is approximate and can fail even if a feasible partition exists; increase this or supply explicit indices.

128

Returns:

Name Type Description
ObjectSplit ObjectSplit

Reusable indices, map-ready assignments, and class counts. Positive buffers can reduce training size and alter class balance.

Source code in obia/classification/splitting.py
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
def split_objects(
    data: pd.DataFrame, *, method: Literal["random", "grouped", "spatial", "explicit"] = "random",
    test_size: float | int | None = 0.2, random_state: int | None = 42, stratify: bool = True,
    groups: str | Sequence[Hashable] | pd.Series | np.ndarray | None = None,
    block_size: float | None = None, block_origin: tuple[float, float] = (0.0, 0.0),
    buffer_distance: float = 0.0, train_indices: ArrayLike | None = None,
    test_indices: ArrayLike | None = None, n_candidates: int = 128,
) -> ObjectSplit:
    """Create a random, grouped, spatial, or explicitly assigned object holdout.

    Args:
        data: Labelled DataFrame/GeoDataFrame with ``feature_class``. Spatial
            methods/buffers require valid geometry and a projected CRS.
        method: ``random``, ``grouped``, ``spatial``, or ``explicit``.
        test_size: Fraction or integer count of rows for random splits, or of
            groups/occupied blocks for grouped/spatial splits. None fits all rows.
            Ignored for explicit partitions, whose indices determine their size.
        random_state: Seed for reproducible holdout selection.
        stratify: Random splits use class stratification. Grouped/spatial splits
            search candidate group partitions for approximate class balance with
            all classes in both subsets. Explicit splits retain supplied subsets
            and check test-class coverage when True. Training always needs all classes.
        groups: Group column name or row-ordered labels; required for grouped
            splits and optional for enforcing group separation in explicit splits.
            A named group column is excluded from classifier features.
        block_size: Positive square-grid width in projected CRS units, required
            for spatial splits. Each object's representative point selects its
            block, and the entire object stays in that block.
        block_origin: Fixed grid origin (x, y), default (0, 0).
        buffer_distance: Exclude training geometries at distance <= this value
            from any test geometry. Zero disables buffering. If groups/blocks
            are used, exclude the entire affected training group/block.
        train_indices: Explicit zero-based row positions or a boolean mask.
        test_indices: Explicit zero-based row positions or a boolean mask.
            Rows in neither subset are marked excluded.
        n_candidates: Number of random group partitions considered for stratified
            grouped/spatial holdouts. The search is approximate and can fail even
            if a feasible partition exists; increase this or supply explicit indices.

    Returns:
        ObjectSplit: Reusable indices, map-ready assignments, and class counts.
            Positive buffers can reduce training size and alter class balance.
    """
    if method not in {"random", "grouped", "spatial", "explicit"}:
        raise ValueError("method must be 'random', 'grouped', 'spatial', or 'explicit'.")
    if not isinstance(stratify, (bool, np.bool_)):
        raise ValueError("stratify must be True or False.")
    y = _labels(data)
    buffer_distance = _nonnegative(buffer_distance, "buffer_distance")
    if buffer_distance:
        _projected(data)
    codes, values, extras, exclusions = _groups(data, method, groups, block_size, block_origin)
    metadata = dict(method=method, test_size=test_size, random_state=random_state,
                    stratify=bool(stratify), buffer_distance=buffer_distance)
    if method == "spatial":
        metadata.update(block_size=float(block_size), block_origin=tuple(map(float, block_origin)))
    positions = np.arange(len(data))
    if method == "explicit":
        train = _positions(train_indices, len(data), "train_indices")
        test = _positions(test_indices, len(data), "test_indices")
        metadata["test_size"] = None
    else:
        if train_indices is not None or test_indices is not None:
            raise ValueError("train_indices and test_indices require method='explicit'.")
        if test_size is None:
            train, test = positions, np.array([], dtype=np.intp)
        elif isinstance(test_size, (bool, np.bool_)):
            raise ValueError("test_size must be a fraction, an integer count, or None.")
        elif codes is None:
            try:
                train, test = train_test_split(positions, test_size=test_size, random_state=random_state,
                                               stratify=y if stratify else None)
            except ValueError as exc:
                raise ValueError(f"Cannot create the requested holdout split: {exc} "
                                 "Add labelled examples, adjust test_size, or use test_size=None.") from exc
        else:
            if isinstance(n_candidates, (bool, np.bool_)) or not isinstance(n_candidates, Integral) or n_candidates < 1:
                raise ValueError("n_candidates must be a positive integer.")
            splitter = GroupShuffleSplit(n_splits=n_candidates if stratify else 1,
                                        test_size=test_size, random_state=random_state)
            metadata["n_candidates"] = n_candidates if stratify else 1
            if stratify:
                _, label_codes = np.unique(y, return_inverse=True)
                proportions = np.bincount(label_codes) / len(y)

                def imbalance(pair):
                    return sum(np.abs(np.bincount(label_codes[part], minlength=len(proportions))
                                      / len(part) - proportions).sum() for part in pair)

            # Keep only the best viable partition, rather than materializing
            # n_candidates copies of the full object's row indices in memory.
            best, best_score = None, np.inf
            for train, test in splitter.split(positions, y, codes):
                score = imbalance((train, test)) if stratify else 0.0
                if score >= best_score:
                    continue
                try:
                    candidate = _make_split(data, y, train, test, codes, values, extras, exclusions,
                                            metadata, require_test_classes=stratify)
                except ValueError:
                    if not stratify:
                        raise
                else:
                    best, best_score = candidate, score
                    if score == 0:
                        break
            if best is not None:
                return best
            raise ValueError(
                "No class-complete grouped split was found. Classes need multiple independent groups. "
                "Adjust test_size, block_size, buffer_distance or n_candidates; collect more groups, "
                "or supply an explicit partition. stratify=False permits absent test classes."
            )
    return _make_split(data, y, train, test, codes, values, extras, exclusions,
                       metadata, require_test_classes=stratify)

Cross-Validation Folds

obia.classification.cross_validation_splits(data, *, method='random', n_splits=5, random_state=42, stratify=True, groups=None, block_size=None, block_origin=(0.0, 0.0), buffer_distance=0.0)

Create reusable random, grouped, or spatial cross-validation folds.

Each object is tested once. Buffers remove training objects separately for each fold; test objects are never removed. Arguments have the same meaning as in split_objects. Grouped/spatial stratification uses scikit-learn's StratifiedGroupKFold and is approximate. Non-stratified group folds use GroupKFold's deterministic allocation (random_state has no effect there).

All folds require every class in training; stratify=True also requires every class in each test fold. Insufficient class/group coverage raises an error, rather than silently moving objects between groups. Reduce n_splits or change the partition when necessary. No models are fitted by this function.

Returns:

Type Description
list[ObjectSplit]

list[ObjectSplit]: Pass each split to classify(..., split=fold) or use [fold.as_sklearn() for fold in folds] as scikit-learn's cv argument. Generate folds only from development data after reserving final tests.

Source code in obia/classification/splitting.py
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
def cross_validation_splits(
    data: pd.DataFrame, *, method: Literal["random", "grouped", "spatial"] = "random",
    n_splits: int = 5, random_state: int | None = 42, stratify: bool = True,
    groups: str | Sequence[Hashable] | pd.Series | np.ndarray | None = None,
    block_size: float | None = None, block_origin: tuple[float, float] = (0.0, 0.0),
    buffer_distance: float = 0.0,
) -> list[ObjectSplit]:
    """Create reusable random, grouped, or spatial cross-validation folds.

    Each object is tested once. Buffers remove training objects separately for
    each fold; test objects are never removed. Arguments have the same meaning
    as in ``split_objects``. Grouped/spatial stratification uses scikit-learn's
    StratifiedGroupKFold and is approximate. Non-stratified group folds use
    GroupKFold's deterministic allocation (random_state has no effect there).

    All folds require every class in training; stratify=True also requires every
    class in each test fold. Insufficient class/group coverage raises an error,
    rather than silently moving objects between groups. Reduce n_splits or change
    the partition when necessary. No models are fitted by this function.

    Returns:
        list[ObjectSplit]: Pass each split to ``classify(..., split=fold)`` or use
            ``[fold.as_sklearn() for fold in folds]`` as scikit-learn's cv argument.
            Generate folds only from development data after reserving final tests.
    """
    if method not in {"random", "grouped", "spatial"}:
        raise ValueError("Cross-validation method must be 'random', 'grouped', or 'spatial'.")
    if isinstance(n_splits, (bool, np.bool_)) or not isinstance(n_splits, Integral) or n_splits < 2:
        raise ValueError("n_splits must be an integer of at least 2.")
    if not isinstance(stratify, (bool, np.bool_)):
        raise ValueError("stratify must be True or False.")
    y = _labels(data)
    buffer_distance = _nonnegative(buffer_distance, "buffer_distance")
    if buffer_distance:
        _projected(data)
    codes, values, extras, exclusions = _groups(data, method, groups, block_size, block_origin)
    if codes is None:
        cls = StratifiedKFold if stratify else KFold
        splitter = cls(n_splits=n_splits, shuffle=True, random_state=random_state)
    elif stratify:
        splitter = StratifiedGroupKFold(n_splits=n_splits, shuffle=True, random_state=random_state)
    else:
        splitter = GroupKFold(n_splits=n_splits)
    metadata = dict(method=method, n_splits=n_splits, random_state=random_state,
                    stratify=bool(stratify), buffer_distance=buffer_distance)
    if method == "spatial":
        metadata.update(block_size=float(block_size), block_origin=tuple(map(float, block_origin)))
    folds = []
    for fold, (train, test) in enumerate(splitter.split(np.arange(len(data)), y, codes)):
        try:
            folds.append(_make_split(data, y, train, test, codes, values, extras, exclusions,
                                     {**metadata, "fold": fold}, require_test_classes=stratify))
        except ValueError as exc:
            raise ValueError(f"Invalid cross-validation fold {fold}: {exc}") from exc
    return folds

Split Assignments and Diagnostics

obia.classification.ObjectSplit dataclass

A reusable partition of a labelled object table, created by split helpers.

Attributes:

Name Type Description
train_indices ndarray

Zero-based row positions for fitting, suitable for iloc.

test_indices ndarray

Zero-based row positions for evaluation.

excluded_indices ndarray

Positions omitted from both subsets, including buffers.

assignments DataFrame

Copy of an inspectable table with row_position, subset (train/test/excluded), labels, IDs when present, and group/block columns. Geometry and CRS are retained when the source is a GeoDataFrame.

class_counts DataFrame

Counts of each class in train, test, and excluded subsets.

metadata dict

Method, settings, and achieved sample sizes. Distances and grid sizes use projected CRS units, not necessarily metres.

feature_exclusions tuple

Grouping column names, excluded from model predictors.

Indices are positional even when the source index contains duplicates. A split can be reused after changing feature values/columns, but its index, labels, IDs, geometry, CRS, and named group columns must remain unchanged.

Source code in obia/classification/splitting.py
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
@dataclass(frozen=True)
class ObjectSplit:
    """A reusable partition of a labelled object table, created by split helpers.

    Attributes:
        train_indices: Zero-based row positions for fitting, suitable for ``iloc``.
        test_indices: Zero-based row positions for evaluation.
        excluded_indices: Positions omitted from both subsets, including buffers.
        assignments: Copy of an inspectable table with ``row_position``, ``subset``
            (train/test/excluded), labels, IDs when present, and group/block columns.
            Geometry and CRS are retained when the source is a GeoDataFrame.
        class_counts: Counts of each class in train, test, and excluded subsets.
        metadata: Method, settings, and achieved sample sizes. Distances and grid
            sizes use projected CRS units, not necessarily metres.
        feature_exclusions: Grouping column names, excluded from model predictors.

    Indices are positional even when the source index contains duplicates. A
    split can be reused after changing feature values/columns, but its index,
    labels, IDs, geometry, CRS, and named group columns must remain unchanged.
    """

    _train: tuple[int, ...] = field(repr=False)
    _test: tuple[int, ...] = field(repr=False)
    _excluded: tuple[int, ...] = field(repr=False)
    _assignments: pd.DataFrame = field(repr=False, compare=False)
    _source: pd.DataFrame = field(repr=False, compare=False)
    _metadata: dict = field(repr=False, compare=False)
    feature_exclusions: tuple = ()

    @property
    def train_indices(self) -> np.ndarray:
        return np.asarray(self._train, dtype=np.intp)

    @property
    def test_indices(self) -> np.ndarray:
        return np.asarray(self._test, dtype=np.intp)

    @property
    def excluded_indices(self) -> np.ndarray:
        return np.asarray(self._excluded, dtype=np.intp)

    @property
    def assignments(self) -> pd.DataFrame:
        return self._assignments.copy(deep=True)

    @property
    def class_counts(self) -> pd.DataFrame:
        counts = pd.crosstab(self._assignments.feature_class, self._assignments.subset)
        return counts.reindex(columns=["train", "test", "excluded"], fill_value=0)

    @property
    def metadata(self) -> dict:
        return self._metadata.copy()

    def as_sklearn(self) -> tuple[np.ndarray, np.ndarray]:
        """Return ``(train_indices, test_indices)`` for scikit-learn's ``cv`` iterable."""
        return self.train_indices, self.test_indices

    def validate_for(self, data: pd.DataFrame) -> None:
        """Reject reuse on reordered or different objects; feature changes are allowed."""
        current = _identity(data, self.feature_exclusions)
        if not current.equals(self._source) or current.attrs != self._source.attrs:
            raise ValueError(
                "The split does not match these labelled objects or their row order. "
                "Recreate the split after changing the index, labels, IDs, groups, geometry, or CRS."
            )

as_sklearn()

Return (train_indices, test_indices) for scikit-learn's cv iterable.

Source code in obia/classification/splitting.py
110
111
112
def as_sklearn(self) -> tuple[np.ndarray, np.ndarray]:
    """Return ``(train_indices, test_indices)`` for scikit-learn's ``cv`` iterable."""
    return self.train_indices, self.test_indices

validate_for(data)

Reject reuse on reordered or different objects; feature changes are allowed.

Source code in obia/classification/splitting.py
114
115
116
117
118
119
120
121
def validate_for(self, data: pd.DataFrame) -> None:
    """Reject reuse on reordered or different objects; feature changes are allowed."""
    current = _identity(data, self.feature_exclusions)
    if not current.equals(self._source) or current.attrs != self._source.attrs:
        raise ValueError(
            "The split does not match these labelled objects or their row order. "
            "Recreate the split after changing the index, labels, IDs, groups, geometry, or CRS."
        )

See the validation guide for choosing groups, block sizes, and buffers, inspecting assignments, and keeping a final test set separate from model selection.

Create a Classifier

obia.classification.create_classifier(method='rf', **kwargs)

Create an unfitted classifier, or clone a supplied estimator or pipeline.

method accepts a name in CLASSIFICATION_METHODS, a scikit-learn class name (for example "SVC"), or a classifier instance. Keyword arguments are constructor parameters for named methods and set_params overrides for instances. XGBoost ships with OBIA and is imported only when requested. Composite classifiers such as voting and stacking require their usual estimators argument.

Source code in obia/classification/estimators.py
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
def create_classifier(method="rf", **kwargs):
    """Create an unfitted classifier, or clone a supplied estimator or pipeline.

    ``method`` accepts a name in ``CLASSIFICATION_METHODS``, a scikit-learn
    class name (for example ``"SVC"``), or a classifier instance. Keyword
    arguments are constructor parameters for named methods and ``set_params``
    overrides for instances. XGBoost ships with OBIA and is imported only when
    requested. Composite classifiers such as
    voting and stacking require their usual ``estimators`` argument.
    """
    if isinstance(method, str):
        name = method.lower().replace("-", "_")
        name = _ALIASES.get(name, name)
        if name in _XGBOOST_METHODS:
            import xgboost
            estimator = getattr(xgboost, _XGBOOST_METHODS[name])(**kwargs)
        elif name in _CLASSIFIERS:
            estimator = _CLASSIFIERS[name](**kwargs)
        else:
            raise ValueError(
                f"Unknown classification method {method!r}. Choose from "
                f"{', '.join(CLASSIFICATION_METHODS)}, or pass a classifier instance."
            )
    else:
        if (isinstance(method, type)
                or not all(callable(getattr(method, attr, None)) for attr in ("fit", "predict", "get_params"))
                or not is_classifier(method)):
            raise TypeError("method must be a classifier name or a scikit-learn classifier instance.")
        estimator = clone(method)
        # FrozenEstimator intentionally returns itself from clone. Parameter
        # overrides must not mutate it, including frozen steps inside pipelines.
        if estimator is method or kwargs:
            estimator = deepcopy(estimator)
        estimator.set_params(**kwargs)
        return estimator
    # Also clone estimator-valued parameters of named composite models.
    return clone(estimator)

Supported Method Names

obia.classification.estimators.CLASSIFICATION_METHODS = tuple(_CLASSIFIERS) + tuple(_XGBOOST_METHODS) module-attribute

Method names accepted by object and pixel classification and create_classifier.

See the classification guide for the complete estimator table, scaling rules, XGBoost options, and pipeline examples.