Syftet med denna analys är att bygga en prediktiv regressionsmodell för att förutsäga försäljningspris på begagnade Volvo-bilar baserat på information från Blocket. Vi utför en Exploratory Data Analysis (EDA), tränar två modeller – en enkel linjär modell och en förbättrad modell med kategoriska variabler – och utvärderar deras prestanda.
Vi fokuserar särskilt på hur variabler som miltal, modellår, hästkrafter, motorstorlek, bränsletyp och växellåda påverkar priset.
Vi tränar två modeller: - Modell 1: En linjär regression på log-transformerat pris med endast numeriska variabler. - Modell 2: En förbättrad modell som även inkluderar bränsletyp och växellåda (kategoriska variabler).
För att hantera skeva data log-transformerar vi prisvariabeln. Tränings- och testdatan delas upp i 80/20.
| Modell | RMSE (kr) | R² | MAE (kr) |
|---|---|---|---|
| Modell 1 | 61,760 | 0.882 | 41,610 |
| Modell 2 | 57,130 | 0.903 | 36,670 |
Tolkning: Modell 2 presterar bättre på samtliga mått, särskilt i R² (andel förklarad variation) och MAE (genomsnittligt absolutfel).
🔍 Nästa steg skulle kunna vara att inkludera ytterligare faktorer som bilens utrustningspaket, miljöklass, eller antal ägare för ännu mer exakta förutsägelser.
📢 Rapporten publiceras online via RPubs.
url <- "https://github.com/siffror/Regressionsmodellering_R_Bilannonser/raw/main/R_Volvo_data_bilar.xlsx"
temp <- tempfile(fileext = ".xlsx")
download.file(url, temp, mode = "wb")
data <- read_excel(temp)
## New names:
## • `` -> `...16`
## • `` -> `...17`
## • `` -> `...18`
## • `` -> `...19`
## • `` -> `...20`
colnames(data) <- make.names(colnames(data))
data <- data %>%
select(-starts_with("...")) %>%
mutate(
Försäljningspris = as.numeric(gsub("[^0-9]", "", Försäljningspris)),
Miltal = as.numeric(gsub("[^0-9]", "", Miltal)),
Hästkrafter = as.numeric(gsub("[^0-9]", "", Hästkrafter)),
Motorstorlek = as.numeric(gsub("[^0-9]", "", Motorstorlek)),
Bränsle = as.factor(Bränsle),
Växellåda = as.factor(Växellåda)
)
ggplot(data, aes(x = Försäljningspris)) +
geom_histogram(binwidth = 25000, fill = "steelblue", color = "white") +
scale_x_continuous(labels = comma) +
labs(title = "Fördelning av försäljningspris", x = "Pris (kr)", y = "Antal bilar")
ggplot(data, aes(x = Modellår)) +
geom_histogram(binwidth = 1, fill = "steelblue", color = "white") +
theme_minimal()
data %>%
group_by(Modellår) %>%
summarise(Antal = n(), MedelPris = mean(Försäljningspris, na.rm = TRUE), MedelMiltal = mean(Miltal, na.rm = TRUE)) %>%
arrange(desc(Modellår))
## # A tibble: 45 × 4
## Modellår Antal MedelPris MedelMiltal
## <dbl> <int> <dbl> <dbl>
## 1 2025 22 569086. 578.
## 2 2024 35 489166. 2350.
## 3 2023 59 464454. 5424.
## 4 2022 82 389990. 7776.
## 5 2021 72 327033. 10502.
## 6 2020 55 311825. 11816.
## 7 2019 51 283408. 12407.
## 8 2018 53 267408. 16699.
## 9 2017 48 216542. 14251.
## 10 2016 40 186825 17615.
## # ℹ 35 more rows
numvars <- data %>% select(where(is.numeric))
cor(numvars, use = "complete.obs")
## Försäljningspris Miltal Modellår Hästkrafter
## Försäljningspris 1.0000000 -0.7213941 0.7535959 0.746077287
## Miltal -0.7213941 1.0000000 -0.6438703 -0.414666541
## Modellår 0.7535959 -0.6438703 1.0000000 0.492512439
## Hästkrafter 0.7460773 -0.4146665 0.4925124 1.000000000
## Motorstorlek -0.2582150 0.3452840 -0.3675741 0.008098585
## Motorstorlek
## Försäljningspris -0.258214969
## Miltal 0.345284041
## Modellår -0.367574144
## Hästkrafter 0.008098585
## Motorstorlek 1.000000000
ggplot(data, aes(x = Bränsle, y = Försäljningspris)) +
geom_boxplot(fill = "lightgreen") +
scale_y_continuous(labels = comma)
ggplot(data, aes(x = Växellåda, y = Försäljningspris)) +
geom_boxplot(fill = "skyblue") +
scale_y_continuous(labels = comma)
# Standardisera nivåer: gör alla till samma form
data$Bränsle <- str_to_title(data$Bränsle)
data$Bränsle <- as.factor(data$Bränsle)
set.seed(123)
train_index <- createDataPartition(data$Försäljningspris, p = 0.8, list = FALSE)
train_data <- data[train_index, ]
test_data <- data[-train_index, ]
train_data$logPris <- log(train_data$Försäljningspris)
test_data$logPris <- log(test_data$Försäljningspris)
model_log <- lm(logPris ~ Miltal + Modellår + Hästkrafter + Motorstorlek, data = train_data)
summary(model_log)
##
## Call:
## lm(formula = logPris ~ Miltal + Modellår + Hästkrafter + Motorstorlek,
## data = train_data)
##
## Residuals:
## Min 1Q Median 3Q Max
## -6.6443 -0.1187 0.0352 0.1598 3.5336
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) -1.356e+02 7.240e+00 -18.722 < 2e-16 ***
## Miltal -2.999e-05 2.318e-06 -12.938 < 2e-16 ***
## Modellår 7.312e-02 3.578e-03 20.437 < 2e-16 ***
## Hästkrafter 2.099e-03 2.764e-04 7.595 1.09e-13 ***
## Motorstorlek 1.138e-04 6.042e-05 1.883 0.0602 .
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 0.4793 on 639 degrees of freedom
## (38 observations deleted due to missingness)
## Multiple R-squared: 0.7725, Adjusted R-squared: 0.7711
## F-statistic: 542.4 on 4 and 639 DF, p-value: < 2.2e-16
pred_log <- predict(model_log, newdata = test_data)
pred <- exp(pred_log)
pred[pred < 0] <- 0
resultat1 <- postResample(pred, test_data$Försäljningspris)
# Snygg tabell
resultat1_df <- data.frame(
Mått = c("RMSE", "R-squared", "MAE"),
Värde = c(resultat1["RMSE"], resultat1["Rsquared"], resultat1["MAE"])
)
resultat1_df$Värde <- format(round(as.numeric(resultat1_df$Värde), 0), big.mark = " ")
knitr::kable(resultat1_df, caption = "📊 Modell 1: Prestanda på testdata")
| Mått | Värde | |
|---|---|---|
| RMSE | RMSE | 61 760 |
| Rsquared | R-squared | 1 |
| MAE | MAE | 41 610 |
ggplot(data.frame(Verkligt = test_data$Försäljningspris, Prediktion = pred),
aes(x = Verkligt, y = Prediktion)) +
geom_point(alpha = 0.6) +
geom_abline(slope = 1, intercept = 0, color = "red", linetype = "dashed") +
scale_x_continuous(labels = comma) +
scale_y_continuous(labels = comma)
## Warning: Removed 9 rows containing missing values or values outside the scale range
## (`geom_point()`).
shapiro.test(residuals(model_log))
##
## Shapiro-Wilk normality test
##
## data: residuals(model_log)
## W = 0.66189, p-value < 2.2e-16
bptest(model_log)
##
## studentized Breusch-Pagan test
##
## data: model_log
## BP = 38.301, df = 4, p-value = 9.714e-08
durbinWatsonTest(model_log)
## lag Autocorrelation D-W Statistic p-value
## 1 0.05313078 1.889682 0.108
## Alternative hypothesis: rho != 0
vif(model_log)
## Miltal Modellår Hästkrafter Motorstorlek
## 1.772803 1.987607 1.450296 1.238397
par(mfrow = c(2, 2))
plot(model_log)
par(mfrow = c(1, 1))
model2 <- lm(logPris ~ Miltal + Modellår + Hästkrafter + Motorstorlek + Bränsle + Växellåda, data = train_data)
summary(model2)
##
## Call:
## lm(formula = logPris ~ Miltal + Modellår + Hästkrafter + Motorstorlek +
## Bränsle + Växellåda, data = train_data)
##
## Residuals:
## Min 1Q Median 3Q Max
## -6.8569 -0.1253 0.0259 0.1604 3.4906
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) -1.012e+02 8.153e+00 -12.407 < 2e-16 ***
## Miltal -3.004e-05 2.249e-06 -13.356 < 2e-16 ***
## Modellår 5.624e-02 4.020e-03 13.990 < 2e-16 ***
## Hästkrafter 2.824e-03 4.086e-04 6.912 1.17e-11 ***
## Motorstorlek -1.271e-04 8.051e-05 -1.579 0.114866
## BränsleDiesel 1.596e-01 4.803e-02 3.322 0.000944 ***
## BränsleEl -3.924e-01 2.431e-01 -1.614 0.107089
## BränsleMiljöbränsle/Hybrid -1.129e-01 7.533e-02 -1.499 0.134312
## VäxellådaManuell -3.605e-01 5.156e-02 -6.992 6.87e-12 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 0.4538 on 635 degrees of freedom
## (38 observations deleted due to missingness)
## Multiple R-squared: 0.7974, Adjusted R-squared: 0.7948
## F-statistic: 312.3 on 8 and 635 DF, p-value: < 2.2e-16
test_data_fixed <- test_data %>%
filter(Bränsle %in% levels(train_data$Bränsle)) %>%
filter(Växellåda %in% levels(train_data$Växellåda))
# Prediktion
pred_log2 <- predict(model2, newdata = test_data_fixed)
pred2 <- exp(pred_log2)
pred2[pred2 < 0] <- 0
# Utvärdera modellen
resultat <- postResample(pred2, test_data_fixed$Försäljningspris)
# Visa i snygg tabell
resultat_df <- data.frame(
Metrik = c("RMSE (kr)", "R-squared", "MAE (kr)"),
Värde = c(round(resultat["RMSE"], 0),
round(resultat["Rsquared"], 3),
round(resultat["MAE"], 0))
)
knitr::kable(resultat_df, caption = "🔎 Modellens prestanda på testdata")
| Metrik | Värde | |
|---|---|---|
| RMSE | RMSE (kr) | 57132.000 |
| Rsquared | R-squared | 0.903 |
| MAE | MAE (kr) | 36671.000 |
✅ Ju lägre RMSE och MAE, desto bättre. Ju högre R², desto mer förklarar modellen.
ggplot(data.frame(Verkligt = test_data_fixed$Försäljningspris, Prediktion = pred2),
aes(x = Verkligt, y = Prediktion)) +
geom_point(alpha = 0.6) +
geom_abline(slope = 1, intercept = 0, color = "red", linetype = "dashed") +
scale_x_continuous(labels = comma) +
scale_y_continuous(labels = comma)
## Warning: Removed 9 rows containing missing values or values outside the scale range
## (`geom_point()`).
shapiro.test(residuals(model2))
##
## Shapiro-Wilk normality test
##
## data: residuals(model2)
## W = 0.62287, p-value < 2.2e-16
bptest(model2)
##
## studentized Breusch-Pagan test
##
## data: model2
## BP = 35.385, df = 8, p-value = 2.275e-05
durbinWatsonTest(model2)
## lag Autocorrelation D-W Statistic p-value
## 1 0.02112962 1.955853 0.448
## Alternative hypothesis: rho != 0
vif(model2)
## GVIF Df GVIF^(1/(2*Df))
## Miltal 1.861638 1 1.364419
## Modellår 2.799305 1 1.673112
## Hästkrafter 3.536061 1 1.880442
## Motorstorlek 2.453535 1 1.566377
## Bränsle 5.336303 3 1.321925
## Växellåda 1.702078 1 1.304637
par(mfrow = c(2, 2))
plot(model2)
par(mfrow = c(1, 1))
### 🔍 Tolkning
- Miltal har negativ effekt på priset
- Modellår positiv effekt
- Växellåda och bränsletyp påverkar också priset signifikant
- Modell 2 ger bättre resultat än modell 1 tack vare inkludering av faktorer