Juan David Velásquez Henao
jdvelasq@unal.edu.co
Universidad Nacional de Colombia, Sede Medellín
Facultad de Minas
Medellín, Colombia
[Licencia]
[Readme]
Software utilizado.
Este es un documento interactivo escrito como un notebook de Jupyter, en el cual se presenta un tutorial sobre regresión logistica usando R en el contexto de aprendizaje de maquinas. Los notebooks de Jupyter permiten incoporar simultáneamente código, texto, gráficos y ecuaciones. El código presentado en este notebook puede ejecutarse en los sistemas operativos Linux y OS X.
Haga click aquí para obtener instrucciones detalladas sobre como instalar Jupyter en Windows y Mac OS X.
Haga clic [aquí] para ver la última versión de este documento en nbviewer.
Descargue la última versión de este documento a su disco duro; luego, carguelo y ejecutelo en línea en Try Jupyter!
Haga clic aquí para ver el tutorial de visualización y gráficas.
Para producir un modelo combinado que prediga de forma más precisa que cada modelo individual separado. En este notebook se abordarán tres métodos: Bagging, Boosting y Random Forest. Para esto se utilizarán modelos vistos en clases anteriores y cómo estos métodos mejoran el poder predictivo de estos.
Este primer método de ensamble conocido como Bagging se basa en utilizar diferentes muestras de todo el conjunto de observaciones para entrenar múltiples versiones del mismo modelo. Cada uno de estos modelos votan (arrojan su predicción) por la respuesta correcta donde gana o se escoje el valor de la predicción con mayores "votos" (es decir, por mayoría) cuando se trata de una variable de respuesta categórica. Análogamente para una variable de respuesta continua el valor de predicción es el promedio de todos los valores arrojados por los modelos.
Básicamente el algoritmo conlleva a entrenar el mismo modelo $M$ veces cada una con diferentes datos de entrenamiento, a través de muestreo con reemplazo, y promediar los resultados de cada uno para tener una salida final. Esta técnica permite inferir que en promedio se obtiene el 63% de observaciones distintas cada vez que se realiza la muestra utilizando reemplazo.
Dentro de este algoritmo las variables de entrada son la base completa de observaciones y la cantidad de modelos a entrenar $M$.
Crear una muestra aleatoria de tamaño $n$ (tamaño de toda la base de datos). Es decir que quedaran observaciones repetidas y otras por fuera. Este proceso se conoce como bootstraping.
Entrenar un modelo de clasificacion usando esta muestra de los datos. Usualmente, no es recomendable utilizar algortimos de regularización o modelos reducidos diseñados para combatir el sobre-ajuste ya que el proceso de agregación es usado al final es capaz de suavizar el ajuste.
Para cada observacion en la muestra de los datos, se almacena la clase asignada por el modelo (0 o 1).
Repertir este proceso $M$ veces para entrenar $M$ modelos.
Para cada observacion en la base de datos original (sin muestreo), se calcula la predicción final de la clase a través del conteo de los resultados de cada modelo donde se escoge aquella con mayor número. Es decir, que si se entrenaron $M=25$, donde 20 de estos dicen que la clase es 0 y los otros 5 dicen que la clase es 1, la predicción final de dicha observación es 0.
Calcular la precisión del modelo.
In [5]:
## Instale y cargue las siguientes librerías
library(rpart)
library(caret)
library(ipred)
In [3]:
## Codigos del capítulo 6. (Para obtener la descripcion de los codigos dirigirese al capitulo)
set.seed(266)
link <-"https://archive.ics.uci.edu/ml/machine-learning-databases/00272/SkillCraft1_Dataset.csv"
skillcraft <-read.csv(url(link))
skillcraft <- skillcraft[-1]
skillcraft$TotalHours <- as.numeric(levels(skillcraft$TotalHours))[skillcraft$TotalHours]
skillcraft$HoursPerWeek <- as.numeric(levels(skillcraft$HoursPerWeek))[skillcraft$HoursPerWeek]
skillcraft$Age <- as.numeric(levels(skillcraft$Age))[skillcraft$Age]
skillcraft <- skillcraft[complete.cases(skillcraft),]
skillcraft_sampling_vector <- createDataPartition(skillcraft$LeagueIndex, p = 0.80, list = FALSE)
skillcraft_train <- skillcraft[skillcraft_sampling_vector,]
skillcraft_test <- skillcraft[-skillcraft_sampling_vector,]
In [6]:
## Bagging al árbol CART
baggedtree <- bagging(LeagueIndex ~ ., # Formula del arbol
data = skillcraft_train, # Data de entrenamiento
nbagg = 100, # Numero de replicas en bootstrapping
coob = T) # calculo estimado out-of-bag estimate de la tasa de error
## Predicción con el bagged tree
baggedtree_predictions <- predict(baggedtree, # Modelo CART bagged
skillcraft_test) # Datos de validación
## Función para calcular el SSE
compute_SSE <- function(correct, predictions) {
return(sum((correct - predictions) ^ 2)) # Suma de los errores al cuadrado.
}
## Calculo error SSE
(baggedtree_SSE <- compute_SSE(baggedtree_predictions, # Valores pronosticados
skillcraft_test$LeagueIndex)) # Valores de validacion
El valor de $SSE$ es de 684.92 el cual es menor que el arbol con mejoramiento de parámetros estimado en el capitlo 6 (701.33)
In [ ]:
## Instale y cargue las siguientes
library(caret)
In [7]:
## Codigos del notebook - Regresión Logística (Para obtener la descripcion de los codigos dirigirese al notebook)
heart <- read.table("http://archive.ics.uci.edu/ml/machine-learning-databases/statlog/heart/heart.dat",
quote="\"")
names(heart) <- c("AGE", "SEX", "CHESTPAIN", "RESTBP", "CHOL",
"SUGAR", "ECG", "MAXHR", "ANGINA", "DEP",
"EXERCISE", "FLUOR","THAL", "OUTPUT")
heart$CHESTPAIN <- factor(heart$CHESTPAIN)
heart$ECG <- factor(heart$ECG)
heart$THAL <- factor(heart$THAL)
heart$EXERCISE <- factor(heart$EXERCISE)
heart$OUTPUT <- heart$OUTPUT - 1
set.seed(987954)
heart_sampling_vector <- createDataPartition(heart$OUTPUT, p = 0.85, list = FALSE)
heart_train <- heart[heart_sampling_vector,]
heart_train_labels <- heart$OUTPUT[heart_sampling_vector]
heart_test <- heart[-heart_sampling_vector,]
heart_test_labels <- heart$OUTPUT[-heart_sampling_vector]
Ya con la base de datos cargada y depurada, se configura los parámetros de modelo de ensamble empezando por la obtención de los vectores de muestro aleatorio con reemplazo bootstraping
In [8]:
## Parámetros del modelo
M <- 11 # Numero de modelos a entrenar
seeds <- 70000 : (70000 + M - 1) # Semillas de cada modelo
n <- nrow(heart_train) # Número de observaciones
## Generación de las posiciones del muestreo con reemplazo Bagging
sample_vectors <- sapply(seeds, # Para cada semilla hacemos
function(x) {set.seed(x); # Establecer la semilla aleatoria
return(sample(n, # Devolver el muestreo del 1 al n
n, # Una muestra de n elementos
replace = T)) # Con reemplazo
}
)
head(sample_vectors) # Primeros valores
Una vez con la matriz de muestreo, se procede a entrenar los $M$ modelos.
In [9]:
## Funcion para realizar el modelo logístico
train_1glm <- function(sample_indices) { # Funcion "train_1glm" que depende de las posiciones del muestreo
data <- heart_train[sample_indices,]; # subset la data con las posiciones de la muestra
model <- glm(OUTPUT ~ ., # Modelo con variable respuesta "OUTPUT"
data = data, # Datos de entrada
family = binomial("logit")); # Regresion Logistica
return(model) # La función devuelve el modelo
}
## Estimación los M modelos
models <- apply(sample_vectors, # A cada vector de muestreo le aplicamos la función
2, # Indicador de aplicación por columnas
train_1glm) # Función de modelo logístico
head(models) # Primeros valores
Una vez entrenados los modelos, se extrae la data de entrenamiento de cada modelo pero sin repeticiones lo cual servierá para hacer medidas de ajuste.
In [15]:
## Funcion para obtener los valores de entrenamiento para cada modelo sin repetición
get_1bag <- function(sample_indices) { # Funcion "get_1bag" que depende de las posiciones del muestreo
unique_sample <- unique(sample_indices); # Posiciones unicas del muestreo
df <- heart_train[unique_sample, ]; # Observaciones de la muestra sin repetirse.
df$ID <- unique_sample; # Agregar columna de identificador de la posisión de la muestra
return(df) # Mostrar el data.frame
}
## Se obtine los bags
bags <- apply(sample_vectors, # Vector de posiciones
2, # Aplicamos por columnas
get_1bag) # Función de obtener los bags únicos
#head(bags)
Predicción con los modelos.
In [17]:
## Función para predecir con el modelo
glm_predictions <- function(model, data, model_index) {
colname <- paste("PREDICTIONS",
model_index);
data[colname] <- as.numeric(
predict(model,
data,
type = "response") > 0.5);
return(data[,c("ID", colname),
drop = FALSE])
}
## Predicciones de cada modelo
training_predictions <- mapply(glm_predictions, # Función a aplicar
models, # Cada uno de los 11 modelos
bags, # Bags (Registros unicos de entrenamiento)
1 : M, # Indice de cada modelo
SIMPLIFY = F) # Devuelve una lista
#head (training_predictions)
Se agrupan todas las predicciones de cada modelo para cada registro. Esto se realiza haciendo un join entre cada predicción con la llave del ID de la fila, luego se reducen las filas y se coloca NA donde no haya predicción.
In [18]:
## Agrupación de todas las predicciones en un data_frame
train_pred_df <- Reduce(function(x, y) merge(x, y, by = "ID", all = T), # Reducción de la matriz a unicamente el merge (JOIN)
training_predictions) # Matriz a aplicar el Join y el Reduce
head(train_pred_df)
In [19]:
## Resultado final del modelo Bagging
train_pred_vote <- apply(train_pred_df, # Data frame al cual le vamos a aplicar
1, # Aplicar por filas
function(x) as.numeric(mean(x, na.rm = TRUE) > 0.5)) # Calcular la media de cada fila. Si la media es mayor que 0.5, la predicción es 1, de lo contrario 0.
head(train_pred_vote)
In [21]:
## calculo de la precisión media del algoritmo
(training_accuracy <- mean(train_pred_vote == # Valores predichos
heart_train$OUTPUT[as.numeric(train_pred_df$ID)])) # Valores reales
Ejercicio.-- A partir de la base de datos de clasificacion de vinos utilice el bagging para entrenar una regresión logística y mejorar el desempeño de los resultados del modelo individual en categorizar cada uno dentro de las 3 clases. Parametrice un porcentaje de entramiento-validación de 90-10; tenga cuidad de no desbalancear las muestras.
Las características de los vinos son:
In [ ]:
Ejercicio.-- Utilice la base de datos de detección de graude en tarjetas de credito en Septiembre de 2013. La descripción de la base de datos se puede enconrtar en la página de Kaggle
Notas a la base de datos: "El conjunto de datos es altamente desequilibrado, la clase positiva (fraudes) representa el 0,172% de todas las transacciones. Contiene sólo variables numéricas de entrada que son el resultado de una transformación PCA. Lamentablemente, debido a problemas de confidencialidad, no podemos proporcionar las características originales y más información de fondo sobre los datos. Las características V1, V2, ... V28 son los componentes principales obtenidos con PCA, las únicas características que no han sido transformadas con PCA son 'Tiempo' y 'Cantidad'. La característica 'Tiempo' contiene los segundos transcurridos entre cada transacción y la primera transacción en el conjunto de datos. El campo 'Import' es la cantidad de la transacción. La característica 'Class' es la variable de respuesta y toma el valor 1 en caso de fraude y 0 en caso contrario. Dada la relación de desequilibrio de clase, recomendamos medir la precisión usando el área bajo la curva Precision-Recall (AUPRC). La precisión de la matriz de confusión no es significativa para la clasificación desequilibrada."
Para este ejercicio, se recomienda balancear la base de datos a un 1:5 o 1:6 (por cada 6 transacciones no fraudlentas, existe 1 fraudlenta). Puede utilizar cualquiera de las técnicas ya vistas en clase (regresión logística, redes neuronales, árboles de decisión). Lo importante es que utilice el concepto de bagging para mostrar la mejora en el desempeño de los modelos individuales.
In [ ]:
El Boosting consiste en entrenar una cadena de modelos y asignarle pesos a las observaciones que fueron clasificadas incorrectamente o cayeron muy lejos de su valor esperado, de tal forma que los modelos siguientes estén forzados a priorizar dichas observaciones.
Este metodo aforce una alternativa especialmente para aquellos algoritmos que son "débiles", es decir, que producen una predicción un poco mejor que elección al azar. En estos modelos usualmente la complejidad es baja, no obstante, se pueden entrenar modelos cuyo parámetro de complejidad sea configurable como las redes neuronales o árbole de decisión.
Una de las diferencias del Boosting respecto al bagging es que no existe un componente aleatorio al momento de elegir los datos de entranmiento. Todos los modelos se entrenan con la misma base de entrenamiento original. Otra diferencia es la cantidad de existente de tecnicas de boosting para abordar los problemas (AdaBoost, BrownBoost, Stochastic Gradient Boost, CoBoost) mientras que el bagging no presenta técnicas con cambios importantes dentro de su implementación.
De forma general, el boosting parte construyendo un modelo con las observaciones de entrenamiento y midiendo la precisión en los mismos datos. Cada una de las observaciones que fueron erroneamente identificadas por el modelo se les da un peso más grande de aquuellas que fueron correctamente clasificadas. De esta forma, el modelo se re-entrena usando estos pesos. Estos pasos se repiten multiples veces, ajustando nuevamente los pesos de los datos erroneamente categorizados en la iteración anterior.
Lógicamente, si este proceso sigue indefinidamente el modelo de la iteración final estará sobre-ajustado. Por lo tanto, para evitar este inconveniente, el modelo ensamblado se construye a partir de el promedio ponderado (usualmente son proporcionales a la precisión) de todos los modelos entrenados en el proceso. Es decir, desde el modelo inicial hasta el modelo final de todo el proceso de ajuste de pesos. Usualmente, en modelos de regresión se ajustan los pesos de las observaciones en base a alguna médida de distancia entre el valor predicho y el valor real.
Existen dos tipos Adaptative Boost (AdaBoost): Discreta (binaria) y Real (multinomial). No obstante también existen extensiones para problemas de regresión. El input de este tipo de algoritmos son los mismos que en el bagging, la base completa de observaciones y la cantidad de modelos a entrenar M.
Se inicia el vector de pesos de cada observación, $w$, de longitud $n$ (numero de observaciones) con el valor de $ w_i = \frac {1}{n} $. Estos valores son actualizados en cada iteración
Se usa el vector actual de pesos y todos los datos para entrenar un modelo $ G_m $.
Se calcula la tasa de error ponderado como la suma de todas las observaciones mal clasificadas multiplicada por su peso, dividido por la suma del vector de pesos. Esto se expresa como:
Se actualizan los pesos de las observaciones $w$ para la próxima iteracion. Los eventos de observaciones mal clasificadas se multiplican su peso actual por $e^{a_m}$, lo que hace que incremente dicho peso. Por el contrario, aquellas que fueron bien clasificadas, se multiplican por $e^{-a_m}$, reduciendo su peso dentro del modelo.
Se renormaliza los pesos del vector de tal forma que la suma de ellos den 1.
Repetir los pasos del 2 al 6, M veces para producir M modelos.
Se define el modelo final como la función signo de la suma ponderada de las salidas de todos los modelos $ G_m $, luego:
In [25]:
## Instale y cargue las siguientes librerías
library(caret)
library(nnet)
In [22]:
## Lectura de datos
magic <- read.csv("http://archive.ics.uci.edu/ml/machine-learning-databases/magic/magic04.data",
header=FALSE) # Sin encabezados
names(magic) <- c("FLENGTH", "FWIDTH", "FSIZE", "FCONC", "FCONC1","FASYM",
"FM3LONG", "FM3TRANS", "FALPHA", "FDIST", "CLASS") # Nombres columnas
magic$CLASS <- as.factor(ifelse(magic$CLASS =='g', 1, -1)) # Conversion CLASS a factor (-1,1)
head(magic)
Creación datasets de entrenamiento y validación.
In [23]:
## Datos de training y test
set.seed(33711209) # Semilla de aleatoriedad
magic_sampling_vector <- createDataPartition(magic$CLASS, # Posiciones de variable CLASS
p = 0.80, # Proporcion de set de training
list = FALSE) # Devolver como lista es FALSO
magic_train <- magic[magic_sampling_vector, 1:10] # Subset de la data de training sin CLASS (Predictoras)
magic_train_output <- magic[magic_sampling_vector, 11] # Subset de la data de training de CLASS (Respuesta)
magic_test <- magic[-magic_sampling_vector, 1:10] # Subset de la data de test sin CLASS (Predictoras)
magic_test_output <- magic[-magic_sampling_vector, 11] # Subset de la data de test de CLASS (Respuesta)
Normalización de los datos con media igual a cero (0) y desviación estándar igual a uno (1)
In [24]:
## Transoformacion de los datos
magic_pp <- preProcess(magic_train, # Generar un modelo de normalización estándar.
method = c("center", "scale")) # Center = Media 0, Scale = Division por la desviación
magic_train_pp <- predict(magic_pp, # Normalización de la data de entrenamiento
magic_train)
magic_train_df_pp <- cbind(magic_train_pp, # Juntar data de entrenamiento normalizada y la variable respuesta
CLASS = magic_train_output)
magic_test_pp <- predict(magic_pp, # Normalización de la data de test
magic_test)
Entrenamiento de una red neuronal con una capa oculta.
In [26]:
## Entrenamos un modelo de red neural con una capa de la misma forma que el capitulo 4.
n_model <- nnet(CLASS ~ ., # Variable respuesta CLASS, predictoras el resto
data = magic_train_df_pp, # Data de entrenamiento normalizada
size = 1) # Tamaño de las capas ocultas
n_test_predictions <- predict(n_model, # Predecimos con la red neuronal
magic_test_pp, # Data de test
type = "class") # Predicción de CLASS (NO PROBABILIDAD)
## calculo de la precisión media del algoritmo
(n_test_accuracy <- mean(n_test_predictions == magic_test_output))
Creación de las propias funciones personalizadas para implementar AdaBoost
In [27]:
## Creamos la función AdaBoost
AdaBoostNN <- function(training_data, output_column, M, hidden_units) { # Funcion AdaBoost (Datos entrenamiento, Variable respuesta, Numero modelos, numero capas escondidas)
require("nnet") # Libreria
models <- list() # lista vacia de modelos
alphas <- list() # lista vacia de alphas
n <- nrow(training_data) # número de observaciones para entrenar
model_formula <- as.formula(paste(output_column, '~ .', sep = '')) # fórmula (Variable respuesta ~ Variables Predictoras)
w <- rep((1/n), n) # Vector de pesos iniciales proporcionales
## Ciclo para los M modelos
for (m in 1:M) { # Ciclo for de 1 a M (numero modelos)
## Generar el modelo y sus predicciones
model <- nnet(model_formula, # Formula
data = training_data, # Datos de training
size = hidden_units, # Capas ocultas
weights = w) # Pesos
models[[m]] <- model # Guardar modelo en la lista
predictions <- as.numeric(predict(model, # Forzar a numerico la predicción delo modelo
training_data[, -which(names(training_data) ==output_column)], # Data training (Sin la variable respuesta)
type = "class")) # Prediccion de clase (no de probabilidad)
## calculo de los errores para ajuste y su corrección
errors <- predictions != training_data[, output_column] # Comparación de las predicciones con los reales
error_rate <- sum(w * as.numeric(errors)) / sum(w) # Calculo de la tasa de error
alpha <- 0.5 * log((1 - error_rate) / error_rate) # Calculo de los alpha de correccion
alphas[[m]] <- alpha # Se almace el alpha
temp_w <- mapply( # Aplicación de la funcion
function(x, y) if (y) { x * exp(alpha) } # Si esta bien clasificada, su peso es con alpha positivo
else { x * exp(-alpha)}, # De lo contrario, su peso es con alpha negativo
w, # Use los pesos actuales como x
errors) # Use los errores como y
w <- temp_w / sum(temp_w) # Normalice los pesos
}
## Termina el for
return(list(models = models, alphas = unlist(alphas))) # Devuelva los modelos, los alphas.
}
## Creacion función para predecir con AdaBoost
AdaBoostNN.predict <- function(ada_model, test_data) { # Función recibe modelo y data test
models <- ada_model$models # Asignación modelos
alphas <- ada_model$alphas # Asignacion alphas
## Creacion matriz de predicciones
prediction_matrix <- sapply(models, # Para cada modelo
function (x) as.numeric(predict(x, # Forzar a numerico las predicciones
test_data, # Con la data de validacion
type = "class"))) # Forzar a salida clase
## Calculo de predicciones ponderadas
weighted_predictions <- t(apply(prediction_matrix, # Transponer el apply de la matriz de prediccion = x
1, # Por filas
function(x) mapply(function(y, z) y * z, # de la multiplicación y * z
x, # Donde y son las predicciones
alphas))) # z son los alphas
## Aplicacion de la función signo (forza -1 o 1)
final_predictions <- apply(weighted_predictions, # Aplicacion a las predicciones ponderadas
1, # Por filas
function(x) sign(sum(x))) # La función signo
return(final_predictions) # Devolver las predicciones
}
Se utiliza las funciones anteriores para entrenar la red neuronal con boosting y predecir.
In [28]:
## Ejecutar el modelos con Boosting y predecimos
ada_model <- AdaBoostNN(magic_train_df_pp, # Datos de entrenamiento
'CLASS', # Variable respuesta
10, # Numero de modelos a entrenar
1) # Numero de capas ocultas de la red
predictions <- AdaBoostNN.predict(ada_model, # Prediccion con el modelo
magic_test_pp, # Datos de testing
'CLASS') # Variable respuesta
## Calculo la precisión media del algoritmo
mean(predictions == magic_test_output)
Ejercicio.-- Utilice la base de datos de sobrevientes del Titanic donde ajuste un modelo de clasificación para predecir si una persona sobrevivió al Titanic a partir de sus características. Mediante estas técnicas mas avanzadas, que modelo tiene mejor desempeño respecto al modelo de regresión logistica realizado al inicio del curso?
In [ ]:
Los bosques aleatorios (Random Forest) es una técnica de ensamble basada en el concepto de árbol que se estudió en el notebook XXXX. Básicamente, la idea se deriva de una situación particular en bagged trees (árboles con bagging). Si se supone que la relación entre las variables predictoras y la respuesta se puede modelar correctamente con un árbol de decisión, es muy probable que dentro del proceso de bagging sigamos escogiendo las mismas variables para particionar las observaciones en todos los modelos. Esto conlleva a que todos los árboles no sean independientes uno de los otros porque tendrán los mismos nodos y valores, por lo tanto el promedio de los resultados será menos exitoso al tratar de reducir la vairanza en el ensamblaje.
Para atacar esto, el algoritmo de bosques aleatorios sigue con el concepto de bagged trees e introduce un elemento de aleatoriedad en el proceso de construcción del arboles imponiendo una restricción. Para cada nodo en el arbol, se extrae una muesta aleatoria de tamaño $ m_{try} $ del total de variables predictoras (se quitan variables) y se determina la partición con esta muestra. Lo anterior asegura que las variables más importantes son muestreadas de forma suficiente.
In [29]:
## Instale y cargue las siguiente librerias
library(randomForest)
library(e1071)
In [ ]:
## calibracion del modelo
rf_ranges <- list(ntree = c(500, 1000, 1500, 2000), # Busqueda del mejor número de arboles en el modelo
mtry = 3:8) # Intentos para cada número de arboles
rf_tune <- tune(randomForest, # Metodo a calibrar, en este caso RandomForest
LeagueIndex ~ ., # variable respuesta ~ Variable predictoras
data =skillcraft_train, # Datos de entrenamiento
ranges = rf_ranges) # Rangos de búsqueda
rf_tune$best.parameters
## Modelo final calibrado y predicciones
rf_best <- rf_tune$best.model # Extraer el mejor modelo
rf_best_predictions <- predict(rf_best, # Predecimos con el mejor modelo
skillcraft_test) # Data de testing
## Calculo SSE e importancia
(rf_best_SSE <- compute_SSE(rf_best_predictions, # Calculo del SSE (Valores predichos vs Valores reales)
skillcraft_test$LeagueIndex))
importance(rf_tune) # Grafico la importancia de cada variable
Ejercicio.-- Se busca predecir el precio de la gasolina dadas las características en la base de datos gasolina. Utilice el método de Random Forest para abarcar dicho problema.
El desarrollo y resultado del ejercicio busca responder:
In [ ]: