Obiettivo e dati

Questo report analizza lo storico mensile delle compravendite residenziali in quattro città del Texas (USA) tra il 2010 e il 2014. L’obiettivo è descrivere le distribuzioni di prezzi e vendite e individuare le differenze tra città, anni e mesi. La base dei dati dello studio è il file realestate_texas.csv.

Esplorazione iniziale

df <- read.csv("realestate_texas.csv", stringsAsFactors = FALSE)
str(df)
## '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 ...
cat("Righe:", nrow(df), "- Colonne:", ncol(df), "- Valori mancanti:", sum(is.na(df)), "\n")
## Righe: 240 - Colonne: 8 - Valori mancanti: 0
cat("Città:", paste(unique(df$city), collapse = ", "), "\n")
## Città: Beaumont, Bryan-College Station, Tyler, Wichita Falls
cat("Anni:", paste(sort(unique(df$year)), collapse = ", "), "\n")
## Anni: 2010, 2011, 2012, 2013, 2014
cat("Mesi:", paste(sort(unique(df$month)), collapse = ", "), "\n")
## Mesi: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12
df %>% count(city, year) %>% pivot_wider(names_from = year, values_from = n) %>%
  tab("Numero di osservazioni per città e anno", 0)
Numero di osservazioni per città e anno
city 2010 2011 2012 2013 2014
Beaumont 12 12 12 12 12
Bryan-College Station 12 12 12 12 12
Tyler 12 12 12 12 12
Wichita Falls 12 12 12 12 12

Il dataset ha 240 righe e 8 colonne, senza valori mancanti. Ogni riga è una tripla città-anno-mese. Il dataset è perfettamente bilanciato.

Classificazione delle variabili

data.frame(
  Variabile = c("city", "year", "month", "sales", "volume", "median_price", "listings", "months_inventory"),
  Tipo = c("Qualitativa nominale", "Temporale (discreta)", "Temporale (discreta, ciclica)",
           "Quantitativa discreta", "Quantitativa continua", "Quantitativa continua",
           "Quantitativa discreta", "Quantitativa continua"),
  Scala = c("Nominale", "Intervallo", "Ordinale/ciclica", "Rapporti", "Rapporti", "Rapporti", "Rapporti", "Rapporti"),
  `Analisi sensata` = c("Frequenze, moda, variabile di raggruppamento",
                        "Frequenze; asse del tempo; raggruppamento (trend)",
                        "Frequenze; raggruppamento (stagionalità); non si fa la media del numero del mese",
                        "Posizione, variabilità, forma, classi",
                        "Posizione, variabilità, forma, classi",
                        "Posizione, variabilità, forma, classi",
                        "Posizione, variabilità, forma",
                        "Posizione, variabilità, forma"),
  check.names = FALSE) %>% tab("Classificazione delle variabili", 0)
Classificazione delle variabili
Variabile Tipo Scala Analisi sensata
city Qualitativa nominale Nominale Frequenze, moda, variabile di raggruppamento
year Temporale (discreta) Intervallo Frequenze; asse del tempo; raggruppamento (trend)
month Temporale (discreta, ciclica) Ordinale/ciclica Frequenze; raggruppamento (stagionalità); non si fa la media del numero del mese
sales Quantitativa discreta Rapporti Posizione, variabilità, forma, classi
volume Quantitativa continua Rapporti Posizione, variabilità, forma, classi
median_price Quantitativa continua Rapporti Posizione, variabilità, forma, classi
listings Quantitativa discreta Rapporti Posizione, variabilità, forma
months_inventory Quantitativa continua Rapporti Posizione, variabilità, forma

Le variabili temporali. year e month sono memorizzate come numeri, ma identificano un periodo, non una quantità. Sono quindi trattate come categorie ordinate: year per leggere il trend 2010–2014, month per leggere la stagionalità, tenendo conto che è ciclica (dicembre è contiguo a gennaio). Su di esse non si calcolano indici di posizione o forma: si costruiscono distribuzioni di frequenza e si usano per raggruppare (group_by) e per gli assi dei grafici. Da entrambe si ricava la colonna date per le serie storiche.

Le variabili quantitative. sales, volume, median_price, listings e months_inventory sono su scala di rapporti (lo zero è assoluto), quindi è possibile calcolare gli indici statistici. Le variabili city, year e month, invece, si descrivono con distribuzioni di frequenza.

df <- df %>%
  mutate(date = as.Date(paste(year, month, "01", sep = "-")),
         mese = factor(month, levels = 1:12, labels = mesi_it))

Indici di posizione, variabilità e forma

Variabili qualitative e temporali: distribuzioni di frequenza

freq <- function(v, nome) {
  df %>% count(.data[[v]], name = "Frequenza assoluta") %>%
    mutate(`Frequenza relativa` = `Frequenza assoluta` / sum(`Frequenza assoluta`)) %>%
    rename(!!nome := 1)
}
freq("city", "Città") %>% tab("Frequenze della città", 3)
Frequenze della città
Città Frequenza assoluta Frequenza relativa
Beaumont 60 0.25
Bryan-College Station 60 0.25
Tyler 60 0.25
Wichita Falls 60 0.25
freq("year", "Anno") %>% tab("Frequenze dell'anno", 3)
Frequenze dell’anno
Anno Frequenza assoluta Frequenza relativa
2010 48 0.2
2011 48 0.2
2012 48 0.2
2013 48 0.2
2014 48 0.2
freq("month", "Mese") %>% tab("Frequenze del mese", 3)
Frequenze del mese
Mese Frequenza assoluta Frequenza relativa
1 20 0.083
2 20 0.083
3 20 0.083
4 20 0.083
5 20 0.083
6 20 0.083
7 20 0.083
8 20 0.083
9 20 0.083
10 20 0.083
11 20 0.083
12 20 0.083

