NOTA: aquest tutorial utilitza R + RStudio + alguns paquets de R per mostrar el potencial de l’ús de la visualització de dades per inspeccionar i analitzar un conjunt de dades. Recomanem explorar els següents enllaços:

  1. RStudio: https://posit.co/downloads/
  2. ggplot2: https://ggplot2.tidyverse.org/
  3. extensions: https://exts.ggplot2.tidyverse.org/gallery/
  4. ggmosaic: aquest paquet ha estat eliminat de CRAN, cal instal·lar-ne una versió antiga:

Baixeu i instal·leu RTools de “https://cran.rstudio.com/bin/windows/Rtools/rtools45/rtools.html

Baixeu ggmosaic executant:
install.packages("ggmosaic", repos = c("https://haleyjeppson.r-universe.dev", "https://cloud.r-project.org"))

NOTA: si voleu utilitzar un entorn virtual creat amb conda per a aquest projecte, podeu seguir les instruccions del fitxer README.md.

NOTA: Si no voleu utilitzar l’entorn virtual i voleu editar i executar aquest fitxer Rmd directament a VSCode, canvieu el camí a l’executable R (RPath) per a la vostra plataforma. En la configuració proporcionada de VSCode (settings.json) el camí al fitxer R utilitza l’entorn virtual creat a la carpeta .conda a la base del directori del projecte en macos. "r.rpath.mac": "${workspaceFolder}/.conda/bin/R"

0.1 Càrrega dels paquets necessaris

library("tidyverse")
## ── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
## ✔ dplyr     1.1.4     ✔ readr     2.1.6
## ✔ forcats   1.0.1     ✔ stringr   1.6.0
## ✔ ggplot2   4.0.1     ✔ tibble    3.3.0
## ✔ lubridate 1.9.4     ✔ tidyr     1.3.2
## ✔ purrr     1.2.0     
## ── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
## ✖ dplyr::filter() masks stats::filter()
## ✖ dplyr::lag()    masks stats::lag()
## ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
library("fitdistrplus")
## Loading required package: MASS
## 
## Attaching package: 'MASS'
## 
## The following object is masked from 'package:dplyr':
## 
##     select
## 
## Loading required package: survival
# library("ggmosaic")
library("ggstatsplot")
## You can cite this package as:
##      Patil, I. (2021). Visualizations with statistical details: The 'ggstatsplot' approach.
##      Journal of Open Source Software, 6(61), 3167, doi:10.21105/joss.03167
library("ggplot2")
library("kableExtra")
## 
## Attaching package: 'kableExtra'
## 
## The following object is masked from 'package:dplyr':
## 
##     group_rows
library("MASS")
library("reshape2")
## 
## Attaching package: 'reshape2'
## 
## The following object is masked from 'package:tidyr':
## 
##     smiths
library("survival")
library("scales")
## 
## Attaching package: 'scales'
## 
## The following object is masked from 'package:purrr':
## 
##     discard
## 
## The following object is masked from 'package:readr':
## 
##     col_factor

0.2 Conjunt de dades

Descripció i tipus de les variables
Variable Tipus Descripcio
hotel factor Tipus d’hotel (ciutat o resort)
is_canceled factor 1 si la reserva es va cancel·lar, 0 en cas contrari
lead_time integer Nombre de dies entre la reserva i l’arribada
arrival_date_year integer Any d’arribada
arrival_date_month integer Mes d’arribada, amb 12 categories: «January» a «December»
arrival_date_week_number integer Setmana d’arribada de l’any
arrival_date_day_of_month integer Dia del mes de l’arribada
stays_in_weekend_nights integer Nombre de nits d’estada en cap de setmana
stays_in_week_nights integer Nombre de nits d’estada entre setmana
adults integer Nombre d’adults
children integer Nombre de nens
babies integer Nombre de nadons
meal factor Tipus de règim alimentari reservat. Tipus d’àpat reservat. Les categories són: Undefined/SC (sense regim alimentari); BB (Allotjament i esmorzar); HB (Mitja pensió amb l’esmorzar i un altre àpat, normalment sopar); FB (Pensió completa amb esmorzar, dinar i sopar)
country factor País de residència de l’hoste
market_segment factor Segment de mercat de l’hotel. «TA» significa «agents de viatges (Travel Agents)» i «TO» significa «operadors turístics (Tour Operators)»
distribution_channel factor Canal de distribució utilitzat. El terme «TA» significa «agents de viatges (Travel Agents)» i «TO» significa «operadors turístics (Tour Operators)».
is_repeated_guest factor 1 si l’hoste és un client repetit, 0 en cas contrari
previous_cancellations integer Nombre de cancel·lacions anteriors per a aquest hoste
previous_bookings_not_canceled integer Nombre de reserves anteriors no cancel·lades per a aquest hoste
reserved_room_type factor Tipus d’habitació reservada
assigned_room_type factor Tipus d’habitació assignada
booking_changes integer Nombre de canvis en la reserva
deposit_type factor Tipus de dipòsit realitzat. Hi ha 3 categories: «No Deposit»: sense dipòsit, «Non Refund»: no reemborsable, «Refundable»: reemborsable
agent factor Identificador de l’agent de viatges
company factor Identificador de l’empresa
days_in_waiting_list integer Nombre de dies a la llista d’espera
customer_type factor Tipus de client. Tipus de reserva, assumint una de les quatre categories següents: Contract - quan la reserva té una assignació o un altre tipus de contracte associat; Group – quan la reserva està associada a un grup; Transient – quan la reserva no forma part d’un grup o contracte, i no està associada a altres reserves transitòries; Transient-party – quan la reserva és transitòria, però està associada almenys a una altra reserva transitòria
adr numeric Tarifa diària mitjana
required_car_parking_spaces integer Nombre d’espais d’aparcament de cotxes requerits
total_of_special_requests integer Nombre total de sol·licituds especials (per exemple, llit individual o pis alt)
reservation_status factor Estat final de la reserva. «Canceled»: el client ha cancel·lat la reserva; «Check-Out»: el client ha fet el check-in però ja ha marxat; «No-Show»: el client no ha fet el check-in i ha informat l’hotel del motiu.
reservation_status_date date Data de l’estat final de la reserva

0.3 Càrrega de dades i dimensions (N x M)

Llegim el conjunt de dades en format CSV, amb 119.390 files i 32 columnes:

x <- read.csv("data/hotel_bookings.csv", stringsAsFactors = TRUE)
dim(x)
## [1] 119390     32

El PMS va assegurar que no falten dades a les taules de la seva base de dades. Tanmateix, en algunes variables categòriques com ara agent o company, el valor “NULL” es presenta com una de les categories. Això no s’ha de considerar com que falta el valor, sinó com a “no aplicable”. Per exemple, si un agent de reserva agent es defineix com a “NULL”, significa que la reserva no prové d’una agència de viatges.

# converteix el tipus de dades de les columnes segons la seva naturalesa
x <- x |>
    mutate(
        # dates
        reservation_status_date = as.Date(reservation_status_date),
        # factors
        hotel = as.factor(hotel),
        agent = as.factor(agent),
        arrival_date_month = factor(arrival_date_month, levels = month.name),
        assigned_room_type = as.factor(assigned_room_type),
        company = as.factor(company),
        country = as.factor(country),
        customer_type = as.factor(customer_type),
        deposit_type = factor(
            deposit_type,
            levels = c("No Deposit", "Refundable", "Non Refund")
        ),
        distribution_channel = as.factor(distribution_channel),
        is_canceled = as.factor(is_canceled),
        is_repeated_guest = as.factor(is_repeated_guest),
        market_segment = as.factor(market_segment),
        meal = factor(
            meal,
            levels = c("Undefined", "SC", "BB", "HB", "FB"), ordered = TRUE
        ),
        reservation_status = factor(
            reservation_status,
            levels = c("No-Show", "Check-Out", "Canceled"), ordered = TRUE
        ),
        reserved_room_type = as.factor(reserved_room_type),
        # numerics
        adr = as.numeric(adr),
        # integers
        adults = as.integer(adults),
        babies = as.integer(babies),
        children = as.integer(children)
    )

0.4 Neteja de dades

Primer, inspeccionarem les dades amb la funció summary() inclosa a R.
Podeu trobar una explicació de cada variable a l’article que descriu aquest conjunt de dades en detall, tot i que els noms de les variables són força explicatives:

summary(x)
##           hotel       is_canceled   lead_time   arrival_date_year
##  City Hotel  :79330   0:75166     Min.   :  0   Min.   :2015     
##  Resort Hotel:40060   1:44224     1st Qu.: 18   1st Qu.:2016     
##                                   Median : 69   Median :2016     
##                                   Mean   :104   Mean   :2016     
##                                   3rd Qu.:160   3rd Qu.:2017     
##                                   Max.   :737   Max.   :2017     
##                                                                  
##  arrival_date_month arrival_date_week_number arrival_date_day_of_month
##  August :13877      Min.   : 1.00            Min.   : 1.0             
##  July   :12661      1st Qu.:16.00            1st Qu.: 8.0             
##  May    :11791      Median :28.00            Median :16.0             
##  October:11160      Mean   :27.17            Mean   :15.8             
##  April  :11089      3rd Qu.:38.00            3rd Qu.:23.0             
##  June   :10939      Max.   :53.00            Max.   :31.0             
##  (Other):47873                                                        
##  stays_in_weekend_nights stays_in_week_nights     adults      
##  Min.   : 0.0000         Min.   : 0.0         Min.   : 0.000  
##  1st Qu.: 0.0000         1st Qu.: 1.0         1st Qu.: 2.000  
##  Median : 1.0000         Median : 2.0         Median : 2.000  
##  Mean   : 0.9276         Mean   : 2.5         Mean   : 1.856  
##  3rd Qu.: 2.0000         3rd Qu.: 3.0         3rd Qu.: 2.000  
##  Max.   :19.0000         Max.   :50.0         Max.   :55.000  
##                                                               
##     children           babies                 meal          country     
##  Min.   : 0.0000   Min.   : 0.000000   Undefined: 1169   PRT    :48590  
##  1st Qu.: 0.0000   1st Qu.: 0.000000   SC       :10650   GBR    :12129  
##  Median : 0.0000   Median : 0.000000   BB       :92310   FRA    :10415  
##  Mean   : 0.1039   Mean   : 0.007949   HB       :14463   ESP    : 8568  
##  3rd Qu.: 0.0000   3rd Qu.: 0.000000   FB       :  798   DEU    : 7287  
##  Max.   :10.0000   Max.   :10.000000                     ITA    : 3766  
##  NA's   :4                                               (Other):28635  
##        market_segment  distribution_channel is_repeated_guest
##  Online TA    :56477   Corporate: 6677      0:115580         
##  Offline TA/TO:24219   Direct   :14645      1:  3810         
##  Groups       :19811   GDS      :  193                       
##  Direct       :12606   TA/TO    :97870                       
##  Corporate    : 5295   Undefined:    5                       
##  Complementary:  743                                         
##  (Other)      :  239                                         
##  previous_cancellations previous_bookings_not_canceled reserved_room_type
##  Min.   : 0.00000       Min.   : 0.0000                A      :85994     
##  1st Qu.: 0.00000       1st Qu.: 0.0000                D      :19201     
##  Median : 0.00000       Median : 0.0000                E      : 6535     
##  Mean   : 0.08712       Mean   : 0.1371                F      : 2897     
##  3rd Qu.: 0.00000       3rd Qu.: 0.0000                G      : 2094     
##  Max.   :26.00000       Max.   :72.0000                B      : 1118     
##                                                        (Other): 1551     
##  assigned_room_type booking_changes       deposit_type        agent      
##  A      :74053      Min.   : 0.0000   No Deposit:104641   9      :31961  
##  D      :25322      1st Qu.: 0.0000   Refundable:   162   NULL   :16340  
##  E      : 7806      Median : 0.0000   Non Refund: 14587   240    :13922  
##  F      : 3751      Mean   : 0.2211                       1      : 7191  
##  G      : 2553      3rd Qu.: 0.0000                       14     : 3640  
##  C      : 2375      Max.   :21.0000                       7      : 3539  
##  (Other): 3530                                            (Other):42797  
##     company       days_in_waiting_list         customer_type  
##  NULL   :112593   Min.   :  0.000      Contract       : 4076  
##  40     :   927   1st Qu.:  0.000      Group          :  577  
##  223    :   784   Median :  0.000      Transient      :89613  
##  67     :   267   Mean   :  2.321      Transient-Party:25124  
##  45     :   250   3rd Qu.:  0.000                             
##  153    :   215   Max.   :391.000                             
##  (Other):  4354                                               
##       adr          required_car_parking_spaces total_of_special_requests
##  Min.   :  -6.38   Min.   :0.00000             Min.   :0.0000           
##  1st Qu.:  69.29   1st Qu.:0.00000             1st Qu.:0.0000           
##  Median :  94.58   Median :0.00000             Median :0.0000           
##  Mean   : 101.83   Mean   :0.06252             Mean   :0.5714           
##  3rd Qu.: 126.00   3rd Qu.:0.00000             3rd Qu.:1.0000           
##  Max.   :5400.00   Max.   :8.00000             Max.   :5.0000           
##                                                                         
##  reservation_status reservation_status_date
##  No-Show  : 1207    Min.   :2014-10-17     
##  Check-Out:75166    1st Qu.:2016-02-01     
##  Canceled :43017    Median :2016-08-07     
##                     Mean   :2016-07-30     
##                     3rd Qu.:2017-02-08     
##                     Max.   :2017-09-14     
## 

