Caricamento del dataset

data_file = read.csv("realestate_texas.csv")

str(data_file)
## 'data.frame':    240 obs. of  8 variables:
##  $ city            : chr  "Beaumont" "Beaumont" "Beaumont" "Beaumont" ...
##  $ year            : int  2010 2010 2010 2010 2010 2010 2010 2010 2010 2010 ...
##  $ month           : int  1 2 3 4 5 6 7 8 9 10 ...
##  $ sales           : int  83 108 182 200 202 189 164 174 124 150 ...
##  $ volume          : num  14.2 17.7 28.7 26.8 28.8 ...
##  $ median_price    : num  163800 138200 122400 123200 123100 ...
##  $ listings        : int  1533 1586 1689 1708 1771 1803 1857 1830 1829 1779 ...
##  $ months_inventory: num  9.5 10 10.6 10.6 10.9 11.1 11.7 11.6 11.7 11.5 ...

Tipologia di variabili e analisi associate

  1. city: qualitativa nominale; indica la città di riferimento; analisi della frequenza con barplot e confronti tra variabili.
  2. year: quantitativa discreta; indica l’anno corrispondente; analisi del trend con linechart.
  3. month: quantitativa discreta; indica il mese corrispondente; analisi della stagionalità delle vendite con boxplot.
  4. sales: quantitativa discreta; indica il numero di vendite totali; analisi della distribuzione e misura degli indici relativi.
  5. volume: quantitativa continua; indica il valore totale delle vendite; analisi del trend e misura degli indici relativi.
  6. median_price: quantitativa continua; indica il prezzo mediano degli immobili venduti; analisi del trend e misura degli indici relativi.
  7. listings: quantitativa discreta; indica il numero totale degli annunci attivi; confronto con la variabile sales e misura degli indici relativi.
  8. months_inventory: quantitativa continua; indica la quantità di tempo necessaria per vendere tutte le inserzioni correnti; analisi del trend e misura degli indici relativi.

Gestione delle variabili temporali (raggruppamento in unica variabile)

data_file$date = as.Date(paste(data_file$year, data_file$month, "01", sep = "-"))

str(data_file$date)
##  Date[1:240], format: "2010-01-01" "2010-02-01" "2010-03-01" "2010-04-01" "2010-05-01" ...

Calcolo indici di posizione, variabilità e forma

library(moments)

quant_var = c("sales", "volume", "median_price", "listings", "months_inventory")