Le tre distribuzioni sono perfettamente uniformi (25% per ogni città, 20% per ogni anno, 8.3% per ogni mese).

Variabili quantitative

vars <- c("sales", "volume", "median_price", "listings", "months_inventory")

stats_table <- map_dfr(vars, function(v) {
  x <- df[[v]]
  tibble(Variabile = v, Media = mean(x), Mediana = median(x), Min = min(x), Max = max(x),
         `Dev. std` = sd(x), IQR = IQR(x), CV = sd(x) / mean(x),
         Asimmetria = skewness(x), Curtosi = kurtosis(x)-3)
})
stats_table %>% tab("Indici descrittivi delle variabili quantitative", 3)
Indici descrittivi delle variabili quantitative
Variabile Media Mediana Min Max Dev. std IQR CV Asimmetria Curtosi
sales 192.292 175.500 79.000 423.000 79.651 120.000 0.414 0.718 -0.313
volume 31.005 27.062 8.166 83.547 16.651 23.233 0.537 0.885 0.177
median_price 132665.417 134500.000 73800.000 180000.000 22662.149 32750.000 0.171 -0.365 -0.623
listings 1738.021 1618.500 743.000 3296.000 752.708 1029.500 0.433 0.649 -0.792
months_inventory 9.193 8.950 3.400 14.900 2.304 3.150 0.251 0.041 -0.174
long <- df %>% select(all_of(vars)) %>% pivot_longer(everything(), names_to = "variabile", values_to = "valore") %>%
  mutate(variabile = factor(variabile, levels = vars))

h <- ggplot(long, aes(valore)) +
  geom_histogram(bins = 20, fill = "#2A9D8F", colour = "white") +
  facet_wrap(~variabile, scales = "free", nrow = 1) +
  labs(title = "Istogrammi", x = NULL, y = "Frequenza") +
  theme(axis.text.x = element_text(angle = 45, hjust = 1))
b <- ggplot(long, aes(y = valore)) +
  geom_boxplot(fill = "#F4A261", alpha = .7) +
  facet_wrap(~variabile, scales = "free", nrow = 1) +
  labs(title = "Boxplot", y = NULL) +
  theme(axis.text.x = element_blank())
h / b

Posizione. Per sales, volume e listings la media supera la mediana (es. vendite: 192.3 contro 175.5): è il segnale di una coda a destra. Per median_price l’andamento è opposto: la media ($132,665) è un po’ sotto la mediana ($134,500). Per months_inventory, media e mediana sono quasi coincidenti (9.19 e 8.95 mesi).

Variabilità. Gli indici assoluti (deviazione standard, IQR) non sono confrontabili tra variabili essendo in unità di misura diverse. Il confronto avviene tramite il Coefficiente di Variazione (CV).

Forma. Le distribuzioni di sales, volume e listings hanno asimmetria positiva (code a destra), median_price ha asimmetria negativa (coda a sinistra) e months_inventory è praticamente simmetrica. La curtosi (misurata in eccesso rispetto alla normale, che vale 0) è negativa (platicurtica) per sales, median_price, listings e months_inventory: il valore più marcato è quello di listings (-0.79). Il volume, al contrario, ha curtosi positiva (leptocurtica), con valore 0.18.

In termini di mercato, l’asimmetria positiva di sales, volume e listings indica che la maggior parte delle osservazioni si concentra su livelli relativamente contenuti, mentre un numero più ristretto di mesi-città presenta valori più elevati e sposta la media verso destra. Per queste variabili la mediana può quindi rappresentare meglio un mese-città tipico rispetto alla media. I picchi stagionali e le differenze di dimensione tra città possono contribuire a questa forma, ma non ne dimostrano da soli la causa. La curtosi negativa suggerisce una distribuzione relativamente piatta e code meno pesanti rispetto alla normale; la presenza di eventuali valori anomali va valutata separatamente con i boxplot.

Queste statistiche mettono insieme quattro città molto diverse per dimensione e livello dei prezzi: prima di interpretare l’asimmetria bisogna verificare quanto dipenda da questa miscela. Si calcola quindi l’asimmetria città per città:

df %>% group_by(Città = city) %>%
  summarise(sales = skewness(sales), volume = skewness(volume), median_price = skewness(median_price)) %>%
  tab("Asimmetria calcolata separatamente per città", 2)
Asimmetria calcolata separatamente per città
Città sales volume median_price
Beaumont 0.19 0.36 0.36
Bryan-College Station 0.65 0.86 0.71
Tyler 0.14 0.35 0.12
Wichita Falls 0.32 0.19 0.22

Il confronto cambia la lettura. L’asimmetria negativa del prezzo mediano (-0.36) è verosimilmente influenzata dalla composizione congiunta di città con livelli di prezzo diversi: dentro ogni città è positiva e diventa negativa nell’insieme. Anche l’asimmetria positiva delle vendite si riduce dentro le singole città, tranne a Bryan-College Station (0.65), dove la forte stagionalità produce una vera coda a destra.

Variabilità e asimmetria a confronto

Per confrontare la variabilità tra variabili con unità diverse (numero di case, dollari, mesi) è utilizzato il coefficiente di variazione \(CV = s/\bar{x}\). L’asimmetria di Fisher è già adimensionale, quindi si confronta direttamente, è stato aggiunto il valore assoluto dell’assimetria.

