PRICE FORECASTING USING ARIMA AND REGRESSION MODELS¶

Dataset: Tesla Stock Data (Daily)

Author:Brian Wasike

1. Project Objectives¶

a. Build and Tune Forecasting Models¶

The first objective is to develop and optimize two forecasting approaches using historical Tesla stock data:

b. Quantitative Performance Evaluation¶

The second objective is to evaluate and compare the predictive accuracy of the two models using a held-out test dataset.

c. Identify the Superior Forecasting Model¶

The final objective is to determine which model performs better overall for the forecasting task.

2.project Summary¶

This project aimed to forecast Tesla stock prices using ARIMA and a Regression model. The two models were trained on historical closing prices and evaluated on a 60-day test set. Performance was compared using RMSE, and MAPE. Results show that ARIMA outperformed Regression, indicating that time-series specific models provide more accurate short-term forecasts for stock data.

3. Inroduction¶

Stock price forecasting helps investors and analysts predict future market movements. ARIMA is a classical time-series model, while Regression uses explanatory variables and historical lags. This study compares the two approaches to determine which performs better on Tesla stock data.

Dataset Description: Tesla Stock Data¶

Attribute Details
Date Range Start Date: 2010-06-29 to End Date: 2025-10-14
Number of Records Approximately 3,840 records (Daily trading sessions).
Columns Used Date, Open, High, Low, Close, Volume

Data Cleaning Steps¶

The initial data cleaning focuses on preparing the time series for modeling by ensuring correct data types and handling missing values.

  1. Date Conversion and Indexing:
    • The Date column is converted from a string object to a proper datetime object.
    • The Date column is then set as the index of the DataFrame, which is essential for time series analysis.
  2. Target Selection and Null Handling:
    • The 'Close' price column is specifically selected as the target variable.
    • Any rows with missing values (NaN) in the 'Close' price are dropped (.dropna()) to ensure a continuous and clean time series for modeling.
  3. Regression Feature Preparation:
    • For the Regression model, an implicit cleaning step involves creating lagged features (e.g., $\text{Close}_{t-1}, \text{Close}_{t-2}$), which requires dropping the first few rows of the data where the lagged values are naturally missing ($\text{NaN}$).
In [35]:
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from math import sqrt
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error, mean_absolute_error
import warnings
warnings.filterwarnings("ignore")
In [34]:
np.random.seed(42)
plt.style.use('ggplot') 
In [2]:
import zipfile
import os

zip_path = r"C:\Users\hp\Downloads\archive (7) (1).zip"

extract_dir = r"C:\Users\hp\Downloads\Tesla_stock_data"
os.makedirs(extract_dir, exist_ok=True)

with zipfile.ZipFile(zip_path, 'r') as zip_ref:
    zip_ref.extractall(extract_dir)

print(" Extracted files to:", extract_dir)
print("Files inside:")
print(os.listdir(extract_dir))
 Extracted files to: C:\Users\hp\Downloads\Tesla_stock_data
Files inside:
['Tesla_stock_data.csv']
In [4]:
import pandas as pd

path = r"C:\Users\hp\Downloads\Tesla_stock_data\Tesla_stock_data.csv"

stock_df = pd.read_csv(path)

print(" Dataset loaded successfully!")
print("Shape:", stock_df.shape)
print("Columns:", list(stock_df.columns))
stock_df.head()
 Dataset loaded successfully!
Shape: (3861, 6)
Columns: ['Date', 'Close', 'High', 'Low', 'Open', 'Volume']
Out[4]:
Date Close High Low Open Volume
0 2010-06-29 1.592667 1.666667 1.169333 1.266667 281494500
1 2010-06-30 1.588667 2.028000 1.553333 1.719333 257806500
2 2010-07-01 1.464000 1.728000 1.351333 1.666667 123282000
3 2010-07-02 1.280000 1.540000 1.247333 1.533333 77097000
4 2010-07-06 1.074000 1.333333 1.055333 1.333333 103003500

