# The Yhat Blog

machine learning, data science, engineering

# Currency Portfolio Optimization Using ScienceOps

#### by Ryan J. O'Neil | January 5, 2015

Portfolio optimization is a problem faced by anyone trying to invest money (or any kind of capital, such as time) in a known group of investments. Its most obvious, and common, application is investing in the stock market. Typically, portfolio managers have two competing goals:

1. Maximize return
2. Minimize risk

Maximizing return means selecting a group of investments that collectively result in the highest expected yield. Minimizing risk means selecting investments that are most likely to actually result in the yields we expect.

Anyone with a little exposure to markets knows that these goals are often diametrically opposed. High yielding investments also tend to have high variances in their return. Thus the problem we face in portfolio optimization is achieving a desired balance between these two goals.

## Exchange rate data

In this post we'll take the view of someone (e.g. a bank or a mutual fund manager) with a lot of cash to invest in foreign currencies. We want to buy foreign currencies such that we maximize our return with respect to the US Dollar (USD) over the next month. We won't worry about the actual amounts invested. We simply want to maximize the percent return given that we convert 100% of some known amount into foreign currencies.

We start by downloading the last 100 months of exchange rate data from the Federal Reserve's FRB H10: Data Download Program. Alternatively, you can simply clone the GitHub repository for this post, which contains exchange rate data as of this writing.

Inspecting the data, we find something a little strange. The first 6 rows are row oriented data and the remaining rows are column oriented. The former contain metadata about the latter, so we read these into two different data frames using pandas. Also, we note that there are data with NA currencies which don't particularly interest us. So we ignore these.

import csv
import pandas

raw_data = [row for row in csv.reader(open('2014-12-28-fed-monthly-currency-data.csv'))]

head_data = [[] for _ in range(len(raw_data))]
for row in raw_data[:6]:
for j, val in enumerate(row):

# Filter out NA currencies


metadata now resembles the following DataFrame:

Series Description Unit: Multiplier: Currency: Unique Identifier: Time Period
3 AUSTRALIA -- SPOT EXCHANGE RATE, US$/AUSTRALIA... Currency:_Per_AUD 1 USD H10/H10/RXI$US_N.M.AL RXI$US_N.M.AL 4 SPOT EXCHANGE RATE - EURO AREA Currency:_Per_EUR 1 USD H10/H10/RXI$US_N.M.EU RXI$US_N.M.EU 5 NEW ZEALAND -- SPOT EXCHANGE RATE, US$/NZ$(RE... Currency:_Per_NZD 1 USD H10/H10/RXI$US_N.M.NZ RXI$US_N.M.NZ 6 United Kingdom -- Spot Exchange Rate, US$/Poun... Currency:_Per_GBP 0.01 USD H10/H10/RXI$US_N.M.UK RXI$US_N.M.UK
7 BRAZIL -- SPOT EXCHANGE RATE, REAIS/US$Currency:_Per_USD 1 BRL H10/H10/RXI_N.M.BZ RXI_N.M.BZ 8 CANADA -- SPOT EXCHANGE RATE, CANADIAN$/US$Currency:_Per_USD 1 CAD H10/H10/RXI_N.M.CA RXI_N.M.CA 9 CHINA -- SPOT EXCHANGE RATE, YUAN/US$ Currency:_Per_USD 1 CNY H10/H10/RXI_N.M.CH RXI_N.M.CH
10 DENMARK -- SPOT EXCHANGE RATE, KRONER/US$Currency:_Per_USD 1 DKK H10/H10/RXI_N.M.DN RXI_N.M.DN 11 HONG KONG -- SPOT EXCHANGE RATE, HK$/US$Currency:_Per_USD 1 HKD H10/H10/RXI_N.M.HK RXI_N.M.HK 12 INDIA -- SPOT EXCHANGE RATE, RUPEES/US$ Currency:_Per_USD 1 INR H10/H10/RXI_N.M.IN RXI_N.M.IN
13 JAPAN -- SPOT EXCHANGE RATE, YEA/US$Currency:_Per_USD 1 JPY H10/H10/RXI_N.M.JA RXI_N.M.JA 14 KOREA -- SPOT EXCHANGE RATE, WON/US$ Currency:_Per_USD 1 KRW H10/H10/RXI_N.M.KO RXI_N.M.KO
15 Malaysia - Spot Exchange Rate, Ringgit/US$Currency:_Per_USD 1 MYR H10/H10/RXI_N.M.MA RXI_N.M.MA 16 MEXICO -- SPOT EXCHANGE RATE, PESOS/US$ Currency:_Per_USD 1 MXN H10/H10/RXI_N.M.MX RXI_N.M.MX
17 NORWAY -- SPOT EXCHANGE RATE, KRONER/US$Currency:_Per_USD 1 NOK H10/H10/RXI_N.M.NO RXI_N.M.NO 18 SWEDEN -- SPOT EXCHANGE RATE, KRONOR/US$ Currency:_Per_USD 1 SEK H10/H10/RXI_N.M.SD RXI_N.M.SD
19 SOUTH AFRICA -- SPOT EXCHANGE RATE, RAND/US$Currency:_Per_USD 1 ZAR H10/H10/RXI_N.M.SF RXI_N.M.SF 20 Singapore - SPOT EXCHANGE RATE, SINGAPORE$/US$Currency:_Per_USD 1 SGD H10/H10/RXI_N.M.SI RXI_N.M.SI 21 SRI LANKA -- SPOT EXCHANGE RATE, RUPEES/US$ Currency:_Per_USD 1 LKR H10/H10/RXI_N.M.SL RXI_N.M.SL
22 SWITZERLAND -- SPOT EXCHANGE RATE, FRANCS/US$Currency:_Per_USD 1 CHF H10/H10/RXI_N.M.SZ RXI_N.M.SZ 23 TAIWAN -- SPOT EXCHANGE RATE, NT$/US$Currency:_Per_USD 1 TWD H10/H10/RXI_N.M.TA RXI_N.M.TA 24 THAILAND -- SPOT EXCHANGE RATE -- THAILAND Currency:_Per_USD 1 THB H10/H10/RXI_N.M.TH RXI_N.M.TH 25 VENEZUELA -- SPOT EXCHANGE RATE, BOLIVARES/US$ Currency:_Per_USD 1 VEB H10/H10/RXI_N.M.VE RXI_N.M.VE

For convenience, we'll create a list of the country names for the currencies. This will come in handy later when we're trying to output portfolio options.

# This will have the same indices as the rest of the metadata.
countries = []
c = c.upper()
c = c.split(' -- ')
c = c.split(' - ')
countries.append(c)
countries = 'EURO AREA'


countries is a list of human-readable country names:

['AUSTRALIA',
'EURO AREA',
'NEW ZEALAND',
'UNITED KINGDOM',
'BRAZIL',
'CHINA',
'DENMARK',
'HONG KONG',
'INDIA',
'JAPAN',
'KOREA',
'MALAYSIA',
'MEXICO',
'NORWAY',
'SWEDEN',
'SOUTH AFRICA',
'SINGAPORE',
'SRI LANKA',
'SWITZERLAND',
'TAIWAN',
'THAILAND',
'VENEZUELA']


Now we read in the second part of the data which contains exchange rate information. We filter out NA exchange rates while we're at it.

exchange_rates = pandas.DataFrame.from_records(data=raw_data[6:], columns=raw_data)
exchange_rates = exchange_rates[['Time Period'] + list(metadata['Time Period'])]
exchange_rates.tail()

Time Period RXI$US_N.M.AL RXI$US_N.M.EU RXI$US_N.M.NZ RXI$US_N.M.UK RXI_N.M.BZ RXI_N.M.CA RXI_N.M.CH RXI_N.M.DN RXI_N.M.HK ... RXI_N.M.MX RXI_N.M.NO RXI_N.M.SD RXI_N.M.SF RXI_N.M.SI RXI_N.M.SL RXI_N.M.SZ RXI_N.M.TA RXI_N.M.TH RXI_N.M.VE
95 2014-08 0.9309 1.3315 0.8437 1.6700 2.2685 1.0926 6.1541 5.5987 7.7504 ... 13.1436 6.1935 6.8983 10.6632 1.2484 130.1748 0.9098 29.9729 32.0048 6.2842
96 2014-09 0.9042 1.2889 0.8134 1.6290 2.3379 1.1011 6.1382 5.7757 7.7526 ... 13.2370 6.3525 7.1302 10.9908 1.2638 130.2562 0.9370 30.1310 32.1971 6.2842
97 2014-10 0.8781 1.2677 0.7876 1.6074 2.4495 1.1212 6.1251 5.8723 7.7572 ... 13.4795 6.5600 7.2456 11.0594 1.2745 130.5618 0.9528 30.3964 32.4418 6.2842
98 2014-11 0.8644 1.2473 0.7834 1.5771 2.5527 1.1325 6.1249 5.9660 7.7543 ... 13.6148 6.8090 7.4155 11.0901 1.2964 130.9278 0.9642 30.7278 32.7878 6.2842
99 2014-12 0.8302 1.2389 0.7765 1.5678 2.6278 1.1496 6.1781 6.0049 7.7528 ... 14.4434 7.2137 7.5631 11.4591 1.3122 131.0627 0.9704 31.2193 32.8847 6.2842

5 rows × 24 columns

## Computing returns

We now have a data frame that contains exchange rate data for each month in relation to USD. This isn't particularly convenient. We're planning on converting our holdings back to USD anyway, so the units themselves don't really matter. What we care about is the actual percentage return. That is, if we bought $1 of currency X, how much of that would we have gained or lost over a month? In order to compute this, we convert each data point in our exchange_rates data to units of its local currency that equal \$1. Most of the data is already like this. Four are not: Australia, the Eurozone, New Zealand, and the United Kingdom are all reported in USD. We divide 1 by these to obtain their local currency equivalents of \$1. $$\1 = x \rightarrow \frac{1}{x} = \frac{1}{\}$$ Now we compute the percentage return for each month in USD. So if$r_m$is the exchange rate in month$m$, then$\frac{r_m - r_{m+1}}{r_{m+1}}$is the percentage return of that currency during month$m$. For example, say in month$m$we can buy 2 units of some currency for \$1. In month $m+1$, \$1 buys 4 units of that same currency. The return on that currency is$\frac{2 - 4}{4} = -50\%$. # Convert the exchange rate data frame into percentage returns. rows = [] for i in range(len(exchange_rates)-1): row = {} for tp, cur in zip(metadata['Time Period'], metadata['Currency:']): x1 = float(exchange_rates[tp][i]) x2 = float(exchange_rates[tp][i+1]) if cur == 'USD': x1 = 1.0 / x1 x2 = 1.0 / x2 # Returns are in units of %. row[tp] = 100 * (x1 - x2) / x2 rows.append(row) returns = pandas.DataFrame(data=rows, columns=list(metadata['Time Period'])) returns.tail()  RXI$US_N.M.AL RXI$US_N.M.EU RXI$US_N.M.NZ RXI$US_N.M.UK RXI_N.M.BZ RXI_N.M.CA RXI_N.M.CH RXI_N.M.DN RXI_N.M.HK RXI_N.M.IN ... RXI_N.M.MX RXI_N.M.NO RXI_N.M.SD RXI_N.M.SF RXI_N.M.SI RXI_N.M.SL RXI_N.M.SZ RXI_N.M.TA RXI_N.M.TH RXI_N.M.VE 94 -0.852061 -1.610877 -2.877863 -2.144615 -1.952832 -1.711514 0.719845 -1.589655 -0.002581 -1.278382 ... -1.157978 0.092032 -1.151008 -0.051579 -0.456584 0.028807 -1.318971 -0.079405 0.254962 0 95 -2.868192 -3.199399 -3.591324 -2.455090 -2.968476 -0.771955 0.259034 -3.064564 -0.028378 -0.039082 ... -0.705598 -2.502952 -3.252363 -2.980675 -1.218547 -0.062492 -2.902882 -0.524709 -0.597259 0 96 -2.886530 -1.644813 -3.171871 -1.325967 -4.556032 -1.792722 0.213874 -1.645011 -0.059300 -0.764583 ... -1.799028 -3.163110 -1.592691 -0.620287 -0.839545 -0.234065 -1.658270 -0.873130 -0.754274 0 97 -1.560187 -1.609214 -0.533266 -1.885032 -4.042778 -0.997792 0.003265 -1.570567 0.037399 -0.512298 ... -0.993771 -3.656925 -2.291147 -0.276823 -1.689293 -0.279543 -1.182327 -1.078502 -1.055271 0 98 -3.956502 -0.673455 -0.880776 -0.589690 -2.857904 -1.487474 -0.861106 -0.647804 0.019348 -1.211577 ... -5.736876 -5.610158 -1.951581 -3.220148 -1.204085 -0.102928 -0.638912 -1.574347 -0.294666 0 5 rows × 23 columns We downloaded 100 months of exchange rate data for each country. These are now in units of % returns. The means and variances of these give us expected returns and variances for investing in those currencies. Note that the expected returns (means) of the currencies are in units of percentage. That means a value 0.15 means a 0.15% expected return over a month. That translates into 1.8% annually, which is not bad for a currency. # Means are the expected returns for each currency. exp_returns = pandas.concat({'mean': returns.mean(), 'variance': returns.var()}, axis=1)  exp_returns is a DataFrame resembling the following: mean variance RXI$US_N.M.AL 0.152151 10.996718
RXI$US_N.M.EU 0.002683 5.928188 RXI$US_N.M.NZ 0.220217 9.690793
RXI$US_N.M.UK -0.159779 5.098969 RXI_N.M.BZ -0.128507 12.743632 RXI_N.M.CA -0.007877 4.380697 RXI_N.M.CH 0.253968 0.212987 RXI_N.M.DN 0.004799 5.810684 RXI_N.M.HK 0.003935 0.014761 RXI_N.M.IN -0.283975 4.784800 RXI_N.M.JA 0.016618 6.449970 RXI_N.M.KO -0.110786 7.580272 RXI_N.M.MA 0.068651 2.424957 RXI_N.M.MX -0.235747 7.786813 RXI_N.M.NO -0.065996 7.727538 RXI_N.M.SD 0.001158 7.832406 RXI_N.M.SF -0.369169 12.837594 RXI_N.M.SI 0.195880 1.596186 RXI_N.M.SL -0.240545 1.289438 RXI_N.M.SZ 0.286951 6.917752 RXI_N.M.TA 0.059511 1.299676 RXI_N.M.TH 0.144215 2.731771 RXI_N.M.VE -0.914809 25.526367 ## Creating currency portfolios So let's say we want to maximize our return in US dollars. The simplest approach is to select the currency that has the best expected return, and convert all our money to it. countries[list(exp_returns.index).index(exp_returns['mean'].idxmax())]  Resulting in: 'SWITZERLAND'  This tells us to invest all our money in Swiss Francs for a 0.2% expected return monthly. The data gives us a 95% confidence interval that the expected monthly return of investing in Swiss Francs is between -0.2% and 0.8%. import math n = len(exchange_rates) sz_mean = exp_returns['mean']['RXI_N.M.SZ'] sz_sd = math.sqrt(exp_returns['variance']['RXI_N.M.SZ']) sz_ci = (sz_mean-1.96*sz_sd/math.sqrt(n), sz_mean+1.We do96*sz_sd/math.sqrt(n)) print 'Monthly expected return (Swiss Francs): %0.3f%%' % sz_mean print '95%% confidence interval on expected return: (%0.3f%%, %.03f%%)' % sz_ci  That gives us our expected return and 95% confidence interval: Monthly expected return (Swiss Francs): 0.287% 95% confidence interval on expected return: (-0.229%, 0.802%)  It's important to note that this is our confidence in the expected return, not even in any particular return. That means that the expected return of our highest yielding currency could be negative or it could be positive. We don't really know. So we could end up deciding to go all in on Swiss Francs, only to discover we should expect to lose 0.2% of our money in relation to the US dollar every month. We'd like to do better, obviously. And there's another piece of information we have that might allow us to: how the different currencies relate to each other. This is the fundamental idea behind portfolio theory as introduced by Harry Markowitz in the 1950s. We can use the covariances of the returns to hedge investments against each other. Our task then becomes to select the investments to make such that maximize return while achieving a desired level of risk. # Get the covariance matrix for our returns. This is normalized by N-1. # As expected, the diagonal contains the variance for each individual currency. returns_cov = returns.cov() returns_cov.head()  RXI$US_N.M.AL RXI$US_N.M.EU RXI$US_N.M.NZ RXI$US_N.M.UK RXI_N.M.BZ RXI_N.M.CA RXI_N.M.CH RXI_N.M.DN RXI_N.M.HK RXI_N.M.IN ... RXI_N.M.MX RXI_N.M.NO RXI_N.M.SD RXI_N.M.SF RXI_N.M.SI RXI_N.M.SL RXI_N.M.SZ RXI_N.M.TA RXI_N.M.TH RXI_N.M.VE RXI$US_N.M.AL 10.996718 5.181782 8.654762 4.457704 9.789818 5.421746 0.194428 5.166875 0.028496 4.379632 ... 6.494732 7.078498 7.083824 8.574515 3.287411 0.343673 4.163674 2.450772 2.381722 0.542450
RXI$US_N.M.EU 5.181782 5.928188 4.487890 3.659548 4.686754 2.467804 0.313081 5.867674 0.038117 2.545200 ... 2.894461 5.350733 5.515164 4.314480 2.265781 0.418269 5.173168 1.559857 1.351946 1.776855 RXI$US_N.M.NZ 8.654762 4.487890 9.690793 4.330531 8.130412 4.268307 0.129303 4.466514 0.017975 3.919170 ... 5.536301 5.602586 6.054567 6.733828 2.811946 0.451742 3.903260 2.135899 2.569281 0.379954
RXI$US_N.M.UK 4.457704 3.659548 4.330531 5.098969 4.301933 2.573100 0.137068 3.632088 0.012420 1.821456 ... 2.775715 4.081803 4.233658 3.749874 1.780620 0.326828 3.335175 1.356927 1.014109 1.584577 RXI_N.M.BZ 9.789818 4.686754 8.130412 4.301933 12.743632 5.433992 0.279201 4.689891 -0.034405 5.567318 ... 7.177333 7.288341 6.774689 8.761299 2.999360 0.843294 4.269524 2.163251 2.439204 0.496304 5 rows × 23 columns The Markwitz portfolio model is given below. In it,$\mu$is the vector of the expected returns for each currency.$x$is a vector of nonnegative values that sum to 1 and represent how much of our portfolio goes into each currency. The first term in our objective function (the first line in the model) thus represents the expected return of a making a set of investments. If that were all we were doing, our model would simply tell us to invest all our money in the highest-paying currency, Swiss Francs. $$\max{\mu^T x - \alpha x \Sigma x}$$ $$e^T x = 1$$ $$x \ge 0$$ The second term is what introduces hedging.$\alpha$is a measure of aversion to risk. You can think of it like this: when$\alpha$is 0, we only invest in Swiss Francs. As$\alpha$gets larger, we introduce more hedging into our set of investments. The$\Sigma$is our covariance matrix, so$x \Sigma x$will be our portfolio variance. Thus we are looking for a good trade-off between our portfolio's expected return and its variance. This happens to be a quadratic optimization problem, so we can use the qp function of CVXOPT to solve it. Let's turn this into a ScienceOps model using CVXOPT. from cvxopt import matrix, solvers from yhat import Yhat, YhatModel, preprocess import numpy class CurrencyPortfolio(YhatModel): @preprocess(in_type=dict, out_type=dict) def execute(self, data): P = matrix(data['risk_aversion'] * returns_cov.as_matrix()) q = matrix(-exp_returns['mean'].as_matrix()) G = matrix(0.0, (len(q),len(q))) G[::len(q)+1] = -1.0 h = matrix(0.0, (len(q),1)) A = matrix(1.0, (1,len(q))) b = matrix(1.0) solution = solvers.qp(P, q, G, h, A, b) expected_return = exp_returns['mean'].dot(solution['x']) variance = sum(solution['x'] * returns_cov.as_matrix().dot(solution['x'])) investments = {} for i, amount in enumerate(solution['x']): # Ignore values that appear to have converged to 0. if amount > 10e-5: investments[countries[i]] = amount*100 return { 'risk_aversion': data['risk_aversion'], 'investments': investments, 'expected_return': expected_return, 'variance': variance }  When the model is deployed to ScienceOps, it has the expected returns, country names, and covariance matrix wrapped up in it already. All it needs in order to build a portfolio is a value for$\alpha$, which we call risk_aversion in the code. You can deploy it to ScienceOps using your USERNAME and APIKEY values and the currency-portfolio-scienceops.py script in this post's the GitHub repository. yh = Yhat('USERNAME', 'APIKEY', 'http://cloud.yhathq.com/') yh.deploy('CurrencyPortfolio', CurrencyPortfolio, globals())  Before we build a portfolio, let's step back for a minute here and take a look at our data. Recall that we are trying to maximize our expected return while minimizing our exposure to risk (variance of the return). If currency exchanges work like other markets, then expected returns farther from 0 should be accompanied by higher variance. We graph our expected returns and their variances to see if this is true. %matplotlib inline import seaborn seaborn.regplot('mean', 'variance', exp_returns) That's quite an outlier there to the left. Stopping to examine the data, we find that it's Venezuela. Their economy is not doing so well, according to the news. It's dominating our data a bit. What does the graph look like without it? exp_returns_without_ve = exp_returns[exp_returns.index != 'RXI_N.M.VE'] seaborn.regplot('mean', 'variance', exp_returns, ci=None) seaborn.regplot('mean', 'variance', exp_returns_without_ve) So without the outlier expected returns farther from 0 appear to have higher variance. That's what we expected, so let's get on with the business of building our portfolio. ## Consuming our model The great thing about deploying a model like this to ScienceOps is that nobody has to install complicated software in order to use it. It's available to anyone we want to give it to using a simple REST API. Let's build a series of portfolios, with$\alpha\$ values from 0 to 20 in increments of 0.5.

risk_aversion = [ra/2.0 for ra in range(41)]

portfolios = []
for ra in risk_aversion:
portfolios.append(yh.predict('CurrencyPortfolio', {'risk_aversion': ra}))


This gives us a an efficient frontier trading off returns and risk. Depending on our tolerance for risk, our best choice of portfolio should be somewhere along this curve.

from matplotlib import pyplot
pyplot.plot(risk_aversion, [p['result']['expected_return'] for p in portfolios], 'o-')
pyplot.axis([-1, 21, 0.0, 0.3])
pyplot.title('Efficient Frontier for Currency Portfolios')
pyplot.text(9, -0.05, 'Risk Aversion')
pyplot.text(-4, 0.18, '% Return', rotation='vertical') We can also look at the trade-off in terms of expected return and variance.

pyplot.plot(
[p['result']['expected_return'] for p in portfolios],
[p['result']['variance'] for p in portfolios],
'o-'
)
pyplot.axis([0, 0.3, -0.5, 7.5])
pyplot.title('Efficient Frontier for Currency Portfolios')
pyplot.text(.1, -2, 'Expected Return (%)')
pyplot.text(-0.025, 5, 'Portfolio Return Variance', rotation='vertical') Finally, let's output all the portfolios we just generated. These plans tell us what percentage of our total money to invest in each different currency. For instance, Portfolio 4 tells us to invest 60.55%, 37.25% and 2.2% of our total investment in the currenies of China, Hong Kong, and Thailand, respectively.

# We print out all the portfolios available to us.
from operator import itemgetter

for i, p in enumerate(portfolios):
print 'Portfolio %d: expected monthly risk_aversion=%.1f, return=%.03f%%, variance=%.03f' % (
i, p['result']['risk_aversion'], p['result']['expected_return'], p['result']['variance']
)
for country, investment in sorted(p['result']['investments'].items(), key=itemgetter(1), reverse=True):
print '\t%5.02f%% %s' % (investment, country)
print


Portfolio output:

Portfolio 0: expected monthly risk_aversion=0.0, return=0.287%, variance=6.918
100.00% SWITZERLAND

Portfolio 1: expected monthly risk_aversion=0.5, return=0.254%, variance=0.213
99.83% CHINA
0.17% NEW ZEALAND

Portfolio 2: expected monthly risk_aversion=1.0, return=0.251%, variance=0.206
97.68% CHINA
2.32% THAILAND

Portfolio 3: expected monthly risk_aversion=1.5, return=0.204%, variance=0.135
78.54% CHINA
18.90% HONG KONG
2.56% THAILAND

Portfolio 4: expected monthly risk_aversion=2.0, return=0.158%, variance=0.082
60.55% CHINA
37.25% HONG KONG
2.20% THAILAND

Portfolio 5: expected monthly risk_aversion=2.5, return=0.131%, variance=0.057
49.76% CHINA
48.26% HONG KONG
1.98% THAILAND

etc.


#### Our Products Rodeo: a native Python editor built for doing data science on your desktop. ScienceOps: deploy predictive models in production applications without IT.

Yhat (pronounced Y-hat) provides data science solutions that let data scientists deploy and integrate predictive models into applications without IT or custom coding.