Nombre de files repetides:

nrow(x[duplicated(x), ])
## [1] 31994

No s’eliminen les files repetides, ja que no es disposa de suficient informació per saber si es tracta d’un error o si realment són reserves diferents amb les mateixes files, p. ex. amb el número d’habitació assignada.

# x <- x[!duplicated(x), ]

1 Variables numèriques

S’observen alguns valors inesperats (outliers?) en diverses variables. Per exemple:

  1. Un màxim de 55 en adults
  2. Un màxim de 10 en children (incloent valors perduts)
  3. Un màxim de 10 en babies
  4. Valors negatius en la tarifa diària mitjana (adr) o molt elevats

Visualitzem l’histograma de la variable adults, amb almenys 55 intervals:

Es pot observar que l’histograma no mostra barres al voltant del valor 55, ja que es tracta d’un conjunt molt gran i probablement només és un o pocs casos. En aquests casos, per analitzar els valors extrems d’una variable, els valors de la variable en qüestió es poden representar gràficament de la següent manera, ordenant i representant les dades (si són numèriques, com en aquest cas):

plot(sort(x$adults),
    main = "Gràfic d'adults ordenats",
    ylab = "Nombre d'adults"
)
grid()

# Desa el gràfic com a fitxer SVG
# svg(filename = "img/plot_adults.svg", width = 8, height = 6)
# plot(sort(x$adults))
# grid()
# dev.off()

L’índex representa la posició de l’element un cop ordenat, però ens interessa més l’eix Y, ja que podem veure que alguns elements tenen valors de 10 o més. Com que aquesta és una variable entera amb un conjunt limitat de valors possibles, podem utilitzar table() per visualitzar-los:

table(x$adults)
## 
##     0     1     2     3     4     5     6    10    20    26    27    40    50 
##   403 23027 89680  6202    62     2     1     1     2     5     2     1     1 
##    55 
##     1

Com es pot veure, hi ha una reserva per a 10 adults, dues per a 20 adults, i així successivament, fins a una per a 55 adults! Sense entrar en més detalls, eliminarem totes les files amb reserves per a 10 o més adults:

x <- x[x$adults < 10, ]

Exercici

Repetiu aquest procés amb les variables children i babies. Proveu també de canviar el llindar a menys de 5 en lloc de 10.

x <- x[x$children < 5, ]

x <- x[x$babies < 5, ]

Fi de l’exercici

L’histograma de la variable adr (tarifa diària mitjana) presenta el mateix problema que la variable adults, així que simplement crearem un gràfic amb els valors ordenats de nou:

En aquest cas, observem que només un valor és significativament més alt que la resta. El considerem un valor atípic i l’eliminem, així com els valors negatius que no tenen una explicació clara, tot i que mantenim els valors 0:

# Elimina els valors extrems que estiguin fora de l'interval [0, 1000)
x <- x[x$adr >= 0 & x$adr < 1000, ]
# Elimina els valors NA
x <- x[!is.na(x$adr), ]
# Mostra un resum de la variable adr
summary(x$adr)
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##    0.00   69.29   94.62  101.80  126.00  510.00

L’histograma ara ens proporciona informació rellevant. El dibuixem utilitzant el paquet ggplot2, que ofereix moltes més opcions que hist():

Exercici:

Milloreu el gràfic per fer que els eixos, el títol, etc. siguin més adequats.

Fi de l’exercici

Podem veure que hi ha un conjunt d’aproximadament 2.000 valors zero, que es podrien analitzar per separat, per exemple. Hi ha paquets R que ens ajuden a estimar aquesta distribució i els paràmetres que la determinen visualment, com el paquet fitdistrplus, que proporciona la funció descdist() (atenció, lent!):

## summary statistics
## ------
## min:  0   max:  510 
## median:  94.62 
## mean:  101.8011 
## estimated sd:  48.14287 
## estimated skewness:  1.018971 
## estimated kurtosis:  5.13332

Com es pot veure, les dades reals (observacions, un punt de color) i les dades simulades (en un altre color) s’aproximen al que podria semblar una distribució lognormal. No obstant això, per experimentar amb el conjunt de dades més net possible, farem:

  1. eliminar les estades de 0 dies
  2. eliminar les estades de cost 0
  3. eliminar les estades sense hostes
  4. substituir els NA en la variable children per 0
x[is.na(x$children), "children"] <- 0
x <- x[x$adr > 0 &
    (x$stays_in_week_nights + x$stays_in_weekend_nights) > 0 &
    (x$adults + x$children + x$babies) > 0 &
    !is.na(x$children), ]

1.1 Variables categòriques

Per a les variables categòriques, la funció summary() ens dóna una primera idea dels valors possibles que pot prendre cada una. Per exemple, en el conjunt original (abans d’eliminar valors extrems), hi ha 79.330 reserves en un hotel de ciutat (Lisboa) i 40.060 en un resort (Algarve). Podem preguntar-nos si la distribució dels costos és la mateixa per a ambdós grups, ja sigui utilitzant la prova estadística adequada o simplement comparant histogrames, en aquest cas utilitzant el paquet ggplot2, que és molt més potent per crear tot tipus de gràfics:

Es pot veure que els preus més comuns a Lisboa (hotels de ciutat) estan lleugerament a la dreta dels preus més comuns a l’Algarve (hotels de resort), tot i que els preus més alts a Lisboa disminueixen més ràpidament que a l’Algarve. Utilitzant un gràfic de violí, podem veure més detalls, especialment si també mostrem els típics quartils d’un diagrama de caixa:

Hi ha un paquet R anomenat ggstatsplot que té funcions específiques per a cada tipus de gràfic, incloent proves estadístiques adequades per determinar si hi ha diferències entre grups:

Una altra variable interessant és l’origen dels hostes de l’hotel (country). El problema és que aquesta variable té molts valors diferents (178), així que hauríem de centrar-nos en els països amb més turistes, mostrant també si trien un hotel de ciutat o un resort:

Obviament, Portugal (PRT) ocupa el primer lloc, seguida dels països veïns com Gran Bretanya, França i Espanya. Els visitants de Gran Bretanya i Irlanda són més propensos a triar un resort, mentre que els de França, Alemanya i Itàlia visiten principalment Lisboa.

Exercici:

Hi ha diferències entre els residents de Portugal i la resta?

Fi de l’exercici

Una altra variable interessant és is_canceled, que indica si una reserva va ser cancel·lada o no (el 37,0% de les vegades). Podem observar la relació entre dues variables categòriques utilitzant un gràfic mosaic:

Es pot veure que la taxa de cancel·lació (denotada per 1 a l’eix Y) en un resort és inferior a la d’un hotel a Lisboa. A l’eix X, la mida relativa de cada columna també correspon a la proporció de cada tipus d’hotel. És important no considerar les etiquetes de l’eix Y (0/1) com la taxa numèrica real de cancel·lació, ja que això pot ser enganyós.

Exercici:

Quin altre tipus de gràfic es podria utilitzar per representar aquestes dades?

Canviar el gràfic mosaic per un gràfic de barres apilades o un gràfic de sectors.

## `summarise()` has grouped output by 'hotel'. You can override using the
## `.groups` argument.

Fi de l’exercici

En el cas de la cancel·lació per país per als països amb més turistes:

Es pot veure que la taxa de cancel·lació és molt més alta per als turistes locals (de Portugal, PRT), mentre que és molt més baixa per a la resta de països. No obstant això, aquest gràfic no és fàcil de llegir; en aquest cas, no hi ha ordre ni dels països ni del percentatge de cancel·lacions.

Exercici

Millora el gràfic anterior per fer-lo més comprensible i considera si és possible visualitzar les relacions entre tres o més variables categòriques.

# Almenys 1000 reserves
xx <- x |>
    group_by(country) |>
    mutate(pais = n()) |>
    filter(pais >= 1000)
ggplot(data = xx, aes(
    x = reorder(country, -as.numeric(is_canceled)),
    fill = is_canceled
)) +
    geom_bar(position = "fill") +
    scale_y_continuous(labels = scales::percent) +
    scale_fill_manual(
        name = "Cancellation Status",
        labels = c("Not Canceled", "Canceled"),
        values = c("0" = "#00BFC4", "1" = "#F8766D")
    ) +
    labs(
        title = "Proportion of Cancellations by Country",
        x = "Country",
        y = "Proportion of Cancellations",
        fill = "Cancellation Status",
    ) +
    # mostra els percentatges en cada barra amb 2 decimals
    geom_text(
        aes(label = scales::percent(after_stat(count) / sum(after_stat(count)),
            accuracy = 0.01, decimal.mark = ","
        )),
        stat = "count",
        # Centra els percentatges verticalment
        position = position_fill(vjust = 0.5),
        # Gira 90 graus els percentatges per adaptar-los a les barres estretes
        angle = 90,
        # Mida de lletra petita
        size = 3,
        # Color del text blanc
        color = "white"
    ) +
    theme_minimal() +
    theme(
        axis.text.x = element_text(angle = 90, vjust = 0.5, hjust = 1),
        plot.title = element_text(hjust = 0.5)
    )

# Desa el gràfic com a fitxer SVG
# ggsave(
#     "img/exercici_bar_country_is_canceled.svg",
#     plot = last_plot(), width = 8, height = 6
# )
## Warning: The dot-dot notation (`..count..`) was deprecated in ggplot2 3.4.0.
## ℹ Please use `after_stat(count)` instead.
## This warning is displayed once every 8 hours.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.

Fi de l’exercici

Finalment, analitzem el comportament de les reserves en relació amb la data d’arribada. Primer, utilitzant el paquet R lubridate (una meravella per manipular dades de data i hora), crearem una variable dia per determinar el dia de la setmana en què es va fer el check-in a l’hotel i analitzarem quantes reserves hi va haver cada dia:

x$dia <- as_date(paste0(
    x$arrival_date_year, "-",
    x$arrival_date_month, "-", x$arrival_date_day_of_month
))

Exercici:

Millora i divideix el gràfic anterior per tipus d’hotel o país d’origen.

Fi de l’exercici

Com s’ha descrit a l’article, les dades cobreixen el període de l’1 de juliol de 2015 al 31 d’agost de 2017. Es poden observar alguns pics que podrien ser interessants d’explicar (què va passar aquells dies, per exemple 2015-12-05?). Pots consultar Google Trends per obtenir algunes idees:

https://trends.google.es/trends/explore?date=2015-01-01%202017-12-31&q=lisboa,algarve&hl=es

max(table(x$dia))
## [1] 439
which.max(table(x$dia))
## 2015-12-05 
##        158

Amb el dia calculat, juntament amb les variables stays_in_week i weekend_nights, podem intentar categoritzar manualment el tipus de viatge segons els següents criteris (això és arbitrari, clarament millorables):

  1. si stays_in_weekend_nights és zero => viatge de feina
  2. si stays_in_week_nights és zero o un i en aquest cas l’entrada és en divendres => cap de setmana
  3. si stays_in_week_nights és cinc i stays_in_weekend_nights és tres (és a dir, de dissabte o diumenge a dissabte o diumenge) => paquet de vacances setmanal
  4. si stays_in_weekend_nights és un o dos i stays_in_week_nights és cinc o menys => feina + descans
  5. la resta de combinacions => vacances
x$tipo <- ifelse(x$stays_in_weekend_nights == 0, "work",
    ifelse(x$stays_in_week_nights == 0, "weekend",
        ifelse(x$stays_in_week_nights == 1 & wday(x$dia) == 6, "weekend",
            ifelse(x$stays_in_week_nights == 5 &
                (x$stays_in_weekend_nights == 3 |
                    x$stays_in_weekend_nights == 4), "package",
            ifelse(x$stays_in_week_nights <= 5 &
                x$stays_in_weekend_nights < 3, "work+rest",
            "rest"
            )
            )
        )
    )
)

Una manera de refinar aquesta classificació seria mirar el nombre d’adults, nens i nadons per decidir si es tracta d’un viatger de negocis o d’una família. Les possibilitats són infinites: es pot enriquir el conjunt de dades amb dades geogràfiques (distància entre països), dades demogràfiques, dades econòmiques (renda per càpita), dades meteorològiques (tant a Portugal com al país d’origen), etc.

Exercici

Has d’explorar aquest conjunt de dades enriquit i, en aquest procés d’exploració, decidir quina història vols explicar sobre ell. Algunes idees:

  1. els turistes de diferents països viatgen en diferents dates?
  2. diferències en les cancel·lacions entre grups (països, tipus d’estada, …)
  3. relació entre el tipus d’estada tipo i el cost adr
  4. diferències entre grups respecte al tipus d’hotel (ciutat / resort)

NOTA: Aquest és un bon exemple d’ús de ChatGPT o altres IA generatives per fer preguntes interessants sobre el conjunt de dades proposat. El següent article descriu els usos potencials de la IA generativa en les diferents fases de la creació d’una visualització de dades per a la narració:

https://ieeexplore.ieee.org/stamp/stamp.jsp?arnumber=10891192

# Filtrar països amb més de 1000 reserves
xx <- x |>
    group_by(country) |>
    mutate(n_bookings = n()) |>
    filter(n_bookings > 1000) |>
    ungroup()