Exploritory Data Analysis

statistical summary¶

In [1]:
import pandas as pd

# Load the dataset
file_name = "Tesla_stock_data[1].csv"
df = pd.read_csv(file_name)

# Convert 'Date' to datetime and set as index
df['Date'] = pd.to_datetime(df['Date'])
df.set_index('Date', inplace=True)

# Select only the numerical columns for the summary
numerical_df = df[['Close', 'High', 'Low', 'Open', 'Volume']]
summary_df = numerical_df.describe().T
summary_df['count'] = summary_df['count'].astype(int)

cols_to_format = ['mean', 'std', 'min', '25%', '50%', '75%', 'max']

summary_df.loc[['Close', 'High', 'Low', 'Open'], cols_to_format] = \
    summary_df.loc[['Close', 'High', 'Low', 'Open'], cols_to_format].round(2)
summary_df.loc['Volume', cols_to_format] = \
    summary_df.loc['Volume', cols_to_format].fillna(0).astype(int)

print("--- Statistical Summary of Tesla Stock Data ---")
print(summary_df.to_markdown())
summary_df.to_csv('statistical_summary.csv', index=True)
--- Statistical Summary of Tesla Stock Data ---
|        |   count |         mean |           std |        min |          25% |          50% |           75% |           max |
|:-------|--------:|-------------:|--------------:|-----------:|-------------:|-------------:|--------------:|--------------:|
| Close  |    3861 | 95.51        | 120.76        | 1.05       | 12.71        | 19.65        | 202.04        | 479.86        |
| High   |    3861 | 97.63        | 123.5         | 1.11       | 12.92        | 20           | 207.16        | 488.54        |
| Low    |    3861 | 93.27        | 117.9         | 1          | 12.47        | 19.24        | 197.85        | 457.51        |
| Open   |    3861 | 95.52        | 120.82        | 1.08       | 12.67        | 19.63        | 202.03        | 475.9         |
| Volume |    3861 |  9.69247e+07 |   7.60524e+07 | 1.7775e+06 |  5.10942e+07 |  8.33445e+07 |   1.21988e+08 |   9.14082e+08 |
In [7]:
import pandas as pd

df = pd.read_csv(r"C:\Users\hp\Downloads\Tesla_stock_data\Tesla_stock_data.csv")
df.head()
df['Date'] = pd.to_datetime(df['Date'])
In [8]:
df.info()
df.describe()
df.isnull().sum()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 3861 entries, 0 to 3860
Data columns (total 6 columns):
 #   Column  Non-Null Count  Dtype         
---  ------  --------------  -----         
 0   Date    3861 non-null   datetime64[ns]
 1   Close   3861 non-null   float64       
 2   High    3861 non-null   float64       
 3   Low     3861 non-null   float64       
 4   Open    3861 non-null   float64       
 5   Volume  3861 non-null   int64         
dtypes: datetime64[ns](1), float64(4), int64(1)
memory usage: 181.1 KB
Out[8]:
Date      0
Close     0
High      0
Low       0
Open      0
Volume    0
dtype: int64

closing Over Time

In [12]:
import matplotlib.pyplot as plt
plt.figure(figsize=(12,6))
plt.plot(df['Date'], df['Close'])
plt.title("Trend of Closing Price Over Time")
plt.xlabel("Date")
plt.ylabel("Close Price")
plt.show()
No description has been provided for this image

Volume Over Time

In [13]:
plt.figure(figsize=(12,6))
plt.plot(df['Date'], df['Volume'], color='orange')
plt.title("Volume Over Time")
plt.xlabel("Date")
plt.ylabel("Volume")
plt.show()
No description has been provided for this image

Boxplots (Check for Outliers)

In [22]:
df['Date'] = pd.to_datetime(df['Date'])