summary(data_file[quant_var]) # minimo, massimo, quartili, mediana, media
##      sales           volume        median_price       listings   
##  Min.   : 79.0   Min.   : 8.166   Min.   : 73800   Min.   : 743  
##  1st Qu.:127.0   1st Qu.:17.660   1st Qu.:117300   1st Qu.:1026  
##  Median :175.5   Median :27.062   Median :134500   Median :1618  
##  Mean   :192.3   Mean   :31.005   Mean   :132665   Mean   :1738  
##  3rd Qu.:247.0   3rd Qu.:40.893   3rd Qu.:150050   3rd Qu.:2056  
##  Max.   :423.0   Max.   :83.547   Max.   :180000   Max.   :3296  
##  months_inventory
##  Min.   : 3.400  
##  1st Qu.: 7.800  
##  Median : 8.950  
##  Mean   : 9.193  
##  3rd Qu.:10.950  
##  Max.   :14.900
apply(data_file[quant_var], 2, function(x){any(x) <= 0}) # controllo zeri
## Warning in any(x): coercizione argomento di tipo 'double' in logico
## Warning in any(x): coercizione argomento di tipo 'double' in logico
## Warning in any(x): coercizione argomento di tipo 'double' in logico
## Warning in any(x): coercizione argomento di tipo 'double' in logico
## Warning in any(x): coercizione argomento di tipo 'double' in logico
##            sales           volume     median_price         listings 
##            FALSE            FALSE            FALSE            FALSE 
## months_inventory 
##            FALSE
apply(data_file[quant_var], 2, function(x){exp(mean(log(x)))}) # media geometrica
##            sales           volume     median_price         listings 
##     1.768815e+02     2.686046e+01     1.305956e+05     1.585449e+03 
## months_inventory 
##     8.880933e+00
apply(data_file[quant_var], 2, var) # varianza
##            sales           volume     median_price         listings 
##     6.344300e+03     2.772707e+02     5.135730e+08     5.665690e+05 
## months_inventory 
##     5.306889e+00
apply(data_file[quant_var], 2, sd) # deviazione standard
##            sales           volume     median_price         listings 
##        79.651111        16.651447     22662.148687       752.707756 
## months_inventory 
##         2.303669
apply(data_file[quant_var], 2, function(x){(sd(x)/mean(x))*100}) # coefficiente di variazione
##            sales           volume     median_price         listings 
##         41.42203         53.70536         17.08218         43.30833 
## months_inventory 
##         25.06031
apply(data_file[quant_var], 2, function(x){max(x)-min(x)}) # range
##            sales           volume     median_price         listings 
##          344.000           75.381       106200.000         2553.000 
## months_inventory 
##           11.500
apply(data_file[quant_var], 2, IQR) # differenza interquartile
##            sales           volume     median_price         listings 
##         120.0000          23.2335       32750.0000        1029.5000 
## months_inventory 
##           3.1500
apply(data_file[quant_var], 2, skewness) # asimmetria
##            sales           volume     median_price         listings 
##       0.71810402       0.88474203      -0.36455288       0.64949823 
## months_inventory 
##       0.04097527
apply(data_file[quant_var], 2, kurtosis) # curtosi (centrata sul 3)
##            sales           volume     median_price         listings 
##         2.686824         3.176987         2.377038         2.208210 
## months_inventory 
##         2.825552
  1. Variabile sales: il range delle vendite è compreso tra 79 e 423 vendite mensili, con media maggiore rispetto alla mediana. La variabilità è del 41.4%, mentre l’asimmetria risulta essere positiva (valori e modalità più basse maggiormente frequenti).
  2. Variabile volume: range non particolarmente ampio, con marcata variabilità (del 53.7%), e distribuzione asimmetrica positiva. L’unica variabile con curtosi maggiore di zero (leptocurtica), e dunque una forma della distribuzione più allungata.
  3. Variabile median_price: media e mediana sono molto simili, e il range è il più ampio, diversamente dalla variabilità che risulta essere la più ridotta (cioè del 17.1%). In ultimo si osserva asimmetria negativa, con valori e modalità più alte maggiormente frequenti (mediana > media).
  4. Variabile listings: variabilità del 43,3% (piuttosto alta), asimmetria positiva e curtosi platicurtica (distribuzione appiattita).
  5. Variabile months_inventory: variabile con il range più basso, caratterizzata da una distribuzione quasi simmetrica, curtosi negativa (ma prossima allo zero), e variabilità del 25,1%.

Distribuzioni di frequenza

cat_var = c("city", "date")

date_city_freq_dist = lapply(cat_var, function(x){
  n = table(data_file[[x]]) # frequenza assoluta
  f = prop.table(n) # frequenza relativa
  data_file_out = data.frame(
    variable_name = names(n),
    ni = as.vector(n),
    fi = as.vector(f)
  )
  data_file_out$Ni = cumsum(data_file_out$ni) # frequenza cumulata
  data_file_out$Fi = cumsum(data_file_out$fi) # frequenza relativa cumulata
  return(data_file_out)
})
  1. Variabile city: la distribuzione è uniforme, con stesse frequenze assolute e relative per ciascuna città. Le frequenze cumulate crescono in modo lineare, e la variabile presa singolarmente non risulta essere informativa. L’unica informazione ottenibile è che non ci sono dati mancanti per città.
  2. Variabile date: anche in questo caso frequenza assoluta e relativa sono costanti (e le frequenze cumulate crescono linearmente). Come per la variabile city, anche la variabile date non fornisce delle informazioni aggiuntive, ma conferma il fatto che non ci sono dati mancanti.

Variabili con maggior variabilità e asimmetria

