Desarrollando estimadores de scikit-learn

Ya sea que estes proponiendo un estimador para su inclusión en scikit-learn, desarrollando un paquete separado compatible con scikit-learn, o implementando componentes personalizados para tus propios proyectos, este capítulo detalla cómo desarrollar objetos que interactúan de forma segura con los Pipelines de scikit-learn y las herramientas de selección de modelos.

APIs de objetos de scikit-learn

Para tener una API uniforme, intentamos tener una API básica común para todos los objetos. Además, para evitar la proliferación de código del framework, intentamos adoptar convenciones sencillas y limitar al mínimo el número de métodos que debe implementar un objeto.

Los elementos de la API de scikit-learn se describen de forma más definitiva en el Glosario de Términos Comunes y Elementos de la API.

Objetos diferentes

Los objetos principales en scikit-learn son (una clase puede implementar múltiples interfaces):

Estimador

El objeto base, implementa un método fit para aprender de los datos, o bien:

estimator = estimator.fit(data, targets)

o:

estimator = estimator.fit(data)
Predictor

Para el aprendizaje supervisado, o algunos problemas no supervisados, implementa:

prediction = predictor.predict(data)

Los algoritmos de clasificación generalmente también ofrecen una manera de cuantificar la certidumbre de una predicción, ya sea usando decision_function o predict_proba:

probability = predictor.predict_proba(data)
Transformador

Para filtrar o modificar los datos, de forma supervisada o no supervisada, implementa:

new_data = transformer.transform(data)

Cuando se ajusta y transforma se puede realizar de forma mucho más eficiente juntos que por separado, implementa:

new_data = transformer.fit_transform(data)
Modelo

Un modelo que puede dar una medida de bondad de ajuste o una probabilidad de los datos no vistos, implementa (mayor es mejor):

score = model.score(data)

Estimadores

La API tiene un objeto predominante: el estimador. Un estimador es un objeto que se ajusta a un modelo basado en algunos datos de capacitación y es capaz de inferir algunas propiedades sobre nuevos datos. Puede ser, por ejemplo, un clasificador o un regresor. Todos los estimadores implementan el método de ajuste:

estimator.fit(X, y)

Todos los estimadores incorporados también tienen un método set_params, que establece parámetros independientes de los datos (anulando los valores de los parámetros anteriores pasados a __init__).

Todos los estimadores en el código base principal de scikit-learn deben heredar de sklearn.base.BaseEstimator.

Instanciación

Se trata de la creación de un objeto. El método __init__ del objeto puede aceptar constantes como argumentos que determinen el comportamiento del estimador (como la constante C en los SVM). Sin embargo, no debería tomar los datos de entrenamiento como argumento, ya que esto se deja al método fit():

clf2 = SVC(C=2.3)
clf3 = SVC([[1, 2], [2, 3]], [-1, 1]) # WRONG!

Los argumentos aceptados por __init__ deberían ser todos argumentos de palabra clave con un valor predeterminado. En otras palabras, un usuario debe ser capaz de instanciar un estimador sin pasarle ningún argumento. Todos los argumentos deberían corresponder a hiperparámetros que describan el modelo o el problema de optimización que el estimador intenta resolver. Estos argumentos (o parámetros) iniciales son siempre recordados por el estimador. También hay que tener en cuenta que no deben documentarse en la sección «Atributos», sino en la sección «Parámetros» de ese estimador.

Además, cada argumento de palabra clave aceptado por __init__ debe corresponder a un atributo en la instancia**. Scikit-learn se basa en esto para encontrar los atributos relevantes para establecer en un estimador al hacer la selección del modelo.

Para resumir, un __init__ debería parecer así:

def __init__(self, param1=1, param2=2):
    self.param1 = param1
    self.param2 = param2

No debería haber lógica, ni siquiera validación de entrada, y los parámetros no deberían cambiarse. La lógica correspondiente debería ponerse donde se utilizan los parámetros, normalmente en fit. Lo siguiente es incorrecto:

def __init__(self, param1=1, param2=2, param3=3):
    # WRONG: parameters should not be modified
    if param1 > 1:
        param2 += 1
    self.param1 = param1
    # WRONG: the object's attributes should have exactly the name of
    # the argument in the constructor
    self.param3 = param2

La razón para posponer la validación es que la misma validación tendría que realizarse en set_params, que se utiliza en algoritmos como GridSearchCV.