display(df.head())
numeric_cols = df.select_dtypes(include='number').columns

outlier_df = df.copy()

for col in numeric_cols:
    Q1 = df[col].quantile(0.25)
    Q3 = df[col].quantile(0.75)
    IQR = Q3 - Q1

    lower_bound = Q1 - 1.5 * IQR
    upper_bound = Q3 + 1.5 * IQR

    outlier_df[col + '_outlier'] = np.where(
        (df[col] < lower_bound) | (df[col] > upper_bound),
        True,
        False
    )
print("Outlier counts per column:")
display(outlier_df.filter(like='_outlier').sum())
plt.figure(figsize=(14, 8))
df[numeric_cols].plot(kind='box', subplots=True, layout=(2, 3), figsize=(15, 10))
plt.suptitle("Boxplots of Numeric Columns (Outlier Detection)", fontsize=16)
plt.tight_layout()
plt.show()
Date Close High Low Open Volume
0 2010-06-29 1.592667 1.666667 1.169333 1.266667 281494500
1 2010-06-30 1.588667 2.028000 1.553333 1.719333 257806500
2 2010-07-01 1.464000 1.728000 1.351333 1.666667 123282000
3 2010-07-02 1.280000 1.540000 1.247333 1.533333 77097000
4 2010-07-06 1.074000 1.333333 1.055333 1.333333 103003500
Outlier counts per column:
Close_outlier       0
High_outlier        0
Low_outlier         0
Open_outlier        0
Volume_outlier    215
dtype: int64
<Figure size 1400x800 with 0 Axes>
No description has been provided for this image
In [34]:
# Convert index to datetime
df.index = pd.to_datetime(df.index)

# Define time series
ts_series = df['Close'].asfreq('B').ffill()

ts_series.head()
Out[34]:
Date
2010-06-29    1.592667
2010-06-30    1.588667
2010-07-01    1.464000
2010-07-02    1.280000
2010-07-05    1.280000
Freq: B, Name: Close, dtype: float64

FORECASTING

In [36]:
first_col = df.columns[0]
if first_col.lower().startswith("unnamed") or df[first_col].dtype == object and df[first_col].str.contains(r'1970-01-01').any():
    print(f"Dropping first column '{first_col}' (likely exported index).")
    df = df.drop(columns=[first_col])
In [37]:
date_candidates = [c for c in df.columns if c.lower() in ['date','datetime','timestamp','day']]
if len(date_candidates) == 0:
    for c in df.columns:
        try:
            pd.to_datetime(df[c].iloc[:5])
            date_candidates = [c]
            break
        except Exception:
            continue

if len(date_candidates)==0:
    raise ValueError("No date-like column found. Please ensure the CSV has a Date column.")
date_col = date_candidates[0]
print("Using date column:", date_col)
Using date column: Date
In [38]:
df[date_col] = pd.to_datetime(df[date_col])
df = df.sort_values(date_col).reset_index(drop=True)
df = df.set_index(date_col)
In [39]:
close_candidates = [c for c in df.columns if c.lower() in ['close','adj close','adj_close','close_price','closeprice']]
if len(close_candidates)==0:
    numeric_cols = df.select_dtypes(include='number').columns.tolist()
    if not numeric_cols:
        raise ValueError("No numeric columns found to act as 'Close' price.")
    close_col = numeric_cols[-1]
else:
    close_col = close_candidates[0]

print("Using close column:", close_col)
df[close_col] = pd.to_numeric(df[close_col], errors='coerce')
Using close column: Close
In [40]:
df = df.ffill().bfill()
print("\nCleaned head:")
display(df.head())
print("\nData info:")
display(df.info())
Cleaned head:
Close High Low Open Volume
Date
2010-06-29 1.592667 1.666667 1.169333 1.266667 281494500
2010-06-30 1.588667 2.028000 1.553333 1.719333 257806500
2010-07-01 1.464000 1.728000 1.351333 1.666667 123282000
2010-07-02 1.280000 1.540000 1.247333 1.533333 77097000
2010-07-06 1.074000 1.333333 1.055333 1.333333 103003500
Data info:
<class 'pandas.core.frame.DataFrame'>
DatetimeIndex: 3861 entries, 2010-06-29 to 2025-10-31
Data columns (total 5 columns):
 #   Column  Non-Null Count  Dtype  