La variabile con maggior variabilità è “volume”, come si può evincere dal coefficiente di variazione (indice ottimale per confrontare variabili su scale differenti). In altre parole la variabile volume è caratterizzata da valori che si discostano maggiormente dal suo valor medio (elevato grado di dispersione). La variabile con maggior asimmetria è “volume”, proprietà osservabile dal risultato dell’indice skewness. Ciò indica la possibile presenza di outlier, o comunque la possibilità che i valori siano sbilanciati verso una delle due parti della distribuzione. Nel caso specifico della variabile volume, l’indice misurato risulta essere positivo, per cui la distribuzione è asimmetrica positiva (valori più bassi più frequenti, media > mediana > moda).

Suddivisione in classi della variabile sales

sales_classes = cut(data_file$sales,
                breaks = c(1,100,200,300,400,500),
                labels = c("1-100", "101-200", "201-300", "301-400", "401-500"),
                right = TRUE,
                include.lowest = TRUE)

s_ni = table(sales_classes)
s_fi = prop.table(s_ni)
s_Ni = cumsum(s_ni)
s_Fi = cumsum(s_fi)

sales_freq_dist = data.frame(Class = names(s_ni),
                             ni = as.numeric(s_ni),
                             fi = round(as.numeric(s_fi), 4),
                             Ni = as.numeric(s_Ni),
                             Fi = round(as.numeric(s_Fi), 4)
                             )

sales_barplot = barplot(height = sales_freq_dist$fi,
                       names.arg = sales_freq_dist$Class,
                       col = "lightblue",
                       main = "Distribuzione delle vendite per classe",
                       xlab = "Classi",
                       ylab = "Frequenza relativa",
                       border = "black",
                       ylim = c(0,0.6)
)

text(x = sales_barplot,
     y = sales_freq_dist$fi,
     label = round(sales_freq_dist$fi, 3),
     pos = 3
     )

Gini.index = function(x){
  ni = table(x)
  fi = table(x)/length(x)
  fi2 = fi^2
  J = length(table(x))
  Gini = 1-sum(fi2)
  norm_Gini = Gini/((J-1)/J)
  return(norm_Gini)
}

Gini.index(data_file$sales)
## [1] 0.998379
Gini.index(sales_classes)
## [1] 0.7796441

Discussione dei risultati

La distribuzione di frequenze mostra che la classe 101-200 registra (in termini di frequenza assoluta e frequenza relativa) i valori più alti, seguita poi dalla classe 201-300. Da ciò si evince che mensilmente il numero delle vendite si concentra nelle classi intermedie, e sono meno frequenti le classi agli estremi della distribuzione (infatti sono rari i casi in cui si registrano più di 400 vendite per mese). L’indice di Gini calcolato sulla variabile sales è 0.78, ovvero prossimo a 1. Tale valore indica una disuguaglianza significativa tra le varie classi (eterogeneità elevata), in accordo con ciò che si osserva dalla distribuzione di frequenze.

Calcolo probabilità

p_Beaumont = sum(data_file$city == "Beaumont")/nrow(data_file)

p_July = sum(data_file$month == "7")/nrow(data_file)

p_Dec2012 = sum(data_file$date == "2012-12-01")/nrow(data_file)

I risultati sono in accordo con le distribuzioni di frequenze.

Prezzo medio immobili ed efficacia annunci

data_file$average_price = (data_file$volume*10^6)/data_file$sales



sales_listings_ratio = data_file$sales/data_file$listings

price_score = data_file$median_price/max(data_file$median_price)

time_score = 1/data_file$months_inventory

data_file$efficacy = (sales_listings_ratio + price_score + time_score)/3

La variabile efficacy misura l’efficacia degli annunci tenendo conto di alcune variabili del data set, e producendo un risultato compreso tra 0 e 1 (per cui 0 rappresenta la totale inefficacia degli annunci, mentre 1 l’efficacia del 100%). Le variabili tenute in considerazione sono sales, listings, median_price e months_inventory. La variabile sales tiene conto del numero di immobili venduti, per cui un maggior numero di immobili rappresenta una maggior efficacia, tuttavia è necessario rapportarlo alla variabile listings (numero di annunci attivi in quell’intervallo temporale) dal momento in cui un rapporto più elevato suggerisce che la maggior parte degli immobili presenti sul mercato sono stati venduti. La variabile median_price invece rappresenta una sorta di grado di difficoltà di vendita, infatti se il rapporto tra annunci venduti e annunci disponibili è pari, l’efficacia può considerarsi maggiore per un prezzo mediano maggiore. In ultimo la quantità di mesi trascorsi per vendere tutti gli annunci attivi, ossia la variabile months_inventory, per la quale si ha che un valore minore rappresenta una maggior efficienza di vendita.