Patrons de cancel·lació

# nombre de files amb reservation_status = 'Check-Out' i is_canceled = 1
nrow(x[x$reservation_status == "Check-Out" & x$is_canceled == 1, ])
## [1] 0
# nombre de files amb reservation_status = 'Canceled' i is_canceled = 0
nrow(x[x$reservation_status == "Canceled" & x$is_canceled == 0, ])
## [1] 0
# nombre de files amb reservation_status = 'Canceled' i is_canceled = 0
nrow(x[x$reservation_status == "No-Show" & x$is_canceled == 0, ])
## [1] 0
# diagrama de sectors de reservation_status
reservation_status_summary <- xx |>
    group_by(hotel, reservation_status) |>
    summarise(n = n(), .groups = "drop") |>
    group_by(hotel) |>
    tidyr::complete(reservation_status = c("Check-Out", "Canceled", "No-Show"), fill = list(n = 0)) |>
    mutate(prop = n / sum(n))
ggplot(reservation_status_summary, aes(x = factor(1), y = prop, fill = reservation_status)) +
    coord_polar(theta = "y", start = 0) +
    facet_wrap(~hotel, strip.position = "bottom") +
    scale_y_continuous(limits = c(0, 1), labels = scales::percent) +
    scale_fill_manual(
        name = "Estat",
        labels = c("Cancel·lada", "Checkout", "No-Show"),
        values = c("Canceled" = "#e74c3c", "Check-Out" = "#2ecc71", "No-Show" = "#f1c40f")
    ) +
    geom_text(aes(label = percent(prop, accuracy = 0.1)), position = position_stack(vjust = 0.5), size = 5, fontface = "bold", color = "white") +
    labs(title = "Percentatge d'estat de la reserva per tipus d'hotel", x = NULL, y = NULL) +
    geom_col(width = 1) +
    theme_void() +
    theme(plot.title = element_text(hjust = 0.5))

# rm(reservation_status_summary)
# diagrama de sectors de is_canceled
cancellation_summary <- xx |>
    group_by(hotel, is_canceled) |>
    summarise(n = n(), .groups = "drop") |>
    group_by(hotel) |>
    tidyr::complete(is_canceled = c("0", "1"), fill = list(n = 0)) |>
    mutate(pct = n / sum(n))
ggplot(cancellation_summary, aes(x = factor(1), y = pct, fill = is_canceled)) +
    coord_polar(theta = "y", start = 0) +
    facet_wrap(~hotel, strip.position = "bottom") +
    scale_y_continuous(limits = c(0, 1), labels = scales::percent) +
    scale_fill_manual(
        name = "Estat de la cancel·lació",
        labels = c("No cancel·lada", "Cancel·lada"),
        values = c("0" = "#2ecc71", "1" = "#e74c3c")
    ) +
    geom_text(aes(label = percent(pct, accuracy = 0.1)), position = position_stack(vjust = 0.5), size = 5, fontface = "bold", color = "white") +
    labs(title = "Percentatge de cancel·lacions per tipus d'hotel", x = NULL, y = NULL) +
    geom_col(width = 1) +
    theme_void() +
    theme(plot.title = element_text(hjust = 0.5))

# rm(cancellation_summary)
# Percentatge de cancel·lacions entre països amb més de 1000 reserves i Portugal
xx_is_portugal <- xx |>
    mutate(is_portugal = ifelse(country == "PRT", "Portugal", "Altres")) |>
    group_by(is_portugal, is_canceled) |>
    summarise(n = n(), .groups = "drop") |>
    group_by(is_portugal) |>
    mutate(pct = n / sum(n))
ggplot(xx_is_portugal, aes(x = factor(1), y = pct, fill = is_canceled)) +
    geom_col(width = 1) +
    coord_polar(theta = "y") +
    facet_wrap(~is_portugal, strip.position = "bottom") +
    scale_y_continuous(labels = scales::percent) +
    scale_fill_manual(name = "Estat", labels = c("No Cancel·lada", "Cancel·lada"), values = c("0" = "#2ecc71", "1" = "#e74c3c")) +
    geom_text(aes(label = percent(pct, accuracy = 0.1)), position = position_stack(vjust = 0.5), size = 5, fontface = "bold", color = "white") +
    labs(title = "Percentatge de cancel·lacions per país d'origen", x = NULL, y = NULL) +
    theme_void() +
    theme(plot.title = element_text(hjust = 0.5))

rm(xx_is_portugal)

xx_is_portugal_by_hotel <- xx |>
    mutate(is_portugal = ifelse(country == "PRT", "Portugal", "Altres")) |>
    group_by(is_portugal, hotel, is_canceled) |>
    summarise(n = n(), .groups = "drop") |>
    group_by(is_portugal) |>
    mutate(pct = n / sum(n))

ggplot(
    data = xx_is_portugal_by_hotel,
    aes(
        x = is_portugal,
        y = n,
        fill = interaction(is_canceled, hotel, lex.order = TRUE)
    )
) +
    geom_col(position = "fill") +
    scale_y_continuous(labels = scales::percent) +
    scale_fill_discrete(
        name = "Estat de la cancel·lació i tipus d'hotel",
        labels = c(
            "No Cancel·lada - Hotel de Ciutat", "No Cancel·lada - Hotel de Resort",
            "Cancel·lada - Hotel de Ciutat", "Cancel·lada - Hotel de Resort"
        )
    ) +
    geom_text(
        aes(label = scales::percent(pct, accuracy = 0.1)),
        position = position_fill(vjust = 0.5),
        size = 5, fontface = "bold", color = "white"
    ) +
    labs(
        title = "Percentatge de cancel·lacions per país d'origen i tipus d'hotel",
        x = "País d'origen",
        y = "Percentatge de cancel·lacions"
    ) +
    theme_minimal() +
    theme(
        plot.title = element_text(hjust = 0.5),
        axis.text.x = element_text(size = 12),
        axis.text.y = element_text(size = 11)
    )

rm(xx_is_portugal_by_hotel)
# Diagrama de sectors amb el percentatge de cancel·lacios per tipus de dipòsit
deposit_type_summary <- xx |>
    group_by(deposit_type, is_canceled) |>
    summarise(n = n(), .groups = "drop") |>
    group_by(deposit_type) |>
    tidyr::complete(is_canceled = c("0", "1"), fill = list(n = 0)) |>
    mutate(prop = n / sum(n))
ggplot(deposit_type_summary, aes(x = factor(1), y = prop, fill = is_canceled)) +
    coord_polar(theta = "y", start = 0) +
    facet_wrap(~deposit_type, strip.position = "bottom") +
    scale_y_continuous(limits = c(0, 1), labels = scales::percent) +
    scale_fill_manual(
        name = "Estat de la cancel·lació",
        labels = c("No cancel·lada", "Cancel·lada"),
        values = c("0" = "#2ecc71", "1" = "#e74c3c")
    ) +
    geom_text(aes(label = percent(prop, accuracy = 0.1)), position = position_stack(vjust = 0.5), size = 5, fontface = "bold", color = "white") +
    labs(title = "Percentatge de cancel·lacions segons el tipus de dipòsit", x = NULL, y = NULL) +
    geom_col(width = 1) +
    theme_void() +
    theme(plot.title = element_text(hjust = 0.5))

rm(deposit_type_summary)
# Diagrama de sectors amb el percentatge de cancel·lacios per tipus de dipòsit
deposit_type_summary <- xx |>
    group_by(hotel, deposit_type, is_canceled) |>
    summarise(n = n(), .groups = "drop") |>
    group_by(hotel, deposit_type) |>
    tidyr::complete(is_canceled = c("0", "1"), fill = list(n = 0)) |>
    mutate(prop = n / sum(n))
ggplot(deposit_type_summary, aes(x = factor(1), y = prop, fill = is_canceled)) +
    coord_polar(theta = "y", start = 0) +
    facet_grid(deposit_type ~ hotel, switch = "y") +
    scale_y_continuous(limits = c(0, 1), labels = scales::percent) +
    scale_fill_manual(
        name = "Estat de la cancel·lació",
        labels = c("No cancel·lada", "Cancel·lada"),
        values = c("0" = "#2ecc71", "1" = "#e74c3c")
    ) +
    geom_text(aes(label = percent(prop, accuracy = 0.1)), position = position_stack(vjust = 0.5), size = 5, fontface = "bold", color = "white") +
    labs(title = "Percentatge de cancel·lacions segons el tipus de dipòsit i tipus d'hotel", x = NULL, y = NULL) +
    geom_col(width = 1) +
    theme_void() +
    theme(plot.title = element_text(hjust = 0.5))

rm(deposit_type_summary)
# Tenen reemborsament les reserves cancel·lades?
ggplot(xx, aes(
    x = reorder(deposit_type, deposit_type, function(x) length(x),
        decreasing = TRUE
    ),
    fill = is_canceled
)) +
    geom_bar(position = "fill") +
    scale_y_continuous(labels = scales::percent) +
    scale_fill_manual(
        name = "Estat de cancel·lació",
        labels = c("No cancel·lat", "Cancel·lat"),
        values = c("0" = "#2ecc71", "1" = "#e74c3c")
    ) +
    facet_wrap(~hotel) +
    labs(
        title = "Percentatge de cancel·lacions segons el tipus de dipòsit",
        x = "Tipus de dipòsit",
        y = "Percentatge de cancel·lacions"
    ) +
    theme_minimal() +
    theme(
        plot.title = element_text(hjust = 0.5),
        axis.text.x = element_text(angle = 90, vjust = 0.5, hjust = 1)
    )

# Nombre de reserves i cancel·lacions per tipus d'hotel
ggplot(xx, aes(x = hotel, fill = is_canceled)) +
    geom_bar(position = "dodge") +
    scale_fill_manual(
        name = "Llegenda",
        labels = c("Reserves", "Cancel·lacions"),
        values = c("0" = "#2ecc71", "1" = "#e74c3c")
    ) +
    labs(
        title = "Nombre de reserves i cancel·lacions per tipus d'hotel",
        x = "Tipus d'hotel",
        y = "Nombre",
    ) +
    theme_minimal() +
    theme(
        legend.title = element_blank(),
        plot.title = element_text(hjust = 0.5)
    )

# El percentatge de cancel·lacions és diferent segons el tipus d'hotel?
ggplot(xx, aes(x = hotel, fill = is_canceled)) +
    geom_bar(position = "fill") +
    scale_fill_manual(
        name = "Estat",
        labels = c("No Cancel·lada", "Cancel·lada"),
        values = c("0" = "#2ecc71", "1" = "#e74c3c")
    ) +
    scale_y_continuous(labels = scales::percent) +
    geom_text(
        aes(label = scales::percent(..count.. / sum(..count..),
            accuracy = 0.1
        )),
        stat = "count",
        position = position_fill(vjust = 0.5),
        size = 5, fontface = "bold", color = "white"
    ) +
    labs(
        title = "Percentatge de cancel·lació per tipus d'hotel",
        x = "Tipus d'hotel",
        y = "Proporció"
    ) +
    theme_minimal() +
    theme(
        plot.title = element_text(hjust = 0.5, size = 14),
        legend.position = "bottom",
        axis.text.x = element_text(size = 12),
        axis.text.y = element_text(size = 11)
    )

# percentatge de cancel·lacions per mes d'arribada
ggplot(xx, aes(
    x = reorder(arrival_date_month, as.numeric(arrival_date_month)),
    fill = is_canceled
)) +
    geom_bar(position = "fill") +
    scale_fill_manual(
        name = "Estat",
        labels = c("No Cancel·lada", "Cancel·lada"),
        values = c("0" = "#2ecc71", "1" = "#e74c3c")
    ) +
    scale_y_continuous(labels = scales::percent) +
    facet_wrap(~hotel, strip.position = "bottom") +
    labs(
        title = "Percentatge de cancel·lacions per mes d'arribada",
        x = "Mes d'arribada",
        y = "Percentatge de cancel·lacions"
    ) +
    theme_minimal() +
    theme(
        plot.title = element_text(hjust = 0.5),
        axis.text.x = element_text(angle = 90, vjust = 0.5, hjust = 1)
    )

# diagrama de sectors del tipus de dipòsit enfrontant el dos tipus d'hotel
deposit_type_summary <- xx |>
    group_by(hotel, deposit_type) |>
    summarise(n = n(), .groups = "drop") |>
    group_by(hotel) |>
    tidyr::complete(deposit_type = unique(xx$deposit_type), fill = list(n = 0)) |>
    mutate(prop = n / sum(n))
ggplot(deposit_type_summary, aes(x = factor(1), y = prop, fill = deposit_type)) +
    coord_polar(theta = "y", start = 0) +
    facet_wrap(~hotel, strip.position = "bottom") +
    scale_y_continuous(limits = c(0, 1), labels = scales::percent) +
    scale_fill_manual(
        name = "Tipus de dipòsit",
        values = c("No Deposit" = "#3498db", "Non Refund" = "#9b59b6", "Refundable" = "#f1c40f")
    ) +
    labs(title = "Percentatge de tipus de dipòsit per tipus d'hotel", x = NULL, y = NULL) +
    geom_col(width = 1) +
    theme_void() +
    theme(plot.title = element_text(hjust = 0.5))

rm(deposit_type_summary)

