NVIDIAの株価を線形回帰で予測



# 1.本投稿の目的
**このブログはAidemy Premiumのカリキュラムの一環で、受講修了条件を満たすために公開しています。**

今年2024年4月より、Pythonの文法のイロハから機械学習のモデル・ディープラーニングの手法など、データサイエンスの基礎を学習しております。駆け足で学習してきましたが、ひとまず形にするところまで来ています。

今回はモデルの精度はさておき、一番簡単な線形回帰を使って簡単な機械学習に挑戦します！

# 2.自己紹介
簡単に自己紹介させていただきますと、都内で金融機関に勤める30代の普通のサラリーマンです。

私自身は高校3年までは数学ⅢCまで学び、一応”理系”だったわけですが、その後の大学ではいわゆる文転し、文系の大学を卒業しました。（理系の大学も受かっていたのになぜそっちに行かなかったのか、、、汗）

よって、数学は全然得意じゃないし、プログラミングも業務で簡単なエクセルVBAをちょこっと書く程度。
統計も概念だけを”なんとなく”理解している、そんなレベルです。

そんなことを言い訳にしてプログラミングから逃げ続けてきたのですが、やはり自分の中でイノベーションを起こしたくPythonに挑戦してみることにしました！


# 3.今回の分析テーマ
私自身金融機関に勤めているということもあり、株価予測に挑戦したいと思います。
とはいえ株価予測の変数は膨大すぎますし、私自身機械学習モデルの勉強を進めれば進めるほど、その難しさを痛感しています。

よって今回は機械学習モデルの基本的なものを使って「こんなことができるんだ～」というレベルにとどめます。

そして、Aidemyの講座ではアプリ開発も学ぶことができるので、私としては「効率よく株価データ等を収集し、現状のデータを素早くきれいに可視化する」を最終目標にしたいと思います。

その可視化されたデータを眺めて、それを基に自分なりに考えたほうが、今のところは株価を正しく予測できると思っています。（いつ追い越されることやら）

では、なぜそれを今回のテーマにしないのか？といいますと、私自身が特に深いことを考えずにデータ分析コースを選択したため、一応は機械学習モデルの成果物を作らなければいけないという事情があります（笑）

従いまして、本分析は株価予測の精緻さを示すものではないことを予めお伝えしておきます。

# 4.今回の分析の流れ
今回はNVIDIA（以下NVDA）の翌日の株価予測に挑戦します。

NVIDIAだけのデータだけでは面白みがないため、SOX（フィラデルフィア半導体）指数の株価データ、TSMC（台湾積体電路製造股份有限公司、NVIDIAが設計した半導体を実際に工場で作っている会社です）の株価データも追加して検証してみます。

最終的にNVIDIAだけのデータを使って株価を予測するのが良いのか、SOXやTSMCのデータも加えたほうが精度が改善するのか比べてみます。

機械学習の手法は、線形回帰（重回帰分析）、ラッソ回帰、リッジ回帰、ElasticNet回帰を取り扱います。


【参考】SOX指数　（ソックスしすう）
SOX指数は米国のフィラデルフィア証券取引所が算出、公表している株価指数で、別名「フィラデルフィア半導体指数」とも呼ばれています。半導体の製造や流通、販売等を手がける30銘柄で構成されています。半導体企業の株価が上昇するとSOX指数も高くなり、一般的に世の中の景気が良いと判断されます。
https://www.smbcnikko.co.jp/terms/eng/s/E0113.html

【参考】TSMC（Wikipedia）
https://ja.wikipedia.org/wiki/%E5%8F%B0%E6%B9%BE%E7%A9%8D%E4%BD%93%E9%9B%BB%E8%B7%AF%E8%A3%BD%E9%80%A0

### 今回の開発環境です。
（OS、エディター）
Windows11
Google Colaboratory

（使用言語）
python

（モデル）
線形回帰
ラッソ回帰
リッジ回帰
ElasticNet回帰

# 5.具体的なコード
早速ですが今回作ったモデルのご紹介に入ります。

まず最初にライブラリのインポートと必要なデータを取得します。