summary(data_file$efficacy)
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##  0.2085  0.2925  0.3151  0.3243  0.3517  0.5300
var(data_file$efficacy)
## [1] 0.002914934
sd(data_file$efficacy)
## [1] 0.05399013
(sd(data_file$efficacy)/mean(data_file$efficacy))*100
## [1] 16.64704
skewness(data_file$efficacy)
## [1] 1.077353
kurtosis(data_file$efficacy)
## [1] 4.77061

Dai risultati si evince che in media l’efficacia è del 34.4% (molto simile alla mediana), con un’efficacia minima del 20.8% e massima del 53.0%. La variabilità dei dati relativi a tale variabile è del 16,6% (piuttosto contenuta), e la distribuzione risulta essere asimmetrica positiva leptocurtica.

Analisi condizionata

library(dplyr)
## 
## Caricamento pacchetto: 'dplyr'
## I seguenti oggetti sono mascherati da 'package:stats':
## 
##     filter, lag
## I seguenti oggetti sono mascherati da 'package:base':
## 
##     intersect, setdiff, setequal, union
city_summary = data_file %>%
  group_by(city) %>%
  summarise(sales_mean = mean(sales),
            sales_sd = sd(sales),
            volume_mean = mean(volume),
            volume_sd = sd(volume),
            median_price_mean = mean(median_price),
            median_price_sd = sd(median_price),
            listings_mean = mean(listings),
            listings_sd = sd(listings),
            months_inventory_mean = mean(months_inventory),
            months_inventory_sd = sd(months_inventory))

year_summary = data_file %>%
  group_by(year) %>%
  summarise(sales_mean = mean(sales),
            sales_sd = sd(sales),
            volume_mean = mean(volume),
            volume_sd = sd(volume),
            median_price_mean = mean(median_price),
            median_price_sd = sd(median_price),
            listings_mean = mean(listings),
            listings_sd = sd(listings),
            months_inventory_mean = mean(months_inventory),
            months_inventory_sd = sd(months_inventory))

month_summary = data_file %>%
  group_by(month) %>%
  summarise(sales_mean = mean(sales),
            sales_sd = sd(sales),
            volume_mean = mean(volume),
            volume_sd = sd(volume),
            median_price_mean = mean(median_price),
            median_price_sd = sd(median_price),
            listings_mean = mean(listings),
            listings_sd = sd(listings),
            months_inventory_mean = mean(months_inventory),
            months_inventory_sd = sd(months_inventory))



library(ggplot2)

library(tidyr)

s_l_city_plot = city_summary %>%
  select(city, sales_mean, listings_mean) %>%
  pivot_longer(cols = c(sales_mean, listings_mean),
               names_to = "variable",
               values_to = "value")

ggplot(s_l_city_plot, aes(x = city, y = value, fill = variable)) +
  geom_col(position = position_dodge(width = 0.8)) +
  geom_text(aes(label = round(value, 1)),
            position = position_dodge(width = 0.8),
            vjust = -0.3,
            size = 3.5) +
  labs(title = "Vendite/Annunci per città",
       x = "Città",
       y = "Media di Vendite/Annunci") +
  scale_fill_manual(values = c("sales_mean" = "lightblue",
                               "listings_mean" = "darkred"),
                    labels = c("Annunci", "Vendite")) +
  theme_classic(base_size = 10) +
  theme(
    axis.text.x = element_text(angle = 0, vjust = 0.5),
    legend.title = element_blank(),
    plot.title = element_text(face = "bold")
  )