Es filtren els clients del 2016 amb status de reserva ‘Check-Out’. El valor de reservation_status = ‘Check-Out’ indica que el client es va presentar i va completar la seva estada a l’hotel.

# Nombre de clients amb Check-Out que repeteixen segons el mes d'arribada per
# a l'any 2016
ggplot(
    xx[xx$arrival_date_year == 2016 & xx$reservation_status == "Check-Out", ],
    aes(
        x = reorder(
            arrival_date_month,
            as.numeric(arrival_date_month)
        ),
        fill = arrival_date_month
    )
) +
    geom_bar(position = "dodge") +
    facet_wrap(~hotel, strip.position = "bottom") +
    labs(
        title = "Nombre de clients amb Check-Out que repeteixen (2016)",
        x = "Mes d'arribada",
        y = "Nombre de clients"
    ) +
    theme_minimal() +
    theme(
        plot.title = element_text(hjust = 0.5),
        axis.text.x = element_text(angle = 90, vjust = 0.5, hjust = 1)
    )

# Nombre de clients amb Check-Out que repeteixen i no repeteixen segons el mes
# d'arribada per a l'any 2016
ggplot(
    xx[xx$arrival_date_year == 2016 & xx$reservation_status == "Check-Out", ],
    aes(
        x = reorder(
            arrival_date_month,
            as.numeric(arrival_date_month)
        ),
        fill = as.factor(is_repeated_guest)
    )
) +
    geom_bar(position = "dodge") +
    scale_fill_manual(
        name = "Client que repeteix",
        labels = c("No", "Sí"),
        values = c("0" = "#3498db", "1" = "#9b59b6")
    ) +
    facet_wrap(~hotel, strip.position = "bottom") +
    labs(
        title = "Nombre de clients amb Check-Out que repeteixen i no (2016)",
        x = "Mes d'arribada",
        y = "Nombre de clients"
    ) +
    theme_minimal() +
    theme(
        plot.title = element_text(hjust = 0.5),
        axis.text.x = element_text(angle = 90, vjust = 0.5, hjust = 1)
    )

# Percentatge de clients amb Check-Out que repeteixen segons el mes d'arribada
# per a l'any 2016
ggplot(
    xx[xx$arrival_date_year == 2016 & xx$reservation_status == "Check-Out", ],
    aes(
        x = reorder(
            arrival_date_month,
            as.numeric(arrival_date_month)
        ),
        fill = as.factor(is_repeated_guest)
    )
) +
    geom_bar(position = "fill") +
    scale_fill_manual(
        name = "Client que repeteix",
        labels = c("No", "Sí"),
        values = c("0" = "#3498db", "1" = "#9b59b6")
    ) +
    scale_y_continuous(labels = scales::percent) +
    facet_wrap(~hotel, strip.position = "bottom") +
    labs(
        title = "Percentatge de clients amb Check-Out que repeteixen (2016)",
        x = "Mes d'arribada",
        y = "Percentatge de clients"
    ) +
    theme_minimal() +
    theme(
        plot.title = element_text(hjust = 0.5),
        axis.text.x = element_text(angle = 90, vjust = 0.5, hjust = 1)
    )

# Diagrames de violins del nombre de dies d'estada (x$stays_in_week_nights +
# x$stays_in_weekend_nights) per a cada mes d'arribada per a l'any 2016 per a
# reserves amb Check-Out
ggplot(
    xx[xx$arrival_date_year == 2016 & xx$reservation_status == "Check-Out", ],
    aes(
        x = reorder(
            arrival_date_month,
            as.numeric(arrival_date_month)
        ),
        y = stays_in_week_nights + stays_in_weekend_nights,
        fill = arrival_date_month
    )
) +
    geom_violin() +
    geom_boxplot(width = .1, outliers = FALSE) +
    facet_wrap(~hotel, strip.position = "bottom") +
    coord_flip() +
    labs(
        title = "Dies d'estada reserves amb Check-Out segons el mes d'arribada (2016)",
        x = "Mes d'arribada",
        y = "Dies d'estada"
    ) +
    theme_minimal() +
    theme(
        plot.title = element_text(hjust = 0.5),
        axis.text.x = element_text(angle = 90, vjust = 0.5, hjust = 1),
        legend.position = "none"
    )

# boxplot de les nits d'estada (x$stays_in_week_nights + x$stays_in_weekend_nights)
# per a cada tipus d'hotel per a reserves amb Check-Out l'any 2016
ggplot(
    xx[xx$reservation_status == "Check-Out" & xx$arrival_date_year == 2016, ],
    aes(x = hotel, y = stays_in_week_nights + stays_in_weekend_nights, fill = hotel)
) +
    geom_violin() +
    geom_boxplot(width = .1, outliers = FALSE) +
    coord_flip() +
    labs(
        title = "Dies d'estada reserves amb Check-Out segons el tipus d'hotel (2016)",
        x = "Tipus d'hotel",
        y = "Dies d'estada"
    ) +
    theme_minimal() +
    theme(plot.title = element_text(hjust = 0.5), legend.position = "none")

# Valors ordenats de stay_days amb reservation_status = 'Check-Out' per a l'any
# 2016 per a cada tipus d'hotel amb plot
ggplot(
    xx[xx$arrival_date_year == 2016 & xx$reservation_status == "Check-Out", ],
    aes(
        x = reorder(
            as.factor(1:nrow(xx[xx$arrival_date_year == 2016 &
                xx$reservation_status == "Check-Out", ])),
            stays_in_week_nights + stays_in_weekend_nights
        ),
        y = stays_in_week_nights + stays_in_weekend_nights,
        color = hotel
    )
) +
    geom_point() +
    labs(
        title = "Dies d'estada reserves amb Check-Out ordenats segons el tipus d'hotel (2016)",
        x = "Reserves ordenades",
        y = "Dies d'estada"
    ) +
    theme_minimal() +
    theme(
        plot.title = element_text(hjust = 0.5), legend.position = "bottom",
        axis.text.x = element_blank()
    )

Preferències de tipus d’hotel segons diferents variables.

# Percentatge de reserves per tipus d'hotel i país
ggplot(
    xx,
    aes(
        x = reorder(country, as.numeric(hotel), decreasing = FALSE),
        fill = hotel
    )
) +
    geom_bar(position = "fill") +
    scale_fill_manual(
        name = "Tipus d'hotel",
        labels = c("City Hotel", "Resort Hotel"),
        values = c("City Hotel" = "#00BFC4", "Resort Hotel" = "#F8766D")
    ) +
    scale_y_continuous(labels = scales::percent) +
    coord_flip() +
    labs(
        title = "Preferència de tipus d'hotel per país",
        x = "País",
        y = "Percentatge de reserves"
    ) +
    theme_minimal() +
    theme(
        axis.text.x = element_text(vjust = 0.5, hjust = 1),
        plot.title = element_text(hjust = 0.5)
    )

# El nombre de dies entre la reserva i l'arribada mig per tipus d'hotel és el
# mateix?
ggplot(
    xx[xx$arrival_date_year == 2016 & xx$reservation_status == "Check-Out", ],
    aes(x = hotel, y = lead_time, fill = hotel)
) +
    geom_violin() +
    geom_boxplot(width = .1, outliers = FALSE) +
    coord_flip() +
    labs(
        title = "Dies entre la reserva i l'arribada amb Check-Out (2016)",
        x = "Tipus d'hotel",
        y = "Dies entre la reserva i l'arribada"
    ) +
    theme_minimal() +
    theme(plot.title = element_text(hjust = 0.5), legend.position = "none")

# Canvia el nombre de dies mig entre la reserva i l'arribada segons el país
# d'origen?
ggplot(
    xx[xx$arrival_date_year == 2016 & xx$reservation_status == "Check-Out", ],
    aes(
        x = reorder(country, lead_time, FUN = median),
        y = lead_time, fill = hotel
    )
) +
    geom_violin() +
    geom_boxplot(width = .1, outliers = FALSE) +
    facet_wrap(~hotel, strip.position = "bottom") +
    coord_flip() +
    labs(
        title = "Dies entre la reserva i l'arribada amb Check-Out segons el país d'origen (2016)",
        x = "País d'origen",
        y = "Dies entre la reserva i l'arribada"
    ) +
    theme_minimal() +
    theme(
        plot.title = element_text(hjust = 0.5),
        legend.position = "none"
    )

# Canvia el nombre de dies mig entre la reserva i l'arribada segons
# l'estacionalitat?
ggplot(
    xx[xx$arrival_date_year == 2016 & xx$reservation_status == "Check-Out", ],
    aes(
        x = arrival_date_month,
        y = lead_time,
        group = arrival_date_month,
        fill = hotel
    )
) +
    geom_violin() +
    geom_boxplot(width = .1, outliers = FALSE) +
    facet_wrap(~hotel, strip.position = "bottom") +
    coord_flip() +
    labs(
        title = "Dies entre la reserva i l'arribada amb Check-Out segons el mes d'arribada (2016)",
        x = "Mes d'arribada",
        y = "Dies entre la reserva i l'arribada"
    ) +
    theme_minimal() +
    theme(
        plot.title = element_text(hjust = 0.5),
        axis.text.x = element_text(angle = 90, vjust = 0.5, hjust = 1),
        legend.position = "none"
    )

Preferències segons el tipus de client.

# Quins tipus de dipòsit trien els diferents països?
ggplot(xx, aes(
    x = reorder(country, deposit_type, FUN = function(x) {
        table(x)[which.max(table(x))]
    }),
    fill = deposit_type
)) +
    geom_bar(position = "fill") +
    scale_y_continuous(labels = scales::percent) +
    labs(
        title = "Tipus de dipòsit segons el país d'origen",
        x = "País",
        y = "Percentatge",
        fill = "Tipus de dipòsit"
    ) +
    theme_minimal() +
    theme(
        axis.text.x = element_text(angle = 90, vjust = 0.5, hjust = 1),
        plot.title = element_text(hjust = 0.5)
    )

# Quin tipus de client acostuma a fer més reserves?
ggplot(xx, aes(
    x = reorder(customer_type, customer_type, function(x) length(x),
        decreasing = TRUE
    ),
    fill = customer_type
)) +
    geom_bar() +
    facet_wrap(~hotel) +
    labs(
        title = "Nombre de reserves segons el tipus de client",
        x = "Tipus de client",
        y = "Nombre de reserves"
    ) +
    theme_minimal() +
    theme(
        plot.title = element_text(hjust = 0.5),
        axis.text.x = element_text(angle = 90, vjust = 0.5, hjust = 1),
        legend.position = "none"
    )

# Diagrama de sectors del percentatge de reserves per tipus de client
customer_type_summary <- xx |>
    group_by(hotel, customer_type) |>
    summarise(count = n()) |>
    mutate(percentage = count / sum(count) * 100)
## `summarise()` has grouped output by 'hotel'. You can override using the
## `.groups` argument.
ggplot(
    customer_type_summary, aes(x = "", y = percentage, fill = customer_type)
) +
    geom_bar(width = 1, stat = "identity") +
    coord_polar("y", start = 0) +
    facet_wrap(~hotel, strip.position = "bottom") +
    labs(
        title = "Percentatge de reserves per tipus de client",
        fill = "Tipus de client"
    ) +
    # Mostra el percentatge a cada sector
    geom_text(aes(label = paste0(round(percentage, 1), "%")),
        position = position_stack(vjust = 0.5)
    ) +
    theme_void() +
    theme(
        # Centra el títol
        plot.title = element_text(hjust = 0.5)
    )

rm(customer_type_summary)

# Quin tipus de de client té una taxa de cancel·lació més alta?
ggplot(xx, aes(
    x = reorder(customer_type, customer_type, function(x) length(x),
        decreasing = TRUE
    ),
    fill = is_canceled
)) +
    geom_bar(position = "fill") +
    scale_y_continuous(labels = scales::percent) +
    scale_fill_manual(
        name = "Estat de cancel·lació",
        labels = c("No cancel·lat", "Cancel·lat"),
        values = c("0" = "#2ecc71", "1" = "#e74c3c")
    ) +
    facet_wrap(~hotel) +
    labs(
        title = "Percentatge de cancel·lació segons el tipus de client",
        x = "Tipus de client",
        y = "Percentatge de cancel·lació"
    ) +
    theme_minimal() +
    theme(
        plot.title = element_text(hjust = 0.5),
        axis.text.x = element_text(angle = 90, vjust = 0.5, hjust = 1)
    )

# Quina és la despesa mitjana per tipus de client?
ggplot(
    xx |> filter(arrival_date_year == 2016),
    aes(
        x = reorder(customer_type, customer_type, function(x) length(x),
            decreasing = TRUE
        ),
        y = adr, fill = customer_type
    )
) +
    geom_violin() +
    geom_boxplot(width = .1, outliers = FALSE) +
    coord_flip() +
    facet_wrap(~hotel) +
    labs(
        title = "Tarifa diària mitjana (ADR) segons el tipus de client (2016)",
        x = "Tipus de client",
        y = "Tarifa diària mitjana (ADR)"
    ) +
    theme_minimal() +
    theme(
        plot.title = element_text(hjust = 0.5),
        legend.position = "none",
        axis.title.x = element_blank(),
        axis.title.y = element_blank()
    )

