This commit is contained in:
Rynare 2025-05-19 22:01:54 +07:00
commit b2810f3d10
6 changed files with 302 additions and 0 deletions

34
.gitignore vendored Normal file
View File

@ -0,0 +1,34 @@
# Folder virtual environment (bisa beda nama, contoh: venv, env, .venv, dll)
venv/
env/
.venv/
# File Python cache dan artefak
__pycache__/
*.py[cod]
*$py.class
# VSCode settings (kalau pakai VSCode)
.vscode/
# File database sementara
*.sqlite3
# Jupyter Notebook checkpoint
.ipynb_checkpoints/
# Log files
*.log
# macOS stuff
.DS_Store
# Python environment config (jika ada)
.Python
# dotenv
.env
.env.*
# Compiled Cython files
*.so

26
main.py Normal file
View File

@ -0,0 +1,26 @@
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from routes.predict_file import router as predict_file_router
from routes.predict_json import router as predict_json_router
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Register API Router
app.include_router(predict_file_router, prefix="/api")
app.include_router(predict_json_router, prefix="/api")
@app.get("/")
async def root():
return {"message": "Welcome to ARIMA Prediction API (JSON version)"}
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=6000)

32
requirements.txt Normal file
View File

@ -0,0 +1,32 @@
annotated-types==0.7.0
anyio==4.9.0
click==8.2.0
colorama==0.4.6
Cython==3.1.0
exceptiongroup==1.3.0
fastapi==0.115.12
h11==0.16.0
idna==3.10
joblib==1.5.0
numpy==1.23.5
packaging==25.0
pandas==2.2.3
patsy==1.0.1
pmdarima==2.0.4
pydantic==2.11.4
pydantic_core==2.33.2
python-dateutil==2.9.0.post0
python-multipart==0.0.20
pytz==2025.2
scikit-learn==1.6.1
scipy==1.15.3
six==1.17.0
sniffio==1.3.1
starlette==0.46.2
statsmodels==0.14.4
threadpoolctl==3.6.0
typing-inspection==0.4.0
typing_extensions==4.13.2
tzdata==2025.2
urllib3==2.4.0
uvicorn==0.34.2

72
routes/predict_file.py Normal file
View File

@ -0,0 +1,72 @@
from fastapi import APIRouter, File, UploadFile, Form, HTTPException
from typing import List, Literal
import pandas as pd
import io
from services.forecastService import forecast_arima_per_product
router = APIRouter()
@router.post("/predict-file")
async def predict(
sheet: UploadFile = File(...),
# recordPeriod: Literal["daily", "weekly", "monthly"] = Form(...),
predictionPeriod: Literal["weekly", "monthly"] = Form(...),
predictionMode: Literal["auto", "optimal", "custom"] = Form(...),
arimaModel: str = Form("")
):
try:
# Parse model
model_values: List[int] = []
if predictionMode == "custom":
if not arimaModel:
raise HTTPException(status_code=400, detail="arimaModel harus diisi saat predictionMode adalah 'custom'")
try:
model_values = list(map(int, arimaModel.split(",")))
if len(model_values) != 3:
raise ValueError
except ValueError:
raise HTTPException(status_code=400, detail="Format arimaModel harus 'p,d,q'.")
# Baca file
content = await sheet.read()
df = pd.read_csv(io.BytesIO(content)) if sheet.filename.endswith(".csv") else pd.read_excel(io.BytesIO(content))
if df.empty:
raise HTTPException(status_code=400, detail="File tidak berisi data.")
# Validasi kolom
if 'product_code' not in df.columns and 'product_name' not in df.columns:
raise HTTPException(status_code=400, detail="Data harus memiliki kolom 'product_code' atau 'product_name'.")
if 'date' not in df.columns or 'sold(qty)' not in df.columns:
raise HTTPException(status_code=400, detail="Data harus memiliki kolom 'date' dan 'sold(qty)'.")
product_column = 'product_name' if 'product_name' in df.columns else 'product_code'
df['date'] = pd.to_datetime(df['date'])
df = df.sort_values(by=[product_column, 'date'])
freq_map = {"daily": "D", "weekly": "W", "monthly": "M"}
horizon = 3
results = []
for product, group in df.groupby(product_column):
try:
result = forecast_arima_per_product(group, freq_map[predictionPeriod], predictionMode, model_values, horizon)
forecast = result["forecast"]
results.append({
"predictionPeriod":predictionPeriod,
"product": product,
"order": ",".join(map(str, result["model_params"])),
"phase1": forecast[0] if len(forecast) > 0 else None,
"phase2": forecast[1] if len(forecast) > 1 else None,
"phase3": forecast[2] if len(forecast) > 2 else None,
})
except Exception as model_err:
results.append({
"product": product,
"error": str(model_err)
})
return {"status": "success", "data": results}
except Exception as e:
raise HTTPException(status_code=500, detail=f"Terjadi kesalahan saat memproses file: {str(e)}")

