Un aspecto importante en cualquier aplicación de Machine Learning que suele pasar desapercibida para mucha gente es la habilidad de los modelos predictivos para explicar porqué devuelven ciertas predicciones en lugar de otras. Llamamos explicabilidad o interpretabilidad a dicha habilidad.
Por ejemplo, supongamos que trabajamos como consultores externos para una empresa elaborando modelos. Seguramente, a dicha empresa no le va a gustar que le digamos que el modelo que forma parte vital de su negocio es una “caja negra” que funciona mágicamente (de forma inexplicable) y que seguramente no podrán mantener una vez hayamos pasado a otro proyecto.
En otros casos, la explicabilidad de un modelo no solo es importante, sino obligatoria. En industrias altamente reguladas (por ejemplo, en el sector financiero) el explicar porqué un modelo funciona de la manera que funciona es obligatorio. En la Unión Europea, la reciente implementación de la nueva directiva de datos GDPR, en concreto su artículo 22 indica que:
El sujeto de los datos tendrá el derecho a no ser sujeto a una decisión basada únicamente en un proceso automático…
según la interpretación (dado que la ley es muy, muy nueva), esto puede llegar a obligar a que los usuarios tengan derecho a saber porqué un sistema automático ha tomado una decisión que les afecta (y digo puede porque la regulación no es muy concreta al respecto, pero hay debate sobre cómo esto puede cargarse modelos de caja negra tipo deep learning).
Vamos a comentar aquí algunas medidas que nos permiten proporcionar cierta explicabilidad a nuestros modelos:
En primer lugar, pongo las características de mi ordenador y de los paquetes que vamos a usar. En concreto, vamos a usar lo siguiente:
1 2 |
%load_ext watermark %watermark -v -m -p sklearn,pdpbox,lime,eli5 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
CPython 3.6.4 IPython 6.3.1 sklearn 0.19.1 pdpbox 0.2.0 lime n eli5 0.8 compiler : GCC 7.2.0 system : Linux release : 4.15.0-36-generic machine : x86_64 processor : x86_64 CPU cores : 8 interpreter: 64bit |
Imports y eso
1 2 3 4 5 6 7 |
from IPython.display import Image import pandas as pd import numpy as np import matplotlib.pyplot as plt from matplotlib import cm import warnings warnings.simplefilter("ignore") %matplotlib inline |
1 2 |
# figuritas grandes plt.rcParams['figure.figsize'] = [10, 10] |
Cargamos los datos
Para empezar con algo sencillito, vamos a usar el dataset de casas de Boston, Boston Housing Dataset que contiene información sobre los precios y características de bloques de casas en la ciudad de Boston. La variable objetivo de este dataset PREDV
es el precio medio de las casas en un bloque en función de las características de dicha área residencial.
Está disponible en scikit-learn por lo tanto no tenemos que descargarnos nada:
1 2 3 4 5 6 7 |
from sklearn.datasets import load_boston from sklearn.model_selection import train_test_split boston = load_boston() X = pd.DataFrame(boston.data, columns=boston.feature_names) y = boston.target X.head() |
CRIM | ZN | INDUS | CHAS | NOX | RM | AGE | DIS | RAD | TAX | PTRATIO | B | LSTAT | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 0.00632 | 18.0 | 2.31 | 0.0 | 0.538 | 6.575 | 65.2 | 4.0900 | 1.0 | 296.0 | 15.3 | 396.90 | 4.98 |
1 | 0.02731 | 0.0 | 7.07 | 0.0 | 0.469 | 6.421 | 78.9 | 4.9671 | 2.0 | 242.0 | 17.8 | 396.90 | 9.14 |
2 | 0.02729 | 0.0 | 7.07 | 0.0 | 0.469 | 7.185 | 61.1 | 4.9671 | 2.0 | 242.0 | 17.8 | 392.83 | 4.03 |
3 | 0.03237 | 0.0 | 2.18 | 0.0 | 0.458 | 6.998 | 45.8 | 6.0622 | 3.0 | 222.0 | 18.7 | 394.63 | 2.94 |
4 | 0.06905 | 0.0 | 2.18 | 0.0 | 0.458 | 7.147 | 54.2 | 6.0622 | 3.0 | 222.0 | 18.7 | 396.90 | 5.33 |
1 |
y[:10] |
1 |
array([24. , 21.6, 34.7, 33.4, 36.2, 28.7, 22.9, 27.1, 16.5, 18.9]) |
Nota: Los precios están en miles de dólares, y el dataset es de 1978. Hoy en dia no hay casas que valgan 24,000$ desgraciadamente 😉
El diccionario de datos lo podéis consultar aquí. De todas formas todos los datasets de scikit-learn tienen el atributo DESCR
que nos proporciona su descripción:
1 |
print(boston.DESCR) |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
Boston House Prices dataset =========================== Notes ------ Data Set Characteristics: :Number of Instances: 506 :Number of Attributes: 13 numeric/categorical predictive :Median Value (attribute 14) is usually the target :Attribute Information (in order): - CRIM per capita crime rate by town - ZN proportion of residential land zoned for lots over 25,000 sq.ft. - INDUS proportion of non-retail business acres per town - CHAS Charles River dummy variable (= 1 if tract bounds river; 0 otherwise) - NOX nitric oxides concentration (parts per 10 million) - RM average number of rooms per dwelling - AGE proportion of owner-occupied units built prior to 1940 - DIS weighted distances to five Boston employment centres - RAD index of accessibility to radial highways - TAX full-value property-tax rate per $10,000 - PTRATIO pupil-teacher ratio by town - B 1000(Bk - 0.63)^2 where Bk is the proportion of blacks by town - LSTAT % lower status of the population - MEDV Median value of owner-occupied homes in $1000's :Missing Attribute Values: None :Creator: Harrison, D. and Rubinfeld, D.L. This is a copy of UCI ML housing dataset. http://archive.ics.uci.edu/ml/datasets/Housing This dataset was taken from the StatLib library which is maintained at Carnegie Mellon University. The Boston house-price data of Harrison, D. and Rubinfeld, D.L. 'Hedonic prices and the demand for clean air', J. Environ. Economics & Management, vol.5, 81-102, 1978. Used in Belsley, Kuh & Welsch, 'Regression diagnostics ...', Wiley, 1980. N.B. Various transformations are used in the table on pages 244-261 of the latter. The Boston house-price data has been used in many machine learning papers that address regression problems. **References** - Belsley, Kuh & Welsch, 'Regression diagnostics: Identifying Influential Data and Sources of Collinearity', Wiley, 1980. 244-261. - Quinlan,R. (1993). Combining Instance-Based and Model-Based Learning. In Proceedings on the Tenth International Conference of Machine Learning, 236-243, University of Massachusetts, Amherst. Morgan Kaufmann. - many more! (see http://archive.ics.uci.edu/ml/datasets/Housing) |
Ahora generamos el dataset de entrenamiento:
1 2 |
variables_independientes = boston.feature_names X_train, X_test, y_train, y_test = train_test_split(boston.data, boston.target, test_size=0.2) |
Ya tenemos un dataset de entrenamiento y otro de validación, tenemos varias opciones a la hora de proporcionar cierta explicabilidad a nuestras predicciones:
Opción 1. Usar modelos explicables.
Hay ciertos modelos que por su definición son intrínsecamente explicables.
Por ejemplo, para modelos de regresión lineal, podemos crear una fórmula y explicar que el modelo simplemente es un producto lineal de coeficientes y las variables independientes.
1 2 3 |
from sklearn.linear_model import LinearRegression model_ols = LinearRegression() model_ols.fit(X_train, y_train) |
1 |
LinearRegression(copy_X=True, fit_intercept=True, n_jobs=1, normalize=False) |
1 |
model_ols.intercept_ |
1 |
42.75925047586816 |
Por ejemplo, podemos ver los coeficientes que se multiplican a cada variable de forma sencilla mediante el atributo _coef
del modelo.
1 |
dict(zip(boston.feature_names, model_ols.coef_)) |
1 2 3 4 5 6 7 8 9 10 11 12 13 |
{'CRIM': -0.10628408093803422, 'ZN': 0.05719302123216638, 'INDUS': 0.01858562853463657, 'CHAS': 3.03591544145478, 'NOX': -20.30932114037633, 'RM': 3.18058175303253, 'AGE': 0.007219326713202369, 'DIS': -1.7383059571474184, 'RAD': 0.3322161390394403, 'TAX': -0.013897273320144073, 'PTRATIO': -0.9461324334577969, 'B': 0.009246372584185768, 'LSTAT': -0.5571793936674317} |
Para modelos basados en árboles, podemos usar el atributo feature_importances
para conocer la importancia de cada variable a la hora de hacer una partición (partir un nodo en subnodos).
1 2 3 |
from sklearn.tree import DecisionTreeRegressor model_tree = DecisionTreeRegressor(max_depth=5) model_tree.fit(X_train, y_train) |
1 2 3 4 5 |
DecisionTreeRegressor(criterion='mse', max_depth=5, max_features=None, max_leaf_nodes=None, min_impurity_decrease=0.0, min_impurity_split=None, min_samples_leaf=1, min_samples_split=2, min_weight_fraction_leaf=0.0, presort=False, random_state=None, splitter='best') |
Ahora que tenemos el modelo ajustado podemos usar feature_importances_
1 |
dict(zip(boston.feature_names, model_tree.feature_importances_)) |
1 2 3 4 5 6 7 8 9 10 11 12 13 |
{'CRIM': 0.00891653770841052, 'ZN': 0.0, 'INDUS': 0.0, 'CHAS': 0.0, 'NOX': 0.030684728646413053, 'RM': 0.27879476963541194, 'AGE': 0.0028730119491539702, 'DIS': 0.09008882718769619, 'RAD': 0.0012602535791255542, 'TAX': 0.01299990801227938, 'PTRATIO': 0.028939765371919998, 'B': 1.4288589332512238e-06, 'LSTAT': 0.5454407690506561} |
Así que en el caso concreto de este modelo, la variable que más
importancia tiene a la hora de realizar una predicción es la variable RM
(lo cual tiene sentido, el precio de una casa está muy relacionado con su número de habitaciones) y la variable LSTAT
(barrios con mayor gente de clase alta tendrán casas más caras y al revés).
Si el modelo que elegimos no es uno de los de arriba, tenemos que usar otras formas de proporcionar explicabilidad al modelo. Vamos a ver un par de métodos.
Permutation importances
Permutation importances (importancia de permutaciones) es un método para proporcionar explicabilidad a un estimador de caja negra.
En este método, la importancia de cada variable independiente a la hora de predecir se estima mirando cómo varía la puntuación o el error (precisión, F1, \(R^{2}\), etc.) del modelo cuando dicha variable no está disponible
Podemos usar el paquete eli5
para importancia de cada variable mediante Permutation Importances.
Por ejemplo, vamos a usar ahora un modelo SVR (Máquina de Vectores Soporte para problemas de regresión) para entrenar en el mismo dataset y usaremos eli5
para calcular la importancia de cada variable independiente mediante Permutation Importance.
1 2 3 |
from sklearn.svm import SVR from eli5.sklearn import PermutationImportance from eli5 import explain_weights |
Ajustamos el modelo a los datos
1 |
svr = SVR().fit(X_train, y_train) |
Ahora entrenamos el the PermutationImportance explainer
en el dataset de test. Debemos pasarle el argumento scoring
que indica la función de evaluación que vamos a usar. Podemos usar cualquiera de las que proporciona scikit-learn por defecto o crear la nuestra propia.
Por ejemplo, nosotros seleccionamos el error absoluto medio (neg_mean_absolute_error
en scikit-learn). Es importante mencionar que en sklearn los errores se proporcionan en negativo.
1 2 3 4 |
explainer = PermutationImportance( svr, scoring="neg_mean_absolute_error" ).fit(X_test, y_test) |
Ahora podemos usar la función explain_weights
para que eli5
nos indique la importancia de cada variable independiente a la hora de tomar una decisión.
1 2 3 4 |
explain_weights( explainer, feature_names=boston.feature_names ) |
Weight | Feature |
---|---|
0.0927 ± 0.0260 | TAX |
0.0750 ± 0.0193 | INDUS |
0.0649 ± 0.0398 | AGE |
0.0638 ± 0.0419 | LSTAT |
0.0546 ± 0.0160 | RAD |
0.0414 ± 0.0384 | ZN |
0.0382 ± 0.0219 | CRIM |
0.0379 ± 0.0165 | PTRATIO |
0.0352 ± 0.0183 | DIS |
0.0030 ± 0.0040 | RM |
0.0007 ± 0.0016 | CHAS |
0.0003 ± 0.0001 | NOX |
-0.0032 ± 0.0357 | B |
Vemos, que para el modelo SVM, la variable más importante a la hora de predecir el precio de una casa es la cantidad de impuestos que se pagan en la zona y la edad de las casas.
Hay que tener en cuenta que diferentes algoritmos darán diferentes importancias a cada variable! Por ejemplo, si ahora usamos un Perceptrón Multicapa (MLP):
1 2 3 4 5 6 7 8 9 10 11 |
from sklearn.neural_network import MLPRegressor np.random.seed(42) mlp = MLPRegressor().fit(X_train, y_train) explainer = PermutationImportance( mlp, scoring="neg_mean_absolute_error" ).fit(X_test, y_test) explain_weights( explainer, feature_names=boston.feature_names ) |
Weight | Feature |
---|---|
1.0067 ± 0.1530 | TAX |
0.8525 ± 0.3151 | B |
0.8334 ± 0.3482 | ZN |
0.6072 ± 0.1576 | LSTAT |
0.1445 ± 0.0718 | CRIM |
0.0991 ± 0.0849 | PTRATIO |
0.0216 ± 0.0207 | DIS |
0.0003 ± 0.0049 | CHAS |
-0.0000 ± 0.0008 | NOX |
-0.0034 ± 0.0320 | RAD |
-0.0333 ± 0.0204 | RM |
-0.0471 ± 0.2234 | AGE |
-0.1557 ± 0.1739 | INDUS |
Vemos que dicho algoritmo predice que a la hora de predecir el precio de las viviendas las variables más importantes son el nivel fiscal y el porcentaje de la población de raza negra (algoritmos racistas, anyone?).
LIME
LIME (Local Interpretable Model-Agnostic Explanations) es un método que se puede usar para explicar el output de cualquier clasificador. Es una técnica bastante reciente (aquí está el paper, y la explicación del método por parte del autor)
A diferencia de Permutacion Importances, LIME se aplica a una observación cada vez, y funciona haciendo perturbaciones aleatorias de dicha observación (es decir, cambiando un poquito la observación cada vez) y viendo cómo varían las prediciones del modelo (es decir, la clase predicha). Una vez hecho esto se entrena un modelo de regresión lineal usando como variables independientes las permutaciones y como variable dependiente (u objetivo) la predicción del modelo para cada perturbación.