stats_table %>% select(Variabile, CV, Asimmetria) %>%
  mutate(`Asimmetria (abs)` = abs(Asimmetria)) %>% arrange(desc(CV)) %>%
  tab("Confronto tra variabili: CV e asimmetria", 3)
Confronto tra variabili: CV e asimmetria
Variabile CV Asimmetria Asimmetria (abs)
volume 0.537 0.885 0.885
listings 0.433 0.649 0.649
sales 0.414 0.718 0.718
months_inventory 0.251 0.041 0.041
median_price 0.171 -0.365 0.365
v_cv   <- stats_table$Variabile[which.max(stats_table$CV)]
v_skew <- stats_table$Variabile[which.max(abs(stats_table$Asimmetria))]

La variabile con maggiore variabilità relativa è volume (CV = 0.54), seguita da listings e sales. La più asimmetrica è volume (0.88). Il prezzo mediano è invece la variabile più stabile (CV = 0.17): i prezzi variano molto meno dei volumi di attività. Per un’agenzia questo suggerisce che le oscillazioni più marcate riguardano le quantità vendute (in buona parte stagionali) più che il livello dei prezzi.

cv_city <- df %>% group_by(Città = city) %>% summarise(across(all_of(vars), ~ sd(.x) / mean(.x)))
cv_city %>% tab("Coefficiente di variazione calcolato dentro ciascuna città", 2)
Coefficiente di variazione calcolato dentro ciascuna città
Città sales volume median_price listings months_inventory
Beaumont 0.23 0.27 0.08 0.05 0.17
Bryan-College Station 0.41 0.45 0.06 0.17 0.29
Tyler 0.23 0.29 0.07 0.08 0.17
Wichita Falls 0.19 0.23 0.11 0.08 0.10

Il confronto dentro le città conferma che il prezzo è la variabile più stabile (CV tra 0.06 e 0.11), mentre vendite e volume oscillano di più. listings appare variabile nell’insieme (CV 0.43) ma è stabile dentro ogni città (da 0.05 a 0.17): la sua variabilità complessiva dipende dalle differenze di dimensione tra le città.

Classi di median_price e indice di Gini

Per calcolare l’indice di Gini, la variabile median_price è suddivisa in classi di ampiezza costante pari a $20.000 , da $60.000 a $180.000 (il minimo osservato è $73,800, il massimo $180,000). Con k = 6 classi si ha abbastanza dettaglio per vedere la forma della distribuzione. La classe più bassa (60–80k) contiene una sola osservazione: è mantenuta per conservare l’ampiezza costante e perché il minimo osservato ricade lì.

df <- df %>%
  mutate(price_class = cut(median_price, breaks = seq(60000, 180000, by = 20000),
                           include.lowest = TRUE,
                           labels = c("60–80k", "80–100k", "100–120k", "120–140k", "140–160k", "160–180k")))

freq_class <- df %>% count(price_class, name = "n", .drop = FALSE) %>%
  mutate(f = n / sum(n), `f %` = 100 * f)
freq_class %>% tab("Distribuzione di frequenze di median_price per classi", 3)
Distribuzione di frequenze di median_price per classi
price_class n f f %
60–80k 1 0.004 0.417
80–100k 25 0.104 10.417
100–120k 40 0.167 16.667
120–140k 75 0.312 31.250
140–160k 78 0.325 32.500
160–180k 21 0.088 8.750
k <- nrow(freq_class)
gini <- 1 - sum(freq_class$f^2)
gini_max <- 1 - 1 / k
gini_norm <- gini / gini_max
ggplot(freq_class, aes(price_class, n, fill = n)) +
  geom_col(width = .7) +
  geom_text(aes(label = n), vjust = -0.4) +
  scale_fill_gradient(low = "#C6DBEF", high = "#08306B", guide = "none") +
  scale_y_continuous(expand = expansion(mult = c(0, .1))) +
  labs(title = "Distribuzione del prezzo mediano per classi di $20.000",
       x = "Classe di prezzo mediano", y = "Numero di osservazioni città-mese")

L’indice di Gini di eterogeneità è \(G = 1 - \sum f_i^2 =\) 0.750, e il massimo con 6 classi è 0.833. Normalizzato: \(G_{norm} =\) 0.900. Valori vicini a 1 indicano che le osservazioni sono ripartite in modo molto uniforme tra le classi; valori vicini a 0, che quasi tutte stanno in una sola.

L’indice è alto (0.90 normalizzato), perché ci sono più fasce di prezzo con frequenze non trascurabili, ma la distribuzione non è uniforme: due classi centrali (140–160ke120–140k) raccolgono il 63.7% delle osservazioni e le code sono sottili. Il Gini dipende da numero e ampiezza delle classi, quindi è utile per confrontare, non come valore assoluto. Parte di questa varietà di fasce deriva dalla presenza di città con prezzi diversi, che il Gini non separa.

Calcolo delle probabilità

Le probabilità richieste sono:

  1. qual è la probabilità che riporti la città “Beaumont”?
  2. qual è la probabilità che riporti il mese di luglio?
  3. qual è la probabilità che riporti dicembre 2012?
N <- nrow(df)
n_b <- sum(df$city == "Beaumont")
n_l <- sum(df$month == 7)
n_d <- sum(df$month == 12 & df$year == 2012)
tibble(Evento = c("Città = Beaumont", "Mese = luglio", "Mese = dicembre E anno = 2012"),
       `Casi favorevoli` = c(n_b, n_l, n_d), `Casi totali` = N,
       Probabilità = `Casi favorevoli` / N) %>% tab("Probabilità frequentiste", 4)