Ajuste

Lo siguiente que probablemente querras hacer es estimar algunos parámetros en el modelo. Esto está implementado en el método fit().

El método fit() toma los datos de entrenamiento como argumentos, que puede ser una matriz en el caso de aprendizaje no supervisado, o dos matrices en el caso de aprendizaje supervisado.

Ten en cuenta que el modelo se ajusta usando X y y, pero el objeto no contiene ninguna referencia a X y y. Sin embargo, hay algunas excepciones a esto. como en el caso de los núcleos precomputados donde estos datos deben ser almacenados para su uso por el método predeterminado.

Parámetros

X

array-like de forma (n_samples, n_features)

y

array-like de forma (n_samples,)

kwargs

parámetros opcionales dependientes de datos

X.shape[0] debe ser el mismo que y.shape[0]. Si no se cumple este requisito, se lanzará una excepción de tipo ValueError.

y podría ignorarse en el caso del aprendizaje no supervisado. Sin embargo, para hacer posible el uso del estimador como parte de una cadena que puede mezclar transformadores supervisados y no supervisados, incluso los estimadores no supervisados necesitan aceptar un argumento de palabra clave y=None en la segunda posición que es simplemente ignorado por el estimador. Por la misma razón, los métodos fit_predict, fit_transform, score y partial_fit necesitan aceptar un argumento y en la segunda posición si se implementan.

El método debe devolver el objeto (self). Este patrón es útil para poder implementar one liners rápidos en una sesión de IPython como:

y_predicted = SVC(C=100).fit(X_train, y_train).predict(X_test)

Dependiendo de la naturaleza del algoritmo, fit a veces también puede aceptar argumentos de palabras clave adicionales. Sin embargo, cualquier parámetro al que se le pueda asignar un valor antes de tener acceso a los datos debería ser un argumento de palabra clave __init__. Los parámetros fit deben restringirse a las variables directamente dependientes de los datos. Por ejemplo, una matriz de Gram o una matriz de afinidad que se calculan previamente a partir de la matriz de datos X son dependientes de los datos. Un criterio de parada de tolerancia tol no depende directamente de los datos (aunque el valor óptimo según alguna función de puntuación probablemente lo sea).

Cuando se llama a fit, cualquier llamada anterior a fit debe ser ignorada. En general, llamar a estimator.fit(X1) y luego a estimator.fit(X2) debería ser lo mismo que llamar sólo a estimator.fit(X2). Sin embargo, esto puede no ser cierto en la práctica cuando fit depende de algún proceso aleatorio, ver random_state. Otra excepción a esta regla es cuando el hiperparámetro inicio de calentamiento se establece en Verdadero para los estimadores que lo admiten. El parámetro warm_start=True significa que se reutiliza el estado anterior de los parámetros entrenables del estimador en lugar de utilizar la estrategia de inicialización por defecto.

Atributos estimados

Los atributos que se han estimado a partir de los datos deben tener siempre un nombre que termine con subrayado final, por ejemplo, los coeficientes de algún estimador de regresión se almacenarían en un atributo coef_ después de que se haya llamado fit.

Se espera que los atributos estimados se anulen cuando llame a fit una segunda vez.

Argumentos opcionales

En algoritmos iterativos, el número de iteraciones debe ser especificado por un entero llamado n_iter.

Atributos en pareja

Un estimador que acepta X de la forma (n_samples, n_samples) y define una propiedad _pairwise igual a True permite la validación cruzada del conjunto de datos, por ejemplo, cuando X es una matriz de núcleo precalculada. Específicamente, la propiedad _pairwise es usada por utils.metaestimators._safe_split para dividir filas y columnas.

Obsoleto desde la versión 0.24: El atributo _pairwise queda obsoleto en 0.24. A partir de la versión 1.1 (cambio de nombre de la versión 0.26), se utilizará la etiqueta del estimador pairwise.

Atributos universales

Los estimadores que esperan una entrada tabular deben establecer un atributo n_features_in_ en el momento de fit para indicar el número de características que el estimador espera para las siguientes llamadas a predict o transform. Ver SLEP010 para más detalles.

Rodando tu propio estimador

