- Deprecated function: Use of "static" in callables is deprecated in Drupal\user\Entity\Role::postLoad() (line 172 of core/modules/user/src/Entity/Role.php).
Drupal\user\Entity\Role::postLoad(Object, Array) (Line: 423)
Drupal\Core\Entity\EntityStorageBase->postLoad(Array) (Line: 353)
Drupal\Core\Entity\EntityStorageBase->loadMultiple(Array) (Line: 16)
Drupal\user\RoleStorage->isPermissionInRoles('access site in maintenance mode', Array) (Line: 112)
Drupal\Core\Session\UserSession->hasPermission('access site in maintenance mode') (Line: 105)
Drupal\Core\Session\AccountProxy->hasPermission('access site in maintenance mode') (Line: 83)
Drupal\redirect\RedirectChecker->canRedirect(Object) (Line: 120)
Drupal\redirect\EventSubscriber\RedirectRequestSubscriber->onKernelRequestCheckRedirect(Object, 'kernel.request', Object)
call_user_func(Array, Object, 'kernel.request', Object) (Line: 142)
Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher->dispatch(Object, 'kernel.request') (Line: 145)
Symfony\Component\HttpKernel\HttpKernel->handleRaw(Object, 1) (Line: 81)
Symfony\Component\HttpKernel\HttpKernel->handle(Object, 1, 1) (Line: 58)
Drupal\Core\StackMiddleware\Session->handle(Object, 1, 1) (Line: 48)
Drupal\Core\StackMiddleware\KernelPreHandle->handle(Object, 1, 1) (Line: 191)
Drupal\page_cache\StackMiddleware\PageCache->fetch(Object, 1, 1) (Line: 128)
Drupal\page_cache\StackMiddleware\PageCache->lookup(Object, 1, 1) (Line: 82)
Drupal\page_cache\StackMiddleware\PageCache->handle(Object, 1, 1) (Line: 48)
Drupal\Core\StackMiddleware\ReverseProxyMiddleware->handle(Object, 1, 1) (Line: 51)
Drupal\Core\StackMiddleware\NegotiationMiddleware->handle(Object, 1, 1) (Line: 23)
Stack\StackedHttpKernel->handle(Object, 1, 1) (Line: 713)
Drupal\Core\DrupalKernel->handle(Object) (Line: 19)
- Deprecated function: Creation of dynamic property Drupal\mysql\Driver\Database\mysql\Select::$alterMetaData is deprecated in Drupal\Core\Database\Query\Select->addMetaData() (line 178 of core/lib/Drupal/Core/Database/Query/Select.php).
Drupal\Core\Database\Query\Select->addMetaData('entity_type', 'file') (Line: 115)
Drupal\Core\Entity\Query\Sql\Query->prepare() (Line: 80)
Drupal\Core\Entity\Query\Sql\Query->execute() (Line: 640)
Drupal\Core\Entity\EntityStorageBase->loadByProperties(Array) (Line: 63)
Drupal\Core\Entity\EntityRepository->loadEntityByUuid('file', '8ddd2985-c1a1-4b04-8418-d79f59caa1f6') (Line: 95)
Drupal\editor\Plugin\Filter\EditorFileReference->process('Para entender el concepto de <strong>curva ROC</strong> (<em>ROC curve</em>, <em>Receiver Operating Characteristic</em>) tenemos que entender previamente cómo funciona un algoritmo de clasificación lineal como la regresión logística: A la hora de realizar una predicción, el modelo asigna una puntuación (un "score") a cada muestra del conjunto de pruebas. Este score se compara con un cierto valor límite ("<em>threshold</em>"). Si, para una muestra, su score es mayor que el threshold, se asigna a la clase principal. Si es menor, se asigna a la clase secundaria.
Siguiendo con el ejemplo visto relativo al Titanic, ya habíamos realizado la predicción:
prediction = model.predict(X_test)
prediction
array([0, 0, 0, 1, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 0, 0, 1, 0, 1,
0, 0, 1, 1, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
1, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0,
1, 0, 1, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1,
1, 0], dtype=int64)
Podemos ver el score que el modelo ha asignado a cada muestra con el método <strong>decision_function</strong>:
scores = model.decision_function(X_test)
scores
array([-1.94301282, -2.36213546, -1.21444007, 2.47651727, 1.1959797 ,
-0.60290339, 2.29157802, 2.53608913, -0.41796414, 0.95146859,
-2.59631885, 1.41196199, -2.15257414, 1.6564731 , 2.71070066,
0.53234595, -2.36213546, -1.71670281, -2.36213546, 0.90989925,
-1.48251942, 2.29157802, -2.15257414, -0.60290339, 0.53234595,
2.47651727, -2.36213546, 0.53234595, 1.6564731 , -0.65922644,
-1.94301282, 1.41196199, -2.36213546, -0.60290339, -2.59631885,
-0.18378075, -2.36213546, -1.48251942, -1.48251942, -1.48251942,
-0.41796414, -1.94301282, -2.36213546, -1.42400139, 2.05739463,
-2.36213546, -2.36213546, 1.99782277, -1.48251942, -0.83708678,
-0.60290339, 0.06073036, 1.1777786 , -2.36213546, -0.18378075,
-2.19539732, -0.60290339, -1.13792094, -0.94530689, -2.86545203,
-1.94301282, 0.77685706, 2.47651727, -0.18378075, 0.77685706,
-2.15257414, 2.29157802, -1.23800831, 1.1777786 , 2.29157802,
1.1777786 , -0.83708678, -0.41796414, -2.36213546, -2.36213546,
0.22038959, -1.79447578, -0.18378075, -2.36213546, -1.06339678,
-2.38675753, 0.01208211, 1.1777786 , -2.36213546, -1.48251942,
2.29157802, 2.71070066, 0.53234595, 1.41196199, -0.41796414])
Por defecto, el threshold aplicado es 0. Podemos ver esto fácilmente viendo, del array anterior, qué valores son mayores que 0 y cuáles son menores. Hagamos esto solo para los 10 primeros valores del array anterior:
scores[:10] > 0
array([False, False, False, True, True, False, True, True, False, True])
...y comparemos este resultado con los 10 primeros valores de la predicción:
prediction[:10]
array([0, 0, 0, 1, 1, 0, 1, 1, 0, 1], dtype=int64)
Vemos que se ha predicho un resultado de 0 (el pasajero no sobrevive) para aquellas muestras (para aquellos pasajeros) cuyos scores son menores que cero.
Pues bien, este clasificador con el threshold por defecto, supone un cierto <em>True Positive Rate</em> y un cierto <em>False Positive Rate</em>. Calculémoslos:
confusion_matrix(prediction, y_test)
array([[47, 9],
[ 4, 30]], dtype=int64)
Si extraemos los valores:
tn, fp, fn, tp = confusion_matrix(prediction, y_test).flatten()
TPR = tp / (tp + fn)
TPR
0.8823529411764706
FPR = (fp / (fp + tn))
FPR
0.16071428571428573
Vamos a almacenar estos valores en sendas listas para poder acceder a ellos posteriormente.
TPRs = [TPR]
FPRs = [FPR]
Y aquí viene lo interesante: hemos dicho que se va a comparar cada score con el threshold. Si el valor es mayor que el threshold se asigna a la clase 1 (o principal), y si es menor, a la clase 0 (o secundaria). Veamos los scores, la predicción que estos scores suponen, y el "ground truth" (el valor real de la variable objetivo) añadiendo estos tres datos a un dataframe ordenando los scores de menor a mayor:
scores_df = pd.DataFrame({
"scores": scores,
"prediction": prediction,
"truth": y_test
})
scores_df.sort_values("scores", inplace = True)
Y mostremos ahora los scores que hay en torno al threshold de cero (se ha buscado manualmente su posición en el dataset):
scores_df.iloc[46:66]
Lógicamente, todas las predicciones correspondientes a scores mayores que 0 aparecen con una predicción de 1, y al revés: las predicciones correspondientes a scores menores que cero aparecen con una predicción de 0.
Vemos, sin embargo, que el verdadero valor de la variable objetivo ("truth") no coincide siempre con la predicción. Lo ideal sería que por encima del threshold solo hubiese muestras de la clase 1 (para tener un TPR de 1), así que ¿qué ocurriría si en lugar de un threshold de 0 considerásemos un threshold de 0.8? En la imagen anterior vemos que, moviendo el threshold hasta dicho valor, estaríamos pasando tres muestras cuyo valor verdadero es 0 al bloque de "predicción 0". Es decir, estaríamos mejorando el TPR de nuestro modelo (al disminuir el número de falsos negativos)... pero a costa de empeorar el FPR. Vamos a calcular estos valores. Para ello tenemos los scores, de forma que podemos realizar la predicción "a mano":
new_prediction = scores > 0.8
new_prediction
array([False, False, False, True, True, False, True, True, False,
True, False, True, False, True, True, False, False, False,
False, True, False, True, False, False, False, True, False,
False, True, False, False, True, False, False, False, False,
False, False, False, False, False, False, False, False, True,
False, False, True, False, False, False, False, True, False,
False, False, False, False, False, False, False, False, True,
False, False, False, True, False, True, True, True, False,
False, False, False, False, False, False, False, False, False,
False, True, False, False, True, True, False, True, False])
confusion_matrix(new_prediction, y_test)
array([[50, 15],
[ 1, 24]], dtype=int64)
tn, fp, fn, tp = confusion_matrix(new_prediction, y_test).flatten()
TPR = tp / (tp + fn)
TPR
0.96
FPR = (fp / (fp + tn))
FPR
0.23076923076923078
Tal y como sospechábamos, el TPR del modelo ha aumentado hasta el 96%, pero el FPR ha aumentado también (lo que significa que hay más falsos positivos que antes).
Almacenemos estos valores en las listas que habíamos creado:
TPRs.append(TPR)
FPRs.append(FPR)
Y, en el ejemplo visto, hemos aumentado el threshold hasta 0.8. Si -nuevamente, viendo los scores- disminuimos el threshold hasta -0.5, vemos que estaríamos "sacando" del bloque de muestras que recibe una predicción 0 varias muestras cuyo valor real es 1... pero también estaríamos moviendo varias muestras que ahora mismo están bien clasificadas. Es decir, estaríamos mejorando el FPR (disminuyendo el número de falsos positivos) a costa de empeorar el TPR:
new_prediction = scores > -0.5
new_prediction
array([False, False, False, True, True, False, True, True, True,
True, False, True, False, True, True, True, False, False,
False, True, False, True, False, False, True, True, False,
True, True, False, False, True, False, False, False, True,
False, False, False, False, True, False, False, False, True,
False, False, True, False, False, False, True, True, False,
True, False, False, False, False, False, False, True, True,
True, True, False, True, False, True, True, True, False,
True, False, False, True, False, True, False, False, False,
True, True, False, False, True, True, True, True, True])
confusion_matrix(new_prediction, y_test)
array([[43, 5],
[ 8, 34]], dtype=int64)
tn, fp, fn, tp = confusion_matrix(new_prediction, y_test).flatten()
TPR = tp / (tp + fn)
TPR
0.8095238095238095
FPR = (fp / (fp + tn))
FPR
0.10416666666666667
Volvamos a almacenar los valores calculados:
TPRs.append(TPR)
FPRs.append(FPR)
También ahora hemos confirmado nuestras sospechas: hemos mejorado el FPR (disminuyéndolo) pero a costa de empeorar el TPR.
Llevemos a la gráfica de TPR vs. FPR las tres parejas de valores encontrados:
t = ["0", "0.8", "-0.5"]
fig, ax = plt.subplots(figsize = (6, 6))
ax.set_xlabel("False Positive Rate", fontsize = 15)
ax.set_ylabel("True Positive Rate", fontsize = 15)
ax.set_xticks(np.linspace(0, 1, 11))
ax.set_yticks(np.linspace(0, 1, 11))
ax.set_xlim(0, 1)
ax.set_ylim(0, 1)
sns.scatterplot(x = FPRs, y = TPRs, s = 50);
for i in range(len(t)):
plt.text(FPRs[i] + 0.02, TPRs[i] - 0.03, t[i], color = "red", fontsize = 15)
...gráfica que muestra exactamente las variaciones que hemos visto.
Hemos probado tres valores del threshold. Vamos a probar todos los thresholds desde el valor mínimo del score hasta el valor máximo, a ver cuál es el resultado:
TPRs = []
FPRs = []
for threshold in np.sort(scores):
new_prediction = scores > threshold
tn, fp, fn, tp = confusion_matrix(y_test, new_prediction).flatten()
TPR = tp / (tp + fn)
FPR = fp / (fp + tn)
TPRs.append(TPR)
FPRs.append(FPR)
Ahora ya tenemos los TPR y FPR en las listas TPRs y FPRs. Mostrémoslas en una gráfica etiquetando cada punto con el threshold al que se corresponde:
fig, ax = plt.subplots(figsize = (10, 10))
ax.set_xlabel("False Positive Rate", fontsize = 15)
ax.set_ylabel("True Positive Rate", fontsize = 15)
ax.set_xticks(np.linspace(0, 1, 11))
ax.set_yticks(np.linspace(0, 1, 11))
sns.scatterplot(x = FPRs, y = TPRs, s = 40);
plt.plot([0, 1], [0, 1], 'k--')
for i in range(len(scores)):
score_i = np.sort(scores)[i]
if score_i > -0.65:
ax.text(FPRs[i] + 0.012, TPRs[i] - 0.004, round(score_i, 2), color = "red", fontsize = 12)
else:
if i % 4 == 0:
ax.text(FPRs[i] - 0.01, TPRs[i] - 0.03, round(score_i, 2), color = "red", fontsize = 12)
Lo que estamos viendo es exactamente la curva ROC del clasificador. Y nos da bastante información: en primer lugar nos dice que aumentando o disminuyendo lo suficiente el threshold podemos conseguir que nuestro algoritmo de clasificación alcance un TPR o un FPR casi perfecto. El único "pero" es que lo conseguiremos a costa de empeorar la otra métrica. Esto significa que, una vez creado el modelo, podemos configurarlo en función de nuestros intereses fijando un threshold u otro (para conseguir un mayor TPR o un FPR). Por otro lado, cuanto más se aproxime la gráfica al punto (0, 1), más se comportará como un clasificador ideal. En la gráfica anterior se muestra la línea diagonal que marca el comportamiento de los clasificadores que tienen tantos verdaderos positivos como falsos negativos, y tantos verdaderos negativos como falsos positivos.
', 'es') (Line: 118)
Drupal\filter\Element\ProcessedText::preRenderText(Array)
call_user_func_array(Array, Array) (Line: 101)
Drupal\Core\Render\Renderer->doTrustedCallback(Array, Array, 'Render #pre_render callbacks must be methods of a class that implements \Drupal\Core\Security\TrustedCallbackInterface or be an anonymous function. The callback was %s. See https://www.drupal.org/node/2966725', 'exception', 'Drupal\Core\Render\Element\RenderCallbackInterface') (Line: 788)
Drupal\Core\Render\Renderer->doCallback('#pre_render', Array, Array) (Line: 374)
Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204)
Drupal\Core\Render\Renderer->render(Array) (Line: 479)
Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 88)
__TwigTemplate_a7d6005c89ae729617b9a0c2bccb1776->doDisplay(Array, Array) (Line: 405)
Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378)
Twig\Template->display(Array, Array) (Line: 46)
__TwigTemplate_804f7948456cfe20e11a34c43439c7c2->doDisplay(Array, Array) (Line: 405)
Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378)
Twig\Template->display(Array, Array) (Line: 43)
__TwigTemplate_bd990293b89f3b78c69fe0ee2f7828b5->doDisplay(Array, Array) (Line: 405)
Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378)
Twig\Template->display(Array) (Line: 390)
Twig\Template->render(Array) (Line: 55)
twig_render_template('themes/contrib/classy/templates/field/field--text-with-summary.html.twig', Array) (Line: 384)
Drupal\Core\Theme\ThemeManager->render('field', Array) (Line: 433)
Drupal\Core\Render\Renderer->doRender(Array) (Line: 446)
Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204)
Drupal\Core\Render\Renderer->render(Array) (Line: 479)
Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 74)
__TwigTemplate_94047fbdba6937b76a4479dfa1763452->doDisplay(Array, Array) (Line: 405)
Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378)
Twig\Template->display(Array) (Line: 390)
Twig\Template->render(Array) (Line: 55)
twig_render_template('themes/custom/yg_aesthetic/templates/node.html.twig', Array) (Line: 384)
Drupal\Core\Theme\ThemeManager->render('node', Array) (Line: 433)
Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204)
Drupal\Core\Render\Renderer->render(Array, ) (Line: 242)
Drupal\Core\Render\MainContent\HtmlRenderer->Drupal\Core\Render\MainContent\{closure}() (Line: 580)
Drupal\Core\Render\Renderer->executeInRenderContext(Object, Object) (Line: 235)
Drupal\Core\Render\MainContent\HtmlRenderer->prepare(Array, Object, Object) (Line: 132)
Drupal\Core\Render\MainContent\HtmlRenderer->renderResponse(Array, Object, Object) (Line: 90)
Drupal\Core\EventSubscriber\MainContentViewSubscriber->onViewRenderArray(Object, 'kernel.view', Object)
call_user_func(Array, Object, 'kernel.view', Object) (Line: 142)
Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher->dispatch(Object, 'kernel.view') (Line: 174)
Symfony\Component\HttpKernel\HttpKernel->handleRaw(Object, 1) (Line: 81)
Symfony\Component\HttpKernel\HttpKernel->handle(Object, 1, 1) (Line: 58)
Drupal\Core\StackMiddleware\Session->handle(Object, 1, 1) (Line: 48)
Drupal\Core\StackMiddleware\KernelPreHandle->handle(Object, 1, 1) (Line: 191)
Drupal\page_cache\StackMiddleware\PageCache->fetch(Object, 1, 1) (Line: 128)
Drupal\page_cache\StackMiddleware\PageCache->lookup(Object, 1, 1) (Line: 82)
Drupal\page_cache\StackMiddleware\PageCache->handle(Object, 1, 1) (Line: 48)
Drupal\Core\StackMiddleware\ReverseProxyMiddleware->handle(Object, 1, 1) (Line: 51)
Drupal\Core\StackMiddleware\NegotiationMiddleware->handle(Object, 1, 1) (Line: 23)
Stack\StackedHttpKernel->handle(Object, 1, 1) (Line: 713)
Drupal\Core\DrupalKernel->handle(Object) (Line: 19)
- Deprecated function: Creation of dynamic property Drupal\mysql\Driver\Database\mysql\Select::$alterTags is deprecated in Drupal\Core\Database\Query\Select->addTag() (line 149 of core/lib/Drupal/Core/Database/Query/Select.php).
Drupal\Core\Database\Query\Select->addTag('entity_query') (Line: 147)
Drupal\Core\Entity\Query\Sql\Query->prepare() (Line: 80)
Drupal\Core\Entity\Query\Sql\Query->execute() (Line: 640)
Drupal\Core\Entity\EntityStorageBase->loadByProperties(Array) (Line: 63)
Drupal\Core\Entity\EntityRepository->loadEntityByUuid('file', '8ddd2985-c1a1-4b04-8418-d79f59caa1f6') (Line: 95)
Drupal\editor\Plugin\Filter\EditorFileReference->process('Para entender el concepto de <strong>curva ROC</strong> (<em>ROC curve</em>, <em>Receiver Operating Characteristic</em>) tenemos que entender previamente cómo funciona un algoritmo de clasificación lineal como la regresión logística: A la hora de realizar una predicción, el modelo asigna una puntuación (un "score") a cada muestra del conjunto de pruebas. Este score se compara con un cierto valor límite ("<em>threshold</em>"). Si, para una muestra, su score es mayor que el threshold, se asigna a la clase principal. Si es menor, se asigna a la clase secundaria.
Siguiendo con el ejemplo visto relativo al Titanic, ya habíamos realizado la predicción:
prediction = model.predict(X_test)
prediction
array([0, 0, 0, 1, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 0, 0, 1, 0, 1,
0, 0, 1, 1, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
1, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0,
1, 0, 1, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1,
1, 0], dtype=int64)
Podemos ver el score que el modelo ha asignado a cada muestra con el método <strong>decision_function</strong>:
scores = model.decision_function(X_test)
scores
array([-1.94301282, -2.36213546, -1.21444007, 2.47651727, 1.1959797 ,
-0.60290339, 2.29157802, 2.53608913, -0.41796414, 0.95146859,
-2.59631885, 1.41196199, -2.15257414, 1.6564731 , 2.71070066,
0.53234595, -2.36213546, -1.71670281, -2.36213546, 0.90989925,
-1.48251942, 2.29157802, -2.15257414, -0.60290339, 0.53234595,
2.47651727, -2.36213546, 0.53234595, 1.6564731 , -0.65922644,
-1.94301282, 1.41196199, -2.36213546, -0.60290339, -2.59631885,
-0.18378075, -2.36213546, -1.48251942, -1.48251942, -1.48251942,
-0.41796414, -1.94301282, -2.36213546, -1.42400139, 2.05739463,
-2.36213546, -2.36213546, 1.99782277, -1.48251942, -0.83708678,
-0.60290339, 0.06073036, 1.1777786 , -2.36213546, -0.18378075,
-2.19539732, -0.60290339, -1.13792094, -0.94530689, -2.86545203,
-1.94301282, 0.77685706, 2.47651727, -0.18378075, 0.77685706,
-2.15257414, 2.29157802, -1.23800831, 1.1777786 , 2.29157802,
1.1777786 , -0.83708678, -0.41796414, -2.36213546, -2.36213546,
0.22038959, -1.79447578, -0.18378075, -2.36213546, -1.06339678,
-2.38675753, 0.01208211, 1.1777786 , -2.36213546, -1.48251942,
2.29157802, 2.71070066, 0.53234595, 1.41196199, -0.41796414])
Por defecto, el threshold aplicado es 0. Podemos ver esto fácilmente viendo, del array anterior, qué valores son mayores que 0 y cuáles son menores. Hagamos esto solo para los 10 primeros valores del array anterior:
scores[:10] > 0
array([False, False, False, True, True, False, True, True, False, True])
...y comparemos este resultado con los 10 primeros valores de la predicción:
prediction[:10]
array([0, 0, 0, 1, 1, 0, 1, 1, 0, 1], dtype=int64)
Vemos que se ha predicho un resultado de 0 (el pasajero no sobrevive) para aquellas muestras (para aquellos pasajeros) cuyos scores son menores que cero.
Pues bien, este clasificador con el threshold por defecto, supone un cierto <em>True Positive Rate</em> y un cierto <em>False Positive Rate</em>. Calculémoslos:
confusion_matrix(prediction, y_test)
array([[47, 9],
[ 4, 30]], dtype=int64)
Si extraemos los valores:
tn, fp, fn, tp = confusion_matrix(prediction, y_test).flatten()
TPR = tp / (tp + fn)
TPR
0.8823529411764706
FPR = (fp / (fp + tn))
FPR
0.16071428571428573
Vamos a almacenar estos valores en sendas listas para poder acceder a ellos posteriormente.
TPRs = [TPR]
FPRs = [FPR]
Y aquí viene lo interesante: hemos dicho que se va a comparar cada score con el threshold. Si el valor es mayor que el threshold se asigna a la clase 1 (o principal), y si es menor, a la clase 0 (o secundaria). Veamos los scores, la predicción que estos scores suponen, y el "ground truth" (el valor real de la variable objetivo) añadiendo estos tres datos a un dataframe ordenando los scores de menor a mayor:
scores_df = pd.DataFrame({
"scores": scores,
"prediction": prediction,
"truth": y_test
})
scores_df.sort_values("scores", inplace = True)
Y mostremos ahora los scores que hay en torno al threshold de cero (se ha buscado manualmente su posición en el dataset):
scores_df.iloc[46:66]
Lógicamente, todas las predicciones correspondientes a scores mayores que 0 aparecen con una predicción de 1, y al revés: las predicciones correspondientes a scores menores que cero aparecen con una predicción de 0.
Vemos, sin embargo, que el verdadero valor de la variable objetivo ("truth") no coincide siempre con la predicción. Lo ideal sería que por encima del threshold solo hubiese muestras de la clase 1 (para tener un TPR de 1), así que ¿qué ocurriría si en lugar de un threshold de 0 considerásemos un threshold de 0.8? En la imagen anterior vemos que, moviendo el threshold hasta dicho valor, estaríamos pasando tres muestras cuyo valor verdadero es 0 al bloque de "predicción 0". Es decir, estaríamos mejorando el TPR de nuestro modelo (al disminuir el número de falsos negativos)... pero a costa de empeorar el FPR. Vamos a calcular estos valores. Para ello tenemos los scores, de forma que podemos realizar la predicción "a mano":
new_prediction = scores > 0.8
new_prediction
array([False, False, False, True, True, False, True, True, False,
True, False, True, False, True, True, False, False, False,
False, True, False, True, False, False, False, True, False,
False, True, False, False, True, False, False, False, False,
False, False, False, False, False, False, False, False, True,
False, False, True, False, False, False, False, True, False,
False, False, False, False, False, False, False, False, True,
False, False, False, True, False, True, True, True, False,
False, False, False, False, False, False, False, False, False,
False, True, False, False, True, True, False, True, False])
confusion_matrix(new_prediction, y_test)
array([[50, 15],
[ 1, 24]], dtype=int64)
tn, fp, fn, tp = confusion_matrix(new_prediction, y_test).flatten()
TPR = tp / (tp + fn)
TPR
0.96
FPR = (fp / (fp + tn))
FPR
0.23076923076923078
Tal y como sospechábamos, el TPR del modelo ha aumentado hasta el 96%, pero el FPR ha aumentado también (lo que significa que hay más falsos positivos que antes).
Almacenemos estos valores en las listas que habíamos creado:
TPRs.append(TPR)
FPRs.append(FPR)
Y, en el ejemplo visto, hemos aumentado el threshold hasta 0.8. Si -nuevamente, viendo los scores- disminuimos el threshold hasta -0.5, vemos que estaríamos "sacando" del bloque de muestras que recibe una predicción 0 varias muestras cuyo valor real es 1... pero también estaríamos moviendo varias muestras que ahora mismo están bien clasificadas. Es decir, estaríamos mejorando el FPR (disminuyendo el número de falsos positivos) a costa de empeorar el TPR:
new_prediction = scores > -0.5
new_prediction
array([False, False, False, True, True, False, True, True, True,
True, False, True, False, True, True, True, False, False,
False, True, False, True, False, False, True, True, False,
True, True, False, False, True, False, False, False, True,
False, False, False, False, True, False, False, False, True,
False, False, True, False, False, False, True, True, False,
True, False, False, False, False, False, False, True, True,
True, True, False, True, False, True, True, True, False,
True, False, False, True, False, True, False, False, False,
True, True, False, False, True, True, True, True, True])
confusion_matrix(new_prediction, y_test)
array([[43, 5],
[ 8, 34]], dtype=int64)
tn, fp, fn, tp = confusion_matrix(new_prediction, y_test).flatten()
TPR = tp / (tp + fn)
TPR
0.8095238095238095
FPR = (fp / (fp + tn))
FPR
0.10416666666666667
Volvamos a almacenar los valores calculados:
TPRs.append(TPR)
FPRs.append(FPR)
También ahora hemos confirmado nuestras sospechas: hemos mejorado el FPR (disminuyéndolo) pero a costa de empeorar el TPR.
Llevemos a la gráfica de TPR vs. FPR las tres parejas de valores encontrados:
t = ["0", "0.8", "-0.5"]
fig, ax = plt.subplots(figsize = (6, 6))
ax.set_xlabel("False Positive Rate", fontsize = 15)
ax.set_ylabel("True Positive Rate", fontsize = 15)
ax.set_xticks(np.linspace(0, 1, 11))
ax.set_yticks(np.linspace(0, 1, 11))
ax.set_xlim(0, 1)
ax.set_ylim(0, 1)
sns.scatterplot(x = FPRs, y = TPRs, s = 50);
for i in range(len(t)):
plt.text(FPRs[i] + 0.02, TPRs[i] - 0.03, t[i], color = "red", fontsize = 15)
...gráfica que muestra exactamente las variaciones que hemos visto.
Hemos probado tres valores del threshold. Vamos a probar todos los thresholds desde el valor mínimo del score hasta el valor máximo, a ver cuál es el resultado:
TPRs = []
FPRs = []
for threshold in np.sort(scores):
new_prediction = scores > threshold
tn, fp, fn, tp = confusion_matrix(y_test, new_prediction).flatten()
TPR = tp / (tp + fn)
FPR = fp / (fp + tn)
TPRs.append(TPR)
FPRs.append(FPR)
Ahora ya tenemos los TPR y FPR en las listas TPRs y FPRs. Mostrémoslas en una gráfica etiquetando cada punto con el threshold al que se corresponde:
fig, ax = plt.subplots(figsize = (10, 10))
ax.set_xlabel("False Positive Rate", fontsize = 15)
ax.set_ylabel("True Positive Rate", fontsize = 15)
ax.set_xticks(np.linspace(0, 1, 11))
ax.set_yticks(np.linspace(0, 1, 11))
sns.scatterplot(x = FPRs, y = TPRs, s = 40);
plt.plot([0, 1], [0, 1], 'k--')
for i in range(len(scores)):
score_i = np.sort(scores)[i]
if score_i > -0.65:
ax.text(FPRs[i] + 0.012, TPRs[i] - 0.004, round(score_i, 2), color = "red", fontsize = 12)
else:
if i % 4 == 0:
ax.text(FPRs[i] - 0.01, TPRs[i] - 0.03, round(score_i, 2), color = "red", fontsize = 12)
Lo que estamos viendo es exactamente la curva ROC del clasificador. Y nos da bastante información: en primer lugar nos dice que aumentando o disminuyendo lo suficiente el threshold podemos conseguir que nuestro algoritmo de clasificación alcance un TPR o un FPR casi perfecto. El único "pero" es que lo conseguiremos a costa de empeorar la otra métrica. Esto significa que, una vez creado el modelo, podemos configurarlo en función de nuestros intereses fijando un threshold u otro (para conseguir un mayor TPR o un FPR). Por otro lado, cuanto más se aproxime la gráfica al punto (0, 1), más se comportará como un clasificador ideal. En la gráfica anterior se muestra la línea diagonal que marca el comportamiento de los clasificadores que tienen tantos verdaderos positivos como falsos negativos, y tantos verdaderos negativos como falsos positivos.
', 'es') (Line: 118)
Drupal\filter\Element\ProcessedText::preRenderText(Array)
call_user_func_array(Array, Array) (Line: 101)
Drupal\Core\Render\Renderer->doTrustedCallback(Array, Array, 'Render #pre_render callbacks must be methods of a class that implements \Drupal\Core\Security\TrustedCallbackInterface or be an anonymous function. The callback was %s. See https://www.drupal.org/node/2966725', 'exception', 'Drupal\Core\Render\Element\RenderCallbackInterface') (Line: 788)
Drupal\Core\Render\Renderer->doCallback('#pre_render', Array, Array) (Line: 374)
Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204)
Drupal\Core\Render\Renderer->render(Array) (Line: 479)
Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 88)
__TwigTemplate_a7d6005c89ae729617b9a0c2bccb1776->doDisplay(Array, Array) (Line: 405)
Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378)
Twig\Template->display(Array, Array) (Line: 46)
__TwigTemplate_804f7948456cfe20e11a34c43439c7c2->doDisplay(Array, Array) (Line: 405)
Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378)
Twig\Template->display(Array, Array) (Line: 43)
__TwigTemplate_bd990293b89f3b78c69fe0ee2f7828b5->doDisplay(Array, Array) (Line: 405)
Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378)
Twig\Template->display(Array) (Line: 390)
Twig\Template->render(Array) (Line: 55)
twig_render_template('themes/contrib/classy/templates/field/field--text-with-summary.html.twig', Array) (Line: 384)
Drupal\Core\Theme\ThemeManager->render('field', Array) (Line: 433)
Drupal\Core\Render\Renderer->doRender(Array) (Line: 446)
Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204)
Drupal\Core\Render\Renderer->render(Array) (Line: 479)
Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 74)
__TwigTemplate_94047fbdba6937b76a4479dfa1763452->doDisplay(Array, Array) (Line: 405)
Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378)
Twig\Template->display(Array) (Line: 390)
Twig\Template->render(Array) (Line: 55)
twig_render_template('themes/custom/yg_aesthetic/templates/node.html.twig', Array) (Line: 384)
Drupal\Core\Theme\ThemeManager->render('node', Array) (Line: 433)
Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204)
Drupal\Core\Render\Renderer->render(Array, ) (Line: 242)
Drupal\Core\Render\MainContent\HtmlRenderer->Drupal\Core\Render\MainContent\{closure}() (Line: 580)
Drupal\Core\Render\Renderer->executeInRenderContext(Object, Object) (Line: 235)
Drupal\Core\Render\MainContent\HtmlRenderer->prepare(Array, Object, Object) (Line: 132)
Drupal\Core\Render\MainContent\HtmlRenderer->renderResponse(Array, Object, Object) (Line: 90)
Drupal\Core\EventSubscriber\MainContentViewSubscriber->onViewRenderArray(Object, 'kernel.view', Object)
call_user_func(Array, Object, 'kernel.view', Object) (Line: 142)
Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher->dispatch(Object, 'kernel.view') (Line: 174)
Symfony\Component\HttpKernel\HttpKernel->handleRaw(Object, 1) (Line: 81)
Symfony\Component\HttpKernel\HttpKernel->handle(Object, 1, 1) (Line: 58)
Drupal\Core\StackMiddleware\Session->handle(Object, 1, 1) (Line: 48)
Drupal\Core\StackMiddleware\KernelPreHandle->handle(Object, 1, 1) (Line: 191)
Drupal\page_cache\StackMiddleware\PageCache->fetch(Object, 1, 1) (Line: 128)
Drupal\page_cache\StackMiddleware\PageCache->lookup(Object, 1, 1) (Line: 82)
Drupal\page_cache\StackMiddleware\PageCache->handle(Object, 1, 1) (Line: 48)
Drupal\Core\StackMiddleware\ReverseProxyMiddleware->handle(Object, 1, 1) (Line: 51)
Drupal\Core\StackMiddleware\NegotiationMiddleware->handle(Object, 1, 1) (Line: 23)
Stack\StackedHttpKernel->handle(Object, 1, 1) (Line: 713)
Drupal\Core\DrupalKernel->handle(Object) (Line: 19)
- Deprecated function: Creation of dynamic property Drupal\mysql\Driver\Database\mysql\Select::$alterMetaData is deprecated in Drupal\Core\Database\Query\Select->addMetaData() (line 178 of core/lib/Drupal/Core/Database/Query/Select.php).
Drupal\Core\Database\Query\Select->addMetaData('entity_type', 'file') (Line: 115)
Drupal\Core\Entity\Query\Sql\Query->prepare() (Line: 80)
Drupal\Core\Entity\Query\Sql\Query->execute() (Line: 640)
Drupal\Core\Entity\EntityStorageBase->loadByProperties(Array) (Line: 63)
Drupal\Core\Entity\EntityRepository->loadEntityByUuid('file', '8ddd2985-c1a1-4b04-8418-d79f59caa1f6') (Line: 124)
Drupal\editor\Plugin\Filter\EditorFileReference->process('Para entender el concepto de <strong>curva ROC</strong> (<em>ROC curve</em>, <em>Receiver Operating Characteristic</em>) tenemos que entender previamente cómo funciona un algoritmo de clasificación lineal como la regresión logística: A la hora de realizar una predicción, el modelo asigna una puntuación (un "score") a cada muestra del conjunto de pruebas. Este score se compara con un cierto valor límite ("<em>threshold</em>"). Si, para una muestra, su score es mayor que el threshold, se asigna a la clase principal. Si es menor, se asigna a la clase secundaria.
Siguiendo con el ejemplo visto relativo al Titanic, ya habíamos realizado la predicción:
prediction = model.predict(X_test)
prediction
array([0, 0, 0, 1, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 0, 0, 1, 0, 1,
0, 0, 1, 1, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
1, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0,
1, 0, 1, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1,
1, 0], dtype=int64)
Podemos ver el score que el modelo ha asignado a cada muestra con el método <strong>decision_function</strong>:
scores = model.decision_function(X_test)
scores
array([-1.94301282, -2.36213546, -1.21444007, 2.47651727, 1.1959797 ,
-0.60290339, 2.29157802, 2.53608913, -0.41796414, 0.95146859,
-2.59631885, 1.41196199, -2.15257414, 1.6564731 , 2.71070066,
0.53234595, -2.36213546, -1.71670281, -2.36213546, 0.90989925,
-1.48251942, 2.29157802, -2.15257414, -0.60290339, 0.53234595,
2.47651727, -2.36213546, 0.53234595, 1.6564731 , -0.65922644,
-1.94301282, 1.41196199, -2.36213546, -0.60290339, -2.59631885,
-0.18378075, -2.36213546, -1.48251942, -1.48251942, -1.48251942,
-0.41796414, -1.94301282, -2.36213546, -1.42400139, 2.05739463,
-2.36213546, -2.36213546, 1.99782277, -1.48251942, -0.83708678,
-0.60290339, 0.06073036, 1.1777786 , -2.36213546, -0.18378075,
-2.19539732, -0.60290339, -1.13792094, -0.94530689, -2.86545203,
-1.94301282, 0.77685706, 2.47651727, -0.18378075, 0.77685706,
-2.15257414, 2.29157802, -1.23800831, 1.1777786 , 2.29157802,
1.1777786 , -0.83708678, -0.41796414, -2.36213546, -2.36213546,
0.22038959, -1.79447578, -0.18378075, -2.36213546, -1.06339678,
-2.38675753, 0.01208211, 1.1777786 , -2.36213546, -1.48251942,
2.29157802, 2.71070066, 0.53234595, 1.41196199, -0.41796414])
Por defecto, el threshold aplicado es 0. Podemos ver esto fácilmente viendo, del array anterior, qué valores son mayores que 0 y cuáles son menores. Hagamos esto solo para los 10 primeros valores del array anterior:
scores[:10] > 0
array([False, False, False, True, True, False, True, True, False, True])
...y comparemos este resultado con los 10 primeros valores de la predicción:
prediction[:10]
array([0, 0, 0, 1, 1, 0, 1, 1, 0, 1], dtype=int64)
Vemos que se ha predicho un resultado de 0 (el pasajero no sobrevive) para aquellas muestras (para aquellos pasajeros) cuyos scores son menores que cero.
Pues bien, este clasificador con el threshold por defecto, supone un cierto <em>True Positive Rate</em> y un cierto <em>False Positive Rate</em>. Calculémoslos:
confusion_matrix(prediction, y_test)
array([[47, 9],
[ 4, 30]], dtype=int64)
Si extraemos los valores:
tn, fp, fn, tp = confusion_matrix(prediction, y_test).flatten()
TPR = tp / (tp + fn)
TPR
0.8823529411764706
FPR = (fp / (fp + tn))
FPR
0.16071428571428573
Vamos a almacenar estos valores en sendas listas para poder acceder a ellos posteriormente.
TPRs = [TPR]
FPRs = [FPR]
Y aquí viene lo interesante: hemos dicho que se va a comparar cada score con el threshold. Si el valor es mayor que el threshold se asigna a la clase 1 (o principal), y si es menor, a la clase 0 (o secundaria). Veamos los scores, la predicción que estos scores suponen, y el "ground truth" (el valor real de la variable objetivo) añadiendo estos tres datos a un dataframe ordenando los scores de menor a mayor:
scores_df = pd.DataFrame({
"scores": scores,
"prediction": prediction,
"truth": y_test
})
scores_df.sort_values("scores", inplace = True)
Y mostremos ahora los scores que hay en torno al threshold de cero (se ha buscado manualmente su posición en el dataset):
scores_df.iloc[46:66]
Lógicamente, todas las predicciones correspondientes a scores mayores que 0 aparecen con una predicción de 1, y al revés: las predicciones correspondientes a scores menores que cero aparecen con una predicción de 0.
Vemos, sin embargo, que el verdadero valor de la variable objetivo ("truth") no coincide siempre con la predicción. Lo ideal sería que por encima del threshold solo hubiese muestras de la clase 1 (para tener un TPR de 1), así que ¿qué ocurriría si en lugar de un threshold de 0 considerásemos un threshold de 0.8? En la imagen anterior vemos que, moviendo el threshold hasta dicho valor, estaríamos pasando tres muestras cuyo valor verdadero es 0 al bloque de "predicción 0". Es decir, estaríamos mejorando el TPR de nuestro modelo (al disminuir el número de falsos negativos)... pero a costa de empeorar el FPR. Vamos a calcular estos valores. Para ello tenemos los scores, de forma que podemos realizar la predicción "a mano":
new_prediction = scores > 0.8
new_prediction
array([False, False, False, True, True, False, True, True, False,
True, False, True, False, True, True, False, False, False,
False, True, False, True, False, False, False, True, False,
False, True, False, False, True, False, False, False, False,
False, False, False, False, False, False, False, False, True,
False, False, True, False, False, False, False, True, False,
False, False, False, False, False, False, False, False, True,
False, False, False, True, False, True, True, True, False,
False, False, False, False, False, False, False, False, False,
False, True, False, False, True, True, False, True, False])
confusion_matrix(new_prediction, y_test)
array([[50, 15],
[ 1, 24]], dtype=int64)
tn, fp, fn, tp = confusion_matrix(new_prediction, y_test).flatten()
TPR = tp / (tp + fn)
TPR
0.96
FPR = (fp / (fp + tn))
FPR
0.23076923076923078
Tal y como sospechábamos, el TPR del modelo ha aumentado hasta el 96%, pero el FPR ha aumentado también (lo que significa que hay más falsos positivos que antes).
Almacenemos estos valores en las listas que habíamos creado:
TPRs.append(TPR)
FPRs.append(FPR)
Y, en el ejemplo visto, hemos aumentado el threshold hasta 0.8. Si -nuevamente, viendo los scores- disminuimos el threshold hasta -0.5, vemos que estaríamos "sacando" del bloque de muestras que recibe una predicción 0 varias muestras cuyo valor real es 1... pero también estaríamos moviendo varias muestras que ahora mismo están bien clasificadas. Es decir, estaríamos mejorando el FPR (disminuyendo el número de falsos positivos) a costa de empeorar el TPR:
new_prediction = scores > -0.5
new_prediction
array([False, False, False, True, True, False, True, True, True,
True, False, True, False, True, True, True, False, False,
False, True, False, True, False, False, True, True, False,
True, True, False, False, True, False, False, False, True,
False, False, False, False, True, False, False, False, True,
False, False, True, False, False, False, True, True, False,
True, False, False, False, False, False, False, True, True,
True, True, False, True, False, True, True, True, False,
True, False, False, True, False, True, False, False, False,
True, True, False, False, True, True, True, True, True])
confusion_matrix(new_prediction, y_test)
array([[43, 5],
[ 8, 34]], dtype=int64)
tn, fp, fn, tp = confusion_matrix(new_prediction, y_test).flatten()
TPR = tp / (tp + fn)
TPR
0.8095238095238095
FPR = (fp / (fp + tn))
FPR
0.10416666666666667
Volvamos a almacenar los valores calculados:
TPRs.append(TPR)
FPRs.append(FPR)
También ahora hemos confirmado nuestras sospechas: hemos mejorado el FPR (disminuyéndolo) pero a costa de empeorar el TPR.
Llevemos a la gráfica de TPR vs. FPR las tres parejas de valores encontrados:
t = ["0", "0.8", "-0.5"]
fig, ax = plt.subplots(figsize = (6, 6))
ax.set_xlabel("False Positive Rate", fontsize = 15)
ax.set_ylabel("True Positive Rate", fontsize = 15)
ax.set_xticks(np.linspace(0, 1, 11))
ax.set_yticks(np.linspace(0, 1, 11))
ax.set_xlim(0, 1)
ax.set_ylim(0, 1)
sns.scatterplot(x = FPRs, y = TPRs, s = 50);
for i in range(len(t)):
plt.text(FPRs[i] + 0.02, TPRs[i] - 0.03, t[i], color = "red", fontsize = 15)
...gráfica que muestra exactamente las variaciones que hemos visto.
Hemos probado tres valores del threshold. Vamos a probar todos los thresholds desde el valor mínimo del score hasta el valor máximo, a ver cuál es el resultado:
TPRs = []
FPRs = []
for threshold in np.sort(scores):
new_prediction = scores > threshold
tn, fp, fn, tp = confusion_matrix(y_test, new_prediction).flatten()
TPR = tp / (tp + fn)
FPR = fp / (fp + tn)
TPRs.append(TPR)
FPRs.append(FPR)
Ahora ya tenemos los TPR y FPR en las listas TPRs y FPRs. Mostrémoslas en una gráfica etiquetando cada punto con el threshold al que se corresponde:
fig, ax = plt.subplots(figsize = (10, 10))
ax.set_xlabel("False Positive Rate", fontsize = 15)
ax.set_ylabel("True Positive Rate", fontsize = 15)
ax.set_xticks(np.linspace(0, 1, 11))
ax.set_yticks(np.linspace(0, 1, 11))
sns.scatterplot(x = FPRs, y = TPRs, s = 40);
plt.plot([0, 1], [0, 1], 'k--')
for i in range(len(scores)):
score_i = np.sort(scores)[i]
if score_i > -0.65:
ax.text(FPRs[i] + 0.012, TPRs[i] - 0.004, round(score_i, 2), color = "red", fontsize = 12)
else:
if i % 4 == 0:
ax.text(FPRs[i] - 0.01, TPRs[i] - 0.03, round(score_i, 2), color = "red", fontsize = 12)
Lo que estamos viendo es exactamente la curva ROC del clasificador. Y nos da bastante información: en primer lugar nos dice que aumentando o disminuyendo lo suficiente el threshold podemos conseguir que nuestro algoritmo de clasificación alcance un TPR o un FPR casi perfecto. El único "pero" es que lo conseguiremos a costa de empeorar la otra métrica. Esto significa que, una vez creado el modelo, podemos configurarlo en función de nuestros intereses fijando un threshold u otro (para conseguir un mayor TPR o un FPR). Por otro lado, cuanto más se aproxime la gráfica al punto (0, 1), más se comportará como un clasificador ideal. En la gráfica anterior se muestra la línea diagonal que marca el comportamiento de los clasificadores que tienen tantos verdaderos positivos como falsos negativos, y tantos verdaderos negativos como falsos positivos.
', 'es') (Line: 118)
Drupal\filter\Element\ProcessedText::preRenderText(Array)
call_user_func_array(Array, Array) (Line: 101)
Drupal\Core\Render\Renderer->doTrustedCallback(Array, Array, 'Render #pre_render callbacks must be methods of a class that implements \Drupal\Core\Security\TrustedCallbackInterface or be an anonymous function. The callback was %s. See https://www.drupal.org/node/2966725', 'exception', 'Drupal\Core\Render\Element\RenderCallbackInterface') (Line: 788)
Drupal\Core\Render\Renderer->doCallback('#pre_render', Array, Array) (Line: 374)
Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204)
Drupal\Core\Render\Renderer->render(Array) (Line: 479)
Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 88)
__TwigTemplate_a7d6005c89ae729617b9a0c2bccb1776->doDisplay(Array, Array) (Line: 405)
Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378)
Twig\Template->display(Array, Array) (Line: 46)
__TwigTemplate_804f7948456cfe20e11a34c43439c7c2->doDisplay(Array, Array) (Line: 405)
Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378)
Twig\Template->display(Array, Array) (Line: 43)
__TwigTemplate_bd990293b89f3b78c69fe0ee2f7828b5->doDisplay(Array, Array) (Line: 405)
Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378)
Twig\Template->display(Array) (Line: 390)
Twig\Template->render(Array) (Line: 55)
twig_render_template('themes/contrib/classy/templates/field/field--text-with-summary.html.twig', Array) (Line: 384)
Drupal\Core\Theme\ThemeManager->render('field', Array) (Line: 433)
Drupal\Core\Render\Renderer->doRender(Array) (Line: 446)
Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204)
Drupal\Core\Render\Renderer->render(Array) (Line: 479)
Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 74)
__TwigTemplate_94047fbdba6937b76a4479dfa1763452->doDisplay(Array, Array) (Line: 405)
Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378)
Twig\Template->display(Array) (Line: 390)
Twig\Template->render(Array) (Line: 55)
twig_render_template('themes/custom/yg_aesthetic/templates/node.html.twig', Array) (Line: 384)
Drupal\Core\Theme\ThemeManager->render('node', Array) (Line: 433)
Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204)
Drupal\Core\Render\Renderer->render(Array, ) (Line: 242)
Drupal\Core\Render\MainContent\HtmlRenderer->Drupal\Core\Render\MainContent\{closure}() (Line: 580)
Drupal\Core\Render\Renderer->executeInRenderContext(Object, Object) (Line: 235)
Drupal\Core\Render\MainContent\HtmlRenderer->prepare(Array, Object, Object) (Line: 132)
Drupal\Core\Render\MainContent\HtmlRenderer->renderResponse(Array, Object, Object) (Line: 90)
Drupal\Core\EventSubscriber\MainContentViewSubscriber->onViewRenderArray(Object, 'kernel.view', Object)
call_user_func(Array, Object, 'kernel.view', Object) (Line: 142)
Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher->dispatch(Object, 'kernel.view') (Line: 174)
Symfony\Component\HttpKernel\HttpKernel->handleRaw(Object, 1) (Line: 81)
Symfony\Component\HttpKernel\HttpKernel->handle(Object, 1, 1) (Line: 58)
Drupal\Core\StackMiddleware\Session->handle(Object, 1, 1) (Line: 48)
Drupal\Core\StackMiddleware\KernelPreHandle->handle(Object, 1, 1) (Line: 191)
Drupal\page_cache\StackMiddleware\PageCache->fetch(Object, 1, 1) (Line: 128)
Drupal\page_cache\StackMiddleware\PageCache->lookup(Object, 1, 1) (Line: 82)
Drupal\page_cache\StackMiddleware\PageCache->handle(Object, 1, 1) (Line: 48)
Drupal\Core\StackMiddleware\ReverseProxyMiddleware->handle(Object, 1, 1) (Line: 51)
Drupal\Core\StackMiddleware\NegotiationMiddleware->handle(Object, 1, 1) (Line: 23)
Stack\StackedHttpKernel->handle(Object, 1, 1) (Line: 713)
Drupal\Core\DrupalKernel->handle(Object) (Line: 19)
- Deprecated function: Creation of dynamic property Drupal\mysql\Driver\Database\mysql\Select::$alterTags is deprecated in Drupal\Core\Database\Query\Select->addTag() (line 149 of core/lib/Drupal/Core/Database/Query/Select.php).
Drupal\Core\Database\Query\Select->addTag('entity_query') (Line: 147)
Drupal\Core\Entity\Query\Sql\Query->prepare() (Line: 80)
Drupal\Core\Entity\Query\Sql\Query->execute() (Line: 640)
Drupal\Core\Entity\EntityStorageBase->loadByProperties(Array) (Line: 63)
Drupal\Core\Entity\EntityRepository->loadEntityByUuid('file', '8ddd2985-c1a1-4b04-8418-d79f59caa1f6') (Line: 124)
Drupal\editor\Plugin\Filter\EditorFileReference->process('Para entender el concepto de <strong>curva ROC</strong> (<em>ROC curve</em>, <em>Receiver Operating Characteristic</em>) tenemos que entender previamente cómo funciona un algoritmo de clasificación lineal como la regresión logística: A la hora de realizar una predicción, el modelo asigna una puntuación (un "score") a cada muestra del conjunto de pruebas. Este score se compara con un cierto valor límite ("<em>threshold</em>"). Si, para una muestra, su score es mayor que el threshold, se asigna a la clase principal. Si es menor, se asigna a la clase secundaria.
Siguiendo con el ejemplo visto relativo al Titanic, ya habíamos realizado la predicción:
prediction = model.predict(X_test)
prediction
array([0, 0, 0, 1, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 0, 0, 1, 0, 1,
0, 0, 1, 1, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
1, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0,
1, 0, 1, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1,
1, 0], dtype=int64)
Podemos ver el score que el modelo ha asignado a cada muestra con el método <strong>decision_function</strong>:
scores = model.decision_function(X_test)
scores
array([-1.94301282, -2.36213546, -1.21444007, 2.47651727, 1.1959797 ,
-0.60290339, 2.29157802, 2.53608913, -0.41796414, 0.95146859,
-2.59631885, 1.41196199, -2.15257414, 1.6564731 , 2.71070066,
0.53234595, -2.36213546, -1.71670281, -2.36213546, 0.90989925,
-1.48251942, 2.29157802, -2.15257414, -0.60290339, 0.53234595,
2.47651727, -2.36213546, 0.53234595, 1.6564731 , -0.65922644,
-1.94301282, 1.41196199, -2.36213546, -0.60290339, -2.59631885,
-0.18378075, -2.36213546, -1.48251942, -1.48251942, -1.48251942,
-0.41796414, -1.94301282, -2.36213546, -1.42400139, 2.05739463,
-2.36213546, -2.36213546, 1.99782277, -1.48251942, -0.83708678,
-0.60290339, 0.06073036, 1.1777786 , -2.36213546, -0.18378075,
-2.19539732, -0.60290339, -1.13792094, -0.94530689, -2.86545203,
-1.94301282, 0.77685706, 2.47651727, -0.18378075, 0.77685706,
-2.15257414, 2.29157802, -1.23800831, 1.1777786 , 2.29157802,
1.1777786 , -0.83708678, -0.41796414, -2.36213546, -2.36213546,
0.22038959, -1.79447578, -0.18378075, -2.36213546, -1.06339678,
-2.38675753, 0.01208211, 1.1777786 , -2.36213546, -1.48251942,
2.29157802, 2.71070066, 0.53234595, 1.41196199, -0.41796414])
Por defecto, el threshold aplicado es 0. Podemos ver esto fácilmente viendo, del array anterior, qué valores son mayores que 0 y cuáles son menores. Hagamos esto solo para los 10 primeros valores del array anterior:
scores[:10] > 0
array([False, False, False, True, True, False, True, True, False, True])
...y comparemos este resultado con los 10 primeros valores de la predicción:
prediction[:10]
array([0, 0, 0, 1, 1, 0, 1, 1, 0, 1], dtype=int64)
Vemos que se ha predicho un resultado de 0 (el pasajero no sobrevive) para aquellas muestras (para aquellos pasajeros) cuyos scores son menores que cero.
Pues bien, este clasificador con el threshold por defecto, supone un cierto <em>True Positive Rate</em> y un cierto <em>False Positive Rate</em>. Calculémoslos:
confusion_matrix(prediction, y_test)
array([[47, 9],
[ 4, 30]], dtype=int64)
Si extraemos los valores:
tn, fp, fn, tp = confusion_matrix(prediction, y_test).flatten()
TPR = tp / (tp + fn)
TPR
0.8823529411764706
FPR = (fp / (fp + tn))
FPR
0.16071428571428573
Vamos a almacenar estos valores en sendas listas para poder acceder a ellos posteriormente.
TPRs = [TPR]
FPRs = [FPR]
Y aquí viene lo interesante: hemos dicho que se va a comparar cada score con el threshold. Si el valor es mayor que el threshold se asigna a la clase 1 (o principal), y si es menor, a la clase 0 (o secundaria). Veamos los scores, la predicción que estos scores suponen, y el "ground truth" (el valor real de la variable objetivo) añadiendo estos tres datos a un dataframe ordenando los scores de menor a mayor:
scores_df = pd.DataFrame({
"scores": scores,
"prediction": prediction,
"truth": y_test
})
scores_df.sort_values("scores", inplace = True)
Y mostremos ahora los scores que hay en torno al threshold de cero (se ha buscado manualmente su posición en el dataset):
scores_df.iloc[46:66]
Lógicamente, todas las predicciones correspondientes a scores mayores que 0 aparecen con una predicción de 1, y al revés: las predicciones correspondientes a scores menores que cero aparecen con una predicción de 0.
Vemos, sin embargo, que el verdadero valor de la variable objetivo ("truth") no coincide siempre con la predicción. Lo ideal sería que por encima del threshold solo hubiese muestras de la clase 1 (para tener un TPR de 1), así que ¿qué ocurriría si en lugar de un threshold de 0 considerásemos un threshold de 0.8? En la imagen anterior vemos que, moviendo el threshold hasta dicho valor, estaríamos pasando tres muestras cuyo valor verdadero es 0 al bloque de "predicción 0". Es decir, estaríamos mejorando el TPR de nuestro modelo (al disminuir el número de falsos negativos)... pero a costa de empeorar el FPR. Vamos a calcular estos valores. Para ello tenemos los scores, de forma que podemos realizar la predicción "a mano":
new_prediction = scores > 0.8
new_prediction
array([False, False, False, True, True, False, True, True, False,
True, False, True, False, True, True, False, False, False,
False, True, False, True, False, False, False, True, False,
False, True, False, False, True, False, False, False, False,
False, False, False, False, False, False, False, False, True,
False, False, True, False, False, False, False, True, False,
False, False, False, False, False, False, False, False, True,
False, False, False, True, False, True, True, True, False,
False, False, False, False, False, False, False, False, False,
False, True, False, False, True, True, False, True, False])
confusion_matrix(new_prediction, y_test)
array([[50, 15],
[ 1, 24]], dtype=int64)
tn, fp, fn, tp = confusion_matrix(new_prediction, y_test).flatten()
TPR = tp / (tp + fn)
TPR
0.96
FPR = (fp / (fp + tn))
FPR
0.23076923076923078
Tal y como sospechábamos, el TPR del modelo ha aumentado hasta el 96%, pero el FPR ha aumentado también (lo que significa que hay más falsos positivos que antes).
Almacenemos estos valores en las listas que habíamos creado:
TPRs.append(TPR)
FPRs.append(FPR)
Y, en el ejemplo visto, hemos aumentado el threshold hasta 0.8. Si -nuevamente, viendo los scores- disminuimos el threshold hasta -0.5, vemos que estaríamos "sacando" del bloque de muestras que recibe una predicción 0 varias muestras cuyo valor real es 1... pero también estaríamos moviendo varias muestras que ahora mismo están bien clasificadas. Es decir, estaríamos mejorando el FPR (disminuyendo el número de falsos positivos) a costa de empeorar el TPR:
new_prediction = scores > -0.5
new_prediction
array([False, False, False, True, True, False, True, True, True,
True, False, True, False, True, True, True, False, False,
False, True, False, True, False, False, True, True, False,
True, True, False, False, True, False, False, False, True,
False, False, False, False, True, False, False, False, True,
False, False, True, False, False, False, True, True, False,
True, False, False, False, False, False, False, True, True,
True, True, False, True, False, True, True, True, False,
True, False, False, True, False, True, False, False, False,
True, True, False, False, True, True, True, True, True])
confusion_matrix(new_prediction, y_test)
array([[43, 5],
[ 8, 34]], dtype=int64)
tn, fp, fn, tp = confusion_matrix(new_prediction, y_test).flatten()
TPR = tp / (tp + fn)
TPR
0.8095238095238095
FPR = (fp / (fp + tn))
FPR
0.10416666666666667
Volvamos a almacenar los valores calculados:
TPRs.append(TPR)
FPRs.append(FPR)
También ahora hemos confirmado nuestras sospechas: hemos mejorado el FPR (disminuyéndolo) pero a costa de empeorar el TPR.
Llevemos a la gráfica de TPR vs. FPR las tres parejas de valores encontrados:
t = ["0", "0.8", "-0.5"]
fig, ax = plt.subplots(figsize = (6, 6))
ax.set_xlabel("False Positive Rate", fontsize = 15)
ax.set_ylabel("True Positive Rate", fontsize = 15)
ax.set_xticks(np.linspace(0, 1, 11))
ax.set_yticks(np.linspace(0, 1, 11))
ax.set_xlim(0, 1)
ax.set_ylim(0, 1)
sns.scatterplot(x = FPRs, y = TPRs, s = 50);
for i in range(len(t)):
plt.text(FPRs[i] + 0.02, TPRs[i] - 0.03, t[i], color = "red", fontsize = 15)
...gráfica que muestra exactamente las variaciones que hemos visto.
Hemos probado tres valores del threshold. Vamos a probar todos los thresholds desde el valor mínimo del score hasta el valor máximo, a ver cuál es el resultado:
TPRs = []
FPRs = []
for threshold in np.sort(scores):
new_prediction = scores > threshold
tn, fp, fn, tp = confusion_matrix(y_test, new_prediction).flatten()
TPR = tp / (tp + fn)
FPR = fp / (fp + tn)
TPRs.append(TPR)
FPRs.append(FPR)
Ahora ya tenemos los TPR y FPR en las listas TPRs y FPRs. Mostrémoslas en una gráfica etiquetando cada punto con el threshold al que se corresponde:
fig, ax = plt.subplots(figsize = (10, 10))
ax.set_xlabel("False Positive Rate", fontsize = 15)
ax.set_ylabel("True Positive Rate", fontsize = 15)
ax.set_xticks(np.linspace(0, 1, 11))
ax.set_yticks(np.linspace(0, 1, 11))
sns.scatterplot(x = FPRs, y = TPRs, s = 40);
plt.plot([0, 1], [0, 1], 'k--')
for i in range(len(scores)):
score_i = np.sort(scores)[i]
if score_i > -0.65:
ax.text(FPRs[i] + 0.012, TPRs[i] - 0.004, round(score_i, 2), color = "red", fontsize = 12)
else:
if i % 4 == 0:
ax.text(FPRs[i] - 0.01, TPRs[i] - 0.03, round(score_i, 2), color = "red", fontsize = 12)
Lo que estamos viendo es exactamente la curva ROC del clasificador. Y nos da bastante información: en primer lugar nos dice que aumentando o disminuyendo lo suficiente el threshold podemos conseguir que nuestro algoritmo de clasificación alcance un TPR o un FPR casi perfecto. El único "pero" es que lo conseguiremos a costa de empeorar la otra métrica. Esto significa que, una vez creado el modelo, podemos configurarlo en función de nuestros intereses fijando un threshold u otro (para conseguir un mayor TPR o un FPR). Por otro lado, cuanto más se aproxime la gráfica al punto (0, 1), más se comportará como un clasificador ideal. En la gráfica anterior se muestra la línea diagonal que marca el comportamiento de los clasificadores que tienen tantos verdaderos positivos como falsos negativos, y tantos verdaderos negativos como falsos positivos.
', 'es') (Line: 118)
Drupal\filter\Element\ProcessedText::preRenderText(Array)
call_user_func_array(Array, Array) (Line: 101)
Drupal\Core\Render\Renderer->doTrustedCallback(Array, Array, 'Render #pre_render callbacks must be methods of a class that implements \Drupal\Core\Security\TrustedCallbackInterface or be an anonymous function. The callback was %s. See https://www.drupal.org/node/2966725', 'exception', 'Drupal\Core\Render\Element\RenderCallbackInterface') (Line: 788)
Drupal\Core\Render\Renderer->doCallback('#pre_render', Array, Array) (Line: 374)
Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204)
Drupal\Core\Render\Renderer->render(Array) (Line: 479)
Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 88)
__TwigTemplate_a7d6005c89ae729617b9a0c2bccb1776->doDisplay(Array, Array) (Line: 405)
Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378)
Twig\Template->display(Array, Array) (Line: 46)
__TwigTemplate_804f7948456cfe20e11a34c43439c7c2->doDisplay(Array, Array) (Line: 405)
Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378)
Twig\Template->display(Array, Array) (Line: 43)
__TwigTemplate_bd990293b89f3b78c69fe0ee2f7828b5->doDisplay(Array, Array) (Line: 405)
Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378)
Twig\Template->display(Array) (Line: 390)
Twig\Template->render(Array) (Line: 55)
twig_render_template('themes/contrib/classy/templates/field/field--text-with-summary.html.twig', Array) (Line: 384)
Drupal\Core\Theme\ThemeManager->render('field', Array) (Line: 433)
Drupal\Core\Render\Renderer->doRender(Array) (Line: 446)
Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204)
Drupal\Core\Render\Renderer->render(Array) (Line: 479)
Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 74)
__TwigTemplate_94047fbdba6937b76a4479dfa1763452->doDisplay(Array, Array) (Line: 405)
Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378)
Twig\Template->display(Array) (Line: 390)
Twig\Template->render(Array) (Line: 55)
twig_render_template('themes/custom/yg_aesthetic/templates/node.html.twig', Array) (Line: 384)
Drupal\Core\Theme\ThemeManager->render('node', Array) (Line: 433)
Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204)
Drupal\Core\Render\Renderer->render(Array, ) (Line: 242)
Drupal\Core\Render\MainContent\HtmlRenderer->Drupal\Core\Render\MainContent\{closure}() (Line: 580)
Drupal\Core\Render\Renderer->executeInRenderContext(Object, Object) (Line: 235)
Drupal\Core\Render\MainContent\HtmlRenderer->prepare(Array, Object, Object) (Line: 132)
Drupal\Core\Render\MainContent\HtmlRenderer->renderResponse(Array, Object, Object) (Line: 90)
Drupal\Core\EventSubscriber\MainContentViewSubscriber->onViewRenderArray(Object, 'kernel.view', Object)
call_user_func(Array, Object, 'kernel.view', Object) (Line: 142)
Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher->dispatch(Object, 'kernel.view') (Line: 174)
Symfony\Component\HttpKernel\HttpKernel->handleRaw(Object, 1) (Line: 81)
Symfony\Component\HttpKernel\HttpKernel->handle(Object, 1, 1) (Line: 58)
Drupal\Core\StackMiddleware\Session->handle(Object, 1, 1) (Line: 48)
Drupal\Core\StackMiddleware\KernelPreHandle->handle(Object, 1, 1) (Line: 191)
Drupal\page_cache\StackMiddleware\PageCache->fetch(Object, 1, 1) (Line: 128)
Drupal\page_cache\StackMiddleware\PageCache->lookup(Object, 1, 1) (Line: 82)
Drupal\page_cache\StackMiddleware\PageCache->handle(Object, 1, 1) (Line: 48)
Drupal\Core\StackMiddleware\ReverseProxyMiddleware->handle(Object, 1, 1) (Line: 51)
Drupal\Core\StackMiddleware\NegotiationMiddleware->handle(Object, 1, 1) (Line: 23)
Stack\StackedHttpKernel->handle(Object, 1, 1) (Line: 713)
Drupal\Core\DrupalKernel->handle(Object) (Line: 19)
- Deprecated function: Creation of dynamic property Drupal\mysql\Driver\Database\mysql\Select::$alterMetaData is deprecated in Drupal\Core\Database\Query\Select->addMetaData() (line 178 of core/lib/Drupal/Core/Database/Query/Select.php).
Drupal\Core\Database\Query\Select->addMetaData('entity_type', 'file') (Line: 115)
Drupal\Core\Entity\Query\Sql\Query->prepare() (Line: 80)
Drupal\Core\Entity\Query\Sql\Query->execute() (Line: 640)
Drupal\Core\Entity\EntityStorageBase->loadByProperties(Array) (Line: 63)
Drupal\Core\Entity\EntityRepository->loadEntityByUuid('file', '446f59ac-0e20-4ea8-9e05-2f5f79825544') (Line: 95)
Drupal\editor\Plugin\Filter\EditorFileReference->process('Para entender el concepto de <strong>curva ROC</strong> (<em>ROC curve</em>, <em>Receiver Operating Characteristic</em>) tenemos que entender previamente cómo funciona un algoritmo de clasificación lineal como la regresión logística: A la hora de realizar una predicción, el modelo asigna una puntuación (un "score") a cada muestra del conjunto de pruebas. Este score se compara con un cierto valor límite ("<em>threshold</em>"). Si, para una muestra, su score es mayor que el threshold, se asigna a la clase principal. Si es menor, se asigna a la clase secundaria.
Siguiendo con el ejemplo visto relativo al Titanic, ya habíamos realizado la predicción:
prediction = model.predict(X_test)
prediction
array([0, 0, 0, 1, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 0, 0, 1, 0, 1,
0, 0, 1, 1, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
1, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0,
1, 0, 1, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1,
1, 0], dtype=int64)
Podemos ver el score que el modelo ha asignado a cada muestra con el método <strong>decision_function</strong>:
scores = model.decision_function(X_test)
scores
array([-1.94301282, -2.36213546, -1.21444007, 2.47651727, 1.1959797 ,
-0.60290339, 2.29157802, 2.53608913, -0.41796414, 0.95146859,
-2.59631885, 1.41196199, -2.15257414, 1.6564731 , 2.71070066,
0.53234595, -2.36213546, -1.71670281, -2.36213546, 0.90989925,
-1.48251942, 2.29157802, -2.15257414, -0.60290339, 0.53234595,
2.47651727, -2.36213546, 0.53234595, 1.6564731 , -0.65922644,
-1.94301282, 1.41196199, -2.36213546, -0.60290339, -2.59631885,
-0.18378075, -2.36213546, -1.48251942, -1.48251942, -1.48251942,
-0.41796414, -1.94301282, -2.36213546, -1.42400139, 2.05739463,
-2.36213546, -2.36213546, 1.99782277, -1.48251942, -0.83708678,
-0.60290339, 0.06073036, 1.1777786 , -2.36213546, -0.18378075,
-2.19539732, -0.60290339, -1.13792094, -0.94530689, -2.86545203,
-1.94301282, 0.77685706, 2.47651727, -0.18378075, 0.77685706,
-2.15257414, 2.29157802, -1.23800831, 1.1777786 , 2.29157802,
1.1777786 , -0.83708678, -0.41796414, -2.36213546, -2.36213546,
0.22038959, -1.79447578, -0.18378075, -2.36213546, -1.06339678,
-2.38675753, 0.01208211, 1.1777786 , -2.36213546, -1.48251942,
2.29157802, 2.71070066, 0.53234595, 1.41196199, -0.41796414])
Por defecto, el threshold aplicado es 0. Podemos ver esto fácilmente viendo, del array anterior, qué valores son mayores que 0 y cuáles son menores. Hagamos esto solo para los 10 primeros valores del array anterior:
scores[:10] > 0
array([False, False, False, True, True, False, True, True, False, True])
...y comparemos este resultado con los 10 primeros valores de la predicción:
prediction[:10]
array([0, 0, 0, 1, 1, 0, 1, 1, 0, 1], dtype=int64)
Vemos que se ha predicho un resultado de 0 (el pasajero no sobrevive) para aquellas muestras (para aquellos pasajeros) cuyos scores son menores que cero.
Pues bien, este clasificador con el threshold por defecto, supone un cierto <em>True Positive Rate</em> y un cierto <em>False Positive Rate</em>. Calculémoslos:
confusion_matrix(prediction, y_test)
array([[47, 9],
[ 4, 30]], dtype=int64)
Si extraemos los valores:
tn, fp, fn, tp = confusion_matrix(prediction, y_test).flatten()
TPR = tp / (tp + fn)
TPR
0.8823529411764706
FPR = (fp / (fp + tn))
FPR
0.16071428571428573
Vamos a almacenar estos valores en sendas listas para poder acceder a ellos posteriormente.
TPRs = [TPR]
FPRs = [FPR]
Y aquí viene lo interesante: hemos dicho que se va a comparar cada score con el threshold. Si el valor es mayor que el threshold se asigna a la clase 1 (o principal), y si es menor, a la clase 0 (o secundaria). Veamos los scores, la predicción que estos scores suponen, y el "ground truth" (el valor real de la variable objetivo) añadiendo estos tres datos a un dataframe ordenando los scores de menor a mayor:
scores_df = pd.DataFrame({
"scores": scores,
"prediction": prediction,
"truth": y_test
})
scores_df.sort_values("scores", inplace = True)
Y mostremos ahora los scores que hay en torno al threshold de cero (se ha buscado manualmente su posición en el dataset):
scores_df.iloc[46:66]
Lógicamente, todas las predicciones correspondientes a scores mayores que 0 aparecen con una predicción de 1, y al revés: las predicciones correspondientes a scores menores que cero aparecen con una predicción de 0.
Vemos, sin embargo, que el verdadero valor de la variable objetivo ("truth") no coincide siempre con la predicción. Lo ideal sería que por encima del threshold solo hubiese muestras de la clase 1 (para tener un TPR de 1), así que ¿qué ocurriría si en lugar de un threshold de 0 considerásemos un threshold de 0.8? En la imagen anterior vemos que, moviendo el threshold hasta dicho valor, estaríamos pasando tres muestras cuyo valor verdadero es 0 al bloque de "predicción 0". Es decir, estaríamos mejorando el TPR de nuestro modelo (al disminuir el número de falsos negativos)... pero a costa de empeorar el FPR. Vamos a calcular estos valores. Para ello tenemos los scores, de forma que podemos realizar la predicción "a mano":
new_prediction = scores > 0.8
new_prediction
array([False, False, False, True, True, False, True, True, False,
True, False, True, False, True, True, False, False, False,
False, True, False, True, False, False, False, True, False,
False, True, False, False, True, False, False, False, False,
False, False, False, False, False, False, False, False, True,
False, False, True, False, False, False, False, True, False,
False, False, False, False, False, False, False, False, True,
False, False, False, True, False, True, True, True, False,
False, False, False, False, False, False, False, False, False,
False, True, False, False, True, True, False, True, False])
confusion_matrix(new_prediction, y_test)
array([[50, 15],
[ 1, 24]], dtype=int64)
tn, fp, fn, tp = confusion_matrix(new_prediction, y_test).flatten()
TPR = tp / (tp + fn)
TPR
0.96
FPR = (fp / (fp + tn))
FPR
0.23076923076923078
Tal y como sospechábamos, el TPR del modelo ha aumentado hasta el 96%, pero el FPR ha aumentado también (lo que significa que hay más falsos positivos que antes).
Almacenemos estos valores en las listas que habíamos creado:
TPRs.append(TPR)
FPRs.append(FPR)
Y, en el ejemplo visto, hemos aumentado el threshold hasta 0.8. Si -nuevamente, viendo los scores- disminuimos el threshold hasta -0.5, vemos que estaríamos "sacando" del bloque de muestras que recibe una predicción 0 varias muestras cuyo valor real es 1... pero también estaríamos moviendo varias muestras que ahora mismo están bien clasificadas. Es decir, estaríamos mejorando el FPR (disminuyendo el número de falsos positivos) a costa de empeorar el TPR:
new_prediction = scores > -0.5
new_prediction
array([False, False, False, True, True, False, True, True, True,
True, False, True, False, True, True, True, False, False,
False, True, False, True, False, False, True, True, False,
True, True, False, False, True, False, False, False, True,
False, False, False, False, True, False, False, False, True,
False, False, True, False, False, False, True, True, False,
True, False, False, False, False, False, False, True, True,
True, True, False, True, False, True, True, True, False,
True, False, False, True, False, True, False, False, False,
True, True, False, False, True, True, True, True, True])
confusion_matrix(new_prediction, y_test)
array([[43, 5],
[ 8, 34]], dtype=int64)
tn, fp, fn, tp = confusion_matrix(new_prediction, y_test).flatten()
TPR = tp / (tp + fn)
TPR
0.8095238095238095
FPR = (fp / (fp + tn))
FPR
0.10416666666666667
Volvamos a almacenar los valores calculados:
TPRs.append(TPR)
FPRs.append(FPR)
También ahora hemos confirmado nuestras sospechas: hemos mejorado el FPR (disminuyéndolo) pero a costa de empeorar el TPR.
Llevemos a la gráfica de TPR vs. FPR las tres parejas de valores encontrados:
t = ["0", "0.8", "-0.5"]
fig, ax = plt.subplots(figsize = (6, 6))
ax.set_xlabel("False Positive Rate", fontsize = 15)
ax.set_ylabel("True Positive Rate", fontsize = 15)
ax.set_xticks(np.linspace(0, 1, 11))
ax.set_yticks(np.linspace(0, 1, 11))
ax.set_xlim(0, 1)
ax.set_ylim(0, 1)
sns.scatterplot(x = FPRs, y = TPRs, s = 50);
for i in range(len(t)):
plt.text(FPRs[i] + 0.02, TPRs[i] - 0.03, t[i], color = "red", fontsize = 15)
...gráfica que muestra exactamente las variaciones que hemos visto.
Hemos probado tres valores del threshold. Vamos a probar todos los thresholds desde el valor mínimo del score hasta el valor máximo, a ver cuál es el resultado:
TPRs = []
FPRs = []
for threshold in np.sort(scores):
new_prediction = scores > threshold
tn, fp, fn, tp = confusion_matrix(y_test, new_prediction).flatten()
TPR = tp / (tp + fn)
FPR = fp / (fp + tn)
TPRs.append(TPR)
FPRs.append(FPR)
Ahora ya tenemos los TPR y FPR en las listas TPRs y FPRs. Mostrémoslas en una gráfica etiquetando cada punto con el threshold al que se corresponde:
fig, ax = plt.subplots(figsize = (10, 10))
ax.set_xlabel("False Positive Rate", fontsize = 15)
ax.set_ylabel("True Positive Rate", fontsize = 15)
ax.set_xticks(np.linspace(0, 1, 11))
ax.set_yticks(np.linspace(0, 1, 11))
sns.scatterplot(x = FPRs, y = TPRs, s = 40);
plt.plot([0, 1], [0, 1], 'k--')
for i in range(len(scores)):
score_i = np.sort(scores)[i]
if score_i > -0.65:
ax.text(FPRs[i] + 0.012, TPRs[i] - 0.004, round(score_i, 2), color = "red", fontsize = 12)
else:
if i % 4 == 0:
ax.text(FPRs[i] - 0.01, TPRs[i] - 0.03, round(score_i, 2), color = "red", fontsize = 12)
Lo que estamos viendo es exactamente la curva ROC del clasificador. Y nos da bastante información: en primer lugar nos dice que aumentando o disminuyendo lo suficiente el threshold podemos conseguir que nuestro algoritmo de clasificación alcance un TPR o un FPR casi perfecto. El único "pero" es que lo conseguiremos a costa de empeorar la otra métrica. Esto significa que, una vez creado el modelo, podemos configurarlo en función de nuestros intereses fijando un threshold u otro (para conseguir un mayor TPR o un FPR). Por otro lado, cuanto más se aproxime la gráfica al punto (0, 1), más se comportará como un clasificador ideal. En la gráfica anterior se muestra la línea diagonal que marca el comportamiento de los clasificadores que tienen tantos verdaderos positivos como falsos negativos, y tantos verdaderos negativos como falsos positivos.
', 'es') (Line: 118)
Drupal\filter\Element\ProcessedText::preRenderText(Array)
call_user_func_array(Array, Array) (Line: 101)
Drupal\Core\Render\Renderer->doTrustedCallback(Array, Array, 'Render #pre_render callbacks must be methods of a class that implements \Drupal\Core\Security\TrustedCallbackInterface or be an anonymous function. The callback was %s. See https://www.drupal.org/node/2966725', 'exception', 'Drupal\Core\Render\Element\RenderCallbackInterface') (Line: 788)
Drupal\Core\Render\Renderer->doCallback('#pre_render', Array, Array) (Line: 374)
Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204)
Drupal\Core\Render\Renderer->render(Array) (Line: 479)
Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 88)
__TwigTemplate_a7d6005c89ae729617b9a0c2bccb1776->doDisplay(Array, Array) (Line: 405)
Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378)
Twig\Template->display(Array, Array) (Line: 46)
__TwigTemplate_804f7948456cfe20e11a34c43439c7c2->doDisplay(Array, Array) (Line: 405)
Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378)
Twig\Template->display(Array, Array) (Line: 43)
__TwigTemplate_bd990293b89f3b78c69fe0ee2f7828b5->doDisplay(Array, Array) (Line: 405)
Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378)
Twig\Template->display(Array) (Line: 390)
Twig\Template->render(Array) (Line: 55)
twig_render_template('themes/contrib/classy/templates/field/field--text-with-summary.html.twig', Array) (Line: 384)
Drupal\Core\Theme\ThemeManager->render('field', Array) (Line: 433)
Drupal\Core\Render\Renderer->doRender(Array) (Line: 446)
Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204)
Drupal\Core\Render\Renderer->render(Array) (Line: 479)
Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 74)
__TwigTemplate_94047fbdba6937b76a4479dfa1763452->doDisplay(Array, Array) (Line: 405)
Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378)
Twig\Template->display(Array) (Line: 390)
Twig\Template->render(Array) (Line: 55)
twig_render_template('themes/custom/yg_aesthetic/templates/node.html.twig', Array) (Line: 384)
Drupal\Core\Theme\ThemeManager->render('node', Array) (Line: 433)
Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204)
Drupal\Core\Render\Renderer->render(Array, ) (Line: 242)
Drupal\Core\Render\MainContent\HtmlRenderer->Drupal\Core\Render\MainContent\{closure}() (Line: 580)
Drupal\Core\Render\Renderer->executeInRenderContext(Object, Object) (Line: 235)
Drupal\Core\Render\MainContent\HtmlRenderer->prepare(Array, Object, Object) (Line: 132)
Drupal\Core\Render\MainContent\HtmlRenderer->renderResponse(Array, Object, Object) (Line: 90)
Drupal\Core\EventSubscriber\MainContentViewSubscriber->onViewRenderArray(Object, 'kernel.view', Object)
call_user_func(Array, Object, 'kernel.view', Object) (Line: 142)
Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher->dispatch(Object, 'kernel.view') (Line: 174)
Symfony\Component\HttpKernel\HttpKernel->handleRaw(Object, 1) (Line: 81)
Symfony\Component\HttpKernel\HttpKernel->handle(Object, 1, 1) (Line: 58)
Drupal\Core\StackMiddleware\Session->handle(Object, 1, 1) (Line: 48)
Drupal\Core\StackMiddleware\KernelPreHandle->handle(Object, 1, 1) (Line: 191)
Drupal\page_cache\StackMiddleware\PageCache->fetch(Object, 1, 1) (Line: 128)
Drupal\page_cache\StackMiddleware\PageCache->lookup(Object, 1, 1) (Line: 82)
Drupal\page_cache\StackMiddleware\PageCache->handle(Object, 1, 1) (Line: 48)
Drupal\Core\StackMiddleware\ReverseProxyMiddleware->handle(Object, 1, 1) (Line: 51)
Drupal\Core\StackMiddleware\NegotiationMiddleware->handle(Object, 1, 1) (Line: 23)
Stack\StackedHttpKernel->handle(Object, 1, 1) (Line: 713)
Drupal\Core\DrupalKernel->handle(Object) (Line: 19)
- Deprecated function: Creation of dynamic property Drupal\mysql\Driver\Database\mysql\Select::$alterTags is deprecated in Drupal\Core\Database\Query\Select->addTag() (line 149 of core/lib/Drupal/Core/Database/Query/Select.php).
Drupal\Core\Database\Query\Select->addTag('entity_query') (Line: 147)
Drupal\Core\Entity\Query\Sql\Query->prepare() (Line: 80)
Drupal\Core\Entity\Query\Sql\Query->execute() (Line: 640)
Drupal\Core\Entity\EntityStorageBase->loadByProperties(Array) (Line: 63)
Drupal\Core\Entity\EntityRepository->loadEntityByUuid('file', '446f59ac-0e20-4ea8-9e05-2f5f79825544') (Line: 95)
Drupal\editor\Plugin\Filter\EditorFileReference->process('Para entender el concepto de <strong>curva ROC</strong> (<em>ROC curve</em>, <em>Receiver Operating Characteristic</em>) tenemos que entender previamente cómo funciona un algoritmo de clasificación lineal como la regresión logística: A la hora de realizar una predicción, el modelo asigna una puntuación (un "score") a cada muestra del conjunto de pruebas. Este score se compara con un cierto valor límite ("<em>threshold</em>"). Si, para una muestra, su score es mayor que el threshold, se asigna a la clase principal. Si es menor, se asigna a la clase secundaria.
Siguiendo con el ejemplo visto relativo al Titanic, ya habíamos realizado la predicción:
prediction = model.predict(X_test)
prediction
array([0, 0, 0, 1, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 0, 0, 1, 0, 1,
0, 0, 1, 1, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
1, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0,
1, 0, 1, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1,
1, 0], dtype=int64)
Podemos ver el score que el modelo ha asignado a cada muestra con el método <strong>decision_function</strong>:
scores = model.decision_function(X_test)
scores
array([-1.94301282, -2.36213546, -1.21444007, 2.47651727, 1.1959797 ,
-0.60290339, 2.29157802, 2.53608913, -0.41796414, 0.95146859,
-2.59631885, 1.41196199, -2.15257414, 1.6564731 , 2.71070066,
0.53234595, -2.36213546, -1.71670281, -2.36213546, 0.90989925,
-1.48251942, 2.29157802, -2.15257414, -0.60290339, 0.53234595,
2.47651727, -2.36213546, 0.53234595, 1.6564731 , -0.65922644,
-1.94301282, 1.41196199, -2.36213546, -0.60290339, -2.59631885,
-0.18378075, -2.36213546, -1.48251942, -1.48251942, -1.48251942,
-0.41796414, -1.94301282, -2.36213546, -1.42400139, 2.05739463,
-2.36213546, -2.36213546, 1.99782277, -1.48251942, -0.83708678,
-0.60290339, 0.06073036, 1.1777786 , -2.36213546, -0.18378075,
-2.19539732, -0.60290339, -1.13792094, -0.94530689, -2.86545203,
-1.94301282, 0.77685706, 2.47651727, -0.18378075, 0.77685706,
-2.15257414, 2.29157802, -1.23800831, 1.1777786 , 2.29157802,
1.1777786 , -0.83708678, -0.41796414, -2.36213546, -2.36213546,
0.22038959, -1.79447578, -0.18378075, -2.36213546, -1.06339678,
-2.38675753, 0.01208211, 1.1777786 , -2.36213546, -1.48251942,
2.29157802, 2.71070066, 0.53234595, 1.41196199, -0.41796414])
Por defecto, el threshold aplicado es 0. Podemos ver esto fácilmente viendo, del array anterior, qué valores son mayores que 0 y cuáles son menores. Hagamos esto solo para los 10 primeros valores del array anterior:
scores[:10] > 0
array([False, False, False, True, True, False, True, True, False, True])
...y comparemos este resultado con los 10 primeros valores de la predicción:
prediction[:10]
array([0, 0, 0, 1, 1, 0, 1, 1, 0, 1], dtype=int64)
Vemos que se ha predicho un resultado de 0 (el pasajero no sobrevive) para aquellas muestras (para aquellos pasajeros) cuyos scores son menores que cero.
Pues bien, este clasificador con el threshold por defecto, supone un cierto <em>True Positive Rate</em> y un cierto <em>False Positive Rate</em>. Calculémoslos:
confusion_matrix(prediction, y_test)
array([[47, 9],
[ 4, 30]], dtype=int64)
Si extraemos los valores:
tn, fp, fn, tp = confusion_matrix(prediction, y_test).flatten()
TPR = tp / (tp + fn)
TPR
0.8823529411764706
FPR = (fp / (fp + tn))
FPR
0.16071428571428573
Vamos a almacenar estos valores en sendas listas para poder acceder a ellos posteriormente.
TPRs = [TPR]
FPRs = [FPR]
Y aquí viene lo interesante: hemos dicho que se va a comparar cada score con el threshold. Si el valor es mayor que el threshold se asigna a la clase 1 (o principal), y si es menor, a la clase 0 (o secundaria). Veamos los scores, la predicción que estos scores suponen, y el "ground truth" (el valor real de la variable objetivo) añadiendo estos tres datos a un dataframe ordenando los scores de menor a mayor:
scores_df = pd.DataFrame({
"scores": scores,
"prediction": prediction,
"truth": y_test
})
scores_df.sort_values("scores", inplace = True)
Y mostremos ahora los scores que hay en torno al threshold de cero (se ha buscado manualmente su posición en el dataset):
scores_df.iloc[46:66]
Lógicamente, todas las predicciones correspondientes a scores mayores que 0 aparecen con una predicción de 1, y al revés: las predicciones correspondientes a scores menores que cero aparecen con una predicción de 0.
Vemos, sin embargo, que el verdadero valor de la variable objetivo ("truth") no coincide siempre con la predicción. Lo ideal sería que por encima del threshold solo hubiese muestras de la clase 1 (para tener un TPR de 1), así que ¿qué ocurriría si en lugar de un threshold de 0 considerásemos un threshold de 0.8? En la imagen anterior vemos que, moviendo el threshold hasta dicho valor, estaríamos pasando tres muestras cuyo valor verdadero es 0 al bloque de "predicción 0". Es decir, estaríamos mejorando el TPR de nuestro modelo (al disminuir el número de falsos negativos)... pero a costa de empeorar el FPR. Vamos a calcular estos valores. Para ello tenemos los scores, de forma que podemos realizar la predicción "a mano":
new_prediction = scores > 0.8
new_prediction
array([False, False, False, True, True, False, True, True, False,
True, False, True, False, True, True, False, False, False,
False, True, False, True, False, False, False, True, False,
False, True, False, False, True, False, False, False, False,
False, False, False, False, False, False, False, False, True,
False, False, True, False, False, False, False, True, False,
False, False, False, False, False, False, False, False, True,
False, False, False, True, False, True, True, True, False,
False, False, False, False, False, False, False, False, False,
False, True, False, False, True, True, False, True, False])
confusion_matrix(new_prediction, y_test)
array([[50, 15],
[ 1, 24]], dtype=int64)
tn, fp, fn, tp = confusion_matrix(new_prediction, y_test).flatten()
TPR = tp / (tp + fn)
TPR
0.96
FPR = (fp / (fp + tn))
FPR
0.23076923076923078
Tal y como sospechábamos, el TPR del modelo ha aumentado hasta el 96%, pero el FPR ha aumentado también (lo que significa que hay más falsos positivos que antes).
Almacenemos estos valores en las listas que habíamos creado:
TPRs.append(TPR)
FPRs.append(FPR)
Y, en el ejemplo visto, hemos aumentado el threshold hasta 0.8. Si -nuevamente, viendo los scores- disminuimos el threshold hasta -0.5, vemos que estaríamos "sacando" del bloque de muestras que recibe una predicción 0 varias muestras cuyo valor real es 1... pero también estaríamos moviendo varias muestras que ahora mismo están bien clasificadas. Es decir, estaríamos mejorando el FPR (disminuyendo el número de falsos positivos) a costa de empeorar el TPR:
new_prediction = scores > -0.5
new_prediction
array([False, False, False, True, True, False, True, True, True,
True, False, True, False, True, True, True, False, False,
False, True, False, True, False, False, True, True, False,
True, True, False, False, True, False, False, False, True,
False, False, False, False, True, False, False, False, True,
False, False, True, False, False, False, True, True, False,
True, False, False, False, False, False, False, True, True,
True, True, False, True, False, True, True, True, False,
True, False, False, True, False, True, False, False, False,
True, True, False, False, True, True, True, True, True])
confusion_matrix(new_prediction, y_test)
array([[43, 5],
[ 8, 34]], dtype=int64)
tn, fp, fn, tp = confusion_matrix(new_prediction, y_test).flatten()
TPR = tp / (tp + fn)
TPR
0.8095238095238095
FPR = (fp / (fp + tn))
FPR
0.10416666666666667
Volvamos a almacenar los valores calculados:
TPRs.append(TPR)
FPRs.append(FPR)
También ahora hemos confirmado nuestras sospechas: hemos mejorado el FPR (disminuyéndolo) pero a costa de empeorar el TPR.
Llevemos a la gráfica de TPR vs. FPR las tres parejas de valores encontrados:
t = ["0", "0.8", "-0.5"]
fig, ax = plt.subplots(figsize = (6, 6))
ax.set_xlabel("False Positive Rate", fontsize = 15)
ax.set_ylabel("True Positive Rate", fontsize = 15)
ax.set_xticks(np.linspace(0, 1, 11))
ax.set_yticks(np.linspace(0, 1, 11))
ax.set_xlim(0, 1)
ax.set_ylim(0, 1)
sns.scatterplot(x = FPRs, y = TPRs, s = 50);
for i in range(len(t)):
plt.text(FPRs[i] + 0.02, TPRs[i] - 0.03, t[i], color = "red", fontsize = 15)
...gráfica que muestra exactamente las variaciones que hemos visto.
Hemos probado tres valores del threshold. Vamos a probar todos los thresholds desde el valor mínimo del score hasta el valor máximo, a ver cuál es el resultado:
TPRs = []
FPRs = []
for threshold in np.sort(scores):
new_prediction = scores > threshold
tn, fp, fn, tp = confusion_matrix(y_test, new_prediction).flatten()
TPR = tp / (tp + fn)
FPR = fp / (fp + tn)
TPRs.append(TPR)
FPRs.append(FPR)
Ahora ya tenemos los TPR y FPR en las listas TPRs y FPRs. Mostrémoslas en una gráfica etiquetando cada punto con el threshold al que se corresponde:
fig, ax = plt.subplots(figsize = (10, 10))
ax.set_xlabel("False Positive Rate", fontsize = 15)
ax.set_ylabel("True Positive Rate", fontsize = 15)
ax.set_xticks(np.linspace(0, 1, 11))
ax.set_yticks(np.linspace(0, 1, 11))
sns.scatterplot(x = FPRs, y = TPRs, s = 40);
plt.plot([0, 1], [0, 1], 'k--')
for i in range(len(scores)):
score_i = np.sort(scores)[i]
if score_i > -0.65:
ax.text(FPRs[i] + 0.012, TPRs[i] - 0.004, round(score_i, 2), color = "red", fontsize = 12)
else:
if i % 4 == 0:
ax.text(FPRs[i] - 0.01, TPRs[i] - 0.03, round(score_i, 2), color = "red", fontsize = 12)
Lo que estamos viendo es exactamente la curva ROC del clasificador. Y nos da bastante información: en primer lugar nos dice que aumentando o disminuyendo lo suficiente el threshold podemos conseguir que nuestro algoritmo de clasificación alcance un TPR o un FPR casi perfecto. El único "pero" es que lo conseguiremos a costa de empeorar la otra métrica. Esto significa que, una vez creado el modelo, podemos configurarlo en función de nuestros intereses fijando un threshold u otro (para conseguir un mayor TPR o un FPR). Por otro lado, cuanto más se aproxime la gráfica al punto (0, 1), más se comportará como un clasificador ideal. En la gráfica anterior se muestra la línea diagonal que marca el comportamiento de los clasificadores que tienen tantos verdaderos positivos como falsos negativos, y tantos verdaderos negativos como falsos positivos.
', 'es') (Line: 118)
Drupal\filter\Element\ProcessedText::preRenderText(Array)
call_user_func_array(Array, Array) (Line: 101)
Drupal\Core\Render\Renderer->doTrustedCallback(Array, Array, 'Render #pre_render callbacks must be methods of a class that implements \Drupal\Core\Security\TrustedCallbackInterface or be an anonymous function. The callback was %s. See https://www.drupal.org/node/2966725', 'exception', 'Drupal\Core\Render\Element\RenderCallbackInterface') (Line: 788)
Drupal\Core\Render\Renderer->doCallback('#pre_render', Array, Array) (Line: 374)
Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204)
Drupal\Core\Render\Renderer->render(Array) (Line: 479)
Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 88)
__TwigTemplate_a7d6005c89ae729617b9a0c2bccb1776->doDisplay(Array, Array) (Line: 405)
Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378)
Twig\Template->display(Array, Array) (Line: 46)
__TwigTemplate_804f7948456cfe20e11a34c43439c7c2->doDisplay(Array, Array) (Line: 405)
Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378)
Twig\Template->display(Array, Array) (Line: 43)
__TwigTemplate_bd990293b89f3b78c69fe0ee2f7828b5->doDisplay(Array, Array) (Line: 405)
Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378)
Twig\Template->display(Array) (Line: 390)
Twig\Template->render(Array) (Line: 55)
twig_render_template('themes/contrib/classy/templates/field/field--text-with-summary.html.twig', Array) (Line: 384)
Drupal\Core\Theme\ThemeManager->render('field', Array) (Line: 433)
Drupal\Core\Render\Renderer->doRender(Array) (Line: 446)
Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204)
Drupal\Core\Render\Renderer->render(Array) (Line: 479)
Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 74)
__TwigTemplate_94047fbdba6937b76a4479dfa1763452->doDisplay(Array, Array) (Line: 405)
Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378)
Twig\Template->display(Array) (Line: 390)
Twig\Template->render(Array) (Line: 55)
twig_render_template('themes/custom/yg_aesthetic/templates/node.html.twig', Array) (Line: 384)
Drupal\Core\Theme\ThemeManager->render('node', Array) (Line: 433)
Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204)
Drupal\Core\Render\Renderer->render(Array, ) (Line: 242)
Drupal\Core\Render\MainContent\HtmlRenderer->Drupal\Core\Render\MainContent\{closure}() (Line: 580)
Drupal\Core\Render\Renderer->executeInRenderContext(Object, Object) (Line: 235)
Drupal\Core\Render\MainContent\HtmlRenderer->prepare(Array, Object, Object) (Line: 132)
Drupal\Core\Render\MainContent\HtmlRenderer->renderResponse(Array, Object, Object) (Line: 90)
Drupal\Core\EventSubscriber\MainContentViewSubscriber->onViewRenderArray(Object, 'kernel.view', Object)
call_user_func(Array, Object, 'kernel.view', Object) (Line: 142)
Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher->dispatch(Object, 'kernel.view') (Line: 174)
Symfony\Component\HttpKernel\HttpKernel->handleRaw(Object, 1) (Line: 81)
Symfony\Component\HttpKernel\HttpKernel->handle(Object, 1, 1) (Line: 58)
Drupal\Core\StackMiddleware\Session->handle(Object, 1, 1) (Line: 48)
Drupal\Core\StackMiddleware\KernelPreHandle->handle(Object, 1, 1) (Line: 191)
Drupal\page_cache\StackMiddleware\PageCache->fetch(Object, 1, 1) (Line: 128)
Drupal\page_cache\StackMiddleware\PageCache->lookup(Object, 1, 1) (Line: 82)
Drupal\page_cache\StackMiddleware\PageCache->handle(Object, 1, 1) (Line: 48)
Drupal\Core\StackMiddleware\ReverseProxyMiddleware->handle(Object, 1, 1) (Line: 51)
Drupal\Core\StackMiddleware\NegotiationMiddleware->handle(Object, 1, 1) (Line: 23)
Stack\StackedHttpKernel->handle(Object, 1, 1) (Line: 713)
Drupal\Core\DrupalKernel->handle(Object) (Line: 19)
- Deprecated function: Creation of dynamic property Drupal\mysql\Driver\Database\mysql\Select::$alterMetaData is deprecated in Drupal\Core\Database\Query\Select->addMetaData() (line 178 of core/lib/Drupal/Core/Database/Query/Select.php).
Drupal\Core\Database\Query\Select->addMetaData('entity_type', 'file') (Line: 115)
Drupal\Core\Entity\Query\Sql\Query->prepare() (Line: 80)
Drupal\Core\Entity\Query\Sql\Query->execute() (Line: 640)
Drupal\Core\Entity\EntityStorageBase->loadByProperties(Array) (Line: 63)
Drupal\Core\Entity\EntityRepository->loadEntityByUuid('file', '446f59ac-0e20-4ea8-9e05-2f5f79825544') (Line: 124)
Drupal\editor\Plugin\Filter\EditorFileReference->process('Para entender el concepto de <strong>curva ROC</strong> (<em>ROC curve</em>, <em>Receiver Operating Characteristic</em>) tenemos que entender previamente cómo funciona un algoritmo de clasificación lineal como la regresión logística: A la hora de realizar una predicción, el modelo asigna una puntuación (un "score") a cada muestra del conjunto de pruebas. Este score se compara con un cierto valor límite ("<em>threshold</em>"). Si, para una muestra, su score es mayor que el threshold, se asigna a la clase principal. Si es menor, se asigna a la clase secundaria.
Siguiendo con el ejemplo visto relativo al Titanic, ya habíamos realizado la predicción:
prediction = model.predict(X_test)
prediction
array([0, 0, 0, 1, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 0, 0, 1, 0, 1,
0, 0, 1, 1, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
1, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0,
1, 0, 1, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1,
1, 0], dtype=int64)
Podemos ver el score que el modelo ha asignado a cada muestra con el método <strong>decision_function</strong>:
scores = model.decision_function(X_test)
scores
array([-1.94301282, -2.36213546, -1.21444007, 2.47651727, 1.1959797 ,
-0.60290339, 2.29157802, 2.53608913, -0.41796414, 0.95146859,
-2.59631885, 1.41196199, -2.15257414, 1.6564731 , 2.71070066,
0.53234595, -2.36213546, -1.71670281, -2.36213546, 0.90989925,
-1.48251942, 2.29157802, -2.15257414, -0.60290339, 0.53234595,
2.47651727, -2.36213546, 0.53234595, 1.6564731 , -0.65922644,
-1.94301282, 1.41196199, -2.36213546, -0.60290339, -2.59631885,
-0.18378075, -2.36213546, -1.48251942, -1.48251942, -1.48251942,
-0.41796414, -1.94301282, -2.36213546, -1.42400139, 2.05739463,
-2.36213546, -2.36213546, 1.99782277, -1.48251942, -0.83708678,
-0.60290339, 0.06073036, 1.1777786 , -2.36213546, -0.18378075,
-2.19539732, -0.60290339, -1.13792094, -0.94530689, -2.86545203,
-1.94301282, 0.77685706, 2.47651727, -0.18378075, 0.77685706,
-2.15257414, 2.29157802, -1.23800831, 1.1777786 , 2.29157802,
1.1777786 , -0.83708678, -0.41796414, -2.36213546, -2.36213546,
0.22038959, -1.79447578, -0.18378075, -2.36213546, -1.06339678,
-2.38675753, 0.01208211, 1.1777786 , -2.36213546, -1.48251942,
2.29157802, 2.71070066, 0.53234595, 1.41196199, -0.41796414])
Por defecto, el threshold aplicado es 0. Podemos ver esto fácilmente viendo, del array anterior, qué valores son mayores que 0 y cuáles son menores. Hagamos esto solo para los 10 primeros valores del array anterior:
scores[:10] > 0
array([False, False, False, True, True, False, True, True, False, True])
...y comparemos este resultado con los 10 primeros valores de la predicción:
prediction[:10]
array([0, 0, 0, 1, 1, 0, 1, 1, 0, 1], dtype=int64)
Vemos que se ha predicho un resultado de 0 (el pasajero no sobrevive) para aquellas muestras (para aquellos pasajeros) cuyos scores son menores que cero.
Pues bien, este clasificador con el threshold por defecto, supone un cierto <em>True Positive Rate</em> y un cierto <em>False Positive Rate</em>. Calculémoslos:
confusion_matrix(prediction, y_test)
array([[47, 9],
[ 4, 30]], dtype=int64)
Si extraemos los valores:
tn, fp, fn, tp = confusion_matrix(prediction, y_test).flatten()
TPR = tp / (tp + fn)
TPR
0.8823529411764706
FPR = (fp / (fp + tn))
FPR
0.16071428571428573
Vamos a almacenar estos valores en sendas listas para poder acceder a ellos posteriormente.
TPRs = [TPR]
FPRs = [FPR]
Y aquí viene lo interesante: hemos dicho que se va a comparar cada score con el threshold. Si el valor es mayor que el threshold se asigna a la clase 1 (o principal), y si es menor, a la clase 0 (o secundaria). Veamos los scores, la predicción que estos scores suponen, y el "ground truth" (el valor real de la variable objetivo) añadiendo estos tres datos a un dataframe ordenando los scores de menor a mayor:
scores_df = pd.DataFrame({
"scores": scores,
"prediction": prediction,
"truth": y_test
})
scores_df.sort_values("scores", inplace = True)
Y mostremos ahora los scores que hay en torno al threshold de cero (se ha buscado manualmente su posición en el dataset):
scores_df.iloc[46:66]
Lógicamente, todas las predicciones correspondientes a scores mayores que 0 aparecen con una predicción de 1, y al revés: las predicciones correspondientes a scores menores que cero aparecen con una predicción de 0.
Vemos, sin embargo, que el verdadero valor de la variable objetivo ("truth") no coincide siempre con la predicción. Lo ideal sería que por encima del threshold solo hubiese muestras de la clase 1 (para tener un TPR de 1), así que ¿qué ocurriría si en lugar de un threshold de 0 considerásemos un threshold de 0.8? En la imagen anterior vemos que, moviendo el threshold hasta dicho valor, estaríamos pasando tres muestras cuyo valor verdadero es 0 al bloque de "predicción 0". Es decir, estaríamos mejorando el TPR de nuestro modelo (al disminuir el número de falsos negativos)... pero a costa de empeorar el FPR. Vamos a calcular estos valores. Para ello tenemos los scores, de forma que podemos realizar la predicción "a mano":
new_prediction = scores > 0.8
new_prediction
array([False, False, False, True, True, False, True, True, False,
True, False, True, False, True, True, False, False, False,
False, True, False, True, False, False, False, True, False,
False, True, False, False, True, False, False, False, False,
False, False, False, False, False, False, False, False, True,
False, False, True, False, False, False, False, True, False,
False, False, False, False, False, False, False, False, True,
False, False, False, True, False, True, True, True, False,
False, False, False, False, False, False, False, False, False,
False, True, False, False, True, True, False, True, False])
confusion_matrix(new_prediction, y_test)
array([[50, 15],
[ 1, 24]], dtype=int64)
tn, fp, fn, tp = confusion_matrix(new_prediction, y_test).flatten()
TPR = tp / (tp + fn)
TPR
0.96
FPR = (fp / (fp + tn))
FPR
0.23076923076923078
Tal y como sospechábamos, el TPR del modelo ha aumentado hasta el 96%, pero el FPR ha aumentado también (lo que significa que hay más falsos positivos que antes).
Almacenemos estos valores en las listas que habíamos creado:
TPRs.append(TPR)
FPRs.append(FPR)
Y, en el ejemplo visto, hemos aumentado el threshold hasta 0.8. Si -nuevamente, viendo los scores- disminuimos el threshold hasta -0.5, vemos que estaríamos "sacando" del bloque de muestras que recibe una predicción 0 varias muestras cuyo valor real es 1... pero también estaríamos moviendo varias muestras que ahora mismo están bien clasificadas. Es decir, estaríamos mejorando el FPR (disminuyendo el número de falsos positivos) a costa de empeorar el TPR:
new_prediction = scores > -0.5
new_prediction
array([False, False, False, True, True, False, True, True, True,
True, False, True, False, True, True, True, False, False,
False, True, False, True, False, False, True, True, False,
True, True, False, False, True, False, False, False, True,
False, False, False, False, True, False, False, False, True,
False, False, True, False, False, False, True, True, False,
True, False, False, False, False, False, False, True, True,
True, True, False, True, False, True, True, True, False,
True, False, False, True, False, True, False, False, False,
True, True, False, False, True, True, True, True, True])
confusion_matrix(new_prediction, y_test)
array([[43, 5],
[ 8, 34]], dtype=int64)
tn, fp, fn, tp = confusion_matrix(new_prediction, y_test).flatten()
TPR = tp / (tp + fn)
TPR
0.8095238095238095
FPR = (fp / (fp + tn))
FPR
0.10416666666666667
Volvamos a almacenar los valores calculados:
TPRs.append(TPR)
FPRs.append(FPR)
También ahora hemos confirmado nuestras sospechas: hemos mejorado el FPR (disminuyéndolo) pero a costa de empeorar el TPR.
Llevemos a la gráfica de TPR vs. FPR las tres parejas de valores encontrados:
t = ["0", "0.8", "-0.5"]
fig, ax = plt.subplots(figsize = (6, 6))
ax.set_xlabel("False Positive Rate", fontsize = 15)
ax.set_ylabel("True Positive Rate", fontsize = 15)
ax.set_xticks(np.linspace(0, 1, 11))
ax.set_yticks(np.linspace(0, 1, 11))
ax.set_xlim(0, 1)
ax.set_ylim(0, 1)
sns.scatterplot(x = FPRs, y = TPRs, s = 50);
for i in range(len(t)):
plt.text(FPRs[i] + 0.02, TPRs[i] - 0.03, t[i], color = "red", fontsize = 15)
...gráfica que muestra exactamente las variaciones que hemos visto.
Hemos probado tres valores del threshold. Vamos a probar todos los thresholds desde el valor mínimo del score hasta el valor máximo, a ver cuál es el resultado:
TPRs = []
FPRs = []
for threshold in np.sort(scores):
new_prediction = scores > threshold
tn, fp, fn, tp = confusion_matrix(y_test, new_prediction).flatten()
TPR = tp / (tp + fn)
FPR = fp / (fp + tn)
TPRs.append(TPR)
FPRs.append(FPR)
Ahora ya tenemos los TPR y FPR en las listas TPRs y FPRs. Mostrémoslas en una gráfica etiquetando cada punto con el threshold al que se corresponde:
fig, ax = plt.subplots(figsize = (10, 10))
ax.set_xlabel("False Positive Rate", fontsize = 15)
ax.set_ylabel("True Positive Rate", fontsize = 15)
ax.set_xticks(np.linspace(0, 1, 11))
ax.set_yticks(np.linspace(0, 1, 11))
sns.scatterplot(x = FPRs, y = TPRs, s = 40);
plt.plot([0, 1], [0, 1], 'k--')
for i in range(len(scores)):
score_i = np.sort(scores)[i]
if score_i > -0.65:
ax.text(FPRs[i] + 0.012, TPRs[i] - 0.004, round(score_i, 2), color = "red", fontsize = 12)
else:
if i % 4 == 0:
ax.text(FPRs[i] - 0.01, TPRs[i] - 0.03, round(score_i, 2), color = "red", fontsize = 12)
Lo que estamos viendo es exactamente la curva ROC del clasificador. Y nos da bastante información: en primer lugar nos dice que aumentando o disminuyendo lo suficiente el threshold podemos conseguir que nuestro algoritmo de clasificación alcance un TPR o un FPR casi perfecto. El único "pero" es que lo conseguiremos a costa de empeorar la otra métrica. Esto significa que, una vez creado el modelo, podemos configurarlo en función de nuestros intereses fijando un threshold u otro (para conseguir un mayor TPR o un FPR). Por otro lado, cuanto más se aproxime la gráfica al punto (0, 1), más se comportará como un clasificador ideal. En la gráfica anterior se muestra la línea diagonal que marca el comportamiento de los clasificadores que tienen tantos verdaderos positivos como falsos negativos, y tantos verdaderos negativos como falsos positivos.
', 'es') (Line: 118)
Drupal\filter\Element\ProcessedText::preRenderText(Array)
call_user_func_array(Array, Array) (Line: 101)
Drupal\Core\Render\Renderer->doTrustedCallback(Array, Array, 'Render #pre_render callbacks must be methods of a class that implements \Drupal\Core\Security\TrustedCallbackInterface or be an anonymous function. The callback was %s. See https://www.drupal.org/node/2966725', 'exception', 'Drupal\Core\Render\Element\RenderCallbackInterface') (Line: 788)
Drupal\Core\Render\Renderer->doCallback('#pre_render', Array, Array) (Line: 374)
Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204)
Drupal\Core\Render\Renderer->render(Array) (Line: 479)
Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 88)
__TwigTemplate_a7d6005c89ae729617b9a0c2bccb1776->doDisplay(Array, Array) (Line: 405)
Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378)
Twig\Template->display(Array, Array) (Line: 46)
__TwigTemplate_804f7948456cfe20e11a34c43439c7c2->doDisplay(Array, Array) (Line: 405)
Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378)
Twig\Template->display(Array, Array) (Line: 43)
__TwigTemplate_bd990293b89f3b78c69fe0ee2f7828b5->doDisplay(Array, Array) (Line: 405)
Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378)
Twig\Template->display(Array) (Line: 390)
Twig\Template->render(Array) (Line: 55)
twig_render_template('themes/contrib/classy/templates/field/field--text-with-summary.html.twig', Array) (Line: 384)
Drupal\Core\Theme\ThemeManager->render('field', Array) (Line: 433)
Drupal\Core\Render\Renderer->doRender(Array) (Line: 446)
Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204)
Drupal\Core\Render\Renderer->render(Array) (Line: 479)
Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 74)
__TwigTemplate_94047fbdba6937b76a4479dfa1763452->doDisplay(Array, Array) (Line: 405)
Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378)
Twig\Template->display(Array) (Line: 390)
Twig\Template->render(Array) (Line: 55)
twig_render_template('themes/custom/yg_aesthetic/templates/node.html.twig', Array) (Line: 384)
Drupal\Core\Theme\ThemeManager->render('node', Array) (Line: 433)
Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204)
Drupal\Core\Render\Renderer->render(Array, ) (Line: 242)
Drupal\Core\Render\MainContent\HtmlRenderer->Drupal\Core\Render\MainContent\{closure}() (Line: 580)
Drupal\Core\Render\Renderer->executeInRenderContext(Object, Object) (Line: 235)
Drupal\Core\Render\MainContent\HtmlRenderer->prepare(Array, Object, Object) (Line: 132)
Drupal\Core\Render\MainContent\HtmlRenderer->renderResponse(Array, Object, Object) (Line: 90)
Drupal\Core\EventSubscriber\MainContentViewSubscriber->onViewRenderArray(Object, 'kernel.view', Object)
call_user_func(Array, Object, 'kernel.view', Object) (Line: 142)
Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher->dispatch(Object, 'kernel.view') (Line: 174)
Symfony\Component\HttpKernel\HttpKernel->handleRaw(Object, 1) (Line: 81)
Symfony\Component\HttpKernel\HttpKernel->handle(Object, 1, 1) (Line: 58)
Drupal\Core\StackMiddleware\Session->handle(Object, 1, 1) (Line: 48)
Drupal\Core\StackMiddleware\KernelPreHandle->handle(Object, 1, 1) (Line: 191)
Drupal\page_cache\StackMiddleware\PageCache->fetch(Object, 1, 1) (Line: 128)
Drupal\page_cache\StackMiddleware\PageCache->lookup(Object, 1, 1) (Line: 82)
Drupal\page_cache\StackMiddleware\PageCache->handle(Object, 1, 1) (Line: 48)
Drupal\Core\StackMiddleware\ReverseProxyMiddleware->handle(Object, 1, 1) (Line: 51)
Drupal\Core\StackMiddleware\NegotiationMiddleware->handle(Object, 1, 1) (Line: 23)
Stack\StackedHttpKernel->handle(Object, 1, 1) (Line: 713)
Drupal\Core\DrupalKernel->handle(Object) (Line: 19)
- Deprecated function: Creation of dynamic property Drupal\mysql\Driver\Database\mysql\Select::$alterTags is deprecated in Drupal\Core\Database\Query\Select->addTag() (line 149 of core/lib/Drupal/Core/Database/Query/Select.php).
Drupal\Core\Database\Query\Select->addTag('entity_query') (Line: 147)
Drupal\Core\Entity\Query\Sql\Query->prepare() (Line: 80)
Drupal\Core\Entity\Query\Sql\Query->execute() (Line: 640)
Drupal\Core\Entity\EntityStorageBase->loadByProperties(Array) (Line: 63)
Drupal\Core\Entity\EntityRepository->loadEntityByUuid('file', '446f59ac-0e20-4ea8-9e05-2f5f79825544') (Line: 124)
Drupal\editor\Plugin\Filter\EditorFileReference->process('Para entender el concepto de <strong>curva ROC</strong> (<em>ROC curve</em>, <em>Receiver Operating Characteristic</em>) tenemos que entender previamente cómo funciona un algoritmo de clasificación lineal como la regresión logística: A la hora de realizar una predicción, el modelo asigna una puntuación (un "score") a cada muestra del conjunto de pruebas. Este score se compara con un cierto valor límite ("<em>threshold</em>"). Si, para una muestra, su score es mayor que el threshold, se asigna a la clase principal. Si es menor, se asigna a la clase secundaria.
Siguiendo con el ejemplo visto relativo al Titanic, ya habíamos realizado la predicción:
prediction = model.predict(X_test)
prediction
array([0, 0, 0, 1, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 0, 0, 1, 0, 1,
0, 0, 1, 1, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
1, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0,
1, 0, 1, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1,
1, 0], dtype=int64)
Podemos ver el score que el modelo ha asignado a cada muestra con el método <strong>decision_function</strong>:
scores = model.decision_function(X_test)
scores
array([-1.94301282, -2.36213546, -1.21444007, 2.47651727, 1.1959797 ,
-0.60290339, 2.29157802, 2.53608913, -0.41796414, 0.95146859,
-2.59631885, 1.41196199, -2.15257414, 1.6564731 , 2.71070066,
0.53234595, -2.36213546, -1.71670281, -2.36213546, 0.90989925,
-1.48251942, 2.29157802, -2.15257414, -0.60290339, 0.53234595,
2.47651727, -2.36213546, 0.53234595, 1.6564731 , -0.65922644,
-1.94301282, 1.41196199, -2.36213546, -0.60290339, -2.59631885,
-0.18378075, -2.36213546, -1.48251942, -1.48251942, -1.48251942,
-0.41796414, -1.94301282, -2.36213546, -1.42400139, 2.05739463,
-2.36213546, -2.36213546, 1.99782277, -1.48251942, -0.83708678,
-0.60290339, 0.06073036, 1.1777786 , -2.36213546, -0.18378075,
-2.19539732, -0.60290339, -1.13792094, -0.94530689, -2.86545203,
-1.94301282, 0.77685706, 2.47651727, -0.18378075, 0.77685706,
-2.15257414, 2.29157802, -1.23800831, 1.1777786 , 2.29157802,
1.1777786 , -0.83708678, -0.41796414, -2.36213546, -2.36213546,
0.22038959, -1.79447578, -0.18378075, -2.36213546, -1.06339678,
-2.38675753, 0.01208211, 1.1777786 , -2.36213546, -1.48251942,
2.29157802, 2.71070066, 0.53234595, 1.41196199, -0.41796414])
Por defecto, el threshold aplicado es 0. Podemos ver esto fácilmente viendo, del array anterior, qué valores son mayores que 0 y cuáles son menores. Hagamos esto solo para los 10 primeros valores del array anterior:
scores[:10] > 0
array([False, False, False, True, True, False, True, True, False, True])
...y comparemos este resultado con los 10 primeros valores de la predicción:
prediction[:10]
array([0, 0, 0, 1, 1, 0, 1, 1, 0, 1], dtype=int64)
Vemos que se ha predicho un resultado de 0 (el pasajero no sobrevive) para aquellas muestras (para aquellos pasajeros) cuyos scores son menores que cero.
Pues bien, este clasificador con el threshold por defecto, supone un cierto <em>True Positive Rate</em> y un cierto <em>False Positive Rate</em>. Calculémoslos:
confusion_matrix(prediction, y_test)
array([[47, 9],
[ 4, 30]], dtype=int64)
Si extraemos los valores:
tn, fp, fn, tp = confusion_matrix(prediction, y_test).flatten()
TPR = tp / (tp + fn)
TPR
0.8823529411764706
FPR = (fp / (fp + tn))
FPR
0.16071428571428573
Vamos a almacenar estos valores en sendas listas para poder acceder a ellos posteriormente.
TPRs = [TPR]
FPRs = [FPR]
Y aquí viene lo interesante: hemos dicho que se va a comparar cada score con el threshold. Si el valor es mayor que el threshold se asigna a la clase 1 (o principal), y si es menor, a la clase 0 (o secundaria). Veamos los scores, la predicción que estos scores suponen, y el "ground truth" (el valor real de la variable objetivo) añadiendo estos tres datos a un dataframe ordenando los scores de menor a mayor:
scores_df = pd.DataFrame({
"scores": scores,
"prediction": prediction,
"truth": y_test
})
scores_df.sort_values("scores", inplace = True)
Y mostremos ahora los scores que hay en torno al threshold de cero (se ha buscado manualmente su posición en el dataset):
scores_df.iloc[46:66]
Lógicamente, todas las predicciones correspondientes a scores mayores que 0 aparecen con una predicción de 1, y al revés: las predicciones correspondientes a scores menores que cero aparecen con una predicción de 0.
Vemos, sin embargo, que el verdadero valor de la variable objetivo ("truth") no coincide siempre con la predicción. Lo ideal sería que por encima del threshold solo hubiese muestras de la clase 1 (para tener un TPR de 1), así que ¿qué ocurriría si en lugar de un threshold de 0 considerásemos un threshold de 0.8? En la imagen anterior vemos que, moviendo el threshold hasta dicho valor, estaríamos pasando tres muestras cuyo valor verdadero es 0 al bloque de "predicción 0". Es decir, estaríamos mejorando el TPR de nuestro modelo (al disminuir el número de falsos negativos)... pero a costa de empeorar el FPR. Vamos a calcular estos valores. Para ello tenemos los scores, de forma que podemos realizar la predicción "a mano":
new_prediction = scores > 0.8
new_prediction
array([False, False, False, True, True, False, True, True, False,
True, False, True, False, True, True, False, False, False,
False, True, False, True, False, False, False, True, False,
False, True, False, False, True, False, False, False, False,
False, False, False, False, False, False, False, False, True,
False, False, True, False, False, False, False, True, False,
False, False, False, False, False, False, False, False, True,
False, False, False, True, False, True, True, True, False,
False, False, False, False, False, False, False, False, False,
False, True, False, False, True, True, False, True, False])
confusion_matrix(new_prediction, y_test)
array([[50, 15],
[ 1, 24]], dtype=int64)
tn, fp, fn, tp = confusion_matrix(new_prediction, y_test).flatten()
TPR = tp / (tp + fn)
TPR
0.96
FPR = (fp / (fp + tn))
FPR
0.23076923076923078
Tal y como sospechábamos, el TPR del modelo ha aumentado hasta el 96%, pero el FPR ha aumentado también (lo que significa que hay más falsos positivos que antes).
Almacenemos estos valores en las listas que habíamos creado:
TPRs.append(TPR)
FPRs.append(FPR)
Y, en el ejemplo visto, hemos aumentado el threshold hasta 0.8. Si -nuevamente, viendo los scores- disminuimos el threshold hasta -0.5, vemos que estaríamos "sacando" del bloque de muestras que recibe una predicción 0 varias muestras cuyo valor real es 1... pero también estaríamos moviendo varias muestras que ahora mismo están bien clasificadas. Es decir, estaríamos mejorando el FPR (disminuyendo el número de falsos positivos) a costa de empeorar el TPR:
new_prediction = scores > -0.5
new_prediction
array([False, False, False, True, True, False, True, True, True,
True, False, True, False, True, True, True, False, False,
False, True, False, True, False, False, True, True, False,
True, True, False, False, True, False, False, False, True,
False, False, False, False, True, False, False, False, True,
False, False, True, False, False, False, True, True, False,
True, False, False, False, False, False, False, True, True,
True, True, False, True, False, True, True, True, False,
True, False, False, True, False, True, False, False, False,
True, True, False, False, True, True, True, True, True])
confusion_matrix(new_prediction, y_test)
array([[43, 5],
[ 8, 34]], dtype=int64)
tn, fp, fn, tp = confusion_matrix(new_prediction, y_test).flatten()
TPR = tp / (tp + fn)
TPR
0.8095238095238095
FPR = (fp / (fp + tn))
FPR
0.10416666666666667
Volvamos a almacenar los valores calculados:
TPRs.append(TPR)
FPRs.append(FPR)
También ahora hemos confirmado nuestras sospechas: hemos mejorado el FPR (disminuyéndolo) pero a costa de empeorar el TPR.
Llevemos a la gráfica de TPR vs. FPR las tres parejas de valores encontrados:
t = ["0", "0.8", "-0.5"]
fig, ax = plt.subplots(figsize = (6, 6))
ax.set_xlabel("False Positive Rate", fontsize = 15)
ax.set_ylabel("True Positive Rate", fontsize = 15)
ax.set_xticks(np.linspace(0, 1, 11))
ax.set_yticks(np.linspace(0, 1, 11))
ax.set_xlim(0, 1)
ax.set_ylim(0, 1)
sns.scatterplot(x = FPRs, y = TPRs, s = 50);
for i in range(len(t)):
plt.text(FPRs[i] + 0.02, TPRs[i] - 0.03, t[i], color = "red", fontsize = 15)
...gráfica que muestra exactamente las variaciones que hemos visto.
Hemos probado tres valores del threshold. Vamos a probar todos los thresholds desde el valor mínimo del score hasta el valor máximo, a ver cuál es el resultado:
TPRs = []
FPRs = []
for threshold in np.sort(scores):
new_prediction = scores > threshold
tn, fp, fn, tp = confusion_matrix(y_test, new_prediction).flatten()
TPR = tp / (tp + fn)
FPR = fp / (fp + tn)
TPRs.append(TPR)
FPRs.append(FPR)
Ahora ya tenemos los TPR y FPR en las listas TPRs y FPRs. Mostrémoslas en una gráfica etiquetando cada punto con el threshold al que se corresponde:
fig, ax = plt.subplots(figsize = (10, 10))
ax.set_xlabel("False Positive Rate", fontsize = 15)
ax.set_ylabel("True Positive Rate", fontsize = 15)
ax.set_xticks(np.linspace(0, 1, 11))
ax.set_yticks(np.linspace(0, 1, 11))
sns.scatterplot(x = FPRs, y = TPRs, s = 40);
plt.plot([0, 1], [0, 1], 'k--')
for i in range(len(scores)):
score_i = np.sort(scores)[i]
if score_i > -0.65:
ax.text(FPRs[i] + 0.012, TPRs[i] - 0.004, round(score_i, 2), color = "red", fontsize = 12)
else:
if i % 4 == 0:
ax.text(FPRs[i] - 0.01, TPRs[i] - 0.03, round(score_i, 2), color = "red", fontsize = 12)
Lo que estamos viendo es exactamente la curva ROC del clasificador. Y nos da bastante información: en primer lugar nos dice que aumentando o disminuyendo lo suficiente el threshold podemos conseguir que nuestro algoritmo de clasificación alcance un TPR o un FPR casi perfecto. El único "pero" es que lo conseguiremos a costa de empeorar la otra métrica. Esto significa que, una vez creado el modelo, podemos configurarlo en función de nuestros intereses fijando un threshold u otro (para conseguir un mayor TPR o un FPR). Por otro lado, cuanto más se aproxime la gráfica al punto (0, 1), más se comportará como un clasificador ideal. En la gráfica anterior se muestra la línea diagonal que marca el comportamiento de los clasificadores que tienen tantos verdaderos positivos como falsos negativos, y tantos verdaderos negativos como falsos positivos.
', 'es') (Line: 118)
Drupal\filter\Element\ProcessedText::preRenderText(Array)
call_user_func_array(Array, Array) (Line: 101)
Drupal\Core\Render\Renderer->doTrustedCallback(Array, Array, 'Render #pre_render callbacks must be methods of a class that implements \Drupal\Core\Security\TrustedCallbackInterface or be an anonymous function. The callback was %s. See https://www.drupal.org/node/2966725', 'exception', 'Drupal\Core\Render\Element\RenderCallbackInterface') (Line: 788)
Drupal\Core\Render\Renderer->doCallback('#pre_render', Array, Array) (Line: 374)
Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204)
Drupal\Core\Render\Renderer->render(Array) (Line: 479)
Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 88)
__TwigTemplate_a7d6005c89ae729617b9a0c2bccb1776->doDisplay(Array, Array) (Line: 405)
Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378)
Twig\Template->display(Array, Array) (Line: 46)
__TwigTemplate_804f7948456cfe20e11a34c43439c7c2->doDisplay(Array, Array) (Line: 405)
Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378)
Twig\Template->display(Array, Array) (Line: 43)
__TwigTemplate_bd990293b89f3b78c69fe0ee2f7828b5->doDisplay(Array, Array) (Line: 405)
Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378)
Twig\Template->display(Array) (Line: 390)
Twig\Template->render(Array) (Line: 55)
twig_render_template('themes/contrib/classy/templates/field/field--text-with-summary.html.twig', Array) (Line: 384)
Drupal\Core\Theme\ThemeManager->render('field', Array) (Line: 433)
Drupal\Core\Render\Renderer->doRender(Array) (Line: 446)
Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204)
Drupal\Core\Render\Renderer->render(Array) (Line: 479)
Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 74)
__TwigTemplate_94047fbdba6937b76a4479dfa1763452->doDisplay(Array, Array) (Line: 405)
Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378)
Twig\Template->display(Array) (Line: 390)
Twig\Template->render(Array) (Line: 55)
twig_render_template('themes/custom/yg_aesthetic/templates/node.html.twig', Array) (Line: 384)
Drupal\Core\Theme\ThemeManager->render('node', Array) (Line: 433)
Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204)
Drupal\Core\Render\Renderer->render(Array, ) (Line: 242)
Drupal\Core\Render\MainContent\HtmlRenderer->Drupal\Core\Render\MainContent\{closure}() (Line: 580)
Drupal\Core\Render\Renderer->executeInRenderContext(Object, Object) (Line: 235)
Drupal\Core\Render\MainContent\HtmlRenderer->prepare(Array, Object, Object) (Line: 132)
Drupal\Core\Render\MainContent\HtmlRenderer->renderResponse(Array, Object, Object) (Line: 90)
Drupal\Core\EventSubscriber\MainContentViewSubscriber->onViewRenderArray(Object, 'kernel.view', Object)
call_user_func(Array, Object, 'kernel.view', Object) (Line: 142)
Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher->dispatch(Object, 'kernel.view') (Line: 174)
Symfony\Component\HttpKernel\HttpKernel->handleRaw(Object, 1) (Line: 81)
Symfony\Component\HttpKernel\HttpKernel->handle(Object, 1, 1) (Line: 58)
Drupal\Core\StackMiddleware\Session->handle(Object, 1, 1) (Line: 48)
Drupal\Core\StackMiddleware\KernelPreHandle->handle(Object, 1, 1) (Line: 191)
Drupal\page_cache\StackMiddleware\PageCache->fetch(Object, 1, 1) (Line: 128)
Drupal\page_cache\StackMiddleware\PageCache->lookup(Object, 1, 1) (Line: 82)
Drupal\page_cache\StackMiddleware\PageCache->handle(Object, 1, 1) (Line: 48)
Drupal\Core\StackMiddleware\ReverseProxyMiddleware->handle(Object, 1, 1) (Line: 51)
Drupal\Core\StackMiddleware\NegotiationMiddleware->handle(Object, 1, 1) (Line: 23)
Stack\StackedHttpKernel->handle(Object, 1, 1) (Line: 713)
Drupal\Core\DrupalKernel->handle(Object) (Line: 19)
- Deprecated function: Creation of dynamic property Drupal\mysql\Driver\Database\mysql\Select::$alterMetaData is deprecated in Drupal\Core\Database\Query\Select->addMetaData() (line 178 of core/lib/Drupal/Core/Database/Query/Select.php).
Drupal\Core\Database\Query\Select->addMetaData('entity_type', 'file') (Line: 115)
Drupal\Core\Entity\Query\Sql\Query->prepare() (Line: 80)
Drupal\Core\Entity\Query\Sql\Query->execute() (Line: 640)
Drupal\Core\Entity\EntityStorageBase->loadByProperties(Array) (Line: 63)
Drupal\Core\Entity\EntityRepository->loadEntityByUuid('file', 'fd14ca8d-6599-4034-842a-45890b045f31') (Line: 95)
Drupal\editor\Plugin\Filter\EditorFileReference->process('Para entender el concepto de <strong>curva ROC</strong> (<em>ROC curve</em>, <em>Receiver Operating Characteristic</em>) tenemos que entender previamente cómo funciona un algoritmo de clasificación lineal como la regresión logística: A la hora de realizar una predicción, el modelo asigna una puntuación (un "score") a cada muestra del conjunto de pruebas. Este score se compara con un cierto valor límite ("<em>threshold</em>"). Si, para una muestra, su score es mayor que el threshold, se asigna a la clase principal. Si es menor, se asigna a la clase secundaria.
Siguiendo con el ejemplo visto relativo al Titanic, ya habíamos realizado la predicción:
prediction = model.predict(X_test)
prediction
array([0, 0, 0, 1, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 0, 0, 1, 0, 1,
0, 0, 1, 1, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
1, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0,
1, 0, 1, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1,
1, 0], dtype=int64)
Podemos ver el score que el modelo ha asignado a cada muestra con el método <strong>decision_function</strong>:
scores = model.decision_function(X_test)
scores
array([-1.94301282, -2.36213546, -1.21444007, 2.47651727, 1.1959797 ,
-0.60290339, 2.29157802, 2.53608913, -0.41796414, 0.95146859,
-2.59631885, 1.41196199, -2.15257414, 1.6564731 , 2.71070066,
0.53234595, -2.36213546, -1.71670281, -2.36213546, 0.90989925,
-1.48251942, 2.29157802, -2.15257414, -0.60290339, 0.53234595,
2.47651727, -2.36213546, 0.53234595, 1.6564731 , -0.65922644,
-1.94301282, 1.41196199, -2.36213546, -0.60290339, -2.59631885,
-0.18378075, -2.36213546, -1.48251942, -1.48251942, -1.48251942,
-0.41796414, -1.94301282, -2.36213546, -1.42400139, 2.05739463,
-2.36213546, -2.36213546, 1.99782277, -1.48251942, -0.83708678,
-0.60290339, 0.06073036, 1.1777786 , -2.36213546, -0.18378075,
-2.19539732, -0.60290339, -1.13792094, -0.94530689, -2.86545203,
-1.94301282, 0.77685706, 2.47651727, -0.18378075, 0.77685706,
-2.15257414, 2.29157802, -1.23800831, 1.1777786 , 2.29157802,
1.1777786 , -0.83708678, -0.41796414, -2.36213546, -2.36213546,
0.22038959, -1.79447578, -0.18378075, -2.36213546, -1.06339678,
-2.38675753, 0.01208211, 1.1777786 , -2.36213546, -1.48251942,
2.29157802, 2.71070066, 0.53234595, 1.41196199, -0.41796414])
Por defecto, el threshold aplicado es 0. Podemos ver esto fácilmente viendo, del array anterior, qué valores son mayores que 0 y cuáles son menores. Hagamos esto solo para los 10 primeros valores del array anterior:
scores[:10] > 0
array([False, False, False, True, True, False, True, True, False, True])
...y comparemos este resultado con los 10 primeros valores de la predicción:
prediction[:10]
array([0, 0, 0, 1, 1, 0, 1, 1, 0, 1], dtype=int64)
Vemos que se ha predicho un resultado de 0 (el pasajero no sobrevive) para aquellas muestras (para aquellos pasajeros) cuyos scores son menores que cero.
Pues bien, este clasificador con el threshold por defecto, supone un cierto <em>True Positive Rate</em> y un cierto <em>False Positive Rate</em>. Calculémoslos:
confusion_matrix(prediction, y_test)
array([[47, 9],
[ 4, 30]], dtype=int64)
Si extraemos los valores:
tn, fp, fn, tp = confusion_matrix(prediction, y_test).flatten()
TPR = tp / (tp + fn)
TPR
0.8823529411764706
FPR = (fp / (fp + tn))
FPR
0.16071428571428573
Vamos a almacenar estos valores en sendas listas para poder acceder a ellos posteriormente.
TPRs = [TPR]
FPRs = [FPR]
Y aquí viene lo interesante: hemos dicho que se va a comparar cada score con el threshold. Si el valor es mayor que el threshold se asigna a la clase 1 (o principal), y si es menor, a la clase 0 (o secundaria). Veamos los scores, la predicción que estos scores suponen, y el "ground truth" (el valor real de la variable objetivo) añadiendo estos tres datos a un dataframe ordenando los scores de menor a mayor:
scores_df = pd.DataFrame({
"scores": scores,
"prediction": prediction,
"truth": y_test
})
scores_df.sort_values("scores", inplace = True)
Y mostremos ahora los scores que hay en torno al threshold de cero (se ha buscado manualmente su posición en el dataset):
scores_df.iloc[46:66]
Lógicamente, todas las predicciones correspondientes a scores mayores que 0 aparecen con una predicción de 1, y al revés: las predicciones correspondientes a scores menores que cero aparecen con una predicción de 0.
Vemos, sin embargo, que el verdadero valor de la variable objetivo ("truth") no coincide siempre con la predicción. Lo ideal sería que por encima del threshold solo hubiese muestras de la clase 1 (para tener un TPR de 1), así que ¿qué ocurriría si en lugar de un threshold de 0 considerásemos un threshold de 0.8? En la imagen anterior vemos que, moviendo el threshold hasta dicho valor, estaríamos pasando tres muestras cuyo valor verdadero es 0 al bloque de "predicción 0". Es decir, estaríamos mejorando el TPR de nuestro modelo (al disminuir el número de falsos negativos)... pero a costa de empeorar el FPR. Vamos a calcular estos valores. Para ello tenemos los scores, de forma que podemos realizar la predicción "a mano":
new_prediction = scores > 0.8
new_prediction
array([False, False, False, True, True, False, True, True, False,
True, False, True, False, True, True, False, False, False,
False, True, False, True, False, False, False, True, False,
False, True, False, False, True, False, False, False, False,
False, False, False, False, False, False, False, False, True,
False, False, True, False, False, False, False, True, False,
False, False, False, False, False, False, False, False, True,
False, False, False, True, False, True, True, True, False,
False, False, False, False, False, False, False, False, False,
False, True, False, False, True, True, False, True, False])
confusion_matrix(new_prediction, y_test)
array([[50, 15],
[ 1, 24]], dtype=int64)
tn, fp, fn, tp = confusion_matrix(new_prediction, y_test).flatten()
TPR = tp / (tp + fn)
TPR
0.96
FPR = (fp / (fp + tn))
FPR
0.23076923076923078
Tal y como sospechábamos, el TPR del modelo ha aumentado hasta el 96%, pero el FPR ha aumentado también (lo que significa que hay más falsos positivos que antes).
Almacenemos estos valores en las listas que habíamos creado:
TPRs.append(TPR)
FPRs.append(FPR)
Y, en el ejemplo visto, hemos aumentado el threshold hasta 0.8. Si -nuevamente, viendo los scores- disminuimos el threshold hasta -0.5, vemos que estaríamos "sacando" del bloque de muestras que recibe una predicción 0 varias muestras cuyo valor real es 1... pero también estaríamos moviendo varias muestras que ahora mismo están bien clasificadas. Es decir, estaríamos mejorando el FPR (disminuyendo el número de falsos positivos) a costa de empeorar el TPR:
new_prediction = scores > -0.5
new_prediction
array([False, False, False, True, True, False, True, True, True,
True, False, True, False, True, True, True, False, False,
False, True, False, True, False, False, True, True, False,
True, True, False, False, True, False, False, False, True,
False, False, False, False, True, False, False, False, True,
False, False, True, False, False, False, True, True, False,
True, False, False, False, False, False, False, True, True,
True, True, False, True, False, True, True, True, False,
True, False, False, True, False, True, False, False, False,
True, True, False, False, True, True, True, True, True])
confusion_matrix(new_prediction, y_test)
array([[43, 5],
[ 8, 34]], dtype=int64)
tn, fp, fn, tp = confusion_matrix(new_prediction, y_test).flatten()
TPR = tp / (tp + fn)
TPR
0.8095238095238095
FPR = (fp / (fp + tn))
FPR
0.10416666666666667
Volvamos a almacenar los valores calculados:
TPRs.append(TPR)
FPRs.append(FPR)
También ahora hemos confirmado nuestras sospechas: hemos mejorado el FPR (disminuyéndolo) pero a costa de empeorar el TPR.
Llevemos a la gráfica de TPR vs. FPR las tres parejas de valores encontrados:
t = ["0", "0.8", "-0.5"]
fig, ax = plt.subplots(figsize = (6, 6))
ax.set_xlabel("False Positive Rate", fontsize = 15)
ax.set_ylabel("True Positive Rate", fontsize = 15)
ax.set_xticks(np.linspace(0, 1, 11))
ax.set_yticks(np.linspace(0, 1, 11))
ax.set_xlim(0, 1)
ax.set_ylim(0, 1)
sns.scatterplot(x = FPRs, y = TPRs, s = 50);
for i in range(len(t)):
plt.text(FPRs[i] + 0.02, TPRs[i] - 0.03, t[i], color = "red", fontsize = 15)
...gráfica que muestra exactamente las variaciones que hemos visto.
Hemos probado tres valores del threshold. Vamos a probar todos los thresholds desde el valor mínimo del score hasta el valor máximo, a ver cuál es el resultado:
TPRs = []
FPRs = []
for threshold in np.sort(scores):
new_prediction = scores > threshold
tn, fp, fn, tp = confusion_matrix(y_test, new_prediction).flatten()
TPR = tp / (tp + fn)
FPR = fp / (fp + tn)
TPRs.append(TPR)
FPRs.append(FPR)
Ahora ya tenemos los TPR y FPR en las listas TPRs y FPRs. Mostrémoslas en una gráfica etiquetando cada punto con el threshold al que se corresponde:
fig, ax = plt.subplots(figsize = (10, 10))
ax.set_xlabel("False Positive Rate", fontsize = 15)
ax.set_ylabel("True Positive Rate", fontsize = 15)
ax.set_xticks(np.linspace(0, 1, 11))
ax.set_yticks(np.linspace(0, 1, 11))
sns.scatterplot(x = FPRs, y = TPRs, s = 40);
plt.plot([0, 1], [0, 1], 'k--')
for i in range(len(scores)):
score_i = np.sort(scores)[i]
if score_i > -0.65:
ax.text(FPRs[i] + 0.012, TPRs[i] - 0.004, round(score_i, 2), color = "red", fontsize = 12)
else:
if i % 4 == 0:
ax.text(FPRs[i] - 0.01, TPRs[i] - 0.03, round(score_i, 2), color = "red", fontsize = 12)
Lo que estamos viendo es exactamente la curva ROC del clasificador. Y nos da bastante información: en primer lugar nos dice que aumentando o disminuyendo lo suficiente el threshold podemos conseguir que nuestro algoritmo de clasificación alcance un TPR o un FPR casi perfecto. El único "pero" es que lo conseguiremos a costa de empeorar la otra métrica. Esto significa que, una vez creado el modelo, podemos configurarlo en función de nuestros intereses fijando un threshold u otro (para conseguir un mayor TPR o un FPR). Por otro lado, cuanto más se aproxime la gráfica al punto (0, 1), más se comportará como un clasificador ideal. En la gráfica anterior se muestra la línea diagonal que marca el comportamiento de los clasificadores que tienen tantos verdaderos positivos como falsos negativos, y tantos verdaderos negativos como falsos positivos.
', 'es') (Line: 118)
Drupal\filter\Element\ProcessedText::preRenderText(Array)
call_user_func_array(Array, Array) (Line: 101)
Drupal\Core\Render\Renderer->doTrustedCallback(Array, Array, 'Render #pre_render callbacks must be methods of a class that implements \Drupal\Core\Security\TrustedCallbackInterface or be an anonymous function. The callback was %s. See https://www.drupal.org/node/2966725', 'exception', 'Drupal\Core\Render\Element\RenderCallbackInterface') (Line: 788)
Drupal\Core\Render\Renderer->doCallback('#pre_render', Array, Array) (Line: 374)
Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204)
Drupal\Core\Render\Renderer->render(Array) (Line: 479)
Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 88)
__TwigTemplate_a7d6005c89ae729617b9a0c2bccb1776->doDisplay(Array, Array) (Line: 405)
Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378)
Twig\Template->display(Array, Array) (Line: 46)
__TwigTemplate_804f7948456cfe20e11a34c43439c7c2->doDisplay(Array, Array) (Line: 405)
Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378)
Twig\Template->display(Array, Array) (Line: 43)
__TwigTemplate_bd990293b89f3b78c69fe0ee2f7828b5->doDisplay(Array, Array) (Line: 405)
Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378)
Twig\Template->display(Array) (Line: 390)
Twig\Template->render(Array) (Line: 55)
twig_render_template('themes/contrib/classy/templates/field/field--text-with-summary.html.twig', Array) (Line: 384)
Drupal\Core\Theme\ThemeManager->render('field', Array) (Line: 433)
Drupal\Core\Render\Renderer->doRender(Array) (Line: 446)
Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204)
Drupal\Core\Render\Renderer->render(Array) (Line: 479)
Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 74)
__TwigTemplate_94047fbdba6937b76a4479dfa1763452->doDisplay(Array, Array) (Line: 405)
Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378)
Twig\Template->display(Array) (Line: 390)
Twig\Template->render(Array) (Line: 55)
twig_render_template('themes/custom/yg_aesthetic/templates/node.html.twig', Array) (Line: 384)
Drupal\Core\Theme\ThemeManager->render('node', Array) (Line: 433)
Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204)
Drupal\Core\Render\Renderer->render(Array, ) (Line: 242)
Drupal\Core\Render\MainContent\HtmlRenderer->Drupal\Core\Render\MainContent\{closure}() (Line: 580)
Drupal\Core\Render\Renderer->executeInRenderContext(Object, Object) (Line: 235)
Drupal\Core\Render\MainContent\HtmlRenderer->prepare(Array, Object, Object) (Line: 132)
Drupal\Core\Render\MainContent\HtmlRenderer->renderResponse(Array, Object, Object) (Line: 90)
Drupal\Core\EventSubscriber\MainContentViewSubscriber->onViewRenderArray(Object, 'kernel.view', Object)
call_user_func(Array, Object, 'kernel.view', Object) (Line: 142)
Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher->dispatch(Object, 'kernel.view') (Line: 174)
Symfony\Component\HttpKernel\HttpKernel->handleRaw(Object, 1) (Line: 81)
Symfony\Component\HttpKernel\HttpKernel->handle(Object, 1, 1) (Line: 58)
Drupal\Core\StackMiddleware\Session->handle(Object, 1, 1) (Line: 48)
Drupal\Core\StackMiddleware\KernelPreHandle->handle(Object, 1, 1) (Line: 191)
Drupal\page_cache\StackMiddleware\PageCache->fetch(Object, 1, 1) (Line: 128)
Drupal\page_cache\StackMiddleware\PageCache->lookup(Object, 1, 1) (Line: 82)
Drupal\page_cache\StackMiddleware\PageCache->handle(Object, 1, 1) (Line: 48)
Drupal\Core\StackMiddleware\ReverseProxyMiddleware->handle(Object, 1, 1) (Line: 51)
Drupal\Core\StackMiddleware\NegotiationMiddleware->handle(Object, 1, 1) (Line: 23)
Stack\StackedHttpKernel->handle(Object, 1, 1) (Line: 713)
Drupal\Core\DrupalKernel->handle(Object) (Line: 19)
- Deprecated function: Creation of dynamic property Drupal\mysql\Driver\Database\mysql\Select::$alterTags is deprecated in Drupal\Core\Database\Query\Select->addTag() (line 149 of core/lib/Drupal/Core/Database/Query/Select.php).
Drupal\Core\Database\Query\Select->addTag('entity_query') (Line: 147)
Drupal\Core\Entity\Query\Sql\Query->prepare() (Line: 80)
Drupal\Core\Entity\Query\Sql\Query->execute() (Line: 640)
Drupal\Core\Entity\EntityStorageBase->loadByProperties(Array) (Line: 63)
Drupal\Core\Entity\EntityRepository->loadEntityByUuid('file', 'fd14ca8d-6599-4034-842a-45890b045f31') (Line: 95)
Drupal\editor\Plugin\Filter\EditorFileReference->process('Para entender el concepto de <strong>curva ROC</strong> (<em>ROC curve</em>, <em>Receiver Operating Characteristic</em>) tenemos que entender previamente cómo funciona un algoritmo de clasificación lineal como la regresión logística: A la hora de realizar una predicción, el modelo asigna una puntuación (un "score") a cada muestra del conjunto de pruebas. Este score se compara con un cierto valor límite ("<em>threshold</em>"). Si, para una muestra, su score es mayor que el threshold, se asigna a la clase principal. Si es menor, se asigna a la clase secundaria.
Siguiendo con el ejemplo visto relativo al Titanic, ya habíamos realizado la predicción:
prediction = model.predict(X_test)
prediction
array([0, 0, 0, 1, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 0, 0, 1, 0, 1,
0, 0, 1, 1, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
1, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0,
1, 0, 1, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1,
1, 0], dtype=int64)
Podemos ver el score que el modelo ha asignado a cada muestra con el método <strong>decision_function</strong>:
scores = model.decision_function(X_test)
scores
array([-1.94301282, -2.36213546, -1.21444007, 2.47651727, 1.1959797 ,
-0.60290339, 2.29157802, 2.53608913, -0.41796414, 0.95146859,
-2.59631885, 1.41196199, -2.15257414, 1.6564731 , 2.71070066,
0.53234595, -2.36213546, -1.71670281, -2.36213546, 0.90989925,
-1.48251942, 2.29157802, -2.15257414, -0.60290339, 0.53234595,
2.47651727, -2.36213546, 0.53234595, 1.6564731 , -0.65922644,
-1.94301282, 1.41196199, -2.36213546, -0.60290339, -2.59631885,
-0.18378075, -2.36213546, -1.48251942, -1.48251942, -1.48251942,
-0.41796414, -1.94301282, -2.36213546, -1.42400139, 2.05739463,
-2.36213546, -2.36213546, 1.99782277, -1.48251942, -0.83708678,
-0.60290339, 0.06073036, 1.1777786 , -2.36213546, -0.18378075,
-2.19539732, -0.60290339, -1.13792094, -0.94530689, -2.86545203,
-1.94301282, 0.77685706, 2.47651727, -0.18378075, 0.77685706,
-2.15257414, 2.29157802, -1.23800831, 1.1777786 , 2.29157802,
1.1777786 , -0.83708678, -0.41796414, -2.36213546, -2.36213546,
0.22038959, -1.79447578, -0.18378075, -2.36213546, -1.06339678,
-2.38675753, 0.01208211, 1.1777786 , -2.36213546, -1.48251942,
2.29157802, 2.71070066, 0.53234595, 1.41196199, -0.41796414])
Por defecto, el threshold aplicado es 0. Podemos ver esto fácilmente viendo, del array anterior, qué valores son mayores que 0 y cuáles son menores. Hagamos esto solo para los 10 primeros valores del array anterior:
scores[:10] > 0
array([False, False, False, True, True, False, True, True, False, True])
...y comparemos este resultado con los 10 primeros valores de la predicción:
prediction[:10]
array([0, 0, 0, 1, 1, 0, 1, 1, 0, 1], dtype=int64)
Vemos que se ha predicho un resultado de 0 (el pasajero no sobrevive) para aquellas muestras (para aquellos pasajeros) cuyos scores son menores que cero.
Pues bien, este clasificador con el threshold por defecto, supone un cierto <em>True Positive Rate</em> y un cierto <em>False Positive Rate</em>. Calculémoslos:
confusion_matrix(prediction, y_test)
array([[47, 9],
[ 4, 30]], dtype=int64)
Si extraemos los valores:
tn, fp, fn, tp = confusion_matrix(prediction, y_test).flatten()
TPR = tp / (tp + fn)
TPR
0.8823529411764706
FPR = (fp / (fp + tn))
FPR
0.16071428571428573
Vamos a almacenar estos valores en sendas listas para poder acceder a ellos posteriormente.
TPRs = [TPR]
FPRs = [FPR]
Y aquí viene lo interesante: hemos dicho que se va a comparar cada score con el threshold. Si el valor es mayor que el threshold se asigna a la clase 1 (o principal), y si es menor, a la clase 0 (o secundaria). Veamos los scores, la predicción que estos scores suponen, y el "ground truth" (el valor real de la variable objetivo) añadiendo estos tres datos a un dataframe ordenando los scores de menor a mayor:
scores_df = pd.DataFrame({
"scores": scores,
"prediction": prediction,
"truth": y_test
})
scores_df.sort_values("scores", inplace = True)
Y mostremos ahora los scores que hay en torno al threshold de cero (se ha buscado manualmente su posición en el dataset):
scores_df.iloc[46:66]
Lógicamente, todas las predicciones correspondientes a scores mayores que 0 aparecen con una predicción de 1, y al revés: las predicciones correspondientes a scores menores que cero aparecen con una predicción de 0.
Vemos, sin embargo, que el verdadero valor de la variable objetivo ("truth") no coincide siempre con la predicción. Lo ideal sería que por encima del threshold solo hubiese muestras de la clase 1 (para tener un TPR de 1), así que ¿qué ocurriría si en lugar de un threshold de 0 considerásemos un threshold de 0.8? En la imagen anterior vemos que, moviendo el threshold hasta dicho valor, estaríamos pasando tres muestras cuyo valor verdadero es 0 al bloque de "predicción 0". Es decir, estaríamos mejorando el TPR de nuestro modelo (al disminuir el número de falsos negativos)... pero a costa de empeorar el FPR. Vamos a calcular estos valores. Para ello tenemos los scores, de forma que podemos realizar la predicción "a mano":
new_prediction = scores > 0.8
new_prediction
array([False, False, False, True, True, False, True, True, False,
True, False, True, False, True, True, False, False, False,
False, True, False, True, False, False, False, True, False,
False, True, False, False, True, False, False, False, False,
False, False, False, False, False, False, False, False, True,
False, False, True, False, False, False, False, True, False,
False, False, False, False, False, False, False, False, True,
False, False, False, True, False, True, True, True, False,
False, False, False, False, False, False, False, False, False,
False, True, False, False, True, True, False, True, False])
confusion_matrix(new_prediction, y_test)
array([[50, 15],
[ 1, 24]], dtype=int64)
tn, fp, fn, tp = confusion_matrix(new_prediction, y_test).flatten()
TPR = tp / (tp + fn)
TPR
0.96
FPR = (fp / (fp + tn))
FPR
0.23076923076923078
Tal y como sospechábamos, el TPR del modelo ha aumentado hasta el 96%, pero el FPR ha aumentado también (lo que significa que hay más falsos positivos que antes).
Almacenemos estos valores en las listas que habíamos creado:
TPRs.append(TPR)
FPRs.append(FPR)
Y, en el ejemplo visto, hemos aumentado el threshold hasta 0.8. Si -nuevamente, viendo los scores- disminuimos el threshold hasta -0.5, vemos que estaríamos "sacando" del bloque de muestras que recibe una predicción 0 varias muestras cuyo valor real es 1... pero también estaríamos moviendo varias muestras que ahora mismo están bien clasificadas. Es decir, estaríamos mejorando el FPR (disminuyendo el número de falsos positivos) a costa de empeorar el TPR:
new_prediction = scores > -0.5
new_prediction
array([False, False, False, True, True, False, True, True, True,
True, False, True, False, True, True, True, False, False,
False, True, False, True, False, False, True, True, False,
True, True, False, False, True, False, False, False, True,
False, False, False, False, True, False, False, False, True,
False, False, True, False, False, False, True, True, False,
True, False, False, False, False, False, False, True, True,
True, True, False, True, False, True, True, True, False,
True, False, False, True, False, True, False, False, False,
True, True, False, False, True, True, True, True, True])
confusion_matrix(new_prediction, y_test)
array([[43, 5],
[ 8, 34]], dtype=int64)
tn, fp, fn, tp = confusion_matrix(new_prediction, y_test).flatten()
TPR = tp / (tp + fn)
TPR
0.8095238095238095
FPR = (fp / (fp + tn))
FPR
0.10416666666666667
Volvamos a almacenar los valores calculados:
TPRs.append(TPR)
FPRs.append(FPR)
También ahora hemos confirmado nuestras sospechas: hemos mejorado el FPR (disminuyéndolo) pero a costa de empeorar el TPR.
Llevemos a la gráfica de TPR vs. FPR las tres parejas de valores encontrados:
t = ["0", "0.8", "-0.5"]
fig, ax = plt.subplots(figsize = (6, 6))
ax.set_xlabel("False Positive Rate", fontsize = 15)
ax.set_ylabel("True Positive Rate", fontsize = 15)
ax.set_xticks(np.linspace(0, 1, 11))
ax.set_yticks(np.linspace(0, 1, 11))
ax.set_xlim(0, 1)
ax.set_ylim(0, 1)
sns.scatterplot(x = FPRs, y = TPRs, s = 50);
for i in range(len(t)):
plt.text(FPRs[i] + 0.02, TPRs[i] - 0.03, t[i], color = "red", fontsize = 15)
...gráfica que muestra exactamente las variaciones que hemos visto.
Hemos probado tres valores del threshold. Vamos a probar todos los thresholds desde el valor mínimo del score hasta el valor máximo, a ver cuál es el resultado:
TPRs = []
FPRs = []
for threshold in np.sort(scores):
new_prediction = scores > threshold
tn, fp, fn, tp = confusion_matrix(y_test, new_prediction).flatten()
TPR = tp / (tp + fn)
FPR = fp / (fp + tn)
TPRs.append(TPR)
FPRs.append(FPR)
Ahora ya tenemos los TPR y FPR en las listas TPRs y FPRs. Mostrémoslas en una gráfica etiquetando cada punto con el threshold al que se corresponde:
fig, ax = plt.subplots(figsize = (10, 10))
ax.set_xlabel("False Positive Rate", fontsize = 15)
ax.set_ylabel("True Positive Rate", fontsize = 15)
ax.set_xticks(np.linspace(0, 1, 11))
ax.set_yticks(np.linspace(0, 1, 11))
sns.scatterplot(x = FPRs, y = TPRs, s = 40);
plt.plot([0, 1], [0, 1], 'k--')
for i in range(len(scores)):
score_i = np.sort(scores)[i]
if score_i > -0.65:
ax.text(FPRs[i] + 0.012, TPRs[i] - 0.004, round(score_i, 2), color = "red", fontsize = 12)
else:
if i % 4 == 0:
ax.text(FPRs[i] - 0.01, TPRs[i] - 0.03, round(score_i, 2), color = "red", fontsize = 12)
Lo que estamos viendo es exactamente la curva ROC del clasificador. Y nos da bastante información: en primer lugar nos dice que aumentando o disminuyendo lo suficiente el threshold podemos conseguir que nuestro algoritmo de clasificación alcance un TPR o un FPR casi perfecto. El único "pero" es que lo conseguiremos a costa de empeorar la otra métrica. Esto significa que, una vez creado el modelo, podemos configurarlo en función de nuestros intereses fijando un threshold u otro (para conseguir un mayor TPR o un FPR). Por otro lado, cuanto más se aproxime la gráfica al punto (0, 1), más se comportará como un clasificador ideal. En la gráfica anterior se muestra la línea diagonal que marca el comportamiento de los clasificadores que tienen tantos verdaderos positivos como falsos negativos, y tantos verdaderos negativos como falsos positivos.
', 'es') (Line: 118)
Drupal\filter\Element\ProcessedText::preRenderText(Array)
call_user_func_array(Array, Array) (Line: 101)
Drupal\Core\Render\Renderer->doTrustedCallback(Array, Array, 'Render #pre_render callbacks must be methods of a class that implements \Drupal\Core\Security\TrustedCallbackInterface or be an anonymous function. The callback was %s. See https://www.drupal.org/node/2966725', 'exception', 'Drupal\Core\Render\Element\RenderCallbackInterface') (Line: 788)
Drupal\Core\Render\Renderer->doCallback('#pre_render', Array, Array) (Line: 374)
Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204)
Drupal\Core\Render\Renderer->render(Array) (Line: 479)
Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 88)
__TwigTemplate_a7d6005c89ae729617b9a0c2bccb1776->doDisplay(Array, Array) (Line: 405)
Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378)
Twig\Template->display(Array, Array) (Line: 46)
__TwigTemplate_804f7948456cfe20e11a34c43439c7c2->doDisplay(Array, Array) (Line: 405)
Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378)
Twig\Template->display(Array, Array) (Line: 43)
__TwigTemplate_bd990293b89f3b78c69fe0ee2f7828b5->doDisplay(Array, Array) (Line: 405)
Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378)
Twig\Template->display(Array) (Line: 390)
Twig\Template->render(Array) (Line: 55)
twig_render_template('themes/contrib/classy/templates/field/field--text-with-summary.html.twig', Array) (Line: 384)
Drupal\Core\Theme\ThemeManager->render('field', Array) (Line: 433)
Drupal\Core\Render\Renderer->doRender(Array) (Line: 446)
Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204)
Drupal\Core\Render\Renderer->render(Array) (Line: 479)
Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 74)
__TwigTemplate_94047fbdba6937b76a4479dfa1763452->doDisplay(Array, Array) (Line: 405)
Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378)
Twig\Template->display(Array) (Line: 390)
Twig\Template->render(Array) (Line: 55)
twig_render_template('themes/custom/yg_aesthetic/templates/node.html.twig', Array) (Line: 384)
Drupal\Core\Theme\ThemeManager->render('node', Array) (Line: 433)
Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204)
Drupal\Core\Render\Renderer->render(Array, ) (Line: 242)
Drupal\Core\Render\MainContent\HtmlRenderer->Drupal\Core\Render\MainContent\{closure}() (Line: 580)
Drupal\Core\Render\Renderer->executeInRenderContext(Object, Object) (Line: 235)
Drupal\Core\Render\MainContent\HtmlRenderer->prepare(Array, Object, Object) (Line: 132)
Drupal\Core\Render\MainContent\HtmlRenderer->renderResponse(Array, Object, Object) (Line: 90)
Drupal\Core\EventSubscriber\MainContentViewSubscriber->onViewRenderArray(Object, 'kernel.view', Object)
call_user_func(Array, Object, 'kernel.view', Object) (Line: 142)
Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher->dispatch(Object, 'kernel.view') (Line: 174)
Symfony\Component\HttpKernel\HttpKernel->handleRaw(Object, 1) (Line: 81)
Symfony\Component\HttpKernel\HttpKernel->handle(Object, 1, 1) (Line: 58)
Drupal\Core\StackMiddleware\Session->handle(Object, 1, 1) (Line: 48)
Drupal\Core\StackMiddleware\KernelPreHandle->handle(Object, 1, 1) (Line: 191)
Drupal\page_cache\StackMiddleware\PageCache->fetch(Object, 1, 1) (Line: 128)
Drupal\page_cache\StackMiddleware\PageCache->lookup(Object, 1, 1) (Line: 82)
Drupal\page_cache\StackMiddleware\PageCache->handle(Object, 1, 1) (Line: 48)
Drupal\Core\StackMiddleware\ReverseProxyMiddleware->handle(Object, 1, 1) (Line: 51)
Drupal\Core\StackMiddleware\NegotiationMiddleware->handle(Object, 1, 1) (Line: 23)
Stack\StackedHttpKernel->handle(Object, 1, 1) (Line: 713)
Drupal\Core\DrupalKernel->handle(Object) (Line: 19)
- Deprecated function: Creation of dynamic property Drupal\mysql\Driver\Database\mysql\Select::$alterMetaData is deprecated in Drupal\Core\Database\Query\Select->addMetaData() (line 178 of core/lib/Drupal/Core/Database/Query/Select.php).
Drupal\Core\Database\Query\Select->addMetaData('entity_type', 'file') (Line: 115)
Drupal\Core\Entity\Query\Sql\Query->prepare() (Line: 80)
Drupal\Core\Entity\Query\Sql\Query->execute() (Line: 640)
Drupal\Core\Entity\EntityStorageBase->loadByProperties(Array) (Line: 63)
Drupal\Core\Entity\EntityRepository->loadEntityByUuid('file', 'fd14ca8d-6599-4034-842a-45890b045f31') (Line: 124)
Drupal\editor\Plugin\Filter\EditorFileReference->process('Para entender el concepto de <strong>curva ROC</strong> (<em>ROC curve</em>, <em>Receiver Operating Characteristic</em>) tenemos que entender previamente cómo funciona un algoritmo de clasificación lineal como la regresión logística: A la hora de realizar una predicción, el modelo asigna una puntuación (un "score") a cada muestra del conjunto de pruebas. Este score se compara con un cierto valor límite ("<em>threshold</em>"). Si, para una muestra, su score es mayor que el threshold, se asigna a la clase principal. Si es menor, se asigna a la clase secundaria.
Siguiendo con el ejemplo visto relativo al Titanic, ya habíamos realizado la predicción:
prediction = model.predict(X_test)
prediction
array([0, 0, 0, 1, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 0, 0, 1, 0, 1,
0, 0, 1, 1, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
1, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0,
1, 0, 1, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1,
1, 0], dtype=int64)
Podemos ver el score que el modelo ha asignado a cada muestra con el método <strong>decision_function</strong>:
scores = model.decision_function(X_test)
scores
array([-1.94301282, -2.36213546, -1.21444007, 2.47651727, 1.1959797 ,
-0.60290339, 2.29157802, 2.53608913, -0.41796414, 0.95146859,
-2.59631885, 1.41196199, -2.15257414, 1.6564731 , 2.71070066,
0.53234595, -2.36213546, -1.71670281, -2.36213546, 0.90989925,
-1.48251942, 2.29157802, -2.15257414, -0.60290339, 0.53234595,
2.47651727, -2.36213546, 0.53234595, 1.6564731 , -0.65922644,
-1.94301282, 1.41196199, -2.36213546, -0.60290339, -2.59631885,
-0.18378075, -2.36213546, -1.48251942, -1.48251942, -1.48251942,
-0.41796414, -1.94301282, -2.36213546, -1.42400139, 2.05739463,
-2.36213546, -2.36213546, 1.99782277, -1.48251942, -0.83708678,
-0.60290339, 0.06073036, 1.1777786 , -2.36213546, -0.18378075,
-2.19539732, -0.60290339, -1.13792094, -0.94530689, -2.86545203,
-1.94301282, 0.77685706, 2.47651727, -0.18378075, 0.77685706,
-2.15257414, 2.29157802, -1.23800831, 1.1777786 , 2.29157802,
1.1777786 , -0.83708678, -0.41796414, -2.36213546, -2.36213546,
0.22038959, -1.79447578, -0.18378075, -2.36213546, -1.06339678,
-2.38675753, 0.01208211, 1.1777786 , -2.36213546, -1.48251942,
2.29157802, 2.71070066, 0.53234595, 1.41196199, -0.41796414])
Por defecto, el threshold aplicado es 0. Podemos ver esto fácilmente viendo, del array anterior, qué valores son mayores que 0 y cuáles son menores. Hagamos esto solo para los 10 primeros valores del array anterior:
scores[:10] > 0
array([False, False, False, True, True, False, True, True, False, True])
...y comparemos este resultado con los 10 primeros valores de la predicción:
prediction[:10]
array([0, 0, 0, 1, 1, 0, 1, 1, 0, 1], dtype=int64)
Vemos que se ha predicho un resultado de 0 (el pasajero no sobrevive) para aquellas muestras (para aquellos pasajeros) cuyos scores son menores que cero.
Pues bien, este clasificador con el threshold por defecto, supone un cierto <em>True Positive Rate</em> y un cierto <em>False Positive Rate</em>. Calculémoslos:
confusion_matrix(prediction, y_test)
array([[47, 9],
[ 4, 30]], dtype=int64)
Si extraemos los valores:
tn, fp, fn, tp = confusion_matrix(prediction, y_test).flatten()
TPR = tp / (tp + fn)
TPR
0.8823529411764706
FPR = (fp / (fp + tn))
FPR
0.16071428571428573
Vamos a almacenar estos valores en sendas listas para poder acceder a ellos posteriormente.
TPRs = [TPR]
FPRs = [FPR]
Y aquí viene lo interesante: hemos dicho que se va a comparar cada score con el threshold. Si el valor es mayor que el threshold se asigna a la clase 1 (o principal), y si es menor, a la clase 0 (o secundaria). Veamos los scores, la predicción que estos scores suponen, y el "ground truth" (el valor real de la variable objetivo) añadiendo estos tres datos a un dataframe ordenando los scores de menor a mayor:
scores_df = pd.DataFrame({
"scores": scores,
"prediction": prediction,
"truth": y_test
})
scores_df.sort_values("scores", inplace = True)
Y mostremos ahora los scores que hay en torno al threshold de cero (se ha buscado manualmente su posición en el dataset):
scores_df.iloc[46:66]
Lógicamente, todas las predicciones correspondientes a scores mayores que 0 aparecen con una predicción de 1, y al revés: las predicciones correspondientes a scores menores que cero aparecen con una predicción de 0.
Vemos, sin embargo, que el verdadero valor de la variable objetivo ("truth") no coincide siempre con la predicción. Lo ideal sería que por encima del threshold solo hubiese muestras de la clase 1 (para tener un TPR de 1), así que ¿qué ocurriría si en lugar de un threshold de 0 considerásemos un threshold de 0.8? En la imagen anterior vemos que, moviendo el threshold hasta dicho valor, estaríamos pasando tres muestras cuyo valor verdadero es 0 al bloque de "predicción 0". Es decir, estaríamos mejorando el TPR de nuestro modelo (al disminuir el número de falsos negativos)... pero a costa de empeorar el FPR. Vamos a calcular estos valores. Para ello tenemos los scores, de forma que podemos realizar la predicción "a mano":
new_prediction = scores > 0.8
new_prediction
array([False, False, False, True, True, False, True, True, False,
True, False, True, False, True, True, False, False, False,
False, True, False, True, False, False, False, True, False,
False, True, False, False, True, False, False, False, False,
False, False, False, False, False, False, False, False, True,
False, False, True, False, False, False, False, True, False,
False, False, False, False, False, False, False, False, True,
False, False, False, True, False, True, True, True, False,
False, False, False, False, False, False, False, False, False,
False, True, False, False, True, True, False, True, False])
confusion_matrix(new_prediction, y_test)
array([[50, 15],
[ 1, 24]], dtype=int64)
tn, fp, fn, tp = confusion_matrix(new_prediction, y_test).flatten()
TPR = tp / (tp + fn)
TPR
0.96
FPR = (fp / (fp + tn))
FPR
0.23076923076923078
Tal y como sospechábamos, el TPR del modelo ha aumentado hasta el 96%, pero el FPR ha aumentado también (lo que significa que hay más falsos positivos que antes).
Almacenemos estos valores en las listas que habíamos creado:
TPRs.append(TPR)
FPRs.append(FPR)
Y, en el ejemplo visto, hemos aumentado el threshold hasta 0.8. Si -nuevamente, viendo los scores- disminuimos el threshold hasta -0.5, vemos que estaríamos "sacando" del bloque de muestras que recibe una predicción 0 varias muestras cuyo valor real es 1... pero también estaríamos moviendo varias muestras que ahora mismo están bien clasificadas. Es decir, estaríamos mejorando el FPR (disminuyendo el número de falsos positivos) a costa de empeorar el TPR:
new_prediction = scores > -0.5
new_prediction
array([False, False, False, True, True, False, True, True, True,
True, False, True, False, True, True, True, False, False,
False, True, False, True, False, False, True, True, False,
True, True, False, False, True, False, False, False, True,
False, False, False, False, True, False, False, False, True,
False, False, True, False, False, False, True, True, False,
True, False, False, False, False, False, False, True, True,
True, True, False, True, False, True, True, True, False,
True, False, False, True, False, True, False, False, False,
True, True, False, False, True, True, True, True, True])
confusion_matrix(new_prediction, y_test)
array([[43, 5],
[ 8, 34]], dtype=int64)
tn, fp, fn, tp = confusion_matrix(new_prediction, y_test).flatten()
TPR = tp / (tp + fn)
TPR
0.8095238095238095
FPR = (fp / (fp + tn))
FPR
0.10416666666666667
Volvamos a almacenar los valores calculados:
TPRs.append(TPR)
FPRs.append(FPR)
También ahora hemos confirmado nuestras sospechas: hemos mejorado el FPR (disminuyéndolo) pero a costa de empeorar el TPR.
Llevemos a la gráfica de TPR vs. FPR las tres parejas de valores encontrados:
t = ["0", "0.8", "-0.5"]
fig, ax = plt.subplots(figsize = (6, 6))
ax.set_xlabel("False Positive Rate", fontsize = 15)
ax.set_ylabel("True Positive Rate", fontsize = 15)
ax.set_xticks(np.linspace(0, 1, 11))
ax.set_yticks(np.linspace(0, 1, 11))
ax.set_xlim(0, 1)
ax.set_ylim(0, 1)
sns.scatterplot(x = FPRs, y = TPRs, s = 50);
for i in range(len(t)):
plt.text(FPRs[i] + 0.02, TPRs[i] - 0.03, t[i], color = "red", fontsize = 15)
...gráfica que muestra exactamente las variaciones que hemos visto.
Hemos probado tres valores del threshold. Vamos a probar todos los thresholds desde el valor mínimo del score hasta el valor máximo, a ver cuál es el resultado:
TPRs = []
FPRs = []
for threshold in np.sort(scores):
new_prediction = scores > threshold
tn, fp, fn, tp = confusion_matrix(y_test, new_prediction).flatten()
TPR = tp / (tp + fn)
FPR = fp / (fp + tn)
TPRs.append(TPR)
FPRs.append(FPR)
Ahora ya tenemos los TPR y FPR en las listas TPRs y FPRs. Mostrémoslas en una gráfica etiquetando cada punto con el threshold al que se corresponde:
fig, ax = plt.subplots(figsize = (10, 10))
ax.set_xlabel("False Positive Rate", fontsize = 15)
ax.set_ylabel("True Positive Rate", fontsize = 15)
ax.set_xticks(np.linspace(0, 1, 11))
ax.set_yticks(np.linspace(0, 1, 11))
sns.scatterplot(x = FPRs, y = TPRs, s = 40);
plt.plot([0, 1], [0, 1], 'k--')
for i in range(len(scores)):
score_i = np.sort(scores)[i]
if score_i > -0.65:
ax.text(FPRs[i] + 0.012, TPRs[i] - 0.004, round(score_i, 2), color = "red", fontsize = 12)
else:
if i % 4 == 0:
ax.text(FPRs[i] - 0.01, TPRs[i] - 0.03, round(score_i, 2), color = "red", fontsize = 12)
Lo que estamos viendo es exactamente la curva ROC del clasificador. Y nos da bastante información: en primer lugar nos dice que aumentando o disminuyendo lo suficiente el threshold podemos conseguir que nuestro algoritmo de clasificación alcance un TPR o un FPR casi perfecto. El único "pero" es que lo conseguiremos a costa de empeorar la otra métrica. Esto significa que, una vez creado el modelo, podemos configurarlo en función de nuestros intereses fijando un threshold u otro (para conseguir un mayor TPR o un FPR). Por otro lado, cuanto más se aproxime la gráfica al punto (0, 1), más se comportará como un clasificador ideal. En la gráfica anterior se muestra la línea diagonal que marca el comportamiento de los clasificadores que tienen tantos verdaderos positivos como falsos negativos, y tantos verdaderos negativos como falsos positivos.
', 'es') (Line: 118)
Drupal\filter\Element\ProcessedText::preRenderText(Array)
call_user_func_array(Array, Array) (Line: 101)
Drupal\Core\Render\Renderer->doTrustedCallback(Array, Array, 'Render #pre_render callbacks must be methods of a class that implements \Drupal\Core\Security\TrustedCallbackInterface or be an anonymous function. The callback was %s. See https://www.drupal.org/node/2966725', 'exception', 'Drupal\Core\Render\Element\RenderCallbackInterface') (Line: 788)
Drupal\Core\Render\Renderer->doCallback('#pre_render', Array, Array) (Line: 374)
Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204)
Drupal\Core\Render\Renderer->render(Array) (Line: 479)
Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 88)
__TwigTemplate_a7d6005c89ae729617b9a0c2bccb1776->doDisplay(Array, Array) (Line: 405)
Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378)
Twig\Template->display(Array, Array) (Line: 46)
__TwigTemplate_804f7948456cfe20e11a34c43439c7c2->doDisplay(Array, Array) (Line: 405)
Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378)
Twig\Template->display(Array, Array) (Line: 43)
__TwigTemplate_bd990293b89f3b78c69fe0ee2f7828b5->doDisplay(Array, Array) (Line: 405)
Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378)
Twig\Template->display(Array) (Line: 390)
Twig\Template->render(Array) (Line: 55)
twig_render_template('themes/contrib/classy/templates/field/field--text-with-summary.html.twig', Array) (Line: 384)
Drupal\Core\Theme\ThemeManager->render('field', Array) (Line: 433)
Drupal\Core\Render\Renderer->doRender(Array) (Line: 446)
Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204)
Drupal\Core\Render\Renderer->render(Array) (Line: 479)
Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 74)
__TwigTemplate_94047fbdba6937b76a4479dfa1763452->doDisplay(Array, Array) (Line: 405)
Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378)
Twig\Template->display(Array) (Line: 390)
Twig\Template->render(Array) (Line: 55)
twig_render_template('themes/custom/yg_aesthetic/templates/node.html.twig', Array) (Line: 384)
Drupal\Core\Theme\ThemeManager->render('node', Array) (Line: 433)
Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204)
Drupal\Core\Render\Renderer->render(Array, ) (Line: 242)
Drupal\Core\Render\MainContent\HtmlRenderer->Drupal\Core\Render\MainContent\{closure}() (Line: 580)
Drupal\Core\Render\Renderer->executeInRenderContext(Object, Object) (Line: 235)
Drupal\Core\Render\MainContent\HtmlRenderer->prepare(Array, Object, Object) (Line: 132)
Drupal\Core\Render\MainContent\HtmlRenderer->renderResponse(Array, Object, Object) (Line: 90)
Drupal\Core\EventSubscriber\MainContentViewSubscriber->onViewRenderArray(Object, 'kernel.view', Object)
call_user_func(Array, Object, 'kernel.view', Object) (Line: 142)
Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher->dispatch(Object, 'kernel.view') (Line: 174)
Symfony\Component\HttpKernel\HttpKernel->handleRaw(Object, 1) (Line: 81)
Symfony\Component\HttpKernel\HttpKernel->handle(Object, 1, 1) (Line: 58)
Drupal\Core\StackMiddleware\Session->handle(Object, 1, 1) (Line: 48)
Drupal\Core\StackMiddleware\KernelPreHandle->handle(Object, 1, 1) (Line: 191)
Drupal\page_cache\StackMiddleware\PageCache->fetch(Object, 1, 1) (Line: 128)
Drupal\page_cache\StackMiddleware\PageCache->lookup(Object, 1, 1) (Line: 82)
Drupal\page_cache\StackMiddleware\PageCache->handle(Object, 1, 1) (Line: 48)
Drupal\Core\StackMiddleware\ReverseProxyMiddleware->handle(Object, 1, 1) (Line: 51)
Drupal\Core\StackMiddleware\NegotiationMiddleware->handle(Object, 1, 1) (Line: 23)
Stack\StackedHttpKernel->handle(Object, 1, 1) (Line: 713)
Drupal\Core\DrupalKernel->handle(Object) (Line: 19)
- Deprecated function: Creation of dynamic property Drupal\mysql\Driver\Database\mysql\Select::$alterTags is deprecated in Drupal\Core\Database\Query\Select->addTag() (line 149 of core/lib/Drupal/Core/Database/Query/Select.php).
Drupal\Core\Database\Query\Select->addTag('entity_query') (Line: 147)
Drupal\Core\Entity\Query\Sql\Query->prepare() (Line: 80)
Drupal\Core\Entity\Query\Sql\Query->execute() (Line: 640)
Drupal\Core\Entity\EntityStorageBase->loadByProperties(Array) (Line: 63)
Drupal\Core\Entity\EntityRepository->loadEntityByUuid('file', 'fd14ca8d-6599-4034-842a-45890b045f31') (Line: 124)
Drupal\editor\Plugin\Filter\EditorFileReference->process('Para entender el concepto de <strong>curva ROC</strong> (<em>ROC curve</em>, <em>Receiver Operating Characteristic</em>) tenemos que entender previamente cómo funciona un algoritmo de clasificación lineal como la regresión logística: A la hora de realizar una predicción, el modelo asigna una puntuación (un "score") a cada muestra del conjunto de pruebas. Este score se compara con un cierto valor límite ("<em>threshold</em>"). Si, para una muestra, su score es mayor que el threshold, se asigna a la clase principal. Si es menor, se asigna a la clase secundaria.
Siguiendo con el ejemplo visto relativo al Titanic, ya habíamos realizado la predicción:
prediction = model.predict(X_test)
prediction
array([0, 0, 0, 1, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 0, 0, 1, 0, 1,
0, 0, 1, 1, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
1, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0,
1, 0, 1, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1,
1, 0], dtype=int64)
Podemos ver el score que el modelo ha asignado a cada muestra con el método <strong>decision_function</strong>:
scores = model.decision_function(X_test)
scores
array([-1.94301282, -2.36213546, -1.21444007, 2.47651727, 1.1959797 ,
-0.60290339, 2.29157802, 2.53608913, -0.41796414, 0.95146859,
-2.59631885, 1.41196199, -2.15257414, 1.6564731 , 2.71070066,
0.53234595, -2.36213546, -1.71670281, -2.36213546, 0.90989925,
-1.48251942, 2.29157802, -2.15257414, -0.60290339, 0.53234595,
2.47651727, -2.36213546, 0.53234595, 1.6564731 , -0.65922644,
-1.94301282, 1.41196199, -2.36213546, -0.60290339, -2.59631885,
-0.18378075, -2.36213546, -1.48251942, -1.48251942, -1.48251942,
-0.41796414, -1.94301282, -2.36213546, -1.42400139, 2.05739463,
-2.36213546, -2.36213546, 1.99782277, -1.48251942, -0.83708678,
-0.60290339, 0.06073036, 1.1777786 , -2.36213546, -0.18378075,
-2.19539732, -0.60290339, -1.13792094, -0.94530689, -2.86545203,
-1.94301282, 0.77685706, 2.47651727, -0.18378075, 0.77685706,
-2.15257414, 2.29157802, -1.23800831, 1.1777786 , 2.29157802,
1.1777786 , -0.83708678, -0.41796414, -2.36213546, -2.36213546,
0.22038959, -1.79447578, -0.18378075, -2.36213546, -1.06339678,
-2.38675753, 0.01208211, 1.1777786 , -2.36213546, -1.48251942,
2.29157802, 2.71070066, 0.53234595, 1.41196199, -0.41796414])
Por defecto, el threshold aplicado es 0. Podemos ver esto fácilmente viendo, del array anterior, qué valores son mayores que 0 y cuáles son menores. Hagamos esto solo para los 10 primeros valores del array anterior:
scores[:10] > 0
array([False, False, False, True, True, False, True, True, False, True])
...y comparemos este resultado con los 10 primeros valores de la predicción:
prediction[:10]
array([0, 0, 0, 1, 1, 0, 1, 1, 0, 1], dtype=int64)
Vemos que se ha predicho un resultado de 0 (el pasajero no sobrevive) para aquellas muestras (para aquellos pasajeros) cuyos scores son menores que cero.
Pues bien, este clasificador con el threshold por defecto, supone un cierto <em>True Positive Rate</em> y un cierto <em>False Positive Rate</em>. Calculémoslos:
confusion_matrix(prediction, y_test)
array([[47, 9],
[ 4, 30]], dtype=int64)
Si extraemos los valores:
tn, fp, fn, tp = confusion_matrix(prediction, y_test).flatten()
TPR = tp / (tp + fn)
TPR
0.8823529411764706
FPR = (fp / (fp + tn))
FPR
0.16071428571428573
Vamos a almacenar estos valores en sendas listas para poder acceder a ellos posteriormente.
TPRs = [TPR]
FPRs = [FPR]
Y aquí viene lo interesante: hemos dicho que se va a comparar cada score con el threshold. Si el valor es mayor que el threshold se asigna a la clase 1 (o principal), y si es menor, a la clase 0 (o secundaria). Veamos los scores, la predicción que estos scores suponen, y el "ground truth" (el valor real de la variable objetivo) añadiendo estos tres datos a un dataframe ordenando los scores de menor a mayor:
scores_df = pd.DataFrame({
"scores": scores,
"prediction": prediction,
"truth": y_test
})
scores_df.sort_values("scores", inplace = True)
Y mostremos ahora los scores que hay en torno al threshold de cero (se ha buscado manualmente su posición en el dataset):
scores_df.iloc[46:66]
Lógicamente, todas las predicciones correspondientes a scores mayores que 0 aparecen con una predicción de 1, y al revés: las predicciones correspondientes a scores menores que cero aparecen con una predicción de 0.
Vemos, sin embargo, que el verdadero valor de la variable objetivo ("truth") no coincide siempre con la predicción. Lo ideal sería que por encima del threshold solo hubiese muestras de la clase 1 (para tener un TPR de 1), así que ¿qué ocurriría si en lugar de un threshold de 0 considerásemos un threshold de 0.8? En la imagen anterior vemos que, moviendo el threshold hasta dicho valor, estaríamos pasando tres muestras cuyo valor verdadero es 0 al bloque de "predicción 0". Es decir, estaríamos mejorando el TPR de nuestro modelo (al disminuir el número de falsos negativos)... pero a costa de empeorar el FPR. Vamos a calcular estos valores. Para ello tenemos los scores, de forma que podemos realizar la predicción "a mano":
new_prediction = scores > 0.8
new_prediction
array([False, False, False, True, True, False, True, True, False,
True, False, True, False, True, True, False, False, False,
False, True, False, True, False, False, False, True, False,
False, True, False, False, True, False, False, False, False,
False, False, False, False, False, False, False, False, True,
False, False, True, False, False, False, False, True, False,
False, False, False, False, False, False, False, False, True,
False, False, False, True, False, True, True, True, False,
False, False, False, False, False, False, False, False, False,
False, True, False, False, True, True, False, True, False])
confusion_matrix(new_prediction, y_test)
array([[50, 15],
[ 1, 24]], dtype=int64)
tn, fp, fn, tp = confusion_matrix(new_prediction, y_test).flatten()
TPR = tp / (tp + fn)
TPR
0.96
FPR = (fp / (fp + tn))
FPR
0.23076923076923078
Tal y como sospechábamos, el TPR del modelo ha aumentado hasta el 96%, pero el FPR ha aumentado también (lo que significa que hay más falsos positivos que antes).
Almacenemos estos valores en las listas que habíamos creado:
TPRs.append(TPR)
FPRs.append(FPR)
Y, en el ejemplo visto, hemos aumentado el threshold hasta 0.8. Si -nuevamente, viendo los scores- disminuimos el threshold hasta -0.5, vemos que estaríamos "sacando" del bloque de muestras que recibe una predicción 0 varias muestras cuyo valor real es 1... pero también estaríamos moviendo varias muestras que ahora mismo están bien clasificadas. Es decir, estaríamos mejorando el FPR (disminuyendo el número de falsos positivos) a costa de empeorar el TPR:
new_prediction = scores > -0.5
new_prediction
array([False, False, False, True, True, False, True, True, True,
True, False, True, False, True, True, True, False, False,
False, True, False, True, False, False, True, True, False,
True, True, False, False, True, False, False, False, True,
False, False, False, False, True, False, False, False, True,
False, False, True, False, False, False, True, True, False,
True, False, False, False, False, False, False, True, True,
True, True, False, True, False, True, True, True, False,
True, False, False, True, False, True, False, False, False,
True, True, False, False, True, True, True, True, True])
confusion_matrix(new_prediction, y_test)
array([[43, 5],
[ 8, 34]], dtype=int64)
tn, fp, fn, tp = confusion_matrix(new_prediction, y_test).flatten()
TPR = tp / (tp + fn)
TPR
0.8095238095238095
FPR = (fp / (fp + tn))
FPR
0.10416666666666667
Volvamos a almacenar los valores calculados:
TPRs.append(TPR)
FPRs.append(FPR)
También ahora hemos confirmado nuestras sospechas: hemos mejorado el FPR (disminuyéndolo) pero a costa de empeorar el TPR.
Llevemos a la gráfica de TPR vs. FPR las tres parejas de valores encontrados:
t = ["0", "0.8", "-0.5"]
fig, ax = plt.subplots(figsize = (6, 6))
ax.set_xlabel("False Positive Rate", fontsize = 15)
ax.set_ylabel("True Positive Rate", fontsize = 15)
ax.set_xticks(np.linspace(0, 1, 11))
ax.set_yticks(np.linspace(0, 1, 11))
ax.set_xlim(0, 1)
ax.set_ylim(0, 1)
sns.scatterplot(x = FPRs, y = TPRs, s = 50);
for i in range(len(t)):
plt.text(FPRs[i] + 0.02, TPRs[i] - 0.03, t[i], color = "red", fontsize = 15)
...gráfica que muestra exactamente las variaciones que hemos visto.
Hemos probado tres valores del threshold. Vamos a probar todos los thresholds desde el valor mínimo del score hasta el valor máximo, a ver cuál es el resultado:
TPRs = []
FPRs = []
for threshold in np.sort(scores):
new_prediction = scores > threshold
tn, fp, fn, tp = confusion_matrix(y_test, new_prediction).flatten()
TPR = tp / (tp + fn)
FPR = fp / (fp + tn)
TPRs.append(TPR)
FPRs.append(FPR)
Ahora ya tenemos los TPR y FPR en las listas TPRs y FPRs. Mostrémoslas en una gráfica etiquetando cada punto con el threshold al que se corresponde:
fig, ax = plt.subplots(figsize = (10, 10))
ax.set_xlabel("False Positive Rate", fontsize = 15)
ax.set_ylabel("True Positive Rate", fontsize = 15)
ax.set_xticks(np.linspace(0, 1, 11))
ax.set_yticks(np.linspace(0, 1, 11))
sns.scatterplot(x = FPRs, y = TPRs, s = 40);
plt.plot([0, 1], [0, 1], 'k--')
for i in range(len(scores)):
score_i = np.sort(scores)[i]
if score_i > -0.65:
ax.text(FPRs[i] + 0.012, TPRs[i] - 0.004, round(score_i, 2), color = "red", fontsize = 12)
else:
if i % 4 == 0:
ax.text(FPRs[i] - 0.01, TPRs[i] - 0.03, round(score_i, 2), color = "red", fontsize = 12)
Lo que estamos viendo es exactamente la curva ROC del clasificador. Y nos da bastante información: en primer lugar nos dice que aumentando o disminuyendo lo suficiente el threshold podemos conseguir que nuestro algoritmo de clasificación alcance un TPR o un FPR casi perfecto. El único "pero" es que lo conseguiremos a costa de empeorar la otra métrica. Esto significa que, una vez creado el modelo, podemos configurarlo en función de nuestros intereses fijando un threshold u otro (para conseguir un mayor TPR o un FPR). Por otro lado, cuanto más se aproxime la gráfica al punto (0, 1), más se comportará como un clasificador ideal. En la gráfica anterior se muestra la línea diagonal que marca el comportamiento de los clasificadores que tienen tantos verdaderos positivos como falsos negativos, y tantos verdaderos negativos como falsos positivos.
', 'es') (Line: 118)
Drupal\filter\Element\ProcessedText::preRenderText(Array)
call_user_func_array(Array, Array) (Line: 101)
Drupal\Core\Render\Renderer->doTrustedCallback(Array, Array, 'Render #pre_render callbacks must be methods of a class that implements \Drupal\Core\Security\TrustedCallbackInterface or be an anonymous function. The callback was %s. See https://www.drupal.org/node/2966725', 'exception', 'Drupal\Core\Render\Element\RenderCallbackInterface') (Line: 788)
Drupal\Core\Render\Renderer->doCallback('#pre_render', Array, Array) (Line: 374)
Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204)
Drupal\Core\Render\Renderer->render(Array) (Line: 479)
Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 88)
__TwigTemplate_a7d6005c89ae729617b9a0c2bccb1776->doDisplay(Array, Array) (Line: 405)
Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378)
Twig\Template->display(Array, Array) (Line: 46)
__TwigTemplate_804f7948456cfe20e11a34c43439c7c2->doDisplay(Array, Array) (Line: 405)
Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378)
Twig\Template->display(Array, Array) (Line: 43)
__TwigTemplate_bd990293b89f3b78c69fe0ee2f7828b5->doDisplay(Array, Array) (Line: 405)
Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378)
Twig\Template->display(Array) (Line: 390)
Twig\Template->render(Array) (Line: 55)
twig_render_template('themes/contrib/classy/templates/field/field--text-with-summary.html.twig', Array) (Line: 384)
Drupal\Core\Theme\ThemeManager->render('field', Array) (Line: 433)
Drupal\Core\Render\Renderer->doRender(Array) (Line: 446)
Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204)
Drupal\Core\Render\Renderer->render(Array) (Line: 479)
Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 74)
__TwigTemplate_94047fbdba6937b76a4479dfa1763452->doDisplay(Array, Array) (Line: 405)
Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378)
Twig\Template->display(Array) (Line: 390)
Twig\Template->render(Array) (Line: 55)
twig_render_template('themes/custom/yg_aesthetic/templates/node.html.twig', Array) (Line: 384)
Drupal\Core\Theme\ThemeManager->render('node', Array) (Line: 433)
Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204)
Drupal\Core\Render\Renderer->render(Array, ) (Line: 242)
Drupal\Core\Render\MainContent\HtmlRenderer->Drupal\Core\Render\MainContent\{closure}() (Line: 580)
Drupal\Core\Render\Renderer->executeInRenderContext(Object, Object) (Line: 235)
Drupal\Core\Render\MainContent\HtmlRenderer->prepare(Array, Object, Object) (Line: 132)
Drupal\Core\Render\MainContent\HtmlRenderer->renderResponse(Array, Object, Object) (Line: 90)
Drupal\Core\EventSubscriber\MainContentViewSubscriber->onViewRenderArray(Object, 'kernel.view', Object)
call_user_func(Array, Object, 'kernel.view', Object) (Line: 142)
Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher->dispatch(Object, 'kernel.view') (Line: 174)
Symfony\Component\HttpKernel\HttpKernel->handleRaw(Object, 1) (Line: 81)
Symfony\Component\HttpKernel\HttpKernel->handle(Object, 1, 1) (Line: 58)
Drupal\Core\StackMiddleware\Session->handle(Object, 1, 1) (Line: 48)
Drupal\Core\StackMiddleware\KernelPreHandle->handle(Object, 1, 1) (Line: 191)
Drupal\page_cache\StackMiddleware\PageCache->fetch(Object, 1, 1) (Line: 128)
Drupal\page_cache\StackMiddleware\PageCache->lookup(Object, 1, 1) (Line: 82)
Drupal\page_cache\StackMiddleware\PageCache->handle(Object, 1, 1) (Line: 48)
Drupal\Core\StackMiddleware\ReverseProxyMiddleware->handle(Object, 1, 1) (Line: 51)
Drupal\Core\StackMiddleware\NegotiationMiddleware->handle(Object, 1, 1) (Line: 23)
Stack\StackedHttpKernel->handle(Object, 1, 1) (Line: 713)
Drupal\Core\DrupalKernel->handle(Object) (Line: 19)
- Deprecated function: Creation of dynamic property Drupal\mysql\Driver\Database\mysql\Select::$alterTags is deprecated in Drupal\Core\Database\Query\Select->addTag() (line 149 of core/lib/Drupal/Core/Database/Query/Select.php).
Drupal\Core\Database\Query\Select->addTag('node_access') (Line: 51)
Drupal\book\BookOutlineStorage->loadMultiple(Array) (Line: 1021)
Drupal\book\BookManager->bookTreeCheckAccess(Array, Array) (Line: 701)
Drupal\book\BookManager->bookTreeBuild('828', Array) (Line: 567)
Drupal\book\BookManager->bookTreeAllData('828', Array, 6) (Line: 792)
Drupal\book\BookManager->bookTreeGetFlat(Array) (Line: 109)
Drupal\book\BookOutline->childrenLinks(Array) (Line: 403)
template_preprocess_book_navigation(Array, 'book_navigation', Array)
call_user_func_array('template_preprocess_book_navigation', Array) (Line: 287)
Drupal\Core\Theme\ThemeManager->render('book_navigation', Array) (Line: 433)
Drupal\Core\Render\Renderer->doRender(Array) (Line: 446)
Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204)
Drupal\Core\Render\Renderer->render(Array) (Line: 479)
Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 74)
__TwigTemplate_94047fbdba6937b76a4479dfa1763452->doDisplay(Array, Array) (Line: 405)
Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378)
Twig\Template->display(Array) (Line: 390)
Twig\Template->render(Array) (Line: 55)
twig_render_template('themes/custom/yg_aesthetic/templates/node.html.twig', Array) (Line: 384)
Drupal\Core\Theme\ThemeManager->render('node', Array) (Line: 433)
Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204)
Drupal\Core\Render\Renderer->render(Array, ) (Line: 242)
Drupal\Core\Render\MainContent\HtmlRenderer->Drupal\Core\Render\MainContent\{closure}() (Line: 580)
Drupal\Core\Render\Renderer->executeInRenderContext(Object, Object) (Line: 235)
Drupal\Core\Render\MainContent\HtmlRenderer->prepare(Array, Object, Object) (Line: 132)
Drupal\Core\Render\MainContent\HtmlRenderer->renderResponse(Array, Object, Object) (Line: 90)
Drupal\Core\EventSubscriber\MainContentViewSubscriber->onViewRenderArray(Object, 'kernel.view', Object)
call_user_func(Array, Object, 'kernel.view', Object) (Line: 142)
Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher->dispatch(Object, 'kernel.view') (Line: 174)
Symfony\Component\HttpKernel\HttpKernel->handleRaw(Object, 1) (Line: 81)
Symfony\Component\HttpKernel\HttpKernel->handle(Object, 1, 1) (Line: 58)
Drupal\Core\StackMiddleware\Session->handle(Object, 1, 1) (Line: 48)
Drupal\Core\StackMiddleware\KernelPreHandle->handle(Object, 1, 1) (Line: 191)
Drupal\page_cache\StackMiddleware\PageCache->fetch(Object, 1, 1) (Line: 128)
Drupal\page_cache\StackMiddleware\PageCache->lookup(Object, 1, 1) (Line: 82)
Drupal\page_cache\StackMiddleware\PageCache->handle(Object, 1, 1) (Line: 48)
Drupal\Core\StackMiddleware\ReverseProxyMiddleware->handle(Object, 1, 1) (Line: 51)
Drupal\Core\StackMiddleware\NegotiationMiddleware->handle(Object, 1, 1) (Line: 23)
Stack\StackedHttpKernel->handle(Object, 1, 1) (Line: 713)
Drupal\Core\DrupalKernel->handle(Object) (Line: 19)
- Deprecated function: Creation of dynamic property Drupal\mysql\Driver\Database\mysql\Select::$alterMetaData is deprecated in Drupal\Core\Database\Query\Select->addMetaData() (line 178 of core/lib/Drupal/Core/Database/Query/Select.php).
Drupal\Core\Database\Query\Select->addMetaData('base_table', 'book') (Line: 52)
Drupal\book\BookOutlineStorage->loadMultiple(Array) (Line: 1021)
Drupal\book\BookManager->bookTreeCheckAccess(Array, Array) (Line: 701)
Drupal\book\BookManager->bookTreeBuild('828', Array) (Line: 567)
Drupal\book\BookManager->bookTreeAllData('828', Array, 6) (Line: 792)
Drupal\book\BookManager->bookTreeGetFlat(Array) (Line: 109)
Drupal\book\BookOutline->childrenLinks(Array) (Line: 403)
template_preprocess_book_navigation(Array, 'book_navigation', Array)
call_user_func_array('template_preprocess_book_navigation', Array) (Line: 287)
Drupal\Core\Theme\ThemeManager->render('book_navigation', Array) (Line: 433)
Drupal\Core\Render\Renderer->doRender(Array) (Line: 446)
Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204)
Drupal\Core\Render\Renderer->render(Array) (Line: 479)
Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 74)
__TwigTemplate_94047fbdba6937b76a4479dfa1763452->doDisplay(Array, Array) (Line: 405)
Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378)
Twig\Template->display(Array) (Line: 390)
Twig\Template->render(Array) (Line: 55)
twig_render_template('themes/custom/yg_aesthetic/templates/node.html.twig', Array) (Line: 384)
Drupal\Core\Theme\ThemeManager->render('node', Array) (Line: 433)
Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204)
Drupal\Core\Render\Renderer->render(Array, ) (Line: 242)
Drupal\Core\Render\MainContent\HtmlRenderer->Drupal\Core\Render\MainContent\{closure}() (Line: 580)
Drupal\Core\Render\Renderer->executeInRenderContext(Object, Object) (Line: 235)
Drupal\Core\Render\MainContent\HtmlRenderer->prepare(Array, Object, Object) (Line: 132)
Drupal\Core\Render\MainContent\HtmlRenderer->renderResponse(Array, Object, Object) (Line: 90)
Drupal\Core\EventSubscriber\MainContentViewSubscriber->onViewRenderArray(Object, 'kernel.view', Object)
call_user_func(Array, Object, 'kernel.view', Object) (Line: 142)
Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher->dispatch(Object, 'kernel.view') (Line: 174)
Symfony\Component\HttpKernel\HttpKernel->handleRaw(Object, 1) (Line: 81)
Symfony\Component\HttpKernel\HttpKernel->handle(Object, 1, 1) (Line: 58)
Drupal\Core\StackMiddleware\Session->handle(Object, 1, 1) (Line: 48)
Drupal\Core\StackMiddleware\KernelPreHandle->handle(Object, 1, 1) (Line: 191)
Drupal\page_cache\StackMiddleware\PageCache->fetch(Object, 1, 1) (Line: 128)
Drupal\page_cache\StackMiddleware\PageCache->lookup(Object, 1, 1) (Line: 82)
Drupal\page_cache\StackMiddleware\PageCache->handle(Object, 1, 1) (Line: 48)
Drupal\Core\StackMiddleware\ReverseProxyMiddleware->handle(Object, 1, 1) (Line: 51)
Drupal\Core\StackMiddleware\NegotiationMiddleware->handle(Object, 1, 1) (Line: 23)
Stack\StackedHttpKernel->handle(Object, 1, 1) (Line: 713)
Drupal\Core\DrupalKernel->handle(Object) (Line: 19)
- Deprecated function: Creation of dynamic property Drupal\mysql\Driver\Database\mysql\Select::$alterTags is deprecated in Drupal\Core\Database\Query\Select->addTag() (line 149 of core/lib/Drupal/Core/Database/Query/Select.php).
Drupal\Core\Database\Query\Select->addTag('node_access') (Line: 51)
Drupal\book\BookOutlineStorage->loadMultiple(Array) (Line: 1021)
Drupal\book\BookManager->bookTreeCheckAccess(Array, Array) (Line: 1179)
Drupal\book\BookManager->bookSubtreeData(Array) (Line: 56)
Drupal\book\BookOutline->prevLink(Array) (Line: 407)
template_preprocess_book_navigation(Array, 'book_navigation', Array)
call_user_func_array('template_preprocess_book_navigation', Array) (Line: 287)
Drupal\Core\Theme\ThemeManager->render('book_navigation', Array) (Line: 433)
Drupal\Core\Render\Renderer->doRender(Array) (Line: 446)
Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204)
Drupal\Core\Render\Renderer->render(Array) (Line: 479)
Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 74)
__TwigTemplate_94047fbdba6937b76a4479dfa1763452->doDisplay(Array, Array) (Line: 405)
Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378)
Twig\Template->display(Array) (Line: 390)
Twig\Template->render(Array) (Line: 55)
twig_render_template('themes/custom/yg_aesthetic/templates/node.html.twig', Array) (Line: 384)
Drupal\Core\Theme\ThemeManager->render('node', Array) (Line: 433)
Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204)
Drupal\Core\Render\Renderer->render(Array, ) (Line: 242)
Drupal\Core\Render\MainContent\HtmlRenderer->Drupal\Core\Render\MainContent\{closure}() (Line: 580)
Drupal\Core\Render\Renderer->executeInRenderContext(Object, Object) (Line: 235)
Drupal\Core\Render\MainContent\HtmlRenderer->prepare(Array, Object, Object) (Line: 132)
Drupal\Core\Render\MainContent\HtmlRenderer->renderResponse(Array, Object, Object) (Line: 90)
Drupal\Core\EventSubscriber\MainContentViewSubscriber->onViewRenderArray(Object, 'kernel.view', Object)
call_user_func(Array, Object, 'kernel.view', Object) (Line: 142)
Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher->dispatch(Object, 'kernel.view') (Line: 174)
Symfony\Component\HttpKernel\HttpKernel->handleRaw(Object, 1) (Line: 81)
Symfony\Component\HttpKernel\HttpKernel->handle(Object, 1, 1) (Line: 58)
Drupal\Core\StackMiddleware\Session->handle(Object, 1, 1) (Line: 48)
Drupal\Core\StackMiddleware\KernelPreHandle->handle(Object, 1, 1) (Line: 191)
Drupal\page_cache\StackMiddleware\PageCache->fetch(Object, 1, 1) (Line: 128)
Drupal\page_cache\StackMiddleware\PageCache->lookup(Object, 1, 1) (Line: 82)
Drupal\page_cache\StackMiddleware\PageCache->handle(Object, 1, 1) (Line: 48)
Drupal\Core\StackMiddleware\ReverseProxyMiddleware->handle(Object, 1, 1) (Line: 51)
Drupal\Core\StackMiddleware\NegotiationMiddleware->handle(Object, 1, 1) (Line: 23)
Stack\StackedHttpKernel->handle(Object, 1, 1) (Line: 713)
Drupal\Core\DrupalKernel->handle(Object) (Line: 19)
- Deprecated function: Creation of dynamic property Drupal\mysql\Driver\Database\mysql\Select::$alterMetaData is deprecated in Drupal\Core\Database\Query\Select->addMetaData() (line 178 of core/lib/Drupal/Core/Database/Query/Select.php).
Drupal\Core\Database\Query\Select->addMetaData('base_table', 'book') (Line: 52)
Drupal\book\BookOutlineStorage->loadMultiple(Array) (Line: 1021)
Drupal\book\BookManager->bookTreeCheckAccess(Array, Array) (Line: 1179)
Drupal\book\BookManager->bookSubtreeData(Array) (Line: 56)
Drupal\book\BookOutline->prevLink(Array) (Line: 407)
template_preprocess_book_navigation(Array, 'book_navigation', Array)
call_user_func_array('template_preprocess_book_navigation', Array) (Line: 287)
Drupal\Core\Theme\ThemeManager->render('book_navigation', Array) (Line: 433)
Drupal\Core\Render\Renderer->doRender(Array) (Line: 446)
Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204)
Drupal\Core\Render\Renderer->render(Array) (Line: 479)
Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 74)
__TwigTemplate_94047fbdba6937b76a4479dfa1763452->doDisplay(Array, Array) (Line: 405)
Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378)
Twig\Template->display(Array) (Line: 390)
Twig\Template->render(Array) (Line: 55)
twig_render_template('themes/custom/yg_aesthetic/templates/node.html.twig', Array) (Line: 384)
Drupal\Core\Theme\ThemeManager->render('node', Array) (Line: 433)
Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204)
Drupal\Core\Render\Renderer->render(Array, ) (Line: 242)
Drupal\Core\Render\MainContent\HtmlRenderer->Drupal\Core\Render\MainContent\{closure}() (Line: 580)
Drupal\Core\Render\Renderer->executeInRenderContext(Object, Object) (Line: 235)
Drupal\Core\Render\MainContent\HtmlRenderer->prepare(Array, Object, Object) (Line: 132)
Drupal\Core\Render\MainContent\HtmlRenderer->renderResponse(Array, Object, Object) (Line: 90)
Drupal\Core\EventSubscriber\MainContentViewSubscriber->onViewRenderArray(Object, 'kernel.view', Object)
call_user_func(Array, Object, 'kernel.view', Object) (Line: 142)
Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher->dispatch(Object, 'kernel.view') (Line: 174)
Symfony\Component\HttpKernel\HttpKernel->handleRaw(Object, 1) (Line: 81)
Symfony\Component\HttpKernel\HttpKernel->handle(Object, 1, 1) (Line: 58)
Drupal\Core\StackMiddleware\Session->handle(Object, 1, 1) (Line: 48)
Drupal\Core\StackMiddleware\KernelPreHandle->handle(Object, 1, 1) (Line: 191)
Drupal\page_cache\StackMiddleware\PageCache->fetch(Object, 1, 1) (Line: 128)
Drupal\page_cache\StackMiddleware\PageCache->lookup(Object, 1, 1) (Line: 82)
Drupal\page_cache\StackMiddleware\PageCache->handle(Object, 1, 1) (Line: 48)
Drupal\Core\StackMiddleware\ReverseProxyMiddleware->handle(Object, 1, 1) (Line: 51)
Drupal\Core\StackMiddleware\NegotiationMiddleware->handle(Object, 1, 1) (Line: 23)
Stack\StackedHttpKernel->handle(Object, 1, 1) (Line: 713)
Drupal\Core\DrupalKernel->handle(Object) (Line: 19)
- Deprecated function: Creation of dynamic property Drupal\mysql\Driver\Database\mysql\Select::$alterTags is deprecated in Drupal\Core\Database\Query\Select->addTag() (line 149 of core/lib/Drupal/Core/Database/Query/Select.php).
Drupal\Core\Database\Query\Select->addTag('node_access') (Line: 51)
Drupal\book\BookOutlineStorage->loadMultiple(Array, 1) (Line: 831)
Drupal\book\BookManager->loadBookLinks(Array, 1) (Line: 823)
Drupal\book\BookManager->loadBookLink('915') (Line: 419)
template_preprocess_book_navigation(Array, 'book_navigation', Array)
call_user_func_array('template_preprocess_book_navigation', Array) (Line: 287)
Drupal\Core\Theme\ThemeManager->render('book_navigation', Array) (Line: 433)
Drupal\Core\Render\Renderer->doRender(Array) (Line: 446)
Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204)
Drupal\Core\Render\Renderer->render(Array) (Line: 479)
Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 74)
__TwigTemplate_94047fbdba6937b76a4479dfa1763452->doDisplay(Array, Array) (Line: 405)
Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378)
Twig\Template->display(Array) (Line: 390)
Twig\Template->render(Array) (Line: 55)
twig_render_template('themes/custom/yg_aesthetic/templates/node.html.twig', Array) (Line: 384)
Drupal\Core\Theme\ThemeManager->render('node', Array) (Line: 433)
Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204)
Drupal\Core\Render\Renderer->render(Array, ) (Line: 242)
Drupal\Core\Render\MainContent\HtmlRenderer->Drupal\Core\Render\MainContent\{closure}() (Line: 580)
Drupal\Core\Render\Renderer->executeInRenderContext(Object, Object) (Line: 235)
Drupal\Core\Render\MainContent\HtmlRenderer->prepare(Array, Object, Object) (Line: 132)
Drupal\Core\Render\MainContent\HtmlRenderer->renderResponse(Array, Object, Object) (Line: 90)
Drupal\Core\EventSubscriber\MainContentViewSubscriber->onViewRenderArray(Object, 'kernel.view', Object)
call_user_func(Array, Object, 'kernel.view', Object) (Line: 142)
Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher->dispatch(Object, 'kernel.view') (Line: 174)
Symfony\Component\HttpKernel\HttpKernel->handleRaw(Object, 1) (Line: 81)
Symfony\Component\HttpKernel\HttpKernel->handle(Object, 1, 1) (Line: 58)
Drupal\Core\StackMiddleware\Session->handle(Object, 1, 1) (Line: 48)
Drupal\Core\StackMiddleware\KernelPreHandle->handle(Object, 1, 1) (Line: 191)
Drupal\page_cache\StackMiddleware\PageCache->fetch(Object, 1, 1) (Line: 128)
Drupal\page_cache\StackMiddleware\PageCache->lookup(Object, 1, 1) (Line: 82)
Drupal\page_cache\StackMiddleware\PageCache->handle(Object, 1, 1) (Line: 48)
Drupal\Core\StackMiddleware\ReverseProxyMiddleware->handle(Object, 1, 1) (Line: 51)
Drupal\Core\StackMiddleware\NegotiationMiddleware->handle(Object, 1, 1) (Line: 23)
Stack\StackedHttpKernel->handle(Object, 1, 1) (Line: 713)
Drupal\Core\DrupalKernel->handle(Object) (Line: 19)
- Deprecated function: Creation of dynamic property Drupal\mysql\Driver\Database\mysql\Select::$alterMetaData is deprecated in Drupal\Core\Database\Query\Select->addMetaData() (line 178 of core/lib/Drupal/Core/Database/Query/Select.php).
Drupal\Core\Database\Query\Select->addMetaData('base_table', 'book') (Line: 52)
Drupal\book\BookOutlineStorage->loadMultiple(Array, 1) (Line: 831)
Drupal\book\BookManager->loadBookLinks(Array, 1) (Line: 823)
Drupal\book\BookManager->loadBookLink('915') (Line: 419)
template_preprocess_book_navigation(Array, 'book_navigation', Array)
call_user_func_array('template_preprocess_book_navigation', Array) (Line: 287)
Drupal\Core\Theme\ThemeManager->render('book_navigation', Array) (Line: 433)
Drupal\Core\Render\Renderer->doRender(Array) (Line: 446)
Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204)
Drupal\Core\Render\Renderer->render(Array) (Line: 479)
Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 74)
__TwigTemplate_94047fbdba6937b76a4479dfa1763452->doDisplay(Array, Array) (Line: 405)
Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378)
Twig\Template->display(Array) (Line: 390)
Twig\Template->render(Array) (Line: 55)
twig_render_template('themes/custom/yg_aesthetic/templates/node.html.twig', Array) (Line: 384)
Drupal\Core\Theme\ThemeManager->render('node', Array) (Line: 433)
Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204)
Drupal\Core\Render\Renderer->render(Array, ) (Line: 242)
Drupal\Core\Render\MainContent\HtmlRenderer->Drupal\Core\Render\MainContent\{closure}() (Line: 580)
Drupal\Core\Render\Renderer->executeInRenderContext(Object, Object) (Line: 235)
Drupal\Core\Render\MainContent\HtmlRenderer->prepare(Array, Object, Object) (Line: 132)
Drupal\Core\Render\MainContent\HtmlRenderer->renderResponse(Array, Object, Object) (Line: 90)
Drupal\Core\EventSubscriber\MainContentViewSubscriber->onViewRenderArray(Object, 'kernel.view', Object)
call_user_func(Array, Object, 'kernel.view', Object) (Line: 142)
Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher->dispatch(Object, 'kernel.view') (Line: 174)
Symfony\Component\HttpKernel\HttpKernel->handleRaw(Object, 1) (Line: 81)
Symfony\Component\HttpKernel\HttpKernel->handle(Object, 1, 1) (Line: 58)
Drupal\Core\StackMiddleware\Session->handle(Object, 1, 1) (Line: 48)
Drupal\Core\StackMiddleware\KernelPreHandle->handle(Object, 1, 1) (Line: 191)
Drupal\page_cache\StackMiddleware\PageCache->fetch(Object, 1, 1) (Line: 128)
Drupal\page_cache\StackMiddleware\PageCache->lookup(Object, 1, 1) (Line: 82)
Drupal\page_cache\StackMiddleware\PageCache->handle(Object, 1, 1) (Line: 48)
Drupal\Core\StackMiddleware\ReverseProxyMiddleware->handle(Object, 1, 1) (Line: 51)
Drupal\Core\StackMiddleware\NegotiationMiddleware->handle(Object, 1, 1) (Line: 23)
Stack\StackedHttpKernel->handle(Object, 1, 1) (Line: 713)
Drupal\Core\DrupalKernel->handle(Object) (Line: 19)
- Deprecated function: Return type of Drupal\google_analytics\Component\Render\GoogleAnalyticsJavaScriptSnippet::jsonSerialize() should either be compatible with JsonSerializable::jsonSerialize(): mixed, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in include() (line 10 of modules/contrib/google_analytics/src/Component/Render/GoogleAnalyticsJavaScriptSnippet.php).
include('/var/www/vhosts/interactivechaos.ovh/httpdocs/modules/contrib/google_analytics/src/Component/Render/GoogleAnalyticsJavaScriptSnippet.php') (Line: 578)
Composer\Autoload\ClassLoader::Composer\Autoload\{closure}('/var/www/vhosts/interactivechaos.ovh/httpdocs/modules/contrib/google_analytics/src/Component/Render/GoogleAnalyticsJavaScriptSnippet.php') (Line: 432)
Composer\Autoload\ClassLoader->loadClass('Drupal\google_analytics\Component\Render\GoogleAnalyticsJavaScriptSnippet') (Line: 372)
google_analytics_page_attachments(Array) (Line: 313)
Drupal\Core\Render\MainContent\HtmlRenderer->Drupal\Core\Render\MainContent\{closure}(Object, 'google_analytics') (Line: 405)
Drupal\Core\Extension\ModuleHandler->invokeAllWith('page_attachments', Object) (Line: 310)
Drupal\Core\Render\MainContent\HtmlRenderer->invokePageAttachmentHooks(Array) (Line: 288)
Drupal\Core\Render\MainContent\HtmlRenderer->prepare(Array, Object, Object) (Line: 132)
Drupal\Core\Render\MainContent\HtmlRenderer->renderResponse(Array, Object, Object) (Line: 90)
Drupal\Core\EventSubscriber\MainContentViewSubscriber->onViewRenderArray(Object, 'kernel.view', Object)
call_user_func(Array, Object, 'kernel.view', Object) (Line: 142)
Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher->dispatch(Object, 'kernel.view') (Line: 174)
Symfony\Component\HttpKernel\HttpKernel->handleRaw(Object, 1) (Line: 81)
Symfony\Component\HttpKernel\HttpKernel->handle(Object, 1, 1) (Line: 58)
Drupal\Core\StackMiddleware\Session->handle(Object, 1, 1) (Line: 48)
Drupal\Core\StackMiddleware\KernelPreHandle->handle(Object, 1, 1) (Line: 191)
Drupal\page_cache\StackMiddleware\PageCache->fetch(Object, 1, 1) (Line: 128)
Drupal\page_cache\StackMiddleware\PageCache->lookup(Object, 1, 1) (Line: 82)
Drupal\page_cache\StackMiddleware\PageCache->handle(Object, 1, 1) (Line: 48)
Drupal\Core\StackMiddleware\ReverseProxyMiddleware->handle(Object, 1, 1) (Line: 51)
Drupal\Core\StackMiddleware\NegotiationMiddleware->handle(Object, 1, 1) (Line: 23)
Stack\StackedHttpKernel->handle(Object, 1, 1) (Line: 713)
Drupal\Core\DrupalKernel->handle(Object) (Line: 19)
- Deprecated function: Use of "static" in callables is deprecated in Drupal\user\Entity\Role::postLoad() (line 172 of core/modules/user/src/Entity/Role.php).
Drupal\user\Entity\Role::postLoad(Object, Array) (Line: 423)
Drupal\Core\Entity\EntityStorageBase->postLoad(Array) (Line: 353)
Drupal\Core\Entity\EntityStorageBase->loadMultiple() (Line: 126)
eu_cookie_compliance_page_attachments(Array) (Line: 313)
Drupal\Core\Render\MainContent\HtmlRenderer->Drupal\Core\Render\MainContent\{closure}(Object, 'eu_cookie_compliance') (Line: 405)
Drupal\Core\Extension\ModuleHandler->invokeAllWith('page_attachments', Object) (Line: 310)
Drupal\Core\Render\MainContent\HtmlRenderer->invokePageAttachmentHooks(Array) (Line: 288)
Drupal\Core\Render\MainContent\HtmlRenderer->prepare(Array, Object, Object) (Line: 132)
Drupal\Core\Render\MainContent\HtmlRenderer->renderResponse(Array, Object, Object) (Line: 90)
Drupal\Core\EventSubscriber\MainContentViewSubscriber->onViewRenderArray(Object, 'kernel.view', Object)
call_user_func(Array, Object, 'kernel.view', Object) (Line: 142)
Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher->dispatch(Object, 'kernel.view') (Line: 174)
Symfony\Component\HttpKernel\HttpKernel->handleRaw(Object, 1) (Line: 81)
Symfony\Component\HttpKernel\HttpKernel->handle(Object, 1, 1) (Line: 58)
Drupal\Core\StackMiddleware\Session->handle(Object, 1, 1) (Line: 48)
Drupal\Core\StackMiddleware\KernelPreHandle->handle(Object, 1, 1) (Line: 191)
Drupal\page_cache\StackMiddleware\PageCache->fetch(Object, 1, 1) (Line: 128)
Drupal\page_cache\StackMiddleware\PageCache->lookup(Object, 1, 1) (Line: 82)
Drupal\page_cache\StackMiddleware\PageCache->handle(Object, 1, 1) (Line: 48)
Drupal\Core\StackMiddleware\ReverseProxyMiddleware->handle(Object, 1, 1) (Line: 51)
Drupal\Core\StackMiddleware\NegotiationMiddleware->handle(Object, 1, 1) (Line: 23)
Stack\StackedHttpKernel->handle(Object, 1, 1) (Line: 713)
Drupal\Core\DrupalKernel->handle(Object) (Line: 19)
- Deprecated function: Creation of dynamic property Drupal\mysql\Driver\Database\mysql\Select::$alterMetaData is deprecated in Drupal\Core\Database\Query\Select->addMetaData() (line 178 of core/lib/Drupal/Core/Database/Query/Select.php).
Drupal\Core\Database\Query\Select->addMetaData('entity_type', 'node') (Line: 115)
Drupal\Core\Entity\Query\Sql\Query->prepare() (Line: 80)
Drupal\Core\Entity\Query\Sql\Query->execute() (Line: 167)
Drupal\book\Plugin\Block\BookNavigationBlock->build() (Line: 171)
Drupal\block\BlockViewBuilder::preRender(Array)
call_user_func_array(Array, Array) (Line: 101)
Drupal\Core\Render\Renderer->doTrustedCallback(Array, Array, 'Render #pre_render callbacks must be methods of a class that implements \Drupal\Core\Security\TrustedCallbackInterface or be an anonymous function. The callback was %s. See https://www.drupal.org/node/2966725', 'exception', 'Drupal\Core\Render\Element\RenderCallbackInterface') (Line: 788)
Drupal\Core\Render\Renderer->doCallback('#pre_render', Array, Array) (Line: 374)
Drupal\Core\Render\Renderer->doRender(Array, 1) (Line: 204)
Drupal\Core\Render\Renderer->render(Array, 1) (Line: 160)
Drupal\Core\Render\Renderer->Drupal\Core\Render\{closure}() (Line: 580)
Drupal\Core\Render\Renderer->executeInRenderContext(Object, Object) (Line: 159)
Drupal\Core\Render\Renderer->renderPlain(Array) (Line: 175)
Drupal\Core\Render\Renderer->renderPlaceholder('', Array) (Line: 665)
Drupal\Core\Render\Renderer->replacePlaceholders(Array) (Line: 550)
Drupal\Core\Render\Renderer->doRender(Array, 1) (Line: 204)
Drupal\Core\Render\Renderer->render(Array, 1) (Line: 148)
Drupal\Core\Render\Renderer->Drupal\Core\Render\{closure}() (Line: 580)
Drupal\Core\Render\Renderer->executeInRenderContext(Object, Object) (Line: 147)
Drupal\Core\Render\Renderer->renderRoot(Array) (Line: 279)
Drupal\Core\Render\HtmlResponseAttachmentsProcessor->renderPlaceholders(Object) (Line: 71)
Drupal\big_pipe\Render\BigPipeResponseAttachmentsProcessor->processAttachments(Object) (Line: 45)
Drupal\Core\EventSubscriber\HtmlResponseSubscriber->onRespond(Object, 'kernel.response', Object)
call_user_func(Array, Object, 'kernel.response', Object) (Line: 142)
Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher->dispatch(Object, 'kernel.response') (Line: 202)
Symfony\Component\HttpKernel\HttpKernel->filterResponse(Object, Object, 1) (Line: 190)
Symfony\Component\HttpKernel\HttpKernel->handleRaw(Object, 1) (Line: 81)
Symfony\Component\HttpKernel\HttpKernel->handle(Object, 1, 1) (Line: 58)
Drupal\Core\StackMiddleware\Session->handle(Object, 1, 1) (Line: 48)
Drupal\Core\StackMiddleware\KernelPreHandle->handle(Object, 1, 1) (Line: 191)
Drupal\page_cache\StackMiddleware\PageCache->fetch(Object, 1, 1) (Line: 128)
Drupal\page_cache\StackMiddleware\PageCache->lookup(Object, 1, 1) (Line: 82)
Drupal\page_cache\StackMiddleware\PageCache->handle(Object, 1, 1) (Line: 48)
Drupal\Core\StackMiddleware\ReverseProxyMiddleware->handle(Object, 1, 1) (Line: 51)
Drupal\Core\StackMiddleware\NegotiationMiddleware->handle(Object, 1, 1) (Line: 23)
Stack\StackedHttpKernel->handle(Object, 1, 1) (Line: 713)
Drupal\Core\DrupalKernel->handle(Object) (Line: 19)
- Deprecated function: Creation of dynamic property Drupal\mysql\Driver\Database\mysql\Select::$alterTags is deprecated in Drupal\Core\Database\Query\Select->addTag() (line 149 of core/lib/Drupal/Core/Database/Query/Select.php).
Drupal\Core\Database\Query\Select->addTag('node_access') (Line: 145)
Drupal\Core\Entity\Query\Sql\Query->prepare() (Line: 80)
Drupal\Core\Entity\Query\Sql\Query->execute() (Line: 167)
Drupal\book\Plugin\Block\BookNavigationBlock->build() (Line: 171)
Drupal\block\BlockViewBuilder::preRender(Array)
call_user_func_array(Array, Array) (Line: 101)
Drupal\Core\Render\Renderer->doTrustedCallback(Array, Array, 'Render #pre_render callbacks must be methods of a class that implements \Drupal\Core\Security\TrustedCallbackInterface or be an anonymous function. The callback was %s. See https://www.drupal.org/node/2966725', 'exception', 'Drupal\Core\Render\Element\RenderCallbackInterface') (Line: 788)
Drupal\Core\Render\Renderer->doCallback('#pre_render', Array, Array) (Line: 374)
Drupal\Core\Render\Renderer->doRender(Array, 1) (Line: 204)
Drupal\Core\Render\Renderer->render(Array, 1) (Line: 160)
Drupal\Core\Render\Renderer->Drupal\Core\Render\{closure}() (Line: 580)
Drupal\Core\Render\Renderer->executeInRenderContext(Object, Object) (Line: 159)
Drupal\Core\Render\Renderer->renderPlain(Array) (Line: 175)
Drupal\Core\Render\Renderer->renderPlaceholder('', Array) (Line: 665)
Drupal\Core\Render\Renderer->replacePlaceholders(Array) (Line: 550)
Drupal\Core\Render\Renderer->doRender(Array, 1) (Line: 204)
Drupal\Core\Render\Renderer->render(Array, 1) (Line: 148)
Drupal\Core\Render\Renderer->Drupal\Core\Render\{closure}() (Line: 580)
Drupal\Core\Render\Renderer->executeInRenderContext(Object, Object) (Line: 147)
Drupal\Core\Render\Renderer->renderRoot(Array) (Line: 279)
Drupal\Core\Render\HtmlResponseAttachmentsProcessor->renderPlaceholders(Object) (Line: 71)
Drupal\big_pipe\Render\BigPipeResponseAttachmentsProcessor->processAttachments(Object) (Line: 45)
Drupal\Core\EventSubscriber\HtmlResponseSubscriber->onRespond(Object, 'kernel.response', Object)
call_user_func(Array, Object, 'kernel.response', Object) (Line: 142)
Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher->dispatch(Object, 'kernel.response') (Line: 202)
Symfony\Component\HttpKernel\HttpKernel->filterResponse(Object, Object, 1) (Line: 190)
Symfony\Component\HttpKernel\HttpKernel->handleRaw(Object, 1) (Line: 81)
Symfony\Component\HttpKernel\HttpKernel->handle(Object, 1, 1) (Line: 58)
Drupal\Core\StackMiddleware\Session->handle(Object, 1, 1) (Line: 48)
Drupal\Core\StackMiddleware\KernelPreHandle->handle(Object, 1, 1) (Line: 191)
Drupal\page_cache\StackMiddleware\PageCache->fetch(Object, 1, 1) (Line: 128)
Drupal\page_cache\StackMiddleware\PageCache->lookup(Object, 1, 1) (Line: 82)
Drupal\page_cache\StackMiddleware\PageCache->handle(Object, 1, 1) (Line: 48)
Drupal\Core\StackMiddleware\ReverseProxyMiddleware->handle(Object, 1, 1) (Line: 51)
Drupal\Core\StackMiddleware\NegotiationMiddleware->handle(Object, 1, 1) (Line: 23)
Stack\StackedHttpKernel->handle(Object, 1, 1) (Line: 713)
Drupal\Core\DrupalKernel->handle(Object) (Line: 19)
- Deprecated function: Creation of dynamic property Drupal\mysql\Driver\Database\mysql\Select::$alterTags is deprecated in Drupal\Core\Database\Query\Select->addTag() (line 149 of core/lib/Drupal/Core/Database/Query/Select.php).
Drupal\Core\Database\Query\Select->addTag('node_access') (Line: 51)
Drupal\book\BookOutlineStorage->loadMultiple(Array) (Line: 1021)
Drupal\book\BookManager->bookTreeCheckAccess(Array, Array) (Line: 701)
Drupal\book\BookManager->bookTreeBuild('828', Array) (Line: 567)
Drupal\book\BookManager->bookTreeAllData('828', Array) (Line: 171)
Drupal\book\Plugin\Block\BookNavigationBlock->build() (Line: 171)
Drupal\block\BlockViewBuilder::preRender(Array)
call_user_func_array(Array, Array) (Line: 101)
Drupal\Core\Render\Renderer->doTrustedCallback(Array, Array, 'Render #pre_render callbacks must be methods of a class that implements \Drupal\Core\Security\TrustedCallbackInterface or be an anonymous function. The callback was %s. See https://www.drupal.org/node/2966725', 'exception', 'Drupal\Core\Render\Element\RenderCallbackInterface') (Line: 788)
Drupal\Core\Render\Renderer->doCallback('#pre_render', Array, Array) (Line: 374)
Drupal\Core\Render\Renderer->doRender(Array, 1) (Line: 204)
Drupal\Core\Render\Renderer->render(Array, 1) (Line: 160)
Drupal\Core\Render\Renderer->Drupal\Core\Render\{closure}() (Line: 580)
Drupal\Core\Render\Renderer->executeInRenderContext(Object, Object) (Line: 159)
Drupal\Core\Render\Renderer->renderPlain(Array) (Line: 175)
Drupal\Core\Render\Renderer->renderPlaceholder('', Array) (Line: 665)
Drupal\Core\Render\Renderer->replacePlaceholders(Array) (Line: 550)
Drupal\Core\Render\Renderer->doRender(Array, 1) (Line: 204)
Drupal\Core\Render\Renderer->render(Array, 1) (Line: 148)
Drupal\Core\Render\Renderer->Drupal\Core\Render\{closure}() (Line: 580)
Drupal\Core\Render\Renderer->executeInRenderContext(Object, Object) (Line: 147)
Drupal\Core\Render\Renderer->renderRoot(Array) (Line: 279)
Drupal\Core\Render\HtmlResponseAttachmentsProcessor->renderPlaceholders(Object) (Line: 71)
Drupal\big_pipe\Render\BigPipeResponseAttachmentsProcessor->processAttachments(Object) (Line: 45)
Drupal\Core\EventSubscriber\HtmlResponseSubscriber->onRespond(Object, 'kernel.response', Object)
call_user_func(Array, Object, 'kernel.response', Object) (Line: 142)
Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher->dispatch(Object, 'kernel.response') (Line: 202)
Symfony\Component\HttpKernel\HttpKernel->filterResponse(Object, Object, 1) (Line: 190)
Symfony\Component\HttpKernel\HttpKernel->handleRaw(Object, 1) (Line: 81)
Symfony\Component\HttpKernel\HttpKernel->handle(Object, 1, 1) (Line: 58)
Drupal\Core\StackMiddleware\Session->handle(Object, 1, 1) (Line: 48)
Drupal\Core\StackMiddleware\KernelPreHandle->handle(Object, 1, 1) (Line: 191)
Drupal\page_cache\StackMiddleware\PageCache->fetch(Object, 1, 1) (Line: 128)
Drupal\page_cache\StackMiddleware\PageCache->lookup(Object, 1, 1) (Line: 82)
Drupal\page_cache\StackMiddleware\PageCache->handle(Object, 1, 1) (Line: 48)
Drupal\Core\StackMiddleware\ReverseProxyMiddleware->handle(Object, 1, 1) (Line: 51)
Drupal\Core\StackMiddleware\NegotiationMiddleware->handle(Object, 1, 1) (Line: 23)
Stack\StackedHttpKernel->handle(Object, 1, 1) (Line: 713)
Drupal\Core\DrupalKernel->handle(Object) (Line: 19)
- Deprecated function: Creation of dynamic property Drupal\mysql\Driver\Database\mysql\Select::$alterMetaData is deprecated in Drupal\Core\Database\Query\Select->addMetaData() (line 178 of core/lib/Drupal/Core/Database/Query/Select.php).
Drupal\Core\Database\Query\Select->addMetaData('base_table', 'book') (Line: 52)
Drupal\book\BookOutlineStorage->loadMultiple(Array) (Line: 1021)
Drupal\book\BookManager->bookTreeCheckAccess(Array, Array) (Line: 701)
Drupal\book\BookManager->bookTreeBuild('828', Array) (Line: 567)
Drupal\book\BookManager->bookTreeAllData('828', Array) (Line: 171)
Drupal\book\Plugin\Block\BookNavigationBlock->build() (Line: 171)
Drupal\block\BlockViewBuilder::preRender(Array)
call_user_func_array(Array, Array) (Line: 101)
Drupal\Core\Render\Renderer->doTrustedCallback(Array, Array, 'Render #pre_render callbacks must be methods of a class that implements \Drupal\Core\Security\TrustedCallbackInterface or be an anonymous function. The callback was %s. See https://www.drupal.org/node/2966725', 'exception', 'Drupal\Core\Render\Element\RenderCallbackInterface') (Line: 788)
Drupal\Core\Render\Renderer->doCallback('#pre_render', Array, Array) (Line: 374)
Drupal\Core\Render\Renderer->doRender(Array, 1) (Line: 204)
Drupal\Core\Render\Renderer->render(Array, 1) (Line: 160)
Drupal\Core\Render\Renderer->Drupal\Core\Render\{closure}() (Line: 580)
Drupal\Core\Render\Renderer->executeInRenderContext(Object, Object) (Line: 159)
Drupal\Core\Render\Renderer->renderPlain(Array) (Line: 175)
Drupal\Core\Render\Renderer->renderPlaceholder('', Array) (Line: 665)
Drupal\Core\Render\Renderer->replacePlaceholders(Array) (Line: 550)
Drupal\Core\Render\Renderer->doRender(Array, 1) (Line: 204)
Drupal\Core\Render\Renderer->render(Array, 1) (Line: 148)
Drupal\Core\Render\Renderer->Drupal\Core\Render\{closure}() (Line: 580)
Drupal\Core\Render\Renderer->executeInRenderContext(Object, Object) (Line: 147)
Drupal\Core\Render\Renderer->renderRoot(Array) (Line: 279)
Drupal\Core\Render\HtmlResponseAttachmentsProcessor->renderPlaceholders(Object) (Line: 71)
Drupal\big_pipe\Render\BigPipeResponseAttachmentsProcessor->processAttachments(Object) (Line: 45)
Drupal\Core\EventSubscriber\HtmlResponseSubscriber->onRespond(Object, 'kernel.response', Object)
call_user_func(Array, Object, 'kernel.response', Object) (Line: 142)
Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher->dispatch(Object, 'kernel.response') (Line: 202)
Symfony\Component\HttpKernel\HttpKernel->filterResponse(Object, Object, 1) (Line: 190)
Symfony\Component\HttpKernel\HttpKernel->handleRaw(Object, 1) (Line: 81)
Symfony\Component\HttpKernel\HttpKernel->handle(Object, 1, 1) (Line: 58)
Drupal\Core\StackMiddleware\Session->handle(Object, 1, 1) (Line: 48)
Drupal\Core\StackMiddleware\KernelPreHandle->handle(Object, 1, 1) (Line: 191)
Drupal\page_cache\StackMiddleware\PageCache->fetch(Object, 1, 1) (Line: 128)
Drupal\page_cache\StackMiddleware\PageCache->lookup(Object, 1, 1) (Line: 82)
Drupal\page_cache\StackMiddleware\PageCache->handle(Object, 1, 1) (Line: 48)
Drupal\Core\StackMiddleware\ReverseProxyMiddleware->handle(Object, 1, 1) (Line: 51)
Drupal\Core\StackMiddleware\NegotiationMiddleware->handle(Object, 1, 1) (Line: 23)
Stack\StackedHttpKernel->handle(Object, 1, 1) (Line: 713)
Drupal\Core\DrupalKernel->handle(Object) (Line: 19)