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.
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)
| 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.
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)
| 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))
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)
| 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)
| 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)
| 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).
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)
| 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)
| 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.
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)
| 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)
| 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à.
median_price e indice di GiniPer 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)
| 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.
Le probabilità richieste sono:
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)
| 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 |
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.
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)
| 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)
| 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.
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à")
| 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")
| 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")
| 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.
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.
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.
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.
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.
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)
| 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 |
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.
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)
| 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)
| 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.
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.
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.