NVIDIAの株価のティッカーシンボルがNVDAのため、以下NVDAと表示します。
同様にSOX指数をSOX、TSMCをTSMと表示します。


```ruby:ライブラリのインポート.py
# ライブラリのインポート
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import plotly.express as px
import plotly.graph_objs as go
from plotly.subplots import make_subplots
```

データはYahoo! finance USからcsvファイルにて取得しています。（スクレイピングなどができるとカッコイイですが、今回は愚直にダウンロードし所定のフォルダに保存します。Pandasのdatareaderを使うこともできそうですね）
https://finance.yahoo.com/

```ruby:NVDAの株価データをPandasのデータフレームに格納.py
# GoogleドライブのMydrive内に保存したcsvファイルをPandasのデータフレームに格納
df_nvda = pd.read_csv("/content/drive/MyDrive/NVDA.csv")
# ’Date’カラムを編集しやすくするためにデータタイム型に変換
df_nvda['Date'] = pd.to_datetime(df_nvda['Date'])
```

```ruby:同様にSOX指数、TSMCの株価データをPandasのデータフレームに格納.py
# 上記委のNVDAと全く同様の処理
df_sox = pd.read_csv("/content/drive/MyDrive/SOX.csv")
df_sox['Date'] = pd.to_datetime(df_sox['Date'])

df_tsm = pd.read_csv("/content/drive/MyDrive/TSM.csv")
df_tsm['Date'] = pd.to_datetime(df_tsm['Date'])

```

NVDAとTSMCのデータフレームを結合する作業を行います。


```ruby:NVDAのデータフレームにNKYのデータフレームを外部結合.py
#外部結合したデータフレームをmerged_dfに格納
#カラムデータの名前に同一のものがあるため、_nvda、_tsmとつけて区別できるようにする
merged_df = pd.merge(df_nvda, df_tsm, on ='Date', how='outer', suffixes=['_nvda', '_tsm'])

#merged_dfにさらにSOX指数のデータも外部結合させます
merged_df_v2 = pd.merge(merged_df, df_sox, on = 'Date', how = 'outer', suffixes=['','_sox'])

#またdfに戻します（merged_dfと入力するのが面倒なため）
df = merged_df_v2.sort_values('Date')

```
カラム名を確認します。
↓一回目の結合までは引数suffixesがワークしてnvda、tsmとついたカラム名に変更ができるのですが、二回目の結合時にどうしてもうまくカラム名変更ができなかったため、力業でカラム名を変更します。
```ruby:カラム名の確認.py
df.columns

Index(['Date', 'Open_nvda', 'High_nvda', 'Low_nvda', 'Close_nvda',
       'Adj Close_nvda', 'Volume_nvda', 'Open_tsm', 'High_tsm', 'Low_tsm',
       'Close_tsm', 'Adj Close_tsm', 'Volume_tsm', 'Open', 'High', 'Low',
       'Close', 'Adj Close', 'Volume'],
      dtype='object')
      
```



```ruby:結合時に名前が変更できなかったカラム名を変更.py
df = df.rename(columns={'Open':'Open_sox',
                        'High':'High_sox',
                        'Low':'Low_sox',
                        'Close':'Close_sox',
                        'Adj Close':'Adj Close_sox',
                        'Volume':'Volume_sox'})
```

続いて、結合したので欠損値があるかの確認を行います。

```ruby:欠損値の確認.py
merged_df.isnull().sum()
```

```ruby:欠損値の結果.py
Date                 0
Open_nvda         1194
High_nvda         1194
Low_nvda          1194
Close_nvda        1194
Adj Close_nvda    1194
Volume_nvda       1194
Open_tsm           869
High_tsm           869
Low_tsm            869
Close_tsm          869
Adj Close_tsm      869
Volume_tsm         869
Open                 0
High                 0
Low                  0
Close                0
Adj Close            0
Volume               0
dtype: int64
```

十分にデータがあるため、欠損値のある行はすべてdropna()で削除します