# El consum mig (ADR) varia en funció del mes d'arribada per a l'any 2016?
ggplot(
    xx |> filter(arrival_date_year == 2016),
    aes(
        x = arrival_date_month,
        y = adr,
        group = arrival_date_month,
        fill = hotel
    )
) +
    geom_violin() +
    geom_boxplot(width = .1, outliers = FALSE) +
    facet_wrap(~hotel, strip.position = "bottom") +
    coord_flip() +
    labs(
        title = "Tarifa diària mitjana (ADR) segons el mes d'arribada (2016)",
        x = "Mes d'arribada",
        y = "Tarifa diària mitjana (ADR)"
    ) +
    theme_minimal() +
    theme(
        plot.title = element_text(hjust = 0.5),
        legend.position = "none"
    )

# quin és el percentatge de reserves cancel·lades per tipus de visita amb
# dipòsit no reemborsable en funció del tipus d'hotel?
ggplot(xx, aes(
    x = tipo,
    fill = is_canceled
)) +
    geom_bar(position = "fill") +
    scale_y_continuous(labels = scales::percent) +
    scale_fill_manual(
        name = "Estat de cancel·lació",
        labels = c("No cancel·lat", "Cancel·lat"),
        values = c("0" = "#2ecc71", "1" = "#e74c3c")
    ) +
    facet_wrap(~deposit_type) +
    labs(
        title = "Percentatge de cancel·lació segons el tipus de visita i tipus de dipòsit",
        x = "Tipus de visita",
        y = "Percentatge de cancel·lació"
    ) +
    theme_minimal() +
    theme(
        plot.title = element_text(hjust = 0.5),
        axis.text.x = element_text(angle = 90, vjust = 0.5, hjust = 1)
    )

# diagrama amb el nombre de cancel·lacions segons el canal de distribució
ggplot(
    xx,
    aes(
        x = reorder(
            distribution_channel,
            distribution_channel,
            function(x) length(x),
            decreasing = TRUE
        ),
        fill = as.factor(is_canceled)
    )
) +
    geom_bar(position = "fill") +
    scale_y_continuous(labels = scales::percent) +
    scale_fill_manual(
        name = "Estat",
        labels = c("No Cancel·lada", "Cancel·lada"),
        values = c("0" = "#2ecc71", "1" = "#e74c3c")
    ) +
    labs(
        title = "Percentatge de cancel·lacions segons el canal de distribució",
        x = "Canal de distribució",
        y = "Percentatge de cancel·lacions"
    ) +
    theme_minimal() +
    theme(
        plot.title = element_text(hjust = 0.5),
        axis.text.x = element_text(angle = 90, vjust = 0.5, hjust = 1)
    )

# diagrama sectors segons el canal de distribució
distribution_channel_summary <- xx |>
    group_by(distribution_channel, is_canceled) |>
    summarise(n = n(), .groups = "drop") |>
    mutate(prop = n / sum(n))
ggplot(
    distribution_channel_summary,
    aes(
        x = factor(1), y = prop,
        fill = interaction(distribution_channel, is_canceled, lex.order = TRUE)
    )
) +
    scale_fill_discrete(
        name = "Canal de distribució i estat",
        labels = c(
            "TA/TO.0" = "TA - No Cancel·lada",
            "TA/TO.1" = "TA - Cancel·lada",
            "GDS.0" = "GDS - No Cancel·lada",
            "GDS.1" = "GDS - Cancel·lada",
            "Corporate.0" = "Corporate - No Cancel·lada",
            "Corporate.1" = "Corporate - Cancel·lada",
            "Direct.0" = "Direct - No Cancel·lada",
            "Direct.1" = "Direct - Cancel·lada"
        )
    ) +
    coord_polar(theta = "y", start = 0) +
    geom_col(width = 1) +
    labs(
        title = "Percentatge de reserves segons el canal de distribució",
        x = NULL, y = NULL, fill = "Canal de distribució"
    ) +
    theme_void() +
    theme(plot.title = element_text(hjust = 0.5))

# percentatge de reserves amb check-out segons el canal de distribució
ggplot(
    xx[xx$reservation_status == "Check-Out", ],
    aes(
        x = reorder(
            distribution_channel,
            distribution_channel,
            function(x) length(x),
            decreasing = TRUE
        ),
        fill = as.factor(is_repeated_guest)
    )
) +
    geom_bar(position = "fill") +
    scale_y_continuous(labels = scales::percent) +
    scale_fill_manual(
        name = "Client que repeteix",
        labels = c("No", "Sí"),
        values = c("0" = "#3498db", "1" = "#9b59b6")
    ) +
    labs(
        title = "Percentatge de fidelització efectiva segons el canal de distribució",
        x = "Canal de distribució",
        y = "Percentatge de clients que repeteixen"
    ) +
    theme_minimal() +
    theme(
        plot.title = element_text(hjust = 0.5),
        axis.text.x = element_text(angle = 90, vjust = 0.5, hjust = 1)
    )

rm(distribution_channel_summary)

# nombre de dies segon el canal de distribució
ggplot(
    xx,
    aes(
        x = reorder(
            distribution_channel,
            distribution_channel,
            function(x) length(x),
            decreasing = TRUE
        ),
        y = stays_in_week_nights + stays_in_weekend_nights,
        fill = distribution_channel
    )
) +
    geom_violin() +
    geom_boxplot(width = .1, outliers = FALSE) +
    coord_flip() +
    labs(
        title = "Nombre de dies d'estada segons el canal de distribució",
        x = "Canal de distribució",
        y = "Nombre de dies d'estada"
    ) +
    theme_minimal() +
    theme(
        plot.title = element_text(hjust = 0.5),
        legend.position = "none"
    )
## Warning: Groups with fewer than two datapoints have been dropped.
## ℹ Set `drop = FALSE` to consider such groups for position adjustment purposes.

# Calcula el nombre de dies d'estada per a cada reserva
x$stay_days <- x$stays_in_week_nights + x$stays_in_weekend_nights

# Diagrama de violí amb el nombre de dies d'estada segons el tipus d'hotel
ggplot(data = x, aes(x = hotel, y = stay_days, fill = hotel)) +
    geom_violin() +
    geom_boxplot(width = .1, outliers = FALSE) +
    coord_flip() +
    labs(
        title = "Nombre de dies d'estada segons el tipus d'hotel",
        x = "Tipus d'hotel",
        y = "Nombre de dies d'estada"
    ) +
    theme_minimal() +
    theme(
        plot.title = element_text(hjust = 0.5),
        legend.position = "none"
    )

# Histograma del nombre de dies d'estada per tipus d'hotel
ggplot(data = x, aes(x = stay_days, fill = hotel)) +
    geom_histogram(position = "dodge", binwidth = 1, color = "black") +
    labs(
        title = "Nombre de dies d'estada segons el tipus d'hotel",
        x = "Nombre de dies de l'estada",
        y = "Freqüència"
    ) +
    theme_minimal() +
    theme(
        plot.title = element_text(hjust = 0.5),
        legend.position = "bottom"
    )

plot(sort(x$stay_days))

# Habitacions ocupades dels hotels per dia
occupation_data <- x |>
    filter(is_canceled == 0) |>
    rowwise() |>
    mutate(stay_dates = list(seq.Date(dia, by = "day", length.out = stay_days))) |>
    unnest(cols = c(stay_dates)) |>
    group_by(hotel, stay_dates) |>
    summarise(occupied_rooms = n(), .groups = "drop")

# Ocupació diària de cada tipus d'hotel
ggplot(occupation_data, aes(x = stay_dates, y = occupied_rooms, color = hotel)) +
    geom_line() +
    labs(
        title = "Habitacions ocupades diàriament segons el tipus d'hotel",
        x = "Data",
        y = "Nombre d'habitacions ocupades",
        color = "Tipus d'hotel"
    ) +
    theme_minimal() +
    theme(
        plot.title = element_text(hjust = 0.5),
        legend.position = "bottom"
    )

occupation_data$year <- as.integer(format.Date(occupation_data$stay_dates, "%Y"))
ggplot(occupation_data, aes(
    x = stay_dates,
    y = occupied_rooms, color = as.factor(year)
)) +
    geom_line() +
    facet_wrap(~hotel) +
    labs(
        title = "Habitacions ocupades diàriament segons el tipus d'hotel i any",
        x = "Data",
        y = "Nombre d'habitacions ocupades",
        color = "Any"
    ) +
    theme_minimal() +
    theme(
        plot.title = element_text(hjust = 0.5),
        legend.position = "bottom"
    )

# Ocupació mitjana setmanal de cada tipus d'hotel
weekly_occupation <- occupation_data |>
    mutate(week = floor_date(stay_dates, "week")) |>
    group_by(hotel, week) |>
    summarise(avg_occupied_rooms = mean(occupied_rooms), .groups = "drop")
ggplot(weekly_occupation, aes(x = week, y = avg_occupied_rooms, color = hotel)) +
    geom_line() +
    labs(
        title = "Habitacions ocupades setmanalment segons el tipus d'hotel",
        x = "Setmana",
        y = "Nombre mitjà d'habitacions ocupades",
        color = "Tipus d'hotel"
    ) +
    theme_minimal() +
    theme(
        plot.title = element_text(hjust = 0.5),
        legend.position = "bottom"
    )

# Ocupació mitjana mensual de cada tipus d'hotel
monthly_occupation <- occupation_data |>
    mutate(month = floor_date(stay_dates, "month")) |>
    group_by(hotel, month) |>
    summarise(avg_occupied_rooms = mean(occupied_rooms), .groups = "drop")
ggplot(monthly_occupation, aes(x = month, y = avg_occupied_rooms, color = hotel)) +
    geom_line() +
    labs(
        title = "Habitacions ocupades mensualment segons el tipus d'hotel",
        x = "Mes",
        y = "Nombre mitjà d'habitacions ocupades",
        color = "Tipus d'hotel"
    ) +
    theme_minimal() +
    theme(
        plot.title = element_text(hjust = 0.5),
        legend.position = "bottom"
    )

# Cost mitja per dia mitjà dels dos hotels
daily_occupation_revenue <- occupation_data |>
    left_join(
        x |>
            filter(is_canceled == 0) |>
            rowwise() |>
            mutate(stay_dates = list(seq.Date(dia, by = "day", length.out = stay_days))) |>
            unnest(cols = c(stay_dates)) |>
            group_by(hotel, stay_dates) |>
            summarise(daily_adr = mean(adr), .groups = "drop"),
        by = c("hotel", "stay_dates")
    ) |>
    mutate(daily_revenue = occupied_rooms * daily_adr)
ggplot(
    daily_occupation_revenue,
    aes(x = stay_dates, y = daily_revenue, color = hotel)
) +
    geom_line() +
    labs(
        title = "Ingressos diaris segons el tipus d'hotel",
        x = "Data",
        y = "Ingressos diaris",
        color = "Tipus d'hotel"
    ) +
    theme_minimal() +
    theme(
        plot.title = element_text(hjust = 0.5),
        legend.position = "bottom"
    )

# Mostra en una línea l'ocupació per dia, i en un altra línea el preu per dia
ggplot(daily_occupation_revenue, aes(x = stay_dates)) +
    geom_line(aes(y = occupied_rooms, color = "Ocupació")) +
    geom_line(aes(y = daily_adr, color = "Preu mitjà (ADR)")) +
    facet_wrap(~hotel) +
    labs(
        title = "Ocupació i preu mitjà diari segons el tipus d'hotel",
        x = "Data",
        y = "Valor",
        color = "Mètrica"
    ) +
    theme_minimal() +
    theme(
        plot.title = element_text(hjust = 0.5),
        legend.position = "bottom"
    )

# Eliminació dels objectes temporals
# rm(occupation_data, monthly_occupation, weekly_occupation, daily_occupation_revenue)
# calcula els beneficis diaris a partir de l'ADR i les habitacions ocupades
daily_revenue <- x |>
    filter(is_canceled == 0) |>
    rowwise() |>
    mutate(stay_dates = list(seq.Date(dia, by = "day", length.out = stay_days))) |>
    unnest(cols = c(stay_dates)) |>
    group_by(hotel, stay_dates) |>
    summarise(daily_revenue = sum(adr), .groups = "drop")

# Ingressos diaris de cada tipus d'hotel
ggplot(daily_revenue, aes(x = stay_dates, y = daily_revenue, color = hotel)) +
    geom_line() +
    labs(
        title = "Ingressos diaris segons el tipus d'hotel",
        x = "Data",
        y = "Ingressos diaris",
        color = "Tipus d'hotel"
    ) +
    theme_minimal() +
    theme(
        plot.title = element_text(hjust = 0.5),
        legend.position = "bottom"
    )

# Ingressos setmanals mitjans de cada tipus d'hotel
weekly_revenue <- daily_revenue |>
    mutate(week = floor_date(stay_dates, "week")) |>
    group_by(hotel, week) |>
    summarise(avg_weekly_revenue = mean(daily_revenue), .groups = "drop")
ggplot(weekly_revenue, aes(x = week, y = avg_weekly_revenue, color = hotel)) +
    geom_line() +
    labs(
        title = "Ingressos setmanals mitjans segons el tipus d'hotel",
        x = "Setmana",
        y = "Ingressos setmanals mitjans",
        color = "Tipus d'hotel"
    ) +
    theme_minimal() +
    theme(
        plot.title = element_text(hjust = 0.5),
        legend.position = "bottom"
    )