ggplot(city_summary, aes(x = city, y = median_price_mean, fill = median_price_mean)) +
  geom_col() +
  geom_errorbar(aes(ymin = median_price_mean - median_price_sd,
                    ymax = median_price_mean + median_price_sd),
                width = 0.2,
                color = "black") +
  geom_text(aes(label = round(median_price_mean, 0)),
            vjust = -3,
            size = 3.5) +
  scale_fill_gradient(low = "lightgreen", high = "darkgreen") +
  labs(title = "Prezzo Mediano per città",
       x = "Città",
       y = "Prezzo Mediano Medio") +
  scale_y_continuous(breaks = seq(0, 160000, by=40000), limits = c(0, 180000)) +
  theme_classic(base_size = 10) +
  theme(
    axis.text.x = element_text(angle = 0, vjust = 0.5),
    legend.position = "none",
    plot.title = element_text(face = "bold")
  )

ggplot(city_summary, aes(x = city, y = months_inventory_mean, fill = months_inventory_mean)) +
  geom_col() + 
  geom_errorbar(aes(ymin = months_inventory_mean - months_inventory_sd,
                    ymax = months_inventory_mean + months_inventory_sd),
                width = 0.2,
                color = "black") +
  geom_text(aes(label = round(months_inventory_mean, 1)),
            vjust = -0.5,
            size = 5) +
  scale_fill_gradient(low = "lightpink", high = "darkred") +
  scale_y_continuous(breaks = seq(0, 14, by=2), limits = c(0, 14)) +
  labs(title = "Tempistiche di Vendita per città",
       x = "Città",
       y = "Mesi impiegati per vendere totale immobili") +
  theme_classic(base_size = 10) +
  theme(
    axis.text.x = element_text(angle = 0, vjust = 0.5),
    legend.position = "none",
    plot.title = element_text(face = "bold")
  )

ggplot(year_summary, aes(x = year, y = sales_mean)) +
  geom_line(color = "red", size = 0.8) +
  geom_point(color = "darkred", size = 3) +
  geom_text(aes(label = round(sales_mean, 0)), 
            vjust = -2, size = 3.5) +
  labs(title = "Andamento temporale delle Vendite",
       x = "Anno",
       y = "Media delle Vendite") +
  scale_y_continuous(breaks = seq(100, 250, by=50), limits = c(100, 260)) +
  theme_bw(base_size = 12) +
  theme(
    plot.title = element_text(face = "bold"),
    axis.text.x = element_text(angle = 0, vjust = 0.5)
  )
## 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.

ggplot(year_summary, aes(x = year, y = median_price_mean)) +
  geom_line(color = "lightblue", size = 0.8) +
  geom_point(color = "darkblue", size = 3) +
  geom_text(aes(label = round(median_price_mean, 0)), 
            vjust = -2, size = 3.5) +
  labs(title = "Andamento temporale del Prezzo Mediano",
       x = "Anno",
       y = "Media del Prezzo Mediano") +
  scale_y_continuous(breaks = seq(100000, 160000, by=20000), limits = c(100000, 160000)) +
  theme_bw(base_size = 12) +
  theme(
    plot.title = element_text(face = "bold"),
    axis.text.x = element_text(angle = 0, vjust = 0.5)
  )

ggplot(month_summary, aes(x = month, y = sales_mean)) +
  geom_line(color = "violet", size = 1) +
  geom_point(color = "darkviolet", size = 3) +
  geom_text(aes(label = round(sales_mean, 1)), 
            vjust = -2, size = 3) +
  labs(
    title = "Vendite Mensili",
    x = "Mese",
    y = "Media delle Vendite"
  ) +
  scale_x_continuous(breaks = seq(0, 12, by=1), limits = c(1, 12)) +
  scale_y_continuous(breaks = seq(100, 250, by=50), limits = c(100, 280)) +
  theme_bw(base_size = 12) +
  theme(
    plot.title = element_text(face = "bold"),
    axis.text.x = element_text(angle = 0, vjust = 0.5)
  )