```ruby:欠損値の削除.py

df = df.dropna()
df.isnull().sum()

Date              0
Open_nvda         0
High_nvda         0
Low_nvda          0
Close_nvda        0
Adj Close_nvda    0
Volume_nvda       0
Open_tsm          0
High_tsm          0
Low_tsm           0
Close_tsm         0
Adj Close_tsm     0
Volume_tsm        0
Open              0
High              0
Low               0
Close             0
Adj Close         0
Volume            0
dtype: int64
```
上記の通り欠損値が削除されました。
データの中身をチェックします。特に問題なさそうです。

```ruby:欠損値処理後のデータ確認.py
merged_df.info()

<class 'pandas.core.frame.DataFrame'>
Index: 7708 entries, 6839 to 6838
Data columns (total 19 columns):
 #   Column          Non-Null Count  Dtype         
---  ------          --------------  -----         
 0   Date            7708 non-null   datetime64[ns]
 1   Open_nvda       6514 non-null   float64       
 2   High_nvda       6514 non-null   float64       
 3   Low_nvda        6514 non-null   float64       
 4   Close_nvda      6514 non-null   float64       
 5   Adj Close_nvda  6514 non-null   float64       
 6   Volume_nvda     6514 non-null   float64       
 7   Open_tsm        6839 non-null   float64       
 8   High_tsm        6839 non-null   float64       
 9   Low_tsm         6839 non-null   float64       
 10  Close_tsm       6839 non-null   float64       
 11  Adj Close_tsm   6839 non-null   float64       
 12  Volume_tsm      6839 non-null   float64       
 13  Open            7708 non-null   float64       
 14  High            7708 non-null   float64       
 15  Low             7708 non-null   float64       
 16  Close           7708 non-null   float64       
 17  Adj Close       7708 non-null   float64       
 18  Volume          7708 non-null   int64         
dtypes: datetime64[ns](1), float64(17), int64(1)
memory usage: 1.2 MB
```

ここから格納したdfの可視化作業を行います。
ひとまずNVDA、SOX、TSMCのデータが揃っている期間について株価の推移を見てみることにします。（SOX、TSMCも同様の手順のためコードは割愛します）

```ruby:全てのデータがそろっている期間の株価データを格納.py

# 2000/1/1 - 2024/6/7 の期間を指定
start_date = '2000-01-01'
end_date = '2024-06-07'

mask = (df['Date'] >= start_date) & (df['Date'] <= end_date)
df = df.loc[mask]
```

```ruby:Dateでソート.py
df = df.sort_values(by='Date')
```



```ruby:NVDAの株価推移を表示.py
# NVDA株価の時系列データ
fig = make_subplots(rows=1, cols=1)

# Add trace for Close Price
fig.add_trace(
    go.Scatter(x=df['Date'], y=df['Adj Close_nvda'], mode='lines', name='NVIDIA Close Price', line=dict(color='blue')),
)

# Update layout
fig.update_layout(
    title='NVDAの株価推移',
    xaxis_title='Date',
    yaxis_title='Close Price',
    showlegend=True,
)

# Show the plot
fig.show()
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3795997/1ab9220f-ded5-f558-465c-c8b579dac35d.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3795997/45dff321-94d5-b228-d442-c7c1ad3626e4.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3795997/398524c9-1db9-6388-ebd5-13cec79e1ece.png)


株価推移を見ると、NVDA、SOX、TSMの間にはコロナショック後に相関性が高まっているような感じに見えました。
そのため（コロナショック真っ只中の2020年はデータから除外し）2021年以降の関連性について調べてみることにします。

```ruby:.py
# 2000/1/1 - 2024/6/7 の期間を指定
new_start_date = '2021-01-01'
end_date = '2024-06-07'