Traducido del artículo del autor:
La función de decisión original del modelo se representa mediante el fondo de color azul/rosa y es claramente no lineal. La equis de color rojo brillante representa la observación que está siendo explicada (llamémosla X). Tomamos un muestreo de observaciones alrededor de X, y les asignamos pesos según su proximidad a X (aquí representamos el peso mediante el tamaño). Calculamos la predicción del modelo original en cada una de estas observaciones perturbadas y entonces aprendemos un modelo lineal (línea intermitente) que aproxima el modelo bien en la vecindad de X. Nótese que la explicación de dicho modelo no es fiable a nivel global, pero es fiable a nivel local alrededor de X.
Podemos usar el paquete lime
para usar LIME en python.
1 |
import lime.lime_tabular |
Para este ejemplo vamos a usar un dataset clásico de clasificación, el dataset de Cáncer de Pecho (Breast Cancer Dataset), que contiene mediciones sobre observaciones de células potencialmente cancerosas, y donde la variable objetivo es si la paciente tiene cáncer maligno o benigno.

1 2 3 |
from sklearn.datasets import load_breast_cancer cancer = load_breast_cancer() print(cancer.DESCR) |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 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 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 |
Breast Cancer Wisconsin (Diagnostic) Database ============================================= Notes ----- Data Set Characteristics: :Number of Instances: 569 :Number of Attributes: 30 numeric, predictive attributes and the class :Attribute Information: - radius (mean of distances from center to points on the perimeter) - texture (standard deviation of gray-scale values) - perimeter - area - smoothness (local variation in radius lengths) - compactness (perimeter^2 / area - 1.0) - concavity (severity of concave portions of the contour) - concave points (number of concave portions of the contour) - symmetry - fractal dimension ("coastline approximation" - 1) The mean, standard error, and "worst" or largest (mean of the three largest values) of these features were computed for each image, resulting in 30 features. For instance, field 3 is Mean Radius, field 13 is Radius SE, field 23 is Worst Radius. - class: - WDBC-Malignant - WDBC-Benign :Summary Statistics: ===================================== ====== ====== Min Max ===================================== ====== ====== radius (mean): 6.981 28.11 texture (mean): 9.71 39.28 perimeter (mean): 43.79 188.5 area (mean): 143.5 2501.0 smoothness (mean): 0.053 0.163 compactness (mean): 0.019 0.345 concavity (mean): 0.0 0.427 concave points (mean): 0.0 0.201 symmetry (mean): 0.106 0.304 fractal dimension (mean): 0.05 0.097 radius (standard error): 0.112 2.873 texture (standard error): 0.36 4.885 perimeter (standard error): 0.757 21.98 area (standard error): 6.802 542.2 smoothness (standard error): 0.002 0.031 compactness (standard error): 0.002 0.135 concavity (standard error): 0.0 0.396 concave points (standard error): 0.0 0.053 symmetry (standard error): 0.008 0.079 fractal dimension (standard error): 0.001 0.03 radius (worst): 7.93 36.04 texture (worst): 12.02 49.54 perimeter (worst): 50.41 251.2 area (worst): 185.2 4254.0 smoothness (worst): 0.071 0.223 compactness (worst): 0.027 1.058 concavity (worst): 0.0 1.252 concave points (worst): 0.0 0.291 symmetry (worst): 0.156 0.664 fractal dimension (worst): 0.055 0.208 ===================================== ====== ====== :Missing Attribute Values: None :Class Distribution: 212 - Malignant, 357 - Benign :Creator: Dr. William H. Wolberg, W. Nick Street, Olvi L. Mangasarian :Donor: Nick Street :Date: November, 1995 This is a copy of UCI ML Breast Cancer Wisconsin (Diagnostic) datasets. https://goo.gl/U2Uwz2 Features are computed from a digitized image of a fine needle aspirate (FNA) of a breast mass. They describe characteristics of the cell nuclei present in the image. Separating plane described above was obtained using Multisurface Method-Tree (MSM-T) [K. P. Bennett, "Decision Tree Construction Via Linear Programming." Proceedings of the 4th Midwest Artificial Intelligence and Cognitive Science Society, pp. 97-101, 1992], a classification method which uses linear programming to construct a decision tree. Relevant features were selected using an exhaustive search in the space of 1-4 features and 1-3 separating planes. The actual linear program used to obtain the separating plane in the 3-dimensional space is that described in: [K. P. Bennett and O. L. Mangasarian: "Robust Linear Programming Discrimination of Two Linearly Inseparable Sets", Optimization Methods and Software 1, 1992, 23-34]. This database is also available through the UW CS ftp server: ftp ftp.cs.wisc.edu cd math-prog/cpo-dataset/machine-learn/WDBC/ References ---------- - W.N. Street, W.H. Wolberg and O.L. Mangasarian. Nuclear feature extraction for breast tumor diagnosis. IS&T/SPIE 1993 International Symposium on Electronic Imaging: Science and Technology, volume 1905, pages 861-870, San Jose, CA, 1993. - O.L. Mangasarian, W.N. Street and W.H. Wolberg. Breast cancer diagnosis and prognosis via linear programming. Operations Research, 43(4), pages 570-577, July-August 1995. - W.H. Wolberg, W.N. Street, and O.L. Mangasarian. Machine learning techniques to diagnose breast cancer from fine-needle aspirates. Cancer Letters 77 (1994) 163-171. |
Generamos dataset de entrenamiento y test de nuevo.
1 2 3 4 5 6 |
X_train, X_test, y_train, y_test = train_test_split( cancer.data, cancer.target, test_size=0.2, random_state=42 ) |
Ahora utilizamos un modelo SVM para clasificación para entrenar un modelo que clasifique correctamente los cánceres malignos
1 2 |
from sklearn.svm import SVC svc = SVC( probability=True).fit(X_train, y_train) |
Como este dataset es un dataset tabular estructurado (o sea, una tabla/dataframe), podemos usar LimeTabularExplainer
para usar LIME. Simplemente tenemos que usar como argumentos los datos de entrenamiento (X_train
, o sea las variables independientes), los nombres de las variables independientes, los nombres de las clases de la variable objetivo. También es conveniente usar discretize_continuous
que agrupa las variables continuas y hace que sea más fácil interpretar los resultados.
1 2 3 4 5 6 |
explainer = lime.lime_tabular.LimeTabularExplainer( X_train, feature_names=cancer.feature_names, class_names=cancer.target_names, discretize_continuous=True ) |
Ahora podemos tomar una observación (una medición de una paciente) y explicar porqué nuestro modelo SVM tomó la decisión de si es cáncer maligno o no. Dado que este artículo está escrito en un jupyter notebook podemos usar la función show_in_notebook
para que nos muestre una bonita tabla.
1 2 3 4 5 6 7 8 |
observation = X_test[1] exp = explainer.explain_instance( observation, svc.predict_proba, num_features=10, top_labels=1 ) exp.show_in_notebook(show_table=True, show_all=False) |
En esta predicción en particular, el modelo predijo que era benigno con un 59% de probabilidades, y esto fué debido sobretodo a las variables texture error, mean smoothness, y mean radius. Básicamente, el valor de las variables en color naranja apoyan el caso benigno y las variables en azul soportan el caso de cáncer maligno.
Partial Dependence Plots
Partial Dependence Plots (Gráficas de Dependencia Parcial) son similares a la técnica de Permutation importances, pero en vez de observar el impacto de la existencia de una variable en las predicciones, se concentra en analizar el impacto del valor de una variable respecto a las predicciones.
Scikit-learn’s tiene la función plot_partial_dependence
que crea este tipo de gráficos. Por algún motivo solo está implementado para algoritmos Gradient Boosting Trees
. Eventualmente estará disponible para otros estimadores, pero mientras tanto podemos usar el paquete pdpbox
para dibujar un gráfico de dependencias (pdp por sus siglas en inglés).
1 |
from pdpbox import pdp |
Usamos el dataset del cáncer de mama de nuevo.
1 2 |
cancer_df = pd.DataFrame(cancer.data, columns=cancer.feature_names) cancer_df["target"] = cancer.target |
Para usar PDP, tenemos que seleccionar una variable independiente a la vez, por ejemplo podemos seleccionar la variable mean radius, que mide el radio medio de todas las células cancerosas en la imagen.
1 |
cancer.feature_names[0] |
1 |
'mean radius' |
Podemos usar el mismo algoritmo SVM para este ejemplo.
1 |
svc = SVC( probability=True).fit(X_train, y_train) |
Ahora simplemente tenemos que usar la función pdp_isolate
para calcular el impacto de la variable mean texture en las predicciones del algoritmo.
1 2 3 4 5 6 7 |
pdp_mean_radius_svc = pdp.pdp_isolate( model=svc, dataset=cancer_df[cancer.feature_names], model_features=cancer.feature_names, feature='mean radius', num_grid_points=20 ) |
Básicamente esta función computa el valor de la variable objetivo en un rango de 20 valores distintos de la variable mean texture.
Ahora podemos usar la función pdp_plot
para ver el gráfico de dependencias en sí.
1 |
pdp.pdp_plot(pdp_mean_radius_svc, 'mean radius'); |

Vemos que hasta un valor de 20mm, conforme aumenta el radio medio de las células las predicciones van bajando, acercándose a la clase 0 que en este dataset codifica el cáncer maligno. Por lo tanto vemos que conforme aumenta el valor del radio medio de las células hay más probabilidades de que el cáncer sea maligno.
Bueno eso es todo y ¡hasta la próxima!