Skip to content

Commit a62d492

Browse files
authored
PRF: Faster encoders thanks to F-order layout (scikit-learn#34392)
1 parent 9dc4f7b commit a62d492

4 files changed

Lines changed: 67 additions & 19 deletions

File tree

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
- :class:`preprocessing.OneHotEncoder`, :class:`preprocessing.OrdinalEncoder`,
2+
and :class:`preprocessing.TargetEncoder` are now faster by using a
3+
Fortran-contiguous memory layout in their encoding paths.
4+
By :user:`Arthur Lacote <cakedev0>`.

sklearn/preprocessing/_encoders.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -204,8 +204,8 @@ def _transform(
204204
)
205205
validate_data(self, X=X, reset=False, skip_check_array=True)
206206

207-
X_int = np.zeros((n_samples, n_features), dtype=int)
208-
X_mask = np.ones((n_samples, n_features), dtype=bool)
207+
X_int = np.zeros((n_samples, n_features), dtype=int, order="F")
208+
X_mask = np.ones((n_samples, n_features), dtype=bool, order="F")
209209

210210
columns_with_unknown = []
211211
for i in range(n_features):
@@ -1072,9 +1072,9 @@ def transform(self, X):
10721072
X_int[X_int > to_drop] -= 1
10731073
X_mask &= keep_cells
10741074

1075-
mask = X_mask.ravel()
10761075
feature_indices = np.cumsum([0] + self._n_features_outs)
1077-
indices = (X_int + feature_indices[:-1]).ravel()[mask]
1076+
X_int += feature_indices[:-1]
1077+
indices = X_int[X_mask].ravel()
10781078

10791079
indptr = np.empty(n_samples + 1, dtype=int)
10801080
indptr[0] = 0

sklearn/preprocessing/_target_encoder.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -390,27 +390,31 @@ def fit_transform(self, X, y, **params):
390390
X_out = np.empty(
391391
(X_ordinal.shape[0], X_ordinal.shape[1] * len(self.classes_)),
392392
dtype=np.float64,
393+
order="F",
393394
)
394395
else:
395396
X_out = np.empty_like(X_ordinal, dtype=np.float64)
396397

397398
for train_idx, test_idx in cv.split(X, y, **routed_params.splitter.split):
398-
X_train, y_train = X_ordinal[train_idx, :], y_encoded[train_idx]
399+
X_indices = np.ascontiguousarray(train_idx, dtype=np.intp)
400+
y_train = y_encoded[train_idx]
399401
y_train_mean = np.mean(y_train, axis=0)
400402

401403
if self.target_type_ == "multiclass":
402404
encodings = self._fit_encoding_multiclass(
403-
X_train,
405+
X_ordinal,
404406
y_train,
405407
n_categories,
406408
y_train_mean,
409+
X_indices=X_indices,
407410
)
408411
else:
409412
encodings = self._fit_encoding_binary_or_continuous(
410-
X_train,
413+
X_ordinal,
411414
y_train,
412415
n_categories,
413416
y_train_mean,
417+
X_indices=X_indices,
414418
)
415419
self._transform_X_ordinal(
416420
X_out,
@@ -453,6 +457,7 @@ def transform(self, X):
453457
X_out = np.empty(
454458
(X_ordinal.shape[0], X_ordinal.shape[1] * len(self.classes_)),
455459
dtype=np.float64,
460+
order="F",
456461
)
457462
else:
458463
X_out = np.empty_like(X_ordinal, dtype=np.float64)
@@ -529,7 +534,7 @@ def _fit_encodings_all(self, X, y):
529534
return X_ordinal, X_known_mask, y, n_categories
530535

531536
def _fit_encoding_binary_or_continuous(
532-
self, X_ordinal, y, n_categories, target_mean
537+
self, X_ordinal, y, n_categories, target_mean, X_indices=None
533538
):
534539
"""Learn target encodings."""
535540
if self.smooth == "auto":
@@ -540,6 +545,7 @@ def _fit_encoding_binary_or_continuous(
540545
n_categories,
541546
target_mean,
542547
y_variance,
548+
X_indices,
543549
)
544550
else:
545551
encodings = _fit_encoding_fast(
@@ -548,10 +554,13 @@ def _fit_encoding_binary_or_continuous(
548554
n_categories,
549555
self.smooth,
550556
target_mean,
557+
X_indices,
551558
)
552559
return encodings
553560

554-
def _fit_encoding_multiclass(self, X_ordinal, y, n_categories, target_mean):
561+
def _fit_encoding_multiclass(
562+
self, X_ordinal, y, n_categories, target_mean, X_indices=None
563+
):
555564
"""Learn multiclass encodings.
556565
557566
Learn encodings for each class (c) then reorder encodings such that
@@ -572,6 +581,7 @@ def _fit_encoding_multiclass(self, X_ordinal, y, n_categories, target_mean):
572581
y_class,
573582
n_categories,
574583
target_mean[i],
584+
X_indices=X_indices,
575585
)
576586
encodings.extend(encoding)
577587

sklearn/preprocessing/_target_encoder_fast.pyx

Lines changed: 44 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
from libc.math cimport isnan
22
from libcpp.vector cimport vector
33

4-
from sklearn.utils._typedefs cimport float32_t, float64_t, int32_t, int64_t
4+
from sklearn.utils._typedefs cimport (
5+
float32_t,
6+
float64_t,
7+
int32_t,
8+
int64_t,
9+
intp_t,
10+
)
511

612
import numpy as np
713

@@ -18,11 +24,12 @@ ctypedef fused Y_DTYPE:
1824

1925

2026
def _fit_encoding_fast(
21-
INT_DTYPE[:, ::1] X_int,
27+
INT_DTYPE[::1, :] X_int,
2228
const Y_DTYPE[:] y,
2329
int64_t[::1] n_categories,
2430
double smooth,
2531
double y_mean,
32+
const intp_t[::1] X_indices=None,
2633
):
2734
"""Fit a target encoding on X_int and y.
2835
@@ -33,9 +40,11 @@ def _fit_encoding_fast(
3340
categorical attributes in classification and prediction problems"
3441
"""
3542
cdef:
36-
int64_t sample_idx, feat_idx, cat_idx, n_cats
43+
intp_t sample_idx, row_idx
44+
int64_t feat_idx, cat_idx, n_cats
3745
INT_DTYPE X_int_tmp
38-
int n_samples = X_int.shape[0]
46+
bint use_X_indices = X_indices is not None
47+
intp_t n_samples
3948
int n_features = X_int.shape[1]
4049
double smooth_sum = smooth * y_mean
4150
int64_t max_n_cats = np.max(n_categories)
@@ -46,6 +55,11 @@ def _fit_encoding_fast(
4655
# Gives access to encodings without gil
4756
vector[double*] encoding_vec
4857

58+
if use_X_indices:
59+
n_samples = X_indices.shape[0]
60+
else:
61+
n_samples = X_int.shape[0]
62+
4963
encoding_vec.resize(n_features)
5064
for feat_idx in range(n_features):
5165
current_encoding = np.empty(shape=n_categories[feat_idx], dtype=np.float64)
@@ -61,7 +75,11 @@ def _fit_encoding_fast(
6175
counts[cat_idx] = smooth
6276

6377
for sample_idx in range(n_samples):
64-
X_int_tmp = X_int[sample_idx, feat_idx]
78+
if use_X_indices:
79+
row_idx = X_indices[sample_idx]
80+
else:
81+
row_idx = sample_idx
82+
X_int_tmp = X_int[row_idx, feat_idx]
6583
# -1 are unknown categories, which are not counted
6684
if X_int_tmp == -1:
6785
continue
@@ -78,11 +96,12 @@ def _fit_encoding_fast(
7896

7997

8098
def _fit_encoding_fast_auto_smooth(
81-
INT_DTYPE[:, ::1] X_int,
99+
INT_DTYPE[::1, :] X_int,
82100
const Y_DTYPE[:] y,
83101
int64_t[::1] n_categories,
84102
double y_mean,
85103
double y_variance,
104+
const intp_t[::1] X_indices=None,
86105
):
87106
"""Fit a target encoding on X_int and y with auto smoothing.
88107
@@ -92,10 +111,12 @@ def _fit_encoding_fast_auto_smooth(
92111
categorical attributes in classification and prediction problems"
93112
"""
94113
cdef:
95-
int64_t sample_idx, feat_idx, cat_idx, n_cats
114+
intp_t sample_idx, row_idx
115+
int64_t feat_idx, cat_idx, n_cats
96116
INT_DTYPE X_int_tmp
97117
double diff
98-
int n_samples = X_int.shape[0]
118+
bint use_X_indices = X_indices is not None
119+
intp_t n_samples
99120
int n_features = X_int.shape[1]
100121
int64_t max_n_cats = np.max(n_categories)
101122
double[::1] means = np.empty(max_n_cats, dtype=np.float64)
@@ -107,6 +128,11 @@ def _fit_encoding_fast_auto_smooth(
107128
# Gives access to encodings without gil
108129
vector[double*] encoding_vec
109130

131+
if use_X_indices:
132+
n_samples = X_indices.shape[0]
133+
else:
134+
n_samples = X_int.shape[0]
135+
110136
encoding_vec.resize(n_features)
111137
for feat_idx in range(n_features):
112138
current_encoding = np.empty(shape=n_categories[feat_idx], dtype=np.float64)
@@ -129,7 +155,11 @@ def _fit_encoding_fast_auto_smooth(
129155

130156
# first pass to compute the mean
131157
for sample_idx in range(n_samples):
132-
X_int_tmp = X_int[sample_idx, feat_idx]
158+
if use_X_indices:
159+
row_idx = X_indices[sample_idx]
160+
else:
161+
row_idx = sample_idx
162+
X_int_tmp = X_int[row_idx, feat_idx]
133163

134164
# -1 are unknown categories, which are not counted
135165
if X_int_tmp == -1:
@@ -142,7 +172,11 @@ def _fit_encoding_fast_auto_smooth(
142172

143173
# second pass to compute the sum of squared differences
144174
for sample_idx in range(n_samples):
145-
X_int_tmp = X_int[sample_idx, feat_idx]
175+
if use_X_indices:
176+
row_idx = X_indices[sample_idx]
177+
else:
178+
row_idx = sample_idx
179+
X_int_tmp = X_int[row_idx, feat_idx]
146180
if X_int_tmp == -1:
147181
continue
148182
diff = y[sample_idx] - means[X_int_tmp]

0 commit comments

Comments
 (0)