mask = (df['Date'] >= new_start_date) & (df['Date'] <= end_date)
df_new = df.loc[mask]
df_new
```

ここで、説明変数を追加します
NVDAの移動平均株価を短期5日間、中期10日間、長期15日間の３種類を追加します。
移動平均にはPandasのRollingメソッドを使用しました。

```ruby:追加する説明変数の準備～その１～：移動平均線.py
#移動平均を追加
SMA1 = 5   #短期5日
SMA2 = 10  #中期10日
SMA3 = 25  #長期15日
df_new['SMA1'] = df_new['Adj Close_nvda'].rolling(SMA1).mean() #短期移動平均の算出
df_new['SMA2'] = df_new['Adj Close_nvda'].rolling(SMA2).mean() #中期移動平均の算出
df_new['SMA3'] = df_new['Adj Close_nvda'].rolling(SMA3).mean() #長期移動平均の算出
```
```ruby:追加する説明変数の可視化：移動平均線.py
# 特徴量を描画して確認
plt.figure(figsize=(15, 6))
plt.plot(df_new['Adj Close_nvda'], label='Close', color='orange')
plt.plot(df_new['SMA1'], label='SMA1', color='red')
plt.plot(df_new['SMA2'], label='SMA2', color='blue')
plt.plot(df_new['SMA3'], label='SMA3', color='green')
plt.xlabel('Date')
plt.ylabel('USD')
plt.legend()
plt.show()
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3795997/17f71dc9-b7ed-9b89-0042-41248006f90a.png)

もう少し説明変数を加えてみます。
①始値と終値の差分を計算。
②前日終値との差分'Close_diff'を追加。(diffメソッド)。
③最後に、目的変数の翌日終値'Close_next'を追加。shiftメソッドで1日前にずらし、翌日の終値を計算。

前日終値との差分'Close_diff'は初めのデータが、翌日の終値'Close_next'は最後のデータが欠損値となっているため、欠損値の処理を行います

```ruby:追加する説明変数と目的変数の準備：.py
# OpenとCloseの差分を実体Bodyとして計算
df_new['Body'] = df_new['Open_tsm'] - df_new['Adj Close_nvda']
# 前日終値との差分Close_diffを計算
df_new['Close_diff'] = df_new['Adj Close_nvda'].diff(1)
# 目的変数となる翌日の終値Close_nextの追加
df_new['Close_next'] = df_new['Adj Close_nvda'].shift(-1)
df_new
```
一部欠損値が出ています。（移動平均の期間に満たない部分（SMA1、SMA2、SMA3）、前日終値との差分の初日（Close_diff）、翌日の終値（Close_Next））
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3795997/e613b1f6-264c-5818-21d2-1fc2bf950827.png)

```ruby:欠損値を削除：.py
df_new = df_new.dropna()
df_new
```
欠損値が削除されたことが確認できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3795997/74d30196-cd92-fa38-fad6-3d4a797fd628.png)



データの準備ができ始めてきたので線形回帰の各モデルをインポートしていきます。

```ruby:線形回帰のモデルをインポート.py
from sklearn.linear_model import LinearRegression
from sklearn.linear_model import Lasso
from sklearn.linear_model import Ridge
from sklearn.linear_model import ElasticNet
from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score
```
```ruby:モデル名と学習器の準備.py
models = {
    'Linear Regression（線形回帰）': LinearRegression(),
    'Lasso（ラッソ回帰）': Lasso(),
    'Ridge（リッジ回帰）': Ridge(),
    'Elastic（ElasticNet回帰）': ElasticNet()
}
```

### それぞれの回帰の説明
線形回帰（重回帰分析）は予測したいデータ1つに対して複数のデータを用いて分析する手法です。

https://bellcurve.jp/statistics/course/9702.html

一方で、回帰分析は過去のデータから学習して未来の予測に役立てるわけですが、その弱点として過去のデータを適合しすぎて”過学習”を起こしてしまうという点が挙げられます。
それを和らげるアプローチを”汎化”と呼び、ラッソ回帰、リッジ回帰、ElasticNet回帰などがあるようですので、それらも同時に計算します。

続いて学習器に入力するデータを作成していきます。

```ruby:データフレームの中身を確認.py

df_new.columns
Index(['Open_nvda',
        'High_nvda',
        'Low_nvda',
        'Close_nvda',
        'Adj Close_nvda',
        'Volume_nvda',
        'Open_tsm',
        'High_tsm',
        'Low_tsm',
        'Close_tsm',
        'Adj Close_tsm',
        'Volume_tsm',
        'Open_sox',
        'High_sox',
        'Low_sox',
        'Close_sox',
        'Adj Close_sox',
        'Volume_sox',
        'SMA1',
        'SMA2',
        'SMA3',
        'Body',
        'Close_diff',
        'Close_next'],
      dtype='object')
```

