Full Stack Machine Learning on Azure | by Doug Foo | Towards Data Science

Guide to building a Full Stack Machine Learning site on Azure using MERN (Mongo, Express, React (Node.js))
Having gotten rusty on the latest tech, I took it upon myself to concurrently learn a whole bunch of things at once and build http://diamonds.foostack.ai which is now live. (Basic machine learning app to price diamonds).
My idea came after my gf asked me to buy her a diamond ring... I told the gf “NO wait !!! I have to build a pricing app/site first... (try that at home)”
Outline of steps and chapters in this blog:
Sourcing data using PythonTraining the ML model in Jupyter Notebooks w/ ScikitLearnDeploying ML models to AzureBuilding Front End on React w/ ExpressBuilding backend Express, MongoDBAzure Web App Deployment & CI/CDSummary, Tips and Tricks
Sourcing data using Python
Training the ML model in Jupyter Notebooks w/ ScikitLearn
Deploying ML models to Azure
Building Front End on React w/ Express
Building backend Express, MongoDB
Azure Web App Deployment & CI/CD
Summary, Tips and Tricks
Looking back at my github check-ins, the project took me 1 month (26 days) — probably spending ~8–10hrs a week, so probably ~40hrs+. The end product isn’t too shabby:
I searched the web for good diamond data. I found:
Kaggle’s diamond dataset has 50k rows but it seemed to be lacking real world pricing accuracy when cross comparing to other datasets I found.
Other “experimental datasets” were old or similarly not in synch w/ current prices.
A few commercial vendors sell updated diamond inventory/price data but I wasn’t willing pay.
Retail websites had data, but it’d require scraping (bluenile.com, rarecarat.com, etc)
Solution: Reinforced by breaking news that scraping is not illegal — I am scraping BLUENILE.COM — (thanks Andrew Marder for the starter code).
Andrews code mostly worked, except:
It was written in Python 2.7 so had a few issues
Data format on website changed since he wrote it
Bluenile evolved and as of now blocks leechers running such scripts
My only hack/contrib was to add retry loops when they kept shutting me off:
# (bluenile API restricts to 1,000 page chunks at a time)home = requests.get('http://www.bluenile.com/')while True:   url = 'http://www.bluenile.com/api/public/diamond-search-grid/v2'   try:      response = requests.get(url, params, cookies=home.cookies)   except:  # request api exception, usually timeout      time.sleep(60*30)      next   if (response.ok == False):  # server may have disconnected us      time.sleep(60*30)      next   else:   # all going well      time.sleep(60*4)      next
The original script did inline cleanup —I opted to cleanup downstream (often a bad idea).
Output example 1 row (note the $ comma quotes etc needing cleanup):
carat,clarity,color,culet,cut,date,dateSet,depth,detailsPageUrl,fluorescence,hasVisualization,id,imageUrl,lxwRatio,measurements,polish,price,pricePerCarat,sellingIndex,shapeCode,shapeName,skus,symmetry,table,v360BaseUrl,visualizationImageUrl,willArriveForHoliday['0.23'],['FL'],['D'],['None'],"[{'label': 'Ideal', 'labelSmall': 'Ideal'}]",['Sep 30'],['Oct 1'],['58.8'],./diamond-details/LD12750672,['None'],[True],LD12750672,,['1.01'],"[{'label': '4.04 x 4.00 x 2.36 mm', 'labelSmall': '4.04 x 4.00 x 2.36'}]",['Excellent'],871,"['$3,787']",0.7348354,['RD'],['Round'],['LD12750672'],['Excellent'],['60.0'],https://bluenile.v360.in/50/imaged/gia-7313767639/1/,https://bnsec.bluenile.com/bnsecure/diamondvis/gia-7313767639/still_230x230.jpg,False
In the end, my heist was successful 140,000 round diamonds! I saved these into a CSV file for the cleanup and model training.
Jupyter Notebooks are great (see my full notebook here):
Easy way to annotate my thinking as I test out different ideas — helps when revising later or passing it to someone else!
Quicker to setup and run than a VS Code proj — the ability to isolate cells and run independently is like modified REPL!
Visualization is far easier to work with since you can re-run the cell doing just the plots rather than re-run an entire program!
(Some of above could be done w/ a debugger and REPL shell, but seems cleaner & easier to reproduce in notebooks)
For data cleanup, some options:
Scrub via a python script: CSV -> scrub.py -> CSVScrub within a notebook: CSV -> notebook -> pandas/python -> CSVScrub inside a database: CSV-> MongoDB -> sqlscrubbing -> CSVUse some other tool like Alteryx, Trifacta, or Informatica
Scrub via a python script: CSV -> scrub.py -> CSV
Scrub within a notebook: CSV -> notebook -> pandas/python -> CSV
Scrub inside a database: CSV-> MongoDB -> sqlscrubbing -> CSV
Use some other tool like Alteryx, Trifacta, or Informatica
Option #1 would work, but why have another script/env to deal with. #2 seemed like the right approach since we are using Python and Notebooks for ML after. #3 sounds like we are deferring the inevitable and rather not put junk into a DB. #4 not a real option for poor people like me.
Scrubbing Using Pandas & DataFrames
Pandas is amazing stuff. Loading up my CSV is easy and describe() on a dataframe shows some basic stats:
import pandas as pddiamonds5 = pd.read_csv('./blue-nile-download-round-full.csv')diamonds5.describe()
As mentioned earlier the data has some problems this is my checklist of data related pre-training tasks:
Determine what columns to use in prod modelConvert text to clean text (ie, remove the [])Convert numerics properly (remove $ and , and remove [] and ‘’)Handle null or out of range values in key fieldsTrim down outliers (use carats ≤ 4.5 since it is sparse after that)
Determine what columns to use in prod model
Convert text to clean text (ie, remove the [])
Convert numerics properly (remove $ and , and remove [] and ‘’)
Handle null or out of range values in key fields
Trim down outliers (use carats ≤ 4.5 since it is sparse after that)
Once that is done, we do a little visual graph inspection for carats & cut (color) to price — it's so easy w/ Jupyter:
fig, ax = plt.subplots()for c, df in diamonds5a.groupby('cut'):    ax.scatter(df['carat'], df['price'], label=c, s=np.pi*3)fig.set_size_inches(18.5, 10.5)
While some metrics like the shape and depth are probably valuable for prediction, I really wanted to just focus on the 4-C’s — carats, clarity, color and cut (+sku which is the ID in BlueNile). I can drop the rest.
(note, I find it annoying DataFrame has some mutator functions that change the obj in place, and some return the changed objects. seems bit inconsistent? )
# basic cleanup functionsdef cleanBracketsToF(x):    return float(cleanBracketsToS(x))def cleanBracketsToS(x):    return x.replace("['",'').replace("']",'')def cleanCut(str):    return str[str.index('label') + 9: str.index('labelSmall')-4]df.loc[:,'carat'] =       df.loc[:,'carat'].to_frame().applymap(cleanBracketsToF)...# clear nullspd.set_option('use_inf_as_na', True)df = df.loc[~df['carat'].isnull()]...
After cleaning and including just the columns needed for training we have this (1st column is the Pandas DF identifier which is internal, and skus we drop later for training but include later for display/linking back to bluenile.com):
Next we need to convert non-numerics (Cut, Color, Clarity). You have a few options on encoding:
Onehot encoding (maybe most common, convert categories to binary columns)Ordinal mapping if its ranked/sequential (ie for Color, D->0, E->1, F->2 ...) or flipped other way around (K->0, J->1, ...)
Onehot encoding (maybe most common, convert categories to binary columns)
Ordinal mapping if its ranked/sequential (ie for Color, D->0, E->1, F->2 ...) or flipped other way around (K->0, J->1, ...)
I went with Onehot using sklearn’s Pandas.get_dummies() call
cut = pd.get_dummies( diamonds5b['cut'], prefix='cut_')# result: cut__Astor Ideal', 'cut__Good', 'cut__Ideal','cut__Very Good'# do the same for color, clarity...
OneHot Encoded our data now looks like this:
One tricky thing I learned is if you encode all columns OneHot you can create a linear algebra problem of having col-linearity & being “singular” which short story screws up the model. Read this article on the Dummy Trap.
This is the net result of applying one-hot to all columns and measuring the coefficients (think of it as weights of each feature):
Exact weights below from reg._coeff
coefficients: [ 1122.35088004  -957.70046613   471.56252273  3604.24362627  3443.65378177  3128.96414383  2340.71261083  1717.39519952  1673.9767721    590.1060328  10566.54997464  1383.32878002 -1131.5953868  -1340.53213295  -457.74564551  -848.77484115   540.46973926 15014.89306894]intercept: -8389.974398175218
What this means is the linear prediction model is heavily based on carat (makes sense), and flawless diamonds. The equation results in something like:
y = -8389 + 1122*(cut == Astor ? 1: 0) + ..... + carat*15014)....
Ie if we are pricing a 1.0 carat, cut Ideal, color G, clarity VVS1, the calc is:
y = -8389 + 1*15014 (carats)+ 1*471 (cut)+ 1*2340 (color)+ 1*540 (clarity) = $9,976 
To train a model in SKLearn — most of the code is like this. Caveat — you can spend a lot of time debugging wrong shapes & orientation of your arrays & matrices!
regr = linear_model.LinearRegression()regr.fit(X_train, y_train)               # trainingy_pred = regr.predict(X_test)            # inferencer2score = regr.score(X_test, y_test)     # get r2 score
R2 score is worth mentioning — it is not an “accuracy” score so don’t expect to get 99%. It is a measure of accuracy/variance, read more here @ R2 Wiki.
I tested other SKLearn models: ridge regression, lasso regression, isotonic regression (single feature on carat), elastic regular regression. Newer AutoML frameworks (H2O, DataRobot, Azure AutoML, SageMaker, etc) test all models & hyperparams simultaneously .. putting a lot of amateur DS’s out of work..
Next I apply tree and ensemble methods again using the same dataset. The best performance (R2) after trying various hyperparam combos is no surprise XGBoost:
est = GradientBoostingRegressor(n_estimators=ne, learning_rate=lr,    max_depth=md, random_state=8, loss='ls').fit(X_train, y_train)score = est.score(X_test, y_test)
See the feature weights — carat is the highest, with others having modest impact (interestingly Flawless clarity not as impactful as it was for linear regression).
Coefficients may not be optimal to determine “importance” of each feature. Shapley values appear to be more industry standard.
Once trained in notebooks, we export all the trained models for later deployment using SKLearn’s joblib library (which “pickles” or serializes files into a proprietary format (read about some bad aspects to pickling)).
joblib.dump(value=regr_full, filename='regr_model.pkl')joblib.dump(value=model, filename='xgb_model.pkl')joblib.dump(value=rfr, filename='randomforest_model.pkl')joblib.dump(value=iso_reg, filename='isoridgelineartreg_model.pkl')
Taking one step back — we have a few ways to run models in production:
Embed a pre-trained model into the backend. In our case, we are using Express (Javascript) which isn’t the best for embedding..Publish the pre-trained model somewhere (like a Python Flask container) and expose via a webservice.Use an API like Azure ML to publish and deploy into their containers.Use a framework like Azure ML Studio to design, train and publish (semi-automatically).
Embed a pre-trained model into the backend. In our case, we are using Express (Javascript) which isn’t the best for embedding..
Publish the pre-trained model somewhere (like a Python Flask container) and expose via a webservice.
Use an API like Azure ML to publish and deploy into their containers.
Use a framework like Azure ML Studio to design, train and publish (semi-automatically).
We’ll be doing #2, #3 and #4
The first step is to get set up on Azure — if this is new to you a few basic steps:
Create an account on portal.azure.comYou can get a free subscription for some period, but eventually you may need to create a paid one (with which be careful $$$).
Create an account on portal.azure.com
You can get a free subscription for some period, but eventually you may need to create a paid one (with which be careful $$$).
We can use the new Azure ML API (though you should take a look at the ML GUI Workspace — you can launch Jupyter notebooks, try their menu driven AutoML, the ML Studio Visual GUI (pic below), and other things which are constantly changing right now...
There are a few steps to setting up your services on Azure using the API
Register your models (your pickle [.pkl] files)Setup an env file & scoring callback file (.py interface)Setup the WebService (AciWebService), Model, InferenceConfig, and deploy.
Register your models (your pickle [.pkl] files)
Setup an env file & scoring callback file (.py interface)
Setup the WebService (AciWebService), Model, InferenceConfig, and deploy.
Registering is really easy, do it once per model (pkl file):
ws = Workspace.get(name='DiamondMLWS',    subscription_id='****',resource_group='diamond-ml')new_model = Model.register(model_path="mymodel.pkl"model_name=name,    description="mymodel descr",workspace=ws)
Env file can be generated w/ a script like this (one time):
myenv = CondaDependencies.create(conda_packages=['numpy',   'scikit-learn'])with open("myenv.yml","w") as f:   f.write(myenv.serialize_to_string())
Scoring file looks like this implementing init() and run() for each model:
#score_mymodel.pydef init():   global model   model_path = os.path.join(os.getenv('AZUREML_MODEL_DIR'),        'mymodel.pkl')   model = joblib.load(model_path)def run(raw_data):   data = np.array(json.loads(raw_data)['data'])   y_hat = model.predict(data)   return y_hat.tolist()
The final step involves setting up the Service, Model, and deploying once per model:
aciconfig = AciWebservice.deploy_configuration(cpu_cores=1,   memory_gb=1,tags={"data": "diamonds","method": "sklearn"},   description='Predict Diamonds with sklearn')ws = Workspace.get(name='DiamondMLWS', subscription_id='****',   resource_group='diamond-ml')model = Model(ws, name)inference_config = InferenceConfig(runtime= "python",   entry_script="score_mymodel.py", conda_file="myenv.yml")service = Model.deploy(workspace=ws,   name=svcname, models=[model], inference_config=inference_config,   deployment_config=aciconfig)service.wait_for_deployment(show_output=True)
That is it! You should see output declaring that the service has been deployed and returns the URI to access, and check on the Azure portal to see it is there.
Running.......................ACI service creation operation finished, operation "Succeeded"http://dds2f-97f5eb5c4163.eastus.azurecontainer.io/score
That is pretty much it. The input format and output is defined by the model specifics that you created in part2. You can test the API with Postman or curl — make sure it is pricing:
POST to azure webservice — price a 1.1carat, F color, unspec cut, VVS2 clarity diamond → $11,272 is the predict!
I won’t go into details of using the GUI driven Azure ML Studio. You drag, drop, train and ultimately publishes a webservice URL (with a different input/output format). For my app I published 3 models in GUI ML Studio and 4 from SKLearn. See output of pricing requests on the web GUI:
Notice the NN model is totally whacked... not sure if I will bother fixing it...
Due to Azure ML container cost I opted to move the models to an Azure hosted Flask Python service. It is very minimal code — and cost is a fraction of Azure ML ACI [$2/day per service, so w/ 4 models deployed, it was like $8 a day.. quickly bankrupting old Doug Foo]. It was super easy to re-deploy the .pkl in Flask:
app = Flask(__name__)models = {}models['XGB2'] = joblib.load('models/sklearn_diamond_xgb_model.pkl')models['ISO'] = joblib.load('models/sklearn_diamond_iso_model.pkl')models['LR3'] = joblib.load('models/sklearn_diamond_regr_model.pkl')print('loaded models', models)@app.route('/models/<model>', methods=['POST'])def predict(model):   if (models.get(model) is None):      print('model not found: ',model)      return jsonify("[-1]")   j_data = np.array(request.get_json()['data'])   y_hat = np.array2string(models[model].predict(j_data))   print('input: ',j_data, ', results:', y_hat)   return y_hatif __name__ == '__main__':   app.run(debug=True)
Again, there are countless stories on starting a React project & Express.js project, so I’ll skip some steps..
install express into my project folder ~/diamonds
install react, create the starter project with create-react-app in a subdir called ~/diamonds/diamond-app
for prod deployment these are merged via webpack into a single express bundle
I’m pretty familiar w/ Web dev on old stuff like Perl-CGI, Php, JSP, ASP and all those “Web 2.0” frameworks including old school AJAX. I’ve also used Django and looked closely at Angular. React is different — there is a learning curve. After a bit of struggle — I’m now a big fan! Key things to keep in mind:
Components — Coding is in JSX which looks like Javascript but is slightly different. You code components (<Tags/>) by way of a JSX Class or Function. The top level component is usually App.js is where you add your custom JSX and other components like <MyTag/>.
class MyTag extends React.Component {    const constvar = "Text"    render() { return (<div>Hi {constvar}</div>) }}
State — dynamic rendering flow is very different than what you may be used to. React has its own lifecycle mostly controlled by “state” (special variable) changes. Declare a set of state variables and React refreshes impacted components like magic. See below, as the value of dynamic changes the GUI component will refresh itself.
class MyTag extends React.Component {    constructor(props) {       super(props);       this.state = { dynamic: 'Dyn txt' }    }    const constvar = "Const Txt";    render() {        return (<div>Hi {constvar} and {this.state.dynamic}  </div>    )}}// equiv as a function, bit tighter ?  also use MyTagfunction MyTag2() {      const constvar = "Const Txt";    const [dynamic, setDynamic] = React.useState('Dyn Txt');    return (<div>Hi {constvar} and {dynamic} and <MyTag/> </div>);}
Props — props are tag attributes and children of your component/tag — a way to pass data. You can pass data down via props, but calls up need a callback function (eg myCallbackFunc defined in the caller/parent).
function MyTag2(props) {    const constvar = "Const Txt";  const [dynamic, setDynamic] = React.useState('Dyn Txt');  return (     <div>        Hi {constvar} and {dynamic}        Hi {props.var1}        <Button onClick={props.callback} />     </div>);  }}function App() {  function myCallbackFunc() {     // do something on button click  }  return (     <div>         <MyTag2 var1=’var' callback={myCallbackFunc}>Text</MyTag2>      </div>);  }}
Above example — your app root is <App/>, inside it, we call MyTag w/ “props” var1 and callback and the internal “Text”.
It takes some fiddling to learn but it is actually very elegant — try it out!
Kickstarting the UI with Material-Design
Google’s Material-UI is a component lib/framework for React that gives you great widgets & default themes so that anyone can make a site look good! I looked thru the gallery and picked the Dashboard (on the left) to start.
Material-UI has components like a <Grid> layout and <Paper> container to manage reactive/auto-resizing components. A nice side-menu <Drawer> is included.
Main things I did on top of the default Dashboard:
Set left drawer to be closed by default, added <Modal> help pop-ups for links to external sites.
Updated top bar w/ new text and replaced notification bell w/ Help
Implemented 3 main “paper” containers in the Grid: Random Diamond, Inventory Chooser/Filter, and the ML Pricer.
This is the easiest one to build. Assuming a server REST endpoint exists at /diamonds/daily/ which returns a json (data.price, data.carat, etc..) — we just fetch on component load (useEffect()) and set the state variables from results.
export default function Daily() {  const [diamond, setDiamond] = useState({    price:0.0,carat:0.0,color:'',cut:'',clarity: '',skus: ''  });  useEffect(() => {    axios.get('/diamonds/daily/').then(response => {      setDiamond({         price: response.data.price,          carat: response.data.carat,          ...      });    })},  []);    return (    <React.Fragment><Title>Random Diamond Featured</Title>         <Typography>Price: {diamond.price}</Typography>      <Typography>Carats: {diamond.carat}</Typography>      ...    </React.Fragment>  );}
I wanted a basic search form to let me filter thru the 140k diamonds — graph + details in a table.
I choose Victory for my graphing/plotting since it works w/ ReactNative (also in consideration was React-Viz and Re-Chart). I didn’t like the material-ui data table (lack of higher level abstraction) so went with the smaller open-source Material-Table project. Lots of components you can pick from out there in React-land!
Main steps in creating this component:
Using Material-UI Form components, added checkboxes into form groups and implementing a Slider for carats.Implement the onClick of the SearchButtonMock or build your backend to return a JSON of diamonds in a list which we render in a graph + table (via tabs) — more in Part 5
Using Material-UI Form components, added checkboxes into form groups and implementing a Slider for carats.
Implement the onClick of the SearchButton
Mock or build your backend to return a JSON of diamonds in a list which we render in a graph + table (via tabs) — more in Part 5
Refer to github for full source. Below shows how the options are passed to the server:
class Chooser extends Component {  constructor(props) {  super(props);  this.state = {    D: true,    E: true,    F: true,    G: true,    H: true,    AstorIdeal: false,    Ideal: true,    diamonds: [],    ...  }  handleSubmit = event => {    // call REST api, setState of return diamonds[] etc  }  render() {    return (      <React.Fragment >         <Title>Filter Diamonds</Title>         <FormControl component="fieldset" >           ... checkboxes and button          </FormControl>         <Paper square className={classes.root}>           <Chart diamonds={this.state.diamonds}/>           <DiamondsTable diamonds={this.state.diamonds} />         </Paper>      </React.Fragment>   )}
DiamondsTable is a 3rd party table component to view a grid of data. Just pass the diamonds[] as a prop. Any change in the diamonds[] state and it forces re-rendering on its own. React refresh magic !
We are close to the goal — to predict diamond pricing based on the 140k diamonds in our training set.
The control is really simple — much like the Filter/Chooser, we have a basic form and button.
Code structure is very similar:
FormGroup and Button with a submitPrice callbacksubmitPrice calls a REST service passing the inputs {carat, clarity, cut, color} and returning a list of diamonds[] which are set as state variables.diamonds[] are passed to the charts and tables as props.The initial and any subsequent change in diamonds[] state triggers re-rendering of sub-components using diamonds[].
FormGroup and Button with a submitPrice callback
submitPrice calls a REST service passing the inputs {carat, clarity, cut, color} and returning a list of diamonds[] which are set as state variables.
diamonds[] are passed to the charts and tables as props.
The initial and any subsequent change in diamonds[] state triggers re-rendering of sub-components using diamonds[].
Filter from prior step (Chooser.js) and Pricer.js are very similar. Pricer is implemented as a Function instead of a Class hence syntax has slight differences. Functions with state are new “Hooks” added to React and makes for a nice higher substitute to full classes.
Express is the JavaScript Node.js webserver. I’d still prefer backends in Python, but I guess this tutorial is MERN so I must use/learn Express (which isn’t too hard). A key advantage is re-use when you have the same front-end and back-end libraries.
Again I won’t go into basics of install. I’ll jump to the server.js script that hosts my API logic. The main REST calls I need:
GET /diamonds/daily — random diamond of the day
POST /diamonds/q/ — to fetch a list w/ some filtering criteria
POST /diamonds/q2/ — to fetch a list w/ matching the pricing criteria
POST /diamonds/price — to price diamonds using my ML models
Setup a Mongoose model for Express, create a DB and bulk load the DB.
Setup Express endpointsMongoDB free on MLab — FYI Azure has CosmoDB which is (mostly) wire compliant/compatible to MongoDB. CosmoDB cheapest plan is $24/month!
Setup Express endpoints
MongoDB free on MLab — FYI Azure has CosmoDB which is (mostly) wire compliant/compatible to MongoDB. CosmoDB cheapest plan is $24/month!
Once you create a DB, you need to upload data or hand enter them. The easiest way for me was to use a bulk loader script — requires you to install MongoDB client on your PC and point to the MLAB connect string.
mongoimport --uri mongodb://user:pass@host.mlab.com:12122/diamonds --collection myc --type csv --headerline  --file jsonfile.csv
Uploading 150k diamond entries was fast, a few minutes and does use close to the 0.5gb limit. All done !
2. Mongoose Model — create a script that defines a schema that matches your MongoDB collection’s JSON format:
#diamond.model.jslet Diamond = new Schema({  carat: {     type: Number,     default: -1  },  color: {     type: String,     default: 'Default'  },  ...}, { collection: 'myc' });module.exports = mongoose.model('Diamond', Diamond);
3. Express.js REST Endpoints (server.js) are the next step.
There, most of it is boilerplate. Open a connection to be shared, set any alternate mappings (react path in this case), and listen to a port. In between, we create routes for various GET/POST handlers (defined in next section).
app.use(cors());app.use(bodyParser.json());mongoose.connect('mongodb://user:pass@host:port/diamonds',    { useNewUrlParser: true, useUnifiedTopology: true  });const connection = mongoose.connection;connection.once('open', function() {  console.log("MongoDB connection established successfully");})//-- fill w/ diamondRoutes start... see below//-- fill endapp.use('/diamonds', diamondRoutes);// for webpack'd react front end app.use(express.static("./diamond-app/build")); app.get("*", (req, res) => {   res.sendFile(path.resolve(__dirname, "./diamond-app",        "build", "index.html"));});app.listen(PORT, function() {   console.log("Server is running on Port..: " + PORT);});
Next we fill in the diamondRoutes — /daily /q /q2 and /price. ‘/daily’ is easy — just a hack randomized query that returns 1 diamond (using json schema defined earlier in diamond.model.js)
diamondRoutes.route('/daily').get(function(req, res) {   let random = Math.floor(Math.random() * 140000);  // skip n   Diamond.findOne().skip(random).exec(function(err, diamonds) {      res.json(diamonds);   });});
The /q and /q2 are all similar query searches that take in POST params and map to a Mongoose query — the POST input is a JSON like{carat:3, color:’D’,cut:’’,clarity:’’}
diamondRoutes.route('/q2').post(function(req, res) {   const qobj = req.body;   query = Diamond.find();    query = query.where('carat').       gt(qobj.carat-0.05).lt(qobj.carat+0.05);      if (qobj.cut !== '') query = query.where('cut').equals(qobj.cut);   if (qobj.color !== '') query =      query.where('color').equals(qobj.color);   if (qobj.clarity !== '') query =      query.where('clarity').equals(qobj.clarity);     query.exec(function(err, diamonds) {         res.json(diamonds);   });});
Calling /price — the Machine Learning Inference call is done via another REST API callout to Azure ML Studio’s farm (now most to an Azure Flask server).
The request chain: React Client -> Express.js -> ML Services-> back to Express.js -> back to React Client.
I think showing the code in chunks will be confusing, so just look at the key step which is to use an ‘asynch’ call to the callSvc function, then ‘await’ them all to join the results:
let r0 = await callSvc(p2.XGB.url, p2.XGB.token, reqJson);let r1 = await callSvc(p2.LR.url, p2.LR.token, reqJson);let r2 = ...
callSvc just does an axios http POST to get the ML predictions. Notice the ‘async’ def on the function, and the await call above that pairs up the async calls.
async function callSvc(apiurl, token, reqJson) {    return axios.post(apiurl, reqJson,         { headers: {            'Content-Type': 'application/json',            'Authorization': token,            }        })        .catch(function (error){            console.log(error);        });}
The responses are rolled into a single response JSON for the front end, notice ML Studio format differs from my custom SKLearn models:
res.json([   { price:r0.data.Results.output1.value.Values[0][6],model:'XGB' },    { price:r1.data.Results.output1.value.Values[0][6],model:'LR' },   { price:r2.data.Results.output1.value.Values[0][6],model:'NN' },   { price: r3.data[0], model: 'ISO' },   { price: r4.data[0], model: 'XGB2' },   { price: r5.data[0], model: 'LR3' },   { price: r6.data[0], model: 'RF' },]);
See how the chain looks via POSTMAN (calling express which calls ML services aggregating the results of all 7 models):
Look at the full source code for the full story. That's mostly it — we’ve now built the express.js backend!
Deploying to an Azure site is really simple (when it works which it usually does) — and frustrating as heck when there is a build or deploy error. Good luck!
First, you should understand in development you may be running 2 servers:
React dev engine (default port 3000)Express dev server (default port 4000)
React dev engine (default port 3000)
Express dev server (default port 4000)
In prod you only run a single server (Express) and the React stuff is “bundled” into a webpack via “npm build” which results i something like this:
Your express.js has code pointing to this build/static folder
app.use(express.static("./diamond-app/build"));app.get("*", (req, res) => {     res.sendFile(path.resolve(__dirname, "./diamond-app",                   "build", "index.html"));});
Check this project into github and you are ready to go! (.gitignore the node_modules at both express.js (~diamonds) and react (~diamonds/diamond-app) levels).
Login into Azure Portal and Create the Web App
“testwebapp23” will become http://testwebapp23.azurewebsites.net so name it carefully, it can’t be changed later (you can externally DNS alias it if you own a domain). Pick the cheapest size, B1 will cost you about $15 a month!
I would check the Config panel in Azure Web App, a few things to confirm:
Next, you need to create a project in Azure DevOps (https://dev.azure.com) to host the build env. Afterward, go to the Project Settings (bottom-left) and link it to your GitHub repo, and enable Pipelines:
Pipelines will enable your CI/CD setup so check-ins to GitHub trigger a build and deployment. There are several ways to set this up and each has its pitfalls. The weird but semi-simple way is to jump back to Azure Portal and finish with their webapp->Deployment Center.
You have the choice of Kudu App Build or Azure Pipelines. Pipelines is the latest Azure CI/CD toolset (replaces TFS). Both are buggy.. but I found for node.js Pipeline deployment works better (and for Python I prefer Kudu).
Finish and this creates your first build and deployment. If you are lucky it will work and you can view your app online like this:
Jump back to Azure DevOps and you’ll see it generated a Pipeline (build) and Release (deployment):
If that passes, it goes onto Release (Deployment):
You can customize each step and add some nifty stuff like pre/post scripts or an approval flow like below to propagate changes thru your environments. A change triggers a pre-prod (Demo) build then I have to manually approve to release to PROD (diamonds.foostack.ai) once I verify the build.
My top list of things I learned and want to warn you about:
Azure Insights is great to track usage statistics (more difficult to setup than you’d think, you have to hook in code to each of your components but then you get some great telemetry)
Azure is evolving (and breaking) so be prepared to have some annoyances esp with newer things like Machine Learning and Pipelines (node.js releases on linux broke suddenly.. for weeks now the Azure team is trying to clean up the debacle):
Scrum Boards and ticket management can be done in Azure DevOps and integrates with GitHub. It is very new but not bad for a small project.
Cost Management — track it closely. The cost per Machine Learning model is a bit crazy ($2 per model container / day cost me $100+ for just 12 days). This motivated me to migrate to Flask. On another project SQL Warehouse for like 2 days was over $100 ! Be careful, set cost alarms!
Python Flask Deploy — I added this late because of the cost issue above. I used the traditional Kudu build from WebApp->Deployment rather than Azure Pipelines due to some errors I kept having w/ Azure Pipelines.
Domain name setup is really easy — Azure doesn't do this, go to namecheap.com — then just link on Azure. Very simple instructions.
Happy coding! Peruse the live http://diamonds.foostack.ai site, github project or the Jupyter notebook online.
Some rights reserved