Si quieres implementar un nuevo estimador que sea compatible con scikit-learn, ya sea sólo para ti o para contribuir a scikit-learn, hay varios aspectos internos de scikit-learn que debes conocer además de la API de scikit-learn descrita anteriormente. Puede comprobar si tu estimador se adhiere a la interfaz y las normas de scikit-learn ejecutando check_estimator en una instancia. El decorador parametrize_with_checks pytest también se puede utilizar (ver su docstring para más detalles y posibles interacciones con pytest):

>>> from sklearn.utils.estimator_checks import check_estimator
>>> from sklearn.svm import LinearSVC
>>> check_estimator(LinearSVC())  # passes

La motivación principal para hacer que una clase sea compatible con la interfaz de estimadores de scikit-learn puede ser que quieras usarla junto con la evaluación del modelo y herramientas de selección como model_selection.GridSearchCV y pipeline.Pipeline.

Antes de detallar la interfaz requerida a continuación, describimos dos maneras de lograr la interfaz correcta más fácilmente.

Project template:

Proveemos una plantilla de proyecto que ayuda en la creación de paquetes Python que contengan estimadores compatibles con scikit-learn. Provee:

  • un repositorio git inicial con estructura de directorio de paquetes Python

  • una plantilla de un estimador de scikit-learn

  • una suite de pruebas inicial incluyendo el uso de check_estimator

  • estructuras de directorios y scripts para compilar documentación y galerías de ejemplo

  • scripts para gestionar la integración continua (pruebas en Linux y Windows)

  • instrucciones para empezar a publicar en PyPi

BaseEstimator y mezclas:

Tendemos a utilizar la «duck typing», por lo que la compilación de un estimador que sigue la API es suficiente para la compatibilidad, sin necesidad de heredar o incluso importar cualquier clase de scikit-learn.

Sin embargo, si una dependencia de scikit-learn es aceptable en su código, puedes evitar un montón de código boilerplate derivando una clase de BaseEstimator y opcionalmente las clases mezcladas en sklearn.base. Por ejemplo, a continuación se muestra un clasificador personalizado, con más ejemplos incluidos en scikit-learn-contrib en su plantilla del proyecto.

>>> import numpy as np
>>> from sklearn.base import BaseEstimator, ClassifierMixin
>>> from sklearn.utils.validation import check_X_y, check_array, check_is_fitted
>>> from sklearn.utils.multiclass import unique_labels
>>> from sklearn.metrics import euclidean_distances
>>> class TemplateClassifier(BaseEstimator, ClassifierMixin):
...
...     def __init__(self, demo_param='demo'):
...         self.demo_param = demo_param
...
...     def fit(self, X, y):
...
...         # Check that X and y have correct shape
...         X, y = check_X_y(X, y)
...         # Store the classes seen during fit
...         self.classes_ = unique_labels(y)
...
...         self.X_ = X
...         self.y_ = y
...         # Return the classifier
...         return self
...
...     def predict(self, X):
...
...         # Check is fit had been called
...         check_is_fitted(self)
...
...         # Input validation
...         X = check_array(X)
...
...         closest = np.argmin(euclidean_distances(X, self.X_), axis=1)
...         return self.y_[closest]

get_params y set_params

Todas las estimaciones de scikit-learn tienen las funciones get_params y set_params. La función get_params no toma argumentos y devuelve un diccionario (dict) de los parámetros __init__ del estimador, junto con sus valores.

Debe tomar un argumento de palabra clave, deep, que recibe un valor booleano que determina si el método debe devolver los parámetros de subestimadores (para la mayoría de los estimadores, esto puede ser ignorado). El valor predeterminado para deep debe ser True. Por ejemplo considerando el siguiente estimador:

>>> from sklearn.base import BaseEstimator
>>> from sklearn.linear_model import LogisticRegression
>>> class MyEstimator(BaseEstimator):
...     def __init__(self, subestimator=None, my_extra_param="random"):
...         self.subestimator = subestimator
...         self.my_extra_param = my_extra_param

El parámetro deep controlará si los parámetros del subsestimator deben ser reportados. Así que cuando deep=True, la salida será:

>>> my_estimator = MyEstimator(subestimator=LogisticRegression())
>>> for param, value in my_estimator.get_params(deep=True).items():
...     print(f"{param} -> {value}")
my_extra_param -> random
subestimator__C -> 1.0
subestimator__class_weight -> None
subestimator__dual -> False
subestimator__fit_intercept -> True
subestimator__intercept_scaling -> 1
subestimator__l1_ratio -> None
subestimator__max_iter -> 100
subestimator__multi_class -> auto
subestimator__n_jobs -> None
subestimator__penalty -> l2
subestimator__random_state -> None
subestimator__solver -> lbfgs
subestimator__tol -> 0.0001
subestimator__verbose -> 0
subestimator__warm_start -> False
subestimator -> LogisticRegression()

A menudo, el subestimador tiene un nombre (como, por ejemplo, los pasos nombrados en un objeto Pipeline), en cuyo caso la clave debería convertirse en <nombre>__C, <nombre>__class_weight, etc.

Mientras que cuando deep=False, la salida será:

>>> for param, value in my_estimator.get_params(deep=False).items():
...     print(f"{param} -> {value}")
my_extra_param -> random
subestimator -> LogisticRegression()

Por otro lado, set_params toma como entrada un diccionario (dict) de la forma 'parameter': value y establece el parámetro del estimador utilizando este diccionario. El valor de retorno debe ser el propio estimador.

Mientras que el mecanismo get_params no es esencial (ver Clonado a continuación), la función set_params es necesaria ya que se usa para establecer parámetros durante las búsquedas en cuadrícula.

La forma más fácil de implementar estas funciones, y de obtener un método __repr__ sensato, es heredar de sklearn.base.BaseEstimator. Si no quieres hacer que tu código dependa de scikit-learn, la forma más fácil de implementar la interfaz es:

def get_params(self, deep=True):
    # suppose this estimator has parameters "alpha" and "recursive"
    return {"alpha": self.alpha, "recursive": self.recursive}

def set_params(self, **parameters):
    for parameter, value in parameters.items():
        setattr(self, parameter, value)
    return self

Parámetros e init

Como model_selection.GridSearchCV utiliza set_params para aplicar la configuración de parámetros a los estimadores, es esencial que llamar a set_params tenga el mismo efecto que la configuración de parámetros utilizando el método __init__. La forma más fácil y recomendada para lograr esto es no hacer ninguna validación de parámetros en __init__. Toda la lógica detrás de los parámetros del estimador, como la traducción de argumentos de cadena en funciones, debe hacerse en fit.

También se espera que los parámetros con la terminación _ no se establezcan dentro del método init__. Todos y sólo los atributos públicos espablecidos por ajuste fit tienen una terminación _. Como resultado, la existencia de los parámetros con el final _ se utiliza para comprobar si el estimador ha sido ajustado.

Clonado

Para su uso con el módulo model_selection, un estimador debe soportar la base.clone para replicar un estimador. Esto puede hacerse proporcionando un método get_params. Si get_params está presente, entonces clone(estimator) será una instancia de type(estimator) en la que set_params ha sido llamado con clones del resultado de estimator.get_params().

Los objetos que no proporcionan este método serán copiados profundamente (usando la función estándar de Python copy.deepcopy) si safe=False es pasado a clone.

Compatibilidad con pipeline

Para que un estimador pueda utilizarse junto con pipeline.Pipeline en cualquier paso, excepto en el último, debe proporcionar una función fit o fit_transform. Para poder evaluar el pipeline en cualquier dato que no sea el conjunto de entrenamiento, también necesita proporcionar una función transform. No hay requisitos especiales para el último paso de un pipeline, excepto que tiene una función fit. Todas las funciones fit y fit_transform deben tomar argumentos X, y, incluso si no se utiliza y. Del mismo modo, para que score sea utilizable, el último paso del pipeline debe tener una función score que acepte un y opcional.

Tipos de estimador