学習器に入力するデータを編集していきます。

本ブログでは二つの分析を行っていきます。
①まず一つ目は、NVDAの株価データ、NVDAに関連するデータのみを使って、学習モデルに学習させて精度を確認します。
②二つ目は、これまで準備してきたNVDA、SOX、TSMのデータを全て使用して、同様に精度を確認します。

まずはこのデータフレームの中からNVDAに関する説明変数のみを取り出します。

```ruby:NVDAに関する説明変数を取り出すために、不要な説明変数+目的変数Close_nextを削除.py
X = df_new.drop(['Open_tsm',
                 'High_tsm',
                 'Low_tsm',
                 'Close_tsm',
                 'Adj Close_tsm',
                 'Volume_tsm',
                 'Open_sox',
                 'High_sox',
                 'Low_sox',
                 'Close_sox',
                 'Adj Close_sox',
                 'Volume_sox',
                 'Volume',
                 'Close_next'],axis=1)
y = df_new['Close_next']
```

```ruby:訓練データとテストデータの準備.py
#2023年末までを訓練データ、2024年以降をテストデータとして格納
X_train = X.loc[:'2023-12-30']
X_test = X.loc['2024-01-02':]

y_train = y.loc[:'2023-12-30']
y_test = y.loc['2024-01-02':]
```

学習器に訓練データとテストデータを入力し、モデルを評価します。


```ruby:それぞれのモデルでの学習と評価（NVDAのみVer.）.py
for name, model in models.items():
    # Train the model
    model.fit(X_train, y_train)

    # Make predictions
    y_pred = model.predict(X_test)

    # Evaluate the model
    mse = mean_squared_error(y_test, y_pred)
    rmse = np.sqrt(mse)
    mae = mean_absolute_error(y_test, y_pred)
    r2 = r2_score(y_test, y_pred)

    print(f'{name}:')
    print(f'Mean Squared Error (MSE): {mse}')
    print(f'Root Mean Squared Error (RMSE): {rmse}')
    print(f'Mean Absolute Error (MAE): {mae}')
    print(f'R-squared (R2): {r2}')
    print('------------------------------------')
```

それぞれの評価モデルについては下記で一覧で説明されており、分かりやすいです。
NRIさんの動画が分かりやすかったです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3795997/6bcbf879-bd41-b03a-671d-5a4ceec6d036.png)


https://www.nri.com/jp/knowledge/glossary/lst/ma/model_evaluation_index



```ruby:それぞれの評価指標の結果（NVDAのみVer.）.py

Linear Regression:
Mean Squared Error (MSE): 778.4749427670104
Root Mean Squared Error (RMSE): 27.90116382459718
Mean Absolute Error (MAE): 19.936868429009948
R-squared (R2): 0.97109414936768
------------------------------------
Lasso:
Mean Squared Error (MSE): 788.7933598751241
Root Mean Squared Error (RMSE): 28.085465277882154
Mean Absolute Error (MAE): 20.11351348330656
R-squared (R2): 0.9707110122783487
------------------------------------
Ridge:
Mean Squared Error (MSE): 779.0741734625585
Root Mean Squared Error (RMSE): 27.911900212320884
Mean Absolute Error (MAE): 19.951542564332666
R-squared (R2): 0.9710718991036982
------------------------------------
Elastic:
Mean Squared Error (MSE): 789.4092905495859
Root Mean Squared Error (RMSE): 28.096428430488917
Mean Absolute Error (MAE): 20.068403901451674
R-squared (R2): 0.9706881419210671
------------------------------------

```


決定係数（R2）はいずれのモデルも0.97越え、当てはまりは非常に良い結果となりました。

それでは次にNVIDIA、SOX、TSMC全ての株価データも追加して予測モデルに学習させます。



