CoolFace
Apppublic

Aluode/PerceptionLabPortable

sourceHugging Faceupdated 9mo agoView on Hugging Face
0likes
_dict_vectorizer.py460 linesDownload Raw Back to feature_extraction
1# Authors: The scikit-learn developers
2# SPDX-License-Identifier: BSD-3-Clause
3
4from array import array
5from collections.abc import Iterable, Mapping
6from numbers import Number
7from operator import itemgetter
8
9import numpy as np
10import scipy.sparse as sp
11
12from sklearn.utils import metadata_routing
13
14from ..base import BaseEstimator, TransformerMixin, _fit_context
15from ..utils import check_array
16from ..utils.validation import check_is_fitted
17
18
19class DictVectorizer(TransformerMixin, BaseEstimator):
20    """Transforms lists of feature-value mappings to vectors.
21
22    This transformer turns lists of mappings (dict-like objects) of feature
23    names to feature values into Numpy arrays or scipy.sparse matrices for use
24    with scikit-learn estimators.
25
26    When feature values are strings, this transformer will do a binary one-hot
27    (aka one-of-K) coding: one boolean-valued feature is constructed for each
28    of the possible string values that the feature can take on. For instance,
29    a feature "f" that can take on the values "ham" and "spam" will become two
30    features in the output, one signifying "f=ham", the other "f=spam".
31
32    If a feature value is a sequence or set of strings, this transformer
33    will iterate over the values and will count the occurrences of each string
34    value.
35
36    However, note that this transformer will only do a binary one-hot encoding
37    when feature values are of type string. If categorical features are
38    represented as numeric values such as int or iterables of strings, the
39    DictVectorizer can be followed by
40    :class:`~sklearn.preprocessing.OneHotEncoder` to complete
41    binary one-hot encoding.
42
43    Features that do not occur in a sample (mapping) will have a zero value
44    in the resulting array/matrix.
45
46    For an efficiency comparison of the different feature extractors, see
47    :ref:`sphx_glr_auto_examples_text_plot_hashing_vs_dict_vectorizer.py`.
48
49    Read more in the :ref:`User Guide <dict_feature_extraction>`.
50
51    Parameters
52    ----------
53    dtype : dtype, default=np.float64
54        The type of feature values. Passed to Numpy array/scipy.sparse matrix
55        constructors as the dtype argument.
56    separator : str, default="="
57        Separator string used when constructing new features for one-hot
58        coding.
59    sparse : bool, default=True
60        Whether transform should produce scipy.sparse matrices.
61    sort : bool, default=True
62        Whether ``feature_names_`` and ``vocabulary_`` should be
63        sorted when fitting.
64
65    Attributes
66    ----------
67    vocabulary_ : dict
68        A dictionary mapping feature names to feature indices.
69
70    feature_names_ : list
71        A list of length n_features containing the feature names (e.g., "f=ham"
72        and "f=spam").
73
74    See Also
75    --------
76    FeatureHasher : Performs vectorization using only a hash function.
77    sklearn.preprocessing.OrdinalEncoder : Handles nominal/categorical
78        features encoded as columns of arbitrary data types.
79
80    Examples
81    --------
82    >>> from sklearn.feature_extraction import DictVectorizer
83    >>> v = DictVectorizer(sparse=False)
84    >>> D = [{'foo': 1, 'bar': 2}, {'foo': 3, 'baz': 1}]
85    >>> X = v.fit_transform(D)
86    >>> X
87    array([[2., 0., 1.],
88           [0., 1., 3.]])
89    >>> v.inverse_transform(X) == [{'bar': 2.0, 'foo': 1.0},
90    ...                            {'baz': 1.0, 'foo': 3.0}]
91    True
92    >>> v.transform({'foo': 4, 'unseen_feature': 3})
93    array([[0., 0., 4.]])
94    """
95
96    # This isn't something that people should be routing / using in a pipeline.
97    __metadata_request__inverse_transform = {"dict_type": metadata_routing.UNUSED}
98
99    _parameter_constraints: dict = {
100        "dtype": "no_validation",  # validation delegated to numpy,
101        "separator": [str],
102        "sparse": ["boolean"],
103        "sort": ["boolean"],
104    }
105
106    def __init__(self, *, dtype=np.float64, separator="=", sparse=True, sort=True):
107        self.dtype = dtype
108        self.separator = separator
109        self.sparse = sparse
110        self.sort = sort
111
112    def _add_iterable_element(
113        self,
114        f,
115        v,
116        feature_names,
117        vocab,
118        *,
119        fitting=True,
120        transforming=False,
121        indices=None,
122        values=None,
123    ):
124        """Add feature names for iterable of strings"""
125        for vv in v:
126            if isinstance(vv, str):
127                feature_name = "%s%s%s" % (f, self.separator, vv)
128                vv = 1
129            else:
130                raise TypeError(
131                    f"Unsupported type {type(vv)} in iterable "
132                    "value. Only iterables of string are "
133                    "supported."
134                )
135            if fitting and feature_name not in vocab:
136                vocab[feature_name] = len(feature_names)
137                feature_names.append(feature_name)
138
139            if transforming and feature_name in vocab:
140                indices.append(vocab[feature_name])
141                values.append(self.dtype(vv))
142
143    @_fit_context(prefer_skip_nested_validation=True)
144    def fit(self, X, y=None):
145        """Learn a list of feature name -> indices mappings.
146
147        Parameters
148        ----------
149        X : Mapping or iterable over Mappings
150            Dict(s) or Mapping(s) from feature names (arbitrary Python
151            objects) to feature values (strings or convertible to dtype).
152
153            .. versionchanged:: 0.24
154               Accepts multiple string values for one categorical feature.
155
156        y : (ignored)
157            Ignored parameter.
158
159        Returns
160        -------
161        self : object
162            DictVectorizer class instance.
163        """
164        feature_names = []
165        vocab = {}
166
167        for x in X:
168            for f, v in x.items():
169                if isinstance(v, str):
170                    feature_name = "%s%s%s" % (f, self.separator, v)
171                elif isinstance(v, Number) or (v is None):
172                    feature_name = f
173                elif isinstance(v, Mapping):
174                    raise TypeError(
175                        f"Unsupported value type {type(v)} "
176                        f"for {f}: {v}.\n"
177                        "Mapping objects are not supported."
178                    )
179                elif isinstance(v, Iterable):
180                    feature_name = None
181                    self._add_iterable_element(f, v, feature_names, vocab)
182
183                if feature_name is not None:
184                    if feature_name not in vocab:
185                        vocab[feature_name] = len(feature_names)
186                        feature_names.append(feature_name)
187
188        if self.sort:
189            feature_names.sort()
190            vocab = {f: i for i, f in enumerate(feature_names)}
191
192        self.feature_names_ = feature_names
193        self.vocabulary_ = vocab
194
195        return self
196
197    def _transform(self, X, fitting):
198        # Sanity check: Python's array has no way of explicitly requesting the
199        # signed 32-bit integers that scipy.sparse needs, so we use the next
200        # best thing: typecode "i" (int). However, if that gives larger or
201        # smaller integers than 32-bit ones, np.frombuffer screws up.
202        assert array("i").itemsize == 4, (
203            "sizeof(int) != 4 on your platform; please report this at"
204            " https://github.com/scikit-learn/scikit-learn/issues and"
205            " include the output from platform.platform() in your bug report"
206        )
207
208        dtype = self.dtype
209        if fitting:
210            feature_names = []
211            vocab = {}
212        else:
213            feature_names = self.feature_names_
214            vocab = self.vocabulary_
215
216        transforming = True
217
218        # Process everything as sparse regardless of setting
219        X = [X] if isinstance(X, Mapping) else X
220
221        indices = array("i")
222        indptr = [0]
223        # XXX we could change values to an array.array as well, but it
224        # would require (heuristic) conversion of dtype to typecode...
225        values = []
226
227        # collect all the possible feature names and build sparse matrix at
228        # same time
229        for x in X:
230            for f, v in x.items():
231                if isinstance(v, str):
232                    feature_name = "%s%s%s" % (f, self.separator, v)
233                    v = 1
234                elif isinstance(v, Number) or (v is None):
235                    feature_name = f
236                elif not isinstance(v, Mapping) and isinstance(v, Iterable):
237                    feature_name = None
238                    self._add_iterable_element(
239                        f,
240                        v,
241                        feature_names,
242                        vocab,
243                        fitting=fitting,
244                        transforming=transforming,
245                        indices=indices,
246                        values=values,
247                    )
248                else:
249                    raise TypeError(
250                        f"Unsupported value Type {type(v)} "
251                        f"for {f}: {v}.\n"
252                        f"{type(v)} objects are not supported."
253                    )
254
255                if feature_name is not None:
256                    if fitting and feature_name not in vocab:
257                        vocab[feature_name] = len(feature_names)
258                        feature_names.append(feature_name)
259
260                    if feature_name in vocab:
261                        indices.append(vocab[feature_name])
262                        values.append(self.dtype(v))
263
264            indptr.append(len(indices))
265
266        if len(indptr) == 1:
267            raise ValueError("Sample sequence X is empty.")
268
269        indices = np.frombuffer(indices, dtype=np.intc)
270        shape = (len(indptr) - 1, len(vocab))
271
272        result_matrix = sp.csr_matrix(
273            (values, indices, indptr), shape=shape, dtype=dtype
274        )
275
276        # Sort everything if asked
277        if fitting and self.sort:
278            feature_names.sort()
279            map_index = np.empty(len(feature_names), dtype=np.int32)
280            for new_val, f in enumerate(feature_names):
281                map_index[new_val] = vocab[f]
282                vocab[f] = new_val
283            result_matrix = result_matrix[:, map_index]
284
285        if self.sparse:
286            result_matrix.sort_indices()
287        else:
288            result_matrix = result_matrix.toarray()
289
290        if fitting:
291            self.feature_names_ = feature_names
292            self.vocabulary_ = vocab
293
294        return result_matrix
295
296    @_fit_context(prefer_skip_nested_validation=True)
297    def fit_transform(self, X, y=None):
298        """Learn a list of feature name -> indices mappings and transform X.
299
300        Like fit(X) followed by transform(X), but does not require
301        materializing X in memory.
302
303        Parameters
304        ----------
305        X : Mapping or iterable over Mappings
306            Dict(s) or Mapping(s) from feature names (arbitrary Python
307            objects) to feature values (strings or convertible to dtype).
308
309            .. versionchanged:: 0.24
310               Accepts multiple string values for one categorical feature.
311
312        y : (ignored)
313            Ignored parameter.
314
315        Returns
316        -------
317        Xa : {array, sparse matrix}
318            Feature vectors; always 2-d.
319        """
320        return self._transform(X, fitting=True)
321
322    def inverse_transform(self, X, dict_type=dict):
323        """Transform array or sparse matrix X back to feature mappings.
324
325        X must have been produced by this DictVectorizer's transform or
326        fit_transform method; it may only have passed through transformers
327        that preserve the number of features and their order.
328
329        In the case of one-hot/one-of-K coding, the constructed feature
330        names and values are returned rather than the original ones.
331
332        Parameters
333        ----------
334        X : {array-like, sparse matrix} of shape (n_samples, n_features)
335            Sample matrix.
336        dict_type : type, default=dict
337            Constructor for feature mappings. Must conform to the
338            collections.Mapping API.
339
340        Returns
341        -------
342        X_original : list of dict_type objects of shape (n_samples,)
343            Feature mappings for the samples in X.
344        """
345        check_is_fitted(self, "feature_names_")
346
347        # COO matrix is not subscriptable
348        X = check_array(X, accept_sparse=["csr", "csc"])
349        n_samples = X.shape[0]
350
351        names = self.feature_names_
352        dicts = [dict_type() for _ in range(n_samples)]
353
354        if sp.issparse(X):
355            for i, j in zip(*X.nonzero()):
356                dicts[i][names[j]] = X[i, j]
357        else:
358            for i, d in enumerate(dicts):
359                for j, v in enumerate(X[i, :]):
360                    if v != 0:
361                        d[names[j]] = X[i, j]
362
363        return dicts
364
365    def transform(self, X):
366        """Transform feature->value dicts to array or sparse matrix.
367
368        Named features not encountered during fit or fit_transform will be
369        silently ignored.
370
371        Parameters
372        ----------
373        X : Mapping or iterable over Mappings of shape (n_samples,)
374            Dict(s) or Mapping(s) from feature names (arbitrary Python
375            objects) to feature values (strings or convertible to dtype).
376
377        Returns
378        -------
379        Xa : {array, sparse matrix}
380            Feature vectors; always 2-d.
381        """
382        check_is_fitted(self, ["feature_names_", "vocabulary_"])
383        return self._transform(X, fitting=False)
384
385    def get_feature_names_out(self, input_features=None):
386        """Get output feature names for transformation.
387
388        Parameters
389        ----------
390        input_features : array-like of str or None, default=None
391            Not used, present here for API consistency by convention.
392
393        Returns
394        -------
395        feature_names_out : ndarray of str objects
396            Transformed feature names.
397        """
398        check_is_fitted(self, "feature_names_")
399        if any(not isinstance(name, str) for name in self.feature_names_):
400            feature_names = [str(name) for name in self.feature_names_]
401        else:
402            feature_names = self.feature_names_
403        return np.asarray(feature_names, dtype=object)
404
405    def restrict(self, support, indices=False):
406        """Restrict the features to those in support using feature selection.
407
408        This function modifies the estimator in-place.
409
410        Parameters
411        ----------
412        support : array-like
413            Boolean mask or list of indices (as returned by the get_support
414            member of feature selectors).
415        indices : bool, default=False
416            Whether support is a list of indices.
417
418        Returns
419        -------
420        self : object
421            DictVectorizer class instance.
422
423        Examples
424        --------
425        >>> from sklearn.feature_extraction import DictVectorizer
426        >>> from sklearn.feature_selection import SelectKBest, chi2
427        >>> v = DictVectorizer()
428        >>> D = [{'foo': 1, 'bar': 2}, {'foo': 3, 'baz': 1}]
429        >>> X = v.fit_transform(D)
430        >>> support = SelectKBest(chi2, k=2).fit(X, [0, 1])
431        >>> v.get_feature_names_out()
432        array(['bar', 'baz', 'foo'], ...)
433        >>> v.restrict(support.get_support())
434        DictVectorizer()
435        >>> v.get_feature_names_out()
436        array(['bar', 'foo'], ...)
437        """
438        check_is_fitted(self, "feature_names_")
439
440        if not indices:
441            support = np.where(support)[0]
442
443        names = self.feature_names_
444        new_vocab = {}
445        for i in support:
446            new_vocab[names[i]] = len(new_vocab)
447
448        self.vocabulary_ = new_vocab
449        self.feature_names_ = [
450            f for f, i in sorted(new_vocab.items(), key=itemgetter(1))
451        ]
452
453        return self
454
455    def __sklearn_tags__(self):
456        tags = super().__sklearn_tags__()
457        tags.input_tags.dict = True
458        tags.input_tags.two_d_array = False
459        return tags
460 
Aluode/PerceptionLabPortable · CoolFace