Algunas funcionalidades comunes dependen de la clase de estimador que se pase. Por ejemplo, la validación cruzada en model_selection.GridSearchCV y model_selection.cross_val_score está por defecto estratificada cuando se utiliza en un clasificador, pero no en otro caso. Del mismo modo, los calificadores de precisión media que toman una predicción continua necesitan llamar a decision_function para los clasificadores, pero a predict para los regresores. Esta distinción entre clasificadores y regresores se implementa mediante el atributo _estimator_type, que toma un valor de cadena. Debe ser classifier para los clasificadores y regresor para los regresores y clusterer para los métodos de clustering, para que funcione como se espera. Al heredar de ClassifierMixin, RegressorMixin o ClusterMixin se establecerá el atributo automáticamente. Cuando un metaestimador necesita distinguir entre tipos de estimadores, en lugar de comprobar _estimator_type directamente, se deben utilizar ayudantes como base.is_classifier`.

Modelos específicos

Los clasificadores deben aceptar argumentos y (objetivo) para fit que son secuencias (listas, arreglos) de cadenas o enteros. No deben asumir que las etiquetas de clase son un rango contiguo de enteros; en su lugar, deben almacenar una lista de clases en un atributo o propiedad classes_. El orden de las etiquetas de clase en este atributo debe coincidir con el orden en que predict_proba, predict_log_proba y decision_function devuelven sus valores. La forma más sencilla de conseguirlo es poner:

self.classes_, y = np.unique(y, return_inverse=True)

en fit. Esto devuelve un nuevo y que contiene índices de clase, en lugar de etiquetas, en el rango [0, n_classes).

El método predict de un clasificador debe devolver arreglos que contengan etiquetas de clase de classes_. En un clasificador que implementa decision_function, esto se puede lograr con:

def predict(self, X):
    D = self.decision_function(X)
    return self.classes_[np.argmax(D, axis=1)]

En modelos lineales, los coeficientes se almacenan en un arreglo llamado coef_, y el término independiente se almacena en intercept_. sklearn.linear_model._base contiene algunas clases base y mezclas que implementan patrones de modelo lineal comunes.

El módulo sklearn.utils.multiclass contiene funciones útiles para trabajar con problemas multiclase y multietiqueta.

Etiquetas de Estimador

Advertencia

Las etiquetas de estimación son experimentales y la API está sujeta a cambios.

Scikit-learn introdujo etiquetas de estimadores en la versión 0.21. Son anotaciones de los estimadores que permiten la inspección programática de sus capacidades, como el soporte de matrices dispersas, los tipos de salida y los métodos soportados. Las etiquetas del estimador son un diccionario devuelto por el método _get_tags(). Estas etiquetas se utilizan en las comprobaciones comunes ejecutadas por la función check_estimator y el decorador parametrize_with_checks. Las etiquetas determinan qué comprobaciones deben realizarse y qué datos de entrada son apropiados. Las etiquetas pueden depender de los parámetros del estimador o incluso de la arquitectura del sistema y, en general, sólo pueden determinarse en tiempo de ejecución.

El conjunto actual de etiquetas de estimación son:

allow_nan (default=False)

si el estimador soporta datos con valores faltantes codificados como np.NaN

binary_only (default=False)

si el estimador soporta la clasificación binaria pero carece de soporte de clasificación multiclase.

multilabel (default=False)

si el estimador soporta salida multietiqueta

multioutput (default=False)

si un regresor admite salidas multiobjetivo o un clasificador admite multiples salidas multiclase.

multioutput_only (default=False)

si el estimador soporta sólo clasificación o regresión de salida múltiple.

no_validation (default=False)

si el estimador se salta la validación de entrada. Esto sólo está pensado para los transformadores sin estado y ficticios!

non_deterministic (default=False)

si el estimador no es determinista dado un random_state fijo

por pares (default=False)

Este atributo booleano indica si los datos (X) fit y métodos similares consisten en medidas de pares sobre muestras en lugar de una representación de características para cada muestra. Suele ser True cuando un estimador tiene un parámetro métric o de affinity o kernel con valor precomputed. Su propósito principal es que cuando un meta-estimator extrae una submuestra de datos destinada a un estimador por pares, los datos necesitan ser indexados en ambos ejes, mientras que otros datos son indexados sólo en el primer eje.

preserves_dtype (default=``[np.float64]``)

se aplica sólo a los transformadores. Corresponde a los tipos de datos que se conservarán de forma que X_trans.dtype sea el mismo que X.dtype después de llamar a transformer.transform(X). Si esta lista está vacía, no se espera que el transformador conserve el tipo de datos. El primer valor de la lista se considera el tipo de datos predeterminados, correspondiente al tipo de datos de la salida cuando no se va a preservar el tipo de datos de entrada.

poor_score (default=False)

si el estimador no proporciona una puntuación «razonable» del conjunto de pruebas, que actualmente para la regresión es un R2 de 0,5 en un subconjunto del conjunto de datos de viviendas de Boston, y para la clasificación una precisión de 0,83 en make_blobs(n_samples=300, random_state=0). Estos conjuntos de datos y valores se basan en los estimadores actuales de sklearn y podrían ser sustituidos por algo más sistemático.

requires_fit (default=True)

si el estimador requiere ser ajustado antes de llamar a una de las funciones transform, predict, predict_proba, o decision_function.

requires_positive_X (default=False)

si el estimador requiere una X positiva.

requires_y (default=False)

si el estimador requiere que se pase y a los métodos fit, fit_predict o fit_transform. La etiqueta es True para los estimadores que heredan de ~sklearn.base.RegressorMixin y ~sklearn.base.ClassifierMixin.

requires_positive_y (default=False)

si el estimador requiere una y positiva (sólo aplicable para la regresión).

_skip_test (default=False)

si omitir por completo las pruebas comunes. No utilice esto a menos que tenga una muy buena.

_xfail_checks (default=False)

diccionario {check_name: reason} de comprobaciones comunes que se marcarán como XFAIL para pytest, cuando se utilice parametrize_with_checks. Estas comprobaciones serán simplemente ignoradas y no serán ejecutadas por check_estimator, pero se lanzará un SkipTestWarning. No utilices esto a menos que haya una muy buena razón para que su estimador no pase la comprobación. También ten en cuenta que el uso de esta etiqueta está muy sujeto a cambios porque estamos tratando de hacerla más flexible: prepárate para cambios de última hora en el futuro.

stateless (default=False)

si el estimador necesita acceso a los datos para ajuste. Aunque un estimador no tiene estado, podría necesitar una llamada a fit para la inicialización.

X_types (default=[“2darray”])

Tipos de entrada soportados para X como lista de cadenas. Actualmente las pruebas sólo se ejecutan si “2darray” está contenido en la lista, lo que significa que el estimador toma arreglos numpy continuos 2d como entrada. El valor por defecto es [“2darray”]. Otros tipos posibles son 'string', 'sparse', 'categorical', dict, '1dlabels' y '2dlabels'. El objetivo es que en el futuro el tipo de entrada soportado determine los datos utilizados durante la prueba, en particular para los datos``”string”, ``'sparse' y 'categorical'. Por ahora, las pruebas para datos dispersos no hacen uso de la etiqueta 'sparse'.

Es poco probable que los valores por defecto de cada etiqueta se ajusten a las necesidades de su estimador específico. Etiquetas adicionales pueden ser creadas o por defecto pueden ser sobreescritas definiendo un método _more_tags() que devuelve un diccionario (dict) con las etiquetas sobreescritas o nuevas etiquetas. Por ejemplo:

class MyMultiOutputEstimator(BaseEstimator):

    def _more_tags(self):
        return {'multioutput_only': True,
                'non_deterministic': True}

Cualquier etiqueta que no esté en _more_tags() simplemente volverá a los valores predeterminados documentados arriba.

Incluso si no se recomienda, es posible sobreescribir el método _get_tags(). Sin embargo, ten en cuenta que todas las etiquetas deben estar presentes en el diccionario (dict). Si alguna de las claves documentadas arriba no está presente en la salida de _get_tags(), ocurrirá un error.

Además de las etiquetas, los estimadores también necesitan declarar cualquier parámetro no opcional a __init__ en el atributo de clase _required_parameters, que es una lista o tupla. Si _required_parameters es sólo ["estimator"] o ["base_estimator"], el estimador se instanciará con una instancia de LinearDiscriminantAnalysis (o RidgeRegression si el estimador es un regresor) en las pruebas. La elección de estos dos modelos es algo idiosincrática, pero ambos deberían proporcionar soluciones robustas de forma cerrada.

Directrices de codificación

A continuación se presentan algunas directrices sobre cómo debe escribirse el nuevo código para su inclusión en scikit-learn, y que puede ser apropiado adoptar en proyectos externos. Por supuesto, hay casos especiales y habrá excepciones a estas reglas. Sin embargo, seguir estas reglas cuando se presenta un nuevo código facilita la revisión, por lo que el nuevo código puede ser integrado en menos tiempo.

Un código con formato uniforme hace más fácil compartir la propiedad del código. El proyecto scikit-learn trata de seguir de cerca las directrices oficiales de Python detalladas en PEP8 que detallan cómo el código debe ser formateado y sangrado. Por favor, Léelo y síguelo.

Además, añadimos las siguientes directrices:

  • Usa guiones bajos para separar palabras en nombres no de clase: n_samples en lugar de nsamples.

  • Evita múltiples sentencias en una línea. Prefiere un retorno de línea después de una sentencia de flujo de control (if/for).

  • Utiliza las importaciones relativas para las referencias dentro de scikit-learn.

  • Las pruebas unitarias son una excepción a la regla anterior; deben usar importaciones absolutas, exactamente como lo haría el código del cliente. Un corolario es que, si sklearn.foo exporta una clase o función que está implementada en sklearn.foo.bar.baz, la prueba debe importarla de sklearn.foo.

  • Por favor, no utilices import * en ningún caso. Se considera perjudicial según las recomendaciones oficiales de Python <https://docs.python.org/3.1/howto/doanddont.html#at-module-level>`_. Hace que el código sea más difícil de leer ya que el origen de los símbolos ya no está explícitamente referenciado, pero lo más importante es que impide el uso de una herramienta de análisis estático como pyflakes para encontrar automáticamente errores en scikit-learn.

  • Usa el numpy docstring standard en todos tus docstrings.

