پیش بینی جهت قیمت در پایتون — راهنمای کاربردی

۸۳۰ بازدید
آخرین به‌روزرسانی: ۰۷ شهریور ۱۴۰۲
زمان مطالعه: ۸ دقیقه
پیش بینی جهت قیمت در پایتون — راهنمای کاربردی

در مطالب گذشته مجله فرادرس، به پیش‌بینی قیمت در پایتون پرداختیم و با استفاده از یک مدل رگرسیون خطی (Linear Regression)، مقدار قیمت را پیش‌بینی کردیم. در این مطلب، قصد داریم به جای پیش‌بینی مقدار قیمت در آینده، به پیش بینی جهت قیمت در پایتون بپردازیم که یک مسئله طبقه‌بندی (Classification) خواهد بود.

پیش بینی جهت قیمت در پایتون

برای شروع کدنویسی، کتابخانه‌های مورد نیاز را فراخوانی می‌کنیم:

1import numpy as np
2import yfinance as yf
3import sklearn.dummy as dm
4import sklearn.metrics as met
5import matplotlib.pyplot as plt
6import sklearn.linear_model as lm
7import sklearn.preprocessing as pp

از این ۷ کتابخانه به ترتیب برای موارد زیر استفاده خواهیم کرد:

  1. محاسبات برداری
  2. دریافت برخط (Online) مجموعه داده تاریخی (Historical Dataset)
  3. ایجاد و آموزش Dummy Classifier
  4. محاسبه معیارهای ارزیابی برای مدل
  5. رسم نمودار
  6. ایجاد و آموزش مدل رگرسیون لجستیک (Logistic Regression)
  7. پیش‌پردازش داده

برای مطالعه بیشتر در مورد رگرسیون لجستیک، می‌توانید به مطلب «پیاده سازی رگرسیون لجستیک در پایتون – راهنمای گام به گام» مراجعه کنید.

برای یادگیری برنامه‌نویسی با زبان پایتون، پیشنهاد می‌کنیم به مجموعه آموزش‌های مقدماتی تا پیشرفته پایتون فرادرس مراجعه کنید که لینک آن در ادامه آورده شده است. حال تنظیمات مورد نیاز را اعمال می‌کنیم:

1np.random.seed(0)
2plt.style.use('ggplot')

حال مجموعه داده مربوط به کل تاریخچه روزانه نماد ETH/USD را دریافت می‌کنیم:

1Ticker = 'ETH-USD'
2Interval = '1d'
3Period = 'max'
4
5DF = yf.download(tickers=Ticker, interval=Interval, period=Period)

اکنون مجموعه داده را بررسی می‌کنیم که تا از صحت آن مطمئن شویم:

1print(DF.head())
2print(DF.tail())

که خواهیم داشت:

                   Open         High          Low        Close    Adj Close       Volume
Date
2017-11-09   308.644989   329.451996   307.056000   320.884003   320.884003    893249984
2017-11-10   320.670990   324.717987   294.541992   299.252991   299.252991    885985984
2017-11-11   298.585999   319.453003   298.191986   314.681000   314.681000    842300992
2017-11-12   314.690002   319.153015   298.513000   307.907990   307.907990   1613479936
2017-11-13   307.024994   328.415009   307.024994   316.716003   316.716003   1041889984

                   Open         High          Low        Close    Adj Close       Volume
Date
2022-03-14  2518.486328  2604.034424  2505.299316  2590.696045  2590.696045  11244398839
2022-03-15  2590.668945  2662.329590  2515.765869  2620.149658  2620.149658  12861105614
2022-03-16  2620.028564  2781.307129  2610.764404  2772.055664  2772.055664  17915109769
2022-03-17  2771.964111  2826.160645  2751.560791  2814.854492  2814.854492  12685265194
2022-03-18  2812.546631  2812.546631  2775.212402  2800.633301  2800.633301  12803630080

به این ترتیب، از صحت داده‌ها مطمئن می‌شویم. حال می‌توانیم درصد تغییرات نسبی در هر روز را محاسبه کنیم:

1DF['RPC'] = 100 * (DF['Close'] / DF['Open'] - 1)

حال می‌توانیم یک نمودار هیستوگرام (Histogram Plot) برای این متغیر رسم کنیم:

1plt.hist(DF['RPC'], bins=41, color='b', alpha=0.6)
2plt.title('ETH-USD Relative Percentage Change')
3plt.xlabel('Relative Change (%)')
4plt.ylabel('Frequency')
5plt.show()

که در خروجی، شکل زیر را خواهیم داشت.

