1 Εισαγωγή

Σε αυτήν την εργασία εφαρμόζουμε Δέντρα Απόφασης (Decision Trees) χρησιμοποιώντας τη μέθοδο CART (Classification and Regression Trees) στα ιστορικά δεδομένα τιμών της μετοχής Microsoft (MSFT). Ο στόχος είναι να ταξινομήσουμε αν η τιμή κλεισίματος της επόμενης ημέρας θα είναι ανοδική (Up) ή καθοδική (Down) σε σχέση με την τρέχουσα.

Η ανάλυση ακολουθεί τα 4 βήματα του μαθήματος:

  1. Συλλογή & Προετοιμασία Δεδομένων
  2. Επιλογή Αλγορίθμου
  3. Εκπαίδευση του Μοντέλου
  4. Αξιολόγηση & Βελτιστοποίηση

2 Βήμα 1: Συλλογή & Προετοιμασία Δεδομένων

2.1 Σχολιασμός/Παρουσίαση του Dataset

Για την ανάλυση επιλέχθηκε το dataset Microsoft Stock Data το οποίο περιλαμβάνει ιστορικά δεδομένα της μετοχής της Microsoft. Περιέχει πληροφορίες για την καθημερινή τιμή της μετοχής στο χρηματιστήριο.

  • Date: η ημερομηνία της συναλλαγής

  • Open: η τιμή της μετοχής όταν άνοιξε το χρηματιστήριο την συγκεκριμένη ημέρα

  • High: η υψηλότερη τιμή που έφτασε η μετοχή μέσα στη μέρα

  • Low: η χαμηλότερη τιμή της μετοχής μέσα στη μέρα

  • Close: η τιμή της μετοχής όταν έκλεισε το χρηματιστήριο

  • Adj Close: τιμή κλεισίματος προσαρμοσμένη ώστε να αντικατοπτρίζει την αξία μετά τον υπολογισμό τυχόν εταιρικών ενεργειών

  • Volume: ο αριθμός των συναλλαγών που πραγματοποιήθηκαν εκείνη την ημέρα

Kagg

MSFT <- read.csv("MSFT.csv")

#Φόρτωση βιβλιοθηκών
library(tidyverse)  # Εργαλεία διαχείρισης δεδομένων & ggplot2
## ── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
## ✔ dplyr     1.2.0     ✔ readr     2.2.0
## ✔ forcats   1.0.1     ✔ stringr   1.6.0
## ✔ ggplot2   4.0.2     ✔ tibble    3.3.1
## ✔ lubridate 1.9.5     ✔ tidyr     1.3.2
## ✔ purrr     1.2.1     
## ── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
## ✖ dplyr::filter() masks stats::filter()
## ✖ dplyr::lag()    masks stats::lag()
## ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
library(rpart)      # CART Decision Trees
## Warning: package 'rpart' was built under R version 4.5.3
library(rpart.plot) # Οπτικοποίηση δέντρων
## Warning: package 'rpart.plot' was built under R version 4.5.3
library(ggplot2)    # Γραφήματα
library(caTools)    # Διαχωρισμός train/test
## Warning: package 'caTools' was built under R version 4.5.3
library(ROCR)       # ROC καμπύλες & AUC
## Warning: package 'ROCR' was built under R version 4.5.3
# Μετατροπή της στήλης Date σε ημερομηνία
MSFT$Date <- as.Date(MSFT$Date)
MSFT      <- MSFT %>% arrange(Date)
cat("Αριθμός παρατηρήσεων :", nrow(MSFT), "\n")
## Αριθμός παρατηρήσεων : 9083
cat("Αριθμός μεταβλητών  :", ncol(MSFT), "\n")
## Αριθμός μεταβλητών  : 7
cat("Χρονικό διάστημα    :", format(min(MSFT$Date)), "→",
                             format(max(MSFT$Date)), "\n")
## Χρονικό διάστημα    : 1986-03-13 → 2022-03-24
summary(MSFT[, c("Open", "High", "Low", "Close", "Volume")])
##       Open                High                Low           
##  Min.   :  0.08854   Min.   :  0.09201   Min.   :  0.08854  
##  1st Qu.:  4.05078   1st Qu.:  4.10205   1st Qu.:  4.02734  
##  Median : 26.82000   Median : 27.10000   Median : 26.52000  
##  Mean   : 41.32494   Mean   : 41.76089   Mean   : 40.87849  
##  3rd Qu.: 40.03500   3rd Qu.: 40.44375   3rd Qu.: 39.50000  
##  Max.   :344.62000   Max.   :349.67001   Max.   :342.20001  
##      Close               Volume         
##  Min.   :  0.09028   Min.   :2.304e+06  
##  1st Qu.:  4.07520   1st Qu.:3.461e+07  
##  Median : 26.84000   Median :5.203e+07  
##  Mean   : 41.33563   Mean   :5.875e+07  
##  3rd Qu.: 39.93750   3rd Qu.:7.265e+07  
##  Max.   :343.10998   Max.   :1.032e+09

2.2 Οπτικοποίηση Ιστορικής Τιμής

