Texas Realty Insights aims to analyze real estate market trends in the state of Texas by analyzing historical real estate sales data. The goal is to provide statistical and visual insights to support strategic decisions regarding sales and the optimization of real estate advertisements.
The objectives of the project are as follows:
The dataset contains the following variables:
First of all, we want to improve the presentation of the tables by defining a dedicated function using kable.
# ----------------------- TABLE FORMATTING FUNCTION -----------------------
show_table <- function(data, digits = 2, scrollable = FALSE){
# data: input data frame to display
# digits: number of decimal places to show in numeric columns
# scrollable: logical, whether to enable horizontal scrolling
numeric_cols <- sapply(data, is.numeric)
data[numeric_cols] <- lapply(data[numeric_cols], round, digits = digits)
table <- kable(data) %>%
kable_styling(
bootstrap_options = c("striped", "hover", "condensed", "bordered"),
full_width = FALSE,
position = "center"
)
if (scrollable == TRUE) {
table <- table %>%
scroll_box(width = "100%", height = "auto")
}
return(table)
}
# -------------------------------------------------------------------------Now we load the CSV dataset from the given URL, inspect its dimensions, verify the presence of missing values, and display the first ten records.
# Load dataset
dataset_url <- "https://drive.google.com/uc?export=download&id=1O4If8876MTwstkrZX0BqpQ_BxcsIMEko"
dataset <- read.csv(dataset_url)
# Inspect the dimensions of dataset
records <- dim(dataset)[1]
variables <- dim(dataset)[2]
# Verify whether the dataset contains any missing values
cat("The dataset",
ifelse(anyNA(dataset), "contains", "does not contain"),
"missing values.\n")
# Display the first 10 records
show_table(head(dataset, 10))## The dataset does not contain missing values.
| city | year | month | sales | volume | median_price | listings | months_inventory |
|---|---|---|---|---|---|---|---|
| Beaumont | 2010 | 1 | 83 | 14.16 | 163800 | 1533 | 9.5 |
| Beaumont | 2010 | 2 | 108 | 17.69 | 138200 | 1586 | 10.0 |
| Beaumont | 2010 | 3 | 182 | 28.70 | 122400 | 1689 | 10.6 |
| Beaumont | 2010 | 4 | 200 | 26.82 | 123200 | 1708 | 10.6 |
| Beaumont | 2010 | 5 | 202 | 28.83 | 123100 | 1771 | 10.9 |
| Beaumont | 2010 | 6 | 189 | 27.22 | 122800 | 1803 | 11.1 |
| Beaumont | 2010 | 7 | 164 | 22.71 | 124300 | 1857 | 11.7 |
| Beaumont | 2010 | 8 | 174 | 25.24 | 136800 | 1830 | 11.6 |
| Beaumont | 2010 | 9 | 124 | 17.23 | 121100 | 1829 | 11.7 |
| Beaumont | 2010 | 10 | 150 | 23.90 | 138500 | 1779 | 11.5 |
The dataset contains 8 variables and 240 records. Let’s show the structure of the dataset and the types of variables.
## '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 ...
We can identify the types of statistical variables in the dataset as follows:
We can carry out different types of analysis depending on the variables in the dataset:
Before starting our analysis, let’s attach the dataset so that the variables are accessible in the workspace.
Now we want to handle the time-based variables more effectively. To do so, we will add to the dataset a date column in day-month-year format, setting the day to the first day of each month, so that we can later perform time series analysis.
# Add the date column to the dataset
dataset$date <- as.Date(paste("01", month, year, sep = "-"), format = "%d-%m-%Y")Now we want to convert the months (labeled as \(1\), \(2\), \(3\), etc.) into text month names for clearer visualization.
dataset$month <- factor(
dataset$month,
levels = 1:12,
labels = c("Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec")
)The dataset is now ready for analysis. Let’s inspect its final structure.
## 'data.frame': 240 obs. of 9 variables:
## $ city : chr "Beaumont" "Beaumont" "Beaumont" "Beaumont" ...
## $ year : int 2010 2010 2010 2010 2010 2010 2010 2010 2010 2010 ...
## $ month : Factor w/ 12 levels "Jan","Feb","Mar",..: 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 ...
## $ date : Date, format: "2010-01-01" "2010-02-01" ...
In this section we aim to compute the measures of central tendency, dispersion and shape for the quantitative variables (sales, volume, median_price, listings and months_inventory).
For the other variables (city, year, month), which can all be considered categorical variables, we will show the tables of absolute and relative frequencies of their categories.
Let’s create a function that displays the statistical measures all in one. In particular, we want to evaluate:
# ---------------------- STATISTICAL SUMMARY FUNCTION ----------------------
stat_summary <- function(df, variables){
# df: data frame containing the data to analyze
# variables: names of the columns to summarize
results <- list()
for(v in variables){
x <- df[[v]]
if(is.numeric(x)){
mu <- mean(x, na.rm = TRUE)
sigma <- sd(x, na.rm = TRUE)
min_val <- min(x, na.rm = TRUE)
max_val <- max(x, na.rm = TRUE)
results[[v]] <- c(
# Measures of central tendency
mean = mu,
min = min_val,
q1 = as.numeric(quantile(x, probs = 0.25, na.rm = TRUE)),
median = median(x, na.rm = TRUE),
q3 = as.numeric(quantile(x, probs = 0.75, na.rm = TRUE)),
max = max_val,
# Measures of variability
variance = var(x, na.rm = TRUE),
std_dev = sigma,
range = max_val - min_val,
interquartile_range = IQR(x, na.rm = TRUE),
cv = ifelse(mu == 0, NA, sigma/mu),
# Measures of shape
skewness_index = skewness(x, na.rm = TRUE),
kurtosis_index = kurtosis(x, na.rm = TRUE) - 3
)
}
}
# Convert the list of results into a data frame
stats <- as.data.frame(results)
# Assign names to the statistics
rownames(stats) <- c(
"Mean value", "Minimum", "First quartile (Q1)", "Median value",
"Third quartile (Q3)", "Maximum", "Variance", "Standard deviation",
"Range", "Interquartile range", "Coefficient of variation",
"Fisher's Skewness", "Excess Kurtosis"
)
return(stats)
}
# --------------------------------------------------------------------------# Compute statistical measures for the selected variables
stats <- stat_summary(dataset, c("sales", "volume", "median_price", "listings", "months_inventory"))
# Display the statistical summary rounded with 2 decimal
show_table(stats)| sales | volume | median_price | listings | months_inventory | |
|---|---|---|---|---|---|
| Mean value | 192.29 | 31.01 | 132665.42 | 1738.02 | 9.19 |
| Minimum | 79.00 | 8.17 | 73800.00 | 743.00 | 3.40 |
| First quartile (Q1) | 127.00 | 17.66 | 117300.00 | 1026.50 | 7.80 |
| Median value | 175.50 | 27.06 | 134500.00 | 1618.50 | 8.95 |
| Third quartile (Q3) | 247.00 | 40.89 | 150050.00 | 2056.00 | 10.95 |
| Maximum | 423.00 | 83.55 | 180000.00 | 3296.00 | 14.90 |
| Variance | 6344.30 | 277.27 | 513572983.09 | 566568.97 | 5.31 |
| Standard deviation | 79.65 | 16.65 | 22662.15 | 752.71 | 2.30 |
| Range | 344.00 | 75.38 | 106200.00 | 2553.00 | 11.50 |
| Interquartile range | 120.00 | 23.23 | 32750.00 | 1029.50 | 3.15 |
| Coefficient of variation | 0.41 | 0.54 | 0.17 | 0.43 | 0.25 |
| Fisher’s Skewness | 0.72 | 0.88 | -0.36 | 0.65 | 0.04 |
| Excess Kurtosis | -0.31 | 0.18 | -0.62 | -0.79 | -0.17 |
As we can see, the quantitative variables contain information on very different scales and orders of magnitude. Let’s summarize some important observations:
Now let’s proceed creating a function that displays the distribution of frequencies for the qualitative variables.
# -------------------- FREQUENCY DISTRIBUTION FUNCTION --------------------
freq_distribution <- function(df, variable){
# df: dataframe containing the data to analyze
# variable: name of the column to evaluate
x <- df[[variable]]
N <- length(x)
ni <- table(x) # absolute frequencies
fi <- ni/N # relative frequencies
freq_summary <- data.frame(
labels = names(ni),
abs_freq = as.vector(ni),
rel_freq = as.vector(fi)
)
colnames(freq_summary) <- c(variable, "Frequency", "Relative frequency")
return(freq_summary)
}
# -------------------------------------------------------------------------Now we can visualize the frequency tables for the qualitative variables.
# Frequency distribution of city
freqs_city <- freq_distribution(dataset, "city")
show_table(freqs_city)| city | Frequency | Relative frequency |
|---|---|---|
| Beaumont | 60 | 0.25 |
| Bryan-College Station | 60 | 0.25 |
| Tyler | 60 | 0.25 |
| Wichita Falls | 60 | 0.25 |
# Frequency distribution of year
freqs_year <- freq_distribution(dataset, "year")
show_table(freqs_year)| year | Frequency | Relative frequency |
|---|---|---|
| 2010 | 48 | 0.2 |
| 2011 | 48 | 0.2 |
| 2012 | 48 | 0.2 |
| 2013 | 48 | 0.2 |
| 2014 | 48 | 0.2 |
# Frequency distribution of month
freqs_month <- freq_distribution(dataset, "month")
show_table(freqs_month)| month | Frequency | Relative frequency |
|---|---|---|
| Jan | 20 | 0.08 |
| Feb | 20 | 0.08 |
| Mar | 20 | 0.08 |
| Apr | 20 | 0.08 |
| May | 20 | 0.08 |
| Jun | 20 | 0.08 |
| Jul | 20 | 0.08 |
| Aug | 20 | 0.08 |
| Sep | 20 | 0.08 |
| Oct | 20 | 0.08 |
| Nov | 20 | 0.08 |
| Dec | 20 | 0.08 |
We can observe that the frequency distribution is uniform within each qualitative variable, as all categories occur with the same frequency in the dataset.
Since the quantitative variables have very different scales, an useful estimator for measuring variability is the coefficient of variation: it is calculated as the standard deviation divided by the mean (\(CV = \frac{\sigma}{\bar{x}}\)), and it measures the relative dispersion of a variable. As shown in the statistical summary above, the variable with the highest coefficient of variation is volume, that represent the total sales value; instead, median_price has the lowest relative dispersion.
Regarding the asymmetry of the variables, this can be evaluated with Fisher’s skewness, a measure of deviation from symmetry. Once again, volume is the most asymmetric variable, as it exhibits the highest absolute value of skewness. Specifically, it shows positive skewness, meaning that most observations are concentrated on the left side of the distribution (at low values), while a few very high values extend the right tail of the distribution.
We can visualize all these characteristics of the volume distribution using a density plot.
# Density plot of volume
ggplot(dataset, aes(x = volume)) +
geom_density(col = "black", fill = "skyblue") +
labs(
title = "Density plot of volume",
x = "Volume (million $)",
y = "Density"
) +
scale_x_continuous(breaks = seq(0, max(volume), by = 10)) +
coord_cartesian(xlim = c(min(volume), max(volume))) +
theme_light() +
theme(
plot.title = element_text(size = 18, hjust = 0.5, face = "bold", color = "red3"),
axis.text.x = element_text(face = "bold"),
axis.text.y = element_text(face = "bold"),
axis.title.x = element_text(size = 13, margin = margin(t = 10)),
axis.title.y = element_text(size = 13, margin = margin(r = 10))
)The first goal of this section is to select the quantitative variable
sales, divide it into classes and create a frequency
distribution.
Let’s start computing the range of interval of
sales.
# Check the min and max values of sales
min_sales <- stats["Minimum", "sales"]
max_sales <- stats["Maximum", "sales"]
range_sales <- stats["Range", "sales"]
cat("Sales minimum value:", min_sales, "\n")
cat("Sales maximum value:", max_sales, "\n")
cat("Sales range:", range_sales)## Sales minimum value: 79
## Sales maximum value: 423
## Sales range: 344
We can see that sales has a minimum value of 79 and a maximum value of 423, with a range of 344. We may choose to divide the observations into \(7\) classes with amplitude \(50\), starting from \(75\) and ending at \(425\): in this way, we obtain classes that are easy to interpret and that cover the entire range of the sales variable. We now create these classes and compute the corresponding frequency distribution.
# Divide sales into 7 classes with amplitude 50
sales_classes <- cut(sales, breaks = seq(from=75, to=425, by=50))
# Evaluate frequencies
N <- length(sales)
ni_sales <- table(sales_classes)
fi_sales <- ni_sales/N
Ni_sales <- cumsum(ni_sales)
Fi_sales <- Ni_sales/N
# Create the dataframe of frequencies
sales_freq <- data.frame(
labels = names(ni_sales),
freq = as.vector(ni_sales),
rel_freq = as.vector(fi_sales),
cum_freq = as.vector(Ni_sales),
cum_rel_freq = as.vector(Fi_sales)
)
colnames(sales_freq) <- c("Sales classes", "Frequency", "Relative frequency",
"Cumulative frequency", "Cumulative relative frequency")
# Display the frequency distribution rounded with 4 decimal
show_table(sales_freq, digits = 4)| Sales classes | Frequency | Relative frequency | Cumulative frequency | Cumulative relative frequency |
|---|---|---|---|---|
| (75,125] | 59 | 0.2458 | 59 | 0.2458 |
| (125,175] | 61 | 0.2542 | 120 | 0.5000 |
| (175,225] | 46 | 0.1917 | 166 | 0.6917 |
| (225,275] | 30 | 0.1250 | 196 | 0.8167 |
| (275,325] | 26 | 0.1083 | 222 | 0.9250 |
| (325,375] | 13 | 0.0542 | 235 | 0.9792 |
| (375,425] | 5 | 0.0208 | 240 | 1.0000 |
We can observe that both the first quartile and the median value fall in the class \((125,175]\), while the third quartile falls in the class \((225,275]\). The modal class is \((125,175]\).
Now let’s visualize the computed frequencies using a column chart.
# Set the correct order of the classes for the plot
sales_freq$`Sales classes` <- factor(
sales_freq$`Sales classes`,
levels = sales_freq$`Sales classes`
)
# Plot a column chart of sales frequency
ggplot(sales_freq, aes(x = `Sales classes`, y = Frequency)) +
geom_col(color = "black", fill = "darkorange") +
geom_text(aes(label = Frequency), vjust = -0.3, size=3, fontface="bold") +
labs(
title = "Distribution of sales classes",
x = "Sales classes",
y = "Frequency"
) +
scale_y_continuous(breaks=seq(0, max(sales_freq$Frequency), by=10)) +
theme_minimal() +
coord_cartesian(ylim = c(0, max(sales_freq$Frequency) * 1.05)) +
theme(
plot.title = element_text(size=18, hjust = 0.5, face = "bold", color="red3"),
axis.text.x = element_text(face = "bold"),
axis.text.y = element_text(face = "bold"),
axis.title.x = element_text(size = 13, margin = margin(t = 10)),
axis.title.y = element_text(size = 13, margin = margin(r = 10))
)We can notice that the distribution of sales classes is right skewed, meaning that most records are concentrated at low sales class values.
Now we want to calculate the normalized Gini index to measure the heterogeneity of the frequency distribution across the classes of the sales variable. The normalized Gini index has a range of \([0, 1]\) and is computed as follows: \[ G = \frac{J}{J-1} \left(1 - \sum_{i=1}^{J} f_i^2 \right) \] where \(f_i\) are the relative frequencies of the \(J\) classes. Let’s create a function to calculate the normalized Gini index.
# ---------- NORMALIZED GINI INDEX FUNCTION ----------
gini_norm_index <- function(x){
# x = variable to calculate normalized Gini index
ni <- table(x)
fi <- ni/length(x)
J <- length(ni)
gini_norm <- (J / (J-1)) * (1 - sum(fi^2))
return(gini_norm)
}
# ----------------------------------------------------# Evaluate normalized Gini index of sales classes
gini_norm_sales <- gini_norm_index(sales_classes)
cat("Normalized Gini index of sales classes =", round(gini_norm_sales, 4))## Normalized Gini index of sales classes = 0.9421
The normalized Gini index is very close to \(1\), which means that the class frequencies of sales are highly heterogeneously distributed.
This section will present some examples of probability calculations, let’s look at them one by one. We will use the frequency distribution computed in the previous sections.
Let’s start by calculating the probability that a random observation in the dataset belongs to the city “Beaumont”: this corresponds to the relative frequency of that city in the cities frequency distribution.
# Probability that city is Beaumont
p_Beaumont <- freqs_city[freqs_city$city == "Beaumont", "Relative frequency"]
cat(paste("Probability (city = Beaumont) =", round(100*p_Beaumont, 2), "%"))## Probability (city = Beaumont) = 25 %
Now let’s compute the probability that a random record contains the month of July: once again, this corresponds to the relative frequency of the month “Jul” in the monthly frequency distribution.
p_July <- freqs_month[freqs_month$month == "Jul", "Relative frequency"]
cat(paste("Probability (month = July) =", round(100*p_July, 2), "%"))## Probability (month = July) = 8.33 %
Finally, let’s evaluate the probability that a random observation refers to the month of December 2012: it corresponds to the compound probability that a random record contains the month “Dec” and the year 2012, so it’s equal to the product of the two relative frequencies.
p_December <- freqs_month[freqs_month$month == "Dec", "Relative frequency"]
p_2012 <- freqs_year[freqs_year$year == "2012", "Relative frequency"]
p_December_2012 <- p_December * p_2012
cat(paste("Probability (date = December 2012) =", round(100*p_December_2012, 2), "%"))## Probability (date = December 2012) = 1.67 %
Let’s create, in the dataset, two new columns using the available variables:
A new column contains the average property price
per unit sold.
The mean price is computed as the volume
(total sales value in million of dollars) divided by the sales
(the total number of sales). Then, the mean_price column will
be converted in dollars to align it with the unit of measurement for the
median price.
A new column measures the efficiency of sales
advertisements.
A useful indicator for estimating the
effectiveness of sales ads can be defined as the sales divided
by the listings; this metric measures the average number of
sales made per active listing and, therefore, how quickly the ads
convert into sales.
# Evaluate the mean price, avoiding division by zero
dataset$mean_price <- ifelse(sales == 0, NA, (volume/sales) * 10^6)
# Evaluate the efficiency of listings, avoiding division by zero
dataset$efficiency <- ifelse(listings == 0, NA, sales/listings)
attach(dataset)
# Show the dataset with the two new variables
show_table(head(dataset, 5)) | city | year | month | sales | volume | median_price | listings | months_inventory | date | mean_price | efficiency |
|---|---|---|---|---|---|---|---|---|---|---|
| Beaumont | 2010 | Jan | 83 | 14.16 | 163800 | 1533 | 9.5 | 2010-01-01 | 170626.5 | 0.05 |
| Beaumont | 2010 | Feb | 108 | 17.69 | 138200 | 1586 | 10.0 | 2010-02-01 | 163796.3 | 0.07 |
| Beaumont | 2010 | Mar | 182 | 28.70 | 122400 | 1689 | 10.6 | 2010-03-01 | 157697.8 | 0.11 |
| Beaumont | 2010 | Apr | 200 | 26.82 | 123200 | 1708 | 10.6 | 2010-04-01 | 134095.0 | 0.12 |
| Beaumont | 2010 | May | 202 | 28.83 | 123100 | 1771 | 10.9 | 2010-05-01 | 142737.6 | 0.11 |
We can summarize the statistical measure of the two new quantitative variables.
| mean_price | efficiency | |
|---|---|---|
| Mean value | 154320.37 | 0.12 |
| Minimum | 97010.20 | 0.05 |
| First quartile (Q1) | 132938.94 | 0.09 |
| Median value | 156588.48 | 0.11 |
| Third quartile (Q3) | 173915.15 | 0.13 |
| Maximum | 213233.94 | 0.39 |
| Variance | 736984385.27 | 0.00 |
| Standard deviation | 27147.46 | 0.05 |
| Range | 116223.74 | 0.34 |
| Interquartile range | 40976.22 | 0.05 |
| Coefficient of variation | 0.18 | 0.39 |
| Fisher’s Skewness | -0.07 | 2.09 |
| Excess Kurtosis | -0.78 | 6.88 |
Let’s visualize the density plot of mean_price.
# Density plot of mean price
ggplot(dataset, aes(x = mean_price)) +
geom_density(col = "black", fill = "skyblue") +
labs(
title = "Density plot of mean price",
x = "Mean price ($)",
y = "Density"
) +
scale_x_continuous(breaks = seq(0, max(mean_price), by = 25000)) +
coord_cartesian(xlim = c(min(mean_price), max(mean_price))) +
theme_light() +
theme(
plot.title = element_text(size = 18, hjust = 0.5, face = "bold", color = "red3"),
axis.text.x = element_text(face = "bold"),
axis.text.y = element_text(face = "bold"),
axis.title.x = element_text(size = 13, margin = margin(t = 10)),
axis.title.y = element_text(size = 13, margin = margin(r = 10))
)Looking at the variable mean_price, we observe a a small coefficient of variation, indicating limited variability in the distribution; in addition, the distribution shows a slightly negative skewness and is platykurtic. We can assirt that mean_price appears to be a fairly stable indicator for measuring real estate prices.
Now let’s visualize the density plot of efficiency.
# Density plot of efficiency
ggplot(dataset, aes(x = efficiency)) +
geom_density(col = "black", fill = "skyblue") +
labs(
title = "Density plot of efficiency",
x = "Efficiency",
y = "Density"
) +
coord_cartesian(xlim = c(min(efficiency), max(efficiency))) +
theme_light() +
theme(
plot.title = element_text(size = 18, hjust = 0.5, face = "bold", color = "red3"),
axis.text.x = element_text(face = "bold"),
axis.text.y = element_text(face = "bold"),
axis.title.x = element_text(size = 13, margin = margin(t = 10)),
axis.title.y = element_text(size = 13, margin = margin(r = 10))
)Regarding the variable efficiency, we observe a higher
coefficient of variation, indicating moderate variability. Moreover, the
distribution is strongly positively skewed and exhibits very high
positive kurtosis, which means it is significantly leptokurtic.
The
pronounced right tail tells us that most observations show low
efficiency, while only a few observations are better at converting
listings into sales.
Overall, the listing efficacy varies considerably across observations, suggesting that efficiency is not uniform throughout the dataset.
In this section we will perform conditional statistical analysis by city, year and month. More specifically, the goal is to consider the quantitative variables and calculate statistics for each category of the qualitative variables.
First, let’s create a function to perform a conditional analysis on a given set of quantitative variables, grouped by one categorical variable.
# -------------------- CONDITIONAL ANALYSIS FUNCTION --------------------
conditional_analysis <- function(df, quant_vars, categ_var){
# df: data frame containing the data to analyze
# quant_vars: vector of quantitative variables to summarise
# categ_var: categorical variable used for grouping
# Compute mean and standard deviation of quantitative variables
# grouped by each category of the qualitative variable
stats <- df %>%
group_by(across(all_of(categ_var))) %>%
summarise(
across(
all_of(quant_vars),
list(avg = mean, std = sd),
.names = "{.fn}({.col})"
),
.groups = "drop"
)
# Reshape the summary table for better visualization
stats_transposed <- stats %>%
pivot_longer(
cols = -all_of(categ_var),
names_to = "statistic",
values_to = "value"
) %>%
pivot_wider(
names_from = all_of(categ_var),
values_from = value
)
return(stats_transposed)
}
# -----------------------------------------------------------------------We will apply this function to all the quantitative variables in the dataset, grouped by the categorical variables city, year, and month. We will report the mean and the standard deviation, and then plot bar charts showing the average volume, used here as the main variable of interest, for each categorical variable.
Let’s summarize the statistics grouped by city, and then plot a bar chart of average volume by city.
# Summarize statistics grouping by city
city_stats <- conditional_analysis(
dataset,
c("sales", "volume", "median_price", "listings", "months_inventory", "mean_price", "efficiency"),
"city"
)
# Show statistics rounded to 2 decimal places
show_table(city_stats)| statistic | Beaumont | Bryan-College Station | Tyler | Wichita Falls |
|---|---|---|---|---|
| avg(sales) | 177.38 | 205.97 | 269.75 | 116.07 |
| std(sales) | 41.48 | 84.98 | 61.96 | 22.15 |
| avg(volume) | 26.13 | 38.19 | 45.77 | 13.93 |
| std(volume) | 6.97 | 17.25 | 13.11 | 3.24 |
| avg(median_price) | 129988.33 | 157488.33 | 141441.67 | 101743.33 |
| std(median_price) | 10104.99 | 8852.24 | 9336.54 | 11320.03 |
| avg(listings) | 1679.32 | 1458.13 | 2905.05 | 909.58 |
| std(listings) | 91.13 | 252.53 | 226.75 | 73.76 |
| avg(months_inventory) | 9.97 | 7.66 | 11.32 | 7.82 |
| std(months_inventory) | 1.65 | 2.25 | 1.89 | 0.78 |
| avg(mean_price) | 146640.41 | 183534.29 | 167676.76 | 119430.00 |
| std(mean_price) | 11232.13 | 15149.35 | 12350.51 | 11398.48 |
| avg(efficiency) | 0.11 | 0.15 | 0.09 | 0.13 |
| std(efficiency) | 0.03 | 0.07 | 0.02 | 0.02 |
# Convert data frame in long format, filtering average volume
volume_by_city <- city_stats %>%
filter(statistic == "avg(volume)") %>%
pivot_longer(
cols = -statistic,
names_to = "city",
values_to = "value"
)
# Plot a bar chart of average volume by city
ggplot(volume_by_city, aes(
x = reorder(city, value),
y = value
))+
geom_col(color = "black", fill = "darkseagreen") +
labs(
title = "Average volume by city",
x = "City",
y = "Average volume (million $)"
) +
scale_y_continuous(breaks=seq(0, max(volume_by_city$value), by=5)) +
theme_minimal() +
theme(
plot.title = element_text(size = 18, hjust = 0.5, face = "bold", color = "red3"),
axis.text.x = element_text(face = "bold"),
axis.text.y = element_text(face = "bold"),
axis.title.x = element_text(size = 13, margin = margin(t = 10)),
axis.title.y = element_text(size = 12, margin = margin(r = 10))
)We can see that Tyler is the Texas city with the highest average volume, followed by Bryan-College Station and Beaumont, while Wichita Falls has the lowest average volume. From the summary above we can also notice that Bryan-College Station, followed by Tyler, shows very high dispersion in the volume distribution, while the variability of volume in Beaumont and Wichita Falls remains relatively low.
Now we summarize the statistics grouped by year, and then plot a bar chart of average volume by year.
# Summarize statistics grouping by year
year_stats <- conditional_analysis(
dataset,
c("sales", "volume", "median_price", "listings", "months_inventory", "mean_price", "efficiency"),
"year"
)
# Show statistics rounded to 2 decimal places
show_table(year_stats)| statistic | 2010 | 2011 | 2012 | 2013 | 2014 |
|---|---|---|---|---|---|
| avg(sales) | 168.67 | 164.12 | 186.15 | 211.92 | 230.60 |
| std(sales) | 60.54 | 63.87 | 70.91 | 84.00 | 95.51 |
| avg(volume) | 25.68 | 25.16 | 29.27 | 35.15 | 39.77 |
| std(volume) | 10.80 | 12.20 | 14.52 | 17.93 | 21.19 |
| avg(median_price) | 130191.67 | 127854.17 | 130077.08 | 135722.92 | 139481.25 |
| std(median_price) | 21821.76 | 21317.80 | 21431.52 | 21708.08 | 25625.41 |
| avg(listings) | 1826.00 | 1849.65 | 1776.81 | 1677.60 | 1560.04 |
| std(listings) | 785.02 | 780.38 | 738.45 | 743.52 | 706.71 |
| avg(months_inventory) | 9.97 | 10.90 | 9.88 | 8.15 | 7.06 |
| std(months_inventory) | 2.08 | 2.07 | 1.61 | 1.69 | 1.75 |
| avg(mean_price) | 150188.58 | 148250.63 | 150898.68 | 158705.25 | 163558.70 |
| std(mean_price) | 23279.55 | 24938.38 | 26438.50 | 26523.81 | 31740.53 |
| avg(efficiency) | 0.10 | 0.09 | 0.11 | 0.13 | 0.16 |
| std(efficiency) | 0.03 | 0.02 | 0.03 | 0.04 | 0.06 |
# Convert data frame in long format, filtering average volume
volume_by_year <- year_stats %>%
filter(statistic == "avg(volume)") %>%
pivot_longer(
cols = -statistic,
names_to = "year",
values_to = "value"
)
# Plot a bar chart of average volume by year
ggplot(volume_by_year, aes(
x = year,
y = value
))+
geom_col(color = "black", fill = "wheat") +
labs(
title = "Average volume by year",
x = "Year",
y = "Average volume (million $)"
) +
scale_y_continuous(breaks=seq(0, max(volume_by_year$value), by=5)) +
theme_minimal() +
theme(
plot.title = element_text(size = 18, hjust = 0.5, face = "bold", color = "red3"),
axis.text.x = element_text(face = "bold"),
axis.text.y = element_text(face = "bold"),
axis.title.x = element_text(size = 13, margin = margin(t = 10)),
axis.title.y = element_text(size = 12, margin = margin(r = 10))
)From the bar chart above we can see that the total average volume of real estate was approximately \(26\) million dollars in 2010, then it decreased slightly in 2011, and gradually increased in the following years, reaching about \(40\) million dollars in 2014. We can also observe that this increase is not driven by significant changes in the mean property price, but rather by an increase in sales over the years. Moreover, we can observe that volume variability has generally increased over the years, more than doubling between 2010 and 2014.
Finally, we summarize the statistics grouped by month, and then plot a bar chart of average volume by month.
# Summarize statistics grouping by month
month_stats <- conditional_analysis(
dataset,
c("sales", "volume", "median_price", "listings", "months_inventory", "mean_price", "efficiency"),
"month"
)
# Show statistics rounded to 2 decimal places
show_table(month_stats, scrollable = TRUE)| statistic | Jan | Feb | Mar | Apr | May | Jun | Jul | Aug | Sep | Oct | Nov | Dec |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| avg(sales) | 127.40 | 140.85 | 189.45 | 211.70 | 238.85 | 243.55 | 235.75 | 231.45 | 182.35 | 179.90 | 156.85 | 169.40 |
| std(sales) | 43.38 | 51.07 | 59.18 | 65.40 | 83.12 | 95.00 | 96.27 | 79.23 | 72.52 | 74.95 | 55.47 | 60.75 |
| avg(volume) | 19.00 | 21.65 | 29.38 | 33.30 | 39.70 | 41.30 | 39.12 | 38.01 | 29.60 | 29.08 | 24.81 | 27.09 |
| std(volume) | 8.37 | 10.09 | 12.02 | 14.52 | 19.02 | 21.08 | 21.41 | 18.05 | 15.22 | 15.13 | 11.15 | 12.57 |
| avg(median_price) | 124250.00 | 130075.00 | 127415.00 | 131490.00 | 134485.00 | 137620.00 | 134750.00 | 136675.00 | 134040.00 | 133480.00 | 134305.00 | 133400.00 |
| std(median_price) | 25151.28 | 22822.59 | 23442.03 | 21458.40 | 18796.26 | 19231.02 | 21944.78 | 22488.38 | 24344.10 | 26358.07 | 24691.47 | 22809.76 |
| avg(listings) | 1647.05 | 1692.50 | 1756.70 | 1825.70 | 1823.85 | 1833.25 | 1821.20 | 1786.30 | 1748.90 | 1710.35 | 1652.70 | 1557.75 |
| std(listings) | 704.61 | 711.20 | 727.35 | 770.43 | 790.22 | 811.63 | 826.72 | 815.87 | 802.66 | 779.16 | 741.25 | 692.57 |
| avg(months_inventory) | 8.84 | 9.06 | 9.40 | 9.72 | 9.68 | 9.70 | 9.62 | 9.39 | 9.19 | 8.94 | 8.66 | 8.12 |
| std(months_inventory) | 1.97 | 1.98 | 2.06 | 2.24 | 2.38 | 2.41 | 2.50 | 2.45 | 2.52 | 2.44 | 2.37 | 2.27 |
| avg(mean_price) | 145640.42 | 148840.48 | 151136.54 | 151461.33 | 158235.03 | 161545.82 | 156881.00 | 156455.56 | 156522.32 | 155897.37 | 154233.00 | 154995.52 |
| std(mean_price) | 29819.11 | 25120.42 | 23237.92 | 26174.30 | 25787.19 | 23470.46 | 27220.12 | 28253.21 | 29669.41 | 32527.29 | 29684.87 | 27008.87 |
| avg(efficiency) | 0.08 | 0.09 | 0.12 | 0.13 | 0.14 | 0.14 | 0.14 | 0.14 | 0.11 | 0.11 | 0.10 | 0.12 |
| std(efficiency) | 0.02 | 0.02 | 0.03 | 0.04 | 0.05 | 0.06 | 0.07 | 0.05 | 0.03 | 0.04 | 0.03 | 0.04 |
# Convert data frame in long format, filtering average volume
volume_by_month <- month_stats %>%
filter(statistic == "avg(volume)") %>%
pivot_longer(
cols = -statistic,
names_to = "month",
values_to = "value"
)
# Order months in calendar order
volume_by_month$month <- factor(
volume_by_month$month,
levels = c("Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec")
)
# Plot a bar chart of sales by month
ggplot(volume_by_month, aes(
x = month,
y = value
))+
geom_col(color = "black", fill = "lightsteelblue") +
labs(
title = "Average volume by month",
x = "Month",
y = "Average volume (million $)"
) +
scale_y_continuous(breaks=seq(0, max(volume_by_month$value), by=5)) +
theme_minimal() +
theme(
plot.title = element_text(size = 18, hjust = 0.5, face = "bold", color = "red3"),
axis.text.x = element_text(face = "bold"),
axis.text.y = element_text(face = "bold"),
axis.title.x = element_text(size = 13, margin = margin(t = 10)),
axis.title.y = element_text(size = 12, margin = margin(r = 10))
)This chart is useful for visualizing changes in average volume across the different months, regardless of the reference year. It allows us to assess the seasonality of volume: we can observe that June has the highest average volume, followed by May and July; on the other hand, January has the lowest average volume, followed by February and November. Moreover, the period from May to August shows the highest average volume, due to an increase in sales. We can also observe that volume dispersion generally follows the same trend of average monthly volume, so the months with the highest volume are also those with the highest volume variability, and vice versa.
To enhance our statistical analysis and gain a deeper understanding of the real estate market, this section presents a variety of visualizations created with the ggplot2 package.
Let’s visualize a boxplot showing the distribution of median_price by city.
# Boxplot of median price by city
ggplot(dataset, aes(
x = city,
y = median_price
)) +
geom_boxplot(
color = "black",
fill = "mistyrose",
outlier.size = 2
) +
labs(
title = "Boxplot of median price by city",
x = "City",
y = "Median price ($)"
) +
theme_light() +
theme(
plot.title = element_text(size = 18, hjust = 0.5, face = "bold", color = "red3"),
axis.text.x = element_text(face = "bold"),
axis.text.y = element_text(face = "bold"),
axis.title.x = element_text(size = 13, margin = margin(t = 10)),
axis.title.y = element_text(size = 13, margin = margin(r = 10))
)We can summarize the boxplot statistics in the table below, including the number of outliers for each city, and the skewness of median price distribution.
# Boxplot statistics of median price by city
outlier_median_price_by_city <- dataset %>%
group_by(city) %>%
summarise(
Q1 = quantile(median_price, 0.25),
median = median(median_price),
Q3 = quantile(median_price, 0.75),
IQR = IQR(median_price),
lower_bound = Q1 - 1.5 * IQR,
upper_bound = Q3 + 1.5 * IQR,
n_outliers = sum(median_price < lower_bound | median_price > upper_bound),
skewness = skewness(median_price)
)
show_table(outlier_median_price_by_city)| city | Q1 | median | Q3 | IQR | lower_bound | upper_bound | n_outliers | skewness |
|---|---|---|---|---|---|---|---|---|
| Beaumont | 123025 | 130750 | 134550 | 11525 | 105737.5 | 151837.5 | 1 | 0.36 |
| Bryan-College Station | 150800 | 155400 | 161975 | 11175 | 134037.5 | 178737.5 | 1 | 0.71 |
| Tyler | 133975 | 142200 | 147675 | 13700 | 113425.0 | 168225.0 | 0 | 0.12 |
| Wichita Falls | 92800 | 102300 | 109175 | 16375 | 68237.5 | 133737.5 | 1 | 0.22 |
We can extract some information from the boxplot and the summary:
Now we present a bar chart illustrating the total sales by month and city. This visualization will help us to assess the seasonality of sales across the different cities.
# Compute total sales by city and month
sales_by_city_month <- dataset %>%
group_by(city, month) %>%
summarise(total_sales = sum(sales))
# Plot a bar chart of sales by city and month
ggplot(sales_by_city_month, aes(
x = month,
y = total_sales,
fill = city
))+
geom_col(
position = position_dodge(width = 0.9),
width = 0.6
) +
labs(
title = "Total sales by month and city",
x = "Month",
y = "Sales",
fill = "City"
) +
theme_minimal() +
theme(
plot.title = element_text(size = 18, hjust = 0.5, face = "bold", color = "red3"),
axis.text.x = element_text(face = "bold"),
axis.text.y = element_text(face = "bold"),
axis.title.x = element_text(size = 13, margin = margin(t = 10)),
axis.title.y = element_text(size = 13, margin = margin(r = 10))
)From the bar chart above we can observe that:
Now let’s plot a normalized stacked bar chart showing the proportion of sales by month and city.
# Plot a normalized stacked bar chart of sales by city and month
ggplot(sales_by_city_month, aes(
x = month,
y = total_sales,
fill = city
))+
geom_col(position = "fill", width = 0.6) +
scale_y_continuous(
labels = scales::percent,
breaks = seq(0, 1, 0.1)
) +
labs(
title = "Percentage of sales by month and city",
x = "Month",
y = "Percentage of Sales",
fill = "City"
) +
theme_minimal() +
theme(
plot.title = element_text(size = 18, hjust = 0.5, face = "bold", color = "red3"),
axis.text.x = element_text(face = "bold"),
axis.text.y = element_text(face = "bold"),
axis.title.x = element_text(size = 13, margin = margin(t = 10)),
axis.title.y = element_text(size = 12, margin = margin(r = 10))
)Once again, we can see that Tyler consistently represents the largest share of sales, followed by Bryan-College Station and Beaumont, while Wichita Falls accounts for the smallest percentage of sales. The distribution among the cities appears fairly stable over the months, with small fluctuations in each city’s share.
To the previous visualization, we can add the variable year showing one stacked bar chart for each year in the dataset.
# Plot normalized stacked bar charts of sales by city and month, one bar chart per year
ggplot(dataset, aes(
x = month,
y = sales,
fill = city
))+
geom_col(position = "fill", width = 0.6) +
facet_wrap(~ year, ncol = 1) +
scale_x_discrete(drop = FALSE) +
scale_y_continuous(
labels = scales::percent,
breaks = seq(0, 1, 0.2)
) +
labs(
title = "Percentage of sales by year, month and city",
x = "Month",
y = "Percentage of Sales",
fill = "City"
) +
theme_minimal() +
theme(
plot.title = element_text(size = 22, hjust = 0.5, face = "bold", color = "red3"),
strip.text = element_text(size = 14, face = "bold"),
strip.background = element_rect(fill = "grey90", color = "grey50"),
axis.text.x = element_text(size = 12, face = "bold"),
axis.text.y = element_text(size = 10, face = "bold"),
axis.title.x = element_text(size = 16, margin = margin(t = 10)),
axis.title.y = element_text(size = 16, margin = margin(r = 10)),
legend.title = element_text(size = 14, face = "bold"),
legend.text = element_text(size = 12),
legend.key.size = unit(0.8, "cm")
)The percentage composition of sales by month and city appears relatively stable across years. Tyler systematically accounts for the largest share of sales over years, while Wichita Falls has the lowest percentage of sales over years.
Let’s plot a line chart showing the sales trend using the date variable. This will allow us to observe how sales evolve across different time periods.
# Compute monthly sales
monthly_sales <- dataset %>%
group_by(date) %>%
summarise(sum_sales = sum(sales), .groups = "drop") %>%
arrange(date)
# Line chart of sales trend
ggplot(monthly_sales, aes(
x = date,
y = sum_sales
)) +
geom_line(col = "lightblue3", linewidth = 1) +
geom_point(col="steelblue4", size = 1.5) +
scale_x_date(
date_breaks = "3 months",
date_labels = "%b %Y"
) +
labs(
title = "Sales trend",
x = "Date",
y = "Total Sales"
) +
theme_minimal() +
theme(
plot.title = element_text(size = 18, hjust = 0.5, face = "bold", color = "red3"),
axis.text.x = element_text(size = 6, face = "bold"),
axis.text.y = element_text(face = "bold"),
axis.title.x = element_text(size = 13, margin = margin(t = 10)),
axis.title.y = element_text(size = 13, margin = margin(r = 10))
)From the line chart above we can clearly observe a recurrent pattern in sales trend. Similar to the average monthly volume studied before, sales also exhibit a seasonal pattern, reaching their highest values in summer and their lowest in winter. Moreover, as with the average volume, the annual mean of sales decreases from 2010 to 2011 and then increases from 2011 to 2014.
Let’s use a boxplot to visualize the distribution of volume grouped by city and year.
# Boxplot of volume by city and year
ggplot(dataset, aes(
x = city,
y = volume,
fill = factor(year)
)) +
geom_boxplot(
color = "black",
outlier.size = 2
) +
scale_fill_brewer(palette = "Set2") +
labs(
title = "Boxplot of volume by city and year",
x = "City",
y = "Volume (million $)",
fill = "Year"
) +
theme_light() +
theme(
plot.title = element_text(size = 18, hjust = 0.5, face = "bold", color = "red3"),
axis.text.x = element_text(face = "bold"),
axis.text.y = element_text(face = "bold"),
axis.title.x = element_text(size = 13, margin = margin(t = 10)),
axis.title.y = element_text(size = 12, margin = margin(r = 10))
)From the boxplot we can extract some insights:
Overall, the boxplot suggests a growth trend in volume over time for Texas cities, with increasing dispersion in the cities with the highest volumes as seen before.
Now we want to plot a line chart showing the average
efficiency (the mean of ratio sales/listings)
grouped by city and year.
This will allow us to study how the
advertisement efficiency varies over time and across cities.
# Compute the average efficiency by city and year
efficiency_by_city_year <- dataset %>%
group_by(city, year) %>%
summarise(avg_efficiency = mean(efficiency))
# Line chart of average efficiency by city and year
ggplot(efficiency_by_city_year, aes(
x = year,
y = avg_efficiency,
color = city,
group = city
)) +
geom_line(linewidth = 1.2) +
geom_point(size = 1.8) +
scale_y_continuous(breaks = seq(0, max(efficiency_by_city_year$avg_efficiency)*1.1, by = 0.025)) +
labs(
title = "Average efficiency by city and year",
x = "Year",
y = "Average efficiency",
color = "City"
) +
theme_minimal() +
theme(
plot.title = element_text(size = 18, hjust = 0.5, face = "bold", color = "red3"),
axis.text.x = element_text(size = 6, face = "bold"),
axis.text.y = element_text(face = "bold"),
axis.title.x = element_text(size = 13, margin = margin(t = 10)),
axis.title.y = element_text(size = 13, margin = margin(r = 10))
)From the line chart of average efficiency we can observe that:
From the statistical analysis conducted on the dataset from Texas Realty Insights, we can extract some useful information to understand the real estate market situation and optimize sales strategies. Let’s summarize the key insights.
General upward trend of market over the years.
Annual sales and volume declined between 2010 and
2011, and then increase from 2011 to 2014.
Advertisements
efficiency followed a similar pattern,suggesting a potential
association between improved listing effectiveness and the overall
growth of the market over time.
More specifically, total sales
value was approximately 25.7 million $ in 2010 and grew to 39.8 million
$ in 2014. The ads efficiency increased from 9.97% in 2010 to 15.7% in
2014, a good change that still has plenty of room for
improvement.
Significant Seasonality of sales and volume.
Both sales and volume exhibit a clear seasonal pattern, reaching their highest values during the summer and their lowest values in winter. June achieves the best performance in terms of sales and volume, whereas January shows the worst results. The company could take advantage of this seasonal trend by improving listing effectiveness during the winter months and maximizing sales during the summer.
Sales and volume vary considerably across cities.
The ranking of cities in terms of both sales and volume is as follows:
It has been observed that the percentage share of sales across cities has remained relatively stable over time.
Geographic variations in median prices.
Median prices exhibit marked differences across cities. In descending order, the median price rank is as follows:
It may be useful for the company to examine if there is a correlation between median property prices and the performance of each city.
Different trends in advertisements efficiency across cities.
Tyler is the only city that shows a steady increase in ads efficiency over time. However, despite this upward trend, this metric remains the lowest among the cities. Given that Tyler also has the highest volume, improving advertisement efficiency could have a substantial impact on market performance.
Beaumont and Bryan-College Station both experienced a slight decline in efficiency from 2010 to 2011, which could help to explain the modest decrease in sales and volume observed during that period. In the following years, from 2011 to 2014, listing efficacy increased, especially in Bryan-College Station, which is the city with the best advertisement efficiency overall.
Wichita Falls, by contrast, exhibits a less stable trend, with a decline in efficiency from 2010 to 2011, a slight increase until 2013, and another decline in 2014. It may be interesting for the company to investigate the reasons behind this pattern.