Un buen ejemplo de código que nos gusta se puede encontrar aquí.

Validación de entrada

El módulo sklearn.utils contiene varias funciones para realizar la validación y conversión de entradas. A veces, np.asarray es suficiente para la validación; no utilices np.asanyarray o np. tleast_2d, ya que estos permiten pasar np.matrix de NumPy, que tiene una API diferente (por ejemplo, * significa producto punto en np.matrix, pero producto de Hadamard en np.ndarray).

En otros casos, asegúrate de llamar a check_array en cualquier argumento array-like pasado a una función API de scikit-learn. Los parámetros exactos a utilizar dependen principalmente de si se deben aceptar las matrices scipy.sparse y de cuáles.

Para obtener más información, consulta la página Utilidades para Desarrolladores.

Números aleatorios

Si su código depende de un generador de números aleatorios, no utilices numpy.random.random() o rutinas similares. Para asegurar la repetibilidad en la comprobación de errores, la rutina debería aceptar una palabra clave random_state y usarla para construir un objeto numpy.random.RandomState. Ver sklearn.utils.check_random_state en Utilidades para Desarrolladores.

Aquí hay un ejemplo simple de código usando algunas de las directrices anteriores:

from sklearn.utils import check_array, check_random_state

def choose_random_sample(X, random_state=0):
    """Choose a random point from X.

    Parameters
    ----------
    X : array-like of shape (n_samples, n_features)
        An array representing the data.
    random_state : int or RandomState instance, default=0
        The seed of the pseudo random number generator that selects a
        random sample. Pass an int for reproducible output across multiple
        function calls.
        See :term:`Glossary <random_state>`.

    Returns
    -------
    x : ndarray of shape (n_features,)
        A random point selected from X.
    """
    X = check_array(X)
    random_state = check_random_state(random_state)
    i = random_state.randint(X.shape[0])
    return X[i]