ggplot(MSFT, aes(x = Date, y = Close)) +
  geom_line(color = "purple", linewidth = 0.35, alpha = 0.85) +
  geom_smooth(method = "loess", span = 0.08,
              color = "#007", se = FALSE, linewidth = 1.1) +
  scale_x_date(date_breaks = "5 years", date_labels = "%Y") +
  scale_y_continuous(labels = scales::dollar_format()) +
  labs(
    title    = "Τιμή Κλεισίματος 1986–2022",
    subtitle = "Ημερίσια Τιμή & Τάση",
    x = NULL, y = "Τιμή Κλεισίματος($)"
  ) +
  theme_minimal(base_size = 13) +
  theme(plot.title = element_text(face = "bold"),
        panel.grid.minor = element_blank())
## `geom_smooth()` using formula = 'y ~ x'
Εικ. 1 — Τιμή κλεισίματος MSFT (1986–2022)

Εικ. 1 — Τιμή κλεισίματος MSFT (1986–2022)

2.3 Κατασκευή Χαρακτηριστικών (Feature Engineering)

Μετατρέπουμε τα ακατέργαστα δεδομένα τιμής σε χαρακτηριστικά (features) που περιγράφουν τη συμπεριφορά της αγοράς. Όλα υπολογίζονται αποκλειστικά από παρελθοντικές τιμές, ώστε να αποφύγουμε data leakage.

library(tidyverse)
# Βοηθητική συνάρτηση κινητού μέσου (χωρίς εξωτερικές βιβλιοθήκες)
roll_mean <- function(x, n) {
  stats::filter(x, rep(1/n, n), sides = 1) %>% as.numeric()
}

roll_sd <- function(x, n) {
  sapply(seq_along(x), function(i) {
    if (i < n) NA else sd(x[(i - n + 1):i])
  })
}

MSFT_feat <- MSFT |>
  mutate(
    # --- Αποδόσεις ---
    Daily_Return  = (Close - lag(Close)) / lag(Close) * 100,
    Intraday_Move = (Close - Open)       / Open       * 100,
    Daily_Range   = (High  - Low)        / Open       * 100,

    # --- Κινητοί Μέσοι Όροι ---
    MA5  = roll_mean(Close,  5),
    MA20 = roll_mean(Close, 20),
    MA50 = roll_mean(Close, 50),

    # --- Απόσταση τιμής από ΚΜΟ (%) ---
    Price_vs_MA5  = (Close - MA5)  / MA5  * 100,
    Price_vs_MA20 = (Close - MA20) / MA20 * 100,
    Price_vs_MA50 = (Close - MA50) / MA50 * 100,

    # --- Momentum & Volatility ---
    Momentum_5d    = (Close - lag(Close, 5)) / lag(Close, 5) * 100,
    Volatility_20d = roll_sd(Daily_Return, 20),

    # --- Μεταβολή όγκου ---
    Vol_Change = (Volume - lag(Volume)) / lag(Volume) * 100,

    # --- Μεταβλητή-Στόχος ---
    # Up: η επόμενη τιμή κλεισίματος > η σημερινή
    Target = ifelse(lead(Close) > Close, "Up", "Down")
  ) |>
  drop_na() |>
  mutate(Target = factor(Target, levels = c("Down", "Up")))

cat("Διαστάσεις dataset μετά την προετοιμασία:",
    nrow(MSFT_feat), "γραμμές ×", ncol(MSFT_feat), "στήλες\n\n")
## Διαστάσεις dataset μετά την προετοιμασία: 9033 γραμμές × 20 στήλες
cat("Κατανομή κλάσεων-στόχου:\n")
## Κατανομή κλάσεων-στόχου:
print(table(MSFT_feat$Target))
## 
## Down   Up 
## 4476 4557
cat("\nΠοσοστά (%):\n")
## 
## Ποσοστά (%):
print(round(prop.table(table(MSFT_feat$Target)) * 100, 1))
## 
## Down   Up 
## 49.6 50.4

2.4 Έλεγχος Ελλείψεων

cat("Ελλείψεις ανά στήλη (μετά drop_na):\n")
## Ελλείψεις ανά στήλη (μετά drop_na):
colSums(is.na(MSFT_feat))
##           Date           Open           High            Low          Close 
##              0              0              0              0              0 
##      Adj.Close         Volume   Daily_Return  Intraday_Move    Daily_Range 
##              0              0              0              0              0 
##            MA5           MA20           MA50   Price_vs_MA5  Price_vs_MA20 
##              0              0              0              0              0 
##  Price_vs_MA50    Momentum_5d Volatility_20d     Vol_Change         Target 
##              0              0              0              0              0

3 Βήμα 2: Επιλογή Αλγορίθμου

Ο CART ακολουθεί άπληστη (greedy) στρατηγική: σε κάθε βήμα επιλέγει τον διαχωρισμό που ελαχιστοποιεί την Gini Impurity.

3.1 Επιλογή Features

feat_cols <- c("Daily_Return", "Intraday_Move", "Daily_Range",
               "Price_vs_MA5", "Price_vs_MA20", "Price_vs_MA50",
               "Momentum_5d", "Volatility_20d", "Vol_Change")