ggplot(month_summary, aes(x = month)) +
  geom_line(aes(y = median_price_mean, color = "Prezzo Mediano Medio"), size = 1) +
  geom_line(aes(y = sales_mean * 1000, color = "Media Vendite"), size = 1) +
  geom_text(aes(y = median_price_mean, label = round(median_price_mean, 0), color = "Prezzo Mediano Medio"),
            vjust = -0.5, size = 3) +
  geom_text(aes(y = sales_mean * 1000, label = round(sales_mean, 0), color = "Media Vendite"),
            vjust = -0.5, size = 3) +
  scale_color_manual(
    name = "Variabili:",
    values = c("Prezzo Mediano Medio" = "blue",
               "Media Vendite" = "red")) +
  scale_y_continuous(
    name = "Media del Prezzo Mediano",
    breaks = seq(125000, 250000, 50000),
    limits = c(120000, 250000),
    sec.axis = sec_axis(~ . / 1000, name = "Media delle Vendite")) +
  labs(
    title = "Prezzi vs Vendite",
    x = "Mese") +
  scale_x_continuous(breaks = seq(1, 12, 1), limits = c(1, 13)) +
  theme_bw(base_size = 12) +
  theme(plot.title = element_text(face = "bold"),
        axis.text.x = element_text(angle = 0, vjust = 0.5),
        legend.position = "bottom")

month_plot = month_summary %>%
  select(month, median_price_mean, volume_mean) %>%
  pivot_longer(cols = c(median_price_mean, volume_mean),
               names_to = "variable",
               values_to = "value")

max_price = max(month_summary$median_price_mean)
max_volume = max(month_summary$volume_mean)
scale_factor = max_price / max_volume

ggplot(month_plot, aes(x = month)) +
  geom_line(aes(y = ifelse(variable == "volume_mean", value * scale_factor, value), 
                color = variable), size = 1) +
  geom_point(aes(y = ifelse(variable == "volume_mean", value * scale_factor, value), 
                 color = variable), size = 3) +
  geom_text(aes(y = ifelse(variable == "volume_mean", value * scale_factor, value),
                label = round(value, 1), color = variable),
            vjust = -1, size = 3, check_overlap = FALSE) +
  scale_color_manual(
    name = "Variabili",
    values = c("median_price_mean" = "blue", "volume_mean" = "red"),
    labels = c("Prezzo Mediano Medio", "Ricavato Totale Medio delle Vendite (Milioni USD)")) +
  scale_y_continuous(
    name = "Prezzo Mediano Medio (USD)",
    sec.axis = sec_axis(~ . / scale_factor, 
                        name = "Ricavato Totale Medio delle Vendite (Milioni USD)")) +
  scale_x_continuous(breaks = 1:12) +
  labs(title = "Prezzo Mediano vs Ricavato Totale delle Vendite Mensili",
       x = "Mese") +
  theme_bw(base_size = 12) +
  theme(
    plot.title = element_text(face = "bold"),
    axis.text.x = element_text(angle = 0, vjust = 0.5),
    legend.position = "bottom")

Visualizzazioni con ggplot2

ggplot(data_file, aes(x = city, y = median_price, fill = city)) +
  geom_boxplot() +
  labs(
    title = "Distribuzione del Prezzo Mediano per Città",
    x = "Città",
    y = "Prezzo Mediano (USD)") +
  theme_classic(base_size = 10) +
  theme(
    axis.text.x = element_text(angle = 0, hjust = 0.5),
    legend.position = "none",
    plot.title = element_text(face = "bold"))

Le città presentano livelli del prezzo mediano differenti. Alcune città mostrano box più compatti (maggior omogeneità del mercato), mentre altre sono caratterizzate da outlier e una più ampia dispersione (indice di maggiore variabilità del prezzo).

ggplot(data_file, aes(x = city, y = volume, fill = city)) +
  geom_boxplot(outlier.colour = "black", outlier.size = 1.5) +
  labs(
    title = "Distribuzione del Ricavato Totale delle Vendite per Città",
    x = "Città",
    y = "Ricavato delle Vendite (Milioni USD)") +
  theme_classic(base_size = 10) +
  theme(
    legend.position = "none",
    plot.title = element_text(face = "bold"),
    axis.text.x = element_text(angle = 0, vjust = 0.5))

