# 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 kurtosis

1 Introduzione

In 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.


2 Analisi delle Variabili

2.1 Caricamento e Struttura del Dataset

# 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, …
cat("Numero di righe:", nrow(df), "\n")
#> Numero di righe: 240
cat("Numero di colonne:", ncol(df), "\n")
#> Numero di colonne: 8
cat("Valori mancanti:", sum(is.na(df)), "\n")
#> 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)
Prime 10 osservazioni del dataset
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

2.2 Classificazione delle Variabili

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).


3 Indici di Posizione, Variabilità e Forma

3.1 Indici per le Variabili Quantitative

# 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%")
Indici descrittivi per le variabili quantitative
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

3.2 Distribuzione di Frequenza per la Variabile Qualitativa city

freq_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)
Distribuzione di frequenza per città
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.

3.3 Commento Generale sugli Indici

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.


4 Identificazione delle Variabili con Maggiore Variabilità e Asimmetria

4.1 Variabile con Maggiore Variabilità

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")
Coefficiente di Variazione per variabile (ordinato per CV decrescente)
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.

4.2 Variabile con Distribuzione più Asimmetrica

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")
Indici di forma per variabile (ordinati per |asimmetria| decrescente)
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.


5 Creazione di Classi per una Variabile Quantitativa

Selezionamo la variabile sales (numero di vendite mensili) per la creazione delle classi.

5.1 Determinazione 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
cat("Range:", max(df$sales) - min(df$sales), "vendite\n")
#> Range: 344 vendite
cat("Ampiezza classe approssimata:", round((max(df$sales) - min(df$sales)) / k_sturges), "\n")
#> 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)
Distribuzione di frequenza per classi di vendite mensili
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

5.2 Grafico a Barre della Distribuzione

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)
  )

5.3 Indice di Eterogeneità di Gini

# 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
cat("Indice di Gini (normalizzato): G' =", round(gini_norm, 4), "\n")
#> Indice di Gini (normalizzato): G' = 0.9376
cat("Numero classi (k):            k  =", k, "\n")
#> 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.


6 Calcolo della Probabilità

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
cat("Righe con città = Beaumont:           ", n_beaumont, "\n")
#> Righe con città = Beaumont:            60
cat("P(Beaumont):                           ", round(p_beaumont, 4),
    "=", paste0(round(p_beaumont * 100, 2), "%"), "\n\n")
#> P(Beaumont):                            0.25 = 25%
cat("Righe con mese = Luglio (7):          ", n_luglio, "\n")
#> Righe con mese = Luglio (7):           20
cat("P(Luglio):                             ", round(p_luglio, 4),
    "=", paste0(round(p_luglio * 100, 2), "%"), "\n\n")
#> P(Luglio):                              0.0833 = 8.33%
cat("Righe con mese = Dicembre 2012:       ", n_dic2012, "\n")
#> Righe con mese = Dicembre 2012:        4
cat("P(Dicembre 2012):                      ", round(p_dic2012, 6),
    "=", paste0(round(p_dic2012 * 100, 4), "%"), "\n")
#> P(Dicembre 2012):                       0.016667 = 1.6667%

Commento:

  • P(Beaumont) = 0.25: Esattamente 1/4, coerente con la distribuzione uniforme delle 4 città (60 osservazioni ciascuna su 240 totali).
  • P(Luglio) = 0.0833: Esattamente 1/12 ≈ 8.33%, confermando che ogni mese ha lo stesso numero di osservazioni (4 città × 5 anni = 20 righe per mese).
  • P(Dicembre 2012) = 0.016667: Le 4 righe corrispondono alle 4 città nel mese di dicembre dell’anno 2012. La probabilità è 4/240 = 1/60 ≈ 1.67%.

7 Creazione di Nuove Variabili

7.1 Prezzo Medio degli Immobili

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)
Confronto tra prezzo medio calcolato e prezzo mediano
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.

7.2 Efficacia degli Annunci di Vendita

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)
Efficacia delle inserzioni per città (%)
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.


8 Analisi Condizionata

8.1 Analisi per Città

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%")
Statistiche riassuntive per città
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

8.2 Analisi per Anno

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)
Statistiche riassuntive per anno
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

8.3 Analisi per Mese

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)
Statistiche riassuntive per mese
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

8.4 Visualizzazione: Media delle Vendite per Città e Anno

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.


9 Creazione di Visualizzazioni con ggplot2

9.1 Boxplot: Distribuzione del Prezzo Mediano per Città

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à:

  • Tyler presenta i prezzi più elevati e variabili, con una distribuzione ampia e outlier verso l’alto. È il mercato che possiamo definire più “premium”.
  • Bryan-College Station ha prezzi intermedi con buona stabilità.
  • Beaumont mostra i prezzi più bassi e distribuzioni compatte, segnalando un mercato più accessibile.
  • Wichita Falls si colloca su livelli paragonabili a Beaumont con volatilità contenuta.

La sovrapposizione dei punti (jitter) rivela che non ci sono cluster anomali evidenti; la distribuzione è abbastanza continua per tutte le città.

9.2 Boxplot del Volume delle Vendite per Città e Anno

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.

9.3 Grafico a Barre Sovrapposte: Vendite per Mese e Città

# 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 di geom_bar() perché i dati sono già stati aggregati con summarise()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.

9.4 Line Chart: Andamento delle Vendite nel Tempo per Città

# 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:

  1. Stagionalità regolare in tutte le città: ogni anno si ripete lo stesso pattern a “W” con picchi estivi.
  2. Bryan-College Station cresce più rapidamente, con picchi sempre più pronunciati.
  3. Wichita Falls rimane su livelli bassi e stabili, senza crescita evidente.

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.


10 Considerazioni Finali

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