Probabilità frequentiste
Evento Casi favorevoli Casi totali Probabilità
Città = Beaumont 60 240 0.2500
Mese = luglio 20 240 0.0833
Mese = dicembre E anno = 2012 4 240 0.0167
  1. P(Beaumont) = 60/240 = 0.2500 (25.0%).
  2. P(luglio) = 20/240 = 0.0833 (8.3%).
  3. P(dicembre 2012) = 4/240 = 0.0167 (1.7%).

Le probabilità coincidono con le frequenze uniformi viste prima: 1/4, 1/12 e 1/60 (= 1/12 × 1/5). Riflettono il disegno del campione, non una proprietà del mercato.

Nuove variabili

1. Prezzo medio (mean_price). La media si ottiene dal valore totale diviso per il numero di vendite: \[\text{mean_price} = \frac{\text{volume} \times 10^6}{\text{sales}}\] Rapportando al prezzo mediano, si ottiene: \[\text{price_ratio} = \frac{\text{mean_price}}{\text{median_price}}\] La variabile confronta il prezzo medio stimato con il prezzo mediano del mese-città.

2. Tasso di assorbimento dell’offerta (absorption_rate). È un indicatore sintetico del rapporto tra vendite mensili e annunci attivi: \[\text{absorption_rate} = \frac{\text{sales}}{\text{listings}} \times 100\] A parità di annunci attivi, un valore più alto indica una maggiore rotazione dell’offerta nel mese.

df <- df %>%
  mutate(mean_price = volume * 1e6 / sales,
         price_ratio = mean_price / median_price,
         absorption_rate = sales / listings * 100)

df %>% select(city, year, month, sales, listings, absorption_rate, mean_price, median_price, price_ratio) %>%
  slice(c(1:3, 61:63, 121:123, 181:183)) %>%
  tab("Esempio di righe con le nuove variabili", 2)
Esempio di righe con le nuove variabili
city year month sales listings absorption_rate mean_price median_price price_ratio
Beaumont 2010 1 83 1533 5.41 170626.5 163800 1.04
Beaumont 2010 2 108 1586 6.81 163796.3 138200 1.19
Beaumont 2010 3 182 1689 10.78 157697.8 122400 1.29
Bryan-College Station 2010 1 89 1298 6.86 170236.0 151900 1.12
Bryan-College Station 2010 2 107 1439 7.44 183981.3 165300 1.11
Bryan-College Station 2010 3 176 1577 11.16 165437.5 148300 1.12
Tyler 2010 1 160 2727 5.87 159293.8 138900 1.15
Tyler 2010 2 181 2763 6.55 166513.8 131500 1.27
Tyler 2010 3 250 2729 9.16 143464.0 130000 1.10
Wichita Falls 2010 1 89 908 9.80 100573.0 87200 1.15
Wichita Falls 2010 2 91 915 9.95 103098.9 89400 1.15
Wichita Falls 2010 3 147 946 15.54 123755.1 88600 1.40
df %>% summarise(across(c(mean_price, price_ratio, absorption_rate),
                        list(Media = mean, Min = min, Q1 = ~quantile(., .25), Mediana = median,
                             Q3 = ~quantile(., .75), Max = max))) %>%
  pivot_longer(everything(), names_to = c("variabile", "stat"), names_pattern = "(.*)_(Media|Min|Q1|Mediana|Q3|Max)$") %>%
  pivot_wider(names_from = stat, values_from = value) %>% select(variabile, Min, Q1, Mediana, Media, Q3, Max) %>%
  tab("Sintesi delle nuove variabili", 2)
Sintesi delle nuove variabili
variabile Min Q1 Mediana Media Q3 Max
mean_price 97010.20 132938.94 156588.48 154320.37 173915.15 213233.94
price_ratio 0.80 1.12 1.16 1.16 1.21 1.45
absorption_rate 5.01 8.98 10.96 11.87 13.49 38.71
n_sotto1 <- sum(df$price_ratio < 1)

Il rapporto price_ratio ha media 1.16: in media, il prezzo medio stimato supera il prezzo mediano di circa il 16%. Questo risultato è compatibile con una composizione mensile delle vendite relativamente più orientata verso fasce di prezzo elevate rispetto al prezzo mediano. Poiché mean_price deriva dal rapporto tra volume totale e numero di vendite, il dato è aggregato per città e mese: non consente di identificare né quantificare le singole transazioni di fascia alta. In 1 casi su 240 il rapporto è inferiore a 1; senza il dettaglio delle transazioni non è possibile stabilire se dipenda dalla struttura del dataset, dalle modalità di rilevazione o da caratteristiche effettive del mercato.

Il tasso di assorbimento ha mediana 11.0% e media 11.9%. Il massimo valore è (38.7%). Il rapporto vendite/annunci attivi è un indicatore sintetico della velocità con cui il mercato assorbe l’offerta disponibile nel mese; non rappresenta il percorso dei singoli annunci.

La colonna months_inventory esprime lo stesso concetto dal lato opposto: la correlazione con il tasso di assorbimento è -0.66 (negativa, perché con molti mesi di stock il tasso di assorbimento è basso), ma le due misure non coincidono.

Analisi condizionate

In questa sezione sono riportate le analisi condizionate per città, anno, mese, calcolo media e deviazione standard (le SD riflettono la variabilità dentro il gruppo, che include stagionalità e trend).

summ <- function(data, g) {
  data %>% group_by(across(all_of(g))) %>%
    summarise(across(c(sales, volume, median_price, absorption_rate),
                     list(media = mean, sd = sd), .names = "{.col}_{.fn}"), .groups = "drop")
}
cond_city  <- summ(df, "city")
cond_year  <- summ(df, "year")
cond_month <- summ(df, "month") %>% mutate(month = mesi_it[month])