Le marcate differenze tra le distribuzioni evidenziano come in alcune città si concentri una quota maggiore del ricavato totale, non necessariamente per prezzi più alti, ma anche per numero di vendite (o viceversa). Inoltre box più lunghi indicano maggior volatilità del fatturato.

ggplot(data_file, aes(x = factor(year), y = volume)) +
  geom_boxplot(fill = "lightblue", outlier.colour = "black", outlier.size = 1.5) +
  labs(
    title = "Distribuzione del Ricavato Totale delle Vendite per Anno",
    x = "Anno",
    y = "Ricavato delle Vendite (Milioni USD)") +
  theme_classic(base_size = 10) +
  theme(
    plot.title = element_text(face = "bold"),
    axis.text.x = element_text(angle = 0, hjust = 0.5))

Nel tempo si osserva un progressivo allungamento delle distribuzioni, infatti la dispersione del ricavato totale è più ampia, e uno spostamento verso l’alto (anche la mediana aumenta). Questi risultati sono indici di espansione disomogenea e crescita strutturale del mercato.

ggplot(data_file, aes(x = month, y = sales, fill = city)) +
  geom_col(position = position_dodge(width = 0.8)) +
  scale_x_continuous(breaks = 1:12) +
  labs(
    title = "Totale Vendite Mensili per Città",
    x = "Mese",
    y = "Numero di Vendite",
    fill = "Città") +
  theme_classic(base_size = 10) +
  theme(
    plot.title = element_text(face = "bold"),
    axis.text.x = element_text(angle = 0, vjust = 0.5))

sales_month_city = data_file %>%
  group_by(month, city) %>%
  summarise(total_sales = sum(sales), .groups = "drop")

ggplot(sales_month_city,
       aes(x = factor(month), y = total_sales, fill = city)) +
  geom_col() +
  scale_x_discrete(drop = FALSE) +
  labs(
    title = "Totale delle Vendite Mensili per Città",
    x = "Mese",
    y = "Numero Totale di Vendite",
    fill = "Città") +
  theme_classic(base_size = 10) +
  theme(
    plot.title = element_text(face = "bold"),
    axis.text.x = element_text(angle = 0))

Per ciascuna città è possibile osservare andamenti stagionali simili, con un aumento delle vendite durante il periodo primaverile/estivo, e un decremento nel periodo autunnale/invernale. In alcuni casi la variazione è più marcata, mentre in altre città il livello delle vendite si mantiene pressoché costante.

ggplot(sales_month_city,
       aes(x = factor(month), y = total_sales, fill = city)) +
  geom_col(position = "fill") +
  scale_y_continuous(labels = scales::percent) +
  scale_x_discrete(drop = FALSE) +
  labs(
    title = "Distribuzione Percentuale delle Vendite Mensili per Città",
    x = "Mese",
    y = "Quota Percentuale delle Vendite",
    fill = "Città") +
  theme_classic(base_size = 10) +
  theme(
    plot.title = element_text(face = "bold"),
    axis.text.x = element_text(angle = 0))

Tramite normalizzazione si può verificare con quale peso incidono le variazioni stagionali per ciascuna città sul totale delle vendite, indipendentemente dalla crescita del numero. Si osserva infatti come alcune città contribuiscano in particolar modo alla crescita del numero totale di vendite in funzione del mese, mentre altre risultano essere meno soggette a tale fenomeno.

ggplot(data_file, aes(x = date, y = sales, color = city)) +
  geom_line(size = 1) +
  labs(
    title = "Andamento Storico delle Vendite Immobiliari in Texas",
    x = "Data",
    y = "Numero di Vendite",
    color = "Città") +
  scale_x_date(date_labels = "%Y-%m",
               date_breaks = "3 months",
               limits = as.Date(c("2010-01-01", max(data_file$date)))) +
  theme_bw(base_size = 10) +
  theme(
    plot.title = element_text(face = "bold", hjust = 0.5),
    axis.text.x = element_text(angle = 60, hjust = 1),
    legend.position = "bottom")

Il confronto temporale mostra che le città non seguono traiettorie identiche nel tempo, bensì alcune sono caratterizzate da una crescita più regolare e continua, mentre altre presentano fasi di accelerazione più marcata. Questo risultato indica che il ciclo immobiliare non è perfettamente sincronizzato a livello geografico, ma risente delle dinamiche locali.

