Skip to content

Commit 2d8ed4f

Browse files
committed
Refactor code structure for improved readability and maintainability
1 parent e889b80 commit 2d8ed4f

43 files changed

Lines changed: 4100 additions & 88 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

ArimaStockForecast.py

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
import os
2+
import time
3+
import warnings
4+
5+
import matplotlib.pyplot as plt
6+
import numpy as np
7+
import korean_font # noqa: F401
8+
from statsmodels.tsa.arima.model import ARIMA
9+
from statsmodels.tsa.stattools import adfuller
10+
11+
warnings.filterwarnings("ignore")
12+
os.makedirs("result", exist_ok=True)
13+
14+
print("=" * 60)
15+
print(" ARIMA 모델: 주가 시계열 예측 실습")
16+
print("=" * 60)
17+
print(" ARIMA(p, d, q)")
18+
print(" p = AR 차수: 몇 일 전까지 자기 자신의 영향을 받는가")
19+
print(" d = 차분 횟수: 정상성을 만들기 위해 몇 번 뺄셈하는가")
20+
print(" q = MA 차수: 몇 일 전까지 과거 오차의 영향을 받는가")
21+
22+
# ── 1. 데이터 로드 ─────────────────────────────────────────
23+
print("\n[1/7] 주가 데이터 로드 중...")
24+
time.sleep(0.5)
25+
np.random.seed(42)
26+
TICKER = '078935.KS'
27+
prices = None
28+
try:
29+
import yfinance as yf
30+
from datetime import date
31+
df = yf.download(TICKER, start='2020-01-01', end=date.today().isoformat(),
32+
auto_adjust=True, progress=False)
33+
if len(df) > 50:
34+
prices = df['Close'].squeeze().dropna().values.flatten().astype(np.float32)
35+
print(f" ✓ {TICKER}: {len(prices)}일 실제 데이터 로드")
36+
except Exception as e:
37+
print(f" yfinance 오류 ({e}) → 가상 데이터 사용")
38+
39+
if prices is None:
40+
days = 200
41+
t = np.arange(days)
42+
prices = (100 + 0.1 * t + 3 * np.sin(t / 15) + np.random.normal(0, 1.2, days)).astype(np.float32)
43+
print(f" → 가상 {days}일치 주가 생성")
44+
45+
days = len(prices)
46+
split_idx = int(days * 0.8)
47+
train_prices = prices[:split_idx]
48+
test_prices = prices[split_idx:]
49+
print(f" → 학습 데이터: {len(train_prices)}일 | 테스트(예측 대상): {len(test_prices)}일")
50+
time.sleep(0.3)
51+
52+
# ── 2. 정상성 검정 ─────────────────────────────────────────
53+
print("\n[2/7] ADF 정상성 검정 중...")
54+
print(" 가설: 'H0 = 비정상 시계열이다' → p < 0.05 이면 H0 기각 = 정상")
55+
time.sleep(0.5)
56+
adf_raw = adfuller(train_prices)
57+
adf_diff = adfuller(np.diff(train_prices))
58+
print(f" 원본 주가 p-value: {adf_raw[1]:.4f}{'비정상 → 차분 필요' if adf_raw[1] > 0.05 else '정상'}")
59+
print(f" 1차 차분 p-value: {adf_diff[1]:.4f}{'비정상' if adf_diff[1] > 0.05 else '정상 → d=1 확정'}")
60+
d = 1 if adf_raw[1] > 0.05 else 0
61+
print(f" → 차분 횟수(d) = {d}")
62+
time.sleep(0.4)
63+
64+
# ── 3. p, q 결정 ──────────────────────────────────────────
65+
print("\n[3/7] ARIMA 차수 결정 중 (p=2, d=1, q=2 사용)...")
66+
print(" 방법: ACF로 q, PACF로 p를 추정합니다")
67+
print(" 이 실습에서는 주가 예측 일반적 조합 ARIMA(2,1,2)를 사용합니다")
68+
p, q = 2, 2
69+
print(f" → 최종 설정: ARIMA({p}, {d}, {q})")
70+
time.sleep(0.5)
71+
72+
# ── 4. 모델 학습 ──────────────────────────────────────────
73+
print("\n[4/7] ARIMA 모델 학습 중...")
74+
print(" AR(자기회귀): 과거 주가가 오늘 주가에 미치는 영향 학습")
75+
print(" MA(이동평균): 과거 예측 오차가 오늘에 미치는 영향 학습")
76+
time.sleep(0.8)
77+
model = ARIMA(train_prices, order=(p, d, q))
78+
fitted = model.fit()
79+
print(f" → 학습 완료!")
80+
print(f" AIC: {fitted.aic:.2f} (작을수록 더 좋은 모델)")
81+
print(f" BIC: {fitted.bic:.2f}")
82+
time.sleep(0.5)
83+
84+
# ── 5. 학습 데이터 내 적합값 ──────────────────────────────
85+
print("\n[5/7] 학습 기간 내 적합값(fitted values) 확인 중...")
86+
time.sleep(0.4)
87+
in_sample_pred = fitted.fittedvalues
88+
residuals = train_prices[1:] - in_sample_pred[1:]
89+
rmse_train = np.sqrt(np.mean(residuals ** 2))
90+
print(f" → 학습 기간 RMSE: {rmse_train:.4f}원")
91+
time.sleep(0.3)
92+
93+
# ── 6. 미래 예측 ──────────────────────────────────────────
94+
print("\n[6/7] 테스트 구간 예측 중 (40일)...")
95+
print(" 방법: 1일씩 예측 후 실제값을 반영해 다음 예측 (워킹 포워드)")
96+
time.sleep(0.5)
97+
history = list(train_prices)
98+
one_step_preds = []
99+
for i in range(len(test_prices)):
100+
m = ARIMA(history, order=(p, d, q))
101+
f = m.fit()
102+
yhat = f.forecast(steps=1)[0]
103+
one_step_preds.append(yhat)
104+
history.append(test_prices[i])
105+
if (i + 1) % 10 == 0:
106+
print(f" {i + 1}/{len(test_prices)}일 예측 완료...")
107+
time.sleep(0.2)
108+
one_step_preds = np.array(one_step_preds)
109+
rmse_test = np.sqrt(np.mean((test_prices - one_step_preds) ** 2))
110+
mae_test = np.mean(np.abs(test_prices - one_step_preds))
111+
print(f" → 테스트 RMSE: {rmse_test:.4f}원 | MAE: {mae_test:.4f}원")
112+
time.sleep(0.3)
113+
114+
# ── 7. 시각화 ─────────────────────────────────────────────
115+
print("\n[7/7] 시각화 저장 중...")
116+
time.sleep(0.5)
117+
118+
fig, axes = plt.subplots(2, 1, figsize=(12, 8))
119+
120+
# 전체 그래프 요약 제목
121+
fig.text(0.5, 0.98, '과거 주가의 패턴을 수식으로 정리해 미래를 예측합니다 (ARIMA)',
122+
ha='center', fontsize=9, color='#333', weight='bold')
123+
124+
# 전체 구간
125+
ax = axes[0]
126+
ax.plot(range(days), prices, color='steelblue', linewidth=1.2, label='실제 주가', alpha=0.8)
127+
ax.plot(range(1, len(in_sample_pred)), in_sample_pred[1:],
128+
color='orange', linewidth=1.0, linestyle='--', label='ARIMA 적합값(학습)')
129+
ax.plot(range(split_idx, days), one_step_preds,
130+
color='tomato', linewidth=1.5, label='ARIMA 예측(테스트)')
131+
ax.axvline(split_idx, color='gray', linestyle=':', linewidth=1.5, label='학습/테스트 경계')
132+
ax.set_title(f"ARIMA({p},{d},{q}) 주가 예측 | 테스트 RMSE={rmse_test:.2f}")
133+
ax.set_xlabel("거래일")
134+
ax.set_ylabel("주가")
135+
ax.legend(fontsize=9)
136+
# 학습 구간 설명
137+
ax.text(0.35, 0.12, '컴퓨터가 공부한 구간', transform=ax.transAxes,
138+
ha='center', fontsize=8, color='steelblue',
139+
bbox=dict(boxstyle='round,pad=0.3', facecolor='lightblue', alpha=0.4))
140+
# 테스트 구간 설명
141+
ax.text(0.88, 0.12, '실제로 예측해본 구간\n(답을 보여주지 않음)', transform=ax.transAxes,
142+
ha='center', fontsize=8, color='tomato',
143+
bbox=dict(boxstyle='round,pad=0.3', facecolor='mistyrose', alpha=0.4))
144+
# 세로 점선 화살표 어노테이션
145+
ax.annotate('여기서부터 진짜 예측 시작!', xy=(0.80, 0.60), xytext=(0.65, 0.78),
146+
xycoords='axes fraction', textcoords='axes fraction',
147+
arrowprops=dict(arrowstyle='->', color='gray'), fontsize=7, color='#333')
148+
# 예측선 설명
149+
ax.annotate('ARIMA 예측값', xy=(0.88, 0.75), xytext=(0.70, 0.90),
150+
xycoords='axes fraction', textcoords='axes fraction',
151+
arrowprops=dict(arrowstyle='->', color='tomato'), fontsize=7, color='darkred')
152+
ax.text(0.5, -0.12, 'ARIMA = 자기회귀(AR) + 차분(I) + 이동평균오차(MA)의 조합 모델',
153+
transform=ax.transAxes, ha='center', fontsize=7, color='gray')
154+
155+
# 테스트 구간 확대
156+
ax2 = axes[1]
157+
ax2.plot(range(len(test_prices)), test_prices,
158+
color='steelblue', linewidth=1.5, label='실제 주가')
159+
ax2.plot(range(len(test_prices)), one_step_preds,
160+
color='tomato', linewidth=1.5, linestyle='--', label='예측 주가')
161+
ax2.fill_between(range(len(test_prices)),
162+
test_prices, one_step_preds, alpha=0.2, color='gray', label='오차 구간')
163+
ax2.set_title("테스트 구간 확대 (실제 vs 예측)")
164+
ax2.set_xlabel("테스트 인덱스")
165+
ax2.set_ylabel("주가")
166+
ax2.legend(fontsize=9)
167+
# 회색 띠 설명
168+
ax2.annotate('이 범위 안에 실제값이 있을 거라는 오차 구간',
169+
xy=(0.5, 0.5), xytext=(0.55, 0.25),
170+
xycoords='axes fraction', textcoords='axes fraction',
171+
arrowprops=dict(arrowstyle='->', color='gray'), fontsize=7, color='#333')
172+
# 오차 설명
173+
ax2.annotate('이 차이가 오차예요',
174+
xy=(0.25, 0.65), xytext=(0.05, 0.85),
175+
xycoords='axes fraction', textcoords='axes fraction',
176+
arrowprops=dict(arrowstyle='->', color='gray'), fontsize=7, color='#333')
177+
ax2.text(0.5, -0.12, '실제선(파랑)과 예측선(빨강)이 가까울수록 예측이 잘 된 것입니다',
178+
transform=ax2.transAxes, ha='center', fontsize=7, color='gray')
179+
180+
plt.tight_layout()
181+
plt.subplots_adjust(top=0.93)
182+
ticker_tag = TICKER.replace('.', '_')
183+
plt.savefig(f"result/ArimaStockForecast_{ticker_tag}.png", dpi=150, bbox_inches="tight")
184+
print(f" → 그래프 저장: result/ArimaStockForecast_{ticker_tag}.png")
185+
186+
print("\n✓ ARIMA 주가 예측 실습 완료!\n")

0 commit comments

Comments
 (0)