nomi_cond <- c("Vendite (media)", "Vendite (SD)", "Volume, M$ (media)", "Volume, M$ (SD)",
               "Prezzo mediano, $ (media)", "Prezzo mediano, $ (SD)",
               "Assorbimento, % (media)", "Assorbimento, % (SD)")
tab_cond <- function(x, cap, primo) {
  x %>%
    kable(caption = cap, digits = 1, col.names = c(primo, nomi_cond),
          format.args = list(big.mark = ",")) %>%
    kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)
}
cond_city  %>% tab_cond("Medie e deviazioni standard per città", "Città")
Medie e deviazioni standard per città
Città Vendite (media) Vendite (SD) Volume, M$ (media) Volume, M$ (SD) Prezzo mediano, $ (media) Prezzo mediano, $ (SD) Assorbimento, % (media) Assorbimento, % (SD)
Beaumont 177.4 41.5 26.1 7.0 129,988.3 10,105.0 10.6 2.7
Bryan-College Station 206.0 85.0 38.2 17.2 157,488.3 8,852.2 14.7 7.3
Tyler 269.8 62.0 45.8 13.1 141,441.7 9,336.5 9.3 2.3
Wichita Falls 116.1 22.2 13.9 3.2 101,743.3 11,320.0 12.8 2.5
cond_year  %>% mutate(year = as.character(year)) %>% tab_cond("Medie e deviazioni standard per anno", "Anno")
Medie e deviazioni standard per anno
Anno Vendite (media) Vendite (SD) Volume, M$ (media) Volume, M$ (SD) Prezzo mediano, $ (media) Prezzo mediano, $ (SD) Assorbimento, % (media) Assorbimento, % (SD)
2010 168.7 60.5 25.7 10.8 130,191.7 21,821.8 10.0 3.4
2011 164.1 63.9 25.2 12.2 127,854.2 21,317.8 9.3 2.3
2012 186.1 70.9 29.3 14.5 130,077.1 21,431.5 11.0 2.8
2013 211.9 84.0 35.2 17.9 135,722.9 21,708.1 13.5 4.5
2014 230.6 95.5 39.8 21.2 139,481.2 25,625.4 15.7 6.2
cond_month %>% tab_cond("Medie e deviazioni standard per mese", "Mese")
Medie e deviazioni standard per mese
Mese Vendite (media) Vendite (SD) Volume, M$ (media) Volume, M$ (SD) Prezzo mediano, $ (media) Prezzo mediano, $ (SD) Assorbimento, % (media) Assorbimento, % (SD)
Gen 127.4 43.4 19.0 8.4 124,250 25,151.3 8.3 2.3
Feb 140.8 51.1 21.7 10.1 130,075 22,822.6 8.8 2.2
Mar 189.4 59.2 29.4 12.0 127,415 23,442.0 11.6 3.5
Apr 211.7 65.4 33.3 14.5 131,490 21,458.4 12.5 3.8
Mag 238.8 83.1 39.7 19.0 134,485 18,796.3 14.1 5.0
Giu 243.6 95.0 41.3 21.1 137,620 19,231.0 14.2 5.8
Lug 235.8 96.3 39.1 21.4 134,750 21,944.8 14.3 7.4
Ago 231.4 79.2 38.0 18.0 136,675 22,488.4 14.2 5.3
Set 182.3 72.5 29.6 15.2 134,040 24,344.1 11.2 3.5
Ott 179.9 75.0 29.1 15.1 133,480 26,358.1 11.2 3.6
Nov 156.8 55.5 24.8 11.2 134,305 24,691.5 10.2 2.9
Dic 169.4 60.7 27.1 12.6 133,400 22,809.8 11.7 3.8
plot_ms <- function(tab_in, g, titolo, pal) {
  tab_in %>%
    pivot_longer(-all_of(g), names_to = c("variabile", ".value"), names_pattern = "(.*)_(media|sd)") %>%
    mutate(variabile = recode(variabile, sales = "Vendite (n.)", volume = "Volume (M$)",
                              median_price = "Prezzo mediano ($)", absorption_rate = "Assorbimento (%)")) %>%
    ggplot(aes(.data[[g]], media, fill = .data[[g]])) +
    geom_col(alpha = .9, show.legend = FALSE) +
    geom_errorbar(aes(ymin = media - sd, ymax = media + sd), width = .25) +
    scale_fill_manual(values = pal) +
    facet_wrap(~variabile, scales = "free_y") +
    labs(title = titolo, subtitle = "Barre: media; baffi: ± 1 deviazione standard", x = NULL, y = NULL) +
    theme(axis.text.x = element_text(angle = 30, hjust = 1))
}
plot_ms(cond_city, "city", "Medie per città", city_colors)

Per città. Tyler è la città con il mercato più grande per vendite (270 al mese in media), Wichita Falls quello più piccolo (116). Per il prezzo mediano il risultato è diverso: Bryan-College Station è la più cara ($157,488), Wichita Falls la più economica ($101,743). Il tasso di assorbimento medio più alto si osserva a Bryan-College Station (14.7%), mentre il valore più basso si osserva a Tyler (9.3%). Bryan-College Station ha anche la deviazione standard più alta sulle vendite (85): è la città con più oscillazioni durante il periodo.

plot_ms(cond_year %>% mutate(year = factor(year)), "year", "Medie per anno", year_colors)