پیش بینی جهت قیمت در پایتون

به این ترتیب، یک توزیع نرمال مشاهده می‌شود. حال می‌توانیم با استفاده از روش دامنه میان چارکی (Interquartile Rage) مقادیر پرت (Outlier) را اصلاح کنیم:

1k = 1.5
2q1 = DF['RPC'].quantile(0.25)
3q3 = DF['RPC'].quantile(0.75)
4iqr = q3 - q1
5lb = q1 - k * iqr
6ub = q3 + k * iqr
7DF['RPC'] = DF['RPC'].clip(lower=lb, upper=ub)

حال اگر دوباره نمودار هیستوگرام را تکرار کنیم، شکل زیر را خواهیم داشت.

نمودار هستوگرام

توجه داشته باشید که انباشت داده‌ها در دو ستون ابتدایی و انتهایی مشاهده می‌شود که به دلیل اصلاح آن داده‌ها است.

برای رفع این مشکل، می‌توان از روش‌های دیگری برای اصلاح داده‌های پرت استفاده کرد. توجه داشته باشید که با افزایش مقدار k از ۱٫۵ به ۲، نتایج را به شکل زیر تغییر می‌دهد.

پیش بینی جهت قیمت در پایتون

به این ترتیب، مشاهده می‌کنیم اندکی نتایج بهبود یافته است.

حال می‌توانیم داده‌های ستون RPC را به صورت آرایه دریافت کنیم:

1S = DF['RPC'].to_numpy()

اکنون تابع Lag را وارد برنامه می‌کنیم:

1def Lag(S:np.ndarray, L:int):
2    nD0 = S.size
3    nD = nD0 - L
4    X = np.zeros((nD, L))
5    Y = np.zeros((nD, 1))
6    for i in range(nD):
7        X[i, :] = S[i:i + L]
8        Y[i, 0] = S[i + L]
9    return X, Y

و برای استفاده از آن، به شکل زیر عمل می‌کنیم:

1nLag = 30
2X0, Y0 = Lag(S, nLag)

به این ترتیب، داده‌های اولیه حاصل می‌شود. حال داده‌ها را به دو مجموعه داده آموزش (Train Dataset) و آزمایش (Test Dataset) تقسیم می‌کنیم:

1sTrain = 0.8
2nDtr = int(sTrain * X0.shape[0])
3
4trX0 = X0[:nDtr]
5teX0 = X0[nDtr:]
6trY0 = Y0[:nDtr]
7teY0 = Y0[nDtr:]

با توجه به اینکه درصد تغییرات نسبی، مقیاس مناسبی ارائه نمی‌دهد، مقیاس آن‌ها را به شکل زیر اصلاح می‌کنیم:

1SSX = pp.StandardScaler()
2trX = SSX.fit_transform(trX0)
3teX = SSX.transform(teX0)

برای مقادیر ویژگی هدف، باید یک تابع گسسته‌ساز (Discretizer) تعریف کنیم و مقادیر را در کلاس مربوط به خود قرار دهیم. برای این کار معمولاً ۳ دسته در نظر می‌گیرم:

  1. کاهش
  2. خنثی
  3. افزایش

برای این کار، یک مقدار مرزی (Threshold) تعریف می‌کنیم:

  1. اگر تغییرات کمتر از قرینه Threshold بود، کاهش قیمت رخ داده است. (دسته ۰)
  2. اگر تغییرات بیشتر از Threshold بود، افزایش قیمت رخ داده است. (دسته ۲)
  3. در غیر این صورت، تغییرات خنثی بوده است. (دسته ۱)

برای این کار، یک تابع Discretizer تعریف می‌کنیم که در ورودی ماتریس Y0 و مقدار Threshold را دریافت می‌کند:

1def Discretizer(Y0:np.ndarray, TH:float):

ابتدا اندازه داده را محاسبه می‌کنیم:

1def Discretizer(Y0:np.ndarray, TH:float):
2    nD = Y0.size

حال یک ماتریس خالی برای ذخیره دسته هر داده ایجاد می‌کنیم:

1def Discretizer(Y0:np.ndarray, TH:float):
2    nD = Y0.size
3    Y = np.zeros((nD, 1))

حال می‌توانیم یک حلقه ایجاد کرده و برای هر داده، شرط‌های گفته شده را بررسی کنیم:

1def Discretizer(Y0:np.ndarray, TH:float):
2    nD = Y0.size
3    Y = np.zeros((nD, 1))
4    for i in range(nD):
5        if Y0[i] < -TH:
6            Y[i, 0] = 0
7        elif Y0[i] > +TH:
8            Y[i, 0] = 2
9        else:
10            Y[i, 0] = 1
11    return Y

