UMAR798 commited on
Commit
c85b60c
Β·
verified Β·
1 Parent(s): d7bb355

Upload 2 files

Browse files
Files changed (2) hide show
  1. app.py +508 -0
  2. requirements.txt +6 -0
app.py ADDED
@@ -0,0 +1,508 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import yfinance as yf
3
+ import pandas as pd
4
+ import numpy as np
5
+ import plotly.graph_objects as go
6
+ from plotly.subplots import make_subplots
7
+ from sklearn.preprocessing import MinMaxScaler
8
+ from sklearn.linear_model import LinearRegression
9
+ from sklearn.ensemble import RandomForestRegressor, GradientBoostingRegressor
10
+ from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score
11
+ import warnings
12
+ warnings.filterwarnings("ignore")
13
+
14
+ # ── Theme colours (dark terminal-finance aesthetic) ──────────────────────────
15
+ BG = "#0d1117"
16
+ SURFACE = "#161b22"
17
+ BORDER = "#30363d"
18
+ GREEN = "#3fb950"
19
+ RED = "#f85149"
20
+ BLUE = "#58a6ff"
21
+ YELLOW = "#d29922"
22
+ TEXT = "#e6edf3"
23
+ MUTED = "#8b949e"
24
+
25
+ POPULAR_TICKERS = [
26
+ "AAPL", "MSFT", "GOOGL", "AMZN", "TSLA",
27
+ "META", "NVDA", "JPM", "BRK-B", "V",
28
+ "NFLX", "DIS", "BABA", "AMD", "INTC",
29
+ "UBER", "SPOT", "PYPL", "SQ", "SNAP",
30
+ ]
31
+
32
+ MODELS = {
33
+ "Linear Regression": LinearRegression(),
34
+ "Random Forest": RandomForestRegressor(n_estimators=100, random_state=42),
35
+ "Gradient Boosting": GradientBoostingRegressor(n_estimators=100, random_state=42),
36
+ }
37
+
38
+ PERIODS = {
39
+ "6 Months": "6mo",
40
+ "1 Year": "1y",
41
+ "2 Years": "2y",
42
+ "5 Years": "5y",
43
+ "10 Years": "10y",
44
+ }
45
+
46
+ INTERVALS = {
47
+ "Daily": "1d",
48
+ "Weekly": "1wk",
49
+ }
50
+
51
+ # ── Feature engineering ───────────────────────────────────────────────────────
52
+ def make_features(df: pd.DataFrame) -> pd.DataFrame:
53
+ df = df.copy()
54
+ df["MA7"] = df["Close"].rolling(7).mean()
55
+ df["MA21"] = df["Close"].rolling(21).mean()
56
+ df["MA50"] = df["Close"].rolling(50).mean()
57
+ df["EMA12"] = df["Close"].ewm(span=12, adjust=False).mean()
58
+ df["EMA26"] = df["Close"].ewm(span=26, adjust=False).mean()
59
+ df["MACD"] = df["EMA12"] - df["EMA26"]
60
+ df["Vol_MA"] = df["Volume"].rolling(7).mean()
61
+ df["Return1"] = df["Close"].pct_change(1)
62
+ df["Return5"] = df["Close"].pct_change(5)
63
+ df["High_Low"] = df["High"] - df["Low"]
64
+ df["Close_Open"] = df["Close"] - df["Open"]
65
+ delta = df["Close"].diff()
66
+ gain = delta.clip(lower=0).rolling(14).mean()
67
+ loss = (-delta.clip(upper=0)).rolling(14).mean()
68
+ rs = gain / loss.replace(0, np.nan)
69
+ df["RSI"] = 100 - (100 / (1 + rs))
70
+ df["Target"] = df["Close"].shift(-1)
71
+ return df.dropna()
72
+
73
+ # ── Core prediction logic ─────────────────────────────────────────────────────
74
+ def predict_stock(ticker, period_label, interval_label, model_name, future_days):
75
+ ticker = ticker.strip().upper()
76
+ period = PERIODS[period_label]
77
+ interval = INTERVALS[interval_label]
78
+
79
+ try:
80
+ raw = yf.download(ticker, period=period, interval=interval, progress=False)
81
+ if raw.empty:
82
+ return None, None, f"❌ No data found for **{ticker}**. Check the ticker symbol."
83
+ if isinstance(raw.columns, pd.MultiIndex):
84
+ raw.columns = raw.columns.get_level_values(0)
85
+ raw = raw[["Open","High","Low","Close","Volume"]].dropna()
86
+ except Exception as e:
87
+ return None, None, f"❌ Data fetch error: {e}"
88
+
89
+ if len(raw) < 60:
90
+ return None, None, f"❌ Not enough data ({len(raw)} rows). Try a longer period."
91
+
92
+ df = make_features(raw)
93
+ feature_cols = [
94
+ "MA7","MA21","MA50","EMA12","EMA26","MACD",
95
+ "Vol_MA","Return1","Return5","High_Low","Close_Open","RSI",
96
+ "Open","High","Low","Volume",
97
+ ]
98
+
99
+ X = df[feature_cols].values
100
+ y = df["Target"].values
101
+
102
+ split = int(len(X) * 0.8)
103
+ X_train, X_test = X[:split], X[split:]
104
+ y_train, y_test = y[:split], y[split:]
105
+
106
+ scaler = MinMaxScaler()
107
+ X_train_s = scaler.fit_transform(X_train)
108
+ X_test_s = scaler.transform(X_test)
109
+
110
+ model = MODELS[model_name]
111
+ model.fit(X_train_s, y_train)
112
+ y_pred = model.predict(X_test_s)
113
+
114
+ mae = mean_absolute_error(y_test, y_pred)
115
+ rmse = np.sqrt(mean_squared_error(y_test, y_pred))
116
+ r2 = r2_score(y_test, y_pred)
117
+ acc = max(0.0, r2) * 100
118
+
119
+ # ── Future forecast ───────────────────────────────────────────────────────
120
+ future_days = int(future_days)
121
+ last_features = X[-1].reshape(1, -1)
122
+ future_prices = []
123
+ cur = last_features.copy()
124
+ for _ in range(future_days):
125
+ cur_s = scaler.transform(cur)
126
+ nxt = model.predict(cur_s)[0]
127
+ future_prices.append(float(nxt))
128
+ cur[0, 0] = nxt # crude: update Close proxy
129
+
130
+ last_date = df.index[-1]
131
+ freq = "B" if interval == "1d" else "W"
132
+ fut_dates = pd.date_range(last_date, periods=future_days + 1, freq=freq)[1:]
133
+
134
+ # ── Candlestick + prediction chart ───────────────────────────────────────
135
+ fig = make_subplots(
136
+ rows=3, cols=1,
137
+ shared_xaxes=True,
138
+ row_heights=[0.55, 0.25, 0.20],
139
+ vertical_spacing=0.04,
140
+ subplot_titles=("Price & Prediction", "Volume", "RSI"),
141
+ )
142
+
143
+ test_dates = df.index[split:]
144
+
145
+ # Candlestick (all history)
146
+ fig.add_trace(go.Candlestick(
147
+ x=raw.index, open=raw["Open"], high=raw["High"],
148
+ low=raw["Low"], close=raw["Close"],
149
+ increasing_line_color=GREEN, decreasing_line_color=RED,
150
+ name="Price", showlegend=False,
151
+ ), row=1, col=1)
152
+
153
+ # Moving averages
154
+ for col, color, label in [("MA21", BLUE, "MA 21"), ("MA50", YELLOW, "MA 50")]:
155
+ fig.add_trace(go.Scatter(
156
+ x=df.index, y=df[col], line=dict(color=color, width=1.2),
157
+ name=label, opacity=0.85,
158
+ ), row=1, col=1)
159
+
160
+ # Test-set predictions
161
+ fig.add_trace(go.Scatter(
162
+ x=test_dates, y=y_pred,
163
+ line=dict(color="#a371f7", width=1.8, dash="dot"),
164
+ name="Model (test)", opacity=0.9,
165
+ ), row=1, col=1)
166
+
167
+ # Future forecast
168
+ fig.add_trace(go.Scatter(
169
+ x=list(fut_dates), y=future_prices,
170
+ line=dict(color="#f0883e", width=2),
171
+ name=f"Forecast ({future_days}d)",
172
+ mode="lines+markers",
173
+ marker=dict(size=5),
174
+ ), row=1, col=1)
175
+
176
+ # Vertical "today" line
177
+ fig.add_vline(x=str(last_date.date()), line_width=1,
178
+ line_dash="dash", line_color=MUTED, row=1, col=1)
179
+
180
+ # Volume bars
181
+ colors_v = [GREEN if c >= o else RED
182
+ for c, o in zip(raw["Close"], raw["Open"])]
183
+ fig.add_trace(go.Bar(
184
+ x=raw.index, y=raw["Volume"],
185
+ marker_color=colors_v, showlegend=False, name="Volume",
186
+ ), row=2, col=1)
187
+
188
+ # RSI
189
+ fig.add_trace(go.Scatter(
190
+ x=df.index, y=df["RSI"],
191
+ line=dict(color=BLUE, width=1.5),
192
+ name="RSI", showlegend=False,
193
+ ), row=3, col=1)
194
+ fig.add_hline(y=70, line_dash="dash", line_color=RED, line_width=0.8, row=3, col=1)
195
+ fig.add_hline(y=30, line_dash="dash", line_color=GREEN, line_width=0.8, row=3, col=1)
196
+
197
+ fig.update_layout(
198
+ paper_bgcolor=BG, plot_bgcolor=SURFACE,
199
+ font=dict(family="'JetBrains Mono', monospace", color=TEXT, size=12),
200
+ legend=dict(bgcolor=SURFACE, bordercolor=BORDER, borderwidth=1,
201
+ x=0.01, y=0.99, font=dict(size=11)),
202
+ margin=dict(l=10, r=10, t=40, b=10),
203
+ xaxis_rangeslider_visible=False,
204
+ height=700,
205
+ )
206
+ for row in [1, 2, 3]:
207
+ fig.update_xaxes(
208
+ gridcolor=BORDER, showgrid=True,
209
+ zeroline=False, row=row, col=1,
210
+ )
211
+ fig.update_yaxes(
212
+ gridcolor=BORDER, showgrid=True,
213
+ zeroline=False, row=row, col=1,
214
+ )
215
+
216
+ # ── Metrics card (markdown) ───────────────────────────────────────────────
217
+ cur_price = float(raw["Close"].iloc[-1])
218
+ fst_fcast = future_prices[0] if future_prices else cur_price
219
+ lst_fcast = future_prices[-1] if future_prices else cur_price
220
+ delta_pct = (lst_fcast - cur_price) / cur_price * 100
221
+ arrow = "β–²" if delta_pct >= 0 else "β–Ό"
222
+ clr_tag = "🟒" if delta_pct >= 0 else "πŸ”΄"
223
+
224
+ rsi_now = float(df["RSI"].iloc[-1])
225
+ rsi_sig = ("Overbought ⚠️" if rsi_now > 70
226
+ else "Oversold πŸ’‘" if rsi_now < 30
227
+ else "Neutral βœ…")
228
+
229
+ stats_md = f"""
230
+ ## {ticker} β€” {model_name}
231
+
232
+ | Metric | Value |
233
+ |--------|-------|
234
+ | **Current Price** | `${cur_price:,.2f}` |
235
+ | **Next-Period Forecast** | `${fst_fcast:,.2f}` |
236
+ | **{future_days}-Day Forecast** | `${lst_fcast:,.2f}` |
237
+ | **Expected Change** | `{clr_tag} {arrow} {abs(delta_pct):.2f}%` |
238
+
239
+ ---
240
+
241
+ ### Model Performance (test set)
242
+ | | |
243
+ |---|---|
244
+ | MAE | `${mae:.4f}` |
245
+ | RMSE | `${rmse:.4f}` |
246
+ | RΒ² | `{r2:.4f}` |
247
+ | Accuracy proxy | `{acc:.1f}%` |
248
+
249
+ ---
250
+
251
+ ### Technical Signals
252
+ | Indicator | Value | Signal |
253
+ |-----------|-------|--------|
254
+ | RSI (14) | `{rsi_now:.1f}` | {rsi_sig} |
255
+ | MACD | `{float(df['MACD'].iloc[-1]):.4f}` | {'Bullish πŸ“ˆ' if float(df['MACD'].iloc[-1]) > 0 else 'Bearish πŸ“‰'} |
256
+ | MA21 vs MA50 | β€” | {'Golden Cross ✨' if float(df['MA21'].iloc[-1]) > float(df['MA50'].iloc[-1]) else 'Death Cross πŸ’€'} |
257
+
258
+ > ⚠️ **Disclaimer:** This tool is for educational purposes only. Not financial advice.
259
+ """
260
+
261
+ return fig, stats_md, ""
262
+
263
+ # ── UI layout ─────────────────────────────────────────────────────────────────
264
+ css = f"""
265
+ @import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;600&family=Inter:wght@400;600&display=swap');
266
+
267
+ * {{ box-sizing: border-box; }}
268
+
269
+ body, .gradio-container {{
270
+ background: {BG} !important;
271
+ color: {TEXT} !important;
272
+ font-family: 'Inter', sans-serif !important;
273
+ }}
274
+
275
+ /* Header */
276
+ .app-header {{
277
+ background: {SURFACE};
278
+ border-bottom: 1px solid {BORDER};
279
+ padding: 28px 32px 20px;
280
+ margin-bottom: 24px;
281
+ }}
282
+ .app-header h1 {{
283
+ font-family: 'JetBrains Mono', monospace;
284
+ font-size: 2rem;
285
+ font-weight: 600;
286
+ color: {TEXT};
287
+ margin: 0 0 4px;
288
+ letter-spacing: -0.5px;
289
+ }}
290
+ .app-header p {{
291
+ color: {MUTED};
292
+ font-size: 0.9rem;
293
+ margin: 0;
294
+ }}
295
+ .accent {{ color: {GREEN}; }}
296
+
297
+ /* Cards */
298
+ .card {{
299
+ background: {SURFACE} !important;
300
+ border: 1px solid {BORDER} !important;
301
+ border-radius: 8px !important;
302
+ padding: 16px !important;
303
+ }}
304
+
305
+ /* Controls */
306
+ label {{
307
+ color: {MUTED} !important;
308
+ font-size: 0.78rem !important;
309
+ font-weight: 600 !important;
310
+ text-transform: uppercase !important;
311
+ letter-spacing: 0.08em !important;
312
+ margin-bottom: 4px !important;
313
+ }}
314
+ input, select, .svelte-1gfkn6j {{
315
+ background: {BG} !important;
316
+ border: 1px solid {BORDER} !important;
317
+ color: {TEXT} !important;
318
+ border-radius: 6px !important;
319
+ font-family: 'JetBrains Mono', monospace !important;
320
+ }}
321
+ input:focus {{ border-color: {BLUE} !important; outline: none !important; }}
322
+
323
+ /* Run button */
324
+ .run-btn button {{
325
+ background: {GREEN} !important;
326
+ color: #0d1117 !important;
327
+ font-weight: 700 !important;
328
+ font-size: 0.95rem !important;
329
+ border: none !important;
330
+ border-radius: 6px !important;
331
+ padding: 12px 0 !important;
332
+ width: 100% !important;
333
+ cursor: pointer !important;
334
+ font-family: 'JetBrains Mono', monospace !important;
335
+ letter-spacing: 0.05em !important;
336
+ transition: opacity .15s;
337
+ }}
338
+ .run-btn button:hover {{ opacity: 0.85; }}
339
+
340
+ /* Metrics markdown */
341
+ .stats-box {{
342
+ background: {BG} !important;
343
+ border: 1px solid {BORDER} !important;
344
+ border-radius: 8px !important;
345
+ padding: 20px !important;
346
+ font-family: 'JetBrains Mono', monospace !important;
347
+ font-size: 0.82rem !important;
348
+ }}
349
+ .stats-box table {{ width: 100%; border-collapse: collapse; }}
350
+ .stats-box td, .stats-box th {{
351
+ padding: 6px 10px;
352
+ border-bottom: 1px solid {BORDER};
353
+ text-align: left;
354
+ }}
355
+ .stats-box th {{ color: {MUTED}; font-weight: 600; }}
356
+ .stats-box code {{
357
+ background: {SURFACE};
358
+ padding: 2px 6px;
359
+ border-radius: 4px;
360
+ color: {BLUE};
361
+ }}
362
+
363
+ /* Error box */
364
+ .error-box textarea {{
365
+ background: transparent !important;
366
+ color: {RED} !important;
367
+ border: none !important;
368
+ font-family: 'JetBrains Mono', monospace !important;
369
+ font-size: 0.85rem !important;
370
+ }}
371
+
372
+ /* Ticker pills */
373
+ .ticker-pills {{
374
+ display: flex; flex-wrap: wrap; gap: 6px;
375
+ margin-top: 8px;
376
+ }}
377
+ .ticker-pills button {{
378
+ background: {SURFACE} !important;
379
+ border: 1px solid {BORDER} !important;
380
+ color: {TEXT} !important;
381
+ border-radius: 4px !important;
382
+ padding: 3px 10px !important;
383
+ font-size: 0.75rem !important;
384
+ font-family: 'JetBrains Mono', monospace !important;
385
+ cursor: pointer !important;
386
+ transition: border-color .15s;
387
+ }}
388
+ .ticker-pills button:hover {{ border-color: {GREEN} !important; color: {GREEN} !important; }}
389
+
390
+ /* Plotly panel */
391
+ .plot-panel {{ border: 1px solid {BORDER}; border-radius: 8px; overflow: hidden; }}
392
+
393
+ /* Slider */
394
+ input[type=range] {{ accent-color: {BLUE}; }}
395
+
396
+ /* Footer */
397
+ .footer {{
398
+ text-align: center;
399
+ color: {MUTED};
400
+ font-size: 0.75rem;
401
+ margin-top: 32px;
402
+ padding: 16px;
403
+ border-top: 1px solid {BORDER};
404
+ }}
405
+ """
406
+
407
+ # ── Build Gradio app ──────────────────────────────────────────────────────────
408
+ with gr.Blocks(css=css, title="StockSense β€” ML Stock Predictor") as demo:
409
+
410
+ # Header
411
+ gr.HTML("""
412
+ <div class="app-header">
413
+ <h1>πŸ“ˆ Stock<span class="accent">Sense</span></h1>
414
+ <p>Machine-learning powered stock analysis &amp; price forecasting Β· Educational use only</p>
415
+ </div>
416
+ """)
417
+
418
+ with gr.Row():
419
+ # ── Left panel: controls ─────────────────────────────────────────────
420
+ with gr.Column(scale=1, elem_classes="card"):
421
+
422
+ gr.HTML("<div style='font-size:0.78rem;color:#8b949e;font-weight:600;text-transform:uppercase;letter-spacing:.08em;margin-bottom:8px'>Quick Pick</div>")
423
+ ticker_pills_html = "".join(
424
+ f'<button onclick="document.querySelector(\'#ticker_input input\').value=\'{t}\';'
425
+ f'document.querySelector(\'#ticker_input input\').dispatchEvent(new Event(\'input\'))">{t}</button>'
426
+ for t in POPULAR_TICKERS
427
+ )
428
+ gr.HTML(f'<div class="ticker-pills">{ticker_pills_html}</div>')
429
+
430
+ ticker_input = gr.Textbox(
431
+ label="Ticker Symbol",
432
+ placeholder="e.g. AAPL, TSLA, MSFT …",
433
+ value="AAPL",
434
+ elem_id="ticker_input",
435
+ )
436
+
437
+ with gr.Row():
438
+ period_input = gr.Dropdown(
439
+ label="History Period",
440
+ choices=list(PERIODS.keys()),
441
+ value="2 Years",
442
+ )
443
+ interval_input = gr.Dropdown(
444
+ label="Interval",
445
+ choices=list(INTERVALS.keys()),
446
+ value="Daily",
447
+ )
448
+
449
+ model_input = gr.Dropdown(
450
+ label="ML Model",
451
+ choices=list(MODELS.keys()),
452
+ value="Random Forest",
453
+ )
454
+
455
+ future_input = gr.Slider(
456
+ label="Forecast Horizon (days)",
457
+ minimum=1, maximum=90, step=1, value=30,
458
+ )
459
+
460
+ run_btn = gr.Button("β–Ά Run Prediction", elem_classes="run-btn")
461
+ error_box = gr.Textbox(
462
+ visible=True, interactive=False,
463
+ show_label=False, elem_classes="error-box",
464
+ )
465
+
466
+ # Stats card below button
467
+ stats_md = gr.Markdown(
468
+ value="*Run a prediction to see metrics here.*",
469
+ elem_classes="stats-box",
470
+ )
471
+
472
+ # ── Right panel: chart ───────────────────────────────────────────────
473
+ with gr.Column(scale=3):
474
+ chart = gr.Plot(elem_classes="plot-panel", label="")
475
+
476
+ # Footer
477
+ gr.HTML(f"""
478
+ <div class="footer">
479
+ StockSense Β· Built with Gradio &amp; scikit-learn Β·
480
+ Data via Yahoo Finance Β·
481
+ <span style="color:{RED}">Not financial advice</span>
482
+ </div>
483
+ """)
484
+
485
+ # ── Wire up ───────────────────────────────────────────────────────────────
486
+ def run(ticker, period, interval, model, future):
487
+ fig, stats, err = predict_stock(ticker, period, interval, model, future)
488
+ return (
489
+ fig if fig else go.Figure(),
490
+ stats if stats else "",
491
+ err,
492
+ )
493
+
494
+ run_btn.click(
495
+ fn=run,
496
+ inputs=[ticker_input, period_input, interval_input, model_input, future_input],
497
+ outputs=[chart, stats_md, error_box],
498
+ )
499
+
500
+ # Auto-run on load
501
+ demo.load(
502
+ fn=run,
503
+ inputs=[ticker_input, period_input, interval_input, model_input, future_input],
504
+ outputs=[chart, stats_md, error_box],
505
+ )
506
+
507
+ if __name__ == "__main__":
508
+ demo.launch()
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ gradio>=4.0.0
2
+ yfinance>=0.2.36
3
+ pandas>=2.0.0
4
+ numpy>=1.24.0
5
+ scikit-learn>=1.3.0
6
+ plotly>=5.17.0