Si utiliza la aleatoriedad en un estimador en lugar de una función independiente, se aplican algunas directrices adicionales.

En primer lugar, el estimador debe tomar un argumento estado_aleatorio en su __init__ con un valor predeterminado de None. Debería almacenar el valor de ese argumento, sin modificar, en un atributo random_state. fit puede llamar a check_random_state en ese atributo para obtener un generador de números aleatorios real. Si, por alguna razón, la aleatoriedad es necesaria después de fit, el RNG debe ser almacenado en un atributo random_state_. El siguiente ejemplo debería aclarar esto:

class GaussianNoise(BaseEstimator, TransformerMixin):
    """This estimator ignores its input and returns random Gaussian noise.

    It also does not adhere to all scikit-learn conventions,
    but showcases how to handle randomness.
    """

    def __init__(self, n_components=100, random_state=None):
        self.random_state = random_state
        self.n_components = n_components

    # the arguments are ignored anyway, so we make them optional
    def fit(self, X=None, y=None):
        self.random_state_ = check_random_state(self.random_state)

    def transform(self, X):
        n_samples = X.shape[0]
        return self.random_state_.randn(n_samples, self.n_components)

La razón de esta configuración es la reproducibilidad: cuando un estimador es ajustado dos veces a los mismos datos, debería producir un modelo idéntico ambas veces, de ahí la validación en fit, no __init__.