# chiamo le librerie
suppressPackageStartupMessages(library(readr))
suppressPackageStartupMessages(library(dplyr))
suppressPackageStartupMessages(library(ggplot2))
suppressPackageStartupMessages(library(knitr))
# carico i dati
data <-read.csv("realestate_texas.csv")
# converto i dati in formato df
re_texas <- as.data.frame(data)
city: variabile qualitativa nominale -> adatta a un’analisi delle frequenze per valutare l’andamento delle vendite nelle singole città
month: variabile qualitativa nominale (ciclica), codificata in numeri -> adatta ad analisi temporale
year: variabili quantitativa continua da trattare come qualitativa ordinale in questo caso
volume, median_pice: variabili quantitative continue
sales, listings: variabili quantitative discrete -> adatta per il calcolo delle frequenze e per il calcolo degli indici di posizione, variabilità e forma
months_inventory: variabile quantitative continue: adatta per il calcolo degli indici di posizione, variabilità e forma
Le quantitative continue sono tutte su scala di rapporti
# inizio definendo una funzione per automatizzare la creazione delle tabelle di frequenze
tab_freq <- function(data, v, x){
# trovo il numero delle occorrenze totali
N <- dim(data)[1]
# ottengo le frequenze assolute creando il DataFrame
df_dist_frq <- as.data.frame(table(v))
# rinomino la prima colonna
colnames(df_dist_frq)[1] <- x
# rinomino la seconda colonna
colnames(df_dist_frq)[2] <- 'freq_ass'
# Calcolo le frequenze relative (Ora funziona perché ret_freq_ass è un numero!)
df_dist_frq$freq_rel <- round(df_dist_frq$freq_ass / N, 4)
# Calcolo le percentuali
df_dist_frq$freq_pct <- round(df_dist_frq$freq_rel * 100, 2)
# Ritorno il dataframe finale
df_dist_frq
}
# chiamo la funzione su city, year e month
kable(tab_freq(re_texas, re_texas$city, 'Città'), row.names = FALSE)
| Città | freq_ass | freq_rel | freq_pct |
|---|---|---|---|
| Beaumont | 60 | 0.25 | 25 |
| Bryan-College Station | 60 | 0.25 | 25 |
| Tyler | 60 | 0.25 | 25 |
| Wichita Falls | 60 | 0.25 | 25 |
kable(tab_freq(re_texas, re_texas$year, 'Anno'), row.names = FALSE)
| Anno | freq_ass | freq_rel | freq_pct |
|---|---|---|---|
| 2010 | 48 | 0.2 | 20 |
| 2011 | 48 | 0.2 | 20 |
| 2012 | 48 | 0.2 | 20 |
| 2013 | 48 | 0.2 | 20 |
| 2014 | 48 | 0.2 | 20 |
kable(tab_freq(re_texas, re_texas$month, 'Mese'), row.names = FALSE)
| Mese | freq_ass | freq_rel | freq_pct |
|---|---|---|---|
| 1 | 20 | 0.0833 | 8.33 |
| 2 | 20 | 0.0833 | 8.33 |
| 3 | 20 | 0.0833 | 8.33 |
| 4 | 20 | 0.0833 | 8.33 |
| 5 | 20 | 0.0833 | 8.33 |
| 6 | 20 | 0.0833 | 8.33 |
| 7 | 20 | 0.0833 | 8.33 |
| 8 | 20 | 0.0833 | 8.33 |
| 9 | 20 | 0.0833 | 8.33 |
| 10 | 20 | 0.0833 | 8.33 |
| 11 | 20 | 0.0833 | 8.33 |
| 12 | 20 | 0.0833 | 8.33 |
# definisco una funzione per automatizzare la visualizzazione
freq_viz <- function(data, v, x){
ggplot(data)+
geom_bar(aes(x=v),
fill = 'red',
colour = 'black')+
labs(title=paste('Frequenze Assolute di', x), x=x, y='Frequenze')+
theme_classic()
}
# chiamo la funzione su tutte e tre le colonne
freq_viz(re_texas, re_texas$city, 'Città')
freq_viz(re_texas, re_texas$year, 'Anno')
freq_viz(re_texas, re_texas$month, 'Mese')
Le variabili ‘city’, ‘year’ e ‘month’ hanno natura differente in quanto ‘city’ e ‘month’ sono qualitative nominale, mentre year quantitativa continua.Tutte e tre presentano una distribuzione uniforme, le modalità non presentano sbilanciamenti evidenti
# definisco una funzione che calcola le statistiche
eda <- function(v){
# creo le variabili media, std e coefficiente di variazione
media <- mean(v, na.rm = TRUE)
std <- sd(v, na.rm = TRUE)
coef_var <- (std/media)*100
# creo un vettore nel quale salvo tutti i risultati
# senza ottenevo solo risultati singoli.
# così sono incapsulati in un unico oggetto e vanno insieme a stampa
# creo il df per mostrare le stats in modo ordinato
var_stats = data.frame(
etichette = c(
"Min", "Max", "Mediana", "Media", "SD", "IQR", "CV"),
stats= c(
min = min(v, na.rm = TRUE),
max = max(v, na.rm = TRUE),
mediana = median(v, na.rm = TRUE),
media = media,
sd = std,
IQR = IQR(v, na.rm = TRUE),
cv = coef_var
))
# imposto rownames = NULL per rimuovere i nomi degli indici e reimpostarli
rownames(var_stats) <- NULL
return(var_stats)
}
# definisco una funziona per automatizzare il calcolo degli indici di forma
indici_f <- function(variabile){
variabile <- na.omit(variabile)
N <- length(variabile)
media <- mean(variabile)
std <- sd(variabile)
mu3 <- sum((variabile - media)^3)/N
mu4 <- sum((variabile - media)^4)/N
# creo il df per mostrare le stats in modo ordinato
ind_f_stats = data.frame(
etichette = c(
"Fisher", "curtosi"),
stats= c(
fisher = mu3/std^3,
curtosi = (mu4/std^4) - 3)
)
# imposto rownames = NULL per rimuovere i nomi degli indici e reimpostarli
rownames(ind_f_stats) <- NULL
return(ind_f_stats)
}
# definisco una funzione per visualizzare le curve di densità e distribuzione
dens_viz <- function(variabile, x){
variabile <- na.omit(variabile)
dens <- density(variabile)
hist(variabile, probability = TRUE, main = paste("Distribuzione di", x), xlab = x, col = 'white', yaxt = "n")
lines(dens, col = "red", lwd = 2)
lines(seq(min(variabile), max(variabile), length.out = 100),
dnorm(seq(min(variabile), max(variabile), length.out = 100),
mean = mean(variabile),
sd = sd(variabile)),
col = "darkgreen", lwd = 2)
# utilizzo questi metodi per rendere l'asse delle y più leggibile,
# senza notazioni scientifiche.
# i valori esprimono la densità di probabilità,
# cioè una misura di quanto i dati sono concentrati in un certo intervallo.
y_ticks <- pretty(dens$y)
y_labels <- format(y_ticks, scientific = FALSE)
axis(2, at = y_ticks, labels = y_labels)
}
# calcolo le stats per le colonne con variabili quantitative:
# sales:
sales_iv <- eda(re_texas$sales)$stats
# calcolo indici di forma:
sales_if <- indici_f(re_texas$sales)$stats
# volume:
vol_iv <- eda(re_texas$volume)$stats
# calcolo indici di forma
vol_if <- indici_f(re_texas$volume)$stats
# median_price
mp_iv <- eda(re_texas$median_price)$stats
# calcolo indici di forma
mp_if <- indici_f(re_texas$median_price)$stats
# listings:
listings_iv <- eda(re_texas$listings)$stats
# calcolo indici di forma
listings_if <- indici_f(re_texas$listings)$stats
# months_inventory:
mi_iv <- eda(re_texas$months_inventory)$stats
# calcolo indici di forma
mi_if <- indici_f(re_texas$months_inventory)$stats
retexas_var_stats <- data.frame(etichette = c(
"Min", "Max", "Mediana", "Media", "SD", "IQR", "CV"),
sales = sales_iv,
volume = vol_iv,
median_price = mp_iv,
listings = listings_iv,
months_inventory = mi_iv)
knitr::kable(retexas_var_stats, row.names = FALSE)
| etichette | sales | volume | median_price | listings | months_inventory |
|---|---|---|---|---|---|
| Min | 79.00000 | 8.16600 | 73800.00000 | 743.00000 | 3.400000 |
| Max | 423.00000 | 83.54700 | 180000.00000 | 3296.00000 | 14.900000 |
| Mediana | 175.50000 | 27.06250 | 134500.00000 | 1618.50000 | 8.950000 |
| Media | 192.29167 | 31.00519 | 132665.41667 | 1738.02083 | 9.192500 |
| SD | 79.65111 | 16.65145 | 22662.14869 | 752.70776 | 2.303669 |
| IQR | 120.00000 | 23.23350 | 32750.00000 | 1029.50000 | 3.150000 |
| CV | 41.42203 | 53.70536 | 17.08218 | 43.30833 | 25.060306 |
retexas_id_f_stats <- data.frame(
etichette = c(
"Fisher", "curtosi"),
sales = sales_if,
volume = vol_if,
median_price = mp_if,
listings = listings_if,
months_inventory = mi_if)
knitr::kable(retexas_id_f_stats, row.names = FALSE)
| etichette | sales | volume | median_price | listings | months_inventory |
|---|---|---|---|---|---|
| Fisher | 0.7136206 | 0.8792182 | -0.3622768 | 0.6454431 | 0.0407194 |
| curtosi | -0.3355200 | 0.1505673 | -0.6427292 | -0.8101534 | -0.1979448 |
# visuzalizzo i grafici per le varibaili
dens_viz(re_texas$sales, 'Sales')
dens_viz(re_texas$volume, 'Volume')
dens_viz(re_texas$median_price, 'Median Price')
dens_viz(re_texas$listings, 'Listings')
dens_viz(re_texas$months_inventory, 'Months Inventory')
Le variabili Sales e Volume presentano una lieve asimmetria positiva, confermata sia dall’indice di asimmetria sia dal grafico di densità. La curva risulta abbastanza vicina alla normale teorica nella parte centrale, ma mostra una coda destra più pronunciata. Nel complesso, le due variabili non evidenziano forti deviazioni dalla campana normale, pur non risultando perfettamente gaussiane. Median Price mostra invece una lieve asimmetria negativa, mentre Months Inventory appare quasi simmetrica e complessivamente vicina a una distribuzione normale. La variabile Listings, infine, presenta una distribuzione meno regolare e apparentemente multimodale, con più concentrazioni di valori e un andamento che si discosta in modo più evidente da una singola curva gaussiana.
Le variabili sono misurate su scale differenti e i coefficienti di variazione risultano diversi tra loro. Questo indica che la variabilità relativa non è omogenea, ma differisce sensibilmente da una variabile all’altra. Ad esempio, volume mostra una dispersione relativa più elevata, mentre median_price ne presenta una minore: il CV passa da ~17 a ~54, volume risulta molto meno omogenea in termini di variabilità relativa rispetto a median_price. Questo aspetto va considerato nell’interpretazione complessiva dei dati, perché può incidere sulla robustezza delle conclusioni e sulla lettura comparata delle variabili.
In termini assoluti, la variabile che presenta l’asimmetria maggiore si conferma essere ‘volume’, con un indice di Fisher pari a ~0.88 presenta una asimmetria positiva. Al contrario, la variabile che si avvicina a una forma simmetrica è ‘months_inventory’: con un indice di Fisher pari a 0.04, mostra una distribuzione quasi simile a una normale. La variabile ‘median_price’ (-0.36) mostra invece una lieve asimmetria negativa.
Analizzando l’indice di curtosi in eccesso, emerge che quasi tutte le variabili quantitative presentano valori negativi ma estremamente prossimi allo zero (in particolare ‘months_inventory’ con ~-0.20 e ‘volume’ con ~0.15. Questo indica che queste distribuzioni sono quasi perfettamente mesocurtiche, ovvero molto vicine alla forma della normale teorica. L’unica eccezione è rappresentata dalla variabile ‘listings’, che registra una curtosi pari a ~-0.81: la campana risulta più schiacciata.
# suddivido in classi le osservazioni della variabile 'sales'
re_texas$sales_class <- cut(re_texas$sales, breaks = c(49, 99, 149, 199, 249, 299, 349, 399, 449))
# mostro la tabella di frequenza per le classi della variabile 'sales_class'
kable(tab_freq(re_texas, re_texas$sales_class, 'Sales Class'), row.names = FALSE)
| Sales Class | freq_ass | freq_rel | freq_pct |
|---|---|---|---|
| (49,99] | 20 | 0.0833 | 8.33 |
| (99,149] | 69 | 0.2875 | 28.75 |
| (149,199] | 58 | 0.2417 | 24.17 |
| (199,249] | 33 | 0.1375 | 13.75 |
| (249,299] | 34 | 0.1417 | 14.17 |
| (299,349] | 14 | 0.0583 | 5.83 |
| (349,399] | 9 | 0.0375 | 3.75 |
| (399,449] | 3 | 0.0125 | 1.25 |
# mostro il grafico
ggplot(re_texas)+
geom_bar(aes(x=sales_class),
fill = 'red',
colour = 'black')+
labs(title=paste('Frequenze Assolute di Sales Class'))+
theme_classic()
Il grafico presenta una distribuzione asimmetrica positiva, con la maggior parte dei valori concentrati tra 99 e 199. Questo risultato indica che i numeri di vendite concluse si concentrano pricipalmente in quell’intervallo. Possiamo notare un secondo gruppo di vendite concentrato nell’intervallo 199 e 249. Questa evidenza grafica è coerente con un indice di asimmetria positivo: conferma i valori più alti per le classi iniziali; valori che vanno diminuendo man mano che le classi aumenetano, più alto è il valore, meno frequente è la vendita.
# calcolo l'indice di eterogeneità di Gini creando una funzione
gini.index <- function(col_nome){
ni<- table(col_nome)
fi<- ni/length(col_nome)
fi2<- fi^2
J<- length(table(col_nome))
# avendo tutti gli elementi calcolo gini
gini<- 1-sum(fi2)
gini.normalizzato<- round(gini/((J-1)/J), 2)
cat("Il valore dell'indice di Gini normalizzato è pari a:", gini.normalizzato)
}
gini.index(re_texas$sales_class)
## Il valore dell'indice di Gini normalizzato è pari a: 0.92
Il valore dell’indice di Gini è pari a 0.92, posso dunque concludere che i valori sono moderatamnte eterogenei
# Per il totale di righe uso già l'oggetto N
N <- dim(re_texas)[1]
# calcolo la probabilità che da un estrazione esca Beaumont
prob_beaumont <- sum(re_texas$city == "Beaumont")/N
# La probabilità è di 0.25, c'è quindi il 25% di possibilità che venga estratta
# calcolo la probabilità che da un estrazione esca Luglio
prob_luglio <- round(sum(re_texas$month == 7)/N, 2)
# La probabilità è di ~0.08, c'è quindi circa l'8% di possibiltà che venga estratta
# calcolo la probabilità che da un estrazione esca Dicembre e 2012
prob_anno_mese <- round(sum(re_texas$month == 12 & re_texas$year == 2012)/N, 2)
# La probabilità è di 0.02, c'è quindi circa il 2% di possibiltà che venga estratta
re_texas$prezzo_medio_mln <- round((re_texas$volume*1000000)/re_texas$sales, 3)
# controllo il range
range(re_texas$prezzo_medio_mln)
## [1] 97010.2 213233.9
Il prezzo medio varia da un minimo di ~97 e un massimo di~213 mila dollari. Considerando che il prezzo mediano si muove in un range tra ~73 e 180 mila posso concludere che all’interno della variabile sales sono presenti immobili con valori alti, degli outliers che di conseguenza spingono il alto in prezzo medio. Per questo è più corretto per Texas Reality Insight considerare il prezzo mediano, in quanto più rappresentativo e robusto rispetto a al prezzo medio.
re_texas$sales_eff <- round(re_texas$sales/re_texas$listings, 2)
# creo uno scatter mettendo in relazione months_inventory e sales_eff
ggplot(data=re_texas) +
geom_point(aes(x=re_texas$sales_eff,
y=re_texas$months_inventory),
colour = 'red')+
labs(x='Indice di efficienza',
y='Mesi Necessari',
title='Rapporto tra efficienza delle vendite e tempo per chiudere la vendita')+
theme_bw()
Lo scatter plot mostra che: osservazioni con livelli simili di efficienza possono presentare valori molto diversi di months inventory. Questo suggerisce che l’efficienza non dipende esclusivamente dalla velocità di assorbimento del mercato, ma anche da altri fattori. In generale, valori più elevati di months inventory sono associati a un mercato più lento e/o a una minore capacità di smaltire rapidamente le inserzioni.
# creo un boxplot mettendo in relazione l'efficienza nei singoli mesi di riferimento
ggplot(data=re_texas) +
geom_boxplot(aes(x=factor(month),
y=sales_eff),
colour = 'red')+
labs(x='Mese',
y='Indice di efficienza',
title='Rapporto tra mese di riferimento ed efficienza delle vendite')+
theme_bw()
Il boxplot suggerisce che: l’indice di efficienza tende a essere leggermente più elevato nei mesi centrali dell’anno. In particolare, tra primavera ed estate si osservano mediane superiori rispetto ai mesi iniziali e finali. La presenza di alcuni outlier nei mesi centrali indica inoltre una maggiore variabilità delle osservazioni in questi periodi.
# definisco una funzione per automatizzare l'EDA
eda_2 <- function(data, g_col, var_col){
data |>
group_by({{g_col}}) |>
summarise(media=mean({{var_col}}),
std=sd({{var_col}}))
}
# creo l'istanza chiamando la funzione creata: raggruppo per city e sales
city_sales <- eda_2(re_texas, city, volume)
kable(city_sales, row.names = FALSE)
| city | media | std |
|---|---|---|
| Beaumont | 26.13160 | 6.970384 |
| Bryan-College Station | 38.19160 | 17.248577 |
| Tyler | 45.76738 | 13.107146 |
| Wichita Falls | 13.93017 | 3.239766 |
# creo l'istanza chiamando la funzione creata: raggruppo per year e volume
year_volume <- eda_2(re_texas, year, volume)
kable(year_volume, row.names = FALSE)
| year | media | std |
|---|---|---|
| 2010 | 25.67590 | 10.79510 |
| 2011 | 25.15781 | 12.20349 |
| 2012 | 29.26756 | 14.52269 |
| 2013 | 35.15240 | 17.93470 |
| 2014 | 39.77227 | 21.18628 |
# creo l'istanza chiamando la funzione creata: raggruppo per city e sales
month_list <- eda_2(re_texas, month, listings)
kable(month_list,row.names = FALSE)
| month | media | std |
|---|---|---|
| 1 | 1647.05 | 704.6140 |
| 2 | 1692.50 | 711.2004 |
| 3 | 1756.70 | 727.3546 |
| 4 | 1825.70 | 770.4287 |
| 5 | 1823.85 | 790.2234 |
| 6 | 1833.25 | 811.6288 |
| 7 | 1821.20 | 826.7196 |
| 8 | 1786.30 | 815.8664 |
| 9 | 1748.90 | 802.6563 |
| 10 | 1710.35 | 779.1649 |
| 11 | 1652.70 | 741.2533 |
| 12 | 1557.75 | 692.5678 |
# rappresento graficamente i risultati, creo una funzione per un grafico a linea
line_viz <- function(dati, col_1, col_2){
ggplot(data = dati)+
geom_line(aes(x = {{col_1}}, y = {{col_2}}))+
theme_bw()
}
# visualizzo un grafico a barre per la combinazione città/vendite
ggplot(data = city_sales)+
geom_bar(aes(x = city, y = media),
stat = "identity",
fill ='red')+
theme_bw()
# visualizzo i trend temporali di 'year' e 'month' con un grafico a linee
line_viz(year_volume, year, media)
line_viz(month_list, month, media)
# BOXPLOT: distribuzione del prezzo mediano tra le città
ggplot(data = re_texas)+
geom_boxplot(aes(x = as.factor(year), y = median_price, fill = city))+
labs(title = "Distribuzione del prezzo mediano tra le città",
x = 'Anno',
y = 'Prezzo Mediano',
fill='Città',
)+
theme_bw()+
theme(
plot.title = element_text(size = 35),
axis.title = element_text(size = 30),
axis.text = element_text(size = 30),
legend.title = element_text(size = 25),
legend.text = element_text(size=20)
)
# BARPLOT: confronto il totale delle vendite raggruppate per mese e città
month_city_volume <- re_texas |>
summarise(.by = c(city, month, year),
tot_volume = sum(volume))
# visualizzo il grafico a barre
ggplot(data = month_city_volume)+
geom_col(aes(x = month, y = tot_volume,
fill = city),
position = position_dodge())+
facet_wrap(~year)+
labs(title = "Totale delle vendite raggruppate per mese e città",
x = 'Mesi',
y = 'Totale volume')+
scale_x_continuous(breaks = seq(1,12,1))+
theme_bw()+
theme(axis.text.x = element_text(size = 4))
# LINECHART: confronto il totale delle vendite per anni differenti
year_tot_sales <- re_texas |>
summarise(.by = c(year, month),
tot_volume = sum(volume))
year_tot_sales$date <- as.Date(paste(year_tot_sales$year, year_tot_sales$month, "01", sep = "-"))
# visualizzo il grafico a linee, una linea per anno di vendite raggruppate
ggplot(data = year_tot_sales)+
geom_line(aes(x = date, y = tot_volume, color = as.factor(year)))+
labs(title = "Andamento dei volumi delle vendite per anno",
x = 'Anni',
y = 'Totale volume',
color = 'Anno')+
scale_x_date(breaks = seq(as.Date("2010-01-01"), as.Date("2015-01-01"), by = "year"))+
theme_bw()
L’analisi descrittiva del dataset evidenzia un mercato caratterizzato da una stagionalità marcata, durante i mesi estivi si registrano picchi nelle vendite concluse e nel volume generato
Dal punto di vista della variabilità, le variabili quantitative presentano CV diversi e dunque una variabilità relativa eterogenea: il prezzo mediano mostra una forte stabilità sul territorio (CV più contenuto), mentre i volumi complessivi di vendita presentano oscillazioni maggiori nel tempo. Per qunato riguarda la forma presentano invece una leggera asimmetria, sia positiva che negativa, a seconda della variabile esaminata.
Le nuove variabili costruite suggeriscono, con forza diversa, trend temporali simili. Mettono in evidenza eterogeneità territoriale e differenze di efficienza: in particolare, l’efficienza delle vendite e il prezzo medio suggeriscono differenze nella capacità di assorbimento del mercato e nella presenza di valori estremi. I valori del prezzo, inoltre, differiscono in relazione all’area geografica; evidenziato dalla distribuzione dei valori nel grafico del boxplot
Nel complesso, i risultati indicano un contesto eterogeneo ma interpretabile, in cui la lettura congiunta di distribuzioni, indici sintetici e grafici permette di individuare con chiarezza le principali tendenze del mercato.