En la presente publicación se expondrá un proyecto de Discovery. Este tipo de proyectos van más enfocados al análisis descriptivo y exploratorio, es decir, a analizar la data para encontrar tendencias, patrones y anomalías (si es que las hay) que ayuden a generar insights al negocio y así poder contribuir a que se tomen mejores decisiones. Ahora, lo anterior, no significa que no se pueda complementar con un análisis más avanzado como, por ejemplo, un modelo de machine learning, incluso, desde mi punto de vista, previo a la creación de un modelo analítico avanzado es altamente recomendable, primero, hacer un análisis de discovery, ya que esto permitirá tener un acercamiento y entendimiento al giro del negocio. En resumen, este proyecto será un trabajo de data mining.
El análisis se hará a la demanda de reservaciones de dos hoteles ubicados en Lisboa, Portugal. Un hotel resort y el otro un hotel en ciudad. Dicha información abarca el periodo comprendido entre el 1 de julio de 2015 al 31 de agosto de 2017. Por razones de seguridad y de confidencialidad, se han omitido los datos de identificación tanto de los hoteles como de los clientes.
Supongamos, para fines didácticos, que se nos ha encargado hacer un análisis con la información antes mencionada para encontrar insights que ayuden a mejorar la toma de decisiones así como a la identificación de diferencias o similitudes (si las hay) entre ambos hoteles con el propósito de elaborar estrategias de marketing diferenciadas. Todo lo anteior, para lograr incrementar la rentabilidad del negocio.
Por tanto, los objetivos del proyecto serán:
Iniciamos configurando las opciones generales que vamos a requerir para el desarrollo de este proyecto..
knitr::opts_chunk$set(echo = TRUE,
warning = FALSE,
message = FALSE,
warning = FALSE,
fig.align = "center",
out.width = "90%",
out.height = "70%")
paquetes <- c('tidyverse', # manipulación datos
'lubridate', # manejar fechas
'knitr', # manejo de tablas
'kableExtra', # manejo de tablas
'plotly', # gráficos interactivos
'DT', # visualizción de tablas
'gridExtra', # layout de graficos ggplot
'nortest', # pruebas estadísticas
"summarytools", # estadísticas univariantes
"sfo") # grafica sankey
instalados <- paquetes %in% installed.packages()
if(sum(instalados == FALSE) > 0) {
install.packages(paquetes[!instalados])
}
lapply(paquetes, require, character.only = TRUE)
La información para este análisis es pública y los datos así como su descripción se encuentran en la página de ScienceDirect. También, se encuentran en mi repositorio de github.
Ambos hoteles comparten la misma estructura de datos contenida en 32 variables, el hotel resort tiene 40,060 observaciones y el hotel de ciudad 79,330 observaciones. Cada registro representa una reservación.
df <- read.csv("hoteles.csv")
table(df$hotel) %>%
kbl(align = "c", digits = 2 ,format.args = list(big.mark = ",")) %>%
kable_paper("hover", full_width = F)
| Var1 | Freq |
|---|---|
| City Hotel | 79,330 |
| Resort Hotel | 40,060 |
La descripción de las variables es la siguiente:
t1 <- read.csv("variables.csv")
DT::datatable(t1,
extensions = 'FixedColumns',
rownames = FALSE,
filter = 'top',
options = list(
pageLength = 5,
autoWidth = TRUE,
columnDefs =
list(list(width = '300px')),
scrollX = TRUE,
escape = T)
)
Hechemos un primer vistazo a la estructura de las variables para identificar si tienen los tipos correctos o de lo contrario cambiarlos.
glimpse(df)
## Rows: 119,390
## Columns: 32
## $ hotel <chr> "Resort Hotel", "Resort Hotel", "Resort~
## $ is_canceled <int> 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, ~
## $ lead_time <int> 342, 737, 7, 13, 14, 14, 0, 9, 85, 75, ~
## $ arrival_date_year <int> 2015, 2015, 2015, 2015, 2015, 2015, 201~
## $ arrival_date_month <chr> "July", "July", "July", "July", "July",~
## $ arrival_date_week_number <int> 27, 27, 27, 27, 27, 27, 27, 27, 27, 27,~
## $ arrival_date_day_of_month <int> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, ~
## $ stays_in_weekend_nights <int> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ~
## $ stays_in_week_nights <int> 0, 0, 1, 1, 2, 2, 2, 2, 3, 3, 4, 4, 4, ~
## $ adults <int> 2, 2, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, ~
## $ children <int> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ~
## $ babies <int> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ~
## $ meal <chr> "BB", "BB", "BB", "BB", "BB", "BB", "BB~
## $ country <chr> "PRT", "PRT", "GBR", "GBR", "GBR", "GBR~
## $ market_segment <chr> "Direct", "Direct", "Direct", "Corporat~
## $ distribution_channel <chr> "Direct", "Direct", "Direct", "Corporat~
## $ is_repeated_guest <int> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ~
## $ previous_cancellations <int> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ~
## $ previous_bookings_not_canceled <int> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ~
## $ reserved_room_type <chr> "C", "C", "A", "A", "A", "A", "C", "C",~
## $ assigned_room_type <chr> "C", "C", "C", "A", "A", "A", "C", "C",~
## $ booking_changes <int> 3, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ~
## $ deposit_type <chr> "No Deposit", "No Deposit", "No Deposit~
## $ agent <chr> "NULL", "NULL", "NULL", "304", "240", "~
## $ company <chr> "NULL", "NULL", "NULL", "NULL", "NULL",~
## $ days_in_waiting_list <int> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ~
## $ customer_type <chr> "Transient", "Transient", "Transient", ~
## $ adr <dbl> 0.00, 0.00, 75.00, 75.00, 98.00, 98.00,~
## $ required_car_parking_spaces <int> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ~
## $ total_of_special_requests <int> 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 3, ~
## $ reservation_status <chr> "Check-Out", "Check-Out", "Check-Out", ~
## $ reservation_status_date <chr> "2015-07-01", "2015-07-01", "2015-07-02~
Con este primer resumen vemos que hay variables a las que tenemos que cambiarles el tipo como, por ejemplo, la variable hotel, que está como character y es recomendable pasarla a tipo factor, sobre todo para que se nos facilite la creación de gráficos, además, hay algoritmos que necesitan que las variables categóricas estén como factores y aunque en este trabajo no se utilizará ningún algoritmo es bueno tenerlo presente (al menos si se está trabajando con R, pues en Python no se tiene ese detalle ya que no existe el tipo factor).
También, vemos a primera vista que hay valores NULL en las variables agent y company.
Ahora, antes de hacer alguna modificación a los tipos es altamente recomendable conocer la cardinalidad de las variables categóricas con el objetivo de identificar desde el inicio del proyecto los posibles datos raros o extraños como espacios o caracteres especiales. Para esto nos apoyaremos de tablas de frecuencias:
df2 <- df %>% select_if(is.character)
for(i in 1:length(df2)){
table(df2[i])
print(table(df2[i]))
}
##
## City Hotel Resort Hotel
## 79330 40060
##
## April August December February January July June March
## 11089 13877 6780 8068 5929 12661 10939 9794
## May November October September
## 11791 6794 11160 10508
##
## BB FB HB SC Undefined
## 92310 798 14463 10650 1169
##
## ABW AGO AIA ALB AND ARE ARG ARM ASM ATA ATF AUS AUT
## 2 362 1 12 7 51 214 8 1 2 1 426 1263
## AZE BDI BEL BEN BFA BGD BGR BHR BHS BIH BLR BOL BRA
## 17 1 2342 3 1 12 75 5 1 13 26 10 2224
## BRB BWA CAF CHE CHL CHN CIV CMR CN COL COM CPV CRI
## 4 1 5 1730 65 999 6 10 1279 71 2 24 19
## CUB CYM CYP CZE DEU DJI DMA DNK DOM DZA ECU EGY ESP
## 8 1 51 171 7287 1 1 435 14 103 27 32 8568
## EST ETH FIN FJI FRA FRO GAB GBR GEO GGY GHA GIB GLP
## 83 3 447 1 10415 5 4 12129 22 3 4 18 2
## GNB GRC GTM GUY HKG HND HRV HUN IDN IMN IND IRL IRN
## 9 128 4 1 29 1 100 230 35 2 152 3375 83
## IRQ ISL ISR ITA JAM JEY JOR JPN KAZ KEN KHM KIR KNA
## 14 57 669 3766 6 8 21 197 19 6 2 1 2
## KOR KWT LAO LBN LBY LCA LIE LKA LTU LUX LVA MAC MAR
## 133 16 2 31 8 1 3 7 81 287 55 16 259
## MCO MDG MDV MEX MKD MLI MLT MMR MNE MOZ MRT MUS MWI
## 4 1 12 85 10 1 18 1 5 67 1 7 2
## MYS MYT NAM NCL NGA NIC NLD NOR NPL NULL NZL OMN PAK
## 28 2 1 1 34 1 2104 607 1 488 74 18 14
## PAN PER PHL PLW POL PRI PRT PRY PYF QAT ROU RUS RWA
## 9 29 40 1 919 12 48590 4 1 15 500 632 2
## SAU SDN SEN SGP SLE SLV SMR SRB STP SUR SVK SVN SWE
## 48 1 11 39 1 2 1 101 2 5 65 57 1024
## SYC SYR TGO THA TJK TMP TUN TUR TWN TZA UGA UKR UMI
## 2 3 2 59 9 3 39 248 51 5 2 68 1
## URY USA UZB VEN VGB VNM ZAF ZMB ZWE
## 32 2097 4 26 1 8 80 2 4
##
## Aviation Complementary Corporate Direct Groups
## 237 743 5295 12606 19811
## Offline TA/TO Online TA Undefined
## 24219 56477 2
##
## Corporate Direct GDS TA/TO Undefined
## 6677 14645 193 97870 5
##
## A B C D E F G H L P
## 85994 1118 932 19201 6535 2897 2094 601 6 12
##
## A B C D E F G H I K L P
## 74053 2163 2375 25322 7806 3751 2553 712 363 279 1 12
##
## No Deposit Non Refund Refundable
## 104641 14587 162
##
## 1 10 103 104 105 106 107 11 110 111 112 114 115
## 7191 260 21 53 14 2 2 395 12 16 15 1 225
## 117 118 119 12 121 122 126 127 128 129 13 132 133
## 1 69 304 578 37 2 14 45 23 14 82 143 56
## 134 135 138 139 14 141 142 143 144 146 147 148 149
## 482 2 287 8 3640 6 137 172 1 124 156 4 28
## 15 150 151 152 153 154 155 156 157 158 159 16 162
## 402 5 56 183 25 193 94 190 61 1 89 246 37
## 163 165 167 168 17 170 171 173 174 175 177 179 180
## 7 1 3 184 241 93 607 29 22 195 347 2 4
## 181 182 183 184 185 187 19 191 192 193 195 196 197
## 59 8 45 52 78 24 1061 198 41 15 193 301 1
## 2 20 201 205 208 21 210 211 213 214 215 216 219
## 162 540 42 27 173 875 7 2 1 5 15 1 13
## 22 220 223 227 229 23 232 234 235 236 24 240 241
## 382 104 18 2 786 25 2 128 29 247 22 13922 1721
## 242 243 244 245 247 248 249 25 250 251 252 253 254
## 780 514 4 37 1 131 51 3 2870 220 29 87 29
## 256 257 258 26 261 262 265 267 269 27 270 273 275
## 24 24 3 401 38 22 1 1 2 450 6 349 8
## 276 278 28 280 281 282 283 285 286 287 288 289 29
## 8 1 1666 1 82 2 2 1 45 8 14 1 683
## 290 291 294 295 296 298 299 3 30 300 301 302 303
## 19 1 1 4 42 472 1 1336 484 1 1 3 2
## 304 305 306 307 308 31 310 313 314 315 32 321 323
## 1 45 35 14 54 162 25 36 927 284 15 3 25
## 324 325 326 327 328 33 330 331 332 333 334 335 336
## 9 6 165 20 9 31 125 2 55 1 28 4 23
## 337 339 34 341 344 346 348 35 350 352 354 355 358
## 1 77 294 4 8 1 22 109 28 1 14 4 1
## 359 36 360 363 364 367 368 37 370 371 375 378 38
## 21 100 15 6 19 1 45 1230 3 4 40 36 274
## 384 385 387 388 39 390 391 393 394 397 4 40 403
## 2 60 32 1 127 57 2 13 33 1 47 1039 4
## 404 405 406 408 41 410 411 414 416 418 42 420 423
## 2 5 1 1 75 133 16 2 1 8 211 3 19
## 425 426 427 429 430 431 432 433 434 436 438 44 440
## 16 3 3 5 4 1 1 1 33 49 2 292 56
## 441 444 446 449 45 450 451 453 454 455 459 461 464
## 7 1 1 2 32 1 1 1 2 19 16 2 98
## 467 468 469 47 472 474 475 476 479 480 481 483 484
## 39 49 2 50 1 17 8 2 32 1 8 1 11
## 492 493 495 497 5 50 502 508 509 510 52 526 527
## 28 35 57 1 330 20 24 6 10 2 137 10 35
## 53 531 535 54 55 56 57 58 59 6 60 61 63
## 18 68 3 1 16 375 28 335 1 3290 19 2 29
## 64 66 67 68 69 7 70 71 72 73 74 75 77
## 23 44 127 211 90 3539 1 73 6 1 20 73 33
## 78 79 8 81 82 83 85 86 87 88 89 9 90
## 37 47 1514 6 77 696 554 338 77 19 99 31961 1
## 91 92 93 94 95 96 98 99 NULL
## 58 7 1 114 135 537 124 68 16340
##
## 10 100 101 102 103 104 105 106 107 108 109
## 1 1 1 1 16 1 8 2 9 11 1
## 11 110 112 113 115 116 118 12 120 122 126
## 1 52 13 36 4 6 7 14 14 18 1
## 127 130 132 135 137 139 14 140 142 143 144
## 15 12 1 66 4 3 9 1 1 17 27
## 146 148 149 150 153 154 158 159 16 160 163
## 3 37 5 19 215 133 2 6 5 1 17
## 165 167 168 169 174 178 179 18 180 183 184
## 3 7 2 65 149 27 24 1 5 16 1
## 185 186 192 193 195 197 20 200 202 203 204
## 4 12 4 16 38 47 50 3 38 13 34
## 207 209 210 212 213 215 216 217 218 219 22
## 9 19 2 1 1 8 21 2 43 141 6
## 220 221 222 223 224 225 227 229 230 232 233
## 4 27 2 784 3 7 24 1 3 2 114
## 234 237 238 240 242 243 245 246 250 251 253
## 1 1 33 3 62 2 3 3 2 18 1
## 254 255 257 258 259 260 263 264 268 269 270
## 10 6 1 1 2 3 14 2 14 33 43
## 271 272 273 274 275 277 278 279 28 280 281
## 2 3 1 14 3 5 2 8 5 48 138
## 282 284 286 287 288 289 29 290 291 292 293
## 4 1 21 5 1 2 2 17 12 18 5
## 297 301 302 304 305 307 308 309 31 311 312
## 7 1 5 2 1 36 33 1 17 2 3
## 313 314 316 317 318 319 32 320 321 323 324
## 1 1 2 9 1 3 1 1 2 10 9
## 325 329 330 331 332 333 334 337 338 34 341
## 2 12 4 61 2 11 3 25 12 8 5
## 342 343 346 347 348 349 35 350 351 352 353
## 48 29 14 1 59 2 1 3 2 1 4
## 355 356 357 358 360 361 362 364 365 366 367
## 13 10 5 7 12 2 2 6 29 24 14
## 368 369 37 370 371 372 373 376 377 378 379
## 1 5 10 2 11 3 1 1 5 3 9
## 38 380 382 383 384 385 386 388 39 390 391
## 51 12 5 6 9 30 1 7 8 13 2
## 392 393 394 395 396 397 398 399 40 400 401
## 4 1 6 4 18 15 1 11 927 2 1
## 402 403 405 407 408 409 410 411 412 413 415
## 1 2 119 22 15 12 5 2 1 1 1
## 416 417 418 419 42 420 421 422 423 424 425
## 1 1 25 1 5 1 9 1 2 24 1
## 426 428 429 43 433 435 436 437 439 442 443
## 4 13 2 29 2 12 2 7 6 1 5
## 444 445 446 447 448 45 450 451 452 454 455
## 5 4 1 2 4 250 10 6 4 1 1
## 456 457 458 459 46 460 461 465 466 47 470
## 2 3 2 5 26 3 1 12 3 72 5
## 477 478 479 48 481 482 483 484 485 486 487
## 23 2 1 5 1 2 2 2 14 2 1
## 489 49 490 491 492 494 496 497 498 499 501
## 1 5 5 2 2 4 1 1 58 1 1
## 504 506 507 51 511 512 513 514 515 516 518
## 11 1 23 99 6 3 2 2 6 1 2
## 52 520 521 523 525 528 53 530 531 534 539
## 2 1 7 19 15 2 8 5 1 2 2
## 54 541 543 59 6 61 62 64 65 67 68
## 1 1 2 7 1 2 47 1 1 267 46
## 71 72 73 76 77 78 8 80 81 82 83
## 2 30 3 1 1 22 1 1 23 14 9
## 84 85 86 88 9 91 92 93 94 96 99
## 3 2 32 22 37 48 13 3 87 1 12
## NULL
## 112593
##
## Contract Group Transient Transient-Party
## 4076 577 89613 25124
##
## Canceled Check-Out No-Show
## 43017 75166 1207
##
## 2014-10-17 2014-11-18 2015-01-01 2015-01-02 2015-01-18 2015-01-20 2015-01-21
## 180 1 763 16 1 2 91
## 2015-01-22 2015-01-28 2015-01-29 2015-01-30 2015-02-02 2015-02-05 2015-02-06
## 6 1 1 67 2 4 1
## 2015-02-09 2015-02-10 2015-02-11 2015-02-12 2015-02-17 2015-02-19 2015-02-20
## 1 2 3 1 2 1 20
## 2015-02-23 2015-02-24 2015-02-25 2015-02-26 2015-02-27 2015-03-03 2015-03-04
## 2 1 2 1 1 38 2
## 2015-03-05 2015-03-06 2015-03-09 2015-03-10 2015-03-11 2015-03-12 2015-03-13
## 1 2 4 1 1 1 1
## 2015-03-17 2015-03-18 2015-03-23 2015-03-24 2015-03-25 2015-03-28 2015-03-29
## 4 1 2 3 17 1 1
## 2015-03-30 2015-03-31 2015-04-02 2015-04-03 2015-04-04 2015-04-05 2015-04-06
## 1 4 18 2 8 6 4
## 2015-04-07 2015-04-08 2015-04-10 2015-04-11 2015-04-13 2015-04-14 2015-04-15
## 1 3 5 3 2 3 9
## 2015-04-16 2015-04-17 2015-04-18 2015-04-20 2015-04-21 2015-04-22 2015-04-23
## 7 2 2 6 1 7 3
## 2015-04-24 2015-04-25 2015-04-27 2015-04-28 2015-04-29 2015-04-30 2015-05-01
## 4 1 1 25 27 1 9
## 2015-05-04 2015-05-05 2015-05-06 2015-05-07 2015-05-08 2015-05-09 2015-05-11
## 3 4 6 6 3 4 5
## 2015-05-12 2015-05-13 2015-05-14 2015-05-15 2015-05-16 2015-05-18 2015-05-19
## 11 13 50 1 3 12 33
## 2015-05-20 2015-05-21 2015-05-22 2015-05-23 2015-05-25 2015-05-26 2015-05-27
## 8 3 10 8 6 2 10
## 2015-05-28 2015-05-29 2015-05-30 2015-06-01 2015-06-02 2015-06-03 2015-06-04
## 28 34 3 13 10 9 6
## 2015-06-05 2015-06-06 2015-06-08 2015-06-09 2015-06-10 2015-06-11 2015-06-12
## 7 3 10 35 4 13 10
## 2015-06-13 2015-06-14 2015-06-15 2015-06-16 2015-06-17 2015-06-18 2015-06-19
## 4 1 53 44 100 7 14
## 2015-06-20 2015-06-22 2015-06-23 2015-06-24 2015-06-25 2015-06-26 2015-06-27
## 8 16 22 6 15 67 29
## 2015-06-29 2015-06-30 2015-07-01 2015-07-02 2015-07-03 2015-07-04 2015-07-05
## 96 64 15 469 153 16 33
## 2015-07-06 2015-07-07 2015-07-08 2015-07-09 2015-07-10 2015-07-11 2015-07-12
## 805 55 98 81 98 42 46
## 2015-07-13 2015-07-14 2015-07-15 2015-07-16 2015-07-17 2015-07-18 2015-07-19
## 51 69 44 80 111 55 97
## 2015-07-20 2015-07-21 2015-07-22 2015-07-23 2015-07-24 2015-07-25 2015-07-26
## 87 55 132 209 113 88 77
## 2015-07-27 2015-07-28 2015-07-29 2015-07-30 2015-07-31 2015-08-01 2015-08-02
## 51 57 99 107 122 107 51
## 2015-08-03 2015-08-04 2015-08-05 2015-08-06 2015-08-07 2015-08-08 2015-08-09
## 55 107 46 97 91 96 102
## 2015-08-10 2015-08-11 2015-08-12 2015-08-13 2015-08-14 2015-08-15 2015-08-16
## 130 155 150 104 166 74 152
## 2015-08-17 2015-08-18 2015-08-19 2015-08-20 2015-08-21 2015-08-22 2015-08-23
## 139 101 121 84 202 94 75
## 2015-08-24 2015-08-25 2015-08-26 2015-08-27 2015-08-28 2015-08-29 2015-08-30
## 84 94 90 68 115 96 85
## 2015-08-31 2015-09-01 2015-09-02 2015-09-03 2015-09-04 2015-09-05 2015-09-06
## 116 103 80 109 124 121 112
## 2015-09-07 2015-09-08 2015-09-09 2015-09-10 2015-09-11 2015-09-12 2015-09-13
## 137 142 290 96 173 103 128
## 2015-09-14 2015-09-15 2015-09-16 2015-09-17 2015-09-18 2015-09-19 2015-09-20
## 109 120 154 183 153 123 137
## 2015-09-21 2015-09-22 2015-09-23 2015-09-24 2015-09-25 2015-09-26 2015-09-27
## 135 124 136 166 149 110 48
## 2015-09-28 2015-09-29 2015-09-30 2015-10-01 2015-10-02 2015-10-03 2015-10-04
## 147 111 194 131 107 83 161
## 2015-10-05 2015-10-06 2015-10-07 2015-10-08 2015-10-09 2015-10-10 2015-10-11
## 119 111 160 118 186 150 133
## 2015-10-12 2015-10-13 2015-10-14 2015-10-15 2015-10-16 2015-10-17 2015-10-18
## 242 177 129 116 187 73 160
## 2015-10-19 2015-10-20 2015-10-21 2015-10-22 2015-10-23 2015-10-24 2015-10-25
## 262 109 1461 203 189 106 82
## 2015-10-26 2015-10-27 2015-10-28 2015-10-29 2015-10-30 2015-10-31 2015-11-01
## 115 128 199 87 96 162 106
## 2015-11-02 2015-11-03 2015-11-04 2015-11-05 2015-11-06 2015-11-07 2015-11-08
## 95 71 90 61 113 73 53
## 2015-11-09 2015-11-10 2015-11-11 2015-11-12 2015-11-13 2015-11-14 2015-11-15
## 129 45 158 57 50 80 148
## 2015-11-16 2015-11-17 2015-11-18 2015-11-19 2015-11-20 2015-11-21 2015-11-22
## 150 256 65 98 73 116 203
## 2015-11-23 2015-11-24 2015-11-25 2015-11-26 2015-11-27 2015-11-28 2015-11-29
## 111 113 52 135 109 50 97
## 2015-11-30 2015-12-01 2015-12-02 2015-12-03 2015-12-04 2015-12-05 2015-12-06
## 120 53 142 99 92 31 40
## 2015-12-07 2015-12-08 2015-12-09 2015-12-10 2015-12-11 2015-12-12 2015-12-13
## 96 254 124 124 90 76 77
## 2015-12-14 2015-12-15 2015-12-16 2015-12-17 2015-12-18 2015-12-19 2015-12-20
## 100 68 45 90 423 63 55
## 2015-12-21 2015-12-22 2015-12-23 2015-12-24 2015-12-25 2015-12-26 2015-12-27
## 36 114 108 24 52 64 75
## 2015-12-28 2015-12-29 2015-12-30 2015-12-31 2016-01-01 2016-01-02 2016-01-03
## 77 172 130 68 99 145 194
## 2016-01-04 2016-01-05 2016-01-06 2016-01-07 2016-01-08 2016-01-09 2016-01-10
## 80 205 231 108 177 59 71
## 2016-01-11 2016-01-12 2016-01-13 2016-01-14 2016-01-15 2016-01-16 2016-01-17
## 126 66 89 104 94 176 118
## 2016-01-18 2016-01-19 2016-01-20 2016-01-21 2016-01-22 2016-01-23 2016-01-24
## 625 233 117 170 216 66 119
## 2016-01-25 2016-01-26 2016-01-27 2016-01-28 2016-01-29 2016-01-30 2016-01-31
## 92 145 101 177 122 64 93
## 2016-02-01 2016-02-02 2016-02-03 2016-02-04 2016-02-05 2016-02-06 2016-02-07
## 258 86 138 123 134 64 133
## 2016-02-08 2016-02-09 2016-02-10 2016-02-11 2016-02-12 2016-02-13 2016-02-14
## 111 412 228 109 134 113 257
## 2016-02-15 2016-02-16 2016-02-17 2016-02-18 2016-02-19 2016-02-20 2016-02-21
## 150 105 129 119 118 182 138
## 2016-02-22 2016-02-23 2016-02-24 2016-02-25 2016-02-26 2016-02-27 2016-02-28
## 132 160 120 212 135 189 189
## 2016-02-29 2016-03-01 2016-03-02 2016-03-03 2016-03-04 2016-03-05 2016-03-06
## 218 141 169 170 201 100 181
## 2016-03-07 2016-03-08 2016-03-09 2016-03-10 2016-03-11 2016-03-12 2016-03-13
## 129 125 187 120 146 156 177
## 2016-03-14 2016-03-15 2016-03-16 2016-03-17 2016-03-18 2016-03-19 2016-03-20
## 261 329 184 143 254 114 167
## 2016-03-21 2016-03-22 2016-03-23 2016-03-24 2016-03-25 2016-03-26 2016-03-27
## 180 171 176 180 135 111 206
## 2016-03-28 2016-03-29 2016-03-30 2016-03-31 2016-04-01 2016-04-02 2016-04-03
## 214 188 131 173 125 114 173
## 2016-04-04 2016-04-05 2016-04-06 2016-04-07 2016-04-08 2016-04-09 2016-04-10
## 382 103 189 172 163 158 152
## 2016-04-11 2016-04-12 2016-04-13 2016-04-14 2016-04-15 2016-04-16 2016-04-17
## 190 148 170 164 201 131 299
## 2016-04-18 2016-04-19 2016-04-20 2016-04-21 2016-04-22 2016-04-23 2016-04-24
## 167 130 181 148 159 127 152
## 2016-04-25 2016-04-26 2016-04-27 2016-04-28 2016-04-29 2016-04-30 2016-05-01
## 185 151 283 177 173 147 125
## 2016-05-02 2016-05-03 2016-05-04 2016-05-05 2016-05-06 2016-05-07 2016-05-08
## 198 180 203 225 193 105 194
## 2016-05-09 2016-05-10 2016-05-11 2016-05-12 2016-05-13 2016-05-14 2016-05-15
## 152 174 130 171 138 129 168
## 2016-05-16 2016-05-17 2016-05-18 2016-05-19 2016-05-20 2016-05-21 2016-05-22
## 200 173 159 187 168 124 155
## 2016-05-23 2016-05-24 2016-05-25 2016-05-26 2016-05-27 2016-05-28 2016-05-29
## 174 144 158 192 108 67 245
## 2016-05-30 2016-05-31 2016-06-01 2016-06-02 2016-06-03 2016-06-04 2016-06-05
## 171 113 96 257 152 115 160
## 2016-06-06 2016-06-07 2016-06-08 2016-06-09 2016-06-10 2016-06-11 2016-06-12
## 164 127 155 166 125 89 181
## 2016-06-13 2016-06-14 2016-06-15 2016-06-16 2016-06-17 2016-06-18 2016-06-19
## 177 137 156 120 202 89 141
## 2016-06-20 2016-06-21 2016-06-22 2016-06-23 2016-06-24 2016-06-25 2016-06-26
## 279 150 148 142 180 64 271
## 2016-06-27 2016-06-28 2016-06-29 2016-06-30 2016-07-01 2016-07-02 2016-07-03
## 119 145 113 132 125 126 125
## 2016-07-04 2016-07-05 2016-07-06 2016-07-07 2016-07-08 2016-07-09 2016-07-10
## 125 127 125 152 140 130 112
## 2016-07-11 2016-07-12 2016-07-13 2016-07-14 2016-07-15 2016-07-16 2016-07-17
## 140 137 230 129 122 145 139
## 2016-07-18 2016-07-19 2016-07-20 2016-07-21 2016-07-22 2016-07-23 2016-07-24
## 197 154 145 195 128 163 147
## 2016-07-25 2016-07-26 2016-07-27 2016-07-28 2016-07-29 2016-07-30 2016-07-31
## 152 120 124 160 140 174 125
## 2016-08-01 2016-08-02 2016-08-03 2016-08-04 2016-08-05 2016-08-06 2016-08-07
## 194 137 164 133 126 131 117
## 2016-08-08 2016-08-09 2016-08-10 2016-08-11 2016-08-12 2016-08-13 2016-08-14
## 186 121 120 184 143 134 114
## 2016-08-15 2016-08-16 2016-08-17 2016-08-18 2016-08-19 2016-08-20 2016-08-21
## 168 139 151 191 135 176 139
## 2016-08-22 2016-08-23 2016-08-24 2016-08-25 2016-08-26 2016-08-27 2016-08-28
## 154 136 167 144 145 100 134
## 2016-08-29 2016-08-30 2016-08-31 2016-09-01 2016-09-02 2016-09-03 2016-09-04
## 175 172 155 138 196 145 127
## 2016-09-05 2016-09-06 2016-09-07 2016-09-08 2016-09-09 2016-09-10 2016-09-11
## 179 271 113 133 137 105 157
## 2016-09-12 2016-09-13 2016-09-14 2016-09-15 2016-09-16 2016-09-17 2016-09-18
## 172 168 139 251 152 127 178
## 2016-09-19 2016-09-20 2016-09-21 2016-09-22 2016-09-23 2016-09-24 2016-09-25
## 175 303 182 163 167 79 219
## 2016-09-26 2016-09-27 2016-09-28 2016-09-29 2016-09-30 2016-10-01 2016-10-02
## 199 137 140 201 140 99 156
## 2016-10-03 2016-10-04 2016-10-05 2016-10-06 2016-10-07 2016-10-08 2016-10-09
## 154 134 151 224 230 103 185
## 2016-10-10 2016-10-11 2016-10-12 2016-10-13 2016-10-14 2016-10-15 2016-10-16
## 242 185 166 186 163 88 223
## 2016-10-17 2016-10-18 2016-10-19 2016-10-20 2016-10-21 2016-10-22 2016-10-23
## 179 151 129 176 265 101 162
## 2016-10-24 2016-10-25 2016-10-26 2016-10-27 2016-10-28 2016-10-29 2016-10-30
## 157 173 176 192 211 163 158
## 2016-10-31 2016-11-01 2016-11-02 2016-11-03 2016-11-04 2016-11-05 2016-11-06
## 139 168 158 118 151 139 184
## 2016-11-07 2016-11-08 2016-11-09 2016-11-10 2016-11-11 2016-11-12 2016-11-13
## 151 78 111 152 142 127 145
## 2016-11-14 2016-11-15 2016-11-16 2016-11-17 2016-11-18 2016-11-19 2016-11-20
## 144 145 122 143 145 123 175
## 2016-11-21 2016-11-22 2016-11-23 2016-11-24 2016-11-25 2016-11-26 2016-11-27
## 340 104 176 172 790 116 169
## 2016-11-28 2016-11-29 2016-11-30 2016-12-01 2016-12-02 2016-12-03 2016-12-04
## 93 103 137 68 128 96 180
## 2016-12-05 2016-12-06 2016-12-07 2016-12-08 2016-12-09 2016-12-10 2016-12-11
## 94 133 450 153 217 81 217
## 2016-12-12 2016-12-13 2016-12-14 2016-12-15 2016-12-16 2016-12-17 2016-12-18
## 243 249 102 134 115 91 111
## 2016-12-19 2016-12-20 2016-12-21 2016-12-22 2016-12-23 2016-12-24 2016-12-25
## 93 109 171 121 123 58 41
## 2016-12-26 2016-12-27 2016-12-28 2016-12-29 2016-12-30 2016-12-31 2017-01-01
## 92 173 148 147 126 74 149
## 2017-01-02 2017-01-03 2017-01-04 2017-01-05 2017-01-06 2017-01-07 2017-01-08
## 214 164 114 159 211 114 135
## 2017-01-09 2017-01-10 2017-01-11 2017-01-12 2017-01-13 2017-01-14 2017-01-15
## 144 177 146 225 111 120 170
## 2017-01-16 2017-01-17 2017-01-18 2017-01-19 2017-01-20 2017-01-21 2017-01-22
## 89 121 206 321 249 108 122
## 2017-01-23 2017-01-24 2017-01-25 2017-01-26 2017-01-27 2017-01-28 2017-01-29
## 134 343 145 155 211 112 184
## 2017-01-30 2017-01-31 2017-02-01 2017-02-02 2017-02-03 2017-02-04 2017-02-05
## 145 253 135 315 194 114 169
## 2017-02-06 2017-02-07 2017-02-08 2017-02-09 2017-02-10 2017-02-11 2017-02-12
## 213 119 139 191 178 112 194
## 2017-02-13 2017-02-14 2017-02-15 2017-02-16 2017-02-17 2017-02-18 2017-02-19
## 150 121 229 163 208 101 203
## 2017-02-20 2017-02-21 2017-02-22 2017-02-23 2017-02-24 2017-02-25 2017-02-26
## 162 169 136 182 231 192 188
## 2017-02-27 2017-02-28 2017-03-01 2017-03-02 2017-03-03 2017-03-04 2017-03-05
## 124 226 165 172 140 105 185
## 2017-03-06 2017-03-07 2017-03-08 2017-03-09 2017-03-10 2017-03-11 2017-03-12
## 221 154 160 143 170 124 189
## 2017-03-13 2017-03-14 2017-03-15 2017-03-16 2017-03-17 2017-03-18 2017-03-19
## 183 110 148 160 139 132 164
## 2017-03-20 2017-03-21 2017-03-22 2017-03-23 2017-03-24 2017-03-25 2017-03-26
## 194 144 143 149 155 109 218
## 2017-03-27 2017-03-28 2017-03-29 2017-03-30 2017-03-31 2017-04-01 2017-04-02
## 129 127 176 139 179 135 147
## 2017-04-03 2017-04-04 2017-04-05 2017-04-06 2017-04-07 2017-04-08 2017-04-09
## 178 147 204 157 158 114 212
## 2017-04-10 2017-04-11 2017-04-12 2017-04-13 2017-04-14 2017-04-15 2017-04-16
## 164 116 130 143 127 143 120
## 2017-04-17 2017-04-18 2017-04-19 2017-04-20 2017-04-21 2017-04-22 2017-04-23
## 177 147 133 134 263 132 183
## 2017-04-24 2017-04-25 2017-04-26 2017-04-27 2017-04-28 2017-04-29 2017-04-30
## 143 139 121 155 178 193 141
## 2017-05-01 2017-05-02 2017-05-03 2017-05-04 2017-05-05 2017-05-06 2017-05-07
## 163 198 136 158 297 163 146
## 2017-05-08 2017-05-09 2017-05-10 2017-05-11 2017-05-12 2017-05-13 2017-05-14
## 177 160 153 165 170 138 166
## 2017-05-15 2017-05-16 2017-05-17 2017-05-18 2017-05-19 2017-05-20 2017-05-21
## 171 138 170 154 130 141 137
## 2017-05-22 2017-05-23 2017-05-24 2017-05-25 2017-05-26 2017-05-27 2017-05-28
## 186 156 184 216 127 104 214
## 2017-05-29 2017-05-30 2017-05-31 2017-06-01 2017-06-02 2017-06-03 2017-06-04
## 156 125 107 161 144 146 131
## 2017-06-05 2017-06-06 2017-06-07 2017-06-08 2017-06-09 2017-06-10 2017-06-11
## 144 152 128 155 154 104 174
## 2017-06-12 2017-06-13 2017-06-14 2017-06-15 2017-06-16 2017-06-17 2017-06-18
## 141 103 97 136 115 118 140
## 2017-06-19 2017-06-20 2017-06-21 2017-06-22 2017-06-23 2017-06-24 2017-06-25
## 111 115 103 145 141 109 140
## 2017-06-26 2017-06-27 2017-06-28 2017-06-29 2017-06-30 2017-07-01 2017-07-02
## 115 177 167 116 178 119 126
## 2017-07-03 2017-07-04 2017-07-05 2017-07-06 2017-07-07 2017-07-08 2017-07-09
## 133 206 84 172 130 102 157
## 2017-07-10 2017-07-11 2017-07-12 2017-07-13 2017-07-14 2017-07-15 2017-07-16
## 138 153 145 176 119 190 125
## 2017-07-17 2017-07-18 2017-07-19 2017-07-20 2017-07-21 2017-07-22 2017-07-23
## 117 129 97 104 115 132 116
## 2017-07-24 2017-07-25 2017-07-26 2017-07-27 2017-07-28 2017-07-29 2017-07-30
## 156 96 109 111 107 143 119
## 2017-07-31 2017-08-01 2017-08-02 2017-08-03 2017-08-04 2017-08-05 2017-08-06
## 112 111 106 132 133 120 99
## 2017-08-07 2017-08-08 2017-08-09 2017-08-10 2017-08-11 2017-08-12 2017-08-13
## 133 89 116 130 76 123 122
## 2017-08-14 2017-08-15 2017-08-16 2017-08-17 2017-08-18 2017-08-19 2017-08-20
## 94 117 108 125 103 116 119
## 2017-08-21 2017-08-22 2017-08-23 2017-08-24 2017-08-25 2017-08-26 2017-08-27
## 101 76 91 106 143 103 159
## 2017-08-28 2017-08-29 2017-08-30 2017-08-31 2017-09-01 2017-09-02 2017-09-03
## 135 86 71 74 136 66 81
## 2017-09-04 2017-09-05 2017-09-06 2017-09-07 2017-09-08 2017-09-09 2017-09-10
## 37 21 16 19 4 6 4
## 2017-09-12 2017-09-14
## 1 2
Es mucha información en esta salida, sin embargo, podemos identificar:
Veamos otro resumen diferente que nos permitirá identificar las variables con valores NA:
map_dbl(df, .f = function(x){sum(is.na(x))})
## hotel is_canceled
## 0 0
## lead_time arrival_date_year
## 0 0
## arrival_date_month arrival_date_week_number
## 0 0
## arrival_date_day_of_month stays_in_weekend_nights
## 0 0
## stays_in_week_nights adults
## 0 0
## children babies
## 4 0
## meal country
## 0 0
## market_segment distribution_channel
## 0 0
## is_repeated_guest previous_cancellations
## 0 0
## previous_bookings_not_canceled reserved_room_type
## 0 0
## assigned_room_type booking_changes
## 0 0
## deposit_type agent
## 0 0
## company days_in_waiting_list
## 0 0
## customer_type adr
## 0 0
## required_car_parking_spaces total_of_special_requests
## 0 0
## reservation_status reservation_status_date
## 0 0
En esta otra vista se puede ver que hay 4 valores perdidos solo en la variable children.
Identifiquemos cuáles son las variables en donde hay valores NULL:
nulls <- function(variable) {
sum(if_else(variable == 'NULL', 1, 0))
}
df %>%
mutate_all(as.character) %>%
lapply(., nulls)
## $hotel
## [1] 0
##
## $is_canceled
## [1] 0
##
## $lead_time
## [1] 0
##
## $arrival_date_year
## [1] 0
##
## $arrival_date_month
## [1] 0
##
## $arrival_date_week_number
## [1] 0
##
## $arrival_date_day_of_month
## [1] 0
##
## $stays_in_weekend_nights
## [1] 0
##
## $stays_in_week_nights
## [1] 0
##
## $adults
## [1] 0
##
## $children
## [1] NA
##
## $babies
## [1] 0
##
## $meal
## [1] 0
##
## $country
## [1] 488
##
## $market_segment
## [1] 0
##
## $distribution_channel
## [1] 0
##
## $is_repeated_guest
## [1] 0
##
## $previous_cancellations
## [1] 0
##
## $previous_bookings_not_canceled
## [1] 0
##
## $reserved_room_type
## [1] 0
##
## $assigned_room_type
## [1] 0
##
## $booking_changes
## [1] 0
##
## $deposit_type
## [1] 0
##
## $agent
## [1] 16340
##
## $company
## [1] 112593
##
## $days_in_waiting_list
## [1] 0
##
## $customer_type
## [1] 0
##
## $adr
## [1] 0
##
## $required_car_parking_spaces
## [1] 0
##
## $total_of_special_requests
## [1] 0
##
## $reservation_status
## [1] 0
##
## $reservation_status_date
## [1] 0
Hemos identificado que en las variables country, agent y company hay valores NULL.
Después de haber realizado un primer análisis exploratorio lo que debemos hacer es:
En esta parte del análisis exploratorio empezaremos por revisar las variables categóricas o de tipo character. Aquí el objetivo es conocer cómo está conformada la cardinalidad en cada variable, ya que de haber pocas observaciones de una categoría será preferible modificarla y crear grupos.
Empecemos con el primer grupo de variables:
df2 %>%
select(1:7) %>%
mutate(id = 1:nrow(.)) %>%
pivot_longer(-id, names_to = 'variable', values_to = 'valor') %>%
ggplot(aes(valor)) +
geom_bar() +
scale_y_continuous(labels = scales::comma) +
facet_wrap(~variable, scales = 'free', ncol = 2)
Al observar las gráficas anteriores podemos determinar las siguientes acciones a seguir:
Segundo grupo de variables tipo character:
df2 %>%
select(8:14) %>%
mutate(id = 1:nrow(.)) %>%
pivot_longer(-id, names_to = 'variable', values_to = 'valor') %>%
ggplot(aes(valor)) +
geom_bar() +
scale_y_continuous(labels = scales::comma) +
facet_wrap(~variable, scales = 'free', ncol = 2)
En esta segunda parte de variables categóricas, podemos concluir las siguientes acciones a seguir:
Ahora, veamos las variables de tipo discreto, o sea, las de tipo entero:
df3 <- df %>% select_if(is.integer)
df3 %>%
select(1:9) %>%
mutate(id = 1:nrow(.)) %>%
pivot_longer(-id, names_to = 'variable', values_to = 'valor') %>%
ggplot(aes(valor)) +
geom_bar() +
scale_y_continuous(labels = scales::comma) +
facet_wrap(vars(variable), scales = 'free',ncol = 2)
df3 %>%
select(10:17) %>%
mutate(id = 1:nrow(.)) %>%
pivot_longer(-id, names_to = 'variable', values_to = 'valor') %>%
ggplot(aes(valor)) +
geom_bar() +
scale_y_continuous(labels = scales::comma) +
facet_wrap(vars(variable), scales = 'free',ncol = 2)
Al analizar visualmente esas variables podemos afirmar que también tenemos que hacer algunas transformaciones:
Finalmente, revisamos la única variable continua que tenemos, adr:
df %>%
ggplot(aes(x = factor(1), y = adr)) +
geom_boxplot(outlier.colour = "red") +
scale_y_continuous(labels = scales::comma) +
labs(
title = "adr"
)
En el boxplot se aprecia claramente que hay un valor extremo, sin embargo, no sabemos su valor. En estos casos es recomendable hacer una tabla resumen con las principales medidas estadísticas:
descr(df$adr,
style = "rmarkdown",
justify = "c",
headings = T) %>%
kbl(align = "c", digits = 2 ,format.args = list(big.mark = ",")) %>%
kable_paper("hover", full_width = F)
| adr | |
|---|---|
| Mean | 101.83 |
| Std.Dev | 50.54 |
| Min | -6.38 |
| Q1 | 69.29 |
| Median | 94.58 |
| Q3 | 126.00 |
| Max | 5,400.00 |
| MAD | 41.25 |
| IQR | 56.71 |
| CV | 0.50 |
| Skewness | 10.53 |
| SE.Skewness | 0.01 |
| Kurtosis | 1,013.13 |
| N.Valid | 119,390.00 |
| Pct.Valid | 100.00 |
arrange(df,desc(adr)) %>%
select(adr) %>%
head(10) %>%
kbl(align = "c", digits = 2 ,format.args = list(big.mark = ",")) %>%
kable_paper("hover", full_width = F)
| adr |
|---|
| 5,400.00 |
| 510.00 |
| 508.00 |
| 451.50 |
| 450.00 |
| 437.00 |
| 426.25 |
| 402.00 |
| 397.38 |
| 392.00 |
Con apoyo de las tablas resumen sabemos que es de 5,400. Es una sola observación la que está afectado a toda la variable. Aquí lo recomendable será:
A continuación, realizaremos las transformaciones que previamente identificamos en la etapa de exploración.
# Se prepara el cambio
a_factor <- c('hotel','arrival_date_year','arrival_date_month','arrival_date_week_number',
'arrival_date_day_of_month','meal','country','market_segment',
'distribution_channel', 'reserved_room_type','assigned_room_type',
'deposit_type','agent','company','customer_type','reservation_status')
a_logico <- c('is_canceled','is_repeated_guest')
a_discreto <- c('stays_in_weekend_nights','stays_in_week_nights','adults','children',
'babies','previous_cancellations','previous_bookings_not_canceled',
'booking_changes','days_in_waiting_list','required_car_parking_spaces',
'total_of_special_requests')
# Hacemos el cambio
df <- df %>%
mutate_at(all_of(a_factor), as.factor) %>%
mutate_at(all_of(a_logico), as.logical) %>%
mutate_at(all_of(a_discreto), as.integer)
glimpse(df)
## Rows: 119,390
## Columns: 32
## $ hotel <fct> Resort Hotel, Resort Hotel, Resort Hote~
## $ is_canceled <lgl> FALSE, FALSE, FALSE, FALSE, FALSE, FALS~
## $ lead_time <int> 342, 737, 7, 13, 14, 14, 0, 9, 85, 75, ~
## $ arrival_date_year <fct> 2015, 2015, 2015, 2015, 2015, 2015, 201~
## $ arrival_date_month <fct> July, July, July, July, July, July, Jul~
## $ arrival_date_week_number <fct> 27, 27, 27, 27, 27, 27, 27, 27, 27, 27,~
## $ arrival_date_day_of_month <fct> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, ~
## $ stays_in_weekend_nights <int> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ~
## $ stays_in_week_nights <int> 0, 0, 1, 1, 2, 2, 2, 2, 3, 3, 4, 4, 4, ~
## $ adults <int> 2, 2, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, ~
## $ children <int> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ~
## $ babies <int> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ~
## $ meal <fct> BB, BB, BB, BB, BB, BB, BB, FB, BB, HB,~
## $ country <fct> PRT, PRT, GBR, GBR, GBR, GBR, PRT, PRT,~
## $ market_segment <fct> Direct, Direct, Direct, Corporate, Onli~
## $ distribution_channel <fct> Direct, Direct, Direct, Corporate, TA/T~
## $ is_repeated_guest <lgl> FALSE, FALSE, FALSE, FALSE, FALSE, FALS~
## $ previous_cancellations <int> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ~
## $ previous_bookings_not_canceled <int> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ~
## $ reserved_room_type <fct> C, C, A, A, A, A, C, C, A, D, E, D, D, ~
## $ assigned_room_type <fct> C, C, C, A, A, A, C, C, A, D, E, D, E, ~
## $ booking_changes <int> 3, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ~
## $ deposit_type <fct> No Deposit, No Deposit, No Deposit, No ~
## $ agent <fct> NULL, NULL, NULL, 304, 240, 240, NULL, ~
## $ company <fct> NULL, NULL, NULL, NULL, NULL, NULL, NUL~
## $ days_in_waiting_list <int> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ~
## $ customer_type <fct> Transient, Transient, Transient, Transi~
## $ adr <dbl> 0.00, 0.00, 75.00, 75.00, 98.00, 98.00,~
## $ required_car_parking_spaces <int> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ~
## $ total_of_special_requests <int> 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 3, ~
## $ reservation_status <fct> Check-Out, Check-Out, Check-Out, Check-~
## $ reservation_status_date <chr> "2015-07-01", "2015-07-01", "2015-07-02~
Ya tenemos todas las variables con el tipo correcto. La variable adr de origen viene bien y la variable reservation_status_date por el momento conviene tenerla como character (en ese tipo no ocupa mucho espacio en memoria).
Ya habíamos identificado que en la variable children hay 4 valores NA, por lo que, se procederá a imputarlos por el valor más frecuente (la moda). También, en la variable country se detectaron 488 valores NULL, por lo que, se seguirá la misma estratégia de imputación debido a que son relativamente pocos datos en relacion con el tamaño de la base de datos. En este paso, aprovecharemos para eliminar las variables agent y company, ya que tienen demasiados valores NULL y no conviene realizar algún tipo de imputación:
# se identifica el valor más frecuente
count(df, children, sort = T) %>% # children
kbl(align = "c", digits = 2 ,format.args = list(big.mark = ",")) %>%
kable_paper("hover", full_width = F)
| children | n |
|---|---|
| 0 | 110,796 |
| 1 | 4,861 |
| 2 | 3,652 |
| 3 | 76 |
| NA | 4 |
| 10 | 1 |
count(df, country, sort = T) %>% head(10) %>% # country
kbl(align = "c", digits = 2 ,format.args = list(big.mark = ",")) %>%
kable_paper("hover", full_width = F)
| country | n |
|---|---|
| PRT | 48,590 |
| GBR | 12,129 |
| FRA | 10,415 |
| ESP | 8,568 |
| DEU | 7,287 |
| ITA | 3,766 |
| IRL | 3,375 |
| BEL | 2,342 |
| BRA | 2,224 |
| NLD | 2,104 |
Se eliminan las variables agent y company. Se imputan los valores NA y NULL.
df <- df %>%
select(-agent, -company) %>% # se eliminan esas variables
mutate(children = ifelse(is.na(children), 0, children), # se sustituyen NA por 0
country = fct_recode(country, 'PRT' = 'NULL') # se sustituyenn NULL por PRT
)
Como solo se identificó una observacion exrema (5,400), se procederá a eliminarla del dataset:
df <- df %>%
filter(adr != 5400)
Las acciones para recodificar algunas variables serán:
df <- df %>%
#Recodificar
mutate(
country = fct_lump(country, n = 15, other_level = 'OTROS'),
market_segment = fct_collapse(market_segment,
'Corporate' = c('Corporate','Undefined','Aviation','Complementary')),
distribution_channel = fct_collapse(distribution_channel,
'Corporate' = c('Corporate','Undefined','GDS')),
reservation_status = fct_collapse(reservation_status,
'Canceled' = c('Canceled','No-Show')),
deposit_type = fct_collapse(deposit_type,
'Deposit' = c('Non Refund','Refundable')),
assigned_room_type = fct_collapse(assigned_room_type,
'A' = 'A',
'D' = 'D',
other_level = 'OTROS'),
reserved_room_type = fct_collapse(reserved_room_type,
'A' = 'A',
'D' = 'D',
other_level = 'OTROS'),
customer_type = fct_collapse(customer_type,
'Transient' = 'Transient',
other_level = 'OTROS'),
meal = fct_collapse(meal,
'BB' = 'BB',
other_level = 'OTROS'))
Ahora, lo siguiente es discretizar las tres variables que por su distribución tan dispersa es mejor modificarlas:
df <- df %>%
mutate(
adults = as.factor(case_when(adults <= 1 ~ '01_Uno',
adults == 2 ~ '02_Dos',
adults > 2 ~ '03_Mas_de_Dos',
TRUE ~ '99_ERROR')),
stays_in_week_nights_disc = as.factor(case_when(stays_in_week_nights <= 1 ~ '01_Uno',
stays_in_week_nights == 2 ~ '02_Dos',
stays_in_week_nights == 3 ~ '03_Tres',
stays_in_week_nights == 4 ~ '04_Cuatro',
stays_in_week_nights > 4 ~ '05_Mas_de_Cuatro',
TRUE ~ '99_ERROR')),
stays_in_weekend_nights_disc = as.factor(case_when(stays_in_weekend_nights <= 1 ~ '01_Uno',
stays_in_weekend_nights > 1 ~ '02_Mas_de_Uno',
TRUE ~ '99_ERROR')),
)
Por último, crear variables binarias para las siguientes variables:
df <- df %>%
mutate(
babies = as.logical(ifelse(babies == 0, 0, 1)),
booking_changes = as.logical(ifelse(booking_changes == 0, 0, 1)),
children = as.logical(ifelse(children == 0, 0, 1)),
previous_cancellations = as.logical(ifelse(previous_cancellations == 0, 0, 1)),
required_car_parking_spaces = as.logical(ifelse(required_car_parking_spaces == 0, 0, 1)),
total_of_special_requests = as.logical(ifelse(total_of_special_requests == 0, 0, 1)),
days_in_waiting_list = as.logical(ifelse(days_in_waiting_list == 0, 0, 1)),
previous_bookings_not_canceled = as.logical(ifelse(previous_bookings_not_canceled == 0, 0, 1))
)
Una vez que hemos realizado todas las transformaciones que identificamos en la parte del análisis exploratorio, Volvemos a hacer los gráficos para comprobar los resultados deseados.
Variables de tipo factor:
# primer bloque de variables
df %>%
select_if(is.factor) %>%
select(1:9) %>%
mutate(id = 1:nrow(.)) %>%
pivot_longer(-id, names_to = 'variable', values_to = 'valor') %>%
ggplot(aes(valor)) +
geom_bar() +
scale_y_continuous(labels = scales::comma) +
facet_wrap(vars(variable), scales = 'free', ncol = 3)
# segundo bloque de variables
df %>%
select_if(is.factor) %>%
select(10:17) %>%
mutate(id = 1:nrow(.)) %>%
pivot_longer(-id, names_to = 'variable', values_to = 'valor') %>%
ggplot(aes(valor)) +
geom_bar() +
scale_y_continuous(labels = scales::comma) +
facet_wrap(vars(variable), scales = 'free', ncol = 3)
Variables binarias o lógicas:
df %>%
select_if(is.logical) %>%
mutate(id = 1:nrow(.)) %>%
pivot_longer(-id, names_to = 'variable', values_to = 'valor') %>%
ggplot(aes(valor)) +
geom_bar() +
scale_y_continuous(labels = scales::comma) +
facet_wrap(vars(variable), scales = 'free',ncol = 2)
Variable continua:
df %>%
ggplot(aes(x = factor(1), y = adr)) +
geom_boxplot(outlier.color = "red") +
labs(
title = "adr"
)
descr(df$adr,
style = "rmarkdown",
justify = "center",
headings = T) %>%
kbl(align = "c", digits = 2 ,format.args = list(big.mark = ",")) %>%
kable_paper("hover", full_width = F)
| adr | |
|---|---|
| Mean | 101.79 |
| Std.Dev | 48.15 |
| Min | -6.38 |
| Q1 | 69.29 |
| Median | 94.56 |
| Q3 | 126.00 |
| Max | 510.00 |
| MAD | 41.28 |
| IQR | 56.71 |
| CV | 0.47 |
| Skewness | 1.02 |
| SE.Skewness | 0.01 |
| Kurtosis | 2.13 |
| N.Valid | 119,389.00 |
| Pct.Valid | 100.00 |
En las gráficas actualizadas, ya podemos apreciar mejor las distribuciones de las variables, pues no hay asimetrías fuertes ni desbalances severos en las variables de tipo factor. Las variables binarias o lógicas se ven bien y la única variable continua aunque presenta valores atípicos ya no hay presencia de valores extremos enmascarados.
Una de las partes más interesantes en cualquier proyecto de discovery es el de crear variables nuevas a partir de las variables originales, las cuales son comúnmente llamadas variables sintéticas. Estas variables deben de crearse con un sentido de negocio. A continuación, crearemos algunas variables que nos servirán para la etapa de discovery.
En la base de datos no viene una fecha de llegada como tal, o sea, compuesta por el día, mes y año, sin embargo, si tenemos cada componente por separado, por lo que, vamos a crearla.
Ademas, tenemos información sobre el número de personas adultas, niños y bebés, por tanto, podemos crear una variable que nos indique el tipo de familia que se ha hospedado.
Podemos incluir otra variable para saber si la persona es extranjera o nacional, entendiendo que nacional correspondería a los clientes de nacionalidad portuguesa.
Una tercera variable que nos sería de utilidad es el cambio de habitación, que la obtendremos de la diferencia entre el tipo de cuarto reservado y el tipo de cuarto asignado.
Tambien, puede ser de utilidad conocer las noches totales que las personas se quedaron en algún hotel, conformadas por la suma de las noches entre semana más las noches en fin de semana.
Finalmente, una variable que sorprende que no venga en la base es la facturación total, por lo que, la crearemos.
En resumen, lo que haremos será crear las siguientes variables:
df <- df %>%
mutate(fecha_llegada = as_date(paste(arrival_date_year,
arrival_date_month,
arrival_date_day_of_month,
sep = '/')),
familia = case_when(
adults == '01_Uno' & children + babies == 0 ~ '01_Soltero',
adults == '01_Uno' & children + babies > 0 ~ '02_Monoparental',
adults == '02_Dos' & children + babies == 0 ~ '03_Pareja_Sin_Hijos',
adults == '02_Dos' & children + babies > 0 ~ '04_Pareja_Con_Hijos',
adults == '03_Mas_de_Dos' & children + babies == 0 ~ '05_Grupo_Amigos',
adults == '03_Mas_de_Dos' & children + babies > 0 ~ '06_Grupo_Amigos_Con_Hijos',
TRUE ~ '99_ERROR'),
extranjero = ifelse(country == 'PRT', 0, 1),
cambio_habit = ifelse(reserved_room_type == assigned_room_type, 0, 1),
noches_totales = stays_in_weekend_nights + stays_in_week_nights,
facturacion_reserva = noches_totales * adr
)
En esta etapa es donde vamos a hacer propiamente el análisis del negocio, pues ya tenemos la data limpia y transformada. Es importante tener presente el tipo de giro o sector, ya que de eso dependerá los insights a buscar.
Para el sector hotelero y, en general, el turismo, el identificar las llamadas temporadas altas y bajas es muy importante, ya que eso les permitirá prepararse, por ejemplo, para la llegada de masiva de personas (temporada alta) y/o hacer remodelaciones o ampliaciones a su infraestructura (temporada baja).
Técnicamente, para poder determinar si una serie presenta estacionalidad, entendida ésta como tendencias o patrones que se repiten constantemente dentro de un periodo de tiempo, es necesario al memos cuatro años de historia, sin embargo, como no contamos con tanta historia solo nos enfocaremos en el único periodo completo que tenemos, año 2016.
Veamos la tendencia del año 2016 en general, para ambos hoteles:
df %>%
filter(arrival_date_year == '2016') %>%
ggplot(aes(fecha_llegada)) +
geom_density(size = 2, color = 'grey') +
theme(axis.text.x = element_text(angle = 90, size = 10)) +
labs(title = "Llegadas 2016")
Al ver la gráfica podemos apreciar que al inicio y al final del año la demanda es baja, pero al inicio del tercer trimestre la demanda es la más alta. Complementemos el análisis visual con una tabla resumen:
df %>%
mutate(reservaciones = rep(1, nrow(df))) %>% # se crea la variable reservaciones
filter(arrival_date_year == 2016) %>% # se filtra por el año 2016
group_by(arrival_date_month) %>% # agrupamos por el mes de llegada
summarise(reservaciones = n()) %>% # contamos el número de reservaciones
arrange(desc(-reservaciones)) %>% # ordenamos de menor a mayor
kbl(align = "c", digits = 2 ,format.args = list(big.mark = ",")) %>%
kable_paper("hover", full_width = F) # acomodamos la salida en una tabla
| arrival_date_month | reservaciones |
|---|---|
| January | 2,248 |
| December | 3,860 |
| February | 3,891 |
| November | 4,454 |
| July | 4,572 |
| March | 4,823 |
| August | 5,063 |
| June | 5,292 |
| September | 5,394 |
| April | 5,428 |
| May | 5,478 |
| October | 6,203 |
Ya con esta otra vista podemos concluir lo siguiente:
Ahora, veamos la tendencia por tipo de hotel:
df %>%
filter(arrival_date_year == '2016') %>%
ggplot(aes(fecha_llegada, color = hotel)) +
scale_color_manual(values = c("steelblue", "grey50"))+
geom_density(size = 2) +
theme(axis.text.x = element_text(angle = 90, size = 10)) +
labs(title = "Llegadas 2016 por tipo de Hotel")
De la gráfica anterior podemos ver que:
Hagamos una tabla para comparar los datos:
df %>%
mutate(reservaciones = rep(1, nrow(df))) %>% # se crea la variable reservaciones
filter(arrival_date_year == 2016) %>% # se filtra por el año 2016
group_by(arrival_date_month, hotel) %>% # se agrupa por el mes de arrivo y hotel
summarise(reservaciones = n()) %>% # se hace conteo de reservaciones
arrange(desc(-reservaciones)) %>% # se ordena de menor a mayor
pivot_wider(names_from = hotel, values_from = reservaciones) %>% # se pasa a formato ancho
kbl(align = "c", digits = 2 ,format.args = list(big.mark = ",")) %>%
kable_paper("hover", full_width = F) # salida en forma de tabla
| arrival_date_month | Resort Hotel | City Hotel |
|---|---|---|
| January | 884 | 1,364 |
| November | 1,332 | 3,122 |
| June | 1,369 | 3,923 |
| December | 1,382 | 2,478 |
| July | 1,441 | 3,131 |
| February | 1,520 | 2,371 |
| September | 1,523 | 3,871 |
| August | 1,685 | 3,378 |
| March | 1,778 | 3,045 |
| May | 1,802 | 3,676 |
| April | 1,867 | 3,561 |
| October | 1,984 | 4,219 |
Para poder analizar la facturación tenemos que trabajar sólo con las reservas que finalmente tuvieron check-out.
df %>%
filter(reservation_status == 'Check-Out') %>%
group_by(arrival_date_year, hotel) %>%
summarise(facturacion = sum(facturacion_reserva)) %>%
ggplot(aes(x = arrival_date_year, y = facturacion, fill = hotel)) +
scale_fill_manual(values = c("steelblue", "grey50")) +
scale_y_continuous(labels = scales::comma) +
geom_col(position = 'dodge') +
labs(title = "Facturación por Tipo de Hotel", y = "Facturación", x = "Año de Llegada")
En la gráfica podemos apreciar claramente que la facturación se ha ido incrementando cada año, ahora, hay que tener presente que solo el año 2016 está completo, sin embargo, vemos que el 2017 se ve bien, pues solo llega al mes de agosto y casi alcanza los niveles de 2016.
Este incremento pudo deberse a varios motivos como, por ejemplo:
Con el apoyo de una tabla veamos si esos motivos explican el incremento en la facturación:
df %>%
filter(reservation_status == 'Check-Out' & arrival_date_year %in% c('2015','2016')) %>%
group_by(hotel,arrival_date_year) %>%
summarise(
num_reservas = n(),
noches_por_reserva = mean(noches_totales),
precio_noche = mean(adr)) %>%
kbl(align = "c", digits = 2 ,format.args = list(big.mark = ",")) %>%
kable_paper("hover", full_width = T)
| hotel | arrival_date_year | num_reservas | noches_por_reserva | precio_noche |
|---|---|---|---|---|
| City Hotel | 2015 | 7,678 | 2.76 | 87.87 |
| City Hotel | 2016 | 22,733 | 2.90 | 104.08 |
| Resort Hotel | 2015 | 6,176 | 4.39 | 89.76 |
| Resort Hotel | 2016 | 13,637 | 3.96 | 83.91 |
Ya con la tabla podemos concluir que, para el caso del hotel de ciudad, el incremento de su facturación se debió, principalmente, al aumento en el número de reservas (casi 3 veceses más) y, en segundo lugar, al aumento del precio promedio por noche. Para el caso del hotel resort, el incremento en su facturación es debido únicamente al aumento en el número de reservas, porque tanto el número de noches por reserva como el precio promedio por noche presentaron disminuciones.
Veamos cómo está conformada la facturación por segmento de mercado:
df %>%
group_by(hotel, market_segment) %>%
summarise(facturacion_segmento = sum(facturacion_reserva)) %>%
group_by(hotel) %>%
mutate(facturacion_total = sum(facturacion_segmento),
porc_facturacion_total = facturacion_segmento / facturacion_total * 100) %>%
dplyr::select(-facturacion_total) %>%
kbl(align = "c", digits = 2 ,format.args = list(big.mark = ",")) %>%
kable_paper("hover", full_width = T)
| hotel | market_segment | facturacion_segmento | porc_facturacion_total |
|---|---|---|---|
| City Hotel | Corporate | 574,501.0 | 2.27 |
| City Hotel | Direct | 2,122,531.5 | 8.40 |
| City Hotel | Groups | 3,121,991.0 | 12.35 |
| City Hotel | Offline TA/TO | 4,469,802.0 | 17.69 |
| City Hotel | Online TA | 14,985,244.4 | 59.29 |
| Resort Hotel | Corporate | 292,371.2 | 1.68 |
| Resort Hotel | Direct | 2,970,496.9 | 17.03 |
| Resort Hotel | Groups | 1,547,645.7 | 8.87 |
| Resort Hotel | Offline TA/TO | 3,676,710.8 | 21.08 |
| Resort Hotel | Online TA | 8,956,803.1 | 51.35 |
De la tabla anterior, podemos concluir que la mayor parte de la facturación en ambos hoteles viene por el lado del segmento Online y, por el contrario, el segmento que menos contribuye es el Corporate.
t0 <- df %>%
group_by(hotel, market_segment) %>%
summarise(facturacion_segmento = sum(facturacion_reserva)) %>%
group_by(hotel) %>%
mutate(facturacion_total = sum(facturacion_segmento),
porc_facturacion_total = facturacion_segmento / facturacion_total * 100) %>%
dplyr::select(-facturacion_total, -porc_facturacion_total)
t0$hotel <- as.character(t0$hotel)
t0$market_segment <- as.character(t0$market_segment)
sankey_ly(x = t0,
cat_cols = c("hotel", "market_segment"),
num_col = "facturacion_segmento",
title = "Distribución de Ingresos por Segmento Según Tipo de Hotel")
Ahora, analicemos la facturación por familia, para ver la contribución de cada segmento:
df %>%
group_by(hotel, familia) %>%
summarise(facturacion_familia = sum(facturacion_reserva)) %>%
group_by(hotel) %>%
mutate(facturacion_total = sum(facturacion_familia),
porc_facturacion_total = facturacion_familia / facturacion_total * 100) %>%
dplyr::select(-facturacion_total) %>%
kbl(align = "c", digits = 2 ,format.args = list(big.mark = ",")) %>%
kable_paper("hover", full_width = T)
| hotel | familia | facturacion_familia | porc_facturacion_total |
|---|---|---|---|
| City Hotel | 01_Soltero | 3,797,560.88 | 15.03 |
| City Hotel | 02_Monoparental | 217,619.03 | 0.86 |
| City Hotel | 03_Pareja_Sin_Hijos | 16,374,295.01 | 64.79 |
| City Hotel | 04_Pareja_Con_Hijos | 2,347,447.84 | 9.29 |
| City Hotel | 05_Grupo_Amigos | 2,435,302.74 | 9.64 |
| City Hotel | 06_Grupo_Amigos_Con_Hijos | 101,844.46 | 0.40 |
| Resort Hotel | 01_Soltero | 1,221,868.26 | 7.00 |
| Resort Hotel | 02_Monoparental | 97,736.74 | 0.56 |
| Resort Hotel | 03_Pareja_Sin_Hijos | 12,250,816.46 | 70.23 |
| Resort Hotel | 04_Pareja_Con_Hijos | 2,676,953.73 | 15.35 |
| Resort Hotel | 05_Grupo_Amigos | 853,298.76 | 4.89 |
| Resort Hotel | 06_Grupo_Amigos_Con_Hijos | 343,353.62 | 1.97 |
Claramente se aprecia en el cuadro anterior, que el segmento de parejas sin hijos es el que más contribuye a la facturación total en ambos hoteles.
t2 <- df %>%
group_by(hotel, familia) %>%
summarise(facturacion_familia = sum(facturacion_reserva)) %>%
group_by(hotel) %>%
mutate(facturacion_total = sum(facturacion_familia),
porc_facturacion_total = facturacion_familia / facturacion_total * 100) %>%
dplyr::select(-facturacion_total, -porc_facturacion_total)
t2$hotel <- as.character(t2$hotel)
sankey_ly(x = t2,
cat_cols = c("hotel", "familia"),
num_col = "facturacion_familia",
title = "Distribución de Ingresos por Familia Según Tipo de Hotel")
En cuanto a los clientes, como ambos hoteles se encuentran en Portugal suponemos que la mayoría son de ese mismo país, sin embargo, hagamos una tabla para ver la distribución entre clientes nacionales y extranjeros:
count(df, hotel, extranjero) %>%
group_by(hotel) %>%
mutate(total_hotel = sum(n),
porc_hotel = n / total_hotel * 100) %>%
kbl(align = "c", digits = 2 ,format.args = list(big.mark = ",")) %>%
kable_paper("hover", full_width = F)
| hotel | extranjero | n | total_hotel | porc_hotel |
|---|---|---|---|---|
| City Hotel | 0 | 30,983 | 79,329 | 39.06 |
| City Hotel | 1 | 48,346 | 79,329 | 60.94 |
| Resort Hotel | 0 | 18,094 | 40,060 | 45.17 |
| Resort Hotel | 1 | 21,966 | 40,060 | 54.83 |
Al ver el cuadro anterior, sorprende identificar que la mayoría de los clientes son extranjeros en ambos hoteles, ademas, en el hotel de ciudad hay casi el doble de clientes que en el hotel resort y en éste último poco menos de la mitad son clientes nacionales.
Ahora, veamos el top 5 de clientes por nacionalidad para ver de qué países hay mayor presencia para ambos hoteles:
count(df, hotel, country) %>%
group_by(hotel) %>%
mutate(total_hotel = sum(n),
porc_hotel = n / total_hotel * 100) %>%
arrange(hotel, desc(porc_hotel)) %>%
top_n(5) %>%
dplyr::select(-total_hotel) %>%
kbl(align = "c", digits = 2 ,format.args = list(big.mark = ",")) %>%
kable_paper("hover", full_width = F)
| hotel | country | n | porc_hotel |
|---|---|---|---|
| City Hotel | PRT | 30,983 | 39.06 |
| City Hotel | FRA | 8,804 | 11.10 |
| City Hotel | OTROS | 8,483 | 10.69 |
| City Hotel | DEU | 6,084 | 7.67 |
| City Hotel | GBR | 5,315 | 6.70 |
| Resort Hotel | PRT | 18,094 | 45.17 |
| Resort Hotel | GBR | 6,814 | 17.01 |
| Resort Hotel | ESP | 3,957 | 9.88 |
| Resort Hotel | OTROS | 2,226 | 5.56 |
| Resort Hotel | IRL | 2,166 | 5.41 |
Como era de esperarse, por país, la mayoría de los clientes de esta cadena de hoteles son de Portugal. Sin embargo, en el hotel de ciudad son los franceses y alemanes los que le siguen, a diferencia del hotel resort donde son los británicos y españoles los que tienen mayor frecuencia después de los portugueses.
A continuación, echemos un vistazo al promedio de los precios por noche:
df %>%
filter(arrival_date_year %in% c('2015','2016')) %>%
group_by(hotel, arrival_date_week_number) %>%
summarise(precio_medio = mean(adr)) %>%
ggplot(aes(x = arrival_date_week_number, y = precio_medio, group = 1)) +
geom_line(color = 'grey50') +
geom_smooth(color = 'red') +
theme(axis.text.x = element_text(size = 8)) +
labs(title = "Tendencia de Precios: 2015-2016")
La gráfica nos muestra que, al inicio de cada año y al final, el precio promedio de las reservas tiene su menor nivel, lo cual coincide con las épocas de baja demanda. Son entre las semanas 29 a 35 donde el precio promedio alcanza sus niveles máximos que, igualmente, se corresponde con la temporada de alta demanda.
Por lo anterior, veamos si existe alguna correlación entre el precio promedio y la demanda de reservas.
Vamos hacer un pequeño análisis estadístico de correlación, por lo que, debemos tener presente algunas consideraciones:
Análisis visual:
c <- df %>%
filter(arrival_date_year %in% c('2015', '2016')) %>%
group_by(hotel, arrival_date_week_number) %>%
summarise(num_reservas = n(),
precio_medio = mean(adr)) %>%
select(num_reservas, precio_medio)
h1 <- ggplot(data = c, aes(x = num_reservas)) + geom_histogram(aes(y = ..density.., fill = ..count..)) + scale_fill_gradient(low = "#DCDCDC", high = "#7C7C7C") + stat_function(fun = dnorm, colour = "firebrick", args = list(mean = mean(c$num_reservas), sd = sd(c$num_reservas))) + ggtitle("Histograma Reservas") + theme_bw()
qq1 <- ggplot(c, aes(sample = num_reservas)) +
stat_qq() +
stat_qq_line() +
labs(title = "Distribución de Reservas")
h2 <- ggplot(data = c, aes(x = precio_medio)) + geom_histogram(aes(y = ..density.., fill = ..count..)) + scale_fill_gradient(low = "#DCDCDC", high = "#7C7C7C") + stat_function(fun = dnorm, colour = "firebrick", args = list(mean = mean(c$precio_medio), sd = sd(c$precio_medio))) + ggtitle("Histograma Precio Medio") + theme_bw()
qq2 <- ggplot(c, aes(sample = precio_medio)) +
stat_qq() +
stat_qq_line() +
labs(title = "Distribución de Precio Medio")
grid.arrange(h1, h2, qq1, qq2, nrow = 2, ncol = 2, top = "Análisis de Normalidad")
Al ver las gráficas podemos sospechar que ambas variables no presentan una distribución normal. Para poder confirmar lo anterior tenemos que hacer pruebas estadísticas. Ahora, como ambas series tienen más de 50 observaciones aplicaremos la prueba de normalidad de Lilliefors:
Pruebas de normalidad:
lillie.test(x = c$num_reservas)
##
## Lilliefors (Kolmogorov-Smirnov) normality test
##
## data: c$num_reservas
## D = 0.12858, p-value = 0.000183
lillie.test(x = c$precio_medio)
##
## Lilliefors (Kolmogorov-Smirnov) normality test
##
## data: c$precio_medio
## D = 0.10501, p-value = 0.005855
Los resultados muestran que las dos series no presentan una distribución normal, ya que ambos p-value son menores al 0.05 de significancia. Por lo anterior, ya podemos decidir cuál coeficiente usar para conocer el grado de asociación entre ellas dadas las características de las series.
Coeficiente de Correlación
Ya sabiendo que las series no presentan distribución normal, se usará el coeficiente de correlación de Spearman:
cor(c$num_reservas, log(c$precio_medio), method = "spearman")
## [1] 0.5401534
La correlación resulta ser positiva de 0.54, lo que nos indica un grado de asociación moderada, sin embargo, hay que hacer el test de significancia, para confirmar que realmente existe correlación entre el número de reservas y el precio medio, ya que de lo contrario dicha asociación puede deberse al azar.
cor.test(x = c$num_reservas,
y = log(c$precio_medio),
alternative = "two.sided",
conf.level = 0.95,
method = "spearman")
##
## Spearman's rank correlation rho
##
## data: c$num_reservas and log(c$precio_medio)
## S = 91273, p-value = 2.289e-09
## alternative hypothesis: true rho is not equal to 0
## sample estimates:
## rho
## 0.5401534
El p-value es cercano a cero (menor a 0.05), por lo que, podemos afirmar que el coeficiente de correlación antes obtenido es significativo, o sea, la asociación entre las reservas y el precio medio no se da por casualidad.
Ya por último, calculemos el coeficiente de determinación para medir el tamaño del efecto:
cor(c$num_reservas, log(c$precio_medio), method = "spearman")^2
## [1] 0.2917657
El tamaño del efecto es mediano, cercano al 0.3. Veamos la relación visualmente:
g <- c %>%
ggplot(aes(num_reservas, precio_medio)) +
geom_point() +
geom_smooth() +
labs(title = "Relación Reservas vs Precio Promedio")
ggplotly(g)
Se aprecia que no hay una clara tendencia positiva o negativa, pues la nube de puntos casi se mantiene constante. Ahí hay una oportunidad de negocio.
veamos ahora por tipo de hotel y por semana durante el periodo de análisis:
df4 <- df %>%
filter(reservation_status == 'Check-Out' & adr > 0)
df4 <- df4 %>%
group_by(arrival_date_year, hotel, arrival_date_week_number) %>%
summarise(num_reservas = n(),
precio_medio = mean(adr)) %>%
select(arrival_date_year, num_reservas, precio_medio, arrival_date_week_number)
df4$arrival_date_week_number <- as.numeric(df4$arrival_date_week_number)
dfr <- df4 %>%
dplyr::filter(hotel == "Resort Hotel")
dfc <- df4 %>%
dplyr::filter(hotel == "City Hotel")
gr <- ggplot(dfr, aes(num_reservas, precio_medio)) +
geom_jitter() +
geom_point(aes(colour = arrival_date_week_number), size = 3) +
labs(title = "Tendencia Semanal Hotel Resort: Precio Medio y Número de Reservaciones")
gc <- ggplot(dfc, aes(num_reservas, precio_medio)) +
geom_jitter() +
geom_point(aes(colour = arrival_date_week_number), size = 3) +
labs(title = "Tendencia Semanal Hotel Ciudad: Precio Medio y Número de Reservaciones")
grid.arrange(gr, gc, nrow = 2, top = "Tendencias Semanales")
En las gráficas anteriores, el color de los puntos indica el tiempo (número de semana), entre más claro es el color más cercano al fin de año nos encontramos. Es en el hotel resort donde, en general, no se corresponde la temporada alta con precios promedio elevados, por lo que, sería conveniente analizar la posibilidad de aumentar los precios en temporada alta, o sea, buscar el precio óptimo en ese periodo del año.
Veamos la distribución de los precios por tipo de hotel:
ggplot(df, aes(x = hotel, y = adr, color = hotel)) +
geom_boxplot() +
scale_color_manual(values = c("steelblue", "grey50")) +
stat_summary(fun = mean,
geom = "point",
shape = 20,
size = 4,
color = "red",
fill = "red") +
labs(title = "Precio Medio por Hotel")
Los precios medios en ambos tipos de hotel son muy parecidos, siendo en el hotel resort donde el 50% de los precios son más bajos.
pm <- df %>%
group_by(hotel, lead_time) %>%
summarise(precio_medio = mean(adr))
gm <- pm %>%
ggplot(aes(x = lead_time, y = precio_medio, color = hotel)) +
geom_point(alpha = 0.3, size = 1) +
scale_color_manual(values = c("blue", "black")) +
geom_smooth(se = F, size = 1) +
geom_vline(xintercept = 180, colour = "red") +
labs(title = "Tiempo de Espera")
ggplotly(gm)
Esta gráfica nos indica que reservando con más de 6 meses de anticipación se consiguen precios más bajos, sobre todo en el hotel resort. Hay dos observaciones con precios de cero, lo que probablemente se deba a error de origen o fueron promociones que se obsequiaron.
A continuación, se graficará la curva de Pareto de la facturación. Recordando que la curva de pareto consiste en mostrar la regla de que, aproximadamente, el 20% de las observaciones analizadas generan el 80% de sus resultados. Esta regla no es fija y puede variar la distribución, sin embargo, es un buen indicador que permite conocer a los mejores clientes, los más rentables.
Entonces, hagamos una curva de pareto de la facturación por país para conocer la contribución de cada uno al negocio con las siguientes consideraciones: solo crearla para las reservaciones que no fueron canceladas y, también, sin considerar la categoría de OTROS que creamos anteriormente (por su nivel marginal de contribución de cada país que integran esa categoría).
df %>%
filter(is_canceled == FALSE & country != "OTROS") %>%
group_by(country) %>%
summarise(total = sum(facturacion_reserva)) %>%
arrange(desc(total)) %>%
mutate(porc.acum.ventas = cumsum(total) / sum(total),
ranking.ventas = row_number(desc(total))) %>%
dplyr::select(ranking.ventas, porc.acum.ventas) %>%
ggplot(aes(x = ranking.ventas, y = porc.acum.ventas)) +
geom_line() +
geom_vline(xintercept = 6, colour = "red") +
geom_hline(yintercept = 0.79, colour = "red") +
scale_x_continuous(breaks = c(1:15), labels = c("PRT",
"GBR",
"FRA",
"ESP",
"DEU",
"IRL",
"ITA",
"BEL",
"NLD",
"CHE",
"USA",
"BRA",
"CN",
"AUT",
"SWE")) +
ggplot2::annotate("text",
label = "80%-6",
x = 5.5, y = 0.83,
size = 3,
color = "navyblue") +
labs(title = "Curva Pareto Facturación")
La gráfica nos muestra la contribución, a nivel de país, que tienen los clientes. Ahora, sin considerar a Portugal (por ser el país en donde se encuentran los hoteles), son solo 5 países los que contribuyen a generar casi el 80% de la facturación total: Gran Bretaña, Francia, España, Alemania e Irlanda. Sabiendo esto podemos recomentar focalizar promociones y/o campañas para los turistas de esas nacionalidades.
Del análisis anterior, podemos señalar las siguentes conclusiones: