├── .dockerignore ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── main.py ├── model.py └── requirements.txt /.dockerignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | env 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | env 3 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.11 2 | 3 | WORKDIR /app 4 | 5 | RUN apt-get -y update && apt-get install -y \ 6 | python3-dev \ 7 | apt-utils \ 8 | python-dev \ 9 | build-essential \ 10 | && rm -rf /var/lib/apt/lists/* 11 | 12 | RUN pip install --upgrade setuptools 13 | RUN pip install \ 14 | cython==0.29.35 \ 15 | numpy==1.24.3 \ 16 | pandas==2.0.1 \ 17 | pystan==3.7.0 18 | 19 | COPY requirements.txt . 20 | RUN pip install -r requirements.txt 21 | 22 | COPY . . 23 | 24 | CMD gunicorn -w 3 -k uvicorn.workers.UvicornWorker main:app --bind 0.0.0.0:$PORT 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 TestDriven.io 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Deploying and Hosting a Machine Learning Model with FastAPI and Heroku 2 | 3 | ## Want to learn how to build this? 4 | 5 | Check out the [tutorial](https://testdriven.io/blog/fastapi-machine-learning). 6 | 7 | ## Want to use this project? 8 | 9 | ### With Docker 10 | 11 | 1. Build and tag the Docker image: 12 | 13 | ```sh 14 | $ docker build -t fastapi-prophet . 15 | ``` 16 | 17 | 1. Spin up the container: 18 | 19 | ```sh 20 | $ docker run --name fastapi-ml -e PORT=8008 -p 8008:8008 -d fastapi-prophet:latest 21 | ``` 22 | 23 | 1. Train the model: 24 | 25 | ```sh 26 | $ docker exec -it fastapi-ml python 27 | 28 | >>> from model import train, predict, convert 29 | >>> train() 30 | ``` 31 | 32 | 1. Test: 33 | 34 | ```sh 35 | $ curl \ 36 | --header "Content-Type: application/json" \ 37 | --request POST \ 38 | --data '{"ticker":"MSFT"}' \ 39 | http://localhost:8008/predict 40 | ``` 41 | 42 | ### Without Docker 43 | 44 | 1. Create and activate a virtual environment: 45 | 46 | ```sh 47 | $ python3 -m venv venv && source venv/bin/activate 48 | ``` 49 | 50 | 1. Install the requirements: 51 | 52 | ```sh 53 | (venv)$ pip install -r requirements.txt 54 | ``` 55 | 56 | 1. Train the model: 57 | 58 | ```sh 59 | (venv)$ python 60 | 61 | >>> from model import train, predict, convert 62 | >>> train() 63 | ``` 64 | 65 | 1. Run the app: 66 | 67 | ```sh 68 | (venv)$ uvicorn main:app --reload --workers 1 --host 0.0.0.0 --port 8008 69 | ``` 70 | 71 | 1. Test: 72 | 73 | ```sh 74 | $ curl \ 75 | --header "Content-Type: application/json" \ 76 | --request POST \ 77 | --data '{"ticker":"MSFT"}' \ 78 | http://localhost:8008/predict 79 | ``` 80 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI, HTTPException 2 | from pydantic import BaseModel 3 | 4 | from model import convert, predict 5 | 6 | app = FastAPI() 7 | 8 | 9 | # pydantic models 10 | 11 | 12 | class StockIn(BaseModel): 13 | ticker: str 14 | 15 | 16 | class StockOut(StockIn): 17 | forecast: dict 18 | 19 | 20 | # routes 21 | 22 | 23 | @app.get("/ping") 24 | async def pong(): 25 | return {"ping": "pong!"} 26 | 27 | 28 | @app.post("/predict", response_model=StockOut, status_code=200) 29 | def get_prediction(payload: StockIn): 30 | ticker = payload.ticker 31 | 32 | prediction_list = predict(ticker) 33 | 34 | if not prediction_list: 35 | raise HTTPException(status_code=400, detail="Model not found.") 36 | 37 | response_object = {"ticker": ticker, "forecast": convert(prediction_list)} 38 | return response_object 39 | -------------------------------------------------------------------------------- /model.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from pathlib import Path 3 | 4 | import joblib 5 | import pandas as pd 6 | import yfinance as yf 7 | from prophet import Prophet 8 | 9 | BASE_DIR = Path(__file__).resolve(strict=True).parent 10 | TODAY = datetime.date.today() 11 | 12 | 13 | def train(ticker="MSFT"): 14 | # data = yf.download("^GSPC", "2008-01-01", TODAY.strftime("%Y-%m-%d")) 15 | data = yf.download(ticker, "2020-01-01", TODAY.strftime("%Y-%m-%d")) 16 | data.head() 17 | data["Adj Close"].plot(title=f"{ticker} Stock Adjusted Closing Price") 18 | 19 | df_forecast = data.copy() 20 | df_forecast.reset_index(inplace=True) 21 | df_forecast["ds"] = df_forecast["Date"] 22 | df_forecast["y"] = df_forecast["Adj Close"] 23 | df_forecast = df_forecast[["ds", "y"]] 24 | df_forecast 25 | 26 | model = Prophet() 27 | model.fit(df_forecast) 28 | 29 | joblib.dump(model, Path(BASE_DIR).joinpath(f"{ticker}.joblib")) 30 | 31 | 32 | def predict(ticker="MSFT", days=7): 33 | model_file = Path(BASE_DIR).joinpath(f"{ticker}.joblib") 34 | if not model_file.exists(): 35 | return False 36 | 37 | model = joblib.load(model_file) 38 | 39 | future = TODAY + datetime.timedelta(days=days) 40 | 41 | dates = pd.date_range(start="2020-01-01", end=future.strftime("%m/%d/%Y"),) 42 | df = pd.DataFrame({"ds": dates}) 43 | 44 | forecast = model.predict(df) 45 | 46 | # model.plot(forecast).savefig(f"{ticker}_plot.png") 47 | # model.plot_components(forecast).savefig(f"{ticker}_plot_components.png") 48 | 49 | return forecast.tail(days).to_dict("records") 50 | 51 | 52 | def convert(prediction_list): 53 | output = {} 54 | for data in prediction_list: 55 | date = data["ds"].strftime("%m/%d/%Y") 56 | output[date] = data["trend"] 57 | return output 58 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # pystan must be installed before prophet 2 | # you may need to pip install it on it's own 3 | # before installing the remaining requirements 4 | # pip install pystan==3.7.0 5 | 6 | pystan==3.7.0 7 | 8 | fastapi==0.95.2 9 | gunicorn==20.1.0 10 | uvicorn==0.22.0 11 | 12 | prophet==1.1.3 13 | joblib==1.2.0 14 | pandas==2.0.1 15 | plotly==5.14.1 16 | yfinance==0.2.18 17 | --------------------------------------------------------------------------------