SINTESI DEI RISULTATI E CONSIDERAZIONI STATISTICHE SULL’ANALISI DEL DATASET

Il mercato immobiliare analizzato mostra un quadro generalmente espansivo. Le vendite crescono in modo piuttosto costante nel tempo, sia in senso stretto (numero di transazioni), sia in senso lato (totale del ricavato), suggerendo un settore con domanda sostenuta e senza evidenti shock recessivi nel periodo coperto dal dataset. La stagionalità risulta evidente: i volumi di vendita tendono a concentrarsi nel periodo primaverile ed estivo, con attenuazioni dell’attività nei mesi più freddi. Questo pattern stagionale orienta le imprese a pianificare pricing e promozioni tenendo conto dei picchi annuali.

Le città invece si differenziano sia per intensità delle vendite, che per livelli di prezzo, infatti alcune piazze mostrano medie di vendita più alte, mentre altre si distinguono per prezzi mediani superiori. La combinazione tra volume e prezzo mediano non è uniforme tra le città, di fatto è possibile osservare come in alcuni casi il volume sia alto, ma il prezzo mediano risulti basso (tante vendite, ma a prezzi ridotti), mentre in altri casi è il volume a risultare basso, con un prezzo mediano più elevato (poche vendite, ma con prezzi alti). Questo suggerisce che le strategie di mercato efficaci non siano trasferibili da una città all’altra, a meno che non si effettuino degli adattamenti. Inoltre, la relazione tra prezzi e numero di vendite non appare inversa; al contrario le due variabili si muovono in modo tendenzialmente concorde nel medio periodo, come evidenziato dai grafici mensili e annuali. Tale dinamica suggerisce che la domanda sia sufficientemente forte da assorbire livelli di prezzo crescenti, evitando compressioni del volume.

La variabile listings e l’indicatore months_inventory confermano un mercato relativamente attivo. Le tempistiche di vendita non risultano eccessivamente lunghe e non seguono trend di deterioramento. La stabilità della variabie inventory indica l’esistenza di un equilibrio tra domanda e offerta (nonostante i prezzi crescenti): l’offerta non appare strutturalmente insufficiente, né sovrabbondante, e contribuisce a consolidare la crescita del mercato.

Nel lungo periodo è possibile osservare un’evoluzione più robusta del prezzo mediano rispetto al numero di vendite. Ciò è coerente con il ciclo espansivo immobiliare: prima la domanda aumenta in quantità, poi i prezzi seguono e si consolidano. Non emergono crolli o inversioni, e le città che partono da livelli di prezzo inferiori mostrano recuperi più marcati nel tempo, riducendo il divario con le città storicamente più care.

Una lettura congiunta dei risultati supporta quindi tre punti chiave. Primo: il mercato del periodo analizzato non sembra essere soggetto ad alcuna speculazione, in quanto volumi, prezzi e inventory si muovono coerentemente e senza segnali di stress. Secondo: la stagionalità ha un ruolo non trascurabile e può essere sfruttata in termini di ottimizzazione dei tempi di immissione dell’offerta. Terzo: la segmentazione geografica è determinante per comprendere le performance del mercato immobiliare; trattare il “Texas” come un unico mercato porterebbe a perdite di informazione e alla definizione di strategie non ottimali.

Alla luce di quanto sopra, si formulano alcune raccomandazioni operative. Nel breve periodo conviene pianificare annunci e campagne di vendita in prossimità della stagione più attiva. Nel medio periodo invece ha senso concentrare l’interesse di investimento nelle città in cui la crescita combinata di prezzo e volume è più marcata, segnale di domanda resiliente. Infine, nel lungo periodo, monitorare attentamente l’indicatore inventory potrà fornire segnali anticipatori di cambiamento del ciclo: un aumento persistente dei tempi di vendita senza crescita dell’offerta indicherebbe indebolimento della domanda, mentre una compressione dei tempi con offerta stagnante potrebbe anticipare nuove spinte al rialzo dei prezzi.