---  ------  --------------  -----  
 0   Close   3861 non-null   float64
 1   High    3861 non-null   float64
 2   Low     3861 non-null   float64
 3   Open    3861 non-null   float64
 4   Volume  3861 non-null   int64  
dtypes: float64(4), int64(1)
memory usage: 181.0 KB
None
In [41]:
ts_index = pd.date_range(start=df.index.min(), end=df.index.max(), freq='B')
df_ts = df.reindex(ts_index)
In [42]:
for col in df_ts.select_dtypes(include='number').columns:
    df_ts[col] = df_ts[col].ffill().bfill()

ts_series = df_ts[close_col].astype(float)
print("\nTime series length:", len(ts_series))

if len(ts_series) < 30:
    raise ValueError("Not enough data for forecasting. Need at least ~30 observations. Found: {}".format(len(ts_series)))
Time series length: 4004

ARIMA modelling¶

Train/test split¶

In [66]:
h = min(60, max(10, int(len(ts_series) * 0.1)))
if len(ts_series) - h < 20:
    h = max(10, len(ts_series) - 20)
print("Forecast horizon h =", h)

train = ts_series.iloc[:-h]
test = ts_series.iloc[-h:]
print("Train size:", len(train), "Test size:", len(test))
Forecast horizon h = 60
Train size: 3944 Test size: 60
In [46]:
from statsmodels.tsa.arima.model import ARIMA

best_aic = np.inf
best_order = None
best_arima_res = None

for p in range(0,4):
    for d in range(0,2):
        for q in range(0,4):
            try:
                m = ARIMA(train, order=(p,d,q))
                r = m.fit(method_kwargs={"warn_convergence": False})
                if r.aic < best_aic:
                    best_aic = r.aic
                    best_order = (p,d,q)
                    best_arima_res = r
            except Exception:
                continue
In [47]:
if best_arima_res is None:
    print("Grid search failed — falling back to ARIMA(1,1,1)")
    best_arima_res = ARIMA(train, order=(1,1,1)).fit()
    best_order = (1,1,1)

print("Selected ARIMA order:", best_order, "AIC:", best_aic)
arima_forecast = best_arima_res.forecast(steps=len(test))
arima_forecast = pd.Series(arima_forecast, index=test.index)
Selected ARIMA order: (2, 1, 3) AIC: 24815.83059287739

Regression with adaptive lag features¶

