📘 Inledning

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.

🧠 Metodik

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.

📊 Sammanfattning av modellprestanda

Modell RMSE (kr) 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).

📌 Slutsats

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

Ladda in data från GitHub

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`

Struktur och omvandling

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

EDA – Histogram och sammanställningar

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

Korrelation och boxplots

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)

Uppdelning i träning/test och log-transformering

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)

Modell 1 – utan kategorier (logPris)

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

Modell 1: Prediktion & utvärdering

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")
📊 Modell 1: Prestanda på testdata
Mått Värde
RMSE RMSE 61 760
Rsquared R-squared 1
MAE MAE 41 610

Modell 1: Visualisering

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

Diagnostik modell 1

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

Modell 2 – med kategorier

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

Filtrering testdata & prediktion

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")
🔎 Modellens prestanda på testdata
Metrik Värde
RMSE RMSE (kr) 57132.000
Rsquared R-squared 0.903
MAE MAE (kr) 36671.000

📊 Förklaring av modellens prestandamått

Ju lägre RMSE och MAE, desto bättre. Ju högre R², desto mer förklarar modellen.

Modell 2: Visualisering

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

Diagnostik modell 2

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

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