# Ingressos mensuals mitjans de cada tipus d'hotel
monthly_revenue <- daily_revenue |>
    mutate(month = floor_date(stay_dates, "month")) |>
    group_by(hotel, month) |>
    summarise(avg_monthly_revenue = mean(daily_revenue), .groups = "drop")
ggplot(monthly_revenue, aes(x = month, y = avg_monthly_revenue, color = hotel)) +
    geom_line() +
    labs(
        title = "Ingressos mensuals mitjans segons el tipus d'hotel",
        x = "Mes",
        y = "Ingressos mensuals mitjans",
        color = "Tipus d'hotel"
    ) +
    theme_minimal() +
    theme(
        plot.title = element_text(hjust = 0.5),
        legend.position = "bottom"
    )

# Comparar el benefici dels dos hotels pel tipus de règim alimentari 'meal'
revenue_by_meal_plan <- x |>
    filter(is_canceled == 0) |>
    rowwise() |>
    mutate(stay_dates = list(seq.Date(dia, by = "day", length.out = stay_days))) |>
    unnest(cols = c(stay_dates)) |>
    group_by(hotel, meal, stay_dates) |>
    summarise(daily_revenue = sum(adr), .groups = "drop")
ggplot(revenue_by_meal_plan, aes(
    x = stay_dates,
    y = daily_revenue, fill = fct_reorder(meal, daily_revenue, .desc = TRUE)
)) +
    geom_bar(stat = "identity", position = "stack") +
    facet_wrap(~hotel) +
    labs(
        title = "Ingressos diaris segons el tipus d'hotel i règim alimentari",
        x = "Data",
        y = "Ingressos diaris",
        color = "Tipus d'hotel i règim alimentari"
    ) +
    theme_minimal() +
    theme(
        plot.title = element_text(hjust = 0.5),
        legend.position = "bottom"
    )
## Ignoring unknown labels:
## • colour : "Tipus d'hotel i règim alimentari"

# Matriu de correlació per a les variables numèriques
# selecciona el nom de les variables numèriques de daily_revenue
xx <- x |>
    group_by(country) |>
    mutate(pais = n()) |>
    filter(pais >= 1000)
numeric_vars <- xx[, sapply(
    xx,
    function(col) is.numeric(col)
)]
correlation_matrix <- cor(numeric_vars, use = "complete.obs")
melted_correlation <- melt(correlation_matrix)
ggplot(data = melted_correlation, aes(
    x = Var1, y = Var2, fill = value
)) +
    geom_tile() +
    scale_fill_gradient2(
        low = "blue", high = "red", mid = "white",
        limit = c(-1, 1), name = "Correlation"
    ) +
    theme_minimal() +
    theme(
        axis.text.x = element_text(angle = 90, vjust = 0.5, hjust = 1),
        plot.title = element_text(hjust = 0.5)
    ) +
    labs(title = "Matriu de correlació de variables numèriques")

1.2 Narració

1.2.1 Introducció: El Desafiament Operatiu

Els patrons importen

# Ingressos setmanals de cada tipus d'hotel
p <- ggplot(
    daily_revenue |>
        mutate(week = floor_date(stay_dates, "week")) |>
        group_by(hotel, week) |>
        summarise(weekly_revenue = sum(daily_revenue), .groups = "drop"),
    aes(x = week, y = weekly_revenue, color = hotel),
) +
    scale_y_continuous(labels = scales::label_number(scale = 1, big.mark = ".")) +
    geom_line(linewidth = 1.5) +
    labs(
        title = "Ingressos setmanals segons el tipus d'hotel",
        x = "Setmana",
        y = "Ingressos setmanals",
        color = "Tipus d'hotel"
    ) +
    theme_minimal() +
    theme(
        plot.title = element_text(hjust = 0, size = 36),
        # text de les marques dels eixos
        axis.text.x = element_text(size = 18),
        axis.text.y = element_text(size = 18),
        # fons transparents
        panel.background = element_rect(fill = "transparent", color = NA),
        plot.background = element_rect(fill = "transparent", color = NA),
        legend.background = element_rect(fill = "transparent", color = NA),
        legend.box.background = element_rect(fill = "transparent", color = NA),
        legend.key = element_rect(fill = "transparent", color = NA),
        # text dels títols dels eixos
        axis.title.y = element_blank(),
        axis.title.x = element_blank(),
        # augmenta la mida de la graella
        panel.grid.major = element_line(size = 1),
        panel.grid.minor = element_line(size = 0.5),
        # text del facet
        strip.text = element_text(size = 24),
        # llegenda
        legend.position = "bottom",
        legend.text = element_text(size = 20),
        legend.title = element_blank()
    )
## Warning: The `size` argument of `element_line()` is deprecated as of ggplot2 3.4.0.
## ℹ Please use the `linewidth` argument instead.
## This warning is displayed once every 8 hours.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
# desa la imatge en un fitxer SVG
# ggsave("img/weekly_revenue.svg",
#     plot = p,
#     width = 11.2, height = 9, # aspect ratio 16:9, width x 0.7 (storytell area)
#     bg = "transparent"
# )
rm(p)

Els hotels s’enfronten a una realitat preocupant

# Mostra en una línia l'ocupació mitjana setmanal, i en un altra línia el preu
# mitjà setmanal
p <- ggplot(weekly_occupation, aes(x = week)) +
    geom_line(
        aes(
            y = avg_occupied_rooms,
            color = "Ocupació mitjana setmanal"
        ),
        linewidth = 1.5
    ) +
    geom_line(
        data = daily_occupation_revenue |>
            mutate(week = floor_date(stay_dates, "week")) |>
            group_by(hotel, week) |>
            summarise(avg_weekly_adr = mean(daily_adr), .groups = "drop"),
        aes(y = avg_weekly_adr, color = "ADR mitjà setmanal"),
        linewidth = 1.5
    ) +
    facet_wrap(~hotel, strip.position = "bottom") +
    labs(
        title = "Ocupació i preu mitjà setmanal",
        x = "Setmana",
        y = "Valor",
        color = "Mètrica"
    ) +
    theme_minimal() +
    theme(
        plot.title = element_text(hjust = 0, size = 36),
        # text de les marques dels eixos
        axis.text.x = element_text(size = 18),
        axis.text.y = element_text(size = 18),
        # fons transparents
        panel.background = element_rect(fill = "transparent", color = NA),
        plot.background = element_rect(fill = "transparent", color = NA),
        legend.background = element_rect(fill = "transparent", color = NA),
        legend.box.background = element_rect(fill = "transparent", color = NA),
        legend.key = element_rect(fill = "transparent", color = NA),
        # text dels títols dels eixos
        axis.title.y = element_blank(),
        axis.title.x = element_blank(),
        # augmenta la mida de la graella
        panel.grid.major = element_line(size = 1),
        panel.grid.minor = element_line(size = 0.5),
        # text del facet
        strip.text = element_text(size = 24),
        # llegenda
        legend.position = "bottom",
        legend.text = element_text(size = 20),
        legend.title = element_blank()
    )
# desa la imatge en un fitxer SVG
# ggsave("img/weekly_occupation_adr.svg",
#     plot = p,
#     width = 11.2, height = 9, # aspect ratio 16:9, width x 0.7 (storytell area)
#     bg = "transparent"
# )
rm(p)

Regim alimentari i ingressos: una relació clau:

# Comparar el benefici dels dos hotels pel tipus de règim alimentari 'meal'
revenue_by_meal_plan <- x |>
    filter(is_canceled == 0) |>
    rowwise() |>
    mutate(stay_dates = list(seq.Date(dia, by = "day", length.out = stay_days))) |>
    unnest(cols = c(stay_dates)) |>
    group_by(hotel, meal, stay_dates) |>
    summarise(daily_revenue = sum(adr), .groups = "drop")
revenue_by_meal_plan
## # A tibble: 4,976 × 4
##    hotel      meal  stay_dates daily_revenue
##    <fct>      <ord> <date>             <dbl>
##  1 City Hotel SC    2015-07-03          60.3
##  2 City Hotel SC    2015-07-04          60.3
##  3 City Hotel SC    2015-07-05          60.3
##  4 City Hotel SC    2015-07-06          60.3
##  5 City Hotel SC    2015-07-07          60.3
##  6 City Hotel SC    2015-07-08          60.3
##  7 City Hotel SC    2015-07-09          60.3
##  8 City Hotel SC    2015-07-11         104. 
##  9 City Hotel SC    2015-07-20          46.7
## 10 City Hotel SC    2015-07-21          46.7
## # ℹ 4,966 more rows
p <- ggplot(revenue_by_meal_plan, aes(
    x = stay_dates,
    # y = daily_revenue, fill = fct_reorder(meal, daily_revenue, .desc = FALSE)
    y = daily_revenue, fill = meal
)) +
    geom_bar(stat = "identity", position = "stack") +
    facet_wrap(~hotel) +
    scale_y_continuous(labels = scales::label_number(scale = 1, big.mark = ".")) +
    scale_fill_manual(
        name = "Règim alimentari",
        labels = c(
            "Undefined" = "No definit",
            "SC" = "Sense menjar",
            "BB" = "Esmorzar",
            "HB" = "Mitja pensió",
            "FB" = "Pensió completa"
        ),
        values = c(
            "Undefined" = "#cbcbcb",
            "SC" = "#dad9d9",
            "BB" = "#8ae3f9",
            "HB" = "#0033ff",
            "FB" = "#FF00CC"
        )
    ) +
    labs(
        title = "Ingressos diaris segons el règim alimentari",
        x = "Data",
        y = "Ingressos diaris",
        fill = "Règim alimentari"
    ) +
    theme_minimal() +
    theme(
        plot.title = element_text(hjust = 0, size = 36),
        axis.text.x = element_text(size = 18),
        axis.text.y = element_text(size = 18),
        panel.background = element_rect(fill = "transparent", color = NA),
        plot.background = element_rect(fill = "transparent", color = NA),
        legend.background = element_rect(fill = "transparent", color = NA),
        legend.box.background = element_rect(fill = "transparent", color = NA),
        legend.key = element_rect(fill = "transparent", color = NA),
        axis.title.y = element_blank(),
        axis.title.x = element_blank(),
        panel.grid.major = element_line(size = 1),
        panel.grid.minor = element_line(size = 0.5),
        strip.text = element_text(size = 24),
        legend.position = "bottom",
        legend.text = element_text(size = 20),
        legend.title = element_blank()
    )
# ggsave("img/revenue_by_meal_plan.svg",
#     plot = p,
#     width = 11.2, height = 9, # aspect ratio 16:9, width x 0.7 (storytell area)
#     bg = "transparent"
# )
rm(p)

1.2.2 PART I: PATRONS DE CANCEL·LACIÓ DIFERENCIATS

Hotels City vs. Resort: Disparitats de Cancel·lació

# Gràfica: Taxa de cancel·lació per tipus d'hotel diagrama de sectors de is_canceled
cancellation_summary <- xx |>
    group_by(hotel, is_canceled) |>
    summarise(n = n(), .groups = "drop") |>
    group_by(hotel) |>
    tidyr::complete(is_canceled = c("0", "1"), fill = list(n = 0)) |>
    mutate(pct = n / sum(n))
p <- ggplot(cancellation_summary, aes(x = factor(1), y = pct, fill = is_canceled)) +
    coord_polar(theta = "y", start = 0) +
    facet_wrap(~hotel, strip.position = "bottom") +
    scale_y_continuous(limits = c(0, 1), labels = scales::percent) +
    scale_fill_manual(
        name = "Estat",
        values = c("0" = "#2ecc71", "1" = "#e74c3c"),
        labels = c("No cancel·lada", "Cancel·lada")
    ) +
    geom_col(width = 1) +
    geom_text(aes(label = percent(pct, accuracy = 0.1)),
        position = position_stack(vjust = 0.5),
        size = 5, fontface = "bold", color = "white"
    ) +
    labs(
        title = "Percentatge de cancel·lacions per tipus d'hotel",
        x = NULL, y = NULL
    ) +
    theme_void() +
    theme(
        plot.title = element_text(hjust = 0, size = 36),
        # text de les marques dels eixos
        axis.text.x = element_blank(),
        axis.text.y = element_blank(),
        # fons transparents
        panel.background = element_rect(fill = "transparent", color = NA),
        plot.background = element_rect(fill = "transparent", color = NA),
        legend.background = element_rect(fill = "transparent", color = NA),
        legend.box.background = element_rect(fill = "transparent", color = NA),
        legend.key = element_rect(fill = "transparent", color = NA),
        # text dels títols dels eixos
        axis.title.y = element_blank(),
        axis.title.x = element_blank(),
        # text del facet
        strip.text = element_text(size = 24),
        # llegenda
        legend.position = "bottom",
        legend.text = element_text(size = 20),
        legend.title = element_blank()
    )
# ggsave("img/cancellation_by_hotel.svg",
#     plot = p,
#     width = 11.2, height = 9, # aspect ratio 16:9, width x 0.7 (storytell area)
#     bg = "transparent"
# )

Origen Geogràfic: Clients Locals vs. Internacionals