```ruby:NVIDIA、SOX、TSMC全ての株価データを入力する.py
#df_newからSOX指数の出来高（Volume_sox）を除外（SOX指数の出来高はゼロなので）、同時に目的変数も除外
X = df_new.drop(['Volume_sox','Close_next'],axis=1)
y = df_new['Close_next']
```

```ruby:先ほどと同様に訓練データとテストデータの期間を分ける.py
#2023年末までを訓練データ、2024年以降をテストデータとして格納
X_train = X.loc[:'2023-12-30']
X_test = X.loc['2024-01-02':]

y_train = y.loc[:'2023-12-30']
y_test = y.loc['2024-01-02':]
```

```ruby:それぞれのモデルでの学習と評価（NVDA、SOX、TSM込みVer.）.py
for name, model in models.items():
    # Train the model
    model.fit(X_train, y_train)

    # Make predictions
    y_pred = model.predict(X_test)

    # Evaluate the model
    mse = mean_squared_error(y_test, y_pred)
    rmse = np.sqrt(mse)
    mae = mean_absolute_error(y_test, y_pred)
    r2 = r2_score(y_test, y_pred)

    print(f'{name}:')
    print(f'Mean Squared Error (MSE): {mse}')
    print(f'Root Mean Squared Error (RMSE): {rmse}')
    print(f'Mean Absolute Error (MAE): {mae}')
    print(f'R-squared (R2): {r2}')
    print('------------------------------------')
```

```ruby:それぞれの評価指標の結果（NVDA、SOX、TSM込みVer.）.py
Linear Regression:
Mean Squared Error (MSE): 766.3168210756977
Root Mean Squared Error (RMSE): 27.682428019877477
Mean Absolute Error (MAE): 19.922013528693352
R-squared (R2): 0.9726927820214478
------------------------------------
Lasso:
Mean Squared Error (MSE): 783.5611343436225
Root Mean Squared Error (RMSE): 27.992162016243448
Mean Absolute Error (MAE): 20.15376943680406
R-squared (R2): 0.9720782917631802
------------------------------------
Ridge:
Mean Squared Error (MSE): 766.1547954845569
Root Mean Squared Error (RMSE): 27.67950135903024
Mean Absolute Error (MAE): 19.89159105972337
R-squared (R2): 0.9726985557014894
------------------------------------
Elastic:
Mean Squared Error (MSE): 781.9196116895635
Root Mean Squared Error (RMSE): 27.962825531222045
Mean Absolute Error (MAE): 20.13667979955643
R-squared (R2): 0.9721367863905958
------------------------------------
```

# 6.結果
説明変数にNVDAの各種データのみを入れたパターン（パターン1）と、
説明変数にNVDA、SOX、TSMのデータを盛り込んだパターン（パターン2）を
比べてみると、わずかにですがいずれのモデルも改善していることが読み取れます。
説明変数を加えることで少しだけ改善させることができました！

```ruby:パターン2はパターン1からどれくらい改善したか.py
#パターン1、パターン2の差分
diff_mse = mse_v2 - mse
diff_rmse = rmse_v2 - rmse 
diff_mae = mae_v2 - mae 
diff_r2 = r2_v2 - r2

print(f'mseの改善幅: {diff_mse}' )
print(f'rmseの改善幅: {diff_rmse}' )
print(f'maeの改善幅: {diff_mae}' )
print(f'R2の改善幅: {diff_r2}' )

＜出力結果＞
mseの改善幅: -0.5307190112670241
rmseの改善幅: -0.009488114610515908
maeの改善幅: 0.24432667495741
R2の改善幅: 1.8911838194690667e-05

```

# 7.考察、今後の課題
今回はデータ分析の第一歩として、”データの前処理”、”機械学習”、”可視化する”という一連のプロセスを体験してみたかったため、線形回帰を中心に機械学習モデルを当てはめてみました。

次は冒頭に申し上げたように、株価データを素早く可視化して、自身の分析に活かすアプリを作っていきたいと思います！（Aidemyで学習するうちに、こちらに重点を置きたいと思うようになりました。）

さらに最後の課題として、時系列分析を学んでLSTMモデルを実装して株価予測をしてみようと思っています。

以上、長文をお読みいただきありがとうございました。