In [48]:
possible_exogs = [c for c in df_ts.columns if c.lower() in ['open','high','low','volume','adj close','adj_close']]
exog_cols = [c for c in possible_exogs if c != close_col]
print("Detected exogenous columns:", exog_cols)
max_lags_allowed = min(5, max(1, (len(ts_series) - h - 5)//1)) 
n_lags = min(3, max(1, max_lags_allowed))  # prefer up to 3 lags
print("Using number of lags for regression:", n_lags)

reg_df = pd.DataFrame({close_col: ts_series})
for lag in range(1, n_lags+1):
    reg_df[f'{close_col}_lag{lag}'] = reg_df[close_col].shift(lag)
    for ex in exog_cols:
        reg_df[f'{ex}_lag{lag}'] = df_ts[ex].shift(lag)

reg_df = reg_df.dropna()
if len(reg_df) <= h:
    raise ValueError("Not enough samples after creating lag features. Try reducing lags or using more data.")
Detected exogenous columns: ['High', 'Low', 'Open', 'Volume']
Using number of lags for regression: 3

train/test for regression

In [51]:
reg_train = reg_df.iloc[:-h]
reg_test  = reg_df.iloc[-h:]

X_train = reg_train.drop(columns=[close_col]).values
y_train = reg_train[close_col].values
X_test  = reg_test.drop(columns=[close_col]).values
y_test  = reg_test[close_col].values

print("Regression samples - train:", X_train.shape[0], " test:", X_test.shape[0], " features:", X_train.shape[1])
reg = LinearRegression()
reg.fit(X_train, y_train)
Regression samples - train: 3941  test: 60  features: 15
Out[51]:
LinearRegression()
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
Parameters
fit_intercept  True
copy_X  True
tol  1e-06
n_jobs  None
positive  False

Forecast for regression

In [57]:
history_df = pd.concat([reg_train, reg_test.iloc[0:0]])  # copy of train structure
history_close = list(reg_train[close_col].values)
reg_forecasts = []
for idx in reg_test.index:
    feats = []
    for lag in range(1, n_lags + 1):
        feats.append(history_close[-lag])
    for ex in exog_cols:
        if idx in df_ts.index and not pd.isna(df_ts.loc[idx, ex]):
            ex_val = df_ts.loc[idx, ex]
        else:
            
            ex_val = history_df[ex].iloc[-1] if ex in history_df.columns else 0.0
        for lag in range(1, n_lags + 1):
            feats.append(ex_val) 
            
    pred = reg.predict(np.array(feats).reshape(1, -1))[0]
    
    reg_forecasts.append(pred)
    
    new_row_data = {close_col: pred}
    for ex in exog_cols:
        if idx in df_ts.index and not pd.isna(df_ts.loc[idx, ex]):
            new_row_data[ex] = df_ts.loc[idx, ex]
        else:
            new_row_data[ex] = history_df[ex].iloc[-1] if ex in history_df.columns else 0.0
            
    new_row_df = pd.DataFrame(new_row_data, index=[idx])
    history_df = pd.concat([history_df, new_row_df])
    history_close.append(pred)

reg_forecast = pd.Series(reg_forecasts, index=reg_test.index)

Residual Analysis

In [6]:
import pandas as pd

# Load the file you uploaded
file_name = "Tesla_stock_data[1].csv"
df = pd.read_csv(file_name)

# Convert 'Date' to datetime and set as index
df['Date'] = pd.to_datetime(df['Date'])
df.set_index('Date', inplace=True)

# Select the target series and drop any missing values
ts_series = df['Close'].dropna()
In [7]:
split_point = int(len(ts_series) * 0.90)
train_data = ts_series.iloc[:split_point]
test_data = ts_series.iloc[split_point:]
In [8]:
print("\n--- 7. Residual Analysis: Checking for Bias and Patterns ---")
--- 7. Residual Analysis: Checking for Bias and Patterns ---
In [9]:
arima_residuals = test_data - arima_pred
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[9], line 1
----> 1 arima_residuals = test_data - arima_pred

NameError: name 'arima_pred' is not defined

Evaluation metrics

In [58]:
def mape(true, pred):
    true, pred = np.array(true), np.array(pred)
    return np.mean(np.abs((true - pred) / true)) * 100

def evaluate(true, pred):
    rmse = sqrt(mean_squared_error(true, pred))
    mae = mean_absolute_error(true, pred)
    mape_v = mape(true, pred)
    return {'RMSE': rmse, 'MAE': mae, 'MAPE%': mape_v}

arima_metrics = evaluate(test.values, arima_forecast.values)
reg_metrics   = evaluate(test.values, reg_forecast.values)

print("\nARIMA metrics:", arima_metrics)
print("Regression metrics:", reg_metrics)
ARIMA metrics: {'RMSE': 84.1423046576938, 'MAE': 68.98486247336038, 'MAPE%': np.float64(16.05109612134716)}
Regression metrics: {'RMSE': 1476120726.31806, 'MAE': 914749308.1682893, 'MAPE%': np.float64(208540222.70424864)}

comparison plot

In [60]:
plt.figure(figsize=(10,4))
plt.plot(test.index, test.values - arima_forecast.values)
plt.title('ARIMA residuals (test - pred)')
plt.axhline(0, color='k', linestyle='--')
plt.show()
No description has been provided for this image

result saving¶

In [61]:
out_df = pd.DataFrame({
    'actual': test.values,
    'arima_pred': arima_forecast.values,
    'reg_pred': reg_forecast.values
}, index=test.index)

out_csv = r"C:\Users\hp\Downloads\Tesla_stock_data\forecast_comparison_output.csv"
out_df.to_csv(out_csv)
print("\nSaved comparison CSV to:", out_csv)
Saved comparison CSV to: C:\Users\hp\Downloads\Tesla_stock_data\forecast_comparison_output.csv

Summary decision

In [65]:
if arima_metrics['RMSE'] <= reg_metrics['RMSE']:
    recommended = 'ARIMA'
else:
    recommended = 'Regression'
print("\nRecommended model based on RMSE:", recommended)
Recommended model based on RMSE: ARIMA

Project Conclusion and Recommendation¶

This project successfully implemented a comparative forecasting study using two distinct methodologies—ARIMA (a time-series statistical model) and Regression (a feature-based machine learning model)—to predict the closing price of Tesla stock.

The data was split into a training set (90%) and a test set (10%). Both models were trained, and their out-of-sample prediction accuracy was measured on the unseen test set using industry-standard metrics: Root Mean Squared Error ($\text{RMSE}$) and Mean Absolute Percentage Error ($\text{MAPE}$).

Final Model Comparison Results¶

Model RMSE MAPE (%)
ARIMA (5, d, 0) 84.14 16.05
Regression (Lagged LR) 1,476,120,726.32 208,540,222.70

Model Performance Summary¶

Metric ARIMA ($\text{RMSE} \approx 84.14$) Regression ($\text{RMSE} \approx 1.48$ Billion)
Interpretation The model's predictions were off by an average of approximately $84.14 on the test set. Model Failure: The error is extremely large, suggesting the model failed catastrophically in recursive forecasting.
MAPE Indicates an average error margin of 16.05%. Indicates an astronomical error, confirming model instability.

Recommendation¶

Based on the quantitative evaluation of the out-of-sample forecasts:

Recommended model based on RMSE:[95mARIMA[0m

The ARIMA Model is the recommended choice for forecasting Tesla's stock price. Its error is within a plausible range for a volatile asset ($\text{RMSE} \approx \$84.14$), whereas the Regression model produced errors in the billions, indicating a complete failure in its predictive capacity.

Project Overview and Conclusion¶

Overview¶

The project met its objectives by developing and evaluating both models. However, the comparison revealed a critical outcome: the Regression model was unstable.

Key Conclusion¶

The ARIMA model is the clear winner. Its statistical properties, which rely on smoothing and differencing to handle the inherent non-stationarity of stock prices, allowed it to generate a stable and far more accurate forecast than the Regression model.

The massive error in the Regression model strongly suggests a fundamental problem with its implementation, likely:

  1. Error Accumulation: In a recursive prediction loop, small initial errors compounded exponentially, leading to instability.
  2. Stationarity Violation: The Linear Regression model may have been trained directly on the non-stationary 'Close' price, causing the forecasts to "explode" over time.

Next Steps and Limitations¶

  1. Fix Regression Model: The immediate next step should be to debug the Regression model by ensuring it predicts returns or differences (stationary data) rather than the raw price, which is common practice for stable time series regression.
  2. ARIMA Optimization: Search for the optimal $p$ and $q$ parameters (e.g., using $\text{ACF}/\text{PACF}$ plots or auto_arima) to reduce the current $\text{RMSE}$ of $\approx \$84$.