# Gràfica: Taxa de cancel·lació per origen (País)
# Categoritzar entre Portugal i International
xx_is_portugal <- xx |>
    mutate(is_portugal = ifelse(country == "PRT", "Portugal", "Altres")) |>
    group_by(is_portugal, is_canceled) |>
    summarise(n = n(), .groups = "drop") |>
    group_by(is_portugal) |>
    mutate(pct = n / sum(n))
p <- ggplot(xx_is_portugal, aes(x = factor(1), y = pct, fill = is_canceled)) +
    geom_col(width = 1) +
    coord_polar(theta = "y") +
    facet_wrap(~is_portugal, strip.position = "bottom") +
    scale_y_continuous(labels = scales::percent) +
    scale_fill_manual(name = "Estat", labels = c("No Cancel·lada", "Cancel·lada"), values = c("0" = "#2ecc71", "1" = "#e74c3c")) +
    geom_text(aes(label = percent(pct, accuracy = 0.1)), position = position_stack(vjust = 0.5), size = 5, fontface = "bold", color = "white") +
    labs(title = "Percentatge de cancel·lacions per país d'origen", x = NULL, y = NULL) +
    theme_void() +
    theme(
        plot.title = element_text(hjust = 0, size = 36),
        # text de les marques dels eixos
        axis.text.x = element_blank(),
        axis.text.y = element_blank(),
        # fons transparents
        panel.background = element_rect(fill = "transparent", color = NA),
        plot.background = element_rect(fill = "transparent", color = NA),
        legend.background = element_rect(fill = "transparent", color = NA),
        legend.box.background = element_rect(fill = "transparent", color = NA),
        legend.key = element_rect(fill = "transparent", color = NA),
        # text dels títols dels eixos
        axis.title.y = element_blank(),
        axis.title.x = element_blank(),
        # text del facet
        strip.text = element_text(size = 24),
        # llegenda
        legend.position = "bottom",
        legend.text = element_text(size = 20),
        legend.title = element_blank()
    )
rm(xx_is_portugal)

# ggsave("img/cancellation_by_origin.svg",
#     plot = p,
#     width = 11.2, height = 9, # aspect ratio 16:9, width x 0.7 (storytell area)
#     bg = "transparent"
# )

Dipòsits No Reemborsables: Una Anomalia Preocupant

# Gràfica: Taxa de cancel·lació per tipus de dipòsit diagrama de sectors de is_canceled
cancellation_summary <- xx |>
    group_by(deposit_type, is_canceled) |>
    summarise(n = n(), .groups = "drop") |>
    group_by(deposit_type) |>
    tidyr::complete(is_canceled = c("0", "1"), fill = list(n = 0)) |>
    mutate(pct = n / sum(n))
p <- ggplot(cancellation_summary, aes(x = factor(1), y = pct, fill = is_canceled)) +
    coord_polar(theta = "y", start = 0) +
    facet_wrap(~deposit_type, strip.position = "bottom") +
    scale_y_continuous(limits = c(0, 1), labels = scales::percent) +
    scale_fill_manual(
        name = "Estat",
        values = c("0" = "#2ecc71", "1" = "#e74c3c"),
        labels = c("No cancel·lada", "Cancel·lada")
    ) +
    geom_col(width = 1) +
    geom_text(aes(label = percent(pct, accuracy = 0.1)),
        position = position_stack(vjust = 0.5),
        size = 5, fontface = "bold", color = "white"
    ) +
    labs(
        title = "Percentatge de cancel·lacions per tipus de dipòsit",
        x = NULL, y = NULL
    ) +
    theme_void() +
    theme(
        plot.title = element_text(hjust = 0, size = 36),
        # text de les marques dels eixos
        axis.text.x = element_blank(),
        axis.text.y = element_blank(),
        # fons transparents
        panel.background = element_rect(fill = "transparent", color = NA),
        plot.background = element_rect(fill = "transparent", color = NA),
        legend.background = element_rect(fill = "transparent", color = NA),
        legend.box.background = element_rect(fill = "transparent", color = NA),
        legend.key = element_rect(fill = "transparent", color = NA),
        # text dels títols dels eixos
        axis.title.y = element_blank(),
        axis.title.x = element_blank(),
        # text del facet
        strip.text = element_text(size = 24),
        # llegenda
        legend.position = "bottom",
        legend.text = element_text(size = 20),
        legend.title = element_blank()
    )
# ggsave("img/cancellation_by_deposit.svg",
#     plot = p,
#     width = 11.2, height = 9, # aspect ratio 16:9, width x 0.7 (storytell area)
#     bg = "transparent"
# )

1.2.3 PART II: ESTACIONALITAT I OCUPACIÓ

Pic d’Ocupació: Maig-Octubre i Nadal:

# Gràfica: Ocupació mensual
monthly_occupancy <- xx |>
    filter(is_canceled == 0) |>
    rowwise() |>
    mutate(stay_dates = list(seq.Date(dia, by = "day", length.out = stay_days))) |>
    unnest(cols = c(stay_dates)) |>
    filter(year(stay_dates) == 2016) |>
    group_by(month = month(stay_dates)) |>
    summarise(
        avg_occupancy = round((100 * n()) / (360 * days_in_month(
            as.Date(paste0("2016-", month(stay_dates), "-01"))
        )
        ), 1),
        total_reservations = n()
    )
## Warning: Returning more (or less) than 1 row per `summarise()` group was deprecated in
## dplyr 1.1.0.
## ℹ Please use `reframe()` instead.
## ℹ When switching from `summarise()` to `reframe()`, remember that `reframe()`
##   always returns an ungrouped data frame and adjust accordingly.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
## `summarise()` has grouped output by 'month'. You can override using the
## `.groups` argument.
monthly_occupancy[!duplicated(monthly_occupancy), ]
## # A tibble: 12 × 3
## # Groups:   month [12]
##    month avg_occupancy total_reservations
##    <dbl>         <dbl>              <int>
##  1     1          35                 3903
##  2     2          58                 6055
##  3     3          79.8               8901
##  4     4          86.9               9387
##  5     5          95.5              10658
##  6     6          92.2               9957
##  7     7          95.7              10678
##  8     8          99.5              11104
##  9     9          96.7              10443
## 10    10          97.4              10873
## 11    11          72.2               7793
## 12    12          58.5               6530
ggplot(
    monthly_occupancy[!duplicated(monthly_occupancy), ],
    aes(x = factor(month, labels = month.abb), y = avg_occupancy)
) +
    geom_line(color = "steelblue", linewidth = 1.5) +
    geom_point(size = 4, color = "steelblue") +
    geom_text(aes(label = paste0(avg_occupancy, "%")), vjust = -1, size = 5) +
    labs(
        title = "Estacionalitat: Ocupació mitjana per mes",
        x = "Mes",
        y = "Ocupació (%)"
    ) +
    theme_minimal() +
    theme(
        plot.title = element_text(hjust = 0, size = 36),
        axis.text = element_text(size = 18),
        panel.background = element_rect(fill = "transparent", color = NA),
        plot.background = element_rect(fill = "transparent", color = NA),
        legend.position = "none",
        axis.title = element_blank()
    )
## `geom_line()`: Each group consists of only one observation.
## ℹ Do you need to adjust the group aesthetic?

# ggsave("img/monthly_occupancy.svg",
#     plot = last_plot(),
#     width = 11.2, height = 9, # aspect ratio 16:9, width x 0.7 (storytell area)
#     bg = "transparent"
# )

Valls de Demanda

# Gràfica: Períodes de baix i alt ocupació
demand_pattern <- xx |>
    filter(is_canceled == 0) |>
    rowwise() |>
    mutate(stay_dates = list(seq.Date(dia, by = "day", length.out = stay_days))) |>
    unnest(cols = c(stay_dates)) |>
    mutate(
        month = month(stay_dates),
        year = year(stay_dates)
    ) |>
    filter(year == 2016) |>
    mutate(season = case_when(
        month %in% c(5, 6, 7, 8, 9, 10) ~ "Alta (Maig-Octubre)",
        month %in% c(12, 1) ~ "Nadal",
        TRUE ~ "Baixa (Nov-Febrer)"
    )) |>
    group_by(season) |>
    summarise(
        avg_occupancy = round((mean((100 * n()) / (360 * case_when(
            season == "Baixa (Nov-Febrer)" ~ 119,
            season == "Nadal" ~ 62,
            TRUE ~ 184
        )))), 1),
        year = year(stay_dates),
        total_bookings = n()
    )
## Warning: Returning more (or less) than 1 row per `summarise()` group was deprecated in
## dplyr 1.1.0.
## ℹ Please use `reframe()` instead.
## ℹ When switching from `summarise()` to `reframe()`, remember that `reframe()`
##   always returns an ungrouped data frame and adjust accordingly.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
## `summarise()` has grouped output by 'season'. You can override using the
## `.groups` argument.
demand_pattern[!duplicated(demand_pattern), ]
## # A tibble: 3 × 4
## # Groups:   season [3]
##   season              avg_occupancy  year total_bookings
##   <chr>                       <dbl> <dbl>          <int>
## 1 Alta (Maig-Octubre)          96.2  2016          63713
## 2 Baixa (Nov-Febrer)           75    2016          32136
## 3 Nadal                        46.7  2016          10433
ggplot(demand_pattern[!duplicated(demand_pattern), ], aes(
    x = factor(season, levels = c("Alta (Maig-Octubre)", "Nadal", "Baixa (Nov-Febrer)")),
    y = avg_occupancy, fill = season
)) +
    geom_col() +
    scale_fill_manual(
        values = c(
            "Alta (Maig-Octubre)" = "#08519c",
            "Nadal" = "#3182bd",
            "Baixa (Nov-Febrer)" = "#9ecae1"
        )
    ) +
    # línia amb la mitjana d'ocupació
    # geom_hline(yintercept = mean(monthly_occupancy$avg_occupancy), linetype = "dashed", color = "red", size = 1) +
    # annotate("text",
    #     x = 2.5, y = mean(monthly_occupancy$avg_occupancy) + 2,
    #     label = paste0("Mitjana d'ocupació: ", round(mean(monthly_occupancy$avg_occupancy), 1), "%"),
    #     color = "red", size = 6
    # ) +
    geom_text(aes(label = paste0(avg_occupancy, "%")), vjust = -0.5, size = 6) +
    labs(
        title = "Patró estacional: Ocupació per temporada",
        x = "Temporada",
        y = "Ocupació (%)"
    ) +
    theme_minimal() +
    theme(
        plot.title = element_text(hjust = 0, size = 36),
        axis.text = element_text(size = 18),
        panel.background = element_rect(fill = "transparent", color = NA),
        plot.background = element_rect(fill = "transparent", color = NA),
        legend.position = "none",
        axis.title = element_blank()
    )

# ggsave("img/seasonal_pattern.svg",
#     plot = last_plot(),
#     width = 11.2, height = 9, # aspect ratio 16:9, width x 0.7 (storytell area)
#     bg = "transparent"
# )

Patró Consistent

# Gràfica: Tendència estacional per any
yearly_monthly_occupancy <- xx |>
    filter(is_canceled == 0) |>
    rowwise() |>
    mutate(stay_dates = list(seq.Date(dia, by = "day", length.out = stay_days))) |>
    unnest(cols = c(stay_dates)) |>
    mutate(
        year = year(stay_dates),
        month = month(stay_dates)
    ) |>
    group_by(year, month) |>
    summarise(
        total_stays = n(),
        avg_occupancy = round((100 * n()) /
            (360 * days_in_month(as.Date(paste(year, month, "01", sep = "-")))), 1),
        .groups = "drop"
    )
## Warning: Returning more (or less) than 1 row per `summarise()` group was deprecated in
## dplyr 1.1.0.
## ℹ Please use `reframe()` instead.
## ℹ When switching from `summarise()` to `reframe()`, remember that `reframe()`
##   always returns an ungrouped data frame and adjust accordingly.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
yearly_monthly_occupancy[!duplicated(yearly_monthly_occupancy), ]
## # A tibble: 27 × 4
##     year month total_stays avg_occupancy
##    <dbl> <dbl>       <int>         <dbl>
##  1  2015     7        5535          49.6
##  2  2015     8        8046          72.1
##  3  2015     9        9714          89.9
##  4  2015    10        9140          81.9
##  5  2015    11        5140          47.6
##  6  2015    12        4851          43.5
##  7  2016     1        3903          35  
##  8  2016     2        6055          58  
##  9  2016     3        8901          79.8
## 10  2016     4        9387          86.9
## # ℹ 17 more rows
ggplot(
    yearly_monthly_occupancy[!duplicated(yearly_monthly_occupancy), ],
    aes(x = month, y = avg_occupancy, color = factor(year), group = year)
) +
    scale_x_continuous(breaks = 1:12, labels = month.abb) +
    geom_line(linewidth = 1.5) +
    geom_point(size = 3) +
    labs(
        title = "Consistència estacional",
        x = "Mes",
        y = "Ocupació (%)",
        color = "Any"
    ) +
    theme_minimal() +
    theme(
        plot.title = element_text(hjust = 0, size = 36),
        axis.text = element_text(size = 18),
        panel.background = element_rect(fill = "transparent", color = NA),
        plot.background = element_rect(fill = "transparent", color = NA),
        legend.text = element_text(size = 16),
        legend.title = element_text(size = 16),
        axis.title = element_blank()
    )

# ggsave("img/yearly_seasonality.svg",
#     plot = last_plot(),
#     width = 11.2, height = 9, # aspect ratio 16:9, width x 0.7 (storytell area)
#     bg = "transparent"
# )

