package hex.glm; import hex.glm.GLMModel.Submodel; import hex.glm.GLMParams.Family; import hex.glm.GLMValidation.GLMXValidation; import water.*; import water.api.*; import water.util.RString; import water.util.UIUtils; import java.text.DecimalFormat; public class GLMModelView extends Request2 { public GLMModelView(){} public GLMModelView(GLMModel m){glm_model = m;} static final int API_WEAVER = 1; // This file has auto-gen'd doc & json fields static public DocGen.FieldDoc[] DOC_FIELDS; // Initialized from Auto-Gen code. @API(help="GLM Model Key", required=true, filter=GLMModelKeyFilter.class) Key _modelKey; @API(help="Lambda value which should be displayed as main model", required=false, filter=Default.class) double lambda = Double.NaN; class GLMModelKeyFilter extends H2OKey { public GLMModelKeyFilter() { super("",true); } } @API(help="GLM Model") GLMModel glm_model; @API(help="job key",required=false, filter=Default.class) Key job_key; public static String link(String txt, Key model) {return link(txt,model,Double.NaN);} public static String link(String txt, Key model, double lambda) { return "<a href='GLMModelView.html?_modelKey=" + model + "&lambda=" + lambda + "'>" + txt + "</a>"; } public static Response redirect(Request req, Key modelKey) { return Response.redirect(req, "/2/GLMModelView", "_modelKey", modelKey); } public static Response redirect(Request req, Key modelKey, Key job_key) { return Response.redirect(req, "/2/GLMModelView", "_modelKey", modelKey,"job_key",job_key); } @Override public boolean toHTML(StringBuilder sb){ // if(title != null && !title.isEmpty())DocGen.HTML.title(sb,title); if(glm_model == null){ sb.append("No model yet..."); return true; } glm_model.get_params().makeJsonBox(sb); DocGen.HTML.paragraph(sb,"Model Key: "+glm_model._key); if(glm_model.submodels != null) { DocGen.HTML.paragraph(sb,water.api.GLMPredict.link(glm_model._key,lambda,"Predict!")); DocGen.HTML.paragraph(sb,UIUtils.qlink(SaveModel.class, "model", glm_model._key, "Save model")); } String succ = (glm_model.warnings == null || glm_model.warnings.length == 0)?"alert-success":"alert-warning"; sb.append("<div class='alert " + succ + "'>"); pprintTime(sb.append(glm_model.iteration() + " iterations computed in "),glm_model.run_time); if(glm_model.warnings != null && glm_model.warnings.length > 0){ sb.append("<ul>"); for(String w:glm_model.warnings)sb.append("<li><b>Warning:</b>" + w + "</li>"); sb.append("</ul>"); } sb.append("</div>"); if(!Double.isNaN(lambda) && lambda != glm_model.submodels[glm_model.best_lambda_idx].lambda_value){ // show button to permanently set lambda_value to this value sb.append("<div class='alert alert-warning'>\n"); sb.append(GLMModelUpdate.link("Set lambda_value to current value!",_modelKey,lambda) + "\n"); sb.append("</div>"); } sb.append("<h4>Parameters</h4>"); parm(sb,"family",glm_model.glm.family); parm(sb,"link",glm_model.glm.link); parm(sb,"ε<sub>β</sub>",glm_model.beta_eps); parm(sb,"α",glm_model.alpha); if(!Double.isNaN(glm_model.lambda_max)) parm(sb,"λ<sub>max</sub>",DFORMAT2.format(glm_model.lambda_max)); parm(sb,"λ",DFORMAT2.format(lambda)); if(glm_model.submodels.length > 1){ sb.append("\n<table class='table table-bordered table-condensed'>\n"); StringBuilder firstRow = new StringBuilder("\t<tr><th>λ</th>\n"); StringBuilder secondRow = new StringBuilder("\t<tr><th>nonzeros</th>\n"); StringBuilder thirdRow = new StringBuilder("\t<tr><th>Deviance Explained</th>\n"); StringBuilder fourthRow = new StringBuilder("\t<tr><th>" + (glm_model.glm.family == Family.binomial?"AUC":"AIC") + "</th>\n"); for(int i = 0; i < glm_model.submodels.length; ++i){ final Submodel sm = glm_model.submodels[i]; if(sm.validation == null)break; if (glm_model.submodels[i].lambda_value == lambda) firstRow.append("\t\t<td><b>" + DFORMAT2.format(glm_model.submodels[i].lambda_value) + "</b></td>\n"); else firstRow.append("\t\t<td>" + link(DFORMAT2.format(glm_model.submodels[i].lambda_value), glm_model._key, glm_model.submodels[i].lambda_value) + "</td>\n"); secondRow.append("\t\t<td>" + Math.max(0,(sm.rank - 1)) + "</td>\n"); // rank counts intercept, that's why -1 is there, however, intercept can be 0 as well, so just prevent -1 if(sm.xvalidation != null){ thirdRow.append("\t\t<td>" + DFORMAT.format(1 - sm.xvalidation.residual_deviance / glm_model.null_validation.residualDeviance()) + "<sub>x</sub>(" + DFORMAT.format(1 - sm.validation.residual_deviance /glm_model.null_validation.residualDeviance()) + ")" + "</td>\n"); fourthRow.append("\t\t<td>" + DFORMAT.format(glm_model.glm.family == Family.binomial ? sm.xvalidation.auc : sm.xvalidation.aic) + "<sub>x</sub>("+ DFORMAT.format(glm_model.glm.family == Family.binomial ? sm.validation.auc : sm.validation.aic) + ")</td>\n"); } else { thirdRow.append("\t\t<td>" + DFORMAT.format(1 - sm.validation.residual_deviance / glm_model.null_validation.residualDeviance()) + "</td>\n"); fourthRow.append("\t\t<td>" + DFORMAT.format(glm_model.glm.family == Family.binomial ? sm.validation.auc : sm.validation.aic) + "</td>\n"); } } sb.append(firstRow.append("\t</tr>\n")); sb.append(secondRow.append("\t</tr>\n")); sb.append(thirdRow.append("\t</tr>\n")); sb.append(fourthRow.append("\t</tr>\n")); sb.append("</table>\n"); } if(glm_model.submodels.length == 0)return true; Submodel sm = glm_model.submodels[glm_model.best_lambda_idx]; if(!Double.isNaN(lambda) && glm_model.submodels[glm_model.best_lambda_idx].lambda_value != lambda){ int ii = 0; sm = glm_model.submodels[0]; while(glm_model.submodels[ii].lambda_value != lambda && ++ii < glm_model.submodels.length) sm = glm_model.submodels[ii]; if(ii == glm_model.submodels.length)throw new IllegalArgumentException("Unexpected value of lambda '" + lambda + "'"); } if(glm_model.submodels != null) coefs2html(sm,sb); if(sm.xvalidation != null) val2HTML(sm,sm.xvalidation,sb); else if(sm.validation != null) val2HTML(sm,sm.validation, sb); // Variable importance if (glm_model.varimp() != null) { glm_model.varimp().toHTML(glm_model, sb); } return true; } public void val2HTML(Submodel sm,GLMValidation val, StringBuilder sb) { String title = (val instanceof GLMXValidation)?"Cross Validation":"Validation"; sb.append("<h4>" + title + "</h4>"); sb.append("<table class='table table-striped table-bordered table-condensed'>"); final long null_dof = val.nobs-1, res_dof = Math.max(0,val.nobs-sm.rank); sb.append("<tr><th>Degrees of freedom:</th><td>" + null_dof + " total (i.e. Null); " + res_dof + " Residual</td></tr>"); sb.append("<tr><th>Null Deviance</th><td>" + glm_model.null_validation.residualDeviance() + "</td></tr>"); sb.append("<tr><th>Residual Deviance</th><td>" + val.residual_deviance + "</td></tr>"); sb.append("<tr><th>AIC</th><td>" + val.aic() + "</td></tr>"); if(glm_model.glm.family == Family.binomial)sb.append("<tr><th>AUC</th><td>" + DFORMAT.format(val.auc()) + "</td></tr>"); sb.append("</table>"); if(glm_model.glm.family == Family.binomial)new AUC(val._cms,val.thresholds,glm_model._domains[glm_model._domains.length-1]).toHTML(sb); if(val instanceof GLMXValidation){ GLMXValidation xval = (GLMXValidation)val; // add links to the xval models sb.append("<h4>Cross Validation Models</h4>"); sb.append("<table class='table table-bordered table-condensed'>"); sb.append("<tr><th>Model</th><th>nonzeros</th>"); sb.append("<th>" + ((glm_model.glm.family == Family.binomial)?"AUC":"AIC") + "</th>"); sb.append("<th>Deviance Explained</th>"); sb.append("</tr>"); int i = 0; for(Key k:xval.xval_models){ Value v = DKV.get(k); if(v == null)continue; GLMModel m = v.get(); sb.append("<tr>"); sb.append("<td>" + GLMModelView.link("Model " + ++i, k) + "</td>"); sb.append("<td>" + (m.rank()-1) + "</td>"); sb.append("<td>" + ((glm_model.glm.family == Family.binomial)?format(m.auc()):format(m.aic())) + "</td>"); sb.append("<td>" + format(m.devExplained()) + "</td>"); sb.append("</tr>"); } sb.append("</table>"); } } private static final DecimalFormat DFORMAT3 = new DecimalFormat("##.##"); private static String format(double d){ return DFORMAT3.format(0.01*(int)(100*d)); } private static void parm( StringBuilder sb, String x, Object... y ) { sb.append("<span><b>").append(x).append(": </b>").append(y[0]).append("</span> "); } private static final DecimalFormat DFORMAT = new DecimalFormat("###.###"); private static final DecimalFormat DFORMAT2 = new DecimalFormat("0.##E0"); private void coefs2html(final Submodel sm,StringBuilder sb){ StringBuilder names = new StringBuilder(); StringBuilder equation = new StringBuilder(); StringBuilder vals = new StringBuilder(); StringBuilder normVals = sm.norm_beta == null?null:new StringBuilder(); int [] sortedIds = new int[sm.beta.length]; for(int i = 0; i < sortedIds.length; ++i) sortedIds[i] = i; final double [] b = sm.norm_beta == null?sm.beta:sm.norm_beta; // now sort the indeces according to their abs value from biggest to smallest (but keep intercept last) int r = sortedIds.length-1; for(int i = 1; i < r; ++i){ for(int j = 1; j < r-i;++j){ if(Math.abs(b[sortedIds[j-1]]) < Math.abs(b[sortedIds[j]])){ int jj = sortedIds[j]; sortedIds[j] = sortedIds[j-1]; sortedIds[j-1] = jj; } } } String [] cNames = glm_model.coefficients_names; boolean first = true; int j = 0; for(int i:sortedIds){ names.append("<th>" + cNames[sm.idxs[i]] + "</th>"); vals.append("<td>" + sm.beta[i] + "</td>"); if(first){ equation.append(DFORMAT.format(sm.beta[i])); first = false; } else { equation.append(sm.beta[i] > 0?" + ":" - "); equation.append(DFORMAT.format(Math.abs(sm.beta[i]))); } if(i < (cNames.length-1)) equation.append("*x[" + cNames[i] + "]"); if(sm.norm_beta != null) normVals.append("<td>" + sm.norm_beta[i] + "</td>"); ++j; } sb.append("<h4>Equation</h4>"); RString eq = null; switch( glm_model.glm.link ) { case identity: eq = new RString("y = %equation"); break; case logit: eq = new RString("y = 1/(1 + Math.exp(-(%equation)))"); break; case log: eq = new RString("y = Math.exp((%equation)))"); break; case inverse: eq = new RString("y = 1/(%equation)"); break; case tweedie: eq = new RString("y = (%equation)^(1 - )"); break; default: eq = new RString("equation display not implemented"); break; } eq.replace("equation",equation.toString()); sb.append("<div style='width:100%;overflow:scroll;'>"); sb.append("<div><code>" + eq + "</code></div>"); sb.append("<h4>Coefficients</h4><table class='table table-bordered table-condensed'>"); sb.append("<tr>" + names.toString() + "</tr>"); sb.append("<tr>" + vals.toString() + "</tr>"); sb.append("</table>"); if(sm.norm_beta != null){ sb.append("<h4>Normalized Coefficients</h4>" + "<table class='table table-bordered table-condensed'>"); sb.append("<tr>" + names.toString() + "</tr>"); sb.append("<tr>" + normVals.toString() + "</tr>"); sb.append("</table>"); } sb.append("</div>"); } private void pprintTime(StringBuilder sb, long t){ long hrs = t / (1000*60*60); long minutes = (t -= 1000*60*60*hrs)/(1000*60); long seconds = (t -= 1000*60*minutes)/1000; t -= 1000*seconds; if(hrs > 0)sb.append(hrs + "hrs "); if(hrs > 0 || minutes > 0)sb.append(minutes + "min "); if(hrs > 0 || minutes > 0 | seconds > 0)sb.append(seconds + "sec "); sb.append(t + "msec"); } // Job jjob = null; // if( job_key != null ) // jjob = Job.findJob(job_key); // if( jjob != null && jjob.exception != null ) // return Response.error(jjob.exception == null ? "cancelled" : jjob.exception); // if( jjob == null || jjob.end_time > 0 || jjob.cancelled() ) // return jobDone(jjob, destination_key); // return jobInProgress(jjob, destination_key); @Override protected Response serve() { Job jjob = ( job_key != null )?Job.findJob(job_key):null; if( jjob != null && jjob.exception != null ) return Response.error(jjob.exception == null ? "cancelled" : jjob.exception); Value v = DKV.get(_modelKey); if(v != null){ glm_model = v.get(); if(Double.isNaN(lambda) && glm_model.submodels.length != 0) lambda = glm_model.submodels[glm_model.best_lambda_idx].lambda_value; } if( jjob == null || jjob.end_time > 0 || jjob.isCancelledOrCrashed() ) return Response.done(this); return Response.poll(this,(int)(100*jjob.progress()),100,"_modelKey",_modelKey.toString()); } // @Override protected Response serve() { // Value v = DKV.get(_modelKey); // if(v == null) // return Response.poll(this, 0, 100, "_modelKey", _modelKey.toString()); // glm_model = v.get(); // if(Double.isNaN(lambda_value))lambda_value = glm_model.lambdas[glm_model.best_lambda_idx]; // Job j; // if((j = Job.findJob(glm_model.job_key)) != null && j.exception != null) // return Response.error(j.exception); // if(DKV.get(glm_model.job_key) != null && j != null) // return Response.poll(this, (int) (100 * j.progress()), 100, "_modelKey", _modelKey.toString()); // else // return Response.done(this); // } }