model_df <- MSFT_feat %>% select(all_of(feat_cols), Target)

cat("Features που χρησιμοποιούνται:\n")
## Features που χρησιμοποιούνται:
cat(paste0("  • ", feat_cols, collapse = "\n"), "\n")
##   • Daily_Return
##   • Intraday_Move
##   • Daily_Range
##   • Price_vs_MA5
##   • Price_vs_MA20
##   • Price_vs_MA50
##   • Momentum_5d
##   • Volatility_20d
##   • Vol_Change

4 Βήμα 3: Εκπαίδευση Μοντέλου

4.1 Διαχωρισμός Train / Test

set.seed(42)

# sample.split: TRUE → train, FALSE → test (80/20)
split_idx <- sample.split(model_df$Target, SplitRatio = 0.80)

train_df <- subset(model_df,  split_idx)
test_df  <- subset(model_df, !split_idx)

cat("Training set :", nrow(train_df), "παρατηρήσεις\n")
## Training set : 7227 παρατηρήσεις
cat("Test set     :", nrow(test_df),  "παρατηρήσεις\n\n")
## Test set     : 1806 παρατηρήσεις
cat("Κατανομή κλάσεων — Train:\n")
## Κατανομή κλάσεων — Train:
print(round(prop.table(table(train_df$Target)) * 100, 1))
## 
## Down   Up 
## 49.6 50.4
cat("\nΚατανομή κλάσεων — Test:\n")
## 
## Κατανομή κλάσεων — Test:
print(round(prop.table(table(test_df$Target)) * 100, 1))
## 
## Down   Up 
## 49.6 50.4

4.2 Εκπαίδευση CART

cart_model <- rpart(
  Target ~ .,
  data   = train_df,
  method = "class",
  parms  = list(split = "gini"),          # Κριτήριο Gini Impurity
  control = rpart.control(
    minsplit  = 30,   # Ελάχ. παρατηρήσεις για να διαχωριστεί κόμβος
    minbucket = 15,   # Ελάχ. παρατηρήσεις σε φύλλο
    cp        = 0.001,# Complexity parameter (πρίν κλάδεμα)
    maxdepth  = 8     # Μέγιστο βάθος
  )
)

# Πίνακας cross-validation
printcp(cart_model)
## 
## Classification tree:
## rpart(formula = Target ~ ., data = train_df, method = "class", 
##     parms = list(split = "gini"), control = rpart.control(minsplit = 30, 
##         minbucket = 15, cp = 0.001, maxdepth = 8))
## 
## Variables actually used in tree construction:
## [1] Daily_Range    Daily_Return   Intraday_Move  Momentum_5d    Price_vs_MA20 
## [6] Price_vs_MA5   Price_vs_MA50  Vol_Change     Volatility_20d
## 
## Root node error: 3581/7227 = 0.4955
## 
## n= 7227 
## 
##           CP nsplit rel error  xerror     xstd
## 1  0.0268082      0   1.00000 1.00000 0.011869
## 2  0.0240156      1   0.97319 1.01480 0.011870
## 3  0.0069813      2   0.94918 0.99302 0.011868
## 4  0.0041888      3   0.94219 0.99469 0.011869
## 5  0.0035744      4   0.93801 0.99581 0.011869
## 6  0.0034906      9   0.92013 0.99693 0.011869
## 7  0.0030718     11   0.91315 0.99693 0.011869
## 8  0.0026994     13   0.90701 0.99916 0.011869
## 9  0.0026529     17   0.89612 0.99832 0.011869
## 10 0.0025133     19   0.89081 0.99888 0.011869
## 11 0.0022340     22   0.88271 1.00195 0.011870
## 12 0.0015359     25   0.87573 0.99553 0.011869
## 13 0.0013963     35   0.85395 0.99497 0.011869
## 14 0.0012566     38   0.84976 0.99721 0.011869
## 15 0.0011170     42   0.84474 1.00000 0.011869
## 16 0.0010000     48   0.83803 1.00140 0.011869

4.3 Κλάδεμα (Pruning)

Επιλέγουμε το cp που ελαχιστοποιεί το cross-validation error (xerror) για να αποφύγουμε το overfitting.

best_cp    <- cart_model$cptable[which.min(cart_model$cptable[,"xerror"]), "CP"]
cat("Βέλτιστο cp:", round(best_cp, 5), "\n")
## Βέλτιστο cp: 0.00698
pruned_cart <- prune(cart_model, cp = best_cp)
plotcp(cart_model)
title("Cross-Validation Error ανά cp — CART", line = 2.5)

4.4 Οπτικοποίηση Κλαδεμένου Δέντρου

rpart.plot(
  pruned_cart,
  type          = 4,
  extra         = 104,   # % ανά κλάση + αριθμός παρατηρήσεων
  under         = TRUE,
  fallen.leaves = TRUE,
  main          = "CART Decision Tree — MSFT Up vs Down",
  cex           = 0.65,
  box.palette   = list("blue", "purple")
)