Nits d’Estada: Resort Supera City en Durada

# Gràfica: Durada mitjana d'estada per hotel
stay_duration <- xx |>
    filter(is_canceled == 0) |>
    mutate(total_stay = stays_in_weekend_nights + stays_in_week_nights) |>
    group_by(hotel) |>
    summarise(
        avg_stay = round(mean(total_stay), 1),
        median_stay = round(median(total_stay), 1),
        sd_stay = round(sd(total_stay), 1)
    )

ggplot(
    xx |> mutate(total_stay = stays_in_weekend_nights + stays_in_week_nights),
    aes(x = hotel, y = total_stay, fill = hotel)
) +
    geom_violin(alpha = 0.7) +
    geom_boxplot(alpha = 0.7, outliers = FALSE) +
    coord_flip() +
    labs(
        title = "Durada de l'estada: City vs. Resort",
        x = "Tipus d'hotel",
        y = "Nits d'estada"
    ) +
    theme_minimal() +
    theme(
        plot.title = element_text(hjust = 0, size = 36),
        axis.text = element_text(size = 18),
        panel.background = element_rect(fill = "transparent", color = NA),
        plot.background = element_rect(fill = "transparent", color = NA),
        legend.position = "none",
        axis.title = element_blank()
    )

# ggsave("img/stay_duration.svg",
#     plot = last_plot(),
#     width = 11.2, height = 9, # aspect ratio 16:9, width x 0.7 (storytell area)
#     bg = "transparent"
# )

1.2.4 PART III: SEGMENTACIÓ DE CLIENTS

Turoperadors (TA/TO): Volum Alt, Cancel·lacions Altes

# Gràfica: Reserves i cancel·lacions per canal de distribució
distribution_channels <- xx |>
    group_by(market_segment) |>
    summarise(
        total_bookings = n(),
        cancellation_number = sum(as.numeric(is_canceled) - 1),
        cancellation_rate = round(mean(as.numeric(is_canceled) - 1) * 100, 1),
        avg_stay = round(mean(stays_in_weekend_nights + stays_in_week_nights), 1)
    ) |>
    filter(total_bookings >= 1000) |>
    arrange(desc(total_bookings))
distribution_channels
## # A tibble: 5 × 5
##   market_segment total_bookings cancellation_number cancellation_rate avg_stay
##   <fct>                   <int>               <dbl>             <dbl>    <dbl>
## 1 Online TA               48478               17578              36.3      3.6
## 2 Offline TA/TO           22765                8236              36.2      3.9
## 3 Groups                  18749               12036              64.2      3  
## 4 Direct                  11197                1718              15.3      3.2
## 5 Corporate                4826                 970              20.1      2.1
ggplot(distribution_channels, aes(
    x = reorder(market_segment, -total_bookings),
    y = cancellation_rate, fill = market_segment
)) +
    geom_col() +
    scale_fill_manual(
        values = c(
            "Online TA" = "#08519c",
            "Offline TA/TO" = "#3182bd",
            "Groups" = "#6baed6",
            "Direct" = "#9ecae1",
            "Corporate" = "#c6dbef"
        )
    ) +
    geom_text(aes(label = paste0(cancellation_rate, "%")), vjust = -0.5, size = 5) +
    # línia amb el percentatge sobre el total de reserves
    geom_line(aes(market_segment, total_bookings / max(total_bookings) * max(cancellation_rate)),
        group = 1, color = "red", size = 1
    ) +
    geom_point(aes(market_segment, total_bookings / max(total_bookings) * max(cancellation_rate)),
        color = "red", size = 3
    ) +
    scale_y_continuous(
        sec.axis = sec_axis(~ . * max(distribution_channels$total_bookings) / max(distribution_channels$cancellation_rate),
            name = "Total de reserves"
        )
    ) +
    labs(
        title = "Taxa de cancel·lació per segment de mercat",
        x = "Segment de mercat",
        y = "Taxa de cancel·lació (%)"
    ) +
    theme_minimal() +
    theme(
        plot.title = element_text(hjust = 0, size = 36),
        axis.title = element_text(size = 20),
        axis.text.x = element_text(size = 16, angle = 45, hjust = 1),
        axis.text.y = element_text(size = 18),
        panel.background = element_rect(fill = "transparent", color = NA),
        plot.background = element_rect(fill = "transparent", color = NA),
        legend.position = "none"
    )
## Warning: Using `size` aesthetic for lines was deprecated in ggplot2 3.4.0.
## ℹ Please use `linewidth` instead.
## This warning is displayed once every 8 hours.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.

# ggsave("img/market_segment_cancellation.svg",
#     plot = last_plot(),
#     width = 11.2, height = 9, # aspect ratio 16:9, width x 0.7 (storytell area)
#     bg = "transparent"
# )

Clients Transitoris: Alta Flexibilitat, Risc Elevat

# Gràfica: Lead time vs. Cancel·lació per segment
lead_time_cancellation <- xx |>
    filter(lead_time >= 0 & !is.na(lead_time)) |>
    mutate(lead_time_category = cut(lead_time,
        breaks = c(0, 7, 30, 90, 2000),
        labels = c("0-7 dies", "8-30 dies", "31-90 dies", ">90 dies")
    )) |>
    group_by(lead_time_category) |>
    summarise(
        cancellation_rate = round(mean(as.numeric(is_canceled) - 1) * 100, 1),
        total_bookings = n()
    )

ggplot(lead_time_cancellation, aes(x = lead_time_category, y = cancellation_rate, fill = lead_time_category)) +
    geom_col() +
    geom_text(aes(label = paste0(cancellation_rate, "%")), vjust = -0.5, size = 5) +
    scale_fill_manual(
        values = c(
            "0-7 dies" = "#6baed6",
            "8-30 dies" = "#3182bd",
            "31-90 dies" = "#08519c",
            ">90 dies" = "#ff0000"
        )
    ) +
    labs(
        title = "Cancel·lacions segons temps d'anticipació",
        x = "Anticipació (Lead time)",
        y = "Taxa de cancel·lació (%)"
    ) +
    theme_minimal() +
    theme(
        plot.title = element_text(hjust = 0, size = 36),
        axis.text = element_text(size = 18),
        panel.background = element_rect(fill = "transparent", color = NA),
        plot.background = element_rect(fill = "transparent", color = NA),
        legend.position = "none",
        axis.title = element_blank()
    )

# ggsave("img/lead_time_cancellation.svg",
#     plot = last_plot(),
#     width = 11.2, height = 9, # aspect ratio 16:9, width x 0.7 (storytell area)
#     bg = "transparent"
# )

Clients Locals: Patrons de Reserva Curts

# Gràfica: Durada d'estada per origen
stay_by_origin <- xx |>
    mutate(
        origin = if_else(country == "PRT", "Portugal", "Internacional"),
        total_stay = stays_in_weekend_nights + stays_in_week_nights
    ) |>
    group_by(origin) |>
    summarise(
        avg_stay = round(mean(total_stay), 1),
        median_stay = round(median(total_stay), 1)
    )

ggplot(
    xx |>
        mutate(
            origin = if_else(country == "PRT", "Portugal", "Internacional"),
            total_stay = stays_in_weekend_nights + stays_in_week_nights
        ),
    aes(x = origin, y = total_stay, fill = origin)
) +
    geom_violin(alpha = 0.7) +
    geom_boxplot(alpha = 0.7, outliers = FALSE) +
    coord_flip() +
    labs(
        title = "Durada d'estada: local vs. extern",
        x = "Origen",
        y = "Nits d'estada"
    ) +
    theme_minimal() +
    theme(
        plot.title = element_text(hjust = 0, size = 36),
        axis.text = element_text(size = 18),
        panel.background = element_rect(fill = "transparent", color = NA),
        plot.background = element_rect(fill = "transparent", color = NA),
        legend.position = "none",
        axis.title = element_blank()
    )

# ggsave("img/stay_by_origin.svg",
#     plot = last_plot(),
#     width = 11.2, height = 9, # aspect ratio 16:9, width x 0.7 (storytell area)
#     bg = "transparent"
# )

Més Enllà de TA/TO: Construir Canals Alternatius

# Gràfica: Distribució de reserves per canal i cancel·lacions
channel_distribution <- xx |>
    group_by(booking_changes) |>
    summarise(
        name = first(booking_changes),
        total_bookings = n(),
        cancellation_rate = round(mean(as.numeric(is_canceled) - 1) * 100, 1),
        percentage = round(n() / nrow(xx) * 100, 1)
    ) |>
    arrange(desc(total_bookings))
channel_distribution
## # A tibble: 18 × 5
##    booking_changes  name total_bookings cancellation_rate percentage
##              <int> <int>          <int>             <dbl>      <dbl>
##  1               0     0          90693              42         85.3
##  2               1     1          11081              14.8       10.4
##  3               2     2           3233              20          3  
##  4               3     3            772              15.9        0.7
##  5               4     4            300              19          0.3
##  6               5     5             90              15.6        0.1
##  7               6     6             48              33.3        0  
##  8               7     7             24              12.5        0  
##  9               8     8             12              33.3        0  
## 10               9     9              7              14.3        0  
## 11              10    10              6              16.7        0  
## 12              13    13              4               0          0  
## 13              14    14              2              50          0  
## 14              15    15              2               0          0  
## 15              11    11              1               0          0  
## 16              12    12              1               0          0  
## 17              16    16              1             100          0  
## 18              17    17              1               0          0
# grafica de bombolles de channel_distribution
ggplot(channel_distribution, aes(
    x = booking_changes,
    y = cancellation_rate,
    size = total_bookings,
    color = booking_changes
)) +
    geom_point(alpha = 0.7) +
    labs(
        title = "Distribució de reserves: Flexibilitat de clients",
        x = "Canvis en la reserva",
        y = "Taxa de cancel·lació (%)",
        size = "Total de reserves",
        color = "Canvis en la reserva"
    ) +
    geom_text(aes(label = ifelse(percentage > 10, paste0(percentage, "%"), "")),
        vjust = -1, size = 6
    ) +
    theme_minimal() +
    theme(
        plot.title = element_text(hjust = 0, size = 36),
        axis.text = element_text(size = 18),
        panel.background = element_rect(fill = "transparent", color = NA),
        plot.background = element_rect(fill = "transparent", color = NA),
        legend.position = "right"
    )

# ggsave("img/client_flexibility_bubble.svg",
#     plot = last_plot(),
#     width = 11.2, height = 9, # aspect ratio 16:9, width x 0.7 (storytell area)
#     bg = "transparent"
# )

# grafica de sectors de channel_distribution
ggplot(channel_distribution, aes(x = "", y = percentage, fill = total_bookings)) +
    geom_col(width = 1, color = "white") +
    coord_polar(theta = "y") +
    labs(
        title = "Distribució de reserves: Flexibilitat de clients",
        fill = "Tipus de client"
    ) +
    theme_minimal() +
    theme(
        plot.title = element_text(hjust = 0, size = 36),
        axis.text = element_text(size = 18),
        panel.background = element_rect(fill = "transparent", color = NA),
        plot.background = element_rect(fill = "transparent", color = NA),
        legend.position = "right",
        axis.title = element_blank(),
        axis.ticks = element_blank(),
        axis.text.x = element_blank()
    )

# ggsave("img/client_flexibility_pie.svg",
#     plot = last_plot(),
#     width = 11.2, height = 9, # aspect ratio 16:9, width x 0.7 (storytell area)
#     bg = "transparent"
# )

# Simplificar a tres categories: 0, 1-2, 3+
channel_summary <- xx |>
    mutate(channel_type = case_when(
        booking_changes == 0 ~ "Sense canvis",
        booking_changes >= 1 & booking_changes <= 2 ~ "1-2 canvis",
        booking_changes >= 3 ~ "3+ canvis"
    )) |>
    group_by(channel_type) |>
    summarise(
        total_bookings = n(),
        cancellation_rate = round(mean(as.numeric(is_canceled) - 1) * 100, 1),
        percentage = round(n() / nrow(xx) * 100, 1)
    )
channel_summary
## # A tibble: 3 × 4
##   channel_type total_bookings cancellation_rate percentage
##   <chr>                 <int>             <dbl>      <dbl>
## 1 1-2 canvis            14314              15.9       13.5
## 2 3+ canvis              1271              17.4        1.2
## 3 Sense canvis          90693              42         85.3
ggplot(channel_summary, aes(x = reorder(channel_type, -total_bookings), y = percentage, fill = channel_type)) +
    geom_col() +
    geom_text(aes(label = paste0(percentage, "%")), vjust = -0.5, size = 5) +
    labs(
        title = "Distribució de reserves: Flexibilitat de clients",
        x = "Tipus de client",
        y = "Percentatge de reserves (%)"
    ) +
    theme_void() +
    theme(
        plot.title = element_text(hjust = 0, size = 36),
        axis.text = element_text(size = 18),
        panel.background = element_rect(fill = "transparent", color = NA),
        plot.background = element_rect(fill = "transparent", color = NA),
        legend.position = "none",
        axis.title = element_blank()
    )

# ggsave("img/client_flexibility.svg",
#     plot = last_plot(),
#     width = 11.2, height = 9, # aspect ratio 16:9, width x 0.7 (storytell area)
#     bg = "transparent"
# )