Note
Click here to download the full example code
Reconcile Forecasts
This tutorial explains how use the
ReconcileAdditiveForecasts
class to create forecasts that satisfy inter-forecast additivity constraints.
The inputs are:
additive constraints to be satisfied
original (base) forecasts (timeseries)
actuals (timeseries)
The output is adjusted forecasts that satisfy the constraints.
Optimization Approach
The adjusted forecasts are computed as a linear transformation of the base forecasts. The linear transform is the solution to an optimization problem (Reconcile Forecasts).
In brief, the objective is to minimize the weighted sum of these error terms:
Training MSE
: empirical MSE of the adjusted forecasts on the training setBias penalty
: estimated squared bias of adjusted forecast errorsVariance penalty
: estimated variance of adjusted forecast errors for an unbiased transformation, assuming base forecasts are unbiased (this underestimates the variance if the transformation is biased).Adjustment penalty
: regularization term that penalizes large adjustments
Subject to these constraints:
Adjusted forecasts satisfy inter-forecast additivity constraints (required)
Transform is unbiased (optional)
Transform matrix entries are between [lower, upper] bound (optional)
ReconcileAdditiveForecasts
allows you to tune the optimization objective and constraints.
It also exposes common methods as special cases of this optimization problem.
The available methods are:
"bottom_up"
(bottom up)"ols"
(OLS)"mint_sample"
(MinT with sample covariance)"custom"
(custom objective and constraints)
Note
"bottom_up"
is applicable when the constraints can be represented as a tree.
It produces reconciled forecasts by summing the leaf nodes. This is equivalent to the
solution to the optimization that only penalizes adjustment to the leaf nodes’ forecasts.
"ols"
and "mint_sample"
include only the variance penalty and require
that the transform be unbiased. The variance penalty depends on forecast error covariances.
"ols"
assumes base forecast errors are uncorrelated with equal variance.
"mint_sample"
uses sample covariance of the forecast errors.
Prepare Input Data
In this tutorial, we consider a 3-level tree with the parent-child relationships below.
00 # level 0
/ \
10 11 # level 1
/ | \ /\
20 21 22 23 24 # level 2
We want the forecasts of parent nodes to equal the sum of the forecasts of their children.
First, we need to generate forecasts for each of the nodes.
One approach is to generate the forecasts independently, using rolling window
forecasting to get h-step ahead forecasts over time, for some constant h
.
This can be done with the benchmark class.
(The variance penalty assumes the residuals have fixed covariance,
and using constant h
helps with that assumption.)
For this tutorial, we assume that forecasts have already been computed.
Below, forecasts
and actuals
are pandas DataFrames in long format, where each column
is a time series, and each row is a time step. The rows are sorted in ascending order.
86 import logging
87 import plotly
88 import warnings
89
90 import pandas as pd
91 import numpy as np
92
93 from greykite.algo.reconcile.convex.reconcile_forecasts import ReconcileAdditiveForecasts
94 from greykite.common.constants import TIME_COL
95 from greykite.common.data_loader import DataLoader
96 from greykite.common.viz.timeseries_plotting import plot_multivariate
97
98 logger = logging.getLogger()
99 logger.setLevel(logging.ERROR) # reduces logging
100 warnings.simplefilter("ignore", category=UserWarning) # ignores matplotlib warnings when rendering documentation
101
102 dl = DataLoader()
103 actuals = dl.load_data(data_name="daily_hierarchical_actuals")
104 forecasts = dl.load_data(data_name="daily_hierarchical_forecasts")
105 actuals.set_index(TIME_COL, inplace=True)
106 forecasts.set_index(TIME_COL, inplace=True)
107 forecasts.head().round(1)
Note
To use the reconcile method, dataframe columns should contain only the forecasts or actuals timeseries. Time should not be its own column.
Above, we set time as the index using .set_index()
.
Index values are ignored by the reconcile method
so you could also choose to drop the column.
The rows and columns in forecasts and actuals correspond to each other.
122 assert forecasts.index.equals(actuals.index)
123 assert forecasts.columns.equals(actuals.columns)
Next, we need to encode the constraints.
In general, these can be defined by constraint_matrix
.
This is a c x m
array encoding c
constraints in m
variables,
where m
is the number of timeseries. The columns in this matrix
correspond to the columns in the forecasts/actuals dataframes below.
The rows encode additive expressions that must equal 0.
132 constraint_matrix = np.array([
133 # 00 10 11 20 21 22 23 24
134 [-1, 1, 1, 0, 0, 0, 0, 0], # 0 = -1*x_00 + 1*x_10 + 1*x_11
135 [ 0, -1, 0, 1, 1, 1, 0, 0], # 0 = -1*x_10 + 1*x_20 + 1*x_21 + 1*x_22
136 [ 0, 0, -1, 0, 0, 0, 1, 1] # 0 = -1*x_11 + 1*x_23 + 1*x_24
137 ])
Alternatively, if the graph is a tree, you can use the levels
parameter. This
is a more concise way to specify additive tree # constraints, where forecasts of
parent nodes must equal the sum of the forecasts of their children. It assumes
the columns in forecasts
and actuals
are in the tree’s breadth first
traversal order: i.e., starting from the root, scan left to right,
top to bottom, as shown below for our example:
0
/ \
1 2
/ | \ / \
3 4 5 6 7
Here is an equivalent specification using the levels
parameter.
157 # The root has two children.
158 # Its children have 3 and 2 children, respectively.
159 levels = [[2], [3, 2]]
160 # Summarize non-leaf nodes by the number of children
161 # they have, and iterate in breadth first traversal order.
162 # Each level in the tree becomes a sublist of `levels`.
163 #
164 # (2) --> [2]
165 # / \
166 # (3) (2) --> [3, 2]
Note
More formally, levels
specifies the number of children of each
internal node in the tree. The ith inner list provides the number
of children of each node in level i. Thus, the first sublist has one
integer, the length of a sublist is the sum of the previous sublist,
and all entries in levels
are positive integers.
All leaf nodes must have the same depth.
For illustration, we plot the inconsistency between forecasts
of the root node, "00"
, and its children.
Notice that the blue and orange lines do not perfectly overlap.
182 parent = "00"
183 children = ["10", "11"]
184 cols = {
185 f"parent-{parent}": forecasts[parent],
186 "sum(children)": sum(forecasts[child] for child in children)
187 }
188 cols.update({f"child-{child}": forecasts[child] for child in children})
189 cols[TIME_COL] = forecasts.index
190 parent_child_df = pd.DataFrame(cols)
191 fig = plot_multivariate(
192 df=parent_child_df,
193 x_col=TIME_COL,
194 title=f"Forecasts of node '{parent}' and its children violate the constraint",
195 )
196 plotly.io.show(fig)
Forecast reconciliation
Training Evaluation
To reconcile these forecasts, we use the
ReconcileAdditiveForecasts
class.
206 raf = ReconcileAdditiveForecasts()
Fit
Call fit()
to learn the linear transform.
Available methods are "bottom_up"
, "ols"
,
"mint_sample"
, "custom"
.
Let’s start with the bottom up method.
Each row in the transform matrix shows how to compute the adjusted forecast as a linear combination of the base forecasts. For the “bottom up” transform, the matrix simply reflects the tree structure.
Out:
array([[0., 0., 0., 1., 1., 1., 1., 1.],
[0., 0., 0., 1., 1., 1., 0., 0.],
[0., 0., 0., 0., 0., 0., 1., 1.],
[0., 0., 0., 1., 0., 0., 0., 0.],
[0., 0., 0., 0., 1., 0., 0., 0.],
[0., 0., 0., 0., 0., 1., 0., 0.],
[0., 0., 0., 0., 0., 0., 1., 0.],
[0., 0., 0., 0., 0., 0., 0., 1.]])
We can visualize this matrix to more easily see how forecasts are combined. The top row in this plot shows that the adjusted forecast for node “00” (tree root) is the sum of all the base forecasts of the leaf nodes. “10” and “11” are the sum of their children, and each leaf node keeps its original value.
233 fig = raf.plot_transform_matrix()
234 plotly.io.show(fig)
Transform
The transform()
method applies the transform and returns the adjusted (consistent) forecasts.
If we call it without arguments, it applies the transform to the training set.
241 adjusted_forecasts = raf.transform()
242 adjusted_forecasts.head().round(1)
The adjusted forecasts on the training set are stored in the adjusted_forecasts
attribute.
246 assert adjusted_forecasts.equals(raf.adjusted_forecasts)
Evaluate
Now that we have the actuals, forecasts, and adjusted forecasts, we can check how the adjustment affects forecast quality. Here, we do evaluation on the training set.
254 _ = raf.evaluate(
255 is_train=True, # evaluates on training set
256 ipython_display=True, # displays evaluation table
257 plot=True, # displays plots
258 plot_num_cols=2, # formats plots into two columns
259 )
Out:
MAPE pp change MedAPE pp change RMSE % change ... Adjusted MedAPE Base RMSE Adjusted RMSE
00 0.4 0.2 15.4 ... 2.8 17.8 20.5
10 -0.1 -0.0 -4.3 ... 1.1 3.7 3.5
11 0.6 0.7 7.4 ... 6.0 20.7 22.2
20 0.0 0.0 0.0 ... 7.4 7.2 7.2
21 0.0 0.0 0.0 ... 4.6 5.0 5.0
22 0.0 0.0 0.0 ... 2.5 4.5 4.5
23 0.0 0.0 0.0 ... 13.4 23.5 23.5
24 0.0 0.0 0.0 ... 0.8 1.7 1.7
[8 rows x 9 columns]
For better formatting in this documentation, let’s display the
table again. evaluation_df
contains the
evaluation table for the training set. The errors for
the leaf nodes are the same, as expected, because their
forecasts have not changed.
The error for nodes “00” and “11” have increased.
268 raf.evaluation_df.round(1)
Note
The ipython_display
parameter controls whether to display the evaluation table.
The “*change” columns show the change in error after adjustment.
The “Base*” columns show evaluation metrics for the original base forecasts.
The “Adjusted*” columns show evaluation metrics for the adjusted forecasts.
MAPE/MedAPE = mean/median absolute percentage error, RMSE = root mean squared error, pp = percentage point.
We can check the diagnostic plots for more information.
The “Base vs Adjusted” and “Adjustment Size” plots show that
the forecasts for “00” and “11” are higher after adjustment.
The “Forecast Error” plot shows that this increased the forecast error.
(Plots are automatically shown when plot=True
.
To make plots appear inline in this tutorial, we need
to explicitly show the figures.)
289 plotly.io.show(raf.figures["base_adj"])
292 plotly.io.show(raf.figures["adj_size"])
295 plotly.io.show(raf.figures["error"])
Note
The plot
parameter controls whether to display
diagnostic plots to adjusted to base forecasts.
“Base vs Adjusted Forecast” shows base forecast (blue) vs adjusted forecast (orange)
“Adjustment Size (%)” shows the size of the adjustment.
“Forecast Error (%)” shows the % error before (blue) and after (orange) adjustment. Closer to 0 is better.
Note that the y-axes are independent.
For completeness, we can verify that the actuals
and adjusted forecasts satisfy the constraints.
constraint_violation
shows constraint violation on the training set,
defined as root mean squared violation
(averaged across time points and constraints),
divided by root mean squared actual value.
It should be close to 0 for “adjusted” and “actual”.
(This is not necessary to check, because
a warning is printed during fitting if actuals do not satisfy the constraints
or if there is no solution to the optimization problem.)
Out:
{'adjusted': 1.2097724692944673e-16, 'forecast': 0.03741571732039168, 'actual': 2.0074667673074238e-16}
Test Set Evaluation
Evaluation on the training set is sufficient for the "bottom_up"
and "ols"
methods, because they do not use the forecasts or actuals
to learn the transform matrix. The transform depends only on the constraints.
The "mint_sample"
and "custom"
methods use forecasts and actuals
in addition to the constraints, so we should evaluate accuracy
on an out-of-sample test set.
constraints |
forecasts |
actuals |
|
---|---|---|---|
|
X |
||
|
X |
||
|
X |
X |
X |
|
X |
X |
X |
"custom"
always uses the constraints. Whether it uses forecasts
and actuals depends on the optimization terms:
forecasts
: used for adjustment penalty, train penalty, variance penalty with “sample” covariance, preset weight options (“MedAPE”, “InverseMedAPE”).actuals
: used for bias penalty, train penalty, variance penalty with “sample” covariance, preset weight options (“MedAPE”, “InverseMedAPE”).
Train
We’ll fit to the first half of the data and evaluate accuracy on the second half.
355 train_size = forecasts.shape[0]//2
356 forecasts_train = forecasts.iloc[:train_size,:]
357 actuals_train = actuals.iloc[:train_size,:]
358 forecasts_test = forecasts.iloc[train_size:,:]
359 actuals_test = actuals.iloc[train_size:,:]
Let’s try the "mint_sample"
method.
First, fit the transform and apply it on the training set.
The transform matrix is more complex than before.
365 raf = ReconcileAdditiveForecasts()
366 raf.fit_transform( # fits and transforms the training data
367 forecasts=forecasts_train,
368 actuals=actuals_train,
369 levels=levels,
370 method="mint_sample"
371 )
372 assert raf.transform_matrix is not None # train fit result, set by fit
373 assert raf.adjusted_forecasts is not None # train transform result, set by transform
374 fig = raf.plot_transform_matrix()
375 plotly.io.show(fig)
Now, evaluate accuracy on the training set. In our example, all the reconciled forecasts have lower error than the base forecasts on the training set.
381 raf.evaluate(is_train=True)
382 assert raf.evaluation_df is not None # train evaluation result, set by evaluate
383 assert raf.figures is not None # train evaluation figures, set by evaluate
384 assert raf.constraint_violation is not None # train constraint violation, set by evaluate
385 raf.evaluation_df.round(1)
Test
Next, apply the transform to the test set and evaluate accuracy. Not all forecasts have improved on the test set. This demonstrates the importance of test set evaluation.
393 raf.transform_evaluate( # transform and evaluates on test data
394 forecasts_test=forecasts_test,
395 actuals_test=actuals_test,
396 ipython_display=False,
397 plot=False,
398 )
399 assert raf.adjusted_forecasts_test is not None # test transform result, set by transform
400 assert raf.evaluation_df_test is not None # test evaluation result, set by evaluate
401 assert raf.figures_test is not None # test evaluation figures, set by evaluate
402 assert raf.constraint_violation_test is not None # test constraint violation, set by evaluate
403 raf.evaluation_df_test.round(1)
Note
The results for the test set are in the
corresponding attributes ending with "_test"
.
As a summary, here are some key attributes containing the results:
transform_matrix : transform learned from train set
adjusted_forecasts : adjusted forecasts on train set
adjusted_forecasts_test : adjusted forecasts on test set
evaluation_df : evaluation result on train set
evaluation_df_test : evaluation result on test set
constraint_violation : normalized constraint violations on train set
constraint_violation_test : normalized constraint violations on test set
figures : evaluation plots on train set
figures_test : evaluation plots on test set
For full attribute details, see
ReconcileAdditiveForecasts
.
Model Tuning
Now that you understand the basic usage, we’ll introduce some tuning parameters. If you have enough holdout data, you can use the out of sample evaluation to tune the model.
First, try the presets for the method
parameter:
"bottom_up"
, "ols"
, "mint_sample"
, "custom"
.
If you’d like to tune further, use the "custom"
method to tune
the optimization objective and constraints.
The tuning parameters and their default values are shown below.
See ReconcileAdditiveForecasts
for details.
442 raf = ReconcileAdditiveForecasts()
443 _ = raf.fit_transform_evaluate( # fits, transforms, and evaluates on training data
444 forecasts=forecasts_train,
445 actuals=actuals_train,
446 fit_kwargs=dict( # additional parameters passed to fit()
447 levels=levels,
448 method="custom",
449 # tuning parameters, with their default values for the custom method
450 lower_bound=None, # Lower bound on each entry of ``transform_matrix``.
451 upper_bound=None, # Upper bound on each entry of ``transform_matrix``.
452 unbiased=True, # Whether the resulting transformation must be unbiased.
453 lam_adj=1.0, # Weight for the adjustment penalty (adj forecast - forecast)
454 lam_bias=1.0, # Weight for the bias penalty (adj actual - actual).
455 lam_train=1.0, # Weight for the training MSE penalty (adj forecast - actual)
456 lam_var=1.0, # Weight for the variance penalty (variance of adjusted forecast errors for an unbiased transformation, assuming base forecasts are unbiased)
457 covariance="sample", # Variance-covariance matrix of base forecast errors, used to compute the variance penalty ("sample", "identity" or numpy array)
458 weight_adj=None, # Weight for the adjustment penalty to put a different weight per-timeseries.
459 weight_bias=None, # Weight for the bias penalty to put a different weight per-timeseries.
460 weight_train=None, # Weight for the train MSE penalty to put a different weight per-timeseries.
461 weight_var=None, # Weight for the variance penalty to put a different weight per-timeseries.
462 ),
463 evaluate_kwargs=dict() # additional parameters passed to evaluate()
464 )
Using "custom"
with default settings,
we find good training set performance overall.
469 raf.evaluation_df.round(1)
Test set performance is also good, except for node “24”.
473 raf.transform_evaluate(
474 forecasts_test=forecasts_test,
475 actuals_test=actuals_test,
476 ipython_display=False,
477 plot=False
478 )
479 raf.evaluation_df_test.round(1)
Notice from the tables that node “24” had the most accurate base forecast of all nodes. Therefore, we don’t want its adjusted forecast to change much. It’s possible that the above transform was overfitting this node.
We can increase the adjustment penalty for node “24” so that its adjusted forecast will be closer to the original one. This should allow us to get good forecasts overall and for node “24” specifically.
492 # the order of `weights` corresponds to `forecasts.columns`
493 weight = np.array([1, 1, 1, 1, 1, 1, 1, 5]) # weight is 5x higher for node "24"
494 raf = ReconcileAdditiveForecasts()
495 _ = raf.fit_transform_evaluate(
496 forecasts=forecasts_train,
497 actuals=actuals_train,
498 fit_kwargs=dict(
499 levels=levels,
500 method="custom",
501 lower_bound=None,
502 upper_bound=None,
503 unbiased=True,
504 lam_adj=1.0,
505 lam_bias=1.0,
506 lam_train=1.0,
507 lam_var=1.0,
508 covariance="sample",
509 weight_adj=weight, # apply the weights to adjustment penalty
510 weight_bias=None,
511 weight_train=None,
512 weight_var=None,
513 )
514 )
Note
The default weight=None
puts equal weight on all nodes.
Weight can also be "MedAPE"
(proportional to MedAPE
of base forecasts), "InverseMedAPE"
(proportional to 1/MedAPE
of base forecasts), or a numpy array that specifies the weight
for each node.
Note
When the transform is unbiased (unbiased=True
),
the bias penalty is zero, so lam_bias
and
weight_bias
have no effect.
The training error looks good.
533 raf.evaluation_df.round(1)
Plots of the transform matrix and adjustment size show that node “24“‘s adjusted forecast is almost the same as its base forecast.
539 fig = raf.plot_transform_matrix()
540 plotly.io.show(fig)
543 plotly.io.show(raf.figures["adj_size"])
The test error looks better than before.
548 # Transform and evaluate on the test set.
549 raf.transform_evaluate(
550 forecasts_test=forecasts_test,
551 actuals_test=actuals_test,
552 ipython_display=False,
553 plot=True,
554 plot_num_cols=2,
555 )
556 raf.evaluation_df_test.round(1)
559 plotly.io.show(raf.figures_test["base_adj"])
562 plotly.io.show(raf.figures_test["adj_size"])
565 plotly.io.show(raf.figures_test["error"])
Tuning Tips
If you have enough data, you can use cross validation with multiple test sets for a better estimate of test error. You can use test error to select the parameters.
To tune the parameters,
Try all four methods.
Tune the lambdas and the weights for the custom method.
For example, start with these lambda settings to see which penalties are useful:
583 lambdas = [
584 # lam_adj, lam_bias, lam_train, lam_var
585 (0, 0, 0, 1), # the same as "mint_sample" if other params are set to default values.
586 (0, 0, 1, 1),
587 (1, 0, 0, 1),
588 (1, 0, 1, 1), # the same as "custom" if other params are set to default values.
589 (1, 1, 1, 1), # try this one with unbiased=False
590 ]
Tips:
var
penalty is usually helpfultrain
,adj
,bias
penalties are sometimes helpfulYou can increase the lambda for penalties that are more helpful.
To try a biased transform, set (unbiased=False, lam_bias>0)
.
Avoid (unbiased=False, lam_bias=0)
, because that can result in high bias.
Choose weights that fit your needs. For example, you may care about the accuracy of some forecasts more than others.
Setting weight_adj
to "InverseMedAPE"
is a convenient way to
penalize adjustment to base forecasts that are already accurate.
Setting weight_bias
, weight_train
, or weight_var
to "MedAPE"
is a convenient way to improve the error
on base forecasts that start with high error.
Debugging
Some tips if you need to debug:
1. Make sure the constraints are properly encoded (for the bottom up method, another way is to check the transform matrix).
Out:
array([[-1., 0., 0., 1., 1., 1., 1., 1.],
[ 0., -1., 0., 1., 1., 1., 0., 0.],
[ 0., 0., -1., 0., 0., 0., 1., 1.]])
The constraint violation should be 0 for the actuals.
Out:
{'adjusted': 1.874446823677435e-15, 'forecast': 0.04015962097753542, 'actual': 1.9465566690613992e-16}
Check the transform matrix to understand predictions.
fig = raf.plot_transform_matrix()
plotly.io.show(fig)
4. For all methods besides “bottom_up”, check if a solution was found to the optimization problem.
If False, then the transform_matrix
may be set to a fallback option (bottom up transform, if available).
A warning is printed when this happens (“Failed to find a solution. Falling back to bottom-up method.”).
Out:
True
5. Check prob.status
for details about cvxpy solver status
and look for printed warnings for any issues. You can pass solver options
to the fit
method. See
ReconcileAdditiveForecasts
for details.
648 raf.prob.status
Out:
'optimal'
6. Inspect the objective function value at the identified solution and its breakdown into components. This shows the terms in the objective after multiplication by the lambdas/weights.
Out:
{'adj': 0.0003175260797858461, 'bias': 1.9020021907581692e-30, 'train': 0.0037819550216529265, 'var': 0.0037819550216529143, 'total': 0.007881436123091686}
7. Check objective function weights, to make sure covariance, etc., match expectations.
Out:
{'weight_adj': array([[0.5, 0. , 0. , 0. , 0. , 0. , 0. , 0. ],
[0. , 0.5, 0. , 0. , 0. , 0. , 0. , 0. ],
[0. , 0. , 0.5, 0. , 0. , 0. , 0. , 0. ],
[0. , 0. , 0. , 0.5, 0. , 0. , 0. , 0. ],
[0. , 0. , 0. , 0. , 0.5, 0. , 0. , 0. ],
[0. , 0. , 0. , 0. , 0. , 0.5, 0. , 0. ],
[0. , 0. , 0. , 0. , 0. , 0. , 0.5, 0. ],
[0. , 0. , 0. , 0. , 0. , 0. , 0. , 2.5]]), 'weight_bias': array([[1., 0., 0., 0., 0., 0., 0., 0.],
[0., 1., 0., 0., 0., 0., 0., 0.],
[0., 0., 1., 0., 0., 0., 0., 0.],
[0., 0., 0., 1., 0., 0., 0., 0.],
[0., 0., 0., 0., 1., 0., 0., 0.],
[0., 0., 0., 0., 0., 1., 0., 0.],
[0., 0., 0., 0., 0., 0., 1., 0.],
[0., 0., 0., 0., 0., 0., 0., 1.]]), 'weight_train': array([[1., 0., 0., 0., 0., 0., 0., 0.],
[0., 1., 0., 0., 0., 0., 0., 0.],
[0., 0., 1., 0., 0., 0., 0., 0.],
[0., 0., 0., 1., 0., 0., 0., 0.],
[0., 0., 0., 0., 1., 0., 0., 0.],
[0., 0., 0., 0., 0., 1., 0., 0.],
[0., 0., 0., 0., 0., 0., 1., 0.],
[0., 0., 0., 0., 0., 0., 0., 1.]]), 'weight_var': array([[1., 0., 0., 0., 0., 0., 0., 0.],
[0., 1., 0., 0., 0., 0., 0., 0.],
[0., 0., 1., 0., 0., 0., 0., 0.],
[0., 0., 0., 1., 0., 0., 0., 0.],
[0., 0., 0., 0., 1., 0., 0., 0.],
[0., 0., 0., 0., 0., 1., 0., 0.],
[0., 0., 0., 0., 0., 0., 1., 0.],
[0., 0., 0., 0., 0., 0., 0., 1.]]), 'covariance': array([[ 0.00846011, -0.00054813, 0.00880111, 0.00252624, -0.00150595,
-0.00141349, 0.00878735, -0.00038106],
[-0.00054813, 0.00036918, -0.0013657 , -0.00042228, 0.00038015,
0.0003675 , -0.0016771 , 0.00015219],
[ 0.00880111, -0.0013657 , 0.0135727 , 0.00320355, -0.00189314,
-0.00254459, 0.01486313, -0.00096638],
[ 0.00252624, -0.00042228, 0.00320355, 0.00182758, -0.00092728,
-0.00099977, 0.00344105, -0.00023999],
[-0.00150595, 0.00038015, -0.00189314, -0.00092728, 0.00073333,
0.00052669, -0.00206424, 0.00015938],
[-0.00141349, 0.0003675 , -0.00254459, -0.00099977, 0.00052669,
0.00070826, -0.0028976 , 0.00022745],
[ 0.00878735, -0.0016771 , 0.01486313, 0.00344105, -0.00206424,
-0.0028976 , 0.0167878 , -0.00114732],
[-0.00038106, 0.00015219, -0.00096638, -0.00023999, 0.00015938,
0.00022745, -0.00114732, 0.00010755]])}
Check the convex optimization problem.
663 print(type(raf.prob))
664 raf.prob
Out:
<class 'cvxpy.problems.problem.Problem'>
Problem(Minimize(Expression(CONVEX, NONNEGATIVE, ())), [Equality(Expression(AFFINE, UNKNOWN, (3, 8)), Constant(CONSTANT, ZERO, ())), Equality(Expression(AFFINE, UNKNOWN, (8, 5)), Constant(CONSTANT, NONNEGATIVE, (8, 5)))])