Per anno. Le vendite totali crescono da 8096 (2010) a 11069 (2014), e il tasso di assorbimento medio passa da 10.0% a 15.7%. Il prezzo mediano resta pressoché stabile fino al 2012 e aumenta successivamente: nel periodo osservato, la crescita delle quantità precede quella dei prezzi.

plot_ms(cond_month %>% mutate(month = factor(month, levels = mesi_it)), "month", "Medie per mese", month_colors)

Per mese. C’è una stagionalità netta: le vendite medie vanno da 127 (Gen) a 244 (Giu), con un aumento di circa 91% tra il mese più debole e quello più forte. Maggio–luglio concentrano il 31.1% delle vendite annue.

Visualizzazioni

Prezzo mediano per città

ggplot(df, aes(fct_reorder(city, median_price, median), median_price, fill = city)) +
  geom_boxplot(alpha = .75, show.legend = FALSE) +
  scale_fill_manual(values = city_colors) +
  scale_y_continuous(labels = usd) +
  labs(title = "Prezzo mediano di vendita per città", x = NULL, y = "Prezzo mediano ($)")

Le quattro città occupano fasce diverse di prezzo. Bryan-College Station è sempre nella fascia alta e Wichita Falls in quella bassa, mentre Beaumont e Tyler stanno nel mezzo con code che si sovrappongono. Nel campione osservato, Bryan-College Station e Wichita Falls occupano fasce di prezzo nettamente separate: il minimo osservato della prima supera il massimo osservato della seconda. Wichita Falls ha la dispersione relativa più alta del prezzo (CV circa 11%), Bryan-College Station combina il livello medio di prezzo più elevato con la minore variabilità relativa osservata.

Valore totale delle vendite: città e anni

p_c <- ggplot(df, aes(fct_reorder(city, volume, median), volume, fill = city)) +
  geom_boxplot(alpha = .75, show.legend = FALSE) + scale_fill_manual(values = city_colors) +
  labs(title = "Per città", x = NULL, y = "Volume (milioni di $)") +
  theme(axis.text.x = element_text(angle = 25, hjust = 1))
p_y <- ggplot(df, aes(factor(year), volume, fill = factor(year))) + 
  geom_boxplot(alpha = .75, show.legend = FALSE) +
  scale_fill_manual(values = year_colors, name = "Anno") +
  labs(title = "Per anno", x = "Anno", y = NULL)
p_c + p_y + plot_annotation(title = "Distribuzione del valore totale delle vendite mensili")

Per città. Tyler ha il valore di vendite più alto (mediana 45.1 milioni di $) e Wichita Falls il più basso (13.7), in linea con il numero di vendite.

rapporto_stag <- df %>% group_by(year, month) %>% summarise(v = sum(volume), .groups = "drop") %>%
  group_by(year) %>% summarise(r = max(v) / min(v))

Per anni. La mediana resta stabile fino al 2011, sale nel 2012 e aumenta nettamente nel 2013 e nel 2014. Anche la dispersione aumenta; questo andamento è coerente sia con livelli medi più elevati sia con differenze di crescita tra città. La stagionalità invece non cambia: in ogni anno il mese con il valore più alto vale tra 2.1 e 2.3 volte quello più basso.

Vendite per mese e città: barre sovrapposte

Il dataset ha una riga per città-anno-mese; per i totali per mese aggrego prima (summarise) e poi uso geom_col(). Con geom_bar() conterei le righe invece di sommare le vendite.

tot <- df %>% group_by(mese, city) %>% summarise(vendite = sum(sales), .groups = "drop")

b1 <- ggplot(tot, aes(mese, vendite, fill = city)) + geom_col() +
  scale_fill_manual(values = city_colors) +
  labs(title = "Totale vendite 2010–2014", x = NULL, y = "Vendite (n.)", fill = "Città")
b2 <- ggplot(tot, aes(mese, vendite, fill = city)) + geom_col(position = "fill") +
  scale_fill_manual(values = city_colors) + scale_y_continuous(labels = percent) +
  labs(title = "Quota di ciascuna città", x = NULL, y = "Quota sulle vendite del mese", fill = "Città")
b1 / b2 + plot_layout(guides = "collect")

share_bcs <- tot %>% group_by(mese) %>% mutate(q = vendite / sum(vendite)) %>% ungroup() %>%
  filter(city == "Bryan-College Station") %>% pull(q)

Nel grafico in valori assoluti, la stagionalità è evidente per tutte le città, il ciclo inizia a gennaio e sale fino a raggiungere il massimo tra maggio e giugno, calando in autunno. Nella versione normalizzata il profilo stagionale sparisce e si notano le differenze di peso delle città: Tyler contribuisce sempre per circa un terzo delle vendite (35% sul totale), Wichita Falls per meno di un sesto. Le quote di Tyler, Beaumont e Wichita Falls restano abbastanza stabili da un mese all’altro; quella di Bryan-College Station oscilla di più (da 21% a 33%), perché è la città con la stagionalità più marcata.

Aggiungere l’anno nello stesso blocco

ggplot(df, aes(mese, sales, fill = city)) +
  geom_col() +
  facet_wrap(~year, ncol = 2) +
  scale_fill_manual(values = city_colors) +
  labs(title = "Vendite mensili per città, anno per anno", x = NULL, y = "Vendite (n.)", fill = "Città") +
  theme(axis.text.x = element_text(angle = 90, vjust = .5), legend.position = "bottom")

La visualizzazione con gli anni separati mostra come la stagionalità è stabile ma i livelli crescono. Il picco più alto si osserva nell’estate del 2014, il quale è nettamente più alto di quello del 2010 e 2011.

Line chart: stagionalità per anno e città

