# Librerie necessarie
library(tidyverse) # dplyr, ggplot2, tidyr, etc.
library(knitr) # tabelle formattate
library(kableExtra) # stile per tabelle
library(scales) # formattazione assi grafici
library(e1071) # skewness e kurtosisIn questo progetto ho analizzato il mercato immobiliare del Texas usando dati di vendita dal 2010 al 2014, relativi a quattro città: Beaumont, Bryan-College Station, Tyler e Wichita Falls.
Lo scopo è quello di capire dove e quando si vende di più, e se ci sono differenze strutturali tra mercati locali che Texas Realty Insights può sfruttare per posizionare meglio le proprie inserzioni.
# Caricamento del dataset
df <- read.csv("Real Estate Texas.csv", stringsAsFactors = FALSE)
# Panoramica della struttura
glimpse(df)#> Rows: 240
#> Columns: 8
#> $ city <chr> "Beaumont", "Beaumont", "Beaumont", "Beaumont", "Beau…
#> $ year <int> 2010, 2010, 2010, 2010, 2010, 2010, 2010, 2010, 2010,…
#> $ month <int> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 1, 2, 3, 4, 5,…
#> $ sales <int> 83, 108, 182, 200, 202, 189, 164, 174, 124, 150, 150,…
#> $ volume <dbl> 14.162, 17.690, 28.701, 26.819, 28.833, 27.219, 22.70…
#> $ median_price <dbl> 163800, 138200, 122400, 123200, 123100, 122800, 12430…
#> $ listings <int> 1533, 1586, 1689, 1708, 1771, 1803, 1857, 1830, 1829,…
#> $ months_inventory <dbl> 9.5, 10.0, 10.6, 10.6, 10.9, 11.1, 11.7, 11.6, 11.7, …
#> Numero di righe: 240
#> Numero di colonne: 8
#> Valori mancanti: 0
# Prime righe del dataset
head(df, 10) %>%
kable(caption = "Prime 10 osservazioni del dataset") %>%
kable_styling(bootstrap_options = c("striped", "hover", "condensed"), full_width = FALSE)| city | year | month | sales | volume | median_price | listings | months_inventory |
|---|---|---|---|---|---|---|---|
| Beaumont | 2010 | 1 | 83 | 14.162 | 163800 | 1533 | 9.5 |
| Beaumont | 2010 | 2 | 108 | 17.690 | 138200 | 1586 | 10.0 |
| Beaumont | 2010 | 3 | 182 | 28.701 | 122400 | 1689 | 10.6 |
| Beaumont | 2010 | 4 | 200 | 26.819 | 123200 | 1708 | 10.6 |
| Beaumont | 2010 | 5 | 202 | 28.833 | 123100 | 1771 | 10.9 |
| Beaumont | 2010 | 6 | 189 | 27.219 | 122800 | 1803 | 11.1 |
| Beaumont | 2010 | 7 | 164 | 22.706 | 124300 | 1857 | 11.7 |
| Beaumont | 2010 | 8 | 174 | 25.237 | 136800 | 1830 | 11.6 |
| Beaumont | 2010 | 9 | 124 | 17.233 | 121100 | 1829 | 11.7 |
| Beaumont | 2010 | 10 | 150 | 23.904 | 138500 | 1779 | 11.5 |
Le variabili del dataset si classificano come segue:
| Variabile | Tipo Statistico | Scala di Misura | Note |
|---|---|---|---|
city |
Qualitativa Nominale | Nominale | 4 categorie: Beaumont, Bryan-College Station, Tyler, Wichita Falls |
year |
Quantitativa Discreta | Intervallo | 5 anni: 2010–2014. Dimensione temporale |
month |
Quantitativa Discreta | Intervallo | 1–12. Dimensione temporale |
sales |
Quantitativa Discreta | Rapporto | Conteggio vendite (intero non negativo) |
volume |
Quantitativa Continua | Rapporto | Milioni di dollari; zero assoluto reale |
median_price |
Quantitativa Continua | Rapporto | Dollari; zero assoluto reale |
listings |
Quantitativa Discreta | Rapporto | Conteggio annunci (intero non negativo) |
months_inventory |
Quantitativa Continua | Rapporto | Mesi necessari a esaurire le inserzioni correnti |
Commento: Le variabili year e
month, pur essendo numeriche, sottendono una
dimensione temporale ordinata: l’anno definisce il
trend di lungo periodo (crescita/decrescita del mercato), mentre il mese
cattura la stagionalità. È quindi opportuno trattarle
come fattori ordinati nelle analisi condizionate e
grafiche, ma usarle come numeriche negli indici di
posizione e variabilità.
Le variabili su cui ha senso calcolare indici statistici riassuntivi
sono quelle quantitative continue e discrete
(sales, volume, median_price,
listings, months_inventory). Per
city si costruirà una distribuzione di
frequenza, mentre per year e month si
effettuerà un’analisi descrittiva nella sezione apposita (Step 7).
# Funzione per calcolare tutti gli indici richiesti
calcola_indici <- function(x, nome) {
q <- quantile(x, probs = c(0.25, 0.50, 0.75), na.rm = TRUE)
data.frame(
Variabile = nome,
N = length(x),
Media = round(mean(x, na.rm = TRUE), 2),
Mediana = round(median(x, na.rm = TRUE), 2),
Moda = round(as.numeric(names(sort(table(x), decreasing = TRUE)[1])), 2),
Q1 = round(q[1], 2),
Q3 = round(q[3], 2),
IQR = round(IQR(x, na.rm = TRUE), 2),
Dev_Std = round(sd(x, na.rm = TRUE), 2),
Varianza = round(var(x, na.rm = TRUE), 2),
CV_perc = round((sd(x, na.rm = TRUE) / mean(x, na.rm = TRUE)) * 100, 2),
Asimmetria = round(skewness(x, na.rm = TRUE), 4),
Curtosi = round(kurtosis(x, na.rm = TRUE), 4),
Min = round(min(x, na.rm = TRUE), 2),
Max = round(max(x, na.rm = TRUE), 2)
)
}var_quant <- c("sales", "volume", "median_price", "listings", "months_inventory")
indici <- bind_rows(lapply(var_quant, function(v) calcola_indici(df[[v]], v)))
indici %>%
kable(caption = "Indici descrittivi per le variabili quantitative") %>%
kable_styling(bootstrap_options = c("striped", "hover", "condensed"), full_width = TRUE) %>%
scroll_box(width = "100%")| Variabile | N | Media | Mediana | Moda | Q1 | Q3 | IQR | Dev_Std | Varianza | CV_perc | Asimmetria | Curtosi | Min | Max | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 25%…1 | sales | 240 | 192.29 | 175.50 | 124.0 | 127.00 | 247.00 | 120.00 | 79.65 | 6.34430e+03 | 41.42 | 0.7136 | -0.3355 | 79.00 | 423.00 |
| 25%…2 | volume | 240 | 31.01 | 27.06 | 14.0 | 17.66 | 40.89 | 23.23 | 16.65 | 2.77270e+02 | 53.71 | 0.8792 | 0.1506 | 8.17 | 83.55 |
| 25%…3 | median_price | 240 | 132665.42 | 134500.00 | 130000.0 | 117300.00 | 150050.00 | 32750.00 | 22662.15 | 5.13573e+08 | 17.08 | -0.3623 | -0.6427 | 73800.00 | 180000.00 |
| 25%…4 | listings | 240 | 1738.02 | 1618.50 | 1581.0 | 1026.50 | 2056.00 | 1029.50 | 752.71 | 5.66569e+05 | 43.31 | 0.6454 | -0.8102 | 743.00 | 3296.00 |
| 25%…5 | months_inventory | 240 | 9.19 | 8.95 | 8.1 | 7.80 | 10.95 | 3.15 | 2.30 | 5.31000e+00 | 25.06 | 0.0407 | -0.1979 | 3.40 | 14.90 |
cityfreq_city <- df %>%
count(city) %>%
mutate(
Freq_Relativa = round(n / sum(n), 4),
Freq_Perc = paste0(round(Freq_Relativa * 100, 2), "%"),
Freq_Cumulata = cumsum(n),
FreqRel_Cumul = cumsum(Freq_Relativa)
) %>%
rename(Città = city, Freq_Assoluta = n)
freq_city %>%
kable(caption = "Distribuzione di frequenza per città") %>%
kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)| Città | Freq_Assoluta | Freq_Relativa | Freq_Perc | Freq_Cumulata | FreqRel_Cumul |
|---|---|---|---|---|---|
| Beaumont | 60 | 0.25 | 25% | 60 | 0.25 |
| Bryan-College Station | 60 | 0.25 | 25% | 120 | 0.50 |
| Tyler | 60 | 0.25 | 25% | 180 | 0.75 |
| Wichita Falls | 60 | 0.25 | 25% | 240 | 1.00 |
Commento: La distribuzione per città è perfettamente uniforme: ogni città conta esattamente 60 osservazioni (5 anni × 12 mesi), pari al 25% del totale. Il dataset è bilanciato per disegno, quindi non è possibile trarre inferenze sulla dimensione relativa dei mercati dalla frequenza semplice.
Vale la pena notare alcune cose sulle distribuzioni. Le
vendite (sales) mostrano una media di
circa 179 mensili, ma la mediana scende a 166 — il che suggerisce che
certi mesi “tirano” il valore verso l’alto più di quanto sembri dalla
sola media. Stesso discorso per il volume.
Il median_price è invece la variabile più “tranquilla”:
il CV si aggira intorno al X% e la distribuzione è quasi simmetrica, con
la maggior parte dei prezzi tra 120.000 e 160.000 $.
Più interessante è sicuramente il caso di listings e
months_inventory: entrambe mostrano alta variabilità e
asimmetria, probabilmente perché risentono molto delle differenze tra
singoli mercati locali.
Il Coefficiente di Variazione (CV) è l’indice più appropriato per confrontare la variabilità tra variabili con unità di misura diverse, in quanto è adimensionale.
cv_df <- data.frame(
Variabile = var_quant,
Media = sapply(var_quant, function(v) mean(df[[v]], na.rm = TRUE)),
Dev_Std = sapply(var_quant, function(v) sd(df[[v]], na.rm = TRUE))
) %>%
mutate(CV_perc = round((Dev_Std / Media) * 100, 2)) %>%
arrange(desc(CV_perc))
cv_df %>%
kable(caption = "Coefficiente di Variazione per variabile (ordinato per CV decrescente)",
digits = 2) %>%
kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE) %>%
row_spec(1, bold = TRUE, color = "white", background = "#2c7a7b")| Variabile | Media | Dev_Std | CV_perc | |
|---|---|---|---|---|
| volume | volume | 31.01 | 16.65 | 53.71 |
| listings | listings | 1738.02 | 752.71 | 43.31 |
| sales | sales | 192.29 | 79.65 | 41.42 |
| months_inventory | months_inventory | 9.19 | 2.30 | 25.06 |
| median_price | median_price | 132665.42 | 22662.15 | 17.08 |
ggplot(cv_df, aes(x = reorder(Variabile, CV_perc), y = CV_perc, fill = CV_perc)) +
geom_col(show.legend = FALSE) +
geom_text(aes(label = paste0(CV_perc, "%")), hjust = -0.1, size = 4) +
coord_flip() +
scale_fill_gradient(low = "#b2d8d8", high = "#2c7a7b") +
labs(
title = "Coefficiente di Variazione per variabile",
x = NULL,
y = "CV (%)"
) +
theme_minimal(base_size = 13) +
theme(plot.title = element_text(face = "bold")) +
ylim(0, max(cv_df$CV_perc) * 1.15)Conclusione: La variabile con la più alta variabilità è volume con un CV del 53.71%. Questo si spiega con le notevoli differenze strutturali tra i mercati delle quattro città: alcune città hanno mercati molto più attivi e liquidi di altre, con numero di annunci che può variare del semplice al triplo.
skew_df <- data.frame(
Variabile = var_quant,
Asimmetria = sapply(var_quant, function(v) round(skewness(df[[v]], na.rm = TRUE), 4)),
Curtosi = sapply(var_quant, function(v) round(kurtosis(df[[v]], na.rm = TRUE), 4))
) %>%
mutate(Abs_Asimmetria = abs(Asimmetria)) %>%
arrange(desc(Abs_Asimmetria))
skew_df %>%
kable(caption = "Indici di forma per variabile (ordinati per |asimmetria| decrescente)",
digits = 4) %>%
kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE) %>%
row_spec(1, bold = TRUE, color = "white", background = "#c0392b")| Variabile | Asimmetria | Curtosi | Abs_Asimmetria | |
|---|---|---|---|---|
| volume | volume | 0.8792 | 0.1506 | 0.8792 |
| sales | sales | 0.7136 | -0.3355 | 0.7136 |
| listings | listings | 0.6454 | -0.8102 | 0.6454 |
| median_price | median_price | -0.3623 | -0.6427 | 0.3623 |
| months_inventory | months_inventory | 0.0407 | -0.1979 | 0.0407 |
Conclusione: La variabile con la
distribuzione più asimmetrica è volume
(skewness = 0.8792). Un’asimmetria positiva elevata indica che la
distribuzione ha una coda destra lunga: la maggior parte delle
osservazioni si concentra su valori bassi, ma esistono valori estremi
molto elevati. Questo è tipico del volume delle vendite,
dove alcune combinazioni città-mese producono transazioni di grande
valore anomalo rispetto alla media.
Selezionamo la variabile sales (numero
di vendite mensili) per la creazione delle classi.
# Regola di Sturges: k = 1 + 3.322 * log10(n)
n <- nrow(df)
k_sturges <- ceiling(1 + 3.322 * log10(n))
cat("Numero classi (Sturges):", k_sturges, "\n")#> Numero classi (Sturges): 9
#> Range: 344 vendite
#> Ampiezza classe approssimata: 38
# Definizione manuale delle classi per chiarezza interpretativa
breaks_sales <- c(0, 50, 100, 150, 200, 250, 300, 400, Inf)
labels_sales <- c("[0–50)", "[50–100)", "[100–150)", "[150–200)",
"[200–250)", "[250–300)", "[300–400)", "[400+)")
df$sales_class <- cut(df$sales,
breaks = breaks_sales,
labels = labels_sales,
right = FALSE)
# Distribuzione di frequenza
freq_sales <- df %>%
count(sales_class) %>%
mutate(
Freq_Relativa = round(n / sum(n), 4),
Freq_Perc = paste0(round(Freq_Relativa * 100, 2), "%"),
Freq_Cumul = cumsum(n),
FreqRel_Cumul = round(cumsum(Freq_Relativa), 4)
) %>%
rename(Classe = sales_class, Freq_Assoluta = n)
freq_sales %>%
kable(caption = "Distribuzione di frequenza per classi di vendite mensili") %>%
kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)| Classe | Freq_Assoluta | Freq_Relativa | Freq_Perc | Freq_Cumul | FreqRel_Cumul |
|---|---|---|---|---|---|
| [50–100) | 20 | 0.0833 | 8.33% | 20 | 0.0833 |
| [100–150) | 69 | 0.2875 | 28.75% | 89 | 0.3708 |
| [150–200) | 58 | 0.2417 | 24.17% | 147 | 0.6125 |
| [200–250) | 33 | 0.1375 | 13.75% | 180 | 0.7500 |
| [250–300) | 34 | 0.1417 | 14.17% | 214 | 0.8917 |
| [300–400) | 23 | 0.0958 | 9.58% | 237 | 0.9875 |
| [400+) | 3 | 0.0125 | 1.25% | 240 | 1.0000 |
ggplot(df, aes(x = sales_class)) +
geom_bar(fill = "#2c7a7b", color = "white", alpha = 0.85) +
geom_text(stat = "count", aes(label = ..count..), vjust = -0.5, size = 4) +
labs(
title = "Distribuzione delle vendite mensili per classi",
subtitle = "Variabile: sales | Dataset Real Estate Texas 2010–2014",
x = "Classi di vendite mensili",
y = "Frequenza assoluta"
) +
theme_minimal(base_size = 13) +
theme(
plot.title = element_text(face = "bold"),
axis.text.x = element_text(angle = 30, hjust = 1)
)# Calcolo indice di Gini per la distribuzione delle classi
fi <- freq_sales$Freq_Relativa
gini_raw <- 1 - sum(fi^2)
# Normalizzazione: G' = G * k/(k-1) con k = numero classi
k <- length(fi)
gini_norm <- gini_raw * k / (k - 1)
cat("Indice di Gini (grezzo): G =", round(gini_raw, 4), "\n")#> Indice di Gini (grezzo): G = 0.8037
#> Indice di Gini (normalizzato): G' = 0.9376
#> Numero classi (k): k = 7
Interpretazione dell’Indice di Gini:
L’indice di Gini \(G = 0.8037\) misura il grado di eterogeneità della distribuzione nelle classi. Il valore normalizzato \(G' = 0.9376\) è compreso tra 0 (massima omogeneità, tutte le osservazioni in una sola classe) e 1 (massima eterogeneità, distribuzione uniforme tra le classi).
Un \(G' \approx 0.94\) indica una eterogeneità medio-alta: le vendite mensili si distribuiscono su più classi senza concentrarsi in una singola fascia, anche se le classi centrali ([100–150), [150–200)) raccolgono la quota maggiore di osservazioni. Questo riflette la varietà dei mercati e la stagionalità delle vendite.
Assumiamo un approccio frequentista: la probabilità di un evento è il rapporto tra le righe che soddisfano la condizione e il totale delle righe.
n_tot <- nrow(df)
# P(città = Beaumont)
n_beaumont <- sum(df$city == "Beaumont")
p_beaumont <- n_beaumont / n_tot
# P(mese = Luglio, mese 7)
n_luglio <- sum(df$month == 7)
p_luglio <- n_luglio / n_tot
# P(mese = Dicembre 2012)
n_dic2012 <- sum(df$month == 12 & df$year == 2012)
p_dic2012 <- n_dic2012 / n_tot
cat("Totale righe nel dataset: ", n_tot, "\n\n")#> Totale righe nel dataset: 240
#> Righe con città = Beaumont: 60
#> P(Beaumont): 0.25 = 25%
#> Righe con mese = Luglio (7): 20
#> P(Luglio): 0.0833 = 8.33%
#> Righe con mese = Dicembre 2012: 4
#> P(Dicembre 2012): 0.016667 = 1.6667%
Commento:
Il dataset fornisce volume (valore totale delle vendite
in milioni di $) e sales (numero di vendite). Il
prezzo medio per transazione si ottiene come:
\[\text{avg\_price} = \frac{\text{volume} \times 1{.}000{.}000}{\text{sales}}\]
df <- df %>%
mutate(avg_price = (volume * 1e6) / sales)
# Statistiche a confronto: avg_price vs median_price
df %>%
select(avg_price, median_price) %>%
summarise(across(everything(), list(
Media = ~round(mean(.x, na.rm=TRUE), 0),
Mediana = ~round(median(.x, na.rm=TRUE), 0),
Dev_Std = ~round(sd(.x, na.rm=TRUE), 0),
Min = ~round(min(.x, na.rm=TRUE), 0),
Max = ~round(max(.x, na.rm=TRUE), 0)
))) %>%
pivot_longer(everything(), names_to = c("Variabile", "Statistica"), names_sep = "_(?=[^_]+$)") %>%
pivot_wider(names_from = Statistica, values_from = value) %>%
kable(caption = "Confronto tra prezzo medio calcolato e prezzo mediano") %>%
kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)| Variabile | Media | Mediana | Std | Min | Max |
|---|---|---|---|---|---|
| avg_price | 154320 | 156588 | NA | 97010 | 213234 |
| avg_price_Dev | NA | NA | 27147 | NA | NA |
| median_price | 132665 | 134500 | NA | 73800 | 180000 |
| median_price_Dev | NA | NA | 22662 | NA | NA |
Commento: Il avg_price (prezzo medio)
risulta generalmente superiore al
median_price (prezzo mediano). Questo è atteso: la
presenza di immobili di lusso con prezzi molto elevati tira verso l’alto
la media, mentre la mediana è più robusta agli outlier. La differenza
tra i due indici segnala una distribuzione asimmetrica
positiva dei prezzi di vendita nel mercato texano, tipica dei
mercati immobiliari.
Definiamo l’efficacia degli annunci come il rapporto tra vendite realizzate e annunci disponibili:
\[\text{listing\_efficiency} = \frac{\text{sales}}{\text{listings}} \times 100\]
Questo indice misura quanti annunci su 100 si traducono effettivamente in una vendita in quel mese.
df <- df %>%
mutate(listing_efficiency = round((sales / listings) * 100, 2))
# Statistiche dell'efficacia
summary_eff <- df %>%
group_by(city) %>%
summarise(
Media_Efficacia = round(mean(listing_efficiency, na.rm=TRUE), 2),
Mediana_Efficacia = round(median(listing_efficiency, na.rm=TRUE), 2),
Dev_Std = round(sd(listing_efficiency, na.rm=TRUE), 2),
Min = round(min(listing_efficiency, na.rm=TRUE), 2),
Max = round(max(listing_efficiency, na.rm=TRUE), 2)
)
summary_eff %>%
kable(caption = "Efficacia delle inserzioni per città (%)") %>%
kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)| city | Media_Efficacia | Mediana_Efficacia | Dev_Std | Min | Max |
|---|---|---|---|---|---|
| Beaumont | 10.61 | 10.30 | 2.67 | 5.41 | 16.51 |
| Bryan-College Station | 14.73 | 12.54 | 7.29 | 6.35 | 38.71 |
| Tyler | 9.35 | 9.23 | 2.35 | 5.01 | 14.82 |
| Wichita Falls | 12.80 | 12.38 | 2.47 | 8.32 | 18.47 |
ggplot(df, aes(x = city, y = listing_efficiency, fill = city)) +
geom_boxplot(alpha = 0.7, outlier.color = "red") +
scale_fill_brewer(palette = "Set2") +
labs(
title = "Efficacia degli annunci di vendita per città",
subtitle = "Indice: (sales / listings) × 100",
x = NULL,
y = "Efficacia (%)",
fill = "Città"
) +
theme_minimal(base_size = 13) +
theme(
plot.title = element_text(face = "bold"),
legend.position = "none",
axis.text.x = element_text(angle = 20, hjust = 1)
)Commento: L’efficacia delle inserzioni varia significativamente tra città. Bryan-College Station e Tyler mostrano in media i valori più alti, indicando mercati più dinamici dove una proporzione maggiore degli annunci si concretizza in vendita. Beaumont e Wichita Falls presentano efficacia inferiore, suggerendo un eccesso relativo di offerta rispetto alla domanda. I valori mensili mostrano inoltre una forte stagionalità (picchi primaverili-estivi), coerente con la ciclicità tipica dei mercati immobiliari.
summary_city <- df %>%
group_by(city) %>%
summarise(
N = n(),
Media_Sales = round(mean(sales), 1),
SD_Sales = round(sd(sales), 1),
Media_Volume = round(mean(volume), 2),
SD_Volume = round(sd(volume), 2),
Media_Prezzo = round(mean(median_price), 0),
SD_Prezzo = round(sd(median_price), 0),
Media_Listings = round(mean(listings), 0),
Media_Inv = round(mean(months_inventory), 2)
) %>%
rename(Città = city)
summary_city %>%
kable(caption = "Statistiche riassuntive per città") %>%
kable_styling(bootstrap_options = c("striped", "hover", "condensed"), full_width = TRUE) %>%
scroll_box(width = "100%")| Città | N | Media_Sales | SD_Sales | Media_Volume | SD_Volume | Media_Prezzo | SD_Prezzo | Media_Listings | Media_Inv |
|---|---|---|---|---|---|---|---|---|---|
| Beaumont | 60 | 177.4 | 41.5 | 26.13 | 6.97 | 129988 | 10105 | 1679 | 9.97 |
| Bryan-College Station | 60 | 206.0 | 85.0 | 38.19 | 17.25 | 157488 | 8852 | 1458 | 7.66 |
| Tyler | 60 | 269.8 | 62.0 | 45.77 | 13.11 | 141442 | 9337 | 2905 | 11.32 |
| Wichita Falls | 60 | 116.1 | 22.2 | 13.93 | 3.24 | 101743 | 11320 | 910 | 7.82 |
summary_year <- df %>%
group_by(year) %>%
summarise(
Media_Sales = round(mean(sales), 1),
SD_Sales = round(sd(sales), 1),
Media_Volume = round(mean(volume), 2),
Media_Prezzo = round(mean(median_price), 0),
Media_Listings = round(mean(listings), 0),
Media_Inv = round(mean(months_inventory), 2)
) %>%
rename(Anno = year)
summary_year %>%
kable(caption = "Statistiche riassuntive per anno") %>%
kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)| Anno | Media_Sales | SD_Sales | Media_Volume | Media_Prezzo | Media_Listings | Media_Inv |
|---|---|---|---|---|---|---|
| 2010 | 168.7 | 60.5 | 25.68 | 130192 | 1826 | 9.97 |
| 2011 | 164.1 | 63.9 | 25.16 | 127854 | 1850 | 10.90 |
| 2012 | 186.1 | 70.9 | 29.27 | 130077 | 1777 | 9.88 |
| 2013 | 211.9 | 84.0 | 35.15 | 135723 | 1678 | 8.15 |
| 2014 | 230.6 | 95.5 | 39.77 | 139481 | 1560 | 7.06 |
nomi_mesi <- c("Gen","Feb","Mar","Apr","Mag","Giu","Lug","Ago","Set","Ott","Nov","Dic")
summary_month <- df %>%
group_by(month) %>%
summarise(
Media_Sales = round(mean(sales), 1),
SD_Sales = round(sd(sales), 1),
Media_Volume = round(mean(volume), 2),
Media_Prezzo = round(mean(median_price), 0)
) %>%
mutate(Mese = nomi_mesi[month]) %>%
select(month, Mese, everything()) %>%
rename(N_Mese = month)
summary_month %>%
kable(caption = "Statistiche riassuntive per mese") %>%
kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)| N_Mese | Mese | Media_Sales | SD_Sales | Media_Volume | Media_Prezzo |
|---|---|---|---|---|---|
| 1 | Gen | 127.4 | 43.4 | 19.00 | 124250 |
| 2 | Feb | 140.8 | 51.1 | 21.65 | 130075 |
| 3 | Mar | 189.4 | 59.2 | 29.38 | 127415 |
| 4 | Apr | 211.7 | 65.4 | 33.30 | 131490 |
| 5 | Mag | 238.8 | 83.1 | 39.70 | 134485 |
| 6 | Giu | 243.6 | 95.0 | 41.30 | 137620 |
| 7 | Lug | 235.8 | 96.3 | 39.12 | 134750 |
| 8 | Ago | 231.4 | 79.2 | 38.01 | 136675 |
| 9 | Set | 182.3 | 72.5 | 29.60 | 134040 |
| 10 | Ott | 179.9 | 75.0 | 29.08 | 133480 |
| 11 | Nov | 156.8 | 55.5 | 24.81 | 134305 |
| 12 | Dic | 169.4 | 60.7 | 27.09 | 133400 |
df %>%
group_by(city, year) %>%
summarise(media_sales = round(mean(sales), 1), .groups = "drop") %>%
ggplot(aes(x = factor(year), y = city, fill = media_sales)) +
geom_tile(color = "white", size = 0.5) +
geom_text(aes(label = media_sales), color = "white", fontface = "bold", size = 4.5) +
scale_fill_gradient(low = "#b2d8d8", high = "#1a4a4a", name = "Vendite\nmedia") +
labs(
title = "Heatmap: media delle vendite mensili per città e anno",
subtitle = "Valori medi mensili aggregati per anno",
x = "Anno",
y = NULL
) +
theme_minimal(base_size = 13) +
theme(plot.title = element_text(face = "bold"))Commento: La heatmap evidenzia chiaramente le differenze strutturali tra città e la tendenza al rialzo nel tempo. Bryan-College Station spicca e domina garzie ai volumi di vendita nettamente superiori alle altre città, con una crescita costante dal 2010 al 2014. Wichita Falls mostra i valori più bassi e relativamente stabili, indicando un mercato maturo e poco dinamico.
ggplot(df, aes(x = reorder(city, median_price, FUN = median), y = median_price, fill = city)) +
geom_boxplot(alpha = 0.75, outlier.shape = 21, outlier.fill = "red", outlier.size = 2) +
geom_jitter(width = 0.15, alpha = 0.25, size = 1.5, color = "gray40") +
scale_fill_brewer(palette = "Set2") +
scale_y_continuous(labels = label_dollar(prefix = "$", big.mark = ",")) +
labs(
title = "Distribuzione del prezzo mediano di vendita per città",
subtitle = "Boxplot con punti individuali sovrapposti | periodo 2010–2014",
x = NULL,
y = "Prezzo mediano ($)",
fill = "Città"
) +
theme_minimal(base_size = 13) +
theme(
plot.title = element_text(face = "bold"),
legend.position = "none",
axis.text.x = element_text(angle = 15, hjust = 1)
)Commento: Il boxplot mostra differenze marcate tra le città:
La sovrapposizione dei punti (jitter) rivela che non ci sono cluster anomali evidenti; la distribuzione è abbastanza continua per tutte le città.
ggplot(df, aes(x = reorder(city, volume, FUN = median), y = volume, fill = city)) +
geom_boxplot(alpha = 0.75, outlier.shape = 21, outlier.fill = "red") +
scale_fill_brewer(palette = "Set2") +
scale_y_continuous(labels = label_number(suffix = "M$", accuracy = 1)) +
labs(
title = "Distribuzione del volume delle vendite per città",
x = NULL,
y = "Volume (milioni $)",
fill = "Città"
) +
theme_minimal(base_size = 13) +
theme(plot.title = element_text(face = "bold"), legend.position = "none",
axis.text.x = element_text(angle = 15, hjust = 1))ggplot(df, aes(x = factor(year), y = volume, fill = factor(year))) +
geom_boxplot(alpha = 0.75, outlier.shape = 21, outlier.fill = "red") +
scale_fill_brewer(palette = "Blues") +
scale_y_continuous(labels = label_number(suffix = "M$", accuracy = 1)) +
labs(
title = "Distribuzione del volume delle vendite per anno",
subtitle = "Considerando tutte le città",
x = "Anno",
y = "Volume (milioni $)",
fill = "Anno"
) +
theme_minimal(base_size = 13) +
theme(plot.title = element_text(face = "bold"), legend.position = "none")Commento: Per città, Bryan-College Station svetta con volumi nettamente superiori e dispersione elevata. Per anno, si osserva una crescita progressiva del volume dal 2010 al 2014, sia in termini di mediana che di ampiezza dell’IQR, segnalando un mercato in espansione. Il 2014 mostra i valori più alti e la variabilità più elevata, coerente con il riscaldamento del mercato immobiliare texano post-crisi.
# Dati aggregati per mese e città
df_month_city <- df %>%
group_by(month, city) %>%
summarise(tot_sales = sum(sales), .groups = "drop") %>%
mutate(month_label = factor(month, levels = 1:12, labels = nomi_mesi))
ggplot(df_month_city, aes(x = month_label, y = tot_sales, fill = city)) +
geom_col(alpha = 0.85) +
scale_fill_brewer(palette = "Set2") +
scale_y_continuous(labels = label_comma()) +
labs(
title = "Totale vendite per mese — barre sovrapposte per città",
subtitle = "Somma 2010–2014",
x = "Mese",
y = "Vendite totali",
fill = "Città"
) +
theme_minimal(base_size = 13) +
theme(
plot.title = element_text(face = "bold"),
axis.text.x = element_text(angle = 30, hjust = 1)
)# Versione normalizzata (100%)
ggplot(df_month_city, aes(x = month_label, y = tot_sales, fill = city)) +
geom_col(position = "fill", alpha = 0.85) +
scale_fill_brewer(palette = "Set2") +
scale_y_continuous(labels = label_percent()) +
labs(
title = "Composizione % delle vendite per mese — barre normalizzate per città",
subtitle = "Quota relativa di ogni città sul totale mensile",
x = "Mese",
y = "Quota (%)",
fill = "Città"
) +
theme_minimal(base_size = 13) +
theme(
plot.title = element_text(face = "bold"),
axis.text.x = element_text(angle = 30, hjust = 1)
)Nota: aggiunto il facet per anno per vedere se il pattern stagionale cambia nel tempo (risposta: no, ma la crescita assoluta è evidente).
df %>%
group_by(year, month, city) %>%
summarise(tot_sales = sum(sales), .groups = "drop") %>%
mutate(month_label = factor(month, levels = 1:12, labels = nomi_mesi)) %>%
ggplot(aes(x = month_label, y = tot_sales, fill = city)) +
geom_col(alpha = 0.85) +
facet_wrap(~year, ncol = 5) +
scale_fill_brewer(palette = "Set2") +
scale_y_continuous(labels = label_comma()) +
labs(
title = "Vendite per mese e città — suddivise per anno (facet)",
x = NULL,
y = "Vendite",
fill = "Città"
) +
theme_minimal(base_size = 11) +
theme(
plot.title = element_text(face = "bold"),
axis.text.x = element_text(angle = 90, vjust = 0.5, size = 7),
legend.position = "bottom"
)Commento: Il grafico a barre sovrapposte mostra in modo esplicit e chiaro la stagionalità del mercato: i mesi estivi (maggio/luglio) registrano i picchi di vendita, mentre gennaio/febbraio sono i mesi più deboli. Il grafico normalizzato rivela che la composizione per città rimane relativamente stabile mese per mese, con Bryan-College Station sempre dominante. Il facet per anno sottolinea la crescita progressiva del volume complessivo e come il pattern stagionale si ripeta costantemente nei 5 anni osservati.
Ho usato
geom_col()invece digeom_bar()perché i dati sono già stati aggregati consummarise()—geom_bar()conterebbe le righegrezze e darebbe risultati sbagliati. Nel facet l’anno è inserito come variabile di stratificazione senza appesantire la leggibilità del singolo grafico.
# Creiamo una variabile data continua per l'asse X
df_line <- df %>%
mutate(data = as.Date(paste(year, month, "01", sep = "-"))) %>%
group_by(city, data) %>%
summarise(sales = sum(sales), .groups = "drop")
ggplot(df_line, aes(x = data, y = sales, color = city, group = city)) +
geom_line(size = 1) +
geom_point(size = 1.5, alpha = 0.6) +
scale_color_brewer(palette = "Set1") +
scale_x_date(date_breaks = "6 months", date_labels = "%b %Y") +
scale_y_continuous(labels = label_comma()) +
labs(
title = "Andamento mensile delle vendite per città (2010–2014)",
subtitle = "Ogni linea rappresenta una città; i punti sono le osservazioni mensili",
x = NULL,
y = "Vendite mensili",
color = "Città"
) +
theme_minimal(base_size = 13) +
theme(
plot.title = element_text(face = "bold"),
axis.text.x = element_text(angle = 45, hjust = 1),
legend.position = "bottom"
)# Line chart del prezzo mediano per confronto fra città
df %>%
mutate(data = as.Date(paste(year, month, "01", sep = "-"))) %>%
ggplot(aes(x = data, y = median_price, color = city, group = city)) +
geom_line(size = 1, alpha = 0.8) +
geom_smooth(se = FALSE, linetype = "dashed", size = 0.6, alpha = 0.5) +
scale_color_brewer(palette = "Set1") +
scale_x_date(date_breaks = "1 year", date_labels = "%Y") +
scale_y_continuous(labels = label_dollar(big.mark = ",")) +
labs(
title = "Andamento del prezzo mediano per città (2010–2014)",
subtitle = "Linea continua = dati mensili | Linea tratteggiata = trend (loess)",
x = NULL,
y = "Prezzo mediano ($)",
color = "Città"
) +
theme_minimal(base_size = 13) +
theme(
plot.title = element_text(face = "bold"),
axis.text.x = element_text(angle = 30, hjust = 1),
legend.position = "bottom"
)Commento: Il line chart delle vendite mette in rilievo tre elementi chiave:
Il line chart dei prezzi mediani rileva una tendenza al rialzo generalizzata, più marcata per Tyler (+XX% nel periodo) e più stabile per Beaumont. La linea di trend loess (tratteggiata) aiuta a separare il segnale dalla stagionalità di breve periodo.
Guardando l’insieme dell’analisi, la cosa che colpisce di più è quanto siano diverse le quattro città, non solo nei numeri ma proprio nel carattere del mercato. Bryan-College Station cresce in modo sostenuto e ha i volumi più alti; Tyler punta su prezzi premium; Beaumont e Wichita Falls giocano su un altro tavolo, più accessibile e meno volatile.
La stagionalità non è sicuramente una sorpresa, poiché i mercati immobiliari hanno quasi ovunque picchi estivi, anche se qui è particolarmente marcata e regolare, il che rappresenta un’informazione utile: sapere quando il mercato si muove vale quasi quanto sapere dove.
Sul fronte della variabilità, listings è la variabile
che varia di più in termini relativi (CV più alto), mentre
volume è quella con la coda più lunga a destra. Alcune
combinazioni città/mese producono transazioni anomale rispetto alla
norma.
Cosa farei con queste informazioni se fossi Texas Realty
Insights? Probabilmente concentrerei le risorse su
Bryan-College Station (crescita più forte), differenzierei il pricing
tra Tyler (premium) e le altre città, e inizierei a pubblicare le
inserzioni più rilevanti già a marzo, prima che il mercato si “accenda”
in estate. Il months_inventory > 8 mesi andrebbe
trattato come segnale di allerta, non solo come dato descrittivo.
Un limite di questa analisi è che 240 osservazioni su 4 città in 5 anni non permettono generalizzazioni forti, ma come punto di partenza esplorativo i pattern emergono abbastanza chiaramente.
Progetto realizzato nell’ambito del Modulo di Statistica Descrittiva | Master in Data Science - Profession AI
#> R version 4.5.2 (2025-10-31 ucrt)
#> Platform: x86_64-w64-mingw32/x64
#> Running under: Windows 11 x64 (build 26200)
#>
#> Matrix products: default
#> LAPACK version 3.12.1
#>
#> locale:
#> [1] LC_COLLATE=Italian_Italy.utf8 LC_CTYPE=Italian_Italy.utf8
#> [3] LC_MONETARY=Italian_Italy.utf8 LC_NUMERIC=C
#> [5] LC_TIME=Italian_Italy.utf8
#>
#> time zone: Europe/Rome
#> tzcode source: internal
#>
#> attached base packages:
#> [1] stats graphics grDevices utils datasets methods base
#>
#> other attached packages:
#> [1] e1071_1.7-17 scales_1.4.0 kableExtra_1.4.0 knitr_1.51
#> [5] lubridate_1.9.5 forcats_1.0.1 stringr_1.6.0 dplyr_1.2.0
#> [9] purrr_1.2.1 readr_2.2.0 tidyr_1.3.2 tibble_3.3.1
#> [13] ggplot2_4.0.2 tidyverse_2.0.0
#>
#> loaded via a namespace (and not attached):
#> [1] sass_0.4.10 generics_0.1.4 class_7.3-23 xml2_1.5.2
#> [5] lattice_0.22-7 stringi_1.8.7 hms_1.1.4 digest_0.6.39
#> [9] magrittr_2.0.4 evaluate_1.0.5 grid_4.5.2 timechange_0.4.0
#> [13] RColorBrewer_1.1-3 fastmap_1.2.0 Matrix_1.7-4 jsonlite_2.0.0
#> [17] mgcv_1.9-3 viridisLite_0.4.3 textshaping_1.0.5 jquerylib_0.1.4
#> [21] cli_3.6.5 rlang_1.1.7 splines_4.5.2 withr_3.0.2
#> [25] cachem_1.1.0 yaml_2.3.12 tools_4.5.2 tzdb_0.5.0
#> [29] vctrs_0.7.1 R6_2.6.1 proxy_0.4-29 lifecycle_1.0.5
#> [33] pkgconfig_2.0.3 pillar_1.11.1 bslib_0.10.0 gtable_0.3.6
#> [37] glue_1.8.0 systemfonts_1.3.2 xfun_0.57 tidyselect_1.2.1
#> [41] rstudioapi_0.18.0 farver_2.1.2 nlme_3.1-168 htmltools_0.5.9
#> [45] rmarkdown_2.31 svglite_2.2.2 labeling_0.4.3 compiler_4.5.2
#> [49] S7_0.2.1