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.
- Date Conversion and Indexing:
- The
Datecolumn is converted from a string object to a proper datetime object. - The
Datecolumn is then set as the index of the DataFrame, which is essential for time series analysis.
- The
- 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.
- 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}$).
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")
np.random.seed(42)
plt.style.use('ggplot')
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']
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']
| 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¶
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 |
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'])
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
Date 0 Close 0 High 0 Low 0 Open 0 Volume 0 dtype: int64
closing Over Time
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()
Volume Over Time
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()
Boxplots (Check for Outliers)
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>
# Convert index to datetime
df.index = pd.to_datetime(df.index)
# Define time series
ts_series = df['Close'].asfreq('B').ffill()
ts_series.head()
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
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])
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
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)
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
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
ts_index = pd.date_range(start=df.index.min(), end=df.index.max(), freq='B')
df_ts = df.reindex(ts_index)
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¶
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
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
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¶
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
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
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
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
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()
split_point = int(len(ts_series) * 0.90)
train_data = ts_series.iloc[:split_point]
test_data = ts_series.iloc[split_point:]
print("\n--- 7. Residual Analysis: Checking for Bias and Patterns ---")
--- 7. Residual Analysis: Checking for Bias and Patterns ---
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
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
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()
result saving¶
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
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:
- Error Accumulation: In a recursive prediction loop, small initial errors compounded exponentially, leading to instability.
- 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¶
- 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.
- 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$.