Il grafico seguente permette di avere una chiara visualizzazione della stagionalità per le quattro città durante gli anni di studio.

ggplot(df, aes(mese, sales, colour = factor(year), group = year)) +
  geom_line(linewidth = .8) + geom_point(size = 1.4) +
  facet_wrap(~city, scales = "free_y") +
 scale_color_manual(values = year_colors, name = "Anno") +
  labs(title = "Profilo stagionale delle vendite, per anno e città",
       subtitle = "Scala verticale diversa per ogni città",
       x = NULL, y = "Vendite (n.)") +
  theme(axis.text.x = element_text(angle = 90, vjust = .5))

Il profilo stagionale è nitido a Bryan-College Station e Tyler e si ripete ogni anno; a Beaumont e Wichita Falls è più irregolare. Il mese più debole cade quasi sempre in inverno (gennaio o febbraio). I picchi cadono in mesi diversi a seconda della città, tra marzo e agosto, con picchi anticipati in alcuni anni a Tyler e Wichita Falls. La tabella seguente riporta, per ciascuna coppia città-anno, il mese di picco, il mese più debole e il rapporto tra i due. Bryan-College Station ha il rapporto picco/minimo più alto in tutti e cinque gli anni: il suo mercato è molto stagionale, in coerenza con la deviazione standard elevata vista sopra. Wichita Falls ha in media l’escursione stagionale più contenuta, anche se il suo profilo è il meno regolare.

df %>% 
  group_by(Città = city, Anno = year, Mese = mese) %>% 
  summarise(m = mean(sales, na.rm = TRUE), .groups = "drop_last") %>% 
  summarise(
    `Mese di picco`  = as.character(Mese[which.max(m)]),
    `Mese debole`    = as.character(Mese[which.min(m)]),
    `Picco / minimo` = max(m, na.rm = TRUE) / min(m, na.rm = TRUE),
    .groups = "drop"
  ) %>% 
  tab("Stagionalità per città e anno", 2)
Stagionalità per città e anno
Città Anno Mese di picco Mese debole Picco / minimo
Beaumont 2010 Mag Gen 2.43
Beaumont 2011 Giu Gen 1.64
Beaumont 2012 Ago Gen 1.98
Beaumont 2013 Ago Feb 1.95
Beaumont 2014 Ago Gen 1.77
Bryan-College Station 2010 Giu Gen 3.21
Bryan-College Station 2011 Giu Gen 3.02
Bryan-College Station 2012 Ago Gen 2.57
Bryan-College Station 2013 Lug Feb 3.22
Bryan-College Station 2014 Lug Gen 2.65
Tyler 2010 Apr Nov 2.04
Tyler 2011 Giu Gen 2.19
Tyler 2012 Lug Gen 1.91
Tyler 2013 Lug Gen 1.87
Tyler 2014 Giu Gen 1.78
Wichita Falls 2010 Apr Gen 1.88
Wichita Falls 2011 Ago Feb 1.71
Wichita Falls 2012 Lug Feb 1.47
Wichita Falls 2013 Mar Gen 2.01
Wichita Falls 2014 Lug Gen 1.69

Line chart: tasso di assorbimento dell’offerta nel tempo

ass <- df %>% arrange(city, date) %>% group_by(city) %>%
  mutate(media_mobile = as.numeric(stats::filter(absorption_rate, rep(1/12, 12), sides = 1))) %>%
  ungroup()

ggplot(ass, aes(date, media_mobile, colour = city)) +
  geom_line(linewidth = 1.1, na.rm = TRUE) +
  scale_colour_manual(values = city_colors) +
  labs(title = "Tasso di assorbimento dell’offerta: media mobile a 12 mesi",
       subtitle = "La media mobile a 12 mesi attenua le oscillazioni stagionali e rende più leggibile l’andamento di fondo.",
       x = NULL, y = "Vendite ogni 100 annunci attivi (%)", colour = "Città")

Attenuate le oscillazioni stagionali dalla media mobile a 12 mesi, il quadro risulta più leggibile. Bryan-College Station mostra un incremento più marcato dal 2013 e raggiunge valori superiori alle altre città. Tyler e Beaumont migliorano più lentamente, mentre Wichita Falls resta nella fascia 11–15%: era la migliore nel 2010 e viene superata da Bryan-College Station nel 2013. Questo grafico integra le evidenze utilizzate nelle conclusioni.

Sintesi per città, 2010 → 2014

crescita <- df %>% group_by(city, year) %>%
  summarise(vendite = sum(sales), prezzo = mean(median_price), assorb = mean(absorption_rate),
            annunci = mean(listings), mesi_stock = mean(months_inventory), .groups = "drop") %>%
  group_by(Città = city) %>%
  summarise(`Vendite %` = 100 * (vendite[year == 2014] / vendite[year == 2010] - 1),
            `Prezzo mediano %` = 100 * (prezzo[year == 2014] / prezzo[year == 2010] - 1),
            `Annunci attivi %` = 100 * (annunci[year == 2014] / annunci[year == 2010] - 1),
            `Assorbimento 2010 (%)` = assorb[year == 2010],
            `Assorbimento 2014 (%)` = assorb[year == 2014],
            `Mesi di stock 2010` = mesi_stock[year == 2010],
            `Mesi di stock 2014` = mesi_stock[year == 2014])
crescita %>% tab("Variazione tra 2010 e 2014 per città", 1)
Variazione tra 2010 e 2014 per città
Città Vendite % Prezzo mediano % Annunci attivi % Assorbimento 2010 (%) Assorbimento 2014 (%) Mesi di stock 2010 Mesi di stock 2014
Beaumont 36.8 -0.7 -8.3 9.0 13.5 10.9 7.6
Bryan-College Station 55.3 10.4 -29.2 10.6 23.6 8.7 4.4
Tyler 45.7 11.3 -12.5 7.5 12.4 12.6 8.8
Wichita Falls -5.2 6.8 -8.6 12.9 13.3 7.7 7.4
cr <- function(c, v) crescita[[v]][crescita$Città == c]
df %>% group_by(Città = city, Anno = year) %>%
  summarise(assorb = mean(absorption_rate), .groups = "drop") %>%
  pivot_wider(names_from = Anno, values_from = assorb) %>%
  tab("Tasso di assorbimento medio (%) per città e anno", 1)
Tasso di assorbimento medio (%) per città e anno
Città 2010 2011 2012 2013 2014
Beaumont 9.0 8.2 10.2 12.3 13.5
Bryan-College Station 10.6 10.3 12.2 17.1 23.6
Tyler 7.5 7.7 9.0 10.1 12.4
Wichita Falls 12.9 10.8 12.6 14.4 13.3

La tabella mostra che l’andamento non è sempre regolare: a Wichita Falls il tasso di assorbimento scende nel 2011, risale fino al 2013 e cala di nuovo nel 2014, mentre a Bryan-College Station accelera soprattutto nel 2013 e nel 2014.

Conclusioni e raccomandazioni

Cosa emerge dai dati

  1. Nel campione osservato, le quantità crescono prima dei prezzi. Tra il 2010 e il 2014 le vendite totali aumentano del 37% e il tasso di assorbimento medio passa da 10.0% a 15.7%, mentre il prezzo mediano resta pressoché stabile fino al 2012 e aumenta successivamente.
  2. Le quattro città sono mercati diversi. Prezzo (Bryan-College Station più cara, Wichita Falls più economica), dimensione (Tyler più grande) e tasso di assorbimento dell’offerta (più alto a Bryan-College Station e più basso a Tyler) non vanno nella stessa direzione.
  3. La stagionalità è forte e comune: maggio–luglio fanno il 31.1% delle vendite annue.
  4. Priorità tra le città. In base agli indicatori descrittivi osservati, Bryan-College Station mostra la dinamica più favorevole nel periodo, seguita da Tyler, che è il mercato più grande e presenta segnali di miglioramento. Beaumont registra una crescita dei volumi con variazioni più contenute dei prezzi, mentre Wichita Falls mostra la dinamica meno favorevole.

Raccomandazioni operative

Bryan-College Station: priorità alla raccolta di nuovi incarichi. Le vendite sono salite del 55% mentre gli annunci attivi sono calati del 29%; il tasso di assorbimento è passato da 10.6% a 23.6%, lo stock da 8.7 a 4.4 mesi e il prezzo mediano è cresciuto del 10%. L’aumento delle vendite, la diminuzione degli annunci attivi e la crescita del tasso di assorbimento sono coerenti con un mercato relativamente dinamico nel periodo osservato. Poiché la stagionalità è marcata, può essere utile pianificare l’acquisizione e la pubblicazione degli incarichi prima dei mesi di maggiore attività, verificando questa scelta con dati operativi più dettagliati.

Tyler: mercato più grande e in miglioramento, ma con tasso di assorbimento ancora basso. Vendite +46%, prezzo +11%, annunci attivi in calo del 12%, tasso di assorbimento da 7.5% a 12.4%. Il tasso di assorbimento rimane il più basso tra le città e i mesi di stock risultano più elevati (8.8 mesi). Questi elementi segnalano un’area prioritaria di approfondimento per individuare eventuali interventi commerciali; i dati disponibili non consentono tuttavia di identificarne le cause.

Beaumont: crescita di volumi senza crescita di prezzi. Vendite +37%, prezzo mediano -0.7%, tasso di assorbimento da 9.0% a 13.5%. L’aumento delle vendite si accompagna a una variazione contenuta del prezzo mediano; nel periodo osservato, la crescita appare quindi più evidente nei volumi che nei prezzi. Una strategia orientata alla rapidità di rotazione può essere valutata, monitorando separatamente vendite, prezzo mediano e disponibilità di offerta.

Wichita Falls: mercato di dimensione ridotta e dinamica meno favorevole nel confronto 2010–2014. Le vendite (-5%) non mostrano la stessa crescita delle altre città, il prezzo mediano resta il più basso tra le città, in crescita del 6.8% in cinque anni, mentre il tasso di assorbimento è oscillante (vedi tabella per anno). In base agli indicatori disponibili, può essere assegnata una priorità inferiore rispetto alle altre aree, mantenendo comunque un monitoraggio periodico.

Tempi: pianificare gli annunci prima dei mesi di maggiore attività. Il tasso di assorbimento medio aumenta da 8.8% a febbraio a 12.5% ad aprile e raggiunge 14.3% a luglio. Poiché i picchi variano tra città, il calendario andrebbe definito per singolo mercato locale. I dati descrivono la stagionalità osservata, ma non dimostrano che anticipare la pubblicazione di un annuncio ne aumenti la probabilità di vendita.

Limiti e ipotesi

L’analisi si basa su cinque anni e quattro città, ed è descrittiva: le differenze osservate non si possono attribuire a cause precise. Il report non indaga le cause (domanda, popolazione, economia locale): eventuali spiegazioni andrebbero verificate con altre fonti. I prezzi sono nominali (non corretti per l’inflazione) e il tasso di assorbimento è una misura mensile semplice: non tiene conto di quanto tempo ciascun annuncio resta effettivamente online.