به این ترتیب، تابع مد نظر پیاده‌سازی می‌شود. برای اندکی ساده‌سازی این تابع، می‌توان به شکل زیر نوشت:

1def Discretizer(Y0:np.ndarray, TH:float):
2    nD = Y0.size
3    Y = np.ones((nD, 1))
4    for i in range(nD):
5        if Y0[i] < -TH:
6            Y[i, 0] = 0
7        elif Y0[i] > +TH:
8            Y[i, 0] = 2
9    return Y

به این ترتیب، تابع مورد نظر پیاده‌سازی شد. حال برای استفاده از تابع، به شکل زیر می‌نویسیم:

1TH = 2
2trY = Discretizer(trY0, TH)
3teY = Discretizer(teY0, TH)

توجه داشته باشید که در خروجی کد فوق، تغییرات بین ۲- و ۲+ به عنوان حرکات خنثی در نظر گرفته می‌شود. تنظیم مقدار TH بسیار حائز اهمیت است.

حال برای استفاده از ماتریس Y برای آموزش و آزمایش مدل، آن‌ها را به شکل تک‌بُعدی تغییر می‌دهیم:

1trY = trY.reshape(-1)
2teY = teY.reshape(-1)

اکنون می‌توانیم مدل رگرسیون لجستیک را ایجاد کرده و آموزش دهیم:

1Model = lm.LogisticRegression()
2Model.fit(trX, trY)

حال می‌توانیم برای داده‌های آموزش و آزمایش پیش‌بینی‌های مدل را دریافت کنیم:

1trPr = Model.predict(trX)
2tePr = Model.predict(teX)

اکنون می‌توانیم گزارش طبقه‌بندی را به شکل زیر محاسبه و نمایش دهیم:

1trCR = met.classification_report(trY, trPr)
2teCR = met.classification_report(teY, tePr)
3
4print(f'Train Classification Report:\n{trCR}')
5print('_'*60)
6print(f'Test  Classification Report:\n{teCR}')

که در خروجی خواهیم داشت:

Train Classification Report:
              precision    recall  f1-score   support

         0.0       0.46      0.09      0.16       321
         1.0       0.49      0.86      0.62       561
         2.0       0.44      0.23      0.31       366

    accuracy                           0.48      1248
   macro avg       0.46      0.40      0.36      1248
weighted avg       0.47      0.48      0.41      1248
______________________________________________________
Test  Classification Report:
              precision    recall  f1-score   support

         0.0       0.32      0.07      0.12        96
         1.0       0.38      0.78      0.51       115
         2.0       0.27      0.14      0.18       102

    accuracy                           0.35       313
   macro avg       0.32      0.33      0.27       313
weighted avg       0.32      0.35      0.28       313

به این ترتیب، مشاهده می‌کنیم که F1 Score Macro Average برای داده‌های آموزش ۰٫۳۶ و برای داده‌های آزمایش ۰٫۲۷ است که نتایج نه‌چندان مطلوبی است. بخشی از این مشکل، از نامتعادل بودن مجموعه داده نشأت می‌گیرد که در مطالب «متعادل کردن داده در پایتون – بخش اول: وزن دهی دسته ها» و «متعادل کردن داده در پایتون – بخش دوم: تغییر مجموعه داده» به روش‌هایی برای رفع آن پرداخته شده است.

برای رفع این مشکل، وزن هر دسته را به شکل زیر محاسبه می‌کنیم:

1nClass = 3
2nTotal = trY.size
3Ns = {i: trY[trY == i].size for i in range(nClass)}
4W = {i: (nTotal - Ns[i])/((nClass - 1) * nTotal) for i in range(nClass)}

حال می‌توانیم دیکشنری وزن را به شکل زیر در تعریف مدل استفاده کنیم:

1Model = lm.LogisticRegression(class_weight=W)

پس از آموزش مدل و تکرار فرآیند، گزارش‌های زیر حاصل می‌شود:

Train Classification Report:
              precision    recall  f1-score   support

         0.0       0.39      0.25      0.30       321
         1.0       0.51      0.65      0.57       561
         2.0       0.41      0.37      0.39       366

    accuracy                           0.47      1248
   macro avg       0.44      0.42      0.42      1248
weighted avg       0.45      0.47      0.45      1248
_____________________________________________________
Test  Classification Report:
              precision    recall  f1-score   support

         0.0       0.31      0.20      0.24        96
         1.0       0.34      0.50      0.40       115
         2.0       0.27      0.22      0.24       102

    accuracy                           0.31       313
   macro avg       0.30      0.30      0.29       313