80
routes/predict_json.py Normal file
View File

@ -0,0 +1,80 @@
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
from typing import List, Optional, Literal
import numpy as np
import pandas as pd
from statsmodels.tsa.arima.model import ARIMA
from statsmodels.tsa.stattools import adfuller, acf, pacf
from sklearn.metrics import mean_squared_error
router = APIRouter()
class TimeSeriesData(BaseModel):
date: List[str]
value: List[float]
class PredictionRequest(BaseModel):
data: TimeSeriesData
model: Literal['optimal', 'custom', 'auto'] = "auto"
forecast_step: int
order: Optional[List[int]] = None
def determine_d(series):
""" Menentukan jumlah differencing (d) berdasarkan uji Augmented Dickey-Fuller """
d = 0
while adfuller(series)[1] > 0.05 and d < 2:
series = series.diff().dropna()
d += 1
return d
def determine_p_q(series):
""" Menentukan p dan q berdasarkan ACF dan PACF """
acf_vals = acf(series.dropna(), nlags=10)
pacf_vals = pacf(series.dropna(), nlags=10)
p = next((i for i, v in enumerate(pacf_vals[1:], start=1) if abs(v) > 0.2), 1)
q = next((i for i, v in enumerate(acf_vals[1:], start=1) if abs(v) > 0.2), 1)
return p, q
@router.post("/predict-json")
async def predict_json(request: PredictionRequest):
if len(request.data.date) != len(request.data.value):
raise HTTPException(status_code=400, detail="Date and value lists must have the same length.")
try:
df = pd.DataFrame({"date": pd.to_datetime(request.data.date), "value": request.data.value})
df = df.dropna().sort_values(by="date").set_index("date")
except Exception as e:
raise HTTPException(status_code=400, detail=f"Invalid data format: {str(e)}")
if len(df) < 60:
raise HTTPException(status_code=400, detail="Insufficient data: At least 60 records required.")
train_size = int(len(df) * 0.7)
train, test = df[:train_size], df[train_size:]
if request.model == "auto":
d = determine_d(train["value"])
p, q = determine_p_q(train["value"])
elif request.model == "optimal":
p, d, q = 2, 1, 2
elif request.model == "custom":
if not request.order or len(request.order) != 3:
raise HTTPException(status_code=400, detail="Custom model requires an array of [p, d, q].")
p, d, q = request.order
else:
raise HTTPException(status_code=400, detail="Invalid model type. Choose 'auto', 'optimal', or 'custom'.")
try:
arima_model = ARIMA(train["value"], order=(p, d, q))
model_fit = arima_model.fit()
predictions = model_fit.forecast(steps=len(test)).tolist()
rmse = np.sqrt(mean_squared_error(test["value"], predictions))
future_forecast = model_fit.forecast(steps=request.forecast_step).tolist()
except Exception as e:
raise HTTPException(status_code=500, detail=f"Model training error: {str(e)}")
return {
"arima_order": [p, d, q],
"rmse": rmse,
"forecast": future_forecast
}

View File

@ -0,0 +1,58 @@
from statsmodels.tsa.arima.model import ARIMA
from pmdarima import auto_arima
from statsmodels.tsa.stattools import adfuller
import pandas as pd
def forecast_arima_per_product(group: pd.DataFrame, freq: str, mode: str, arima_order: list[int], horizon: int):
group = group.set_index('date')
df_resampled = group.resample(freq).sum().dropna()
series = df_resampled['sold(qty)']
if adfuller(series)[1] > 0.05:
series = series.diff().dropna()
try:
if mode == "auto":
model = auto_arima(
series,
start_p=0, start_q=0,
max_p=5, max_q=5,
d=None,
seasonal=False,
stepwise=True,
suppress_warnings=True,
error_action="ignore"
)
forecast = model.predict(n_periods=horizon)
return {
"forecast": forecast.tolist(),
"model_params": model.order
}
elif mode == "optimal":
model_order = (2, 1, 2)
model = ARIMA(series, order=model_order)
model_fit = model.fit()
forecast = model_fit.forecast(steps=horizon)
return {
"forecast": forecast.tolist(),
"model_params": model_order
}
elif mode == "custom":
if len(arima_order) != 3:
raise ValueError("Parameter ARIMA harus 3 angka: p,d,q.")
model = ARIMA(series, order=tuple(arima_order))
model_fit = model.fit()
forecast = model_fit.forecast(steps=horizon)
return {
"forecast": forecast.tolist(),
"model_params": arima_order
}
else:
raise ValueError("Mode prediksi tidak valid.")
except Exception as e:
raise RuntimeError(f"Model ARIMA gagal dibentuk: {str(e)}")