weighted avg       0.31      0.31      0.30       313

به این ترتیب، مشاهده می‌کنیم که F1 Score Macro Average برای داده‌های آموزش و آزمایش به ترتیب برابر ۰٫۴۲ و ۰٫۲۹ می‌شود. به این ترتیب، مشاهده می‌کنیم که ۰٫۰۶ واحد در مجموعه داده آموزش و ۰٫۰۲ واحد در مجموعه داده آزمایش بهبود رخ داده است.

برای درک بهتر این دقت‌ها، می‌توان یک Dummy Classifier آموزش داد و نتایج آن را با مدلِ آموزش‌دیده مقایسه کرد:

1Dummy = dm.DummyClassifier(strategy='most_frequent')
2Dummy.fit(trX, trY)
3
4trPr = Dummy.predict(trX)
5tePr = Dummy.predict(teX)
6
7trF1ScoreMA = met.f1_score(trY, trPr, average='macro')
8teF1ScoreMA = met.f1_score(teY, tePr, average='macro')
9
10print(f'Dummy Train F1 Score Macro Average: {trF1ScoreMA}')
11print(f'Dummy Test  F1 Score Macro Average: {teF1ScoreMA}')

که برای این مدل نتایج زیر حاصل می‌شود:

Dummy Train F1 Score Macro Average: 0.20674405749032612
Dummy Test  F1 Score Macro Average: 0.17798594847775176

به این ترتیب، مشاهده می‌کنیم که نتایج رگرسیون لجستیک خیلی بهتر از Dummy Classifier است. اما حتماً باید توجه کرد دقت‌های حاصل‌شده به‌اندازه‌ای خوب نیستند که قابل اعتماد باشند.

نکته دیگری که باید به آن اشاره کرد، پیچیدگی موجود در داده است. داده‌های مالی در ذات خود پیچیدگی بالایی دارند و سری زمانی بودن آن‌ها، بر این پیچیدگی می‌افزاید. به همین دلیل، باید به نکات ریز موجود در پردازش داده، انتخاب مدل و تنظیم مدل توجه کرد.

برای دست یافتن به دقت‌های بالاتر، می‌توان مدل‌هایی دیگر را مقایسه کرد. در زیر، مثال‌هایی از این مدل‌ها آورده شده است:

  1. K-نزدیک‌ترین همسایه (K-Nearest Neighbors - KNN)
  2. جنگل تصادفی (Random Forest - RF)
  3. پرسپترون چند لایه (Multi-Layer Perceptron - MLP)
  4. ماشین بردار پشتیبان (Support Vector Machine - SVM)

جمع‌بندی پیش بینی جهت قیمت در پایتون

در این مطلب، بحث پیش بینی جهت قیمت برای داده‌های مالی را بررسی کردیم.

برای مطالعه بیشتر، می‌توان موارد زیر را بررسی کرد:

  1. هرکدام از مدل‌های معرفی شده را بررسی کرده و دقت هرکدام را محاسبه کنید.
  2. چه اشکالاتی ممکن است در پیش‌پردازش داده‌ها وجود داشته باشد؟
  3. اگر داده را به جای ۳ دسته، به ۵ دسته تقسیم کنیم، تابع Discretizer به چه شکل تغییر خواهد کرد؟
  4. بین F1 Score و Accuracy کدام‌یک بیشتر قابل اعتماد است؟ چرا؟
  5. بین Macro Average و Weighted Average کدام‌یک بیشتر قابل اعتماد است؟ چرا؟
بر اساس رای ۱۱ نفر
آیا این مطلب برای شما مفید بود؟
اگر بازخوردی درباره این مطلب دارید یا پرسشی دارید که بدون پاسخ مانده است، آن را از طریق بخش نظرات مطرح کنید.
منابع:
مجله فرادرس
۲ دیدگاه برای «پیش بینی جهت قیمت در پایتون — راهنمای کاربردی»

با سلام خدمت جناب اقای کلامی و تشکر از زحمات جنابعالی و مطالب خوبتون امیدوارم همیشه پویا و زنده باشید. درخواستی داشتم اگر ممکن باشه اموزشی راجب یادگیری تقویتی در امور مالی اگر تدریس بشه خیلی عالی خواهد بود. با تشکر

سلام، تاکنون مطالب و آموزش‌هایی در این باره در مجله فرادرس منتشر شده است. می‌توانید از این مطالب استفاده نمایید.

نظر شما چیست؟

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *